Geometry Lookup
Runnable three.js example: Geometry Lookup. Open the live demo, read the complete source, and copy the file into your project. No build step or API key.
single HTML file · CDN dependencies onlyinterface may contain Chinese labels
Source
<!--
说明:把色带做成一维查找表纹理,着色器按海拔比例采样,改色带不用重写顶点数据
核心逻辑:DataTexture 存 256 级颜色,自定义 ShaderMaterial 用属性 aT 采样
依赖:three.js 0.160.0、OrbitControls · jsDelivr CDN(ESM + importmap)
-->
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta name="robots" content="noindex,follow">
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>高程色带查找</title>
<style>
html,body{height:100%;margin:0;background:#0d1117}
body{display:flex;flex-direction:column;overflow:hidden;
font:13px/1.6 -apple-system,"Segoe UI","PingFang SC",sans-serif;color:#e6e8ec}
.bar{flex:0 0 auto;padding:8px 12px;background:#161b22;border-bottom:1px solid #252c36;
display:flex;gap:14px;align-items:center;font-size:12px;color:#8b93a1;flex-wrap:wrap}
.bar b{color:#58a6ff;font-weight:600}
.bar button{font:inherit;font-size:12px;padding:4px 10px;border-radius:5px;cursor:pointer;
border:1px solid #303845;background:#1d2531;color:#c8d2e0}
.bar button:hover{background:#253040}
.bar button.on{background:#1f3a5c;border-color:#2f5c8f;color:#cfe4ff}
.bar .sp{flex:1 1 auto}
#stage{flex:1 1 auto;min-height:0;position:relative}
#stage canvas{display:block}
</style>
</head>
<body>
<div class="bar">
<span>色带 <b id="rampName">地形</b></span>
<button id="r1" class="on">地形</button>
<button id="r2">热力</button>
<button id="r3">灰阶</button>
<span class="sp"></span>
<span>拖拽旋转 · 滚轮缩放</span>
</div>
<div id="stage"></div>
<script type="importmap">
{
"imports": {
"three": "https://cdn.jsdelivr.net/npm/three@0.160.0/build/three.module.js",
"three/addons/": "https://cdn.jsdelivr.net/npm/three@0.160.0/examples/jsm/"
}
}
</script>
<script type="module">
import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
const stage = document.getElementById('stage');
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
renderer.setSize(stage.clientWidth, stage.clientHeight);
renderer.outputColorSpace = THREE.SRGBColorSpace;
stage.appendChild(renderer.domElement);
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x0d1117);
const camera = new THREE.PerspectiveCamera(45, stage.clientWidth / stage.clientHeight, 0.1, 300);
camera.position.set(18, 15, 22);
const controls = new OrbitControls(camera, renderer.domElement);
controls.target.set(0, 2, 0);
controls.enableDamping = true;
controls.maxPolarAngle = Math.PI * 0.49;
controls.update();
const SIZE = 32;
const SEG = 110;
function heightAt(x, z) {
const a = 1.5 * Math.sin(x * 0.34) * Math.cos(z * 0.3);
const b = 0.8 * Math.sin(x * 0.85 + z * 0.6);
const c = 4.0 * Math.exp(-((x - 2) * (x - 2) + (z + 3) * (z + 3)) / 28);
return a + b + c;
}
const geo = new THREE.PlaneGeometry(SIZE, SIZE, SEG, SEG);
geo.rotateX(-Math.PI / 2);
const pos = geo.attributes.position;
const tAttr = new Float32Array(pos.count);
let minH = Infinity;
let maxH = -Infinity;
for (let i = 0; i < pos.count; i++) {
const h = heightAt(pos.getX(i), pos.getZ(i));
pos.setY(i, h);
minH = Math.min(minH, h);
maxH = Math.max(maxH, h);
}
for (let i = 0; i < pos.count; i++) {
tAttr[i] = (pos.getY(i) - minH) / (maxH - minH);
}
geo.setAttribute('aT', new THREE.BufferAttribute(tAttr, 1));
geo.computeVertexNormals();
/* 256 × 1 的查找表纹理,切色带时只重写这 1 KB 数据 */
const LUT_W = 256;
const lutData = new Uint8Array(LUT_W * 4);
const lut = new THREE.DataTexture(lutData, LUT_W, 1, THREE.RGBAFormat);
lut.colorSpace = THREE.SRGBColorSpace;
lut.minFilter = THREE.LinearFilter;
lut.magFilter = THREE.LinearFilter;
lut.needsUpdate = true;
const RAMPS = {
terrain: ['#12324f', '#2c7a86', '#8fbf8a', '#d8d08a', '#a9764f', '#b9b6b0', '#ffffff'],
heat: ['#1b0c3a', '#5a1a8a', '#c2185b', '#ff6d00', '#ffd400', '#fff9c4'],
gray: ['#101418', '#303a44', '#6a7885', '#aab4bd', '#eef2f5']
};
function rebuildLut(name) {
const stops = RAMPS[name].map(function (h) { return new THREE.Color(h); });
for (let i = 0; i < LUT_W; i++) {
const s = (i / (LUT_W - 1)) * (stops.length - 1);
const a = Math.floor(s);
const f = s - a;
const c = stops[a].clone().lerp(stops[Math.min(a + 1, stops.length - 1)], f);
lutData[i * 4] = Math.round(c.r * 255);
lutData[i * 4 + 1] = Math.round(c.g * 255);
lutData[i * 4 + 2] = Math.round(c.b * 255);
lutData[i * 4 + 3] = 255;
}
lut.needsUpdate = true;
}
rebuildLut('terrain');
const mat = new THREE.ShaderMaterial({
uniforms: {
uLut: { value: lut },
uLightDir: { value: new THREE.Vector3(-0.5, 0.8, 0.35).normalize() }
},
vertexShader: `
attribute float aT;
varying float vT;
varying vec3 vN;
void main() {
vT = aT;
vN = normalize(normalMatrix * normal);
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
`,
fragmentShader: `
uniform sampler2D uLut;
uniform vec3 uLightDir;
varying float vT;
varying vec3 vN;
void main() {
vec3 base = texture2D(uLut, vec2(vT * 0.996 + 0.002, 0.5)).rgb;
float lam = max(dot(normalize(vN), normalize(uLightDir)), 0.0);
// 半兰伯特,背光面留一点底色,避免整片死黑
float light = 0.35 + 0.75 * lam;
gl_FragColor = vec4(base * light, 1.0);
}
`
});
const terrain = new THREE.Mesh(geo, mat);
scene.add(terrain);
const base = new THREE.Mesh(
new THREE.BoxGeometry(SIZE, 1.2, SIZE),
new THREE.MeshBasicMaterial({ color: 0x0f151c })
);
base.position.y = minH - 0.6;
scene.add(base);
const rampName = document.getElementById('rampName');
const buttons = { r1: 'terrain', r2: 'heat', r3: 'gray' };
const LABEL = { terrain: '地形', heat: '热力', gray: '灰阶' };
Object.keys(buttons).forEach(function (id) {
document.getElementById(id).addEventListener('click', function () {
Object.keys(buttons).forEach(function (k) {
document.getElementById(k).classList.toggle('on', k === id);
});
rebuildLut(buttons[id]);
rampName.textContent = LABEL[buttons[id]];
});
});
renderer.setAnimationLoop(function () {
controls.update();
renderer.render(scene, camera);
});
window.addEventListener('resize', function () {
const w = stage.clientWidth;
const h = stage.clientHeight;
camera.aspect = w / h;
camera.updateProjectionMatrix();
renderer.setSize(w, h);
});
</script>
</body>
</html>
Language: the code and this page are in English; the full tutorial and API notes for this example are in Chinese. The demo itself is the authoritative version — it runs exactly as shown.