@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.
- package/README.md +220 -23
- package/bin/cli.js +106 -14
- package/dist-lib/app/App.d.ts +143 -15
- package/dist-lib/app/App.d.ts.map +1 -1
- package/dist-lib/app/App.js +1343 -108
- package/dist-lib/app/app.css +349 -24
- package/dist-lib/app/types.d.ts +48 -5
- package/dist-lib/app/types.d.ts.map +1 -1
- package/dist-lib/editor/EditorPanel.d.ts +2 -2
- package/dist-lib/editor/EditorPanel.d.ts.map +1 -1
- package/dist-lib/editor/EditorPanel.js +1 -1
- package/dist-lib/editor/editor-panel.css +55 -32
- package/dist-lib/editor/prism-editor.css +16 -16
- package/dist-lib/embed.js +1 -1
- package/dist-lib/engine/{ShadertoyEngine.d.ts → ShaderEngine.d.ts} +134 -10
- package/dist-lib/engine/ShaderEngine.d.ts.map +1 -0
- package/dist-lib/engine/ShaderEngine.js +1523 -0
- package/dist-lib/engine/glHelpers.d.ts +24 -0
- package/dist-lib/engine/glHelpers.d.ts.map +1 -1
- package/dist-lib/engine/glHelpers.js +88 -0
- package/dist-lib/engine/std140.d.ts +47 -0
- package/dist-lib/engine/std140.d.ts.map +1 -0
- package/dist-lib/engine/std140.js +119 -0
- package/dist-lib/engine/types.d.ts +55 -5
- package/dist-lib/engine/types.d.ts.map +1 -1
- package/dist-lib/engine/types.js +1 -1
- package/dist-lib/index.d.ts +4 -3
- package/dist-lib/index.d.ts.map +1 -1
- package/dist-lib/index.js +2 -1
- package/dist-lib/layouts/SplitLayout.d.ts +2 -1
- package/dist-lib/layouts/SplitLayout.d.ts.map +1 -1
- package/dist-lib/layouts/SplitLayout.js +3 -0
- package/dist-lib/layouts/TabbedLayout.d.ts.map +1 -1
- package/dist-lib/layouts/UILayout.d.ts +55 -0
- package/dist-lib/layouts/UILayout.d.ts.map +1 -0
- package/dist-lib/layouts/UILayout.js +147 -0
- package/dist-lib/layouts/default.css +2 -2
- package/dist-lib/layouts/index.d.ts +11 -1
- package/dist-lib/layouts/index.d.ts.map +1 -1
- package/dist-lib/layouts/index.js +17 -1
- package/dist-lib/layouts/split.css +33 -31
- package/dist-lib/layouts/tabbed.css +127 -74
- package/dist-lib/layouts/types.d.ts +14 -3
- package/dist-lib/layouts/types.d.ts.map +1 -1
- package/dist-lib/main.js +33 -0
- package/dist-lib/project/configHelpers.d.ts +45 -0
- package/dist-lib/project/configHelpers.d.ts.map +1 -0
- package/dist-lib/project/configHelpers.js +196 -0
- package/dist-lib/project/generatedLoader.d.ts +2 -2
- package/dist-lib/project/generatedLoader.d.ts.map +1 -1
- package/dist-lib/project/generatedLoader.js +23 -5
- package/dist-lib/project/loadProject.d.ts +6 -6
- package/dist-lib/project/loadProject.d.ts.map +1 -1
- package/dist-lib/project/loadProject.js +396 -144
- package/dist-lib/project/loaderHelper.d.ts +4 -4
- package/dist-lib/project/loaderHelper.d.ts.map +1 -1
- package/dist-lib/project/loaderHelper.js +278 -116
- package/dist-lib/project/types.d.ts +292 -13
- package/dist-lib/project/types.d.ts.map +1 -1
- package/dist-lib/project/types.js +13 -1
- package/dist-lib/styles/base.css +5 -1
- package/dist-lib/uniforms/UniformControls.d.ts +60 -0
- package/dist-lib/uniforms/UniformControls.d.ts.map +1 -0
- package/dist-lib/uniforms/UniformControls.js +518 -0
- package/dist-lib/uniforms/UniformStore.d.ts +74 -0
- package/dist-lib/uniforms/UniformStore.d.ts.map +1 -0
- package/dist-lib/uniforms/UniformStore.js +145 -0
- package/dist-lib/uniforms/UniformsPanel.d.ts +53 -0
- package/dist-lib/uniforms/UniformsPanel.d.ts.map +1 -0
- package/dist-lib/uniforms/UniformsPanel.js +124 -0
- package/dist-lib/uniforms/index.d.ts +11 -0
- package/dist-lib/uniforms/index.d.ts.map +1 -0
- package/dist-lib/uniforms/index.js +8 -0
- package/package.json +1 -1
- package/src/app/App.ts +1469 -126
- package/src/app/app.css +349 -24
- package/src/app/types.ts +53 -5
- package/src/editor/EditorPanel.ts +5 -5
- package/src/editor/editor-panel.css +55 -32
- package/src/editor/prism-editor.css +16 -16
- package/src/embed.ts +1 -1
- package/src/engine/ShaderEngine.ts +1934 -0
- package/src/engine/glHelpers.ts +117 -0
- package/src/engine/std140.ts +136 -0
- package/src/engine/types.ts +69 -5
- package/src/index.ts +4 -3
- package/src/layouts/SplitLayout.ts +8 -3
- package/src/layouts/TabbedLayout.ts +3 -3
- package/src/layouts/UILayout.ts +185 -0
- package/src/layouts/default.css +2 -2
- package/src/layouts/index.ts +20 -1
- package/src/layouts/split.css +33 -31
- package/src/layouts/tabbed.css +127 -74
- package/src/layouts/types.ts +19 -3
- package/src/layouts/ui.css +289 -0
- package/src/main.ts +39 -1
- package/src/project/configHelpers.ts +225 -0
- package/src/project/generatedLoader.ts +27 -6
- package/src/project/loadProject.ts +459 -173
- package/src/project/loaderHelper.ts +377 -130
- package/src/project/types.ts +360 -14
- package/src/styles/base.css +5 -1
- package/src/styles/theme.css +292 -0
- package/src/uniforms/UniformControls.ts +660 -0
- package/src/uniforms/UniformStore.ts +166 -0
- package/src/uniforms/UniformsPanel.ts +163 -0
- package/src/uniforms/index.ts +13 -0
- package/src/uniforms/uniform-controls.css +342 -0
- package/src/uniforms/uniforms-panel.css +277 -0
- package/templates/shaders/example-buffer/config.json +1 -0
- package/dist-lib/engine/ShadertoyEngine.d.ts.map +0 -1
- package/dist-lib/engine/ShadertoyEngine.js +0 -704
- 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
|
-
}
|