@needle-tools/engine
Version:
Needle Engine is a web-based runtime for 3D apps. It runs on your machine for development with great integrations into editors like Unity or Blender - and can be deployed onto any device! It is flexible, extensible and networking and XR are built-in.
187 lines (161 loc) • 9.26 kB
text/typescript
import { LODsManager as _LODsManager, NEEDLE_progressive, NEEDLE_progressive_plugin } from "@needle-tools/gltf-progressive";
import type { LOD_Results } from "@needle-tools/gltf-progressive/src/lods_manager.js";
import { Box3, Camera, Mesh, PerspectiveCamera, Scene, Sphere, WebGLRenderer } from "three";
import { findResourceUsers } from "./engine_assetdatabase.js";
import type { Context } from "./engine_context.js";
import { Gizmos } from "./engine_gizmos.js";
import { getTempVector } from "./engine_three_utils.js";
import { IGameObject } from "./engine_types.js";
import { getParam } from "./engine_utils.js";
const debug = getParam("debugprogressive");
const _tempBox: Box3 = new Box3();
const _tempSphere: Sphere = new Sphere();
/**
* Needle Engine LODs manager. Wrapper around the internal LODs manager.
* It uses the @needle-tools/gltf-progressive package to manage LODs.
* @link https://npmjs.com/package/@needle-tools/gltf-progressive
*/
export class LODsManager implements NEEDLE_progressive_plugin {
readonly context: Context;
private _lodsManager?: _LODsManager;
private _settings: Partial<Pick<_LODsManager, "skinnedMeshAutoUpdateBoundsInterval" | "targetTriangleDensity">> = {
}
/**
* The internal LODs manager. See @needle-tools/gltf-progressive for more information.
* @link https://npmjs.com/package/@needle-tools/gltf-progressive
*/
get manager() {
return this._lodsManager;
}
get skinnedMeshAutoUpdateBoundsInterval() {
return this._lodsManager?.skinnedMeshAutoUpdateBoundsInterval || this._settings.skinnedMeshAutoUpdateBoundsInterval || 0;
}
set skinnedMeshAutoUpdateBoundsInterval(value: number) {
this._settings.skinnedMeshAutoUpdateBoundsInterval = value;
this.applySettings();
}
/**
* The target triangle density is the desired max amount of triangles on screen when the mesh is filling the screen.
* @default 200_000
*/
get targetTriangleDensity() {
return this._lodsManager?.targetTriangleDensity || this._settings.targetTriangleDensity || 200_000; // default value
}
set targetTriangleDensity(value: number) {
this._settings.targetTriangleDensity = value;
this.applySettings();
}
constructor(context: Context) {
this.context = context;
}
private applySettings() {
if(this._lodsManager) {
for(const key in this._settings) {
this._lodsManager[key] = this._settings[key];
}
}
}
/** @internal */
setRenderer(renderer: WebGLRenderer) {
this._lodsManager?.disable();
_LODsManager.removePlugin(this);
_LODsManager.addPlugin(this);
_LODsManager.debugDrawLine = Gizmos.DrawLine;
this._lodsManager = _LODsManager.get(renderer);
this.applySettings();
this._lodsManager.enable();
}
disable() {
this._lodsManager?.disable();
_LODsManager.removePlugin(this);
}
/** @internal */
onAfterUpdatedLOD(_renderer: WebGLRenderer, _scene: Scene, camera: Camera, mesh: Mesh, level: LOD_Results): void {
if (debug) this.onRenderDebug(camera, mesh, level);
}
private onRenderDebug(camera: Camera, mesh: Mesh, results: LOD_Results) {
if (!mesh.geometry) return;
if (!NEEDLE_progressive.hasLODLevelAvailable(mesh.geometry) && !NEEDLE_progressive.hasLODLevelAvailable(mesh.material)) return;
const state = _LODsManager.getObjectLODState(mesh);
if (!state) return;
let level = results.mesh_lod;
const changed = results.mesh_lod != state.lastLodLevel_Mesh || results.texture_lod != state.lastLodLevel_Texture;
if (debug && mesh.geometry.boundingSphere) {
const bounds = mesh.geometry.boundingSphere;
_tempSphere.copy(bounds);
_tempSphere.applyMatrix4(mesh.matrixWorld);
const boundsCenter = _tempSphere.center;
const radius = _tempSphere.radius;
const colors = ["#76c43e", "#bcc43e", "#c4ac3e", "#c4673e", "#ff3e3e"];
// if the lod has changed we just want to draw the gizmo for the changed mesh
if (changed) {
Gizmos.DrawWireSphere(boundsCenter, radius, colors[level], .1);
}
else {
// Mesh Density is calculated as: triangle count per square meter of surface area, normalized to the bounding box size of the model.
// Our goal for automatic switching of LODs is that the resulting triangle count per screen area is constant.
// We assume a uniform distribution of triangles over the surface area; which means that
// we can express a ratio of "screen area to surface area".
const triangleCount = mesh.geometry.index?.count ?? 0 / 3;
const lods = NEEDLE_progressive.getMeshLODExtension(mesh.geometry)?.lods;
level = lods ? Math.min(lods?.length - 1, level) : 0;
let allLods = "";
if (lods && state.lastScreenCoverage > 0) {
for (let i = 0; i < lods.length; i++) {
const d = lods[i].density;
const last = i == lods.length - 1;
allLods += d.toFixed(0) + ">" + (d / state.lastScreenCoverage).toFixed(0) + (last ? "" : ",");
}
}
const density = lods ? lods[level]?.density : -1;
// const box = mesh.geometry.boundingBox;
// const boxSize = box ? box.getSize(getTempVector()) : new Vector3();
// const maxBoxSize = Math.max(boxSize.x, boxSize.y, boxSize.z);
// Surface area is in local space of the model;
// we need to scale it by the model's world scale and the model's geometry bounding box size.
// const ws = mesh.getWorldScale(getTempVector());
// const wsMedian = (ws.x + ws.y + ws.z) / 3;
// Area is squared, so both maxBoxSize and wsMedian are squared here
// Here, we're basically reverting the calculations that have happened in the pipeline for debugging.
// const surfaceArea = 1 / density * triangleCount * (maxBoxSize * maxBoxSize) * (wsMedian * wsMedian);
let text = "LOD " + results.mesh_lod + "\nTEX " + results.texture_lod;
if (debug == "density") {
text +=
"\n" + triangleCount + " tris" +
// This is key – basically how we're switching
"\n" + (density / state.lastScreenCoverage).toFixed(0) + " dens" +
"\n" + (state.lastScreenCoverage * 100).toFixed(1) + "% cov" +
// "\n" + (this._lastScreenspaceVolume.x.toFixed(2) + "x" + this._lastScreenspaceVolume.y.toFixed(2) + "x" + this._lastScreenspaceVolume.z.toFixed(2)) + " vol" +
// + "\n" + (surfaceArea).toFixed(2) + " m2" +
"\n" + (state.lastCentrality * 100).toFixed(1) + "% centr" +
"\n" + (_tempBox.min.x.toFixed(2) + "-" + _tempBox.max.x.toFixed(2) + "x" + _tempBox.min.y.toFixed(2) + "-" + _tempBox.max.y.toFixed(2)) + " scr" +
// "\n" + (ws.x).toFixed(2) + "x" + " " + maxBoxSize.toFixed(2) + "b" + "\n" +
// allLods + "\n" +
//"----" + "\n" +
// "1000" + " ideal dens"
"";
}
// if (helper) {
// helper?.setText(text);
// continue;
// }
if (state.lastScreenCoverage > .1) {
const cam = camera as any as IGameObject;
const camForward = cam.worldForward;
const camWorld = cam.worldPosition;
const fwd = getTempVector(camForward);
// for debugging very close LDOs, we need to flip the radius...
const pos = fwd.multiplyScalar(radius * .7).add(boundsCenter);
const distance = pos.distanceTo(camWorld);
// const vertexCount = mesh.geometry.index!.count / 3;
// const vertexCountFactor = Math.min(1, vertexCount / 1000);
const col = colors[Math.min(colors.length - 1, Math.max(0, level))] + "88";
// const size = Math.min(10, radius);
const windowScale = this.context.domHeight > 0 ? screen.height / this.context.domHeight : 1;
const fieldOfViewScale = (camera as PerspectiveCamera).isPerspectiveCamera ? Math.tan((camera as PerspectiveCamera).fov * Math.PI / 180 / 2) : 1;
Gizmos.DrawLabel(pos, text, distance * .012 * windowScale * fieldOfViewScale, undefined, 0xffffff, col);
}
}
}
}
}