snow.ts
examples/spark/js/snow.ts · raw · open the example · all examples · site home
Exports: DEFAULT_SNOWDEFAULT_RAINSnowOptionssnowBox
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123
// Spark's generators.snowBox (src/generators/snow.ts) as a TSL
// SplatGenerator: numSplats particles drifting through a box, wrapping
// around it, each wandering on its own phase. The returned uniforms have
// Spark's names (.value to change them); call update(seconds, delta) every
// frame. snow is a SplatModel to add to the scene and a SplatSet.
import * as THREE from "three";
import { GpuSplats, TSL, type TslNode } from "@sparkjsdev/radix";
import { SplatModel } from "./objects.js";
const { float, fract, hash, max, mix, sin, uint, uniform, vec3, vec4 } = TSL;
export const DEFAULT_SNOW = {
box: new THREE.Box3(new THREE.Vector3(-1, -1, -1), new THREE.Vector3(1, 1, 1)),
density: 100,
fallDirection: new THREE.Vector3(-1, -3, 1).normalize(),
fallVelocity: 0.02,
wanderScale: 0.04,
wanderVariance: 2,
color1: new THREE.Color(1, 1, 1),
color2: new THREE.Color(0.5, 0.5, 1),
minScale: 0.001,
maxScale: 0.005,
anisoScale: new THREE.Vector3(1, 1, 1),
};
export const DEFAULT_RAIN = {
box: new THREE.Box3(new THREE.Vector3(-2, -1, -2), new THREE.Vector3(2, 5, 2)),
density: 10,
fallDirection: new THREE.Vector3(0, -1, 0),
fallVelocity: 2,
wanderScale: 0.1,
wanderVariance: 1,
color1: new THREE.Color(1, 1, 1),
color2: new THREE.Color(0.25, 0.25, 0.5),
minScale: 0.005,
maxScale: 0.01,
anisoScale: new THREE.Vector3(0.1, 1, 0.1),
};
// Four hashes in [0, 1) of a uint seed and a salt
const hashVec4 = (seed: TslNode, salt: number) => vec4(
hash(seed.mul(uint(4)).add(uint(salt))),
hash(seed.mul(uint(4)).add(uint(salt + 1))),
hash(seed.mul(uint(4)).add(uint(salt + 2))),
hash(seed.mul(uint(4)).add(uint(salt + 3))),
);
export interface SnowOptions {
box?: THREE.Box3;
minY?: number;
numSplats?: number;
density?: number;
anisoScale?: THREE.Vector3;
minScale?: number;
maxScale?: number;
fallDirection?: THREE.Vector3;
fallVelocity?: number;
wanderScale?: number;
wanderVariance?: number;
color1?: THREE.Color;
color2?: THREE.Color;
opacity?: number;
}
export function snowBox({
box = DEFAULT_SNOW.box, minY, numSplats, density = 100, anisoScale, minScale = 0.001, maxScale = 0.005, fallDirection,
fallVelocity = 0.02, wanderScale = 0.01, wanderVariance = 2, color1, color2, opacity = 1,
}: SnowOptions = {}) {
const size = box.getSize(new THREE.Vector3());
numSplats ??= Math.max(1, Math.min(1000000, Math.round(size.x * size.y * size.z * density)));
const toVector = ({ r, g, b }: THREE.Color) => new THREE.Vector3(r, g, b);
const u = {
min: uniform(box.min.clone()),
max: uniform(box.max.clone()),
// Spark's -Infinity; a large finite value keeps shader math finite
minY: uniform(minY ?? -1e30),
color1: uniform(toVector(color1 ?? new THREE.Color(1, 1, 1))),
color2: uniform(toVector(color2 ?? new THREE.Color(0.5, 0.5, 1))),
opacity: uniform(opacity),
fallVelocity: uniform(fallVelocity),
wanderVariance: uniform(wanderVariance),
wanderScale: uniform(wanderScale),
fallDirection: uniform((fallDirection ?? new THREE.Vector3(0, -1, 0)).clone().normalize()),
minScale: uniform(minScale),
maxScale: uniform(maxScale),
anisoScale: uniform((anisoScale?.clone() ?? new THREE.Vector3(1, 1, 1)).normalize()),
};
const time = uniform(0);
const offset = uniform(new THREE.Vector3());
const generator = {
generateSplat(index: TslNode) {
const random = hashVec4(index, 0).toVar();
const w = random.w;
const wave = sin(fract(w.mul(100)).mul(Math.PI));
const scales = u.anisoScale.mul(u.minScale.add(wave.mul(u.maxScale.sub(u.minScale))));
const intensity = fract(w.mul(10));
const rgb = mix(u.color1, u.color2, fract(w)).mul(intensity);
const random2 = hashVec4(index, 0x1ab5).toVar();
const phase = time.add(random2.w.mul(u.wanderVariance));
const position = u.min.add(u.max.sub(u.min).mul(fract(random.xyz.add(offset))));
const perturb = sin(vec3(phase).add(random2.xyz)).mul(u.wanderScale);
const center = position.add(perturb).toVar();
const y = max(u.minY, center.y);
return GpuSplats.Splat(vec3(center.x, y, center.z), scales, vec4(0, 0, 0, 1), rgb, float(u.opacity), uint(index));
},
};
const snow = new SplatModel({ generator, count: numSplats });
// Each frame: the time in seconds and the frame's delta
function update(seconds: number, delta: number) {
time.value = seconds;
offset.value.addScaledVector(u.fallDirection.value, u.fallVelocity.value * delta);
// Wrap so float precision holds up over long runs
offset.value.set(offset.value.x % 1, offset.value.y % 1, offset.value.z % 1);
snow.visible = u.opacity.value > 0;
}
return { snow, update, count: numSplats, ...u };
}