playcanvas
Version:
Open-source WebGL/WebGPU 3D engine for the web
228 lines (194 loc) • 7.13 kB
JavaScript
import { Script } from 'playcanvas';
/**
* Base class for gsplat shader effects.
* Handles common functionality like material management, shader application,
* time tracking, and uniform updates.
*
* **Usage:**
* Attach this script to an entity with a gsplat component. The script automatically detects
* whether the gsplat is in unified or non-unified mode and handles material access accordingly.
*
* **Non-Unified Mode (`unified=false`):**
* Each gsplat component has its own material. The script accesses the material directly via
* `entity.gsplat.material` and applies shader customizations immediately or when the asset loads.
*
* **Unified Mode (`unified=true`):**
* Multiple gsplat components share a template material accessible via `app.scene.gsplat.material`.
* The script applies shader customizations to this template material.
*
* **Enable/Disable:**
* When enabled, the shader effect is applied and effectTime starts tracking from 0.
* When disabled, the custom shader is removed and materials revert to default rendering.
*
* Subclasses must implement:
* - getShaderGLSL(): Return GLSL shader string
* - getShaderWGSL(): Return WGSL shader string
* - updateEffect(effectTime, dt): Update effect each frame
*
* @abstract
*/
class GsplatShaderEffect extends Script {
static scriptName = 'gsplatShaderEffect';
/**
* Time since effect was enabled
* @type {number}
*/
effectTime = 0;
/**
* The material this effect is applied to
* @type {import('playcanvas').Material | null}
*/
material = null;
initialize() {
this.initialized = false;
this.effectTime = 0;
this.material = null;
this.shadersNeedApplication = false;
// Listen to enable/disable events
this.on('enable', () => {
// Reset effect time when enabling
this.effectTime = 0;
// Ensure we're initialized
if (!this.initialized && this.entity.gsplat) {
this.initialized = true;
}
// Apply shaders if initialized, otherwise flag for application
if (this.initialized) {
this.applyShaders();
} else {
this.shadersNeedApplication = true;
}
});
this.on('disable', () => {
// Remove shaders when disabling
this.removeShaders();
});
if (!this.entity.gsplat) {
// gsplat component not yet available, will retry each frame
return;
}
this.initialized = true;
// Apply shaders immediately since we're enabled by default
if (this.enabled) {
this.applyShaders();
}
}
applyShaders() {
if (this.entity.gsplat?.unified) {
// Unified mode: Apply to template material
this.applyToUnifiedMaterials();
} else {
// Non-unified mode: Apply to component's material
this.applyToComponentMaterial();
}
}
removeShaders() {
if (!this.material) return;
const device = this.app.graphicsDevice;
const shaderLanguage = device?.isWebGPU ? 'wgsl' : 'glsl';
this.material.getShaderChunks(shaderLanguage).delete('gsplatModifyVS');
this.material.update();
this.material = null;
}
applyToComponentMaterial() {
const applyShader = () => {
this.material = this.entity.gsplat?.material ?? null;
if (!this.material) {
console.error(`${this.constructor.name}: gsplat material not available.`);
return;
}
this.applyShaderToMaterial(this.material);
};
if (this.entity.gsplat?.material) {
applyShader();
} else {
// Listen for when the gsplat component is ready
this.entity.gsplat?.once('load', applyShader);
}
}
applyToUnifiedMaterials() {
this.material = this.app.scene.gsplat?.material ?? null;
if (!this.material) {
console.warn(`${this.constructor.name}: gsplat template material not available.`);
return;
}
this.applyShaderToMaterial(this.material);
}
applyShaderToMaterial(material) {
const device = this.app.graphicsDevice;
const shaderLanguage = device?.isWebGPU ? 'wgsl' : 'glsl';
const customShader = shaderLanguage === 'wgsl' ? this.getShaderWGSL() : this.getShaderGLSL();
material.getShaderChunks(shaderLanguage).set('gsplatModifyVS', customShader);
material.update();
}
update(dt) {
// If not initialized, try to complete initialization
if (!this.initialized) {
if (this.entity.gsplat) {
this.initialized = true;
// Apply shaders now if we're enabled and they're needed
if (this.enabled && this.shadersNeedApplication) {
this.applyShaders();
this.shadersNeedApplication = false;
}
}
return; // Don't proceed with updates until initialized
}
// Apply shaders if they're needed (can happen if enabled after initialization)
if (this.shadersNeedApplication) {
this.applyShaders();
this.shadersNeedApplication = false;
}
if (!this.material) return;
// Update time
this.effectTime += dt;
// Let subclass update the effect
this.updateEffect(this.effectTime, dt);
// Update material after all parameters have been set (if still valid)
// Note: material may be set to null by removeShaders() if effect disables itself
if (this.material) {
this.material.update();
}
}
destroy() {
// Remove shaders if they're still applied
this.removeShaders();
}
/**
* Get the GLSL shader string.
* Must be implemented by subclasses.
* @returns {string} GLSL shader code
* @abstract
*/
getShaderGLSL() {
throw new Error(`${this.constructor.name} must implement getShaderGLSL()`);
}
/**
* Get the WGSL shader string.
* Must be implemented by subclasses.
* @returns {string} WGSL shader code
* @abstract
*/
getShaderWGSL() {
throw new Error(`${this.constructor.name} must implement getShaderWGSL()`);
}
/**
* Set a uniform value on the material.
* @param {string} name - The uniform name
* @param {*} value - The uniform value
*/
setUniform(name, value) {
this.material?.setParameter(name, value);
}
/**
* Update effect each frame.
* Must be implemented by subclasses if they need to update uniforms or check completion.
* @param {number} effectTime - Time since effect was enabled in seconds
* @param {number} dt - Delta time in seconds
* @abstract
*/
updateEffect(effectTime, dt) {
// Optional to override
}
}
export { GsplatShaderEffect };