Geometry Dynamic
Runnable three.js example: Geometry Dynamic. 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
<!--
说明:每帧改写顶点高度,让网格像被水流推动一样起伏,演示动态几何更新
核心逻辑:保留原始高度,逐帧写入 position.y 后重算法线并置 needsUpdate
依赖: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:#08131a}
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;font-variant-numeric:tabular-nums}
.bar input[type=range]{width:130px;accent-color:#4f9ef8}
.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 .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>80 × 80</b></span>
<label>振幅 <input type="range" id="amp" min="0.2" max="2.4" step="0.1" value="1"></label>
<span><b id="ampv">1.0</b></span>
<button id="pause">暂停</button>
<span class="sp"></span>
<span id="fps">— 帧/秒</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;
renderer.toneMapping = THREE.ACESFilmicToneMapping;
stage.appendChild(renderer.domElement);
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x08131a);
scene.fog = new THREE.Fog(0x08131a, 26, 52);
const camera = new THREE.PerspectiveCamera(45, stage.clientWidth / stage.clientHeight, 0.1, 200);
camera.position.set(15, 11, 17);
const controls = new OrbitControls(camera, renderer.domElement);
controls.target.set(0, 0, 0);
controls.enableDamping = true;
controls.maxPolarAngle = Math.PI * 0.47;
controls.update();
scene.add(new THREE.HemisphereLight(0x9ed4ff, 0x0a1a20, 1.3));
const sun = new THREE.DirectionalLight(0xffd9a0, 2.2);
sun.position.set(-12, 20, 10);
scene.add(sun);
const SEG = 80;
const SIZE = 26;
const geo = new THREE.PlaneGeometry(SIZE, SIZE, SEG, SEG);
geo.rotateX(-Math.PI / 2);
const pos = geo.attributes.position;
const baseY = new Float32Array(pos.count);
const baseX = new Float32Array(pos.count);
const baseZ = new Float32Array(pos.count);
for (let i = 0; i < pos.count; i++) {
baseX[i] = pos.getX(i);
baseZ[i] = pos.getZ(i);
baseY[i] = Math.sin(baseX[i] * 0.3) * Math.cos(baseZ[i] * 0.28) * 0.8;
pos.setY(i, baseY[i]);
}
geo.computeVertexNormals();
const colors = new Float32Array(pos.count * 3);
geo.setAttribute('color', new THREE.BufferAttribute(colors, 3));
const mat = new THREE.MeshStandardMaterial({
vertexColors: true,
roughness: 0.32,
metalness: 0.42,
side: THREE.DoubleSide
});
const water = new THREE.Mesh(geo, mat);
scene.add(water);
const c = new THREE.Color();
function paint() {
for (let i = 0; i < pos.count; i++) {
const h = pos.getY(i);
const t = THREE.MathUtils.clamp(h / 2.4 * 0.5 + 0.5, 0, 1);
c.setHSL(0.54 - 0.16 * t, 0.62, 0.16 + 0.44 * t);
colors[i * 3] = c.r;
colors[i * 3 + 1] = c.g;
colors[i * 3 + 2] = c.b;
}
geo.getAttribute('color').needsUpdate = true;
}
let amp = 1;
let paused = false;
const ampEl = document.getElementById('amp');
const ampvEl = document.getElementById('ampv');
ampEl.addEventListener('input', function () {
amp = Number(ampEl.value);
ampvEl.textContent = amp.toFixed(1);
});
const pauseBtn = document.getElementById('pause');
pauseBtn.addEventListener('click', function () {
paused = !paused;
pauseBtn.textContent = paused ? '继续' : '暂停';
});
const fpsEl = document.getElementById('fps');
const clock = new THREE.Clock();
let frames = 0;
let acc = 0;
let t = 0;
renderer.setAnimationLoop(function () {
const dt = Math.min(clock.getDelta(), 0.1);
if (!paused) t += dt;
for (let i = 0; i < pos.count; i++) {
const x = baseX[i];
const z = baseZ[i];
const r = Math.sqrt(x * x + z * z);
/* 中心向外的同心波 + 两组交叉波纹 */
const wave = Math.sin(r * 1.35 - t * 2.4) * Math.exp(-r * 0.08);
const cross = Math.sin(x * 0.55 + t * 1.1) * Math.cos(z * 0.5 - t * 0.9) * 0.5;
pos.setY(i, baseY[i] + (wave * 1.6 + cross) * amp);
}
pos.needsUpdate = true;
geo.computeVertexNormals();
paint();
frames++;
acc += dt;
if (acc >= 0.5) {
fpsEl.textContent = Math.round(frames / acc) + ' 帧/秒';
frames = 0;
acc = 0;
}
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.