tela.js 1.1.8 → 1.1.10
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/README.md +11 -6
- package/package.json +1 -1
- package/src/Camera/Camera.js +14 -2
- package/src/Camera/parallel.js +92 -15
- package/src/Camera/rayMapWorker.js +81 -0
- package/src/Camera/rayTrace.js +1 -1
- package/src/Camera/rayTraceWorker.js +3 -2
- package/src/Geometry/Box.js +7 -3
- package/src/Geometry/Line.js +11 -1
- package/src/IO/IO.js +3 -3
- package/src/Scene/BScene.js +1 -1
- package/src/Scene/NaiveScene.js +9 -2
- package/src/Scene/utils.js +2 -0
- package/src/Tela/Canvas.js +3 -80
- package/src/Tela/Image.js +2 -87
- package/src/Tela/Tela.js +30 -2
- package/src/Tela/Window.js +8 -82
- package/src/Tela/parallel.js +90 -0
- package/src/Tela/telaWorker.js +62 -0
- package/src/Utils/Constants.js +1 -0
- package/src/Utils/Math.js +17 -0
- package/src/Utils/SVG.js +553 -40
- package/src/Utils/Stream.js +4 -3
- package/src/Utils/Utils.js +11 -10
- package/src/index.js +2 -1
package/README.md
CHANGED
|
@@ -16,6 +16,8 @@ Playground usage:
|
|
|
16
16
|
|
|
17
17
|
- [Quick start](#quick-start)
|
|
18
18
|
- [Dependencies](#dependencies)
|
|
19
|
+
- [Acknowledgements](#acknowledgements)
|
|
20
|
+
- [TODOs](#todos)
|
|
19
21
|
|
|
20
22
|
# Quick start
|
|
21
23
|
|
|
@@ -36,7 +38,7 @@ Playground usage:
|
|
|
36
38
|
|
|
37
39
|
const width = 640;
|
|
38
40
|
const height = 480;
|
|
39
|
-
const canvas = Canvas.ofSize(
|
|
41
|
+
const canvas = Canvas.ofSize(width, height);
|
|
40
42
|
loop(({ time, dt }) => {
|
|
41
43
|
document.title = `FPS: ${(Math.floor(1 / dt))}`;
|
|
42
44
|
canvas.map((x, y) => {
|
|
@@ -60,7 +62,7 @@ import { loop, Color, Window } from "tela.js/src/index.node.js";
|
|
|
60
62
|
|
|
61
63
|
const width = 640;
|
|
62
64
|
const height = 480;
|
|
63
|
-
const window = Window.ofSize(
|
|
65
|
+
const window = Window.ofSize(width, height);
|
|
64
66
|
loop(({ time, dt }) => {
|
|
65
67
|
window.setTitle(`FPS: ${Math.floor(1 / dt)}`);
|
|
66
68
|
window.map((x, y) => {
|
|
@@ -111,9 +113,6 @@ video(
|
|
|
111
113
|
|
|
112
114
|
And run it: `node index.mjs` / `bun index.js`
|
|
113
115
|
|
|
114
|
-
### Bun install issue
|
|
115
|
-
|
|
116
|
-
Bun install is currently not working properly on these project. Although you can run examples with bun.
|
|
117
116
|
|
|
118
117
|
### Note on generating videos
|
|
119
118
|
|
|
@@ -139,8 +138,14 @@ You can find more examples of usage in:
|
|
|
139
138
|
- [`ffmpeg`][ffmpeg]
|
|
140
139
|
- [`node-sdl`][sdl]
|
|
141
140
|
|
|
142
|
-
[Node][node] is preferred when running the demos (it is faster, [opened a bug in bun](https://github.com/oven-sh/bun/issues/9218))
|
|
141
|
+
[Node][node] is preferred when running the demos (it is faster, [opened a bug in bun](https://github.com/oven-sh/bun/issues/9218)).
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
# Acknowledgements
|
|
143
145
|
|
|
146
|
+
- [Keenan's 3D Model Repository](https://www.cs.cmu.edu/~kmcrane/Projects/ModelRepository/)
|
|
147
|
+
- [The models resource](https://www.models-resource.com/)
|
|
148
|
+
- [otaviogood fonts](https://github.com/otaviogood/shader_fontgen)
|
|
144
149
|
|
|
145
150
|
# TODOs
|
|
146
151
|
|
package/package.json
CHANGED
package/src/Camera/Camera.js
CHANGED
|
@@ -4,7 +4,7 @@ import { getRayTracer } from "./rayTrace.js";
|
|
|
4
4
|
import { rasterGraphics } from "./raster.js";
|
|
5
5
|
import { sdfTrace } from "./sdf.js";
|
|
6
6
|
import { normalTrace } from "./normal.js";
|
|
7
|
-
import {
|
|
7
|
+
import { rayMapWorkers, rayTraceWorkers } from "./parallel.js";
|
|
8
8
|
|
|
9
9
|
export default class Camera {
|
|
10
10
|
constructor(props = {}) {
|
|
@@ -107,6 +107,18 @@ export default class Camera {
|
|
|
107
107
|
}
|
|
108
108
|
}
|
|
109
109
|
|
|
110
|
+
rayMapParallel(lambdaWithRays, dependencies) {
|
|
111
|
+
return {
|
|
112
|
+
to: (canvas, { scene, ...vars }) => {
|
|
113
|
+
return Promise
|
|
114
|
+
.allSettled(rayMapWorkers(this, scene, canvas, lambdaWithRays, vars, dependencies))
|
|
115
|
+
.then(() => {
|
|
116
|
+
return canvas.paint();
|
|
117
|
+
})
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
110
122
|
sceneShot(scene, params) {
|
|
111
123
|
return this.rayMap(getRayTracer(scene, params));
|
|
112
124
|
}
|
|
@@ -129,7 +141,7 @@ export default class Camera {
|
|
|
129
141
|
return {
|
|
130
142
|
to: canvas => {
|
|
131
143
|
return Promise
|
|
132
|
-
.
|
|
144
|
+
.allSettled(rayTraceWorkers(this, scene, canvas, params))
|
|
133
145
|
.then(() => {
|
|
134
146
|
return canvas.paint();
|
|
135
147
|
})
|
package/src/Camera/parallel.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import { IS_NODE, NUMBER_OF_CORES } from "../Utils/Constants.js";
|
|
1
|
+
import { CHANNELS, IS_NODE, NUMBER_OF_CORES } from "../Utils/Constants.js";
|
|
2
2
|
import Color from "../Color/Color.js";
|
|
3
|
-
import { CHANNELS } from "../Tela/Tela.js";
|
|
4
3
|
|
|
5
4
|
//========================================================================================
|
|
6
5
|
/* *
|
|
@@ -23,8 +22,14 @@ class MyWorker {
|
|
|
23
22
|
if (IS_NODE) {
|
|
24
23
|
this.worker.removeAllListeners('message');
|
|
25
24
|
this.worker.on("message", lambda);
|
|
25
|
+
this.worker.on("error", e => console.log("Caught error on worker", e))
|
|
26
26
|
} else {
|
|
27
|
-
|
|
27
|
+
if (this.__lambda) {
|
|
28
|
+
this.worker.removeEventListener('message', this.__lambda);
|
|
29
|
+
}
|
|
30
|
+
this.__lambda = message => lambda(message.data);
|
|
31
|
+
this.worker.addEventListener("message", this.__lambda);
|
|
32
|
+
this.worker.addEventListener("error", e => console.log("Caught error on worker", e));
|
|
28
33
|
}
|
|
29
34
|
}
|
|
30
35
|
|
|
@@ -33,29 +38,39 @@ class MyWorker {
|
|
|
33
38
|
}
|
|
34
39
|
}
|
|
35
40
|
|
|
36
|
-
let
|
|
41
|
+
let RAY_TRACE_WORKERS = [];
|
|
42
|
+
let RAY_MAP_WORKERS = [];
|
|
37
43
|
let prevSceneHash = undefined;
|
|
44
|
+
let isFirstTimeCounter = NUMBER_OF_CORES;
|
|
45
|
+
let serializedScene = undefined;
|
|
38
46
|
|
|
47
|
+
const MAGIC_SETUP_TIME = 800;
|
|
39
48
|
//========================================================================================
|
|
40
49
|
/* *
|
|
41
50
|
* MAIN *
|
|
42
51
|
* */
|
|
43
52
|
//========================================================================================
|
|
44
53
|
|
|
45
|
-
export function
|
|
54
|
+
export function rayTraceWorkers(camera, scene, canvas, params = {}) {
|
|
46
55
|
// lazy loading workers
|
|
47
|
-
if (
|
|
56
|
+
if (RAY_TRACE_WORKERS.length === 0) {
|
|
48
57
|
// needs to be here...
|
|
49
58
|
const isGithub = typeof window !== "undefined" && (window.location.host || window.LOCATION_HOST) === "pedroth.github.io";
|
|
50
59
|
const SOURCE = isGithub ? "/tela.js" : ""
|
|
51
|
-
|
|
60
|
+
RAY_TRACE_WORKERS = [...Array(NUMBER_OF_CORES)]
|
|
52
61
|
.map(() => new MyWorker(`${IS_NODE ? "." : SOURCE}/src/Camera/rayTraceWorker.js`));
|
|
53
62
|
}
|
|
54
63
|
const w = canvas.width;
|
|
55
64
|
const h = canvas.height;
|
|
56
|
-
const
|
|
57
|
-
|
|
58
|
-
|
|
65
|
+
const newHash = scene?.getHash();
|
|
66
|
+
const isNewScene = prevSceneHash !== newHash;
|
|
67
|
+
if (isNewScene) {
|
|
68
|
+
prevSceneHash = newHash;
|
|
69
|
+
serializedScene = scene.serialize()
|
|
70
|
+
} else {
|
|
71
|
+
serializedScene = undefined;
|
|
72
|
+
}
|
|
73
|
+
return RAY_TRACE_WORKERS.map((worker, k) => {
|
|
59
74
|
return new Promise((resolve) => {
|
|
60
75
|
worker.onMessage(message => {
|
|
61
76
|
const { image, startRow, endRow, } = message;
|
|
@@ -67,18 +82,80 @@ export function parallelWorkers(camera, scene, canvas, params = {}) {
|
|
|
67
82
|
}
|
|
68
83
|
resolve();
|
|
69
84
|
})
|
|
70
|
-
const ratio = Math.floor(h /
|
|
85
|
+
const ratio = Math.floor(h / RAY_TRACE_WORKERS.length);
|
|
86
|
+
|
|
87
|
+
const message = {
|
|
88
|
+
width: w,
|
|
89
|
+
height: h,
|
|
90
|
+
vars: params,
|
|
91
|
+
startRow: k * ratio,
|
|
92
|
+
endRow: Math.min(h, (k + 1) * ratio),
|
|
93
|
+
camera: camera.serialize(),
|
|
94
|
+
scene: serializedScene
|
|
95
|
+
};
|
|
96
|
+
if (isFirstTimeCounter > 0 && !IS_NODE) {
|
|
97
|
+
// hack to work in the browser, don't know why it works
|
|
98
|
+
isFirstTimeCounter--;
|
|
99
|
+
setTimeout(() => worker.postMessage(message), MAGIC_SETUP_TIME);
|
|
100
|
+
} else {
|
|
101
|
+
worker.postMessage(message)
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
})
|
|
105
|
+
}
|
|
71
106
|
|
|
107
|
+
export function rayMapWorkers(camera, scene, canvas, lambda, vars = [], dependencies = []) {
|
|
108
|
+
// lazy loading workers
|
|
109
|
+
if (RAY_MAP_WORKERS.length === 0) {
|
|
110
|
+
// needs to be here...
|
|
111
|
+
const isGithub = typeof window !== "undefined" && (window.location.host || window.LOCATION_HOST) === "pedroth.github.io";
|
|
112
|
+
const SOURCE = isGithub ? "/tela.js" : ""
|
|
113
|
+
RAY_MAP_WORKERS = [...Array(NUMBER_OF_CORES)]
|
|
114
|
+
.map(() => new MyWorker(`${IS_NODE ? "." : SOURCE}/src/Camera/rayMapWorker.js`));
|
|
115
|
+
}
|
|
116
|
+
const w = canvas.width;
|
|
117
|
+
const h = canvas.height;
|
|
118
|
+
const newHash = scene?.getHash();
|
|
119
|
+
const isNewScene = prevSceneHash !== newHash;
|
|
120
|
+
if (isNewScene) {
|
|
121
|
+
prevSceneHash = newHash;
|
|
122
|
+
serializedScene = scene.serialize()
|
|
123
|
+
} else {
|
|
124
|
+
serializedScene = undefined;
|
|
125
|
+
}
|
|
126
|
+
return RAY_MAP_WORKERS.map((worker, k) => {
|
|
127
|
+
return new Promise((resolve) => {
|
|
128
|
+
worker.onMessage(message => {
|
|
129
|
+
const { image, startRow, endRow, } = message;
|
|
130
|
+
let index = 0;
|
|
131
|
+
const startIndex = CHANNELS * w * startRow;
|
|
132
|
+
const endIndex = CHANNELS * w * endRow;
|
|
133
|
+
for (let i = startIndex; i < endIndex; i += CHANNELS) {
|
|
134
|
+
canvas.setPxlData(i, Color.ofRGB(image[index++], image[index++], image[index++], image[index++]));
|
|
135
|
+
}
|
|
136
|
+
resolve();
|
|
137
|
+
})
|
|
138
|
+
const ratio = Math.floor(h / RAY_MAP_WORKERS.length);
|
|
139
|
+
|
|
72
140
|
const message = {
|
|
73
141
|
width: w,
|
|
74
142
|
height: h,
|
|
75
|
-
|
|
143
|
+
vars: vars,
|
|
144
|
+
lambda: lambda.toString(),
|
|
76
145
|
startRow: k * ratio,
|
|
77
146
|
endRow: Math.min(h, (k + 1) * ratio),
|
|
78
147
|
camera: camera.serialize(),
|
|
79
|
-
|
|
148
|
+
dependencies: dependencies.map(d => d.toString()),
|
|
149
|
+
scene: serializedScene
|
|
80
150
|
};
|
|
81
|
-
|
|
151
|
+
if (isFirstTimeCounter > 0 && !IS_NODE) {
|
|
152
|
+
// hack to work in the browser, don't know why it works
|
|
153
|
+
isFirstTimeCounter--;
|
|
154
|
+
setTimeout(() => worker.postMessage(message), MAGIC_SETUP_TIME);
|
|
155
|
+
} else {
|
|
156
|
+
worker.postMessage(message)
|
|
157
|
+
}
|
|
158
|
+
|
|
82
159
|
});
|
|
83
160
|
})
|
|
84
|
-
}
|
|
161
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import Camera from "./Camera.js";
|
|
2
|
+
import { memoize } from "../Utils/Utils.js";
|
|
3
|
+
import { CHANNELS, IS_NODE } from "../Utils/Constants.js";
|
|
4
|
+
import { deserializeScene } from "../Scene/utils.js";
|
|
5
|
+
import Color from "../Color/Color.js";
|
|
6
|
+
import Box from "../Geometry/Box.js";
|
|
7
|
+
import Vec, { Vec2, Vec3 } from "../Vector/Vector.js";
|
|
8
|
+
import Ray from "../Ray/Ray.js";
|
|
9
|
+
|
|
10
|
+
const parentPort = IS_NODE ? (await import("node:worker_threads")).parentPort : undefined;
|
|
11
|
+
|
|
12
|
+
let scene = undefined;
|
|
13
|
+
|
|
14
|
+
function getScene(serializedScene) {
|
|
15
|
+
return deserializeScene(serializedScene).then(s => s.rebuild());
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async function main(inputs) {
|
|
19
|
+
const {
|
|
20
|
+
startRow,
|
|
21
|
+
endRow,
|
|
22
|
+
width,
|
|
23
|
+
height,
|
|
24
|
+
vars,
|
|
25
|
+
dependencies,
|
|
26
|
+
lambda,
|
|
27
|
+
scene: serializedScene,
|
|
28
|
+
camera: serializedCamera,
|
|
29
|
+
} = inputs;
|
|
30
|
+
|
|
31
|
+
scene = serializedScene ? (await getScene(serializedScene)).rebuild() : scene;
|
|
32
|
+
const camera = Camera.deserialize(serializedCamera);
|
|
33
|
+
const rayGen = camera.rayFromImage(width, height);
|
|
34
|
+
const __lambda = getLambda(lambda, dependencies);
|
|
35
|
+
const bufferSize = width * (endRow - startRow + 1) * CHANNELS;
|
|
36
|
+
const image = new Float32Array(bufferSize);
|
|
37
|
+
let index = 0;
|
|
38
|
+
// the order does matter
|
|
39
|
+
for (let y = startRow; y < endRow; y++) {
|
|
40
|
+
for (let x = 0; x < width; x++) {
|
|
41
|
+
const ray = rayGen(x, height - 1 - y);
|
|
42
|
+
const color = __lambda(ray, { ...vars, scene });
|
|
43
|
+
if(!color) continue;
|
|
44
|
+
image[index++] = color.red;
|
|
45
|
+
image[index++] = color.green;
|
|
46
|
+
image[index++] = color.blue;
|
|
47
|
+
image[index++] = color.alpha;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return { image, startRow, endRow };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const getLambda = memoize((lambda, dependencies) => {
|
|
54
|
+
const __lambda = eval(`
|
|
55
|
+
${dependencies.map(d => d.toString()).join("\n")}
|
|
56
|
+
const __lambda = ${lambda};
|
|
57
|
+
__lambda;
|
|
58
|
+
`)
|
|
59
|
+
return __lambda;
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
if (IS_NODE) {
|
|
64
|
+
parentPort.on("message", async message => {
|
|
65
|
+
const input = message;
|
|
66
|
+
const output = await main(input);
|
|
67
|
+
parentPort.postMessage(output);
|
|
68
|
+
});
|
|
69
|
+
} else {
|
|
70
|
+
self.onmessage = async message => {
|
|
71
|
+
const input = message.data;
|
|
72
|
+
const output = await main(input);
|
|
73
|
+
postMessage(output);
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
self.onerror = e => {
|
|
77
|
+
console.log(`Caught error inside ray map worker ${e}`)
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
}
|
package/src/Camera/rayTrace.js
CHANGED
|
@@ -4,7 +4,7 @@ import Ray from "../Ray/Ray.js";
|
|
|
4
4
|
import Vec from "../Vector/Vector.js";
|
|
5
5
|
import { getBiLinearTexColor, getDefaultTexColor, getTexColor } from "./common.js";
|
|
6
6
|
|
|
7
|
-
export function rayTrace(ray, scene, params) {
|
|
7
|
+
export function rayTrace(ray, scene, params = {}) {
|
|
8
8
|
let { samplesPerPxl, bounces, variance, gamma, bilinearTexture } = params;
|
|
9
9
|
bounces = bounces ?? 10;
|
|
10
10
|
variance = variance ?? 0.001;
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import Camera from "./Camera.js";
|
|
2
2
|
import { rayTrace } from "./rayTrace.js";
|
|
3
|
-
import { CHANNELS } from "../
|
|
4
|
-
import { IS_NODE } from "../Utils/Constants.js";
|
|
3
|
+
import { CHANNELS, IS_NODE } from "../Utils/Constants.js";
|
|
5
4
|
import { deserializeScene } from "../Scene/utils.js";
|
|
6
5
|
import Color from "../Color/Color.js";
|
|
7
6
|
|
|
@@ -56,4 +55,6 @@ if (IS_NODE) {
|
|
|
56
55
|
const output = await main(input);
|
|
57
56
|
postMessage(output);
|
|
58
57
|
};
|
|
58
|
+
|
|
59
|
+
onerror = e => console.log("Caught error on rayTrace worker", e);
|
|
59
60
|
}
|
package/src/Geometry/Box.js
CHANGED
|
@@ -142,11 +142,15 @@ export default class Box {
|
|
|
142
142
|
}
|
|
143
143
|
|
|
144
144
|
serialize() {
|
|
145
|
-
|
|
145
|
+
return {
|
|
146
|
+
type: Box.name,
|
|
147
|
+
min: this.min.toArray(),
|
|
148
|
+
max: this.max.toArray()
|
|
149
|
+
}
|
|
146
150
|
}
|
|
147
151
|
|
|
148
|
-
deserialize() {
|
|
149
|
-
|
|
152
|
+
static async deserialize(json) {
|
|
153
|
+
return new Box(Vec.fromArray(json.min), Vec.fromArray(json.max));
|
|
150
154
|
}
|
|
151
155
|
|
|
152
156
|
static EMPTY = new Box();
|
package/src/Geometry/Line.js
CHANGED
|
@@ -77,7 +77,17 @@ export default class Line {
|
|
|
77
77
|
}
|
|
78
78
|
|
|
79
79
|
serialize() {
|
|
80
|
-
|
|
80
|
+
return {
|
|
81
|
+
type: Line.name,
|
|
82
|
+
name: this.name,
|
|
83
|
+
radius: this.radius,
|
|
84
|
+
emissive: this.emissive,
|
|
85
|
+
colors: this.colors.map(x => x.toArray()),
|
|
86
|
+
texCoords: this.texCoords.map(x => x.toArray()),
|
|
87
|
+
positions: this.positions.map(x => x.toArray()),
|
|
88
|
+
texture: this.texture ? this.texture.serialize() : undefined,
|
|
89
|
+
material: { type: this.material.type, args: this.material.args }
|
|
90
|
+
}
|
|
81
91
|
}
|
|
82
92
|
|
|
83
93
|
static deserialize(json) {
|
package/src/IO/IO.js
CHANGED
|
@@ -7,7 +7,7 @@ export function saveImageToFile(fileAddress, image) {
|
|
|
7
7
|
const ppmName = `${fileName}.ppm`;
|
|
8
8
|
writeFileSync(ppmName, createPPMFromImage(image));
|
|
9
9
|
if (extension !== "ppm") {
|
|
10
|
-
execSync(`ffmpeg -i ${ppmName} ${fileName}.${extension}`);
|
|
10
|
+
execSync(`ffmpeg -i ${ppmName} -y ${fileName}.${extension}`);
|
|
11
11
|
unlinkSync(ppmName)
|
|
12
12
|
}
|
|
13
13
|
}
|
|
@@ -89,7 +89,7 @@ export function saveImageStreamToVideo(fileAddress, streamWithImages, { imageGet
|
|
|
89
89
|
s = await s.tail;
|
|
90
90
|
}
|
|
91
91
|
if (!fps) fps = ite / time;
|
|
92
|
-
execSync(`ffmpeg -framerate ${fps} -i ${fileName}_%d.ppm ${fileName}.${extension}`);
|
|
92
|
+
execSync(`ffmpeg -framerate ${fps} -i ${fileName}_%d.ppm -y ${fileName}.${extension}`);
|
|
93
93
|
for (let i = 0; i < ite; i++) {
|
|
94
94
|
unlinkSync(`${fileName}_${i}.ppm`);
|
|
95
95
|
}
|
|
@@ -106,8 +106,8 @@ export function saveParallelImageStreamToVideo(fileAddress, parallelStreamOfImag
|
|
|
106
106
|
const promises = inputParamsPartitions.map((inputParams, i) => {
|
|
107
107
|
const spawnFile = "IO_parallel" + i + ".js";
|
|
108
108
|
writeFileSync(spawnFile, `
|
|
109
|
-
import * as _module from "./src/index.node.js"
|
|
110
109
|
import fs from "node:fs";
|
|
110
|
+
import * as _module from "./src/index.node.js"
|
|
111
111
|
const {
|
|
112
112
|
Box,
|
|
113
113
|
Vec,
|
package/src/Scene/BScene.js
CHANGED
package/src/Scene/NaiveScene.js
CHANGED
|
@@ -9,8 +9,15 @@ export default class NaiveScene {
|
|
|
9
9
|
this.sceneElements = [];
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
|
|
12
|
+
getHash() {
|
|
13
|
+
const elements = this.getElements();
|
|
14
|
+
let combinedHash = 0;
|
|
15
|
+
const prime = 31; // A prime number, typically used in hash functions
|
|
16
|
+
for (let i = 0; i < elements.length; i++) {
|
|
17
|
+
const hash = hashStr(elements[i].name);
|
|
18
|
+
combinedHash = (combinedHash * prime) ^ hash;
|
|
19
|
+
}
|
|
20
|
+
return combinedHash >>> 0; // unsigned shift operator, converts combinedHash to unsigned number
|
|
14
21
|
}
|
|
15
22
|
|
|
16
23
|
add(...elements) {
|
package/src/Scene/utils.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import Line from "../Geometry/Line.js";
|
|
1
2
|
import Sphere from "../Geometry/Sphere.js";
|
|
2
3
|
import Triangle from "../Geometry/Triangle.js";
|
|
3
4
|
import BScene from "./BScene.js";
|
|
@@ -21,6 +22,7 @@ export async function deserializeScene(sceneJson) {
|
|
|
21
22
|
const serializedElement = sceneData[i];
|
|
22
23
|
if (serializedElement.type === Triangle.name) sceneElements.push(await Triangle.deserialize(serializedElement, artifacts));
|
|
23
24
|
if (serializedElement.type === Sphere.name) sceneElements.push(await Sphere.deserialize(serializedElement, artifacts));
|
|
25
|
+
if (serializedElement.type === Line.name) sceneElements.push(await Line.deserialize(serializedElement, artifacts));
|
|
24
26
|
}
|
|
25
27
|
return new SceneClass(...params)
|
|
26
28
|
.addList(sceneElements);
|
package/src/Tela/Canvas.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import Color from "../Color/Color.js";
|
|
2
|
-
import { MAX_8BIT } from "../Utils/Constants.js";
|
|
3
|
-
import
|
|
4
|
-
import Tela, { CHANNELS } from "./Tela.js";
|
|
2
|
+
import { CHANNELS, MAX_8BIT } from "../Utils/Constants.js";
|
|
3
|
+
import Tela from "./Tela.js";
|
|
5
4
|
|
|
6
5
|
export default class Canvas extends Tela {
|
|
7
6
|
|
|
@@ -26,66 +25,6 @@ export default class Canvas extends Tela {
|
|
|
26
25
|
return this;
|
|
27
26
|
}
|
|
28
27
|
|
|
29
|
-
mapParallel = memoize((lambda, dependencies = []) => {
|
|
30
|
-
const N = navigator.hardwareConcurrency;
|
|
31
|
-
const w = this.width;
|
|
32
|
-
const h = this.height;
|
|
33
|
-
const fun = ({ _start_row, _end_row, _width_, _height_, _worker_id_, _vars_ }) => {
|
|
34
|
-
const image = new Float32Array(CHANNELS * _width_ * (_end_row - _start_row));
|
|
35
|
-
const startIndex = CHANNELS * _width_ * _start_row;
|
|
36
|
-
const endIndex = CHANNELS * _width_ * _end_row;
|
|
37
|
-
let index = 0;
|
|
38
|
-
for (let k = startIndex; k < endIndex; k += CHANNELS) {
|
|
39
|
-
const i = Math.floor(k / (CHANNELS * _width_));
|
|
40
|
-
const j = Math.floor((k / CHANNELS) % _width_);
|
|
41
|
-
const x = j;
|
|
42
|
-
const y = _height_ - 1 - i;
|
|
43
|
-
const color = lambda(x, y, { ..._vars_ });
|
|
44
|
-
if (!color) return;
|
|
45
|
-
image[index] = color.red;
|
|
46
|
-
image[index + 1] = color.green;
|
|
47
|
-
image[index + 2] = color.blue;
|
|
48
|
-
image[index + 3] = color.alpha;
|
|
49
|
-
index += CHANNELS;
|
|
50
|
-
}
|
|
51
|
-
return { image, _start_row, _end_row, _worker_id_ };
|
|
52
|
-
}
|
|
53
|
-
const workers = [...Array(N)].map(() => createWorker(fun, lambda, dependencies));
|
|
54
|
-
return {
|
|
55
|
-
run: (vars = {}) => {
|
|
56
|
-
// works better than Promise.all solution
|
|
57
|
-
return new Promise((resolve) => {
|
|
58
|
-
const allWorkersDone = [...Array(N)].fill(false);
|
|
59
|
-
workers.forEach((worker, k) => {
|
|
60
|
-
worker.onmessage = (event) => {
|
|
61
|
-
const { image, _start_row, _end_row, _worker_id_ } = event.data;
|
|
62
|
-
let index = 0;
|
|
63
|
-
const startIndex = CHANNELS * w * _start_row;
|
|
64
|
-
const endIndex = CHANNELS * w * _end_row;
|
|
65
|
-
for (let i = startIndex; i < endIndex; i++) {
|
|
66
|
-
this.image[i] = image[index];
|
|
67
|
-
index++;
|
|
68
|
-
}
|
|
69
|
-
allWorkersDone[_worker_id_] = true;
|
|
70
|
-
if (allWorkersDone.every(x => x)) {
|
|
71
|
-
return resolve(this.paint());
|
|
72
|
-
}
|
|
73
|
-
};
|
|
74
|
-
const ratio = Math.floor(h / N);
|
|
75
|
-
worker.postMessage({
|
|
76
|
-
_start_row: k * ratio,
|
|
77
|
-
_end_row: Math.min(h - 1, (k + 1) * ratio),
|
|
78
|
-
_width_: w,
|
|
79
|
-
_height_: h,
|
|
80
|
-
_worker_id_: k,
|
|
81
|
-
_vars_: vars
|
|
82
|
-
});
|
|
83
|
-
})
|
|
84
|
-
})
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
});
|
|
88
|
-
|
|
89
28
|
onMouseDown(lambda) {
|
|
90
29
|
this.canvas.addEventListener("mousedown", handleMouse(this, lambda), false);
|
|
91
30
|
this.canvas.addEventListener("touchstart", handleMouse(this, lambda), false);
|
|
@@ -218,20 +157,4 @@ function handleMouse(canvas, lambda) {
|
|
|
218
157
|
const y = Math.floor(h - 1 - my * h);
|
|
219
158
|
return lambda(x, y, event);
|
|
220
159
|
}
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
const createWorker = (main, lambda, dependencies) => {
|
|
224
|
-
const workerFile = `
|
|
225
|
-
const CHANNELS = ${CHANNELS};
|
|
226
|
-
${Color.toString()}
|
|
227
|
-
${dependencies.map(d => d.toString()).join("\n")}
|
|
228
|
-
const lambda = ${lambda.toString()};
|
|
229
|
-
const __main__ = ${main.toString()};
|
|
230
|
-
onmessage = e => {
|
|
231
|
-
const input = e.data
|
|
232
|
-
const output = __main__(input);
|
|
233
|
-
self.postMessage(output);
|
|
234
|
-
};
|
|
235
|
-
`;
|
|
236
|
-
return new Worker(URL.createObjectURL(new Blob([workerFile])));
|
|
237
|
-
};
|
|
160
|
+
}
|
package/src/Tela/Image.js
CHANGED
|
@@ -1,71 +1,9 @@
|
|
|
1
|
+
import Tela from "./Tela.js";
|
|
1
2
|
import Color from "../Color/Color.js";
|
|
2
3
|
import { readImageFrom } from "../IO/IO.js";
|
|
3
|
-
import { memoize } from "../Utils/Utils.js";
|
|
4
|
-
import Tela, { CHANNELS } from "./Tela.js";
|
|
5
|
-
import { Worker } from "node:worker_threads";
|
|
6
|
-
import os from "node:os";
|
|
7
4
|
|
|
8
5
|
export default class Image extends Tela {
|
|
9
6
|
|
|
10
|
-
mapParallel = memoize((lambda, dependencies = []) => {
|
|
11
|
-
const N = os.cpus().length;
|
|
12
|
-
const w = this.width;
|
|
13
|
-
const h = this.height;
|
|
14
|
-
const fun = ({ _start_row, _end_row, _width_, _height_, _worker_id_, _vars_ }) => {
|
|
15
|
-
const image = new Float32Array(CHANNELS * _width_ * (_end_row - _start_row));
|
|
16
|
-
const startIndex = CHANNELS * _width_ * _start_row;
|
|
17
|
-
const endIndex = CHANNELS * _width_ * _end_row;
|
|
18
|
-
let index = 0;
|
|
19
|
-
for (let k = startIndex; k < endIndex; k += CHANNELS) {
|
|
20
|
-
const i = Math.floor(k / (CHANNELS * _width_));
|
|
21
|
-
const j = Math.floor((k / CHANNELS) % _width_);
|
|
22
|
-
const x = j;
|
|
23
|
-
const y = _height_ - 1 - i;
|
|
24
|
-
const color = lambda(x, y, { ..._vars_ });
|
|
25
|
-
if (!color) continue;
|
|
26
|
-
image[index] = color.red;
|
|
27
|
-
image[index + 1] = color.green;
|
|
28
|
-
image[index + 2] = color.blue;
|
|
29
|
-
image[index + 3] = 1;
|
|
30
|
-
index += CHANNELS;
|
|
31
|
-
}
|
|
32
|
-
return { image, _start_row, _end_row, _worker_id_ };
|
|
33
|
-
}
|
|
34
|
-
const workers = [...Array(N)].map(() => createWorker(fun, lambda, dependencies));
|
|
35
|
-
return {
|
|
36
|
-
run: (vars = {}) => {
|
|
37
|
-
// in node promise.all is faster than the canvas method
|
|
38
|
-
return Promise
|
|
39
|
-
.all(workers.map((worker, k) => {
|
|
40
|
-
return new Promise((resolve) => {
|
|
41
|
-
worker.removeAllListeners('message');
|
|
42
|
-
worker.on("message", (message) => {
|
|
43
|
-
const { image, _start_row, _end_row } = message;
|
|
44
|
-
let index = 0;
|
|
45
|
-
const startIndex = CHANNELS * w * _start_row;
|
|
46
|
-
const endIndex = CHANNELS * w * _end_row;
|
|
47
|
-
for (let i = startIndex; i < endIndex; i++) {
|
|
48
|
-
this.image[i] = image[index];
|
|
49
|
-
index++;
|
|
50
|
-
}
|
|
51
|
-
return resolve();
|
|
52
|
-
});
|
|
53
|
-
const ratio = Math.floor(h / N);
|
|
54
|
-
worker.postMessage({
|
|
55
|
-
_start_row: k * ratio,
|
|
56
|
-
_end_row: Math.min(h - 1, (k + 1) * ratio),
|
|
57
|
-
_width_: w,
|
|
58
|
-
_height_: h,
|
|
59
|
-
_worker_id_: k,
|
|
60
|
-
_vars_: vars
|
|
61
|
-
});
|
|
62
|
-
})
|
|
63
|
-
}))
|
|
64
|
-
.then(() => this.paint());
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
});
|
|
68
|
-
|
|
69
7
|
serialize() {
|
|
70
8
|
return { type: Image.name, url: this.url };
|
|
71
9
|
}
|
|
@@ -104,27 +42,4 @@ export default class Image extends Tela {
|
|
|
104
42
|
return image.getPxl(x, y);
|
|
105
43
|
})
|
|
106
44
|
}
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
//========================================================================================
|
|
110
|
-
/* *
|
|
111
|
-
* Private *
|
|
112
|
-
* */
|
|
113
|
-
//========================================================================================
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
const createWorker = (main, lambda, dependencies) => {
|
|
117
|
-
const workerFile = `
|
|
118
|
-
const { parentPort } = require("node:worker_threads");
|
|
119
|
-
const CHANNELS = ${CHANNELS};
|
|
120
|
-
${dependencies.concat([Color]).map(d => d.toString()).join("\n")}
|
|
121
|
-
const lambda = ${lambda.toString()};
|
|
122
|
-
const __main__ = ${main.toString()};
|
|
123
|
-
parentPort.on("message", message => {
|
|
124
|
-
const output = __main__(message);
|
|
125
|
-
parentPort.postMessage(output);
|
|
126
|
-
});
|
|
127
|
-
`;
|
|
128
|
-
const worker = new Worker(workerFile, { eval: true });
|
|
129
|
-
return worker;
|
|
130
|
-
};
|
|
45
|
+
}
|