import { Utils, BaseTickableObject3D, MathUtils } from '@uino/thing';
const __ = {
private: Symbol('private'),
};
/**
* @class InstancedLOD
* @summary 实例化 LOD(Level of Detail)对象,基于空间分割树(四叉树/八叉树)管理大量实例化物体的细节层次。
* 通过注册不同 LOD 级别的模型资源,结合空间分割算法自动管理大量实例的显示与隐藏,
* 根据摄像机距离动态切换细节级别,在保证视觉质量的同时大幅减少渲染开销。
* 支持注册多个 LOD 资源、批量添加实例节点、缓存探测结果,
* 适用于大规模植被、城市建筑群、粒子系统等需要大量相同/相似对象高效渲染的场景。
* @public
* @extends THING.BaseTickableObject3D
* @memberof THING
* @example
* // 创建 InstancedLOD 对象
* const lodSystem = new THING.EXTEND.InstancedLOD({
* boundary: {
* min: [-100, -10, -100],
* max: [100, 50, 100]
* },
* treeType: 'quadtree',
* maxDepth: 6
* });
*
* // 注册 LOD 资源(高、中、低三种精度)
* lodSystem.registerResources('tree', {
* urls: [
* './models/tree_lod0.glb',
* './models/tree_lod1.glb',
* './models/tree_lod2.glb'
* ],
* distances: [50, 100, 200],
* maxCount: 1000,
* radius: 2
* }).then(function() {
* // 批量添加实例
* lodSystem.addNodes('tree', {
* position: [xyz coordinates],
* angle: [ angles array],
* scale: [ scale array ]
* });
* });
*/
class InstancedLOD extends BaseTickableObject3D {
static DefaultPickId = 500000;
/**
* constructor.
* @param {object} param
* @param {THING.Box3} param.boundary the bounding box of all LODs
* @param {string} param.treeType the type of the space partition tree, either 'octree' or 'quadtree'
* @param {number} param.maxDepth the maximum depth of the space partition tree
* @param {boolean} param.strictHide whether to strictly hide LODs when update
* @param {number} param.distance the distance from the camera at which the LODs are shown
*/
constructor(param = {}) {
super(param);
}
onSetupAttributes(param) {
super.onSetupAttributes(param);
this[__.private] = {};
const _private = this[__.private];
const external = param['extras'] || param['external'];
if (external) {
param.treeType = external.treeType;
param.boundary = external.boundary;
param.maxDepth = external.maxDepth;
param.strictHide = external.strictHide;
param.distance = external.distance;
}
_private.treeType = param.treeType || 'quadtree';
const boundary = param.boundary || {};
_private.boundary = {
min: boundary.min || [-500, -100, -500],
max: boundary.max || [500, 100, 500]
};
_private.maxDepth = param.maxDepth || 6;
}
onLoadResource(options, resolve, reject) {
let dynamic = Utils.parseValue(options['dynamic'], false);
if (!dynamic) {
const _private = this[__.private];
options.defaultPickId = InstancedLOD.DefaultPickId;
const node = Utils.createObject('LODNode', { external: { uScene: this.node._uScene, param: options }});
this.body.setNode(node);
_private.resources = new Map();
_private.camera = this.app.camera;
const external = options['extras'] || options['external'];
if (!external) {
super.onLoadResource(options, resolve, reject);
}
else {
const resources = external.resources;
if (!resources) { resolve(); }
const nodes = external.nodes;
if (!nodes) { resolve(); }
for (let i = 0, l = nodes.length; i < l; i++) {
const count = nodes[i].position.length / 3;
InstancedLOD.DefaultPickId += count;
}
const registers = resources.map(resource => {
return this.registerResources(resource.name, resource);
})
const registersPromise = Promise.all(registers);
registersPromise
.then(() => {
for (let i = 0, l = nodes.length; i < l; i++) {
const node = nodes[i];
const index = node.resource;
const name = resources[index].name;
this.addNodes(name, node);
}
resolve();
})
.catch((error) => {
reject(error);
});
}
}
else {
resolve();
}
}
/**
* Registers an instanced resource.
* @param {string} name the name of the resource
* @param {object} options the resource
* @param {Array<string>} options.urls urls to model resources
* @param {Array<number>} options.distances the distance on which the classification levels are based
* @param {number} options.maxCount the maximum number of instances
* @param {number} options.radius the radius of LOD node
* @returns {Promise}
*/
registerResources(name, options) {
const _private = this[__.private];
if (_private.resources.has(name)) {
console.warn('InstancedLOD: Resource has been registered.');
return;
}
const app = this.app;
const loadTask = options.urls.map(url => {
const modelUrl = Utils.resolveURL(url)
const load = app.resourceManager.loadModelAsync(modelUrl);
return load;
})
const promise = Promise.all(loadTask);
promise.then((result) => {
this.bodyNode.registerResources(options, result);
options.nodes = {
position: [],
angle: [],
scale: [],
scaleFactor: options.scaleFactor || 1
};
options.ids = [];
_private.resources.set(name, options);
});
return promise;
}
/**
* Unregisters an instanced resource.
* @param {string} name the name of the resource
*/
unRegisterResource(name) {
const _private = this[__.private];
if (!_private.resources.has(name)) {
console.warn('InstancedLOD: Resource has not been registered.');
return;
}
const resources = _private.resources.get(name);
resources.urls.forEach(url => {
this.bodyNode.unRegisterResource(url);
})
_private.resources.delete(name);
}
/**
* Add an instanced LOD node.
* @param {string} name the resource name has been registered
* @param {object} data the data
* @param {Array<number>} data.position the position of node
* @param {Array<number>} data.angle the angle of node
* @param {Array<number>} data.scale the scale of node
* @return {number} the node id
*/
addNode(name, data) {
const _private = this[__.private];
if (!_private.resources.has(name)) {
console.warn('InstancedLOD: Resource has not been registered.');
return;
}
const worldMatrix = MathUtils.createMat4(data.position, data.angle, data.scale);
const resourcesData = _private.resources.get(name);
const radius = data.radius ? data.radius : resourcesData.radius;
resourcesData.nodes.position.push(data.position);
resourcesData.nodes.angle.push(data.angle);
THING.MathUtils.vec3.scale(data.scale, data.scale, scaleFactor)
resourcesData.nodes.scale.push(data.scale);
const id = this.bodyNode.createInstancedLODNode({
resources: resourcesData.urls,
distances: resourcesData.distances,
worldMatrix,
radius
})
resourcesData.ids.push(id);
return id;
}
/**
* destroy an instanced LOD node.
* @param {number} id the node id
*/
destroyNode(id) {
this.bodyNode.destroyInstancedLODNode(id);
}
/**
* Add instanced LOD nodes.
* @param {string} name the resource name has been registered
* @param {object} data the data
* @param {Array<Array>} data.position the position array of nodes
* @param {Array<Array>} data.angle the angle array of nodes
* @param {Array<Array>} data.scale the scale array of nodes
* @return {Array<number>} the nodes id array
*/
addNodes(name, data) {
const _private = this[__.private];
if (!_private.resources.has(name)) {
console.warn('InstancedLOD: Resource has not been registered.');
return;
}
const resourcesData = _private.resources.get(name);
let position = data.position || [];
let angle = data.angle || [];
let scale = data.scale || [];
let scaleFactor = resourcesData.nodes.scaleFactor;
if (!(position.length === angle.length && angle.length === scale.length)) {
console.warn('The position angle and scale array lengths are not equal, and the shortest length will be used for calculation.')
const length = MathUtils.min(position.length, angle.length, scale.length);
position.length = length;
angle.length = length;
scale.length = length;
}
const maxCount = resourcesData.maxCount * 3;
if (position.length > maxCount) {
console.warn('The maximum value supported by this resource is ' + maxCount);
position.length = maxCount;
angle.length = maxCount;
scale.length = maxCount;
}
resourcesData.nodes.position = position;
resourcesData.nodes.angle = angle;
resourcesData.nodes.scale = scale;
const worldMatrices = [], radiuses = [];
for (let i = 0, l = position.length; i < l; i += 3) {
const vec3_position = [position[i], position[i + 1], position[i + 2]];
const vec3_angle = [angle[i], angle[i + 1], angle[i + 2]];
const vec3_scale = [scale[i] * scaleFactor, scale[i + 1] * scaleFactor, scale[i + 2] * scaleFactor];
const worldMatrix = MathUtils.createMat4(vec3_position, vec3_angle, vec3_scale);
worldMatrices.push(worldMatrix);
const radius = data.radius ? data.radius[i] : resourcesData.radius;
radiuses.push(radius);
}
const ids = this.bodyNode.createInstancedLODNodes({
resources: resourcesData.urls,
distances: resourcesData.distances,
worldMatrices,
radiuses
});
resourcesData.ids = ids;
return ids;
}
destroyResourcesNodes(name) {
const _private = this[__.private];
if (!_private.resources.has(name)) {
console.warn('InstancedLOD: Resource has not been registered.');
return;
}
const resourcesData = _private.resources.get(name);
this.bodyNode.destroyResourcesNodes(resourcesData.urls, resourcesData.ids);
resourcesData.nodes.position.length = 0;
resourcesData.nodes.angle.length = 0;
resourcesData.nodes.scale.length = 0;
resourcesData.nodes.scaleFactor = 1;
resourcesData.ids.length = 0;
}
/**
* Set the camera on which to update the state of the LODs.
* @param {THING.Camera} camera the camera
*/
setCamera(camera) {
const _private = this[__.private];
if (!camera.isCamera) {
console.warn('InstancedLOD: The setted is not a camera.');
return;
}
_private.camera = camera;
}
onUpdate(deltaTime) {
if (this.bodyNode) {
const _private = this[__.private];
this.bodyNode.update(_private.camera.node);
}
}
onExportExternalData() {
let external = Object.assign({}, super.onExportExternalData());
const _private = this[__.private];
external.boundary = _private.boundary;
external.treeType = _private.treeType;
external.maxDepth = _private.maxDepth;
external.strictHide = this.strictHide;
external.distance = this.distance;
const resources = [];
const nodes = [];
const resourcesMap = _private.resources;
for (let [key, value] of resourcesMap) {
const index = resources.length;
const nodeOptions = Object.assign({ resource: index }, value.nodes);
delete nodeOptions.scaleFactor;
nodes.push(nodeOptions);
const resourceOption = Object.assign({ name: key }, value);
resourceOption.scaleFactor = resourceOption.nodes.scaleFactor;
delete resourceOption.nodes;
delete resourceOption.ids;
resources.push(resourceOption);
}
external.resources = resources;
external.nodes = nodes;
return external;
}
/**
* 获取/设置距离。
* @type {number}
* @public
*/
set distance(distance) {
this.bodyNode.setDistance(distance);
}
get distance() {
return this.bodyNode.getDistance();
}
/**
* 获取/设置严格隐藏。
* @type {boolean}
* @public
*/
set strictHide(strictHide) {
this.bodyNode.setStrictHide(strictHide);
}
get strictHide() {
return this.bodyNode.getStrictHide();
}
getScale(name) {
const _private = this[__.private];
if (!_private.resources.has(name)) {
console.warn('InstancedLOD: Resource has not been registered.');
return;
}
const resourcesData = _private.resources.get(name);
if (resourcesData.node.scaleFactor) {
return resourcesData.node.scaleFactor;
}
return 1;
}
setScale(name, value) {
const _private = this[__.private];
if (!_private.resources.has(name)) {
console.warn('InstancedLOD: Resource has not been registered.');
return;
}
const resourcesData = _private.resources.get(name);
this.bodyNode.destroyResourcesNodes(resourcesData.urls, resourcesData.ids);
resourcesData.nodes.scaleFactor = value;
resourcesData.ids.length = 0;
this.addNodes(name, resourcesData.nodes);
}
/**
* 获取资源。
* @type {Map}
* @public
*/
get resources() {
const _private = this[__.private];
return _private.resources;
}
/**
* 检查是否为 InstancedLOD 类型或继承自它。
* @type {boolean}
* @public
*/
get isInstancedLOD() {
return true
}
}
export { InstancedLOD };