forked from Rezmason/matrix
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathutils.js
More file actions
299 lines (276 loc) · 8.3 KB
/
utils.js
File metadata and controls
299 lines (276 loc) · 8.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
/*
* WebGL utilities — helpers for the WebGL renderer (`js/webgl/`)
*
* Drawing is driven by regl today (see `lib/regl.min.js`); regl is slated for removal — DEPENDENCY_POLICY.md + migration_repl.md.
* Like the utilities that keep Zion's machines running, these functions handle
* the fundamental operations needed for Matrix rain rendering.
*/
/*
* Pass Texture Factory
*
* Creates textures used for intermediate rendering passes in the graphics pipeline.
* These textures serve as temporary storage for image data between processing steps,
* like frames in the Matrix's simulation buffer.
*
* @param {Object} regl - REGL context for WebGL operations
* @param {boolean} halfFloat - Whether to use 16-bit (half) or 8-bit precision
* @returns {Object} REGL texture configuration object
*
* Half-float textures provide higher precision for calculations while using
* less memory than full 32-bit floats - crucial for smooth raindrop animation.
*/
const makePassTexture = (regl, halfFloat) =>
regl.texture({
width: 1, // Initial size - will be resized when used
height: 1, // Initial size - will be resized when used
type: halfFloat ? "half float" : "uint8", // Data precision type
wrap: "clamp", // Prevent texture wrapping at edges
min: "linear", // Linear filtering for smooth scaling
mag: "linear", // Linear filtering for smooth scaling
});
/*
* Frame Buffer Object (FBO) Factory
*
* Creates frame buffer objects for off-screen rendering. Think of these as
* alternative reality buffers where we can render effects before compositing
* them into the final Matrix display.
*
* @param {Object} regl - REGL context
* @param {boolean} halfFloat - Precision level for the color buffer
* @returns {Object} REGL framebuffer object
*/
const makePassFBO = (regl, halfFloat) => regl.framebuffer({ color: makePassTexture(regl, halfFloat) });
/*
* Double Buffer System - Ping-Pong Rendering
*
* Creates a pair of framebuffers that alternate between being source and destination.
* This is essential for iterative GPU computations like particle physics - each frame
* reads from one buffer while writing to the other, then they swap roles.
*
* Like the Matrix's temporal duality, we need two states to create the illusion
* of continuous change from discrete computational steps.
*
* @param {Object} regl - REGL context
* @param {Object} props - Texture properties for the buffers
* @returns {Object} Double buffer with front() and back() accessor functions
*/
const makeDoubleBuffer = (regl, props) => {
/*
* Create Two Identical Framebuffers
* Each contains a color texture with the specified properties.
* No depth/stencil buffer needed for most Matrix effects.
*/
const state = Array(2)
.fill()
.map(() =>
regl.framebuffer({
color: regl.texture(props),
depthStencil: false, // No depth testing needed for 2D effects
}),
);
/*
* Buffer Access Functions
* front() and back() alternate based on the frame tick count.
* This creates the ping-pong effect needed for iterative calculations.
*/
return {
front: ({ tick }) => state[tick % 2], // Current frame's source buffer
back: ({ tick }) => state[(tick + 1) % 2], // Current frame's destination buffer
};
};
/*
* Power-of-Two Check
*
* WebGL has historically required power-of-two textures for certain operations
* like mipmapping. This utility checks if a number is a power of two using
* logarithmic math instead of bit manipulation for clarity.
*
* @param {number} x - Number to test
* @returns {boolean} True if x is a power of 2
*/
const isPowerOfTwo = (x) => Math.log2(x) % 1 == 0;
/*
* Asynchronous Image Loader
*
* Loads images for use as textures while providing immediate fallback texture.
* This prevents rendering errors when assets haven't loaded yet - the Matrix
* continues running even while loading new reality data.
*
* @param {Object} regl - REGL context
* @param {string} url - Image URL to load
* @param {boolean} mipmap - Whether to generate mipmaps for the texture
* @returns {Object} Texture accessor with loading state management
*/
const loadImage = (regl, url, mipmap) => {
/*
* Initialize with 1x1 black pixel as fallback
* This ensures rendering can proceed immediately even before image loads
*/
let texture = regl.texture([[0]]);
let loaded = false;
return {
/*
* Texture Accessor
* Returns the current texture, warning if still loading
*/
texture: () => {
if (!loaded && url != null) {
console.warn(`texture still loading: ${url}`);
}
return texture;
},
/*
* Width Accessor
* Returns texture width, defaulting to 1 if still loading
*/
width: () => {
if (!loaded && url != null) {
console.warn(`texture still loading: ${url}`);
}
return loaded ? texture.width : 1;
},
/*
* Height Accessor
* Returns texture height, defaulting to 1 if still loading
*/
height: () => {
if (!loaded && url != null) {
console.warn(`texture still loading: ${url}`);
}
return loaded ? texture.height : 1;
},
loaded: (async () => {
if (url != null) {
const data = new Image();
data.crossOrigin = "anonymous";
data.src = url;
await data.decode();
loaded = true;
if (mipmap) {
if (!isPowerOfTwo(data.width) || !isPowerOfTwo(data.height)) {
console.warn(`Can't mipmap a non-power-of-two image: ${url}`);
}
mipmap = false;
}
texture = regl.texture({
data,
mag: "linear",
min: mipmap ? "mipmap" : "linear",
flipY: true,
});
}
})(),
};
};
const loadText = (url) => {
let text = "";
let loaded = false;
return {
text: () => {
if (!loaded) {
console.warn(`text still loading: ${url}`);
return "";
}
return text;
},
loaded: (async () => {
if (url != null) {
const res = await fetch(url);
if (!res.ok) {
throw new Error(`[Matrix] Failed to load shader ${url}: ${res.status} ${res.statusText}`);
}
const body = await res.text();
text = typeof body === "string" ? body : String(body ?? "");
loaded = true;
}
})(),
};
};
// Shared fullscreen triangle (must match attribute name `aPosition` + buffer layout).
const fullscreenPassVertGLSL = `
precision mediump float;
attribute vec2 aPosition;
varying vec2 vUV;
void main() {
vUV = 0.5 * (aPosition + 1.0);
gl_Position = vec4(aPosition, 0, 1);
}
`;
const fullscreenPassAttributes = {
aPosition: [-4, -4, 4, -4, 0, 4],
};
const fullscreenPassVertexCount = 3;
/** Spread into `regl({ ... })` for full-screen fragment passes that use a static `frag` string. */
const fullscreenQuadReglBase = {
vert: fullscreenPassVertGLSL,
attributes: fullscreenPassAttributes,
count: fullscreenPassVertexCount,
depth: { enable: false },
};
/**
* @param {string} label
* @param {() => string} getText e.g. `() => handle.text()`
*/
const requireShaderString = (label, getText) => {
const s = getText();
if (typeof s !== "string" || !s.trim()) {
throw new Error(`[Matrix] ${label} shader missing after load (${s?.length ?? "n/a"} chars).`);
}
return s;
};
const makeFullScreenQuad = (regl, uniforms = {}, context = {}) =>
regl({
vert: fullscreenPassVertGLSL,
frag: `
precision mediump float;
varying vec2 vUV;
uniform sampler2D tex;
void main() {
gl_FragColor = texture2D(tex, vUV);
}
`,
attributes: fullscreenPassAttributes,
count: fullscreenPassVertexCount,
uniforms: {
...uniforms,
time: regl.context("time"),
tick: regl.context("tick"),
},
context,
depth: { enable: false },
});
const make1DTexture = (regl, rgbas) => {
const data = rgbas.map((rgba) => rgba.map((f) => Math.floor(f * 0xff))).flat();
return regl.texture({
data,
width: data.length / 4,
height: 1,
format: "rgba",
mag: "linear",
min: "linear",
});
};
const makePass = (outputs, ready, setSize, execute) => ({
outputs: outputs ?? {},
ready: ready ?? Promise.resolve(),
setSize: setSize ?? (() => {}),
execute: execute ?? (() => {}),
});
const makePipeline = (context, steps) =>
steps.filter((f) => f != null).reduce((pipeline, f, i) => [...pipeline, f(context, i == 0 ? null : pipeline[i - 1].outputs)], []);
export {
makePassTexture,
makePassFBO,
makeDoubleBuffer,
loadImage,
loadText,
fullscreenPassVertGLSL,
fullscreenPassAttributes,
fullscreenPassVertexCount,
fullscreenQuadReglBase,
requireShaderString,
makeFullScreenQuad,
make1DTexture,
makePass,
makePipeline,
};