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.

190 lines (169 loc) • 6.73 kB
import { LOD as ThreeLOD, Object3D, Vector3 } from "three"; import { serializable } from "../engine/engine_serialization_decorator.js"; import { getParam } from "../engine/engine_utils.js"; import { Behaviour, GameObject } from "./Component.js"; import { Renderer } from "./Renderer.js"; const debug = getParam("debuglods"); const noLods = getParam("nolods"); /** * Defines how LOD levels transition between each other */ enum LODFadeMode { /** Instant switch between LOD levels */ None = 0, /** Smooth cross-fade transition between levels */ CrossFade = 1, /** SpeedTree-style blending for vegetation */ SpeedTree = 2, } /** * Defines a single LOD level with its transition distance and associated renderers. * Used by {@link LODGroup} to configure level of detail switching. */ export class LODModel { /** Screen height ratio (0-1) at which this LOD becomes active */ @serializable() screenRelativeTransitionHeight!: number; /** Distance from camera at which this LOD becomes active */ @serializable() distance!: number; /** Renderers to show at this LOD level */ @serializable(Renderer) renderers!: Renderer[]; } class LOD { readonly model: LODModel; get renderers(): Renderer[] { return this.model.renderers; } constructor(model: LODModel) { this.model = model; } } declare class LODSetting { lod: ThreeLOD; levelIndex: number; distance: number; } /** * LODGroup manages multiple levels of detail for optimized rendering. * Objects switch between different detail levels based on distance from camera. * * LOD levels are defined in {@link LODModel} objects, each specifying: * - The distance at which that level becomes active * - The {@link Renderer} components to show at that level * * This is useful for performance optimization - showing high-detail models up close * and lower-detail versions at distance where the difference isn't visible. * * **Progressive Loading:** * For automatic texture/mesh LOD streaming, see the `@needle-tools/gltf-progressive` package * which provides progressive loading capabilities independent of this component. * * **Debug options:** * - `?debuglods` - Log LOD switching information * - `?nolods` - Disable LOD system entirely * * @summary Level of Detail Group for optimizing rendering * @category Rendering * @group Components * @see {@link LODModel} for configuring individual LOD levels * @see {@link Renderer} for the renderers controlled by LOD * @see {@link LODsManager} for programmatic control of progressive LODs * @link https://npmjs.com/package/@needle-tools/gltf-progressive */ export class LODGroup extends Behaviour { /** Array of LOD level configurations */ @serializable(LODModel) readonly lodModels: LODModel[] = [] private _lods: LOD[] = []; private _settings: LODSetting[] = []; // https://threejs.org/docs/#api/en/objects/LOD private _lodsHandler?: Array<ThreeLOD>; start(): void { if (debug) console.log("LODGROUP", this.name, this.lodModels, this); if (noLods) return; if (this._lodsHandler) return; if (!this.gameObject) return; if (this.lodModels && Array.isArray(this.lodModels)) { const renderers: Renderer[] = []; for (const model of this.lodModels) { const lod = new LOD(model); this._lods.push(lod); for (const rend of lod.renderers) { if (!renderers.includes(rend)) renderers.push(rend); } } this._lodsHandler = new Array<ThreeLOD>(); for (let i = 0; i < renderers.length; i++) { const handler = new ThreeLOD(); this._lodsHandler.push(handler); this.gameObject.add(handler); } const empty = new Object3D(); empty.name = "Cull " + this.name; for (let i = 0; i < renderers.length; i++) { const rend = renderers[i]; const handler = this._lodsHandler[i]; const obj = rend.gameObject; if (debug) console.log(i, obj.name); for (const lod of this._lods) { const dist = lod.model.distance; // get object to be lodded, it can be empty let object: Object3D | null = null; if (lod.renderers.includes(rend)) { object = obj; } else { object = empty; } if (object.type === "Group") { console.warn(`LODGroup ${this.name}: Group or MultiMaterial object's are not supported as LOD object: ${object.name}`); continue; } if (debug) console.log("LEVEL", object.name, dist); handler.autoUpdate = false; this.onAddLodLevel(handler, object, lod.model.distance); } } } } onAfterRender() { if (!this.gameObject) return; if (!this._lodsHandler) return; const cam = this.context.mainCamera; if (!cam) return; for (const h of this._lodsHandler) { h.update(cam); const levelIndex = h.getCurrentLevel(); const level = h.levels[levelIndex]; h.layers.mask = level.object.layers.mask; } } private onAddLodLevel(lod: ThreeLOD, obj: Object3D, dist: number) { if(obj === this.gameObject) { console.warn("LODGroup component must be on parent object and not mesh directly at the moment", obj.name, obj) return; } lod.addLevel(obj, dist * this._distanceFactor, .01); const setting = { lod: lod, levelIndex: lod.levels.length - 1, distance: dist }; this._settings.push(setting) } private _distanceFactor = 1; /** * Adjusts all LOD transition distances by a multiplier. * Values > 1 push LOD transitions further away (higher quality at distance). * Values < 1 bring transitions closer (better performance). * @param factor Multiplier to apply to all LOD distances */ distanceFactor(factor: number) { if (factor === this._distanceFactor) return; this._distanceFactor = factor; for (const setting of this._settings) { const level = setting.lod.levels[setting.levelIndex]; level.distance = setting.distance * factor; } } }