tela.js 1.0.4 → 1.1.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/README.md +37 -26
- package/package.json +3 -5
- package/src/Camera/Camera.js +69 -21
- package/src/Camera/common.js +36 -0
- package/src/Camera/parallel.js +74 -0
- package/src/Camera/raster.js +9 -39
- package/src/Camera/rayTrace.js +84 -0
- package/src/Camera/rayTraceWorker.js +59 -0
- package/src/Color/Color.js +1 -1
- package/src/Geometry/Box.js +8 -0
- package/src/Geometry/Geometry.js +16 -0
- package/src/Geometry/Line.js +19 -2
- package/src/Geometry/Mesh.js +13 -4
- package/src/Geometry/Path.js +3 -0
- package/src/Geometry/{Point.js → Sphere.js} +39 -8
- package/src/Geometry/Triangle.js +33 -4
- package/src/IO/IO.js +21 -37
- package/src/Material/Material.js +18 -1
- package/src/Scene/BScene.js +44 -27
- package/src/Scene/KScene.js +54 -37
- package/src/Scene/NaiveScene.js +21 -2
- package/src/Scene/RandomScene.js +3 -4
- package/src/Scene/VoxelScene.js +32 -29
- package/src/Scene/utils.js +27 -0
- package/src/Tela/Canvas.js +76 -236
- package/src/Tela/Image.js +88 -187
- package/src/Tela/Tela.js +365 -0
- package/src/Tela/Window.js +111 -190
- package/src/Tela/utils.js +17 -0
- package/src/Utils/Constants.js +4 -31
- package/src/Utils/Math.js +1 -122
- package/src/Utils/Stream.js +7 -5
- package/src/Utils/Utils.js +39 -11
- package/src/Utils/Utils3D.js +22 -4
- package/src/Vector/Vector.js +2 -3
- package/src/index.js +5 -9
- package/dist/node/index.js +0 -4859
- package/dist/web/index.js +0 -4396
- package/src/Camera/raytrace.js +0 -40
- package/src/Scene/Scene.js +0 -284
- package/src/Utils/Animation.js +0 -88
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# tela.js
|
|
2
2
|
|
|
3
|
-
Experimental graphic library from scratch, with reference implementation of computer graphics algorithms.
|
|
3
|
+
Experimental graphic library from scratch(software-only), with reference implementation of computer graphics algorithms.
|
|
4
4
|
|
|
5
5
|
## Purpose
|
|
6
6
|
|
|
@@ -29,25 +29,23 @@ Playground usage:
|
|
|
29
29
|
|
|
30
30
|
</body>
|
|
31
31
|
<script type="module">
|
|
32
|
-
import { Canvas,
|
|
32
|
+
import { Canvas, Color, loop } from "https://cdn.jsdelivr.net/npm/tela.js/src/index.js";
|
|
33
33
|
|
|
34
34
|
// You can also import from local file
|
|
35
|
-
// import { Canvas,
|
|
35
|
+
// import { Canvas, Color, loop } from "./node_modules/tela.js/src/index.js";
|
|
36
36
|
|
|
37
37
|
const width = 640;
|
|
38
38
|
const height = 480;
|
|
39
39
|
const canvas = Canvas.ofSize(640, 480);
|
|
40
|
-
|
|
41
|
-
.
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
)
|
|
48
|
-
})
|
|
40
|
+
loop(({ time, dt }) => {
|
|
41
|
+
document.title = `FPS: ${(Math.floor(1 / dt))}`;
|
|
42
|
+
canvas.map((x, y) => {
|
|
43
|
+
return Color.ofRGB(
|
|
44
|
+
((x * time) / width) % 1,
|
|
45
|
+
((y * time) / height) % 1
|
|
46
|
+
)
|
|
49
47
|
})
|
|
50
|
-
|
|
48
|
+
}).play();
|
|
51
49
|
document.body.appendChild(canvas.DOM);
|
|
52
50
|
|
|
53
51
|
</script>
|
|
@@ -59,23 +57,22 @@ Playground usage:
|
|
|
59
57
|
Install `tela.js` it using `npm install tela.js` / `bun add tela.js`.
|
|
60
58
|
|
|
61
59
|
```js
|
|
62
|
-
import {
|
|
60
|
+
import { loop, Color } from "tela.js/src/index.node.js";
|
|
63
61
|
import Window from "tela.js/src/Tela/Window.js";
|
|
64
62
|
|
|
65
63
|
const width = 640;
|
|
66
64
|
const height = 480;
|
|
67
65
|
const window = Window.ofSize(640, 480);
|
|
68
|
-
|
|
69
|
-
.
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
)
|
|
76
|
-
})
|
|
66
|
+
loop(({ time, dt }) => {
|
|
67
|
+
window.setTitle(`FPS: ${Math.floor(1 / dt)}`);
|
|
68
|
+
window.map((x, y) => {
|
|
69
|
+
return Color.ofRGB(
|
|
70
|
+
((x * time) / width) % 1,
|
|
71
|
+
((y * time) / height) % 1
|
|
72
|
+
)
|
|
77
73
|
})
|
|
78
|
-
|
|
74
|
+
})
|
|
75
|
+
.play();
|
|
79
76
|
```
|
|
80
77
|
|
|
81
78
|
And run it: `node index.mjs` / `bun index.js`
|
|
@@ -90,7 +87,7 @@ Install `tela.js` it using `npm install tela.js` / `bun add tela.js`.
|
|
|
90
87
|
Create a file:
|
|
91
88
|
```js
|
|
92
89
|
// index.js
|
|
93
|
-
import { Color, video } from "tela.js/
|
|
90
|
+
import { Color, video } from "tela.js/src/index.node.js";
|
|
94
91
|
|
|
95
92
|
const width = 640;
|
|
96
93
|
const height = 480;
|
|
@@ -111,7 +108,7 @@ video(
|
|
|
111
108
|
animation,
|
|
112
109
|
{ width, height, FPS }
|
|
113
110
|
)
|
|
114
|
-
.
|
|
111
|
+
.while(({ time }) => time < maxVideoTime);
|
|
115
112
|
```
|
|
116
113
|
|
|
117
114
|
And run it: `node index.mjs` / `bun index.js`
|
|
@@ -127,6 +124,13 @@ ffmpeg -version
|
|
|
127
124
|
|
|
128
125
|
```
|
|
129
126
|
|
|
127
|
+
## More examples
|
|
128
|
+
|
|
129
|
+
You can find more examples of usage in:
|
|
130
|
+
- [Playground (for web examples)](https://pedroth.github.io/tela.js)
|
|
131
|
+
- [Test folder (for desktop)](/test/node/)
|
|
132
|
+
|
|
133
|
+
|
|
130
134
|
# Dependencies
|
|
131
135
|
|
|
132
136
|
- [`bun`][bun]/[`node`][node]
|
|
@@ -136,6 +140,13 @@ ffmpeg -version
|
|
|
136
140
|
[Node][node] is preferred when running the demos (it is faster, [opened a bug in bun](https://github.com/oven-sh/bun/issues/9218)), [bun][bun] is needed to build the library.
|
|
137
141
|
|
|
138
142
|
|
|
143
|
+
# TODOs
|
|
144
|
+
|
|
145
|
+
- Fix parallel ray trace refresh bug
|
|
146
|
+
- Parallel ray map
|
|
147
|
+
- Serialize meshes not only triangles
|
|
148
|
+
- Optimize data serialization in parallel ray tracer
|
|
149
|
+
|
|
139
150
|
|
|
140
151
|
[ffmpeg]: https://ffmpeg.org/
|
|
141
152
|
[bun]: https://bun.sh/
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tela.js",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.1",
|
|
4
4
|
"author": "Pedroth",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -37,11 +37,9 @@
|
|
|
37
37
|
],
|
|
38
38
|
"license": "Apache-2.0",
|
|
39
39
|
"scripts": {
|
|
40
|
-
"build": "bun run clean; bun i; bun bundle.js",
|
|
41
|
-
"buildDev": "bun bundle.js --watch",
|
|
42
40
|
"serve": "bunx http-server",
|
|
43
|
-
"clean": "rm -fr node_modules; rm
|
|
44
|
-
"pub": "npm version patch;
|
|
41
|
+
"clean": "rm -fr node_modules; rm *.png *.jpeg *.ppm *.webm *.mp4",
|
|
42
|
+
"pub": "npm version patch; npm publish"
|
|
45
43
|
},
|
|
46
44
|
"type": "module"
|
|
47
45
|
}
|
package/src/Camera/Camera.js
CHANGED
|
@@ -1,29 +1,22 @@
|
|
|
1
|
-
import { Vec2, Vec3 } from "../Vector/Vector.js"
|
|
1
|
+
import Vec, { Vec2, Vec3 } from "../Vector/Vector.js"
|
|
2
2
|
import Ray from "../Ray/Ray.js";
|
|
3
|
-
import {
|
|
3
|
+
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
|
-
|
|
8
|
-
|
|
7
|
+
import { parallelWorkers } from "./parallel.js";
|
|
9
8
|
|
|
10
9
|
export default class Camera {
|
|
11
10
|
constructor(props = {}) {
|
|
12
|
-
const { lookAt, distanceToPlane, position } = props;
|
|
11
|
+
const { lookAt, distanceToPlane, position, orientCoords, orbitCoords } = props;
|
|
13
12
|
this.lookAt = lookAt ?? Vec3(0, 0, 0);
|
|
14
13
|
this.distanceToPlane = distanceToPlane ?? 1;
|
|
15
14
|
this.position = position ?? Vec3(3, 0, 0);
|
|
16
|
-
this._orientCoords = Vec2();
|
|
17
|
-
this._orbitCoords =
|
|
18
|
-
this.
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
clone() {
|
|
22
|
-
return new Camera({
|
|
23
|
-
lookAt: this.lookAt,
|
|
24
|
-
position: this.position,
|
|
25
|
-
distanceToPlane: this.distanceToPlane,
|
|
26
|
-
})
|
|
15
|
+
this._orientCoords = orientCoords ?? Vec2();
|
|
16
|
+
this._orbitCoords = orbitCoords;
|
|
17
|
+
if (this._orbitCoords) this.orbit(...this._orbitCoords.toArray());
|
|
18
|
+
else this.orient(...this._orientCoords.toArray());
|
|
19
|
+
this._orbitCoords = this._orbitCoords ?? Vec3(this.position.length());
|
|
27
20
|
}
|
|
28
21
|
|
|
29
22
|
look(at, up = Vec3(0, 0, 1)) {
|
|
@@ -91,11 +84,13 @@ export default class Camera {
|
|
|
91
84
|
return {
|
|
92
85
|
to: canvas => {
|
|
93
86
|
const w = canvas.width;
|
|
87
|
+
const invW = 1 / w;
|
|
94
88
|
const h = canvas.height;
|
|
89
|
+
const invH = 1 / h;
|
|
95
90
|
const ans = canvas.map((x, y) => {
|
|
96
91
|
const dirInLocal = [
|
|
97
|
-
(x
|
|
98
|
-
(y
|
|
92
|
+
(x * invW - 0.5),
|
|
93
|
+
(y * invH - 0.5),
|
|
99
94
|
this.distanceToPlane
|
|
100
95
|
]
|
|
101
96
|
const dir = Vec3(
|
|
@@ -112,11 +107,11 @@ export default class Camera {
|
|
|
112
107
|
}
|
|
113
108
|
}
|
|
114
109
|
|
|
115
|
-
sceneShot(scene, params
|
|
116
|
-
return this.rayMap(
|
|
110
|
+
sceneShot(scene, params) {
|
|
111
|
+
return this.rayMap(getRayTracer(scene, params));
|
|
117
112
|
}
|
|
118
113
|
|
|
119
|
-
reverseShot(scene, params
|
|
114
|
+
reverseShot(scene, params) {
|
|
120
115
|
return {
|
|
121
116
|
to: rasterGraphics(scene, this, params)
|
|
122
117
|
}
|
|
@@ -130,6 +125,18 @@ export default class Camera {
|
|
|
130
125
|
return this.rayMap(normalTrace(scene));
|
|
131
126
|
}
|
|
132
127
|
|
|
128
|
+
parallelShot(scene, params) {
|
|
129
|
+
return {
|
|
130
|
+
to: canvas => {
|
|
131
|
+
return Promise
|
|
132
|
+
.all(parallelWorkers(this, scene, canvas, params))
|
|
133
|
+
.then(() => {
|
|
134
|
+
canvas.paint();
|
|
135
|
+
})
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
133
140
|
toCameraCoord(x) {
|
|
134
141
|
let pointInCamCoord = x.sub(this.position);
|
|
135
142
|
pointInCamCoord = Vec3(
|
|
@@ -147,4 +154,45 @@ export default class Camera {
|
|
|
147
154
|
}
|
|
148
155
|
return x;
|
|
149
156
|
}
|
|
157
|
+
|
|
158
|
+
rayFromImage(width, height) {
|
|
159
|
+
const w = width;
|
|
160
|
+
const invW = 1 / w;
|
|
161
|
+
const h = height;
|
|
162
|
+
const invH = 1 / h;
|
|
163
|
+
return (x, y) => {
|
|
164
|
+
const dirInLocal = [
|
|
165
|
+
(x * invW - 0.5),
|
|
166
|
+
(y * invH - 0.5),
|
|
167
|
+
this.distanceToPlane
|
|
168
|
+
]
|
|
169
|
+
const dir = Vec3(
|
|
170
|
+
this.basis[0].x * dirInLocal[0] + this.basis[1].x * dirInLocal[1] + this.basis[2].x * dirInLocal[2],
|
|
171
|
+
this.basis[0].y * dirInLocal[0] + this.basis[1].y * dirInLocal[1] + this.basis[2].y * dirInLocal[2],
|
|
172
|
+
this.basis[0].z * dirInLocal[0] + this.basis[1].z * dirInLocal[1] + this.basis[2].z * dirInLocal[2]
|
|
173
|
+
)
|
|
174
|
+
.normalize()
|
|
175
|
+
return Ray(this.position, dir);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
serialize() {
|
|
180
|
+
return {
|
|
181
|
+
lookAt: this.lookAt.toArray(),
|
|
182
|
+
distanceToPlane: this.distanceToPlane,
|
|
183
|
+
position: this.position.toArray(),
|
|
184
|
+
orientCoords: this._orientCoords.toArray(),
|
|
185
|
+
orbitCoords: this._orbitCoords.toArray(),
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
static deserialize(json) {
|
|
190
|
+
return new Camera({
|
|
191
|
+
lookAt: Vec.fromArray(json.lookAt),
|
|
192
|
+
distanceToPlane: json.distanceToPlane,
|
|
193
|
+
position: Vec.fromArray(json.position),
|
|
194
|
+
orientCoords: Vec.fromArray(json.orientCoords),
|
|
195
|
+
orbitCoords: Vec.fromArray(json.orbitCoords)
|
|
196
|
+
})
|
|
197
|
+
}
|
|
150
198
|
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import Color from "../Color/Color.js";
|
|
2
|
+
import { lerp } from "../Utils/Math.js";
|
|
3
|
+
import { Vec2 } from "../Vector/Vector.js";
|
|
4
|
+
|
|
5
|
+
export function getDefaultTexColor(texUV) {
|
|
6
|
+
texUV = texUV.scale(16).map(x => x % 1)
|
|
7
|
+
return texUV.x < 0.5 && texUV.y < 0.5 ?
|
|
8
|
+
Color.BLACK :
|
|
9
|
+
texUV.x > 0.5 && texUV.y > 0.5 ?
|
|
10
|
+
Color.BLACK :
|
|
11
|
+
Color.PURPLE;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function getBiLinearTexColor(texUV, texture) {
|
|
15
|
+
const size = Vec2(texture.width, texture.height);
|
|
16
|
+
const texInt = texUV.mul(size);
|
|
17
|
+
|
|
18
|
+
const texInt0 = texInt.map(Math.floor);
|
|
19
|
+
const texInt1 = texInt0.add(Vec2(1, 0));
|
|
20
|
+
const texInt2 = texInt0.add(Vec2(0, 1));
|
|
21
|
+
const texInt3 = texInt0.add(Vec2(1, 1));
|
|
22
|
+
|
|
23
|
+
const color0 = texture.getPxl(...texInt0.toArray());
|
|
24
|
+
const color1 = texture.getPxl(...texInt1.toArray());
|
|
25
|
+
const color2 = texture.getPxl(...texInt2.toArray());
|
|
26
|
+
const color3 = texture.getPxl(...texInt3.toArray());
|
|
27
|
+
|
|
28
|
+
const x = texInt.sub(texInt0);
|
|
29
|
+
const bottomX = lerp(color0, color1)(x.x);
|
|
30
|
+
const topX = lerp(color2, color3)(x.x);
|
|
31
|
+
return lerp(bottomX, topX)(x.y);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function getTexColor(texUV, texture) {
|
|
35
|
+
return texture.getPxl(texUV.x * texture.width, texUV.y * texture.height);
|
|
36
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { IS_NODE, NUMBER_OF_CORES } from "../Utils/Constants.js";
|
|
2
|
+
import Color from "../Color/Color.js";
|
|
3
|
+
import { CHANNELS } from "../Tela/Tela.js";
|
|
4
|
+
|
|
5
|
+
//========================================================================================
|
|
6
|
+
/* *
|
|
7
|
+
* UTILS *
|
|
8
|
+
* */
|
|
9
|
+
//========================================================================================
|
|
10
|
+
|
|
11
|
+
const __Worker = IS_NODE ? (await import("node:worker_threads")).Worker : Worker;
|
|
12
|
+
class MyWorker {
|
|
13
|
+
constructor(path) {
|
|
14
|
+
this.path = path;
|
|
15
|
+
this.worker = new __Worker(path, { type: 'module' });
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
onMessage(lambda) {
|
|
19
|
+
if (IS_NODE) {
|
|
20
|
+
this.worker.removeAllListeners('message');
|
|
21
|
+
this.worker.on("message", lambda);
|
|
22
|
+
} else {
|
|
23
|
+
this.worker.onmessage = message => lambda(message.data);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
postMessage(message) {
|
|
28
|
+
return this.worker.postMessage(message);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
let WORKERS = [];
|
|
33
|
+
let prevSceneHash = undefined;
|
|
34
|
+
|
|
35
|
+
//========================================================================================
|
|
36
|
+
/* *
|
|
37
|
+
* MAIN *
|
|
38
|
+
* */
|
|
39
|
+
//========================================================================================
|
|
40
|
+
|
|
41
|
+
export function parallelWorkers(camera, scene, canvas, params = {}) {
|
|
42
|
+
// lazy loading workers
|
|
43
|
+
if (WORKERS.length === 0) WORKERS = [...Array(NUMBER_OF_CORES)].map(() => new MyWorker(`/src/Camera/rayTraceWorker.js`));
|
|
44
|
+
const w = canvas.width;
|
|
45
|
+
const h = canvas.height;
|
|
46
|
+
const isNewScene = prevSceneHash !== scene.hash;
|
|
47
|
+
if (isNewScene) prevSceneHash = scene.hash;
|
|
48
|
+
return WORKERS.map((worker, k) => {
|
|
49
|
+
return new Promise((resolve) => {
|
|
50
|
+
worker.onMessage(message => {
|
|
51
|
+
const { image, startRow, endRow, } = message;
|
|
52
|
+
let index = 0;
|
|
53
|
+
const startIndex = CHANNELS * w * startRow;
|
|
54
|
+
const endIndex = CHANNELS * w * endRow;
|
|
55
|
+
for (let i = startIndex; i < endIndex; i += CHANNELS) {
|
|
56
|
+
canvas.setPxlData(i, Color.ofRGB(image[index++], image[index++], image[index++], image[index++]));
|
|
57
|
+
}
|
|
58
|
+
resolve();
|
|
59
|
+
})
|
|
60
|
+
const ratio = Math.floor(h / WORKERS.length);
|
|
61
|
+
|
|
62
|
+
const message = {
|
|
63
|
+
width: w,
|
|
64
|
+
height: h,
|
|
65
|
+
params: params,
|
|
66
|
+
startRow: k * ratio,
|
|
67
|
+
endRow: Math.min(h, (k + 1) * ratio),
|
|
68
|
+
camera: camera.serialize(),
|
|
69
|
+
scene: isNewScene ? scene.serialize() : undefined
|
|
70
|
+
};
|
|
71
|
+
worker.postMessage(message);
|
|
72
|
+
});
|
|
73
|
+
})
|
|
74
|
+
}
|
package/src/Camera/raster.js
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import Color from "../Color/Color.js";
|
|
2
2
|
import Line from "../Geometry/Line.js";
|
|
3
|
-
import
|
|
3
|
+
import Sphere from "../Geometry/Sphere.js";
|
|
4
4
|
import Mesh from "../Geometry/Mesh.js";
|
|
5
5
|
import Triangle from "../Geometry/Triangle.js";
|
|
6
|
-
import { lerp } from "../Utils/Math.js";
|
|
7
6
|
import { Vec2 } from "../Vector/Vector.js";
|
|
7
|
+
import { getBiLinearTexColor, getDefaultTexColor, getTexColor } from "./common.js";
|
|
8
8
|
|
|
9
|
-
export function rasterGraphics(scene, camera, params) {
|
|
9
|
+
export function rasterGraphics(scene, camera, params = {}) {
|
|
10
10
|
const type2render = {
|
|
11
|
-
[
|
|
11
|
+
[Sphere.name]: rasterSphere,
|
|
12
12
|
[Line.name]: rasterLine,
|
|
13
13
|
[Triangle.name]: rasterTriangle,
|
|
14
14
|
[Mesh.name]: rasterMesh,
|
|
@@ -52,7 +52,7 @@ export function rasterGraphics(scene, camera, params) {
|
|
|
52
52
|
}
|
|
53
53
|
|
|
54
54
|
|
|
55
|
-
function
|
|
55
|
+
function rasterSphere({ canvas, camera, elem, w, h, zBuffer }) {
|
|
56
56
|
const point = elem;
|
|
57
57
|
const { distanceToPlane } = camera;
|
|
58
58
|
const { texCoord, texture, position, color, radius } = point;
|
|
@@ -71,6 +71,7 @@ function rasterPoint({ canvas, camera, elem, w, h, zBuffer }) {
|
|
|
71
71
|
y = Math.floor(y);
|
|
72
72
|
if (x < 0 || x >= w || y < 0 || y >= h) return;
|
|
73
73
|
const intRadius = Math.ceil((radius) * (distanceToPlane / z) * w);
|
|
74
|
+
const intRadiusSquare = intRadius * intRadius;
|
|
74
75
|
let finalColor = color;
|
|
75
76
|
if (
|
|
76
77
|
texture &&
|
|
@@ -83,6 +84,8 @@ function rasterPoint({ canvas, camera, elem, w, h, zBuffer }) {
|
|
|
83
84
|
for (let l = -intRadius; l < intRadius; l++) {
|
|
84
85
|
const xl = Math.max(0, Math.min(w - 1, x + k));
|
|
85
86
|
const yl = Math.floor(y + l);
|
|
87
|
+
const squareLength = k * k + l * l;
|
|
88
|
+
if (squareLength > intRadiusSquare) continue;
|
|
86
89
|
const [i, j] = canvas.canvas2grid(xl, yl);
|
|
87
90
|
const zBufferIndex = Math.floor(w * i + j);
|
|
88
91
|
if (z < zBuffer[zBufferIndex]) {
|
|
@@ -227,7 +230,7 @@ function rasterTriangle({ canvas, camera, elem, w, h, zBuffer, params }) {
|
|
|
227
230
|
params.bilinearTexture ?
|
|
228
231
|
getBiLinearTexColor(texUV, texture) :
|
|
229
232
|
getTexColor(texUV, texture) :
|
|
230
|
-
getDefaultTexColor(texUV);
|
|
233
|
+
c ? c : getDefaultTexColor(texUV); // TODO: review this
|
|
231
234
|
c = texColor;
|
|
232
235
|
}
|
|
233
236
|
const [i, j] = canvas.canvas2grid(x, y);
|
|
@@ -253,37 +256,4 @@ function lineCameraPlaneIntersection(vertexOut, vertexIn, camera) {
|
|
|
253
256
|
const alpha = (distanceToPlane - vertexOut.z) / v.z;
|
|
254
257
|
const p = vertexOut.add(v.scale(alpha));
|
|
255
258
|
return p;
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
function getDefaultTexColor(texUV) {
|
|
259
|
-
texUV = texUV.scale(16).map(x => x % 1)
|
|
260
|
-
return texUV.x < 0.5 && texUV.y < 0.5 ?
|
|
261
|
-
Color.BLACK :
|
|
262
|
-
texUV.x > 0.5 && texUV.y > 0.5 ?
|
|
263
|
-
Color.BLACK :
|
|
264
|
-
Color.PURPLE;
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
function getBiLinearTexColor(texUV, texture) {
|
|
268
|
-
const size = Vec2(texture.width, texture.height);
|
|
269
|
-
const texInt = texUV.mul(size);
|
|
270
|
-
|
|
271
|
-
const texInt0 = texInt.map(Math.floor);
|
|
272
|
-
const texInt1 = texInt0.add(Vec2(1, 0));
|
|
273
|
-
const texInt2 = texInt0.add(Vec2(0, 1));
|
|
274
|
-
const texInt3 = texInt0.add(Vec2(1, 1));
|
|
275
|
-
|
|
276
|
-
const color0 = texture.getPxl(...texInt0.toArray());
|
|
277
|
-
const color1 = texture.getPxl(...texInt1.toArray());
|
|
278
|
-
const color2 = texture.getPxl(...texInt2.toArray());
|
|
279
|
-
const color3 = texture.getPxl(...texInt3.toArray());
|
|
280
|
-
|
|
281
|
-
const x = texInt.sub(texInt0);
|
|
282
|
-
const bottomX = lerp(color0, color1)(x.x);
|
|
283
|
-
const topX = lerp(color2, color3)(x.x);
|
|
284
|
-
return lerp(bottomX, topX)(x.y);
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
function getTexColor(texUV, texture) {
|
|
288
|
-
return texture.getPxl(texUV.x * texture.width, texUV.y * texture.height);
|
|
289
259
|
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import Color from "../Color/Color.js";
|
|
2
|
+
import Triangle from "../Geometry/Triangle.js";
|
|
3
|
+
import Ray from "../Ray/Ray.js";
|
|
4
|
+
import Vec from "../Vector/Vector.js";
|
|
5
|
+
import { getBiLinearTexColor, getDefaultTexColor, getTexColor } from "./common.js";
|
|
6
|
+
|
|
7
|
+
export function rayTrace(ray, scene, params) {
|
|
8
|
+
let { samplesPerPxl, bounces, variance, gamma, bilinearTexture } = params;
|
|
9
|
+
bounces = bounces ?? 10;
|
|
10
|
+
variance = variance ?? 0.001;
|
|
11
|
+
samplesPerPxl = samplesPerPxl ?? 1;
|
|
12
|
+
gamma = gamma ?? 0.5;
|
|
13
|
+
bilinearTexture = bilinearTexture ?? false;
|
|
14
|
+
const invSamples = (bounces ?? 1) / samplesPerPxl
|
|
15
|
+
let c = Color.BLACK;
|
|
16
|
+
for (let i = 0; i < samplesPerPxl; i++) {
|
|
17
|
+
const epsilon = Vec.RANDOM(3).scale(variance);
|
|
18
|
+
const epsilonOrtho = epsilon.sub(ray.dir.scale(epsilon.dot(ray.dir)));
|
|
19
|
+
const r = Ray(ray.init, ray.dir.add(epsilonOrtho).normalize());
|
|
20
|
+
c = c.add(trace(r, scene, { bounces, bilinearTexture }));
|
|
21
|
+
}
|
|
22
|
+
return c.scale(invSamples).toGamma(gamma);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function getRayTracer(scene, params = {}) {
|
|
26
|
+
const lambda = ray => rayTrace(ray, scene, params);
|
|
27
|
+
return lambda;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function trace(ray, scene, options) {
|
|
31
|
+
const { bounces, bilinearTexture } = options;
|
|
32
|
+
if (bounces < 0) return Color.BLACK;
|
|
33
|
+
const hit = scene.interceptWithRay(ray);
|
|
34
|
+
if (!hit) return Color.BLACK;
|
|
35
|
+
const [, p, e] = hit;
|
|
36
|
+
const color = getColorFromElement(e, ray, { bilinearTexture });
|
|
37
|
+
const mat = e.material;
|
|
38
|
+
let r = mat.scatter(ray, p, e);
|
|
39
|
+
let finalC = trace(
|
|
40
|
+
r,
|
|
41
|
+
scene,
|
|
42
|
+
{ bounces: bounces - 1, bilinearTexture }
|
|
43
|
+
);
|
|
44
|
+
return e.emissive ? color.add(color.mul(finalC)) : color.mul(finalC);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function getColorFromElement(e, ray, params) {
|
|
48
|
+
if (Triangle.name === e.constructor.name) {
|
|
49
|
+
return getTriangleColor(e, ray, params);
|
|
50
|
+
}
|
|
51
|
+
return e.color ?? e.colors[0]
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function getTriangleColor(triangle, ray, params) {
|
|
55
|
+
const { tangents, positions, texCoords, texture, colors } = triangle;
|
|
56
|
+
|
|
57
|
+
const haveTextures = texture &&
|
|
58
|
+
texCoords &&
|
|
59
|
+
texCoords.length > 0 &&
|
|
60
|
+
!texCoords.some(x => x === undefined);
|
|
61
|
+
|
|
62
|
+
const v = ray.init.sub(positions[0]);
|
|
63
|
+
const u1 = tangents[0];
|
|
64
|
+
const u2 = tangents[1];
|
|
65
|
+
const r = ray.dir;
|
|
66
|
+
const detInv = 1 / u1.cross(u2).dot(r);
|
|
67
|
+
const alpha = v.cross(u2).dot(r) * detInv
|
|
68
|
+
const beta = u1.cross(v).dot(r) * detInv;
|
|
69
|
+
if (haveTextures) {
|
|
70
|
+
const texUV = texCoords[0].scale(1 - alpha - beta)
|
|
71
|
+
.add(texCoords[1].scale(alpha))
|
|
72
|
+
.add(texCoords[2].scale(beta));
|
|
73
|
+
const texColor = texture ?
|
|
74
|
+
params.bilinearTexture ?
|
|
75
|
+
getBiLinearTexColor(texUV, texture) :
|
|
76
|
+
getTexColor(texUV, texture) :
|
|
77
|
+
getDefaultTexColor(texUV);
|
|
78
|
+
return texColor;
|
|
79
|
+
}
|
|
80
|
+
return colors[0]
|
|
81
|
+
.scale(alpha)
|
|
82
|
+
.add(colors[1].scale(beta))
|
|
83
|
+
.add(colors[2].scale(1 - alpha - beta))
|
|
84
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import Camera from "./Camera.js";
|
|
2
|
+
import { rayTrace } from "./rayTrace.js";
|
|
3
|
+
import { CHANNELS } from "../Tela/Tela.js";
|
|
4
|
+
import { IS_NODE } from "../Utils/Constants.js";
|
|
5
|
+
import { deserializeScene } from "../Scene/utils.js";
|
|
6
|
+
import Color from "../Color/Color.js";
|
|
7
|
+
|
|
8
|
+
const parentPort = IS_NODE ? (await import("node:worker_threads")).parentPort : undefined;
|
|
9
|
+
|
|
10
|
+
let scene = undefined;
|
|
11
|
+
|
|
12
|
+
function getScene(serializedScene) {
|
|
13
|
+
return deserializeScene(serializedScene).then(s => s.rebuild());
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
async function main(inputs) {
|
|
17
|
+
const {
|
|
18
|
+
startRow,
|
|
19
|
+
endRow,
|
|
20
|
+
width,
|
|
21
|
+
height,
|
|
22
|
+
params,
|
|
23
|
+
scene: serializedScene,
|
|
24
|
+
camera: serializedCamera,
|
|
25
|
+
} = inputs;
|
|
26
|
+
scene = serializedScene ? await getScene(serializedScene) : scene;
|
|
27
|
+
const camera = Camera.deserialize(serializedCamera);
|
|
28
|
+
const rayGen = camera.rayFromImage(width, height);
|
|
29
|
+
const bufferSize = width * (endRow - startRow + 1) * CHANNELS;
|
|
30
|
+
const image = new Float32Array(bufferSize);
|
|
31
|
+
let index = 0;
|
|
32
|
+
// the order does matter
|
|
33
|
+
for (let y = startRow; y < endRow; y++) {
|
|
34
|
+
for (let x = 0; x < width; x++) {
|
|
35
|
+
const ray = rayGen(x, height - 1 - y);
|
|
36
|
+
const color = scene ? rayTrace(ray, scene, params) : Color.random();
|
|
37
|
+
image[index++] = color.red;
|
|
38
|
+
image[index++] = color.green;
|
|
39
|
+
image[index++] = color.blue;
|
|
40
|
+
image[index++] = color.alpha;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return { image, startRow, endRow };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
if (IS_NODE) {
|
|
48
|
+
parentPort.on("message", async message => {
|
|
49
|
+
const input = message;
|
|
50
|
+
const output = await main(input);
|
|
51
|
+
parentPort.postMessage(output);
|
|
52
|
+
});
|
|
53
|
+
} else {
|
|
54
|
+
onmessage = async message => {
|
|
55
|
+
const input = message.data;
|
|
56
|
+
const output = await main(input);
|
|
57
|
+
postMessage(output);
|
|
58
|
+
};
|
|
59
|
+
}
|
package/src/Color/Color.js
CHANGED
package/src/Geometry/Box.js
CHANGED
package/src/Geometry/Geometry.js
CHANGED
|
@@ -35,4 +35,20 @@ export default class Geometry {
|
|
|
35
35
|
interceptWithRay(ray) {
|
|
36
36
|
throw Error("Not implemented");
|
|
37
37
|
}
|
|
38
|
+
|
|
39
|
+
sample() {
|
|
40
|
+
throw Error("Not implemented");
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
equals(obj) {
|
|
44
|
+
return this === obj;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
serialize() {
|
|
48
|
+
throw Error("Not implemented");
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
static deserialize(json) {
|
|
52
|
+
throw Error("Not implemented");
|
|
53
|
+
}
|
|
38
54
|
}
|