@volter/blender-engine 0.1.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/LICENSE +724 -0
- package/README.md +48 -0
- package/browser/blender-emscripten-engine.mts +289 -0
- package/browser/blender-engine.mts +412 -0
- package/browser/blender-wali-engine.mts +362 -0
- package/browser/index.ts +7 -0
- package/browser/protocol.ts +202 -0
- package/browser/rna.ts +697 -0
- package/browser/runtime.ts +511 -0
- package/browser/session-frame.mts +169 -0
- package/browser/session.py +4166 -0
- package/browser/three/agx-base-srgb.lut +0 -0
- package/browser/three/agx-look-medium-high-contrast.lut +0 -0
- package/browser/three/agx-look-punchy.lut +0 -0
- package/browser/three/attach-presenter.ts +140 -0
- package/browser/three/blender-agx.ts +235 -0
- package/browser/three/blender-base64.ts +42 -0
- package/browser/three/blender-corner-normals.ts +432 -0
- package/browser/three/blender-display-lut.ts +145 -0
- package/browser/three/blender-filmic.ts +49 -0
- package/browser/three/blender-frame-columns.ts +100 -0
- package/browser/three/blender-gradient-texture.ts +57 -0
- package/browser/three/blender-runtime-armature.ts +528 -0
- package/browser/three/blender-runtime-frame.ts +39 -0
- package/browser/three/blender-runtime-geometry.ts +342 -0
- package/browser/three/blender-runtime-lighting.ts +829 -0
- package/browser/three/blender-runtime-shadows.ts +107 -0
- package/browser/three/blender-runtime-view.ts +1481 -0
- package/browser/three/blender-runtime-volume.ts +128 -0
- package/browser/three/blender-runtime-weights.ts +306 -0
- package/browser/three/blender-sky.ts +461 -0
- package/browser/three/blender-standard.ts +68 -0
- package/browser/three/blender-triangulate.ts +181 -0
- package/browser/three/filmic-srgb.lut +0 -0
- package/browser/three/presenter.ts +265 -0
- package/browser/three/release.ts +27 -0
- package/browser/three/sky-precompute-worker.ts +45 -0
- package/browser/three/sky-worker.ts +79 -0
- package/browser/three/world-field-sampler.ts +358 -0
- package/browser/three/world-math.ts +59 -0
- package/browser/vgai_three.py +554 -0
- package/browser/worker.ts +648 -0
- package/package.json +48 -0
- package/wasm/BUNDLE.json +65 -0
- package/wasm/DEPENDENCY-LICENSES.txt +4879 -0
- package/wasm/blender_browser.data.br +0 -0
- package/wasm/blender_browser.js +2 -0
- package/wasm/blender_browser.wasm.br +0 -0
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sampling a world's radiance field — the expensive half of the world texture,
|
|
3
|
+
* kept in its own module so a WORKER can own it.
|
|
4
|
+
*
|
|
5
|
+
* This is where the time goes. A sky expression derives Blender's
|
|
6
|
+
* multiple-scattering model (`blender-sky.ts`, 512x256 with 64 in-scattering
|
|
7
|
+
* steps per pixel) and then this samples 32,768 directions out of it. MEASURED:
|
|
8
|
+
* 15.4 seconds. On the page's main thread that is not "slow", it is a FROZEN
|
|
9
|
+
* EDITOR — the owner saw exactly that, and a status-bar spinner could not have
|
|
10
|
+
* helped, because a blocked main thread cannot paint one.
|
|
11
|
+
*
|
|
12
|
+
* Nothing here touches the DOM, three's renderer, or any editor state: it takes
|
|
13
|
+
* a description and returns numbers. That is what makes it movable, and it is
|
|
14
|
+
* the reason the split is here rather than around something larger.
|
|
15
|
+
*/
|
|
16
|
+
import * as THREE from 'three';
|
|
17
|
+
import { gradientTexture } from './blender-gradient-texture';
|
|
18
|
+
// Type-only, so this module keeps NO runtime edge back to the contribution
|
|
19
|
+
// that uses it -- which is what lets a worker import it alone.
|
|
20
|
+
import type { WorldExpression } from './blender-runtime-lighting';
|
|
21
|
+
import { type SkyParameters, skyField } from './blender-sky';
|
|
22
|
+
import { worldMath } from './world-math';
|
|
23
|
+
|
|
24
|
+
export function worldField(
|
|
25
|
+
expression: WorldExpression,
|
|
26
|
+
windowCoordinates?: (direction: THREE.Vector3) => THREE.Vector3,
|
|
27
|
+
): (direction: THREE.Vector3) => number | THREE.Vector3 {
|
|
28
|
+
if (typeof expression === 'number') return () => expression;
|
|
29
|
+
if (Array.isArray(expression)) {
|
|
30
|
+
const value = new THREE.Vector3(...expression);
|
|
31
|
+
return () => value;
|
|
32
|
+
}
|
|
33
|
+
if (expression.kind === 'direction') return (direction) => direction;
|
|
34
|
+
if (expression.kind === 'window') {
|
|
35
|
+
if (!windowCoordinates) throw new Error('World Window coordinates need a render camera');
|
|
36
|
+
return windowCoordinates;
|
|
37
|
+
}
|
|
38
|
+
if (expression.kind === 'gradient') {
|
|
39
|
+
const source = worldField(expression.vector, windowCoordinates);
|
|
40
|
+
const options = { type: expression.gradient_type };
|
|
41
|
+
const color = new THREE.Vector3();
|
|
42
|
+
return (direction) => {
|
|
43
|
+
const input = source(direction);
|
|
44
|
+
const x = typeof input === 'number' ? input : input.x;
|
|
45
|
+
const y = typeof input === 'number' ? input : input.y;
|
|
46
|
+
const z = typeof input === 'number' ? input : input.z;
|
|
47
|
+
const factor = THREE.MathUtils.clamp(gradientTexture(x, y, z, options), 0, 1);
|
|
48
|
+
return expression.output === 'Fac' ? factor : color.setScalar(factor);
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
if (expression.kind === 'to_float') {
|
|
52
|
+
const source = worldField(expression.value, windowCoordinates);
|
|
53
|
+
return (direction) => {
|
|
54
|
+
const value = source(direction);
|
|
55
|
+
if (typeof value === 'number') return value;
|
|
56
|
+
// Blender's default scene-linear Rec.709 -> XYZ Y, from its OCIO config.
|
|
57
|
+
return Math.fround(
|
|
58
|
+
expression.source_type === 'RGBA'
|
|
59
|
+
? value.x * 0.212639 + value.y * 0.7151687 + value.z * 0.0721923
|
|
60
|
+
: (value.x + value.y + value.z) / 3,
|
|
61
|
+
);
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
if (expression.kind === 'math') {
|
|
65
|
+
const inputs = expression.inputs.map((input) => worldField(input, windowCoordinates));
|
|
66
|
+
const operation = worldMath[expression.operation];
|
|
67
|
+
const scalar = (index: number, direction: THREE.Vector3): number => {
|
|
68
|
+
const value = inputs[index]?.(direction) ?? 0;
|
|
69
|
+
if (typeof value !== 'number') throw new Error('World Math needs scalar inputs');
|
|
70
|
+
return Math.fround(value);
|
|
71
|
+
};
|
|
72
|
+
return (direction) => {
|
|
73
|
+
const value = Math.fround(
|
|
74
|
+
operation(scalar(0, direction), scalar(1, direction), scalar(2, direction)),
|
|
75
|
+
);
|
|
76
|
+
return expression.clamp ? THREE.MathUtils.clamp(value, 0, 1) : value;
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
if (expression.kind === 'mix_color') {
|
|
80
|
+
const factor = worldField(expression.factor, windowCoordinates);
|
|
81
|
+
const a = worldField(expression.a, windowCoordinates);
|
|
82
|
+
const b = worldField(expression.b, windowCoordinates);
|
|
83
|
+
const color = new THREE.Vector3();
|
|
84
|
+
const second = new THREE.Vector3();
|
|
85
|
+
return (direction) => {
|
|
86
|
+
const amount = factor(direction);
|
|
87
|
+
if (typeof amount !== 'number') throw new Error('World color Mix needs a scalar factor');
|
|
88
|
+
const firstValue = a(direction);
|
|
89
|
+
if (typeof firstValue === 'number') color.setScalar(firstValue);
|
|
90
|
+
else color.copy(firstValue);
|
|
91
|
+
const secondValue = b(direction);
|
|
92
|
+
if (typeof secondValue === 'number') second.setScalar(secondValue);
|
|
93
|
+
else second.copy(secondValue);
|
|
94
|
+
color.lerp(second, expression.clamp_factor ? THREE.MathUtils.clamp(amount, 0, 1) : amount);
|
|
95
|
+
if (expression.clamp_result) color.clampScalar(0, 1);
|
|
96
|
+
return color;
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
if (expression.kind === 'sky') {
|
|
100
|
+
// Built ONCE per node: the model precomputes a 512x256 texture, and
|
|
101
|
+
// rebuilding it per direction would be a 65,536-pixel job per texel.
|
|
102
|
+
const field = skyField({
|
|
103
|
+
sunElevation: expression.sun_elevation,
|
|
104
|
+
sunRotation: expression.sun_rotation,
|
|
105
|
+
altitude: expression.altitude,
|
|
106
|
+
airDensity: expression.air_density,
|
|
107
|
+
aerosolDensity: expression.aerosol_density,
|
|
108
|
+
ozoneDensity: expression.ozone_density,
|
|
109
|
+
});
|
|
110
|
+
return (direction) => field(direction);
|
|
111
|
+
}
|
|
112
|
+
if (expression.kind === 'mapping') {
|
|
113
|
+
const source = worldField(expression.vector, windowCoordinates);
|
|
114
|
+
const matrix = new THREE.Matrix4().compose(
|
|
115
|
+
new THREE.Vector3(...expression.location),
|
|
116
|
+
new THREE.Quaternion().setFromEuler(
|
|
117
|
+
new THREE.Euler(
|
|
118
|
+
expression.rotation[0],
|
|
119
|
+
expression.rotation[1],
|
|
120
|
+
expression.rotation[2],
|
|
121
|
+
'ZYX',
|
|
122
|
+
),
|
|
123
|
+
),
|
|
124
|
+
new THREE.Vector3(...expression.scale),
|
|
125
|
+
);
|
|
126
|
+
const value = new THREE.Vector3();
|
|
127
|
+
return (direction) => {
|
|
128
|
+
const input = source(direction);
|
|
129
|
+
if (typeof input === 'number') value.setScalar(input);
|
|
130
|
+
else value.copy(input);
|
|
131
|
+
return value.applyMatrix4(matrix);
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
if (expression.kind === 'map_range') {
|
|
135
|
+
// `intern/cycles/kernel/svm/map_range.h`. Every branch answers 0 when the
|
|
136
|
+
// input range is empty, which is native's own guard and not a tolerance.
|
|
137
|
+
const value = worldField(expression.value, windowCoordinates);
|
|
138
|
+
const fromMin = worldField(expression.from_min, windowCoordinates);
|
|
139
|
+
const fromMax = worldField(expression.from_max, windowCoordinates);
|
|
140
|
+
const toMin = worldField(expression.to_min, windowCoordinates);
|
|
141
|
+
const toMax = worldField(expression.to_max, windowCoordinates);
|
|
142
|
+
const steps = worldField(expression.steps, windowCoordinates);
|
|
143
|
+
const scalar = (f: ReturnType<typeof worldField>, d: THREE.Vector3): number => {
|
|
144
|
+
const got = f(d);
|
|
145
|
+
if (typeof got !== 'number') throw new Error('World Map Range needs scalar inputs');
|
|
146
|
+
return got;
|
|
147
|
+
};
|
|
148
|
+
const mode = expression.interpolation;
|
|
149
|
+
const clamped = expression.clamp;
|
|
150
|
+
return (direction) => {
|
|
151
|
+
const x = scalar(value, direction);
|
|
152
|
+
const a = scalar(fromMin, direction),
|
|
153
|
+
b = scalar(fromMax, direction);
|
|
154
|
+
const lo = scalar(toMin, direction),
|
|
155
|
+
hi = scalar(toMax, direction);
|
|
156
|
+
if (a === b) return 0;
|
|
157
|
+
let factor: number;
|
|
158
|
+
if (mode === 'STEPPED') {
|
|
159
|
+
const n = scalar(steps, direction);
|
|
160
|
+
const raw = (x - a) / (b - a);
|
|
161
|
+
factor = n > 0 ? Math.floor(raw * (n + 1)) / n : 0;
|
|
162
|
+
} else if (mode === 'SMOOTHSTEP' || mode === 'SMOOTHERSTEP') {
|
|
163
|
+
const t = THREE.MathUtils.clamp(
|
|
164
|
+
(x - Math.min(a, b)) / (Math.max(a, b) - Math.min(a, b)),
|
|
165
|
+
0,
|
|
166
|
+
1,
|
|
167
|
+
);
|
|
168
|
+
const shaped =
|
|
169
|
+
mode === 'SMOOTHSTEP' ? t * t * (3 - 2 * t) : t * t * t * (t * (t * 6 - 15) + 10);
|
|
170
|
+
// `fromMin > fromMax` mirrors the curve rather than reversing the ends.
|
|
171
|
+
factor = a > b ? 1 - shaped : shaped;
|
|
172
|
+
} else {
|
|
173
|
+
factor = (x - a) / (b - a);
|
|
174
|
+
}
|
|
175
|
+
const result = lo + factor * (hi - lo);
|
|
176
|
+
if (!clamped) return result;
|
|
177
|
+
return lo > hi
|
|
178
|
+
? THREE.MathUtils.clamp(result, hi, lo)
|
|
179
|
+
: THREE.MathUtils.clamp(result, lo, hi);
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
if (expression.kind === 'separate') {
|
|
183
|
+
const source = worldField(expression.vector, windowCoordinates);
|
|
184
|
+
return (direction) => {
|
|
185
|
+
const input = source(direction);
|
|
186
|
+
return typeof input === 'number' ? input : input.getComponent(expression.axis);
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
const factor = worldField(expression.factor, windowCoordinates);
|
|
190
|
+
const value = new THREE.Vector3();
|
|
191
|
+
return (direction) => {
|
|
192
|
+
const input = factor(direction);
|
|
193
|
+
if (typeof input !== 'number') throw new Error('World ColorRamp needs a scalar factor');
|
|
194
|
+
const position = THREE.MathUtils.clamp(input, 0, 1) * 256;
|
|
195
|
+
const index = Math.floor(position);
|
|
196
|
+
const left = expression.colors[index]!;
|
|
197
|
+
const right = expression.colors[Math.min(index + 1, 256)]!;
|
|
198
|
+
const t = expression.interpolate ? position - index : 0;
|
|
199
|
+
return value.set(
|
|
200
|
+
THREE.MathUtils.lerp(left[0], right[0], t),
|
|
201
|
+
THREE.MathUtils.lerp(left[1], right[1], t),
|
|
202
|
+
THREE.MathUtils.lerp(left[2], right[2], t),
|
|
203
|
+
);
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/** One field, sampled over the equirectangular grid the texture uses. Three
|
|
208
|
+
* floats per texel, UNSCALED: strength is a multiply the caller applies, and
|
|
209
|
+
* keeping it out of here is what lets one derivation serve two strengths. */
|
|
210
|
+
export function sampleWorldField(
|
|
211
|
+
expression: WorldExpression,
|
|
212
|
+
width: number,
|
|
213
|
+
height: number,
|
|
214
|
+
windowCoordinates?: (direction: THREE.Vector3) => THREE.Vector3,
|
|
215
|
+
): Float32Array {
|
|
216
|
+
const samples = new Float32Array(width * height * 3);
|
|
217
|
+
const field = worldField(expression, windowCoordinates);
|
|
218
|
+
const direction = new THREE.Vector3();
|
|
219
|
+
for (let y = 0; y < height; y++) {
|
|
220
|
+
const latitude = ((y + 0.5) / height - 0.5) * Math.PI;
|
|
221
|
+
for (let x = 0; x < width; x++) {
|
|
222
|
+
const longitude = ((x + 0.5) / width - 0.5) * 2 * Math.PI;
|
|
223
|
+
direction.set(
|
|
224
|
+
Math.cos(latitude) * Math.cos(longitude),
|
|
225
|
+
-Math.cos(latitude) * Math.sin(longitude),
|
|
226
|
+
Math.sin(latitude),
|
|
227
|
+
);
|
|
228
|
+
const color = field(direction),
|
|
229
|
+
offset = (y * width + x) * 3;
|
|
230
|
+
samples[offset] = typeof color === 'number' ? color : color.x;
|
|
231
|
+
samples[offset + 1] = typeof color === 'number' ? color : color.y;
|
|
232
|
+
samples[offset + 2] = typeof color === 'number' ? color : color.z;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
return samples;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/** Every Sky Texture node in an expression, as the parameters that identify its
|
|
239
|
+
* precomputed texture. This is the WORK LIST: a caller that must not block the
|
|
240
|
+
* main thread primes each of these off-thread before it samples anything. */
|
|
241
|
+
export function collectSkyParameters(expression: WorldExpression): SkyParameters[] {
|
|
242
|
+
const found: SkyParameters[] = [];
|
|
243
|
+
const walk = (node: WorldExpression): void => {
|
|
244
|
+
if (typeof node === 'number' || Array.isArray(node)) return;
|
|
245
|
+
switch (node.kind) {
|
|
246
|
+
case 'sky':
|
|
247
|
+
found.push({
|
|
248
|
+
sunElevation: node.sun_elevation,
|
|
249
|
+
sunRotation: node.sun_rotation,
|
|
250
|
+
altitude: node.altitude,
|
|
251
|
+
airDensity: node.air_density,
|
|
252
|
+
aerosolDensity: node.aerosol_density,
|
|
253
|
+
ozoneDensity: node.ozone_density,
|
|
254
|
+
});
|
|
255
|
+
return;
|
|
256
|
+
case 'mapping':
|
|
257
|
+
case 'gradient':
|
|
258
|
+
case 'separate':
|
|
259
|
+
walk(node.vector);
|
|
260
|
+
return;
|
|
261
|
+
case 'ramp':
|
|
262
|
+
walk(node.factor);
|
|
263
|
+
return;
|
|
264
|
+
case 'mix_color':
|
|
265
|
+
walk(node.factor);
|
|
266
|
+
walk(node.a);
|
|
267
|
+
walk(node.b);
|
|
268
|
+
return;
|
|
269
|
+
case 'math':
|
|
270
|
+
node.inputs.forEach(walk);
|
|
271
|
+
return;
|
|
272
|
+
case 'to_float':
|
|
273
|
+
walk(node.value);
|
|
274
|
+
return;
|
|
275
|
+
case 'map_range':
|
|
276
|
+
walk(node.value);
|
|
277
|
+
walk(node.from_min);
|
|
278
|
+
walk(node.from_max);
|
|
279
|
+
walk(node.to_min);
|
|
280
|
+
walk(node.to_max);
|
|
281
|
+
walk(node.steps);
|
|
282
|
+
return;
|
|
283
|
+
default:
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
};
|
|
287
|
+
walk(expression);
|
|
288
|
+
return found;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/** Whether this expression depends on the camera's normalized image coordinates. */
|
|
292
|
+
export function usesWindowCoordinates(expression: WorldExpression): boolean {
|
|
293
|
+
if (typeof expression === 'number' || Array.isArray(expression)) return false;
|
|
294
|
+
switch (expression.kind) {
|
|
295
|
+
case 'window':
|
|
296
|
+
return true;
|
|
297
|
+
case 'mapping':
|
|
298
|
+
case 'gradient':
|
|
299
|
+
case 'separate':
|
|
300
|
+
return usesWindowCoordinates(expression.vector);
|
|
301
|
+
case 'ramp':
|
|
302
|
+
return usesWindowCoordinates(expression.factor);
|
|
303
|
+
case 'mix_color':
|
|
304
|
+
return [expression.factor, expression.a, expression.b].some(usesWindowCoordinates);
|
|
305
|
+
case 'math':
|
|
306
|
+
return expression.inputs.some(usesWindowCoordinates);
|
|
307
|
+
case 'to_float':
|
|
308
|
+
return usesWindowCoordinates(expression.value);
|
|
309
|
+
case 'map_range':
|
|
310
|
+
return [
|
|
311
|
+
expression.value,
|
|
312
|
+
expression.from_min,
|
|
313
|
+
expression.from_max,
|
|
314
|
+
expression.to_min,
|
|
315
|
+
expression.to_max,
|
|
316
|
+
expression.steps,
|
|
317
|
+
].some(usesWindowCoordinates);
|
|
318
|
+
default:
|
|
319
|
+
return false;
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/** Camera rays carry pixel coordinates even when orthographic directions are parallel. */
|
|
324
|
+
export function sampleWorldScreen(
|
|
325
|
+
expression: WorldExpression,
|
|
326
|
+
width: number,
|
|
327
|
+
height: number,
|
|
328
|
+
camera: THREE.Camera,
|
|
329
|
+
): Float32Array {
|
|
330
|
+
camera.updateMatrixWorld();
|
|
331
|
+
const pixels = new Float32Array(width * height * 3);
|
|
332
|
+
const windowPoint = new THREE.Vector3();
|
|
333
|
+
const field = worldField(expression, () => windowPoint);
|
|
334
|
+
const direction = new THREE.Vector3();
|
|
335
|
+
const position = camera.getWorldPosition(new THREE.Vector3());
|
|
336
|
+
const forward = camera.getWorldDirection(new THREE.Vector3());
|
|
337
|
+
for (let y = 0; y < height; y++) {
|
|
338
|
+
for (let x = 0; x < width; x++) {
|
|
339
|
+
const u = (x + 0.5) / width,
|
|
340
|
+
v = (y + 0.5) / height;
|
|
341
|
+
windowPoint.set(u, v, 0);
|
|
342
|
+
if ((camera as THREE.OrthographicCamera).isOrthographicCamera) direction.copy(forward);
|
|
343
|
+
else
|
|
344
|
+
direction
|
|
345
|
+
.set(u * 2 - 1, v * 2 - 1, 0.5)
|
|
346
|
+
.unproject(camera)
|
|
347
|
+
.sub(position)
|
|
348
|
+
.normalize();
|
|
349
|
+
direction.set(direction.x, -direction.z, direction.y);
|
|
350
|
+
const color = field(direction),
|
|
351
|
+
offset = (y * width + x) * 3;
|
|
352
|
+
pixels[offset] = typeof color === 'number' ? color : color.x;
|
|
353
|
+
pixels[offset + 1] = typeof color === 'number' ? color : color.y;
|
|
354
|
+
pixels[offset + 2] = typeof color === 'number' ? color : color.z;
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
return pixels;
|
|
358
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/** Blender 5.2 `intern/cycles/kernel/svm/math_util.h` scalar Math operations. */
|
|
2
|
+
const divide = (a: number, b: number): number => (b !== 0 ? a / b : 0);
|
|
3
|
+
const clampUnit = (a: number): number => Math.min(1, Math.max(-1, a));
|
|
4
|
+
const smoothMin = (a: number, b: number, k: number): number => {
|
|
5
|
+
const h = k !== 0 ? Math.max(k - Math.abs(a - b), 0) / k : 0;
|
|
6
|
+
return Math.min(a, b) - (h * h * h * k) / 6;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export const worldMath = {
|
|
10
|
+
ADD: (a: number, b: number) => a + b,
|
|
11
|
+
SUBTRACT: (a: number, b: number) => a - b,
|
|
12
|
+
MULTIPLY: (a: number, b: number) => a * b,
|
|
13
|
+
DIVIDE: divide,
|
|
14
|
+
POWER: (a: number, b: number) =>
|
|
15
|
+
b === 0 ? 1 : a === 0 || (a < 0 && !Number.isInteger(b)) ? 0 : a ** b,
|
|
16
|
+
LOGARITHM: (a: number, b: number) => (a <= 0 || b <= 0 ? 0 : divide(Math.log(a), Math.log(b))),
|
|
17
|
+
SQRT: (a: number) => Math.sqrt(Math.max(a, 0)),
|
|
18
|
+
INVERSE_SQRT: (a: number) => (a > 0 ? 1 / Math.sqrt(a) : 0),
|
|
19
|
+
ABSOLUTE: Math.abs,
|
|
20
|
+
RADIANS: (a: number) => (a * Math.PI) / 180,
|
|
21
|
+
DEGREES: (a: number) => (a * 180) / Math.PI,
|
|
22
|
+
MINIMUM: (a: number, b: number) => Math.min(a, b),
|
|
23
|
+
MAXIMUM: (a: number, b: number) => Math.max(a, b),
|
|
24
|
+
LESS_THAN: (a: number, b: number) => Number(a < b),
|
|
25
|
+
GREATER_THAN: (a: number, b: number) => Number(a > b),
|
|
26
|
+
ROUND: (a: number) => Math.floor(a + 0.5),
|
|
27
|
+
FLOOR: Math.floor,
|
|
28
|
+
CEIL: Math.ceil,
|
|
29
|
+
FRACT: (a: number) => a - Math.floor(a),
|
|
30
|
+
MODULO: (a: number, b: number) => (b !== 0 ? a % b : 0),
|
|
31
|
+
FLOORED_MODULO: (a: number, b: number) => (b !== 0 ? a - Math.floor(a / b) * b : 0),
|
|
32
|
+
TRUNC: Math.trunc,
|
|
33
|
+
SNAP: (a: number, b: number) => Math.floor(divide(a, b)) * b,
|
|
34
|
+
WRAP: (a: number, b: number, c: number) =>
|
|
35
|
+
b !== c ? a - (b - c) * Math.floor((a - c) / (b - c)) : c,
|
|
36
|
+
PINGPONG: (a: number, b: number) => {
|
|
37
|
+
if (b === 0) return 0;
|
|
38
|
+
const t = (a - b) / (b * 2);
|
|
39
|
+
return Math.abs((t - Math.floor(t)) * b * 2 - b);
|
|
40
|
+
},
|
|
41
|
+
SINE: Math.sin,
|
|
42
|
+
COSINE: Math.cos,
|
|
43
|
+
TANGENT: Math.tan,
|
|
44
|
+
SINH: Math.sinh,
|
|
45
|
+
COSH: Math.cosh,
|
|
46
|
+
TANH: Math.tanh,
|
|
47
|
+
ARCSINE: (a: number) => Math.asin(clampUnit(a)),
|
|
48
|
+
ARCCOSINE: (a: number) => Math.acos(clampUnit(a)),
|
|
49
|
+
ARCTANGENT: Math.atan,
|
|
50
|
+
ARCTAN2: (a: number, b: number) => (a === 0 && b === 0 ? 0 : Math.atan2(a, b)),
|
|
51
|
+
SIGN: Math.sign,
|
|
52
|
+
EXPONENT: Math.exp,
|
|
53
|
+
COMPARE: (a: number, b: number, c: number) =>
|
|
54
|
+
Number(a === b || Math.abs(a - b) <= Math.max(c, 2 ** -23)),
|
|
55
|
+
MULTIPLY_ADD: (a: number, b: number, c: number) => a * b + c,
|
|
56
|
+
SMOOTH_MIN: smoothMin,
|
|
57
|
+
SMOOTH_MAX: (a: number, b: number, c: number) => -smoothMin(-a, -b, c),
|
|
58
|
+
};
|
|
59
|
+
export type WorldMathOperation = keyof typeof worldMath;
|