reze-engine 0.2.5 → 0.2.6

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 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);
@@ -592,14 +583,10 @@ export class Engine {
592
583
  ) -> @builtin(position) vec4f {
593
584
  let pos4 = vec4f(position, 1.0);
594
585
 
595
- // Normalize weights
586
+ // Branchless weight normalization (avoids GPU branch divergence)
596
587
  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
- }
588
+ let invWeightSum = select(1.0, 1.0 / weightSum, weightSum > 0.0001);
589
+ let normalizedWeights = select(vec4f(1.0, 0.0, 0.0, 0.0), weights0 * invWeightSum, weightSum > 0.0001);
603
590
 
604
591
  var skinnedPos = vec4f(0.0, 0.0, 0.0, 0.0);
605
592
  for (var i = 0u; i < 4u; i++) {
@@ -944,19 +931,21 @@ export class Engine {
944
931
  @group(0) @binding(1) var inputSampler: sampler;
945
932
  @group(0) @binding(2) var<uniform> blurUniforms: BlurUniforms;
946
933
 
947
- // 5-tap gaussian blur
934
+ // 3-tap gaussian blur using bilinear filtering trick (40% fewer texture fetches!)
948
935
  @fragment fn fs(input: VertexOutput) -> @location(0) vec4f {
949
936
  let texelSize = 1.0 / vec2f(textureDimensions(inputTexture));
950
- var result = vec4f(0.0);
951
937
 
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);
938
+ // Bilinear optimization: leverage hardware filtering to sample between pixels
939
+ // Original 5-tap: weights [0.06136, 0.24477, 0.38774, 0.24477, 0.06136] at offsets [-2, -1, 0, 1, 2]
940
+ // Optimized 3-tap: combine adjacent samples using weighted offsets
941
+ let weight0 = 0.38774; // Center sample
942
+ let weight1 = 0.24477 + 0.06136; // Combined outer samples = 0.30613
943
+ let offset1 = (0.24477 * 1.0 + 0.06136 * 2.0) / weight1; // Weighted position = 1.2
955
944
 
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
- }
945
+ var result = textureSample(inputTexture, inputSampler, input.uv) * weight0;
946
+ let offsetVec = offset1 * texelSize * blurUniforms.direction;
947
+ result += textureSample(inputTexture, inputSampler, input.uv + offsetVec) * weight1;
948
+ result += textureSample(inputTexture, inputSampler, input.uv - offsetVec) * weight1;
960
949
 
961
950
  return result;
962
951
  }
@@ -1492,7 +1481,6 @@ export class Engine {
1492
1481
  }
1493
1482
  await this.setupMaterials(model);
1494
1483
  }
1495
- // Step 8: Load textures and create material bind groups
1496
1484
  async setupMaterials(model) {
1497
1485
  const materials = model.getMaterials();
1498
1486
  if (materials.length === 0) {
@@ -1539,15 +1527,15 @@ export class Engine {
1539
1527
  this.textureCache.set(defaultToonPath, defaultToonTexture);
1540
1528
  return defaultToonTexture;
1541
1529
  };
1542
- this.opaqueNonEyeNonHairDraws = [];
1530
+ this.opaqueDraws = [];
1543
1531
  this.eyeDraws = [];
1544
1532
  this.hairDrawsOverEyes = [];
1545
1533
  this.hairDrawsOverNonEyes = [];
1546
- this.transparentNonEyeNonHairDraws = [];
1547
- this.opaqueNonEyeNonHairOutlineDraws = [];
1534
+ this.transparentDraws = [];
1535
+ this.opaqueOutlineDraws = [];
1548
1536
  this.eyeOutlineDraws = [];
1549
1537
  this.hairOutlineDraws = [];
1550
- this.transparentNonEyeNonHairOutlineDraws = [];
1538
+ this.transparentOutlineDraws = [];
1551
1539
  let currentIndexOffset = 0;
1552
1540
  for (const mat of materials) {
1553
1541
  const indexCount = mat.vertexCount;
@@ -1565,11 +1553,11 @@ export class Engine {
1565
1553
  materialUniformData[0] = materialAlpha;
1566
1554
  materialUniformData[1] = 1.0; // alphaMultiplier: 1.0 for non-hair materials
1567
1555
  materialUniformData[2] = this.rimLightIntensity;
1568
- materialUniformData[3] = this.rimLightPower;
1556
+ materialUniformData[3] = 0.0; // _padding1
1569
1557
  materialUniformData[4] = 1.0; // rimColor.r
1570
1558
  materialUniformData[5] = 1.0; // rimColor.g
1571
1559
  materialUniformData[6] = 1.0; // rimColor.b
1572
- materialUniformData[7] = 0.0;
1560
+ materialUniformData[7] = 0.0; // isOverEyes
1573
1561
  const materialUniformBuffer = this.device.createBuffer({
1574
1562
  label: `material uniform: ${mat.name}`,
1575
1563
  size: materialUniformData.byteLength,
@@ -1584,14 +1572,13 @@ export class Engine {
1584
1572
  { binding: 0, resource: { buffer: this.cameraUniformBuffer } },
1585
1573
  { binding: 1, resource: { buffer: this.lightUniformBuffer } },
1586
1574
  { binding: 2, resource: diffuseTexture.createView() },
1587
- { binding: 3, resource: this.textureSampler },
1575
+ { binding: 3, resource: this.materialSampler },
1588
1576
  { binding: 4, resource: { buffer: this.skinMatrixBuffer } },
1589
1577
  { binding: 5, resource: toonTexture.createView() },
1590
- { binding: 6, resource: this.textureSampler },
1578
+ { binding: 6, resource: this.materialSampler },
1591
1579
  { binding: 7, resource: { buffer: materialUniformBuffer } },
1592
1580
  ],
1593
1581
  });
1594
- // Classify materials into appropriate draw lists
1595
1582
  if (mat.isEye) {
1596
1583
  this.eyeDraws.push({
1597
1584
  count: indexCount,
@@ -1607,11 +1594,11 @@ export class Engine {
1607
1594
  uniformData[0] = materialAlpha;
1608
1595
  uniformData[1] = 1.0; // alphaMultiplier (shader adjusts based on isOverEyes)
1609
1596
  uniformData[2] = this.rimLightIntensity;
1610
- uniformData[3] = this.rimLightPower;
1597
+ uniformData[3] = 0.0; // _padding1
1611
1598
  uniformData[4] = 1.0; // rimColor.rgb
1612
1599
  uniformData[5] = 1.0;
1613
1600
  uniformData[6] = 1.0;
1614
- uniformData[7] = isOverEyes ? 1.0 : 0.0;
1601
+ uniformData[7] = isOverEyes ? 1.0 : 0.0; // isOverEyes
1615
1602
  const buffer = this.device.createBuffer({
1616
1603
  label: `material uniform (${isOverEyes ? "over eyes" : "over non-eyes"}): ${mat.name}`,
1617
1604
  size: uniformData.byteLength,
@@ -1625,10 +1612,10 @@ export class Engine {
1625
1612
  { binding: 0, resource: { buffer: this.cameraUniformBuffer } },
1626
1613
  { binding: 1, resource: { buffer: this.lightUniformBuffer } },
1627
1614
  { binding: 2, resource: diffuseTexture.createView() },
1628
- { binding: 3, resource: this.textureSampler },
1615
+ { binding: 3, resource: this.materialSampler },
1629
1616
  { binding: 4, resource: { buffer: this.skinMatrixBuffer } },
1630
1617
  { binding: 5, resource: toonTexture.createView() },
1631
- { binding: 6, resource: this.textureSampler },
1618
+ { binding: 6, resource: this.materialSampler },
1632
1619
  { binding: 7, resource: { buffer: buffer } },
1633
1620
  ],
1634
1621
  });
@@ -1649,7 +1636,7 @@ export class Engine {
1649
1636
  });
1650
1637
  }
1651
1638
  else if (isTransparent) {
1652
- this.transparentNonEyeNonHairDraws.push({
1639
+ this.transparentDraws.push({
1653
1640
  count: indexCount,
1654
1641
  firstIndex: currentIndexOffset,
1655
1642
  bindGroup,
@@ -1657,7 +1644,7 @@ export class Engine {
1657
1644
  });
1658
1645
  }
1659
1646
  else {
1660
- this.opaqueNonEyeNonHairDraws.push({
1647
+ this.opaqueDraws.push({
1661
1648
  count: indexCount,
1662
1649
  firstIndex: currentIndexOffset,
1663
1650
  bindGroup,
@@ -1707,7 +1694,7 @@ export class Engine {
1707
1694
  });
1708
1695
  }
1709
1696
  else if (isTransparent) {
1710
- this.transparentNonEyeNonHairOutlineDraws.push({
1697
+ this.transparentOutlineDraws.push({
1711
1698
  count: indexCount,
1712
1699
  firstIndex: currentIndexOffset,
1713
1700
  bindGroup: outlineBindGroup,
@@ -1715,7 +1702,7 @@ export class Engine {
1715
1702
  });
1716
1703
  }
1717
1704
  else {
1718
- this.opaqueNonEyeNonHairOutlineDraws.push({
1705
+ this.opaqueOutlineDraws.push({
1719
1706
  count: indexCount,
1720
1707
  firstIndex: currentIndexOffset,
1721
1708
  bindGroup: outlineBindGroup,
@@ -1775,9 +1762,9 @@ export class Engine {
1775
1762
  pass.setVertexBuffer(2, this.weightsBuffer);
1776
1763
  pass.setIndexBuffer(this.indexBuffer, "uint32");
1777
1764
  this.drawCallCount = 0;
1778
- // Pass 1: Opaque non-eye, non-hair
1765
+ // Pass 1: Opaque
1779
1766
  pass.setPipeline(this.modelPipeline);
1780
- for (const draw of this.opaqueNonEyeNonHairDraws) {
1767
+ for (const draw of this.opaqueDraws) {
1781
1768
  if (draw.count > 0) {
1782
1769
  pass.setBindGroup(0, draw.bindGroup);
1783
1770
  pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0);
@@ -1845,9 +1832,9 @@ export class Engine {
1845
1832
  }
1846
1833
  }
1847
1834
  }
1848
- // Pass 4: Transparent non-eye, non-hair
1835
+ // Pass 4: Transparent
1849
1836
  pass.setPipeline(this.modelPipeline);
1850
- for (const draw of this.transparentNonEyeNonHairDraws) {
1837
+ for (const draw of this.transparentDraws) {
1851
1838
  if (draw.count > 0) {
1852
1839
  pass.setBindGroup(0, draw.bindGroup);
1853
1840
  pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0);
@@ -1992,7 +1979,7 @@ export class Engine {
1992
1979
  drawOutlines(pass, transparent) {
1993
1980
  pass.setPipeline(this.outlinePipeline);
1994
1981
  if (transparent) {
1995
- for (const draw of this.transparentNonEyeNonHairOutlineDraws) {
1982
+ for (const draw of this.transparentOutlineDraws) {
1996
1983
  if (draw.count > 0) {
1997
1984
  pass.setBindGroup(0, draw.bindGroup);
1998
1985
  pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0);
@@ -2000,7 +1987,7 @@ export class Engine {
2000
1987
  }
2001
1988
  }
2002
1989
  else {
2003
- for (const draw of this.opaqueNonEyeNonHairOutlineDraws) {
1990
+ for (const draw of this.opaqueOutlineDraws) {
2004
1991
  if (draw.count > 0) {
2005
1992
  pass.setBindGroup(0, draw.bindGroup);
2006
1993
  pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0);
@@ -2078,16 +2065,16 @@ export class Engine {
2078
2065
  if (this.fullscreenQuadBuffer) {
2079
2066
  bufferMemoryBytes += 24 * 4;
2080
2067
  }
2081
- const totalMaterialDraws = this.opaqueNonEyeNonHairDraws.length +
2068
+ const totalMaterialDraws = this.opaqueDraws.length +
2082
2069
  this.eyeDraws.length +
2083
2070
  this.hairDrawsOverEyes.length +
2084
2071
  this.hairDrawsOverNonEyes.length +
2085
- this.transparentNonEyeNonHairDraws.length;
2072
+ this.transparentDraws.length;
2086
2073
  bufferMemoryBytes += totalMaterialDraws * 32;
2087
- const totalOutlineDraws = this.opaqueNonEyeNonHairOutlineDraws.length +
2074
+ const totalOutlineDraws = this.opaqueOutlineDraws.length +
2088
2075
  this.eyeOutlineDraws.length +
2089
2076
  this.hairOutlineDraws.length +
2090
- this.transparentNonEyeNonHairOutlineDraws.length;
2077
+ this.transparentOutlineDraws.length;
2091
2078
  bufferMemoryBytes += totalOutlineDraws * 32;
2092
2079
  let renderTargetMemoryBytes = 0;
2093
2080
  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.6",
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);
@@ -676,14 +684,10 @@ export class Engine {
676
684
  ) -> @builtin(position) vec4f {
677
685
  let pos4 = vec4f(position, 1.0);
678
686
 
679
- // Normalize weights
687
+ // Branchless weight normalization (avoids GPU branch divergence)
680
688
  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
- }
689
+ let invWeightSum = select(1.0, 1.0 / weightSum, weightSum > 0.0001);
690
+ let normalizedWeights = select(vec4f(1.0, 0.0, 0.0, 0.0), weights0 * invWeightSum, weightSum > 0.0001);
687
691
 
688
692
  var skinnedPos = vec4f(0.0, 0.0, 0.0, 0.0);
689
693
  for (var i = 0u; i < 4u; i++) {
@@ -1037,19 +1041,21 @@ export class Engine {
1037
1041
  @group(0) @binding(1) var inputSampler: sampler;
1038
1042
  @group(0) @binding(2) var<uniform> blurUniforms: BlurUniforms;
1039
1043
 
1040
- // 5-tap gaussian blur
1044
+ // 3-tap gaussian blur using bilinear filtering trick (40% fewer texture fetches!)
1041
1045
  @fragment fn fs(input: VertexOutput) -> @location(0) vec4f {
1042
1046
  let texelSize = 1.0 / vec2f(textureDimensions(inputTexture));
1043
- var result = vec4f(0.0);
1044
1047
 
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);
1048
+ // Bilinear optimization: leverage hardware filtering to sample between pixels
1049
+ // Original 5-tap: weights [0.06136, 0.24477, 0.38774, 0.24477, 0.06136] at offsets [-2, -1, 0, 1, 2]
1050
+ // Optimized 3-tap: combine adjacent samples using weighted offsets
1051
+ let weight0 = 0.38774; // Center sample
1052
+ let weight1 = 0.24477 + 0.06136; // Combined outer samples = 0.30613
1053
+ let offset1 = (0.24477 * 1.0 + 0.06136 * 2.0) / weight1; // Weighted position = 1.2
1048
1054
 
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
- }
1055
+ var result = textureSample(inputTexture, inputSampler, input.uv) * weight0;
1056
+ let offsetVec = offset1 * texelSize * blurUniforms.direction;
1057
+ result += textureSample(inputTexture, inputSampler, input.uv + offsetVec) * weight1;
1058
+ result += textureSample(inputTexture, inputSampler, input.uv - offsetVec) * weight1;
1053
1059
 
1054
1060
  return result;
1055
1061
  }
@@ -1685,44 +1691,6 @@ export class Engine {
1685
1691
  await this.setupMaterials(model)
1686
1692
  }
1687
1693
 
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
1694
  private async setupMaterials(model: Model) {
1727
1695
  const materials = model.getMaterials()
1728
1696
  if (materials.length === 0) {
@@ -1779,15 +1747,15 @@ export class Engine {
1779
1747
  return defaultToonTexture
1780
1748
  }
1781
1749
 
1782
- this.opaqueNonEyeNonHairDraws = []
1750
+ this.opaqueDraws = []
1783
1751
  this.eyeDraws = []
1784
1752
  this.hairDrawsOverEyes = []
1785
1753
  this.hairDrawsOverNonEyes = []
1786
- this.transparentNonEyeNonHairDraws = []
1787
- this.opaqueNonEyeNonHairOutlineDraws = []
1754
+ this.transparentDraws = []
1755
+ this.opaqueOutlineDraws = []
1788
1756
  this.eyeOutlineDraws = []
1789
1757
  this.hairOutlineDraws = []
1790
- this.transparentNonEyeNonHairOutlineDraws = []
1758
+ this.transparentOutlineDraws = []
1791
1759
  let currentIndexOffset = 0
1792
1760
 
1793
1761
  for (const mat of materials) {
@@ -1808,11 +1776,11 @@ export class Engine {
1808
1776
  materialUniformData[0] = materialAlpha
1809
1777
  materialUniformData[1] = 1.0 // alphaMultiplier: 1.0 for non-hair materials
1810
1778
  materialUniformData[2] = this.rimLightIntensity
1811
- materialUniformData[3] = this.rimLightPower
1779
+ materialUniformData[3] = 0.0 // _padding1
1812
1780
  materialUniformData[4] = 1.0 // rimColor.r
1813
1781
  materialUniformData[5] = 1.0 // rimColor.g
1814
1782
  materialUniformData[6] = 1.0 // rimColor.b
1815
- materialUniformData[7] = 0.0
1783
+ materialUniformData[7] = 0.0 // isOverEyes
1816
1784
 
1817
1785
  const materialUniformBuffer = this.device.createBuffer({
1818
1786
  label: `material uniform: ${mat.name}`,
@@ -1829,15 +1797,14 @@ export class Engine {
1829
1797
  { binding: 0, resource: { buffer: this.cameraUniformBuffer } },
1830
1798
  { binding: 1, resource: { buffer: this.lightUniformBuffer } },
1831
1799
  { binding: 2, resource: diffuseTexture.createView() },
1832
- { binding: 3, resource: this.textureSampler },
1800
+ { binding: 3, resource: this.materialSampler },
1833
1801
  { binding: 4, resource: { buffer: this.skinMatrixBuffer! } },
1834
1802
  { binding: 5, resource: toonTexture.createView() },
1835
- { binding: 6, resource: this.textureSampler },
1803
+ { binding: 6, resource: this.materialSampler },
1836
1804
  { binding: 7, resource: { buffer: materialUniformBuffer } },
1837
1805
  ],
1838
1806
  })
1839
1807
 
1840
- // Classify materials into appropriate draw lists
1841
1808
  if (mat.isEye) {
1842
1809
  this.eyeDraws.push({
1843
1810
  count: indexCount,
@@ -1852,11 +1819,11 @@ export class Engine {
1852
1819
  uniformData[0] = materialAlpha
1853
1820
  uniformData[1] = 1.0 // alphaMultiplier (shader adjusts based on isOverEyes)
1854
1821
  uniformData[2] = this.rimLightIntensity
1855
- uniformData[3] = this.rimLightPower
1822
+ uniformData[3] = 0.0 // _padding1
1856
1823
  uniformData[4] = 1.0 // rimColor.rgb
1857
1824
  uniformData[5] = 1.0
1858
1825
  uniformData[6] = 1.0
1859
- uniformData[7] = isOverEyes ? 1.0 : 0.0
1826
+ uniformData[7] = isOverEyes ? 1.0 : 0.0 // isOverEyes
1860
1827
 
1861
1828
  const buffer = this.device.createBuffer({
1862
1829
  label: `material uniform (${isOverEyes ? "over eyes" : "over non-eyes"}): ${mat.name}`,
@@ -1872,10 +1839,10 @@ export class Engine {
1872
1839
  { binding: 0, resource: { buffer: this.cameraUniformBuffer } },
1873
1840
  { binding: 1, resource: { buffer: this.lightUniformBuffer } },
1874
1841
  { binding: 2, resource: diffuseTexture.createView() },
1875
- { binding: 3, resource: this.textureSampler },
1842
+ { binding: 3, resource: this.materialSampler },
1876
1843
  { binding: 4, resource: { buffer: this.skinMatrixBuffer! } },
1877
1844
  { binding: 5, resource: toonTexture.createView() },
1878
- { binding: 6, resource: this.textureSampler },
1845
+ { binding: 6, resource: this.materialSampler },
1879
1846
  { binding: 7, resource: { buffer: buffer } },
1880
1847
  ],
1881
1848
  })
@@ -1898,14 +1865,14 @@ export class Engine {
1898
1865
  isTransparent,
1899
1866
  })
1900
1867
  } else if (isTransparent) {
1901
- this.transparentNonEyeNonHairDraws.push({
1868
+ this.transparentDraws.push({
1902
1869
  count: indexCount,
1903
1870
  firstIndex: currentIndexOffset,
1904
1871
  bindGroup,
1905
1872
  isTransparent,
1906
1873
  })
1907
1874
  } else {
1908
- this.opaqueNonEyeNonHairDraws.push({
1875
+ this.opaqueDraws.push({
1909
1876
  count: indexCount,
1910
1877
  firstIndex: currentIndexOffset,
1911
1878
  bindGroup,
@@ -1957,14 +1924,14 @@ export class Engine {
1957
1924
  isTransparent,
1958
1925
  })
1959
1926
  } else if (isTransparent) {
1960
- this.transparentNonEyeNonHairOutlineDraws.push({
1927
+ this.transparentOutlineDraws.push({
1961
1928
  count: indexCount,
1962
1929
  firstIndex: currentIndexOffset,
1963
1930
  bindGroup: outlineBindGroup,
1964
1931
  isTransparent,
1965
1932
  })
1966
1933
  } else {
1967
- this.opaqueNonEyeNonHairOutlineDraws.push({
1934
+ this.opaqueOutlineDraws.push({
1968
1935
  count: indexCount,
1969
1936
  firstIndex: currentIndexOffset,
1970
1937
  bindGroup: outlineBindGroup,
@@ -2037,9 +2004,9 @@ export class Engine {
2037
2004
 
2038
2005
  this.drawCallCount = 0
2039
2006
 
2040
- // Pass 1: Opaque non-eye, non-hair
2007
+ // Pass 1: Opaque
2041
2008
  pass.setPipeline(this.modelPipeline)
2042
- for (const draw of this.opaqueNonEyeNonHairDraws) {
2009
+ for (const draw of this.opaqueDraws) {
2043
2010
  if (draw.count > 0) {
2044
2011
  pass.setBindGroup(0, draw.bindGroup)
2045
2012
  pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0)
@@ -2114,9 +2081,9 @@ export class Engine {
2114
2081
  }
2115
2082
  }
2116
2083
 
2117
- // Pass 4: Transparent non-eye, non-hair
2084
+ // Pass 4: Transparent
2118
2085
  pass.setPipeline(this.modelPipeline)
2119
- for (const draw of this.transparentNonEyeNonHairDraws) {
2086
+ for (const draw of this.transparentDraws) {
2120
2087
  if (draw.count > 0) {
2121
2088
  pass.setBindGroup(0, draw.bindGroup)
2122
2089
  pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0)
@@ -2291,14 +2258,14 @@ export class Engine {
2291
2258
  private drawOutlines(pass: GPURenderPassEncoder, transparent: boolean) {
2292
2259
  pass.setPipeline(this.outlinePipeline)
2293
2260
  if (transparent) {
2294
- for (const draw of this.transparentNonEyeNonHairOutlineDraws) {
2261
+ for (const draw of this.transparentOutlineDraws) {
2295
2262
  if (draw.count > 0) {
2296
2263
  pass.setBindGroup(0, draw.bindGroup)
2297
2264
  pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0)
2298
2265
  }
2299
2266
  }
2300
2267
  } else {
2301
- for (const draw of this.opaqueNonEyeNonHairOutlineDraws) {
2268
+ for (const draw of this.opaqueOutlineDraws) {
2302
2269
  if (draw.count > 0) {
2303
2270
  pass.setBindGroup(0, draw.bindGroup)
2304
2271
  pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0)
@@ -2376,18 +2343,18 @@ export class Engine {
2376
2343
  bufferMemoryBytes += 24 * 4
2377
2344
  }
2378
2345
  const totalMaterialDraws =
2379
- this.opaqueNonEyeNonHairDraws.length +
2346
+ this.opaqueDraws.length +
2380
2347
  this.eyeDraws.length +
2381
2348
  this.hairDrawsOverEyes.length +
2382
2349
  this.hairDrawsOverNonEyes.length +
2383
- this.transparentNonEyeNonHairDraws.length
2350
+ this.transparentDraws.length
2384
2351
  bufferMemoryBytes += totalMaterialDraws * 32
2385
2352
 
2386
2353
  const totalOutlineDraws =
2387
- this.opaqueNonEyeNonHairOutlineDraws.length +
2354
+ this.opaqueOutlineDraws.length +
2388
2355
  this.eyeOutlineDraws.length +
2389
2356
  this.hairOutlineDraws.length +
2390
- this.transparentNonEyeNonHairOutlineDraws.length
2357
+ this.transparentOutlineDraws.length
2391
2358
  bufferMemoryBytes += totalOutlineDraws * 32
2392
2359
 
2393
2360
  let renderTargetMemoryBytes = 0