reze-engine 0.65.0 → 0.66.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/engine.d.ts +156 -42
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +876 -186
- package/dist/graph/compile.d.ts.map +1 -1
- package/dist/graph/compile.js +5 -0
- package/dist/graph/presets/body.d.ts.map +1 -1
- package/dist/graph/presets/body.js +26 -24
- package/dist/graph/presets/cloth_rough.js +8 -8
- package/dist/graph/presets/cloth_smooth.js +8 -8
- package/dist/graph/presets/default.js +4 -4
- package/dist/graph/presets/eye.js +10 -10
- package/dist/graph/presets/face.d.ts.map +1 -1
- package/dist/graph/presets/face.js +28 -26
- package/dist/graph/presets/hair.js +6 -6
- package/dist/graph/presets/metal.js +8 -8
- package/dist/graph/presets/stockings.d.ts.map +1 -1
- package/dist/graph/presets/stockings.js +4 -11
- package/dist/graph/presets/unlit.d.ts.map +1 -1
- package/dist/graph/presets/unlit.js +9 -13
- package/dist/graph/registry.d.ts.map +1 -1
- package/dist/graph/registry.js +128 -157
- package/dist/graph/render-class.d.ts +5 -2
- package/dist/graph/render-class.d.ts.map +1 -1
- package/dist/graph/slots.d.ts.map +1 -1
- package/dist/graph/slots.js +5 -1
- package/dist/model.d.ts +14 -1
- package/dist/model.d.ts.map +1 -1
- package/dist/model.js +27 -1
- package/dist/pmx-loader.d.ts +4 -0
- package/dist/pmx-loader.d.ts.map +1 -1
- package/dist/pmx-loader.js +11 -2
- package/dist/shaders/anchor-table.d.ts +11 -0
- package/dist/shaders/anchor-table.d.ts.map +1 -1
- package/dist/shaders/anchor-table.js +9 -3
- package/dist/shaders/directives.d.ts +42 -5
- package/dist/shaders/directives.d.ts.map +1 -1
- package/dist/shaders/directives.js +51 -4
- package/dist/shaders/lights.d.ts +11 -7
- package/dist/shaders/lights.d.ts.map +1 -1
- package/dist/shaders/lights.js +315 -271
- package/dist/shaders/materials/common.d.ts.map +1 -1
- package/dist/shaders/materials/common.js +26 -3
- package/dist/shaders/materials/nodes.d.ts +1 -1
- package/dist/shaders/materials/nodes.d.ts.map +1 -1
- package/dist/shaders/materials/nodes.js +191 -271
- package/dist/shaders/passes/composite.d.ts +1 -1
- package/dist/shaders/passes/composite.d.ts.map +1 -1
- package/dist/shaders/passes/composite.js +3 -5
- package/dist/shaders/passes/ground.js +1 -1
- package/dist/shaders/passes/particles.d.ts.map +1 -1
- package/dist/shaders/passes/particles.js +9 -3
- package/dist/shaders/passes/probe.d.ts +45 -0
- package/dist/shaders/passes/probe.d.ts.map +1 -0
- package/dist/shaders/passes/probe.js +230 -0
- package/dist/shaders/passes/scene-contract.d.ts +13 -0
- package/dist/shaders/passes/scene-contract.d.ts.map +1 -1
- package/dist/shaders/passes/scene-contract.js +4 -0
- package/dist/shaders/scene-light-api.d.ts.map +1 -1
- package/dist/shaders/scene-light-api.js +1 -0
- package/dist/unity/globals.d.ts +7 -0
- package/dist/unity/globals.d.ts.map +1 -1
- package/dist/unity/globals.js +4 -3
- package/dist/unity/host.d.ts +3 -0
- package/dist/unity/host.d.ts.map +1 -1
- package/dist/unity/host.js +41 -7
- package/dist/unity/looks.d.ts +42 -0
- package/dist/unity/looks.d.ts.map +1 -1
- package/dist/unity/looks.js +131 -4
- package/dist/unity/stage.d.ts +17 -0
- package/dist/unity/stage.d.ts.map +1 -1
- package/dist/unity/stage.js +22 -5
- package/package.json +1 -1
- package/src/engine.ts +949 -215
- package/src/graph/compile.ts +4 -0
- package/src/graph/presets/body.ts +26 -24
- package/src/graph/presets/cloth_rough.ts +8 -8
- package/src/graph/presets/cloth_smooth.ts +8 -8
- package/src/graph/presets/default.ts +4 -4
- package/src/graph/presets/eye.ts +10 -10
- package/src/graph/presets/face.ts +28 -26
- package/src/graph/presets/hair.ts +6 -6
- package/src/graph/presets/metal.ts +8 -8
- package/src/graph/presets/stockings.ts +4 -11
- package/src/graph/presets/unlit.ts +9 -13
- package/src/graph/registry.ts +133 -158
- package/src/graph/render-class.ts +5 -2
- package/src/graph/slots.ts +5 -1
- package/src/model.ts +33 -1
- package/src/pmx-loader.ts +11 -2
- package/src/shaders/anchor-table.ts +15 -3
- package/src/shaders/directives.ts +88 -6
- package/src/shaders/lights.ts +464 -415
- package/src/shaders/materials/common.ts +26 -3
- package/src/shaders/materials/nodes.ts +191 -271
- package/src/shaders/passes/composite.ts +3 -5
- package/src/shaders/passes/ground.ts +1 -1
- package/src/shaders/passes/particles.ts +9 -3
- package/src/shaders/passes/probe.ts +240 -0
- package/src/shaders/passes/scene-contract.ts +17 -0
- package/src/shaders/scene-light-api.ts +1 -0
- package/src/unity/globals.ts +7 -3
- package/src/unity/host.ts +36 -6
- package/src/unity/looks.ts +161 -7
- package/src/unity/stage.ts +28 -5
- package/dist/physics-debug.d.ts +0 -30
- package/dist/physics-debug.d.ts.map +0 -1
- package/dist/physics-debug.js +0 -526
- package/dist/shaders/agx-lut.d.ts +0 -11
- package/dist/shaders/agx-lut.d.ts.map +0 -1
- package/dist/shaders/agx-lut.js +0 -29
- package/dist/shaders/dfg_lut.d.ts +0 -3
- package/dist/shaders/dfg_lut.d.ts.map +0 -1
- package/dist/shaders/dfg_lut.js +0 -129
- package/dist/shaders/ltc_mag_lut.d.ts +0 -3
- package/dist/shaders/ltc_mag_lut.d.ts.map +0 -1
- package/dist/shaders/ltc_mag_lut.js +0 -1033
- package/dist/shaders/materials/body.d.ts +0 -2
- package/dist/shaders/materials/body.d.ts.map +0 -1
- package/dist/shaders/materials/body.js +0 -95
- package/dist/shaders/materials/cloth_rough.d.ts +0 -2
- package/dist/shaders/materials/cloth_rough.d.ts.map +0 -1
- package/dist/shaders/materials/cloth_rough.js +0 -69
- package/dist/shaders/materials/cloth_smooth.d.ts +0 -2
- package/dist/shaders/materials/cloth_smooth.d.ts.map +0 -1
- package/dist/shaders/materials/cloth_smooth.js +0 -61
- package/dist/shaders/materials/default.d.ts +0 -2
- package/dist/shaders/materials/default.d.ts.map +0 -1
- package/dist/shaders/materials/default.js +0 -43
- package/dist/shaders/materials/eye.d.ts +0 -2
- package/dist/shaders/materials/eye.d.ts.map +0 -1
- package/dist/shaders/materials/eye.js +0 -60
- package/dist/shaders/materials/face.d.ts +0 -2
- package/dist/shaders/materials/face.d.ts.map +0 -1
- package/dist/shaders/materials/face.js +0 -95
- package/dist/shaders/materials/hair.d.ts +0 -2
- package/dist/shaders/materials/hair.d.ts.map +0 -1
- package/dist/shaders/materials/hair.js +0 -90
- package/dist/shaders/materials/metal.d.ts +0 -2
- package/dist/shaders/materials/metal.d.ts.map +0 -1
- package/dist/shaders/materials/metal.js +0 -77
- package/dist/shaders/materials/mmd_classic.d.ts +0 -2
- package/dist/shaders/materials/mmd_classic.d.ts.map +0 -1
- package/dist/shaders/materials/mmd_classic.js +0 -66
- package/dist/shaders/materials/stockings.d.ts +0 -2
- package/dist/shaders/materials/stockings.d.ts.map +0 -1
- package/dist/shaders/materials/stockings.js +0 -122
- package/dist/shaders/passes/physics-debug.d.ts +0 -2
- package/dist/shaders/passes/physics-debug.d.ts.map +0 -1
- package/dist/shaders/passes/physics-debug.js +0 -69
- package/src/shaders/dfg_lut.ts +0 -131
- package/src/shaders/ltc_mag_lut.ts +0 -1035
package/src/engine.ts
CHANGED
|
@@ -25,8 +25,6 @@ import {
|
|
|
25
25
|
normalizeAssetPath,
|
|
26
26
|
type AssetReader,
|
|
27
27
|
} from "./asset-reader"
|
|
28
|
-
import { BRDF_LUT_SIZE, BRDF_LUT_BAKE_WGSL } from "./shaders/dfg_lut"
|
|
29
|
-
import { LTC_MAG_LUT_SIZE, LTC_MAG_LUT_DATA } from "./shaders/ltc_mag_lut"
|
|
30
28
|
import { SHADOW_DEPTH_SHADER_WGSL } from "./shaders/passes/shadow"
|
|
31
29
|
import { ID_DEBUG_SHADER_WGSL } from "./shaders/passes/id-debug"
|
|
32
30
|
import { paramChanged, sampleParamTrack, type ParamKey, type ParamValue } from "./param-track"
|
|
@@ -46,10 +44,10 @@ import {
|
|
|
46
44
|
import { parentKeySpan, type ModelParentKey } from "./parent-keys"
|
|
47
45
|
import { NativeHost } from "./unity/host"
|
|
48
46
|
import { NativeLooks, type NativeLook } from "./unity/looks"
|
|
49
|
-
import { NativeStage, type NativeStagePackage, type NativeStageReader } from "./unity/stage"
|
|
47
|
+
import { NativeStage, type NativeStageLights, type NativeStagePackage, type NativeStageReader } from "./unity/stage"
|
|
50
48
|
import { gameDir, unityFrameGlobals } from "./unity/globals"
|
|
51
49
|
import { outlineMaxOffsetMultiplier, towardMatrix } from "./unity/character"
|
|
52
|
-
import type
|
|
50
|
+
import { sameValue, type NativeValue } from "./unity/host"
|
|
53
51
|
import { SHADOW_ATLAS_SIZE, SHADOW_CASCADES, buildShadowCascades, cascadeSpheres, type ShadowBounds, type ShadowView } from "./shadow-cascades"
|
|
54
52
|
import { REFLECTION_DEBUG_WGSL, buildMirrorCamera, planeFromPointNormal } from "./reflection"
|
|
55
53
|
import {
|
|
@@ -159,6 +157,17 @@ import {
|
|
|
159
157
|
import { buildTrailShader, trailEntryPoints, TRAIL_SUBDIVISIONS } from "./shaders/passes/trails"
|
|
160
158
|
import { PICK_SHADER_WGSL } from "./shaders/passes/pick"
|
|
161
159
|
import { MIPMAP_BLIT_SHADER_WGSL } from "./shaders/passes/mipmap"
|
|
160
|
+
import {
|
|
161
|
+
PROBE_CAPTURE_LEVELS,
|
|
162
|
+
PROBE_CAPTURE_SIZE,
|
|
163
|
+
PROBE_COMPOSE_WGSL,
|
|
164
|
+
PROBE_FILTER_WGSL,
|
|
165
|
+
PROBE_LEVELS,
|
|
166
|
+
PROBE_SIZE,
|
|
167
|
+
SKY_PREFILTER_WGSL,
|
|
168
|
+
probeLevelRoughness,
|
|
169
|
+
skyLevelRoughness,
|
|
170
|
+
} from "./shaders/passes/probe"
|
|
162
171
|
import { compileGraph, type CompileOptions, type StyleSlot } from "./graph/compile"
|
|
163
172
|
import type { Diagnostic, ShaderGraph } from "./graph/schema"
|
|
164
173
|
import type { AlphaMode, RenderClass, StyleBlend } from "./graph/render-class"
|
|
@@ -361,12 +370,13 @@ export type LoadModelFromFilesOptions = {
|
|
|
361
370
|
pmxFile?: File
|
|
362
371
|
}
|
|
363
372
|
|
|
364
|
-
//
|
|
365
|
-
// Sun = the
|
|
373
|
+
// Scene config, as Unity's Lighting window lays it out: World = Environment
|
|
374
|
+
// Lighting (ambient and reflections); Sun = the main directional light;
|
|
375
|
+
// Camera = view framing.
|
|
366
376
|
type WorldOptions = {
|
|
367
|
-
/** Linear
|
|
377
|
+
/** Linear colour of the ambient when the World is a flat colour (Unity: Environment Lighting > Source Color). */
|
|
368
378
|
color?: Vec3
|
|
369
|
-
/** Multiplier on
|
|
379
|
+
/** Multiplier on the ambient and the sky's reflection (Unity: the Environment Lighting intensity multiplier). */
|
|
370
380
|
strength?: number
|
|
371
381
|
/**
|
|
372
382
|
* A sky that changes with the direction you look: one colour overhead, one at
|
|
@@ -474,14 +484,14 @@ type HeldParentKey = { time: number; parent: string | null; bone: string; positi
|
|
|
474
484
|
type ParentTrack = { keys: HeldParentKey[]; applied: number; seat: [number, number, number] }
|
|
475
485
|
|
|
476
486
|
type SunOptions = {
|
|
477
|
-
/** Linear
|
|
487
|
+
/** Linear colour of the main light (Unity: Light > Color). */
|
|
478
488
|
color?: Vec3
|
|
479
489
|
/** The rendering layers it keys, as bits; every layer by default. A
|
|
480
490
|
* directional light from setLights takes the layers the sun leaves. */
|
|
481
491
|
layers?: number
|
|
482
|
-
/**
|
|
492
|
+
/** Unity's light intensity: a white sun of 1 lights albedo 1:1 at N·L = 1. */
|
|
483
493
|
strength?: number
|
|
484
|
-
/** Direction sunlight travels (points FROM sun TO scene,
|
|
494
|
+
/** Direction sunlight travels (points FROM the sun TO the scene, Unity: the light's forward). */
|
|
485
495
|
direction?: Vec3
|
|
486
496
|
/**
|
|
487
497
|
* How much shadow the sun casts: 1 full, 0 none, and anything between.
|
|
@@ -570,8 +580,9 @@ export type BloomOptions = {
|
|
|
570
580
|
/** Brightest-channel level where the glow starts (the game's per-scene
|
|
571
581
|
* threshold). The soft knee is always half of it. */
|
|
572
582
|
threshold: number
|
|
573
|
-
/** 0..1: how far each level leans toward the wider one on the
|
|
574
|
-
* a tight halo, high a wide
|
|
583
|
+
/** 0..1, URP's Scatter: how far each level leans toward the wider one on the
|
|
584
|
+
* way up, as lerp(0.05, 0.95, scatter) — low is a tight halo, high a wide
|
|
585
|
+
* haze, and the top still keeps the finest level. 0.8 is the game's 0.77. */
|
|
575
586
|
scatter: number
|
|
576
587
|
/** Tint on the glow; white is the game. */
|
|
577
588
|
color: Vec3
|
|
@@ -582,7 +593,7 @@ export type BloomOptions = {
|
|
|
582
593
|
export const DEFAULT_BLOOM_OPTIONS: BloomOptions = {
|
|
583
594
|
enabled: true,
|
|
584
595
|
threshold: 0.7,
|
|
585
|
-
scatter: 0.
|
|
596
|
+
scatter: 0.8,
|
|
586
597
|
color: new Vec3(1, 1, 1),
|
|
587
598
|
intensity: 1,
|
|
588
599
|
}
|
|
@@ -755,7 +766,7 @@ export type EngineOptions = {
|
|
|
755
766
|
|
|
756
767
|
const DEFAULT_ENGINE_OPTIONS = {
|
|
757
768
|
world: { color: new Vec3(0.4014, 0.4944, 0.647), strength: 0.3 },
|
|
758
|
-
sun: { color: new Vec3(1.0, 1.0, 1.0), strength:
|
|
769
|
+
sun: { color: new Vec3(1.0, 1.0, 1.0), strength: 0.64, direction: new Vec3(-0.0873, -0.3844, 0.919) },
|
|
759
770
|
camera: { distance: 26.6, target: new Vec3(0, 12.5, 0), fov: Math.PI / 4 },
|
|
760
771
|
onRaycast: undefined,
|
|
761
772
|
}
|
|
@@ -939,6 +950,9 @@ interface ModelInstance {
|
|
|
939
950
|
indexBuffer: GPUBuffer
|
|
940
951
|
jointsBuffer: GPUBuffer
|
|
941
952
|
weightsBuffer: GPUBuffer
|
|
953
|
+
/** Vertex colour and second UV, per vertex — the material pipelines' slot 4.
|
|
954
|
+
* See createAttributeBuffer. */
|
|
955
|
+
attributeBuffer: GPUBuffer
|
|
942
956
|
/** The outline hull's own stream — smoothed normal + PMX edge scale per
|
|
943
957
|
* vertex (outline-normals.ts). Made with the first edge-flagged material,
|
|
944
958
|
* so a model with no outline never pays for it. */
|
|
@@ -1758,7 +1772,7 @@ interface EffectInstance {
|
|
|
1758
1772
|
distVariant: number
|
|
1759
1773
|
/** Bones this source asked for, in ITS OWN declaration order. The scene table
|
|
1760
1774
|
* maps these onto shared addresses; this list is what it is rebuilt from. */
|
|
1761
|
-
anchors: { bone: string; trail: boolean }[]
|
|
1775
|
+
anchors: { bone: string; trail: boolean; along?: number; step?: number }[]
|
|
1762
1776
|
/** Where this effect's own clock started, in scene seconds. */
|
|
1763
1777
|
epochScene: number
|
|
1764
1778
|
/**
|
|
@@ -1863,6 +1877,17 @@ export type SceneLight = {
|
|
|
1863
1877
|
* cast's key on RENDERING_LAYER_CHARACTER is how a game lights the two apart.
|
|
1864
1878
|
*/
|
|
1865
1879
|
layers?: number
|
|
1880
|
+
/**
|
|
1881
|
+
* The bulb's radius in engine units: inside it the lamp stops getting
|
|
1882
|
+
* brighter (intensity / max(d², near²)). Omitted keeps the default small
|
|
1883
|
+
* bulb; a large soft lamp (a game rig light's shape radius) is wider.
|
|
1884
|
+
*/
|
|
1885
|
+
near?: number
|
|
1886
|
+
/**
|
|
1887
|
+
* A spot's cookie: the key of a picture given to loadLightCookie, projected
|
|
1888
|
+
* along the cone the way a stage light throws a gobo. Ignored on a point.
|
|
1889
|
+
*/
|
|
1890
|
+
cookie?: string
|
|
1866
1891
|
}
|
|
1867
1892
|
|
|
1868
1893
|
/**
|
|
@@ -1938,6 +1963,9 @@ export class Engine {
|
|
|
1938
1963
|
private mainPipelineLayout!: GPUPipelineLayout
|
|
1939
1964
|
private sceneTargets!: GPUColorTargetState[]
|
|
1940
1965
|
private sceneTargetsAdditive!: GPUColorTargetState[]
|
|
1966
|
+
/** A group blending "premultiplied" — see scene-contract's
|
|
1967
|
+
* material-premultiplied. */
|
|
1968
|
+
private sceneTargetsPremultiplied!: GPUColorTargetState[]
|
|
1941
1969
|
/** The scene pass's attachment formats, settled at init once the device has
|
|
1942
1970
|
* said which HDR format it will blend. Every scene-pass pipeline asks
|
|
1943
1971
|
* scene-contract for its targets against these. */
|
|
@@ -1945,6 +1973,11 @@ export class Engine {
|
|
|
1945
1973
|
return { hdr: this.hdrFormat, aux: Engine.BLOOM_MASK_FORMAT }
|
|
1946
1974
|
}
|
|
1947
1975
|
private fullVertexBufferLayouts!: GPUVertexBufferLayout[]
|
|
1976
|
+
/** What a MATERIAL pipeline reads: the full set, then (slot 4, past the
|
|
1977
|
+
* outline hull's slot 3) the model's attribute stream — vertex colour and a
|
|
1978
|
+
* second UV (see createAttributeBuffer). Only the material shaders declare
|
|
1979
|
+
* those inputs; every other pass keeps fullVertexBufferLayouts. */
|
|
1980
|
+
private materialVertexBufferLayouts!: (GPUVertexBufferLayout | null)[]
|
|
1948
1981
|
// 1×64 vertical ramp for shared-toon materials: lit (top) → soft shadow
|
|
1949
1982
|
// tone (bottom). Stand-in for MMD's toon01–10.bmp, which we can't ship.
|
|
1950
1983
|
private defaultToonRampTexture!: GPUTexture
|
|
@@ -2097,6 +2130,8 @@ export class Engine {
|
|
|
2097
2130
|
private multisampleTexture!: GPUTexture
|
|
2098
2131
|
private hdrResolveTexture!: GPUTexture
|
|
2099
2132
|
private static readonly MULTISAMPLE_COUNT = 4
|
|
2133
|
+
/** Floats per vertex in a model's attribute stream: colour RGBA, then UV2. */
|
|
2134
|
+
private static readonly ATTRIBUTE_FLOATS = 6
|
|
2100
2135
|
/**
|
|
2101
2136
|
* Shadow map depth format — 16-bit, deliberately.
|
|
2102
2137
|
*
|
|
@@ -2490,6 +2525,9 @@ export class Engine {
|
|
|
2490
2525
|
/** Where addGround put the floor. The mirror reflects across it, so a raised
|
|
2491
2526
|
* floor that kept y = 0 would reflect the scene into the wrong plane. */
|
|
2492
2527
|
private groundY = 0
|
|
2528
|
+
/** The built-in floor's half size on x and z — what it covers, for the
|
|
2529
|
+
* shadow's reach (updateShadowSceneBounds). */
|
|
2530
|
+
private groundHalf: [number, number] = [0, 0]
|
|
2493
2531
|
private mirrorPlane = new Float32Array([0, 1, 0, 0])
|
|
2494
2532
|
private mirrorCameraData = new Float32Array(40)
|
|
2495
2533
|
private mirrorCameraBuffer!: GPUBuffer
|
|
@@ -2624,6 +2662,50 @@ export class Engine {
|
|
|
2624
2662
|
private worldAmbientSH: Float32Array | null = null
|
|
2625
2663
|
private fallbackEquirectTexture!: GPUTexture
|
|
2626
2664
|
private fallbackEquirectView!: GPUTextureView
|
|
2665
|
+
// ── The stage's reflection probe (captureReflectionProbe) ──
|
|
2666
|
+
/** The cube the materials read: 64² GGX-prefiltered down seven levels. */
|
|
2667
|
+
private probeTexture!: GPUTexture
|
|
2668
|
+
private probeView!: GPUTextureView
|
|
2669
|
+
/** 1×1 black cube, bound while a probe is being captured so no material
|
|
2670
|
+
* reads the cube it is drawing into. */
|
|
2671
|
+
private probeFallbackView!: GPUTextureView
|
|
2672
|
+
/** The spot lamps' cookies: one picture a layer of a 512² array, made on the
|
|
2673
|
+
* first cookie (a 1×1 white stand-in until then), and the layer each key holds. */
|
|
2674
|
+
private cookieTexture!: GPUTexture
|
|
2675
|
+
private cookieView!: GPUTextureView
|
|
2676
|
+
private cookieLayers = new Map<string, number>()
|
|
2677
|
+
/** What setLights was last given, rewritten when a cookie it names arrives. */
|
|
2678
|
+
private sceneLights: SceneLight[] | null = null
|
|
2679
|
+
private static readonly COOKIE_SIZE = 512
|
|
2680
|
+
private static readonly COOKIE_LAYERS = 16
|
|
2681
|
+
/** ProbeUniforms for the scene (centre + strength, box min + on-flag, box
|
|
2682
|
+
* max), and a zeroed copy — the off flag — for the capture's own draws. */
|
|
2683
|
+
private probeUniformBuffer!: GPUBuffer
|
|
2684
|
+
private probeOffBuffer!: GPUBuffer
|
|
2685
|
+
private probeData = new Float32Array(12)
|
|
2686
|
+
/** The capture camera: CameraUniforms for one cube face at a time. */
|
|
2687
|
+
private probeCameraBuffer!: GPUBuffer
|
|
2688
|
+
private probeCameraData = new Float32Array(40)
|
|
2689
|
+
private probeCaptureBindGroup: GPUBindGroup | null = null
|
|
2690
|
+
/** Set when the stage or what it wears changes; the capture runs once the
|
|
2691
|
+
* scene has stayed put for PROBE_SETTLE_FRAMES (see maybeCaptureProbe). */
|
|
2692
|
+
private probeDirty = false
|
|
2693
|
+
private probeDirtyFrame = 0
|
|
2694
|
+
private probeFrame = 0
|
|
2695
|
+
private probeCapture: {
|
|
2696
|
+
msTargets: GPUTexture[]
|
|
2697
|
+
depth: GPUTexture
|
|
2698
|
+
color: GPUTexture
|
|
2699
|
+
aux: GPUTexture
|
|
2700
|
+
cube: GPUTexture
|
|
2701
|
+
steps: GPUBuffer
|
|
2702
|
+
composePipeline: GPURenderPipeline
|
|
2703
|
+
filterPipeline: GPURenderPipeline
|
|
2704
|
+
composeGroups: GPUBindGroup[]
|
|
2705
|
+
filterGroups: GPUBindGroup[]
|
|
2706
|
+
} | null = null
|
|
2707
|
+
private static readonly PROBE_SETTLE_FRAMES = 20
|
|
2708
|
+
private skyPrefilterPipeline: GPURenderPipeline | null = null
|
|
2627
2709
|
// The scene's user WGSL effect (setEffect). ONE per scene, mounted under the
|
|
2628
2710
|
// scene, over it, or both — whichever of background()/foreground() the code
|
|
2629
2711
|
// defines. The composite pipelines are REBUILT with the user code injected;
|
|
@@ -2691,7 +2773,7 @@ export class Engine {
|
|
|
2691
2773
|
* shader's index 0 is now — written by unshifting rather than by tracking a
|
|
2692
2774
|
* head, because 64 is short and the alternative is an index the GPU side
|
|
2693
2775
|
* would also have to know about. */
|
|
2694
|
-
private anchorTrail = new Map<string, { pos: number[]; t: number[] }>()
|
|
2776
|
+
private anchorTrail = new Map<string, { pos: number[]; t: number[]; live?: boolean }>()
|
|
2695
2777
|
/** Scene seconds, advanced by the frame delta — NOT wall time, so an offline
|
|
2696
2778
|
* export samples the same path the editor showed. */
|
|
2697
2779
|
private sceneClock = 0
|
|
@@ -2806,8 +2888,6 @@ export class Engine {
|
|
|
2806
2888
|
/** The sun's shadow atlas: every cascade in its own tile (shadow-cascades.ts). */
|
|
2807
2889
|
private shadowAtlasTexture!: GPUTexture
|
|
2808
2890
|
private shadowAtlasView!: GPUTextureView
|
|
2809
|
-
private brdfLutTexture!: GPUTexture
|
|
2810
|
-
private brdfLutView!: GPUTextureView
|
|
2811
2891
|
private shadowDepthPipeline!: GPURenderPipeline
|
|
2812
2892
|
private shadowLightVPBuffer!: GPUBuffer
|
|
2813
2893
|
// The shadow PASS reads one cascade's matrix per pass, and a uniform binding
|
|
@@ -3075,8 +3155,8 @@ export class Engine {
|
|
|
3075
3155
|
}
|
|
3076
3156
|
}
|
|
3077
3157
|
|
|
3078
|
-
/** Sensor grain: how much
|
|
3079
|
-
private grain = { amount: 0
|
|
3158
|
+
/** Sensor grain: how much. */
|
|
3159
|
+
private grain = { amount: 0 }
|
|
3080
3160
|
|
|
3081
3161
|
/**
|
|
3082
3162
|
* Film grain over the rendered scene, 0–1.
|
|
@@ -3086,19 +3166,14 @@ export class Engine {
|
|
|
3086
3166
|
* a background image or a backdrop video, which arrived with grain of their
|
|
3087
3167
|
* own and would be graded rather than matched by a second helping.
|
|
3088
3168
|
*
|
|
3089
|
-
* `animated` false freezes it. A still photograph's grain does not move, and
|
|
3090
|
-
* noise crawling over a frozen picture makes the rendering look more alive
|
|
3091
|
-
* than the thing it is standing in.
|
|
3092
|
-
*
|
|
3093
3169
|
* Costs one hash per pixel in a pass that already runs, and nothing at all at
|
|
3094
3170
|
* zero — the branch is on a uniform.
|
|
3095
3171
|
*/
|
|
3096
|
-
setFilmGrain(amount: number
|
|
3172
|
+
setFilmGrain(amount: number): void {
|
|
3097
3173
|
this.grain.amount = Math.min(Math.max(amount, 0), 1)
|
|
3098
|
-
this.grain.animated = animated
|
|
3099
3174
|
if (this.device && this.compositeUniformBuffer) this.writeCompositeViewUniforms()
|
|
3100
3175
|
}
|
|
3101
|
-
getFilmGrain(): Readonly<{ amount: number
|
|
3176
|
+
getFilmGrain(): Readonly<{ amount: number }> {
|
|
3102
3177
|
return this.grain
|
|
3103
3178
|
}
|
|
3104
3179
|
|
|
@@ -3152,10 +3227,8 @@ export class Engine {
|
|
|
3152
3227
|
// a uniform branch that skips the pow entirely in the common case.
|
|
3153
3228
|
u[1] = 1.0 / Math.max(v.gamma, 1e-4)
|
|
3154
3229
|
u[2] = this.grain.amount
|
|
3155
|
-
// The seed
|
|
3156
|
-
|
|
3157
|
-
// look more alive than the footage — the opposite of the point.
|
|
3158
|
-
u[3] = this.grain.animated ? Math.floor(this.sceneClock * 24) % 1024 : 0
|
|
3230
|
+
// The seed, stepped at 24 a second like a film gate.
|
|
3231
|
+
u[3] = Math.floor(this.sceneClock * 24) % 1024
|
|
3159
3232
|
u[4] = b.color.x
|
|
3160
3233
|
u[5] = b.color.y
|
|
3161
3234
|
u[6] = b.color.z
|
|
@@ -3167,7 +3240,7 @@ export class Engine {
|
|
|
3167
3240
|
// exposure and view transform as the scene — a sun rolls off like a sun).
|
|
3168
3241
|
// The camera basis at u[12..23] is refreshed per frame.
|
|
3169
3242
|
// In modes 2 and 3 the colour slot is dead, so mode 3 carries the world
|
|
3170
|
-
// STRENGTH in u[8] —
|
|
3243
|
+
// STRENGTH in u[8] — the World's intensity, the SAME number that
|
|
3171
3244
|
// scales the irradiance. A sky you can see at full brightness while it
|
|
3172
3245
|
// lights at two thirds is two skies.
|
|
3173
3246
|
const bg = this.backgroundColor
|
|
@@ -4688,7 +4761,7 @@ export class Engine {
|
|
|
4688
4761
|
* whether or not it is the thing you see — and it IS the thing you see until
|
|
4689
4762
|
* a backdrop is set, which is what an HDRI on its own has always done.
|
|
4690
4763
|
*
|
|
4691
|
-
* `strength` is
|
|
4764
|
+
* `strength` is the World's intensity and is folded into the
|
|
4692
4765
|
* coefficients, so what lights her is what you see.
|
|
4693
4766
|
*/
|
|
4694
4767
|
setWorldEquirect(source: HdrImage | null): void {
|
|
@@ -4718,7 +4791,8 @@ export class Engine {
|
|
|
4718
4791
|
size: [source.width, source.height],
|
|
4719
4792
|
format: "rgba16float",
|
|
4720
4793
|
mipLevelCount: levels,
|
|
4721
|
-
|
|
4794
|
+
// COPY_SRC: prefilterSky copies its level 0 into the filtered chain.
|
|
4795
|
+
usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST | GPUTextureUsage.COPY_SRC,
|
|
4722
4796
|
})
|
|
4723
4797
|
let data = source.data
|
|
4724
4798
|
let w = source.width
|
|
@@ -4755,8 +4829,11 @@ export class Engine {
|
|
|
4755
4829
|
[w, h],
|
|
4756
4830
|
)
|
|
4757
4831
|
}
|
|
4758
|
-
|
|
4759
|
-
|
|
4832
|
+
// The box chain above is the SOURCE: the levels the materials read are
|
|
4833
|
+
// the GGX convolution of it, one roughness per level (prefilterSky).
|
|
4834
|
+
const filtered = this.prefilterSky(tex, source.width, source.height, levels)
|
|
4835
|
+
this.worldEquirectTexture = filtered
|
|
4836
|
+
this.worldEquirectView = filtered.createView()
|
|
4760
4837
|
// The sky lights the scene, not only backs it. The sun keeps the toon
|
|
4761
4838
|
// ramp — this is the ambient term, exactly where the flat world colour
|
|
4762
4839
|
// used to sit.
|
|
@@ -4776,6 +4853,9 @@ export class Engine {
|
|
|
4776
4853
|
this.worldBindingsDirty = true
|
|
4777
4854
|
this.rebuildPerFrameBindGroups()
|
|
4778
4855
|
this.rebuildCompositeBindGroup()
|
|
4856
|
+
// The probe's compose step binds the sky it lays in behind the stage.
|
|
4857
|
+
this.dropProbeCapture()
|
|
4858
|
+
this.captureReflectionProbe()
|
|
4779
4859
|
if (this.device && this.compositeUniformBuffer) this.writeCompositeViewUniforms()
|
|
4780
4860
|
}
|
|
4781
4861
|
|
|
@@ -4902,7 +4982,7 @@ export class Engine {
|
|
|
4902
4982
|
params: Record<string, EffectParamValue> | undefined,
|
|
4903
4983
|
/** This effect's own declarations, already parsed by the caller — which had
|
|
4904
4984
|
* to read them anyway to build the scene table. */
|
|
4905
|
-
anchors: { bone: string; trail: boolean }[],
|
|
4985
|
+
anchors: { bone: string; trail: boolean; along?: number; step?: number }[],
|
|
4906
4986
|
/** Its row of that table: local slot → scene slot. */
|
|
4907
4987
|
alias: number[],
|
|
4908
4988
|
/** The pictures for `#textures`, as the host passed them. */
|
|
@@ -5574,7 +5654,7 @@ export class Engine {
|
|
|
5574
5654
|
/** What the file declared. Read here rather than re-parsed: the source no
|
|
5575
5655
|
* longer carries the lines, and two readers is how they drift. */
|
|
5576
5656
|
d: EffectDirectives,
|
|
5577
|
-
anchors: { bone: string; trail: boolean }[],
|
|
5657
|
+
anchors: { bone: string; trail: boolean; along?: number; step?: number }[],
|
|
5578
5658
|
/** This effect's local→scene slot map. Passed rather than read off the
|
|
5579
5659
|
* engine: the builders run BEFORE the swap, so this.anchorTable still
|
|
5580
5660
|
* describes the effect that is still on screen. */
|
|
@@ -5862,7 +5942,11 @@ export class Engine {
|
|
|
5862
5942
|
// shading ten layers and shading about three.
|
|
5863
5943
|
depthStencil: prepass
|
|
5864
5944
|
? { format: this.depthFormat, depthWriteEnabled: false, depthCompare: "equal" }
|
|
5865
|
-
:
|
|
5945
|
+
: d.depthAlways
|
|
5946
|
+
? // `#depth always`: over the scene, Unity's ZTest Always (a moon's halo
|
|
5947
|
+
// drawn in front of the sky dome it sits behind)
|
|
5948
|
+
{ format: this.depthFormat, depthWriteEnabled: false, depthCompare: "always" }
|
|
5949
|
+
: { format: this.depthFormat, depthWriteEnabled: cutout, depthCompare: this.depthAhead },
|
|
5866
5950
|
multisample: { count: Engine.MULTISAMPLE_COUNT },
|
|
5867
5951
|
}),
|
|
5868
5952
|
])
|
|
@@ -5946,7 +6030,7 @@ export class Engine {
|
|
|
5946
6030
|
wgsl: string,
|
|
5947
6031
|
count: number,
|
|
5948
6032
|
alias: number[],
|
|
5949
|
-
anchors: { bone: string; trail: boolean }[],
|
|
6033
|
+
anchors: { bone: string; trail: boolean; along?: number; step?: number }[],
|
|
5950
6034
|
params: EffectParamsBinding,
|
|
5951
6035
|
): Promise<{ ok: true; state: NonNullable<EffectInstance["lights"]> } | { ok: false; diagnostics: string[] }> {
|
|
5952
6036
|
const layout = this.device.createBindGroupLayout({
|
|
@@ -6200,7 +6284,7 @@ export class Engine {
|
|
|
6200
6284
|
/** What the file declared. Read here rather than re-parsed: the source no
|
|
6201
6285
|
* longer carries the lines, and two readers is how they drift. */
|
|
6202
6286
|
d: EffectDirectives,
|
|
6203
|
-
anchors: { bone: string; trail: boolean }[],
|
|
6287
|
+
anchors: { bone: string; trail: boolean; along?: number; step?: number }[],
|
|
6204
6288
|
/** This effect's local→scene slot map. Passed rather than read off the
|
|
6205
6289
|
* engine: the builders run BEFORE the swap, so this.anchorTable still
|
|
6206
6290
|
* describes the effect that is still on screen. */
|
|
@@ -6290,8 +6374,10 @@ export class Engine {
|
|
|
6290
6374
|
// point: a layer composited after tone mapping can never bloom.
|
|
6291
6375
|
//
|
|
6292
6376
|
// Additive, where this used to be MAX. Max was right for a post-tonemap
|
|
6293
|
-
// layer; in HDR before bloom, overlapping light sums.
|
|
6294
|
-
|
|
6377
|
+
// layer; in HDR before bloom, overlapping light sums. `#blend over` lays the
|
|
6378
|
+
// ribbon over the scene by its alpha instead — a game trail's own blend,
|
|
6379
|
+
// under which its dark smoke darkens rather than vanishing.
|
|
6380
|
+
const targets = sceneTargetsFor(d.trailBlend === "over" ? "trail-over" : "trail", this.sceneFormats)
|
|
6295
6381
|
this.device.pushErrorScope("validation")
|
|
6296
6382
|
try {
|
|
6297
6383
|
const pipeline = await this.device.createRenderPipelineAsync({
|
|
@@ -6709,7 +6795,7 @@ export class Engine {
|
|
|
6709
6795
|
/** What the file declared. Read here rather than re-parsed: the source no
|
|
6710
6796
|
* longer carries the lines, and two readers is how they drift. */
|
|
6711
6797
|
d: EffectDirectives,
|
|
6712
|
-
anchors: { bone: string; trail: boolean }[],
|
|
6798
|
+
anchors: { bone: string; trail: boolean; along?: number; step?: number }[],
|
|
6713
6799
|
/** This effect's local→scene slot map. Passed rather than read off the
|
|
6714
6800
|
* engine: the builders run BEFORE the swap, so this.anchorTable still
|
|
6715
6801
|
* describes the effect that is still on screen. */
|
|
@@ -7304,7 +7390,9 @@ export class Engine {
|
|
|
7304
7390
|
u[0] = Math.max(0, b.threshold)
|
|
7305
7391
|
u[1] = Math.max(0, b.threshold) * 0.5
|
|
7306
7392
|
u[2] = Engine.BLOOM_CLAMP
|
|
7307
|
-
|
|
7393
|
+
// URP's mapping: never all the way to the widest level, which alone is a
|
|
7394
|
+
// few pixels smeared over the screen — the glow would vanish.
|
|
7395
|
+
u[3] = 0.05 + 0.9 * Math.min(1, Math.max(0, b.scatter))
|
|
7308
7396
|
this.device.queue.writeBuffer(this.bloomUniformBuffer, 0, u)
|
|
7309
7397
|
}
|
|
7310
7398
|
|
|
@@ -7528,81 +7616,6 @@ export class Engine {
|
|
|
7528
7616
|
this.device.queue.submit([encoder.finish()])
|
|
7529
7617
|
}
|
|
7530
7618
|
|
|
7531
|
-
private bakeBrdfLut() {
|
|
7532
|
-
if (BRDF_LUT_SIZE !== LTC_MAG_LUT_SIZE) {
|
|
7533
|
-
throw new Error("BRDF LUT bake requires DFG size == LTC size (both 64).")
|
|
7534
|
-
}
|
|
7535
|
-
|
|
7536
|
-
// Temp rg16float LTC source — loaded 1:1 by the bake fragment shader, then dropped.
|
|
7537
|
-
const ltcTemp = this.device.createTexture({
|
|
7538
|
-
label: "LTC mag LUT (bake input)",
|
|
7539
|
-
size: [LTC_MAG_LUT_SIZE, LTC_MAG_LUT_SIZE],
|
|
7540
|
-
format: "rg16float",
|
|
7541
|
-
usage: GPUTextureUsage.COPY_DST | GPUTextureUsage.TEXTURE_BINDING,
|
|
7542
|
-
})
|
|
7543
|
-
const n = LTC_MAG_LUT_DATA.length
|
|
7544
|
-
const half = new Uint16Array(n)
|
|
7545
|
-
const f32 = new Float32Array(1)
|
|
7546
|
-
const u32 = new Uint32Array(f32.buffer)
|
|
7547
|
-
for (let i = 0; i < n; i++) {
|
|
7548
|
-
f32[0] = LTC_MAG_LUT_DATA[i]
|
|
7549
|
-
const x = u32[0]
|
|
7550
|
-
const sign = (x >>> 16) & 0x8000
|
|
7551
|
-
let exp = ((x >>> 23) & 0xff) - 127 + 15
|
|
7552
|
-
const mant = x & 0x7fffff
|
|
7553
|
-
if (exp <= 0) {
|
|
7554
|
-
half[i] = sign
|
|
7555
|
-
} else if (exp >= 31) {
|
|
7556
|
-
half[i] = sign | 0x7c00
|
|
7557
|
-
} else {
|
|
7558
|
-
half[i] = sign | (exp << 10) | (mant >>> 13)
|
|
7559
|
-
}
|
|
7560
|
-
}
|
|
7561
|
-
this.device.queue.writeTexture(
|
|
7562
|
-
{ texture: ltcTemp },
|
|
7563
|
-
half,
|
|
7564
|
-
{ bytesPerRow: LTC_MAG_LUT_SIZE * 4, rowsPerImage: LTC_MAG_LUT_SIZE },
|
|
7565
|
-
{ width: LTC_MAG_LUT_SIZE, height: LTC_MAG_LUT_SIZE, depthOrArrayLayers: 1 },
|
|
7566
|
-
)
|
|
7567
|
-
|
|
7568
|
-
this.brdfLutTexture = this.device.createTexture({
|
|
7569
|
-
label: "BRDF LUT (DFG + LTC packed)",
|
|
7570
|
-
size: [BRDF_LUT_SIZE, BRDF_LUT_SIZE],
|
|
7571
|
-
format: "rgba8unorm",
|
|
7572
|
-
usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.TEXTURE_BINDING,
|
|
7573
|
-
})
|
|
7574
|
-
this.brdfLutView = this.brdfLutTexture.createView()
|
|
7575
|
-
|
|
7576
|
-
const module = this.device.createShaderModule({ label: "BRDF LUT bake", code: BRDF_LUT_BAKE_WGSL })
|
|
7577
|
-
const pipeline = this.device.createRenderPipeline({
|
|
7578
|
-
label: "BRDF LUT bake pipeline",
|
|
7579
|
-
layout: "auto",
|
|
7580
|
-
vertex: { module, entryPoint: "vs" },
|
|
7581
|
-
fragment: { module, entryPoint: "fs", targets: [{ format: "rgba8unorm" }] },
|
|
7582
|
-
primitive: { topology: "triangle-list" },
|
|
7583
|
-
})
|
|
7584
|
-
|
|
7585
|
-
const bakeBindGroup = this.device.createBindGroup({
|
|
7586
|
-
label: "BRDF LUT bake bind group",
|
|
7587
|
-
layout: pipeline.getBindGroupLayout(0),
|
|
7588
|
-
entries: [{ binding: 0, resource: ltcTemp.createView() }],
|
|
7589
|
-
})
|
|
7590
|
-
|
|
7591
|
-
const enc = this.device.createCommandEncoder({ label: "BRDF LUT bake encoder" })
|
|
7592
|
-
const pass = enc.beginRenderPass({
|
|
7593
|
-
colorAttachments: [
|
|
7594
|
-
{ view: this.brdfLutView, clearValue: { r: 0, g: 0, b: 0, a: 1 }, loadOp: "clear", storeOp: "store" },
|
|
7595
|
-
],
|
|
7596
|
-
})
|
|
7597
|
-
pass.setPipeline(pipeline)
|
|
7598
|
-
pass.setBindGroup(0, bakeBindGroup)
|
|
7599
|
-
pass.draw(3, 1, 0, 0)
|
|
7600
|
-
pass.end()
|
|
7601
|
-
this.device.queue.submit([enc.finish()])
|
|
7602
|
-
|
|
7603
|
-
ltcTemp.destroy()
|
|
7604
|
-
}
|
|
7605
|
-
|
|
7606
7619
|
private createRenderPipeline(config: Parameters<Engine["renderPipelineDesc"]>[0]): GPURenderPipeline {
|
|
7607
7620
|
return this.createModelPipeline(this.renderPipelineDesc(config))
|
|
7608
7621
|
}
|
|
@@ -7611,7 +7624,7 @@ export class Engine {
|
|
|
7611
7624
|
label: string
|
|
7612
7625
|
layout: GPUPipelineLayout
|
|
7613
7626
|
shaderModule: GPUShaderModule
|
|
7614
|
-
vertexBuffers: GPUVertexBufferLayout[]
|
|
7627
|
+
vertexBuffers: (GPUVertexBufferLayout | null)[]
|
|
7615
7628
|
fragmentTarget?: GPUColorTargetState
|
|
7616
7629
|
fragmentTargets?: GPUColorTargetState[]
|
|
7617
7630
|
fragmentEntryPoint?: string
|
|
@@ -7928,7 +7941,22 @@ export class Engine {
|
|
|
7928
7941
|
// The same attachments blended as light — for a group that declared
|
|
7929
7942
|
// blend: "additive". See scene-contract's material-additive.
|
|
7930
7943
|
this.sceneTargetsAdditive = sceneTargetsFor("material-additive", this.sceneFormats)
|
|
7944
|
+
this.sceneTargetsPremultiplied = sceneTargetsFor("material-premultiplied", this.sceneFormats)
|
|
7931
7945
|
this.fullVertexBufferLayouts = fullVertexBuffers
|
|
7946
|
+
// Slot 3 stays empty: the outline hull binds its own stream there between
|
|
7947
|
+
// two material draws, and a material reading slot 3 would pick it up.
|
|
7948
|
+
const materialVertexBuffers: (GPUVertexBufferLayout | null)[] = [
|
|
7949
|
+
...fullVertexBuffers,
|
|
7950
|
+
null,
|
|
7951
|
+
{
|
|
7952
|
+
arrayStride: Engine.ATTRIBUTE_FLOATS * 4,
|
|
7953
|
+
attributes: [
|
|
7954
|
+
{ shaderLocation: 5, offset: 0, format: "float32x4" as GPUVertexFormat },
|
|
7955
|
+
{ shaderLocation: 6, offset: 4 * 4, format: "float32x2" as GPUVertexFormat },
|
|
7956
|
+
],
|
|
7957
|
+
},
|
|
7958
|
+
]
|
|
7959
|
+
this.materialVertexBufferLayouts = materialVertexBuffers
|
|
7932
7960
|
|
|
7933
7961
|
// group 0: per-frame (camera + light + sampler + shadow) — bound once per pass
|
|
7934
7962
|
this.mainPerFrameBindGroupLayout = this.device.createBindGroupLayout({
|
|
@@ -7944,14 +7972,19 @@ export class Engine {
|
|
|
7944
7972
|
// The positional lights. Always bound, empty or not, so every material
|
|
7945
7973
|
// pipeline shares one layout whether or not the scene has any.
|
|
7946
7974
|
{ binding: 6, visibility: GPUShaderStage.FRAGMENT, buffer: { type: "read-only-storage" } },
|
|
7947
|
-
// The world's sky (8)
|
|
7948
|
-
//
|
|
7949
|
-
//
|
|
7950
|
-
// which the atlas at 3 replaces.)
|
|
7975
|
+
// The world's sky (8). Always bound — the 1x1 fallback when the scene
|
|
7976
|
+
// has none — so every material pipeline keeps sharing one layout. (7
|
|
7977
|
+
// was the far cascade's map, which the atlas at 3 replaces.)
|
|
7951
7978
|
{ binding: 8, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: "float" } },
|
|
7952
|
-
|
|
7979
|
+
// The spot lamps' cookies (loadLightCookie), a 1x1 white until one loads.
|
|
7980
|
+
{ binding: 9, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: "float", viewDimension: "2d-array" } },
|
|
7953
7981
|
// The cast's shadow on a stage — setStageCastShadow.
|
|
7954
7982
|
{ binding: 10, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: "depth" } },
|
|
7983
|
+
// The stage's reflection probe and where it projects — see
|
|
7984
|
+
// captureReflectionProbe. Always bound: a 1x1 cube and the off flag
|
|
7985
|
+
// when there is no stage.
|
|
7986
|
+
{ binding: 11, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: "float", viewDimension: "cube" } },
|
|
7987
|
+
{ binding: 12, visibility: GPUShaderStage.FRAGMENT, buffer: { type: "uniform" } },
|
|
7955
7988
|
],
|
|
7956
7989
|
})
|
|
7957
7990
|
// group 1: per-instance (skinMats) — bound once per model
|
|
@@ -8024,7 +8057,7 @@ export class Engine {
|
|
|
8024
8057
|
label: "neutral base pipeline",
|
|
8025
8058
|
layout: mainPipelineLayout,
|
|
8026
8059
|
shaderModule: neutralModule,
|
|
8027
|
-
vertexBuffers:
|
|
8060
|
+
vertexBuffers: materialVertexBuffers,
|
|
8028
8061
|
fragmentTargets: sceneTargets,
|
|
8029
8062
|
cullMode: "none",
|
|
8030
8063
|
depthStencil: {
|
|
@@ -8042,7 +8075,7 @@ export class Engine {
|
|
|
8042
8075
|
label: "neutral base pipeline (no depth write)",
|
|
8043
8076
|
layout: mainPipelineLayout,
|
|
8044
8077
|
shaderModule: neutralModule,
|
|
8045
|
-
vertexBuffers:
|
|
8078
|
+
vertexBuffers: materialVertexBuffers,
|
|
8046
8079
|
fragmentTargets: sceneTargets,
|
|
8047
8080
|
cullMode: "none",
|
|
8048
8081
|
depthStencil: {
|
|
@@ -8212,7 +8245,6 @@ export class Engine {
|
|
|
8212
8245
|
})
|
|
8213
8246
|
|
|
8214
8247
|
// One-shot bake of Blender EEVEE's combined BRDF LUT (DFG + LTC packed rgba8unorm).
|
|
8215
|
-
this.bakeBrdfLut()
|
|
8216
8248
|
this.bakeGroundNoise()
|
|
8217
8249
|
// The mipmap blit for the formats model textures arrive in, built here so
|
|
8218
8250
|
// the first texture of the first model does not compile it synchronously
|
|
@@ -8269,6 +8301,48 @@ export class Engine {
|
|
|
8269
8301
|
usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST,
|
|
8270
8302
|
})
|
|
8271
8303
|
this.fallbackEquirectView = this.fallbackEquirectTexture.createView()
|
|
8304
|
+
// The reflection probe's resources, bound from the first frame: the cube
|
|
8305
|
+
// (empty and switched off until a stage is captured), its stand-in and the
|
|
8306
|
+
// uniforms. Before the per-frame groups, which bind all of them.
|
|
8307
|
+
this.probeTexture = this.device.createTexture({
|
|
8308
|
+
label: "reflection probe",
|
|
8309
|
+
size: [PROBE_SIZE, PROBE_SIZE, 6],
|
|
8310
|
+
format: "rgba16float",
|
|
8311
|
+
mipLevelCount: PROBE_LEVELS,
|
|
8312
|
+
usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.RENDER_ATTACHMENT,
|
|
8313
|
+
})
|
|
8314
|
+
this.probeView = this.probeTexture.createView({ dimension: "cube" })
|
|
8315
|
+
this.probeFallbackView = this.device
|
|
8316
|
+
.createTexture({
|
|
8317
|
+
label: "reflection probe stand-in",
|
|
8318
|
+
size: [1, 1, 6],
|
|
8319
|
+
format: "rgba16float",
|
|
8320
|
+
usage: GPUTextureUsage.TEXTURE_BINDING,
|
|
8321
|
+
})
|
|
8322
|
+
.createView({ dimension: "cube" })
|
|
8323
|
+
this.cookieTexture = this.device.createTexture({
|
|
8324
|
+
label: "light cookies (none yet)",
|
|
8325
|
+
size: [1, 1, 1],
|
|
8326
|
+
format: "rgba8unorm",
|
|
8327
|
+
usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST,
|
|
8328
|
+
})
|
|
8329
|
+
this.device.queue.writeTexture({ texture: this.cookieTexture }, new Uint8Array([255, 255, 255, 255]), { bytesPerRow: 4 }, [1, 1, 1])
|
|
8330
|
+
this.cookieView = this.cookieTexture.createView({ dimension: "2d-array" })
|
|
8331
|
+
this.probeUniformBuffer = this.device.createBuffer({
|
|
8332
|
+
label: "reflection probe uniforms",
|
|
8333
|
+
size: 48,
|
|
8334
|
+
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
|
|
8335
|
+
})
|
|
8336
|
+
this.probeOffBuffer = this.device.createBuffer({
|
|
8337
|
+
label: "reflection probe uniforms (off)",
|
|
8338
|
+
size: 48,
|
|
8339
|
+
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
|
|
8340
|
+
})
|
|
8341
|
+
this.probeCameraBuffer = this.device.createBuffer({
|
|
8342
|
+
label: "reflection probe capture camera",
|
|
8343
|
+
size: 40 * 4,
|
|
8344
|
+
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
|
|
8345
|
+
})
|
|
8272
8346
|
|
|
8273
8347
|
// Zero-filled = zero lines, which every accessor answers gracefully.
|
|
8274
8348
|
this.lyricsBuffer = this.device.createBuffer({
|
|
@@ -8316,6 +8390,8 @@ export class Engine {
|
|
|
8316
8390
|
{ binding: 13, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: "float" } },
|
|
8317
8391
|
// The mirror's plane, and whether this draw is inside the mirror pass.
|
|
8318
8392
|
{ binding: 14, visibility: GPUShaderStage.FRAGMENT, buffer: { type: "uniform" } },
|
|
8393
|
+
// The spot lamps' cookies — the floor takes their patterns too.
|
|
8394
|
+
{ binding: 15, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: "float", viewDimension: "2d-array" } },
|
|
8319
8395
|
],
|
|
8320
8396
|
})
|
|
8321
8397
|
this.groundShadowPipelineDesc = {
|
|
@@ -10391,6 +10467,415 @@ export class Engine {
|
|
|
10391
10467
|
* Retried at the top of a frame until one of them takes. */
|
|
10392
10468
|
private worldBindingsDirty = false
|
|
10393
10469
|
|
|
10470
|
+
// ─── The stage's reflection probe ───────────────────────────────────
|
|
10471
|
+
//
|
|
10472
|
+
// What a game bakes for every room: a cube of the stage itself, seen from
|
|
10473
|
+
// its middle, prefiltered down its mips by the specular lobe, and read by
|
|
10474
|
+
// every surface box-projected onto the stage's bounds — so a lacquered
|
|
10475
|
+
// counter reflects the bottles behind it and a wet floor the lamps above,
|
|
10476
|
+
// not a sky the room has no window onto. Captured here automatically once a
|
|
10477
|
+
// stage has loaded and its looks have settled; nothing to set.
|
|
10478
|
+
|
|
10479
|
+
/** Drop the capture's resources, so the next capture rebinds the sky. */
|
|
10480
|
+
private dropProbeCapture(): void {
|
|
10481
|
+
const cap = this.probeCapture
|
|
10482
|
+
if (!cap) return
|
|
10483
|
+
this.probeCapture = null
|
|
10484
|
+
void this.device.queue.onSubmittedWorkDone().then(() => {
|
|
10485
|
+
for (const t of cap.msTargets) t.destroy()
|
|
10486
|
+
cap.depth.destroy()
|
|
10487
|
+
cap.color.destroy()
|
|
10488
|
+
cap.aux.destroy()
|
|
10489
|
+
cap.cube.destroy()
|
|
10490
|
+
cap.steps.destroy()
|
|
10491
|
+
})
|
|
10492
|
+
}
|
|
10493
|
+
|
|
10494
|
+
/** Ask for a fresh capture of the stage's reflection probe. It runs at the
|
|
10495
|
+
* start of a frame once the scene has held still for a moment — asked for
|
|
10496
|
+
* by the engine itself whenever the stage or its looks change; a host may
|
|
10497
|
+
* call it after changing something the engine cannot see. */
|
|
10498
|
+
captureReflectionProbe(): void {
|
|
10499
|
+
if (!this.probeDirty) this.probeDirtyFrame = this.probeFrame
|
|
10500
|
+
this.probeDirty = true
|
|
10501
|
+
}
|
|
10502
|
+
|
|
10503
|
+
/** The stage's world-space bounds: every stage model's material boxes
|
|
10504
|
+
* through its placement. Null without a stage. */
|
|
10505
|
+
private stageBounds(): { min: [number, number, number]; max: [number, number, number] } | null {
|
|
10506
|
+
const min: [number, number, number] = [Infinity, Infinity, Infinity]
|
|
10507
|
+
const max: [number, number, number] = [-Infinity, -Infinity, -Infinity]
|
|
10508
|
+
const p = new Vec3(0, 0, 0)
|
|
10509
|
+
let any = false
|
|
10510
|
+
for (const inst of this.modelInstances.values()) {
|
|
10511
|
+
if (!inst.isStage || !inst.model.visible) continue
|
|
10512
|
+
const m = inst.model
|
|
10513
|
+
for (const draw of inst.drawCalls) {
|
|
10514
|
+
const b = draw.bounds
|
|
10515
|
+
if (!b || b.length < 6 || !(b[0] <= b[3])) continue
|
|
10516
|
+
// The eight corners of the material's model-space box, placed.
|
|
10517
|
+
for (let k = 0; k < 8; k++) {
|
|
10518
|
+
p.setXYZ((k & 1 ? b[3] : b[0]) * m.scale, (k & 2 ? b[4] : b[1]) * m.scale, (k & 4 ? b[5] : b[2]) * m.scale)
|
|
10519
|
+
Quat.rotateVecInto(m.rotation, p, p)
|
|
10520
|
+
const w = [p.x + m.position.x, p.y + m.position.y, p.z + m.position.z]
|
|
10521
|
+
for (let a = 0; a < 3; a++) {
|
|
10522
|
+
if (w[a] < min[a]) min[a] = w[a]
|
|
10523
|
+
if (w[a] > max[a]) max[a] = w[a]
|
|
10524
|
+
}
|
|
10525
|
+
any = true
|
|
10526
|
+
}
|
|
10527
|
+
}
|
|
10528
|
+
}
|
|
10529
|
+
return any ? { min, max } : null
|
|
10530
|
+
}
|
|
10531
|
+
|
|
10532
|
+
/** Run a pending capture once the scene has settled — see captureReflectionProbe. */
|
|
10533
|
+
private maybeCaptureProbe(): void {
|
|
10534
|
+
this.probeFrame++
|
|
10535
|
+
if (!this.probeDirty || !this.probeCaptureBindGroup) return
|
|
10536
|
+
if (this.probeFrame - this.probeDirtyFrame < Engine.PROBE_SETTLE_FRAMES) return
|
|
10537
|
+
this.probeDirty = false
|
|
10538
|
+
const bounds = this.stageBounds()
|
|
10539
|
+
if (!bounds) {
|
|
10540
|
+
// No stage: the probe goes dark and every reader falls back to the sky.
|
|
10541
|
+
this.probeData.fill(0)
|
|
10542
|
+
this.device.queue.writeBuffer(this.probeUniformBuffer, 0, this.probeData)
|
|
10543
|
+
return
|
|
10544
|
+
}
|
|
10545
|
+
try {
|
|
10546
|
+
this.renderProbe(bounds)
|
|
10547
|
+
} catch (e) {
|
|
10548
|
+
console.warn("[reze-engine] reflection probe capture failed:", e)
|
|
10549
|
+
}
|
|
10550
|
+
}
|
|
10551
|
+
|
|
10552
|
+
private ensureProbeCapture(): NonNullable<Engine["probeCapture"]> {
|
|
10553
|
+
if (this.probeCapture) return this.probeCapture
|
|
10554
|
+
const size = PROBE_CAPTURE_SIZE
|
|
10555
|
+
const formats = sceneColorFormats(this.sceneFormats)
|
|
10556
|
+
const msTargets = formats.map((format, i) =>
|
|
10557
|
+
this.device.createTexture({
|
|
10558
|
+
label: `reflection probe capture target ${i}`,
|
|
10559
|
+
size: [size, size],
|
|
10560
|
+
sampleCount: Engine.MULTISAMPLE_COUNT,
|
|
10561
|
+
format,
|
|
10562
|
+
usage: GPUTextureUsage.RENDER_ATTACHMENT,
|
|
10563
|
+
}),
|
|
10564
|
+
)
|
|
10565
|
+
const depth = this.device.createTexture({
|
|
10566
|
+
label: "reflection probe capture depth",
|
|
10567
|
+
size: [size, size],
|
|
10568
|
+
sampleCount: Engine.MULTISAMPLE_COUNT,
|
|
10569
|
+
format: this.depthFormat,
|
|
10570
|
+
usage: GPUTextureUsage.RENDER_ATTACHMENT,
|
|
10571
|
+
})
|
|
10572
|
+
const layered = (label: string, format: GPUTextureFormat) =>
|
|
10573
|
+
this.device.createTexture({
|
|
10574
|
+
label,
|
|
10575
|
+
size: [size, size, 6],
|
|
10576
|
+
format,
|
|
10577
|
+
usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.TEXTURE_BINDING,
|
|
10578
|
+
})
|
|
10579
|
+
// The resolves take the scene pass's own formats: colour and coverage.
|
|
10580
|
+
const color = layered("reflection probe capture colour", formats[0])
|
|
10581
|
+
const aux = layered("reflection probe capture coverage", formats[1])
|
|
10582
|
+
const cube = this.device.createTexture({
|
|
10583
|
+
label: "reflection probe capture cube",
|
|
10584
|
+
size: [size, size, 6],
|
|
10585
|
+
format: "rgba16float",
|
|
10586
|
+
mipLevelCount: PROBE_CAPTURE_LEVELS,
|
|
10587
|
+
usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.TEXTURE_BINDING,
|
|
10588
|
+
})
|
|
10589
|
+
// One 256-byte slot per step: 6 composes, then the capture's mips, then
|
|
10590
|
+
// the probe's levels, six faces each.
|
|
10591
|
+
const slots = 6 + PROBE_CAPTURE_LEVELS * 6 + PROBE_LEVELS * 6
|
|
10592
|
+
const steps = this.device.createBuffer({
|
|
10593
|
+
label: "reflection probe steps",
|
|
10594
|
+
size: slots * 256,
|
|
10595
|
+
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
|
|
10596
|
+
})
|
|
10597
|
+
const composeModule = this.device.createShaderModule({ label: "reflection probe compose", code: PROBE_COMPOSE_WGSL })
|
|
10598
|
+
const composePipeline = this.device.createRenderPipeline({
|
|
10599
|
+
label: "reflection probe compose",
|
|
10600
|
+
layout: "auto",
|
|
10601
|
+
vertex: { module: composeModule, entryPoint: "vs_full" },
|
|
10602
|
+
fragment: { module: composeModule, entryPoint: "fs_compose", targets: [{ format: "rgba16float" }] },
|
|
10603
|
+
primitive: { topology: "triangle-list" },
|
|
10604
|
+
})
|
|
10605
|
+
const filterModule = this.device.createShaderModule({ label: "reflection probe filter", code: PROBE_FILTER_WGSL })
|
|
10606
|
+
const filterPipeline = this.device.createRenderPipeline({
|
|
10607
|
+
label: "reflection probe filter",
|
|
10608
|
+
layout: "auto",
|
|
10609
|
+
vertex: { module: filterModule, entryPoint: "vs_full" },
|
|
10610
|
+
fragment: { module: filterModule, entryPoint: "fs_filter", targets: [{ format: "rgba16float" }] },
|
|
10611
|
+
primitive: { topology: "triangle-list" },
|
|
10612
|
+
})
|
|
10613
|
+
const colorArray = color.createView({ dimension: "2d-array" })
|
|
10614
|
+
const auxArray = aux.createView({ dimension: "2d-array" })
|
|
10615
|
+
const composeLayout = composePipeline.getBindGroupLayout(0)
|
|
10616
|
+
const composeGroups: GPUBindGroup[] = []
|
|
10617
|
+
for (let f = 0; f < 6; f++) {
|
|
10618
|
+
composeGroups.push(
|
|
10619
|
+
this.device.createBindGroup({
|
|
10620
|
+
label: `reflection probe compose ${f}`,
|
|
10621
|
+
layout: composeLayout,
|
|
10622
|
+
entries: [
|
|
10623
|
+
{ binding: 1, resource: { buffer: this.lightUniformBuffer } },
|
|
10624
|
+
{ binding: 2, resource: this.materialSampler },
|
|
10625
|
+
{ binding: 8, resource: this.worldEquirectView ?? this.fallbackEquirectView },
|
|
10626
|
+
{ binding: 20, resource: colorArray },
|
|
10627
|
+
{ binding: 21, resource: auxArray },
|
|
10628
|
+
{ binding: 22, resource: { buffer: steps, offset: f * 256, size: 32 } },
|
|
10629
|
+
],
|
|
10630
|
+
}),
|
|
10631
|
+
)
|
|
10632
|
+
}
|
|
10633
|
+
const filterLayout = filterPipeline.getBindGroupLayout(0)
|
|
10634
|
+
const cubeView = cube.createView({ dimension: "cube" })
|
|
10635
|
+
const filterGroups: GPUBindGroup[] = []
|
|
10636
|
+
for (let i = 0; i < PROBE_LEVELS * 6; i++) {
|
|
10637
|
+
filterGroups.push(
|
|
10638
|
+
this.device.createBindGroup({
|
|
10639
|
+
label: `reflection probe level ${i}`,
|
|
10640
|
+
layout: filterLayout,
|
|
10641
|
+
entries: [
|
|
10642
|
+
{ binding: 0, resource: cubeView },
|
|
10643
|
+
{ binding: 1, resource: this.materialSampler },
|
|
10644
|
+
{ binding: 2, resource: { buffer: steps, offset: (6 + PROBE_CAPTURE_LEVELS * 6 + i) * 256, size: 32 } },
|
|
10645
|
+
],
|
|
10646
|
+
}),
|
|
10647
|
+
)
|
|
10648
|
+
}
|
|
10649
|
+
this.probeCapture = {
|
|
10650
|
+
msTargets,
|
|
10651
|
+
depth,
|
|
10652
|
+
color,
|
|
10653
|
+
aux,
|
|
10654
|
+
cube,
|
|
10655
|
+
steps,
|
|
10656
|
+
composePipeline,
|
|
10657
|
+
filterPipeline,
|
|
10658
|
+
composeGroups,
|
|
10659
|
+
filterGroups,
|
|
10660
|
+
}
|
|
10661
|
+
return this.probeCapture
|
|
10662
|
+
}
|
|
10663
|
+
|
|
10664
|
+
/**
|
|
10665
|
+
* Capture the probe: the stage drawn six times from one point into a 128²
|
|
10666
|
+
* cube, the sky laid in behind it, a mip chain, and the GGX convolution
|
|
10667
|
+
* into the 64² cube the materials read.
|
|
10668
|
+
*
|
|
10669
|
+
* WHERE: the middle of the stage's bounds across the floor, at about a
|
|
10670
|
+
* character's chest height above the floor (y = 0 where the bounds span it,
|
|
10671
|
+
* the MMD convention, else their bottom) — where a game puts the probe of a
|
|
10672
|
+
* room the player stands in. The box it projects onto is the bounds.
|
|
10673
|
+
*
|
|
10674
|
+
* WHAT: the stage alone, in its own looks, lit by the scene's sun, lamps
|
|
10675
|
+
* and sky as they stand. The cast is not in it (a baked probe never holds
|
|
10676
|
+
* the characters), nor are effects.
|
|
10677
|
+
*/
|
|
10678
|
+
private renderProbe(bounds: { min: [number, number, number]; max: [number, number, number] }): void {
|
|
10679
|
+
const cap = this.ensureProbeCapture()
|
|
10680
|
+
const { min, max } = bounds
|
|
10681
|
+
const floor = min[1] <= 0 && max[1] >= 0 ? 0 : min[1]
|
|
10682
|
+
const eye: [number, number, number] = [
|
|
10683
|
+
(min[0] + max[0]) * 0.5,
|
|
10684
|
+
Math.min(floor + 12, (min[1] + max[1]) * 0.5),
|
|
10685
|
+
(min[2] + max[2]) * 0.5,
|
|
10686
|
+
]
|
|
10687
|
+
const span = Math.hypot(max[0] - min[0], max[1] - min[1], max[2] - min[2])
|
|
10688
|
+
const near = 0.25
|
|
10689
|
+
const far = Math.max(span * 2, 100)
|
|
10690
|
+
// Each face's forward and up, in the convention the GPU samples a cube
|
|
10691
|
+
// with (see rz_face_dir in passes/probe.ts): +X, -X, +Y, -Y, +Z, -Z.
|
|
10692
|
+
const faces: [number, number, number, number, number, number][] = [
|
|
10693
|
+
[1, 0, 0, 0, 1, 0],
|
|
10694
|
+
[-1, 0, 0, 0, 1, 0],
|
|
10695
|
+
[0, 1, 0, 0, 0, -1],
|
|
10696
|
+
[0, -1, 0, 0, 0, 1],
|
|
10697
|
+
[0, 0, 1, 0, 1, 0],
|
|
10698
|
+
[0, 0, -1, 0, 1, 0],
|
|
10699
|
+
]
|
|
10700
|
+
const cam = this.probeCameraData
|
|
10701
|
+
const view = { perFrame: this.probeCaptureBindGroup!, args: "probe" as const, outlines: false }
|
|
10702
|
+
const formats = sceneColorFormats(this.sceneFormats)
|
|
10703
|
+
for (let f = 0; f < 6; f++) {
|
|
10704
|
+
const [fx, fy, fz, ux, uy, uz] = faces[f]
|
|
10705
|
+
Mat4.lookAtInto(cam.subarray(0, 16), eye[0], eye[1], eye[2], eye[0] + fx, eye[1] + fy, eye[2] + fz, ux, uy, uz)
|
|
10706
|
+
if (this.reversedZ) Mat4.perspectiveInto(cam.subarray(16, 32), Math.PI / 2, 1, far, near)
|
|
10707
|
+
else Mat4.perspectiveInto(cam.subarray(16, 32), Math.PI / 2, 1, near, far)
|
|
10708
|
+
cam[32] = eye[0]
|
|
10709
|
+
cam[33] = eye[1]
|
|
10710
|
+
cam[34] = eye[2]
|
|
10711
|
+
cam[35] = PROBE_CAPTURE_SIZE
|
|
10712
|
+
cam[36] = this.sceneClock
|
|
10713
|
+
this.device.queue.writeBuffer(this.probeCameraBuffer, 0, cam)
|
|
10714
|
+
const encoder = this.device.createCommandEncoder({ label: `reflection probe face ${f}` })
|
|
10715
|
+
const colorAttachments: GPURenderPassColorAttachment[] = formats.map((_, i) => ({
|
|
10716
|
+
view: cap.msTargets[i].createView(),
|
|
10717
|
+
resolveTarget:
|
|
10718
|
+
i === 0
|
|
10719
|
+
? cap.color.createView({ dimension: "2d", baseArrayLayer: f, arrayLayerCount: 1 })
|
|
10720
|
+
: i === 1
|
|
10721
|
+
? cap.aux.createView({ dimension: "2d", baseArrayLayer: f, arrayLayerCount: 1 })
|
|
10722
|
+
: undefined,
|
|
10723
|
+
clearValue: { r: 0, g: 0, b: 0, a: 0 },
|
|
10724
|
+
loadOp: "clear",
|
|
10725
|
+
storeOp: "discard",
|
|
10726
|
+
}))
|
|
10727
|
+
const pass = encoder.beginRenderPass({
|
|
10728
|
+
label: `reflection probe face ${f}`,
|
|
10729
|
+
colorAttachments,
|
|
10730
|
+
depthStencilAttachment: {
|
|
10731
|
+
view: cap.depth.createView(),
|
|
10732
|
+
depthClearValue: this.depthClear,
|
|
10733
|
+
depthLoadOp: "clear",
|
|
10734
|
+
depthStoreOp: "discard",
|
|
10735
|
+
stencilClearValue: 0,
|
|
10736
|
+
stencilLoadOp: "clear",
|
|
10737
|
+
stencilStoreOp: "discard",
|
|
10738
|
+
},
|
|
10739
|
+
})
|
|
10740
|
+
pass.setStencilReference(Engine.STENCIL_EYE_VALUE)
|
|
10741
|
+
for (const inst of this.modelInstances.values()) {
|
|
10742
|
+
if (!inst.isStage || !inst.model.visible) continue
|
|
10743
|
+
this.setModelDrawState(pass, inst)
|
|
10744
|
+
this.drawMaterials(pass, inst, "opaque", view)
|
|
10745
|
+
}
|
|
10746
|
+
for (const inst of this.modelInstances.values()) {
|
|
10747
|
+
if (!inst.isStage || !inst.model.visible) continue
|
|
10748
|
+
this.setModelDrawState(pass, inst)
|
|
10749
|
+
this.drawMaterials(pass, inst, "transparent", view)
|
|
10750
|
+
}
|
|
10751
|
+
pass.end()
|
|
10752
|
+
this.device.queue.submit([encoder.finish()])
|
|
10753
|
+
}
|
|
10754
|
+
|
|
10755
|
+
// The steps: compose, capture mips, then the probe's levels.
|
|
10756
|
+
const steps = new ArrayBuffer((6 + PROBE_CAPTURE_LEVELS * 6 + PROBE_LEVELS * 6) * 256)
|
|
10757
|
+
const put = (slot: number, face: number, lod: number, roughness: number, srcSize: number, mode: number) => {
|
|
10758
|
+
const u = new Uint32Array(steps, slot * 256, 8)
|
|
10759
|
+
const fl = new Float32Array(steps, slot * 256, 8)
|
|
10760
|
+
u[0] = face
|
|
10761
|
+
fl[1] = lod
|
|
10762
|
+
fl[2] = roughness
|
|
10763
|
+
fl[3] = srcSize
|
|
10764
|
+
u[4] = mode
|
|
10765
|
+
}
|
|
10766
|
+
for (let f = 0; f < 6; f++) put(f, f, 0, 0, PROBE_CAPTURE_SIZE, 0)
|
|
10767
|
+
for (let m = 0; m < PROBE_CAPTURE_LEVELS; m++)
|
|
10768
|
+
for (let f = 0; f < 6; f++) put(6 + m * 6 + f, f, Math.max(m - 1, 0), 0, PROBE_CAPTURE_SIZE, 0)
|
|
10769
|
+
for (let m = 0; m < PROBE_LEVELS; m++)
|
|
10770
|
+
for (let f = 0; f < 6; f++)
|
|
10771
|
+
// Level 0 is the capture at the probe's size: one tap of the 128²
|
|
10772
|
+
// level-0 cube at a 64² texel's centre is its 2×2 average.
|
|
10773
|
+
put(6 + PROBE_CAPTURE_LEVELS * 6 + m * 6 + f, f, 0, probeLevelRoughness(m), PROBE_CAPTURE_SIZE, m === 0 ? 0 : 1)
|
|
10774
|
+
this.device.queue.writeBuffer(cap.steps, 0, steps)
|
|
10775
|
+
|
|
10776
|
+
const encoder = this.device.createCommandEncoder({ label: "reflection probe filter" })
|
|
10777
|
+
const run = (target: GPUTextureView, pipeline: GPURenderPipeline, group: GPUBindGroup) => {
|
|
10778
|
+
const p = encoder.beginRenderPass({ colorAttachments: [{ view: target, loadOp: "clear", storeOp: "store" }] })
|
|
10779
|
+
p.setPipeline(pipeline)
|
|
10780
|
+
p.setBindGroup(0, group)
|
|
10781
|
+
p.draw(3)
|
|
10782
|
+
p.end()
|
|
10783
|
+
}
|
|
10784
|
+
const face = (tex: GPUTexture, level: number, f: number) =>
|
|
10785
|
+
tex.createView({ dimension: "2d", baseMipLevel: level, mipLevelCount: 1, baseArrayLayer: f, arrayLayerCount: 1 })
|
|
10786
|
+
for (let f = 0; f < 6; f++) run(face(cap.cube, 0, f), cap.composePipeline, cap.composeGroups[f])
|
|
10787
|
+
// Each level from the one above. The source view spans the whole chain,
|
|
10788
|
+
// which a pass writing one of its levels may not bind — so the reads go
|
|
10789
|
+
// through a view of the levels ABOVE the one being written.
|
|
10790
|
+
for (let m = 1; m < PROBE_CAPTURE_LEVELS; m++) {
|
|
10791
|
+
const above = cap.cube.createView({ dimension: "cube", baseMipLevel: 0, mipLevelCount: m })
|
|
10792
|
+
for (let f = 0; f < 6; f++) {
|
|
10793
|
+
const group = this.device.createBindGroup({
|
|
10794
|
+
label: `reflection probe capture mip ${m}/${f}`,
|
|
10795
|
+
layout: cap.filterPipeline.getBindGroupLayout(0),
|
|
10796
|
+
entries: [
|
|
10797
|
+
{ binding: 0, resource: above },
|
|
10798
|
+
{ binding: 1, resource: this.materialSampler },
|
|
10799
|
+
{ binding: 2, resource: { buffer: cap.steps, offset: (6 + m * 6 + f) * 256, size: 32 } },
|
|
10800
|
+
],
|
|
10801
|
+
})
|
|
10802
|
+
run(face(cap.cube, m, f), cap.filterPipeline, group)
|
|
10803
|
+
}
|
|
10804
|
+
}
|
|
10805
|
+
for (let m = 0; m < PROBE_LEVELS; m++)
|
|
10806
|
+
for (let f = 0; f < 6; f++) run(face(this.probeTexture, m, f), cap.filterPipeline, cap.filterGroups[m * 6 + f])
|
|
10807
|
+
this.device.queue.submit([encoder.finish()])
|
|
10808
|
+
|
|
10809
|
+
this.probeData.set([eye[0], eye[1], eye[2], 1, min[0], min[1], min[2], 1, max[0], max[1], max[2], 0])
|
|
10810
|
+
this.device.queue.writeBuffer(this.probeUniformBuffer, 0, this.probeData)
|
|
10811
|
+
}
|
|
10812
|
+
|
|
10813
|
+
/**
|
|
10814
|
+
* The sky's mips, convolved with the GGX lobe each stands for — the box
|
|
10815
|
+
* average the upload built is only the source. Level 0 is copied as it is
|
|
10816
|
+
* (it is also what the backdrop draws); every level below it becomes the
|
|
10817
|
+
* sky as a surface of that roughness reflects it (skyLevelRoughness).
|
|
10818
|
+
*/
|
|
10819
|
+
private prefilterSky(source: GPUTexture, width: number, height: number, levels: number): GPUTexture {
|
|
10820
|
+
if (levels <= 1) return source
|
|
10821
|
+
if (!this.skyPrefilterPipeline) {
|
|
10822
|
+
const module = this.device.createShaderModule({ label: "sky prefilter", code: SKY_PREFILTER_WGSL })
|
|
10823
|
+
this.skyPrefilterPipeline = this.device.createRenderPipeline({
|
|
10824
|
+
label: "sky prefilter",
|
|
10825
|
+
layout: "auto",
|
|
10826
|
+
vertex: { module, entryPoint: "vs_full" },
|
|
10827
|
+
fragment: { module, entryPoint: "fs_sky", targets: [{ format: "rgba16float" }] },
|
|
10828
|
+
primitive: { topology: "triangle-list" },
|
|
10829
|
+
})
|
|
10830
|
+
}
|
|
10831
|
+
const pipeline = this.skyPrefilterPipeline
|
|
10832
|
+
const out = this.device.createTexture({
|
|
10833
|
+
label: "world equirect (HDR, GGX-prefiltered)",
|
|
10834
|
+
size: [width, height],
|
|
10835
|
+
format: "rgba16float",
|
|
10836
|
+
mipLevelCount: levels,
|
|
10837
|
+
usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST | GPUTextureUsage.RENDER_ATTACHMENT,
|
|
10838
|
+
})
|
|
10839
|
+
const encoder = this.device.createCommandEncoder({ label: "sky prefilter" })
|
|
10840
|
+
encoder.copyTextureToTexture({ texture: source, mipLevel: 0 }, { texture: out, mipLevel: 0 }, [width, height])
|
|
10841
|
+
const params = this.device.createBuffer({
|
|
10842
|
+
label: "sky prefilter steps",
|
|
10843
|
+
size: levels * 256,
|
|
10844
|
+
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
|
|
10845
|
+
})
|
|
10846
|
+
const data = new Float32Array(levels * 64)
|
|
10847
|
+
for (let m = 1; m < levels; m++) data.set([skyLevelRoughness(m, levels), width, height, 0], m * 64)
|
|
10848
|
+
this.device.queue.writeBuffer(params, 0, data)
|
|
10849
|
+
const srcView = source.createView()
|
|
10850
|
+
for (let m = 1; m < levels; m++) {
|
|
10851
|
+
const group = this.device.createBindGroup({
|
|
10852
|
+
label: `sky prefilter ${m}`,
|
|
10853
|
+
layout: pipeline.getBindGroupLayout(0),
|
|
10854
|
+
entries: [
|
|
10855
|
+
{ binding: 0, resource: srcView },
|
|
10856
|
+
{ binding: 1, resource: this.materialSampler },
|
|
10857
|
+
{ binding: 2, resource: { buffer: params, offset: m * 256, size: 16 } },
|
|
10858
|
+
],
|
|
10859
|
+
})
|
|
10860
|
+
const p = encoder.beginRenderPass({
|
|
10861
|
+
label: `sky prefilter ${m}`,
|
|
10862
|
+
colorAttachments: [
|
|
10863
|
+
{ view: out.createView({ baseMipLevel: m, mipLevelCount: 1 }), loadOp: "clear", storeOp: "store" },
|
|
10864
|
+
],
|
|
10865
|
+
})
|
|
10866
|
+
p.setPipeline(pipeline)
|
|
10867
|
+
p.setBindGroup(0, group)
|
|
10868
|
+
p.draw(3)
|
|
10869
|
+
p.end()
|
|
10870
|
+
}
|
|
10871
|
+
this.device.queue.submit([encoder.finish()])
|
|
10872
|
+
// The box chain and the steps are done with once that work is: the source
|
|
10873
|
+
// retires with the skies (destroyed at the next frame's start).
|
|
10874
|
+
this.retiredSkies.push(source)
|
|
10875
|
+
void this.device.queue.onSubmittedWorkDone().then(() => params.destroy())
|
|
10876
|
+
return out
|
|
10877
|
+
}
|
|
10878
|
+
|
|
10394
10879
|
/** Returns whether it actually rebuilt — see rebuildCompositeBindGroup. */
|
|
10395
10880
|
private rebuildPerFrameBindGroups(): boolean {
|
|
10396
10881
|
// EVERY resource, not just the layout. This runs twice: once at init, where
|
|
@@ -10409,7 +10894,7 @@ export class Engine {
|
|
|
10409
10894
|
!this.shadowLightVPBuffer ||
|
|
10410
10895
|
!this.materialSampler ||
|
|
10411
10896
|
!this.shadowComparisonSampler ||
|
|
10412
|
-
!this.
|
|
10897
|
+
!this.probeUniformBuffer ||
|
|
10413
10898
|
!(this.worldEquirectView ?? this.fallbackEquirectView)
|
|
10414
10899
|
) {
|
|
10415
10900
|
return false
|
|
@@ -10432,8 +10917,31 @@ export class Engine {
|
|
|
10432
10917
|
{ binding: 5, resource: { buffer: this.shadowLightVPBuffer } },
|
|
10433
10918
|
{ binding: 6, resource: { buffer: this.lightsBuffer } },
|
|
10434
10919
|
{ binding: 8, resource: env },
|
|
10435
|
-
{ binding: 9, resource: this.
|
|
10920
|
+
{ binding: 9, resource: this.cookieView },
|
|
10436
10921
|
{ binding: 10, resource: this.castShadowView },
|
|
10922
|
+
{ binding: 11, resource: this.probeView },
|
|
10923
|
+
{ binding: 12, resource: { buffer: this.probeUniformBuffer } },
|
|
10924
|
+
],
|
|
10925
|
+
})
|
|
10926
|
+
// The probe's own view of the scene: one cube face at a time through its
|
|
10927
|
+
// capture camera, and the probe itself switched off — a material inside
|
|
10928
|
+
// the capture reflects the sky, never the cube being filled.
|
|
10929
|
+
this.probeCaptureBindGroup = this.device.createBindGroup({
|
|
10930
|
+
label: "reflection probe capture per-frame bind group",
|
|
10931
|
+
layout: this.mainPerFrameBindGroupLayout,
|
|
10932
|
+
entries: [
|
|
10933
|
+
{ binding: 0, resource: { buffer: this.probeCameraBuffer } },
|
|
10934
|
+
{ binding: 1, resource: { buffer: this.lightUniformBuffer } },
|
|
10935
|
+
{ binding: 2, resource: this.materialSampler },
|
|
10936
|
+
{ binding: 3, resource: this.shadowAtlasView },
|
|
10937
|
+
{ binding: 4, resource: this.shadowComparisonSampler },
|
|
10938
|
+
{ binding: 5, resource: { buffer: this.shadowLightVPBuffer } },
|
|
10939
|
+
{ binding: 6, resource: { buffer: this.lightsBuffer } },
|
|
10940
|
+
{ binding: 8, resource: env },
|
|
10941
|
+
{ binding: 9, resource: this.cookieView },
|
|
10942
|
+
{ binding: 10, resource: this.castShadowView },
|
|
10943
|
+
{ binding: 11, resource: this.probeFallbackView },
|
|
10944
|
+
{ binding: 12, resource: { buffer: this.probeOffBuffer } },
|
|
10437
10945
|
],
|
|
10438
10946
|
})
|
|
10439
10947
|
this.mirrorPerFrameBindGroup = this.device.createBindGroup({
|
|
@@ -10448,8 +10956,10 @@ export class Engine {
|
|
|
10448
10956
|
{ binding: 5, resource: { buffer: this.shadowLightVPBuffer } },
|
|
10449
10957
|
{ binding: 6, resource: { buffer: this.lightsBuffer } },
|
|
10450
10958
|
{ binding: 8, resource: env },
|
|
10451
|
-
{ binding: 9, resource: this.
|
|
10959
|
+
{ binding: 9, resource: this.cookieView },
|
|
10452
10960
|
{ binding: 10, resource: this.castShadowView },
|
|
10961
|
+
{ binding: 11, resource: this.probeView },
|
|
10962
|
+
{ binding: 12, resource: { buffer: this.probeUniformBuffer } },
|
|
10453
10963
|
],
|
|
10454
10964
|
})
|
|
10455
10965
|
return true
|
|
@@ -10511,13 +11021,13 @@ export class Engine {
|
|
|
10511
11021
|
this.lightData[base + 4] = this.sun.color.x
|
|
10512
11022
|
this.lightData[base + 5] = this.sun.color.y
|
|
10513
11023
|
this.lightData[base + 6] = this.sun.color.z
|
|
11024
|
+
// Unity's light intensity: a white sun of 1 lights albedo 1:1 at N·L = 1.
|
|
10514
11025
|
this.lightData[base + 7] = this.sun.strength
|
|
10515
11026
|
this.lightDataWords[DIR_LAYERS_AT + index] = this.sunLayers
|
|
10516
11027
|
if (index >= this.lightCount) this.lightCount = index + 1
|
|
10517
11028
|
this.updateLightBuffer()
|
|
10518
11029
|
}
|
|
10519
11030
|
|
|
10520
|
-
/** Update the world environment (Blender: World Background). Ambient recomputes immediately. */
|
|
10521
11031
|
/**
|
|
10522
11032
|
* Distance fog on every lit surface, as a game on SimPipeline lays it: per
|
|
10523
11033
|
* layer, t = saturate(f − (1 − f)·h) with f = saturate(depth·distance[0] +
|
|
@@ -10642,7 +11152,7 @@ export class Engine {
|
|
|
10642
11152
|
this.writeWorld()
|
|
10643
11153
|
}
|
|
10644
11154
|
|
|
10645
|
-
/** Update the
|
|
11155
|
+
/** Update the main light (Unity's directional Sun). Direction change marks shadow VP dirty. */
|
|
10646
11156
|
setSun(options: SunOptions): void {
|
|
10647
11157
|
if (options.color) this.sun.color = options.color
|
|
10648
11158
|
if (options.strength !== undefined) this.sun.strength = options.strength
|
|
@@ -10730,6 +11240,7 @@ export class Engine {
|
|
|
10730
11240
|
...options,
|
|
10731
11241
|
}
|
|
10732
11242
|
this.groundY = opts.y
|
|
11243
|
+
this.groundHalf = [opts.width / 2, opts.height / 2]
|
|
10733
11244
|
this.createGroundGeometry(opts.width, opts.height, opts.y)
|
|
10734
11245
|
this.createShadowGroundResources(opts)
|
|
10735
11246
|
this.hasGround = true
|
|
@@ -10782,6 +11293,7 @@ export class Engine {
|
|
|
10782
11293
|
* literal typed into a console. Vec3 satisfies it either way. */
|
|
10783
11294
|
lights: SceneLight[] | null,
|
|
10784
11295
|
): void {
|
|
11296
|
+
this.sceneLights = lights
|
|
10785
11297
|
// The directional ones go to the light uniform's slots beside the sun's —
|
|
10786
11298
|
// three of them, in order; the rest are records the grid indexes.
|
|
10787
11299
|
const directional = (lights ?? []).filter((l) => l.kind === "directional")
|
|
@@ -10841,8 +11353,12 @@ export class Engine {
|
|
|
10841
11353
|
this.lightsData[b + 12] = Math.max(inner, outer)
|
|
10842
11354
|
// The layers it does NOT reach, as bits — inverted so a zero reaches all.
|
|
10843
11355
|
this.lightsWords[b + 13] = ~(l.layers ?? ALL_LAYERS) >>> 0
|
|
10844
|
-
|
|
10845
|
-
this.lightsData[b +
|
|
11356
|
+
// The bulb's radius (rzLightNear): 0 keeps the engine's default
|
|
11357
|
+
this.lightsData[b + 14] = l.near !== undefined && Number.isFinite(l.near) && l.near > 0 ? l.near : 0
|
|
11358
|
+
// Its cookie's layer + 1, 0 for none or one not loaded yet (a load
|
|
11359
|
+
// rewrites the lamps when it lands).
|
|
11360
|
+
const cookie = l.cookie !== undefined ? this.cookieLayers.get(l.cookie) : undefined
|
|
11361
|
+
this.lightsData[b + 15] = cookie !== undefined ? cookie + 1 : 0
|
|
10846
11362
|
}
|
|
10847
11363
|
// The grid, from the records as STORED — normalised aim, clamped reach, the
|
|
10848
11364
|
// cosines in f32 — so it indexes exactly what the shader will evaluate.
|
|
@@ -10881,6 +11397,58 @@ export class Engine {
|
|
|
10881
11397
|
this.allocateLightSlots()
|
|
10882
11398
|
}
|
|
10883
11399
|
|
|
11400
|
+
/**
|
|
11401
|
+
* A cookie, under `key`, for every spot lamp that names it (SceneLight.cookie):
|
|
11402
|
+
* the picture a stage light throws through its gobo, projected along the
|
|
11403
|
+
* cone — its RGB is the light let through, so black blocks, white passes and
|
|
11404
|
+
* a colour tints. Square, stretched to 512². Up to 16 at once; loading a key
|
|
11405
|
+
* again replaces its picture. False when every layer is taken.
|
|
11406
|
+
*/
|
|
11407
|
+
async loadLightCookie(key: string, source: Blob | ImageBitmap): Promise<boolean> {
|
|
11408
|
+
if (!this.device) return false
|
|
11409
|
+
let layer = this.cookieLayers.get(key)
|
|
11410
|
+
if (layer === undefined) {
|
|
11411
|
+
const used = new Set(this.cookieLayers.values())
|
|
11412
|
+
for (let k = 0; k < Engine.COOKIE_LAYERS && layer === undefined; k++) if (!used.has(k)) layer = k
|
|
11413
|
+
if (layer === undefined) {
|
|
11414
|
+
console.warn(`[reze] light cookie "${key}": all ${Engine.COOKIE_LAYERS} cookie layers are in use`)
|
|
11415
|
+
return false
|
|
11416
|
+
}
|
|
11417
|
+
}
|
|
11418
|
+
const size = Engine.COOKIE_SIZE
|
|
11419
|
+
const bmp = await createImageBitmap(source, {
|
|
11420
|
+
resizeWidth: size,
|
|
11421
|
+
resizeHeight: size,
|
|
11422
|
+
resizeQuality: "high",
|
|
11423
|
+
colorSpaceConversion: "none",
|
|
11424
|
+
premultiplyAlpha: "none",
|
|
11425
|
+
})
|
|
11426
|
+
// The real array on the first cookie, and every group that binds the
|
|
11427
|
+
// stand-in rebuilt against it (the stand-in itself is a 1x1 and stays).
|
|
11428
|
+
if (this.cookieTexture.width === 1) {
|
|
11429
|
+
this.cookieTexture = this.device.createTexture({
|
|
11430
|
+
label: "light cookies",
|
|
11431
|
+
size: [size, size, Engine.COOKIE_LAYERS],
|
|
11432
|
+
format: "rgba8unorm",
|
|
11433
|
+
usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST | GPUTextureUsage.RENDER_ATTACHMENT,
|
|
11434
|
+
})
|
|
11435
|
+
this.cookieView = this.cookieTexture.createView({ dimension: "2d-array" })
|
|
11436
|
+
this.worldBindingsDirty = true
|
|
11437
|
+
this.buildGroundBindGroup()
|
|
11438
|
+
}
|
|
11439
|
+
this.device.queue.copyExternalImageToTexture({ source: bmp }, { texture: this.cookieTexture, origin: [0, 0, layer] }, [size, size, 1])
|
|
11440
|
+
bmp.close()
|
|
11441
|
+
this.cookieLayers.set(key, layer)
|
|
11442
|
+
if (this.sceneLights) this.setLights(this.sceneLights)
|
|
11443
|
+
return true
|
|
11444
|
+
}
|
|
11445
|
+
|
|
11446
|
+
/** Forget a cookie: lamps naming it light plainly again. */
|
|
11447
|
+
removeLightCookie(key: string): void {
|
|
11448
|
+
if (!this.cookieLayers.delete(key)) return
|
|
11449
|
+
if (this.sceneLights) this.setLights(this.sceneLights)
|
|
11450
|
+
}
|
|
11451
|
+
|
|
10884
11452
|
/** How many positional lights the scene is carrying. */
|
|
10885
11453
|
getLightCount(): number {
|
|
10886
11454
|
return this.lightHeader[0]
|
|
@@ -13171,8 +13739,12 @@ export class Engine {
|
|
|
13171
13739
|
const d = positions[i] * positions[i] + positions[i + 1] * positions[i + 1] + positions[i + 2] * positions[i + 2]
|
|
13172
13740
|
if (d > most) most = d
|
|
13173
13741
|
}
|
|
13174
|
-
|
|
13175
|
-
|
|
13742
|
+
this.raiseSceneExtent(Math.sqrt(most))
|
|
13743
|
+
}
|
|
13744
|
+
|
|
13745
|
+
/** Raise the scene's reach (never lower it) and hand it to the camera. */
|
|
13746
|
+
private raiseSceneExtent(extent: number): void {
|
|
13747
|
+
if (!(extent > this.sceneExtent)) return
|
|
13176
13748
|
this.sceneExtent = extent
|
|
13177
13749
|
this.camera?.setSceneExtent(extent)
|
|
13178
13750
|
}
|
|
@@ -13253,6 +13825,16 @@ export class Engine {
|
|
|
13253
13825
|
minZ = Math.min(minZ, data[o + 18] - r); maxZ = Math.max(maxZ, data[o + 18] + r)
|
|
13254
13826
|
}
|
|
13255
13827
|
}
|
|
13828
|
+
// The built-in floor, while it is drawn. It is no draw of the cull table,
|
|
13829
|
+
// so without this the box ended at the cast: the cascades stopped just
|
|
13830
|
+
// past her, and a low sun's long shadow ran off the last of them — the
|
|
13831
|
+
// floor beyond read as lit and her head and shoulders went missing.
|
|
13832
|
+
if (this.hasGround && !this.groundIsSuppressed()) {
|
|
13833
|
+
const [hx, hz] = this.groundHalf
|
|
13834
|
+
minX = Math.min(minX, -hx); maxX = Math.max(maxX, hx)
|
|
13835
|
+
minY = Math.min(minY, this.groundY); maxY = Math.max(maxY, this.groundY)
|
|
13836
|
+
minZ = Math.min(minZ, -hz); maxZ = Math.max(maxZ, hz)
|
|
13837
|
+
}
|
|
13256
13838
|
if (this.nativeStage) {
|
|
13257
13839
|
const nb = this.nativeStage.bounds
|
|
13258
13840
|
minX = Math.min(minX, nb.min[0]); maxX = Math.max(maxX, nb.max[0])
|
|
@@ -13557,6 +14139,10 @@ export class Engine {
|
|
|
13557
14139
|
*/
|
|
13558
14140
|
private recordBundles(): void {
|
|
13559
14141
|
this.bundlesDirty = false
|
|
14142
|
+
// What the bundles record is what the probe would see: a stage arriving or
|
|
14143
|
+
// leaving, a look installed on it, a pipeline landing. Re-captured once it
|
|
14144
|
+
// settles (and switched off when the stage is gone).
|
|
14145
|
+
this.captureReflectionProbe()
|
|
13560
14146
|
const scene = {
|
|
13561
14147
|
colorFormats: sceneColorFormats(this.sceneFormats),
|
|
13562
14148
|
depthStencilFormat: this.depthFormat,
|
|
@@ -13820,10 +14406,17 @@ export class Engine {
|
|
|
13820
14406
|
private issueDraw(
|
|
13821
14407
|
pass: GPURenderPassEncoder | GPURenderBundleEncoder,
|
|
13822
14408
|
draw: DrawCall,
|
|
13823
|
-
kind: "camera" | "shadow" | "mirror",
|
|
14409
|
+
kind: "camera" | "shadow" | "mirror" | "probe",
|
|
13824
14410
|
): void {
|
|
14411
|
+
// The probe's cube faces have no cull of their own: drawn whole.
|
|
13825
14412
|
const args =
|
|
13826
|
-
kind === "
|
|
14413
|
+
kind === "probe"
|
|
14414
|
+
? null
|
|
14415
|
+
: kind === "shadow"
|
|
14416
|
+
? this.cullShadowArgs
|
|
14417
|
+
: kind === "mirror"
|
|
14418
|
+
? this.cullMirrorArgs
|
|
14419
|
+
: this.cullCameraArgs
|
|
13827
14420
|
if (args && draw.cullIndex >= 0) {
|
|
13828
14421
|
pass.drawIndexedIndirect(args, draw.cullIndex * Engine.CULL_ARG_WORDS * 4)
|
|
13829
14422
|
} else {
|
|
@@ -14127,7 +14720,8 @@ export class Engine {
|
|
|
14127
14720
|
entries: [{ binding: 0, resource: { buffer: skinMatrixBuffer } }],
|
|
14128
14721
|
})
|
|
14129
14722
|
|
|
14130
|
-
const
|
|
14723
|
+
const attributeBuffer = this.createAttributeBuffer(name, model)
|
|
14724
|
+
const gpuBuffers: GPUBuffer[] = [vertexBuffer, indexBuffer, jointsBuffer, weightsBuffer, skinMatrixBuffer, attributeBuffer]
|
|
14131
14725
|
|
|
14132
14726
|
const gpuMorph = this.createGpuMorph(name, model, vertexBuffer, gpuBuffers)
|
|
14133
14727
|
|
|
@@ -14152,6 +14746,7 @@ export class Engine {
|
|
|
14152
14746
|
indexBuffer,
|
|
14153
14747
|
jointsBuffer,
|
|
14154
14748
|
weightsBuffer,
|
|
14749
|
+
attributeBuffer,
|
|
14155
14750
|
skinMatrixBuffer,
|
|
14156
14751
|
wireEdges: new Map(),
|
|
14157
14752
|
drawCalls: [],
|
|
@@ -14198,6 +14793,46 @@ export class Engine {
|
|
|
14198
14793
|
this.updateOrderDirty = true
|
|
14199
14794
|
}
|
|
14200
14795
|
|
|
14796
|
+
/**
|
|
14797
|
+
* A model's attribute stream: vertex colour (RGBA) and a second UV set, six
|
|
14798
|
+
* floats a vertex, read by the material pipelines at slot 4.
|
|
14799
|
+
*
|
|
14800
|
+
* PMX has no vertex colour and one UV, but it has four ADDITIONAL UV
|
|
14801
|
+
* channels of four floats each, and that is where a model that needs more
|
|
14802
|
+
* keeps it: the first additional UV is the vertex colour, the second's xy
|
|
14803
|
+
* the second UV — the convention a converted game stage is written in (a
|
|
14804
|
+
* plant's occlusion rides in its vertex alpha, a light card's mask in its
|
|
14805
|
+
* second UV). A model without them reads white and its own UV, which is
|
|
14806
|
+
* what every graph that reads neither already assumed.
|
|
14807
|
+
*/
|
|
14808
|
+
private createAttributeBuffer(name: string, model: Model): GPUBuffer {
|
|
14809
|
+
const vertices = model.getVertices()
|
|
14810
|
+
const count = Math.floor(vertices.length / 8)
|
|
14811
|
+
const data = new Float32Array(count * Engine.ATTRIBUTE_FLOATS)
|
|
14812
|
+
const color = model.getAdditionalUv(1)
|
|
14813
|
+
const uv2 = model.getAdditionalUv(2)
|
|
14814
|
+
for (let i = 0; i < count; i++) {
|
|
14815
|
+
const o = i * Engine.ATTRIBUTE_FLOATS
|
|
14816
|
+
if (color) {
|
|
14817
|
+
data[o] = color[i * 4]
|
|
14818
|
+
data[o + 1] = color[i * 4 + 1]
|
|
14819
|
+
data[o + 2] = color[i * 4 + 2]
|
|
14820
|
+
data[o + 3] = color[i * 4 + 3]
|
|
14821
|
+
} else {
|
|
14822
|
+
data[o] = data[o + 1] = data[o + 2] = data[o + 3] = 1
|
|
14823
|
+
}
|
|
14824
|
+
data[o + 4] = uv2 ? uv2[i * 4] : vertices[i * 8 + 6]
|
|
14825
|
+
data[o + 5] = uv2 ? uv2[i * 4 + 1] : vertices[i * 8 + 7]
|
|
14826
|
+
}
|
|
14827
|
+
const buffer = this.device.createBuffer({
|
|
14828
|
+
label: `${name}: attribute buffer`,
|
|
14829
|
+
size: Math.max(data.byteLength, 4 * Engine.ATTRIBUTE_FLOATS),
|
|
14830
|
+
usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
|
|
14831
|
+
})
|
|
14832
|
+
if (data.byteLength > 0) this.device.queue.writeBuffer(buffer, 0, data)
|
|
14833
|
+
return buffer
|
|
14834
|
+
}
|
|
14835
|
+
|
|
14201
14836
|
// Build the per-model GPU vertex-morph state. Returns null (and leaves the model on the
|
|
14202
14837
|
// CPU morph path) when the model has no vertex morphs. Created buffers are pushed into
|
|
14203
14838
|
// gpuBuffers so they're released with the instance.
|
|
@@ -14530,6 +15165,7 @@ export class Engine {
|
|
|
14530
15165
|
{ binding: 12, resource: this.groundNoiseView },
|
|
14531
15166
|
{ binding: 13, resource: this.mirrorMaskView! },
|
|
14532
15167
|
{ binding: 14, resource: { buffer: this.groundClipOffBuffer } },
|
|
15168
|
+
{ binding: 15, resource: this.cookieView },
|
|
14533
15169
|
],
|
|
14534
15170
|
})
|
|
14535
15171
|
this.groundMirrorViewBindGroup = this.device.createBindGroup({
|
|
@@ -14552,6 +15188,7 @@ export class Engine {
|
|
|
14552
15188
|
{ binding: 12, resource: this.groundNoiseView },
|
|
14553
15189
|
{ binding: 13, resource: this.mirrorDummyColorView! },
|
|
14554
15190
|
{ binding: 14, resource: { buffer: this.groundClipMirrorBuffer } },
|
|
15191
|
+
{ binding: 15, resource: this.cookieView },
|
|
14555
15192
|
],
|
|
14556
15193
|
})
|
|
14557
15194
|
if (this.groundDrawCall) this.groundDrawCall.bindGroup = this.groundShadowBindGroup
|
|
@@ -15854,38 +16491,6 @@ export class Engine {
|
|
|
15854
16491
|
}
|
|
15855
16492
|
|
|
15856
16493
|
// World-space ray from camera through a canvas pixel. Uses WebGPU's NDC z ∈ [0,1].
|
|
15857
|
-
/**
|
|
15858
|
-
* Where a point on the canvas lands on a horizontal plane.
|
|
15859
|
-
*
|
|
15860
|
-
* `px,py` are canvas-relative pixels, top-left origin — what a pointer event
|
|
15861
|
-
* gives you after subtracting the element's rect. Returns null when the ray
|
|
15862
|
-
* cannot reach the plane: parallel to it, or pointing the other way, which is
|
|
15863
|
-
* what a click on the sky above the horizon is.
|
|
15864
|
-
*
|
|
15865
|
-
* The one primitive a placement UI needs. Dragging a thing across the floor is
|
|
15866
|
-
* otherwise three sliders in world units, which asks someone to guess numbers
|
|
15867
|
-
* that have no visible relation to the picture they are looking at — and it
|
|
15868
|
-
* throws away the property that makes pointing work at all: under perspective,
|
|
15869
|
-
* moving something further away makes it smaller by exactly the right amount,
|
|
15870
|
-
* so position and size stop being two controls to tune against each other.
|
|
15871
|
-
*/
|
|
15872
|
-
groundPointAt(px: number, py: number, planeY = 0): Vec3 | null {
|
|
15873
|
-
const ray = this.buildMouseRay(px, py)
|
|
15874
|
-
if (!ray) return null
|
|
15875
|
-
// Parallel to the plane: no intersection, and a huge one is not an answer.
|
|
15876
|
-
if (Math.abs(ray.dir.y) < 1e-6) return null
|
|
15877
|
-
const t = (planeY - ray.origin.y) / ray.dir.y
|
|
15878
|
-
// Behind the camera — the plane is there, but not in this shot.
|
|
15879
|
-
if (!(t > 0) || !isFinite(t)) return null
|
|
15880
|
-
return new Vec3(ray.origin.x + ray.dir.x * t, planeY, ray.origin.z + ray.dir.z * t)
|
|
15881
|
-
}
|
|
15882
|
-
|
|
15883
|
-
/** Hand the pointer to something else — a placement drag, a gizmo, a host's own
|
|
15884
|
-
* overlay — so the orbit does not also act on it. */
|
|
15885
|
-
setCameraInputLocked(locked: boolean): void {
|
|
15886
|
-
this.camera?.setInputLocked(locked)
|
|
15887
|
-
}
|
|
15888
|
-
|
|
15889
16494
|
private buildMouseRay(px: number, py: number): { origin: Vec3; dir: Vec3 } | null {
|
|
15890
16495
|
if (!this.camera) return null
|
|
15891
16496
|
const width = this.canvas.clientWidth
|
|
@@ -16478,6 +17083,10 @@ export class Engine {
|
|
|
16478
17083
|
idAtt.storeOp = idsRead ? "store" : "discard"
|
|
16479
17084
|
}
|
|
16480
17085
|
|
|
17086
|
+
// The stage's reflection probe, when one is due: its own submits, ahead of
|
|
17087
|
+
// this frame's, so every material below reads the fresh cube.
|
|
17088
|
+
if (hasModels) this.maybeCaptureProbe()
|
|
17089
|
+
|
|
16481
17090
|
const encoder = this.device.createCommandEncoder()
|
|
16482
17091
|
|
|
16483
17092
|
// GPU vertex morphs: write morphed positions into vertex buffers before any pass reads
|
|
@@ -17481,7 +18090,7 @@ export class Engine {
|
|
|
17481
18090
|
const base = {
|
|
17482
18091
|
label: `style ${renderClass}${overEyes ? " (over eyes)" : ""}`,
|
|
17483
18092
|
layout: this.mainPipelineLayout,
|
|
17484
|
-
vertex: { module, buffers: this.
|
|
18093
|
+
vertex: { module, buffers: this.materialVertexBufferLayouts },
|
|
17485
18094
|
// The eye front-culls — the see-through-hair look is draw order plus that
|
|
17486
18095
|
// cull plus the stencil stamp, and it is the deployed appearance of every
|
|
17487
18096
|
// published scene. In the MIRROR the winding is flipped (determinant -1,
|
|
@@ -17538,7 +18147,16 @@ export class Engine {
|
|
|
17538
18147
|
}
|
|
17539
18148
|
const desc: GPURenderPipelineDescriptor = {
|
|
17540
18149
|
...base,
|
|
17541
|
-
fragment: {
|
|
18150
|
+
fragment: {
|
|
18151
|
+
module,
|
|
18152
|
+
constants,
|
|
18153
|
+
targets:
|
|
18154
|
+
blend === "additive"
|
|
18155
|
+
? this.sceneTargetsAdditive
|
|
18156
|
+
: blend === "premultiplied"
|
|
18157
|
+
? this.sceneTargetsPremultiplied
|
|
18158
|
+
: this.sceneTargets,
|
|
18159
|
+
},
|
|
17542
18160
|
depthStencil,
|
|
17543
18161
|
}
|
|
17544
18162
|
return this.cachedRenderPipeline(desc).then((pipeline) => {
|
|
@@ -17786,6 +18404,46 @@ export class Engine {
|
|
|
17786
18404
|
* makes outlines compose like MMD: every material drawn later in the author's
|
|
17787
18405
|
* order covers earlier hulls, and each hull sits over everything drawn before it.
|
|
17788
18406
|
*/
|
|
18407
|
+
/**
|
|
18408
|
+
* A stage's draws, far to near from the eye: its transparent queue, in the
|
|
18409
|
+
* order a game sorts one. Each draw's distance is its material box's centre,
|
|
18410
|
+
* placed, against the camera — a pane or a water surface is one material,
|
|
18411
|
+
* which is the unit a game sorts too. The array is kept per instance and
|
|
18412
|
+
* re-sorted in place, so an unchanged order costs a comparison per draw.
|
|
18413
|
+
*/
|
|
18414
|
+
private backToFront(inst: ModelInstance): DrawCall[] {
|
|
18415
|
+
let list = this.sortedDraws.get(inst)
|
|
18416
|
+
if (!list || list.length !== inst.drawCalls.length || list.some((d) => !inst.drawCalls.includes(d))) {
|
|
18417
|
+
list = [...inst.drawCalls]
|
|
18418
|
+
this.sortedDraws.set(inst, list)
|
|
18419
|
+
}
|
|
18420
|
+
const m = inst.model
|
|
18421
|
+
const ex = this.cameraMatrixData[32]
|
|
18422
|
+
const ey = this.cameraMatrixData[33]
|
|
18423
|
+
const ez = this.cameraMatrixData[34]
|
|
18424
|
+
const p = this.sortScratch
|
|
18425
|
+
const dist = this.sortDistance
|
|
18426
|
+
dist.clear()
|
|
18427
|
+
for (const d of list) {
|
|
18428
|
+
const b = d.bounds
|
|
18429
|
+
if (!b || b.length < 6 || !(b[0] <= b[3])) {
|
|
18430
|
+
dist.set(d, 0)
|
|
18431
|
+
continue
|
|
18432
|
+
}
|
|
18433
|
+
p.setXYZ(((b[0] + b[3]) * 0.5) * m.scale, ((b[1] + b[4]) * 0.5) * m.scale, ((b[2] + b[5]) * 0.5) * m.scale)
|
|
18434
|
+
Quat.rotateVecInto(m.rotation, p, p)
|
|
18435
|
+
const dx = p.x + m.position.x - ex
|
|
18436
|
+
const dy = p.y + m.position.y - ey
|
|
18437
|
+
const dz = p.z + m.position.z - ez
|
|
18438
|
+
dist.set(d, dx * dx + dy * dy + dz * dz)
|
|
18439
|
+
}
|
|
18440
|
+
list.sort((a, b) => dist.get(b)! - dist.get(a)!)
|
|
18441
|
+
return list
|
|
18442
|
+
}
|
|
18443
|
+
private sortedDraws = new WeakMap<ModelInstance, DrawCall[]>()
|
|
18444
|
+
private sortScratch = new Vec3(0, 0, 0)
|
|
18445
|
+
private sortDistance = new Map<DrawCall, number>()
|
|
18446
|
+
|
|
17789
18447
|
/** Is this draw's compiled class "hair"? Ungrouped draws never are — the
|
|
17790
18448
|
* neutral pipeline is the auto class. */
|
|
17791
18449
|
private isHairDraw(inst: ModelInstance, dc: DrawCall): boolean {
|
|
@@ -17798,7 +18456,7 @@ export class Engine {
|
|
|
17798
18456
|
pass: GPURenderPassEncoder | GPURenderBundleEncoder,
|
|
17799
18457
|
inst: ModelInstance,
|
|
17800
18458
|
type: "opaque" | "transparent",
|
|
17801
|
-
view: { perFrame: GPUBindGroup; args: "camera" | "mirror"; outlines: boolean },
|
|
18459
|
+
view: { perFrame: GPUBindGroup; args: "camera" | "mirror" | "probe"; outlines: boolean },
|
|
17802
18460
|
// The opaque phase walks its author order twice — non-hair, then hair — so
|
|
17803
18461
|
// the hair depth prime can sit between the eye's stencil write and the hair
|
|
17804
18462
|
// colour that must respect it. See renderModelOpaquePhase.
|
|
@@ -17806,9 +18464,17 @@ export class Engine {
|
|
|
17806
18464
|
): void {
|
|
17807
18465
|
let currentPipeline: GPURenderPipeline | null = null
|
|
17808
18466
|
let bound = false
|
|
17809
|
-
|
|
18467
|
+
const outlined: DrawCall[] = []
|
|
18468
|
+
// A stage's glass and water in the camera pass go far to near, as a
|
|
18469
|
+
// game's transparent queue does: author order put a far pane over a near
|
|
18470
|
+
// one wherever the author listed it last. Characters keep MMD's author
|
|
18471
|
+
// order — their layers are fabric, built around it.
|
|
18472
|
+
const draws = type === "transparent" && inst.isStage && view.args === "camera" ? this.backToFront(inst) : inst.drawCalls
|
|
18473
|
+
for (const draw of draws) {
|
|
17810
18474
|
if (draw.type !== type) continue
|
|
17811
18475
|
if (only && (only === "hair") !== this.isHairDraw(inst, draw)) continue
|
|
18476
|
+
// The probe draws directly, past the cull: honour the switches it holds.
|
|
18477
|
+
if (view.args === "probe" && !this.shouldRenderDrawCall(inst, draw)) continue
|
|
17812
18478
|
if (!bound) {
|
|
17813
18479
|
pass.setBindGroup(0, view.perFrame)
|
|
17814
18480
|
pass.setBindGroup(1, inst.mainPerInstanceBindGroup)
|
|
@@ -17822,35 +18488,42 @@ export class Engine {
|
|
|
17822
18488
|
}
|
|
17823
18489
|
pass.setBindGroup(2, draw.bindGroup)
|
|
17824
18490
|
this.issueDraw(pass, draw, view.args)
|
|
17825
|
-
if (draw.outline && this.outlineEnabled && view.outlines)
|
|
17826
|
-
|
|
17827
|
-
|
|
17828
|
-
|
|
17829
|
-
|
|
17830
|
-
|
|
17831
|
-
|
|
17832
|
-
|
|
17833
|
-
|
|
17834
|
-
|
|
17835
|
-
|
|
18491
|
+
if (draw.outline && this.outlineEnabled && view.outlines) outlined.push(draw)
|
|
18492
|
+
}
|
|
18493
|
+
// THE HULLS AFTER THE SURFACES, as MMD draws a model's edges. A hull held
|
|
18494
|
+
// at the minimum width is faded, and it still writes depth: drawn between
|
|
18495
|
+
// two materials it hid the second wherever it stood in front of it — a
|
|
18496
|
+
// shoe's sole's hull cut the upper behind it, and the floor showed through
|
|
18497
|
+
// as pale lines along the seam. After every surface of this phase it only
|
|
18498
|
+
// ever blends over finished ones. Same index ranges; own pipeline and
|
|
18499
|
+
// groups 0/2 — group 1 (skinMats) is layout-identical and stays bound.
|
|
18500
|
+
if (outlined.length) {
|
|
18501
|
+
const mirrored = view.args === "mirror"
|
|
18502
|
+
pass.setPipeline(mirrored ? this.outlineMirrorPipeline : this.outlinePipeline)
|
|
18503
|
+
pass.setBindGroup(0, mirrored ? this.outlineMirrorPerFrameBindGroup : this.outlinePerFrameBindGroup)
|
|
18504
|
+
// Slot 3: the hull's smoothed normals + edge scale. Only the outline
|
|
18505
|
+
// pipeline reads it; the others leave the slot alone.
|
|
18506
|
+
if (inst.outlineVertexBuffer) pass.setVertexBuffer(3, inst.outlineVertexBuffer)
|
|
18507
|
+
for (const draw of outlined) {
|
|
18508
|
+
pass.setBindGroup(2, draw.outline!.bindGroup)
|
|
17836
18509
|
this.issueDraw(pass, draw, view.args)
|
|
17837
|
-
pass.setBindGroup(0, view.perFrame)
|
|
17838
|
-
currentPipeline = null
|
|
17839
18510
|
}
|
|
18511
|
+
pass.setBindGroup(0, view.perFrame)
|
|
17840
18512
|
}
|
|
17841
18513
|
}
|
|
17842
18514
|
|
|
17843
18515
|
/**
|
|
17844
18516
|
* Main-pass render sequence for one model instance — babylon-mmd parity:
|
|
17845
18517
|
* opaque bucket, the hair-over-eyes stencil pass, then alpha-blend materials
|
|
17846
|
-
* in PMX author order with depth write ON (forceDepthWrite). Outlines
|
|
17847
|
-
*
|
|
17848
|
-
*
|
|
18518
|
+
* in PMX author order with depth write ON (forceDepthWrite). Outlines close
|
|
18519
|
+
* each drawMaterials call: the edge-flagged materials' hulls after all of its
|
|
18520
|
+
* surfaces, as MMD draws a model's edges.
|
|
17849
18521
|
*/
|
|
17850
18522
|
private setModelDrawState(pass: GPURenderPassEncoder | GPURenderBundleEncoder, inst: ModelInstance): void {
|
|
17851
18523
|
pass.setVertexBuffer(0, inst.vertexBuffer)
|
|
17852
18524
|
pass.setVertexBuffer(1, inst.jointsBuffer)
|
|
17853
18525
|
pass.setVertexBuffer(2, inst.weightsBuffer)
|
|
18526
|
+
pass.setVertexBuffer(4, inst.attributeBuffer)
|
|
17854
18527
|
pass.setIndexBuffer(inst.indexBuffer, "uint32")
|
|
17855
18528
|
// The stencil reference used to be set here. It is pass state, not bundle
|
|
17856
18529
|
// state — GPURenderBundleEncoder has no setStencilReference at all — so it
|
|
@@ -18217,7 +18890,7 @@ export class Engine {
|
|
|
18217
18890
|
// still pattern welded to the picture. On the SCENE clock like everything
|
|
18218
18891
|
// else here, so an export reproduces the editor exactly rather than
|
|
18219
18892
|
// scattering differently at whatever rate the encoder ran.
|
|
18220
|
-
u[3] =
|
|
18893
|
+
u[3] = Math.floor((this.sceneClock * 24) % 1024)
|
|
18221
18894
|
u[26] = this.canvas.width
|
|
18222
18895
|
u[27] = this.canvas.height
|
|
18223
18896
|
// Camera world position (viewU[10]) — the other half of bgWorldPos. It
|
|
@@ -18451,7 +19124,10 @@ export class Engine {
|
|
|
18451
19124
|
}
|
|
18452
19125
|
for (let s = 0; s < anchors.length; s++) {
|
|
18453
19126
|
const a = CAST_SUBJECT_VEC4S * 4 + (s * MAX_EFFECT_SUBJECTS + n) * 12
|
|
18454
|
-
|
|
19127
|
+
// `along`: the point that far down the bone's own axis, still model space
|
|
19128
|
+
// here, so the placement below turns and scales it like the joint.
|
|
19129
|
+
const along = anchors[s].along
|
|
19130
|
+
const pos = along ? m.getBonePointAlong(anchors[s].bone, along) : m.getBoneWorldPosition(anchors[s].bone)
|
|
18455
19131
|
if (!pos) {
|
|
18456
19132
|
cd[a + 3] = 0
|
|
18457
19133
|
continue
|
|
@@ -18473,7 +19149,7 @@ export class Engine {
|
|
|
18473
19149
|
cd[a + 4] = vx
|
|
18474
19150
|
cd[a + 5] = vy
|
|
18475
19151
|
cd[a + 6] = vz
|
|
18476
|
-
if (anchors[s].trail) this.writeTrail(inst.name, s, n, pos, cd, a)
|
|
19152
|
+
if (anchors[s].trail) this.writeTrail(inst.name, s, n, pos, cd, a, anchors[s].step)
|
|
18477
19153
|
const fwd = m.getBoneWorldForward(anchors[s].bone)
|
|
18478
19154
|
if (fwd) {
|
|
18479
19155
|
Quat.rotateVecInto(m.rotation, fwd, fwd)
|
|
@@ -18515,7 +19191,7 @@ export class Engine {
|
|
|
18515
19191
|
this.castData.fill(0, CAST_SUBJECT_VEC4S * 4)
|
|
18516
19192
|
}
|
|
18517
19193
|
|
|
18518
|
-
private writeTrail(model: string, slot: number, n: number, pos: Vec3, cd: Float32Array, anchorBase: number): void {
|
|
19194
|
+
private writeTrail(model: string, slot: number, n: number, pos: Vec3, cd: Float32Array, anchorBase: number, step?: number): void {
|
|
18519
19195
|
const key = `${model}\u0000${slot}`
|
|
18520
19196
|
let ring = this.anchorTrail.get(key)
|
|
18521
19197
|
if (!ring) {
|
|
@@ -18536,9 +19212,44 @@ export class Engine {
|
|
|
18536
19212
|
if (Math.hypot(dx, dy, dz) / dt > 50) {
|
|
18537
19213
|
ring.pos.length = 0
|
|
18538
19214
|
ring.t.length = 0
|
|
19215
|
+
ring.live = false
|
|
18539
19216
|
}
|
|
18540
19217
|
}
|
|
18541
|
-
if (
|
|
19218
|
+
if (step && ring.pos.length > 0) {
|
|
19219
|
+
// `trail step d` — the path a game's trail keeps (Unity's
|
|
19220
|
+
// minVertexDistance). Point 0 is the LIVE head: it follows the bone every
|
|
19221
|
+
// frame. It is left behind as a kept point only on a tick, and only once
|
|
19222
|
+
// the bone is `step` from the last point kept, so no segment is shorter
|
|
19223
|
+
// than that and a slowing hand cannot leave a knot of jittery stubs for a
|
|
19224
|
+
// wide ribbon to fan across. Still at most one point per tick, so a stepped
|
|
19225
|
+
// ring never covers LESS time than a sampled one.
|
|
19226
|
+
const live = ring.live === true
|
|
19227
|
+
const k = live ? 3 : 0
|
|
19228
|
+
const far = Math.hypot(pos.x - ring.pos[k], pos.y - ring.pos[k + 1], pos.z - ring.pos[k + 2]) >= step
|
|
19229
|
+
if (live && !(far && this.trailDue > 0)) {
|
|
19230
|
+
ring.pos[0] = pos.x
|
|
19231
|
+
ring.pos[1] = pos.y
|
|
19232
|
+
ring.pos[2] = pos.z
|
|
19233
|
+
ring.t[0] = this.sceneClock
|
|
19234
|
+
} else {
|
|
19235
|
+
// keep the head where it is as a point (live) or start a new head
|
|
19236
|
+
if (live) {
|
|
19237
|
+
ring.pos[0] = pos.x
|
|
19238
|
+
ring.pos[1] = pos.y
|
|
19239
|
+
ring.pos[2] = pos.z
|
|
19240
|
+
ring.t[0] = this.sceneClock
|
|
19241
|
+
ring.live = false
|
|
19242
|
+
} else {
|
|
19243
|
+
ring.pos.unshift(pos.x, pos.y, pos.z)
|
|
19244
|
+
ring.t.unshift(this.sceneClock)
|
|
19245
|
+
ring.live = !(far && this.trailDue > 0)
|
|
19246
|
+
}
|
|
19247
|
+
if (ring.t.length > TRAIL_SAMPLES) {
|
|
19248
|
+
ring.t.length = TRAIL_SAMPLES
|
|
19249
|
+
ring.pos.length = TRAIL_SAMPLES * 3
|
|
19250
|
+
}
|
|
19251
|
+
}
|
|
19252
|
+
} else if (this.trailDue > 0 || ring.pos.length === 0) {
|
|
18542
19253
|
// ONE sample per frame, never one per due tick. A frame that spanned
|
|
18543
19254
|
// several 60Hz ticks only knows where the bone is NOW, and unshifting that
|
|
18544
19255
|
// position once per tick fabricated duplicate samples — same point, same
|
|
@@ -18622,6 +19333,9 @@ export class Engine {
|
|
|
18622
19333
|
headSkin: () => (head >= 0 ? inst.model.getSkinMatrices().subarray(head * 16, head * 16 + 16) : null),
|
|
18623
19334
|
headRest,
|
|
18624
19335
|
layers: new Uint32Array(inst.objectLight.buffer)[0],
|
|
19336
|
+
additionalUv: (channel) => inst.model.getAdditionalUv(channel),
|
|
19337
|
+
morphs: inst.model.getMorphing().morphs,
|
|
19338
|
+
morphWeights: () => inst.model.getEffectiveMorphWeights(),
|
|
18625
19339
|
},
|
|
18626
19340
|
look,
|
|
18627
19341
|
)
|
|
@@ -18636,6 +19350,20 @@ export class Engine {
|
|
|
18636
19350
|
return this.nativeLooks?.ready(name) ?? Promise.resolve()
|
|
18637
19351
|
}
|
|
18638
19352
|
|
|
19353
|
+
/**
|
|
19354
|
+
* Set some of the values of one material a model's native look dresses —
|
|
19355
|
+
* its Unity material properties by name, colours linear, as the look's
|
|
19356
|
+
* `values` hold them — over the look's own, until set again. What a game
|
|
19357
|
+
* animates on a material over a take (its timeline's material curves)
|
|
19358
|
+
* reaches the shaders this way, one frame at a time: the caller samples its
|
|
19359
|
+
* keys on its own clock and sets what it got. Only the names the material's
|
|
19360
|
+
* look carries are taken, and a value equal to the one in place writes
|
|
19361
|
+
* nothing. False when the model wears no look dressing that material.
|
|
19362
|
+
*/
|
|
19363
|
+
setModelNativeUniforms(name: string, material: string, values: Record<string, number | ArrayLike<number>>): boolean {
|
|
19364
|
+
return this.nativeLooks?.setUniforms(name, material, values) ?? false
|
|
19365
|
+
}
|
|
19366
|
+
|
|
18639
19367
|
/** The hosts for the game's shaders: the scene pass's and the shadow atlas's,
|
|
18640
19368
|
* with the stand-ins an empty slot reads. Built on first use. */
|
|
18641
19369
|
private ensureNativeHosts(): NativeHost {
|
|
@@ -18682,6 +19410,18 @@ export class Engine {
|
|
|
18682
19410
|
white,
|
|
18683
19411
|
black,
|
|
18684
19412
|
blackCube: solid("native: black cube", [0, 0, 0, 255], true),
|
|
19413
|
+
// an empty volume (a froxel buffer no pass filled): no in-scatter, no extinction
|
|
19414
|
+
emptyVolume: (() => {
|
|
19415
|
+
const t = d.createTexture({
|
|
19416
|
+
label: "native: empty volume",
|
|
19417
|
+
size: [1, 1, 1],
|
|
19418
|
+
dimension: "3d",
|
|
19419
|
+
format: "rgba8unorm",
|
|
19420
|
+
usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST,
|
|
19421
|
+
})
|
|
19422
|
+
d.queue.writeTexture({ texture: t }, new Uint8Array([0, 0, 0, 0]), { bytesPerRow: 4, rowsPerImage: 1 }, [1, 1, 1])
|
|
19423
|
+
return t.createView({ dimension: "3d" })
|
|
19424
|
+
})(),
|
|
18685
19425
|
depth: this.nativeNoShadowView,
|
|
18686
19426
|
comparison: this.shadowComparisonSampler,
|
|
18687
19427
|
}
|
|
@@ -18720,7 +19460,8 @@ export class Engine {
|
|
|
18720
19460
|
{ mipmaps: (t, levels) => this.generateMipmaps(t, levels), fallback: this.nativeFallback },
|
|
18721
19461
|
onProgress,
|
|
18722
19462
|
)
|
|
18723
|
-
|
|
19463
|
+
// The stage's sky is in it: the far plane reaches as far as the stage does.
|
|
19464
|
+
this.raiseSceneExtent(this.nativeStage.extent)
|
|
18724
19465
|
}
|
|
18725
19466
|
|
|
18726
19467
|
/** What went wrong in the game's shaders, and the names nothing supplied. */
|
|
@@ -18770,11 +19511,17 @@ export class Engine {
|
|
|
18770
19511
|
let sunDirection: [number, number, number] = [dir.x, dir.y, dir.z]
|
|
18771
19512
|
let sunColor: [number, number, number] = [sun.color.x * k, sun.color.y * k, sun.color.z * k]
|
|
18772
19513
|
let sunShadow = this.sunShadow
|
|
19514
|
+
let sunUnity: NativeStageLights["main"]["unity"] | null = null
|
|
18773
19515
|
if (stage) {
|
|
18774
19516
|
// Its lamps are the stage's own; its key is the scene's sun, which the
|
|
18775
|
-
// host sets from the stage (and a person may then move)
|
|
19517
|
+
// host sets from the stage (and a person may then move) - along the
|
|
19518
|
+
// scene sun's direction, in the stage's own main-light colour. The scene
|
|
19519
|
+
// sun's colour is the cast's key, which the game keeps apart (a
|
|
19520
|
+
// character's light is _ProbeLightingBase, not the stage's main light).
|
|
18776
19521
|
const L = stage.pkg.lights
|
|
18777
19522
|
const sc = stage.pkg.scale
|
|
19523
|
+
sunColor = [L.main.color[0], L.main.color[1], L.main.color[2]]
|
|
19524
|
+
sunUnity = L.main.unity ?? null
|
|
18778
19525
|
lights.length = 0
|
|
18779
19526
|
for (const l of L.additional) {
|
|
18780
19527
|
lights.push({
|
|
@@ -18804,6 +19551,7 @@ export class Engine {
|
|
|
18804
19551
|
dt: 1 / 60,
|
|
18805
19552
|
sunDirection,
|
|
18806
19553
|
sunColor,
|
|
19554
|
+
sunUnity,
|
|
18807
19555
|
sunShadow,
|
|
18808
19556
|
lights,
|
|
18809
19557
|
ambientSH: sh ? Array.from(sh, (v) => v * s) : null,
|
|
@@ -18917,17 +19665,3 @@ function stableGlobals(prev: Record<string, NativeValue>, next: Record<string, N
|
|
|
18917
19665
|
}
|
|
18918
19666
|
return next
|
|
18919
19667
|
}
|
|
18920
|
-
|
|
18921
|
-
function sameValue(a: NativeValue, b: NativeValue): boolean {
|
|
18922
|
-
if (typeof a === "number" || typeof b === "number") return a === b
|
|
18923
|
-
if (a.length !== b.length) return false
|
|
18924
|
-
if (a instanceof Uint32Array !== b instanceof Uint32Array) return false
|
|
18925
|
-
for (let i = 0; i < a.length; i++) {
|
|
18926
|
-
const x = a[i] as number | ArrayLike<number>
|
|
18927
|
-
const y = b[i] as number | ArrayLike<number>
|
|
18928
|
-
if (typeof x === "number" || typeof y === "number") {
|
|
18929
|
-
if (x !== y) return false
|
|
18930
|
-
} else if (!sameValue(x, y)) return false
|
|
18931
|
-
}
|
|
18932
|
-
return true
|
|
18933
|
-
}
|