reze-engine 0.66.5 → 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.
@@ -470,9 +470,10 @@ fn _ag_ramp(slot: u32, uv: vec2f) -> vec4f {
470
470
  * Lamps and fog are the engine's, laid on after.
471
471
  */
472
472
  fn ag_uber(slot: u32, base: vec3f, metallic: f32, roughness: f32, occlusion: f32, n: vec3f, l: vec3f,
473
- row: f32, receive: f32, rimMask: f32, rimMid: f32, rimWidth: f32, rimTint: vec3f,
473
+ lightColor: vec3f, row: f32, receive: f32, rimMask: f32, rimMid: f32, rimWidth: f32, rimTint: vec3f,
474
474
  rimIntensity: f32, rimAlbedo: f32, rimInLight: f32, emissive: vec3f, reflection: f32,
475
- shade: f32, face: f32, wp: vec3f, v: vec3f) -> vec3f {
475
+ studio: vec3f, studioBlend: f32, shade: f32, face: f32, faceSpec: f32, hair: f32, hairSpec: vec3f,
476
+ wp: vec3f, v: vec3f) -> vec3f {
476
477
  let diffuse = base * (1.0 - metallic);
477
478
  let f0 = mix(vec3f(0.04), base, metallic);
478
479
  let pr = clamp(roughness, 0.0, 1.0);
@@ -509,9 +510,15 @@ fn ag_uber(slot: u32, base: vec3f, metallic: f32, roughness: f32, occlusion: f32
509
510
  let rim = tint * edge * (1.0 + rimInLight * (u - 1.0));
510
511
 
511
512
  // A face (FACE_MODE) takes no GGX: its light is the ramp, the rim and its
512
- // optional SDF highlight (off unless _FaceSpecular).
513
+ // SDF highlight (ag_face_spec), grey, × (0.9u + 0.1).
513
514
  let isFace = face > 0.5;
514
- let direct = (select(spec * f0, vec3f(0.0), isFace) + rim) * u + diffuse * ramp;
515
+ // Hair (_ANISOTROPIC_SPECULAR) takes its angel ring (ag_hair_ring) in place of
516
+ // GGX, not × F0: × (0.29u + 0.1/π).
517
+ let isHair = hair > 0.5;
518
+ var hl = select(spec * f0, hairSpec * (0.29 * u + 0.03183099), isHair);
519
+ hl = select(hl, vec3f(faceSpec * (0.9 * u + 0.1)), isFace);
520
+ // under the key light's colour, as the pass ends its direct term (× _LocalLightColor)
521
+ let direct = ((hl + rim) * u + diffuse * ramp) * lightColor;
515
522
 
516
523
  // the environment, with the game's fit of the split-sum
517
524
  let a004 = min((1.0 - pr) * (1.0 - pr), exp2(-9.28 * ndv)) * (1.0 - pr) + (0.0425 - 0.0275 * pr);
@@ -521,11 +528,33 @@ fn ag_uber(slot: u32, base: vec3f, metallic: f32, roughness: f32, occlusion: f32
521
528
  let env = rzWorldSpecularLod(reflect(-v, n), pr, 1.0) * reflection;
522
529
 
523
530
  // ... and the environment at a flat 0.0157 rather than through the BRDF fit.
524
- let envTerm = select((f0 * A + B) * env, env * 0.0157, isFace);
525
- return emissive + rzObjectAmbient(n) * diffuse + envTerm + direct;
531
+ let envTerm = select((f0 * A + B) * env, env * 0.0157, isFace || isHair);
532
+ // the ambient: the scene's, blended toward the studio's by the material
533
+ let ambient = mix(rzObjectAmbient(n), studio, saturate(studioBlend));
534
+ return emissive + ambient * diffuse + envTerm + direct;
526
535
  }
527
536
 
528
537
 
538
+ /**
539
+ * How much an albedo reads as skin, 0..1 — for a general look that has no
540
+ * game property mask to pick a surface's ramp row with. Skin across the game's
541
+ * newer characters (and the PMX textures ripped from them) is warm — red over
542
+ * green over blue, a hue within about 0..45 degrees — moderately saturated and
543
+ * fairly light; cloth, hair, metal and eyes are rarely all three.
544
+ */
545
+ fn ag_skin(c: vec3f) -> f32 {
546
+ let s = clamp(c, vec3f(0.0), vec3f(1.0));
547
+ let mx = max(s.r, max(s.g, s.b));
548
+ let mn = min(s.r, min(s.g, s.b));
549
+ let sat = (mx - mn) / max(mx, 1e-4);
550
+ // hue in degrees for the red-to-yellow sextant, where skin lives
551
+ 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);
552
+ let warm = smoothstep(-2.0, 4.0, hue) * (1.0 - smoothstep(38.0, 52.0, hue));
553
+ let tinted = smoothstep(0.06, 0.14, sat) * (1.0 - smoothstep(0.55, 0.75, sat));
554
+ let light = smoothstep(0.35, 0.55, mx);
555
+ return warm * tinted * light;
556
+ }
557
+
529
558
  /**
530
559
  * The face's shade from its SDF image (Uber FACE_MODE, _SDFType 0), in place of
531
560
  * N·L: the key light taken into the face's frame and flattened, the field read
@@ -559,11 +588,151 @@ fn ag_face_sdf(slot: u32, uv: vec2f, l: vec3f, n: vec3f, smoothness: f32, invert
559
588
  var m2 = max(saturate((b - th - 1.0) / k + 0.5), saturate((a - w) / k + 0.5));
560
589
  m2 = min(m2, min(saturate((a + w) / k + 0.5), saturate((b + w) / k + 0.5)));
561
590
  var r = flag * (m1 - m2) + m2;
562
- r *= (1.0 - abs(lf.z)) * 0.5 + 0.5;
591
+ r *= (1.0 - abs(lf.y)) * 0.5 + 0.5;
563
592
  r *= saturate(dot(n, l)) * 0.5 + 0.5;
564
593
  return r;
565
594
  }
566
595
 
596
+ /** The game's face frame (_World2Face) from the head bone: x to the face's
597
+ * right, y up, z out of it — a vector in world space taken into it. */
598
+ fn _ag_face_frame(d: vec3f) -> vec3f {
599
+ let hb = skinMats[u32(max(material.headBoneIndex, 0.0))];
600
+ return vec3f(dot(d, -normalize(hb[0].xyz)), dot(d, normalize(hb[1].xyz)), dot(d, -normalize(hb[2].xyz)));
601
+ }
602
+
603
+ /**
604
+ * The face's shade from its newer SDF image (Uber FACE_MODE, _SDFType 1), in
605
+ * place of N·L. The image paints, per texel, a normal in the face's level
606
+ * plane (alpha: its angle off the face's forward, toward the side the light
607
+ * is on) and how far it replaces the surface's own (red); three taps soften
608
+ * it. The painted normal yields to the real one as the light climbs, and the
609
+ * shade is a linear step of it against the light, _BlendSmoothness wide.
610
+ */
611
+ fn ag_face_sdf_new(slot: u32, uv: vec2f, l: vec3f, n: vec3f, smoothness: f32, invert: f32) -> f32 {
612
+ let lf = normalize(_ag_face_frame(l));
613
+ let nf = normalize(_ag_face_frame(n));
614
+ let flip = lf.x > 0.0;
615
+ // Unity's v runs up: uv is the mesh's (PMX, v down)
616
+ let ub = vec2f(mix(uv.x, 1.0 - uv.x, invert), 1.0 - uv.y);
617
+ let x0 = select(ub.x, 1.0 - ub.x, flip);
618
+ let x1 = select(ub.x - 0.004, 1.0 - (ub.x - 0.004), flip);
619
+ let x2 = select(ub.x + 0.004, 1.0 - (ub.x + 0.004), flip);
620
+ let t0 = _ag_ramp(slot, vec2f(x0, ub.y));
621
+ let t1 = _ag_ramp(slot, vec2f(x1, ub.y - 0.004));
622
+ let t2 = _ag_ramp(slot, vec2f(x2, ub.y - 0.004));
623
+ let w = (t0.r + t1.r + t2.r) / 3.0;
624
+ let th = ((t0.a + t1.a + t2.a) / 3.0 * 2.0 - 1.0) * 1.5707964;
625
+ let side = select(vec3f(-1.0, 0.0, 0.0), vec3f(1.0, 0.0, 0.0), flip);
626
+ var ns = side * sin(th) + vec3f(0.0, 0.0, 1.0) * cos(th);
627
+ ns = normalize(mix(ns, nf, lf.y * lf.y));
628
+ ns = normalize(mix(nf, ns, w));
629
+ let sm = max(smoothness, 0.001);
630
+ let lo = -sm * w;
631
+ let hi = w * (sm - 1.0) + 1.0;
632
+ return (clamp(dot(ns, lf), lo, hi) - lo) / (hi - lo);
633
+ }
634
+
635
+ /**
636
+ * The face's highlight (Uber FACE_MODE with the newer SDF), its GGX: the
637
+ * SDF image's green and blue paint a nose and cheek glint per light angle, lit
638
+ * where both pass a threshold set by how far the light, swung 85% toward the
639
+ * camera, comes from the front; the painted band slides up with the view
640
+ * (_AnisotropyShift). × _Anisotropy/π, the light's frontality and N·V. The
641
+ * pass lays it on as u·(0.9u + 0.1)·this — ag_uber's face_spec.
642
+ */
643
+ fn ag_face_spec(slot: u32, uv: vec2f, l: vec3f, n: vec3f, v: vec3f, anisotropy: f32, shift: f32) -> f32 {
644
+ let lf = _ag_face_frame(l);
645
+ let lxz = lf.xz * inverseSqrt(max(dot(lf.xz, lf.xz), 1.1754944e-38));
646
+ // unity_CameraToWorld column 2: OpenGL-style, the camera's BACK, toward the viewer
647
+ let cf = _ag_face_frame(vector_camera_to_world(vec3f(0.0, 0.0, 1.0))).xz;
648
+ let hd = normalize(mix(lxz, cf, 0.85));
649
+ let ub = vec2f(uv.x, 1.0 - uv.y);
650
+ let s = _ag_ramp(slot, vec2f(select(ub.x, 1.0 - ub.x, hd.x < 0.0), ub.y - v.y * shift));
651
+ let t = clamp(saturate(-hd.y - 0.70710677) * 3.4142134, 0.01, 0.99);
652
+ let on = select(0.0, 1.0, s.g >= 1.0 - t && s.b >= t);
653
+ return on * anisotropy / 3.14159265 * saturate((lxz.y * 0.5 + 0.5) * 4.0 - 3.0) * saturate(dot(n, v));
654
+ }
655
+
656
+ /**
657
+ * Aether Gazer's pupil (SimPipeline/Character/Eye, ForwardBase): unlit. The
658
+ * iris is a parallax-occlusion relief on the depth image (slot 0, red = height
659
+ * up from 1 − depth), marched in the eye's own tangent frame toward the eye of
660
+ * the viewer, _MinLayer..._MaxLayer layers as the view grazes; the material's
661
+ * picture is read where the march stops, brightened by its depth × _Intensity.
662
+ * A matcap (slot 2) on the normal map (slot 1, tangent normal as 0..1) scales
663
+ * it by (1 + matcap), and the sparkle mask (slot 3, blue / green) adds its
664
+ * glints, pulsing and wobbling with the clock. × level (_ProbeLightingBase).
665
+ */
666
+ fn ag_eye(uv: vec2f, n: vec3f, v: vec3f, wp: vec3f, heightScale: f32, minLayer: f32, maxLayer: f32,
667
+ intensity: f32, mainColor: vec3f, matColor: vec3f, matPow: f32, maskScale: f32, maskSoft: f32,
668
+ mask2: f32, mask2Color: vec3f, mask3: f32, mask3Color: vec3f, blink: f32, blinkScale: f32,
669
+ scaleSpeed: f32, rotateSpeed: f32, blinkAngle: f32, level: f32) -> vec3f {
670
+ // the tangent frame of the mesh uvs, as the march walks them
671
+ let dp1 = dpdx(wp);
672
+ let dp2 = dpdy(wp);
673
+ let du1 = dpdx(uv);
674
+ let du2 = dpdy(uv);
675
+ let det = du1.x * du2.y - du2.x * du1.y;
676
+ let inv = select(0.0, 1.0 / det, abs(det) > 1e-12);
677
+ let tu = (dp1 * du2.y - dp2 * du1.y) * inv;
678
+ let tv = (dp2 * du1.x - dp1 * du2.x) * inv;
679
+ let vt = vec3f(dot(v, safe_normal(tu)), dot(v, safe_normal(tv)), dot(v, n));
680
+ let vz = max(abs(vt.z), 1e-3);
681
+ let layers = mix(ceil(maxLayer), ceil(minLayer), saturate(abs(vt.z)));
682
+ // against the view, both ways in these uvs: measured, the iris error 48 -> 10 of 255
683
+ let stp = vec3f(-vt.xy * heightScale, vz) / (layers * vz);
684
+ var p = vec3f(uv, 0.0) + stp;
685
+ var prev = vec3f(uv, 0.0);
686
+ var depth = 1.0 - textureSampleLevel(groupTexture0, diffuseSampler, p.xy, 0.0).r;
687
+ var prevDepth = 0.0;
688
+ for (var i = 0; i < 16; i++) {
689
+ if (p.z >= depth) { break; }
690
+ prev = p;
691
+ prevDepth = depth;
692
+ p += stp;
693
+ depth = 1.0 - textureSampleLevel(groupTexture0, diffuseSampler, p.xy, 0.0).r;
694
+ }
695
+ // between the last two layers, where the ray crossed the relief
696
+ let after = depth - p.z;
697
+ let before = prevDepth - prev.z;
698
+ let w = select(0.0, after / (after - before), abs(after - before) > 1e-6);
699
+ let uvP = mix(p.xy, prev.xy, w);
700
+ let dLast = depth;
701
+ var alb = textureSampleLevel(diffuseTexture, diffuseSampler, uvP, 0.0).rgb * mainColor;
702
+ alb += alb * dLast * intensity;
703
+ // the matcap, multiplied in
704
+ let nm = node_normal_map(textureSampleLevel(groupTexture1, diffuseSampler, uv, 0.0).rgb, 1.0, n, wp, uv);
705
+ let vn = (camera.view * vec4f(nm, 0.0)).xy;
706
+ let mc = textureSampleLevel(groupTexture2, diffuseSampler, vn * 0.5 + 0.5, 0.0).rgb * matColor * matPow;
707
+ var col = alb + alb * mc;
708
+ // the glints: the mask wobbled about the eye's centre, pulsing
709
+ let t = camera.time;
710
+ let a = sin(radians(cos(t * rotateSpeed) * blinkAngle));
711
+ let c = cos(a);
712
+ let d = uv - vec2f(0.5);
713
+ let uvR = select(uv, vec2f(c * d.x - a * d.y, a * d.x + c * d.y) + 0.5, blink > 0.5);
714
+ let m = textureSampleLevel(groupTexture3, diffuseSampler, uvR, 0.0);
715
+ let lo = saturate(maskScale - maskSoft);
716
+ let hi = saturate(maskScale + maskSoft);
717
+ let s1 = select(0.0, smoothstep(lo, hi, m.g), mask2 > 0.5);
718
+ let s2 = select(0.0, smoothstep(lo, hi, m.b), mask3 > 0.5);
719
+ let k = select(1.0, (sin(12.0 * t * scaleSpeed) * 0.5 + 0.7) * blinkScale, blink > 0.5);
720
+ col += saturate(k * s1 * mask2Color + k * s2 * mask3Color);
721
+ return col * level;
722
+ }
723
+
724
+ /**
725
+ * Aether Gazer's hair highlight, its angel ring (Uber _ANISOTROPIC_SPECULAR,
726
+ * _HairLightingMode 0): a band painted in the hair's uvs (the slot's image),
727
+ * sliding up the strands as the view climbs (_AnisotropyShift), × the ring's
728
+ * colour and N·V·_Anisotropy. ag_uber's hair_spec; it lays on the u factor.
729
+ */
730
+ fn ag_hair_ring(slot: u32, uv: vec2f, n: vec3f, v: vec3f, color: vec3f, anisotropy: f32, shift: f32) -> vec3f {
731
+ let ub = vec2f(uv.x, 1.0 - uv.y);
732
+ let b = _ag_ramp(slot, vec2f(ub.x, ub.y - v.y * shift)).rgb;
733
+ return b * color * (saturate(dot(n, v)) * anisotropy);
734
+ }
735
+
567
736
  fn group_tex0(uv: vec2f) -> vec4f { return textureSample(groupTexture0, diffuseSampler, uv); }
568
737
  fn group_tex1(uv: vec2f) -> vec4f { return textureSample(groupTexture1, diffuseSampler, uv); }
569
738
  fn group_tex2(uv: vec2f) -> vec4f { return textureSample(groupTexture2, diffuseSampler, uv); }
@@ -1 +1 @@
1
- {"version":3,"file":"composite.d.ts","sourceRoot":"","sources":["../../../src/shaders/passes/composite.ts"],"names":[],"mappings":"AA8BA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;+BAgC+B;AAC/B;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,OAAO,CAAA;CAAE,EAAE,CAIhG;AAID,OAAO,EAAE,cAAc,EAAE,eAAe,EAAE,iBAAiB,EAAE,oBAAoB,EAAE,MAAM,gBAAgB,CAAA;AAEzG,KAAK,qBAAqB,GAAG;IAC3B,gFAAgF;IAChF,IAAI,EAAE,MAAM,CAAA;IACZ,kFAAkF;IAClF,UAAU,EAAE,MAAM,CAAA;IAClB,4DAA4D;IAC5D,aAAa,EAAE,OAAO,CAAA;IACtB,oEAAoE;IACpE,aAAa,EAAE,OAAO,CAAA;IACtB;6DACyD;IACzD,MAAM,CAAC,EAAE,OAAO,CAAA;IAChB,gFAAgF;IAChF,aAAa,CAAC,EAAE,OAAO,CAAA;IACvB,gEAAgE;IAChE,QAAQ,EAAE,MAAM,CAAA;IAChB;mFAC+E;IAC/E,GAAG,CAAC,EAAE,OAAO,CAAA;IACb;;;yEAGqE;IACrE,UAAU,EAAE,MAAM,CAAA;IAClB;;8EAE0E;IAC1E,KAAK,CAAC,EAAE,MAAM,EAAE,CAAA;CACjB,CAAA;AAwRD;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,gBAAgB,g6fAuI5B,CAAA;AA4MD,wBAAgB,oBAAoB,CAAC,MAAM,CAAC,EAAE,qBAAqB,GAAG,IAAI,GAAG,MAAM,CAclF;AAED;;;;;;;;GAQG;AACH,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,qBAAqB,GAAG,MAAM,CA2GtE;AAED,iFAAiF;AACjF,eAAO,MAAM,qBAAqB,QAA6B,CAAA"}
1
+ {"version":3,"file":"composite.d.ts","sourceRoot":"","sources":["../../../src/shaders/passes/composite.ts"],"names":[],"mappings":"AA8BA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;+BAgC+B;AAC/B;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,OAAO,CAAA;CAAE,EAAE,CAIhG;AAID,OAAO,EAAE,cAAc,EAAE,eAAe,EAAE,iBAAiB,EAAE,oBAAoB,EAAE,MAAM,gBAAgB,CAAA;AAEzG,KAAK,qBAAqB,GAAG;IAC3B,gFAAgF;IAChF,IAAI,EAAE,MAAM,CAAA;IACZ,kFAAkF;IAClF,UAAU,EAAE,MAAM,CAAA;IAClB,4DAA4D;IAC5D,aAAa,EAAE,OAAO,CAAA;IACtB,oEAAoE;IACpE,aAAa,EAAE,OAAO,CAAA;IACtB;6DACyD;IACzD,MAAM,CAAC,EAAE,OAAO,CAAA;IAChB,gFAAgF;IAChF,aAAa,CAAC,EAAE,OAAO,CAAA;IACvB,gEAAgE;IAChE,QAAQ,EAAE,MAAM,CAAA;IAChB;mFAC+E;IAC/E,GAAG,CAAC,EAAE,OAAO,CAAA;IACb;;;yEAGqE;IACrE,UAAU,EAAE,MAAM,CAAA;IAClB;;8EAE0E;IAC1E,KAAK,CAAC,EAAE,MAAM,EAAE,CAAA;CACjB,CAAA;AA0RD;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,gBAAgB,g6fAuI5B,CAAA;AA4MD,wBAAgB,oBAAoB,CAAC,MAAM,CAAC,EAAE,qBAAqB,GAAG,IAAI,GAAG,MAAM,CAclF;AAED;;;;;;;;GAQG;AACH,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,qBAAqB,GAAG,MAAM,CA2GtE;AAED,iFAAiF;AACjF,eAAO,MAAM,qBAAqB,QAA6B,CAAA"}
@@ -97,7 +97,7 @@ override APPLY_GAMMA: bool = true;
97
97
  @group(0) @binding(0) var hdrTex: texture_2d<f32>;
98
98
  @group(0) @binding(1) var bloomTex: texture_2d<f32>; // bloomUpTexture mip 0 (full pyramid top)
99
99
  @group(0) @binding(2) var bloomSamp: sampler;
100
- @group(0) @binding(3) var<uniform> viewU: array<vec4<f32>, 15>;
100
+ @group(0) @binding(3) var<uniform> viewU: array<vec4<f32>, 16>;
101
101
  // Aux mask/alpha texture. .r = bloom mask (unused here; bloom blit uses it).
102
102
  // .g = accumulated canvas alpha (what hdr.a carried before the HDR format
103
103
  // became rg11b10ufloat). We unpremultiply HDR by this alpha for tonemap, then
@@ -119,6 +119,8 @@ override APPLY_GAMMA: bool = true;
119
119
  // viewU[9] = (grade slope.rgb, grade flags) — bit 0 the CDL grade(), bit 1 the
120
120
  // scene's own LUT (setStageGrade) — see _rzGradeScene() below.
121
121
  // viewU[10] = (camera world position, _) — refreshed with the basis above.
122
+ // viewU[11..14] = the cast's positions (effects; count in viewU[10].w).
123
+ // viewU[15] = (soft curve contrast, _, _, _).
122
124
  // invGamma = 1/gamma precomputed on CPU — avoids a per-pixel divide.
123
125
  @group(0) @binding(6) var bgEquirect: texture_2d<f32>;
124
126
  // The scene pass's own MSAA depth buffer, bound depth-only. NOT an extra
@@ -217,7 +219,7 @@ fn srgb_encode(x: f32) -> f32 {
217
219
  * The game's ACES branch is off in every scene measured.
218
220
  */
219
221
  fn softTransform(c: vec3f) -> vec3f {
220
- let t = pow(max(vec3f(1.0) - exp(-2.5 * max(c, vec3f(0.0))), vec3f(0.0)), vec3f(1.4));
222
+ let t = pow(max(vec3f(1.0) - exp(-2.5 * max(c, vec3f(0.0))), vec3f(0.0)), vec3f(viewU[15].x));
221
223
  return vec3f(srgb_encode(t.r), srgb_encode(t.g), srgb_encode(t.b));
222
224
  }
223
225
 
@@ -12,5 +12,12 @@ export declare const RZ_OUTLINE_WIDTH_OFFSET = 24;
12
12
  /** Where the scene-wide colour override sits, in bytes: rgb and a weight, 0
13
13
  * for the material's own edge colour (Engine.setOutlineColor). */
14
14
  export declare const RZ_OUTLINE_COLOR_OFFSET = 32;
15
+ /** Where a style graph's outline (ShaderGraph.outline) sits, in bytes: the
16
+ * textured flag at 28, then its shadow colour at 48. Unset, a hull draws its
17
+ * PMX edge colour flat, as MMD does. */
18
+ export declare const RZ_OUTLINE_TEXTURED_OFFSET = 28;
19
+ export declare const RZ_OUTLINE_SHADOW_OFFSET = 48;
20
+ /** The hull's own block, in floats. */
21
+ export declare const RZ_OUTLINE_UNIFORM_FLOATS = 16;
15
22
  export declare function outlineShaderWgsl(): string;
16
23
  //# sourceMappingURL=outline.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"outline.d.ts","sourceRoot":"","sources":["../../../src/shaders/passes/outline.ts"],"names":[],"mappings":"AAqBA;;;;;;GAMG;AACH,eAAO,MAAM,0BAA0B,KAAK,CAAA;AAC5C;yCACyC;AACzC,eAAO,MAAM,uBAAuB,KAAK,CAAA;AACzC;mEACmE;AACnE,eAAO,MAAM,uBAAuB,KAAK,CAAA;AAEzC,wBAAgB,iBAAiB,IAAI,MAAM,CAoM1C"}
1
+ {"version":3,"file":"outline.d.ts","sourceRoot":"","sources":["../../../src/shaders/passes/outline.ts"],"names":[],"mappings":"AAqBA;;;;;;GAMG;AACH,eAAO,MAAM,0BAA0B,KAAK,CAAA;AAC5C;yCACyC;AACzC,eAAO,MAAM,uBAAuB,KAAK,CAAA;AACzC;mEACmE;AACnE,eAAO,MAAM,uBAAuB,KAAK,CAAA;AACzC;;yCAEyC;AACzC,eAAO,MAAM,0BAA0B,KAAK,CAAA;AAC5C,eAAO,MAAM,wBAAwB,KAAK,CAAA;AAC1C,uCAAuC;AACvC,eAAO,MAAM,yBAAyB,KAAK,CAAA;AAE3C,wBAAgB,iBAAiB,IAAI,MAAM,CAgP1C"}
@@ -30,6 +30,13 @@ export const RZ_OUTLINE_WIDTH_OFFSET = 24;
30
30
  /** Where the scene-wide colour override sits, in bytes: rgb and a weight, 0
31
31
  * for the material's own edge colour (Engine.setOutlineColor). */
32
32
  export const RZ_OUTLINE_COLOR_OFFSET = 32;
33
+ /** Where a style graph's outline (ShaderGraph.outline) sits, in bytes: the
34
+ * textured flag at 28, then its shadow colour at 48. Unset, a hull draws its
35
+ * PMX edge colour flat, as MMD does. */
36
+ export const RZ_OUTLINE_TEXTURED_OFFSET = 28;
37
+ export const RZ_OUTLINE_SHADOW_OFFSET = 48;
38
+ /** The hull's own block, in floats. */
39
+ export const RZ_OUTLINE_UNIFORM_FLOATS = 16;
33
40
  export function outlineShaderWgsl() {
34
41
  return /* wgsl */ `
35
42
  ${DISSOLVE_WGSL}
@@ -59,17 +66,30 @@ struct MaterialUniforms {
59
66
  * scales the author's edgeSize rather than replacing it, so a model's thin
60
67
  * and thick lines keep their proportion. */
61
68
  widthScale: f32,
62
- _padding3: f32,
69
+ /** 1: a style graph's outline, the game's (Uber pass "Outline"): edgeSize is
70
+ * a world width, held between 1.2 and 3 px of a 1920-wide frame, and the
71
+ * colour is the picture × lerp(shadowColor, edgeColor, the key's
72
+ * half-Lambert). 0: the PMX edge, flat. */
73
+ textured: f32,
63
74
  /** The scene's colour in place of edgeColor.rgb, by .a (0 or 1). The
64
75
  * material's own alpha still holds — a line its author made sheer stays so. */
65
76
  colorOverride: vec4f,
77
+ shadowColor: vec4f,
66
78
  };
67
79
 
80
+ struct OutlineLight { direction: vec4f, color: vec4f, };
81
+ /** The head of the scene's light block: the key is lights[0]. */
82
+ struct OutlineLights { ambientColor: vec4f, lights: array<OutlineLight, 4>, };
83
+
68
84
  @group(0) @binding(0) var<uniform> camera: CameraUniforms;
69
85
  @group(0) @binding(1) var edgeSampler: sampler;
86
+ @group(0) @binding(2) var<uniform> light: OutlineLights;
70
87
  @group(1) @binding(0) var<storage, read> skinMats: array<mat4x4f>;
71
88
  @group(2) @binding(0) var<uniform> material: MaterialUniforms;
72
89
  @group(2) @binding(1) var diffuseTexture: texture_2d<f32>;
90
+ /** A look's outline tint (ShaderGraph.outline.tint): one of its images, white
91
+ * when it names none. */
92
+ @group(2) @binding(2) var tintTexture: texture_2d<f32>;
73
93
 
74
94
  struct VertexOutput {
75
95
  @builtin(position) position: vec4f,
@@ -84,6 +104,7 @@ struct VertexOutput {
84
104
  /** How much of a pixel-wide line this one really is (1 = all of it): a line
85
105
  * held at the minimum width fades by what it was short. */
86
106
  @location(3) coverage: f32,
107
+ @location(4) normal: vec3f,
87
108
  };
88
109
 
89
110
  /**
@@ -106,6 +127,11 @@ const RZ_OUTLINE_FULL_FIGURE = 12.5;
106
127
  * the shortfall, so far lines thin out without aliasing into dashes. */
107
128
  const RZ_OUTLINE_MIN_PX = 1.5;
108
129
 
130
+ fn safe_normal2(v: vec2f) -> vec2f {
131
+ let l = length(v);
132
+ return select(vec2f(0.0), v / l, l > 1e-6);
133
+ }
134
+
109
135
  @vertex fn vs(
110
136
  @location(0) position: vec3f,
111
137
  @location(1) normal: vec3f,
@@ -165,21 +191,40 @@ const RZ_OUTLINE_MIN_PX = 1.5;
165
191
  // once the frame is taller than a figure. The PMX per-vertex edge scale
166
192
  // multiplies it all, as vertex colour alpha does in the game.
167
193
  let w = max(clipPos.w, 1e-4);
168
- let breakDepth = RZ_OUTLINE_FULL_FIGURE * camera.projection[1][1];
169
- let nearNdc = viewNormal.xy * (material.edgeSize * material.widthScale * outlineNormal.w * 4.0 / refViewport);
170
- var ndc = nearNdc * (min(w, breakDepth) / w);
171
-
172
- // Antialiased minimum (the game's _OutlineAntialias): held at the minimum
173
- // width and faded by the shortfall, never thinner. The game fades by
174
- // sqrt(coverage.x · coverage.y), per axis; for one width that is the ratio
175
- // itself.
176
- let px = length(ndc * deviceViewport * 0.5);
177
- let minPx = RZ_OUTLINE_MIN_PX * max(1.0, camera.viewportHeight / 1080.0);
194
+ var ndc: vec2f;
178
195
  var coverage = 1.0;
179
- if (px < minPx) {
180
- coverage = px / minPx;
181
- ndc = select(vec2f(0.0), ndc * (minPx / px), px > 1e-6);
196
+ if (material.textured > 0.5) {
197
+ // The game's: a view-space offset of edgeSize world units along the view
198
+ // normal, projected, then each axis held between 1.2 px and 1.2 px ×
199
+ // (1 + _OutlineMaxOffsetMultiplier, 1.5) of a 1920-wide frame; a line the
200
+ // minimum had to push fades by how far it was pushed.
201
+ let off = viewNormal.xy * material.edgeSize * material.widthScale * outlineNormal.w;
202
+ let d1 = vec2f(off.x * camera.projection[0][0], off.y * camera.projection[1][1]) / w;
203
+ // the minimum along the push direction (the game's dir · 2/screen · 1.2)
204
+ let pxNdc = 2.0 / refViewport * 1.2 * safe_normal2(viewNormal.xy);
205
+ let mx = abs(pxNdc) * 2.5;
206
+ let o = min(mx, max(abs(d1), abs(pxNdc)));
207
+ // fadeOut = min(length(saturate(off − |d1|))·75, 1), on clip offsets (× w)
208
+ // in game units: w here is MMD units, 8 to the game's metre
209
+ coverage = 1.0 - min(length(saturate((o - abs(d1)) * w / 8.0)) * 75.0, 1.0);
210
+ ndc = o * select(vec2f(1.0), sign(d1), abs(d1) > vec2f(0.0));
211
+ } else {
212
+ let breakDepth = RZ_OUTLINE_FULL_FIGURE * camera.projection[1][1];
213
+ let nearNdc = viewNormal.xy * (material.edgeSize * material.widthScale * outlineNormal.w * 4.0 / refViewport);
214
+ ndc = nearNdc * (min(w, breakDepth) / w);
215
+
216
+ // Antialiased minimum (the game's _OutlineAntialias): held at the minimum
217
+ // width and faded by the shortfall, never thinner. The game fades by
218
+ // sqrt(coverage.x · coverage.y), per axis; for one width that is the ratio
219
+ // itself.
220
+ let px = length(ndc * deviceViewport * 0.5);
221
+ let minPx = RZ_OUTLINE_MIN_PX * max(1.0, camera.viewportHeight / 1080.0);
222
+ if (px < minPx) {
223
+ coverage = px / minPx;
224
+ ndc = select(vec2f(0.0), ndc * (minPx / px), px > 1e-6);
225
+ }
182
226
  }
227
+ output.normal = worldNormal;
183
228
  output.coverage = coverage;
184
229
  output.position = vec4f(clipPos.xy + ndc * w, clipPos.z, clipPos.w);
185
230
  output.uv = uv;
@@ -211,7 +256,8 @@ ${sceneFsOutWgsl({ name: "FSOut", aux: "mask" })}
211
256
  // bind-pose position — three passes, one rule, or they disagree about which
212
257
  // pieces are still there.
213
258
  if (material.dissolve < 0.9995 && input.faceT > material.dissolve) { discard; }
214
- let texA = textureSample(diffuseTexture, edgeSampler, input.uv).a;
259
+ let tex = textureSample(diffuseTexture, edgeSampler, input.uv);
260
+ let texA = tex.a;
215
261
  if (texA < 0.05) {
216
262
  discard;
217
263
  }
@@ -220,7 +266,12 @@ ${sceneFsOutWgsl({ name: "FSOut", aux: "mask" })}
220
266
  // alpha above), it is drawn right after the surface it traces so what lies
221
267
  // under a faded line is already there, and 4× MSAA would quantize a fade to
222
268
  // four steps.
223
- let rgb = mix(material.edgeColor.rgb, material.colorOverride.rgb, material.colorOverride.a);
269
+ var rgb = mix(material.edgeColor.rgb, material.colorOverride.rgb, material.colorOverride.a);
270
+ if (material.textured > 0.5) {
271
+ let lit = dot(-light.lights[0].direction.xyz, normalize(input.normal)) * 0.5 + 0.5;
272
+ let tint = textureSample(tintTexture, edgeSampler, input.uv).rgb;
273
+ rgb = tex.rgb * tint * mix(material.shadowColor.rgb, rgb, lit);
274
+ }
224
275
  out.color = vec4f(rgb, material.edgeColor.a * texA * input.coverage);
225
276
  out.mask = vec4f(1.0, 1.0, 0.0, out.color.a);
226
277
  ${sceneIdPadWgsl("out")} return out;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "reze-engine",
3
- "version": "0.66.5",
3
+ "version": "0.67.0",
4
4
  "description": "A lightweight WebGPU engine for real-time 3D MMD/PMX model rendering",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",