reze-engine 0.66.4 → 0.67.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.
@@ -28,7 +28,7 @@ export const CLOTH_SMOOTH_GRAPH: ShaderGraph = {
28
28
  { id: "mix_003", type: "mix/blend" },
29
29
  { id: "hue_004", type: "hue_sat", inputs: { hue: 0.5, saturation: 0.800000011920929, value: 2.0, fac: 1.0 } },
30
30
  { id: "npr_overlay", type: "mix/overlay", inputs: { fac: 1.0 } },
31
- { id: "npr_emit", type: "vector_math/scale", inputs: { scale: 18.200000762939453 } },
31
+ { id: "npr_emit", type: "vector_math/scale", inputs: { scale: 14.56 } },
32
32
  { id: "principled_base", type: "hue_sat", inputs: { hue: 0.5, saturation: 1.0, value: 0.800000011920929, fac: 1.0 } },
33
33
  {
34
34
  id: "principled",
@@ -29,7 +29,7 @@ export const FACE_GRAPH: ShaderGraph = {
29
29
  { id: "lit_tint", type: "hue_sat", inputs: { hue: 0.46000000834465027, saturation: 1.600000023841858, value: 1.5, fac: 1.0 } },
30
30
  { id: "toon_color", type: "mix/blend" },
31
31
  { id: "bc", type: "bright_contrast", inputs: { bright: 0.1, contrast: 0.2 } },
32
- { id: "emission3", type: "vector_math/scale", inputs: { scale: 2.5 } },
32
+ { id: "emission3", type: "vector_math/scale", inputs: { scale: 2.0 } },
33
33
 
34
34
  // ── warm rim (toon*0.5+0.5 → cardinal ramp) ──
35
35
  { id: "warm_mul", type: "math/multiply", inputs: { b: 0.5 } },
@@ -27,7 +27,7 @@ export const METAL_GRAPH: ShaderGraph = {
27
27
  { id: "mix_004", type: "mix/blend" },
28
28
  { id: "hue_004", type: "hue_sat", inputs: { hue: 0.5, saturation: 1.0, value: 2.0, fac: 1.0 } },
29
29
  { id: "npr_overlay", type: "mix/overlay", inputs: { fac: 1.0 } },
30
- { id: "npr_emit", type: "vector_math/scale", inputs: { scale: 8.100000381469727 } },
30
+ { id: "npr_emit", type: "vector_math/scale", inputs: { scale: 6.48 } },
31
31
  { id: "voro_cross", type: "vect_cross", inputs: { b: [0, 1, 0] } },
32
32
  { id: "voro", type: "tex_voronoi/color", inputs: { scale: 4.3 } },
33
33
  { id: "voro_ramp", type: "ramp_linear", inputs: { pos0: 0.0, pos1: 1.0 } },
@@ -123,7 +123,11 @@ const uberNode = (slot: number): NodeSpec => ({
123
123
  roughness: F(0.6),
124
124
  occlusion: F(1),
125
125
  normal: { type: "vector", contextDefault: "n" },
126
- direction: { type: "vector", requiresLink: true },
126
+ // The key light: this draw's, by default — the sun, or the light on the
127
+ // character's own layer — its way and its colour (the game's _LocalLightDir,
128
+ // _LocalLightColor). Linked, a look supplies its own (ag_key_light).
129
+ direction: { type: "vector", contextDefault: "l" },
130
+ light_color: { type: "color", contextDefault: "sun" },
127
131
  // Unity's v into the ramp: the property map's alpha, which picks a
128
132
  // surface's band (skin 0.702, hair and pale cloth 0.506).
129
133
  row: F(0.702),
@@ -137,24 +141,45 @@ const uberNode = (slot: number): NodeSpec => ({
137
141
  rim_in_light: F(1),
138
142
  emission: C([0, 0, 0]),
139
143
  reflection: F(1),
144
+ // THE STUDIO AMBIENT (_GlobalIlluminationOverride): the game blends a
145
+ // character's ambient from its own reflection cube's smallest mip — near
146
+ // neutral, 0.4–0.9 across the newer skins — over the scene's light, by the
147
+ // material's blend (bodies 0.8, hair 1, faces 0.5). It is what keeps a
148
+ // character clean on a coloured stage.
149
+ studio: C([0.6, 0.6, 0.6]),
150
+ studio_blend: F(0),
140
151
  // A face's SDF shade (ag_face_sdf); left at -1, the material reads N·L.
141
152
  shade: F(-1),
142
153
  // 1: the game's FACE_MODE — no GGX, the environment at a flat 0.0157.
143
154
  face: F(0),
155
+ // A face's SDF highlight (ag_face_spec), in place of GGX.
156
+ face_spec: F(0),
157
+ // 1: hair (_ANISOTROPIC_SPECULAR) — its ring (ag_hair_ring) in place of GGX,
158
+ // the environment at a flat 0.0157.
159
+ hair: F(0),
160
+ hair_spec: C([0, 0, 0]),
144
161
  },
145
162
  outputs: { color: "color" },
146
163
  emit: (a) =>
147
- `ag_uber(${slot}u, ${a.base}, ${a.metallic}, ${a.roughness}, ${a.occlusion}, ${a.normal}, ${a.direction}, ${a.row}, ${a.receive_shadow}, ` +
148
- `${a.rim_mask}, ${a.rim_mid}, ${a.rim_width}, ${a.rim_tint}, ${a.rim_intensity}, ${a.rim_albedo}, ${a.rim_in_light}, ${a.emission}, ${a.reflection}, ${a.shade}, ${a.face}, input.worldPos, v)`,
164
+ `ag_uber(${slot}u, ${a.base}, ${a.metallic}, ${a.roughness}, ${a.occlusion}, ${a.normal}, ${a.direction}, ${a.light_color}, ${a.row}, ${a.receive_shadow}, ` +
165
+ `${a.rim_mask}, ${a.rim_mid}, ${a.rim_width}, ${a.rim_tint}, ${a.rim_intensity}, ${a.rim_albedo}, ${a.rim_in_light}, ${a.emission}, ${a.reflection}, ${a.studio}, ${a.studio_blend}, ${a.shade}, ${a.face}, ${a.face_spec}, ${a.hair}, ${a.hair_spec}, input.worldPos, v)`,
149
166
  takesLight: true,
150
167
  })
151
168
  const UBER_NODES: Record<string, NodeSpec> = Object.fromEntries([0, 1, 2, 3].map((s) => [`ag_uber/${s}`, uberNode(s)]))
152
- // The face's SDF shade, the image on the slot the type names — 104903's face
153
- // template (SDF_04_shaonv) by default in the AG pack.
169
+ // How much a colour reads as skin (ag_skin): what picks a surface's ramp row in
170
+ // a general look, where the game would read its property mask.
171
+ const AG_SKIN: NodeSpec = {
172
+ inputs: { color: C([1, 1, 1], true) },
173
+ outputs: { value: "float" },
174
+ emit: (a) => `ag_skin(${a.color})`,
175
+ }
176
+ // The face's SDF shade, the image on the slot the type names. An empty slot
177
+ // reads white: the whole face takes one shade from where the key light stands
178
+ // to the head, the game's flat face for a model without its SDF.
154
179
  const faceSdfNode = (slot: number): NodeSpec => ({
155
180
  inputs: {
156
181
  uv: { type: "vector", contextDefault: "vec3f(input.uv, 0.0)" },
157
- direction: { type: "vector", requiresLink: true },
182
+ direction: { type: "vector", contextDefault: "l" },
158
183
  normal: { type: "vector", contextDefault: "n" },
159
184
  smoothness: F(0.1),
160
185
  invert: F(0),
@@ -163,6 +188,55 @@ const faceSdfNode = (slot: number): NodeSpec => ({
163
188
  emit: (a) => `ag_face_sdf(${slot}u, ${a.uv}.xy, ${a.direction}, ${a.normal}, ${a.smoothness}, ${a.invert})`,
164
189
  })
165
190
  const FACE_SDF_NODES: Record<string, NodeSpec> = Object.fromEntries([0, 1, 2, 3].map((s) => [`ag_face_sdf/${s}`, faceSdfNode(s)]))
191
+ // The newer SDF (_SDFType 1): a painted face normal, not a threshold field.
192
+ const faceSdfNewNode = (slot: number): NodeSpec => ({
193
+ ...faceSdfNode(slot),
194
+ emit: (a) => `ag_face_sdf_new(${slot}u, ${a.uv}.xy, ${a.direction}, ${a.normal}, ${a.smoothness}, ${a.invert})`,
195
+ })
196
+ const FACE_SDF_NEW_NODES: Record<string, NodeSpec> = Object.fromEntries([0, 1, 2, 3].map((s) => [`ag_face_sdf_new/${s}`, faceSdfNewNode(s)]))
197
+ // The face's highlight from the newer SDF's green and blue.
198
+ const faceSpecNode = (slot: number): NodeSpec => ({
199
+ inputs: {
200
+ uv: { type: "vector", contextDefault: "vec3f(input.uv, 0.0)" },
201
+ direction: { type: "vector", contextDefault: "l" },
202
+ normal: { type: "vector", contextDefault: "n" },
203
+ anisotropy: F(1),
204
+ shift: F(0),
205
+ },
206
+ outputs: { value: "float" },
207
+ emit: (a) => `ag_face_spec(${slot}u, ${a.uv}.xy, ${a.direction}, ${a.normal}, v, ${a.anisotropy}, ${a.shift})`,
208
+ })
209
+ // The hair's angel ring, the band on the slot's image.
210
+ const hairRingNode = (slot: number): NodeSpec => ({
211
+ inputs: {
212
+ uv: { type: "vector", contextDefault: "vec3f(input.uv, 0.0)" },
213
+ normal: { type: "vector", contextDefault: "n" },
214
+ color: C([1, 1, 1]),
215
+ anisotropy: F(1),
216
+ shift: F(0),
217
+ },
218
+ outputs: { color: "color" },
219
+ emit: (a) => `ag_hair_ring(${slot}u, ${a.uv}.xy, ${a.normal}, v, ${a.color}, ${a.anisotropy}, ${a.shift})`,
220
+ })
221
+ const HAIR_RING_NODES: Record<string, NodeSpec> = Object.fromEntries([0, 1, 2, 3].map((s) => [`ag_hair_ring/${s}`, hairRingNode(s)]))
222
+ // The game's pupil (SimPipeline/Character/Eye): unlit relief, matcap and
223
+ // glints — slots: 0 depth, 1 normal, 2 matcap, 3 glint mask.
224
+ const AG_EYE: NodeSpec = {
225
+ inputs: {
226
+ height_scale: F(0.2), min_layer: F(8), max_layer: F(32), intensity: F(0.9),
227
+ main_color: C([1, 1, 1]), matcap_color: C([1, 1, 1]), matcap_pow: F(1),
228
+ mask_scale: F(0.15), mask_soft: F(0.014),
229
+ mask2: F(0), mask2_color: C([1, 1, 1]), mask3: F(0), mask3_color: C([1, 1, 1]),
230
+ blink: F(0), blink_scale: F(10), scale_speed: F(0.1), rotate_speed: F(0.6), blink_angle: F(5),
231
+ level: F(1),
232
+ },
233
+ outputs: { color: "color" },
234
+ emit: (a) =>
235
+ `ag_eye(input.uv, n, v, input.worldPos, ${a.height_scale}, ${a.min_layer}, ${a.max_layer}, ${a.intensity}, ${a.main_color}, ` +
236
+ `${a.matcap_color}, ${a.matcap_pow}, ${a.mask_scale}, ${a.mask_soft}, ${a.mask2}, ${a.mask2_color}, ${a.mask3}, ${a.mask3_color}, ` +
237
+ `${a.blink}, ${a.blink_scale}, ${a.scale_speed}, ${a.rotate_speed}, ${a.blink_angle}, ${a.level})`,
238
+ }
239
+ const FACE_SPEC_NODES: Record<string, NodeSpec> = Object.fromEntries([0, 1, 2, 3].map((s) => [`ag_face_spec/${s}`, faceSpecNode(s)]))
166
240
 
167
241
  // A packed property map on the slot the type names — the game's material
168
242
  // contract: R metal, G perceptual roughness (answered as smoothness, Unity's
@@ -630,7 +704,12 @@ export const NODE_REGISTRY: Record<string, NodeSpec> = {
630
704
  // Principled is one: the ramp image on the group slot named by the type.
631
705
  // Defaults are 104903's skin.
632
706
  ...UBER_NODES,
707
+ ag_skin: AG_SKIN,
633
708
  ...FACE_SDF_NODES,
709
+ ...FACE_SDF_NEW_NODES,
710
+ ...FACE_SPEC_NODES,
711
+ ag_eye: AG_EYE,
712
+ ...HAIR_RING_NODES,
634
713
  ...PROPERTY_MAP_NODES,
635
714
 
636
715
  /**
@@ -75,6 +75,20 @@ export type ShaderGraph = {
75
75
  * images keeps those.
76
76
  */
77
77
  images?: ({ url: string; srgb?: boolean } | null)[]
78
+ /**
79
+ * The look's outline, the anime game's in place of MMD's flat edge: the
80
+ * material's picture × colour, toward shadowColor where the key light leaves
81
+ * it, `width` world units wide (held 1.2–3 px of a 1920-wide frame) — an Aether
82
+ * Gazer _OutlineWidth w is w·1.3/1920·8 here. Unset, the PMX edge stands.
83
+ */
84
+ outline?: {
85
+ color: [number, number, number]
86
+ shadowColor: [number, number, number]
87
+ width: number
88
+ /** One of `images` (its index) whose colour multiplies the outline at the
89
+ * mesh's uvs — the game's per-texel outline mask. */
90
+ tint?: number
91
+ }
78
92
  }
79
93
 
80
94
  export type Diagnostic = {
@@ -471,9 +471,10 @@ fn _ag_ramp(slot: u32, uv: vec2f) -> vec4f {
471
471
  * Lamps and fog are the engine's, laid on after.
472
472
  */
473
473
  fn ag_uber(slot: u32, base: vec3f, metallic: f32, roughness: f32, occlusion: f32, n: vec3f, l: vec3f,
474
- row: f32, receive: f32, rimMask: f32, rimMid: f32, rimWidth: f32, rimTint: vec3f,
474
+ lightColor: vec3f, row: f32, receive: f32, rimMask: f32, rimMid: f32, rimWidth: f32, rimTint: vec3f,
475
475
  rimIntensity: f32, rimAlbedo: f32, rimInLight: f32, emissive: vec3f, reflection: f32,
476
- shade: f32, face: f32, wp: vec3f, v: vec3f) -> vec3f {
476
+ studio: vec3f, studioBlend: f32, shade: f32, face: f32, faceSpec: f32, hair: f32, hairSpec: vec3f,
477
+ wp: vec3f, v: vec3f) -> vec3f {
477
478
  let diffuse = base * (1.0 - metallic);
478
479
  let f0 = mix(vec3f(0.04), base, metallic);
479
480
  let pr = clamp(roughness, 0.0, 1.0);
@@ -510,9 +511,15 @@ fn ag_uber(slot: u32, base: vec3f, metallic: f32, roughness: f32, occlusion: f32
510
511
  let rim = tint * edge * (1.0 + rimInLight * (u - 1.0));
511
512
 
512
513
  // A face (FACE_MODE) takes no GGX: its light is the ramp, the rim and its
513
- // optional SDF highlight (off unless _FaceSpecular).
514
+ // SDF highlight (ag_face_spec), grey, × (0.9u + 0.1).
514
515
  let isFace = face > 0.5;
515
- let direct = (select(spec * f0, vec3f(0.0), isFace) + rim) * u + diffuse * ramp;
516
+ // Hair (_ANISOTROPIC_SPECULAR) takes its angel ring (ag_hair_ring) in place of
517
+ // GGX, not × F0: × (0.29u + 0.1/π).
518
+ let isHair = hair > 0.5;
519
+ var hl = select(spec * f0, hairSpec * (0.29 * u + 0.03183099), isHair);
520
+ hl = select(hl, vec3f(faceSpec * (0.9 * u + 0.1)), isFace);
521
+ // under the key light's colour, as the pass ends its direct term (× _LocalLightColor)
522
+ let direct = ((hl + rim) * u + diffuse * ramp) * lightColor;
516
523
 
517
524
  // the environment, with the game's fit of the split-sum
518
525
  let a004 = min((1.0 - pr) * (1.0 - pr), exp2(-9.28 * ndv)) * (1.0 - pr) + (0.0425 - 0.0275 * pr);
@@ -522,11 +529,33 @@ fn ag_uber(slot: u32, base: vec3f, metallic: f32, roughness: f32, occlusion: f32
522
529
  let env = rzWorldSpecularLod(reflect(-v, n), pr, 1.0) * reflection;
523
530
 
524
531
  // ... and the environment at a flat 0.0157 rather than through the BRDF fit.
525
- let envTerm = select((f0 * A + B) * env, env * 0.0157, isFace);
526
- return emissive + rzObjectAmbient(n) * diffuse + envTerm + direct;
532
+ let envTerm = select((f0 * A + B) * env, env * 0.0157, isFace || isHair);
533
+ // the ambient: the scene's, blended toward the studio's by the material
534
+ let ambient = mix(rzObjectAmbient(n), studio, saturate(studioBlend));
535
+ return emissive + ambient * diffuse + envTerm + direct;
527
536
  }
528
537
 
529
538
 
539
+ /**
540
+ * How much an albedo reads as skin, 0..1 — for a general look that has no
541
+ * game property mask to pick a surface's ramp row with. Skin across the game's
542
+ * newer characters (and the PMX textures ripped from them) is warm — red over
543
+ * green over blue, a hue within about 0..45 degrees — moderately saturated and
544
+ * fairly light; cloth, hair, metal and eyes are rarely all three.
545
+ */
546
+ fn ag_skin(c: vec3f) -> f32 {
547
+ let s = clamp(c, vec3f(0.0), vec3f(1.0));
548
+ let mx = max(s.r, max(s.g, s.b));
549
+ let mn = min(s.r, min(s.g, s.b));
550
+ let sat = (mx - mn) / max(mx, 1e-4);
551
+ // hue in degrees for the red-to-yellow sextant, where skin lives
552
+ let hue = select(-1.0, 60.0 * (s.g - s.b) / max(mx - mn, 1e-4), s.r >= s.g && s.g >= s.b && mx - mn > 1e-4);
553
+ let warm = smoothstep(-2.0, 4.0, hue) * (1.0 - smoothstep(38.0, 52.0, hue));
554
+ let tinted = smoothstep(0.06, 0.14, sat) * (1.0 - smoothstep(0.55, 0.75, sat));
555
+ let light = smoothstep(0.35, 0.55, mx);
556
+ return warm * tinted * light;
557
+ }
558
+
530
559
  /**
531
560
  * The face's shade from its SDF image (Uber FACE_MODE, _SDFType 0), in place of
532
561
  * N·L: the key light taken into the face's frame and flattened, the field read
@@ -560,11 +589,151 @@ fn ag_face_sdf(slot: u32, uv: vec2f, l: vec3f, n: vec3f, smoothness: f32, invert
560
589
  var m2 = max(saturate((b - th - 1.0) / k + 0.5), saturate((a - w) / k + 0.5));
561
590
  m2 = min(m2, min(saturate((a + w) / k + 0.5), saturate((b + w) / k + 0.5)));
562
591
  var r = flag * (m1 - m2) + m2;
563
- r *= (1.0 - abs(lf.z)) * 0.5 + 0.5;
592
+ r *= (1.0 - abs(lf.y)) * 0.5 + 0.5;
564
593
  r *= saturate(dot(n, l)) * 0.5 + 0.5;
565
594
  return r;
566
595
  }
567
596
 
597
+ /** The game's face frame (_World2Face) from the head bone: x to the face's
598
+ * right, y up, z out of it — a vector in world space taken into it. */
599
+ fn _ag_face_frame(d: vec3f) -> vec3f {
600
+ let hb = skinMats[u32(max(material.headBoneIndex, 0.0))];
601
+ return vec3f(dot(d, -normalize(hb[0].xyz)), dot(d, normalize(hb[1].xyz)), dot(d, -normalize(hb[2].xyz)));
602
+ }
603
+
604
+ /**
605
+ * The face's shade from its newer SDF image (Uber FACE_MODE, _SDFType 1), in
606
+ * place of N·L. The image paints, per texel, a normal in the face's level
607
+ * plane (alpha: its angle off the face's forward, toward the side the light
608
+ * is on) and how far it replaces the surface's own (red); three taps soften
609
+ * it. The painted normal yields to the real one as the light climbs, and the
610
+ * shade is a linear step of it against the light, _BlendSmoothness wide.
611
+ */
612
+ fn ag_face_sdf_new(slot: u32, uv: vec2f, l: vec3f, n: vec3f, smoothness: f32, invert: f32) -> f32 {
613
+ let lf = normalize(_ag_face_frame(l));
614
+ let nf = normalize(_ag_face_frame(n));
615
+ let flip = lf.x > 0.0;
616
+ // Unity's v runs up: uv is the mesh's (PMX, v down)
617
+ let ub = vec2f(mix(uv.x, 1.0 - uv.x, invert), 1.0 - uv.y);
618
+ let x0 = select(ub.x, 1.0 - ub.x, flip);
619
+ let x1 = select(ub.x - 0.004, 1.0 - (ub.x - 0.004), flip);
620
+ let x2 = select(ub.x + 0.004, 1.0 - (ub.x + 0.004), flip);
621
+ let t0 = _ag_ramp(slot, vec2f(x0, ub.y));
622
+ let t1 = _ag_ramp(slot, vec2f(x1, ub.y - 0.004));
623
+ let t2 = _ag_ramp(slot, vec2f(x2, ub.y - 0.004));
624
+ let w = (t0.r + t1.r + t2.r) / 3.0;
625
+ let th = ((t0.a + t1.a + t2.a) / 3.0 * 2.0 - 1.0) * 1.5707964;
626
+ let side = select(vec3f(-1.0, 0.0, 0.0), vec3f(1.0, 0.0, 0.0), flip);
627
+ var ns = side * sin(th) + vec3f(0.0, 0.0, 1.0) * cos(th);
628
+ ns = normalize(mix(ns, nf, lf.y * lf.y));
629
+ ns = normalize(mix(nf, ns, w));
630
+ let sm = max(smoothness, 0.001);
631
+ let lo = -sm * w;
632
+ let hi = w * (sm - 1.0) + 1.0;
633
+ return (clamp(dot(ns, lf), lo, hi) - lo) / (hi - lo);
634
+ }
635
+
636
+ /**
637
+ * The face's highlight (Uber FACE_MODE with the newer SDF), its GGX: the
638
+ * SDF image's green and blue paint a nose and cheek glint per light angle, lit
639
+ * where both pass a threshold set by how far the light, swung 85% toward the
640
+ * camera, comes from the front; the painted band slides up with the view
641
+ * (_AnisotropyShift). × _Anisotropy/π, the light's frontality and N·V. The
642
+ * pass lays it on as u·(0.9u + 0.1)·this — ag_uber's face_spec.
643
+ */
644
+ fn ag_face_spec(slot: u32, uv: vec2f, l: vec3f, n: vec3f, v: vec3f, anisotropy: f32, shift: f32) -> f32 {
645
+ let lf = _ag_face_frame(l);
646
+ let lxz = lf.xz * inverseSqrt(max(dot(lf.xz, lf.xz), 1.1754944e-38));
647
+ // unity_CameraToWorld column 2: OpenGL-style, the camera's BACK, toward the viewer
648
+ let cf = _ag_face_frame(vector_camera_to_world(vec3f(0.0, 0.0, 1.0))).xz;
649
+ let hd = normalize(mix(lxz, cf, 0.85));
650
+ let ub = vec2f(uv.x, 1.0 - uv.y);
651
+ let s = _ag_ramp(slot, vec2f(select(ub.x, 1.0 - ub.x, hd.x < 0.0), ub.y - v.y * shift));
652
+ let t = clamp(saturate(-hd.y - 0.70710677) * 3.4142134, 0.01, 0.99);
653
+ let on = select(0.0, 1.0, s.g >= 1.0 - t && s.b >= t);
654
+ return on * anisotropy / 3.14159265 * saturate((lxz.y * 0.5 + 0.5) * 4.0 - 3.0) * saturate(dot(n, v));
655
+ }
656
+
657
+ /**
658
+ * Aether Gazer's pupil (SimPipeline/Character/Eye, ForwardBase): unlit. The
659
+ * iris is a parallax-occlusion relief on the depth image (slot 0, red = height
660
+ * up from 1 − depth), marched in the eye's own tangent frame toward the eye of
661
+ * the viewer, _MinLayer..._MaxLayer layers as the view grazes; the material's
662
+ * picture is read where the march stops, brightened by its depth × _Intensity.
663
+ * A matcap (slot 2) on the normal map (slot 1, tangent normal as 0..1) scales
664
+ * it by (1 + matcap), and the sparkle mask (slot 3, blue / green) adds its
665
+ * glints, pulsing and wobbling with the clock. × level (_ProbeLightingBase).
666
+ */
667
+ fn ag_eye(uv: vec2f, n: vec3f, v: vec3f, wp: vec3f, heightScale: f32, minLayer: f32, maxLayer: f32,
668
+ intensity: f32, mainColor: vec3f, matColor: vec3f, matPow: f32, maskScale: f32, maskSoft: f32,
669
+ mask2: f32, mask2Color: vec3f, mask3: f32, mask3Color: vec3f, blink: f32, blinkScale: f32,
670
+ scaleSpeed: f32, rotateSpeed: f32, blinkAngle: f32, level: f32) -> vec3f {
671
+ // the tangent frame of the mesh uvs, as the march walks them
672
+ let dp1 = dpdx(wp);
673
+ let dp2 = dpdy(wp);
674
+ let du1 = dpdx(uv);
675
+ let du2 = dpdy(uv);
676
+ let det = du1.x * du2.y - du2.x * du1.y;
677
+ let inv = select(0.0, 1.0 / det, abs(det) > 1e-12);
678
+ let tu = (dp1 * du2.y - dp2 * du1.y) * inv;
679
+ let tv = (dp2 * du1.x - dp1 * du2.x) * inv;
680
+ let vt = vec3f(dot(v, safe_normal(tu)), dot(v, safe_normal(tv)), dot(v, n));
681
+ let vz = max(abs(vt.z), 1e-3);
682
+ let layers = mix(ceil(maxLayer), ceil(minLayer), saturate(abs(vt.z)));
683
+ // against the view, both ways in these uvs: measured, the iris error 48 -> 10 of 255
684
+ let stp = vec3f(-vt.xy * heightScale, vz) / (layers * vz);
685
+ var p = vec3f(uv, 0.0) + stp;
686
+ var prev = vec3f(uv, 0.0);
687
+ var depth = 1.0 - textureSampleLevel(groupTexture0, diffuseSampler, p.xy, 0.0).r;
688
+ var prevDepth = 0.0;
689
+ for (var i = 0; i < 16; i++) {
690
+ if (p.z >= depth) { break; }
691
+ prev = p;
692
+ prevDepth = depth;
693
+ p += stp;
694
+ depth = 1.0 - textureSampleLevel(groupTexture0, diffuseSampler, p.xy, 0.0).r;
695
+ }
696
+ // between the last two layers, where the ray crossed the relief
697
+ let after = depth - p.z;
698
+ let before = prevDepth - prev.z;
699
+ let w = select(0.0, after / (after - before), abs(after - before) > 1e-6);
700
+ let uvP = mix(p.xy, prev.xy, w);
701
+ let dLast = depth;
702
+ var alb = textureSampleLevel(diffuseTexture, diffuseSampler, uvP, 0.0).rgb * mainColor;
703
+ alb += alb * dLast * intensity;
704
+ // the matcap, multiplied in
705
+ let nm = node_normal_map(textureSampleLevel(groupTexture1, diffuseSampler, uv, 0.0).rgb, 1.0, n, wp, uv);
706
+ let vn = (camera.view * vec4f(nm, 0.0)).xy;
707
+ let mc = textureSampleLevel(groupTexture2, diffuseSampler, vn * 0.5 + 0.5, 0.0).rgb * matColor * matPow;
708
+ var col = alb + alb * mc;
709
+ // the glints: the mask wobbled about the eye's centre, pulsing
710
+ let t = camera.time;
711
+ let a = sin(radians(cos(t * rotateSpeed) * blinkAngle));
712
+ let c = cos(a);
713
+ let d = uv - vec2f(0.5);
714
+ let uvR = select(uv, vec2f(c * d.x - a * d.y, a * d.x + c * d.y) + 0.5, blink > 0.5);
715
+ let m = textureSampleLevel(groupTexture3, diffuseSampler, uvR, 0.0);
716
+ let lo = saturate(maskScale - maskSoft);
717
+ let hi = saturate(maskScale + maskSoft);
718
+ let s1 = select(0.0, smoothstep(lo, hi, m.g), mask2 > 0.5);
719
+ let s2 = select(0.0, smoothstep(lo, hi, m.b), mask3 > 0.5);
720
+ let k = select(1.0, (sin(12.0 * t * scaleSpeed) * 0.5 + 0.7) * blinkScale, blink > 0.5);
721
+ col += saturate(k * s1 * mask2Color + k * s2 * mask3Color);
722
+ return col * level;
723
+ }
724
+
725
+ /**
726
+ * Aether Gazer's hair highlight, its angel ring (Uber _ANISOTROPIC_SPECULAR,
727
+ * _HairLightingMode 0): a band painted in the hair's uvs (the slot's image),
728
+ * sliding up the strands as the view climbs (_AnisotropyShift), × the ring's
729
+ * colour and N·V·_Anisotropy. ag_uber's hair_spec; it lays on the u factor.
730
+ */
731
+ fn ag_hair_ring(slot: u32, uv: vec2f, n: vec3f, v: vec3f, color: vec3f, anisotropy: f32, shift: f32) -> vec3f {
732
+ let ub = vec2f(uv.x, 1.0 - uv.y);
733
+ let b = _ag_ramp(slot, vec2f(ub.x, ub.y - v.y * shift)).rgb;
734
+ return b * color * (saturate(dot(n, v)) * anisotropy);
735
+ }
736
+
568
737
  fn group_tex0(uv: vec2f) -> vec4f { return textureSample(groupTexture0, diffuseSampler, uv); }
569
738
  fn group_tex1(uv: vec2f) -> vec4f { return textureSample(groupTexture1, diffuseSampler, uv); }
570
739
  fn group_tex2(uv: vec2f) -> vec4f { return textureSample(groupTexture2, diffuseSampler, uv); }
@@ -131,7 +131,7 @@ override APPLY_GAMMA: bool = true;
131
131
  @group(0) @binding(0) var hdrTex: texture_2d<f32>;
132
132
  @group(0) @binding(1) var bloomTex: texture_2d<f32>; // bloomUpTexture mip 0 (full pyramid top)
133
133
  @group(0) @binding(2) var bloomSamp: sampler;
134
- @group(0) @binding(3) var<uniform> viewU: array<vec4<f32>, 15>;
134
+ @group(0) @binding(3) var<uniform> viewU: array<vec4<f32>, 16>;
135
135
  // Aux mask/alpha texture. .r = bloom mask (unused here; bloom blit uses it).
136
136
  // .g = accumulated canvas alpha (what hdr.a carried before the HDR format
137
137
  // became rg11b10ufloat). We unpremultiply HDR by this alpha for tonemap, then
@@ -153,6 +153,8 @@ override APPLY_GAMMA: bool = true;
153
153
  // viewU[9] = (grade slope.rgb, grade flags) — bit 0 the CDL grade(), bit 1 the
154
154
  // scene's own LUT (setStageGrade) — see _rzGradeScene() below.
155
155
  // viewU[10] = (camera world position, _) — refreshed with the basis above.
156
+ // viewU[11..14] = the cast's positions (effects; count in viewU[10].w).
157
+ // viewU[15] = (soft curve contrast, _, _, _).
156
158
  // invGamma = 1/gamma precomputed on CPU — avoids a per-pixel divide.
157
159
  @group(0) @binding(6) var bgEquirect: texture_2d<f32>;
158
160
  // The scene pass's own MSAA depth buffer, bound depth-only. NOT an extra
@@ -251,7 +253,7 @@ fn srgb_encode(x: f32) -> f32 {
251
253
  * The game's ACES branch is off in every scene measured.
252
254
  */
253
255
  fn softTransform(c: vec3f) -> vec3f {
254
- let t = pow(max(vec3f(1.0) - exp(-2.5 * max(c, vec3f(0.0))), vec3f(0.0)), vec3f(1.4));
256
+ let t = pow(max(vec3f(1.0) - exp(-2.5 * max(c, vec3f(0.0))), vec3f(0.0)), vec3f(viewU[15].x));
255
257
  return vec3f(srgb_encode(t.r), srgb_encode(t.g), srgb_encode(t.b));
256
258
  }
257
259
 
@@ -27,6 +27,19 @@ import { DISSOLVE_WGSL } from "../materials/common"
27
27
  * edgeSize 16..20, so this is 20.
28
28
  */
29
29
  export const RZ_OUTLINE_DISSOLVE_OFFSET = 20
30
+ /** Where the scene-wide width multiplier sits, in bytes — the float after
31
+ * dissolve (Engine.setOutlineWidth). */
32
+ export const RZ_OUTLINE_WIDTH_OFFSET = 24
33
+ /** Where the scene-wide colour override sits, in bytes: rgb and a weight, 0
34
+ * for the material's own edge colour (Engine.setOutlineColor). */
35
+ export const RZ_OUTLINE_COLOR_OFFSET = 32
36
+ /** Where a style graph's outline (ShaderGraph.outline) sits, in bytes: the
37
+ * textured flag at 28, then its shadow colour at 48. Unset, a hull draws its
38
+ * PMX edge colour flat, as MMD does. */
39
+ export const RZ_OUTLINE_TEXTURED_OFFSET = 28
40
+ export const RZ_OUTLINE_SHADOW_OFFSET = 48
41
+ /** The hull's own block, in floats. */
42
+ export const RZ_OUTLINE_UNIFORM_FLOATS = 16
30
43
 
31
44
  export function outlineShaderWgsl(): string {
32
45
  return /* wgsl */ `
@@ -40,7 +53,7 @@ struct CameraUniforms {
40
53
  viewportHeight: f32,
41
54
  };
42
55
 
43
- // THE HULL'S OWN BLOCK, not the material's. 32 bytes of edge data, which is all
56
+ // THE HULL'S OWN BLOCK, not the material's. 48 bytes of edge data, which is all
44
57
  // this pass shades with — and dissolve, which it has to obey.
45
58
  //
46
59
  // It reached for the material block's own layout first, declaring the skipped
@@ -53,15 +66,34 @@ struct MaterialUniforms {
53
66
  edgeColor: vec4f,
54
67
  edgeSize: f32,
55
68
  dissolve: f32,
56
- _padding2: f32,
57
- _padding3: f32,
69
+ /** The scene's width multiplier (setOutlineWidth), the same on every hull:
70
+ * scales the author's edgeSize rather than replacing it, so a model's thin
71
+ * and thick lines keep their proportion. */
72
+ widthScale: f32,
73
+ /** 1: a style graph's outline, the game's (Uber pass "Outline"): edgeSize is
74
+ * a world width, held between 1.2 and 3 px of a 1920-wide frame, and the
75
+ * colour is the picture × lerp(shadowColor, edgeColor, the key's
76
+ * half-Lambert). 0: the PMX edge, flat. */
77
+ textured: f32,
78
+ /** The scene's colour in place of edgeColor.rgb, by .a (0 or 1). The
79
+ * material's own alpha still holds — a line its author made sheer stays so. */
80
+ colorOverride: vec4f,
81
+ shadowColor: vec4f,
58
82
  };
59
83
 
84
+ struct OutlineLight { direction: vec4f, color: vec4f, };
85
+ /** The head of the scene's light block: the key is lights[0]. */
86
+ struct OutlineLights { ambientColor: vec4f, lights: array<OutlineLight, 4>, };
87
+
60
88
  @group(0) @binding(0) var<uniform> camera: CameraUniforms;
61
89
  @group(0) @binding(1) var edgeSampler: sampler;
90
+ @group(0) @binding(2) var<uniform> light: OutlineLights;
62
91
  @group(1) @binding(0) var<storage, read> skinMats: array<mat4x4f>;
63
92
  @group(2) @binding(0) var<uniform> material: MaterialUniforms;
64
93
  @group(2) @binding(1) var diffuseTexture: texture_2d<f32>;
94
+ /** A look's outline tint (ShaderGraph.outline.tint): one of its images, white
95
+ * when it names none. */
96
+ @group(2) @binding(2) var tintTexture: texture_2d<f32>;
65
97
 
66
98
  struct VertexOutput {
67
99
  @builtin(position) position: vec4f,
@@ -76,6 +108,7 @@ struct VertexOutput {
76
108
  /** How much of a pixel-wide line this one really is (1 = all of it): a line
77
109
  * held at the minimum width fades by what it was short. */
78
110
  @location(3) coverage: f32,
111
+ @location(4) normal: vec3f,
79
112
  };
80
113
 
81
114
  /**
@@ -98,6 +131,11 @@ const RZ_OUTLINE_FULL_FIGURE = 12.5;
98
131
  * the shortfall, so far lines thin out without aliasing into dashes. */
99
132
  const RZ_OUTLINE_MIN_PX = 1.5;
100
133
 
134
+ fn safe_normal2(v: vec2f) -> vec2f {
135
+ let l = length(v);
136
+ return select(vec2f(0.0), v / l, l > 1e-6);
137
+ }
138
+
101
139
  @vertex fn vs(
102
140
  @location(0) position: vec3f,
103
141
  @location(1) normal: vec3f,
@@ -157,21 +195,40 @@ const RZ_OUTLINE_MIN_PX = 1.5;
157
195
  // once the frame is taller than a figure. The PMX per-vertex edge scale
158
196
  // multiplies it all, as vertex colour alpha does in the game.
159
197
  let w = max(clipPos.w, 1e-4);
160
- let breakDepth = RZ_OUTLINE_FULL_FIGURE * camera.projection[1][1];
161
- let nearNdc = viewNormal.xy * (material.edgeSize * outlineNormal.w * 4.0 / refViewport);
162
- var ndc = nearNdc * (min(w, breakDepth) / w);
163
-
164
- // Antialiased minimum (the game's _OutlineAntialias): held at the minimum
165
- // width and faded by the shortfall, never thinner. The game fades by
166
- // sqrt(coverage.x · coverage.y), per axis; for one width that is the ratio
167
- // itself.
168
- let px = length(ndc * deviceViewport * 0.5);
169
- let minPx = RZ_OUTLINE_MIN_PX * max(1.0, camera.viewportHeight / 1080.0);
198
+ var ndc: vec2f;
170
199
  var coverage = 1.0;
171
- if (px < minPx) {
172
- coverage = px / minPx;
173
- ndc = select(vec2f(0.0), ndc * (minPx / px), px > 1e-6);
200
+ if (material.textured > 0.5) {
201
+ // The game's: a view-space offset of edgeSize world units along the view
202
+ // normal, projected, then each axis held between 1.2 px and 1.2 px ×
203
+ // (1 + _OutlineMaxOffsetMultiplier, 1.5) of a 1920-wide frame; a line the
204
+ // minimum had to push fades by how far it was pushed.
205
+ let off = viewNormal.xy * material.edgeSize * material.widthScale * outlineNormal.w;
206
+ let d1 = vec2f(off.x * camera.projection[0][0], off.y * camera.projection[1][1]) / w;
207
+ // the minimum along the push direction (the game's dir · 2/screen · 1.2)
208
+ let pxNdc = 2.0 / refViewport * 1.2 * safe_normal2(viewNormal.xy);
209
+ let mx = abs(pxNdc) * 2.5;
210
+ let o = min(mx, max(abs(d1), abs(pxNdc)));
211
+ // fadeOut = min(length(saturate(off − |d1|))·75, 1), on clip offsets (× w)
212
+ // in game units: w here is MMD units, 8 to the game's metre
213
+ coverage = 1.0 - min(length(saturate((o - abs(d1)) * w / 8.0)) * 75.0, 1.0);
214
+ ndc = o * select(vec2f(1.0), sign(d1), abs(d1) > vec2f(0.0));
215
+ } else {
216
+ let breakDepth = RZ_OUTLINE_FULL_FIGURE * camera.projection[1][1];
217
+ let nearNdc = viewNormal.xy * (material.edgeSize * material.widthScale * outlineNormal.w * 4.0 / refViewport);
218
+ ndc = nearNdc * (min(w, breakDepth) / w);
219
+
220
+ // Antialiased minimum (the game's _OutlineAntialias): held at the minimum
221
+ // width and faded by the shortfall, never thinner. The game fades by
222
+ // sqrt(coverage.x · coverage.y), per axis; for one width that is the ratio
223
+ // itself.
224
+ let px = length(ndc * deviceViewport * 0.5);
225
+ let minPx = RZ_OUTLINE_MIN_PX * max(1.0, camera.viewportHeight / 1080.0);
226
+ if (px < minPx) {
227
+ coverage = px / minPx;
228
+ ndc = select(vec2f(0.0), ndc * (minPx / px), px > 1e-6);
229
+ }
174
230
  }
231
+ output.normal = worldNormal;
175
232
  output.coverage = coverage;
176
233
  output.position = vec4f(clipPos.xy + ndc * w, clipPos.z, clipPos.w);
177
234
  output.uv = uv;
@@ -203,7 +260,8 @@ ${sceneFsOutWgsl({ name: "FSOut", aux: "mask" })}
203
260
  // bind-pose position — three passes, one rule, or they disagree about which
204
261
  // pieces are still there.
205
262
  if (material.dissolve < 0.9995 && input.faceT > material.dissolve) { discard; }
206
- let texA = textureSample(diffuseTexture, edgeSampler, input.uv).a;
263
+ let tex = textureSample(diffuseTexture, edgeSampler, input.uv);
264
+ let texA = tex.a;
207
265
  if (texA < 0.05) {
208
266
  discard;
209
267
  }
@@ -212,7 +270,13 @@ ${sceneFsOutWgsl({ name: "FSOut", aux: "mask" })}
212
270
  // alpha above), it is drawn right after the surface it traces so what lies
213
271
  // under a faded line is already there, and 4× MSAA would quantize a fade to
214
272
  // four steps.
215
- out.color = vec4f(material.edgeColor.rgb, material.edgeColor.a * texA * input.coverage);
273
+ var rgb = mix(material.edgeColor.rgb, material.colorOverride.rgb, material.colorOverride.a);
274
+ if (material.textured > 0.5) {
275
+ let lit = dot(-light.lights[0].direction.xyz, normalize(input.normal)) * 0.5 + 0.5;
276
+ let tint = textureSample(tintTexture, edgeSampler, input.uv).rgb;
277
+ rgb = tex.rgb * tint * mix(material.shadowColor.rgb, rgb, lit);
278
+ }
279
+ out.color = vec4f(rgb, material.edgeColor.a * texA * input.coverage);
216
280
  out.mask = vec4f(1.0, 1.0, 0.0, out.color.a);
217
281
  ${sceneIdPadWgsl("out")} return out;
218
282
  }