Use child mesh in SplatMesh to auto-inject SparkRenderer instead of monkey-patching (#158)

This commit is contained in:
Noeri Huisman
2025-09-15 23:54:50 +02:00
committed by GitHub
parent 350a677984
commit 0c07ee2fd4
5 changed files with 42 additions and 44 deletions
+1
View File
@@ -79,6 +79,7 @@
const canvas = document.getElementById("canvas"); const canvas = document.getElementById("canvas");
const renderer = new THREE.WebGLRenderer({ canvas }); const renderer = new THREE.WebGLRenderer({ canvas });
const spark = new SparkRenderer({ renderer }); const spark = new SparkRenderer({ renderer });
scene.add(spark);
function handleResize() { function handleResize() {
const width = canvas.clientWidth; const width = canvas.clientWidth;
+1
View File
@@ -61,6 +61,7 @@
// Explicitly create a SparkRenderer to render environment maps // Explicitly create a SparkRenderer to render environment maps
const spark = new SparkRenderer({ renderer }); const spark = new SparkRenderer({ renderer });
scene.add(spark);
const splatURL = await getAssetFileURL("fireplace.spz"); const splatURL = await getAssetFileURL("fireplace.spz");
const packedSplats = new PackedSplats({ url: splatURL }); const packedSplats = new PackedSplats({ url: splatURL });
+1
View File
@@ -47,6 +47,7 @@
// Explicitly create a SparkRenderer in the scene to spawn new viewpoints // Explicitly create a SparkRenderer in the scene to spawn new viewpoints
const spark = new SparkRenderer({ renderer }); const spark = new SparkRenderer({ renderer });
scene.add(spark);
const splatURL = await getAssetFileURL("butterfly.spz"); const splatURL = await getAssetFileURL("butterfly.spz");
const butterfly = new SplatMesh({ url: splatURL }); const butterfly = new SplatMesh({ url: splatURL });
-44
View File
@@ -40,48 +40,6 @@ import {
// of 5 to avoid excessive memory usage. // of 5 to avoid excessive memory usage.
const MAX_ACCUMULATORS = 5; const MAX_ACCUMULATORS = 5;
// Scene.onBeforeRender monkey-patch to
// inject a SparkRenderer into a scene with SplatMeshes if there isn't
// one already. Restore original Scene.onBeforeRenderer and Scene.add when done.
let hasSplatMesh = false;
let hasSparkRenderer = false;
let sparkRendererInstance: SparkRenderer;
function containsSplatMesh(object3D: THREE.Object3D) {
let hasSplatMesh = false;
if (object3D instanceof SplatMesh) {
return true;
}
object3D.traverse((child: THREE.Object3D) => {
hasSplatMesh = hasSplatMesh || child instanceof SplatMesh;
});
return hasSplatMesh;
}
const sceneAdd = THREE.Scene.prototype.add;
THREE.Scene.prototype.add = function (object: THREE.Object3D) {
hasSplatMesh = hasSplatMesh || containsSplatMesh(object);
hasSparkRenderer = hasSparkRenderer || object instanceof SparkRenderer;
sceneAdd.call(this, object);
return this;
};
const sceneOnBeforeRender = THREE.Scene.prototype.onBeforeRender;
THREE.Scene.prototype.onBeforeRender = function (
renderer: THREE.WebGLRenderer,
) {
if (!hasSplatMesh) {
return;
}
if (!hasSparkRenderer) {
const spark = sparkRendererInstance || new SparkRenderer({ renderer });
this.add(spark);
}
THREE.Scene.prototype.onBeforeRender = sceneOnBeforeRender;
THREE.Scene.prototype.add = sceneAdd;
};
export type SparkRendererOptions = { export type SparkRendererOptions = {
/** /**
* Pass in your THREE.WebGLRenderer instance so Spark can perform work * Pass in your THREE.WebGLRenderer instance so Spark can perform work
@@ -368,8 +326,6 @@ export class SparkRenderer extends THREE.Mesh {
this.prepareViewpoint(this.viewpoint); this.prepareViewpoint(this.viewpoint);
this.clock = options.clock ? cloneClock(options.clock) : new THREE.Clock(); this.clock = options.clock ? cloneClock(options.clock) : new THREE.Clock();
sparkRendererInstance = this;
} }
static makeUniforms() { static makeUniforms() {
+39
View File
@@ -7,6 +7,7 @@ import {
type SplatEncoding, type SplatEncoding,
} from "./PackedSplats"; } from "./PackedSplats";
import { type RgbaArray, readRgbaArray } from "./RgbaArray"; import { type RgbaArray, readRgbaArray } from "./RgbaArray";
import { SparkRenderer } from "./SparkRenderer";
import { SplatEdit, SplatEditSdf, SplatEdits } from "./SplatEdit"; import { SplatEdit, SplatEditSdf, SplatEdits } from "./SplatEdit";
import { import {
type GsplatModifier, type GsplatModifier,
@@ -235,6 +236,8 @@ export class SplatMesh extends SplatGenerator {
} }
} }
} }
this.add(createRendererDetectionMesh());
} }
async asyncInitialize(options: SplatMeshOptions) { async asyncInitialize(options: SplatMeshOptions) {
@@ -933,3 +936,39 @@ export function evaluateSH3(
`), `),
}).outputs.rgb; }).outputs.rgb;
} }
const EMPTY_GEOMETRY = new THREE.BufferGeometry();
const EMPTY_MATERIAL = new THREE.ShaderMaterial();
// Creates an empty mesh to hook into Three.js rendering.
// This is used to detect if a SparkRenderer is present in the scene.
// If not, one will be injected automatically.
function createRendererDetectionMesh(): THREE.Mesh {
const mesh = new THREE.Mesh(EMPTY_GEOMETRY, EMPTY_MATERIAL);
mesh.frustumCulled = false;
mesh.onBeforeRender = function (renderer, scene) {
if (!scene.isScene) {
// The SplatMesh is part of render call that doesn't have a Scene at its root
// Don't auto-inject a renderer.
this.removeFromParent();
return;
}
// Check if the scene has a SparkRenderer instance
let hasSparkRenderer = false;
scene.traverse((c) => {
if (c instanceof SparkRenderer) {
hasSparkRenderer = true;
}
});
if (!hasSparkRenderer) {
// No spark renderer present in the scene, inject one.
scene.add(new SparkRenderer({ renderer }));
}
// Remove mesh to stop checking
this.removeFromParent();
};
return mesh;
}