UNPKG

@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
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); } } } } }