@bigmistqke/view.gl 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/settings.local.json +8 -0
- package/.prettierrc +10 -0
- package/README.md +338 -0
- package/dev/bezier.ts +428 -0
- package/dev/game_of_life.ts +250 -0
- package/dev/grid.ts +223 -0
- package/dev/index.html +20 -0
- package/dev/index.ts +94 -0
- package/dev/pix_sim.ts +549 -0
- package/dev/tetris.ts +439 -0
- package/dev/tsconfig.json +4 -0
- package/dev/utils.ts +61 -0
- package/dev/vite.config.ts +42 -0
- package/package.json +29 -0
- package/src/features.ts +59 -0
- package/src/index.ts +313 -0
- package/src/tag.ts +193 -0
- package/src/types.ts +718 -0
- package/src/utils.ts +266 -0
- package/tsconfig.json +30 -0
- package/tsup.config.ts +11 -0
package/dev/pix_sim.ts
ADDED
|
@@ -0,0 +1,549 @@
|
|
|
1
|
+
import { FramebufferOptions } from 'src/types'
|
|
2
|
+
import { createFramebuffer } from 'src/utils'
|
|
3
|
+
import { view } from 'view.gl'
|
|
4
|
+
import { attribute, compile, glsl, uniform } from 'view.gl/tag'
|
|
5
|
+
|
|
6
|
+
export const MATERIALS = {
|
|
7
|
+
SAND: {
|
|
8
|
+
color: [1, 0.8, 0], // Yellow sand
|
|
9
|
+
id: 1,
|
|
10
|
+
},
|
|
11
|
+
WATER: {
|
|
12
|
+
color: [0, 0.5, 1], // Blue water
|
|
13
|
+
id: 2,
|
|
14
|
+
},
|
|
15
|
+
STONE: {
|
|
16
|
+
color: [0.5, 0.5, 0.5], // Gray stone
|
|
17
|
+
id: 3,
|
|
18
|
+
},
|
|
19
|
+
} as const
|
|
20
|
+
|
|
21
|
+
const MATERIAL_SIZE = Object.keys(MATERIALS).length
|
|
22
|
+
|
|
23
|
+
let playing = false
|
|
24
|
+
let currentMaterial = MATERIALS.SAND.id
|
|
25
|
+
|
|
26
|
+
const canvas = document.createElement('canvas')
|
|
27
|
+
const gl = canvas.getContext('webgl2', { antialias: false })!
|
|
28
|
+
if (!gl) {
|
|
29
|
+
throw new Error('WebGL not supported')
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Create UI container
|
|
33
|
+
const container = document.createElement('div')
|
|
34
|
+
container.style.cssText = 'display: flex; flex-direction: column; align-items: center; gap: 10px;'
|
|
35
|
+
|
|
36
|
+
// Create material buttons
|
|
37
|
+
const buttonContainer = document.createElement('div')
|
|
38
|
+
buttonContainer.style.cssText = 'display: flex; gap: 10px; margin-bottom: 10px;'
|
|
39
|
+
|
|
40
|
+
Object.entries(MATERIALS).forEach(([name, material]) => {
|
|
41
|
+
const button = document.createElement('button')
|
|
42
|
+
button.textContent = name.toLowerCase()
|
|
43
|
+
button.style.cssText = `
|
|
44
|
+
padding: 5px 10px;
|
|
45
|
+
font-size: 14px;
|
|
46
|
+
font-family: times new roman;
|
|
47
|
+
cursor: pointer;
|
|
48
|
+
background: ${
|
|
49
|
+
currentMaterial === material.id
|
|
50
|
+
? `rgb(${material.color.map(c => Math.floor(c * 255)).join(',')})`
|
|
51
|
+
: 'transparent'
|
|
52
|
+
};
|
|
53
|
+
border-color: ${name === 'SAND' ? 'black' : 'white'};
|
|
54
|
+
border: 2px solid rgb(${material.color.map(c => Math.floor(c * 255)).join(',')});
|
|
55
|
+
border-radius: 2px;
|
|
56
|
+
`
|
|
57
|
+
button.onclick = () => {
|
|
58
|
+
currentMaterial = material.id
|
|
59
|
+
// Update all button borders
|
|
60
|
+
buttonContainer.querySelectorAll('button').forEach((btn, idx) => {
|
|
61
|
+
btn.style.background =
|
|
62
|
+
idx === material.id - 1
|
|
63
|
+
? `rgb(${material.color.map(c => Math.floor(c * 255)).join(',')})`
|
|
64
|
+
: 'transparent'
|
|
65
|
+
})
|
|
66
|
+
}
|
|
67
|
+
buttonContainer.appendChild(button)
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
container.appendChild(canvas)
|
|
71
|
+
container.appendChild(buttonContainer)
|
|
72
|
+
document.body.append(container)
|
|
73
|
+
|
|
74
|
+
function spray() {
|
|
75
|
+
return Math.floor(Math.random() * 8) * (Math.random() < 0.5 ? 1 : -1)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
canvas.addEventListener('mousedown', event => {
|
|
79
|
+
const rect = canvas.getBoundingClientRect()
|
|
80
|
+
const controller = new AbortController()
|
|
81
|
+
// playing = false
|
|
82
|
+
window.addEventListener(
|
|
83
|
+
'mousemove',
|
|
84
|
+
event => {
|
|
85
|
+
const x = Math.floor((event.clientX - rect.left + spray()) * (canvas.width / rect.width))
|
|
86
|
+
const y = Math.floor((rect.bottom - event.clientY + spray()) * (canvas.height / rect.height))
|
|
87
|
+
|
|
88
|
+
const dimensions = [8, 8] as const
|
|
89
|
+
|
|
90
|
+
// Encode selected material using bit encoding
|
|
91
|
+
const data = new Uint8Array(
|
|
92
|
+
(function* () {
|
|
93
|
+
for (let i = 0; i < dimensions[0] * dimensions[1]; i++) {
|
|
94
|
+
yield (currentMaterial & 1) * 255 // Red channel (bit 0)
|
|
95
|
+
yield ((currentMaterial >> 1) & 1) * 255 // Green channel (bit 1)
|
|
96
|
+
yield ((currentMaterial >> 2) & 1) * 255 // Blue channel (bit 2)
|
|
97
|
+
yield 255 // Alpha channel
|
|
98
|
+
}
|
|
99
|
+
})(),
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
// Update both read and write textures
|
|
103
|
+
gl.bindTexture(gl.TEXTURE_2D, read.texture)
|
|
104
|
+
gl.texSubImage2D(
|
|
105
|
+
gl.TEXTURE_2D,
|
|
106
|
+
0,
|
|
107
|
+
Math.max(0, Math.min(WIDTH - dimensions[0], x)),
|
|
108
|
+
Math.max(0, Math.min(HEIGHT - dimensions[1], y)),
|
|
109
|
+
dimensions[0],
|
|
110
|
+
dimensions[1],
|
|
111
|
+
gl.RGBA,
|
|
112
|
+
gl.UNSIGNED_BYTE,
|
|
113
|
+
data,
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
gl.bindTexture(gl.TEXTURE_2D, write.texture)
|
|
117
|
+
gl.texSubImage2D(
|
|
118
|
+
gl.TEXTURE_2D,
|
|
119
|
+
0,
|
|
120
|
+
Math.max(0, Math.min(WIDTH - dimensions[0], x)),
|
|
121
|
+
Math.max(0, Math.min(HEIGHT - dimensions[1], y)),
|
|
122
|
+
dimensions[0],
|
|
123
|
+
dimensions[1],
|
|
124
|
+
gl.RGBA,
|
|
125
|
+
gl.UNSIGNED_BYTE,
|
|
126
|
+
data,
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
render()
|
|
130
|
+
},
|
|
131
|
+
controller,
|
|
132
|
+
)
|
|
133
|
+
window.addEventListener(
|
|
134
|
+
'mouseup',
|
|
135
|
+
() => {
|
|
136
|
+
controller.abort()
|
|
137
|
+
// playing = true
|
|
138
|
+
// animate()
|
|
139
|
+
},
|
|
140
|
+
controller,
|
|
141
|
+
)
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
canvas.width = 256 * 2
|
|
145
|
+
canvas.height = 256 * 2
|
|
146
|
+
const WIDTH = canvas.width
|
|
147
|
+
const HEIGHT = canvas.height
|
|
148
|
+
|
|
149
|
+
// --- Shaders ---
|
|
150
|
+
|
|
151
|
+
// Vertex shader (simple fullscreen quad)
|
|
152
|
+
const vertex = glsl`#version 300 es
|
|
153
|
+
${attribute.vec2('a_vertex')}
|
|
154
|
+
out vec2 v_uv;
|
|
155
|
+
void main() {
|
|
156
|
+
v_uv = a_vertex * 0.5 + 0.5;
|
|
157
|
+
gl_Position = vec4(a_vertex, 0, 1);
|
|
158
|
+
}`
|
|
159
|
+
|
|
160
|
+
// Fragment shader for sand simulation step
|
|
161
|
+
const stepFragment = glsl`#version 300 es
|
|
162
|
+
precision highp float;
|
|
163
|
+
in vec2 v_uv;
|
|
164
|
+
${uniform.sampler2D('u_texture')}
|
|
165
|
+
${uniform.vec2('u_texelSize')}
|
|
166
|
+
out vec4 fragColor;
|
|
167
|
+
|
|
168
|
+
// Material constants (like enums)
|
|
169
|
+
const int MAT_EMPTY = 0;
|
|
170
|
+
const int MAT_SAND = 1;
|
|
171
|
+
const int MAT_WATER = 2;
|
|
172
|
+
const int MAT_STONE = 3;
|
|
173
|
+
|
|
174
|
+
// Direction constants
|
|
175
|
+
const vec2 DIR_UP = vec2(0.0, 1.0);
|
|
176
|
+
const vec2 DIR_DOWN = vec2(0.0, -1.0);
|
|
177
|
+
const vec2 DIR_LEFT = vec2(-1.0, 0.0);
|
|
178
|
+
const vec2 DIR_RIGHT = vec2(1.0, 0.0);
|
|
179
|
+
|
|
180
|
+
// Helper function to decode material from RGB
|
|
181
|
+
int getMaterial(vec4 color) {
|
|
182
|
+
return int(color.r + color.g * 2.0 + color.b * 4.0);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Helper function to encode material to RGB
|
|
186
|
+
vec4 encodeMaterial(int material) {
|
|
187
|
+
return vec4(
|
|
188
|
+
float(material & 1),
|
|
189
|
+
float((material >> 1) & 1),
|
|
190
|
+
float((material >> 2) & 1),
|
|
191
|
+
1.0
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
void main() {
|
|
196
|
+
// Sample the current state
|
|
197
|
+
vec4 current = texture(u_texture, v_uv);
|
|
198
|
+
|
|
199
|
+
// Check boundaries
|
|
200
|
+
bool atBottom = v_uv.y - u_texelSize.y < 0.0;
|
|
201
|
+
bool atLeft = v_uv.x - u_texelSize.x < 0.0;
|
|
202
|
+
bool atRight = v_uv.x + u_texelSize.x > 1.0;
|
|
203
|
+
bool atTop = v_uv.y + u_texelSize.y > 1.0;
|
|
204
|
+
|
|
205
|
+
// Default: keep current state
|
|
206
|
+
fragColor = current;
|
|
207
|
+
|
|
208
|
+
int material = getMaterial(current);
|
|
209
|
+
|
|
210
|
+
int below = getMaterial(texture(u_texture, v_uv + DIR_DOWN * u_texelSize));
|
|
211
|
+
int above = getMaterial(texture(u_texture, v_uv + DIR_UP * u_texelSize));
|
|
212
|
+
int aboveLeft = getMaterial(texture(u_texture, v_uv + (DIR_UP + DIR_LEFT) * u_texelSize));
|
|
213
|
+
int aboveRight = getMaterial(texture(u_texture, v_uv + (DIR_UP + DIR_RIGHT) * u_texelSize));
|
|
214
|
+
int belowLeft = getMaterial(texture(u_texture, v_uv + (DIR_DOWN + DIR_LEFT) * u_texelSize));
|
|
215
|
+
int belowRight = getMaterial(texture(u_texture, v_uv + (DIR_DOWN + DIR_RIGHT) * u_texelSize));
|
|
216
|
+
|
|
217
|
+
// Physics simulation using switch
|
|
218
|
+
switch (material) {
|
|
219
|
+
case MAT_EMPTY:{
|
|
220
|
+
// Empty cell - check what can fall/flow into it
|
|
221
|
+
|
|
222
|
+
// Check for sand from above (highest priority - sand is denser)
|
|
223
|
+
bool sandDirectlyAbove = !atTop && above == MAT_SAND;
|
|
224
|
+
if (sandDirectlyAbove) {
|
|
225
|
+
fragColor = encodeMaterial(MAT_SAND);
|
|
226
|
+
break;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// Check for sand sliding from diagonal
|
|
230
|
+
bool sandAboveLeft = !atTop && !atRight && aboveLeft == MAT_SAND;
|
|
231
|
+
bool sandAboveRight = !atTop && !atLeft && aboveRight == MAT_SAND;
|
|
232
|
+
|
|
233
|
+
if (sandAboveLeft) {
|
|
234
|
+
vec4 left = texture(u_texture, v_uv + DIR_LEFT * u_texelSize);
|
|
235
|
+
bool leftBlocked = getMaterial(left) != MAT_EMPTY;
|
|
236
|
+
bool canSlideFromLeft = leftBlocked || atBottom;
|
|
237
|
+
if (canSlideFromLeft) {
|
|
238
|
+
fragColor = encodeMaterial(MAT_SAND);
|
|
239
|
+
break;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
if (sandAboveRight) {
|
|
244
|
+
vec4 right = texture(u_texture, v_uv + DIR_RIGHT * u_texelSize);
|
|
245
|
+
bool rightBlocked = getMaterial(right) != MAT_EMPTY;
|
|
246
|
+
bool canSlideFromRight = rightBlocked || atBottom;
|
|
247
|
+
if (canSlideFromRight) {
|
|
248
|
+
fragColor = encodeMaterial(MAT_SAND);
|
|
249
|
+
break;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// Check for water from above
|
|
254
|
+
bool waterDirectlyAbove = !atTop && above == MAT_WATER;
|
|
255
|
+
if (waterDirectlyAbove) {
|
|
256
|
+
fragColor = encodeMaterial(MAT_WATER);
|
|
257
|
+
break;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// Check for water sliding from diagonal
|
|
261
|
+
bool waterAboveLeft = !atTop && !atRight && aboveLeft == MAT_WATER;
|
|
262
|
+
bool waterAboveRight = !atTop && !atLeft && aboveRight == MAT_WATER;
|
|
263
|
+
|
|
264
|
+
if (waterAboveLeft) {
|
|
265
|
+
vec4 left = texture(u_texture, v_uv + DIR_LEFT * u_texelSize);
|
|
266
|
+
bool sandToLeft = getMaterial(left) == MAT_SAND;
|
|
267
|
+
if (!sandToLeft) {
|
|
268
|
+
fragColor = encodeMaterial(MAT_WATER);
|
|
269
|
+
break;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
if (waterAboveRight) {
|
|
274
|
+
vec4 right = texture(u_texture, v_uv + DIR_RIGHT * u_texelSize);
|
|
275
|
+
bool sandToRight = getMaterial(right) == MAT_SAND;
|
|
276
|
+
if (!sandToRight) {
|
|
277
|
+
fragColor = encodeMaterial(MAT_WATER);
|
|
278
|
+
break;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// Check for water flowing horizontally
|
|
283
|
+
vec4 left = texture(u_texture, v_uv + DIR_LEFT * u_texelSize);
|
|
284
|
+
vec4 right = texture(u_texture, v_uv + DIR_RIGHT * u_texelSize);
|
|
285
|
+
int matLeft = getMaterial(left);
|
|
286
|
+
int matRight = getMaterial(right);
|
|
287
|
+
|
|
288
|
+
bool waterToLeft = !atRight && matLeft == MAT_WATER;
|
|
289
|
+
bool waterToRight = !atLeft && matRight == MAT_WATER;
|
|
290
|
+
|
|
291
|
+
if (!waterToLeft && !waterToRight) break;
|
|
292
|
+
|
|
293
|
+
// Random choice for symmetric flow
|
|
294
|
+
float rand = fract(sin(dot(v_uv * 1000.0, vec2(12.9898, 78.233))) * 43758.5453);
|
|
295
|
+
bool preferLeft = rand < 0.5;
|
|
296
|
+
|
|
297
|
+
if (
|
|
298
|
+
waterToLeft && waterToRight ||
|
|
299
|
+
waterToLeft ||
|
|
300
|
+
waterToRight
|
|
301
|
+
) {
|
|
302
|
+
fragColor = encodeMaterial(MAT_WATER);
|
|
303
|
+
break;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
case MAT_SAND: {
|
|
308
|
+
// Sand particle - check if it should fall
|
|
309
|
+
bool canFallThrough = below == MAT_EMPTY || below == MAT_WATER;
|
|
310
|
+
bool canFallDown = !atBottom && canFallThrough;
|
|
311
|
+
|
|
312
|
+
if (canFallDown) {
|
|
313
|
+
fragColor = encodeMaterial(MAT_EMPTY);
|
|
314
|
+
break;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// Check diagonal falling
|
|
318
|
+
bool canFallThroughLeft = belowLeft == MAT_EMPTY || belowLeft == MAT_WATER;
|
|
319
|
+
bool canFallThroughRight = belowRight == MAT_EMPTY || belowRight == MAT_WATER;
|
|
320
|
+
|
|
321
|
+
bool canFallDownLeft = !atBottom && !atLeft && canFallThroughLeft;
|
|
322
|
+
bool canFallDownRight = !atBottom && !atRight && canFallThroughRight;
|
|
323
|
+
|
|
324
|
+
if (canFallDownLeft || canFallDownRight) {
|
|
325
|
+
fragColor = encodeMaterial(MAT_EMPTY);
|
|
326
|
+
break;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// Sand stays in place
|
|
330
|
+
break;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
case MAT_WATER: {
|
|
334
|
+
// Water - check if being displaced by sand first
|
|
335
|
+
bool sandFallingFromAbove = !atTop && above == MAT_SAND;
|
|
336
|
+
if (sandFallingFromAbove) {
|
|
337
|
+
fragColor = encodeMaterial(MAT_SAND);
|
|
338
|
+
break;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// Check sand sliding into water from diagonals
|
|
342
|
+
bool sandFromAboveLeft = !atTop && !atRight && aboveLeft == MAT_SAND;
|
|
343
|
+
bool sandFromAboveRight = !atTop && !atLeft && aboveRight == MAT_SAND;
|
|
344
|
+
|
|
345
|
+
if (sandFromAboveLeft) {
|
|
346
|
+
vec4 left = texture(u_texture, v_uv + DIR_LEFT * u_texelSize);
|
|
347
|
+
bool leftNotEmpty = getMaterial(left) != MAT_EMPTY;
|
|
348
|
+
bool sandCanSlideIn = leftNotEmpty || atBottom;
|
|
349
|
+
|
|
350
|
+
if (sandCanSlideIn) {
|
|
351
|
+
fragColor = encodeMaterial(MAT_SAND);
|
|
352
|
+
break;
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
if (sandFromAboveRight) {
|
|
357
|
+
vec4 right = texture(u_texture, v_uv + DIR_RIGHT * u_texelSize);
|
|
358
|
+
bool rightNotEmpty = getMaterial(right) != MAT_EMPTY;
|
|
359
|
+
bool sandCanSlideIn = rightNotEmpty || atBottom;
|
|
360
|
+
|
|
361
|
+
if (sandCanSlideIn) {
|
|
362
|
+
fragColor = encodeMaterial(MAT_SAND);
|
|
363
|
+
break;
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// Normal water physics - falling
|
|
368
|
+
bool canFallDown = !atBottom && below == MAT_EMPTY;
|
|
369
|
+
bool canFlowDownLeft = !atBottom && !atLeft && belowLeft == MAT_EMPTY;
|
|
370
|
+
bool canFlowDownRight = !atBottom && !atRight && belowRight == MAT_EMPTY;
|
|
371
|
+
|
|
372
|
+
if (canFallDown || canFlowDownLeft || canFlowDownRight) {
|
|
373
|
+
fragColor = encodeMaterial(MAT_EMPTY);
|
|
374
|
+
break;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// Horizontal spreading
|
|
378
|
+
vec4 left = texture(u_texture, v_uv + DIR_LEFT * u_texelSize);
|
|
379
|
+
vec4 right = texture(u_texture, v_uv + DIR_RIGHT * u_texelSize);
|
|
380
|
+
int matLeft = getMaterial(left);
|
|
381
|
+
int matRight = getMaterial(right);
|
|
382
|
+
|
|
383
|
+
bool canFlowLeft = !atLeft && matLeft == MAT_EMPTY;
|
|
384
|
+
bool canFlowRight = !atRight && matRight == MAT_EMPTY;
|
|
385
|
+
|
|
386
|
+
if (!canFlowLeft && !canFlowRight) {
|
|
387
|
+
break;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// Random horizontal movement for fluid behavior
|
|
391
|
+
float rand = fract(sin(dot(v_uv * 1000.0, vec2(12.9898, 78.233))) * 43758.5453);
|
|
392
|
+
bool preferLeft = rand < 0.5;
|
|
393
|
+
|
|
394
|
+
if (
|
|
395
|
+
canFlowLeft && preferLeft ||
|
|
396
|
+
canFlowRight && !preferLeft ||
|
|
397
|
+
canFlowLeft ||
|
|
398
|
+
canFlowDownRight
|
|
399
|
+
) {
|
|
400
|
+
fragColor = encodeMaterial(MAT_EMPTY);
|
|
401
|
+
break;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
case MAT_STONE:
|
|
407
|
+
// Stone doesn't move
|
|
408
|
+
break;
|
|
409
|
+
|
|
410
|
+
default:
|
|
411
|
+
// Unknown material - keep as is
|
|
412
|
+
break;
|
|
413
|
+
}
|
|
414
|
+
}`
|
|
415
|
+
|
|
416
|
+
// Fragment shader for rendering the state texture to screen
|
|
417
|
+
const renderFragment = glsl`#version 300 es
|
|
418
|
+
precision mediump float;
|
|
419
|
+
in vec2 v_uv;
|
|
420
|
+
${uniform.sampler2D('u_texture')}
|
|
421
|
+
${uniform.vec3('u_palette', { size: MATERIAL_SIZE })}
|
|
422
|
+
out vec4 fragColor;
|
|
423
|
+
|
|
424
|
+
// Material constants (must match stepFragment)
|
|
425
|
+
const int MAT_EMPTY = 0;
|
|
426
|
+
const int MAT_SAND = 1;
|
|
427
|
+
const int MAT_WATER = 2;
|
|
428
|
+
const int MAT_STONE = 3;
|
|
429
|
+
|
|
430
|
+
// Helper function to decode material from RGB
|
|
431
|
+
int getMaterial(vec4 color) {
|
|
432
|
+
return int(color.r + color.g * 2.0 + color.b * 4.0);
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
void main() {
|
|
436
|
+
vec4 texel = texture(u_texture, v_uv);
|
|
437
|
+
int material = getMaterial(texel);
|
|
438
|
+
|
|
439
|
+
if (material == MAT_EMPTY) {
|
|
440
|
+
fragColor = vec4(0.0, 0.0, 0.0, 1.0); // Black background
|
|
441
|
+
} else if (material > 0 && material <= ${MATERIAL_SIZE}) {
|
|
442
|
+
fragColor = vec4(u_palette[material - 1], 1.0);
|
|
443
|
+
} else {
|
|
444
|
+
fragColor = vec4(1.0, 0.0, 1.0, 1.0); // Magenta for unknown materials
|
|
445
|
+
}
|
|
446
|
+
}`
|
|
447
|
+
|
|
448
|
+
// --- Create programs ---
|
|
449
|
+
const { program: stepProgram, schema: stepSchema } = compile(gl, vertex, stepFragment)
|
|
450
|
+
const stepView = view(gl, stepProgram, stepSchema)
|
|
451
|
+
|
|
452
|
+
const { program: renderProgram, schema: renderSchema } = compile(gl, vertex, renderFragment)
|
|
453
|
+
const renderView = view(gl, renderProgram, {
|
|
454
|
+
...renderSchema,
|
|
455
|
+
attributes: {
|
|
456
|
+
...renderSchema.attributes,
|
|
457
|
+
a_vertex: {
|
|
458
|
+
...renderSchema.attributes.a_vertex,
|
|
459
|
+
buffer: stepView.attributes.a_vertex.buffer,
|
|
460
|
+
},
|
|
461
|
+
},
|
|
462
|
+
})
|
|
463
|
+
|
|
464
|
+
gl.useProgram(renderProgram)
|
|
465
|
+
renderView.uniforms.u_palette.set(
|
|
466
|
+
new Float32Array(Object.values(MATERIALS).flatMap(({ color }) => color)),
|
|
467
|
+
)
|
|
468
|
+
|
|
469
|
+
gl.useProgram(stepProgram)
|
|
470
|
+
// Setup quad
|
|
471
|
+
stepView.attributes.a_vertex.set(new Float32Array([-1, -1, 1, -1, -1, 1, -1, 1, 1, -1, 1, 1]))
|
|
472
|
+
|
|
473
|
+
// Initialize two framebuffers (with their respective texture) for ping-pong
|
|
474
|
+
const framebufferOptions = {
|
|
475
|
+
attachment: 'color',
|
|
476
|
+
width: WIDTH,
|
|
477
|
+
height: HEIGHT,
|
|
478
|
+
type: 'UNSIGNED_BYTE',
|
|
479
|
+
magFilter: 'NEAREST',
|
|
480
|
+
minFilter: 'NEAREST',
|
|
481
|
+
internalFormat: 'RGBA',
|
|
482
|
+
format: 'RGBA',
|
|
483
|
+
} satisfies FramebufferOptions
|
|
484
|
+
|
|
485
|
+
let read = createFramebuffer(gl, framebufferOptions)
|
|
486
|
+
let write = createFramebuffer(gl, framebufferOptions)
|
|
487
|
+
|
|
488
|
+
// Initialize state texture (empty)
|
|
489
|
+
const initData = new Uint8Array(
|
|
490
|
+
(function* () {
|
|
491
|
+
for (let i = 0; i < WIDTH * HEIGHT; i++) {
|
|
492
|
+
yield 0 // Red channel (material ID)
|
|
493
|
+
yield 0 // Green channel
|
|
494
|
+
yield 0 // Blue channel
|
|
495
|
+
yield 255 // Alpha channel
|
|
496
|
+
}
|
|
497
|
+
})(),
|
|
498
|
+
)
|
|
499
|
+
|
|
500
|
+
gl.bindTexture(gl.TEXTURE_2D, read.texture)
|
|
501
|
+
gl.texSubImage2D(gl.TEXTURE_2D, 0, 0, 0, WIDTH, HEIGHT, gl.RGBA, gl.UNSIGNED_BYTE, initData)
|
|
502
|
+
|
|
503
|
+
gl.bindTexture(gl.TEXTURE_2D, write.texture)
|
|
504
|
+
gl.texSubImage2D(gl.TEXTURE_2D, 0, 0, 0, WIDTH, HEIGHT, gl.RGBA, gl.UNSIGNED_BYTE, initData)
|
|
505
|
+
|
|
506
|
+
function step() {
|
|
507
|
+
// Run sand simulation step: render to write-framebuffer using read-texture as input
|
|
508
|
+
gl.bindFramebuffer(gl.FRAMEBUFFER, write.framebuffer)
|
|
509
|
+
gl.viewport(0, 0, WIDTH, HEIGHT)
|
|
510
|
+
gl.useProgram(stepProgram)
|
|
511
|
+
|
|
512
|
+
gl.activeTexture(gl.TEXTURE0)
|
|
513
|
+
gl.bindTexture(gl.TEXTURE_2D, read.texture)
|
|
514
|
+
|
|
515
|
+
stepView.uniforms.u_texture.set(0)
|
|
516
|
+
stepView.uniforms.u_texelSize.set(1 / WIDTH, 1 / HEIGHT)
|
|
517
|
+
|
|
518
|
+
stepView.attributes.a_vertex.bind()
|
|
519
|
+
gl.drawArrays(gl.TRIANGLES, 0, 6)
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
function render() {
|
|
523
|
+
// Render to screen
|
|
524
|
+
gl.bindFramebuffer(gl.FRAMEBUFFER, null)
|
|
525
|
+
gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight)
|
|
526
|
+
gl.useProgram(renderProgram)
|
|
527
|
+
|
|
528
|
+
gl.activeTexture(gl.TEXTURE0)
|
|
529
|
+
gl.bindTexture(gl.TEXTURE_2D, write.texture)
|
|
530
|
+
|
|
531
|
+
renderView.uniforms.u_texture.set(0)
|
|
532
|
+
renderView.attributes.a_vertex.bind()
|
|
533
|
+
|
|
534
|
+
gl.drawArrays(gl.TRIANGLES, 0, 6)
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
function animate() {
|
|
538
|
+
if (playing) {
|
|
539
|
+
step()
|
|
540
|
+
render()
|
|
541
|
+
// Swap read and write buffers
|
|
542
|
+
;[read, write] = [write, read]
|
|
543
|
+
requestAnimationFrame(animate)
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
// Start animation
|
|
548
|
+
playing = true
|
|
549
|
+
animate()
|