Atlaslog
Atlaslog / Examples / three.js / Geometry Lookup
three.js · Runnable Example

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.

Open standalone → Chinese notes All three.js examples
/demos/three-geometry-lookup.htmlLIVE
single HTML file · CDN dependencies onlyinterface may contain Chinese labels

Source

demos/three-geometry-lookup.html
<!--
  说明:把色带做成一维查找表纹理,着色器按海拔比例采样,改色带不用重写顶点数据
  核心逻辑: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.

More three.js examples

three.js · EXAMPLEGeometry Shapes three.js · EXAMPLEGeometry Splines three.js · EXAMPLEGlobe