@@ -14,9 +14,8 @@ import { Script } from 'playcanvas';
1414 * `entity.gsplat.material` and applies shader customizations immediately or when the asset loads.
1515 *
1616 * **Unified Mode (`unified=true`):**
17- * Multiple gsplat components share materials per camera/layer combination. Materials are created
18- * during the first frame render. The script listens to the 'material:created' event for immediate
19- * notification and retries each frame as fallback to ensure materials are applied.
17+ * Multiple gsplat components share a template material accessible via `app.scene.gsplat.material`.
18+ * The script applies shader customizations to this template material.
2019 *
2120 * **Enable/Disable:**
2221 * When enabled, the shader effect is applied and effectTime starts tracking from 0.
@@ -32,30 +31,22 @@ import { Script } from 'playcanvas';
3231class GsplatShaderEffect extends Script {
3332 static scriptName = 'gsplatShaderEffect' ;
3433
35- /**
36- * Optional camera entity to target in unified mode. If not set, applies to all cameras.
37- *
38- * @attribute
39- * @type {import('playcanvas').Entity | null }
40- */
41- camera = null ;
42-
4334 /**
4435 * Time since effect was enabled
4536 * @type {number }
4637 */
4738 effectTime = 0 ;
4839
4940 /**
50- * Set of materials with applied shader
51- * @type {Set< import('playcanvas').Material> }
41+ * The material this effect is applied to
42+ * @type {import('playcanvas').Material | null }
5243 */
53- materialsApplied = new Set ( ) ;
44+ material = null ;
5445
5546 initialize ( ) {
5647 this . initialized = false ;
5748 this . effectTime = 0 ;
58- this . materialsApplied . clear ( ) ;
49+ this . material = null ;
5950 this . shadersNeedApplication = false ;
6051
6152 // Listen to enable/disable events
@@ -81,10 +72,6 @@ class GsplatShaderEffect extends Script {
8172 this . removeShaders ( ) ;
8273 } ) ;
8374
84- // Register event listener immediately for unified mode to catch materials created on first frame
85- // This is safe to call even if the gsplat component doesn't exist yet
86- this . setupUnifiedEventListener ( ) ;
87-
8875 if ( ! this . entity . gsplat ) {
8976 // gsplat component not yet available, will retry each frame
9077 return ;
@@ -100,7 +87,7 @@ class GsplatShaderEffect extends Script {
10087
10188 applyShaders ( ) {
10289 if ( this . entity . gsplat ?. unified ) {
103- // Unified mode: Apply to specified camera (or all cameras if not specified)
90+ // Unified mode: Apply to template material
10491 this . applyToUnifiedMaterials ( ) ;
10592 } else {
10693 // Non-unified mode: Apply to component's material
@@ -109,63 +96,24 @@ class GsplatShaderEffect extends Script {
10996 }
11097
11198 removeShaders ( ) {
112- if ( this . materialsApplied . size === 0 ) return ;
99+ if ( ! this . material ) return ;
113100
114101 const device = this . app . graphicsDevice ;
115102 const shaderLanguage = device ?. isWebGPU ? 'wgsl' : 'glsl' ;
116103
117- // Remove custom shader chunk from all materials
118- this . materialsApplied . forEach ( ( material ) => {
119- material . getShaderChunks ( shaderLanguage ) . delete ( 'gsplatCustomizeVS' ) ;
120- material . update ( ) ;
121- } ) ;
122-
123- // Clear the set and stop tracking
124- this . materialsApplied . clear ( ) ;
125- }
126-
127- setupUnifiedEventListener ( ) {
128- // Only set up once
129- if ( this . _materialCreatedHandler ) return ;
130-
131- // @ts -ignore - gsplat system exists at runtime
132- const gsplatSystem = this . app . systems . gsplat ;
133-
134- // Set up event listener
135- this . _materialCreatedHandler = ( material , camera , layer ) => {
136- // Only apply if enabled
137- if ( ! this . enabled ) return ;
138-
139- // Apply shader immediately when material is created
140- // The gsplat component may not be fully initialized yet, so we can't check it here
141- if ( ! this . materialsApplied . has ( material ) ) {
142- // Check camera filter if specified
143- if ( this . camera && this . camera . camera && this . camera . camera . camera !== camera ) {
144- return ;
145- }
146-
147- this . applyShaderToMaterial ( material ) ;
148- this . materialsApplied . add ( material ) ;
149-
150- // Store layer info for potential validation later
151- if ( ! this . _materialLayers ) {
152- this . _materialLayers = new Map ( ) ;
153- }
154- this . _materialLayers . set ( material , layer . id ) ;
155- }
156- } ;
157-
158- gsplatSystem . on ( 'material:created' , this . _materialCreatedHandler ) ;
104+ this . material . getShaderChunks ( shaderLanguage ) . delete ( 'gsplatCustomizeVS' ) ;
105+ this . material . update ( ) ;
106+ this . material = null ;
159107 }
160108
161109 applyToComponentMaterial ( ) {
162110 const applyShader = ( ) => {
163- const material = this . entity . gsplat ?. material ;
164- if ( ! material ) {
111+ this . material = this . entity . gsplat ?. material ?? null ;
112+ if ( ! this . material ) {
165113 console . error ( `${ this . constructor . name } : gsplat material not available.` ) ;
166114 return ;
167115 }
168- this . applyShaderToMaterial ( material ) ;
116+ this . applyShaderToMaterial ( this . material ) ;
169117 } ;
170118
171119 if ( this . entity . gsplat ?. material ) {
@@ -177,58 +125,13 @@ class GsplatShaderEffect extends Script {
177125 }
178126
179127 applyToUnifiedMaterials ( ) {
180- // Try to apply immediately to any existing materials
181- this . updateUnifiedMaterials ( ) ;
182-
183- // If no materials yet, set retry flag (event listener is already set up)
184- if ( this . materialsApplied . size === 0 ) {
185- this . needsRetry = true ;
186- }
187- }
188-
189- updateUnifiedMaterials ( ) {
190- // @ts -ignore - gsplat system exists at runtime
191- const gsplatSystem = this . app . systems . gsplat ;
192- const scene = this . app . scene ;
193- const composition = scene . layers ;
194-
195- // Get all layers this component is on
196- const componentLayers = this . entity . gsplat ?. layers ;
197- if ( ! componentLayers ) return ;
198-
199- // Determine which cameras to target
200- let targetCameras ;
201- const cam = this . camera ?. camera ?. camera ;
202- if ( cam ) {
203- // Specific camera specified via attribute
204- targetCameras = [ cam ] ;
205- } else {
206- // All cameras in the composition
207- targetCameras = composition . cameras . map ( cameraComponent => cameraComponent . camera ) ;
128+ this . material = this . app . scene . gsplat ?. material ?? null ;
129+ if ( ! this . material ) {
130+ console . warn ( `${ this . constructor . name } : gsplat template material not available.` ) ;
131+ return ;
208132 }
209133
210- // Iterate through target cameras (already Camera objects, not CameraComponents)
211- targetCameras . forEach ( ( camera ) => {
212- // For each layer this component is on
213- componentLayers . forEach ( ( layerId ) => {
214- // Check if this camera renders this layer
215- if ( camera . layers . indexOf ( layerId ) >= 0 ) {
216- const layer = composition . getLayerById ( layerId ) ;
217- if ( layer ) {
218- const material = gsplatSystem . getGSplatMaterial ( camera , layer ) ;
219- if ( material && ! this . materialsApplied . has ( material ) ) {
220- this . applyShaderToMaterial ( material ) ;
221- this . materialsApplied . add ( material ) ;
222- }
223- }
224- }
225- } ) ;
226- } ) ;
227-
228- if ( this . materialsApplied . size > 0 ) {
229- this . needsRetry = false ;
230- // Keep event listener active to catch any new materials created later
231- }
134+ this . applyShaderToMaterial ( this . material ) ;
232135 }
233136
234137 applyShaderToMaterial ( material ) {
@@ -260,30 +163,24 @@ class GsplatShaderEffect extends Script {
260163 this . shadersNeedApplication = false ;
261164 }
262165
263- // Retry applying to unified materials if needed
264- if ( this . entity . gsplat ?. unified && this . needsRetry ) {
265- this . updateUnifiedMaterials ( ) ;
266- }
267-
268- if ( this . materialsApplied . size === 0 ) return ;
166+ if ( ! this . material ) return ;
269167
270168 // Update time
271169 this . effectTime += dt ;
272170
273171 // Let subclass update the effect
274172 this . updateEffect ( this . effectTime , dt ) ;
173+
174+ // Update material after all parameters have been set (if still valid)
175+ // Note: material may be set to null by removeShaders() if effect disables itself
176+ if ( this . material ) {
177+ this . material . update ( ) ;
178+ }
275179 }
276180
277181 destroy ( ) {
278182 // Remove shaders if they're still applied
279183 this . removeShaders ( ) ;
280-
281- // Clean up event listener
282- if ( this . _materialCreatedHandler ) {
283- // @ts -ignore - gsplat system exists at runtime
284- this . app . systems . gsplat . off ( 'material:created' , this . _materialCreatedHandler ) ;
285- this . _materialCreatedHandler = null ;
286- }
287184 }
288185
289186 /**
@@ -307,14 +204,12 @@ class GsplatShaderEffect extends Script {
307204 }
308205
309206 /**
310- * Set a uniform value on all applied materials .
207+ * Set a uniform value on the material .
311208 * @param {string } name - The uniform name
312209 * @param {* } value - The uniform value
313210 */
314211 setUniform ( name , value ) {
315- this . materialsApplied . forEach ( ( material ) => {
316- material . setParameter ( name , value ) ;
317- } ) ;
212+ this . material ?. setParameter ( name , value ) ;
318213 }
319214
320215 /**
0 commit comments