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,375 @@
|
|
|
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
|
+
import { gameToEngine, invert4, mul4, scale4, flipY } from "./globals";
|
|
21
|
+
const MAIN_MODES = ["ALWAYS", "FORWARDBASE"];
|
|
22
|
+
export class NativeStage {
|
|
23
|
+
constructor(device, pkg) {
|
|
24
|
+
this.device = device;
|
|
25
|
+
this.buffers = [];
|
|
26
|
+
this.textures = [];
|
|
27
|
+
this.opaque = [];
|
|
28
|
+
this.transparent = [];
|
|
29
|
+
this.casters = [];
|
|
30
|
+
this.name = pkg.name;
|
|
31
|
+
this.pkg = pkg;
|
|
32
|
+
this.bounds = {
|
|
33
|
+
min: [Infinity, Infinity, Infinity],
|
|
34
|
+
max: [-Infinity, -Infinity, -Infinity],
|
|
35
|
+
};
|
|
36
|
+
this.zero = device.createBuffer({
|
|
37
|
+
label: "native stage: zero stream",
|
|
38
|
+
size: 64,
|
|
39
|
+
usage: GPUBufferUsage.VERTEX,
|
|
40
|
+
});
|
|
41
|
+
this.white = device.createBuffer({
|
|
42
|
+
label: "native stage: white stream",
|
|
43
|
+
size: 64,
|
|
44
|
+
usage: GPUBufferUsage.VERTEX,
|
|
45
|
+
mappedAtCreation: true,
|
|
46
|
+
});
|
|
47
|
+
new Float32Array(this.white.getMappedRange()).fill(1);
|
|
48
|
+
this.white.unmap();
|
|
49
|
+
this.buffers.push(this.zero, this.white);
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Load a package: every mesh and texture it names through `read`, every
|
|
53
|
+
* shader registered with the hosts (the scene pass's and the shadow atlas's).
|
|
54
|
+
*/
|
|
55
|
+
static async load(device, pkg, read, hosts, tools, onProgress) {
|
|
56
|
+
if (pkg.kind !== "unity-native-stage")
|
|
57
|
+
throw new Error("not a native stage package");
|
|
58
|
+
const st = new NativeStage(device, pkg);
|
|
59
|
+
// Progress by weight, not by file: a 2048² picture is most of a second to
|
|
60
|
+
// decode, a shader or a small mesh nothing — so pictures weigh their
|
|
61
|
+
// megapixels, everything else one unit of a quarter of one.
|
|
62
|
+
const texWeight = (t) => Math.max(0.25, ((t.cube ? 6 : 1) * t.width * t.height) / (1 << 20));
|
|
63
|
+
const total = 0.25 * (pkg.shaders.length + pkg.meshes.length) + pkg.textures.reduce((s, t) => s + texWeight(t), 0);
|
|
64
|
+
let done = 0;
|
|
65
|
+
const tick = (w = 0.25) => onProgress?.((done += w), total);
|
|
66
|
+
const text = async (p) => new TextDecoder().decode(await read(p));
|
|
67
|
+
const shaders = new Map();
|
|
68
|
+
await Promise.all(pkg.shaders.map(async (n) => {
|
|
69
|
+
const [vert, frag, info] = await Promise.all([
|
|
70
|
+
text(`shaders/${n}.vert.wgsl`),
|
|
71
|
+
text(`shaders/${n}.frag.wgsl`),
|
|
72
|
+
text(`shaders/${n}.json`),
|
|
73
|
+
]);
|
|
74
|
+
shaders.set(n, { name: n, vert, frag, info: JSON.parse(info) });
|
|
75
|
+
tick();
|
|
76
|
+
}));
|
|
77
|
+
const casterShaders = new Set();
|
|
78
|
+
for (const m of pkg.materials)
|
|
79
|
+
for (const p of m.passes)
|
|
80
|
+
if (p.lightMode === "SHADOWCASTER")
|
|
81
|
+
casterShaders.add(p.shader);
|
|
82
|
+
for (const [n, s] of shaders) {
|
|
83
|
+
if (casterShaders.has(n))
|
|
84
|
+
hosts.shadow.register(s);
|
|
85
|
+
else
|
|
86
|
+
hosts.scene.register(s);
|
|
87
|
+
}
|
|
88
|
+
const meshes = new Map();
|
|
89
|
+
await Promise.all(pkg.meshes.map(async (m) => {
|
|
90
|
+
const data = await read(`meshes/${m.id}.bin`);
|
|
91
|
+
const buffer = device.createBuffer({
|
|
92
|
+
label: `native stage mesh ${m.id}`,
|
|
93
|
+
size: Math.max(16, Math.ceil(data.byteLength / 4) * 4),
|
|
94
|
+
usage: GPUBufferUsage.VERTEX | GPUBufferUsage.INDEX | GPUBufferUsage.COPY_DST,
|
|
95
|
+
});
|
|
96
|
+
device.queue.writeBuffer(buffer, 0, data, 0, data.byteLength & ~3);
|
|
97
|
+
st.buffers.push(buffer);
|
|
98
|
+
meshes.set(m.id, { info: m, buffer });
|
|
99
|
+
tick();
|
|
100
|
+
}));
|
|
101
|
+
const textures = new Map();
|
|
102
|
+
await Promise.all(pkg.textures.map(async (t) => {
|
|
103
|
+
try {
|
|
104
|
+
textures.set(t.id, await st.loadTexture(t, read, tools.mipmaps));
|
|
105
|
+
}
|
|
106
|
+
catch (e) {
|
|
107
|
+
console.warn(`native stage: texture ${t.id} ${t.name}:`, e);
|
|
108
|
+
}
|
|
109
|
+
tick(texWeight(t));
|
|
110
|
+
}));
|
|
111
|
+
st.build(meshes, textures, tools.fallback);
|
|
112
|
+
return st;
|
|
113
|
+
}
|
|
114
|
+
async loadTexture(t, read, mipmaps) {
|
|
115
|
+
const d = this.device;
|
|
116
|
+
const full = Math.max(1, Math.floor(Math.log2(Math.max(t.width, t.height))) + 1);
|
|
117
|
+
const format = t.hdr ? "rgba16float" : t.srgb ? "rgba8unorm-srgb" : "rgba8unorm";
|
|
118
|
+
if (t.cube) {
|
|
119
|
+
// Every face and mip as recorded: face-major <id>_f<face>_m<mip>.bin, RGBA16F
|
|
120
|
+
// rows along WebGPU's face directions (AGDlcRecord's AGCubeFace).
|
|
121
|
+
const mips = t.mipsExported || 1;
|
|
122
|
+
const texture = d.createTexture({
|
|
123
|
+
label: `native stage ${t.name}`,
|
|
124
|
+
size: [t.width, t.height, 6],
|
|
125
|
+
format,
|
|
126
|
+
mipLevelCount: mips,
|
|
127
|
+
usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST,
|
|
128
|
+
});
|
|
129
|
+
await Promise.all(t.files.map(async (file, k) => {
|
|
130
|
+
const face = Math.floor(k / mips);
|
|
131
|
+
const mip = k % mips;
|
|
132
|
+
const s = Math.max(1, t.width >> mip);
|
|
133
|
+
const data = await read(`textures/${file}`);
|
|
134
|
+
d.queue.writeTexture({ texture, origin: [0, 0, face], mipLevel: mip }, data, { bytesPerRow: s * 8, rowsPerImage: s }, [s, s, 1]);
|
|
135
|
+
}));
|
|
136
|
+
this.textures.push(texture);
|
|
137
|
+
const sampler = d.createSampler({
|
|
138
|
+
magFilter: "linear",
|
|
139
|
+
minFilter: "linear",
|
|
140
|
+
mipmapFilter: "linear",
|
|
141
|
+
});
|
|
142
|
+
return {
|
|
143
|
+
texture,
|
|
144
|
+
view: texture.createView({ dimension: "cube" }),
|
|
145
|
+
sampler,
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
const mips = t.hdr ? 1 : full;
|
|
149
|
+
const texture = d.createTexture({
|
|
150
|
+
label: `native stage ${t.name}`,
|
|
151
|
+
size: [t.width, t.height],
|
|
152
|
+
format,
|
|
153
|
+
mipLevelCount: mips,
|
|
154
|
+
usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST | GPUTextureUsage.RENDER_ATTACHMENT,
|
|
155
|
+
});
|
|
156
|
+
const data = await read(`textures/${t.files[0]}`);
|
|
157
|
+
if (t.hdr) {
|
|
158
|
+
// Raw RGBA16F rows as Unity holds them (row 0 is v 0): as they are.
|
|
159
|
+
d.queue.writeTexture({ texture }, data, { bytesPerRow: t.width * 8 }, [t.width, t.height]);
|
|
160
|
+
}
|
|
161
|
+
else {
|
|
162
|
+
// A PNG runs top down; Unity's row 0 is the bottom (v 0): flipped on the way in.
|
|
163
|
+
const bmp = await createImageBitmap(new Blob([data]), {
|
|
164
|
+
imageOrientation: "flipY",
|
|
165
|
+
colorSpaceConversion: "none",
|
|
166
|
+
premultiplyAlpha: "none",
|
|
167
|
+
// A picture swapped into the folder need not be the size it replaced.
|
|
168
|
+
resizeWidth: t.width,
|
|
169
|
+
resizeHeight: t.height,
|
|
170
|
+
resizeQuality: "high",
|
|
171
|
+
});
|
|
172
|
+
d.queue.copyExternalImageToTexture({ source: bmp }, { texture }, [t.width, t.height]);
|
|
173
|
+
bmp.close();
|
|
174
|
+
if (mips > 1)
|
|
175
|
+
mipmaps(texture, mips);
|
|
176
|
+
}
|
|
177
|
+
this.textures.push(texture);
|
|
178
|
+
const wrap = {
|
|
179
|
+
Repeat: "repeat",
|
|
180
|
+
Clamp: "clamp-to-edge",
|
|
181
|
+
Mirror: "mirror-repeat",
|
|
182
|
+
MirrorOnce: "mirror-repeat",
|
|
183
|
+
}[t.wrap ?? "Repeat"] ?? "repeat";
|
|
184
|
+
const filter = t.filter === "Point" ? "nearest" : "linear";
|
|
185
|
+
const trilinear = t.filter === "Trilinear";
|
|
186
|
+
const sampler = d.createSampler({
|
|
187
|
+
addressModeU: wrap,
|
|
188
|
+
addressModeV: wrap,
|
|
189
|
+
magFilter: filter,
|
|
190
|
+
minFilter: filter,
|
|
191
|
+
mipmapFilter: trilinear ? "linear" : "nearest",
|
|
192
|
+
maxAnisotropy: filter === "linear" && trilinear ? Math.min(16, Math.max(1, t.aniso || 1)) : 1,
|
|
193
|
+
});
|
|
194
|
+
return { texture, view: texture.createView(), sampler };
|
|
195
|
+
}
|
|
196
|
+
/** Every renderer's draws, resolved once: the stage does not move. */
|
|
197
|
+
build(meshes, textures, fallback) {
|
|
198
|
+
const pkg = this.pkg;
|
|
199
|
+
const s = pkg.scale;
|
|
200
|
+
const mats = new Map(pkg.materials.map((m) => [m.id, m]));
|
|
201
|
+
const globalViews = {};
|
|
202
|
+
const globalSamplers = {};
|
|
203
|
+
for (const [slot, id] of Object.entries(pkg.globalTextures)) {
|
|
204
|
+
const t = textures.get(id);
|
|
205
|
+
if (t) {
|
|
206
|
+
globalViews[slot] = t.view;
|
|
207
|
+
globalSamplers[slot] = t.sampler;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
for (const r of pkg.renderers) {
|
|
211
|
+
const mesh = meshes.get(r.mesh);
|
|
212
|
+
if (!mesh)
|
|
213
|
+
continue;
|
|
214
|
+
const m = r.matrix;
|
|
215
|
+
// centre and corners of the mesh box, to engine units
|
|
216
|
+
const b = mesh.info.bounds;
|
|
217
|
+
const cx = b.center[0], cy = b.center[1], cz = b.center[2];
|
|
218
|
+
const center = [
|
|
219
|
+
-(m[0] * cx + m[4] * cy + m[8] * cz + m[12]) * s,
|
|
220
|
+
(m[1] * cx + m[5] * cy + m[9] * cz + m[13]) * s,
|
|
221
|
+
-(m[2] * cx + m[6] * cy + m[10] * cz + m[14]) * s,
|
|
222
|
+
];
|
|
223
|
+
const ex = b.size[0] / 2, ey = b.size[1] / 2, ez = b.size[2] / 2;
|
|
224
|
+
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);
|
|
225
|
+
const world = Float32Array.from(m);
|
|
226
|
+
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]);
|
|
227
|
+
const perDraw = {
|
|
228
|
+
unity_ObjectToWorld: world,
|
|
229
|
+
unity_WorldToObject: invert4(world),
|
|
230
|
+
unity_WorldTransformParams: [0, 0, 0, det < 0 ? -1 : 1],
|
|
231
|
+
unity_LightData: [0, 0, 0, 0],
|
|
232
|
+
unity_LightIndices: [
|
|
233
|
+
[0, 0, 0, 0],
|
|
234
|
+
[0, 0, 0, 0],
|
|
235
|
+
],
|
|
236
|
+
unity_RenderingLayer: new Uint32Array([r.layers >>> 0, 0, 0, 0]),
|
|
237
|
+
unity_LODFade: [1, 1, 0, 0],
|
|
238
|
+
...r.values,
|
|
239
|
+
};
|
|
240
|
+
this.addItems(r, mesh, perDraw, center, mats, globalViews, globalSamplers, textures, fallback);
|
|
241
|
+
if (r.castShadows) {
|
|
242
|
+
for (let i = 0; i < 3; i++) {
|
|
243
|
+
this.bounds.min[i] = Math.min(this.bounds.min[i], center[i] - half[i]);
|
|
244
|
+
this.bounds.max[i] = Math.max(this.bounds.max[i], center[i] + half[i]);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
addItems(r, mesh, perDraw, center, mats, globalViews, globalSamplers, textures, fallback) {
|
|
250
|
+
r.materials.forEach((mid, sub) => {
|
|
251
|
+
const mat = mid ? mats.get(mid) : undefined;
|
|
252
|
+
if (!mat || sub >= mesh.info.submeshes.length)
|
|
253
|
+
return;
|
|
254
|
+
const views = {
|
|
255
|
+
...globalViews,
|
|
256
|
+
};
|
|
257
|
+
const samplers = {
|
|
258
|
+
...globalSamplers,
|
|
259
|
+
};
|
|
260
|
+
// an empty slot reads what the shader declared it defaults to
|
|
261
|
+
for (const [slot, def] of Object.entries(mat.defaults ?? {}))
|
|
262
|
+
views[slot] = fallback[def] ?? fallback.white;
|
|
263
|
+
for (const [slot, id] of [...Object.entries(mat.textures), ...Object.entries(r.textures)]) {
|
|
264
|
+
const t = textures.get(id);
|
|
265
|
+
if (!t)
|
|
266
|
+
continue;
|
|
267
|
+
views[slot] = t.view;
|
|
268
|
+
samplers[slot] = t.sampler;
|
|
269
|
+
}
|
|
270
|
+
for (const pass of mat.passes) {
|
|
271
|
+
const item = {
|
|
272
|
+
renderer: r,
|
|
273
|
+
material: mat,
|
|
274
|
+
sub,
|
|
275
|
+
mesh,
|
|
276
|
+
pass,
|
|
277
|
+
perDraw,
|
|
278
|
+
textures: views,
|
|
279
|
+
samplers,
|
|
280
|
+
center,
|
|
281
|
+
key: `stage|${r.id}|${sub}|${pass.lightMode}|${pass.shader}`,
|
|
282
|
+
};
|
|
283
|
+
if (pass.lightMode === "SHADOWCASTER") {
|
|
284
|
+
if (r.castShadows)
|
|
285
|
+
this.casters.push(item);
|
|
286
|
+
}
|
|
287
|
+
else if (MAIN_MODES.includes(pass.lightMode)) {
|
|
288
|
+
;
|
|
289
|
+
(mat.queue > 2500 ? this.transparent : this.opaque).push(item);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
streams(host, it) {
|
|
295
|
+
const info = it.mesh.info;
|
|
296
|
+
return host.inputs(it.pass.shader).map(({ semantic }) => {
|
|
297
|
+
const st = info.streams[semantic];
|
|
298
|
+
if (!st) {
|
|
299
|
+
// A missing vertex colour reads white, as Unity binds it; anything else zero.
|
|
300
|
+
return {
|
|
301
|
+
buffer: semantic === "COLOR" ? this.white : this.zero,
|
|
302
|
+
offset: 0,
|
|
303
|
+
stride: 0,
|
|
304
|
+
format: "float32x4",
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
const n = st[1];
|
|
308
|
+
return {
|
|
309
|
+
buffer: it.mesh.buffer,
|
|
310
|
+
offset: st[0],
|
|
311
|
+
stride: n * 4,
|
|
312
|
+
format: (n === 1 ? "float32" : `float32x${n}`),
|
|
313
|
+
};
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
draw(pass, host, it, globals, frame, keySuffix = "") {
|
|
317
|
+
const sm = it.mesh.info.submeshes[it.sub];
|
|
318
|
+
host.draw(pass, {
|
|
319
|
+
shader: it.pass.shader,
|
|
320
|
+
state: it.pass.state,
|
|
321
|
+
streams: this.streams(host, it),
|
|
322
|
+
index: { buffer: it.mesh.buffer, format: "uint32", offset: sm.offset, count: sm.count },
|
|
323
|
+
sources: [it.perDraw, it.material.values, globals],
|
|
324
|
+
textures: { ...it.textures, ...frame },
|
|
325
|
+
samplers: it.samplers,
|
|
326
|
+
key: it.key + keySuffix,
|
|
327
|
+
pass: keySuffix || "main",
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
/** One cascade tile of the engine's atlas: every caster's SHADOWCASTER pass. */
|
|
331
|
+
drawShadow(pass, host, globals, cascade, viewProj, engineScale) {
|
|
332
|
+
const vp = flipY(mul4(viewProj, gameToEngine(engineScale)));
|
|
333
|
+
// ShadowUtils.GetShadowBias as the game set it: half a texel along the light
|
|
334
|
+
// and half a texel in along the normal, a texel being this tile's width in
|
|
335
|
+
// game units over its resolution (2048).
|
|
336
|
+
const rowLen = Math.hypot(vp[0], vp[4], vp[8]);
|
|
337
|
+
const texel = rowLen > 0 ? 2 / rowLen / 2048 : 0;
|
|
338
|
+
const g = {
|
|
339
|
+
...globals,
|
|
340
|
+
unity_MatrixVP: vp,
|
|
341
|
+
unity_MatrixV: scale4(1),
|
|
342
|
+
unity_MatrixP: vp,
|
|
343
|
+
glstate_matrix_projection: vp,
|
|
344
|
+
sim_ShadowBias: [0.5 * texel, -0.5 * texel, 0, 0],
|
|
345
|
+
};
|
|
346
|
+
for (const it of this.casters)
|
|
347
|
+
this.draw(pass, host, it, g, {}, `|c${cascade}`);
|
|
348
|
+
}
|
|
349
|
+
/** The opaque queue, front to back within each queue. */
|
|
350
|
+
drawOpaque(pass, host, globals, frame, eye) {
|
|
351
|
+
const d = (it) => (it.center[0] - eye[0]) ** 2 + (it.center[1] - eye[1]) ** 2 + (it.center[2] - eye[2]) ** 2;
|
|
352
|
+
const list = this.opaque.slice().sort((a, b) => a.material.queue - b.material.queue || d(a) - d(b));
|
|
353
|
+
for (const it of list)
|
|
354
|
+
this.draw(pass, host, it, globals, frame);
|
|
355
|
+
}
|
|
356
|
+
/** The transparent queue, back to front within each queue. */
|
|
357
|
+
drawTransparent(pass, host, globals, frame, eye) {
|
|
358
|
+
const d = (it) => (it.center[0] - eye[0]) ** 2 + (it.center[1] - eye[1]) ** 2 + (it.center[2] - eye[2]) ** 2;
|
|
359
|
+
const list = this.transparent.slice().sort((a, b) => a.material.queue - b.material.queue || d(b) - d(a));
|
|
360
|
+
for (const it of list)
|
|
361
|
+
this.draw(pass, host, it, globals, frame);
|
|
362
|
+
}
|
|
363
|
+
destroy(hosts) {
|
|
364
|
+
for (const h of hosts)
|
|
365
|
+
h.release("stage|");
|
|
366
|
+
const bufs = this.buffers;
|
|
367
|
+
const texs = this.textures;
|
|
368
|
+
void this.device.queue.onSubmittedWorkDone().then(() => {
|
|
369
|
+
for (const b of bufs)
|
|
370
|
+
b.destroy();
|
|
371
|
+
for (const t of texs)
|
|
372
|
+
t.destroy();
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/** A pass's state as the exporter writes it: each key's ShaderLab arguments. */
|
|
2
|
+
export type PassState = Record<string, (string | number)[]>;
|
|
3
|
+
export declare function blendFactor(v: string | number | undefined): GPUBlendFactor;
|
|
4
|
+
export declare function blendOp(v: string | number | undefined): GPUBlendOperation;
|
|
5
|
+
export declare function compare(v: string | number | undefined, fallback?: GPUCompareFunction): GPUCompareFunction;
|
|
6
|
+
/** A ZTest, flipped when the depth buffer is reversed — as Unity flips it on D3D. */
|
|
7
|
+
export declare function depthCompare(v: string | number | undefined, reversed: boolean): GPUCompareFunction;
|
|
8
|
+
export declare function stencilOp(v: string | number | undefined): GPUStencilOperation;
|
|
9
|
+
export declare function cull(v: string | number | undefined): GPUCullMode;
|
|
10
|
+
export declare function zwrite(v: string | number | undefined): boolean;
|
|
11
|
+
/** Unity ColorWriteMask (A 1, B 2, G 4, R 8, or letters) to WebGPU's (R 1, G 2, B 4, A 8). */
|
|
12
|
+
export declare function colorMask(v: string | number | undefined): number;
|
|
13
|
+
/** The colour blend a pass declares, or undefined for opaque (One Zero). */
|
|
14
|
+
export declare function blendState(st: PassState): GPUBlendState | undefined;
|
|
15
|
+
/** The stencil a pass declares, or undefined when it touches none. */
|
|
16
|
+
export declare function stencilState(st: PassState): {
|
|
17
|
+
face: GPUStencilFaceState;
|
|
18
|
+
read: number;
|
|
19
|
+
write: number;
|
|
20
|
+
ref: number;
|
|
21
|
+
} | undefined;
|
|
22
|
+
//# sourceMappingURL=state.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"state.d.ts","sourceRoot":"","sources":["../../src/unity/state.ts"],"names":[],"mappings":"AAKA,gFAAgF;AAChF,MAAM,MAAM,SAAS,GAAG,MAAM,CAAC,MAAM,EAAE,CAAC,MAAM,GAAG,MAAM,CAAC,EAAE,CAAC,CAAA;AA8B3D,wBAAgB,WAAW,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,GAAG,cAAc,CAI1E;AAGD,wBAAgB,OAAO,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,GAAG,iBAAiB,CAKzE;AAiBD,wBAAgB,OAAO,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,EAAE,QAAQ,GAAE,kBAA6B,GAAG,kBAAkB,CAInH;AAQD,qFAAqF;AACrF,wBAAgB,YAAY,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,EAAE,QAAQ,EAAE,OAAO,GAAG,kBAAkB,CAGlG;AAuBD,wBAAgB,SAAS,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,GAAG,mBAAmB,CAI7E;AAGD,wBAAgB,IAAI,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,GAAG,WAAW,CAMhE;AAED,wBAAgB,MAAM,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,GAAG,OAAO,CAI9D;AAED,8FAA8F;AAC9F,wBAAgB,SAAS,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,GAAG,MAAM,CAQhE;AAED,4EAA4E;AAC5E,wBAAgB,UAAU,CAAC,EAAE,EAAE,SAAS,GAAG,aAAa,GAAG,SAAS,CAkBnE;AAED,sEAAsE;AACtE,wBAAgB,YAAY,CAAC,EAAE,EAAE,SAAS,GAAG;IAAE,IAAI,EAAE,mBAAmB,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,GAAG,SAAS,CAa/H"}
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
// A Unity pass's fixed-function state (Blend, ZTest, Cull, Stencil, ColorMask,
|
|
2
|
+
// Offset), as its ShaderLab names or UnityEngine.Rendering enum numbers, into
|
|
3
|
+
// WebGPU's. Ported from the compare page's unity.js, where these were checked
|
|
4
|
+
// draw for draw against the game's frames.
|
|
5
|
+
const BLEND = [
|
|
6
|
+
"zero",
|
|
7
|
+
"one",
|
|
8
|
+
"dst",
|
|
9
|
+
"src",
|
|
10
|
+
"one-minus-dst",
|
|
11
|
+
"src-alpha",
|
|
12
|
+
"one-minus-src",
|
|
13
|
+
"dst-alpha",
|
|
14
|
+
"one-minus-dst-alpha",
|
|
15
|
+
"src-alpha-saturated",
|
|
16
|
+
"one-minus-src-alpha",
|
|
17
|
+
];
|
|
18
|
+
const BLEND_NAMES = {
|
|
19
|
+
zero: "zero",
|
|
20
|
+
one: "one",
|
|
21
|
+
dstcolor: "dst",
|
|
22
|
+
srccolor: "src",
|
|
23
|
+
oneminusdstcolor: "one-minus-dst",
|
|
24
|
+
srcalpha: "src-alpha",
|
|
25
|
+
oneminussrccolor: "one-minus-src",
|
|
26
|
+
dstalpha: "dst-alpha",
|
|
27
|
+
oneminusdstalpha: "one-minus-dst-alpha",
|
|
28
|
+
srcalphasaturate: "src-alpha-saturated",
|
|
29
|
+
oneminussrcalpha: "one-minus-src-alpha",
|
|
30
|
+
};
|
|
31
|
+
const isNum = (v) => typeof v === "number" || /^-?\d+(\.\d+)?$/.test(String(v));
|
|
32
|
+
export function blendFactor(v) {
|
|
33
|
+
if (v == null)
|
|
34
|
+
return "one";
|
|
35
|
+
if (isNum(v))
|
|
36
|
+
return BLEND[Math.round(+v)] ?? "one";
|
|
37
|
+
return BLEND_NAMES[String(v).toLowerCase()] ?? "one";
|
|
38
|
+
}
|
|
39
|
+
const BLEND_OP = ["add", "subtract", "reverse-subtract", "min", "max"];
|
|
40
|
+
export function blendOp(v) {
|
|
41
|
+
if (v == null)
|
|
42
|
+
return "add";
|
|
43
|
+
if (isNum(v))
|
|
44
|
+
return BLEND_OP[+v] ?? "add";
|
|
45
|
+
const s = String(v).toLowerCase();
|
|
46
|
+
return s === "sub" ? "subtract" : s === "revsub" ? "reverse-subtract" : s === "min" ? "min" : s === "max" ? "max" : "add";
|
|
47
|
+
}
|
|
48
|
+
// CompareFunction: 0 Disabled 1 Never 2 Less 3 Equal 4 LessEqual 5 Greater 6 NotEqual 7 GreaterEqual 8 Always
|
|
49
|
+
const CMP = [null, "never", "less", "equal", "less-equal", "greater", "not-equal", "greater-equal", "always"];
|
|
50
|
+
const CMP_NAMES = {
|
|
51
|
+
never: 1,
|
|
52
|
+
less: 2,
|
|
53
|
+
equal: 3,
|
|
54
|
+
lequal: 4,
|
|
55
|
+
lessequal: 4,
|
|
56
|
+
greater: 5,
|
|
57
|
+
notequal: 6,
|
|
58
|
+
gequal: 7,
|
|
59
|
+
greaterequal: 7,
|
|
60
|
+
always: 8,
|
|
61
|
+
disabled: 8,
|
|
62
|
+
};
|
|
63
|
+
export function compare(v, fallback = "always") {
|
|
64
|
+
const n = v != null && isNum(v) ? +v : CMP_NAMES[String(v ?? "").toLowerCase()];
|
|
65
|
+
if (n == null)
|
|
66
|
+
return fallback;
|
|
67
|
+
return CMP[n] ?? "always";
|
|
68
|
+
}
|
|
69
|
+
const REVERSE = {
|
|
70
|
+
less: "greater",
|
|
71
|
+
"less-equal": "greater-equal",
|
|
72
|
+
greater: "less",
|
|
73
|
+
"greater-equal": "less-equal",
|
|
74
|
+
};
|
|
75
|
+
/** A ZTest, flipped when the depth buffer is reversed — as Unity flips it on D3D. */
|
|
76
|
+
export function depthCompare(v, reversed) {
|
|
77
|
+
const c = compare(v ?? "LEqual", "less-equal");
|
|
78
|
+
return reversed ? (REVERSE[c] ?? c) : c;
|
|
79
|
+
}
|
|
80
|
+
// StencilOp: 0 Keep 1 Zero 2 Replace 3 IncrSat 4 DecrSat 5 Invert 6 IncrWrap 7 DecrWrap
|
|
81
|
+
const SOP = [
|
|
82
|
+
"keep",
|
|
83
|
+
"zero",
|
|
84
|
+
"replace",
|
|
85
|
+
"increment-clamp",
|
|
86
|
+
"decrement-clamp",
|
|
87
|
+
"invert",
|
|
88
|
+
"increment-wrap",
|
|
89
|
+
"decrement-wrap",
|
|
90
|
+
];
|
|
91
|
+
const SOP_NAMES = {
|
|
92
|
+
keep: 0,
|
|
93
|
+
zero: 1,
|
|
94
|
+
replace: 2,
|
|
95
|
+
incrsat: 3,
|
|
96
|
+
decrsat: 4,
|
|
97
|
+
invert: 5,
|
|
98
|
+
incrwrap: 6,
|
|
99
|
+
decrwrap: 7,
|
|
100
|
+
};
|
|
101
|
+
export function stencilOp(v) {
|
|
102
|
+
if (v == null)
|
|
103
|
+
return "keep";
|
|
104
|
+
const n = isNum(v) ? +v : SOP_NAMES[String(v).toLowerCase()];
|
|
105
|
+
return SOP[n ?? 0] ?? "keep";
|
|
106
|
+
}
|
|
107
|
+
// CullMode: 0 Off 1 Front 2 Back
|
|
108
|
+
export function cull(v) {
|
|
109
|
+
if (v == null)
|
|
110
|
+
return "back";
|
|
111
|
+
const s = String(v).toLowerCase();
|
|
112
|
+
if (s === "0" || s === "off")
|
|
113
|
+
return "none";
|
|
114
|
+
if (s === "1" || s === "front")
|
|
115
|
+
return "front";
|
|
116
|
+
return "back";
|
|
117
|
+
}
|
|
118
|
+
export function zwrite(v) {
|
|
119
|
+
if (v == null)
|
|
120
|
+
return true;
|
|
121
|
+
const s = String(v).toLowerCase();
|
|
122
|
+
return !(s === "0" || s === "off" || s === "false");
|
|
123
|
+
}
|
|
124
|
+
/** Unity ColorWriteMask (A 1, B 2, G 4, R 8, or letters) to WebGPU's (R 1, G 2, B 4, A 8). */
|
|
125
|
+
export function colorMask(v) {
|
|
126
|
+
if (v == null)
|
|
127
|
+
return 0xf;
|
|
128
|
+
const s = String(v).toUpperCase();
|
|
129
|
+
if (/^\d+$/.test(s)) {
|
|
130
|
+
const n = +s;
|
|
131
|
+
return (n & 8 ? 1 : 0) | (n & 4 ? 2 : 0) | (n & 2 ? 4 : 0) | (n & 1 ? 8 : 0);
|
|
132
|
+
}
|
|
133
|
+
return (s.includes("R") ? 1 : 0) | (s.includes("G") ? 2 : 0) | (s.includes("B") ? 4 : 0) | (s.includes("A") ? 8 : 0);
|
|
134
|
+
}
|
|
135
|
+
/** The colour blend a pass declares, or undefined for opaque (One Zero). */
|
|
136
|
+
export function blendState(st) {
|
|
137
|
+
const b = st.Blend?.map(String);
|
|
138
|
+
if (!b?.length || b[0].toLowerCase() === "off")
|
|
139
|
+
return undefined;
|
|
140
|
+
const color = {
|
|
141
|
+
srcFactor: blendFactor(b[0]),
|
|
142
|
+
dstFactor: blendFactor(b[1] ?? "zero"),
|
|
143
|
+
operation: blendOp(st.BlendOp?.[0]),
|
|
144
|
+
};
|
|
145
|
+
const alpha = b.length >= 4
|
|
146
|
+
? {
|
|
147
|
+
srcFactor: blendFactor(b[2]),
|
|
148
|
+
dstFactor: blendFactor(b[3]),
|
|
149
|
+
operation: blendOp(st.BlendOp?.[1] ?? st.BlendOp?.[0]),
|
|
150
|
+
}
|
|
151
|
+
: color;
|
|
152
|
+
const opaque = (c) => c.srcFactor === "one" && c.dstFactor === "zero";
|
|
153
|
+
return opaque(color) && opaque(alpha) ? undefined : { color, alpha };
|
|
154
|
+
}
|
|
155
|
+
/** The stencil a pass declares, or undefined when it touches none. */
|
|
156
|
+
export function stencilState(st) {
|
|
157
|
+
if (!st.StencilRef && !st.StencilComp)
|
|
158
|
+
return undefined;
|
|
159
|
+
return {
|
|
160
|
+
face: {
|
|
161
|
+
compare: compare(st.StencilComp?.[0] ?? "Always"),
|
|
162
|
+
passOp: stencilOp(st.StencilPass?.[0]),
|
|
163
|
+
failOp: stencilOp(st.StencilFail?.[0]),
|
|
164
|
+
depthFailOp: stencilOp(st.StencilZFail?.[0]),
|
|
165
|
+
},
|
|
166
|
+
read: +(st.StencilReadMask?.[0] ?? 255),
|
|
167
|
+
write: +(st.StencilWriteMask?.[0] ?? 255),
|
|
168
|
+
ref: st.StencilRef ? +st.StencilRef[0] : 0,
|
|
169
|
+
};
|
|
170
|
+
}
|