paperlab 0.7.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -5
- package/dist/{FxPostPass-M66QMVVC.js → FxPostPass-V6YDK4H2.js} +23 -5
- package/dist/FxPostPass-V6YDK4H2.js.map +1 -0
- package/dist/chunk-232SU3AQ.js +34 -0
- package/dist/chunk-232SU3AQ.js.map +1 -0
- package/dist/{chunk-GB6BMHC3.js → chunk-7WOGU2D4.js} +7 -19
- package/dist/{chunk-GB6BMHC3.js.map → chunk-7WOGU2D4.js.map} +1 -1
- package/dist/{chunk-MZCAN3AR.js → chunk-SVWWC5UL.js} +576 -160
- package/dist/chunk-SVWWC5UL.js.map +1 -0
- package/dist/{damageContract-DPF1YFLZ.d.cts → damageContract-nF1fgK7P.d.cts} +31 -3
- package/dist/{damageContract-DPF1YFLZ.d.ts → damageContract-nF1fgK7P.d.ts} +31 -3
- package/dist/fx.cjs +320 -91
- package/dist/fx.cjs.map +1 -1
- package/dist/fx.d.cts +81 -55
- package/dist/fx.d.ts +81 -55
- package/dist/fx.js +278 -48
- package/dist/fx.js.map +1 -1
- package/dist/index.cjs +606 -181
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +13 -5
- package/dist/index.d.ts +13 -5
- package/dist/index.js +17 -4
- package/dist/index.js.map +1 -1
- package/dist/{slots-C5PqY0Q5.d.cts → slots-D_EE8GXn.d.cts} +12 -0
- package/dist/{slots-C5PqY0Q5.d.ts → slots-D_EE8GXn.d.ts} +12 -0
- package/dist/stage.cjs +600 -188
- package/dist/stage.cjs.map +1 -1
- package/dist/stage.d.cts +1 -1
- package/dist/stage.d.ts +1 -1
- package/dist/stage.js +2 -2
- package/package.json +1 -1
- package/dist/FxPostPass-M66QMVVC.js.map +0 -1
- package/dist/chunk-7TLIWPGM.js +0 -36
- package/dist/chunk-7TLIWPGM.js.map +0 -1
- package/dist/chunk-MZCAN3AR.js.map +0 -1
package/dist/stage.cjs
CHANGED
|
@@ -1766,8 +1766,23 @@ var backdropSchema = import_zod15.z.object({
|
|
|
1766
1766
|
/** Out of focus, for the same reason. */
|
|
1767
1767
|
blur: import_zod15.z.number().min(0).max(1).default(0.2)
|
|
1768
1768
|
});
|
|
1769
|
+
var floorSchema = import_zod15.z.object({
|
|
1770
|
+
enabled: import_zod15.z.boolean().default(false),
|
|
1771
|
+
/** Dark and matte: it is the ground, not the subject. */
|
|
1772
|
+
color: import_zod15.z.string().default("#1a1917").describe("color"),
|
|
1773
|
+
/**
|
|
1774
|
+
* Where it lies, in the sheet's own space. The contact shadow falls here
|
|
1775
|
+
* too, and a simulated sheet stops here — one floor, one height, so paper
|
|
1776
|
+
* cannot land through the thing its shadow is on.
|
|
1777
|
+
*/
|
|
1778
|
+
y: import_zod15.z.number().min(-4).max(0).default(-1.05),
|
|
1779
|
+
/** How rough it is, 0 mirror to 1 chalk. Slate, by default. */
|
|
1780
|
+
roughness: import_zod15.z.number().min(0).max(1).default(0.92)
|
|
1781
|
+
});
|
|
1769
1782
|
var sceneSchema = import_zod15.z.object({
|
|
1770
1783
|
lighting: import_zod15.z.enum(lightingNames).default("studio"),
|
|
1784
|
+
/** The ground under the sheet — see `floorSchema`. Off unless asked for. */
|
|
1785
|
+
floor: floorSchema.prefault({}),
|
|
1771
1786
|
/** What is behind the sheet. Unset leaves the canvas alone. */
|
|
1772
1787
|
backdrop: backdropSchema.optional(),
|
|
1773
1788
|
/**
|
|
@@ -2195,6 +2210,34 @@ function buildEnvironment(renderer, preset) {
|
|
|
2195
2210
|
};
|
|
2196
2211
|
}
|
|
2197
2212
|
|
|
2213
|
+
// src/surface/damageContract.ts
|
|
2214
|
+
function roomLight(source) {
|
|
2215
|
+
const room = source?.firelight?.room;
|
|
2216
|
+
if (room === void 0 || !(room >= 0)) return 1;
|
|
2217
|
+
return Math.min(1, room);
|
|
2218
|
+
}
|
|
2219
|
+
var DAMAGE_LOOK_DEFAULTS = {
|
|
2220
|
+
emberWidth: 1.25,
|
|
2221
|
+
emberIntensity: 1.05,
|
|
2222
|
+
emberCoverage: 0.6,
|
|
2223
|
+
emberFlicker: 1.65,
|
|
2224
|
+
emberGlow: 1.25,
|
|
2225
|
+
sparkle: 0.5,
|
|
2226
|
+
// As wide as the char band (Noor, 2026-09-13): the lip has to be seen. At
|
|
2227
|
+
// 0.95 mm it was a hairline tracing the edge.
|
|
2228
|
+
lipWidth: 3.5,
|
|
2229
|
+
charWidth: 3.5,
|
|
2230
|
+
lipBrightness: 0.76,
|
|
2231
|
+
charWarmth: 0.32,
|
|
2232
|
+
charCracks: 0.45,
|
|
2233
|
+
scorchReach: 9,
|
|
2234
|
+
scorchDarkness: 0.86,
|
|
2235
|
+
fingers: 1.95,
|
|
2236
|
+
edgeWave: 13,
|
|
2237
|
+
edgeBite: 6
|
|
2238
|
+
};
|
|
2239
|
+
var DAMAGE_CHANNELS = { char: 0, saturation: 1, heat: 2, presence: 3 };
|
|
2240
|
+
|
|
2198
2241
|
// src/scene/PaperLighting.tsx
|
|
2199
2242
|
var import_jsx_runtime2 = require("react/jsx-runtime");
|
|
2200
2243
|
var toneMappings = {
|
|
@@ -2293,7 +2336,8 @@ function PaperLighting({
|
|
|
2293
2336
|
reducedMotion,
|
|
2294
2337
|
shadowMapSize,
|
|
2295
2338
|
contactShadow = true,
|
|
2296
|
-
environment = true
|
|
2339
|
+
environment = true,
|
|
2340
|
+
damage
|
|
2297
2341
|
}) {
|
|
2298
2342
|
const p2 = (0, import_react2.useMemo)(() => rig ?? resolveLighting(preset, light), [rig, preset, light]);
|
|
2299
2343
|
const mapSize = shadowMapSize ?? p2.shadow.mapSize;
|
|
@@ -2327,6 +2371,20 @@ function PaperLighting({
|
|
|
2327
2371
|
driftRef.current += delta * p2.gobo.drift;
|
|
2328
2372
|
goboMap.offset.set(driftRef.current, driftRef.current * 0.6);
|
|
2329
2373
|
});
|
|
2374
|
+
const keyIntensity = p2.gobo && goboMap ? p2.key.intensity * 3.2 : p2.key.intensity;
|
|
2375
|
+
const keyRef = (0, import_react2.useRef)(null);
|
|
2376
|
+
const ambientRef = (0, import_react2.useRef)(null);
|
|
2377
|
+
const hemisphereRef = (0, import_react2.useRef)(null);
|
|
2378
|
+
const roomRef = (0, import_react2.useRef)(1);
|
|
2379
|
+
(0, import_fiber.useFrame)(() => {
|
|
2380
|
+
const room = roomLight(damage);
|
|
2381
|
+
if (room === 1 && roomRef.current === 1) return;
|
|
2382
|
+
roomRef.current = room;
|
|
2383
|
+
if (keyRef.current) keyRef.current.intensity = keyIntensity * room;
|
|
2384
|
+
if (ambientRef.current) ambientRef.current.intensity = p2.ambient * room;
|
|
2385
|
+
if (hemisphereRef.current) hemisphereRef.current.intensity = p2.studio * HEMISPHERE_STAND_IN * room;
|
|
2386
|
+
if (p2.studio > 0 && environment) scene.environmentIntensity = p2.studio * room;
|
|
2387
|
+
});
|
|
2330
2388
|
return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_jsx_runtime2.Fragment, { children: [
|
|
2331
2389
|
p2.studio > 0 && (environment ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(StudioLight, { rig: p2 }) : (
|
|
2332
2390
|
// The studio light degrades rather than disappearing. A hemisphere
|
|
@@ -2337,19 +2395,21 @@ function PaperLighting({
|
|
|
2337
2395
|
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
2338
2396
|
"hemisphereLight",
|
|
2339
2397
|
{
|
|
2398
|
+
ref: hemisphereRef,
|
|
2340
2399
|
color: p2.sky.horizon,
|
|
2341
2400
|
groundColor: p2.sky.ground,
|
|
2342
2401
|
intensity: p2.studio * HEMISPHERE_STAND_IN
|
|
2343
2402
|
}
|
|
2344
2403
|
)
|
|
2345
2404
|
)),
|
|
2346
|
-
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("ambientLight", { intensity: p2.ambient }),
|
|
2405
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("ambientLight", { ref: ambientRef, intensity: p2.ambient }),
|
|
2347
2406
|
p2.gobo && goboMap ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
2348
2407
|
"spotLight",
|
|
2349
2408
|
{
|
|
2409
|
+
ref: keyRef,
|
|
2350
2410
|
position: p2.key.position,
|
|
2351
2411
|
color: p2.key.color,
|
|
2352
|
-
intensity:
|
|
2412
|
+
intensity: keyIntensity,
|
|
2353
2413
|
angle: p2.gobo.angle,
|
|
2354
2414
|
penumbra: 0.5,
|
|
2355
2415
|
decay: 0,
|
|
@@ -2362,9 +2422,10 @@ function PaperLighting({
|
|
|
2362
2422
|
) : /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
2363
2423
|
"directionalLight",
|
|
2364
2424
|
{
|
|
2425
|
+
ref: keyRef,
|
|
2365
2426
|
position: p2.key.position,
|
|
2366
2427
|
color: p2.key.color,
|
|
2367
|
-
intensity:
|
|
2428
|
+
intensity: keyIntensity,
|
|
2368
2429
|
castShadow,
|
|
2369
2430
|
"shadow-mapSize": [mapSize || 1, mapSize || 1],
|
|
2370
2431
|
"shadow-radius": p2.shadow.radius,
|
|
@@ -4360,6 +4421,8 @@ var ClothSim = class {
|
|
|
4360
4421
|
height;
|
|
4361
4422
|
positions;
|
|
4362
4423
|
prev;
|
|
4424
|
+
/** Where the constructor laid every particle out — what {@link restore} puts back. */
|
|
4425
|
+
laidOut;
|
|
4363
4426
|
pinned;
|
|
4364
4427
|
pinTargets;
|
|
4365
4428
|
/**
|
|
@@ -4435,6 +4498,16 @@ var ClothSim = class {
|
|
|
4435
4498
|
* cannot do this job: it moves paper, and a uniformly curled sheet in still
|
|
4436
4499
|
* air has no business going anywhere.
|
|
4437
4500
|
*/
|
|
4501
|
+
/**
|
|
4502
|
+
* An acceleration on each particle from whatever is in the air around the
|
|
4503
|
+
* sheet, xyz — the fire's updraft, so far (`DamageCoupling`). Not the wind:
|
|
4504
|
+
* the wind is one thing blowing on the whole sheet, and this is local, a
|
|
4505
|
+
* hot column over a burning rim and nothing a hand's width away.
|
|
4506
|
+
*
|
|
4507
|
+
* An acceleration rather than a force, so it lifts a soaked sheet no
|
|
4508
|
+
* differently from a dry one: hot air does not know what paper weighs.
|
|
4509
|
+
*/
|
|
4510
|
+
draught;
|
|
4438
4511
|
restBend;
|
|
4439
4512
|
/** For each bend spring, the particle between its ends; -1 for every other kind. */
|
|
4440
4513
|
constraintMiddle;
|
|
@@ -4493,6 +4566,7 @@ var ClothSim = class {
|
|
|
4493
4566
|
}
|
|
4494
4567
|
}
|
|
4495
4568
|
this.prev.set(this.positions);
|
|
4569
|
+
this.laidOut = this.positions.slice();
|
|
4496
4570
|
const idx = (r2, c2) => r2 * cols + c2;
|
|
4497
4571
|
const links = [];
|
|
4498
4572
|
const link = (a, b2, kind) => {
|
|
@@ -4520,6 +4594,7 @@ var ClothSim = class {
|
|
|
4520
4594
|
this.constraintStiffness = new Float64Array(m).fill(1);
|
|
4521
4595
|
this.broken = new Uint8Array(m);
|
|
4522
4596
|
this.restBend = new Float64Array(m);
|
|
4597
|
+
this.draught = new Float64Array(this.count * 3);
|
|
4523
4598
|
this.constraintMiddle = new Int32Array(m).fill(-1);
|
|
4524
4599
|
for (let k2 = 0; k2 < m; k2++) {
|
|
4525
4600
|
const a = links[k2 * 3];
|
|
@@ -4610,6 +4685,22 @@ var ClothSim = class {
|
|
|
4610
4685
|
this.asleep = false;
|
|
4611
4686
|
this.stillFrames = 0;
|
|
4612
4687
|
}
|
|
4688
|
+
/**
|
|
4689
|
+
* Back to the sheet as it was laid out: flat, still, every particle where
|
|
4690
|
+
* the constructor put it. Pins, a grab and every lever are left alone.
|
|
4691
|
+
*
|
|
4692
|
+
* For a history that has been rewound — a burn played back from an earlier
|
|
4693
|
+
* moment, say, which hands back paper that had burnt away and paper that
|
|
4694
|
+
* had FALLEN, lying on the floor with its springs to the sheet suddenly
|
|
4695
|
+
* whole again. Solved from there, the solve hauls it up through the sheet
|
|
4696
|
+
* in a tangle. Laid out afresh, it is simply the sheet it is again.
|
|
4697
|
+
*/
|
|
4698
|
+
restore() {
|
|
4699
|
+
this.positions.set(this.laidOut);
|
|
4700
|
+
this.prev.set(this.laidOut);
|
|
4701
|
+
this.accumulator = 0;
|
|
4702
|
+
this.wake();
|
|
4703
|
+
}
|
|
4613
4704
|
/**
|
|
4614
4705
|
* Take hold of the sheet at one particle.
|
|
4615
4706
|
*
|
|
@@ -4842,9 +4933,9 @@ var ClothSim = class {
|
|
|
4842
4933
|
this.prev[i3] = x2;
|
|
4843
4934
|
this.prev[i3 + 1] = y2;
|
|
4844
4935
|
this.prev[i3 + 2] = z31;
|
|
4845
|
-
p2[i3] = x2 + vx + ax * im * dt2;
|
|
4846
|
-
p2[i3 + 1] = y2 + vy + (ay * im - gravity * 3.2) * dt2;
|
|
4847
|
-
p2[i3 + 2] = z31 + vz + az * im * dt2;
|
|
4936
|
+
p2[i3] = x2 + vx + (ax * im + this.draught[i3]) * dt2;
|
|
4937
|
+
p2[i3 + 1] = y2 + vy + (ay * im - gravity * 3.2 + this.draught[i3 + 1]) * dt2;
|
|
4938
|
+
p2[i3 + 2] = z31 + vz + (az * im + this.draught[i3 + 2]) * dt2;
|
|
4848
4939
|
maxTravel = Math.max(maxTravel, vx * vx + vy * vy + vz * vz);
|
|
4849
4940
|
}
|
|
4850
4941
|
for (let k2 = 0; k2 < this.grabbed.length; k2++) {
|
|
@@ -4942,44 +5033,17 @@ var ClothSim = class {
|
|
|
4942
5033
|
}
|
|
4943
5034
|
};
|
|
4944
5035
|
|
|
4945
|
-
// src/surface/damageContract.ts
|
|
4946
|
-
var DAMAGE_LOOK_DEFAULTS = {
|
|
4947
|
-
// §5 asks for 0.3–1 mm. 1.8 was outside it, and outside the slider's range
|
|
4948
|
-
// it was tuned in — a control at its limit is a report that something
|
|
4949
|
-
// underneath is wrong, which in this case was a fire nothing could see.
|
|
4950
|
-
emberWidth: 0.9,
|
|
4951
|
-
emberIntensity: 1.45,
|
|
4952
|
-
emberCoverage: 0.6,
|
|
4953
|
-
emberFlicker: 1.65,
|
|
4954
|
-
emberGlow: 1.25,
|
|
4955
|
-
sparkle: 0.5,
|
|
4956
|
-
lipWidth: 1.2,
|
|
4957
|
-
// Was 1.5, its slider's ceiling, which made the ash lip brighter than the
|
|
4958
|
-
// paper it sits on. Ash is pale GREY; the reference's lip is dimmer than
|
|
4959
|
-
// the sheet, not a highlight drawn on it.
|
|
4960
|
-
lipBrightness: 0.85,
|
|
4961
|
-
// Was 1, also a ceiling. At full warmth the char is milk chocolate —
|
|
4962
|
-
// closer to cardboard than to charcoal (§5). Burnt paper keeps a little
|
|
4963
|
-
// warmth in the plates and reads near black in a frame with a fire in it.
|
|
4964
|
-
charWarmth: 0.3,
|
|
4965
|
-
// Was 1, also a ceiling. The cracks are drawn as thin polygon outlines, so
|
|
4966
|
-
// at full strength the char reads as a mosaic rather than as broken plates.
|
|
4967
|
-
charCracks: 0.55,
|
|
4968
|
-
scorchReach: 30,
|
|
4969
|
-
scorchDarkness: 1.17,
|
|
4970
|
-
fingers: 1.25,
|
|
4971
|
-
edgeWave: 8.5,
|
|
4972
|
-
edgeBite: 3.6
|
|
4973
|
-
};
|
|
4974
|
-
var DAMAGE_CHANNELS = { char: 0, saturation: 1, heat: 2, presence: 3 };
|
|
4975
|
-
|
|
4976
5036
|
// src/physics/damage.ts
|
|
4977
|
-
var { char: CHAR, saturation: SATURATION, presence: PRESENCE } = DAMAGE_CHANNELS;
|
|
5037
|
+
var { char: CHAR, saturation: SATURATION, heat: HEAT, presence: PRESENCE } = DAMAGE_CHANNELS;
|
|
4978
5038
|
var GONE_BELOW = 128;
|
|
4979
5039
|
var CHAR_SHRINK = 0.12;
|
|
5040
|
+
var ACROSS_SHRINK = 0.55;
|
|
5041
|
+
var UPDRAFT = 1.1;
|
|
5042
|
+
var UPDRAFT_OUT = 0.35;
|
|
4980
5043
|
var CHAR_CURL = 6;
|
|
4981
5044
|
var FRONT_GRADIENT = 16 * 16;
|
|
4982
5045
|
var WET_MASS = 2;
|
|
5046
|
+
var TEAR_STRETCH = 1.5;
|
|
4983
5047
|
var DamageCoupling = class {
|
|
4984
5048
|
sim;
|
|
4985
5049
|
source = null;
|
|
@@ -4989,12 +5053,31 @@ var DamageCoupling = class {
|
|
|
4989
5053
|
particleTexel = new Int32Array(0);
|
|
4990
5054
|
/** Gone, per particle, as of the last read. */
|
|
4991
5055
|
gone;
|
|
5056
|
+
/** Whether any particle has ever been gone since the last reset — nothing can tear before. */
|
|
5057
|
+
holed = false;
|
|
4992
5058
|
/** The gone particles with live paper beside them. See {@link follow}. */
|
|
4993
5059
|
followers = [];
|
|
4994
5060
|
/**
|
|
4995
|
-
*
|
|
5061
|
+
* Which piece of paper each particle belongs to, named by the lowest index
|
|
5062
|
+
* in it — or -1 in the middle of a hole.
|
|
5063
|
+
*
|
|
5064
|
+
* A piece is whatever the sim's unbroken springs still hold together, which
|
|
5065
|
+
* is exactly what moves as one: two pieces are two things the solve lets
|
|
5066
|
+
* part. A gone particle on a cut takes the piece it borders MOST — a cut a
|
|
5067
|
+
* single particle wide borders two, and a particle averaged between them
|
|
5068
|
+
* would be dragged across the gap as one piece fell, stretching both edges
|
|
5069
|
+
* into it.
|
|
5070
|
+
*/
|
|
5071
|
+
piece;
|
|
5072
|
+
/** Union-find over the particles, reused on every read. */
|
|
5073
|
+
parent;
|
|
5074
|
+
/**
|
|
5075
|
+
* Per spring, how squarely it lies ACROSS the burn front, 0..1 — and
|
|
4996
5076
|
* remembered once it has been.
|
|
4997
5077
|
*
|
|
5078
|
+
* It decides two things: how far the spring shrinks ({@link ACROSS_SHRINK})
|
|
5079
|
+
* and, for a bend spring, which way the paper rolls ({@link CHAR_CURL}).
|
|
5080
|
+
*
|
|
4998
5081
|
* A burnt edge rolls about an axis along the front, like a scroll; it does
|
|
4999
5082
|
* not dish into a bowl. Measured with the curl in every bend spring alike: a
|
|
5000
5083
|
* burnt band settled into a saddle, its corners forward and its middle BACK
|
|
@@ -5007,11 +5090,22 @@ var DamageCoupling = class {
|
|
|
5007
5090
|
* gone by, the char behind it is uniform and says nothing about direction,
|
|
5008
5091
|
* but the paper stays rolled the way it rolled.
|
|
5009
5092
|
*/
|
|
5010
|
-
|
|
5093
|
+
acrossFront;
|
|
5094
|
+
/** The mesh's index as it was built — what {@link tear} hides triangles from and mends them back to. */
|
|
5095
|
+
original = null;
|
|
5096
|
+
/** Per triangle of that index: torn, and hidden until the history is rewound. */
|
|
5097
|
+
torn = new Uint8Array(0);
|
|
5098
|
+
/** The triangles with a gone corner — the only ones that can tear. */
|
|
5099
|
+
rim = [];
|
|
5100
|
+
rimStale = true;
|
|
5101
|
+
/** Every torn triangle has to be put back: the history was rewound, or the source went. */
|
|
5102
|
+
mend = false;
|
|
5011
5103
|
constructor(sim) {
|
|
5012
5104
|
this.sim = sim;
|
|
5013
5105
|
this.gone = new Uint8Array(sim.count);
|
|
5014
|
-
this.
|
|
5106
|
+
this.piece = new Int32Array(sim.count);
|
|
5107
|
+
this.parent = new Int32Array(sim.count);
|
|
5108
|
+
this.acrossFront = new Float32Array(sim.constraintCount);
|
|
5015
5109
|
}
|
|
5016
5110
|
/**
|
|
5017
5111
|
* Bring the sim's levers up to date with `source`. Call before stepping.
|
|
@@ -5022,7 +5116,13 @@ var DamageCoupling = class {
|
|
|
5022
5116
|
update(source) {
|
|
5023
5117
|
const next = source ?? null;
|
|
5024
5118
|
if (next === this.source && (next === null || next.version === this.version)) return false;
|
|
5025
|
-
if (next !== this.source)
|
|
5119
|
+
if (next !== this.source) {
|
|
5120
|
+
this.acrossFront.fill(0);
|
|
5121
|
+
if (next && this.holed) {
|
|
5122
|
+
this.rewind();
|
|
5123
|
+
this.gone.fill(0);
|
|
5124
|
+
}
|
|
5125
|
+
}
|
|
5026
5126
|
this.source = next;
|
|
5027
5127
|
if (!next) {
|
|
5028
5128
|
this.reset();
|
|
@@ -5042,12 +5142,13 @@ var DamageCoupling = class {
|
|
|
5042
5142
|
* laid out flat around them — their positions plus its rest offset from
|
|
5043
5143
|
* each, averaged. The triangles that straddle the burnt edge then keep
|
|
5044
5144
|
* roughly their shape instead of stretching back to wherever the particle
|
|
5045
|
-
* was when it burnt. Only
|
|
5046
|
-
*
|
|
5145
|
+
* was when it burnt. Only the neighbours in its own {@link piece} count, so
|
|
5146
|
+
* on a cut between two pieces it leaves with one of them.
|
|
5047
5147
|
*/
|
|
5048
5148
|
follow() {
|
|
5049
5149
|
if (this.followers.length === 0) return;
|
|
5050
5150
|
const { gone, sim } = this;
|
|
5151
|
+
const piece = this.piece;
|
|
5051
5152
|
const p2 = sim.positions;
|
|
5052
5153
|
const { cols, rows } = sim;
|
|
5053
5154
|
const cellX = cols > 1 ? sim.width / (cols - 1) : 0;
|
|
@@ -5055,6 +5156,7 @@ var DamageCoupling = class {
|
|
|
5055
5156
|
for (const g of this.followers) {
|
|
5056
5157
|
const r2 = g / cols | 0;
|
|
5057
5158
|
const c2 = g % cols;
|
|
5159
|
+
const own = piece[g];
|
|
5058
5160
|
let sx = 0;
|
|
5059
5161
|
let sy = 0;
|
|
5060
5162
|
let sz = 0;
|
|
@@ -5066,7 +5168,7 @@ var DamageCoupling = class {
|
|
|
5066
5168
|
const nc = c2 + dc;
|
|
5067
5169
|
if (dr === 0 && dc === 0 || nc < 0 || nc >= cols) continue;
|
|
5068
5170
|
const j3 = nr * cols + nc;
|
|
5069
|
-
if (gone[j3]) continue;
|
|
5171
|
+
if (gone[j3] || piece[j3] !== own) continue;
|
|
5070
5172
|
const j32 = j3 * 3;
|
|
5071
5173
|
sx += p2[j32] - dc * cellX;
|
|
5072
5174
|
sy += p2[j32 + 1] + dr * cellY;
|
|
@@ -5081,6 +5183,70 @@ var DamageCoupling = class {
|
|
|
5081
5183
|
p2[g3 + 2] = sz / n;
|
|
5082
5184
|
}
|
|
5083
5185
|
}
|
|
5186
|
+
/**
|
|
5187
|
+
* Hide the triangles a piece has torn away from. Call after {@link follow},
|
|
5188
|
+
* with the sheet's index buffer: it is rewritten in place, and the return
|
|
5189
|
+
* says whether it changed, so the caller knows to upload it.
|
|
5190
|
+
*
|
|
5191
|
+
* A triangle tears when one of its edges has stretched past
|
|
5192
|
+
* {@link TEAR_STRETCH} — which, among triangles with a burnt-away corner,
|
|
5193
|
+
* only ever happens across a gap that is opening. It is then collapsed to a
|
|
5194
|
+
* point, which draws nothing, casts no shadow and adds nothing to its
|
|
5195
|
+
* corners' normals, and it stays hidden: torn paper does not mend because
|
|
5196
|
+
* the two sides swing close again. Only a rewound history puts it back.
|
|
5197
|
+
*
|
|
5198
|
+
* A sheet in one piece, held, never stretches one that far — so a burn that
|
|
5199
|
+
* cuts nothing loose leaves this index exactly as it was built.
|
|
5200
|
+
*/
|
|
5201
|
+
tear(index) {
|
|
5202
|
+
if (!this.holed && !this.mend) return false;
|
|
5203
|
+
if (!this.original || this.original.length !== index.length) {
|
|
5204
|
+
this.original = index.slice();
|
|
5205
|
+
this.torn = new Uint8Array(index.length / 3);
|
|
5206
|
+
this.rimStale = true;
|
|
5207
|
+
}
|
|
5208
|
+
const { original, torn, gone, sim } = this;
|
|
5209
|
+
let changed = false;
|
|
5210
|
+
if (this.mend) {
|
|
5211
|
+
this.mend = false;
|
|
5212
|
+
index.set(original);
|
|
5213
|
+
changed = true;
|
|
5214
|
+
}
|
|
5215
|
+
if (this.rimStale) {
|
|
5216
|
+
this.rimStale = false;
|
|
5217
|
+
const rim = [];
|
|
5218
|
+
for (let t = 0; t < torn.length; t++) {
|
|
5219
|
+
if (gone[original[t * 3]] || gone[original[t * 3 + 1]] || gone[original[t * 3 + 2]]) rim.push(t);
|
|
5220
|
+
}
|
|
5221
|
+
this.rim = rim;
|
|
5222
|
+
}
|
|
5223
|
+
const p2 = sim.positions;
|
|
5224
|
+
const { cols, rows } = sim;
|
|
5225
|
+
const cellX = cols > 1 ? sim.width / (cols - 1) : 0;
|
|
5226
|
+
const cellY = rows > 1 ? sim.height / (rows - 1) : 0;
|
|
5227
|
+
const limit = TEAR_STRETCH * TEAR_STRETCH;
|
|
5228
|
+
const stretched = (i, j3) => {
|
|
5229
|
+
const dc = i % cols - j3 % cols;
|
|
5230
|
+
const dr = (i / cols | 0) - (j3 / cols | 0);
|
|
5231
|
+
const rest = (dc * cellX) ** 2 + (dr * cellY) ** 2;
|
|
5232
|
+
const i3 = i * 3;
|
|
5233
|
+
const j32 = j3 * 3;
|
|
5234
|
+
const now = (p2[i3] - p2[j32]) ** 2 + (p2[i3 + 1] - p2[j32 + 1]) ** 2 + (p2[i3 + 2] - p2[j32 + 2]) ** 2;
|
|
5235
|
+
return now > rest * limit;
|
|
5236
|
+
};
|
|
5237
|
+
for (const t of this.rim) {
|
|
5238
|
+
if (torn[t]) continue;
|
|
5239
|
+
const a = original[t * 3];
|
|
5240
|
+
const b2 = original[t * 3 + 1];
|
|
5241
|
+
const c2 = original[t * 3 + 2];
|
|
5242
|
+
if (!stretched(a, b2) && !stretched(b2, c2) && !stretched(c2, a)) continue;
|
|
5243
|
+
torn[t] = 1;
|
|
5244
|
+
index[t * 3 + 1] = a;
|
|
5245
|
+
index[t * 3 + 2] = a;
|
|
5246
|
+
changed = true;
|
|
5247
|
+
}
|
|
5248
|
+
return changed;
|
|
5249
|
+
}
|
|
5084
5250
|
/** Map each particle to its texel. The damage grid's row 0 is v = 0, the sheet's BOTTOM. */
|
|
5085
5251
|
layout(size) {
|
|
5086
5252
|
const { cols, rows, count } = this.sim;
|
|
@@ -5096,8 +5262,10 @@ var DamageCoupling = class {
|
|
|
5096
5262
|
}
|
|
5097
5263
|
}
|
|
5098
5264
|
read(pixels) {
|
|
5099
|
-
const { sim, gone, particleTexel,
|
|
5265
|
+
const { sim, gone, particleTexel, acrossFront, size } = this;
|
|
5100
5266
|
const {
|
|
5267
|
+
draught,
|
|
5268
|
+
cols,
|
|
5101
5269
|
invMass,
|
|
5102
5270
|
restLength,
|
|
5103
5271
|
naturalLength,
|
|
@@ -5108,12 +5276,25 @@ var DamageCoupling = class {
|
|
|
5108
5276
|
constraintKind,
|
|
5109
5277
|
constraintMiddle
|
|
5110
5278
|
} = sim;
|
|
5279
|
+
let rewound = false;
|
|
5280
|
+
let holed = false;
|
|
5111
5281
|
for (let i = 0; i < sim.count; i++) {
|
|
5112
5282
|
const t = particleTexel[i] * 4;
|
|
5113
5283
|
const isGone = pixels[t + PRESENCE] < GONE_BELOW;
|
|
5284
|
+
if (gone[i] && !isGone) rewound = true;
|
|
5285
|
+
if (isGone) holed = true;
|
|
5114
5286
|
gone[i] = isGone ? 1 : 0;
|
|
5115
5287
|
invMass[i] = isGone ? 0 : 1 / (1 + WET_MASS * pixels[t + SATURATION] / 255);
|
|
5288
|
+
const heat = isGone ? 0 : pixels[t + HEAT] / 255;
|
|
5289
|
+
const lift = heat > 0.15 ? (heat - 0.15) / 0.85 : 0;
|
|
5290
|
+
const i3 = i * 3;
|
|
5291
|
+
draught[i3] = 0;
|
|
5292
|
+
draught[i3 + 1] = UPDRAFT * lift;
|
|
5293
|
+
draught[i3 + 2] = UPDRAFT * UPDRAFT_OUT * lift;
|
|
5116
5294
|
}
|
|
5295
|
+
if (rewound) this.rewind();
|
|
5296
|
+
this.holed = holed;
|
|
5297
|
+
this.rimStale = true;
|
|
5117
5298
|
for (let k2 = 0; k2 < sim.constraintCount; k2++) {
|
|
5118
5299
|
const a = constraintA[k2];
|
|
5119
5300
|
const b2 = constraintB[k2];
|
|
@@ -5130,54 +5311,125 @@ var DamageCoupling = class {
|
|
|
5130
5311
|
samples = 3;
|
|
5131
5312
|
}
|
|
5132
5313
|
const char = charSum / (samples * 255);
|
|
5133
|
-
|
|
5314
|
+
const t = particleTexel[m >= 0 ? m : a];
|
|
5315
|
+
const x2 = t % size;
|
|
5316
|
+
const y2 = t / size | 0;
|
|
5317
|
+
const gx = pixels[(y2 * size + Math.min(size - 1, x2 + 2)) * 4 + CHAR] - pixels[(y2 * size + Math.max(0, x2 - 2)) * 4 + CHAR];
|
|
5318
|
+
const gy = pixels[(Math.min(size - 1, y2 + 2) * size + x2) * 4 + CHAR] - pixels[(Math.max(0, y2 - 2) * size + x2) * 4 + CHAR];
|
|
5319
|
+
const steep = gx * gx + gy * gy;
|
|
5320
|
+
if (steep > FRONT_GRADIENT) {
|
|
5321
|
+
const dc = b2 % cols - a % cols;
|
|
5322
|
+
const dr = (b2 / cols | 0) - (a / cols | 0);
|
|
5323
|
+
const along = dc * gx - dr * gy;
|
|
5324
|
+
const span = dc * dc + dr * dr || 1;
|
|
5325
|
+
const across2 = along * along / (span * steep);
|
|
5326
|
+
if (across2 > acrossFront[k2]) acrossFront[k2] = across2;
|
|
5327
|
+
}
|
|
5328
|
+
const across = acrossFront[k2];
|
|
5329
|
+
restLength[k2] = naturalLength[k2] * (1 - CHAR_SHRINK * char * (1 - (1 - ACROSS_SHRINK) * across));
|
|
5134
5330
|
if (constraintKind[k2] === 2) {
|
|
5135
|
-
const t = particleTexel[m];
|
|
5136
|
-
const x2 = t % size;
|
|
5137
|
-
const y2 = t / size | 0;
|
|
5138
|
-
const gx = pixels[(y2 * size + Math.min(size - 1, x2 + 2)) * 4 + CHAR] - pixels[(y2 * size + Math.max(0, x2 - 2)) * 4 + CHAR];
|
|
5139
|
-
const gy = pixels[(Math.min(size - 1, y2 + 2) * size + x2) * 4 + CHAR] - pixels[(Math.max(0, y2 - 2) * size + x2) * 4 + CHAR];
|
|
5140
|
-
const steep = gx * gx + gy * gy;
|
|
5141
|
-
if (steep > FRONT_GRADIENT) {
|
|
5142
|
-
const across = (b2 - a === 2 ? gx * gx : gy * gy) / steep;
|
|
5143
|
-
if (across > curlWeight[k2]) curlWeight[k2] = across;
|
|
5144
|
-
}
|
|
5145
5331
|
const span = naturalLength[k2];
|
|
5146
|
-
restBend[k2] = CHAR_CURL * char *
|
|
5332
|
+
restBend[k2] = CHAR_CURL * char * across * span * span / 8;
|
|
5333
|
+
}
|
|
5334
|
+
}
|
|
5335
|
+
this.findPieces();
|
|
5336
|
+
}
|
|
5337
|
+
/**
|
|
5338
|
+
* Which piece each particle is in ({@link piece}), and which gone ones
|
|
5339
|
+
* follow a piece at all ({@link followers}).
|
|
5340
|
+
*
|
|
5341
|
+
* Pieces by union-find over the springs that still hold, rooted at the
|
|
5342
|
+
* lowest index so the same burn names the same pieces every time. Then each
|
|
5343
|
+
* gone particle with live paper beside it joins the piece it has the most
|
|
5344
|
+
* neighbours in, the lower-named on a tie.
|
|
5345
|
+
*/
|
|
5346
|
+
findPieces() {
|
|
5347
|
+
const { sim, gone } = this;
|
|
5348
|
+
const parent = this.parent;
|
|
5349
|
+
const piece = this.piece;
|
|
5350
|
+
const { broken, constraintA, constraintB } = sim;
|
|
5351
|
+
for (let i = 0; i < sim.count; i++) parent[i] = i;
|
|
5352
|
+
const find = (i) => {
|
|
5353
|
+
let r2 = i;
|
|
5354
|
+
while (parent[r2] !== r2) r2 = parent[r2];
|
|
5355
|
+
while (parent[i] !== r2) {
|
|
5356
|
+
const up = parent[i];
|
|
5357
|
+
parent[i] = r2;
|
|
5358
|
+
i = up;
|
|
5147
5359
|
}
|
|
5360
|
+
return r2;
|
|
5361
|
+
};
|
|
5362
|
+
for (let k2 = 0; k2 < sim.constraintCount; k2++) {
|
|
5363
|
+
if (broken[k2]) continue;
|
|
5364
|
+
const ra = find(constraintA[k2]);
|
|
5365
|
+
const rb = find(constraintB[k2]);
|
|
5366
|
+
if (ra < rb) parent[rb] = ra;
|
|
5367
|
+
else if (rb < ra) parent[ra] = rb;
|
|
5148
5368
|
}
|
|
5369
|
+
for (let i = 0; i < sim.count; i++) piece[i] = gone[i] ? -1 : find(i);
|
|
5149
5370
|
const followers = [];
|
|
5150
5371
|
const { cols, rows } = sim;
|
|
5372
|
+
const seen = [];
|
|
5373
|
+
const votes = [];
|
|
5151
5374
|
for (let i = 0; i < sim.count; i++) {
|
|
5152
5375
|
if (!gone[i]) continue;
|
|
5153
5376
|
const r2 = i / cols | 0;
|
|
5154
5377
|
const c2 = i % cols;
|
|
5155
|
-
|
|
5156
|
-
|
|
5378
|
+
seen.length = 0;
|
|
5379
|
+
votes.length = 0;
|
|
5380
|
+
for (let dr = -1; dr <= 1; dr++) {
|
|
5157
5381
|
const nr = r2 + dr;
|
|
5158
5382
|
if (nr < 0 || nr >= rows) continue;
|
|
5159
5383
|
for (let dc = -1; dc <= 1; dc++) {
|
|
5160
5384
|
const nc = c2 + dc;
|
|
5161
5385
|
if (nc < 0 || nc >= cols) continue;
|
|
5162
|
-
|
|
5163
|
-
|
|
5164
|
-
|
|
5386
|
+
const j3 = nr * cols + nc;
|
|
5387
|
+
if (gone[j3]) continue;
|
|
5388
|
+
const at = seen.indexOf(piece[j3]);
|
|
5389
|
+
if (at < 0) {
|
|
5390
|
+
seen.push(piece[j3]);
|
|
5391
|
+
votes.push(1);
|
|
5392
|
+
} else {
|
|
5393
|
+
votes[at]++;
|
|
5165
5394
|
}
|
|
5166
5395
|
}
|
|
5167
5396
|
}
|
|
5168
|
-
if (
|
|
5397
|
+
if (seen.length === 0) continue;
|
|
5398
|
+
let best = 0;
|
|
5399
|
+
for (let k2 = 1; k2 < seen.length; k2++) {
|
|
5400
|
+
if (votes[k2] > votes[best] || votes[k2] === votes[best] && seen[k2] < seen[best]) best = k2;
|
|
5401
|
+
}
|
|
5402
|
+
piece[i] = seen[best];
|
|
5403
|
+
followers.push(i);
|
|
5169
5404
|
}
|
|
5170
5405
|
this.followers = followers;
|
|
5171
5406
|
}
|
|
5407
|
+
/**
|
|
5408
|
+
* A rewound history: the sheet starts over.
|
|
5409
|
+
*
|
|
5410
|
+
* Paper that had fallen away is paper again, with every spring back to the
|
|
5411
|
+
* sheet it was cut from — solved from where it lies, the solve would haul it
|
|
5412
|
+
* up off the floor through the sheet in a tangle. And the curl it rolled
|
|
5413
|
+
* into belonged to a front that has not passed yet.
|
|
5414
|
+
*/
|
|
5415
|
+
rewind() {
|
|
5416
|
+
this.sim.restore();
|
|
5417
|
+
this.acrossFront.fill(0);
|
|
5418
|
+
this.torn.fill(0);
|
|
5419
|
+
this.mend = true;
|
|
5420
|
+
}
|
|
5172
5421
|
reset() {
|
|
5173
5422
|
const { sim } = this;
|
|
5423
|
+
if (this.holed) this.rewind();
|
|
5424
|
+
sim.draught.fill(0);
|
|
5174
5425
|
sim.invMass.fill(1);
|
|
5175
5426
|
sim.restBend.fill(0);
|
|
5176
5427
|
sim.broken.fill(0);
|
|
5177
5428
|
sim.restLength.set(sim.naturalLength);
|
|
5178
5429
|
this.gone.fill(0);
|
|
5179
|
-
this.
|
|
5430
|
+
this.acrossFront.fill(0);
|
|
5180
5431
|
this.followers = [];
|
|
5432
|
+
this.holed = false;
|
|
5181
5433
|
this.version = -1;
|
|
5182
5434
|
sim.wake();
|
|
5183
5435
|
}
|
|
@@ -6545,6 +6797,82 @@ function p({
|
|
|
6545
6797
|
}
|
|
6546
6798
|
var j2 = r.forwardRef(p);
|
|
6547
6799
|
|
|
6800
|
+
// src/surface/cutDistance.ts
|
|
6801
|
+
var CUT_REACH_MM = 16;
|
|
6802
|
+
var SPECK_TEXELS = 3;
|
|
6803
|
+
var GONE_BELOW2 = 128;
|
|
6804
|
+
var scratch = null;
|
|
6805
|
+
function cutDistance(pixels, size, texelMm, out) {
|
|
6806
|
+
const n = size * size;
|
|
6807
|
+
if (!scratch || scratch.size !== size) {
|
|
6808
|
+
scratch = { size, dist: new Float32Array(n), kept: new Uint8Array(n), stack: new Int32Array(n) };
|
|
6809
|
+
}
|
|
6810
|
+
const { dist, kept, stack } = scratch;
|
|
6811
|
+
const gone = (i) => pixels[i * 4 + 3] < GONE_BELOW2;
|
|
6812
|
+
kept.fill(0);
|
|
6813
|
+
const seen = dist;
|
|
6814
|
+
seen.fill(0);
|
|
6815
|
+
for (let start = 0; start < n; start++) {
|
|
6816
|
+
if (seen[start] || !gone(start)) continue;
|
|
6817
|
+
let top = 0;
|
|
6818
|
+
let count = 0;
|
|
6819
|
+
stack[top++] = start;
|
|
6820
|
+
seen[start] = 1;
|
|
6821
|
+
const first = top;
|
|
6822
|
+
let head = 0;
|
|
6823
|
+
while (head < top) {
|
|
6824
|
+
const i = stack[head++];
|
|
6825
|
+
count++;
|
|
6826
|
+
const x2 = i % size;
|
|
6827
|
+
const y2 = i / size | 0;
|
|
6828
|
+
const next = [
|
|
6829
|
+
x2 > 0 ? i - 1 : -1,
|
|
6830
|
+
x2 < size - 1 ? i + 1 : -1,
|
|
6831
|
+
y2 > 0 ? i - size : -1,
|
|
6832
|
+
y2 < size - 1 ? i + size : -1
|
|
6833
|
+
];
|
|
6834
|
+
for (const j3 of next) {
|
|
6835
|
+
if (j3 < 0 || seen[j3] || !gone(j3)) continue;
|
|
6836
|
+
seen[j3] = 1;
|
|
6837
|
+
stack[top++] = j3;
|
|
6838
|
+
}
|
|
6839
|
+
}
|
|
6840
|
+
if (count > SPECK_TEXELS) for (let k2 = first - 1; k2 < top; k2++) kept[stack[k2]] = 1;
|
|
6841
|
+
}
|
|
6842
|
+
const [dx, dy] = texelMm;
|
|
6843
|
+
const dd = Math.hypot(dx, dy);
|
|
6844
|
+
for (let i = 0; i < n; i++) dist[i] = kept[i] ? 0 : Number.POSITIVE_INFINITY;
|
|
6845
|
+
for (let y2 = 0; y2 < size; y2++) {
|
|
6846
|
+
for (let x2 = 0; x2 < size; x2++) {
|
|
6847
|
+
const i = y2 * size + x2;
|
|
6848
|
+
let v2 = dist[i];
|
|
6849
|
+
if (v2 === 0) continue;
|
|
6850
|
+
if (x2 > 0) v2 = Math.min(v2, dist[i - 1] + dx);
|
|
6851
|
+
if (y2 > 0) {
|
|
6852
|
+
v2 = Math.min(v2, dist[i - size] + dy);
|
|
6853
|
+
if (x2 > 0) v2 = Math.min(v2, dist[i - size - 1] + dd);
|
|
6854
|
+
if (x2 < size - 1) v2 = Math.min(v2, dist[i - size + 1] + dd);
|
|
6855
|
+
}
|
|
6856
|
+
dist[i] = v2;
|
|
6857
|
+
}
|
|
6858
|
+
}
|
|
6859
|
+
for (let y2 = size - 1; y2 >= 0; y2--) {
|
|
6860
|
+
for (let x2 = size - 1; x2 >= 0; x2--) {
|
|
6861
|
+
const i = y2 * size + x2;
|
|
6862
|
+
let v2 = dist[i];
|
|
6863
|
+
if (v2 === 0) continue;
|
|
6864
|
+
if (x2 < size - 1) v2 = Math.min(v2, dist[i + 1] + dx);
|
|
6865
|
+
if (y2 < size - 1) {
|
|
6866
|
+
v2 = Math.min(v2, dist[i + size] + dy);
|
|
6867
|
+
if (x2 < size - 1) v2 = Math.min(v2, dist[i + size + 1] + dd);
|
|
6868
|
+
if (x2 > 0) v2 = Math.min(v2, dist[i + size - 1] + dd);
|
|
6869
|
+
}
|
|
6870
|
+
dist[i] = v2;
|
|
6871
|
+
}
|
|
6872
|
+
}
|
|
6873
|
+
for (let i = 0; i < n; i++) out[i] = Math.round(Math.min(dist[i], CUT_REACH_MM) / CUT_REACH_MM * 255);
|
|
6874
|
+
}
|
|
6875
|
+
|
|
6548
6876
|
// src/surface/compose.ts
|
|
6549
6877
|
var THREE9 = __toESM(require("three"), 1);
|
|
6550
6878
|
|
|
@@ -6960,7 +7288,7 @@ var DAMAGE_CHUNK = (
|
|
|
6960
7288
|
`
|
|
6961
7289
|
uniform sampler2D uDamage;
|
|
6962
7290
|
uniform float uDamageDetail;
|
|
6963
|
-
// (edgeWave mm, edgeBite mm, sparkle,
|
|
7291
|
+
// (edgeWave mm, edgeBite mm, sparkle, charWidth mm) \u2014 see DamageLook.
|
|
6964
7292
|
uniform vec4 uLook3;
|
|
6965
7293
|
|
|
6966
7294
|
// The damage grid's texels sit ON the sheet \u2014 texel x at u = x / (N - 1), its
|
|
@@ -7097,6 +7425,8 @@ var DAMAGE_SHADE_CHUNK = (
|
|
|
7097
7425
|
/* glsl */
|
|
7098
7426
|
`
|
|
7099
7427
|
uniform float uDamageTime;
|
|
7428
|
+
// How far each texel is from the cut, as a fraction of PL_CUT_REACH \u2014 see cutDistance.
|
|
7429
|
+
uniform sampler2D uDamageEdge;
|
|
7100
7430
|
// The look, packed \u2014 see DamageLook for what each number means.
|
|
7101
7431
|
uniform vec4 uLook0; // emberWidth mm, emberIntensity, emberCoverage, emberFlicker
|
|
7102
7432
|
uniform vec4 uLook1; // emberGlow, lipWidth mm, lipBrightness, charWarmth
|
|
@@ -7106,6 +7436,10 @@ vec3 plDamageLight;
|
|
|
7106
7436
|
|
|
7107
7437
|
// World units per millimetre: a default sheet is one unit across, and A4 is 210 mm.
|
|
7108
7438
|
const float PL_MM = 1.0 / 210.0;
|
|
7439
|
+
// How far out a burnt edge's zones can be measured from the cut, mm: past
|
|
7440
|
+
// the widest lip, ember line, char and scorch the look allows by default.
|
|
7441
|
+
// The same number cutDistance encodes with, so the two cannot drift.
|
|
7442
|
+
const float PL_CUT_REACH = ${CUT_REACH_MM.toFixed(1)};
|
|
7109
7443
|
|
|
7110
7444
|
vec3 plLinear(vec3 srgb) {
|
|
7111
7445
|
return pow(srgb, vec3(2.2));
|
|
@@ -7162,7 +7496,12 @@ vec3 plScorchTint(float t) {
|
|
|
7162
7496
|
vec3 c3 = plLinear(vec3(0.541, 0.310, 0.141)); // #8A4F24
|
|
7163
7497
|
vec3 c4 = plLinear(vec3(0.471, 0.275, 0.149)); // #784626
|
|
7164
7498
|
vec3 c5 = plLinear(vec3(0.380, 0.216, 0.133)); // #613722
|
|
7165
|
-
|
|
7499
|
+
// The dark end hands over to the char's black: a nearly neutral umber, not
|
|
7500
|
+
// the saturated brown (#4F3323) it used to stop at. Beside black char that
|
|
7501
|
+
// brown showed through wherever the band thinned, and turned the black
|
|
7502
|
+
// brown \u2014 measured, the cold char's saturation went from 0.43 to 0.11 with
|
|
7503
|
+
// the scorch's darkening switched off.
|
|
7504
|
+
vec3 c6 = plLinear(vec3(0.169, 0.133, 0.110)); // #2B221C
|
|
7166
7505
|
float s = clamp(t, 0.0, 1.0) * 6.0;
|
|
7167
7506
|
// Each stop is blended with a SMOOTH step, not a linear one.
|
|
7168
7507
|
//
|
|
@@ -7213,32 +7552,6 @@ void plDamage(inout vec4 color, inout float roughness) {
|
|
|
7213
7552
|
vec2 perWorld = (vec2(textureSize(uDamage, 0)) - 1.0) / uSheetSize;
|
|
7214
7553
|
vec2 gradP = vec2(e.a - w.a, n.a - s.a) * 0.5 * perWorld;
|
|
7215
7554
|
vec2 gradC = vec2(e.r - w.r, n.r - s.r) * 0.5 * perWorld;
|
|
7216
|
-
// Millimetres from the cut, on the paper side \u2014 measured against the field
|
|
7217
|
-
// the cut is DRAWN from (softened, frayed, warped), through its own screen
|
|
7218
|
-
// derivatives. It used to divide by the slope of the raw texels, and since
|
|
7219
|
-
// paper burns through within a texel that slope is a cliff while the drawn
|
|
7220
|
-
// edge is spread over millimetres: a point well into the char computed as a
|
|
7221
|
-
// hair from the cut, and the ember line drew there \u2014 found by the gate.
|
|
7222
|
-
// The slope, though, is the SMOOTH presence's. The fray's fibre noise
|
|
7223
|
-
// changes pixel to pixel, and its slope is a cliff everywhere it touches:
|
|
7224
|
-
// divided by that, a point deep in the char came out a hair from the cut
|
|
7225
|
-
// and the ember glow drew there \u2014 the gate's stray pixels. The fray still
|
|
7226
|
-
// moves where the edge is; it just cannot fake how steep it is.
|
|
7227
|
-
float smoothA = plDamageSmooth;
|
|
7228
|
-
vec2 cutSlope = vec2(dFdx(smoothA), dFdy(smoothA));
|
|
7229
|
-
float pxFromCut = (d.a - 0.5) / max(length(cutSlope), 1e-4);
|
|
7230
|
-
// Millimetres per pixel ALONG the way to the cut, not across the screen's
|
|
7231
|
-
// x: a sheet leaning away is foreshortened vertically, so above and below a
|
|
7232
|
-
// hole a pixel covers more paper than beside it, and a scale taken from x
|
|
7233
|
-
// alone drew the ember zone too many pixels deep on the top and bottom rims.
|
|
7234
|
-
vec2 toCutPx = cutSlope / max(length(cutSlope), 1e-6);
|
|
7235
|
-
float mmPerPx = length(dFdx(-vViewPosition) * toCutPx.x + dFdy(-vViewPosition) * toCutPx.y) / PL_MM;
|
|
7236
|
-
float mm = pxFromCut * mmPerPx;
|
|
7237
|
-
// About a pixel of antialiasing, never more: where the warp makes the
|
|
7238
|
-
// estimate jump between neighbours, fwidth(mm) is millimetres wide, and the
|
|
7239
|
-
// ember's soft tail \u2014 faint, but multiplied by its light \u2014 drew stray glow
|
|
7240
|
-
// well out into the char.
|
|
7241
|
-
float aa = min(fwidth(mm), 1.5 * mmPerPx);
|
|
7242
7555
|
// Scorch reaches ahead of the char, and further UP than anywhere else:
|
|
7243
7556
|
// the char a centimetre below this point, and the char a few millimetres
|
|
7244
7557
|
// around it, each lent to the scorch at a discount (\xA74.2, \xA74.5).
|
|
@@ -7252,38 +7565,61 @@ void plDamage(inout vec4 color, inout float roughness) {
|
|
|
7252
7565
|
charWide += texture2D(uDamage, uv + vec2(cos(a), sin(a)) * 7.0 * mmUv).r;
|
|
7253
7566
|
}
|
|
7254
7567
|
charWide /= 8.0;
|
|
7255
|
-
//
|
|
7256
|
-
//
|
|
7257
|
-
//
|
|
7258
|
-
//
|
|
7259
|
-
//
|
|
7260
|
-
//
|
|
7261
|
-
|
|
7262
|
-
//
|
|
7263
|
-
//
|
|
7264
|
-
//
|
|
7265
|
-
//
|
|
7266
|
-
//
|
|
7267
|
-
//
|
|
7268
|
-
//
|
|
7269
|
-
//
|
|
7270
|
-
vec2
|
|
7271
|
-
|
|
7272
|
-
float
|
|
7273
|
-
|
|
7274
|
-
|
|
7275
|
-
//
|
|
7276
|
-
float
|
|
7277
|
-
|
|
7568
|
+
// \u2500\u2500 The cut, measured \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
7569
|
+
// How far this point is from the cut, in millimetres, out to PL_CUT_REACH \u2014
|
|
7570
|
+
// the one measure every zone of a burnt edge is laid out on: the ash lip,
|
|
7571
|
+
// the ember line, the char and the scorch (Ember_line annotated). Worked
|
|
7572
|
+
// out on the CPU once per change of the field (cutDistance), where a speck
|
|
7573
|
+
// of burnt-through paper can be told from a cut: measured here per pixel,
|
|
7574
|
+
// every speck near the rim grew its own lip, char and scorch, and the bands
|
|
7575
|
+
// came apart into blotches.
|
|
7576
|
+
//
|
|
7577
|
+
// Burnt AWAY, not charred: the black and the lip belong to paper beside a
|
|
7578
|
+
// cut, so a burn toasts before it blackens.
|
|
7579
|
+
//
|
|
7580
|
+
// Read through the same warp as the cut, so the zones follow the edge that
|
|
7581
|
+
// is drawn. Bilinear between a gone texel and the paper beside it, the
|
|
7582
|
+
// drawn edge reads half a texel, which comes back off.
|
|
7583
|
+
vec2 edgeTexel = 1.0 / vec2(textureSize(uDamageEdge, 0));
|
|
7584
|
+
vec2 edgeWorld = uSheetSize / (vec2(textureSize(uDamageEdge, 0)) - 1.0);
|
|
7585
|
+
float edgeRaw = texture2D(uDamageEdge, uv).r;
|
|
7586
|
+
float edgeHalf = 0.5 * min(edgeWorld.x, edgeWorld.y) / PL_MM;
|
|
7587
|
+
float xCut = max(edgeRaw * PL_CUT_REACH - edgeHalf, 0.0);
|
|
7588
|
+
// A pixel or so of antialiasing, measured on the same measure.
|
|
7589
|
+
float aa = clamp(fwidth(xCut), 0.05, 1.0);
|
|
7590
|
+
// Which way the cut lies, from how the distance falls \u2014 and along the edge,
|
|
7591
|
+
// across that. In the sheet's own space, so noise laid along it follows the
|
|
7592
|
+
// rim wherever the rim goes.
|
|
7593
|
+
vec2 toward = -vec2(
|
|
7594
|
+
(texture2D(uDamageEdge, uv + vec2(edgeTexel.x, 0.0)).r - texture2D(uDamageEdge, uv - vec2(edgeTexel.x, 0.0)).r) / edgeWorld.x,
|
|
7595
|
+
(texture2D(uDamageEdge, uv + vec2(0.0, edgeTexel.y)).r - texture2D(uDamageEdge, uv - vec2(0.0, edgeTexel.y)).r) / edgeWorld.y
|
|
7596
|
+
);
|
|
7597
|
+
vec2 tangentR = length(toward) > 1e-4 ? normalize(vec2(-toward.y, toward.x)) : vec2(1.0, 0.0);
|
|
7598
|
+
// Paper with a cut within reach, eased out toward the reach.
|
|
7599
|
+
float cutZone = 1.0 - smoothstep(PL_CUT_REACH - 2.0, PL_CUT_REACH - 0.5, xCut);
|
|
7278
7600
|
|
|
7279
7601
|
// An untouched texel is left exactly as it was \u2014 the identity the whole
|
|
7280
7602
|
// seam is built on. Nothing here has happened to this paper.
|
|
7281
|
-
if (d.r <= 0.0 && d.g <= 0.0 && d.a >= 1.0 && charBelow <= 0.0 && charWide <= 0.0 && e.a >= 1.0 && w.a >= 1.0 && n.a >= 1.0 && s.a >= 1.0) {
|
|
7603
|
+
if (d.r <= 0.0 && d.g <= 0.0 && d.a >= 1.0 && charBelow <= 0.0 && charWide <= 0.0 && edgeRaw >= 1.0 && e.a >= 1.0 && w.a >= 1.0 && n.a >= 1.0 && s.a >= 1.0) {
|
|
7282
7604
|
return;
|
|
7283
7605
|
}
|
|
7284
7606
|
|
|
7285
7607
|
vec2 p = plLocal();
|
|
7286
7608
|
|
|
7609
|
+
// The zones, out from the cut: the ash lip, the ember line on its outer
|
|
7610
|
+
// edge, the char, then the scorch \u2014 each as wide as the look says, and
|
|
7611
|
+
// uneven along the rim so none of them is a ribbon. The lip's unevenness
|
|
7612
|
+
// stays inside the zone the ember gate allows it (lip plus bead).
|
|
7613
|
+
float lipEdge = uLook1.y * mix(0.7, 1.1, plFbm(vec2(dot(p, tangentR) / (5.0 * PL_MM), 1.7)));
|
|
7614
|
+
float charEnd = lipEdge + uLook0.x + uLook3.w * mix(0.8, 1.2, plFbm(vec2(dot(p, tangentR) / (7.0 * PL_MM), 4.3)));
|
|
7615
|
+
// Further above the burn than below it \u2014 hot gas rises and cooks the paper
|
|
7616
|
+
// over it (\xA74.5) \u2014 out to the scorch's full reach straight up, and about
|
|
7617
|
+
// half of it below and beside.
|
|
7618
|
+
vec2 upMm = normalize(upUv * uSheetSize);
|
|
7619
|
+
float above = length(toward) > 1e-3 ? max(0.0, dot(-normalize(toward), upMm)) : 0.0;
|
|
7620
|
+
float scorchEnd = charEnd + uLook2.y * mix(0.45, 1.0, above)
|
|
7621
|
+
+ (plFbm(vec2(dot(p, tangentR) / (4.0 * PL_MM), 3.1)) - 0.5) * uLook2.w * 2.5;
|
|
7622
|
+
|
|
7287
7623
|
// Wet paper is darker and smoother: water fills the gaps between fibres
|
|
7288
7624
|
// that scatter light, which is both effects from one cause.
|
|
7289
7625
|
color.rgb *= 1.0 - 0.38 * d.g;
|
|
@@ -7335,27 +7671,64 @@ void plDamage(inout vec4 color, inout float roughness) {
|
|
|
7335
7671
|
// being finer than a level it can only ever move a value into the
|
|
7336
7672
|
// neighbouring one.
|
|
7337
7673
|
c = clamp(c + (plNoise(p * 900.0) - 0.5) * (1.5 / 255.0), 0.0, 1.0);
|
|
7674
|
+
// The colour INSIDE the scorch reads a smoothed c: the same char and the
|
|
7675
|
+
// same reaches, without the fingers. The fingers belong to the front's
|
|
7676
|
+
// silhouette (lead, below); carried into the ramp as well, a strong fingers
|
|
7677
|
+
// setting swung the scorch between tan and near-black umber a few
|
|
7678
|
+
// millimetres apart \u2014 a leopard print, not a toast.
|
|
7679
|
+
float cSmooth = 1.0 - (1.0 - clamp(mix(d.r, soft, 0.65), 0.0, 1.0)) * (1.0 - clamp(0.62 * charBelow * 0.75, 0.0, 1.0)) * (1.0 - clamp(0.4 * charWide * 0.75, 0.0, 1.0));
|
|
7680
|
+
cSmooth = clamp(cSmooth + ((plNoise(p * 300.0) - 0.5) * 0.05 + (plNoise(p * 90.0) - 0.5) * 0.04) * smoothstep(0.0, 0.1, cSmooth), 0.0, 1.0);
|
|
7338
7681
|
// A steep leading edge \u2014 paper to straw in a sliver \u2014 then the long ramp.
|
|
7339
|
-
|
|
7340
|
-
|
|
7341
|
-
|
|
7682
|
+
// The front's teeth from the noisy c; filled in behind them from the smooth
|
|
7683
|
+
// one, so a dip in the noise cannot open a patch of clean paper inside it.
|
|
7684
|
+
float lead = max(smoothstep(0.02, 0.05, c), smoothstep(0.1, 0.2, cSmooth));
|
|
7685
|
+
// The browns are done by 0.45, where the char's umber hand-over takes them:
|
|
7686
|
+
// the ramp used to run on to 0.7, over the char, and most of what read as
|
|
7687
|
+
// "char" was this ramp's darkest brown.
|
|
7688
|
+
float ramp = smoothstep(0.05, 0.45, cSmooth);
|
|
7689
|
+
// That toast is for paper with no cut near it \u2014 the first stage of a burn,
|
|
7690
|
+
// before anything has gone through.
|
|
7691
|
+
color.rgb *= mix(vec3(1.0), pow(plScorchTint(ramp), vec3(uLook2.z)), lead * (1.0 - cutZone));
|
|
7692
|
+
// Round a cut, the scorch is a band of its own: dark, only a little lighter
|
|
7693
|
+
// than the char it leads from, a shade warmer toward its outer edge, then a
|
|
7694
|
+
// steep leading edge into clean paper (Ember_line annotated, zone 5: "steep
|
|
7695
|
+
// leading edge \xB7 albedo only"). A black band beside a pale tan one was the
|
|
7696
|
+
// contrast that read as wrong.
|
|
7697
|
+
float scorchT = smoothstep(charEnd, scorchEnd, xCut);
|
|
7698
|
+
vec3 scorchTint = mix(plLinear(vec3(0.24, 0.17, 0.12)), plLinear(vec3(0.42, 0.28, 0.17)), scorchT) / plLinear(vec3(0.90, 0.885, 0.855));
|
|
7699
|
+
float scorchZone = (1.0 - smoothstep(scorchEnd - 0.35, scorchEnd + 0.35, xCut)) * cutZone;
|
|
7700
|
+
color.rgb *= mix(vec3(1.0), pow(min(scorchTint, vec3(1.0)), vec3(uLook2.z)), scorchZone);
|
|
7342
7701
|
|
|
7343
7702
|
// \u2500\u2500 Char \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
7344
7703
|
// A wide, smooth hand-over from scorch to char \u2014 the gradient is the point.
|
|
7345
|
-
|
|
7704
|
+
// From 0.3: the field carries char in about one texel beside the cut, so
|
|
7705
|
+
// past a couple of millimetres c is mostly the scorch borrowed from below,
|
|
7706
|
+
// and a zone that began at 0.5 was hardly ever char at all.
|
|
7707
|
+
// Away from a cut, c only makes char where it is really high: at 0.3\u20130.6
|
|
7708
|
+
// the finger noise pushed it across the line all through the scorch, and
|
|
7709
|
+
// every crossing was a black island in the orange \u2014 a leopard print.
|
|
7710
|
+
// Round a cut, the band from the lip out to charEnd; away from one, only
|
|
7711
|
+
// where the toast has gone all the way to char.
|
|
7712
|
+
float charBand = (1.0 - smoothstep(charEnd - 0.5, charEnd + 0.5, xCut)) * cutZone;
|
|
7713
|
+
float charZone = max(smoothstep(0.6, 0.85, cSmooth) * (1.0 - cutZone), charBand);
|
|
7346
7714
|
vec2 cells = plVoronoi(p / (3.6 * PL_MM));
|
|
7347
7715
|
// Sparse: a real crack network is broken, not a tiled floor \u2014 most cell
|
|
7348
7716
|
// borders never split.
|
|
7349
7717
|
float crack = (1.0 - smoothstep(0.008, 0.03, cells.y - cells.x)) * smoothstep(0.45, 0.65, plNoise(p / (7.0 * PL_MM)));
|
|
7350
7718
|
float plate = plHash(floor(p / (3.6 * PL_MM)) + 3.7);
|
|
7351
|
-
//
|
|
7352
|
-
//
|
|
7353
|
-
//
|
|
7354
|
-
//
|
|
7355
|
-
|
|
7356
|
-
|
|
7719
|
+
// Black. Real char is a near-neutral black \u2014 (22, 20, 18) on the cold
|
|
7720
|
+
// macro references, about 11% of the paper's value. The warm brown this
|
|
7721
|
+
// used to be was Hero.png's char LIT BY ITS FLAMES, painted into the paper,
|
|
7722
|
+
// which is why ours stayed orange on a frame with no fire in it. Warmth on
|
|
7723
|
+
// char comes from the fire's light now, and goes when the fire does.
|
|
7724
|
+
// charWarmth adds only a trace of brown to the black, and a warmer umber
|
|
7725
|
+
// where the char hands over to the scorch.
|
|
7726
|
+
vec3 body = mix(plLinear(vec3(0.086, 0.078, 0.071)), plLinear(vec3(0.11, 0.08, 0.06)), uLook1.w);
|
|
7727
|
+
vec3 under = mix(plLinear(vec3(0.20, 0.15, 0.11)), plLinear(vec3(0.28, 0.16, 0.08)), uLook1.w);
|
|
7357
7728
|
vec3 cracks = plLinear(vec3(0.039, 0.024, 0.016)); // #0A0604
|
|
7358
|
-
|
|
7729
|
+
// Black through the band; the umber only in its outer millimetre.
|
|
7730
|
+
float toUnder = mix(1.0 - smoothstep(0.45, 0.8, cSmooth), smoothstep(charEnd - 1.2, charEnd + 0.3, xCut), cutZone);
|
|
7731
|
+
vec3 charColor = mix(body, under, toUnder);
|
|
7359
7732
|
// Mottled at the plate scale and finer, and darker than the sampled body
|
|
7360
7733
|
// under a bright key \u2014 the reference's char reads near black in the frame.
|
|
7361
7734
|
charColor *= (0.75 + 0.35 * plate) * (0.8 + 0.4 * plNoise(p * 260.0));
|
|
@@ -7367,42 +7740,42 @@ void plDamage(inout vec4 color, inout float roughness) {
|
|
|
7367
7740
|
plHeight += charZone * ((plNoise(p * 380.0) - 0.5) * 0.00008 + (plate - 0.5) * 0.00018 - crack * 0.00016);
|
|
7368
7741
|
|
|
7369
7742
|
// \u2500\u2500 Ash lip \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
7370
|
-
//
|
|
7371
|
-
//
|
|
7372
|
-
//
|
|
7373
|
-
//
|
|
7374
|
-
//
|
|
7375
|
-
//
|
|
7376
|
-
//
|
|
7377
|
-
//
|
|
7378
|
-
|
|
7379
|
-
|
|
7380
|
-
float
|
|
7381
|
-
//
|
|
7382
|
-
//
|
|
7383
|
-
|
|
7384
|
-
// middle of it, which read as a neon tube round the hole \u2014 the single most
|
|
7385
|
-
// synthetic thing at wide view.
|
|
7386
|
-
float lipAlong = dot(p, tangentP) / (3.2 * PL_MM);
|
|
7387
|
-
float lipBreak = smoothstep(0.34, 0.56, plFbm(vec2(lipAlong, 0.9)));
|
|
7388
|
-
float lip = (1.0 - smoothstep(lipWidth - aa, lipWidth + aa, mm)) * step(d.a, 0.999) * cutNear * lipBreak;
|
|
7389
|
-
vec3 ash = plLinear(mix(vec3(0.525, 0.498, 0.490), vec3(0.741, 0.725, 0.725), plNoise(p * 520.0) * 0.6 + plNoise(p * 90.0) * 0.4)) * uLook1.z;
|
|
7743
|
+
// A crust as wide as the char band, from the cut to lipEdge \u2014 the pale
|
|
7744
|
+
// thing that makes a hole read as a hole on a black stage (Ember_line
|
|
7745
|
+
// annotated, zone 2). It used to be held to about a millimetre, which at a
|
|
7746
|
+
// reading distance is a hairline tracing the edge, not ash.
|
|
7747
|
+
//
|
|
7748
|
+
// Broken, not a hem: ash flakes off a cooling edge in pieces, so the lip
|
|
7749
|
+
// comes and goes along the rim and wanders in width. Mostly there, though \u2014
|
|
7750
|
+
// it has to be seen.
|
|
7751
|
+
float lipAlong = dot(p, tangentR) / (3.2 * PL_MM);
|
|
7752
|
+
float lipBreak = smoothstep(0.1, 0.3, plFbm(vec2(lipAlong, 0.9)));
|
|
7753
|
+
float lip = (1.0 - smoothstep(lipEdge - aa, lipEdge + aa, xCut)) * cutZone * lipBreak;
|
|
7754
|
+
// Pale: the reference's #A49E9D up to a near-white grey, so it reads white
|
|
7755
|
+
// beside the char. Still matte and still dimmer than the paper.
|
|
7756
|
+
vec3 ash = plLinear(mix(vec3(0.643, 0.620, 0.616), vec3(0.84, 0.82, 0.81), plNoise(p * 520.0) * 0.6 + plNoise(p * 90.0) * 0.4)) * uLook1.z;
|
|
7390
7757
|
color.rgb = mix(color.rgb, ash, lip);
|
|
7391
7758
|
roughness = mix(roughness, 0.99, lip);
|
|
7392
|
-
//
|
|
7393
|
-
//
|
|
7394
|
-
//
|
|
7395
|
-
//
|
|
7396
|
-
// of
|
|
7397
|
-
//
|
|
7398
|
-
|
|
7759
|
+
// And the crust the ragged edge is made of. The cut is frayed at fibre
|
|
7760
|
+
// scale, and the fray opened specks of void all through the first few
|
|
7761
|
+
// millimetres \u2014 exactly where the lip lies \u2014 so at a reading distance the
|
|
7762
|
+
// lip was grey specks in black. Paper inside the fray's band is kept, as
|
|
7763
|
+
// ash, instead of cut away: the lip overhangs the hole a little, broken
|
|
7764
|
+
// where the lip is broken, the way the reference's crust does.
|
|
7765
|
+
float crust = lipBreak * cutZone * smoothstep(0.18, 0.3, d.a) * (1.0 - step(0.5, d.a));
|
|
7766
|
+
color.rgb = mix(color.rgb, ash, crust);
|
|
7767
|
+
roughness = mix(roughness, 0.99, crust);
|
|
7768
|
+
// Raised, and crumbly rather than rounded: a smooth ridge round the hole is
|
|
7769
|
+
// a cylinder, and a cylinder under a key light has a specular line down it
|
|
7770
|
+
// however rough it is \u2014 the neon tube this once was.
|
|
7771
|
+
plHeight += lip * (0.55 + 0.45 * plNoise(p * 700.0)) * 0.00016;
|
|
7399
7772
|
|
|
7400
7773
|
// \u2500\u2500 The ember line \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
7401
7774
|
// Beads along the cut: position along the edge, crawling on the burn's
|
|
7402
7775
|
// clock, with a flicker of their own. Only where it is hot \u2014 and as the
|
|
7403
7776
|
// heat goes the threshold rises, so the line breaks into fewer, dimmer
|
|
7404
7777
|
// beads and they go out one by one.
|
|
7405
|
-
float s1 = dot(p,
|
|
7778
|
+
float s1 = dot(p, tangentR) / (2.4 * PL_MM) - uDamageTime * 0.55 * uLook0.w;
|
|
7406
7779
|
float bead = plNoise(vec2(s1, 0.37)) * 0.7 + plNoise(vec2(s1 * 2.3, 5.1)) * 0.3;
|
|
7407
7780
|
float flicker = 0.62 + 0.38 * plNoise(vec2(floor(s1) * 7.13, uDamageTime * 9.0 * uLook0.w));
|
|
7408
7781
|
// Down to a smoulder's last flicker: a spent edge keeps a little heat in
|
|
@@ -7417,7 +7790,8 @@ void plDamage(inout vec4 color, inout float roughness) {
|
|
|
7417
7790
|
float lit = smoothstep(mix(0.82, 0.45, hot) - shift, mix(0.9, 0.58, hot) - shift, bead);
|
|
7418
7791
|
// Each bead tapers at its ends \u2014 a bead, not a dash cut from a strip.
|
|
7419
7792
|
float width = max(mix(0.4, max(uLook0.x, 0.41), plNoise(vec2(s1 * 0.7, 9.0))) * (0.35 + 0.65 * lit), 1.4 * aa);
|
|
7420
|
-
|
|
7793
|
+
// On the lip's outer edge, between the ash and the char.
|
|
7794
|
+
float band = smoothstep(lipEdge - aa, lipEdge + aa, xCut) * (1.0 - smoothstep(lipEdge + width - aa, lipEdge + width + aa, xCut)) * cutZone;
|
|
7421
7795
|
// Mostly orange: a bead is red at its ends and yellow-white only at its
|
|
7422
7796
|
// hottest core, which is what Ember_line.png shows. Mapped low and pushed
|
|
7423
7797
|
// high by a power, so white is the exception rather than the line.
|
|
@@ -7437,13 +7811,15 @@ void plDamage(inout vec4 color, inout float roughness) {
|
|
|
7437
7811
|
// edge is hot, and inside the zone the beads are held to \u2014 the lip plus
|
|
7438
7812
|
// the widest bead, measured from the cut \u2014 so the glow never reaches out
|
|
7439
7813
|
// over paper the gate says heat must not light.
|
|
7440
|
-
float zoneEnd =
|
|
7441
|
-
float seam = smoothstep(
|
|
7814
|
+
float zoneEnd = lipEdge + uLook0.x;
|
|
7815
|
+
float seam = smoothstep(lipEdge - aa, lipEdge + aa, xCut) * (1.0 - smoothstep(zoneEnd - 0.6, zoneEnd, xCut)) * cutZone;
|
|
7442
7816
|
float patch_ = smoothstep(0.35, 0.72, plFbm(p / (4.0 * PL_MM) + vec2(uDamageTime * 0.15 * uLook0.w, 0.0)));
|
|
7443
7817
|
plDamageLight += vec3(0.55, 0.06, 0.01) * seam * patch_ * hot * uLook1.x;
|
|
7444
7818
|
float speck = step(0.985, plHash(floor(p / (0.35 * PL_MM)) + floor(uDamageTime * 8.0 * uLook0.w)));
|
|
7445
7819
|
plDamageLight += plEmberRamp(0.9) * 1.6 * speck * seam * hot * uLook3.z;
|
|
7446
7820
|
|
|
7821
|
+
// The crust stays, however the fray cut it.
|
|
7822
|
+
d.a = mix(d.a, max(d.a, 0.5), step(0.5, crust));
|
|
7447
7823
|
plDamageCut(color, d);
|
|
7448
7824
|
}
|
|
7449
7825
|
`
|
|
@@ -7554,6 +7930,7 @@ function composeSurface(surface, stock, thickness, maps = { hasFrontMap: false,
|
|
|
7554
7930
|
chunks.push(DAMAGE_CHUNK, DAMAGE_SHADE_CHUNK);
|
|
7555
7931
|
calls.push("plDamage(csm_DiffuseColor, csm_Roughness);");
|
|
7556
7932
|
uniforms.uDamage = { value: null };
|
|
7933
|
+
uniforms.uDamageEdge = { value: null };
|
|
7557
7934
|
uniforms.uDamageDetail = { value: 1 };
|
|
7558
7935
|
uniforms.uDamageTime = { value: 0 };
|
|
7559
7936
|
const look = DAMAGE_LOOK_DEFAULTS;
|
|
@@ -7566,7 +7943,7 @@ function composeSurface(surface, stock, thickness, maps = { hasFrontMap: false,
|
|
|
7566
7943
|
uniforms.uLook2 = {
|
|
7567
7944
|
value: new THREE9.Vector4(look.charCracks, look.scorchReach, look.scorchDarkness, look.fingers)
|
|
7568
7945
|
};
|
|
7569
|
-
uniforms.uLook3 = { value: new THREE9.Vector4(look.edgeWave, look.edgeBite, look.sparkle,
|
|
7946
|
+
uniforms.uLook3 = { value: new THREE9.Vector4(look.edgeWave, look.edgeBite, look.sparkle, look.charWidth) };
|
|
7570
7947
|
}
|
|
7571
7948
|
const cutting = [];
|
|
7572
7949
|
const cuts = [];
|
|
@@ -7670,35 +8047,65 @@ function useLightRig(own) {
|
|
|
7670
8047
|
var THREE10 = __toESM(require("three"), 1);
|
|
7671
8048
|
var import_fiber4 = require("@react-three/fiber");
|
|
7672
8049
|
var import_react9 = require("react");
|
|
7673
|
-
|
|
7674
|
-
|
|
8050
|
+
var MM_PER_WORLD = 210;
|
|
8051
|
+
function useDamageTexture(source, sheet2 = { width: 1, height: 1.4 }) {
|
|
8052
|
+
const { width, height } = sheet2;
|
|
8053
|
+
const textures = (0, import_react9.useMemo)(() => {
|
|
7675
8054
|
if (!source) return null;
|
|
7676
|
-
const
|
|
8055
|
+
const damage = new THREE10.DataTexture(
|
|
7677
8056
|
source.pixels,
|
|
7678
8057
|
source.size,
|
|
7679
8058
|
source.size,
|
|
7680
8059
|
THREE10.RGBAFormat,
|
|
7681
8060
|
THREE10.UnsignedByteType
|
|
7682
8061
|
);
|
|
7683
|
-
|
|
7684
|
-
|
|
7685
|
-
|
|
7686
|
-
|
|
7687
|
-
|
|
7688
|
-
|
|
7689
|
-
|
|
7690
|
-
t
|
|
7691
|
-
|
|
7692
|
-
|
|
7693
|
-
|
|
7694
|
-
|
|
8062
|
+
const edge = new THREE10.DataTexture(
|
|
8063
|
+
new Uint8Array(source.size * source.size),
|
|
8064
|
+
source.size,
|
|
8065
|
+
source.size,
|
|
8066
|
+
THREE10.RedFormat,
|
|
8067
|
+
THREE10.UnsignedByteType
|
|
8068
|
+
);
|
|
8069
|
+
for (const t of [damage, edge]) {
|
|
8070
|
+
t.magFilter = THREE10.LinearFilter;
|
|
8071
|
+
t.minFilter = THREE10.LinearFilter;
|
|
8072
|
+
t.wrapS = THREE10.ClampToEdgeWrapping;
|
|
8073
|
+
t.wrapT = THREE10.ClampToEdgeWrapping;
|
|
8074
|
+
t.generateMipmaps = false;
|
|
8075
|
+
t.colorSpace = THREE10.NoColorSpace;
|
|
8076
|
+
t.flipY = false;
|
|
8077
|
+
t.unpackAlignment = 1;
|
|
8078
|
+
}
|
|
8079
|
+
measureEdge(source, edge, width, height);
|
|
8080
|
+
damage.userData.version = source.version;
|
|
8081
|
+
damage.needsUpdate = true;
|
|
8082
|
+
return { damage, edge };
|
|
8083
|
+
}, [source, width, height]);
|
|
8084
|
+
(0, import_react9.useEffect)(
|
|
8085
|
+
() => () => {
|
|
8086
|
+
textures?.damage.dispose();
|
|
8087
|
+
textures?.edge.dispose();
|
|
8088
|
+
},
|
|
8089
|
+
[textures]
|
|
8090
|
+
);
|
|
7695
8091
|
(0, import_fiber4.useFrame)(() => {
|
|
7696
|
-
if (!source || !
|
|
7697
|
-
if (
|
|
7698
|
-
|
|
7699
|
-
|
|
8092
|
+
if (!source || !textures) return;
|
|
8093
|
+
if (textures.damage.userData.version === source.version) return;
|
|
8094
|
+
textures.damage.userData.version = source.version;
|
|
8095
|
+
textures.damage.needsUpdate = true;
|
|
8096
|
+
measureEdge(source, textures.edge, width, height);
|
|
7700
8097
|
});
|
|
7701
|
-
return
|
|
8098
|
+
return textures;
|
|
8099
|
+
}
|
|
8100
|
+
function measureEdge(source, edge, width, height) {
|
|
8101
|
+
const last = Math.max(1, source.size - 1);
|
|
8102
|
+
cutDistance(
|
|
8103
|
+
source.pixels,
|
|
8104
|
+
source.size,
|
|
8105
|
+
[width * MM_PER_WORLD / last, height * MM_PER_WORLD / last],
|
|
8106
|
+
edge.image.data
|
|
8107
|
+
);
|
|
8108
|
+
edge.needsUpdate = true;
|
|
7702
8109
|
}
|
|
7703
8110
|
|
|
7704
8111
|
// src/surface/PaperMaterial.tsx
|
|
@@ -7715,7 +8122,7 @@ function PaperMaterial({
|
|
|
7715
8122
|
damage
|
|
7716
8123
|
}) {
|
|
7717
8124
|
const rig = useLightRig(lighting);
|
|
7718
|
-
const
|
|
8125
|
+
const damageTextures = useDamageTexture(damage, sheet2);
|
|
7719
8126
|
const composed = composeSurface(
|
|
7720
8127
|
surface,
|
|
7721
8128
|
stock,
|
|
@@ -7723,7 +8130,7 @@ function PaperMaterial({
|
|
|
7723
8130
|
{
|
|
7724
8131
|
hasFrontMap: Boolean(texture),
|
|
7725
8132
|
hasBackMap: Boolean(backTexture),
|
|
7726
|
-
hasDamage: Boolean(
|
|
8133
|
+
hasDamage: Boolean(damageTextures)
|
|
7727
8134
|
},
|
|
7728
8135
|
sheet2,
|
|
7729
8136
|
rig,
|
|
@@ -7732,7 +8139,7 @@ function PaperMaterial({
|
|
|
7732
8139
|
const bound = (0, import_react10.useMemo)(() => composed.uniforms, [composed.structureKey]);
|
|
7733
8140
|
(0, import_react10.useEffect)(() => {
|
|
7734
8141
|
for (const [key, uniform] of Object.entries(composed.uniforms)) {
|
|
7735
|
-
if (!bound[key] || key === "uFrontMap" || key === "uBackMap" || key === "uDamage" || key === "uDamageDetail" || key === "uDamageTime" || key.startsWith("uLook")) {
|
|
8142
|
+
if (!bound[key] || key === "uFrontMap" || key === "uBackMap" || key === "uDamage" || key === "uDamageEdge" || key === "uDamageDetail" || key === "uDamageTime" || key.startsWith("uLook")) {
|
|
7736
8143
|
continue;
|
|
7737
8144
|
}
|
|
7738
8145
|
if (bound[key].value instanceof THREE11.Color && uniform.value instanceof THREE11.Color) {
|
|
@@ -7746,8 +8153,9 @@ function PaperMaterial({
|
|
|
7746
8153
|
(0, import_react10.useEffect)(() => {
|
|
7747
8154
|
if (bound.uFrontMap) bound.uFrontMap.value = texture;
|
|
7748
8155
|
if (bound.uBackMap) bound.uBackMap.value = backTexture ?? null;
|
|
7749
|
-
if (bound.uDamage) bound.uDamage.value =
|
|
7750
|
-
|
|
8156
|
+
if (bound.uDamage) bound.uDamage.value = damageTextures?.damage ?? null;
|
|
8157
|
+
if (bound.uDamageEdge) bound.uDamageEdge.value = damageTextures?.edge ?? null;
|
|
8158
|
+
}, [bound, texture, backTexture, damageTextures]);
|
|
7751
8159
|
(0, import_fiber5.useFrame)((state) => {
|
|
7752
8160
|
if (!bound.uDamageDetail) return;
|
|
7753
8161
|
if (bound.uDamageTime) {
|
|
@@ -7780,7 +8188,7 @@ function PaperMaterial({
|
|
|
7780
8188
|
v2(look.edgeWave, d.edgeWave),
|
|
7781
8189
|
v2(look.edgeBite, d.edgeBite),
|
|
7782
8190
|
v2(look.sparkle, d.sparkle),
|
|
7783
|
-
|
|
8191
|
+
v2(look.charWidth, d.charWidth)
|
|
7784
8192
|
);
|
|
7785
8193
|
}
|
|
7786
8194
|
const detail = damage?.detail ?? 1;
|
|
@@ -8491,7 +8899,11 @@ var PaperMesh = (0, import_react12.forwardRef)(function PaperMesh2(props, ref) {
|
|
|
8491
8899
|
coupling?.update(props.damage);
|
|
8492
8900
|
sim.step(delta);
|
|
8493
8901
|
const moved = !sim.asleep;
|
|
8494
|
-
if (moved
|
|
8902
|
+
if (moved && coupling) {
|
|
8903
|
+
coupling.follow();
|
|
8904
|
+
const index = geometry.index;
|
|
8905
|
+
if (index && coupling.tear(index.array)) index.needsUpdate = true;
|
|
8906
|
+
}
|
|
8495
8907
|
if (!applyShape(sim.positions, moved) && moved) {
|
|
8496
8908
|
const position = geometry.attributes.position;
|
|
8497
8909
|
position.array.set(sim.positions);
|