Skip to content

Instantly share code, notes, and snippets.

@Ctrlmonster
Created March 16, 2025 16:52
Show Gist options
  • Save Ctrlmonster/ba448e28108cc7a9cdd6741292ece6e2 to your computer and use it in GitHub Desktop.
Save Ctrlmonster/ba448e28108cc7a9cdd6741292ece6e2 to your computer and use it in GitHub Desktop.
import {MeshoptSimplifier, MeshoptEncoder} from "meshoptimizer";
import {BufferAttribute, BufferGeometry, Mesh} from "three";
await MeshoptSimplifier.ready; // await wasm promise
await MeshoptEncoder.ready; // await wasm promise
MeshoptSimplifier.useExperimentalFeatures = true;
export function createLOD_Levels(originalMesh: Mesh, simplificationLevel: number[]) {
const lodMeshes: {meshes: Mesh[], errors: number[]} = {errors:[], meshes: []};
for (let i = 0; i < simplificationLevel.length; i++) {
const errorLevel = simplificationLevel[i];
const {error, mesh} = simplifyMesh(originalMesh, errorLevel);
lodMeshes.meshes.push(mesh);
lodMeshes.errors.push(error);
}
return lodMeshes;
}
export function simplifyMesh(mesh: Mesh, error=0.005) {
const indexedGeometry = mesh.geometry;
// Ensure the geometry is indexed
if (indexedGeometry.index === null) {
throw new Error(`LOD generation failed: Geometry needs an index buffer`);
}
// Extract position attribute and index array
const positionAttribute = indexedGeometry.getAttribute("position");
if (!positionAttribute) {
throw new Error("Geometry must have a position attribute.");
}
const normalAttribute = indexedGeometry.getAttribute("normal");
const uvAttribute = indexedGeometry.getAttribute("uv");
const positions = indexedGeometry.getAttribute("position").array as Float32Array;
//const reductionPercent = 0.5;
//const targetIndexCount = indexedGeometry.index.count * reductionPercent + (indexedGeometry.index.count * reductionPercent % 3);
const [simplifiedIdxBuffer, _visibleErr] = MeshoptSimplifier.simplify(
indexedGeometry.index!.array as Uint32Array,
positions,
positionAttribute.itemSize,
0, // just set to 0 for max reduction while keeping the error boundary
error, // error boundary
["LockBorder", "Prune"]
);
const scaleFactor = MeshoptSimplifier.getScale(positions, 3);
const absError = scaleFactor * error;
//console.log(`Absolute Error: ${} - Relative Error: ${_visibleErr}`);
const newBuffer = new Uint32Array<ArrayBufferLike>(simplifiedIdxBuffer);
const [remap, uniqueVerts] = MeshoptEncoder.reorderMesh(newBuffer, true, false);
//console.log(`${(1 - uniqueVerts / indexedGeometry.index.array.length).toFixed(4)} vertex reduction with ${_visibleErr.toFixed(4)} visual error.`);
//const [remap, uniqueVerts] = MeshoptSimplifier.compactMesh(newBuffer);
const finalIdxBuffer = (uniqueVerts <= 256)
? new Uint8Array(simplifiedIdxBuffer.length)
: (uniqueVerts <= 65536)
? new Uint16Array(simplifiedIdxBuffer.length)
: new Uint32Array(simplifiedIdxBuffer.length);
for (let i = 0; i < simplifiedIdxBuffer.length; i++) {
const oldVertexIndex = simplifiedIdxBuffer[i];
const newVertexIndex = remap[oldVertexIndex];
finalIdxBuffer[i] = newVertexIndex;
}
const newGeo = new BufferGeometry();
newGeo.setIndex(new BufferAttribute(finalIdxBuffer, 1));
// we need to update all existing attributes
const newPositionAttribute = new BufferAttribute(
new Float32Array(uniqueVerts * positionAttribute.itemSize),
positionAttribute.itemSize
);
const newUvAttribute = uvAttribute
? new BufferAttribute(new Float32Array(uniqueVerts * uvAttribute.itemSize), uvAttribute.itemSize)
: null;
const newNormalAttribute = normalAttribute
? new BufferAttribute(new Float32Array(uniqueVerts * normalAttribute.itemSize), normalAttribute.itemSize)
: null;
for (let oldIdx = 0; oldIdx < remap.length; oldIdx++) {
const newIdx = remap[oldIdx];
if (newIdx === 0xffffffff) continue;
// Remap positions
newPositionAttribute.setXYZ(
newIdx,
positionAttribute.getX(oldIdx),
positionAttribute.getY(oldIdx),
positionAttribute.getZ(oldIdx)
);
// Remap UVs
if (newUvAttribute && uvAttribute) {
newUvAttribute.setXY(
newIdx,
uvAttribute.getX(oldIdx),
uvAttribute.getY(oldIdx)
);
}
// Remap normals
if (newNormalAttribute && normalAttribute) {
newNormalAttribute.setXYZ(
newIdx,
normalAttribute.getX(oldIdx),
normalAttribute.getY(oldIdx),
normalAttribute.getZ(oldIdx)
);
}
}
newGeo.setAttribute("position", newPositionAttribute);
newGeo.applyMatrix4(mesh.matrixWorld);
if (newUvAttribute) newGeo.setAttribute("uv", newUvAttribute);
if (newNormalAttribute) newGeo.setAttribute("normal", newNormalAttribute);
return {mesh: new Mesh(newGeo, mesh.material), error: absError};
}
@Ctrlmonster
Copy link
Author

Lod selection system

import {createSystem} from "../create-system.ts";
import {LodMesh} from "../traits/lod-mesh.ts";
import {IsActiveCamera} from "../traits/is-active-camera.ts";
import {IsPerspectiveCamera} from "../traits/is-perspective-camera.ts";
import {TCamera} from "../traits/camera-trait.ts";
import {PerspectiveCamera, Vector2, Vector3} from "three";
import {Transforms} from "../traits/transforms.ts";
import {TWebGLRenderer} from "../traits/web-gl-renderer-trait.ts";


const sizeVec = new Vector2();
const tempVec = new Vector3();
export const UpdateLodMeshes = createSystem(({world}) => {

  const renderer = world.queryFirst(TWebGLRenderer)?.get(TWebGLRenderer);
  if (!renderer) return;

  const activeCam = world.queryFirst(IsActiveCamera, IsPerspectiveCamera, TCamera)?.get(TCamera) as PerspectiveCamera;
  if (!activeCam) return;

  const halfCamFov = (activeCam.fov / 2) / 180 * Math.PI;
  const camPos = activeCam.position;
  const screenHeight = renderer.getSize(sizeVec).y


  const lodMeshes = world.query(LodMesh, Transforms);
  lodMeshes.updateEach(([{meshes, lod_factor, visibleError}, {position: p}]) => {

    if (!meshes[0].geometry.boundingSphere) {
      meshes[0].geometry.computeBoundingSphere();
    }
    const meshRadius = meshes[0].geometry.boundingSphere!.radius;
    const position = tempVec.copy(meshes[0].geometry.boundingSphere!.center).add(p);

    const distance = position.distanceTo(camPos);
    const d = Math.max(0, distance - meshRadius);
    const e = d * (Math.tan(halfCamFov) * 2 / screenHeight);

    // we start from the lowest level one
    let selectedMeshIdx: number = 0;
    for (let i = visibleError.length - 1; i > 0; i--) {
      const lodOk = e * lod_factor >= visibleError[i];
      //console.log(visibleError[i], Math.tan(halfCamFov));
      if (lodOk) {
        selectedMeshIdx = i;
        break;
      }
    }

    for (let i = 0; i < meshes.length; i++) {
      meshes[i].visible = (i === selectedMeshIdx);
    }


  });


});

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment