reze-engine 0.1.14 → 0.1.15
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 +4 -0
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +391 -62
- package/package.json +1 -1
- package/src/engine.ts +404 -66
package/dist/engine.d.ts
CHANGED
|
@@ -24,8 +24,12 @@ export declare class Engine {
|
|
|
24
24
|
private outlinePipeline;
|
|
25
25
|
private hairOutlinePipeline;
|
|
26
26
|
private hairOutlineOverEyesPipeline;
|
|
27
|
+
private hairUnifiedOutlinePipeline;
|
|
27
28
|
private hairMultiplyPipeline;
|
|
28
29
|
private hairOpaquePipeline;
|
|
30
|
+
private hairUnifiedPipelineOverEyes;
|
|
31
|
+
private hairUnifiedPipelineOverNonEyes;
|
|
32
|
+
private hairDepthPipeline;
|
|
29
33
|
private eyePipeline;
|
|
30
34
|
private hairBindGroupLayout;
|
|
31
35
|
private outlineBindGroupLayout;
|
package/dist/engine.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"engine.d.ts","sourceRoot":"","sources":["../src/engine.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"engine.d.ts","sourceRoot":"","sources":["../src/engine.ts"],"names":[],"mappings":"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"}
|
package/dist/engine.js
CHANGED
|
@@ -110,7 +110,7 @@ export class Engine {
|
|
|
110
110
|
rimIntensity: f32,
|
|
111
111
|
rimPower: f32,
|
|
112
112
|
rimColor: vec3f,
|
|
113
|
-
|
|
113
|
+
isOverEyes: f32, // 1.0 if rendering over eyes, 0.0 otherwise
|
|
114
114
|
};
|
|
115
115
|
|
|
116
116
|
struct VertexOutput {
|
|
@@ -188,7 +188,14 @@ export class Engine {
|
|
|
188
188
|
let rimLight = material.rimColor * material.rimIntensity * rimFactor;
|
|
189
189
|
|
|
190
190
|
let color = albedo * lightAccum + rimLight;
|
|
191
|
-
|
|
191
|
+
|
|
192
|
+
// Dynamic branching: adjust alpha based on whether we're over eyes
|
|
193
|
+
// This allows single-pass hair rendering instead of two separate passes
|
|
194
|
+
var finalAlpha = material.alpha * material.alphaMultiplier;
|
|
195
|
+
if (material.isOverEyes > 0.5) {
|
|
196
|
+
finalAlpha *= 0.5; // Hair over eyes gets 50% alpha
|
|
197
|
+
}
|
|
198
|
+
|
|
192
199
|
if (finalAlpha < 0.001) {
|
|
193
200
|
discard;
|
|
194
201
|
}
|
|
@@ -297,9 +304,9 @@ export class Engine {
|
|
|
297
304
|
struct MaterialUniforms {
|
|
298
305
|
edgeColor: vec4f,
|
|
299
306
|
edgeSize: f32,
|
|
307
|
+
isOverEyes: f32, // 1.0 if rendering over eyes, 0.0 otherwise (for hair outlines)
|
|
300
308
|
_padding1: f32,
|
|
301
309
|
_padding2: f32,
|
|
302
|
-
_padding3: f32,
|
|
303
310
|
};
|
|
304
311
|
|
|
305
312
|
@group(0) @binding(0) var<uniform> camera: CameraUniforms;
|
|
@@ -349,7 +356,15 @@ export class Engine {
|
|
|
349
356
|
}
|
|
350
357
|
|
|
351
358
|
@fragment fn fs() -> @location(0) vec4f {
|
|
352
|
-
|
|
359
|
+
var color = material.edgeColor;
|
|
360
|
+
|
|
361
|
+
// Dynamic branching: adjust alpha for hair outlines over eyes
|
|
362
|
+
// This allows single-pass outline rendering instead of two separate passes
|
|
363
|
+
if (material.isOverEyes > 0.5) {
|
|
364
|
+
color.a *= 0.5; // Hair outlines over eyes get 50% alpha
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
return color;
|
|
353
368
|
}
|
|
354
369
|
`,
|
|
355
370
|
});
|
|
@@ -571,6 +586,76 @@ export class Engine {
|
|
|
571
586
|
count: this.sampleCount,
|
|
572
587
|
},
|
|
573
588
|
});
|
|
589
|
+
// Unified hair outline pipeline: single pass without stencil testing
|
|
590
|
+
// Uses depth test "less-equal" to draw everywhere hair exists
|
|
591
|
+
// Shader branches on isOverEyes uniform to adjust alpha dynamically
|
|
592
|
+
// This eliminates the need for two separate outline passes
|
|
593
|
+
this.hairUnifiedOutlinePipeline = this.device.createRenderPipeline({
|
|
594
|
+
label: "unified hair outline pipeline",
|
|
595
|
+
layout: outlinePipelineLayout,
|
|
596
|
+
vertex: {
|
|
597
|
+
module: outlineShaderModule,
|
|
598
|
+
buffers: [
|
|
599
|
+
{
|
|
600
|
+
arrayStride: 8 * 4,
|
|
601
|
+
attributes: [
|
|
602
|
+
{
|
|
603
|
+
shaderLocation: 0,
|
|
604
|
+
offset: 0,
|
|
605
|
+
format: "float32x3",
|
|
606
|
+
},
|
|
607
|
+
{
|
|
608
|
+
shaderLocation: 1,
|
|
609
|
+
offset: 3 * 4,
|
|
610
|
+
format: "float32x3",
|
|
611
|
+
},
|
|
612
|
+
],
|
|
613
|
+
},
|
|
614
|
+
{
|
|
615
|
+
arrayStride: 4 * 2,
|
|
616
|
+
attributes: [{ shaderLocation: 3, offset: 0, format: "uint16x4" }],
|
|
617
|
+
},
|
|
618
|
+
{
|
|
619
|
+
arrayStride: 4,
|
|
620
|
+
attributes: [{ shaderLocation: 4, offset: 0, format: "unorm8x4" }],
|
|
621
|
+
},
|
|
622
|
+
],
|
|
623
|
+
},
|
|
624
|
+
fragment: {
|
|
625
|
+
module: outlineShaderModule,
|
|
626
|
+
targets: [
|
|
627
|
+
{
|
|
628
|
+
format: this.presentationFormat,
|
|
629
|
+
blend: {
|
|
630
|
+
color: {
|
|
631
|
+
srcFactor: "src-alpha",
|
|
632
|
+
dstFactor: "one-minus-src-alpha",
|
|
633
|
+
operation: "add",
|
|
634
|
+
},
|
|
635
|
+
alpha: {
|
|
636
|
+
srcFactor: "one",
|
|
637
|
+
dstFactor: "one-minus-src-alpha",
|
|
638
|
+
operation: "add",
|
|
639
|
+
},
|
|
640
|
+
},
|
|
641
|
+
},
|
|
642
|
+
],
|
|
643
|
+
},
|
|
644
|
+
primitive: {
|
|
645
|
+
cullMode: "back",
|
|
646
|
+
},
|
|
647
|
+
depthStencil: {
|
|
648
|
+
format: "depth24plus-stencil8",
|
|
649
|
+
depthWriteEnabled: false, // Don't write depth - let hair geometry control depth
|
|
650
|
+
depthCompare: "less-equal", // Only draw where hair depth exists (no stencil test needed)
|
|
651
|
+
depthBias: -0.0001, // Small negative bias to bring outline slightly closer for depth test
|
|
652
|
+
depthBiasSlopeScale: 0.0,
|
|
653
|
+
depthBiasClamp: 0.0,
|
|
654
|
+
},
|
|
655
|
+
multisample: {
|
|
656
|
+
count: this.sampleCount,
|
|
657
|
+
},
|
|
658
|
+
});
|
|
574
659
|
// Unified hair pipeline - can be used for both over-eyes and over-non-eyes
|
|
575
660
|
// The difference is controlled by stencil state and alpha multiplier in material uniform
|
|
576
661
|
this.hairMultiplyPipeline = this.device.createRenderPipeline({
|
|
@@ -767,6 +852,235 @@ export class Engine {
|
|
|
767
852
|
},
|
|
768
853
|
multisample: { count: this.sampleCount },
|
|
769
854
|
});
|
|
855
|
+
// Depth-only shader for hair pre-pass (reduces overdraw by early depth rejection)
|
|
856
|
+
const depthOnlyShaderModule = this.device.createShaderModule({
|
|
857
|
+
label: "depth only shader",
|
|
858
|
+
code: /* wgsl */ `
|
|
859
|
+
struct CameraUniforms {
|
|
860
|
+
view: mat4x4f,
|
|
861
|
+
projection: mat4x4f,
|
|
862
|
+
viewPos: vec3f,
|
|
863
|
+
_padding: f32,
|
|
864
|
+
};
|
|
865
|
+
|
|
866
|
+
@group(0) @binding(0) var<uniform> camera: CameraUniforms;
|
|
867
|
+
@group(0) @binding(4) var<storage, read> skinMats: array<mat4x4f>;
|
|
868
|
+
|
|
869
|
+
@vertex fn vs(
|
|
870
|
+
@location(0) position: vec3f,
|
|
871
|
+
@location(1) normal: vec3f,
|
|
872
|
+
@location(3) joints0: vec4<u32>,
|
|
873
|
+
@location(4) weights0: vec4<f32>
|
|
874
|
+
) -> @builtin(position) vec4f {
|
|
875
|
+
let pos4 = vec4f(position, 1.0);
|
|
876
|
+
|
|
877
|
+
// Normalize weights
|
|
878
|
+
let weightSum = weights0.x + weights0.y + weights0.z + weights0.w;
|
|
879
|
+
var normalizedWeights: vec4f;
|
|
880
|
+
if (weightSum > 0.0001) {
|
|
881
|
+
normalizedWeights = weights0 / weightSum;
|
|
882
|
+
} else {
|
|
883
|
+
normalizedWeights = vec4f(1.0, 0.0, 0.0, 0.0);
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
var skinnedPos = vec4f(0.0, 0.0, 0.0, 0.0);
|
|
887
|
+
for (var i = 0u; i < 4u; i++) {
|
|
888
|
+
let j = joints0[i];
|
|
889
|
+
let w = normalizedWeights[i];
|
|
890
|
+
let m = skinMats[j];
|
|
891
|
+
skinnedPos += (m * pos4) * w;
|
|
892
|
+
}
|
|
893
|
+
let worldPos = skinnedPos.xyz;
|
|
894
|
+
let clipPos = camera.projection * camera.view * vec4f(worldPos, 1.0);
|
|
895
|
+
return clipPos;
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
// Minimal fragment shader - returns transparent, no color writes (writeMask: 0)
|
|
899
|
+
// Required because render pass has color attachments
|
|
900
|
+
// Depth is still written even though we don't write color
|
|
901
|
+
@fragment fn fs() -> @location(0) vec4f {
|
|
902
|
+
return vec4f(0.0, 0.0, 0.0, 0.0); // Transparent - color writes disabled via writeMask
|
|
903
|
+
}
|
|
904
|
+
`,
|
|
905
|
+
});
|
|
906
|
+
// Hair depth pre-pass pipeline (depth-only, no color writes)
|
|
907
|
+
// This eliminates most overdraw by rejecting fragments early before expensive shading
|
|
908
|
+
// Note: Must have a color target to match render pass, but we disable all color writes
|
|
909
|
+
this.hairDepthPipeline = this.device.createRenderPipeline({
|
|
910
|
+
label: "hair depth pre-pass",
|
|
911
|
+
layout: sharedPipelineLayout,
|
|
912
|
+
vertex: {
|
|
913
|
+
module: depthOnlyShaderModule,
|
|
914
|
+
buffers: [
|
|
915
|
+
{
|
|
916
|
+
arrayStride: 8 * 4,
|
|
917
|
+
attributes: [
|
|
918
|
+
{ shaderLocation: 0, offset: 0, format: "float32x3" },
|
|
919
|
+
{ shaderLocation: 1, offset: 3 * 4, format: "float32x3" },
|
|
920
|
+
],
|
|
921
|
+
},
|
|
922
|
+
{
|
|
923
|
+
arrayStride: 4 * 2,
|
|
924
|
+
attributes: [{ shaderLocation: 3, offset: 0, format: "uint16x4" }],
|
|
925
|
+
},
|
|
926
|
+
{
|
|
927
|
+
arrayStride: 4,
|
|
928
|
+
attributes: [{ shaderLocation: 4, offset: 0, format: "unorm8x4" }],
|
|
929
|
+
},
|
|
930
|
+
],
|
|
931
|
+
},
|
|
932
|
+
fragment: {
|
|
933
|
+
module: depthOnlyShaderModule,
|
|
934
|
+
entryPoint: "fs",
|
|
935
|
+
targets: [
|
|
936
|
+
{
|
|
937
|
+
format: this.presentationFormat,
|
|
938
|
+
writeMask: 0, // Disable all color writes - we only care about depth
|
|
939
|
+
},
|
|
940
|
+
],
|
|
941
|
+
},
|
|
942
|
+
primitive: { cullMode: "none" },
|
|
943
|
+
depthStencil: {
|
|
944
|
+
format: "depth24plus-stencil8",
|
|
945
|
+
depthWriteEnabled: true,
|
|
946
|
+
depthCompare: "less",
|
|
947
|
+
},
|
|
948
|
+
multisample: { count: this.sampleCount },
|
|
949
|
+
});
|
|
950
|
+
// Unified hair pipeline: single pass with dynamic branching in shader
|
|
951
|
+
// Uses stencil testing to filter fragments, then shader branches on isOverEyes uniform
|
|
952
|
+
// This eliminates the need for separate pipelines - same shader, different stencil states
|
|
953
|
+
// We create two variants: one for over-eyes (stencil == 1) and one for over-non-eyes (stencil != 1)
|
|
954
|
+
// Unified pipeline for hair over eyes (stencil == 1)
|
|
955
|
+
this.hairUnifiedPipelineOverEyes = this.device.createRenderPipeline({
|
|
956
|
+
label: "unified hair pipeline (over eyes)",
|
|
957
|
+
layout: sharedPipelineLayout,
|
|
958
|
+
vertex: {
|
|
959
|
+
module: shaderModule,
|
|
960
|
+
buffers: [
|
|
961
|
+
{
|
|
962
|
+
arrayStride: 8 * 4,
|
|
963
|
+
attributes: [
|
|
964
|
+
{ shaderLocation: 0, offset: 0, format: "float32x3" },
|
|
965
|
+
{ shaderLocation: 1, offset: 3 * 4, format: "float32x3" },
|
|
966
|
+
{ shaderLocation: 2, offset: 6 * 4, format: "float32x2" },
|
|
967
|
+
],
|
|
968
|
+
},
|
|
969
|
+
{
|
|
970
|
+
arrayStride: 4 * 2,
|
|
971
|
+
attributes: [{ shaderLocation: 3, offset: 0, format: "uint16x4" }],
|
|
972
|
+
},
|
|
973
|
+
{
|
|
974
|
+
arrayStride: 4,
|
|
975
|
+
attributes: [{ shaderLocation: 4, offset: 0, format: "unorm8x4" }],
|
|
976
|
+
},
|
|
977
|
+
],
|
|
978
|
+
},
|
|
979
|
+
fragment: {
|
|
980
|
+
module: shaderModule,
|
|
981
|
+
targets: [
|
|
982
|
+
{
|
|
983
|
+
format: this.presentationFormat,
|
|
984
|
+
blend: {
|
|
985
|
+
color: {
|
|
986
|
+
srcFactor: "src-alpha",
|
|
987
|
+
dstFactor: "one-minus-src-alpha",
|
|
988
|
+
operation: "add",
|
|
989
|
+
},
|
|
990
|
+
alpha: {
|
|
991
|
+
srcFactor: "one",
|
|
992
|
+
dstFactor: "one-minus-src-alpha",
|
|
993
|
+
operation: "add",
|
|
994
|
+
},
|
|
995
|
+
},
|
|
996
|
+
},
|
|
997
|
+
],
|
|
998
|
+
},
|
|
999
|
+
primitive: { cullMode: "none" },
|
|
1000
|
+
depthStencil: {
|
|
1001
|
+
format: "depth24plus-stencil8",
|
|
1002
|
+
depthWriteEnabled: false, // Don't write depth (already written in pre-pass)
|
|
1003
|
+
depthCompare: "equal", // Only render where depth matches pre-pass
|
|
1004
|
+
stencilFront: {
|
|
1005
|
+
compare: "equal", // Only render where stencil == 1 (over eyes)
|
|
1006
|
+
failOp: "keep",
|
|
1007
|
+
depthFailOp: "keep",
|
|
1008
|
+
passOp: "keep",
|
|
1009
|
+
},
|
|
1010
|
+
stencilBack: {
|
|
1011
|
+
compare: "equal",
|
|
1012
|
+
failOp: "keep",
|
|
1013
|
+
depthFailOp: "keep",
|
|
1014
|
+
passOp: "keep",
|
|
1015
|
+
},
|
|
1016
|
+
},
|
|
1017
|
+
multisample: { count: this.sampleCount },
|
|
1018
|
+
});
|
|
1019
|
+
// Unified pipeline for hair over non-eyes (stencil != 1)
|
|
1020
|
+
this.hairUnifiedPipelineOverNonEyes = this.device.createRenderPipeline({
|
|
1021
|
+
label: "unified hair pipeline (over non-eyes)",
|
|
1022
|
+
layout: sharedPipelineLayout,
|
|
1023
|
+
vertex: {
|
|
1024
|
+
module: shaderModule,
|
|
1025
|
+
buffers: [
|
|
1026
|
+
{
|
|
1027
|
+
arrayStride: 8 * 4,
|
|
1028
|
+
attributes: [
|
|
1029
|
+
{ shaderLocation: 0, offset: 0, format: "float32x3" },
|
|
1030
|
+
{ shaderLocation: 1, offset: 3 * 4, format: "float32x3" },
|
|
1031
|
+
{ shaderLocation: 2, offset: 6 * 4, format: "float32x2" },
|
|
1032
|
+
],
|
|
1033
|
+
},
|
|
1034
|
+
{
|
|
1035
|
+
arrayStride: 4 * 2,
|
|
1036
|
+
attributes: [{ shaderLocation: 3, offset: 0, format: "uint16x4" }],
|
|
1037
|
+
},
|
|
1038
|
+
{
|
|
1039
|
+
arrayStride: 4,
|
|
1040
|
+
attributes: [{ shaderLocation: 4, offset: 0, format: "unorm8x4" }],
|
|
1041
|
+
},
|
|
1042
|
+
],
|
|
1043
|
+
},
|
|
1044
|
+
fragment: {
|
|
1045
|
+
module: shaderModule,
|
|
1046
|
+
targets: [
|
|
1047
|
+
{
|
|
1048
|
+
format: this.presentationFormat,
|
|
1049
|
+
blend: {
|
|
1050
|
+
color: {
|
|
1051
|
+
srcFactor: "src-alpha",
|
|
1052
|
+
dstFactor: "one-minus-src-alpha",
|
|
1053
|
+
operation: "add",
|
|
1054
|
+
},
|
|
1055
|
+
alpha: {
|
|
1056
|
+
srcFactor: "one",
|
|
1057
|
+
dstFactor: "one-minus-src-alpha",
|
|
1058
|
+
operation: "add",
|
|
1059
|
+
},
|
|
1060
|
+
},
|
|
1061
|
+
},
|
|
1062
|
+
],
|
|
1063
|
+
},
|
|
1064
|
+
primitive: { cullMode: "none" },
|
|
1065
|
+
depthStencil: {
|
|
1066
|
+
format: "depth24plus-stencil8",
|
|
1067
|
+
depthWriteEnabled: false, // Don't write depth (already written in pre-pass)
|
|
1068
|
+
depthCompare: "equal", // Only render where depth matches pre-pass
|
|
1069
|
+
stencilFront: {
|
|
1070
|
+
compare: "not-equal", // Only render where stencil != 1 (over non-eyes)
|
|
1071
|
+
failOp: "keep",
|
|
1072
|
+
depthFailOp: "keep",
|
|
1073
|
+
passOp: "keep",
|
|
1074
|
+
},
|
|
1075
|
+
stencilBack: {
|
|
1076
|
+
compare: "not-equal",
|
|
1077
|
+
failOp: "keep",
|
|
1078
|
+
depthFailOp: "keep",
|
|
1079
|
+
passOp: "keep",
|
|
1080
|
+
},
|
|
1081
|
+
},
|
|
1082
|
+
multisample: { count: this.sampleCount },
|
|
1083
|
+
});
|
|
770
1084
|
}
|
|
771
1085
|
// Create compute shader for skin matrix computation
|
|
772
1086
|
createSkinMatrixComputePipeline() {
|
|
@@ -1444,7 +1758,7 @@ export class Engine {
|
|
|
1444
1758
|
materialUniformData[4] = this.rimLightColor[0]; // rimColor.r
|
|
1445
1759
|
materialUniformData[5] = this.rimLightColor[1]; // rimColor.g
|
|
1446
1760
|
materialUniformData[6] = this.rimLightColor[2]; // rimColor.b
|
|
1447
|
-
materialUniformData[7] = 0.0; //
|
|
1761
|
+
materialUniformData[7] = 0.0; // isOverEyes: 0.0 for non-hair materials
|
|
1448
1762
|
const materialUniformBuffer = this.device.createBuffer({
|
|
1449
1763
|
label: `material uniform: ${mat.name}`,
|
|
1450
1764
|
size: materialUniformData.byteLength,
|
|
@@ -1476,22 +1790,36 @@ export class Engine {
|
|
|
1476
1790
|
});
|
|
1477
1791
|
}
|
|
1478
1792
|
else if (mat.isHair) {
|
|
1479
|
-
// For hair materials, create
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1793
|
+
// For hair materials, create a single bind group that will be used with the unified pipeline
|
|
1794
|
+
// The shader will dynamically branch based on isOverEyes uniform
|
|
1795
|
+
// We still need two uniform buffers (one for each render mode) but can reuse the same bind group structure
|
|
1796
|
+
const materialUniformDataHair = new Float32Array(8);
|
|
1797
|
+
materialUniformDataHair[0] = materialAlpha;
|
|
1798
|
+
materialUniformDataHair[1] = 1.0; // alphaMultiplier: base value, shader will adjust
|
|
1799
|
+
materialUniformDataHair[2] = this.rimLightIntensity;
|
|
1800
|
+
materialUniformDataHair[3] = this.rimLightPower;
|
|
1801
|
+
materialUniformDataHair[4] = this.rimLightColor[0]; // rimColor.r
|
|
1802
|
+
materialUniformDataHair[5] = this.rimLightColor[1]; // rimColor.g
|
|
1803
|
+
materialUniformDataHair[6] = this.rimLightColor[2]; // rimColor.b
|
|
1804
|
+
materialUniformDataHair[7] = 0.0; // isOverEyes: will be set per draw call
|
|
1805
|
+
// Create uniform buffers for both modes (we'll update them per frame)
|
|
1489
1806
|
const materialUniformBufferOverEyes = this.device.createBuffer({
|
|
1490
1807
|
label: `material uniform (over eyes): ${mat.name}`,
|
|
1491
|
-
size:
|
|
1808
|
+
size: materialUniformDataHair.byteLength,
|
|
1492
1809
|
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
|
|
1493
1810
|
});
|
|
1811
|
+
const materialUniformDataOverEyes = new Float32Array(materialUniformDataHair);
|
|
1812
|
+
materialUniformDataOverEyes[7] = 1.0; // isOverEyes = 1.0
|
|
1494
1813
|
this.device.queue.writeBuffer(materialUniformBufferOverEyes, 0, materialUniformDataOverEyes);
|
|
1814
|
+
const materialUniformBufferOverNonEyes = this.device.createBuffer({
|
|
1815
|
+
label: `material uniform (over non-eyes): ${mat.name}`,
|
|
1816
|
+
size: materialUniformDataHair.byteLength,
|
|
1817
|
+
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
|
|
1818
|
+
});
|
|
1819
|
+
const materialUniformDataOverNonEyes = new Float32Array(materialUniformDataHair);
|
|
1820
|
+
materialUniformDataOverNonEyes[7] = 0.0; // isOverEyes = 0.0
|
|
1821
|
+
this.device.queue.writeBuffer(materialUniformBufferOverNonEyes, 0, materialUniformDataOverNonEyes);
|
|
1822
|
+
// Create bind groups for both modes (they share everything except the uniform buffer)
|
|
1495
1823
|
const bindGroupOverEyes = this.device.createBindGroup({
|
|
1496
1824
|
label: `material bind group (over eyes): ${mat.name}`,
|
|
1497
1825
|
layout: this.hairBindGroupLayout,
|
|
@@ -1506,28 +1834,6 @@ export class Engine {
|
|
|
1506
1834
|
{ binding: 7, resource: { buffer: materialUniformBufferOverEyes } },
|
|
1507
1835
|
],
|
|
1508
1836
|
});
|
|
1509
|
-
this.hairDrawsOverEyes.push({
|
|
1510
|
-
count: matCount,
|
|
1511
|
-
firstIndex: runningFirstIndex,
|
|
1512
|
-
bindGroup: bindGroupOverEyes,
|
|
1513
|
-
isTransparent,
|
|
1514
|
-
});
|
|
1515
|
-
// Create material uniform for hair over non-eyes (alphaMultiplier = 1.0)
|
|
1516
|
-
const materialUniformDataOverNonEyes = new Float32Array(8);
|
|
1517
|
-
materialUniformDataOverNonEyes[0] = materialAlpha;
|
|
1518
|
-
materialUniformDataOverNonEyes[1] = 1.0; // alphaMultiplier: 1.0 for over-non-eyes
|
|
1519
|
-
materialUniformDataOverNonEyes[2] = this.rimLightIntensity;
|
|
1520
|
-
materialUniformDataOverNonEyes[3] = this.rimLightPower;
|
|
1521
|
-
materialUniformDataOverNonEyes[4] = this.rimLightColor[0]; // rimColor.r
|
|
1522
|
-
materialUniformDataOverNonEyes[5] = this.rimLightColor[1]; // rimColor.g
|
|
1523
|
-
materialUniformDataOverNonEyes[6] = this.rimLightColor[2]; // rimColor.b
|
|
1524
|
-
materialUniformDataOverNonEyes[7] = 0.0; // _padding1
|
|
1525
|
-
const materialUniformBufferOverNonEyes = this.device.createBuffer({
|
|
1526
|
-
label: `material uniform (over non-eyes): ${mat.name}`,
|
|
1527
|
-
size: materialUniformDataOverNonEyes.byteLength,
|
|
1528
|
-
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
|
|
1529
|
-
});
|
|
1530
|
-
this.device.queue.writeBuffer(materialUniformBufferOverNonEyes, 0, materialUniformDataOverNonEyes);
|
|
1531
1837
|
const bindGroupOverNonEyes = this.device.createBindGroup({
|
|
1532
1838
|
label: `material bind group (over non-eyes): ${mat.name}`,
|
|
1533
1839
|
layout: this.hairBindGroupLayout,
|
|
@@ -1542,6 +1848,13 @@ export class Engine {
|
|
|
1542
1848
|
{ binding: 7, resource: { buffer: materialUniformBufferOverNonEyes } },
|
|
1543
1849
|
],
|
|
1544
1850
|
});
|
|
1851
|
+
// Store both bind groups - we'll use them with the unified pipeline
|
|
1852
|
+
this.hairDrawsOverEyes.push({
|
|
1853
|
+
count: matCount,
|
|
1854
|
+
firstIndex: runningFirstIndex,
|
|
1855
|
+
bindGroup: bindGroupOverEyes,
|
|
1856
|
+
isTransparent,
|
|
1857
|
+
});
|
|
1545
1858
|
this.hairDrawsOverNonEyes.push({
|
|
1546
1859
|
count: matCount,
|
|
1547
1860
|
firstIndex: runningFirstIndex,
|
|
@@ -1568,11 +1881,14 @@ export class Engine {
|
|
|
1568
1881
|
// Outline for all materials (including transparent) - Edge flag is at bit 4 (0x10) in PMX format, not bit 0 (0x01)
|
|
1569
1882
|
if ((mat.edgeFlag & 0x10) !== 0 && mat.edgeSize > 0) {
|
|
1570
1883
|
const materialUniformData = new Float32Array(8);
|
|
1571
|
-
materialUniformData[0] = mat.edgeColor[0];
|
|
1572
|
-
materialUniformData[1] = mat.edgeColor[1];
|
|
1573
|
-
materialUniformData[2] = mat.edgeColor[2];
|
|
1574
|
-
materialUniformData[3] = mat.edgeColor[3];
|
|
1884
|
+
materialUniformData[0] = mat.edgeColor[0]; // edgeColor.r
|
|
1885
|
+
materialUniformData[1] = mat.edgeColor[1]; // edgeColor.g
|
|
1886
|
+
materialUniformData[2] = mat.edgeColor[2]; // edgeColor.b
|
|
1887
|
+
materialUniformData[3] = mat.edgeColor[3]; // edgeColor.a
|
|
1575
1888
|
materialUniformData[4] = mat.edgeSize;
|
|
1889
|
+
materialUniformData[5] = mat.isHair ? 0.0 : 0.0; // isOverEyes: 0.0 for all (unified pipeline doesn't use stencil)
|
|
1890
|
+
materialUniformData[6] = 0.0; // _padding1
|
|
1891
|
+
materialUniformData[7] = 0.0; // _padding2
|
|
1576
1892
|
const materialUniformBuffer = this.device.createBuffer({
|
|
1577
1893
|
label: `outline material uniform: ${mat.name}`,
|
|
1578
1894
|
size: materialUniformData.byteLength,
|
|
@@ -1706,27 +2022,37 @@ export class Engine {
|
|
|
1706
2022
|
this.drawCallCount++;
|
|
1707
2023
|
}
|
|
1708
2024
|
}
|
|
1709
|
-
// PASS 3: Hair rendering - optimized
|
|
1710
|
-
//
|
|
1711
|
-
//
|
|
2025
|
+
// PASS 3: Hair rendering - optimized with depth pre-pass and unified pipeline
|
|
2026
|
+
// Depth pre-pass: render hair depth-only to eliminate overdraw early
|
|
2027
|
+
// Then render shaded hair once with depth test "equal" to only shade visible fragments
|
|
1712
2028
|
this.drawOutlines(pass, false); // Opaque outlines
|
|
1713
|
-
// 3a: Hair
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
pass.
|
|
2029
|
+
// 3a: Hair depth pre-pass (depth-only, no color writes)
|
|
2030
|
+
// This eliminates most overdraw by rejecting fragments early before expensive shading
|
|
2031
|
+
if (this.hairDrawsOverEyes.length > 0 || this.hairDrawsOverNonEyes.length > 0) {
|
|
2032
|
+
pass.setPipeline(this.hairDepthPipeline);
|
|
2033
|
+
// Render all hair materials for depth (no stencil test needed for depth pass)
|
|
1717
2034
|
for (const draw of this.hairDrawsOverEyes) {
|
|
2035
|
+
if (draw.count > 0) {
|
|
2036
|
+
// Use the same bind group structure (camera, skin matrices) for depth pass
|
|
2037
|
+
pass.setBindGroup(0, draw.bindGroup);
|
|
2038
|
+
pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0);
|
|
2039
|
+
}
|
|
2040
|
+
}
|
|
2041
|
+
for (const draw of this.hairDrawsOverNonEyes) {
|
|
1718
2042
|
if (draw.count > 0) {
|
|
1719
2043
|
pass.setBindGroup(0, draw.bindGroup);
|
|
1720
2044
|
pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0);
|
|
1721
|
-
this.drawCallCount++;
|
|
1722
2045
|
}
|
|
1723
2046
|
}
|
|
1724
2047
|
}
|
|
1725
|
-
// 3b: Hair
|
|
1726
|
-
|
|
1727
|
-
|
|
2048
|
+
// 3b: Hair shading pass - unified pipeline with dynamic branching
|
|
2049
|
+
// Uses depth test "equal" to only render where depth was written in pre-pass
|
|
2050
|
+
// Shader branches on isOverEyes uniform to adjust alpha dynamically
|
|
2051
|
+
// This eliminates one full geometry pass compared to the old approach
|
|
2052
|
+
if (this.hairDrawsOverEyes.length > 0) {
|
|
2053
|
+
pass.setPipeline(this.hairUnifiedPipelineOverEyes);
|
|
1728
2054
|
pass.setStencilReference(1);
|
|
1729
|
-
for (const draw of this.
|
|
2055
|
+
for (const draw of this.hairDrawsOverEyes) {
|
|
1730
2056
|
if (draw.count > 0) {
|
|
1731
2057
|
pass.setBindGroup(0, draw.bindGroup);
|
|
1732
2058
|
pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0);
|
|
@@ -1734,20 +2060,23 @@ export class Engine {
|
|
|
1734
2060
|
}
|
|
1735
2061
|
}
|
|
1736
2062
|
}
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
// Over eyes
|
|
1740
|
-
pass.setPipeline(this.hairOutlineOverEyesPipeline);
|
|
2063
|
+
if (this.hairDrawsOverNonEyes.length > 0) {
|
|
2064
|
+
pass.setPipeline(this.hairUnifiedPipelineOverNonEyes);
|
|
1741
2065
|
pass.setStencilReference(1);
|
|
1742
|
-
for (const draw of this.
|
|
2066
|
+
for (const draw of this.hairDrawsOverNonEyes) {
|
|
1743
2067
|
if (draw.count > 0) {
|
|
1744
2068
|
pass.setBindGroup(0, draw.bindGroup);
|
|
1745
2069
|
pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0);
|
|
2070
|
+
this.drawCallCount++;
|
|
1746
2071
|
}
|
|
1747
2072
|
}
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
2073
|
+
}
|
|
2074
|
+
// 3c: Hair outlines - unified single pass without stencil testing
|
|
2075
|
+
// Uses depth test "less-equal" to draw everywhere hair exists
|
|
2076
|
+
// Shader branches on isOverEyes uniform to adjust alpha dynamically (currently always 0.0)
|
|
2077
|
+
// This eliminates the need for two separate outline passes
|
|
2078
|
+
if (this.hairOutlineDraws.length > 0) {
|
|
2079
|
+
pass.setPipeline(this.hairUnifiedOutlinePipeline);
|
|
1751
2080
|
for (const draw of this.hairOutlineDraws) {
|
|
1752
2081
|
if (draw.count > 0) {
|
|
1753
2082
|
pass.setBindGroup(0, draw.bindGroup);
|
package/package.json
CHANGED
package/src/engine.ts
CHANGED
|
@@ -29,8 +29,12 @@ export class Engine {
|
|
|
29
29
|
private outlinePipeline!: GPURenderPipeline
|
|
30
30
|
private hairOutlinePipeline!: GPURenderPipeline
|
|
31
31
|
private hairOutlineOverEyesPipeline!: GPURenderPipeline
|
|
32
|
+
private hairUnifiedOutlinePipeline!: GPURenderPipeline // Unified hair outline pipeline without stencil testing
|
|
32
33
|
private hairMultiplyPipeline!: GPURenderPipeline
|
|
33
34
|
private hairOpaquePipeline!: GPURenderPipeline
|
|
35
|
+
private hairUnifiedPipelineOverEyes!: GPURenderPipeline // Unified hair pipeline for over-eyes (stencil == 1)
|
|
36
|
+
private hairUnifiedPipelineOverNonEyes!: GPURenderPipeline // Unified hair pipeline for over-non-eyes (stencil != 1)
|
|
37
|
+
private hairDepthPipeline!: GPURenderPipeline // Depth-only pipeline for hair pre-pass
|
|
34
38
|
private eyePipeline!: GPURenderPipeline
|
|
35
39
|
private hairBindGroupLayout!: GPUBindGroupLayout
|
|
36
40
|
private outlineBindGroupLayout!: GPUBindGroupLayout
|
|
@@ -169,7 +173,7 @@ export class Engine {
|
|
|
169
173
|
rimIntensity: f32,
|
|
170
174
|
rimPower: f32,
|
|
171
175
|
rimColor: vec3f,
|
|
172
|
-
|
|
176
|
+
isOverEyes: f32, // 1.0 if rendering over eyes, 0.0 otherwise
|
|
173
177
|
};
|
|
174
178
|
|
|
175
179
|
struct VertexOutput {
|
|
@@ -247,7 +251,14 @@ export class Engine {
|
|
|
247
251
|
let rimLight = material.rimColor * material.rimIntensity * rimFactor;
|
|
248
252
|
|
|
249
253
|
let color = albedo * lightAccum + rimLight;
|
|
250
|
-
|
|
254
|
+
|
|
255
|
+
// Dynamic branching: adjust alpha based on whether we're over eyes
|
|
256
|
+
// This allows single-pass hair rendering instead of two separate passes
|
|
257
|
+
var finalAlpha = material.alpha * material.alphaMultiplier;
|
|
258
|
+
if (material.isOverEyes > 0.5) {
|
|
259
|
+
finalAlpha *= 0.5; // Hair over eyes gets 50% alpha
|
|
260
|
+
}
|
|
261
|
+
|
|
251
262
|
if (finalAlpha < 0.001) {
|
|
252
263
|
discard;
|
|
253
264
|
}
|
|
@@ -362,9 +373,9 @@ export class Engine {
|
|
|
362
373
|
struct MaterialUniforms {
|
|
363
374
|
edgeColor: vec4f,
|
|
364
375
|
edgeSize: f32,
|
|
376
|
+
isOverEyes: f32, // 1.0 if rendering over eyes, 0.0 otherwise (for hair outlines)
|
|
365
377
|
_padding1: f32,
|
|
366
378
|
_padding2: f32,
|
|
367
|
-
_padding3: f32,
|
|
368
379
|
};
|
|
369
380
|
|
|
370
381
|
@group(0) @binding(0) var<uniform> camera: CameraUniforms;
|
|
@@ -414,7 +425,15 @@ export class Engine {
|
|
|
414
425
|
}
|
|
415
426
|
|
|
416
427
|
@fragment fn fs() -> @location(0) vec4f {
|
|
417
|
-
|
|
428
|
+
var color = material.edgeColor;
|
|
429
|
+
|
|
430
|
+
// Dynamic branching: adjust alpha for hair outlines over eyes
|
|
431
|
+
// This allows single-pass outline rendering instead of two separate passes
|
|
432
|
+
if (material.isOverEyes > 0.5) {
|
|
433
|
+
color.a *= 0.5; // Hair outlines over eyes get 50% alpha
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
return color;
|
|
418
437
|
}
|
|
419
438
|
`,
|
|
420
439
|
})
|
|
@@ -641,6 +660,77 @@ export class Engine {
|
|
|
641
660
|
},
|
|
642
661
|
})
|
|
643
662
|
|
|
663
|
+
// Unified hair outline pipeline: single pass without stencil testing
|
|
664
|
+
// Uses depth test "less-equal" to draw everywhere hair exists
|
|
665
|
+
// Shader branches on isOverEyes uniform to adjust alpha dynamically
|
|
666
|
+
// This eliminates the need for two separate outline passes
|
|
667
|
+
this.hairUnifiedOutlinePipeline = this.device.createRenderPipeline({
|
|
668
|
+
label: "unified hair outline pipeline",
|
|
669
|
+
layout: outlinePipelineLayout,
|
|
670
|
+
vertex: {
|
|
671
|
+
module: outlineShaderModule,
|
|
672
|
+
buffers: [
|
|
673
|
+
{
|
|
674
|
+
arrayStride: 8 * 4,
|
|
675
|
+
attributes: [
|
|
676
|
+
{
|
|
677
|
+
shaderLocation: 0,
|
|
678
|
+
offset: 0,
|
|
679
|
+
format: "float32x3" as GPUVertexFormat,
|
|
680
|
+
},
|
|
681
|
+
{
|
|
682
|
+
shaderLocation: 1,
|
|
683
|
+
offset: 3 * 4,
|
|
684
|
+
format: "float32x3" as GPUVertexFormat,
|
|
685
|
+
},
|
|
686
|
+
],
|
|
687
|
+
},
|
|
688
|
+
{
|
|
689
|
+
arrayStride: 4 * 2,
|
|
690
|
+
attributes: [{ shaderLocation: 3, offset: 0, format: "uint16x4" as GPUVertexFormat }],
|
|
691
|
+
},
|
|
692
|
+
{
|
|
693
|
+
arrayStride: 4,
|
|
694
|
+
attributes: [{ shaderLocation: 4, offset: 0, format: "unorm8x4" as GPUVertexFormat }],
|
|
695
|
+
},
|
|
696
|
+
],
|
|
697
|
+
},
|
|
698
|
+
fragment: {
|
|
699
|
+
module: outlineShaderModule,
|
|
700
|
+
targets: [
|
|
701
|
+
{
|
|
702
|
+
format: this.presentationFormat,
|
|
703
|
+
blend: {
|
|
704
|
+
color: {
|
|
705
|
+
srcFactor: "src-alpha",
|
|
706
|
+
dstFactor: "one-minus-src-alpha",
|
|
707
|
+
operation: "add",
|
|
708
|
+
},
|
|
709
|
+
alpha: {
|
|
710
|
+
srcFactor: "one",
|
|
711
|
+
dstFactor: "one-minus-src-alpha",
|
|
712
|
+
operation: "add",
|
|
713
|
+
},
|
|
714
|
+
},
|
|
715
|
+
},
|
|
716
|
+
],
|
|
717
|
+
},
|
|
718
|
+
primitive: {
|
|
719
|
+
cullMode: "back",
|
|
720
|
+
},
|
|
721
|
+
depthStencil: {
|
|
722
|
+
format: "depth24plus-stencil8",
|
|
723
|
+
depthWriteEnabled: false, // Don't write depth - let hair geometry control depth
|
|
724
|
+
depthCompare: "less-equal", // Only draw where hair depth exists (no stencil test needed)
|
|
725
|
+
depthBias: -0.0001, // Small negative bias to bring outline slightly closer for depth test
|
|
726
|
+
depthBiasSlopeScale: 0.0,
|
|
727
|
+
depthBiasClamp: 0.0,
|
|
728
|
+
},
|
|
729
|
+
multisample: {
|
|
730
|
+
count: this.sampleCount,
|
|
731
|
+
},
|
|
732
|
+
})
|
|
733
|
+
|
|
644
734
|
// Unified hair pipeline - can be used for both over-eyes and over-non-eyes
|
|
645
735
|
// The difference is controlled by stencil state and alpha multiplier in material uniform
|
|
646
736
|
this.hairMultiplyPipeline = this.device.createRenderPipeline({
|
|
@@ -839,6 +929,240 @@ export class Engine {
|
|
|
839
929
|
},
|
|
840
930
|
multisample: { count: this.sampleCount },
|
|
841
931
|
})
|
|
932
|
+
|
|
933
|
+
// Depth-only shader for hair pre-pass (reduces overdraw by early depth rejection)
|
|
934
|
+
const depthOnlyShaderModule = this.device.createShaderModule({
|
|
935
|
+
label: "depth only shader",
|
|
936
|
+
code: /* wgsl */ `
|
|
937
|
+
struct CameraUniforms {
|
|
938
|
+
view: mat4x4f,
|
|
939
|
+
projection: mat4x4f,
|
|
940
|
+
viewPos: vec3f,
|
|
941
|
+
_padding: f32,
|
|
942
|
+
};
|
|
943
|
+
|
|
944
|
+
@group(0) @binding(0) var<uniform> camera: CameraUniforms;
|
|
945
|
+
@group(0) @binding(4) var<storage, read> skinMats: array<mat4x4f>;
|
|
946
|
+
|
|
947
|
+
@vertex fn vs(
|
|
948
|
+
@location(0) position: vec3f,
|
|
949
|
+
@location(1) normal: vec3f,
|
|
950
|
+
@location(3) joints0: vec4<u32>,
|
|
951
|
+
@location(4) weights0: vec4<f32>
|
|
952
|
+
) -> @builtin(position) vec4f {
|
|
953
|
+
let pos4 = vec4f(position, 1.0);
|
|
954
|
+
|
|
955
|
+
// Normalize weights
|
|
956
|
+
let weightSum = weights0.x + weights0.y + weights0.z + weights0.w;
|
|
957
|
+
var normalizedWeights: vec4f;
|
|
958
|
+
if (weightSum > 0.0001) {
|
|
959
|
+
normalizedWeights = weights0 / weightSum;
|
|
960
|
+
} else {
|
|
961
|
+
normalizedWeights = vec4f(1.0, 0.0, 0.0, 0.0);
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
var skinnedPos = vec4f(0.0, 0.0, 0.0, 0.0);
|
|
965
|
+
for (var i = 0u; i < 4u; i++) {
|
|
966
|
+
let j = joints0[i];
|
|
967
|
+
let w = normalizedWeights[i];
|
|
968
|
+
let m = skinMats[j];
|
|
969
|
+
skinnedPos += (m * pos4) * w;
|
|
970
|
+
}
|
|
971
|
+
let worldPos = skinnedPos.xyz;
|
|
972
|
+
let clipPos = camera.projection * camera.view * vec4f(worldPos, 1.0);
|
|
973
|
+
return clipPos;
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
// Minimal fragment shader - returns transparent, no color writes (writeMask: 0)
|
|
977
|
+
// Required because render pass has color attachments
|
|
978
|
+
// Depth is still written even though we don't write color
|
|
979
|
+
@fragment fn fs() -> @location(0) vec4f {
|
|
980
|
+
return vec4f(0.0, 0.0, 0.0, 0.0); // Transparent - color writes disabled via writeMask
|
|
981
|
+
}
|
|
982
|
+
`,
|
|
983
|
+
})
|
|
984
|
+
|
|
985
|
+
// Hair depth pre-pass pipeline (depth-only, no color writes)
|
|
986
|
+
// This eliminates most overdraw by rejecting fragments early before expensive shading
|
|
987
|
+
// Note: Must have a color target to match render pass, but we disable all color writes
|
|
988
|
+
this.hairDepthPipeline = this.device.createRenderPipeline({
|
|
989
|
+
label: "hair depth pre-pass",
|
|
990
|
+
layout: sharedPipelineLayout,
|
|
991
|
+
vertex: {
|
|
992
|
+
module: depthOnlyShaderModule,
|
|
993
|
+
buffers: [
|
|
994
|
+
{
|
|
995
|
+
arrayStride: 8 * 4,
|
|
996
|
+
attributes: [
|
|
997
|
+
{ shaderLocation: 0, offset: 0, format: "float32x3" as GPUVertexFormat },
|
|
998
|
+
{ shaderLocation: 1, offset: 3 * 4, format: "float32x3" as GPUVertexFormat },
|
|
999
|
+
],
|
|
1000
|
+
},
|
|
1001
|
+
{
|
|
1002
|
+
arrayStride: 4 * 2,
|
|
1003
|
+
attributes: [{ shaderLocation: 3, offset: 0, format: "uint16x4" as GPUVertexFormat }],
|
|
1004
|
+
},
|
|
1005
|
+
{
|
|
1006
|
+
arrayStride: 4,
|
|
1007
|
+
attributes: [{ shaderLocation: 4, offset: 0, format: "unorm8x4" as GPUVertexFormat }],
|
|
1008
|
+
},
|
|
1009
|
+
],
|
|
1010
|
+
},
|
|
1011
|
+
fragment: {
|
|
1012
|
+
module: depthOnlyShaderModule,
|
|
1013
|
+
entryPoint: "fs",
|
|
1014
|
+
targets: [
|
|
1015
|
+
{
|
|
1016
|
+
format: this.presentationFormat,
|
|
1017
|
+
writeMask: 0, // Disable all color writes - we only care about depth
|
|
1018
|
+
},
|
|
1019
|
+
],
|
|
1020
|
+
},
|
|
1021
|
+
primitive: { cullMode: "none" },
|
|
1022
|
+
depthStencil: {
|
|
1023
|
+
format: "depth24plus-stencil8",
|
|
1024
|
+
depthWriteEnabled: true,
|
|
1025
|
+
depthCompare: "less",
|
|
1026
|
+
},
|
|
1027
|
+
multisample: { count: this.sampleCount },
|
|
1028
|
+
})
|
|
1029
|
+
|
|
1030
|
+
// Unified hair pipeline: single pass with dynamic branching in shader
|
|
1031
|
+
// Uses stencil testing to filter fragments, then shader branches on isOverEyes uniform
|
|
1032
|
+
// This eliminates the need for separate pipelines - same shader, different stencil states
|
|
1033
|
+
// We create two variants: one for over-eyes (stencil == 1) and one for over-non-eyes (stencil != 1)
|
|
1034
|
+
|
|
1035
|
+
// Unified pipeline for hair over eyes (stencil == 1)
|
|
1036
|
+
this.hairUnifiedPipelineOverEyes = this.device.createRenderPipeline({
|
|
1037
|
+
label: "unified hair pipeline (over eyes)",
|
|
1038
|
+
layout: sharedPipelineLayout,
|
|
1039
|
+
vertex: {
|
|
1040
|
+
module: shaderModule,
|
|
1041
|
+
buffers: [
|
|
1042
|
+
{
|
|
1043
|
+
arrayStride: 8 * 4,
|
|
1044
|
+
attributes: [
|
|
1045
|
+
{ shaderLocation: 0, offset: 0, format: "float32x3" as GPUVertexFormat },
|
|
1046
|
+
{ shaderLocation: 1, offset: 3 * 4, format: "float32x3" as GPUVertexFormat },
|
|
1047
|
+
{ shaderLocation: 2, offset: 6 * 4, format: "float32x2" as GPUVertexFormat },
|
|
1048
|
+
],
|
|
1049
|
+
},
|
|
1050
|
+
{
|
|
1051
|
+
arrayStride: 4 * 2,
|
|
1052
|
+
attributes: [{ shaderLocation: 3, offset: 0, format: "uint16x4" as GPUVertexFormat }],
|
|
1053
|
+
},
|
|
1054
|
+
{
|
|
1055
|
+
arrayStride: 4,
|
|
1056
|
+
attributes: [{ shaderLocation: 4, offset: 0, format: "unorm8x4" as GPUVertexFormat }],
|
|
1057
|
+
},
|
|
1058
|
+
],
|
|
1059
|
+
},
|
|
1060
|
+
fragment: {
|
|
1061
|
+
module: shaderModule,
|
|
1062
|
+
targets: [
|
|
1063
|
+
{
|
|
1064
|
+
format: this.presentationFormat,
|
|
1065
|
+
blend: {
|
|
1066
|
+
color: {
|
|
1067
|
+
srcFactor: "src-alpha",
|
|
1068
|
+
dstFactor: "one-minus-src-alpha",
|
|
1069
|
+
operation: "add",
|
|
1070
|
+
},
|
|
1071
|
+
alpha: {
|
|
1072
|
+
srcFactor: "one",
|
|
1073
|
+
dstFactor: "one-minus-src-alpha",
|
|
1074
|
+
operation: "add",
|
|
1075
|
+
},
|
|
1076
|
+
},
|
|
1077
|
+
},
|
|
1078
|
+
],
|
|
1079
|
+
},
|
|
1080
|
+
primitive: { cullMode: "none" },
|
|
1081
|
+
depthStencil: {
|
|
1082
|
+
format: "depth24plus-stencil8",
|
|
1083
|
+
depthWriteEnabled: false, // Don't write depth (already written in pre-pass)
|
|
1084
|
+
depthCompare: "equal", // Only render where depth matches pre-pass
|
|
1085
|
+
stencilFront: {
|
|
1086
|
+
compare: "equal", // Only render where stencil == 1 (over eyes)
|
|
1087
|
+
failOp: "keep",
|
|
1088
|
+
depthFailOp: "keep",
|
|
1089
|
+
passOp: "keep",
|
|
1090
|
+
},
|
|
1091
|
+
stencilBack: {
|
|
1092
|
+
compare: "equal",
|
|
1093
|
+
failOp: "keep",
|
|
1094
|
+
depthFailOp: "keep",
|
|
1095
|
+
passOp: "keep",
|
|
1096
|
+
},
|
|
1097
|
+
},
|
|
1098
|
+
multisample: { count: this.sampleCount },
|
|
1099
|
+
})
|
|
1100
|
+
|
|
1101
|
+
// Unified pipeline for hair over non-eyes (stencil != 1)
|
|
1102
|
+
this.hairUnifiedPipelineOverNonEyes = this.device.createRenderPipeline({
|
|
1103
|
+
label: "unified hair pipeline (over non-eyes)",
|
|
1104
|
+
layout: sharedPipelineLayout,
|
|
1105
|
+
vertex: {
|
|
1106
|
+
module: shaderModule,
|
|
1107
|
+
buffers: [
|
|
1108
|
+
{
|
|
1109
|
+
arrayStride: 8 * 4,
|
|
1110
|
+
attributes: [
|
|
1111
|
+
{ shaderLocation: 0, offset: 0, format: "float32x3" as GPUVertexFormat },
|
|
1112
|
+
{ shaderLocation: 1, offset: 3 * 4, format: "float32x3" as GPUVertexFormat },
|
|
1113
|
+
{ shaderLocation: 2, offset: 6 * 4, format: "float32x2" as GPUVertexFormat },
|
|
1114
|
+
],
|
|
1115
|
+
},
|
|
1116
|
+
{
|
|
1117
|
+
arrayStride: 4 * 2,
|
|
1118
|
+
attributes: [{ shaderLocation: 3, offset: 0, format: "uint16x4" as GPUVertexFormat }],
|
|
1119
|
+
},
|
|
1120
|
+
{
|
|
1121
|
+
arrayStride: 4,
|
|
1122
|
+
attributes: [{ shaderLocation: 4, offset: 0, format: "unorm8x4" as GPUVertexFormat }],
|
|
1123
|
+
},
|
|
1124
|
+
],
|
|
1125
|
+
},
|
|
1126
|
+
fragment: {
|
|
1127
|
+
module: shaderModule,
|
|
1128
|
+
targets: [
|
|
1129
|
+
{
|
|
1130
|
+
format: this.presentationFormat,
|
|
1131
|
+
blend: {
|
|
1132
|
+
color: {
|
|
1133
|
+
srcFactor: "src-alpha",
|
|
1134
|
+
dstFactor: "one-minus-src-alpha",
|
|
1135
|
+
operation: "add",
|
|
1136
|
+
},
|
|
1137
|
+
alpha: {
|
|
1138
|
+
srcFactor: "one",
|
|
1139
|
+
dstFactor: "one-minus-src-alpha",
|
|
1140
|
+
operation: "add",
|
|
1141
|
+
},
|
|
1142
|
+
},
|
|
1143
|
+
},
|
|
1144
|
+
],
|
|
1145
|
+
},
|
|
1146
|
+
primitive: { cullMode: "none" },
|
|
1147
|
+
depthStencil: {
|
|
1148
|
+
format: "depth24plus-stencil8",
|
|
1149
|
+
depthWriteEnabled: false, // Don't write depth (already written in pre-pass)
|
|
1150
|
+
depthCompare: "equal", // Only render where depth matches pre-pass
|
|
1151
|
+
stencilFront: {
|
|
1152
|
+
compare: "not-equal", // Only render where stencil != 1 (over non-eyes)
|
|
1153
|
+
failOp: "keep",
|
|
1154
|
+
depthFailOp: "keep",
|
|
1155
|
+
passOp: "keep",
|
|
1156
|
+
},
|
|
1157
|
+
stencilBack: {
|
|
1158
|
+
compare: "not-equal",
|
|
1159
|
+
failOp: "keep",
|
|
1160
|
+
depthFailOp: "keep",
|
|
1161
|
+
passOp: "keep",
|
|
1162
|
+
},
|
|
1163
|
+
},
|
|
1164
|
+
multisample: { count: this.sampleCount },
|
|
1165
|
+
})
|
|
842
1166
|
}
|
|
843
1167
|
|
|
844
1168
|
// Create compute shader for skin matrix computation
|
|
@@ -1652,7 +1976,7 @@ export class Engine {
|
|
|
1652
1976
|
materialUniformData[4] = this.rimLightColor[0] // rimColor.r
|
|
1653
1977
|
materialUniformData[5] = this.rimLightColor[1] // rimColor.g
|
|
1654
1978
|
materialUniformData[6] = this.rimLightColor[2] // rimColor.b
|
|
1655
|
-
materialUniformData[7] = 0.0 //
|
|
1979
|
+
materialUniformData[7] = 0.0 // isOverEyes: 0.0 for non-hair materials
|
|
1656
1980
|
|
|
1657
1981
|
const materialUniformBuffer = this.device.createBuffer({
|
|
1658
1982
|
label: `material uniform: ${mat.name}`,
|
|
@@ -1686,24 +2010,39 @@ export class Engine {
|
|
|
1686
2010
|
isTransparent,
|
|
1687
2011
|
})
|
|
1688
2012
|
} else if (mat.isHair) {
|
|
1689
|
-
// For hair materials, create
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
2013
|
+
// For hair materials, create a single bind group that will be used with the unified pipeline
|
|
2014
|
+
// The shader will dynamically branch based on isOverEyes uniform
|
|
2015
|
+
// We still need two uniform buffers (one for each render mode) but can reuse the same bind group structure
|
|
2016
|
+
const materialUniformDataHair = new Float32Array(8)
|
|
2017
|
+
materialUniformDataHair[0] = materialAlpha
|
|
2018
|
+
materialUniformDataHair[1] = 1.0 // alphaMultiplier: base value, shader will adjust
|
|
2019
|
+
materialUniformDataHair[2] = this.rimLightIntensity
|
|
2020
|
+
materialUniformDataHair[3] = this.rimLightPower
|
|
2021
|
+
materialUniformDataHair[4] = this.rimLightColor[0] // rimColor.r
|
|
2022
|
+
materialUniformDataHair[5] = this.rimLightColor[1] // rimColor.g
|
|
2023
|
+
materialUniformDataHair[6] = this.rimLightColor[2] // rimColor.b
|
|
2024
|
+
materialUniformDataHair[7] = 0.0 // isOverEyes: will be set per draw call
|
|
2025
|
+
|
|
2026
|
+
// Create uniform buffers for both modes (we'll update them per frame)
|
|
1700
2027
|
const materialUniformBufferOverEyes = this.device.createBuffer({
|
|
1701
2028
|
label: `material uniform (over eyes): ${mat.name}`,
|
|
1702
|
-
size:
|
|
2029
|
+
size: materialUniformDataHair.byteLength,
|
|
1703
2030
|
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
|
|
1704
2031
|
})
|
|
2032
|
+
const materialUniformDataOverEyes = new Float32Array(materialUniformDataHair)
|
|
2033
|
+
materialUniformDataOverEyes[7] = 1.0 // isOverEyes = 1.0
|
|
1705
2034
|
this.device.queue.writeBuffer(materialUniformBufferOverEyes, 0, materialUniformDataOverEyes)
|
|
1706
2035
|
|
|
2036
|
+
const materialUniformBufferOverNonEyes = this.device.createBuffer({
|
|
2037
|
+
label: `material uniform (over non-eyes): ${mat.name}`,
|
|
2038
|
+
size: materialUniformDataHair.byteLength,
|
|
2039
|
+
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
|
|
2040
|
+
})
|
|
2041
|
+
const materialUniformDataOverNonEyes = new Float32Array(materialUniformDataHair)
|
|
2042
|
+
materialUniformDataOverNonEyes[7] = 0.0 // isOverEyes = 0.0
|
|
2043
|
+
this.device.queue.writeBuffer(materialUniformBufferOverNonEyes, 0, materialUniformDataOverNonEyes)
|
|
2044
|
+
|
|
2045
|
+
// Create bind groups for both modes (they share everything except the uniform buffer)
|
|
1707
2046
|
const bindGroupOverEyes = this.device.createBindGroup({
|
|
1708
2047
|
label: `material bind group (over eyes): ${mat.name}`,
|
|
1709
2048
|
layout: this.hairBindGroupLayout,
|
|
@@ -1719,31 +2058,6 @@ export class Engine {
|
|
|
1719
2058
|
],
|
|
1720
2059
|
})
|
|
1721
2060
|
|
|
1722
|
-
this.hairDrawsOverEyes.push({
|
|
1723
|
-
count: matCount,
|
|
1724
|
-
firstIndex: runningFirstIndex,
|
|
1725
|
-
bindGroup: bindGroupOverEyes,
|
|
1726
|
-
isTransparent,
|
|
1727
|
-
})
|
|
1728
|
-
|
|
1729
|
-
// Create material uniform for hair over non-eyes (alphaMultiplier = 1.0)
|
|
1730
|
-
const materialUniformDataOverNonEyes = new Float32Array(8)
|
|
1731
|
-
materialUniformDataOverNonEyes[0] = materialAlpha
|
|
1732
|
-
materialUniformDataOverNonEyes[1] = 1.0 // alphaMultiplier: 1.0 for over-non-eyes
|
|
1733
|
-
materialUniformDataOverNonEyes[2] = this.rimLightIntensity
|
|
1734
|
-
materialUniformDataOverNonEyes[3] = this.rimLightPower
|
|
1735
|
-
materialUniformDataOverNonEyes[4] = this.rimLightColor[0] // rimColor.r
|
|
1736
|
-
materialUniformDataOverNonEyes[5] = this.rimLightColor[1] // rimColor.g
|
|
1737
|
-
materialUniformDataOverNonEyes[6] = this.rimLightColor[2] // rimColor.b
|
|
1738
|
-
materialUniformDataOverNonEyes[7] = 0.0 // _padding1
|
|
1739
|
-
|
|
1740
|
-
const materialUniformBufferOverNonEyes = this.device.createBuffer({
|
|
1741
|
-
label: `material uniform (over non-eyes): ${mat.name}`,
|
|
1742
|
-
size: materialUniformDataOverNonEyes.byteLength,
|
|
1743
|
-
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
|
|
1744
|
-
})
|
|
1745
|
-
this.device.queue.writeBuffer(materialUniformBufferOverNonEyes, 0, materialUniformDataOverNonEyes)
|
|
1746
|
-
|
|
1747
2061
|
const bindGroupOverNonEyes = this.device.createBindGroup({
|
|
1748
2062
|
label: `material bind group (over non-eyes): ${mat.name}`,
|
|
1749
2063
|
layout: this.hairBindGroupLayout,
|
|
@@ -1759,6 +2073,14 @@ export class Engine {
|
|
|
1759
2073
|
],
|
|
1760
2074
|
})
|
|
1761
2075
|
|
|
2076
|
+
// Store both bind groups - we'll use them with the unified pipeline
|
|
2077
|
+
this.hairDrawsOverEyes.push({
|
|
2078
|
+
count: matCount,
|
|
2079
|
+
firstIndex: runningFirstIndex,
|
|
2080
|
+
bindGroup: bindGroupOverEyes,
|
|
2081
|
+
isTransparent,
|
|
2082
|
+
})
|
|
2083
|
+
|
|
1762
2084
|
this.hairDrawsOverNonEyes.push({
|
|
1763
2085
|
count: matCount,
|
|
1764
2086
|
firstIndex: runningFirstIndex,
|
|
@@ -1784,11 +2106,14 @@ export class Engine {
|
|
|
1784
2106
|
// Outline for all materials (including transparent) - Edge flag is at bit 4 (0x10) in PMX format, not bit 0 (0x01)
|
|
1785
2107
|
if ((mat.edgeFlag & 0x10) !== 0 && mat.edgeSize > 0) {
|
|
1786
2108
|
const materialUniformData = new Float32Array(8)
|
|
1787
|
-
materialUniformData[0] = mat.edgeColor[0]
|
|
1788
|
-
materialUniformData[1] = mat.edgeColor[1]
|
|
1789
|
-
materialUniformData[2] = mat.edgeColor[2]
|
|
1790
|
-
materialUniformData[3] = mat.edgeColor[3]
|
|
2109
|
+
materialUniformData[0] = mat.edgeColor[0] // edgeColor.r
|
|
2110
|
+
materialUniformData[1] = mat.edgeColor[1] // edgeColor.g
|
|
2111
|
+
materialUniformData[2] = mat.edgeColor[2] // edgeColor.b
|
|
2112
|
+
materialUniformData[3] = mat.edgeColor[3] // edgeColor.a
|
|
1791
2113
|
materialUniformData[4] = mat.edgeSize
|
|
2114
|
+
materialUniformData[5] = mat.isHair ? 0.0 : 0.0 // isOverEyes: 0.0 for all (unified pipeline doesn't use stencil)
|
|
2115
|
+
materialUniformData[6] = 0.0 // _padding1
|
|
2116
|
+
materialUniformData[7] = 0.0 // _padding2
|
|
1792
2117
|
|
|
1793
2118
|
const materialUniformBuffer = this.device.createBuffer({
|
|
1794
2119
|
label: `outline material uniform: ${mat.name}`,
|
|
@@ -1938,30 +2263,40 @@ export class Engine {
|
|
|
1938
2263
|
}
|
|
1939
2264
|
}
|
|
1940
2265
|
|
|
1941
|
-
// PASS 3: Hair rendering - optimized
|
|
1942
|
-
//
|
|
1943
|
-
//
|
|
2266
|
+
// PASS 3: Hair rendering - optimized with depth pre-pass and unified pipeline
|
|
2267
|
+
// Depth pre-pass: render hair depth-only to eliminate overdraw early
|
|
2268
|
+
// Then render shaded hair once with depth test "equal" to only shade visible fragments
|
|
1944
2269
|
|
|
1945
2270
|
this.drawOutlines(pass, false) // Opaque outlines
|
|
1946
2271
|
|
|
1947
|
-
// 3a: Hair
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
pass.
|
|
2272
|
+
// 3a: Hair depth pre-pass (depth-only, no color writes)
|
|
2273
|
+
// This eliminates most overdraw by rejecting fragments early before expensive shading
|
|
2274
|
+
if (this.hairDrawsOverEyes.length > 0 || this.hairDrawsOverNonEyes.length > 0) {
|
|
2275
|
+
pass.setPipeline(this.hairDepthPipeline)
|
|
2276
|
+
// Render all hair materials for depth (no stencil test needed for depth pass)
|
|
1951
2277
|
for (const draw of this.hairDrawsOverEyes) {
|
|
2278
|
+
if (draw.count > 0) {
|
|
2279
|
+
// Use the same bind group structure (camera, skin matrices) for depth pass
|
|
2280
|
+
pass.setBindGroup(0, draw.bindGroup)
|
|
2281
|
+
pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0)
|
|
2282
|
+
}
|
|
2283
|
+
}
|
|
2284
|
+
for (const draw of this.hairDrawsOverNonEyes) {
|
|
1952
2285
|
if (draw.count > 0) {
|
|
1953
2286
|
pass.setBindGroup(0, draw.bindGroup)
|
|
1954
2287
|
pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0)
|
|
1955
|
-
this.drawCallCount++
|
|
1956
2288
|
}
|
|
1957
2289
|
}
|
|
1958
2290
|
}
|
|
1959
2291
|
|
|
1960
|
-
// 3b: Hair
|
|
1961
|
-
|
|
1962
|
-
|
|
2292
|
+
// 3b: Hair shading pass - unified pipeline with dynamic branching
|
|
2293
|
+
// Uses depth test "equal" to only render where depth was written in pre-pass
|
|
2294
|
+
// Shader branches on isOverEyes uniform to adjust alpha dynamically
|
|
2295
|
+
// This eliminates one full geometry pass compared to the old approach
|
|
2296
|
+
if (this.hairDrawsOverEyes.length > 0) {
|
|
2297
|
+
pass.setPipeline(this.hairUnifiedPipelineOverEyes)
|
|
1963
2298
|
pass.setStencilReference(1)
|
|
1964
|
-
for (const draw of this.
|
|
2299
|
+
for (const draw of this.hairDrawsOverEyes) {
|
|
1965
2300
|
if (draw.count > 0) {
|
|
1966
2301
|
pass.setBindGroup(0, draw.bindGroup)
|
|
1967
2302
|
pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0)
|
|
@@ -1970,21 +2305,24 @@ export class Engine {
|
|
|
1970
2305
|
}
|
|
1971
2306
|
}
|
|
1972
2307
|
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
// Over eyes
|
|
1976
|
-
pass.setPipeline(this.hairOutlineOverEyesPipeline)
|
|
2308
|
+
if (this.hairDrawsOverNonEyes.length > 0) {
|
|
2309
|
+
pass.setPipeline(this.hairUnifiedPipelineOverNonEyes)
|
|
1977
2310
|
pass.setStencilReference(1)
|
|
1978
|
-
for (const draw of this.
|
|
2311
|
+
for (const draw of this.hairDrawsOverNonEyes) {
|
|
1979
2312
|
if (draw.count > 0) {
|
|
1980
2313
|
pass.setBindGroup(0, draw.bindGroup)
|
|
1981
2314
|
pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0)
|
|
2315
|
+
this.drawCallCount++
|
|
1982
2316
|
}
|
|
1983
2317
|
}
|
|
2318
|
+
}
|
|
1984
2319
|
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
2320
|
+
// 3c: Hair outlines - unified single pass without stencil testing
|
|
2321
|
+
// Uses depth test "less-equal" to draw everywhere hair exists
|
|
2322
|
+
// Shader branches on isOverEyes uniform to adjust alpha dynamically (currently always 0.0)
|
|
2323
|
+
// This eliminates the need for two separate outline passes
|
|
2324
|
+
if (this.hairOutlineDraws.length > 0) {
|
|
2325
|
+
pass.setPipeline(this.hairUnifiedOutlinePipeline)
|
|
1988
2326
|
for (const draw of this.hairOutlineDraws) {
|
|
1989
2327
|
if (draw.count > 0) {
|
|
1990
2328
|
pass.setBindGroup(0, draw.bindGroup)
|