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,372 @@
|
|
|
1
|
+
// Native looks: a model dressed in the game's own materials.
|
|
2
|
+
//
|
|
3
|
+
// A look names translated shader variants (host.ts), the shared images they
|
|
4
|
+
// sample (ramps, matcaps), and per material the passes it draws and the values
|
|
5
|
+
// it carries — what a game character's material held, with the model's own
|
|
6
|
+
// texture standing in for its _MainTex. Optionally a CharacterRig, which makes
|
|
7
|
+
// the model a character in the game's sense (character.ts).
|
|
8
|
+
//
|
|
9
|
+
// The frame, as the game's CharacterFeature lays it out after the opaques:
|
|
10
|
+
// 1. every native opaque material's passes (Outline, ForwardBase), in queue
|
|
11
|
+
// order — after the engine's own opaque bundle, so both share one depth;
|
|
12
|
+
// 2. CharHairShadow, CharFaceShadow, CharHairTransE, each over every
|
|
13
|
+
// material that has one (the hair's shadow on the face, by stencil);
|
|
14
|
+
// 3. the eyes' see-through-hair passes, Override1 for all, then 2, then 3;
|
|
15
|
+
// and the transparent queue in the engine's transparent phase. Shadows are the
|
|
16
|
+
// engine's: a native material still casts through its pass.
|
|
17
|
+
|
|
18
|
+
import { NativeHost, type NativeShader, type NativeStream, type NativeValue, type ValueSource } from "./host"
|
|
19
|
+
import type { PassState } from "./state"
|
|
20
|
+
import { UNITY_SKIN_WGSL, tangentsFor } from "./skin"
|
|
21
|
+
import { characterBlock, type CharacterRig } from "./character"
|
|
22
|
+
import { engineToGame, gameToEngine } from "./globals"
|
|
23
|
+
|
|
24
|
+
export type NativeTexture = {
|
|
25
|
+
width: number
|
|
26
|
+
height: number
|
|
27
|
+
/** RGBA8, rows top first. */
|
|
28
|
+
data: Uint8Array
|
|
29
|
+
/** Colour (sRGB) or data (linear) — how the shader expects to read it. */
|
|
30
|
+
srgb: boolean
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export type NativeMaterialSpec = {
|
|
34
|
+
/** The model's materials this dresses, by name. */
|
|
35
|
+
materials: string[]
|
|
36
|
+
/** Unity render queue: above 2500 draws in the transparent phase. */
|
|
37
|
+
queue: number
|
|
38
|
+
passes: { lightMode: string; shader: string; state: PassState }[]
|
|
39
|
+
/** Material properties, by name (colours linear, as Unity uploads them). */
|
|
40
|
+
values: Record<string, NativeValue>
|
|
41
|
+
/** Texture slots: a key into the look's images, or "@diffuse" for the
|
|
42
|
+
* material's own texture. */
|
|
43
|
+
textures: Record<string, string>
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export type NativeLook = {
|
|
47
|
+
shaders: NativeShader[]
|
|
48
|
+
textures: Record<string, NativeTexture>
|
|
49
|
+
materials: NativeMaterialSpec[]
|
|
50
|
+
rig?: CharacterRig
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** What the engine lends for one model. */
|
|
54
|
+
export type NativeModel = {
|
|
55
|
+
name: string
|
|
56
|
+
vertices: Float32Array
|
|
57
|
+
indices: Uint32Array
|
|
58
|
+
vertexBuffer: GPUBuffer
|
|
59
|
+
jointsBuffer: GPUBuffer
|
|
60
|
+
weightsBuffer: GPUBuffer
|
|
61
|
+
skinMatrixBuffer: GPUBuffer
|
|
62
|
+
indexBuffer: GPUBuffer
|
|
63
|
+
draws: {
|
|
64
|
+
materialName: string
|
|
65
|
+
firstIndex: number
|
|
66
|
+
count: number
|
|
67
|
+
diffuse: GPUTextureView
|
|
68
|
+
}[]
|
|
69
|
+
hidden: (material: string) => boolean
|
|
70
|
+
/** The head bone's current skinning matrix, or null. */
|
|
71
|
+
headSkin: () => ArrayLike<number> | null
|
|
72
|
+
headRest: [number, number, number]
|
|
73
|
+
/** Its rendering-layer bits, for a look without a rig. */
|
|
74
|
+
layers: number
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
type Install = {
|
|
78
|
+
model: NativeModel
|
|
79
|
+
look: NativeLook
|
|
80
|
+
views: Map<string, GPUTextureView>
|
|
81
|
+
textures: GPUTexture[]
|
|
82
|
+
pos: GPUBuffer
|
|
83
|
+
nrm: GPUBuffer
|
|
84
|
+
tan: GPUBuffer
|
|
85
|
+
restTan: GPUBuffer
|
|
86
|
+
params: GPUBuffer
|
|
87
|
+
skinBind: GPUBindGroup
|
|
88
|
+
/** Per draw of the model: the spec that dresses it, if any. */
|
|
89
|
+
dressed: { draw: NativeModel["draws"][number]; spec: NativeMaterialSpec }[]
|
|
90
|
+
block: Record<string, NativeValue>
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const OPAQUE_MODES = ["ALWAYS", "FORWARDBASE"]
|
|
94
|
+
const CHARACTER_MODES = ["CHARHAIRSHADOW", "CHARFACESHADOW", "CHARHAIRTRANSE"]
|
|
95
|
+
const OVERRIDE_MODES = ["OVERRIDE1", "OVERRIDE2", "OVERRIDE3"]
|
|
96
|
+
|
|
97
|
+
export class NativeLooks {
|
|
98
|
+
readonly host: NativeHost
|
|
99
|
+
private installs = new Map<string, Install>()
|
|
100
|
+
private skinPipeline: GPUComputePipeline
|
|
101
|
+
private zero: GPUBuffer
|
|
102
|
+
private white: GPUBuffer
|
|
103
|
+
private globals: Record<string, NativeValue> = {}
|
|
104
|
+
private frameTextures: Record<string, GPUTextureView | undefined> = {}
|
|
105
|
+
private scale = 8
|
|
106
|
+
|
|
107
|
+
constructor(
|
|
108
|
+
private device: GPUDevice,
|
|
109
|
+
host: NativeHost,
|
|
110
|
+
) {
|
|
111
|
+
this.host = host
|
|
112
|
+
this.skinPipeline = device.createComputePipeline({
|
|
113
|
+
label: "native skinning",
|
|
114
|
+
layout: "auto",
|
|
115
|
+
compute: {
|
|
116
|
+
module: device.createShaderModule({
|
|
117
|
+
label: "native skinning",
|
|
118
|
+
code: UNITY_SKIN_WGSL,
|
|
119
|
+
}),
|
|
120
|
+
entryPoint: "main",
|
|
121
|
+
},
|
|
122
|
+
})
|
|
123
|
+
this.zero = device.createBuffer({
|
|
124
|
+
label: "native zero stream",
|
|
125
|
+
size: 64,
|
|
126
|
+
usage: GPUBufferUsage.VERTEX,
|
|
127
|
+
})
|
|
128
|
+
this.white = device.createBuffer({
|
|
129
|
+
label: "native white stream",
|
|
130
|
+
size: 64,
|
|
131
|
+
usage: GPUBufferUsage.VERTEX,
|
|
132
|
+
mappedAtCreation: true,
|
|
133
|
+
})
|
|
134
|
+
new Float32Array(this.white.getMappedRange()).fill(1)
|
|
135
|
+
this.white.unmap()
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
get size(): number {
|
|
139
|
+
return this.installs.size
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
has(model: string): boolean {
|
|
143
|
+
return this.installs.has(model)
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** Is this model's material drawn by its native look? */
|
|
147
|
+
dresses(model: string, material: string): boolean {
|
|
148
|
+
const i = this.installs.get(model)
|
|
149
|
+
return !!i && i.dressed.some((d) => d.draw.materialName === material)
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
install(model: NativeModel, look: NativeLook): void {
|
|
153
|
+
this.remove(model.name)
|
|
154
|
+
const d = this.device
|
|
155
|
+
for (const s of look.shaders) this.host.register(s)
|
|
156
|
+
const views = new Map<string, GPUTextureView>()
|
|
157
|
+
const textures: GPUTexture[] = []
|
|
158
|
+
for (const [key, t] of Object.entries(look.textures)) {
|
|
159
|
+
const tex = d.createTexture({
|
|
160
|
+
label: `${model.name}: ${key}`,
|
|
161
|
+
size: [t.width, t.height],
|
|
162
|
+
format: t.srgb ? "rgba8unorm-srgb" : "rgba8unorm",
|
|
163
|
+
usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST,
|
|
164
|
+
})
|
|
165
|
+
d.queue.writeTexture({ texture: tex }, t.data as Uint8Array<ArrayBuffer>, { bytesPerRow: t.width * 4 }, [t.width, t.height])
|
|
166
|
+
textures.push(tex)
|
|
167
|
+
views.set(key, tex.createView())
|
|
168
|
+
}
|
|
169
|
+
const n = model.vertices.length / 8
|
|
170
|
+
const buf = (label: string, floats: number, usage: number) =>
|
|
171
|
+
d.createBuffer({
|
|
172
|
+
label: `${model.name}: ${label}`,
|
|
173
|
+
size: Math.max(16, floats * 4),
|
|
174
|
+
usage,
|
|
175
|
+
})
|
|
176
|
+
const out = GPUBufferUsage.STORAGE | GPUBufferUsage.VERTEX
|
|
177
|
+
const pos = buf("native position", n * 3, out)
|
|
178
|
+
const nrm = buf("native normal", n * 3, out)
|
|
179
|
+
const tan = buf("native tangent", n * 4, out)
|
|
180
|
+
const restTan = buf("rest tangent", n * 4, GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST)
|
|
181
|
+
d.queue.writeBuffer(restTan, 0, tangentsFor(model.vertices, model.indices) as Float32Array<ArrayBuffer>)
|
|
182
|
+
const params = d.createBuffer({
|
|
183
|
+
label: `${model.name}: skin params`,
|
|
184
|
+
size: 16,
|
|
185
|
+
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
|
|
186
|
+
})
|
|
187
|
+
d.queue.writeBuffer(params, 0, new Uint32Array([n, 0, 0, 0]))
|
|
188
|
+
const skinBind = d.createBindGroup({
|
|
189
|
+
label: `${model.name}: native skinning`,
|
|
190
|
+
layout: this.skinPipeline.getBindGroupLayout(0),
|
|
191
|
+
entries: [
|
|
192
|
+
{ binding: 0, resource: { buffer: params } },
|
|
193
|
+
{ binding: 1, resource: { buffer: model.vertexBuffer } },
|
|
194
|
+
{ binding: 2, resource: { buffer: model.jointsBuffer } },
|
|
195
|
+
{ binding: 3, resource: { buffer: model.weightsBuffer } },
|
|
196
|
+
{ binding: 4, resource: { buffer: model.skinMatrixBuffer } },
|
|
197
|
+
{ binding: 5, resource: { buffer: restTan } },
|
|
198
|
+
{ binding: 6, resource: { buffer: pos } },
|
|
199
|
+
{ binding: 7, resource: { buffer: nrm } },
|
|
200
|
+
{ binding: 8, resource: { buffer: tan } },
|
|
201
|
+
],
|
|
202
|
+
})
|
|
203
|
+
const dressed: Install["dressed"] = []
|
|
204
|
+
for (const draw of model.draws) {
|
|
205
|
+
const spec = look.materials.find((m) => m.materials.includes(draw.materialName))
|
|
206
|
+
if (spec) dressed.push({ draw, spec })
|
|
207
|
+
}
|
|
208
|
+
this.installs.set(model.name, {
|
|
209
|
+
model,
|
|
210
|
+
look,
|
|
211
|
+
views,
|
|
212
|
+
textures,
|
|
213
|
+
pos,
|
|
214
|
+
nrm,
|
|
215
|
+
tan,
|
|
216
|
+
restTan,
|
|
217
|
+
params,
|
|
218
|
+
skinBind,
|
|
219
|
+
dressed,
|
|
220
|
+
block: {},
|
|
221
|
+
})
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
remove(name: string): void {
|
|
225
|
+
const i = this.installs.get(name)
|
|
226
|
+
if (!i) return
|
|
227
|
+
this.installs.delete(name)
|
|
228
|
+
this.host.release(`${name}|`)
|
|
229
|
+
const bufs = [i.pos, i.nrm, i.tan, i.restTan, i.params]
|
|
230
|
+
const texs = i.textures
|
|
231
|
+
// Freed once the GPU is done with the frames that may still name them.
|
|
232
|
+
void this.device.queue.onSubmittedWorkDone().then(() => {
|
|
233
|
+
for (const b of bufs) b.destroy()
|
|
234
|
+
for (const t of texs) t.destroy()
|
|
235
|
+
})
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Before the scene pass: skin every dressed model and compute each
|
|
240
|
+
* character's block; the frame's Unity globals come computed (engine.ts).
|
|
241
|
+
*/
|
|
242
|
+
prepare(
|
|
243
|
+
encoder: GPUCommandEncoder,
|
|
244
|
+
globals: Record<string, NativeValue>,
|
|
245
|
+
scale: number,
|
|
246
|
+
textures: Record<string, GPUTextureView | undefined>,
|
|
247
|
+
): void {
|
|
248
|
+
if (!this.installs.size) return
|
|
249
|
+
this.scale = scale
|
|
250
|
+
this.globals = globals
|
|
251
|
+
this.frameTextures = textures
|
|
252
|
+
const pass = encoder.beginComputePass({ label: "native skinning" })
|
|
253
|
+
pass.setPipeline(this.skinPipeline)
|
|
254
|
+
for (const i of this.installs.values()) {
|
|
255
|
+
pass.setBindGroup(0, i.skinBind)
|
|
256
|
+
pass.dispatchWorkgroups(Math.ceil(i.model.vertices.length / 8 / 64))
|
|
257
|
+
i.block = i.look.rig
|
|
258
|
+
? characterBlock(i.look.rig, i.model.headSkin(), i.model.headRest, scale)
|
|
259
|
+
: {
|
|
260
|
+
unity_RenderingLayer: new Uint32Array([i.model.layers >>> 0, 0, 0, 0]),
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
pass.end()
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
private streams(i: Install, shader: string): NativeStream[] {
|
|
267
|
+
return this.host.inputs(shader).map(({ semantic }) => {
|
|
268
|
+
if (semantic === "POSITION")
|
|
269
|
+
return {
|
|
270
|
+
buffer: i.pos,
|
|
271
|
+
offset: 0,
|
|
272
|
+
stride: 12,
|
|
273
|
+
format: "float32x3" as const,
|
|
274
|
+
}
|
|
275
|
+
if (semantic === "NORMAL")
|
|
276
|
+
return {
|
|
277
|
+
buffer: i.nrm,
|
|
278
|
+
offset: 0,
|
|
279
|
+
stride: 12,
|
|
280
|
+
format: "float32x3" as const,
|
|
281
|
+
}
|
|
282
|
+
if (semantic === "TANGENT")
|
|
283
|
+
return {
|
|
284
|
+
buffer: i.tan,
|
|
285
|
+
offset: 0,
|
|
286
|
+
stride: 16,
|
|
287
|
+
format: "float32x4" as const,
|
|
288
|
+
}
|
|
289
|
+
if (semantic === "TEXCOORD0")
|
|
290
|
+
return {
|
|
291
|
+
buffer: i.model.vertexBuffer,
|
|
292
|
+
offset: 24,
|
|
293
|
+
stride: 32,
|
|
294
|
+
format: "float32x2" as const,
|
|
295
|
+
}
|
|
296
|
+
// A missing vertex colour reads white, as Unity binds it; any other
|
|
297
|
+
// missing stream reads zero.
|
|
298
|
+
if (semantic === "COLOR")
|
|
299
|
+
return {
|
|
300
|
+
buffer: this.white,
|
|
301
|
+
offset: 0,
|
|
302
|
+
stride: 0,
|
|
303
|
+
format: "float32x4" as const,
|
|
304
|
+
}
|
|
305
|
+
return {
|
|
306
|
+
buffer: this.zero,
|
|
307
|
+
offset: 0,
|
|
308
|
+
stride: 0,
|
|
309
|
+
format: "float32x4" as const,
|
|
310
|
+
}
|
|
311
|
+
})
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
private drawPass(pass: GPURenderPassEncoder, i: Install, d: Install["dressed"][number], p: NativeMaterialSpec["passes"][number]): void {
|
|
315
|
+
if (i.model.hidden(d.draw.materialName)) return
|
|
316
|
+
const s = this.scale
|
|
317
|
+
const perDraw: ValueSource = {
|
|
318
|
+
// The skinned streams are in engine units; the game's world is 1/scale of them.
|
|
319
|
+
unity_ObjectToWorld: engineToGame(s),
|
|
320
|
+
unity_WorldToObject: gameToEngine(s),
|
|
321
|
+
unity_WorldTransformParams: [0, 0, 0, 1],
|
|
322
|
+
unity_LightData: [0, 0, 0, 0],
|
|
323
|
+
unity_LightIndices: [
|
|
324
|
+
[0, 0, 0, 0],
|
|
325
|
+
[0, 0, 0, 0],
|
|
326
|
+
],
|
|
327
|
+
unity_LODFade: [1, 1, 0, 0],
|
|
328
|
+
}
|
|
329
|
+
const textures: Record<string, GPUTextureView | undefined> = {
|
|
330
|
+
...this.frameTextures,
|
|
331
|
+
}
|
|
332
|
+
for (const [slot, key] of Object.entries(d.spec.textures)) textures[slot] = key === "@diffuse" ? d.draw.diffuse : i.views.get(key)
|
|
333
|
+
this.host.draw(pass, {
|
|
334
|
+
shader: p.shader,
|
|
335
|
+
state: p.state,
|
|
336
|
+
streams: this.streams(i, p.shader),
|
|
337
|
+
index: {
|
|
338
|
+
buffer: i.model.indexBuffer,
|
|
339
|
+
format: "uint32",
|
|
340
|
+
offset: d.draw.firstIndex * 4,
|
|
341
|
+
count: d.draw.count,
|
|
342
|
+
},
|
|
343
|
+
sources: [perDraw, i.block, d.spec.values, this.globals],
|
|
344
|
+
textures,
|
|
345
|
+
key: `${i.model.name}|${d.draw.materialName}|${p.lightMode}|${p.shader}`,
|
|
346
|
+
pass: "main",
|
|
347
|
+
})
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/** Every dressed material whose queue is in range, in queue order. */
|
|
351
|
+
private each(opaque: boolean): { i: Install; d: Install["dressed"][number] }[] {
|
|
352
|
+
const out: { i: Install; d: Install["dressed"][number] }[] = []
|
|
353
|
+
for (const i of this.installs.values()) for (const d of i.dressed) if (d.spec.queue <= 2500 === opaque) out.push({ i, d })
|
|
354
|
+
return out.sort((a, b) => a.d.spec.queue - b.d.spec.queue)
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/** After the engine's opaque phase: the opaques, then the character passes. */
|
|
358
|
+
drawOpaque(pass: GPURenderPassEncoder): void {
|
|
359
|
+
if (!this.installs.size) return
|
|
360
|
+
const list = this.each(true)
|
|
361
|
+
for (const { i, d } of list) for (const p of d.spec.passes) if (OPAQUE_MODES.includes(p.lightMode)) this.drawPass(pass, i, d, p)
|
|
362
|
+
for (const mode of [...CHARACTER_MODES, ...OVERRIDE_MODES])
|
|
363
|
+
for (const { i, d } of list) for (const p of d.spec.passes) if (p.lightMode === mode) this.drawPass(pass, i, d, p)
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/** In the engine's transparent phase. */
|
|
367
|
+
drawTransparent(pass: GPURenderPassEncoder): void {
|
|
368
|
+
if (!this.installs.size) return
|
|
369
|
+
for (const { i, d } of this.each(false))
|
|
370
|
+
for (const p of d.spec.passes) if (OPAQUE_MODES.includes(p.lightMode)) this.drawPass(pass, i, d, p)
|
|
371
|
+
}
|
|
372
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
// Skinned vertex streams for native (game-shader) materials.
|
|
2
|
+
//
|
|
3
|
+
// The engine skins in the vertex stage of its own material shaders, from the
|
|
4
|
+
// bone palette (skinMats) and the post-morph vertex buffer. A game shader skins
|
|
5
|
+
// nothing: Unity hands it world-space-ready POSITION / NORMAL / TANGENT streams
|
|
6
|
+
// from its SkinnedMeshRenderer. So a model that wears native materials is
|
|
7
|
+
// skinned once per frame here, by compute, into three plain vertex buffers the
|
|
8
|
+
// game shaders read as Unity's — the same streams the compare page feeds them.
|
|
9
|
+
//
|
|
10
|
+
// Inputs are the engine's own buffers, untouched: the interleaved vertex
|
|
11
|
+
// buffer (position, normal, uv — 8 floats, morphs already applied), the joints
|
|
12
|
+
// (uint16x4, two words a vertex), the weights (unorm8x4, one word) and the
|
|
13
|
+
// skin matrices. Tangents are not in the engine's vertex format; they are
|
|
14
|
+
// derived once at install (tangentsFor) and skinned here with the normal.
|
|
15
|
+
|
|
16
|
+
export const UNITY_SKIN_WGSL = /* wgsl */ `
|
|
17
|
+
struct Params { count: u32, _a: u32, _b: u32, _c: u32 }
|
|
18
|
+
@group(0) @binding(0) var<uniform> P: Params;
|
|
19
|
+
@group(0) @binding(1) var<storage, read> verts: array<f32>; // pos(3) nrm(3) uv(2)
|
|
20
|
+
@group(0) @binding(2) var<storage, read> joints: array<u32>; // uint16x4: two words
|
|
21
|
+
@group(0) @binding(3) var<storage, read> weights: array<u32>; // unorm8x4: one word
|
|
22
|
+
@group(0) @binding(4) var<storage, read> skinMats: array<mat4x4f>;
|
|
23
|
+
@group(0) @binding(5) var<storage, read> tangents: array<f32>; // rest tangent(4)
|
|
24
|
+
@group(0) @binding(6) var<storage, read_write> outPos: array<f32>;
|
|
25
|
+
@group(0) @binding(7) var<storage, read_write> outNrm: array<f32>;
|
|
26
|
+
@group(0) @binding(8) var<storage, read_write> outTan: array<f32>;
|
|
27
|
+
|
|
28
|
+
@compute @workgroup_size(64)
|
|
29
|
+
fn main(@builtin(global_invocation_id) id: vec3u) {
|
|
30
|
+
let v = id.x;
|
|
31
|
+
if (v >= P.count) { return; }
|
|
32
|
+
let p = vec4f(verts[v * 8u], verts[v * 8u + 1u], verts[v * 8u + 2u], 1.0);
|
|
33
|
+
let n = vec3f(verts[v * 8u + 3u], verts[v * 8u + 4u], verts[v * 8u + 5u]);
|
|
34
|
+
let t = vec4f(tangents[v * 4u], tangents[v * 4u + 1u], tangents[v * 4u + 2u], tangents[v * 4u + 3u]);
|
|
35
|
+
let j01 = joints[v * 2u];
|
|
36
|
+
let j23 = joints[v * 2u + 1u];
|
|
37
|
+
let j = vec4u(j01 & 0xffffu, j01 >> 16u, j23 & 0xffffu, j23 >> 16u);
|
|
38
|
+
var w = unpack4x8unorm(weights[v]);
|
|
39
|
+
// Renormalised as the material vertex stage does: PMX ships unnormalised
|
|
40
|
+
// weights on extras, and a sum of zero is the bind pose.
|
|
41
|
+
let s = w.x + w.y + w.z + w.w;
|
|
42
|
+
w = select(vec4f(1.0, 0.0, 0.0, 0.0), w / s, s > 0.0001);
|
|
43
|
+
var sp = vec3f(0.0);
|
|
44
|
+
var sn = vec3f(0.0);
|
|
45
|
+
var st = vec3f(0.0);
|
|
46
|
+
for (var k = 0u; k < 4u; k++) {
|
|
47
|
+
let m = skinMats[j[k]];
|
|
48
|
+
let r = mat3x3f(m[0].xyz, m[1].xyz, m[2].xyz);
|
|
49
|
+
sp += (m * p).xyz * w[k];
|
|
50
|
+
sn += (r * n) * w[k];
|
|
51
|
+
st += (r * t.xyz) * w[k];
|
|
52
|
+
}
|
|
53
|
+
// Degenerate blends (opposing bones at 50/50) cancel to zero; up, not NaN.
|
|
54
|
+
let ln = dot(sn, sn);
|
|
55
|
+
let lt = dot(st, st);
|
|
56
|
+
sn = select(vec3f(0.0, 1.0, 0.0), sn * inverseSqrt(ln), ln > 1e-12);
|
|
57
|
+
st = select(vec3f(1.0, 0.0, 0.0), st * inverseSqrt(lt), lt > 1e-12);
|
|
58
|
+
outPos[v * 3u] = sp.x; outPos[v * 3u + 1u] = sp.y; outPos[v * 3u + 2u] = sp.z;
|
|
59
|
+
outNrm[v * 3u] = sn.x; outNrm[v * 3u + 1u] = sn.y; outNrm[v * 3u + 2u] = sn.z;
|
|
60
|
+
outTan[v * 4u] = st.x; outTan[v * 4u + 1u] = st.y; outTan[v * 4u + 2u] = st.z; outTan[v * 4u + 3u] = t.w;
|
|
61
|
+
}
|
|
62
|
+
`
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Per-vertex tangents (xyz, w = handedness) from positions, normals and uvs,
|
|
66
|
+
* accumulated per triangle and orthogonalised against the normal — the
|
|
67
|
+
* standard construction, which is what a mesh imported without tangents gets
|
|
68
|
+
* in Unity too. `verts` is the engine's interleaved layout (8 floats).
|
|
69
|
+
*/
|
|
70
|
+
export function tangentsFor(verts: Float32Array, indices: Uint32Array | Uint16Array): Float32Array {
|
|
71
|
+
const n = verts.length / 8
|
|
72
|
+
const tan = new Float64Array(n * 3)
|
|
73
|
+
const bit = new Float64Array(n * 3)
|
|
74
|
+
for (let i = 0; i + 2 < indices.length; i += 3) {
|
|
75
|
+
const a = indices[i],
|
|
76
|
+
b = indices[i + 1],
|
|
77
|
+
c = indices[i + 2]
|
|
78
|
+
const p = (v: number, k: number) => verts[v * 8 + k]
|
|
79
|
+
const e1 = [p(b, 0) - p(a, 0), p(b, 1) - p(a, 1), p(b, 2) - p(a, 2)]
|
|
80
|
+
const e2 = [p(c, 0) - p(a, 0), p(c, 1) - p(a, 1), p(c, 2) - p(a, 2)]
|
|
81
|
+
const du1 = p(b, 6) - p(a, 6),
|
|
82
|
+
dv1 = p(b, 7) - p(a, 7)
|
|
83
|
+
const du2 = p(c, 6) - p(a, 6),
|
|
84
|
+
dv2 = p(c, 7) - p(a, 7)
|
|
85
|
+
const det = du1 * dv2 - du2 * dv1
|
|
86
|
+
if (Math.abs(det) < 1e-12) continue
|
|
87
|
+
const r = 1 / det
|
|
88
|
+
for (const v of [a, b, c]) {
|
|
89
|
+
for (let k = 0; k < 3; k++) {
|
|
90
|
+
tan[v * 3 + k] += (e1[k] * dv2 - e2[k] * dv1) * r
|
|
91
|
+
bit[v * 3 + k] += (e2[k] * du1 - e1[k] * du2) * r
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
const out = new Float32Array(n * 4)
|
|
96
|
+
for (let v = 0; v < n; v++) {
|
|
97
|
+
const nx = verts[v * 8 + 3],
|
|
98
|
+
ny = verts[v * 8 + 4],
|
|
99
|
+
nz = verts[v * 8 + 5]
|
|
100
|
+
let tx = tan[v * 3],
|
|
101
|
+
ty = tan[v * 3 + 1],
|
|
102
|
+
tz = tan[v * 3 + 2]
|
|
103
|
+
const d = tx * nx + ty * ny + tz * nz
|
|
104
|
+
tx -= nx * d
|
|
105
|
+
ty -= ny * d
|
|
106
|
+
tz -= nz * d
|
|
107
|
+
let len = Math.hypot(tx, ty, tz)
|
|
108
|
+
if (len < 1e-12) {
|
|
109
|
+
// Any direction perpendicular to the normal: a surface with no uv
|
|
110
|
+
// gradient has no tangent to speak of, and a zero one is NaN downstream.
|
|
111
|
+
const ax = Math.abs(nx) < 0.9 ? 1 : 0
|
|
112
|
+
tx = ax ? 0 : -nz
|
|
113
|
+
ty = ax ? nz : 0
|
|
114
|
+
tz = ax ? -ny : nx
|
|
115
|
+
len = Math.hypot(tx, ty, tz) || 1
|
|
116
|
+
}
|
|
117
|
+
tx /= len
|
|
118
|
+
ty /= len
|
|
119
|
+
tz /= len
|
|
120
|
+
// Handedness: whether the bitangent agrees with n × t.
|
|
121
|
+
const cx = ny * tz - nz * ty,
|
|
122
|
+
cy = nz * tx - nx * tz,
|
|
123
|
+
cz = nx * ty - ny * tx
|
|
124
|
+
const w = cx * bit[v * 3] + cy * bit[v * 3 + 1] + cz * bit[v * 3 + 2] < 0 ? -1 : 1
|
|
125
|
+
out.set([tx, ty, tz, w], v * 4)
|
|
126
|
+
}
|
|
127
|
+
return out
|
|
128
|
+
}
|