forked from Rezmason/matrix
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathrainPass.js
More file actions
381 lines (344 loc) · 12.3 KB
/
rainPass.js
File metadata and controls
381 lines (344 loc) · 12.3 KB
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
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
/*
* Matrix Rain Pass - WebGPU Implementation
*
* This is the heart of the Matrix digital rain effect using WebGPU rendering.
* Like the cascading green code Neo sees when he first perceives the Matrix's
* true nature, this pass generates the endless streams of falling glyphs.
*
* The rain pass handles:
* - Glyph positioning and animation in columns
* - Multi-channel Signed Distance Field (MSDF) glyph rendering
* - Sawtooth wave functions for non-colliding raindrop behavior
* - Volumetric 3D effects with perspective depth
* - Interactive ripple effects for user input
*/
import { structs } from "../../lib/gpu-buffer.js";
import { makeRenderTarget, loadTexture, loadShader, makeUniformBuffer, makeBindGroup, makePass } from "./utils.js";
/*
* Ripple Effect Types
*
* When the Matrix responds to external stimuli (like mouse clicks),
* it creates ripples that propagate through the digital rain.
* These ripples represent disturbances in the simulation's fabric.
*/
const rippleTypes = {
box: 0, // Square ripple effect - geometric, digital disturbance
circle: 1, // Circular ripple effect - organic, natural disturbance
};
/*
* Geometry Constants
*
* Each raindrop glyph is rendered as a quad (rectangle) made of two triangles.
* This constant defines how many vertices we need per quad for efficient
* GPU batch rendering.
*/
const numVerticesPerQuad = 2 * 3;
/*
* Configuration Buffer Creation
*
* This function creates a GPU buffer containing all the parameters needed
* to control the rain effect. It's like uploading the Matrix's operating
* parameters to the simulation's memory banks.
*
* @param {GPUDevice} device - WebGPU device for buffer creation
* @param {Object} configUniforms - Uniform buffer layout specification
* @param {Object} config - User configuration parameters
* @param {number} density - Rain density multiplier for 3D effects
* @param {Array} gridSize - [width, height] of the glyph grid
* @param {Matrix} glyphTransform - Transformation matrix for glyph rotation/flipping
* @returns {GPUBuffer} Uniform buffer containing configuration data
*/
const makeConfigBuffer = (device, configUniforms, config, density, gridSize, glyphTransform) => {
/*
* Configuration Data Assembly
*
* Combine user configuration with computed values needed for rendering:
* - Grid dimensions for proper raindrop column calculation
* - Debug view toggle for development and troubleshooting
* - Ripple parameters for interactive effects
* - Slant calculations for angled rain (like wind in the Matrix)
* - MSDF parameters for crisp glyph rendering
* - Glyph transformation matrix for rotation and mirroring
*/
const configData = {
...config,
gridSize,
density,
showDebugView: config.effect === "none", // Debug mode shows raw rain structure
rippleType: config.rippleTypeName in rippleTypes ? rippleTypes[config.rippleTypeName] : -1,
/*
* Slant Scale Compensation
* When rain falls at an angle, we need to adjust the visual scale
* to maintain consistent column spacing. This formula prevents
* gaps or overlaps when the rain is tilted.
*/
slantScale: 1 / (Math.abs(Math.sin(2 * config.slant)) * (Math.sqrt(2) - 1) + 1),
slantVec: [Math.cos(config.slant), Math.sin(config.slant)],
msdfPxRange: 4, // MSDF pixel range for distance field sampling
glyphTransform,
};
/* Uncomment for debugging configuration values:
console.table(configData); */
return makeUniformBuffer(device, configUniforms, configData);
};
/*
* Rain Pass Factory Function
*
* This function creates the complete Matrix rain rendering system.
* Like constructing the digital world from its fundamental code,
* this assembles all the components needed to generate the iconic
* cascading green glyphs.
*
* @param {Object} params - Factory parameters
* @param {Object} params.config - Matrix configuration settings
* @param {GPUDevice} params.device - WebGPU device for GPU operations
* @param {GPUBuffer} params.timeBuffer - Time uniform for animation
* @returns {Object} Complete rain pass with render function
*/
export default ({ config, device, timeBuffer }) => {
/* Import glMatrix for 3D math operations */
const { mat2, mat4, vec2, vec3 } = glMatrix;
/*
* Asset Loading Pipeline
*
* Load all the resources needed for the Matrix rain:
* - MSDF glyph texture: The actual Matrix symbols
* - Glint texture: Highlights and special effects on glyphs
* - Base texture: Optional surface material for glyphs
* - Glint surface texture: Optional material for glyph highlights
* - Shader code: GPU programs for rendering the rain
*/
const assets = [
loadTexture(device, config.glyphMSDFURL), // Main glyph texture
loadTexture(device, config.glintMSDFURL), // Glyph highlight texture
loadTexture(device, config.baseTextureURL, false, true), // Optional base material
loadTexture(device, config.glintTextureURL, false, true), // Optional glint material
loadShader(device, "shaders/wgsl/rainPass.wgsl"), // Rain rendering shader
];
/*
* Volumetric Density Calculation
*
* In 3D volumetric mode, we multiply the number of columns to create
* depth layers that overlap, simulating raindrops at different distances.
* This creates the illusion that the code extends infinitely into
* the screen, just like Neo's perception in the construct loading program.
*/
const density = config.volumetric && config.effect !== "none" ? config.density : 1;
const gridSize = [Math.floor(config.numColumns * density), config.numColumns];
const numCells = gridSize[0] * gridSize[1];
/*
* Geometry Generation Strategy
*
* - 2D Mode: Single fullscreen quad that samples the rain texture
* - 3D Mode: Grid of quads positioned at different depths in space
*
* The volumetric mode requires individual quads for each column/row
* combination so they can be positioned independently in 3D space.
*/
const numQuads = config.volumetric ? numCells : 1;
/*
* Glyph Transformation Matrix
*
* This matrix handles user-requested glyph modifications:
* - Horizontal flipping (mirroring) for alternate aesthetics
* - Rotation for angled or upside-down code effects
*
* The transformations are applied in matrix multiplication order:
* scaling first (for flipping), then rotation.
*/
const glyphTransform = mat2.fromScaling(mat2.create(), vec2.fromValues(config.glyphFlip ? -1 : 1, 1));
mat2.rotate(glyphTransform, glyphTransform, (config.glyphRotation * Math.PI) / 180);
const transform = mat4.create();
if (config.volumetric && config.isometric) {
mat4.rotateX(transform, transform, (Math.PI * 1) / 8);
mat4.rotateY(transform, transform, (Math.PI * 1) / 4);
mat4.translate(transform, transform, vec3.fromValues(0, 0, -1));
mat4.scale(transform, transform, vec3.fromValues(1, 1, 2));
} else {
mat4.translate(transform, transform, vec3.fromValues(0, 0, -1));
}
const camera = mat4.create();
// TODO: vantage points, multiple renders
// We use the different channels for different parts of the raindrop
const renderFormat = "rgba8unorm";
const linearSampler = device.createSampler({
magFilter: "linear",
minFilter: "linear",
});
const renderPassConfig = {
colorAttachments: [
{
// view: null,
loadOp: "clear",
storeOp: "store",
},
{
// view: null,
loadOp: "clear",
storeOp: "store",
},
],
};
let configBuffer;
let sceneUniforms;
let sceneBuffer;
let introPipeline;
let computePipeline;
let renderPipeline;
let introBindGroup;
let computeBindGroup;
let renderBindGroup;
let output;
let highPassOutput;
let depthTexture;
const loaded = (async () => {
const [glyphMSDFTexture, glintMSDFTexture, baseTexture, glintTexture, rainShader] = await Promise.all(assets);
const rainShaderUniforms = structs.from(rainShader.code);
configBuffer = makeConfigBuffer(device, rainShaderUniforms.Config, config, density, gridSize, glyphTransform);
const introCellsBuffer = device.createBuffer({
size: gridSize[0] * rainShaderUniforms.IntroCell.minSize,
usage: GPUBufferUsage.STORAGE,
});
const cellsBuffer = device.createBuffer({
size: numCells * rainShaderUniforms.Cell.minSize,
usage: GPUBufferUsage.STORAGE,
});
sceneUniforms = rainShaderUniforms.Scene;
sceneBuffer = makeUniformBuffer(device, sceneUniforms);
const additiveBlendComponent = {
operation: "add",
srcFactor: "one",
dstFactor: "one",
};
[introPipeline, computePipeline, renderPipeline] = await Promise.all([
device.createComputePipelineAsync({
layout: "auto",
compute: {
module: rainShader.module,
entryPoint: "computeIntro",
},
}),
device.createComputePipelineAsync({
layout: "auto",
compute: {
module: rainShader.module,
entryPoint: "computeMain",
},
}),
device.createRenderPipelineAsync({
layout: "auto",
vertex: {
module: rainShader.module,
entryPoint: "vertMain",
},
...(config.volumetric
? {
depthStencil: {
format: "depth24plus",
depthWriteEnabled: true,
depthCompare: "less",
},
}
: {}),
fragment: {
module: rainShader.module,
entryPoint: "fragMain",
targets: [
{
format: renderFormat,
blend: {
color: additiveBlendComponent,
alpha: additiveBlendComponent,
},
},
{
format: renderFormat,
blend: {
color: additiveBlendComponent,
alpha: additiveBlendComponent,
},
},
],
},
}),
]);
introBindGroup = makeBindGroup(device, introPipeline, 0, [configBuffer, timeBuffer, introCellsBuffer]);
computeBindGroup = makeBindGroup(device, computePipeline, 0, [configBuffer, timeBuffer, cellsBuffer, introCellsBuffer]);
renderBindGroup = makeBindGroup(device, renderPipeline, 0, [
configBuffer,
timeBuffer,
sceneBuffer,
linearSampler,
glyphMSDFTexture.createView(),
glintMSDFTexture.createView(),
baseTexture.createView(),
glintTexture.createView(),
cellsBuffer,
]);
})();
const build = (size) => {
// Update scene buffer: camera and transform math for the volumetric mode
const aspectRatio = size[0] / size[1];
if (config.volumetric && config.isometric) {
if (aspectRatio > 1) {
mat4.orthoZO(camera, -1.5 * aspectRatio, 1.5 * aspectRatio, -1.5, 1.5, -1000, 1000);
} else {
mat4.orthoZO(camera, -1.5, 1.5, -1.5 / aspectRatio, 1.5 / aspectRatio, -1000, 1000);
}
} else {
mat4.perspectiveZO(camera, (Math.PI / 180) * 90, aspectRatio, 0.0001, 1000);
}
const screenSize = aspectRatio > 1 ? [1, aspectRatio] : [1 / aspectRatio, 1];
device.queue.writeBuffer(sceneBuffer, 0, sceneUniforms.toBuffer({ screenSize, camera, transform }));
// Update
output?.destroy();
output = makeRenderTarget(device, size, renderFormat);
highPassOutput?.destroy();
highPassOutput = makeRenderTarget(device, size, renderFormat);
depthTexture?.destroy();
depthTexture = config.volumetric
? device.createTexture({
size: [size[0], size[1], 1],
format: "depth24plus",
usage: GPUTextureUsage.RENDER_ATTACHMENT,
})
: null;
return {
primary: output,
highPass: highPassOutput,
};
};
const run = (encoder, shouldRender) => {
// We render the code into an Target using MSDFs: https://github.com/Chlumsky/msdfgen
const introPass = encoder.beginComputePass();
introPass.setPipeline(introPipeline);
introPass.setBindGroup(0, introBindGroup);
introPass.dispatchWorkgroups(Math.ceil(gridSize[0] / 32), 1, 1);
introPass.end();
const computePass = encoder.beginComputePass();
computePass.setPipeline(computePipeline);
computePass.setBindGroup(0, computeBindGroup);
computePass.dispatchWorkgroups(Math.ceil(gridSize[0] / 32), gridSize[1], 1);
computePass.end();
if (shouldRender) {
renderPassConfig.colorAttachments[0].view = output.createView();
renderPassConfig.colorAttachments[1].view = highPassOutput.createView();
if (config.volumetric && depthTexture != null) {
renderPassConfig.depthStencilAttachment = {
view: depthTexture.createView(),
depthLoadOp: "clear",
depthStoreOp: "store",
depthClearValue: 1.0,
};
} else {
delete renderPassConfig.depthStencilAttachment;
}
const renderPass = encoder.beginRenderPass(renderPassConfig);
renderPass.setPipeline(renderPipeline);
renderPass.setBindGroup(0, renderBindGroup);
renderPass.draw(numVerticesPerQuad * numQuads, 1, 0, 0);
renderPass.end();
}
};
return makePass("Rain", loaded, build, run);
};