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/index.cjs
CHANGED
|
@@ -1562,8 +1562,23 @@ var backdropSchema = import_zod15.z.object({
|
|
|
1562
1562
|
/** Out of focus, for the same reason. */
|
|
1563
1563
|
blur: import_zod15.z.number().min(0).max(1).default(0.2)
|
|
1564
1564
|
});
|
|
1565
|
+
var floorSchema = import_zod15.z.object({
|
|
1566
|
+
enabled: import_zod15.z.boolean().default(false),
|
|
1567
|
+
/** Dark and matte: it is the ground, not the subject. */
|
|
1568
|
+
color: import_zod15.z.string().default("#1a1917").describe("color"),
|
|
1569
|
+
/**
|
|
1570
|
+
* Where it lies, in the sheet's own space. The contact shadow falls here
|
|
1571
|
+
* too, and a simulated sheet stops here — one floor, one height, so paper
|
|
1572
|
+
* cannot land through the thing its shadow is on.
|
|
1573
|
+
*/
|
|
1574
|
+
y: import_zod15.z.number().min(-4).max(0).default(-1.05),
|
|
1575
|
+
/** How rough it is, 0 mirror to 1 chalk. Slate, by default. */
|
|
1576
|
+
roughness: import_zod15.z.number().min(0).max(1).default(0.92)
|
|
1577
|
+
});
|
|
1565
1578
|
var sceneSchema = import_zod15.z.object({
|
|
1566
1579
|
lighting: import_zod15.z.enum(lightingNames).default("studio"),
|
|
1580
|
+
/** The ground under the sheet — see `floorSchema`. Off unless asked for. */
|
|
1581
|
+
floor: floorSchema.prefault({}),
|
|
1567
1582
|
/** What is behind the sheet. Unset leaves the canvas alone. */
|
|
1568
1583
|
backdrop: backdropSchema.optional(),
|
|
1569
1584
|
/**
|
|
@@ -3718,6 +3733,8 @@ var ClothSim = class {
|
|
|
3718
3733
|
height;
|
|
3719
3734
|
positions;
|
|
3720
3735
|
prev;
|
|
3736
|
+
/** Where the constructor laid every particle out — what {@link restore} puts back. */
|
|
3737
|
+
laidOut;
|
|
3721
3738
|
pinned;
|
|
3722
3739
|
pinTargets;
|
|
3723
3740
|
/**
|
|
@@ -3793,6 +3810,16 @@ var ClothSim = class {
|
|
|
3793
3810
|
* cannot do this job: it moves paper, and a uniformly curled sheet in still
|
|
3794
3811
|
* air has no business going anywhere.
|
|
3795
3812
|
*/
|
|
3813
|
+
/**
|
|
3814
|
+
* An acceleration on each particle from whatever is in the air around the
|
|
3815
|
+
* sheet, xyz — the fire's updraft, so far (`DamageCoupling`). Not the wind:
|
|
3816
|
+
* the wind is one thing blowing on the whole sheet, and this is local, a
|
|
3817
|
+
* hot column over a burning rim and nothing a hand's width away.
|
|
3818
|
+
*
|
|
3819
|
+
* An acceleration rather than a force, so it lifts a soaked sheet no
|
|
3820
|
+
* differently from a dry one: hot air does not know what paper weighs.
|
|
3821
|
+
*/
|
|
3822
|
+
draught;
|
|
3796
3823
|
restBend;
|
|
3797
3824
|
/** For each bend spring, the particle between its ends; -1 for every other kind. */
|
|
3798
3825
|
constraintMiddle;
|
|
@@ -3851,6 +3878,7 @@ var ClothSim = class {
|
|
|
3851
3878
|
}
|
|
3852
3879
|
}
|
|
3853
3880
|
this.prev.set(this.positions);
|
|
3881
|
+
this.laidOut = this.positions.slice();
|
|
3854
3882
|
const idx = (r2, c2) => r2 * cols + c2;
|
|
3855
3883
|
const links = [];
|
|
3856
3884
|
const link = (a, b2, kind) => {
|
|
@@ -3878,6 +3906,7 @@ var ClothSim = class {
|
|
|
3878
3906
|
this.constraintStiffness = new Float64Array(m).fill(1);
|
|
3879
3907
|
this.broken = new Uint8Array(m);
|
|
3880
3908
|
this.restBend = new Float64Array(m);
|
|
3909
|
+
this.draught = new Float64Array(this.count * 3);
|
|
3881
3910
|
this.constraintMiddle = new Int32Array(m).fill(-1);
|
|
3882
3911
|
for (let k2 = 0; k2 < m; k2++) {
|
|
3883
3912
|
const a = links[k2 * 3];
|
|
@@ -3968,6 +3997,22 @@ var ClothSim = class {
|
|
|
3968
3997
|
this.asleep = false;
|
|
3969
3998
|
this.stillFrames = 0;
|
|
3970
3999
|
}
|
|
4000
|
+
/**
|
|
4001
|
+
* Back to the sheet as it was laid out: flat, still, every particle where
|
|
4002
|
+
* the constructor put it. Pins, a grab and every lever are left alone.
|
|
4003
|
+
*
|
|
4004
|
+
* For a history that has been rewound — a burn played back from an earlier
|
|
4005
|
+
* moment, say, which hands back paper that had burnt away and paper that
|
|
4006
|
+
* had FALLEN, lying on the floor with its springs to the sheet suddenly
|
|
4007
|
+
* whole again. Solved from there, the solve hauls it up through the sheet
|
|
4008
|
+
* in a tangle. Laid out afresh, it is simply the sheet it is again.
|
|
4009
|
+
*/
|
|
4010
|
+
restore() {
|
|
4011
|
+
this.positions.set(this.laidOut);
|
|
4012
|
+
this.prev.set(this.laidOut);
|
|
4013
|
+
this.accumulator = 0;
|
|
4014
|
+
this.wake();
|
|
4015
|
+
}
|
|
3971
4016
|
/**
|
|
3972
4017
|
* Take hold of the sheet at one particle.
|
|
3973
4018
|
*
|
|
@@ -4200,9 +4245,9 @@ var ClothSim = class {
|
|
|
4200
4245
|
this.prev[i3] = x2;
|
|
4201
4246
|
this.prev[i3 + 1] = y2;
|
|
4202
4247
|
this.prev[i3 + 2] = z26;
|
|
4203
|
-
p2[i3] = x2 + vx + ax * im * dt2;
|
|
4204
|
-
p2[i3 + 1] = y2 + vy + (ay * im - gravity * 3.2) * dt2;
|
|
4205
|
-
p2[i3 + 2] = z26 + vz + az * im * dt2;
|
|
4248
|
+
p2[i3] = x2 + vx + (ax * im + this.draught[i3]) * dt2;
|
|
4249
|
+
p2[i3 + 1] = y2 + vy + (ay * im - gravity * 3.2 + this.draught[i3 + 1]) * dt2;
|
|
4250
|
+
p2[i3 + 2] = z26 + vz + (az * im + this.draught[i3 + 2]) * dt2;
|
|
4206
4251
|
maxTravel = Math.max(maxTravel, vx * vx + vy * vy + vz * vz);
|
|
4207
4252
|
}
|
|
4208
4253
|
for (let k2 = 0; k2 < this.grabbed.length; k2++) {
|
|
@@ -4301,43 +4346,44 @@ var ClothSim = class {
|
|
|
4301
4346
|
};
|
|
4302
4347
|
|
|
4303
4348
|
// src/surface/damageContract.ts
|
|
4349
|
+
function roomLight(source) {
|
|
4350
|
+
const room = source?.firelight?.room;
|
|
4351
|
+
if (room === void 0 || !(room >= 0)) return 1;
|
|
4352
|
+
return Math.min(1, room);
|
|
4353
|
+
}
|
|
4304
4354
|
var DAMAGE_LOOK_DEFAULTS = {
|
|
4305
|
-
|
|
4306
|
-
|
|
4307
|
-
// underneath is wrong, which in this case was a fire nothing could see.
|
|
4308
|
-
emberWidth: 0.9,
|
|
4309
|
-
emberIntensity: 1.45,
|
|
4355
|
+
emberWidth: 1.25,
|
|
4356
|
+
emberIntensity: 1.05,
|
|
4310
4357
|
emberCoverage: 0.6,
|
|
4311
4358
|
emberFlicker: 1.65,
|
|
4312
4359
|
emberGlow: 1.25,
|
|
4313
4360
|
sparkle: 0.5,
|
|
4314
|
-
|
|
4315
|
-
//
|
|
4316
|
-
|
|
4317
|
-
|
|
4318
|
-
lipBrightness: 0.
|
|
4319
|
-
|
|
4320
|
-
|
|
4321
|
-
|
|
4322
|
-
|
|
4323
|
-
|
|
4324
|
-
|
|
4325
|
-
|
|
4326
|
-
scorchReach: 30,
|
|
4327
|
-
scorchDarkness: 1.17,
|
|
4328
|
-
fingers: 1.25,
|
|
4329
|
-
edgeWave: 8.5,
|
|
4330
|
-
edgeBite: 3.6
|
|
4361
|
+
// As wide as the char band (Noor, 2026-09-13): the lip has to be seen. At
|
|
4362
|
+
// 0.95 mm it was a hairline tracing the edge.
|
|
4363
|
+
lipWidth: 3.5,
|
|
4364
|
+
charWidth: 3.5,
|
|
4365
|
+
lipBrightness: 0.76,
|
|
4366
|
+
charWarmth: 0.32,
|
|
4367
|
+
charCracks: 0.45,
|
|
4368
|
+
scorchReach: 9,
|
|
4369
|
+
scorchDarkness: 0.86,
|
|
4370
|
+
fingers: 1.95,
|
|
4371
|
+
edgeWave: 13,
|
|
4372
|
+
edgeBite: 6
|
|
4331
4373
|
};
|
|
4332
4374
|
var DAMAGE_CHANNELS = { char: 0, saturation: 1, heat: 2, presence: 3 };
|
|
4333
4375
|
|
|
4334
4376
|
// src/physics/damage.ts
|
|
4335
|
-
var { char: CHAR, saturation: SATURATION, presence: PRESENCE } = DAMAGE_CHANNELS;
|
|
4377
|
+
var { char: CHAR, saturation: SATURATION, heat: HEAT, presence: PRESENCE } = DAMAGE_CHANNELS;
|
|
4336
4378
|
var GONE_BELOW = 128;
|
|
4337
4379
|
var CHAR_SHRINK = 0.12;
|
|
4380
|
+
var ACROSS_SHRINK = 0.55;
|
|
4381
|
+
var UPDRAFT = 1.1;
|
|
4382
|
+
var UPDRAFT_OUT = 0.35;
|
|
4338
4383
|
var CHAR_CURL = 6;
|
|
4339
4384
|
var FRONT_GRADIENT = 16 * 16;
|
|
4340
4385
|
var WET_MASS = 2;
|
|
4386
|
+
var TEAR_STRETCH = 1.5;
|
|
4341
4387
|
var DamageCoupling = class {
|
|
4342
4388
|
sim;
|
|
4343
4389
|
source = null;
|
|
@@ -4347,12 +4393,31 @@ var DamageCoupling = class {
|
|
|
4347
4393
|
particleTexel = new Int32Array(0);
|
|
4348
4394
|
/** Gone, per particle, as of the last read. */
|
|
4349
4395
|
gone;
|
|
4396
|
+
/** Whether any particle has ever been gone since the last reset — nothing can tear before. */
|
|
4397
|
+
holed = false;
|
|
4350
4398
|
/** The gone particles with live paper beside them. See {@link follow}. */
|
|
4351
4399
|
followers = [];
|
|
4352
4400
|
/**
|
|
4353
|
-
*
|
|
4401
|
+
* Which piece of paper each particle belongs to, named by the lowest index
|
|
4402
|
+
* in it — or -1 in the middle of a hole.
|
|
4403
|
+
*
|
|
4404
|
+
* A piece is whatever the sim's unbroken springs still hold together, which
|
|
4405
|
+
* is exactly what moves as one: two pieces are two things the solve lets
|
|
4406
|
+
* part. A gone particle on a cut takes the piece it borders MOST — a cut a
|
|
4407
|
+
* single particle wide borders two, and a particle averaged between them
|
|
4408
|
+
* would be dragged across the gap as one piece fell, stretching both edges
|
|
4409
|
+
* into it.
|
|
4410
|
+
*/
|
|
4411
|
+
piece;
|
|
4412
|
+
/** Union-find over the particles, reused on every read. */
|
|
4413
|
+
parent;
|
|
4414
|
+
/**
|
|
4415
|
+
* Per spring, how squarely it lies ACROSS the burn front, 0..1 — and
|
|
4354
4416
|
* remembered once it has been.
|
|
4355
4417
|
*
|
|
4418
|
+
* It decides two things: how far the spring shrinks ({@link ACROSS_SHRINK})
|
|
4419
|
+
* and, for a bend spring, which way the paper rolls ({@link CHAR_CURL}).
|
|
4420
|
+
*
|
|
4356
4421
|
* A burnt edge rolls about an axis along the front, like a scroll; it does
|
|
4357
4422
|
* not dish into a bowl. Measured with the curl in every bend spring alike: a
|
|
4358
4423
|
* burnt band settled into a saddle, its corners forward and its middle BACK
|
|
@@ -4365,11 +4430,22 @@ var DamageCoupling = class {
|
|
|
4365
4430
|
* gone by, the char behind it is uniform and says nothing about direction,
|
|
4366
4431
|
* but the paper stays rolled the way it rolled.
|
|
4367
4432
|
*/
|
|
4368
|
-
|
|
4433
|
+
acrossFront;
|
|
4434
|
+
/** The mesh's index as it was built — what {@link tear} hides triangles from and mends them back to. */
|
|
4435
|
+
original = null;
|
|
4436
|
+
/** Per triangle of that index: torn, and hidden until the history is rewound. */
|
|
4437
|
+
torn = new Uint8Array(0);
|
|
4438
|
+
/** The triangles with a gone corner — the only ones that can tear. */
|
|
4439
|
+
rim = [];
|
|
4440
|
+
rimStale = true;
|
|
4441
|
+
/** Every torn triangle has to be put back: the history was rewound, or the source went. */
|
|
4442
|
+
mend = false;
|
|
4369
4443
|
constructor(sim) {
|
|
4370
4444
|
this.sim = sim;
|
|
4371
4445
|
this.gone = new Uint8Array(sim.count);
|
|
4372
|
-
this.
|
|
4446
|
+
this.piece = new Int32Array(sim.count);
|
|
4447
|
+
this.parent = new Int32Array(sim.count);
|
|
4448
|
+
this.acrossFront = new Float32Array(sim.constraintCount);
|
|
4373
4449
|
}
|
|
4374
4450
|
/**
|
|
4375
4451
|
* Bring the sim's levers up to date with `source`. Call before stepping.
|
|
@@ -4380,7 +4456,13 @@ var DamageCoupling = class {
|
|
|
4380
4456
|
update(source) {
|
|
4381
4457
|
const next = source ?? null;
|
|
4382
4458
|
if (next === this.source && (next === null || next.version === this.version)) return false;
|
|
4383
|
-
if (next !== this.source)
|
|
4459
|
+
if (next !== this.source) {
|
|
4460
|
+
this.acrossFront.fill(0);
|
|
4461
|
+
if (next && this.holed) {
|
|
4462
|
+
this.rewind();
|
|
4463
|
+
this.gone.fill(0);
|
|
4464
|
+
}
|
|
4465
|
+
}
|
|
4384
4466
|
this.source = next;
|
|
4385
4467
|
if (!next) {
|
|
4386
4468
|
this.reset();
|
|
@@ -4400,12 +4482,13 @@ var DamageCoupling = class {
|
|
|
4400
4482
|
* laid out flat around them — their positions plus its rest offset from
|
|
4401
4483
|
* each, averaged. The triangles that straddle the burnt edge then keep
|
|
4402
4484
|
* roughly their shape instead of stretching back to wherever the particle
|
|
4403
|
-
* was when it burnt. Only
|
|
4404
|
-
*
|
|
4485
|
+
* was when it burnt. Only the neighbours in its own {@link piece} count, so
|
|
4486
|
+
* on a cut between two pieces it leaves with one of them.
|
|
4405
4487
|
*/
|
|
4406
4488
|
follow() {
|
|
4407
4489
|
if (this.followers.length === 0) return;
|
|
4408
4490
|
const { gone, sim } = this;
|
|
4491
|
+
const piece = this.piece;
|
|
4409
4492
|
const p2 = sim.positions;
|
|
4410
4493
|
const { cols, rows } = sim;
|
|
4411
4494
|
const cellX = cols > 1 ? sim.width / (cols - 1) : 0;
|
|
@@ -4413,6 +4496,7 @@ var DamageCoupling = class {
|
|
|
4413
4496
|
for (const g of this.followers) {
|
|
4414
4497
|
const r2 = g / cols | 0;
|
|
4415
4498
|
const c2 = g % cols;
|
|
4499
|
+
const own = piece[g];
|
|
4416
4500
|
let sx = 0;
|
|
4417
4501
|
let sy = 0;
|
|
4418
4502
|
let sz = 0;
|
|
@@ -4424,7 +4508,7 @@ var DamageCoupling = class {
|
|
|
4424
4508
|
const nc = c2 + dc;
|
|
4425
4509
|
if (dr === 0 && dc === 0 || nc < 0 || nc >= cols) continue;
|
|
4426
4510
|
const j3 = nr * cols + nc;
|
|
4427
|
-
if (gone[j3]) continue;
|
|
4511
|
+
if (gone[j3] || piece[j3] !== own) continue;
|
|
4428
4512
|
const j32 = j3 * 3;
|
|
4429
4513
|
sx += p2[j32] - dc * cellX;
|
|
4430
4514
|
sy += p2[j32 + 1] + dr * cellY;
|
|
@@ -4439,6 +4523,70 @@ var DamageCoupling = class {
|
|
|
4439
4523
|
p2[g3 + 2] = sz / n;
|
|
4440
4524
|
}
|
|
4441
4525
|
}
|
|
4526
|
+
/**
|
|
4527
|
+
* Hide the triangles a piece has torn away from. Call after {@link follow},
|
|
4528
|
+
* with the sheet's index buffer: it is rewritten in place, and the return
|
|
4529
|
+
* says whether it changed, so the caller knows to upload it.
|
|
4530
|
+
*
|
|
4531
|
+
* A triangle tears when one of its edges has stretched past
|
|
4532
|
+
* {@link TEAR_STRETCH} — which, among triangles with a burnt-away corner,
|
|
4533
|
+
* only ever happens across a gap that is opening. It is then collapsed to a
|
|
4534
|
+
* point, which draws nothing, casts no shadow and adds nothing to its
|
|
4535
|
+
* corners' normals, and it stays hidden: torn paper does not mend because
|
|
4536
|
+
* the two sides swing close again. Only a rewound history puts it back.
|
|
4537
|
+
*
|
|
4538
|
+
* A sheet in one piece, held, never stretches one that far — so a burn that
|
|
4539
|
+
* cuts nothing loose leaves this index exactly as it was built.
|
|
4540
|
+
*/
|
|
4541
|
+
tear(index) {
|
|
4542
|
+
if (!this.holed && !this.mend) return false;
|
|
4543
|
+
if (!this.original || this.original.length !== index.length) {
|
|
4544
|
+
this.original = index.slice();
|
|
4545
|
+
this.torn = new Uint8Array(index.length / 3);
|
|
4546
|
+
this.rimStale = true;
|
|
4547
|
+
}
|
|
4548
|
+
const { original, torn, gone, sim } = this;
|
|
4549
|
+
let changed = false;
|
|
4550
|
+
if (this.mend) {
|
|
4551
|
+
this.mend = false;
|
|
4552
|
+
index.set(original);
|
|
4553
|
+
changed = true;
|
|
4554
|
+
}
|
|
4555
|
+
if (this.rimStale) {
|
|
4556
|
+
this.rimStale = false;
|
|
4557
|
+
const rim = [];
|
|
4558
|
+
for (let t = 0; t < torn.length; t++) {
|
|
4559
|
+
if (gone[original[t * 3]] || gone[original[t * 3 + 1]] || gone[original[t * 3 + 2]]) rim.push(t);
|
|
4560
|
+
}
|
|
4561
|
+
this.rim = rim;
|
|
4562
|
+
}
|
|
4563
|
+
const p2 = sim.positions;
|
|
4564
|
+
const { cols, rows } = sim;
|
|
4565
|
+
const cellX = cols > 1 ? sim.width / (cols - 1) : 0;
|
|
4566
|
+
const cellY = rows > 1 ? sim.height / (rows - 1) : 0;
|
|
4567
|
+
const limit = TEAR_STRETCH * TEAR_STRETCH;
|
|
4568
|
+
const stretched = (i, j3) => {
|
|
4569
|
+
const dc = i % cols - j3 % cols;
|
|
4570
|
+
const dr = (i / cols | 0) - (j3 / cols | 0);
|
|
4571
|
+
const rest = (dc * cellX) ** 2 + (dr * cellY) ** 2;
|
|
4572
|
+
const i3 = i * 3;
|
|
4573
|
+
const j32 = j3 * 3;
|
|
4574
|
+
const now = (p2[i3] - p2[j32]) ** 2 + (p2[i3 + 1] - p2[j32 + 1]) ** 2 + (p2[i3 + 2] - p2[j32 + 2]) ** 2;
|
|
4575
|
+
return now > rest * limit;
|
|
4576
|
+
};
|
|
4577
|
+
for (const t of this.rim) {
|
|
4578
|
+
if (torn[t]) continue;
|
|
4579
|
+
const a = original[t * 3];
|
|
4580
|
+
const b2 = original[t * 3 + 1];
|
|
4581
|
+
const c2 = original[t * 3 + 2];
|
|
4582
|
+
if (!stretched(a, b2) && !stretched(b2, c2) && !stretched(c2, a)) continue;
|
|
4583
|
+
torn[t] = 1;
|
|
4584
|
+
index[t * 3 + 1] = a;
|
|
4585
|
+
index[t * 3 + 2] = a;
|
|
4586
|
+
changed = true;
|
|
4587
|
+
}
|
|
4588
|
+
return changed;
|
|
4589
|
+
}
|
|
4442
4590
|
/** Map each particle to its texel. The damage grid's row 0 is v = 0, the sheet's BOTTOM. */
|
|
4443
4591
|
layout(size) {
|
|
4444
4592
|
const { cols, rows, count } = this.sim;
|
|
@@ -4454,8 +4602,10 @@ var DamageCoupling = class {
|
|
|
4454
4602
|
}
|
|
4455
4603
|
}
|
|
4456
4604
|
read(pixels) {
|
|
4457
|
-
const { sim, gone, particleTexel,
|
|
4605
|
+
const { sim, gone, particleTexel, acrossFront, size } = this;
|
|
4458
4606
|
const {
|
|
4607
|
+
draught,
|
|
4608
|
+
cols,
|
|
4459
4609
|
invMass,
|
|
4460
4610
|
restLength,
|
|
4461
4611
|
naturalLength,
|
|
@@ -4466,12 +4616,25 @@ var DamageCoupling = class {
|
|
|
4466
4616
|
constraintKind,
|
|
4467
4617
|
constraintMiddle
|
|
4468
4618
|
} = sim;
|
|
4619
|
+
let rewound = false;
|
|
4620
|
+
let holed = false;
|
|
4469
4621
|
for (let i = 0; i < sim.count; i++) {
|
|
4470
4622
|
const t = particleTexel[i] * 4;
|
|
4471
4623
|
const isGone = pixels[t + PRESENCE] < GONE_BELOW;
|
|
4624
|
+
if (gone[i] && !isGone) rewound = true;
|
|
4625
|
+
if (isGone) holed = true;
|
|
4472
4626
|
gone[i] = isGone ? 1 : 0;
|
|
4473
4627
|
invMass[i] = isGone ? 0 : 1 / (1 + WET_MASS * pixels[t + SATURATION] / 255);
|
|
4628
|
+
const heat = isGone ? 0 : pixels[t + HEAT] / 255;
|
|
4629
|
+
const lift = heat > 0.15 ? (heat - 0.15) / 0.85 : 0;
|
|
4630
|
+
const i3 = i * 3;
|
|
4631
|
+
draught[i3] = 0;
|
|
4632
|
+
draught[i3 + 1] = UPDRAFT * lift;
|
|
4633
|
+
draught[i3 + 2] = UPDRAFT * UPDRAFT_OUT * lift;
|
|
4474
4634
|
}
|
|
4635
|
+
if (rewound) this.rewind();
|
|
4636
|
+
this.holed = holed;
|
|
4637
|
+
this.rimStale = true;
|
|
4475
4638
|
for (let k2 = 0; k2 < sim.constraintCount; k2++) {
|
|
4476
4639
|
const a = constraintA[k2];
|
|
4477
4640
|
const b2 = constraintB[k2];
|
|
@@ -4488,54 +4651,125 @@ var DamageCoupling = class {
|
|
|
4488
4651
|
samples = 3;
|
|
4489
4652
|
}
|
|
4490
4653
|
const char = charSum / (samples * 255);
|
|
4491
|
-
|
|
4654
|
+
const t = particleTexel[m >= 0 ? m : a];
|
|
4655
|
+
const x2 = t % size;
|
|
4656
|
+
const y2 = t / size | 0;
|
|
4657
|
+
const gx = pixels[(y2 * size + Math.min(size - 1, x2 + 2)) * 4 + CHAR] - pixels[(y2 * size + Math.max(0, x2 - 2)) * 4 + CHAR];
|
|
4658
|
+
const gy = pixels[(Math.min(size - 1, y2 + 2) * size + x2) * 4 + CHAR] - pixels[(Math.max(0, y2 - 2) * size + x2) * 4 + CHAR];
|
|
4659
|
+
const steep = gx * gx + gy * gy;
|
|
4660
|
+
if (steep > FRONT_GRADIENT) {
|
|
4661
|
+
const dc = b2 % cols - a % cols;
|
|
4662
|
+
const dr = (b2 / cols | 0) - (a / cols | 0);
|
|
4663
|
+
const along = dc * gx - dr * gy;
|
|
4664
|
+
const span = dc * dc + dr * dr || 1;
|
|
4665
|
+
const across2 = along * along / (span * steep);
|
|
4666
|
+
if (across2 > acrossFront[k2]) acrossFront[k2] = across2;
|
|
4667
|
+
}
|
|
4668
|
+
const across = acrossFront[k2];
|
|
4669
|
+
restLength[k2] = naturalLength[k2] * (1 - CHAR_SHRINK * char * (1 - (1 - ACROSS_SHRINK) * across));
|
|
4492
4670
|
if (constraintKind[k2] === 2) {
|
|
4493
|
-
const t = particleTexel[m];
|
|
4494
|
-
const x2 = t % size;
|
|
4495
|
-
const y2 = t / size | 0;
|
|
4496
|
-
const gx = pixels[(y2 * size + Math.min(size - 1, x2 + 2)) * 4 + CHAR] - pixels[(y2 * size + Math.max(0, x2 - 2)) * 4 + CHAR];
|
|
4497
|
-
const gy = pixels[(Math.min(size - 1, y2 + 2) * size + x2) * 4 + CHAR] - pixels[(Math.max(0, y2 - 2) * size + x2) * 4 + CHAR];
|
|
4498
|
-
const steep = gx * gx + gy * gy;
|
|
4499
|
-
if (steep > FRONT_GRADIENT) {
|
|
4500
|
-
const across = (b2 - a === 2 ? gx * gx : gy * gy) / steep;
|
|
4501
|
-
if (across > curlWeight[k2]) curlWeight[k2] = across;
|
|
4502
|
-
}
|
|
4503
4671
|
const span = naturalLength[k2];
|
|
4504
|
-
restBend[k2] = CHAR_CURL * char *
|
|
4672
|
+
restBend[k2] = CHAR_CURL * char * across * span * span / 8;
|
|
4673
|
+
}
|
|
4674
|
+
}
|
|
4675
|
+
this.findPieces();
|
|
4676
|
+
}
|
|
4677
|
+
/**
|
|
4678
|
+
* Which piece each particle is in ({@link piece}), and which gone ones
|
|
4679
|
+
* follow a piece at all ({@link followers}).
|
|
4680
|
+
*
|
|
4681
|
+
* Pieces by union-find over the springs that still hold, rooted at the
|
|
4682
|
+
* lowest index so the same burn names the same pieces every time. Then each
|
|
4683
|
+
* gone particle with live paper beside it joins the piece it has the most
|
|
4684
|
+
* neighbours in, the lower-named on a tie.
|
|
4685
|
+
*/
|
|
4686
|
+
findPieces() {
|
|
4687
|
+
const { sim, gone } = this;
|
|
4688
|
+
const parent = this.parent;
|
|
4689
|
+
const piece = this.piece;
|
|
4690
|
+
const { broken, constraintA, constraintB } = sim;
|
|
4691
|
+
for (let i = 0; i < sim.count; i++) parent[i] = i;
|
|
4692
|
+
const find = (i) => {
|
|
4693
|
+
let r2 = i;
|
|
4694
|
+
while (parent[r2] !== r2) r2 = parent[r2];
|
|
4695
|
+
while (parent[i] !== r2) {
|
|
4696
|
+
const up = parent[i];
|
|
4697
|
+
parent[i] = r2;
|
|
4698
|
+
i = up;
|
|
4505
4699
|
}
|
|
4700
|
+
return r2;
|
|
4701
|
+
};
|
|
4702
|
+
for (let k2 = 0; k2 < sim.constraintCount; k2++) {
|
|
4703
|
+
if (broken[k2]) continue;
|
|
4704
|
+
const ra = find(constraintA[k2]);
|
|
4705
|
+
const rb = find(constraintB[k2]);
|
|
4706
|
+
if (ra < rb) parent[rb] = ra;
|
|
4707
|
+
else if (rb < ra) parent[ra] = rb;
|
|
4506
4708
|
}
|
|
4709
|
+
for (let i = 0; i < sim.count; i++) piece[i] = gone[i] ? -1 : find(i);
|
|
4507
4710
|
const followers = [];
|
|
4508
4711
|
const { cols, rows } = sim;
|
|
4712
|
+
const seen = [];
|
|
4713
|
+
const votes = [];
|
|
4509
4714
|
for (let i = 0; i < sim.count; i++) {
|
|
4510
4715
|
if (!gone[i]) continue;
|
|
4511
4716
|
const r2 = i / cols | 0;
|
|
4512
4717
|
const c2 = i % cols;
|
|
4513
|
-
|
|
4514
|
-
|
|
4718
|
+
seen.length = 0;
|
|
4719
|
+
votes.length = 0;
|
|
4720
|
+
for (let dr = -1; dr <= 1; dr++) {
|
|
4515
4721
|
const nr = r2 + dr;
|
|
4516
4722
|
if (nr < 0 || nr >= rows) continue;
|
|
4517
4723
|
for (let dc = -1; dc <= 1; dc++) {
|
|
4518
4724
|
const nc = c2 + dc;
|
|
4519
4725
|
if (nc < 0 || nc >= cols) continue;
|
|
4520
|
-
|
|
4521
|
-
|
|
4522
|
-
|
|
4726
|
+
const j3 = nr * cols + nc;
|
|
4727
|
+
if (gone[j3]) continue;
|
|
4728
|
+
const at = seen.indexOf(piece[j3]);
|
|
4729
|
+
if (at < 0) {
|
|
4730
|
+
seen.push(piece[j3]);
|
|
4731
|
+
votes.push(1);
|
|
4732
|
+
} else {
|
|
4733
|
+
votes[at]++;
|
|
4523
4734
|
}
|
|
4524
4735
|
}
|
|
4525
4736
|
}
|
|
4526
|
-
if (
|
|
4737
|
+
if (seen.length === 0) continue;
|
|
4738
|
+
let best = 0;
|
|
4739
|
+
for (let k2 = 1; k2 < seen.length; k2++) {
|
|
4740
|
+
if (votes[k2] > votes[best] || votes[k2] === votes[best] && seen[k2] < seen[best]) best = k2;
|
|
4741
|
+
}
|
|
4742
|
+
piece[i] = seen[best];
|
|
4743
|
+
followers.push(i);
|
|
4527
4744
|
}
|
|
4528
4745
|
this.followers = followers;
|
|
4529
4746
|
}
|
|
4747
|
+
/**
|
|
4748
|
+
* A rewound history: the sheet starts over.
|
|
4749
|
+
*
|
|
4750
|
+
* Paper that had fallen away is paper again, with every spring back to the
|
|
4751
|
+
* sheet it was cut from — solved from where it lies, the solve would haul it
|
|
4752
|
+
* up off the floor through the sheet in a tangle. And the curl it rolled
|
|
4753
|
+
* into belonged to a front that has not passed yet.
|
|
4754
|
+
*/
|
|
4755
|
+
rewind() {
|
|
4756
|
+
this.sim.restore();
|
|
4757
|
+
this.acrossFront.fill(0);
|
|
4758
|
+
this.torn.fill(0);
|
|
4759
|
+
this.mend = true;
|
|
4760
|
+
}
|
|
4530
4761
|
reset() {
|
|
4531
4762
|
const { sim } = this;
|
|
4763
|
+
if (this.holed) this.rewind();
|
|
4764
|
+
sim.draught.fill(0);
|
|
4532
4765
|
sim.invMass.fill(1);
|
|
4533
4766
|
sim.restBend.fill(0);
|
|
4534
4767
|
sim.broken.fill(0);
|
|
4535
4768
|
sim.restLength.set(sim.naturalLength);
|
|
4536
4769
|
this.gone.fill(0);
|
|
4537
|
-
this.
|
|
4770
|
+
this.acrossFront.fill(0);
|
|
4538
4771
|
this.followers = [];
|
|
4772
|
+
this.holed = false;
|
|
4539
4773
|
this.version = -1;
|
|
4540
4774
|
sim.wake();
|
|
4541
4775
|
}
|
|
@@ -5907,6 +6141,82 @@ function p({
|
|
|
5907
6141
|
}
|
|
5908
6142
|
var j2 = r.forwardRef(p);
|
|
5909
6143
|
|
|
6144
|
+
// src/surface/cutDistance.ts
|
|
6145
|
+
var CUT_REACH_MM = 16;
|
|
6146
|
+
var SPECK_TEXELS = 3;
|
|
6147
|
+
var GONE_BELOW2 = 128;
|
|
6148
|
+
var scratch = null;
|
|
6149
|
+
function cutDistance(pixels, size, texelMm, out) {
|
|
6150
|
+
const n = size * size;
|
|
6151
|
+
if (!scratch || scratch.size !== size) {
|
|
6152
|
+
scratch = { size, dist: new Float32Array(n), kept: new Uint8Array(n), stack: new Int32Array(n) };
|
|
6153
|
+
}
|
|
6154
|
+
const { dist, kept, stack } = scratch;
|
|
6155
|
+
const gone = (i) => pixels[i * 4 + 3] < GONE_BELOW2;
|
|
6156
|
+
kept.fill(0);
|
|
6157
|
+
const seen = dist;
|
|
6158
|
+
seen.fill(0);
|
|
6159
|
+
for (let start = 0; start < n; start++) {
|
|
6160
|
+
if (seen[start] || !gone(start)) continue;
|
|
6161
|
+
let top = 0;
|
|
6162
|
+
let count = 0;
|
|
6163
|
+
stack[top++] = start;
|
|
6164
|
+
seen[start] = 1;
|
|
6165
|
+
const first = top;
|
|
6166
|
+
let head = 0;
|
|
6167
|
+
while (head < top) {
|
|
6168
|
+
const i = stack[head++];
|
|
6169
|
+
count++;
|
|
6170
|
+
const x2 = i % size;
|
|
6171
|
+
const y2 = i / size | 0;
|
|
6172
|
+
const next = [
|
|
6173
|
+
x2 > 0 ? i - 1 : -1,
|
|
6174
|
+
x2 < size - 1 ? i + 1 : -1,
|
|
6175
|
+
y2 > 0 ? i - size : -1,
|
|
6176
|
+
y2 < size - 1 ? i + size : -1
|
|
6177
|
+
];
|
|
6178
|
+
for (const j3 of next) {
|
|
6179
|
+
if (j3 < 0 || seen[j3] || !gone(j3)) continue;
|
|
6180
|
+
seen[j3] = 1;
|
|
6181
|
+
stack[top++] = j3;
|
|
6182
|
+
}
|
|
6183
|
+
}
|
|
6184
|
+
if (count > SPECK_TEXELS) for (let k2 = first - 1; k2 < top; k2++) kept[stack[k2]] = 1;
|
|
6185
|
+
}
|
|
6186
|
+
const [dx, dy] = texelMm;
|
|
6187
|
+
const dd = Math.hypot(dx, dy);
|
|
6188
|
+
for (let i = 0; i < n; i++) dist[i] = kept[i] ? 0 : Number.POSITIVE_INFINITY;
|
|
6189
|
+
for (let y2 = 0; y2 < size; y2++) {
|
|
6190
|
+
for (let x2 = 0; x2 < size; x2++) {
|
|
6191
|
+
const i = y2 * size + x2;
|
|
6192
|
+
let v2 = dist[i];
|
|
6193
|
+
if (v2 === 0) continue;
|
|
6194
|
+
if (x2 > 0) v2 = Math.min(v2, dist[i - 1] + dx);
|
|
6195
|
+
if (y2 > 0) {
|
|
6196
|
+
v2 = Math.min(v2, dist[i - size] + dy);
|
|
6197
|
+
if (x2 > 0) v2 = Math.min(v2, dist[i - size - 1] + dd);
|
|
6198
|
+
if (x2 < size - 1) v2 = Math.min(v2, dist[i - size + 1] + dd);
|
|
6199
|
+
}
|
|
6200
|
+
dist[i] = v2;
|
|
6201
|
+
}
|
|
6202
|
+
}
|
|
6203
|
+
for (let y2 = size - 1; y2 >= 0; y2--) {
|
|
6204
|
+
for (let x2 = size - 1; x2 >= 0; x2--) {
|
|
6205
|
+
const i = y2 * size + x2;
|
|
6206
|
+
let v2 = dist[i];
|
|
6207
|
+
if (v2 === 0) continue;
|
|
6208
|
+
if (x2 < size - 1) v2 = Math.min(v2, dist[i + 1] + dx);
|
|
6209
|
+
if (y2 < size - 1) {
|
|
6210
|
+
v2 = Math.min(v2, dist[i + size] + dy);
|
|
6211
|
+
if (x2 < size - 1) v2 = Math.min(v2, dist[i + size + 1] + dd);
|
|
6212
|
+
if (x2 > 0) v2 = Math.min(v2, dist[i + size - 1] + dd);
|
|
6213
|
+
}
|
|
6214
|
+
dist[i] = v2;
|
|
6215
|
+
}
|
|
6216
|
+
}
|
|
6217
|
+
for (let i = 0; i < n; i++) out[i] = Math.round(Math.min(dist[i], CUT_REACH_MM) / CUT_REACH_MM * 255);
|
|
6218
|
+
}
|
|
6219
|
+
|
|
5910
6220
|
// src/surface/compose.ts
|
|
5911
6221
|
var THREE5 = __toESM(require("three"), 1);
|
|
5912
6222
|
|
|
@@ -6536,7 +6846,7 @@ var DAMAGE_CHUNK = (
|
|
|
6536
6846
|
`
|
|
6537
6847
|
uniform sampler2D uDamage;
|
|
6538
6848
|
uniform float uDamageDetail;
|
|
6539
|
-
// (edgeWave mm, edgeBite mm, sparkle,
|
|
6849
|
+
// (edgeWave mm, edgeBite mm, sparkle, charWidth mm) \u2014 see DamageLook.
|
|
6540
6850
|
uniform vec4 uLook3;
|
|
6541
6851
|
|
|
6542
6852
|
// The damage grid's texels sit ON the sheet \u2014 texel x at u = x / (N - 1), its
|
|
@@ -6673,6 +6983,8 @@ var DAMAGE_SHADE_CHUNK = (
|
|
|
6673
6983
|
/* glsl */
|
|
6674
6984
|
`
|
|
6675
6985
|
uniform float uDamageTime;
|
|
6986
|
+
// How far each texel is from the cut, as a fraction of PL_CUT_REACH \u2014 see cutDistance.
|
|
6987
|
+
uniform sampler2D uDamageEdge;
|
|
6676
6988
|
// The look, packed \u2014 see DamageLook for what each number means.
|
|
6677
6989
|
uniform vec4 uLook0; // emberWidth mm, emberIntensity, emberCoverage, emberFlicker
|
|
6678
6990
|
uniform vec4 uLook1; // emberGlow, lipWidth mm, lipBrightness, charWarmth
|
|
@@ -6682,6 +6994,10 @@ vec3 plDamageLight;
|
|
|
6682
6994
|
|
|
6683
6995
|
// World units per millimetre: a default sheet is one unit across, and A4 is 210 mm.
|
|
6684
6996
|
const float PL_MM = 1.0 / 210.0;
|
|
6997
|
+
// How far out a burnt edge's zones can be measured from the cut, mm: past
|
|
6998
|
+
// the widest lip, ember line, char and scorch the look allows by default.
|
|
6999
|
+
// The same number cutDistance encodes with, so the two cannot drift.
|
|
7000
|
+
const float PL_CUT_REACH = ${CUT_REACH_MM.toFixed(1)};
|
|
6685
7001
|
|
|
6686
7002
|
vec3 plLinear(vec3 srgb) {
|
|
6687
7003
|
return pow(srgb, vec3(2.2));
|
|
@@ -6738,7 +7054,12 @@ vec3 plScorchTint(float t) {
|
|
|
6738
7054
|
vec3 c3 = plLinear(vec3(0.541, 0.310, 0.141)); // #8A4F24
|
|
6739
7055
|
vec3 c4 = plLinear(vec3(0.471, 0.275, 0.149)); // #784626
|
|
6740
7056
|
vec3 c5 = plLinear(vec3(0.380, 0.216, 0.133)); // #613722
|
|
6741
|
-
|
|
7057
|
+
// The dark end hands over to the char's black: a nearly neutral umber, not
|
|
7058
|
+
// the saturated brown (#4F3323) it used to stop at. Beside black char that
|
|
7059
|
+
// brown showed through wherever the band thinned, and turned the black
|
|
7060
|
+
// brown \u2014 measured, the cold char's saturation went from 0.43 to 0.11 with
|
|
7061
|
+
// the scorch's darkening switched off.
|
|
7062
|
+
vec3 c6 = plLinear(vec3(0.169, 0.133, 0.110)); // #2B221C
|
|
6742
7063
|
float s = clamp(t, 0.0, 1.0) * 6.0;
|
|
6743
7064
|
// Each stop is blended with a SMOOTH step, not a linear one.
|
|
6744
7065
|
//
|
|
@@ -6789,32 +7110,6 @@ void plDamage(inout vec4 color, inout float roughness) {
|
|
|
6789
7110
|
vec2 perWorld = (vec2(textureSize(uDamage, 0)) - 1.0) / uSheetSize;
|
|
6790
7111
|
vec2 gradP = vec2(e.a - w.a, n.a - s.a) * 0.5 * perWorld;
|
|
6791
7112
|
vec2 gradC = vec2(e.r - w.r, n.r - s.r) * 0.5 * perWorld;
|
|
6792
|
-
// Millimetres from the cut, on the paper side \u2014 measured against the field
|
|
6793
|
-
// the cut is DRAWN from (softened, frayed, warped), through its own screen
|
|
6794
|
-
// derivatives. It used to divide by the slope of the raw texels, and since
|
|
6795
|
-
// paper burns through within a texel that slope is a cliff while the drawn
|
|
6796
|
-
// edge is spread over millimetres: a point well into the char computed as a
|
|
6797
|
-
// hair from the cut, and the ember line drew there \u2014 found by the gate.
|
|
6798
|
-
// The slope, though, is the SMOOTH presence's. The fray's fibre noise
|
|
6799
|
-
// changes pixel to pixel, and its slope is a cliff everywhere it touches:
|
|
6800
|
-
// divided by that, a point deep in the char came out a hair from the cut
|
|
6801
|
-
// and the ember glow drew there \u2014 the gate's stray pixels. The fray still
|
|
6802
|
-
// moves where the edge is; it just cannot fake how steep it is.
|
|
6803
|
-
float smoothA = plDamageSmooth;
|
|
6804
|
-
vec2 cutSlope = vec2(dFdx(smoothA), dFdy(smoothA));
|
|
6805
|
-
float pxFromCut = (d.a - 0.5) / max(length(cutSlope), 1e-4);
|
|
6806
|
-
// Millimetres per pixel ALONG the way to the cut, not across the screen's
|
|
6807
|
-
// x: a sheet leaning away is foreshortened vertically, so above and below a
|
|
6808
|
-
// hole a pixel covers more paper than beside it, and a scale taken from x
|
|
6809
|
-
// alone drew the ember zone too many pixels deep on the top and bottom rims.
|
|
6810
|
-
vec2 toCutPx = cutSlope / max(length(cutSlope), 1e-6);
|
|
6811
|
-
float mmPerPx = length(dFdx(-vViewPosition) * toCutPx.x + dFdy(-vViewPosition) * toCutPx.y) / PL_MM;
|
|
6812
|
-
float mm = pxFromCut * mmPerPx;
|
|
6813
|
-
// About a pixel of antialiasing, never more: where the warp makes the
|
|
6814
|
-
// estimate jump between neighbours, fwidth(mm) is millimetres wide, and the
|
|
6815
|
-
// ember's soft tail \u2014 faint, but multiplied by its light \u2014 drew stray glow
|
|
6816
|
-
// well out into the char.
|
|
6817
|
-
float aa = min(fwidth(mm), 1.5 * mmPerPx);
|
|
6818
7113
|
// Scorch reaches ahead of the char, and further UP than anywhere else:
|
|
6819
7114
|
// the char a centimetre below this point, and the char a few millimetres
|
|
6820
7115
|
// around it, each lent to the scorch at a discount (\xA74.2, \xA74.5).
|
|
@@ -6828,38 +7123,61 @@ void plDamage(inout vec4 color, inout float roughness) {
|
|
|
6828
7123
|
charWide += texture2D(uDamage, uv + vec2(cos(a), sin(a)) * 7.0 * mmUv).r;
|
|
6829
7124
|
}
|
|
6830
7125
|
charWide /= 8.0;
|
|
6831
|
-
//
|
|
6832
|
-
//
|
|
6833
|
-
//
|
|
6834
|
-
//
|
|
6835
|
-
//
|
|
6836
|
-
//
|
|
6837
|
-
|
|
6838
|
-
//
|
|
6839
|
-
//
|
|
6840
|
-
//
|
|
6841
|
-
//
|
|
6842
|
-
//
|
|
6843
|
-
//
|
|
6844
|
-
//
|
|
6845
|
-
//
|
|
6846
|
-
vec2
|
|
6847
|
-
|
|
6848
|
-
float
|
|
6849
|
-
|
|
6850
|
-
|
|
6851
|
-
//
|
|
6852
|
-
float
|
|
6853
|
-
|
|
7126
|
+
// \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
|
|
7127
|
+
// How far this point is from the cut, in millimetres, out to PL_CUT_REACH \u2014
|
|
7128
|
+
// the one measure every zone of a burnt edge is laid out on: the ash lip,
|
|
7129
|
+
// the ember line, the char and the scorch (Ember_line annotated). Worked
|
|
7130
|
+
// out on the CPU once per change of the field (cutDistance), where a speck
|
|
7131
|
+
// of burnt-through paper can be told from a cut: measured here per pixel,
|
|
7132
|
+
// every speck near the rim grew its own lip, char and scorch, and the bands
|
|
7133
|
+
// came apart into blotches.
|
|
7134
|
+
//
|
|
7135
|
+
// Burnt AWAY, not charred: the black and the lip belong to paper beside a
|
|
7136
|
+
// cut, so a burn toasts before it blackens.
|
|
7137
|
+
//
|
|
7138
|
+
// Read through the same warp as the cut, so the zones follow the edge that
|
|
7139
|
+
// is drawn. Bilinear between a gone texel and the paper beside it, the
|
|
7140
|
+
// drawn edge reads half a texel, which comes back off.
|
|
7141
|
+
vec2 edgeTexel = 1.0 / vec2(textureSize(uDamageEdge, 0));
|
|
7142
|
+
vec2 edgeWorld = uSheetSize / (vec2(textureSize(uDamageEdge, 0)) - 1.0);
|
|
7143
|
+
float edgeRaw = texture2D(uDamageEdge, uv).r;
|
|
7144
|
+
float edgeHalf = 0.5 * min(edgeWorld.x, edgeWorld.y) / PL_MM;
|
|
7145
|
+
float xCut = max(edgeRaw * PL_CUT_REACH - edgeHalf, 0.0);
|
|
7146
|
+
// A pixel or so of antialiasing, measured on the same measure.
|
|
7147
|
+
float aa = clamp(fwidth(xCut), 0.05, 1.0);
|
|
7148
|
+
// Which way the cut lies, from how the distance falls \u2014 and along the edge,
|
|
7149
|
+
// across that. In the sheet's own space, so noise laid along it follows the
|
|
7150
|
+
// rim wherever the rim goes.
|
|
7151
|
+
vec2 toward = -vec2(
|
|
7152
|
+
(texture2D(uDamageEdge, uv + vec2(edgeTexel.x, 0.0)).r - texture2D(uDamageEdge, uv - vec2(edgeTexel.x, 0.0)).r) / edgeWorld.x,
|
|
7153
|
+
(texture2D(uDamageEdge, uv + vec2(0.0, edgeTexel.y)).r - texture2D(uDamageEdge, uv - vec2(0.0, edgeTexel.y)).r) / edgeWorld.y
|
|
7154
|
+
);
|
|
7155
|
+
vec2 tangentR = length(toward) > 1e-4 ? normalize(vec2(-toward.y, toward.x)) : vec2(1.0, 0.0);
|
|
7156
|
+
// Paper with a cut within reach, eased out toward the reach.
|
|
7157
|
+
float cutZone = 1.0 - smoothstep(PL_CUT_REACH - 2.0, PL_CUT_REACH - 0.5, xCut);
|
|
6854
7158
|
|
|
6855
7159
|
// An untouched texel is left exactly as it was \u2014 the identity the whole
|
|
6856
7160
|
// seam is built on. Nothing here has happened to this paper.
|
|
6857
|
-
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) {
|
|
7161
|
+
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) {
|
|
6858
7162
|
return;
|
|
6859
7163
|
}
|
|
6860
7164
|
|
|
6861
7165
|
vec2 p = plLocal();
|
|
6862
7166
|
|
|
7167
|
+
// The zones, out from the cut: the ash lip, the ember line on its outer
|
|
7168
|
+
// edge, the char, then the scorch \u2014 each as wide as the look says, and
|
|
7169
|
+
// uneven along the rim so none of them is a ribbon. The lip's unevenness
|
|
7170
|
+
// stays inside the zone the ember gate allows it (lip plus bead).
|
|
7171
|
+
float lipEdge = uLook1.y * mix(0.7, 1.1, plFbm(vec2(dot(p, tangentR) / (5.0 * PL_MM), 1.7)));
|
|
7172
|
+
float charEnd = lipEdge + uLook0.x + uLook3.w * mix(0.8, 1.2, plFbm(vec2(dot(p, tangentR) / (7.0 * PL_MM), 4.3)));
|
|
7173
|
+
// Further above the burn than below it \u2014 hot gas rises and cooks the paper
|
|
7174
|
+
// over it (\xA74.5) \u2014 out to the scorch's full reach straight up, and about
|
|
7175
|
+
// half of it below and beside.
|
|
7176
|
+
vec2 upMm = normalize(upUv * uSheetSize);
|
|
7177
|
+
float above = length(toward) > 1e-3 ? max(0.0, dot(-normalize(toward), upMm)) : 0.0;
|
|
7178
|
+
float scorchEnd = charEnd + uLook2.y * mix(0.45, 1.0, above)
|
|
7179
|
+
+ (plFbm(vec2(dot(p, tangentR) / (4.0 * PL_MM), 3.1)) - 0.5) * uLook2.w * 2.5;
|
|
7180
|
+
|
|
6863
7181
|
// Wet paper is darker and smoother: water fills the gaps between fibres
|
|
6864
7182
|
// that scatter light, which is both effects from one cause.
|
|
6865
7183
|
color.rgb *= 1.0 - 0.38 * d.g;
|
|
@@ -6911,27 +7229,64 @@ void plDamage(inout vec4 color, inout float roughness) {
|
|
|
6911
7229
|
// being finer than a level it can only ever move a value into the
|
|
6912
7230
|
// neighbouring one.
|
|
6913
7231
|
c = clamp(c + (plNoise(p * 900.0) - 0.5) * (1.5 / 255.0), 0.0, 1.0);
|
|
7232
|
+
// The colour INSIDE the scorch reads a smoothed c: the same char and the
|
|
7233
|
+
// same reaches, without the fingers. The fingers belong to the front's
|
|
7234
|
+
// silhouette (lead, below); carried into the ramp as well, a strong fingers
|
|
7235
|
+
// setting swung the scorch between tan and near-black umber a few
|
|
7236
|
+
// millimetres apart \u2014 a leopard print, not a toast.
|
|
7237
|
+
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));
|
|
7238
|
+
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);
|
|
6914
7239
|
// A steep leading edge \u2014 paper to straw in a sliver \u2014 then the long ramp.
|
|
6915
|
-
|
|
6916
|
-
|
|
6917
|
-
|
|
7240
|
+
// The front's teeth from the noisy c; filled in behind them from the smooth
|
|
7241
|
+
// one, so a dip in the noise cannot open a patch of clean paper inside it.
|
|
7242
|
+
float lead = max(smoothstep(0.02, 0.05, c), smoothstep(0.1, 0.2, cSmooth));
|
|
7243
|
+
// The browns are done by 0.45, where the char's umber hand-over takes them:
|
|
7244
|
+
// the ramp used to run on to 0.7, over the char, and most of what read as
|
|
7245
|
+
// "char" was this ramp's darkest brown.
|
|
7246
|
+
float ramp = smoothstep(0.05, 0.45, cSmooth);
|
|
7247
|
+
// That toast is for paper with no cut near it \u2014 the first stage of a burn,
|
|
7248
|
+
// before anything has gone through.
|
|
7249
|
+
color.rgb *= mix(vec3(1.0), pow(plScorchTint(ramp), vec3(uLook2.z)), lead * (1.0 - cutZone));
|
|
7250
|
+
// Round a cut, the scorch is a band of its own: dark, only a little lighter
|
|
7251
|
+
// than the char it leads from, a shade warmer toward its outer edge, then a
|
|
7252
|
+
// steep leading edge into clean paper (Ember_line annotated, zone 5: "steep
|
|
7253
|
+
// leading edge \xB7 albedo only"). A black band beside a pale tan one was the
|
|
7254
|
+
// contrast that read as wrong.
|
|
7255
|
+
float scorchT = smoothstep(charEnd, scorchEnd, xCut);
|
|
7256
|
+
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));
|
|
7257
|
+
float scorchZone = (1.0 - smoothstep(scorchEnd - 0.35, scorchEnd + 0.35, xCut)) * cutZone;
|
|
7258
|
+
color.rgb *= mix(vec3(1.0), pow(min(scorchTint, vec3(1.0)), vec3(uLook2.z)), scorchZone);
|
|
6918
7259
|
|
|
6919
7260
|
// \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
|
|
6920
7261
|
// A wide, smooth hand-over from scorch to char \u2014 the gradient is the point.
|
|
6921
|
-
|
|
7262
|
+
// From 0.3: the field carries char in about one texel beside the cut, so
|
|
7263
|
+
// past a couple of millimetres c is mostly the scorch borrowed from below,
|
|
7264
|
+
// and a zone that began at 0.5 was hardly ever char at all.
|
|
7265
|
+
// Away from a cut, c only makes char where it is really high: at 0.3\u20130.6
|
|
7266
|
+
// the finger noise pushed it across the line all through the scorch, and
|
|
7267
|
+
// every crossing was a black island in the orange \u2014 a leopard print.
|
|
7268
|
+
// Round a cut, the band from the lip out to charEnd; away from one, only
|
|
7269
|
+
// where the toast has gone all the way to char.
|
|
7270
|
+
float charBand = (1.0 - smoothstep(charEnd - 0.5, charEnd + 0.5, xCut)) * cutZone;
|
|
7271
|
+
float charZone = max(smoothstep(0.6, 0.85, cSmooth) * (1.0 - cutZone), charBand);
|
|
6922
7272
|
vec2 cells = plVoronoi(p / (3.6 * PL_MM));
|
|
6923
7273
|
// Sparse: a real crack network is broken, not a tiled floor \u2014 most cell
|
|
6924
7274
|
// borders never split.
|
|
6925
7275
|
float crack = (1.0 - smoothstep(0.008, 0.03, cells.y - cells.x)) * smoothstep(0.45, 0.65, plNoise(p / (7.0 * PL_MM)));
|
|
6926
7276
|
float plate = plHash(floor(p / (3.6 * PL_MM)) + 3.7);
|
|
6927
|
-
//
|
|
6928
|
-
//
|
|
6929
|
-
//
|
|
6930
|
-
//
|
|
6931
|
-
|
|
6932
|
-
|
|
7277
|
+
// Black. Real char is a near-neutral black \u2014 (22, 20, 18) on the cold
|
|
7278
|
+
// macro references, about 11% of the paper's value. The warm brown this
|
|
7279
|
+
// used to be was Hero.png's char LIT BY ITS FLAMES, painted into the paper,
|
|
7280
|
+
// which is why ours stayed orange on a frame with no fire in it. Warmth on
|
|
7281
|
+
// char comes from the fire's light now, and goes when the fire does.
|
|
7282
|
+
// charWarmth adds only a trace of brown to the black, and a warmer umber
|
|
7283
|
+
// where the char hands over to the scorch.
|
|
7284
|
+
vec3 body = mix(plLinear(vec3(0.086, 0.078, 0.071)), plLinear(vec3(0.11, 0.08, 0.06)), uLook1.w);
|
|
7285
|
+
vec3 under = mix(plLinear(vec3(0.20, 0.15, 0.11)), plLinear(vec3(0.28, 0.16, 0.08)), uLook1.w);
|
|
6933
7286
|
vec3 cracks = plLinear(vec3(0.039, 0.024, 0.016)); // #0A0604
|
|
6934
|
-
|
|
7287
|
+
// Black through the band; the umber only in its outer millimetre.
|
|
7288
|
+
float toUnder = mix(1.0 - smoothstep(0.45, 0.8, cSmooth), smoothstep(charEnd - 1.2, charEnd + 0.3, xCut), cutZone);
|
|
7289
|
+
vec3 charColor = mix(body, under, toUnder);
|
|
6935
7290
|
// Mottled at the plate scale and finer, and darker than the sampled body
|
|
6936
7291
|
// under a bright key \u2014 the reference's char reads near black in the frame.
|
|
6937
7292
|
charColor *= (0.75 + 0.35 * plate) * (0.8 + 0.4 * plNoise(p * 260.0));
|
|
@@ -6943,42 +7298,42 @@ void plDamage(inout vec4 color, inout float roughness) {
|
|
|
6943
7298
|
plHeight += charZone * ((plNoise(p * 380.0) - 0.5) * 0.00008 + (plate - 0.5) * 0.00018 - crack * 0.00016);
|
|
6944
7299
|
|
|
6945
7300
|
// \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
|
|
6946
|
-
//
|
|
6947
|
-
//
|
|
6948
|
-
//
|
|
6949
|
-
//
|
|
6950
|
-
//
|
|
6951
|
-
//
|
|
6952
|
-
//
|
|
6953
|
-
//
|
|
6954
|
-
|
|
6955
|
-
|
|
6956
|
-
float
|
|
6957
|
-
//
|
|
6958
|
-
//
|
|
6959
|
-
|
|
6960
|
-
// middle of it, which read as a neon tube round the hole \u2014 the single most
|
|
6961
|
-
// synthetic thing at wide view.
|
|
6962
|
-
float lipAlong = dot(p, tangentP) / (3.2 * PL_MM);
|
|
6963
|
-
float lipBreak = smoothstep(0.34, 0.56, plFbm(vec2(lipAlong, 0.9)));
|
|
6964
|
-
float lip = (1.0 - smoothstep(lipWidth - aa, lipWidth + aa, mm)) * step(d.a, 0.999) * cutNear * lipBreak;
|
|
6965
|
-
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;
|
|
7301
|
+
// A crust as wide as the char band, from the cut to lipEdge \u2014 the pale
|
|
7302
|
+
// thing that makes a hole read as a hole on a black stage (Ember_line
|
|
7303
|
+
// annotated, zone 2). It used to be held to about a millimetre, which at a
|
|
7304
|
+
// reading distance is a hairline tracing the edge, not ash.
|
|
7305
|
+
//
|
|
7306
|
+
// Broken, not a hem: ash flakes off a cooling edge in pieces, so the lip
|
|
7307
|
+
// comes and goes along the rim and wanders in width. Mostly there, though \u2014
|
|
7308
|
+
// it has to be seen.
|
|
7309
|
+
float lipAlong = dot(p, tangentR) / (3.2 * PL_MM);
|
|
7310
|
+
float lipBreak = smoothstep(0.1, 0.3, plFbm(vec2(lipAlong, 0.9)));
|
|
7311
|
+
float lip = (1.0 - smoothstep(lipEdge - aa, lipEdge + aa, xCut)) * cutZone * lipBreak;
|
|
7312
|
+
// Pale: the reference's #A49E9D up to a near-white grey, so it reads white
|
|
7313
|
+
// beside the char. Still matte and still dimmer than the paper.
|
|
7314
|
+
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;
|
|
6966
7315
|
color.rgb = mix(color.rgb, ash, lip);
|
|
6967
7316
|
roughness = mix(roughness, 0.99, lip);
|
|
6968
|
-
//
|
|
6969
|
-
//
|
|
6970
|
-
//
|
|
6971
|
-
//
|
|
6972
|
-
// of
|
|
6973
|
-
//
|
|
6974
|
-
|
|
7317
|
+
// And the crust the ragged edge is made of. The cut is frayed at fibre
|
|
7318
|
+
// scale, and the fray opened specks of void all through the first few
|
|
7319
|
+
// millimetres \u2014 exactly where the lip lies \u2014 so at a reading distance the
|
|
7320
|
+
// lip was grey specks in black. Paper inside the fray's band is kept, as
|
|
7321
|
+
// ash, instead of cut away: the lip overhangs the hole a little, broken
|
|
7322
|
+
// where the lip is broken, the way the reference's crust does.
|
|
7323
|
+
float crust = lipBreak * cutZone * smoothstep(0.18, 0.3, d.a) * (1.0 - step(0.5, d.a));
|
|
7324
|
+
color.rgb = mix(color.rgb, ash, crust);
|
|
7325
|
+
roughness = mix(roughness, 0.99, crust);
|
|
7326
|
+
// Raised, and crumbly rather than rounded: a smooth ridge round the hole is
|
|
7327
|
+
// a cylinder, and a cylinder under a key light has a specular line down it
|
|
7328
|
+
// however rough it is \u2014 the neon tube this once was.
|
|
7329
|
+
plHeight += lip * (0.55 + 0.45 * plNoise(p * 700.0)) * 0.00016;
|
|
6975
7330
|
|
|
6976
7331
|
// \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
|
|
6977
7332
|
// Beads along the cut: position along the edge, crawling on the burn's
|
|
6978
7333
|
// clock, with a flicker of their own. Only where it is hot \u2014 and as the
|
|
6979
7334
|
// heat goes the threshold rises, so the line breaks into fewer, dimmer
|
|
6980
7335
|
// beads and they go out one by one.
|
|
6981
|
-
float s1 = dot(p,
|
|
7336
|
+
float s1 = dot(p, tangentR) / (2.4 * PL_MM) - uDamageTime * 0.55 * uLook0.w;
|
|
6982
7337
|
float bead = plNoise(vec2(s1, 0.37)) * 0.7 + plNoise(vec2(s1 * 2.3, 5.1)) * 0.3;
|
|
6983
7338
|
float flicker = 0.62 + 0.38 * plNoise(vec2(floor(s1) * 7.13, uDamageTime * 9.0 * uLook0.w));
|
|
6984
7339
|
// Down to a smoulder's last flicker: a spent edge keeps a little heat in
|
|
@@ -6993,7 +7348,8 @@ void plDamage(inout vec4 color, inout float roughness) {
|
|
|
6993
7348
|
float lit = smoothstep(mix(0.82, 0.45, hot) - shift, mix(0.9, 0.58, hot) - shift, bead);
|
|
6994
7349
|
// Each bead tapers at its ends \u2014 a bead, not a dash cut from a strip.
|
|
6995
7350
|
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);
|
|
6996
|
-
|
|
7351
|
+
// On the lip's outer edge, between the ash and the char.
|
|
7352
|
+
float band = smoothstep(lipEdge - aa, lipEdge + aa, xCut) * (1.0 - smoothstep(lipEdge + width - aa, lipEdge + width + aa, xCut)) * cutZone;
|
|
6997
7353
|
// Mostly orange: a bead is red at its ends and yellow-white only at its
|
|
6998
7354
|
// hottest core, which is what Ember_line.png shows. Mapped low and pushed
|
|
6999
7355
|
// high by a power, so white is the exception rather than the line.
|
|
@@ -7013,13 +7369,15 @@ void plDamage(inout vec4 color, inout float roughness) {
|
|
|
7013
7369
|
// edge is hot, and inside the zone the beads are held to \u2014 the lip plus
|
|
7014
7370
|
// the widest bead, measured from the cut \u2014 so the glow never reaches out
|
|
7015
7371
|
// over paper the gate says heat must not light.
|
|
7016
|
-
float zoneEnd =
|
|
7017
|
-
float seam = smoothstep(
|
|
7372
|
+
float zoneEnd = lipEdge + uLook0.x;
|
|
7373
|
+
float seam = smoothstep(lipEdge - aa, lipEdge + aa, xCut) * (1.0 - smoothstep(zoneEnd - 0.6, zoneEnd, xCut)) * cutZone;
|
|
7018
7374
|
float patch_ = smoothstep(0.35, 0.72, plFbm(p / (4.0 * PL_MM) + vec2(uDamageTime * 0.15 * uLook0.w, 0.0)));
|
|
7019
7375
|
plDamageLight += vec3(0.55, 0.06, 0.01) * seam * patch_ * hot * uLook1.x;
|
|
7020
7376
|
float speck = step(0.985, plHash(floor(p / (0.35 * PL_MM)) + floor(uDamageTime * 8.0 * uLook0.w)));
|
|
7021
7377
|
plDamageLight += plEmberRamp(0.9) * 1.6 * speck * seam * hot * uLook3.z;
|
|
7022
7378
|
|
|
7379
|
+
// The crust stays, however the fray cut it.
|
|
7380
|
+
d.a = mix(d.a, max(d.a, 0.5), step(0.5, crust));
|
|
7023
7381
|
plDamageCut(color, d);
|
|
7024
7382
|
}
|
|
7025
7383
|
`
|
|
@@ -7130,6 +7488,7 @@ function composeSurface(surface, stock, thickness, maps = { hasFrontMap: false,
|
|
|
7130
7488
|
chunks.push(DAMAGE_CHUNK, DAMAGE_SHADE_CHUNK);
|
|
7131
7489
|
calls.push("plDamage(csm_DiffuseColor, csm_Roughness);");
|
|
7132
7490
|
uniforms.uDamage = { value: null };
|
|
7491
|
+
uniforms.uDamageEdge = { value: null };
|
|
7133
7492
|
uniforms.uDamageDetail = { value: 1 };
|
|
7134
7493
|
uniforms.uDamageTime = { value: 0 };
|
|
7135
7494
|
const look = DAMAGE_LOOK_DEFAULTS;
|
|
@@ -7142,7 +7501,7 @@ function composeSurface(surface, stock, thickness, maps = { hasFrontMap: false,
|
|
|
7142
7501
|
uniforms.uLook2 = {
|
|
7143
7502
|
value: new THREE5.Vector4(look.charCracks, look.scorchReach, look.scorchDarkness, look.fingers)
|
|
7144
7503
|
};
|
|
7145
|
-
uniforms.uLook3 = { value: new THREE5.Vector4(look.edgeWave, look.edgeBite, look.sparkle,
|
|
7504
|
+
uniforms.uLook3 = { value: new THREE5.Vector4(look.edgeWave, look.edgeBite, look.sparkle, look.charWidth) };
|
|
7146
7505
|
}
|
|
7147
7506
|
const cutting = [];
|
|
7148
7507
|
const cuts = [];
|
|
@@ -7246,35 +7605,65 @@ function useLightRig(own) {
|
|
|
7246
7605
|
var THREE6 = __toESM(require("three"), 1);
|
|
7247
7606
|
var import_fiber = require("@react-three/fiber");
|
|
7248
7607
|
var import_react4 = require("react");
|
|
7249
|
-
|
|
7250
|
-
|
|
7608
|
+
var MM_PER_WORLD = 210;
|
|
7609
|
+
function useDamageTexture(source, sheet2 = { width: 1, height: 1.4 }) {
|
|
7610
|
+
const { width, height } = sheet2;
|
|
7611
|
+
const textures = (0, import_react4.useMemo)(() => {
|
|
7251
7612
|
if (!source) return null;
|
|
7252
|
-
const
|
|
7613
|
+
const damage = new THREE6.DataTexture(
|
|
7253
7614
|
source.pixels,
|
|
7254
7615
|
source.size,
|
|
7255
7616
|
source.size,
|
|
7256
7617
|
THREE6.RGBAFormat,
|
|
7257
7618
|
THREE6.UnsignedByteType
|
|
7258
7619
|
);
|
|
7259
|
-
|
|
7260
|
-
|
|
7261
|
-
|
|
7262
|
-
|
|
7263
|
-
|
|
7264
|
-
|
|
7265
|
-
|
|
7266
|
-
t
|
|
7267
|
-
|
|
7268
|
-
|
|
7269
|
-
|
|
7270
|
-
|
|
7620
|
+
const edge = new THREE6.DataTexture(
|
|
7621
|
+
new Uint8Array(source.size * source.size),
|
|
7622
|
+
source.size,
|
|
7623
|
+
source.size,
|
|
7624
|
+
THREE6.RedFormat,
|
|
7625
|
+
THREE6.UnsignedByteType
|
|
7626
|
+
);
|
|
7627
|
+
for (const t of [damage, edge]) {
|
|
7628
|
+
t.magFilter = THREE6.LinearFilter;
|
|
7629
|
+
t.minFilter = THREE6.LinearFilter;
|
|
7630
|
+
t.wrapS = THREE6.ClampToEdgeWrapping;
|
|
7631
|
+
t.wrapT = THREE6.ClampToEdgeWrapping;
|
|
7632
|
+
t.generateMipmaps = false;
|
|
7633
|
+
t.colorSpace = THREE6.NoColorSpace;
|
|
7634
|
+
t.flipY = false;
|
|
7635
|
+
t.unpackAlignment = 1;
|
|
7636
|
+
}
|
|
7637
|
+
measureEdge(source, edge, width, height);
|
|
7638
|
+
damage.userData.version = source.version;
|
|
7639
|
+
damage.needsUpdate = true;
|
|
7640
|
+
return { damage, edge };
|
|
7641
|
+
}, [source, width, height]);
|
|
7642
|
+
(0, import_react4.useEffect)(
|
|
7643
|
+
() => () => {
|
|
7644
|
+
textures?.damage.dispose();
|
|
7645
|
+
textures?.edge.dispose();
|
|
7646
|
+
},
|
|
7647
|
+
[textures]
|
|
7648
|
+
);
|
|
7271
7649
|
(0, import_fiber.useFrame)(() => {
|
|
7272
|
-
if (!source || !
|
|
7273
|
-
if (
|
|
7274
|
-
|
|
7275
|
-
|
|
7650
|
+
if (!source || !textures) return;
|
|
7651
|
+
if (textures.damage.userData.version === source.version) return;
|
|
7652
|
+
textures.damage.userData.version = source.version;
|
|
7653
|
+
textures.damage.needsUpdate = true;
|
|
7654
|
+
measureEdge(source, textures.edge, width, height);
|
|
7276
7655
|
});
|
|
7277
|
-
return
|
|
7656
|
+
return textures;
|
|
7657
|
+
}
|
|
7658
|
+
function measureEdge(source, edge, width, height) {
|
|
7659
|
+
const last = Math.max(1, source.size - 1);
|
|
7660
|
+
cutDistance(
|
|
7661
|
+
source.pixels,
|
|
7662
|
+
source.size,
|
|
7663
|
+
[width * MM_PER_WORLD / last, height * MM_PER_WORLD / last],
|
|
7664
|
+
edge.image.data
|
|
7665
|
+
);
|
|
7666
|
+
edge.needsUpdate = true;
|
|
7278
7667
|
}
|
|
7279
7668
|
|
|
7280
7669
|
// src/surface/PaperMaterial.tsx
|
|
@@ -7291,7 +7680,7 @@ function PaperMaterial({
|
|
|
7291
7680
|
damage
|
|
7292
7681
|
}) {
|
|
7293
7682
|
const rig = useLightRig(lighting);
|
|
7294
|
-
const
|
|
7683
|
+
const damageTextures = useDamageTexture(damage, sheet2);
|
|
7295
7684
|
const composed = composeSurface(
|
|
7296
7685
|
surface,
|
|
7297
7686
|
stock,
|
|
@@ -7299,7 +7688,7 @@ function PaperMaterial({
|
|
|
7299
7688
|
{
|
|
7300
7689
|
hasFrontMap: Boolean(texture),
|
|
7301
7690
|
hasBackMap: Boolean(backTexture),
|
|
7302
|
-
hasDamage: Boolean(
|
|
7691
|
+
hasDamage: Boolean(damageTextures)
|
|
7303
7692
|
},
|
|
7304
7693
|
sheet2,
|
|
7305
7694
|
rig,
|
|
@@ -7308,7 +7697,7 @@ function PaperMaterial({
|
|
|
7308
7697
|
const bound = (0, import_react5.useMemo)(() => composed.uniforms, [composed.structureKey]);
|
|
7309
7698
|
(0, import_react5.useEffect)(() => {
|
|
7310
7699
|
for (const [key, uniform] of Object.entries(composed.uniforms)) {
|
|
7311
|
-
if (!bound[key] || key === "uFrontMap" || key === "uBackMap" || key === "uDamage" || key === "uDamageDetail" || key === "uDamageTime" || key.startsWith("uLook")) {
|
|
7700
|
+
if (!bound[key] || key === "uFrontMap" || key === "uBackMap" || key === "uDamage" || key === "uDamageEdge" || key === "uDamageDetail" || key === "uDamageTime" || key.startsWith("uLook")) {
|
|
7312
7701
|
continue;
|
|
7313
7702
|
}
|
|
7314
7703
|
if (bound[key].value instanceof THREE7.Color && uniform.value instanceof THREE7.Color) {
|
|
@@ -7322,8 +7711,9 @@ function PaperMaterial({
|
|
|
7322
7711
|
(0, import_react5.useEffect)(() => {
|
|
7323
7712
|
if (bound.uFrontMap) bound.uFrontMap.value = texture;
|
|
7324
7713
|
if (bound.uBackMap) bound.uBackMap.value = backTexture ?? null;
|
|
7325
|
-
if (bound.uDamage) bound.uDamage.value =
|
|
7326
|
-
|
|
7714
|
+
if (bound.uDamage) bound.uDamage.value = damageTextures?.damage ?? null;
|
|
7715
|
+
if (bound.uDamageEdge) bound.uDamageEdge.value = damageTextures?.edge ?? null;
|
|
7716
|
+
}, [bound, texture, backTexture, damageTextures]);
|
|
7327
7717
|
(0, import_fiber2.useFrame)((state) => {
|
|
7328
7718
|
if (!bound.uDamageDetail) return;
|
|
7329
7719
|
if (bound.uDamageTime) {
|
|
@@ -7356,7 +7746,7 @@ function PaperMaterial({
|
|
|
7356
7746
|
v2(look.edgeWave, d.edgeWave),
|
|
7357
7747
|
v2(look.edgeBite, d.edgeBite),
|
|
7358
7748
|
v2(look.sparkle, d.sparkle),
|
|
7359
|
-
|
|
7749
|
+
v2(look.charWidth, d.charWidth)
|
|
7360
7750
|
);
|
|
7361
7751
|
}
|
|
7362
7752
|
const detail = damage?.detail ?? 1;
|
|
@@ -8188,7 +8578,11 @@ var PaperMesh = (0, import_react8.forwardRef)(function PaperMesh2(props, ref) {
|
|
|
8188
8578
|
coupling?.update(props.damage);
|
|
8189
8579
|
sim.step(delta);
|
|
8190
8580
|
const moved = !sim.asleep;
|
|
8191
|
-
if (moved
|
|
8581
|
+
if (moved && coupling) {
|
|
8582
|
+
coupling.follow();
|
|
8583
|
+
const index = geometry.index;
|
|
8584
|
+
if (index && coupling.tear(index.array)) index.needsUpdate = true;
|
|
8585
|
+
}
|
|
8192
8586
|
if (!applyShape(sim.positions, moved) && moved) {
|
|
8193
8587
|
const position = geometry.attributes.position;
|
|
8194
8588
|
position.array.set(sim.positions);
|
|
@@ -8588,7 +8982,8 @@ function PaperLighting({
|
|
|
8588
8982
|
reducedMotion,
|
|
8589
8983
|
shadowMapSize,
|
|
8590
8984
|
contactShadow = true,
|
|
8591
|
-
environment = true
|
|
8985
|
+
environment = true,
|
|
8986
|
+
damage
|
|
8592
8987
|
}) {
|
|
8593
8988
|
const p2 = (0, import_react9.useMemo)(() => rig ?? resolveLighting(preset, light), [rig, preset, light]);
|
|
8594
8989
|
const mapSize = shadowMapSize ?? p2.shadow.mapSize;
|
|
@@ -8622,6 +9017,20 @@ function PaperLighting({
|
|
|
8622
9017
|
driftRef.current += delta * p2.gobo.drift;
|
|
8623
9018
|
goboMap.offset.set(driftRef.current, driftRef.current * 0.6);
|
|
8624
9019
|
});
|
|
9020
|
+
const keyIntensity = p2.gobo && goboMap ? p2.key.intensity * 3.2 : p2.key.intensity;
|
|
9021
|
+
const keyRef = (0, import_react9.useRef)(null);
|
|
9022
|
+
const ambientRef = (0, import_react9.useRef)(null);
|
|
9023
|
+
const hemisphereRef = (0, import_react9.useRef)(null);
|
|
9024
|
+
const roomRef = (0, import_react9.useRef)(1);
|
|
9025
|
+
(0, import_fiber4.useFrame)(() => {
|
|
9026
|
+
const room = roomLight(damage);
|
|
9027
|
+
if (room === 1 && roomRef.current === 1) return;
|
|
9028
|
+
roomRef.current = room;
|
|
9029
|
+
if (keyRef.current) keyRef.current.intensity = keyIntensity * room;
|
|
9030
|
+
if (ambientRef.current) ambientRef.current.intensity = p2.ambient * room;
|
|
9031
|
+
if (hemisphereRef.current) hemisphereRef.current.intensity = p2.studio * HEMISPHERE_STAND_IN * room;
|
|
9032
|
+
if (p2.studio > 0 && environment) scene.environmentIntensity = p2.studio * room;
|
|
9033
|
+
});
|
|
8625
9034
|
return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(import_jsx_runtime6.Fragment, { children: [
|
|
8626
9035
|
p2.studio > 0 && (environment ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(StudioLight, { rig: p2 }) : (
|
|
8627
9036
|
// The studio light degrades rather than disappearing. A hemisphere
|
|
@@ -8632,19 +9041,21 @@ function PaperLighting({
|
|
|
8632
9041
|
/* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
|
|
8633
9042
|
"hemisphereLight",
|
|
8634
9043
|
{
|
|
9044
|
+
ref: hemisphereRef,
|
|
8635
9045
|
color: p2.sky.horizon,
|
|
8636
9046
|
groundColor: p2.sky.ground,
|
|
8637
9047
|
intensity: p2.studio * HEMISPHERE_STAND_IN
|
|
8638
9048
|
}
|
|
8639
9049
|
)
|
|
8640
9050
|
)),
|
|
8641
|
-
/* @__PURE__ */ (0, import_jsx_runtime6.jsx)("ambientLight", { intensity: p2.ambient }),
|
|
9051
|
+
/* @__PURE__ */ (0, import_jsx_runtime6.jsx)("ambientLight", { ref: ambientRef, intensity: p2.ambient }),
|
|
8642
9052
|
p2.gobo && goboMap ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
|
|
8643
9053
|
"spotLight",
|
|
8644
9054
|
{
|
|
9055
|
+
ref: keyRef,
|
|
8645
9056
|
position: p2.key.position,
|
|
8646
9057
|
color: p2.key.color,
|
|
8647
|
-
intensity:
|
|
9058
|
+
intensity: keyIntensity,
|
|
8648
9059
|
angle: p2.gobo.angle,
|
|
8649
9060
|
penumbra: 0.5,
|
|
8650
9061
|
decay: 0,
|
|
@@ -8657,9 +9068,10 @@ function PaperLighting({
|
|
|
8657
9068
|
) : /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
|
|
8658
9069
|
"directionalLight",
|
|
8659
9070
|
{
|
|
9071
|
+
ref: keyRef,
|
|
8660
9072
|
position: p2.key.position,
|
|
8661
9073
|
color: p2.key.color,
|
|
8662
|
-
intensity:
|
|
9074
|
+
intensity: keyIntensity,
|
|
8663
9075
|
castShadow,
|
|
8664
9076
|
"shadow-mapSize": [mapSize || 1, mapSize || 1],
|
|
8665
9077
|
"shadow-radius": p2.shadow.radius,
|
|
@@ -8773,11 +9185,24 @@ var Paper = (0, import_react12.forwardRef)(function Paper2({ children, className
|
|
|
8773
9185
|
{
|
|
8774
9186
|
preset: config.scene.lighting,
|
|
8775
9187
|
light: config.scene.light,
|
|
8776
|
-
floor: -1.05,
|
|
9188
|
+
floor: config.scene.floor.enabled ? config.scene.floor.y : -1.05,
|
|
8777
9189
|
scale: 8,
|
|
8778
|
-
reducedMotion: meshProps.reducedMotion
|
|
9190
|
+
reducedMotion: meshProps.reducedMotion,
|
|
9191
|
+
damage: meshProps.damage
|
|
8779
9192
|
}
|
|
8780
9193
|
),
|
|
9194
|
+
config.scene.floor.enabled && // Dark, matte and large enough to leave no edge in frame: what the
|
|
9195
|
+
// fire's light pools on, and what the paper it cuts loose lands on.
|
|
9196
|
+
/* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("mesh", { "rotation-x": -Math.PI / 2, position: [0, config.scene.floor.y, 0], receiveShadow: true, children: [
|
|
9197
|
+
/* @__PURE__ */ (0, import_jsx_runtime7.jsx)("planeGeometry", { args: [40, 40] }),
|
|
9198
|
+
/* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
|
|
9199
|
+
"meshStandardMaterial",
|
|
9200
|
+
{
|
|
9201
|
+
color: config.scene.floor.color,
|
|
9202
|
+
roughness: config.scene.floor.roughness
|
|
9203
|
+
}
|
|
9204
|
+
)
|
|
9205
|
+
] }),
|
|
8781
9206
|
/* @__PURE__ */ (0, import_jsx_runtime7.jsx)(PaperMesh, { ref, ...meshProps }),
|
|
8782
9207
|
children,
|
|
8783
9208
|
/* @__PURE__ */ (0, import_jsx_runtime7.jsx)(ReleaseContextOnUnmount, {})
|