reze-engine 0.1.5 → 0.1.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/engine.d.ts +15 -3
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +696 -46
- package/dist/model.d.ts +3 -0
- package/dist/model.d.ts.map +1 -1
- package/dist/pmx-loader.d.ts.map +1 -1
- package/dist/pmx-loader.js +20 -2
- package/package.json +1 -1
- package/src/engine.ts +740 -47
- package/src/model.ts +3 -0
- package/src/pmx-loader.ts +25 -2
package/src/engine.ts
CHANGED
|
@@ -28,6 +28,13 @@ export class Engine {
|
|
|
28
28
|
private depthTexture!: GPUTexture
|
|
29
29
|
private pipeline!: GPURenderPipeline
|
|
30
30
|
private outlinePipeline!: GPURenderPipeline
|
|
31
|
+
private hairOutlinePipeline!: GPURenderPipeline
|
|
32
|
+
private hairOutlineOverEyesPipeline!: GPURenderPipeline
|
|
33
|
+
private hairMultiplyPipeline!: GPURenderPipeline
|
|
34
|
+
private hairOpaquePipeline!: GPURenderPipeline
|
|
35
|
+
private eyePipeline!: GPURenderPipeline
|
|
36
|
+
private hairBindGroupLayout!: GPUBindGroupLayout
|
|
37
|
+
private outlineBindGroupLayout!: GPUBindGroupLayout
|
|
31
38
|
private jointsBuffer!: GPUBuffer
|
|
32
39
|
private weightsBuffer!: GPUBuffer
|
|
33
40
|
private skinMatrixBuffer?: GPUBuffer
|
|
@@ -212,10 +219,146 @@ export class Engine {
|
|
|
212
219
|
`,
|
|
213
220
|
})
|
|
214
221
|
|
|
222
|
+
// Create a separate shader for hair-over-eyes that outputs pre-multiplied color for darkening effect
|
|
223
|
+
const hairMultiplyShaderModule = this.device.createShaderModule({
|
|
224
|
+
label: "hair multiply shaders",
|
|
225
|
+
code: /* wgsl */ `
|
|
226
|
+
struct CameraUniforms {
|
|
227
|
+
view: mat4x4f,
|
|
228
|
+
projection: mat4x4f,
|
|
229
|
+
viewPos: vec3f,
|
|
230
|
+
_padding: f32,
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
struct Light {
|
|
234
|
+
direction: vec3f,
|
|
235
|
+
_padding1: f32,
|
|
236
|
+
color: vec3f,
|
|
237
|
+
intensity: f32,
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
struct LightUniforms {
|
|
241
|
+
ambient: f32,
|
|
242
|
+
lightCount: f32,
|
|
243
|
+
_padding1: f32,
|
|
244
|
+
_padding2: f32,
|
|
245
|
+
lights: array<Light, 4>,
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
struct MaterialUniforms {
|
|
249
|
+
alpha: f32,
|
|
250
|
+
_padding1: f32,
|
|
251
|
+
_padding2: f32,
|
|
252
|
+
_padding3: f32,
|
|
253
|
+
};
|
|
254
|
+
|
|
255
|
+
struct VertexOutput {
|
|
256
|
+
@builtin(position) position: vec4f,
|
|
257
|
+
@location(0) normal: vec3f,
|
|
258
|
+
@location(1) uv: vec2f,
|
|
259
|
+
@location(2) worldPos: vec3f,
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
@group(0) @binding(0) var<uniform> camera: CameraUniforms;
|
|
263
|
+
@group(0) @binding(1) var<uniform> light: LightUniforms;
|
|
264
|
+
@group(0) @binding(2) var diffuseTexture: texture_2d<f32>;
|
|
265
|
+
@group(0) @binding(3) var diffuseSampler: sampler;
|
|
266
|
+
@group(0) @binding(4) var<storage, read> skinMats: array<mat4x4f>;
|
|
267
|
+
@group(0) @binding(5) var toonTexture: texture_2d<f32>;
|
|
268
|
+
@group(0) @binding(6) var toonSampler: sampler;
|
|
269
|
+
@group(0) @binding(7) var<uniform> material: MaterialUniforms;
|
|
270
|
+
|
|
271
|
+
@vertex fn vs(
|
|
272
|
+
@location(0) position: vec3f,
|
|
273
|
+
@location(1) normal: vec3f,
|
|
274
|
+
@location(2) uv: vec2f,
|
|
275
|
+
@location(3) joints0: vec4<u32>,
|
|
276
|
+
@location(4) weights0: vec4<f32>
|
|
277
|
+
) -> VertexOutput {
|
|
278
|
+
var output: VertexOutput;
|
|
279
|
+
let pos4 = vec4f(position, 1.0);
|
|
280
|
+
|
|
281
|
+
let weightSum = weights0.x + weights0.y + weights0.z + weights0.w;
|
|
282
|
+
var normalizedWeights: vec4f;
|
|
283
|
+
if (weightSum > 0.0001) {
|
|
284
|
+
normalizedWeights = weights0 / weightSum;
|
|
285
|
+
} else {
|
|
286
|
+
normalizedWeights = vec4f(1.0, 0.0, 0.0, 0.0);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
var skinnedPos = vec4f(0.0, 0.0, 0.0, 0.0);
|
|
290
|
+
var skinnedNrm = vec3f(0.0, 0.0, 0.0);
|
|
291
|
+
for (var i = 0u; i < 4u; i++) {
|
|
292
|
+
let j = joints0[i];
|
|
293
|
+
let w = normalizedWeights[i];
|
|
294
|
+
let m = skinMats[j];
|
|
295
|
+
skinnedPos += (m * pos4) * w;
|
|
296
|
+
let r3 = mat3x3f(m[0].xyz, m[1].xyz, m[2].xyz);
|
|
297
|
+
skinnedNrm += (r3 * normal) * w;
|
|
298
|
+
}
|
|
299
|
+
let worldPos = skinnedPos.xyz;
|
|
300
|
+
output.position = camera.projection * camera.view * vec4f(worldPos, 1.0);
|
|
301
|
+
output.normal = normalize(skinnedNrm);
|
|
302
|
+
output.uv = uv;
|
|
303
|
+
output.worldPos = worldPos;
|
|
304
|
+
return output;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
@fragment fn fs(input: VertexOutput) -> @location(0) vec4f {
|
|
308
|
+
let n = normalize(input.normal);
|
|
309
|
+
let albedo = textureSample(diffuseTexture, diffuseSampler, input.uv).rgb;
|
|
310
|
+
|
|
311
|
+
var lightAccum = vec3f(light.ambient);
|
|
312
|
+
let numLights = u32(light.lightCount);
|
|
313
|
+
for (var i = 0u; i < numLights; i++) {
|
|
314
|
+
let l = -light.lights[i].direction;
|
|
315
|
+
let nDotL = max(dot(n, l), 0.0);
|
|
316
|
+
let toonUV = vec2f(nDotL, 0.5);
|
|
317
|
+
let toonFactor = textureSample(toonTexture, toonSampler, toonUV).rgb;
|
|
318
|
+
let radiance = light.lights[i].color * light.lights[i].intensity;
|
|
319
|
+
lightAccum += toonFactor * radiance * nDotL;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
let color = albedo * lightAccum;
|
|
323
|
+
let finalAlpha = material.alpha;
|
|
324
|
+
if (finalAlpha < 0.001) {
|
|
325
|
+
discard;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// For hair-over-eyes effect: simple half-transparent overlay
|
|
329
|
+
// Use 50% opacity to create a semi-transparent hair color overlay
|
|
330
|
+
let overlayAlpha = finalAlpha * 0.5;
|
|
331
|
+
|
|
332
|
+
return vec4f(clamp(color, vec3f(0.0), vec3f(1.0)), overlayAlpha);
|
|
333
|
+
}
|
|
334
|
+
`,
|
|
335
|
+
})
|
|
336
|
+
|
|
337
|
+
// Create explicit bind group layout for all pipelines using the main shader
|
|
338
|
+
// This ensures compatibility across all pipelines (main, eye, hair multiply, hair opaque)
|
|
339
|
+
this.hairBindGroupLayout = this.device.createBindGroupLayout({
|
|
340
|
+
label: "shared material bind group layout",
|
|
341
|
+
entries: [
|
|
342
|
+
{ binding: 0, visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT, buffer: { type: "uniform" } }, // camera
|
|
343
|
+
{ binding: 1, visibility: GPUShaderStage.FRAGMENT, buffer: { type: "uniform" } }, // light
|
|
344
|
+
{ binding: 2, visibility: GPUShaderStage.FRAGMENT, texture: {} }, // diffuseTexture
|
|
345
|
+
{ binding: 3, visibility: GPUShaderStage.FRAGMENT, sampler: {} }, // diffuseSampler
|
|
346
|
+
{ binding: 4, visibility: GPUShaderStage.VERTEX, buffer: { type: "read-only-storage" } }, // skinMats
|
|
347
|
+
{ binding: 5, visibility: GPUShaderStage.FRAGMENT, texture: {} }, // toonTexture
|
|
348
|
+
{ binding: 6, visibility: GPUShaderStage.FRAGMENT, sampler: {} }, // toonSampler
|
|
349
|
+
{ binding: 7, visibility: GPUShaderStage.FRAGMENT, buffer: { type: "uniform" } }, // material
|
|
350
|
+
],
|
|
351
|
+
})
|
|
352
|
+
|
|
353
|
+
const sharedPipelineLayout = this.device.createPipelineLayout({
|
|
354
|
+
label: "shared pipeline layout",
|
|
355
|
+
bindGroupLayouts: [this.hairBindGroupLayout],
|
|
356
|
+
})
|
|
357
|
+
|
|
215
358
|
// Single pipeline for all materials with alpha blending
|
|
216
359
|
this.pipeline = this.device.createRenderPipeline({
|
|
217
360
|
label: "model pipeline",
|
|
218
|
-
layout:
|
|
361
|
+
layout: sharedPipelineLayout,
|
|
219
362
|
vertex: {
|
|
220
363
|
module: shaderModule,
|
|
221
364
|
buffers: [
|
|
@@ -259,7 +402,7 @@ export class Engine {
|
|
|
259
402
|
},
|
|
260
403
|
primitive: { cullMode: "none" },
|
|
261
404
|
depthStencil: {
|
|
262
|
-
format: "depth24plus",
|
|
405
|
+
format: "depth24plus-stencil8",
|
|
263
406
|
depthWriteEnabled: true,
|
|
264
407
|
depthCompare: "less",
|
|
265
408
|
},
|
|
@@ -268,6 +411,21 @@ export class Engine {
|
|
|
268
411
|
},
|
|
269
412
|
})
|
|
270
413
|
|
|
414
|
+
// Create bind group layout for outline pipelines
|
|
415
|
+
this.outlineBindGroupLayout = this.device.createBindGroupLayout({
|
|
416
|
+
label: "outline bind group layout",
|
|
417
|
+
entries: [
|
|
418
|
+
{ binding: 0, visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT, buffer: { type: "uniform" } }, // camera
|
|
419
|
+
{ binding: 1, visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT, buffer: { type: "uniform" } }, // material
|
|
420
|
+
{ binding: 2, visibility: GPUShaderStage.VERTEX, buffer: { type: "read-only-storage" } }, // skinMats
|
|
421
|
+
],
|
|
422
|
+
})
|
|
423
|
+
|
|
424
|
+
const outlinePipelineLayout = this.device.createPipelineLayout({
|
|
425
|
+
label: "outline pipeline layout",
|
|
426
|
+
bindGroupLayouts: [this.outlineBindGroupLayout],
|
|
427
|
+
})
|
|
428
|
+
|
|
271
429
|
const outlineShaderModule = this.device.createShaderModule({
|
|
272
430
|
label: "outline shaders",
|
|
273
431
|
code: /* wgsl */ `
|
|
@@ -341,7 +499,7 @@ export class Engine {
|
|
|
341
499
|
|
|
342
500
|
this.outlinePipeline = this.device.createRenderPipeline({
|
|
343
501
|
label: "outline pipeline",
|
|
344
|
-
layout:
|
|
502
|
+
layout: outlinePipelineLayout,
|
|
345
503
|
vertex: {
|
|
346
504
|
module: outlineShaderModule,
|
|
347
505
|
buffers: [
|
|
@@ -399,7 +557,7 @@ export class Engine {
|
|
|
399
557
|
cullMode: "back",
|
|
400
558
|
},
|
|
401
559
|
depthStencil: {
|
|
402
|
-
format: "depth24plus",
|
|
560
|
+
format: "depth24plus-stencil8",
|
|
403
561
|
depthWriteEnabled: true,
|
|
404
562
|
depthCompare: "less",
|
|
405
563
|
},
|
|
@@ -407,6 +565,376 @@ export class Engine {
|
|
|
407
565
|
count: this.sampleCount,
|
|
408
566
|
},
|
|
409
567
|
})
|
|
568
|
+
|
|
569
|
+
// Hair outline pipeline: draws hair outlines over non-eyes (stencil != 1)
|
|
570
|
+
// Drawn after hair geometry, so depth testing ensures outlines only appear where hair exists
|
|
571
|
+
this.hairOutlinePipeline = this.device.createRenderPipeline({
|
|
572
|
+
label: "hair outline pipeline",
|
|
573
|
+
layout: outlinePipelineLayout,
|
|
574
|
+
vertex: {
|
|
575
|
+
module: outlineShaderModule,
|
|
576
|
+
buffers: [
|
|
577
|
+
{
|
|
578
|
+
arrayStride: 8 * 4,
|
|
579
|
+
attributes: [
|
|
580
|
+
{
|
|
581
|
+
shaderLocation: 0,
|
|
582
|
+
offset: 0,
|
|
583
|
+
format: "float32x3" as GPUVertexFormat,
|
|
584
|
+
},
|
|
585
|
+
{
|
|
586
|
+
shaderLocation: 1,
|
|
587
|
+
offset: 3 * 4,
|
|
588
|
+
format: "float32x3" as GPUVertexFormat,
|
|
589
|
+
},
|
|
590
|
+
{
|
|
591
|
+
shaderLocation: 2,
|
|
592
|
+
offset: 6 * 4,
|
|
593
|
+
format: "float32x2" as GPUVertexFormat,
|
|
594
|
+
},
|
|
595
|
+
],
|
|
596
|
+
},
|
|
597
|
+
{
|
|
598
|
+
arrayStride: 4 * 2,
|
|
599
|
+
attributes: [{ shaderLocation: 3, offset: 0, format: "uint16x4" as GPUVertexFormat }],
|
|
600
|
+
},
|
|
601
|
+
{
|
|
602
|
+
arrayStride: 4,
|
|
603
|
+
attributes: [{ shaderLocation: 4, offset: 0, format: "unorm8x4" as GPUVertexFormat }],
|
|
604
|
+
},
|
|
605
|
+
],
|
|
606
|
+
},
|
|
607
|
+
fragment: {
|
|
608
|
+
module: outlineShaderModule,
|
|
609
|
+
targets: [
|
|
610
|
+
{
|
|
611
|
+
format: this.presentationFormat,
|
|
612
|
+
blend: {
|
|
613
|
+
color: {
|
|
614
|
+
srcFactor: "src-alpha",
|
|
615
|
+
dstFactor: "one-minus-src-alpha",
|
|
616
|
+
operation: "add",
|
|
617
|
+
},
|
|
618
|
+
alpha: {
|
|
619
|
+
srcFactor: "one",
|
|
620
|
+
dstFactor: "one-minus-src-alpha",
|
|
621
|
+
operation: "add",
|
|
622
|
+
},
|
|
623
|
+
},
|
|
624
|
+
},
|
|
625
|
+
],
|
|
626
|
+
},
|
|
627
|
+
primitive: {
|
|
628
|
+
cullMode: "back",
|
|
629
|
+
},
|
|
630
|
+
depthStencil: {
|
|
631
|
+
format: "depth24plus-stencil8",
|
|
632
|
+
depthWriteEnabled: false, // Don't write depth - let hair geometry control depth
|
|
633
|
+
depthCompare: "less-equal", // Only draw where hair depth exists
|
|
634
|
+
stencilFront: {
|
|
635
|
+
compare: "not-equal", // Only render where stencil != 1 (not over eyes)
|
|
636
|
+
failOp: "keep",
|
|
637
|
+
depthFailOp: "keep",
|
|
638
|
+
passOp: "keep",
|
|
639
|
+
},
|
|
640
|
+
stencilBack: {
|
|
641
|
+
compare: "not-equal",
|
|
642
|
+
failOp: "keep",
|
|
643
|
+
depthFailOp: "keep",
|
|
644
|
+
passOp: "keep",
|
|
645
|
+
},
|
|
646
|
+
},
|
|
647
|
+
multisample: {
|
|
648
|
+
count: this.sampleCount,
|
|
649
|
+
},
|
|
650
|
+
})
|
|
651
|
+
|
|
652
|
+
// Hair outline pipeline for over eyes: draws where stencil == 1, but only where hair depth exists
|
|
653
|
+
// Uses depth compare "equal" with a small bias to only appear where hair geometry exists
|
|
654
|
+
this.hairOutlineOverEyesPipeline = this.device.createRenderPipeline({
|
|
655
|
+
label: "hair outline over eyes pipeline",
|
|
656
|
+
layout: outlinePipelineLayout,
|
|
657
|
+
vertex: {
|
|
658
|
+
module: outlineShaderModule,
|
|
659
|
+
buffers: [
|
|
660
|
+
{
|
|
661
|
+
arrayStride: 8 * 4,
|
|
662
|
+
attributes: [
|
|
663
|
+
{
|
|
664
|
+
shaderLocation: 0,
|
|
665
|
+
offset: 0,
|
|
666
|
+
format: "float32x3" as GPUVertexFormat,
|
|
667
|
+
},
|
|
668
|
+
{
|
|
669
|
+
shaderLocation: 1,
|
|
670
|
+
offset: 3 * 4,
|
|
671
|
+
format: "float32x3" as GPUVertexFormat,
|
|
672
|
+
},
|
|
673
|
+
{
|
|
674
|
+
shaderLocation: 2,
|
|
675
|
+
offset: 6 * 4,
|
|
676
|
+
format: "float32x2" as GPUVertexFormat,
|
|
677
|
+
},
|
|
678
|
+
],
|
|
679
|
+
},
|
|
680
|
+
{
|
|
681
|
+
arrayStride: 4 * 2,
|
|
682
|
+
attributes: [{ shaderLocation: 3, offset: 0, format: "uint16x4" as GPUVertexFormat }],
|
|
683
|
+
},
|
|
684
|
+
{
|
|
685
|
+
arrayStride: 4,
|
|
686
|
+
attributes: [{ shaderLocation: 4, offset: 0, format: "unorm8x4" as GPUVertexFormat }],
|
|
687
|
+
},
|
|
688
|
+
],
|
|
689
|
+
},
|
|
690
|
+
fragment: {
|
|
691
|
+
module: outlineShaderModule,
|
|
692
|
+
targets: [
|
|
693
|
+
{
|
|
694
|
+
format: this.presentationFormat,
|
|
695
|
+
blend: {
|
|
696
|
+
color: {
|
|
697
|
+
srcFactor: "src-alpha",
|
|
698
|
+
dstFactor: "one-minus-src-alpha",
|
|
699
|
+
operation: "add",
|
|
700
|
+
},
|
|
701
|
+
alpha: {
|
|
702
|
+
srcFactor: "one",
|
|
703
|
+
dstFactor: "one-minus-src-alpha",
|
|
704
|
+
operation: "add",
|
|
705
|
+
},
|
|
706
|
+
},
|
|
707
|
+
},
|
|
708
|
+
],
|
|
709
|
+
},
|
|
710
|
+
primitive: {
|
|
711
|
+
cullMode: "back",
|
|
712
|
+
},
|
|
713
|
+
depthStencil: {
|
|
714
|
+
format: "depth24plus-stencil8",
|
|
715
|
+
depthWriteEnabled: false, // Don't write depth
|
|
716
|
+
|
|
717
|
+
depthCompare: "less-equal", // Draw where outline depth <= existing depth (hair depth)
|
|
718
|
+
depthBias: -0.0001, // Small negative bias to bring outline slightly closer for depth test
|
|
719
|
+
depthBiasSlopeScale: 0.0,
|
|
720
|
+
depthBiasClamp: 0.0,
|
|
721
|
+
stencilFront: {
|
|
722
|
+
compare: "equal", // Only render where stencil == 1 (over eyes)
|
|
723
|
+
failOp: "keep",
|
|
724
|
+
depthFailOp: "keep",
|
|
725
|
+
passOp: "keep",
|
|
726
|
+
},
|
|
727
|
+
stencilBack: {
|
|
728
|
+
compare: "equal",
|
|
729
|
+
failOp: "keep",
|
|
730
|
+
depthFailOp: "keep",
|
|
731
|
+
passOp: "keep",
|
|
732
|
+
},
|
|
733
|
+
},
|
|
734
|
+
multisample: {
|
|
735
|
+
count: this.sampleCount,
|
|
736
|
+
},
|
|
737
|
+
})
|
|
738
|
+
|
|
739
|
+
// Hair pipeline with multiplicative blending (for hair over eyes)
|
|
740
|
+
this.hairMultiplyPipeline = this.device.createRenderPipeline({
|
|
741
|
+
label: "hair multiply pipeline",
|
|
742
|
+
layout: sharedPipelineLayout,
|
|
743
|
+
vertex: {
|
|
744
|
+
module: hairMultiplyShaderModule,
|
|
745
|
+
buffers: [
|
|
746
|
+
{
|
|
747
|
+
arrayStride: 8 * 4,
|
|
748
|
+
attributes: [
|
|
749
|
+
{ shaderLocation: 0, offset: 0, format: "float32x3" as GPUVertexFormat },
|
|
750
|
+
{ shaderLocation: 1, offset: 3 * 4, format: "float32x3" as GPUVertexFormat },
|
|
751
|
+
{ shaderLocation: 2, offset: 6 * 4, format: "float32x2" as GPUVertexFormat },
|
|
752
|
+
],
|
|
753
|
+
},
|
|
754
|
+
{
|
|
755
|
+
arrayStride: 4 * 2,
|
|
756
|
+
attributes: [{ shaderLocation: 3, offset: 0, format: "uint16x4" as GPUVertexFormat }],
|
|
757
|
+
},
|
|
758
|
+
{
|
|
759
|
+
arrayStride: 4,
|
|
760
|
+
attributes: [{ shaderLocation: 4, offset: 0, format: "unorm8x4" as GPUVertexFormat }],
|
|
761
|
+
},
|
|
762
|
+
],
|
|
763
|
+
},
|
|
764
|
+
fragment: {
|
|
765
|
+
module: hairMultiplyShaderModule,
|
|
766
|
+
targets: [
|
|
767
|
+
{
|
|
768
|
+
format: this.presentationFormat,
|
|
769
|
+
blend: {
|
|
770
|
+
color: {
|
|
771
|
+
// Simple half-transparent overlay effect
|
|
772
|
+
// Blend: hairColor * overlayAlpha + eyeColor * (1 - overlayAlpha)
|
|
773
|
+
srcFactor: "src-alpha",
|
|
774
|
+
dstFactor: "one-minus-src-alpha",
|
|
775
|
+
operation: "add",
|
|
776
|
+
},
|
|
777
|
+
alpha: {
|
|
778
|
+
srcFactor: "one",
|
|
779
|
+
dstFactor: "one-minus-src-alpha",
|
|
780
|
+
operation: "add",
|
|
781
|
+
},
|
|
782
|
+
},
|
|
783
|
+
},
|
|
784
|
+
],
|
|
785
|
+
},
|
|
786
|
+
primitive: { cullMode: "none" },
|
|
787
|
+
depthStencil: {
|
|
788
|
+
format: "depth24plus-stencil8",
|
|
789
|
+
depthWriteEnabled: true, // Write depth so outlines can test against it
|
|
790
|
+
depthCompare: "less",
|
|
791
|
+
stencilFront: {
|
|
792
|
+
compare: "equal", // Only render where stencil == 1
|
|
793
|
+
failOp: "keep",
|
|
794
|
+
depthFailOp: "keep",
|
|
795
|
+
passOp: "keep",
|
|
796
|
+
},
|
|
797
|
+
stencilBack: {
|
|
798
|
+
compare: "equal",
|
|
799
|
+
failOp: "keep",
|
|
800
|
+
depthFailOp: "keep",
|
|
801
|
+
passOp: "keep",
|
|
802
|
+
},
|
|
803
|
+
},
|
|
804
|
+
multisample: { count: this.sampleCount },
|
|
805
|
+
})
|
|
806
|
+
|
|
807
|
+
// Hair pipeline for opaque rendering (hair over non-eyes)
|
|
808
|
+
this.hairOpaquePipeline = this.device.createRenderPipeline({
|
|
809
|
+
label: "hair opaque pipeline",
|
|
810
|
+
layout: sharedPipelineLayout,
|
|
811
|
+
vertex: {
|
|
812
|
+
module: shaderModule,
|
|
813
|
+
buffers: [
|
|
814
|
+
{
|
|
815
|
+
arrayStride: 8 * 4,
|
|
816
|
+
attributes: [
|
|
817
|
+
{ shaderLocation: 0, offset: 0, format: "float32x3" as GPUVertexFormat },
|
|
818
|
+
{ shaderLocation: 1, offset: 3 * 4, format: "float32x3" as GPUVertexFormat },
|
|
819
|
+
{ shaderLocation: 2, offset: 6 * 4, format: "float32x2" as GPUVertexFormat },
|
|
820
|
+
],
|
|
821
|
+
},
|
|
822
|
+
{
|
|
823
|
+
arrayStride: 4 * 2,
|
|
824
|
+
attributes: [{ shaderLocation: 3, offset: 0, format: "uint16x4" as GPUVertexFormat }],
|
|
825
|
+
},
|
|
826
|
+
{
|
|
827
|
+
arrayStride: 4,
|
|
828
|
+
attributes: [{ shaderLocation: 4, offset: 0, format: "unorm8x4" as GPUVertexFormat }],
|
|
829
|
+
},
|
|
830
|
+
],
|
|
831
|
+
},
|
|
832
|
+
fragment: {
|
|
833
|
+
module: shaderModule,
|
|
834
|
+
targets: [
|
|
835
|
+
{
|
|
836
|
+
format: this.presentationFormat,
|
|
837
|
+
blend: {
|
|
838
|
+
color: {
|
|
839
|
+
srcFactor: "src-alpha",
|
|
840
|
+
dstFactor: "one-minus-src-alpha",
|
|
841
|
+
operation: "add",
|
|
842
|
+
},
|
|
843
|
+
alpha: {
|
|
844
|
+
srcFactor: "one",
|
|
845
|
+
dstFactor: "one-minus-src-alpha",
|
|
846
|
+
operation: "add",
|
|
847
|
+
},
|
|
848
|
+
},
|
|
849
|
+
},
|
|
850
|
+
],
|
|
851
|
+
},
|
|
852
|
+
primitive: { cullMode: "none" },
|
|
853
|
+
depthStencil: {
|
|
854
|
+
format: "depth24plus-stencil8",
|
|
855
|
+
depthWriteEnabled: true,
|
|
856
|
+
depthCompare: "less",
|
|
857
|
+
stencilFront: {
|
|
858
|
+
compare: "not-equal", // Only render where stencil != 1
|
|
859
|
+
failOp: "keep",
|
|
860
|
+
depthFailOp: "keep",
|
|
861
|
+
passOp: "keep",
|
|
862
|
+
},
|
|
863
|
+
stencilBack: {
|
|
864
|
+
compare: "not-equal",
|
|
865
|
+
failOp: "keep",
|
|
866
|
+
depthFailOp: "keep",
|
|
867
|
+
passOp: "keep",
|
|
868
|
+
},
|
|
869
|
+
},
|
|
870
|
+
multisample: { count: this.sampleCount },
|
|
871
|
+
})
|
|
872
|
+
|
|
873
|
+
// Eye overlay pipeline (renders after opaque, writes stencil)
|
|
874
|
+
this.eyePipeline = this.device.createRenderPipeline({
|
|
875
|
+
label: "eye overlay pipeline",
|
|
876
|
+
layout: sharedPipelineLayout,
|
|
877
|
+
vertex: {
|
|
878
|
+
module: shaderModule,
|
|
879
|
+
buffers: [
|
|
880
|
+
{
|
|
881
|
+
arrayStride: 8 * 4,
|
|
882
|
+
attributes: [
|
|
883
|
+
{ shaderLocation: 0, offset: 0, format: "float32x3" as GPUVertexFormat },
|
|
884
|
+
{ shaderLocation: 1, offset: 3 * 4, format: "float32x3" as GPUVertexFormat },
|
|
885
|
+
{ shaderLocation: 2, offset: 6 * 4, format: "float32x2" as GPUVertexFormat },
|
|
886
|
+
],
|
|
887
|
+
},
|
|
888
|
+
{
|
|
889
|
+
arrayStride: 4 * 2,
|
|
890
|
+
attributes: [{ shaderLocation: 3, offset: 0, format: "uint16x4" as GPUVertexFormat }],
|
|
891
|
+
},
|
|
892
|
+
{
|
|
893
|
+
arrayStride: 4,
|
|
894
|
+
attributes: [{ shaderLocation: 4, offset: 0, format: "unorm8x4" as GPUVertexFormat }],
|
|
895
|
+
},
|
|
896
|
+
],
|
|
897
|
+
},
|
|
898
|
+
fragment: {
|
|
899
|
+
module: shaderModule,
|
|
900
|
+
targets: [
|
|
901
|
+
{
|
|
902
|
+
format: this.presentationFormat,
|
|
903
|
+
blend: {
|
|
904
|
+
color: {
|
|
905
|
+
srcFactor: "src-alpha",
|
|
906
|
+
dstFactor: "one-minus-src-alpha",
|
|
907
|
+
operation: "add",
|
|
908
|
+
},
|
|
909
|
+
alpha: {
|
|
910
|
+
srcFactor: "one",
|
|
911
|
+
dstFactor: "one-minus-src-alpha",
|
|
912
|
+
operation: "add",
|
|
913
|
+
},
|
|
914
|
+
},
|
|
915
|
+
},
|
|
916
|
+
],
|
|
917
|
+
},
|
|
918
|
+
primitive: { cullMode: "none" },
|
|
919
|
+
depthStencil: {
|
|
920
|
+
format: "depth24plus-stencil8",
|
|
921
|
+
depthWriteEnabled: false, // Don't write depth
|
|
922
|
+
depthCompare: "less", // Respect existing depth
|
|
923
|
+
stencilFront: {
|
|
924
|
+
compare: "always",
|
|
925
|
+
failOp: "keep",
|
|
926
|
+
depthFailOp: "keep",
|
|
927
|
+
passOp: "replace", // Write stencil value 1
|
|
928
|
+
},
|
|
929
|
+
stencilBack: {
|
|
930
|
+
compare: "always",
|
|
931
|
+
failOp: "keep",
|
|
932
|
+
depthFailOp: "keep",
|
|
933
|
+
passOp: "replace",
|
|
934
|
+
},
|
|
935
|
+
},
|
|
936
|
+
multisample: { count: this.sampleCount },
|
|
937
|
+
})
|
|
410
938
|
}
|
|
411
939
|
|
|
412
940
|
// Create compute shader for skin matrix computation
|
|
@@ -481,7 +1009,7 @@ export class Engine {
|
|
|
481
1009
|
label: "depth texture",
|
|
482
1010
|
size: [width, height],
|
|
483
1011
|
sampleCount: this.sampleCount,
|
|
484
|
-
format: "depth24plus",
|
|
1012
|
+
format: "depth24plus-stencil8",
|
|
485
1013
|
usage: GPUTextureUsage.RENDER_ATTACHMENT,
|
|
486
1014
|
})
|
|
487
1015
|
|
|
@@ -511,6 +1039,9 @@ export class Engine {
|
|
|
511
1039
|
depthClearValue: 1.0,
|
|
512
1040
|
depthLoadOp: "clear",
|
|
513
1041
|
depthStoreOp: "store",
|
|
1042
|
+
stencilClearValue: 0, // New: clear stencil to 0
|
|
1043
|
+
stencilLoadOp: "clear", // New: clear stencil each frame
|
|
1044
|
+
stencilStoreOp: "store", // New: store stencil
|
|
514
1045
|
},
|
|
515
1046
|
}
|
|
516
1047
|
|
|
@@ -722,8 +1253,35 @@ export class Engine {
|
|
|
722
1253
|
await this.setupMaterials(model)
|
|
723
1254
|
}
|
|
724
1255
|
|
|
725
|
-
private
|
|
726
|
-
|
|
1256
|
+
private opaqueNonEyeNonHairDraws: {
|
|
1257
|
+
count: number
|
|
1258
|
+
firstIndex: number
|
|
1259
|
+
bindGroup: GPUBindGroup
|
|
1260
|
+
isTransparent: boolean
|
|
1261
|
+
}[] = []
|
|
1262
|
+
private eyeDraws: { count: number; firstIndex: number; bindGroup: GPUBindGroup; isTransparent: boolean }[] = []
|
|
1263
|
+
private hairDraws: { count: number; firstIndex: number; bindGroup: GPUBindGroup; isTransparent: boolean }[] = []
|
|
1264
|
+
private transparentNonEyeNonHairDraws: {
|
|
1265
|
+
count: number
|
|
1266
|
+
firstIndex: number
|
|
1267
|
+
bindGroup: GPUBindGroup
|
|
1268
|
+
isTransparent: boolean
|
|
1269
|
+
}[] = []
|
|
1270
|
+
private opaqueNonEyeNonHairOutlineDraws: {
|
|
1271
|
+
count: number
|
|
1272
|
+
firstIndex: number
|
|
1273
|
+
bindGroup: GPUBindGroup
|
|
1274
|
+
isTransparent: boolean
|
|
1275
|
+
}[] = []
|
|
1276
|
+
private eyeOutlineDraws: { count: number; firstIndex: number; bindGroup: GPUBindGroup; isTransparent: boolean }[] = []
|
|
1277
|
+
private hairOutlineDraws: { count: number; firstIndex: number; bindGroup: GPUBindGroup; isTransparent: boolean }[] =
|
|
1278
|
+
[]
|
|
1279
|
+
private transparentNonEyeNonHairOutlineDraws: {
|
|
1280
|
+
count: number
|
|
1281
|
+
firstIndex: number
|
|
1282
|
+
bindGroup: GPUBindGroup
|
|
1283
|
+
isTransparent: boolean
|
|
1284
|
+
}[] = []
|
|
727
1285
|
|
|
728
1286
|
// Step 8: Load textures and create material bind groups
|
|
729
1287
|
private async setupMaterials(model: Model) {
|
|
@@ -783,9 +1341,14 @@ export class Engine {
|
|
|
783
1341
|
return defaultToonTexture
|
|
784
1342
|
}
|
|
785
1343
|
|
|
786
|
-
this.
|
|
787
|
-
this.
|
|
788
|
-
|
|
1344
|
+
this.opaqueNonEyeNonHairDraws = []
|
|
1345
|
+
this.eyeDraws = []
|
|
1346
|
+
this.hairDraws = []
|
|
1347
|
+
this.transparentNonEyeNonHairDraws = []
|
|
1348
|
+
this.opaqueNonEyeNonHairOutlineDraws = []
|
|
1349
|
+
this.eyeOutlineDraws = []
|
|
1350
|
+
this.hairOutlineDraws = []
|
|
1351
|
+
this.transparentNonEyeNonHairOutlineDraws = []
|
|
789
1352
|
let runningFirstIndex = 0
|
|
790
1353
|
|
|
791
1354
|
for (const mat of materials) {
|
|
@@ -814,9 +1377,11 @@ export class Engine {
|
|
|
814
1377
|
})
|
|
815
1378
|
this.device.queue.writeBuffer(materialUniformBuffer, 0, materialUniformData)
|
|
816
1379
|
|
|
1380
|
+
// Create bind groups using the shared bind group layout
|
|
1381
|
+
// All pipelines (main, eye, hair multiply, hair opaque) use the same shader and layout
|
|
817
1382
|
const bindGroup = this.device.createBindGroup({
|
|
818
1383
|
label: `material bind group: ${mat.name}`,
|
|
819
|
-
layout: this.
|
|
1384
|
+
layout: this.hairBindGroupLayout,
|
|
820
1385
|
entries: [
|
|
821
1386
|
{ binding: 0, resource: { buffer: this.cameraUniformBuffer } },
|
|
822
1387
|
{ binding: 1, resource: { buffer: this.lightUniformBuffer } },
|
|
@@ -829,13 +1394,36 @@ export class Engine {
|
|
|
829
1394
|
],
|
|
830
1395
|
})
|
|
831
1396
|
|
|
832
|
-
//
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
1397
|
+
// Classify materials into appropriate draw lists
|
|
1398
|
+
if (mat.isEye) {
|
|
1399
|
+
this.eyeDraws.push({
|
|
1400
|
+
count: matCount,
|
|
1401
|
+
firstIndex: runningFirstIndex,
|
|
1402
|
+
bindGroup,
|
|
1403
|
+
isTransparent,
|
|
1404
|
+
})
|
|
1405
|
+
} else if (mat.isHair) {
|
|
1406
|
+
this.hairDraws.push({
|
|
1407
|
+
count: matCount,
|
|
1408
|
+
firstIndex: runningFirstIndex,
|
|
1409
|
+
bindGroup,
|
|
1410
|
+
isTransparent,
|
|
1411
|
+
})
|
|
1412
|
+
} else if (isTransparent) {
|
|
1413
|
+
this.transparentNonEyeNonHairDraws.push({
|
|
1414
|
+
count: matCount,
|
|
1415
|
+
firstIndex: runningFirstIndex,
|
|
1416
|
+
bindGroup,
|
|
1417
|
+
isTransparent,
|
|
1418
|
+
})
|
|
1419
|
+
} else {
|
|
1420
|
+
this.opaqueNonEyeNonHairDraws.push({
|
|
1421
|
+
count: matCount,
|
|
1422
|
+
firstIndex: runningFirstIndex,
|
|
1423
|
+
bindGroup,
|
|
1424
|
+
isTransparent,
|
|
1425
|
+
})
|
|
1426
|
+
}
|
|
839
1427
|
|
|
840
1428
|
// Outline for all materials (including transparent)
|
|
841
1429
|
// Edge flag is at bit 4 (0x10) in PMX format, not bit 0 (0x01)
|
|
@@ -856,7 +1444,7 @@ export class Engine {
|
|
|
856
1444
|
|
|
857
1445
|
const outlineBindGroup = this.device.createBindGroup({
|
|
858
1446
|
label: `outline bind group: ${mat.name}`,
|
|
859
|
-
layout: outlineBindGroupLayout,
|
|
1447
|
+
layout: this.outlineBindGroupLayout,
|
|
860
1448
|
entries: [
|
|
861
1449
|
{ binding: 0, resource: { buffer: this.cameraUniformBuffer } },
|
|
862
1450
|
{ binding: 1, resource: { buffer: materialUniformBuffer } },
|
|
@@ -864,13 +1452,36 @@ export class Engine {
|
|
|
864
1452
|
],
|
|
865
1453
|
})
|
|
866
1454
|
|
|
867
|
-
//
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
1455
|
+
// Classify outlines into appropriate draw lists
|
|
1456
|
+
if (mat.isEye) {
|
|
1457
|
+
this.eyeOutlineDraws.push({
|
|
1458
|
+
count: matCount,
|
|
1459
|
+
firstIndex: runningFirstIndex,
|
|
1460
|
+
bindGroup: outlineBindGroup,
|
|
1461
|
+
isTransparent,
|
|
1462
|
+
})
|
|
1463
|
+
} else if (mat.isHair) {
|
|
1464
|
+
this.hairOutlineDraws.push({
|
|
1465
|
+
count: matCount,
|
|
1466
|
+
firstIndex: runningFirstIndex,
|
|
1467
|
+
bindGroup: outlineBindGroup,
|
|
1468
|
+
isTransparent,
|
|
1469
|
+
})
|
|
1470
|
+
} else if (isTransparent) {
|
|
1471
|
+
this.transparentNonEyeNonHairOutlineDraws.push({
|
|
1472
|
+
count: matCount,
|
|
1473
|
+
firstIndex: runningFirstIndex,
|
|
1474
|
+
bindGroup: outlineBindGroup,
|
|
1475
|
+
isTransparent,
|
|
1476
|
+
})
|
|
1477
|
+
} else {
|
|
1478
|
+
this.opaqueNonEyeNonHairOutlineDraws.push({
|
|
1479
|
+
count: matCount,
|
|
1480
|
+
firstIndex: runningFirstIndex,
|
|
1481
|
+
bindGroup: outlineBindGroup,
|
|
1482
|
+
isTransparent,
|
|
1483
|
+
})
|
|
1484
|
+
}
|
|
874
1485
|
}
|
|
875
1486
|
|
|
876
1487
|
runningFirstIndex += matCount
|
|
@@ -948,10 +1559,89 @@ export class Engine {
|
|
|
948
1559
|
pass.setIndexBuffer(this.indexBuffer!, "uint32")
|
|
949
1560
|
|
|
950
1561
|
this.drawCallCount = 0
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
this.
|
|
954
|
-
|
|
1562
|
+
|
|
1563
|
+
// === PASS 1: Opaque non-eye, non-hair (face, body, etc) ===
|
|
1564
|
+
// this.drawOutlines(pass, false) // Opaque outlines
|
|
1565
|
+
|
|
1566
|
+
pass.setPipeline(this.pipeline)
|
|
1567
|
+
for (const draw of this.opaqueNonEyeNonHairDraws) {
|
|
1568
|
+
if (draw.count > 0) {
|
|
1569
|
+
pass.setBindGroup(0, draw.bindGroup)
|
|
1570
|
+
pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0)
|
|
1571
|
+
this.drawCallCount++
|
|
1572
|
+
}
|
|
1573
|
+
}
|
|
1574
|
+
|
|
1575
|
+
// === PASS 2: Eyes (writes stencil = 1) ===
|
|
1576
|
+
pass.setPipeline(this.eyePipeline)
|
|
1577
|
+
pass.setStencilReference(1) // Set stencil reference value to 1
|
|
1578
|
+
for (const draw of this.eyeDraws) {
|
|
1579
|
+
if (draw.count > 0) {
|
|
1580
|
+
pass.setBindGroup(0, draw.bindGroup)
|
|
1581
|
+
pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0)
|
|
1582
|
+
this.drawCallCount++
|
|
1583
|
+
}
|
|
1584
|
+
}
|
|
1585
|
+
|
|
1586
|
+
// === PASS 3a: Hair over eyes (stencil == 1, multiply blend) ===
|
|
1587
|
+
// Draw hair geometry first to establish depth
|
|
1588
|
+
pass.setPipeline(this.hairMultiplyPipeline)
|
|
1589
|
+
pass.setStencilReference(1) // Check against stencil value 1
|
|
1590
|
+
for (const draw of this.hairDraws) {
|
|
1591
|
+
if (draw.count > 0) {
|
|
1592
|
+
pass.setBindGroup(0, draw.bindGroup)
|
|
1593
|
+
pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0)
|
|
1594
|
+
this.drawCallCount++
|
|
1595
|
+
}
|
|
1596
|
+
}
|
|
1597
|
+
|
|
1598
|
+
// === PASS 3a.5: Hair outlines over eyes (stencil == 1, depth test to only draw near hair) ===
|
|
1599
|
+
// Use depth compare "less-equal" with the hair depth to only draw outline where hair exists
|
|
1600
|
+
// The outline is expanded outward, so we need to ensure it only appears near the hair edge
|
|
1601
|
+
pass.setPipeline(this.hairOutlineOverEyesPipeline)
|
|
1602
|
+
pass.setStencilReference(1) // Check against stencil value 1 (with equal test)
|
|
1603
|
+
for (const draw of this.hairOutlineDraws) {
|
|
1604
|
+
if (draw.count > 0) {
|
|
1605
|
+
pass.setBindGroup(0, draw.bindGroup)
|
|
1606
|
+
pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0)
|
|
1607
|
+
}
|
|
1608
|
+
}
|
|
1609
|
+
|
|
1610
|
+
// === PASS 3b: Hair over non-eyes (stencil != 1, opaque) ===
|
|
1611
|
+
pass.setPipeline(this.hairOpaquePipeline)
|
|
1612
|
+
pass.setStencilReference(1) // Check against stencil value 1 (with not-equal test)
|
|
1613
|
+
for (const draw of this.hairDraws) {
|
|
1614
|
+
if (draw.count > 0) {
|
|
1615
|
+
pass.setBindGroup(0, draw.bindGroup)
|
|
1616
|
+
pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0)
|
|
1617
|
+
this.drawCallCount++
|
|
1618
|
+
}
|
|
1619
|
+
}
|
|
1620
|
+
|
|
1621
|
+
// === PASS 3b.5: Hair outlines over non-eyes (stencil != 1) ===
|
|
1622
|
+
// Draw hair outlines after hair geometry, so they only appear where hair exists
|
|
1623
|
+
pass.setPipeline(this.hairOutlinePipeline)
|
|
1624
|
+
pass.setStencilReference(1) // Check against stencil value 1 (with not-equal test)
|
|
1625
|
+
for (const draw of this.hairOutlineDraws) {
|
|
1626
|
+
if (draw.count > 0) {
|
|
1627
|
+
pass.setBindGroup(0, draw.bindGroup)
|
|
1628
|
+
pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0)
|
|
1629
|
+
}
|
|
1630
|
+
}
|
|
1631
|
+
|
|
1632
|
+
this.drawOutlines(pass, false) // Opaque outlines
|
|
1633
|
+
|
|
1634
|
+
// === PASS 4: Transparent non-eye, non-hair ===
|
|
1635
|
+
pass.setPipeline(this.pipeline)
|
|
1636
|
+
for (const draw of this.transparentNonEyeNonHairDraws) {
|
|
1637
|
+
if (draw.count > 0) {
|
|
1638
|
+
pass.setBindGroup(0, draw.bindGroup)
|
|
1639
|
+
pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0)
|
|
1640
|
+
this.drawCallCount++
|
|
1641
|
+
}
|
|
1642
|
+
}
|
|
1643
|
+
|
|
1644
|
+
this.drawOutlines(pass, true) // Transparent outlines
|
|
955
1645
|
|
|
956
1646
|
pass.end()
|
|
957
1647
|
this.device.queue.submit([encoder.finish()])
|
|
@@ -1046,24 +1736,22 @@ export class Engine {
|
|
|
1046
1736
|
|
|
1047
1737
|
// Draw outlines (opaque or transparent)
|
|
1048
1738
|
private drawOutlines(pass: GPURenderPassEncoder, transparent: boolean) {
|
|
1049
|
-
if (this.outlineDraws.length === 0) return
|
|
1050
1739
|
pass.setPipeline(this.outlinePipeline)
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1740
|
+
if (transparent) {
|
|
1741
|
+
// Draw transparent outlines (if any)
|
|
1742
|
+
for (const draw of this.transparentNonEyeNonHairOutlineDraws) {
|
|
1743
|
+
if (draw.count > 0) {
|
|
1744
|
+
pass.setBindGroup(0, draw.bindGroup)
|
|
1745
|
+
pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0)
|
|
1746
|
+
}
|
|
1055
1747
|
}
|
|
1056
|
-
}
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
if (draw.count > 0 && draw.isTransparent === transparent) {
|
|
1064
|
-
pass.setBindGroup(0, draw.bindGroup)
|
|
1065
|
-
pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0)
|
|
1066
|
-
this.drawCallCount++
|
|
1748
|
+
} else {
|
|
1749
|
+
// Draw opaque outlines before main geometry
|
|
1750
|
+
for (const draw of this.opaqueNonEyeNonHairOutlineDraws) {
|
|
1751
|
+
if (draw.count > 0) {
|
|
1752
|
+
pass.setBindGroup(0, draw.bindGroup)
|
|
1753
|
+
pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0)
|
|
1754
|
+
}
|
|
1067
1755
|
}
|
|
1068
1756
|
}
|
|
1069
1757
|
}
|
|
@@ -1120,7 +1808,12 @@ export class Engine {
|
|
|
1120
1808
|
}
|
|
1121
1809
|
bufferMemoryBytes += 40 * 4 // cameraUniformBuffer
|
|
1122
1810
|
bufferMemoryBytes += 64 * 4 // lightUniformBuffer
|
|
1123
|
-
|
|
1811
|
+
const totalMaterialDraws =
|
|
1812
|
+
this.opaqueNonEyeNonHairDraws.length +
|
|
1813
|
+
this.eyeDraws.length +
|
|
1814
|
+
this.hairDraws.length +
|
|
1815
|
+
this.transparentNonEyeNonHairDraws.length
|
|
1816
|
+
bufferMemoryBytes += totalMaterialDraws * 4 // Material uniform buffers
|
|
1124
1817
|
|
|
1125
1818
|
let renderTargetMemoryBytes = 0
|
|
1126
1819
|
if (this.multisampleTexture) {
|