reze-engine 0.16.2 → 0.17.0

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.
Files changed (80) hide show
  1. package/dist/engine.d.ts +3 -1
  2. package/dist/engine.d.ts.map +1 -1
  3. package/dist/engine.js +165 -26
  4. package/dist/model.d.ts +5 -1
  5. package/dist/model.d.ts.map +1 -1
  6. package/dist/model.js +14 -1
  7. package/dist/physics/body.d.ts +4 -1
  8. package/dist/physics/body.d.ts.map +1 -1
  9. package/dist/physics/body.js +86 -20
  10. package/dist/physics/constraint.d.ts +15 -2
  11. package/dist/physics/constraint.d.ts.map +1 -1
  12. package/dist/physics/constraint.js +49 -2
  13. package/dist/physics/physics.d.ts +3 -0
  14. package/dist/physics/physics.d.ts.map +1 -1
  15. package/dist/physics/physics.js +69 -1
  16. package/dist/physics/solver.d.ts.map +1 -1
  17. package/dist/physics/solver.js +289 -113
  18. package/dist/physics/types.d.ts +2 -1
  19. package/dist/physics/types.d.ts.map +1 -1
  20. package/dist/physics/types.js +5 -0
  21. package/dist/physics/world.d.ts +3 -0
  22. package/dist/physics/world.d.ts.map +1 -1
  23. package/dist/physics/world.js +19 -3
  24. package/dist/pmx-loader.d.ts +2 -0
  25. package/dist/pmx-loader.d.ts.map +1 -1
  26. package/dist/pmx-loader.js +38 -20
  27. package/dist/shaders/materials/body.d.ts +1 -1
  28. package/dist/shaders/materials/body.d.ts.map +1 -1
  29. package/dist/shaders/materials/body.js +90 -85
  30. package/dist/shaders/materials/cloth_rough.d.ts +1 -1
  31. package/dist/shaders/materials/cloth_rough.d.ts.map +1 -1
  32. package/dist/shaders/materials/cloth_rough.js +1 -1
  33. package/dist/shaders/materials/cloth_smooth.d.ts +1 -1
  34. package/dist/shaders/materials/cloth_smooth.d.ts.map +1 -1
  35. package/dist/shaders/materials/cloth_smooth.js +2 -2
  36. package/dist/shaders/materials/common.d.ts +1 -1
  37. package/dist/shaders/materials/common.d.ts.map +1 -1
  38. package/dist/shaders/materials/common.js +137 -112
  39. package/dist/shaders/materials/default.d.ts +1 -1
  40. package/dist/shaders/materials/default.d.ts.map +1 -1
  41. package/dist/shaders/materials/default.js +37 -32
  42. package/dist/shaders/materials/eye.d.ts +1 -1
  43. package/dist/shaders/materials/eye.d.ts.map +1 -1
  44. package/dist/shaders/materials/eye.js +54 -35
  45. package/dist/shaders/materials/face.d.ts +1 -1
  46. package/dist/shaders/materials/face.d.ts.map +1 -1
  47. package/dist/shaders/materials/face.js +90 -85
  48. package/dist/shaders/materials/hair.d.ts +1 -1
  49. package/dist/shaders/materials/hair.d.ts.map +1 -1
  50. package/dist/shaders/materials/hair.js +85 -75
  51. package/dist/shaders/materials/metal.d.ts +1 -1
  52. package/dist/shaders/materials/metal.d.ts.map +1 -1
  53. package/dist/shaders/materials/metal.js +1 -1
  54. package/dist/shaders/materials/mmd_classic.d.ts +2 -0
  55. package/dist/shaders/materials/mmd_classic.d.ts.map +1 -0
  56. package/dist/shaders/materials/mmd_classic.js +66 -0
  57. package/dist/shaders/materials/stockings.d.ts +1 -1
  58. package/dist/shaders/materials/stockings.d.ts.map +1 -1
  59. package/dist/shaders/materials/stockings.js +1 -1
  60. package/package.json +1 -1
  61. package/src/engine.ts +4198 -4030
  62. package/src/model.ts +21 -1
  63. package/src/physics/body.ts +87 -24
  64. package/src/physics/constraint.ts +80 -6
  65. package/src/physics/physics.ts +68 -1
  66. package/src/physics/solver.ts +915 -744
  67. package/src/physics/types.ts +10 -2
  68. package/src/physics/world.ts +20 -3
  69. package/src/pmx-loader.ts +39 -20
  70. package/src/shaders/materials/body.ts +97 -92
  71. package/src/shaders/materials/cloth_rough.ts +1 -1
  72. package/src/shaders/materials/cloth_smooth.ts +2 -2
  73. package/src/shaders/materials/common.ts +204 -179
  74. package/src/shaders/materials/default.ts +45 -40
  75. package/src/shaders/materials/eye.ts +62 -43
  76. package/src/shaders/materials/face.ts +97 -92
  77. package/src/shaders/materials/hair.ts +92 -82
  78. package/src/shaders/materials/metal.ts +1 -1
  79. package/src/shaders/materials/mmd_classic.ts +68 -0
  80. package/src/shaders/materials/stockings.ts +1 -1
@@ -6,10 +6,15 @@ export enum RigidbodyShape {
6
6
  Capsule = 2,
7
7
  }
8
8
 
9
+ // PMX physics mode: 0 = follow bone, 1 = physics, 2 = physics + bone-position
10
+ // alignment. Mode 2 is DYNAMIC — the loader maps it to Dynamic with
11
+ // `aligned: true` (bone gets rotation only, body position re-pins to the
12
+ // animated bone each frame). Mapping the raw byte 1:1 onto this enum froze
13
+ // mode-2 bodies (most modern cloth rigs, and every 胸_回転 breast body).
9
14
  export enum RigidbodyType {
10
- Static = 0,
15
+ Static = 0, // follows bone (anchor)
11
16
  Dynamic = 1,
12
- Kinematic = 2,
17
+ Kinematic = 2, // follows bone (legacy alias; loader no longer emits it)
13
18
  }
14
19
 
15
20
  export interface Rigidbody {
@@ -28,6 +33,9 @@ export interface Rigidbody {
28
33
  restitution: number
29
34
  friction: number
30
35
  type: RigidbodyType
36
+ // PMX mode 2: dynamic body whose bone takes rotation only; the body's
37
+ // position re-pins to the animated bone each frame.
38
+ aligned?: boolean
31
39
  bodyOffsetMatrixInverse: Mat4 // Inverse of body offset matrix, used to sync rigidbody to bone
32
40
  bodyOffsetMatrix?: Mat4 // Cached non-inverse for performance (computed once during initialization)
33
41
  }
@@ -14,6 +14,12 @@ export class World {
14
14
  readonly gravity: Vec3
15
15
  solverIterations = 10
16
16
 
17
+ // Per-body damping factors pow(1−damping, dt), cached because damping and
18
+ // the fixed dt never change — two Math.pow per body per substep otherwise.
19
+ private dampCacheDt = -1
20
+ private linDampFactor: Float32Array | null = null
21
+ private angDampFactor: Float32Array | null = null
22
+
17
23
  constructor(gravity: Vec3) {
18
24
  this.gravity = new Vec3(gravity.x, gravity.y, gravity.z)
19
25
  }
@@ -43,15 +49,26 @@ export class World {
43
49
 
44
50
  // 1. Predict — gravity + damping. The pow form (vs the linear
45
51
  // 1−damping·dt approximation) stays stable at high PMX damping
46
- // values like 0.99.
52
+ // values like 0.99. Factors are cached (damping and dt are constant).
53
+ if (this.dampCacheDt !== dt || !this.linDampFactor || this.linDampFactor.length !== N) {
54
+ this.dampCacheDt = dt
55
+ this.linDampFactor = new Float32Array(N)
56
+ this.angDampFactor = new Float32Array(N)
57
+ for (let i = 0; i < N; i++) {
58
+ this.linDampFactor[i] = Math.pow(Math.max(0, 1 - ldamp[i]), dt)
59
+ this.angDampFactor[i] = Math.pow(Math.max(0, 1 - adamp[i]), dt)
60
+ }
61
+ }
62
+ const linDamp = this.linDampFactor
63
+ const angDamp = this.angDampFactor!
47
64
  for (let i = 0; i < N; i++) {
48
65
  if (types[i] !== RigidbodyType.Dynamic || invMass[i] <= 0) continue
49
66
  const i3 = i * 3
50
67
  lv[i3 + 0] += gx * dt
51
68
  lv[i3 + 1] += gy * dt
52
69
  lv[i3 + 2] += gz * dt
53
- const ld = Math.pow(Math.max(0, 1 - ldamp[i]), dt)
54
- const ad = Math.pow(Math.max(0, 1 - adamp[i]), dt)
70
+ const ld = linDamp[i]
71
+ const ad = angDamp[i]
55
72
  lv[i3 + 0] *= ld; lv[i3 + 1] *= ld; lv[i3 + 2] *= ld
56
73
  av[i3 + 0] *= ad; av[i3 + 1] *= ad; av[i3 + 2] *= ad
57
74
  }
package/src/pmx-loader.ts CHANGED
@@ -37,11 +37,22 @@ export class PmxLoader {
37
37
  private vertexCount: number = 0
38
38
  private rigidbodies: Rigidbody[] = []
39
39
  private joints: Joint[] = []
40
+ private warnings: string[] = []
40
41
 
41
42
  private constructor(buffer: ArrayBuffer) {
42
43
  this.view = new DataView(buffer)
43
44
  }
44
45
 
46
+ // Parse problems are non-fatal (the mesh may still render), but they must
47
+ // be queryable afterwards — a rig whose rigidbody section failed to parse
48
+ // silently loses physics, and console.warn alone drowns in browser log
49
+ // limits. Collected warnings ship on the Model (getLoadWarnings()).
50
+ private warn(message: string, error?: unknown): void {
51
+ const full = error instanceof Error ? `${message}: ${error.message}` : message
52
+ this.warnings.push(full)
53
+ console.warn(message, error ?? "")
54
+ }
55
+
45
56
  static async load(url: string): Promise<Model> {
46
57
  return PmxLoader.loadFromReader(createFetchAssetReader(), url)
47
58
  }
@@ -82,7 +93,7 @@ export class PmxLoader {
82
93
  const version = this.getFloat32()
83
94
  if (version < 2.0 || version > 2.2) {
84
95
  // Continue, but warn for unexpected version
85
- console.warn(`PMX version ${version} may not be fully supported`)
96
+ this.warn(`PMX version ${version} may not be fully supported`)
86
97
  }
87
98
 
88
99
  // PMX: globals count (uint8) followed by that many bytes describing encoding and index sizes
@@ -297,6 +308,7 @@ export class PmxLoader {
297
308
  sphereTextureIndex,
298
309
  sphereMode: sphereTextureMode,
299
310
  toonTextureIndex,
311
+ sharedToon: isSharedToonTexture,
300
312
  edgeFlag: flag,
301
313
  edgeColor,
302
314
  edgeSize,
@@ -472,7 +484,7 @@ export class PmxLoader {
472
484
  }
473
485
  this.bones = bones
474
486
  } catch (e) {
475
- console.warn("Error parsing bones:", e)
487
+ this.warn("Error parsing bones:", e)
476
488
  this.bones = []
477
489
  }
478
490
  }
@@ -487,7 +499,7 @@ export class PmxLoader {
487
499
  const count = this.getInt32()
488
500
  if (count < 0 || count > 100000) {
489
501
  // Suspicious count, likely corrupted
490
- console.warn(`Suspicious morph count: ${count}`)
502
+ this.warn(`Suspicious morph count: ${count}`)
491
503
  this.morphs = []
492
504
  return
493
505
  }
@@ -623,11 +635,11 @@ export class PmxLoader {
623
635
  this.morphs.push(morph)
624
636
  } catch (e) {
625
637
  // If we fail to read a morph, skip it
626
- console.warn(`Error reading morph ${i}:`, e)
638
+ this.warn(`Error reading morph ${i}:`, e)
627
639
  }
628
640
  }
629
641
  } catch (e) {
630
- console.warn("Error parsing morphs:", e)
642
+ this.warn("Error parsing morphs:", e)
631
643
  this.morphs = []
632
644
  }
633
645
  }
@@ -669,13 +681,13 @@ export class PmxLoader {
669
681
  }
670
682
  } catch (e) {
671
683
  // If we fail to read a frame, stop skipping
672
- console.warn(`Error reading display frame ${i}:`, e)
684
+ this.warn(`Error reading display frame ${i}:`, e)
673
685
  return false
674
686
  }
675
687
  }
676
688
  return true
677
689
  } catch (e) {
678
- console.warn("Error skipping display frames:", e)
690
+ this.warn("Error skipping display frames:", e)
679
691
  return false
680
692
  }
681
693
  }
@@ -685,7 +697,7 @@ export class PmxLoader {
685
697
  // Note: morphs and display frames are already skipped in parse() before calling this
686
698
  // Check bounds before reading rigidbody count
687
699
  if (this.offset + 4 > this.view.buffer.byteLength) {
688
- console.warn("Not enough bytes for rigidbody count")
700
+ this.warn("Not enough bytes for rigidbody count")
689
701
  this.rigidbodies = []
690
702
  return
691
703
  }
@@ -693,7 +705,7 @@ export class PmxLoader {
693
705
  const count = this.getInt32()
694
706
  if (count < 0 || count > 10000) {
695
707
  // Suspicious count
696
- console.warn(`Suspicious rigidbody count: ${count}`)
708
+ this.warn(`Suspicious rigidbody count: ${count}`)
697
709
  this.rigidbodies = []
698
710
  return
699
711
  }
@@ -704,7 +716,7 @@ export class PmxLoader {
704
716
  try {
705
717
  // Check bounds before reading each rigidbody
706
718
  if (this.offset >= this.view.buffer.byteLength) {
707
- console.warn(`Reached end of buffer while reading rigidbody ${i} of ${count}`)
719
+ this.warn(`Reached end of buffer while reading rigidbody ${i} of ${count}`)
708
720
  break
709
721
  }
710
722
 
@@ -737,7 +749,12 @@ export class PmxLoader {
737
749
  const angularDamping = this.getFloat32()
738
750
  const restitution = this.getFloat32()
739
751
  const friction = this.getFloat32()
740
- const type = this.getUint8() // 0=static, 1=dynamic, 2=kinematic
752
+ // PMX physics mode: 0 = follow bone, 1 = physics,
753
+ // 2 = physics + bone-position alignment. Mode 2 is dynamic — it
754
+ // must NOT be cast onto RigidbodyType (2 = Kinematic there, which
755
+ // freezes the body to its bone and kills physics for most modern
756
+ // cloth rigs and every 胸_回転 breast body).
757
+ const mode = this.getUint8()
741
758
 
742
759
  this.rigidbodies.push({
743
760
  name,
@@ -754,17 +771,18 @@ export class PmxLoader {
754
771
  angularDamping,
755
772
  restitution,
756
773
  friction,
757
- type: type as RigidbodyType,
774
+ type: mode === 0 ? RigidbodyType.Static : RigidbodyType.Dynamic,
775
+ aligned: mode === 2,
758
776
  bodyOffsetMatrixInverse: Mat4.identity(),
759
777
  })
760
778
  } catch (e) {
761
- console.warn(`Error reading rigidbody ${i} of ${count}:`, e)
779
+ this.warn(`Error reading rigidbody ${i} of ${count}:`, e)
762
780
  // Stop parsing if we encounter an error
763
781
  break
764
782
  }
765
783
  }
766
784
  } catch (e) {
767
- console.warn("Error parsing rigidbodies:", e)
785
+ this.warn("Error parsing rigidbodies:", e)
768
786
  this.rigidbodies = []
769
787
  }
770
788
  }
@@ -773,14 +791,14 @@ export class PmxLoader {
773
791
  try {
774
792
  // Check bounds before reading joint count
775
793
  if (this.offset + 4 > this.view.buffer.byteLength) {
776
- console.warn("Not enough bytes for joint count")
794
+ this.warn("Not enough bytes for joint count")
777
795
  this.joints = []
778
796
  return
779
797
  }
780
798
 
781
799
  const count = this.getInt32()
782
800
  if (count < 0 || count > 10000) {
783
- console.warn(`Suspicious joint count: ${count}`)
801
+ this.warn(`Suspicious joint count: ${count}`)
784
802
  this.joints = []
785
803
  return
786
804
  }
@@ -791,7 +809,7 @@ export class PmxLoader {
791
809
  try {
792
810
  // Check bounds before reading each joint
793
811
  if (this.offset >= this.view.buffer.byteLength) {
794
- console.warn(`Reached end of buffer while reading joint ${i} of ${count}`)
812
+ this.warn(`Reached end of buffer while reading joint ${i} of ${count}`)
795
813
  break
796
814
  }
797
815
 
@@ -857,13 +875,13 @@ export class PmxLoader {
857
875
  springRotation: new Vec3(springRotX, springRotY, springRotZ), // Store as Vec3 (stiffness values)
858
876
  })
859
877
  } catch (e) {
860
- console.warn(`Error reading joint ${i} of ${count}:`, e)
878
+ this.warn(`Error reading joint ${i} of ${count}:`, e)
861
879
  // Stop parsing if we encounter an error
862
880
  break
863
881
  }
864
882
  }
865
883
  } catch (e) {
866
- console.warn("Error parsing joints:", e)
884
+ this.warn("Error parsing joints:", e)
867
885
  this.joints = []
868
886
  }
869
887
  }
@@ -1044,7 +1062,8 @@ export class PmxLoader {
1044
1062
  skinning,
1045
1063
  morphing,
1046
1064
  this.rigidbodies,
1047
- this.joints
1065
+ this.joints,
1066
+ this.warnings
1048
1067
  )
1049
1068
  }
1050
1069
 
@@ -1,92 +1,97 @@
1
- // M_Body — 仿深空之眼渲染预设v1.0_by_小绿毛猫 "M_Body". Toon + warm rim + rim1/rim2
2
- // stack mixed 50/50 against a Principled BSDF with noise-bumped normal.
3
-
4
- import { NODES_WGSL } from "./nodes"
5
- import { COMMON_MATERIAL_PRELUDE_WGSL } from "./common"
6
-
7
- export const BODY_SHADER_WGSL = /* wgsl */ `
8
-
9
- ${NODES_WGSL}
10
- ${COMMON_MATERIAL_PRELUDE_WGSL}
11
-
12
- const BODY_ROUGHNESS: f32 = 0.3;
13
- const BODY_SPECULAR: f32 = 0.5;
14
- const BODY_MIX_NPR: f32 = 0.5;
15
- const BODY_SPEC_CLAMP: f32 = 10.0;
16
- const BODY_RIM2_LAYER_BLEND: f32 = 0.20000000298023224;
17
- const BODY_RIM2_POW: f32 = 1.4300000667572021;
18
- const BODY_RIM2_BG: vec3f = vec3f(1.0, 0.4303792119026184, 0.3315804898738861);
19
- const BODY_WARM_STR: f32 = 0.30000001192092896;
20
-
21
- // smoothstep-based ramp: t*t*(3-2*t) between two color stops
22
- fn ramp_ease(f: f32, p0: f32, c0: vec4f, p1: f32, c1: vec4f) -> vec4f {
23
- let t = saturate((f - p0) / max(p1 - p0, 1e-6));
24
- let ss = t * t * (3.0 - 2.0 * t);
25
- return mix(c0, c1, ss);
26
- }
27
-
28
- @fragment fn fs(input: VertexOutput) -> FSOut {
29
- let alpha = material.alpha;
30
- if (alpha < 0.001) { discard; }
31
-
32
- let n = normalize(input.normal);
33
- let v = normalize(camera.viewPos - input.worldPos);
34
- let l = -light.lights[0].direction.xyz;
35
- let sun = light.lights[0].color.xyz * light.lights[0].color.w;
36
- let amb = light.ambientColor.xyz;
37
- let shadow = sampleShadow(input.worldPos, n);
38
-
39
- let tex_color = textureSample(diffuseTexture, diffuseSampler, input.uv).rgb;
40
-
41
- // ═══ NPR STACK ═══
42
- let ndotl_raw = shader_to_rgb_diffuse(n, l, sun, amb, shadow);
43
- let toon = ramp_constant(ndotl_raw, 0.0, vec4f(0,0,0,1), 0.2966, vec4f(1,1,1,1)).r;
44
-
45
- let shadow_tint = hue_sat_id(2.0, 0.3499999940395355, 1.0, tex_color);
46
- let lit_tint = hue_sat_id(1.5, 1.0, 1.0, tex_color);
47
- let toon_color = mix_blend(toon, shadow_tint, lit_tint);
48
- let bc = bright_contrast(toon_color, 0.1, 0.2);
49
-
50
- let emission3 = bc * 4.0;
51
-
52
- let warm_input = clamp(toon + 0.5, 0.0, 1.0);
53
- let warm_color = ramp_cardinal(warm_input, 0.2409,
54
- vec4f(0.2426, 0.068, 0.0588, 1.0), 0.4663,
55
- vec4f(0.6677, 0.5024, 0.5126, 1.0)).rgb;
56
- let warm_emission = warm_color * BODY_WARM_STR;
57
-
58
- let rim1_str = fresnel(2.0, n, v) * layer_weight_facing(0.24000005424022675, n, v);
59
- let rim1 = vec3f(0.984157919883728, 0.6110184788703918, 0.5736401677131653) * rim1_str;
60
-
61
- let facing_raw = layer_weight_facing(BODY_RIM2_LAYER_BLEND, n, v);
62
- let facing_pow = math_power(facing_raw, BODY_RIM2_POW);
63
- let rim2_fac = ramp_ease(facing_pow, 0.0, vec4f(0,0,0,1), 0.5052, vec4f(1,1,1,1)).r;
64
- let rim2_mixed = mix(emission3, BODY_RIM2_BG, rim2_fac);
65
-
66
- let npr_stack = rim1 + rim2_mixed + warm_emission;
67
-
68
- // ═══ PRINCIPLED BSDF with noise bump ═══
69
- // Mapping loc=rot=0 in the Blender graph folds to a plain scale multiply.
70
- // Height in rest space (restPos) so the bump rides with the skin; geometric basis stays
71
- // worldPos so the perturbed normal matches the world-space n (see face.ts note).
72
- let noise_val = tex_noise_d2(input.restPos * vec3f(1.0, 1.0, 1.5), 1.0);
73
- let noise_ramp = ramp_linear(noise_val, 0.0, vec4f(0,0,0,1), 1.0, vec4f(1,1,1,1)).r;
74
- let bumped_n = bump_lh(0.324644535779953, noise_ramp, n, input.worldPos);
75
-
76
- let principled_base = mix_blend(noise_ramp, bc, vec3f(0.6831911206245422, 0.19474034011363983, 0.13732507824897766));
77
- let p_emission = bc * 0.2;
78
-
79
- let principled = eval_principled(
80
- PrincipledIn(principled_base, 0.0, BODY_SPECULAR, BODY_ROUGHNESS, BODY_SPEC_CLAMP, 0.0, 0.0),
81
- bumped_n, l, v, sun, amb, shadow
82
- ) + p_emission;
83
-
84
- let final_color = mix(npr_stack, principled, BODY_MIX_NPR);
85
-
86
- var out: FSOut;
87
- out.color = vec4f(final_color, alpha);
88
- out.mask = vec4f(1.0, 1.0, 0.0, out.color.a);
89
- return out;
90
- }
91
-
92
- `
1
+ // M_Body — 仿深空之眼渲染预设v1.0_by_小绿毛猫 "M_Body". Toon + warm rim + rim1/rim2
2
+ // stack mixed 50/50 against a Principled BSDF with noise-bumped normal.
3
+
4
+ import { NODES_WGSL } from "./nodes"
5
+ import { COMMON_MATERIAL_PRELUDE_WGSL } from "./common"
6
+
7
+ export const BODY_SHADER_WGSL = /* wgsl */ `
8
+
9
+ ${NODES_WGSL}
10
+ ${COMMON_MATERIAL_PRELUDE_WGSL}
11
+
12
+ const BODY_ROUGHNESS: f32 = 0.3;
13
+ const BODY_SPECULAR: f32 = 0.5;
14
+ const BODY_MIX_NPR: f32 = 0.5;
15
+ const BODY_SPEC_CLAMP: f32 = 10.0;
16
+ const BODY_RIM2_LAYER_BLEND: f32 = 0.20000000298023224;
17
+ const BODY_RIM2_POW: f32 = 1.4300000667572021;
18
+ const BODY_RIM2_BG: vec3f = vec3f(1.0, 0.4303792119026184, 0.3315804898738861);
19
+ const BODY_WARM_STR: f32 = 0.30000001192092896;
20
+
21
+ // smoothstep-based ramp: t*t*(3-2*t) between two color stops
22
+ fn ramp_ease(f: f32, p0: f32, c0: vec4f, p1: f32, c1: vec4f) -> vec4f {
23
+ let t = saturate((f - p0) / max(p1 - p0, 1e-6));
24
+ let ss = t * t * (3.0 - 2.0 * t);
25
+ return mix(c0, c1, ss);
26
+ }
27
+
28
+ @fragment fn fs(input: VertexOutput) -> FSOut {
29
+ let tex_s = textureSample(diffuseTexture, diffuseSampler, input.uv);
30
+ // MMD alpha semantics: material alpha × texture alpha. Hair/lace/accessory
31
+ // textures cut their shapes in the alpha channel — ignoring it renders each
32
+ // card's full quad with the texture's padding color (white shimmer on dark
33
+ // hair for models whose textures pad with white).
34
+ let alpha = material.alpha * tex_s.a;
35
+ if (alpha < 0.001) { discard; }
36
+
37
+ let n = safe_normal(input.normal);
38
+ let v = normalize(camera.viewPos - input.worldPos);
39
+ let l = -light.lights[0].direction.xyz;
40
+ let sun = light.lights[0].color.xyz * light.lights[0].color.w;
41
+ let amb = light.ambientColor.xyz;
42
+ let shadow = sampleShadow(input.worldPos, n);
43
+
44
+ let tex_color = tex_s.rgb;
45
+
46
+ // ═══ NPR STACK ═══
47
+ let ndotl_raw = shader_to_rgb_diffuse(n, l, sun, amb, shadow);
48
+ let toon = ramp_constant(ndotl_raw, 0.0, vec4f(0,0,0,1), 0.2966, vec4f(1,1,1,1)).r;
49
+
50
+ let shadow_tint = hue_sat_id(2.0, 0.3499999940395355, 1.0, tex_color);
51
+ let lit_tint = hue_sat_id(1.5, 1.0, 1.0, tex_color);
52
+ let toon_color = mix_blend(toon, shadow_tint, lit_tint);
53
+ let bc = bright_contrast(toon_color, 0.1, 0.2);
54
+
55
+ let emission3 = bc * 4.0;
56
+
57
+ let warm_input = clamp(toon + 0.5, 0.0, 1.0);
58
+ let warm_color = ramp_cardinal(warm_input, 0.2409,
59
+ vec4f(0.2426, 0.068, 0.0588, 1.0), 0.4663,
60
+ vec4f(0.6677, 0.5024, 0.5126, 1.0)).rgb;
61
+ let warm_emission = warm_color * BODY_WARM_STR;
62
+
63
+ let rim1_str = fresnel(2.0, n, v) * layer_weight_facing(0.24000005424022675, n, v);
64
+ let rim1 = vec3f(0.984157919883728, 0.6110184788703918, 0.5736401677131653) * rim1_str;
65
+
66
+ let facing_raw = layer_weight_facing(BODY_RIM2_LAYER_BLEND, n, v);
67
+ let facing_pow = math_power(facing_raw, BODY_RIM2_POW);
68
+ let rim2_fac = ramp_ease(facing_pow, 0.0, vec4f(0,0,0,1), 0.5052, vec4f(1,1,1,1)).r;
69
+ let rim2_mixed = mix(emission3, BODY_RIM2_BG, rim2_fac);
70
+
71
+ let npr_stack = rim1 + rim2_mixed + warm_emission;
72
+
73
+ // ═══ PRINCIPLED BSDF with noise bump ═══
74
+ // Mapping loc=rot=0 in the Blender graph folds to a plain scale multiply.
75
+ // Height in rest space (restPos) so the bump rides with the skin; geometric basis stays
76
+ // worldPos so the perturbed normal matches the world-space n (see face.ts note).
77
+ let noise_val = tex_noise_d2(input.restPos * vec3f(1.0, 1.0, 1.5), 1.0);
78
+ let noise_ramp = ramp_linear(noise_val, 0.0, vec4f(0,0,0,1), 1.0, vec4f(1,1,1,1)).r;
79
+ let bumped_n = bump_lh(0.324644535779953, noise_ramp, n, input.worldPos);
80
+
81
+ let principled_base = mix_blend(noise_ramp, bc, vec3f(0.6831911206245422, 0.19474034011363983, 0.13732507824897766));
82
+ let p_emission = bc * 0.2;
83
+
84
+ let principled = eval_principled(
85
+ PrincipledIn(principled_base, 0.0, BODY_SPECULAR, BODY_ROUGHNESS, BODY_SPEC_CLAMP, 0.0, 0.0),
86
+ bumped_n, l, v, sun, amb, shadow
87
+ ) + p_emission;
88
+
89
+ let final_color = mix(npr_stack, principled, BODY_MIX_NPR);
90
+
91
+ var out: FSOut;
92
+ out.color = vec4f(final_color, alpha);
93
+ out.mask = vec4f(1.0, 1.0, 0.0, out.color.a);
94
+ return out;
95
+ }
96
+
97
+ `
@@ -20,7 +20,7 @@ const CLOTH_R_BUMP_STR: f32 = 1.0;
20
20
  const CLOTH_R_SPEC_CLAMP: f32 = 10.0;
21
21
 
22
22
  @fragment fn fs(input: VertexOutput) -> FSOut {
23
- let n = normalize(input.normal);
23
+ let n = safe_normal(input.normal);
24
24
  let v = normalize(camera.viewPos - input.worldPos);
25
25
  let l = -light.lights[0].direction.xyz;
26
26
  let sun = light.lights[0].color.xyz * light.lights[0].color.w;
@@ -17,7 +17,7 @@ const NPR_EMIT_STR: f32 = 18.200000762939453;
17
17
  const NPR_MIX_SHADER_FAC: f32 = 0.8999999761581421;
18
18
 
19
19
  @fragment fn fs(input: VertexOutput) -> FSOut {
20
- let n = normalize(input.normal);
20
+ let n = safe_normal(input.normal);
21
21
  let v = normalize(camera.viewPos - input.worldPos);
22
22
  let l = -light.lights[0].direction.xyz;
23
23
  let sun = light.lights[0].color.xyz * light.lights[0].color.w;
@@ -47,7 +47,7 @@ const NPR_MIX_SHADER_FAC: f32 = 0.8999999761581421;
47
47
  // ═══ PRINCIPLED BSDF ═══
48
48
  let principled_base = hue_sat_id(1.0, 0.800000011920929, 1.0, tex_rgb);
49
49
  let principled = eval_principled(
50
- PrincipledIn(principled_base, 0.0, CLOTH_SPECULAR, CLOTH_ROUGHNESS, 1e30, 0.0, 0.0),
50
+ PrincipledIn(principled_base, 0.0, CLOTH_SPECULAR, CLOTH_ROUGHNESS, 10.0, 0.0, 0.0),
51
51
  n, l, v, sun, amb, shadow
52
52
  );
53
53