Refactor spline repeater renderer to use InstancedMesh for optimized rendering
This commit is contained in:
@@ -1,13 +1,22 @@
|
|||||||
import { Group, MathUtils } from "three";
|
import {
|
||||||
|
BufferGeometry,
|
||||||
|
Group,
|
||||||
|
InstancedMesh,
|
||||||
|
Matrix4,
|
||||||
|
MathUtils,
|
||||||
|
Mesh,
|
||||||
|
Quaternion,
|
||||||
|
Vector3,
|
||||||
|
type Material
|
||||||
|
} from "three";
|
||||||
|
|
||||||
import { instantiateModelTemplate } from "../assets/gltf-model-import";
|
|
||||||
import { disposeModelInstance } from "../assets/model-instance-rendering";
|
|
||||||
import type { Terrain } from "../document/terrains";
|
import type { Terrain } from "../document/terrains";
|
||||||
import { applyRendererRenderCategoryFromMaterial } from "../rendering/render-layers";
|
import { applyRendererRenderCategoryFromMaterial } from "../rendering/render-layers";
|
||||||
|
|
||||||
import { loadBundledSplineCorridorModelTemplate } from "./bundled-spline-corridor-model-loader";
|
import { loadBundledSplineCorridorModelTemplate } from "./bundled-spline-corridor-model-loader";
|
||||||
import {
|
import {
|
||||||
deriveSplineRepeaterInstances,
|
deriveSplineRepeaterInstances,
|
||||||
|
type SplineRepeaterInstance,
|
||||||
type SplineRepeaterPathLike
|
type SplineRepeaterPathLike
|
||||||
} from "./spline-repeaters";
|
} from "./spline-repeaters";
|
||||||
|
|
||||||
@@ -21,6 +30,13 @@ interface SplineRepeaterRendererOptions {
|
|||||||
onDiagnostic?: (message: string) => void;
|
onDiagnostic?: (message: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface TemplateMeshSource {
|
||||||
|
name: string;
|
||||||
|
geometry: BufferGeometry;
|
||||||
|
material: Material | Material[];
|
||||||
|
matrixWorld: Matrix4;
|
||||||
|
}
|
||||||
|
|
||||||
export class SplineRepeaterRenderer {
|
export class SplineRepeaterRenderer {
|
||||||
readonly group = new Group();
|
readonly group = new Group();
|
||||||
|
|
||||||
@@ -57,10 +73,33 @@ export class SplineRepeaterRenderer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.group.remove(this.activeGroup);
|
this.group.remove(this.activeGroup);
|
||||||
disposeModelInstance(this.activeGroup);
|
this.disposeInstancedGroup(this.activeGroup);
|
||||||
this.activeGroup = null;
|
this.activeGroup = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private disposeInstancedGroup(group: Group) {
|
||||||
|
group.traverse((object) => {
|
||||||
|
const maybeInstancedMesh = object as InstancedMesh & {
|
||||||
|
isInstancedMesh?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (maybeInstancedMesh.isInstancedMesh !== true) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
maybeInstancedMesh.geometry.dispose();
|
||||||
|
|
||||||
|
if (Array.isArray(maybeInstancedMesh.material)) {
|
||||||
|
for (const material of maybeInstancedMesh.material) {
|
||||||
|
material.dispose();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
maybeInstancedMesh.material.dispose();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
private emitDiagnostic(message: string) {
|
private emitDiagnostic(message: string) {
|
||||||
if (this.onDiagnostic !== undefined) {
|
if (this.onDiagnostic !== undefined) {
|
||||||
this.onDiagnostic(message);
|
this.onDiagnostic(message);
|
||||||
@@ -70,6 +109,87 @@ export class SplineRepeaterRenderer {
|
|||||||
console.warn(message);
|
console.warn(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private cloneMaterial(material: Material | Material[]): Material | Material[] {
|
||||||
|
return Array.isArray(material)
|
||||||
|
? material.map((entry) => entry.clone())
|
||||||
|
: material.clone();
|
||||||
|
}
|
||||||
|
|
||||||
|
private collectTemplateMeshes(template: Group): TemplateMeshSource[] {
|
||||||
|
const meshSources: TemplateMeshSource[] = [];
|
||||||
|
|
||||||
|
template.updateMatrixWorld(true);
|
||||||
|
template.traverse((object) => {
|
||||||
|
const maybeMesh = object as Mesh & { isMesh?: boolean };
|
||||||
|
|
||||||
|
if (maybeMesh.isMesh !== true) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
meshSources.push({
|
||||||
|
name: maybeMesh.name,
|
||||||
|
geometry: maybeMesh.geometry.clone(),
|
||||||
|
material: this.cloneMaterial(maybeMesh.material),
|
||||||
|
matrixWorld: maybeMesh.matrixWorld.clone()
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return meshSources;
|
||||||
|
}
|
||||||
|
|
||||||
|
private createInstancedMeshesForAsset(options: {
|
||||||
|
assetId: string;
|
||||||
|
instances: readonly SplineRepeaterInstance[];
|
||||||
|
meshSources: readonly TemplateMeshSource[];
|
||||||
|
}): InstancedMesh[] {
|
||||||
|
const instancePosition = new Vector3();
|
||||||
|
const instanceScale = new Vector3();
|
||||||
|
const instanceQuaternion = new Quaternion();
|
||||||
|
const instanceMatrix = new Matrix4();
|
||||||
|
const finalMatrix = new Matrix4();
|
||||||
|
|
||||||
|
return options.meshSources.map((meshSource, meshIndex) => {
|
||||||
|
const instancedMesh = new InstancedMesh(
|
||||||
|
meshSource.geometry,
|
||||||
|
meshSource.material,
|
||||||
|
options.instances.length
|
||||||
|
);
|
||||||
|
instancedMesh.name =
|
||||||
|
meshSource.name.trim().length > 0
|
||||||
|
? `SplineRepeater:${options.assetId}:${meshSource.name}`
|
||||||
|
: `SplineRepeater:${options.assetId}:mesh-${meshIndex}`;
|
||||||
|
instancedMesh.userData.nonPickable = true;
|
||||||
|
instancedMesh.userData.splineCorridorAssetId = options.assetId;
|
||||||
|
instancedMesh.frustumCulled = true;
|
||||||
|
|
||||||
|
options.instances.forEach((instance, instanceIndex) => {
|
||||||
|
instancePosition.set(
|
||||||
|
instance.position.x,
|
||||||
|
instance.position.y,
|
||||||
|
instance.position.z
|
||||||
|
);
|
||||||
|
instanceQuaternion.setFromAxisAngle(
|
||||||
|
new Vector3(0, 1, 0),
|
||||||
|
MathUtils.degToRad(instance.yawDegrees)
|
||||||
|
);
|
||||||
|
instanceScale.set(instance.scale, instance.scale, instance.scale);
|
||||||
|
instanceMatrix.compose(
|
||||||
|
instancePosition,
|
||||||
|
instanceQuaternion,
|
||||||
|
instanceScale
|
||||||
|
);
|
||||||
|
finalMatrix.multiplyMatrices(instanceMatrix, meshSource.matrixWorld);
|
||||||
|
instancedMesh.setMatrixAt(instanceIndex, finalMatrix);
|
||||||
|
});
|
||||||
|
|
||||||
|
instancedMesh.instanceMatrix.needsUpdate = true;
|
||||||
|
instancedMesh.computeBoundingBox();
|
||||||
|
instancedMesh.computeBoundingSphere();
|
||||||
|
applyRendererRenderCategoryFromMaterial(instancedMesh);
|
||||||
|
return instancedMesh;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
private async rebuildAsync(
|
private async rebuildAsync(
|
||||||
requestId: number,
|
requestId: number,
|
||||||
input: SplineRepeaterRendererInput
|
input: SplineRepeaterRendererInput
|
||||||
@@ -83,54 +203,61 @@ export class SplineRepeaterRenderer {
|
|||||||
const nextGroup = new Group();
|
const nextGroup = new Group();
|
||||||
nextGroup.name = "splineRepeaterInstances";
|
nextGroup.name = "splineRepeaterInstances";
|
||||||
nextGroup.userData.nonPickable = true;
|
nextGroup.userData.nonPickable = true;
|
||||||
|
const instancesByAssetId = new Map<string, SplineRepeaterInstance[]>();
|
||||||
|
|
||||||
for (const instance of instances) {
|
for (const instance of instances) {
|
||||||
|
const assetInstances = instancesByAssetId.get(instance.asset.id) ?? [];
|
||||||
|
assetInstances.push(instance);
|
||||||
|
instancesByAssetId.set(instance.asset.id, assetInstances);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const [assetId, assetInstances] of instancesByAssetId) {
|
||||||
|
const asset = assetInstances[0]?.asset;
|
||||||
|
|
||||||
|
if (asset === undefined) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
let template: Group;
|
let template: Group;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
template = await loadBundledSplineCorridorModelTemplate(
|
template = await loadBundledSplineCorridorModelTemplate(
|
||||||
instance.asset.bundledPath
|
asset.bundledPath
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.emitDiagnostic(
|
this.emitDiagnostic(
|
||||||
error instanceof Error
|
error instanceof Error
|
||||||
? error.message
|
? error.message
|
||||||
: `Bundled spline corridor model failed to load from ${instance.asset.bundledPath}.`
|
: `Bundled spline corridor model failed to load from ${asset.bundledPath}.`
|
||||||
);
|
);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (requestId !== this.requestId) {
|
if (requestId !== this.requestId) {
|
||||||
disposeModelInstance(nextGroup);
|
this.disposeInstancedGroup(nextGroup);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const model = instantiateModelTemplate(template);
|
const instancedMeshes = this.createInstancedMeshesForAsset({
|
||||||
model.name = `SplineRepeater:${instance.asset.id}`;
|
assetId,
|
||||||
model.position.set(
|
instances: assetInstances,
|
||||||
instance.position.x,
|
meshSources: this.collectTemplateMeshes(template)
|
||||||
instance.position.y,
|
});
|
||||||
instance.position.z
|
|
||||||
);
|
for (const instancedMesh of instancedMeshes) {
|
||||||
model.rotation.set(0, MathUtils.degToRad(instance.yawDegrees), 0);
|
nextGroup.add(instancedMesh);
|
||||||
model.scale.setScalar(instance.scale);
|
}
|
||||||
model.userData.nonPickable = true;
|
|
||||||
model.userData.pathId = instance.pathId;
|
|
||||||
model.userData.pathRepeaterId = instance.repeaterId;
|
|
||||||
model.userData.splineCorridorAssetId = instance.asset.id;
|
|
||||||
applyRendererRenderCategoryFromMaterial(model);
|
|
||||||
nextGroup.add(model);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (requestId !== this.requestId) {
|
if (requestId !== this.requestId) {
|
||||||
disposeModelInstance(nextGroup);
|
this.disposeInstancedGroup(nextGroup);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.clearActiveGroup();
|
this.clearActiveGroup();
|
||||||
|
|
||||||
if (nextGroup.children.length === 0) {
|
if (nextGroup.children.length === 0) {
|
||||||
disposeModelInstance(nextGroup);
|
this.disposeInstancedGroup(nextGroup);
|
||||||
this.onRebuilt?.();
|
this.onRebuilt?.();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user