reze-engine 0.50.10 → 0.50.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +21 -0
- package/dist/graph/slots.d.ts.map +1 -1
- package/dist/graph/slots.js +7 -1
- package/dist/physics/autofit.d.ts +147 -0
- package/dist/physics/autofit.d.ts.map +1 -0
- package/dist/physics/autofit.js +501 -0
- package/dist/shaders/anchor-table.d.ts.map +1 -1
- package/dist/shaders/anchor-table.js +17 -0
- 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 +53 -15
- package/dist/shaders/passes/field-blit.d.ts +26 -0
- package/dist/shaders/passes/field-blit.d.ts.map +1 -0
- package/dist/shaders/passes/field-blit.js +65 -0
- package/dist/shaders/passes/ground-noise.d.ts +7 -0
- package/dist/shaders/passes/ground-noise.d.ts.map +1 -0
- package/dist/shaders/passes/ground-noise.js +88 -0
- package/dist/shaders/passes/shadow.d.ts +1 -1
- package/dist/shaders/passes/shadow.d.ts.map +1 -1
- package/dist/shaders/passes/sim.d.ts +34 -0
- package/dist/shaders/passes/sim.d.ts.map +1 -0
- package/dist/shaders/passes/sim.js +169 -0
- package/dist/shaders/score-api.d.ts +10 -0
- package/dist/shaders/score-api.d.ts.map +1 -0
- package/dist/shaders/score-api.js +114 -0
- package/package.json +1 -1
- package/src/engine.ts +21 -0
- package/src/graph/slots.ts +7 -1
- package/src/shaders/anchor-table.ts +17 -0
- package/src/shaders/materials/common.ts +53 -15
- package/dist/_dbg-loader.mjs +0 -1100
- package/dist/physics-debug.d.ts +0 -30
- package/dist/physics-debug.d.ts.map +0 -1
- package/dist/physics-debug.js +0 -526
- package/dist/shaders/materials/body.d.ts +0 -2
- package/dist/shaders/materials/body.d.ts.map +0 -1
- package/dist/shaders/materials/body.js +0 -95
- package/dist/shaders/materials/cloth_rough.d.ts +0 -2
- package/dist/shaders/materials/cloth_rough.d.ts.map +0 -1
- package/dist/shaders/materials/cloth_rough.js +0 -69
- package/dist/shaders/materials/cloth_smooth.d.ts +0 -2
- package/dist/shaders/materials/cloth_smooth.d.ts.map +0 -1
- package/dist/shaders/materials/cloth_smooth.js +0 -61
- package/dist/shaders/materials/default.d.ts +0 -2
- package/dist/shaders/materials/default.d.ts.map +0 -1
- package/dist/shaders/materials/default.js +0 -43
- package/dist/shaders/materials/eye.d.ts +0 -2
- package/dist/shaders/materials/eye.d.ts.map +0 -1
- package/dist/shaders/materials/eye.js +0 -60
- package/dist/shaders/materials/face.d.ts +0 -2
- package/dist/shaders/materials/face.d.ts.map +0 -1
- package/dist/shaders/materials/face.js +0 -95
- package/dist/shaders/materials/hair.d.ts +0 -2
- package/dist/shaders/materials/hair.d.ts.map +0 -1
- package/dist/shaders/materials/hair.js +0 -90
- package/dist/shaders/materials/metal.d.ts +0 -2
- package/dist/shaders/materials/metal.d.ts.map +0 -1
- package/dist/shaders/materials/metal.js +0 -77
- package/dist/shaders/materials/mmd_classic.d.ts +0 -2
- package/dist/shaders/materials/mmd_classic.d.ts.map +0 -1
- package/dist/shaders/materials/mmd_classic.js +0 -66
- package/dist/shaders/materials/stockings.d.ts +0 -2
- package/dist/shaders/materials/stockings.d.ts.map +0 -1
- package/dist/shaders/materials/stockings.js +0 -122
- package/dist/shaders/passes/physics-debug.d.ts +0 -2
- package/dist/shaders/passes/physics-debug.d.ts.map +0 -1
- package/dist/shaders/passes/physics-debug.js +0 -69
|
@@ -0,0 +1,501 @@
|
|
|
1
|
+
import { Vec3, Quat, Mat4 } from "../math";
|
|
2
|
+
import { RigidbodyShape, RigidbodyType } from "./types";
|
|
3
|
+
const DEFAULTS = {
|
|
4
|
+
minProtrusion: 0.05,
|
|
5
|
+
maxProtrusionRatio: 1.0,
|
|
6
|
+
minVertices: 24,
|
|
7
|
+
quantile: 0.02,
|
|
8
|
+
minGain: 0.1,
|
|
9
|
+
minDirectionality: 0.5,
|
|
10
|
+
lobeCos: 0.5,
|
|
11
|
+
minConcentration: 1.3,
|
|
12
|
+
maxShapesPerBody: 3,
|
|
13
|
+
radiusQuantile: 0.5,
|
|
14
|
+
lobeDepthFraction: 0.45,
|
|
15
|
+
};
|
|
16
|
+
const VERTEX_STRIDE = 8;
|
|
17
|
+
/**
|
|
18
|
+
* Fit supplementary colliders for every bone-following body whose own mesh
|
|
19
|
+
* escapes it.
|
|
20
|
+
*
|
|
21
|
+
* `vertices` is the model's interleaved vertex buffer (stride 8, position
|
|
22
|
+
* first) and `skinning` its joints/weights — both in PMX bind pose, the same
|
|
23
|
+
* space as `shapePosition` / `shapeRotation`, so nothing has to be skinned.
|
|
24
|
+
*
|
|
25
|
+
* Returns one entry per body it chose to supplement; the caller appends
|
|
26
|
+
* `.body` to the rigid body list before constructing RezePhysics.
|
|
27
|
+
*/
|
|
28
|
+
export function fitSupplementaryColliders(rigidbodies, vertices, skinning, options = {}) {
|
|
29
|
+
const opt = { ...DEFAULTS, ...options };
|
|
30
|
+
const vertexCount = vertices.length / VERTEX_STRIDE;
|
|
31
|
+
if (vertexCount === 0)
|
|
32
|
+
return [];
|
|
33
|
+
// Bucket vertices by their dominant bone once. Every body then reads only the
|
|
34
|
+
// mesh it stands in for, instead of the pass being O(bodies × vertices).
|
|
35
|
+
const byBone = new Map();
|
|
36
|
+
for (let i = 0; i < vertexCount; i++) {
|
|
37
|
+
let bestW = -1;
|
|
38
|
+
let bestB = -1;
|
|
39
|
+
for (let k = 0; k < 4; k++) {
|
|
40
|
+
const w = skinning.weights[i * 4 + k];
|
|
41
|
+
if (w > bestW) {
|
|
42
|
+
bestW = w;
|
|
43
|
+
bestB = skinning.joints[i * 4 + k];
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
if (bestB < 0)
|
|
47
|
+
continue;
|
|
48
|
+
const list = byBone.get(bestB);
|
|
49
|
+
if (list === undefined)
|
|
50
|
+
byBone.set(bestB, [i]);
|
|
51
|
+
else
|
|
52
|
+
list.push(i);
|
|
53
|
+
}
|
|
54
|
+
const out = [];
|
|
55
|
+
for (let b = 0; b < rigidbodies.length; b++) {
|
|
56
|
+
const rb = rigidbodies[b];
|
|
57
|
+
// Only bone-following proxies. A dynamic body IS the cloth — it has no
|
|
58
|
+
// skinned surface of its own to escape from.
|
|
59
|
+
if (rb.type === RigidbodyType.Dynamic && rb.mass > 0)
|
|
60
|
+
continue;
|
|
61
|
+
if (rb.boneIndex < 0)
|
|
62
|
+
continue;
|
|
63
|
+
const owned = byBone.get(rb.boneIndex);
|
|
64
|
+
if (owned === undefined || owned.length < opt.minVertices)
|
|
65
|
+
continue;
|
|
66
|
+
// Fit repeatedly, each pass measuring against what the earlier ones cover.
|
|
67
|
+
const covered = [];
|
|
68
|
+
for (let pass = 0; pass < opt.maxShapesPerBody; pass++) {
|
|
69
|
+
const fitted = fitOne(rb, b, owned, vertices, opt, covered);
|
|
70
|
+
if (fitted === null)
|
|
71
|
+
break;
|
|
72
|
+
covered.push(fitted.local);
|
|
73
|
+
out.push(fitted.result);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return out;
|
|
77
|
+
}
|
|
78
|
+
// Scratch, reused across bodies.
|
|
79
|
+
const _local = [];
|
|
80
|
+
function fitOne(rb, index, owned, vertices, opt, covered) {
|
|
81
|
+
const q = Quat.fromEuler(rb.shapeRotation.x, rb.shapeRotation.y, rb.shapeRotation.z);
|
|
82
|
+
const radius = shapeRadius(rb);
|
|
83
|
+
if (radius <= 0)
|
|
84
|
+
return null;
|
|
85
|
+
const maxProtrusion = radius * opt.maxProtrusionRatio;
|
|
86
|
+
// Collect protruding vertices in the SOURCE BODY's local frame. Working in
|
|
87
|
+
// that frame rather than the bone's means the fitted box inherits the
|
|
88
|
+
// proxy's own orientation, which is already aligned with the limb — no
|
|
89
|
+
// covariance fit, and no second convention to keep straight.
|
|
90
|
+
//
|
|
91
|
+
// Each one is stored with the direction it escapes in (the gradient of the
|
|
92
|
+
// signed distance), because which way the mesh escapes is what decides
|
|
93
|
+
// whether this is a feature worth fitting at all.
|
|
94
|
+
_local.length = 0;
|
|
95
|
+
let deepest = 0;
|
|
96
|
+
let seedX = 0, seedY = 1, seedZ = 0;
|
|
97
|
+
let sumProtrusion = 0;
|
|
98
|
+
for (let n = 0; n < owned.length; n++) {
|
|
99
|
+
const o = owned[n] * VERTEX_STRIDE;
|
|
100
|
+
_v.x = vertices[o] - rb.shapePosition.x;
|
|
101
|
+
_v.y = vertices[o + 1] - rb.shapePosition.y;
|
|
102
|
+
_v.z = vertices[o + 2] - rb.shapePosition.z;
|
|
103
|
+
// Into the body's local frame: rotate by the conjugate.
|
|
104
|
+
Quat.rotateVecInvInto(q, _v, _r);
|
|
105
|
+
const px = _r.x, py = _r.y, pz = _r.z;
|
|
106
|
+
const sd = coveredSignedDistance(rb, covered, px, py, pz);
|
|
107
|
+
if (sd <= opt.minProtrusion || sd > maxProtrusion)
|
|
108
|
+
continue;
|
|
109
|
+
coveredOutwardDir(rb, covered, px, py, pz, _r);
|
|
110
|
+
// Seed the lobe on the DEEPEST vertex, not the mean direction. A pelvis
|
|
111
|
+
// escapes its capsule at the back by 0.53 and at the front by 0.14, so the
|
|
112
|
+
// mean cancels below any usable threshold and the butt — the whole point —
|
|
113
|
+
// is never fitted. The deepest point is always ON the feature.
|
|
114
|
+
if (sd > deepest) {
|
|
115
|
+
deepest = sd;
|
|
116
|
+
seedX = _r.x;
|
|
117
|
+
seedY = _r.y;
|
|
118
|
+
seedZ = _r.z;
|
|
119
|
+
}
|
|
120
|
+
sumProtrusion += sd;
|
|
121
|
+
_local.push(px, py, pz, _r.x, _r.y, _r.z, sd);
|
|
122
|
+
}
|
|
123
|
+
let found = _local.length / 7;
|
|
124
|
+
if (found < opt.minVertices)
|
|
125
|
+
return null;
|
|
126
|
+
// Averaged over the PROTRUDING vertices, deliberately — the question being
|
|
127
|
+
// asked is "is the deepest lobe deeper than protrusion typically is on this
|
|
128
|
+
// body", which is exactly the test that separates a butt from a hairstyle.
|
|
129
|
+
//
|
|
130
|
+
// Averaging over all the body's vertices instead was tried and is worse: it
|
|
131
|
+
// lifts every ratio, and measured on 托特 it scored the head's hair (2.4–4.2)
|
|
132
|
+
// ABOVE the pelvis (2.85), so no threshold could separate them and the pass
|
|
133
|
+
// fitted 33 bodies including three around the hair.
|
|
134
|
+
//
|
|
135
|
+
// The known blind spot is a mesh that escapes in exactly one place at uniform
|
|
136
|
+
// depth: the lobe IS the whole protruding set, so the ratio is 1 and nothing
|
|
137
|
+
// is fitted. Real skin always has a gradient, and for a pass that adds bodies
|
|
138
|
+
// to someone else's rig, declining when the evidence is ambiguous is the
|
|
139
|
+
// right way to be wrong.
|
|
140
|
+
const meanProtrusion = sumProtrusion / found;
|
|
141
|
+
// Settle the lobe axis: take everything within lobeCos of the seed, recentre
|
|
142
|
+
// on that set's depth-weighted mean direction, repeat. Two passes is enough —
|
|
143
|
+
// the seed already sits on the feature, this only centres it.
|
|
144
|
+
let mdx = seedX, mdy = seedY, mdz = seedZ;
|
|
145
|
+
for (let pass = 0; pass < 2; pass++) {
|
|
146
|
+
let ax = 0, ay = 0, az = 0;
|
|
147
|
+
for (let i = 0; i < found; i++) {
|
|
148
|
+
const s = i * 7;
|
|
149
|
+
if (_local[s + 3] * mdx + _local[s + 4] * mdy + _local[s + 5] * mdz < opt.lobeCos)
|
|
150
|
+
continue;
|
|
151
|
+
const wgt = _local[s + 6];
|
|
152
|
+
ax += _local[s + 3] * wgt;
|
|
153
|
+
ay += _local[s + 4] * wgt;
|
|
154
|
+
az += _local[s + 5] * wgt;
|
|
155
|
+
}
|
|
156
|
+
const len = Math.sqrt(ax * ax + ay * ay + az * az);
|
|
157
|
+
if (len < 1e-9)
|
|
158
|
+
break;
|
|
159
|
+
mdx = ax / len;
|
|
160
|
+
mdy = ay / len;
|
|
161
|
+
mdz = az / len;
|
|
162
|
+
}
|
|
163
|
+
// Keep only the lobe. Fitting the whole protruding shell would wrap the limb
|
|
164
|
+
// and put box corners out in open air on the side that was already covered.
|
|
165
|
+
// Depth floor for the lobe's core, relative to the deepest point IN the lobe
|
|
166
|
+
// (not the body's overall deepest, which may sit in a different direction).
|
|
167
|
+
let lobeDeepest = 0;
|
|
168
|
+
for (let i = 0; i < found; i++) {
|
|
169
|
+
const s = i * 7;
|
|
170
|
+
if (_local[s + 3] * mdx + _local[s + 4] * mdy + _local[s + 5] * mdz < opt.lobeCos)
|
|
171
|
+
continue;
|
|
172
|
+
if (_local[s + 6] > lobeDeepest)
|
|
173
|
+
lobeDeepest = _local[s + 6];
|
|
174
|
+
}
|
|
175
|
+
const depthFloor = lobeDeepest * opt.lobeDepthFraction;
|
|
176
|
+
let w = 0;
|
|
177
|
+
let lobeSum = 0;
|
|
178
|
+
let dirX = 0, dirY = 0, dirZ = 0;
|
|
179
|
+
for (let i = 0; i < found; i++) {
|
|
180
|
+
const s = i * 7;
|
|
181
|
+
if (_local[s + 3] * mdx + _local[s + 4] * mdy + _local[s + 5] * mdz < opt.lobeCos)
|
|
182
|
+
continue;
|
|
183
|
+
if (_local[s + 6] < depthFloor)
|
|
184
|
+
continue;
|
|
185
|
+
lobeSum += _local[s + 6];
|
|
186
|
+
dirX += _local[s + 3];
|
|
187
|
+
dirY += _local[s + 4];
|
|
188
|
+
dirZ += _local[s + 5];
|
|
189
|
+
_local[w * 3 + 0] = _local[s + 0];
|
|
190
|
+
_local[w * 3 + 1] = _local[s + 1];
|
|
191
|
+
_local[w * 3 + 2] = _local[s + 2];
|
|
192
|
+
w++;
|
|
193
|
+
}
|
|
194
|
+
found = w;
|
|
195
|
+
if (found < opt.minVertices)
|
|
196
|
+
return null;
|
|
197
|
+
// Concentration: is this lobe actually a FEATURE, or is the whole mesh
|
|
198
|
+
// uniformly outside its proxy? Hair around a skull escapes by a similar depth
|
|
199
|
+
// in every direction, so its deepest lobe is no deeper than typical and the
|
|
200
|
+
// ratio sits near 1. A butt is markedly deeper than the pelvis average. This
|
|
201
|
+
// is what stops the pass from boxing a hairstyle.
|
|
202
|
+
//
|
|
203
|
+
// Only the FIRST pass is judged on it. The question it answers — is this
|
|
204
|
+
// proxy standing in for this mesh at all — is settled once per body. Later
|
|
205
|
+
// passes are filling in what the first capsule could not reach, and by then
|
|
206
|
+
// the remaining protrusion is evenly spread by construction, so re-applying
|
|
207
|
+
// the test would reject every one of them and cap coverage at a single
|
|
208
|
+
// capsule. They still have to clear minGain and the vertex count.
|
|
209
|
+
const concentration = meanProtrusion > 1e-6 ? lobeSum / found / meanProtrusion : 0;
|
|
210
|
+
if (covered.length === 0 && concentration < opt.minConcentration)
|
|
211
|
+
return null;
|
|
212
|
+
const directionality = Math.sqrt(dirX * dirX + dirY * dirY + dirZ * dirZ) / found;
|
|
213
|
+
if (directionality < opt.minDirectionality)
|
|
214
|
+
return null;
|
|
215
|
+
// Quantile-clipped extent per axis.
|
|
216
|
+
const lo = [0, 0, 0];
|
|
217
|
+
const hi = [0, 0, 0];
|
|
218
|
+
const axis = new Array(found);
|
|
219
|
+
for (let a = 0; a < 3; a++) {
|
|
220
|
+
for (let i = 0; i < found; i++)
|
|
221
|
+
axis[i] = _local[i * 3 + a];
|
|
222
|
+
axis.sort((x, y) => x - y);
|
|
223
|
+
const k = Math.min(found - 1, Math.floor(found * opt.quantile));
|
|
224
|
+
lo[a] = axis[k];
|
|
225
|
+
hi[a] = axis[found - 1 - k];
|
|
226
|
+
}
|
|
227
|
+
const hx = (hi[0] - lo[0]) * 0.5;
|
|
228
|
+
const hy = (hi[1] - lo[1]) * 0.5;
|
|
229
|
+
const hz = (hi[2] - lo[2]) * 0.5;
|
|
230
|
+
if (hx <= 0 || hy <= 0 || hz <= 0)
|
|
231
|
+
return null;
|
|
232
|
+
const cx = (hi[0] + lo[0]) * 0.5;
|
|
233
|
+
const cy = (hi[1] + lo[1]) * 0.5;
|
|
234
|
+
const cz = (hi[2] + lo[2]) * 0.5;
|
|
235
|
+
// Fit a CAPSULE, not a box, and keep the source body's own orientation.
|
|
236
|
+
//
|
|
237
|
+
// A box fitted to a curved lobe puts its eight corners out in open air — on
|
|
238
|
+
// 托特's butt the worst corner measured 0.81 units from the nearest skin
|
|
239
|
+
// vertex, and cloth resting on that corner would float further off the body
|
|
240
|
+
// than the clipping it was added to fix. A capsule has no corners.
|
|
241
|
+
//
|
|
242
|
+
// Reusing the source rotation verbatim is what makes this cheap AND correct.
|
|
243
|
+
// PMX capsules are Y-aligned in their own frame, and a proxy's axis already
|
|
244
|
+
// runs along its limb — 下半身 is a transverse pill across the hips, which is
|
|
245
|
+
// exactly the way a butt lobe runs. Inheriting the rotation means no
|
|
246
|
+
// covariance fit and, more to the point, no quaternion→Euler conversion whose
|
|
247
|
+
// convention could disagree with the loader's.
|
|
248
|
+
// Radius from the lobe's own perpendicular spread, at a quantile — NOT from
|
|
249
|
+
// the enclosing half-extent.
|
|
250
|
+
//
|
|
251
|
+
// This is what decides whether the result looks right. A capsule inflates
|
|
252
|
+
// equally in every direction perpendicular to its axis, so sizing the radius
|
|
253
|
+
// to ENCLOSE the lobe makes a wide shallow patch into a fat capsule: it only
|
|
254
|
+
// needed to reach the butt's depth, but it also grew to the butt's width, and
|
|
255
|
+
// the surplus is surface standing off the skin everywhere else. Measured on
|
|
256
|
+
// 托特's pelvis that was 0.68 units of standoff, which reads as a skirt
|
|
257
|
+
// hovering in idle — trading a clipping artifact for a worse one.
|
|
258
|
+
//
|
|
259
|
+
// Taking a quantile of the actual distances puts the capsule's surface
|
|
260
|
+
// through the lobe rather than around it. The deepest points end up slightly
|
|
261
|
+
// outside it, which the next pass picks up.
|
|
262
|
+
const perp = new Array(found);
|
|
263
|
+
for (let i = 0; i < found; i++) {
|
|
264
|
+
const dx = _local[i * 3] - cx;
|
|
265
|
+
const dz = _local[i * 3 + 2] - cz;
|
|
266
|
+
perp[i] = Math.sqrt(dx * dx + dz * dz);
|
|
267
|
+
}
|
|
268
|
+
perp.sort((a, b) => a - b);
|
|
269
|
+
const fitRadius = perp[Math.min(found - 1, Math.floor(found * opt.radiusQuantile))];
|
|
270
|
+
if (fitRadius <= 0)
|
|
271
|
+
return null;
|
|
272
|
+
// size.y is the FULL cylinder length between cap centres, so the capsule
|
|
273
|
+
// spans h + 2r along the axis. Solve for the lobe's own extent.
|
|
274
|
+
const cyl = Math.max(0, hy * 2 - fitRadius * 2);
|
|
275
|
+
// Gain and overshoot at the capsule's six axis extremes: how far it reaches
|
|
276
|
+
// past the shape it supplements, and how far it sits from the surface it was
|
|
277
|
+
// fitted to. Overshoot is the number to watch if a result looks puffy.
|
|
278
|
+
let gain = 0;
|
|
279
|
+
let overshoot = 0;
|
|
280
|
+
const half = cyl * 0.5;
|
|
281
|
+
const probes = [
|
|
282
|
+
[cx + fitRadius, cy, cz], [cx - fitRadius, cy, cz],
|
|
283
|
+
[cx, cy + half + fitRadius, cz], [cx, cy - half - fitRadius, cz],
|
|
284
|
+
[cx, cy, cz + fitRadius], [cx, cy, cz - fitRadius],
|
|
285
|
+
];
|
|
286
|
+
for (const [px, py, pz] of probes) {
|
|
287
|
+
const sd = coveredSignedDistance(rb, covered, px, py, pz);
|
|
288
|
+
if (sd > gain)
|
|
289
|
+
gain = sd;
|
|
290
|
+
// Only the part of the capsule that reaches OUTSIDE the original shape is
|
|
291
|
+
// new collision surface. A probe pointing inward has no skin vertex near it
|
|
292
|
+
// by construction — the lobe is a patch on the outside — and counting it
|
|
293
|
+
// would report overshoot for geometry that is buried in the character.
|
|
294
|
+
if (sd <= 0)
|
|
295
|
+
continue;
|
|
296
|
+
let nearest = Infinity;
|
|
297
|
+
for (let i = 0; i < found; i++) {
|
|
298
|
+
const dx = _local[i * 3] - px;
|
|
299
|
+
const dy = _local[i * 3 + 1] - py;
|
|
300
|
+
const dz = _local[i * 3 + 2] - pz;
|
|
301
|
+
const d2 = dx * dx + dy * dy + dz * dz;
|
|
302
|
+
if (d2 < nearest)
|
|
303
|
+
nearest = d2;
|
|
304
|
+
}
|
|
305
|
+
const d = Math.sqrt(nearest);
|
|
306
|
+
if (d > overshoot)
|
|
307
|
+
overshoot = d;
|
|
308
|
+
}
|
|
309
|
+
if (gain < opt.minGain)
|
|
310
|
+
return null;
|
|
311
|
+
// Back out to bind-pose world. Orientation is copied verbatim from the source
|
|
312
|
+
// rather than converted — the capsule is expressed in that frame, and
|
|
313
|
+
// re-deriving Euler angles would only introduce a convention to get wrong.
|
|
314
|
+
_v.x = cx;
|
|
315
|
+
_v.y = cy;
|
|
316
|
+
_v.z = cz;
|
|
317
|
+
Quat.rotateVecInto(q, _v, _r);
|
|
318
|
+
const wx = rb.shapePosition.x + _r.x;
|
|
319
|
+
const wy = rb.shapePosition.y + _r.y;
|
|
320
|
+
const wz = rb.shapePosition.z + _r.z;
|
|
321
|
+
const result = {
|
|
322
|
+
sourceIndex: index,
|
|
323
|
+
sourceName: rb.name,
|
|
324
|
+
vertexCount: owned.length,
|
|
325
|
+
protrudingCount: found,
|
|
326
|
+
maxProtrusion: deepest,
|
|
327
|
+
gain,
|
|
328
|
+
directionality,
|
|
329
|
+
concentration,
|
|
330
|
+
overshoot,
|
|
331
|
+
body: {
|
|
332
|
+
name: `${rb.name}__fit`,
|
|
333
|
+
englishName: `${rb.englishName}__fit`,
|
|
334
|
+
boneIndex: rb.boneIndex,
|
|
335
|
+
// Same group and mask as the body it supplements: it must be visible to
|
|
336
|
+
// exactly the cloth that was already meant to collide with this limb, and
|
|
337
|
+
// invisible to whatever the rigger excluded.
|
|
338
|
+
group: rb.group,
|
|
339
|
+
collisionMask: rb.collisionMask,
|
|
340
|
+
shape: RigidbodyShape.Capsule,
|
|
341
|
+
// PMX capsule semantics: size.x is the radius, size.y the FULL cylinder
|
|
342
|
+
// length between cap centres (Bullet halves it internally), size.z unused.
|
|
343
|
+
size: new Vec3(fitRadius, cyl, fitRadius),
|
|
344
|
+
shapePosition: new Vec3(wx, wy, wz),
|
|
345
|
+
shapeRotation: new Vec3(rb.shapeRotation.x, rb.shapeRotation.y, rb.shapeRotation.z),
|
|
346
|
+
// Static: it follows its bone and never integrates. Mass 0 keeps it out
|
|
347
|
+
// of the dynamic path entirely.
|
|
348
|
+
mass: 0,
|
|
349
|
+
linearDamping: 0,
|
|
350
|
+
angularDamping: 0,
|
|
351
|
+
restitution: rb.restitution,
|
|
352
|
+
friction: rb.friction,
|
|
353
|
+
type: RigidbodyType.Static,
|
|
354
|
+
bodyOffsetMatrixInverse: Mat4.identity(),
|
|
355
|
+
},
|
|
356
|
+
};
|
|
357
|
+
return { result, local: { cx, cy, cz, radius: fitRadius, cyl } };
|
|
358
|
+
}
|
|
359
|
+
/** Smallest characteristic dimension — the "how wide is this proxy" scale the
|
|
360
|
+
* protrusion cap is judged against. */
|
|
361
|
+
function shapeRadius(rb) {
|
|
362
|
+
switch (rb.shape) {
|
|
363
|
+
case RigidbodyShape.Sphere:
|
|
364
|
+
return rb.size.x;
|
|
365
|
+
case RigidbodyShape.Capsule:
|
|
366
|
+
return rb.size.x;
|
|
367
|
+
case RigidbodyShape.Box:
|
|
368
|
+
return Math.min(rb.size.x, rb.size.y, rb.size.z);
|
|
369
|
+
default:
|
|
370
|
+
return 0;
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
/** Signed distance to a Y-axis capsule centred at (cx,cy,cz), in the same
|
|
374
|
+
* local frame. `cyl` is the full length between cap centres. */
|
|
375
|
+
function capsuleSignedDistance(c, px, py, pz) {
|
|
376
|
+
const half = c.cyl * 0.5;
|
|
377
|
+
const dy = py - c.cy;
|
|
378
|
+
const t = dy > half ? half : dy < -half ? -half : dy;
|
|
379
|
+
const ax = px - c.cx;
|
|
380
|
+
const az = pz - c.cz;
|
|
381
|
+
const by = dy - t;
|
|
382
|
+
return Math.sqrt(ax * ax + by * by + az * az) - c.radius;
|
|
383
|
+
}
|
|
384
|
+
/** Distance to the UNION of the source shape and everything already fitted to
|
|
385
|
+
* it. Union is a min over signed distances, so a later pass sees only what is
|
|
386
|
+
* still exposed. */
|
|
387
|
+
function coveredSignedDistance(rb, covered, px, py, pz) {
|
|
388
|
+
let best = localSignedDistance(rb, px, py, pz);
|
|
389
|
+
for (let i = 0; i < covered.length; i++) {
|
|
390
|
+
const d = capsuleSignedDistance(covered[i], px, py, pz);
|
|
391
|
+
if (d < best)
|
|
392
|
+
best = d;
|
|
393
|
+
}
|
|
394
|
+
return best;
|
|
395
|
+
}
|
|
396
|
+
/** Outward direction from whichever member of the union is nearest — the
|
|
397
|
+
* gradient of coveredSignedDistance. */
|
|
398
|
+
function coveredOutwardDir(rb, covered, px, py, pz, out) {
|
|
399
|
+
let best = localSignedDistance(rb, px, py, pz);
|
|
400
|
+
let nearest = -1;
|
|
401
|
+
for (let i = 0; i < covered.length; i++) {
|
|
402
|
+
const d = capsuleSignedDistance(covered[i], px, py, pz);
|
|
403
|
+
if (d < best) {
|
|
404
|
+
best = d;
|
|
405
|
+
nearest = i;
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
if (nearest < 0) {
|
|
409
|
+
localOutwardDir(rb, px, py, pz, out);
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
412
|
+
// Same construction as localOutwardDir, for the winning capsule.
|
|
413
|
+
const c = covered[nearest];
|
|
414
|
+
const half = c.cyl * 0.5;
|
|
415
|
+
const dy = py - c.cy;
|
|
416
|
+
const t = dy > half ? half : dy < -half ? -half : dy;
|
|
417
|
+
const dx = px - c.cx;
|
|
418
|
+
const dz = pz - c.cz;
|
|
419
|
+
const by = dy - t;
|
|
420
|
+
const len = Math.sqrt(dx * dx + by * by + dz * dz);
|
|
421
|
+
if (len > 1e-9) {
|
|
422
|
+
out.x = dx / len;
|
|
423
|
+
out.y = by / len;
|
|
424
|
+
out.z = dz / len;
|
|
425
|
+
}
|
|
426
|
+
else {
|
|
427
|
+
out.x = 0;
|
|
428
|
+
out.y = 1;
|
|
429
|
+
out.z = 0;
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
/** Direction a body-local point escapes the shape in — the gradient of the
|
|
433
|
+
* signed distance, i.e. the unit vector from its closest surface point. */
|
|
434
|
+
function localOutwardDir(rb, px, py, pz, out) {
|
|
435
|
+
let dx, dy, dz;
|
|
436
|
+
switch (rb.shape) {
|
|
437
|
+
case RigidbodyShape.Capsule: {
|
|
438
|
+
const half = rb.size.y * 0.5;
|
|
439
|
+
const t = py > half ? half : py < -half ? -half : py;
|
|
440
|
+
dx = px;
|
|
441
|
+
dy = py - t;
|
|
442
|
+
dz = pz;
|
|
443
|
+
break;
|
|
444
|
+
}
|
|
445
|
+
case RigidbodyShape.Box: {
|
|
446
|
+
dx = px - Math.max(-rb.size.x, Math.min(rb.size.x, px));
|
|
447
|
+
dy = py - Math.max(-rb.size.y, Math.min(rb.size.y, py));
|
|
448
|
+
dz = pz - Math.max(-rb.size.z, Math.min(rb.size.z, pz));
|
|
449
|
+
break;
|
|
450
|
+
}
|
|
451
|
+
default: {
|
|
452
|
+
dx = px;
|
|
453
|
+
dy = py;
|
|
454
|
+
dz = pz;
|
|
455
|
+
break;
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
const len = Math.sqrt(dx * dx + dy * dy + dz * dz);
|
|
459
|
+
if (len > 1e-9) {
|
|
460
|
+
out.x = dx / len;
|
|
461
|
+
out.y = dy / len;
|
|
462
|
+
out.z = dz / len;
|
|
463
|
+
}
|
|
464
|
+
else {
|
|
465
|
+
out.x = 0;
|
|
466
|
+
out.y = 1;
|
|
467
|
+
out.z = 0;
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
/** Signed distance from a body-local point to the shape's surface; > 0 outside. */
|
|
471
|
+
function localSignedDistance(rb, px, py, pz) {
|
|
472
|
+
switch (rb.shape) {
|
|
473
|
+
case RigidbodyShape.Sphere:
|
|
474
|
+
return Math.sqrt(px * px + py * py + pz * pz) - rb.size.x;
|
|
475
|
+
case RigidbodyShape.Capsule: {
|
|
476
|
+
// PMX capsules are Y-aligned and size.y is the FULL cylinder length
|
|
477
|
+
// between cap centres, so the segment runs ±size.y/2.
|
|
478
|
+
const half = rb.size.y * 0.5;
|
|
479
|
+
const t = py > half ? half : py < -half ? -half : py;
|
|
480
|
+
const dy = py - t;
|
|
481
|
+
return Math.sqrt(px * px + dy * dy + pz * pz) - rb.size.x;
|
|
482
|
+
}
|
|
483
|
+
case RigidbodyShape.Box: {
|
|
484
|
+
// size is half-extents for boxes.
|
|
485
|
+
const qx = Math.abs(px) - rb.size.x;
|
|
486
|
+
const qy = Math.abs(py) - rb.size.y;
|
|
487
|
+
const qz = Math.abs(pz) - rb.size.z;
|
|
488
|
+
const ox = Math.max(qx, 0), oy = Math.max(qy, 0), oz = Math.max(qz, 0);
|
|
489
|
+
const outside = Math.sqrt(ox * ox + oy * oy + oz * oz);
|
|
490
|
+
return outside + Math.min(Math.max(qx, qy, qz), 0);
|
|
491
|
+
}
|
|
492
|
+
default:
|
|
493
|
+
return -Infinity;
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
// Rotation goes through Quat.rotateVecInto / rotateVecInvInto rather than
|
|
497
|
+
// open-coded component formulas — this pass runs once at load, so there is
|
|
498
|
+
// nothing to win by hand-rolling it and a sign error here would misplace every
|
|
499
|
+
// fitted body.
|
|
500
|
+
const _v = new Vec3(0, 0, 0);
|
|
501
|
+
const _r = new Vec3(0, 0, 0);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"anchor-table.d.ts","sourceRoot":"","sources":["../../src/shaders/anchor-table.ts"],"names":[],"mappings":"AAsBA,UAAU,aAAa;IACrB,IAAI,EAAE,MAAM,CAAA;IACZ,KAAK,EAAE,OAAO,CAAA;CACf;AAED,4EAA4E;AAC5E,eAAO,MAAM,kBAAkB,EAAE,WAAqD,CAAA;AAEtF,MAAM,WAAW,WAAW;IAC1B,+EAA+E;IAC/E,OAAO,EAAE,aAAa,EAAE,CAAA;IACxB,8EAA8E;IAC9E,KAAK,EAAE,MAAM,EAAE,EAAE,CAAA;IACjB,6EAA6E;IAC7E,OAAO,EAAE;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,EAAE,CAAA;CAC5C;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,aAAa,EAAE,EAAE,EAAE,GAAG,EAAE,MAAM,GAAG,WAAW,CAiCtF;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,cAAc,CAAC,UAAU,EAAE,MAAM,EAAE,GAAG,MAAM,CAkB3D;AAED;;;;;;;GAOG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,MAAM,
|
|
1
|
+
{"version":3,"file":"anchor-table.d.ts","sourceRoot":"","sources":["../../src/shaders/anchor-table.ts"],"names":[],"mappings":"AAsBA,UAAU,aAAa;IACrB,IAAI,EAAE,MAAM,CAAA;IACZ,KAAK,EAAE,OAAO,CAAA;CACf;AAED,4EAA4E;AAC5E,eAAO,MAAM,kBAAkB,EAAE,WAAqD,CAAA;AAEtF,MAAM,WAAW,WAAW;IAC1B,+EAA+E;IAC/E,OAAO,EAAE,aAAa,EAAE,CAAA;IACxB,8EAA8E;IAC9E,KAAK,EAAE,MAAM,EAAE,EAAE,CAAA;IACjB,6EAA6E;IAC7E,OAAO,EAAE;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,EAAE,CAAA;CAC5C;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,aAAa,EAAE,EAAE,EAAE,GAAG,EAAE,MAAM,GAAG,WAAW,CAiCtF;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,cAAc,CAAC,UAAU,EAAE,MAAM,EAAE,GAAG,MAAM,CAkB3D;AAED;;;;;;;GAOG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,MAAM,CAmCvD"}
|
|
@@ -108,6 +108,23 @@ ${cases}
|
|
|
108
108
|
* folds away.
|
|
109
109
|
*/
|
|
110
110
|
export function anchorAliasWgsl(alias) {
|
|
111
|
+
// NO @anchor AT ALL. Every slot is unmapped, and rzAnchor() must report
|
|
112
|
+
// .valid false for all of them.
|
|
113
|
+
//
|
|
114
|
+
// This used to fall through to the identity branch below — `[].every()` is
|
|
115
|
+
// true — so an effect that declared no bone and called rzAnchor(c, 0) got
|
|
116
|
+
// slot 0 of the SCENE's table: whichever bone the first effect that did
|
|
117
|
+
// declare one happened to name. Alone in a scene it read a zeroed buffer and
|
|
118
|
+
// looked correct; add a second effect and it silently anchored to that
|
|
119
|
+
// effect's wrist. Cross-effect bleed, and invisible from the file you were
|
|
120
|
+
// reading.
|
|
121
|
+
if (alias.length === 0) {
|
|
122
|
+
return `
|
|
123
|
+
/** No @anchor in this effect: every slot is unmapped, so rzAnchor() is invalid
|
|
124
|
+
* rather than reading whichever bone another effect declared first. */
|
|
125
|
+
fn _rzSlot(local: i32) -> i32 { return -1; }
|
|
126
|
+
`;
|
|
127
|
+
}
|
|
111
128
|
const identity = alias.every((g, i) => g === i);
|
|
112
129
|
if (identity) {
|
|
113
130
|
return `
|
|
@@ -19,6 +19,6 @@ export declare function commonFsOutWgsl(): string;
|
|
|
19
19
|
* clumps of a few millimetres rather than as single-pixel snow, which at any
|
|
20
20
|
* distance is just a fade.
|
|
21
21
|
*/
|
|
22
|
-
export declare const DISSOLVE_WGSL = "\n/** Clump size, in object-space units \u2014 MMD's are roughly centimetres. */\nconst RZ_DISSOLVE_GRAIN: f32 = 0.42;\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.
|
|
22
|
+
export declare const DISSOLVE_WGSL = "\n/** Clump size, in object-space units \u2014 MMD's are roughly centimetres. */\nconst RZ_DISSOLVE_GRAIN: f32 = 0.42;\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.06;\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";
|
|
23
23
|
export declare const COMMON_MATERIAL_PRELUDE_WGSL: string;
|
|
24
24
|
//# sourceMappingURL=common.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"common.d.ts","sourceRoot":"","sources":["../../../src/shaders/materials/common.ts"],"names":[],"mappings":"AA2BA,eAAO,MAAM,oBAAoB,QAuHhC,CAAC;AAgIF,wBAAgB,eAAe,IAAI,MAAM,CAExC;AAGD;;;;;;;;;;;;;;;;;;GAkBG;AACH,eAAO,MAAM,aAAa,
|
|
1
|
+
{"version":3,"file":"common.d.ts","sourceRoot":"","sources":["../../../src/shaders/materials/common.ts"],"names":[],"mappings":"AA2BA,eAAO,MAAM,oBAAoB,QAuHhC,CAAC;AAgIF,wBAAgB,eAAe,IAAI,MAAM,CAExC;AAGD;;;;;;;;;;;;;;;;;;GAkBG;AACH,eAAO,MAAM,aAAa,k8LA4GzB,CAAA;AAQD,eAAO,MAAM,4BAA4B,QAC0D,CAAA"}
|
|
@@ -288,10 +288,15 @@ export function commonFsOutWgsl() {
|
|
|
288
288
|
export const DISSOLVE_WGSL = /* wgsl */ `
|
|
289
289
|
/** Clump size, in object-space units — MMD's are roughly centimetres. */
|
|
290
290
|
const RZ_DISSOLVE_GRAIN: f32 = 0.42;
|
|
291
|
+
/** How much the clumps pull a fragment off its own grit value — the whole of
|
|
292
|
+
* the LOOK, and none of the rate. See rz_dissolve_threshold. */
|
|
293
|
+
const RZ_DISSOLVE_CLUMP: f32 = 0.6;
|
|
291
294
|
/** Grit cell, as a fraction of a clump. See rz_dissolve_threshold: this is what
|
|
292
295
|
* keeps a material SMALLER than a clump from having one threshold for all of
|
|
293
|
-
* it
|
|
294
|
-
|
|
296
|
+
* it, and it has to be smaller than the SMALLEST material a model carries —
|
|
297
|
+
* an eye highlight, a button, a buckle — not merely small. Two and a half
|
|
298
|
+
* millimetres on a normal model, which is under all of them. */
|
|
299
|
+
const RZ_DISSOLVE_GRIT: f32 = 0.06;
|
|
295
300
|
/** How wide the glowing front is, in threshold units. Thin: the front is a
|
|
296
301
|
* rim, and a wide one lights whole limbs at once. */
|
|
297
302
|
const RZ_DISSOLVE_EDGE: f32 = 0.11;
|
|
@@ -302,9 +307,22 @@ const RZ_DISSOLVE_EDGE: f32 = 0.11;
|
|
|
302
307
|
const RZ_BURN_COLOR: vec3f = vec3f(0.45, 0.62, 2.40);
|
|
303
308
|
|
|
304
309
|
fn rz_dissolve_hash(p: vec3f) -> f32 {
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
310
|
+
// INTEGER, for exactly the reason _hash33 in nodes.ts is, and its comment
|
|
311
|
+
// says it: above 24 bits an f32 loses the low ones. The float hash this
|
|
312
|
+
// replaces multiplied three terms into the hundreds of millions and took
|
|
313
|
+
// fract() of the product — which past 2^24 is not noise, it is a quantised
|
|
314
|
+
// staircase, and wherever that staircase landed on zero the threshold was
|
|
315
|
+
// zero. A surface whose threshold is zero never crosses it: it stays behind
|
|
316
|
+
// while the rest of her leaves, and it sits inside the burning front the
|
|
317
|
+
// whole time she is gone. Those were the white pieces that would not go.
|
|
318
|
+
//
|
|
319
|
+
// Floored first, unlike _hash33: vec3i() truncates toward zero, so -0.5 and
|
|
320
|
+
// +0.5 are the same cell, and half this model is at negative x.
|
|
321
|
+
var h = vec3u(vec3i(floor(p)) + vec3i(32768));
|
|
322
|
+
h = h * vec3u(1664525u, 1013904223u, 2654435761u);
|
|
323
|
+
h = (h.yzx ^ h) * vec3u(2246822519u, 3266489917u, 668265263u);
|
|
324
|
+
h = h ^ (h >> vec3u(16u));
|
|
325
|
+
return f32((h.x ^ h.y ^ h.z) & 16777215u) * (1.0 / 16777216.0);
|
|
308
326
|
}
|
|
309
327
|
|
|
310
328
|
/** Value noise over that hash — smooth within a flake, uncorrelated between. */
|
|
@@ -338,22 +356,42 @@ fn rz_dissolve_field(p: vec3f) -> f32 {
|
|
|
338
356
|
* Everywhere at once is also the honest match for what leaves: the motes are
|
|
339
357
|
* shed from every limb in the same breath.
|
|
340
358
|
*
|
|
341
|
-
* TWO SCALES
|
|
342
|
-
* failure. Clumps alone meant every fragment of a material SMALLER than one
|
|
359
|
+
* TWO SCALES. Clumps alone meant every fragment of a material SMALLER than one
|
|
343
360
|
* cell shared a single threshold: an eye highlight, a brow, a button, a hair
|
|
344
361
|
* clip. All of it sat inside the glowing front at once, so the piece flashed
|
|
345
|
-
* white as a whole and then popped
|
|
346
|
-
*
|
|
347
|
-
*
|
|
362
|
+
* white as a whole and then popped — the two things a disintegration must never
|
|
363
|
+
* do. The grit is a per-cell hash at a fraction of the clump, so a part a few
|
|
364
|
+
* millimetres across still comes apart in pieces.
|
|
365
|
+
*
|
|
366
|
+
* THE GRIT IS THE BASE, though, and the clumps only push a fragment off it.
|
|
367
|
+
* That ordering is what makes the dissolve smooth, and it is not obvious: the
|
|
368
|
+
* two used to be WEIGHTED AND ADDED, and a sum is not uniformly distributed
|
|
369
|
+
* however uniform each half is. It piles up in the middle, so most of the
|
|
370
|
+
* surface had a threshold near a half — all of it went as the dissolve swept
|
|
371
|
+
* through the middle, and what remained was the thin tail near zero. That tail
|
|
372
|
+
* was the second stage everyone could see: a body that mostly vanished, then a
|
|
373
|
+
* scatter of glowing scraps, then nothing.
|
|
374
|
+
*
|
|
375
|
+
* WRAPPED, not added. Offsetting a uniform value and taking fract() leaves it
|
|
376
|
+
* EXACTLY uniform, where scaling a sum back into range only flattens the middle
|
|
377
|
+
* — measured over four hundred thousand samples, the sum removed 12% of her per
|
|
378
|
+
* step at the halfway mark and 0.1% at each end (a 110x swing), the scaled sum
|
|
379
|
+
* still swung 89x, and the wrap is flat: 5.0% per step from the first flake to
|
|
380
|
+
* the last, a ratio of 1.0.
|
|
381
|
+
*
|
|
382
|
+
* That flatness IS the smoothness. An uneven rate is what produced the two
|
|
383
|
+
* stages you could see — a body that mostly vanished at once, then a scatter of
|
|
384
|
+
* scraps that took as long again to follow.
|
|
348
385
|
*
|
|
349
|
-
*
|
|
350
|
-
*
|
|
351
|
-
* the
|
|
386
|
+
* The clumping survives the wrap: neighbouring fragments share a clump value,
|
|
387
|
+
* so the offset moves them together. What wraps past 1 comes back at 0 and goes
|
|
388
|
+
* at the other end instead, which costs a little speckle in a region and buys
|
|
389
|
+
* an even rate everywhere.
|
|
352
390
|
*/
|
|
353
391
|
fn rz_dissolve_threshold(restPos: vec3f) -> f32 {
|
|
354
|
-
let clump = rz_dissolve_field(restPos / RZ_DISSOLVE_GRAIN);
|
|
355
392
|
let grit = rz_dissolve_hash(floor(restPos / (RZ_DISSOLVE_GRAIN * RZ_DISSOLVE_GRIT)));
|
|
356
|
-
|
|
393
|
+
let clump = rz_dissolve_field(restPos / RZ_DISSOLVE_GRAIN) - 0.5;
|
|
394
|
+
return fract(grit + clump * RZ_DISSOLVE_CLUMP);
|
|
357
395
|
}
|
|
358
396
|
`;
|
|
359
397
|
// ─── Convenience: full shared prelude ───────────────────────────────
|