pelican-engine 0.0.0-stage → 0.3.1
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/AGENTS.md +147 -0
- package/LICENSE +22 -0
- package/README.md +129 -2
- package/distribution.json +197 -0
- package/docs/AI-WORKFLOW.md +517 -0
- package/docs/ARCHITECTURE.md +268 -0
- package/docs/CAPABILITIES.md +264 -0
- package/docs/DECISIONS.md +107 -0
- package/docs/M3-USAGE.md +192 -0
- package/docs/PELICAN-MIGRATION.md +38 -0
- package/docs/RENDER-BACKENDS.md +50 -0
- package/docs/ROADMAP.md +239 -0
- package/docs/THIRD-PARTY-NOTICES.md +151 -0
- package/docs/api/REFERENCE.md +1435 -0
- package/docs/api/index.json +1139 -0
- package/docs/npm/DISTRIBUTION.md +130 -0
- package/docs/npm/GAME-AUTHOR.md +147 -0
- package/engine.manifest.json +15 -0
- package/lib/assets/audio.js +67 -0
- package/lib/assets/errors.js +36 -0
- package/lib/assets/glb.js +277 -0
- package/lib/assets/index.js +53 -0
- package/lib/assets/jpeg.js +199 -0
- package/lib/assets/model.js +558 -0
- package/lib/assets/optimize.js +95 -0
- package/lib/assets/texture.js +187 -0
- package/lib/compiler/distribution.js +17 -0
- package/lib/compiler/extract.js +425 -0
- package/lib/compiler/graph.js +366 -0
- package/lib/compiler/index.js +574 -0
- package/lib/compiler/normalize.js +1194 -0
- package/lib/compiler/source-map.js +25 -0
- package/lib/compiler/types.js +2 -0
- package/lib/compiler/version.js +93 -0
- package/lib/experiments/simulation/index.js +2 -0
- package/lib/experiments/simulation/math.js +41 -0
- package/lib/experiments/simulation/probe.js +215 -0
- package/lib/experiments/simulation/scenes.js +192 -0
- package/lib/experiments/simulation/solver.js +346 -0
- package/lib/experiments/simulation/types.js +1 -0
- package/lib/experiments/world/adapters.js +30 -0
- package/lib/experiments/world/geometry.js +180 -0
- package/lib/experiments/world/index.js +2 -0
- package/lib/experiments/world/probe.js +288 -0
- package/lib/experiments/world/types.js +1 -0
- package/lib/experiments/world/world.js +599 -0
- package/lib/navigation/index.js +157 -0
- package/lib/physics/index.js +909 -0
- package/lib/protocol/index.js +92 -0
- package/lib/renderer/acceleration.js +609 -0
- package/lib/renderer/browser.js +2015 -0
- package/lib/renderer/bsdf.js +260 -0
- package/lib/renderer/checkpoint.js +88 -0
- package/lib/renderer/compute-executor.js +1034 -0
- package/lib/renderer/compute-texture-assets.js +49 -0
- package/lib/renderer/compute-view-contract.js +1 -0
- package/lib/renderer/denoiser.js +361 -0
- package/lib/renderer/dielectric.js +360 -0
- package/lib/renderer/embedded-image-pixels.js +368 -0
- package/lib/renderer/emissive-lights.js +67 -0
- package/lib/renderer/environment-importance.js +55 -0
- package/lib/renderer/environment.js +346 -0
- package/lib/renderer/gameplay.js +464 -0
- package/lib/renderer/geometry.js +103 -0
- package/lib/renderer/graph-expressions.js +485 -0
- package/lib/renderer/graph-tsl.js +141 -0
- package/lib/renderer/graph.js +214 -0
- package/lib/renderer/managed-texture-sampling.js +29 -0
- package/lib/renderer/material-graphs.js +297 -0
- package/lib/renderer/medium.js +348 -0
- package/lib/renderer/models.js +1053 -0
- package/lib/renderer/motion-shader.js +40 -0
- package/lib/renderer/path-shader.js +196 -0
- package/lib/renderer/pathtrace.js +324 -0
- package/lib/renderer/postprocess-contract.js +1 -0
- package/lib/renderer/postprocess.js +494 -0
- package/lib/renderer/render-buffers.js +132 -0
- package/lib/renderer/render-textures.js +155 -0
- package/lib/renderer/startup.js +50 -0
- package/lib/renderer/surface-capture.js +199 -0
- package/lib/renderer/surface-gpu.js +270 -0
- package/lib/renderer/surface-motion.js +84 -0
- package/lib/renderer/surface-ray-shader.js +91 -0
- package/lib/renderer/surface-shader.js +88 -0
- package/lib/renderer/surface-tracer.js +1720 -0
- package/lib/renderer/surface.js +1425 -0
- package/lib/renderer/temporal-filter.js +512 -0
- package/lib/renderer/texture-pixels.js +207 -0
- package/lib/renderer/traversal-nodes.js +54 -0
- package/lib/renderer/ui.js +446 -0
- package/lib/renderer/vertex-bounds.js +127 -0
- package/lib/renderer/vertex-contract.js +1 -0
- package/lib/renderer/vertex-graphs.js +197 -0
- package/lib/renderer/volume-shader.js +50 -0
- package/lib/runtime/compute-contract.js +1 -0
- package/lib/runtime/compute-service.js +674 -0
- package/lib/runtime/contracts.js +1 -0
- package/lib/runtime/error.js +37 -0
- package/lib/runtime/events.js +189 -0
- package/lib/runtime/game-services.js +772 -0
- package/lib/runtime/host.js +839 -0
- package/lib/runtime/material-parameters.js +145 -0
- package/lib/runtime/plugins.js +1 -0
- package/lib/runtime/postprocess-contract.js +1 -0
- package/lib/runtime/postprocess.js +189 -0
- package/lib/runtime/prefabs.js +13 -0
- package/lib/runtime/random.js +88 -0
- package/lib/runtime/render-buffer-contract.js +1 -0
- package/lib/runtime/render-buffers.js +243 -0
- package/lib/runtime/render-capabilities.js +17 -0
- package/lib/runtime/render-settings.js +249 -0
- package/lib/runtime/render-texture-contract.js +1 -0
- package/lib/runtime/render-textures.js +271 -0
- package/lib/runtime/resources.js +648 -0
- package/lib/runtime/scheduler.js +765 -0
- package/lib/runtime/storage.js +169 -0
- package/lib/runtime/world.js +594 -0
- package/lib/schema/index.js +24 -0
- package/lib/sdk/graph-values.js +16 -0
- package/lib/sdk/index.d.ts +800 -0
- package/lib/sdk/index.js +82 -0
- package/lib/tools/api-index.js +113 -0
- package/lib/tools/browser-session.js +89 -0
- package/lib/tools/cli.js +513 -0
- package/lib/tools/compress-dist.js +59 -0
- package/lib/tools/control-client.js +153 -0
- package/lib/tools/control-server.js +633 -0
- package/lib/tools/diagnostic-code.js +3 -0
- package/lib/tools/doctor.js +496 -0
- package/lib/tools/npm/browser-cli.js +6 -0
- package/lib/tools/npm/entry.js +40 -0
- package/lib/tools/npm/layout.js +195 -0
- package/lib/tools/setup.js +153 -0
- package/licenses/@dimforge/rapier2d-compat/LICENSE +201 -0
- package/licenses/@dimforge/rapier3d-compat/LICENSE +201 -0
- package/licenses/@gltf-transform/core/LICENSE.md +21 -0
- package/licenses/@gltf-transform/extensions/LICENSE.md +21 -0
- package/licenses/@gltf-transform/functions/LICENSE.md +21 -0
- package/licenses/@img/colour/LICENSE.md +82 -0
- package/licenses/@img/sharp-win32-x64/LICENSE +191 -0
- package/licenses/@oxc-project/types/LICENSE +22 -0
- package/licenses/@rolldown/pluginutils/LICENSE +21 -0
- package/licenses/@tweenjs/tween.js/LICENSE +23 -0
- package/licenses/@types/ndarray/LICENSE +21 -0
- package/licenses/@types/node/LICENSE +21 -0
- package/licenses/@types/stats.js/LICENSE +21 -0
- package/licenses/@types/three/@dimforge/rapier3d-compat/LICENSE +201 -0
- package/licenses/@types/three/LICENSE +21 -0
- package/licenses/@types/webxr/LICENSE +21 -0
- package/licenses/@webgpu/types/LICENSE +26 -0
- package/licenses/THIRD-PARTY-NOTICES.md +139 -0
- package/licenses/cwise-compiler/LICENSE +22 -0
- package/licenses/detect-libc/LICENSE +201 -0
- package/licenses/esbuild/LICENSE.md +21 -0
- package/licenses/fast-png/LICENSE +21 -0
- package/licenses/fdir/LICENSE +7 -0
- package/licenses/fflate/LICENSE +21 -0
- package/licenses/iobuffer/LICENSE +22 -0
- package/licenses/iota-array/LICENSE +22 -0
- package/licenses/is-buffer/LICENSE +21 -0
- package/licenses/ktx-parse/LICENSE +21 -0
- package/licenses/lightningcss/LICENSE +373 -0
- package/licenses/lightningcss-win32-x64-msvc/LICENSE +373 -0
- package/licenses/meshoptimizer/LICENSE.md +21 -0
- package/licenses/nanoid/LICENSE +20 -0
- package/licenses/ndarray/LICENSE +22 -0
- package/licenses/ndarray-lanczos/LICENSE +21 -0
- package/licenses/ndarray-ops/LICENSE +22 -0
- package/licenses/ndarray-pixels/LICENSE +21 -0
- package/licenses/ngraph.path/LICENSE +21 -0
- package/licenses/picocolors/LICENSE +15 -0
- package/licenses/picomatch/LICENSE +21 -0
- package/licenses/playwright/LICENSE +202 -0
- package/licenses/playwright/NOTICE +5 -0
- package/licenses/playwright-core/LICENSE +202 -0
- package/licenses/playwright-core/NOTICE +5 -0
- package/licenses/postcss/LICENSE +20 -0
- package/licenses/prettier/LICENSE +7 -0
- package/licenses/property-graph/LICENSE +21 -0
- package/licenses/rolldown/LICENSE +25 -0
- package/licenses/semver/LICENSE +15 -0
- package/licenses/sharp/LICENSE +191 -0
- package/licenses/source-map-js/LICENSE +28 -0
- package/licenses/three/LICENSE +21 -0
- package/licenses/tinyglobby/LICENSE +21 -0
- package/licenses/tsx/LICENSE +21 -0
- package/licenses/typescript/LICENSE.txt +55 -0
- package/licenses/undici-types/LICENSE +21 -0
- package/licenses/uniq/LICENSE +22 -0
- package/licenses/vite/LICENSE.md +2084 -0
- package/package.json +49 -4
- package/web/index.html +34 -0
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import * as TSL from "three/tsl";
|
|
2
|
+
import { Vector2, Vector3, Vector4 } from "three";
|
|
3
|
+
import { compileGraphDeclaration } from "../compiler/graph.js";
|
|
4
|
+
import { evaluateTypedGraph, lowerGraphWGSL, } from "./graph-expressions.js";
|
|
5
|
+
import { typedGraphToTSL } from "./graph-tsl.js";
|
|
6
|
+
function typedGraph(graph) {
|
|
7
|
+
if (!graph || graph.nodeTypes !== undefined)
|
|
8
|
+
return graph;
|
|
9
|
+
// M0 reference fixtures and serialized graphs predate per-node type metadata.
|
|
10
|
+
const result = compileGraphDeclaration({
|
|
11
|
+
nodes: graph.nodes,
|
|
12
|
+
output: graph.output,
|
|
13
|
+
...(graph.parameters ? { parameters: graph.parameters } : {}),
|
|
14
|
+
}, graph.outputType);
|
|
15
|
+
if (result.issues.length)
|
|
16
|
+
throw Error(result.issues[0].code + ": " + result.issues[0].message);
|
|
17
|
+
return result.graph;
|
|
18
|
+
}
|
|
19
|
+
/** Legacy signatures remain compatible; typed material adapters supply richer bindings. */
|
|
20
|
+
export function graphToTSL(input, time) {
|
|
21
|
+
const graph = typedGraph(input);
|
|
22
|
+
const parameters = new Map((graph.parameters ?? []).map((p) => {
|
|
23
|
+
const value = Array.isArray(p.default)
|
|
24
|
+
? p.default.length === 2
|
|
25
|
+
? new Vector2(...p.default)
|
|
26
|
+
: p.default.length === 3
|
|
27
|
+
? new Vector3(...p.default)
|
|
28
|
+
: new Vector4(...p.default)
|
|
29
|
+
: p.default;
|
|
30
|
+
return [p.name, TSL.uniform(value, p.type)];
|
|
31
|
+
}));
|
|
32
|
+
return typedGraphToTSL(graph, time, parameters, new Map());
|
|
33
|
+
}
|
|
34
|
+
export function evaluateGraph(graph, time) {
|
|
35
|
+
return evaluateTypedGraph(typedGraph(graph), { time });
|
|
36
|
+
}
|
|
37
|
+
/** The active check prevents uncancellable GPU promises from creating late resources. */
|
|
38
|
+
export async function withGraphGPUDeadline(device, prefix, work, timeoutMs = 10000) {
|
|
39
|
+
let stopped = false, timer;
|
|
40
|
+
let scope;
|
|
41
|
+
device.pushErrorScope("validation");
|
|
42
|
+
const closeScope = () => (scope ??= device.popErrorScope());
|
|
43
|
+
const active = () => {
|
|
44
|
+
if (stopped)
|
|
45
|
+
throw Error(prefix + "_CANCELLED: GPU graph work is no longer active.");
|
|
46
|
+
};
|
|
47
|
+
try {
|
|
48
|
+
return await Promise.race([
|
|
49
|
+
Promise.resolve()
|
|
50
|
+
.then(() => work(active))
|
|
51
|
+
.then(async (value) => {
|
|
52
|
+
active();
|
|
53
|
+
const validation = await closeScope();
|
|
54
|
+
active();
|
|
55
|
+
if (validation)
|
|
56
|
+
throw Error(prefix + "_GPU: " + validation.message);
|
|
57
|
+
return value;
|
|
58
|
+
}),
|
|
59
|
+
device.lost.then((info) => {
|
|
60
|
+
throw Error(prefix + "_DEVICE_LOST: " + info.reason);
|
|
61
|
+
}),
|
|
62
|
+
new Promise((_, reject) => {
|
|
63
|
+
timer = setTimeout(() => reject(Error(prefix +
|
|
64
|
+
"_TIMEOUT: GPU graph work exceeded " +
|
|
65
|
+
timeoutMs +
|
|
66
|
+
"ms.")), timeoutMs);
|
|
67
|
+
}),
|
|
68
|
+
]);
|
|
69
|
+
}
|
|
70
|
+
finally {
|
|
71
|
+
stopped = true;
|
|
72
|
+
clearTimeout(timer);
|
|
73
|
+
// popErrorScope may itself wait for a stuck device; cleanup must not await it.
|
|
74
|
+
void closeScope().catch(() => null);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
export async function runComputeGraph(device, input, count, workgroupSize, time, parameters = {}) {
|
|
78
|
+
if (!Number.isSafeInteger(count) ||
|
|
79
|
+
count < 1 ||
|
|
80
|
+
count > 65536 ||
|
|
81
|
+
count * 16 > device.limits.maxStorageBufferBindingSize ||
|
|
82
|
+
count * 16 > device.limits.maxBufferSize)
|
|
83
|
+
throw Error("TC_COMPUTE_COUNT: Count must fit the validated output budget.");
|
|
84
|
+
if (!Number.isSafeInteger(workgroupSize) ||
|
|
85
|
+
workgroupSize < 1 ||
|
|
86
|
+
workgroupSize > 256 ||
|
|
87
|
+
workgroupSize > device.limits.maxComputeWorkgroupSizeX ||
|
|
88
|
+
workgroupSize > device.limits.maxComputeInvocationsPerWorkgroup ||
|
|
89
|
+
Math.ceil(count / workgroupSize) >
|
|
90
|
+
device.limits.maxComputeWorkgroupsPerDimension)
|
|
91
|
+
throw Error("TC_COMPUTE_WORKGROUP: Dispatch exceeds actual device limits.");
|
|
92
|
+
const graph = typedGraph(input);
|
|
93
|
+
if (graph.nodes.some((node) => node.op === "sampleTexture"))
|
|
94
|
+
throw Error("TC_COMPUTE_TEXTURE: Compute texture bindings are not supported yet.");
|
|
95
|
+
if (graph.nodes.some(node => node.op === "readBuffer"))
|
|
96
|
+
throw Error("TC_COMPUTE_BINDING: Buffered kernels require the managed compute service and explicit buffer bindings.");
|
|
97
|
+
const context = {
|
|
98
|
+
time,
|
|
99
|
+
parameters,
|
|
100
|
+
inputs: { index: 0 },
|
|
101
|
+
};
|
|
102
|
+
const expected = evaluateTypedGraph(graph, context);
|
|
103
|
+
const declarations = graph.parameters ?? [];
|
|
104
|
+
const uniforms = new Float32Array((declarations.length + 1) * 4);
|
|
105
|
+
uniforms[0] = time;
|
|
106
|
+
for (let i = 0; i < declarations.length; i++) {
|
|
107
|
+
const p = declarations[i], value = Object.hasOwn(parameters, p.name)
|
|
108
|
+
? parameters[p.name]
|
|
109
|
+
: p.default;
|
|
110
|
+
if (typeof value === "number" || typeof value === "boolean")
|
|
111
|
+
uniforms[(i + 1) * 4] = Number(value);
|
|
112
|
+
else
|
|
113
|
+
uniforms.set(value, (i + 1) * 4);
|
|
114
|
+
}
|
|
115
|
+
const lowered = lowerGraphWGSL(graph, {
|
|
116
|
+
prefix: "tcCompute",
|
|
117
|
+
input: (name) => {
|
|
118
|
+
if (name === "time")
|
|
119
|
+
return "params[0].x";
|
|
120
|
+
if (name === "index")
|
|
121
|
+
return "f32(id.x)";
|
|
122
|
+
throw Error("TC_GRAPH_CONTEXT: Unsupported compute input " + name);
|
|
123
|
+
},
|
|
124
|
+
parameter: (name, type) => {
|
|
125
|
+
const index = declarations.findIndex((p) => p.name === name) + 1;
|
|
126
|
+
if (index === 0)
|
|
127
|
+
throw Error("TC_GRAPH_PARAMETER: " + name);
|
|
128
|
+
const base = "params[" + index + "]";
|
|
129
|
+
return type === "float"
|
|
130
|
+
? base + ".x"
|
|
131
|
+
: type === "bool"
|
|
132
|
+
? "(" + base + ".x != 0.0)"
|
|
133
|
+
: base + "." + "xyzw".slice(0, Number(type.slice(3)));
|
|
134
|
+
},
|
|
135
|
+
sampleTexture: () => {
|
|
136
|
+
throw Error("TC_COMPUTE_TEXTURE: A compute texture binding must be supplied by the engine resource API.");
|
|
137
|
+
},
|
|
138
|
+
});
|
|
139
|
+
const value = lowered.type === "float"
|
|
140
|
+
? "vec4f(" + lowered.result + ",0,0,1)"
|
|
141
|
+
: lowered.type === "bool"
|
|
142
|
+
? "vec4f(select(0.0,1.0," + lowered.result + "),0,0,1)"
|
|
143
|
+
: lowered.type === "vec2"
|
|
144
|
+
? "vec4f(" + lowered.result + ",0,1)"
|
|
145
|
+
: lowered.type === "vec3"
|
|
146
|
+
? "vec4f(" + lowered.result + ",1)"
|
|
147
|
+
: lowered.result;
|
|
148
|
+
const source = "@group(0) @binding(0) var<storage,read_write> result:array<vec4f>; @group(0) @binding(1) var<uniform> params:array<vec4f," +
|
|
149
|
+
(declarations.length + 1) +
|
|
150
|
+
">;\n" +
|
|
151
|
+
lowered.helpers +
|
|
152
|
+
"\n@compute @workgroup_size(" +
|
|
153
|
+
workgroupSize +
|
|
154
|
+
") fn main(@builtin(global_invocation_id) id:vec3u){if(id.x>=" +
|
|
155
|
+
count +
|
|
156
|
+
"u){return;}\n" +
|
|
157
|
+
lowered.code +
|
|
158
|
+
"\nresult[id.x]=" +
|
|
159
|
+
value +
|
|
160
|
+
";}";
|
|
161
|
+
const buffers = [];
|
|
162
|
+
try {
|
|
163
|
+
const values = await withGraphGPUDeadline(device, "TC_COMPUTE", async (active) => {
|
|
164
|
+
active();
|
|
165
|
+
const module = device.createShaderModule({
|
|
166
|
+
code: source,
|
|
167
|
+
label: "pelican controlled compute graph",
|
|
168
|
+
});
|
|
169
|
+
const info = await module.getCompilationInfo();
|
|
170
|
+
active();
|
|
171
|
+
const errors = info.messages.filter((m) => m.type === "error");
|
|
172
|
+
if (errors.length)
|
|
173
|
+
throw Error("TC_COMPUTE_SHADER: " + errors.map((e) => e.message).join("; "));
|
|
174
|
+
const pipeline = await device.createComputePipelineAsync({
|
|
175
|
+
layout: "auto",
|
|
176
|
+
compute: { module, entryPoint: "main" },
|
|
177
|
+
});
|
|
178
|
+
active();
|
|
179
|
+
const make = (size, usage) => {
|
|
180
|
+
const b = device.createBuffer({ size, usage });
|
|
181
|
+
buffers.push(b);
|
|
182
|
+
return b;
|
|
183
|
+
};
|
|
184
|
+
const output = make(count * 16, GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC), uniform = make(uniforms.byteLength, GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST), read = make(count * 16, GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ);
|
|
185
|
+
device.queue.writeBuffer(uniform, 0, uniforms);
|
|
186
|
+
const group = device.createBindGroup({
|
|
187
|
+
layout: pipeline.getBindGroupLayout(0),
|
|
188
|
+
entries: [
|
|
189
|
+
{ binding: 0, resource: { buffer: output } },
|
|
190
|
+
{ binding: 1, resource: { buffer: uniform } },
|
|
191
|
+
],
|
|
192
|
+
});
|
|
193
|
+
const encoder = device.createCommandEncoder(), pass = encoder.beginComputePass();
|
|
194
|
+
pass.setPipeline(pipeline);
|
|
195
|
+
pass.setBindGroup(0, group);
|
|
196
|
+
pass.dispatchWorkgroups(Math.ceil(count / workgroupSize));
|
|
197
|
+
pass.end();
|
|
198
|
+
encoder.copyBufferToBuffer(output, 0, read, 0, count * 16);
|
|
199
|
+
device.queue.submit([encoder.finish()]);
|
|
200
|
+
await read.mapAsync(GPUMapMode.READ);
|
|
201
|
+
active();
|
|
202
|
+
const values = Array.from(new Float32Array(read.getMappedRange().slice(0)));
|
|
203
|
+
read.unmap();
|
|
204
|
+
if (values.some((value) => !Number.isFinite(value)))
|
|
205
|
+
throw Error("TC_COMPUTE_NONFINITE: Graph arithmetic produced a nonfinite result; reduce parameter magnitude.");
|
|
206
|
+
return values;
|
|
207
|
+
});
|
|
208
|
+
return { count, outputType: graph.outputType, values, expected };
|
|
209
|
+
}
|
|
210
|
+
finally {
|
|
211
|
+
for (const buffer of buffers)
|
|
212
|
+
buffer.destroy();
|
|
213
|
+
}
|
|
214
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/** CPU vertex-geometry oracle only; GPU presentation never uploads this result. */
|
|
2
|
+
export function sampleManagedTexture(descriptor, uv, sampler) {
|
|
3
|
+
const shape = descriptor.texture;
|
|
4
|
+
if (!shape || descriptor.type !== "vec4" || !Number.isSafeInteger(shape.width) || !Number.isSafeInteger(shape.height) || shape.width < 1 || shape.height < 1 || shape.width * shape.height !== descriptor.values.length)
|
|
5
|
+
throw new Error("TC_RENDER_TEXTURE_RESOURCE: Expected a live linear rgba32float descriptor with matching dimensions.");
|
|
6
|
+
const f = Math.fround, u = f(uv[0]), v = f(uv[1]);
|
|
7
|
+
if (!Number.isFinite(u) || !Number.isFinite(v))
|
|
8
|
+
return [0, 0, 0, 0];
|
|
9
|
+
const normalize = (value, wrap = "clamp") => wrap === "repeat" ? f(value - Math.floor(value)) : wrap === "mirror" ? f(1 - Math.abs(f(value - f(2 * Math.floor(f(value * 0.5)))) - 1)) : Math.max(0, Math.min(1, value));
|
|
10
|
+
const address = (index, size, wrap = "clamp") => {
|
|
11
|
+
if (wrap === "clamp")
|
|
12
|
+
return Math.max(0, Math.min(size - 1, index));
|
|
13
|
+
const period = wrap === "mirror" ? size * 2 : size, cell = ((index % period) + period) % period;
|
|
14
|
+
return wrap === "mirror" ? Math.min(cell, size * 2 - 1 - cell) : cell;
|
|
15
|
+
};
|
|
16
|
+
const read = (x, y) => {
|
|
17
|
+
const value = descriptor.values[address(y, shape.height, sampler.wrapV) * shape.width + address(x, shape.width, sampler.wrapU)];
|
|
18
|
+
if (!Array.isArray(value) || value.length !== 4 || value.some(v => !Number.isFinite(f(v))))
|
|
19
|
+
throw new Error("TC_RENDER_TEXTURE_RESOURCE: Invalid sampled Vec4 texel.");
|
|
20
|
+
return value;
|
|
21
|
+
};
|
|
22
|
+
const x = f(normalize(u, sampler.wrapU) * shape.width), y = f(normalize(v, sampler.wrapV) * shape.height);
|
|
23
|
+
if (sampler.filter === "nearest")
|
|
24
|
+
return read(Math.floor(x), Math.floor(y)).map(f);
|
|
25
|
+
const px = f(x - 0.5), py = f(y - 0.5), bx = Math.floor(px), by = Math.floor(py), wx = f(px - bx), wy = f(py - by);
|
|
26
|
+
const a = read(bx, by), b = read(bx + 1, by), c = read(bx, by + 1), d = read(bx + 1, by + 1);
|
|
27
|
+
const mix = (left, right, weight) => f(f(f(left) * f(1 - weight)) + f(f(right) * weight));
|
|
28
|
+
return [0, 1, 2, 3].map(i => mix(mix(a[i], b[i], wx), mix(c[i], d[i], wx), wy));
|
|
29
|
+
}
|
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
import { lowerGraphWGSL } from "./graph-expressions.js";
|
|
2
|
+
export const DEFAULT_SURFACE_GRAPHS = /* wgsl */ String.raw `
|
|
3
|
+
fn surfaceGraphOpacity(index:u32,bary:vec3f,front:bool)->vec2f{return vec2f(0);}
|
|
4
|
+
fn surfaceGraphApply(index:u32,bary:vec3f,front:bool,view:vec3f,value:SurfaceValue)->SurfaceValue{return value;}
|
|
5
|
+
`;
|
|
6
|
+
/** Manual filtering over the eight vec4 fields in each existing material row. */
|
|
7
|
+
function textureHelpers(helper, row, width, height, sampler) {
|
|
8
|
+
const wrap = (axis, mode = "clamp") => mode === "repeat"
|
|
9
|
+
? `(${axis}-floor(${axis}))`
|
|
10
|
+
: mode === "mirror"
|
|
11
|
+
? `(1.0-abs((${axis}-2.0*floor(${axis}*0.5))-1.0))`
|
|
12
|
+
: `clamp(${axis},0.0,1.0)`;
|
|
13
|
+
const address = (axis, size, mode = "clamp") => {
|
|
14
|
+
if (mode === "clamp")
|
|
15
|
+
return `let ${axis}=clamp(cell.${axis},0,${size - 1});`;
|
|
16
|
+
const period = mode === "mirror" ? size * 2 : size;
|
|
17
|
+
return `let ${axis}Wrapped=((cell.${axis}%${period})+${period})%${period};let ${axis}=${mode === "mirror" ? `min(${axis}Wrapped,${period - 1}-${axis}Wrapped)` : axis + "Wrapped"};`;
|
|
18
|
+
};
|
|
19
|
+
return /* wgsl */ `
|
|
20
|
+
fn ${helper}Load(cell:vec2i)->vec4f{
|
|
21
|
+
${address("x", width, sampler.wrapU)}${address("y", height, sampler.wrapV)}
|
|
22
|
+
let element=u32(y)*${width}u+u32(x);
|
|
23
|
+
let row=surfaceMaterials[${row}u+element/8u];
|
|
24
|
+
switch(element%8u){
|
|
25
|
+
case 0u:{return row.base;}case 1u:{return row.factors;}
|
|
26
|
+
case 2u:{return row.emission;}case 3u:{return row.maps0;}
|
|
27
|
+
case 4u:{return row.maps1;}case 5u:{return row.normal;}
|
|
28
|
+
case 6u:{return row.physical;}default:{return row.attenuation;}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
fn ${helper}(uv:vec2f)->vec4f{
|
|
32
|
+
if(any((bitcast<vec2u>(uv)&vec2u(0x7f800000u))==vec2u(0x7f800000u))){return vec4f(0);}
|
|
33
|
+
let coord=vec2f(${wrap("uv.x", sampler.wrapU)},${wrap("uv.y", sampler.wrapV)})*vec2f(${width}.0,${height}.0);
|
|
34
|
+
${sampler.filter === "nearest"
|
|
35
|
+
? `return ${helper}Load(vec2i(floor(coord)));`
|
|
36
|
+
: `let pixel=coord-vec2f(0.5);let base=vec2i(floor(pixel));let weight=fract(pixel);
|
|
37
|
+
let a=${helper}Load(base);let b=${helper}Load(base+vec2i(1,0));
|
|
38
|
+
let c=${helper}Load(base+vec2i(0,1));let d=${helper}Load(base+vec2i(1,1));
|
|
39
|
+
let top=a*(1.0-weight.x)+b*weight.x;let bottom=c*(1.0-weight.x)+d*weight.x;
|
|
40
|
+
return top*(1.0-weight.y)+bottom*weight.y;`}
|
|
41
|
+
}`;
|
|
42
|
+
}
|
|
43
|
+
/** Private compiled switches share texture/material bindings with the surface evaluator. */
|
|
44
|
+
export function compileSurfaceGraphs(snapshot, bindings, maxMaterialBytes = Number.MAX_SAFE_INTEGER) {
|
|
45
|
+
const byUuid = new Map([...bindings].map(([material, binding]) => [material.uuid, binding]));
|
|
46
|
+
const rows = [], helpers = [], full = [], opacity = [];
|
|
47
|
+
const bufferCopies = [];
|
|
48
|
+
let bufferFloats = 0;
|
|
49
|
+
const put = (value) => {
|
|
50
|
+
const row = snapshot.materials.length + rows.length / 32, values = Array(32).fill(0);
|
|
51
|
+
if (typeof value === "number" || typeof value === "boolean")
|
|
52
|
+
values[0] = Number(value);
|
|
53
|
+
else
|
|
54
|
+
values.splice(0, value.length, ...value);
|
|
55
|
+
if (values.some((v) => !Number.isFinite(Math.fround(v))))
|
|
56
|
+
throw Error("TC_GRAPH_VALUE: Material parameters/time must fit finite f32.");
|
|
57
|
+
rows.push(...values);
|
|
58
|
+
return row;
|
|
59
|
+
};
|
|
60
|
+
// Allocate every parameter first so GPU ranges never overlap later materials' parameters.
|
|
61
|
+
const contexts = new Map();
|
|
62
|
+
for (const [index, material] of snapshot.materials.entries()) {
|
|
63
|
+
const binding = byUuid.get(material.uuid);
|
|
64
|
+
if (!binding || !Object.keys(binding.graphs).length)
|
|
65
|
+
continue;
|
|
66
|
+
const timed = Object.values(binding.graphs).some((graph) => graph.nodes.some((node) => node.op === "time"));
|
|
67
|
+
const time = put(timed ? binding.time : 0), parameterRows = new Map();
|
|
68
|
+
for (const graph of Object.values(binding.graphs))
|
|
69
|
+
for (const p of graph.parameters ?? [])
|
|
70
|
+
if (!parameterRows.has(p.name))
|
|
71
|
+
parameterRows.set(p.name, put(Object.hasOwn(binding.parameters, p.name)
|
|
72
|
+
? binding.parameters[p.name]
|
|
73
|
+
: p.default));
|
|
74
|
+
contexts.set(index, { time, parameterRows });
|
|
75
|
+
}
|
|
76
|
+
for (const [index, material] of snapshot.materials.entries()) {
|
|
77
|
+
const binding = byUuid.get(material.uuid), allocated = contexts.get(index);
|
|
78
|
+
if (!binding || !allocated)
|
|
79
|
+
continue;
|
|
80
|
+
const { time, parameterRows } = allocated;
|
|
81
|
+
const slots = new Map();
|
|
82
|
+
const textureSlots = new Map();
|
|
83
|
+
const textureSamplers = new Map();
|
|
84
|
+
const context = /* wgsl */ `let triangle=surfaceTriangle(index);let uv=triangle.ua*bary.x+triangle.ub*bary.y+triangle.uc*bary.z;let positionWorld=triangle.a.xyz*bary.x+triangle.b.xyz*bary.y+triangle.c.xyz*bary.z;let normalWorld=normalize(triangle.na.xyz*bary.x+triangle.nb.xyz*bary.y+triangle.nc.xyz*bary.z);let vertexColor=triangle.ca*bary.x+triangle.cb*bary.y+triangle.cc*bary.z;`;
|
|
85
|
+
const lower = (graph, channel) => lowerGraphWGSL(graph, {
|
|
86
|
+
prefix: "tcMaterial" + index + "_" + channel,
|
|
87
|
+
input: (name) => {
|
|
88
|
+
if (name === "time")
|
|
89
|
+
return "surfaceMaterials[" + time + "u].base.x";
|
|
90
|
+
if (name === "uv")
|
|
91
|
+
return "uv.xy";
|
|
92
|
+
if (name === "uv1")
|
|
93
|
+
return "uv.zw";
|
|
94
|
+
if (name === "viewDirectionWorld")
|
|
95
|
+
return "view";
|
|
96
|
+
if (["positionWorld", "normalWorld", "vertexColor"].includes(name))
|
|
97
|
+
return name;
|
|
98
|
+
throw Error("TC_GRAPH_CONTEXT: material " + binding.id + " input " + name);
|
|
99
|
+
},
|
|
100
|
+
parameter: (name, type) => {
|
|
101
|
+
const row = parameterRows.get(name);
|
|
102
|
+
if (row === undefined)
|
|
103
|
+
throw Error("TC_GRAPH_PARAMETER: " + binding.id + ":" + name);
|
|
104
|
+
const base = "surfaceMaterials[" + row + "u].base";
|
|
105
|
+
return type === "float"
|
|
106
|
+
? base + ".x"
|
|
107
|
+
: type === "bool"
|
|
108
|
+
? "(" + base + ".x != 0.0)"
|
|
109
|
+
: base + "." + "xyzw".slice(0, Number(type.slice(3)));
|
|
110
|
+
},
|
|
111
|
+
readBuffer: (name, element, type) => {
|
|
112
|
+
let slot = slots.get(name);
|
|
113
|
+
if (slot && slot.type !== type)
|
|
114
|
+
throw Error("TC_RENDER_BUFFER_TYPE: " + binding.id + ":" + name);
|
|
115
|
+
if (!slot) {
|
|
116
|
+
const descriptor = binding.buffers && Object.hasOwn(binding.buffers, name)
|
|
117
|
+
? binding.buffers[name]
|
|
118
|
+
: undefined;
|
|
119
|
+
if (!descriptor)
|
|
120
|
+
throw Error("TC_RENDER_BUFFER_UNBOUND: " + binding.id + ":" + name);
|
|
121
|
+
if (descriptor.type !== type)
|
|
122
|
+
throw Error("TC_RENDER_BUFFER_TYPE: " + binding.id + ":" + name);
|
|
123
|
+
const count = descriptor.values.length;
|
|
124
|
+
if (!Number.isSafeInteger(count) || count < 1 || count > 0xffffffff)
|
|
125
|
+
throw Error("TC_RENDER_BUFFER_COUNT: " + binding.id + ":" + name);
|
|
126
|
+
const row = snapshot.materials.length + (rows.length + bufferFloats) / 32;
|
|
127
|
+
const reserved = Math.ceil(count / 8) * 32;
|
|
128
|
+
if ((row * 32 + reserved) * 4 > maxMaterialBytes)
|
|
129
|
+
throw Error("TC_RENDER_BUFFER_LIMIT: Material table exceeds the device buffer limits.");
|
|
130
|
+
bufferFloats += reserved;
|
|
131
|
+
const helper = "tcMaterialBuffer" + index + "_" + slots.size;
|
|
132
|
+
slot = { helper, type };
|
|
133
|
+
slots.set(name, slot);
|
|
134
|
+
bufferCopies.push({
|
|
135
|
+
descriptor,
|
|
136
|
+
byteOffset: row * 128,
|
|
137
|
+
count,
|
|
138
|
+
type,
|
|
139
|
+
material: binding.id,
|
|
140
|
+
name,
|
|
141
|
+
});
|
|
142
|
+
const resultType = type === "float" ? "f32" : type === "bool" ? "bool" : type + "f";
|
|
143
|
+
const zero = type === "bool" ? "false" : resultType + "(0)";
|
|
144
|
+
const value = type === "bool"
|
|
145
|
+
? "(value.x != 0.0)"
|
|
146
|
+
: type === "float"
|
|
147
|
+
? "value.x"
|
|
148
|
+
: "value." + "xyzw".slice(0, Number(type.slice(3)));
|
|
149
|
+
helpers.push(/* wgsl */ `
|
|
150
|
+
fn ${helper}(index:f32)->${resultType}{
|
|
151
|
+
if ((bitcast<u32>(index)&0x7f800000u)==0x7f800000u){return ${zero};}
|
|
152
|
+
if (!(index>=0.0 && index<4294967296.0 && floor(index)==index)){return ${zero};}
|
|
153
|
+
let element=u32(index);if(element>=${count}u){return ${zero};}
|
|
154
|
+
let row=surfaceMaterials[${row}u+element/8u];var value=vec4f(0);
|
|
155
|
+
switch (element%8u){
|
|
156
|
+
case 0u:{value=row.base;}case 1u:{value=row.factors;}
|
|
157
|
+
case 2u:{value=row.emission;}case 3u:{value=row.maps0;}
|
|
158
|
+
case 4u:{value=row.maps1;}case 5u:{value=row.normal;}
|
|
159
|
+
case 6u:{value=row.physical;}default:{value=row.attenuation;}
|
|
160
|
+
}
|
|
161
|
+
return ${value};
|
|
162
|
+
}`);
|
|
163
|
+
}
|
|
164
|
+
return slot.helper + "(" + element + ")";
|
|
165
|
+
},
|
|
166
|
+
sampleTexture: (asset, uv) => {
|
|
167
|
+
const sampler = graph.textureSlots && Object.hasOwn(graph.textureSlots, asset)
|
|
168
|
+
? graph.textureSlots[asset]
|
|
169
|
+
: undefined;
|
|
170
|
+
if (sampler) {
|
|
171
|
+
let slot = textureSlots.get(asset);
|
|
172
|
+
if (!slot) {
|
|
173
|
+
const descriptor = binding.managedTextures &&
|
|
174
|
+
Object.hasOwn(binding.managedTextures, asset)
|
|
175
|
+
? binding.managedTextures[asset]
|
|
176
|
+
: undefined;
|
|
177
|
+
if (!descriptor)
|
|
178
|
+
throw Error("TC_RENDER_TEXTURE_UNBOUND: " + binding.id + ":" + asset);
|
|
179
|
+
const shape = descriptor.texture, count = descriptor.values.length;
|
|
180
|
+
if (descriptor.type !== "vec4" ||
|
|
181
|
+
!shape ||
|
|
182
|
+
!Number.isSafeInteger(shape.width) ||
|
|
183
|
+
!Number.isSafeInteger(shape.height) ||
|
|
184
|
+
shape.width < 1 ||
|
|
185
|
+
shape.height < 1 ||
|
|
186
|
+
!Number.isSafeInteger(count) ||
|
|
187
|
+
count < 1 ||
|
|
188
|
+
count > 0xffffffff ||
|
|
189
|
+
shape.width * shape.height !== count)
|
|
190
|
+
throw Error("TC_RENDER_TEXTURE_RESOURCE: " + binding.id + ":" + asset);
|
|
191
|
+
const row = snapshot.materials.length + (rows.length + bufferFloats) / 32;
|
|
192
|
+
const reserved = Math.ceil(count / 8) * 32;
|
|
193
|
+
if ((row * 32 + reserved) * 4 > maxMaterialBytes)
|
|
194
|
+
throw Error("TC_RENDER_TEXTURE_LIMIT: Material table exceeds the device buffer limits.");
|
|
195
|
+
bufferFloats += reserved;
|
|
196
|
+
slot = { row, width: shape.width, height: shape.height };
|
|
197
|
+
textureSlots.set(asset, slot);
|
|
198
|
+
bufferCopies.push({
|
|
199
|
+
descriptor,
|
|
200
|
+
byteOffset: row * 128,
|
|
201
|
+
count,
|
|
202
|
+
type: "vec4",
|
|
203
|
+
material: binding.id,
|
|
204
|
+
name: asset,
|
|
205
|
+
texture: { width: shape.width, height: shape.height },
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
const signature = JSON.stringify([
|
|
209
|
+
asset,
|
|
210
|
+
sampler.filter ?? "linear",
|
|
211
|
+
sampler.wrapU ?? "clamp",
|
|
212
|
+
sampler.wrapV ?? "clamp",
|
|
213
|
+
]);
|
|
214
|
+
let helper = textureSamplers.get(signature);
|
|
215
|
+
if (!helper) {
|
|
216
|
+
helper = "tcMaterialTexture" + index + "_" + textureSamplers.size;
|
|
217
|
+
textureSamplers.set(signature, helper);
|
|
218
|
+
helpers.push(textureHelpers(helper, slot.row, slot.width, slot.height, sampler));
|
|
219
|
+
}
|
|
220
|
+
return helper + "(" + uv + ")";
|
|
221
|
+
}
|
|
222
|
+
const source = binding.textures.get(asset);
|
|
223
|
+
const id = snapshot.textures.findIndex((texture) => texture.uuid === source?.uuid);
|
|
224
|
+
if (id < 0)
|
|
225
|
+
throw Error("TC_GRAPH_TEXTURE: " + binding.id + ":" + asset);
|
|
226
|
+
return "surfaceMap(" + id + ".0,vec4f(" + uv + "," + uv + "))";
|
|
227
|
+
},
|
|
228
|
+
});
|
|
229
|
+
const statements = [];
|
|
230
|
+
for (const [channel, graph] of Object.entries(binding.graphs)) {
|
|
231
|
+
const g = lower(graph, channel);
|
|
232
|
+
helpers.push(g.helpers);
|
|
233
|
+
statements.push(g.code);
|
|
234
|
+
const value = g.result;
|
|
235
|
+
if (channel === "baseColor")
|
|
236
|
+
statements.push("result.base=" +
|
|
237
|
+
value +
|
|
238
|
+
"*vec3f(triangle.b.w,triangle.c.w,triangle.na.w);if(triangle.nb.w>0.5){result.base*=vertexColor.rgb;}");
|
|
239
|
+
else if (channel === "emissive")
|
|
240
|
+
statements.push("result.emissive=" + value + ";");
|
|
241
|
+
else if (channel === "normalWorld")
|
|
242
|
+
statements.push("result.normal=surfaceGraphNormal(" +
|
|
243
|
+
value +
|
|
244
|
+
",normalWorld)*surfaceNormalSign(surfaceMaterials[u32(triangle.a.w)],front);");
|
|
245
|
+
else if (channel === "opacity")
|
|
246
|
+
statements.push("result.alpha*=clamp(" + value + ",0.0,1.0);");
|
|
247
|
+
else
|
|
248
|
+
statements.push("result." +
|
|
249
|
+
(channel === "opacity" ? "alpha" : channel) +
|
|
250
|
+
"=clamp(" +
|
|
251
|
+
value +
|
|
252
|
+
",0.0,1.0);");
|
|
253
|
+
if (channel === "opacity") {
|
|
254
|
+
const alpha = lower(graph, "coverage");
|
|
255
|
+
helpers.push(alpha.helpers);
|
|
256
|
+
opacity.push("case " +
|
|
257
|
+
index +
|
|
258
|
+
"u:{" +
|
|
259
|
+
context +
|
|
260
|
+
"let view=vec3f(0);" +
|
|
261
|
+
alpha.code +
|
|
262
|
+
"return vec2f(clamp(" +
|
|
263
|
+
alpha.result +
|
|
264
|
+
",0.0,1.0),1);}");
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
full.push("case " + index + "u:{" + context + statements.join("\n") + "}");
|
|
268
|
+
}
|
|
269
|
+
const source = full.length
|
|
270
|
+
? helpers.join("\n") +
|
|
271
|
+
/* wgsl */ `
|
|
272
|
+
fn surfaceGraphOpacity(index:u32,bary:vec3f,front:bool)->vec2f{
|
|
273
|
+
switch u32(surfaceTriangle(index).a.w){${opacity.join("\n")}default:{}}return vec2f(0);
|
|
274
|
+
}
|
|
275
|
+
fn surfaceGraphApply(index:u32,bary:vec3f,front:bool,view:vec3f,value:SurfaceValue)->SurfaceValue{
|
|
276
|
+
var result=value;switch u32(surfaceTriangle(index).a.w){${full.join("\n")}default:{}}return result;
|
|
277
|
+
}
|
|
278
|
+
`
|
|
279
|
+
: DEFAULT_SURFACE_GRAPHS;
|
|
280
|
+
const tableRows = new Float32Array(rows.length + bufferFloats);
|
|
281
|
+
tableRows.set(rows);
|
|
282
|
+
return { source, rows: tableRows, materials: full.length, bufferCopies };
|
|
283
|
+
}
|
|
284
|
+
/** Proposal only: every graph emitter needs support, even with a black standard emissive. */
|
|
285
|
+
export function graphEmissiveProposal(snapshot, bindings) {
|
|
286
|
+
const emitters = new Set([...bindings]
|
|
287
|
+
.filter(([, binding]) => binding.graphs.emissive)
|
|
288
|
+
.map(([material]) => material.uuid));
|
|
289
|
+
if (!emitters.size)
|
|
290
|
+
return snapshot;
|
|
291
|
+
return {
|
|
292
|
+
...snapshot,
|
|
293
|
+
materials: snapshot.materials.map((material) => emitters.has(material.uuid)
|
|
294
|
+
? { ...material, emissive: [1, 1, 1], emissiveIntensity: 1 }
|
|
295
|
+
: material),
|
|
296
|
+
};
|
|
297
|
+
}
|