reze-engine 0.25.0 → 0.25.2
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 +5 -1
- package/dist/engine.d.ts +95 -16
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +205 -72
- package/dist/graph/slots.d.ts.map +1 -1
- package/dist/graph/slots.js +10 -2
- package/dist/shaders/materials/common.d.ts +1 -1
- package/dist/shaders/materials/common.d.ts.map +1 -1
- package/dist/shaders/materials/common.js +143 -141
- package/dist/shaders/passes/composite.d.ts.map +1 -1
- package/dist/shaders/passes/composite.js +18 -3
- package/dist/shaders/passes/depth-prepass.d.ts +2 -0
- package/dist/shaders/passes/depth-prepass.d.ts.map +1 -0
- package/dist/shaders/passes/depth-prepass.js +56 -0
- package/dist/shaders/passes/outline.d.ts +1 -1
- package/dist/shaders/passes/outline.d.ts.map +1 -1
- package/dist/shaders/passes/outline.js +50 -23
- package/package.json +1 -1
- package/src/engine.ts +220 -75
- package/src/graph/slots.ts +11 -2
- package/src/shaders/materials/common.ts +207 -205
- package/src/shaders/passes/composite.ts +23 -3
- package/src/shaders/passes/depth-prepass.ts +57 -0
- package/src/shaders/passes/outline.ts +50 -23
package/dist/engine.js
CHANGED
|
@@ -12,6 +12,7 @@ import { LTC_MAG_LUT_SIZE, LTC_MAG_LUT_DATA } from "./shaders/ltc_mag_lut";
|
|
|
12
12
|
import { SHADOW_DEPTH_SHADER_WGSL } from "./shaders/passes/shadow";
|
|
13
13
|
import { GROUND_SHADOW_SHADER_WGSL } from "./shaders/passes/ground";
|
|
14
14
|
import { OUTLINE_SHADER_WGSL } from "./shaders/passes/outline";
|
|
15
|
+
import { TRANSPARENT_DEPTH_PREPASS_WGSL } from "./shaders/passes/depth-prepass";
|
|
15
16
|
import { SELECTION_MASK_SHADER_WGSL, SELECTION_EDGE_SHADER_WGSL } from "./shaders/passes/selection";
|
|
16
17
|
import { GIZMO_SHADER_WGSL } from "./shaders/passes/gizmo";
|
|
17
18
|
import { BLOOM_BLIT_SHADER_WGSL, BLOOM_DOWNSAMPLE_SHADER_WGSL, BLOOM_UPSAMPLE_SHADER_WGSL, } from "./shaders/passes/bloom";
|
|
@@ -192,17 +193,22 @@ function buildAlphaSampler(source, rgba, width, height) {
|
|
|
192
193
|
return null;
|
|
193
194
|
}
|
|
194
195
|
}
|
|
195
|
-
/**
|
|
196
|
-
*
|
|
196
|
+
/** Texture-alpha statistics over ≤400 of the material's triangle centroids:
|
|
197
|
+
* `avg` (0..1) and `translucentFrac` — the fraction of samples that are
|
|
198
|
+
* neither fully opaque nor fully cut out (alpha in ~0.03..0.97). Together
|
|
199
|
+
* Bucketing itself is binary (babylon-mmd parity): ANY translucent coverage
|
|
200
|
+
* routes to the alpha-blend bucket. `avg` below this threshold additionally
|
|
201
|
+
* marks a material as fully sheer (a veil), which vetoes shadow casting. */
|
|
197
202
|
const SHEER_ALPHA_THRESHOLD = 0.7;
|
|
198
|
-
function
|
|
203
|
+
function materialAlphaStats(verts, indices, firstIndex, count, sampler) {
|
|
199
204
|
if (!sampler)
|
|
200
|
-
return
|
|
205
|
+
return { avg: 1, translucentFrac: 0 };
|
|
201
206
|
const triCount = Math.floor(count / 3);
|
|
202
207
|
if (triCount === 0)
|
|
203
|
-
return
|
|
208
|
+
return { avg: 1, translucentFrac: 0 };
|
|
204
209
|
const step = Math.max(1, Math.floor(triCount / 400));
|
|
205
210
|
let sum = 0;
|
|
211
|
+
let translucent = 0;
|
|
206
212
|
let n = 0;
|
|
207
213
|
for (let t = 0; t < triCount; t += step) {
|
|
208
214
|
const i0 = indices[firstIndex + t * 3];
|
|
@@ -213,10 +219,15 @@ function materialIsSheer(verts, indices, firstIndex, count, sampler) {
|
|
|
213
219
|
// Wrap (MMD UVs may tile), then nearest-sample the downsampled plane.
|
|
214
220
|
const x = Math.min(sampler.w - 1, Math.max(0, Math.floor((u - Math.floor(u)) * sampler.w)));
|
|
215
221
|
const y = Math.min(sampler.h - 1, Math.max(0, Math.floor((v - Math.floor(v)) * sampler.h)));
|
|
216
|
-
|
|
222
|
+
const a = sampler.a[y * sampler.w + x];
|
|
223
|
+
sum += a;
|
|
224
|
+
if (a > 8 && a < 247)
|
|
225
|
+
translucent++;
|
|
217
226
|
n++;
|
|
218
227
|
}
|
|
219
|
-
|
|
228
|
+
if (n === 0)
|
|
229
|
+
return { avg: 1, translucentFrac: 0 };
|
|
230
|
+
return { avg: sum / n / 255, translucentFrac: translucent / n };
|
|
220
231
|
}
|
|
221
232
|
export class Engine {
|
|
222
233
|
static getInstance() {
|
|
@@ -322,6 +333,13 @@ export class Engine {
|
|
|
322
333
|
};
|
|
323
334
|
this.animationFrameId = null;
|
|
324
335
|
this.renderLoopCallback = null;
|
|
336
|
+
/** Debug/diagnostic: skip every inverted-hull outline draw. */
|
|
337
|
+
// OFF by default — the product aesthetic. Modern high-detail models read
|
|
338
|
+
// better without hulls (babylon-mmd's own demos disable its outline renderer
|
|
339
|
+
// too), and no hull pass means no depth-tie edge cases against near-coplanar
|
|
340
|
+
// cloth. The full MMD-faithful machinery (interleaved per-material hulls,
|
|
341
|
+
// texture-alpha-modulated rims) stays in place behind setOutlineEnabled(true).
|
|
342
|
+
this.outlineEnabled = false;
|
|
325
343
|
/** When set, render resolution is pinned to this size instead of tracking the
|
|
326
344
|
* canvas's CSS size × devicePixelRatio (see setRenderSize). */
|
|
327
345
|
this.fixedRenderSize = null;
|
|
@@ -640,6 +658,9 @@ export class Engine {
|
|
|
640
658
|
if (this.device && this.compositeUniformBuffer)
|
|
641
659
|
this.writeCompositeViewUniforms();
|
|
642
660
|
}
|
|
661
|
+
setOutlineEnabled(on) {
|
|
662
|
+
this.outlineEnabled = on;
|
|
663
|
+
}
|
|
643
664
|
rebuildCompositeBindGroup() {
|
|
644
665
|
if (!this.device || !this.hdrResolveTexture || !this.compositeBloomView)
|
|
645
666
|
return;
|
|
@@ -1174,6 +1195,8 @@ export class Engine {
|
|
|
1174
1195
|
attributes: [
|
|
1175
1196
|
{ shaderLocation: 0, offset: 0, format: "float32x3" },
|
|
1176
1197
|
{ shaderLocation: 1, offset: 3 * 4, format: "float32x3" },
|
|
1198
|
+
// uv — the outline FS alpha-tests the diffuse texture (babylon-mmd parity)
|
|
1199
|
+
{ shaderLocation: 2, offset: 6 * 4, format: "float32x2" },
|
|
1177
1200
|
],
|
|
1178
1201
|
},
|
|
1179
1202
|
{
|
|
@@ -1300,6 +1323,44 @@ export class Engine {
|
|
|
1300
1323
|
depthCompare: "less-equal",
|
|
1301
1324
|
},
|
|
1302
1325
|
});
|
|
1326
|
+
// Depth-write-off twin for transparent-bucket draws (see pipelineForDrawCall).
|
|
1327
|
+
this.neutralPipelineNoDepthWrite = this.createRenderPipeline({
|
|
1328
|
+
label: "neutral base pipeline (no depth write)",
|
|
1329
|
+
layout: mainPipelineLayout,
|
|
1330
|
+
shaderModule: neutralModule,
|
|
1331
|
+
vertexBuffers: fullVertexBuffers,
|
|
1332
|
+
fragmentTargets: sceneTargets,
|
|
1333
|
+
cullMode: "none",
|
|
1334
|
+
depthStencil: {
|
|
1335
|
+
format: "depth24plus-stencil8",
|
|
1336
|
+
depthWriteEnabled: false,
|
|
1337
|
+
depthCompare: "less-equal",
|
|
1338
|
+
},
|
|
1339
|
+
});
|
|
1340
|
+
// Depth-only prepass for transparent draws (see depth-prepass.ts): writes the
|
|
1341
|
+
// fabric's depth AFTER its color blended, so outlines drawn later are
|
|
1342
|
+
// occluded behind it. Color targets kept for pass compatibility, writeMask 0.
|
|
1343
|
+
const prepassModule = this.device.createShaderModule({
|
|
1344
|
+
label: "transparent depth prepass",
|
|
1345
|
+
code: TRANSPARENT_DEPTH_PREPASS_WGSL,
|
|
1346
|
+
});
|
|
1347
|
+
this.transparentDepthPrepassPipeline = this.device.createRenderPipeline({
|
|
1348
|
+
label: "transparent depth prepass",
|
|
1349
|
+
layout: mainPipelineLayout,
|
|
1350
|
+
vertex: { module: prepassModule, entryPoint: "vs", buffers: fullVertexBuffers },
|
|
1351
|
+
fragment: {
|
|
1352
|
+
module: prepassModule,
|
|
1353
|
+
entryPoint: "fs",
|
|
1354
|
+
targets: sceneTargets.map((t) => ({ format: t.format, writeMask: 0 })),
|
|
1355
|
+
},
|
|
1356
|
+
primitive: { cullMode: "none" },
|
|
1357
|
+
multisample: { count: Engine.MULTISAMPLE_COUNT },
|
|
1358
|
+
depthStencil: {
|
|
1359
|
+
format: "depth24plus-stencil8",
|
|
1360
|
+
depthWriteEnabled: true,
|
|
1361
|
+
depthCompare: "less-equal",
|
|
1362
|
+
},
|
|
1363
|
+
});
|
|
1303
1364
|
this.shadowLightVPBuffer = this.device.createBuffer({
|
|
1304
1365
|
size: 64,
|
|
1305
1366
|
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
|
|
@@ -1390,6 +1451,7 @@ export class Engine {
|
|
|
1390
1451
|
label: "outline per-frame bind group layout",
|
|
1391
1452
|
entries: [
|
|
1392
1453
|
{ binding: 0, visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT, buffer: { type: "uniform" } },
|
|
1454
|
+
{ binding: 1, visibility: GPUShaderStage.FRAGMENT, sampler: { type: "filtering" } },
|
|
1393
1455
|
],
|
|
1394
1456
|
});
|
|
1395
1457
|
// Outline per-instance reuses mainPerInstanceBindGroupLayout (same skinMats binding)
|
|
@@ -1397,6 +1459,7 @@ export class Engine {
|
|
|
1397
1459
|
label: "outline per-material bind group layout",
|
|
1398
1460
|
entries: [
|
|
1399
1461
|
{ binding: 0, visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT, buffer: { type: "uniform" } },
|
|
1462
|
+
{ binding: 1, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: "float" } },
|
|
1400
1463
|
],
|
|
1401
1464
|
});
|
|
1402
1465
|
const outlinePipelineLayout = this.device.createPipelineLayout({
|
|
@@ -1410,7 +1473,10 @@ export class Engine {
|
|
|
1410
1473
|
this.outlinePerFrameBindGroup = this.device.createBindGroup({
|
|
1411
1474
|
label: "outline per-frame bind group",
|
|
1412
1475
|
layout: this.outlinePerFrameBindGroupLayout,
|
|
1413
|
-
entries: [
|
|
1476
|
+
entries: [
|
|
1477
|
+
{ binding: 0, resource: { buffer: this.cameraUniformBuffer } },
|
|
1478
|
+
{ binding: 1, resource: this.materialSampler },
|
|
1479
|
+
],
|
|
1414
1480
|
});
|
|
1415
1481
|
const outlineShaderModule = this.device.createShaderModule({
|
|
1416
1482
|
label: "outline shaders",
|
|
@@ -1425,9 +1491,21 @@ export class Engine {
|
|
|
1425
1491
|
cullMode: "back",
|
|
1426
1492
|
depthStencil: {
|
|
1427
1493
|
format: "depth24plus-stencil8",
|
|
1428
|
-
//
|
|
1429
|
-
|
|
1494
|
+
// babylon-mmd draws outlines WITH depth write (its _afterRenderingMesh
|
|
1495
|
+
// forces setDepthWrite(true)); the constant bias below still makes
|
|
1496
|
+
// hulls lose depth ties against their own near-coplanar geometry.
|
|
1497
|
+
depthWriteEnabled: true,
|
|
1430
1498
|
depthCompare: "less-equal",
|
|
1499
|
+
// CONFIRMED fix (bisected live via setOutlineEnabled): hull fragments
|
|
1500
|
+
// carry their surface's exact depth, so against this model's paired
|
|
1501
|
+
// near-coplanar skirt layers the hulls WON depth ties in patches —
|
|
1502
|
+
// the black shapes on the dress. A small constant bias makes hulls lose
|
|
1503
|
+
// every tie; silhouette rims compare against the far background and are
|
|
1504
|
+
// unaffected. No slope term — slope explodes at silhouettes and would
|
|
1505
|
+
// erase the rims themselves (previous regression).
|
|
1506
|
+
depthBias: 4,
|
|
1507
|
+
depthBiasSlopeScale: 0,
|
|
1508
|
+
depthBiasClamp: 0,
|
|
1431
1509
|
// Skip fragments where the eye stamped stencil=EYE_VALUE. Those pixels are owned by
|
|
1432
1510
|
// the see-through-hair blend (hair-over-eyes), so letting the outline's near-black
|
|
1433
1511
|
// edge color overwrite them would re-introduce the dark almond we just killed.
|
|
@@ -2970,17 +3048,25 @@ export class Engine {
|
|
|
2970
3048
|
diffuseTexture = this.fallbackMaterialTexture;
|
|
2971
3049
|
}
|
|
2972
3050
|
const materialAlpha = mat.diffuse[3];
|
|
2973
|
-
// Transparent bucket when the MATERIAL says so — or when the TEXTURE does
|
|
2974
|
-
// (sheer cloth almost always ships with diffuse alpha 1.0 and carries its
|
|
2975
|
-
// translucency in texture alpha). Transparent-bucket draws happen after
|
|
2976
|
-
// the opaque bucket (and after the late-drawn hair render-class), so a
|
|
2977
|
-
// veil composites over the hair behind it instead of depth-rejecting it;
|
|
2978
|
-
// they are also excluded from the shadow map, so sheer cloth stops
|
|
2979
|
-
// casting the solid shadow of an opaque sheet.
|
|
2980
3051
|
const diffusePath = texLogicalPath(mat.diffuseTextureIndex);
|
|
2981
3052
|
const alphaSampler = diffusePath ? this.textureAlphaCache.get(diffusePath) : null;
|
|
2982
|
-
const
|
|
2983
|
-
|
|
3053
|
+
const stats = materialAlphaStats(meshVertices, meshIndices, currentIndexOffset, indexCount, alphaSampler);
|
|
3054
|
+
// babylon-mmd parity (its default DepthWriteAlphaBlendingWithEvaluation
|
|
3055
|
+
// method): the bucket decision is BINARY. A material with ANY translucent
|
|
3056
|
+
// texels on its geometry is alpha-blend — drawn in PMX author order with
|
|
3057
|
+
// depth write ON (forceDepthWrite); everything else is opaque. The old
|
|
3058
|
+
// avg/frac tier system left mostly-opaque lace (translucentFrac 0.09) in
|
|
3059
|
+
// the opaque bucket while its sibling panels went transparent, breaking
|
|
3060
|
+
// the author's compositing order — the gray fold patches. The 2% floor
|
|
3061
|
+
// only guards against centroid-sampling noise on genuinely solid cloth.
|
|
3062
|
+
const sheer = stats.avg < SHEER_ALPHA_THRESHOLD;
|
|
3063
|
+
const isTransparent = materialAlpha < 1.0 - 0.001 || sheer || stats.translucentFrac > 0.02;
|
|
3064
|
+
// Shadow casting: the PMX author's own flag (bit 0x04, cast self-shadow),
|
|
3065
|
+
// still vetoed for fully sheer cloth — a veil must not cast a solid sheet.
|
|
3066
|
+
const castsShadow = (mat.edgeFlag & 0x04) !== 0 && !sheer;
|
|
3067
|
+
// Load-time classification log — one line per material, cheap and
|
|
3068
|
+
// invaluable when a model renders wrong (bucket/outline/shadow disputes).
|
|
3069
|
+
console.info(`[reze] ${mat.name}: alpha=${materialAlpha.toFixed(2)} avg=${stats.avg.toFixed(2)} translucentFrac=${stats.translucentFrac.toFixed(2)} bucket=${isTransparent ? "transparent" : "opaque"} castsShadow=${castsShadow} edge=${(mat.edgeFlag & 0x10) !== 0 && mat.edgeSize > 0 ? "on" : "off"}`);
|
|
2984
3070
|
// Sphere map (sph=1 multiply / spa=2 add). Mode 3 (sub-texture UV) is
|
|
2985
3071
|
// rare and not implemented — treated as none, like a failed load.
|
|
2986
3072
|
let sphereMode = mat.sphereMode === 1 || mat.sphereMode === 2 ? mat.sphereMode : 0;
|
|
@@ -3011,16 +3097,12 @@ export class Engine {
|
|
|
3011
3097
|
// Ungrouped at load — binding(4) = zero buffer, neutral base pipeline. autoStyleGroups
|
|
3012
3098
|
// / applyStyleGroups rebind grouped materials to their group's buffer + pipeline.
|
|
3013
3099
|
const bindGroup = this.createMaterialBindGroup(`${prefix}material: ${mat.name}`, baseBindGroupEntries, this.zeroStyleBuffer);
|
|
3014
|
-
|
|
3015
|
-
|
|
3016
|
-
|
|
3017
|
-
|
|
3018
|
-
|
|
3019
|
-
|
|
3020
|
-
materialName: mat.name,
|
|
3021
|
-
groupId: null,
|
|
3022
|
-
baseBindGroupEntries,
|
|
3023
|
-
});
|
|
3100
|
+
// Inverted-hull outline for EVERY edge-flagged material (PMX bit 0x10) —
|
|
3101
|
+
// the outline FS alpha-tests the diffuse texture, so sheer fabric masks
|
|
3102
|
+
// its own hull where it is see-through instead of us skipping it here.
|
|
3103
|
+
// Drawn interleaved right after this material's color draw (babylon-mmd's
|
|
3104
|
+
// per-mesh afterRender outline stage) — see drawMaterials.
|
|
3105
|
+
let outline;
|
|
3024
3106
|
if ((mat.edgeFlag & 0x10) !== 0 && mat.edgeSize > 0) {
|
|
3025
3107
|
const materialUniformData = new Float32Array([
|
|
3026
3108
|
mat.edgeColor[0],
|
|
@@ -3037,18 +3119,25 @@ export class Engine {
|
|
|
3037
3119
|
const outlineBindGroup = this.device.createBindGroup({
|
|
3038
3120
|
label: `${prefix}outline: ${mat.name}`,
|
|
3039
3121
|
layout: this.outlinePerMaterialBindGroupLayout,
|
|
3040
|
-
entries: [
|
|
3041
|
-
|
|
3042
|
-
|
|
3043
|
-
|
|
3044
|
-
type: outlineType,
|
|
3045
|
-
count: indexCount,
|
|
3046
|
-
firstIndex: currentIndexOffset,
|
|
3047
|
-
bindGroup: outlineBindGroup,
|
|
3048
|
-
materialName: mat.name,
|
|
3049
|
-
groupId: null,
|
|
3122
|
+
entries: [
|
|
3123
|
+
{ binding: 0, resource: { buffer: outlineUniformBuffer } },
|
|
3124
|
+
{ binding: 1, resource: textureView },
|
|
3125
|
+
],
|
|
3050
3126
|
});
|
|
3127
|
+
outline = { bindGroup: outlineBindGroup };
|
|
3051
3128
|
}
|
|
3129
|
+
const type = isTransparent ? "transparent" : "opaque";
|
|
3130
|
+
inst.drawCalls.push({
|
|
3131
|
+
type,
|
|
3132
|
+
count: indexCount,
|
|
3133
|
+
firstIndex: currentIndexOffset,
|
|
3134
|
+
bindGroup,
|
|
3135
|
+
materialName: mat.name,
|
|
3136
|
+
groupId: null,
|
|
3137
|
+
baseBindGroupEntries,
|
|
3138
|
+
castsShadow,
|
|
3139
|
+
outline,
|
|
3140
|
+
});
|
|
3052
3141
|
if (this.onRaycast) {
|
|
3053
3142
|
const pickIdData = new Float32Array([modelId, materialId, 0, 0]);
|
|
3054
3143
|
const pickIdBuffer = this.createUniformBuffer(`${prefix}pick: ${mat.name}`, pickIdData);
|
|
@@ -3629,13 +3718,25 @@ export class Engine {
|
|
|
3629
3718
|
sp.end();
|
|
3630
3719
|
}
|
|
3631
3720
|
const pass = encoder.beginRenderPass(this.renderPassDescriptor);
|
|
3721
|
+
// Phase order: opaque models → ground → transparent fabric.
|
|
3722
|
+
// The ground shader is the most expensive full-coverage draw in the frame
|
|
3723
|
+
// (9-tap PCF on the 4096² shadow map per pixel), so it draws AFTER the
|
|
3724
|
+
// opaque phase to get early-z rejected behind the body — drawing it first
|
|
3725
|
+
// shaded every covered pixel and measurably dropped Safari fps. It still
|
|
3726
|
+
// draws BEFORE the transparent phase so sheer fabric blends over the floor
|
|
3727
|
+
// instead of over the background with the floor depth-rejected behind it.
|
|
3632
3728
|
if (hasModels)
|
|
3633
3729
|
this.forEachInstance((inst) => {
|
|
3634
3730
|
if (inst.model.visible)
|
|
3635
|
-
this.
|
|
3731
|
+
this.renderModelOpaquePhase(pass, inst);
|
|
3636
3732
|
});
|
|
3637
3733
|
if (this.hasGround)
|
|
3638
3734
|
this.renderGround(pass);
|
|
3735
|
+
if (hasModels)
|
|
3736
|
+
this.forEachInstance((inst) => {
|
|
3737
|
+
if (inst.model.visible)
|
|
3738
|
+
this.renderModelTransparentPhase(pass, inst);
|
|
3739
|
+
});
|
|
3639
3740
|
pass.end();
|
|
3640
3741
|
// Bloom pyramid (EEVEE 3.6):
|
|
3641
3742
|
// 1. Blit: HDR → bloomDown[0] (Karis prefilter, half-res)
|
|
@@ -3904,9 +4005,12 @@ export class Engine {
|
|
|
3904
4005
|
return { ok: false, diagnostics, slotMap: result.slotMap };
|
|
3905
4006
|
}
|
|
3906
4007
|
let pipeline;
|
|
4008
|
+
let pipelineNoDepthWrite;
|
|
3907
4009
|
let overEyesPipeline;
|
|
3908
4010
|
try {
|
|
3909
4011
|
pipeline = await this.createRenderClassPipeline(renderClass, module, false);
|
|
4012
|
+
// Dormant OIT twin — kept for a future order-independent-transparency path.
|
|
4013
|
+
pipelineNoDepthWrite = await this.createRenderClassPipeline(renderClass, module, false, false);
|
|
3910
4014
|
if (renderClass === "hair")
|
|
3911
4015
|
overEyesPipeline = await this.createRenderClassPipeline(renderClass, module, true);
|
|
3912
4016
|
}
|
|
@@ -3930,6 +4034,7 @@ export class Engine {
|
|
|
3930
4034
|
renderClass,
|
|
3931
4035
|
alphaMode,
|
|
3932
4036
|
pipeline,
|
|
4037
|
+
pipelineNoDepthWrite,
|
|
3933
4038
|
overEyesPipeline,
|
|
3934
4039
|
uniformBuffer,
|
|
3935
4040
|
slotMap: result.slotMap,
|
|
@@ -3988,14 +4093,14 @@ export class Engine {
|
|
|
3988
4093
|
ground: 4,
|
|
3989
4094
|
};
|
|
3990
4095
|
inst.drawCalls.sort((a, b) => typeOrder[a.type] - typeOrder[b.type] || this.drawCallRank(inst, a) - this.drawCallRank(inst, b));
|
|
3991
|
-
inst.shadowDrawCalls = inst.drawCalls.filter((d) => d.type === "opaque");
|
|
4096
|
+
inst.shadowDrawCalls = inst.drawCalls.filter((d) => (d.type === "opaque" || d.type === "transparent") && d.castsShadow === true);
|
|
3992
4097
|
}
|
|
3993
4098
|
/**
|
|
3994
4099
|
* Render-class pipeline state. A group's compiled graph swaps the fragment shading; the
|
|
3995
4100
|
* render-class owns pass integration (stencil interplay, depth bias, cull). auto = plain;
|
|
3996
4101
|
* eye = stamp + front cull + bias; hair = stencil-test (+ the over-eyes variant).
|
|
3997
4102
|
*/
|
|
3998
|
-
createRenderClassPipeline(renderClass, module, overEyes) {
|
|
4103
|
+
createRenderClassPipeline(renderClass, module, overEyes, depthWrite = true) {
|
|
3999
4104
|
const base = {
|
|
4000
4105
|
label: `style ${renderClass}${overEyes ? " (over eyes)" : ""}`,
|
|
4001
4106
|
layout: this.mainPipelineLayout,
|
|
@@ -4005,7 +4110,7 @@ export class Engine {
|
|
|
4005
4110
|
};
|
|
4006
4111
|
const plainDepth = {
|
|
4007
4112
|
format: "depth24plus-stencil8",
|
|
4008
|
-
depthWriteEnabled:
|
|
4113
|
+
depthWriteEnabled: depthWrite,
|
|
4009
4114
|
depthCompare: "less-equal",
|
|
4010
4115
|
};
|
|
4011
4116
|
let depthStencil = plainDepth;
|
|
@@ -4050,7 +4155,9 @@ export class Engine {
|
|
|
4050
4155
|
});
|
|
4051
4156
|
}
|
|
4052
4157
|
// Pipeline for a material draw call: its group's compiled pipeline when grouped, else
|
|
4053
|
-
// the neutral base (ungrouped materials render the default graph).
|
|
4158
|
+
// the neutral base (ungrouped materials render the default graph). Transparent-bucket
|
|
4159
|
+
// draws use the SAME depth-write-on pipeline — babylon-mmd's forceDepthWrite
|
|
4160
|
+
// blending (see renderModelTransparentPhase for the trade-off record).
|
|
4054
4161
|
pipelineForDrawCall(inst, dc) {
|
|
4055
4162
|
if (dc.groupId) {
|
|
4056
4163
|
const install = inst.styleGroups.get(dc.groupId);
|
|
@@ -4061,9 +4168,10 @@ export class Engine {
|
|
|
4061
4168
|
}
|
|
4062
4169
|
/**
|
|
4063
4170
|
* Draw every material of a given type (`opaque` or `transparent`) using the main
|
|
4064
|
-
* pipeline(s)
|
|
4065
|
-
*
|
|
4066
|
-
*
|
|
4171
|
+
* pipeline(s), and — babylon-mmd's per-mesh outline stage — each edge-flagged
|
|
4172
|
+
* material's inverted hull IMMEDIATELY after its color draw. Interleaving is what
|
|
4173
|
+
* makes outlines compose like MMD: every material drawn later in the author's
|
|
4174
|
+
* order covers earlier hulls, and each hull sits over everything drawn before it.
|
|
4067
4175
|
*/
|
|
4068
4176
|
drawMaterials(pass, inst, type) {
|
|
4069
4177
|
let currentPipeline = null;
|
|
@@ -4083,36 +4191,27 @@ export class Engine {
|
|
|
4083
4191
|
}
|
|
4084
4192
|
pass.setBindGroup(2, draw.bindGroup);
|
|
4085
4193
|
pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0);
|
|
4086
|
-
|
|
4087
|
-
|
|
4088
|
-
|
|
4089
|
-
|
|
4090
|
-
* Uses its own pipeline layout (group 0 = camera-only, group 2 = edge uniforms), so
|
|
4091
|
-
* every batch binds its own groups from scratch — the next drawMaterials call will
|
|
4092
|
-
* rebind group 0/1 correctly if needed.
|
|
4093
|
-
*/
|
|
4094
|
-
drawOutlines(pass, inst, type) {
|
|
4095
|
-
let bound = false;
|
|
4096
|
-
for (const draw of inst.drawCalls) {
|
|
4097
|
-
if (draw.type !== type || !this.shouldRenderDrawCall(inst, draw))
|
|
4098
|
-
continue;
|
|
4099
|
-
if (!bound) {
|
|
4194
|
+
if (draw.outline && this.outlineEnabled) {
|
|
4195
|
+
// Same index range; own pipeline + groups 0/2. Group 1 (skinMats) is
|
|
4196
|
+
// layout-identical between the main and outline pipelines and stays
|
|
4197
|
+
// bound. Restore group 0 afterwards and force a pipeline re-set.
|
|
4100
4198
|
pass.setPipeline(this.outlinePipeline);
|
|
4101
4199
|
pass.setBindGroup(0, this.outlinePerFrameBindGroup);
|
|
4102
|
-
pass.setBindGroup(
|
|
4103
|
-
|
|
4200
|
+
pass.setBindGroup(2, draw.outline.bindGroup);
|
|
4201
|
+
pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0);
|
|
4202
|
+
pass.setBindGroup(0, this.perFrameBindGroup);
|
|
4203
|
+
currentPipeline = null;
|
|
4104
4204
|
}
|
|
4105
|
-
pass.setBindGroup(2, draw.bindGroup);
|
|
4106
|
-
pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0);
|
|
4107
4205
|
}
|
|
4108
4206
|
}
|
|
4109
4207
|
/**
|
|
4110
|
-
* Main-pass render sequence for one model instance:
|
|
4111
|
-
*
|
|
4112
|
-
*
|
|
4113
|
-
*
|
|
4208
|
+
* Main-pass render sequence for one model instance — babylon-mmd parity:
|
|
4209
|
+
* opaque bucket, the hair-over-eyes stencil pass, then alpha-blend materials
|
|
4210
|
+
* in PMX author order with depth write ON (forceDepthWrite). Outlines are not
|
|
4211
|
+
* a separate phase: drawMaterials draws each edge-flagged material's hull
|
|
4212
|
+
* right after the material itself, like MMD's per-mesh outline stage.
|
|
4114
4213
|
*/
|
|
4115
|
-
|
|
4214
|
+
setModelDrawState(pass, inst) {
|
|
4116
4215
|
pass.setVertexBuffer(0, inst.vertexBuffer);
|
|
4117
4216
|
pass.setVertexBuffer(1, inst.jointsBuffer);
|
|
4118
4217
|
pass.setVertexBuffer(2, inst.weightsBuffer);
|
|
@@ -4120,11 +4219,41 @@ export class Engine {
|
|
|
4120
4219
|
// Single stencil-reference set covers eye (write), hair (read not-equal),
|
|
4121
4220
|
// and hairOverEyes (read equal). Non-stencil pipelines ignore the value.
|
|
4122
4221
|
pass.setStencilReference(Engine.STENCIL_EYE_VALUE);
|
|
4222
|
+
}
|
|
4223
|
+
renderModelOpaquePhase(pass, inst) {
|
|
4224
|
+
this.setModelDrawState(pass, inst);
|
|
4123
4225
|
this.drawMaterials(pass, inst, "opaque");
|
|
4124
|
-
this.drawOutlines(pass, inst, "opaque-outline");
|
|
4125
4226
|
this.drawHairOverEyes(pass, inst);
|
|
4227
|
+
}
|
|
4228
|
+
renderModelTransparentPhase(pass, inst) {
|
|
4229
|
+
this.setModelDrawState(pass, inst);
|
|
4230
|
+
// Transparent: babylon-mmd's forceDepthWrite blending — PMX author order
|
|
4231
|
+
// with depth write ON. The accepted trade-off after trying every variant:
|
|
4232
|
+
// · depth-write ON (this): a fold hides its far side; rare view-dependent
|
|
4233
|
+
// double-blend seams at some angles. MMD's own known behavior.
|
|
4234
|
+
// · nearest-surface prepass: view-independent, but punched see-through
|
|
4235
|
+
// holes to whatever sat far behind a fold.
|
|
4236
|
+
// · depth-write OFF layering: every overlap visible everywhere — MORE
|
|
4237
|
+
// gray patches and texture artifacts in practice.
|
|
4126
4238
|
this.drawMaterials(pass, inst, "transparent");
|
|
4127
|
-
|
|
4239
|
+
}
|
|
4240
|
+
/** Depth-only re-draw of transparent-bucket materials (see depth-prepass.ts).
|
|
4241
|
+
* Dormant — kept for a future order-independent-transparency path. */
|
|
4242
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
4243
|
+
drawTransparentDepthPrepass(pass, inst) {
|
|
4244
|
+
let bound = false;
|
|
4245
|
+
for (const draw of inst.drawCalls) {
|
|
4246
|
+
if (draw.type !== "transparent" || !this.shouldRenderDrawCall(inst, draw))
|
|
4247
|
+
continue;
|
|
4248
|
+
if (!bound) {
|
|
4249
|
+
pass.setPipeline(this.transparentDepthPrepassPipeline);
|
|
4250
|
+
pass.setBindGroup(0, this.perFrameBindGroup);
|
|
4251
|
+
pass.setBindGroup(1, inst.mainPerInstanceBindGroup);
|
|
4252
|
+
bound = true;
|
|
4253
|
+
}
|
|
4254
|
+
pass.setBindGroup(2, draw.bindGroup);
|
|
4255
|
+
pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0);
|
|
4256
|
+
}
|
|
4128
4257
|
}
|
|
4129
4258
|
/**
|
|
4130
4259
|
* Second hair pass for the see-through-hair effect. Re-draws every hair-class grouped
|
|
@@ -4171,6 +4300,10 @@ export class Engine {
|
|
|
4171
4300
|
this.cameraMatrixData[32] = cameraPos.x;
|
|
4172
4301
|
this.cameraMatrixData[33] = cameraPos.y;
|
|
4173
4302
|
this.cameraMatrixData[34] = cameraPos.z;
|
|
4303
|
+
// Spare float after viewPos: render-target height in device px — the outline
|
|
4304
|
+
// shader derives the full viewport (width via projection aspect) for its
|
|
4305
|
+
// babylon-mmd constant-pixel edge extrusion.
|
|
4306
|
+
this.cameraMatrixData[35] = this.canvas.height;
|
|
4174
4307
|
this.device.queue.writeBuffer(this.cameraUniformBuffer, 0, this.cameraMatrixData);
|
|
4175
4308
|
// 360 backdrop: the composite reconstructs each pixel's view ray from the
|
|
4176
4309
|
// camera basis — refresh it every frame the skybox is active. The view matrix
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"slots.d.ts","sourceRoot":"","sources":["../../src/graph/slots.ts"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,SAAS,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAA;AAiD5D,eAAO,MAAM,mBAAmB,gHAG/B,CAAA;
|
|
1
|
+
{"version":3,"file":"slots.d.ts","sourceRoot":"","sources":["../../src/graph/slots.ts"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,SAAS,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAA;AAiD5D,eAAO,MAAM,mBAAmB,gHAG/B,CAAA;AAgED;;;;GAIG;AACH,wBAAgB,cAAc,CAC5B,WAAW,EAAE,WAAW,EACxB,SAAS,EAAE,SAAS,EACpB,MAAM,EAAE,MAAM,EACd,oBAAoB,EAAE,OAAO,GAC5B,MAAM,CAYR"}
|
package/dist/graph/slots.js
CHANGED
|
@@ -67,14 +67,22 @@ function prelude(renderClass, alphaMode) {
|
|
|
67
67
|
? " if (alpha < hashed_alpha_threshold(input.restPos)) { discard; }"
|
|
68
68
|
: " if (alpha < 0.001) { discard; }";
|
|
69
69
|
const gate = renderClass === "eye" ? EYE_REAR_GATE : "";
|
|
70
|
+
// Double-sided shading, winding-independent: a normal pointing away from the
|
|
71
|
+
// camera means we're seeing the surface's other side — flip it. Only genuinely
|
|
72
|
+
// camera-averted fragments change (front surfaces have dot(n,v) > 0 and are
|
|
73
|
+
// untouched), unlike a front_facing test, which PMX's clockwise winding
|
|
74
|
+
// inverts. Fixes inner skirt linings (裙子白1-style flipped-normal copies)
|
|
75
|
+
// shading near-black through sheer outer layers. Eye is exempt: it FRONT-culls
|
|
76
|
+
// by design, so its visible fragments are back faces with intended normals.
|
|
77
|
+
const flip = renderClass === "eye" ? "" : "\n n = select(-n, n, dot(n, v) >= 0.0);";
|
|
70
78
|
return `@fragment fn fs(input: VertexOutput) -> FSOut {
|
|
71
79
|
let tex_s = textureSample(diffuseTexture, diffuseSampler, input.uv);
|
|
72
80
|
// MMD alpha semantics: material alpha × texture alpha.
|
|
73
81
|
let alpha = material.alpha * tex_s.a;
|
|
74
82
|
${discard}
|
|
75
83
|
|
|
76
|
-
|
|
77
|
-
let v = normalize(camera.viewPos - input.worldPos)
|
|
84
|
+
var n = safe_normal(input.normal);
|
|
85
|
+
let v = normalize(camera.viewPos - input.worldPos);${flip}
|
|
78
86
|
${gate}
|
|
79
87
|
let l = -light.lights[0].direction.xyz;
|
|
80
88
|
let sun = light.lights[0].color.xyz * light.lights[0].color.w;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export declare const COMMON_BINDINGS_WGSL = "\n\nstruct CameraUniforms {\n view: mat4x4f,\n projection: mat4x4f,\n viewPos: vec3f,\n _padding: f32,\n};\n\nstruct Light {\n direction: vec4f,\n color: vec4f,\n};\n\nstruct LightUniforms {\n ambientColor: vec4f,\n lights: array<Light, 4>,\n};\n\n// Per-material uniforms. Every material binds this layout even if it ignores fields;\n// the engine keeps one bind group layout across all material pipelines. The PMX\n// classic-material fields (ambient/specular/shininess) are carried for graph nodes that\n// want them; most graphs read only diffuseColor + alpha.\nstruct MaterialUniforms {\n diffuseColor: vec3f, // PMX diffuse rgb \u2014 the material_diffuse node reads this\n alpha: f32, // 0 \u2192 discard; <1 \u2192 transparent draw call\n ambient: vec3f, // PMX ambient rgb\n shininess: f32, // PMX specular power\n specular: vec3f, // PMX specular rgb\n sphereMode: f32, // 0 none \u00B7 1 multiply (sph) \u00B7 2 add (spa)\n // Skeleton index of the \u982D (head) bone, or -1. Lets the eye shader gate\n // the post-alpha-eye stencil by camera-vs-face hemisphere.\n headBoneIndex: f32,\n _pad0: f32,\n _pad1: f32,\n _pad2: f32,\n};\n\nstruct VertexOutput {\n @builtin(position) position: vec4f,\n @location(0) normal: vec3f,\n @location(1) uv: vec2f,\n @location(2) worldPos: vec3f,\n // Bind-pose object-space position (the raw pre-skin vertex attribute). Procedural\n // textures (noise bump, sparkle, Generated-coord gradients) key off this instead of\n // worldPos so the pattern rides with the surface \u2014 otherwise the mesh swims through a\n // world-static noise field under any skinning deformation or root (\u30BB\u30F3\u30BF\u30FC) motion.\n // At rest skinMats are identity so restPos == worldPos, which is why existing noise-\n // scale constants stay valid without retuning.\n @location(3) restPos: vec3f,\n};\n\nstruct LightVP { viewProj: mat4x4f, };\n\n@group(0) @binding(0) var<uniform> camera: CameraUniforms;\n@group(0) @binding(1) var<uniform> light: LightUniforms;\n@group(0) @binding(2) var diffuseSampler: sampler;\n@group(0) @binding(3) var shadowMap: texture_depth_2d;\n@group(0) @binding(4) var shadowSampler: sampler_comparison;\n@group(0) @binding(5) var<uniform> lightVP: LightVP;\n// binding(9) brdfLut is declared inside NODES_WGSL (nodes.ts).\n@group(1) @binding(0) var<storage, read> skinMats: array<mat4x4f>;\n@group(2) @binding(0) var diffuseTexture: texture_2d<f32>;\n@group(2) @binding(1) var<uniform> material: MaterialUniforms;\n// Reserved for future sphere/toon graph nodes; graphs that don't read them get the\n// 1\u00D71 white fallback bound here.\n@group(2) @binding(2) var toonTexture: texture_2d<f32>;\n@group(2) @binding(3) var sphereTexture: texture_2d<f32>;\n\n// Four-bone blended normals can cancel to ~zero on physics-driven parts\n// (opposing bone rotations at 50/50 weights) \u2014 normalize(0) is 0/0 = NaN,\n// which poisons the whole shading stack and flashes through bloom. Fall\n// back to up for degenerate normals instead.\nfn safe_normal(nIn: vec3f) -> vec3f {\n let l2 = dot(nIn, nIn);\n if (l2 < 1e-12) { return vec3f(0.0, 1.0, 0.0); }\n return nIn * inverseSqrt(l2);\n}\n\n";
|
|
2
|
-
export declare const SAMPLE_SHADOW_WGSL = "\n\nfn sampleShadow(worldPos: vec3f, n: vec3f) -> f32 {\n if (dot(n, -light.lights[0].direction.xyz) <= 0.0) { return 0.0; }\n let biasedPos = worldPos + n * 0.08;\n let lclip = lightVP.viewProj * vec4f(biasedPos, 1.0);\n let ndc = lclip.xyz / max(lclip.w, 1e-6);\n let suv = vec2f(ndc.x * 0.5 + 0.5, 0.5 - ndc.y * 0.5);\n let cmpZ = ndc.z - 0.001;\n let ts = 1.0 /
|
|
2
|
+
export declare const SAMPLE_SHADOW_WGSL = "\n\nfn sampleShadow(worldPos: vec3f, n: vec3f) -> f32 {\n if (dot(n, -light.lights[0].direction.xyz) <= 0.0) { return 0.0; }\n let biasedPos = worldPos + n * 0.08;\n let lclip = lightVP.viewProj * vec4f(biasedPos, 1.0);\n let ndc = lclip.xyz / max(lclip.w, 1e-6);\n let suv = vec2f(ndc.x * 0.5 + 0.5, 0.5 - ndc.y * 0.5);\n let cmpZ = ndc.z - 0.001;\n let ts = 1.0 / 4096.0;\n let s00 = textureSampleCompareLevel(shadowMap, shadowSampler, suv + vec2f(-ts, -ts), cmpZ);\n let s10 = textureSampleCompareLevel(shadowMap, shadowSampler, suv + vec2f(0.0, -ts), cmpZ);\n let s20 = textureSampleCompareLevel(shadowMap, shadowSampler, suv + vec2f( ts, -ts), cmpZ);\n let s01 = textureSampleCompareLevel(shadowMap, shadowSampler, suv + vec2f(-ts, 0.0), cmpZ);\n let s11 = textureSampleCompareLevel(shadowMap, shadowSampler, suv, cmpZ);\n let s21 = textureSampleCompareLevel(shadowMap, shadowSampler, suv + vec2f( ts, 0.0), cmpZ);\n let s02 = textureSampleCompareLevel(shadowMap, shadowSampler, suv + vec2f(-ts, ts), cmpZ);\n let s12 = textureSampleCompareLevel(shadowMap, shadowSampler, suv + vec2f(0.0, ts), cmpZ);\n let s22 = textureSampleCompareLevel(shadowMap, shadowSampler, suv + vec2f( ts, ts), cmpZ);\n return (s00 + s10 + s20 + s01 + s11 + s21 + s02 + s12 + s22) * (1.0 / 9.0);\n}\n\n";
|
|
3
3
|
export declare const COMMON_VS_WGSL = "\n\n@vertex fn vs(\n @location(0) position: vec3f,\n @location(1) normal: vec3f,\n @location(2) uv: vec2f,\n @location(3) joints0: vec4<u32>,\n @location(4) weights0: vec4<f32>\n) -> VertexOutput {\n var output: VertexOutput;\n let pos4 = vec4f(position, 1.0);\n let weightSum = weights0.x + weights0.y + weights0.z + weights0.w;\n let invWeightSum = select(1.0, 1.0 / weightSum, weightSum > 0.0001);\n let nw = select(vec4f(1.0, 0.0, 0.0, 0.0), weights0 * invWeightSum, weightSum > 0.0001);\n var skinnedPos = vec4f(0.0);\n var skinnedNrm = vec3f(0.0);\n for (var i = 0u; i < 4u; i++) {\n let m = skinMats[joints0[i]];\n let w = nw[i];\n skinnedPos += (m * pos4) * w;\n skinnedNrm += (mat3x3f(m[0].xyz, m[1].xyz, m[2].xyz) * normal) * w;\n }\n output.position = camera.projection * camera.view * vec4f(skinnedPos.xyz, 1.0);\n output.normal = skinnedNrm;\n output.uv = uv;\n output.worldPos = skinnedPos.xyz;\n output.restPos = position;\n return output;\n}\n\n";
|
|
4
4
|
export declare const COMMON_FS_OUT_WGSL = "\n\nstruct FSOut {\n @location(0) color: vec4f,\n @location(1) mask: vec4f,\n};\n\n";
|
|
5
5
|
export declare const COMMON_MATERIAL_PRELUDE_WGSL: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"common.d.ts","sourceRoot":"","sources":["../../../src/shaders/materials/common.ts"],"names":[],"mappings":"AAsBA,eAAO,MAAM,oBAAoB,8pGA+EhC,CAAC;
|
|
1
|
+
{"version":3,"file":"common.d.ts","sourceRoot":"","sources":["../../../src/shaders/materials/common.ts"],"names":[],"mappings":"AAsBA,eAAO,MAAM,oBAAoB,8pGA+EhC,CAAC;AASF,eAAO,MAAM,kBAAkB,4xCAsB9B,CAAC;AAQF,eAAO,MAAM,cAAc,s+BA8B1B,CAAC;AAsBF,eAAO,MAAM,kBAAkB,0FAO9B,CAAC;AAMF,eAAO,MAAM,4BAA4B,QACwC,CAAA"}
|