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.
- package/dist/engine.d.ts +20 -1
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +83 -17
- package/dist/graph/presets/body.js +1 -1
- package/dist/graph/presets/cloth_rough.js +1 -1
- package/dist/graph/presets/cloth_smooth.js +1 -1
- package/dist/graph/presets/face.js +1 -1
- package/dist/graph/presets/metal.js +1 -1
- package/dist/graph/registry.d.ts.map +1 -1
- package/dist/graph/registry.js +84 -6
- package/dist/graph/schema.d.ts +14 -0
- package/dist/graph/schema.d.ts.map +1 -1
- package/dist/shaders/materials/nodes.d.ts +1 -1
- package/dist/shaders/materials/nodes.d.ts.map +1 -1
- package/dist/shaders/materials/nodes.js +176 -7
- package/dist/shaders/passes/composite.d.ts.map +1 -1
- package/dist/shaders/passes/composite.js +4 -2
- package/dist/shaders/passes/outline.d.ts +7 -0
- package/dist/shaders/passes/outline.d.ts.map +1 -1
- package/dist/shaders/passes/outline.js +67 -16
- package/package.json +1 -1
- package/src/engine.ts +101 -19
- package/src/graph/presets/body.ts +1 -1
- package/src/graph/presets/cloth_rough.ts +1 -1
- package/src/graph/presets/cloth_smooth.ts +1 -1
- package/src/graph/presets/face.ts +1 -1
- package/src/graph/presets/metal.ts +1 -1
- package/src/graph/registry.ts +85 -6
- package/src/graph/schema.ts +14 -0
- package/src/shaders/materials/nodes.ts +176 -7
- package/src/shaders/passes/composite.ts +4 -2
- package/src/shaders/passes/outline.ts +67 -16
|
@@ -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,
|
|
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
|
-
//
|
|
514
|
+
// SDF highlight (ag_face_spec), grey, × (0.9u + 0.1).
|
|
514
515
|
let isFace = face > 0.5;
|
|
515
|
-
|
|
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
|
-
|
|
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.
|
|
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>,
|
|
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(
|
|
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
|
|
|
@@ -33,6 +33,13 @@ export const RZ_OUTLINE_WIDTH_OFFSET = 24
|
|
|
33
33
|
/** Where the scene-wide colour override sits, in bytes: rgb and a weight, 0
|
|
34
34
|
* for the material's own edge colour (Engine.setOutlineColor). */
|
|
35
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
|
|
36
43
|
|
|
37
44
|
export function outlineShaderWgsl(): string {
|
|
38
45
|
return /* wgsl */ `
|
|
@@ -63,17 +70,30 @@ struct MaterialUniforms {
|
|
|
63
70
|
* scales the author's edgeSize rather than replacing it, so a model's thin
|
|
64
71
|
* and thick lines keep their proportion. */
|
|
65
72
|
widthScale: f32,
|
|
66
|
-
|
|
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,
|
|
67
78
|
/** The scene's colour in place of edgeColor.rgb, by .a (0 or 1). The
|
|
68
79
|
* material's own alpha still holds — a line its author made sheer stays so. */
|
|
69
80
|
colorOverride: vec4f,
|
|
81
|
+
shadowColor: vec4f,
|
|
70
82
|
};
|
|
71
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
|
+
|
|
72
88
|
@group(0) @binding(0) var<uniform> camera: CameraUniforms;
|
|
73
89
|
@group(0) @binding(1) var edgeSampler: sampler;
|
|
90
|
+
@group(0) @binding(2) var<uniform> light: OutlineLights;
|
|
74
91
|
@group(1) @binding(0) var<storage, read> skinMats: array<mat4x4f>;
|
|
75
92
|
@group(2) @binding(0) var<uniform> material: MaterialUniforms;
|
|
76
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>;
|
|
77
97
|
|
|
78
98
|
struct VertexOutput {
|
|
79
99
|
@builtin(position) position: vec4f,
|
|
@@ -88,6 +108,7 @@ struct VertexOutput {
|
|
|
88
108
|
/** How much of a pixel-wide line this one really is (1 = all of it): a line
|
|
89
109
|
* held at the minimum width fades by what it was short. */
|
|
90
110
|
@location(3) coverage: f32,
|
|
111
|
+
@location(4) normal: vec3f,
|
|
91
112
|
};
|
|
92
113
|
|
|
93
114
|
/**
|
|
@@ -110,6 +131,11 @@ const RZ_OUTLINE_FULL_FIGURE = 12.5;
|
|
|
110
131
|
* the shortfall, so far lines thin out without aliasing into dashes. */
|
|
111
132
|
const RZ_OUTLINE_MIN_PX = 1.5;
|
|
112
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
|
+
|
|
113
139
|
@vertex fn vs(
|
|
114
140
|
@location(0) position: vec3f,
|
|
115
141
|
@location(1) normal: vec3f,
|
|
@@ -169,21 +195,40 @@ const RZ_OUTLINE_MIN_PX = 1.5;
|
|
|
169
195
|
// once the frame is taller than a figure. The PMX per-vertex edge scale
|
|
170
196
|
// multiplies it all, as vertex colour alpha does in the game.
|
|
171
197
|
let w = max(clipPos.w, 1e-4);
|
|
172
|
-
|
|
173
|
-
let nearNdc = viewNormal.xy * (material.edgeSize * material.widthScale * outlineNormal.w * 4.0 / refViewport);
|
|
174
|
-
var ndc = nearNdc * (min(w, breakDepth) / w);
|
|
175
|
-
|
|
176
|
-
// Antialiased minimum (the game's _OutlineAntialias): held at the minimum
|
|
177
|
-
// width and faded by the shortfall, never thinner. The game fades by
|
|
178
|
-
// sqrt(coverage.x · coverage.y), per axis; for one width that is the ratio
|
|
179
|
-
// itself.
|
|
180
|
-
let px = length(ndc * deviceViewport * 0.5);
|
|
181
|
-
let minPx = RZ_OUTLINE_MIN_PX * max(1.0, camera.viewportHeight / 1080.0);
|
|
198
|
+
var ndc: vec2f;
|
|
182
199
|
var coverage = 1.0;
|
|
183
|
-
if (
|
|
184
|
-
|
|
185
|
-
|
|
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
|
+
}
|
|
186
230
|
}
|
|
231
|
+
output.normal = worldNormal;
|
|
187
232
|
output.coverage = coverage;
|
|
188
233
|
output.position = vec4f(clipPos.xy + ndc * w, clipPos.z, clipPos.w);
|
|
189
234
|
output.uv = uv;
|
|
@@ -215,7 +260,8 @@ ${sceneFsOutWgsl({ name: "FSOut", aux: "mask" })}
|
|
|
215
260
|
// bind-pose position — three passes, one rule, or they disagree about which
|
|
216
261
|
// pieces are still there.
|
|
217
262
|
if (material.dissolve < 0.9995 && input.faceT > material.dissolve) { discard; }
|
|
218
|
-
let
|
|
263
|
+
let tex = textureSample(diffuseTexture, edgeSampler, input.uv);
|
|
264
|
+
let texA = tex.a;
|
|
219
265
|
if (texA < 0.05) {
|
|
220
266
|
discard;
|
|
221
267
|
}
|
|
@@ -224,7 +270,12 @@ ${sceneFsOutWgsl({ name: "FSOut", aux: "mask" })}
|
|
|
224
270
|
// alpha above), it is drawn right after the surface it traces so what lies
|
|
225
271
|
// under a faded line is already there, and 4× MSAA would quantize a fade to
|
|
226
272
|
// four steps.
|
|
227
|
-
|
|
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
|
+
}
|
|
228
279
|
out.color = vec4f(rgb, material.edgeColor.a * texA * input.coverage);
|
|
229
280
|
out.mask = vec4f(1.0, 1.0, 0.0, out.color.a);
|
|
230
281
|
${sceneIdPadWgsl("out")} return out;
|