reze-engine 0.1.9 → 0.1.11

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
@@ -2,6 +2,19 @@
2
2
 
3
3
  A lightweight engine built with WebGPU and TypeScript for real-time 3D anime character MMD model rendering.
4
4
 
5
+ ## Features
6
+
7
+ - Physics
8
+ - Alpha blending
9
+ - Post alpha eye rendering
10
+ - Rim lighting
11
+ - Bloom
12
+ - Outlines
13
+ - Toon shading with directional lights
14
+ - MSAA 4x anti-aliasing
15
+ - GPU-accelerated skinning
16
+ - Bone rotation api
17
+
5
18
  ## Usage
6
19
 
7
20
  ```typescript
@@ -13,14 +26,6 @@ export default function Home() {
13
26
  const [stats, setStats] = useState<EngineStats>({
14
27
  fps: 0,
15
28
  frameTime: 0,
16
- memoryUsed: 0,
17
- drawCalls: 0,
18
- vertices: 0,
19
- triangles: 0,
20
- materials: 0,
21
- textures: 0,
22
- textureMemory: 0,
23
- bufferMemory: 0,
24
29
  gpuMemory: 0,
25
30
  })
26
31
  const [progress, setProgress] = useState(0)
package/dist/engine.d.ts CHANGED
@@ -17,7 +17,6 @@ export declare class Engine {
17
17
  private lightData;
18
18
  private lightCount;
19
19
  private vertexBuffer;
20
- private vertexCount;
21
20
  private indexBuffer?;
22
21
  private resizeObserver;
23
22
  private depthTexture;
@@ -55,6 +54,9 @@ export declare class Engine {
55
54
  private linearSampler;
56
55
  bloomThreshold: number;
57
56
  bloomIntensity: number;
57
+ private rimLightIntensity;
58
+ private rimLightPower;
59
+ private rimLightColor;
58
60
  private currentModel;
59
61
  private modelDir;
60
62
  private physics;
@@ -91,7 +93,8 @@ export declare class Engine {
91
93
  private setupModelBuffers;
92
94
  private opaqueNonEyeNonHairDraws;
93
95
  private eyeDraws;
94
- private hairDraws;
96
+ private hairDrawsOverEyes;
97
+ private hairDrawsOverNonEyes;
95
98
  private transparentNonEyeNonHairDraws;
96
99
  private opaqueNonEyeNonHairOutlineDraws;
97
100
  private eyeOutlineDraws;
@@ -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
@@ -7,12 +7,15 @@ export class Engine {
7
7
  this.cameraMatrixData = new Float32Array(36);
8
8
  this.lightData = new Float32Array(64);
9
9
  this.lightCount = 0;
10
- this.vertexCount = 0;
11
10
  this.resizeObserver = null;
12
11
  this.sampleCount = 4; // MSAA 4x
13
12
  // Bloom settings
14
13
  this.bloomThreshold = 0.3;
15
14
  this.bloomIntensity = 0.13;
15
+ // Rim light settings
16
+ this.rimLightIntensity = 0.35;
17
+ this.rimLightPower = 2.0;
18
+ this.rimLightColor = [1.0, 1.0, 1.0];
16
19
  this.currentModel = null;
17
20
  this.modelDir = "";
18
21
  this.physics = null;
@@ -33,7 +36,8 @@ export class Engine {
33
36
  this.renderLoopCallback = null;
34
37
  this.opaqueNonEyeNonHairDraws = [];
35
38
  this.eyeDraws = [];
36
- this.hairDraws = [];
39
+ this.hairDrawsOverEyes = [];
40
+ this.hairDrawsOverNonEyes = [];
37
41
  this.transparentNonEyeNonHairDraws = [];
38
42
  this.opaqueNonEyeNonHairOutlineDraws = [];
39
43
  this.eyeOutlineDraws = [];
@@ -102,9 +106,11 @@ export class Engine {
102
106
 
103
107
  struct MaterialUniforms {
104
108
  alpha: f32,
109
+ rimIntensity: f32,
110
+ rimPower: f32,
105
111
  _padding1: f32,
112
+ rimColor: vec3f,
106
113
  _padding2: f32,
107
- _padding3: f32,
108
114
  };
109
115
 
110
116
  struct VertexOutput {
@@ -175,7 +181,13 @@ export class Engine {
175
181
  lightAccum += toonFactor * radiance * nDotL;
176
182
  }
177
183
 
178
- let color = albedo * lightAccum;
184
+ // Rim light calculation
185
+ let viewDir = normalize(camera.viewPos - input.worldPos);
186
+ var rimFactor = 1.0 - max(dot(n, viewDir), 0.0);
187
+ rimFactor = pow(rimFactor, material.rimPower);
188
+ let rimLight = material.rimColor * material.rimIntensity * rimFactor;
189
+
190
+ let color = albedo * lightAccum + rimLight;
179
191
  let finalAlpha = material.alpha;
180
192
  if (finalAlpha < 0.001) {
181
193
  discard;
@@ -185,9 +197,10 @@ export class Engine {
185
197
  }
186
198
  `,
187
199
  });
188
- // Create a separate shader for hair-over-eyes that outputs pre-multiplied color for darkening effect
189
- const hairMultiplyShaderModule = this.device.createShaderModule({
190
- label: "hair multiply shaders",
200
+ // Unified hair shader that can handle both over-eyes and over-non-eyes cases
201
+ // Uses material.alpha multiplier to control opacity (0.5 for over-eyes, 1.0 for over-non-eyes)
202
+ const hairShaderModule = this.device.createShaderModule({
203
+ label: "unified hair shaders",
191
204
  code: /* wgsl */ `
192
205
  struct CameraUniforms {
193
206
  view: mat4x4f,
@@ -213,9 +226,11 @@ export class Engine {
213
226
 
214
227
  struct MaterialUniforms {
215
228
  alpha: f32,
229
+ alphaMultiplier: f32, // New: multiplier for alpha (0.5 for over-eyes, 1.0 for over-non-eyes)
230
+ rimIntensity: f32,
231
+ rimPower: f32,
232
+ rimColor: vec3f,
216
233
  _padding1: f32,
217
- _padding2: f32,
218
- _padding3: f32,
219
234
  };
220
235
 
221
236
  struct VertexOutput {
@@ -285,16 +300,19 @@ export class Engine {
285
300
  lightAccum += toonFactor * radiance * nDotL;
286
301
  }
287
302
 
288
- let color = albedo * lightAccum;
289
- let finalAlpha = material.alpha;
303
+ // Rim light calculation
304
+ let viewDir = normalize(camera.viewPos - input.worldPos);
305
+ var rimFactor = 1.0 - max(dot(n, viewDir), 0.0);
306
+ rimFactor = pow(rimFactor, material.rimPower);
307
+ let rimLight = material.rimColor * material.rimIntensity * rimFactor;
308
+
309
+ let color = albedo * lightAccum + rimLight;
310
+ let finalAlpha = material.alpha * material.alphaMultiplier;
290
311
  if (finalAlpha < 0.001) {
291
312
  discard;
292
313
  }
293
314
 
294
- // For hair-over-eyes effect: simple half-transparent overlay - Use 50% opacity to create a semi-transparent hair color overlay
295
- let overlayAlpha = finalAlpha * 0.5;
296
-
297
- return vec4f(clamp(color, vec3f(0.0), vec3f(1.0)), overlayAlpha);
315
+ return vec4f(clamp(color, vec3f(0.0), vec3f(1.0)), finalAlpha);
298
316
  }
299
317
  `,
300
318
  });
@@ -688,12 +706,13 @@ export class Engine {
688
706
  count: this.sampleCount,
689
707
  },
690
708
  });
691
- // Hair pipeline with multiplicative blending (for hair over eyes)
709
+ // Unified hair pipeline - can be used for both over-eyes and over-non-eyes
710
+ // The difference is controlled by stencil state and alpha multiplier in material uniform
692
711
  this.hairMultiplyPipeline = this.device.createRenderPipeline({
693
- label: "hair multiply pipeline",
712
+ label: "hair pipeline (over eyes)",
694
713
  layout: sharedPipelineLayout,
695
714
  vertex: {
696
- module: hairMultiplyShaderModule,
715
+ module: hairShaderModule,
697
716
  buffers: [
698
717
  {
699
718
  arrayStride: 8 * 4,
@@ -714,13 +733,12 @@ export class Engine {
714
733
  ],
715
734
  },
716
735
  fragment: {
717
- module: hairMultiplyShaderModule,
736
+ module: hairShaderModule,
718
737
  targets: [
719
738
  {
720
739
  format: this.presentationFormat,
721
740
  blend: {
722
741
  color: {
723
- // Simple half-transparent overlay effect - Blend: hairColor * overlayAlpha + eyeColor * (1 - overlayAlpha)
724
742
  srcFactor: "src-alpha",
725
743
  dstFactor: "one-minus-src-alpha",
726
744
  operation: "add",
@@ -754,12 +772,12 @@ export class Engine {
754
772
  },
755
773
  multisample: { count: this.sampleCount },
756
774
  });
757
- // Hair pipeline for opaque rendering (hair over non-eyes)
775
+ // Hair pipeline for opaque rendering (hair over non-eyes) - uses same shader, different stencil state
758
776
  this.hairOpaquePipeline = this.device.createRenderPipeline({
759
- label: "hair opaque pipeline",
777
+ label: "hair pipeline (over non-eyes)",
760
778
  layout: sharedPipelineLayout,
761
779
  vertex: {
762
- module: shaderModule,
780
+ module: hairShaderModule,
763
781
  buffers: [
764
782
  {
765
783
  arrayStride: 8 * 4,
@@ -780,7 +798,7 @@ export class Engine {
780
798
  ],
781
799
  },
782
800
  fragment: {
783
- module: shaderModule,
801
+ module: hairShaderModule,
784
802
  targets: [
785
803
  {
786
804
  format: this.presentationFormat,
@@ -1379,7 +1397,6 @@ export class Engine {
1379
1397
  usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
1380
1398
  });
1381
1399
  this.device.queue.writeBuffer(this.vertexBuffer, 0, vertices);
1382
- this.vertexCount = model.getVertexCount();
1383
1400
  this.jointsBuffer = this.device.createBuffer({
1384
1401
  label: "joints buffer",
1385
1402
  size: skinning.joints.byteLength,
@@ -1484,7 +1501,8 @@ export class Engine {
1484
1501
  };
1485
1502
  this.opaqueNonEyeNonHairDraws = [];
1486
1503
  this.eyeDraws = [];
1487
- this.hairDraws = [];
1504
+ this.hairDrawsOverEyes = [];
1505
+ this.hairDrawsOverNonEyes = [];
1488
1506
  this.transparentNonEyeNonHairDraws = [];
1489
1507
  this.opaqueNonEyeNonHairOutlineDraws = [];
1490
1508
  this.eyeOutlineDraws = [];
@@ -1502,11 +1520,17 @@ export class Engine {
1502
1520
  const materialAlpha = mat.diffuse[3];
1503
1521
  const EPSILON = 0.001;
1504
1522
  const isTransparent = materialAlpha < 1.0 - EPSILON;
1505
- const materialUniformData = new Float32Array(4);
1523
+ // Create material uniform data - for hair materials, we'll create two versions
1524
+ // MaterialUniforms struct: alpha, rimIntensity, rimPower, _padding1, rimColor (vec3), _padding2
1525
+ const materialUniformData = new Float32Array(8);
1506
1526
  materialUniformData[0] = materialAlpha;
1507
- materialUniformData[1] = 0.0;
1508
- materialUniformData[2] = 0.0;
1509
- materialUniformData[3] = 0.0;
1527
+ materialUniformData[1] = this.rimLightIntensity;
1528
+ materialUniformData[2] = this.rimLightPower;
1529
+ materialUniformData[3] = 0.0; // _padding1
1530
+ materialUniformData[4] = this.rimLightColor[0]; // rimColor.r
1531
+ materialUniformData[5] = this.rimLightColor[1]; // rimColor.g
1532
+ materialUniformData[6] = this.rimLightColor[2]; // rimColor.b
1533
+ materialUniformData[7] = 0.0; // _padding2
1510
1534
  const materialUniformBuffer = this.device.createBuffer({
1511
1535
  label: `material uniform: ${mat.name}`,
1512
1536
  size: materialUniformData.byteLength,
@@ -1538,10 +1562,77 @@ export class Engine {
1538
1562
  });
1539
1563
  }
1540
1564
  else if (mat.isHair) {
1541
- this.hairDraws.push({
1565
+ // For hair materials, create two bind groups: one for over-eyes (alphaMultiplier = 0.5) and one for over-non-eyes (alphaMultiplier = 1.0)
1566
+ // Hair MaterialUniforms struct: alpha, alphaMultiplier, rimIntensity, rimPower, rimColor (vec3), _padding1
1567
+ const materialUniformDataOverEyes = new Float32Array(8);
1568
+ materialUniformDataOverEyes[0] = materialAlpha;
1569
+ materialUniformDataOverEyes[1] = 0.5; // alphaMultiplier: 0.5 for over-eyes
1570
+ materialUniformDataOverEyes[2] = this.rimLightIntensity;
1571
+ materialUniformDataOverEyes[3] = this.rimLightPower;
1572
+ materialUniformDataOverEyes[4] = this.rimLightColor[0]; // rimColor.r
1573
+ materialUniformDataOverEyes[5] = this.rimLightColor[1]; // rimColor.g
1574
+ materialUniformDataOverEyes[6] = this.rimLightColor[2]; // rimColor.b
1575
+ materialUniformDataOverEyes[7] = 0.0; // _padding1
1576
+ const materialUniformBufferOverEyes = this.device.createBuffer({
1577
+ label: `material uniform (over eyes): ${mat.name}`,
1578
+ size: materialUniformDataOverEyes.byteLength,
1579
+ usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
1580
+ });
1581
+ this.device.queue.writeBuffer(materialUniformBufferOverEyes, 0, materialUniformDataOverEyes);
1582
+ const bindGroupOverEyes = this.device.createBindGroup({
1583
+ label: `material bind group (over eyes): ${mat.name}`,
1584
+ layout: this.hairBindGroupLayout,
1585
+ entries: [
1586
+ { binding: 0, resource: { buffer: this.cameraUniformBuffer } },
1587
+ { binding: 1, resource: { buffer: this.lightUniformBuffer } },
1588
+ { binding: 2, resource: diffuseTexture.createView() },
1589
+ { binding: 3, resource: this.textureSampler },
1590
+ { binding: 4, resource: { buffer: this.skinMatrixBuffer } },
1591
+ { binding: 5, resource: toonTexture.createView() },
1592
+ { binding: 6, resource: this.textureSampler },
1593
+ { binding: 7, resource: { buffer: materialUniformBufferOverEyes } },
1594
+ ],
1595
+ });
1596
+ this.hairDrawsOverEyes.push({
1542
1597
  count: matCount,
1543
1598
  firstIndex: runningFirstIndex,
1544
- bindGroup,
1599
+ bindGroup: bindGroupOverEyes,
1600
+ isTransparent,
1601
+ });
1602
+ // Create material uniform for hair over non-eyes (alphaMultiplier = 1.0)
1603
+ const materialUniformDataOverNonEyes = new Float32Array(8);
1604
+ materialUniformDataOverNonEyes[0] = materialAlpha;
1605
+ materialUniformDataOverNonEyes[1] = 1.0; // alphaMultiplier: 1.0 for over-non-eyes
1606
+ materialUniformDataOverNonEyes[2] = this.rimLightIntensity;
1607
+ materialUniformDataOverNonEyes[3] = this.rimLightPower;
1608
+ materialUniformDataOverNonEyes[4] = this.rimLightColor[0]; // rimColor.r
1609
+ materialUniformDataOverNonEyes[5] = this.rimLightColor[1]; // rimColor.g
1610
+ materialUniformDataOverNonEyes[6] = this.rimLightColor[2]; // rimColor.b
1611
+ materialUniformDataOverNonEyes[7] = 0.0; // _padding1
1612
+ const materialUniformBufferOverNonEyes = this.device.createBuffer({
1613
+ label: `material uniform (over non-eyes): ${mat.name}`,
1614
+ size: materialUniformDataOverNonEyes.byteLength,
1615
+ usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
1616
+ });
1617
+ this.device.queue.writeBuffer(materialUniformBufferOverNonEyes, 0, materialUniformDataOverNonEyes);
1618
+ const bindGroupOverNonEyes = this.device.createBindGroup({
1619
+ label: `material bind group (over non-eyes): ${mat.name}`,
1620
+ layout: this.hairBindGroupLayout,
1621
+ entries: [
1622
+ { binding: 0, resource: { buffer: this.cameraUniformBuffer } },
1623
+ { binding: 1, resource: { buffer: this.lightUniformBuffer } },
1624
+ { binding: 2, resource: diffuseTexture.createView() },
1625
+ { binding: 3, resource: this.textureSampler },
1626
+ { binding: 4, resource: { buffer: this.skinMatrixBuffer } },
1627
+ { binding: 5, resource: toonTexture.createView() },
1628
+ { binding: 6, resource: this.textureSampler },
1629
+ { binding: 7, resource: { buffer: materialUniformBufferOverNonEyes } },
1630
+ ],
1631
+ });
1632
+ this.hairDrawsOverNonEyes.push({
1633
+ count: matCount,
1634
+ firstIndex: runningFirstIndex,
1635
+ bindGroup: bindGroupOverNonEyes,
1545
1636
  isTransparent,
1546
1637
  });
1547
1638
  }
@@ -1702,45 +1793,55 @@ export class Engine {
1702
1793
  this.drawCallCount++;
1703
1794
  }
1704
1795
  }
1705
- // PASS 3a: Hair over eyes (stencil == 1, multiply blend) - Draw hair geometry first to establish depth
1706
- pass.setPipeline(this.hairMultiplyPipeline);
1707
- pass.setStencilReference(1); // Check against stencil value 1
1708
- for (const draw of this.hairDraws) {
1709
- if (draw.count > 0) {
1710
- pass.setBindGroup(0, draw.bindGroup);
1711
- pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0);
1712
- this.drawCallCount++;
1796
+ // PASS 3: Hair rendering - optimized single pass approach
1797
+ // Since both hair passes use the same shader, we batch them together
1798
+ // but still need separate passes due to stencil requirements (equal vs not-equal)
1799
+ this.drawOutlines(pass, false); // Opaque outlines
1800
+ // 3a: Hair over eyes (stencil == 1, alphaMultiplier = 0.5)
1801
+ if (this.hairDrawsOverEyes.length > 0) {
1802
+ pass.setPipeline(this.hairMultiplyPipeline);
1803
+ pass.setStencilReference(1);
1804
+ for (const draw of this.hairDrawsOverEyes) {
1805
+ if (draw.count > 0) {
1806
+ pass.setBindGroup(0, draw.bindGroup);
1807
+ pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0);
1808
+ this.drawCallCount++;
1809
+ }
1713
1810
  }
1714
1811
  }
1715
- // PASS 3a.5: Hair outlines over eyes (stencil == 1, depth test to only draw near hair)
1716
- pass.setPipeline(this.hairOutlineOverEyesPipeline);
1717
- pass.setStencilReference(1); // Check against stencil value 1 (with equal test)
1718
- for (const draw of this.hairOutlineDraws) {
1719
- if (draw.count > 0) {
1720
- pass.setBindGroup(0, draw.bindGroup);
1721
- pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0);
1812
+ // 3b: Hair over non-eyes (stencil != 1, alphaMultiplier = 1.0)
1813
+ if (this.hairDrawsOverNonEyes.length > 0) {
1814
+ pass.setPipeline(this.hairOpaquePipeline);
1815
+ pass.setStencilReference(1);
1816
+ for (const draw of this.hairDrawsOverNonEyes) {
1817
+ if (draw.count > 0) {
1818
+ pass.setBindGroup(0, draw.bindGroup);
1819
+ pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0);
1820
+ this.drawCallCount++;
1821
+ }
1722
1822
  }
1723
1823
  }
1724
- // PASS 3b: Hair over non-eyes (stencil != 1, opaque)
1725
- pass.setPipeline(this.hairOpaquePipeline);
1726
- pass.setStencilReference(1); // Check against stencil value 1 (with not-equal test)
1727
- for (const draw of this.hairDraws) {
1728
- if (draw.count > 0) {
1729
- pass.setBindGroup(0, draw.bindGroup);
1730
- pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0);
1731
- this.drawCallCount++;
1824
+ // 3c: Hair outlines - batched together, only draw if outlines exist
1825
+ if (this.hairOutlineDraws.length > 0) {
1826
+ // Over eyes
1827
+ pass.setPipeline(this.hairOutlineOverEyesPipeline);
1828
+ pass.setStencilReference(1);
1829
+ for (const draw of this.hairOutlineDraws) {
1830
+ if (draw.count > 0) {
1831
+ pass.setBindGroup(0, draw.bindGroup);
1832
+ pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0);
1833
+ }
1732
1834
  }
1733
- }
1734
- // PASS 3b.5: Hair outlines over non-eyes (stencil != 1) - Draw hair outlines after hair geometry, so they only appear where hair exists
1735
- pass.setPipeline(this.hairOutlinePipeline);
1736
- pass.setStencilReference(1); // Check against stencil value 1 (with not-equal test)
1737
- for (const draw of this.hairOutlineDraws) {
1738
- if (draw.count > 0) {
1739
- pass.setBindGroup(0, draw.bindGroup);
1740
- pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0);
1835
+ // Over non-eyes
1836
+ pass.setPipeline(this.hairOutlinePipeline);
1837
+ pass.setStencilReference(1);
1838
+ for (const draw of this.hairOutlineDraws) {
1839
+ if (draw.count > 0) {
1840
+ pass.setBindGroup(0, draw.bindGroup);
1841
+ pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0);
1842
+ }
1741
1843
  }
1742
1844
  }
1743
- this.drawOutlines(pass, false); // Opaque outlines
1744
1845
  // PASS 4: Transparent non-eye, non-hair
1745
1846
  pass.setPipeline(this.pipeline);
1746
1847
  for (const draw of this.transparentNonEyeNonHairDraws) {
@@ -2026,7 +2127,8 @@ export class Engine {
2026
2127
  bufferMemoryBytes += 64 * 4; // lightUniformBuffer
2027
2128
  const totalMaterialDraws = this.opaqueNonEyeNonHairDraws.length +
2028
2129
  this.eyeDraws.length +
2029
- this.hairDraws.length +
2130
+ this.hairDrawsOverEyes.length +
2131
+ this.hairDrawsOverNonEyes.length +
2030
2132
  this.transparentNonEyeNonHairDraws.length;
2031
2133
  bufferMemoryBytes += totalMaterialDraws * 4; // Material uniform buffers
2032
2134
  let renderTargetMemoryBytes = 0;
package/package.json CHANGED
@@ -1,10 +1,14 @@
1
1
  {
2
2
  "name": "reze-engine",
3
- "version": "0.1.9",
3
+ "version": "0.1.11",
4
4
  "description": "A WebGPU-based MMD model renderer",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
7
7
  "type": "module",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "https://github.com/AmyangXYZ/reze-engine"
11
+ },
8
12
  "exports": {
9
13
  ".": {
10
14
  "types": "./dist/index.d.ts",
package/src/engine.ts CHANGED
@@ -22,7 +22,6 @@ export class Engine {
22
22
  private lightData = new Float32Array(64)
23
23
  private lightCount = 0
24
24
  private vertexBuffer!: GPUBuffer
25
- private vertexCount: number = 0
26
25
  private indexBuffer?: GPUBuffer
27
26
  private resizeObserver: ResizeObserver | null = null
28
27
  private depthTexture!: GPUTexture
@@ -64,6 +63,10 @@ export class Engine {
64
63
  // Bloom settings
65
64
  public bloomThreshold: number = 0.3
66
65
  public bloomIntensity: number = 0.13
66
+ // Rim light settings
67
+ private rimLightIntensity: number = 0.35
68
+ private rimLightPower: number = 2.0
69
+ private rimLightColor: [number, number, number] = [1.0, 1.0, 1.0]
67
70
  private currentModel: Model | null = null
68
71
  private modelDir: string = ""
69
72
  private physics: Physics | null = null
@@ -156,9 +159,11 @@ export class Engine {
156
159
 
157
160
  struct MaterialUniforms {
158
161
  alpha: f32,
162
+ rimIntensity: f32,
163
+ rimPower: f32,
159
164
  _padding1: f32,
165
+ rimColor: vec3f,
160
166
  _padding2: f32,
161
- _padding3: f32,
162
167
  };
163
168
 
164
169
  struct VertexOutput {
@@ -229,7 +234,13 @@ export class Engine {
229
234
  lightAccum += toonFactor * radiance * nDotL;
230
235
  }
231
236
 
232
- let color = albedo * lightAccum;
237
+ // Rim light calculation
238
+ let viewDir = normalize(camera.viewPos - input.worldPos);
239
+ var rimFactor = 1.0 - max(dot(n, viewDir), 0.0);
240
+ rimFactor = pow(rimFactor, material.rimPower);
241
+ let rimLight = material.rimColor * material.rimIntensity * rimFactor;
242
+
243
+ let color = albedo * lightAccum + rimLight;
233
244
  let finalAlpha = material.alpha;
234
245
  if (finalAlpha < 0.001) {
235
246
  discard;
@@ -240,9 +251,10 @@ export class Engine {
240
251
  `,
241
252
  })
242
253
 
243
- // Create a separate shader for hair-over-eyes that outputs pre-multiplied color for darkening effect
244
- const hairMultiplyShaderModule = this.device.createShaderModule({
245
- label: "hair multiply shaders",
254
+ // Unified hair shader that can handle both over-eyes and over-non-eyes cases
255
+ // Uses material.alpha multiplier to control opacity (0.5 for over-eyes, 1.0 for over-non-eyes)
256
+ const hairShaderModule = this.device.createShaderModule({
257
+ label: "unified hair shaders",
246
258
  code: /* wgsl */ `
247
259
  struct CameraUniforms {
248
260
  view: mat4x4f,
@@ -268,9 +280,11 @@ export class Engine {
268
280
 
269
281
  struct MaterialUniforms {
270
282
  alpha: f32,
283
+ alphaMultiplier: f32, // New: multiplier for alpha (0.5 for over-eyes, 1.0 for over-non-eyes)
284
+ rimIntensity: f32,
285
+ rimPower: f32,
286
+ rimColor: vec3f,
271
287
  _padding1: f32,
272
- _padding2: f32,
273
- _padding3: f32,
274
288
  };
275
289
 
276
290
  struct VertexOutput {
@@ -340,16 +354,19 @@ export class Engine {
340
354
  lightAccum += toonFactor * radiance * nDotL;
341
355
  }
342
356
 
343
- let color = albedo * lightAccum;
344
- let finalAlpha = material.alpha;
357
+ // Rim light calculation
358
+ let viewDir = normalize(camera.viewPos - input.worldPos);
359
+ var rimFactor = 1.0 - max(dot(n, viewDir), 0.0);
360
+ rimFactor = pow(rimFactor, material.rimPower);
361
+ let rimLight = material.rimColor * material.rimIntensity * rimFactor;
362
+
363
+ let color = albedo * lightAccum + rimLight;
364
+ let finalAlpha = material.alpha * material.alphaMultiplier;
345
365
  if (finalAlpha < 0.001) {
346
366
  discard;
347
367
  }
348
368
 
349
- // For hair-over-eyes effect: simple half-transparent overlay - Use 50% opacity to create a semi-transparent hair color overlay
350
- let overlayAlpha = finalAlpha * 0.5;
351
-
352
- return vec4f(clamp(color, vec3f(0.0), vec3f(1.0)), overlayAlpha);
369
+ return vec4f(clamp(color, vec3f(0.0), vec3f(1.0)), finalAlpha);
353
370
  }
354
371
  `,
355
372
  })
@@ -754,12 +771,13 @@ export class Engine {
754
771
  },
755
772
  })
756
773
 
757
- // Hair pipeline with multiplicative blending (for hair over eyes)
774
+ // Unified hair pipeline - can be used for both over-eyes and over-non-eyes
775
+ // The difference is controlled by stencil state and alpha multiplier in material uniform
758
776
  this.hairMultiplyPipeline = this.device.createRenderPipeline({
759
- label: "hair multiply pipeline",
777
+ label: "hair pipeline (over eyes)",
760
778
  layout: sharedPipelineLayout,
761
779
  vertex: {
762
- module: hairMultiplyShaderModule,
780
+ module: hairShaderModule,
763
781
  buffers: [
764
782
  {
765
783
  arrayStride: 8 * 4,
@@ -780,13 +798,12 @@ export class Engine {
780
798
  ],
781
799
  },
782
800
  fragment: {
783
- module: hairMultiplyShaderModule,
801
+ module: hairShaderModule,
784
802
  targets: [
785
803
  {
786
804
  format: this.presentationFormat,
787
805
  blend: {
788
806
  color: {
789
- // Simple half-transparent overlay effect - Blend: hairColor * overlayAlpha + eyeColor * (1 - overlayAlpha)
790
807
  srcFactor: "src-alpha",
791
808
  dstFactor: "one-minus-src-alpha",
792
809
  operation: "add",
@@ -821,12 +838,12 @@ export class Engine {
821
838
  multisample: { count: this.sampleCount },
822
839
  })
823
840
 
824
- // Hair pipeline for opaque rendering (hair over non-eyes)
841
+ // Hair pipeline for opaque rendering (hair over non-eyes) - uses same shader, different stencil state
825
842
  this.hairOpaquePipeline = this.device.createRenderPipeline({
826
- label: "hair opaque pipeline",
843
+ label: "hair pipeline (over non-eyes)",
827
844
  layout: sharedPipelineLayout,
828
845
  vertex: {
829
- module: shaderModule,
846
+ module: hairShaderModule,
830
847
  buffers: [
831
848
  {
832
849
  arrayStride: 8 * 4,
@@ -847,7 +864,7 @@ export class Engine {
847
864
  ],
848
865
  },
849
866
  fragment: {
850
- module: shaderModule,
867
+ module: hairShaderModule,
851
868
  targets: [
852
869
  {
853
870
  format: this.presentationFormat,
@@ -1498,7 +1515,6 @@ export class Engine {
1498
1515
  usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
1499
1516
  })
1500
1517
  this.device.queue.writeBuffer(this.vertexBuffer, 0, vertices)
1501
- this.vertexCount = model.getVertexCount()
1502
1518
 
1503
1519
  this.jointsBuffer = this.device.createBuffer({
1504
1520
  label: "joints buffer",
@@ -1589,7 +1605,14 @@ export class Engine {
1589
1605
  isTransparent: boolean
1590
1606
  }[] = []
1591
1607
  private eyeDraws: { count: number; firstIndex: number; bindGroup: GPUBindGroup; isTransparent: boolean }[] = []
1592
- private hairDraws: { count: number; firstIndex: number; bindGroup: GPUBindGroup; isTransparent: boolean }[] = []
1608
+ private hairDrawsOverEyes: { count: number; firstIndex: number; bindGroup: GPUBindGroup; isTransparent: boolean }[] =
1609
+ []
1610
+ private hairDrawsOverNonEyes: {
1611
+ count: number
1612
+ firstIndex: number
1613
+ bindGroup: GPUBindGroup
1614
+ isTransparent: boolean
1615
+ }[] = []
1593
1616
  private transparentNonEyeNonHairDraws: {
1594
1617
  count: number
1595
1618
  firstIndex: number
@@ -1672,7 +1695,8 @@ export class Engine {
1672
1695
 
1673
1696
  this.opaqueNonEyeNonHairDraws = []
1674
1697
  this.eyeDraws = []
1675
- this.hairDraws = []
1698
+ this.hairDrawsOverEyes = []
1699
+ this.hairDrawsOverNonEyes = []
1676
1700
  this.transparentNonEyeNonHairDraws = []
1677
1701
  this.opaqueNonEyeNonHairOutlineDraws = []
1678
1702
  this.eyeOutlineDraws = []
@@ -1693,11 +1717,17 @@ export class Engine {
1693
1717
  const EPSILON = 0.001
1694
1718
  const isTransparent = materialAlpha < 1.0 - EPSILON
1695
1719
 
1696
- const materialUniformData = new Float32Array(4)
1720
+ // Create material uniform data - for hair materials, we'll create two versions
1721
+ // MaterialUniforms struct: alpha, rimIntensity, rimPower, _padding1, rimColor (vec3), _padding2
1722
+ const materialUniformData = new Float32Array(8)
1697
1723
  materialUniformData[0] = materialAlpha
1698
- materialUniformData[1] = 0.0
1699
- materialUniformData[2] = 0.0
1700
- materialUniformData[3] = 0.0
1724
+ materialUniformData[1] = this.rimLightIntensity
1725
+ materialUniformData[2] = this.rimLightPower
1726
+ materialUniformData[3] = 0.0 // _padding1
1727
+ materialUniformData[4] = this.rimLightColor[0] // rimColor.r
1728
+ materialUniformData[5] = this.rimLightColor[1] // rimColor.g
1729
+ materialUniformData[6] = this.rimLightColor[2] // rimColor.b
1730
+ materialUniformData[7] = 0.0 // _padding2
1701
1731
 
1702
1732
  const materialUniformBuffer = this.device.createBuffer({
1703
1733
  label: `material uniform: ${mat.name}`,
@@ -1731,10 +1761,84 @@ export class Engine {
1731
1761
  isTransparent,
1732
1762
  })
1733
1763
  } else if (mat.isHair) {
1734
- this.hairDraws.push({
1764
+ // For hair materials, create two bind groups: one for over-eyes (alphaMultiplier = 0.5) and one for over-non-eyes (alphaMultiplier = 1.0)
1765
+ // Hair MaterialUniforms struct: alpha, alphaMultiplier, rimIntensity, rimPower, rimColor (vec3), _padding1
1766
+ const materialUniformDataOverEyes = new Float32Array(8)
1767
+ materialUniformDataOverEyes[0] = materialAlpha
1768
+ materialUniformDataOverEyes[1] = 0.5 // alphaMultiplier: 0.5 for over-eyes
1769
+ materialUniformDataOverEyes[2] = this.rimLightIntensity
1770
+ materialUniformDataOverEyes[3] = this.rimLightPower
1771
+ materialUniformDataOverEyes[4] = this.rimLightColor[0] // rimColor.r
1772
+ materialUniformDataOverEyes[5] = this.rimLightColor[1] // rimColor.g
1773
+ materialUniformDataOverEyes[6] = this.rimLightColor[2] // rimColor.b
1774
+ materialUniformDataOverEyes[7] = 0.0 // _padding1
1775
+
1776
+ const materialUniformBufferOverEyes = this.device.createBuffer({
1777
+ label: `material uniform (over eyes): ${mat.name}`,
1778
+ size: materialUniformDataOverEyes.byteLength,
1779
+ usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
1780
+ })
1781
+ this.device.queue.writeBuffer(materialUniformBufferOverEyes, 0, materialUniformDataOverEyes)
1782
+
1783
+ const bindGroupOverEyes = this.device.createBindGroup({
1784
+ label: `material bind group (over eyes): ${mat.name}`,
1785
+ layout: this.hairBindGroupLayout,
1786
+ entries: [
1787
+ { binding: 0, resource: { buffer: this.cameraUniformBuffer } },
1788
+ { binding: 1, resource: { buffer: this.lightUniformBuffer } },
1789
+ { binding: 2, resource: diffuseTexture.createView() },
1790
+ { binding: 3, resource: this.textureSampler },
1791
+ { binding: 4, resource: { buffer: this.skinMatrixBuffer! } },
1792
+ { binding: 5, resource: toonTexture.createView() },
1793
+ { binding: 6, resource: this.textureSampler },
1794
+ { binding: 7, resource: { buffer: materialUniformBufferOverEyes } },
1795
+ ],
1796
+ })
1797
+
1798
+ this.hairDrawsOverEyes.push({
1735
1799
  count: matCount,
1736
1800
  firstIndex: runningFirstIndex,
1737
- bindGroup,
1801
+ bindGroup: bindGroupOverEyes,
1802
+ isTransparent,
1803
+ })
1804
+
1805
+ // Create material uniform for hair over non-eyes (alphaMultiplier = 1.0)
1806
+ const materialUniformDataOverNonEyes = new Float32Array(8)
1807
+ materialUniformDataOverNonEyes[0] = materialAlpha
1808
+ materialUniformDataOverNonEyes[1] = 1.0 // alphaMultiplier: 1.0 for over-non-eyes
1809
+ materialUniformDataOverNonEyes[2] = this.rimLightIntensity
1810
+ materialUniformDataOverNonEyes[3] = this.rimLightPower
1811
+ materialUniformDataOverNonEyes[4] = this.rimLightColor[0] // rimColor.r
1812
+ materialUniformDataOverNonEyes[5] = this.rimLightColor[1] // rimColor.g
1813
+ materialUniformDataOverNonEyes[6] = this.rimLightColor[2] // rimColor.b
1814
+ materialUniformDataOverNonEyes[7] = 0.0 // _padding1
1815
+
1816
+ const materialUniformBufferOverNonEyes = this.device.createBuffer({
1817
+ label: `material uniform (over non-eyes): ${mat.name}`,
1818
+ size: materialUniformDataOverNonEyes.byteLength,
1819
+ usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
1820
+ })
1821
+ this.device.queue.writeBuffer(materialUniformBufferOverNonEyes, 0, materialUniformDataOverNonEyes)
1822
+
1823
+ const bindGroupOverNonEyes = this.device.createBindGroup({
1824
+ label: `material bind group (over non-eyes): ${mat.name}`,
1825
+ layout: this.hairBindGroupLayout,
1826
+ entries: [
1827
+ { binding: 0, resource: { buffer: this.cameraUniformBuffer } },
1828
+ { binding: 1, resource: { buffer: this.lightUniformBuffer } },
1829
+ { binding: 2, resource: diffuseTexture.createView() },
1830
+ { binding: 3, resource: this.textureSampler },
1831
+ { binding: 4, resource: { buffer: this.skinMatrixBuffer! } },
1832
+ { binding: 5, resource: toonTexture.createView() },
1833
+ { binding: 6, resource: this.textureSampler },
1834
+ { binding: 7, resource: { buffer: materialUniformBufferOverNonEyes } },
1835
+ ],
1836
+ })
1837
+
1838
+ this.hairDrawsOverNonEyes.push({
1839
+ count: matCount,
1840
+ firstIndex: runningFirstIndex,
1841
+ bindGroup: bindGroupOverNonEyes,
1738
1842
  isTransparent,
1739
1843
  })
1740
1844
  } else if (isTransparent) {
@@ -1910,50 +2014,61 @@ export class Engine {
1910
2014
  }
1911
2015
  }
1912
2016
 
1913
- // PASS 3a: Hair over eyes (stencil == 1, multiply blend) - Draw hair geometry first to establish depth
1914
- pass.setPipeline(this.hairMultiplyPipeline)
1915
- pass.setStencilReference(1) // Check against stencil value 1
1916
- for (const draw of this.hairDraws) {
1917
- if (draw.count > 0) {
1918
- pass.setBindGroup(0, draw.bindGroup)
1919
- pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0)
1920
- this.drawCallCount++
2017
+ // PASS 3: Hair rendering - optimized single pass approach
2018
+ // Since both hair passes use the same shader, we batch them together
2019
+ // but still need separate passes due to stencil requirements (equal vs not-equal)
2020
+
2021
+ this.drawOutlines(pass, false) // Opaque outlines
2022
+
2023
+ // 3a: Hair over eyes (stencil == 1, alphaMultiplier = 0.5)
2024
+ if (this.hairDrawsOverEyes.length > 0) {
2025
+ pass.setPipeline(this.hairMultiplyPipeline)
2026
+ pass.setStencilReference(1)
2027
+ for (const draw of this.hairDrawsOverEyes) {
2028
+ if (draw.count > 0) {
2029
+ pass.setBindGroup(0, draw.bindGroup)
2030
+ pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0)
2031
+ this.drawCallCount++
2032
+ }
1921
2033
  }
1922
2034
  }
1923
2035
 
1924
- // PASS 3a.5: Hair outlines over eyes (stencil == 1, depth test to only draw near hair)
1925
- pass.setPipeline(this.hairOutlineOverEyesPipeline)
1926
- pass.setStencilReference(1) // Check against stencil value 1 (with equal test)
1927
- for (const draw of this.hairOutlineDraws) {
1928
- if (draw.count > 0) {
1929
- pass.setBindGroup(0, draw.bindGroup)
1930
- pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0)
2036
+ // 3b: Hair over non-eyes (stencil != 1, alphaMultiplier = 1.0)
2037
+ if (this.hairDrawsOverNonEyes.length > 0) {
2038
+ pass.setPipeline(this.hairOpaquePipeline)
2039
+ pass.setStencilReference(1)
2040
+ for (const draw of this.hairDrawsOverNonEyes) {
2041
+ if (draw.count > 0) {
2042
+ pass.setBindGroup(0, draw.bindGroup)
2043
+ pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0)
2044
+ this.drawCallCount++
2045
+ }
1931
2046
  }
1932
2047
  }
1933
2048
 
1934
- // PASS 3b: Hair over non-eyes (stencil != 1, opaque)
1935
- pass.setPipeline(this.hairOpaquePipeline)
1936
- pass.setStencilReference(1) // Check against stencil value 1 (with not-equal test)
1937
- for (const draw of this.hairDraws) {
1938
- if (draw.count > 0) {
1939
- pass.setBindGroup(0, draw.bindGroup)
1940
- pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0)
1941
- this.drawCallCount++
2049
+ // 3c: Hair outlines - batched together, only draw if outlines exist
2050
+ if (this.hairOutlineDraws.length > 0) {
2051
+ // Over eyes
2052
+ pass.setPipeline(this.hairOutlineOverEyesPipeline)
2053
+ pass.setStencilReference(1)
2054
+ for (const draw of this.hairOutlineDraws) {
2055
+ if (draw.count > 0) {
2056
+ pass.setBindGroup(0, draw.bindGroup)
2057
+ pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0)
2058
+ }
1942
2059
  }
1943
- }
1944
2060
 
1945
- // PASS 3b.5: Hair outlines over non-eyes (stencil != 1) - Draw hair outlines after hair geometry, so they only appear where hair exists
1946
- pass.setPipeline(this.hairOutlinePipeline)
1947
- pass.setStencilReference(1) // Check against stencil value 1 (with not-equal test)
1948
- for (const draw of this.hairOutlineDraws) {
1949
- if (draw.count > 0) {
1950
- pass.setBindGroup(0, draw.bindGroup)
1951
- pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0)
2061
+ // Over non-eyes
2062
+ pass.setPipeline(this.hairOutlinePipeline)
2063
+ pass.setStencilReference(1)
2064
+ for (const draw of this.hairOutlineDraws) {
2065
+ if (draw.count > 0) {
2066
+ pass.setBindGroup(0, draw.bindGroup)
2067
+ pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0)
2068
+ }
1952
2069
  }
1953
2070
  }
1954
2071
 
1955
- this.drawOutlines(pass, false) // Opaque outlines
1956
-
1957
2072
  // PASS 4: Transparent non-eye, non-hair
1958
2073
  pass.setPipeline(this.pipeline)
1959
2074
  for (const draw of this.transparentNonEyeNonHairDraws) {
@@ -2282,7 +2397,8 @@ export class Engine {
2282
2397
  const totalMaterialDraws =
2283
2398
  this.opaqueNonEyeNonHairDraws.length +
2284
2399
  this.eyeDraws.length +
2285
- this.hairDraws.length +
2400
+ this.hairDrawsOverEyes.length +
2401
+ this.hairDrawsOverNonEyes.length +
2286
2402
  this.transparentNonEyeNonHairDraws.length
2287
2403
  bufferMemoryBytes += totalMaterialDraws * 4 // Material uniform buffers
2288
2404