@triforge/shader-core 0.1.2 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/bundle.js CHANGED
@@ -84,6 +84,12 @@ var OutputSocket = class {
84
84
  };
85
85
 
86
86
  // src/core/ShaderNode.ts
87
+ function finiteOr(value, fallback) {
88
+ return Number.isFinite(value) ? value : fallback;
89
+ }
90
+ function sanitizeVec3(v) {
91
+ return [finiteOr(v[0], 0), finiteOr(v[1], 0), finiteOr(v[2], 0)];
92
+ }
87
93
  function hexToRgb(hex) {
88
94
  const h = hex.replace("#", "");
89
95
  return [
@@ -178,14 +184,14 @@ var ShaderNode = class {
178
184
  if (type === "float" && typeof literal === "number") {
179
185
  socket.uniformName = `u_${this.id}_${name}`;
180
186
  this._uniformMap.set(name, socket.uniformName);
181
- this.parameters[name] = literal;
187
+ this.parameters[name] = finiteOr(literal, 0);
182
188
  }
183
189
  if (type === "color" && !(value instanceof OutputSocket)) {
184
190
  if (typeof literal === "string" || Array.isArray(literal)) {
185
191
  socket.uniformName = `u_${this.id}_${name}`;
186
192
  this._uniformMap.set(name, socket.uniformName);
187
193
  this._colorParams.add(name);
188
- this.parameters[name] = typeof literal === "string" ? hexToRgb(literal) : literal;
194
+ this.parameters[name] = typeof literal === "string" ? hexToRgb(literal) : sanitizeVec3(literal);
189
195
  }
190
196
  }
191
197
  sockets[name] = socket;
@@ -217,7 +223,7 @@ var ShaderNode = class {
217
223
  Object.defineProperty(this.parameters, paramName, {
218
224
  get: () => uniforms[uniformName].value,
219
225
  set: (v) => {
220
- uniforms[uniformName].value = v;
226
+ uniforms[uniformName].value = sanitizeVec3(v);
221
227
  },
222
228
  enumerable: true,
223
229
  configurable: true
@@ -226,7 +232,7 @@ var ShaderNode = class {
226
232
  Object.defineProperty(this.parameters, paramName, {
227
233
  get: () => uniforms[uniformName].value,
228
234
  set: (v) => {
229
- uniforms[uniformName].value = v;
235
+ uniforms[uniformName].value = finiteOr(v, 0);
230
236
  },
231
237
  enumerable: true,
232
238
  configurable: true
@@ -308,16 +314,24 @@ var CompileContext = class _CompileContext {
308
314
  */
309
315
  resolveInput(socket) {
310
316
  if (socket.connection) {
311
- return this.outputVar(socket.connection.node, socket.connection.name);
317
+ const varName = this.outputVar(socket.connection.node, socket.connection.name);
318
+ if (socket.type === "vector" && socket.connection.type !== "vector") {
319
+ return `(${varName}).xy`;
320
+ }
321
+ if (socket.type === "color" && socket.connection.type === "vector") {
322
+ return `vec3(${varName}, 0.0)`;
323
+ }
324
+ return varName;
312
325
  }
313
326
  if (socket.uniformName !== null) {
314
327
  if (!(socket.uniformName in this._uniforms)) {
315
328
  if (socket.type === "color") {
316
329
  const raw = socket.defaultValue;
317
330
  const rgb = typeof raw === "string" ? _CompileContext.hexToRgbArray(raw) : Array.isArray(raw) ? raw : [1, 1, 1];
318
- this._uniforms[socket.uniformName] = { value: rgb };
331
+ this._uniforms[socket.uniformName] = { value: rgb.map((n) => Number.isFinite(n) ? n : 0) };
319
332
  } else {
320
- this._uniforms[socket.uniformName] = { value: socket.defaultValue };
333
+ const n = socket.defaultValue;
334
+ this._uniforms[socket.uniformName] = { value: Number.isFinite(n) ? n : 0 };
321
335
  }
322
336
  }
323
337
  return socket.uniformName;
@@ -421,6 +435,8 @@ var CompileContext = class _CompileContext {
421
435
  typesCompatible(from, to) {
422
436
  if (from === to) return true;
423
437
  if ((from === "shader" || from === "color") && (to === "shader" || to === "color")) return true;
438
+ if ((from === "color" || from === "shader") && to === "vector") return true;
439
+ if (from === "vector" && to === "color") return true;
424
440
  return false;
425
441
  }
426
442
  // ── GLSL builders ─────────────────────────────────────────────────
@@ -469,8 +485,10 @@ ${callsBlock}
469
485
  // ── Literal conversion ────────────────────────────────────────────
470
486
  toLiteral(value, type) {
471
487
  switch (type) {
472
- case "float":
473
- return (typeof value === "number" ? value : 0).toFixed(4);
488
+ case "float": {
489
+ const n = typeof value === "number" && Number.isFinite(value) ? value : 0;
490
+ return n.toFixed(4);
491
+ }
474
492
  case "color":
475
493
  return typeof value === "string" ? this.hexToVec3(value) : "vec3(1.0, 1.0, 1.0)";
476
494
  case "vector":
@@ -2033,7 +2051,7 @@ var ImageTexture = class extends ProcessNode {
2033
2051
  return `uniform sampler2D ${this.uniformName};`;
2034
2052
  }
2035
2053
  compileCall(ctx) {
2036
- const uv = this._inputs.vector.isConnected() ? ctx.outputVar(this._inputs.vector.connection.node, this._inputs.vector.connection.name) : "vUv";
2054
+ const uv = this._inputs.vector.isConnected() ? ctx.resolveInput(this._inputs.vector) : "vUv";
2037
2055
  const cv = ctx.outputVar(this, "Color");
2038
2056
  const av = ctx.outputVar(this, "Alpha");
2039
2057
  return [
@@ -2644,7 +2662,7 @@ vec3 _st_wavelength(float w) {
2644
2662
  };
2645
2663
 
2646
2664
  // src/nodes/process/NormalMap.ts
2647
- var NormalMap = class extends ProcessNode {
2665
+ var NormalMap = class _NormalMap extends ProcessNode {
2648
2666
  get nodeType() {
2649
2667
  return "NormalMap";
2650
2668
  }
@@ -2657,19 +2675,35 @@ var NormalMap = class extends ProcessNode {
2657
2675
  category: "Vector",
2658
2676
  color: "#4a3a8a",
2659
2677
  cost: "medium",
2660
- costNote: "Samples input Fac four times for gradient computation."
2678
+ costNote: "Builds a derivative-based TBN frame per fragment."
2661
2679
  };
2662
2680
  }
2663
2681
  static {
2664
2682
  this.glslFunction = `
2665
- vec3 _st_normalMap(float fac, float strength) {
2666
- return vec3(0.0, 1.0, 0.0);
2683
+ vec3 _st_normalMap(vec3 sampledColor, float strength, vec3 N, vec3 P, vec2 uv) {
2684
+ vec3 n = sampledColor * 2.0 - 1.0;
2685
+ n.xy *= strength;
2686
+
2687
+ // Derivative-based cotangent frame (Sch\xFCler) \u2014 no vertex tangent attribute needed.
2688
+ vec3 dp1 = dFdx(P);
2689
+ vec3 dp2 = dFdy(P);
2690
+ vec2 duv1 = dFdx(uv);
2691
+ vec2 duv2 = dFdy(uv);
2692
+
2693
+ vec3 dp2perp = cross(dp2, N);
2694
+ vec3 dp1perp = cross(N, dp1);
2695
+ vec3 T = dp2perp * duv1.x + dp1perp * duv2.x;
2696
+ vec3 B = dp2perp * duv1.y + dp1perp * duv2.y;
2697
+
2698
+ float invmax = inversesqrt(max(dot(T, T), dot(B, B)));
2699
+ mat3 TBN = mat3(T * invmax, B * invmax, N);
2700
+ return normalize(TBN * n);
2667
2701
  }`;
2668
2702
  }
2669
2703
  constructor(inputs = {}) {
2670
2704
  super("NormalMap");
2671
2705
  this._inputs = this.createInputs(inputs, {
2672
- fac: ["float", 0.5],
2706
+ color: ["color", [0.5, 0.5, 1]],
2673
2707
  strength: ["float", inputs.strength ?? 1]
2674
2708
  });
2675
2709
  this._outputs = this.createOutputs({ Normal: "color" });
@@ -2681,17 +2715,13 @@ vec3 _st_normalMap(float fac, float strength) {
2681
2715
  return this._outputs;
2682
2716
  }
2683
2717
  compileDefs() {
2684
- return "";
2718
+ return _NormalMap.glslFunction;
2685
2719
  }
2686
2720
  compileCall(ctx) {
2687
- const facVar = this._inputs.fac.isConnected() ? ctx.outputVar(this._inputs.fac.connection.node, this._inputs.fac.connection.name) : ctx.resolveInput(this._inputs.fac);
2721
+ const color = ctx.resolveInput(this._inputs.color);
2688
2722
  const strength = ctx.resolveInput(this._inputs.strength);
2689
2723
  const normalVar = ctx.outputVar(this, "Normal");
2690
- return [
2691
- `float _nm_nx_${this.id} = dFdx(${facVar});`,
2692
- `float _nm_ny_${this.id} = dFdy(${facVar});`,
2693
- `vec3 ${normalVar} = normalize(vec3(-_nm_nx_${this.id} * ${strength}, 1.0, -_nm_ny_${this.id} * ${strength}));`
2694
- ].join("\n ");
2724
+ return `vec3 ${normalVar} = _st_normalMap(${color}, ${strength}, normalize(vNormal), vPosition, vUv);`;
2695
2725
  }
2696
2726
  };
2697
2727
 
@@ -2793,10 +2823,12 @@ vec3 _st_rotateZ(vec3 v, float a) {
2793
2823
  constructor(inputs = {}) {
2794
2824
  super("Mapping");
2795
2825
  this.mode = inputs.mode ?? "TEXTURE";
2796
- this.loc = inputs.location ?? [0, 0, 0];
2797
- this.rot = inputs.rotation ?? [0, 0, 0];
2798
- this.scl = inputs.scale ?? [1, 1, 1];
2799
- this._inputs = this.createInputs(inputs, { vector: ["color", null] });
2826
+ this._inputs = this.createInputs(inputs, {
2827
+ vector: ["color", null],
2828
+ location: ["color", inputs.location ?? [0, 0, 0]],
2829
+ rotation: ["color", inputs.rotation ?? [0, 0, 0]],
2830
+ scale: ["color", inputs.scale ?? [1, 1, 1]]
2831
+ });
2800
2832
  this._outputs = this.createOutputs({ Vector: "color" });
2801
2833
  }
2802
2834
  getInputSockets() {
@@ -2810,18 +2842,19 @@ vec3 _st_rotateZ(vec3 v, float a) {
2810
2842
  }
2811
2843
  compileCall(ctx) {
2812
2844
  const v = ctx.resolveInput(this._inputs.vector);
2845
+ const loc = ctx.resolveInput(this._inputs.location);
2846
+ const rot = ctx.resolveInput(this._inputs.rotation);
2847
+ const scl = ctx.resolveInput(this._inputs.scale);
2813
2848
  const out = ctx.outputVar(this, "Vector");
2814
- const toRad = (d) => (d * Math.PI / 180).toFixed(6);
2815
- const [lx, ly, lz] = this.loc;
2816
- const [rx, ry, rz] = this.rot.map(Number);
2817
- const [sx, sy, sz] = this.scl;
2849
+ const toRad = (Math.PI / 180).toFixed(6);
2818
2850
  return [
2819
2851
  `vec3 _mp_v_${this.id} = ${v};`,
2820
- `_mp_v_${this.id} = _mp_v_${this.id} - vec3(${lx.toFixed(4)}, ${ly.toFixed(4)}, ${lz.toFixed(4)});`,
2821
- `_mp_v_${this.id} = _st_rotateX(_mp_v_${this.id}, ${toRad(rx)});`,
2822
- `_mp_v_${this.id} = _st_rotateY(_mp_v_${this.id}, ${toRad(ry)});`,
2823
- `_mp_v_${this.id} = _st_rotateZ(_mp_v_${this.id}, ${toRad(rz)});`,
2824
- `_mp_v_${this.id} = _mp_v_${this.id} * vec3(${sx.toFixed(4)}, ${sy.toFixed(4)}, ${sz.toFixed(4)});`,
2852
+ `vec3 _mp_r_${this.id} = ${rot} * ${toRad};`,
2853
+ `_mp_v_${this.id} = _mp_v_${this.id} - ${loc};`,
2854
+ `_mp_v_${this.id} = _st_rotateX(_mp_v_${this.id}, _mp_r_${this.id}.x);`,
2855
+ `_mp_v_${this.id} = _st_rotateY(_mp_v_${this.id}, _mp_r_${this.id}.y);`,
2856
+ `_mp_v_${this.id} = _st_rotateZ(_mp_v_${this.id}, _mp_r_${this.id}.z);`,
2857
+ `_mp_v_${this.id} = _mp_v_${this.id} * ${scl};`,
2825
2858
  `vec3 ${out} = _mp_v_${this.id};`
2826
2859
  ].join("\n ");
2827
2860
  }
@@ -4859,6 +4892,7 @@ var ShaderScript = class extends ProcessNode {
4859
4892
  const defs = {};
4860
4893
  const supplied = {};
4861
4894
  for (const [name, spec] of Object.entries(config.inputs ?? {})) {
4895
+ assertGlslIdentifier(name, "ShaderScript input name");
4862
4896
  if (Array.isArray(spec)) {
4863
4897
  const [type, defaultVal] = spec;
4864
4898
  defs[name] = [type, defaultVal];
@@ -4868,6 +4902,9 @@ var ShaderScript = class extends ProcessNode {
4868
4902
  supplied[name] = sock;
4869
4903
  }
4870
4904
  }
4905
+ for (const name of Object.keys(this._outputDecl)) {
4906
+ assertGlslIdentifier(name, "ShaderScript output name");
4907
+ }
4871
4908
  this._inputs = this.createInputs(supplied, defs);
4872
4909
  this._outputs = this.createOutputs(this._outputDecl);
4873
4910
  }
@@ -1 +1 @@
1
- {"version":3,"file":"CompileContext.d.ts","sourceRoot":"","sources":["../../src/core/CompileContext.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAA;AACjD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAA;AACnD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAA;AAEjD,MAAM,WAAW,gBAAgB;IAC/B,YAAY,EAAI,MAAM,CAAA;IACtB,cAAc,EAAE,MAAM,CAAA;IACtB,QAAQ,EAAQ,MAAM,CAAC,MAAM,EAAE;QAAE,KAAK,EAAE,OAAO,CAAA;KAAE,CAAC,CAAA;IAClD,KAAK,EAAW,UAAU,EAAE,CAAA;CAC7B;AAED;;;;;;;;;GASG;AACH,qBAAa,cAAc;IACzB,OAAO,CAAC,OAAO,CAAqD;IACpE,OAAO,CAAC,QAAQ,CAAoD;IACpE,OAAO,CAAC,KAAK,CAAgD;IAC7D,OAAO,CAAC,WAAW,CAAiD;IACpE,OAAO,CAAC,IAAI,CAAiD;IAC7D,OAAO,CAAC,KAAK,CAAgD;IAC7D,OAAO,CAAC,SAAS,CAA4C;IAC7D,OAAO,CAAC,SAAS,CAAuD;IACxE,0FAA0F;IAC1F,OAAO,CAAC,iBAAiB,CAAiE;IAI1F,OAAO,CAAC,UAAU,EAAE,UAAU,GAAG,gBAAgB;IAcjD;;;OAGG;IACH,SAAS,CAAC,IAAI,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,GAAG,MAAM;IAKvD;;;;;OAKG;IACH,YAAY,CAAC,MAAM,EAAE,WAAW,CAAC,OAAO,CAAC,GAAG,MAAM;IAuBlD,OAAO,CAAC,KAAK;IA8Bb,OAAO,CAAC,IAAI;IAyBZ,OAAO,CAAC,QAAQ;IA0BhB,OAAO,CAAC,QAAQ;IAKhB,OAAO,CAAC,YAAY;IAsCpB,OAAO,CAAC,eAAe;IASvB,OAAO,CAAC,iBAAiB;IAuBzB,OAAO,CAAC,wBAAwB;IAMhC,OAAO,CAAC,mBAAmB;IA2B3B,OAAO,CAAC,SAAS;IAqBjB,MAAM,CAAC,aAAa,CAAC,GAAG,EAAE,MAAM,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC;IAS3D,MAAM,CAAC,SAAS,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM;IAQrC,OAAO,CAAC,SAAS;CAGlB"}
1
+ {"version":3,"file":"CompileContext.d.ts","sourceRoot":"","sources":["../../src/core/CompileContext.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAA;AACjD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAA;AACnD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAA;AAEjD,MAAM,WAAW,gBAAgB;IAC/B,YAAY,EAAI,MAAM,CAAA;IACtB,cAAc,EAAE,MAAM,CAAA;IACtB,QAAQ,EAAQ,MAAM,CAAC,MAAM,EAAE;QAAE,KAAK,EAAE,OAAO,CAAA;KAAE,CAAC,CAAA;IAClD,KAAK,EAAW,UAAU,EAAE,CAAA;CAC7B;AAED;;;;;;;;;GASG;AACH,qBAAa,cAAc;IACzB,OAAO,CAAC,OAAO,CAAqD;IACpE,OAAO,CAAC,QAAQ,CAAoD;IACpE,OAAO,CAAC,KAAK,CAAgD;IAC7D,OAAO,CAAC,WAAW,CAAiD;IACpE,OAAO,CAAC,IAAI,CAAiD;IAC7D,OAAO,CAAC,KAAK,CAAgD;IAC7D,OAAO,CAAC,SAAS,CAA4C;IAC7D,OAAO,CAAC,SAAS,CAAuD;IACxE,0FAA0F;IAC1F,OAAO,CAAC,iBAAiB,CAAiE;IAI1F,OAAO,CAAC,UAAU,EAAE,UAAU,GAAG,gBAAgB;IAcjD;;;OAGG;IACH,SAAS,CAAC,IAAI,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,GAAG,MAAM;IAKvD;;;;;OAKG;IACH,YAAY,CAAC,MAAM,EAAE,WAAW,CAAC,OAAO,CAAC,GAAG,MAAM;IAkClD,OAAO,CAAC,KAAK;IA8Bb,OAAO,CAAC,IAAI;IAyBZ,OAAO,CAAC,QAAQ;IA0BhB,OAAO,CAAC,QAAQ;IAKhB,OAAO,CAAC,YAAY;IAsCpB,OAAO,CAAC,eAAe;IAcvB,OAAO,CAAC,iBAAiB;IAuBzB,OAAO,CAAC,wBAAwB;IAMhC,OAAO,CAAC,mBAAmB;IA2B3B,OAAO,CAAC,SAAS;IAwBjB,MAAM,CAAC,aAAa,CAAC,GAAG,EAAE,MAAM,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC;IAS3D,MAAM,CAAC,SAAS,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM;IAQrC,OAAO,CAAC,SAAS;CAGlB"}
@@ -53,7 +53,16 @@ export class CompileContext {
53
53
  */
54
54
  resolveInput(socket) {
55
55
  if (socket.connection) {
56
- return this.outputVar(socket.connection.node, socket.connection.name);
56
+ const varName = this.outputVar(socket.connection.node, socket.connection.name);
57
+ // vec3 (color/shader) → vec2 (vector) implicit conversion, e.g. Mapping.Vector → ImageTexture.vector
58
+ if (socket.type === 'vector' && socket.connection.type !== 'vector') {
59
+ return `(${varName}).xy`;
60
+ }
61
+ // vec2 (vector) → vec3 (color) implicit conversion, e.g. TextureCoordinate.UV → Mapping.vector
62
+ if (socket.type === 'color' && socket.connection.type === 'vector') {
63
+ return `vec3(${varName}, 0.0)`;
64
+ }
65
+ return varName;
57
66
  }
58
67
  if (socket.uniformName !== null) {
59
68
  if (!(socket.uniformName in this._uniforms)) {
@@ -62,10 +71,12 @@ export class CompileContext {
62
71
  const rgb = typeof raw === 'string'
63
72
  ? CompileContext.hexToRgbArray(raw)
64
73
  : (Array.isArray(raw) ? raw : [1, 1, 1]);
65
- this._uniforms[socket.uniformName] = { value: rgb };
74
+ // Guard against NaN/Infinity reaching the GPU uniform.
75
+ this._uniforms[socket.uniformName] = { value: rgb.map(n => Number.isFinite(n) ? n : 0) };
66
76
  }
67
77
  else {
68
- this._uniforms[socket.uniformName] = { value: socket.defaultValue };
78
+ const n = socket.defaultValue;
79
+ this._uniforms[socket.uniformName] = { value: Number.isFinite(n) ? n : 0 };
69
80
  }
70
81
  }
71
82
  return socket.uniformName;
@@ -192,6 +203,13 @@ export class CompileContext {
192
203
  // shader and color are both vec3 — compatible
193
204
  if ((from === 'shader' || from === 'color') && (to === 'shader' || to === 'color'))
194
205
  return true;
206
+ // vec3 (color/shader) implicitly narrows to vec2 (vector) via .xy, mirroring Blender's
207
+ // Mapping (vector output) → Image Texture Vector panning graph
208
+ if ((from === 'color' || from === 'shader') && to === 'vector')
209
+ return true;
210
+ // vec2 (vector) implicitly widens to vec3 (color), e.g. Texture Coordinate UV → Mapping.vector
211
+ if (from === 'vector' && to === 'color')
212
+ return true;
195
213
  return false;
196
214
  }
197
215
  // ── GLSL builders ─────────────────────────────────────────────────
@@ -247,8 +265,11 @@ ${callsBlock}
247
265
  // ── Literal conversion ────────────────────────────────────────────
248
266
  toLiteral(value, type) {
249
267
  switch (type) {
250
- case 'float':
251
- return (typeof value === 'number' ? value : 0).toFixed(4);
268
+ case 'float': {
269
+ // Guard against NaN/Infinity reaching the shader source as an invalid GLSL literal.
270
+ const n = typeof value === 'number' && Number.isFinite(value) ? value : 0;
271
+ return n.toFixed(4);
272
+ }
252
273
  case 'color':
253
274
  return typeof value === 'string'
254
275
  ? this.hexToVec3(value)
@@ -1 +1 @@
1
- {"version":3,"file":"ShaderNode.d.ts","sourceRoot":"","sources":["../../src/core/ShaderNode.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAA;AAC9C,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAA;AAChD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAA;AAajD,6DAA6D;AAC7D,KAAK,QAAQ,GAAG,CAAC,UAAU,EAAE,OAAO,EAAE,OAAO,CAAC,CAAC,CAAA;AAE/C,8CAA8C;AAC9C,KAAK,SAAS,GAAG,UAAU,CAAA;AAE3B,MAAM,WAAW,YAAY;IAC3B,KAAK,EAAK,MAAM,CAAA;IAChB,QAAQ,EAAE,MAAM,CAAA;IAChB,KAAK,EAAK,MAAM,CAAA;IAChB,IAAI,EAAM,KAAK,GAAG,QAAQ,GAAG,MAAM,CAAA;IACnC,QAAQ,CAAC,EAAE,MAAM,CAAA;CAClB;AAED;;;GAGG;AACH,8BAAsB,UAAU;IAC9B,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAA;IAEnB,2EAA2E;IAC3E,YAAY,CAAC,EAAE,MAAM,CAAA;IAErB;;;;;OAKG;IACH,aAAa,IAAI,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC;IAEvC;;;;;;;;OAQG;IACH,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,CAAC,CAAK;IAElE,2EAA2E;IAC3E,OAAO,CAAC,WAAW,CAAmC;IACtD,yDAAyD;IACzD,OAAO,CAAC,YAAY,CAAkC;gBAE1C,QAAQ,EAAE,MAAM;IAI5B,QAAQ,KAAK,QAAQ,IAAI,MAAM,CAAA;IAC/B,QAAQ,KAAK,QAAQ,IAAI,YAAY,CAAA;IAErC,QAAQ,CAAC,eAAe,IAAK,MAAM,CAAC,MAAM,EAAE,WAAW,CAAC,OAAO,CAAC,CAAC;IACjE,QAAQ,CAAC,gBAAgB,IAAI,MAAM,CAAC,MAAM,EAAE,YAAY,CAAC;IAEzD;;;;;;;;;OASG;IACH,gBAAgB,IAAI,KAAK,CAAC;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,OAAO,GAAG,MAAM,GAAG,MAAM,CAAC;QAAC,WAAW,EAAE,MAAM,CAAA;KAAE,CAAC;IAIzG;;;OAGG;IACH,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,YAAY;IAelC;;;OAGG;IACH,QAAQ,CAAC,WAAW,IAAI,MAAM;IAE9B;;;OAGG;IACH,QAAQ,CAAC,WAAW,CAAC,GAAG,EAAE,OAAO,qBAAqB,EAAE,cAAc,GAAG,MAAM;IAE/E;;;;OAIG;IACH,MAAM,CAAC,mBAAmB,UAAQ;IAElC;;;;;;;;;;OAUG;IACH,SAAS,CAAC,YAAY,CACpB,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACjC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,QAAQ,CAAC,GAC7B,MAAM,CAAC,MAAM,EAAE,WAAW,CAAC,OAAO,CAAC,CAAC;IAgCvC;;;;;OAKG;IACH,SAAS,CAAC,aAAa,CACrB,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,GAC9B,MAAM,CAAC,MAAM,EAAE,YAAY,CAAC;IAQ/B;;;;OAIG;IACH,eAAe,CAAC,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE;QAAE,KAAK,EAAE,OAAO,CAAA;KAAE,CAAC,GAAG,IAAI;CAqBpE"}
1
+ {"version":3,"file":"ShaderNode.d.ts","sourceRoot":"","sources":["../../src/core/ShaderNode.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAA;AAC9C,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAA;AAChD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAA;AAsBjD,6DAA6D;AAC7D,KAAK,QAAQ,GAAG,CAAC,UAAU,EAAE,OAAO,EAAE,OAAO,CAAC,CAAC,CAAA;AAE/C,8CAA8C;AAC9C,KAAK,SAAS,GAAG,UAAU,CAAA;AAE3B,MAAM,WAAW,YAAY;IAC3B,KAAK,EAAK,MAAM,CAAA;IAChB,QAAQ,EAAE,MAAM,CAAA;IAChB,KAAK,EAAK,MAAM,CAAA;IAChB,IAAI,EAAM,KAAK,GAAG,QAAQ,GAAG,MAAM,CAAA;IACnC,QAAQ,CAAC,EAAE,MAAM,CAAA;CAClB;AAED;;;GAGG;AACH,8BAAsB,UAAU;IAC9B,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAA;IAEnB,2EAA2E;IAC3E,YAAY,CAAC,EAAE,MAAM,CAAA;IAErB;;;;;OAKG;IACH,aAAa,IAAI,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC;IAEvC;;;;;;;;OAQG;IACH,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,CAAC,CAAK;IAElE,2EAA2E;IAC3E,OAAO,CAAC,WAAW,CAAmC;IACtD,yDAAyD;IACzD,OAAO,CAAC,YAAY,CAAkC;gBAE1C,QAAQ,EAAE,MAAM;IAI5B,QAAQ,KAAK,QAAQ,IAAI,MAAM,CAAA;IAC/B,QAAQ,KAAK,QAAQ,IAAI,YAAY,CAAA;IAErC,QAAQ,CAAC,eAAe,IAAK,MAAM,CAAC,MAAM,EAAE,WAAW,CAAC,OAAO,CAAC,CAAC;IACjE,QAAQ,CAAC,gBAAgB,IAAI,MAAM,CAAC,MAAM,EAAE,YAAY,CAAC;IAEzD;;;;;;;;;OASG;IACH,gBAAgB,IAAI,KAAK,CAAC;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,OAAO,GAAG,MAAM,GAAG,MAAM,CAAC;QAAC,WAAW,EAAE,MAAM,CAAA;KAAE,CAAC;IAIzG;;;OAGG;IACH,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,YAAY;IAelC;;;OAGG;IACH,QAAQ,CAAC,WAAW,IAAI,MAAM;IAE9B;;;OAGG;IACH,QAAQ,CAAC,WAAW,CAAC,GAAG,EAAE,OAAO,qBAAqB,EAAE,cAAc,GAAG,MAAM;IAE/E;;;;OAIG;IACH,MAAM,CAAC,mBAAmB,UAAQ;IAElC;;;;;;;;;;OAUG;IACH,SAAS,CAAC,YAAY,CACpB,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACjC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,QAAQ,CAAC,GAC7B,MAAM,CAAC,MAAM,EAAE,WAAW,CAAC,OAAO,CAAC,CAAC;IAkCvC;;;;;OAKG;IACH,SAAS,CAAC,aAAa,CACrB,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,GAC9B,MAAM,CAAC,MAAM,EAAE,YAAY,CAAC;IAQ/B;;;;OAIG;IACH,eAAe,CAAC,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE;QAAE,KAAK,EAAE,OAAO,CAAA;KAAE,CAAC,GAAG,IAAI;CAqBpE"}
@@ -2,6 +2,13 @@ import { ShaderNodeError } from './ShaderNodeError.js';
2
2
  import { ShaderConfig } from './ShaderConfig.js';
3
3
  import { InputSocket } from './InputSocket.js';
4
4
  import { OutputSocket } from './OutputSocket.js';
5
+ /** Guards against NaN/Infinity reaching a GLSL uniform (would emit invalid GLSL literals like "NaN"). */
6
+ function finiteOr(value, fallback) {
7
+ return Number.isFinite(value) ? value : fallback;
8
+ }
9
+ function sanitizeVec3(v) {
10
+ return [finiteOr(v[0], 0), finiteOr(v[1], 0), finiteOr(v[2], 0)];
11
+ }
5
12
  function hexToRgb(hex) {
6
13
  const h = hex.replace('#', '');
7
14
  return [
@@ -92,7 +99,7 @@ export class ShaderNode {
92
99
  if (type === 'float' && typeof literal === 'number') {
93
100
  socket.uniformName = `u_${this.id}_${name}`;
94
101
  this._uniformMap.set(name, socket.uniformName);
95
- this.parameters[name] = literal;
102
+ this.parameters[name] = finiteOr(literal, 0);
96
103
  }
97
104
  // Auto-register unconnected color inputs as vec3 uniforms
98
105
  if (type === 'color' && !(value instanceof OutputSocket)) {
@@ -100,7 +107,9 @@ export class ShaderNode {
100
107
  socket.uniformName = `u_${this.id}_${name}`;
101
108
  this._uniformMap.set(name, socket.uniformName);
102
109
  this._colorParams.add(name);
103
- this.parameters[name] = typeof literal === 'string' ? hexToRgb(literal) : literal;
110
+ this.parameters[name] = typeof literal === 'string'
111
+ ? hexToRgb(literal)
112
+ : sanitizeVec3(literal);
104
113
  }
105
114
  }
106
115
  sockets[name] = socket;
@@ -131,7 +140,7 @@ export class ShaderNode {
131
140
  if (this._colorParams.has(paramName)) {
132
141
  Object.defineProperty(this.parameters, paramName, {
133
142
  get: () => uniforms[uniformName].value,
134
- set: (v) => { uniforms[uniformName].value = v; },
143
+ set: (v) => { uniforms[uniformName].value = sanitizeVec3(v); },
135
144
  enumerable: true,
136
145
  configurable: true,
137
146
  });
@@ -139,7 +148,7 @@ export class ShaderNode {
139
148
  else {
140
149
  Object.defineProperty(this.parameters, paramName, {
141
150
  get: () => uniforms[uniformName].value,
142
- set: (v) => { uniforms[uniformName].value = v; },
151
+ set: (v) => { uniforms[uniformName].value = finiteOr(v, 0); },
143
152
  enumerable: true,
144
153
  configurable: true,
145
154
  });
@@ -41,7 +41,7 @@ export class ImageTexture extends ProcessNode {
41
41
  }
42
42
  compileCall(ctx) {
43
43
  const uv = this._inputs.vector.isConnected()
44
- ? ctx.outputVar(this._inputs.vector.connection.node, this._inputs.vector.connection.name)
44
+ ? ctx.resolveInput(this._inputs.vector)
45
45
  : 'vUv';
46
46
  const cv = ctx.outputVar(this, 'Color');
47
47
  const av = ctx.outputVar(this, 'Alpha');
@@ -22,9 +22,6 @@ export declare class Mapping extends ProcessNode {
22
22
  static instanceSpecificDef: boolean;
23
23
  get metadata(): NodeMetadata;
24
24
  static glslFunction: string;
25
- private readonly loc;
26
- private readonly rot;
27
- private readonly scl;
28
25
  private readonly mode;
29
26
  private readonly _inputs;
30
27
  private readonly _outputs;
@@ -1 +1 @@
1
- {"version":3,"file":"Mapping.d.ts","sourceRoot":"","sources":["../../../src/nodes/process/Mapping.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,2BAA2B,CAAA;AACvD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAA;AAC5D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,8BAA8B,CAAA;AAClE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,4BAA4B,CAAA;AAE9D,MAAM,MAAM,WAAW,GAAG,SAAS,GAAG,OAAO,GAAG,QAAQ,GAAG,QAAQ,CAAA;AAEnE,MAAM,WAAW,aAAa;IAC5B,IAAI,CAAC,EAAM,WAAW,CAAA;IACtB,MAAM,CAAC,EAAI,YAAY,CAAA;IACvB,QAAQ,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,CAAA;IACnC,QAAQ,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,CAAA;IACnC,KAAK,CAAC,EAAK,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,CAAA;CACpC;AAED;;;;;;GAMG;AACH,qBAAa,OAAQ,SAAQ,WAAW;IACtC,IAAI,QAAQ,WAAuB;IACnC,MAAM,CAAC,mBAAmB,UAAQ;IAElC,IAAI,QAAQ,IAAI,YAAY,CAE3B;IAED,MAAM,CAAC,YAAY,SAYnB;IAEA,OAAO,CAAC,QAAQ,CAAC,GAAG,CAA0B;IAC9C,OAAO,CAAC,QAAQ,CAAC,GAAG,CAA0B;IAC9C,OAAO,CAAC,QAAQ,CAAC,GAAG,CAA0B;IAC9C,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAa;IAClC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAA2E;IACnG,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAmE;gBAEhF,MAAM,GAAE,aAAkB;IAUtC,eAAe;IACf,gBAAgB;IAChB,WAAW;IAEX,WAAW,CAAC,GAAG,EAAE,cAAc,GAAG,MAAM;CAkBzC"}
1
+ {"version":3,"file":"Mapping.d.ts","sourceRoot":"","sources":["../../../src/nodes/process/Mapping.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,2BAA2B,CAAA;AACvD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAA;AAC5D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,8BAA8B,CAAA;AAClE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,4BAA4B,CAAA;AAE9D,MAAM,MAAM,WAAW,GAAG,SAAS,GAAG,OAAO,GAAG,QAAQ,GAAG,QAAQ,CAAA;AAEnE,MAAM,WAAW,aAAa;IAC5B,IAAI,CAAC,EAAM,WAAW,CAAA;IACtB,MAAM,CAAC,EAAI,YAAY,CAAA;IACvB,QAAQ,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,CAAA;IACnC,QAAQ,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,CAAA;IACnC,KAAK,CAAC,EAAK,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,CAAA;CACpC;AAED;;;;;;GAMG;AACH,qBAAa,OAAQ,SAAQ,WAAW;IACtC,IAAI,QAAQ,WAAuB;IACnC,MAAM,CAAC,mBAAmB,UAAQ;IAElC,IAAI,QAAQ,IAAI,YAAY,CAE3B;IAED,MAAM,CAAC,YAAY,SAYnB;IAEA,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAa;IAClC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAA2E;IACnG,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAmE;gBAEhF,MAAM,GAAE,aAAkB;IAYtC,eAAe;IACf,gBAAgB;IAChB,WAAW;IAEX,WAAW,CAAC,GAAG,EAAE,cAAc,GAAG,MAAM;CAmBzC"}
@@ -14,10 +14,12 @@ export class Mapping extends ProcessNode {
14
14
  constructor(inputs = {}) {
15
15
  super('Mapping');
16
16
  this.mode = inputs.mode ?? 'TEXTURE';
17
- this.loc = inputs.location ?? [0, 0, 0];
18
- this.rot = inputs.rotation ?? [0, 0, 0];
19
- this.scl = inputs.scale ?? [1, 1, 1];
20
- this._inputs = this.createInputs(inputs, { vector: ['color', null] });
17
+ this._inputs = this.createInputs(inputs, {
18
+ vector: ['color', null],
19
+ location: ['color', inputs.location ?? [0, 0, 0]],
20
+ rotation: ['color', inputs.rotation ?? [0, 0, 0]],
21
+ scale: ['color', inputs.scale ?? [1, 1, 1]],
22
+ });
21
23
  this._outputs = this.createOutputs({ Vector: 'color' });
22
24
  }
23
25
  getInputSockets() { return this._inputs; }
@@ -25,33 +27,34 @@ export class Mapping extends ProcessNode {
25
27
  compileDefs() { return Mapping.glslFunction; }
26
28
  compileCall(ctx) {
27
29
  const v = ctx.resolveInput(this._inputs.vector);
30
+ const loc = ctx.resolveInput(this._inputs.location);
31
+ const rot = ctx.resolveInput(this._inputs.rotation);
32
+ const scl = ctx.resolveInput(this._inputs.scale);
28
33
  const out = ctx.outputVar(this, 'Vector');
29
- const toRad = (d) => (d * Math.PI / 180).toFixed(6);
30
- const [lx, ly, lz] = this.loc;
31
- const [rx, ry, rz] = this.rot.map(Number);
32
- const [sx, sy, sz] = this.scl;
34
+ const toRad = (Math.PI / 180).toFixed(6);
33
35
  return [
34
36
  `vec3 _mp_v_${this.id} = ${v};`,
35
- `_mp_v_${this.id} = _mp_v_${this.id} - vec3(${lx.toFixed(4)}, ${ly.toFixed(4)}, ${lz.toFixed(4)});`,
36
- `_mp_v_${this.id} = _st_rotateX(_mp_v_${this.id}, ${toRad(rx)});`,
37
- `_mp_v_${this.id} = _st_rotateY(_mp_v_${this.id}, ${toRad(ry)});`,
38
- `_mp_v_${this.id} = _st_rotateZ(_mp_v_${this.id}, ${toRad(rz)});`,
39
- `_mp_v_${this.id} = _mp_v_${this.id} * vec3(${sx.toFixed(4)}, ${sy.toFixed(4)}, ${sz.toFixed(4)});`,
37
+ `vec3 _mp_r_${this.id} = ${rot} * ${toRad};`,
38
+ `_mp_v_${this.id} = _mp_v_${this.id} - ${loc};`,
39
+ `_mp_v_${this.id} = _st_rotateX(_mp_v_${this.id}, _mp_r_${this.id}.x);`,
40
+ `_mp_v_${this.id} = _st_rotateY(_mp_v_${this.id}, _mp_r_${this.id}.y);`,
41
+ `_mp_v_${this.id} = _st_rotateZ(_mp_v_${this.id}, _mp_r_${this.id}.z);`,
42
+ `_mp_v_${this.id} = _mp_v_${this.id} * ${scl};`,
40
43
  `vec3 ${out} = _mp_v_${this.id};`,
41
44
  ].join('\n ');
42
45
  }
43
46
  }
44
47
  Mapping.instanceSpecificDef = false;
45
- Mapping.glslFunction = `
46
- vec3 _st_rotateX(vec3 v, float a) {
47
- float c = cos(a), s = sin(a);
48
- return vec3(v.x, c * v.y - s * v.z, s * v.y + c * v.z);
49
- }
50
- vec3 _st_rotateY(vec3 v, float a) {
51
- float c = cos(a), s = sin(a);
52
- return vec3(c * v.x + s * v.z, v.y, -s * v.x + c * v.z);
53
- }
54
- vec3 _st_rotateZ(vec3 v, float a) {
55
- float c = cos(a), s = sin(a);
56
- return vec3(c * v.x - s * v.y, s * v.x + c * v.y, v.z);
48
+ Mapping.glslFunction = `
49
+ vec3 _st_rotateX(vec3 v, float a) {
50
+ float c = cos(a), s = sin(a);
51
+ return vec3(v.x, c * v.y - s * v.z, s * v.y + c * v.z);
52
+ }
53
+ vec3 _st_rotateY(vec3 v, float a) {
54
+ float c = cos(a), s = sin(a);
55
+ return vec3(c * v.x + s * v.z, v.y, -s * v.x + c * v.z);
56
+ }
57
+ vec3 _st_rotateZ(vec3 v, float a) {
58
+ float c = cos(a), s = sin(a);
59
+ return vec3(c * v.x - s * v.y, s * v.x + c * v.y, v.z);
57
60
  }`;
@@ -3,20 +3,24 @@ import type { NodeMetadata } from '../../core/ShaderNode.js';
3
3
  import type { CompileContext } from '../../core/CompileContext.js';
4
4
  import type { OutputSocket } from '../../core/OutputSocket.js';
5
5
  export interface NormalMapInputs {
6
- fac?: OutputSocket;
6
+ color?: OutputSocket;
7
7
  strength?: number | OutputSocket;
8
8
  }
9
9
  /**
10
- * Normal Map — converts a float noise value into a surface normal.
11
- * Derives normals via numerical gradient of the input Fac value.
12
- * Connect to PrincipledBSDF.normal input for surface detail lighting.
10
+ * Normal Map — Blender "Normal Map" node equivalent.
11
+ * Decodes a tangent-space normal texture (RGB → vector, [0,1] → [-1,1]) and
12
+ * rotates it into world space using a derivative-based cotangent frame, so no
13
+ * explicit vertex tangent attribute is required.
14
+ *
15
+ * Connect ImageTexture.Color (sampling a non-color / linear normal map texture)
16
+ * to `color`. Connect the result to PrincipledBSDF.normal.
13
17
  *
14
18
  * Inputs:
15
- * fac (float) — greyscale height value, typically from NoiseTexture.Fac
16
- * strength (float) — normal intensity [0-10] default: 1.0
19
+ * color (color) — sampled RGB normal texture, still in [0,1] range
20
+ * strength (float) — normal intensity, default: 1.0
17
21
  *
18
22
  * Outputs:
19
- * Normal (vector) — perturbed surface normal, ready for PrincipledBSDF.normal
23
+ * Normal (color) — world-space perturbed normal, ready for PrincipledBSDF.normal
20
24
  */
21
25
  export declare class NormalMap extends ProcessNode {
22
26
  get nodeType(): string;
@@ -1 +1 @@
1
- {"version":3,"file":"NormalMap.d.ts","sourceRoot":"","sources":["../../../src/nodes/process/NormalMap.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,2BAA2B,CAAA;AACvD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAA;AAC5D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,8BAA8B,CAAA;AAClE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,4BAA4B,CAAA;AAE9D,MAAM,WAAW,eAAe;IAC9B,GAAG,CAAC,EAAO,YAAY,CAAA;IACvB,QAAQ,CAAC,EAAE,MAAM,GAAG,YAAY,CAAA;CACjC;AAED;;;;;;;;;;;GAWG;AACH,qBAAa,SAAU,SAAQ,WAAW;IACxC,IAAI,QAAQ,WAAyB;IACrC,MAAM,CAAC,mBAAmB,UAAQ;IAElC,IAAI,QAAQ,IAAI,YAAY,CAQ3B;IAED,MAAM,CAAC,YAAY,SAGnB;IAEA,OAAO,CAAC,QAAQ,CAAC,OAAO,CAA2E;IACnG,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAmE;gBAEhF,MAAM,GAAE,eAAoB;IASxC,eAAe;IACf,gBAAgB;IAChB,WAAW;IAEX,WAAW,CAAC,GAAG,EAAE,cAAc,GAAG,MAAM;CAezC"}
1
+ {"version":3,"file":"NormalMap.d.ts","sourceRoot":"","sources":["../../../src/nodes/process/NormalMap.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,2BAA2B,CAAA;AACvD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAA;AAC5D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,8BAA8B,CAAA;AAClE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,4BAA4B,CAAA;AAE9D,MAAM,WAAW,eAAe;IAC9B,KAAK,CAAC,EAAK,YAAY,CAAA;IACvB,QAAQ,CAAC,EAAE,MAAM,GAAG,YAAY,CAAA;CACjC;AAED;;;;;;;;;;;;;;;GAeG;AACH,qBAAa,SAAU,SAAQ,WAAW;IACxC,IAAI,QAAQ,WAAyB;IACrC,MAAM,CAAC,mBAAmB,UAAQ;IAElC,IAAI,QAAQ,IAAI,YAAY,CAQ3B;IAED,MAAM,CAAC,YAAY,SAmBnB;IAEA,OAAO,CAAC,QAAQ,CAAC,OAAO,CAA2E;IACnG,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAmE;gBAEhF,MAAM,GAAE,eAAoB;IASxC,eAAe;IACf,gBAAgB;IAChB,WAAW;IAEX,WAAW,CAAC,GAAG,EAAE,cAAc,GAAG,MAAM;CAOzC"}
@@ -1,15 +1,19 @@
1
1
  import { ProcessNode } from '../../core/ProcessNode.js';
2
2
  /**
3
- * Normal Map — converts a float noise value into a surface normal.
4
- * Derives normals via numerical gradient of the input Fac value.
5
- * Connect to PrincipledBSDF.normal input for surface detail lighting.
3
+ * Normal Map — Blender "Normal Map" node equivalent.
4
+ * Decodes a tangent-space normal texture (RGB → vector, [0,1] → [-1,1]) and
5
+ * rotates it into world space using a derivative-based cotangent frame, so no
6
+ * explicit vertex tangent attribute is required.
7
+ *
8
+ * Connect ImageTexture.Color (sampling a non-color / linear normal map texture)
9
+ * to `color`. Connect the result to PrincipledBSDF.normal.
6
10
  *
7
11
  * Inputs:
8
- * fac (float) — greyscale height value, typically from NoiseTexture.Fac
9
- * strength (float) — normal intensity [0-10] default: 1.0
12
+ * color (color) — sampled RGB normal texture, still in [0,1] range
13
+ * strength (float) — normal intensity, default: 1.0
10
14
  *
11
15
  * Outputs:
12
- * Normal (vector) — perturbed surface normal, ready for PrincipledBSDF.normal
16
+ * Normal (color) — world-space perturbed normal, ready for PrincipledBSDF.normal
13
17
  */
14
18
  export class NormalMap extends ProcessNode {
15
19
  get nodeType() { return 'NormalMap'; }
@@ -19,36 +23,45 @@ export class NormalMap extends ProcessNode {
19
23
  category: 'Vector',
20
24
  color: '#4a3a8a',
21
25
  cost: 'medium',
22
- costNote: 'Samples input Fac four times for gradient computation.',
26
+ costNote: 'Builds a derivative-based TBN frame per fragment.',
23
27
  };
24
28
  }
25
29
  constructor(inputs = {}) {
26
30
  super('NormalMap');
27
31
  this._inputs = this.createInputs(inputs, {
28
- fac: ['float', 0.5],
32
+ color: ['color', [0.5, 0.5, 1.0]],
29
33
  strength: ['float', inputs.strength ?? 1.0],
30
34
  });
31
35
  this._outputs = this.createOutputs({ Normal: 'color' });
32
36
  }
33
37
  getInputSockets() { return this._inputs; }
34
38
  getOutputSockets() { return this._outputs; }
35
- compileDefs() { return ''; }
39
+ compileDefs() { return NormalMap.glslFunction; }
36
40
  compileCall(ctx) {
37
- const facVar = this._inputs.fac.isConnected()
38
- ? ctx.outputVar(this._inputs.fac.connection.node, this._inputs.fac.connection.name)
39
- : ctx.resolveInput(this._inputs.fac);
41
+ const color = ctx.resolveInput(this._inputs.color);
40
42
  const strength = ctx.resolveInput(this._inputs.strength);
41
43
  const normalVar = ctx.outputVar(this, 'Normal');
42
- // Numerical gradient — samples fac variable neighbour delta via world position
43
- return [
44
- `float _nm_nx_${this.id} = dFdx(${facVar});`,
45
- `float _nm_ny_${this.id} = dFdy(${facVar});`,
46
- `vec3 ${normalVar} = normalize(vec3(-_nm_nx_${this.id} * ${strength}, 1.0, -_nm_ny_${this.id} * ${strength}));`,
47
- ].join('\n ');
44
+ return `vec3 ${normalVar} = _st_normalMap(${color}, ${strength}, normalize(vNormal), vPosition, vUv);`;
48
45
  }
49
46
  }
50
47
  NormalMap.instanceSpecificDef = false;
51
- NormalMap.glslFunction = `
52
- vec3 _st_normalMap(float fac, float strength) {
53
- return vec3(0.0, 1.0, 0.0);
48
+ NormalMap.glslFunction = `
49
+ vec3 _st_normalMap(vec3 sampledColor, float strength, vec3 N, vec3 P, vec2 uv) {
50
+ vec3 n = sampledColor * 2.0 - 1.0;
51
+ n.xy *= strength;
52
+
53
+ // Derivative-based cotangent frame (Schüler) — no vertex tangent attribute needed.
54
+ vec3 dp1 = dFdx(P);
55
+ vec3 dp2 = dFdy(P);
56
+ vec2 duv1 = dFdx(uv);
57
+ vec2 duv2 = dFdy(uv);
58
+
59
+ vec3 dp2perp = cross(dp2, N);
60
+ vec3 dp1perp = cross(N, dp1);
61
+ vec3 T = dp2perp * duv1.x + dp1perp * duv2.x;
62
+ vec3 B = dp2perp * duv1.y + dp1perp * duv2.y;
63
+
64
+ float invmax = inversesqrt(max(dot(T, T), dot(B, B)));
65
+ mat3 TBN = mat3(T * invmax, B * invmax, N);
66
+ return normalize(TBN * n);
54
67
  }`;
@@ -1 +1 @@
1
- {"version":3,"file":"ShaderScript.d.ts","sourceRoot":"","sources":["../../../src/nodes/process/ShaderScript.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,2BAA2B,CAAA;AACvD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAA;AAC5D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,8BAA8B,CAAA;AAClE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,4BAA4B,CAAA;AAK9D;;;;;;GAMG;AACH,MAAM,MAAM,eAAe,GAAG,CAAC,OAAO,GAAG,OAAO,GAAG,QAAQ,GAAG,QAAQ,EAAE,OAAO,CAAC,CAAA;AAEhF,oDAAoD;AACpD,MAAM,MAAM,gBAAgB,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,GAAG,OAAO,GAAG,QAAQ,GAAG,QAAQ,CAAC,CAAA;AAEtF,MAAM,WAAW,kBAAkB;IACjC;;;;OAIG;IACH,MAAM,CAAC,EAAG,MAAM,CAAC,MAAM,EAAE,eAAe,GAAG,YAAY,CAAC,CAAA;IAExD,2CAA2C;IAC3C,OAAO,CAAC,EAAE,gBAAgB,CAAA;IAE1B;;;;;;;;;OASG;IACH,IAAI,EAAE,MAAM,CAAA;IAEZ;;;OAGG;IACH,IAAI,CAAC,EAAE,MAAM,CAAA;IAEb,yDAAyD;IACzD,KAAK,CAAC,EAAE,MAAM,CAAA;CACf;AAID;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,qBAAa,YAAa,SAAQ,WAAW;IAC3C,IAAI,QAAQ,WAA4B;IACxC,MAAM,CAAC,mBAAmB,UAAO;IAEjC,IAAI,QAAQ,IAAI,YAAY,CAQ3B;IAED,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAe;IACrC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAe;IACrC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAc;IACrC,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAmB;IAC/C,OAAO,CAAC,QAAQ,CAAC,OAAO,CAA+E;IACvG,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAkC;gBAE/C,MAAM,EAAE,kBAAkB;IA8BtC,eAAe;IACf,gBAAgB;IAEhB,mDAAmD;IACnD,WAAW,IAAI,MAAM;IAErB;;;;;;OAMG;IACH,WAAW,CAAC,GAAG,EAAE,cAAc,GAAG,MAAM;IAkCxC,OAAO,CAAC,eAAe;IAMvB,OAAO,CAAC,aAAa;CAKtB"}
1
+ {"version":3,"file":"ShaderScript.d.ts","sourceRoot":"","sources":["../../../src/nodes/process/ShaderScript.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,2BAA2B,CAAA;AAEvD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAA;AAC5D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,8BAA8B,CAAA;AAClE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,4BAA4B,CAAA;AAK9D;;;;;;GAMG;AACH,MAAM,MAAM,eAAe,GAAG,CAAC,OAAO,GAAG,OAAO,GAAG,QAAQ,GAAG,QAAQ,EAAE,OAAO,CAAC,CAAA;AAEhF,oDAAoD;AACpD,MAAM,MAAM,gBAAgB,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,GAAG,OAAO,GAAG,QAAQ,GAAG,QAAQ,CAAC,CAAA;AAEtF,MAAM,WAAW,kBAAkB;IACjC;;;;OAIG;IACH,MAAM,CAAC,EAAG,MAAM,CAAC,MAAM,EAAE,eAAe,GAAG,YAAY,CAAC,CAAA;IAExD,2CAA2C;IAC3C,OAAO,CAAC,EAAE,gBAAgB,CAAA;IAE1B;;;;;;;;;OASG;IACH,IAAI,EAAE,MAAM,CAAA;IAEZ;;;OAGG;IACH,IAAI,CAAC,EAAE,MAAM,CAAA;IAEb,yDAAyD;IACzD,KAAK,CAAC,EAAE,MAAM,CAAA;CACf;AAID;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,qBAAa,YAAa,SAAQ,WAAW;IAC3C,IAAI,QAAQ,WAA4B;IACxC,MAAM,CAAC,mBAAmB,UAAO;IAEjC,IAAI,QAAQ,IAAI,YAAY,CAQ3B;IAED,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAe;IACrC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAe;IACrC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAc;IACrC,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAmB;IAC/C,OAAO,CAAC,QAAQ,CAAC,OAAO,CAA+E;IACvG,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAkC;gBAE/C,MAAM,EAAE,kBAAkB;IAmCtC,eAAe;IACf,gBAAgB;IAEhB,mDAAmD;IACnD,WAAW,IAAI,MAAM;IAErB;;;;;;OAMG;IACH,WAAW,CAAC,GAAG,EAAE,cAAc,GAAG,MAAM;IAkCxC,OAAO,CAAC,eAAe;IAMvB,OAAO,CAAC,aAAa;CAKtB"}
@@ -1,4 +1,5 @@
1
1
  import { ProcessNode } from '../../core/ProcessNode.js';
2
+ import { assertGlslIdentifier } from '../../core/glslIdentifier.js';
2
3
  // ── ShaderScript node ─────────────────────────────────────────────────────────
3
4
  /**
4
5
  * ShaderScript — Blender "Script Node (OSL)" equivalent for st-shader-core.
@@ -47,6 +48,7 @@ export class ShaderScript extends ProcessNode {
47
48
  const defs = {};
48
49
  const supplied = {};
49
50
  for (const [name, spec] of Object.entries(config.inputs ?? {})) {
51
+ assertGlslIdentifier(name, 'ShaderScript input name');
50
52
  if (Array.isArray(spec)) {
51
53
  // ['float'|'color'|'vector'|'shader', defaultValue]
52
54
  const [type, defaultVal] = spec;
@@ -60,6 +62,9 @@ export class ShaderScript extends ProcessNode {
60
62
  supplied[name] = sock;
61
63
  }
62
64
  }
65
+ for (const name of Object.keys(this._outputDecl)) {
66
+ assertGlslIdentifier(name, 'ShaderScript output name');
67
+ }
63
68
  this._inputs = this.createInputs(supplied, defs);
64
69
  this._outputs = this.createOutputs(this._outputDecl);
65
70
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@triforge/shader-core",
3
- "version": "0.1.2",
3
+ "version": "0.2.1",
4
4
  "description": "GLSL shader library for Three.js with Blender workflow",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",