reze-engine 0.62.1 → 0.64.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 +95 -57
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +466 -162
- package/dist/graph/compile.d.ts.map +1 -1
- package/dist/graph/compile.js +4 -1
- package/dist/graph/registry.d.ts +3 -0
- package/dist/graph/registry.d.ts.map +1 -1
- package/dist/graph/registry.js +133 -0
- package/dist/graph/schema.d.ts +12 -0
- package/dist/graph/schema.d.ts.map +1 -1
- package/dist/graph/slots.d.ts.map +1 -1
- package/dist/graph/slots.js +11 -20
- package/dist/index.d.ts +9 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/shaders/lights.d.ts +3 -2
- package/dist/shaders/lights.d.ts.map +1 -1
- package/dist/shaders/lights.js +16 -3
- package/dist/shaders/materials/common.d.ts.map +1 -1
- package/dist/shaders/materials/common.js +63 -56
- 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 +210 -0
- package/dist/shaders/passes/composite.d.ts.map +1 -1
- package/dist/shaders/passes/composite.js +13 -1
- package/dist/shaders/passes/cull.d.ts +1 -1
- package/dist/shaders/passes/cull.d.ts.map +1 -1
- package/dist/shaders/passes/cull.js +7 -3
- package/dist/shaders/passes/ground.d.ts.map +1 -1
- package/dist/shaders/passes/ground.js +32 -55
- package/dist/shaders/passes/particles.d.ts +11 -1
- package/dist/shaders/passes/particles.d.ts.map +1 -1
- package/dist/shaders/passes/particles.js +9 -4
- package/dist/shaders/scene-light-api.d.ts +18 -10
- package/dist/shaders/scene-light-api.d.ts.map +1 -1
- package/dist/shaders/scene-light-api.js +58 -46
- package/dist/shadow-cascades.d.ts +47 -20
- package/dist/shadow-cascades.d.ts.map +1 -1
- package/dist/shadow-cascades.js +36 -13
- package/dist/unity/character.d.ts +59 -0
- package/dist/unity/character.d.ts.map +1 -0
- package/dist/unity/character.js +120 -0
- package/dist/unity/globals.d.ts +93 -0
- package/dist/unity/globals.d.ts.map +1 -0
- package/dist/unity/globals.js +340 -0
- package/dist/unity/host.d.ts +137 -0
- package/dist/unity/host.d.ts.map +1 -0
- package/dist/unity/host.js +489 -0
- package/dist/unity/looks.d.ts +88 -0
- package/dist/unity/looks.d.ts.map +1 -0
- package/dist/unity/looks.js +295 -0
- package/dist/unity/skin.d.ts +9 -0
- package/dist/unity/skin.d.ts.map +1 -0
- package/dist/unity/skin.js +117 -0
- package/dist/unity/stage.d.ts +148 -0
- package/dist/unity/stage.d.ts.map +1 -0
- package/dist/unity/stage.js +375 -0
- package/dist/unity/state.d.ts +22 -0
- package/dist/unity/state.d.ts.map +1 -0
- package/dist/unity/state.js +170 -0
- package/package.json +1 -1
- package/src/engine.ts +504 -176
- package/src/graph/compile.ts +3 -1
- package/src/graph/registry.ts +142 -0
- package/src/graph/schema.ts +9 -0
- package/src/graph/slots.ts +11 -20
- package/src/index.ts +11 -0
- package/src/shaders/lights.ts +16 -3
- package/src/shaders/materials/common.ts +63 -57
- package/src/shaders/materials/nodes.ts +210 -0
- package/src/shaders/passes/composite.ts +13 -1
- package/src/shaders/passes/cull.ts +7 -3
- package/src/shaders/passes/ground.ts +32 -55
- package/src/shaders/passes/particles.ts +19 -5
- package/src/shaders/scene-light-api.ts +59 -46
- package/src/shadow-cascades.ts +61 -32
- package/src/unity/character.ts +169 -0
- package/src/unity/globals.ts +443 -0
- package/src/unity/host.ts +587 -0
- package/src/unity/looks.ts +372 -0
- package/src/unity/skin.ts +128 -0
- package/src/unity/stage.ts +568 -0
- package/src/unity/state.ts +172 -0
package/src/engine.ts
CHANGED
|
@@ -44,7 +44,13 @@ import {
|
|
|
44
44
|
type SimClock,
|
|
45
45
|
} from "./effect-schedule"
|
|
46
46
|
import { parentKeySpan, type ModelParentKey } from "./parent-keys"
|
|
47
|
-
import {
|
|
47
|
+
import { NativeHost } from "./unity/host"
|
|
48
|
+
import { NativeLooks, type NativeLook } from "./unity/looks"
|
|
49
|
+
import { NativeStage, type NativeStagePackage, type NativeStageReader } from "./unity/stage"
|
|
50
|
+
import { gameDir, unityFrameGlobals } from "./unity/globals"
|
|
51
|
+
import { outlineMaxOffsetMultiplier, towardMatrix } from "./unity/character"
|
|
52
|
+
import type { NativeValue } from "./unity/host"
|
|
53
|
+
import { SHADOW_ATLAS_SIZE, SHADOW_CASCADES, buildShadowCascades, cascadeSpheres, type ShadowBounds, type ShadowView } from "./shadow-cascades"
|
|
48
54
|
import { REFLECTION_DEBUG_WGSL, buildMirrorCamera, planeFromPointNormal } from "./reflection"
|
|
49
55
|
import { MIRROR_MASK_DOWNSAMPLE_WGSL, MIRROR_MAT_BYTES, mirrorShaderWgsl, mirrorShadowWgsl } from "./shaders/passes/mirror"
|
|
50
56
|
import { packHalf, type HdrImage } from "./hdr"
|
|
@@ -464,6 +470,9 @@ type ParentTrack = { keys: HeldParentKey[]; applied: number; seat: [number, numb
|
|
|
464
470
|
type SunOptions = {
|
|
465
471
|
/** Linear color of the sun lamp (Blender: Light > Color). */
|
|
466
472
|
color?: Vec3
|
|
473
|
+
/** The rendering layers it keys, as bits; every layer by default. A
|
|
474
|
+
* directional light from setLights takes the layers the sun leaves. */
|
|
475
|
+
layers?: number
|
|
467
476
|
/** Lamp power in Blender units (Blender: Light > Strength). */
|
|
468
477
|
strength?: number
|
|
469
478
|
/** Direction sunlight travels (points FROM sun TO scene, Blender: -light.rotation.Z). */
|
|
@@ -619,8 +628,11 @@ export type ViewTransformOptions = {
|
|
|
619
628
|
* reference Wuthering Waves projects render this way.
|
|
620
629
|
*
|
|
621
630
|
* "filmic" is Blender 3.6's Filmic, Medium High Contrast, baked as a LUT.
|
|
631
|
+
*
|
|
632
|
+
* "aether-gazer" is that game's own final curve, (1 - e^(-2.5x))^1.4 — what
|
|
633
|
+
* its scenes are graded through (composite.ts, agTransform).
|
|
622
634
|
*/
|
|
623
|
-
transform: "agx" | "filmic" | "standard"
|
|
635
|
+
transform: "agx" | "filmic" | "standard" | "aether-gazer"
|
|
624
636
|
}
|
|
625
637
|
|
|
626
638
|
// Matches the reference Blender project: Filmic view, Medium High Contrast look,
|
|
@@ -923,11 +935,10 @@ interface ModelInstance {
|
|
|
923
935
|
shadowDrawCalls: DrawCall[]
|
|
924
936
|
shadowBindGroups: GPUBindGroup[]
|
|
925
937
|
mainPerInstanceBindGroup: GPUBindGroup
|
|
926
|
-
/** Its
|
|
927
|
-
*
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
modelLight: Float32Array | null
|
|
938
|
+
/** Its ObjectLight — rendering layers, its own ambient — and the CPU copy it
|
|
939
|
+
* is written from. Every model has one: its layers decide its lights. */
|
|
940
|
+
lightBuffer: GPUBuffer
|
|
941
|
+
objectLight: Float32Array<ArrayBuffer>
|
|
931
942
|
pickPerInstanceBindGroup: GPUBindGroup
|
|
932
943
|
pickDrawCalls: PickDrawCall[]
|
|
933
944
|
/** Environment geometry added via addStage — no physics, no IK, and it
|
|
@@ -1827,8 +1838,33 @@ export type SceneLight = {
|
|
|
1827
1838
|
aim?: XYZ
|
|
1828
1839
|
angle?: number
|
|
1829
1840
|
innerAngle?: number
|
|
1841
|
+
/**
|
|
1842
|
+
* "directional": light along `aim` from infinitely far, as the sun is —
|
|
1843
|
+
* position and radius unused; up to three, beside the sun. Otherwise a spot
|
|
1844
|
+
* where an aim is given and a point where none is.
|
|
1845
|
+
*/
|
|
1846
|
+
kind?: "point" | "spot" | "directional"
|
|
1847
|
+
/**
|
|
1848
|
+
* The rendering layers it reaches, as bits (RENDERING_LAYER_*). Omitted
|
|
1849
|
+
* reaches every layer. A stage's daylight on RENDERING_LAYER_DEFAULT and the
|
|
1850
|
+
* cast's key on RENDERING_LAYER_CHARACTER is how a game lights the two apart.
|
|
1851
|
+
*/
|
|
1852
|
+
layers?: number
|
|
1830
1853
|
}
|
|
1831
1854
|
|
|
1855
|
+
/**
|
|
1856
|
+
* Rendering layers, as the game's pipeline (URP's rendering layers) uses them:
|
|
1857
|
+
* every drawing carries layer bits and a light reaches it only where its mask
|
|
1858
|
+
* shares one. A stage, prop or plane draws on DEFAULT; the cast on DEFAULT and
|
|
1859
|
+
* CHARACTER, the bits Aether Gazer's characters carry (0x40000001).
|
|
1860
|
+
*/
|
|
1861
|
+
export const RENDERING_LAYER_DEFAULT = 1
|
|
1862
|
+
export const RENDERING_LAYER_CHARACTER = 1 << 30
|
|
1863
|
+
/** Every layer — what a light without a mask reaches. */
|
|
1864
|
+
const ALL_LAYERS = 0xffffffff
|
|
1865
|
+
/** Where the directional slots' layer bits sit in the light uniform, in words. */
|
|
1866
|
+
const DIR_LAYERS_AT = 108
|
|
1867
|
+
|
|
1832
1868
|
export class Engine {
|
|
1833
1869
|
private static instance: Engine | null = null
|
|
1834
1870
|
|
|
@@ -1857,7 +1893,11 @@ export class Engine {
|
|
|
1857
1893
|
// padded to 80. sh[0].w is the IBL flag: 0 = flat world colour, 1 = the sky.
|
|
1858
1894
|
// …then the scene fog at [72..87] — see setSceneFog — and the cast's
|
|
1859
1895
|
// shadow on a stage at [88..107] (colour, amount; its view-projection).
|
|
1896
|
+
// …and the directional slots' rendering layers at [108..111], as u32 bits
|
|
1897
|
+
// through lightDataWords — never through a float.
|
|
1860
1898
|
private lightData = new Float32Array(112)
|
|
1899
|
+
private lightDataWords = new Uint32Array(this.lightData.buffer)
|
|
1900
|
+
private sunLayers = ALL_LAYERS
|
|
1861
1901
|
private castShadow: StageCastShadow | null = null
|
|
1862
1902
|
private castShadowTexture!: GPUTexture
|
|
1863
1903
|
private castShadowView!: GPUTextureView
|
|
@@ -2036,7 +2076,6 @@ export class Engine {
|
|
|
2036
2076
|
private mainPerFrameBindGroupLayout!: GPUBindGroupLayout
|
|
2037
2077
|
private mainPerInstanceBindGroupLayout!: GPUBindGroupLayout
|
|
2038
2078
|
/** What a model with no fill of its own binds: zero light. */
|
|
2039
|
-
private noFillBuffer!: GPUBuffer
|
|
2040
2079
|
private mainPerMaterialBindGroupLayout!: GPUBindGroupLayout
|
|
2041
2080
|
private outlinePerFrameBindGroupLayout!: GPUBindGroupLayout
|
|
2042
2081
|
private outlinePerMaterialBindGroupLayout!: GPUBindGroupLayout
|
|
@@ -2642,6 +2681,20 @@ export class Engine {
|
|
|
2642
2681
|
/** Scene seconds, advanced by the frame delta — NOT wall time, so an offline
|
|
2643
2682
|
* export samples the same path the editor showed. */
|
|
2644
2683
|
private sceneClock = 0
|
|
2684
|
+
/** Models dressed in the game's own materials (unity/looks.ts); built on the
|
|
2685
|
+
* first look installed. */
|
|
2686
|
+
private nativeLooks: NativeLooks | null = null
|
|
2687
|
+
/** The scene pass's host for the game's shaders, and the shadow atlas's. */
|
|
2688
|
+
private nativeHost: NativeHost | null = null
|
|
2689
|
+
private nativeShadowHost: NativeHost | null = null
|
|
2690
|
+
/** A game stage drawn by its own shaders (unity/stage.ts). */
|
|
2691
|
+
private nativeStage: NativeStage | null = null
|
|
2692
|
+
/** The frame's Unity globals, computed once for every native draw. */
|
|
2693
|
+
private nativeGlobals: Record<string, NativeValue> = {}
|
|
2694
|
+
private nativeFallback: Record<string, GPUTextureView> = {}
|
|
2695
|
+
/** A 1x1 depth map at the far plane: a game shader's shadow lookup into a
|
|
2696
|
+
* map the scene does not have reads lit. */
|
|
2697
|
+
private nativeNoShadowView: GPUTextureView | null = null
|
|
2645
2698
|
private trailAccum = 0
|
|
2646
2699
|
/** Trail samples owed this frame, computed once so every trail on every
|
|
2647
2700
|
* character samples in lockstep and their paths stay comparable. */
|
|
@@ -2719,8 +2772,9 @@ export class Engine {
|
|
|
2719
2772
|
/** The user's own "no ground" switch — see setGroundVisible. Distinct from
|
|
2720
2773
|
* hasGround, which records whether a ground was ever built. */
|
|
2721
2774
|
private groundHidden = false
|
|
2722
|
-
|
|
2723
|
-
private
|
|
2775
|
+
/** The sun's shadow atlas: every cascade in its own tile (shadow-cascades.ts). */
|
|
2776
|
+
private shadowAtlasTexture!: GPUTexture
|
|
2777
|
+
private shadowAtlasView!: GPUTextureView
|
|
2724
2778
|
private brdfLutTexture!: GPUTexture
|
|
2725
2779
|
private brdfLutView!: GPUTextureView
|
|
2726
2780
|
private filmicLutTexture!: GPUTexture
|
|
@@ -3098,7 +3152,7 @@ export class Engine {
|
|
|
3098
3152
|
// rebuilt per effect, so the compiled variant IS the flag.
|
|
3099
3153
|
u[11] = this.backdropEquirectView ? 2 : showingWorld ? 3 : bg ? 1 : 0
|
|
3100
3154
|
// Which display transform forms the frame (see viewTransform in composite.ts).
|
|
3101
|
-
u[25] = v.transform === "agx" ? 2 : v.transform === "standard" ? 1 : 0
|
|
3155
|
+
u[25] = v.transform === "aether-gazer" ? 3 : v.transform === "agx" ? 2 : v.transform === "standard" ? 1 : 0
|
|
3102
3156
|
u[26] = this.canvas.width
|
|
3103
3157
|
u[27] = this.canvas.height
|
|
3104
3158
|
// ── Grade (viewU[7..9]) ── The UI's three tonal COLORS map to ASC CDL here,
|
|
@@ -5508,6 +5562,7 @@ export class Engine {
|
|
|
5508
5562
|
paramsDecl: params.wgsl(EFFECT_PARAMS_BINDING),
|
|
5509
5563
|
gridSize: grid?.size ?? 0,
|
|
5510
5564
|
cover: particleEntryPoints(wgsl).cover,
|
|
5565
|
+
orient: particleEntryPoints(wgsl).orient,
|
|
5511
5566
|
live: particleEntryPoints(wgsl).count,
|
|
5512
5567
|
textures: d.textures,
|
|
5513
5568
|
}
|
|
@@ -5631,8 +5686,7 @@ export class Engine {
|
|
|
5631
5686
|
{ binding: PARTICLE_LIGHT_BINDING, visibility: GPUShaderStage.FRAGMENT, buffer: { type: "uniform" as const } },
|
|
5632
5687
|
{ binding: PARTICLE_LIGHT_BINDING + 1, visibility: GPUShaderStage.FRAGMENT, buffer: { type: "uniform" as const } },
|
|
5633
5688
|
{ binding: PARTICLE_LIGHT_BINDING + 2, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: "depth" as const } },
|
|
5634
|
-
{ binding: PARTICLE_LIGHT_BINDING + 3, visibility: GPUShaderStage.FRAGMENT,
|
|
5635
|
-
{ binding: PARTICLE_LIGHT_BINDING + 4, visibility: GPUShaderStage.FRAGMENT, sampler: { type: "comparison" as const } },
|
|
5689
|
+
{ binding: PARTICLE_LIGHT_BINDING + 3, visibility: GPUShaderStage.FRAGMENT, sampler: { type: "comparison" as const } },
|
|
5636
5690
|
]
|
|
5637
5691
|
: []),
|
|
5638
5692
|
// The live count's arguments, for the COUNT kernel's layout alone. The
|
|
@@ -5681,9 +5735,8 @@ export class Engine {
|
|
|
5681
5735
|
? [
|
|
5682
5736
|
{ binding: PARTICLE_LIGHT_BINDING, resource: { buffer: this.lightUniformBuffer } },
|
|
5683
5737
|
{ binding: PARTICLE_LIGHT_BINDING + 1, resource: { buffer: this.shadowLightVPBuffer } },
|
|
5684
|
-
{ binding: PARTICLE_LIGHT_BINDING + 2, resource: this.
|
|
5685
|
-
{ binding: PARTICLE_LIGHT_BINDING + 3, resource: this.
|
|
5686
|
-
{ binding: PARTICLE_LIGHT_BINDING + 4, resource: this.shadowComparisonSampler },
|
|
5738
|
+
{ binding: PARTICLE_LIGHT_BINDING + 2, resource: this.shadowAtlasView },
|
|
5739
|
+
{ binding: PARTICLE_LIGHT_BINDING + 3, resource: this.shadowComparisonSampler },
|
|
5687
5740
|
]
|
|
5688
5741
|
: []),
|
|
5689
5742
|
...(withIndirect && indirect ? [{ binding: PARTICLE_INDIRECT_BINDING, resource: { buffer: indirect } }] : []),
|
|
@@ -7711,12 +7764,6 @@ export class Engine {
|
|
|
7711
7764
|
[1, 1],
|
|
7712
7765
|
)
|
|
7713
7766
|
|
|
7714
|
-
this.noFillBuffer = this.device.createBuffer({
|
|
7715
|
-
label: "model light (none)",
|
|
7716
|
-
size: 48,
|
|
7717
|
-
usage: GPUBufferUsage.UNIFORM,
|
|
7718
|
-
})
|
|
7719
|
-
|
|
7720
7767
|
// Generic shared-toon ramp: lit white down to a soft cool shadow tone with
|
|
7721
7768
|
// a tight terminator around the midpoint, approximating MMD's toon ramps.
|
|
7722
7769
|
const TOON_H = 64
|
|
@@ -7812,10 +7859,10 @@ export class Engine {
|
|
|
7812
7859
|
// The positional lights. Always bound, empty or not, so every material
|
|
7813
7860
|
// pipeline shares one layout whether or not the scene has any.
|
|
7814
7861
|
{ binding: 6, visibility: GPUShaderStage.FRAGMENT, buffer: { type: "read-only-storage" } },
|
|
7815
|
-
// The
|
|
7816
|
-
//
|
|
7817
|
-
//
|
|
7818
|
-
|
|
7862
|
+
// The world's sky (8) and the BRDF LUT (9). The sky is always bound —
|
|
7863
|
+
// the 1x1 fallback when the scene has none — so every material
|
|
7864
|
+
// pipeline keeps sharing one layout. (7 was the far cascade's map,
|
|
7865
|
+
// which the atlas at 3 replaces.)
|
|
7819
7866
|
{ binding: 8, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: "float" } },
|
|
7820
7867
|
{ binding: 9, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: "float" } },
|
|
7821
7868
|
// The cast's shadow on a stage — setStageCastShadow.
|
|
@@ -8030,17 +8077,15 @@ export class Engine {
|
|
|
8030
8077
|
magFilter: "linear",
|
|
8031
8078
|
minFilter: "linear",
|
|
8032
8079
|
})
|
|
8033
|
-
// One
|
|
8034
|
-
//
|
|
8035
|
-
this.
|
|
8036
|
-
|
|
8037
|
-
|
|
8038
|
-
|
|
8039
|
-
|
|
8040
|
-
|
|
8041
|
-
|
|
8042
|
-
)
|
|
8043
|
-
this.shadowMapDepthViews = this.shadowMapTextures.map((t) => t.createView())
|
|
8080
|
+
// One atlas, a tile per cascade — the layout the game's pipeline uses, and
|
|
8081
|
+
// one binding for every reader.
|
|
8082
|
+
this.shadowAtlasTexture = this.device.createTexture({
|
|
8083
|
+
label: "sun shadow atlas",
|
|
8084
|
+
size: [SHADOW_ATLAS_SIZE, SHADOW_ATLAS_SIZE],
|
|
8085
|
+
format: Engine.SHADOW_DEPTH_FORMAT,
|
|
8086
|
+
usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.TEXTURE_BINDING,
|
|
8087
|
+
})
|
|
8088
|
+
this.shadowAtlasView = this.shadowAtlasTexture.createView()
|
|
8044
8089
|
// The cast's shadow on a stage (setStageCastShadow): one map, only the cast
|
|
8045
8090
|
// in it, from the stage's own direction. Always allocated so every material
|
|
8046
8091
|
// bind group and every instance's shadow bind group stays one shape; drawn
|
|
@@ -8140,7 +8185,6 @@ export class Engine {
|
|
|
8140
8185
|
// Same lights the materials read. A lamp that lit the cast and not the
|
|
8141
8186
|
// floor under her would read as a sticker.
|
|
8142
8187
|
{ binding: 6, visibility: GPUShaderStage.FRAGMENT, buffer: { type: "read-only-storage" } },
|
|
8143
|
-
{ binding: 7, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: "depth" } },
|
|
8144
8188
|
// The floor mirror: the mirror camera's view-projection, the reflection
|
|
8145
8189
|
// resolve, an ordinary sampler beside the comparison one, and the
|
|
8146
8190
|
// mirror pass's own depth for the depth-proportional blur.
|
|
@@ -10346,7 +10390,7 @@ export class Engine {
|
|
|
10346
10390
|
if (
|
|
10347
10391
|
!this.device ||
|
|
10348
10392
|
!this.mainPerFrameBindGroupLayout ||
|
|
10349
|
-
!this.
|
|
10393
|
+
!this.shadowAtlasView ||
|
|
10350
10394
|
!this.cameraUniformBuffer ||
|
|
10351
10395
|
!this.mirrorCameraBuffer ||
|
|
10352
10396
|
!this.lightUniformBuffer ||
|
|
@@ -10372,11 +10416,10 @@ export class Engine {
|
|
|
10372
10416
|
{ binding: 0, resource: { buffer: this.cameraUniformBuffer } },
|
|
10373
10417
|
{ binding: 1, resource: { buffer: this.lightUniformBuffer } },
|
|
10374
10418
|
{ binding: 2, resource: this.materialSampler },
|
|
10375
|
-
{ binding: 3, resource: this.
|
|
10419
|
+
{ binding: 3, resource: this.shadowAtlasView },
|
|
10376
10420
|
{ binding: 4, resource: this.shadowComparisonSampler },
|
|
10377
10421
|
{ binding: 5, resource: { buffer: this.shadowLightVPBuffer } },
|
|
10378
10422
|
{ binding: 6, resource: { buffer: this.lightsBuffer } },
|
|
10379
|
-
{ binding: 7, resource: this.shadowMapDepthViews[SHADOW_CASCADES.length - 1] },
|
|
10380
10423
|
{ binding: 8, resource: env },
|
|
10381
10424
|
{ binding: 9, resource: this.brdfLutView },
|
|
10382
10425
|
{ binding: 10, resource: this.castShadowView },
|
|
@@ -10389,11 +10432,10 @@ export class Engine {
|
|
|
10389
10432
|
{ binding: 0, resource: { buffer: this.mirrorCameraBuffer } },
|
|
10390
10433
|
{ binding: 1, resource: { buffer: this.lightUniformBuffer } },
|
|
10391
10434
|
{ binding: 2, resource: this.materialSampler },
|
|
10392
|
-
{ binding: 3, resource: this.
|
|
10435
|
+
{ binding: 3, resource: this.shadowAtlasView },
|
|
10393
10436
|
{ binding: 4, resource: this.shadowComparisonSampler },
|
|
10394
10437
|
{ binding: 5, resource: { buffer: this.shadowLightVPBuffer } },
|
|
10395
10438
|
{ binding: 6, resource: { buffer: this.lightsBuffer } },
|
|
10396
|
-
{ binding: 7, resource: this.shadowMapDepthViews[SHADOW_CASCADES.length - 1] },
|
|
10397
10439
|
{ binding: 8, resource: env },
|
|
10398
10440
|
{ binding: 9, resource: this.brdfLutView },
|
|
10399
10441
|
{ binding: 10, resource: this.castShadowView },
|
|
@@ -10459,6 +10501,7 @@ export class Engine {
|
|
|
10459
10501
|
this.lightData[base + 5] = this.sun.color.y
|
|
10460
10502
|
this.lightData[base + 6] = this.sun.color.z
|
|
10461
10503
|
this.lightData[base + 7] = this.sun.strength
|
|
10504
|
+
this.lightDataWords[DIR_LAYERS_AT + index] = this.sunLayers
|
|
10462
10505
|
if (index >= this.lightCount) this.lightCount = index + 1
|
|
10463
10506
|
this.updateLightBuffer()
|
|
10464
10507
|
}
|
|
@@ -10593,6 +10636,7 @@ export class Engine {
|
|
|
10593
10636
|
if (options.color) this.sun.color = options.color
|
|
10594
10637
|
if (options.strength !== undefined) this.sun.strength = options.strength
|
|
10595
10638
|
if (options.shadow !== undefined) this.sunShadow = Math.min(Math.max(options.shadow, 0), 1)
|
|
10639
|
+
if (options.layers !== undefined) this.sunLayers = options.layers >>> 0
|
|
10596
10640
|
if (options.direction) {
|
|
10597
10641
|
this.sun.direction = options.direction
|
|
10598
10642
|
this.shadowLightVPDirty = true
|
|
@@ -10727,7 +10771,23 @@ export class Engine {
|
|
|
10727
10771
|
* literal typed into a console. Vec3 satisfies it either way. */
|
|
10728
10772
|
lights: SceneLight[] | null,
|
|
10729
10773
|
): void {
|
|
10730
|
-
|
|
10774
|
+
// The directional ones go to the light uniform's slots beside the sun's —
|
|
10775
|
+
// three of them, in order; the rest are records the grid indexes.
|
|
10776
|
+
const directional = (lights ?? []).filter((l) => l.kind === "directional")
|
|
10777
|
+
for (let k = 1; k < 4; k++) {
|
|
10778
|
+
const l = directional[k - 1]
|
|
10779
|
+
const base = 4 + k * 8
|
|
10780
|
+
const a = l?.aim
|
|
10781
|
+
const len = a ? Math.hypot(a.x, a.y, a.z) : 0
|
|
10782
|
+
const i = l ? Math.max(l.intensity ?? 1, 0) : 0
|
|
10783
|
+
this.lightData.set(
|
|
10784
|
+
l && len > 0 ? [a!.x / len, a!.y / len, a!.z / len, 0, l.color.x * i, l.color.y * i, l.color.z * i, 1] : [0, 0, -1, 0, 0, 0, 0, 0],
|
|
10785
|
+
base,
|
|
10786
|
+
)
|
|
10787
|
+
this.lightDataWords[DIR_LAYERS_AT + k] = l && len > 0 ? (l.layers ?? ALL_LAYERS) >>> 0 : 0
|
|
10788
|
+
}
|
|
10789
|
+
this.updateLightBuffer()
|
|
10790
|
+
const list = (lights ?? []).filter((l) => l.kind !== "directional").slice(0, MAX_LIGHTS)
|
|
10731
10791
|
this.docLightCount = list.length
|
|
10732
10792
|
// RECORDS only — the header belongs to allocateLightSlots, the one writer.
|
|
10733
10793
|
// This used to zero-and-upload the header region too, which left two CPU
|
|
@@ -10756,7 +10816,7 @@ export class Engine {
|
|
|
10756
10816
|
// vector cannot be normalised, and a cone around nothing would light
|
|
10757
10817
|
// nothing at all — a light that vanishes because its aim was left at the
|
|
10758
10818
|
// default is worse than one that shines everywhere.
|
|
10759
|
-
const len = l.aim ? Math.hypot(l.aim.x, l.aim.y, l.aim.z) : 0
|
|
10819
|
+
const len = l.aim && l.kind !== "point" ? Math.hypot(l.aim.x, l.aim.y, l.aim.z) : 0
|
|
10760
10820
|
this.lightsData[b + 7] = len > 0 ? 1 : 0
|
|
10761
10821
|
this.lightsData[b + 8] = len > 0 ? l.aim!.x / len : 0
|
|
10762
10822
|
this.lightsData[b + 9] = len > 0 ? l.aim!.y / len : 0
|
|
@@ -10768,7 +10828,8 @@ export class Engine {
|
|
|
10768
10828
|
const inner = len > 0 ? Math.cos((Math.min(Math.max(l.innerAngle ?? (l.angle ?? 45) * 0.8, 0), 179) / 2) * (Math.PI / 180)) : -1
|
|
10769
10829
|
this.lightsData[b + 11] = outer
|
|
10770
10830
|
this.lightsData[b + 12] = Math.max(inner, outer)
|
|
10771
|
-
|
|
10831
|
+
// The layers it does NOT reach, as bits — inverted so a zero reaches all.
|
|
10832
|
+
this.lightsWords[b + 13] = ~(l.layers ?? ALL_LAYERS) >>> 0
|
|
10772
10833
|
this.lightsData[b + 14] = 0
|
|
10773
10834
|
this.lightsData[b + 15] = 0
|
|
10774
10835
|
}
|
|
@@ -11251,6 +11312,7 @@ export class Engine {
|
|
|
11251
11312
|
* ground plane must not draw, and the far shadow cascade has nothing to
|
|
11252
11313
|
* cover without one (see the cascade loop). */
|
|
11253
11314
|
hasStage(): boolean {
|
|
11315
|
+
if (this.nativeStage) return true
|
|
11254
11316
|
for (const inst of this.modelInstances.values()) if (inst.isStage) return true
|
|
11255
11317
|
return false
|
|
11256
11318
|
}
|
|
@@ -11280,11 +11342,6 @@ export class Engine {
|
|
|
11280
11342
|
this.groundHidden = !on
|
|
11281
11343
|
}
|
|
11282
11344
|
|
|
11283
|
-
/** Per cascade: does its map currently hold nothing but the cleared far plane?
|
|
11284
|
-
* Set by the cascade loop, which skips a cascade that is unwanted and already
|
|
11285
|
-
* cleared rather than re-clearing it every frame. */
|
|
11286
|
-
private shadowCascadeCleared: boolean[] = []
|
|
11287
|
-
|
|
11288
11345
|
/**
|
|
11289
11346
|
* Moves a model to a new key. Nothing about it is rebuilt — same GPU
|
|
11290
11347
|
* buffers, same style groups, same physics — only which string the rest
|
|
@@ -11306,96 +11363,34 @@ export class Engine {
|
|
|
11306
11363
|
}
|
|
11307
11364
|
|
|
11308
11365
|
/**
|
|
11309
|
-
*
|
|
11310
|
-
*
|
|
11311
|
-
*
|
|
11312
|
-
* After the graph, not in its ambient: an NPR ramp read an ambient fill as
|
|
11313
|
-
* light, so sliding it moved her shadows and flipped whole regions across a
|
|
11314
|
-
* hard step. Added after, it brightens her evenly and every shadow stays put.
|
|
11315
|
-
*
|
|
11316
|
-
* A game lights its stage and its characters apart. Aether Gazer's rooms are
|
|
11317
|
-
* lit by their lamps and a near-black ambient, while its characters take a
|
|
11318
|
-
* flat base light of their own — so a stage whose World is right for the room
|
|
11319
|
-
* leaves a face turned from the lamps in the dark. This is that second light,
|
|
11320
|
-
* for the models the host counts as cast.
|
|
11321
|
-
*/
|
|
11322
|
-
setModelFill(name: string, fill: Vec3 | null): boolean {
|
|
11323
|
-
return this.writeModelLight(name, 0, fill)
|
|
11324
|
-
}
|
|
11325
|
-
|
|
11326
|
-
/**
|
|
11327
|
-
* A model's own sun: the colour and strength the scene's sun has FOR THIS
|
|
11328
|
-
* MODEL, in place of the scene's. Null gives it the scene's sun back.
|
|
11329
|
-
*
|
|
11330
|
-
* The other half of lighting a stage and its cast apart. A game's stage is
|
|
11331
|
-
* lit by its own daylight — Aether Gazer's kitchen at twenty times white —
|
|
11332
|
-
* while its characters take a key light of their own that the stage never
|
|
11333
|
-
* states. So a stage carries the sun it was lit by, and the scene's sun
|
|
11334
|
-
* stays the cast's. Direction and shadow are still the scene's: one sun
|
|
11335
|
-
* casts, and it casts the same way on both.
|
|
11336
|
-
*/
|
|
11337
|
-
setModelSun(name: string, sun: Vec3 | null): boolean {
|
|
11338
|
-
return this.writeModelLight(name, 4, sun)
|
|
11339
|
-
}
|
|
11340
|
-
|
|
11341
|
-
/**
|
|
11342
|
-
* Light one model by the sky's AVERAGE rather than its shape: the world's
|
|
11343
|
-
* colour and brightness from every direction alike. False gives it the
|
|
11344
|
-
* directional sky back.
|
|
11366
|
+
* One model's own ambient: the irradiance arriving at it, as the 27 floats of
|
|
11367
|
+
* folded SH setWorldAmbient takes (ibl.ts), in place of the world's. Null
|
|
11368
|
+
* gives it the world's again.
|
|
11345
11369
|
*
|
|
11346
|
-
*
|
|
11347
|
-
*
|
|
11348
|
-
*
|
|
11349
|
-
* Gazer gives its characters one flat base light instead. World strength and
|
|
11350
|
-
* colour still reach her; only the shape is gone. A flat World is its own
|
|
11351
|
-
* average, so without an HDRI this changes nothing.
|
|
11370
|
+
* The game's pipeline lights every renderer by its own probe sample, and a
|
|
11371
|
+
* character by an ambient of its own; a loader that carries those hands them
|
|
11372
|
+
* in here. Linear radiance, strength included.
|
|
11352
11373
|
*/
|
|
11353
|
-
|
|
11354
|
-
return this.writeModelLight(name, 8, flat ? new Vec3(1, 0, 0) : null)
|
|
11355
|
-
}
|
|
11356
|
-
|
|
11357
|
-
/** One part of a model's light, at `at` (0 the fill, 4 the sun, 8 its
|
|
11358
|
-
* options) in its ModelLight; w says whether that part is set. All unset
|
|
11359
|
-
* releases the buffer and the model takes the shared zero stand-in again. */
|
|
11360
|
-
private writeModelLight(name: string, at: 0 | 4 | 8, value: Vec3 | null): boolean {
|
|
11374
|
+
setModelAmbient(name: string, sh: ArrayLike<number> | null): boolean {
|
|
11361
11375
|
const inst = this.modelInstances.get(name)
|
|
11362
11376
|
if (!inst || !this.device) return false
|
|
11363
|
-
const
|
|
11364
|
-
|
|
11365
|
-
|
|
11366
|
-
|
|
11367
|
-
if (!inst.lightBuffer) return true
|
|
11368
|
-
const retired = inst.lightBuffer
|
|
11369
|
-
inst.lightBuffer = null
|
|
11370
|
-
inst.modelLight = null
|
|
11371
|
-
inst.mainPerInstanceBindGroup = this.perInstanceBindGroup(name, inst.skinMatrixBuffer, null)
|
|
11372
|
-
this.bundlesDirty = true
|
|
11373
|
-
// Retired once the GPU is done with it: an encoded frame may still name it.
|
|
11374
|
-
void this.device.queue.onSubmittedWorkDone().then(() => retired.destroy())
|
|
11375
|
-
return true
|
|
11376
|
-
}
|
|
11377
|
-
inst.modelLight = light
|
|
11378
|
-
if (!inst.lightBuffer) {
|
|
11379
|
-
inst.lightBuffer = this.device.createBuffer({
|
|
11380
|
-
label: `${name}: light`,
|
|
11381
|
-
size: 48,
|
|
11382
|
-
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
|
|
11383
|
-
})
|
|
11384
|
-
inst.mainPerInstanceBindGroup = this.perInstanceBindGroup(name, inst.skinMatrixBuffer, inst.lightBuffer)
|
|
11385
|
-
this.bundlesDirty = true
|
|
11377
|
+
const o = inst.objectLight
|
|
11378
|
+
o[4] = sh && sh.length >= 27 ? 1 : 0
|
|
11379
|
+
for (let i = 0; i < 9; i++) {
|
|
11380
|
+
o.set(sh && sh.length >= 27 ? [sh[i * 3], sh[i * 3 + 1], sh[i * 3 + 2], 0] : [0, 0, 0, 0], 8 + i * 4)
|
|
11386
11381
|
}
|
|
11387
|
-
this.device.queue.writeBuffer(inst.lightBuffer, 0,
|
|
11382
|
+
this.device.queue.writeBuffer(inst.lightBuffer, 0, o)
|
|
11388
11383
|
return true
|
|
11389
11384
|
}
|
|
11390
11385
|
|
|
11391
|
-
/** The per-model group: its skinning matrices and its
|
|
11392
|
-
private perInstanceBindGroup(name: string, skinMatrixBuffer: GPUBuffer, light: GPUBuffer
|
|
11386
|
+
/** The per-model group: its skinning matrices and its ObjectLight. */
|
|
11387
|
+
private perInstanceBindGroup(name: string, skinMatrixBuffer: GPUBuffer, light: GPUBuffer): GPUBindGroup {
|
|
11393
11388
|
return this.device.createBindGroup({
|
|
11394
11389
|
label: `${name}: main per-instance bind group`,
|
|
11395
11390
|
layout: this.mainPerInstanceBindGroupLayout,
|
|
11396
11391
|
entries: [
|
|
11397
11392
|
{ binding: 0, resource: { buffer: skinMatrixBuffer } },
|
|
11398
|
-
{ binding: 1, resource: { buffer: light
|
|
11393
|
+
{ binding: 1, resource: { buffer: light } },
|
|
11399
11394
|
],
|
|
11400
11395
|
})
|
|
11401
11396
|
}
|
|
@@ -13080,8 +13075,10 @@ export class Engine {
|
|
|
13080
13075
|
let changed = force
|
|
13081
13076
|
for (let i = 0; i < this.cullDraws.length; i++) {
|
|
13082
13077
|
const { inst, draw } = this.cullDraws[i]
|
|
13078
|
+
// bit 0: switched off; bit 1: drawn by its native look, not its graph
|
|
13083
13079
|
const v =
|
|
13084
|
-
inst.hiddenMaterials.has(draw.materialName) || inst.morphHiddenMaterials.has(draw.materialName) ? 1 : 0
|
|
13080
|
+
(inst.hiddenMaterials.has(draw.materialName) || inst.morphHiddenMaterials.has(draw.materialName) ? 1 : 0) |
|
|
13081
|
+
(this.nativeLooks?.dresses(inst.name, draw.materialName) ? 2 : 0)
|
|
13085
13082
|
if (out[i] !== v) {
|
|
13086
13083
|
out[i] = v
|
|
13087
13084
|
changed = true
|
|
@@ -13219,6 +13216,12 @@ export class Engine {
|
|
|
13219
13216
|
minZ = Math.min(minZ, data[o + 18] - r); maxZ = Math.max(maxZ, data[o + 18] + r)
|
|
13220
13217
|
}
|
|
13221
13218
|
}
|
|
13219
|
+
if (this.nativeStage) {
|
|
13220
|
+
const nb = this.nativeStage.bounds
|
|
13221
|
+
minX = Math.min(minX, nb.min[0]); maxX = Math.max(maxX, nb.max[0])
|
|
13222
|
+
minY = Math.min(minY, nb.min[1]); maxY = Math.max(maxY, nb.max[1])
|
|
13223
|
+
minZ = Math.min(minZ, nb.min[2]); maxZ = Math.max(maxZ, nb.max[2])
|
|
13224
|
+
}
|
|
13222
13225
|
if (!Number.isFinite(minX)) {
|
|
13223
13226
|
this.shadowSceneBounds = null
|
|
13224
13227
|
return
|
|
@@ -14066,7 +14069,19 @@ export class Engine {
|
|
|
14066
14069
|
}),
|
|
14067
14070
|
)
|
|
14068
14071
|
|
|
14069
|
-
|
|
14072
|
+
// Its ObjectLight: the world's ambient, and the layers its kind draws on —
|
|
14073
|
+
// the cast on the character layer as well as the default one. Words:
|
|
14074
|
+
// layers [0..3], ambient flag [4..7], SH [8..43] — see materials/common.ts.
|
|
14075
|
+
const objectLight = new Float32Array(44)
|
|
14076
|
+
new Uint32Array(objectLight.buffer)[0] =
|
|
14077
|
+
isStage || isPlane || isProp ? RENDERING_LAYER_DEFAULT : (RENDERING_LAYER_DEFAULT | RENDERING_LAYER_CHARACTER) >>> 0
|
|
14078
|
+
const lightBuffer = this.device.createBuffer({
|
|
14079
|
+
label: `${name}: object light`,
|
|
14080
|
+
size: objectLight.byteLength,
|
|
14081
|
+
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
|
|
14082
|
+
})
|
|
14083
|
+
this.device.queue.writeBuffer(lightBuffer, 0, objectLight)
|
|
14084
|
+
const mainPerInstanceBindGroup = this.perInstanceBindGroup(name, skinMatrixBuffer, lightBuffer)
|
|
14070
14085
|
|
|
14071
14086
|
const pickPerInstanceBindGroup = this.device.createBindGroup({
|
|
14072
14087
|
label: `${name}: pick per-instance bind group`,
|
|
@@ -14105,8 +14120,8 @@ export class Engine {
|
|
|
14105
14120
|
shadowDrawCalls: [],
|
|
14106
14121
|
shadowBindGroups,
|
|
14107
14122
|
mainPerInstanceBindGroup,
|
|
14108
|
-
lightBuffer
|
|
14109
|
-
|
|
14123
|
+
lightBuffer,
|
|
14124
|
+
objectLight,
|
|
14110
14125
|
pickPerInstanceBindGroup,
|
|
14111
14126
|
pickDrawCalls: [],
|
|
14112
14127
|
isStage,
|
|
@@ -14377,7 +14392,6 @@ export class Engine {
|
|
|
14377
14392
|
gb[3] = fadeStart
|
|
14378
14393
|
gb[4] = fadeEnd
|
|
14379
14394
|
gb[5] = shadowStrength
|
|
14380
|
-
gb[6] = 1 / SHADOW_CASCADES[0].mapSize
|
|
14381
14395
|
gb[7] = gridSpacing
|
|
14382
14396
|
gb[8] = gridLineWidth
|
|
14383
14397
|
gb[9] = gridLineOpacity
|
|
@@ -14399,19 +14413,6 @@ export class Engine {
|
|
|
14399
14413
|
// Which variant the draw picks. Zero is the sharp shader, which is the one
|
|
14400
14414
|
// that existed before softness did.
|
|
14401
14415
|
this.groundSoft = gb[18] > 0
|
|
14402
|
-
// gb[17] — does the FAR cascade hold anything?
|
|
14403
|
-
//
|
|
14404
|
-
// It holds something only when a stage is loaded; that is what it exists for
|
|
14405
|
-
// and the cascade loop already skips drawing into it otherwise, leaving it
|
|
14406
|
-
// cleared. A cleared depth map compares as "no occluder", so the ground's far
|
|
14407
|
-
// branch is nine comparison taps whose answer is known in advance.
|
|
14408
|
-
//
|
|
14409
|
-
// That branch runs wherever the NEAR cascade does not reach, and the near one
|
|
14410
|
-
// is a 64-unit box around the camera target — so on a floor receding to the
|
|
14411
|
-
// horizon it is most of the visible pixels, on the most expensive
|
|
14412
|
-
// full-coverage draw in the frame. Skipping it is free in the exact sense:
|
|
14413
|
-
// the shader takes vis = 1.0, which is what the taps would have returned.
|
|
14414
|
-
gb[17] = this.hasStage() ? 1 : 0
|
|
14415
14416
|
// gb[20..23] — the caster sphere, refreshed every frame by
|
|
14416
14417
|
// writeGroundCasterSphere. Zero here so a frame that renders before the
|
|
14417
14418
|
// first cull (there is one) reads "nothing casts" and skips the taps, which
|
|
@@ -14476,12 +14477,11 @@ export class Engine {
|
|
|
14476
14477
|
entries: [
|
|
14477
14478
|
{ binding: 0, resource: { buffer: this.cameraUniformBuffer } },
|
|
14478
14479
|
{ binding: 1, resource: { buffer: this.lightUniformBuffer } },
|
|
14479
|
-
{ binding: 2, resource: this.
|
|
14480
|
+
{ binding: 2, resource: this.shadowAtlasView },
|
|
14480
14481
|
{ binding: 3, resource: this.shadowComparisonSampler },
|
|
14481
14482
|
{ binding: 4, resource: { buffer: this.groundShadowMaterialBuffer } },
|
|
14482
14483
|
{ binding: 5, resource: { buffer: this.shadowLightVPBuffer } },
|
|
14483
14484
|
{ binding: 6, resource: { buffer: this.lightsBuffer } },
|
|
14484
|
-
{ binding: 7, resource: this.shadowMapDepthViews[SHADOW_CASCADES.length - 1] },
|
|
14485
14485
|
{ binding: 8, resource: { buffer: this.mirrorVPBuffer } },
|
|
14486
14486
|
// Created in handleResize, which runs during init — before any ground
|
|
14487
14487
|
// can exist to bind it.
|
|
@@ -14499,12 +14499,11 @@ export class Engine {
|
|
|
14499
14499
|
entries: [
|
|
14500
14500
|
{ binding: 0, resource: { buffer: this.mirrorCameraBuffer } },
|
|
14501
14501
|
{ binding: 1, resource: { buffer: this.lightUniformBuffer } },
|
|
14502
|
-
{ binding: 2, resource: this.
|
|
14502
|
+
{ binding: 2, resource: this.shadowAtlasView },
|
|
14503
14503
|
{ binding: 3, resource: this.shadowComparisonSampler },
|
|
14504
14504
|
{ binding: 4, resource: { buffer: this.groundShadowMaterialBuffer } },
|
|
14505
14505
|
{ binding: 5, resource: { buffer: this.shadowLightVPBuffer } },
|
|
14506
14506
|
{ binding: 6, resource: { buffer: this.lightsBuffer } },
|
|
14507
|
-
{ binding: 7, resource: this.shadowMapDepthViews[SHADOW_CASCADES.length - 1] },
|
|
14508
14507
|
{ binding: 8, resource: { buffer: this.mirrorVPBuffer } },
|
|
14509
14508
|
// Stand-ins, not the live mirror textures: this group draws INTO the
|
|
14510
14509
|
// mirror pass, where those are the attachments.
|
|
@@ -16455,52 +16454,51 @@ export class Engine {
|
|
|
16455
16454
|
// the final frame's depth — and the ground, which draws on `hasGround` alone,
|
|
16456
16455
|
// keeps PCF-sampling a character that is no longer in the scene. One clearing
|
|
16457
16456
|
// pass on the transition to empty, then it stops.
|
|
16458
|
-
|
|
16459
|
-
|
|
16460
|
-
|
|
16461
|
-
|
|
16462
|
-
//
|
|
16463
|
-
//
|
|
16464
|
-
//
|
|
16465
|
-
//
|
|
16466
|
-
// So
|
|
16467
|
-
// the
|
|
16468
|
-
//
|
|
16469
|
-
// here rather than a missing one. Load a stage and it comes straight back.
|
|
16470
|
-
//
|
|
16471
|
-
// 0.43 had ONE shadow map. This is half of what the second one costs.
|
|
16457
|
+
// The game's globals before anything native draws: the stage's casters
|
|
16458
|
+
// into the atlas are the first.
|
|
16459
|
+
if (this.nativeStage || this.nativeLooks?.size) this.prepareNativeLooks(encoder)
|
|
16460
|
+
if (hasModels || this.nativeStage || this.shadowMapPopulated) {
|
|
16461
|
+
// The outer cascades are the STAGE's, and each costs a pass over the whole
|
|
16462
|
+
// cast. A scene with no stage has no set piece out there: every caster
|
|
16463
|
+
// sits inside the inner cascade, which follows the camera target, and
|
|
16464
|
+
// the outer tiles' only readers are ground pixels beyond it, where
|
|
16465
|
+
// nothing is casting. So without a stage only the inner tile is drawn;
|
|
16466
|
+
// the others keep the far plane cascade 0's pass cleared the whole atlas
|
|
16467
|
+
// to, which compares as "no occluder" — the correct answer there.
|
|
16472
16468
|
const stage = this.hasStage()
|
|
16473
16469
|
for (let ci = 0; ci < SHADOW_CASCADES.length; ci++) {
|
|
16474
|
-
|
|
16475
|
-
|
|
16476
|
-
// holds the far plane from the pass that cleared it.
|
|
16477
|
-
if (!wanted && this.shadowCascadeCleared[ci]) continue
|
|
16470
|
+
if (ci > 0 && !stage) continue
|
|
16471
|
+
const c = SHADOW_CASCADES[ci]
|
|
16478
16472
|
const sp = encoder.beginRenderPass({
|
|
16479
|
-
// One timestamp pair exists for "shadow"; the
|
|
16473
|
+
// One timestamp pair exists for "shadow"; the inner cascade wears it.
|
|
16480
16474
|
timestampWrites: ci === 0 ? this.stamps("shadow") : undefined,
|
|
16481
16475
|
colorAttachments: [],
|
|
16482
16476
|
depthStencilAttachment: {
|
|
16483
|
-
view: this.
|
|
16477
|
+
view: this.shadowAtlasView,
|
|
16484
16478
|
depthClearValue: 1.0,
|
|
16485
|
-
depthLoadOp: "clear",
|
|
16479
|
+
depthLoadOp: ci === 0 ? "clear" : "load",
|
|
16486
16480
|
depthStoreOp: "store",
|
|
16487
16481
|
},
|
|
16488
16482
|
})
|
|
16483
|
+
// The tile: a bundle draws through the pass's viewport, which it
|
|
16484
|
+
// cannot set itself, so one recording serves whichever tile is live.
|
|
16485
|
+
sp.setViewport(c.origin[0], c.origin[1], c.mapSize, c.mapSize, 0, 1)
|
|
16489
16486
|
// The per-model `visible` test that used to guard this is gone: it is a
|
|
16490
16487
|
// per-frame boolean, and baking it into a bundle would make toggling a
|
|
16491
16488
|
// model re-record. It lives in the cull compute now, which zeroes the
|
|
16492
16489
|
// instance count of an invisible model's draws.
|
|
16493
|
-
if (
|
|
16490
|
+
if (this.shadowBundles[ci]) sp.executeBundles([this.shadowBundles[ci]])
|
|
16494
16491
|
// The mirror throws shade like anything else standing on the floor.
|
|
16495
16492
|
// Direct rather than in the bundle: the bundles are recorded on scene
|
|
16496
16493
|
// STRUCTURE, and a mirror comes and goes with an effect's weight.
|
|
16497
|
-
if (
|
|
16494
|
+
if (this.mirrorSurface && this.mirrorShadowPipeline) {
|
|
16498
16495
|
sp.setPipeline(this.mirrorShadowPipeline)
|
|
16499
16496
|
sp.setBindGroup(0, this.mirrorShadowBindGroups[ci])
|
|
16500
16497
|
sp.draw(6)
|
|
16501
16498
|
}
|
|
16499
|
+
if (this.nativeStage && this.nativeShadowHost)
|
|
16500
|
+
this.nativeStage.drawShadow(sp, this.nativeShadowHost, this.nativeGlobals, ci, this.shadowLightVPMatrix.subarray(ci * 16, ci * 16 + 16), 8)
|
|
16502
16501
|
sp.end()
|
|
16503
|
-
this.shadowCascadeCleared[ci] = !wanted
|
|
16504
16502
|
}
|
|
16505
16503
|
// The cast's shadow on a stage: drawn while a stage asks for it and has a
|
|
16506
16504
|
// cast to shade it with; cleared once when it stops, then left alone.
|
|
@@ -16514,7 +16512,7 @@ export class Engine {
|
|
|
16514
16512
|
cp.end()
|
|
16515
16513
|
this.castShadowCleared = !castOn
|
|
16516
16514
|
}
|
|
16517
|
-
this.shadowMapPopulated = hasModels
|
|
16515
|
+
this.shadowMapPopulated = hasModels || !!this.nativeStage
|
|
16518
16516
|
}
|
|
16519
16517
|
|
|
16520
16518
|
// Before the particles and before the field pass: both may read the grid,
|
|
@@ -16560,6 +16558,17 @@ export class Engine {
|
|
|
16560
16558
|
// is FALSE for the cleared buffer — every such fragment silently rejected.
|
|
16561
16559
|
// One redundant word against a whole class of invisible failure.
|
|
16562
16560
|
pass.setStencilReference(Engine.STENCIL_EYE_VALUE)
|
|
16561
|
+
// The models dressed in the game's materials, and the game's character
|
|
16562
|
+
// passes after them (unity/looks.ts). They set their own stencil
|
|
16563
|
+
// references, so the engine's is put back after.
|
|
16564
|
+
if (this.nativeLooks?.size) {
|
|
16565
|
+
this.nativeLooks.drawOpaque(pass)
|
|
16566
|
+
pass.setStencilReference(Engine.STENCIL_EYE_VALUE)
|
|
16567
|
+
}
|
|
16568
|
+
if (this.nativeStage && this.nativeHost) {
|
|
16569
|
+
this.nativeStage.drawOpaque(pass, this.nativeHost, this.nativeGlobals, this.nativeFrameTextures(), this.eyeArray())
|
|
16570
|
+
pass.setStencilReference(Engine.STENCIL_EYE_VALUE)
|
|
16571
|
+
}
|
|
16563
16572
|
if (this.hasGround) this.renderGround(pass)
|
|
16564
16573
|
// After the ground for the same early-z reason, and before the transparent
|
|
16565
16574
|
// phase so sheer fabric blends over the glass rather than being depth
|
|
@@ -16591,6 +16600,14 @@ export class Engine {
|
|
|
16591
16600
|
// which was never this renderer's bottleneck.
|
|
16592
16601
|
const camView = this.sceneView("camera")
|
|
16593
16602
|
this.forEachInstance((inst) => this.renderModelTransparentPhase(pass, inst, camView))
|
|
16603
|
+
if (this.nativeStage && this.nativeHost) {
|
|
16604
|
+
this.nativeStage.drawTransparent(pass, this.nativeHost, this.nativeGlobals, this.nativeFrameTextures(), this.eyeArray())
|
|
16605
|
+
pass.setStencilReference(Engine.STENCIL_EYE_VALUE)
|
|
16606
|
+
}
|
|
16607
|
+
if (this.nativeLooks?.size) {
|
|
16608
|
+
this.nativeLooks.drawTransparent(pass)
|
|
16609
|
+
pass.setStencilReference(Engine.STENCIL_EYE_VALUE)
|
|
16610
|
+
}
|
|
16594
16611
|
// Last in the pass: depth-tested against everything drawn above, so a
|
|
16595
16612
|
// particle behind the character is simply hidden, and still inside the HDR
|
|
16596
16613
|
// target so an `#bloom` effect reaches the pyramid below.
|
|
@@ -17125,11 +17142,44 @@ export class Engine {
|
|
|
17125
17142
|
|
|
17126
17143
|
// Compile a group's graph → WGSL → pipeline(s), install keyed by group id. Reuses the
|
|
17127
17144
|
// install (pipeline + uniform buffer) when the graph/integration is byte-unchanged.
|
|
17145
|
+
/** A graph's own images (ShaderGraph.images), fetched and decoded once per URL. */
|
|
17146
|
+
private graphImageCache = new Map<string, Promise<ImageBitmap | null>>()
|
|
17147
|
+
|
|
17148
|
+
private graphImage(url: string): Promise<ImageBitmap | null> {
|
|
17149
|
+
let p = this.graphImageCache.get(url)
|
|
17150
|
+
if (!p) {
|
|
17151
|
+
p = fetch(url)
|
|
17152
|
+
.then((r) => (r.ok ? r.blob() : Promise.reject(new Error(`${r.status} ${url}`))))
|
|
17153
|
+
.then((b) => createImageBitmap(b, { colorSpaceConversion: "none", premultiplyAlpha: "none" }))
|
|
17154
|
+
.catch((e) => {
|
|
17155
|
+
console.warn(`[style] graph image: ${(e as Error).message}`)
|
|
17156
|
+
return null
|
|
17157
|
+
})
|
|
17158
|
+
this.graphImageCache.set(url, p)
|
|
17159
|
+
}
|
|
17160
|
+
return p
|
|
17161
|
+
}
|
|
17162
|
+
|
|
17163
|
+
/** The group with its graph's images, where the group brings none of its own. */
|
|
17164
|
+
private async withGraphImages(group: StyleGroup): Promise<StyleGroup> {
|
|
17165
|
+
const wanted = group.graph.images
|
|
17166
|
+
if (!wanted?.length || group.images?.length) return group
|
|
17167
|
+
const images = await Promise.all(
|
|
17168
|
+
wanted.slice(0, 4).map(async (w) => {
|
|
17169
|
+
if (!w) return null
|
|
17170
|
+
const source = await this.graphImage(w.url)
|
|
17171
|
+
return source ? { source, srgb: w.srgb ?? false } : null
|
|
17172
|
+
}),
|
|
17173
|
+
)
|
|
17174
|
+
return { ...group, images }
|
|
17175
|
+
}
|
|
17176
|
+
|
|
17128
17177
|
private async compileAndInstallGroup(
|
|
17129
17178
|
inst: ModelInstance,
|
|
17130
|
-
|
|
17179
|
+
groupIn: StyleGroup,
|
|
17131
17180
|
opts?: CompileOptions,
|
|
17132
17181
|
): Promise<ApplyStyleGroupResult> {
|
|
17182
|
+
const group = await this.withGraphImages(groupIn)
|
|
17133
17183
|
const renderClass = group.renderClass ?? "auto"
|
|
17134
17184
|
const alphaMode = group.alphaMode ?? "opaque"
|
|
17135
17185
|
// THE MAPS ARE PART OF THE SIGNATURE, because a matching one skips the
|
|
@@ -18283,6 +18333,257 @@ export class Engine {
|
|
|
18283
18333
|
cd[anchorBase + 11] = count
|
|
18284
18334
|
}
|
|
18285
18335
|
|
|
18336
|
+
/**
|
|
18337
|
+
* Dress a model in the game's own materials — a look exported with ag-rip
|
|
18338
|
+
* (translated shaders, their images, per-material passes and values, and a
|
|
18339
|
+
* character rig) — or take it off with null. The materials it names are
|
|
18340
|
+
* drawn by the game's shaders from then on; the rest keep their graphs, and
|
|
18341
|
+
* every one still casts its shadow through the engine's pass.
|
|
18342
|
+
*/
|
|
18343
|
+
setModelNativeLook(name: string, look: NativeLook | null): boolean {
|
|
18344
|
+
const inst = this.modelInstances.get(name)
|
|
18345
|
+
if (!inst || !this.device) return false
|
|
18346
|
+
if (!look) {
|
|
18347
|
+
this.nativeLooks?.remove(name)
|
|
18348
|
+
this.writeCullHidden(true)
|
|
18349
|
+
return true
|
|
18350
|
+
}
|
|
18351
|
+
if (!this.nativeLooks) this.nativeLooks = new NativeLooks(this.device, this.ensureNativeHosts())
|
|
18352
|
+
const skeleton = inst.model.getSkeleton()
|
|
18353
|
+
const head = skeleton.bones.findIndex((b) => b.name === "頭")
|
|
18354
|
+
// The head's rest position: a rigid inverse bind [R t] is the bind pose
|
|
18355
|
+
// inverted, so the bind position is −Rᵀt.
|
|
18356
|
+
let headRest: [number, number, number] = [0, 0, 0]
|
|
18357
|
+
if (head >= 0) {
|
|
18358
|
+
const m = skeleton.inverseBindMatrices.subarray(head * 16, head * 16 + 16)
|
|
18359
|
+
headRest = [
|
|
18360
|
+
-(m[0] * m[12] + m[1] * m[13] + m[2] * m[14]),
|
|
18361
|
+
-(m[4] * m[12] + m[5] * m[13] + m[6] * m[14]),
|
|
18362
|
+
-(m[8] * m[12] + m[9] * m[13] + m[10] * m[14]),
|
|
18363
|
+
]
|
|
18364
|
+
}
|
|
18365
|
+
const draws = inst.drawCalls
|
|
18366
|
+
.filter((dc) => dc.baseBindGroupEntries)
|
|
18367
|
+
.map((dc) => ({
|
|
18368
|
+
materialName: dc.materialName,
|
|
18369
|
+
firstIndex: dc.firstIndex,
|
|
18370
|
+
count: dc.count,
|
|
18371
|
+
diffuse: dc.baseBindGroupEntries!.find((e) => e.binding === 0)!.resource as GPUTextureView,
|
|
18372
|
+
}))
|
|
18373
|
+
this.nativeLooks.install(
|
|
18374
|
+
{
|
|
18375
|
+
name,
|
|
18376
|
+
vertices: inst.model.getVertices(),
|
|
18377
|
+
indices: inst.model.getIndices(),
|
|
18378
|
+
vertexBuffer: inst.vertexBuffer,
|
|
18379
|
+
jointsBuffer: inst.jointsBuffer,
|
|
18380
|
+
weightsBuffer: inst.weightsBuffer,
|
|
18381
|
+
skinMatrixBuffer: inst.skinMatrixBuffer,
|
|
18382
|
+
indexBuffer: inst.indexBuffer,
|
|
18383
|
+
draws,
|
|
18384
|
+
hidden: (m) => inst.hiddenMaterials.has(m) || inst.morphHiddenMaterials.has(m),
|
|
18385
|
+
headSkin: () => (head >= 0 ? inst.model.getSkinMatrices().subarray(head * 16, head * 16 + 16) : null),
|
|
18386
|
+
headRest,
|
|
18387
|
+
layers: new Uint32Array(inst.objectLight.buffer)[0],
|
|
18388
|
+
},
|
|
18389
|
+
look,
|
|
18390
|
+
)
|
|
18391
|
+
this.writeCullHidden(true)
|
|
18392
|
+
return true
|
|
18393
|
+
}
|
|
18394
|
+
|
|
18395
|
+
/** The hosts for the game's shaders: the scene pass's and the shadow atlas's,
|
|
18396
|
+
* with the stand-ins an empty slot reads. Built on first use. */
|
|
18397
|
+
private ensureNativeHosts(): NativeHost {
|
|
18398
|
+
if (this.nativeHost) return this.nativeHost
|
|
18399
|
+
const d = this.device
|
|
18400
|
+
const noShadow = d.createTexture({
|
|
18401
|
+
label: "native: no shadow",
|
|
18402
|
+
size: [1, 1],
|
|
18403
|
+
format: "depth32float",
|
|
18404
|
+
usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.TEXTURE_BINDING,
|
|
18405
|
+
})
|
|
18406
|
+
const enc = d.createCommandEncoder()
|
|
18407
|
+
enc
|
|
18408
|
+
.beginRenderPass({
|
|
18409
|
+
colorAttachments: [],
|
|
18410
|
+
depthStencilAttachment: { view: noShadow.createView(), depthClearValue: 1, depthLoadOp: "clear", depthStoreOp: "store" },
|
|
18411
|
+
})
|
|
18412
|
+
.end()
|
|
18413
|
+
d.queue.submit([enc.finish()])
|
|
18414
|
+
this.nativeNoShadowView = noShadow.createView()
|
|
18415
|
+
const solid = (label: string, rgba: number[], cube = false) => {
|
|
18416
|
+
const t = d.createTexture({
|
|
18417
|
+
label,
|
|
18418
|
+
size: [1, 1, cube ? 6 : 1],
|
|
18419
|
+
format: "rgba8unorm",
|
|
18420
|
+
usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST,
|
|
18421
|
+
})
|
|
18422
|
+
for (let l = 0; l < (cube ? 6 : 1); l++) d.queue.writeTexture({ texture: t, origin: [0, 0, l] }, new Uint8Array(rgba), { bytesPerRow: 4 }, [1, 1])
|
|
18423
|
+
return t.createView(cube ? { dimension: "cube" } : undefined)
|
|
18424
|
+
}
|
|
18425
|
+
const white = solid("native: white", [255, 255, 255, 255])
|
|
18426
|
+
const black = solid("native: black", [0, 0, 0, 255])
|
|
18427
|
+
// Unity's stand-ins for an empty slot, by the name a shader declares.
|
|
18428
|
+
this.nativeFallback = {
|
|
18429
|
+
white,
|
|
18430
|
+
black,
|
|
18431
|
+
"": white,
|
|
18432
|
+
grey: solid("native: grey", [128, 128, 128, 255]),
|
|
18433
|
+
gray: solid("native: gray", [128, 128, 128, 255]),
|
|
18434
|
+
bump: solid("native: bump", [128, 128, 255, 255]),
|
|
18435
|
+
red: solid("native: red", [255, 0, 0, 255]),
|
|
18436
|
+
}
|
|
18437
|
+
const fallback = {
|
|
18438
|
+
white,
|
|
18439
|
+
black,
|
|
18440
|
+
blackCube: solid("native: black cube", [0, 0, 0, 255], true),
|
|
18441
|
+
depth: this.nativeNoShadowView,
|
|
18442
|
+
comparison: this.shadowComparisonSampler,
|
|
18443
|
+
}
|
|
18444
|
+
this.nativeHost = new NativeHost(
|
|
18445
|
+
d,
|
|
18446
|
+
{
|
|
18447
|
+
colorFormats: sceneColorFormats(this.sceneFormats),
|
|
18448
|
+
depthFormat: this.depthFormat,
|
|
18449
|
+
sampleCount: Engine.MULTISAMPLE_COUNT,
|
|
18450
|
+
reversedZ: this.reversedZ,
|
|
18451
|
+
},
|
|
18452
|
+
fallback,
|
|
18453
|
+
)
|
|
18454
|
+
this.nativeShadowHost = new NativeHost(d, { colorFormats: [], depthFormat: Engine.SHADOW_DEPTH_FORMAT, sampleCount: 1, reversedZ: false }, fallback)
|
|
18455
|
+
return this.nativeHost
|
|
18456
|
+
}
|
|
18457
|
+
|
|
18458
|
+
/**
|
|
18459
|
+
* Load a game stage as the game draws it - a package ag-rip's
|
|
18460
|
+
* stage_native.py exported (stage.json and the files beside it, read through
|
|
18461
|
+
* `read` by their paths in it) - or remove the one loaded with null. Its
|
|
18462
|
+
* scene settings (fog, tint, ambient, environment) become the game shaders'
|
|
18463
|
+
* for the whole scene; its lights are the caller's to set, as lights.
|
|
18464
|
+
*/
|
|
18465
|
+
async setNativeStage(pkg: NativeStagePackage | null, read?: NativeStageReader, onProgress?: (done: number, total: number) => void): Promise<void> {
|
|
18466
|
+
const old = this.nativeStage
|
|
18467
|
+
this.nativeStage = null
|
|
18468
|
+
if (old) old.destroy([this.nativeHost!, this.nativeShadowHost!])
|
|
18469
|
+
if (!pkg || !read || !this.device) return
|
|
18470
|
+
const scene = this.ensureNativeHosts()
|
|
18471
|
+
this.nativeStage = await NativeStage.load(
|
|
18472
|
+
this.device,
|
|
18473
|
+
pkg,
|
|
18474
|
+
read,
|
|
18475
|
+
{ scene, shadow: this.nativeShadowHost! },
|
|
18476
|
+
{ mipmaps: (t, levels) => this.generateMipmaps(t, levels), fallback: this.nativeFallback },
|
|
18477
|
+
onProgress,
|
|
18478
|
+
)
|
|
18479
|
+
|
|
18480
|
+
}
|
|
18481
|
+
|
|
18482
|
+
/** What went wrong in the game's shaders, and the names nothing supplied. */
|
|
18483
|
+
nativeReport(): { errors: string[]; missing: Record<string, number> } {
|
|
18484
|
+
const errors = [...(this.nativeHost?.errors ?? []), ...(this.nativeShadowHost?.errors ?? [])]
|
|
18485
|
+
const missing: Record<string, number> = {}
|
|
18486
|
+
for (const h of [this.nativeHost, this.nativeShadowHost]) for (const [k, v] of h?.missing ?? []) missing[k] = (missing[k] ?? 0) + v
|
|
18487
|
+
return { errors, missing }
|
|
18488
|
+
}
|
|
18489
|
+
|
|
18490
|
+
private nativeFrameTextures(): Record<string, GPUTextureView | undefined> {
|
|
18491
|
+
return { _MainLightShadowmapTexture: this.shadowAtlasView, sim_CharacterShadowmap: this.nativeNoShadowView ?? undefined }
|
|
18492
|
+
}
|
|
18493
|
+
|
|
18494
|
+
private eyeArray(): [number, number, number] {
|
|
18495
|
+
const e = this.camera.getEyePosition()
|
|
18496
|
+
return [e.x, e.y, e.z]
|
|
18497
|
+
}
|
|
18498
|
+
|
|
18499
|
+
/** Before the scene pass: the dressed models' skinning and the game's globals. */
|
|
18500
|
+
private prepareNativeLooks(encoder: GPUCommandEncoder): void {
|
|
18501
|
+
const looks = this.nativeLooks
|
|
18502
|
+
const view = this.camera.getViewMatrix().values
|
|
18503
|
+
const proj = this.camera.getProjectionMatrix().values
|
|
18504
|
+
const eye = this.camera.getEyePosition()
|
|
18505
|
+
const sun = this.sun
|
|
18506
|
+
const dir = sun.direction.normalize()
|
|
18507
|
+
const k = sun.strength
|
|
18508
|
+
// The document's lamps as their records store them.
|
|
18509
|
+
const lights = []
|
|
18510
|
+
for (let i = 0; i < this.docLightCount; i++) {
|
|
18511
|
+
const b = LIGHT_HEADER + i * LIGHT_STRIDE
|
|
18512
|
+
const d = this.lightsData
|
|
18513
|
+
lights.push({
|
|
18514
|
+
position: [d[b], d[b + 1], d[b + 2]] as [number, number, number],
|
|
18515
|
+
radius: d[b + 3],
|
|
18516
|
+
color: [d[b + 4], d[b + 5], d[b + 6]] as [number, number, number],
|
|
18517
|
+
aim: [d[b + 8], d[b + 9], d[b + 10]] as [number, number, number],
|
|
18518
|
+
cosOuter: d[b + 11],
|
|
18519
|
+
cosInner: d[b + 12],
|
|
18520
|
+
layers: ~this.lightsWords[b + 13] >>> 0,
|
|
18521
|
+
})
|
|
18522
|
+
}
|
|
18523
|
+
// A game stage brings its own lights: the game's shaders get exactly those
|
|
18524
|
+
// (its sun, its lamps, all layers), whatever the scene's lamps are.
|
|
18525
|
+
const stage = this.nativeStage
|
|
18526
|
+
let sunDirection: [number, number, number] = [dir.x, dir.y, dir.z]
|
|
18527
|
+
let sunColor: [number, number, number] = [sun.color.x * k, sun.color.y * k, sun.color.z * k]
|
|
18528
|
+
let sunShadow = this.sunShadow
|
|
18529
|
+
if (stage) {
|
|
18530
|
+
// Its lamps are the stage's own; its key is the scene's sun, which the
|
|
18531
|
+
// host sets from the stage (and a person may then move).
|
|
18532
|
+
const L = stage.pkg.lights
|
|
18533
|
+
const sc = stage.pkg.scale
|
|
18534
|
+
lights.length = 0
|
|
18535
|
+
for (const l of L.additional) {
|
|
18536
|
+
lights.push({
|
|
18537
|
+
position: [-l.position[0] * sc, l.position[1] * sc, -l.position[2] * sc] as [number, number, number],
|
|
18538
|
+
radius: l.range * sc,
|
|
18539
|
+
color: [l.color[0] * sc * sc, l.color[1] * sc * sc, l.color[2] * sc * sc] as [number, number, number],
|
|
18540
|
+
aim: (l.aim ? gameDir(l.aim) : [0, 0, 0]) as [number, number, number],
|
|
18541
|
+
cosOuter: l.cosOuter ?? -1,
|
|
18542
|
+
cosInner: l.cosInner ?? -1,
|
|
18543
|
+
layers: 0xffffffff,
|
|
18544
|
+
unity: l.unity,
|
|
18545
|
+
})
|
|
18546
|
+
}
|
|
18547
|
+
}
|
|
18548
|
+
const s = this.world.strength
|
|
18549
|
+
const sh = this.worldAmbientSH ?? this.worldSH ?? this.worldGradientSH
|
|
18550
|
+
const g = unityFrameGlobals({
|
|
18551
|
+
scale: 8,
|
|
18552
|
+
view,
|
|
18553
|
+
proj,
|
|
18554
|
+
eye: [eye.x, eye.y, eye.z],
|
|
18555
|
+
near: this.camera.near,
|
|
18556
|
+
far: this.camera.far,
|
|
18557
|
+
width: this.canvas.width,
|
|
18558
|
+
height: this.canvas.height,
|
|
18559
|
+
time: this.sceneClock,
|
|
18560
|
+
dt: 1 / 60,
|
|
18561
|
+
sunDirection,
|
|
18562
|
+
sunColor,
|
|
18563
|
+
sunShadow,
|
|
18564
|
+
lights,
|
|
18565
|
+
ambientSH: sh ? Array.from(sh, (v) => v * s) : null,
|
|
18566
|
+
ambientFlat: [this.world.color.x * s, this.world.color.y * s, this.world.color.z * s],
|
|
18567
|
+
shadow: {
|
|
18568
|
+
viewProj: SHADOW_CASCADES.map((_, i) => this.shadowLightVPMatrix.subarray(i * 16, i * 16 + 16)),
|
|
18569
|
+
tiles: SHADOW_CASCADES.map((c) => [c.origin[0], c.origin[1]] as [number, number]),
|
|
18570
|
+
tileSize: SHADOW_CASCADES[0].mapSize,
|
|
18571
|
+
atlasSize: SHADOW_ATLAS_SIZE,
|
|
18572
|
+
spheres: cascadeSpheres(this.shadowView, this.shadowSceneBounds).map((p) => ({
|
|
18573
|
+
center: [p.center.x, p.center.y, p.center.z] as [number, number, number],
|
|
18574
|
+
radius: p.radius,
|
|
18575
|
+
})),
|
|
18576
|
+
},
|
|
18577
|
+
settings: this.nativeStage?.pkg.settings ?? null,
|
|
18578
|
+
})
|
|
18579
|
+
g.sim_TowardMatrix = towardMatrix(gameDir([view[2], view[6], view[10]]))
|
|
18580
|
+
g._OutlineMaxOffsetMultiplier = outlineMaxOffsetMultiplier(this.canvas.width)
|
|
18581
|
+
this.nativeGlobals = stableGlobals(this.nativeGlobals, g)
|
|
18582
|
+
this.nativeHost?.beginFrame()
|
|
18583
|
+
this.nativeShadowHost?.beginFrame()
|
|
18584
|
+
looks?.prepare(encoder, this.nativeGlobals, 8, this.nativeFrameTextures())
|
|
18585
|
+
}
|
|
18586
|
+
|
|
18286
18587
|
private updateSkinMatrices() {
|
|
18287
18588
|
this.forEachInstance((inst) => {
|
|
18288
18589
|
// Only a pose pass can change these, and an idle stage did not run one —
|
|
@@ -18359,3 +18660,30 @@ export class Engine {
|
|
|
18359
18660
|
this.stats.cpuRenderMs = Math.round(this.cpuRenderMs * 100) / 100
|
|
18360
18661
|
}
|
|
18361
18662
|
}
|
|
18663
|
+
|
|
18664
|
+
/**
|
|
18665
|
+
* This frame's Unity globals, keeping last frame's object for every value that
|
|
18666
|
+
* did not change: the native host refills a uniform member only when its value
|
|
18667
|
+
* object is new, so a light table or an SH that holds still costs nothing.
|
|
18668
|
+
*/
|
|
18669
|
+
function stableGlobals(prev: Record<string, NativeValue>, next: Record<string, NativeValue>): Record<string, NativeValue> {
|
|
18670
|
+
for (const k in next) {
|
|
18671
|
+
const a = prev[k]
|
|
18672
|
+
if (a !== undefined && sameValue(a, next[k])) next[k] = a
|
|
18673
|
+
}
|
|
18674
|
+
return next
|
|
18675
|
+
}
|
|
18676
|
+
|
|
18677
|
+
function sameValue(a: NativeValue, b: NativeValue): boolean {
|
|
18678
|
+
if (typeof a === "number" || typeof b === "number") return a === b
|
|
18679
|
+
if (a.length !== b.length) return false
|
|
18680
|
+
if (a instanceof Uint32Array !== b instanceof Uint32Array) return false
|
|
18681
|
+
for (let i = 0; i < a.length; i++) {
|
|
18682
|
+
const x = a[i] as number | ArrayLike<number>
|
|
18683
|
+
const y = b[i] as number | ArrayLike<number>
|
|
18684
|
+
if (typeof x === "number" || typeof y === "number") {
|
|
18685
|
+
if (x !== y) return false
|
|
18686
|
+
} else if (!sameValue(x, y)) return false
|
|
18687
|
+
}
|
|
18688
|
+
return true
|
|
18689
|
+
}
|