A LOD object holds several versions of a model and shows the one registered for the camera's distance. The renderer updates it each frame; this Node.js 2,131 listing calls update() by hand at five distances:
import * as THREE from 'three';
const spine = (segments) => new THREE.Mesh(new THREE.CylinderGeometry(0.07, 0.07, 0.9,
segments, 1, true, -Math.PI / 2, Math.PI)); // Section 5.10.1's half-tube spine
const lod = new THREE.LOD();
lod.addLevel(spine(24), 0); // near: a smooth curve
lod.addLevel(spine(6), 4); // from 4 m: six facets
lod.addLevel(spine(1), 12, 0.1); // from 12 m: flat, 10% hysteresis
const camera = new THREE.PerspectiveCamera();
for (const z of [2, 6, 13, 11.5, 10.5]) {
camera.position.z = z;
camera.updateMatrixWorld();
lod.update(camera); // the renderer does this if autoUpdate
console.log(`camera at ${z} m: level ${lod.getCurrentLevel()}`);
}Output
camera at 2 m: level 0 camera at 6 m: level 1 camera at 13 m: level 2 camera at 11.5 m: level 2 camera at 10.5 m: level 1
The levels have 48, 12 and 2 triangles. Hysteresis keeps the far level until the camera is 10% inside its 12 m threshold (10.8 m), so objects on the boundary do not flicker. The SimplifyModifier add-on or gltf-transform simplify builds the low levels.
<!doctype html>
<style>
body { margin: 0; background: #f5f1e8; font: 13px system-ui, sans-serif; color: #263238; }
canvas { display: block; max-width: 100%; }
.ui { padding: 4px 8px; }
</style>
<script type="importmap">
{ "imports": {
"three": "https://cdn.jsdelivr.net/npm/three@0.186.1/build/three.module.js",
"three/addons/": "https://cdn.jsdelivr.net/npm/three@0.186.1/examples/jsm/" } }
</script>
<div class="ui">Camera distance <input type="range" id="dist" min="1.5" max="16" step="0.1" value="3.5"> <span id="out"></span></div>
<script type="module">
import * as THREE from 'three';
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setPixelRatio(Math.min(devicePixelRatio, 2));
renderer.setSize(600, 260);
document.body.prepend(renderer.domElement);
const scene = new THREE.Scene();
scene.background = new THREE.Color('#fbf8f2');
const camera = new THREE.PerspectiveCamera(30, 600 / 260, 0.1, 100);
scene.add(new THREE.HemisphereLight('#ffffff', '#8d6e63', 1.2));
const sun = new THREE.DirectionalLight('#ffffff', 2.5);
sun.position.set(2, 3, 4);
scene.add(sun);
// The half-tube spine at three levels of detail; wireframe overlay shows the facets
const levelColors = ['#3f7d3a', '#e09a10', '#b5452f'];
const spine = (segments, i) => {
const geometry = new THREE.CylinderGeometry(0.14, 0.14, 1.2, segments, 1, true, -Math.PI / 2, Math.PI);
const mesh = new THREE.Mesh(geometry, new THREE.MeshStandardMaterial({ color: levelColors[i], side: THREE.DoubleSide, flatShading: true }));
mesh.add(new THREE.LineSegments(new THREE.WireframeGeometry(geometry), new THREE.LineBasicMaterial({ color: '#263238' })));
return mesh;
};
const lods = [];
for (let k = 0; k < 7; k++) {
const lod = new THREE.LOD();
lod.addLevel(spine(24, 0), 0); // near: a smooth curve
lod.addLevel(spine(6, 1), 4); // from 4 m: six facets
lod.addLevel(spine(1, 2), 12, 0.1); // from 12 m: flat, with 10% hysteresis
lod.position.set(-0.5 + k * 0.6, 0, -k * 2.3); // a receding row, 2 to 16 m away
lod.rotation.y = 0.4;
scene.add(lod);
lods.push(lod);
}
const slider = document.getElementById('dist');
renderer.setAnimationLoop((t) => {
const d = +slider.value;
camera.position.set(0.8, 0.5, d);
camera.lookAt(0.8, 0, -6);
renderer.render(scene, camera); // the renderer calls lod.update(camera) (autoUpdate)
document.getElementById('out').textContent =
'levels, near to far: ' + lods.map((l) => l.getCurrentLevel()).join(' ') + ' (0 = 24 segments, 1 = 6, 2 = 1)';
});
</script>