reze-engine 0.65.0 → 0.66.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/engine.d.ts +156 -42
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +874 -185
- 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 +947 -214
- 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 {
|
|
@@ -4755,8 +4828,11 @@ export class Engine {
|
|
|
4755
4828
|
[w, h],
|
|
4756
4829
|
)
|
|
4757
4830
|
}
|
|
4758
|
-
|
|
4759
|
-
|
|
4831
|
+
// The box chain above is the SOURCE: the levels the materials read are
|
|
4832
|
+
// the GGX convolution of it, one roughness per level (prefilterSky).
|
|
4833
|
+
const filtered = this.prefilterSky(tex, source.width, source.height, levels)
|
|
4834
|
+
this.worldEquirectTexture = filtered
|
|
4835
|
+
this.worldEquirectView = filtered.createView()
|
|
4760
4836
|
// The sky lights the scene, not only backs it. The sun keeps the toon
|
|
4761
4837
|
// ramp — this is the ambient term, exactly where the flat world colour
|
|
4762
4838
|
// used to sit.
|
|
@@ -4776,6 +4852,9 @@ export class Engine {
|
|
|
4776
4852
|
this.worldBindingsDirty = true
|
|
4777
4853
|
this.rebuildPerFrameBindGroups()
|
|
4778
4854
|
this.rebuildCompositeBindGroup()
|
|
4855
|
+
// The probe's compose step binds the sky it lays in behind the stage.
|
|
4856
|
+
this.dropProbeCapture()
|
|
4857
|
+
this.captureReflectionProbe()
|
|
4779
4858
|
if (this.device && this.compositeUniformBuffer) this.writeCompositeViewUniforms()
|
|
4780
4859
|
}
|
|
4781
4860
|
|
|
@@ -4902,7 +4981,7 @@ export class Engine {
|
|
|
4902
4981
|
params: Record<string, EffectParamValue> | undefined,
|
|
4903
4982
|
/** This effect's own declarations, already parsed by the caller — which had
|
|
4904
4983
|
* to read them anyway to build the scene table. */
|
|
4905
|
-
anchors: { bone: string; trail: boolean }[],
|
|
4984
|
+
anchors: { bone: string; trail: boolean; along?: number; step?: number }[],
|
|
4906
4985
|
/** Its row of that table: local slot → scene slot. */
|
|
4907
4986
|
alias: number[],
|
|
4908
4987
|
/** The pictures for `#textures`, as the host passed them. */
|
|
@@ -5574,7 +5653,7 @@ export class Engine {
|
|
|
5574
5653
|
/** What the file declared. Read here rather than re-parsed: the source no
|
|
5575
5654
|
* longer carries the lines, and two readers is how they drift. */
|
|
5576
5655
|
d: EffectDirectives,
|
|
5577
|
-
anchors: { bone: string; trail: boolean }[],
|
|
5656
|
+
anchors: { bone: string; trail: boolean; along?: number; step?: number }[],
|
|
5578
5657
|
/** This effect's local→scene slot map. Passed rather than read off the
|
|
5579
5658
|
* engine: the builders run BEFORE the swap, so this.anchorTable still
|
|
5580
5659
|
* describes the effect that is still on screen. */
|
|
@@ -5862,7 +5941,11 @@ export class Engine {
|
|
|
5862
5941
|
// shading ten layers and shading about three.
|
|
5863
5942
|
depthStencil: prepass
|
|
5864
5943
|
? { format: this.depthFormat, depthWriteEnabled: false, depthCompare: "equal" }
|
|
5865
|
-
:
|
|
5944
|
+
: d.depthAlways
|
|
5945
|
+
? // `#depth always`: over the scene, Unity's ZTest Always (a moon's halo
|
|
5946
|
+
// drawn in front of the sky dome it sits behind)
|
|
5947
|
+
{ format: this.depthFormat, depthWriteEnabled: false, depthCompare: "always" }
|
|
5948
|
+
: { format: this.depthFormat, depthWriteEnabled: cutout, depthCompare: this.depthAhead },
|
|
5866
5949
|
multisample: { count: Engine.MULTISAMPLE_COUNT },
|
|
5867
5950
|
}),
|
|
5868
5951
|
])
|
|
@@ -5946,7 +6029,7 @@ export class Engine {
|
|
|
5946
6029
|
wgsl: string,
|
|
5947
6030
|
count: number,
|
|
5948
6031
|
alias: number[],
|
|
5949
|
-
anchors: { bone: string; trail: boolean }[],
|
|
6032
|
+
anchors: { bone: string; trail: boolean; along?: number; step?: number }[],
|
|
5950
6033
|
params: EffectParamsBinding,
|
|
5951
6034
|
): Promise<{ ok: true; state: NonNullable<EffectInstance["lights"]> } | { ok: false; diagnostics: string[] }> {
|
|
5952
6035
|
const layout = this.device.createBindGroupLayout({
|
|
@@ -6200,7 +6283,7 @@ export class Engine {
|
|
|
6200
6283
|
/** What the file declared. Read here rather than re-parsed: the source no
|
|
6201
6284
|
* longer carries the lines, and two readers is how they drift. */
|
|
6202
6285
|
d: EffectDirectives,
|
|
6203
|
-
anchors: { bone: string; trail: boolean }[],
|
|
6286
|
+
anchors: { bone: string; trail: boolean; along?: number; step?: number }[],
|
|
6204
6287
|
/** This effect's local→scene slot map. Passed rather than read off the
|
|
6205
6288
|
* engine: the builders run BEFORE the swap, so this.anchorTable still
|
|
6206
6289
|
* describes the effect that is still on screen. */
|
|
@@ -6290,8 +6373,10 @@ export class Engine {
|
|
|
6290
6373
|
// point: a layer composited after tone mapping can never bloom.
|
|
6291
6374
|
//
|
|
6292
6375
|
// Additive, where this used to be MAX. Max was right for a post-tonemap
|
|
6293
|
-
// layer; in HDR before bloom, overlapping light sums.
|
|
6294
|
-
|
|
6376
|
+
// layer; in HDR before bloom, overlapping light sums. `#blend over` lays the
|
|
6377
|
+
// ribbon over the scene by its alpha instead — a game trail's own blend,
|
|
6378
|
+
// under which its dark smoke darkens rather than vanishing.
|
|
6379
|
+
const targets = sceneTargetsFor(d.trailBlend === "over" ? "trail-over" : "trail", this.sceneFormats)
|
|
6295
6380
|
this.device.pushErrorScope("validation")
|
|
6296
6381
|
try {
|
|
6297
6382
|
const pipeline = await this.device.createRenderPipelineAsync({
|
|
@@ -6709,7 +6794,7 @@ export class Engine {
|
|
|
6709
6794
|
/** What the file declared. Read here rather than re-parsed: the source no
|
|
6710
6795
|
* longer carries the lines, and two readers is how they drift. */
|
|
6711
6796
|
d: EffectDirectives,
|
|
6712
|
-
anchors: { bone: string; trail: boolean }[],
|
|
6797
|
+
anchors: { bone: string; trail: boolean; along?: number; step?: number }[],
|
|
6713
6798
|
/** This effect's local→scene slot map. Passed rather than read off the
|
|
6714
6799
|
* engine: the builders run BEFORE the swap, so this.anchorTable still
|
|
6715
6800
|
* describes the effect that is still on screen. */
|
|
@@ -7304,7 +7389,9 @@ export class Engine {
|
|
|
7304
7389
|
u[0] = Math.max(0, b.threshold)
|
|
7305
7390
|
u[1] = Math.max(0, b.threshold) * 0.5
|
|
7306
7391
|
u[2] = Engine.BLOOM_CLAMP
|
|
7307
|
-
|
|
7392
|
+
// URP's mapping: never all the way to the widest level, which alone is a
|
|
7393
|
+
// few pixels smeared over the screen — the glow would vanish.
|
|
7394
|
+
u[3] = 0.05 + 0.9 * Math.min(1, Math.max(0, b.scatter))
|
|
7308
7395
|
this.device.queue.writeBuffer(this.bloomUniformBuffer, 0, u)
|
|
7309
7396
|
}
|
|
7310
7397
|
|
|
@@ -7528,81 +7615,6 @@ export class Engine {
|
|
|
7528
7615
|
this.device.queue.submit([encoder.finish()])
|
|
7529
7616
|
}
|
|
7530
7617
|
|
|
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
7618
|
private createRenderPipeline(config: Parameters<Engine["renderPipelineDesc"]>[0]): GPURenderPipeline {
|
|
7607
7619
|
return this.createModelPipeline(this.renderPipelineDesc(config))
|
|
7608
7620
|
}
|
|
@@ -7611,7 +7623,7 @@ export class Engine {
|
|
|
7611
7623
|
label: string
|
|
7612
7624
|
layout: GPUPipelineLayout
|
|
7613
7625
|
shaderModule: GPUShaderModule
|
|
7614
|
-
vertexBuffers: GPUVertexBufferLayout[]
|
|
7626
|
+
vertexBuffers: (GPUVertexBufferLayout | null)[]
|
|
7615
7627
|
fragmentTarget?: GPUColorTargetState
|
|
7616
7628
|
fragmentTargets?: GPUColorTargetState[]
|
|
7617
7629
|
fragmentEntryPoint?: string
|
|
@@ -7928,7 +7940,22 @@ export class Engine {
|
|
|
7928
7940
|
// The same attachments blended as light — for a group that declared
|
|
7929
7941
|
// blend: "additive". See scene-contract's material-additive.
|
|
7930
7942
|
this.sceneTargetsAdditive = sceneTargetsFor("material-additive", this.sceneFormats)
|
|
7943
|
+
this.sceneTargetsPremultiplied = sceneTargetsFor("material-premultiplied", this.sceneFormats)
|
|
7931
7944
|
this.fullVertexBufferLayouts = fullVertexBuffers
|
|
7945
|
+
// Slot 3 stays empty: the outline hull binds its own stream there between
|
|
7946
|
+
// two material draws, and a material reading slot 3 would pick it up.
|
|
7947
|
+
const materialVertexBuffers: (GPUVertexBufferLayout | null)[] = [
|
|
7948
|
+
...fullVertexBuffers,
|
|
7949
|
+
null,
|
|
7950
|
+
{
|
|
7951
|
+
arrayStride: Engine.ATTRIBUTE_FLOATS * 4,
|
|
7952
|
+
attributes: [
|
|
7953
|
+
{ shaderLocation: 5, offset: 0, format: "float32x4" as GPUVertexFormat },
|
|
7954
|
+
{ shaderLocation: 6, offset: 4 * 4, format: "float32x2" as GPUVertexFormat },
|
|
7955
|
+
],
|
|
7956
|
+
},
|
|
7957
|
+
]
|
|
7958
|
+
this.materialVertexBufferLayouts = materialVertexBuffers
|
|
7932
7959
|
|
|
7933
7960
|
// group 0: per-frame (camera + light + sampler + shadow) — bound once per pass
|
|
7934
7961
|
this.mainPerFrameBindGroupLayout = this.device.createBindGroupLayout({
|
|
@@ -7944,14 +7971,19 @@ export class Engine {
|
|
|
7944
7971
|
// The positional lights. Always bound, empty or not, so every material
|
|
7945
7972
|
// pipeline shares one layout whether or not the scene has any.
|
|
7946
7973
|
{ 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.)
|
|
7974
|
+
// The world's sky (8). Always bound — the 1x1 fallback when the scene
|
|
7975
|
+
// has none — so every material pipeline keeps sharing one layout. (7
|
|
7976
|
+
// was the far cascade's map, which the atlas at 3 replaces.)
|
|
7951
7977
|
{ binding: 8, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: "float" } },
|
|
7952
|
-
|
|
7978
|
+
// The spot lamps' cookies (loadLightCookie), a 1x1 white until one loads.
|
|
7979
|
+
{ binding: 9, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: "float", viewDimension: "2d-array" } },
|
|
7953
7980
|
// The cast's shadow on a stage — setStageCastShadow.
|
|
7954
7981
|
{ binding: 10, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: "depth" } },
|
|
7982
|
+
// The stage's reflection probe and where it projects — see
|
|
7983
|
+
// captureReflectionProbe. Always bound: a 1x1 cube and the off flag
|
|
7984
|
+
// when there is no stage.
|
|
7985
|
+
{ binding: 11, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: "float", viewDimension: "cube" } },
|
|
7986
|
+
{ binding: 12, visibility: GPUShaderStage.FRAGMENT, buffer: { type: "uniform" } },
|
|
7955
7987
|
],
|
|
7956
7988
|
})
|
|
7957
7989
|
// group 1: per-instance (skinMats) — bound once per model
|
|
@@ -8024,7 +8056,7 @@ export class Engine {
|
|
|
8024
8056
|
label: "neutral base pipeline",
|
|
8025
8057
|
layout: mainPipelineLayout,
|
|
8026
8058
|
shaderModule: neutralModule,
|
|
8027
|
-
vertexBuffers:
|
|
8059
|
+
vertexBuffers: materialVertexBuffers,
|
|
8028
8060
|
fragmentTargets: sceneTargets,
|
|
8029
8061
|
cullMode: "none",
|
|
8030
8062
|
depthStencil: {
|
|
@@ -8042,7 +8074,7 @@ export class Engine {
|
|
|
8042
8074
|
label: "neutral base pipeline (no depth write)",
|
|
8043
8075
|
layout: mainPipelineLayout,
|
|
8044
8076
|
shaderModule: neutralModule,
|
|
8045
|
-
vertexBuffers:
|
|
8077
|
+
vertexBuffers: materialVertexBuffers,
|
|
8046
8078
|
fragmentTargets: sceneTargets,
|
|
8047
8079
|
cullMode: "none",
|
|
8048
8080
|
depthStencil: {
|
|
@@ -8212,7 +8244,6 @@ export class Engine {
|
|
|
8212
8244
|
})
|
|
8213
8245
|
|
|
8214
8246
|
// One-shot bake of Blender EEVEE's combined BRDF LUT (DFG + LTC packed rgba8unorm).
|
|
8215
|
-
this.bakeBrdfLut()
|
|
8216
8247
|
this.bakeGroundNoise()
|
|
8217
8248
|
// The mipmap blit for the formats model textures arrive in, built here so
|
|
8218
8249
|
// the first texture of the first model does not compile it synchronously
|
|
@@ -8269,6 +8300,48 @@ export class Engine {
|
|
|
8269
8300
|
usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST,
|
|
8270
8301
|
})
|
|
8271
8302
|
this.fallbackEquirectView = this.fallbackEquirectTexture.createView()
|
|
8303
|
+
// The reflection probe's resources, bound from the first frame: the cube
|
|
8304
|
+
// (empty and switched off until a stage is captured), its stand-in and the
|
|
8305
|
+
// uniforms. Before the per-frame groups, which bind all of them.
|
|
8306
|
+
this.probeTexture = this.device.createTexture({
|
|
8307
|
+
label: "reflection probe",
|
|
8308
|
+
size: [PROBE_SIZE, PROBE_SIZE, 6],
|
|
8309
|
+
format: "rgba16float",
|
|
8310
|
+
mipLevelCount: PROBE_LEVELS,
|
|
8311
|
+
usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.RENDER_ATTACHMENT,
|
|
8312
|
+
})
|
|
8313
|
+
this.probeView = this.probeTexture.createView({ dimension: "cube" })
|
|
8314
|
+
this.probeFallbackView = this.device
|
|
8315
|
+
.createTexture({
|
|
8316
|
+
label: "reflection probe stand-in",
|
|
8317
|
+
size: [1, 1, 6],
|
|
8318
|
+
format: "rgba16float",
|
|
8319
|
+
usage: GPUTextureUsage.TEXTURE_BINDING,
|
|
8320
|
+
})
|
|
8321
|
+
.createView({ dimension: "cube" })
|
|
8322
|
+
this.cookieTexture = this.device.createTexture({
|
|
8323
|
+
label: "light cookies (none yet)",
|
|
8324
|
+
size: [1, 1, 1],
|
|
8325
|
+
format: "rgba8unorm",
|
|
8326
|
+
usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST,
|
|
8327
|
+
})
|
|
8328
|
+
this.device.queue.writeTexture({ texture: this.cookieTexture }, new Uint8Array([255, 255, 255, 255]), { bytesPerRow: 4 }, [1, 1, 1])
|
|
8329
|
+
this.cookieView = this.cookieTexture.createView({ dimension: "2d-array" })
|
|
8330
|
+
this.probeUniformBuffer = this.device.createBuffer({
|
|
8331
|
+
label: "reflection probe uniforms",
|
|
8332
|
+
size: 48,
|
|
8333
|
+
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
|
|
8334
|
+
})
|
|
8335
|
+
this.probeOffBuffer = this.device.createBuffer({
|
|
8336
|
+
label: "reflection probe uniforms (off)",
|
|
8337
|
+
size: 48,
|
|
8338
|
+
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
|
|
8339
|
+
})
|
|
8340
|
+
this.probeCameraBuffer = this.device.createBuffer({
|
|
8341
|
+
label: "reflection probe capture camera",
|
|
8342
|
+
size: 40 * 4,
|
|
8343
|
+
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
|
|
8344
|
+
})
|
|
8272
8345
|
|
|
8273
8346
|
// Zero-filled = zero lines, which every accessor answers gracefully.
|
|
8274
8347
|
this.lyricsBuffer = this.device.createBuffer({
|
|
@@ -8316,6 +8389,8 @@ export class Engine {
|
|
|
8316
8389
|
{ binding: 13, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: "float" } },
|
|
8317
8390
|
// The mirror's plane, and whether this draw is inside the mirror pass.
|
|
8318
8391
|
{ binding: 14, visibility: GPUShaderStage.FRAGMENT, buffer: { type: "uniform" } },
|
|
8392
|
+
// The spot lamps' cookies — the floor takes their patterns too.
|
|
8393
|
+
{ binding: 15, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: "float", viewDimension: "2d-array" } },
|
|
8319
8394
|
],
|
|
8320
8395
|
})
|
|
8321
8396
|
this.groundShadowPipelineDesc = {
|
|
@@ -10391,6 +10466,415 @@ export class Engine {
|
|
|
10391
10466
|
* Retried at the top of a frame until one of them takes. */
|
|
10392
10467
|
private worldBindingsDirty = false
|
|
10393
10468
|
|
|
10469
|
+
// ─── The stage's reflection probe ───────────────────────────────────
|
|
10470
|
+
//
|
|
10471
|
+
// What a game bakes for every room: a cube of the stage itself, seen from
|
|
10472
|
+
// its middle, prefiltered down its mips by the specular lobe, and read by
|
|
10473
|
+
// every surface box-projected onto the stage's bounds — so a lacquered
|
|
10474
|
+
// counter reflects the bottles behind it and a wet floor the lamps above,
|
|
10475
|
+
// not a sky the room has no window onto. Captured here automatically once a
|
|
10476
|
+
// stage has loaded and its looks have settled; nothing to set.
|
|
10477
|
+
|
|
10478
|
+
/** Drop the capture's resources, so the next capture rebinds the sky. */
|
|
10479
|
+
private dropProbeCapture(): void {
|
|
10480
|
+
const cap = this.probeCapture
|
|
10481
|
+
if (!cap) return
|
|
10482
|
+
this.probeCapture = null
|
|
10483
|
+
void this.device.queue.onSubmittedWorkDone().then(() => {
|
|
10484
|
+
for (const t of cap.msTargets) t.destroy()
|
|
10485
|
+
cap.depth.destroy()
|
|
10486
|
+
cap.color.destroy()
|
|
10487
|
+
cap.aux.destroy()
|
|
10488
|
+
cap.cube.destroy()
|
|
10489
|
+
cap.steps.destroy()
|
|
10490
|
+
})
|
|
10491
|
+
}
|
|
10492
|
+
|
|
10493
|
+
/** Ask for a fresh capture of the stage's reflection probe. It runs at the
|
|
10494
|
+
* start of a frame once the scene has held still for a moment — asked for
|
|
10495
|
+
* by the engine itself whenever the stage or its looks change; a host may
|
|
10496
|
+
* call it after changing something the engine cannot see. */
|
|
10497
|
+
captureReflectionProbe(): void {
|
|
10498
|
+
if (!this.probeDirty) this.probeDirtyFrame = this.probeFrame
|
|
10499
|
+
this.probeDirty = true
|
|
10500
|
+
}
|
|
10501
|
+
|
|
10502
|
+
/** The stage's world-space bounds: every stage model's material boxes
|
|
10503
|
+
* through its placement. Null without a stage. */
|
|
10504
|
+
private stageBounds(): { min: [number, number, number]; max: [number, number, number] } | null {
|
|
10505
|
+
const min: [number, number, number] = [Infinity, Infinity, Infinity]
|
|
10506
|
+
const max: [number, number, number] = [-Infinity, -Infinity, -Infinity]
|
|
10507
|
+
const p = new Vec3(0, 0, 0)
|
|
10508
|
+
let any = false
|
|
10509
|
+
for (const inst of this.modelInstances.values()) {
|
|
10510
|
+
if (!inst.isStage || !inst.model.visible) continue
|
|
10511
|
+
const m = inst.model
|
|
10512
|
+
for (const draw of inst.drawCalls) {
|
|
10513
|
+
const b = draw.bounds
|
|
10514
|
+
if (!b || b.length < 6 || !(b[0] <= b[3])) continue
|
|
10515
|
+
// The eight corners of the material's model-space box, placed.
|
|
10516
|
+
for (let k = 0; k < 8; k++) {
|
|
10517
|
+
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)
|
|
10518
|
+
Quat.rotateVecInto(m.rotation, p, p)
|
|
10519
|
+
const w = [p.x + m.position.x, p.y + m.position.y, p.z + m.position.z]
|
|
10520
|
+
for (let a = 0; a < 3; a++) {
|
|
10521
|
+
if (w[a] < min[a]) min[a] = w[a]
|
|
10522
|
+
if (w[a] > max[a]) max[a] = w[a]
|
|
10523
|
+
}
|
|
10524
|
+
any = true
|
|
10525
|
+
}
|
|
10526
|
+
}
|
|
10527
|
+
}
|
|
10528
|
+
return any ? { min, max } : null
|
|
10529
|
+
}
|
|
10530
|
+
|
|
10531
|
+
/** Run a pending capture once the scene has settled — see captureReflectionProbe. */
|
|
10532
|
+
private maybeCaptureProbe(): void {
|
|
10533
|
+
this.probeFrame++
|
|
10534
|
+
if (!this.probeDirty || !this.probeCaptureBindGroup) return
|
|
10535
|
+
if (this.probeFrame - this.probeDirtyFrame < Engine.PROBE_SETTLE_FRAMES) return
|
|
10536
|
+
this.probeDirty = false
|
|
10537
|
+
const bounds = this.stageBounds()
|
|
10538
|
+
if (!bounds) {
|
|
10539
|
+
// No stage: the probe goes dark and every reader falls back to the sky.
|
|
10540
|
+
this.probeData.fill(0)
|
|
10541
|
+
this.device.queue.writeBuffer(this.probeUniformBuffer, 0, this.probeData)
|
|
10542
|
+
return
|
|
10543
|
+
}
|
|
10544
|
+
try {
|
|
10545
|
+
this.renderProbe(bounds)
|
|
10546
|
+
} catch (e) {
|
|
10547
|
+
console.warn("[reze-engine] reflection probe capture failed:", e)
|
|
10548
|
+
}
|
|
10549
|
+
}
|
|
10550
|
+
|
|
10551
|
+
private ensureProbeCapture(): NonNullable<Engine["probeCapture"]> {
|
|
10552
|
+
if (this.probeCapture) return this.probeCapture
|
|
10553
|
+
const size = PROBE_CAPTURE_SIZE
|
|
10554
|
+
const formats = sceneColorFormats(this.sceneFormats)
|
|
10555
|
+
const msTargets = formats.map((format, i) =>
|
|
10556
|
+
this.device.createTexture({
|
|
10557
|
+
label: `reflection probe capture target ${i}`,
|
|
10558
|
+
size: [size, size],
|
|
10559
|
+
sampleCount: Engine.MULTISAMPLE_COUNT,
|
|
10560
|
+
format,
|
|
10561
|
+
usage: GPUTextureUsage.RENDER_ATTACHMENT,
|
|
10562
|
+
}),
|
|
10563
|
+
)
|
|
10564
|
+
const depth = this.device.createTexture({
|
|
10565
|
+
label: "reflection probe capture depth",
|
|
10566
|
+
size: [size, size],
|
|
10567
|
+
sampleCount: Engine.MULTISAMPLE_COUNT,
|
|
10568
|
+
format: this.depthFormat,
|
|
10569
|
+
usage: GPUTextureUsage.RENDER_ATTACHMENT,
|
|
10570
|
+
})
|
|
10571
|
+
const layered = (label: string, format: GPUTextureFormat) =>
|
|
10572
|
+
this.device.createTexture({
|
|
10573
|
+
label,
|
|
10574
|
+
size: [size, size, 6],
|
|
10575
|
+
format,
|
|
10576
|
+
usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.TEXTURE_BINDING,
|
|
10577
|
+
})
|
|
10578
|
+
// The resolves take the scene pass's own formats: colour and coverage.
|
|
10579
|
+
const color = layered("reflection probe capture colour", formats[0])
|
|
10580
|
+
const aux = layered("reflection probe capture coverage", formats[1])
|
|
10581
|
+
const cube = this.device.createTexture({
|
|
10582
|
+
label: "reflection probe capture cube",
|
|
10583
|
+
size: [size, size, 6],
|
|
10584
|
+
format: "rgba16float",
|
|
10585
|
+
mipLevelCount: PROBE_CAPTURE_LEVELS,
|
|
10586
|
+
usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.TEXTURE_BINDING,
|
|
10587
|
+
})
|
|
10588
|
+
// One 256-byte slot per step: 6 composes, then the capture's mips, then
|
|
10589
|
+
// the probe's levels, six faces each.
|
|
10590
|
+
const slots = 6 + PROBE_CAPTURE_LEVELS * 6 + PROBE_LEVELS * 6
|
|
10591
|
+
const steps = this.device.createBuffer({
|
|
10592
|
+
label: "reflection probe steps",
|
|
10593
|
+
size: slots * 256,
|
|
10594
|
+
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
|
|
10595
|
+
})
|
|
10596
|
+
const composeModule = this.device.createShaderModule({ label: "reflection probe compose", code: PROBE_COMPOSE_WGSL })
|
|
10597
|
+
const composePipeline = this.device.createRenderPipeline({
|
|
10598
|
+
label: "reflection probe compose",
|
|
10599
|
+
layout: "auto",
|
|
10600
|
+
vertex: { module: composeModule, entryPoint: "vs_full" },
|
|
10601
|
+
fragment: { module: composeModule, entryPoint: "fs_compose", targets: [{ format: "rgba16float" }] },
|
|
10602
|
+
primitive: { topology: "triangle-list" },
|
|
10603
|
+
})
|
|
10604
|
+
const filterModule = this.device.createShaderModule({ label: "reflection probe filter", code: PROBE_FILTER_WGSL })
|
|
10605
|
+
const filterPipeline = this.device.createRenderPipeline({
|
|
10606
|
+
label: "reflection probe filter",
|
|
10607
|
+
layout: "auto",
|
|
10608
|
+
vertex: { module: filterModule, entryPoint: "vs_full" },
|
|
10609
|
+
fragment: { module: filterModule, entryPoint: "fs_filter", targets: [{ format: "rgba16float" }] },
|
|
10610
|
+
primitive: { topology: "triangle-list" },
|
|
10611
|
+
})
|
|
10612
|
+
const colorArray = color.createView({ dimension: "2d-array" })
|
|
10613
|
+
const auxArray = aux.createView({ dimension: "2d-array" })
|
|
10614
|
+
const composeLayout = composePipeline.getBindGroupLayout(0)
|
|
10615
|
+
const composeGroups: GPUBindGroup[] = []
|
|
10616
|
+
for (let f = 0; f < 6; f++) {
|
|
10617
|
+
composeGroups.push(
|
|
10618
|
+
this.device.createBindGroup({
|
|
10619
|
+
label: `reflection probe compose ${f}`,
|
|
10620
|
+
layout: composeLayout,
|
|
10621
|
+
entries: [
|
|
10622
|
+
{ binding: 1, resource: { buffer: this.lightUniformBuffer } },
|
|
10623
|
+
{ binding: 2, resource: this.materialSampler },
|
|
10624
|
+
{ binding: 8, resource: this.worldEquirectView ?? this.fallbackEquirectView },
|
|
10625
|
+
{ binding: 20, resource: colorArray },
|
|
10626
|
+
{ binding: 21, resource: auxArray },
|
|
10627
|
+
{ binding: 22, resource: { buffer: steps, offset: f * 256, size: 32 } },
|
|
10628
|
+
],
|
|
10629
|
+
}),
|
|
10630
|
+
)
|
|
10631
|
+
}
|
|
10632
|
+
const filterLayout = filterPipeline.getBindGroupLayout(0)
|
|
10633
|
+
const cubeView = cube.createView({ dimension: "cube" })
|
|
10634
|
+
const filterGroups: GPUBindGroup[] = []
|
|
10635
|
+
for (let i = 0; i < PROBE_LEVELS * 6; i++) {
|
|
10636
|
+
filterGroups.push(
|
|
10637
|
+
this.device.createBindGroup({
|
|
10638
|
+
label: `reflection probe level ${i}`,
|
|
10639
|
+
layout: filterLayout,
|
|
10640
|
+
entries: [
|
|
10641
|
+
{ binding: 0, resource: cubeView },
|
|
10642
|
+
{ binding: 1, resource: this.materialSampler },
|
|
10643
|
+
{ binding: 2, resource: { buffer: steps, offset: (6 + PROBE_CAPTURE_LEVELS * 6 + i) * 256, size: 32 } },
|
|
10644
|
+
],
|
|
10645
|
+
}),
|
|
10646
|
+
)
|
|
10647
|
+
}
|
|
10648
|
+
this.probeCapture = {
|
|
10649
|
+
msTargets,
|
|
10650
|
+
depth,
|
|
10651
|
+
color,
|
|
10652
|
+
aux,
|
|
10653
|
+
cube,
|
|
10654
|
+
steps,
|
|
10655
|
+
composePipeline,
|
|
10656
|
+
filterPipeline,
|
|
10657
|
+
composeGroups,
|
|
10658
|
+
filterGroups,
|
|
10659
|
+
}
|
|
10660
|
+
return this.probeCapture
|
|
10661
|
+
}
|
|
10662
|
+
|
|
10663
|
+
/**
|
|
10664
|
+
* Capture the probe: the stage drawn six times from one point into a 128²
|
|
10665
|
+
* cube, the sky laid in behind it, a mip chain, and the GGX convolution
|
|
10666
|
+
* into the 64² cube the materials read.
|
|
10667
|
+
*
|
|
10668
|
+
* WHERE: the middle of the stage's bounds across the floor, at about a
|
|
10669
|
+
* character's chest height above the floor (y = 0 where the bounds span it,
|
|
10670
|
+
* the MMD convention, else their bottom) — where a game puts the probe of a
|
|
10671
|
+
* room the player stands in. The box it projects onto is the bounds.
|
|
10672
|
+
*
|
|
10673
|
+
* WHAT: the stage alone, in its own looks, lit by the scene's sun, lamps
|
|
10674
|
+
* and sky as they stand. The cast is not in it (a baked probe never holds
|
|
10675
|
+
* the characters), nor are effects.
|
|
10676
|
+
*/
|
|
10677
|
+
private renderProbe(bounds: { min: [number, number, number]; max: [number, number, number] }): void {
|
|
10678
|
+
const cap = this.ensureProbeCapture()
|
|
10679
|
+
const { min, max } = bounds
|
|
10680
|
+
const floor = min[1] <= 0 && max[1] >= 0 ? 0 : min[1]
|
|
10681
|
+
const eye: [number, number, number] = [
|
|
10682
|
+
(min[0] + max[0]) * 0.5,
|
|
10683
|
+
Math.min(floor + 12, (min[1] + max[1]) * 0.5),
|
|
10684
|
+
(min[2] + max[2]) * 0.5,
|
|
10685
|
+
]
|
|
10686
|
+
const span = Math.hypot(max[0] - min[0], max[1] - min[1], max[2] - min[2])
|
|
10687
|
+
const near = 0.25
|
|
10688
|
+
const far = Math.max(span * 2, 100)
|
|
10689
|
+
// Each face's forward and up, in the convention the GPU samples a cube
|
|
10690
|
+
// with (see rz_face_dir in passes/probe.ts): +X, -X, +Y, -Y, +Z, -Z.
|
|
10691
|
+
const faces: [number, number, number, number, number, number][] = [
|
|
10692
|
+
[1, 0, 0, 0, 1, 0],
|
|
10693
|
+
[-1, 0, 0, 0, 1, 0],
|
|
10694
|
+
[0, 1, 0, 0, 0, -1],
|
|
10695
|
+
[0, -1, 0, 0, 0, 1],
|
|
10696
|
+
[0, 0, 1, 0, 1, 0],
|
|
10697
|
+
[0, 0, -1, 0, 1, 0],
|
|
10698
|
+
]
|
|
10699
|
+
const cam = this.probeCameraData
|
|
10700
|
+
const view = { perFrame: this.probeCaptureBindGroup!, args: "probe" as const, outlines: false }
|
|
10701
|
+
const formats = sceneColorFormats(this.sceneFormats)
|
|
10702
|
+
for (let f = 0; f < 6; f++) {
|
|
10703
|
+
const [fx, fy, fz, ux, uy, uz] = faces[f]
|
|
10704
|
+
Mat4.lookAtInto(cam.subarray(0, 16), eye[0], eye[1], eye[2], eye[0] + fx, eye[1] + fy, eye[2] + fz, ux, uy, uz)
|
|
10705
|
+
if (this.reversedZ) Mat4.perspectiveInto(cam.subarray(16, 32), Math.PI / 2, 1, far, near)
|
|
10706
|
+
else Mat4.perspectiveInto(cam.subarray(16, 32), Math.PI / 2, 1, near, far)
|
|
10707
|
+
cam[32] = eye[0]
|
|
10708
|
+
cam[33] = eye[1]
|
|
10709
|
+
cam[34] = eye[2]
|
|
10710
|
+
cam[35] = PROBE_CAPTURE_SIZE
|
|
10711
|
+
cam[36] = this.sceneClock
|
|
10712
|
+
this.device.queue.writeBuffer(this.probeCameraBuffer, 0, cam)
|
|
10713
|
+
const encoder = this.device.createCommandEncoder({ label: `reflection probe face ${f}` })
|
|
10714
|
+
const colorAttachments: GPURenderPassColorAttachment[] = formats.map((_, i) => ({
|
|
10715
|
+
view: cap.msTargets[i].createView(),
|
|
10716
|
+
resolveTarget:
|
|
10717
|
+
i === 0
|
|
10718
|
+
? cap.color.createView({ dimension: "2d", baseArrayLayer: f, arrayLayerCount: 1 })
|
|
10719
|
+
: i === 1
|
|
10720
|
+
? cap.aux.createView({ dimension: "2d", baseArrayLayer: f, arrayLayerCount: 1 })
|
|
10721
|
+
: undefined,
|
|
10722
|
+
clearValue: { r: 0, g: 0, b: 0, a: 0 },
|
|
10723
|
+
loadOp: "clear",
|
|
10724
|
+
storeOp: "discard",
|
|
10725
|
+
}))
|
|
10726
|
+
const pass = encoder.beginRenderPass({
|
|
10727
|
+
label: `reflection probe face ${f}`,
|
|
10728
|
+
colorAttachments,
|
|
10729
|
+
depthStencilAttachment: {
|
|
10730
|
+
view: cap.depth.createView(),
|
|
10731
|
+
depthClearValue: this.depthClear,
|
|
10732
|
+
depthLoadOp: "clear",
|
|
10733
|
+
depthStoreOp: "discard",
|
|
10734
|
+
stencilClearValue: 0,
|
|
10735
|
+
stencilLoadOp: "clear",
|
|
10736
|
+
stencilStoreOp: "discard",
|
|
10737
|
+
},
|
|
10738
|
+
})
|
|
10739
|
+
pass.setStencilReference(Engine.STENCIL_EYE_VALUE)
|
|
10740
|
+
for (const inst of this.modelInstances.values()) {
|
|
10741
|
+
if (!inst.isStage || !inst.model.visible) continue
|
|
10742
|
+
this.setModelDrawState(pass, inst)
|
|
10743
|
+
this.drawMaterials(pass, inst, "opaque", view)
|
|
10744
|
+
}
|
|
10745
|
+
for (const inst of this.modelInstances.values()) {
|
|
10746
|
+
if (!inst.isStage || !inst.model.visible) continue
|
|
10747
|
+
this.setModelDrawState(pass, inst)
|
|
10748
|
+
this.drawMaterials(pass, inst, "transparent", view)
|
|
10749
|
+
}
|
|
10750
|
+
pass.end()
|
|
10751
|
+
this.device.queue.submit([encoder.finish()])
|
|
10752
|
+
}
|
|
10753
|
+
|
|
10754
|
+
// The steps: compose, capture mips, then the probe's levels.
|
|
10755
|
+
const steps = new ArrayBuffer((6 + PROBE_CAPTURE_LEVELS * 6 + PROBE_LEVELS * 6) * 256)
|
|
10756
|
+
const put = (slot: number, face: number, lod: number, roughness: number, srcSize: number, mode: number) => {
|
|
10757
|
+
const u = new Uint32Array(steps, slot * 256, 8)
|
|
10758
|
+
const fl = new Float32Array(steps, slot * 256, 8)
|
|
10759
|
+
u[0] = face
|
|
10760
|
+
fl[1] = lod
|
|
10761
|
+
fl[2] = roughness
|
|
10762
|
+
fl[3] = srcSize
|
|
10763
|
+
u[4] = mode
|
|
10764
|
+
}
|
|
10765
|
+
for (let f = 0; f < 6; f++) put(f, f, 0, 0, PROBE_CAPTURE_SIZE, 0)
|
|
10766
|
+
for (let m = 0; m < PROBE_CAPTURE_LEVELS; m++)
|
|
10767
|
+
for (let f = 0; f < 6; f++) put(6 + m * 6 + f, f, Math.max(m - 1, 0), 0, PROBE_CAPTURE_SIZE, 0)
|
|
10768
|
+
for (let m = 0; m < PROBE_LEVELS; m++)
|
|
10769
|
+
for (let f = 0; f < 6; f++)
|
|
10770
|
+
// Level 0 is the capture at the probe's size: one tap of the 128²
|
|
10771
|
+
// level-0 cube at a 64² texel's centre is its 2×2 average.
|
|
10772
|
+
put(6 + PROBE_CAPTURE_LEVELS * 6 + m * 6 + f, f, 0, probeLevelRoughness(m), PROBE_CAPTURE_SIZE, m === 0 ? 0 : 1)
|
|
10773
|
+
this.device.queue.writeBuffer(cap.steps, 0, steps)
|
|
10774
|
+
|
|
10775
|
+
const encoder = this.device.createCommandEncoder({ label: "reflection probe filter" })
|
|
10776
|
+
const run = (target: GPUTextureView, pipeline: GPURenderPipeline, group: GPUBindGroup) => {
|
|
10777
|
+
const p = encoder.beginRenderPass({ colorAttachments: [{ view: target, loadOp: "clear", storeOp: "store" }] })
|
|
10778
|
+
p.setPipeline(pipeline)
|
|
10779
|
+
p.setBindGroup(0, group)
|
|
10780
|
+
p.draw(3)
|
|
10781
|
+
p.end()
|
|
10782
|
+
}
|
|
10783
|
+
const face = (tex: GPUTexture, level: number, f: number) =>
|
|
10784
|
+
tex.createView({ dimension: "2d", baseMipLevel: level, mipLevelCount: 1, baseArrayLayer: f, arrayLayerCount: 1 })
|
|
10785
|
+
for (let f = 0; f < 6; f++) run(face(cap.cube, 0, f), cap.composePipeline, cap.composeGroups[f])
|
|
10786
|
+
// Each level from the one above. The source view spans the whole chain,
|
|
10787
|
+
// which a pass writing one of its levels may not bind — so the reads go
|
|
10788
|
+
// through a view of the levels ABOVE the one being written.
|
|
10789
|
+
for (let m = 1; m < PROBE_CAPTURE_LEVELS; m++) {
|
|
10790
|
+
const above = cap.cube.createView({ dimension: "cube", baseMipLevel: 0, mipLevelCount: m })
|
|
10791
|
+
for (let f = 0; f < 6; f++) {
|
|
10792
|
+
const group = this.device.createBindGroup({
|
|
10793
|
+
label: `reflection probe capture mip ${m}/${f}`,
|
|
10794
|
+
layout: cap.filterPipeline.getBindGroupLayout(0),
|
|
10795
|
+
entries: [
|
|
10796
|
+
{ binding: 0, resource: above },
|
|
10797
|
+
{ binding: 1, resource: this.materialSampler },
|
|
10798
|
+
{ binding: 2, resource: { buffer: cap.steps, offset: (6 + m * 6 + f) * 256, size: 32 } },
|
|
10799
|
+
],
|
|
10800
|
+
})
|
|
10801
|
+
run(face(cap.cube, m, f), cap.filterPipeline, group)
|
|
10802
|
+
}
|
|
10803
|
+
}
|
|
10804
|
+
for (let m = 0; m < PROBE_LEVELS; m++)
|
|
10805
|
+
for (let f = 0; f < 6; f++) run(face(this.probeTexture, m, f), cap.filterPipeline, cap.filterGroups[m * 6 + f])
|
|
10806
|
+
this.device.queue.submit([encoder.finish()])
|
|
10807
|
+
|
|
10808
|
+
this.probeData.set([eye[0], eye[1], eye[2], 1, min[0], min[1], min[2], 1, max[0], max[1], max[2], 0])
|
|
10809
|
+
this.device.queue.writeBuffer(this.probeUniformBuffer, 0, this.probeData)
|
|
10810
|
+
}
|
|
10811
|
+
|
|
10812
|
+
/**
|
|
10813
|
+
* The sky's mips, convolved with the GGX lobe each stands for — the box
|
|
10814
|
+
* average the upload built is only the source. Level 0 is copied as it is
|
|
10815
|
+
* (it is also what the backdrop draws); every level below it becomes the
|
|
10816
|
+
* sky as a surface of that roughness reflects it (skyLevelRoughness).
|
|
10817
|
+
*/
|
|
10818
|
+
private prefilterSky(source: GPUTexture, width: number, height: number, levels: number): GPUTexture {
|
|
10819
|
+
if (levels <= 1) return source
|
|
10820
|
+
if (!this.skyPrefilterPipeline) {
|
|
10821
|
+
const module = this.device.createShaderModule({ label: "sky prefilter", code: SKY_PREFILTER_WGSL })
|
|
10822
|
+
this.skyPrefilterPipeline = this.device.createRenderPipeline({
|
|
10823
|
+
label: "sky prefilter",
|
|
10824
|
+
layout: "auto",
|
|
10825
|
+
vertex: { module, entryPoint: "vs_full" },
|
|
10826
|
+
fragment: { module, entryPoint: "fs_sky", targets: [{ format: "rgba16float" }] },
|
|
10827
|
+
primitive: { topology: "triangle-list" },
|
|
10828
|
+
})
|
|
10829
|
+
}
|
|
10830
|
+
const pipeline = this.skyPrefilterPipeline
|
|
10831
|
+
const out = this.device.createTexture({
|
|
10832
|
+
label: "world equirect (HDR, GGX-prefiltered)",
|
|
10833
|
+
size: [width, height],
|
|
10834
|
+
format: "rgba16float",
|
|
10835
|
+
mipLevelCount: levels,
|
|
10836
|
+
usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST | GPUTextureUsage.RENDER_ATTACHMENT,
|
|
10837
|
+
})
|
|
10838
|
+
const encoder = this.device.createCommandEncoder({ label: "sky prefilter" })
|
|
10839
|
+
encoder.copyTextureToTexture({ texture: source, mipLevel: 0 }, { texture: out, mipLevel: 0 }, [width, height])
|
|
10840
|
+
const params = this.device.createBuffer({
|
|
10841
|
+
label: "sky prefilter steps",
|
|
10842
|
+
size: levels * 256,
|
|
10843
|
+
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
|
|
10844
|
+
})
|
|
10845
|
+
const data = new Float32Array(levels * 64)
|
|
10846
|
+
for (let m = 1; m < levels; m++) data.set([skyLevelRoughness(m, levels), width, height, 0], m * 64)
|
|
10847
|
+
this.device.queue.writeBuffer(params, 0, data)
|
|
10848
|
+
const srcView = source.createView()
|
|
10849
|
+
for (let m = 1; m < levels; m++) {
|
|
10850
|
+
const group = this.device.createBindGroup({
|
|
10851
|
+
label: `sky prefilter ${m}`,
|
|
10852
|
+
layout: pipeline.getBindGroupLayout(0),
|
|
10853
|
+
entries: [
|
|
10854
|
+
{ binding: 0, resource: srcView },
|
|
10855
|
+
{ binding: 1, resource: this.materialSampler },
|
|
10856
|
+
{ binding: 2, resource: { buffer: params, offset: m * 256, size: 16 } },
|
|
10857
|
+
],
|
|
10858
|
+
})
|
|
10859
|
+
const p = encoder.beginRenderPass({
|
|
10860
|
+
label: `sky prefilter ${m}`,
|
|
10861
|
+
colorAttachments: [
|
|
10862
|
+
{ view: out.createView({ baseMipLevel: m, mipLevelCount: 1 }), loadOp: "clear", storeOp: "store" },
|
|
10863
|
+
],
|
|
10864
|
+
})
|
|
10865
|
+
p.setPipeline(pipeline)
|
|
10866
|
+
p.setBindGroup(0, group)
|
|
10867
|
+
p.draw(3)
|
|
10868
|
+
p.end()
|
|
10869
|
+
}
|
|
10870
|
+
this.device.queue.submit([encoder.finish()])
|
|
10871
|
+
// The box chain and the steps are done with once that work is: the source
|
|
10872
|
+
// retires with the skies (destroyed at the next frame's start).
|
|
10873
|
+
this.retiredSkies.push(source)
|
|
10874
|
+
void this.device.queue.onSubmittedWorkDone().then(() => params.destroy())
|
|
10875
|
+
return out
|
|
10876
|
+
}
|
|
10877
|
+
|
|
10394
10878
|
/** Returns whether it actually rebuilt — see rebuildCompositeBindGroup. */
|
|
10395
10879
|
private rebuildPerFrameBindGroups(): boolean {
|
|
10396
10880
|
// EVERY resource, not just the layout. This runs twice: once at init, where
|
|
@@ -10409,7 +10893,7 @@ export class Engine {
|
|
|
10409
10893
|
!this.shadowLightVPBuffer ||
|
|
10410
10894
|
!this.materialSampler ||
|
|
10411
10895
|
!this.shadowComparisonSampler ||
|
|
10412
|
-
!this.
|
|
10896
|
+
!this.probeUniformBuffer ||
|
|
10413
10897
|
!(this.worldEquirectView ?? this.fallbackEquirectView)
|
|
10414
10898
|
) {
|
|
10415
10899
|
return false
|
|
@@ -10432,8 +10916,31 @@ export class Engine {
|
|
|
10432
10916
|
{ binding: 5, resource: { buffer: this.shadowLightVPBuffer } },
|
|
10433
10917
|
{ binding: 6, resource: { buffer: this.lightsBuffer } },
|
|
10434
10918
|
{ binding: 8, resource: env },
|
|
10435
|
-
{ binding: 9, resource: this.
|
|
10919
|
+
{ binding: 9, resource: this.cookieView },
|
|
10436
10920
|
{ binding: 10, resource: this.castShadowView },
|
|
10921
|
+
{ binding: 11, resource: this.probeView },
|
|
10922
|
+
{ binding: 12, resource: { buffer: this.probeUniformBuffer } },
|
|
10923
|
+
],
|
|
10924
|
+
})
|
|
10925
|
+
// The probe's own view of the scene: one cube face at a time through its
|
|
10926
|
+
// capture camera, and the probe itself switched off — a material inside
|
|
10927
|
+
// the capture reflects the sky, never the cube being filled.
|
|
10928
|
+
this.probeCaptureBindGroup = this.device.createBindGroup({
|
|
10929
|
+
label: "reflection probe capture per-frame bind group",
|
|
10930
|
+
layout: this.mainPerFrameBindGroupLayout,
|
|
10931
|
+
entries: [
|
|
10932
|
+
{ binding: 0, resource: { buffer: this.probeCameraBuffer } },
|
|
10933
|
+
{ binding: 1, resource: { buffer: this.lightUniformBuffer } },
|
|
10934
|
+
{ binding: 2, resource: this.materialSampler },
|
|
10935
|
+
{ binding: 3, resource: this.shadowAtlasView },
|
|
10936
|
+
{ binding: 4, resource: this.shadowComparisonSampler },
|
|
10937
|
+
{ binding: 5, resource: { buffer: this.shadowLightVPBuffer } },
|
|
10938
|
+
{ binding: 6, resource: { buffer: this.lightsBuffer } },
|
|
10939
|
+
{ binding: 8, resource: env },
|
|
10940
|
+
{ binding: 9, resource: this.cookieView },
|
|
10941
|
+
{ binding: 10, resource: this.castShadowView },
|
|
10942
|
+
{ binding: 11, resource: this.probeFallbackView },
|
|
10943
|
+
{ binding: 12, resource: { buffer: this.probeOffBuffer } },
|
|
10437
10944
|
],
|
|
10438
10945
|
})
|
|
10439
10946
|
this.mirrorPerFrameBindGroup = this.device.createBindGroup({
|
|
@@ -10448,8 +10955,10 @@ export class Engine {
|
|
|
10448
10955
|
{ binding: 5, resource: { buffer: this.shadowLightVPBuffer } },
|
|
10449
10956
|
{ binding: 6, resource: { buffer: this.lightsBuffer } },
|
|
10450
10957
|
{ binding: 8, resource: env },
|
|
10451
|
-
{ binding: 9, resource: this.
|
|
10958
|
+
{ binding: 9, resource: this.cookieView },
|
|
10452
10959
|
{ binding: 10, resource: this.castShadowView },
|
|
10960
|
+
{ binding: 11, resource: this.probeView },
|
|
10961
|
+
{ binding: 12, resource: { buffer: this.probeUniformBuffer } },
|
|
10453
10962
|
],
|
|
10454
10963
|
})
|
|
10455
10964
|
return true
|
|
@@ -10511,13 +11020,13 @@ export class Engine {
|
|
|
10511
11020
|
this.lightData[base + 4] = this.sun.color.x
|
|
10512
11021
|
this.lightData[base + 5] = this.sun.color.y
|
|
10513
11022
|
this.lightData[base + 6] = this.sun.color.z
|
|
11023
|
+
// Unity's light intensity: a white sun of 1 lights albedo 1:1 at N·L = 1.
|
|
10514
11024
|
this.lightData[base + 7] = this.sun.strength
|
|
10515
11025
|
this.lightDataWords[DIR_LAYERS_AT + index] = this.sunLayers
|
|
10516
11026
|
if (index >= this.lightCount) this.lightCount = index + 1
|
|
10517
11027
|
this.updateLightBuffer()
|
|
10518
11028
|
}
|
|
10519
11029
|
|
|
10520
|
-
/** Update the world environment (Blender: World Background). Ambient recomputes immediately. */
|
|
10521
11030
|
/**
|
|
10522
11031
|
* Distance fog on every lit surface, as a game on SimPipeline lays it: per
|
|
10523
11032
|
* layer, t = saturate(f − (1 − f)·h) with f = saturate(depth·distance[0] +
|
|
@@ -10642,7 +11151,7 @@ export class Engine {
|
|
|
10642
11151
|
this.writeWorld()
|
|
10643
11152
|
}
|
|
10644
11153
|
|
|
10645
|
-
/** Update the
|
|
11154
|
+
/** Update the main light (Unity's directional Sun). Direction change marks shadow VP dirty. */
|
|
10646
11155
|
setSun(options: SunOptions): void {
|
|
10647
11156
|
if (options.color) this.sun.color = options.color
|
|
10648
11157
|
if (options.strength !== undefined) this.sun.strength = options.strength
|
|
@@ -10730,6 +11239,7 @@ export class Engine {
|
|
|
10730
11239
|
...options,
|
|
10731
11240
|
}
|
|
10732
11241
|
this.groundY = opts.y
|
|
11242
|
+
this.groundHalf = [opts.width / 2, opts.height / 2]
|
|
10733
11243
|
this.createGroundGeometry(opts.width, opts.height, opts.y)
|
|
10734
11244
|
this.createShadowGroundResources(opts)
|
|
10735
11245
|
this.hasGround = true
|
|
@@ -10782,6 +11292,7 @@ export class Engine {
|
|
|
10782
11292
|
* literal typed into a console. Vec3 satisfies it either way. */
|
|
10783
11293
|
lights: SceneLight[] | null,
|
|
10784
11294
|
): void {
|
|
11295
|
+
this.sceneLights = lights
|
|
10785
11296
|
// The directional ones go to the light uniform's slots beside the sun's —
|
|
10786
11297
|
// three of them, in order; the rest are records the grid indexes.
|
|
10787
11298
|
const directional = (lights ?? []).filter((l) => l.kind === "directional")
|
|
@@ -10841,8 +11352,12 @@ export class Engine {
|
|
|
10841
11352
|
this.lightsData[b + 12] = Math.max(inner, outer)
|
|
10842
11353
|
// The layers it does NOT reach, as bits — inverted so a zero reaches all.
|
|
10843
11354
|
this.lightsWords[b + 13] = ~(l.layers ?? ALL_LAYERS) >>> 0
|
|
10844
|
-
|
|
10845
|
-
this.lightsData[b +
|
|
11355
|
+
// The bulb's radius (rzLightNear): 0 keeps the engine's default
|
|
11356
|
+
this.lightsData[b + 14] = l.near !== undefined && Number.isFinite(l.near) && l.near > 0 ? l.near : 0
|
|
11357
|
+
// Its cookie's layer + 1, 0 for none or one not loaded yet (a load
|
|
11358
|
+
// rewrites the lamps when it lands).
|
|
11359
|
+
const cookie = l.cookie !== undefined ? this.cookieLayers.get(l.cookie) : undefined
|
|
11360
|
+
this.lightsData[b + 15] = cookie !== undefined ? cookie + 1 : 0
|
|
10846
11361
|
}
|
|
10847
11362
|
// The grid, from the records as STORED — normalised aim, clamped reach, the
|
|
10848
11363
|
// cosines in f32 — so it indexes exactly what the shader will evaluate.
|
|
@@ -10881,6 +11396,58 @@ export class Engine {
|
|
|
10881
11396
|
this.allocateLightSlots()
|
|
10882
11397
|
}
|
|
10883
11398
|
|
|
11399
|
+
/**
|
|
11400
|
+
* A cookie, under `key`, for every spot lamp that names it (SceneLight.cookie):
|
|
11401
|
+
* the picture a stage light throws through its gobo, projected along the
|
|
11402
|
+
* cone — its RGB is the light let through, so black blocks, white passes and
|
|
11403
|
+
* a colour tints. Square, stretched to 512². Up to 16 at once; loading a key
|
|
11404
|
+
* again replaces its picture. False when every layer is taken.
|
|
11405
|
+
*/
|
|
11406
|
+
async loadLightCookie(key: string, source: Blob | ImageBitmap): Promise<boolean> {
|
|
11407
|
+
if (!this.device) return false
|
|
11408
|
+
let layer = this.cookieLayers.get(key)
|
|
11409
|
+
if (layer === undefined) {
|
|
11410
|
+
const used = new Set(this.cookieLayers.values())
|
|
11411
|
+
for (let k = 0; k < Engine.COOKIE_LAYERS && layer === undefined; k++) if (!used.has(k)) layer = k
|
|
11412
|
+
if (layer === undefined) {
|
|
11413
|
+
console.warn(`[reze] light cookie "${key}": all ${Engine.COOKIE_LAYERS} cookie layers are in use`)
|
|
11414
|
+
return false
|
|
11415
|
+
}
|
|
11416
|
+
}
|
|
11417
|
+
const size = Engine.COOKIE_SIZE
|
|
11418
|
+
const bmp = await createImageBitmap(source, {
|
|
11419
|
+
resizeWidth: size,
|
|
11420
|
+
resizeHeight: size,
|
|
11421
|
+
resizeQuality: "high",
|
|
11422
|
+
colorSpaceConversion: "none",
|
|
11423
|
+
premultiplyAlpha: "none",
|
|
11424
|
+
})
|
|
11425
|
+
// The real array on the first cookie, and every group that binds the
|
|
11426
|
+
// stand-in rebuilt against it (the stand-in itself is a 1x1 and stays).
|
|
11427
|
+
if (this.cookieTexture.width === 1) {
|
|
11428
|
+
this.cookieTexture = this.device.createTexture({
|
|
11429
|
+
label: "light cookies",
|
|
11430
|
+
size: [size, size, Engine.COOKIE_LAYERS],
|
|
11431
|
+
format: "rgba8unorm",
|
|
11432
|
+
usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST | GPUTextureUsage.RENDER_ATTACHMENT,
|
|
11433
|
+
})
|
|
11434
|
+
this.cookieView = this.cookieTexture.createView({ dimension: "2d-array" })
|
|
11435
|
+
this.worldBindingsDirty = true
|
|
11436
|
+
this.buildGroundBindGroup()
|
|
11437
|
+
}
|
|
11438
|
+
this.device.queue.copyExternalImageToTexture({ source: bmp }, { texture: this.cookieTexture, origin: [0, 0, layer] }, [size, size, 1])
|
|
11439
|
+
bmp.close()
|
|
11440
|
+
this.cookieLayers.set(key, layer)
|
|
11441
|
+
if (this.sceneLights) this.setLights(this.sceneLights)
|
|
11442
|
+
return true
|
|
11443
|
+
}
|
|
11444
|
+
|
|
11445
|
+
/** Forget a cookie: lamps naming it light plainly again. */
|
|
11446
|
+
removeLightCookie(key: string): void {
|
|
11447
|
+
if (!this.cookieLayers.delete(key)) return
|
|
11448
|
+
if (this.sceneLights) this.setLights(this.sceneLights)
|
|
11449
|
+
}
|
|
11450
|
+
|
|
10884
11451
|
/** How many positional lights the scene is carrying. */
|
|
10885
11452
|
getLightCount(): number {
|
|
10886
11453
|
return this.lightHeader[0]
|
|
@@ -13171,8 +13738,12 @@ export class Engine {
|
|
|
13171
13738
|
const d = positions[i] * positions[i] + positions[i + 1] * positions[i + 1] + positions[i + 2] * positions[i + 2]
|
|
13172
13739
|
if (d > most) most = d
|
|
13173
13740
|
}
|
|
13174
|
-
|
|
13175
|
-
|
|
13741
|
+
this.raiseSceneExtent(Math.sqrt(most))
|
|
13742
|
+
}
|
|
13743
|
+
|
|
13744
|
+
/** Raise the scene's reach (never lower it) and hand it to the camera. */
|
|
13745
|
+
private raiseSceneExtent(extent: number): void {
|
|
13746
|
+
if (!(extent > this.sceneExtent)) return
|
|
13176
13747
|
this.sceneExtent = extent
|
|
13177
13748
|
this.camera?.setSceneExtent(extent)
|
|
13178
13749
|
}
|
|
@@ -13253,6 +13824,16 @@ export class Engine {
|
|
|
13253
13824
|
minZ = Math.min(minZ, data[o + 18] - r); maxZ = Math.max(maxZ, data[o + 18] + r)
|
|
13254
13825
|
}
|
|
13255
13826
|
}
|
|
13827
|
+
// The built-in floor, while it is drawn. It is no draw of the cull table,
|
|
13828
|
+
// so without this the box ended at the cast: the cascades stopped just
|
|
13829
|
+
// past her, and a low sun's long shadow ran off the last of them — the
|
|
13830
|
+
// floor beyond read as lit and her head and shoulders went missing.
|
|
13831
|
+
if (this.hasGround && !this.groundIsSuppressed()) {
|
|
13832
|
+
const [hx, hz] = this.groundHalf
|
|
13833
|
+
minX = Math.min(minX, -hx); maxX = Math.max(maxX, hx)
|
|
13834
|
+
minY = Math.min(minY, this.groundY); maxY = Math.max(maxY, this.groundY)
|
|
13835
|
+
minZ = Math.min(minZ, -hz); maxZ = Math.max(maxZ, hz)
|
|
13836
|
+
}
|
|
13256
13837
|
if (this.nativeStage) {
|
|
13257
13838
|
const nb = this.nativeStage.bounds
|
|
13258
13839
|
minX = Math.min(minX, nb.min[0]); maxX = Math.max(maxX, nb.max[0])
|
|
@@ -13557,6 +14138,10 @@ export class Engine {
|
|
|
13557
14138
|
*/
|
|
13558
14139
|
private recordBundles(): void {
|
|
13559
14140
|
this.bundlesDirty = false
|
|
14141
|
+
// What the bundles record is what the probe would see: a stage arriving or
|
|
14142
|
+
// leaving, a look installed on it, a pipeline landing. Re-captured once it
|
|
14143
|
+
// settles (and switched off when the stage is gone).
|
|
14144
|
+
this.captureReflectionProbe()
|
|
13560
14145
|
const scene = {
|
|
13561
14146
|
colorFormats: sceneColorFormats(this.sceneFormats),
|
|
13562
14147
|
depthStencilFormat: this.depthFormat,
|
|
@@ -13820,10 +14405,17 @@ export class Engine {
|
|
|
13820
14405
|
private issueDraw(
|
|
13821
14406
|
pass: GPURenderPassEncoder | GPURenderBundleEncoder,
|
|
13822
14407
|
draw: DrawCall,
|
|
13823
|
-
kind: "camera" | "shadow" | "mirror",
|
|
14408
|
+
kind: "camera" | "shadow" | "mirror" | "probe",
|
|
13824
14409
|
): void {
|
|
14410
|
+
// The probe's cube faces have no cull of their own: drawn whole.
|
|
13825
14411
|
const args =
|
|
13826
|
-
kind === "
|
|
14412
|
+
kind === "probe"
|
|
14413
|
+
? null
|
|
14414
|
+
: kind === "shadow"
|
|
14415
|
+
? this.cullShadowArgs
|
|
14416
|
+
: kind === "mirror"
|
|
14417
|
+
? this.cullMirrorArgs
|
|
14418
|
+
: this.cullCameraArgs
|
|
13827
14419
|
if (args && draw.cullIndex >= 0) {
|
|
13828
14420
|
pass.drawIndexedIndirect(args, draw.cullIndex * Engine.CULL_ARG_WORDS * 4)
|
|
13829
14421
|
} else {
|
|
@@ -14127,7 +14719,8 @@ export class Engine {
|
|
|
14127
14719
|
entries: [{ binding: 0, resource: { buffer: skinMatrixBuffer } }],
|
|
14128
14720
|
})
|
|
14129
14721
|
|
|
14130
|
-
const
|
|
14722
|
+
const attributeBuffer = this.createAttributeBuffer(name, model)
|
|
14723
|
+
const gpuBuffers: GPUBuffer[] = [vertexBuffer, indexBuffer, jointsBuffer, weightsBuffer, skinMatrixBuffer, attributeBuffer]
|
|
14131
14724
|
|
|
14132
14725
|
const gpuMorph = this.createGpuMorph(name, model, vertexBuffer, gpuBuffers)
|
|
14133
14726
|
|
|
@@ -14152,6 +14745,7 @@ export class Engine {
|
|
|
14152
14745
|
indexBuffer,
|
|
14153
14746
|
jointsBuffer,
|
|
14154
14747
|
weightsBuffer,
|
|
14748
|
+
attributeBuffer,
|
|
14155
14749
|
skinMatrixBuffer,
|
|
14156
14750
|
wireEdges: new Map(),
|
|
14157
14751
|
drawCalls: [],
|
|
@@ -14198,6 +14792,46 @@ export class Engine {
|
|
|
14198
14792
|
this.updateOrderDirty = true
|
|
14199
14793
|
}
|
|
14200
14794
|
|
|
14795
|
+
/**
|
|
14796
|
+
* A model's attribute stream: vertex colour (RGBA) and a second UV set, six
|
|
14797
|
+
* floats a vertex, read by the material pipelines at slot 4.
|
|
14798
|
+
*
|
|
14799
|
+
* PMX has no vertex colour and one UV, but it has four ADDITIONAL UV
|
|
14800
|
+
* channels of four floats each, and that is where a model that needs more
|
|
14801
|
+
* keeps it: the first additional UV is the vertex colour, the second's xy
|
|
14802
|
+
* the second UV — the convention a converted game stage is written in (a
|
|
14803
|
+
* plant's occlusion rides in its vertex alpha, a light card's mask in its
|
|
14804
|
+
* second UV). A model without them reads white and its own UV, which is
|
|
14805
|
+
* what every graph that reads neither already assumed.
|
|
14806
|
+
*/
|
|
14807
|
+
private createAttributeBuffer(name: string, model: Model): GPUBuffer {
|
|
14808
|
+
const vertices = model.getVertices()
|
|
14809
|
+
const count = Math.floor(vertices.length / 8)
|
|
14810
|
+
const data = new Float32Array(count * Engine.ATTRIBUTE_FLOATS)
|
|
14811
|
+
const color = model.getAdditionalUv(1)
|
|
14812
|
+
const uv2 = model.getAdditionalUv(2)
|
|
14813
|
+
for (let i = 0; i < count; i++) {
|
|
14814
|
+
const o = i * Engine.ATTRIBUTE_FLOATS
|
|
14815
|
+
if (color) {
|
|
14816
|
+
data[o] = color[i * 4]
|
|
14817
|
+
data[o + 1] = color[i * 4 + 1]
|
|
14818
|
+
data[o + 2] = color[i * 4 + 2]
|
|
14819
|
+
data[o + 3] = color[i * 4 + 3]
|
|
14820
|
+
} else {
|
|
14821
|
+
data[o] = data[o + 1] = data[o + 2] = data[o + 3] = 1
|
|
14822
|
+
}
|
|
14823
|
+
data[o + 4] = uv2 ? uv2[i * 4] : vertices[i * 8 + 6]
|
|
14824
|
+
data[o + 5] = uv2 ? uv2[i * 4 + 1] : vertices[i * 8 + 7]
|
|
14825
|
+
}
|
|
14826
|
+
const buffer = this.device.createBuffer({
|
|
14827
|
+
label: `${name}: attribute buffer`,
|
|
14828
|
+
size: Math.max(data.byteLength, 4 * Engine.ATTRIBUTE_FLOATS),
|
|
14829
|
+
usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
|
|
14830
|
+
})
|
|
14831
|
+
if (data.byteLength > 0) this.device.queue.writeBuffer(buffer, 0, data)
|
|
14832
|
+
return buffer
|
|
14833
|
+
}
|
|
14834
|
+
|
|
14201
14835
|
// Build the per-model GPU vertex-morph state. Returns null (and leaves the model on the
|
|
14202
14836
|
// CPU morph path) when the model has no vertex morphs. Created buffers are pushed into
|
|
14203
14837
|
// gpuBuffers so they're released with the instance.
|
|
@@ -14530,6 +15164,7 @@ export class Engine {
|
|
|
14530
15164
|
{ binding: 12, resource: this.groundNoiseView },
|
|
14531
15165
|
{ binding: 13, resource: this.mirrorMaskView! },
|
|
14532
15166
|
{ binding: 14, resource: { buffer: this.groundClipOffBuffer } },
|
|
15167
|
+
{ binding: 15, resource: this.cookieView },
|
|
14533
15168
|
],
|
|
14534
15169
|
})
|
|
14535
15170
|
this.groundMirrorViewBindGroup = this.device.createBindGroup({
|
|
@@ -14552,6 +15187,7 @@ export class Engine {
|
|
|
14552
15187
|
{ binding: 12, resource: this.groundNoiseView },
|
|
14553
15188
|
{ binding: 13, resource: this.mirrorDummyColorView! },
|
|
14554
15189
|
{ binding: 14, resource: { buffer: this.groundClipMirrorBuffer } },
|
|
15190
|
+
{ binding: 15, resource: this.cookieView },
|
|
14555
15191
|
],
|
|
14556
15192
|
})
|
|
14557
15193
|
if (this.groundDrawCall) this.groundDrawCall.bindGroup = this.groundShadowBindGroup
|
|
@@ -15854,38 +16490,6 @@ export class Engine {
|
|
|
15854
16490
|
}
|
|
15855
16491
|
|
|
15856
16492
|
// 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
16493
|
private buildMouseRay(px: number, py: number): { origin: Vec3; dir: Vec3 } | null {
|
|
15890
16494
|
if (!this.camera) return null
|
|
15891
16495
|
const width = this.canvas.clientWidth
|
|
@@ -16478,6 +17082,10 @@ export class Engine {
|
|
|
16478
17082
|
idAtt.storeOp = idsRead ? "store" : "discard"
|
|
16479
17083
|
}
|
|
16480
17084
|
|
|
17085
|
+
// The stage's reflection probe, when one is due: its own submits, ahead of
|
|
17086
|
+
// this frame's, so every material below reads the fresh cube.
|
|
17087
|
+
if (hasModels) this.maybeCaptureProbe()
|
|
17088
|
+
|
|
16481
17089
|
const encoder = this.device.createCommandEncoder()
|
|
16482
17090
|
|
|
16483
17091
|
// GPU vertex morphs: write morphed positions into vertex buffers before any pass reads
|
|
@@ -17481,7 +18089,7 @@ export class Engine {
|
|
|
17481
18089
|
const base = {
|
|
17482
18090
|
label: `style ${renderClass}${overEyes ? " (over eyes)" : ""}`,
|
|
17483
18091
|
layout: this.mainPipelineLayout,
|
|
17484
|
-
vertex: { module, buffers: this.
|
|
18092
|
+
vertex: { module, buffers: this.materialVertexBufferLayouts },
|
|
17485
18093
|
// The eye front-culls — the see-through-hair look is draw order plus that
|
|
17486
18094
|
// cull plus the stencil stamp, and it is the deployed appearance of every
|
|
17487
18095
|
// published scene. In the MIRROR the winding is flipped (determinant -1,
|
|
@@ -17538,7 +18146,16 @@ export class Engine {
|
|
|
17538
18146
|
}
|
|
17539
18147
|
const desc: GPURenderPipelineDescriptor = {
|
|
17540
18148
|
...base,
|
|
17541
|
-
fragment: {
|
|
18149
|
+
fragment: {
|
|
18150
|
+
module,
|
|
18151
|
+
constants,
|
|
18152
|
+
targets:
|
|
18153
|
+
blend === "additive"
|
|
18154
|
+
? this.sceneTargetsAdditive
|
|
18155
|
+
: blend === "premultiplied"
|
|
18156
|
+
? this.sceneTargetsPremultiplied
|
|
18157
|
+
: this.sceneTargets,
|
|
18158
|
+
},
|
|
17542
18159
|
depthStencil,
|
|
17543
18160
|
}
|
|
17544
18161
|
return this.cachedRenderPipeline(desc).then((pipeline) => {
|
|
@@ -17786,6 +18403,46 @@ export class Engine {
|
|
|
17786
18403
|
* makes outlines compose like MMD: every material drawn later in the author's
|
|
17787
18404
|
* order covers earlier hulls, and each hull sits over everything drawn before it.
|
|
17788
18405
|
*/
|
|
18406
|
+
/**
|
|
18407
|
+
* A stage's draws, far to near from the eye: its transparent queue, in the
|
|
18408
|
+
* order a game sorts one. Each draw's distance is its material box's centre,
|
|
18409
|
+
* placed, against the camera — a pane or a water surface is one material,
|
|
18410
|
+
* which is the unit a game sorts too. The array is kept per instance and
|
|
18411
|
+
* re-sorted in place, so an unchanged order costs a comparison per draw.
|
|
18412
|
+
*/
|
|
18413
|
+
private backToFront(inst: ModelInstance): DrawCall[] {
|
|
18414
|
+
let list = this.sortedDraws.get(inst)
|
|
18415
|
+
if (!list || list.length !== inst.drawCalls.length || list.some((d) => !inst.drawCalls.includes(d))) {
|
|
18416
|
+
list = [...inst.drawCalls]
|
|
18417
|
+
this.sortedDraws.set(inst, list)
|
|
18418
|
+
}
|
|
18419
|
+
const m = inst.model
|
|
18420
|
+
const ex = this.cameraMatrixData[32]
|
|
18421
|
+
const ey = this.cameraMatrixData[33]
|
|
18422
|
+
const ez = this.cameraMatrixData[34]
|
|
18423
|
+
const p = this.sortScratch
|
|
18424
|
+
const dist = this.sortDistance
|
|
18425
|
+
dist.clear()
|
|
18426
|
+
for (const d of list) {
|
|
18427
|
+
const b = d.bounds
|
|
18428
|
+
if (!b || b.length < 6 || !(b[0] <= b[3])) {
|
|
18429
|
+
dist.set(d, 0)
|
|
18430
|
+
continue
|
|
18431
|
+
}
|
|
18432
|
+
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)
|
|
18433
|
+
Quat.rotateVecInto(m.rotation, p, p)
|
|
18434
|
+
const dx = p.x + m.position.x - ex
|
|
18435
|
+
const dy = p.y + m.position.y - ey
|
|
18436
|
+
const dz = p.z + m.position.z - ez
|
|
18437
|
+
dist.set(d, dx * dx + dy * dy + dz * dz)
|
|
18438
|
+
}
|
|
18439
|
+
list.sort((a, b) => dist.get(b)! - dist.get(a)!)
|
|
18440
|
+
return list
|
|
18441
|
+
}
|
|
18442
|
+
private sortedDraws = new WeakMap<ModelInstance, DrawCall[]>()
|
|
18443
|
+
private sortScratch = new Vec3(0, 0, 0)
|
|
18444
|
+
private sortDistance = new Map<DrawCall, number>()
|
|
18445
|
+
|
|
17789
18446
|
/** Is this draw's compiled class "hair"? Ungrouped draws never are — the
|
|
17790
18447
|
* neutral pipeline is the auto class. */
|
|
17791
18448
|
private isHairDraw(inst: ModelInstance, dc: DrawCall): boolean {
|
|
@@ -17798,7 +18455,7 @@ export class Engine {
|
|
|
17798
18455
|
pass: GPURenderPassEncoder | GPURenderBundleEncoder,
|
|
17799
18456
|
inst: ModelInstance,
|
|
17800
18457
|
type: "opaque" | "transparent",
|
|
17801
|
-
view: { perFrame: GPUBindGroup; args: "camera" | "mirror"; outlines: boolean },
|
|
18458
|
+
view: { perFrame: GPUBindGroup; args: "camera" | "mirror" | "probe"; outlines: boolean },
|
|
17802
18459
|
// The opaque phase walks its author order twice — non-hair, then hair — so
|
|
17803
18460
|
// the hair depth prime can sit between the eye's stencil write and the hair
|
|
17804
18461
|
// colour that must respect it. See renderModelOpaquePhase.
|
|
@@ -17806,9 +18463,17 @@ export class Engine {
|
|
|
17806
18463
|
): void {
|
|
17807
18464
|
let currentPipeline: GPURenderPipeline | null = null
|
|
17808
18465
|
let bound = false
|
|
17809
|
-
|
|
18466
|
+
const outlined: DrawCall[] = []
|
|
18467
|
+
// A stage's glass and water in the camera pass go far to near, as a
|
|
18468
|
+
// game's transparent queue does: author order put a far pane over a near
|
|
18469
|
+
// one wherever the author listed it last. Characters keep MMD's author
|
|
18470
|
+
// order — their layers are fabric, built around it.
|
|
18471
|
+
const draws = type === "transparent" && inst.isStage && view.args === "camera" ? this.backToFront(inst) : inst.drawCalls
|
|
18472
|
+
for (const draw of draws) {
|
|
17810
18473
|
if (draw.type !== type) continue
|
|
17811
18474
|
if (only && (only === "hair") !== this.isHairDraw(inst, draw)) continue
|
|
18475
|
+
// The probe draws directly, past the cull: honour the switches it holds.
|
|
18476
|
+
if (view.args === "probe" && !this.shouldRenderDrawCall(inst, draw)) continue
|
|
17812
18477
|
if (!bound) {
|
|
17813
18478
|
pass.setBindGroup(0, view.perFrame)
|
|
17814
18479
|
pass.setBindGroup(1, inst.mainPerInstanceBindGroup)
|
|
@@ -17822,35 +18487,42 @@ export class Engine {
|
|
|
17822
18487
|
}
|
|
17823
18488
|
pass.setBindGroup(2, draw.bindGroup)
|
|
17824
18489
|
this.issueDraw(pass, draw, view.args)
|
|
17825
|
-
if (draw.outline && this.outlineEnabled && view.outlines)
|
|
17826
|
-
|
|
17827
|
-
|
|
17828
|
-
|
|
17829
|
-
|
|
17830
|
-
|
|
17831
|
-
|
|
17832
|
-
|
|
17833
|
-
|
|
17834
|
-
|
|
17835
|
-
|
|
18490
|
+
if (draw.outline && this.outlineEnabled && view.outlines) outlined.push(draw)
|
|
18491
|
+
}
|
|
18492
|
+
// THE HULLS AFTER THE SURFACES, as MMD draws a model's edges. A hull held
|
|
18493
|
+
// at the minimum width is faded, and it still writes depth: drawn between
|
|
18494
|
+
// two materials it hid the second wherever it stood in front of it — a
|
|
18495
|
+
// shoe's sole's hull cut the upper behind it, and the floor showed through
|
|
18496
|
+
// as pale lines along the seam. After every surface of this phase it only
|
|
18497
|
+
// ever blends over finished ones. Same index ranges; own pipeline and
|
|
18498
|
+
// groups 0/2 — group 1 (skinMats) is layout-identical and stays bound.
|
|
18499
|
+
if (outlined.length) {
|
|
18500
|
+
const mirrored = view.args === "mirror"
|
|
18501
|
+
pass.setPipeline(mirrored ? this.outlineMirrorPipeline : this.outlinePipeline)
|
|
18502
|
+
pass.setBindGroup(0, mirrored ? this.outlineMirrorPerFrameBindGroup : this.outlinePerFrameBindGroup)
|
|
18503
|
+
// Slot 3: the hull's smoothed normals + edge scale. Only the outline
|
|
18504
|
+
// pipeline reads it; the others leave the slot alone.
|
|
18505
|
+
if (inst.outlineVertexBuffer) pass.setVertexBuffer(3, inst.outlineVertexBuffer)
|
|
18506
|
+
for (const draw of outlined) {
|
|
18507
|
+
pass.setBindGroup(2, draw.outline!.bindGroup)
|
|
17836
18508
|
this.issueDraw(pass, draw, view.args)
|
|
17837
|
-
pass.setBindGroup(0, view.perFrame)
|
|
17838
|
-
currentPipeline = null
|
|
17839
18509
|
}
|
|
18510
|
+
pass.setBindGroup(0, view.perFrame)
|
|
17840
18511
|
}
|
|
17841
18512
|
}
|
|
17842
18513
|
|
|
17843
18514
|
/**
|
|
17844
18515
|
* Main-pass render sequence for one model instance — babylon-mmd parity:
|
|
17845
18516
|
* 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
|
-
*
|
|
18517
|
+
* in PMX author order with depth write ON (forceDepthWrite). Outlines close
|
|
18518
|
+
* each drawMaterials call: the edge-flagged materials' hulls after all of its
|
|
18519
|
+
* surfaces, as MMD draws a model's edges.
|
|
17849
18520
|
*/
|
|
17850
18521
|
private setModelDrawState(pass: GPURenderPassEncoder | GPURenderBundleEncoder, inst: ModelInstance): void {
|
|
17851
18522
|
pass.setVertexBuffer(0, inst.vertexBuffer)
|
|
17852
18523
|
pass.setVertexBuffer(1, inst.jointsBuffer)
|
|
17853
18524
|
pass.setVertexBuffer(2, inst.weightsBuffer)
|
|
18525
|
+
pass.setVertexBuffer(4, inst.attributeBuffer)
|
|
17854
18526
|
pass.setIndexBuffer(inst.indexBuffer, "uint32")
|
|
17855
18527
|
// The stencil reference used to be set here. It is pass state, not bundle
|
|
17856
18528
|
// state — GPURenderBundleEncoder has no setStencilReference at all — so it
|
|
@@ -18217,7 +18889,7 @@ export class Engine {
|
|
|
18217
18889
|
// still pattern welded to the picture. On the SCENE clock like everything
|
|
18218
18890
|
// else here, so an export reproduces the editor exactly rather than
|
|
18219
18891
|
// scattering differently at whatever rate the encoder ran.
|
|
18220
|
-
u[3] =
|
|
18892
|
+
u[3] = Math.floor((this.sceneClock * 24) % 1024)
|
|
18221
18893
|
u[26] = this.canvas.width
|
|
18222
18894
|
u[27] = this.canvas.height
|
|
18223
18895
|
// Camera world position (viewU[10]) — the other half of bgWorldPos. It
|
|
@@ -18451,7 +19123,10 @@ export class Engine {
|
|
|
18451
19123
|
}
|
|
18452
19124
|
for (let s = 0; s < anchors.length; s++) {
|
|
18453
19125
|
const a = CAST_SUBJECT_VEC4S * 4 + (s * MAX_EFFECT_SUBJECTS + n) * 12
|
|
18454
|
-
|
|
19126
|
+
// `along`: the point that far down the bone's own axis, still model space
|
|
19127
|
+
// here, so the placement below turns and scales it like the joint.
|
|
19128
|
+
const along = anchors[s].along
|
|
19129
|
+
const pos = along ? m.getBonePointAlong(anchors[s].bone, along) : m.getBoneWorldPosition(anchors[s].bone)
|
|
18455
19130
|
if (!pos) {
|
|
18456
19131
|
cd[a + 3] = 0
|
|
18457
19132
|
continue
|
|
@@ -18473,7 +19148,7 @@ export class Engine {
|
|
|
18473
19148
|
cd[a + 4] = vx
|
|
18474
19149
|
cd[a + 5] = vy
|
|
18475
19150
|
cd[a + 6] = vz
|
|
18476
|
-
if (anchors[s].trail) this.writeTrail(inst.name, s, n, pos, cd, a)
|
|
19151
|
+
if (anchors[s].trail) this.writeTrail(inst.name, s, n, pos, cd, a, anchors[s].step)
|
|
18477
19152
|
const fwd = m.getBoneWorldForward(anchors[s].bone)
|
|
18478
19153
|
if (fwd) {
|
|
18479
19154
|
Quat.rotateVecInto(m.rotation, fwd, fwd)
|
|
@@ -18515,7 +19190,7 @@ export class Engine {
|
|
|
18515
19190
|
this.castData.fill(0, CAST_SUBJECT_VEC4S * 4)
|
|
18516
19191
|
}
|
|
18517
19192
|
|
|
18518
|
-
private writeTrail(model: string, slot: number, n: number, pos: Vec3, cd: Float32Array, anchorBase: number): void {
|
|
19193
|
+
private writeTrail(model: string, slot: number, n: number, pos: Vec3, cd: Float32Array, anchorBase: number, step?: number): void {
|
|
18519
19194
|
const key = `${model}\u0000${slot}`
|
|
18520
19195
|
let ring = this.anchorTrail.get(key)
|
|
18521
19196
|
if (!ring) {
|
|
@@ -18536,9 +19211,44 @@ export class Engine {
|
|
|
18536
19211
|
if (Math.hypot(dx, dy, dz) / dt > 50) {
|
|
18537
19212
|
ring.pos.length = 0
|
|
18538
19213
|
ring.t.length = 0
|
|
19214
|
+
ring.live = false
|
|
18539
19215
|
}
|
|
18540
19216
|
}
|
|
18541
|
-
if (
|
|
19217
|
+
if (step && ring.pos.length > 0) {
|
|
19218
|
+
// `trail step d` — the path a game's trail keeps (Unity's
|
|
19219
|
+
// minVertexDistance). Point 0 is the LIVE head: it follows the bone every
|
|
19220
|
+
// frame. It is left behind as a kept point only on a tick, and only once
|
|
19221
|
+
// the bone is `step` from the last point kept, so no segment is shorter
|
|
19222
|
+
// than that and a slowing hand cannot leave a knot of jittery stubs for a
|
|
19223
|
+
// wide ribbon to fan across. Still at most one point per tick, so a stepped
|
|
19224
|
+
// ring never covers LESS time than a sampled one.
|
|
19225
|
+
const live = ring.live === true
|
|
19226
|
+
const k = live ? 3 : 0
|
|
19227
|
+
const far = Math.hypot(pos.x - ring.pos[k], pos.y - ring.pos[k + 1], pos.z - ring.pos[k + 2]) >= step
|
|
19228
|
+
if (live && !(far && this.trailDue > 0)) {
|
|
19229
|
+
ring.pos[0] = pos.x
|
|
19230
|
+
ring.pos[1] = pos.y
|
|
19231
|
+
ring.pos[2] = pos.z
|
|
19232
|
+
ring.t[0] = this.sceneClock
|
|
19233
|
+
} else {
|
|
19234
|
+
// keep the head where it is as a point (live) or start a new head
|
|
19235
|
+
if (live) {
|
|
19236
|
+
ring.pos[0] = pos.x
|
|
19237
|
+
ring.pos[1] = pos.y
|
|
19238
|
+
ring.pos[2] = pos.z
|
|
19239
|
+
ring.t[0] = this.sceneClock
|
|
19240
|
+
ring.live = false
|
|
19241
|
+
} else {
|
|
19242
|
+
ring.pos.unshift(pos.x, pos.y, pos.z)
|
|
19243
|
+
ring.t.unshift(this.sceneClock)
|
|
19244
|
+
ring.live = !(far && this.trailDue > 0)
|
|
19245
|
+
}
|
|
19246
|
+
if (ring.t.length > TRAIL_SAMPLES) {
|
|
19247
|
+
ring.t.length = TRAIL_SAMPLES
|
|
19248
|
+
ring.pos.length = TRAIL_SAMPLES * 3
|
|
19249
|
+
}
|
|
19250
|
+
}
|
|
19251
|
+
} else if (this.trailDue > 0 || ring.pos.length === 0) {
|
|
18542
19252
|
// ONE sample per frame, never one per due tick. A frame that spanned
|
|
18543
19253
|
// several 60Hz ticks only knows where the bone is NOW, and unshifting that
|
|
18544
19254
|
// position once per tick fabricated duplicate samples — same point, same
|
|
@@ -18622,6 +19332,9 @@ export class Engine {
|
|
|
18622
19332
|
headSkin: () => (head >= 0 ? inst.model.getSkinMatrices().subarray(head * 16, head * 16 + 16) : null),
|
|
18623
19333
|
headRest,
|
|
18624
19334
|
layers: new Uint32Array(inst.objectLight.buffer)[0],
|
|
19335
|
+
additionalUv: (channel) => inst.model.getAdditionalUv(channel),
|
|
19336
|
+
morphs: inst.model.getMorphing().morphs,
|
|
19337
|
+
morphWeights: () => inst.model.getEffectiveMorphWeights(),
|
|
18625
19338
|
},
|
|
18626
19339
|
look,
|
|
18627
19340
|
)
|
|
@@ -18636,6 +19349,20 @@ export class Engine {
|
|
|
18636
19349
|
return this.nativeLooks?.ready(name) ?? Promise.resolve()
|
|
18637
19350
|
}
|
|
18638
19351
|
|
|
19352
|
+
/**
|
|
19353
|
+
* Set some of the values of one material a model's native look dresses —
|
|
19354
|
+
* its Unity material properties by name, colours linear, as the look's
|
|
19355
|
+
* `values` hold them — over the look's own, until set again. What a game
|
|
19356
|
+
* animates on a material over a take (its timeline's material curves)
|
|
19357
|
+
* reaches the shaders this way, one frame at a time: the caller samples its
|
|
19358
|
+
* keys on its own clock and sets what it got. Only the names the material's
|
|
19359
|
+
* look carries are taken, and a value equal to the one in place writes
|
|
19360
|
+
* nothing. False when the model wears no look dressing that material.
|
|
19361
|
+
*/
|
|
19362
|
+
setModelNativeUniforms(name: string, material: string, values: Record<string, number | ArrayLike<number>>): boolean {
|
|
19363
|
+
return this.nativeLooks?.setUniforms(name, material, values) ?? false
|
|
19364
|
+
}
|
|
19365
|
+
|
|
18639
19366
|
/** The hosts for the game's shaders: the scene pass's and the shadow atlas's,
|
|
18640
19367
|
* with the stand-ins an empty slot reads. Built on first use. */
|
|
18641
19368
|
private ensureNativeHosts(): NativeHost {
|
|
@@ -18682,6 +19409,18 @@ export class Engine {
|
|
|
18682
19409
|
white,
|
|
18683
19410
|
black,
|
|
18684
19411
|
blackCube: solid("native: black cube", [0, 0, 0, 255], true),
|
|
19412
|
+
// an empty volume (a froxel buffer no pass filled): no in-scatter, no extinction
|
|
19413
|
+
emptyVolume: (() => {
|
|
19414
|
+
const t = d.createTexture({
|
|
19415
|
+
label: "native: empty volume",
|
|
19416
|
+
size: [1, 1, 1],
|
|
19417
|
+
dimension: "3d",
|
|
19418
|
+
format: "rgba8unorm",
|
|
19419
|
+
usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST,
|
|
19420
|
+
})
|
|
19421
|
+
d.queue.writeTexture({ texture: t }, new Uint8Array([0, 0, 0, 0]), { bytesPerRow: 4, rowsPerImage: 1 }, [1, 1, 1])
|
|
19422
|
+
return t.createView({ dimension: "3d" })
|
|
19423
|
+
})(),
|
|
18685
19424
|
depth: this.nativeNoShadowView,
|
|
18686
19425
|
comparison: this.shadowComparisonSampler,
|
|
18687
19426
|
}
|
|
@@ -18720,7 +19459,8 @@ export class Engine {
|
|
|
18720
19459
|
{ mipmaps: (t, levels) => this.generateMipmaps(t, levels), fallback: this.nativeFallback },
|
|
18721
19460
|
onProgress,
|
|
18722
19461
|
)
|
|
18723
|
-
|
|
19462
|
+
// The stage's sky is in it: the far plane reaches as far as the stage does.
|
|
19463
|
+
this.raiseSceneExtent(this.nativeStage.extent)
|
|
18724
19464
|
}
|
|
18725
19465
|
|
|
18726
19466
|
/** What went wrong in the game's shaders, and the names nothing supplied. */
|
|
@@ -18770,11 +19510,17 @@ export class Engine {
|
|
|
18770
19510
|
let sunDirection: [number, number, number] = [dir.x, dir.y, dir.z]
|
|
18771
19511
|
let sunColor: [number, number, number] = [sun.color.x * k, sun.color.y * k, sun.color.z * k]
|
|
18772
19512
|
let sunShadow = this.sunShadow
|
|
19513
|
+
let sunUnity: NativeStageLights["main"]["unity"] | null = null
|
|
18773
19514
|
if (stage) {
|
|
18774
19515
|
// 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)
|
|
19516
|
+
// host sets from the stage (and a person may then move) - along the
|
|
19517
|
+
// scene sun's direction, in the stage's own main-light colour. The scene
|
|
19518
|
+
// sun's colour is the cast's key, which the game keeps apart (a
|
|
19519
|
+
// character's light is _ProbeLightingBase, not the stage's main light).
|
|
18776
19520
|
const L = stage.pkg.lights
|
|
18777
19521
|
const sc = stage.pkg.scale
|
|
19522
|
+
sunColor = [L.main.color[0], L.main.color[1], L.main.color[2]]
|
|
19523
|
+
sunUnity = L.main.unity ?? null
|
|
18778
19524
|
lights.length = 0
|
|
18779
19525
|
for (const l of L.additional) {
|
|
18780
19526
|
lights.push({
|
|
@@ -18804,6 +19550,7 @@ export class Engine {
|
|
|
18804
19550
|
dt: 1 / 60,
|
|
18805
19551
|
sunDirection,
|
|
18806
19552
|
sunColor,
|
|
19553
|
+
sunUnity,
|
|
18807
19554
|
sunShadow,
|
|
18808
19555
|
lights,
|
|
18809
19556
|
ambientSH: sh ? Array.from(sh, (v) => v * s) : null,
|
|
@@ -18917,17 +19664,3 @@ function stableGlobals(prev: Record<string, NativeValue>, next: Record<string, N
|
|
|
18917
19664
|
}
|
|
18918
19665
|
return next
|
|
18919
19666
|
}
|
|
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
|
-
}
|