@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,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Project Layer - Config Loader
|
|
2
|
+
* Project Layer - Config Loader (Node/CLI)
|
|
3
3
|
*
|
|
4
|
-
* Loads
|
|
4
|
+
* Loads shader projects from disk into normalized ShaderProject representation.
|
|
5
5
|
* Handles both single-pass (no config) and multi-pass (with config) projects.
|
|
6
6
|
*
|
|
7
7
|
* Based on docs/project-spec.md
|
|
@@ -13,14 +13,28 @@ import {
|
|
|
13
13
|
PassName,
|
|
14
14
|
ChannelSource,
|
|
15
15
|
Channels,
|
|
16
|
-
ChannelValue,
|
|
17
16
|
ChannelJSONObject,
|
|
18
17
|
ShadertoyConfig,
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
18
|
+
StandardConfig,
|
|
19
|
+
StandardBufferConfig,
|
|
20
|
+
ShaderPass,
|
|
21
|
+
ShaderProject,
|
|
22
|
+
ShaderTexture2D,
|
|
22
23
|
PassConfigSimplified,
|
|
24
|
+
UniformDefinitions,
|
|
25
|
+
isArrayUniform,
|
|
23
26
|
} from './types';
|
|
27
|
+
import {
|
|
28
|
+
isPassName,
|
|
29
|
+
parseChannelValue,
|
|
30
|
+
defaultSourceForPass,
|
|
31
|
+
validateConfig,
|
|
32
|
+
CHANNEL_KEYS,
|
|
33
|
+
BUFFER_PASS_NAMES,
|
|
34
|
+
DEFAULT_LAYOUT,
|
|
35
|
+
DEFAULT_CONTROLS,
|
|
36
|
+
DEFAULT_THEME,
|
|
37
|
+
} from './configHelpers';
|
|
24
38
|
|
|
25
39
|
// =============================================================================
|
|
26
40
|
// Helper Functions
|
|
@@ -38,13 +52,6 @@ async function fileExists(p: string): Promise<boolean> {
|
|
|
38
52
|
}
|
|
39
53
|
}
|
|
40
54
|
|
|
41
|
-
/**
|
|
42
|
-
* Type guard for PassName.
|
|
43
|
-
*/
|
|
44
|
-
function isPassName(s: string): s is PassName {
|
|
45
|
-
return s === 'Image' || s === 'BufferA' || s === 'BufferB' || s === 'BufferC' || s === 'BufferD';
|
|
46
|
-
}
|
|
47
|
-
|
|
48
55
|
/**
|
|
49
56
|
* List all .glsl files in a directory.
|
|
50
57
|
*/
|
|
@@ -66,21 +73,21 @@ async function hasTexturesDirWithFiles(root: string): Promise<boolean> {
|
|
|
66
73
|
}
|
|
67
74
|
|
|
68
75
|
/**
|
|
69
|
-
*
|
|
76
|
+
* Resolve common source from config or default common.glsl.
|
|
70
77
|
*/
|
|
71
|
-
function
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
return 'bufferB.glsl';
|
|
79
|
-
case 'BufferC':
|
|
80
|
-
return 'bufferC.glsl';
|
|
81
|
-
case 'BufferD':
|
|
82
|
-
return 'bufferD.glsl';
|
|
78
|
+
async function resolveCommonSource(root: string, commonField?: string): Promise<string | null> {
|
|
79
|
+
if (commonField) {
|
|
80
|
+
const commonPath = path.join(root, commonField);
|
|
81
|
+
if (!(await fileExists(commonPath))) {
|
|
82
|
+
throw new Error(`Common GLSL file '${commonField}' not found in '${root}'.`);
|
|
83
|
+
}
|
|
84
|
+
return await fs.readFile(commonPath, 'utf8');
|
|
83
85
|
}
|
|
86
|
+
const defaultCommonPath = path.join(root, 'common.glsl');
|
|
87
|
+
if (await fileExists(defaultCommonPath)) {
|
|
88
|
+
return await fs.readFile(defaultCommonPath, 'utf8');
|
|
89
|
+
}
|
|
90
|
+
return null;
|
|
84
91
|
}
|
|
85
92
|
|
|
86
93
|
// =============================================================================
|
|
@@ -88,24 +95,23 @@ function defaultSourceForPass(name: PassName): string {
|
|
|
88
95
|
// =============================================================================
|
|
89
96
|
|
|
90
97
|
/**
|
|
91
|
-
* Load a
|
|
98
|
+
* Load a shader project from disk.
|
|
92
99
|
*
|
|
93
100
|
* Automatically detects:
|
|
94
101
|
* - Single-pass mode (no config, just image.glsl)
|
|
95
102
|
* - Multi-pass mode (config.json present)
|
|
96
103
|
*
|
|
97
104
|
* @param root - Absolute path to project directory
|
|
98
|
-
* @returns Fully normalized
|
|
105
|
+
* @returns Fully normalized ShaderProject
|
|
99
106
|
* @throws Error with descriptive message if project is invalid
|
|
100
107
|
*/
|
|
101
|
-
export async function loadProject(root: string): Promise<
|
|
108
|
+
export async function loadProject(root: string): Promise<ShaderProject> {
|
|
102
109
|
const configPath = path.join(root, 'config.json');
|
|
103
110
|
const hasConfig = await fileExists(configPath);
|
|
104
111
|
|
|
105
112
|
if (hasConfig) {
|
|
106
|
-
// Multi-pass mode: parse config
|
|
107
113
|
const raw = await fs.readFile(configPath, 'utf8');
|
|
108
|
-
let config:
|
|
114
|
+
let config: any;
|
|
109
115
|
try {
|
|
110
116
|
config = JSON.parse(raw);
|
|
111
117
|
} catch (err: any) {
|
|
@@ -113,9 +119,13 @@ export async function loadProject(root: string): Promise<ShadertoyProject> {
|
|
|
113
119
|
`Invalid JSON in config.json at '${root}': ${err?.message ?? String(err)}`
|
|
114
120
|
);
|
|
115
121
|
}
|
|
116
|
-
|
|
122
|
+
validateConfig(config, root);
|
|
123
|
+
|
|
124
|
+
if (config.mode === 'shadertoy') {
|
|
125
|
+
return await loadShadertoyProject(root, config as ShadertoyConfig);
|
|
126
|
+
}
|
|
127
|
+
return await loadStandardProject(root, config as StandardConfig);
|
|
117
128
|
} else {
|
|
118
|
-
// Single-pass mode: just image.glsl
|
|
119
129
|
return await loadSinglePassProject(root);
|
|
120
130
|
}
|
|
121
131
|
}
|
|
@@ -132,17 +142,13 @@ export async function loadProject(root: string): Promise<ShadertoyProject> {
|
|
|
132
142
|
* - Cannot have other .glsl files
|
|
133
143
|
* - Cannot have textures/ directory
|
|
134
144
|
* - No common.glsl allowed
|
|
135
|
-
*
|
|
136
|
-
* @param root - Project directory
|
|
137
|
-
* @returns ShadertoyProject with only Image pass
|
|
138
145
|
*/
|
|
139
|
-
async function loadSinglePassProject(root: string): Promise<
|
|
146
|
+
async function loadSinglePassProject(root: string): Promise<ShaderProject> {
|
|
140
147
|
const imagePath = path.join(root, 'image.glsl');
|
|
141
148
|
if (!(await fileExists(imagePath))) {
|
|
142
149
|
throw new Error(`Single-pass project at '${root}' requires 'image.glsl'.`);
|
|
143
150
|
}
|
|
144
151
|
|
|
145
|
-
// Check for extra GLSL files
|
|
146
152
|
const glslFiles = await listGlslFiles(root);
|
|
147
153
|
const extraGlsl = glslFiles.filter((name) => name !== 'image.glsl');
|
|
148
154
|
if (extraGlsl.length > 0) {
|
|
@@ -153,26 +159,24 @@ async function loadSinglePassProject(root: string): Promise<ShadertoyProject> {
|
|
|
153
159
|
);
|
|
154
160
|
}
|
|
155
161
|
|
|
156
|
-
// Check for textures
|
|
157
162
|
if (await hasTexturesDirWithFiles(root)) {
|
|
158
163
|
throw new Error(
|
|
159
164
|
`Project at '${root}' uses textures (in 'textures/' folder) but has no 'config.json'. Add a config file to define texture bindings.`
|
|
160
165
|
);
|
|
161
166
|
}
|
|
162
167
|
|
|
163
|
-
// Load shader source
|
|
164
168
|
const imageSource = await fs.readFile(imagePath, 'utf8');
|
|
165
169
|
const title = path.basename(root);
|
|
166
170
|
|
|
167
|
-
|
|
171
|
+
return {
|
|
172
|
+
mode: 'standard',
|
|
168
173
|
root,
|
|
169
|
-
meta: {
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
controls: false,
|
|
174
|
+
meta: { title, author: null, description: null },
|
|
175
|
+
layout: DEFAULT_LAYOUT,
|
|
176
|
+
theme: DEFAULT_THEME,
|
|
177
|
+
controls: DEFAULT_CONTROLS,
|
|
178
|
+
startPaused: false,
|
|
179
|
+
pixelRatio: null,
|
|
176
180
|
commonSource: null,
|
|
177
181
|
passes: {
|
|
178
182
|
Image: {
|
|
@@ -187,49 +191,96 @@ async function loadSinglePassProject(root: string): Promise<ShadertoyProject> {
|
|
|
187
191
|
},
|
|
188
192
|
},
|
|
189
193
|
textures: [],
|
|
194
|
+
uniforms: {},
|
|
195
|
+
script: null,
|
|
190
196
|
};
|
|
191
|
-
|
|
192
|
-
return project;
|
|
193
197
|
}
|
|
194
198
|
|
|
195
199
|
// =============================================================================
|
|
196
|
-
//
|
|
200
|
+
// Uniform Validation
|
|
197
201
|
// =============================================================================
|
|
198
202
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
return { buffer: value };
|
|
203
|
+
const SCALAR_TYPES = new Set(['float', 'int', 'bool', 'vec2', 'vec3', 'vec4']);
|
|
204
|
+
const ARRAY_TYPES = new Set(['float', 'vec2', 'vec3', 'vec4', 'mat3', 'mat4']);
|
|
205
|
+
const COMPONENT_COUNTS: Record<string, number> = {
|
|
206
|
+
vec2: 2, vec3: 3, vec4: 4,
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
function validateUniforms(uniforms: UniformDefinitions, root: string): void {
|
|
210
|
+
for (const [name, def] of Object.entries(uniforms)) {
|
|
211
|
+
const prefix = `Uniform '${name}' in '${root}'`;
|
|
212
|
+
|
|
213
|
+
if (!def.type) {
|
|
214
|
+
throw new Error(`${prefix}: missing 'type' field`);
|
|
212
215
|
}
|
|
213
|
-
|
|
214
|
-
if (
|
|
215
|
-
|
|
216
|
+
|
|
217
|
+
if (isArrayUniform(def)) {
|
|
218
|
+
if (!ARRAY_TYPES.has(def.type)) {
|
|
219
|
+
throw new Error(`${prefix}: invalid array type '${def.type}'. Expected one of: ${[...ARRAY_TYPES].join(', ')}`);
|
|
220
|
+
}
|
|
221
|
+
if (typeof def.count !== 'number' || def.count < 1 || !Number.isInteger(def.count)) {
|
|
222
|
+
throw new Error(`${prefix}: 'count' must be a positive integer, got ${def.count}`);
|
|
223
|
+
}
|
|
224
|
+
continue;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
if (!SCALAR_TYPES.has(def.type)) {
|
|
228
|
+
throw new Error(`${prefix}: invalid type '${def.type}'. Expected one of: ${[...SCALAR_TYPES].join(', ')}`);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
switch (def.type) {
|
|
232
|
+
case 'float':
|
|
233
|
+
case 'int':
|
|
234
|
+
if (typeof def.value !== 'number') {
|
|
235
|
+
throw new Error(`${prefix}: 'value' must be a number for type '${(def as any).type}', got ${typeof (def as any).value}`);
|
|
236
|
+
}
|
|
237
|
+
if (def.min !== undefined && typeof def.min !== 'number') {
|
|
238
|
+
throw new Error(`${prefix}: 'min' must be a number`);
|
|
239
|
+
}
|
|
240
|
+
if (def.max !== undefined && typeof def.max !== 'number') {
|
|
241
|
+
throw new Error(`${prefix}: 'max' must be a number`);
|
|
242
|
+
}
|
|
243
|
+
if (def.step !== undefined && typeof def.step !== 'number') {
|
|
244
|
+
throw new Error(`${prefix}: 'step' must be a number`);
|
|
245
|
+
}
|
|
246
|
+
break;
|
|
247
|
+
|
|
248
|
+
case 'bool':
|
|
249
|
+
if (typeof def.value !== 'boolean') {
|
|
250
|
+
throw new Error(`${prefix}: 'value' must be a boolean for type 'bool', got ${typeof (def as any).value}`);
|
|
251
|
+
}
|
|
252
|
+
break;
|
|
253
|
+
|
|
254
|
+
case 'vec2':
|
|
255
|
+
case 'vec3':
|
|
256
|
+
case 'vec4': {
|
|
257
|
+
const n = COMPONENT_COUNTS[def.type];
|
|
258
|
+
if (!Array.isArray(def.value) || def.value.length !== n) {
|
|
259
|
+
throw new Error(`${prefix}: 'value' must be an array of ${n} numbers for type '${def.type}'`);
|
|
260
|
+
}
|
|
261
|
+
if (def.value.some((v: any) => typeof v !== 'number')) {
|
|
262
|
+
throw new Error(`${prefix}: all components of 'value' must be numbers`);
|
|
263
|
+
}
|
|
264
|
+
const vecDef = def as { min?: number[]; max?: number[]; step?: number[] };
|
|
265
|
+
for (const field of ['min', 'max', 'step'] as const) {
|
|
266
|
+
const arr = vecDef[field];
|
|
267
|
+
if (arr !== undefined) {
|
|
268
|
+
if (!Array.isArray(arr) || arr.length !== n) {
|
|
269
|
+
throw new Error(`${prefix}: '${field}' must be an array of ${n} numbers for type '${def.type}'`);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
break;
|
|
274
|
+
}
|
|
216
275
|
}
|
|
217
|
-
// Assume texture (file path)
|
|
218
|
-
return { texture: value };
|
|
219
276
|
}
|
|
220
|
-
// Already an object
|
|
221
|
-
return value;
|
|
222
277
|
}
|
|
223
278
|
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
* @returns Normalized ShadertoyProject
|
|
230
|
-
*/
|
|
231
|
-
async function loadProjectWithConfig(root: string, config: ShadertoyConfig): Promise<ShadertoyProject> {
|
|
232
|
-
// Extract pass configs from top level
|
|
279
|
+
// =============================================================================
|
|
280
|
+
// Shadertoy Mode (iChannel-based)
|
|
281
|
+
// =============================================================================
|
|
282
|
+
|
|
283
|
+
async function loadShadertoyProject(root: string, config: ShadertoyConfig): Promise<ShaderProject> {
|
|
233
284
|
const passConfigs = {
|
|
234
285
|
Image: config.Image,
|
|
235
286
|
BufferA: config.BufferA,
|
|
@@ -238,174 +289,302 @@ async function loadProjectWithConfig(root: string, config: ShadertoyConfig): Pro
|
|
|
238
289
|
BufferD: config.BufferD,
|
|
239
290
|
};
|
|
240
291
|
|
|
241
|
-
// Validate: must have Image pass (or be empty config for simple shader)
|
|
242
292
|
const hasAnyPass = passConfigs.Image || passConfigs.BufferA || passConfigs.BufferB ||
|
|
243
293
|
passConfigs.BufferC || passConfigs.BufferD;
|
|
244
294
|
|
|
245
295
|
if (!hasAnyPass) {
|
|
246
|
-
// Empty config = simple Image pass with no channels
|
|
247
296
|
passConfigs.Image = {};
|
|
248
297
|
}
|
|
249
298
|
|
|
250
|
-
|
|
251
|
-
let commonSource: string | null = null;
|
|
252
|
-
if (config.common) {
|
|
253
|
-
const commonPath = path.join(root, config.common);
|
|
254
|
-
if (!(await fileExists(commonPath))) {
|
|
255
|
-
throw new Error(
|
|
256
|
-
`Common GLSL file '${config.common}' not found in '${root}'.`
|
|
257
|
-
);
|
|
258
|
-
}
|
|
259
|
-
commonSource = await fs.readFile(commonPath, 'utf8');
|
|
260
|
-
} else {
|
|
261
|
-
// Check for default common.glsl
|
|
262
|
-
const defaultCommonPath = path.join(root, 'common.glsl');
|
|
263
|
-
if (await fileExists(defaultCommonPath)) {
|
|
264
|
-
commonSource = await fs.readFile(defaultCommonPath, 'utf8');
|
|
265
|
-
}
|
|
266
|
-
}
|
|
299
|
+
const commonSource = await resolveCommonSource(root, config.common);
|
|
267
300
|
|
|
268
|
-
// Texture deduplication
|
|
269
|
-
const textureMap = new Map<string,
|
|
301
|
+
// Texture deduplication
|
|
302
|
+
const textureMap = new Map<string, ShaderTexture2D>();
|
|
270
303
|
|
|
271
|
-
/**
|
|
272
|
-
* Register a texture and return its internal name.
|
|
273
|
-
*/
|
|
274
304
|
function registerTexture(j: { texture: string; filter?: 'nearest' | 'linear'; wrap?: 'clamp' | 'repeat' }): string {
|
|
275
305
|
const filter = j.filter ?? 'linear';
|
|
276
306
|
const wrap = j.wrap ?? 'repeat';
|
|
277
307
|
const key = `${j.texture}|${filter}|${wrap}`;
|
|
278
308
|
|
|
279
|
-
|
|
280
|
-
if (existing)
|
|
281
|
-
return existing.name;
|
|
282
|
-
}
|
|
309
|
+
const existing = textureMap.get(key);
|
|
310
|
+
if (existing) return existing.name;
|
|
283
311
|
|
|
284
312
|
const name = `tex${textureMap.size}`;
|
|
285
|
-
|
|
286
|
-
name,
|
|
287
|
-
source: j.texture,
|
|
288
|
-
filter,
|
|
289
|
-
wrap,
|
|
290
|
-
};
|
|
291
|
-
textureMap.set(key, tex);
|
|
313
|
+
textureMap.set(key, { name, source: j.texture, filter, wrap });
|
|
292
314
|
return name;
|
|
293
315
|
}
|
|
294
316
|
|
|
295
|
-
/**
|
|
296
|
-
* Parse a channel object into ChannelSource.
|
|
297
|
-
*/
|
|
298
317
|
function parseChannelObject(value: ChannelJSONObject, passName: PassName, channelKey: string): ChannelSource {
|
|
299
|
-
// Buffer channel
|
|
300
318
|
if ('buffer' in value) {
|
|
301
319
|
const buf = value.buffer;
|
|
302
320
|
if (!isPassName(buf)) {
|
|
303
|
-
throw new Error(
|
|
304
|
-
`Invalid buffer name '${buf}' for ${channelKey} in pass '${passName}' at '${root}'.`
|
|
305
|
-
);
|
|
321
|
+
throw new Error(`Invalid buffer name '${buf}' for ${channelKey} in pass '${passName}' at '${root}'.`);
|
|
306
322
|
}
|
|
307
|
-
return {
|
|
308
|
-
kind: 'buffer',
|
|
309
|
-
buffer: buf,
|
|
310
|
-
current: !!value.current,
|
|
311
|
-
};
|
|
323
|
+
return { kind: 'buffer', buffer: buf, current: !!value.current };
|
|
312
324
|
}
|
|
313
|
-
|
|
314
|
-
// Texture channel
|
|
315
325
|
if ('texture' in value) {
|
|
316
|
-
|
|
317
|
-
return {
|
|
318
|
-
kind: 'texture',
|
|
319
|
-
name: internalName,
|
|
320
|
-
cubemap: value.type === 'cubemap',
|
|
321
|
-
};
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
// Keyboard channel
|
|
325
|
-
if ('keyboard' in value) {
|
|
326
|
-
return { kind: 'keyboard' };
|
|
326
|
+
return { kind: 'texture', name: registerTexture(value), cubemap: value.type === 'cubemap' };
|
|
327
327
|
}
|
|
328
|
+
if ('keyboard' in value) return { kind: 'keyboard' };
|
|
329
|
+
if ('audio' in value) return { kind: 'audio' };
|
|
330
|
+
if ('webcam' in value) return { kind: 'webcam' };
|
|
331
|
+
if ('video' in value) return { kind: 'video', src: value.video };
|
|
332
|
+
if ('script' in value) return { kind: 'script', name: value.script };
|
|
328
333
|
|
|
329
|
-
throw new Error(
|
|
330
|
-
`Invalid channel object for ${channelKey} in pass '${passName}' at '${root}'.`
|
|
331
|
-
);
|
|
334
|
+
throw new Error(`Invalid channel object for ${channelKey} in pass '${passName}' at '${root}'.`);
|
|
332
335
|
}
|
|
333
336
|
|
|
334
|
-
/**
|
|
335
|
-
* Load a single pass from simplified config.
|
|
336
|
-
*/
|
|
337
337
|
async function loadPass(
|
|
338
338
|
name: PassName,
|
|
339
339
|
passConfig: PassConfigSimplified | undefined
|
|
340
|
-
): Promise<
|
|
340
|
+
): Promise<ShaderPass | undefined> {
|
|
341
341
|
if (!passConfig) return undefined;
|
|
342
342
|
|
|
343
343
|
const sourceRel = passConfig.source ?? defaultSourceForPass(name);
|
|
344
344
|
const sourcePath = path.join(root, sourceRel);
|
|
345
345
|
|
|
346
346
|
if (!(await fileExists(sourcePath))) {
|
|
347
|
-
throw new Error(
|
|
348
|
-
`Source GLSL file for pass '${name}' not found at '${sourceRel}' in '${root}'.`
|
|
349
|
-
);
|
|
347
|
+
throw new Error(`Source GLSL file for pass '${name}' not found at '${sourceRel}' in '${root}'.`);
|
|
350
348
|
}
|
|
351
349
|
|
|
352
350
|
const glslSource = await fs.readFile(sourcePath, 'utf8');
|
|
353
351
|
|
|
354
|
-
// Normalize channels (always 4 channels)
|
|
355
352
|
const channelSources: ChannelSource[] = [];
|
|
356
|
-
const
|
|
357
|
-
|
|
358
|
-
for (const key of channelKeys) {
|
|
353
|
+
for (const key of CHANNEL_KEYS) {
|
|
359
354
|
const rawValue = passConfig[key];
|
|
360
355
|
if (!rawValue) {
|
|
361
356
|
channelSources.push({ kind: 'none' });
|
|
362
357
|
continue;
|
|
363
358
|
}
|
|
364
|
-
|
|
365
|
-
// Parse string shorthand or use object directly
|
|
366
359
|
const parsed = parseChannelValue(rawValue);
|
|
367
360
|
if (!parsed) {
|
|
368
361
|
channelSources.push({ kind: 'none' });
|
|
369
362
|
continue;
|
|
370
363
|
}
|
|
371
|
-
|
|
372
364
|
channelSources.push(parseChannelObject(parsed, name, key));
|
|
373
365
|
}
|
|
374
366
|
|
|
375
|
-
return {
|
|
376
|
-
name,
|
|
377
|
-
glslSource,
|
|
378
|
-
channels: channelSources as Channels,
|
|
379
|
-
};
|
|
367
|
+
return { name, glslSource, channels: channelSources as Channels };
|
|
380
368
|
}
|
|
381
369
|
|
|
382
|
-
// Load all passes
|
|
383
370
|
const imagePass = await loadPass('Image', passConfigs.Image);
|
|
384
371
|
const bufferAPass = await loadPass('BufferA', passConfigs.BufferA);
|
|
385
372
|
const bufferBPass = await loadPass('BufferB', passConfigs.BufferB);
|
|
386
373
|
const bufferCPass = await loadPass('BufferC', passConfigs.BufferC);
|
|
387
374
|
const bufferDPass = await loadPass('BufferD', passConfigs.BufferD);
|
|
388
375
|
|
|
389
|
-
// If no Image pass was loaded but we have buffers, that's an error
|
|
390
376
|
if (!imagePass && (bufferAPass || bufferBPass || bufferCPass || bufferDPass)) {
|
|
391
377
|
throw new Error(`config.json at '${root}' has buffers but no Image pass.`);
|
|
392
378
|
}
|
|
379
|
+
if (!imagePass) {
|
|
380
|
+
throw new Error(`config.json at '${root}' must define an Image pass.`);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
const title = config.title ?? path.basename(root);
|
|
384
|
+
|
|
385
|
+
return {
|
|
386
|
+
mode: 'shadertoy',
|
|
387
|
+
root,
|
|
388
|
+
meta: { title, author: config.author ?? null, description: config.description ?? null },
|
|
389
|
+
layout: config.layout ?? DEFAULT_LAYOUT,
|
|
390
|
+
theme: config.theme ?? DEFAULT_THEME,
|
|
391
|
+
controls: config.controls ?? DEFAULT_CONTROLS,
|
|
392
|
+
startPaused: config.startPaused ?? false,
|
|
393
|
+
pixelRatio: config.pixelRatio ?? null,
|
|
394
|
+
commonSource,
|
|
395
|
+
passes: {
|
|
396
|
+
Image: imagePass,
|
|
397
|
+
BufferA: bufferAPass,
|
|
398
|
+
BufferB: bufferBPass,
|
|
399
|
+
BufferC: bufferCPass,
|
|
400
|
+
BufferD: bufferDPass,
|
|
401
|
+
},
|
|
402
|
+
textures: Array.from(textureMap.values()),
|
|
403
|
+
uniforms: {},
|
|
404
|
+
script: null,
|
|
405
|
+
};
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
// =============================================================================
|
|
409
|
+
// Standard Mode
|
|
410
|
+
// =============================================================================
|
|
411
|
+
|
|
412
|
+
/**
|
|
413
|
+
* Normalize buffers config: array shorthand to object form.
|
|
414
|
+
*/
|
|
415
|
+
function normalizeBuffersConfig(
|
|
416
|
+
buffers: string[] | Record<string, StandardBufferConfig> | undefined
|
|
417
|
+
): Record<string, StandardBufferConfig> {
|
|
418
|
+
if (!buffers) return {};
|
|
419
|
+
if (Array.isArray(buffers)) {
|
|
420
|
+
const result: Record<string, StandardBufferConfig> = {};
|
|
421
|
+
for (const name of buffers) {
|
|
422
|
+
result[name] = {};
|
|
423
|
+
}
|
|
424
|
+
return result;
|
|
425
|
+
}
|
|
426
|
+
return buffers;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
/**
|
|
430
|
+
* Load a standard mode project.
|
|
431
|
+
* Supports both named buffers and pass-level configs (Image/BufferA/etc.).
|
|
432
|
+
*/
|
|
433
|
+
async function loadStandardProject(root: string, config: StandardConfig): Promise<ShaderProject> {
|
|
434
|
+
// Validate uniforms early
|
|
435
|
+
if (config.uniforms) {
|
|
436
|
+
validateUniforms(config.uniforms, root);
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
const commonSource = await resolveCommonSource(root, config.common);
|
|
440
|
+
|
|
441
|
+
// Check if using named buffers or pass-level configs
|
|
442
|
+
const hasNamedBuffers = config.buffers && Object.keys(normalizeBuffersConfig(config.buffers)).length > 0;
|
|
443
|
+
const hasPassConfigs = config.Image || config.BufferA || config.BufferB ||
|
|
444
|
+
config.BufferC || config.BufferD;
|
|
445
|
+
|
|
446
|
+
if (hasNamedBuffers && hasPassConfigs) {
|
|
447
|
+
throw new Error(
|
|
448
|
+
`Standard mode at '${root}' cannot use both named 'buffers' and pass-level configs (Image/BufferA/etc.). Choose one approach.`
|
|
449
|
+
);
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
if (hasNamedBuffers) {
|
|
453
|
+
return loadStandardWithNamedBuffers(root, config, commonSource);
|
|
454
|
+
}
|
|
393
455
|
|
|
394
|
-
|
|
456
|
+
if (hasPassConfigs) {
|
|
457
|
+
return loadStandardWithPassConfigs(root, config, commonSource);
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
// Simple single-pass standard project (config with settings only)
|
|
461
|
+
const imagePath = path.join(root, 'image.glsl');
|
|
462
|
+
if (!(await fileExists(imagePath))) {
|
|
463
|
+
throw new Error(`Standard mode project at '${root}' requires 'image.glsl'.`);
|
|
464
|
+
}
|
|
465
|
+
const imageSource = await fs.readFile(imagePath, 'utf8');
|
|
466
|
+
const title = config.title ?? path.basename(root);
|
|
467
|
+
|
|
468
|
+
return {
|
|
469
|
+
mode: 'standard',
|
|
470
|
+
root,
|
|
471
|
+
meta: { title, author: config.author ?? null, description: config.description ?? null },
|
|
472
|
+
layout: config.layout ?? DEFAULT_LAYOUT,
|
|
473
|
+
theme: config.theme ?? DEFAULT_THEME,
|
|
474
|
+
controls: config.controls ?? DEFAULT_CONTROLS,
|
|
475
|
+
startPaused: config.startPaused ?? false,
|
|
476
|
+
pixelRatio: config.pixelRatio ?? null,
|
|
477
|
+
commonSource,
|
|
478
|
+
passes: {
|
|
479
|
+
Image: {
|
|
480
|
+
name: 'Image',
|
|
481
|
+
glslSource: imageSource,
|
|
482
|
+
channels: [{ kind: 'none' }, { kind: 'none' }, { kind: 'none' }, { kind: 'none' }] as Channels,
|
|
483
|
+
},
|
|
484
|
+
},
|
|
485
|
+
textures: [],
|
|
486
|
+
uniforms: config.uniforms ?? {},
|
|
487
|
+
script: null,
|
|
488
|
+
};
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
/**
|
|
492
|
+
* Standard mode with pass-level configs (Image, BufferA, etc.) + uniforms.
|
|
493
|
+
* Reuses the same iChannel mechanism as shadertoy mode but sets mode to 'standard'.
|
|
494
|
+
*/
|
|
495
|
+
async function loadStandardWithPassConfigs(
|
|
496
|
+
root: string,
|
|
497
|
+
config: StandardConfig,
|
|
498
|
+
commonSource: string | null
|
|
499
|
+
): Promise<ShaderProject> {
|
|
500
|
+
const passConfigs = {
|
|
501
|
+
Image: config.Image,
|
|
502
|
+
BufferA: config.BufferA,
|
|
503
|
+
BufferB: config.BufferB,
|
|
504
|
+
BufferC: config.BufferC,
|
|
505
|
+
BufferD: config.BufferD,
|
|
506
|
+
};
|
|
507
|
+
|
|
508
|
+
// Texture deduplication
|
|
509
|
+
const textureMap = new Map<string, ShaderTexture2D>();
|
|
510
|
+
|
|
511
|
+
function registerTexture(j: { texture: string; filter?: 'nearest' | 'linear'; wrap?: 'clamp' | 'repeat' }): string {
|
|
512
|
+
const filter = j.filter ?? 'linear';
|
|
513
|
+
const wrap = j.wrap ?? 'repeat';
|
|
514
|
+
const key = `${j.texture}|${filter}|${wrap}`;
|
|
515
|
+
const existing = textureMap.get(key);
|
|
516
|
+
if (existing) return existing.name;
|
|
517
|
+
const name = `tex${textureMap.size}`;
|
|
518
|
+
textureMap.set(key, { name, source: j.texture, filter, wrap });
|
|
519
|
+
return name;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
function parseChannelObject(value: ChannelJSONObject, passName: PassName, channelKey: string): ChannelSource {
|
|
523
|
+
if ('buffer' in value) {
|
|
524
|
+
if (!isPassName(value.buffer)) {
|
|
525
|
+
throw new Error(`Invalid buffer name '${value.buffer}' for ${channelKey} in pass '${passName}' at '${root}'.`);
|
|
526
|
+
}
|
|
527
|
+
return { kind: 'buffer', buffer: value.buffer, current: !!value.current };
|
|
528
|
+
}
|
|
529
|
+
if ('texture' in value) {
|
|
530
|
+
return { kind: 'texture', name: registerTexture(value), cubemap: value.type === 'cubemap' };
|
|
531
|
+
}
|
|
532
|
+
if ('keyboard' in value) return { kind: 'keyboard' };
|
|
533
|
+
if ('audio' in value) return { kind: 'audio' };
|
|
534
|
+
if ('webcam' in value) return { kind: 'webcam' };
|
|
535
|
+
if ('video' in value) return { kind: 'video', src: value.video };
|
|
536
|
+
if ('script' in value) return { kind: 'script', name: value.script };
|
|
537
|
+
throw new Error(`Invalid channel object for ${channelKey} in pass '${passName}' at '${root}'.`);
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
async function loadPass(
|
|
541
|
+
name: PassName,
|
|
542
|
+
passConfig: PassConfigSimplified | undefined
|
|
543
|
+
): Promise<ShaderPass | undefined> {
|
|
544
|
+
if (!passConfig) return undefined;
|
|
545
|
+
|
|
546
|
+
const sourceRel = passConfig.source ?? defaultSourceForPass(name);
|
|
547
|
+
const sourcePath = path.join(root, sourceRel);
|
|
548
|
+
if (!(await fileExists(sourcePath))) {
|
|
549
|
+
throw new Error(`Source GLSL file for pass '${name}' not found at '${sourceRel}' in '${root}'.`);
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
const glslSource = await fs.readFile(sourcePath, 'utf8');
|
|
553
|
+
const channelSources: ChannelSource[] = [];
|
|
554
|
+
for (const key of CHANNEL_KEYS) {
|
|
555
|
+
const rawValue = passConfig[key];
|
|
556
|
+
if (!rawValue) { channelSources.push({ kind: 'none' }); continue; }
|
|
557
|
+
const parsed = parseChannelValue(rawValue);
|
|
558
|
+
if (!parsed) { channelSources.push({ kind: 'none' }); continue; }
|
|
559
|
+
channelSources.push(parseChannelObject(parsed, name, key));
|
|
560
|
+
}
|
|
561
|
+
return { name, glslSource, channels: channelSources as Channels };
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
const imagePass = await loadPass('Image', passConfigs.Image);
|
|
565
|
+
const bufferAPass = await loadPass('BufferA', passConfigs.BufferA);
|
|
566
|
+
const bufferBPass = await loadPass('BufferB', passConfigs.BufferB);
|
|
567
|
+
const bufferCPass = await loadPass('BufferC', passConfigs.BufferC);
|
|
568
|
+
const bufferDPass = await loadPass('BufferD', passConfigs.BufferD);
|
|
569
|
+
|
|
570
|
+
if (!imagePass && (bufferAPass || bufferBPass || bufferCPass || bufferDPass)) {
|
|
571
|
+
throw new Error(`config.json at '${root}' has buffers but no Image pass.`);
|
|
572
|
+
}
|
|
395
573
|
if (!imagePass) {
|
|
396
574
|
throw new Error(`config.json at '${root}' must define an Image pass.`);
|
|
397
575
|
}
|
|
398
576
|
|
|
399
|
-
// Build metadata
|
|
400
577
|
const title = config.title ?? path.basename(root);
|
|
401
|
-
const author = config.author ?? null;
|
|
402
|
-
const description = config.description ?? null;
|
|
403
578
|
|
|
404
|
-
|
|
579
|
+
return {
|
|
580
|
+
mode: 'standard',
|
|
405
581
|
root,
|
|
406
|
-
meta: { title, author, description },
|
|
407
|
-
layout: config.layout ??
|
|
408
|
-
|
|
582
|
+
meta: { title, author: config.author ?? null, description: config.description ?? null },
|
|
583
|
+
layout: config.layout ?? DEFAULT_LAYOUT,
|
|
584
|
+
theme: config.theme ?? DEFAULT_THEME,
|
|
585
|
+
controls: config.controls ?? DEFAULT_CONTROLS,
|
|
586
|
+
startPaused: config.startPaused ?? false,
|
|
587
|
+
pixelRatio: config.pixelRatio ?? null,
|
|
409
588
|
commonSource,
|
|
410
589
|
passes: {
|
|
411
590
|
Image: imagePass,
|
|
@@ -415,7 +594,114 @@ async function loadProjectWithConfig(root: string, config: ShadertoyConfig): Pro
|
|
|
415
594
|
BufferD: bufferDPass,
|
|
416
595
|
},
|
|
417
596
|
textures: Array.from(textureMap.values()),
|
|
597
|
+
uniforms: config.uniforms ?? {},
|
|
598
|
+
script: null,
|
|
599
|
+
};
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
/**
|
|
603
|
+
* Standard mode with named buffers and textures.
|
|
604
|
+
*/
|
|
605
|
+
async function loadStandardWithNamedBuffers(
|
|
606
|
+
root: string,
|
|
607
|
+
config: StandardConfig,
|
|
608
|
+
commonSource: string | null
|
|
609
|
+
): Promise<ShaderProject> {
|
|
610
|
+
const buffersConfig = normalizeBuffersConfig(config.buffers);
|
|
611
|
+
const bufferNames = Object.keys(buffersConfig);
|
|
612
|
+
|
|
613
|
+
if (bufferNames.length > 4) {
|
|
614
|
+
throw new Error(
|
|
615
|
+
`Standard mode at '${root}' supports max 4 buffers, got ${bufferNames.length}: ${bufferNames.join(', ')}`
|
|
616
|
+
);
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
const bufferNameToPass = new Map<string, PassName>();
|
|
620
|
+
for (let i = 0; i < bufferNames.length; i++) {
|
|
621
|
+
bufferNameToPass.set(bufferNames[i], BUFFER_PASS_NAMES[i]);
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
// Texture deduplication
|
|
625
|
+
const textureMap = new Map<string, ShaderTexture2D>();
|
|
626
|
+
|
|
627
|
+
function registerTexture(source: string, filter: 'nearest' | 'linear' = 'linear', wrap: 'clamp' | 'repeat' = 'repeat'): string {
|
|
628
|
+
const key = `${source}|${filter}|${wrap}`;
|
|
629
|
+
const existing = textureMap.get(key);
|
|
630
|
+
if (existing) return existing.name;
|
|
631
|
+
const name = `tex${textureMap.size}`;
|
|
632
|
+
textureMap.set(key, { name, source, filter, wrap });
|
|
633
|
+
return name;
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
// Build namedSamplers
|
|
637
|
+
const namedSamplers = new Map<string, ChannelSource>();
|
|
638
|
+
|
|
639
|
+
for (const [bufName, passName] of bufferNameToPass) {
|
|
640
|
+
namedSamplers.set(bufName, { kind: 'buffer', buffer: passName, current: false });
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
for (const [texName, texValue] of Object.entries(config.textures ?? {})) {
|
|
644
|
+
if (texValue === 'keyboard') {
|
|
645
|
+
namedSamplers.set(texName, { kind: 'keyboard' });
|
|
646
|
+
} else if (texValue === 'audio') {
|
|
647
|
+
namedSamplers.set(texName, { kind: 'audio' });
|
|
648
|
+
} else if (texValue === 'webcam') {
|
|
649
|
+
namedSamplers.set(texName, { kind: 'webcam' });
|
|
650
|
+
} else {
|
|
651
|
+
const internalName = registerTexture(texValue);
|
|
652
|
+
namedSamplers.set(texName, { kind: 'texture', name: internalName, cubemap: false });
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
const noChannels: Channels = [{ kind: 'none' }, { kind: 'none' }, { kind: 'none' }, { kind: 'none' }];
|
|
657
|
+
|
|
658
|
+
// Load Image pass
|
|
659
|
+
const imagePath = path.join(root, 'image.glsl');
|
|
660
|
+
if (!(await fileExists(imagePath))) {
|
|
661
|
+
throw new Error(`Standard mode project at '${root}' requires 'image.glsl'.`);
|
|
662
|
+
}
|
|
663
|
+
const imageSource = await fs.readFile(imagePath, 'utf8');
|
|
664
|
+
|
|
665
|
+
const passes: ShaderProject['passes'] = {
|
|
666
|
+
Image: {
|
|
667
|
+
name: 'Image',
|
|
668
|
+
glslSource: imageSource,
|
|
669
|
+
channels: noChannels,
|
|
670
|
+
namedSamplers: new Map(namedSamplers),
|
|
671
|
+
},
|
|
418
672
|
};
|
|
419
673
|
|
|
420
|
-
|
|
674
|
+
// Load buffer passes
|
|
675
|
+
for (const [bufName, passName] of bufferNameToPass) {
|
|
676
|
+
const sourcePath = path.join(root, `${bufName}.glsl`);
|
|
677
|
+
if (!(await fileExists(sourcePath))) {
|
|
678
|
+
throw new Error(`Buffer '${bufName}' requires '${bufName}.glsl' in '${root}'.`);
|
|
679
|
+
}
|
|
680
|
+
const glslSource = await fs.readFile(sourcePath, 'utf8');
|
|
681
|
+
|
|
682
|
+
passes[passName] = {
|
|
683
|
+
name: passName,
|
|
684
|
+
glslSource,
|
|
685
|
+
channels: noChannels,
|
|
686
|
+
namedSamplers: new Map(namedSamplers),
|
|
687
|
+
};
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
const title = config.title ?? path.basename(root);
|
|
691
|
+
|
|
692
|
+
return {
|
|
693
|
+
mode: 'standard',
|
|
694
|
+
root,
|
|
695
|
+
meta: { title, author: config.author ?? null, description: config.description ?? null },
|
|
696
|
+
layout: config.layout ?? DEFAULT_LAYOUT,
|
|
697
|
+
theme: config.theme ?? DEFAULT_THEME,
|
|
698
|
+
controls: config.controls ?? DEFAULT_CONTROLS,
|
|
699
|
+
startPaused: config.startPaused ?? false,
|
|
700
|
+
pixelRatio: config.pixelRatio ?? null,
|
|
701
|
+
commonSource,
|
|
702
|
+
passes,
|
|
703
|
+
textures: Array.from(textureMap.values()),
|
|
704
|
+
uniforms: config.uniforms ?? {},
|
|
705
|
+
script: null,
|
|
706
|
+
};
|
|
421
707
|
}
|