@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
text/typescript
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 */
()
screenRelativeTransitionHeight!: number;
/** Distance from camera at which this LOD becomes active */
()
distance!: number;
/** Renderers to show at this LOD level */
(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 */
(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;
}
}
}