paperlab 0.7.1 → 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-NTDVOOBA.js → chunk-SVWWC5UL.js} +372 -151
- 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 +402 -172
- 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 +396 -179
- 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-NTDVOOBA.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,
|
|
@@ -4437,6 +4498,16 @@ var ClothSim = class {
|
|
|
4437
4498
|
* cannot do this job: it moves paper, and a uniformly curled sheet in still
|
|
4438
4499
|
* air has no business going anywhere.
|
|
4439
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;
|
|
4440
4511
|
restBend;
|
|
4441
4512
|
/** For each bend spring, the particle between its ends; -1 for every other kind. */
|
|
4442
4513
|
constraintMiddle;
|
|
@@ -4523,6 +4594,7 @@ var ClothSim = class {
|
|
|
4523
4594
|
this.constraintStiffness = new Float64Array(m).fill(1);
|
|
4524
4595
|
this.broken = new Uint8Array(m);
|
|
4525
4596
|
this.restBend = new Float64Array(m);
|
|
4597
|
+
this.draught = new Float64Array(this.count * 3);
|
|
4526
4598
|
this.constraintMiddle = new Int32Array(m).fill(-1);
|
|
4527
4599
|
for (let k2 = 0; k2 < m; k2++) {
|
|
4528
4600
|
const a = links[k2 * 3];
|
|
@@ -4861,9 +4933,9 @@ var ClothSim = class {
|
|
|
4861
4933
|
this.prev[i3] = x2;
|
|
4862
4934
|
this.prev[i3 + 1] = y2;
|
|
4863
4935
|
this.prev[i3 + 2] = z31;
|
|
4864
|
-
p2[i3] = x2 + vx + ax * im * dt2;
|
|
4865
|
-
p2[i3 + 1] = y2 + vy + (ay * im - gravity * 3.2) * dt2;
|
|
4866
|
-
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;
|
|
4867
4939
|
maxTravel = Math.max(maxTravel, vx * vx + vy * vy + vz * vz);
|
|
4868
4940
|
}
|
|
4869
4941
|
for (let k2 = 0; k2 < this.grabbed.length; k2++) {
|
|
@@ -4961,41 +5033,13 @@ var ClothSim = class {
|
|
|
4961
5033
|
}
|
|
4962
5034
|
};
|
|
4963
5035
|
|
|
4964
|
-
// src/surface/damageContract.ts
|
|
4965
|
-
var DAMAGE_LOOK_DEFAULTS = {
|
|
4966
|
-
// §5 asks for 0.3–1 mm. 1.8 was outside it, and outside the slider's range
|
|
4967
|
-
// it was tuned in — a control at its limit is a report that something
|
|
4968
|
-
// underneath is wrong, which in this case was a fire nothing could see.
|
|
4969
|
-
emberWidth: 0.9,
|
|
4970
|
-
emberIntensity: 1.45,
|
|
4971
|
-
emberCoverage: 0.6,
|
|
4972
|
-
emberFlicker: 1.65,
|
|
4973
|
-
emberGlow: 1.25,
|
|
4974
|
-
sparkle: 0.5,
|
|
4975
|
-
lipWidth: 1.2,
|
|
4976
|
-
// Was 1.5, its slider's ceiling, which made the ash lip brighter than the
|
|
4977
|
-
// paper it sits on. Ash is pale GREY; the reference's lip is dimmer than
|
|
4978
|
-
// the sheet, not a highlight drawn on it.
|
|
4979
|
-
lipBrightness: 0.85,
|
|
4980
|
-
// Was 1, also a ceiling. At full warmth the char is milk chocolate —
|
|
4981
|
-
// closer to cardboard than to charcoal (§5). Burnt paper keeps a little
|
|
4982
|
-
// warmth in the plates and reads near black in a frame with a fire in it.
|
|
4983
|
-
charWarmth: 0.3,
|
|
4984
|
-
// Was 1, also a ceiling. The cracks are drawn as thin polygon outlines, so
|
|
4985
|
-
// at full strength the char reads as a mosaic rather than as broken plates.
|
|
4986
|
-
charCracks: 0.55,
|
|
4987
|
-
scorchReach: 30,
|
|
4988
|
-
scorchDarkness: 1.17,
|
|
4989
|
-
fingers: 1.25,
|
|
4990
|
-
edgeWave: 8.5,
|
|
4991
|
-
edgeBite: 3.6
|
|
4992
|
-
};
|
|
4993
|
-
var DAMAGE_CHANNELS = { char: 0, saturation: 1, heat: 2, presence: 3 };
|
|
4994
|
-
|
|
4995
5036
|
// src/physics/damage.ts
|
|
4996
|
-
var { char: CHAR, saturation: SATURATION, presence: PRESENCE } = DAMAGE_CHANNELS;
|
|
5037
|
+
var { char: CHAR, saturation: SATURATION, heat: HEAT, presence: PRESENCE } = DAMAGE_CHANNELS;
|
|
4997
5038
|
var GONE_BELOW = 128;
|
|
4998
5039
|
var CHAR_SHRINK = 0.12;
|
|
5040
|
+
var ACROSS_SHRINK = 0.55;
|
|
5041
|
+
var UPDRAFT = 1.1;
|
|
5042
|
+
var UPDRAFT_OUT = 0.35;
|
|
4999
5043
|
var CHAR_CURL = 6;
|
|
5000
5044
|
var FRONT_GRADIENT = 16 * 16;
|
|
5001
5045
|
var WET_MASS = 2;
|
|
@@ -5028,9 +5072,12 @@ var DamageCoupling = class {
|
|
|
5028
5072
|
/** Union-find over the particles, reused on every read. */
|
|
5029
5073
|
parent;
|
|
5030
5074
|
/**
|
|
5031
|
-
* Per
|
|
5075
|
+
* Per spring, how squarely it lies ACROSS the burn front, 0..1 — and
|
|
5032
5076
|
* remembered once it has been.
|
|
5033
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
|
+
*
|
|
5034
5081
|
* A burnt edge rolls about an axis along the front, like a scroll; it does
|
|
5035
5082
|
* not dish into a bowl. Measured with the curl in every bend spring alike: a
|
|
5036
5083
|
* burnt band settled into a saddle, its corners forward and its middle BACK
|
|
@@ -5043,7 +5090,7 @@ var DamageCoupling = class {
|
|
|
5043
5090
|
* gone by, the char behind it is uniform and says nothing about direction,
|
|
5044
5091
|
* but the paper stays rolled the way it rolled.
|
|
5045
5092
|
*/
|
|
5046
|
-
|
|
5093
|
+
acrossFront;
|
|
5047
5094
|
/** The mesh's index as it was built — what {@link tear} hides triangles from and mends them back to. */
|
|
5048
5095
|
original = null;
|
|
5049
5096
|
/** Per triangle of that index: torn, and hidden until the history is rewound. */
|
|
@@ -5058,7 +5105,7 @@ var DamageCoupling = class {
|
|
|
5058
5105
|
this.gone = new Uint8Array(sim.count);
|
|
5059
5106
|
this.piece = new Int32Array(sim.count);
|
|
5060
5107
|
this.parent = new Int32Array(sim.count);
|
|
5061
|
-
this.
|
|
5108
|
+
this.acrossFront = new Float32Array(sim.constraintCount);
|
|
5062
5109
|
}
|
|
5063
5110
|
/**
|
|
5064
5111
|
* Bring the sim's levers up to date with `source`. Call before stepping.
|
|
@@ -5070,7 +5117,7 @@ var DamageCoupling = class {
|
|
|
5070
5117
|
const next = source ?? null;
|
|
5071
5118
|
if (next === this.source && (next === null || next.version === this.version)) return false;
|
|
5072
5119
|
if (next !== this.source) {
|
|
5073
|
-
this.
|
|
5120
|
+
this.acrossFront.fill(0);
|
|
5074
5121
|
if (next && this.holed) {
|
|
5075
5122
|
this.rewind();
|
|
5076
5123
|
this.gone.fill(0);
|
|
@@ -5215,8 +5262,10 @@ var DamageCoupling = class {
|
|
|
5215
5262
|
}
|
|
5216
5263
|
}
|
|
5217
5264
|
read(pixels) {
|
|
5218
|
-
const { sim, gone, particleTexel,
|
|
5265
|
+
const { sim, gone, particleTexel, acrossFront, size } = this;
|
|
5219
5266
|
const {
|
|
5267
|
+
draught,
|
|
5268
|
+
cols,
|
|
5220
5269
|
invMass,
|
|
5221
5270
|
restLength,
|
|
5222
5271
|
naturalLength,
|
|
@@ -5236,6 +5285,12 @@ var DamageCoupling = class {
|
|
|
5236
5285
|
if (isGone) holed = true;
|
|
5237
5286
|
gone[i] = isGone ? 1 : 0;
|
|
5238
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;
|
|
5239
5294
|
}
|
|
5240
5295
|
if (rewound) this.rewind();
|
|
5241
5296
|
this.holed = holed;
|
|
@@ -5256,20 +5311,25 @@ var DamageCoupling = class {
|
|
|
5256
5311
|
samples = 3;
|
|
5257
5312
|
}
|
|
5258
5313
|
const char = charSum / (samples * 255);
|
|
5259
|
-
|
|
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));
|
|
5260
5330
|
if (constraintKind[k2] === 2) {
|
|
5261
|
-
const t = particleTexel[m];
|
|
5262
|
-
const x2 = t % size;
|
|
5263
|
-
const y2 = t / size | 0;
|
|
5264
|
-
const gx = pixels[(y2 * size + Math.min(size - 1, x2 + 2)) * 4 + CHAR] - pixels[(y2 * size + Math.max(0, x2 - 2)) * 4 + CHAR];
|
|
5265
|
-
const gy = pixels[(Math.min(size - 1, y2 + 2) * size + x2) * 4 + CHAR] - pixels[(Math.max(0, y2 - 2) * size + x2) * 4 + CHAR];
|
|
5266
|
-
const steep = gx * gx + gy * gy;
|
|
5267
|
-
if (steep > FRONT_GRADIENT) {
|
|
5268
|
-
const across = (b2 - a === 2 ? gx * gx : gy * gy) / steep;
|
|
5269
|
-
if (across > curlWeight[k2]) curlWeight[k2] = across;
|
|
5270
|
-
}
|
|
5271
5331
|
const span = naturalLength[k2];
|
|
5272
|
-
restBend[k2] = CHAR_CURL * char *
|
|
5332
|
+
restBend[k2] = CHAR_CURL * char * across * span * span / 8;
|
|
5273
5333
|
}
|
|
5274
5334
|
}
|
|
5275
5335
|
this.findPieces();
|
|
@@ -5354,19 +5414,20 @@ var DamageCoupling = class {
|
|
|
5354
5414
|
*/
|
|
5355
5415
|
rewind() {
|
|
5356
5416
|
this.sim.restore();
|
|
5357
|
-
this.
|
|
5417
|
+
this.acrossFront.fill(0);
|
|
5358
5418
|
this.torn.fill(0);
|
|
5359
5419
|
this.mend = true;
|
|
5360
5420
|
}
|
|
5361
5421
|
reset() {
|
|
5362
5422
|
const { sim } = this;
|
|
5363
5423
|
if (this.holed) this.rewind();
|
|
5424
|
+
sim.draught.fill(0);
|
|
5364
5425
|
sim.invMass.fill(1);
|
|
5365
5426
|
sim.restBend.fill(0);
|
|
5366
5427
|
sim.broken.fill(0);
|
|
5367
5428
|
sim.restLength.set(sim.naturalLength);
|
|
5368
5429
|
this.gone.fill(0);
|
|
5369
|
-
this.
|
|
5430
|
+
this.acrossFront.fill(0);
|
|
5370
5431
|
this.followers = [];
|
|
5371
5432
|
this.holed = false;
|
|
5372
5433
|
this.version = -1;
|
|
@@ -6736,6 +6797,82 @@ function p({
|
|
|
6736
6797
|
}
|
|
6737
6798
|
var j2 = r.forwardRef(p);
|
|
6738
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
|
+
|
|
6739
6876
|
// src/surface/compose.ts
|
|
6740
6877
|
var THREE9 = __toESM(require("three"), 1);
|
|
6741
6878
|
|
|
@@ -7151,7 +7288,7 @@ var DAMAGE_CHUNK = (
|
|
|
7151
7288
|
`
|
|
7152
7289
|
uniform sampler2D uDamage;
|
|
7153
7290
|
uniform float uDamageDetail;
|
|
7154
|
-
// (edgeWave mm, edgeBite mm, sparkle,
|
|
7291
|
+
// (edgeWave mm, edgeBite mm, sparkle, charWidth mm) \u2014 see DamageLook.
|
|
7155
7292
|
uniform vec4 uLook3;
|
|
7156
7293
|
|
|
7157
7294
|
// The damage grid's texels sit ON the sheet \u2014 texel x at u = x / (N - 1), its
|
|
@@ -7288,6 +7425,8 @@ var DAMAGE_SHADE_CHUNK = (
|
|
|
7288
7425
|
/* glsl */
|
|
7289
7426
|
`
|
|
7290
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;
|
|
7291
7430
|
// The look, packed \u2014 see DamageLook for what each number means.
|
|
7292
7431
|
uniform vec4 uLook0; // emberWidth mm, emberIntensity, emberCoverage, emberFlicker
|
|
7293
7432
|
uniform vec4 uLook1; // emberGlow, lipWidth mm, lipBrightness, charWarmth
|
|
@@ -7297,6 +7436,10 @@ vec3 plDamageLight;
|
|
|
7297
7436
|
|
|
7298
7437
|
// World units per millimetre: a default sheet is one unit across, and A4 is 210 mm.
|
|
7299
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)};
|
|
7300
7443
|
|
|
7301
7444
|
vec3 plLinear(vec3 srgb) {
|
|
7302
7445
|
return pow(srgb, vec3(2.2));
|
|
@@ -7353,7 +7496,12 @@ vec3 plScorchTint(float t) {
|
|
|
7353
7496
|
vec3 c3 = plLinear(vec3(0.541, 0.310, 0.141)); // #8A4F24
|
|
7354
7497
|
vec3 c4 = plLinear(vec3(0.471, 0.275, 0.149)); // #784626
|
|
7355
7498
|
vec3 c5 = plLinear(vec3(0.380, 0.216, 0.133)); // #613722
|
|
7356
|
-
|
|
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
|
|
7357
7505
|
float s = clamp(t, 0.0, 1.0) * 6.0;
|
|
7358
7506
|
// Each stop is blended with a SMOOTH step, not a linear one.
|
|
7359
7507
|
//
|
|
@@ -7404,32 +7552,6 @@ void plDamage(inout vec4 color, inout float roughness) {
|
|
|
7404
7552
|
vec2 perWorld = (vec2(textureSize(uDamage, 0)) - 1.0) / uSheetSize;
|
|
7405
7553
|
vec2 gradP = vec2(e.a - w.a, n.a - s.a) * 0.5 * perWorld;
|
|
7406
7554
|
vec2 gradC = vec2(e.r - w.r, n.r - s.r) * 0.5 * perWorld;
|
|
7407
|
-
// Millimetres from the cut, on the paper side \u2014 measured against the field
|
|
7408
|
-
// the cut is DRAWN from (softened, frayed, warped), through its own screen
|
|
7409
|
-
// derivatives. It used to divide by the slope of the raw texels, and since
|
|
7410
|
-
// paper burns through within a texel that slope is a cliff while the drawn
|
|
7411
|
-
// edge is spread over millimetres: a point well into the char computed as a
|
|
7412
|
-
// hair from the cut, and the ember line drew there \u2014 found by the gate.
|
|
7413
|
-
// The slope, though, is the SMOOTH presence's. The fray's fibre noise
|
|
7414
|
-
// changes pixel to pixel, and its slope is a cliff everywhere it touches:
|
|
7415
|
-
// divided by that, a point deep in the char came out a hair from the cut
|
|
7416
|
-
// and the ember glow drew there \u2014 the gate's stray pixels. The fray still
|
|
7417
|
-
// moves where the edge is; it just cannot fake how steep it is.
|
|
7418
|
-
float smoothA = plDamageSmooth;
|
|
7419
|
-
vec2 cutSlope = vec2(dFdx(smoothA), dFdy(smoothA));
|
|
7420
|
-
float pxFromCut = (d.a - 0.5) / max(length(cutSlope), 1e-4);
|
|
7421
|
-
// Millimetres per pixel ALONG the way to the cut, not across the screen's
|
|
7422
|
-
// x: a sheet leaning away is foreshortened vertically, so above and below a
|
|
7423
|
-
// hole a pixel covers more paper than beside it, and a scale taken from x
|
|
7424
|
-
// alone drew the ember zone too many pixels deep on the top and bottom rims.
|
|
7425
|
-
vec2 toCutPx = cutSlope / max(length(cutSlope), 1e-6);
|
|
7426
|
-
float mmPerPx = length(dFdx(-vViewPosition) * toCutPx.x + dFdy(-vViewPosition) * toCutPx.y) / PL_MM;
|
|
7427
|
-
float mm = pxFromCut * mmPerPx;
|
|
7428
|
-
// About a pixel of antialiasing, never more: where the warp makes the
|
|
7429
|
-
// estimate jump between neighbours, fwidth(mm) is millimetres wide, and the
|
|
7430
|
-
// ember's soft tail \u2014 faint, but multiplied by its light \u2014 drew stray glow
|
|
7431
|
-
// well out into the char.
|
|
7432
|
-
float aa = min(fwidth(mm), 1.5 * mmPerPx);
|
|
7433
7555
|
// Scorch reaches ahead of the char, and further UP than anywhere else:
|
|
7434
7556
|
// the char a centimetre below this point, and the char a few millimetres
|
|
7435
7557
|
// around it, each lent to the scorch at a discount (\xA74.2, \xA74.5).
|
|
@@ -7443,38 +7565,61 @@ void plDamage(inout vec4 color, inout float roughness) {
|
|
|
7443
7565
|
charWide += texture2D(uDamage, uv + vec2(cos(a), sin(a)) * 7.0 * mmUv).r;
|
|
7444
7566
|
}
|
|
7445
7567
|
charWide /= 8.0;
|
|
7446
|
-
//
|
|
7447
|
-
//
|
|
7448
|
-
//
|
|
7449
|
-
//
|
|
7450
|
-
//
|
|
7451
|
-
//
|
|
7452
|
-
|
|
7453
|
-
//
|
|
7454
|
-
//
|
|
7455
|
-
//
|
|
7456
|
-
//
|
|
7457
|
-
//
|
|
7458
|
-
//
|
|
7459
|
-
//
|
|
7460
|
-
//
|
|
7461
|
-
vec2
|
|
7462
|
-
|
|
7463
|
-
float
|
|
7464
|
-
|
|
7465
|
-
|
|
7466
|
-
//
|
|
7467
|
-
float
|
|
7468
|
-
|
|
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);
|
|
7469
7600
|
|
|
7470
7601
|
// An untouched texel is left exactly as it was \u2014 the identity the whole
|
|
7471
7602
|
// seam is built on. Nothing here has happened to this paper.
|
|
7472
|
-
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) {
|
|
7473
7604
|
return;
|
|
7474
7605
|
}
|
|
7475
7606
|
|
|
7476
7607
|
vec2 p = plLocal();
|
|
7477
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
|
+
|
|
7478
7623
|
// Wet paper is darker and smoother: water fills the gaps between fibres
|
|
7479
7624
|
// that scatter light, which is both effects from one cause.
|
|
7480
7625
|
color.rgb *= 1.0 - 0.38 * d.g;
|
|
@@ -7526,27 +7671,64 @@ void plDamage(inout vec4 color, inout float roughness) {
|
|
|
7526
7671
|
// being finer than a level it can only ever move a value into the
|
|
7527
7672
|
// neighbouring one.
|
|
7528
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);
|
|
7529
7681
|
// A steep leading edge \u2014 paper to straw in a sliver \u2014 then the long ramp.
|
|
7530
|
-
|
|
7531
|
-
|
|
7532
|
-
|
|
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);
|
|
7533
7701
|
|
|
7534
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
|
|
7535
7703
|
// A wide, smooth hand-over from scorch to char \u2014 the gradient is the point.
|
|
7536
|
-
|
|
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);
|
|
7537
7714
|
vec2 cells = plVoronoi(p / (3.6 * PL_MM));
|
|
7538
7715
|
// Sparse: a real crack network is broken, not a tiled floor \u2014 most cell
|
|
7539
7716
|
// borders never split.
|
|
7540
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)));
|
|
7541
7718
|
float plate = plHash(floor(p / (3.6 * PL_MM)) + 3.7);
|
|
7542
|
-
//
|
|
7543
|
-
//
|
|
7544
|
-
//
|
|
7545
|
-
//
|
|
7546
|
-
|
|
7547
|
-
|
|
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);
|
|
7548
7728
|
vec3 cracks = plLinear(vec3(0.039, 0.024, 0.016)); // #0A0604
|
|
7549
|
-
|
|
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);
|
|
7550
7732
|
// Mottled at the plate scale and finer, and darker than the sampled body
|
|
7551
7733
|
// under a bright key \u2014 the reference's char reads near black in the frame.
|
|
7552
7734
|
charColor *= (0.75 + 0.35 * plate) * (0.8 + 0.4 * plNoise(p * 260.0));
|
|
@@ -7558,42 +7740,42 @@ void plDamage(inout vec4 color, inout float roughness) {
|
|
|
7558
7740
|
plHeight += charZone * ((plNoise(p * 380.0) - 0.5) * 0.00008 + (plate - 0.5) * 0.00018 - crack * 0.00016);
|
|
7559
7741
|
|
|
7560
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
|
|
7561
|
-
//
|
|
7562
|
-
//
|
|
7563
|
-
//
|
|
7564
|
-
//
|
|
7565
|
-
//
|
|
7566
|
-
//
|
|
7567
|
-
//
|
|
7568
|
-
//
|
|
7569
|
-
|
|
7570
|
-
|
|
7571
|
-
float
|
|
7572
|
-
//
|
|
7573
|
-
//
|
|
7574
|
-
|
|
7575
|
-
// middle of it, which read as a neon tube round the hole \u2014 the single most
|
|
7576
|
-
// synthetic thing at wide view.
|
|
7577
|
-
float lipAlong = dot(p, tangentP) / (3.2 * PL_MM);
|
|
7578
|
-
float lipBreak = smoothstep(0.34, 0.56, plFbm(vec2(lipAlong, 0.9)));
|
|
7579
|
-
float lip = (1.0 - smoothstep(lipWidth - aa, lipWidth + aa, mm)) * step(d.a, 0.999) * cutNear * lipBreak;
|
|
7580
|
-
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;
|
|
7581
7757
|
color.rgb = mix(color.rgb, ash, lip);
|
|
7582
7758
|
roughness = mix(roughness, 0.99, lip);
|
|
7583
|
-
//
|
|
7584
|
-
//
|
|
7585
|
-
//
|
|
7586
|
-
//
|
|
7587
|
-
// of
|
|
7588
|
-
//
|
|
7589
|
-
|
|
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;
|
|
7590
7772
|
|
|
7591
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
|
|
7592
7774
|
// Beads along the cut: position along the edge, crawling on the burn's
|
|
7593
7775
|
// clock, with a flicker of their own. Only where it is hot \u2014 and as the
|
|
7594
7776
|
// heat goes the threshold rises, so the line breaks into fewer, dimmer
|
|
7595
7777
|
// beads and they go out one by one.
|
|
7596
|
-
float s1 = dot(p,
|
|
7778
|
+
float s1 = dot(p, tangentR) / (2.4 * PL_MM) - uDamageTime * 0.55 * uLook0.w;
|
|
7597
7779
|
float bead = plNoise(vec2(s1, 0.37)) * 0.7 + plNoise(vec2(s1 * 2.3, 5.1)) * 0.3;
|
|
7598
7780
|
float flicker = 0.62 + 0.38 * plNoise(vec2(floor(s1) * 7.13, uDamageTime * 9.0 * uLook0.w));
|
|
7599
7781
|
// Down to a smoulder's last flicker: a spent edge keeps a little heat in
|
|
@@ -7608,7 +7790,8 @@ void plDamage(inout vec4 color, inout float roughness) {
|
|
|
7608
7790
|
float lit = smoothstep(mix(0.82, 0.45, hot) - shift, mix(0.9, 0.58, hot) - shift, bead);
|
|
7609
7791
|
// Each bead tapers at its ends \u2014 a bead, not a dash cut from a strip.
|
|
7610
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);
|
|
7611
|
-
|
|
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;
|
|
7612
7795
|
// Mostly orange: a bead is red at its ends and yellow-white only at its
|
|
7613
7796
|
// hottest core, which is what Ember_line.png shows. Mapped low and pushed
|
|
7614
7797
|
// high by a power, so white is the exception rather than the line.
|
|
@@ -7628,13 +7811,15 @@ void plDamage(inout vec4 color, inout float roughness) {
|
|
|
7628
7811
|
// edge is hot, and inside the zone the beads are held to \u2014 the lip plus
|
|
7629
7812
|
// the widest bead, measured from the cut \u2014 so the glow never reaches out
|
|
7630
7813
|
// over paper the gate says heat must not light.
|
|
7631
|
-
float zoneEnd =
|
|
7632
|
-
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;
|
|
7633
7816
|
float patch_ = smoothstep(0.35, 0.72, plFbm(p / (4.0 * PL_MM) + vec2(uDamageTime * 0.15 * uLook0.w, 0.0)));
|
|
7634
7817
|
plDamageLight += vec3(0.55, 0.06, 0.01) * seam * patch_ * hot * uLook1.x;
|
|
7635
7818
|
float speck = step(0.985, plHash(floor(p / (0.35 * PL_MM)) + floor(uDamageTime * 8.0 * uLook0.w)));
|
|
7636
7819
|
plDamageLight += plEmberRamp(0.9) * 1.6 * speck * seam * hot * uLook3.z;
|
|
7637
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));
|
|
7638
7823
|
plDamageCut(color, d);
|
|
7639
7824
|
}
|
|
7640
7825
|
`
|
|
@@ -7745,6 +7930,7 @@ function composeSurface(surface, stock, thickness, maps = { hasFrontMap: false,
|
|
|
7745
7930
|
chunks.push(DAMAGE_CHUNK, DAMAGE_SHADE_CHUNK);
|
|
7746
7931
|
calls.push("plDamage(csm_DiffuseColor, csm_Roughness);");
|
|
7747
7932
|
uniforms.uDamage = { value: null };
|
|
7933
|
+
uniforms.uDamageEdge = { value: null };
|
|
7748
7934
|
uniforms.uDamageDetail = { value: 1 };
|
|
7749
7935
|
uniforms.uDamageTime = { value: 0 };
|
|
7750
7936
|
const look = DAMAGE_LOOK_DEFAULTS;
|
|
@@ -7757,7 +7943,7 @@ function composeSurface(surface, stock, thickness, maps = { hasFrontMap: false,
|
|
|
7757
7943
|
uniforms.uLook2 = {
|
|
7758
7944
|
value: new THREE9.Vector4(look.charCracks, look.scorchReach, look.scorchDarkness, look.fingers)
|
|
7759
7945
|
};
|
|
7760
|
-
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) };
|
|
7761
7947
|
}
|
|
7762
7948
|
const cutting = [];
|
|
7763
7949
|
const cuts = [];
|
|
@@ -7861,35 +8047,65 @@ function useLightRig(own) {
|
|
|
7861
8047
|
var THREE10 = __toESM(require("three"), 1);
|
|
7862
8048
|
var import_fiber4 = require("@react-three/fiber");
|
|
7863
8049
|
var import_react9 = require("react");
|
|
7864
|
-
|
|
7865
|
-
|
|
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)(() => {
|
|
7866
8054
|
if (!source) return null;
|
|
7867
|
-
const
|
|
8055
|
+
const damage = new THREE10.DataTexture(
|
|
7868
8056
|
source.pixels,
|
|
7869
8057
|
source.size,
|
|
7870
8058
|
source.size,
|
|
7871
8059
|
THREE10.RGBAFormat,
|
|
7872
8060
|
THREE10.UnsignedByteType
|
|
7873
8061
|
);
|
|
7874
|
-
|
|
7875
|
-
|
|
7876
|
-
|
|
7877
|
-
|
|
7878
|
-
|
|
7879
|
-
|
|
7880
|
-
|
|
7881
|
-
t
|
|
7882
|
-
|
|
7883
|
-
|
|
7884
|
-
|
|
7885
|
-
|
|
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
|
+
);
|
|
7886
8091
|
(0, import_fiber4.useFrame)(() => {
|
|
7887
|
-
if (!source || !
|
|
7888
|
-
if (
|
|
7889
|
-
|
|
7890
|
-
|
|
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);
|
|
7891
8097
|
});
|
|
7892
|
-
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;
|
|
7893
8109
|
}
|
|
7894
8110
|
|
|
7895
8111
|
// src/surface/PaperMaterial.tsx
|
|
@@ -7906,7 +8122,7 @@ function PaperMaterial({
|
|
|
7906
8122
|
damage
|
|
7907
8123
|
}) {
|
|
7908
8124
|
const rig = useLightRig(lighting);
|
|
7909
|
-
const
|
|
8125
|
+
const damageTextures = useDamageTexture(damage, sheet2);
|
|
7910
8126
|
const composed = composeSurface(
|
|
7911
8127
|
surface,
|
|
7912
8128
|
stock,
|
|
@@ -7914,7 +8130,7 @@ function PaperMaterial({
|
|
|
7914
8130
|
{
|
|
7915
8131
|
hasFrontMap: Boolean(texture),
|
|
7916
8132
|
hasBackMap: Boolean(backTexture),
|
|
7917
|
-
hasDamage: Boolean(
|
|
8133
|
+
hasDamage: Boolean(damageTextures)
|
|
7918
8134
|
},
|
|
7919
8135
|
sheet2,
|
|
7920
8136
|
rig,
|
|
@@ -7923,7 +8139,7 @@ function PaperMaterial({
|
|
|
7923
8139
|
const bound = (0, import_react10.useMemo)(() => composed.uniforms, [composed.structureKey]);
|
|
7924
8140
|
(0, import_react10.useEffect)(() => {
|
|
7925
8141
|
for (const [key, uniform] of Object.entries(composed.uniforms)) {
|
|
7926
|
-
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")) {
|
|
7927
8143
|
continue;
|
|
7928
8144
|
}
|
|
7929
8145
|
if (bound[key].value instanceof THREE11.Color && uniform.value instanceof THREE11.Color) {
|
|
@@ -7937,8 +8153,9 @@ function PaperMaterial({
|
|
|
7937
8153
|
(0, import_react10.useEffect)(() => {
|
|
7938
8154
|
if (bound.uFrontMap) bound.uFrontMap.value = texture;
|
|
7939
8155
|
if (bound.uBackMap) bound.uBackMap.value = backTexture ?? null;
|
|
7940
|
-
if (bound.uDamage) bound.uDamage.value =
|
|
7941
|
-
|
|
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]);
|
|
7942
8159
|
(0, import_fiber5.useFrame)((state) => {
|
|
7943
8160
|
if (!bound.uDamageDetail) return;
|
|
7944
8161
|
if (bound.uDamageTime) {
|
|
@@ -7971,7 +8188,7 @@ function PaperMaterial({
|
|
|
7971
8188
|
v2(look.edgeWave, d.edgeWave),
|
|
7972
8189
|
v2(look.edgeBite, d.edgeBite),
|
|
7973
8190
|
v2(look.sparkle, d.sparkle),
|
|
7974
|
-
|
|
8191
|
+
v2(look.charWidth, d.charWidth)
|
|
7975
8192
|
);
|
|
7976
8193
|
}
|
|
7977
8194
|
const detail = damage?.detail ?? 1;
|