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,485 @@
|
|
|
1
|
+
import { GRAPH_INPUT_TYPES, GRAPH_OPERATIONS, GRAPH_TYPES } from "../sdk/index.js";
|
|
2
|
+
const own = (object, key) => Object.prototype.hasOwnProperty.call(object, key);
|
|
3
|
+
function fail(code, id) {
|
|
4
|
+
throw new Error(`TC_GRAPH_${code}:${id}`);
|
|
5
|
+
}
|
|
6
|
+
const types = GRAPH_TYPES;
|
|
7
|
+
const numeric = (type) => type !== "bool";
|
|
8
|
+
const width = (type) => (type.startsWith("vec") ? Number(type[3]) : 1);
|
|
9
|
+
const unary = new Set(GRAPH_OPERATIONS.unary);
|
|
10
|
+
const binary = new Set(GRAPH_OPERATIONS.binary);
|
|
11
|
+
const ternary = new Set(GRAPH_OPERATIONS.ternary);
|
|
12
|
+
function valueChecked(value, type, id) {
|
|
13
|
+
const finite = (n) => typeof n === "number" && Number.isFinite(n) && Number.isFinite(Math.fround(n));
|
|
14
|
+
if (type === "bool" && typeof value === "boolean")
|
|
15
|
+
return value;
|
|
16
|
+
if (type === "float" && finite(value))
|
|
17
|
+
return value;
|
|
18
|
+
if (type.startsWith("vec") && Array.isArray(value) && value.length === width(type) && Array.from(value).every(finite))
|
|
19
|
+
return [...value];
|
|
20
|
+
return fail("VALUE", id);
|
|
21
|
+
}
|
|
22
|
+
function broadcast(types, id) {
|
|
23
|
+
const vector = types.find((t) => width(t) > 1) ?? "float";
|
|
24
|
+
if (types.some((t) => !numeric(t) || (t !== "float" && t !== vector)))
|
|
25
|
+
fail("TYPE", id);
|
|
26
|
+
return vector;
|
|
27
|
+
}
|
|
28
|
+
function swizzle(node, type) {
|
|
29
|
+
const mask = node.components;
|
|
30
|
+
if (width(type) === 1 || typeof mask !== "string" || !/^(?:[xyzw]{1,4}|[rgba]{1,4})$/.test(mask))
|
|
31
|
+
fail("TYPE", node.id);
|
|
32
|
+
const family = /^[rgba]+$/.test(mask) ? "rgba" : "xyzw";
|
|
33
|
+
const indices = [...mask].map((c) => family.indexOf(c));
|
|
34
|
+
if (indices.some((i) => i >= width(type)))
|
|
35
|
+
fail("TYPE", node.id);
|
|
36
|
+
return indices.map((i) => "xyzw"[i]).join("");
|
|
37
|
+
}
|
|
38
|
+
function operationType(node, args) {
|
|
39
|
+
const op = node.op, id = node.id;
|
|
40
|
+
const require = (condition, result) => (condition ? result : fail("TYPE", id));
|
|
41
|
+
const a = args[0], b = args[1];
|
|
42
|
+
if (op === "sampleTexture")
|
|
43
|
+
return require(a === "vec2", "vec4");
|
|
44
|
+
if (op === "swizzle") {
|
|
45
|
+
const n = swizzle(node, a).length;
|
|
46
|
+
return n === 1 ? "float" : `vec${n}`;
|
|
47
|
+
}
|
|
48
|
+
if (op.startsWith("compose"))
|
|
49
|
+
return require(args.every((t) => t === "float"), `vec${args.length}`);
|
|
50
|
+
if (op === "not")
|
|
51
|
+
return require(a === "bool", "bool");
|
|
52
|
+
if (op === "and" || op === "or")
|
|
53
|
+
return require(a === "bool" && b === "bool", "bool");
|
|
54
|
+
if (["lessThan", "lessEqual", "greaterThan", "greaterEqual"].includes(op))
|
|
55
|
+
return require(a === "float" && b === "float", "bool");
|
|
56
|
+
if (op === "equal")
|
|
57
|
+
return require(a === b && (a === "float" || a === "bool"), "bool");
|
|
58
|
+
if (op === "select")
|
|
59
|
+
return require(a === b && args[2] === "bool", a);
|
|
60
|
+
if (op === "dot")
|
|
61
|
+
return require(a === b && width(a) > 1, "float");
|
|
62
|
+
if (op === "cross")
|
|
63
|
+
return require(a === "vec3" && b === "vec3", "vec3");
|
|
64
|
+
if (op === "normalize")
|
|
65
|
+
return require(width(a) > 1, a);
|
|
66
|
+
if (op === "length")
|
|
67
|
+
return require(numeric(a), "float");
|
|
68
|
+
if (op === "add" || op === "subtract")
|
|
69
|
+
return require(a === b && numeric(a), a);
|
|
70
|
+
if (op === "mix")
|
|
71
|
+
return require(a === b && numeric(a) && (args[2] === "float" || args[2] === a), a);
|
|
72
|
+
if (unary.has(op))
|
|
73
|
+
return require(numeric(a), a);
|
|
74
|
+
if (binary.has(op) || op === "clamp" || op === "smoothstep")
|
|
75
|
+
return broadcast(args, id);
|
|
76
|
+
return fail("OP", id);
|
|
77
|
+
}
|
|
78
|
+
/** Renderer-private traversal. Only output-reachable nodes invoke builder callbacks. */
|
|
79
|
+
export function mapTypedGraph(graph, builder) {
|
|
80
|
+
if (!graph || !Array.isArray(graph.nodes) || !graph.nodeTypes || typeof graph.output !== "string")
|
|
81
|
+
fail("IR", "graph");
|
|
82
|
+
const nodes = new Map(), values = new Map(), active = new Set();
|
|
83
|
+
if (graph.parameters !== undefined &&
|
|
84
|
+
(!Array.isArray(graph.parameters) || Array.from(graph.parameters).some((p) => !p || typeof p.name !== "string")))
|
|
85
|
+
fail("IR", "parameters");
|
|
86
|
+
const parameters = new Map((graph.parameters ?? []).map((p) => [p.name, p]));
|
|
87
|
+
if (parameters.size !== (graph.parameters?.length ?? 0))
|
|
88
|
+
fail("IR", "parameters");
|
|
89
|
+
for (const node of graph.nodes) {
|
|
90
|
+
if (!node || typeof node.id !== "string" || nodes.has(node.id))
|
|
91
|
+
fail("IR", node?.id ?? "graph");
|
|
92
|
+
nodes.set(node.id, node);
|
|
93
|
+
}
|
|
94
|
+
const typeOf = (id) => {
|
|
95
|
+
if (!nodes.has(id))
|
|
96
|
+
fail("REF", id);
|
|
97
|
+
const type = graph.nodeTypes[id];
|
|
98
|
+
if (!own(graph.nodeTypes, id) || !types.includes(type))
|
|
99
|
+
fail("TYPE", id);
|
|
100
|
+
return type;
|
|
101
|
+
};
|
|
102
|
+
const stack = [{ id: graph.output, ready: false }];
|
|
103
|
+
if (graph.outputType !== typeOf(graph.output))
|
|
104
|
+
fail("TYPE", graph.output);
|
|
105
|
+
while (stack.length) {
|
|
106
|
+
const { id, ready } = stack.pop();
|
|
107
|
+
if (values.has(id))
|
|
108
|
+
continue;
|
|
109
|
+
const type = typeOf(id), node = nodes.get(id);
|
|
110
|
+
const checked = (expected) => (expected === type ? expected : fail("TYPE", id));
|
|
111
|
+
const refs = "inputs" in node ? node.inputs : [];
|
|
112
|
+
if (!Array.isArray(refs) || refs.some((ref) => typeof ref !== "string"))
|
|
113
|
+
fail("REF", id);
|
|
114
|
+
if (!ready) {
|
|
115
|
+
if (active.has(id))
|
|
116
|
+
fail("CYCLE", id);
|
|
117
|
+
active.add(id);
|
|
118
|
+
stack.push({ id, ready: true });
|
|
119
|
+
for (let i = refs.length - 1; i >= 0; i--)
|
|
120
|
+
stack.push({ id: refs[i], ready: false });
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
let result, expected;
|
|
124
|
+
if (node.op === "float" || node.op === "bool" || node.op === "vec2" || node.op === "vec3" || node.op === "vec4") {
|
|
125
|
+
expected = checked(node.op);
|
|
126
|
+
result = builder.constant(valueChecked(node.value, expected, id), expected, node);
|
|
127
|
+
}
|
|
128
|
+
else if (node.op === "time" || node.op === "input") {
|
|
129
|
+
const name = node.op === "time" ? "time" : node.name;
|
|
130
|
+
if (node.op === "input" && !own(GRAPH_INPUT_TYPES, node.name))
|
|
131
|
+
fail("CONTEXT", id);
|
|
132
|
+
expected = checked(name === "time" ? "float" : GRAPH_INPUT_TYPES[name]);
|
|
133
|
+
result = builder.input(name, expected, node);
|
|
134
|
+
}
|
|
135
|
+
else if (node.op === "parameter") {
|
|
136
|
+
const parameter = parameters.get(node.name);
|
|
137
|
+
if (!parameter || !types.includes(parameter.type))
|
|
138
|
+
fail("CONTEXT", id);
|
|
139
|
+
expected = checked(parameter.type);
|
|
140
|
+
result = builder.parameter(node.name, expected, valueChecked(parameter.default, expected, id), node);
|
|
141
|
+
}
|
|
142
|
+
else if (node.op === "sampleFrame" || node.op === "sampleHistory") {
|
|
143
|
+
if (refs.length !== 1 || typeOf(refs[0]) !== "vec2" || typeof node.channel !== "string" || !node.channel.length)
|
|
144
|
+
fail("TYPE", id);
|
|
145
|
+
expected = checked("vec4");
|
|
146
|
+
const callback = node.op === "sampleFrame" ? builder.frame : builder.history;
|
|
147
|
+
if (!callback)
|
|
148
|
+
fail("CONTEXT", id);
|
|
149
|
+
result = callback(node.channel, values.get(refs[0]), node);
|
|
150
|
+
}
|
|
151
|
+
else if (node.op === "readBuffer") {
|
|
152
|
+
if (refs.length !== 1 || typeOf(refs[0]) !== "float" || !graph.bufferTypes || !own(graph.bufferTypes, node.buffer) || !types.includes(graph.bufferTypes[node.buffer]))
|
|
153
|
+
fail("TYPE", id);
|
|
154
|
+
expected = checked(graph.bufferTypes[node.buffer]);
|
|
155
|
+
if (!builder.buffer)
|
|
156
|
+
fail("CONTEXT", id);
|
|
157
|
+
result = builder.buffer(node.buffer, values.get(refs[0]), expected, node);
|
|
158
|
+
}
|
|
159
|
+
else {
|
|
160
|
+
const op = node.op;
|
|
161
|
+
const count = unary.has(op) || op === "not" || op === "swizzle" || op === "sampleTexture"
|
|
162
|
+
? 1
|
|
163
|
+
: binary.has(op) || op === "compose2"
|
|
164
|
+
? 2
|
|
165
|
+
: ternary.has(op) || op === "compose3"
|
|
166
|
+
? 3
|
|
167
|
+
: op === "compose4"
|
|
168
|
+
? 4
|
|
169
|
+
: fail("OP", id);
|
|
170
|
+
if (refs.length !== count)
|
|
171
|
+
fail("TYPE", id);
|
|
172
|
+
const inputTypes = refs.map(typeOf);
|
|
173
|
+
expected = checked(operationType(node, inputTypes));
|
|
174
|
+
if (node.op === "sampleTexture") {
|
|
175
|
+
const asset = typeof node.texture === "string" ? node.texture : node.texture?.id;
|
|
176
|
+
if (typeof asset !== "string" || !asset.length)
|
|
177
|
+
fail("CONTEXT", id);
|
|
178
|
+
result = builder.texture(asset, values.get(refs[0]), node);
|
|
179
|
+
}
|
|
180
|
+
else
|
|
181
|
+
result = builder.operation(node, refs.map((ref) => values.get(ref)), expected, inputTypes);
|
|
182
|
+
}
|
|
183
|
+
values.set(id, result);
|
|
184
|
+
active.delete(id);
|
|
185
|
+
}
|
|
186
|
+
return values.get(graph.output);
|
|
187
|
+
}
|
|
188
|
+
const components = (v) => (typeof v === "number" ? [v] : v);
|
|
189
|
+
function arithmetic(node, args, type) {
|
|
190
|
+
const [a, b, c] = args, op = node.op;
|
|
191
|
+
const aa = components(a), bb = components(b);
|
|
192
|
+
const componentwise = (fn) => {
|
|
193
|
+
const result = Array.from({ length: width(type) }, (_, i) => fn(...args.map((v) => {
|
|
194
|
+
const a = components(v);
|
|
195
|
+
return a[a.length === 1 ? 0 : i];
|
|
196
|
+
})));
|
|
197
|
+
return type === "float" ? result[0] : result;
|
|
198
|
+
};
|
|
199
|
+
const clamp = (v, lo, hi) => Math.min(Math.max(v, lo), hi);
|
|
200
|
+
switch (op) {
|
|
201
|
+
case "not":
|
|
202
|
+
return !a;
|
|
203
|
+
case "and":
|
|
204
|
+
return Boolean(a && b);
|
|
205
|
+
case "or":
|
|
206
|
+
return Boolean(a || b);
|
|
207
|
+
case "equal":
|
|
208
|
+
return a === b;
|
|
209
|
+
case "lessThan":
|
|
210
|
+
return a < b;
|
|
211
|
+
case "lessEqual":
|
|
212
|
+
return a <= b;
|
|
213
|
+
case "greaterThan":
|
|
214
|
+
return a > b;
|
|
215
|
+
case "greaterEqual":
|
|
216
|
+
return a >= b;
|
|
217
|
+
case "select":
|
|
218
|
+
return c ? b : a;
|
|
219
|
+
case "compose2":
|
|
220
|
+
case "compose3":
|
|
221
|
+
case "compose4":
|
|
222
|
+
return [...args];
|
|
223
|
+
case "swizzle": {
|
|
224
|
+
const v = [...swizzle(node, `vec${aa.length}`)].map((c) => aa["xyzw".indexOf(c)]);
|
|
225
|
+
return v.length === 1 ? v[0] : v;
|
|
226
|
+
}
|
|
227
|
+
case "length":
|
|
228
|
+
return Math.hypot(...aa);
|
|
229
|
+
case "normalize": {
|
|
230
|
+
const scale = Math.max(...aa.map(Math.abs));
|
|
231
|
+
const v = scale === 0 ? aa.map(() => 0) : aa.map((n) => n / scale);
|
|
232
|
+
const len = Math.hypot(...v);
|
|
233
|
+
return v.map((n) => (len === 0 ? 0 : n / len));
|
|
234
|
+
}
|
|
235
|
+
case "dot":
|
|
236
|
+
return aa.reduce((sum, n, i) => sum + n * bb[i], 0);
|
|
237
|
+
case "cross":
|
|
238
|
+
return [aa[1] * bb[2] - aa[2] * bb[1], aa[2] * bb[0] - aa[0] * bb[2], aa[0] * bb[1] - aa[1] * bb[0]];
|
|
239
|
+
case "add":
|
|
240
|
+
return componentwise((x, y) => x + y);
|
|
241
|
+
case "subtract":
|
|
242
|
+
return componentwise((x, y) => x - y);
|
|
243
|
+
case "multiply":
|
|
244
|
+
return componentwise((x, y) => x * y);
|
|
245
|
+
case "safeDivide":
|
|
246
|
+
return componentwise((x, y) => (y === 0 ? 0 : x / y));
|
|
247
|
+
case "min":
|
|
248
|
+
return componentwise(Math.min);
|
|
249
|
+
case "max":
|
|
250
|
+
return componentwise(Math.max);
|
|
251
|
+
case "step":
|
|
252
|
+
return componentwise((edge, v) => (v < edge ? 0 : 1));
|
|
253
|
+
case "mix":
|
|
254
|
+
return componentwise((x, y, t) => x * (1 - t) + y * t);
|
|
255
|
+
case "clamp":
|
|
256
|
+
return componentwise(clamp);
|
|
257
|
+
case "smoothstep":
|
|
258
|
+
return componentwise((lo, hi, v) => {
|
|
259
|
+
const t = clamp(hi === lo ? 0 : (v - lo) / (hi - lo), 0, 1);
|
|
260
|
+
return t * t * (3 - 2 * t);
|
|
261
|
+
});
|
|
262
|
+
case "sin":
|
|
263
|
+
return componentwise(Math.sin);
|
|
264
|
+
case "cos":
|
|
265
|
+
return componentwise(Math.cos);
|
|
266
|
+
case "abs":
|
|
267
|
+
return componentwise(Math.abs);
|
|
268
|
+
case "floor":
|
|
269
|
+
return componentwise(Math.floor);
|
|
270
|
+
case "ceil":
|
|
271
|
+
return componentwise(Math.ceil);
|
|
272
|
+
case "fract":
|
|
273
|
+
return componentwise((x) => x - Math.floor(x));
|
|
274
|
+
case "negate":
|
|
275
|
+
return componentwise((x) => -x);
|
|
276
|
+
case "saturate":
|
|
277
|
+
return componentwise((x) => clamp(x, 0, 1));
|
|
278
|
+
case "safeSqrt":
|
|
279
|
+
return componentwise((x) => Math.sqrt(Math.max(x, 0)));
|
|
280
|
+
default:
|
|
281
|
+
return fail("OP", node.id);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
/** f64 intermediates intentionally preserve legacy CPU evaluation. */
|
|
285
|
+
export function evaluateTypedGraph(graph, context = {}) {
|
|
286
|
+
if (!context || typeof context !== "object" || Array.isArray(context))
|
|
287
|
+
fail("CONTEXT", "graph");
|
|
288
|
+
if (context.precision !== undefined && context.precision !== "f32" && context.precision !== "f64")
|
|
289
|
+
fail("CONTEXT", "precision");
|
|
290
|
+
const round = (value, id) => {
|
|
291
|
+
if (context.precision !== "f32" || typeof value === "boolean")
|
|
292
|
+
return value;
|
|
293
|
+
const rounded = typeof value === "number" ? Math.fround(value) : value.map(Math.fround);
|
|
294
|
+
if (typeof rounded !== "boolean" && components(rounded).some(number => !Number.isFinite(number)))
|
|
295
|
+
fail("ARITHMETIC", id);
|
|
296
|
+
return rounded;
|
|
297
|
+
};
|
|
298
|
+
for (const record of [context.parameters, context.inputs]) {
|
|
299
|
+
if (record !== undefined && (!record || typeof record !== "object" || Array.isArray(record)))
|
|
300
|
+
fail("CONTEXT", "graph");
|
|
301
|
+
}
|
|
302
|
+
const result = mapTypedGraph(graph, {
|
|
303
|
+
constant: (value, _type, node) => round(value, node.id),
|
|
304
|
+
input: (name, type, node) => {
|
|
305
|
+
const value = name === "time"
|
|
306
|
+
? context.time === undefined
|
|
307
|
+
? 0
|
|
308
|
+
: context.time
|
|
309
|
+
: context.inputs && own(context.inputs, name)
|
|
310
|
+
? context.inputs[name]
|
|
311
|
+
: undefined;
|
|
312
|
+
if (value === undefined)
|
|
313
|
+
fail("CONTEXT", node.id);
|
|
314
|
+
return round(valueChecked(value, type, node.id), node.id);
|
|
315
|
+
},
|
|
316
|
+
parameter: (name, type, fallback, node) => round(valueChecked(context.parameters && own(context.parameters, name) ? context.parameters[name] : fallback, type, node.id), node.id),
|
|
317
|
+
texture: (asset, uv, node) => {
|
|
318
|
+
if (typeof context.sampleTexture !== "function")
|
|
319
|
+
fail("CONTEXT", node.id);
|
|
320
|
+
return round(valueChecked(context.sampleTexture(asset, [...uv]), "vec4", node.id), node.id);
|
|
321
|
+
},
|
|
322
|
+
buffer: (name, index, type, node) => {
|
|
323
|
+
if (typeof context.readBuffer !== "function" || typeof index !== "number")
|
|
324
|
+
fail("CONTEXT", node.id);
|
|
325
|
+
return round(valueChecked(context.readBuffer(name, index), type, node.id), node.id);
|
|
326
|
+
},
|
|
327
|
+
frame: (channel, uv, node) => {
|
|
328
|
+
if (!context.sampleFrame)
|
|
329
|
+
fail("CONTEXT", node.id);
|
|
330
|
+
return round(valueChecked(context.sampleFrame(channel, [...uv]), "vec4", node.id), node.id);
|
|
331
|
+
},
|
|
332
|
+
history: (channel, uv, node) => {
|
|
333
|
+
if (!context.sampleHistory)
|
|
334
|
+
fail("CONTEXT", node.id);
|
|
335
|
+
return round(valueChecked(context.sampleHistory(channel, [...uv]), "vec4", node.id), node.id);
|
|
336
|
+
},
|
|
337
|
+
operation: (node, inputs, type) => {
|
|
338
|
+
const value = arithmetic(node, inputs, type);
|
|
339
|
+
if (typeof value !== "boolean" && components(value).some((n) => !Number.isFinite(n)))
|
|
340
|
+
fail("ARITHMETIC", node.id);
|
|
341
|
+
return round(value, node.id);
|
|
342
|
+
},
|
|
343
|
+
});
|
|
344
|
+
return Array.isArray(result) ? [...result] : result;
|
|
345
|
+
}
|
|
346
|
+
const wgslType = (type) => (type === "float" ? "f32" : type === "bool" ? "bool" : `${type}f`);
|
|
347
|
+
const literal = (value) => (Object.is(value, -0) ? "-0.0" : /[.e]/i.test(String(value)) ? String(value) : `${value}.0`);
|
|
348
|
+
/** Bindings contain trusted engine code, never SDK/user-provided shader source. */
|
|
349
|
+
export function lowerGraphWGSL(graph, bindings) {
|
|
350
|
+
if (!bindings || typeof bindings.prefix !== "string" || !/^[A-Za-z][A-Za-z0-9_]*$/.test(bindings.prefix))
|
|
351
|
+
fail("PREFIX", "graph");
|
|
352
|
+
const lines = [], helpers = new Map(), prefix = bindings.prefix;
|
|
353
|
+
const emit = (expression, type) => {
|
|
354
|
+
const symbol = `${prefix}_g${lines.length}`;
|
|
355
|
+
lines.push(`let ${symbol}: ${wgslType(type)} = ${expression};`);
|
|
356
|
+
return symbol;
|
|
357
|
+
};
|
|
358
|
+
const binding = (callback, args, id) => {
|
|
359
|
+
if (typeof callback !== "function")
|
|
360
|
+
fail("CONTEXT", id);
|
|
361
|
+
const result = callback(...args);
|
|
362
|
+
return typeof result === "string" && result.trim().length ? result : fail("CONTEXT", id);
|
|
363
|
+
};
|
|
364
|
+
const helper = (kind, type) => {
|
|
365
|
+
const name = `${prefix}_${kind}_${type}`, t = wgslType(type), zero = `${t}(0.0)`, one = `${t}(1.0)`;
|
|
366
|
+
if (!helpers.has(name)) {
|
|
367
|
+
if (kind === "safeDivide")
|
|
368
|
+
helpers.set(name, `fn ${name}(a: ${t}, b: ${t}) -> ${t} {\n let zero = b == ${zero};\n let denominator = select(b, ${one}, zero);\n return select(a / denominator, ${zero}, zero);\n}`);
|
|
369
|
+
else {
|
|
370
|
+
const maximum = Array.from({ length: width(type) }, (_, i) => `abs(v.${"xyzw"[i]})`).reduce((a, b) => `max(${a}, ${b})`);
|
|
371
|
+
helpers.set(name, `fn ${name}(v: ${t}) -> ${t} {\n let scale = ${maximum};\n if (scale == 0.0) { return ${zero}; }\n let scaled = v / scale;\n return scaled / length(scaled);\n}`);
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
return name;
|
|
375
|
+
};
|
|
376
|
+
const result = mapTypedGraph(graph, {
|
|
377
|
+
constant: (value, type) => emit(typeof value === "boolean"
|
|
378
|
+
? String(value)
|
|
379
|
+
: typeof value === "number"
|
|
380
|
+
? literal(value)
|
|
381
|
+
: `${wgslType(type)}(${value.map(literal).join(", ")})`, type),
|
|
382
|
+
input: (name, type, node) => emit(binding(bindings.input, [name, type], node.id), type),
|
|
383
|
+
parameter: (name, type, _fallback, node) => emit(binding(bindings.parameter, [name, type], node.id), type),
|
|
384
|
+
texture: (asset, uv, node) => emit(binding(bindings.sampleTexture, [asset, uv], node.id), "vec4"),
|
|
385
|
+
buffer: (name, index, type, node) => {
|
|
386
|
+
if (!bindings.readBuffer)
|
|
387
|
+
fail("CONTEXT", node.id);
|
|
388
|
+
return emit(binding(bindings.readBuffer, [name, index, type], node.id), type);
|
|
389
|
+
},
|
|
390
|
+
frame: (channel, uv, node) => {
|
|
391
|
+
if (!bindings.sampleFrame)
|
|
392
|
+
fail("CONTEXT", node.id);
|
|
393
|
+
return emit(binding(bindings.sampleFrame, [channel, uv], node.id), "vec4");
|
|
394
|
+
},
|
|
395
|
+
history: (channel, uv, node) => {
|
|
396
|
+
if (!bindings.sampleHistory)
|
|
397
|
+
fail("CONTEXT", node.id);
|
|
398
|
+
return emit(binding(bindings.sampleHistory, [channel, uv], node.id), "vec4");
|
|
399
|
+
},
|
|
400
|
+
operation: (node, inputs, type, inputTypes) => {
|
|
401
|
+
const op = node.op, t = wgslType(type);
|
|
402
|
+
const cast = (symbol, index) => (inputTypes[index] === "float" && width(type) > 1 ? `${t}(${symbol})` : symbol);
|
|
403
|
+
const [a, b, c] = inputs.map(cast);
|
|
404
|
+
let expression;
|
|
405
|
+
switch (op) {
|
|
406
|
+
case "add":
|
|
407
|
+
case "subtract":
|
|
408
|
+
case "multiply":
|
|
409
|
+
expression = `(${a} ${{ add: "+", subtract: "-", multiply: "*" }[op]} ${b})`;
|
|
410
|
+
break;
|
|
411
|
+
case "lessThan":
|
|
412
|
+
case "lessEqual":
|
|
413
|
+
case "greaterThan":
|
|
414
|
+
case "greaterEqual":
|
|
415
|
+
case "equal":
|
|
416
|
+
case "and":
|
|
417
|
+
case "or": {
|
|
418
|
+
const operator = { lessThan: "<", lessEqual: "<=", greaterThan: ">", greaterEqual: ">=", equal: "==", and: "&&", or: "||" }[op];
|
|
419
|
+
expression = `(${a} ${operator} ${b})`;
|
|
420
|
+
break;
|
|
421
|
+
}
|
|
422
|
+
case "not":
|
|
423
|
+
expression = `(!${a})`;
|
|
424
|
+
break;
|
|
425
|
+
case "negate":
|
|
426
|
+
expression = `(-${a})`;
|
|
427
|
+
break;
|
|
428
|
+
case "select":
|
|
429
|
+
expression = `select(${a}, ${b}, ${inputs[2]})`;
|
|
430
|
+
break;
|
|
431
|
+
case "compose2":
|
|
432
|
+
case "compose3":
|
|
433
|
+
case "compose4":
|
|
434
|
+
expression = `${t}(${inputs.join(", ")})`;
|
|
435
|
+
break;
|
|
436
|
+
case "swizzle":
|
|
437
|
+
expression = `${inputs[0]}.${swizzle(node, inputTypes[0])}`;
|
|
438
|
+
break;
|
|
439
|
+
case "safeDivide":
|
|
440
|
+
expression = `${helper("safeDivide", type)}(${a}, ${b})`;
|
|
441
|
+
break;
|
|
442
|
+
case "normalize":
|
|
443
|
+
expression = `${helper("safeNormalize", type)}(${a})`;
|
|
444
|
+
break;
|
|
445
|
+
case "safeSqrt":
|
|
446
|
+
expression = `sqrt(max(${a}, ${t}(0.0)))`;
|
|
447
|
+
break;
|
|
448
|
+
case "saturate":
|
|
449
|
+
expression = `clamp(${a}, ${t}(0.0), ${t}(1.0))`;
|
|
450
|
+
break;
|
|
451
|
+
case "length":
|
|
452
|
+
expression = inputTypes[0] === "float" ? `abs(${a})` : `length(${a})`;
|
|
453
|
+
break;
|
|
454
|
+
case "smoothstep": {
|
|
455
|
+
const tt = emit(`clamp(${helper("safeDivide", type)}(${c} - ${a}, ${b} - ${a}), ${t}(0.0), ${t}(1.0))`, type);
|
|
456
|
+
expression = `(${tt} * ${tt} * (${t}(3.0) - ${t}(2.0) * ${tt}))`;
|
|
457
|
+
break;
|
|
458
|
+
}
|
|
459
|
+
case "sin":
|
|
460
|
+
case "cos":
|
|
461
|
+
case "abs":
|
|
462
|
+
case "floor":
|
|
463
|
+
case "ceil":
|
|
464
|
+
case "fract":
|
|
465
|
+
expression = `${op}(${a})`;
|
|
466
|
+
break;
|
|
467
|
+
case "min":
|
|
468
|
+
case "max":
|
|
469
|
+
case "dot":
|
|
470
|
+
case "cross":
|
|
471
|
+
case "step":
|
|
472
|
+
expression = `${op}(${a}, ${b})`;
|
|
473
|
+
break;
|
|
474
|
+
case "mix":
|
|
475
|
+
case "clamp":
|
|
476
|
+
expression = `${op}(${a}, ${b}, ${c})`;
|
|
477
|
+
break;
|
|
478
|
+
default:
|
|
479
|
+
return fail("OP", node.id);
|
|
480
|
+
}
|
|
481
|
+
return emit(expression, type);
|
|
482
|
+
},
|
|
483
|
+
});
|
|
484
|
+
return { helpers: [...helpers.values()].join("\n"), code: lines.join("\n"), result, type: graph.outputType };
|
|
485
|
+
}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import * as TSL from "three/tsl";
|
|
2
|
+
import { mapTypedGraph } from "./graph-expressions.js";
|
|
3
|
+
export function typedGraphToTSL(graph, time, parameters, textures, context) {
|
|
4
|
+
const construct = (type, value) => type === "bool"
|
|
5
|
+
? TSL.bool(value)
|
|
6
|
+
: type === "float"
|
|
7
|
+
? TSL.float(value)
|
|
8
|
+
: TSL[type](...(Array.isArray(value) ? value : [value]));
|
|
9
|
+
const safeDivide = (a, b, type) => {
|
|
10
|
+
const zero = b.equal(0);
|
|
11
|
+
return TSL.select(zero, construct(type, 0), a.div(TSL.select(zero, construct(type, 1), b)));
|
|
12
|
+
};
|
|
13
|
+
return mapTypedGraph(graph, {
|
|
14
|
+
constant: (value, type) => construct(type, value),
|
|
15
|
+
input(name) {
|
|
16
|
+
if (name === "time")
|
|
17
|
+
return time;
|
|
18
|
+
if (context?.inputs && Object.hasOwn(context.inputs, name))
|
|
19
|
+
return context.inputs[name];
|
|
20
|
+
if (name === "uv")
|
|
21
|
+
return TSL.uv(0);
|
|
22
|
+
if (name === "uv1")
|
|
23
|
+
return TSL.uv(1);
|
|
24
|
+
if (name === "positionWorld")
|
|
25
|
+
return TSL.positionWorld;
|
|
26
|
+
if (name === "normalWorld")
|
|
27
|
+
return TSL.normalWorldGeometry;
|
|
28
|
+
if (name === "viewDirectionWorld")
|
|
29
|
+
return TSL.cameraPosition.sub(TSL.positionWorld).normalize();
|
|
30
|
+
if (name === "vertexColor")
|
|
31
|
+
return TSL.vertexColor();
|
|
32
|
+
throw Error("TC_GRAPH_CONTEXT: raster material input " + name);
|
|
33
|
+
},
|
|
34
|
+
parameter(name) {
|
|
35
|
+
const value = parameters.get(name);
|
|
36
|
+
if (!value)
|
|
37
|
+
throw Error("TC_GRAPH_PARAMETER: " + name);
|
|
38
|
+
return value;
|
|
39
|
+
},
|
|
40
|
+
texture(asset, uv) {
|
|
41
|
+
if (Object.hasOwn(graph.textureSlots ?? {}, asset)) {
|
|
42
|
+
if (!context?.sampleTextureSlot)
|
|
43
|
+
throw Error("TC_RENDER_TEXTURE_CAPABILITY: Managed texture sampling requires a render texture bridge.");
|
|
44
|
+
return context.sampleTextureSlot(asset, uv);
|
|
45
|
+
}
|
|
46
|
+
const texture = textures.get(asset);
|
|
47
|
+
if (!texture)
|
|
48
|
+
throw Error("TC_GRAPH_TEXTURE: " + asset);
|
|
49
|
+
return TSL.texture(texture, uv).level(TSL.float(0));
|
|
50
|
+
},
|
|
51
|
+
frame(channel, uv) {
|
|
52
|
+
if (!context?.sampleFrame)
|
|
53
|
+
throw Error("TC_GRAPH_CONTEXT: Frame sampling requires a postprocess source.");
|
|
54
|
+
return context.sampleFrame(channel, uv);
|
|
55
|
+
},
|
|
56
|
+
history(channel, uv) {
|
|
57
|
+
if (!context?.sampleHistory)
|
|
58
|
+
throw Error("TC_GRAPH_CONTEXT: History sampling requires a postprocess source.");
|
|
59
|
+
return context.sampleHistory(channel, uv);
|
|
60
|
+
},
|
|
61
|
+
buffer(name, index, type) {
|
|
62
|
+
if (!context?.readBuffer)
|
|
63
|
+
throw new Error("TC_GRAPH_CONTEXT: Buffer reads require a managed render slot.");
|
|
64
|
+
return context.readBuffer(name, index, type);
|
|
65
|
+
},
|
|
66
|
+
operation(node, inputs, type, inputTypes) {
|
|
67
|
+
const cast = (v, i) => inputTypes[i] === "float" && type.startsWith("vec")
|
|
68
|
+
? construct(type, v)
|
|
69
|
+
: v;
|
|
70
|
+
const [a, b, c] = inputs.map(cast), op = node.op;
|
|
71
|
+
if (op === "compose2" || op === "compose3" || op === "compose4")
|
|
72
|
+
return TSL[type](...inputs);
|
|
73
|
+
if (op === "swizzle") {
|
|
74
|
+
const family = /^[rgba]+$/.test(node.components) ? "rgba" : "xyzw";
|
|
75
|
+
return inputs[0][[...node.components].map((c) => "xyzw"[family.indexOf(c)]).join("")];
|
|
76
|
+
}
|
|
77
|
+
if (op === "add")
|
|
78
|
+
return a.add(b);
|
|
79
|
+
if (op === "subtract")
|
|
80
|
+
return a.sub(b);
|
|
81
|
+
if (op === "multiply")
|
|
82
|
+
return a.mul(b);
|
|
83
|
+
if (op === "safeDivide")
|
|
84
|
+
return safeDivide(a, b, type);
|
|
85
|
+
if (op === "negate")
|
|
86
|
+
return a.negate();
|
|
87
|
+
if (op === "not")
|
|
88
|
+
return a.not();
|
|
89
|
+
if (op === "and")
|
|
90
|
+
return a.and(b);
|
|
91
|
+
if (op === "or")
|
|
92
|
+
return a.or(b);
|
|
93
|
+
if (op === "equal")
|
|
94
|
+
return a.equal(b);
|
|
95
|
+
if (op === "lessThan")
|
|
96
|
+
return a.lessThan(b);
|
|
97
|
+
if (op === "lessEqual")
|
|
98
|
+
return a.lessThanEqual(b);
|
|
99
|
+
if (op === "greaterThan")
|
|
100
|
+
return a.greaterThan(b);
|
|
101
|
+
if (op === "greaterEqual")
|
|
102
|
+
return a.greaterThanEqual(b);
|
|
103
|
+
if (op === "select")
|
|
104
|
+
return TSL.select(inputs[2], b, a);
|
|
105
|
+
if (op === "saturate")
|
|
106
|
+
return TSL.clamp(a, 0, 1);
|
|
107
|
+
if (op === "safeSqrt")
|
|
108
|
+
return TSL.sqrt(TSL.max(a, 0));
|
|
109
|
+
if (op === "normalize") {
|
|
110
|
+
const n = Number(type.slice(3));
|
|
111
|
+
let scale = TSL.abs(a.x);
|
|
112
|
+
for (let i = 1; i < n; i++)
|
|
113
|
+
scale = TSL.max(scale, TSL.abs(a["xyzw"[i]]));
|
|
114
|
+
const scaled = a.div(TSL.select(scale.equal(0), 1, scale));
|
|
115
|
+
const length = TSL.length(scaled);
|
|
116
|
+
return scaled.div(TSL.select(length.equal(0), 1, length));
|
|
117
|
+
}
|
|
118
|
+
if (op === "smoothstep") {
|
|
119
|
+
const t = TSL.clamp(safeDivide(c.sub(a), b.sub(a), type), 0, 1);
|
|
120
|
+
return t.mul(t).mul(construct(type, 3).sub(t.mul(2)));
|
|
121
|
+
}
|
|
122
|
+
if (op === "length")
|
|
123
|
+
return inputTypes[0] === "float" ? TSL.abs(a) : TSL.length(a);
|
|
124
|
+
if (["sin", "cos", "abs", "floor", "ceil", "fract"].includes(op))
|
|
125
|
+
return TSL[op](a);
|
|
126
|
+
if (["min", "max", "dot", "cross", "step"].includes(op))
|
|
127
|
+
return TSL[op](a, b);
|
|
128
|
+
if (op === "mix" || op === "clamp")
|
|
129
|
+
return TSL[op](a, b, c);
|
|
130
|
+
throw Error("TC_GRAPH_OP: " + node.id + ":" + op);
|
|
131
|
+
},
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
/** normalNode expects an already oriented, normalized view-space normal. */
|
|
135
|
+
export function materialGraphNormalToTSL(value) {
|
|
136
|
+
const scale = TSL.max(TSL.max(TSL.abs(value.x), TSL.abs(value.y)), TSL.abs(value.z));
|
|
137
|
+
const scaled = value.div(TSL.select(scale.equal(0), 1, scale));
|
|
138
|
+
const length = TSL.length(scaled);
|
|
139
|
+
const world = TSL.select(scale.equal(0), TSL.normalWorldGeometry, scaled.div(TSL.select(length.equal(0), 1, length)));
|
|
140
|
+
return TSL.negateOnBackSide(world.transformDirection(TSL.cameraViewMatrix));
|
|
141
|
+
}
|