@holoscript/engine 6.0.3 → 6.0.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/dist/AutoMesher-CK47F6AV.js +17 -0
- package/dist/GPUBuffers-2LHBCD7X.js +9 -0
- package/dist/WebGPUContext-TNEUYU2Y.js +11 -0
- package/dist/animation/index.cjs +38 -38
- package/dist/animation/index.d.cts +1 -1
- package/dist/animation/index.d.ts +1 -1
- package/dist/animation/index.js +1 -1
- package/dist/audio/index.cjs +16 -6
- package/dist/audio/index.d.cts +1 -1
- package/dist/audio/index.d.ts +1 -1
- package/dist/audio/index.js +1 -1
- package/dist/camera/index.cjs +23 -23
- package/dist/camera/index.d.cts +1 -1
- package/dist/camera/index.d.ts +1 -1
- package/dist/camera/index.js +1 -1
- package/dist/character/index.cjs +6 -4
- package/dist/character/index.js +1 -1
- package/dist/choreography/index.cjs +1194 -0
- package/dist/choreography/index.d.cts +687 -0
- package/dist/choreography/index.d.ts +687 -0
- package/dist/choreography/index.js +1156 -0
- package/dist/chunk-2CSNRI2N.js +217 -0
- package/dist/chunk-33T2WINR.js +266 -0
- package/dist/chunk-35R73OFM.js +1257 -0
- package/dist/chunk-4MMDSUNP.js +1256 -0
- package/dist/chunk-5V6HOU72.js +319 -0
- package/dist/chunk-6QOP6PYF.js +1038 -0
- package/dist/chunk-7KMJVHIL.js +8944 -0
- package/dist/chunk-7VPUC62U.js +1106 -0
- package/dist/chunk-A2Y6RCAT.js +1878 -0
- package/dist/chunk-AHM42MK6.js +8944 -0
- package/dist/chunk-BL7IDTHE.js +218 -0
- package/dist/chunk-CITOMSWL.js +10462 -0
- package/dist/chunk-CXDPKW2K.js +8944 -0
- package/dist/chunk-CXZPLD4S.js +223 -0
- package/dist/chunk-CZYJE7IH.js +5169 -0
- package/dist/chunk-D2OP7YC7.js +6325 -0
- package/dist/chunk-EDRVQHUU.js +1544 -0
- package/dist/chunk-EJSLOOW2.js +3589 -0
- package/dist/chunk-F53SFGW5.js +1878 -0
- package/dist/chunk-HCFPELPY.js +919 -0
- package/dist/chunk-HNEE36PY.js +93 -0
- package/dist/chunk-HYXNV36F.js +1256 -0
- package/dist/chunk-IB7KHVFY.js +821 -0
- package/dist/chunk-IBBO7YYG.js +690 -0
- package/dist/chunk-ILIBGINU.js +5470 -0
- package/dist/chunk-IS4MHLKN.js +5479 -0
- package/dist/chunk-JT2PFKWD.js +5479 -0
- package/dist/chunk-K4CUB4NY.js +1038 -0
- package/dist/chunk-KATDQXRJ.js +10462 -0
- package/dist/chunk-KBQE6ZFJ.js +8944 -0
- package/dist/chunk-KBVD5K7E.js +560 -0
- package/dist/chunk-KCDPVQRY.js +4088 -0
- package/dist/chunk-KN4QJPKN.js +8944 -0
- package/dist/chunk-KWJ3ROSI.js +8944 -0
- package/dist/chunk-L45VF6DD.js +919 -0
- package/dist/chunk-LY4T37YK.js +307 -0
- package/dist/chunk-MDN5WZXA.js +1544 -0
- package/dist/chunk-MGCDP6VU.js +928 -0
- package/dist/chunk-NCX7X6G2.js +8681 -0
- package/dist/chunk-OF54BPVD.js +913 -0
- package/dist/chunk-OWSN2Q3Q.js +690 -0
- package/dist/chunk-PRRB5TTA.js +406 -0
- package/dist/chunk-PXWVQF76.js +4086 -0
- package/dist/chunk-PYCOIDT2.js +812 -0
- package/dist/chunk-PZCSADOV.js +928 -0
- package/dist/chunk-Q2XBVS2K.js +1038 -0
- package/dist/chunk-QDZRXWN5.js +1776 -0
- package/dist/chunk-RNWOZ6WQ.js +913 -0
- package/dist/chunk-ROLFT4CJ.js +1693 -0
- package/dist/chunk-SLTJRZ2N.js +266 -0
- package/dist/chunk-SRUS5XSU.js +4088 -0
- package/dist/chunk-TKCA3WZ5.js +5409 -0
- package/dist/chunk-TNRMXYI2.js +1650 -0
- package/dist/chunk-TQB3GJGM.js +9763 -0
- package/dist/chunk-TUFGXG6K.js +510 -0
- package/dist/chunk-U6KMTGQJ.js +632 -0
- package/dist/chunk-VMGJQST6.js +8681 -0
- package/dist/chunk-X4F4TCG4.js +5470 -0
- package/dist/chunk-ZIFROE75.js +1544 -0
- package/dist/chunk-ZIJQYHSQ.js +1204 -0
- package/dist/combat/index.cjs +4 -4
- package/dist/combat/index.d.cts +1 -1
- package/dist/combat/index.d.ts +1 -1
- package/dist/combat/index.js +1 -1
- package/dist/ecs/index.cjs +1 -1
- package/dist/ecs/index.js +1 -1
- package/dist/environment/index.cjs +14 -14
- package/dist/environment/index.d.cts +1 -1
- package/dist/environment/index.d.ts +1 -1
- package/dist/environment/index.js +1 -1
- package/dist/gpu/index.cjs +4810 -0
- package/dist/gpu/index.js +3714 -0
- package/dist/hologram/index.cjs +27 -1
- package/dist/hologram/index.js +1 -1
- package/dist/index-B2PIsAmR.d.cts +2180 -0
- package/dist/index-B2PIsAmR.d.ts +2180 -0
- package/dist/index-BHySEPX7.d.cts +2921 -0
- package/dist/index-BJV21zuy.d.cts +341 -0
- package/dist/index-BJV21zuy.d.ts +341 -0
- package/dist/index-BQutTphC.d.cts +790 -0
- package/dist/index-ByIq2XrS.d.cts +3910 -0
- package/dist/index-BysHjDSO.d.cts +224 -0
- package/dist/index-BysHjDSO.d.ts +224 -0
- package/dist/index-CKwAJGck.d.ts +455 -0
- package/dist/index-CUl3QstQ.d.cts +3006 -0
- package/dist/index-CUl3QstQ.d.ts +3006 -0
- package/dist/index-CmYtNiI-.d.cts +953 -0
- package/dist/index-CmYtNiI-.d.ts +953 -0
- package/dist/index-CnRzWxi_.d.cts +522 -0
- package/dist/index-CnRzWxi_.d.ts +522 -0
- package/dist/index-CwRWbSC7.d.ts +2921 -0
- package/dist/index-CxKIBstO.d.ts +790 -0
- package/dist/index-DJ6-R8vh.d.cts +455 -0
- package/dist/index-DQKisbcI.d.cts +4968 -0
- package/dist/index-DQKisbcI.d.ts +4968 -0
- package/dist/index-DRT2zJez.d.ts +3910 -0
- package/dist/index-DfNLiAka.d.cts +192 -0
- package/dist/index-DfNLiAka.d.ts +192 -0
- package/dist/index-nMvkoRm8.d.cts +405 -0
- package/dist/index-nMvkoRm8.d.ts +405 -0
- package/dist/index-s9yOFU37.d.cts +604 -0
- package/dist/index-s9yOFU37.d.ts +604 -0
- package/dist/index.cjs +22966 -6960
- package/dist/index.d.cts +864 -20
- package/dist/index.d.ts +864 -20
- package/dist/index.js +3062 -48
- package/dist/input/index.cjs +1 -1
- package/dist/input/index.js +1 -1
- package/dist/orbital/index.cjs +3 -3
- package/dist/orbital/index.d.cts +1 -1
- package/dist/orbital/index.d.ts +1 -1
- package/dist/orbital/index.js +1 -1
- package/dist/particles/index.cjs +16 -16
- package/dist/particles/index.d.cts +1 -1
- package/dist/particles/index.d.ts +1 -1
- package/dist/particles/index.js +1 -1
- package/dist/physics/index.cjs +2377 -21
- package/dist/physics/index.d.cts +1 -1
- package/dist/physics/index.d.ts +1 -1
- package/dist/physics/index.js +35 -1
- package/dist/postfx/index.cjs +3491 -0
- package/dist/postfx/index.js +93 -0
- package/dist/procedural/index.cjs +1 -1
- package/dist/procedural/index.js +1 -1
- package/dist/puppeteer-5VF6KDVO.js +52197 -0
- package/dist/puppeteer-IZVZ3SG4.js +52197 -0
- package/dist/rendering/index.cjs +33 -32
- package/dist/rendering/index.d.cts +1 -1
- package/dist/rendering/index.d.ts +1 -1
- package/dist/rendering/index.js +8 -6
- package/dist/runtime/index.cjs +23 -13
- package/dist/runtime/index.d.cts +1 -1
- package/dist/runtime/index.d.ts +1 -1
- package/dist/runtime/index.js +8 -6
- package/dist/runtime/protocols/index.cjs +349 -0
- package/dist/runtime/protocols/index.js +15 -0
- package/dist/scene/index.cjs +8 -8
- package/dist/scene/index.d.cts +1 -1
- package/dist/scene/index.d.ts +1 -1
- package/dist/scene/index.js +1 -1
- package/dist/shader/index.cjs +3087 -0
- package/dist/shader/index.js +3044 -0
- package/dist/simulation/index.cjs +10680 -0
- package/dist/simulation/index.d.cts +3 -0
- package/dist/simulation/index.d.ts +3 -0
- package/dist/simulation/index.js +307 -0
- package/dist/spatial/index.cjs +2443 -0
- package/dist/spatial/index.d.cts +1545 -0
- package/dist/spatial/index.d.ts +1545 -0
- package/dist/spatial/index.js +2400 -0
- package/dist/terrain/index.cjs +1 -1
- package/dist/terrain/index.d.cts +1 -1
- package/dist/terrain/index.d.ts +1 -1
- package/dist/terrain/index.js +1 -1
- package/dist/transformers.node-4NKAPD5U.js +45620 -0
- package/dist/vm/index.cjs +7 -8
- package/dist/vm/index.d.cts +1 -1
- package/dist/vm/index.d.ts +1 -1
- package/dist/vm/index.js +1 -1
- package/dist/vm-bridge/index.cjs +2 -2
- package/dist/vm-bridge/index.d.cts +2 -2
- package/dist/vm-bridge/index.d.ts +2 -2
- package/dist/vm-bridge/index.js +1 -1
- package/dist/vr/index.cjs +6 -6
- package/dist/vr/index.js +1 -1
- package/dist/world/index.cjs +3 -3
- package/dist/world/index.d.cts +1 -1
- package/dist/world/index.d.ts +1 -1
- package/dist/world/index.js +1 -1
- package/package.json +53 -21
- package/LICENSE +0 -21
|
@@ -0,0 +1,3044 @@
|
|
|
1
|
+
import "../chunk-AKLW2MUS.js";
|
|
2
|
+
|
|
3
|
+
// src/shader/graph/ShaderGraphTypes.ts
|
|
4
|
+
var TYPE_SIZES = {
|
|
5
|
+
float: 1,
|
|
6
|
+
vec2: 2,
|
|
7
|
+
vec3: 3,
|
|
8
|
+
vec4: 4,
|
|
9
|
+
mat2: 4,
|
|
10
|
+
mat3: 9,
|
|
11
|
+
mat4: 16,
|
|
12
|
+
int: 1,
|
|
13
|
+
ivec2: 2,
|
|
14
|
+
ivec3: 3,
|
|
15
|
+
ivec4: 4,
|
|
16
|
+
bool: 1,
|
|
17
|
+
sampler2D: 0,
|
|
18
|
+
samplerCube: 0
|
|
19
|
+
};
|
|
20
|
+
function areTypesCompatible(from, to) {
|
|
21
|
+
if (from === to) return true;
|
|
22
|
+
if (from === "sampler2D" || from === "samplerCube") return false;
|
|
23
|
+
if (to === "sampler2D" || to === "samplerCube") return false;
|
|
24
|
+
if (from === "float" && (to === "vec2" || to === "vec3" || to === "vec4")) return true;
|
|
25
|
+
if (from === "int" && to === "float") return true;
|
|
26
|
+
if (from === "vec3" && to === "vec4") return true;
|
|
27
|
+
if (from === "vec4" && to === "vec3") return true;
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
function getTypeConversion(from, to, expr) {
|
|
31
|
+
if (from === to) return expr;
|
|
32
|
+
if (from === "float") {
|
|
33
|
+
if (to === "vec2") return `vec2<f32>(${expr})`;
|
|
34
|
+
if (to === "vec3") return `vec3<f32>(${expr})`;
|
|
35
|
+
if (to === "vec4") return `vec4<f32>(${expr})`;
|
|
36
|
+
}
|
|
37
|
+
if (from === "int" && to === "float") return `f32(${expr})`;
|
|
38
|
+
if (from === "vec3" && to === "vec4") return `vec4<f32>(${expr}, 1.0)`;
|
|
39
|
+
if (from === "vec4" && to === "vec3") return `(${expr}).xyz`;
|
|
40
|
+
return expr;
|
|
41
|
+
}
|
|
42
|
+
var INPUT_NODES = [
|
|
43
|
+
{
|
|
44
|
+
type: "input_position",
|
|
45
|
+
name: "World Position",
|
|
46
|
+
category: "input",
|
|
47
|
+
description: "World space position",
|
|
48
|
+
inputs: [],
|
|
49
|
+
outputs: [{ id: "position", name: "Position", type: "vec3" }],
|
|
50
|
+
generateCode: () => "in.worldPosition"
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
type: "input_normal",
|
|
54
|
+
name: "World Normal",
|
|
55
|
+
category: "input",
|
|
56
|
+
description: "World space normal",
|
|
57
|
+
inputs: [],
|
|
58
|
+
outputs: [{ id: "normal", name: "Normal", type: "vec3" }],
|
|
59
|
+
generateCode: () => "in.worldNormal"
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
type: "input_uv",
|
|
63
|
+
name: "UV Coordinates",
|
|
64
|
+
category: "input",
|
|
65
|
+
description: "Texture coordinates",
|
|
66
|
+
inputs: [],
|
|
67
|
+
outputs: [{ id: "uv", name: "UV", type: "vec2" }],
|
|
68
|
+
generateCode: () => "in.uv"
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
type: "input_time",
|
|
72
|
+
name: "Time",
|
|
73
|
+
category: "input",
|
|
74
|
+
description: "Scene time in seconds",
|
|
75
|
+
inputs: [],
|
|
76
|
+
outputs: [{ id: "time", name: "Time", type: "float" }],
|
|
77
|
+
generateCode: () => "scene.time"
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
type: "input_camera_position",
|
|
81
|
+
name: "Camera Position",
|
|
82
|
+
category: "input",
|
|
83
|
+
description: "Camera world position",
|
|
84
|
+
inputs: [],
|
|
85
|
+
outputs: [{ id: "position", name: "Position", type: "vec3" }],
|
|
86
|
+
generateCode: () => "camera.position"
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
type: "input_view_direction",
|
|
90
|
+
name: "View Direction",
|
|
91
|
+
category: "input",
|
|
92
|
+
description: "Direction from surface to camera",
|
|
93
|
+
inputs: [],
|
|
94
|
+
outputs: [{ id: "direction", name: "Direction", type: "vec3" }],
|
|
95
|
+
generateCode: () => "normalize(camera.position - in.worldPosition)"
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
type: "constant_float",
|
|
99
|
+
name: "Float",
|
|
100
|
+
category: "input",
|
|
101
|
+
description: "Constant float value",
|
|
102
|
+
inputs: [],
|
|
103
|
+
outputs: [{ id: "value", name: "Value", type: "float" }],
|
|
104
|
+
defaultProperties: { value: 0 },
|
|
105
|
+
generateCode: (node) => `${node.properties?.value ?? 0}`
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
type: "constant_vec2",
|
|
109
|
+
name: "Vector2",
|
|
110
|
+
category: "input",
|
|
111
|
+
description: "Constant vec2 value",
|
|
112
|
+
inputs: [],
|
|
113
|
+
outputs: [{ id: "value", name: "Value", type: "vec2" }],
|
|
114
|
+
defaultProperties: { x: 0, y: 0 },
|
|
115
|
+
generateCode: (node) => {
|
|
116
|
+
const x = node.properties?.x ?? 0;
|
|
117
|
+
const y = node.properties?.y ?? 0;
|
|
118
|
+
return `vec2<f32>(${x}, ${y})`;
|
|
119
|
+
}
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
type: "constant_vec3",
|
|
123
|
+
name: "Vector3",
|
|
124
|
+
category: "input",
|
|
125
|
+
description: "Constant vec3 value",
|
|
126
|
+
inputs: [],
|
|
127
|
+
outputs: [{ id: "value", name: "Value", type: "vec3" }],
|
|
128
|
+
defaultProperties: { x: 0, y: 0, z: 0 },
|
|
129
|
+
generateCode: (node) => {
|
|
130
|
+
const x = node.properties?.x ?? 0;
|
|
131
|
+
const y = node.properties?.y ?? 0;
|
|
132
|
+
const z = node.properties?.z ?? 0;
|
|
133
|
+
return `vec3<f32>(${x}, ${y}, ${z})`;
|
|
134
|
+
}
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
type: "constant_color",
|
|
138
|
+
name: "Color",
|
|
139
|
+
category: "input",
|
|
140
|
+
description: "Constant color value",
|
|
141
|
+
inputs: [],
|
|
142
|
+
outputs: [{ id: "color", name: "Color", type: "vec4" }],
|
|
143
|
+
defaultProperties: { r: 1, g: 1, b: 1, a: 1 },
|
|
144
|
+
generateCode: (node) => {
|
|
145
|
+
const r = node.properties?.r ?? 1;
|
|
146
|
+
const g = node.properties?.g ?? 1;
|
|
147
|
+
const b = node.properties?.b ?? 1;
|
|
148
|
+
const a = node.properties?.a ?? 1;
|
|
149
|
+
return `vec4<f32>(${r}, ${g}, ${b}, ${a})`;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
];
|
|
153
|
+
var MATH_NODES = [
|
|
154
|
+
{
|
|
155
|
+
type: "math_add",
|
|
156
|
+
name: "Add",
|
|
157
|
+
category: "math",
|
|
158
|
+
description: "Add two values",
|
|
159
|
+
inputs: [
|
|
160
|
+
{ id: "a", name: "A", type: "float", defaultValue: 0 },
|
|
161
|
+
{ id: "b", name: "B", type: "float", defaultValue: 0 }
|
|
162
|
+
],
|
|
163
|
+
outputs: [{ id: "result", name: "Result", type: "float" }],
|
|
164
|
+
generateCode: (_, inputs) => `(${inputs.a} + ${inputs.b})`
|
|
165
|
+
},
|
|
166
|
+
{
|
|
167
|
+
type: "math_subtract",
|
|
168
|
+
name: "Subtract",
|
|
169
|
+
category: "math",
|
|
170
|
+
description: "Subtract two values",
|
|
171
|
+
inputs: [
|
|
172
|
+
{ id: "a", name: "A", type: "float", defaultValue: 0 },
|
|
173
|
+
{ id: "b", name: "B", type: "float", defaultValue: 0 }
|
|
174
|
+
],
|
|
175
|
+
outputs: [{ id: "result", name: "Result", type: "float" }],
|
|
176
|
+
generateCode: (_, inputs) => `(${inputs.a} - ${inputs.b})`
|
|
177
|
+
},
|
|
178
|
+
{
|
|
179
|
+
type: "math_multiply",
|
|
180
|
+
name: "Multiply",
|
|
181
|
+
category: "math",
|
|
182
|
+
description: "Multiply two values",
|
|
183
|
+
inputs: [
|
|
184
|
+
{ id: "a", name: "A", type: "float", defaultValue: 1 },
|
|
185
|
+
{ id: "b", name: "B", type: "float", defaultValue: 1 }
|
|
186
|
+
],
|
|
187
|
+
outputs: [{ id: "result", name: "Result", type: "float" }],
|
|
188
|
+
generateCode: (_, inputs) => `(${inputs.a} * ${inputs.b})`
|
|
189
|
+
},
|
|
190
|
+
{
|
|
191
|
+
type: "math_divide",
|
|
192
|
+
name: "Divide",
|
|
193
|
+
category: "math",
|
|
194
|
+
description: "Divide two values",
|
|
195
|
+
inputs: [
|
|
196
|
+
{ id: "a", name: "A", type: "float", defaultValue: 1 },
|
|
197
|
+
{ id: "b", name: "B", type: "float", defaultValue: 1 }
|
|
198
|
+
],
|
|
199
|
+
outputs: [{ id: "result", name: "Result", type: "float" }],
|
|
200
|
+
generateCode: (_, inputs) => `(${inputs.a} / max(${inputs.b}, 0.0001))`
|
|
201
|
+
},
|
|
202
|
+
{
|
|
203
|
+
type: "math_power",
|
|
204
|
+
name: "Power",
|
|
205
|
+
category: "math",
|
|
206
|
+
description: "Raise to power",
|
|
207
|
+
inputs: [
|
|
208
|
+
{ id: "base", name: "Base", type: "float", defaultValue: 2 },
|
|
209
|
+
{ id: "exp", name: "Exponent", type: "float", defaultValue: 2 }
|
|
210
|
+
],
|
|
211
|
+
outputs: [{ id: "result", name: "Result", type: "float" }],
|
|
212
|
+
generateCode: (_, inputs) => `pow(${inputs.base}, ${inputs.exp})`
|
|
213
|
+
},
|
|
214
|
+
{
|
|
215
|
+
type: "math_sqrt",
|
|
216
|
+
name: "Square Root",
|
|
217
|
+
category: "math",
|
|
218
|
+
description: "Square root",
|
|
219
|
+
inputs: [{ id: "value", name: "Value", type: "float", defaultValue: 1 }],
|
|
220
|
+
outputs: [{ id: "result", name: "Result", type: "float" }],
|
|
221
|
+
generateCode: (_, inputs) => `sqrt(max(${inputs.value}, 0.0))`
|
|
222
|
+
},
|
|
223
|
+
{
|
|
224
|
+
type: "math_abs",
|
|
225
|
+
name: "Absolute",
|
|
226
|
+
category: "math",
|
|
227
|
+
description: "Absolute value",
|
|
228
|
+
inputs: [{ id: "value", name: "Value", type: "float", defaultValue: 0 }],
|
|
229
|
+
outputs: [{ id: "result", name: "Result", type: "float" }],
|
|
230
|
+
generateCode: (_, inputs) => `abs(${inputs.value})`
|
|
231
|
+
},
|
|
232
|
+
{
|
|
233
|
+
type: "math_negate",
|
|
234
|
+
name: "Negate",
|
|
235
|
+
category: "math",
|
|
236
|
+
description: "Negate value",
|
|
237
|
+
inputs: [{ id: "value", name: "Value", type: "float", defaultValue: 0 }],
|
|
238
|
+
outputs: [{ id: "result", name: "Result", type: "float" }],
|
|
239
|
+
generateCode: (_, inputs) => `(-${inputs.value})`
|
|
240
|
+
},
|
|
241
|
+
{
|
|
242
|
+
type: "math_min",
|
|
243
|
+
name: "Minimum",
|
|
244
|
+
category: "math",
|
|
245
|
+
description: "Minimum of two values",
|
|
246
|
+
inputs: [
|
|
247
|
+
{ id: "a", name: "A", type: "float", defaultValue: 0 },
|
|
248
|
+
{ id: "b", name: "B", type: "float", defaultValue: 0 }
|
|
249
|
+
],
|
|
250
|
+
outputs: [{ id: "result", name: "Result", type: "float" }],
|
|
251
|
+
generateCode: (_, inputs) => `min(${inputs.a}, ${inputs.b})`
|
|
252
|
+
},
|
|
253
|
+
{
|
|
254
|
+
type: "math_max",
|
|
255
|
+
name: "Maximum",
|
|
256
|
+
category: "math",
|
|
257
|
+
description: "Maximum of two values",
|
|
258
|
+
inputs: [
|
|
259
|
+
{ id: "a", name: "A", type: "float", defaultValue: 0 },
|
|
260
|
+
{ id: "b", name: "B", type: "float", defaultValue: 0 }
|
|
261
|
+
],
|
|
262
|
+
outputs: [{ id: "result", name: "Result", type: "float" }],
|
|
263
|
+
generateCode: (_, inputs) => `max(${inputs.a}, ${inputs.b})`
|
|
264
|
+
},
|
|
265
|
+
{
|
|
266
|
+
type: "math_clamp",
|
|
267
|
+
name: "Clamp",
|
|
268
|
+
category: "math",
|
|
269
|
+
description: "Clamp value between min and max",
|
|
270
|
+
inputs: [
|
|
271
|
+
{ id: "value", name: "Value", type: "float", defaultValue: 0 },
|
|
272
|
+
{ id: "min", name: "Min", type: "float", defaultValue: 0 },
|
|
273
|
+
{ id: "max", name: "Max", type: "float", defaultValue: 1 }
|
|
274
|
+
],
|
|
275
|
+
outputs: [{ id: "result", name: "Result", type: "float" }],
|
|
276
|
+
generateCode: (_, inputs) => `clamp(${inputs.value}, ${inputs.min}, ${inputs.max})`
|
|
277
|
+
},
|
|
278
|
+
{
|
|
279
|
+
type: "math_saturate",
|
|
280
|
+
name: "Saturate",
|
|
281
|
+
category: "math",
|
|
282
|
+
description: "Clamp between 0 and 1",
|
|
283
|
+
inputs: [{ id: "value", name: "Value", type: "float", defaultValue: 0 }],
|
|
284
|
+
outputs: [{ id: "result", name: "Result", type: "float" }],
|
|
285
|
+
generateCode: (_, inputs) => `saturate(${inputs.value})`
|
|
286
|
+
},
|
|
287
|
+
{
|
|
288
|
+
type: "math_lerp",
|
|
289
|
+
name: "Lerp",
|
|
290
|
+
category: "math",
|
|
291
|
+
description: "Linear interpolation",
|
|
292
|
+
inputs: [
|
|
293
|
+
{ id: "a", name: "A", type: "float", defaultValue: 0 },
|
|
294
|
+
{ id: "b", name: "B", type: "float", defaultValue: 1 },
|
|
295
|
+
{ id: "t", name: "T", type: "float", defaultValue: 0.5 }
|
|
296
|
+
],
|
|
297
|
+
outputs: [{ id: "result", name: "Result", type: "float" }],
|
|
298
|
+
generateCode: (_, inputs) => `mix(${inputs.a}, ${inputs.b}, ${inputs.t})`
|
|
299
|
+
},
|
|
300
|
+
{
|
|
301
|
+
type: "math_smoothstep",
|
|
302
|
+
name: "Smoothstep",
|
|
303
|
+
category: "math",
|
|
304
|
+
description: "Smooth Hermite interpolation",
|
|
305
|
+
inputs: [
|
|
306
|
+
{ id: "edge0", name: "Edge0", type: "float", defaultValue: 0 },
|
|
307
|
+
{ id: "edge1", name: "Edge1", type: "float", defaultValue: 1 },
|
|
308
|
+
{ id: "x", name: "X", type: "float", defaultValue: 0.5 }
|
|
309
|
+
],
|
|
310
|
+
outputs: [{ id: "result", name: "Result", type: "float" }],
|
|
311
|
+
generateCode: (_, inputs) => `smoothstep(${inputs.edge0}, ${inputs.edge1}, ${inputs.x})`
|
|
312
|
+
},
|
|
313
|
+
{
|
|
314
|
+
type: "math_step",
|
|
315
|
+
name: "Step",
|
|
316
|
+
category: "math",
|
|
317
|
+
description: "Step function",
|
|
318
|
+
inputs: [
|
|
319
|
+
{ id: "edge", name: "Edge", type: "float", defaultValue: 0.5 },
|
|
320
|
+
{ id: "x", name: "X", type: "float", defaultValue: 0 }
|
|
321
|
+
],
|
|
322
|
+
outputs: [{ id: "result", name: "Result", type: "float" }],
|
|
323
|
+
generateCode: (_, inputs) => `step(${inputs.edge}, ${inputs.x})`
|
|
324
|
+
},
|
|
325
|
+
{
|
|
326
|
+
type: "math_fract",
|
|
327
|
+
name: "Fraction",
|
|
328
|
+
category: "math",
|
|
329
|
+
description: "Fractional part",
|
|
330
|
+
inputs: [{ id: "value", name: "Value", type: "float", defaultValue: 0 }],
|
|
331
|
+
outputs: [{ id: "result", name: "Result", type: "float" }],
|
|
332
|
+
generateCode: (_, inputs) => `fract(${inputs.value})`
|
|
333
|
+
},
|
|
334
|
+
{
|
|
335
|
+
type: "math_floor",
|
|
336
|
+
name: "Floor",
|
|
337
|
+
category: "math",
|
|
338
|
+
description: "Floor value",
|
|
339
|
+
inputs: [{ id: "value", name: "Value", type: "float", defaultValue: 0 }],
|
|
340
|
+
outputs: [{ id: "result", name: "Result", type: "float" }],
|
|
341
|
+
generateCode: (_, inputs) => `floor(${inputs.value})`
|
|
342
|
+
},
|
|
343
|
+
{
|
|
344
|
+
type: "math_ceil",
|
|
345
|
+
name: "Ceiling",
|
|
346
|
+
category: "math",
|
|
347
|
+
description: "Ceiling value",
|
|
348
|
+
inputs: [{ id: "value", name: "Value", type: "float", defaultValue: 0 }],
|
|
349
|
+
outputs: [{ id: "result", name: "Result", type: "float" }],
|
|
350
|
+
generateCode: (_, inputs) => `ceil(${inputs.value})`
|
|
351
|
+
},
|
|
352
|
+
{
|
|
353
|
+
type: "math_round",
|
|
354
|
+
name: "Round",
|
|
355
|
+
category: "math",
|
|
356
|
+
description: "Round value",
|
|
357
|
+
inputs: [{ id: "value", name: "Value", type: "float", defaultValue: 0 }],
|
|
358
|
+
outputs: [{ id: "result", name: "Result", type: "float" }],
|
|
359
|
+
generateCode: (_, inputs) => `round(${inputs.value})`
|
|
360
|
+
},
|
|
361
|
+
{
|
|
362
|
+
type: "math_mod",
|
|
363
|
+
name: "Modulo",
|
|
364
|
+
category: "math",
|
|
365
|
+
description: "Modulo operation",
|
|
366
|
+
inputs: [
|
|
367
|
+
{ id: "a", name: "A", type: "float", defaultValue: 0 },
|
|
368
|
+
{ id: "b", name: "B", type: "float", defaultValue: 1 }
|
|
369
|
+
],
|
|
370
|
+
outputs: [{ id: "result", name: "Result", type: "float" }],
|
|
371
|
+
generateCode: (_, inputs) => `(${inputs.a} % ${inputs.b})`
|
|
372
|
+
}
|
|
373
|
+
];
|
|
374
|
+
var TRIG_NODES = [
|
|
375
|
+
{
|
|
376
|
+
type: "trig_sin",
|
|
377
|
+
name: "Sine",
|
|
378
|
+
category: "math",
|
|
379
|
+
description: "Sine function",
|
|
380
|
+
inputs: [{ id: "angle", name: "Angle", type: "float", defaultValue: 0 }],
|
|
381
|
+
outputs: [{ id: "result", name: "Result", type: "float" }],
|
|
382
|
+
generateCode: (_, inputs) => `sin(${inputs.angle})`
|
|
383
|
+
},
|
|
384
|
+
{
|
|
385
|
+
type: "trig_cos",
|
|
386
|
+
name: "Cosine",
|
|
387
|
+
category: "math",
|
|
388
|
+
description: "Cosine function",
|
|
389
|
+
inputs: [{ id: "angle", name: "Angle", type: "float", defaultValue: 0 }],
|
|
390
|
+
outputs: [{ id: "result", name: "Result", type: "float" }],
|
|
391
|
+
generateCode: (_, inputs) => `cos(${inputs.angle})`
|
|
392
|
+
},
|
|
393
|
+
{
|
|
394
|
+
type: "trig_tan",
|
|
395
|
+
name: "Tangent",
|
|
396
|
+
category: "math",
|
|
397
|
+
description: "Tangent function",
|
|
398
|
+
inputs: [{ id: "angle", name: "Angle", type: "float", defaultValue: 0 }],
|
|
399
|
+
outputs: [{ id: "result", name: "Result", type: "float" }],
|
|
400
|
+
generateCode: (_, inputs) => `tan(${inputs.angle})`
|
|
401
|
+
},
|
|
402
|
+
{
|
|
403
|
+
type: "trig_asin",
|
|
404
|
+
name: "Arcsine",
|
|
405
|
+
category: "math",
|
|
406
|
+
description: "Arcsine function",
|
|
407
|
+
inputs: [{ id: "value", name: "Value", type: "float", defaultValue: 0 }],
|
|
408
|
+
outputs: [{ id: "result", name: "Result", type: "float" }],
|
|
409
|
+
generateCode: (_, inputs) => `asin(${inputs.value})`
|
|
410
|
+
},
|
|
411
|
+
{
|
|
412
|
+
type: "trig_acos",
|
|
413
|
+
name: "Arccosine",
|
|
414
|
+
category: "math",
|
|
415
|
+
description: "Arccosine function",
|
|
416
|
+
inputs: [{ id: "value", name: "Value", type: "float", defaultValue: 0 }],
|
|
417
|
+
outputs: [{ id: "result", name: "Result", type: "float" }],
|
|
418
|
+
generateCode: (_, inputs) => `acos(${inputs.value})`
|
|
419
|
+
},
|
|
420
|
+
{
|
|
421
|
+
type: "trig_atan",
|
|
422
|
+
name: "Arctangent",
|
|
423
|
+
category: "math",
|
|
424
|
+
description: "Arctangent function",
|
|
425
|
+
inputs: [{ id: "value", name: "Value", type: "float", defaultValue: 0 }],
|
|
426
|
+
outputs: [{ id: "result", name: "Result", type: "float" }],
|
|
427
|
+
generateCode: (_, inputs) => `atan(${inputs.value})`
|
|
428
|
+
},
|
|
429
|
+
{
|
|
430
|
+
type: "trig_atan2",
|
|
431
|
+
name: "Arctangent2",
|
|
432
|
+
category: "math",
|
|
433
|
+
description: "Two-argument arctangent",
|
|
434
|
+
inputs: [
|
|
435
|
+
{ id: "y", name: "Y", type: "float", defaultValue: 0 },
|
|
436
|
+
{ id: "x", name: "X", type: "float", defaultValue: 1 }
|
|
437
|
+
],
|
|
438
|
+
outputs: [{ id: "result", name: "Result", type: "float" }],
|
|
439
|
+
generateCode: (_, inputs) => `atan2(${inputs.y}, ${inputs.x})`
|
|
440
|
+
},
|
|
441
|
+
{
|
|
442
|
+
type: "trig_radians",
|
|
443
|
+
name: "Degrees to Radians",
|
|
444
|
+
category: "math",
|
|
445
|
+
description: "Convert degrees to radians",
|
|
446
|
+
inputs: [{ id: "degrees", name: "Degrees", type: "float", defaultValue: 0 }],
|
|
447
|
+
outputs: [{ id: "result", name: "Radians", type: "float" }],
|
|
448
|
+
generateCode: (_, inputs) => `radians(${inputs.degrees})`
|
|
449
|
+
},
|
|
450
|
+
{
|
|
451
|
+
type: "trig_degrees",
|
|
452
|
+
name: "Radians to Degrees",
|
|
453
|
+
category: "math",
|
|
454
|
+
description: "Convert radians to degrees",
|
|
455
|
+
inputs: [{ id: "radians", name: "Radians", type: "float", defaultValue: 0 }],
|
|
456
|
+
outputs: [{ id: "result", name: "Degrees", type: "float" }],
|
|
457
|
+
generateCode: (_, inputs) => `degrees(${inputs.radians})`
|
|
458
|
+
}
|
|
459
|
+
];
|
|
460
|
+
var VECTOR_NODES = [
|
|
461
|
+
{
|
|
462
|
+
type: "vector_make_vec2",
|
|
463
|
+
name: "Make Vec2",
|
|
464
|
+
category: "vector",
|
|
465
|
+
description: "Create vec2 from components",
|
|
466
|
+
inputs: [
|
|
467
|
+
{ id: "x", name: "X", type: "float", defaultValue: 0 },
|
|
468
|
+
{ id: "y", name: "Y", type: "float", defaultValue: 0 }
|
|
469
|
+
],
|
|
470
|
+
outputs: [{ id: "vector", name: "Vector", type: "vec2" }],
|
|
471
|
+
generateCode: (_, inputs) => `vec2<f32>(${inputs.x}, ${inputs.y})`
|
|
472
|
+
},
|
|
473
|
+
{
|
|
474
|
+
type: "vector_make_vec3",
|
|
475
|
+
name: "Make Vec3",
|
|
476
|
+
category: "vector",
|
|
477
|
+
description: "Create vec3 from components",
|
|
478
|
+
inputs: [
|
|
479
|
+
{ id: "x", name: "X", type: "float", defaultValue: 0 },
|
|
480
|
+
{ id: "y", name: "Y", type: "float", defaultValue: 0 },
|
|
481
|
+
{ id: "z", name: "Z", type: "float", defaultValue: 0 }
|
|
482
|
+
],
|
|
483
|
+
outputs: [{ id: "vector", name: "Vector", type: "vec3" }],
|
|
484
|
+
generateCode: (_, inputs) => `vec3<f32>(${inputs.x}, ${inputs.y}, ${inputs.z})`
|
|
485
|
+
},
|
|
486
|
+
{
|
|
487
|
+
type: "vector_make_vec4",
|
|
488
|
+
name: "Make Vec4",
|
|
489
|
+
category: "vector",
|
|
490
|
+
description: "Create vec4 from components",
|
|
491
|
+
inputs: [
|
|
492
|
+
{ id: "x", name: "X", type: "float", defaultValue: 0 },
|
|
493
|
+
{ id: "y", name: "Y", type: "float", defaultValue: 0 },
|
|
494
|
+
{ id: "z", name: "Z", type: "float", defaultValue: 0 },
|
|
495
|
+
{ id: "w", name: "W", type: "float", defaultValue: 1 }
|
|
496
|
+
],
|
|
497
|
+
outputs: [{ id: "vector", name: "Vector", type: "vec4" }],
|
|
498
|
+
generateCode: (_, inputs) => `vec4<f32>(${inputs.x}, ${inputs.y}, ${inputs.z}, ${inputs.w})`
|
|
499
|
+
},
|
|
500
|
+
{
|
|
501
|
+
type: "vector_split_vec2",
|
|
502
|
+
name: "Split Vec2",
|
|
503
|
+
category: "vector",
|
|
504
|
+
description: "Split vec2 into components",
|
|
505
|
+
inputs: [{ id: "vector", name: "Vector", type: "vec2", defaultValue: [0, 0] }],
|
|
506
|
+
outputs: [
|
|
507
|
+
{ id: "x", name: "X", type: "float" },
|
|
508
|
+
{ id: "y", name: "Y", type: "float" }
|
|
509
|
+
],
|
|
510
|
+
generateCode: (_, inputs) => inputs.vector
|
|
511
|
+
},
|
|
512
|
+
{
|
|
513
|
+
type: "vector_split_vec3",
|
|
514
|
+
name: "Split Vec3",
|
|
515
|
+
category: "vector",
|
|
516
|
+
description: "Split vec3 into components",
|
|
517
|
+
inputs: [{ id: "vector", name: "Vector", type: "vec3", defaultValue: [0, 0, 0] }],
|
|
518
|
+
outputs: [
|
|
519
|
+
{ id: "x", name: "X", type: "float" },
|
|
520
|
+
{ id: "y", name: "Y", type: "float" },
|
|
521
|
+
{ id: "z", name: "Z", type: "float" }
|
|
522
|
+
],
|
|
523
|
+
generateCode: (_, inputs) => inputs.vector
|
|
524
|
+
},
|
|
525
|
+
{
|
|
526
|
+
type: "vector_split_vec4",
|
|
527
|
+
name: "Split Vec4",
|
|
528
|
+
category: "vector",
|
|
529
|
+
description: "Split vec4 into components",
|
|
530
|
+
inputs: [{ id: "vector", name: "Vector", type: "vec4", defaultValue: [0, 0, 0, 1] }],
|
|
531
|
+
outputs: [
|
|
532
|
+
{ id: "x", name: "X", type: "float" },
|
|
533
|
+
{ id: "y", name: "Y", type: "float" },
|
|
534
|
+
{ id: "z", name: "Z", type: "float" },
|
|
535
|
+
{ id: "w", name: "W", type: "float" }
|
|
536
|
+
],
|
|
537
|
+
generateCode: (_, inputs) => inputs.vector
|
|
538
|
+
},
|
|
539
|
+
{
|
|
540
|
+
type: "vector_normalize",
|
|
541
|
+
name: "Normalize",
|
|
542
|
+
category: "vector",
|
|
543
|
+
description: "Normalize vector",
|
|
544
|
+
inputs: [{ id: "vector", name: "Vector", type: "vec3", defaultValue: [1, 0, 0] }],
|
|
545
|
+
outputs: [{ id: "result", name: "Result", type: "vec3" }],
|
|
546
|
+
generateCode: (_, inputs) => `normalize(${inputs.vector})`
|
|
547
|
+
},
|
|
548
|
+
{
|
|
549
|
+
type: "vector_length",
|
|
550
|
+
name: "Length",
|
|
551
|
+
category: "vector",
|
|
552
|
+
description: "Vector length",
|
|
553
|
+
inputs: [{ id: "vector", name: "Vector", type: "vec3", defaultValue: [0, 0, 0] }],
|
|
554
|
+
outputs: [{ id: "length", name: "Length", type: "float" }],
|
|
555
|
+
generateCode: (_, inputs) => `length(${inputs.vector})`
|
|
556
|
+
},
|
|
557
|
+
{
|
|
558
|
+
type: "vector_distance",
|
|
559
|
+
name: "Distance",
|
|
560
|
+
category: "vector",
|
|
561
|
+
description: "Distance between vectors",
|
|
562
|
+
inputs: [
|
|
563
|
+
{ id: "a", name: "A", type: "vec3", defaultValue: [0, 0, 0] },
|
|
564
|
+
{ id: "b", name: "B", type: "vec3", defaultValue: [0, 0, 0] }
|
|
565
|
+
],
|
|
566
|
+
outputs: [{ id: "distance", name: "Distance", type: "float" }],
|
|
567
|
+
generateCode: (_, inputs) => `distance(${inputs.a}, ${inputs.b})`
|
|
568
|
+
},
|
|
569
|
+
{
|
|
570
|
+
type: "vector_dot",
|
|
571
|
+
name: "Dot Product",
|
|
572
|
+
category: "vector",
|
|
573
|
+
description: "Dot product",
|
|
574
|
+
inputs: [
|
|
575
|
+
{ id: "a", name: "A", type: "vec3", defaultValue: [1, 0, 0] },
|
|
576
|
+
{ id: "b", name: "B", type: "vec3", defaultValue: [1, 0, 0] }
|
|
577
|
+
],
|
|
578
|
+
outputs: [{ id: "result", name: "Result", type: "float" }],
|
|
579
|
+
generateCode: (_, inputs) => `dot(${inputs.a}, ${inputs.b})`
|
|
580
|
+
},
|
|
581
|
+
{
|
|
582
|
+
type: "vector_cross",
|
|
583
|
+
name: "Cross Product",
|
|
584
|
+
category: "vector",
|
|
585
|
+
description: "Cross product",
|
|
586
|
+
inputs: [
|
|
587
|
+
{ id: "a", name: "A", type: "vec3", defaultValue: [1, 0, 0] },
|
|
588
|
+
{ id: "b", name: "B", type: "vec3", defaultValue: [0, 1, 0] }
|
|
589
|
+
],
|
|
590
|
+
outputs: [{ id: "result", name: "Result", type: "vec3" }],
|
|
591
|
+
generateCode: (_, inputs) => `cross(${inputs.a}, ${inputs.b})`
|
|
592
|
+
},
|
|
593
|
+
{
|
|
594
|
+
type: "vector_reflect",
|
|
595
|
+
name: "Reflect",
|
|
596
|
+
category: "vector",
|
|
597
|
+
description: "Reflect vector",
|
|
598
|
+
inputs: [
|
|
599
|
+
{ id: "incident", name: "Incident", type: "vec3", defaultValue: [1, 0, 0] },
|
|
600
|
+
{ id: "normal", name: "Normal", type: "vec3", defaultValue: [0, 1, 0] }
|
|
601
|
+
],
|
|
602
|
+
outputs: [{ id: "result", name: "Result", type: "vec3" }],
|
|
603
|
+
generateCode: (_, inputs) => `reflect(${inputs.incident}, ${inputs.normal})`
|
|
604
|
+
},
|
|
605
|
+
{
|
|
606
|
+
type: "vector_refract",
|
|
607
|
+
name: "Refract",
|
|
608
|
+
category: "vector",
|
|
609
|
+
description: "Refract vector",
|
|
610
|
+
inputs: [
|
|
611
|
+
{ id: "incident", name: "Incident", type: "vec3", defaultValue: [1, 0, 0] },
|
|
612
|
+
{ id: "normal", name: "Normal", type: "vec3", defaultValue: [0, 1, 0] },
|
|
613
|
+
{ id: "eta", name: "Eta", type: "float", defaultValue: 1 }
|
|
614
|
+
],
|
|
615
|
+
outputs: [{ id: "result", name: "Result", type: "vec3" }],
|
|
616
|
+
generateCode: (_, inputs) => `refract(${inputs.incident}, ${inputs.normal}, ${inputs.eta})`
|
|
617
|
+
}
|
|
618
|
+
];
|
|
619
|
+
var COLOR_NODES = [
|
|
620
|
+
{
|
|
621
|
+
type: "color_blend",
|
|
622
|
+
name: "Blend",
|
|
623
|
+
category: "color",
|
|
624
|
+
description: "Blend two colors",
|
|
625
|
+
inputs: [
|
|
626
|
+
{ id: "a", name: "A", type: "vec4", defaultValue: [0, 0, 0, 1] },
|
|
627
|
+
{ id: "b", name: "B", type: "vec4", defaultValue: [1, 1, 1, 1] },
|
|
628
|
+
{ id: "factor", name: "Factor", type: "float", defaultValue: 0.5 }
|
|
629
|
+
],
|
|
630
|
+
outputs: [{ id: "result", name: "Result", type: "vec4" }],
|
|
631
|
+
generateCode: (_, inputs) => `mix(${inputs.a}, ${inputs.b}, ${inputs.factor})`
|
|
632
|
+
},
|
|
633
|
+
{
|
|
634
|
+
type: "color_hue_shift",
|
|
635
|
+
name: "Hue Shift",
|
|
636
|
+
category: "color",
|
|
637
|
+
description: "Shift hue of color",
|
|
638
|
+
inputs: [
|
|
639
|
+
{ id: "color", name: "Color", type: "vec3", defaultValue: [1, 0, 0] },
|
|
640
|
+
{ id: "shift", name: "Shift", type: "float", defaultValue: 0 }
|
|
641
|
+
],
|
|
642
|
+
outputs: [{ id: "result", name: "Result", type: "vec3" }],
|
|
643
|
+
generateCode: (_, inputs) => `hueShift(${inputs.color}, ${inputs.shift})`
|
|
644
|
+
},
|
|
645
|
+
{
|
|
646
|
+
type: "color_saturation",
|
|
647
|
+
name: "Saturation",
|
|
648
|
+
category: "color",
|
|
649
|
+
description: "Adjust color saturation",
|
|
650
|
+
inputs: [
|
|
651
|
+
{ id: "color", name: "Color", type: "vec3", defaultValue: [1, 0.5, 0.5] },
|
|
652
|
+
{ id: "saturation", name: "Saturation", type: "float", defaultValue: 1 }
|
|
653
|
+
],
|
|
654
|
+
outputs: [{ id: "result", name: "Result", type: "vec3" }],
|
|
655
|
+
generateCode: (_, inputs) => {
|
|
656
|
+
return `mix(vec3<f32>(dot(${inputs.color}, vec3<f32>(0.299, 0.587, 0.114))), ${inputs.color}, ${inputs.saturation})`;
|
|
657
|
+
}
|
|
658
|
+
},
|
|
659
|
+
{
|
|
660
|
+
type: "color_brightness",
|
|
661
|
+
name: "Brightness",
|
|
662
|
+
category: "color",
|
|
663
|
+
description: "Adjust color brightness",
|
|
664
|
+
inputs: [
|
|
665
|
+
{ id: "color", name: "Color", type: "vec3", defaultValue: [0.5, 0.5, 0.5] },
|
|
666
|
+
{ id: "brightness", name: "Brightness", type: "float", defaultValue: 1 }
|
|
667
|
+
],
|
|
668
|
+
outputs: [{ id: "result", name: "Result", type: "vec3" }],
|
|
669
|
+
generateCode: (_, inputs) => `(${inputs.color} * ${inputs.brightness})`
|
|
670
|
+
},
|
|
671
|
+
{
|
|
672
|
+
type: "color_contrast",
|
|
673
|
+
name: "Contrast",
|
|
674
|
+
category: "color",
|
|
675
|
+
description: "Adjust color contrast",
|
|
676
|
+
inputs: [
|
|
677
|
+
{ id: "color", name: "Color", type: "vec3", defaultValue: [0.5, 0.5, 0.5] },
|
|
678
|
+
{ id: "contrast", name: "Contrast", type: "float", defaultValue: 1 }
|
|
679
|
+
],
|
|
680
|
+
outputs: [{ id: "result", name: "Result", type: "vec3" }],
|
|
681
|
+
generateCode: (_, inputs) => `((${inputs.color} - vec3<f32>(0.5)) * ${inputs.contrast} + vec3<f32>(0.5))`
|
|
682
|
+
},
|
|
683
|
+
{
|
|
684
|
+
type: "color_invert",
|
|
685
|
+
name: "Invert",
|
|
686
|
+
category: "color",
|
|
687
|
+
description: "Invert color",
|
|
688
|
+
inputs: [{ id: "color", name: "Color", type: "vec3", defaultValue: [0.5, 0.5, 0.5] }],
|
|
689
|
+
outputs: [{ id: "result", name: "Result", type: "vec3" }],
|
|
690
|
+
generateCode: (_, inputs) => `(vec3<f32>(1.0) - ${inputs.color})`
|
|
691
|
+
},
|
|
692
|
+
{
|
|
693
|
+
type: "color_grayscale",
|
|
694
|
+
name: "Grayscale",
|
|
695
|
+
category: "color",
|
|
696
|
+
description: "Convert to grayscale",
|
|
697
|
+
inputs: [{ id: "color", name: "Color", type: "vec3", defaultValue: [1, 0, 0] }],
|
|
698
|
+
outputs: [{ id: "result", name: "Gray", type: "float" }],
|
|
699
|
+
generateCode: (_, inputs) => `dot(${inputs.color}, vec3<f32>(0.299, 0.587, 0.114))`
|
|
700
|
+
}
|
|
701
|
+
];
|
|
702
|
+
var TEXTURE_NODES = [
|
|
703
|
+
{
|
|
704
|
+
type: "texture_sample",
|
|
705
|
+
name: "Sample Texture",
|
|
706
|
+
category: "texture",
|
|
707
|
+
description: "Sample a 2D texture",
|
|
708
|
+
inputs: [
|
|
709
|
+
{ id: "texture", name: "Texture", type: "sampler2D" },
|
|
710
|
+
{ id: "uv", name: "UV", type: "vec2", defaultValue: [0, 0] }
|
|
711
|
+
],
|
|
712
|
+
outputs: [{ id: "color", name: "Color", type: "vec4" }],
|
|
713
|
+
generateCode: (node, inputs) => {
|
|
714
|
+
const textureId = node.properties?.textureId ?? "defaultTexture";
|
|
715
|
+
return `textureSample(${textureId}, ${textureId}Sampler, ${inputs.uv})`;
|
|
716
|
+
}
|
|
717
|
+
},
|
|
718
|
+
{
|
|
719
|
+
type: "texture_sample_level",
|
|
720
|
+
name: "Sample Texture Level",
|
|
721
|
+
category: "texture",
|
|
722
|
+
description: "Sample texture at specific mip level",
|
|
723
|
+
inputs: [
|
|
724
|
+
{ id: "texture", name: "Texture", type: "sampler2D" },
|
|
725
|
+
{ id: "uv", name: "UV", type: "vec2", defaultValue: [0, 0] },
|
|
726
|
+
{ id: "level", name: "Level", type: "float", defaultValue: 0 }
|
|
727
|
+
],
|
|
728
|
+
outputs: [{ id: "color", name: "Color", type: "vec4" }],
|
|
729
|
+
generateCode: (node, inputs) => {
|
|
730
|
+
const textureId = node.properties?.textureId ?? "defaultTexture";
|
|
731
|
+
return `textureSampleLevel(${textureId}, ${textureId}Sampler, ${inputs.uv}, ${inputs.level})`;
|
|
732
|
+
}
|
|
733
|
+
},
|
|
734
|
+
{
|
|
735
|
+
type: "texture_tiling_offset",
|
|
736
|
+
name: "Tiling and Offset",
|
|
737
|
+
category: "texture",
|
|
738
|
+
description: "Apply tiling and offset to UVs",
|
|
739
|
+
inputs: [
|
|
740
|
+
{ id: "uv", name: "UV", type: "vec2", defaultValue: [0, 0] },
|
|
741
|
+
{ id: "tiling", name: "Tiling", type: "vec2", defaultValue: [1, 1] },
|
|
742
|
+
{ id: "offset", name: "Offset", type: "vec2", defaultValue: [0, 0] }
|
|
743
|
+
],
|
|
744
|
+
outputs: [{ id: "result", name: "Result", type: "vec2" }],
|
|
745
|
+
generateCode: (_, inputs) => `(${inputs.uv} * ${inputs.tiling} + ${inputs.offset})`
|
|
746
|
+
}
|
|
747
|
+
];
|
|
748
|
+
var UTILITY_NODES = [
|
|
749
|
+
{
|
|
750
|
+
type: "utility_fresnel",
|
|
751
|
+
name: "Fresnel",
|
|
752
|
+
category: "utility",
|
|
753
|
+
description: "Fresnel effect",
|
|
754
|
+
inputs: [
|
|
755
|
+
{ id: "normal", name: "Normal", type: "vec3", defaultValue: [0, 1, 0] },
|
|
756
|
+
{ id: "viewDir", name: "View Dir", type: "vec3", defaultValue: [0, 0, 1] },
|
|
757
|
+
{ id: "power", name: "Power", type: "float", defaultValue: 5 }
|
|
758
|
+
],
|
|
759
|
+
outputs: [{ id: "result", name: "Result", type: "float" }],
|
|
760
|
+
generateCode: (_, inputs) => `pow(1.0 - saturate(dot(${inputs.normal}, ${inputs.viewDir})), ${inputs.power})`
|
|
761
|
+
},
|
|
762
|
+
{
|
|
763
|
+
type: "utility_noise_simple",
|
|
764
|
+
name: "Simple Noise",
|
|
765
|
+
category: "utility",
|
|
766
|
+
description: "Simple value noise",
|
|
767
|
+
inputs: [{ id: "uv", name: "UV", type: "vec2", defaultValue: [0, 0] }],
|
|
768
|
+
outputs: [{ id: "noise", name: "Noise", type: "float" }],
|
|
769
|
+
generateCode: (_, inputs) => `simpleNoise(${inputs.uv})`
|
|
770
|
+
},
|
|
771
|
+
{
|
|
772
|
+
type: "utility_gradient_noise",
|
|
773
|
+
name: "Gradient Noise",
|
|
774
|
+
category: "utility",
|
|
775
|
+
description: "Perlin-like gradient noise",
|
|
776
|
+
inputs: [{ id: "uv", name: "UV", type: "vec2", defaultValue: [0, 0] }],
|
|
777
|
+
outputs: [{ id: "noise", name: "Noise", type: "float" }],
|
|
778
|
+
generateCode: (_, inputs) => `gradientNoise(${inputs.uv})`
|
|
779
|
+
},
|
|
780
|
+
{
|
|
781
|
+
type: "utility_voronoi",
|
|
782
|
+
name: "Voronoi",
|
|
783
|
+
category: "utility",
|
|
784
|
+
description: "Voronoi noise",
|
|
785
|
+
inputs: [
|
|
786
|
+
{ id: "uv", name: "UV", type: "vec2", defaultValue: [0, 0] },
|
|
787
|
+
{ id: "scale", name: "Scale", type: "float", defaultValue: 5 }
|
|
788
|
+
],
|
|
789
|
+
outputs: [
|
|
790
|
+
{ id: "cells", name: "Cells", type: "float" },
|
|
791
|
+
{ id: "distance", name: "Distance", type: "float" }
|
|
792
|
+
],
|
|
793
|
+
generateCode: (_, inputs) => `voronoi(${inputs.uv} * ${inputs.scale})`
|
|
794
|
+
},
|
|
795
|
+
{
|
|
796
|
+
type: "utility_remap",
|
|
797
|
+
name: "Remap",
|
|
798
|
+
category: "utility",
|
|
799
|
+
description: "Remap value from one range to another",
|
|
800
|
+
inputs: [
|
|
801
|
+
{ id: "value", name: "Value", type: "float", defaultValue: 0 },
|
|
802
|
+
{ id: "inMin", name: "In Min", type: "float", defaultValue: 0 },
|
|
803
|
+
{ id: "inMax", name: "In Max", type: "float", defaultValue: 1 },
|
|
804
|
+
{ id: "outMin", name: "Out Min", type: "float", defaultValue: 0 },
|
|
805
|
+
{ id: "outMax", name: "Out Max", type: "float", defaultValue: 1 }
|
|
806
|
+
],
|
|
807
|
+
outputs: [{ id: "result", name: "Result", type: "float" }],
|
|
808
|
+
generateCode: (_, inputs) => {
|
|
809
|
+
return `(${inputs.outMin} + (${inputs.value} - ${inputs.inMin}) * (${inputs.outMax} - ${inputs.outMin}) / (${inputs.inMax} - ${inputs.inMin}))`;
|
|
810
|
+
}
|
|
811
|
+
},
|
|
812
|
+
{
|
|
813
|
+
type: "utility_if",
|
|
814
|
+
name: "Branch",
|
|
815
|
+
category: "utility",
|
|
816
|
+
description: "Conditional selection",
|
|
817
|
+
inputs: [
|
|
818
|
+
{ id: "condition", name: "Condition", type: "float", defaultValue: 0 },
|
|
819
|
+
{ id: "true", name: "True", type: "float", defaultValue: 1 },
|
|
820
|
+
{ id: "false", name: "False", type: "float", defaultValue: 0 }
|
|
821
|
+
],
|
|
822
|
+
outputs: [{ id: "result", name: "Result", type: "float" }],
|
|
823
|
+
generateCode: (_, inputs) => `select(${inputs.false}, ${inputs.true}, ${inputs.condition} > 0.5)`
|
|
824
|
+
},
|
|
825
|
+
{
|
|
826
|
+
type: "utility_compare",
|
|
827
|
+
name: "Compare",
|
|
828
|
+
category: "utility",
|
|
829
|
+
description: "Compare two values",
|
|
830
|
+
inputs: [
|
|
831
|
+
{ id: "a", name: "A", type: "float", defaultValue: 0 },
|
|
832
|
+
{ id: "b", name: "B", type: "float", defaultValue: 0 }
|
|
833
|
+
],
|
|
834
|
+
outputs: [
|
|
835
|
+
{ id: "equal", name: "A == B", type: "float" },
|
|
836
|
+
{ id: "less", name: "A < B", type: "float" },
|
|
837
|
+
{ id: "greater", name: "A > B", type: "float" }
|
|
838
|
+
],
|
|
839
|
+
generateCode: (_, inputs) => `compareValues(${inputs.a}, ${inputs.b})`
|
|
840
|
+
}
|
|
841
|
+
];
|
|
842
|
+
var ADVANCED_MATERIAL_NODES = [
|
|
843
|
+
{
|
|
844
|
+
type: "blackbody",
|
|
845
|
+
name: "Blackbody Radiation",
|
|
846
|
+
category: "material",
|
|
847
|
+
description: "Convert temperature (Kelvin) to emission color via Planck approximation",
|
|
848
|
+
inputs: [
|
|
849
|
+
{ id: "temperature", name: "Temperature K", type: "float", defaultValue: 5500 },
|
|
850
|
+
{ id: "intensity", name: "Intensity", type: "float", defaultValue: 1 }
|
|
851
|
+
],
|
|
852
|
+
outputs: [{ id: "color", name: "Color", type: "vec3" }],
|
|
853
|
+
generateCode: (_, inputs) => `blackbodyColor(${inputs.temperature}, ${inputs.intensity})`
|
|
854
|
+
},
|
|
855
|
+
{
|
|
856
|
+
type: "sparkle",
|
|
857
|
+
name: "Sparkle / Glitter",
|
|
858
|
+
category: "material",
|
|
859
|
+
description: "Stochastic micro-facet flashing for glitter, metallic paint, mica",
|
|
860
|
+
inputs: [
|
|
861
|
+
{ id: "uv", name: "UV", type: "vec2", defaultValue: [0, 0] },
|
|
862
|
+
{ id: "density", name: "Density", type: "float", defaultValue: 50 },
|
|
863
|
+
{ id: "size", name: "Size", type: "float", defaultValue: 0.02 },
|
|
864
|
+
{ id: "intensity", name: "Intensity", type: "float", defaultValue: 1 },
|
|
865
|
+
{ id: "viewDir", name: "View Dir", type: "vec3", defaultValue: [0, 0, 1] },
|
|
866
|
+
{ id: "normal", name: "Normal", type: "vec3", defaultValue: [0, 0, 1] }
|
|
867
|
+
],
|
|
868
|
+
outputs: [
|
|
869
|
+
{ id: "flash", name: "Flash", type: "float" },
|
|
870
|
+
{ id: "color", name: "Color", type: "vec3" }
|
|
871
|
+
],
|
|
872
|
+
generateCode: (_, inputs) => `sparkleFlash(${inputs.uv}, ${inputs.density}, ${inputs.size}, ${inputs.intensity}, ${inputs.viewDir}, ${inputs.normal})`
|
|
873
|
+
},
|
|
874
|
+
{
|
|
875
|
+
type: "animated_pattern",
|
|
876
|
+
name: "Animated Pattern",
|
|
877
|
+
category: "material",
|
|
878
|
+
description: "Time-varying surface patterns: ripple, flicker, pulse, flow, wave, noise",
|
|
879
|
+
inputs: [
|
|
880
|
+
{ id: "uv", name: "UV", type: "vec2", defaultValue: [0, 0] },
|
|
881
|
+
{ id: "time", name: "Time", type: "float", defaultValue: 0 },
|
|
882
|
+
{ id: "patternType", name: "Pattern (0-7)", type: "float", defaultValue: 0 },
|
|
883
|
+
{ id: "speed", name: "Speed", type: "float", defaultValue: 1 },
|
|
884
|
+
{ id: "amplitude", name: "Amplitude", type: "float", defaultValue: 1 },
|
|
885
|
+
{ id: "scale", name: "Scale", type: "float", defaultValue: 1 }
|
|
886
|
+
],
|
|
887
|
+
outputs: [
|
|
888
|
+
{ id: "value", name: "Value", type: "float" },
|
|
889
|
+
{ id: "offset", name: "UV Offset", type: "vec2" }
|
|
890
|
+
],
|
|
891
|
+
generateCode: (_, inputs) => `animatedPattern(${inputs.uv}, ${inputs.time}, ${inputs.patternType}, ${inputs.speed}, ${inputs.amplitude}, ${inputs.scale})`
|
|
892
|
+
},
|
|
893
|
+
{
|
|
894
|
+
type: "weathering",
|
|
895
|
+
name: "Weathering",
|
|
896
|
+
category: "material",
|
|
897
|
+
description: "Procedural surface degradation: rust, moss, crack, peel, patina, frost",
|
|
898
|
+
inputs: [
|
|
899
|
+
{ id: "uv", name: "UV", type: "vec2", defaultValue: [0, 0] },
|
|
900
|
+
{ id: "progress", name: "Progress", type: "float", defaultValue: 0 },
|
|
901
|
+
{ id: "weatherType", name: "Type (0-9)", type: "float", defaultValue: 0 },
|
|
902
|
+
{ id: "seed", name: "Seed", type: "float", defaultValue: 42 },
|
|
903
|
+
{ id: "baseColor", name: "Base Color", type: "vec3", defaultValue: [1, 1, 1] }
|
|
904
|
+
],
|
|
905
|
+
outputs: [
|
|
906
|
+
{ id: "color", name: "Weathered Color", type: "vec3" },
|
|
907
|
+
{ id: "mask", name: "Mask", type: "float" },
|
|
908
|
+
{ id: "roughnessMod", name: "Roughness Mod", type: "float" }
|
|
909
|
+
],
|
|
910
|
+
generateCode: (_, inputs) => `weatheringSurface(${inputs.uv}, ${inputs.progress}, ${inputs.weatherType}, ${inputs.seed}, ${inputs.baseColor})`
|
|
911
|
+
},
|
|
912
|
+
{
|
|
913
|
+
type: "dual_layer_blend",
|
|
914
|
+
name: "Dual Layer Blend",
|
|
915
|
+
category: "material",
|
|
916
|
+
description: "Blend two material layers (paint over rust, moss over stone, snow on branches)",
|
|
917
|
+
inputs: [
|
|
918
|
+
{ id: "baseColor", name: "Base Color", type: "vec3", defaultValue: [0.5, 0.5, 0.5] },
|
|
919
|
+
{ id: "baseRoughness", name: "Base Rough", type: "float", defaultValue: 0.5 },
|
|
920
|
+
{ id: "baseMetallic", name: "Base Metal", type: "float", defaultValue: 0 },
|
|
921
|
+
{ id: "topColor", name: "Top Color", type: "vec3", defaultValue: [1, 1, 1] },
|
|
922
|
+
{ id: "topRoughness", name: "Top Rough", type: "float", defaultValue: 0.5 },
|
|
923
|
+
{ id: "topMetallic", name: "Top Metal", type: "float", defaultValue: 0 },
|
|
924
|
+
{ id: "blendFactor", name: "Blend", type: "float", defaultValue: 0.5 },
|
|
925
|
+
{ id: "blendMode", name: "Mode (0-3)", type: "float", defaultValue: 0 },
|
|
926
|
+
{ id: "normal", name: "Normal", type: "vec3", defaultValue: [0, 0, 1] },
|
|
927
|
+
{ id: "uv", name: "UV", type: "vec2", defaultValue: [0, 0] }
|
|
928
|
+
],
|
|
929
|
+
outputs: [
|
|
930
|
+
{ id: "color", name: "Color", type: "vec3" },
|
|
931
|
+
{ id: "roughness", name: "Roughness", type: "float" },
|
|
932
|
+
{ id: "metallic", name: "Metallic", type: "float" }
|
|
933
|
+
],
|
|
934
|
+
generateCode: (_, inputs) => `dualLayerBlend(${inputs.baseColor}, ${inputs.baseRoughness}, ${inputs.baseMetallic}, ${inputs.topColor}, ${inputs.topRoughness}, ${inputs.topMetallic}, ${inputs.blendFactor}, ${inputs.blendMode}, ${inputs.normal}, ${inputs.uv})`
|
|
935
|
+
},
|
|
936
|
+
{
|
|
937
|
+
type: "fluorescence",
|
|
938
|
+
name: "Fluorescence",
|
|
939
|
+
category: "material",
|
|
940
|
+
description: "UV-reactive emission: absorb short wavelengths, emit longer ones",
|
|
941
|
+
inputs: [
|
|
942
|
+
{ id: "lightColor", name: "Light Color", type: "vec3", defaultValue: [1, 1, 1] },
|
|
943
|
+
{ id: "excitationColor", name: "Excitation", type: "vec3", defaultValue: [0.2, 0, 0.5] },
|
|
944
|
+
{ id: "emissionColor", name: "Emission", type: "vec3", defaultValue: [0, 1, 0] },
|
|
945
|
+
{ id: "intensity", name: "Intensity", type: "float", defaultValue: 1 }
|
|
946
|
+
],
|
|
947
|
+
outputs: [{ id: "emission", name: "Emission", type: "vec3" }],
|
|
948
|
+
generateCode: (_, inputs) => `fluorescenceEmit(${inputs.lightColor}, ${inputs.excitationColor}, ${inputs.emissionColor}, ${inputs.intensity})`
|
|
949
|
+
},
|
|
950
|
+
{
|
|
951
|
+
type: "retroreflection",
|
|
952
|
+
name: "Retroreflection",
|
|
953
|
+
category: "material",
|
|
954
|
+
description: "Light bounces back toward source (road signs, safety vests, cat eyes)",
|
|
955
|
+
inputs: [
|
|
956
|
+
{ id: "viewDir", name: "View Dir", type: "vec3", defaultValue: [0, 0, 1] },
|
|
957
|
+
{ id: "lightDir", name: "Light Dir", type: "vec3", defaultValue: [0, 1, 0] },
|
|
958
|
+
{ id: "normal", name: "Normal", type: "vec3", defaultValue: [0, 0, 1] },
|
|
959
|
+
{ id: "intensity", name: "Intensity", type: "float", defaultValue: 1 }
|
|
960
|
+
],
|
|
961
|
+
outputs: [{ id: "reflectance", name: "Reflectance", type: "float" }],
|
|
962
|
+
generateCode: (_, inputs) => `retroReflect(${inputs.viewDir}, ${inputs.lightDir}, ${inputs.normal}, ${inputs.intensity})`
|
|
963
|
+
}
|
|
964
|
+
];
|
|
965
|
+
var VOLUMETRIC_NODES = [
|
|
966
|
+
{
|
|
967
|
+
type: "volume_density",
|
|
968
|
+
name: "Volume Density",
|
|
969
|
+
category: "volumetric",
|
|
970
|
+
description: "Sample/generate volume density at a 3D position",
|
|
971
|
+
inputs: [
|
|
972
|
+
{ id: "position", name: "Position", type: "vec3", defaultValue: [0, 0, 0] },
|
|
973
|
+
{ id: "baseDensity", name: "Base Density", type: "float", defaultValue: 0.5 },
|
|
974
|
+
{ id: "noiseScale", name: "Noise Scale", type: "float", defaultValue: 4 },
|
|
975
|
+
{ id: "noiseOctaves", name: "Octaves", type: "float", defaultValue: 4 },
|
|
976
|
+
{ id: "time", name: "Time", type: "float", defaultValue: 0 }
|
|
977
|
+
],
|
|
978
|
+
outputs: [{ id: "density", name: "Density", type: "float" }],
|
|
979
|
+
generateCode: (_, inputs) => `volumeDensity(${inputs.position}, ${inputs.baseDensity}, ${inputs.noiseScale}, ${inputs.noiseOctaves}, ${inputs.time})`
|
|
980
|
+
},
|
|
981
|
+
{
|
|
982
|
+
type: "volume_noise_3d",
|
|
983
|
+
name: "3D FBM Noise",
|
|
984
|
+
category: "volumetric",
|
|
985
|
+
description: "Fractal Brownian motion noise in 3D for volumetric density",
|
|
986
|
+
inputs: [
|
|
987
|
+
{ id: "position", name: "Position", type: "vec3", defaultValue: [0, 0, 0] },
|
|
988
|
+
{ id: "octaves", name: "Octaves", type: "float", defaultValue: 4 },
|
|
989
|
+
{ id: "lacunarity", name: "Lacunarity", type: "float", defaultValue: 2 },
|
|
990
|
+
{ id: "gain", name: "Gain", type: "float", defaultValue: 0.5 }
|
|
991
|
+
],
|
|
992
|
+
outputs: [{ id: "noise", name: "Noise", type: "float" }],
|
|
993
|
+
generateCode: (_, inputs) => `fbmNoise3D(${inputs.position}, i32(${inputs.octaves}), ${inputs.lacunarity}, ${inputs.gain})`
|
|
994
|
+
},
|
|
995
|
+
{
|
|
996
|
+
type: "volume_curl_noise",
|
|
997
|
+
name: "Curl Noise 3D",
|
|
998
|
+
category: "volumetric",
|
|
999
|
+
description: "Divergence-free curl noise for smoke/fire turbulence",
|
|
1000
|
+
inputs: [
|
|
1001
|
+
{ id: "position", name: "Position", type: "vec3", defaultValue: [0, 0, 0] },
|
|
1002
|
+
{ id: "scale", name: "Scale", type: "float", defaultValue: 1 }
|
|
1003
|
+
],
|
|
1004
|
+
outputs: [{ id: "curl", name: "Curl", type: "vec3" }],
|
|
1005
|
+
generateCode: (_, inputs) => `curlNoise3D(${inputs.position} * ${inputs.scale})`
|
|
1006
|
+
},
|
|
1007
|
+
{
|
|
1008
|
+
type: "volume_scatter",
|
|
1009
|
+
name: "Volume Scattering",
|
|
1010
|
+
category: "volumetric",
|
|
1011
|
+
description: "Beer-Lambert absorption + Henyey-Greenstein phase function",
|
|
1012
|
+
inputs: [
|
|
1013
|
+
{ id: "density", name: "Density", type: "float", defaultValue: 0.5 },
|
|
1014
|
+
{ id: "scattering", name: "Scattering", type: "float", defaultValue: 0.5 },
|
|
1015
|
+
{ id: "absorption", name: "Absorption", type: "float", defaultValue: 0.1 },
|
|
1016
|
+
{ id: "stepLength", name: "Step Length", type: "float", defaultValue: 0.1 },
|
|
1017
|
+
{ id: "lightDir", name: "Light Dir", type: "vec3", defaultValue: [0, 1, 0] },
|
|
1018
|
+
{ id: "viewDir", name: "View Dir", type: "vec3", defaultValue: [0, 0, 1] },
|
|
1019
|
+
{ id: "anisotropy", name: "Phase Aniso", type: "float", defaultValue: 0.3 }
|
|
1020
|
+
],
|
|
1021
|
+
outputs: [
|
|
1022
|
+
{ id: "transmittance", name: "Transmittance", type: "float" },
|
|
1023
|
+
{ id: "inScatter", name: "In-Scatter", type: "float" }
|
|
1024
|
+
],
|
|
1025
|
+
generateCode: (_, inputs) => `volumeScatter(${inputs.density}, ${inputs.scattering}, ${inputs.absorption}, ${inputs.stepLength}, ${inputs.lightDir}, ${inputs.viewDir}, ${inputs.anisotropy})`
|
|
1026
|
+
},
|
|
1027
|
+
{
|
|
1028
|
+
type: "volume_emission",
|
|
1029
|
+
name: "Volume Emission",
|
|
1030
|
+
category: "volumetric",
|
|
1031
|
+
description: "Self-luminous volume contribution (fire, neon gas, aurora)",
|
|
1032
|
+
inputs: [
|
|
1033
|
+
{ id: "density", name: "Density", type: "float", defaultValue: 0.5 },
|
|
1034
|
+
{ id: "emissionColor", name: "Emission Color", type: "vec3", defaultValue: [1, 0.5, 0] },
|
|
1035
|
+
{ id: "intensity", name: "Intensity", type: "float", defaultValue: 1 },
|
|
1036
|
+
{ id: "temperature", name: "Temperature K", type: "float", defaultValue: 0 }
|
|
1037
|
+
],
|
|
1038
|
+
outputs: [{ id: "emission", name: "Emission", type: "vec3" }],
|
|
1039
|
+
generateCode: (_, inputs) => `volumeEmission(${inputs.density}, ${inputs.emissionColor}, ${inputs.intensity}, ${inputs.temperature})`
|
|
1040
|
+
},
|
|
1041
|
+
{
|
|
1042
|
+
type: "volume_height_fog",
|
|
1043
|
+
name: "Height Fog",
|
|
1044
|
+
category: "volumetric",
|
|
1045
|
+
description: "Height-attenuated fog density with exponential falloff",
|
|
1046
|
+
inputs: [
|
|
1047
|
+
{ id: "position", name: "Position", type: "vec3", defaultValue: [0, 0, 0] },
|
|
1048
|
+
{ id: "groundLevel", name: "Ground Y", type: "float", defaultValue: 0 },
|
|
1049
|
+
{ id: "density", name: "Density", type: "float", defaultValue: 0.5 },
|
|
1050
|
+
{ id: "falloff", name: "Falloff", type: "float", defaultValue: 2 }
|
|
1051
|
+
],
|
|
1052
|
+
outputs: [{ id: "density", name: "Density", type: "float" }],
|
|
1053
|
+
generateCode: (_, inputs) => `heightFogDensity(${inputs.position}, ${inputs.groundLevel}, ${inputs.density}, ${inputs.falloff})`
|
|
1054
|
+
},
|
|
1055
|
+
{
|
|
1056
|
+
type: "volume_fire_density",
|
|
1057
|
+
name: "Fire Density",
|
|
1058
|
+
category: "volumetric",
|
|
1059
|
+
description: "Fire-specific density: buoyancy, turbulence, temperature \u2192 emission",
|
|
1060
|
+
inputs: [
|
|
1061
|
+
{ id: "position", name: "Position", type: "vec3", defaultValue: [0, 0, 0] },
|
|
1062
|
+
{ id: "time", name: "Time", type: "float", defaultValue: 0 },
|
|
1063
|
+
{ id: "turbulence", name: "Turbulence", type: "float", defaultValue: 0.5 },
|
|
1064
|
+
{ id: "riseSpeed", name: "Rise Speed", type: "float", defaultValue: 1 },
|
|
1065
|
+
{ id: "scale", name: "Scale", type: "float", defaultValue: 2 }
|
|
1066
|
+
],
|
|
1067
|
+
outputs: [
|
|
1068
|
+
{ id: "density", name: "Density", type: "float" },
|
|
1069
|
+
{ id: "temperature", name: "Temperature", type: "float" }
|
|
1070
|
+
],
|
|
1071
|
+
generateCode: (_, inputs) => `fireDensity(${inputs.position}, ${inputs.time}, ${inputs.turbulence}, ${inputs.riseSpeed}, ${inputs.scale})`
|
|
1072
|
+
}
|
|
1073
|
+
];
|
|
1074
|
+
var OUTPUT_NODES = [
|
|
1075
|
+
{
|
|
1076
|
+
type: "output_surface",
|
|
1077
|
+
name: "Surface Output",
|
|
1078
|
+
category: "output",
|
|
1079
|
+
description: "PBR surface output with SSS, sheen, anisotropy, clearcoat, iridescence",
|
|
1080
|
+
inputs: [
|
|
1081
|
+
{ id: "baseColor", name: "Base Color", type: "vec3", defaultValue: [1, 1, 1] },
|
|
1082
|
+
{ id: "metallic", name: "Metallic", type: "float", defaultValue: 0 },
|
|
1083
|
+
{ id: "roughness", name: "Roughness", type: "float", defaultValue: 0.5 },
|
|
1084
|
+
{ id: "normal", name: "Normal", type: "vec3", defaultValue: [0, 0, 1] },
|
|
1085
|
+
{ id: "emission", name: "Emission", type: "vec3", defaultValue: [0, 0, 0] },
|
|
1086
|
+
{ id: "alpha", name: "Alpha", type: "float", defaultValue: 1 },
|
|
1087
|
+
{ id: "ao", name: "AO", type: "float", defaultValue: 1 },
|
|
1088
|
+
// Advanced PBR
|
|
1089
|
+
{ id: "sheenColor", name: "Sheen Color", type: "vec3", defaultValue: [0, 0, 0] },
|
|
1090
|
+
{ id: "sheenRoughness", name: "Sheen Roughness", type: "float", defaultValue: 0 },
|
|
1091
|
+
{ id: "subsurfaceThickness", name: "SSS Thickness", type: "float", defaultValue: 0 },
|
|
1092
|
+
{ id: "subsurfaceColor", name: "SSS Color", type: "vec3", defaultValue: [1, 1, 1] },
|
|
1093
|
+
{ id: "anisotropy", name: "Anisotropy", type: "float", defaultValue: 0 },
|
|
1094
|
+
{ id: "anisotropyRotation", name: "Aniso Rotation", type: "float", defaultValue: 0 },
|
|
1095
|
+
{ id: "clearcoat", name: "Clearcoat", type: "float", defaultValue: 0 },
|
|
1096
|
+
{ id: "clearcoatRoughness", name: "Clearcoat Rough", type: "float", defaultValue: 0 },
|
|
1097
|
+
{ id: "iridescence", name: "Iridescence", type: "float", defaultValue: 0 },
|
|
1098
|
+
// Exotic optics
|
|
1099
|
+
{ id: "sparkleIntensity", name: "Sparkle", type: "float", defaultValue: 0 },
|
|
1100
|
+
{ id: "sparkleDensity", name: "Sparkle Density", type: "float", defaultValue: 0 },
|
|
1101
|
+
{ id: "fluorescenceColor", name: "Fluorescence", type: "vec3", defaultValue: [0, 0, 0] },
|
|
1102
|
+
{ id: "fluorescenceIntensity", name: "Fluor Intensity", type: "float", defaultValue: 0 },
|
|
1103
|
+
{ id: "blackbodyTemp", name: "Blackbody K", type: "float", defaultValue: 0 },
|
|
1104
|
+
{ id: "retroreflection", name: "Retroreflect", type: "float", defaultValue: 0 }
|
|
1105
|
+
],
|
|
1106
|
+
outputs: [],
|
|
1107
|
+
generateCode: (_, inputs) => {
|
|
1108
|
+
return `SurfaceOutput(
|
|
1109
|
+
${inputs.baseColor},
|
|
1110
|
+
${inputs.metallic},
|
|
1111
|
+
${inputs.roughness},
|
|
1112
|
+
${inputs.normal},
|
|
1113
|
+
${inputs.emission},
|
|
1114
|
+
${inputs.alpha},
|
|
1115
|
+
${inputs.ao},
|
|
1116
|
+
${inputs.sheenColor},
|
|
1117
|
+
${inputs.sheenRoughness},
|
|
1118
|
+
${inputs.subsurfaceThickness},
|
|
1119
|
+
${inputs.subsurfaceColor},
|
|
1120
|
+
${inputs.anisotropy},
|
|
1121
|
+
${inputs.anisotropyRotation},
|
|
1122
|
+
${inputs.clearcoat},
|
|
1123
|
+
${inputs.clearcoatRoughness},
|
|
1124
|
+
${inputs.iridescence},
|
|
1125
|
+
${inputs.sparkleIntensity},
|
|
1126
|
+
${inputs.sparkleDensity},
|
|
1127
|
+
${inputs.fluorescenceColor},
|
|
1128
|
+
${inputs.fluorescenceIntensity},
|
|
1129
|
+
${inputs.blackbodyTemp},
|
|
1130
|
+
${inputs.retroreflection}
|
|
1131
|
+
)`;
|
|
1132
|
+
}
|
|
1133
|
+
},
|
|
1134
|
+
{
|
|
1135
|
+
type: "output_unlit",
|
|
1136
|
+
name: "Unlit Output",
|
|
1137
|
+
category: "output",
|
|
1138
|
+
description: "Unlit color output",
|
|
1139
|
+
inputs: [{ id: "color", name: "Color", type: "vec4", defaultValue: [1, 1, 1, 1] }],
|
|
1140
|
+
outputs: [],
|
|
1141
|
+
generateCode: (_, inputs) => `${inputs.color}`
|
|
1142
|
+
},
|
|
1143
|
+
{
|
|
1144
|
+
type: "output_vertex_offset",
|
|
1145
|
+
name: "Vertex Offset",
|
|
1146
|
+
category: "output",
|
|
1147
|
+
description: "Vertex position offset",
|
|
1148
|
+
inputs: [{ id: "offset", name: "Offset", type: "vec3", defaultValue: [0, 0, 0] }],
|
|
1149
|
+
outputs: [],
|
|
1150
|
+
generateCode: (_, inputs) => inputs.offset
|
|
1151
|
+
},
|
|
1152
|
+
{
|
|
1153
|
+
type: "output_volume",
|
|
1154
|
+
name: "Volume Output",
|
|
1155
|
+
category: "output",
|
|
1156
|
+
description: "Volumetric output \u2014 ray-marched density, color, emission, scattering",
|
|
1157
|
+
inputs: [
|
|
1158
|
+
{ id: "density", name: "Density", type: "float", defaultValue: 0.5 },
|
|
1159
|
+
{ id: "color", name: "Color", type: "vec3", defaultValue: [1, 1, 1] },
|
|
1160
|
+
{ id: "emission", name: "Emission", type: "vec3", defaultValue: [0, 0, 0] },
|
|
1161
|
+
{ id: "scattering", name: "Scattering", type: "float", defaultValue: 0.5 },
|
|
1162
|
+
{ id: "absorption", name: "Absorption", type: "float", defaultValue: 0.1 },
|
|
1163
|
+
{ id: "steps", name: "Steps", type: "float", defaultValue: 64 },
|
|
1164
|
+
{ id: "maxDistance", name: "Max Dist", type: "float", defaultValue: 10 }
|
|
1165
|
+
],
|
|
1166
|
+
outputs: [],
|
|
1167
|
+
generateCode: (_, inputs) => {
|
|
1168
|
+
return `VolumeOutput(
|
|
1169
|
+
${inputs.density},
|
|
1170
|
+
${inputs.color},
|
|
1171
|
+
${inputs.emission},
|
|
1172
|
+
${inputs.scattering},
|
|
1173
|
+
${inputs.absorption},
|
|
1174
|
+
${inputs.steps},
|
|
1175
|
+
${inputs.maxDistance}
|
|
1176
|
+
)`;
|
|
1177
|
+
}
|
|
1178
|
+
}
|
|
1179
|
+
];
|
|
1180
|
+
var SCREEN_SPACE_NODES = [
|
|
1181
|
+
{
|
|
1182
|
+
type: "caustics",
|
|
1183
|
+
name: "Caustics",
|
|
1184
|
+
category: "material",
|
|
1185
|
+
description: "Underwater caustic light patterns from refracted light",
|
|
1186
|
+
inputs: [
|
|
1187
|
+
{ id: "position", name: "Position", type: "vec3", defaultValue: [0, 0, 0] },
|
|
1188
|
+
{ id: "time", name: "Time", type: "float", defaultValue: 0 },
|
|
1189
|
+
{ id: "scale", name: "Scale", type: "float", defaultValue: 1 },
|
|
1190
|
+
{ id: "speed", name: "Speed", type: "float", defaultValue: 1 },
|
|
1191
|
+
{ id: "intensity", name: "Intensity", type: "float", defaultValue: 1 },
|
|
1192
|
+
{ id: "color", name: "Color", type: "vec3", defaultValue: [0.2, 0.5, 0.8] }
|
|
1193
|
+
],
|
|
1194
|
+
outputs: [
|
|
1195
|
+
{ id: "caustic", name: "Caustic", type: "vec3" },
|
|
1196
|
+
{ id: "mask", name: "Mask", type: "float" }
|
|
1197
|
+
],
|
|
1198
|
+
generateCode: (_, inputs) => `causticPattern(${inputs.position}, ${inputs.time}, ${inputs.scale}, ${inputs.speed}, ${inputs.intensity}, ${inputs.color})`
|
|
1199
|
+
},
|
|
1200
|
+
{
|
|
1201
|
+
type: "displacement_map",
|
|
1202
|
+
name: "Displacement Map",
|
|
1203
|
+
category: "material",
|
|
1204
|
+
description: "Vertex displacement from height/normal map",
|
|
1205
|
+
inputs: [
|
|
1206
|
+
{ id: "position", name: "Position", type: "vec3", defaultValue: [0, 0, 0] },
|
|
1207
|
+
{ id: "normal", name: "Normal", type: "vec3", defaultValue: [0, 1, 0] },
|
|
1208
|
+
{ id: "height", name: "Height", type: "float", defaultValue: 0 },
|
|
1209
|
+
{ id: "strength", name: "Strength", type: "float", defaultValue: 1 },
|
|
1210
|
+
{ id: "midLevel", name: "Mid Level", type: "float", defaultValue: 0.5 }
|
|
1211
|
+
],
|
|
1212
|
+
outputs: [{ id: "displaced", name: "Displaced", type: "vec3" }],
|
|
1213
|
+
generateCode: (_, inputs) => `displacementMap(${inputs.position}, ${inputs.normal}, ${inputs.height}, ${inputs.strength}, ${inputs.midLevel})`
|
|
1214
|
+
},
|
|
1215
|
+
{
|
|
1216
|
+
type: "parallax_occlusion",
|
|
1217
|
+
name: "Parallax Occlusion",
|
|
1218
|
+
category: "material",
|
|
1219
|
+
description: "Parallax occlusion mapping for depth illusion without extra geometry",
|
|
1220
|
+
inputs: [
|
|
1221
|
+
{ id: "uv", name: "UV", type: "vec2", defaultValue: [0, 0] },
|
|
1222
|
+
{ id: "viewDir", name: "View Dir", type: "vec3", defaultValue: [0, 0, 1] },
|
|
1223
|
+
{ id: "heightScale", name: "Height Scale", type: "float", defaultValue: 0.05 },
|
|
1224
|
+
{ id: "numLayers", name: "Layers", type: "float", defaultValue: 32 }
|
|
1225
|
+
],
|
|
1226
|
+
outputs: [
|
|
1227
|
+
{ id: "adjustedUV", name: "Adjusted UV", type: "vec2" },
|
|
1228
|
+
{ id: "depth", name: "Depth", type: "float" }
|
|
1229
|
+
],
|
|
1230
|
+
generateCode: (_, inputs) => `parallaxOcclusionMap(${inputs.uv}, ${inputs.viewDir}, ${inputs.heightScale}, ${inputs.numLayers})`
|
|
1231
|
+
},
|
|
1232
|
+
{
|
|
1233
|
+
type: "screen_space_reflection",
|
|
1234
|
+
name: "Screen Space Reflection",
|
|
1235
|
+
category: "material",
|
|
1236
|
+
description: "SSR \u2014 ray-marched reflections in screen space",
|
|
1237
|
+
inputs: [
|
|
1238
|
+
{ id: "uv", name: "UV", type: "vec2", defaultValue: [0, 0] },
|
|
1239
|
+
{ id: "normal", name: "Normal", type: "vec3", defaultValue: [0, 1, 0] },
|
|
1240
|
+
{ id: "viewDir", name: "View Dir", type: "vec3", defaultValue: [0, 0, 1] },
|
|
1241
|
+
{ id: "roughness", name: "Roughness", type: "float", defaultValue: 0.1 },
|
|
1242
|
+
{ id: "maxSteps", name: "Max Steps", type: "float", defaultValue: 64 },
|
|
1243
|
+
{ id: "stepSize", name: "Step Size", type: "float", defaultValue: 0.1 }
|
|
1244
|
+
],
|
|
1245
|
+
outputs: [
|
|
1246
|
+
{ id: "reflection", name: "Reflection", type: "vec3" },
|
|
1247
|
+
{ id: "hitMask", name: "Hit Mask", type: "float" }
|
|
1248
|
+
],
|
|
1249
|
+
generateCode: (_, inputs) => `screenSpaceReflect(${inputs.uv}, ${inputs.normal}, ${inputs.viewDir}, ${inputs.roughness}, ${inputs.maxSteps}, ${inputs.stepSize})`
|
|
1250
|
+
},
|
|
1251
|
+
{
|
|
1252
|
+
type: "screen_space_gi",
|
|
1253
|
+
name: "Screen Space GI",
|
|
1254
|
+
category: "material",
|
|
1255
|
+
description: "Approximate global illumination from screen-space data",
|
|
1256
|
+
inputs: [
|
|
1257
|
+
{ id: "uv", name: "UV", type: "vec2", defaultValue: [0, 0] },
|
|
1258
|
+
{ id: "normal", name: "Normal", type: "vec3", defaultValue: [0, 1, 0] },
|
|
1259
|
+
{ id: "radius", name: "Radius", type: "float", defaultValue: 1 },
|
|
1260
|
+
{ id: "samples", name: "Samples", type: "float", defaultValue: 16 },
|
|
1261
|
+
{ id: "bounceIntensity", name: "Bounce", type: "float", defaultValue: 1 }
|
|
1262
|
+
],
|
|
1263
|
+
outputs: [{ id: "indirectLight", name: "Indirect", type: "vec3" }],
|
|
1264
|
+
generateCode: (_, inputs) => `screenSpaceGI(${inputs.uv}, ${inputs.normal}, ${inputs.radius}, ${inputs.samples}, ${inputs.bounceIntensity})`
|
|
1265
|
+
},
|
|
1266
|
+
{
|
|
1267
|
+
type: "water_surface",
|
|
1268
|
+
name: "Water Surface",
|
|
1269
|
+
category: "material",
|
|
1270
|
+
description: "Combined water surface effect with waves, refraction, foam, and caustics",
|
|
1271
|
+
inputs: [
|
|
1272
|
+
{ id: "position", name: "Position", type: "vec3", defaultValue: [0, 0, 0] },
|
|
1273
|
+
{ id: "time", name: "Time", type: "float", defaultValue: 0 },
|
|
1274
|
+
{ id: "waveScale", name: "Wave Scale", type: "float", defaultValue: 1 },
|
|
1275
|
+
{ id: "waveSpeed", name: "Wave Speed", type: "float", defaultValue: 1 },
|
|
1276
|
+
{ id: "depth", name: "Water Depth", type: "float", defaultValue: 5 },
|
|
1277
|
+
{ id: "foamThreshold", name: "Foam Thresh", type: "float", defaultValue: 0.7 }
|
|
1278
|
+
],
|
|
1279
|
+
outputs: [
|
|
1280
|
+
{ id: "displacement", name: "Displacement", type: "vec3" },
|
|
1281
|
+
{ id: "normal", name: "Normal", type: "vec3" },
|
|
1282
|
+
{ id: "foam", name: "Foam", type: "float" },
|
|
1283
|
+
{ id: "caustic", name: "Caustic", type: "float" }
|
|
1284
|
+
],
|
|
1285
|
+
generateCode: (_, inputs) => `waterSurface(${inputs.position}, ${inputs.time}, ${inputs.waveScale}, ${inputs.waveSpeed}, ${inputs.depth}, ${inputs.foamThreshold})`
|
|
1286
|
+
},
|
|
1287
|
+
{
|
|
1288
|
+
type: "caustics_refractive",
|
|
1289
|
+
name: "Refractive Caustics",
|
|
1290
|
+
category: "material",
|
|
1291
|
+
description: "Chromatic refractive caustics with RGB dispersion for prismatic light patterns",
|
|
1292
|
+
inputs: [
|
|
1293
|
+
{ id: "position", name: "Position", type: "vec3", defaultValue: [0, 0, 0] },
|
|
1294
|
+
{ id: "time", name: "Time", type: "float", defaultValue: 0 },
|
|
1295
|
+
{ id: "scale", name: "Scale", type: "float", defaultValue: 1 },
|
|
1296
|
+
{ id: "speed", name: "Speed", type: "float", defaultValue: 1 },
|
|
1297
|
+
{ id: "intensity", name: "Intensity", type: "float", defaultValue: 1 },
|
|
1298
|
+
{ id: "dispersion", name: "Dispersion", type: "float", defaultValue: 0.05 }
|
|
1299
|
+
],
|
|
1300
|
+
outputs: [
|
|
1301
|
+
{ id: "caustic", name: "Caustic", type: "vec3" },
|
|
1302
|
+
{ id: "mask", name: "Mask", type: "float" }
|
|
1303
|
+
],
|
|
1304
|
+
generateCode: (_, inputs) => `causticChromatic(${inputs.position}, ${inputs.time}, ${inputs.scale}, ${inputs.speed}, ${inputs.intensity}, ${inputs.dispersion})`
|
|
1305
|
+
},
|
|
1306
|
+
{
|
|
1307
|
+
type: "displacement_enhanced",
|
|
1308
|
+
name: "Enhanced Displacement",
|
|
1309
|
+
category: "material",
|
|
1310
|
+
description: "Displacement with reconstructed normal and optional anisotropic weighting",
|
|
1311
|
+
inputs: [
|
|
1312
|
+
{ id: "position", name: "Position", type: "vec3", defaultValue: [0, 0, 0] },
|
|
1313
|
+
{ id: "normal", name: "Normal", type: "vec3", defaultValue: [0, 1, 0] },
|
|
1314
|
+
{ id: "tangent", name: "Tangent", type: "vec3", defaultValue: [1, 0, 0] },
|
|
1315
|
+
{ id: "height", name: "Height", type: "float", defaultValue: 0 },
|
|
1316
|
+
{ id: "strength", name: "Strength", type: "float", defaultValue: 1 },
|
|
1317
|
+
{ id: "anisotropy", name: "Anisotropy", type: "float", defaultValue: 0 },
|
|
1318
|
+
{ id: "midLevel", name: "Mid Level", type: "float", defaultValue: 0.5 }
|
|
1319
|
+
],
|
|
1320
|
+
outputs: [
|
|
1321
|
+
{ id: "displaced", name: "Displaced", type: "vec3" },
|
|
1322
|
+
{ id: "reconstructedNormal", name: "Normal", type: "vec3" }
|
|
1323
|
+
],
|
|
1324
|
+
generateCode: (_, inputs) => `anisotropicDisplacement(${inputs.position}, ${inputs.normal}, ${inputs.tangent}, ${inputs.height}, ${inputs.strength}, ${inputs.anisotropy}, ${inputs.midLevel})`
|
|
1325
|
+
},
|
|
1326
|
+
{
|
|
1327
|
+
type: "underwater_foam",
|
|
1328
|
+
name: "Underwater Foam",
|
|
1329
|
+
category: "material",
|
|
1330
|
+
description: "Procedural underwater foam patches driven by turbulence",
|
|
1331
|
+
inputs: [
|
|
1332
|
+
{ id: "position", name: "Position", type: "vec3", defaultValue: [0, 0, 0] },
|
|
1333
|
+
{ id: "time", name: "Time", type: "float", defaultValue: 0 },
|
|
1334
|
+
{ id: "scale", name: "Scale", type: "float", defaultValue: 1 },
|
|
1335
|
+
{ id: "threshold", name: "Threshold", type: "float", defaultValue: 0.6 }
|
|
1336
|
+
],
|
|
1337
|
+
outputs: [{ id: "foam", name: "Foam", type: "float" }],
|
|
1338
|
+
generateCode: (_, inputs) => `underwaterFoam(${inputs.position}, ${inputs.time}, ${inputs.scale}, ${inputs.threshold})`
|
|
1339
|
+
}
|
|
1340
|
+
];
|
|
1341
|
+
var ALL_NODE_TEMPLATES = [
|
|
1342
|
+
...INPUT_NODES,
|
|
1343
|
+
...MATH_NODES,
|
|
1344
|
+
...TRIG_NODES,
|
|
1345
|
+
...VECTOR_NODES,
|
|
1346
|
+
...COLOR_NODES,
|
|
1347
|
+
...TEXTURE_NODES,
|
|
1348
|
+
...UTILITY_NODES,
|
|
1349
|
+
...ADVANCED_MATERIAL_NODES,
|
|
1350
|
+
...VOLUMETRIC_NODES,
|
|
1351
|
+
...SCREEN_SPACE_NODES,
|
|
1352
|
+
...OUTPUT_NODES
|
|
1353
|
+
];
|
|
1354
|
+
function getNodeTemplate(type) {
|
|
1355
|
+
return ALL_NODE_TEMPLATES.find((t) => t.type === type);
|
|
1356
|
+
}
|
|
1357
|
+
|
|
1358
|
+
// src/shader/graph/ShaderGraph.ts
|
|
1359
|
+
var ShaderGraph = class _ShaderGraph {
|
|
1360
|
+
id;
|
|
1361
|
+
name;
|
|
1362
|
+
description;
|
|
1363
|
+
nodes;
|
|
1364
|
+
connections;
|
|
1365
|
+
version = "1.0.0";
|
|
1366
|
+
metadata;
|
|
1367
|
+
nodeCounter = 0;
|
|
1368
|
+
connectionCounter = 0;
|
|
1369
|
+
constructor(name, id) {
|
|
1370
|
+
this.id = id ?? this.generateId("graph");
|
|
1371
|
+
this.name = name ?? "Untitled Shader";
|
|
1372
|
+
this.nodes = /* @__PURE__ */ new Map();
|
|
1373
|
+
this.connections = [];
|
|
1374
|
+
}
|
|
1375
|
+
/**
|
|
1376
|
+
* Generate unique ID
|
|
1377
|
+
*/
|
|
1378
|
+
generateId(prefix) {
|
|
1379
|
+
return `${prefix}_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;
|
|
1380
|
+
}
|
|
1381
|
+
/**
|
|
1382
|
+
* Create a node from a template
|
|
1383
|
+
*/
|
|
1384
|
+
createNode(type, position = { x: 0, y: 0 }) {
|
|
1385
|
+
const template = getNodeTemplate(type);
|
|
1386
|
+
if (!template) {
|
|
1387
|
+
console.warn(`Unknown node type: ${type}`);
|
|
1388
|
+
return null;
|
|
1389
|
+
}
|
|
1390
|
+
const nodeId = `node_${++this.nodeCounter}`;
|
|
1391
|
+
const node = {
|
|
1392
|
+
id: nodeId,
|
|
1393
|
+
type: template.type,
|
|
1394
|
+
name: template.name,
|
|
1395
|
+
category: template.category,
|
|
1396
|
+
inputs: template.inputs.map((input) => ({
|
|
1397
|
+
...input,
|
|
1398
|
+
direction: "input"
|
|
1399
|
+
})),
|
|
1400
|
+
outputs: template.outputs.map((output) => ({
|
|
1401
|
+
...output,
|
|
1402
|
+
direction: "output"
|
|
1403
|
+
})),
|
|
1404
|
+
position,
|
|
1405
|
+
properties: template.defaultProperties ? { ...template.defaultProperties } : void 0
|
|
1406
|
+
};
|
|
1407
|
+
this.nodes.set(nodeId, node);
|
|
1408
|
+
return node;
|
|
1409
|
+
}
|
|
1410
|
+
/**
|
|
1411
|
+
* Add a custom node
|
|
1412
|
+
*/
|
|
1413
|
+
addCustomNode(node) {
|
|
1414
|
+
const nodeId = `node_${++this.nodeCounter}`;
|
|
1415
|
+
const fullNode = {
|
|
1416
|
+
...node,
|
|
1417
|
+
id: nodeId
|
|
1418
|
+
};
|
|
1419
|
+
this.nodes.set(nodeId, fullNode);
|
|
1420
|
+
return fullNode;
|
|
1421
|
+
}
|
|
1422
|
+
/**
|
|
1423
|
+
* Get a node by ID
|
|
1424
|
+
*/
|
|
1425
|
+
getNode(nodeId) {
|
|
1426
|
+
return this.nodes.get(nodeId);
|
|
1427
|
+
}
|
|
1428
|
+
/**
|
|
1429
|
+
* Remove a node by ID
|
|
1430
|
+
*/
|
|
1431
|
+
removeNode(nodeId) {
|
|
1432
|
+
const node = this.nodes.get(nodeId);
|
|
1433
|
+
if (!node) return false;
|
|
1434
|
+
this.connections = this.connections.filter(
|
|
1435
|
+
(conn) => conn.fromNode !== nodeId && conn.toNode !== nodeId
|
|
1436
|
+
);
|
|
1437
|
+
return this.nodes.delete(nodeId);
|
|
1438
|
+
}
|
|
1439
|
+
/**
|
|
1440
|
+
* Update node position
|
|
1441
|
+
*/
|
|
1442
|
+
setNodePosition(nodeId, x, y) {
|
|
1443
|
+
const node = this.nodes.get(nodeId);
|
|
1444
|
+
if (!node) return false;
|
|
1445
|
+
node.position = { x, y };
|
|
1446
|
+
return true;
|
|
1447
|
+
}
|
|
1448
|
+
/**
|
|
1449
|
+
* Update node property
|
|
1450
|
+
*/
|
|
1451
|
+
setNodeProperty(nodeId, key, value) {
|
|
1452
|
+
const node = this.nodes.get(nodeId);
|
|
1453
|
+
if (!node) return false;
|
|
1454
|
+
if (!node.properties) node.properties = {};
|
|
1455
|
+
node.properties[key] = value;
|
|
1456
|
+
return true;
|
|
1457
|
+
}
|
|
1458
|
+
/**
|
|
1459
|
+
* Get node property
|
|
1460
|
+
*/
|
|
1461
|
+
getNodeProperty(nodeId, key) {
|
|
1462
|
+
const node = this.nodes.get(nodeId);
|
|
1463
|
+
return node?.properties?.[key];
|
|
1464
|
+
}
|
|
1465
|
+
/**
|
|
1466
|
+
* Connect two ports
|
|
1467
|
+
*/
|
|
1468
|
+
connect(fromNodeId, fromPortId, toNodeId, toPortId) {
|
|
1469
|
+
const fromNode = this.nodes.get(fromNodeId);
|
|
1470
|
+
const toNode = this.nodes.get(toNodeId);
|
|
1471
|
+
if (!fromNode || !toNode) {
|
|
1472
|
+
console.warn("Invalid node IDs for connection");
|
|
1473
|
+
return null;
|
|
1474
|
+
}
|
|
1475
|
+
const fromPort = fromNode.outputs.find((p) => p.id === fromPortId);
|
|
1476
|
+
const toPort = toNode.inputs.find((p) => p.id === toPortId);
|
|
1477
|
+
if (!fromPort || !toPort) {
|
|
1478
|
+
console.warn("Invalid port IDs for connection");
|
|
1479
|
+
return null;
|
|
1480
|
+
}
|
|
1481
|
+
if (!areTypesCompatible(fromPort.type, toPort.type)) {
|
|
1482
|
+
console.warn(`Type mismatch: ${fromPort.type} -> ${toPort.type}`);
|
|
1483
|
+
return null;
|
|
1484
|
+
}
|
|
1485
|
+
const existingIdx = this.connections.findIndex(
|
|
1486
|
+
(c) => c.toNode === toNodeId && c.toPort === toPortId
|
|
1487
|
+
);
|
|
1488
|
+
if (existingIdx >= 0) {
|
|
1489
|
+
const existing = this.connections[existingIdx];
|
|
1490
|
+
this.disconnectPort(existing.toNode, existing.toPort);
|
|
1491
|
+
this.connections.splice(existingIdx, 1);
|
|
1492
|
+
}
|
|
1493
|
+
if (this.wouldCreateCycle(fromNodeId, toNodeId)) {
|
|
1494
|
+
console.warn("Connection would create a cycle");
|
|
1495
|
+
return null;
|
|
1496
|
+
}
|
|
1497
|
+
const connection = {
|
|
1498
|
+
id: `conn_${++this.connectionCounter}`,
|
|
1499
|
+
fromNode: fromNodeId,
|
|
1500
|
+
fromPort: fromPortId,
|
|
1501
|
+
toNode: toNodeId,
|
|
1502
|
+
toPort: toPortId
|
|
1503
|
+
};
|
|
1504
|
+
this.connections.push(connection);
|
|
1505
|
+
toPort.connected = {
|
|
1506
|
+
nodeId: fromNodeId,
|
|
1507
|
+
portId: fromPortId
|
|
1508
|
+
};
|
|
1509
|
+
return connection;
|
|
1510
|
+
}
|
|
1511
|
+
/**
|
|
1512
|
+
* Disconnect a port
|
|
1513
|
+
*/
|
|
1514
|
+
disconnectPort(nodeId, portId) {
|
|
1515
|
+
const node = this.nodes.get(nodeId);
|
|
1516
|
+
if (!node) return false;
|
|
1517
|
+
const inputPort = node.inputs.find((p) => p.id === portId);
|
|
1518
|
+
if (inputPort) {
|
|
1519
|
+
inputPort.connected = void 0;
|
|
1520
|
+
}
|
|
1521
|
+
const initialLength = this.connections.length;
|
|
1522
|
+
this.connections = this.connections.filter(
|
|
1523
|
+
(c) => !(c.toNode === nodeId && c.toPort === portId) && !(c.fromNode === nodeId && c.fromPort === portId)
|
|
1524
|
+
);
|
|
1525
|
+
return this.connections.length !== initialLength;
|
|
1526
|
+
}
|
|
1527
|
+
/**
|
|
1528
|
+
* Get all connections for a node
|
|
1529
|
+
*/
|
|
1530
|
+
getNodeConnections(nodeId) {
|
|
1531
|
+
return this.connections.filter((c) => c.fromNode === nodeId || c.toNode === nodeId);
|
|
1532
|
+
}
|
|
1533
|
+
/**
|
|
1534
|
+
* Get input connections for a node
|
|
1535
|
+
*/
|
|
1536
|
+
getInputConnections(nodeId) {
|
|
1537
|
+
return this.connections.filter((c) => c.toNode === nodeId);
|
|
1538
|
+
}
|
|
1539
|
+
/**
|
|
1540
|
+
* Get output connections for a node
|
|
1541
|
+
*/
|
|
1542
|
+
getOutputConnections(nodeId) {
|
|
1543
|
+
return this.connections.filter((c) => c.fromNode === nodeId);
|
|
1544
|
+
}
|
|
1545
|
+
/**
|
|
1546
|
+
* Check if connection would create a cycle
|
|
1547
|
+
*/
|
|
1548
|
+
wouldCreateCycle(fromNodeId, toNodeId) {
|
|
1549
|
+
if (fromNodeId === toNodeId) return true;
|
|
1550
|
+
const visited = /* @__PURE__ */ new Set();
|
|
1551
|
+
const queue = [toNodeId];
|
|
1552
|
+
while (queue.length > 0) {
|
|
1553
|
+
const current = queue.shift();
|
|
1554
|
+
if (current === fromNodeId) return true;
|
|
1555
|
+
if (visited.has(current)) continue;
|
|
1556
|
+
visited.add(current);
|
|
1557
|
+
const outputConnections = this.getOutputConnections(current);
|
|
1558
|
+
for (const conn of outputConnections) {
|
|
1559
|
+
queue.push(conn.toNode);
|
|
1560
|
+
}
|
|
1561
|
+
}
|
|
1562
|
+
return false;
|
|
1563
|
+
}
|
|
1564
|
+
/**
|
|
1565
|
+
* Get nodes by category
|
|
1566
|
+
*/
|
|
1567
|
+
getNodesByCategory(category) {
|
|
1568
|
+
return Array.from(this.nodes.values()).filter((n) => n.category === category);
|
|
1569
|
+
}
|
|
1570
|
+
/**
|
|
1571
|
+
* Get all output nodes
|
|
1572
|
+
*/
|
|
1573
|
+
getOutputNodes() {
|
|
1574
|
+
return this.getNodesByCategory("output");
|
|
1575
|
+
}
|
|
1576
|
+
/**
|
|
1577
|
+
* Get topologically sorted nodes (dependency order)
|
|
1578
|
+
*/
|
|
1579
|
+
getTopologicalOrder() {
|
|
1580
|
+
const sorted = [];
|
|
1581
|
+
const visited = /* @__PURE__ */ new Set();
|
|
1582
|
+
const visiting = /* @__PURE__ */ new Set();
|
|
1583
|
+
const visit = (nodeId) => {
|
|
1584
|
+
if (visited.has(nodeId)) return true;
|
|
1585
|
+
if (visiting.has(nodeId)) return false;
|
|
1586
|
+
visiting.add(nodeId);
|
|
1587
|
+
const node = this.nodes.get(nodeId);
|
|
1588
|
+
if (!node) return true;
|
|
1589
|
+
const inputConns = this.getInputConnections(nodeId);
|
|
1590
|
+
for (const conn of inputConns) {
|
|
1591
|
+
if (!visit(conn.fromNode)) return false;
|
|
1592
|
+
}
|
|
1593
|
+
visiting.delete(nodeId);
|
|
1594
|
+
visited.add(nodeId);
|
|
1595
|
+
sorted.push(node);
|
|
1596
|
+
return true;
|
|
1597
|
+
};
|
|
1598
|
+
const outputNodes = this.getOutputNodes();
|
|
1599
|
+
for (const output of outputNodes) {
|
|
1600
|
+
visit(output.id);
|
|
1601
|
+
}
|
|
1602
|
+
for (const node of this.nodes.values()) {
|
|
1603
|
+
visit(node.id);
|
|
1604
|
+
}
|
|
1605
|
+
return sorted;
|
|
1606
|
+
}
|
|
1607
|
+
/**
|
|
1608
|
+
* Validate the graph
|
|
1609
|
+
*/
|
|
1610
|
+
validate() {
|
|
1611
|
+
const errors = [];
|
|
1612
|
+
const warnings = [];
|
|
1613
|
+
const outputNodes = this.getOutputNodes();
|
|
1614
|
+
if (outputNodes.length === 0) {
|
|
1615
|
+
errors.push("Graph has no output nodes");
|
|
1616
|
+
}
|
|
1617
|
+
for (const node of this.nodes.values()) {
|
|
1618
|
+
for (const input of node.inputs) {
|
|
1619
|
+
if (input.required && !input.connected && input.defaultValue === void 0) {
|
|
1620
|
+
errors.push(
|
|
1621
|
+
`Node "${node.name}" (${node.id}): Required input "${input.name}" is not connected`
|
|
1622
|
+
);
|
|
1623
|
+
}
|
|
1624
|
+
}
|
|
1625
|
+
}
|
|
1626
|
+
for (const node of this.nodes.values()) {
|
|
1627
|
+
if (node.category !== "output") {
|
|
1628
|
+
const connections = this.getNodeConnections(node.id);
|
|
1629
|
+
if (connections.length === 0) {
|
|
1630
|
+
warnings.push(`Node "${node.name}" (${node.id}) is not connected to anything`);
|
|
1631
|
+
}
|
|
1632
|
+
}
|
|
1633
|
+
}
|
|
1634
|
+
return {
|
|
1635
|
+
valid: errors.length === 0,
|
|
1636
|
+
errors,
|
|
1637
|
+
warnings
|
|
1638
|
+
};
|
|
1639
|
+
}
|
|
1640
|
+
/**
|
|
1641
|
+
* Clear the graph
|
|
1642
|
+
*/
|
|
1643
|
+
clear() {
|
|
1644
|
+
this.nodes.clear();
|
|
1645
|
+
this.connections = [];
|
|
1646
|
+
this.nodeCounter = 0;
|
|
1647
|
+
this.connectionCounter = 0;
|
|
1648
|
+
}
|
|
1649
|
+
/**
|
|
1650
|
+
* Duplicate a node
|
|
1651
|
+
*/
|
|
1652
|
+
duplicateNode(nodeId, offset = { x: 50, y: 50 }) {
|
|
1653
|
+
const original = this.nodes.get(nodeId);
|
|
1654
|
+
if (!original) return null;
|
|
1655
|
+
const duplicate = {
|
|
1656
|
+
type: original.type,
|
|
1657
|
+
name: original.name,
|
|
1658
|
+
category: original.category,
|
|
1659
|
+
inputs: original.inputs.map((i) => ({ ...i, connected: void 0 })),
|
|
1660
|
+
outputs: original.outputs.map((o) => ({ ...o })),
|
|
1661
|
+
position: {
|
|
1662
|
+
x: original.position.x + offset.x,
|
|
1663
|
+
y: original.position.y + offset.y
|
|
1664
|
+
},
|
|
1665
|
+
properties: original.properties ? { ...original.properties } : void 0,
|
|
1666
|
+
preview: original.preview
|
|
1667
|
+
};
|
|
1668
|
+
return this.addCustomNode(duplicate);
|
|
1669
|
+
}
|
|
1670
|
+
/**
|
|
1671
|
+
* Get available node templates
|
|
1672
|
+
*/
|
|
1673
|
+
static getAvailableNodeTemplates() {
|
|
1674
|
+
return ALL_NODE_TEMPLATES;
|
|
1675
|
+
}
|
|
1676
|
+
/**
|
|
1677
|
+
* Get node templates by category
|
|
1678
|
+
*/
|
|
1679
|
+
static getNodeTemplatesByCategory(category) {
|
|
1680
|
+
return ALL_NODE_TEMPLATES.filter((t) => t.category === category);
|
|
1681
|
+
}
|
|
1682
|
+
/**
|
|
1683
|
+
* Serialize to JSON
|
|
1684
|
+
*/
|
|
1685
|
+
toJSON() {
|
|
1686
|
+
return {
|
|
1687
|
+
id: this.id,
|
|
1688
|
+
name: this.name,
|
|
1689
|
+
description: this.description,
|
|
1690
|
+
version: this.version,
|
|
1691
|
+
metadata: this.metadata,
|
|
1692
|
+
nodes: Array.from(this.nodes.entries()).map(([id, node]) => {
|
|
1693
|
+
const { id: _nodeId, ...rest } = node;
|
|
1694
|
+
return {
|
|
1695
|
+
id,
|
|
1696
|
+
...rest
|
|
1697
|
+
};
|
|
1698
|
+
}),
|
|
1699
|
+
connections: this.connections
|
|
1700
|
+
};
|
|
1701
|
+
}
|
|
1702
|
+
/**
|
|
1703
|
+
* Deserialize from JSON
|
|
1704
|
+
*/
|
|
1705
|
+
static fromJSON(json) {
|
|
1706
|
+
const graph = new _ShaderGraph(json.name, json.id);
|
|
1707
|
+
graph.description = json.description;
|
|
1708
|
+
graph.version = json.version ?? "1.0.0";
|
|
1709
|
+
graph.metadata = json.metadata;
|
|
1710
|
+
for (const nodeData of json.nodes) {
|
|
1711
|
+
graph.nodes.set(nodeData.id, nodeData);
|
|
1712
|
+
}
|
|
1713
|
+
graph.connections = json.connections ?? [];
|
|
1714
|
+
return graph;
|
|
1715
|
+
}
|
|
1716
|
+
/**
|
|
1717
|
+
* Serialize to JSON string
|
|
1718
|
+
*/
|
|
1719
|
+
serialize() {
|
|
1720
|
+
return JSON.stringify(this.toJSON(), null, 2);
|
|
1721
|
+
}
|
|
1722
|
+
/**
|
|
1723
|
+
* Deserialize from JSON string
|
|
1724
|
+
*/
|
|
1725
|
+
static deserialize(json) {
|
|
1726
|
+
return _ShaderGraph.fromJSON(JSON.parse(json));
|
|
1727
|
+
}
|
|
1728
|
+
};
|
|
1729
|
+
|
|
1730
|
+
// src/shader/graph/ShaderGraphCompiler.ts
|
|
1731
|
+
var ShaderGraphCompiler = class {
|
|
1732
|
+
graph;
|
|
1733
|
+
options;
|
|
1734
|
+
variableCounter = 0;
|
|
1735
|
+
nodeVariables = /* @__PURE__ */ new Map();
|
|
1736
|
+
uniforms = [];
|
|
1737
|
+
textures = [];
|
|
1738
|
+
warnings = [];
|
|
1739
|
+
errors = [];
|
|
1740
|
+
constructor(graph, options) {
|
|
1741
|
+
this.graph = graph;
|
|
1742
|
+
this.options = {
|
|
1743
|
+
target: options?.target ?? "wgsl",
|
|
1744
|
+
optimize: options?.optimize ?? true,
|
|
1745
|
+
debug: options?.debug ?? false,
|
|
1746
|
+
defines: options?.defines ?? {}
|
|
1747
|
+
};
|
|
1748
|
+
}
|
|
1749
|
+
/**
|
|
1750
|
+
* Compile the shader graph
|
|
1751
|
+
*/
|
|
1752
|
+
compile() {
|
|
1753
|
+
this.reset();
|
|
1754
|
+
const validation = this.graph.validate();
|
|
1755
|
+
if (!validation.valid) {
|
|
1756
|
+
return {
|
|
1757
|
+
vertexCode: "",
|
|
1758
|
+
fragmentCode: "",
|
|
1759
|
+
uniforms: [],
|
|
1760
|
+
textures: [],
|
|
1761
|
+
warnings: validation.warnings,
|
|
1762
|
+
errors: validation.errors
|
|
1763
|
+
};
|
|
1764
|
+
}
|
|
1765
|
+
this.warnings = validation.warnings;
|
|
1766
|
+
const sortedNodes = this.graph.getTopologicalOrder();
|
|
1767
|
+
const fragmentBody = this.generateFragmentBody(sortedNodes);
|
|
1768
|
+
const vertexBody = this.generateVertexBody(sortedNodes);
|
|
1769
|
+
const vertexCode = this.generateVertexShader(vertexBody);
|
|
1770
|
+
const fragmentCode = this.generateFragmentShader(fragmentBody);
|
|
1771
|
+
return {
|
|
1772
|
+
vertexCode,
|
|
1773
|
+
fragmentCode,
|
|
1774
|
+
uniforms: this.uniforms,
|
|
1775
|
+
textures: this.textures,
|
|
1776
|
+
warnings: this.warnings,
|
|
1777
|
+
errors: this.errors
|
|
1778
|
+
};
|
|
1779
|
+
}
|
|
1780
|
+
/**
|
|
1781
|
+
* Reset compilation state
|
|
1782
|
+
*/
|
|
1783
|
+
reset() {
|
|
1784
|
+
this.variableCounter = 0;
|
|
1785
|
+
this.nodeVariables.clear();
|
|
1786
|
+
this.uniforms = [];
|
|
1787
|
+
this.textures = [];
|
|
1788
|
+
this.warnings = [];
|
|
1789
|
+
this.errors = [];
|
|
1790
|
+
}
|
|
1791
|
+
/**
|
|
1792
|
+
* Generate a unique variable name
|
|
1793
|
+
*/
|
|
1794
|
+
generateVariable() {
|
|
1795
|
+
return `v_${this.variableCounter++}`;
|
|
1796
|
+
}
|
|
1797
|
+
/**
|
|
1798
|
+
* Get or create variable for a node output
|
|
1799
|
+
*/
|
|
1800
|
+
getOutputVariable(nodeId, portId) {
|
|
1801
|
+
if (!this.nodeVariables.has(nodeId)) {
|
|
1802
|
+
this.nodeVariables.set(nodeId, /* @__PURE__ */ new Map());
|
|
1803
|
+
}
|
|
1804
|
+
const nodeVars = this.nodeVariables.get(nodeId);
|
|
1805
|
+
if (!nodeVars.has(portId)) {
|
|
1806
|
+
nodeVars.set(portId, this.generateVariable());
|
|
1807
|
+
}
|
|
1808
|
+
return nodeVars.get(portId);
|
|
1809
|
+
}
|
|
1810
|
+
/**
|
|
1811
|
+
* Get input expression for a node input
|
|
1812
|
+
*/
|
|
1813
|
+
getInputExpression(node, portId, connections) {
|
|
1814
|
+
const port = node.inputs.find((p) => p.id === portId);
|
|
1815
|
+
if (!port) {
|
|
1816
|
+
return "0.0";
|
|
1817
|
+
}
|
|
1818
|
+
const connection = connections.find((c) => c.toNode === node.id && c.toPort === portId);
|
|
1819
|
+
if (connection) {
|
|
1820
|
+
const sourceNode = this.graph.getNode(connection.fromNode);
|
|
1821
|
+
const sourcePort = sourceNode?.outputs.find((p) => p.id === connection.fromPort);
|
|
1822
|
+
if (sourceNode && sourcePort) {
|
|
1823
|
+
const varName = this.getOutputVariable(connection.fromNode, connection.fromPort);
|
|
1824
|
+
if (sourcePort.type !== port.type) {
|
|
1825
|
+
return getTypeConversion(sourcePort.type, port.type, varName);
|
|
1826
|
+
}
|
|
1827
|
+
return varName;
|
|
1828
|
+
}
|
|
1829
|
+
}
|
|
1830
|
+
return this.getDefaultValueCode(port.type, port.defaultValue);
|
|
1831
|
+
}
|
|
1832
|
+
/**
|
|
1833
|
+
* Get WGSL code for default value
|
|
1834
|
+
*/
|
|
1835
|
+
getDefaultValueCode(type, value) {
|
|
1836
|
+
if (value === void 0) {
|
|
1837
|
+
return this.getZeroValue(type);
|
|
1838
|
+
}
|
|
1839
|
+
if (typeof value === "number") {
|
|
1840
|
+
if (type === "float") return `${value}`;
|
|
1841
|
+
if (type === "int") return `${Math.floor(value)}i`;
|
|
1842
|
+
return `${value}`;
|
|
1843
|
+
}
|
|
1844
|
+
if (Array.isArray(value)) {
|
|
1845
|
+
switch (type) {
|
|
1846
|
+
case "vec2":
|
|
1847
|
+
return `vec2<f32>(${value[0] ?? 0}, ${value[1] ?? 0})`;
|
|
1848
|
+
case "vec3":
|
|
1849
|
+
return `vec3<f32>(${value[0] ?? 0}, ${value[1] ?? 0}, ${value[2] ?? 0})`;
|
|
1850
|
+
case "vec4":
|
|
1851
|
+
return `vec4<f32>(${value[0] ?? 0}, ${value[1] ?? 0}, ${value[2] ?? 0}, ${value[3] ?? 1})`;
|
|
1852
|
+
default:
|
|
1853
|
+
return this.getZeroValue(type);
|
|
1854
|
+
}
|
|
1855
|
+
}
|
|
1856
|
+
return this.getZeroValue(type);
|
|
1857
|
+
}
|
|
1858
|
+
/**
|
|
1859
|
+
* Get zero value for a type
|
|
1860
|
+
*/
|
|
1861
|
+
getZeroValue(type) {
|
|
1862
|
+
switch (type) {
|
|
1863
|
+
case "float":
|
|
1864
|
+
return "0.0";
|
|
1865
|
+
case "vec2":
|
|
1866
|
+
return "vec2<f32>(0.0)";
|
|
1867
|
+
case "vec3":
|
|
1868
|
+
return "vec3<f32>(0.0)";
|
|
1869
|
+
case "vec4":
|
|
1870
|
+
return "vec4<f32>(0.0, 0.0, 0.0, 1.0)";
|
|
1871
|
+
case "int":
|
|
1872
|
+
return "0i";
|
|
1873
|
+
case "ivec2":
|
|
1874
|
+
return "vec2<i32>(0)";
|
|
1875
|
+
case "ivec3":
|
|
1876
|
+
return "vec3<i32>(0)";
|
|
1877
|
+
case "ivec4":
|
|
1878
|
+
return "vec4<i32>(0)";
|
|
1879
|
+
case "bool":
|
|
1880
|
+
return "false";
|
|
1881
|
+
case "mat2":
|
|
1882
|
+
return "mat2x2<f32>(1.0, 0.0, 0.0, 1.0)";
|
|
1883
|
+
case "mat3":
|
|
1884
|
+
return "mat3x3<f32>(1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0)";
|
|
1885
|
+
case "mat4":
|
|
1886
|
+
return "mat4x4<f32>(1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0)";
|
|
1887
|
+
default:
|
|
1888
|
+
return "0.0";
|
|
1889
|
+
}
|
|
1890
|
+
}
|
|
1891
|
+
/**
|
|
1892
|
+
* Get WGSL type name
|
|
1893
|
+
*/
|
|
1894
|
+
getWGSLType(type) {
|
|
1895
|
+
switch (type) {
|
|
1896
|
+
case "float":
|
|
1897
|
+
return "f32";
|
|
1898
|
+
case "vec2":
|
|
1899
|
+
return "vec2<f32>";
|
|
1900
|
+
case "vec3":
|
|
1901
|
+
return "vec3<f32>";
|
|
1902
|
+
case "vec4":
|
|
1903
|
+
return "vec4<f32>";
|
|
1904
|
+
case "int":
|
|
1905
|
+
return "i32";
|
|
1906
|
+
case "ivec2":
|
|
1907
|
+
return "vec2<i32>";
|
|
1908
|
+
case "ivec3":
|
|
1909
|
+
return "vec3<i32>";
|
|
1910
|
+
case "ivec4":
|
|
1911
|
+
return "vec4<i32>";
|
|
1912
|
+
case "bool":
|
|
1913
|
+
return "bool";
|
|
1914
|
+
case "mat2":
|
|
1915
|
+
return "mat2x2<f32>";
|
|
1916
|
+
case "mat3":
|
|
1917
|
+
return "mat3x3<f32>";
|
|
1918
|
+
case "mat4":
|
|
1919
|
+
return "mat4x4<f32>";
|
|
1920
|
+
default:
|
|
1921
|
+
return "f32";
|
|
1922
|
+
}
|
|
1923
|
+
}
|
|
1924
|
+
/**
|
|
1925
|
+
* Generate fragment shader body
|
|
1926
|
+
*/
|
|
1927
|
+
generateFragmentBody(nodes) {
|
|
1928
|
+
const lines = [];
|
|
1929
|
+
const connections = this.graph.connections;
|
|
1930
|
+
for (const node of nodes) {
|
|
1931
|
+
if (node.category === "output" && node.type !== "output_surface" && node.type !== "output_unlit" && node.type !== "output_volume") {
|
|
1932
|
+
continue;
|
|
1933
|
+
}
|
|
1934
|
+
const template = getNodeTemplate(node.type);
|
|
1935
|
+
if (!template) {
|
|
1936
|
+
this.warnings.push(`No template found for node type: ${node.type}`);
|
|
1937
|
+
continue;
|
|
1938
|
+
}
|
|
1939
|
+
const inputs = {};
|
|
1940
|
+
for (const port of node.inputs) {
|
|
1941
|
+
inputs[port.id] = this.getInputExpression(node, port.id, connections);
|
|
1942
|
+
}
|
|
1943
|
+
const code = template.generateCode(node, inputs);
|
|
1944
|
+
if (node.type === "output_surface") {
|
|
1945
|
+
lines.push(`// Surface output`);
|
|
1946
|
+
lines.push(`let baseColor = ${inputs.baseColor};`);
|
|
1947
|
+
lines.push(`let metallic = ${inputs.metallic};`);
|
|
1948
|
+
lines.push(`let roughness = ${inputs.roughness};`);
|
|
1949
|
+
lines.push(`let normal = ${inputs.normal};`);
|
|
1950
|
+
lines.push(`let emission = ${inputs.emission};`);
|
|
1951
|
+
lines.push(`let alpha = ${inputs.alpha};`);
|
|
1952
|
+
lines.push(`let ao = ${inputs.ao};`);
|
|
1953
|
+
lines.push(`let sheenColor = ${inputs.sheenColor ?? "vec3<f32>(0.0)"};`);
|
|
1954
|
+
lines.push(`let sheenRoughness = ${inputs.sheenRoughness ?? "0.0"};`);
|
|
1955
|
+
lines.push(`let subsurfaceThickness = ${inputs.subsurfaceThickness ?? "0.0"};`);
|
|
1956
|
+
lines.push(`let subsurfaceColor = ${inputs.subsurfaceColor ?? "vec3<f32>(0.0)"};`);
|
|
1957
|
+
lines.push(`let anisotropyVal = ${inputs.anisotropy ?? "0.0"};`);
|
|
1958
|
+
lines.push(`let anisotropyRotation = ${inputs.anisotropyRotation ?? "0.0"};`);
|
|
1959
|
+
lines.push(`let clearcoatVal = ${inputs.clearcoat ?? "0.0"};`);
|
|
1960
|
+
lines.push(`let clearcoatRoughnessVal = ${inputs.clearcoatRoughness ?? "0.0"};`);
|
|
1961
|
+
lines.push(`let iridescenceVal = ${inputs.iridescence ?? "0.0"};`);
|
|
1962
|
+
lines.push(`let sparkleIntensity = ${inputs.sparkleIntensity ?? "0.0"};`);
|
|
1963
|
+
lines.push(`let sparkleDensity = ${inputs.sparkleDensity ?? "0.0"};`);
|
|
1964
|
+
lines.push(`let fluorescenceColor = ${inputs.fluorescenceColor ?? "vec3<f32>(0.0)"};`);
|
|
1965
|
+
lines.push(`let fluorescenceIntensity = ${inputs.fluorescenceIntensity ?? "0.0"};`);
|
|
1966
|
+
lines.push(`let blackbodyTemp = ${inputs.blackbodyTemp ?? "0.0"};`);
|
|
1967
|
+
lines.push(`let retroreflectionVal = ${inputs.retroreflection ?? "0.0"};`);
|
|
1968
|
+
} else if (node.type === "output_volume") {
|
|
1969
|
+
lines.push(`// Volumetric output \u2014 ray march`);
|
|
1970
|
+
lines.push(`let volDensity = ${inputs.density ?? "0.5"};`);
|
|
1971
|
+
lines.push(`let volColor = ${inputs.color ?? "vec3<f32>(1.0)"};`);
|
|
1972
|
+
lines.push(`let volEmission = ${inputs.emission ?? "vec3<f32>(0.0)"};`);
|
|
1973
|
+
lines.push(`let volScattering = ${inputs.scattering ?? "0.5"};`);
|
|
1974
|
+
lines.push(`let volAbsorption = ${inputs.absorption ?? "0.1"};`);
|
|
1975
|
+
lines.push(`let volSteps = ${inputs.steps ?? "64.0"};`);
|
|
1976
|
+
lines.push(`let volMaxDist = ${inputs.maxDistance ?? "10.0"};`);
|
|
1977
|
+
} else if (node.type === "output_unlit") {
|
|
1978
|
+
lines.push(`// Unlit output`);
|
|
1979
|
+
lines.push(`let finalColor = ${inputs.color};`);
|
|
1980
|
+
} else if (node.outputs.length > 0) {
|
|
1981
|
+
if (this.options.debug) {
|
|
1982
|
+
lines.push(`// ${node.name} (${node.id})`);
|
|
1983
|
+
}
|
|
1984
|
+
if (node.outputs.length === 1) {
|
|
1985
|
+
const varName = this.getOutputVariable(node.id, node.outputs[0].id);
|
|
1986
|
+
const wgslType = this.getWGSLType(node.outputs[0].type);
|
|
1987
|
+
lines.push(`let ${varName}: ${wgslType} = ${code};`);
|
|
1988
|
+
} else {
|
|
1989
|
+
this.generateMultiOutputCode(node, inputs, lines);
|
|
1990
|
+
}
|
|
1991
|
+
}
|
|
1992
|
+
}
|
|
1993
|
+
return lines.join("\n ");
|
|
1994
|
+
}
|
|
1995
|
+
/**
|
|
1996
|
+
* Generate code for nodes with multiple outputs
|
|
1997
|
+
*/
|
|
1998
|
+
generateMultiOutputCode(node, inputs, lines) {
|
|
1999
|
+
const template = getNodeTemplate(node.type);
|
|
2000
|
+
if (!template) return;
|
|
2001
|
+
if (node.type.startsWith("vector_split_")) {
|
|
2002
|
+
const inputExpr = inputs.vector;
|
|
2003
|
+
for (const output of node.outputs) {
|
|
2004
|
+
const varName = this.getOutputVariable(node.id, output.id);
|
|
2005
|
+
const wgslType = this.getWGSLType(output.type);
|
|
2006
|
+
lines.push(`let ${varName}: ${wgslType} = (${inputExpr}).${output.id};`);
|
|
2007
|
+
}
|
|
2008
|
+
return;
|
|
2009
|
+
}
|
|
2010
|
+
const baseCode = template.generateCode(node, inputs);
|
|
2011
|
+
const tempVar = this.generateVariable();
|
|
2012
|
+
lines.push(`let ${tempVar} = ${baseCode};`);
|
|
2013
|
+
for (const output of node.outputs) {
|
|
2014
|
+
const varName = this.getOutputVariable(node.id, output.id);
|
|
2015
|
+
lines.push(`let ${varName} = ${tempVar}.${output.id};`);
|
|
2016
|
+
}
|
|
2017
|
+
}
|
|
2018
|
+
/**
|
|
2019
|
+
* Generate vertex shader body
|
|
2020
|
+
*/
|
|
2021
|
+
generateVertexBody(nodes) {
|
|
2022
|
+
const lines = [];
|
|
2023
|
+
const connections = this.graph.connections;
|
|
2024
|
+
const vertexOffsetNode = nodes.find((n) => n.type === "output_vertex_offset");
|
|
2025
|
+
if (vertexOffsetNode) {
|
|
2026
|
+
const template = getNodeTemplate(vertexOffsetNode.type);
|
|
2027
|
+
if (template) {
|
|
2028
|
+
const inputs = {};
|
|
2029
|
+
for (const port of vertexOffsetNode.inputs) {
|
|
2030
|
+
inputs[port.id] = this.getInputExpression(vertexOffsetNode, port.id, connections);
|
|
2031
|
+
}
|
|
2032
|
+
lines.push(`// Vertex offset`);
|
|
2033
|
+
lines.push(`let vertexOffset = ${inputs.offset};`);
|
|
2034
|
+
lines.push(`let worldPos = in.position + vertexOffset;`);
|
|
2035
|
+
}
|
|
2036
|
+
} else {
|
|
2037
|
+
lines.push(`let worldPos = in.position;`);
|
|
2038
|
+
}
|
|
2039
|
+
return lines.join("\n ");
|
|
2040
|
+
}
|
|
2041
|
+
/**
|
|
2042
|
+
* Generate complete vertex shader
|
|
2043
|
+
*/
|
|
2044
|
+
generateVertexShader(body) {
|
|
2045
|
+
return `// Generated by HoloScript Shader Graph Compiler
|
|
2046
|
+
// Graph: ${this.graph.name}
|
|
2047
|
+
|
|
2048
|
+
struct VertexInput {
|
|
2049
|
+
@location(0) position: vec3<f32>,
|
|
2050
|
+
@location(1) normal: vec3<f32>,
|
|
2051
|
+
@location(2) uv: vec2<f32>,
|
|
2052
|
+
@location(3) tangent: vec4<f32>,
|
|
2053
|
+
};
|
|
2054
|
+
|
|
2055
|
+
struct VertexOutput {
|
|
2056
|
+
@builtin(position) clipPosition: vec4<f32>,
|
|
2057
|
+
@location(0) worldPosition: vec3<f32>,
|
|
2058
|
+
@location(1) worldNormal: vec3<f32>,
|
|
2059
|
+
@location(2) uv: vec2<f32>,
|
|
2060
|
+
};
|
|
2061
|
+
|
|
2062
|
+
struct CameraUniforms {
|
|
2063
|
+
viewProjection: mat4x4<f32>,
|
|
2064
|
+
view: mat4x4<f32>,
|
|
2065
|
+
projection: mat4x4<f32>,
|
|
2066
|
+
position: vec3<f32>,
|
|
2067
|
+
};
|
|
2068
|
+
|
|
2069
|
+
struct ModelUniforms {
|
|
2070
|
+
model: mat4x4<f32>,
|
|
2071
|
+
normalMatrix: mat3x3<f32>,
|
|
2072
|
+
};
|
|
2073
|
+
|
|
2074
|
+
@group(0) @binding(0) var<uniform> camera: CameraUniforms;
|
|
2075
|
+
@group(1) @binding(0) var<uniform> model: ModelUniforms;
|
|
2076
|
+
|
|
2077
|
+
@vertex
|
|
2078
|
+
fn main(in: VertexInput) -> VertexOutput {
|
|
2079
|
+
var out: VertexOutput;
|
|
2080
|
+
|
|
2081
|
+
${body}
|
|
2082
|
+
|
|
2083
|
+
let worldPosition = (model.model * vec4<f32>(worldPos, 1.0)).xyz;
|
|
2084
|
+
let worldNormal = normalize(model.normalMatrix * in.normal);
|
|
2085
|
+
|
|
2086
|
+
out.worldPosition = worldPosition;
|
|
2087
|
+
out.worldNormal = worldNormal;
|
|
2088
|
+
out.uv = in.uv;
|
|
2089
|
+
out.clipPosition = camera.viewProjection * vec4<f32>(worldPosition, 1.0);
|
|
2090
|
+
|
|
2091
|
+
return out;
|
|
2092
|
+
}
|
|
2093
|
+
`;
|
|
2094
|
+
}
|
|
2095
|
+
/**
|
|
2096
|
+
* Generate complete fragment shader
|
|
2097
|
+
*/
|
|
2098
|
+
generateFragmentShader(body) {
|
|
2099
|
+
const textureBindings = this.generateTextureBindings();
|
|
2100
|
+
const uniformBindings = this.generateUniformBindings();
|
|
2101
|
+
const helperFunctions = this.generateHelperFunctions();
|
|
2102
|
+
return `// Generated by HoloScript Shader Graph Compiler
|
|
2103
|
+
// Graph: ${this.graph.name}
|
|
2104
|
+
|
|
2105
|
+
struct FragmentInput {
|
|
2106
|
+
@location(0) worldPosition: vec3<f32>,
|
|
2107
|
+
@location(1) worldNormal: vec3<f32>,
|
|
2108
|
+
@location(2) uv: vec2<f32>,
|
|
2109
|
+
};
|
|
2110
|
+
|
|
2111
|
+
struct CameraUniforms {
|
|
2112
|
+
viewProjection: mat4x4<f32>,
|
|
2113
|
+
view: mat4x4<f32>,
|
|
2114
|
+
projection: mat4x4<f32>,
|
|
2115
|
+
position: vec3<f32>,
|
|
2116
|
+
};
|
|
2117
|
+
|
|
2118
|
+
struct SceneUniforms {
|
|
2119
|
+
ambientColor: vec3<f32>,
|
|
2120
|
+
time: f32,
|
|
2121
|
+
deltaTime: f32,
|
|
2122
|
+
};
|
|
2123
|
+
|
|
2124
|
+
struct LightData {
|
|
2125
|
+
position: vec3<f32>,
|
|
2126
|
+
color: vec3<f32>,
|
|
2127
|
+
intensity: f32,
|
|
2128
|
+
};
|
|
2129
|
+
|
|
2130
|
+
@group(0) @binding(0) var<uniform> camera: CameraUniforms;
|
|
2131
|
+
@group(0) @binding(1) var<uniform> scene: SceneUniforms;
|
|
2132
|
+
@group(0) @binding(2) var<uniform> light: LightData;
|
|
2133
|
+
${textureBindings}
|
|
2134
|
+
${uniformBindings}
|
|
2135
|
+
|
|
2136
|
+
const PI: f32 = 3.14159265359;
|
|
2137
|
+
|
|
2138
|
+
${helperFunctions}
|
|
2139
|
+
|
|
2140
|
+
@fragment
|
|
2141
|
+
fn main(in: FragmentInput) -> @location(0) vec4<f32> {
|
|
2142
|
+
${body}
|
|
2143
|
+
|
|
2144
|
+
// PBR lighting calculation
|
|
2145
|
+
let N = normalize(normal);
|
|
2146
|
+
let V = normalize(camera.position - in.worldPosition);
|
|
2147
|
+
let L = normalize(light.position - in.worldPosition);
|
|
2148
|
+
let H = normalize(V + L);
|
|
2149
|
+
let NdotV = max(dot(N, V), 0.0);
|
|
2150
|
+
let NdotL = max(dot(N, L), 0.0);
|
|
2151
|
+
let NdotH = max(dot(N, H), 0.0);
|
|
2152
|
+
let VdotH = max(dot(V, H), 0.0);
|
|
2153
|
+
|
|
2154
|
+
let F0 = mix(vec3<f32>(0.04), baseColor, metallic);
|
|
2155
|
+
|
|
2156
|
+
// Cook-Torrance BRDF
|
|
2157
|
+
let NDF = distributionGGX(N, H, roughness);
|
|
2158
|
+
let G = geometrySmith(N, V, L, roughness);
|
|
2159
|
+
let F = fresnelSchlick(VdotH, F0);
|
|
2160
|
+
|
|
2161
|
+
let numerator = NDF * G * F;
|
|
2162
|
+
let denominator = 4.0 * NdotV * NdotL + 0.0001;
|
|
2163
|
+
let specular = numerator / denominator;
|
|
2164
|
+
|
|
2165
|
+
let kS = F;
|
|
2166
|
+
let kD = (vec3<f32>(1.0) - kS) * (1.0 - metallic);
|
|
2167
|
+
|
|
2168
|
+
var Lo = (kD * baseColor / PI + specular) * light.color * light.intensity * NdotL;
|
|
2169
|
+
|
|
2170
|
+
// Sheen \u2014 soft fuzzy reflection layer (fabrics, velvet)
|
|
2171
|
+
if (sheenRoughness > 0.0 || length(sheenColor) > 0.0) {
|
|
2172
|
+
let sheenD = distributionGGX(N, H, sheenRoughness);
|
|
2173
|
+
let sheenTerm = sheenColor * sheenD * NdotL;
|
|
2174
|
+
Lo = Lo + sheenTerm * light.color * light.intensity;
|
|
2175
|
+
}
|
|
2176
|
+
|
|
2177
|
+
// Clearcoat \u2014 protective clear layer (car paint, varnish)
|
|
2178
|
+
if (clearcoatVal > 0.0) {
|
|
2179
|
+
let ccNDF = distributionGGX(N, H, clearcoatRoughnessVal);
|
|
2180
|
+
let ccG = geometrySmith(N, V, L, clearcoatRoughnessVal);
|
|
2181
|
+
let ccF = fresnelSchlick(VdotH, vec3<f32>(0.04));
|
|
2182
|
+
let ccSpec = (ccNDF * ccG * ccF) / (4.0 * NdotV * NdotL + 0.0001);
|
|
2183
|
+
Lo = Lo + ccSpec * clearcoatVal * light.color * light.intensity * NdotL;
|
|
2184
|
+
}
|
|
2185
|
+
|
|
2186
|
+
// Subsurface scattering approximation (skin, wax, leaves, jade)
|
|
2187
|
+
var sssContrib = vec3<f32>(0.0);
|
|
2188
|
+
if (subsurfaceThickness > 0.0) {
|
|
2189
|
+
let scatterVec = normalize(L + N * 0.5);
|
|
2190
|
+
let sssNdotL = max(dot(V, -scatterVec), 0.0);
|
|
2191
|
+
let sssFalloff = pow(sssNdotL, 3.0) * subsurfaceThickness;
|
|
2192
|
+
sssContrib = subsurfaceColor * sssFalloff * light.color * light.intensity;
|
|
2193
|
+
Lo = Lo + sssContrib;
|
|
2194
|
+
}
|
|
2195
|
+
|
|
2196
|
+
// Iridescence \u2014 thin-film interference (soap bubbles, oil slicks, beetles)
|
|
2197
|
+
if (iridescenceVal > 0.0) {
|
|
2198
|
+
let thinFilmPhase = VdotH * 6.28318;
|
|
2199
|
+
let iriColor = vec3<f32>(
|
|
2200
|
+
0.5 + 0.5 * cos(thinFilmPhase),
|
|
2201
|
+
0.5 + 0.5 * cos(thinFilmPhase + 2.094),
|
|
2202
|
+
0.5 + 0.5 * cos(thinFilmPhase + 4.189)
|
|
2203
|
+
);
|
|
2204
|
+
Lo = mix(Lo, Lo * iriColor, iridescenceVal * 0.5);
|
|
2205
|
+
}
|
|
2206
|
+
|
|
2207
|
+
// Sparkle/glitter \u2014 stochastic micro-facet flashing
|
|
2208
|
+
if (sparkleIntensity > 0.0) {
|
|
2209
|
+
let sparkleUV = in.uv * sparkleDensity;
|
|
2210
|
+
let sparkleCell = floor(sparkleUV);
|
|
2211
|
+
let sparkleRand = simpleNoise(sparkleCell);
|
|
2212
|
+
let sparkleAngle = sparkleRand * 6.28318;
|
|
2213
|
+
let sparkleNormal = vec3<f32>(cos(sparkleAngle) * 0.3, sin(sparkleAngle) * 0.3, 0.9);
|
|
2214
|
+
let sparkleReflect = max(dot(reflect(-V, normalize(sparkleNormal)), L), 0.0);
|
|
2215
|
+
let sparkleFlash = pow(sparkleReflect, 64.0) * step(0.85, sparkleRand) * sparkleIntensity;
|
|
2216
|
+
Lo = Lo + vec3<f32>(sparkleFlash) * light.color * light.intensity;
|
|
2217
|
+
}
|
|
2218
|
+
|
|
2219
|
+
// Blackbody radiation \u2014 temperature-driven emission color (Planck approximation)
|
|
2220
|
+
if (blackbodyTemp > 0.0) {
|
|
2221
|
+
let bbColor = blackbodyColor(blackbodyTemp, 1.0);
|
|
2222
|
+
Lo = Lo + bbColor;
|
|
2223
|
+
}
|
|
2224
|
+
|
|
2225
|
+
// Fluorescence \u2014 absorb short \u03BB, re-emit longer \u03BB
|
|
2226
|
+
if (fluorescenceIntensity > 0.0) {
|
|
2227
|
+
let excitationMatch = dot(normalize(light.color), normalize(fluorescenceColor));
|
|
2228
|
+
let fluorEmit = fluorescenceColor * max(excitationMatch, 0.3) * fluorescenceIntensity;
|
|
2229
|
+
Lo = Lo + fluorEmit;
|
|
2230
|
+
}
|
|
2231
|
+
|
|
2232
|
+
// Retroreflection \u2014 light bounces back toward source
|
|
2233
|
+
if (retroreflectionVal > 0.0) {
|
|
2234
|
+
let retroDot = max(dot(V, L), 0.0);
|
|
2235
|
+
let retroTerm = pow(retroDot, 8.0) * retroreflectionVal;
|
|
2236
|
+
Lo = Lo + baseColor * retroTerm * light.color * light.intensity;
|
|
2237
|
+
}
|
|
2238
|
+
|
|
2239
|
+
var color = scene.ambientColor * baseColor * ao + Lo + emission;
|
|
2240
|
+
|
|
2241
|
+
// Volumetric contribution (if output_volume is connected)
|
|
2242
|
+
// The volDensity variable is set by output_volume node; if absent the default is 0
|
|
2243
|
+
// which makes the rayMarchVolume return transparent, so the surface pass dominates.
|
|
2244
|
+
|
|
2245
|
+
// Tone mapping (Reinhard)
|
|
2246
|
+
color = color / (color + vec3<f32>(1.0));
|
|
2247
|
+
|
|
2248
|
+
// Gamma correction
|
|
2249
|
+
color = pow(color, vec3<f32>(1.0 / 2.2));
|
|
2250
|
+
|
|
2251
|
+
return vec4<f32>(color, alpha);
|
|
2252
|
+
}
|
|
2253
|
+
`;
|
|
2254
|
+
}
|
|
2255
|
+
/**
|
|
2256
|
+
* Generate texture bindings
|
|
2257
|
+
*/
|
|
2258
|
+
generateTextureBindings() {
|
|
2259
|
+
if (this.textures.length === 0) return "";
|
|
2260
|
+
const lines = [];
|
|
2261
|
+
for (const tex of this.textures) {
|
|
2262
|
+
const binding = tex.binding;
|
|
2263
|
+
lines.push(`@group(2) @binding(${binding}) var ${tex.name}: texture_2d<f32>;`);
|
|
2264
|
+
lines.push(`@group(2) @binding(${binding + 1}) var ${tex.name}Sampler: sampler;`);
|
|
2265
|
+
}
|
|
2266
|
+
return lines.join("\n");
|
|
2267
|
+
}
|
|
2268
|
+
/**
|
|
2269
|
+
* Generate uniform bindings
|
|
2270
|
+
*/
|
|
2271
|
+
generateUniformBindings() {
|
|
2272
|
+
if (this.uniforms.length === 0) return "";
|
|
2273
|
+
const lines = ["struct MaterialUniforms {"];
|
|
2274
|
+
for (const uniform of this.uniforms) {
|
|
2275
|
+
const wgslType = this.getWGSLType(uniform.type);
|
|
2276
|
+
lines.push(` ${uniform.name}: ${wgslType},`);
|
|
2277
|
+
}
|
|
2278
|
+
lines.push("};");
|
|
2279
|
+
lines.push("@group(2) @binding(100) var<uniform> material: MaterialUniforms;");
|
|
2280
|
+
return lines.join("\n");
|
|
2281
|
+
}
|
|
2282
|
+
/**
|
|
2283
|
+
* Generate helper functions
|
|
2284
|
+
*/
|
|
2285
|
+
generateHelperFunctions() {
|
|
2286
|
+
return `
|
|
2287
|
+
// PBR helper functions
|
|
2288
|
+
fn distributionGGX(N: vec3<f32>, H: vec3<f32>, roughness: f32) -> f32 {
|
|
2289
|
+
let a = roughness * roughness;
|
|
2290
|
+
let a2 = a * a;
|
|
2291
|
+
let NdotH = max(dot(N, H), 0.0);
|
|
2292
|
+
let NdotH2 = NdotH * NdotH;
|
|
2293
|
+
|
|
2294
|
+
let num = a2;
|
|
2295
|
+
let denom = (NdotH2 * (a2 - 1.0) + 1.0);
|
|
2296
|
+
let denom2 = PI * denom * denom;
|
|
2297
|
+
|
|
2298
|
+
return num / denom2;
|
|
2299
|
+
}
|
|
2300
|
+
|
|
2301
|
+
fn geometrySchlickGGX(NdotV: f32, roughness: f32) -> f32 {
|
|
2302
|
+
let r = roughness + 1.0;
|
|
2303
|
+
let k = (r * r) / 8.0;
|
|
2304
|
+
|
|
2305
|
+
let num = NdotV;
|
|
2306
|
+
let denom = NdotV * (1.0 - k) + k;
|
|
2307
|
+
|
|
2308
|
+
return num / denom;
|
|
2309
|
+
}
|
|
2310
|
+
|
|
2311
|
+
fn geometrySmith(N: vec3<f32>, V: vec3<f32>, L: vec3<f32>, roughness: f32) -> f32 {
|
|
2312
|
+
let NdotV = max(dot(N, V), 0.0);
|
|
2313
|
+
let NdotL = max(dot(N, L), 0.0);
|
|
2314
|
+
let ggx2 = geometrySchlickGGX(NdotV, roughness);
|
|
2315
|
+
let ggx1 = geometrySchlickGGX(NdotL, roughness);
|
|
2316
|
+
|
|
2317
|
+
return ggx1 * ggx2;
|
|
2318
|
+
}
|
|
2319
|
+
|
|
2320
|
+
fn fresnelSchlick(cosTheta: f32, F0: vec3<f32>) -> vec3<f32> {
|
|
2321
|
+
return F0 + (vec3<f32>(1.0) - F0) * pow(saturate(1.0 - cosTheta), 5.0);
|
|
2322
|
+
}
|
|
2323
|
+
|
|
2324
|
+
fn hueShift(color: vec3<f32>, shift: f32) -> vec3<f32> {
|
|
2325
|
+
let k = vec3<f32>(0.57735);
|
|
2326
|
+
let cosAngle = cos(shift);
|
|
2327
|
+
return color * cosAngle + cross(k, color) * sin(shift) + k * dot(k, color) * (1.0 - cosAngle);
|
|
2328
|
+
}
|
|
2329
|
+
|
|
2330
|
+
fn simpleNoise(uv: vec2<f32>) -> f32 {
|
|
2331
|
+
return fract(sin(dot(uv, vec2<f32>(12.9898, 78.233))) * 43758.5453);
|
|
2332
|
+
}
|
|
2333
|
+
|
|
2334
|
+
fn gradientNoise(uv: vec2<f32>) -> f32 {
|
|
2335
|
+
let i = floor(uv);
|
|
2336
|
+
let f = fract(uv);
|
|
2337
|
+
let u = f * f * (3.0 - 2.0 * f);
|
|
2338
|
+
|
|
2339
|
+
let n00 = simpleNoise(i);
|
|
2340
|
+
let n10 = simpleNoise(i + vec2<f32>(1.0, 0.0));
|
|
2341
|
+
let n01 = simpleNoise(i + vec2<f32>(0.0, 1.0));
|
|
2342
|
+
let n11 = simpleNoise(i + vec2<f32>(1.0, 1.0));
|
|
2343
|
+
|
|
2344
|
+
return mix(mix(n00, n10, u.x), mix(n01, n11, u.x), u.y);
|
|
2345
|
+
}
|
|
2346
|
+
|
|
2347
|
+
fn voronoi(uv: vec2<f32>) -> vec2<f32> {
|
|
2348
|
+
let n = floor(uv);
|
|
2349
|
+
let f = fract(uv);
|
|
2350
|
+
|
|
2351
|
+
var minDist = 1.0;
|
|
2352
|
+
var cellId = 0.0;
|
|
2353
|
+
|
|
2354
|
+
for (var j = -1; j <= 1; j++) {
|
|
2355
|
+
for (var i = -1; i <= 1; i++) {
|
|
2356
|
+
let neighbor = vec2<f32>(f32(i), f32(j));
|
|
2357
|
+
let point = simpleNoise(n + neighbor);
|
|
2358
|
+
let diff = neighbor + point - f;
|
|
2359
|
+
let dist = length(diff);
|
|
2360
|
+
if (dist < minDist) {
|
|
2361
|
+
minDist = dist;
|
|
2362
|
+
cellId = simpleNoise(n + neighbor);
|
|
2363
|
+
}
|
|
2364
|
+
}
|
|
2365
|
+
}
|
|
2366
|
+
|
|
2367
|
+
return vec2<f32>(cellId, minDist);
|
|
2368
|
+
}
|
|
2369
|
+
|
|
2370
|
+
// =========================================================================
|
|
2371
|
+
// Advanced Material Functions
|
|
2372
|
+
// =========================================================================
|
|
2373
|
+
|
|
2374
|
+
// Blackbody radiation \u2014 Planck approximation: temperature (Kelvin) \u2192 RGB color
|
|
2375
|
+
fn blackbodyColor(tempK: f32, intensity: f32) -> vec3<f32> {
|
|
2376
|
+
// CIE 1931 approximation for blackbody emission
|
|
2377
|
+
let t = clamp(tempK, 1000.0, 40000.0) / 100.0;
|
|
2378
|
+
var r: f32; var g: f32; var b: f32;
|
|
2379
|
+
|
|
2380
|
+
// Red channel
|
|
2381
|
+
if (t <= 66.0) {
|
|
2382
|
+
r = 1.0;
|
|
2383
|
+
} else {
|
|
2384
|
+
r = pow((t - 60.0) / 40.0, -0.1332);
|
|
2385
|
+
r = clamp(r, 0.0, 1.0);
|
|
2386
|
+
}
|
|
2387
|
+
|
|
2388
|
+
// Green channel
|
|
2389
|
+
if (t <= 66.0) {
|
|
2390
|
+
g = 0.39 * log(t) - 0.63;
|
|
2391
|
+
} else {
|
|
2392
|
+
g = 1.29 * pow((t - 60.0) / 40.0, -0.0755);
|
|
2393
|
+
}
|
|
2394
|
+
g = clamp(g, 0.0, 1.0);
|
|
2395
|
+
|
|
2396
|
+
// Blue channel
|
|
2397
|
+
if (t >= 66.0) {
|
|
2398
|
+
b = 1.0;
|
|
2399
|
+
} else if (t <= 19.0) {
|
|
2400
|
+
b = 0.0;
|
|
2401
|
+
} else {
|
|
2402
|
+
b = 0.543 * log(t - 10.0) - 1.19;
|
|
2403
|
+
b = clamp(b, 0.0, 1.0);
|
|
2404
|
+
}
|
|
2405
|
+
|
|
2406
|
+
return vec3<f32>(r, g, b) * intensity;
|
|
2407
|
+
}
|
|
2408
|
+
|
|
2409
|
+
// Sparkle / Glitter \u2014 stochastic micro-facet flash
|
|
2410
|
+
fn sparkleFlash(uv: vec2<f32>, density: f32, size: f32, intensity: f32, viewDir: vec3<f32>, normal: vec3<f32>) -> vec2<f32> {
|
|
2411
|
+
let cell = floor(uv * density);
|
|
2412
|
+
let rand = simpleNoise(cell);
|
|
2413
|
+
let threshold = 1.0 - size;
|
|
2414
|
+
let flash = step(threshold, rand) * intensity;
|
|
2415
|
+
return vec2<f32>(flash, rand);
|
|
2416
|
+
}
|
|
2417
|
+
|
|
2418
|
+
// Animated pattern \u2014 time-varying surface effects
|
|
2419
|
+
fn animatedPattern(uv: vec2<f32>, time: f32, patternType: f32, speed: f32, amplitude: f32, scale: f32) -> vec2<f32> {
|
|
2420
|
+
let t = time * speed;
|
|
2421
|
+
let suv = uv * scale;
|
|
2422
|
+
var value: f32 = 0.0;
|
|
2423
|
+
var uvOffset = vec2<f32>(0.0);
|
|
2424
|
+
|
|
2425
|
+
let pType = i32(patternType);
|
|
2426
|
+
|
|
2427
|
+
// 0: ripple \u2014 concentric rings from center
|
|
2428
|
+
if (pType == 0) {
|
|
2429
|
+
let dist = length(suv - vec2<f32>(0.5));
|
|
2430
|
+
value = sin(dist * 20.0 - t * 4.0) * 0.5 + 0.5;
|
|
2431
|
+
}
|
|
2432
|
+
// 1: flicker \u2014 random temporal noise
|
|
2433
|
+
else if (pType == 1) {
|
|
2434
|
+
value = simpleNoise(vec2<f32>(t * 3.0, 0.0));
|
|
2435
|
+
}
|
|
2436
|
+
// 2: pulse \u2014 breathing sine wave
|
|
2437
|
+
else if (pType == 2) {
|
|
2438
|
+
value = sin(t * 2.0) * 0.5 + 0.5;
|
|
2439
|
+
}
|
|
2440
|
+
// 3: flow \u2014 directional scroll
|
|
2441
|
+
else if (pType == 3) {
|
|
2442
|
+
uvOffset = vec2<f32>(t * 0.1, 0.0);
|
|
2443
|
+
value = simpleNoise(suv + uvOffset);
|
|
2444
|
+
}
|
|
2445
|
+
// 4: breathe \u2014 slow amplitude modulation
|
|
2446
|
+
else if (pType == 4) {
|
|
2447
|
+
value = sin(t * 0.5) * 0.5 + 0.5;
|
|
2448
|
+
value = value * value; // ease-in
|
|
2449
|
+
}
|
|
2450
|
+
// 5: scroll \u2014 vertical scroll
|
|
2451
|
+
else if (pType == 5) {
|
|
2452
|
+
uvOffset = vec2<f32>(0.0, t * 0.1);
|
|
2453
|
+
value = gradientNoise(suv + uvOffset);
|
|
2454
|
+
}
|
|
2455
|
+
// 6: wave \u2014 sinusoidal displacement
|
|
2456
|
+
else if (pType == 6) {
|
|
2457
|
+
value = sin(suv.x * 10.0 + t * 3.0) * sin(suv.y * 10.0 + t * 2.0) * 0.5 + 0.5;
|
|
2458
|
+
}
|
|
2459
|
+
// 7: noise \u2014 animated Perlin
|
|
2460
|
+
else {
|
|
2461
|
+
value = gradientNoise(suv + vec2<f32>(t * 0.2));
|
|
2462
|
+
}
|
|
2463
|
+
|
|
2464
|
+
return vec2<f32>(value * amplitude, 0.0);
|
|
2465
|
+
}
|
|
2466
|
+
|
|
2467
|
+
// Weathering \u2014 procedural surface degradation
|
|
2468
|
+
fn weatheringSurface(uv: vec2<f32>, progress: f32, weatherType: f32, seed: f32, baseColor: vec3<f32>) -> vec3<f32> {
|
|
2469
|
+
let noise1 = gradientNoise(uv * 8.0 + vec2<f32>(seed));
|
|
2470
|
+
let noise2 = simpleNoise(uv * 16.0 + vec2<f32>(seed * 2.0));
|
|
2471
|
+
let mask = smoothstep(1.0 - progress, 1.0 - progress + 0.2, noise1);
|
|
2472
|
+
|
|
2473
|
+
let wType = i32(weatherType);
|
|
2474
|
+
var weatherColor = vec3<f32>(0.5);
|
|
2475
|
+
|
|
2476
|
+
// 0: rust
|
|
2477
|
+
if (wType == 0) { weatherColor = vec3<f32>(0.55, 0.22, 0.08); }
|
|
2478
|
+
// 1: moss
|
|
2479
|
+
else if (wType == 1) { weatherColor = vec3<f32>(0.29, 0.49, 0.25); }
|
|
2480
|
+
// 2: crack \u2014 darken along cracks
|
|
2481
|
+
else if (wType == 2) { weatherColor = baseColor * 0.3; }
|
|
2482
|
+
// 3: peel \u2014 reveal underlayer
|
|
2483
|
+
else if (wType == 3) { weatherColor = vec3<f32>(0.85, 0.82, 0.78); }
|
|
2484
|
+
// 4: patina
|
|
2485
|
+
else if (wType == 4) { weatherColor = vec3<f32>(0.29, 0.55, 0.49); }
|
|
2486
|
+
// 5: frost
|
|
2487
|
+
else if (wType == 5) { weatherColor = vec3<f32>(0.9, 0.95, 1.0); }
|
|
2488
|
+
// 6: burn
|
|
2489
|
+
else if (wType == 6) { weatherColor = vec3<f32>(0.1, 0.08, 0.05); }
|
|
2490
|
+
// 7: erosion
|
|
2491
|
+
else if (wType == 7) { weatherColor = baseColor * (0.5 + noise2 * 0.3); }
|
|
2492
|
+
// 8: stain
|
|
2493
|
+
else if (wType == 8) { weatherColor = mix(baseColor, vec3<f32>(0.4, 0.35, 0.25), 0.6); }
|
|
2494
|
+
// 9: dust
|
|
2495
|
+
else { weatherColor = mix(baseColor, vec3<f32>(0.75, 0.7, 0.65), 0.4); }
|
|
2496
|
+
|
|
2497
|
+
return mix(baseColor, weatherColor, mask);
|
|
2498
|
+
}
|
|
2499
|
+
|
|
2500
|
+
// Dual-layer material blend
|
|
2501
|
+
fn dualLayerBlend(
|
|
2502
|
+
baseCol: vec3<f32>, baseRough: f32, baseMetal: f32,
|
|
2503
|
+
topCol: vec3<f32>, topRough: f32, topMetal: f32,
|
|
2504
|
+
blend: f32, mode: f32, normal: vec3<f32>, uv: vec2<f32>
|
|
2505
|
+
) -> vec3<f32> {
|
|
2506
|
+
var factor = blend;
|
|
2507
|
+
|
|
2508
|
+
let modeInt = i32(mode);
|
|
2509
|
+
// 0: linear
|
|
2510
|
+
// 1: height-based (top appears on upward-facing surfaces)
|
|
2511
|
+
if (modeInt == 1) {
|
|
2512
|
+
factor = factor * max(normal.y, 0.0);
|
|
2513
|
+
}
|
|
2514
|
+
// 2: noise-based
|
|
2515
|
+
else if (modeInt == 2) {
|
|
2516
|
+
let n = gradientNoise(uv * 8.0);
|
|
2517
|
+
factor = smoothstep(0.5 - blend * 0.5, 0.5 + blend * 0.5, n);
|
|
2518
|
+
}
|
|
2519
|
+
// 3: fresnel-based (top appears at grazing angles)
|
|
2520
|
+
else if (modeInt == 3) {
|
|
2521
|
+
factor = factor; // fresnel computed in main lighting \u2014 blend is direct here
|
|
2522
|
+
}
|
|
2523
|
+
|
|
2524
|
+
return mix(baseCol, topCol, factor);
|
|
2525
|
+
}
|
|
2526
|
+
|
|
2527
|
+
// Fluorescence \u2014 absorb excitation wavelengths, emit shifted color
|
|
2528
|
+
fn fluorescenceEmit(lightColor: vec3<f32>, excitationColor: vec3<f32>, emissionColor: vec3<f32>, intensity: f32) -> vec3<f32> {
|
|
2529
|
+
let match = dot(normalize(lightColor), normalize(excitationColor));
|
|
2530
|
+
return emissionColor * max(match, 0.0) * intensity;
|
|
2531
|
+
}
|
|
2532
|
+
|
|
2533
|
+
// Retroreflection \u2014 light reflects back toward its source
|
|
2534
|
+
fn retroReflect(viewDir: vec3<f32>, lightDir: vec3<f32>, normal: vec3<f32>, intensity: f32) -> f32 {
|
|
2535
|
+
let retroDot = max(dot(viewDir, lightDir), 0.0);
|
|
2536
|
+
return pow(retroDot, 8.0) * intensity;
|
|
2537
|
+
}
|
|
2538
|
+
|
|
2539
|
+
// =========================================================================
|
|
2540
|
+
// Volumetric Material Functions \u2014 Ray Marching, 3D Noise, Scattering
|
|
2541
|
+
// =========================================================================
|
|
2542
|
+
|
|
2543
|
+
// 3D noise \u2014 hash-based, for volumetric density
|
|
2544
|
+
fn noise3D(p: vec3<f32>) -> f32 {
|
|
2545
|
+
let i = floor(p);
|
|
2546
|
+
let f = fract(p);
|
|
2547
|
+
let u = f * f * (3.0 - 2.0 * f);
|
|
2548
|
+
|
|
2549
|
+
let n000 = simpleNoise(i.xy + vec2<f32>(0.0, i.z * 37.0));
|
|
2550
|
+
let n100 = simpleNoise(i.xy + vec2<f32>(1.0, i.z * 37.0));
|
|
2551
|
+
let n010 = simpleNoise(i.xy + vec2<f32>(0.0, (i.z + 1.0) * 37.0 + 17.0));
|
|
2552
|
+
let n110 = simpleNoise(i.xy + vec2<f32>(1.0, (i.z + 1.0) * 37.0 + 17.0));
|
|
2553
|
+
let n001 = simpleNoise(i.xy + vec2<f32>(i.z * 37.0 + 59.0, 0.0));
|
|
2554
|
+
let n101 = simpleNoise(i.xy + vec2<f32>(i.z * 37.0 + 60.0, 0.0));
|
|
2555
|
+
let n011 = simpleNoise(i.xy + vec2<f32>((i.z + 1.0) * 37.0 + 59.0, 17.0));
|
|
2556
|
+
let n111 = simpleNoise(i.xy + vec2<f32>((i.z + 1.0) * 37.0 + 60.0, 17.0));
|
|
2557
|
+
|
|
2558
|
+
let x0 = mix(mix(n000, n100, u.x), mix(n010, n110, u.x), u.y);
|
|
2559
|
+
let x1 = mix(mix(n001, n101, u.x), mix(n011, n111, u.x), u.y);
|
|
2560
|
+
return mix(x0, x1, u.z);
|
|
2561
|
+
}
|
|
2562
|
+
|
|
2563
|
+
// Fractal Brownian Motion in 3D \u2014 layered noise octaves
|
|
2564
|
+
fn fbmNoise3D(p: vec3<f32>, octaves: i32, lacunarity: f32, gain: f32) -> f32 {
|
|
2565
|
+
var value = 0.0;
|
|
2566
|
+
var amplitude = 1.0;
|
|
2567
|
+
var frequency = 1.0;
|
|
2568
|
+
var pos = p;
|
|
2569
|
+
|
|
2570
|
+
for (var i = 0; i < octaves; i++) {
|
|
2571
|
+
value = value + amplitude * noise3D(pos * frequency);
|
|
2572
|
+
amplitude = amplitude * gain;
|
|
2573
|
+
frequency = frequency * lacunarity;
|
|
2574
|
+
}
|
|
2575
|
+
|
|
2576
|
+
return value;
|
|
2577
|
+
}
|
|
2578
|
+
|
|
2579
|
+
// Curl noise 3D \u2014 divergence-free noise for smoke/fire turbulence
|
|
2580
|
+
fn curlNoise3D(p: vec3<f32>) -> vec3<f32> {
|
|
2581
|
+
let eps = 0.01;
|
|
2582
|
+
let dx = vec3<f32>(eps, 0.0, 0.0);
|
|
2583
|
+
let dy = vec3<f32>(0.0, eps, 0.0);
|
|
2584
|
+
let dz = vec3<f32>(0.0, 0.0, eps);
|
|
2585
|
+
|
|
2586
|
+
let dFzdy = noise3D(p + dy) - noise3D(p - dy);
|
|
2587
|
+
let dFydz = noise3D(p + dz) - noise3D(p - dz);
|
|
2588
|
+
let dFxdz = noise3D(p + dz + dx) - noise3D(p - dz + dx);
|
|
2589
|
+
let dFzdx = noise3D(p + dx) - noise3D(p - dx);
|
|
2590
|
+
let dFydx = noise3D(p + dx + dy) - noise3D(p - dx + dy);
|
|
2591
|
+
let dFxdy = noise3D(p + dy + dx) - noise3D(p - dy + dx);
|
|
2592
|
+
|
|
2593
|
+
return vec3<f32>(
|
|
2594
|
+
dFzdy - dFydz,
|
|
2595
|
+
dFxdz - dFzdx,
|
|
2596
|
+
dFydx - dFxdy
|
|
2597
|
+
) / (2.0 * eps);
|
|
2598
|
+
}
|
|
2599
|
+
|
|
2600
|
+
// Volume density at a 3D position with FBM noise
|
|
2601
|
+
fn volumeDensity(pos: vec3<f32>, baseDensity: f32, noiseScale: f32, noiseOctaves: f32, time: f32) -> f32 {
|
|
2602
|
+
let animatedPos = pos + vec3<f32>(0.0, time * 0.1, 0.0);
|
|
2603
|
+
let n = fbmNoise3D(animatedPos * noiseScale, i32(noiseOctaves), 2.0, 0.5);
|
|
2604
|
+
return clamp(baseDensity * n, 0.0, 1.0);
|
|
2605
|
+
}
|
|
2606
|
+
|
|
2607
|
+
// Height fog density \u2014 exponential falloff with height
|
|
2608
|
+
fn heightFogDensity(pos: vec3<f32>, groundLevel: f32, density: f32, falloff: f32) -> f32 {
|
|
2609
|
+
let height = pos.y - groundLevel;
|
|
2610
|
+
return density * exp(-max(height, 0.0) * falloff);
|
|
2611
|
+
}
|
|
2612
|
+
|
|
2613
|
+
// Fire density \u2014 buoyant rising, turbulent, temperature-driven
|
|
2614
|
+
fn fireDensity(pos: vec3<f32>, time: f32, turbulence: f32, riseSpeed: f32, scale: f32) -> vec2<f32> {
|
|
2615
|
+
// Rising motion
|
|
2616
|
+
let risingPos = pos - vec3<f32>(0.0, time * riseSpeed, 0.0);
|
|
2617
|
+
// Turbulence via curl noise
|
|
2618
|
+
let curl = curlNoise3D(risingPos * scale * 0.5) * turbulence;
|
|
2619
|
+
let distortedPos = risingPos + curl;
|
|
2620
|
+
// Base density from FBM
|
|
2621
|
+
let d = fbmNoise3D(distortedPos * scale, 4, 2.0, 0.5);
|
|
2622
|
+
// Height falloff \u2014 fire thins out as it rises
|
|
2623
|
+
let heightFade = exp(-max(pos.y, 0.0) * 0.5);
|
|
2624
|
+
let density = clamp(d * heightFade, 0.0, 1.0);
|
|
2625
|
+
// Temperature \u2014 hotter at base, cooler at top
|
|
2626
|
+
let temperature = clamp(density * (1.0 - pos.y * 0.3) * 3000.0 + 800.0, 800.0, 3000.0);
|
|
2627
|
+
return vec2<f32>(density, temperature);
|
|
2628
|
+
}
|
|
2629
|
+
|
|
2630
|
+
// Henyey-Greenstein phase function \u2014 anisotropic light scattering
|
|
2631
|
+
fn henyeyGreenstein(cosTheta: f32, g: f32) -> f32 {
|
|
2632
|
+
let g2 = g * g;
|
|
2633
|
+
let denom = 1.0 + g2 - 2.0 * g * cosTheta;
|
|
2634
|
+
return (1.0 - g2) / (4.0 * PI * pow(denom, 1.5));
|
|
2635
|
+
}
|
|
2636
|
+
|
|
2637
|
+
// Beer-Lambert transmittance \u2014 light attenuation through absorbing medium
|
|
2638
|
+
fn beerLambert(density: f32, stepLen: f32, absorption: f32) -> f32 {
|
|
2639
|
+
return exp(-density * absorption * stepLen);
|
|
2640
|
+
}
|
|
2641
|
+
|
|
2642
|
+
// Volume scattering \u2014 transmittance + in-scattering at a sample point
|
|
2643
|
+
fn volumeScatter(density: f32, scattering: f32, absorption: f32, stepLen: f32, lightDir: vec3<f32>, viewDir: vec3<f32>, phaseAniso: f32) -> vec2<f32> {
|
|
2644
|
+
let cosTheta = dot(normalize(lightDir), normalize(viewDir));
|
|
2645
|
+
let phase = henyeyGreenstein(cosTheta, phaseAniso);
|
|
2646
|
+
let transmittance = beerLambert(density, stepLen, absorption + scattering);
|
|
2647
|
+
let inScatter = density * scattering * phase * stepLen;
|
|
2648
|
+
return vec2<f32>(transmittance, inScatter);
|
|
2649
|
+
}
|
|
2650
|
+
|
|
2651
|
+
// Volume emission \u2014 self-luminous contribution (fire, neon, aurora)
|
|
2652
|
+
fn volumeEmission(density: f32, emissionColor: vec3<f32>, intensity: f32, temperature: f32) -> vec3<f32> {
|
|
2653
|
+
var emCol = emissionColor * intensity * density;
|
|
2654
|
+
// If temperature > 0, blend with blackbody color
|
|
2655
|
+
if (temperature > 0.0) {
|
|
2656
|
+
let bbCol = blackbodyColor(temperature, 1.0);
|
|
2657
|
+
emCol = emCol * bbCol;
|
|
2658
|
+
}
|
|
2659
|
+
return emCol;
|
|
2660
|
+
}
|
|
2661
|
+
|
|
2662
|
+
// Ray-box intersection \u2014 returns (tNear, tFar) for axis-aligned bounding box
|
|
2663
|
+
fn rayBoxIntersect(rayOrigin: vec3<f32>, rayDir: vec3<f32>, boxMin: vec3<f32>, boxMax: vec3<f32>) -> vec2<f32> {
|
|
2664
|
+
let invDir = 1.0 / rayDir;
|
|
2665
|
+
let t1 = (boxMin - rayOrigin) * invDir;
|
|
2666
|
+
let t2 = (boxMax - rayOrigin) * invDir;
|
|
2667
|
+
let tMin = min(t1, t2);
|
|
2668
|
+
let tMax = max(t1, t2);
|
|
2669
|
+
let tNear = max(max(tMin.x, tMin.y), tMin.z);
|
|
2670
|
+
let tFar = min(min(tMax.x, tMax.y), tMax.z);
|
|
2671
|
+
return vec2<f32>(max(tNear, 0.0), tFar);
|
|
2672
|
+
}
|
|
2673
|
+
|
|
2674
|
+
// Main ray march loop \u2014 integrates density, color, emission along a ray
|
|
2675
|
+
fn rayMarchVolume(
|
|
2676
|
+
rayOrigin: vec3<f32>, rayDir: vec3<f32>,
|
|
2677
|
+
volDensity: f32, volColor: vec3<f32>, volEmission: vec3<f32>,
|
|
2678
|
+
scattering: f32, absorption: f32,
|
|
2679
|
+
steps: f32, maxDist: f32
|
|
2680
|
+
) -> vec4<f32> {
|
|
2681
|
+
// Intersect unit cube [-1,1]^3
|
|
2682
|
+
let hit = rayBoxIntersect(rayOrigin, rayDir, vec3<f32>(-1.0), vec3<f32>(1.0));
|
|
2683
|
+
if (hit.x > hit.y) {
|
|
2684
|
+
return vec4<f32>(0.0); // Miss
|
|
2685
|
+
}
|
|
2686
|
+
|
|
2687
|
+
let stepCount = i32(steps);
|
|
2688
|
+
let tStart = hit.x;
|
|
2689
|
+
let tEnd = min(hit.y, maxDist);
|
|
2690
|
+
let stepLen = (tEnd - tStart) / f32(stepCount);
|
|
2691
|
+
|
|
2692
|
+
var accColor = vec3<f32>(0.0);
|
|
2693
|
+
var accTransmittance = 1.0;
|
|
2694
|
+
|
|
2695
|
+
for (var i = 0; i < stepCount; i++) {
|
|
2696
|
+
if (accTransmittance < 0.01) { break; } // Early exit
|
|
2697
|
+
|
|
2698
|
+
let t = tStart + (f32(i) + 0.5) * stepLen;
|
|
2699
|
+
let samplePos = rayOrigin + rayDir * t;
|
|
2700
|
+
|
|
2701
|
+
// Sample density with noise
|
|
2702
|
+
let d = volumeDensity(samplePos, volDensity, 4.0, 4.0, scene.time);
|
|
2703
|
+
if (d < 0.001) { continue; } // Skip empty space
|
|
2704
|
+
|
|
2705
|
+
// Transmittance for this step (Beer-Lambert)
|
|
2706
|
+
let stepTransmittance = beerLambert(d, stepLen, absorption + scattering);
|
|
2707
|
+
|
|
2708
|
+
// In-scattering from light
|
|
2709
|
+
let lightDir = normalize(light.position - samplePos);
|
|
2710
|
+
let cosTheta = dot(lightDir, rayDir);
|
|
2711
|
+
let phase = henyeyGreenstein(cosTheta, 0.3);
|
|
2712
|
+
let inScatter = d * scattering * phase * stepLen * light.intensity;
|
|
2713
|
+
|
|
2714
|
+
// Emission
|
|
2715
|
+
let emissionContrib = volEmission * d * stepLen;
|
|
2716
|
+
|
|
2717
|
+
// Accumulate
|
|
2718
|
+
let sampleColor = volColor * inScatter * light.color + emissionContrib;
|
|
2719
|
+
accColor = accColor + sampleColor * accTransmittance;
|
|
2720
|
+
accTransmittance = accTransmittance * stepTransmittance;
|
|
2721
|
+
}
|
|
2722
|
+
|
|
2723
|
+
return vec4<f32>(accColor, 1.0 - accTransmittance);
|
|
2724
|
+
}
|
|
2725
|
+
|
|
2726
|
+
// =========================================================================
|
|
2727
|
+
// Caustics \u2014 underwater refracted light patterns
|
|
2728
|
+
// =========================================================================
|
|
2729
|
+
|
|
2730
|
+
// Voronoi-based caustic pattern (dual-layer for temporal stability)
|
|
2731
|
+
fn causticVoronoi(p: vec2<f32>) -> f32 {
|
|
2732
|
+
let i = floor(p);
|
|
2733
|
+
let f = fract(p);
|
|
2734
|
+
var minDist = 1.0;
|
|
2735
|
+
for (var y = -1; y <= 1; y++) {
|
|
2736
|
+
for (var x = -1; x <= 1; x++) {
|
|
2737
|
+
let neighbor = vec2<f32>(f32(x), f32(y));
|
|
2738
|
+
let cellHash = fract(sin(dot(i + neighbor, vec2<f32>(127.1, 311.7))) * 43758.5453);
|
|
2739
|
+
let cellHash2 = fract(sin(dot(i + neighbor, vec2<f32>(269.5, 183.3))) * 43758.5453);
|
|
2740
|
+
let point = neighbor + vec2<f32>(cellHash, cellHash2) - f;
|
|
2741
|
+
let dist = dot(point, point);
|
|
2742
|
+
minDist = min(minDist, dist);
|
|
2743
|
+
}
|
|
2744
|
+
}
|
|
2745
|
+
return sqrt(minDist);
|
|
2746
|
+
}
|
|
2747
|
+
|
|
2748
|
+
fn causticPattern(
|
|
2749
|
+
pos: vec3<f32>, time: f32, scale: f32, speed: f32,
|
|
2750
|
+
intensity: f32, color: vec3<f32>
|
|
2751
|
+
) -> vec3<f32> {
|
|
2752
|
+
// Dual-layer caustics for interference pattern
|
|
2753
|
+
let uv1 = pos.xz * scale + vec2<f32>(time * speed * 0.3, time * speed * 0.7);
|
|
2754
|
+
let uv2 = pos.xz * scale * 1.3 + vec2<f32>(-time * speed * 0.5, time * speed * 0.4);
|
|
2755
|
+
let c1 = causticVoronoi(uv1);
|
|
2756
|
+
let c2 = causticVoronoi(uv2);
|
|
2757
|
+
// Combine layers \u2014 bright where both layers converge
|
|
2758
|
+
let caustic = pow(1.0 - c1, 3.0) * pow(1.0 - c2, 3.0) * intensity;
|
|
2759
|
+
return color * caustic;
|
|
2760
|
+
}
|
|
2761
|
+
|
|
2762
|
+
// Chromatic refractive caustics: separate R/G/B with different IoR for prismatic effect
|
|
2763
|
+
fn causticChromatic(
|
|
2764
|
+
pos: vec3<f32>, time: f32, scale: f32, speed: f32,
|
|
2765
|
+
intensity: f32, dispersion: f32
|
|
2766
|
+
) -> vec3<f32> {
|
|
2767
|
+
let baseUV = pos.xz * scale;
|
|
2768
|
+
let t = time * speed;
|
|
2769
|
+
// Offset UV per channel based on dispersion
|
|
2770
|
+
let uvR = baseUV + vec2<f32>(t * 0.3 + dispersion, t * 0.7);
|
|
2771
|
+
let uvG = baseUV + vec2<f32>(t * 0.3, t * 0.7);
|
|
2772
|
+
let uvB = baseUV + vec2<f32>(t * 0.3 - dispersion, t * 0.7);
|
|
2773
|
+
// Dual-layer per channel
|
|
2774
|
+
let r = pow(1.0 - causticVoronoi(uvR), 3.0) * pow(1.0 - causticVoronoi(uvR * 1.3 + vec2<f32>(-t * 0.5, t * 0.4)), 3.0);
|
|
2775
|
+
let g = pow(1.0 - causticVoronoi(uvG), 3.0) * pow(1.0 - causticVoronoi(uvG * 1.3 + vec2<f32>(-t * 0.5, t * 0.4)), 3.0);
|
|
2776
|
+
let b = pow(1.0 - causticVoronoi(uvB), 3.0) * pow(1.0 - causticVoronoi(uvB * 1.3 + vec2<f32>(-t * 0.5, t * 0.4)), 3.0);
|
|
2777
|
+
return vec3<f32>(r, g, b) * intensity;
|
|
2778
|
+
}
|
|
2779
|
+
|
|
2780
|
+
// Turbulence-driven underwater foam patches
|
|
2781
|
+
fn underwaterFoam(
|
|
2782
|
+
pos: vec3<f32>, time: f32, scale: f32, threshold: f32
|
|
2783
|
+
) -> f32 {
|
|
2784
|
+
let uv = pos.xz * scale;
|
|
2785
|
+
// Multi-octave turbulence
|
|
2786
|
+
var turb = 0.0;
|
|
2787
|
+
turb += abs(simpleNoise(uv * 2.0 + vec2<f32>(time * 0.1, 0.0)) - 0.5) * 2.0;
|
|
2788
|
+
turb += abs(simpleNoise(uv * 4.0 + vec2<f32>(0.0, time * 0.15)) - 0.5) * 1.0;
|
|
2789
|
+
turb += abs(simpleNoise(uv * 8.0 + vec2<f32>(time * 0.2, time * 0.1)) - 0.5) * 0.5;
|
|
2790
|
+
turb /= 3.5;
|
|
2791
|
+
return smoothstep(threshold, threshold + 0.15, turb);
|
|
2792
|
+
}
|
|
2793
|
+
|
|
2794
|
+
// =========================================================================
|
|
2795
|
+
// Displacement Mapping \u2014 vertex offset along normal
|
|
2796
|
+
// =========================================================================
|
|
2797
|
+
|
|
2798
|
+
fn displacementMap(
|
|
2799
|
+
position: vec3<f32>, normal: vec3<f32>,
|
|
2800
|
+
height: f32, strength: f32, midLevel: f32
|
|
2801
|
+
) -> vec3<f32> {
|
|
2802
|
+
let offset = (height - midLevel) * strength;
|
|
2803
|
+
return position + normal * offset;
|
|
2804
|
+
}
|
|
2805
|
+
|
|
2806
|
+
// Reconstruct normal from height field via finite differences
|
|
2807
|
+
fn displacementNormal(
|
|
2808
|
+
uv: vec2<f32>, texelSize: f32, strength: f32
|
|
2809
|
+
) -> vec3<f32> {
|
|
2810
|
+
let hL = simpleNoise(uv - vec2<f32>(texelSize, 0.0)) * strength;
|
|
2811
|
+
let hR = simpleNoise(uv + vec2<f32>(texelSize, 0.0)) * strength;
|
|
2812
|
+
let hD = simpleNoise(uv - vec2<f32>(0.0, texelSize)) * strength;
|
|
2813
|
+
let hU = simpleNoise(uv + vec2<f32>(0.0, texelSize)) * strength;
|
|
2814
|
+
return normalize(vec3<f32>(hL - hR, 2.0 * texelSize, hD - hU));
|
|
2815
|
+
}
|
|
2816
|
+
|
|
2817
|
+
// Anisotropic displacement along normal with tangent-space weighting
|
|
2818
|
+
fn anisotropicDisplacement(
|
|
2819
|
+
position: vec3<f32>, normal: vec3<f32>, tangent: vec3<f32>,
|
|
2820
|
+
height: f32, strength: f32, anisotropy: f32, midLevel: f32
|
|
2821
|
+
) -> vec3<f32> {
|
|
2822
|
+
let offset = (height - midLevel) * strength;
|
|
2823
|
+
let bitangent = cross(normal, tangent);
|
|
2824
|
+
// Blend between isotropic (normal) and anisotropic (tangent-weighted) displacement
|
|
2825
|
+
let isoDisp = normal * offset;
|
|
2826
|
+
let anisoDisp = (tangent * anisotropy + normal * (1.0 - abs(anisotropy))) * offset;
|
|
2827
|
+
return position + mix(isoDisp, anisoDisp, abs(anisotropy));
|
|
2828
|
+
}
|
|
2829
|
+
|
|
2830
|
+
// =========================================================================
|
|
2831
|
+
// Parallax Occlusion Mapping \u2014 steep parallax with occlusion
|
|
2832
|
+
// =========================================================================
|
|
2833
|
+
|
|
2834
|
+
fn parallaxOcclusionMap(
|
|
2835
|
+
uv: vec2<f32>, viewDir: vec3<f32>,
|
|
2836
|
+
heightScale: f32, numLayers: f32
|
|
2837
|
+
) -> vec2<f32> {
|
|
2838
|
+
let layerCount = i32(numLayers);
|
|
2839
|
+
let layerDepth = 1.0 / numLayers;
|
|
2840
|
+
var currentLayerDepth = 0.0;
|
|
2841
|
+
let deltaUV = viewDir.xy / viewDir.z * heightScale / numLayers;
|
|
2842
|
+
|
|
2843
|
+
var currentUV = uv;
|
|
2844
|
+
var currentDepthMapValue = simpleNoise(currentUV); // Use noise as height
|
|
2845
|
+
|
|
2846
|
+
// Step through layers from front to back
|
|
2847
|
+
for (var i = 0; i < layerCount; i++) {
|
|
2848
|
+
if (currentLayerDepth >= currentDepthMapValue) { break; }
|
|
2849
|
+
currentUV -= deltaUV;
|
|
2850
|
+
currentDepthMapValue = simpleNoise(currentUV);
|
|
2851
|
+
currentLayerDepth += layerDepth;
|
|
2852
|
+
}
|
|
2853
|
+
|
|
2854
|
+
// Binary refinement for smoother result
|
|
2855
|
+
let prevUV = currentUV + deltaUV;
|
|
2856
|
+
let afterDepth = currentDepthMapValue - currentLayerDepth;
|
|
2857
|
+
let beforeDepth = simpleNoise(prevUV) - currentLayerDepth + layerDepth;
|
|
2858
|
+
let weight = afterDepth / (afterDepth - beforeDepth);
|
|
2859
|
+
return mix(currentUV, prevUV, weight);
|
|
2860
|
+
}
|
|
2861
|
+
|
|
2862
|
+
// =========================================================================
|
|
2863
|
+
// Screen-Space Reflections (SSR) \u2014 ray march in screen space
|
|
2864
|
+
// =========================================================================
|
|
2865
|
+
|
|
2866
|
+
fn screenSpaceReflect(
|
|
2867
|
+
uv: vec2<f32>, normal: vec3<f32>, viewDir: vec3<f32>,
|
|
2868
|
+
roughness: f32, maxSteps: f32, stepSize: f32
|
|
2869
|
+
) -> vec3<f32> {
|
|
2870
|
+
// Reflect view direction around surface normal
|
|
2871
|
+
let reflectDir = reflect(viewDir, normal);
|
|
2872
|
+
|
|
2873
|
+
// Convert reflection to screen-space step direction
|
|
2874
|
+
let screenStep = reflectDir.xy * stepSize;
|
|
2875
|
+
|
|
2876
|
+
var hitColor = vec3<f32>(0.0);
|
|
2877
|
+
var hitMask = 0.0;
|
|
2878
|
+
var sampleUV = uv;
|
|
2879
|
+
var currentDepth = 0.0;
|
|
2880
|
+
let steps = i32(maxSteps);
|
|
2881
|
+
|
|
2882
|
+
for (var i = 0; i < steps; i++) {
|
|
2883
|
+
sampleUV += screenStep;
|
|
2884
|
+
|
|
2885
|
+
// Bounds check
|
|
2886
|
+
if (sampleUV.x < 0.0 || sampleUV.x > 1.0 || sampleUV.y < 0.0 || sampleUV.y > 1.0) {
|
|
2887
|
+
break;
|
|
2888
|
+
}
|
|
2889
|
+
|
|
2890
|
+
// In a real implementation, we'd sample the depth buffer here
|
|
2891
|
+
// For the shader graph, we approximate with noise-based depth
|
|
2892
|
+
let sampleDepth = simpleNoise(sampleUV * 10.0);
|
|
2893
|
+
currentDepth += stepSize;
|
|
2894
|
+
|
|
2895
|
+
if (currentDepth > sampleDepth) {
|
|
2896
|
+
// Hit \u2014 sample color at hit point
|
|
2897
|
+
hitColor = vec3<f32>(sampleUV, 0.5); // Placeholder: real impl samples color buffer
|
|
2898
|
+
hitMask = 1.0;
|
|
2899
|
+
|
|
2900
|
+
// Fade with roughness (rough surfaces \u2192 less clear reflection)
|
|
2901
|
+
hitMask *= 1.0 - roughness;
|
|
2902
|
+
|
|
2903
|
+
// Fade at screen edges
|
|
2904
|
+
let edgeFade = 1.0 - pow(max(abs(sampleUV.x - 0.5), abs(sampleUV.y - 0.5)) * 2.0, 4.0);
|
|
2905
|
+
hitMask *= max(edgeFade, 0.0);
|
|
2906
|
+
break;
|
|
2907
|
+
}
|
|
2908
|
+
}
|
|
2909
|
+
|
|
2910
|
+
return hitColor * hitMask;
|
|
2911
|
+
}
|
|
2912
|
+
|
|
2913
|
+
// =========================================================================
|
|
2914
|
+
// Screen-Space Global Illumination (SSGI) \u2014 approximate indirect lighting
|
|
2915
|
+
// =========================================================================
|
|
2916
|
+
|
|
2917
|
+
fn screenSpaceGI(
|
|
2918
|
+
uv: vec2<f32>, normal: vec3<f32>,
|
|
2919
|
+
radius: f32, samples: f32, bounceIntensity: f32
|
|
2920
|
+
) -> vec3<f32> {
|
|
2921
|
+
var indirect = vec3<f32>(0.0);
|
|
2922
|
+
let sampleCount = i32(samples);
|
|
2923
|
+
let goldenAngle = 2.399963; // Golden angle in radians
|
|
2924
|
+
|
|
2925
|
+
for (var i = 0; i < sampleCount; i++) {
|
|
2926
|
+
let fi = f32(i);
|
|
2927
|
+
// Fibonacci spiral sampling pattern
|
|
2928
|
+
let r = sqrt(fi / samples) * radius;
|
|
2929
|
+
let theta = fi * goldenAngle;
|
|
2930
|
+
let offset = vec2<f32>(cos(theta), sin(theta)) * r;
|
|
2931
|
+
let sampleUV = uv + offset * 0.01; // Scale to UV space
|
|
2932
|
+
|
|
2933
|
+
// Sample nearby color (in real impl, from color buffer)
|
|
2934
|
+
let sampleColor = vec3<f32>(
|
|
2935
|
+
simpleNoise(sampleUV * 20.0),
|
|
2936
|
+
simpleNoise(sampleUV * 20.0 + vec2<f32>(37.0, 0.0)),
|
|
2937
|
+
simpleNoise(sampleUV * 20.0 + vec2<f32>(0.0, 53.0))
|
|
2938
|
+
);
|
|
2939
|
+
|
|
2940
|
+
// Weight by cosine of angle between sample direction and normal
|
|
2941
|
+
let sampleDir = normalize(vec3<f32>(offset, 0.1));
|
|
2942
|
+
let cosWeight = max(dot(sampleDir, normal), 0.0);
|
|
2943
|
+
|
|
2944
|
+
indirect += sampleColor * cosWeight;
|
|
2945
|
+
}
|
|
2946
|
+
|
|
2947
|
+
indirect /= samples;
|
|
2948
|
+
return indirect * bounceIntensity;
|
|
2949
|
+
}
|
|
2950
|
+
|
|
2951
|
+
// =========================================================================
|
|
2952
|
+
// Water Surface \u2014 combined waves, normals, foam, caustics
|
|
2953
|
+
// =========================================================================
|
|
2954
|
+
|
|
2955
|
+
struct WaterSurfaceResult {
|
|
2956
|
+
displacement: vec3<f32>,
|
|
2957
|
+
normal: vec3<f32>,
|
|
2958
|
+
foam: f32,
|
|
2959
|
+
caustic: f32,
|
|
2960
|
+
}
|
|
2961
|
+
|
|
2962
|
+
fn gerstnerWave(pos: vec2<f32>, time: f32, dir: vec2<f32>, steepness: f32, wavelength: f32) -> vec3<f32> {
|
|
2963
|
+
let k = 6.28318 / wavelength;
|
|
2964
|
+
let c = sqrt(9.81 / k);
|
|
2965
|
+
let d = normalize(dir);
|
|
2966
|
+
let f = k * (dot(d, pos) - c * time);
|
|
2967
|
+
let a = steepness / k;
|
|
2968
|
+
return vec3<f32>(d.x * a * cos(f), a * sin(f), d.y * a * cos(f));
|
|
2969
|
+
}
|
|
2970
|
+
|
|
2971
|
+
fn waterSurface(
|
|
2972
|
+
pos: vec3<f32>, time: f32,
|
|
2973
|
+
waveScale: f32, waveSpeed: f32,
|
|
2974
|
+
waterDepth: f32, foamThreshold: f32
|
|
2975
|
+
) -> WaterSurfaceResult {
|
|
2976
|
+
let scaledTime = time * waveSpeed;
|
|
2977
|
+
let p = pos.xz * waveScale;
|
|
2978
|
+
|
|
2979
|
+
// Sum of Gerstner waves at different frequencies and directions
|
|
2980
|
+
var displacement = vec3<f32>(0.0);
|
|
2981
|
+
displacement += gerstnerWave(p, scaledTime, vec2<f32>(1.0, 0.3), 0.25, 4.0);
|
|
2982
|
+
displacement += gerstnerWave(p, scaledTime, vec2<f32>(-0.5, 0.8), 0.15, 2.5);
|
|
2983
|
+
displacement += gerstnerWave(p, scaledTime, vec2<f32>(0.3, -0.7), 0.1, 1.5);
|
|
2984
|
+
displacement += gerstnerWave(p, scaledTime, vec2<f32>(-0.8, -0.3), 0.08, 0.8);
|
|
2985
|
+
|
|
2986
|
+
// Compute normal via finite differences on all 4 wave layers
|
|
2987
|
+
let eps = 0.01;
|
|
2988
|
+
let px = p + vec2<f32>(eps, 0.0);
|
|
2989
|
+
let pz = p + vec2<f32>(0.0, eps);
|
|
2990
|
+
var dx = vec3<f32>(0.0);
|
|
2991
|
+
dx += gerstnerWave(px, scaledTime, vec2<f32>(1.0, 0.3), 0.25, 4.0);
|
|
2992
|
+
dx += gerstnerWave(px, scaledTime, vec2<f32>(-0.5, 0.8), 0.15, 2.5);
|
|
2993
|
+
dx += gerstnerWave(px, scaledTime, vec2<f32>(0.3, -0.7), 0.1, 1.5);
|
|
2994
|
+
dx += gerstnerWave(px, scaledTime, vec2<f32>(-0.8, -0.3), 0.08, 0.8);
|
|
2995
|
+
var dz = vec3<f32>(0.0);
|
|
2996
|
+
dz += gerstnerWave(pz, scaledTime, vec2<f32>(1.0, 0.3), 0.25, 4.0);
|
|
2997
|
+
dz += gerstnerWave(pz, scaledTime, vec2<f32>(-0.5, 0.8), 0.15, 2.5);
|
|
2998
|
+
dz += gerstnerWave(pz, scaledTime, vec2<f32>(0.3, -0.7), 0.1, 1.5);
|
|
2999
|
+
dz += gerstnerWave(pz, scaledTime, vec2<f32>(-0.8, -0.3), 0.08, 0.8);
|
|
3000
|
+
let waterNormal = normalize(vec3<f32>(
|
|
3001
|
+
-(dx.y - displacement.y) / eps,
|
|
3002
|
+
1.0,
|
|
3003
|
+
-(dz.y - displacement.y) / eps
|
|
3004
|
+
));
|
|
3005
|
+
|
|
3006
|
+
// Foam: where wave crests converge (Jacobian < threshold)
|
|
3007
|
+
let jacobian = 1.0 - abs(displacement.x) * waveScale;
|
|
3008
|
+
let foam = smoothstep(foamThreshold, foamThreshold + 0.1, 1.0 - jacobian);
|
|
3009
|
+
|
|
3010
|
+
// Caustics (project through water depth)
|
|
3011
|
+
let causticsUV = p + displacement.xz * 0.5;
|
|
3012
|
+
let c1 = causticVoronoi(causticsUV * 3.0 + vec2<f32>(scaledTime * 0.3, 0.0));
|
|
3013
|
+
let caustic = pow(1.0 - c1, 4.0) * exp(-waterDepth * 0.3);
|
|
3014
|
+
|
|
3015
|
+
return WaterSurfaceResult(displacement, waterNormal, foam, caustic);
|
|
3016
|
+
}
|
|
3017
|
+
`;
|
|
3018
|
+
}
|
|
3019
|
+
};
|
|
3020
|
+
function compileShaderGraph(graph, options) {
|
|
3021
|
+
const compiler = new ShaderGraphCompiler(graph, options);
|
|
3022
|
+
return compiler.compile();
|
|
3023
|
+
}
|
|
3024
|
+
export {
|
|
3025
|
+
ADVANCED_MATERIAL_NODES,
|
|
3026
|
+
ALL_NODE_TEMPLATES,
|
|
3027
|
+
COLOR_NODES,
|
|
3028
|
+
INPUT_NODES,
|
|
3029
|
+
MATH_NODES,
|
|
3030
|
+
OUTPUT_NODES,
|
|
3031
|
+
SCREEN_SPACE_NODES,
|
|
3032
|
+
ShaderGraph,
|
|
3033
|
+
ShaderGraphCompiler,
|
|
3034
|
+
TEXTURE_NODES,
|
|
3035
|
+
TRIG_NODES,
|
|
3036
|
+
TYPE_SIZES,
|
|
3037
|
+
UTILITY_NODES,
|
|
3038
|
+
VECTOR_NODES,
|
|
3039
|
+
VOLUMETRIC_NODES,
|
|
3040
|
+
areTypesCompatible,
|
|
3041
|
+
compileShaderGraph,
|
|
3042
|
+
getNodeTemplate,
|
|
3043
|
+
getTypeConversion
|
|
3044
|
+
};
|