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"); enum LODFadeMode { None = 0, CrossFade = 1, SpeedTree = 2, } export class LODModel { @serializable() screenRelativeTransitionHeight!: number; @serializable() distance!: number; @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 allows to create a group of LOD levels for an object. * @category Rendering * @group Components */ export class LODGroup extends Behaviour { fadeMode: LODFadeMode = LODFadeMode.None; @serializable(Vector3) localReferencePoint: Vector3 | undefined = undefined; lodCount: number = 0; size: number = 0; animateCrossFading: boolean = false; @serializable(LODModel) lodModels?: LODModel[]; private _lods: LOD[] = []; private _settings: LODSetting[] = []; // https://threejs.org/docs/#api/en/objects/LOD private _lodsHandler?: Array; 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(); 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; 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; } } }