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 +13 -8
- package/dist/engine.d.ts +5 -2
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +167 -65
- package/package.json +5 -1
- package/src/engine.ts +182 -66
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
|
|
96
|
+
private hairDrawsOverEyes;
|
|
97
|
+
private hairDrawsOverNonEyes;
|
|
95
98
|
private transparentNonEyeNonHairDraws;
|
|
96
99
|
private opaqueNonEyeNonHairOutlineDraws;
|
|
97
100
|
private eyeOutlineDraws;
|
package/dist/engine.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"engine.d.ts","sourceRoot":"","sources":["../src/engine.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,UAAU,CAAA;AACjC,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAA;AAKnC,MAAM,WAAW,WAAW;IAC1B,GAAG,EAAE,MAAM,CAAA;IACX,SAAS,EAAE,MAAM,CAAA;IACjB,SAAS,EAAE,MAAM,CAAA;CAClB;AAED,qBAAa,MAAM;IACjB,OAAO,CAAC,MAAM,CAAmB;IACjC,OAAO,CAAC,MAAM,CAAY;IAC1B,OAAO,CAAC,OAAO,CAAmB;IAClC,OAAO,CAAC,kBAAkB,CAAmB;IACtC,MAAM,EAAG,MAAM,CAAA;IACtB,OAAO,CAAC,mBAAmB,CAAY;IACvC,OAAO,CAAC,gBAAgB,CAAuB;IAC/C,OAAO,CAAC,kBAAkB,CAAY;IACtC,OAAO,CAAC,SAAS,CAAuB;IACxC,OAAO,CAAC,UAAU,CAAI;IACtB,OAAO,CAAC,YAAY,CAAY;IAChC,OAAO,CAAC,WAAW,
|
|
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.
|
|
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
|
-
|
|
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
|
-
//
|
|
189
|
-
|
|
190
|
-
|
|
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
|
-
|
|
289
|
-
let
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
|
712
|
+
label: "hair pipeline (over eyes)",
|
|
694
713
|
layout: sharedPipelineLayout,
|
|
695
714
|
vertex: {
|
|
696
|
-
module:
|
|
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:
|
|
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
|
|
777
|
+
label: "hair pipeline (over non-eyes)",
|
|
760
778
|
layout: sharedPipelineLayout,
|
|
761
779
|
vertex: {
|
|
762
|
-
module:
|
|
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:
|
|
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.
|
|
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
|
-
|
|
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] =
|
|
1508
|
-
materialUniformData[2] =
|
|
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
|
-
|
|
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
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
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
|
-
//
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
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
|
-
//
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
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
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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
|
-
//
|
|
244
|
-
|
|
245
|
-
|
|
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
|
-
|
|
344
|
-
let
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
|
777
|
+
label: "hair pipeline (over eyes)",
|
|
760
778
|
layout: sharedPipelineLayout,
|
|
761
779
|
vertex: {
|
|
762
|
-
module:
|
|
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:
|
|
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
|
|
843
|
+
label: "hair pipeline (over non-eyes)",
|
|
827
844
|
layout: sharedPipelineLayout,
|
|
828
845
|
vertex: {
|
|
829
|
-
module:
|
|
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:
|
|
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
|
|
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.
|
|
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
|
-
|
|
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] =
|
|
1699
|
-
materialUniformData[2] =
|
|
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
|
-
|
|
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
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
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
|
-
//
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
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
|
-
//
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
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
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
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.
|
|
2400
|
+
this.hairDrawsOverEyes.length +
|
|
2401
|
+
this.hairDrawsOverNonEyes.length +
|
|
2286
2402
|
this.transparentNonEyeNonHairDraws.length
|
|
2287
2403
|
bufferMemoryBytes += totalMaterialDraws * 4 // Material uniform buffers
|
|
2288
2404
|
|