@shaderfrog/core 0.0.1

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.
@@ -0,0 +1,571 @@
1
+ import { Vector2, Vector3, Vector4, Color } from 'three';
2
+ import { Program } from '@shaderfrog/glsl-parser/ast';
3
+ import {
4
+ Graph,
5
+ NodeParser,
6
+ NodeType,
7
+ ShaderStage,
8
+ prepopulatePropertyInputs,
9
+ mangleMainFn,
10
+ } from '../../core/graph';
11
+ import importers from './importers';
12
+
13
+ import { Engine, EngineContext, EngineNodeType } from '../../core/engine';
14
+ import { GraphNode, doesLinkThruShader, nodeName } from '../../core/graph';
15
+ import {
16
+ returnGlPosition,
17
+ returnGlPositionHardCoded,
18
+ returnGlPositionVec3Right,
19
+ } from '../../ast/manipulate';
20
+ import {
21
+ CodeNode,
22
+ NodeProperty,
23
+ property,
24
+ SourceNode,
25
+ } from '../../core/nodes/code-nodes';
26
+ import { NodeInput, NodePosition } from '../../core/nodes/core-node';
27
+ import { DataNode, UniformDataType } from '../../core/nodes/data-nodes';
28
+ import {
29
+ namedAttributeStrategy,
30
+ texture2DStrategy,
31
+ uniformStrategy,
32
+ } from '../../core/strategy';
33
+
34
+ const log = (...args: any[]) =>
35
+ console.log.call(console, '\x1b[35m(three)\x1b[0m', ...args);
36
+
37
+ export const physicalNode = (
38
+ id: string,
39
+ name: string,
40
+ groupId: string | null | undefined,
41
+ position: NodePosition,
42
+ uniforms: UniformDataType[],
43
+ stage: ShaderStage | undefined,
44
+ nextStageNodeId?: string
45
+ ): CodeNode =>
46
+ prepopulatePropertyInputs({
47
+ id,
48
+ name,
49
+ groupId,
50
+ position,
51
+ type: EngineNodeType.physical,
52
+ config: {
53
+ uniforms,
54
+ version: 3,
55
+ mangle: false,
56
+ preprocess: true,
57
+ properties: [
58
+ property('Color', 'color', 'rgb', 'uniform_diffuse'),
59
+ property('Texture', 'map', 'texture', 'filler_map'),
60
+ property('Normal Map', 'normalMap', 'texture', 'filler_normalMap'),
61
+ property('Normal Scale', 'normalScale', 'vector2'),
62
+ property('Metalness', 'metalness', 'number', 'uniform_metalness'),
63
+ property('Roughness', 'roughness', 'number', 'uniform_roughness'),
64
+ property(
65
+ 'Roughness Map',
66
+ 'roughnessMap',
67
+ 'texture',
68
+ 'filler_roughnessMap'
69
+ ),
70
+ property('Displacement Map', 'displacementMap', 'texture'),
71
+ // MeshPhysicalMaterial gets envMap from the scene. MeshStandardMaterial
72
+ // gets it from the material
73
+ // property('Env Map', 'envMap', 'samplerCube'),
74
+ property('Transmission', 'transmission', 'number'),
75
+ property(
76
+ 'Transmission Map',
77
+ 'transmissionMap',
78
+ 'texture',
79
+ 'filler_transmissionMap'
80
+ ),
81
+ property('Thickness', 'thickness', 'number'),
82
+ property('Index of Refraction', 'ior', 'number'),
83
+ property('Sheen', 'sheen', 'number'),
84
+ property('Reflectivity', 'reflectivity', 'number'),
85
+ property('Clearcoat', 'clearcoat', 'number'),
86
+ property('Iridescence', 'iridescence', 'number'),
87
+ property('Iridescence IOR', 'iridescenceIOR', 'number'),
88
+ property(
89
+ 'Iridescence Thickness Range',
90
+ 'iridescenceThicknessRange',
91
+ 'array',
92
+ undefined,
93
+ ['100', '400']
94
+ ),
95
+ ],
96
+ hardCodedProperties: {
97
+ isMeshPhysicalMaterial: true,
98
+ isMeshStandardMaterial: true,
99
+ },
100
+ strategies: [
101
+ uniformStrategy(),
102
+ stage === 'fragment'
103
+ ? texture2DStrategy()
104
+ : namedAttributeStrategy('position'),
105
+ ],
106
+ },
107
+ inputs: [],
108
+ outputs: [
109
+ {
110
+ name: 'vector4',
111
+ category: 'data',
112
+ id: '1',
113
+ },
114
+ ],
115
+ source: '',
116
+ stage,
117
+ nextStageNodeId,
118
+ });
119
+
120
+ const cacher = (
121
+ engineContext: EngineContext,
122
+ graph: Graph,
123
+ node: SourceNode,
124
+ sibling: SourceNode,
125
+ newValue: (...args: any[]) => any
126
+ ) => {
127
+ const cacheKey = programCacheKey(engineContext, graph, node, sibling);
128
+
129
+ if (engineContext.runtime.cache.data[cacheKey]) {
130
+ log('Cache hit', cacheKey);
131
+ } else {
132
+ log('Cache miss', cacheKey);
133
+ }
134
+ const materialData = engineContext.runtime.cache.data[cacheKey] || newValue();
135
+
136
+ engineContext.runtime.cache.data[cacheKey] = materialData;
137
+ engineContext.runtime.engineMaterial = materialData.material;
138
+
139
+ // TODO: We mutate the nodes here, can we avoid that later?
140
+ node.source =
141
+ node.stage === 'fragment' ? materialData.fragment : materialData.vertex;
142
+ sibling.source =
143
+ sibling.stage === 'fragment' ? materialData.fragment : materialData.vertex;
144
+ };
145
+
146
+ const onBeforeCompileMegaShader = (
147
+ engineContext: EngineContext,
148
+ newMat: any
149
+ ) => {
150
+ log('compiling three megashader!');
151
+ const { renderer, sceneData, scene, camera } = engineContext.runtime;
152
+ const { mesh } = sceneData;
153
+
154
+ // Temporarily swap the mesh material to the new one, since materials can
155
+ // be mesh specific, render, then get its source code
156
+ const originalMaterial = mesh.material;
157
+ mesh.material = newMat;
158
+ renderer.compile(scene, camera);
159
+
160
+ // The references to the compiled shaders in WebGL
161
+ const fragmentRef = renderer.properties
162
+ .get(mesh.material)
163
+ .programs.values()
164
+ .next().value.fragmentShader;
165
+ const vertexRef = renderer.properties
166
+ .get(mesh.material)
167
+ .programs.values()
168
+ .next().value.vertexShader;
169
+
170
+ const gl = renderer.getContext();
171
+ const fragment = gl.getShaderSource(fragmentRef);
172
+ const vertex = gl.getShaderSource(vertexRef);
173
+
174
+ // Reset the material on the mesh, since the shader we're computing context
175
+ // for might not be the one actually want on the mesh - like if a toon node
176
+ // was added to the graph but not connected
177
+ mesh.material = originalMaterial;
178
+
179
+ // Do we even need to do this? This is just for debugging right? Using the
180
+ // source on the node is the important thing.
181
+ return {
182
+ material: newMat,
183
+ fragmentRef,
184
+ vertexRef,
185
+ fragment,
186
+ vertex,
187
+ };
188
+ };
189
+
190
+ const megaShaderMainpulateAst: NodeParser['manipulateAst'] = (
191
+ engineContext,
192
+ engine,
193
+ graph,
194
+ node,
195
+ ast,
196
+ inputEdges
197
+ ) => {
198
+ const programAst = ast as Program;
199
+ const mainName = 'main' || nodeName(node);
200
+ if (node.stage === 'vertex') {
201
+ if (doesLinkThruShader(graph, node)) {
202
+ returnGlPositionHardCoded(mainName, programAst, 'vec3', 'transformed');
203
+ } else {
204
+ returnGlPosition(mainName, programAst);
205
+ }
206
+ }
207
+
208
+ // We specify engine nodes are mangle: false, which is the graph step that
209
+ // handles renaming the main fn, so we have to do it ourselves
210
+ mangleMainFn(programAst, node);
211
+ return programAst;
212
+ };
213
+
214
+ const nodeCacheKey = (graph: Graph, node: SourceNode) => {
215
+ return (
216
+ '[ID:' +
217
+ node.id +
218
+ 'Edges:' +
219
+ graph.edges
220
+ .filter((edge) => edge.to === node.id)
221
+ .map((edge) => `(${edge.to}->${edge.input})`)
222
+ .sort()
223
+ .join(',') +
224
+ ']'
225
+ // Currently excluding node inputs because these are calculated *after*
226
+ // the onbeforecompile, so the next compile, they'll all change!
227
+ // node.inputs.map((i) => `${i.id}${i.bakeable}`)
228
+ );
229
+ };
230
+
231
+ const programCacheKey = (
232
+ engineContext: EngineContext,
233
+ graph: Graph,
234
+ node: SourceNode,
235
+ sibling: SourceNode
236
+ ) => {
237
+ // The megashader source is dependent on scene information, like the number
238
+ // and type of lights in the scene. This kinda sucks - it's duplicating
239
+ // three's material cache key, and is coupled to how three builds shaders
240
+ const { three, scene } = engineContext.runtime;
241
+ const lights: string[] = [];
242
+ scene.traverse((obj: any) => {
243
+ if (obj instanceof three.Light) {
244
+ lights.push(obj.type as string);
245
+ }
246
+ });
247
+
248
+ return (
249
+ [node, sibling]
250
+ .sort((a, b) => a.id.localeCompare(b.id))
251
+ .map((n) => nodeCacheKey(graph, n))
252
+ .join('-') +
253
+ '|Lights:' +
254
+ lights.join(',') +
255
+ '|Envtex:' +
256
+ scene.environmentTexture
257
+ );
258
+ };
259
+
260
+ const threeMaterialProperties = (
261
+ three: any,
262
+ graph: Graph,
263
+ node: SourceNode,
264
+ sibling?: SourceNode
265
+ ): Record<string, any> => {
266
+ // Find inputs to this node that are dependent on a property of the material
267
+ const propertyInputs = node.inputs
268
+ .filter((i) => i.property)
269
+ .reduce<Record<string, NodeInput>>(
270
+ (acc, input) => ({ ...acc, [input.id]: input }),
271
+ {}
272
+ );
273
+
274
+ // Then look for any edges into those inputs and set the material property
275
+ return graph.edges
276
+ .filter((edge) => edge.to === node.id || edge.to === sibling?.id)
277
+ .reduce<Record<string, any>>((acc, edge) => {
278
+ // Check if we've plugged into an input for a property
279
+ const propertyInput = propertyInputs[edge.input];
280
+ if (propertyInput) {
281
+ // Find the property itself
282
+ const property = (node.config.properties || []).find(
283
+ (p) => p.property === propertyInput.property
284
+ ) as NodeProperty;
285
+
286
+ // Initialize the property on the material
287
+ if (property.type === 'texture') {
288
+ acc[property.property] = new three.Texture();
289
+ } else if (property.type === 'number') {
290
+ acc[property.property] = 0.5;
291
+ } else if (property.type === 'rgb') {
292
+ acc[property.property] = new three.Color(1, 1, 1);
293
+ } else if (property.type === 'rgba') {
294
+ acc[property.property] = new three.Color(1, 1, 1, 1);
295
+ }
296
+ }
297
+ return acc;
298
+ }, {});
299
+ };
300
+
301
+ export type ThreeRuntime = {
302
+ scene: any;
303
+ camera: any;
304
+ renderer: any;
305
+ three: any;
306
+ sceneData: any;
307
+ engineMaterial: any;
308
+ index: number;
309
+ cache: {
310
+ data: {
311
+ [key: string]: any;
312
+ };
313
+ nodes: {
314
+ [id: string]: {
315
+ fragmentRef: any;
316
+ vertexRef: any;
317
+ fragment: string;
318
+ vertex: string;
319
+ };
320
+ };
321
+ };
322
+ };
323
+
324
+ const evaluateNode = (node: DataNode) => {
325
+ if (node.type === 'number') {
326
+ return parseFloat(node.value);
327
+ }
328
+
329
+ if (node.type === 'vector2') {
330
+ return new Vector2(parseFloat(node.value[0]), parseFloat(node.value[1]));
331
+ } else if (node.type === 'vector3') {
332
+ return new Vector3(
333
+ parseFloat(node.value[0]),
334
+ parseFloat(node.value[1]),
335
+ parseFloat(node.value[2])
336
+ );
337
+ } else if (node.type === 'vector4') {
338
+ return new Vector4(
339
+ parseFloat(node.value[0]),
340
+ parseFloat(node.value[1]),
341
+ parseFloat(node.value[2]),
342
+ parseFloat(node.value[3])
343
+ );
344
+ } else if (node.type === 'rgb') {
345
+ return new Color(
346
+ parseFloat(node.value[0]),
347
+ parseFloat(node.value[1]),
348
+ parseFloat(node.value[2])
349
+ );
350
+ } else if (node.type === 'rgba') {
351
+ return new Vector4(
352
+ parseFloat(node.value[0]),
353
+ parseFloat(node.value[1]),
354
+ parseFloat(node.value[2]),
355
+ parseFloat(node.value[3])
356
+ );
357
+ } else {
358
+ return node.value;
359
+ }
360
+ };
361
+
362
+ export const toonNode = (
363
+ id: string,
364
+ name: string,
365
+ groupId: string | null | undefined,
366
+ position: NodePosition,
367
+ uniforms: UniformDataType[],
368
+ stage: ShaderStage | undefined,
369
+ nextStageNodeId?: string
370
+ ): CodeNode =>
371
+ prepopulatePropertyInputs({
372
+ id,
373
+ name,
374
+ groupId,
375
+ position,
376
+ type: EngineNodeType.toon,
377
+ config: {
378
+ uniforms,
379
+ version: 3,
380
+ preprocess: true,
381
+ mangle: false,
382
+ properties: [
383
+ property('Color', 'color', 'rgb', 'uniform_diffuse'),
384
+ property('Texture', 'map', 'texture', 'filler_map'),
385
+ property(
386
+ 'Gradient Map',
387
+ 'gradientMap',
388
+ 'texture',
389
+ 'filler_gradientMap'
390
+ ),
391
+ property('Normal Map', 'normalMap', 'texture', 'filler_normalMap'),
392
+ property('Normal Scale', 'normalScale', 'vector2'),
393
+ property('Displacement Map', 'displacementMap', 'texture'),
394
+ property('Env Map', 'envMap', 'samplerCube'),
395
+ ],
396
+ strategies: [
397
+ uniformStrategy(),
398
+ stage === 'fragment'
399
+ ? texture2DStrategy()
400
+ : namedAttributeStrategy('position'),
401
+ ],
402
+ },
403
+ inputs: [],
404
+ outputs: [
405
+ {
406
+ name: 'vector4',
407
+ category: 'data',
408
+ id: '1',
409
+ },
410
+ ],
411
+ source: '',
412
+ stage,
413
+ nextStageNodeId,
414
+ });
415
+
416
+ export const threngine: Engine = {
417
+ name: 'three',
418
+ importers,
419
+ mergeOptions: {
420
+ includePrecisions: true,
421
+ includeVersion: true,
422
+ },
423
+ evaluateNode,
424
+ constructors: {
425
+ [EngineNodeType.physical]: physicalNode,
426
+ [EngineNodeType.toon]: toonNode,
427
+ },
428
+ // TODO: Get from uniform lib?
429
+ preserve: new Set<string>([
430
+ 'viewMatrix',
431
+ 'modelMatrix',
432
+ 'modelViewMatrix',
433
+ 'projectionMatrix',
434
+ 'normalMatrix',
435
+ 'uvTransform',
436
+ // Attributes
437
+ 'position',
438
+ 'normal',
439
+ 'uv',
440
+ 'uv2',
441
+ // Varyings
442
+ 'vUv',
443
+ 'vUv2',
444
+ 'vViewPosition',
445
+ 'vNormal',
446
+ 'vPosition',
447
+ // Uniforms
448
+ 'cameraPosition',
449
+ 'isOrthographic',
450
+ 'diffuse',
451
+ 'emissive',
452
+ 'specular',
453
+ 'shininess',
454
+ 'opacity',
455
+ 'map',
456
+ 'specularTint',
457
+ 'time',
458
+ 'normalScale',
459
+ 'normalMap',
460
+ 'envMap',
461
+ 'envMapIntensity',
462
+ 'flipEnvMap',
463
+ 'maxMipLevel',
464
+ 'roughnessMap',
465
+ // Uniforms for lighting
466
+ 'receiveShadow',
467
+ 'ambientLightColor',
468
+ 'lightProbe',
469
+ // Light uniform arrays
470
+ 'spotLights',
471
+ 'pointLights',
472
+ // This isn't three wtf
473
+ 'resolution',
474
+ 'color',
475
+ 'image',
476
+ 'gradientMap',
477
+ // TODO: This isn't specific to threejs as an engine, it's specific to the
478
+ // phong shader. If a *shader* node has brightness, it should be unique, not
479
+ // use the threejs one!
480
+ 'brightness',
481
+ // TODO: These depend on the shaderlib, this might need to be a runtime
482
+ // concern
483
+ // Metalness
484
+ 'roughness',
485
+ 'metalness',
486
+ 'ior',
487
+ 'specularIntensity',
488
+ 'clearcoat',
489
+ 'clearcoatRoughness',
490
+ 'transmission',
491
+ 'thickness',
492
+ 'attenuationDistance',
493
+ 'attenuationTint',
494
+ 'transmissionSamplerMap',
495
+ 'transmissionSamplerSize',
496
+ 'displacementMap',
497
+ 'displacementScale',
498
+ 'displacementBias',
499
+ ]),
500
+ parsers: {
501
+ [NodeType.SOURCE]: {
502
+ manipulateAst: (engineContext, engine, graph, node, ast, inputEdges) => {
503
+ const programAst = ast as Program;
504
+ const mainName = 'main' || nodeName(node);
505
+
506
+ // This hinges on the vertex shader calling vec3(p)
507
+ if (node.stage === 'vertex') {
508
+ if (doesLinkThruShader(graph, node)) {
509
+ returnGlPositionVec3Right(mainName, programAst);
510
+ } else {
511
+ returnGlPosition(mainName, programAst);
512
+ }
513
+ }
514
+ return ast;
515
+ },
516
+ },
517
+ [EngineNodeType.phong]: {
518
+ onBeforeCompile: async (graph, engineContext, node, sibling) => {
519
+ const { three } = engineContext.runtime;
520
+ cacher(engineContext, graph, node, sibling as SourceNode, () =>
521
+ onBeforeCompileMegaShader(
522
+ engineContext,
523
+ new three.MeshPhongMaterial({
524
+ isMeshPhongMaterial: true,
525
+ ...threeMaterialProperties(three, graph, node, sibling),
526
+ })
527
+ )
528
+ );
529
+ },
530
+ manipulateAst: megaShaderMainpulateAst,
531
+ },
532
+ [EngineNodeType.physical]: {
533
+ onBeforeCompile: async (graph, engineContext, node, sibling) => {
534
+ const { three } = engineContext.runtime;
535
+
536
+ cacher(engineContext, graph, node, sibling as SourceNode, () =>
537
+ onBeforeCompileMegaShader(
538
+ engineContext,
539
+ new three.MeshPhysicalMaterial({
540
+ // These properties are copied onto the runtime RawShaderMaterial.
541
+ // These exist on the MeshPhysicalMaterial but only in the
542
+ // prototype. We have to hard code them for Object.keys() to work
543
+ ...node.config.hardCodedProperties,
544
+ ...threeMaterialProperties(three, graph, node, sibling),
545
+ iridescence: 1.0,
546
+ iridescenceIOR: 2.0,
547
+ })
548
+ )
549
+ );
550
+ },
551
+ manipulateAst: megaShaderMainpulateAst,
552
+ },
553
+ [EngineNodeType.toon]: {
554
+ onBeforeCompile: async (graph, engineContext, node, sibling) => {
555
+ const { three } = engineContext.runtime;
556
+
557
+ cacher(engineContext, graph, node, sibling as SourceNode, () =>
558
+ onBeforeCompileMegaShader(
559
+ engineContext,
560
+ new three.MeshToonMaterial({
561
+ gradientMap: new three.Texture(),
562
+ isMeshToonMaterial: true,
563
+ ...threeMaterialProperties(three, graph, node, sibling),
564
+ })
565
+ )
566
+ );
567
+ },
568
+ manipulateAst: megaShaderMainpulateAst,
569
+ },
570
+ },
571
+ };
@@ -0,0 +1,10 @@
1
+ export const ensure = <T>(
2
+ argument: T | undefined | null,
3
+ message: string = 'This value was promised to be there.'
4
+ ): T => {
5
+ if (argument === undefined || argument === null) {
6
+ throw new TypeError(message);
7
+ }
8
+
9
+ return argument;
10
+ };
package/src/util/id.ts ADDED
@@ -0,0 +1,2 @@
1
+ let counter = 0;
2
+ export const makeId = () => '' + counter++;