reze-engine 0.41.0 → 0.41.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -2
- package/dist/engine.d.ts +31 -3
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +190 -18
- package/dist/graph/presets/body.js +2 -2
- package/dist/graph/presets/cloth_rough.js +2 -2
- package/dist/graph/presets/cloth_smooth.js +2 -2
- package/dist/graph/presets/default.js +2 -2
- package/dist/graph/presets/eye.js +2 -2
- package/dist/graph/presets/face.js +2 -2
- package/dist/graph/presets/hair.js +2 -2
- package/dist/graph/presets/metal.js +2 -2
- package/dist/graph/presets/stockings.js +3 -3
- package/dist/graph/registry.d.ts.map +1 -1
- package/dist/graph/registry.js +426 -10
- package/dist/graph/style-group.d.ts +46 -0
- package/dist/graph/style-group.d.ts.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/shaders/agx-lut.d.ts +11 -0
- package/dist/shaders/agx-lut.d.ts.map +1 -0
- package/dist/shaders/agx-lut.js +29 -0
- package/dist/shaders/materials/common.d.ts +1 -1
- package/dist/shaders/materials/common.d.ts.map +1 -1
- package/dist/shaders/materials/common.js +10 -0
- package/dist/shaders/materials/nodes.d.ts +1 -1
- package/dist/shaders/materials/nodes.d.ts.map +1 -1
- package/dist/shaders/materials/nodes.js +957 -576
- package/dist/shaders/passes/composite.d.ts.map +1 -1
- package/dist/shaders/passes/composite.js +76 -4
- package/package.json +1 -1
- package/src/engine.ts +211 -17
- package/src/graph/presets/body.ts +2 -2
- package/src/graph/presets/cloth_rough.ts +2 -2
- package/src/graph/presets/cloth_smooth.ts +2 -2
- package/src/graph/presets/default.ts +2 -2
- package/src/graph/presets/eye.ts +2 -2
- package/src/graph/presets/face.ts +2 -2
- package/src/graph/presets/hair.ts +2 -2
- package/src/graph/presets/metal.ts +2 -2
- package/src/graph/presets/stockings.ts +3 -3
- package/src/graph/registry.ts +439 -10
- package/src/graph/style-group.ts +44 -0
- package/src/index.ts +2 -0
- package/src/shaders/agx-lut.ts +30 -0
- package/src/shaders/materials/common.ts +10 -0
- package/src/shaders/materials/nodes.ts +962 -581
- package/src/shaders/passes/composite.ts +76 -4
|
@@ -1,580 +1,961 @@
|
|
|
1
1
|
// Shared WGSL primitives for Blender-style NPR material nodes.
|
|
2
2
|
// Every function here maps 1:1 to a Blender shader node type used in the preset JSONs.
|
|
3
3
|
// Hand-ported material shaders concatenate this block before their own code.
|
|
4
|
-
export const NODES_WGSL = /* wgsl */ `
|
|
5
|
-
|
|
6
|
-
// Baked 64×64 rgba8unorm combined BRDF LUT — created once at engine init by dfg_lut.ts.
|
|
7
|
-
// .rg = split-sum DFG (Karis: tint = f0·x + f90·y) → F_brdf_*_scatter
|
|
8
|
-
// .ba = Heitz 2016 LTC magnitude (ltc_mag_ggx) → ltc_brdf_scale_from_lut
|
|
9
|
-
// Paired with group(0) binding(2) diffuseSampler (linear filter). Sample once per
|
|
10
|
-
// fragment via brdf_lut_sample() — callers feed .rg and the whole vec4 into the
|
|
11
|
-
// helpers below, halving LUT taps on the default Principled path.
|
|
12
|
-
@group(0) @binding(9) var brdfLut: texture_2d<f32>;
|
|
13
|
-
|
|
14
|
-
// ─── RGB ↔ HSV ──────────────────────────────────────────────────────
|
|
15
|
-
|
|
16
|
-
fn rgb_to_hsv(rgb: vec3f) -> vec3f {
|
|
17
|
-
let c_max = max(rgb.r, max(rgb.g, rgb.b));
|
|
18
|
-
let c_min = min(rgb.r, min(rgb.g, rgb.b));
|
|
19
|
-
let delta = c_max - c_min;
|
|
20
|
-
|
|
21
|
-
var h = 0.0;
|
|
22
|
-
if (delta > 1e-6) {
|
|
23
|
-
if (c_max == rgb.r) {
|
|
24
|
-
h = (rgb.g - rgb.b) / delta;
|
|
25
|
-
if (h < 0.0) { h += 6.0; }
|
|
26
|
-
} else if (c_max == rgb.g) {
|
|
27
|
-
h = 2.0 + (rgb.b - rgb.r) / delta;
|
|
28
|
-
} else {
|
|
29
|
-
h = 4.0 + (rgb.r - rgb.g) / delta;
|
|
30
|
-
}
|
|
31
|
-
h /= 6.0;
|
|
32
|
-
}
|
|
33
|
-
let s = select(0.0, delta / c_max, c_max > 1e-6);
|
|
34
|
-
return vec3f(h, s, c_max);
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
fn hsv_to_rgb(hsv: vec3f) -> vec3f {
|
|
38
|
-
let h = hsv.x;
|
|
39
|
-
let s = hsv.y;
|
|
40
|
-
let v = hsv.z;
|
|
41
|
-
if (s < 1e-6) { return vec3f(v); }
|
|
42
|
-
|
|
43
|
-
let hh = fract(h) * 6.0;
|
|
44
|
-
let sector = u32(hh);
|
|
45
|
-
let f = hh - f32(sector);
|
|
46
|
-
let p = v * (1.0 - s);
|
|
47
|
-
let q = v * (1.0 - s * f);
|
|
48
|
-
let t = v * (1.0 - s * (1.0 - f));
|
|
49
|
-
|
|
50
|
-
switch (sector) {
|
|
51
|
-
case 0u: { return vec3f(v, t, p); }
|
|
52
|
-
case 1u: { return vec3f(q, v, p); }
|
|
53
|
-
case 2u: { return vec3f(p, v, t); }
|
|
54
|
-
case 3u: { return vec3f(p, q, v); }
|
|
55
|
-
case 4u: { return vec3f(t, p, v); }
|
|
56
|
-
default: { return vec3f(v, p, q); }
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
// ─── HUE_SAT node ───────────────────────────────────────────────────
|
|
61
|
-
|
|
62
|
-
fn hue_sat(hue: f32, saturation: f32, value: f32, fac: f32, color: vec3f) -> vec3f {
|
|
63
|
-
var hsv = rgb_to_hsv(color);
|
|
64
|
-
hsv.x = fract(hsv.x + hue - 0.5);
|
|
65
|
-
hsv.y = clamp(hsv.y * saturation, 0.0, 1.0);
|
|
66
|
-
hsv.z *= value;
|
|
67
|
-
return mix(color, hsv_to_rgb(hsv), fac);
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
// hue_sat specialization for hue=0.5 (identity hue shift — fract(h + 0.5 - 0.5) = h).
|
|
71
|
-
// Branchless equivalent that skips the rgb_to_hsv → hsv_to_rgb roundtrip: WebKit's
|
|
72
|
-
// Metal backend serializes the 3-way if chain in rgb_to_hsv and the 6-way switch in
|
|
73
|
-
// hsv_to_rgb, where this form compiles to linear SIMD ops + a single select.
|
|
74
|
-
fn hue_sat_id(saturation: f32, value: f32, fac: f32, color: vec3f) -> vec3f {
|
|
75
|
-
let m = max(max(color.r, color.g), color.b);
|
|
76
|
-
let n = min(min(color.r, color.g), color.b);
|
|
77
|
-
// Unclamped (sat*old_s ≤ 1): reproj = mix(vec3f(m), color, saturation).
|
|
78
|
-
// Clamped (saturated to 1): reproj = (color - n) * m / (m - n).
|
|
79
|
-
let range = max(m - n, 1e-6);
|
|
80
|
-
let unclamped = mix(vec3f(m), color, saturation);
|
|
81
|
-
let clamped = (color - vec3f(n)) * m / range;
|
|
82
|
-
let needs_clamp = (m - n) * saturation >= m;
|
|
83
|
-
let reproj = select(unclamped, clamped, needs_clamp);
|
|
84
|
-
return mix(color, reproj * value, fac);
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
// ─── BRIGHTCONTRAST node ────────────────────────────────────────────
|
|
88
|
-
|
|
89
|
-
fn bright_contrast(color: vec3f, bright: f32, contrast: f32) -> vec3f {
|
|
90
|
-
let a = 1.0 + contrast;
|
|
91
|
-
let b = bright - contrast * 0.5;
|
|
92
|
-
return max(vec3f(0.0), color * a + vec3f(b));
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
// ─── INVERT node ────────────────────────────────────────────────────
|
|
96
|
-
|
|
97
|
-
fn invert(fac: f32, color: vec3f) -> vec3f {
|
|
98
|
-
return mix(color, vec3f(1.0) - color, fac);
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
fn invert_f(fac: f32, val: f32) -> f32 {
|
|
102
|
-
return mix(val, 1.0 - val, fac);
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
// ─── Color ramp (VALTORGB) — 2-stop variants ───────────────────────
|
|
106
|
-
// All 7 presets use exclusively 2-stop ramps.
|
|
107
|
-
|
|
108
|
-
fn ramp_constant(f: f32, p0: f32, c0: vec4f, p1: f32, c1: vec4f) -> vec4f {
|
|
109
|
-
return select(c0, c1, f >= p1);
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
// CONSTANT ramp with screen-space edge AA — kills sparkle where fwidth(f) straddles a hard step (NPR terminator)
|
|
113
|
-
fn ramp_constant_edge_aa(f: f32, edge: f32, c0: vec4f, c1: vec4f) -> vec4f {
|
|
114
|
-
let w = max(fwidth(f) * 1.75, 6e-6);
|
|
115
|
-
let t = smoothstep(edge - w, edge + w, f);
|
|
116
|
-
return mix(c0, c1, t);
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
fn ramp_linear(f: f32, p0: f32, c0: vec4f, p1: f32, c1: vec4f) -> vec4f {
|
|
120
|
-
let t = saturate((f - p0) / max(p1 - p0, 1e-6));
|
|
121
|
-
return mix(c0, c1, t);
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
fn
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
//
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
fn
|
|
148
|
-
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
fn
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
fn
|
|
159
|
-
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
fn
|
|
163
|
-
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
fn
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
fn
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
let
|
|
219
|
-
let
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
let
|
|
229
|
-
let
|
|
230
|
-
let
|
|
231
|
-
let
|
|
232
|
-
let
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
fn
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
let
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
let
|
|
265
|
-
let
|
|
266
|
-
let
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
//
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
fn
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
return
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
let
|
|
345
|
-
|
|
346
|
-
let
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
fn
|
|
395
|
-
let
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
let
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
}
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
return (
|
|
474
|
-
}
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
let
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
)
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
4
|
+
export const NODES_WGSL = /* wgsl */ `
|
|
5
|
+
|
|
6
|
+
// Baked 64×64 rgba8unorm combined BRDF LUT — created once at engine init by dfg_lut.ts.
|
|
7
|
+
// .rg = split-sum DFG (Karis: tint = f0·x + f90·y) → F_brdf_*_scatter
|
|
8
|
+
// .ba = Heitz 2016 LTC magnitude (ltc_mag_ggx) → ltc_brdf_scale_from_lut
|
|
9
|
+
// Paired with group(0) binding(2) diffuseSampler (linear filter). Sample once per
|
|
10
|
+
// fragment via brdf_lut_sample() — callers feed .rg and the whole vec4 into the
|
|
11
|
+
// helpers below, halving LUT taps on the default Principled path.
|
|
12
|
+
@group(0) @binding(9) var brdfLut: texture_2d<f32>;
|
|
13
|
+
|
|
14
|
+
// ─── RGB ↔ HSV ──────────────────────────────────────────────────────
|
|
15
|
+
|
|
16
|
+
fn rgb_to_hsv(rgb: vec3f) -> vec3f {
|
|
17
|
+
let c_max = max(rgb.r, max(rgb.g, rgb.b));
|
|
18
|
+
let c_min = min(rgb.r, min(rgb.g, rgb.b));
|
|
19
|
+
let delta = c_max - c_min;
|
|
20
|
+
|
|
21
|
+
var h = 0.0;
|
|
22
|
+
if (delta > 1e-6) {
|
|
23
|
+
if (c_max == rgb.r) {
|
|
24
|
+
h = (rgb.g - rgb.b) / delta;
|
|
25
|
+
if (h < 0.0) { h += 6.0; }
|
|
26
|
+
} else if (c_max == rgb.g) {
|
|
27
|
+
h = 2.0 + (rgb.b - rgb.r) / delta;
|
|
28
|
+
} else {
|
|
29
|
+
h = 4.0 + (rgb.r - rgb.g) / delta;
|
|
30
|
+
}
|
|
31
|
+
h /= 6.0;
|
|
32
|
+
}
|
|
33
|
+
let s = select(0.0, delta / c_max, c_max > 1e-6);
|
|
34
|
+
return vec3f(h, s, c_max);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
fn hsv_to_rgb(hsv: vec3f) -> vec3f {
|
|
38
|
+
let h = hsv.x;
|
|
39
|
+
let s = hsv.y;
|
|
40
|
+
let v = hsv.z;
|
|
41
|
+
if (s < 1e-6) { return vec3f(v); }
|
|
42
|
+
|
|
43
|
+
let hh = fract(h) * 6.0;
|
|
44
|
+
let sector = u32(hh);
|
|
45
|
+
let f = hh - f32(sector);
|
|
46
|
+
let p = v * (1.0 - s);
|
|
47
|
+
let q = v * (1.0 - s * f);
|
|
48
|
+
let t = v * (1.0 - s * (1.0 - f));
|
|
49
|
+
|
|
50
|
+
switch (sector) {
|
|
51
|
+
case 0u: { return vec3f(v, t, p); }
|
|
52
|
+
case 1u: { return vec3f(q, v, p); }
|
|
53
|
+
case 2u: { return vec3f(p, v, t); }
|
|
54
|
+
case 3u: { return vec3f(p, q, v); }
|
|
55
|
+
case 4u: { return vec3f(t, p, v); }
|
|
56
|
+
default: { return vec3f(v, p, q); }
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// ─── HUE_SAT node ───────────────────────────────────────────────────
|
|
61
|
+
|
|
62
|
+
fn hue_sat(hue: f32, saturation: f32, value: f32, fac: f32, color: vec3f) -> vec3f {
|
|
63
|
+
var hsv = rgb_to_hsv(color);
|
|
64
|
+
hsv.x = fract(hsv.x + hue - 0.5);
|
|
65
|
+
hsv.y = clamp(hsv.y * saturation, 0.0, 1.0);
|
|
66
|
+
hsv.z *= value;
|
|
67
|
+
return mix(color, hsv_to_rgb(hsv), fac);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// hue_sat specialization for hue=0.5 (identity hue shift — fract(h + 0.5 - 0.5) = h).
|
|
71
|
+
// Branchless equivalent that skips the rgb_to_hsv → hsv_to_rgb roundtrip: WebKit's
|
|
72
|
+
// Metal backend serializes the 3-way if chain in rgb_to_hsv and the 6-way switch in
|
|
73
|
+
// hsv_to_rgb, where this form compiles to linear SIMD ops + a single select.
|
|
74
|
+
fn hue_sat_id(saturation: f32, value: f32, fac: f32, color: vec3f) -> vec3f {
|
|
75
|
+
let m = max(max(color.r, color.g), color.b);
|
|
76
|
+
let n = min(min(color.r, color.g), color.b);
|
|
77
|
+
// Unclamped (sat*old_s ≤ 1): reproj = mix(vec3f(m), color, saturation).
|
|
78
|
+
// Clamped (saturated to 1): reproj = (color - n) * m / (m - n).
|
|
79
|
+
let range = max(m - n, 1e-6);
|
|
80
|
+
let unclamped = mix(vec3f(m), color, saturation);
|
|
81
|
+
let clamped = (color - vec3f(n)) * m / range;
|
|
82
|
+
let needs_clamp = (m - n) * saturation >= m;
|
|
83
|
+
let reproj = select(unclamped, clamped, needs_clamp);
|
|
84
|
+
return mix(color, reproj * value, fac);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// ─── BRIGHTCONTRAST node ────────────────────────────────────────────
|
|
88
|
+
|
|
89
|
+
fn bright_contrast(color: vec3f, bright: f32, contrast: f32) -> vec3f {
|
|
90
|
+
let a = 1.0 + contrast;
|
|
91
|
+
let b = bright - contrast * 0.5;
|
|
92
|
+
return max(vec3f(0.0), color * a + vec3f(b));
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// ─── INVERT node ────────────────────────────────────────────────────
|
|
96
|
+
|
|
97
|
+
fn invert(fac: f32, color: vec3f) -> vec3f {
|
|
98
|
+
return mix(color, vec3f(1.0) - color, fac);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
fn invert_f(fac: f32, val: f32) -> f32 {
|
|
102
|
+
return mix(val, 1.0 - val, fac);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// ─── Color ramp (VALTORGB) — 2-stop variants ───────────────────────
|
|
106
|
+
// All 7 presets use exclusively 2-stop ramps.
|
|
107
|
+
|
|
108
|
+
fn ramp_constant(f: f32, p0: f32, c0: vec4f, p1: f32, c1: vec4f) -> vec4f {
|
|
109
|
+
return select(c0, c1, f >= p1);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// CONSTANT ramp with screen-space edge AA — kills sparkle where fwidth(f) straddles a hard step (NPR terminator)
|
|
113
|
+
fn ramp_constant_edge_aa(f: f32, edge: f32, c0: vec4f, c1: vec4f) -> vec4f {
|
|
114
|
+
let w = max(fwidth(f) * 1.75, 6e-6);
|
|
115
|
+
let t = smoothstep(edge - w, edge + w, f);
|
|
116
|
+
return mix(c0, c1, t);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
fn ramp_linear(f: f32, p0: f32, c0: vec4f, p1: f32, c1: vec4f) -> vec4f {
|
|
120
|
+
let t = saturate((f - p0) / max(p1 - p0, 1e-6));
|
|
121
|
+
return mix(c0, c1, t);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// LINEAR ramp, three stops. Both halves are evaluated and selected between rather
|
|
125
|
+
// than branched on, so there is no divergence; saturate outside each span already
|
|
126
|
+
// gives Blender's clamp-to-end-stop behaviour.
|
|
127
|
+
fn ramp_linear3(f: f32, p0: f32, c0: vec4f, p1: f32, c1: vec4f, p2: f32, c2: vec4f) -> vec4f {
|
|
128
|
+
let lo = mix(c0, c1, saturate((f - p0) / max(p1 - p0, 1e-6)));
|
|
129
|
+
let hi = mix(c1, c2, saturate((f - p1) / max(p2 - p1, 1e-6)));
|
|
130
|
+
return select(lo, hi, f >= p1);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
fn ramp_cardinal(f: f32, p0: f32, c0: vec4f, p1: f32, c1: vec4f) -> vec4f {
|
|
134
|
+
// cardinal spline with 2 stops degrades to smoothstep
|
|
135
|
+
let t = saturate((f - p0) / max(p1 - p0, 1e-6));
|
|
136
|
+
let ss = t * t * (3.0 - 2.0 * t);
|
|
137
|
+
return mix(c0, c1, ss);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// ─── MATH node operations ───────────────────────────────────────────
|
|
141
|
+
|
|
142
|
+
// Transcribed from Blender 5.2's gpu_shader_common_math.glsl, including the
|
|
143
|
+
// safe-guards — a Math node that divides by zero yields 0 there, not inf, and a
|
|
144
|
+
// graph written against that behaviour relies on it.
|
|
145
|
+
fn math_add(a: f32, b: f32) -> f32 { return a + b; }
|
|
146
|
+
fn math_subtract(a: f32, b: f32) -> f32 { return a - b; }
|
|
147
|
+
fn math_multiply(a: f32, b: f32) -> f32 { return a * b; }
|
|
148
|
+
fn math_divide(a: f32, b: f32) -> f32 { return select(0.0, a / b, b != 0.0); }
|
|
149
|
+
fn math_multiply_add(a: f32, b: f32, c: f32) -> f32 { return a * b + c; }
|
|
150
|
+
fn math_power(a: f32, b: f32) -> f32 { return pow(max(a, 0.0), b); }
|
|
151
|
+
fn math_logarithm(a: f32, b: f32) -> f32 { return select(0.0, log(a) / log(b), a > 0.0 && b > 0.0 && b != 1.0); }
|
|
152
|
+
fn math_sqrt(a: f32) -> f32 { return sqrt(max(a, 0.0)); }
|
|
153
|
+
fn math_inversesqrt(a: f32) -> f32 { return inverseSqrt(max(a, 1e-8)); }
|
|
154
|
+
fn math_absolute(a: f32) -> f32 { return abs(a); }
|
|
155
|
+
fn math_exponent(a: f32) -> f32 { return exp(a); }
|
|
156
|
+
fn math_minimum(a: f32, b: f32) -> f32 { return min(a, b); }
|
|
157
|
+
fn math_maximum(a: f32, b: f32) -> f32 { return max(a, b); }
|
|
158
|
+
fn math_less_than(a: f32, b: f32) -> f32 { return select(0.0, 1.0, a < b); }
|
|
159
|
+
fn math_greater_than(a: f32, b: f32) -> f32 { return select(0.0, 1.0, a > b); }
|
|
160
|
+
fn math_sign(a: f32) -> f32 { return sign(a); }
|
|
161
|
+
// The tolerance floors at 1e-5 in Blender, so a zero third input still compares.
|
|
162
|
+
fn math_compare(a: f32, b: f32, c: f32) -> f32 { return select(0.0, 1.0, abs(a - b) <= max(c, 1e-5)); }
|
|
163
|
+
fn math_smooth_min(a: f32, b: f32, c: f32) -> f32 {
|
|
164
|
+
if (c == 0.0) { return min(a, b); }
|
|
165
|
+
let h = max(c - abs(a - b), 0.0) / c;
|
|
166
|
+
return min(a, b) - h * h * h * c * (1.0 / 6.0);
|
|
167
|
+
}
|
|
168
|
+
fn math_smooth_max(a: f32, b: f32, c: f32) -> f32 { return -math_smooth_min(-a, -b, c); }
|
|
169
|
+
fn math_round(a: f32) -> f32 { return floor(a + 0.5); }
|
|
170
|
+
fn math_floor(a: f32) -> f32 { return floor(a); }
|
|
171
|
+
fn math_ceil(a: f32) -> f32 { return ceil(a); }
|
|
172
|
+
fn math_truncate(a: f32) -> f32 { return trunc(a); }
|
|
173
|
+
fn math_fraction(a: f32) -> f32 { return a - floor(a); }
|
|
174
|
+
// Blender's MODULO is fmod (truncated), not floorMod — the sign follows a.
|
|
175
|
+
fn math_modulo(a: f32, b: f32) -> f32 { return select(0.0, a - b * trunc(a / b), b != 0.0); }
|
|
176
|
+
fn math_floored_modulo(a: f32, b: f32) -> f32 { return select(0.0, a - floor(a / b) * b, b != 0.0); }
|
|
177
|
+
fn math_snap(a: f32, b: f32) -> f32 { return floor(select(0.0, a / b, b != 0.0)) * b; }
|
|
178
|
+
fn math_wrap(a: f32, b: f32, c: f32) -> f32 {
|
|
179
|
+
let range = b - c;
|
|
180
|
+
return select(c, a - range * floor((a - c) / range), range != 0.0);
|
|
181
|
+
}
|
|
182
|
+
fn math_pingpong(a: f32, b: f32) -> f32 {
|
|
183
|
+
return select(0.0, abs(fract((a - b) / (b * 2.0)) * b * 2.0 - b), b != 0.0);
|
|
184
|
+
}
|
|
185
|
+
fn math_sine(a: f32) -> f32 { return sin(a); }
|
|
186
|
+
fn math_cosine(a: f32) -> f32 { return cos(a); }
|
|
187
|
+
fn math_tangent(a: f32) -> f32 { return tan(a); }
|
|
188
|
+
fn math_arcsine(a: f32) -> f32 { return asin(clamp(a, -1.0, 1.0)); }
|
|
189
|
+
fn math_arccosine(a: f32) -> f32 { return acos(clamp(a, -1.0, 1.0)); }
|
|
190
|
+
fn math_arctangent(a: f32) -> f32 { return atan(a); }
|
|
191
|
+
fn math_arctan2(a: f32, b: f32) -> f32 { return atan2(a, b); }
|
|
192
|
+
fn math_radians(a: f32) -> f32 { return radians(a); }
|
|
193
|
+
fn math_degrees(a: f32) -> f32 { return degrees(a); }
|
|
194
|
+
|
|
195
|
+
// ─── RGB Curves, as five samples ──────────────────────────────────
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Blender's RGB Curves holds arbitrary control points, which cannot cross into a
|
|
199
|
+
* shader as sockets. So the curve arrives SAMPLED: five values at t = 0, .25,
|
|
200
|
+
* .5, .75, 1, and this rebuilds it with the same monotone (Fritsch-Carlson)
|
|
201
|
+
* interpolation the Filmic LUT uses — C1 continuous, and it cannot overshoot,
|
|
202
|
+
* which matters because a curve that overshoots turns a highlight roll-off into
|
|
203
|
+
* a bright ring.
|
|
204
|
+
*
|
|
205
|
+
* Five points captures the levels-and-contrast adjustments these curves are
|
|
206
|
+
* actually used for. A curve with more structure than that will not survive, and
|
|
207
|
+
* should be a ramp instead.
|
|
208
|
+
*/
|
|
209
|
+
fn curve_slope(d0: f32, d1: f32) -> f32 {
|
|
210
|
+
if (d0 * d1 <= 0.0) { return 0.0; }
|
|
211
|
+
let m = (d0 + d1) * 0.5;
|
|
212
|
+
return sign(d0) * min(abs(m), 2.0 * min(abs(d0), abs(d1)));
|
|
213
|
+
}
|
|
214
|
+
fn curve5(t0: f32, y0: f32, y1: f32, y2: f32, y3: f32, y4: f32) -> f32 {
|
|
215
|
+
let t = clamp(t0, 0.0, 1.0) * 4.0;
|
|
216
|
+
let i = min(floor(t), 3.0);
|
|
217
|
+
let f = t - i;
|
|
218
|
+
// A var, not a let: WGSL only allows a dynamic index on a REFERENCE, and a
|
|
219
|
+
// let-bound array is a value. Indexing it with a runtime k is a compile error —
|
|
220
|
+
// and because this file is concatenated into every material shader, that error
|
|
221
|
+
// took every graph in the library down with it, not just curves.
|
|
222
|
+
var ys = array<f32, 5>(y0, y1, y2, y3, y4);
|
|
223
|
+
let k = i32(i);
|
|
224
|
+
let pa = ys[k];
|
|
225
|
+
let pb = ys[k + 1];
|
|
226
|
+
// Secants either side of each knot, clamped at the ends.
|
|
227
|
+
let dPrev = select(ys[max(k, 1)] - ys[max(k, 1) - 1], pb - pa, k == 0);
|
|
228
|
+
let dHere = pb - pa;
|
|
229
|
+
let dNext = select(ys[min(k + 2, 4)] - ys[min(k + 1, 3)], pb - pa, k == 3);
|
|
230
|
+
let m0 = curve_slope(dPrev, dHere);
|
|
231
|
+
let m1 = curve_slope(dHere, dNext);
|
|
232
|
+
let f2 = f * f;
|
|
233
|
+
let f3 = f2 * f;
|
|
234
|
+
return (2.0 * f3 - 3.0 * f2 + 1.0) * pa + (f3 - 2.0 * f2 + f) * m0 +
|
|
235
|
+
(-2.0 * f3 + 3.0 * f2) * pb + (f3 - f2) * m1;
|
|
236
|
+
}
|
|
237
|
+
fn rgb_curve(c: vec3f, y0: f32, y1: f32, y2: f32, y3: f32, y4: f32) -> vec3f {
|
|
238
|
+
return vec3f(
|
|
239
|
+
curve5(c.r, y0, y1, y2, y3, y4),
|
|
240
|
+
curve5(c.g, y0, y1, y2, y3, y4),
|
|
241
|
+
curve5(c.b, y0, y1, y2, y3, y4),
|
|
242
|
+
);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// ─── Normal Map / Vector Transform ────────────────────────────────
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Normal Map, with the tangent frame derived rather than read.
|
|
249
|
+
*
|
|
250
|
+
* Blender reads a per-vertex tangent; PMX files do not carry one, so there is
|
|
251
|
+
* nothing to read. The frame is rebuilt per-pixel from screen-space derivatives
|
|
252
|
+
* of position and UV — the standard substitute, exact on flat-ish shading and
|
|
253
|
+
* off only where UVs are severely sheared. Cheaper than the alternative (running
|
|
254
|
+
* a tangent-generation pass over every imported mesh) and invisible on the
|
|
255
|
+
* character work this is for.
|
|
256
|
+
*/
|
|
257
|
+
fn node_normal_map(color: vec3f, strength: f32, N: vec3f, worldPos: vec3f, uv: vec2f) -> vec3f {
|
|
258
|
+
let dp1 = dpdx(worldPos);
|
|
259
|
+
let dp2 = dpdy(worldPos);
|
|
260
|
+
let duv1 = dpdx(uv);
|
|
261
|
+
let duv2 = dpdy(uv);
|
|
262
|
+
let det = duv1.x * duv2.y - duv2.x * duv1.y;
|
|
263
|
+
if (abs(det) < 1e-12) { return N; }
|
|
264
|
+
let inv = 1.0 / det;
|
|
265
|
+
let T = safe_normal((dp1 * duv2.y - dp2 * duv1.y) * inv);
|
|
266
|
+
let B = safe_normal(cross(N, T));
|
|
267
|
+
// Tangent-space [0,1] → [-1,1], then into world.
|
|
268
|
+
let t = color * 2.0 - vec3f(1.0);
|
|
269
|
+
let mapped = safe_normal(T * t.x + B * t.y + N * t.z);
|
|
270
|
+
return safe_normal(mix(N, mapped, clamp(strength, 0.0, 1.0)));
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/** Vector Transform, world ↔ camera. Object space IS world here: a PMX model is
|
|
274
|
+
* placed by its own transform and shaded in world space, so there is no
|
|
275
|
+
* separate object frame to convert into. */
|
|
276
|
+
fn vector_world_to_camera(v: vec3f) -> vec3f { return (camera.view * vec4f(v, 0.0)).xyz; }
|
|
277
|
+
fn vector_camera_to_world(v: vec3f) -> vec3f { return (transpose(camera.view) * vec4f(v, 0.0)).xyz; }
|
|
278
|
+
fn point_world_to_camera(p: vec3f) -> vec3f { return (camera.view * vec4f(p, 1.0)).xyz; }
|
|
279
|
+
|
|
280
|
+
// ─── Style-group image maps (Image Texture on a non-PMX slot) ──────
|
|
281
|
+
// One function per slot because WGSL has no texture arrays-of-bindings here; the
|
|
282
|
+
// slot is topology, exactly like a Math node's operation.
|
|
283
|
+
|
|
284
|
+
fn group_tex0(uv: vec2f) -> vec4f { return textureSample(groupTexture0, diffuseSampler, uv); }
|
|
285
|
+
fn group_tex1(uv: vec2f) -> vec4f { return textureSample(groupTexture1, diffuseSampler, uv); }
|
|
286
|
+
fn group_tex2(uv: vec2f) -> vec4f { return textureSample(groupTexture2, diffuseSampler, uv); }
|
|
287
|
+
fn group_tex3(uv: vec2f) -> vec4f { return textureSample(groupTexture3, diffuseSampler, uv); }
|
|
288
|
+
|
|
289
|
+
// ─── VECTOR MATH node operations ───────────────────────────────────
|
|
290
|
+
|
|
291
|
+
fn vector_add(a: vec3f, b: vec3f) -> vec3f { return a + b; }
|
|
292
|
+
fn vector_subtract(a: vec3f, b: vec3f) -> vec3f { return a - b; }
|
|
293
|
+
fn vector_multiply(a: vec3f, b: vec3f) -> vec3f { return a * b; }
|
|
294
|
+
fn vector_divide(a: vec3f, b: vec3f) -> vec3f {
|
|
295
|
+
return vec3f(math_divide(a.x, b.x), math_divide(a.y, b.y), math_divide(a.z, b.z));
|
|
296
|
+
}
|
|
297
|
+
fn vector_multiply_add(a: vec3f, b: vec3f, c: vec3f) -> vec3f { return a * b + c; }
|
|
298
|
+
fn vector_cross(a: vec3f, b: vec3f) -> vec3f { return cross(a, b); }
|
|
299
|
+
fn vector_project(a: vec3f, b: vec3f) -> vec3f {
|
|
300
|
+
let l = dot(b, b);
|
|
301
|
+
return select(vec3f(0.0), b * (dot(a, b) / l), l != 0.0);
|
|
302
|
+
}
|
|
303
|
+
fn vector_reflect(a: vec3f, b: vec3f) -> vec3f { return reflect(a, safe_normal(b)); }
|
|
304
|
+
fn vector_refract(a: vec3f, b: vec3f, ior: f32) -> vec3f { return refract(a, safe_normal(b), ior); }
|
|
305
|
+
fn vector_faceforward(a: vec3f, b: vec3f, c: vec3f) -> vec3f {
|
|
306
|
+
return select(a, -a, dot(b, c) < 0.0);
|
|
307
|
+
}
|
|
308
|
+
fn vector_dot(a: vec3f, b: vec3f) -> f32 { return dot(a, b); }
|
|
309
|
+
fn vector_distance(a: vec3f, b: vec3f) -> f32 { return length(a - b); }
|
|
310
|
+
fn vector_length(a: vec3f) -> f32 { return length(a); }
|
|
311
|
+
fn vector_scale(a: vec3f, s: f32) -> vec3f { return a * s; }
|
|
312
|
+
// Blender returns the zero vector rather than NaN for a degenerate input.
|
|
313
|
+
fn vector_normalize(a: vec3f) -> vec3f {
|
|
314
|
+
let l = length(a);
|
|
315
|
+
return select(vec3f(0.0), a / l, l > 0.0);
|
|
316
|
+
}
|
|
317
|
+
fn vector_absolute(a: vec3f) -> vec3f { return abs(a); }
|
|
318
|
+
fn vector_minimum(a: vec3f, b: vec3f) -> vec3f { return min(a, b); }
|
|
319
|
+
fn vector_maximum(a: vec3f, b: vec3f) -> vec3f { return max(a, b); }
|
|
320
|
+
fn vector_floor(a: vec3f) -> vec3f { return floor(a); }
|
|
321
|
+
fn vector_ceil(a: vec3f) -> vec3f { return ceil(a); }
|
|
322
|
+
fn vector_fraction(a: vec3f) -> vec3f { return a - floor(a); }
|
|
323
|
+
fn vector_modulo(a: vec3f, b: vec3f) -> vec3f {
|
|
324
|
+
return vec3f(math_modulo(a.x, b.x), math_modulo(a.y, b.y), math_modulo(a.z, b.z));
|
|
325
|
+
}
|
|
326
|
+
fn vector_snap(a: vec3f, b: vec3f) -> vec3f {
|
|
327
|
+
return vec3f(math_snap(a.x, b.x), math_snap(a.y, b.y), math_snap(a.z, b.z));
|
|
328
|
+
}
|
|
329
|
+
fn vector_wrap(a: vec3f, b: vec3f, c: vec3f) -> vec3f {
|
|
330
|
+
return vec3f(math_wrap(a.x, b.x, c.x), math_wrap(a.y, b.y, c.y), math_wrap(a.z, b.z, c.z));
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/** Vector Rotate, axis-angle form — Rodrigues about an arbitrary axis. */
|
|
334
|
+
fn vector_rotate_axis(v: vec3f, center: vec3f, axis: vec3f, angle: f32) -> vec3f {
|
|
335
|
+
let n = safe_normal(axis);
|
|
336
|
+
let p = v - center;
|
|
337
|
+
let c = cos(angle);
|
|
338
|
+
return center + p * c + cross(n, p) * sin(angle) + n * dot(n, p) * (1.0 - c);
|
|
339
|
+
}
|
|
340
|
+
fn vector_rotate_euler(v: vec3f, center: vec3f, rot: vec3f) -> vec3f {
|
|
341
|
+
let p = v - center;
|
|
342
|
+
let cx = cos(rot.x); let sx = sin(rot.x);
|
|
343
|
+
let cy = cos(rot.y); let sy = sin(rot.y);
|
|
344
|
+
let cz = cos(rot.z); let sz = sin(rot.z);
|
|
345
|
+
// XYZ order, matching Blender's default euler.
|
|
346
|
+
let rx = vec3f(p.x, p.y * cx - p.z * sx, p.y * sx + p.z * cx);
|
|
347
|
+
let ry = vec3f(rx.x * cy + rx.z * sy, rx.y, -rx.x * sy + rx.z * cy);
|
|
348
|
+
return center + vec3f(ry.x * cz - ry.y * sz, ry.x * sz + ry.y * cz, ry.z);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// Blender's implicit Color → Float socket conversion uses BT.601 grayscale
|
|
352
|
+
// (rgb_to_grayscale in blenkernel/intern/node.cc). When a material graph plugs a
|
|
353
|
+
// Color output into a Math node's Value input, this is the scalar it actually sees.
|
|
354
|
+
fn color_to_value(c: vec3f) -> f32 {
|
|
355
|
+
return 0.299 * c.r + 0.587 * c.g + 0.114 * c.b;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// ─── MIX node (blend_type variants) ────────────────────────────────
|
|
359
|
+
|
|
360
|
+
fn mix_blend(fac: f32, a: vec3f, b: vec3f) -> vec3f {
|
|
361
|
+
return mix(a, b, fac);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
fn mix_overlay(fac: f32, a: vec3f, b: vec3f) -> vec3f {
|
|
365
|
+
let lo = 2.0 * a * b;
|
|
366
|
+
let hi = vec3f(1.0) - 2.0 * (vec3f(1.0) - a) * (vec3f(1.0) - b);
|
|
367
|
+
let overlay = select(hi, lo, a < vec3f(0.5));
|
|
368
|
+
return mix(a, overlay, fac);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
fn mix_multiply(fac: f32, a: vec3f, b: vec3f) -> vec3f {
|
|
372
|
+
return mix(a, a * b, fac);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
fn mix_lighten(fac: f32, a: vec3f, b: vec3f) -> vec3f {
|
|
376
|
+
return mix(a, max(a, b), fac);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// Blender Mix (Color) blend LINEAR_LIGHT: result = mix(A, A + 2*B - 1, Fac)
|
|
380
|
+
fn mix_linear_light(fac: f32, a: vec3f, b: vec3f) -> vec3f {
|
|
381
|
+
return mix(a, a + 2.0 * b - vec3f(1.0), fac);
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// The rest of Blender's blend set, transcribed from
|
|
385
|
+
// gpu_shader_material_mix_color.glsl. Note DODGE, BURN and DIVIDE are
|
|
386
|
+
// per-channel with their own guards there, not vector expressions.
|
|
387
|
+
fn mix_add(fac: f32, a: vec3f, b: vec3f) -> vec3f { return mix(a, a + b, fac); }
|
|
388
|
+
fn mix_subtract(fac: f32, a: vec3f, b: vec3f) -> vec3f { return mix(a, a - b, fac); }
|
|
389
|
+
fn mix_darken(fac: f32, a: vec3f, b: vec3f) -> vec3f { return mix(a, min(a, b), fac); }
|
|
390
|
+
fn mix_difference(fac: f32, a: vec3f, b: vec3f) -> vec3f { return mix(a, abs(a - b), fac); }
|
|
391
|
+
fn mix_exclusion(fac: f32, a: vec3f, b: vec3f) -> vec3f {
|
|
392
|
+
return max(mix(a, a + b - 2.0 * a * b, fac), vec3f(0.0));
|
|
393
|
+
}
|
|
394
|
+
fn mix_screen(fac: f32, a: vec3f, b: vec3f) -> vec3f {
|
|
395
|
+
let facm = 1.0 - fac;
|
|
396
|
+
return vec3f(1.0) - (vec3f(facm) + fac * (vec3f(1.0) - b)) * (vec3f(1.0) - a);
|
|
397
|
+
}
|
|
398
|
+
fn mix_soft_light(fac: f32, a: vec3f, b: vec3f) -> vec3f {
|
|
399
|
+
let facm = 1.0 - fac;
|
|
400
|
+
let scr = vec3f(1.0) - (vec3f(1.0) - b) * (vec3f(1.0) - a);
|
|
401
|
+
return facm * a + fac * ((vec3f(1.0) - a) * b * a + a * scr);
|
|
402
|
+
}
|
|
403
|
+
fn mix_dodge_1(fac: f32, a: f32, b: f32) -> f32 {
|
|
404
|
+
if (a == 0.0) { return 0.0; }
|
|
405
|
+
let t = 1.0 - fac * b;
|
|
406
|
+
if (t <= 0.0) { return 1.0; }
|
|
407
|
+
return min(a / t, 1.0);
|
|
408
|
+
}
|
|
409
|
+
fn mix_dodge(fac: f32, a: vec3f, b: vec3f) -> vec3f {
|
|
410
|
+
return vec3f(mix_dodge_1(fac, a.r, b.r), mix_dodge_1(fac, a.g, b.g), mix_dodge_1(fac, a.b, b.b));
|
|
411
|
+
}
|
|
412
|
+
fn mix_burn_1(fac: f32, a: f32, b: f32) -> f32 {
|
|
413
|
+
let facm = 1.0 - fac;
|
|
414
|
+
let t = facm + fac * b;
|
|
415
|
+
if (t <= 0.0) { return 0.0; }
|
|
416
|
+
return clamp(1.0 - (1.0 - a) / t, 0.0, 1.0);
|
|
417
|
+
}
|
|
418
|
+
fn mix_burn(fac: f32, a: vec3f, b: vec3f) -> vec3f {
|
|
419
|
+
return vec3f(mix_burn_1(fac, a.r, b.r), mix_burn_1(fac, a.g, b.g), mix_burn_1(fac, a.b, b.b));
|
|
420
|
+
}
|
|
421
|
+
fn mix_divide_1(fac: f32, a: f32, b: f32) -> f32 {
|
|
422
|
+
return select(a, (1.0 - fac) * a + fac * a / b, b != 0.0);
|
|
423
|
+
}
|
|
424
|
+
fn mix_divide(fac: f32, a: vec3f, b: vec3f) -> vec3f {
|
|
425
|
+
return vec3f(mix_divide_1(fac, a.r, b.r), mix_divide_1(fac, a.g, b.g), mix_divide_1(fac, a.b, b.b));
|
|
426
|
+
}
|
|
427
|
+
// The HSV-routed blends: take one channel from B, the rest from A.
|
|
428
|
+
fn mix_hue(fac: f32, a: vec3f, b: vec3f) -> vec3f {
|
|
429
|
+
let hsvB = rgb_to_hsv(b);
|
|
430
|
+
if (hsvB.y == 0.0) { return a; }
|
|
431
|
+
var hsv = rgb_to_hsv(a);
|
|
432
|
+
hsv.x = hsvB.x;
|
|
433
|
+
return mix(a, hsv_to_rgb(hsv), fac);
|
|
434
|
+
}
|
|
435
|
+
fn mix_saturation(fac: f32, a: vec3f, b: vec3f) -> vec3f {
|
|
436
|
+
var hsv = rgb_to_hsv(a);
|
|
437
|
+
if (hsv.y == 0.0) { return a; }
|
|
438
|
+
hsv.y = mix(hsv.y, rgb_to_hsv(b).y, fac);
|
|
439
|
+
return hsv_to_rgb(hsv);
|
|
440
|
+
}
|
|
441
|
+
fn mix_color_blend(fac: f32, a: vec3f, b: vec3f) -> vec3f {
|
|
442
|
+
let hsvB = rgb_to_hsv(b);
|
|
443
|
+
if (hsvB.y == 0.0) { return a; }
|
|
444
|
+
var hsv = rgb_to_hsv(a);
|
|
445
|
+
hsv.x = hsvB.x;
|
|
446
|
+
hsv.y = hsvB.y;
|
|
447
|
+
return mix(a, hsv_to_rgb(hsv), fac);
|
|
448
|
+
}
|
|
449
|
+
fn mix_value(fac: f32, a: vec3f, b: vec3f) -> vec3f {
|
|
450
|
+
var hsv = rgb_to_hsv(a);
|
|
451
|
+
hsv.z = mix(hsv.z, rgb_to_hsv(b).z, fac);
|
|
452
|
+
return hsv_to_rgb(hsv);
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
// ─── Colour utility nodes ──────────────────────────────────────────
|
|
456
|
+
|
|
457
|
+
/** Gamma node — Blender guards the zero case rather than emitting NaN. */
|
|
458
|
+
fn node_gamma(c: vec3f, g: f32) -> vec3f {
|
|
459
|
+
return vec3f(
|
|
460
|
+
select(0.0, pow(c.r, g), c.r > 0.0),
|
|
461
|
+
select(0.0, pow(c.g, g), c.g > 0.0),
|
|
462
|
+
select(0.0, pow(c.b, g), c.b > 0.0),
|
|
463
|
+
);
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
/** Map Range, linear, with the clamp Blender applies when the toggle is on. */
|
|
467
|
+
fn map_range_linear(v: f32, fromMin: f32, fromMax: f32, toMin: f32, toMax: f32) -> f32 {
|
|
468
|
+
let d = fromMax - fromMin;
|
|
469
|
+
return select(0.0, toMin + ((v - fromMin) / d) * (toMax - toMin), d != 0.0);
|
|
470
|
+
}
|
|
471
|
+
fn map_range_clamped(v: f32, fromMin: f32, fromMax: f32, toMin: f32, toMax: f32) -> f32 {
|
|
472
|
+
let r = map_range_linear(v, fromMin, fromMax, toMin, toMax);
|
|
473
|
+
return clamp(r, min(toMin, toMax), max(toMin, toMax));
|
|
474
|
+
}
|
|
475
|
+
/** Map Range, smoothstep interpolation. */
|
|
476
|
+
fn map_range_smooth(v: f32, fromMin: f32, fromMax: f32, toMin: f32, toMax: f32) -> f32 {
|
|
477
|
+
let d = fromMax - fromMin;
|
|
478
|
+
if (d == 0.0) { return 0.0; }
|
|
479
|
+
let t = clamp((v - fromMin) / d, 0.0, 1.0);
|
|
480
|
+
return toMin + (t * t * (3.0 - 2.0 * t)) * (toMax - toMin);
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
/** RGB→HSL and back, for Separate/Combine Color's HSL mode. */
|
|
484
|
+
fn rgb_to_hsl(c: vec3f) -> vec3f {
|
|
485
|
+
let mx = max(c.r, max(c.g, c.b));
|
|
486
|
+
let mn = min(c.r, min(c.g, c.b));
|
|
487
|
+
let l = (mx + mn) * 0.5;
|
|
488
|
+
if (mx == mn) { return vec3f(0.0, 0.0, l); }
|
|
489
|
+
let d = mx - mn;
|
|
490
|
+
let s = select(d / (2.0 - mx - mn), d / (mx + mn), l <= 0.5);
|
|
491
|
+
var h: f32;
|
|
492
|
+
if (mx == c.r) { h = (c.g - c.b) / d + select(0.0, 6.0, c.g < c.b); }
|
|
493
|
+
else if (mx == c.g) { h = (c.b - c.r) / d + 2.0; }
|
|
494
|
+
else { h = (c.r - c.g) / d + 4.0; }
|
|
495
|
+
return vec3f(h / 6.0, s, l);
|
|
496
|
+
}
|
|
497
|
+
fn hsl_channel(p: f32, q: f32, t0: f32) -> f32 {
|
|
498
|
+
var t = fract(t0);
|
|
499
|
+
if (t < 1.0 / 6.0) { return p + (q - p) * 6.0 * t; }
|
|
500
|
+
if (t < 0.5) { return q; }
|
|
501
|
+
if (t < 2.0 / 3.0) { return p + (q - p) * (2.0 / 3.0 - t) * 6.0; }
|
|
502
|
+
return p;
|
|
503
|
+
}
|
|
504
|
+
fn hsl_to_rgb(hsl: vec3f) -> vec3f {
|
|
505
|
+
if (hsl.y == 0.0) { return vec3f(hsl.z); }
|
|
506
|
+
let q = select(hsl.z + hsl.y - hsl.z * hsl.y, hsl.z * (1.0 + hsl.y), hsl.z < 0.5);
|
|
507
|
+
let p = 2.0 * hsl.z - q;
|
|
508
|
+
return vec3f(
|
|
509
|
+
hsl_channel(p, q, hsl.x + 1.0 / 3.0),
|
|
510
|
+
hsl_channel(p, q, hsl.x),
|
|
511
|
+
hsl_channel(p, q, hsl.x - 1.0 / 3.0),
|
|
512
|
+
);
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
// Luminance for Shader→RGB scalar gates (linear RGB, Rec.709 weights)
|
|
516
|
+
fn luminance_rec709_linear(c: vec3f) -> f32 {
|
|
517
|
+
return dot(max(c, vec3f(0.0)), vec3f(0.2126, 0.7152, 0.0722));
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
// ─── FRESNEL node ───────────────────────────────────────────────────
|
|
521
|
+
// Schlick approximation matching Blender's Fresnel node
|
|
522
|
+
|
|
523
|
+
fn fresnel(ior: f32, n: vec3f, v: vec3f) -> f32 {
|
|
524
|
+
let r = (ior - 1.0) / (ior + 1.0);
|
|
525
|
+
let f0 = r * r;
|
|
526
|
+
let cos_theta = clamp(dot(n, v), 0.0, 1.0);
|
|
527
|
+
let m = 1.0 - cos_theta;
|
|
528
|
+
let m2 = m * m;
|
|
529
|
+
let m5 = m2 * m2 * m;
|
|
530
|
+
return f0 + (1.0 - f0) * m5;
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
// ─── LAYER_WEIGHT node ──────────────────────────────────────────────
|
|
534
|
+
|
|
535
|
+
fn layer_weight_fresnel(blend: f32, n: vec3f, v: vec3f) -> f32 {
|
|
536
|
+
let eta = max(1.0 - blend, 1e-4);
|
|
537
|
+
let r = (1.0 - eta) / (1.0 + eta);
|
|
538
|
+
let f0 = r * r;
|
|
539
|
+
let cos_theta = clamp(abs(dot(n, v)), 0.0, 1.0);
|
|
540
|
+
let m = 1.0 - cos_theta;
|
|
541
|
+
let m2 = m * m;
|
|
542
|
+
let m5 = m2 * m2 * m;
|
|
543
|
+
return f0 + (1.0 - f0) * m5;
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
fn layer_weight_facing(blend: f32, n: vec3f, v: vec3f) -> f32 {
|
|
547
|
+
var facing = abs(dot(n, v));
|
|
548
|
+
let b = clamp(blend, 0.0, 0.99999);
|
|
549
|
+
if (b != 0.5) {
|
|
550
|
+
let exponent = select(2.0 * b, 0.5 / (1.0 - b), b >= 0.5);
|
|
551
|
+
facing = pow(facing, exponent);
|
|
552
|
+
}
|
|
553
|
+
return 1.0 - facing;
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
// ─── SHADER_TO_RGB (white DiffuseBSDF) ──────────────────────────────
|
|
557
|
+
// Eevee captures lit diffuse: (albedo/π)*sun*N·L*shadow + ambient (linear). Albedo=1.
|
|
558
|
+
// Matches default.ts direct term scale so VALTORGB thresholds from Blender JSON stay valid.
|
|
559
|
+
|
|
560
|
+
/**
|
|
561
|
+
* Shader → RGB on a white diffuse closure, as a COLOUR.
|
|
562
|
+
*
|
|
563
|
+
* Blender's ShaderToRGB hands back what the closure actually radiates, and that
|
|
564
|
+
* is coloured: a warm sun against a cool ambient gives warm light and cool
|
|
565
|
+
* shadow, which is most of what an NPR ramp is reading. Collapsing it to
|
|
566
|
+
* luminance first — which this used to do — throws that away and leaves every
|
|
567
|
+
* ramp working on grey.
|
|
568
|
+
*
|
|
569
|
+
* Feeding this into a scalar socket still yields a float, because Blender's own
|
|
570
|
+
* implicit Color → Value conversion (BT.601, see color_to_value) does it at the
|
|
571
|
+
* link. So a graph that wants the scalar gets Blender's scalar, and one that
|
|
572
|
+
* wants the colour now gets that too.
|
|
573
|
+
*
|
|
574
|
+
* The maths matches 5.2's bxdf_diffuse_eval, which is saturate(dot(N,L))
|
|
575
|
+
* integrated against a cosine distribution: radiance = E·N·L/π for the sun, plus
|
|
576
|
+
* the world's own irradiance. EEVEE Next's extra transport — probe GI, virtual
|
|
577
|
+
* shadow maps, ray-traced bounce — is machinery rather than a term, and is not
|
|
578
|
+
* here.
|
|
579
|
+
*/
|
|
580
|
+
fn shader_to_rgb_lit(n: vec3f, l: vec3f, sun_rgb: vec3f, ambient_rgb: vec3f, shadow: f32) -> vec3f {
|
|
581
|
+
const PI_S: f32 = 3.141592653589793;
|
|
582
|
+
let ndotl = max(dot(n, l), 0.0);
|
|
583
|
+
return sun_rgb * (ndotl * shadow / PI_S) + ambient_rgb;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
fn shader_to_rgb_diffuse(n: vec3f, l: vec3f, sun_rgb: vec3f, ambient_rgb: vec3f, shadow: f32) -> f32 {
|
|
587
|
+
return luminance_rec709_linear(shader_to_rgb_lit(n, l, sun_rgb, ambient_rgb, shadow));
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
// ─── BUMP node ──────────────────────────────────────────────────────
|
|
591
|
+
// Screen-space bump from a scalar height field. Needs dFdx/dFdy which
|
|
592
|
+
// WGSL provides as dpdx/dpdy.
|
|
593
|
+
|
|
594
|
+
fn bump(strength: f32, height: f32, normal: vec3f, world_pos: vec3f) -> vec3f {
|
|
595
|
+
let dhdx = dpdx(height);
|
|
596
|
+
let dhdy = dpdy(height);
|
|
597
|
+
let dpdx_pos = dpdx(world_pos);
|
|
598
|
+
let dpdy_pos = dpdy(world_pos);
|
|
599
|
+
let perturbed = normalize(normal) - strength * (dhdx * normalize(cross(dpdy_pos, normal)) + dhdy * normalize(cross(normal, dpdx_pos)));
|
|
600
|
+
return normalize(perturbed);
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
// LH engine + WebGPU fragment Y: flip dhdy contribution so height peaks read as outward bumps vs Blender reference
|
|
604
|
+
fn bump_lh(strength: f32, height: f32, normal: vec3f, world_pos: vec3f) -> vec3f {
|
|
605
|
+
let dhdx = dpdx(height);
|
|
606
|
+
let dhdy = dpdy(height);
|
|
607
|
+
let dpdx_pos = dpdx(world_pos);
|
|
608
|
+
let dpdy_pos = dpdy(world_pos);
|
|
609
|
+
let perturbed = normalize(normal) - strength * (dhdx * normalize(cross(dpdy_pos, normal)) - dhdy * normalize(cross(normal, dpdx_pos)));
|
|
610
|
+
return normalize(perturbed);
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
// ─── NOISE texture (Perlin-style) ───────────────────────────────────
|
|
614
|
+
// Simplified gradient noise matching Blender's default noise output.
|
|
615
|
+
|
|
616
|
+
// PCG-style integer hash. Replaces the classic 'fract(sin(q) * LARGE)' trick because
|
|
617
|
+
// WebKit's Metal backend compiles 'sin' to a full transcendental op (slow), while
|
|
618
|
+
// Safari's Apple-GPU scalar ALU handles int muls/xors near free. Inputs arrive as
|
|
619
|
+
// integer-valued floats (floor(p) + unit offsets) from _noise3, so vec3i cast is exact.
|
|
620
|
+
fn _hash33(p: vec3f) -> vec3f {
|
|
621
|
+
var h = vec3u(vec3i(p) + vec3i(32768));
|
|
622
|
+
h = h * vec3u(1664525u, 1013904223u, 2654435761u);
|
|
623
|
+
h = (h.yzx ^ h) * vec3u(2246822519u, 3266489917u, 668265263u);
|
|
624
|
+
h = h ^ (h >> vec3u(16u));
|
|
625
|
+
// Mask to 24 bits — above that f32 loses precision on the u32→f32 convert.
|
|
626
|
+
let hm = h & vec3u(16777215u);
|
|
627
|
+
return vec3f(hm) * (2.0 / 16777216.0) - 1.0;
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
fn _noise3(p: vec3f) -> f32 {
|
|
631
|
+
let i = floor(p);
|
|
632
|
+
let f = fract(p);
|
|
633
|
+
let u = f * f * (3.0 - 2.0 * f);
|
|
634
|
+
|
|
635
|
+
return mix(
|
|
636
|
+
mix(
|
|
637
|
+
mix(dot(_hash33(i + vec3f(0,0,0)), f - vec3f(0,0,0)),
|
|
638
|
+
dot(_hash33(i + vec3f(1,0,0)), f - vec3f(1,0,0)), u.x),
|
|
639
|
+
mix(dot(_hash33(i + vec3f(0,1,0)), f - vec3f(0,1,0)),
|
|
640
|
+
dot(_hash33(i + vec3f(1,1,0)), f - vec3f(1,1,0)), u.x), u.y),
|
|
641
|
+
mix(
|
|
642
|
+
mix(dot(_hash33(i + vec3f(0,0,1)), f - vec3f(0,0,1)),
|
|
643
|
+
dot(_hash33(i + vec3f(1,0,1)), f - vec3f(1,0,1)), u.x),
|
|
644
|
+
mix(dot(_hash33(i + vec3f(0,1,1)), f - vec3f(0,1,1)),
|
|
645
|
+
dot(_hash33(i + vec3f(1,1,1)), f - vec3f(1,1,1)), u.x), u.y),
|
|
646
|
+
u.z);
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
fn tex_noise(p: vec3f, scale: f32, detail: f32, roughness: f32, distortion: f32) -> f32 {
|
|
650
|
+
var q = p;
|
|
651
|
+
if (abs(distortion) > 1e-6) {
|
|
652
|
+
let w = _noise3(p * scale * 1.37 + vec3f(2.31, 5.17, 8.09));
|
|
653
|
+
q = p + (w * 2.0 - 1.0) * distortion;
|
|
654
|
+
}
|
|
655
|
+
let coords = q * scale;
|
|
656
|
+
var value = 0.0;
|
|
657
|
+
var amplitude = 1.0;
|
|
658
|
+
var frequency = 1.0;
|
|
659
|
+
var total_amp = 0.0;
|
|
660
|
+
let octaves = i32(clamp(detail, 0.0, 15.0)) + 1;
|
|
661
|
+
for (var i = 0; i < octaves; i++) {
|
|
662
|
+
value += amplitude * _noise3(coords * frequency);
|
|
663
|
+
total_amp += amplitude;
|
|
664
|
+
amplitude *= roughness;
|
|
665
|
+
frequency *= 2.0;
|
|
666
|
+
}
|
|
667
|
+
return value / max(total_amp, 1e-6) * 0.5 + 0.5;
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
// tex_noise specialization: detail=2.0 (3 octaves), roughness=0.5, distortion=0.
|
|
671
|
+
// WebKit can't unroll tex_noise's for-loop because 'octaves' is a runtime value;
|
|
672
|
+
// this variant is fully unrolled with constants folded (total_amp = 1.75).
|
|
673
|
+
fn tex_noise_d2(p: vec3f, scale: f32) -> f32 {
|
|
674
|
+
let c = p * scale;
|
|
675
|
+
let v = _noise3(c) + 0.5 * _noise3(c * 2.0) + 0.25 * _noise3(c * 4.0);
|
|
676
|
+
return v * (1.0 / 1.75) * 0.5 + 0.5;
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
// ─── TEX_GRADIENT (linear) ──────────────────────────────────────────
|
|
680
|
+
// Used by Stockings preset. Maps the input vector's X to a 0–1 gradient.
|
|
681
|
+
|
|
682
|
+
fn tex_gradient_linear(uv: vec3f) -> f32 {
|
|
683
|
+
return clamp(uv.x, 0.0, 1.0);
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
// ─── TEX_VORONOI ────────────────────────────────────────────────────
|
|
687
|
+
// 3D F1 voronoi. _f1 returns Distance; _color returns per-cell hash color
|
|
688
|
+
// (matches Blender voronoi.cc: outColor = hash_int3_to_float3(cell + targetOffset)).
|
|
689
|
+
|
|
690
|
+
fn tex_voronoi_f1(p: vec3f, scale: f32) -> f32 {
|
|
691
|
+
let coords = p * scale;
|
|
692
|
+
let i = floor(coords);
|
|
693
|
+
let f = fract(coords);
|
|
694
|
+
var min_dist = 1e10;
|
|
695
|
+
for (var z = -1; z <= 1; z++) {
|
|
696
|
+
for (var y = -1; y <= 1; y++) {
|
|
697
|
+
for (var x = -1; x <= 1; x++) {
|
|
698
|
+
let neighbor = vec3f(f32(x), f32(y), f32(z));
|
|
699
|
+
let point = _hash33(i + neighbor) * 0.5 + 0.5;
|
|
700
|
+
let diff = neighbor + point - f;
|
|
701
|
+
min_dist = min(min_dist, dot(diff, diff));
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
return sqrt(min_dist);
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
// The per-cell jitter hash IS the Color output in Blender — reuse the same hash
|
|
709
|
+
// tap for jitter + color instead of computing two.
|
|
710
|
+
fn tex_voronoi_color(p: vec3f, scale: f32) -> vec3f {
|
|
711
|
+
let coords = p * scale;
|
|
712
|
+
let i = floor(coords);
|
|
713
|
+
let f = fract(coords);
|
|
714
|
+
var min_dist = 1e10;
|
|
715
|
+
var min_hash = vec3f(0.5);
|
|
716
|
+
for (var z = -1; z <= 1; z++) {
|
|
717
|
+
for (var y = -1; y <= 1; y++) {
|
|
718
|
+
for (var x = -1; x <= 1; x++) {
|
|
719
|
+
let neighbor = vec3f(f32(x), f32(y), f32(z));
|
|
720
|
+
let h = _hash33(i + neighbor) * 0.5 + 0.5;
|
|
721
|
+
let diff = neighbor + h - f;
|
|
722
|
+
let d = dot(diff, diff);
|
|
723
|
+
if (d < min_dist) {
|
|
724
|
+
min_dist = d;
|
|
725
|
+
min_hash = h;
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
return min_hash;
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
// ─── SEPXYZ node ────────────────────────────────────────────────────
|
|
734
|
+
|
|
735
|
+
fn separate_xyz(v: vec3f) -> vec3f { return v; }
|
|
736
|
+
|
|
737
|
+
// ─── VECT_MATH (cross product) ──────────────────────────────────────
|
|
738
|
+
|
|
739
|
+
fn vect_math_cross(a: vec3f, b: vec3f) -> vec3f { return cross(a, b); }
|
|
740
|
+
|
|
741
|
+
// ─── MAPPING node ───────────────────────────────────────────────────
|
|
742
|
+
// Point-type mapping: scale, rotate (euler XYZ), translate.
|
|
743
|
+
|
|
744
|
+
fn mapping_point(v: vec3f, loc: vec3f, rot: vec3f, scl: vec3f) -> vec3f {
|
|
745
|
+
var p = v * scl;
|
|
746
|
+
// simplified: skip rotation when all angles are zero (common case)
|
|
747
|
+
if (abs(rot.x) + abs(rot.y) + abs(rot.z) > 1e-6) {
|
|
748
|
+
let cx = cos(rot.x); let sx = sin(rot.x);
|
|
749
|
+
let cy = cos(rot.y); let sy = sin(rot.y);
|
|
750
|
+
let cz = cos(rot.z); let sz = sin(rot.z);
|
|
751
|
+
let rx = vec3f(p.x, cx*p.y - sx*p.z, sx*p.y + cx*p.z);
|
|
752
|
+
let ry = vec3f(cy*rx.x + sy*rx.z, rx.y, -sy*rx.x + cy*rx.z);
|
|
753
|
+
p = vec3f(cz*ry.x - sz*ry.y, sz*ry.x + cz*ry.y, ry.z);
|
|
754
|
+
}
|
|
755
|
+
return p + loc;
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
// ─── NORMAL_MAP node (tangent-space) ────────────────────────────────
|
|
759
|
+
// Applies a tangent-space normal map. Requires TBN from vertex stage.
|
|
760
|
+
|
|
761
|
+
fn normal_map(strength: f32, map_color: vec3f, normal: vec3f, tangent: vec3f, bitangent: vec3f) -> vec3f {
|
|
762
|
+
let ts = map_color * 2.0 - 1.0;
|
|
763
|
+
let perturbed = normalize(tangent * ts.x + bitangent * ts.y + normal * ts.z);
|
|
764
|
+
return normalize(mix(normal, perturbed, strength));
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
// ─── EEVEE Principled BSDF primitives ───────────────────────────────
|
|
768
|
+
// Ports from Blender 3.6 source/blender/draw/engines/eevee/shaders/
|
|
769
|
+
// bsdf_common_lib.glsl + gpu_shader_material_principled.glsl.
|
|
770
|
+
// Usage pattern (see material shaders): direct spec = bsdf_ggx × sun × shadow
|
|
771
|
+
// (NL baked in, no F yet); ambient spec = probe_radiance; tint both with
|
|
772
|
+
// reflection_color = F_brdf_multi_scatter(f0, f90, split_sum) AFTER summing.
|
|
773
|
+
|
|
774
|
+
const EEVEE_PI: f32 = 3.141592653589793;
|
|
775
|
+
|
|
776
|
+
// Fused analytic GGX specular (direct lights). Returns BRDF × NL.
|
|
777
|
+
// 4·NL·NV is cancelled via G1_Smith reciprocal form — see bsdf_common_lib.glsl:115.
|
|
778
|
+
// Caller passes NL, NV (already computed for diffuse + brdf_lut_sample) so WebKit
|
|
779
|
+
// can reuse them instead of recomputing dot products across the function boundary.
|
|
780
|
+
fn bsdf_ggx(N: vec3f, L: vec3f, V: vec3f, NL_in: f32, NV_in: f32, roughness: f32) -> f32 {
|
|
781
|
+
let a = max(roughness, 1e-4);
|
|
782
|
+
let a2 = a * a;
|
|
783
|
+
let H = normalize(L + V);
|
|
784
|
+
let NH = max(dot(N, H), 1e-8);
|
|
785
|
+
let NL = max(NL_in, 1e-8);
|
|
786
|
+
let NV = max(NV_in, 1e-8);
|
|
787
|
+
// G1_Smith_GGX_opti reciprocal form — denominator piece only.
|
|
788
|
+
let G1L = NL + sqrt(NL * (NL - NL * a2) + a2);
|
|
789
|
+
let G1V = NV + sqrt(NV * (NV - NV * a2) + a2);
|
|
790
|
+
let G = G1L * G1V;
|
|
791
|
+
// D_ggx_opti = pi * denom² — reciprocal D × a².
|
|
792
|
+
let tmp = (NH * a2 - NH) * NH + 1.0;
|
|
793
|
+
let D_opti = EEVEE_PI * tmp * tmp;
|
|
794
|
+
return NL * a2 / (D_opti * G);
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
// Split-sum DFG LUT — Karis 2013 curve fit stand-in for the 64×64 baked LUT.
|
|
798
|
+
// Returns (lut.x, lut.y) in Blender convention: tint = f0·lut.x + f90·lut.y.
|
|
799
|
+
fn brdf_lut_approx(NV: f32, roughness: f32) -> vec2f {
|
|
800
|
+
let c0 = vec4f(-1.0, -0.0275, -0.572, 0.022);
|
|
801
|
+
let c1 = vec4f(1.0, 0.0425, 1.04, -0.04);
|
|
802
|
+
let r = roughness * c0 + c1;
|
|
803
|
+
let a004 = min(r.x * r.x, exp2(-9.28 * NV)) * r.x + r.y;
|
|
804
|
+
return vec2f(-1.04, 1.04) * a004 + r.zw;
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
// Baked combined BRDF LUT — exact port of Blender bsdf_lut_frag.glsl packed with
|
|
808
|
+
// ltc_mag_ggx from eevee_lut.c. Single sample returns DFG (.rg) and LTC mag (.ba).
|
|
809
|
+
// Addressed as Blender's common_utiltex_lib.glsl:lut_coords:
|
|
810
|
+
// coords = (roughness, sqrt(1 - NV)), then half-texel bias for filtering.
|
|
811
|
+
// Requires group(0) binding(9) brdfLut + binding(2) diffuseSampler in the host shader.
|
|
812
|
+
fn brdf_lut_sample(NV: f32, roughness: f32) -> vec4f {
|
|
813
|
+
let LUT_SIZE: f32 = 64.0;
|
|
814
|
+
var uv = vec2f(saturate(roughness), sqrt(saturate(1.0 - NV)));
|
|
815
|
+
uv = uv * ((LUT_SIZE - 1.0) / LUT_SIZE) + 0.5 / LUT_SIZE;
|
|
816
|
+
return textureSampleLevel(brdfLut, diffuseSampler, uv, 0.0);
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
fn F_brdf_single_scatter(f0: vec3f, f90: vec3f, lut: vec2f) -> vec3f {
|
|
820
|
+
return lut.y * f90 + lut.x * f0;
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
// Fdez-Agüera 2019 multi-scatter compensation (EEVEE do_multiscatter=1).
|
|
824
|
+
fn F_brdf_multi_scatter(f0: vec3f, f90: vec3f, lut: vec2f) -> vec3f {
|
|
825
|
+
let FssEss = lut.y * f90 + lut.x * f0;
|
|
826
|
+
let Ess = lut.x + lut.y;
|
|
827
|
+
let Ems = 1.0 - Ess;
|
|
828
|
+
let Favg = f0 + (1.0 - f0) / 21.0;
|
|
829
|
+
let Fms = FssEss * Favg / (1.0 - (1.0 - Ess) * Favg);
|
|
830
|
+
return FssEss + Fms * Ems;
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
// EEVEE direct-specular energy compensation factor — closure_eval_glossy_lib.glsl:79-81:
|
|
834
|
+
// ltc_brdf_scale = (ltc.x + ltc.y) / (split_sum.x + split_sum.y)
|
|
835
|
+
// Blender evaluates direct lights via LTC (Heitz 2016) but indirect via split-sum;
|
|
836
|
+
// direct radiance is rescaled so total-energy matches the split-sum LUT.
|
|
837
|
+
// Takes a pre-sampled vec4f from brdf_lut_sample() to share the fetch with
|
|
838
|
+
// F_brdf_multi_scatter on the same fragment.
|
|
839
|
+
fn ltc_brdf_scale_from_lut(lut: vec4f) -> f32 {
|
|
840
|
+
return (lut.z + lut.w) / max(lut.x + lut.y, 1e-6);
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
// Luminance-normalized hue extraction — Blender tint_from_color (isolates hue+sat).
|
|
844
|
+
fn tint_from_color(color: vec3f) -> vec3f {
|
|
845
|
+
let lum = dot(color, vec3f(0.3, 0.6, 0.1));
|
|
846
|
+
return select(vec3f(1.0), color / lum, lum > 0.0);
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
// ─── Principled sheen (gpu_shader_material_principled.glsl:8-14) ────
|
|
850
|
+
// Empirical NV-only curve that approximates grazing retroreflection on cloth/velvet.
|
|
851
|
+
// Scales the sheen layer's diffuse contribution; no sheen call site has sheen=0
|
|
852
|
+
// shortcut because the multiplier is tiny at normal view angles anyway.
|
|
853
|
+
fn principled_sheen(NV: f32) -> f32 {
|
|
854
|
+
let f = 1.0 - NV;
|
|
855
|
+
return f * f * f * 0.077 + f * 0.01 + 0.00026;
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
// ─── Principled BSDF eval ───────────────────────────────────────────
|
|
859
|
+
// Shared EEVEE Principled path used by every material in the engine — metallic,
|
|
860
|
+
// dielectric, and sheen variants all fold into these ~15 lines via the struct
|
|
861
|
+
// fields. NPR materials still compute a separate toon/rim/warm stack on top and
|
|
862
|
+
// mix(npr_stack, eval_principled(...), fac); see body.ts / face.ts / etc.
|
|
863
|
+
//
|
|
864
|
+
// Field conventions:
|
|
865
|
+
// base — diffuse albedo. Mixed into f0 only when metallic > 0.
|
|
866
|
+
// metallic — 0 = dielectric (f0 from specular), 1 = pure metal (f0 = base).
|
|
867
|
+
// specular — Principled Specular input (0.5 default → f0 = 0.04). sqrt for f90.
|
|
868
|
+
// roughness — GGX roughness; drives BRDF LUT coord + bsdf_ggx.
|
|
869
|
+
// spec_clamp — EEVEE Light Clamp equivalent. Caps firefly spec from noise-bumped
|
|
870
|
+
// NDF aliasing (Blender hides this via TAA which we don't have).
|
|
871
|
+
// Pass 1e30 (effectively disabled) for materials that don't bump.
|
|
872
|
+
// sheen — 0 disables. Scales the sheen diffuse add; cloth/stockings use ~0.7.
|
|
873
|
+
// sheen_tint — 0 = white sheen, 1 = fully tinted by base. Multiplied by sheen,
|
|
874
|
+
// so value is don't-care when sheen=0.
|
|
875
|
+
// The PMX sphere map, applied the way MMD applies it: the texture is a
|
|
876
|
+
// VIEW-SPACE lighting mask, sampled by the camera-space normal rather than by
|
|
877
|
+
// any UV the mesh carries, which is why it tracks the viewer and reads as a
|
|
878
|
+
// highlight. The material's own mode picks the operator — .sph (1) multiplies
|
|
879
|
+
// the shaded base, .spa (2) adds — so a graph asks for the effect and the model
|
|
880
|
+
// decides which it meant. Mode 0, or a material with no sphere texture (the 1×1
|
|
881
|
+
// white fallback is bound then), is an exact no-op.
|
|
882
|
+
fn pmx_sphere_map(base: vec3f, strength: f32, N: vec3f) -> vec3f {
|
|
883
|
+
if (material.sphereMode < 0.5) {
|
|
884
|
+
return base;
|
|
885
|
+
}
|
|
886
|
+
let camera_n = safe_normal((camera.view * vec4f(N, 0.0)).xyz);
|
|
887
|
+
let sphere_uv = clamp(camera_n.xy * 0.5 + vec2f(0.5), vec2f(0.0), vec2f(1.0));
|
|
888
|
+
let sphere_rgb = textureSample(sphereTexture, diffuseSampler, sphere_uv).rgb;
|
|
889
|
+
let amount = max(strength, 0.0);
|
|
890
|
+
if (material.sphereMode < 1.5) {
|
|
891
|
+
return mix(base, base * sphere_rgb, min(amount, 1.0));
|
|
892
|
+
}
|
|
893
|
+
return base + sphere_rgb * amount;
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
/**
|
|
897
|
+
* Principled v2 reflectance: IOR sets it, the level scales it.
|
|
898
|
+
*
|
|
899
|
+
* Blender 3.6 took Specular directly, where 0.5 meant f0 = 0.04. v2 computes
|
|
900
|
+
* f0 from IOR — ((ior-1)/(ior+1))² — and Specular IOR Level scales that, with
|
|
901
|
+
* 0.5 meaning "the IOR's own value". Returned in the 3.6 convention the BSDF
|
|
902
|
+
* below still speaks (f0 = 0.08 · specular), so the two agree at their defaults
|
|
903
|
+
* and v2 wins whenever a preset moves either socket.
|
|
904
|
+
*/
|
|
905
|
+
fn principled_specular(ior: f32, level: f32) -> f32 {
|
|
906
|
+
let r = (ior - 1.0) / max(ior + 1.0, 1e-6);
|
|
907
|
+
return (r * r * 2.0 * max(level, 0.0)) / 0.08;
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
struct PrincipledIn {
|
|
911
|
+
base: vec3f,
|
|
912
|
+
metallic: f32,
|
|
913
|
+
specular: f32,
|
|
914
|
+
roughness: f32,
|
|
915
|
+
spec_clamp: f32,
|
|
916
|
+
sheen: f32,
|
|
917
|
+
sheen_tint: f32,
|
|
918
|
+
};
|
|
919
|
+
|
|
920
|
+
fn eval_principled(
|
|
921
|
+
p: PrincipledIn,
|
|
922
|
+
N: vec3f, L: vec3f, V: vec3f,
|
|
923
|
+
sun_rgb: vec3f, amb_rgb: vec3f, shadow: f32
|
|
924
|
+
) -> vec3f {
|
|
925
|
+
let NL = max(dot(N, L), 0.0);
|
|
926
|
+
let NV = max(dot(N, V), 1e-4);
|
|
927
|
+
|
|
928
|
+
// f0/f90 per gpu_shader_material_principled.glsl. specular_tint=0 is assumed
|
|
929
|
+
// (all presets in this engine use the default white dielectric tint).
|
|
930
|
+
let dielectric_f0 = vec3f(0.08 * p.specular);
|
|
931
|
+
let f0 = mix(dielectric_f0, p.base, p.metallic);
|
|
932
|
+
let f90 = mix(f0, vec3f(1.0), sqrt(p.specular));
|
|
933
|
+
|
|
934
|
+
// Single LUT tap feeds both F_brdf_multi_scatter (split-sum DFG) and
|
|
935
|
+
// ltc_brdf_scale_from_lut (LTC mag in .ba). See nodes.ts brdf_lut_sample.
|
|
936
|
+
let lut = brdf_lut_sample(NV, p.roughness);
|
|
937
|
+
let reflection_color = F_brdf_multi_scatter(f0, f90, lut.xy);
|
|
938
|
+
|
|
939
|
+
// Direct glossy — bsdf_ggx already includes NL; no F applied here (tinted after
|
|
940
|
+
// accum with reflection_color). ltc_brdf_scale rescales direct to match the
|
|
941
|
+
// split-sum indirect path, matching EEVEE closure_eval_glossy_lib behavior.
|
|
942
|
+
let spec_direct_raw = bsdf_ggx(N, L, V, NL, NV, p.roughness)
|
|
943
|
+
* sun_rgb * shadow * ltc_brdf_scale_from_lut(lut);
|
|
944
|
+
let spec_direct = min(spec_direct_raw, vec3f(p.spec_clamp));
|
|
945
|
+
let spec_indirect = amb_rgb;
|
|
946
|
+
let spec_radiance = (spec_direct + spec_indirect) * reflection_color;
|
|
947
|
+
|
|
948
|
+
// Sheen add — when p.sheen=0 the whole term collapses, leaving diffuse_color=base.
|
|
949
|
+
let base_tint = tint_from_color(p.base);
|
|
950
|
+
let sheen_color = mix(vec3f(1.0), base_tint, p.sheen_tint);
|
|
951
|
+
let diffuse_color = p.base + p.sheen * sheen_color * principled_sheen(NV);
|
|
952
|
+
|
|
953
|
+
// diffuse_weight = (1-metallic). Indirect diffuse uses amb (L_w) with no π factor
|
|
954
|
+
// (probe_evaluate_world_diff returns SH-projected radiance, not cosine-convolved).
|
|
955
|
+
let diffuse_weight = 1.0 - p.metallic;
|
|
956
|
+
let diffuse_radiance = diffuse_color * (sun_rgb * NL * shadow / EEVEE_PI + amb_rgb) * diffuse_weight;
|
|
957
|
+
|
|
958
|
+
return diffuse_radiance + spec_radiance;
|
|
959
|
+
}
|
|
960
|
+
|
|
580
961
|
`;
|