reze-engine 0.25.1 → 0.26.0
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 +4 -2
- package/dist/engine.d.ts +49 -18
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +190 -104
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/shaders/passes/composite.d.ts.map +1 -1
- package/dist/shaders/passes/composite.js +46 -4
- 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 +214 -106
- package/src/index.ts +2 -0
- package/src/shaders/passes/composite.ts +51 -4
- package/src/shaders/passes/outline.ts +50 -23
package/dist/engine.js
CHANGED
|
@@ -142,6 +142,14 @@ export const DEFAULT_VIEW_TRANSFORM = {
|
|
|
142
142
|
gamma: 1.0,
|
|
143
143
|
look: "medium_high_contrast",
|
|
144
144
|
};
|
|
145
|
+
const NEUTRAL_GRADE_CHANNEL = 0.5;
|
|
146
|
+
export const DEFAULT_COLOR_GRADING = {
|
|
147
|
+
shadows: new Vec3(NEUTRAL_GRADE_CHANNEL, NEUTRAL_GRADE_CHANNEL, NEUTRAL_GRADE_CHANNEL),
|
|
148
|
+
midtones: new Vec3(NEUTRAL_GRADE_CHANNEL, NEUTRAL_GRADE_CHANNEL, NEUTRAL_GRADE_CHANNEL),
|
|
149
|
+
highlights: new Vec3(NEUTRAL_GRADE_CHANNEL, NEUTRAL_GRADE_CHANNEL, NEUTRAL_GRADE_CHANNEL),
|
|
150
|
+
contrast: 1,
|
|
151
|
+
saturation: 1,
|
|
152
|
+
};
|
|
145
153
|
export const DEFAULT_ENGINE_OPTIONS = {
|
|
146
154
|
world: { color: new Vec3(0.4014, 0.4944, 0.647), strength: 0.3 },
|
|
147
155
|
sun: { color: new Vec3(1.0, 1.0, 1.0), strength: 2.0, direction: new Vec3(-0.0873, -0.3844, 0.919) },
|
|
@@ -196,14 +204,10 @@ function buildAlphaSampler(source, rgba, width, height) {
|
|
|
196
204
|
/** Texture-alpha statistics over ≤400 of the material's triangle centroids:
|
|
197
205
|
* `avg` (0..1) and `translucentFrac` — the fraction of samples that are
|
|
198
206
|
* neither fully opaque nor fully cut out (alpha in ~0.03..0.97). Together
|
|
199
|
-
*
|
|
200
|
-
*
|
|
201
|
-
*
|
|
202
|
-
* e.g. a lace skirt panel
|
|
203
|
-
* Both route to the transparent bucket; only `sheer` is excluded from the
|
|
204
|
-
* shadow map and outline pass. */
|
|
207
|
+
* Bucketing itself is binary (babylon-mmd parity): ANY translucent coverage
|
|
208
|
+
* routes to the alpha-blend bucket. `avg` below this threshold additionally
|
|
209
|
+
* marks a material as fully sheer (a veil), which vetoes shadow casting. */
|
|
205
210
|
const SHEER_ALPHA_THRESHOLD = 0.7;
|
|
206
|
-
const PARTIAL_TRANSLUCENT_FRAC = 0.15;
|
|
207
211
|
function materialAlphaStats(verts, indices, firstIndex, count, sampler) {
|
|
208
212
|
if (!sampler)
|
|
209
213
|
return { avg: 1, translucentFrac: 0 };
|
|
@@ -274,7 +278,7 @@ export class Engine {
|
|
|
274
278
|
// unchanged).
|
|
275
279
|
this.hdrFormat = "rgba16float";
|
|
276
280
|
// [exposure, invGamma, _, _, bloomTint.x, bloomTint.y, bloomTint.z, bloomIntensity]
|
|
277
|
-
this.compositeUniformData = new Float32Array(
|
|
281
|
+
this.compositeUniformData = new Float32Array(40);
|
|
278
282
|
/** Composite background (display-space sRGB 0–1) — null = transparent canvas. */
|
|
279
283
|
this.backgroundColor = null;
|
|
280
284
|
// 360 backdrop (equirectangular skybox, sampled by view ray in composite).
|
|
@@ -337,8 +341,20 @@ export class Engine {
|
|
|
337
341
|
};
|
|
338
342
|
this.animationFrameId = null;
|
|
339
343
|
this.renderLoopCallback = null;
|
|
344
|
+
this.colorGrading = {
|
|
345
|
+
shadows: new Vec3(NEUTRAL_GRADE_CHANNEL, NEUTRAL_GRADE_CHANNEL, NEUTRAL_GRADE_CHANNEL),
|
|
346
|
+
midtones: new Vec3(NEUTRAL_GRADE_CHANNEL, NEUTRAL_GRADE_CHANNEL, NEUTRAL_GRADE_CHANNEL),
|
|
347
|
+
highlights: new Vec3(NEUTRAL_GRADE_CHANNEL, NEUTRAL_GRADE_CHANNEL, NEUTRAL_GRADE_CHANNEL),
|
|
348
|
+
contrast: DEFAULT_COLOR_GRADING.contrast,
|
|
349
|
+
saturation: DEFAULT_COLOR_GRADING.saturation,
|
|
350
|
+
};
|
|
340
351
|
/** Debug/diagnostic: skip every inverted-hull outline draw. */
|
|
341
|
-
|
|
352
|
+
// OFF by default — the product aesthetic. Modern high-detail models read
|
|
353
|
+
// better without hulls (babylon-mmd's own demos disable its outline renderer
|
|
354
|
+
// too), and no hull pass means no depth-tie edge cases against near-coplanar
|
|
355
|
+
// cloth. The full MMD-faithful machinery (interleaved per-material hulls,
|
|
356
|
+
// texture-alpha-modulated rims) stays in place behind setOutlineEnabled(true).
|
|
357
|
+
this.outlineEnabled = false;
|
|
342
358
|
/** When set, render resolution is pinned to this size instead of tracking the
|
|
343
359
|
* canvas's CSS size × devicePixelRatio (see setRenderSize). */
|
|
344
360
|
this.fixedRenderSize = null;
|
|
@@ -601,6 +617,38 @@ export class Engine {
|
|
|
601
617
|
const v = this.viewTransform;
|
|
602
618
|
return { exposure: v.exposure, gamma: v.gamma, look: v.look };
|
|
603
619
|
}
|
|
620
|
+
/**
|
|
621
|
+
* Color-grade the tonemapped scene (ASC CDL slope/offset/power + saturation).
|
|
622
|
+
* The background layer is deliberately left ungraded — see the call site in
|
|
623
|
+
* composite.ts. Uniforms-only: no pipeline rebuild, safe to call per frame
|
|
624
|
+
* (e.g. from a slider drag).
|
|
625
|
+
*/
|
|
626
|
+
setColorGrading(patch) {
|
|
627
|
+
const g = this.colorGrading;
|
|
628
|
+
if (patch.shadows)
|
|
629
|
+
g.shadows = new Vec3(patch.shadows.x, patch.shadows.y, patch.shadows.z);
|
|
630
|
+
if (patch.midtones)
|
|
631
|
+
g.midtones = new Vec3(patch.midtones.x, patch.midtones.y, patch.midtones.z);
|
|
632
|
+
if (patch.highlights)
|
|
633
|
+
g.highlights = new Vec3(patch.highlights.x, patch.highlights.y, patch.highlights.z);
|
|
634
|
+
if (patch.contrast !== undefined)
|
|
635
|
+
g.contrast = patch.contrast;
|
|
636
|
+
if (patch.saturation !== undefined)
|
|
637
|
+
g.saturation = patch.saturation;
|
|
638
|
+
if (this.device && this.compositeUniformBuffer)
|
|
639
|
+
this.writeCompositeViewUniforms();
|
|
640
|
+
}
|
|
641
|
+
/** Current grade (for serialization into a scene descriptor). */
|
|
642
|
+
getColorGrading() {
|
|
643
|
+
const g = this.colorGrading;
|
|
644
|
+
return {
|
|
645
|
+
shadows: new Vec3(g.shadows.x, g.shadows.y, g.shadows.z),
|
|
646
|
+
midtones: new Vec3(g.midtones.x, g.midtones.y, g.midtones.z),
|
|
647
|
+
highlights: new Vec3(g.highlights.x, g.highlights.y, g.highlights.z),
|
|
648
|
+
contrast: g.contrast,
|
|
649
|
+
saturation: g.saturation,
|
|
650
|
+
};
|
|
651
|
+
}
|
|
604
652
|
setViewTransformOptions(patch) {
|
|
605
653
|
const v = this.viewTransform;
|
|
606
654
|
if (patch.exposure !== undefined)
|
|
@@ -643,6 +691,31 @@ export class Engine {
|
|
|
643
691
|
u[25] = this.backgroundEffect ? 1 : 0;
|
|
644
692
|
u[26] = this.canvas.width;
|
|
645
693
|
u[27] = this.canvas.height;
|
|
694
|
+
// ── Grade (viewU[7..9]) ── The UI's three tonal COLORS map to ASC CDL here,
|
|
695
|
+
// on the CPU, so the shader only ever sees slope/offset/power. Mid-gray is
|
|
696
|
+
// neutral in all three; the signed distance from it is the amount.
|
|
697
|
+
const g = this.colorGrading;
|
|
698
|
+
const off = (c) => (c - NEUTRAL_GRADE_CHANNEL) * 0.5; // ±0.25 lift
|
|
699
|
+
// power < 1 brightens, so midtones ABOVE neutral must lower the exponent.
|
|
700
|
+
const pow_ = (c) => Math.max(0.05, 1 - (c - NEUTRAL_GRADE_CHANNEL) * 1.5);
|
|
701
|
+
const slope = (c) => Math.max(0, 1 + (c - NEUTRAL_GRADE_CHANNEL) * 1.5);
|
|
702
|
+
u[28] = off(g.shadows.x);
|
|
703
|
+
u[29] = off(g.shadows.y);
|
|
704
|
+
u[30] = off(g.shadows.z);
|
|
705
|
+
u[31] = g.contrast;
|
|
706
|
+
u[32] = pow_(g.midtones.x);
|
|
707
|
+
u[33] = pow_(g.midtones.y);
|
|
708
|
+
u[34] = pow_(g.midtones.z);
|
|
709
|
+
u[35] = g.saturation;
|
|
710
|
+
u[36] = slope(g.highlights.x);
|
|
711
|
+
u[37] = slope(g.highlights.y);
|
|
712
|
+
u[38] = slope(g.highlights.z);
|
|
713
|
+
// Neutral grade → flag off, so the default pipeline pays nothing per pixel.
|
|
714
|
+
const neutral = u[28] === 0 && u[29] === 0 && u[30] === 0 &&
|
|
715
|
+
u[32] === 1 && u[33] === 1 && u[34] === 1 &&
|
|
716
|
+
u[36] === 1 && u[37] === 1 && u[38] === 1 &&
|
|
717
|
+
g.contrast === 1 && g.saturation === 1;
|
|
718
|
+
u[39] = neutral ? 0 : 1;
|
|
646
719
|
this.device.queue.writeBuffer(this.compositeUniformBuffer, 0, u);
|
|
647
720
|
}
|
|
648
721
|
/**
|
|
@@ -1194,6 +1267,8 @@ export class Engine {
|
|
|
1194
1267
|
attributes: [
|
|
1195
1268
|
{ shaderLocation: 0, offset: 0, format: "float32x3" },
|
|
1196
1269
|
{ shaderLocation: 1, offset: 3 * 4, format: "float32x3" },
|
|
1270
|
+
// uv — the outline FS alpha-tests the diffuse texture (babylon-mmd parity)
|
|
1271
|
+
{ shaderLocation: 2, offset: 6 * 4, format: "float32x2" },
|
|
1197
1272
|
],
|
|
1198
1273
|
},
|
|
1199
1274
|
{
|
|
@@ -1448,6 +1523,7 @@ export class Engine {
|
|
|
1448
1523
|
label: "outline per-frame bind group layout",
|
|
1449
1524
|
entries: [
|
|
1450
1525
|
{ binding: 0, visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT, buffer: { type: "uniform" } },
|
|
1526
|
+
{ binding: 1, visibility: GPUShaderStage.FRAGMENT, sampler: { type: "filtering" } },
|
|
1451
1527
|
],
|
|
1452
1528
|
});
|
|
1453
1529
|
// Outline per-instance reuses mainPerInstanceBindGroupLayout (same skinMats binding)
|
|
@@ -1455,6 +1531,7 @@ export class Engine {
|
|
|
1455
1531
|
label: "outline per-material bind group layout",
|
|
1456
1532
|
entries: [
|
|
1457
1533
|
{ binding: 0, visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT, buffer: { type: "uniform" } },
|
|
1534
|
+
{ binding: 1, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: "float" } },
|
|
1458
1535
|
],
|
|
1459
1536
|
});
|
|
1460
1537
|
const outlinePipelineLayout = this.device.createPipelineLayout({
|
|
@@ -1468,7 +1545,10 @@ export class Engine {
|
|
|
1468
1545
|
this.outlinePerFrameBindGroup = this.device.createBindGroup({
|
|
1469
1546
|
label: "outline per-frame bind group",
|
|
1470
1547
|
layout: this.outlinePerFrameBindGroupLayout,
|
|
1471
|
-
entries: [
|
|
1548
|
+
entries: [
|
|
1549
|
+
{ binding: 0, resource: { buffer: this.cameraUniformBuffer } },
|
|
1550
|
+
{ binding: 1, resource: this.materialSampler },
|
|
1551
|
+
],
|
|
1472
1552
|
});
|
|
1473
1553
|
const outlineShaderModule = this.device.createShaderModule({
|
|
1474
1554
|
label: "outline shaders",
|
|
@@ -1483,8 +1563,10 @@ export class Engine {
|
|
|
1483
1563
|
cullMode: "back",
|
|
1484
1564
|
depthStencil: {
|
|
1485
1565
|
format: "depth24plus-stencil8",
|
|
1486
|
-
//
|
|
1487
|
-
|
|
1566
|
+
// babylon-mmd draws outlines WITH depth write (its _afterRenderingMesh
|
|
1567
|
+
// forces setDepthWrite(true)); the constant bias below still makes
|
|
1568
|
+
// hulls lose depth ties against their own near-coplanar geometry.
|
|
1569
|
+
depthWriteEnabled: true,
|
|
1488
1570
|
depthCompare: "less-equal",
|
|
1489
1571
|
// CONFIRMED fix (bisected live via setOutlineEnabled): hull fragments
|
|
1490
1572
|
// carry their surface's exact depth, so against this model's paired
|
|
@@ -1666,10 +1748,11 @@ export class Engine {
|
|
|
1666
1748
|
// mirroring EEVEE where bloom color/intensity are combine-stage params, not prefilter).
|
|
1667
1749
|
this.compositeUniformBuffer = this.device.createBuffer({
|
|
1668
1750
|
label: "composite view uniforms",
|
|
1669
|
-
//
|
|
1751
|
+
// 10 × vec4f: (exposure, invGamma, _, _) · (bloom tint, intensity) ·
|
|
1670
1752
|
// (bg rgb, mode) · camera right/up/forward basis for the 360 skybox ray ·
|
|
1671
|
-
// (time, _, canvas width, canvas height) for user background effects
|
|
1672
|
-
|
|
1753
|
+
// (time, _, canvas width, canvas height) for user background effects ·
|
|
1754
|
+
// three grade vectors (CDL offset+contrast, power+saturation, slope+flag).
|
|
1755
|
+
size: 160,
|
|
1673
1756
|
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
|
|
1674
1757
|
});
|
|
1675
1758
|
this.bgParamsDummyBuffer = this.device.createBuffer({
|
|
@@ -3038,27 +3121,25 @@ export class Engine {
|
|
|
3038
3121
|
diffuseTexture = this.fallbackMaterialTexture;
|
|
3039
3122
|
}
|
|
3040
3123
|
const materialAlpha = mat.diffuse[3];
|
|
3041
|
-
// Transparent bucket when the MATERIAL says so — or when the TEXTURE does
|
|
3042
|
-
// (sheer cloth almost always ships with diffuse alpha 1.0 and carries its
|
|
3043
|
-
// translucency in texture alpha). Transparent-bucket draws happen after
|
|
3044
|
-
// the opaque bucket (and after the late-drawn hair render-class), so a
|
|
3045
|
-
// veil composites over the hair behind it instead of depth-rejecting it;
|
|
3046
|
-
// they are also excluded from the shadow map, so sheer cloth stops
|
|
3047
|
-
// casting the solid shadow of an opaque sheet.
|
|
3048
3124
|
const diffusePath = texLogicalPath(mat.diffuseTextureIndex);
|
|
3049
3125
|
const alphaSampler = diffusePath ? this.textureAlphaCache.get(diffusePath) : null;
|
|
3050
3126
|
const stats = materialAlphaStats(meshVertices, meshIndices, currentIndexOffset, indexCount, alphaSampler);
|
|
3127
|
+
// babylon-mmd parity (its default DepthWriteAlphaBlendingWithEvaluation
|
|
3128
|
+
// method): the bucket decision is BINARY. A material with ANY translucent
|
|
3129
|
+
// texels on its geometry is alpha-blend — drawn in PMX author order with
|
|
3130
|
+
// depth write ON (forceDepthWrite); everything else is opaque. The old
|
|
3131
|
+
// avg/frac tier system left mostly-opaque lace (translucentFrac 0.09) in
|
|
3132
|
+
// the opaque bucket while its sibling panels went transparent, breaking
|
|
3133
|
+
// the author's compositing order — the gray fold patches. The 2% floor
|
|
3134
|
+
// only guards against centroid-sampling noise on genuinely solid cloth.
|
|
3051
3135
|
const sheer = stats.avg < SHEER_ALPHA_THRESHOLD;
|
|
3052
|
-
const
|
|
3053
|
-
//
|
|
3054
|
-
//
|
|
3055
|
-
|
|
3056
|
-
// coverage. Partial-sheer cloth still casts shadows and keeps its outline;
|
|
3057
|
-
// fully sheer cloth (veil) does neither.
|
|
3058
|
-
const isTransparent = materialAlpha < 1.0 - 0.001 || sheer || partial;
|
|
3136
|
+
const isTransparent = materialAlpha < 1.0 - 0.001 || sheer || stats.translucentFrac > 0.02;
|
|
3137
|
+
// Shadow casting: the PMX author's own flag (bit 0x04, cast self-shadow),
|
|
3138
|
+
// still vetoed for fully sheer cloth — a veil must not cast a solid sheet.
|
|
3139
|
+
const castsShadow = (mat.edgeFlag & 0x04) !== 0 && !sheer;
|
|
3059
3140
|
// Load-time classification log — one line per material, cheap and
|
|
3060
|
-
// invaluable when a model renders wrong (bucket/outline/
|
|
3061
|
-
console.info(`[reze] ${mat.name}: alpha=${materialAlpha.toFixed(2)} avg=${stats.avg.toFixed(2)}
|
|
3141
|
+
// invaluable when a model renders wrong (bucket/outline/shadow disputes).
|
|
3142
|
+
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"}`);
|
|
3062
3143
|
// Sphere map (sph=1 multiply / spa=2 add). Mode 3 (sub-texture UV) is
|
|
3063
3144
|
// rare and not implemented — treated as none, like a failed load.
|
|
3064
3145
|
let sphereMode = mat.sphereMode === 1 || mat.sphereMode === 2 ? mat.sphereMode : 0;
|
|
@@ -3089,23 +3170,13 @@ export class Engine {
|
|
|
3089
3170
|
// Ungrouped at load — binding(4) = zero buffer, neutral base pipeline. autoStyleGroups
|
|
3090
3171
|
// / applyStyleGroups rebind grouped materials to their group's buffer + pipeline.
|
|
3091
3172
|
const bindGroup = this.createMaterialBindGroup(`${prefix}material: ${mat.name}`, baseBindGroupEntries, this.zeroStyleBuffer);
|
|
3092
|
-
|
|
3093
|
-
|
|
3094
|
-
|
|
3095
|
-
|
|
3096
|
-
|
|
3097
|
-
|
|
3098
|
-
|
|
3099
|
-
groupId: null,
|
|
3100
|
-
baseBindGroupEntries,
|
|
3101
|
-
castsShadow: !sheer,
|
|
3102
|
-
});
|
|
3103
|
-
// No inverted-hull outline for SHEER materials: the outline shader draws a
|
|
3104
|
-
// solid edgeColor silhouette (it never samples texture alpha), so a
|
|
3105
|
-
// see-through veil dragged a near-black hull over the cloth behind it —
|
|
3106
|
-
// broken black shapes that waved with physics and flickered with camera
|
|
3107
|
-
// angle. A solid rim on see-through fabric is wrong in principle.
|
|
3108
|
-
if ((mat.edgeFlag & 0x10) !== 0 && mat.edgeSize > 0 && !sheer) {
|
|
3173
|
+
// Inverted-hull outline for EVERY edge-flagged material (PMX bit 0x10) —
|
|
3174
|
+
// the outline FS alpha-tests the diffuse texture, so sheer fabric masks
|
|
3175
|
+
// its own hull where it is see-through instead of us skipping it here.
|
|
3176
|
+
// Drawn interleaved right after this material's color draw (babylon-mmd's
|
|
3177
|
+
// per-mesh afterRender outline stage) — see drawMaterials.
|
|
3178
|
+
let outline;
|
|
3179
|
+
if ((mat.edgeFlag & 0x10) !== 0 && mat.edgeSize > 0) {
|
|
3109
3180
|
const materialUniformData = new Float32Array([
|
|
3110
3181
|
mat.edgeColor[0],
|
|
3111
3182
|
mat.edgeColor[1],
|
|
@@ -3121,18 +3192,25 @@ export class Engine {
|
|
|
3121
3192
|
const outlineBindGroup = this.device.createBindGroup({
|
|
3122
3193
|
label: `${prefix}outline: ${mat.name}`,
|
|
3123
3194
|
layout: this.outlinePerMaterialBindGroupLayout,
|
|
3124
|
-
entries: [
|
|
3125
|
-
|
|
3126
|
-
|
|
3127
|
-
|
|
3128
|
-
type: outlineType,
|
|
3129
|
-
count: indexCount,
|
|
3130
|
-
firstIndex: currentIndexOffset,
|
|
3131
|
-
bindGroup: outlineBindGroup,
|
|
3132
|
-
materialName: mat.name,
|
|
3133
|
-
groupId: null,
|
|
3195
|
+
entries: [
|
|
3196
|
+
{ binding: 0, resource: { buffer: outlineUniformBuffer } },
|
|
3197
|
+
{ binding: 1, resource: textureView },
|
|
3198
|
+
],
|
|
3134
3199
|
});
|
|
3200
|
+
outline = { bindGroup: outlineBindGroup };
|
|
3135
3201
|
}
|
|
3202
|
+
const type = isTransparent ? "transparent" : "opaque";
|
|
3203
|
+
inst.drawCalls.push({
|
|
3204
|
+
type,
|
|
3205
|
+
count: indexCount,
|
|
3206
|
+
firstIndex: currentIndexOffset,
|
|
3207
|
+
bindGroup,
|
|
3208
|
+
materialName: mat.name,
|
|
3209
|
+
groupId: null,
|
|
3210
|
+
baseBindGroupEntries,
|
|
3211
|
+
castsShadow,
|
|
3212
|
+
outline,
|
|
3213
|
+
});
|
|
3136
3214
|
if (this.onRaycast) {
|
|
3137
3215
|
const pickIdData = new Float32Array([modelId, materialId, 0, 0]);
|
|
3138
3216
|
const pickIdBuffer = this.createUniformBuffer(`${prefix}pick: ${mat.name}`, pickIdData);
|
|
@@ -3713,13 +3791,25 @@ export class Engine {
|
|
|
3713
3791
|
sp.end();
|
|
3714
3792
|
}
|
|
3715
3793
|
const pass = encoder.beginRenderPass(this.renderPassDescriptor);
|
|
3794
|
+
// Phase order: opaque models → ground → transparent fabric.
|
|
3795
|
+
// The ground shader is the most expensive full-coverage draw in the frame
|
|
3796
|
+
// (9-tap PCF on the 4096² shadow map per pixel), so it draws AFTER the
|
|
3797
|
+
// opaque phase to get early-z rejected behind the body — drawing it first
|
|
3798
|
+
// shaded every covered pixel and measurably dropped Safari fps. It still
|
|
3799
|
+
// draws BEFORE the transparent phase so sheer fabric blends over the floor
|
|
3800
|
+
// instead of over the background with the floor depth-rejected behind it.
|
|
3716
3801
|
if (hasModels)
|
|
3717
3802
|
this.forEachInstance((inst) => {
|
|
3718
3803
|
if (inst.model.visible)
|
|
3719
|
-
this.
|
|
3804
|
+
this.renderModelOpaquePhase(pass, inst);
|
|
3720
3805
|
});
|
|
3721
3806
|
if (this.hasGround)
|
|
3722
3807
|
this.renderGround(pass);
|
|
3808
|
+
if (hasModels)
|
|
3809
|
+
this.forEachInstance((inst) => {
|
|
3810
|
+
if (inst.model.visible)
|
|
3811
|
+
this.renderModelTransparentPhase(pass, inst);
|
|
3812
|
+
});
|
|
3723
3813
|
pass.end();
|
|
3724
3814
|
// Bloom pyramid (EEVEE 3.6):
|
|
3725
3815
|
// 1. Blit: HDR → bloomDown[0] (Karis prefilter, half-res)
|
|
@@ -3992,8 +4082,7 @@ export class Engine {
|
|
|
3992
4082
|
let overEyesPipeline;
|
|
3993
4083
|
try {
|
|
3994
4084
|
pipeline = await this.createRenderClassPipeline(renderClass, module, false);
|
|
3995
|
-
//
|
|
3996
|
-
// patchwork) — same shading, different depth state.
|
|
4085
|
+
// Dormant OIT twin — kept for a future order-independent-transparency path.
|
|
3997
4086
|
pipelineNoDepthWrite = await this.createRenderClassPipeline(renderClass, module, false, false);
|
|
3998
4087
|
if (renderClass === "hair")
|
|
3999
4088
|
overEyesPipeline = await this.createRenderClassPipeline(renderClass, module, true);
|
|
@@ -4139,14 +4228,10 @@ export class Engine {
|
|
|
4139
4228
|
});
|
|
4140
4229
|
}
|
|
4141
4230
|
// Pipeline for a material draw call: its group's compiled pipeline when grouped, else
|
|
4142
|
-
// the neutral base (ungrouped materials render the default graph).
|
|
4231
|
+
// the neutral base (ungrouped materials render the default graph). Transparent-bucket
|
|
4232
|
+
// draws use the SAME depth-write-on pipeline — babylon-mmd's forceDepthWrite
|
|
4233
|
+
// blending (see renderModelTransparentPhase for the trade-off record).
|
|
4143
4234
|
pipelineForDrawCall(inst, dc) {
|
|
4144
|
-
// Transparent draws WRITE depth — MMD semantics: PMX triangle/material order
|
|
4145
|
-
// is the author's compositing order, and a fold HIDES its far side rather
|
|
4146
|
-
// than blending it (the far side shades dark — light-averted — so letting it
|
|
4147
|
-
// show through read as gray fold-shaped stains; depth-write-off made every
|
|
4148
|
-
// fold do that). The no-write twins stay available for a future true-OIT
|
|
4149
|
-
// path but are deliberately unused.
|
|
4150
4235
|
if (dc.groupId) {
|
|
4151
4236
|
const install = inst.styleGroups.get(dc.groupId);
|
|
4152
4237
|
if (install)
|
|
@@ -4156,9 +4241,10 @@ export class Engine {
|
|
|
4156
4241
|
}
|
|
4157
4242
|
/**
|
|
4158
4243
|
* Draw every material of a given type (`opaque` or `transparent`) using the main
|
|
4159
|
-
* pipeline(s)
|
|
4160
|
-
*
|
|
4161
|
-
*
|
|
4244
|
+
* pipeline(s), and — babylon-mmd's per-mesh outline stage — each edge-flagged
|
|
4245
|
+
* material's inverted hull IMMEDIATELY after its color draw. Interleaving is what
|
|
4246
|
+
* makes outlines compose like MMD: every material drawn later in the author's
|
|
4247
|
+
* order covers earlier hulls, and each hull sits over everything drawn before it.
|
|
4162
4248
|
*/
|
|
4163
4249
|
drawMaterials(pass, inst, type) {
|
|
4164
4250
|
let currentPipeline = null;
|
|
@@ -4178,38 +4264,27 @@ export class Engine {
|
|
|
4178
4264
|
}
|
|
4179
4265
|
pass.setBindGroup(2, draw.bindGroup);
|
|
4180
4266
|
pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0);
|
|
4181
|
-
|
|
4182
|
-
|
|
4183
|
-
|
|
4184
|
-
|
|
4185
|
-
* Uses its own pipeline layout (group 0 = camera-only, group 2 = edge uniforms), so
|
|
4186
|
-
* every batch binds its own groups from scratch — the next drawMaterials call will
|
|
4187
|
-
* rebind group 0/1 correctly if needed.
|
|
4188
|
-
*/
|
|
4189
|
-
drawOutlines(pass, inst, type) {
|
|
4190
|
-
if (!this.outlineEnabled)
|
|
4191
|
-
return;
|
|
4192
|
-
let bound = false;
|
|
4193
|
-
for (const draw of inst.drawCalls) {
|
|
4194
|
-
if (draw.type !== type || !this.shouldRenderDrawCall(inst, draw))
|
|
4195
|
-
continue;
|
|
4196
|
-
if (!bound) {
|
|
4267
|
+
if (draw.outline && this.outlineEnabled) {
|
|
4268
|
+
// Same index range; own pipeline + groups 0/2. Group 1 (skinMats) is
|
|
4269
|
+
// layout-identical between the main and outline pipelines and stays
|
|
4270
|
+
// bound. Restore group 0 afterwards and force a pipeline re-set.
|
|
4197
4271
|
pass.setPipeline(this.outlinePipeline);
|
|
4198
4272
|
pass.setBindGroup(0, this.outlinePerFrameBindGroup);
|
|
4199
|
-
pass.setBindGroup(
|
|
4200
|
-
|
|
4273
|
+
pass.setBindGroup(2, draw.outline.bindGroup);
|
|
4274
|
+
pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0);
|
|
4275
|
+
pass.setBindGroup(0, this.perFrameBindGroup);
|
|
4276
|
+
currentPipeline = null;
|
|
4201
4277
|
}
|
|
4202
|
-
pass.setBindGroup(2, draw.bindGroup);
|
|
4203
|
-
pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0);
|
|
4204
4278
|
}
|
|
4205
4279
|
}
|
|
4206
4280
|
/**
|
|
4207
|
-
* Main-pass render sequence for one model instance:
|
|
4208
|
-
*
|
|
4209
|
-
*
|
|
4210
|
-
*
|
|
4281
|
+
* Main-pass render sequence for one model instance — babylon-mmd parity:
|
|
4282
|
+
* opaque bucket, the hair-over-eyes stencil pass, then alpha-blend materials
|
|
4283
|
+
* in PMX author order with depth write ON (forceDepthWrite). Outlines are not
|
|
4284
|
+
* a separate phase: drawMaterials draws each edge-flagged material's hull
|
|
4285
|
+
* right after the material itself, like MMD's per-mesh outline stage.
|
|
4211
4286
|
*/
|
|
4212
|
-
|
|
4287
|
+
setModelDrawState(pass, inst) {
|
|
4213
4288
|
pass.setVertexBuffer(0, inst.vertexBuffer);
|
|
4214
4289
|
pass.setVertexBuffer(1, inst.jointsBuffer);
|
|
4215
4290
|
pass.setVertexBuffer(2, inst.weightsBuffer);
|
|
@@ -4217,19 +4292,26 @@ export class Engine {
|
|
|
4217
4292
|
// Single stencil-reference set covers eye (write), hair (read not-equal),
|
|
4218
4293
|
// and hairOverEyes (read equal). Non-stencil pipelines ignore the value.
|
|
4219
4294
|
pass.setStencilReference(Engine.STENCIL_EYE_VALUE);
|
|
4220
|
-
|
|
4221
|
-
|
|
4222
|
-
|
|
4223
|
-
// — otherwise every hull behind a sheer skirt shows straight through it.
|
|
4295
|
+
}
|
|
4296
|
+
renderModelOpaquePhase(pass, inst) {
|
|
4297
|
+
this.setModelDrawState(pass, inst);
|
|
4224
4298
|
this.drawMaterials(pass, inst, "opaque");
|
|
4225
4299
|
this.drawHairOverEyes(pass, inst);
|
|
4300
|
+
}
|
|
4301
|
+
renderModelTransparentPhase(pass, inst) {
|
|
4302
|
+
this.setModelDrawState(pass, inst);
|
|
4303
|
+
// Transparent: babylon-mmd's forceDepthWrite blending — PMX author order
|
|
4304
|
+
// with depth write ON. The accepted trade-off after trying every variant:
|
|
4305
|
+
// · depth-write ON (this): a fold hides its far side; rare view-dependent
|
|
4306
|
+
// double-blend seams at some angles. MMD's own known behavior.
|
|
4307
|
+
// · nearest-surface prepass: view-independent, but punched see-through
|
|
4308
|
+
// holes to whatever sat far behind a fold.
|
|
4309
|
+
// · depth-write OFF layering: every overlap visible everywhere — MORE
|
|
4310
|
+
// gray patches and texture artifacts in practice.
|
|
4226
4311
|
this.drawMaterials(pass, inst, "transparent");
|
|
4227
|
-
this.drawOutlines(pass, inst, "opaque-outline");
|
|
4228
|
-
this.drawOutlines(pass, inst, "transparent-outline");
|
|
4229
4312
|
}
|
|
4230
4313
|
/** Depth-only re-draw of transparent-bucket materials (see depth-prepass.ts).
|
|
4231
|
-
*
|
|
4232
|
-
* for the dormant no-write/OIT path. */
|
|
4314
|
+
* Dormant — kept for a future order-independent-transparency path. */
|
|
4233
4315
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
4234
4316
|
drawTransparentDepthPrepass(pass, inst) {
|
|
4235
4317
|
let bound = false;
|
|
@@ -4291,6 +4373,10 @@ export class Engine {
|
|
|
4291
4373
|
this.cameraMatrixData[32] = cameraPos.x;
|
|
4292
4374
|
this.cameraMatrixData[33] = cameraPos.y;
|
|
4293
4375
|
this.cameraMatrixData[34] = cameraPos.z;
|
|
4376
|
+
// Spare float after viewPos: render-target height in device px — the outline
|
|
4377
|
+
// shader derives the full viewport (width via projection aspect) for its
|
|
4378
|
+
// babylon-mmd constant-pixel edge extrusion.
|
|
4379
|
+
this.cameraMatrixData[35] = this.canvas.height;
|
|
4294
4380
|
this.device.queue.writeBuffer(this.cameraUniformBuffer, 0, this.cameraMatrixData);
|
|
4295
4381
|
// 360 backdrop: the composite reconstructs each pixel's view ray from the
|
|
4296
4382
|
// camera basis — refresh it every frame the skybox is active. The view matrix
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { Engine, DEFAULT_BLOOM_OPTIONS, DEFAULT_VIEW_TRANSFORM, type EngineStats, type EngineOptions, type BloomOptions, type ViewTransformOptions, type LoadModelFromFilesOptions, type MaterialPreset, type MaterialPresetMap, type ModelTransform, type GizmoDragEvent, type GizmoDragCallback, type GizmoDragKind, type BackgroundEffectParamValue, type BackgroundEffectResult, } from "./engine";
|
|
1
|
+
export { Engine, DEFAULT_BLOOM_OPTIONS, DEFAULT_VIEW_TRANSFORM, DEFAULT_COLOR_GRADING, type ColorGradingOptions, type EngineStats, type EngineOptions, type BloomOptions, type ViewTransformOptions, type LoadModelFromFilesOptions, type MaterialPreset, type MaterialPresetMap, type ModelTransform, type GizmoDragEvent, type GizmoDragCallback, type GizmoDragKind, type BackgroundEffectParamValue, type BackgroundEffectResult, } from "./engine";
|
|
2
2
|
export { parsePmxFolderInput, pmxFileAtRelativePath, type PmxFolderInputResult } from "./folder-upload";
|
|
3
3
|
export { compileGraph, validateGraph, assignStyleSlots, type CompileOptions, type CompileResult, type StyleSlot, } from "./graph/compile";
|
|
4
4
|
export type { ShaderGraph, GraphNode, GraphLink, ExposedParam, SocketValue, Diagnostic, } from "./graph/schema";
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,MAAM,EACN,qBAAqB,EACrB,sBAAsB,EACtB,KAAK,WAAW,EAChB,KAAK,aAAa,EAClB,KAAK,YAAY,EACjB,KAAK,oBAAoB,EACzB,KAAK,yBAAyB,EAC9B,KAAK,cAAc,EACnB,KAAK,iBAAiB,EACtB,KAAK,cAAc,EACnB,KAAK,cAAc,EACnB,KAAK,iBAAiB,EACtB,KAAK,aAAa,EAClB,KAAK,0BAA0B,EAC/B,KAAK,sBAAsB,GAC5B,MAAM,UAAU,CAAA;AACjB,OAAO,EAAE,mBAAmB,EAAE,qBAAqB,EAAE,KAAK,oBAAoB,EAAE,MAAM,iBAAiB,CAAA;AACvG,OAAO,EACL,YAAY,EACZ,aAAa,EACb,gBAAgB,EAChB,KAAK,cAAc,EACnB,KAAK,aAAa,EAClB,KAAK,SAAS,GACf,MAAM,iBAAiB,CAAA;AACxB,YAAY,EACV,WAAW,EACX,SAAS,EACT,SAAS,EACT,YAAY,EACZ,WAAW,EACX,UAAU,GACX,MAAM,gBAAgB,CAAA;AACvB,OAAO,EAAE,aAAa,EAAE,KAAK,QAAQ,EAAE,KAAK,KAAK,EAAE,MAAM,kBAAkB,CAAA;AAC3E,OAAO,EAAE,cAAc,EAAE,KAAK,WAAW,EAAE,KAAK,SAAS,EAAE,KAAK,eAAe,EAAE,MAAM,sBAAsB,CAAA;AAC7G,YAAY,EACV,UAAU,EACV,eAAe,EACf,sBAAsB,EACtB,qBAAqB,GACtB,MAAM,qBAAqB,CAAA;AAC5B,OAAO,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAA;AACjD,OAAO,EAAE,aAAa,EAAE,MAAM,yBAAyB,CAAA;AACvD,OAAO,EAAE,kBAAkB,EAAE,MAAM,8BAA8B,CAAA;AACjE,OAAO,EAAE,iBAAiB,EAAE,MAAM,6BAA6B,CAAA;AAC/D,OAAO,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAA;AACnD,OAAO,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAA;AACjD,OAAO,EAAE,eAAe,EAAE,MAAM,2BAA2B,CAAA;AAC3D,OAAO,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAA;AAC/C,OAAO,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAA;AACjD,OAAO,EAAE,KAAK,EAAE,MAAM,SAAS,CAAA;AAC/B,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAA;AACzC,YAAY,EACV,aAAa,EACb,oBAAoB,EACpB,iBAAiB,EACjB,YAAY,EACZ,aAAa,EACb,iBAAiB,EACjB,YAAY,GACb,MAAM,aAAa,CAAA;AACpB,OAAO,EAAE,GAAG,EAAE,MAAM,aAAa,CAAA;AACjC,OAAO,EAAE,SAAS,EAAE,KAAK,cAAc,EAAE,MAAM,cAAc,CAAA;AAC7D,OAAO,EAAE,eAAe,EAAE,KAAK,UAAU,EAAE,MAAM,oBAAoB,CAAA;AACrE,OAAO,EAAE,WAAW,EAAE,MAAM,WAAW,CAAA"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,MAAM,EACN,qBAAqB,EACrB,sBAAsB,EACtB,qBAAqB,EACrB,KAAK,mBAAmB,EACxB,KAAK,WAAW,EAChB,KAAK,aAAa,EAClB,KAAK,YAAY,EACjB,KAAK,oBAAoB,EACzB,KAAK,yBAAyB,EAC9B,KAAK,cAAc,EACnB,KAAK,iBAAiB,EACtB,KAAK,cAAc,EACnB,KAAK,cAAc,EACnB,KAAK,iBAAiB,EACtB,KAAK,aAAa,EAClB,KAAK,0BAA0B,EAC/B,KAAK,sBAAsB,GAC5B,MAAM,UAAU,CAAA;AACjB,OAAO,EAAE,mBAAmB,EAAE,qBAAqB,EAAE,KAAK,oBAAoB,EAAE,MAAM,iBAAiB,CAAA;AACvG,OAAO,EACL,YAAY,EACZ,aAAa,EACb,gBAAgB,EAChB,KAAK,cAAc,EACnB,KAAK,aAAa,EAClB,KAAK,SAAS,GACf,MAAM,iBAAiB,CAAA;AACxB,YAAY,EACV,WAAW,EACX,SAAS,EACT,SAAS,EACT,YAAY,EACZ,WAAW,EACX,UAAU,GACX,MAAM,gBAAgB,CAAA;AACvB,OAAO,EAAE,aAAa,EAAE,KAAK,QAAQ,EAAE,KAAK,KAAK,EAAE,MAAM,kBAAkB,CAAA;AAC3E,OAAO,EAAE,cAAc,EAAE,KAAK,WAAW,EAAE,KAAK,SAAS,EAAE,KAAK,eAAe,EAAE,MAAM,sBAAsB,CAAA;AAC7G,YAAY,EACV,UAAU,EACV,eAAe,EACf,sBAAsB,EACtB,qBAAqB,GACtB,MAAM,qBAAqB,CAAA;AAC5B,OAAO,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAA;AACjD,OAAO,EAAE,aAAa,EAAE,MAAM,yBAAyB,CAAA;AACvD,OAAO,EAAE,kBAAkB,EAAE,MAAM,8BAA8B,CAAA;AACjE,OAAO,EAAE,iBAAiB,EAAE,MAAM,6BAA6B,CAAA;AAC/D,OAAO,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAA;AACnD,OAAO,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAA;AACjD,OAAO,EAAE,eAAe,EAAE,MAAM,2BAA2B,CAAA;AAC3D,OAAO,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAA;AAC/C,OAAO,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAA;AACjD,OAAO,EAAE,KAAK,EAAE,MAAM,SAAS,CAAA;AAC/B,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAA;AACzC,YAAY,EACV,aAAa,EACb,oBAAoB,EACpB,iBAAiB,EACjB,YAAY,EACZ,aAAa,EACb,iBAAiB,EACjB,YAAY,GACb,MAAM,aAAa,CAAA;AACpB,OAAO,EAAE,GAAG,EAAE,MAAM,aAAa,CAAA;AACjC,OAAO,EAAE,SAAS,EAAE,KAAK,cAAc,EAAE,MAAM,cAAc,CAAA;AAC7D,OAAO,EAAE,eAAe,EAAE,KAAK,UAAU,EAAE,MAAM,oBAAoB,CAAA;AACrE,OAAO,EAAE,WAAW,EAAE,MAAM,WAAW,CAAA"}
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { Engine, DEFAULT_BLOOM_OPTIONS, DEFAULT_VIEW_TRANSFORM, } from "./engine";
|
|
1
|
+
export { Engine, DEFAULT_BLOOM_OPTIONS, DEFAULT_VIEW_TRANSFORM, DEFAULT_COLOR_GRADING, } from "./engine";
|
|
2
2
|
export { parsePmxFolderInput, pmxFileAtRelativePath } from "./folder-upload";
|
|
3
3
|
export { compileGraph, validateGraph, assignStyleSlots, } from "./graph/compile";
|
|
4
4
|
export { NODE_REGISTRY } from "./graph/registry";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"composite.d.ts","sourceRoot":"","sources":["../../../src/shaders/passes/composite.ts"],"names":[],"mappings":"AASA;;;;;;;;;;;;;yEAayE;AACzE,MAAM,MAAM,qBAAqB,GAAG;IAClC,wEAAwE;IACxE,IAAI,EAAE,MAAM,CAAA;IACZ,8EAA8E;IAC9E,UAAU,EAAE,MAAM,CAAA;CACnB,CAAA;
|
|
1
|
+
{"version":3,"file":"composite.d.ts","sourceRoot":"","sources":["../../../src/shaders/passes/composite.ts"],"names":[],"mappings":"AASA;;;;;;;;;;;;;yEAayE;AACzE,MAAM,MAAM,qBAAqB,GAAG;IAClC,wEAAwE;IACxE,IAAI,EAAE,MAAM,CAAA;IACZ,8EAA8E;IAC9E,UAAU,EAAE,MAAM,CAAA;CACnB,CAAA;AAyKD,wBAAgB,oBAAoB,CAAC,MAAM,CAAC,EAAE,qBAAqB,GAAG,IAAI,GAAG,MAAM,CAelF;AAED,iFAAiF;AACjF,eAAO,MAAM,qBAAqB,QAA6B,CAAA"}
|
|
@@ -17,7 +17,7 @@ override APPLY_GAMMA: bool = true;
|
|
|
17
17
|
@group(0) @binding(0) var hdrTex: texture_2d<f32>;
|
|
18
18
|
@group(0) @binding(1) var bloomTex: texture_2d<f32>; // bloomUpTexture mip 0 (full pyramid top)
|
|
19
19
|
@group(0) @binding(2) var bloomSamp: sampler;
|
|
20
|
-
@group(0) @binding(3) var<uniform> viewU: array<vec4<f32>,
|
|
20
|
+
@group(0) @binding(3) var<uniform> viewU: array<vec4<f32>, 10>;
|
|
21
21
|
// Aux mask/alpha texture. .r = bloom mask (unused here; bloom blit uses it).
|
|
22
22
|
// .g = accumulated canvas alpha (what hdr.a carried before the HDR format
|
|
23
23
|
// became rg11b10ufloat). We unpremultiply HDR by this alpha for tonemap, then
|
|
@@ -40,6 +40,8 @@ override APPLY_GAMMA: bool = true;
|
|
|
40
40
|
// viewU[3] = (camera right, tanHalfFov·aspect); viewU[4] = (camera up, tanHalfFov);
|
|
41
41
|
// viewU[5] = (camera forward, _) — refreshed per frame while skybox/effect active.
|
|
42
42
|
// viewU[6] = (time seconds, effect on/off, canvas width, canvas height).
|
|
43
|
+
// viewU[7] = (grade offset.rgb, contrast); viewU[8] = (grade power.rgb, saturation);
|
|
44
|
+
// viewU[9] = (grade slope.rgb, grade on/off) — see grade() below.
|
|
43
45
|
// invGamma = 1/gamma precomputed on CPU — avoids a per-pixel divide.
|
|
44
46
|
@group(0) @binding(6) var bgEquirect: texture_2d<f32>;
|
|
45
47
|
|
|
@@ -59,6 +61,24 @@ fn filmic(x: f32) -> f32 {
|
|
|
59
61
|
|
|
60
62
|
/** Canvas size in pixels — for user background effects (aspect correction). */
|
|
61
63
|
fn bgResolution() -> vec2f { return viewU[6].zw; }
|
|
64
|
+
|
|
65
|
+
/** Color grading, applied to the tonemapped SCENE (not the background — see the
|
|
66
|
+
* call site). The core is ASC CDL, the film-industry interchange standard:
|
|
67
|
+
*
|
|
68
|
+
* out = (in · slope + offset) ^ power then saturation (SOP → SAT)
|
|
69
|
+
*
|
|
70
|
+
* Using the real standard rather than invented controls means a look authored
|
|
71
|
+
* here maps onto any grading tool. slope/offset/power are derived on the CPU
|
|
72
|
+
* from the UI's shadow/midtone/highlight colors (see setColorGrading), so the
|
|
73
|
+
* per-pixel cost is one mul-add, one pow, one lerp. */
|
|
74
|
+
fn grade(c: vec3f) -> vec3f {
|
|
75
|
+
var x = pow(max(c * viewU[9].xyz + viewU[7].xyz, vec3f(0.0)), viewU[8].xyz);
|
|
76
|
+
// Contrast pivots on 0.5 — display-referred midpoint, since we grade post-Filmic.
|
|
77
|
+
x = (x - vec3f(0.5)) * viewU[7].w + vec3f(0.5);
|
|
78
|
+
// Rec.709 luma, matching the ASC SAT node.
|
|
79
|
+
let luma = dot(x, vec3f(0.2126, 0.7152, 0.0722));
|
|
80
|
+
return max(mix(vec3f(luma), x, viewU[8].w), vec3f(0.0));
|
|
81
|
+
}
|
|
62
82
|
`;
|
|
63
83
|
const COMPOSITE_BODY = /* wgsl */ `
|
|
64
84
|
@vertex fn vs(@builtin(vertex_index) vi: u32) -> @builtin(position) vec4f {
|
|
@@ -84,6 +104,13 @@ const COMPOSITE_BODY = /* wgsl */ `
|
|
|
84
104
|
let exposed = combined * exp2(viewU[0].x);
|
|
85
105
|
let tm = vec3f(filmic(exposed.r), filmic(exposed.g), filmic(exposed.b));
|
|
86
106
|
var disp = max(tm, vec3f(0.0));
|
|
107
|
+
// Grade the SCENE only, before the display gamma. Deliberately not applied to
|
|
108
|
+
// the background: it keeps a picked background color exactly as picked, and —
|
|
109
|
+
// load-bearing — leaves green-screen mode's key color unshifted so chroma
|
|
110
|
+
// keying still works. Skipped entirely when the grade is neutral.
|
|
111
|
+
if (viewU[9].w > 0.5) {
|
|
112
|
+
disp = grade(disp);
|
|
113
|
+
}
|
|
87
114
|
if (APPLY_GAMMA) {
|
|
88
115
|
disp = pow(disp, vec3f(viewU[0].y));
|
|
89
116
|
}
|
|
@@ -94,7 +121,7 @@ const COMPOSITE_BODY = /* wgsl */ `
|
|
|
94
121
|
var bgA = select(0.0, 1.0, bg.w > 0.5);
|
|
95
122
|
var bgPm = bg.rgb * bgA; // premultiplied accumulator
|
|
96
123
|
let fxOn = viewU[6].y > 0.5;
|
|
97
|
-
if (bg.w > 1.5 || fxOn) {
|
|
124
|
+
if ((bg.w > 1.5 || fxOn) COVERAGE_GATE) {
|
|
98
125
|
// The equirect and any effect both need this pixel's world-space view ray,
|
|
99
126
|
// rebuilt from the camera basis. The dome sits at infinity (no parallax) —
|
|
100
127
|
// PhotoDome-style, display-only.
|
|
@@ -126,16 +153,31 @@ const EFFECT_CALL = /* wgsl */ `
|
|
|
126
153
|
bgA = fx.a + bgA * (1.0 - fx.a);
|
|
127
154
|
}
|
|
128
155
|
`;
|
|
156
|
+
// Derivative builtins are illegal in non-uniform control flow (WGSL uniformity
|
|
157
|
+
// analysis rejects the pipeline), so the coverage gate below can only wrap
|
|
158
|
+
// effect code that doesn't use them. Checked textually at build time.
|
|
159
|
+
const USES_DERIVATIVES = /\b(?:fwidth|dpdx|dpdy)(?:Fine|Coarse)?\s*\(/;
|
|
160
|
+
/** Skip the whole background block (equirect sample + effect) behind pixels the
|
|
161
|
+
* model fully covers — the composite multiplies the result by (1 - alpha) = 0
|
|
162
|
+
* there anyway, and on a full-screen effect that's a third or more of the frame
|
|
163
|
+
* (the cost Safari feels most). The equirect uses explicit-LOD sampling, which
|
|
164
|
+
* is always legal in non-uniform flow; only derivative-using effects must keep
|
|
165
|
+
* uniform control flow and forgo the gate. */
|
|
166
|
+
function coverageGate(effect) {
|
|
167
|
+
const gated = !effect || !USES_DERIVATIVES.test(effect.wgsl);
|
|
168
|
+
return gated ? "&& alpha < 0.999" : "";
|
|
169
|
+
}
|
|
129
170
|
export function buildCompositeShader(effect) {
|
|
130
171
|
if (!effect)
|
|
131
|
-
return COMPOSITE_HEAD +
|
|
172
|
+
return (COMPOSITE_HEAD +
|
|
173
|
+
COMPOSITE_BODY.replace("BG_EFFECT_CALL", NO_EFFECT_CALL).replace("COVERAGE_GATE", coverageGate(null)));
|
|
132
174
|
return (COMPOSITE_HEAD +
|
|
133
175
|
"\n// ── user background effect (setBackgroundEffect) ──\n" +
|
|
134
176
|
effect.paramsDecl +
|
|
135
177
|
"\n" +
|
|
136
178
|
effect.wgsl +
|
|
137
179
|
"\n" +
|
|
138
|
-
COMPOSITE_BODY.replace("BG_EFFECT_CALL", EFFECT_CALL.trim()));
|
|
180
|
+
COMPOSITE_BODY.replace("BG_EFFECT_CALL", EFFECT_CALL.trim()).replace("COVERAGE_GATE", coverageGate(effect)));
|
|
139
181
|
}
|
|
140
182
|
/** Kept for compatibility with existing imports (the base, no-effect shader). */
|
|
141
183
|
export const COMPOSITE_SHADER_WGSL = buildCompositeShader(null);
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export declare const OUTLINE_SHADER_WGSL = "\nstruct CameraUniforms {\n view: mat4x4f,\n projection: mat4x4f,\n viewPos: vec3f,\n
|
|
1
|
+
export declare const OUTLINE_SHADER_WGSL = "\nstruct CameraUniforms {\n view: mat4x4f,\n projection: mat4x4f,\n viewPos: vec3f,\n // Render-target height in device pixels (engine writes it each frame);\n // width is recovered via the projection matrix's aspect.\n viewportHeight: f32,\n};\n\nstruct MaterialUniforms {\n edgeColor: vec4f,\n edgeSize: f32,\n _padding1: f32,\n _padding2: f32,\n _padding3: f32,\n};\n\n@group(0) @binding(0) var<uniform> camera: CameraUniforms;\n@group(0) @binding(1) var edgeSampler: sampler;\n@group(1) @binding(0) var<storage, read> skinMats: array<mat4x4f>;\n@group(2) @binding(0) var<uniform> material: MaterialUniforms;\n@group(2) @binding(1) var diffuseTexture: texture_2d<f32>;\n\nstruct VertexOutput {\n @builtin(position) position: vec4f,\n @location(0) uv: vec2f,\n};\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\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 normalizedWeights = select(vec4f(1.0, 0.0, 0.0, 0.0), weights0 * invWeightSum, weightSum > 0.0001);\n\n var skinnedPos = vec4f(0.0, 0.0, 0.0, 0.0);\n var skinnedNrm = vec3f(0.0, 0.0, 0.0);\n for (var i = 0u; i < 4u; i++) {\n let j = joints0[i];\n let w = normalizedWeights[i];\n let m = skinMats[j];\n skinnedPos += (m * pos4) * w;\n let r3 = mat3x3f(m[0].xyz, m[1].xyz, m[2].xyz);\n skinnedNrm += (r3 * normal) * w;\n }\n let worldPos = skinnedPos.xyz;\n let worldNormal = normalize(skinnedNrm);\n\n let clipPos = camera.projection * camera.view * vec4f(worldPos, 1.0);\n\n // babylon-mmd: screenNormal = normalize((view * worldNormal).xy)\n let viewNormal = (camera.view * vec4f(worldNormal, 0.0)).xyz;\n let snLen = length(viewNormal.xy);\n let screenNormal = select(vec2f(0.0, 0.0), viewNormal.xy / snLen, snLen > 1e-5);\n\n // Reference-height normalization (babylon-mmd ships this variant commented\n // out as `renderHeight = 1080`): thickness is a constant FRACTION of the\n // frame \u2014 2\u00B7edgeSize px at 1080p \u2014 so retina DPR and 4K export don't thin\n // the rims to sub-pixel. Width follows the projection aspect.\n // projection[1][1]/projection[0][0] = width/height for a symmetric frustum.\n let aspect = camera.projection[1][1] / camera.projection[0][0];\n let viewport = vec2f(1080.0 * aspect, 1080.0);\n\n // NDC offset = edgeSize \u00B7 4/viewport, \u00D7w so the perspective divide cancels:\n // constant screen thickness at any distance (babylon-mmd parity).\n let offset = screenNormal * (material.edgeSize * 4.0 / viewport) * clipPos.w;\n output.position = vec4f(clipPos.xy + offset, clipPos.z, clipPos.w);\n output.uv = uv;\n return output;\n}\n\nstruct FSOut { @location(0) color: vec4f, @location(1) mask: vec4f };\n@fragment fn fs(input: VertexOutput) -> FSOut {\n // Rim alpha FOLLOWS the fabric's texture alpha instead of a hard alpha test:\n // MMD draws blend-material edges solid (only cutout materials alpha-test), so\n // a 0.4 discard erased the whole hull on semi-transparent cloth \u2014 stockinged\n // legs crossing lost their outline entirely. Modulating instead keeps a\n // proportional rim on sheer weave (never a solid black hull) and still\n // discards true cut-out margins like hair-card borders.\n let texA = textureSample(diffuseTexture, edgeSampler, input.uv).a;\n if (texA < 0.05) {\n discard;\n }\n var out: FSOut;\n out.color = vec4f(material.edgeColor.rgb, material.edgeColor.a * texA);\n out.mask = vec4f(1.0, 1.0, 0.0, out.color.a);\n return out;\n}\n";
|
|
2
2
|
//# sourceMappingURL=outline.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"outline.d.ts","sourceRoot":"","sources":["../../../src/shaders/passes/outline.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"outline.d.ts","sourceRoot":"","sources":["../../../src/shaders/passes/outline.ts"],"names":[],"mappings":"AAcA,eAAO,MAAM,mBAAmB,ksHAgG/B,CAAA"}
|