playcanvas
Version:
PlayCanvas WebGL game engine
468 lines (396 loc) • 14.8 kB
JavaScript
import { Vec3, Color } from 'playcanvas';
import { GsplatShaderEffect } from './gsplat-shader-effect.mjs';
const shaderGLSL = /* glsl */`
uniform float uTime;
uniform vec3 uCenter;
uniform float uDistance;
uniform float uSpeed;
uniform float uAcceleration;
uniform float uFlightTime;
uniform float uRainSize;
uniform float uRotation;
uniform vec3 uFallTint;
uniform float uFallTintIntensity;
uniform vec3 uHitTint;
uniform float uHitDuration;
uniform float uEndRadius;
// Shared globals (initialized once per vertex)
float g_dist2D;
float g_dist3D;
float g_tStart;
float g_tLand;
// Solve: distance = speed * t + 0.5 * acceleration * t²
float solveWaveTime(float dist) {
if (uAcceleration == 0.0) {
return dist / uSpeed;
} else {
// Quadratic formula: t = (-v + sqrt(v² + 2ad)) / a
float discriminant = uSpeed * uSpeed + 2.0 * uAcceleration * dist;
return (-uSpeed + sqrt(max(discriminant, 0.0))) / uAcceleration;
}
}
void initShared(vec3 center) {
vec2 center2D = center.xz;
vec2 origin2D = uCenter.xz;
g_dist2D = length(center2D - origin2D);
g_dist3D = length(center - uCenter);
g_tStart = solveWaveTime(g_dist2D);
g_tLand = g_tStart + uFlightTime;
}
void modifyCenter(inout vec3 center) {
vec3 originalCenter = center;
initShared(center);
// Check if animation is complete (after landing transition)
float timeSinceLanding = uTime - g_tLand;
if (timeSinceLanding >= 0.5) return; // Effect complete, no modifications
// Early exit optimization during animation
if (g_dist3D > uEndRadius) return;
// If not started yet, do nothing (will be invisible)
if (uTime < g_tStart) return;
// If falling: interpolate Y from elevated to original and apply rotation
if (uTime < g_tLand) {
float fallProgress = (uTime - g_tStart) / (g_tLand - g_tStart);
// Vertical movement
center.y += uDistance * (1.0 - fallProgress);
// Rotation around Y axis through uCenter
float angle = fallProgress * uRotation * 6.283185; // uRotation * 2π
vec3 offset = originalCenter - uCenter;
float cosAngle = cos(angle);
float sinAngle = sin(angle);
offset.x = originalCenter.x - uCenter.x;
offset.z = originalCenter.z - uCenter.z;
center.x = uCenter.x + offset.x * cosAngle - offset.z * sinAngle;
center.z = uCenter.z + offset.x * sinAngle + offset.z * cosAngle;
}
// If landed: stay at original position (no change needed)
}
void modifyCovariance(vec3 originalCenter, vec3 modifiedCenter, inout vec3 covA, inout vec3 covB) {
// Check if animation is complete (after landing transition)
float timeSinceLanding = uTime - g_tLand;
if (timeSinceLanding >= 0.5) return; // Effect complete, no modifications
// Early exit for distant splats during animation
if (g_dist3D > uEndRadius) {
gsplatMakeRound(covA, covB, 0.0);
return;
}
// Before 2D wave reaches: invisible
if (uTime < g_tStart) {
gsplatMakeRound(covA, covB, 0.0);
return;
}
// During fall and transition after landing
if (timeSinceLanding < 0.5) {
// Still falling or transitioning
float originalSize = gsplatExtractSize(covA, covB);
if (timeSinceLanding < 0.0) {
// Falling: small round dots
gsplatMakeRound(covA, covB, min(uRainSize, originalSize));
} else {
// Landing transition: lerp from dots to original over 0.5s
float t = timeSinceLanding * 2.0; // normalize [0, 0.5] to [0, 1]
float size = mix(uRainSize, originalSize, t);
// Lerp between round and original shape
vec3 origCovA = covA;
vec3 origCovB = covB;
gsplatMakeRound(covA, covB, size);
covA = mix(covA, origCovA, t);
covB = mix(covB, origCovB, t);
}
}
// After transition: original shape/size (no-op)
}
void modifyColor(vec3 center, inout vec4 color) {
// Check if animation is complete
float timeSinceLanding = uTime - g_tLand;
if (timeSinceLanding >= uHitDuration) return; // Effect complete, no modifications
// Early exit for distant splats during animation
if (g_dist3D > uEndRadius) return;
// Before wave reaches: no color change (invisible anyway)
if (uTime < g_tStart) return;
if (timeSinceLanding < 0.0) {
// Falling: blend between original and fall tint
color.rgb = mix(color.rgb, uFallTint, uFallTintIntensity);
} else if (timeSinceLanding < uHitDuration) {
// Landing: apply hit tint, fade out
float fadeOut = 1.0 - (timeSinceLanding / uHitDuration);
color.rgb += uHitTint * fadeOut;
}
// After hit duration: original color (no change)
}
`;
const shaderWGSL = /* wgsl */`
uniform uTime: f32;
uniform uCenter: vec3f;
uniform uDistance: f32;
uniform uSpeed: f32;
uniform uAcceleration: f32;
uniform uFlightTime: f32;
uniform uRainSize: f32;
uniform uRotation: f32;
uniform uFallTint: vec3f;
uniform uFallTintIntensity: f32;
uniform uHitTint: vec3f;
uniform uHitDuration: f32;
uniform uEndRadius: f32;
// Shared globals (initialized once per vertex)
var<private> g_dist2D: f32;
var<private> g_dist3D: f32;
var<private> g_tStart: f32;
var<private> g_tLand: f32;
// Solve: distance = speed * t + 0.5 * acceleration * t²
fn solveWaveTime(dist: f32) -> f32 {
if (uniform.uAcceleration == 0.0) {
return dist / uniform.uSpeed;
} else {
// Quadratic formula: t = (-v + sqrt(v² + 2ad)) / a
let discriminant = uniform.uSpeed * uniform.uSpeed + 2.0 * uniform.uAcceleration * dist;
return (-uniform.uSpeed + sqrt(max(discriminant, 0.0))) / uniform.uAcceleration;
}
}
fn initShared(center: vec3f) {
let center2D = center.xz;
let origin2D = uniform.uCenter.xz;
g_dist2D = length(center2D - origin2D);
g_dist3D = length(center - uniform.uCenter);
g_tStart = solveWaveTime(g_dist2D);
g_tLand = g_tStart + uniform.uFlightTime;
}
fn modifyCenter(center: ptr<function, vec3f>) {
let originalCenter = *center;
initShared(*center);
// Check if animation is complete (after landing transition)
let timeSinceLanding = uniform.uTime - g_tLand;
if (timeSinceLanding >= 0.5) {
return; // Effect complete, no modifications
}
// Early exit optimization during animation
if (g_dist3D > uniform.uEndRadius) {
return;
}
// If not started yet, do nothing (will be invisible)
if (uniform.uTime < g_tStart) {
return;
}
// If falling: interpolate Y from elevated to original and apply rotation
if (uniform.uTime < g_tLand) {
let fallProgress = (uniform.uTime - g_tStart) / (g_tLand - g_tStart);
// Vertical movement
(*center).y += uniform.uDistance * (1.0 - fallProgress);
// Rotation around Y axis through uCenter
let angle = fallProgress * uniform.uRotation * 6.283185; // uRotation * 2π
let offset = originalCenter - uniform.uCenter;
let cosAngle = cos(angle);
let sinAngle = sin(angle);
let offsetX = originalCenter.x - uniform.uCenter.x;
let offsetZ = originalCenter.z - uniform.uCenter.z;
(*center).x = uniform.uCenter.x + offsetX * cosAngle - offsetZ * sinAngle;
(*center).z = uniform.uCenter.z + offsetX * sinAngle + offsetZ * cosAngle;
}
// If landed: stay at original position (no change needed)
}
fn modifyCovariance(originalCenter: vec3f, modifiedCenter: vec3f, covA: ptr<function, vec3f>, covB: ptr<function, vec3f>) {
// Check if animation is complete (after landing transition)
let timeSinceLanding = uniform.uTime - g_tLand;
if (timeSinceLanding >= 0.5) {
return; // Effect complete, no modifications
}
// Early exit for distant splats during animation
if (g_dist3D > uniform.uEndRadius) {
gsplatMakeRound(covA, covB, 0.0);
return;
}
// Before 2D wave reaches: invisible
if (uniform.uTime < g_tStart) {
gsplatMakeRound(covA, covB, 0.0);
return;
}
// During fall and transition after landing
if (timeSinceLanding < 0.5) {
// Still falling or transitioning
let originalSize = gsplatExtractSize(*covA, *covB);
if (timeSinceLanding < 0.0) {
// Falling: small round dots
gsplatMakeRound(covA, covB, min(uniform.uRainSize, originalSize));
} else {
// Landing transition: lerp from dots to original over 0.5s
let t = timeSinceLanding * 2.0; // normalize [0, 0.5] to [0, 1]
let size = mix(uniform.uRainSize, originalSize, t);
// Lerp between round and original shape
let origCovA = *covA;
let origCovB = *covB;
gsplatMakeRound(covA, covB, size);
*covA = mix(*covA, origCovA, t);
*covB = mix(*covB, origCovB, t);
}
}
// After transition: original shape/size (no-op)
}
fn modifyColor(center: vec3f, color: ptr<function, vec4f>) {
// Check if animation is complete
let timeSinceLanding = uniform.uTime - g_tLand;
if (timeSinceLanding >= uniform.uHitDuration) {
return; // Effect complete, no modifications
}
// Early exit for distant splats during animation
if (g_dist3D > uniform.uEndRadius) {
return;
}
// Before wave reaches: no color change (invisible anyway)
if (uniform.uTime < g_tStart) {
return;
}
if (timeSinceLanding < 0.0) {
// Falling: blend between original and fall tint
(*color) = vec4f(mix((*color).rgb, uniform.uFallTint, uniform.uFallTintIntensity), (*color).a);
} else if (timeSinceLanding < uniform.uHitDuration) {
// Landing: apply hit tint, fade out
let fadeOut = 1.0 - (timeSinceLanding / uniform.uHitDuration);
(*color) = vec4f((*color).rgb + uniform.uHitTint * fadeOut, (*color).a);
}
// After hit duration: original color (no change)
}
`;
/**
* Rain reveal effect for gaussian splats.
* Splats appear as small dots at an elevated position and fall down to land
* when an expanding 3D sphere wave reaches them.
*
* @example
* // Add the script to a gsplat entity
* entity.addComponent('script');
* const rainScript = entity.script.create(GsplatRevealRain);
* rainScript.center.set(0, 0, 0);
* rainScript.distance = 5;
* rainScript.speed = 3;
*/
class GsplatRevealRain extends GsplatShaderEffect {
static scriptName = 'gsplatRevealRain';
// Reusable arrays for uniform updates
_centerArray = [0, 0, 0];
_fallTintArray = [0, 0, 0];
_hitTintArray = [0, 0, 0];
/**
* Origin point for the wave
* @attribute
*/
center = new Vec3(0, 0, 0);
/**
* Elevation above target position where splats start
* @attribute
* @range [0, 50]
*/
distance = 30;
/**
* Wave speed in units/second
* @attribute
* @range [0, 10]
*/
speed = 2;
/**
* Speed increase over time
* @attribute
* @range [0, 5]
*/
acceleration = 0;
/**
* Duration of fall in seconds
* @attribute
* @range [0.1, 5]
*/
flightTime = 2;
/**
* Size of particles while falling
* @attribute
* @range [0, 0.1]
*/
rainSize = 0.015;
/**
* Rotation amount during fall (fraction of full circle, 0.9 = 90%)
* @attribute
* @range [0, 2]
*/
rotation = 0.9;
/**
* Color during fall
* @attribute
*/
fallTint = new Color(0, 1, 1);
/**
* Blend intensity between original color and fall tint (0=original, 1=full tint)
* @attribute
* @range [0, 1]
*/
fallTintIntensity = 0.2;
/**
* Additive color on landing (flash)
* @attribute
*/
hitTint = new Color(2, 0, 0);
/**
* Duration of hit tint flash in seconds
* @attribute
* @range [0, 2]
*/
hitDuration = 0.5;
/**
* Distance at which to disable effect for performance
* @attribute
* @range [0, 500]
*/
endRadius = 25;
getShaderGLSL() {
return shaderGLSL;
}
getShaderWGSL() {
return shaderWGSL;
}
updateEffect(effectTime, dt) {
// Check if effect is complete and disable if so
if (this.isEffectComplete()) {
this.enabled = false;
return;
}
this.setUniform('uTime', effectTime);
this._centerArray[0] = this.center.x;
this._centerArray[1] = this.center.y;
this._centerArray[2] = this.center.z;
this.setUniform('uCenter', this._centerArray);
this.setUniform('uDistance', this.distance);
this.setUniform('uSpeed', this.speed);
this.setUniform('uAcceleration', this.acceleration);
this.setUniform('uFlightTime', this.flightTime);
this.setUniform('uRainSize', this.rainSize);
this.setUniform('uRotation', this.rotation);
this._fallTintArray[0] = this.fallTint.r;
this._fallTintArray[1] = this.fallTint.g;
this._fallTintArray[2] = this.fallTint.b;
this.setUniform('uFallTint', this._fallTintArray);
this.setUniform('uFallTintIntensity', this.fallTintIntensity);
this._hitTintArray[0] = this.hitTint.r;
this._hitTintArray[1] = this.hitTint.g;
this._hitTintArray[2] = this.hitTint.b;
this.setUniform('uHitTint', this._hitTintArray);
this.setUniform('uHitDuration', this.hitDuration);
this.setUniform('uEndRadius', this.endRadius);
}
/**
* Calculate when the effect is complete.
* Effect completes when the furthest splat within endRadius has finished animating.
* @returns {boolean} True if effect is complete
*/
isEffectComplete() {
// Calculate time for furthest splat within endRadius
let maxTStart;
if (this.acceleration === 0) {
maxTStart = this.endRadius / this.speed;
} else {
// Quadratic formula: t = (-v + sqrt(v² + 2ad)) / a
const discriminant = this.speed * this.speed + 2.0 * this.acceleration * this.endRadius;
maxTStart = (-this.speed + Math.sqrt(Math.max(discriminant, 0.0))) / this.acceleration;
}
const maxTLand = maxTStart + this.flightTime;
const maxCompletionTime = maxTLand + Math.max(0.5, this.hitDuration);
return this.effectTime >= maxCompletionTime;
}
}
export { GsplatRevealRain };