reze-engine 0.56.6 → 0.56.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/effect-schedule.d.ts +57 -0
- package/dist/effect-schedule.d.ts.map +1 -1
- package/dist/effect-schedule.js +69 -0
- package/dist/engine.d.ts +45 -34
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +107 -43
- package/dist/shaders/passes/ground.d.ts.map +1 -1
- package/dist/shaders/passes/ground.js +7 -6
- package/package.json +1 -1
- package/src/effect-schedule.ts +96 -0
- package/src/engine.ts +131 -58
- package/src/shaders/passes/ground.ts +7 -6
package/src/engine.ts
CHANGED
|
@@ -30,7 +30,19 @@ import { LTC_MAG_LUT_SIZE, LTC_MAG_LUT_DATA } from "./shaders/ltc_mag_lut"
|
|
|
30
30
|
import { SHADOW_DEPTH_SHADER_WGSL } from "./shaders/passes/shadow"
|
|
31
31
|
import { ID_DEBUG_SHADER_WGSL } from "./shaders/passes/id-debug"
|
|
32
32
|
import { paramChanged, sampleParamTrack, type ParamKey, type ParamValue } from "./param-track"
|
|
33
|
-
import {
|
|
33
|
+
import {
|
|
34
|
+
advanceSim,
|
|
35
|
+
DISSOLVE_PARAMS,
|
|
36
|
+
dissolveConstants,
|
|
37
|
+
dissolveCycleOf,
|
|
38
|
+
effectState,
|
|
39
|
+
sampleDissolveCycle,
|
|
40
|
+
scheduledDissolve,
|
|
41
|
+
type DissolveCycle,
|
|
42
|
+
type DissolveTimings,
|
|
43
|
+
type EffectWindow,
|
|
44
|
+
type SimClock,
|
|
45
|
+
} from "./effect-schedule"
|
|
34
46
|
import { parentKeySpan, type ModelParentKey } from "./parent-keys"
|
|
35
47
|
import { SHADOW_CASCADES, buildShadowVP } from "./shadow-cascades"
|
|
36
48
|
import { REFLECTION_DEBUG_WGSL, buildMirrorCamera, planeFromPointNormal } from "./reflection"
|
|
@@ -745,24 +757,8 @@ interface PickDrawCall {
|
|
|
745
757
|
bindGroup: GPUBindGroup
|
|
746
758
|
}
|
|
747
759
|
|
|
748
|
-
/**
|
|
749
|
-
|
|
750
|
-
*
|
|
751
|
-
* Four moments rather than a duration and a delay: every one of them is a thing
|
|
752
|
-
* you can see happen, and an author tuning this is watching for exactly those
|
|
753
|
-
* four frames.
|
|
754
|
-
*/
|
|
755
|
-
export interface DissolveCycle {
|
|
756
|
-
period: number
|
|
757
|
-
/** She starts to come apart. */
|
|
758
|
-
breakAt: number
|
|
759
|
-
/** Fully gone. */
|
|
760
|
-
hiddenAt: number
|
|
761
|
-
/** She starts to come back. */
|
|
762
|
-
backAt: number
|
|
763
|
-
/** Whole again. */
|
|
764
|
-
doneAt: number
|
|
765
|
-
}
|
|
760
|
+
/** Authored in effect-schedule, beside the evaluator that performs it. */
|
|
761
|
+
export type { DissolveCycle }
|
|
766
762
|
|
|
767
763
|
interface ModelInstance {
|
|
768
764
|
name: string
|
|
@@ -1540,6 +1536,12 @@ interface EffectInstance {
|
|
|
1540
1536
|
simStep: number
|
|
1541
1537
|
/** Empty the particle pool and the grid before this frame's step. */
|
|
1542
1538
|
simReset: boolean
|
|
1539
|
+
/**
|
|
1540
|
+
* The four durations this effect's `#dissolve` takes the cast apart over, as
|
|
1541
|
+
* its source wrote them. Null unless it declared one. A dial of the same name
|
|
1542
|
+
* wins over the constant — see dissolveTimingsOf.
|
|
1543
|
+
*/
|
|
1544
|
+
dissolve: DissolveTimings | null
|
|
1543
1545
|
/** This effect's OWN clock, as a uniform the field shader reads. Per effect
|
|
1544
1546
|
* because the shared one (viewU[6].x) is measured from the first installed
|
|
1545
1547
|
* effect's epoch, so everything later started mid-stream. Null when the
|
|
@@ -2140,6 +2142,9 @@ export class Engine {
|
|
|
2140
2142
|
// because two planes would need two of every attachment below. Which one is
|
|
2141
2143
|
// in force is mirrorPlane, refreshed by updateMirrorCamera.
|
|
2142
2144
|
private static readonly GROUND_PLANE: readonly number[] = [0, 1, 0, 0]
|
|
2145
|
+
/** Where addGround put the floor. The mirror reflects across it, so a raised
|
|
2146
|
+
* floor that kept y = 0 would reflect the scene into the wrong plane. */
|
|
2147
|
+
private groundY = 0
|
|
2143
2148
|
private mirrorPlane = new Float32Array([0, 1, 0, 0])
|
|
2144
2149
|
private mirrorCameraData = new Float32Array(40)
|
|
2145
2150
|
private mirrorCameraBuffer!: GPUBuffer
|
|
@@ -3290,7 +3295,9 @@ export class Engine {
|
|
|
3290
3295
|
const m = this.mirrorSurfaceModel
|
|
3291
3296
|
planeFromPointNormal(m[12], m[13], m[14], m[8], m[9], m[10], this.mirrorPlane)
|
|
3292
3297
|
} else {
|
|
3298
|
+
// The floor's own plane: up, offset to wherever addGround put it.
|
|
3293
3299
|
this.mirrorPlane.set(Engine.GROUND_PLANE)
|
|
3300
|
+
this.mirrorPlane[3] = -this.groundY
|
|
3294
3301
|
}
|
|
3295
3302
|
// The ground inside the mirror pass clips against this same plane — a
|
|
3296
3303
|
// mirror shows nothing behind itself, and the floor is what is behind one.
|
|
@@ -4557,6 +4564,7 @@ export class Engine {
|
|
|
4557
4564
|
sim: { start: null, time: null },
|
|
4558
4565
|
simStep: 0,
|
|
4559
4566
|
simReset: false,
|
|
4567
|
+
dissolve: d.dissolve ? dissolveConstants(authored) : null,
|
|
4560
4568
|
// Its OWN resolution, no longer the scene's: an effect that never asked
|
|
4561
4569
|
// for full res is not promoted because a neighbour did.
|
|
4562
4570
|
// FULL RESOLUTION UNLESS TOLD OTHERWISE.
|
|
@@ -9204,6 +9212,15 @@ export class Engine {
|
|
|
9204
9212
|
addGround(options?: {
|
|
9205
9213
|
width?: number
|
|
9206
9214
|
height?: number
|
|
9215
|
+
/**
|
|
9216
|
+
* Where the plane sits on Y, in world units. 0 (default) is MMD's floor.
|
|
9217
|
+
*
|
|
9218
|
+
* A framing offset, not a stage: a camera motion authored for a taller or
|
|
9219
|
+
* shorter character fits by moving the model and the floor together, which
|
|
9220
|
+
* is cheaper than retiming the shot. What stands ON the floor does not
|
|
9221
|
+
* follow it — the physics floor is model space, at the figure's own feet.
|
|
9222
|
+
*/
|
|
9223
|
+
y?: number
|
|
9207
9224
|
diffuseColor?: Vec3
|
|
9208
9225
|
fadeStart?: number
|
|
9209
9226
|
fadeEnd?: number
|
|
@@ -9243,6 +9260,7 @@ export class Engine {
|
|
|
9243
9260
|
const opts = {
|
|
9244
9261
|
width: 160,
|
|
9245
9262
|
height: 160,
|
|
9263
|
+
y: 0,
|
|
9246
9264
|
diffuseColor: new Vec3(0.9, 0.1, 1.0),
|
|
9247
9265
|
fadeStart: 10.0,
|
|
9248
9266
|
fadeEnd: 80.0,
|
|
@@ -9258,7 +9276,8 @@ export class Engine {
|
|
|
9258
9276
|
shadowSoftness: 0,
|
|
9259
9277
|
...options,
|
|
9260
9278
|
}
|
|
9261
|
-
this.
|
|
9279
|
+
this.groundY = opts.y
|
|
9280
|
+
this.createGroundGeometry(opts.width, opts.height, opts.y)
|
|
9262
9281
|
this.createShadowGroundResources(opts)
|
|
9263
9282
|
this.hasGround = true
|
|
9264
9283
|
this.groundDrawCall = {
|
|
@@ -9271,7 +9290,14 @@ export class Engine {
|
|
|
9271
9290
|
// The ground belongs to no model instance, so it is not in the cull list —
|
|
9272
9291
|
// cullIndex -1 leaves renderGround unconditional. Its box is filled in
|
|
9273
9292
|
// anyway rather than left a lie for whoever reads this next.
|
|
9274
|
-
bounds: new Float32Array([
|
|
9293
|
+
bounds: new Float32Array([
|
|
9294
|
+
-opts.width / 2,
|
|
9295
|
+
opts.y,
|
|
9296
|
+
-opts.height / 2,
|
|
9297
|
+
opts.width / 2,
|
|
9298
|
+
opts.y,
|
|
9299
|
+
opts.height / 2,
|
|
9300
|
+
]),
|
|
9275
9301
|
cullIndex: -1,
|
|
9276
9302
|
}
|
|
9277
9303
|
}
|
|
@@ -12316,14 +12342,14 @@ export class Engine {
|
|
|
12316
12342
|
}
|
|
12317
12343
|
}
|
|
12318
12344
|
|
|
12319
|
-
private createGroundGeometry(width: number = 100, height: number = 100) {
|
|
12345
|
+
private createGroundGeometry(width: number = 100, height: number = 100, y: number = 0) {
|
|
12320
12346
|
const halfWidth = width / 2
|
|
12321
12347
|
const halfHeight = height / 2
|
|
12322
12348
|
|
|
12323
12349
|
const vertices = new Float32Array([
|
|
12324
12350
|
// Bottom-left
|
|
12325
12351
|
-halfWidth,
|
|
12326
|
-
|
|
12352
|
+
y,
|
|
12327
12353
|
-halfHeight, // position
|
|
12328
12354
|
0,
|
|
12329
12355
|
1,
|
|
@@ -12333,7 +12359,7 @@ export class Engine {
|
|
|
12333
12359
|
|
|
12334
12360
|
// Bottom-right
|
|
12335
12361
|
halfWidth,
|
|
12336
|
-
|
|
12362
|
+
y,
|
|
12337
12363
|
-halfHeight, // position
|
|
12338
12364
|
0,
|
|
12339
12365
|
1,
|
|
@@ -12343,7 +12369,7 @@ export class Engine {
|
|
|
12343
12369
|
|
|
12344
12370
|
// Top-right
|
|
12345
12371
|
halfWidth,
|
|
12346
|
-
|
|
12372
|
+
y,
|
|
12347
12373
|
halfHeight, // position
|
|
12348
12374
|
0,
|
|
12349
12375
|
1,
|
|
@@ -12353,7 +12379,7 @@ export class Engine {
|
|
|
12353
12379
|
|
|
12354
12380
|
// Top-left
|
|
12355
12381
|
-halfWidth,
|
|
12356
|
-
|
|
12382
|
+
y,
|
|
12357
12383
|
halfHeight, // position
|
|
12358
12384
|
0,
|
|
12359
12385
|
1,
|
|
@@ -12433,6 +12459,7 @@ export class Engine {
|
|
|
12433
12459
|
|
|
12434
12460
|
private createShadowGroundResources(opts: {
|
|
12435
12461
|
diffuseColor: Vec3
|
|
12462
|
+
y: number
|
|
12436
12463
|
fadeStart: number
|
|
12437
12464
|
fadeEnd: number
|
|
12438
12465
|
shadowStrength: number
|
|
@@ -12448,6 +12475,7 @@ export class Engine {
|
|
|
12448
12475
|
}) {
|
|
12449
12476
|
const {
|
|
12450
12477
|
diffuseColor,
|
|
12478
|
+
y,
|
|
12451
12479
|
fadeStart,
|
|
12452
12480
|
fadeEnd,
|
|
12453
12481
|
shadowStrength,
|
|
@@ -12488,6 +12516,9 @@ export class Engine {
|
|
|
12488
12516
|
// gb[18] — shadow edge softness. Was padding; the shader reads it as the
|
|
12489
12517
|
// Vogel disk's radius, and 0 takes the sharp nine-tap path unchanged.
|
|
12490
12518
|
gb[18] = Math.min(Math.max(shadowSoftness, 0), 1)
|
|
12519
|
+
// gb[19] — the floor's height. Was padding; the mirror branch reflects the
|
|
12520
|
+
// eye across it, and a hardcoded 0 slid the reflection off a raised floor.
|
|
12521
|
+
gb[19] = y
|
|
12491
12522
|
// Which variant the draw picks. Zero is the sharp shader, which is the one
|
|
12492
12523
|
// that existed before softness did.
|
|
12493
12524
|
this.groundSoft = gb[18] > 0
|
|
@@ -14390,6 +14421,9 @@ export class Engine {
|
|
|
14390
14421
|
if (pose) this.camera.setVmdPose(pose)
|
|
14391
14422
|
}
|
|
14392
14423
|
|
|
14424
|
+
// Before the cast is written, which carries every subject's dissolve to the
|
|
14425
|
+
// effects reading it.
|
|
14426
|
+
this.evaluateDissolves()
|
|
14393
14427
|
this.updateCameraUniforms()
|
|
14394
14428
|
this.updateShadowLightVP()
|
|
14395
14429
|
|
|
@@ -14531,7 +14565,6 @@ export class Engine {
|
|
|
14531
14565
|
// and a grid stepped after them is one frame stale in everything that used it.
|
|
14532
14566
|
// Material parameters on the scene clock, before anything reads their
|
|
14533
14567
|
// uniforms this frame.
|
|
14534
|
-
this.evaluateDissolveCycles()
|
|
14535
14568
|
this.evaluateParamTracks()
|
|
14536
14569
|
// FIRST among the things that read an effect, because every one of them
|
|
14537
14570
|
// reads what this writes: the sim's clock, the particle uniform's weight,
|
|
@@ -15012,22 +15045,6 @@ export class Engine {
|
|
|
15012
15045
|
return true
|
|
15013
15046
|
}
|
|
15014
15047
|
|
|
15015
|
-
/**
|
|
15016
|
-
* A repeating dissolve, on the scene clock.
|
|
15017
|
-
*
|
|
15018
|
-
* The alternative was a host calling setModelDissolve every frame, and it is
|
|
15019
|
-
* the wrong shape twice: an exported take stepped at another rate would land
|
|
15020
|
-
* on different values than the preview did, and the effect drawing the sparks
|
|
15021
|
-
* would be reading a number some other clock wrote. Here the engine samples it
|
|
15022
|
-
* where it samples everything else time-driven, so a take reproduces exactly
|
|
15023
|
-
* and rzSubject().dissolve is the same value the material shell used on that
|
|
15024
|
-
* very frame.
|
|
15025
|
-
*
|
|
15026
|
-
* The five numbers are seconds within one cycle: when she starts to go, when
|
|
15027
|
-
* she is fully gone, when she starts to come back, and when she is whole. The
|
|
15028
|
-
* gaps between them are the timing, and the hold between the middle two is how
|
|
15029
|
-
* long she is away.
|
|
15030
|
-
*/
|
|
15031
15048
|
/**
|
|
15032
15049
|
* Eyes on the camera, per model — see Model.setEyeTracking. Live: solved
|
|
15033
15050
|
* every frame against wherever the camera is, orbit or motion alike. Null
|
|
@@ -15040,6 +15057,20 @@ export class Engine {
|
|
|
15040
15057
|
return true
|
|
15041
15058
|
}
|
|
15042
15059
|
|
|
15060
|
+
/**
|
|
15061
|
+
* A repeating dissolve on one model, on the scene clock.
|
|
15062
|
+
*
|
|
15063
|
+
* The alternative was a host calling setModelDissolve every frame, and it is
|
|
15064
|
+
* the wrong shape twice: an exported take stepped at another rate would land
|
|
15065
|
+
* on different values than the preview did, and the effect drawing the sparks
|
|
15066
|
+
* would be reading a number some other clock wrote. Here the engine samples it
|
|
15067
|
+
* where it samples everything else time-driven, so a take reproduces exactly
|
|
15068
|
+
* and rzSubject().dissolve is the same value the material shell used on that
|
|
15069
|
+
* very frame.
|
|
15070
|
+
*
|
|
15071
|
+
* An effect that declares `#dissolve` needs none of this: it carries its own
|
|
15072
|
+
* cycle and follows its own clips — see evaluateDissolves.
|
|
15073
|
+
*/
|
|
15043
15074
|
setModelDissolveCycle(modelName: string, cycle: DissolveCycle | null): boolean {
|
|
15044
15075
|
if (!this.modelInstances.has(modelName)) return false
|
|
15045
15076
|
if (!cycle) {
|
|
@@ -15050,23 +15081,65 @@ export class Engine {
|
|
|
15050
15081
|
return true
|
|
15051
15082
|
}
|
|
15052
15083
|
|
|
15053
|
-
/**
|
|
15054
|
-
|
|
15055
|
-
|
|
15056
|
-
|
|
15057
|
-
|
|
15058
|
-
|
|
15059
|
-
|
|
15060
|
-
|
|
15061
|
-
|
|
15062
|
-
|
|
15063
|
-
|
|
15064
|
-
|
|
15065
|
-
|
|
15066
|
-
|
|
15084
|
+
/** The model at one cast slot — the subject rzSubject(index) reads. */
|
|
15085
|
+
private castSubjectName(index: number): string | null {
|
|
15086
|
+
let n = 0
|
|
15087
|
+
let found: string | null = null
|
|
15088
|
+
this.forEachInstance((inst) => {
|
|
15089
|
+
if (found !== null || n >= MAX_EFFECT_SUBJECTS || inst.isStage || inst.isPlane || inst.isProp) return
|
|
15090
|
+
if (n === index) found = inst.name
|
|
15091
|
+
n++
|
|
15092
|
+
})
|
|
15093
|
+
return found
|
|
15094
|
+
}
|
|
15095
|
+
|
|
15096
|
+
/** One effect's four durations: its dials where it declares them, the
|
|
15097
|
+
* constants it wrote where it does not. */
|
|
15098
|
+
private dissolveTimingsOf(fx: EffectInstance, wrote: DissolveTimings): DissolveTimings {
|
|
15099
|
+
const t = { ...wrote }
|
|
15100
|
+
for (const [key, name] of DISSOLVE_PARAMS) {
|
|
15101
|
+
const slot = fx.paramLayout.get(name)
|
|
15102
|
+
if (slot) t[key] = Math.max(0, fx.paramsData[slot.offset])
|
|
15103
|
+
}
|
|
15104
|
+
return t
|
|
15105
|
+
}
|
|
15106
|
+
|
|
15107
|
+
/** Models this wrote a dissolve onto last frame, so one nothing asks for any
|
|
15108
|
+
* more comes back whole instead of holding wherever it was left. */
|
|
15109
|
+
private dissolveTouched = new Set<string>()
|
|
15110
|
+
|
|
15111
|
+
/**
|
|
15112
|
+
* Every dissolve, once a frame, before the cast is written — so the value a
|
|
15113
|
+
* mote reads as rzSubject(i).dissolve is the one her materials wore on that
|
|
15114
|
+
* very frame, rather than the one they wore on the last.
|
|
15115
|
+
*
|
|
15116
|
+
* Two things ask for one. A cycle set on a model (setModelDissolveCycle) runs
|
|
15117
|
+
* on the scene clock and repeats, as it always has. An effect declaring
|
|
15118
|
+
* `#dissolve` takes subject 0 apart on ITS OWN terms: scheduled, the cycle on
|
|
15119
|
+
* each clip's clock with her whole between clips; unscheduled, the same
|
|
15120
|
+
* repeating cycle as before. Where both ask, the more dissolved wins — two
|
|
15121
|
+
* answers about one body, and half a body is not one of them.
|
|
15122
|
+
*/
|
|
15123
|
+
private evaluateDissolves(): void {
|
|
15124
|
+
const want = new Map<string, number>()
|
|
15125
|
+
const put = (name: string, v: number) => want.set(name, Math.min(want.get(name) ?? 1, v))
|
|
15126
|
+
for (const [name, c] of this.dissolveCycles) put(name, sampleDissolveCycle(c, this.sceneClock))
|
|
15127
|
+
let transport: number | null = null
|
|
15128
|
+
for (const fx of this.effects) {
|
|
15129
|
+
if (!fx.dissolve) continue
|
|
15130
|
+
const cycle = dissolveCycleOf(this.dissolveTimingsOf(fx, fx.dissolve))
|
|
15131
|
+
const subject = this.castSubjectName(0)
|
|
15132
|
+
if (!cycle || !subject) continue
|
|
15133
|
+
if (fx.window && fx.window.length > 0) {
|
|
15134
|
+
transport ??= this.transportTime()
|
|
15135
|
+
put(subject, scheduledDissolve(fx.window, cycle, transport))
|
|
15136
|
+
} else {
|
|
15137
|
+
put(subject, sampleDissolveCycle(cycle, this.sceneClock))
|
|
15067
15138
|
}
|
|
15068
|
-
this.setModelDissolve(name, v)
|
|
15069
15139
|
}
|
|
15140
|
+
for (const name of this.dissolveTouched) if (!want.has(name)) this.setModelDissolve(name, 1)
|
|
15141
|
+
this.dissolveTouched = new Set(want.keys())
|
|
15142
|
+
for (const [name, v] of want) this.setModelDissolve(name, v)
|
|
15070
15143
|
}
|
|
15071
15144
|
|
|
15072
15145
|
/** What setModelDissolve last set, or 1 for a model that has never dissolved. */
|
|
@@ -94,7 +94,7 @@ struct GroundShadowMat {
|
|
|
94
94
|
gridLineColor: vec3f, mirror: f32,
|
|
95
95
|
// farCascade: 1 while a stage is loaded, 0 otherwise. See the branch below —
|
|
96
96
|
// with no stage the far map is never drawn into, so its taps are known.
|
|
97
|
-
mirrorBlur: f32, farCascade: f32, shadowSoftness: f32,
|
|
97
|
+
mirrorBlur: f32, farCascade: f32, shadowSoftness: f32, groundY: f32,
|
|
98
98
|
// Every shadow caster in one sphere, refreshed per frame. w = radius; 0 means
|
|
99
99
|
// nothing casts, negative means "do not use this" (a rigid caster has no
|
|
100
100
|
// sphere, so a scene with a stage keeps the taps). See rzShadowPossible.
|
|
@@ -362,20 +362,21 @@ ${pcfWgsl("shadowMap", "suv_c", "material.pcfTexel", "compareZ", "acc", " ",
|
|
|
362
362
|
// shared projection's pair (viewZ = projB / (z - projA); the formula
|
|
363
363
|
// inverts both depth conventions, see the composite's linearDepth), and
|
|
364
364
|
// reconstruct the reflected image point along the ray from the MIRRORED
|
|
365
|
-
// eye through this fragment.
|
|
366
|
-
//
|
|
367
|
-
//
|
|
365
|
+
// eye through this fragment. Mirroring across the floor's own plane is a
|
|
366
|
+
// sign flip on y about that height (the forward vector is a direction, so
|
|
367
|
+
// it flips alone), and the image point's depth below the plane IS the
|
|
368
|
+
// reflected object's height above it.
|
|
368
369
|
let dims = vec2f(textureDimensions(mirrorDepth));
|
|
369
370
|
let texel = clamp(vec2i(muv * dims), vec2i(0), vec2i(dims) - vec2i(1));
|
|
370
371
|
let z = textureLoad(mirrorDepth, texel, 0);
|
|
371
372
|
let viewZ = clamp(mirrorVP.params.y / (z - mirrorVP.params.x), 0.05, 100000.0);
|
|
372
|
-
let eyeM = vec3f(camera.viewPos.x, -camera.viewPos.y, camera.viewPos.z);
|
|
373
|
+
let eyeM = vec3f(camera.viewPos.x, 2.0 * material.groundY - camera.viewPos.y, camera.viewPos.z);
|
|
373
374
|
let fwd = vec3f(camera.view[0][2], camera.view[1][2], camera.view[2][2]);
|
|
374
375
|
let fwdM = vec3f(fwd.x, -fwd.y, fwd.z);
|
|
375
376
|
let toFrag = i.worldPos - eyeM;
|
|
376
377
|
let dir = toFrag * inverseSqrt(max(dot(toFrag, toFrag), 1e-8));
|
|
377
378
|
let t = viewZ / max(dot(dir, fwdM), 1e-4);
|
|
378
|
-
let height = max(-(eyeM.y + dir.y * t), 0.0);
|
|
379
|
+
let height = max(material.groundY - (eyeM.y + dir.y * t), 0.0);
|
|
379
380
|
// Full softness by BLUR_SPAN units above the floor — about a character's
|
|
380
381
|
// height. The hardware clamps the level, so the nominal 5.0 needs no
|
|
381
382
|
// knowledge of the real chain length, and blur 0 reads exactly level 0.
|