From 0c07ee2fd43ff4fce825a380db3a1596562a43d6 Mon Sep 17 00:00:00 2001 From: Noeri Huisman <8823461+mrxz@users.noreply.github.com> Date: Mon, 15 Sep 2025 23:54:50 +0200 Subject: [PATCH] Use child mesh in `SplatMesh` to auto-inject `SparkRenderer` instead of monkey-patching (#158) --- examples/editor/index.html | 1 + examples/envmap/index.html | 1 + examples/multiple-viewpoints/index.html | 1 + src/SparkRenderer.ts | 44 ------------------------- src/SplatMesh.ts | 39 ++++++++++++++++++++++ 5 files changed, 42 insertions(+), 44 deletions(-) diff --git a/examples/editor/index.html b/examples/editor/index.html index 946ab9d..fc5a851 100644 --- a/examples/editor/index.html +++ b/examples/editor/index.html @@ -79,6 +79,7 @@ const canvas = document.getElementById("canvas"); const renderer = new THREE.WebGLRenderer({ canvas }); const spark = new SparkRenderer({ renderer }); + scene.add(spark); function handleResize() { const width = canvas.clientWidth; diff --git a/examples/envmap/index.html b/examples/envmap/index.html index 60c08a3..b260e1f 100644 --- a/examples/envmap/index.html +++ b/examples/envmap/index.html @@ -61,6 +61,7 @@ // Explicitly create a SparkRenderer to render environment maps const spark = new SparkRenderer({ renderer }); + scene.add(spark); const splatURL = await getAssetFileURL("fireplace.spz"); const packedSplats = new PackedSplats({ url: splatURL }); diff --git a/examples/multiple-viewpoints/index.html b/examples/multiple-viewpoints/index.html index c3ff97e..90984c2 100644 --- a/examples/multiple-viewpoints/index.html +++ b/examples/multiple-viewpoints/index.html @@ -47,6 +47,7 @@ // Explicitly create a SparkRenderer in the scene to spawn new viewpoints const spark = new SparkRenderer({ renderer }); + scene.add(spark); const splatURL = await getAssetFileURL("butterfly.spz"); const butterfly = new SplatMesh({ url: splatURL }); diff --git a/src/SparkRenderer.ts b/src/SparkRenderer.ts index 6562ec3..226aef8 100644 --- a/src/SparkRenderer.ts +++ b/src/SparkRenderer.ts @@ -40,48 +40,6 @@ import { // of 5 to avoid excessive memory usage. 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 = { /** * 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.clock = options.clock ? cloneClock(options.clock) : new THREE.Clock(); - - sparkRendererInstance = this; } static makeUniforms() { diff --git a/src/SplatMesh.ts b/src/SplatMesh.ts index 2940b16..3ab66cf 100644 --- a/src/SplatMesh.ts +++ b/src/SplatMesh.ts @@ -7,6 +7,7 @@ import { type SplatEncoding, } from "./PackedSplats"; import { type RgbaArray, readRgbaArray } from "./RgbaArray"; +import { SparkRenderer } from "./SparkRenderer"; import { SplatEdit, SplatEditSdf, SplatEdits } from "./SplatEdit"; import { type GsplatModifier, @@ -235,6 +236,8 @@ export class SplatMesh extends SplatGenerator { } } } + + this.add(createRendererDetectionMesh()); } async asyncInitialize(options: SplatMeshOptions) { @@ -933,3 +936,39 @@ export function evaluateSH3( `), }).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; +}