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 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
- ```typescript
21
- export default function Home() {
22
- const canvasRef = useRef<HTMLCanvasElement>(null)
23
- const engineRef = useRef<Engine | null>(null)
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
- setEngineError(error instanceof Error ? error.message : "Unknown error")
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
- useEffect(() => {
66
- if (loading) {
67
- const interval = setInterval(() => {
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
- return () => clearInterval(interval)
77
- }
78
- }, [loading])
61
+ ## Projects Using This Engine
79
62
 
80
- return (
81
- <div
82
- className="fixed inset-0 w-full h-full overflow-hidden touch-none"
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
- {engineError && (
91
- <div className="absolute inset-0 w-full h-full flex items-center justify-center text-white p-6">
92
- Engine Error: {engineError}
93
- </div>
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 outlinePipeline;
29
- private hairOutlinePipeline;
28
+ private eyePipeline;
30
29
  private hairPipelineOverEyes;
31
30
  private hairPipelineOverNonEyes;
32
31
  private hairDepthPipeline;
33
- private eyePipeline;
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 textureSampler;
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;
@@ -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
@@ -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.textureSampler = this.device.createSampler({
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
- rimPower: f32,
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
- // Normalize weights to ensure they sum to 1.0 (handles floating-point precision issues)
154
+ // Branchless weight normalization (avoids GPU branch divergence)
156
155
  let weightSum = weights0.x + weights0.y + weights0.z + weights0.w;
157
- var normalizedWeights: vec4f;
158
- if (weightSum > 0.0001) {
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(rimFactor, material.rimPower);
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
- var finalAlpha = material.alpha * material.alphaMultiplier;
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
- // Normalize weights to ensure they sum to 1.0 (handles floating-point precision issues)
333
+ // Branchless weight normalization (avoids GPU branch divergence)
339
334
  let weightSum = weights0.x + weights0.y + weights0.z + weights0.w;
340
- var normalizedWeights: vec4f;
341
- if (weightSum > 0.0001) {
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: false, // Don't write depth
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
- // Normalize weights
589
+ // Branchless weight normalization (avoids GPU branch divergence)
596
590
  let weightSum = weights0.x + weights0.y + weights0.z + weights0.w;
597
- var normalizedWeights: vec4f;
598
- if (weightSum > 0.0001) {
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
- // 5-tap gaussian blur
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
- // Optimized 5-tap Gaussian filter (faster, nearly same quality)
953
- let weights = array<f32, 5>(0.06136, 0.24477, 0.38774, 0.24477, 0.06136);
954
- let offsets = array<f32, 5>(-2.0, -1.0, 0.0, 1.0, 2.0);
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
- for (var i = 0u; i < 5u; i++) {
957
- let offset = offsets[i] * texelSize * blurUniforms.direction;
958
- result += textureSample(inputTexture, inputSampler, input.uv + offset) * weights[i];
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.opaqueNonEyeNonHairDraws = [];
1533
+ this.opaqueDraws = [];
1543
1534
  this.eyeDraws = [];
1544
1535
  this.hairDrawsOverEyes = [];
1545
1536
  this.hairDrawsOverNonEyes = [];
1546
- this.transparentNonEyeNonHairDraws = [];
1547
- this.opaqueNonEyeNonHairOutlineDraws = [];
1537
+ this.transparentDraws = [];
1538
+ this.opaqueOutlineDraws = [];
1548
1539
  this.eyeOutlineDraws = [];
1549
1540
  this.hairOutlineDraws = [];
1550
- this.transparentNonEyeNonHairOutlineDraws = [];
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] = this.rimLightPower;
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.textureSampler },
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.textureSampler },
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] = this.rimLightPower;
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.textureSampler },
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.textureSampler },
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.transparentNonEyeNonHairDraws.push({
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.opaqueNonEyeNonHairDraws.push({
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.transparentNonEyeNonHairOutlineDraws.push({
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.opaqueNonEyeNonHairOutlineDraws.push({
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 non-eye, non-hair
1768
+ // Pass 1: Opaque
1779
1769
  pass.setPipeline(this.modelPipeline);
1780
- for (const draw of this.opaqueNonEyeNonHairDraws) {
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 non-eye, non-hair
1838
+ // Pass 4: Transparent
1849
1839
  pass.setPipeline(this.modelPipeline);
1850
- for (const draw of this.transparentNonEyeNonHairDraws) {
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.transparentNonEyeNonHairOutlineDraws) {
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.opaqueNonEyeNonHairOutlineDraws) {
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.opaqueNonEyeNonHairDraws.length +
2067
+ const totalMaterialDraws = this.opaqueDraws.length +
2082
2068
  this.eyeDraws.length +
2083
2069
  this.hairDrawsOverEyes.length +
2084
2070
  this.hairDrawsOverNonEyes.length +
2085
- this.transparentNonEyeNonHairDraws.length;
2071
+ this.transparentDraws.length;
2086
2072
  bufferMemoryBytes += totalMaterialDraws * 32;
2087
- const totalOutlineDraws = this.opaqueNonEyeNonHairOutlineDraws.length +
2073
+ const totalOutlineDraws = this.opaqueOutlineDraws.length +
2088
2074
  this.eyeOutlineDraws.length +
2089
2075
  this.hairOutlineDraws.length +
2090
- this.transparentNonEyeNonHairOutlineDraws.length;
2076
+ this.transparentOutlineDraws.length;
2091
2077
  bufferMemoryBytes += totalOutlineDraws * 32;
2092
2078
  let renderTargetMemoryBytes = 0;
2093
2079
  if (this.multisampleTexture) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "reze-engine",
3
- "version": "0.2.5",
3
+ "version": "0.2.7",
4
4
  "description": "A WebGPU-based MMD model renderer",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
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
- // Internal type for organizing bone keyframes during animation playback
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 outlinePipeline!: GPURenderPipeline
44
- private hairOutlinePipeline!: GPURenderPipeline
50
+ private eyePipeline!: GPURenderPipeline
45
51
  private hairPipelineOverEyes!: GPURenderPipeline
46
52
  private hairPipelineOverNonEyes!: GPURenderPipeline
47
53
  private hairDepthPipeline!: GPURenderPipeline
48
- private eyePipeline!: GPURenderPipeline
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
- // Bloom post-processing pipelines
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 textureSampler!: GPUSampler
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.textureSampler = this.device.createSampler({
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
- rimPower: f32,
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
- // Normalize weights to ensure they sum to 1.0 (handles floating-point precision issues)
245
+ // Branchless weight normalization (avoids GPU branch divergence)
230
246
  let weightSum = weights0.x + weights0.y + weights0.z + weights0.w;
231
- var normalizedWeights: vec4f;
232
- if (weightSum > 0.0001) {
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(rimFactor, material.rimPower);
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
- var finalAlpha = material.alpha * material.alphaMultiplier;
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
- // Normalize weights to ensure they sum to 1.0 (handles floating-point precision issues)
430
+ // Branchless weight normalization (avoids GPU branch divergence)
419
431
  let weightSum = weights0.x + weights0.y + weights0.z + weights0.w;
420
- var normalizedWeights: vec4f;
421
- if (weightSum > 0.0001) {
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: false, // Don't write depth
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
- // Normalize weights
690
+ // Branchless weight normalization (avoids GPU branch divergence)
680
691
  let weightSum = weights0.x + weights0.y + weights0.z + weights0.w;
681
- var normalizedWeights: vec4f;
682
- if (weightSum > 0.0001) {
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
- // 5-tap gaussian blur
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
- // Optimized 5-tap Gaussian filter (faster, nearly same quality)
1046
- let weights = array<f32, 5>(0.06136, 0.24477, 0.38774, 0.24477, 0.06136);
1047
- let offsets = array<f32, 5>(-2.0, -1.0, 0.0, 1.0, 2.0);
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
- for (var i = 0u; i < 5u; i++) {
1050
- let offset = offsets[i] * texelSize * blurUniforms.direction;
1051
- result += textureSample(inputTexture, inputSampler, input.uv + offset) * weights[i];
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.opaqueNonEyeNonHairDraws = []
1753
+ this.opaqueDraws = []
1783
1754
  this.eyeDraws = []
1784
1755
  this.hairDrawsOverEyes = []
1785
1756
  this.hairDrawsOverNonEyes = []
1786
- this.transparentNonEyeNonHairDraws = []
1787
- this.opaqueNonEyeNonHairOutlineDraws = []
1757
+ this.transparentDraws = []
1758
+ this.opaqueOutlineDraws = []
1788
1759
  this.eyeOutlineDraws = []
1789
1760
  this.hairOutlineDraws = []
1790
- this.transparentNonEyeNonHairOutlineDraws = []
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] = this.rimLightPower
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.textureSampler },
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.textureSampler },
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] = this.rimLightPower
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.textureSampler },
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.textureSampler },
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.transparentNonEyeNonHairDraws.push({
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.opaqueNonEyeNonHairDraws.push({
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.transparentNonEyeNonHairOutlineDraws.push({
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.opaqueNonEyeNonHairOutlineDraws.push({
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 non-eye, non-hair
2010
+ // Pass 1: Opaque
2041
2011
  pass.setPipeline(this.modelPipeline)
2042
- for (const draw of this.opaqueNonEyeNonHairDraws) {
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 non-eye, non-hair
2087
+ // Pass 4: Transparent
2118
2088
  pass.setPipeline(this.modelPipeline)
2119
- for (const draw of this.transparentNonEyeNonHairDraws) {
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.transparentNonEyeNonHairOutlineDraws) {
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.opaqueNonEyeNonHairOutlineDraws) {
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.opaqueNonEyeNonHairDraws.length +
2345
+ this.opaqueDraws.length +
2380
2346
  this.eyeDraws.length +
2381
2347
  this.hairDrawsOverEyes.length +
2382
2348
  this.hairDrawsOverNonEyes.length +
2383
- this.transparentNonEyeNonHairDraws.length
2349
+ this.transparentDraws.length
2384
2350
  bufferMemoryBytes += totalMaterialDraws * 32
2385
2351
 
2386
2352
  const totalOutlineDraws =
2387
- this.opaqueNonEyeNonHairOutlineDraws.length +
2353
+ this.opaqueOutlineDraws.length +
2388
2354
  this.eyeOutlineDraws.length +
2389
2355
  this.hairOutlineDraws.length +
2390
- this.transparentNonEyeNonHairOutlineDraws.length
2356
+ this.transparentOutlineDraws.length
2391
2357
  bufferMemoryBytes += totalOutlineDraws * 32
2392
2358
 
2393
2359
  let renderTargetMemoryBytes = 0