@stevejtrettel/shader-sandbox 0.1.3 → 0.1.4

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.
Files changed (113) hide show
  1. package/README.md +220 -23
  2. package/bin/cli.js +106 -14
  3. package/dist-lib/app/App.d.ts +143 -15
  4. package/dist-lib/app/App.d.ts.map +1 -1
  5. package/dist-lib/app/App.js +1343 -108
  6. package/dist-lib/app/app.css +349 -24
  7. package/dist-lib/app/types.d.ts +48 -5
  8. package/dist-lib/app/types.d.ts.map +1 -1
  9. package/dist-lib/editor/EditorPanel.d.ts +2 -2
  10. package/dist-lib/editor/EditorPanel.d.ts.map +1 -1
  11. package/dist-lib/editor/EditorPanel.js +1 -1
  12. package/dist-lib/editor/editor-panel.css +55 -32
  13. package/dist-lib/editor/prism-editor.css +16 -16
  14. package/dist-lib/embed.js +1 -1
  15. package/dist-lib/engine/{ShadertoyEngine.d.ts → ShaderEngine.d.ts} +134 -10
  16. package/dist-lib/engine/ShaderEngine.d.ts.map +1 -0
  17. package/dist-lib/engine/ShaderEngine.js +1523 -0
  18. package/dist-lib/engine/glHelpers.d.ts +24 -0
  19. package/dist-lib/engine/glHelpers.d.ts.map +1 -1
  20. package/dist-lib/engine/glHelpers.js +88 -0
  21. package/dist-lib/engine/std140.d.ts +47 -0
  22. package/dist-lib/engine/std140.d.ts.map +1 -0
  23. package/dist-lib/engine/std140.js +119 -0
  24. package/dist-lib/engine/types.d.ts +55 -5
  25. package/dist-lib/engine/types.d.ts.map +1 -1
  26. package/dist-lib/engine/types.js +1 -1
  27. package/dist-lib/index.d.ts +4 -3
  28. package/dist-lib/index.d.ts.map +1 -1
  29. package/dist-lib/index.js +2 -1
  30. package/dist-lib/layouts/SplitLayout.d.ts +2 -1
  31. package/dist-lib/layouts/SplitLayout.d.ts.map +1 -1
  32. package/dist-lib/layouts/SplitLayout.js +3 -0
  33. package/dist-lib/layouts/TabbedLayout.d.ts.map +1 -1
  34. package/dist-lib/layouts/UILayout.d.ts +55 -0
  35. package/dist-lib/layouts/UILayout.d.ts.map +1 -0
  36. package/dist-lib/layouts/UILayout.js +147 -0
  37. package/dist-lib/layouts/default.css +2 -2
  38. package/dist-lib/layouts/index.d.ts +11 -1
  39. package/dist-lib/layouts/index.d.ts.map +1 -1
  40. package/dist-lib/layouts/index.js +17 -1
  41. package/dist-lib/layouts/split.css +33 -31
  42. package/dist-lib/layouts/tabbed.css +127 -74
  43. package/dist-lib/layouts/types.d.ts +14 -3
  44. package/dist-lib/layouts/types.d.ts.map +1 -1
  45. package/dist-lib/main.js +33 -0
  46. package/dist-lib/project/configHelpers.d.ts +45 -0
  47. package/dist-lib/project/configHelpers.d.ts.map +1 -0
  48. package/dist-lib/project/configHelpers.js +196 -0
  49. package/dist-lib/project/generatedLoader.d.ts +2 -2
  50. package/dist-lib/project/generatedLoader.d.ts.map +1 -1
  51. package/dist-lib/project/generatedLoader.js +23 -5
  52. package/dist-lib/project/loadProject.d.ts +6 -6
  53. package/dist-lib/project/loadProject.d.ts.map +1 -1
  54. package/dist-lib/project/loadProject.js +396 -144
  55. package/dist-lib/project/loaderHelper.d.ts +4 -4
  56. package/dist-lib/project/loaderHelper.d.ts.map +1 -1
  57. package/dist-lib/project/loaderHelper.js +278 -116
  58. package/dist-lib/project/types.d.ts +292 -13
  59. package/dist-lib/project/types.d.ts.map +1 -1
  60. package/dist-lib/project/types.js +13 -1
  61. package/dist-lib/styles/base.css +5 -1
  62. package/dist-lib/uniforms/UniformControls.d.ts +60 -0
  63. package/dist-lib/uniforms/UniformControls.d.ts.map +1 -0
  64. package/dist-lib/uniforms/UniformControls.js +518 -0
  65. package/dist-lib/uniforms/UniformStore.d.ts +74 -0
  66. package/dist-lib/uniforms/UniformStore.d.ts.map +1 -0
  67. package/dist-lib/uniforms/UniformStore.js +145 -0
  68. package/dist-lib/uniforms/UniformsPanel.d.ts +53 -0
  69. package/dist-lib/uniforms/UniformsPanel.d.ts.map +1 -0
  70. package/dist-lib/uniforms/UniformsPanel.js +124 -0
  71. package/dist-lib/uniforms/index.d.ts +11 -0
  72. package/dist-lib/uniforms/index.d.ts.map +1 -0
  73. package/dist-lib/uniforms/index.js +8 -0
  74. package/package.json +1 -1
  75. package/src/app/App.ts +1469 -126
  76. package/src/app/app.css +349 -24
  77. package/src/app/types.ts +53 -5
  78. package/src/editor/EditorPanel.ts +5 -5
  79. package/src/editor/editor-panel.css +55 -32
  80. package/src/editor/prism-editor.css +16 -16
  81. package/src/embed.ts +1 -1
  82. package/src/engine/ShaderEngine.ts +1934 -0
  83. package/src/engine/glHelpers.ts +117 -0
  84. package/src/engine/std140.ts +136 -0
  85. package/src/engine/types.ts +69 -5
  86. package/src/index.ts +4 -3
  87. package/src/layouts/SplitLayout.ts +8 -3
  88. package/src/layouts/TabbedLayout.ts +3 -3
  89. package/src/layouts/UILayout.ts +185 -0
  90. package/src/layouts/default.css +2 -2
  91. package/src/layouts/index.ts +20 -1
  92. package/src/layouts/split.css +33 -31
  93. package/src/layouts/tabbed.css +127 -74
  94. package/src/layouts/types.ts +19 -3
  95. package/src/layouts/ui.css +289 -0
  96. package/src/main.ts +39 -1
  97. package/src/project/configHelpers.ts +225 -0
  98. package/src/project/generatedLoader.ts +27 -6
  99. package/src/project/loadProject.ts +459 -173
  100. package/src/project/loaderHelper.ts +377 -130
  101. package/src/project/types.ts +360 -14
  102. package/src/styles/base.css +5 -1
  103. package/src/styles/theme.css +292 -0
  104. package/src/uniforms/UniformControls.ts +660 -0
  105. package/src/uniforms/UniformStore.ts +166 -0
  106. package/src/uniforms/UniformsPanel.ts +163 -0
  107. package/src/uniforms/index.ts +13 -0
  108. package/src/uniforms/uniform-controls.css +342 -0
  109. package/src/uniforms/uniforms-panel.css +277 -0
  110. package/templates/shaders/example-buffer/config.json +1 -0
  111. package/dist-lib/engine/ShadertoyEngine.d.ts.map +0 -1
  112. package/dist-lib/engine/ShadertoyEngine.js +0 -704
  113. package/src/engine/ShadertoyEngine.ts +0 -929
@@ -1,704 +0,0 @@
1
- /**
2
- * Engine - Shadertoy Execution Engine
3
- *
4
- * Implements the execution model described in docs/engine-spec.md.
5
- *
6
- * Responsibilities:
7
- * - Own WebGL resources for passes (programs, VAOs, textures, FBOs).
8
- * - Execute passes each frame in Shadertoy order: BufferA→BufferB→BufferC→BufferD→Image.
9
- * - Bind Shadertoy uniforms (iResolution, iTime, iTimeDelta, iFrame, iMouse).
10
- * - Bind iChannel0..3 according to ChannelSource.
11
- */
12
- import { createProgramFromSources, createFullscreenTriangleVAO, createRenderTargetTexture, createFramebufferWithColorAttachment, createBlackTexture, createKeyboardTexture, updateKeyboardTexture, } from './glHelpers';
13
- // =============================================================================
14
- // Vertex Shader (Shared across all passes)
15
- // =============================================================================
16
- const VERTEX_SHADER_SOURCE = `#version 300 es
17
- precision highp float;
18
-
19
- layout(location = 0) in vec2 position;
20
-
21
- void main() {
22
- gl_Position = vec4(position, 0.0, 1.0);
23
- }
24
- `;
25
- // =============================================================================
26
- // Fragment Shader Boilerplate (before common code)
27
- // =============================================================================
28
- const FRAGMENT_PREAMBLE = `#version 300 es
29
- precision highp float;
30
-
31
- // Shadertoy compatibility: equirectangular texture sampling
32
- const float ST_PI = 3.14159265359;
33
- const float ST_TWOPI = 6.28318530718;
34
- vec2 _st_dirToEquirect(vec3 dir) {
35
- float phi = atan(dir.z, dir.x);
36
- float theta = asin(dir.y);
37
- return vec2(phi / ST_TWOPI + 0.5, theta / ST_PI + 0.5);
38
- }
39
- `;
40
- // Line count computed from actual preamble (for error line mapping)
41
- const PREAMBLE_LINE_COUNT = FRAGMENT_PREAMBLE.split('\n').length - 1; // -1 because split adds empty at end
42
- // =============================================================================
43
- // ShadertoyEngine Implementation
44
- // =============================================================================
45
- export class ShadertoyEngine {
46
- constructor(opts) {
47
- this._frame = 0;
48
- this._time = 0;
49
- this._lastStepTime = null;
50
- this._passes = [];
51
- this._textures = [];
52
- this._keyboardTexture = null;
53
- this._blackTexture = null;
54
- // Keyboard state tracking (Maps keycodes to state)
55
- this._keyStates = new Map(); // true = down, false = up
56
- this._toggleStates = new Map(); // 0.0 or 1.0
57
- // Compilation errors (if any occurred during initialization)
58
- this._compilationErrors = [];
59
- this.gl = opts.gl;
60
- this.project = opts.project;
61
- // Initialize width/height from current drawing buffer
62
- this._width = this.gl.drawingBufferWidth;
63
- this._height = this.gl.drawingBufferHeight;
64
- // 1. Initialize extensions
65
- this.initExtensions();
66
- // 2. Create black texture for unused channels
67
- this._blackTexture = createBlackTexture(this.gl);
68
- // 3. Create keyboard texture (256x3, Shadertoy format)
69
- const keyboardTex = createKeyboardTexture(this.gl);
70
- this._keyboardTexture = {
71
- texture: keyboardTex,
72
- width: 256,
73
- height: 3,
74
- };
75
- // 4. Initialize external textures (from project.textures)
76
- // NOTE: This requires actual image data; for now just stub the array.
77
- // Real implementation would load images here.
78
- this.initProjectTextures();
79
- // 5. Compile shaders + create runtime passes
80
- this.initRuntimePasses();
81
- }
82
- // ===========================================================================
83
- // Public API
84
- // ===========================================================================
85
- get width() {
86
- return this._width;
87
- }
88
- get height() {
89
- return this._height;
90
- }
91
- get stats() {
92
- const dt = this._lastStepTime === null ? 0 : this._time - this._lastStepTime;
93
- return {
94
- frame: this._frame,
95
- time: this._time,
96
- deltaTime: dt,
97
- width: this._width,
98
- height: this._height,
99
- };
100
- }
101
- /**
102
- * Get shader compilation errors (if any occurred during initialization).
103
- * Returns empty array if all shaders compiled successfully.
104
- */
105
- getCompilationErrors() {
106
- return this._compilationErrors;
107
- }
108
- /**
109
- * Check if there were any compilation errors.
110
- */
111
- hasErrors() {
112
- return this._compilationErrors.length > 0;
113
- }
114
- /**
115
- * Get the framebuffer for the Image pass (for presenting to screen).
116
- */
117
- getImageFramebuffer() {
118
- const imagePass = this._passes.find((p) => p.name === 'Image');
119
- return imagePass?.framebuffer ?? null;
120
- }
121
- /**
122
- * Run one full frame of all passes.
123
- *
124
- * @param timeSeconds - global time in seconds (monotone, from App)
125
- * @param mouse - iMouse as [x, y, clickX, clickY]
126
- */
127
- step(timeSeconds, mouse) {
128
- const gl = this.gl;
129
- // Compute time/deltaTime/iFrame
130
- const deltaTime = this._lastStepTime === null ? 0.0 : timeSeconds - this._lastStepTime;
131
- this._lastStepTime = timeSeconds;
132
- this._time = timeSeconds;
133
- const iResolution = [this._width, this._height, 1.0];
134
- const iTime = this._time;
135
- const iTimeDelta = deltaTime;
136
- const iFrame = this._frame;
137
- const iMouse = mouse;
138
- // Set viewport for all passes
139
- gl.viewport(0, 0, this._width, this._height);
140
- // Execute passes in Shadertoy order
141
- const passOrder = ['BufferA', 'BufferB', 'BufferC', 'BufferD', 'Image'];
142
- for (const passName of passOrder) {
143
- const runtimePass = this._passes.find((p) => p.name === passName);
144
- if (!runtimePass)
145
- continue;
146
- this.executePass(runtimePass, {
147
- iResolution,
148
- iTime,
149
- iTimeDelta,
150
- iFrame,
151
- iMouse,
152
- });
153
- // Swap ping-pong textures after pass execution
154
- this.swapPassTextures(runtimePass);
155
- }
156
- // Monotone frame counter (increment AFTER all passes)
157
- this._frame += 1;
158
- }
159
- /**
160
- * Resize all internal render targets to new width/height.
161
- * Does not reset time or frame count.
162
- */
163
- resize(width, height) {
164
- this._width = width;
165
- this._height = height;
166
- const gl = this.gl;
167
- // Reallocate ALL pass textures to new resolution
168
- for (const pass of this._passes) {
169
- // Delete old textures and framebuffer
170
- gl.deleteTexture(pass.currentTexture);
171
- gl.deleteTexture(pass.previousTexture);
172
- gl.deleteFramebuffer(pass.framebuffer);
173
- // Create new textures at new resolution
174
- pass.currentTexture = createRenderTargetTexture(gl, width, height);
175
- pass.previousTexture = createRenderTargetTexture(gl, width, height);
176
- // Create new framebuffer (attached to current texture)
177
- pass.framebuffer = createFramebufferWithColorAttachment(gl, pass.currentTexture);
178
- }
179
- }
180
- /**
181
- * Reset frame counter and clear all render targets.
182
- * Used for playback controls to restart shader from frame 0.
183
- */
184
- reset() {
185
- this._frame = 0;
186
- // Clear all pass textures (both current and previous for ping-pong)
187
- // This is critical for accumulation shaders that read from previous frame
188
- const gl = this.gl;
189
- for (const pass of this._passes) {
190
- // Clear current texture (already attached to framebuffer)
191
- gl.bindFramebuffer(gl.FRAMEBUFFER, pass.framebuffer);
192
- gl.clearColor(0, 0, 0, 0);
193
- gl.clear(gl.COLOR_BUFFER_BIT);
194
- // Also clear previous texture (temporarily attach it)
195
- gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, pass.previousTexture, 0);
196
- gl.clear(gl.COLOR_BUFFER_BIT);
197
- // Re-attach current texture
198
- gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, pass.currentTexture, 0);
199
- }
200
- gl.bindFramebuffer(gl.FRAMEBUFFER, null);
201
- }
202
- /**
203
- * Update keyboard key state (called from App on keydown/keyup events).
204
- *
205
- * @param keycode ASCII keycode (e.g., 65 for 'A')
206
- * @param isDown true if key pressed, false if released
207
- */
208
- updateKeyState(keycode, isDown) {
209
- const wasDown = this._keyStates.get(keycode) || false;
210
- // Update current state
211
- this._keyStates.set(keycode, isDown);
212
- // Toggle on press (down transition)
213
- if (isDown && !wasDown) {
214
- const currentToggle = this._toggleStates.get(keycode) || 0.0;
215
- this._toggleStates.set(keycode, currentToggle === 0.0 ? 1.0 : 0.0);
216
- }
217
- }
218
- /**
219
- * Update keyboard texture with current key states.
220
- * Should be called once per frame before rendering.
221
- */
222
- updateKeyboardTexture() {
223
- if (!this._keyboardTexture) {
224
- return; // No keyboard texture to update
225
- }
226
- updateKeyboardTexture(this.gl, this._keyboardTexture.texture, this._keyStates, this._toggleStates);
227
- }
228
- /**
229
- * Recompile a single pass with new GLSL source code.
230
- * Used for live editing - keeps the old shader running if compilation fails.
231
- *
232
- * @param passName - Name of the pass to recompile ('Image', 'BufferA', etc.)
233
- * @param newSource - New GLSL source code for the pass
234
- * @returns Object with success status and error message if failed
235
- */
236
- recompilePass(passName, newSource) {
237
- const gl = this.gl;
238
- // Find the runtime pass
239
- const runtimePass = this._passes.find((p) => p.name === passName);
240
- if (!runtimePass) {
241
- return { success: false, error: `Pass '${passName}' not found` };
242
- }
243
- // Update the project's pass source (so buildFragmentShader uses it)
244
- const projectPass = this.project.passes[passName];
245
- if (!projectPass) {
246
- return { success: false, error: `Project pass '${passName}' not found` };
247
- }
248
- // Build new fragment shader
249
- const fragmentSource = this.buildFragmentShader(newSource, projectPass.channels);
250
- try {
251
- // Try to compile new program
252
- const newProgram = createProgramFromSources(gl, VERTEX_SHADER_SOURCE, fragmentSource);
253
- // Success! Delete old program and update runtime pass
254
- gl.deleteProgram(runtimePass.uniforms.program);
255
- // Cache new uniform locations
256
- const uniforms = {
257
- program: newProgram,
258
- iResolution: gl.getUniformLocation(newProgram, 'iResolution'),
259
- iTime: gl.getUniformLocation(newProgram, 'iTime'),
260
- iTimeDelta: gl.getUniformLocation(newProgram, 'iTimeDelta'),
261
- iFrame: gl.getUniformLocation(newProgram, 'iFrame'),
262
- iMouse: gl.getUniformLocation(newProgram, 'iMouse'),
263
- iChannel: [
264
- gl.getUniformLocation(newProgram, 'iChannel0'),
265
- gl.getUniformLocation(newProgram, 'iChannel1'),
266
- gl.getUniformLocation(newProgram, 'iChannel2'),
267
- gl.getUniformLocation(newProgram, 'iChannel3'),
268
- ],
269
- };
270
- runtimePass.uniforms = uniforms;
271
- // Update the stored source in the project
272
- projectPass.glslSource = newSource;
273
- // Clear any previous compilation errors for this pass
274
- this._compilationErrors = this._compilationErrors.filter(e => e.passName !== passName);
275
- return { success: true };
276
- }
277
- catch (err) {
278
- // Compilation failed - keep old shader running
279
- const errorMessage = err instanceof Error ? err.message : String(err);
280
- return { success: false, error: errorMessage };
281
- }
282
- }
283
- /**
284
- * Recompile common.glsl and all passes that use it.
285
- * Used for live editing of common code.
286
- *
287
- * @param newCommonSource - New GLSL source code for common.glsl
288
- * @returns Object with success status and errors for each failed pass
289
- */
290
- recompileCommon(newCommonSource) {
291
- const oldCommonSource = this.project.commonSource;
292
- // Temporarily update common source
293
- this.project.commonSource = newCommonSource;
294
- const errors = [];
295
- const passOrder = ['BufferA', 'BufferB', 'BufferC', 'BufferD', 'Image'];
296
- // Try to recompile all passes
297
- for (const passName of passOrder) {
298
- const projectPass = this.project.passes[passName];
299
- if (!projectPass)
300
- continue;
301
- const result = this.recompilePass(passName, projectPass.glslSource);
302
- if (!result.success) {
303
- errors.push({ passName, error: result.error || 'Unknown error' });
304
- }
305
- }
306
- // If any failed, restore old common source and recompile successful ones back
307
- if (errors.length > 0) {
308
- this.project.commonSource = oldCommonSource;
309
- // Recompile passes back to working state
310
- for (const passName of passOrder) {
311
- const projectPass = this.project.passes[passName];
312
- if (!projectPass)
313
- continue;
314
- // Skip passes that failed (they still have old shader)
315
- if (errors.some(e => e.passName === passName))
316
- continue;
317
- // Recompile with old common source
318
- this.recompilePass(passName, projectPass.glslSource);
319
- }
320
- return { success: false, errors };
321
- }
322
- return { success: true, errors: [] };
323
- }
324
- /**
325
- * Delete all GL resources.
326
- */
327
- dispose() {
328
- const gl = this.gl;
329
- // Delete passes (programs, VAOs, FBOs, textures)
330
- for (const pass of this._passes) {
331
- gl.deleteProgram(pass.uniforms.program);
332
- gl.deleteVertexArray(pass.vao);
333
- gl.deleteFramebuffer(pass.framebuffer);
334
- gl.deleteTexture(pass.currentTexture);
335
- gl.deleteTexture(pass.previousTexture);
336
- }
337
- // Delete external textures
338
- for (const tex of this._textures) {
339
- gl.deleteTexture(tex.texture);
340
- }
341
- // Delete keyboard texture
342
- if (this._keyboardTexture) {
343
- gl.deleteTexture(this._keyboardTexture.texture);
344
- }
345
- // Delete black texture
346
- if (this._blackTexture) {
347
- gl.deleteTexture(this._blackTexture);
348
- }
349
- // Clear arrays
350
- this._passes = [];
351
- this._textures = [];
352
- this._keyboardTexture = null;
353
- this._blackTexture = null;
354
- }
355
- // ===========================================================================
356
- // Initialization Helpers
357
- // ===========================================================================
358
- initExtensions() {
359
- const gl = this.gl;
360
- // MUST enable EXT_color_buffer_float for RGBA32F render targets
361
- const ext = gl.getExtension('EXT_color_buffer_float');
362
- if (!ext) {
363
- throw new Error('EXT_color_buffer_float not supported. WebGL2 with float rendering is required.');
364
- }
365
- // Optionally check for OES_texture_float_linear (for smooth filtering of float textures)
366
- // Not strictly required for Shadertoy, but nice to have
367
- gl.getExtension('OES_texture_float_linear');
368
- }
369
- /**
370
- * Initialize external textures based on project.textures.
371
- *
372
- * NOTE: This function as written assumes that actual image loading
373
- * is handled elsewhere. For now we just construct an empty array.
374
- * In a real implementation, you would load images here.
375
- */
376
- initProjectTextures() {
377
- const gl = this.gl;
378
- this._textures = [];
379
- // Load each texture from the project
380
- for (const texDef of this.project.textures) {
381
- // Create a placeholder 1x1 texture immediately
382
- const texture = gl.createTexture();
383
- if (!texture) {
384
- throw new Error('Failed to create texture');
385
- }
386
- // Bind and set initial 1x1 black pixel
387
- gl.bindTexture(gl.TEXTURE_2D, texture);
388
- gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, 1, 1, 0, gl.RGBA, gl.UNSIGNED_BYTE, new Uint8Array([0, 0, 0, 255]));
389
- // Store in runtime array
390
- const runtimeTex = {
391
- name: texDef.name,
392
- texture,
393
- width: 1,
394
- height: 1,
395
- };
396
- this._textures.push(runtimeTex);
397
- // Load the actual image asynchronously
398
- const image = new Image();
399
- image.crossOrigin = 'anonymous';
400
- image.onload = () => {
401
- gl.bindTexture(gl.TEXTURE_2D, texture);
402
- gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image);
403
- // Set filter
404
- const filter = texDef.filter === 'nearest' ? gl.NEAREST : gl.LINEAR;
405
- gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, filter);
406
- gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, filter);
407
- // Set wrap mode
408
- const wrap = texDef.wrap === 'clamp' ? gl.CLAMP_TO_EDGE : gl.REPEAT;
409
- gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, wrap);
410
- gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, wrap);
411
- // Generate mipmaps if using linear filtering
412
- if (texDef.filter === 'linear') {
413
- gl.generateMipmap(gl.TEXTURE_2D);
414
- }
415
- // Update dimensions
416
- runtimeTex.width = image.width;
417
- runtimeTex.height = image.height;
418
- console.log(`Loaded texture '${texDef.name}': ${image.width}x${image.height}`);
419
- };
420
- image.onerror = () => {
421
- console.error(`Failed to load texture '${texDef.name}' from ${texDef.source}`);
422
- };
423
- image.src = texDef.source;
424
- }
425
- }
426
- /**
427
- * Compile shaders, create VAOs/FBOs/textures, and build RuntimePass array.
428
- */
429
- initRuntimePasses() {
430
- const gl = this.gl;
431
- const project = this.project;
432
- // Shared VAO (all passes use the same fullscreen triangle)
433
- const sharedVAO = createFullscreenTriangleVAO(gl);
434
- // Build passes in Shadertoy order
435
- const passOrder = ['BufferA', 'BufferB', 'BufferC', 'BufferD', 'Image'];
436
- for (const passName of passOrder) {
437
- const projectPass = project.passes[passName];
438
- if (!projectPass)
439
- continue;
440
- // Build fragment shader source (outside try so we can access in catch)
441
- const fragmentSource = this.buildFragmentShader(projectPass.glslSource, projectPass.channels);
442
- try {
443
- // Compile program
444
- const program = createProgramFromSources(gl, VERTEX_SHADER_SOURCE, fragmentSource);
445
- // Cache uniform locations
446
- const uniforms = {
447
- program,
448
- iResolution: gl.getUniformLocation(program, 'iResolution'),
449
- iTime: gl.getUniformLocation(program, 'iTime'),
450
- iTimeDelta: gl.getUniformLocation(program, 'iTimeDelta'),
451
- iFrame: gl.getUniformLocation(program, 'iFrame'),
452
- iMouse: gl.getUniformLocation(program, 'iMouse'),
453
- iChannel: [
454
- gl.getUniformLocation(program, 'iChannel0'),
455
- gl.getUniformLocation(program, 'iChannel1'),
456
- gl.getUniformLocation(program, 'iChannel2'),
457
- gl.getUniformLocation(program, 'iChannel3'),
458
- ],
459
- };
460
- // Create ping-pong textures (MUST allocate both for all passes)
461
- const currentTexture = createRenderTargetTexture(gl, this._width, this._height);
462
- const previousTexture = createRenderTargetTexture(gl, this._width, this._height);
463
- // Create framebuffer (attached to current texture)
464
- const framebuffer = createFramebufferWithColorAttachment(gl, currentTexture);
465
- // Build RuntimePass
466
- const runtimePass = {
467
- name: passName,
468
- projectChannels: projectPass.channels,
469
- vao: sharedVAO,
470
- uniforms,
471
- framebuffer,
472
- currentTexture,
473
- previousTexture,
474
- };
475
- this._passes.push(runtimePass);
476
- }
477
- catch (err) {
478
- // Store compilation error with source code for context display
479
- const errorMessage = err instanceof Error ? err.message : String(err);
480
- // Detect if error is from common.glsl
481
- const lineMapping = this.getLineMapping();
482
- const errorLineMatch = errorMessage.match(/ERROR:\s*\d+:(\d+):/);
483
- let isFromCommon = false;
484
- let originalLine = null;
485
- if (errorLineMatch && this.project.commonSource) {
486
- const errorLine = parseInt(errorLineMatch[1], 10);
487
- const commonStartLine = lineMapping.boilerplateLinesBeforeCommon + 2; // +1 for comment, +1 for 1-indexed
488
- const commonEndLine = commonStartLine + lineMapping.commonLineCount - 1;
489
- if (errorLine >= commonStartLine && errorLine <= commonEndLine) {
490
- isFromCommon = true;
491
- // Calculate line number relative to common.glsl
492
- originalLine = errorLine - commonStartLine + 1;
493
- }
494
- }
495
- this._compilationErrors.push({
496
- passName,
497
- error: errorMessage,
498
- source: fragmentSource,
499
- isFromCommon,
500
- originalLine,
501
- });
502
- console.error(`Failed to compile ${passName}:`, errorMessage);
503
- }
504
- }
505
- }
506
- /**
507
- * Calculate line number mappings for error reporting.
508
- * Returns info about where common.glsl code lives in the compiled shader.
509
- */
510
- getLineMapping() {
511
- // +1 for the "// Common code" comment line added before common source
512
- const boilerplateLinesBeforeCommon = PREAMBLE_LINE_COUNT + 1;
513
- const commonLineCount = this.project.commonSource
514
- ? this.project.commonSource.split('\n').length
515
- : 0;
516
- return { boilerplateLinesBeforeCommon, commonLineCount };
517
- }
518
- /**
519
- * Build complete fragment shader source with Shadertoy boilerplate.
520
- *
521
- * @param userSource - The user's GLSL source code
522
- * @param channels - Channel configuration for this pass (to detect cubemap textures)
523
- */
524
- buildFragmentShader(userSource, channels) {
525
- const parts = [FRAGMENT_PREAMBLE];
526
- // Common code (if any)
527
- if (this.project.commonSource) {
528
- parts.push('// Common code');
529
- parts.push(this.project.commonSource);
530
- parts.push('');
531
- }
532
- // Shadertoy built-in uniforms
533
- parts.push(`// Shadertoy built-in uniforms
534
- uniform vec3 iResolution;
535
- uniform float iTime;
536
- uniform float iTimeDelta;
537
- uniform int iFrame;
538
- uniform vec4 iMouse;
539
- uniform sampler2D iChannel0;
540
- uniform sampler2D iChannel1;
541
- uniform sampler2D iChannel2;
542
- uniform sampler2D iChannel3;
543
- `);
544
- // Preprocess user shader code to handle cubemap-style texture sampling
545
- const processedSource = this.preprocessCubemapTextures(userSource, channels);
546
- // User shader code
547
- parts.push('// User shader code');
548
- parts.push(processedSource);
549
- parts.push('');
550
- // mainImage() wrapper
551
- parts.push(`// Main wrapper
552
- out vec4 fragColor;
553
-
554
- void main() {
555
- mainImage(fragColor, gl_FragCoord.xy);
556
- }`);
557
- return parts.join('\n');
558
- }
559
- /**
560
- * Preprocess shader to convert cubemap-style texture() calls to equirectangular.
561
- *
562
- * Uses the channel configuration to determine which channels are cubemaps.
563
- * Only channels explicitly marked as `type: 'cubemap'` in config.json will have
564
- * their texture() calls wrapped with _st_dirToEquirect().
565
- *
566
- * @param source - User's GLSL source code
567
- * @param channels - Channel configuration for this pass
568
- */
569
- preprocessCubemapTextures(source, channels) {
570
- // Build set of channel names that are cubemaps
571
- const cubemapChannels = new Set();
572
- channels.forEach((ch, i) => {
573
- if (ch.kind === 'texture' && ch.cubemap) {
574
- cubemapChannels.add(`iChannel${i}`);
575
- }
576
- });
577
- // If no cubemap channels, return source unchanged
578
- if (cubemapChannels.size === 0) {
579
- return source;
580
- }
581
- // Match: texture(iChannelN, ...)
582
- const textureCallRegex = /texture\s*\(\s*(iChannel[0-3])\s*,\s*([^)]+)\)/g;
583
- return source.replace(textureCallRegex, (match, channel, coord) => {
584
- // Only wrap if this channel is explicitly marked as cubemap
585
- if (cubemapChannels.has(channel)) {
586
- return `texture(${channel}, _st_dirToEquirect(${coord}))`;
587
- }
588
- else {
589
- return match;
590
- }
591
- });
592
- }
593
- // ===========================================================================
594
- // Pass Execution
595
- // ===========================================================================
596
- executePass(runtimePass, builtinUniforms) {
597
- const gl = this.gl;
598
- // Bind framebuffer (write to current texture)
599
- gl.bindFramebuffer(gl.FRAMEBUFFER, runtimePass.framebuffer);
600
- // Use program
601
- gl.useProgram(runtimePass.uniforms.program);
602
- // Bind VAO
603
- gl.bindVertexArray(runtimePass.vao);
604
- // Bind built-in uniforms
605
- this.bindBuiltinUniforms(runtimePass.uniforms, builtinUniforms);
606
- // Bind iChannel textures
607
- this.bindChannelTextures(runtimePass);
608
- // Draw fullscreen triangle
609
- gl.drawArrays(gl.TRIANGLES, 0, 3);
610
- // Unbind
611
- gl.bindVertexArray(null);
612
- gl.useProgram(null);
613
- gl.bindFramebuffer(gl.FRAMEBUFFER, null);
614
- }
615
- bindBuiltinUniforms(uniforms, values) {
616
- const gl = this.gl;
617
- if (uniforms.iResolution) {
618
- gl.uniform3f(uniforms.iResolution, values.iResolution[0], values.iResolution[1], values.iResolution[2]);
619
- }
620
- if (uniforms.iTime) {
621
- gl.uniform1f(uniforms.iTime, values.iTime);
622
- }
623
- if (uniforms.iTimeDelta) {
624
- gl.uniform1f(uniforms.iTimeDelta, values.iTimeDelta);
625
- }
626
- if (uniforms.iFrame) {
627
- gl.uniform1i(uniforms.iFrame, values.iFrame);
628
- }
629
- if (uniforms.iMouse) {
630
- gl.uniform4f(uniforms.iMouse, values.iMouse[0], values.iMouse[1], values.iMouse[2], values.iMouse[3]);
631
- }
632
- }
633
- bindChannelTextures(runtimePass) {
634
- const gl = this.gl;
635
- for (let i = 0; i < 4; i++) {
636
- const channelSource = runtimePass.projectChannels[i];
637
- const texture = this.resolveChannelTexture(channelSource);
638
- // Bind texture to texture unit i
639
- gl.activeTexture(gl.TEXTURE0 + i);
640
- gl.bindTexture(gl.TEXTURE_2D, texture);
641
- // Set uniform to use texture unit i
642
- const uniformLoc = runtimePass.uniforms.iChannel[i];
643
- if (uniformLoc) {
644
- gl.uniform1i(uniformLoc, i);
645
- }
646
- }
647
- }
648
- /**
649
- * Resolve a ChannelSource to an actual WebGLTexture to bind.
650
- */
651
- resolveChannelTexture(source) {
652
- switch (source.kind) {
653
- case 'none':
654
- // Unused channel → bind black texture
655
- if (!this._blackTexture) {
656
- throw new Error('Black texture not initialized');
657
- }
658
- return this._blackTexture;
659
- case 'buffer': {
660
- // Buffer reference → find RuntimePass and return current or previous texture
661
- const targetPass = this._passes.find((p) => p.name === source.buffer);
662
- if (!targetPass) {
663
- throw new Error(`Buffer '${source.buffer}' not found`);
664
- }
665
- // Default to previous frame (safer, matches common use case)
666
- // Only use current frame if explicitly requested with current: true
667
- return source.current ? targetPass.currentTexture : targetPass.previousTexture;
668
- }
669
- case 'texture': {
670
- // External texture → find RuntimeTexture by name
671
- const tex = this._textures.find((t) => t.name === source.name);
672
- if (!tex) {
673
- throw new Error(`Texture '${source.name}' not found`);
674
- }
675
- return tex.texture;
676
- }
677
- case 'keyboard':
678
- // Keyboard texture (always available)
679
- if (!this._keyboardTexture) {
680
- throw new Error('Internal error: keyboard texture not initialized');
681
- }
682
- return this._keyboardTexture.texture;
683
- default:
684
- // Exhaustive check
685
- const _exhaustive = source;
686
- throw new Error(`Unknown channel source: ${JSON.stringify(_exhaustive)}`);
687
- }
688
- }
689
- /**
690
- * Swap current and previous textures for a pass (ping-pong).
691
- * Also reattach framebuffer to new current texture.
692
- */
693
- swapPassTextures(pass) {
694
- const gl = this.gl;
695
- // Swap texture references
696
- const temp = pass.currentTexture;
697
- pass.currentTexture = pass.previousTexture;
698
- pass.previousTexture = temp;
699
- // Re-attach framebuffer to new current texture
700
- gl.bindFramebuffer(gl.FRAMEBUFFER, pass.framebuffer);
701
- gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, pass.currentTexture, 0);
702
- gl.bindFramebuffer(gl.FRAMEBUFFER, null);
703
- }
704
- }