reze-engine 0.2.5 → 0.2.7
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/README.md +24 -57
- package/dist/engine.d.ts +13 -14
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +74 -88
- package/package.json +1 -1
- package/src/engine.ts +88 -122
package/README.md
CHANGED
|
@@ -14,37 +14,30 @@ A lightweight engine built with WebGPU and TypeScript for real-time 3D anime cha
|
|
|
14
14
|
- MSAA 4x anti-aliasing
|
|
15
15
|
- GPU-accelerated skinning
|
|
16
16
|
- Bone rotation api
|
|
17
|
+
- VMD animation
|
|
17
18
|
|
|
18
19
|
## Usage
|
|
19
20
|
|
|
20
|
-
```
|
|
21
|
-
export default function
|
|
22
|
-
const canvasRef = useRef<HTMLCanvasElement>
|
|
23
|
-
const engineRef = useRef<Engine
|
|
24
|
-
const [engineError, setEngineError] = useState<string | null>(null)
|
|
25
|
-
const [loading, setLoading] = useState(true)
|
|
26
|
-
const [stats, setStats] = useState<EngineStats>({
|
|
27
|
-
fps: 0,
|
|
28
|
-
frameTime: 0,
|
|
29
|
-
gpuMemory: 0,
|
|
30
|
-
})
|
|
31
|
-
const [progress, setProgress] = useState(0)
|
|
21
|
+
```javascript
|
|
22
|
+
export default function Scene() {
|
|
23
|
+
const canvasRef = useRef < HTMLCanvasElement > null
|
|
24
|
+
const engineRef = useRef < Engine > null
|
|
32
25
|
|
|
33
26
|
const initEngine = useCallback(async () => {
|
|
34
27
|
if (canvasRef.current) {
|
|
35
|
-
// Initialize engine
|
|
36
28
|
try {
|
|
37
|
-
const engine = new Engine(canvasRef.current
|
|
29
|
+
const engine = new Engine(canvasRef.current, {
|
|
30
|
+
ambient: 1.0,
|
|
31
|
+
rimLightIntensity: 0.1,
|
|
32
|
+
bloomIntensity: 0.1,
|
|
33
|
+
})
|
|
38
34
|
engineRef.current = engine
|
|
39
35
|
await engine.init()
|
|
40
36
|
await engine.loadModel("/models/塞尔凯特/塞尔凯特.pmx")
|
|
41
|
-
setLoading(false)
|
|
42
37
|
|
|
43
|
-
engine.runRenderLoop(() => {
|
|
44
|
-
setStats(engine.getStats())
|
|
45
|
-
})
|
|
38
|
+
engine.runRenderLoop(() => {})
|
|
46
39
|
} catch (error) {
|
|
47
|
-
|
|
40
|
+
console.error(error)
|
|
48
41
|
}
|
|
49
42
|
}
|
|
50
43
|
}, [])
|
|
@@ -54,7 +47,6 @@ export default function Home() {
|
|
|
54
47
|
initEngine()
|
|
55
48
|
})()
|
|
56
49
|
|
|
57
|
-
// Cleanup on unmount
|
|
58
50
|
return () => {
|
|
59
51
|
if (engineRef.current) {
|
|
60
52
|
engineRef.current.dispose()
|
|
@@ -62,43 +54,18 @@ export default function Home() {
|
|
|
62
54
|
}
|
|
63
55
|
}, [initEngine])
|
|
64
56
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
setProgress((prev) => {
|
|
69
|
-
if (prev >= 100) {
|
|
70
|
-
return 0
|
|
71
|
-
}
|
|
72
|
-
return prev + 1
|
|
73
|
-
})
|
|
74
|
-
}, 50)
|
|
57
|
+
return <canvas ref={canvasRef} className="w-full h-full" />
|
|
58
|
+
}
|
|
59
|
+
```
|
|
75
60
|
|
|
76
|
-
|
|
77
|
-
}
|
|
78
|
-
}, [loading])
|
|
61
|
+
## Projects Using This Engine
|
|
79
62
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
style={{
|
|
84
|
-
background:
|
|
85
|
-
"radial-gradient(ellipse at center, rgba(35, 35, 45, 0.8) 0%, rgba(35, 35, 45, 0.8) 8%, rgba(8, 8, 12, 0.95) 65%, rgba(0, 0, 0, 1) 100%)",
|
|
86
|
-
}}
|
|
87
|
-
>
|
|
88
|
-
<Header stats={stats} />
|
|
63
|
+
- **[MiKaPo](https://mikapo.vercel.app)** - Online real-time motion capture for MMD using webcam and MediaPipe
|
|
64
|
+
- **[Popo](https://popo.love)** - Fine-tuned LLM that generates MMD poses from natural language descriptions
|
|
65
|
+
- **[MPL](https://mmd-mpl.vercel.app)** - Semantic motion programming language for scripting MMD animations with intuitive syntax
|
|
89
66
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
{loading && !engineError && (
|
|
96
|
-
<div className="absolute inset-0 max-w-xs mx-auto w-full h-full flex items-center justify-center text-white p-6">
|
|
97
|
-
<Progress value={progress} className="rounded-none" />
|
|
98
|
-
</div>
|
|
99
|
-
)}
|
|
100
|
-
<canvas ref={canvasRef} className="absolute inset-0 w-full h-full touch-none z-1" />
|
|
101
|
-
</div>
|
|
102
|
-
)
|
|
103
|
-
}
|
|
104
|
-
```
|
|
67
|
+
## Tutorial
|
|
68
|
+
|
|
69
|
+
Learn WebGPU from scratch by building an anime character renderer in incremental steps. The tutorial covers the complete rendering pipeline from a simple triangle to fully textured, skeletal-animated characters.
|
|
70
|
+
|
|
71
|
+
[How to Render an Anime Character with WebGPU](https://reze.one/tutorial)
|
package/dist/engine.d.ts
CHANGED
|
@@ -25,12 +25,12 @@ export declare class Engine {
|
|
|
25
25
|
private resizeObserver;
|
|
26
26
|
private depthTexture;
|
|
27
27
|
private modelPipeline;
|
|
28
|
-
private
|
|
29
|
-
private hairOutlinePipeline;
|
|
28
|
+
private eyePipeline;
|
|
30
29
|
private hairPipelineOverEyes;
|
|
31
30
|
private hairPipelineOverNonEyes;
|
|
32
31
|
private hairDepthPipeline;
|
|
33
|
-
private
|
|
32
|
+
private outlinePipeline;
|
|
33
|
+
private hairOutlinePipeline;
|
|
34
34
|
private mainBindGroupLayout;
|
|
35
35
|
private outlineBindGroupLayout;
|
|
36
36
|
private jointsBuffer;
|
|
@@ -68,12 +68,20 @@ export declare class Engine {
|
|
|
68
68
|
private bloomThreshold;
|
|
69
69
|
private bloomIntensity;
|
|
70
70
|
private rimLightIntensity;
|
|
71
|
-
private rimLightPower;
|
|
72
71
|
private currentModel;
|
|
73
72
|
private modelDir;
|
|
74
73
|
private physics;
|
|
75
|
-
private
|
|
74
|
+
private materialSampler;
|
|
76
75
|
private textureCache;
|
|
76
|
+
private opaqueDraws;
|
|
77
|
+
private eyeDraws;
|
|
78
|
+
private hairDrawsOverEyes;
|
|
79
|
+
private hairDrawsOverNonEyes;
|
|
80
|
+
private transparentDraws;
|
|
81
|
+
private opaqueOutlineDraws;
|
|
82
|
+
private eyeOutlineDraws;
|
|
83
|
+
private hairOutlineDraws;
|
|
84
|
+
private transparentOutlineDraws;
|
|
77
85
|
private lastFpsUpdate;
|
|
78
86
|
private framesSinceLastUpdate;
|
|
79
87
|
private frameTimeSamples;
|
|
@@ -109,15 +117,6 @@ export declare class Engine {
|
|
|
109
117
|
loadModel(path: string): Promise<void>;
|
|
110
118
|
rotateBones(bones: string[], rotations: Quat[], durationMs?: number): void;
|
|
111
119
|
private setupModelBuffers;
|
|
112
|
-
private opaqueNonEyeNonHairDraws;
|
|
113
|
-
private eyeDraws;
|
|
114
|
-
private hairDrawsOverEyes;
|
|
115
|
-
private hairDrawsOverNonEyes;
|
|
116
|
-
private transparentNonEyeNonHairDraws;
|
|
117
|
-
private opaqueNonEyeNonHairOutlineDraws;
|
|
118
|
-
private eyeOutlineDraws;
|
|
119
|
-
private hairOutlineDraws;
|
|
120
|
-
private transparentNonEyeNonHairOutlineDraws;
|
|
121
120
|
private setupMaterials;
|
|
122
121
|
private createTextureFromPath;
|
|
123
122
|
render(): void;
|
package/dist/engine.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"engine.d.ts","sourceRoot":"","sources":["../src/engine.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,IAAI,EAAQ,MAAM,QAAQ,CAAA;AAMnC,MAAM,MAAM,aAAa,GAAG;IAC1B,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,iBAAiB,CAAC,EAAE,MAAM,CAAA;CAC3B,CAAA;AAED,MAAM,WAAW,WAAW;IAC1B,GAAG,EAAE,MAAM,CAAA;IACX,SAAS,EAAE,MAAM,CAAA;IACjB,SAAS,EAAE,MAAM,CAAA;CAClB;
|
|
1
|
+
{"version":3,"file":"engine.d.ts","sourceRoot":"","sources":["../src/engine.ts"],"names":[],"mappings":"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"}
|
package/dist/engine.js
CHANGED
|
@@ -21,11 +21,20 @@ export class Engine {
|
|
|
21
21
|
this.bloomIntensity = 0.12;
|
|
22
22
|
// Rim light settings
|
|
23
23
|
this.rimLightIntensity = 0.45;
|
|
24
|
-
this.rimLightPower = 2.0;
|
|
25
24
|
this.currentModel = null;
|
|
26
25
|
this.modelDir = "";
|
|
27
26
|
this.physics = null;
|
|
28
27
|
this.textureCache = new Map();
|
|
28
|
+
// Draw lists
|
|
29
|
+
this.opaqueDraws = [];
|
|
30
|
+
this.eyeDraws = [];
|
|
31
|
+
this.hairDrawsOverEyes = [];
|
|
32
|
+
this.hairDrawsOverNonEyes = [];
|
|
33
|
+
this.transparentDraws = [];
|
|
34
|
+
this.opaqueOutlineDraws = [];
|
|
35
|
+
this.eyeOutlineDraws = [];
|
|
36
|
+
this.hairOutlineDraws = [];
|
|
37
|
+
this.transparentOutlineDraws = [];
|
|
29
38
|
this.lastFpsUpdate = performance.now();
|
|
30
39
|
this.framesSinceLastUpdate = 0;
|
|
31
40
|
this.frameTimeSamples = [];
|
|
@@ -42,15 +51,6 @@ export class Engine {
|
|
|
42
51
|
this.animationFrames = [];
|
|
43
52
|
this.animationTimeouts = [];
|
|
44
53
|
this.gpuMemoryMB = 0;
|
|
45
|
-
this.opaqueNonEyeNonHairDraws = [];
|
|
46
|
-
this.eyeDraws = [];
|
|
47
|
-
this.hairDrawsOverEyes = [];
|
|
48
|
-
this.hairDrawsOverNonEyes = [];
|
|
49
|
-
this.transparentNonEyeNonHairDraws = [];
|
|
50
|
-
this.opaqueNonEyeNonHairOutlineDraws = [];
|
|
51
|
-
this.eyeOutlineDraws = [];
|
|
52
|
-
this.hairOutlineDraws = [];
|
|
53
|
-
this.transparentNonEyeNonHairOutlineDraws = [];
|
|
54
54
|
this.canvas = canvas;
|
|
55
55
|
if (options) {
|
|
56
56
|
this.ambient = options.ambient ?? 1.0;
|
|
@@ -84,9 +84,8 @@ export class Engine {
|
|
|
84
84
|
this.createBloomPipelines();
|
|
85
85
|
this.setupResize();
|
|
86
86
|
}
|
|
87
|
-
// Step 2: Create shaders and render pipelines
|
|
88
87
|
createPipelines() {
|
|
89
|
-
this.
|
|
88
|
+
this.materialSampler = this.device.createSampler({
|
|
90
89
|
magFilter: "linear",
|
|
91
90
|
minFilter: "linear",
|
|
92
91
|
addressModeU: "repeat",
|
|
@@ -121,7 +120,7 @@ export class Engine {
|
|
|
121
120
|
alpha: f32,
|
|
122
121
|
alphaMultiplier: f32,
|
|
123
122
|
rimIntensity: f32,
|
|
124
|
-
|
|
123
|
+
_padding1: f32,
|
|
125
124
|
rimColor: vec3f,
|
|
126
125
|
isOverEyes: f32, // 1.0 if rendering over eyes, 0.0 otherwise
|
|
127
126
|
};
|
|
@@ -152,14 +151,10 @@ export class Engine {
|
|
|
152
151
|
var output: VertexOutput;
|
|
153
152
|
let pos4 = vec4f(position, 1.0);
|
|
154
153
|
|
|
155
|
-
//
|
|
154
|
+
// Branchless weight normalization (avoids GPU branch divergence)
|
|
156
155
|
let weightSum = weights0.x + weights0.y + weights0.z + weights0.w;
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
normalizedWeights = weights0 / weightSum;
|
|
160
|
-
} else {
|
|
161
|
-
normalizedWeights = vec4f(1.0, 0.0, 0.0, 0.0);
|
|
162
|
-
}
|
|
156
|
+
let invWeightSum = select(1.0, 1.0 / weightSum, weightSum > 0.0001);
|
|
157
|
+
let normalizedWeights = select(vec4f(1.0, 0.0, 0.0, 0.0), weights0 * invWeightSum, weightSum > 0.0001);
|
|
163
158
|
|
|
164
159
|
var skinnedPos = vec4f(0.0, 0.0, 0.0, 0.0);
|
|
165
160
|
var skinnedNrm = vec3f(0.0, 0.0, 0.0);
|
|
@@ -180,6 +175,15 @@ export class Engine {
|
|
|
180
175
|
}
|
|
181
176
|
|
|
182
177
|
@fragment fn fs(input: VertexOutput) -> @location(0) vec4f {
|
|
178
|
+
// Early alpha test - discard before expensive calculations
|
|
179
|
+
var finalAlpha = material.alpha * material.alphaMultiplier;
|
|
180
|
+
if (material.isOverEyes > 0.5) {
|
|
181
|
+
finalAlpha *= 0.5; // Hair over eyes gets 50% alpha
|
|
182
|
+
}
|
|
183
|
+
if (finalAlpha < 0.001) {
|
|
184
|
+
discard;
|
|
185
|
+
}
|
|
186
|
+
|
|
183
187
|
let n = normalize(input.normal);
|
|
184
188
|
let albedo = textureSample(diffuseTexture, diffuseSampler, input.uv).rgb;
|
|
185
189
|
|
|
@@ -197,21 +201,12 @@ export class Engine {
|
|
|
197
201
|
// Rim light calculation
|
|
198
202
|
let viewDir = normalize(camera.viewPos - input.worldPos);
|
|
199
203
|
var rimFactor = 1.0 - max(dot(n, viewDir), 0.0);
|
|
200
|
-
rimFactor = pow(
|
|
204
|
+
rimFactor = rimFactor * rimFactor; // Optimized: direct multiply instead of pow(x, 2.0)
|
|
201
205
|
let rimLight = material.rimColor * material.rimIntensity * rimFactor;
|
|
202
206
|
|
|
203
207
|
let color = albedo * lightAccum + rimLight;
|
|
204
208
|
|
|
205
|
-
|
|
206
|
-
if (material.isOverEyes > 0.5) {
|
|
207
|
-
finalAlpha *= 0.5; // Hair over eyes gets 50% alpha
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
if (finalAlpha < 0.001) {
|
|
211
|
-
discard;
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
return vec4f(clamp(color, vec3f(0.0), vec3f(1.0)), finalAlpha);
|
|
209
|
+
return vec4f(color, finalAlpha);
|
|
215
210
|
}
|
|
216
211
|
`,
|
|
217
212
|
});
|
|
@@ -335,14 +330,10 @@ export class Engine {
|
|
|
335
330
|
var output: VertexOutput;
|
|
336
331
|
let pos4 = vec4f(position, 1.0);
|
|
337
332
|
|
|
338
|
-
//
|
|
333
|
+
// Branchless weight normalization (avoids GPU branch divergence)
|
|
339
334
|
let weightSum = weights0.x + weights0.y + weights0.z + weights0.w;
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
normalizedWeights = weights0 / weightSum;
|
|
343
|
-
} else {
|
|
344
|
-
normalizedWeights = vec4f(1.0, 0.0, 0.0, 0.0);
|
|
345
|
-
}
|
|
335
|
+
let invWeightSum = select(1.0, 1.0 / weightSum, weightSum > 0.0001);
|
|
336
|
+
let normalizedWeights = select(vec4f(1.0, 0.0, 0.0, 0.0), weights0 * invWeightSum, weightSum > 0.0001);
|
|
346
337
|
|
|
347
338
|
var skinnedPos = vec4f(0.0, 0.0, 0.0, 0.0);
|
|
348
339
|
var skinnedNrm = vec3f(0.0, 0.0, 0.0);
|
|
@@ -553,8 +544,11 @@ export class Engine {
|
|
|
553
544
|
primitive: { cullMode: "none" },
|
|
554
545
|
depthStencil: {
|
|
555
546
|
format: "depth24plus-stencil8",
|
|
556
|
-
depthWriteEnabled:
|
|
557
|
-
depthCompare: "less", // Respect existing depth
|
|
547
|
+
depthWriteEnabled: true, // Write depth to occlude back of head
|
|
548
|
+
depthCompare: "less", // Respect existing depth (face)
|
|
549
|
+
depthBias: -0.0001, // Small negative bias to bring eyes slightly forward
|
|
550
|
+
depthBiasSlopeScale: 0.0,
|
|
551
|
+
depthBiasClamp: 0.0,
|
|
558
552
|
stencilFront: {
|
|
559
553
|
compare: "always",
|
|
560
554
|
failOp: "keep",
|
|
@@ -592,14 +586,10 @@ export class Engine {
|
|
|
592
586
|
) -> @builtin(position) vec4f {
|
|
593
587
|
let pos4 = vec4f(position, 1.0);
|
|
594
588
|
|
|
595
|
-
//
|
|
589
|
+
// Branchless weight normalization (avoids GPU branch divergence)
|
|
596
590
|
let weightSum = weights0.x + weights0.y + weights0.z + weights0.w;
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
normalizedWeights = weights0 / weightSum;
|
|
600
|
-
} else {
|
|
601
|
-
normalizedWeights = vec4f(1.0, 0.0, 0.0, 0.0);
|
|
602
|
-
}
|
|
591
|
+
let invWeightSum = select(1.0, 1.0 / weightSum, weightSum > 0.0001);
|
|
592
|
+
let normalizedWeights = select(vec4f(1.0, 0.0, 0.0, 0.0), weights0 * invWeightSum, weightSum > 0.0001);
|
|
603
593
|
|
|
604
594
|
var skinnedPos = vec4f(0.0, 0.0, 0.0, 0.0);
|
|
605
595
|
for (var i = 0u; i < 4u; i++) {
|
|
@@ -944,19 +934,21 @@ export class Engine {
|
|
|
944
934
|
@group(0) @binding(1) var inputSampler: sampler;
|
|
945
935
|
@group(0) @binding(2) var<uniform> blurUniforms: BlurUniforms;
|
|
946
936
|
|
|
947
|
-
//
|
|
937
|
+
// 3-tap gaussian blur using bilinear filtering trick (40% fewer texture fetches!)
|
|
948
938
|
@fragment fn fs(input: VertexOutput) -> @location(0) vec4f {
|
|
949
939
|
let texelSize = 1.0 / vec2f(textureDimensions(inputTexture));
|
|
950
|
-
var result = vec4f(0.0);
|
|
951
940
|
|
|
952
|
-
//
|
|
953
|
-
|
|
954
|
-
|
|
941
|
+
// Bilinear optimization: leverage hardware filtering to sample between pixels
|
|
942
|
+
// Original 5-tap: weights [0.06136, 0.24477, 0.38774, 0.24477, 0.06136] at offsets [-2, -1, 0, 1, 2]
|
|
943
|
+
// Optimized 3-tap: combine adjacent samples using weighted offsets
|
|
944
|
+
let weight0 = 0.38774; // Center sample
|
|
945
|
+
let weight1 = 0.24477 + 0.06136; // Combined outer samples = 0.30613
|
|
946
|
+
let offset1 = (0.24477 * 1.0 + 0.06136 * 2.0) / weight1; // Weighted position = 1.2
|
|
955
947
|
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
948
|
+
var result = textureSample(inputTexture, inputSampler, input.uv) * weight0;
|
|
949
|
+
let offsetVec = offset1 * texelSize * blurUniforms.direction;
|
|
950
|
+
result += textureSample(inputTexture, inputSampler, input.uv + offsetVec) * weight1;
|
|
951
|
+
result += textureSample(inputTexture, inputSampler, input.uv - offsetVec) * weight1;
|
|
960
952
|
|
|
961
953
|
return result;
|
|
962
954
|
}
|
|
@@ -1492,7 +1484,6 @@ export class Engine {
|
|
|
1492
1484
|
}
|
|
1493
1485
|
await this.setupMaterials(model);
|
|
1494
1486
|
}
|
|
1495
|
-
// Step 8: Load textures and create material bind groups
|
|
1496
1487
|
async setupMaterials(model) {
|
|
1497
1488
|
const materials = model.getMaterials();
|
|
1498
1489
|
if (materials.length === 0) {
|
|
@@ -1539,15 +1530,15 @@ export class Engine {
|
|
|
1539
1530
|
this.textureCache.set(defaultToonPath, defaultToonTexture);
|
|
1540
1531
|
return defaultToonTexture;
|
|
1541
1532
|
};
|
|
1542
|
-
this.
|
|
1533
|
+
this.opaqueDraws = [];
|
|
1543
1534
|
this.eyeDraws = [];
|
|
1544
1535
|
this.hairDrawsOverEyes = [];
|
|
1545
1536
|
this.hairDrawsOverNonEyes = [];
|
|
1546
|
-
this.
|
|
1547
|
-
this.
|
|
1537
|
+
this.transparentDraws = [];
|
|
1538
|
+
this.opaqueOutlineDraws = [];
|
|
1548
1539
|
this.eyeOutlineDraws = [];
|
|
1549
1540
|
this.hairOutlineDraws = [];
|
|
1550
|
-
this.
|
|
1541
|
+
this.transparentOutlineDraws = [];
|
|
1551
1542
|
let currentIndexOffset = 0;
|
|
1552
1543
|
for (const mat of materials) {
|
|
1553
1544
|
const indexCount = mat.vertexCount;
|
|
@@ -1565,11 +1556,11 @@ export class Engine {
|
|
|
1565
1556
|
materialUniformData[0] = materialAlpha;
|
|
1566
1557
|
materialUniformData[1] = 1.0; // alphaMultiplier: 1.0 for non-hair materials
|
|
1567
1558
|
materialUniformData[2] = this.rimLightIntensity;
|
|
1568
|
-
materialUniformData[3] =
|
|
1559
|
+
materialUniformData[3] = 0.0; // _padding1
|
|
1569
1560
|
materialUniformData[4] = 1.0; // rimColor.r
|
|
1570
1561
|
materialUniformData[5] = 1.0; // rimColor.g
|
|
1571
1562
|
materialUniformData[6] = 1.0; // rimColor.b
|
|
1572
|
-
materialUniformData[7] = 0.0;
|
|
1563
|
+
materialUniformData[7] = 0.0; // isOverEyes
|
|
1573
1564
|
const materialUniformBuffer = this.device.createBuffer({
|
|
1574
1565
|
label: `material uniform: ${mat.name}`,
|
|
1575
1566
|
size: materialUniformData.byteLength,
|
|
@@ -1584,14 +1575,13 @@ export class Engine {
|
|
|
1584
1575
|
{ binding: 0, resource: { buffer: this.cameraUniformBuffer } },
|
|
1585
1576
|
{ binding: 1, resource: { buffer: this.lightUniformBuffer } },
|
|
1586
1577
|
{ binding: 2, resource: diffuseTexture.createView() },
|
|
1587
|
-
{ binding: 3, resource: this.
|
|
1578
|
+
{ binding: 3, resource: this.materialSampler },
|
|
1588
1579
|
{ binding: 4, resource: { buffer: this.skinMatrixBuffer } },
|
|
1589
1580
|
{ binding: 5, resource: toonTexture.createView() },
|
|
1590
|
-
{ binding: 6, resource: this.
|
|
1581
|
+
{ binding: 6, resource: this.materialSampler },
|
|
1591
1582
|
{ binding: 7, resource: { buffer: materialUniformBuffer } },
|
|
1592
1583
|
],
|
|
1593
1584
|
});
|
|
1594
|
-
// Classify materials into appropriate draw lists
|
|
1595
1585
|
if (mat.isEye) {
|
|
1596
1586
|
this.eyeDraws.push({
|
|
1597
1587
|
count: indexCount,
|
|
@@ -1607,11 +1597,11 @@ export class Engine {
|
|
|
1607
1597
|
uniformData[0] = materialAlpha;
|
|
1608
1598
|
uniformData[1] = 1.0; // alphaMultiplier (shader adjusts based on isOverEyes)
|
|
1609
1599
|
uniformData[2] = this.rimLightIntensity;
|
|
1610
|
-
uniformData[3] =
|
|
1600
|
+
uniformData[3] = 0.0; // _padding1
|
|
1611
1601
|
uniformData[4] = 1.0; // rimColor.rgb
|
|
1612
1602
|
uniformData[5] = 1.0;
|
|
1613
1603
|
uniformData[6] = 1.0;
|
|
1614
|
-
uniformData[7] = isOverEyes ? 1.0 : 0.0;
|
|
1604
|
+
uniformData[7] = isOverEyes ? 1.0 : 0.0; // isOverEyes
|
|
1615
1605
|
const buffer = this.device.createBuffer({
|
|
1616
1606
|
label: `material uniform (${isOverEyes ? "over eyes" : "over non-eyes"}): ${mat.name}`,
|
|
1617
1607
|
size: uniformData.byteLength,
|
|
@@ -1625,10 +1615,10 @@ export class Engine {
|
|
|
1625
1615
|
{ binding: 0, resource: { buffer: this.cameraUniformBuffer } },
|
|
1626
1616
|
{ binding: 1, resource: { buffer: this.lightUniformBuffer } },
|
|
1627
1617
|
{ binding: 2, resource: diffuseTexture.createView() },
|
|
1628
|
-
{ binding: 3, resource: this.
|
|
1618
|
+
{ binding: 3, resource: this.materialSampler },
|
|
1629
1619
|
{ binding: 4, resource: { buffer: this.skinMatrixBuffer } },
|
|
1630
1620
|
{ binding: 5, resource: toonTexture.createView() },
|
|
1631
|
-
{ binding: 6, resource: this.
|
|
1621
|
+
{ binding: 6, resource: this.materialSampler },
|
|
1632
1622
|
{ binding: 7, resource: { buffer: buffer } },
|
|
1633
1623
|
],
|
|
1634
1624
|
});
|
|
@@ -1649,7 +1639,7 @@ export class Engine {
|
|
|
1649
1639
|
});
|
|
1650
1640
|
}
|
|
1651
1641
|
else if (isTransparent) {
|
|
1652
|
-
this.
|
|
1642
|
+
this.transparentDraws.push({
|
|
1653
1643
|
count: indexCount,
|
|
1654
1644
|
firstIndex: currentIndexOffset,
|
|
1655
1645
|
bindGroup,
|
|
@@ -1657,7 +1647,7 @@ export class Engine {
|
|
|
1657
1647
|
});
|
|
1658
1648
|
}
|
|
1659
1649
|
else {
|
|
1660
|
-
this.
|
|
1650
|
+
this.opaqueDraws.push({
|
|
1661
1651
|
count: indexCount,
|
|
1662
1652
|
firstIndex: currentIndexOffset,
|
|
1663
1653
|
bindGroup,
|
|
@@ -1707,7 +1697,7 @@ export class Engine {
|
|
|
1707
1697
|
});
|
|
1708
1698
|
}
|
|
1709
1699
|
else if (isTransparent) {
|
|
1710
|
-
this.
|
|
1700
|
+
this.transparentOutlineDraws.push({
|
|
1711
1701
|
count: indexCount,
|
|
1712
1702
|
firstIndex: currentIndexOffset,
|
|
1713
1703
|
bindGroup: outlineBindGroup,
|
|
@@ -1715,7 +1705,7 @@ export class Engine {
|
|
|
1715
1705
|
});
|
|
1716
1706
|
}
|
|
1717
1707
|
else {
|
|
1718
|
-
this.
|
|
1708
|
+
this.opaqueOutlineDraws.push({
|
|
1719
1709
|
count: indexCount,
|
|
1720
1710
|
firstIndex: currentIndexOffset,
|
|
1721
1711
|
bindGroup: outlineBindGroup,
|
|
@@ -1775,9 +1765,9 @@ export class Engine {
|
|
|
1775
1765
|
pass.setVertexBuffer(2, this.weightsBuffer);
|
|
1776
1766
|
pass.setIndexBuffer(this.indexBuffer, "uint32");
|
|
1777
1767
|
this.drawCallCount = 0;
|
|
1778
|
-
// Pass 1: Opaque
|
|
1768
|
+
// Pass 1: Opaque
|
|
1779
1769
|
pass.setPipeline(this.modelPipeline);
|
|
1780
|
-
for (const draw of this.
|
|
1770
|
+
for (const draw of this.opaqueDraws) {
|
|
1781
1771
|
if (draw.count > 0) {
|
|
1782
1772
|
pass.setBindGroup(0, draw.bindGroup);
|
|
1783
1773
|
pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0);
|
|
@@ -1845,9 +1835,9 @@ export class Engine {
|
|
|
1845
1835
|
}
|
|
1846
1836
|
}
|
|
1847
1837
|
}
|
|
1848
|
-
// Pass 4: Transparent
|
|
1838
|
+
// Pass 4: Transparent
|
|
1849
1839
|
pass.setPipeline(this.modelPipeline);
|
|
1850
|
-
for (const draw of this.
|
|
1840
|
+
for (const draw of this.transparentDraws) {
|
|
1851
1841
|
if (draw.count > 0) {
|
|
1852
1842
|
pass.setBindGroup(0, draw.bindGroup);
|
|
1853
1843
|
pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0);
|
|
@@ -1873,10 +1863,6 @@ export class Engine {
|
|
|
1873
1863
|
intensityData[0] = this.bloomIntensity;
|
|
1874
1864
|
this.device.queue.writeBuffer(this.bloomIntensityBuffer, 0, intensityData);
|
|
1875
1865
|
const encoder = this.device.createCommandEncoder();
|
|
1876
|
-
const width = this.canvas.width;
|
|
1877
|
-
const height = this.canvas.height;
|
|
1878
|
-
const bloomWidth = Math.floor(width / this.BLOOM_DOWNSCALE_FACTOR);
|
|
1879
|
-
const bloomHeight = Math.floor(height / this.BLOOM_DOWNSCALE_FACTOR);
|
|
1880
1866
|
// Extract bright areas
|
|
1881
1867
|
const extractPass = encoder.beginRenderPass({
|
|
1882
1868
|
label: "bloom extract",
|
|
@@ -1992,7 +1978,7 @@ export class Engine {
|
|
|
1992
1978
|
drawOutlines(pass, transparent) {
|
|
1993
1979
|
pass.setPipeline(this.outlinePipeline);
|
|
1994
1980
|
if (transparent) {
|
|
1995
|
-
for (const draw of this.
|
|
1981
|
+
for (const draw of this.transparentOutlineDraws) {
|
|
1996
1982
|
if (draw.count > 0) {
|
|
1997
1983
|
pass.setBindGroup(0, draw.bindGroup);
|
|
1998
1984
|
pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0);
|
|
@@ -2000,7 +1986,7 @@ export class Engine {
|
|
|
2000
1986
|
}
|
|
2001
1987
|
}
|
|
2002
1988
|
else {
|
|
2003
|
-
for (const draw of this.
|
|
1989
|
+
for (const draw of this.opaqueOutlineDraws) {
|
|
2004
1990
|
if (draw.count > 0) {
|
|
2005
1991
|
pass.setBindGroup(0, draw.bindGroup);
|
|
2006
1992
|
pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0);
|
|
@@ -2078,16 +2064,16 @@ export class Engine {
|
|
|
2078
2064
|
if (this.fullscreenQuadBuffer) {
|
|
2079
2065
|
bufferMemoryBytes += 24 * 4;
|
|
2080
2066
|
}
|
|
2081
|
-
const totalMaterialDraws = this.
|
|
2067
|
+
const totalMaterialDraws = this.opaqueDraws.length +
|
|
2082
2068
|
this.eyeDraws.length +
|
|
2083
2069
|
this.hairDrawsOverEyes.length +
|
|
2084
2070
|
this.hairDrawsOverNonEyes.length +
|
|
2085
|
-
this.
|
|
2071
|
+
this.transparentDraws.length;
|
|
2086
2072
|
bufferMemoryBytes += totalMaterialDraws * 32;
|
|
2087
|
-
const totalOutlineDraws = this.
|
|
2073
|
+
const totalOutlineDraws = this.opaqueOutlineDraws.length +
|
|
2088
2074
|
this.eyeOutlineDraws.length +
|
|
2089
2075
|
this.hairOutlineDraws.length +
|
|
2090
|
-
this.
|
|
2076
|
+
this.transparentOutlineDraws.length;
|
|
2091
2077
|
bufferMemoryBytes += totalOutlineDraws * 32;
|
|
2092
2078
|
let renderTargetMemoryBytes = 0;
|
|
2093
2079
|
if (this.multisampleTexture) {
|
package/package.json
CHANGED
package/src/engine.ts
CHANGED
|
@@ -17,7 +17,13 @@ export interface EngineStats {
|
|
|
17
17
|
gpuMemory: number // MB (estimated total GPU memory)
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
-
|
|
20
|
+
interface DrawCall {
|
|
21
|
+
count: number
|
|
22
|
+
firstIndex: number
|
|
23
|
+
bindGroup: GPUBindGroup
|
|
24
|
+
isTransparent: boolean
|
|
25
|
+
}
|
|
26
|
+
|
|
21
27
|
type BoneKeyFrame = {
|
|
22
28
|
boneName: string
|
|
23
29
|
time: number
|
|
@@ -39,13 +45,15 @@ export class Engine {
|
|
|
39
45
|
private indexBuffer?: GPUBuffer
|
|
40
46
|
private resizeObserver: ResizeObserver | null = null
|
|
41
47
|
private depthTexture!: GPUTexture
|
|
48
|
+
// Material rendering pipelines
|
|
42
49
|
private modelPipeline!: GPURenderPipeline
|
|
43
|
-
private
|
|
44
|
-
private hairOutlinePipeline!: GPURenderPipeline
|
|
50
|
+
private eyePipeline!: GPURenderPipeline
|
|
45
51
|
private hairPipelineOverEyes!: GPURenderPipeline
|
|
46
52
|
private hairPipelineOverNonEyes!: GPURenderPipeline
|
|
47
53
|
private hairDepthPipeline!: GPURenderPipeline
|
|
48
|
-
|
|
54
|
+
// Outline pipelines
|
|
55
|
+
private outlinePipeline!: GPURenderPipeline
|
|
56
|
+
private hairOutlinePipeline!: GPURenderPipeline
|
|
49
57
|
private mainBindGroupLayout!: GPUBindGroupLayout
|
|
50
58
|
private outlineBindGroupLayout!: GPUBindGroupLayout
|
|
51
59
|
private jointsBuffer!: GPUBuffer
|
|
@@ -71,7 +79,7 @@ export class Engine {
|
|
|
71
79
|
private bloomExtractTexture!: GPUTexture
|
|
72
80
|
private bloomBlurTexture1!: GPUTexture
|
|
73
81
|
private bloomBlurTexture2!: GPUTexture
|
|
74
|
-
//
|
|
82
|
+
// Post-processing pipelines
|
|
75
83
|
private bloomExtractPipeline!: GPURenderPipeline
|
|
76
84
|
private bloomBlurPipeline!: GPURenderPipeline
|
|
77
85
|
private bloomComposePipeline!: GPURenderPipeline
|
|
@@ -91,13 +99,22 @@ export class Engine {
|
|
|
91
99
|
private bloomIntensity: number = 0.12
|
|
92
100
|
// Rim light settings
|
|
93
101
|
private rimLightIntensity: number = 0.45
|
|
94
|
-
private rimLightPower: number = 2.0
|
|
95
102
|
|
|
96
103
|
private currentModel: Model | null = null
|
|
97
104
|
private modelDir: string = ""
|
|
98
105
|
private physics: Physics | null = null
|
|
99
|
-
private
|
|
106
|
+
private materialSampler!: GPUSampler
|
|
100
107
|
private textureCache = new Map<string, GPUTexture>()
|
|
108
|
+
// Draw lists
|
|
109
|
+
private opaqueDraws: DrawCall[] = []
|
|
110
|
+
private eyeDraws: DrawCall[] = []
|
|
111
|
+
private hairDrawsOverEyes: DrawCall[] = []
|
|
112
|
+
private hairDrawsOverNonEyes: DrawCall[] = []
|
|
113
|
+
private transparentDraws: DrawCall[] = []
|
|
114
|
+
private opaqueOutlineDraws: DrawCall[] = []
|
|
115
|
+
private eyeOutlineDraws: DrawCall[] = []
|
|
116
|
+
private hairOutlineDraws: DrawCall[] = []
|
|
117
|
+
private transparentOutlineDraws: DrawCall[] = []
|
|
101
118
|
|
|
102
119
|
private lastFpsUpdate = performance.now()
|
|
103
120
|
private framesSinceLastUpdate = 0
|
|
@@ -157,9 +174,8 @@ export class Engine {
|
|
|
157
174
|
this.setupResize()
|
|
158
175
|
}
|
|
159
176
|
|
|
160
|
-
// Step 2: Create shaders and render pipelines
|
|
161
177
|
private createPipelines() {
|
|
162
|
-
this.
|
|
178
|
+
this.materialSampler = this.device.createSampler({
|
|
163
179
|
magFilter: "linear",
|
|
164
180
|
minFilter: "linear",
|
|
165
181
|
addressModeU: "repeat",
|
|
@@ -195,7 +211,7 @@ export class Engine {
|
|
|
195
211
|
alpha: f32,
|
|
196
212
|
alphaMultiplier: f32,
|
|
197
213
|
rimIntensity: f32,
|
|
198
|
-
|
|
214
|
+
_padding1: f32,
|
|
199
215
|
rimColor: vec3f,
|
|
200
216
|
isOverEyes: f32, // 1.0 if rendering over eyes, 0.0 otherwise
|
|
201
217
|
};
|
|
@@ -226,14 +242,10 @@ export class Engine {
|
|
|
226
242
|
var output: VertexOutput;
|
|
227
243
|
let pos4 = vec4f(position, 1.0);
|
|
228
244
|
|
|
229
|
-
//
|
|
245
|
+
// Branchless weight normalization (avoids GPU branch divergence)
|
|
230
246
|
let weightSum = weights0.x + weights0.y + weights0.z + weights0.w;
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
normalizedWeights = weights0 / weightSum;
|
|
234
|
-
} else {
|
|
235
|
-
normalizedWeights = vec4f(1.0, 0.0, 0.0, 0.0);
|
|
236
|
-
}
|
|
247
|
+
let invWeightSum = select(1.0, 1.0 / weightSum, weightSum > 0.0001);
|
|
248
|
+
let normalizedWeights = select(vec4f(1.0, 0.0, 0.0, 0.0), weights0 * invWeightSum, weightSum > 0.0001);
|
|
237
249
|
|
|
238
250
|
var skinnedPos = vec4f(0.0, 0.0, 0.0, 0.0);
|
|
239
251
|
var skinnedNrm = vec3f(0.0, 0.0, 0.0);
|
|
@@ -254,6 +266,15 @@ export class Engine {
|
|
|
254
266
|
}
|
|
255
267
|
|
|
256
268
|
@fragment fn fs(input: VertexOutput) -> @location(0) vec4f {
|
|
269
|
+
// Early alpha test - discard before expensive calculations
|
|
270
|
+
var finalAlpha = material.alpha * material.alphaMultiplier;
|
|
271
|
+
if (material.isOverEyes > 0.5) {
|
|
272
|
+
finalAlpha *= 0.5; // Hair over eyes gets 50% alpha
|
|
273
|
+
}
|
|
274
|
+
if (finalAlpha < 0.001) {
|
|
275
|
+
discard;
|
|
276
|
+
}
|
|
277
|
+
|
|
257
278
|
let n = normalize(input.normal);
|
|
258
279
|
let albedo = textureSample(diffuseTexture, diffuseSampler, input.uv).rgb;
|
|
259
280
|
|
|
@@ -271,21 +292,12 @@ export class Engine {
|
|
|
271
292
|
// Rim light calculation
|
|
272
293
|
let viewDir = normalize(camera.viewPos - input.worldPos);
|
|
273
294
|
var rimFactor = 1.0 - max(dot(n, viewDir), 0.0);
|
|
274
|
-
rimFactor = pow(
|
|
295
|
+
rimFactor = rimFactor * rimFactor; // Optimized: direct multiply instead of pow(x, 2.0)
|
|
275
296
|
let rimLight = material.rimColor * material.rimIntensity * rimFactor;
|
|
276
297
|
|
|
277
298
|
let color = albedo * lightAccum + rimLight;
|
|
278
299
|
|
|
279
|
-
|
|
280
|
-
if (material.isOverEyes > 0.5) {
|
|
281
|
-
finalAlpha *= 0.5; // Hair over eyes gets 50% alpha
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
if (finalAlpha < 0.001) {
|
|
285
|
-
discard;
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
return vec4f(clamp(color, vec3f(0.0), vec3f(1.0)), finalAlpha);
|
|
300
|
+
return vec4f(color, finalAlpha);
|
|
289
301
|
}
|
|
290
302
|
`,
|
|
291
303
|
})
|
|
@@ -415,14 +427,10 @@ export class Engine {
|
|
|
415
427
|
var output: VertexOutput;
|
|
416
428
|
let pos4 = vec4f(position, 1.0);
|
|
417
429
|
|
|
418
|
-
//
|
|
430
|
+
// Branchless weight normalization (avoids GPU branch divergence)
|
|
419
431
|
let weightSum = weights0.x + weights0.y + weights0.z + weights0.w;
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
normalizedWeights = weights0 / weightSum;
|
|
423
|
-
} else {
|
|
424
|
-
normalizedWeights = vec4f(1.0, 0.0, 0.0, 0.0);
|
|
425
|
-
}
|
|
432
|
+
let invWeightSum = select(1.0, 1.0 / weightSum, weightSum > 0.0001);
|
|
433
|
+
let normalizedWeights = select(vec4f(1.0, 0.0, 0.0, 0.0), weights0 * invWeightSum, weightSum > 0.0001);
|
|
426
434
|
|
|
427
435
|
var skinnedPos = vec4f(0.0, 0.0, 0.0, 0.0);
|
|
428
436
|
var skinnedNrm = vec3f(0.0, 0.0, 0.0);
|
|
@@ -636,8 +644,11 @@ export class Engine {
|
|
|
636
644
|
primitive: { cullMode: "none" },
|
|
637
645
|
depthStencil: {
|
|
638
646
|
format: "depth24plus-stencil8",
|
|
639
|
-
depthWriteEnabled:
|
|
640
|
-
depthCompare: "less", // Respect existing depth
|
|
647
|
+
depthWriteEnabled: true, // Write depth to occlude back of head
|
|
648
|
+
depthCompare: "less", // Respect existing depth (face)
|
|
649
|
+
depthBias: -0.0001, // Small negative bias to bring eyes slightly forward
|
|
650
|
+
depthBiasSlopeScale: 0.0,
|
|
651
|
+
depthBiasClamp: 0.0,
|
|
641
652
|
stencilFront: {
|
|
642
653
|
compare: "always",
|
|
643
654
|
failOp: "keep",
|
|
@@ -676,14 +687,10 @@ export class Engine {
|
|
|
676
687
|
) -> @builtin(position) vec4f {
|
|
677
688
|
let pos4 = vec4f(position, 1.0);
|
|
678
689
|
|
|
679
|
-
//
|
|
690
|
+
// Branchless weight normalization (avoids GPU branch divergence)
|
|
680
691
|
let weightSum = weights0.x + weights0.y + weights0.z + weights0.w;
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
normalizedWeights = weights0 / weightSum;
|
|
684
|
-
} else {
|
|
685
|
-
normalizedWeights = vec4f(1.0, 0.0, 0.0, 0.0);
|
|
686
|
-
}
|
|
692
|
+
let invWeightSum = select(1.0, 1.0 / weightSum, weightSum > 0.0001);
|
|
693
|
+
let normalizedWeights = select(vec4f(1.0, 0.0, 0.0, 0.0), weights0 * invWeightSum, weightSum > 0.0001);
|
|
687
694
|
|
|
688
695
|
var skinnedPos = vec4f(0.0, 0.0, 0.0, 0.0);
|
|
689
696
|
for (var i = 0u; i < 4u; i++) {
|
|
@@ -1037,19 +1044,21 @@ export class Engine {
|
|
|
1037
1044
|
@group(0) @binding(1) var inputSampler: sampler;
|
|
1038
1045
|
@group(0) @binding(2) var<uniform> blurUniforms: BlurUniforms;
|
|
1039
1046
|
|
|
1040
|
-
//
|
|
1047
|
+
// 3-tap gaussian blur using bilinear filtering trick (40% fewer texture fetches!)
|
|
1041
1048
|
@fragment fn fs(input: VertexOutput) -> @location(0) vec4f {
|
|
1042
1049
|
let texelSize = 1.0 / vec2f(textureDimensions(inputTexture));
|
|
1043
|
-
var result = vec4f(0.0);
|
|
1044
1050
|
|
|
1045
|
-
//
|
|
1046
|
-
|
|
1047
|
-
|
|
1051
|
+
// Bilinear optimization: leverage hardware filtering to sample between pixels
|
|
1052
|
+
// Original 5-tap: weights [0.06136, 0.24477, 0.38774, 0.24477, 0.06136] at offsets [-2, -1, 0, 1, 2]
|
|
1053
|
+
// Optimized 3-tap: combine adjacent samples using weighted offsets
|
|
1054
|
+
let weight0 = 0.38774; // Center sample
|
|
1055
|
+
let weight1 = 0.24477 + 0.06136; // Combined outer samples = 0.30613
|
|
1056
|
+
let offset1 = (0.24477 * 1.0 + 0.06136 * 2.0) / weight1; // Weighted position = 1.2
|
|
1048
1057
|
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1058
|
+
var result = textureSample(inputTexture, inputSampler, input.uv) * weight0;
|
|
1059
|
+
let offsetVec = offset1 * texelSize * blurUniforms.direction;
|
|
1060
|
+
result += textureSample(inputTexture, inputSampler, input.uv + offsetVec) * weight1;
|
|
1061
|
+
result += textureSample(inputTexture, inputSampler, input.uv - offsetVec) * weight1;
|
|
1053
1062
|
|
|
1054
1063
|
return result;
|
|
1055
1064
|
}
|
|
@@ -1685,44 +1694,6 @@ export class Engine {
|
|
|
1685
1694
|
await this.setupMaterials(model)
|
|
1686
1695
|
}
|
|
1687
1696
|
|
|
1688
|
-
private opaqueNonEyeNonHairDraws: {
|
|
1689
|
-
count: number
|
|
1690
|
-
firstIndex: number
|
|
1691
|
-
bindGroup: GPUBindGroup
|
|
1692
|
-
isTransparent: boolean
|
|
1693
|
-
}[] = []
|
|
1694
|
-
private eyeDraws: { count: number; firstIndex: number; bindGroup: GPUBindGroup; isTransparent: boolean }[] = []
|
|
1695
|
-
private hairDrawsOverEyes: { count: number; firstIndex: number; bindGroup: GPUBindGroup; isTransparent: boolean }[] =
|
|
1696
|
-
[]
|
|
1697
|
-
private hairDrawsOverNonEyes: {
|
|
1698
|
-
count: number
|
|
1699
|
-
firstIndex: number
|
|
1700
|
-
bindGroup: GPUBindGroup
|
|
1701
|
-
isTransparent: boolean
|
|
1702
|
-
}[] = []
|
|
1703
|
-
private transparentNonEyeNonHairDraws: {
|
|
1704
|
-
count: number
|
|
1705
|
-
firstIndex: number
|
|
1706
|
-
bindGroup: GPUBindGroup
|
|
1707
|
-
isTransparent: boolean
|
|
1708
|
-
}[] = []
|
|
1709
|
-
private opaqueNonEyeNonHairOutlineDraws: {
|
|
1710
|
-
count: number
|
|
1711
|
-
firstIndex: number
|
|
1712
|
-
bindGroup: GPUBindGroup
|
|
1713
|
-
isTransparent: boolean
|
|
1714
|
-
}[] = []
|
|
1715
|
-
private eyeOutlineDraws: { count: number; firstIndex: number; bindGroup: GPUBindGroup; isTransparent: boolean }[] = []
|
|
1716
|
-
private hairOutlineDraws: { count: number; firstIndex: number; bindGroup: GPUBindGroup; isTransparent: boolean }[] =
|
|
1717
|
-
[]
|
|
1718
|
-
private transparentNonEyeNonHairOutlineDraws: {
|
|
1719
|
-
count: number
|
|
1720
|
-
firstIndex: number
|
|
1721
|
-
bindGroup: GPUBindGroup
|
|
1722
|
-
isTransparent: boolean
|
|
1723
|
-
}[] = []
|
|
1724
|
-
|
|
1725
|
-
// Step 8: Load textures and create material bind groups
|
|
1726
1697
|
private async setupMaterials(model: Model) {
|
|
1727
1698
|
const materials = model.getMaterials()
|
|
1728
1699
|
if (materials.length === 0) {
|
|
@@ -1779,15 +1750,15 @@ export class Engine {
|
|
|
1779
1750
|
return defaultToonTexture
|
|
1780
1751
|
}
|
|
1781
1752
|
|
|
1782
|
-
this.
|
|
1753
|
+
this.opaqueDraws = []
|
|
1783
1754
|
this.eyeDraws = []
|
|
1784
1755
|
this.hairDrawsOverEyes = []
|
|
1785
1756
|
this.hairDrawsOverNonEyes = []
|
|
1786
|
-
this.
|
|
1787
|
-
this.
|
|
1757
|
+
this.transparentDraws = []
|
|
1758
|
+
this.opaqueOutlineDraws = []
|
|
1788
1759
|
this.eyeOutlineDraws = []
|
|
1789
1760
|
this.hairOutlineDraws = []
|
|
1790
|
-
this.
|
|
1761
|
+
this.transparentOutlineDraws = []
|
|
1791
1762
|
let currentIndexOffset = 0
|
|
1792
1763
|
|
|
1793
1764
|
for (const mat of materials) {
|
|
@@ -1808,11 +1779,11 @@ export class Engine {
|
|
|
1808
1779
|
materialUniformData[0] = materialAlpha
|
|
1809
1780
|
materialUniformData[1] = 1.0 // alphaMultiplier: 1.0 for non-hair materials
|
|
1810
1781
|
materialUniformData[2] = this.rimLightIntensity
|
|
1811
|
-
materialUniformData[3] =
|
|
1782
|
+
materialUniformData[3] = 0.0 // _padding1
|
|
1812
1783
|
materialUniformData[4] = 1.0 // rimColor.r
|
|
1813
1784
|
materialUniformData[5] = 1.0 // rimColor.g
|
|
1814
1785
|
materialUniformData[6] = 1.0 // rimColor.b
|
|
1815
|
-
materialUniformData[7] = 0.0
|
|
1786
|
+
materialUniformData[7] = 0.0 // isOverEyes
|
|
1816
1787
|
|
|
1817
1788
|
const materialUniformBuffer = this.device.createBuffer({
|
|
1818
1789
|
label: `material uniform: ${mat.name}`,
|
|
@@ -1829,15 +1800,14 @@ export class Engine {
|
|
|
1829
1800
|
{ binding: 0, resource: { buffer: this.cameraUniformBuffer } },
|
|
1830
1801
|
{ binding: 1, resource: { buffer: this.lightUniformBuffer } },
|
|
1831
1802
|
{ binding: 2, resource: diffuseTexture.createView() },
|
|
1832
|
-
{ binding: 3, resource: this.
|
|
1803
|
+
{ binding: 3, resource: this.materialSampler },
|
|
1833
1804
|
{ binding: 4, resource: { buffer: this.skinMatrixBuffer! } },
|
|
1834
1805
|
{ binding: 5, resource: toonTexture.createView() },
|
|
1835
|
-
{ binding: 6, resource: this.
|
|
1806
|
+
{ binding: 6, resource: this.materialSampler },
|
|
1836
1807
|
{ binding: 7, resource: { buffer: materialUniformBuffer } },
|
|
1837
1808
|
],
|
|
1838
1809
|
})
|
|
1839
1810
|
|
|
1840
|
-
// Classify materials into appropriate draw lists
|
|
1841
1811
|
if (mat.isEye) {
|
|
1842
1812
|
this.eyeDraws.push({
|
|
1843
1813
|
count: indexCount,
|
|
@@ -1852,11 +1822,11 @@ export class Engine {
|
|
|
1852
1822
|
uniformData[0] = materialAlpha
|
|
1853
1823
|
uniformData[1] = 1.0 // alphaMultiplier (shader adjusts based on isOverEyes)
|
|
1854
1824
|
uniformData[2] = this.rimLightIntensity
|
|
1855
|
-
uniformData[3] =
|
|
1825
|
+
uniformData[3] = 0.0 // _padding1
|
|
1856
1826
|
uniformData[4] = 1.0 // rimColor.rgb
|
|
1857
1827
|
uniformData[5] = 1.0
|
|
1858
1828
|
uniformData[6] = 1.0
|
|
1859
|
-
uniformData[7] = isOverEyes ? 1.0 : 0.0
|
|
1829
|
+
uniformData[7] = isOverEyes ? 1.0 : 0.0 // isOverEyes
|
|
1860
1830
|
|
|
1861
1831
|
const buffer = this.device.createBuffer({
|
|
1862
1832
|
label: `material uniform (${isOverEyes ? "over eyes" : "over non-eyes"}): ${mat.name}`,
|
|
@@ -1872,10 +1842,10 @@ export class Engine {
|
|
|
1872
1842
|
{ binding: 0, resource: { buffer: this.cameraUniformBuffer } },
|
|
1873
1843
|
{ binding: 1, resource: { buffer: this.lightUniformBuffer } },
|
|
1874
1844
|
{ binding: 2, resource: diffuseTexture.createView() },
|
|
1875
|
-
{ binding: 3, resource: this.
|
|
1845
|
+
{ binding: 3, resource: this.materialSampler },
|
|
1876
1846
|
{ binding: 4, resource: { buffer: this.skinMatrixBuffer! } },
|
|
1877
1847
|
{ binding: 5, resource: toonTexture.createView() },
|
|
1878
|
-
{ binding: 6, resource: this.
|
|
1848
|
+
{ binding: 6, resource: this.materialSampler },
|
|
1879
1849
|
{ binding: 7, resource: { buffer: buffer } },
|
|
1880
1850
|
],
|
|
1881
1851
|
})
|
|
@@ -1898,14 +1868,14 @@ export class Engine {
|
|
|
1898
1868
|
isTransparent,
|
|
1899
1869
|
})
|
|
1900
1870
|
} else if (isTransparent) {
|
|
1901
|
-
this.
|
|
1871
|
+
this.transparentDraws.push({
|
|
1902
1872
|
count: indexCount,
|
|
1903
1873
|
firstIndex: currentIndexOffset,
|
|
1904
1874
|
bindGroup,
|
|
1905
1875
|
isTransparent,
|
|
1906
1876
|
})
|
|
1907
1877
|
} else {
|
|
1908
|
-
this.
|
|
1878
|
+
this.opaqueDraws.push({
|
|
1909
1879
|
count: indexCount,
|
|
1910
1880
|
firstIndex: currentIndexOffset,
|
|
1911
1881
|
bindGroup,
|
|
@@ -1957,14 +1927,14 @@ export class Engine {
|
|
|
1957
1927
|
isTransparent,
|
|
1958
1928
|
})
|
|
1959
1929
|
} else if (isTransparent) {
|
|
1960
|
-
this.
|
|
1930
|
+
this.transparentOutlineDraws.push({
|
|
1961
1931
|
count: indexCount,
|
|
1962
1932
|
firstIndex: currentIndexOffset,
|
|
1963
1933
|
bindGroup: outlineBindGroup,
|
|
1964
1934
|
isTransparent,
|
|
1965
1935
|
})
|
|
1966
1936
|
} else {
|
|
1967
|
-
this.
|
|
1937
|
+
this.opaqueOutlineDraws.push({
|
|
1968
1938
|
count: indexCount,
|
|
1969
1939
|
firstIndex: currentIndexOffset,
|
|
1970
1940
|
bindGroup: outlineBindGroup,
|
|
@@ -2037,9 +2007,9 @@ export class Engine {
|
|
|
2037
2007
|
|
|
2038
2008
|
this.drawCallCount = 0
|
|
2039
2009
|
|
|
2040
|
-
// Pass 1: Opaque
|
|
2010
|
+
// Pass 1: Opaque
|
|
2041
2011
|
pass.setPipeline(this.modelPipeline)
|
|
2042
|
-
for (const draw of this.
|
|
2012
|
+
for (const draw of this.opaqueDraws) {
|
|
2043
2013
|
if (draw.count > 0) {
|
|
2044
2014
|
pass.setBindGroup(0, draw.bindGroup)
|
|
2045
2015
|
pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0)
|
|
@@ -2114,9 +2084,9 @@ export class Engine {
|
|
|
2114
2084
|
}
|
|
2115
2085
|
}
|
|
2116
2086
|
|
|
2117
|
-
// Pass 4: Transparent
|
|
2087
|
+
// Pass 4: Transparent
|
|
2118
2088
|
pass.setPipeline(this.modelPipeline)
|
|
2119
|
-
for (const draw of this.
|
|
2089
|
+
for (const draw of this.transparentDraws) {
|
|
2120
2090
|
if (draw.count > 0) {
|
|
2121
2091
|
pass.setBindGroup(0, draw.bindGroup)
|
|
2122
2092
|
pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0)
|
|
@@ -2150,10 +2120,6 @@ export class Engine {
|
|
|
2150
2120
|
this.device.queue.writeBuffer(this.bloomIntensityBuffer, 0, intensityData)
|
|
2151
2121
|
|
|
2152
2122
|
const encoder = this.device.createCommandEncoder()
|
|
2153
|
-
const width = this.canvas.width
|
|
2154
|
-
const height = this.canvas.height
|
|
2155
|
-
const bloomWidth = Math.floor(width / this.BLOOM_DOWNSCALE_FACTOR)
|
|
2156
|
-
const bloomHeight = Math.floor(height / this.BLOOM_DOWNSCALE_FACTOR)
|
|
2157
2123
|
|
|
2158
2124
|
// Extract bright areas
|
|
2159
2125
|
const extractPass = encoder.beginRenderPass({
|
|
@@ -2291,14 +2257,14 @@ export class Engine {
|
|
|
2291
2257
|
private drawOutlines(pass: GPURenderPassEncoder, transparent: boolean) {
|
|
2292
2258
|
pass.setPipeline(this.outlinePipeline)
|
|
2293
2259
|
if (transparent) {
|
|
2294
|
-
for (const draw of this.
|
|
2260
|
+
for (const draw of this.transparentOutlineDraws) {
|
|
2295
2261
|
if (draw.count > 0) {
|
|
2296
2262
|
pass.setBindGroup(0, draw.bindGroup)
|
|
2297
2263
|
pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0)
|
|
2298
2264
|
}
|
|
2299
2265
|
}
|
|
2300
2266
|
} else {
|
|
2301
|
-
for (const draw of this.
|
|
2267
|
+
for (const draw of this.opaqueOutlineDraws) {
|
|
2302
2268
|
if (draw.count > 0) {
|
|
2303
2269
|
pass.setBindGroup(0, draw.bindGroup)
|
|
2304
2270
|
pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0)
|
|
@@ -2376,18 +2342,18 @@ export class Engine {
|
|
|
2376
2342
|
bufferMemoryBytes += 24 * 4
|
|
2377
2343
|
}
|
|
2378
2344
|
const totalMaterialDraws =
|
|
2379
|
-
this.
|
|
2345
|
+
this.opaqueDraws.length +
|
|
2380
2346
|
this.eyeDraws.length +
|
|
2381
2347
|
this.hairDrawsOverEyes.length +
|
|
2382
2348
|
this.hairDrawsOverNonEyes.length +
|
|
2383
|
-
this.
|
|
2349
|
+
this.transparentDraws.length
|
|
2384
2350
|
bufferMemoryBytes += totalMaterialDraws * 32
|
|
2385
2351
|
|
|
2386
2352
|
const totalOutlineDraws =
|
|
2387
|
-
this.
|
|
2353
|
+
this.opaqueOutlineDraws.length +
|
|
2388
2354
|
this.eyeOutlineDraws.length +
|
|
2389
2355
|
this.hairOutlineDraws.length +
|
|
2390
|
-
this.
|
|
2356
|
+
this.transparentOutlineDraws.length
|
|
2391
2357
|
bufferMemoryBytes += totalOutlineDraws * 32
|
|
2392
2358
|
|
|
2393
2359
|
let renderTargetMemoryBytes = 0
|