Custom Shaders
The PlayCanvas Engine lets you customize how Gaussian Splats are rendered by overriding the gsplatModifyVS shader chunk. The chunk is set on the scene-wide gsplat material (app.scene.gsplat.material), so a single custom shader applies to all splats in the scene.
View Live Example - See shader chunk customization in action with animated splats.
Overridable Functions
The gsplatModifyVS chunk lets you override three functions in the splat vertex stage:
| Function | Purpose |
|---|---|
modifySplatCenter | Transform the splat center position (model space) |
modifySplatRotationScale | Adjust the splat rotation quaternion and scale |
modifySplatColor | Transform the splat color and opacity |
- GLSL
- WGSL
void modifySplatCenter(inout vec3 center);
void modifySplatRotationScale(vec3 originalCenter, vec3 modifiedCenter, inout vec4 rotation, inout vec3 scale);
void modifySplatColor(vec3 center, inout vec4 color);
fn modifySplatCenter(center: ptr<function, vec3f>);
fn modifySplatRotationScale(originalCenter: vec3f, modifiedCenter: vec3f, rotation: ptr<function, vec4f>, scale: ptr<function, vec3f>);
fn modifySplatColor(center: vec3f, color: ptr<function, vec4f>);
You only need to implement the functions you want to change.
How the Example Works
The live example above animates every splat with a sine-wave displacement and a golden color pulse. It comes together in three steps.
1. Write the shader chunk, overriding the functions you need. The example animates using a uTime uniform:
- GLSL
- WGSL
uniform float uTime;
void modifySplatCenter(inout vec3 center) {
float heightIntensity = center.y * 0.2;
center.x += sin(uTime * 5.0 + center.y) * 0.3 * heightIntensity;
}
void modifySplatRotationScale(vec3 originalCenter, vec3 modifiedCenter, inout vec4 rotation, inout vec3 scale) {
// no modification
}
void modifySplatColor(vec3 center, inout vec4 clr) {
float sineValue = abs(sin(uTime * 5.0 + center.y));
vec3 gold = vec3(1.0, 0.85, 0.0);
float blend = smoothstep(0.9, 1.0, sineValue);
clr.xyz = mix(clr.xyz, gold, blend);
}
uniform uTime: f32;
fn modifySplatCenter(center: ptr<function, vec3f>) {
let heightIntensity = (*center).y * 0.2;
(*center).x += sin(uniform.uTime * 5.0 + (*center).y) * 0.3 * heightIntensity;
}
fn modifySplatRotationScale(originalCenter: vec3f, modifiedCenter: vec3f, rotation: ptr<function, vec4f>, scale: ptr<function, vec3f>) {
// no modification
}
fn modifySplatColor(center: vec3f, clr: ptr<function, vec4f>) {
let sineValue = abs(sin(uniform.uTime * 5.0 + center.y));
let gold = vec3f(1.0, 0.85, 0.0);
let blend = smoothstep(0.9, 1.0, sineValue);
(*clr) = vec4f(mix((*clr).xyz, gold, blend), (*clr).a);
}
2. Apply the chunk to the scene gsplat material, then update the material so it recompiles. Setting both the GLSL and WGSL chunk covers WebGL and WebGPU devices:
const sceneMat = app.scene.gsplat.material;
sceneMat.getShaderChunks('glsl').set('gsplatModifyVS', glslVertShader);
sceneMat.getShaderChunks('wgsl').set('gsplatModifyVS', wgslVertShader);
sceneMat.update();
3. Drive any uniforms each frame:
let currentTime = 0;
app.on('update', (dt) => {
currentTime += dt;
sceneMat.setParameter('uTime', currentTime);
sceneMat.update();
});
Removing a Custom Shader
To revert to default rendering, delete the chunk override and update the material:
const sceneMat = app.scene.gsplat.material;
sceneMat.getShaderChunks('glsl').delete('gsplatModifyVS');
sceneMat.getShaderChunks('wgsl').delete('gsplatModifyVS');
sceneMat.update();
See Also
- Work Buffer Rendering — customize the global render pass that draws the sorted splats