fuzzyworld

nordic-skating / files / src / render / snow.ts

snow.ts

3.6 KB · history · blame

1import * as THREE from 'three'
2import { mulberry32 } from '../world/lake.js'
3
4/** The box of air round the skater that has snow in it: metres across, up, and along. */
5const BOX = [44, 22, 44] as const
6/** Snowflakes in a blizzard; lighter snow shows a share of them. */
7const FLAKES = 6000
8/** How fast a snowflake falls, m/s. */
9const FALL = 1.5
10
11const vertex = /* glsl */ `
12 attribute float aOrder;
13 uniform vec3 uBox;
14 uniform vec3 uDrift;
15 uniform float uAmount;
16 uniform float uPixels;
17 varying float vShow;
18 void main() {
19 // Every flake is somewhere in the box, and the box goes round: what leaves one side
20 // of it comes in at the other, so the air is always full without a flake being made.
21 vec3 at = fract(position + uDrift) * uBox - vec3(uBox.x * 0.5, 3.0, uBox.z * 0.5);
22 vec4 seen = modelViewMatrix * vec4(at, 1.0);
23 gl_Position = projectionMatrix * seen;
24 float far = length(seen.xyz);
25 gl_PointSize = uPixels * (0.05 + 0.06 * fract(aOrder * 7.3)) / far;
26 // Lighter snow is fewer flakes; and they fade out towards the edge of the box.
27 vShow = step(aOrder, uAmount) * smoothstep(21.0, 14.0, far) * smoothstep(0.3, 1.5, far);
28 }
29`
30
31const fragment = /* glsl */ `
32 uniform vec3 uColour;
33 varying float vShow;
34 void main() {
35 float soft = smoothstep(0.5, 0.15, length(gl_PointCoord - 0.5));
36 if (vShow * soft < 0.01) discard;
37 gl_FragColor = vec4(uColour, vShow * soft * 0.9);
38 #include <colorspace_fragment>
39 }
40`
41
42/**
43 * Falling snow: a box of flakes that stays round the skater, through which the flakes
44 * fall, are blown by the wind, and stream past as the skater moves.
45 */
46export class Snow {
47 readonly points: THREE.Points
48 private readonly uniforms = {
49 uBox: { value: new THREE.Vector3(...BOX) },
50 uDrift: { value: new THREE.Vector3() },
51 uAmount: { value: 0 },
52 uPixels: { value: 1 },
53 uColour: { value: new THREE.Color() },
54 }
55
56 constructor() {
57 const random = mulberry32(1947)
58 const places = new Float32Array(FLAKES * 3)
59 const order = new Float32Array(FLAKES)
60 for (let i = 0; i < FLAKES; i++) {
61 for (let c = 0; c < 3; c++) places[i * 3 + c] = random()
62 order[i] = random()
63 }
64 const geometry = new THREE.BufferGeometry()
65 geometry.setAttribute('position', new THREE.BufferAttribute(places, 3))
66 geometry.setAttribute('aOrder', new THREE.BufferAttribute(order, 1))
67 this.points = new THREE.Points(geometry, new THREE.ShaderMaterial({
68 uniforms: this.uniforms, vertexShader: vertex, fragmentShader: fragment,
69 transparent: true, depthWrite: false,
70 }))
71 this.points.frustumCulled = false
72 }
73
74 /** How much snow: 0 for none, 1 for a blizzard; and its colour in this light. */
75 show(amount: number, colour: number): void {
76 this.uniforms.uAmount.value = amount
77 this.uniforms.uColour.value.set(colour)
78 this.points.visible = amount > 0
79 }
80
81 /** `height` is the height of the picture in pixels, which sets how big a flake is drawn. */
82 resize(height: number): void {
83 this.uniforms.uPixels.value = height
84 }
85
86 /**
87 * Move the snow on by `dt` seconds: falling, blown by a wind of `windX` m/s across the
88 * lake and `windS` along it, and left behind by a skater moving at `moveX` and `moveS`.
89 */
90 update(dt: number, windX: number, windS: number, moveX: number, moveS: number): void {
91 const drift = this.uniforms.uDrift.value
92 // What is drawn looks down -z, so along the lake is the other way round.
93 drift.x = (drift.x + ((windX - moveX) * dt) / BOX[0]) % 1
94 drift.y = (drift.y - (FALL * dt) / BOX[1]) % 1
95 drift.z = (drift.z - ((windS - moveS) * dt) / BOX[2]) % 1
96 }
97}