reze-engine 0.56.8 → 0.56.10
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 +7 -3
- package/dist/engine.d.ts +41 -3
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +96 -13
- package/dist/graph/slots.js +1 -1
- 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 +20 -3
- package/dist/shaders/passes/cast-distance.d.ts +5 -1
- package/dist/shaders/passes/cast-distance.d.ts.map +1 -1
- package/dist/shaders/passes/cast-distance.js +21 -5
- package/dist/shaders/passes/depth-prepass.d.ts.map +1 -1
- package/dist/shaders/passes/depth-prepass.js +5 -1
- package/dist/shaders/passes/outline.d.ts.map +1 -1
- package/dist/shaders/passes/outline.js +5 -1
- package/dist/shaders/passes/shadow.d.ts +1 -1
- package/dist/shaders/passes/shadow.d.ts.map +1 -1
- package/dist/shaders/passes/shadow.js +5 -1
- package/package.json +1 -1
- package/src/engine.ts +110 -16
- package/src/graph/slots.ts +1 -1
- package/src/shaders/materials/common.ts +20 -3
- package/src/shaders/passes/cast-distance.ts +21 -5
- package/src/shaders/passes/depth-prepass.ts +5 -1
- package/src/shaders/passes/outline.ts +5 -1
- package/src/shaders/passes/shadow.ts +5 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cast-distance.d.ts","sourceRoot":"","sources":["../../../src/shaders/passes/cast-distance.ts"],"names":[],"mappings":"AA0CA;4EAC4E;AAC5E,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAEtD;AAED;;kEAEkE;AAClE,eAAO,MAAM,gBAAgB,EAAE,gBAA8B,CAAA;AAC7D;;8DAE8D;AAC9D,eAAO,MAAM,gBAAgB,EAAE,gBAA6B,CAAA;AAE5D;uDACuD;AACvD,eAAO,MAAM,oBAAoB,EAAE,gBAA4B,CAAA;AAE/D,iFAAiF;AACjF,eAAO,MAAM,cAAc,IAAI,CAAA;AAgB/B
|
|
1
|
+
{"version":3,"file":"cast-distance.d.ts","sourceRoot":"","sources":["../../../src/shaders/passes/cast-distance.ts"],"names":[],"mappings":"AA0CA;4EAC4E;AAC5E,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAEtD;AAED;;kEAEkE;AAClE,eAAO,MAAM,gBAAgB,EAAE,gBAA8B,CAAA;AAC7D;;8DAE8D;AAC9D,eAAO,MAAM,gBAAgB,EAAE,gBAA6B,CAAA;AAE5D;uDACuD;AACvD,eAAO,MAAM,oBAAoB,EAAE,gBAA4B,CAAA;AAE/D,iFAAiF;AACjF,eAAO,MAAM,cAAc,IAAI,CAAA;AAgB/B;;;;;;;;;;GAUG;AACH,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAqF3D;AAED;;;;;;;GAOG;AACH,wBAAgB,mBAAmB,IAAI,MAAM,CA4B5C;AAED;;;;;;;GAOG;AACH,wBAAgB,sBAAsB,IAAI,MAAM,CA4B/C;AAED;;;;;;;GAOG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAmC/F;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,gBAAgB,IAAI,MAAM,CAIzC"}
|
|
@@ -74,7 +74,11 @@ fn vs(@builtin(vertex_index) i: u32) -> VSOut {
|
|
|
74
74
|
*
|
|
75
75
|
* The ground, a stage and a media plane all write ids exactly as she does, so
|
|
76
76
|
* seeding "anything drawn" would grow a border around the floor: a rectangle
|
|
77
|
-
* round the frame rather than a sticker.
|
|
77
|
+
* round the frame rather than a sticker. The subject ids seed, and so do the
|
|
78
|
+
* props': a sword in her hand is part of her silhouette. A prop left out is a
|
|
79
|
+
* HOLE in the seeds wherever it covers her, and the field grows into it from
|
|
80
|
+
* her side, so the border ran round the inside of the prop instead of the
|
|
81
|
+
* outside.
|
|
78
82
|
*/
|
|
79
83
|
export function buildCastSeedShader(samples) {
|
|
80
84
|
return (
|
|
@@ -85,6 +89,9 @@ const RZ_ID_SAMPLES: i32 = ${samples};
|
|
|
85
89
|
|
|
86
90
|
@group(0) @binding(0) var _rzIdTex: texture_multisampled_2d<u32>;
|
|
87
91
|
@group(0) @binding(1) var<storage, read> _rzCast: array<vec4f>;
|
|
92
|
+
/** The visible props' object ids, count first. Props are not subjects, so the
|
|
93
|
+
* cast buffer does not carry them. */
|
|
94
|
+
@group(0) @binding(2) var<storage, read> _rzSeedProps: array<u32>;
|
|
88
95
|
|
|
89
96
|
// The two cast accessors this pass needs, spelled out rather than pulled in.
|
|
90
97
|
// CAST_API brings subjects, trails, anchors and their aliases with it, and a
|
|
@@ -100,6 +107,17 @@ fn rzSubjectId(i: i32) -> u32 {
|
|
|
100
107
|
if (i < 0 || i >= rzSubjectCount()) { return 0u; }
|
|
101
108
|
return u32(_rzCast[i * ${EFFECT_SUBJECT_VEC4S} + 1].w);
|
|
102
109
|
}
|
|
110
|
+
/** Does this object seed the field: a subject, or a prop. */
|
|
111
|
+
fn rzSeeds(o: u32, subjects: i32) -> bool {
|
|
112
|
+
for (var i = 0; i < subjects; i++) {
|
|
113
|
+
if (o == rzSubjectId(i)) { return true; }
|
|
114
|
+
}
|
|
115
|
+
let props = min(_rzSeedProps[0], arrayLength(&_rzSeedProps) - 1u);
|
|
116
|
+
for (var j = 1u; j <= props; j++) {
|
|
117
|
+
if (o == _rzSeedProps[j]) { return true; }
|
|
118
|
+
}
|
|
119
|
+
return false;
|
|
120
|
+
}
|
|
103
121
|
|
|
104
122
|
${FULLSCREEN_VS}
|
|
105
123
|
|
|
@@ -133,9 +151,7 @@ fn fs(@builtin(position) pos: vec4f) -> SeedOut {
|
|
|
133
151
|
for (var sample = 0; sample < RZ_ID_SAMPLES; sample++) {
|
|
134
152
|
let o = textureLoad(_rzIdTex, p, sample).y;
|
|
135
153
|
if (o == 0u) { continue; }
|
|
136
|
-
|
|
137
|
-
if (o == rzSubjectId(i)) { covered += 1.0; break; }
|
|
138
|
-
}
|
|
154
|
+
if (rzSeeds(o, n)) { covered += 1.0; }
|
|
139
155
|
}
|
|
140
156
|
var out: SeedOut;
|
|
141
157
|
out.coverage = covered / f32(RZ_ID_SAMPLES);
|
|
@@ -254,7 +270,7 @@ export function castDistanceApi(group, tex, samp, scale) {
|
|
|
254
270
|
*
|
|
255
271
|
* There is no ceiling. A pixel on the far side of the frame gets the real
|
|
256
272
|
* number. The ground, a stage and a media plane are not the cast and do not
|
|
257
|
-
* seed it.
|
|
273
|
+
* seed it. A prop does: what she holds is inside her border.
|
|
258
274
|
*
|
|
259
275
|
* Screen pixels whatever the field's own resolution is, so an author writes the
|
|
260
276
|
* width they mean and never has to know how this is built. uv is the effect's
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"depth-prepass.d.ts","sourceRoot":"","sources":["../../../src/shaders/passes/depth-prepass.ts"],"names":[],"mappings":"AAgCA,wBAAgB,2BAA2B,IAAI,MAAM,
|
|
1
|
+
{"version":3,"file":"depth-prepass.d.ts","sourceRoot":"","sources":["../../../src/shaders/passes/depth-prepass.ts"],"names":[],"mappings":"AAgCA,wBAAgB,2BAA2B,IAAI,MAAM,CA0FpD"}
|
|
@@ -60,6 +60,9 @@ struct VSOut {
|
|
|
60
60
|
// The bind-pose position, carried for one reason: the dissolve test below has
|
|
61
61
|
// to be the SAME test the colour pass runs, and that one is in object space.
|
|
62
62
|
@location(1) restPos: vec3f,
|
|
63
|
+
/** The triangle's own threshold, flat — see the material VertexOutput. This
|
|
64
|
+
* pass must throw away exactly the faces the colour pass does. */
|
|
65
|
+
@location(2) @interpolate(flat) faceT: f32,
|
|
63
66
|
};
|
|
64
67
|
|
|
65
68
|
@vertex fn vs(
|
|
@@ -81,6 +84,7 @@ struct VSOut {
|
|
|
81
84
|
o.position = camera.projection * camera.view * vec4f(skinned.xyz, 1.0);
|
|
82
85
|
o.uv = uv;
|
|
83
86
|
o.restPos = position;
|
|
87
|
+
o.faceT = rz_dissolve_threshold(position);
|
|
84
88
|
return o;
|
|
85
89
|
}
|
|
86
90
|
|
|
@@ -107,7 +111,7 @@ override CUTOFF: f32 = 0.5;
|
|
|
107
111
|
// the colour pass throws away would punch holes that occlude the floor behind
|
|
108
112
|
// her: you would see sky through her, which is the failure this line exists
|
|
109
113
|
// to prevent.
|
|
110
|
-
if (material.dissolve < 0.9995 &&
|
|
114
|
+
if (material.dissolve < 0.9995 && in.faceT > material.dissolve) { discard; }
|
|
111
115
|
var out: PrepassOut;
|
|
112
116
|
out.color = vec4f(0.0);
|
|
113
117
|
out.mask = vec4f(0.0);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"outline.d.ts","sourceRoot":"","sources":["../../../src/shaders/passes/outline.ts"],"names":[],"mappings":"AAqBA;;;;;;GAMG;AACH,eAAO,MAAM,0BAA0B,KAAK,CAAA;AAE5C,wBAAgB,iBAAiB,IAAI,MAAM,
|
|
1
|
+
{"version":3,"file":"outline.d.ts","sourceRoot":"","sources":["../../../src/shaders/passes/outline.ts"],"names":[],"mappings":"AAqBA;;;;;;GAMG;AACH,eAAO,MAAM,0BAA0B,KAAK,CAAA;AAE5C,wBAAgB,iBAAiB,IAAI,MAAM,CAoI1C"}
|
|
@@ -66,6 +66,9 @@ struct VertexOutput {
|
|
|
66
66
|
* the depth prepass measure against, so all three agree about which flakes
|
|
67
67
|
* are gone. */
|
|
68
68
|
@location(1) restPos: vec3f,
|
|
69
|
+
/** The triangle's own threshold, flat — see the material VertexOutput. The
|
|
70
|
+
* hull traces the body's faces, so it has to lose the same ones. */
|
|
71
|
+
@location(2) @interpolate(flat) faceT: f32,
|
|
69
72
|
};
|
|
70
73
|
|
|
71
74
|
@vertex fn vs(
|
|
@@ -116,6 +119,7 @@ struct VertexOutput {
|
|
|
116
119
|
output.position = vec4f(clipPos.xy + offset, clipPos.z, clipPos.w);
|
|
117
120
|
output.uv = uv;
|
|
118
121
|
output.restPos = position;
|
|
122
|
+
output.faceT = rz_dissolve_threshold(position);
|
|
119
123
|
return output;
|
|
120
124
|
}
|
|
121
125
|
|
|
@@ -141,7 +145,7 @@ ${sceneFsOutWgsl({ name: "FSOut", aux: "mask" })}
|
|
|
141
145
|
// The same test the colour pass and the depth prepass run, against the same
|
|
142
146
|
// bind-pose position — three passes, one rule, or they disagree about which
|
|
143
147
|
// pieces are still there.
|
|
144
|
-
if (material.dissolve < 0.9995 &&
|
|
148
|
+
if (material.dissolve < 0.9995 && input.faceT > material.dissolve) { discard; }
|
|
145
149
|
let texA = textureSample(diffuseTexture, edgeSampler, input.uv).a;
|
|
146
150
|
if (texA < 0.05) {
|
|
147
151
|
discard;
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export declare const SHADOW_DEPTH_SHADER_WGSL = "\n\n/** Clump size, in object-space units \u2014 MMD's are roughly centimetres. */\nconst RZ_DISSOLVE_GRAIN: f32 =
|
|
1
|
+
export declare const SHADOW_DEPTH_SHADER_WGSL = "\n\n/** Clump size, in object-space units \u2014 MMD's are roughly centimetres.\n *\n * Raised from 0.42: at that size the pieces are near enough to speckle that a\n * departure reads as a fade rather than as a body coming apart, which is the\n * thing the clumping exists to avoid. Hand-sized pieces are visible ones. */\nconst RZ_DISSOLVE_GRAIN: f32 = 1.2;\n/** How much the clumps pull a fragment off its own grit value \u2014 the whole of\n * the LOOK, and none of the rate. See rz_dissolve_threshold. */\nconst RZ_DISSOLVE_CLUMP: f32 = 0.6;\n/** Grit cell, as a fraction of a clump. See rz_dissolve_threshold: this is what\n * keeps a material SMALLER than a clump from having one threshold for all of\n * it, and it has to be smaller than the SMALLEST material a model carries \u2014\n * an eye highlight, a button, a buckle \u2014 not merely small. Two and a half\n * millimetres on a normal model, which is under all of them. */\nconst RZ_DISSOLVE_GRIT: f32 = 0.021;\n/** How wide the glowing front is, in threshold units. Thin: the front is a\n * rim, and a wide one lights whole limbs at once. */\nconst RZ_DISSOLVE_EDGE: f32 = 0.11;\n/** What the front burns with. Emission, added after lighting and after the\n * graph \u2014 a colour that ramps or takes a shadow reads as paint, not as heat.\n * Above 1 on purpose: it is meant to reach the bloom pyramid. Blue-violet,\n * the same light the motes leaving her are drawn and lit with. */\nconst RZ_BURN_COLOR: vec3f = vec3f(0.45, 0.62, 2.40);\n\nfn rz_dissolve_hash(p: vec3f) -> f32 {\n // INTEGER, for exactly the reason _hash33 in nodes.ts is, and its comment\n // says it: above 24 bits an f32 loses the low ones. The float hash this\n // replaces multiplied three terms into the hundreds of millions and took\n // fract() of the product \u2014 which past 2^24 is not noise, it is a quantised\n // staircase, and wherever that staircase landed on zero the threshold was\n // zero. A surface whose threshold is zero never crosses it: it stays behind\n // while the rest of her leaves, and it sits inside the burning front the\n // whole time she is gone. Those were the white pieces that would not go.\n //\n // Floored first, unlike _hash33: vec3i() truncates toward zero, so -0.5 and\n // +0.5 are the same cell, and half this model is at negative x.\n var h = vec3u(vec3i(floor(p)) + vec3i(32768));\n h = h * vec3u(1664525u, 1013904223u, 2654435761u);\n h = (h.yzx ^ h) * vec3u(2246822519u, 3266489917u, 668265263u);\n h = h ^ (h >> vec3u(16u));\n return f32((h.x ^ h.y ^ h.z) & 16777215u) * (1.0 / 16777216.0);\n}\n\n/** Value noise over that hash \u2014 smooth within a flake, uncorrelated between. */\nfn rz_dissolve_field(p: vec3f) -> f32 {\n let i = floor(p);\n let f = fract(p);\n let u = f * f * (3.0 - 2.0 * f);\n let c000 = rz_dissolve_hash(i);\n let c100 = rz_dissolve_hash(i + vec3f(1.0, 0.0, 0.0));\n let c010 = rz_dissolve_hash(i + vec3f(0.0, 1.0, 0.0));\n let c110 = rz_dissolve_hash(i + vec3f(1.0, 1.0, 0.0));\n let c001 = rz_dissolve_hash(i + vec3f(0.0, 0.0, 1.0));\n let c101 = rz_dissolve_hash(i + vec3f(1.0, 0.0, 1.0));\n let c011 = rz_dissolve_hash(i + vec3f(0.0, 1.0, 1.0));\n let c111 = rz_dissolve_hash(i + vec3f(1.0, 1.0, 1.0));\n let x00 = mix(c000, c100, u.x);\n let x10 = mix(c010, c110, u.x);\n let x01 = mix(c001, c101, u.x);\n let x11 = mix(c011, c111, u.x);\n return mix(mix(x00, x10, u.y), mix(x01, x11, u.y), u.z);\n}\n\n/**\n * The threshold this fragment is measured against.\n *\n * NOISE ALONE, so the whole body comes apart at once \u2014 a hand, a shoulder and\n * an ankle all thinning together rather than a line travelling up her. It was\n * tilted by height at first, on the argument that a sweep reads as something\n * happening TO her; what it actually reads as is a wipe, and a wipe has a\n * direction, an edge and a speed the rest of the effect knows nothing about.\n * Everywhere at once is also the honest match for what leaves: the motes are\n * shed from every limb in the same breath.\n *\n * TWO SCALES. Clumps alone meant every fragment of a material SMALLER than one\n * cell shared a single threshold: an eye highlight, a brow, a button, a hair\n * clip. All of it sat inside the glowing front at once, so the piece flashed\n * white as a whole and then popped \u2014 the two things a disintegration must never\n * do. The grit is a per-cell hash at a fraction of the clump, so a part a few\n * millimetres across still comes apart in pieces.\n *\n * THE GRIT IS THE BASE, though, and the clumps only push a fragment off it.\n * That ordering is what makes the dissolve smooth, and it is not obvious: the\n * two used to be WEIGHTED AND ADDED, and a sum is not uniformly distributed\n * however uniform each half is. It piles up in the middle, so most of the\n * surface had a threshold near a half \u2014 all of it went as the dissolve swept\n * through the middle, and what remained was the thin tail near zero. That tail\n * was the second stage everyone could see: a body that mostly vanished, then a\n * scatter of glowing scraps, then nothing.\n *\n * WRAPPED, not added. Offsetting a uniform value and taking fract() leaves it\n * EXACTLY uniform, where scaling a sum back into range only flattens the middle\n * \u2014 measured over four hundred thousand samples, the sum removed 12% of her per\n * step at the halfway mark and 0.1% at each end (a 110x swing), the scaled sum\n * still swung 89x, and the wrap is flat: 5.0% per step from the first flake to\n * the last, a ratio of 1.0.\n *\n * That flatness IS the smoothness. An uneven rate is what produced the two\n * stages you could see \u2014 a body that mostly vanished at once, then a scatter of\n * scraps that took as long again to follow.\n *\n * The clumping survives the wrap: neighbouring fragments share a clump value,\n * so the offset moves them together. What wraps past 1 comes back at 0 and goes\n * at the other end instead, which costs a little speckle in a region and buys\n * an even rate everywhere.\n */\nfn rz_dissolve_threshold(restPos: vec3f) -> f32 {\n let grit = rz_dissolve_hash(floor(restPos / (RZ_DISSOLVE_GRAIN * RZ_DISSOLVE_GRIT)));\n let clump = rz_dissolve_field(restPos / RZ_DISSOLVE_GRAIN) - 0.5;\n return fract(grit + clump * RZ_DISSOLVE_CLUMP);\n}\n\nstruct LightVP { viewProj: mat4x4f, };\n@group(0) @binding(0) var<uniform> lp: LightVP;\n@group(0) @binding(1) var<storage, read> skinMats: array<mat4x4f>;\n@group(0) @binding(2) var texSampler: sampler;\n@group(1) @binding(0) var diffuseTexture: texture_2d<f32>;\n// The head of the material block and the one field at its tail \u2014 the same\n// reach the depth prepass takes, and for the same reason: a shadow cast by a\n// body that is no longer drawn is the tell that the vanishing is a trick.\nstruct MaterialDiffuse {\n diffuse: vec4f,\n _skip0: vec4f,\n _skip1: vec4f,\n _skip2: vec3f,\n dissolve: f32,\n};\n@group(1) @binding(1) var<uniform> material: MaterialDiffuse;\n\nstruct VSOut {\n @builtin(position) position: vec4f,\n @location(0) uv: vec2f,\n /** Bind-pose position, for the dissolve test \u2014 object space, as everywhere. */\n @location(1) restPos: vec3f,\n /** The triangle's own threshold, flat \u2014 see the material VertexOutput. A\n * face that is gone must stop casting, and stop as a whole face. */\n @location(2) @interpolate(flat) faceT: f32,\n};\n\n@vertex fn vs(@location(0) position: vec3f, @location(1) normal: vec3f, @location(2) uv: vec2f,\n @location(3) joints0: vec4<u32>, @location(4) weights0: vec4<f32>) -> VSOut {\n let pos4 = vec4f(position, 1.0);\n let ws = weights0.x + weights0.y + weights0.z + weights0.w;\n let inv = select(1.0, 1.0 / ws, ws > 0.0001);\n let nw = select(vec4f(1.0,0.0,0.0,0.0), weights0 * inv, ws > 0.0001);\n var sp = vec4f(0.0);\n for (var i = 0u; i < 4u; i++) { sp += (skinMats[joints0[i]] * pos4) * nw[i]; }\n var out: VSOut;\n out.position = lp.viewProj * vec4f(sp.xyz, 1.0);\n out.uv = uv;\n out.restPos = position;\n out.faceT = rz_dissolve_threshold(position);\n return out;\n}\n\n@fragment fn fs(in: VSOut) {\n let alpha = textureSample(diffuseTexture, texSampler, in.uv).a * material.diffuse.a;\n if (alpha < 0.5) { discard; }\n // The dissolve, run as the colour pass runs it. A flake that is gone stops\n // casting: without this she leaves a whole shadow standing on the floor while\n // her body is in the air.\n if (material.dissolve < 0.9995 && in.faceT > material.dissolve) { discard; }\n}\n";
|
|
2
2
|
//# sourceMappingURL=shadow.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"shadow.d.ts","sourceRoot":"","sources":["../../../src/shaders/passes/shadow.ts"],"names":[],"mappings":"AASA,eAAO,MAAM,wBAAwB,
|
|
1
|
+
{"version":3,"file":"shadow.d.ts","sourceRoot":"","sources":["../../../src/shaders/passes/shadow.ts"],"names":[],"mappings":"AASA,eAAO,MAAM,wBAAwB,y1QAqDpC,CAAA"}
|
|
@@ -29,6 +29,9 @@ struct VSOut {
|
|
|
29
29
|
@location(0) uv: vec2f,
|
|
30
30
|
/** Bind-pose position, for the dissolve test — object space, as everywhere. */
|
|
31
31
|
@location(1) restPos: vec3f,
|
|
32
|
+
/** The triangle's own threshold, flat — see the material VertexOutput. A
|
|
33
|
+
* face that is gone must stop casting, and stop as a whole face. */
|
|
34
|
+
@location(2) @interpolate(flat) faceT: f32,
|
|
32
35
|
};
|
|
33
36
|
|
|
34
37
|
@vertex fn vs(@location(0) position: vec3f, @location(1) normal: vec3f, @location(2) uv: vec2f,
|
|
@@ -43,6 +46,7 @@ struct VSOut {
|
|
|
43
46
|
out.position = lp.viewProj * vec4f(sp.xyz, 1.0);
|
|
44
47
|
out.uv = uv;
|
|
45
48
|
out.restPos = position;
|
|
49
|
+
out.faceT = rz_dissolve_threshold(position);
|
|
46
50
|
return out;
|
|
47
51
|
}
|
|
48
52
|
|
|
@@ -52,6 +56,6 @@ struct VSOut {
|
|
|
52
56
|
// The dissolve, run as the colour pass runs it. A flake that is gone stops
|
|
53
57
|
// casting: without this she leaves a whole shadow standing on the floor while
|
|
54
58
|
// her body is in the air.
|
|
55
|
-
if (material.dissolve < 0.9995 &&
|
|
59
|
+
if (material.dissolve < 0.9995 && in.faceT > material.dissolve) { discard; }
|
|
56
60
|
}
|
|
57
61
|
`;
|
package/package.json
CHANGED
package/src/engine.ts
CHANGED
|
@@ -783,6 +783,17 @@ interface ModelInstance {
|
|
|
783
783
|
* outline. Kept here so that write has somewhere to go. */
|
|
784
784
|
outlineUniformBuffers: GPUBuffer[]
|
|
785
785
|
model: Model
|
|
786
|
+
/**
|
|
787
|
+
* Keep simulating this model's cloth while it is HIDDEN.
|
|
788
|
+
*
|
|
789
|
+
* Off by default, which is what makes a roster of resident alternate skins
|
|
790
|
+
* affordable: invisible cloth costs nothing. On for a model whose visibility
|
|
791
|
+
* is scheduled, because the frame it appears on is the frame its skirt has to
|
|
792
|
+
* be already dancing — a dress simulated from rest at the reveal snaps into
|
|
793
|
+
* place in front of the audience, and that pop is the whole reason a costume
|
|
794
|
+
* change reads as a glitch rather than as a cut.
|
|
795
|
+
*/
|
|
796
|
+
simulateWhileHidden: boolean
|
|
786
797
|
basePath: string
|
|
787
798
|
assetReader: AssetReader
|
|
788
799
|
gpuBuffers: GPUBuffer[]
|
|
@@ -815,9 +826,10 @@ interface ModelInstance {
|
|
|
815
826
|
/**
|
|
816
827
|
* A PROP: a PMX object a character holds or wears — a microphone, a fan, a
|
|
817
828
|
* sword. The third answer beside stage and plane. It keeps what a cast member
|
|
818
|
-
* has that scenery does not (physics, outlines, its own clip
|
|
819
|
-
*
|
|
820
|
-
* no
|
|
829
|
+
* has that scenery does not (physics, outlines, its own clip, a place in the
|
|
830
|
+
* cast's silhouette for rzCastDistance) and drops what makes one a performer:
|
|
831
|
+
* no effect subject id, no seeding of the scene clock, no bone picking. Like a
|
|
832
|
+
* card it leaves the floor alone. See addProp.
|
|
821
833
|
*/
|
|
822
834
|
isProp: boolean
|
|
823
835
|
/** Who this model hangs from, or null. Any model can: a prop by design, a
|
|
@@ -1982,6 +1994,10 @@ export class Engine {
|
|
|
1982
1994
|
private castStepBindGroups: GPUBindGroup[] = []
|
|
1983
1995
|
private castResolveBindGroup: GPUBindGroup | null = null
|
|
1984
1996
|
private castStepStrideBuffers: GPUBuffer[] = []
|
|
1997
|
+
/** The visible props' object ids for the seed pass, count in slot 0. Grown in
|
|
1998
|
+
* powers of two; see writeCastSeedProps. */
|
|
1999
|
+
private castSeedPropBuffer: GPUBuffer | null = null
|
|
2000
|
+
private castSeedPropData = new Uint32Array(8)
|
|
1985
2001
|
/** Does anything installed actually read the field? Set from the effect list. */
|
|
1986
2002
|
private castDistanceWanted = false
|
|
1987
2003
|
|
|
@@ -3679,14 +3695,7 @@ export class Engine {
|
|
|
3679
3695
|
const strides: number[] = []
|
|
3680
3696
|
for (let k = 1 << Math.ceil(Math.log2(Math.max(w, h))); k >= 1; k >>= 1) strides.push(k)
|
|
3681
3697
|
|
|
3682
|
-
this.
|
|
3683
|
-
label: "cast distance seed",
|
|
3684
|
-
layout: this.castSeedPipeline.getBindGroupLayout(0),
|
|
3685
|
-
entries: [
|
|
3686
|
-
{ binding: 0, resource: this.idView! },
|
|
3687
|
-
{ binding: 1, resource: { buffer: this.castBuffer } },
|
|
3688
|
-
],
|
|
3689
|
-
})
|
|
3698
|
+
this.createCastSeedBindGroup()
|
|
3690
3699
|
strides.forEach((stride, i) => {
|
|
3691
3700
|
const buf = this.device!.createBuffer({
|
|
3692
3701
|
label: `cast distance stride ${stride}`,
|
|
@@ -3718,6 +3727,59 @@ export class Engine {
|
|
|
3718
3727
|
})
|
|
3719
3728
|
}
|
|
3720
3729
|
|
|
3730
|
+
/** The seed pass's inputs: the id attachment, the cast, and the prop ids. */
|
|
3731
|
+
private createCastSeedBindGroup(): void {
|
|
3732
|
+
const device = this.device!
|
|
3733
|
+
if (!this.castSeedPropBuffer) {
|
|
3734
|
+
this.castSeedPropBuffer = device.createBuffer({
|
|
3735
|
+
label: "cast distance prop seeds",
|
|
3736
|
+
size: this.castSeedPropData.byteLength,
|
|
3737
|
+
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST,
|
|
3738
|
+
})
|
|
3739
|
+
device.queue.writeBuffer(this.castSeedPropBuffer, 0, this.castSeedPropData)
|
|
3740
|
+
}
|
|
3741
|
+
this.castSeedBindGroup = device.createBindGroup({
|
|
3742
|
+
label: "cast distance seed",
|
|
3743
|
+
layout: this.castSeedPipeline!.getBindGroupLayout(0),
|
|
3744
|
+
entries: [
|
|
3745
|
+
{ binding: 0, resource: this.idView! },
|
|
3746
|
+
{ binding: 1, resource: { buffer: this.castBuffer } },
|
|
3747
|
+
{ binding: 2, resource: { buffer: this.castSeedPropBuffer } },
|
|
3748
|
+
],
|
|
3749
|
+
})
|
|
3750
|
+
}
|
|
3751
|
+
|
|
3752
|
+
/**
|
|
3753
|
+
* Which props seed the field this frame: every visible one.
|
|
3754
|
+
*
|
|
3755
|
+
* A prop is not a subject — nothing follows it and no effect reads its bones —
|
|
3756
|
+
* but it is part of her silhouette. Uploaded only when the list changes.
|
|
3757
|
+
*/
|
|
3758
|
+
private writeCastSeedProps(): void {
|
|
3759
|
+
let n = 0
|
|
3760
|
+
this.forEachInstance((inst) => {
|
|
3761
|
+
if (inst.isProp && inst.model.visible) n++
|
|
3762
|
+
})
|
|
3763
|
+
let data = this.castSeedPropData
|
|
3764
|
+
let changed = data[0] !== n
|
|
3765
|
+
if (n + 1 > data.length) {
|
|
3766
|
+
data = new Uint32Array(1 << Math.ceil(Math.log2(n + 1)))
|
|
3767
|
+
this.castSeedPropData = data
|
|
3768
|
+
this.castSeedPropBuffer?.destroy()
|
|
3769
|
+
this.castSeedPropBuffer = null
|
|
3770
|
+
this.createCastSeedBindGroup()
|
|
3771
|
+
changed = true
|
|
3772
|
+
}
|
|
3773
|
+
data[0] = n
|
|
3774
|
+
let i = 1
|
|
3775
|
+
this.forEachInstance((inst) => {
|
|
3776
|
+
if (!inst.isProp || !inst.model.visible) return
|
|
3777
|
+
if (data[i] !== inst.objectId) changed = true
|
|
3778
|
+
data[i++] = inst.objectId
|
|
3779
|
+
})
|
|
3780
|
+
if (changed) this.device!.queue.writeBuffer(this.castSeedPropBuffer!, 0, data)
|
|
3781
|
+
}
|
|
3782
|
+
|
|
3721
3783
|
/**
|
|
3722
3784
|
* The flood, encoded once a frame before the field pass reads it.
|
|
3723
3785
|
*
|
|
@@ -3727,6 +3789,7 @@ export class Engine {
|
|
|
3727
3789
|
*/
|
|
3728
3790
|
private encodeCastDistance(encoder: GPUCommandEncoder): void {
|
|
3729
3791
|
if (!this.castDistanceWanted || !this.castSeedBindGroup || !this.castResolveBindGroup) return
|
|
3792
|
+
this.writeCastSeedProps()
|
|
3730
3793
|
const seed = encoder.beginRenderPass({
|
|
3731
3794
|
label: "cast distance (seed)",
|
|
3732
3795
|
colorAttachments: [
|
|
@@ -9455,6 +9518,8 @@ export class Engine {
|
|
|
9455
9518
|
inst.styleGroups.clear()
|
|
9456
9519
|
})
|
|
9457
9520
|
this.zeroStyleBuffer?.destroy()
|
|
9521
|
+
this.castSeedPropBuffer?.destroy()
|
|
9522
|
+
this.castSeedPropBuffer = null
|
|
9458
9523
|
this.releaseCullBuffers()
|
|
9459
9524
|
this.cullFrustaBuffer?.destroy()
|
|
9460
9525
|
this.cullFrustaBuffer = null
|
|
@@ -11029,6 +11094,25 @@ export class Engine {
|
|
|
11029
11094
|
this.forEachInstance((inst) => inst.physics?.setFloor(on))
|
|
11030
11095
|
}
|
|
11031
11096
|
|
|
11097
|
+
/**
|
|
11098
|
+
* Keep one model's cloth simulating while it is hidden.
|
|
11099
|
+
*
|
|
11100
|
+
* For scheduled visibility — a costume the scene swaps to, a double that
|
|
11101
|
+
* appears mid-shot. The body of a hidden model animates either way, so what
|
|
11102
|
+
* this buys is the cloth: switch it on and the skirt is already moving when
|
|
11103
|
+
* the model is revealed, instead of falling into place from rest in view.
|
|
11104
|
+
*
|
|
11105
|
+
* Per model rather than scene-wide, because it is exactly the models taking
|
|
11106
|
+
* part in a swap that need it, and a scene that never swaps should not pay
|
|
11107
|
+
* for cloth nobody can see.
|
|
11108
|
+
*/
|
|
11109
|
+
setModelPhysicsWhileHidden(modelName: string, on: boolean): boolean {
|
|
11110
|
+
const inst = this.modelInstances.get(modelName)
|
|
11111
|
+
if (!inst) return false
|
|
11112
|
+
inst.simulateWhileHidden = on
|
|
11113
|
+
return true
|
|
11114
|
+
}
|
|
11115
|
+
|
|
11032
11116
|
getPhysicsFloor(): boolean {
|
|
11033
11117
|
return this.physicsFloor
|
|
11034
11118
|
}
|
|
@@ -11120,10 +11204,13 @@ export class Engine {
|
|
|
11120
11204
|
inst.vertexBufferNeedsUpdate = true
|
|
11121
11205
|
}
|
|
11122
11206
|
// Hidden models keep animating (cheap, and a reveal must not pop a stale
|
|
11123
|
-
// pose) but skip cloth simulation
|
|
11207
|
+
// pose) but skip cloth simulation by default — a roster of resident
|
|
11124
11208
|
// alternate skins would otherwise pay full physics for invisible cloth.
|
|
11125
|
-
//
|
|
11126
|
-
|
|
11209
|
+
//
|
|
11210
|
+
// UNLESS the host asked for it: a model whose visibility is scheduled has
|
|
11211
|
+
// to arrive with its skirt already in motion, and cloth caught up at the
|
|
11212
|
+
// reveal is a snap everyone sees. See setModelPhysicsWhileHidden.
|
|
11213
|
+
if (inst.physics && this.physicsEnabled && (inst.model.visible || inst.simulateWhileHidden)) {
|
|
11127
11214
|
const tPhys = performance.now()
|
|
11128
11215
|
inst.physics.step(deltaTime, inst.model.getWorldMatrices(), inst.model.getBoneInverseBindMatrices())
|
|
11129
11216
|
// The step published new world matrices for the simulated bones; the
|
|
@@ -12218,6 +12305,7 @@ export class Engine {
|
|
|
12218
12305
|
const inst: ModelInstance = {
|
|
12219
12306
|
name,
|
|
12220
12307
|
model,
|
|
12308
|
+
simulateWhileHidden: false,
|
|
12221
12309
|
basePath,
|
|
12222
12310
|
assetReader,
|
|
12223
12311
|
gpuBuffers,
|
|
@@ -15086,7 +15174,8 @@ export class Engine {
|
|
|
15086
15174
|
let n = 0
|
|
15087
15175
|
let found: string | null = null
|
|
15088
15176
|
this.forEachInstance((inst) => {
|
|
15089
|
-
if (found !== null || n >= MAX_EFFECT_SUBJECTS || inst.isStage || inst.isPlane || inst.isProp)
|
|
15177
|
+
if (found !== null || n >= MAX_EFFECT_SUBJECTS || inst.isStage || inst.isPlane || inst.isProp || !inst.model.visible)
|
|
15178
|
+
return
|
|
15090
15179
|
if (n === index) found = inst.name
|
|
15091
15180
|
n++
|
|
15092
15181
|
})
|
|
@@ -15846,9 +15935,14 @@ export class Engine {
|
|
|
15846
15935
|
// video behind her and none at all around her. Four is the cap because the
|
|
15847
15936
|
// uniform is small and a scene with five characters is not the case this
|
|
15848
15937
|
// serves.
|
|
15938
|
+
// A HIDDEN model is not a subject either, and that is not tidiness: a
|
|
15939
|
+
// scene holding a costume's twin keeps it loaded and invisible, and with
|
|
15940
|
+
// it in the list it takes subject 0 by load order — so Teleportation
|
|
15941
|
+
// spawned its motes off a body nobody could see, and the silhouette field
|
|
15942
|
+
// seeded on one. The cast is who is ON STAGE.
|
|
15849
15943
|
let n = 0
|
|
15850
15944
|
this.forEachInstance((inst) => {
|
|
15851
|
-
if (n >= MAX_EFFECT_SUBJECTS || inst.isStage || inst.isPlane || inst.isProp) return
|
|
15945
|
+
if (n >= MAX_EFFECT_SUBJECTS || inst.isStage || inst.isPlane || inst.isProp || !inst.model.visible) return
|
|
15852
15946
|
const m = inst.model
|
|
15853
15947
|
// The model transform is only where the model was PLACED. A motion moves
|
|
15854
15948
|
// the character by animating bones, so an effect anchored to the
|
package/src/graph/slots.ts
CHANGED
|
@@ -115,7 +115,7 @@ ${discard}
|
|
|
115
115
|
// as burning away rather than as a stencil moving over her.
|
|
116
116
|
var rz_burn = 0.0;
|
|
117
117
|
if (material.dissolve < 0.9995) {
|
|
118
|
-
let rz_t =
|
|
118
|
+
let rz_t = input.faceT;
|
|
119
119
|
if (rz_t > material.dissolve) { discard; }
|
|
120
120
|
// Put OUT as the last of her goes. Near zero the front is wider than what
|
|
121
121
|
// is left of the surface, so every surviving fragment sits inside it and
|
|
@@ -99,6 +99,18 @@ struct VertexOutput {
|
|
|
99
99
|
// At rest skinMats are identity so restPos == worldPos, which is why existing noise-
|
|
100
100
|
// scale constants stay valid without retuning.
|
|
101
101
|
@location(3) restPos: vec3f,
|
|
102
|
+
/**
|
|
103
|
+
* This TRIANGLE's dissolve threshold.
|
|
104
|
+
*
|
|
105
|
+
* FLAT, so every fragment of a face shares the provoking vertex's value and
|
|
106
|
+
* the face goes as a unit — the mesh comes apart in triangles that light and
|
|
107
|
+
* leave in turn, rather than in a scatter that crosses them. Computed in the
|
|
108
|
+
* vertex stage from the bind-pose attribute, which is what lets the depth
|
|
109
|
+
* prepass, the shadow map and the outline hull reach the same number for the
|
|
110
|
+
* same face: one function, one input, one provoking vertex, one answer. They
|
|
111
|
+
* must agree — a pass that disagrees punches holes through her.
|
|
112
|
+
*/
|
|
113
|
+
@location(4) @interpolate(flat) faceT: f32,
|
|
102
114
|
};
|
|
103
115
|
|
|
104
116
|
// One view-projection per shadow cascade, inner to outer — the volumes built
|
|
@@ -239,6 +251,7 @@ const COMMON_VS_WGSL = /* wgsl */ `
|
|
|
239
251
|
output.uv = uv;
|
|
240
252
|
output.worldPos = skinnedPos.xyz;
|
|
241
253
|
output.restPos = position;
|
|
254
|
+
output.faceT = rz_dissolve_threshold(position);
|
|
242
255
|
return output;
|
|
243
256
|
}
|
|
244
257
|
|
|
@@ -297,8 +310,12 @@ export function commonFsOutWgsl(): string {
|
|
|
297
310
|
* distance is just a fade.
|
|
298
311
|
*/
|
|
299
312
|
export const DISSOLVE_WGSL = /* wgsl */ `
|
|
300
|
-
/** Clump size, in object-space units — MMD's are roughly centimetres.
|
|
301
|
-
|
|
313
|
+
/** Clump size, in object-space units — MMD's are roughly centimetres.
|
|
314
|
+
*
|
|
315
|
+
* Raised from 0.42: at that size the pieces are near enough to speckle that a
|
|
316
|
+
* departure reads as a fade rather than as a body coming apart, which is the
|
|
317
|
+
* thing the clumping exists to avoid. Hand-sized pieces are visible ones. */
|
|
318
|
+
const RZ_DISSOLVE_GRAIN: f32 = 1.2;
|
|
302
319
|
/** How much the clumps pull a fragment off its own grit value — the whole of
|
|
303
320
|
* the LOOK, and none of the rate. See rz_dissolve_threshold. */
|
|
304
321
|
const RZ_DISSOLVE_CLUMP: f32 = 0.6;
|
|
@@ -307,7 +324,7 @@ const RZ_DISSOLVE_CLUMP: f32 = 0.6;
|
|
|
307
324
|
* it, and it has to be smaller than the SMALLEST material a model carries —
|
|
308
325
|
* an eye highlight, a button, a buckle — not merely small. Two and a half
|
|
309
326
|
* millimetres on a normal model, which is under all of them. */
|
|
310
|
-
const RZ_DISSOLVE_GRIT: f32 = 0.
|
|
327
|
+
const RZ_DISSOLVE_GRIT: f32 = 0.021;
|
|
311
328
|
/** How wide the glowing front is, in threshold units. Thin: the front is a
|
|
312
329
|
* rim, and a wide one lights whole limbs at once. */
|
|
313
330
|
const RZ_DISSOLVE_EDGE: f32 = 0.11;
|
|
@@ -81,7 +81,11 @@ fn vs(@builtin(vertex_index) i: u32) -> VSOut {
|
|
|
81
81
|
*
|
|
82
82
|
* The ground, a stage and a media plane all write ids exactly as she does, so
|
|
83
83
|
* seeding "anything drawn" would grow a border around the floor: a rectangle
|
|
84
|
-
* round the frame rather than a sticker.
|
|
84
|
+
* round the frame rather than a sticker. The subject ids seed, and so do the
|
|
85
|
+
* props': a sword in her hand is part of her silhouette. A prop left out is a
|
|
86
|
+
* HOLE in the seeds wherever it covers her, and the field grows into it from
|
|
87
|
+
* her side, so the border ran round the inside of the prop instead of the
|
|
88
|
+
* outside.
|
|
85
89
|
*/
|
|
86
90
|
export function buildCastSeedShader(samples: number): string {
|
|
87
91
|
return (
|
|
@@ -92,6 +96,9 @@ const RZ_ID_SAMPLES: i32 = ${samples};
|
|
|
92
96
|
|
|
93
97
|
@group(0) @binding(0) var _rzIdTex: texture_multisampled_2d<u32>;
|
|
94
98
|
@group(0) @binding(1) var<storage, read> _rzCast: array<vec4f>;
|
|
99
|
+
/** The visible props' object ids, count first. Props are not subjects, so the
|
|
100
|
+
* cast buffer does not carry them. */
|
|
101
|
+
@group(0) @binding(2) var<storage, read> _rzSeedProps: array<u32>;
|
|
95
102
|
|
|
96
103
|
// The two cast accessors this pass needs, spelled out rather than pulled in.
|
|
97
104
|
// CAST_API brings subjects, trails, anchors and their aliases with it, and a
|
|
@@ -107,6 +114,17 @@ fn rzSubjectId(i: i32) -> u32 {
|
|
|
107
114
|
if (i < 0 || i >= rzSubjectCount()) { return 0u; }
|
|
108
115
|
return u32(_rzCast[i * ${EFFECT_SUBJECT_VEC4S} + 1].w);
|
|
109
116
|
}
|
|
117
|
+
/** Does this object seed the field: a subject, or a prop. */
|
|
118
|
+
fn rzSeeds(o: u32, subjects: i32) -> bool {
|
|
119
|
+
for (var i = 0; i < subjects; i++) {
|
|
120
|
+
if (o == rzSubjectId(i)) { return true; }
|
|
121
|
+
}
|
|
122
|
+
let props = min(_rzSeedProps[0], arrayLength(&_rzSeedProps) - 1u);
|
|
123
|
+
for (var j = 1u; j <= props; j++) {
|
|
124
|
+
if (o == _rzSeedProps[j]) { return true; }
|
|
125
|
+
}
|
|
126
|
+
return false;
|
|
127
|
+
}
|
|
110
128
|
|
|
111
129
|
${FULLSCREEN_VS}
|
|
112
130
|
|
|
@@ -140,9 +158,7 @@ fn fs(@builtin(position) pos: vec4f) -> SeedOut {
|
|
|
140
158
|
for (var sample = 0; sample < RZ_ID_SAMPLES; sample++) {
|
|
141
159
|
let o = textureLoad(_rzIdTex, p, sample).y;
|
|
142
160
|
if (o == 0u) { continue; }
|
|
143
|
-
|
|
144
|
-
if (o == rzSubjectId(i)) { covered += 1.0; break; }
|
|
145
|
-
}
|
|
161
|
+
if (rzSeeds(o, n)) { covered += 1.0; }
|
|
146
162
|
}
|
|
147
163
|
var out: SeedOut;
|
|
148
164
|
out.coverage = covered / f32(RZ_ID_SAMPLES);
|
|
@@ -265,7 +281,7 @@ export function castDistanceApi(group: number, tex: number, samp: number, scale:
|
|
|
265
281
|
*
|
|
266
282
|
* There is no ceiling. A pixel on the far side of the frame gets the real
|
|
267
283
|
* number. The ground, a stage and a media plane are not the cast and do not
|
|
268
|
-
* seed it.
|
|
284
|
+
* seed it. A prop does: what she holds is inside her border.
|
|
269
285
|
*
|
|
270
286
|
* Screen pixels whatever the field's own resolution is, so an author writes the
|
|
271
287
|
* width they mean and never has to know how this is built. uv is the effect's
|
|
@@ -62,6 +62,9 @@ struct VSOut {
|
|
|
62
62
|
// The bind-pose position, carried for one reason: the dissolve test below has
|
|
63
63
|
// to be the SAME test the colour pass runs, and that one is in object space.
|
|
64
64
|
@location(1) restPos: vec3f,
|
|
65
|
+
/** The triangle's own threshold, flat — see the material VertexOutput. This
|
|
66
|
+
* pass must throw away exactly the faces the colour pass does. */
|
|
67
|
+
@location(2) @interpolate(flat) faceT: f32,
|
|
65
68
|
};
|
|
66
69
|
|
|
67
70
|
@vertex fn vs(
|
|
@@ -83,6 +86,7 @@ struct VSOut {
|
|
|
83
86
|
o.position = camera.projection * camera.view * vec4f(skinned.xyz, 1.0);
|
|
84
87
|
o.uv = uv;
|
|
85
88
|
o.restPos = position;
|
|
89
|
+
o.faceT = rz_dissolve_threshold(position);
|
|
86
90
|
return o;
|
|
87
91
|
}
|
|
88
92
|
|
|
@@ -109,7 +113,7 @@ override CUTOFF: f32 = 0.5;
|
|
|
109
113
|
// the colour pass throws away would punch holes that occlude the floor behind
|
|
110
114
|
// her: you would see sky through her, which is the failure this line exists
|
|
111
115
|
// to prevent.
|
|
112
|
-
if (material.dissolve < 0.9995 &&
|
|
116
|
+
if (material.dissolve < 0.9995 && in.faceT > material.dissolve) { discard; }
|
|
113
117
|
var out: PrepassOut;
|
|
114
118
|
out.color = vec4f(0.0);
|
|
115
119
|
out.mask = vec4f(0.0);
|
|
@@ -70,6 +70,9 @@ struct VertexOutput {
|
|
|
70
70
|
* the depth prepass measure against, so all three agree about which flakes
|
|
71
71
|
* are gone. */
|
|
72
72
|
@location(1) restPos: vec3f,
|
|
73
|
+
/** The triangle's own threshold, flat — see the material VertexOutput. The
|
|
74
|
+
* hull traces the body's faces, so it has to lose the same ones. */
|
|
75
|
+
@location(2) @interpolate(flat) faceT: f32,
|
|
73
76
|
};
|
|
74
77
|
|
|
75
78
|
@vertex fn vs(
|
|
@@ -120,6 +123,7 @@ struct VertexOutput {
|
|
|
120
123
|
output.position = vec4f(clipPos.xy + offset, clipPos.z, clipPos.w);
|
|
121
124
|
output.uv = uv;
|
|
122
125
|
output.restPos = position;
|
|
126
|
+
output.faceT = rz_dissolve_threshold(position);
|
|
123
127
|
return output;
|
|
124
128
|
}
|
|
125
129
|
|
|
@@ -145,7 +149,7 @@ ${sceneFsOutWgsl({ name: "FSOut", aux: "mask" })}
|
|
|
145
149
|
// The same test the colour pass and the depth prepass run, against the same
|
|
146
150
|
// bind-pose position — three passes, one rule, or they disagree about which
|
|
147
151
|
// pieces are still there.
|
|
148
|
-
if (material.dissolve < 0.9995 &&
|
|
152
|
+
if (material.dissolve < 0.9995 && input.faceT > material.dissolve) { discard; }
|
|
149
153
|
let texA = textureSample(diffuseTexture, edgeSampler, input.uv).a;
|
|
150
154
|
if (texA < 0.05) {
|
|
151
155
|
discard;
|
|
@@ -31,6 +31,9 @@ struct VSOut {
|
|
|
31
31
|
@location(0) uv: vec2f,
|
|
32
32
|
/** Bind-pose position, for the dissolve test — object space, as everywhere. */
|
|
33
33
|
@location(1) restPos: vec3f,
|
|
34
|
+
/** The triangle's own threshold, flat — see the material VertexOutput. A
|
|
35
|
+
* face that is gone must stop casting, and stop as a whole face. */
|
|
36
|
+
@location(2) @interpolate(flat) faceT: f32,
|
|
34
37
|
};
|
|
35
38
|
|
|
36
39
|
@vertex fn vs(@location(0) position: vec3f, @location(1) normal: vec3f, @location(2) uv: vec2f,
|
|
@@ -45,6 +48,7 @@ struct VSOut {
|
|
|
45
48
|
out.position = lp.viewProj * vec4f(sp.xyz, 1.0);
|
|
46
49
|
out.uv = uv;
|
|
47
50
|
out.restPos = position;
|
|
51
|
+
out.faceT = rz_dissolve_threshold(position);
|
|
48
52
|
return out;
|
|
49
53
|
}
|
|
50
54
|
|
|
@@ -54,6 +58,6 @@ struct VSOut {
|
|
|
54
58
|
// The dissolve, run as the colour pass runs it. A flake that is gone stops
|
|
55
59
|
// casting: without this she leaves a whole shadow standing on the floor while
|
|
56
60
|
// her body is in the air.
|
|
57
|
-
if (material.dissolve < 0.9995 &&
|
|
61
|
+
if (material.dissolve < 0.9995 && in.faceT > material.dissolve) { discard; }
|
|
58
62
|
}
|
|
59
63
|
`
|