reze-engine 0.25.0 → 0.25.1
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 +78 -1
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +134 -14
- 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/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/package.json +1 -1
- package/src/engine.ts +143 -15
- package/src/graph/slots.ts +11 -2
- package/src/shaders/materials/common.ts +207 -205
- package/src/shaders/passes/depth-prepass.ts +57 -0
package/src/engine.ts
CHANGED
|
@@ -22,6 +22,7 @@ import { LTC_MAG_LUT_SIZE, LTC_MAG_LUT_DATA } from "./shaders/ltc_mag_lut"
|
|
|
22
22
|
import { SHADOW_DEPTH_SHADER_WGSL } from "./shaders/passes/shadow"
|
|
23
23
|
import { GROUND_SHADOW_SHADER_WGSL } from "./shaders/passes/ground"
|
|
24
24
|
import { OUTLINE_SHADER_WGSL } from "./shaders/passes/outline"
|
|
25
|
+
import { TRANSPARENT_DEPTH_PREPASS_WGSL } from "./shaders/passes/depth-prepass"
|
|
25
26
|
import { SELECTION_MASK_SHADER_WGSL, SELECTION_EDGE_SHADER_WGSL } from "./shaders/passes/selection"
|
|
26
27
|
import { GIZMO_SHADER_WGSL } from "./shaders/passes/gizmo"
|
|
27
28
|
import {
|
|
@@ -172,6 +173,8 @@ type GroupInstall = {
|
|
|
172
173
|
renderClass: RenderClass
|
|
173
174
|
alphaMode: AlphaMode
|
|
174
175
|
pipeline: GPURenderPipeline
|
|
176
|
+
/** Depth-write-off twin, used when this group's material draws in the transparent bucket. */
|
|
177
|
+
pipelineNoDepthWrite: GPURenderPipeline
|
|
175
178
|
/** hair render-class only: the stencil-matched IS_OVER_EYES=true variant. */
|
|
176
179
|
overEyesPipeline?: GPURenderPipeline
|
|
177
180
|
uniformBuffer: GPUBuffer
|
|
@@ -354,6 +357,8 @@ interface DrawCall {
|
|
|
354
357
|
// Present only for material draw calls (opaque/transparent) — the grouping walk skips
|
|
355
358
|
// any draw call without it.
|
|
356
359
|
baseBindGroupEntries?: GPUBindGroupEntry[]
|
|
360
|
+
/** Material draws only: false = excluded from the shadow map (fully sheer). */
|
|
361
|
+
castsShadow?: boolean
|
|
357
362
|
}
|
|
358
363
|
|
|
359
364
|
interface PickDrawCall {
|
|
@@ -448,21 +453,30 @@ function buildAlphaSampler(
|
|
|
448
453
|
}
|
|
449
454
|
}
|
|
450
455
|
|
|
451
|
-
/**
|
|
452
|
-
*
|
|
456
|
+
/** Texture-alpha statistics over ≤400 of the material's triangle centroids:
|
|
457
|
+
* `avg` (0..1) and `translucentFrac` — the fraction of samples that are
|
|
458
|
+
* neither fully opaque nor fully cut out (alpha in ~0.03..0.97). Together
|
|
459
|
+
* they classify two distinct things:
|
|
460
|
+
* sheer (avg low) — a veil: mostly see-through everywhere
|
|
461
|
+
* partial (translucentFrac up) — mostly-opaque cloth with sheer REGIONS,
|
|
462
|
+
* e.g. a lace skirt panel
|
|
463
|
+
* Both route to the transparent bucket; only `sheer` is excluded from the
|
|
464
|
+
* shadow map and outline pass. */
|
|
453
465
|
const SHEER_ALPHA_THRESHOLD = 0.7
|
|
454
|
-
|
|
466
|
+
const PARTIAL_TRANSLUCENT_FRAC = 0.15
|
|
467
|
+
function materialAlphaStats(
|
|
455
468
|
verts: Float32Array,
|
|
456
469
|
indices: Uint32Array,
|
|
457
470
|
firstIndex: number,
|
|
458
471
|
count: number,
|
|
459
472
|
sampler: { a: Uint8ClampedArray; w: number; h: number } | null | undefined,
|
|
460
|
-
):
|
|
461
|
-
if (!sampler) return
|
|
473
|
+
): { avg: number; translucentFrac: number } {
|
|
474
|
+
if (!sampler) return { avg: 1, translucentFrac: 0 }
|
|
462
475
|
const triCount = Math.floor(count / 3)
|
|
463
|
-
if (triCount === 0) return
|
|
476
|
+
if (triCount === 0) return { avg: 1, translucentFrac: 0 }
|
|
464
477
|
const step = Math.max(1, Math.floor(triCount / 400))
|
|
465
478
|
let sum = 0
|
|
479
|
+
let translucent = 0
|
|
466
480
|
let n = 0
|
|
467
481
|
for (let t = 0; t < triCount; t += step) {
|
|
468
482
|
const i0 = indices[firstIndex + t * 3]
|
|
@@ -473,10 +487,13 @@ function materialIsSheer(
|
|
|
473
487
|
// Wrap (MMD UVs may tile), then nearest-sample the downsampled plane.
|
|
474
488
|
const x = Math.min(sampler.w - 1, Math.max(0, Math.floor((u - Math.floor(u)) * sampler.w)))
|
|
475
489
|
const y = Math.min(sampler.h - 1, Math.max(0, Math.floor((v - Math.floor(v)) * sampler.h)))
|
|
476
|
-
|
|
490
|
+
const a = sampler.a[y * sampler.w + x]
|
|
491
|
+
sum += a
|
|
492
|
+
if (a > 8 && a < 247) translucent++
|
|
477
493
|
n++
|
|
478
494
|
}
|
|
479
|
-
|
|
495
|
+
if (n === 0) return { avg: 1, translucentFrac: 0 }
|
|
496
|
+
return { avg: sum / n / 255, translucentFrac: translucent / n }
|
|
480
497
|
}
|
|
481
498
|
|
|
482
499
|
export class Engine {
|
|
@@ -509,6 +526,8 @@ export class Engine {
|
|
|
509
526
|
// The one base shading model: ungrouped materials render this (compiled DEFAULT_GRAPH).
|
|
510
527
|
// Grouped materials use their group's own compiled pipeline.
|
|
511
528
|
private neutralPipeline!: GPURenderPipeline
|
|
529
|
+
private neutralPipelineNoDepthWrite!: GPURenderPipeline
|
|
530
|
+
private transparentDepthPrepassPipeline!: GPURenderPipeline
|
|
512
531
|
// ── Style group runtime ──
|
|
513
532
|
// Shared 256 B zero StyleUniforms buffer (group(2) binding(4)) bound by every ungrouped
|
|
514
533
|
// material; grouped materials rebind to their group's own buffer (per-model, in the
|
|
@@ -897,6 +916,12 @@ export class Engine {
|
|
|
897
916
|
if (this.device && this.compositeUniformBuffer) this.writeCompositeViewUniforms()
|
|
898
917
|
}
|
|
899
918
|
|
|
919
|
+
/** Debug/diagnostic: skip every inverted-hull outline draw. */
|
|
920
|
+
private outlineEnabled = true
|
|
921
|
+
setOutlineEnabled(on: boolean): void {
|
|
922
|
+
this.outlineEnabled = on
|
|
923
|
+
}
|
|
924
|
+
|
|
900
925
|
private rebuildCompositeBindGroup(): void {
|
|
901
926
|
if (!this.device || !this.hdrResolveTexture || !this.compositeBloomView) return
|
|
902
927
|
this.compositeBindGroup = this.device.createBindGroup({
|
|
@@ -1600,6 +1625,44 @@ export class Engine {
|
|
|
1600
1625
|
depthCompare: "less-equal",
|
|
1601
1626
|
},
|
|
1602
1627
|
})
|
|
1628
|
+
// Depth-write-off twin for transparent-bucket draws (see pipelineForDrawCall).
|
|
1629
|
+
this.neutralPipelineNoDepthWrite = this.createRenderPipeline({
|
|
1630
|
+
label: "neutral base pipeline (no depth write)",
|
|
1631
|
+
layout: mainPipelineLayout,
|
|
1632
|
+
shaderModule: neutralModule,
|
|
1633
|
+
vertexBuffers: fullVertexBuffers,
|
|
1634
|
+
fragmentTargets: sceneTargets,
|
|
1635
|
+
cullMode: "none",
|
|
1636
|
+
depthStencil: {
|
|
1637
|
+
format: "depth24plus-stencil8",
|
|
1638
|
+
depthWriteEnabled: false,
|
|
1639
|
+
depthCompare: "less-equal",
|
|
1640
|
+
},
|
|
1641
|
+
})
|
|
1642
|
+
// Depth-only prepass for transparent draws (see depth-prepass.ts): writes the
|
|
1643
|
+
// fabric's depth AFTER its color blended, so outlines drawn later are
|
|
1644
|
+
// occluded behind it. Color targets kept for pass compatibility, writeMask 0.
|
|
1645
|
+
const prepassModule = this.device.createShaderModule({
|
|
1646
|
+
label: "transparent depth prepass",
|
|
1647
|
+
code: TRANSPARENT_DEPTH_PREPASS_WGSL,
|
|
1648
|
+
})
|
|
1649
|
+
this.transparentDepthPrepassPipeline = this.device.createRenderPipeline({
|
|
1650
|
+
label: "transparent depth prepass",
|
|
1651
|
+
layout: mainPipelineLayout,
|
|
1652
|
+
vertex: { module: prepassModule, entryPoint: "vs", buffers: fullVertexBuffers as GPUVertexBufferLayout[] },
|
|
1653
|
+
fragment: {
|
|
1654
|
+
module: prepassModule,
|
|
1655
|
+
entryPoint: "fs",
|
|
1656
|
+
targets: sceneTargets.map((t) => ({ format: (t as GPUColorTargetState).format, writeMask: 0 })),
|
|
1657
|
+
},
|
|
1658
|
+
primitive: { cullMode: "none" },
|
|
1659
|
+
multisample: { count: Engine.MULTISAMPLE_COUNT },
|
|
1660
|
+
depthStencil: {
|
|
1661
|
+
format: "depth24plus-stencil8",
|
|
1662
|
+
depthWriteEnabled: true,
|
|
1663
|
+
depthCompare: "less-equal",
|
|
1664
|
+
},
|
|
1665
|
+
})
|
|
1603
1666
|
|
|
1604
1667
|
this.shadowLightVPBuffer = this.device.createBuffer({
|
|
1605
1668
|
size: 64,
|
|
@@ -1737,6 +1800,16 @@ export class Engine {
|
|
|
1737
1800
|
// Don’t write outline into depth buffer — stops z-fighting / black cracks vs body (MMD-style; body depth stays authoritative)
|
|
1738
1801
|
depthWriteEnabled: false,
|
|
1739
1802
|
depthCompare: "less-equal",
|
|
1803
|
+
// CONFIRMED fix (bisected live via setOutlineEnabled): hull fragments
|
|
1804
|
+
// carry their surface's exact depth, so against this model's paired
|
|
1805
|
+
// near-coplanar skirt layers the hulls WON depth ties in patches —
|
|
1806
|
+
// the black shapes on the dress. A small constant bias makes hulls lose
|
|
1807
|
+
// every tie; silhouette rims compare against the far background and are
|
|
1808
|
+
// unaffected. No slope term — slope explodes at silhouettes and would
|
|
1809
|
+
// erase the rims themselves (previous regression).
|
|
1810
|
+
depthBias: 4,
|
|
1811
|
+
depthBiasSlopeScale: 0,
|
|
1812
|
+
depthBiasClamp: 0,
|
|
1740
1813
|
// Skip fragments where the eye stamped stencil=EYE_VALUE. Those pixels are owned by
|
|
1741
1814
|
// the see-through-hair blend (hair-over-eyes), so letting the outline's near-black
|
|
1742
1815
|
// edge color overwrite them would re-introduce the dark almond we just killed.
|
|
@@ -3491,9 +3564,20 @@ export class Engine {
|
|
|
3491
3564
|
// casting the solid shadow of an opaque sheet.
|
|
3492
3565
|
const diffusePath = texLogicalPath(mat.diffuseTextureIndex)
|
|
3493
3566
|
const alphaSampler = diffusePath ? this.textureAlphaCache.get(diffusePath) : null
|
|
3494
|
-
const
|
|
3495
|
-
|
|
3496
|
-
|
|
3567
|
+
const stats = materialAlphaStats(meshVertices, meshIndices, currentIndexOffset, indexCount, alphaSampler)
|
|
3568
|
+
const sheer = stats.avg < SHEER_ALPHA_THRESHOLD
|
|
3569
|
+
const partial = !sheer && stats.translucentFrac > PARTIAL_TRANSLUCENT_FRAC
|
|
3570
|
+
// Transparent bucket: drawn after the opaque bucket (and hair) with depth
|
|
3571
|
+
// WRITE OFF, so a lace panel folded in front of itself blends both layers
|
|
3572
|
+
// consistently instead of a triangle-order patchwork of single/double
|
|
3573
|
+
// coverage. Partial-sheer cloth still casts shadows and keeps its outline;
|
|
3574
|
+
// fully sheer cloth (veil) does neither.
|
|
3575
|
+
const isTransparent = materialAlpha < 1.0 - 0.001 || sheer || partial
|
|
3576
|
+
// Load-time classification log — one line per material, cheap and
|
|
3577
|
+
// invaluable when a model renders wrong (bucket/outline/sheer disputes).
|
|
3578
|
+
console.info(
|
|
3579
|
+
`[reze] ${mat.name}: alpha=${materialAlpha.toFixed(2)} avg=${stats.avg.toFixed(2)} sheerFrac=${stats.translucentFrac.toFixed(2)} ${sheer ? "SHEER" : partial ? "PARTIAL" : "opaque"} bucket=${isTransparent ? "transparent" : "opaque"} edge=${(mat.edgeFlag & 0x10) !== 0 && mat.edgeSize > 0 ? (sheer ? "skipped(sheer)" : "on") : "off"}`,
|
|
3580
|
+
)
|
|
3497
3581
|
|
|
3498
3582
|
// Sphere map (sph=1 multiply / spa=2 add). Mode 3 (sub-texture UV) is
|
|
3499
3583
|
// rare and not implemented — treated as none, like a failed load.
|
|
@@ -3540,9 +3624,15 @@ export class Engine {
|
|
|
3540
3624
|
materialName: mat.name,
|
|
3541
3625
|
groupId: null,
|
|
3542
3626
|
baseBindGroupEntries,
|
|
3627
|
+
castsShadow: !sheer,
|
|
3543
3628
|
})
|
|
3544
3629
|
|
|
3545
|
-
|
|
3630
|
+
// No inverted-hull outline for SHEER materials: the outline shader draws a
|
|
3631
|
+
// solid edgeColor silhouette (it never samples texture alpha), so a
|
|
3632
|
+
// see-through veil dragged a near-black hull over the cloth behind it —
|
|
3633
|
+
// broken black shapes that waved with physics and flickered with camera
|
|
3634
|
+
// angle. A solid rim on see-through fabric is wrong in principle.
|
|
3635
|
+
if ((mat.edgeFlag & 0x10) !== 0 && mat.edgeSize > 0 && !sheer) {
|
|
3546
3636
|
const materialUniformData = new Float32Array([
|
|
3547
3637
|
mat.edgeColor[0],
|
|
3548
3638
|
mat.edgeColor[1],
|
|
@@ -4711,9 +4801,13 @@ export class Engine {
|
|
|
4711
4801
|
}
|
|
4712
4802
|
|
|
4713
4803
|
let pipeline: GPURenderPipeline
|
|
4804
|
+
let pipelineNoDepthWrite: GPURenderPipeline
|
|
4714
4805
|
let overEyesPipeline: GPURenderPipeline | undefined
|
|
4715
4806
|
try {
|
|
4716
4807
|
pipeline = await this.createRenderClassPipeline(renderClass, module, false)
|
|
4808
|
+
// Transparent-bucket draws don't write depth (self-overlap must blend, not
|
|
4809
|
+
// patchwork) — same shading, different depth state.
|
|
4810
|
+
pipelineNoDepthWrite = await this.createRenderClassPipeline(renderClass, module, false, false)
|
|
4717
4811
|
if (renderClass === "hair") overEyesPipeline = await this.createRenderClassPipeline(renderClass, module, true)
|
|
4718
4812
|
} catch (e) {
|
|
4719
4813
|
diagnostics.push({ severity: "error", message: `pipeline creation failed: ${(e as Error).message}` })
|
|
@@ -4739,6 +4833,7 @@ export class Engine {
|
|
|
4739
4833
|
renderClass,
|
|
4740
4834
|
alphaMode,
|
|
4741
4835
|
pipeline,
|
|
4836
|
+
pipelineNoDepthWrite,
|
|
4742
4837
|
overEyesPipeline,
|
|
4743
4838
|
uniformBuffer,
|
|
4744
4839
|
slotMap: result.slotMap,
|
|
@@ -4802,7 +4897,9 @@ export class Engine {
|
|
|
4802
4897
|
inst.drawCalls.sort(
|
|
4803
4898
|
(a, b) => typeOrder[a.type] - typeOrder[b.type] || this.drawCallRank(inst, a) - this.drawCallRank(inst, b),
|
|
4804
4899
|
)
|
|
4805
|
-
inst.shadowDrawCalls = inst.drawCalls.filter(
|
|
4900
|
+
inst.shadowDrawCalls = inst.drawCalls.filter(
|
|
4901
|
+
(d) => (d.type === "opaque" || d.type === "transparent") && d.castsShadow === true,
|
|
4902
|
+
)
|
|
4806
4903
|
}
|
|
4807
4904
|
|
|
4808
4905
|
/**
|
|
@@ -4814,6 +4911,7 @@ export class Engine {
|
|
|
4814
4911
|
renderClass: RenderClass,
|
|
4815
4912
|
module: GPUShaderModule,
|
|
4816
4913
|
overEyes: boolean,
|
|
4914
|
+
depthWrite = true,
|
|
4817
4915
|
): Promise<GPURenderPipeline> {
|
|
4818
4916
|
const base = {
|
|
4819
4917
|
label: `style ${renderClass}${overEyes ? " (over eyes)" : ""}`,
|
|
@@ -4824,7 +4922,7 @@ export class Engine {
|
|
|
4824
4922
|
}
|
|
4825
4923
|
const plainDepth: GPUDepthStencilState = {
|
|
4826
4924
|
format: "depth24plus-stencil8",
|
|
4827
|
-
depthWriteEnabled:
|
|
4925
|
+
depthWriteEnabled: depthWrite,
|
|
4828
4926
|
depthCompare: "less-equal",
|
|
4829
4927
|
}
|
|
4830
4928
|
let depthStencil: GPUDepthStencilState = plainDepth
|
|
@@ -4870,6 +4968,12 @@ export class Engine {
|
|
|
4870
4968
|
// Pipeline for a material draw call: its group's compiled pipeline when grouped, else
|
|
4871
4969
|
// the neutral base (ungrouped materials render the default graph).
|
|
4872
4970
|
private pipelineForDrawCall(inst: ModelInstance, dc: DrawCall): GPURenderPipeline {
|
|
4971
|
+
// Transparent draws WRITE depth — MMD semantics: PMX triangle/material order
|
|
4972
|
+
// is the author's compositing order, and a fold HIDES its far side rather
|
|
4973
|
+
// than blending it (the far side shades dark — light-averted — so letting it
|
|
4974
|
+
// show through read as gray fold-shaped stains; depth-write-off made every
|
|
4975
|
+
// fold do that). The no-write twins stay available for a future true-OIT
|
|
4976
|
+
// path but are deliberately unused.
|
|
4873
4977
|
if (dc.groupId) {
|
|
4874
4978
|
const install = inst.styleGroups.get(dc.groupId)
|
|
4875
4979
|
if (install) return install.pipeline
|
|
@@ -4910,6 +5014,7 @@ export class Engine {
|
|
|
4910
5014
|
* rebind group 0/1 correctly if needed.
|
|
4911
5015
|
*/
|
|
4912
5016
|
private drawOutlines(pass: GPURenderPassEncoder, inst: ModelInstance, type: DrawCallType): void {
|
|
5017
|
+
if (!this.outlineEnabled) return
|
|
4913
5018
|
let bound = false
|
|
4914
5019
|
for (const draw of inst.drawCalls) {
|
|
4915
5020
|
if (draw.type !== type || !this.shouldRenderDrawCall(inst, draw)) continue
|
|
@@ -4940,13 +5045,36 @@ export class Engine {
|
|
|
4940
5045
|
// and hairOverEyes (read equal). Non-stencil pipelines ignore the value.
|
|
4941
5046
|
pass.setStencilReference(Engine.STENCIL_EYE_VALUE)
|
|
4942
5047
|
|
|
5048
|
+
// Order matters (the black-hull saga): transparent color draws don't write
|
|
5049
|
+
// depth (self-overlap must double-blend, not patchwork), so ALL outlines
|
|
5050
|
+
// draw after the transparent depth PREPASS has recorded the fabric's depth
|
|
5051
|
+
// — otherwise every hull behind a sheer skirt shows straight through it.
|
|
4943
5052
|
this.drawMaterials(pass, inst, "opaque")
|
|
4944
|
-
this.drawOutlines(pass, inst, "opaque-outline")
|
|
4945
5053
|
this.drawHairOverEyes(pass, inst)
|
|
4946
5054
|
this.drawMaterials(pass, inst, "transparent")
|
|
5055
|
+
this.drawOutlines(pass, inst, "opaque-outline")
|
|
4947
5056
|
this.drawOutlines(pass, inst, "transparent-outline")
|
|
4948
5057
|
}
|
|
4949
5058
|
|
|
5059
|
+
/** Depth-only re-draw of transparent-bucket materials (see depth-prepass.ts).
|
|
5060
|
+
* Unused while transparent draws write depth themselves (MMD parity) — kept
|
|
5061
|
+
* for the dormant no-write/OIT path. */
|
|
5062
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
5063
|
+
protected drawTransparentDepthPrepass(pass: GPURenderPassEncoder, inst: ModelInstance): void {
|
|
5064
|
+
let bound = false
|
|
5065
|
+
for (const draw of inst.drawCalls) {
|
|
5066
|
+
if (draw.type !== "transparent" || !this.shouldRenderDrawCall(inst, draw)) continue
|
|
5067
|
+
if (!bound) {
|
|
5068
|
+
pass.setPipeline(this.transparentDepthPrepassPipeline)
|
|
5069
|
+
pass.setBindGroup(0, this.perFrameBindGroup)
|
|
5070
|
+
pass.setBindGroup(1, inst.mainPerInstanceBindGroup)
|
|
5071
|
+
bound = true
|
|
5072
|
+
}
|
|
5073
|
+
pass.setBindGroup(2, draw.bindGroup)
|
|
5074
|
+
pass.drawIndexed(draw.count, 1, draw.firstIndex, 0, 0)
|
|
5075
|
+
}
|
|
5076
|
+
}
|
|
5077
|
+
|
|
4950
5078
|
/**
|
|
4951
5079
|
* Second hair pass for the see-through-hair effect. Re-draws every hair-class grouped
|
|
4952
5080
|
* opaque draw with its compiled over-eyes pipeline — stencil-matched to `EYE_VALUE`,
|
package/src/graph/slots.ts
CHANGED
|
@@ -76,14 +76,23 @@ function prelude(renderClass: RenderClass, alphaMode: AlphaMode): string {
|
|
|
76
76
|
? " if (alpha < hashed_alpha_threshold(input.restPos)) { discard; }"
|
|
77
77
|
: " if (alpha < 0.001) { discard; }"
|
|
78
78
|
const gate = renderClass === "eye" ? EYE_REAR_GATE : ""
|
|
79
|
+
// Double-sided shading, winding-independent: a normal pointing away from the
|
|
80
|
+
// camera means we're seeing the surface's other side — flip it. Only genuinely
|
|
81
|
+
// camera-averted fragments change (front surfaces have dot(n,v) > 0 and are
|
|
82
|
+
// untouched), unlike a front_facing test, which PMX's clockwise winding
|
|
83
|
+
// inverts. Fixes inner skirt linings (裙子白1-style flipped-normal copies)
|
|
84
|
+
// shading near-black through sheer outer layers. Eye is exempt: it FRONT-culls
|
|
85
|
+
// by design, so its visible fragments are back faces with intended normals.
|
|
86
|
+
const flip =
|
|
87
|
+
renderClass === "eye" ? "" : "\n n = select(-n, n, dot(n, v) >= 0.0);"
|
|
79
88
|
return `@fragment fn fs(input: VertexOutput) -> FSOut {
|
|
80
89
|
let tex_s = textureSample(diffuseTexture, diffuseSampler, input.uv);
|
|
81
90
|
// MMD alpha semantics: material alpha × texture alpha.
|
|
82
91
|
let alpha = material.alpha * tex_s.a;
|
|
83
92
|
${discard}
|
|
84
93
|
|
|
85
|
-
|
|
86
|
-
let v = normalize(camera.viewPos - input.worldPos)
|
|
94
|
+
var n = safe_normal(input.normal);
|
|
95
|
+
let v = normalize(camera.viewPos - input.worldPos);${flip}
|
|
87
96
|
${gate}
|
|
88
97
|
let l = -light.lights[0].direction.xyz;
|
|
89
98
|
let sun = light.lights[0].color.xyz * light.lights[0].color.w;
|