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
|
@@ -0,0 +1,568 @@
|
|
|
1
|
+
// A game stage drawn by its own shaders: the package ag-rip's stage_native.py
|
|
2
|
+
// writes from a recording of the game's scene — its meshes, textures and
|
|
3
|
+
// materials as the game holds them, every renderer at its place, and the
|
|
4
|
+
// translated shader variants each material's passes use.
|
|
5
|
+
//
|
|
6
|
+
// This is the compare page's renderer (agtools/web/agplay) inside the engine,
|
|
7
|
+
// with one difference that matters: nothing here is recorded per frame. The
|
|
8
|
+
// camera, the clock, the sun and lamps and the cascades are the engine's, turned
|
|
9
|
+
// into the game's globals by globals.ts; the stage brings only what is its own —
|
|
10
|
+
// geometry, materials, and its scene settings (fog, tint, ambient, environment
|
|
11
|
+
// cube), which it hands the engine to set for every native draw.
|
|
12
|
+
//
|
|
13
|
+
// SPACE. Meshes and renderer matrices stay in the game's world, as recorded:
|
|
14
|
+
// the globals carry the engine's camera into it (globals.ts, gameToEngine), so
|
|
15
|
+
// a native draw lands where the game drew it, as the MMD export places it.
|
|
16
|
+
//
|
|
17
|
+
// SHADOWS. Every caster draws its material's own SHADOWCASTER pass into the
|
|
18
|
+
// engine's cascade tiles, as the game drew its own atlas — alpha-tested leaves
|
|
19
|
+
// cast their leaves, not their quads.
|
|
20
|
+
|
|
21
|
+
import { NativeHost, type NativeShader, type NativeStream, type NativeValue, type ValueSource } from "./host"
|
|
22
|
+
import type { PassState } from "./state"
|
|
23
|
+
import { gameToEngine, invert4, mul4, scale4, flipY } from "./globals"
|
|
24
|
+
|
|
25
|
+
export type NativeStageMesh = {
|
|
26
|
+
id: string
|
|
27
|
+
vertexCount: number
|
|
28
|
+
/** Semantic -> [byte offset, components] (float32). */
|
|
29
|
+
streams: Record<string, [number, number]>
|
|
30
|
+
submeshes: { offset: number; count: number; topology?: string }[]
|
|
31
|
+
bounds: { center: [number, number, number]; size: [number, number, number] }
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export type NativeStageTexture = {
|
|
35
|
+
id: string
|
|
36
|
+
name: string
|
|
37
|
+
width: number
|
|
38
|
+
height: number
|
|
39
|
+
hdr: number
|
|
40
|
+
srgb: number
|
|
41
|
+
cube: number
|
|
42
|
+
wrap?: string
|
|
43
|
+
filter?: string
|
|
44
|
+
mips?: number
|
|
45
|
+
mipsExported?: number
|
|
46
|
+
aniso?: number
|
|
47
|
+
files: string[]
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export type NativeStageMaterial = {
|
|
51
|
+
id: string
|
|
52
|
+
name: string
|
|
53
|
+
shader: string
|
|
54
|
+
queue: number
|
|
55
|
+
passes: { lightMode: string; shader: string; state: PassState }[]
|
|
56
|
+
values: Record<string, NativeValue>
|
|
57
|
+
textures: Record<string, string>
|
|
58
|
+
/** An empty slot's stand-in, per the shader: white, black, grey, bump. */
|
|
59
|
+
defaults?: Record<string, string>
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export type NativeStageRenderer = {
|
|
63
|
+
id: string
|
|
64
|
+
path: string
|
|
65
|
+
mesh: string
|
|
66
|
+
materials: (string | null)[]
|
|
67
|
+
/** Object to world, game units, column-major. */
|
|
68
|
+
matrix: number[]
|
|
69
|
+
layers: number
|
|
70
|
+
castShadows: number
|
|
71
|
+
receiveShadows: number
|
|
72
|
+
values: Record<string, NativeValue>
|
|
73
|
+
textures: Record<string, string>
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export type NativeStageLights = {
|
|
77
|
+
main: {
|
|
78
|
+
direction: [number, number, number]
|
|
79
|
+
color: [number, number, number]
|
|
80
|
+
shadow: number
|
|
81
|
+
}
|
|
82
|
+
additional: {
|
|
83
|
+
position: [number, number, number]
|
|
84
|
+
range: number
|
|
85
|
+
color: [number, number, number]
|
|
86
|
+
type: "point" | "spot"
|
|
87
|
+
aim?: [number, number, number]
|
|
88
|
+
cosOuter?: number
|
|
89
|
+
cosInner?: number
|
|
90
|
+
unity?: {
|
|
91
|
+
spotDir: number[]
|
|
92
|
+
colorW: number
|
|
93
|
+
extra: number[]
|
|
94
|
+
worldToLight: number[] | null
|
|
95
|
+
lightType: number
|
|
96
|
+
simSpotDir?: number[] | null
|
|
97
|
+
simColorW?: number
|
|
98
|
+
}
|
|
99
|
+
}[]
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** stage.json, as stage_native.py writes it. */
|
|
103
|
+
export type NativeStagePackage = {
|
|
104
|
+
version: number
|
|
105
|
+
kind: "unity-native-stage"
|
|
106
|
+
name: string
|
|
107
|
+
source?: string
|
|
108
|
+
/** Engine units per game unit. */
|
|
109
|
+
scale: number
|
|
110
|
+
keywords: string[]
|
|
111
|
+
shaders: string[]
|
|
112
|
+
meshes: NativeStageMesh[]
|
|
113
|
+
textures: NativeStageTexture[]
|
|
114
|
+
materials: NativeStageMaterial[]
|
|
115
|
+
renderers: NativeStageRenderer[]
|
|
116
|
+
lights: NativeStageLights
|
|
117
|
+
settings: Record<string, NativeValue>
|
|
118
|
+
globalTextures: Record<string, string>
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** Reads one file of the package by its path inside it (stage.json's folder). */
|
|
122
|
+
export type NativeStageReader = (path: string) => Promise<ArrayBuffer>
|
|
123
|
+
|
|
124
|
+
type Tex = { texture: GPUTexture; view: GPUTextureView; sampler: GPUSampler }
|
|
125
|
+
|
|
126
|
+
type Item = {
|
|
127
|
+
renderer: { id: string; castShadows: number }
|
|
128
|
+
material: NativeStageMaterial
|
|
129
|
+
sub: number
|
|
130
|
+
mesh: { info: NativeStageMesh; buffer: GPUBuffer }
|
|
131
|
+
pass: NativeStageMaterial["passes"][number]
|
|
132
|
+
perDraw: ValueSource
|
|
133
|
+
textures: Record<string, GPUTextureView | undefined>
|
|
134
|
+
samplers: Record<string, GPUSampler | undefined>
|
|
135
|
+
/** World-space centre, engine units — for sorting. */
|
|
136
|
+
center: [number, number, number]
|
|
137
|
+
key: string
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const MAIN_MODES = ["ALWAYS", "FORWARDBASE"]
|
|
141
|
+
|
|
142
|
+
export class NativeStage {
|
|
143
|
+
readonly name: string
|
|
144
|
+
readonly pkg: NativeStagePackage
|
|
145
|
+
/** Engine-unit box around everything that casts, for the cascades. */
|
|
146
|
+
readonly bounds: {
|
|
147
|
+
min: [number, number, number]
|
|
148
|
+
max: [number, number, number]
|
|
149
|
+
}
|
|
150
|
+
private buffers: GPUBuffer[] = []
|
|
151
|
+
private textures: GPUTexture[] = []
|
|
152
|
+
private opaque: Item[] = []
|
|
153
|
+
private transparent: Item[] = []
|
|
154
|
+
private casters: Item[] = []
|
|
155
|
+
private zero: GPUBuffer
|
|
156
|
+
private white: GPUBuffer
|
|
157
|
+
|
|
158
|
+
private constructor(
|
|
159
|
+
private device: GPUDevice,
|
|
160
|
+
pkg: NativeStagePackage,
|
|
161
|
+
) {
|
|
162
|
+
this.name = pkg.name
|
|
163
|
+
this.pkg = pkg
|
|
164
|
+
this.bounds = {
|
|
165
|
+
min: [Infinity, Infinity, Infinity],
|
|
166
|
+
max: [-Infinity, -Infinity, -Infinity],
|
|
167
|
+
}
|
|
168
|
+
this.zero = device.createBuffer({
|
|
169
|
+
label: "native stage: zero stream",
|
|
170
|
+
size: 64,
|
|
171
|
+
usage: GPUBufferUsage.VERTEX,
|
|
172
|
+
})
|
|
173
|
+
this.white = device.createBuffer({
|
|
174
|
+
label: "native stage: white stream",
|
|
175
|
+
size: 64,
|
|
176
|
+
usage: GPUBufferUsage.VERTEX,
|
|
177
|
+
mappedAtCreation: true,
|
|
178
|
+
})
|
|
179
|
+
new Float32Array(this.white.getMappedRange()).fill(1)
|
|
180
|
+
this.white.unmap()
|
|
181
|
+
this.buffers.push(this.zero, this.white)
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Load a package: every mesh and texture it names through `read`, every
|
|
186
|
+
* shader registered with the hosts (the scene pass's and the shadow atlas's).
|
|
187
|
+
*/
|
|
188
|
+
static async load(
|
|
189
|
+
device: GPUDevice,
|
|
190
|
+
pkg: NativeStagePackage,
|
|
191
|
+
read: NativeStageReader,
|
|
192
|
+
hosts: { scene: NativeHost; shadow: NativeHost },
|
|
193
|
+
tools: {
|
|
194
|
+
mipmaps: (t: GPUTexture, levels: number) => void
|
|
195
|
+
fallback: Record<string, GPUTextureView>
|
|
196
|
+
},
|
|
197
|
+
onProgress?: (done: number, total: number) => void,
|
|
198
|
+
): Promise<NativeStage> {
|
|
199
|
+
if (pkg.kind !== "unity-native-stage") throw new Error("not a native stage package")
|
|
200
|
+
const st = new NativeStage(device, pkg)
|
|
201
|
+
// Progress by weight, not by file: a 2048² picture is most of a second to
|
|
202
|
+
// decode, a shader or a small mesh nothing — so pictures weigh their
|
|
203
|
+
// megapixels, everything else one unit of a quarter of one.
|
|
204
|
+
const texWeight = (t: NativeStageTexture) => Math.max(0.25, ((t.cube ? 6 : 1) * t.width * t.height) / (1 << 20))
|
|
205
|
+
const total = 0.25 * (pkg.shaders.length + pkg.meshes.length) + pkg.textures.reduce((s, t) => s + texWeight(t), 0)
|
|
206
|
+
let done = 0
|
|
207
|
+
const tick = (w = 0.25) => onProgress?.((done += w), total)
|
|
208
|
+
const text = async (p: string) => new TextDecoder().decode(await read(p))
|
|
209
|
+
|
|
210
|
+
const shaders = new Map<string, NativeShader>()
|
|
211
|
+
await Promise.all(
|
|
212
|
+
pkg.shaders.map(async (n) => {
|
|
213
|
+
const [vert, frag, info] = await Promise.all([
|
|
214
|
+
text(`shaders/${n}.vert.wgsl`),
|
|
215
|
+
text(`shaders/${n}.frag.wgsl`),
|
|
216
|
+
text(`shaders/${n}.json`),
|
|
217
|
+
])
|
|
218
|
+
shaders.set(n, { name: n, vert, frag, info: JSON.parse(info) })
|
|
219
|
+
tick()
|
|
220
|
+
}),
|
|
221
|
+
)
|
|
222
|
+
const casterShaders = new Set<string>()
|
|
223
|
+
for (const m of pkg.materials) for (const p of m.passes) if (p.lightMode === "SHADOWCASTER") casterShaders.add(p.shader)
|
|
224
|
+
for (const [n, s] of shaders) {
|
|
225
|
+
if (casterShaders.has(n)) hosts.shadow.register(s)
|
|
226
|
+
else hosts.scene.register(s)
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const meshes = new Map<string, { info: NativeStageMesh; buffer: GPUBuffer }>()
|
|
230
|
+
await Promise.all(
|
|
231
|
+
pkg.meshes.map(async (m) => {
|
|
232
|
+
const data = await read(`meshes/${m.id}.bin`)
|
|
233
|
+
const buffer = device.createBuffer({
|
|
234
|
+
label: `native stage mesh ${m.id}`,
|
|
235
|
+
size: Math.max(16, Math.ceil(data.byteLength / 4) * 4),
|
|
236
|
+
usage: GPUBufferUsage.VERTEX | GPUBufferUsage.INDEX | GPUBufferUsage.COPY_DST,
|
|
237
|
+
})
|
|
238
|
+
device.queue.writeBuffer(buffer, 0, data, 0, data.byteLength & ~3)
|
|
239
|
+
st.buffers.push(buffer)
|
|
240
|
+
meshes.set(m.id, { info: m, buffer })
|
|
241
|
+
tick()
|
|
242
|
+
}),
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
const textures = new Map<string, Tex>()
|
|
246
|
+
await Promise.all(
|
|
247
|
+
pkg.textures.map(async (t) => {
|
|
248
|
+
try {
|
|
249
|
+
textures.set(t.id, await st.loadTexture(t, read, tools.mipmaps))
|
|
250
|
+
} catch (e) {
|
|
251
|
+
console.warn(`native stage: texture ${t.id} ${t.name}:`, e)
|
|
252
|
+
}
|
|
253
|
+
tick(texWeight(t))
|
|
254
|
+
}),
|
|
255
|
+
)
|
|
256
|
+
|
|
257
|
+
st.build(meshes, textures, tools.fallback)
|
|
258
|
+
return st
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
private async loadTexture(
|
|
262
|
+
t: NativeStageTexture,
|
|
263
|
+
read: NativeStageReader,
|
|
264
|
+
mipmaps: (t: GPUTexture, levels: number) => void,
|
|
265
|
+
): Promise<Tex> {
|
|
266
|
+
const d = this.device
|
|
267
|
+
const full = Math.max(1, Math.floor(Math.log2(Math.max(t.width, t.height))) + 1)
|
|
268
|
+
const format: GPUTextureFormat = t.hdr ? "rgba16float" : t.srgb ? "rgba8unorm-srgb" : "rgba8unorm"
|
|
269
|
+
if (t.cube) {
|
|
270
|
+
// Every face and mip as recorded: face-major <id>_f<face>_m<mip>.bin, RGBA16F
|
|
271
|
+
// rows along WebGPU's face directions (AGDlcRecord's AGCubeFace).
|
|
272
|
+
const mips = t.mipsExported || 1
|
|
273
|
+
const texture = d.createTexture({
|
|
274
|
+
label: `native stage ${t.name}`,
|
|
275
|
+
size: [t.width, t.height, 6],
|
|
276
|
+
format,
|
|
277
|
+
mipLevelCount: mips,
|
|
278
|
+
usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST,
|
|
279
|
+
})
|
|
280
|
+
await Promise.all(
|
|
281
|
+
t.files.map(async (file, k) => {
|
|
282
|
+
const face = Math.floor(k / mips)
|
|
283
|
+
const mip = k % mips
|
|
284
|
+
const s = Math.max(1, t.width >> mip)
|
|
285
|
+
const data = await read(`textures/${file}`)
|
|
286
|
+
d.queue.writeTexture({ texture, origin: [0, 0, face], mipLevel: mip }, data, { bytesPerRow: s * 8, rowsPerImage: s }, [s, s, 1])
|
|
287
|
+
}),
|
|
288
|
+
)
|
|
289
|
+
this.textures.push(texture)
|
|
290
|
+
const sampler = d.createSampler({
|
|
291
|
+
magFilter: "linear",
|
|
292
|
+
minFilter: "linear",
|
|
293
|
+
mipmapFilter: "linear",
|
|
294
|
+
})
|
|
295
|
+
return {
|
|
296
|
+
texture,
|
|
297
|
+
view: texture.createView({ dimension: "cube" }),
|
|
298
|
+
sampler,
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
const mips = t.hdr ? 1 : full
|
|
302
|
+
const texture = d.createTexture({
|
|
303
|
+
label: `native stage ${t.name}`,
|
|
304
|
+
size: [t.width, t.height],
|
|
305
|
+
format,
|
|
306
|
+
mipLevelCount: mips,
|
|
307
|
+
usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST | GPUTextureUsage.RENDER_ATTACHMENT,
|
|
308
|
+
})
|
|
309
|
+
const data = await read(`textures/${t.files[0]}`)
|
|
310
|
+
if (t.hdr) {
|
|
311
|
+
// Raw RGBA16F rows as Unity holds them (row 0 is v 0): as they are.
|
|
312
|
+
d.queue.writeTexture({ texture }, data, { bytesPerRow: t.width * 8 }, [t.width, t.height])
|
|
313
|
+
} else {
|
|
314
|
+
// A PNG runs top down; Unity's row 0 is the bottom (v 0): flipped on the way in.
|
|
315
|
+
const bmp = await createImageBitmap(new Blob([data]), {
|
|
316
|
+
imageOrientation: "flipY",
|
|
317
|
+
colorSpaceConversion: "none",
|
|
318
|
+
premultiplyAlpha: "none",
|
|
319
|
+
// A picture swapped into the folder need not be the size it replaced.
|
|
320
|
+
resizeWidth: t.width,
|
|
321
|
+
resizeHeight: t.height,
|
|
322
|
+
resizeQuality: "high",
|
|
323
|
+
})
|
|
324
|
+
d.queue.copyExternalImageToTexture({ source: bmp }, { texture }, [t.width, t.height])
|
|
325
|
+
bmp.close()
|
|
326
|
+
if (mips > 1) mipmaps(texture, mips)
|
|
327
|
+
}
|
|
328
|
+
this.textures.push(texture)
|
|
329
|
+
const wrap: GPUAddressMode =
|
|
330
|
+
(
|
|
331
|
+
{
|
|
332
|
+
Repeat: "repeat",
|
|
333
|
+
Clamp: "clamp-to-edge",
|
|
334
|
+
Mirror: "mirror-repeat",
|
|
335
|
+
MirrorOnce: "mirror-repeat",
|
|
336
|
+
} as Record<string, GPUAddressMode>
|
|
337
|
+
)[t.wrap ?? "Repeat"] ?? "repeat"
|
|
338
|
+
const filter: GPUFilterMode = t.filter === "Point" ? "nearest" : "linear"
|
|
339
|
+
const trilinear = t.filter === "Trilinear"
|
|
340
|
+
const sampler = d.createSampler({
|
|
341
|
+
addressModeU: wrap,
|
|
342
|
+
addressModeV: wrap,
|
|
343
|
+
magFilter: filter,
|
|
344
|
+
minFilter: filter,
|
|
345
|
+
mipmapFilter: trilinear ? "linear" : "nearest",
|
|
346
|
+
maxAnisotropy: filter === "linear" && trilinear ? Math.min(16, Math.max(1, t.aniso || 1)) : 1,
|
|
347
|
+
})
|
|
348
|
+
return { texture, view: texture.createView(), sampler }
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/** Every renderer's draws, resolved once: the stage does not move. */
|
|
352
|
+
private build(
|
|
353
|
+
meshes: Map<string, { info: NativeStageMesh; buffer: GPUBuffer }>,
|
|
354
|
+
textures: Map<string, Tex>,
|
|
355
|
+
fallback: Record<string, GPUTextureView>,
|
|
356
|
+
): void {
|
|
357
|
+
const pkg = this.pkg
|
|
358
|
+
const s = pkg.scale
|
|
359
|
+
const mats = new Map(pkg.materials.map((m) => [m.id, m]))
|
|
360
|
+
const globalViews: Record<string, GPUTextureView | undefined> = {}
|
|
361
|
+
const globalSamplers: Record<string, GPUSampler | undefined> = {}
|
|
362
|
+
for (const [slot, id] of Object.entries(pkg.globalTextures)) {
|
|
363
|
+
const t = textures.get(id)
|
|
364
|
+
if (t) {
|
|
365
|
+
globalViews[slot] = t.view
|
|
366
|
+
globalSamplers[slot] = t.sampler
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
for (const r of pkg.renderers) {
|
|
370
|
+
const mesh = meshes.get(r.mesh)
|
|
371
|
+
if (!mesh) continue
|
|
372
|
+
const m = r.matrix
|
|
373
|
+
// centre and corners of the mesh box, to engine units
|
|
374
|
+
const b = mesh.info.bounds
|
|
375
|
+
const cx = b.center[0],
|
|
376
|
+
cy = b.center[1],
|
|
377
|
+
cz = b.center[2]
|
|
378
|
+
const center: [number, number, number] = [
|
|
379
|
+
-(m[0] * cx + m[4] * cy + m[8] * cz + m[12]) * s,
|
|
380
|
+
(m[1] * cx + m[5] * cy + m[9] * cz + m[13]) * s,
|
|
381
|
+
-(m[2] * cx + m[6] * cy + m[10] * cz + m[14]) * s,
|
|
382
|
+
]
|
|
383
|
+
const ex = b.size[0] / 2,
|
|
384
|
+
ey = b.size[1] / 2,
|
|
385
|
+
ez = b.size[2] / 2
|
|
386
|
+
const half = [0, 1, 2].map((i) => (Math.abs(m[i]) * ex + Math.abs(m[4 + i]) * ey + Math.abs(m[8 + i]) * ez) * s)
|
|
387
|
+
const world = Float32Array.from(m)
|
|
388
|
+
const det = m[0] * (m[5] * m[10] - m[9] * m[6]) - m[4] * (m[1] * m[10] - m[9] * m[2]) + m[8] * (m[1] * m[6] - m[5] * m[2])
|
|
389
|
+
const perDraw: ValueSource = {
|
|
390
|
+
unity_ObjectToWorld: world,
|
|
391
|
+
unity_WorldToObject: invert4(world),
|
|
392
|
+
unity_WorldTransformParams: [0, 0, 0, det < 0 ? -1 : 1],
|
|
393
|
+
unity_LightData: [0, 0, 0, 0],
|
|
394
|
+
unity_LightIndices: [
|
|
395
|
+
[0, 0, 0, 0],
|
|
396
|
+
[0, 0, 0, 0],
|
|
397
|
+
],
|
|
398
|
+
unity_RenderingLayer: new Uint32Array([r.layers >>> 0, 0, 0, 0]),
|
|
399
|
+
unity_LODFade: [1, 1, 0, 0],
|
|
400
|
+
...r.values,
|
|
401
|
+
}
|
|
402
|
+
this.addItems(r, mesh, perDraw, center, mats, globalViews, globalSamplers, textures, fallback)
|
|
403
|
+
if (r.castShadows) {
|
|
404
|
+
for (let i = 0; i < 3; i++) {
|
|
405
|
+
this.bounds.min[i] = Math.min(this.bounds.min[i], center[i] - half[i])
|
|
406
|
+
this.bounds.max[i] = Math.max(this.bounds.max[i], center[i] + half[i])
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
private addItems(
|
|
413
|
+
r: { id: string; materials: (string | null)[]; castShadows: number; textures: Record<string, string> },
|
|
414
|
+
mesh: { info: NativeStageMesh; buffer: GPUBuffer },
|
|
415
|
+
perDraw: ValueSource,
|
|
416
|
+
center: [number, number, number],
|
|
417
|
+
mats: Map<string, NativeStageMaterial>,
|
|
418
|
+
globalViews: Record<string, GPUTextureView | undefined>,
|
|
419
|
+
globalSamplers: Record<string, GPUSampler | undefined>,
|
|
420
|
+
textures: Map<string, Tex>,
|
|
421
|
+
fallback: Record<string, GPUTextureView>,
|
|
422
|
+
): void {
|
|
423
|
+
r.materials.forEach((mid, sub) => {
|
|
424
|
+
const mat = mid ? mats.get(mid) : undefined
|
|
425
|
+
if (!mat || sub >= mesh.info.submeshes.length) return
|
|
426
|
+
const views: Record<string, GPUTextureView | undefined> = {
|
|
427
|
+
...globalViews,
|
|
428
|
+
}
|
|
429
|
+
const samplers: Record<string, GPUSampler | undefined> = {
|
|
430
|
+
...globalSamplers,
|
|
431
|
+
}
|
|
432
|
+
// an empty slot reads what the shader declared it defaults to
|
|
433
|
+
for (const [slot, def] of Object.entries(mat.defaults ?? {})) views[slot] = fallback[def] ?? fallback.white
|
|
434
|
+
for (const [slot, id] of [...Object.entries(mat.textures), ...Object.entries(r.textures)]) {
|
|
435
|
+
const t = textures.get(id)
|
|
436
|
+
if (!t) continue
|
|
437
|
+
views[slot] = t.view
|
|
438
|
+
samplers[slot] = t.sampler
|
|
439
|
+
}
|
|
440
|
+
for (const pass of mat.passes) {
|
|
441
|
+
const item: Item = {
|
|
442
|
+
renderer: r,
|
|
443
|
+
material: mat,
|
|
444
|
+
sub,
|
|
445
|
+
mesh,
|
|
446
|
+
pass,
|
|
447
|
+
perDraw,
|
|
448
|
+
textures: views,
|
|
449
|
+
samplers,
|
|
450
|
+
center,
|
|
451
|
+
key: `stage|${r.id}|${sub}|${pass.lightMode}|${pass.shader}`,
|
|
452
|
+
}
|
|
453
|
+
if (pass.lightMode === "SHADOWCASTER") {
|
|
454
|
+
if (r.castShadows) this.casters.push(item)
|
|
455
|
+
} else if (MAIN_MODES.includes(pass.lightMode)) {
|
|
456
|
+
;(mat.queue > 2500 ? this.transparent : this.opaque).push(item)
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
})
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
private streams(host: NativeHost, it: Item): NativeStream[] {
|
|
463
|
+
const info = it.mesh.info
|
|
464
|
+
return host.inputs(it.pass.shader).map(({ semantic }) => {
|
|
465
|
+
const st = info.streams[semantic]
|
|
466
|
+
if (!st) {
|
|
467
|
+
// A missing vertex colour reads white, as Unity binds it; anything else zero.
|
|
468
|
+
return {
|
|
469
|
+
buffer: semantic === "COLOR" ? this.white : this.zero,
|
|
470
|
+
offset: 0,
|
|
471
|
+
stride: 0,
|
|
472
|
+
format: "float32x4" as const,
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
const n = st[1]
|
|
476
|
+
return {
|
|
477
|
+
buffer: it.mesh.buffer,
|
|
478
|
+
offset: st[0],
|
|
479
|
+
stride: n * 4,
|
|
480
|
+
format: (n === 1 ? "float32" : `float32x${n}`) as GPUVertexFormat,
|
|
481
|
+
}
|
|
482
|
+
})
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
private draw(
|
|
486
|
+
pass: GPURenderPassEncoder,
|
|
487
|
+
host: NativeHost,
|
|
488
|
+
it: Item,
|
|
489
|
+
globals: Record<string, NativeValue>,
|
|
490
|
+
frame: Record<string, GPUTextureView | undefined>,
|
|
491
|
+
keySuffix = "",
|
|
492
|
+
): void {
|
|
493
|
+
const sm = it.mesh.info.submeshes[it.sub]
|
|
494
|
+
host.draw(pass, {
|
|
495
|
+
shader: it.pass.shader,
|
|
496
|
+
state: it.pass.state,
|
|
497
|
+
streams: this.streams(host, it),
|
|
498
|
+
index: { buffer: it.mesh.buffer, format: "uint32", offset: sm.offset, count: sm.count },
|
|
499
|
+
sources: [it.perDraw, it.material.values, globals],
|
|
500
|
+
textures: { ...it.textures, ...frame },
|
|
501
|
+
samplers: it.samplers,
|
|
502
|
+
key: it.key + keySuffix,
|
|
503
|
+
pass: keySuffix || "main",
|
|
504
|
+
})
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
/** One cascade tile of the engine's atlas: every caster's SHADOWCASTER pass. */
|
|
508
|
+
drawShadow(
|
|
509
|
+
pass: GPURenderPassEncoder,
|
|
510
|
+
host: NativeHost,
|
|
511
|
+
globals: Record<string, NativeValue>,
|
|
512
|
+
cascade: number,
|
|
513
|
+
viewProj: ArrayLike<number>,
|
|
514
|
+
engineScale: number,
|
|
515
|
+
): void {
|
|
516
|
+
const vp = flipY(mul4(viewProj, gameToEngine(engineScale)))
|
|
517
|
+
// ShadowUtils.GetShadowBias as the game set it: half a texel along the light
|
|
518
|
+
// and half a texel in along the normal, a texel being this tile's width in
|
|
519
|
+
// game units over its resolution (2048).
|
|
520
|
+
const rowLen = Math.hypot(vp[0], vp[4], vp[8])
|
|
521
|
+
const texel = rowLen > 0 ? 2 / rowLen / 2048 : 0
|
|
522
|
+
const g: Record<string, NativeValue> = {
|
|
523
|
+
...globals,
|
|
524
|
+
unity_MatrixVP: vp,
|
|
525
|
+
unity_MatrixV: scale4(1),
|
|
526
|
+
unity_MatrixP: vp,
|
|
527
|
+
glstate_matrix_projection: vp,
|
|
528
|
+
sim_ShadowBias: [0.5 * texel, -0.5 * texel, 0, 0],
|
|
529
|
+
}
|
|
530
|
+
for (const it of this.casters) this.draw(pass, host, it, g, {}, `|c${cascade}`)
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
/** The opaque queue, front to back within each queue. */
|
|
534
|
+
drawOpaque(
|
|
535
|
+
pass: GPURenderPassEncoder,
|
|
536
|
+
host: NativeHost,
|
|
537
|
+
globals: Record<string, NativeValue>,
|
|
538
|
+
frame: Record<string, GPUTextureView | undefined>,
|
|
539
|
+
eye: [number, number, number],
|
|
540
|
+
): void {
|
|
541
|
+
const d = (it: Item) => (it.center[0] - eye[0]) ** 2 + (it.center[1] - eye[1]) ** 2 + (it.center[2] - eye[2]) ** 2
|
|
542
|
+
const list = this.opaque.slice().sort((a, b) => a.material.queue - b.material.queue || d(a) - d(b))
|
|
543
|
+
for (const it of list) this.draw(pass, host, it, globals, frame)
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
/** The transparent queue, back to front within each queue. */
|
|
547
|
+
drawTransparent(
|
|
548
|
+
pass: GPURenderPassEncoder,
|
|
549
|
+
host: NativeHost,
|
|
550
|
+
globals: Record<string, NativeValue>,
|
|
551
|
+
frame: Record<string, GPUTextureView | undefined>,
|
|
552
|
+
eye: [number, number, number],
|
|
553
|
+
): void {
|
|
554
|
+
const d = (it: Item) => (it.center[0] - eye[0]) ** 2 + (it.center[1] - eye[1]) ** 2 + (it.center[2] - eye[2]) ** 2
|
|
555
|
+
const list = this.transparent.slice().sort((a, b) => a.material.queue - b.material.queue || d(b) - d(a))
|
|
556
|
+
for (const it of list) this.draw(pass, host, it, globals, frame)
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
destroy(hosts: NativeHost[]): void {
|
|
560
|
+
for (const h of hosts) h.release("stage|")
|
|
561
|
+
const bufs = this.buffers
|
|
562
|
+
const texs = this.textures
|
|
563
|
+
void this.device.queue.onSubmittedWorkDone().then(() => {
|
|
564
|
+
for (const b of bufs) b.destroy()
|
|
565
|
+
for (const t of texs) t.destroy()
|
|
566
|
+
})
|
|
567
|
+
}
|
|
568
|
+
}
|