├── libs
├── ray.js
├── geometry
│ ├── Primitive.js
│ ├── CPoly.js
│ ├── Star.js
│ ├── CanvasBoundsIntersection.js
│ ├── Geometry.js
│ ├── AABB.js
│ ├── Circle.js
│ └── Edge.js
├── globals.js
├── material
│ ├── beamEmitter.js
│ ├── material.js
│ ├── lambertEmitter.js
│ ├── contributionModifier.js
│ ├── lambert.js
│ ├── microfacet.js
│ ├── emitter.js
│ ├── dielectric.js
│ └── experimentalDielectric.js
├── createScene.js
├── scenes
│ ├── features
│ │ ├── offscreenCanvas.js
│ │ ├── dispersion.js
│ │ ├── starGeometry.js
│ │ ├── quads.js
│ │ ├── dielectricBeerLambertAbsorption.js
│ │ ├── sellmierCoefficients.js
│ │ ├── perlin-noise.js
│ │ ├── contributionModifierExample.js
│ │ ├── motionBlur.js
│ │ ├── archs.js
│ │ └── castaway.js
│ └── examples
│ │ ├── primitives-1.js
│ │ ├── tiling1.js
│ │ └── circle-packing-scene.js
├── utils.js
├── dependencies
│ ├── download.js
│ └── quick-noise.js
├── scene.js
├── videoManager.js
├── main.js
├── bvh.js
└── worker.js
├── main.css
├── .gitignore
├── TODO.md
├── index.html
├── LICENSE
└── README.md
/libs/ray.js:
--------------------------------------------------------------------------------
1 | class Ray {
2 | constructor(origin, direction) {
3 | this.o = origin;
4 | this.d = direction;
5 | }
6 | }
7 |
8 | export { Ray }
9 |
--------------------------------------------------------------------------------
/main.css:
--------------------------------------------------------------------------------
1 | html, body {
2 | width: 100%;
3 | height: 100%;
4 | overflow: hidden;
5 | margin: 0;
6 |
7 | display: flex;
8 | justify-content: center;
9 | align-items: center;
10 | }
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/ignore-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 |
6 | # testing
7 | /coverage
8 |
9 | # production
10 |
11 | # misc
12 | /renders
13 | /misc
14 | /insp
15 |
16 | .DS_Store
17 | .env.local
18 | .env.development.local
19 | .env.test.local
20 | .env.production.local
21 |
22 | npm-debug.log*
23 | yarn-debug.log*
24 | yarn-error.log*
--------------------------------------------------------------------------------
/TODO.md:
--------------------------------------------------------------------------------
1 | • if(previousPixel[0] == px && previousPixel[1] == py || px >= canvasSize || py >= canvasSize || px < 0 || py < 0) {
2 | there needs to be an intersection test between the ray and the world scene, and only draw samples inside that area otherwise we can waste a lot of cpu cycles for nothing
3 |
4 | • give an option to use to dot( wi, normal ) as an absorption factor for Dielectric materials since it looked cool on some renders
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/libs/geometry/Primitive.js:
--------------------------------------------------------------------------------
1 | class Primitive {
2 | constructor() {
3 | this.material = undefined;
4 | }
5 |
6 | computeAABB() { /* not implemented */ }
7 |
8 | intersect(ray) { /* not implemented */ }
9 | setMaterial(material) {
10 | this.material = material;
11 | }
12 | getMaterial() {
13 | return this.material;
14 | }
15 |
16 | getRandomPoint() { /* not implemented */ }
17 |
18 | flipNormal() { /* not implemented */ }
19 | rotate(radians) { /* not implemented */ }
20 | translate(point) { /* not implemented */ }
21 | scale(amount) { /* not implemented */ }
22 | }
23 |
24 | export { Primitive }
--------------------------------------------------------------------------------
/libs/geometry/CPoly.js:
--------------------------------------------------------------------------------
1 | import { Geometry } from "./Geometry.js";
2 | import { Edge } from "./Edge.js";
3 | import { glMatrix, vec2, vec3 } from "./../dependencies/gl-matrix-es6.js";
4 |
5 | class CPoly extends Geometry {
6 | constructor(sides, x, y, radius) {
7 | super();
8 |
9 | for(let i = 0; i < sides; i++) {
10 | let a1 = (i / sides) * Math.PI * 2;
11 | let a2 = ((i+1) / sides) * Math.PI * 2;
12 |
13 | let x1 = Math.cos(a1) * radius;
14 | let y1 = Math.sin(a1) * radius;
15 | let x2 = Math.cos(a2) * radius;
16 | let y2 = Math.sin(a2) * radius;
17 |
18 | let edge = new Edge(x2 + x, y2 + y, x1 + x, y1 + y);
19 | this.addPrimitive(edge);
20 | }
21 | }
22 | }
23 |
24 | export { CPoly }
--------------------------------------------------------------------------------
/libs/geometry/Star.js:
--------------------------------------------------------------------------------
1 | import { Geometry } from "./Geometry.js";
2 | import { Edge } from "./Edge.js";
3 | import { glMatrix, vec2, vec3 } from "../dependencies/gl-matrix-es6.js";
4 |
5 | class Star extends Geometry {
6 | constructor(sides, x, y, radius1, radius2) {
7 | super();
8 |
9 | for(let i = 0; i < sides*2; i++) {
10 | let a1 = (i / (sides*2)) * Math.PI * 2;
11 | let a2 = ((i+1) / (sides*2)) * Math.PI * 2;
12 |
13 | let rad1 = radius1;
14 | let rad2 = radius2;
15 | if(i%2 === 1) {
16 | rad1 = radius2;
17 | rad2 = radius1;
18 | }
19 |
20 | let x1 = Math.cos(a1) * rad1;
21 | let y1 = Math.sin(a1) * rad1;
22 | let x2 = Math.cos(a2) * rad2;
23 | let y2 = Math.sin(a2) * rad2;
24 |
25 | let edge = new Edge(x2 + x, y2 + y, x1 + x, y1 + y);
26 | this.addPrimitive(edge);
27 | }
28 | }
29 | }
30 |
31 | export { Star }
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2019
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/libs/globals.js:
--------------------------------------------------------------------------------
1 | var Globals = {
2 |
3 | // Sampling
4 | epsilon: 0.00005,
5 | highPrecision: false, // if set to true, uses Float64Arrays which are 2x slower to work with
6 | USE_STRATIFIED_SAMPLING: true,
7 | samplingRatioPerPixelCovered: 0.14,
8 | LIGHT_BOUNCES: 35,
9 | skipBounce: 0,
10 |
11 | // Threading
12 | workersCount: 5,
13 | PHOTONS_PER_UPDATE: 50000,
14 |
15 | // Environment
16 | WORLD_SIZE: 20,
17 | worldAttenuation: 0.01,
18 |
19 | // Video export
20 | registerVideo: false,
21 | photonsPerVideoFrame: 5000000,
22 | framesPerSecond: 30,
23 | framesCount: 30,
24 | frameStart: 0,
25 |
26 | // Motion blur
27 | motionBlur: false,
28 | motionBlurFramePhotons: 5000,
29 |
30 | // Offscreen canvas
31 | deactivateOffscreenCanvas: true, // setting it to false slows down render times by about 1.7x
32 | offscreenCanvasCPow: 1.1,
33 |
34 | // Canvas size
35 | canvasSize: {
36 | width: 1300,
37 | height: 1000,
38 | },
39 |
40 | // Reinhard tonemapping
41 | toneMapping: true,
42 | gamma: 2.2,
43 | exposure: 1,
44 | }
45 |
46 | export { Globals };
--------------------------------------------------------------------------------
/libs/geometry/CanvasBoundsIntersection.js:
--------------------------------------------------------------------------------
1 | import { glMatrix, vec2 } from "./../dependencies/gl-matrix-es6.js";
2 |
3 | class CanvasBoundsIntersection {
4 | constructor(WORLD_SIZE_X, WORLD_SIZE_Y) {
5 | this.min = vec2.fromValues(-WORLD_SIZE_X / 2, -WORLD_SIZE_Y / 2);
6 | this.max = vec2.fromValues(+WORLD_SIZE_X / 2, +WORLD_SIZE_Y / 2);
7 | }
8 |
9 | intersect(ray, result) {
10 | let dirfrac_x = 1 / ray.d[0];
11 | let dirfrac_y = 1 / ray.d[1];
12 |
13 | let t1 = (this.min[0] - ray.o[0]) * dirfrac_x;
14 | let t2 = (this.max[0] - ray.o[0]) * dirfrac_x;
15 | let t3 = (this.min[1] - ray.o[1]) * dirfrac_y;
16 | let t4 = (this.max[1] - ray.o[1]) * dirfrac_y;
17 |
18 | let tmin = Math.max(Math.min(t1, t2), Math.min(t3, t4));
19 | let tmax = Math.min(Math.max(t1, t2), Math.max(t3, t4));
20 |
21 | // if tmax < 0, ray (line) is intersecting AABB, but the whole AABB is behind us
22 | if (tmax < 0) return false;
23 |
24 | // if tmin > tmax, ray doesn't intersect AABB
25 | if (tmin > tmax) return false;
26 |
27 | result.tmin = tmin;
28 | result.tmax = tmax;
29 |
30 | return true;
31 | }
32 | }
33 |
34 | export { CanvasBoundsIntersection };
--------------------------------------------------------------------------------
/libs/material/beamEmitter.js:
--------------------------------------------------------------------------------
1 | import { EmitterMaterial } from "./emitter.js";
2 | import { Ray } from "../ray.js";
3 | import { glMatrix, vec2, vec3 } from "./../dependencies/gl-matrix-es6.js";
4 | import { Globals } from "../globals.js";
5 |
6 | class BeamEmitterMaterial extends EmitterMaterial {
7 | constructor(options) {
8 | super(options);
9 |
10 | options.beamDirection = options.beamDirection !== undefined ? options.beamDirection : vec2.fromValues(-1, 0);
11 | this.beamDirection = options.beamDirection;
12 | }
13 |
14 | getPhoton(geometryObject) {
15 | let res = geometryObject.getRandomPoint();
16 | let point = res.p;
17 |
18 | let normal = res.normal;
19 |
20 | let offsetDir = 1;
21 | if(vec2.dot(normal, this.beamDirection) < 0) {
22 | offsetDir = -1;
23 | }
24 |
25 | // avoids self-intersections
26 | point[0] += res.normal[0] * 0.00001 * offsetDir;
27 | point[1] += res.normal[1] * 0.00001 * offsetDir;
28 |
29 | let newDirection = vec2.clone(this.beamDirection);
30 | vec2.normalize(newDirection, newDirection);
31 |
32 | return {
33 | ray: new Ray(point, newDirection),
34 | spectrum: this.getSpectrum(this.color),
35 | }
36 | }
37 | }
38 |
39 | export { BeamEmitterMaterial }
--------------------------------------------------------------------------------
/libs/material/material.js:
--------------------------------------------------------------------------------
1 | import { glMatrix, vec2, mat2 } from "./../dependencies/gl-matrix-es6.js";
2 | import { Globals } from "../globals.js";
3 |
4 | class Material {
5 | constructor(options) {
6 | if(!options) options = { };
7 |
8 | // remember: 0 is a valid opacity option, so we need to check for undefined instead of just going with: options.opacity || 1
9 | this.opacity = options.opacity !== undefined ? options.opacity : 1;
10 | }
11 |
12 | opacityTest(t, worldAttenuation, ray, contribution) {
13 | // opacity test, if it passes we're going to let the ray pass through the object
14 | if(Math.random() > this.opacity) {
15 |
16 | let wa = Math.exp(-t * worldAttenuation);
17 | contribution.r *= wa;
18 | contribution.g *= wa;
19 | contribution.b *= wa;
20 |
21 |
22 | let newOrigin = vec2.create();
23 | // it's important that the epsilon value is subtracted/added instead of doing t * 0.999999 since that caused floating point precision issues
24 | vec2.scaleAndAdd(newOrigin, ray.o, ray.d, t + Globals.epsilon);
25 | vec2.copy(ray.o, newOrigin);
26 |
27 | return true;
28 | }
29 |
30 | return false;
31 | }
32 |
33 | computeScattering() {
34 |
35 | }
36 | }
37 |
38 | export { Material }
--------------------------------------------------------------------------------
/libs/createScene.js:
--------------------------------------------------------------------------------
1 | import { Edge } from "./geometry/Edge.js";
2 | import { Star } from "./geometry/Star.js";
3 | import { LambertMaterial } from "./material/lambert.js";
4 | import { LambertEmitterMaterial } from "./material/lambertEmitter.js";
5 |
6 | function createScene(scene, workerData, motionBlurT, ctx, frameNumber) {
7 |
8 | createWorldBounds(scene);
9 |
10 | let star = new Star(5, 0, 0, 5, 2.5);
11 | let material = new LambertMaterial({ opacity: 0.6, color: [1, 0.05, 0] });
12 | scene.add(star, material);
13 |
14 | let lightSource = new Edge(10, -5, 10, 5);
15 | let lightMaterial = new LambertEmitterMaterial({ color: [500, 500, 500] });
16 | scene.add(lightSource, lightMaterial);
17 | }
18 |
19 | function createWorldBounds(scene) {
20 | let edgeMaterial = new LambertMaterial({ opacity: 1 });
21 | let tbound = 11;
22 | let lbound = 19.5;
23 | let rbound = 19.5;
24 | let bbound = 11;
25 | let ledge = new Edge(-lbound, -bbound, -lbound, tbound, 0, 1, 0);
26 | let redge = new Edge( rbound, -bbound, rbound, tbound, 0, -1, 0);
27 | let tedge = new Edge(-lbound, tbound, rbound, tbound, 0, 0, -1);
28 | let bedge = new Edge(-lbound, -bbound, rbound, -bbound, 0, 0, 1);
29 |
30 | scene.add(ledge, edgeMaterial);
31 | scene.add(redge, edgeMaterial);
32 | scene.add(tedge, edgeMaterial);
33 | scene.add(bedge, edgeMaterial);
34 | }
35 |
36 | export { createScene };
--------------------------------------------------------------------------------
/libs/geometry/Geometry.js:
--------------------------------------------------------------------------------
1 | class Geometry {
2 | constructor() {
3 | this.primitives = [];
4 | }
5 |
6 | addPrimitive(primitive) {
7 | this.primitives.push(primitive);
8 | }
9 | setMaterial(material) {
10 | for(let i = 0; i < this.primitives.length; i++) {
11 | this.primitives[i].setMaterial(material);
12 | }
13 | }
14 | setMaterials(materialsArray) {
15 | for(let i = 0; i < this.primitives.length; i++) {
16 | this.primitives[i].setMaterial(materialsArray[i]);
17 | }
18 | }
19 | getPrimitives() {
20 | return this.primitives;
21 | }
22 | rotate(radians) {
23 | for(let i = 0; i < this.primitives.length; i++) {
24 | this.primitives[i].rotate(radians);
25 | }
26 |
27 | return this;
28 | }
29 | translate(point) {
30 | for(let i = 0; i < this.primitives.length; i++) {
31 | this.primitives[i].translate(point);
32 | }
33 |
34 | return this;
35 | }
36 | scale(amount) {
37 | for(let i = 0; i < this.primitives.length; i++) {
38 | this.primitives[i].scale(amount);
39 | }
40 |
41 | return this;
42 | }
43 | flipNormal() {
44 | for(let i = 0; i < this.primitives.length; i++) {
45 | this.primitives[i].flipNormal();
46 | }
47 |
48 | return this;
49 | }
50 | }
51 |
52 | export { Geometry }
--------------------------------------------------------------------------------
/libs/material/lambertEmitter.js:
--------------------------------------------------------------------------------
1 | import { Material } from "./material.js";
2 | import { Ray } from "../ray.js";
3 | import { glMatrix, vec2, mat2, vec3 } from "../dependencies/gl-matrix-es6.js";
4 | import { Globals } from "../globals.js";
5 | import { EmitterMaterial } from "./emitter.js";
6 |
7 | class LambertEmitterMaterial extends EmitterMaterial {
8 | constructor(options) {
9 | super(options);
10 | }
11 |
12 | getPhoton(geometryObject) {
13 | let res = geometryObject.getRandomPoint();
14 | let point = res.p;
15 | // avoids self-intersections
16 | point[0] += res.normal[0] * 0.00001; // normals are always normalized to a length of 1, so there shouldn't be a precision problem with *= 0.00001
17 | point[1] += res.normal[1] * 0.00001;
18 | let input_normal = res.normal;
19 |
20 |
21 | let newDirection = vec2.create();
22 |
23 | // evaluate BRDF
24 | let xi = Math.random();
25 | let sinThetaI = 2 * xi - 1;
26 | let cosThetaI = Math.sqrt(1 - sinThetaI*sinThetaI);
27 | let tv1 = vec2.fromValues(sinThetaI, cosThetaI);
28 |
29 | // normal rotation matrix
30 | let m = mat2.fromValues(input_normal[1], -input_normal[0], input_normal[0], input_normal[1]);
31 | vec2.transformMat2(tv1, tv1, m);
32 |
33 | vec2.copy(newDirection, tv1);
34 | // evaluate BRDF - END
35 |
36 | let spectrum = this.getSpectrum(this.color);
37 |
38 | return {
39 | ray: new Ray(point, newDirection),
40 | spectrum: spectrum
41 | }
42 | }
43 | }
44 |
45 | export { LambertEmitterMaterial }
--------------------------------------------------------------------------------
/libs/material/contributionModifier.js:
--------------------------------------------------------------------------------
1 | import { Material } from "./material.js";
2 | import { glMatrix, vec2, mat2 } from "./../dependencies/gl-matrix-es6.js";
3 | import { Globals } from "../globals.js";
4 |
5 | class ContributionModifierMaterial extends Material {
6 | constructor(options) {
7 | super(options);
8 |
9 | if(!options) options = { };
10 |
11 | this.modifier = options.modifier !== undefined ? options.modifier : 0;
12 | }
13 |
14 | computeScattering(ray, input_normal, t, contribution, worldAttenuation, wavelength) {
15 |
16 | let dot = vec2.dot(ray.d, input_normal);
17 |
18 | if (dot < 0) { // light is entering the surface
19 | contribution.r *= this.modifier;
20 | contribution.g *= this.modifier;
21 | contribution.b *= this.modifier;
22 | } else { // light is exiting the surface, restore original contribution
23 | // restore contribution previous to hitting this object
24 | contribution.r *= (1 / this.modifier);
25 | contribution.g *= (1 / this.modifier);
26 | contribution.b *= (1 / this.modifier);
27 | }
28 |
29 |
30 | let wa = Math.exp(-t * worldAttenuation);
31 | contribution.r *= wa;
32 | contribution.g *= wa;
33 | contribution.b *= wa;
34 |
35 |
36 | let newOrigin = vec2.create(); // light needs to always pass through the object with this material
37 | vec2.scaleAndAdd(newOrigin, ray.o, ray.d, t + Globals.epsilon);
38 | vec2.copy(ray.o, newOrigin);
39 | }
40 | }
41 |
42 | export { ContributionModifierMaterial }
--------------------------------------------------------------------------------
/libs/scenes/features/offscreenCanvas.js:
--------------------------------------------------------------------------------
1 | import { Edge } from "./geometry/Edge.js";
2 | import { Circle } from "./geometry/Circle.js";
3 | import { LambertMaterial } from "./material/lambert.js";
4 | import { LambertEmitterMaterial } from "./material/lambertEmitter.js";
5 |
6 | function createScene(scene, workerData, motionBlurT, ctx, frameNumber) {
7 |
8 | createWorldBounds(scene);
9 |
10 |
11 | scene.add(new Circle(0,0,3), new LambertMaterial({ opacity: 0.6, color: [1, 0.25, 0] }));
12 |
13 |
14 | // you need to disable Globals.deactivateOffscreenCanvas to be able to
15 | // run this example!
16 |
17 |
18 | for(let i = 0; i < 5; i++) {
19 | ctx.strokeStyle = "#444";
20 | if(i % 2 === 1) ctx.strokeStyle = "#aaa";
21 |
22 | ctx.lineWidth = (0.1 + i * 0.05);
23 | ctx.beginPath();
24 | ctx.arc(0, 0, 3.5 + i * 0.95, 0, Math.PI*2);
25 | ctx.stroke();
26 | }
27 | }
28 |
29 | function createWorldBounds(scene) {
30 | let edgeMaterial = new LambertMaterial({ opacity: 1 });
31 | let tbound = 11;
32 | let lbound = 19.5;
33 | let rbound = 19.5;
34 | let bbound = 11;
35 | let ledge = new Edge(-lbound, -bbound, -lbound, tbound, 0, 1, 0);
36 | let redge = new Edge( rbound, -bbound, rbound, tbound, 0, -1, 0);
37 | let tedge = new Edge(-lbound, tbound, rbound, tbound, 0, 0, -1);
38 | let bedge = new Edge(-lbound, -bbound, rbound, -bbound, 0, 0, 1);
39 |
40 | scene.add(ledge, edgeMaterial);
41 | scene.add(redge, new LambertEmitterMaterial({ color: [500, 500, 500] }));
42 | scene.add(tedge, edgeMaterial);
43 | scene.add(bedge, edgeMaterial);
44 | }
45 |
46 | export { createScene };
--------------------------------------------------------------------------------
/libs/utils.js:
--------------------------------------------------------------------------------
1 | let Utils = {
2 |
3 | };
4 |
5 | function sfc32(a, b, c, d) {
6 | return function() {
7 | a >>>= 0; b >>>= 0; c >>>= 0; d >>>= 0;
8 | var t = (a + b) | 0;
9 | a = b ^ b >>> 9;
10 | b = c + (c << 3) | 0;
11 | c = (c << 21 | c >>> 11);
12 | d = d + 1 | 0;
13 | t = t + d | 0;
14 | c = c + t | 0;
15 | return (t >>> 0) / 4294967296;
16 | }
17 | }
18 |
19 | function xmur3(str) {
20 | for(var i = 0, h = 1779033703 ^ str.length; i < str.length; i++)
21 | h = Math.imul(h ^ str.charCodeAt(i), 3432918353);
22 | h = h << 13 | h >>> 19;
23 | return function() {
24 | h = Math.imul(h ^ h >>> 16, 2246822507);
25 | h = Math.imul(h ^ h >>> 13, 3266489909);
26 | return (h ^= h >>> 16) >>> 0;
27 | }
28 | }
29 |
30 | var seed = xmur3("apples");
31 | Utils.rand = sfc32(seed(), seed(), seed(), seed());
32 |
33 | Utils.setSeed = function(string) {
34 | seed = xmur3(string);
35 | Utils.rand = sfc32(seed(), seed(), seed(), seed());
36 | }
37 |
38 | Utils.smoothstep = function(t) {
39 | return t * t * (3.0 - 2.0 * t);
40 | }
41 |
42 | Utils.hslToRgb = function(h, s, l) {
43 | var r, g, b;
44 |
45 | if(s == 0){
46 | r = g = b = l; // achromatic
47 | }else{
48 | var hue2rgb = function hue2rgb(p, q, t){
49 | if(t < 0) t += 1;
50 | if(t > 1) t -= 1;
51 | if(t < 1/6) return p + (q - p) * 6 * t;
52 | if(t < 1/2) return q;
53 | if(t < 2/3) return p + (q - p) * (2/3 - t) * 6;
54 | return p;
55 | }
56 |
57 | var q = l < 0.5 ? l * (1 + s) : l + s - l * s;
58 | var p = 2 * l - q;
59 | r = hue2rgb(p, q, h + 1/3);
60 | g = hue2rgb(p, q, h);
61 | b = hue2rgb(p, q, h - 1/3);
62 | }
63 |
64 | return [r, g, b];
65 | }
66 |
67 |
68 |
69 | export { Utils };
--------------------------------------------------------------------------------
/libs/geometry/AABB.js:
--------------------------------------------------------------------------------
1 | import { glMatrix, vec2 } from "./../dependencies/gl-matrix-es6.js";
2 |
3 | class AABB {
4 | constructor() {
5 | this.min = vec2.fromValues(+Infinity, +Infinity);
6 | this.max = vec2.fromValues(-Infinity, -Infinity);
7 | }
8 |
9 | addVertex(vertex) {
10 | if(vertex[0] < this.min[0]) this.min[0] = vertex[0];
11 | if(vertex[1] < this.min[1]) this.min[1] = vertex[1];
12 |
13 | if(vertex[0] > this.max[0]) this.max[0] = vertex[0];
14 | if(vertex[1] > this.max[1]) this.max[1] = vertex[1];
15 | }
16 |
17 | addAABB(aabb) {
18 | this.addVertex(aabb.min);
19 | this.addVertex(aabb.max);
20 | }
21 |
22 | intersect(ray) {
23 | // r.dir is unit direction vector of ray
24 | let dirfrac_x = 1 / ray.d[0];
25 | let dirfrac_y = 1 / ray.d[1];
26 | // lb is the corner of AABB with minimal coordinates - left bottom, rt is maximal corner
27 | // r.org is origin of ray
28 | let t1 = (this.min[0] - ray.o[0]) * dirfrac_x;
29 | let t2 = (this.max[0] - ray.o[0]) * dirfrac_x;
30 | let t3 = (this.min[1] - ray.o[1]) * dirfrac_y;
31 | let t4 = (this.max[1] - ray.o[1]) * dirfrac_y;
32 |
33 | let t = Infinity;
34 | let tmin = Math.max(Math.min(t1, t2), Math.min(t3, t4));
35 | let tmax = Math.min(Math.max(t1, t2), Math.max(t3, t4));
36 |
37 | // if tmax < 0, ray (line) is intersecting AABB, but the whole AABB is behind us
38 | if (tmax < 0) {
39 | t = tmax;
40 | return false;
41 | }
42 |
43 | // if tmin > tmax, ray doesn't intersect AABB
44 | if (tmin > tmax) {
45 | t = tmax;
46 | return false;
47 | }
48 |
49 | t = tmin;
50 |
51 | let result = {
52 | t: tmin,
53 | };
54 |
55 | return result;
56 | }
57 | }
58 |
59 | export { AABB }
--------------------------------------------------------------------------------
/libs/material/lambert.js:
--------------------------------------------------------------------------------
1 | import { Material } from "./material.js";
2 | import { glMatrix, vec2, mat2 } from "./../dependencies/gl-matrix-es6.js";
3 | import { Globals } from "../globals.js";
4 |
5 | class LambertMaterial extends Material {
6 | constructor(options) {
7 | super(options);
8 |
9 | if(!options) options = { };
10 |
11 | this.color = options.color !== undefined ? options.color : [1,1,1];
12 | }
13 |
14 | computeScattering(ray, input_normal, t, contribution, worldAttenuation, wavelength) {
15 |
16 |
17 | // opacity test, if it passes we're going to let the ray pass through the object
18 | if(this.opacityTest(t, worldAttenuation, ray, contribution, this.color)) {
19 | return;
20 | }
21 |
22 |
23 |
24 | let dot = vec2.dot(ray.d, input_normal);
25 | let normal = vec2.clone(input_normal);
26 | if(dot > 0.0) {
27 | vec2.negate(normal, normal);
28 | }
29 |
30 |
31 | dot = Math.abs( vec2.dot(ray.d, input_normal) );
32 |
33 |
34 |
35 | let contrib = Math.exp(-t * worldAttenuation) * dot;
36 | contribution.r *= contrib * this.color[0];
37 | contribution.g *= contrib * this.color[1];
38 | contribution.b *= contrib * this.color[2];
39 |
40 |
41 |
42 |
43 | let newDirection = vec2.create();
44 |
45 | // evaluate BRDF
46 | let xi = Math.random();
47 | let sinThetaI = 2 * xi - 1;
48 | let cosThetaI = Math.sqrt(1 - sinThetaI*sinThetaI);
49 | let tv1 = vec2.fromValues(sinThetaI, cosThetaI);
50 |
51 | // normal rotation matrix
52 | let m = mat2.fromValues(normal[1], -normal[0], normal[0], normal[1]);
53 | vec2.transformMat2(tv1, tv1, m);
54 |
55 | vec2.copy(newDirection, tv1);
56 | // evaluate BRDF - END
57 |
58 |
59 |
60 |
61 | let newOrigin = vec2.create();
62 | // it's important that the epsilon value is subtracted/added instead of doing t * 0.999999 since that caused floating point precision issues
63 | vec2.scaleAndAdd(newOrigin, ray.o, ray.d, t - Globals.epsilon);
64 |
65 |
66 | vec2.copy(ray.o, newOrigin);
67 | vec2.copy(ray.d, newDirection);
68 |
69 | }
70 | }
71 |
72 | export { LambertMaterial }
--------------------------------------------------------------------------------
/libs/scenes/features/dispersion.js:
--------------------------------------------------------------------------------
1 | import { Edge } from "./geometry/Edge.js";
2 | import { LambertMaterial } from "./material/lambert.js";
3 | import { BeamEmitterMaterial } from "./material/beamEmitter.js";
4 | import { Utils } from "./utils.js";
5 | import { DielectricMaterial } from "./material/dielectric.js";
6 |
7 |
8 | function createScene(scene, workerData, motionBlurT, ctx, frameNumber) {
9 |
10 | let edgeMaterial = new LambertMaterial({ opacity: 1 });
11 | let tbound = 16;
12 | let lbound = 19.5;
13 | let rbound = 19.5;
14 | let bbound = 11;
15 | let ledge = new Edge(-lbound, -bbound, -lbound, tbound);
16 | let redge = new Edge( rbound, -bbound, rbound, tbound);
17 | let tedge = new Edge(-lbound, tbound, rbound, tbound);
18 | let bedge = new Edge(-lbound, -bbound, rbound, -bbound);
19 |
20 |
21 | scene.add(ledge, edgeMaterial);
22 | scene.add(redge, edgeMaterial);
23 | scene.add(tedge, edgeMaterial);
24 | scene.add(bedge, edgeMaterial);
25 |
26 |
27 |
28 | let seed = Math.floor(workerData.randomNumber * 1000000000);
29 | Utils.setSeed(seed);
30 | let rand = Utils.rand;
31 |
32 |
33 |
34 | for(let i = 0; i < 3; i++) {
35 | let angle1 = (i / 3) * Math.PI * 2;
36 | let angle2 = ((i+1) / 3) * Math.PI * 2;
37 |
38 | angle1 += Math.PI / 2;
39 | angle2 += Math.PI / 2;
40 |
41 | let radius = 4;
42 |
43 | let tx1 = Math.cos(angle1) * radius;
44 | let ty1 = Math.sin(angle1) * radius;
45 | let tx2 = Math.cos(angle2) * radius;
46 | let ty2 = Math.sin(angle2) * radius;
47 |
48 | scene.add(
49 | new Edge(tx2, ty2, tx1, ty1),
50 | new DielectricMaterial({
51 | opacity: 1,
52 | transmittance: 0.5,
53 | ior: 1.4,
54 | roughness: 0.15,
55 | dispersion: 0.125,
56 | })
57 | );
58 | }
59 |
60 | let edge = new Edge(-16, -3.1, -16, -3.2);
61 |
62 | scene.add(
63 | edge,
64 | new BeamEmitterMaterial({
65 | color: function() {
66 | return {
67 | wavelength: Math.random() * 360 + 380,
68 | intensity: 1.5,
69 | }
70 | },
71 | // since the Scene class samples lightsources depending on their strenght, we can't know beforehand what's the value inside
72 | // the "color" property (it's a function!) so we *have* to specify a sampling value for this light source
73 | samplePower: 150,
74 | beamDirection: [1, 0.3]
75 | })
76 | );
77 | }
78 |
79 | export { createScene };
--------------------------------------------------------------------------------
/libs/scenes/features/starGeometry.js:
--------------------------------------------------------------------------------
1 | import { Edge } from "./geometry/Edge.js";
2 | import { Star } from "./geometry/Star.js";
3 | import { LambertMaterial } from "./material/lambert.js";
4 | import { LambertEmitterMaterial } from "./material/lambertEmitter.js";
5 |
6 | /*
7 | Global settings used to render this scene
8 |
9 | var Globals = {
10 |
11 | // Sampling
12 | epsilon: 0.00005,
13 | highPrecision: false, // if set to true, uses Float64Arrays which are 2x slower to work with
14 | USE_STRATIFIED_SAMPLING: true,
15 | samplingRatioPerPixelCovered: 0.14,
16 | LIGHT_BOUNCES: 35,
17 | skipBounce: 0,
18 |
19 | // Threading
20 | workersCount: 5,
21 | PHOTONS_PER_UPDATE: 50000,
22 |
23 | // Environment
24 | WORLD_SIZE: 20,
25 | worldAttenuation: 0.01,
26 |
27 | // Video export
28 | registerVideo: false,
29 | photonsPerVideoFrame: 5000000,
30 | framesPerSecond: 30,
31 | framesCount: 30,
32 | frameStart: 0,
33 |
34 | // Motion blur
35 | motionBlur: false,
36 | motionBlurFramePhotons: 5000,
37 |
38 | // Offscreen canvas
39 | deactivateOffscreenCanvas: true, // setting it to false slows down render times by about 1.7x
40 | offscreenCanvasCPow: 1.1,
41 |
42 | // Canvas size
43 | canvasSize: {
44 | width: 1300,
45 | height: 1000,
46 | },
47 |
48 | // Reinhard tonemapping
49 | toneMapping: true,
50 | gamma: 2.2,
51 | exposure: 1,
52 | }
53 |
54 | export { Globals };
55 | */
56 |
57 | function createScene(scene, workerData, motionBlurT, ctx, frameNumber) {
58 |
59 | createWorldBounds(scene);
60 |
61 | let star = new Star(5, 0, 0, 5, 2.5);
62 | let material = new LambertMaterial({ opacity: 0.6, color: [1, 0.05, 0] });
63 | scene.add(star, material);
64 |
65 | let lightSource = new Edge(10, -5, 10, 5);
66 | let lightMaterial = new LambertEmitterMaterial({ color: [500, 500, 500] });
67 | scene.add(lightSource, lightMaterial);
68 | }
69 |
70 | function createWorldBounds(scene) {
71 | let edgeMaterial = new LambertMaterial({ opacity: 1 });
72 | let tbound = 11;
73 | let lbound = 19.5;
74 | let rbound = 19.5;
75 | let bbound = 11;
76 | let ledge = new Edge(-lbound, -bbound, -lbound, tbound, 0, 1, 0);
77 | let redge = new Edge( rbound, -bbound, rbound, tbound, 0, -1, 0);
78 | let tedge = new Edge(-lbound, tbound, rbound, tbound, 0, 0, -1);
79 | let bedge = new Edge(-lbound, -bbound, rbound, -bbound, 0, 0, 1);
80 |
81 | scene.add(ledge, edgeMaterial);
82 | scene.add(redge, edgeMaterial);
83 | scene.add(tedge, edgeMaterial);
84 | scene.add(bedge, edgeMaterial);
85 | }
86 |
87 | export { createScene };
--------------------------------------------------------------------------------
/libs/material/microfacet.js:
--------------------------------------------------------------------------------
1 | import { Material } from "./material.js";
2 | import { glMatrix, vec2, mat2 } from "./../dependencies/gl-matrix-es6.js";
3 | import { Globals } from "../globals.js";
4 |
5 | class MicrofacetMaterial extends Material {
6 | constructor(options) {
7 | super(options);
8 |
9 | if(!options) options = { };
10 |
11 | this.roughness = options.roughness !== undefined ? options.roughness : 0.25;
12 | }
13 |
14 | computeScattering(ray, input_normal, t, contribution, worldAttenuation, wavelength) {
15 |
16 | // opacity test, if it passes we're going to let the ray pass through the object
17 | if(this.opacityTest(t, worldAttenuation, ray, contribution)) {
18 | return;
19 | }
20 |
21 |
22 |
23 | let dot = vec2.dot(ray.d, input_normal);
24 | let normal = vec2.clone(input_normal);
25 | if(dot > 0.0) {
26 | vec2.negate(normal, normal);
27 | }
28 |
29 | dot = Math.abs( vec2.dot(ray.d, input_normal) );
30 |
31 | let contrib = Math.exp(-t * worldAttenuation) * dot;
32 | contribution.r *= contrib;
33 | contribution.g *= contrib;
34 | contribution.b *= contrib;
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 | let newDirection = vec2.create();
43 |
44 | // evaluate BRDF
45 | let w_o = vec2.fromValues(-ray.d[0], -ray.d[1]);
46 |
47 |
48 | // normal rotation matrix
49 | let mat = mat2.fromValues(normal[1], -normal[0], normal[0], normal[1]);
50 | let imat = mat2.create();
51 | mat2.invert(imat, mat);
52 | vec2.transformMat2(w_o, w_o, imat);
53 |
54 | let thetaMin = Math.max(Math.asin(w_o[0]), 0.0) - (Math.PI / 2);
55 | let thetaMax = Math.min(Math.asin(w_o[0]), 0.0) + (Math.PI / 2);
56 |
57 | let s = this.roughness;
58 | let xi = Math.random();
59 | let a = Math.tanh(thetaMin/(2.0*s));
60 | let b = Math.tanh(thetaMax/(2.0*s));
61 | let thetaM = 2.0*s*Math.atanh(a + (b - a)*xi);
62 |
63 | let m = vec2.fromValues(Math.sin(thetaM), Math.cos(thetaM));
64 |
65 | let w_oDm = vec2.dot(w_o, m);
66 | m[0] = m[0] * (w_oDm * 2) - w_o[0];
67 | m[1] = m[1] * (w_oDm * 2) - w_o[1];
68 | // return m*(dot(w_o, m)*2.0) - w_o;
69 | vec2.transformMat2(m, m, mat);
70 |
71 | vec2.copy(newDirection, m);
72 | vec2.normalize(newDirection, newDirection);
73 | // evaluate BRDF - END
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 | let newOrigin = vec2.create();
86 | vec2.scaleAndAdd(newOrigin, ray.o, ray.d, t - Globals.epsilon);
87 |
88 | vec2.copy(ray.o, newOrigin);
89 | vec2.copy(ray.d, newDirection);
90 | }
91 | }
92 |
93 | export { MicrofacetMaterial }
--------------------------------------------------------------------------------
/libs/scenes/features/quads.js:
--------------------------------------------------------------------------------
1 | import { Edge } from "./geometry/Edge.js";
2 | import { Circle } from "./geometry/Circle.js";
3 | import { LambertMaterial } from "./material/lambert.js";
4 | import { EmitterMaterial } from "./material/emitter.js";
5 | import { Utils } from "./utils.js";
6 | import { DielectricMaterial } from "./material/dielectric.js";
7 |
8 |
9 | function createScene(scene, workerData, motionBlurT, ctx, frameNumber) {
10 |
11 | let edgeMaterial = new LambertMaterial({ opacity: 1 });
12 | let tbound = 13;
13 | let lbound = 19.5;
14 | let rbound = 19.5;
15 | let bbound = 11;
16 | let ledge = new Edge(-lbound, -bbound, -lbound, tbound);
17 | let redge = new Edge( rbound, -bbound, rbound, tbound);
18 | let tedge = new Edge(-lbound, tbound, rbound, tbound);
19 | let bedge = new Edge(-lbound, -bbound, rbound, -bbound);
20 |
21 |
22 | scene.add(ledge, edgeMaterial);
23 | scene.add(redge, edgeMaterial);
24 | scene.add(tedge, edgeMaterial);
25 | scene.add(bedge, edgeMaterial);
26 |
27 |
28 |
29 | let seed = Math.floor(workerData.randomNumber * 1000000000);
30 | Utils.setSeed(seed);
31 | let rand = Utils.rand;
32 |
33 |
34 |
35 | let lss = 23;
36 | let xs = 20;
37 |
38 | scene.add(new Circle(-xs, 0, 2.5), new EmitterMaterial({ opacity: 0, color: [ 55*lss, 50 *lss, 210 *lss ], beamDirection: [1, 0] }));
39 | scene.add(new Circle(+xs, 0, 2.5), new EmitterMaterial({ opacity: 0, color: [ 230 *lss, 110 *lss, 50*lss ], beamDirection: [-1, 0] }));
40 |
41 |
42 |
43 | let material1 = new LambertMaterial({ opacity: 0.8 });
44 | let material2 = new DielectricMaterial({ opacity: 1, transmittance: 0.8, ior: 1.5, roughness: 0.1 });
45 |
46 | for(let i = 0; i < 10; i++) {
47 | for(let j = 0; j < 10; j++) {
48 |
49 | let xoff = ((i / 9) * 2 - 1) * 7;
50 | let yoff = ((j / 9) * 2 - 1) * 7;
51 | let angleIncr = (i * 10 + j) * 0.02;
52 |
53 | let material = (i+j) % 3 === 0 ? material2 : material1;
54 | if((i + j) % 2 === 0) continue;
55 |
56 | let radius = 0.7 + Math.sin((i * 10 + j) * 0.1) * 0.3;
57 |
58 |
59 | for(let r = 0; r < 4; r++) {
60 | let angle1 = (r / 4) * Math.PI * 2;
61 | let angle2 = ((r+1) / 4) * Math.PI * 2;
62 | angle1 += angleIncr;
63 | angle2 += angleIncr;
64 | angle1 = angle1 % (Math.PI * 2);
65 | angle2 = angle2 % (Math.PI * 2);
66 |
67 |
68 | let px1 = Math.cos(angle1) * radius;
69 | let py1 = Math.sin(angle1) * radius;
70 |
71 | let px2 = Math.cos(angle2) * radius;
72 | let py2 = Math.sin(angle2) * radius;
73 |
74 | scene.add(new Edge(px1 + xoff, py1 + yoff, px2 + xoff, py2 + yoff), material);
75 | }
76 | }
77 | }
78 | }
79 |
80 | export { createScene };
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Lumen 2D javascript renderer
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 | How to use
17 | ======
18 | 1. **You need to enable this chrome flag to be able to use this project**
19 |
20 | --enable-experimental-web-platform-features
21 |
22 | This is currently required because the project uses es6 modules inside webworkers
23 | 2. Download the repo
24 | 3. Inside the script `./libs/createScene.js` you can code the scene you want to render
25 | 4. Then simply open index.html with a local server
26 |
27 | ------
28 |
29 | For an in-depth explanation on how to use Lumen-2D [consult the wiki](https://github.com/Domenicobrz/Lumen-2D/wiki)
30 |
31 | Credits
32 | ------
33 | I can't thank [Benedikt Bitterli](https://benedikt-bitterli.me/) enough for his post [The Secret Life of Photons](https://benedikt-bitterli.me/tantalum/), where he presented the derivation and usage of few BRDFs which I used in this project
34 |
35 | [Zhang Yuning / codeworm96](https://github.com/codeworm96) for his awesome renderer [light2d-rs](https://github.com/codeworm96/light2d-rs) and his inspiring work with simulating Beer-Lambert absorption in dielectric materials
36 |
37 | [Nicholas Sherlock](https://github.com/thenickdude) - [webm-writer-js](https://github.com/thenickdude/webm-writer-js)
38 |
39 | [Thom Chiovoloni](https://github.com/thomcc) - [quick-noise.js](https://github.com/thomcc/quick-noise.js)
40 |
41 | [dandavis](https://github.com/rndme) - [download.js](https://github.com/rndme/download)
42 |
--------------------------------------------------------------------------------
/libs/scenes/features/dielectricBeerLambertAbsorption.js:
--------------------------------------------------------------------------------
1 | import { Edge } from "./geometry/Edge.js";
2 | import { Circle } from "./geometry/Circle.js";
3 | import { LambertMaterial } from "./material/lambert.js";
4 | import { ExperimentalDielectricMaterial } from "./material/experimentalDielectric.js";
5 | import { LambertEmitterMaterial } from "./material/lambertEmitter.js";
6 |
7 |
8 | /**
9 | *
10 | * Assing these values to the Globals object
11 | *
12 | * WORLD_SIZE = 20;
13 | * worldAttenuation = 0.05;
14 | * LIGHT_BOUNCES: 10,
15 | *
16 | */
17 |
18 | var scene;
19 | function createScene(gscene, workerData, motionBlurT, ctx, frameNumber) {
20 |
21 | scene = gscene;
22 | createWorldBounds(scene);
23 |
24 | Pentagon(0, 0, 5, 0, true, new ExperimentalDielectricMaterial({
25 | opacity: 1,
26 | transmittance: 1,
27 | ior: 1.5,
28 | absorption: 0.35,
29 | dispersion: 0.27,
30 | roughness: 0.05,
31 | volumeAbsorption: [0, 0.15, 0.25],
32 | reflectionStrenght: 2.35,
33 | }));
34 |
35 | let lightSource2 = new Circle(0, 13, 3);
36 | let lightMaterial2 = new LambertEmitterMaterial({
37 | opacity: 0,
38 | color: function() {
39 | let w = Math.random() * 360 + 380;
40 | let it = 1;
41 | if(w < 450) it = 1.5;
42 |
43 | return {
44 | wavelength: w,
45 | intensity: 18 * it,
46 | }
47 | },
48 | sampleStrenght: 150,
49 | });
50 | scene.add(lightSource2, lightMaterial2);
51 | }
52 |
53 | function Pentagon(x, y, radius1, rotation, nothollow, material) {
54 |
55 | if(!material) material = new LambertMaterial({ opacity: 0.7 });
56 |
57 | for(let i = 0; i < 5; i++) {
58 | let angle1 = (i / 5) * Math.PI * 2;
59 | let angle2 = ((i+1) / 5) * Math.PI * 2;
60 |
61 | let x1 = Math.cos(angle1) * radius1;
62 | let y1 = Math.sin(angle1) * radius1;
63 |
64 | let x2 = Math.cos(angle2) * radius1;
65 | let y2 = Math.sin(angle2) * radius1;
66 |
67 | x1 += x;
68 | x2 += x;
69 |
70 | y1 += y;
71 | y2 += y;
72 |
73 | scene.add(new Edge(x2, y2, x1, y1), material);
74 | }
75 | }
76 |
77 | function createWorldBounds(scene) {
78 | let edgeMaterial = new LambertMaterial({ opacity: 1 });
79 | let tbound = 11 * 2;
80 | let lbound = 19.5 * 2;
81 | let rbound = 19.5 * 2;
82 | let bbound = 11 * 2;
83 | let ledge = new Edge(-lbound, -bbound, -lbound, tbound, 0, 1, 0);
84 | let redge = new Edge( rbound, -bbound, rbound, tbound, 0, -1, 0);
85 | let tedge = new Edge(-lbound, tbound, rbound, tbound, 0, 0, -1);
86 | let bedge = new Edge(-lbound, -bbound, rbound, -bbound, 0, 0, 1);
87 |
88 | scene.add(ledge, edgeMaterial);
89 | scene.add(redge, edgeMaterial);
90 | scene.add(tedge, edgeMaterial);
91 | scene.add(bedge, edgeMaterial);
92 | }
93 |
94 | export { createScene };
--------------------------------------------------------------------------------
/libs/scenes/features/sellmierCoefficients.js:
--------------------------------------------------------------------------------
1 | import { Edge } from "./geometry/Edge.js";
2 | import { LambertMaterial } from "./material/lambert.js";
3 | import { BeamEmitterMaterial } from "./material/beamEmitter.js";
4 | import { Utils } from "./utils.js";
5 | import { DielectricMaterial } from "./material/dielectric.js";
6 |
7 |
8 | function createScene(scene, workerData, motionBlurT, ctx, frameNumber) {
9 |
10 | let edgeMaterial = new LambertMaterial({ opacity: 1 });
11 | let tbound = 11;
12 | let lbound = 19.5;
13 | let rbound = 19.5;
14 | let bbound = 11;
15 | let ledge = new Edge(-lbound, -bbound, -lbound, tbound);
16 | let redge = new Edge( rbound, -bbound, rbound, tbound);
17 | let tedge = new Edge(-lbound, tbound, rbound, tbound);
18 | let bedge = new Edge(-lbound, -bbound, rbound, -bbound);
19 |
20 |
21 | scene.add(ledge, edgeMaterial);
22 | scene.add(redge, edgeMaterial);
23 | scene.add(tedge, edgeMaterial);
24 | scene.add(bedge, edgeMaterial);
25 |
26 |
27 |
28 | let seed = Math.floor(workerData.randomNumber * 1000000000);
29 | Utils.setSeed(seed);
30 |
31 |
32 | for(let i = 0; i < 3; i++) {
33 | let angle1 = (i / 3) * Math.PI * 2;
34 | let angle2 = ((i+1) / 3) * Math.PI * 2;
35 |
36 | angle1 += Math.PI / 2;
37 | angle2 += Math.PI / 2;
38 |
39 | let radius = 4;
40 |
41 | let tx1 = Math.cos(angle1) * radius;
42 | let ty1 = Math.sin(angle1) * radius;
43 | let tx2 = Math.cos(angle2) * radius;
44 | let ty2 = Math.sin(angle2) * radius;
45 |
46 | let triangleMaterial = new DielectricMaterial({
47 | opacity: 1,
48 | transmittance: 1,
49 | ior: 1.4,
50 | roughness: 0.15,
51 | dispersion: 0.125,
52 | });
53 |
54 | triangleMaterial.setSellmierCoefficients(
55 | 9 * 1.03961212,
56 | 9 * 0.231792344,
57 | 9 * 1.01046945,
58 | 9 * 0.00600069867,
59 | 9 * 0.0200179144,
60 | 9 * 13.560653,
61 | 1
62 | );
63 |
64 | scene.add(
65 | new Edge(tx2, ty2, tx1, ty1),
66 | triangleMaterial
67 | );
68 | }
69 |
70 | let edge = new Edge(-16, -3.1, -16, -3.2);
71 |
72 | scene.add(
73 | edge,
74 | new BeamEmitterMaterial({
75 | color: function() {
76 |
77 | let w = 680;
78 | if(Math.random() > 0) w = Math.random() * 360 + 380;
79 |
80 | return {
81 | wavelength: w,
82 | intensity: 1.5,
83 | }
84 | },
85 | // since the Scene class samples lightsources depending on their strenght, we can't know beforehand what's the value inside
86 | // the "color" property (it's a function!) so we *have* to specify a sampling value for this light source
87 | samplePower: 150,
88 | beamDirection: [1, 0.3]
89 | })
90 | );
91 | }
92 |
93 | export { createScene };
--------------------------------------------------------------------------------
/libs/scenes/features/perlin-noise.js:
--------------------------------------------------------------------------------
1 | import { Edge } from "./geometry/Edge.js";
2 | import { Circle } from "./geometry/Circle.js";
3 | import { LambertMaterial } from "./material/lambert.js";
4 | import { EmitterMaterial } from "./material/emitter.js";
5 | import { Utils } from "./utils.js";
6 | import { DielectricMaterial } from "./material/dielectric.js";
7 | import { quickNoise } from "./dependencies/quick-noise.js";
8 |
9 |
10 | function createScene(scene, workerData, motionBlurT, ctx, frameNumber) {
11 |
12 | let edgeMaterial = new LambertMaterial({ opacity: 1 });
13 | let tbound = 11;
14 | let lbound = 19.5;
15 | let rbound = 19.5;
16 | let bbound = 11;
17 | let ledge = new Edge(-lbound, -bbound, -lbound, tbound);
18 | let redge = new Edge( rbound, -bbound, rbound, tbound);
19 | let tedge = new Edge(-lbound, tbound, rbound, tbound);
20 | let bedge = new Edge(-lbound, -bbound, rbound, -bbound);
21 |
22 |
23 | scene.add(ledge, edgeMaterial);
24 | scene.add(redge, edgeMaterial);
25 | scene.add(tedge, edgeMaterial);
26 | scene.add(bedge, edgeMaterial);
27 |
28 |
29 |
30 | let seed = Math.floor(workerData.randomNumber * 1000000000);
31 | Utils.setSeed(seed);
32 | let rand = Utils.rand;
33 |
34 |
35 |
36 | let triangleMaterial = new DielectricMaterial({
37 | opacity: 1,
38 | transmittance: 1,
39 | ior: 1.4,
40 | roughness: 0.000004,
41 | dispersion: 0.05,
42 | absorption: 0.45
43 | });
44 |
45 | let sides = 5;
46 | for(let j = 0; j < 1; j++) {
47 | let xOff = 0;
48 | let yOff = 0;
49 |
50 | let radius = 5;
51 |
52 | for(let i = 0; i < sides; i++) {
53 | let angle1 = (i / sides) * Math.PI * 2;
54 | let angle2 = ((i+1) / sides) * Math.PI * 2;
55 |
56 | angle1 += Math.PI / 2;
57 | angle2 += Math.PI / 2;
58 |
59 | for(let i = 0; i < 100; i++) {
60 | radius += quickNoise.noise(i * 0.2, i * 0.2, i * 0.2) * 0.2;
61 |
62 | let tx1 = Math.cos(angle1) * radius;
63 | let ty1 = Math.sin(angle1) * radius;
64 | let tx2 = Math.cos(angle2) * radius;
65 | let ty2 = Math.sin(angle2) * radius;
66 |
67 |
68 | let t1 = (i / 99);
69 | let t2 = ((i+1) / 99);
70 | let dx = tx2-tx1;
71 | let dy = ty2-ty1;
72 |
73 | scene.add(
74 | new Edge(
75 | tx1 + dx * t1 + xOff,
76 | ty1 + dy * t1 + yOff,
77 | tx1 + dx * t2 + xOff,
78 | ty1 + dy * t2 + yOff),
79 | new LambertMaterial({opacity: 0.92})
80 | );
81 | }
82 | }
83 | }
84 |
85 | scene.add(
86 | new Circle(0,0, 7),
87 | triangleMaterial,
88 | );
89 |
90 | scene.add(
91 | new Circle(-21, 0, 3),
92 | new EmitterMaterial({
93 | opacity: 0,
94 | color: function() {
95 |
96 | let w = 680;
97 | if(Math.random() > 0) w = Math.random() * 360 + 380;
98 |
99 | return {
100 | wavelength: w,
101 | intensity: 10.5,
102 | }
103 | },
104 | // since the Scene class samples lightsources depending on their strenght, we can't know beforehand what's the value inside
105 | // the "color" property (it's a function!) so we *have* to specify a sampling value for this light source
106 | samplePower: 150,
107 | })
108 | );
109 | }
110 |
111 | export { createScene };
--------------------------------------------------------------------------------
/libs/scenes/features/contributionModifierExample.js:
--------------------------------------------------------------------------------
1 | import { Edge } from "./geometry/Edge.js";
2 | import { Circle } from "./geometry/Circle.js";
3 | import { LambertMaterial } from "./material/lambert.js";
4 | import { EmitterMaterial } from "./material/emitter.js";
5 | import { Utils } from "./utils.js";
6 | import { DielectricMaterial } from "./material/dielectric.js";
7 | import { ContributionModifierMaterial } from "./material/contributionModifier.js";
8 |
9 |
10 | function createScene(scene, workerData, motionBlurT, ctx, frameNumber) {
11 |
12 | let edgeMaterial = new LambertMaterial({ opacity: 1 });
13 | let tbound = 11;
14 | let lbound = 19.5;
15 | let rbound = 19.5;
16 | let bbound = 11;
17 | let ledge = new Edge(-lbound, -bbound, -lbound, tbound);
18 | let redge = new Edge( rbound, -bbound, rbound, tbound);
19 | let tedge = new Edge(-lbound, tbound, rbound, tbound);
20 | let bedge = new Edge(-lbound, -bbound, rbound, -bbound);
21 |
22 |
23 | scene.add(ledge, edgeMaterial);
24 | scene.add(redge, edgeMaterial);
25 | scene.add(tedge, edgeMaterial);
26 | scene.add(bedge, edgeMaterial);
27 |
28 |
29 |
30 | let seed = Math.floor(workerData.randomNumber * 1000000000);
31 | Utils.setSeed(seed);
32 | let rand = Utils.rand;
33 |
34 | let triangleMaterial2 = new DielectricMaterial({
35 | opacity: 1,
36 | transmittance: 1,
37 | ior: 1.4,
38 | roughness: 0.05,
39 | dispersion: 0.15,
40 | absorption: 0.35
41 | });
42 |
43 | let edgesMaterial2 = triangleMaterial2;
44 | let edgesMaterial3 = new ContributionModifierMaterial({ modifier: 0.2 });
45 | let edgesMaterial4 = new ContributionModifierMaterial({ modifier: 1 / 0.2 });
46 |
47 | for(let i = 0; i < 9; i++) {
48 | let radius = 2;
49 |
50 | let xt = 5;
51 | let yt = 5;
52 |
53 | let xo = Utils.rand() * 0.5 - 0.25;
54 | let yo = Utils.rand() * 0.5 - 0.25;
55 |
56 | for(let j = 0; j < 4; j++) {
57 | let x1 = -1;
58 | let y1 = -1;
59 |
60 | let x2 = -1;
61 | let y2 = +1;
62 |
63 | let x4 = +1;
64 | let y4 = -1;
65 |
66 | let x3 = +1;
67 | let y3 = +1;
68 |
69 | x1 *= radius;
70 | y1 *= radius;
71 | x2 *= radius;
72 | y2 *= radius;
73 | x3 *= radius;
74 | y3 *= radius;
75 | x4 *= radius;
76 | y4 *= radius;
77 |
78 | let xOff = xo * j;
79 | let yOff = yo * j;
80 |
81 |
82 |
83 | let ix = i % 3 - 1;
84 | let iy = Math.floor(i / 3) - 1;
85 |
86 | xOff += xt * ix;
87 | yOff += yt * iy;
88 |
89 |
90 | x1 += xOff;
91 | x2 += xOff;
92 | x3 += xOff;
93 | x4 += xOff;
94 | y1 += yOff;
95 | y2 += yOff;
96 | y3 += yOff;
97 | y4 += yOff;
98 |
99 | let blur = 0;
100 | let edgesMaterial = edgesMaterial3;
101 | if(((i * 9) + j) % 2 === 1) edgesMaterial = edgesMaterial4;
102 | if(i === 4) edgesMaterial = edgesMaterial2;
103 |
104 | scene.add(new Edge(x1, y1, x2, y2, blur), edgesMaterial);
105 | scene.add(new Edge(x2, y2, x3, y3, blur), edgesMaterial);
106 | scene.add(new Edge(x3, y3, x4, y4, blur), edgesMaterial);
107 | scene.add(new Edge(x4, y4, x1, y1, blur), edgesMaterial);
108 |
109 | radius *= 0.7;
110 | }
111 | }
112 |
113 | scene.add(
114 | new Circle(16, 7.5, 3),
115 | new EmitterMaterial({
116 | opacity: 0,
117 | color: [40, 90, 250]
118 | })
119 | );
120 | }
121 |
122 | export { createScene };
--------------------------------------------------------------------------------
/libs/dependencies/download.js:
--------------------------------------------------------------------------------
1 | //download.js v3.0, by dandavis; 2008-2014. [CCBY2] see http://danml.com/download.html for tests/usage
2 | // v1 landed a FF+Chrome compat way of downloading strings to local un-named files, upgraded to use a hidden frame and optional mime
3 | // v2 added named files via a[download], msSaveBlob, IE (10+) support, and window.URL support for larger+faster saves than dataURLs
4 | // v3 added dataURL and Blob Input, bind-toggle arity, and legacy dataURL fallback was improved with force-download mime and base64 support
5 |
6 | // data can be a string, Blob, File, or dataURL
7 |
8 |
9 |
10 |
11 | function download(data, strFileName, strMimeType) {
12 |
13 | var self = window, // this script is only for browsers anyway...
14 | u = "application/octet-stream", // this default mime also triggers iframe downloads
15 | m = strMimeType || u,
16 | x = data,
17 | D = document,
18 | a = D.createElement("a"),
19 | z = function(a){return String(a);},
20 |
21 |
22 | B = self.Blob || self.MozBlob || self.WebKitBlob || z,
23 | BB = self.MSBlobBuilder || self.WebKitBlobBuilder || self.BlobBuilder,
24 | fn = strFileName || "download",
25 | blob,
26 | b,
27 | ua,
28 | fr;
29 |
30 | //if(typeof B.bind === 'function' ){ B=B.bind(self); }
31 |
32 | if(String(this)==="true"){ //reverse arguments, allowing download.bind(true, "text/xml", "export.xml") to act as a callback
33 | x=[x, m];
34 | m=x[0];
35 | x=x[1];
36 | }
37 |
38 |
39 |
40 | //go ahead and download dataURLs right away
41 | if(String(x).match(/^data\:[\w+\-]+\/[\w+\-]+[,;]/)){
42 | return navigator.msSaveBlob ? // IE10 can't do a[download], only Blobs:
43 | navigator.msSaveBlob(d2b(x), fn) :
44 | saver(x) ; // everyone else can save dataURLs un-processed
45 | }//end if dataURL passed?
46 |
47 | try{
48 |
49 | blob = x instanceof B ?
50 | x :
51 | new B([x], {type: m}) ;
52 | }catch(y){
53 | if(BB){
54 | b = new BB();
55 | b.append([x]);
56 | blob = b.getBlob(m); // the blob
57 | }
58 |
59 | }
60 |
61 |
62 |
63 | function d2b(u) {
64 | var p= u.split(/[:;,]/),
65 | t= p[1],
66 | dec= p[2] == "base64" ? atob : decodeURIComponent,
67 | bin= dec(p.pop()),
68 | mx= bin.length,
69 | i= 0,
70 | uia= new Uint8Array(mx);
71 |
72 | for(i;i 0.0) {
29 | vec2.negate(normal, normal);
30 | }
31 |
32 |
33 | dot = Math.abs( vec2.dot(ray.d, input_normal) );
34 |
35 |
36 | let contrib = Math.exp(-t * worldAttenuation) * dot;
37 | contribution.r *= contrib;
38 | contribution.g *= contrib;
39 | contribution.b *= contrib;
40 |
41 |
42 |
43 |
44 | let newDirection = vec2.create();
45 |
46 | // evaluate BRDF
47 | let xi = Math.random();
48 | let sinThetaI = 2 * xi - 1;
49 | let cosThetaI = Math.sqrt(1 - sinThetaI*sinThetaI);
50 | let tv1 = vec2.fromValues(sinThetaI, cosThetaI);
51 |
52 | // normal rotation matrix
53 | let m = mat2.fromValues(normal[1], -normal[0], normal[0], normal[1]);
54 | vec2.transformMat2(tv1, tv1, m);
55 |
56 | vec2.copy(newDirection, tv1);
57 | // evaluate BRDF - END
58 |
59 |
60 |
61 | // bounce off again
62 | let newOrigin = vec2.create();
63 | // it's important that the epsilon value is subtracted/added instead of doing t * 0.999999 since that caused floating point precision issues
64 | vec2.scaleAndAdd(newOrigin, ray.o, ray.d, t - Globals.epsilon);
65 |
66 |
67 | vec2.copy(ray.o, newOrigin);
68 | vec2.copy(ray.d, newDirection);
69 |
70 | }
71 |
72 |
73 | getSpectrum(color) {
74 | let spectrum;
75 |
76 | if(Array.isArray(color)) {
77 | spectrum = {
78 | color: color
79 | }
80 | }
81 |
82 | if(typeof color === "function") {
83 | spectrum = color();
84 | }
85 |
86 | return spectrum;
87 | }
88 |
89 | getPhoton(geometryObject) {
90 | let res = geometryObject.getRandomPoint();
91 | let point = res.p;
92 | // avoids self-intersections
93 | point[0] += res.normal[0] * 0.00001; // normals are always normalized to a length of 1, so there shouldn't be a precision problem with *= 0.00001
94 | point[1] += res.normal[1] * 0.00001;
95 | let normal = res.normal;
96 |
97 | let newDirection = vec2.create();
98 | let nv = normal;
99 | let nu = vec2.fromValues(-normal[1], normal[0]);
100 |
101 | let angleInHemisphere = Math.random() * Math.PI;
102 | let nudx = Math.cos(angleInHemisphere) * nu[0];
103 | let nudy = Math.cos(angleInHemisphere) * nu[1];
104 | let nvdx = Math.sin(angleInHemisphere) * nv[0];
105 | let nvdy = Math.sin(angleInHemisphere) * nv[1];
106 |
107 | newDirection[0] = nudx + nvdx;
108 | newDirection[1] = nudy + nvdy;
109 | vec2.normalize(newDirection, newDirection);
110 |
111 |
112 |
113 | let spectrum = this.getSpectrum(this.color);
114 |
115 | return {
116 | ray: new Ray(point, newDirection),
117 | spectrum: spectrum
118 | }
119 | }
120 | }
121 |
122 | export { EmitterMaterial }
--------------------------------------------------------------------------------
/libs/scene.js:
--------------------------------------------------------------------------------
1 | import { EmitterMaterial } from "./material/emitter.js";
2 | import { LambertMaterial } from "./material/lambert.js";
3 | import { Geometry } from "./geometry/Geometry.js";
4 | import { Primitive } from "./geometry/Primitive.js";
5 | import { glMatrix, vec2 } from "./dependencies/gl-matrix-es6.js";
6 | import { BVH } from "./bvh.js";
7 |
8 | class Scene {
9 | constructor(args) {
10 | this._objects = [];
11 | this._bvh;
12 |
13 | // using a cumulative distribution function to sample emitters
14 | this._emittersCdfArray = [];
15 | this._emittersCdfMax = 0;
16 | this.args = args;
17 | }
18 |
19 | reset() {
20 | this._emittersCdfArray = [];
21 | this._emittersCdfMax = 0;
22 | this._objects = [];
23 | this._bvh = undefined;
24 | }
25 |
26 | add(object, overrideMaterial) {
27 | if(overrideMaterial !== undefined)
28 | object.setMaterial(overrideMaterial);
29 |
30 |
31 | let primitivesArray = [];
32 | if(object instanceof Geometry) {
33 | primitivesArray = object.getPrimitives();
34 | } else if (object instanceof Primitive) {
35 | primitivesArray = [ object ];
36 | }
37 |
38 |
39 |
40 | for(let i = 0; i < primitivesArray.length; i++) {
41 | let primitive = primitivesArray[i];
42 | let material = primitive.getMaterial();
43 | if(material === undefined) {
44 | console.error("Lumen-2D error: Material not specified");
45 | return;
46 | }
47 |
48 | this._objects.push(primitive);
49 |
50 | if(material instanceof EmitterMaterial) {
51 | let prevCdfValue = 0;
52 | if(this._emittersCdfArray.length !== 0)
53 | prevCdfValue = this._emittersCdfArray[this._emittersCdfArray.length - 1].cdfValue;
54 |
55 | let sampleValue;
56 | if(material.samplePower) {
57 | sampleValue = material.samplePower;
58 | } else {
59 | sampleValue = (material.color[0] + material.color[1] + material.color[2]) * material.sampleWeight;
60 | }
61 |
62 | let newCdfValue = prevCdfValue + sampleValue;
63 | this._emittersCdfArray.push({ object: primitive, cdfValue: newCdfValue });
64 |
65 | this._emittersCdfMax = newCdfValue;
66 | }
67 | }
68 | }
69 |
70 | // Uses a binary search to choose an emitter, the probability of choosing an emitter over the other depends on the emitter's color strenght
71 | // Emitter.sampleWeight can be used to change the cdf value for that particular sample
72 | getEmitter() {
73 | let t = Math.random();
74 | let sampledCdfValue = t * this._emittersCdfMax;
75 |
76 | // binary search
77 | let iLow = 0;
78 | let iHigh = this._emittersCdfArray.length - 1;
79 |
80 | if(this._emittersCdfArray.length === 1) return this._emittersCdfArray[0].object;
81 |
82 | while(iLow <= iHigh) {
83 | let iMiddle = Math.floor((iLow + iHigh) / 2);
84 |
85 | let iMiddleCdfValue = this._emittersCdfArray[iMiddle].cdfValue;
86 | let prevCdfValue = 0;
87 | if(iMiddle > 0) prevCdfValue = this._emittersCdfArray[iMiddle - 1].cdfValue;
88 |
89 | if(sampledCdfValue > prevCdfValue && sampledCdfValue < iMiddleCdfValue) {
90 | iLow = iMiddle; // found
91 | break;
92 | }
93 | else if(sampledCdfValue < prevCdfValue) {
94 | iHigh = iMiddle - 1;
95 | } else {
96 | iLow = iMiddle + 1;
97 | }
98 | }
99 |
100 | return this._emittersCdfArray[iLow].object;
101 | }
102 |
103 | intersect(ray) {
104 |
105 | if(!this._bvh) {
106 | this._bvh = new BVH(this._objects, {
107 | showDebug: this.args.showBVHdebug
108 | });
109 | }
110 |
111 | let result = this._bvh.intersect(ray);
112 |
113 | return result;
114 | }
115 | }
116 |
117 | export { Scene }
--------------------------------------------------------------------------------
/libs/scenes/features/motionBlur.js:
--------------------------------------------------------------------------------
1 | import { Edge } from "./geometry/Edge.js";
2 | import { Circle } from "./geometry/Circle.js";
3 | import { LambertMaterial } from "./material/lambert.js";
4 | import { EmitterMaterial } from "./material/emitter.js";
5 | import { Utils } from "./utils.js";
6 | import { DielectricMaterial } from "./material/dielectric.js";
7 | import { ContributionModifierMaterial } from "./material/contributionModifier.js";
8 |
9 |
10 | function createScene(scene, workerData, motionBlurT, ctx, frameNumber) {
11 |
12 | let edgeMaterial = new LambertMaterial({ opacity: 1 });
13 | let tbound = 11;
14 | let lbound = 19.5;
15 | let rbound = 19.5;
16 | let bbound = 11;
17 | let ledge = new Edge(-lbound, -bbound, -lbound, tbound);
18 | let redge = new Edge( rbound, -bbound, rbound, tbound);
19 | let tedge = new Edge(-lbound, tbound, rbound, tbound);
20 | let bedge = new Edge(-lbound, -bbound, rbound, -bbound);
21 |
22 |
23 | scene.add(ledge, edgeMaterial);
24 | scene.add(redge, edgeMaterial);
25 | scene.add(tedge, edgeMaterial);
26 | scene.add(bedge, edgeMaterial);
27 |
28 |
29 |
30 | let seed = "juice921";
31 | Utils.setSeed(seed);
32 | let rand = Utils.rand;
33 |
34 |
35 | let triangleMaterial2 = new DielectricMaterial({
36 | opacity: 1,
37 | transmittance: 1,
38 | ior: 1.4,
39 | roughness: 0.05,
40 | dispersion: 0.15,
41 | absorption: 0.35
42 | });
43 |
44 |
45 | let edgesMaterial2 = triangleMaterial2;
46 | let edgesMaterial3 = new ContributionModifierMaterial({ modifier: 0.2 });
47 | let edgesMaterial4 = new ContributionModifierMaterial({ modifier: 1 / 0.2 });
48 |
49 | for(let i = 0; i < 9; i++) {
50 | let radius = 2;
51 |
52 | let xt = 5;
53 | let yt = 5;
54 |
55 | let xo = Utils.rand() * 0.5 - 0.25;
56 | let yo = Utils.rand() * 0.5 - 0.25;
57 |
58 | for(let j = 0; j < 4; j++) {
59 | let x1 = -1;
60 | let y1 = -1;
61 |
62 | let x2 = -1;
63 | let y2 = +1;
64 |
65 | let x4 = +1;
66 | let y4 = -1;
67 |
68 | let x3 = +1;
69 | let y3 = +1;
70 |
71 | x1 *= radius;
72 | y1 *= radius;
73 | x2 *= radius;
74 | y2 *= radius;
75 | x3 *= radius;
76 | y3 *= radius;
77 | x4 *= radius;
78 | y4 *= radius;
79 |
80 | let xOff = xo * j;
81 | let yOff = yo * j;
82 |
83 |
84 | let ix = i % 3 - 1;
85 | let iy = Math.floor(i / 3) - 1;
86 |
87 |
88 | xOff += xt * ix;
89 | yOff += yt * iy;
90 |
91 | if(i === 5) {
92 | xOff += 2 * motionBlurT;
93 | }
94 | if(i === 3) {
95 | xOff -= 2 * motionBlurT;
96 | }
97 | if(i === 1) {
98 | yOff -= 2 * motionBlurT;
99 | }
100 | if(i === 7) {
101 | yOff += 2 * motionBlurT;
102 | }
103 |
104 |
105 | x1 += xOff;
106 | x2 += xOff;
107 | x3 += xOff;
108 | x4 += xOff;
109 | y1 += yOff;
110 | y2 += yOff;
111 | y3 += yOff;
112 | y4 += yOff;
113 |
114 | let blur = 0;
115 | let edgesMaterial = edgesMaterial3;
116 | if(((i * 9) + j) % 2 === 1) edgesMaterial = edgesMaterial4;
117 | if(i === 4) edgesMaterial = edgesMaterial2;
118 |
119 |
120 |
121 |
122 | scene.add(new Edge(x1, y1, x2, y2, blur), edgesMaterial);
123 | scene.add(new Edge(x2, y2, x3, y3, blur), edgesMaterial);
124 | scene.add(new Edge(x3, y3, x4, y4, blur), edgesMaterial);
125 | scene.add(new Edge(x4, y4, x1, y1, blur), edgesMaterial);
126 |
127 | radius *= 0.7;
128 | }
129 | }
130 |
131 | scene.add(
132 | new Circle(16, 7.5, 3),
133 | new EmitterMaterial({
134 | opacity: 0,
135 | color: [40, 90, 250]
136 | })
137 | );
138 | }
139 |
140 | export { createScene };
--------------------------------------------------------------------------------
/libs/scenes/features/archs.js:
--------------------------------------------------------------------------------
1 | import { Edge } from "./geometry/Edge.js";
2 | import { Circle } from "./geometry/Circle.js";
3 | import { LambertMaterial } from "./material/lambert.js";
4 | import { EmitterMaterial } from "./material/emitter.js";
5 | import { glMatrix, vec2 } from "./dependencies/gl-matrix-es6.js";
6 | import { Utils } from "./utils.js";
7 | import { DielectricMaterial } from "./material/dielectric.js";
8 |
9 |
10 | function createScene(scene, workerData, motionBlurT, ctx, frameNumber) {
11 |
12 | let edgeMaterial = new LambertMaterial({ opacity: 1 });
13 | let tbound = 13;
14 | let lbound = 19.5;
15 | let rbound = 19.5;
16 | let bbound = 11;
17 | let ledge = new Edge(-lbound, -bbound, -lbound, tbound);
18 | let redge = new Edge( rbound, -bbound, rbound, tbound);
19 | let tedge = new Edge(-lbound, tbound, rbound, tbound);
20 | let bedge = new Edge(-lbound, -bbound, rbound, -bbound);
21 |
22 |
23 | scene.add(ledge, edgeMaterial);
24 | scene.add(redge, edgeMaterial);
25 | scene.add(tedge, edgeMaterial);
26 | scene.add(bedge, edgeMaterial);
27 |
28 |
29 |
30 | let seed = Math.floor(workerData.randomNumber * 1000000000);
31 | Utils.setSeed(seed);
32 | let rand = Utils.rand;
33 |
34 |
35 | let material1 = new LambertMaterial({ opacity: 0.8 });
36 | let material2 = new DielectricMaterial({ opacity: 1, transmittance: 0.8, ior: 1.5, roughness: 0.1 });
37 | let material3 = new EmitterMaterial({ opacity: 1, color: [1500, 150, 10] });
38 | let material4 = new EmitterMaterial({ opacity: 1, color: [45000, 4500, 10], sampleWeight: 0.01 });
39 |
40 |
41 |
42 | let nodes = [];
43 | let nodesCount = 300;
44 | let howCloseToAllowConnection = 1.5;
45 | let archsLimitPerNode = 3;
46 | for(let i = 0; i < nodesCount; i++) {
47 | let angle = rand() * Math.PI * 2;
48 | let radius = Math.pow(rand(), 0.65) * 8.5;
49 |
50 | let node = {
51 | x: Math.cos(angle) * radius,
52 | y: Math.sin(angle) * radius,
53 | };
54 | nodes.push(node);
55 |
56 | if(i % 8 > 2)
57 | scene.add(new Circle(node.x, node.y, 0.15), material1);
58 | else if (i % 8 === 0)
59 | scene.add(new Circle(node.x, node.y, 0.15), material2);
60 | else if (i % 8 === 1) {
61 | scene.add(new Circle(node.x, node.y, 0.15), material3);
62 | scene.add(new Circle(node.x, node.y, 0.0001), material4);
63 | }
64 | }
65 |
66 |
67 | let existingArchs = { };
68 | let archsCount = { };
69 | for(let i = 0; i < nodesCount; i++) {
70 | let randomNodeIndex = Math.floor(rand() * nodesCount);
71 | let randomNode = nodes[randomNodeIndex];
72 |
73 | for(let j = 0; j < nodesCount; j++) {
74 | let randomNodeIndex2 = j;
75 | let randomNode2 = nodes[randomNodeIndex2];
76 |
77 | if(archsCount[randomNodeIndex] > archsLimitPerNode) continue;
78 | if(archsCount[randomNodeIndex2] > archsLimitPerNode) continue;
79 | if(randomNodeIndex === randomNodeIndex2) continue;
80 | if (existingArchs[randomNodeIndex + " " + randomNodeIndex2]) continue;
81 |
82 |
83 | let v1 = vec2.fromValues(randomNode.x, randomNode.y);
84 | let v2 = vec2.fromValues(randomNode2.x, randomNode2.y);
85 | let v3 = vec2.create();
86 | vec2.sub(v3, v2, v1);
87 |
88 | if(vec2.length(v3) < howCloseToAllowConnection) {
89 | if(i % 8 > 2)
90 | scene.add(new Edge(randomNode.x, randomNode.y, randomNode2.x, randomNode2.y), material1);
91 | else if (i % 8 === 0)
92 | scene.add(new Edge(randomNode.x, randomNode.y, randomNode2.x, randomNode2.y), material2);
93 |
94 | existingArchs[randomNodeIndex + " " + randomNodeIndex2] = true;
95 | existingArchs[randomNodeIndex2 + " " + randomNodeIndex] = true;
96 |
97 | if(archsCount[randomNodeIndex] === undefined) archsCount[randomNodeIndex] = 0;
98 | if(archsCount[randomNodeIndex2] === undefined) archsCount[randomNodeIndex2] = 0;
99 | archsCount[randomNodeIndex]++;
100 | archsCount[randomNodeIndex2]++;
101 | }
102 | }
103 | }
104 | }
105 |
106 | export { createScene };
--------------------------------------------------------------------------------
/libs/scenes/features/castaway.js:
--------------------------------------------------------------------------------
1 | import { Edge } from "./geometry/Edge.js";
2 | import { Circle } from "./geometry/Circle.js";
3 | import { LambertMaterial } from "./material/lambert.js";
4 | import { LambertEmitterMaterial } from "./material/lambertEmitter.js";
5 | import { ExperimentalDielectricMaterial } from "./material/experimentalDielectric.js";
6 |
7 | /*
8 | copy in ./libs/Globals.js
9 |
10 | var Globals = {
11 |
12 | // Sampling
13 | epsilon: 0.00005,
14 | highPrecision: false,
15 | USE_STRATIFIED_SAMPLING: true,
16 | samplingRatioPerPixelCovered: 0.14,
17 | LIGHT_BOUNCES: 25,
18 | skipBounce: 0,
19 |
20 | // Threading
21 | workersCount: 5,
22 | PHOTONS_PER_UPDATE: 20000,
23 |
24 | // Environment
25 | WORLD_SIZE: 20,
26 | worldAttenuation: 0.025,
27 |
28 | // Video export
29 | registerVideo: false,
30 | photonsPerVideoFrame: 5000000,
31 | framesPerSecond: 30,
32 | framesCount: 30,
33 | frameStart: 0,
34 |
35 | // Motion blur
36 | motionBlur: false,
37 | motionBlurFramePhotons: 5000,
38 |
39 | // Offscreen canvas
40 | deactivateOffscreenCanvas: false, // setting it to false slows down render times by about 1.7x
41 | offscreenCanvasCPow: 1.1,
42 |
43 | // Canvas size
44 | canvasSize: {
45 | width: 950,
46 | height: 800,
47 | },
48 |
49 | // Reinhard tonemapping
50 | toneMapping: true,
51 | gamma: 2.2,
52 | exposure: 1,
53 | }
54 |
55 | export { Globals };
56 |
57 | */
58 |
59 | function createScene(scene, workerData, motionBlurT, ctx, frameNumber) {
60 |
61 | createWorldBounds(scene);
62 |
63 | let yo = 1;
64 |
65 | for(let i = -370; i < 370; i++) {
66 | let y1 = Math.sin(i * 0.02) * 0.1 + Math.sin(i * 0.04) * 0.37;
67 | let y2 = Math.sin((i+1) * 0.02) * 0.1 + Math.sin((i+1) * 0.04) * 0.37;
68 |
69 | y1 -= yo;
70 | y2 -= yo;
71 |
72 | let at = (i + 370) / 740;
73 |
74 | let seaMaterial = new ExperimentalDielectricMaterial({
75 | opacity: 1,
76 | transmittance: 1,
77 | ior: 1.5,
78 | absorption: 0.35,
79 | dispersion: 0.27,
80 | roughness: 0.05,
81 | volumeAbsorption: [1.2, 0.35 + at * 0.2, 0.45 - at * 0.2],
82 | // reflectionStrenght: 2.35,
83 | })
84 |
85 | let seaEdge = new Edge(i * 0.0385, y1, i * 0.0385 + 0.0385, y2);
86 | scene.add(seaEdge, seaMaterial);
87 | }
88 |
89 | Pentagon(0, 0 -yo, 3.6, 0, true, new LambertMaterial({ opacity: 0.6}), scene);
90 |
91 | let lightSource = new Edge(5, 11, -5, 11);
92 | let lightMaterial = new LambertEmitterMaterial({ color: [1000, 800, 700] });
93 | scene.add(lightSource, lightMaterial);
94 | }
95 |
96 | function Pentagon(x, y, radius1, rotation, nothollow, material, scene) {
97 |
98 | if(!material) material = new LambertMaterial({ opacity: 0.7 });
99 |
100 | for(let i = 0; i < 5; i++) {
101 | let angle1 = (i / 5) * Math.PI * 2;
102 | let angle2 = ((i+1) / 5) * Math.PI * 2;
103 |
104 | let x1 = Math.cos(angle1) * radius1;
105 | let y1 = Math.sin(angle1) * radius1;
106 |
107 | let x2 = Math.cos(angle2) * radius1;
108 | let y2 = Math.sin(angle2) * radius1;
109 |
110 | x1 += x;
111 | x2 += x;
112 |
113 | y1 += y;
114 | y2 += y;
115 |
116 | scene.add(new Edge(x2, y2, x1, y1), material);
117 | }
118 | }
119 |
120 | function createWorldBounds(scene) {
121 | let edgeMaterial = new LambertMaterial({ opacity: 1 });
122 | let tbound = 11.5;
123 | let lbound = 12;
124 | let rbound = 12;
125 | let bbound = 11.5;
126 | let ledge = new Edge(-lbound, -bbound, -lbound, tbound, 0, 1, 0);
127 | let redge = new Edge( rbound, -bbound, rbound, tbound, 0, -1, 0);
128 | let tedge = new Edge(-lbound, tbound, rbound, tbound, 0, 0, -1);
129 | let bedge = new Edge(-lbound, -bbound, rbound, -bbound, 0, 0, 1);
130 |
131 | scene.add(ledge, edgeMaterial);
132 | scene.add(redge, edgeMaterial);
133 | scene.add(tedge, edgeMaterial);
134 | scene.add(bedge, edgeMaterial);
135 | }
136 |
137 | export { createScene };
--------------------------------------------------------------------------------
/libs/scenes/examples/primitives-1.js:
--------------------------------------------------------------------------------
1 | import { Edge } from "../../geometry/Edge.js";
2 | import { Circle } from "../../geometry/Circle.js";
3 | import { LambertMaterial } from "../../material/lambert.js";
4 | import { MicrofacetMaterial } from "../../material/microfacet.js";
5 | import { LambertEmitterMaterial } from "../../material/lambertEmitter.js";
6 | import { BeamEmitterMaterial } from "../../material/beamEmitter.js";
7 | import { DielectricMaterial } from "../../material/dielectric.js";
8 | import { Utils } from "../../utils.js";
9 |
10 |
11 | /**
12 | Global settings used to render this scene
13 |
14 | var Globals = {
15 |
16 | // Sampling
17 | epsilon: 0.00005,
18 | highPrecision: false, // if set to true, uses Float64Arrays which are 2x slower to work with
19 | USE_STRATIFIED_SAMPLING: true,
20 | samplingRatioPerPixelCovered: 0.14,
21 | LIGHT_BOUNCES: 35,
22 | skipBounce: 0,
23 |
24 | // Threading
25 | workersCount: 5,
26 | PHOTONS_PER_UPDATE: 50000,
27 |
28 | // Environment
29 | WORLD_SIZE: 20,
30 | worldAttenuation: 0.01,
31 |
32 | // Video export
33 | registerVideo: false,
34 | photonsPerVideoFrame: 5000000,
35 | framesPerSecond: 30,
36 | framesCount: 30,
37 | frameStart: 0,
38 |
39 | // Motion blur
40 | motionBlur: false,
41 | motionBlurFramePhotons: 5000,
42 |
43 | // Offscreen canvas
44 | deactivateOffscreenCanvas: false, // setting it to false slows down render times by about 1.7x
45 | offscreenCanvasCPow: 1.1,
46 |
47 | // Canvas size
48 | canvasSize: {
49 | width: 1300,
50 | height: 1000,
51 | },
52 |
53 | // Reinhard tonemapping
54 | toneMapping: true,
55 | gamma: 2.2,
56 | exposure: 1.3,
57 | }
58 |
59 | export { Globals };
60 | */
61 |
62 |
63 | function createScene(scene, workerData, motionBlurT, ctx, frameNumber) {
64 |
65 | createWorldBounds(scene);
66 |
67 |
68 |
69 |
70 |
71 | for(let i = 0; i < 50; i++) {
72 | let angle = (i / 50) * Math.PI * 2;
73 | let radius = 7;
74 | let edge = new Edge().scale(0.2 + i * 0.067).rotate(0.5).translate(radius, 0).rotate(angle);
75 | let mat1 = new MicrofacetMaterial({ opacity: 1, roughness: 0.005 });
76 |
77 | let radius2 = 4;
78 | let edge2 = new Edge().scale(0.4).rotate(-0.5).translate(radius2, 0).rotate(angle);
79 |
80 |
81 | scene.add(edge, mat1);
82 | scene.add(edge2, mat1);
83 | }
84 |
85 |
86 | let circle = new Circle(0,0, 1.4);
87 | circle.setMaterial(new DielectricMaterial({
88 | opacity: 1,
89 | ior: 1.4,
90 | absorption: 0.4,
91 | volumeAbsorption: [0, 0.15, 0.2],
92 | }));
93 |
94 |
95 |
96 |
97 | for(let i = 0; i < 1; i++) {
98 | let r = 1.1; //Utils.rand() * 2 + 4;
99 | // let sa = Utils.rand() * Math.PI * 2;
100 | // let ea = sa + Utils.rand() * Math.PI * 1.5;
101 | let sa = 0;
102 | let ea = Math.PI * 2;
103 |
104 | ctx.strokeStyle = "rgb(215, 215, 215)";
105 | ctx.lineWidth = 0.1;
106 |
107 | ctx.beginPath();
108 | ctx.arc(0, 0, r, sa, ea);
109 | ctx.stroke();
110 | }
111 |
112 |
113 |
114 |
115 |
116 |
117 | scene.add(circle);
118 |
119 | // let lightSource = new Circle(0,0,1).flipNormal();
120 | // let lightMaterial = new LambertEmitterMaterial({ color: [500, 500, 500], opacity: 0 });
121 | // scene.add(lightSource, lightMaterial);
122 |
123 |
124 | let lightSource = new Edge().scale(2).rotate(Math.PI * 0.5).translate(15, 3);
125 | let lightMaterial = new LambertEmitterMaterial({ color: [1200, 1100, 1000] });
126 | scene.add(lightSource, lightMaterial);
127 | }
128 |
129 | function createWorldBounds(scene) {
130 | let edgeMaterial = new LambertMaterial({ opacity: 1 });
131 | let tbound = 11 * 3;
132 | let lbound = 19 * 3;
133 | let rbound = 19 * 3;
134 | let bbound = 11 * 3;
135 | let ledge = new Edge(-lbound, -bbound, -lbound, tbound, 0, 1, 0);
136 | let redge = new Edge( rbound, -bbound, rbound, tbound, 0, -1, 0);
137 | let tedge = new Edge(-lbound, tbound, rbound, tbound, 0, 0, -1);
138 | let bedge = new Edge(-lbound, -bbound, rbound, -bbound, 0, 0, 1);
139 |
140 | scene.add(ledge, edgeMaterial);
141 | scene.add(redge, edgeMaterial);
142 | scene.add(tedge, edgeMaterial);
143 | scene.add(bedge, edgeMaterial);
144 | }
145 |
146 | export { createScene };
--------------------------------------------------------------------------------
/libs/geometry/Circle.js:
--------------------------------------------------------------------------------
1 | import { Primitive } from "./Primitive.js"
2 | import { glMatrix, vec2, vec3 } from "./../dependencies/gl-matrix-es6.js";
3 | import { AABB } from "./AABB.js";
4 |
5 | class Circle extends Primitive {
6 | constructor(x, y, radius, blur) {
7 | super();
8 |
9 | // if no arguments are passed
10 | if(x === undefined) {
11 | x = 0;
12 | y = 0;
13 | radius = 1;
14 | }
15 |
16 | this.center = vec2.fromValues(x, y);
17 | this.radius = radius;
18 | this.blur = blur || 0;
19 |
20 | this.invNormal = false;
21 |
22 | this.computeAABB();
23 | }
24 |
25 | computeAABB() {
26 | let x = this.center[0];
27 | let y = this.center[1];
28 | let radius = this.radius;
29 |
30 | this.aabb = new AABB();
31 | this.aabb.addVertex(vec2.fromValues(x - radius, y - radius));
32 | this.aabb.addVertex(vec2.fromValues(x + radius, y + radius));
33 |
34 | if(this.blur > 0) {
35 | let minx = x - radius - this.blur;
36 | let miny = y - radius - this.blur;
37 | let maxx = x + radius + this.blur;
38 | let maxy = y + radius + this.blur;
39 | let blurV0 = vec2.fromValues(minx, miny);
40 | let blurV1 = vec2.fromValues(maxx, maxy);
41 | this.aabb.addVertex(blurV0);
42 | this.aabb.addVertex(blurV1);
43 | }
44 | }
45 |
46 | intersect(ray) {
47 | let e = ray.d
48 |
49 | let center = vec2.clone(this.center);
50 | if(this.blur > 0) {
51 | // perturb this edge's center
52 | // since the AABB was expanded to fit this perturbation we can be sure
53 | // everything will stay inside the AABB bounds
54 | let randomAngle = Math.random() * Math.PI * 2;
55 | let randomRadius = this.blur * Math.random();
56 | let offx = randomRadius * Math.cos(randomAngle);
57 | let offy = randomRadius * Math.sin(randomAngle);
58 |
59 | center[0] += offx;
60 | center[1] += offy;
61 | }
62 |
63 |
64 |
65 |
66 | let h = vec2.create();
67 | vec2.sub(h, center, ray.o);
68 |
69 | let lf = vec2.dot(e, h);
70 | let s = this.radius * this.radius - vec2.dot(h,h) + lf * lf;
71 | if (s < 0.0) return false; // no intersection points ?
72 | s = Math.sqrt(s);
73 |
74 | let intersectionPoints = 0;
75 | if (lf < s) { // S1 behind A ?
76 | if (lf+s >= 0) { // S2 before A ?
77 | s = -s; // swap S1 <-> S2
78 | intersectionPoints = 1;
79 | }
80 | } else intersectionPoints = 2;
81 |
82 | if(intersectionPoints === 0) return false;
83 |
84 |
85 | let S1 = vec2.create();
86 | let S2 = vec2.create();
87 |
88 | vec2.scale(S1, e, lf-s);
89 | let t = vec2.length(S1);
90 | vec2.add(S1, S1, ray.o);
91 |
92 | vec2.scale(S2, e, lf+s);
93 | vec2.add(S2, S2, ray.o);
94 |
95 |
96 | let normal = vec2.create();
97 | vec2.sub(normal, S1, center);
98 | vec2.normalize(normal, normal);
99 |
100 | if(this.invNormal) {
101 | vec2.negate(normal, normal);
102 | }
103 |
104 | let result = {
105 | t: t,
106 | normal: normal,
107 | }
108 |
109 | return result;
110 | }
111 |
112 | // used to sample a point if this object is an emitter
113 | getRandomPoint() {
114 | let randomAngle = Math.random() * Math.PI * 2;
115 |
116 | let x = Math.cos(randomAngle) * this.radius + this.center[0];
117 | let y = Math.sin(randomAngle) * this.radius + this.center[1];
118 | let randomPoint = vec2.fromValues(x, y);
119 |
120 | let normal = vec2.create();
121 | vec2.subtract(normal, randomPoint, this.center);
122 | vec2.normalize(normal, normal);
123 |
124 | if(this.invNormal) {
125 | vec2.negate(normal, normal);
126 | }
127 |
128 | return {
129 | p: randomPoint,
130 | normal: normal
131 | }
132 | }
133 |
134 | rotate(radians) {
135 | /* not implemented */
136 | return this;
137 | }
138 | translate(x, y) {
139 | this.center[0] += x;
140 | this.center[1] += y;
141 |
142 | this.computeAABB();
143 |
144 | return this;
145 | }
146 | scale(amount) {
147 | this.radius *= amount;
148 |
149 | this.computeAABB();
150 |
151 | return this;
152 | }
153 | flipNormal() {
154 | this.invNormal = !this.invNormal;
155 | return this;
156 | }
157 | }
158 |
159 | export { Circle }
160 |
--------------------------------------------------------------------------------
/libs/videoManager.js:
--------------------------------------------------------------------------------
1 | import "./dependencies/webm-writer-0.2.0.js";
2 | import { download } from "./dependencies/download.js";
3 |
4 |
5 | class VideoManager {
6 |
7 | constructor(Globals, workers, canvas) {
8 | this.videoWriter = new WebMWriter({
9 | quality: 1, // WebM image quality from 0.0 (worst) to 1.0 (best)
10 | fileWriter: null, // FileWriter in order to stream to a file instead of buffering to memory (optional)
11 | fd: null, // Node.js file handle to write to instead of buffering to memory (optional)
12 |
13 | // You must supply one of:
14 | frameDuration: null, // Duration of frames in milliseconds
15 | frameRate: Globals.framesPerSecond, // Number of frames per second
16 | });
17 |
18 | this.currentVideoFrame = Globals.frameStart;
19 | this.activeWorkers = Globals.workersCount;
20 |
21 | this.videoPhotonsCounter = 0;
22 | this.preparingNextFrame = false;
23 |
24 | // reads as "next video frame steps"
25 | this.nvfSteps = {
26 | STOP_WORKERS: 0,
27 | WAITING_WORKERS_BLOCK: 3,
28 | ALL_WORKERS_BLOCKED: 1,
29 | ALL_WORKERS_ACTIVE: 2,
30 | currentStep: 2,
31 | }
32 |
33 | this.Globals = Globals;
34 |
35 | this.workers = workers;
36 |
37 | this.canvas = canvas;
38 |
39 | this.events = { };
40 | }
41 |
42 |
43 |
44 | onWorkerAcknowledge() {
45 | this.activeWorkers--;
46 | }
47 |
48 |
49 | prepareNextVideoFrame() {
50 | this.preparingNextFrame = true;
51 |
52 | if(this.nvfSteps.currentStep === this.nvfSteps.ALL_WORKERS_ACTIVE) {
53 | this.preparingNextFrame = false;
54 | }
55 |
56 |
57 |
58 |
59 | // stop every active webworker
60 | if(this.nvfSteps.currentStep === this.nvfSteps.STOP_WORKERS) {
61 |
62 | for(let i = 0; i < this.Globals.workersCount; i++) {
63 | this.workers[i].postMessage({ messageType: "stop-rendering" });
64 | }
65 |
66 | this.nvfSteps.currentStep = this.nvfSteps.WAITING_WORKERS_BLOCK;
67 | }
68 |
69 | // wait until all webworkers have received the stop message and acknowledged it,
70 | // then reset the current canvas state to prepare for a new frame
71 | if(this.nvfSteps.currentStep === this.nvfSteps.WAITING_WORKERS_BLOCK) {
72 | if(this.activeWorkers === 0) {
73 | this.nvfSteps.currentStep = this.nvfSteps.ALL_WORKERS_BLOCKED;
74 | this.fireEvent("reset-samples");
75 | this.videoPhotonsCounter = 0;
76 | }
77 | }
78 |
79 | // restart all webworkers and start computing the next video frame
80 | if(this.nvfSteps.currentStep === this.nvfSteps.ALL_WORKERS_BLOCKED) {
81 |
82 | for(let i = 0; i < this.Globals.workersCount; i++) {
83 | this.workers[i].postMessage({ messageType: "compute-next-video-frame", frameNumber: this.currentVideoFrame });
84 | }
85 |
86 | this.activeWorkers = this.Globals.workersCount;
87 | this.nvfSteps.currentStep = this.nvfSteps.ALL_WORKERS_ACTIVE;
88 | }
89 |
90 |
91 |
92 |
93 |
94 | // fire this function again until we're done with it
95 | if(this.preparingNextFrame) {
96 | requestAnimationFrame(this.prepareNextVideoFrame.bind(this));
97 | }
98 | }
99 |
100 |
101 | addEventListener(type, callback) {
102 | if(this.events[type] === undefined) {
103 | this.events[type] = [];
104 | }
105 | this.events[type].push(callback);
106 | }
107 |
108 | fireEvent(type) {
109 | if(this.events[type] === undefined) return;
110 |
111 | for(let i = 0; i < this.events[type].length; i++) {
112 | let callback = this.events[type][i];
113 | callback();
114 | }
115 | }
116 |
117 | update(photonsCount) {
118 | // if the amount of photons fired since last frame exceeds the value in Globals.photonsPerVideoFrame
119 | // begin webworkers synchronization to reset their state and compute a new frame
120 | if((photonsCount - this.videoPhotonsCounter) > this.Globals.photonsPerVideoFrame) {
121 |
122 | this.videoPhotonsCounter = photonsCount;
123 |
124 | let framesComputed = this.currentVideoFrame - this.Globals.frameStart;
125 |
126 | if(framesComputed >= this.Globals.framesCount) {
127 | this.videoWriter.complete().then(function(webMBlob) {
128 | download(webMBlob, "video.webm", 'video/webm');
129 | });
130 |
131 | this.videoPhotonsCounter = Infinity;
132 | } else {
133 | this.currentVideoFrame++;
134 | this.nvfSteps.currentStep = this.nvfSteps.STOP_WORKERS;
135 | this.preparingNextFrame = true;
136 |
137 | this.prepareNextVideoFrame();
138 |
139 | console.log("video frame saved: " + this.currentVideoFrame);
140 |
141 | this.videoWriter.addFrame(this.canvas);
142 | }
143 | }
144 | }
145 | }
146 |
147 | export { VideoManager };
--------------------------------------------------------------------------------
/libs/geometry/Edge.js:
--------------------------------------------------------------------------------
1 | import { Primitive } from "./Primitive.js"
2 | import { glMatrix, vec2, vec3 } from "./../dependencies/gl-matrix-es6.js";
3 | import { AABB } from "./AABB.js";
4 |
5 | class Edge extends Primitive {
6 | constructor(x, y, dx, dy, blur, nx, ny) {
7 | super();
8 |
9 | // if no arguments are passed
10 | if(x === undefined) {
11 | x = -1;
12 | y = 0;
13 | dx = 1;
14 | dy = 0;
15 | }
16 |
17 |
18 | this.v0 = vec2.fromValues(x, y);
19 | this.v1 = vec2.fromValues(dx, dy);
20 | this.blur = blur || 0;
21 | this.center = vec2.fromValues((this.v0[0] + this.v1[0]) / 2,
22 | (this.v0[1] + this.v1[1]) / 2);
23 |
24 | this.computeAABB();
25 |
26 |
27 | if(nx !== undefined && ny !== undefined) {
28 | this.normal = vec2.fromValues(nx, ny);
29 | vec2.normalize(this.normal, this.normal);
30 | } else {
31 | let nx = dx - x;
32 | let ny = dy - y;
33 | this.normal = vec2.fromValues(-ny, nx);
34 | vec2.normalize(this.normal, this.normal);
35 | }
36 | }
37 |
38 | computeAABB() {
39 | let x = this.v0[0];
40 | let y = this.v0[1];
41 | let dx = this.v1[0];
42 | let dy = this.v1[1];
43 |
44 | this.aabb = new AABB();
45 | this.aabb.addVertex(this.v0);
46 | this.aabb.addVertex(this.v1);
47 |
48 | if(this.blur > 0) {
49 | let minx = Math.min(x, dx) - this.blur;
50 | let miny = Math.min(y, dy) - this.blur;
51 | let maxx = Math.max(x, dx) + this.blur;
52 | let maxy = Math.max(y, dy) + this.blur;
53 | let blurV0 = vec2.fromValues(minx, miny);
54 | let blurV1 = vec2.fromValues(maxx, maxy);
55 | this.aabb.addVertex(blurV0);
56 | this.aabb.addVertex(blurV1);
57 | }
58 | }
59 |
60 | intersect(ray) {
61 | let p0_x = ray.o[0];
62 | let p0_y = ray.o[1];
63 | let p1_x = ray.o[0] + ray.d[0];
64 | let p1_y = ray.o[1] + ray.d[1];
65 | let p2_x = this.v0[0];
66 | let p2_y = this.v0[1];
67 | let p3_x = this.v1[0];
68 | let p3_y = this.v1[1];
69 | if(this.blur > 0) {
70 | // perturb this edge's center
71 | // since the AABB was expanded to fit this perturbation we can be sure
72 | // everything will stay inside the AABB bounds
73 | let randomAngle = Math.random() * Math.PI * 2;
74 | let randomRadius = this.blur * Math.random();
75 | let offx = randomRadius * Math.cos(randomAngle);
76 | let offy = randomRadius * Math.sin(randomAngle);
77 | p2_x += offx;
78 | p2_y += offy;
79 | p3_x += offx;
80 | p3_y += offy;
81 | }
82 |
83 | let s1_x, s1_y, s2_x, s2_y;
84 | s1_x = p1_x - p0_x; s1_y = p1_y - p0_y;
85 | s2_x = p3_x - p2_x; s2_y = p3_y - p2_y;
86 |
87 | let s, t;
88 | s = (-s1_y * (p0_x - p2_x) + s1_x * (p0_y - p2_y)) / (-s2_x * s1_y + s1_x * s2_y);
89 | t = ( s2_x * (p0_y - p2_y) - s2_y * (p0_x - p2_x)) / (-s2_x * s1_y + s1_x * s2_y);
90 |
91 | if (s >= 0 && s <= 1 && t >= 0) { // && t <= 1) { <-- the original version of the algorithm was restricted to two line segments instead of ray-segment intersections
92 | let result = {
93 | t: t,
94 | normal: this.normal
95 | }
96 | return result;
97 | }
98 |
99 | return false; // No collision
100 | }
101 |
102 | // used to sample a point if this object is an emitter
103 | getRandomPoint() {
104 | let t = Math.random();
105 |
106 | let x = this.v0[0] * t + this.v1[0] * (1 - t);
107 | let y = this.v0[1] * t + this.v1[1] * (1 - t);
108 |
109 | let randomPoint = vec2.fromValues(x, y);
110 |
111 | return {
112 | p: randomPoint,
113 | normal: this.normal
114 | }
115 | }
116 |
117 |
118 | rotate(radians) {
119 | vec2.rotate(this.v0, this.v0, vec2.fromValues(0,0), radians);
120 | vec2.rotate(this.v1, this.v1, vec2.fromValues(0,0), radians);
121 | vec2.rotate(this.normal, this.normal, vec2.fromValues(0,0), radians);
122 | this.center = vec2.fromValues((this.v0[0] + this.v1[0]) / 2,
123 | (this.v0[1] + this.v1[1]) / 2);
124 |
125 | this.computeAABB();
126 |
127 | return this;
128 | }
129 | translate(x, y) {
130 | this.center[0] += x;
131 | this.center[1] += y;
132 | this.v0[0] += x;
133 | this.v0[1] += y;
134 | this.v1[0] += x;
135 | this.v1[1] += y;
136 |
137 | this.computeAABB();
138 |
139 | return this;
140 | }
141 | scale(amount) {
142 | vec2.scale(this.v0, this.v0, amount);
143 | vec2.scale(this.v1, this.v1, amount);
144 | this.center = vec2.fromValues((this.v0[0] + this.v1[0]) / 2,
145 | (this.v0[1] + this.v1[1]) / 2);
146 |
147 | this.computeAABB();
148 |
149 | return this;
150 | }
151 | flipNormal() {
152 | vec2.negate(this.normal, this.normal);
153 | return this;
154 | }
155 | }
156 |
157 | export { Edge }
158 |
--------------------------------------------------------------------------------
/libs/scenes/examples/tiling1.js:
--------------------------------------------------------------------------------
1 | import { Edge } from "./geometry/Edge.js";
2 | import { Circle } from "./geometry/Circle.js";
3 | import { CPoly } from "./geometry/CPoly.js";
4 | import { LambertMaterial } from "./material/lambert.js";
5 | import { LambertEmitterMaterial } from "./material/lambertEmitter.js";
6 | import { BeamEmitterMaterial } from "./material/beamEmitter.js";
7 | import { ExperimentalDielectricMaterial } from "./material/experimentalDielectric.js";
8 | import { Utils } from "./utils.js";
9 |
10 |
11 | /*
12 | Global settings used to render this scene
13 |
14 | var Globals = {
15 |
16 | // Sampling
17 | epsilon: 0.00005,
18 | highPrecision: false, // if set to true, uses Float64Arrays which are 2x slower to work with
19 | USE_STRATIFIED_SAMPLING: true,
20 | samplingRatioPerPixelCovered: 0.14,
21 | LIGHT_BOUNCES: 35,
22 | skipBounce: 0,
23 |
24 | // Threading
25 | workersCount: 5,
26 | PHOTONS_PER_UPDATE: 50000,
27 |
28 | // Environment
29 | WORLD_SIZE: 25,
30 | worldAttenuation: 0.07,
31 |
32 | // Video export
33 | registerVideo: false,
34 | photonsPerVideoFrame: 5000000,
35 | framesPerSecond: 30,
36 | framesCount: 30,
37 | frameStart: 0,
38 |
39 | // Motion blur
40 | motionBlur: false,
41 | motionBlurFramePhotons: 5000,
42 |
43 | // Offscreen canvas
44 | deactivateOffscreenCanvas: true, // setting it to false slows down render times by about 1.7x
45 | offscreenCanvasCPow: 1.1,
46 |
47 | // Canvas size
48 | canvasSize: {
49 | width: 1300,
50 | height: 1000,
51 | },
52 |
53 | // Reinhard tonemapping
54 | toneMapping: true,
55 | gamma: 2.2,
56 | exposure: 1,
57 | }
58 |
59 | export { Globals };
60 | */
61 |
62 |
63 |
64 |
65 |
66 | function createScene(scene, workerData, motionBlurT, ctx, frameNumber) {
67 | createWorldBounds(scene);
68 |
69 | let r = 2;
70 | for(let i = -r * 2; i <= r * 2; i++) {
71 | for(let j = -r; j <= r; j++) {
72 | let verse = Math.floor(Utils.rand() * 2);
73 | let squareSize = 2.15;
74 | tiling(scene, i * squareSize, j * squareSize, squareSize, verse);
75 | }
76 | }
77 |
78 | let lss = 65;
79 | let lightSource1 = new Circle(0,15, 2);
80 | let lightMaterial1 = new LambertEmitterMaterial({ color: [100 * lss, 90 * lss, 80 * lss], opacity: 0 });
81 | lightSource1.setMaterial(lightMaterial1);
82 | scene.add(lightSource1);
83 | }
84 |
85 |
86 | function tiling(scene, x, y, s, t) {
87 | let precision = 50;
88 | for(let j = 0; j < 2; j++) {
89 | let angleOffset = j * Math.PI + t * Math.PI * 0.5;
90 |
91 | for(let i = 0; i < precision; i++) {
92 | let angle1 = (i / precision) * Math.PI * 0.5 + angleOffset;
93 | let angle2 = ((i+1) / precision) * Math.PI * 0.5 + angleOffset;
94 |
95 | let r1 = 0.9 + Math.cos((i / precision) * Math.PI * 4) * 0.2;
96 | let r2 = 0.9 + Math.cos(((i+1) / precision) * Math.PI * 4) * 0.2;
97 |
98 | let x1 = Math.cos(angle1) * ( s * 0.5 * r1 );
99 | let y1 = Math.sin(angle1) * ( s * 0.5 * r1 );
100 | let x2 = Math.cos(angle2) * ( s * 0.5 * r2 );
101 | let y2 = Math.sin(angle2) * ( s * 0.5 * r2 );
102 |
103 | if(j === 0 && t === 0) {
104 | x1 -= s * 0.5;
105 | y1 -= s * 0.5;
106 | x2 -= s * 0.5;
107 | y2 -= s * 0.5;
108 | }
109 | if(j === 1 && t === 0) {
110 | x1 += s * 0.5;
111 | y1 += s * 0.5;
112 | x2 += s * 0.5;
113 | y2 += s * 0.5;
114 | }
115 | if(j === 0 && t === 1) {
116 | x1 += s * 0.5;
117 | y1 -= s * 0.5;
118 | x2 += s * 0.5;
119 | y2 -= s * 0.5;
120 | }
121 | if(j === 1 && t === 1) {
122 | x1 -= s * 0.5;
123 | y1 += s * 0.5;
124 | x2 -= s * 0.5;
125 | y2 += s * 0.5;
126 | }
127 |
128 | x1 += x;
129 | x2 += x;
130 | y1 += y;
131 | y2 += y;
132 |
133 | let matId = ((y+5) / 10) * 0.55 - Math.abs(x1)*0.35;
134 | let material;
135 | if(matId > 0) {
136 | material = new ExperimentalDielectricMaterial({
137 | opacity: 1,
138 | transmittance: 1,
139 | ior: 1.5,
140 | absorption: 0.1,
141 | dispersion: 0.2,
142 | roughness: 0.01,
143 | refractionStrenght: 1.15,
144 | });
145 | } else {
146 | material = new LambertMaterial({ opacity: 0.97 });
147 | }
148 |
149 | scene.add(new Edge(x2, y2, x1, y1), material);
150 | }
151 | }
152 | }
153 |
154 |
155 |
156 |
157 | function createWorldBounds(scene) {
158 | let edgeMaterial = new LambertMaterial({ opacity: 1 });
159 | let tbound = 11 * 1.5;
160 | let lbound = 19.5 * 1;
161 | let rbound = 19.5 * 1;
162 | let bbound = 11 * 1.3;
163 | let ledge = new Edge(-lbound, -bbound, -lbound, tbound, 0, 1, 0);
164 | let redge = new Edge( rbound, -bbound, rbound, tbound, 0, -1, 0);
165 | let tedge = new Edge(-lbound, tbound, rbound, tbound, 0, 0, -1);
166 | let bedge = new Edge(-lbound, -bbound, rbound, -bbound, 0, 0, 1);
167 |
168 | scene.add(ledge, edgeMaterial);
169 | scene.add(redge, edgeMaterial);
170 | scene.add(tedge, edgeMaterial);
171 | scene.add(bedge, edgeMaterial);
172 | }
173 |
174 | export { createScene };
--------------------------------------------------------------------------------
/libs/main.js:
--------------------------------------------------------------------------------
1 | import { Globals } from "./globals.js";
2 | import { glMatrix, vec2, mat2, vec3 } from "./dependencies/gl-matrix-es6.js";
3 | import { VideoManager } from "./videoManager.js";
4 |
5 | window.addEventListener("load", init);
6 |
7 | var canvas;
8 | var context;
9 | var imageDataObject;
10 |
11 | var canvasSize = Globals.canvasSize;
12 | var sharedArray;
13 | // used to track how many photons have been fired by each worker
14 | var sharedInfoArray;
15 |
16 | var photonsFired = 0;
17 | var coloredPixels = 0;
18 |
19 | var workers = [];
20 |
21 | var videoManager;
22 |
23 | function init() {
24 |
25 | canvas = document.getElementById('canvas');
26 | canvas.width = canvasSize.width;
27 | canvas.height = canvasSize.height;
28 | context = canvas.getContext('2d');
29 | imageDataObject = context.createImageData(canvasSize.width, canvasSize.height);
30 |
31 |
32 |
33 | var canvasPixelsCount = canvasSize.width * canvasSize.height * 3;
34 | var size = Float32Array.BYTES_PER_ELEMENT * canvasPixelsCount;
35 | var sharedBuffer = new SharedArrayBuffer(size);
36 | // will be used to store information on photons traced
37 | var sharedInfoBuffer = new SharedArrayBuffer(Globals.workersCount * 4);
38 | sharedArray = new Float32Array(sharedBuffer);
39 | sharedInfoArray = new Float32Array(sharedInfoBuffer);
40 | for (let i = 0; i < canvasPixelsCount; i++) {
41 | sharedArray[i] = 0;
42 | }
43 | for (let i = 0; i < Globals.workersCount; i++) {
44 | sharedInfoArray[i] = 0;
45 | }
46 |
47 |
48 |
49 | startWebworkers(sharedBuffer, sharedInfoBuffer);
50 |
51 |
52 | videoManager = new VideoManager(Globals, workers, canvas);
53 | videoManager.addEventListener("reset-samples", resetAccumulatedSamples);
54 |
55 |
56 | requestAnimationFrame(renderSample);
57 | }
58 |
59 |
60 | function startWebworkers(sharedBuffer, sharedInfoBuffer) {
61 | let startWorkerMessage = {
62 | randomNumber: Math.random(),
63 | messageType: "start",
64 | sharedBuffer: sharedBuffer,
65 | sharedInfoBuffer: sharedInfoBuffer,
66 | workerIndex: 0,
67 | Globals: Globals,
68 | };
69 |
70 | let onWorkerMessage = e => {
71 | if(e.data.messageType == "photons-fired-update") {
72 |
73 | let message = e.data;
74 | let workerPhotonsFired = e.data.photonsFired;
75 |
76 | photonsFired += workerPhotonsFired;
77 | coloredPixels += e.data.coloredPixels;
78 |
79 | console.log("photons fired: " + photonsFired + " -- colored pixels: " + coloredPixels);
80 | }
81 |
82 | if(e.data.messageType == "stop-render-acknowledge") {
83 | videoManager.onWorkerAcknowledge();
84 | }
85 | };
86 |
87 | let onWorkerError = e => {
88 | console.log(e);
89 | }
90 |
91 | let workersCount = Globals.workersCount;
92 | for(let i = 0; i < workersCount; i++) {
93 | workers.push(new Worker("./libs/worker.js", { type: "module" }));
94 | startWorkerMessage.workerIndex = i;
95 | workers[i].postMessage(startWorkerMessage);
96 | workers[i].onmessage = onWorkerMessage;
97 | }
98 | }
99 |
100 |
101 | function resetAccumulatedSamples() {
102 | var length = canvasSize.width * canvasSize.height * 3;
103 | for (let i = 0; i < length; i++) {
104 | sharedArray[i] = 0;
105 | }
106 | for (let i = 0; i < Globals.workersCount; i++) {
107 | sharedInfoArray[i] = 0;
108 | }
109 |
110 | photonsFired = 0;
111 | coloredPixels = 0;
112 | }
113 |
114 |
115 | function renderSample() {
116 | requestAnimationFrame(renderSample);
117 | if(Globals.registerVideo && videoManager.preparingNextFrame) return;
118 |
119 |
120 | var imageData = imageDataObject.data;
121 |
122 | let mappedColor = vec3.create();
123 | let gamma = Globals.gamma;
124 | let exposure = Globals.exposure;
125 |
126 |
127 | // counting how many photons have been traced, each worker will update its own slot in sharedInfoArray
128 | let photonsCount = 0;
129 | for (let i = 0; i < Globals.workersCount; i++) {
130 | photonsCount += sharedInfoArray[i];
131 | }
132 |
133 |
134 | for (var i = 0; i < canvasSize.width * canvasSize.height * 4; i += 4) {
135 | let pixelIndex = Math.floor(i / 4);
136 | let y = canvasSize.height - 1 - Math.floor(pixelIndex / canvasSize.width);
137 | let x = pixelIndex % canvasSize.width;
138 |
139 | let index = (y * canvasSize.width + x) * 3;
140 |
141 | let r = sharedArray[index + 0] / (photonsCount);
142 | let g = sharedArray[index + 1] / (photonsCount);
143 | let b = sharedArray[index + 2] / (photonsCount);
144 |
145 |
146 | // Exposure tone mapping
147 | // from: https://learnopengl.com/Advanced-Lighting/HDR
148 | if(Globals.toneMapping) {
149 | mappedColor[0] = 1 - Math.exp(-r * exposure);
150 | mappedColor[1] = 1 - Math.exp(-g * exposure);
151 | mappedColor[2] = 1 - Math.exp(-b * exposure);
152 |
153 | mappedColor[0] = Math.pow(mappedColor[0], 1 / gamma);
154 | mappedColor[1] = Math.pow(mappedColor[1], 1 / gamma);
155 | mappedColor[2] = Math.pow(mappedColor[2], 1 / gamma);
156 |
157 | r = mappedColor[0] * 255;
158 | g = mappedColor[1] * 255;
159 | b = mappedColor[2] * 255;
160 | } else {
161 | r *= 255;
162 | g *= 255;
163 | b *= 255;
164 | }
165 |
166 |
167 | if(r > 255) r = 255;
168 | if(g > 255) g = 255;
169 | if(b > 255) b = 255;
170 |
171 | imageData[i + 0] = r;
172 | imageData[i + 1] = g;
173 | imageData[i + 2] = b;
174 | imageData[i + 3] = 255;
175 | }
176 |
177 | context.putImageData(imageDataObject, 0, 0);
178 |
179 |
180 |
181 |
182 | if(Globals.registerVideo) {
183 | videoManager.update(photonsCount);
184 | }
185 | }
--------------------------------------------------------------------------------
/libs/dependencies/quick-noise.js:
--------------------------------------------------------------------------------
1 | //
2 | // Perlin noise module.
3 | //
4 | // Written by Thom Chiovoloni, dedicated into the public domain (as explained at
5 | // http://creativecommons.org/publicdomain/zero/1.0/).
6 | //
7 | var quickNoise = (function() {
8 | 'use strict';
9 |
10 | function buildTable(randFunc) {
11 | if (!randFunc) {
12 | randFunc = Math.random;
13 | }
14 | // @NOTE(thom): could optimize this for allocations, but it
15 | // shouldn't be near anybody's fast path...
16 | var arr = new Array(256).map(function(v, i) { return i; });
17 | // shuffle numbers 0 through 255
18 | for (var i = arr.length-1; i > 0; --i) {
19 | var r = Math.floor(randFunc() * (i+1));
20 | var t = arr[r];
21 | arr[r] = arr[i];
22 | arr[i] = t;
23 | }
24 | return arr;
25 | }
26 |
27 | var gradBasis = [ 1,1,0, -1,1,0, 1,-1,0, -1,-1,0, 1,0,1, -1,0,1, 1,0,-1, -1,0,-1, 0,1,1, 0,-1,1, 0,1,-1, 0,-1,-1 ]
28 |
29 | function initTables(tab, permTable, gradTable) {
30 | if (tab == null || typeof tab === 'function') {
31 | tab = buildTable(tab)
32 | }
33 | else if (tab.length !== 256) {
34 | console.error("create(): Expected array of length 256, got ", tab);
35 | tab = buildTable();
36 | }
37 | for (var i = 0; i < 256; ++i) {
38 | permTable[i] = tab[i];
39 | permTable[i+256] = tab[i];
40 | }
41 | var gradIdx = 0;
42 | for (var i = 0; i < permTable.length; ++i) {
43 | var v = (permTable[i]%12)*3;
44 | gradTable[gradIdx++] = gradBasis[v];
45 | gradTable[gradIdx++] = gradBasis[v+1];
46 | gradTable[gradIdx++] = gradBasis[v+2];
47 | }
48 | }
49 |
50 | var permTableSize = 256*2;
51 | var gradTableSize = permTableSize*3;
52 | var totalSize = permTableSize + gradTableSize;
53 |
54 | //
55 | // function quickNoise.create(tableOrRng=Math.random);
56 | //
57 | // `tableOrRng` must either be:
58 | //
59 | // - A function that takes 0 arguments and returns a uniformly distributed
60 | // random number between 0 and 1 (like `Math.random`).
61 | // - An array of length 256, where the array is generated by shuffling all
62 | // integers between 0 and 255 (inclusive).
63 | //
64 | // If no argument (or a bad argument) is provided, it defaults to Math.random.
65 | //
66 | // This creates a perlin noise generation function. For more documentation about
67 | // the function returned by this call, see the documentation for `quickNoise.noise`, below.
68 | //
69 | // If you provide a function, this will be used only to generate the permutation table, and
70 | // will not be called after this function returns.
71 | //
72 | // The array argument provided in case you want to provide a specific permutation table.
73 | //
74 |
75 | function create(tab) {
76 | var ab = new ArrayBuffer(totalSize);
77 | var permTable = new Uint8Array(ab, 0, permTableSize);
78 | var gradTable = new Int8Array(ab, permTableSize, gradTableSize);
79 | initTables(tab, permTable, gradTable);
80 |
81 | function noise(x, y, z, xWrap, yWrap, zWrap) {
82 | // coersce to integers and handle missing arguments
83 | xWrap = xWrap | 0;
84 | yWrap = yWrap | 0;
85 | zWrap = zWrap | 0;
86 |
87 | // type hints for vm
88 | x = +x;
89 | y = +y;
90 | z = +z;
91 |
92 | var xMask = ((xWrap-1) & 255) >>> 0;
93 | var yMask = ((yWrap-1) & 255) >>> 0;
94 | var zMask = ((zWrap-1) & 255) >>> 0;
95 |
96 | var px = Math.floor(x);
97 | var py = Math.floor(y);
98 | var pz = Math.floor(z);
99 |
100 | var x0 = (px+0) & xMask;
101 | var x1 = (px+1) & xMask;
102 |
103 | var y0 = (py+0) & yMask;
104 | var y1 = (py+1) & yMask;
105 |
106 | var z0 = (pz+0) & zMask;
107 | var z1 = (pz+1) & zMask;
108 |
109 | x -= px;
110 | y -= py;
111 | z -= pz;
112 |
113 | var u = ((x*6.0-15.0)*x + 10.0) * x * x * x;
114 | var v = ((y*6.0-15.0)*y + 10.0) * y * y * y;
115 | var w = ((z*6.0-15.0)*z + 10.0) * z * z * z;
116 |
117 | var r0 = permTable[x0];
118 | var r1 = permTable[x1];
119 |
120 | var r00 = permTable[r0+y0];
121 | var r01 = permTable[r0+y1];
122 | var r10 = permTable[r1+y0];
123 | var r11 = permTable[r1+y1];
124 |
125 | var h000 = permTable[r00+z0] * 3;
126 | var h001 = permTable[r00+z1] * 3;
127 | var h010 = permTable[r01+z0] * 3;
128 | var h011 = permTable[r01+z1] * 3;
129 | var h100 = permTable[r10+z0] * 3;
130 | var h101 = permTable[r10+z1] * 3;
131 | var h110 = permTable[r11+z0] * 3;
132 | var h111 = permTable[r11+z1] * 3;
133 |
134 | var n000 = gradTable[h000]*(x+0) + gradTable[h000+1]*(y+0) + gradTable[h000+2]*(z+0);
135 | var n001 = gradTable[h001]*(x+0) + gradTable[h001+1]*(y+0) + gradTable[h001+2]*(z-1);
136 | var n010 = gradTable[h010]*(x+0) + gradTable[h010+1]*(y-1) + gradTable[h010+2]*(z+0);
137 | var n011 = gradTable[h011]*(x+0) + gradTable[h011+1]*(y-1) + gradTable[h011+2]*(z-1);
138 | var n100 = gradTable[h100]*(x-1) + gradTable[h100+1]*(y+0) + gradTable[h100+2]*(z+0);
139 | var n101 = gradTable[h101]*(x-1) + gradTable[h101+1]*(y+0) + gradTable[h101+2]*(z-1);
140 | var n110 = gradTable[h110]*(x-1) + gradTable[h110+1]*(y-1) + gradTable[h110+2]*(z+0);
141 | var n111 = gradTable[h111]*(x-1) + gradTable[h111+1]*(y-1) + gradTable[h111+2]*(z-1);
142 |
143 | var n00 = n000 + (n001-n000) * w;
144 | var n01 = n010 + (n011-n010) * w;
145 | var n10 = n100 + (n101-n100) * w;
146 | var n11 = n110 + (n111-n110) * w;
147 |
148 | var n0 = n00 + (n01-n00) * v;
149 | var n1 = n10 + (n11-n10) * v;
150 |
151 | return n0 + (n1-n0) * u;
152 | }
153 | return noise;
154 | }
155 |
156 | //
157 | // function quickNoise.noise(x, y, z, xWrap=0, yWrap=0, zWrap=0);
158 | //
159 | // - `x`, `y`, `z` are numbers.
160 | // - `xWrap`, `yWrap`, and `zWrap` are integer powers of two between 0 and 256.
161 | // (0 and 256 are equivalent). If these aren't provided, they default to 0.
162 | //
163 | // This implements Ken Perlin's revised noise function from 2002, in 3D. It
164 | // computes a random value for the coordinate `x`, `y`, `z`, where adjacent
165 | // values are continuous with a period of 1 (Values at integer points are
166 | // entirely unrelated).
167 | //
168 | // This function is seeded. That is, it will return the same results when
169 | // called with the same arguments, across successive program runs. An unseeded
170 | // version may be created with the `quickNoise.create` function. The table it is
171 | // seeded is the one from the `stb_perlin.h` library.
172 | //
173 | var noise = create([
174 | 23, 125, 161, 52, 103, 117, 70, 37, 247, 101, 203, 169, 124, 126, 44, 123,
175 | 152, 238, 145, 45, 171, 114, 253, 10, 192, 136, 4, 157, 249, 30, 35, 72,
176 | 175, 63, 77, 90, 181, 16, 96, 111, 133, 104, 75, 162, 93, 56, 66, 240,
177 | 8, 50, 84, 229, 49, 210, 173, 239, 141, 1, 87, 18, 2, 198, 143, 57,
178 | 225, 160, 58, 217, 168, 206, 245, 204, 199, 6, 73, 60, 20, 230, 211, 233,
179 | 94, 200, 88, 9, 74, 155, 33, 15, 219, 130, 226, 202, 83, 236, 42, 172,
180 | 165, 218, 55, 222, 46, 107, 98, 154, 109, 67, 196, 178, 127, 158, 13, 243,
181 | 65, 79, 166, 248, 25, 224, 115, 80, 68, 51, 184, 128, 232, 208, 151, 122,
182 | 26, 212, 105, 43, 179, 213, 235, 148, 146, 89, 14, 195, 28, 78, 112, 76,
183 | 250, 47, 24, 251, 140, 108, 186, 190, 228, 170, 183, 139, 39, 188, 244, 246,
184 | 132, 48, 119, 144, 180, 138, 134, 193, 82, 182, 120, 121, 86, 220, 209, 3,
185 | 91, 241, 149, 85, 205, 150, 113, 216, 31, 100, 41, 164, 177, 214, 153, 231,
186 | 38, 71, 185, 174, 97, 201, 29, 95, 7, 92, 54, 254, 191, 118, 34, 221,
187 | 131, 11, 163, 99, 234, 81, 227, 147, 156, 176, 17, 142, 69, 12, 110, 62,
188 | 27, 255, 0, 194, 59, 116, 242, 252, 19, 21, 187, 53, 207, 129, 64, 135,
189 | 61, 40, 167, 237, 102, 223, 106, 159, 197, 189, 215, 137, 36, 32, 22, 5
190 | ]);
191 |
192 | return {
193 | create: create,
194 | noise: noise
195 | };
196 |
197 | }());
198 |
199 | export { quickNoise };
200 |
201 | // if (typeof module !== 'undefined' && module.exports) {
202 | // module.exports = quickNoise;
203 | // }
--------------------------------------------------------------------------------
/libs/material/dielectric.js:
--------------------------------------------------------------------------------
1 | import { Material } from "./material.js";
2 | import { glMatrix, vec2, mat2 } from "./../dependencies/gl-matrix-es6.js";
3 | import { Globals } from "../globals.js";
4 |
5 | class DielectricMaterial extends Material {
6 | constructor(options) {
7 | super(options);
8 |
9 | if(!options) options = { };
10 |
11 | this.roughness = options.roughness !== undefined ? options.roughness : 0.15;
12 | this.ior = options.ior !== undefined ? options.ior : 1.4;
13 | this.transmittance = options.transmittance !== undefined ? options.transmittance : 1;
14 | this.dispersion = options.dispersion !== undefined ? options.dispersion : 0;
15 | this.absorption = options.absorption !== undefined ? options.absorption : 0.1;
16 | this.volumeAbsorption = options.volumeAbsorption !== undefined ? options.volumeAbsorption : 0;
17 |
18 | this.hasVolumeAbsorption = (this.volumeAbsorption[0] + this.volumeAbsorption[1] + this.volumeAbsorption[2]) > 0;
19 | }
20 |
21 | setSellmierCoefficients(b1, b2, b3, c1, c2, c3, d) {
22 |
23 | /*
24 | B1 = 12 * 1.03961212;
25 | B2 = 12 * 0.231792344;
26 | B3 = 12 * 1.01046945;
27 | C1 = 12 * 0.00600069867;
28 | C2 = 12 * 0.0200179144;
29 | C3 = 12 * 103.560653;
30 | */
31 |
32 | this.b1 = b1;
33 | this.b2 = b2;
34 | this.b3 = b3;
35 |
36 | this.c1 = c1;
37 | this.c2 = c2;
38 | this.c3 = c3;
39 |
40 | this.d = d;
41 | }
42 |
43 | computeScattering(ray, input_normal, t, contribution, worldAttenuation, wavelength) {
44 |
45 |
46 | // opacity test, if it passes we're going to let the ray pass through the object
47 | if(this.opacityTest(t, worldAttenuation, ray, contribution)) {
48 | return;
49 | }
50 |
51 |
52 |
53 | let dot = vec2.dot(ray.d, input_normal);
54 |
55 | let normal = vec2.clone(input_normal);
56 | if(dot > 0.0) {
57 | vec2.negate(normal, normal);
58 | }
59 |
60 |
61 | // dot = Math.abs( vec2.dot(ray.d, input_normal) );
62 | // contribution *= dot;
63 |
64 |
65 |
66 |
67 | /* a dielectric material in Lumen2D "should" specify an absorption value:
68 |
69 | imagine two mirror objects reflecting the same beam in the same direction
70 | with an infinite number of bounces.
71 |
72 | | |
73 | | --> x <-- |
74 | | |
75 |
76 | what's the "fluency" at point x ?
77 | It would be "infinite"! if we don't use an absorption coefficient,
78 | and we set a very high light-bounce limit (e.g. 400) the light that enters a dielectric poligon
79 | would bounce around it (thanks to fresnel reflection) a lot of times thus increasing its
80 | "brightness" on screen, and that could make the dielectric shape look brighter than its lightsource
81 | */
82 |
83 | let absorption = (1 - this.absorption);
84 | let wa = (Math.exp(-t * worldAttenuation)) * absorption;
85 | contribution.r *= wa;
86 | contribution.g *= wa;
87 | contribution.b *= wa;
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 | let newDirection = vec2.create();
97 |
98 | // evaluate BRDF - from Tantalum
99 | let w_o = vec2.fromValues(-ray.d[0], -ray.d[1]);
100 |
101 |
102 | // normal rotation matrix
103 | let mat = mat2.fromValues(input_normal[1], -input_normal[0], input_normal[0], input_normal[1]);
104 | let imat = mat2.create();
105 | mat2.invert(imat, mat);
106 | vec2.transformMat2(w_o, w_o, imat);
107 |
108 |
109 |
110 | let sigma = this.roughness;
111 | let ior = wavelength !== undefined ? this.getIOR(wavelength) : this.ior;
112 |
113 | let PI_HALF = Math.PI * 0.5;
114 | let theta = Math.asin(Math.min(Math.abs(w_o[0]), 1.0));
115 | let theta0 = Math.max(theta - PI_HALF, -PI_HALF);
116 | let theta1 = Math.min(theta + PI_HALF, PI_HALF);
117 |
118 |
119 | let xi = Math.random();
120 | let sigmaSq = sigma*sigma;
121 | let invSigmaSq = 1 / sigmaSq;
122 |
123 | let cdf0 = Math.tanh(theta0 * 0.5 * invSigmaSq);
124 | let cdf1 = Math.tanh(theta1 * 0.5 * invSigmaSq);
125 |
126 | let thetaM = 2 * sigmaSq * Math.atanh(cdf0 + (cdf1 - cdf0) * xi);
127 |
128 |
129 | let m = vec2.fromValues(Math.sin(thetaM), Math.cos(thetaM));
130 |
131 | let wiDotM = vec2.dot(w_o, m);
132 | let cosThetaT;
133 | let etaM = wiDotM < 0 ? ior : 1 / ior;
134 |
135 |
136 | let F = 0;
137 | let cosThetaI = Math.abs(wiDotM);
138 | let sinThetaTSq = etaM * etaM * (1 - cosThetaI * cosThetaI);
139 | if (sinThetaTSq > 1) {
140 | cosThetaT = 0;
141 | F = 1;
142 | } else {
143 | cosThetaT = Math.sqrt(1 - sinThetaTSq);
144 | let Rs = (etaM * cosThetaI - cosThetaT) / (etaM * cosThetaI + cosThetaT);
145 | let Rp = (etaM * cosThetaT - cosThetaI) / (etaM * cosThetaT + cosThetaI);
146 | F = (Rs * Rs + Rp * Rp) * 0.5;
147 | }
148 |
149 | F += 1 - this.transmittance;
150 |
151 | if (wiDotM < 0) cosThetaT = -cosThetaT;
152 |
153 | let refracted = false;
154 | if (Math.random() < F) {
155 | // reflection
156 | m[0] = 2 * wiDotM * m[0] - w_o[0];
157 | m[1] = 2 * wiDotM * m[1] - w_o[1];
158 | } else {
159 | // refraction
160 | m[0] = (etaM * wiDotM - cosThetaT) * m[0] - etaM * w_o[0];
161 | m[1] = (etaM * wiDotM - cosThetaT) * m[1] - etaM * w_o[1];
162 | refracted = true;
163 | }
164 |
165 | vec2.transformMat2(m, m, mat);
166 |
167 | vec2.copy(newDirection, m);
168 | vec2.normalize(newDirection, newDirection);
169 | // evaluate BRDF - END
170 |
171 |
172 |
173 |
174 |
175 |
176 |
177 |
178 |
179 | let newOrigin = vec2.create();
180 |
181 | if(!refracted) t = t - Globals.epsilon;
182 | else t = t + Globals.epsilon; // refracted rays needs to "pass through"
183 |
184 | vec2.scaleAndAdd(newOrigin, ray.o, ray.d, t);
185 |
186 | vec2.copy(ray.o, newOrigin);
187 | vec2.copy(ray.d, newDirection);
188 |
189 |
190 |
191 |
192 |
193 |
194 |
195 |
196 | // compute volume absorption
197 | if(this.hasVolumeAbsorption) {
198 | if(dot > 0.0 && !refracted) {
199 | // keep absorption as is
200 | } else if (dot > 0.0 && refracted) {
201 | contribution.var -= this.volumeAbsorption[0];
202 | contribution.vag -= this.volumeAbsorption[1];
203 | contribution.vab -= this.volumeAbsorption[2];
204 |
205 | if(contribution.var < 0) contribution.var = 0;
206 | if(contribution.vag < 0) contribution.vag = 0;
207 | if(contribution.vab < 0) contribution.vab = 0;
208 | } else if (dot < 0.0 && refracted) {
209 | contribution.var += this.volumeAbsorption[0];
210 | contribution.vag += this.volumeAbsorption[1];
211 | contribution.vab += this.volumeAbsorption[2];
212 | } else if (dot < 0.0 && !refracted) {
213 | // keep absorption as is
214 | }
215 | }
216 | }
217 |
218 |
219 | getIOR(wavelength) {
220 | let iorType = 0;
221 | if(this.b1 !== undefined) iorType = 1; // if Sellmeier's coefficients are provided, use those
222 |
223 |
224 | if (iorType === 0) {
225 | let t = ((wavelength - 380) / 360) * 2 - 1;
226 | return (this.ior + t * this.dispersion);
227 |
228 | } else if (iorType === 1) {
229 | let B1 = this.b1;
230 | let B2 = this.b2;
231 | let B3 = this.b3;
232 | let C1 = this.c1;
233 | let C2 = this.c2;
234 | let C3 = this.c3;
235 | let w2 = (wavelength*0.001) * (wavelength*0.001);
236 |
237 | let res = Math.sqrt(1 + (B1 * w2) / (w2 - C1) + (B2 * w2) / (w2 - C2) + (B3 * w2) / (w2 - C3));
238 | return res / this.d;
239 | }
240 | }
241 | }
242 |
243 | export { DielectricMaterial }
--------------------------------------------------------------------------------
/libs/material/experimentalDielectric.js:
--------------------------------------------------------------------------------
1 | import { Material } from "./material.js";
2 | import { glMatrix, vec2, mat2 } from "../dependencies/gl-matrix-es6.js";
3 | import { Globals } from "../globals.js";
4 |
5 | class ExperimentalDielectricMaterial extends Material {
6 | constructor(options) {
7 | super(options);
8 |
9 | if(!options) options = { };
10 |
11 | this.roughness = options.roughness !== undefined ? options.roughness : 0.15;
12 | this.ior = options.ior !== undefined ? options.ior : 1.4;
13 | this.transmittance = options.transmittance !== undefined ? options.transmittance : 1;
14 | this.dispersion = options.dispersion !== undefined ? options.dispersion : 0;
15 | this.absorption = options.absorption !== undefined ? options.absorption : 0.1;
16 | this.volumeAbsorption = options.volumeAbsorption !== undefined ? options.volumeAbsorption : 0;
17 | this.reflectionStrenght = options.reflectionStrenght !== undefined ? options.reflectionStrenght : 1;
18 | this.refractionStrenght = options.refractionStrenght !== undefined ? options.refractionStrenght : 1;
19 |
20 | this.hasVolumeAbsorption = (this.volumeAbsorption[0] + this.volumeAbsorption[1] + this.volumeAbsorption[2]) > 0;
21 | }
22 |
23 | setSellmierCoefficients(b1, b2, b3, c1, c2, c3, d) {
24 |
25 | /*
26 | B1 = 12 * 1.03961212;
27 | B2 = 12 * 0.231792344;
28 | B3 = 12 * 1.01046945;
29 | C1 = 12 * 0.00600069867;
30 | C2 = 12 * 0.0200179144;
31 | C3 = 12 * 103.560653;
32 | */
33 |
34 | this.b1 = b1;
35 | this.b2 = b2;
36 | this.b3 = b3;
37 |
38 | this.c1 = c1;
39 | this.c2 = c2;
40 | this.c3 = c3;
41 |
42 | this.d = d;
43 | }
44 |
45 | computeScattering(ray, input_normal, t, contribution, worldAttenuation, wavelength) {
46 |
47 |
48 | // opacity test, if it passes we're going to let the ray pass through the object
49 | if(this.opacityTest(t, worldAttenuation, ray, contribution)) {
50 | return;
51 | }
52 |
53 |
54 |
55 | let dot = vec2.dot(ray.d, input_normal);
56 |
57 | let normal = vec2.clone(input_normal);
58 | if(dot > 0.0) {
59 | vec2.negate(normal, normal);
60 | }
61 |
62 |
63 | // dot = Math.abs( vec2.dot(ray.d, input_normal) );
64 | // contribution *= dot;
65 |
66 |
67 |
68 |
69 | /* a dielectric material in Lumen2D "should" specify an absorption value:
70 |
71 | imagine two mirror objects reflecting the same beam in the same direction
72 | with an infinite number of bounces.
73 |
74 | | |
75 | | --> x <-- |
76 | | |
77 |
78 | what's the "fluency" at point x ?
79 | It would be "infinite"! if we don't use an absorption coefficient,
80 | and we set a very high light-bounce limit (e.g. 400) the light that enters a dielectric poligon
81 | would bounce around it (thanks to fresnel reflection) a lot of times thus increasing its
82 | "brightness" on screen, and that could make the dielectric shape look brighter than its lightsource
83 | */
84 |
85 | let absorption = (1 - this.absorption);
86 | let wa = (Math.exp(-t * worldAttenuation)) * absorption;
87 | contribution.r *= wa;
88 | contribution.g *= wa;
89 | contribution.b *= wa;
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 | let newDirection = vec2.create();
99 |
100 | // evaluate BRDF - from Tantalum
101 | let w_o = vec2.fromValues(-ray.d[0], -ray.d[1]);
102 |
103 |
104 | // normal rotation matrix
105 | let mat = mat2.fromValues(input_normal[1], -input_normal[0], input_normal[0], input_normal[1]);
106 | let imat = mat2.create();
107 | mat2.invert(imat, mat);
108 | vec2.transformMat2(w_o, w_o, imat);
109 |
110 |
111 |
112 | let sigma = this.roughness;
113 | let ior = wavelength !== undefined ? this.getIOR(wavelength) : this.ior;
114 |
115 | let PI_HALF = Math.PI * 0.5;
116 | let theta = Math.asin(Math.min(Math.abs(w_o[0]), 1.0));
117 | let theta0 = Math.max(theta - PI_HALF, -PI_HALF);
118 | let theta1 = Math.min(theta + PI_HALF, PI_HALF);
119 |
120 |
121 | let xi = Math.random();
122 | let sigmaSq = sigma*sigma;
123 | let invSigmaSq = 1 / sigmaSq;
124 |
125 | let cdf0 = Math.tanh(theta0 * 0.5 * invSigmaSq);
126 | let cdf1 = Math.tanh(theta1 * 0.5 * invSigmaSq);
127 |
128 | let thetaM = 2 * sigmaSq * Math.atanh(cdf0 + (cdf1 - cdf0) * xi);
129 |
130 |
131 | let m = vec2.fromValues(Math.sin(thetaM), Math.cos(thetaM));
132 |
133 | let wiDotM = vec2.dot(w_o, m);
134 | let cosThetaT;
135 | let etaM = wiDotM < 0 ? ior : 1 / ior;
136 |
137 |
138 | let F = 0;
139 | let cosThetaI = Math.abs(wiDotM);
140 | let sinThetaTSq = etaM * etaM * (1 - cosThetaI * cosThetaI);
141 | if (sinThetaTSq > 1) {
142 | cosThetaT = 0;
143 | F = 1;
144 | } else {
145 | cosThetaT = Math.sqrt(1 - sinThetaTSq);
146 | let Rs = (etaM * cosThetaI - cosThetaT) / (etaM * cosThetaI + cosThetaT);
147 | let Rp = (etaM * cosThetaT - cosThetaI) / (etaM * cosThetaT + cosThetaI);
148 | F = (Rs * Rs + Rp * Rp) * 0.5;
149 | }
150 |
151 | F += 1 - this.transmittance;
152 |
153 | if (wiDotM < 0) cosThetaT = -cosThetaT;
154 |
155 | let refracted = false;
156 | if (Math.random() < F) {
157 | // reflection
158 | m[0] = 2 * wiDotM * m[0] - w_o[0];
159 | m[1] = 2 * wiDotM * m[1] - w_o[1];
160 |
161 | contribution.r *= this.reflectionStrenght;
162 | contribution.g *= this.reflectionStrenght;
163 | contribution.b *= this.reflectionStrenght;
164 | } else {
165 | // refraction
166 | m[0] = (etaM * wiDotM - cosThetaT) * m[0] - etaM * w_o[0];
167 | m[1] = (etaM * wiDotM - cosThetaT) * m[1] - etaM * w_o[1];
168 | refracted = true;
169 |
170 | contribution.r *= this.refractionStrenght;
171 | contribution.g *= this.refractionStrenght;
172 | contribution.b *= this.refractionStrenght;
173 | }
174 |
175 | vec2.transformMat2(m, m, mat);
176 |
177 | vec2.copy(newDirection, m);
178 | vec2.normalize(newDirection, newDirection);
179 | // evaluate BRDF - END
180 |
181 |
182 |
183 |
184 |
185 |
186 |
187 |
188 |
189 | let newOrigin = vec2.create();
190 |
191 | if(!refracted) t = t - Globals.epsilon;
192 | else t = t + Globals.epsilon; // refracted rays needs to "pass through"
193 |
194 | vec2.scaleAndAdd(newOrigin, ray.o, ray.d, t);
195 |
196 | vec2.copy(ray.o, newOrigin);
197 | vec2.copy(ray.d, newDirection);
198 |
199 |
200 |
201 |
202 |
203 |
204 |
205 |
206 | // compute volume absorption
207 | if(this.hasVolumeAbsorption) {
208 | if(dot > 0.0 && !refracted) {
209 | // keep absorption as is
210 | } else if (dot > 0.0 && refracted) {
211 | contribution.var -= this.volumeAbsorption[0];
212 | contribution.vag -= this.volumeAbsorption[1];
213 | contribution.vab -= this.volumeAbsorption[2];
214 |
215 | if(contribution.var < 0) contribution.var = 0;
216 | if(contribution.vag < 0) contribution.vag = 0;
217 | if(contribution.vab < 0) contribution.vab = 0;
218 | } else if (dot < 0.0 && refracted) {
219 | contribution.var += this.volumeAbsorption[0];
220 | contribution.vag += this.volumeAbsorption[1];
221 | contribution.vab += this.volumeAbsorption[2];
222 | } else if (dot < 0.0 && !refracted) {
223 | // keep absorption as is
224 | }
225 | }
226 | }
227 |
228 |
229 | getIOR(wavelength) {
230 | let iorType = 0;
231 | if(this.b1 !== undefined) iorType = 1; // if Sellmeier's coefficients are provided, use those
232 |
233 |
234 | if (iorType === 0) {
235 | let t = ((wavelength - 380) / 360) * 2 - 1;
236 | return (this.ior + t * this.dispersion);
237 |
238 | } else if (iorType === 1) {
239 | let B1 = this.b1;
240 | let B2 = this.b2;
241 | let B3 = this.b3;
242 | let C1 = this.c1;
243 | let C2 = this.c2;
244 | let C3 = this.c3;
245 | let w2 = (wavelength*0.001) * (wavelength*0.001);
246 |
247 | let res = Math.sqrt(1 + (B1 * w2) / (w2 - C1) + (B2 * w2) / (w2 - C2) + (B3 * w2) / (w2 - C3));
248 | return res / this.d;
249 | }
250 | }
251 | }
252 |
253 | export { ExperimentalDielectricMaterial }
--------------------------------------------------------------------------------
/libs/bvh.js:
--------------------------------------------------------------------------------
1 | import { glMatrix, vec2 } from "./dependencies/gl-matrix-es6.js";
2 | import { AABB } from "./geometry/AABB.js";
3 |
4 | class BVH {
5 | constructor(objects, args) {
6 | let stats = {
7 | degenerateNodes: 0,
8 | maxLevel: 0,
9 | splitOnX: 0,
10 | splitOnY: 0,
11 | intersectionTests: 0,
12 | intersectionCalls: 0,
13 | averageIntersectionCalls: 0,
14 | }
15 |
16 | // create a BVHNode root and add it to the queue
17 | this.stats = stats;
18 | this.root = new BVHNode();
19 | this.root.leaf = false;
20 | this.root.level = 0;
21 | this.root.objectsIndices = [];
22 |
23 | for(let i = 0; i < objects.length; i++) {
24 | this.root.objectsIndices.push(i);
25 | }
26 |
27 | let queue = [];
28 | queue.push(this.root);
29 |
30 | // until queue is not empty
31 | while ( queue.length > 0 ) {
32 | // remove the first node from the queue
33 | let node = queue.shift();
34 |
35 | // calculate a possible splitting axis / point
36 | let nodeAABB = new AABB();
37 | let nodeCentersAABB = new AABB(); // used to decide the splitting axis
38 | for(let i = 0; i < node.objectsIndices.length; i++) {
39 | let objectIndex = node.objectsIndices[i];
40 | let object = objects[objectIndex];
41 |
42 | nodeAABB.addAABB(object.aabb);
43 | nodeCentersAABB.addVertex(object.center);
44 | }
45 | node.aabb = nodeAABB;
46 |
47 | // if the node has two children, it becomes a leaf
48 | if ( node.objectsIndices.length <= 2 ) {
49 | node.leaf = true;
50 |
51 | let object1, object2;
52 | if(node.objectsIndices[0] !== undefined) object1 = objects[node.objectsIndices[0]];
53 | if(node.objectsIndices[1] !== undefined) object2 = objects[node.objectsIndices[1]];
54 |
55 | if(object1) node.child1 = object1;
56 | if(object2) node.child2 = object2;
57 | } // otherwise we need to compute its children
58 | else {
59 | // choose a splitting axis
60 | let xAxisLength = Math.abs(nodeCentersAABB.min[0] - nodeCentersAABB.max[0]);
61 | let xAxisCenter = (nodeCentersAABB.min[0] + nodeCentersAABB.max[0]) / 2;
62 | let yAxisLength = Math.abs(nodeCentersAABB.min[1] - nodeCentersAABB.max[1]);
63 | let yAxisCenter = (nodeCentersAABB.min[1] + nodeCentersAABB.max[1]) / 2;
64 | let splitOnX = xAxisLength > yAxisLength ? true : false;
65 | let splitCenter = xAxisLength > yAxisLength ? xAxisCenter : yAxisCenter;
66 |
67 | // create child nodes
68 | let node1 = new BVHNode();
69 | let node2 = new BVHNode();
70 | node1.leaf = false;
71 | node2.leaf = false;
72 | node1.level = node.level + 1;
73 | node2.level = node.level + 1;
74 | node1.parent = node;
75 | node2.parent = node;
76 |
77 | node.child1 = node1;
78 | node.child2 = node2;
79 |
80 |
81 | // stats collection
82 | stats.maxLevel = Math.max(node1.level, stats.maxLevel);
83 | if(splitOnX) stats.splitOnX++;
84 | else stats.splitOnY++;
85 | // stats collection - END
86 |
87 |
88 | node1.objectsIndices = [];
89 | node2.objectsIndices = [];
90 | let axisIndex = splitOnX ? 0 : 1;
91 | for (let i = 0; i < node.objectsIndices.length; i++) {
92 | let objectIndex = node.objectsIndices[i];
93 | let object = objects[objectIndex];
94 |
95 | if (object.center[axisIndex] > splitCenter) {
96 | node2.objectsIndices.push(objectIndex);
97 | } else {
98 | node1.objectsIndices.push(objectIndex);
99 | }
100 | }
101 |
102 | // make sure both children have at least one element (might happen that one of the two arrays is empty if all objects share the same center)
103 | // if that doesn't happen, partition elements in both nodes
104 | if(node1.objectsIndices.length === 0) {
105 | // m will be at least 1. if we got in here, node2.objectsIndices.length is at least 3
106 | let m = Math.floor(node2.objectsIndices.length / 2);
107 | for (let i = 0; i < m; i++) {
108 | node1.objectsIndices.push(node2.objectsIndices.shift());
109 | }
110 | stats.degenerateNodes++;
111 | }
112 | if(node2.objectsIndices.length === 0) {
113 | // m will be at least 1. if we got in here, node1.objectsIndices.length is at least 3
114 | let m = Math.floor(node1.objectsIndices.length / 2);
115 | for (let i = 0; i < m; i++) {
116 | node2.objectsIndices.push(node1.objectsIndices.shift());
117 | }
118 | stats.degenerateNodes++;
119 | }
120 |
121 | // push to queue
122 | queue.push(node1);
123 | queue.push(node2);
124 | }
125 |
126 | // reset the "temporary children" array of the node since we don't need it anymore
127 | node.objectsIndices = [];
128 | }
129 |
130 |
131 | if(args.showDebug) {
132 | console.log("--- BVH stats ---");
133 | console.log("max depth: " + stats.maxLevel);
134 | console.log("splits on x axis: " + stats.splitOnX);
135 | console.log("splits on y axis: " + stats.splitOnY);
136 | console.log("degenerate nodes: " + stats.degenerateNodes);
137 | console.log("--- END ---");
138 | }
139 | }
140 |
141 | intersect(ray) {
142 |
143 | this.stats.intersectionTests++;
144 |
145 | let mint = Infinity;
146 | let closestObject;
147 | let closestResult;
148 |
149 | let toVisit = [this.root];
150 |
151 | while (toVisit.length !== 0) {
152 | let node = toVisit.pop();
153 | this.stats.intersectionCalls++;
154 |
155 |
156 | if (!node.leaf) {
157 | let res1 = false;
158 | let res2 = false;
159 | if(node.child1) res1 = node.child1.aabb.intersect(ray);
160 | if(node.child2) res2 = node.child2.aabb.intersect(ray);
161 |
162 | // USE FALSE AND NOT THE CONTRACTED IF FORM -- ZERO WOULD TEST AS FALSE!! (and we might need to check for a value of 0 instead of false)
163 | if(res1 === false && res2 === false) {
164 | continue;
165 | } else if (res1 !== false && res2 === false && (res1.t < mint)) {
166 | toVisit.push(node.child1);
167 | } else if (res1 === false && res2 !== false && (res2.t < mint)) {
168 | toVisit.push(node.child2);
169 | } else {
170 | if (res1.t < res2.t) {
171 | if (res2.t < mint) toVisit.push(node.child2);
172 | if (res1.t < mint) toVisit.push(node.child1);
173 | } else {
174 | if (res1.t < mint) toVisit.push(node.child1);
175 | if (res2.t < mint) toVisit.push(node.child2);
176 | }
177 | }
178 | }
179 |
180 | if (node.leaf) {
181 | let res1 = false;
182 | let res2 = false;
183 |
184 | // here we don't test the intersection with the aabb, but with the object itself !
185 | if(node.child1) res1 = node.child1.intersect(ray);
186 | if(node.child2) res2 = node.child2.intersect(ray);
187 |
188 | if(res1 !== false && res1.t < mint) {
189 | mint = res1.t;
190 | closestObject = node.child1;
191 | closestResult = res1;
192 | }
193 | if(res2 !== false && res2.t < mint) {
194 | mint = res2.t;
195 | closestObject = node.child2;
196 | closestResult = res2;
197 | }
198 | }
199 | }
200 |
201 | this.stats.averageIntersectionCalls = this.stats.intersectionCalls / this.stats.intersectionTests;
202 |
203 | if(closestResult)
204 | return { t: closestResult.t, normal: closestResult.normal, object: closestObject };
205 | else
206 | return false;
207 | }
208 | }
209 |
210 | class BVHNode {
211 | constructor() {
212 | this.parent;
213 | this.leaf;
214 | this.level;
215 |
216 | this.objectsIndices;
217 |
218 | this.child1;
219 | this.child2;
220 |
221 | this.aabb;
222 | }
223 | }
224 |
225 |
226 | export { BVH }
--------------------------------------------------------------------------------
/libs/worker.js:
--------------------------------------------------------------------------------
1 | import { Scene } from "./scene.js";
2 | import { glMatrix, vec2 } from "./dependencies/gl-matrix-es6.js";
3 | import { createScene } from "./createScene.js";
4 | import { CanvasBoundsIntersection } from "./geometry/CanvasBoundsIntersection.js";
5 |
6 | var canvasSize;
7 | var scene;
8 | var sceneArgs;
9 | var coloredPixels = 0;
10 | var sharedArray;
11 | var sharedInfoArray;
12 |
13 | var workerDataReference;
14 | var workerIndex;
15 |
16 | var WORLD_SIZE = {
17 | w: 0,
18 | h: 0,
19 | };
20 | var LIGHT_BOUNCES;
21 | var canvasBoundsIntersector;
22 | var canvasIntersectorResult = {
23 | tmin: Infinity,
24 | tmax: Infinity,
25 | }
26 |
27 | var USE_STRATIFIED_SAMPLING;
28 | var SAMPLING_RATIO_PER_PIXEL_COVERED;
29 |
30 | var Globals;
31 | var motionBlurPhotonsCount = 0;
32 |
33 | var offcanvas;
34 | var offscreenCanvasCtx;
35 | var offscreenCanvasPixels;
36 | var offscreenPixelNormalizationFactor = 1 / 255;
37 |
38 | var stopRendering = false;
39 |
40 | var currentVideoFrame = 0;
41 |
42 | onmessage = e => {
43 |
44 | if(e.data.messageType == "start") {
45 | // passing globals from main.js since they could change while the app is running
46 | Globals = e.data.Globals;
47 |
48 | canvasSize = Globals.canvasSize;
49 |
50 | sharedArray = new Float32Array(e.data.sharedBuffer);
51 | sharedInfoArray = new Float32Array(e.data.sharedInfoBuffer);
52 |
53 | if(Globals.highPrecision)
54 | glMatrix.setMatrixArrayType(Float64Array);
55 |
56 |
57 | WORLD_SIZE.h = Globals.WORLD_SIZE;
58 | WORLD_SIZE.w = Globals.WORLD_SIZE * (canvasSize.width / canvasSize.height);
59 | LIGHT_BOUNCES = Globals.LIGHT_BOUNCES;
60 | USE_STRATIFIED_SAMPLING = Globals.USE_STRATIFIED_SAMPLING;
61 | SAMPLING_RATIO_PER_PIXEL_COVERED = Globals.samplingRatioPerPixelCovered;
62 |
63 | canvasBoundsIntersector = new CanvasBoundsIntersection(WORLD_SIZE.w, WORLD_SIZE.h);
64 |
65 | workerDataReference = e.data;
66 | workerIndex = e.data.workerIndex;
67 |
68 |
69 | // we need to save a reference of sceneArgs globally since it will be used again inside the renderSample function
70 | sceneArgs = {
71 | showBVHdebug: workerIndex === 0 ? true : false,
72 | };
73 | scene = new Scene(sceneArgs);
74 |
75 |
76 | initOffscreenCanvas();
77 | createScene(scene, e.data, Math.random(), offscreenCanvasCtx, currentVideoFrame);
78 | // get data written to canvas
79 | offscreenCanvasPixels = offscreenCanvasCtx.getImageData(0, 0, offcanvas.width, offcanvas.height).data;
80 |
81 | requestAnimationFrame(renderSample);
82 | }
83 |
84 | if(e.data.messageType == "compute-next-video-frame") {
85 | currentVideoFrame = e.data.frameNumber;
86 |
87 | updateScene(Math.random());
88 | motionBlurPhotonsCount = 0;
89 |
90 | stopRendering = false;
91 |
92 | requestAnimationFrame(renderSample);
93 | }
94 |
95 | if(e.data.messageType == "stop-rendering") {
96 | stopRendering = true;
97 | }
98 |
99 | if(e.data.messageType == "Globals-update") {
100 | Globals = e.data.Globals;
101 | }
102 | };
103 |
104 |
105 | function initOffscreenCanvas() {
106 | offcanvas = new OffscreenCanvas(Globals.canvasSize.width, Globals.canvasSize.height);
107 | offscreenCanvasCtx = offcanvas.getContext("2d");
108 |
109 | let verticalScale = Globals.canvasSize.height / WORLD_SIZE.h;
110 |
111 | offscreenCanvasCtx.translate(Globals.canvasSize.width / 2, Globals.canvasSize.height / 2);
112 | offscreenCanvasCtx.scale(verticalScale, verticalScale);
113 |
114 | clearCanvas();
115 | offscreenCanvasCtx.save();
116 | }
117 |
118 | function clearCanvas() {
119 | offscreenCanvasCtx.fillStyle = "rgb(128,128,128)";
120 | offscreenCanvasCtx.fillRect(
121 | -WORLD_SIZE.w / 2 - 1, // -1 and +2 are added to make sure the edges are fully covered
122 | -WORLD_SIZE.h / 2 - 1,
123 | +WORLD_SIZE.w + 2,
124 | +WORLD_SIZE.h + 2);
125 | }
126 |
127 | function resetCanvasState() {
128 | clearCanvas();
129 | offscreenCanvasCtx.restore();
130 | }
131 |
132 | function updateScene(motionBlurT) {
133 | sceneArgs.showBVHdebug = false;
134 | scene.args = sceneArgs;
135 | scene.reset();
136 | resetCanvasState();
137 | createScene(scene, workerDataReference, motionBlurT, offscreenCanvasCtx, currentVideoFrame);
138 | offscreenCanvasPixels = offscreenCanvasCtx.getImageData(0, 0, offcanvas.width, offcanvas.height).data;
139 | }
140 |
141 | function renderSample() {
142 | if(!stopRendering) {
143 | requestAnimationFrame(renderSample);
144 | } else {
145 | postMessage({ messageType: "stop-render-acknowledge" });
146 | return;
147 | }
148 |
149 |
150 | coloredPixels = 0;
151 | let photonCount = Globals.PHOTONS_PER_UPDATE;
152 |
153 |
154 | for(let i = 0; i < photonCount; i++) {
155 | emitPhoton();
156 | // increase the counter of photons fired for this webworker
157 | sharedInfoArray[workerIndex] += 1;
158 |
159 |
160 | // Motion blur logic
161 | motionBlurPhotonsCount += 1;
162 | if(Globals.motionBlur && (motionBlurPhotonsCount >= Globals.motionBlurFramePhotons)) {
163 | updateScene(Math.random());
164 | motionBlurPhotonsCount = 0;
165 | }
166 | }
167 |
168 | postMessage({
169 | messageType: "photons-fired-update",
170 | photonsFired: Globals.PHOTONS_PER_UPDATE,
171 | coloredPixels: coloredPixels,
172 | });
173 | }
174 |
175 | function colorPhoton(ray, t, emitterColor, contribution, worldAttenuation) {
176 | let worldPixelSize = WORLD_SIZE.h / canvasSize.height;
177 | let step = worldPixelSize;
178 |
179 |
180 | let worldPoint = vec2.create();
181 | let previousPixel = [-1, -1];
182 |
183 | let volumeAbsorption = contribution.var + contribution.vag + contribution.vab;
184 |
185 |
186 |
187 |
188 | canvasIntersectorResult.tmin = Infinity;
189 | canvasIntersectorResult.tmax = Infinity;
190 | let rayIntersectsVisibleCanvas = canvasBoundsIntersector.intersect(ray, canvasIntersectorResult);
191 | let tstart = Infinity;
192 | let tend = Infinity;
193 | if(rayIntersectsVisibleCanvas) {
194 | if(canvasIntersectorResult.tmin < 0) tstart = 0; // tstart coincide with the origin of the ray
195 | else tstart = canvasIntersectorResult.tmin;
196 |
197 | // canvasIntersectorResult.tmax represents the intersection with the visible canvas bounds,
198 | // which can be greater than the actual end of this ray (e.g. if the ray hits an object inside the visible scene)
199 | tend = Math.min(canvasIntersectorResult.tmax, t);
200 | }
201 |
202 |
203 | // we can't use "steps" as a base value for a random sampling strategy, because we're sampling in a "continuous" domain
204 | // e.g.: if t / step ends up being 2.5, 'steps' will be set to 2, and assume we choose to compute only 1 sample, (since remember that RENDER_TYPE_NOISE
205 | // only chooses to compute a subset of the total amount of pixels touched by a light ray) then SAMPLES_STRENGHT would hold (steps / SAMPLES) == 2,
206 | // but the "real" sample_strenght should be 2.5
207 | let continuousSteps = (tend - tstart) / step;
208 |
209 | // we need to take less samples if the ray is short (proportionally) - otherwise we would increase radiance along short rays in an unproportional way
210 | // because we would add more emitterColor along those smaller rays
211 | let SAMPLES = Math.max( Math.floor(continuousSteps * SAMPLING_RATIO_PER_PIXEL_COVERED), 1 );
212 | let SAMPLES_STRENGHT = continuousSteps / SAMPLES; // e.g. if the line touches 30 pixels, but instead we're just
213 | // coloring two, then these two pixels need 15x times the amount of radiance
214 |
215 | // if this ray doesn't intersects the visible canvas, don't compute any sample and skip this for loop entirely
216 | // note that we might still want to compute volume absorption so we can't simply 'return;' here
217 | if (!rayIntersectsVisibleCanvas) SAMPLES = 0;
218 |
219 | let sample_step = (tend - tstart) / SAMPLES;
220 | for(let i = 0; i < SAMPLES; i++) {
221 |
222 | let tt = tstart;
223 | if(USE_STRATIFIED_SAMPLING) {
224 | tt += sample_step * i;
225 | tt += sample_step * Math.random();
226 | } else {
227 | tt += (tend - tstart) * Math.random();
228 | }
229 |
230 | vec2.scaleAndAdd(worldPoint, ray.o, ray.d, tt);
231 |
232 | // convert world point to pixel coordinate
233 | let u = (worldPoint[0] + WORLD_SIZE.w / 2) / WORLD_SIZE.w;
234 | let v = (worldPoint[1] + WORLD_SIZE.h / 2) / WORLD_SIZE.h;
235 |
236 | let px = Math.floor(u * canvasSize.width);
237 | let py = Math.floor(v * canvasSize.height);
238 |
239 | let attenuation = Math.exp(-tt * worldAttenuation);
240 |
241 | if(previousPixel[0] == px && previousPixel[1] == py || px >= canvasSize.width || py >= canvasSize.height || px < 0 || py < 0) {
242 | continue;
243 | } else {
244 | previousPixel[0] = px;
245 | previousPixel[1] = py;
246 |
247 | let index = (py * canvasSize.width + px) * 3;
248 | let cindex = (py * canvasSize.width + px) * 4;
249 |
250 |
251 | let ocr = 1;
252 | let ocg = 1;
253 | let ocb = 1;
254 |
255 | if(!Globals.deactivateOffscreenCanvas) {
256 | ocr = (offscreenCanvasPixels[cindex + 0] * offscreenPixelNormalizationFactor) * 2 - 1;
257 | ocg = (offscreenCanvasPixels[cindex + 1] * offscreenPixelNormalizationFactor) * 2 - 1;
258 | ocb = (offscreenCanvasPixels[cindex + 2] * offscreenPixelNormalizationFactor) * 2 - 1;
259 |
260 | // at this point ocr, ocg, ogb are in the range [-1 ... +1]
261 | // offscreenCanvasCPow decides how "strong" the drawing effect is,
262 | // by using an exponential function that increases the original -1 ... +1 range
263 | ocr = Math.exp(ocr * Globals.offscreenCanvasCPow);
264 | ocg = Math.exp(ocg * Globals.offscreenCanvasCPow);
265 | ocb = Math.exp(ocb * Globals.offscreenCanvasCPow);
266 | }
267 |
268 | if(volumeAbsorption > 0) {
269 | ocr *= Math.exp(-tt * contribution.var);
270 | ocg *= Math.exp(-tt * contribution.vag);
271 | ocb *= Math.exp(-tt * contribution.vab);
272 | }
273 |
274 | let prevR = sharedArray[index + 0];
275 | let prevG = sharedArray[index + 1];
276 | let prevB = sharedArray[index + 2];
277 |
278 | let ss = SAMPLES_STRENGHT * attenuation;
279 |
280 | sharedArray[index + 0] = prevR + emitterColor[0] * ss * contribution.r * ocr;
281 | sharedArray[index + 1] = prevG + emitterColor[1] * ss * contribution.g * ocg;
282 | sharedArray[index + 2] = prevB + emitterColor[2] * ss * contribution.b * ocb;
283 | }
284 | }
285 |
286 | // diminish the contribution of this light ray after it passed through the
287 | // medium
288 | if(volumeAbsorption > 0) {
289 | contribution.r *= Math.exp(-t * contribution.var);
290 | contribution.g *= Math.exp(-t * contribution.vag);
291 | contribution.b *= Math.exp(-t * contribution.vab);
292 | }
293 |
294 | coloredPixels += SAMPLES;
295 | }
296 |
297 | function getRGBfromWavelength(Wavelength) {
298 | let Gamma = 0.80;
299 | let IntensityMax = 255;
300 | let factor;
301 | let Red,Green,Blue;
302 |
303 | if ((Wavelength >= 380) && (Wavelength<440)) {
304 | Red = -(Wavelength - 440) / (440 - 380);
305 | Green = 0.0;
306 | Blue = 1.0;
307 | } else if ((Wavelength >= 440) && (Wavelength<490)) {
308 | Red = 0.0;
309 | Green = (Wavelength - 440) / (490 - 440);
310 | Blue = 1.0;
311 | } else if ((Wavelength >= 490) && (Wavelength<510)) {
312 | Red = 0.0;
313 | Green = 1.0;
314 | Blue = -(Wavelength - 510) / (510 - 490);
315 | } else if ((Wavelength >= 510) && (Wavelength<580)) {
316 | Red = (Wavelength - 510) / (580 - 510);
317 | Green = 1.0;
318 | Blue = 0.0;
319 | } else if ((Wavelength >= 580) && (Wavelength<645)) {
320 | Red = 1.0;
321 | Green = -(Wavelength - 645) / (645 - 580);
322 | Blue = 0.0;
323 | } else if ((Wavelength >= 645) && (Wavelength<781)) {
324 | Red = 1.0;
325 | Green = 0.0;
326 | Blue = 0.0;
327 | } else{
328 | Red = 0.0;
329 | Green = 0.0;
330 | Blue = 0.0;
331 | };
332 |
333 | // Let the intensity fall off near the vision limits
334 |
335 | if ((Wavelength >= 380) && (Wavelength<420)) {
336 | factor = 0.3 + 0.7*(Wavelength - 380) / (420 - 380);
337 | } else if ((Wavelength >= 420) && (Wavelength<701)) {
338 | factor = 1.0;
339 | } else if ((Wavelength >= 701) && (Wavelength<781)) {
340 | factor = 0.3 + 0.7*(780 - Wavelength) / (780 - 700);
341 | } else {
342 | factor = 0.0;
343 | };
344 |
345 |
346 | let rgb = [0,0,0];
347 |
348 | // Don't want 0^x = 1 for x <> 0
349 | rgb[0] = Red === 0 ? 0 : Math.floor( Math.round(IntensityMax * Math.pow(Red * factor, Gamma)) );
350 | rgb[1] = Green === 0 ? 0 : Math.floor( Math.round(IntensityMax * Math.pow(Green * factor, Gamma)) );
351 | rgb[2] = Blue === 0 ? 0 : Math.floor( Math.round(IntensityMax * Math.pow(Blue * factor, Gamma)) );
352 |
353 | return rgb;
354 | }
355 |
356 | function getColorFromEmitterSpectrum(spectrum) {
357 | let color;
358 |
359 | if(spectrum.wavelength) {
360 | color = getRGBfromWavelength(spectrum.wavelength);
361 |
362 | color[0] *= spectrum.intensity;
363 | color[1] *= spectrum.intensity;
364 | color[2] *= spectrum.intensity;
365 | } else {
366 | color = spectrum.color;
367 | }
368 |
369 | return color;
370 | }
371 |
372 | function emitPhoton() {
373 |
374 | let emitter = scene.getEmitter();
375 | let photon = emitter.material.getPhoton(emitter);
376 |
377 | let ray = photon.ray;
378 | let spectrum = photon.spectrum;
379 | let wavelength = spectrum.wavelength;
380 | let color = getColorFromEmitterSpectrum(spectrum);
381 |
382 | let contribution = {
383 | r: 1,
384 | g: 1,
385 | b: 1,
386 | var: 0,
387 | vag: 0,
388 | vab: 0,
389 | };
390 | let worldAttenuation = Globals.worldAttenuation;
391 |
392 |
393 | for(let i = 0; i < LIGHT_BOUNCES; i++) {
394 | let result = scene.intersect(ray);
395 |
396 | // if we had an intersection
397 | if(result.t) {
398 |
399 | let object = result.object;
400 | let material = object.material;
401 |
402 | if(i >= Globals.skipBounce)
403 | colorPhoton(ray, result.t, color, contribution, worldAttenuation);
404 |
405 | material.computeScattering(ray, result.normal, result.t, contribution, worldAttenuation, wavelength);
406 |
407 | if(contribution.r < 0) contribution.r = 0;
408 | if(contribution.g < 0) contribution.g = 0;
409 | if(contribution.b < 0) contribution.b = 0;
410 | }
411 | }
412 | }
--------------------------------------------------------------------------------
/libs/scenes/examples/circle-packing-scene.js:
--------------------------------------------------------------------------------
1 | import { Edge } from "./geometry/Edge.js";
2 | import { LambertMaterial } from "./material/lambert.js";
3 | import { LambertEmitterMaterial } from "./material/lambertEmitter.js";
4 | import { Circle } from "./geometry/Circle.js";
5 | import { Utils } from "./utils.js";
6 | import { DielectricMaterial } from "./material/dielectric.js";
7 | import { MicrofacetMaterial } from "./material/microfacet.js";
8 |
9 | /*
10 | Global settings used to render this scene
11 |
12 | var Globals = {
13 |
14 | // Sampling
15 | epsilon: 0.00005,
16 | highPrecision: false, // if set to true, uses Float64Arrays which are 2x slower to work with
17 | USE_STRATIFIED_SAMPLING: true,
18 | samplingRatioPerPixelCovered: 0.14,
19 | LIGHT_BOUNCES: 40,
20 | skipBounce: 1,
21 |
22 | // Threading
23 | workersCount: 5,
24 | PHOTONS_PER_UPDATE: 50000,
25 |
26 | // Environment
27 | WORLD_SIZE: 20,
28 | worldAttenuation: 0.01,
29 |
30 | // Video export
31 | registerVideo: false,
32 | photonsPerVideoFrame: 2000000,
33 | framesPerSecond: 60,
34 | framesCount: 180,
35 | frameStart: 0,
36 |
37 | // Motion blur
38 | motionBlur: false,
39 | motionBlurFramePhotons: 5000,
40 |
41 | // Offscreen canvas
42 | deactivateOffscreenCanvas: true, // setting it to false slows down render times by about 1.7x
43 | offscreenCanvasCPow: 1.1,
44 |
45 | // Canvas size
46 | canvasSize: {
47 | width: 900,
48 | height: 900,
49 | },
50 |
51 | // Reinhard tonemapping
52 | toneMapping: true,
53 | gamma: 2.2,
54 | exposure: 1,
55 | }
56 |
57 | */
58 |
59 | function createScene(scene, workerData, motionBlurT, ctx, frameNumber) {
60 |
61 | createWorldBounds(scene);
62 | Utils.setSeed("apples");
63 |
64 |
65 | for(let i = 0; i < sceneData.length; i++) {
66 | let cd = sceneData[i];
67 | cd.x = parseFloat(cd.x);
68 | cd.y = parseFloat(cd.y);
69 | cd.r = parseFloat(cd.r);
70 |
71 | if(Utils.rand() > 0.7) continue;
72 |
73 | let absorption = 0;
74 |
75 | let material = new DielectricMaterial({
76 | roughness: 0,
77 | ior: 1.35,
78 | transmittance: 1,
79 | dispersion: 0.1,
80 | absorption: absorption,
81 | volumeAbsorption: [
82 | 0, //Math.min(Math.max(cd.x + 287, 0) * 0.0005, 1.5),
83 | 0.15,
84 | 0.25],
85 | });
86 |
87 | if(Utils.rand() > 0.9) {
88 | material = new MicrofacetMaterial({
89 | opacity: 0.9,
90 | roughness: 0,
91 | });
92 | }
93 |
94 | let s = 0.02;
95 | let c = new Circle(cd.x * s, cd.y * s, cd.r * s);
96 |
97 | scene.add(c, material);
98 | }
99 |
100 | let yo = Math.cos(frameNumber / 45 * Math.PI * 2) * 1;
101 | let xo = Math.sin(frameNumber / 45 * Math.PI * 2) * 1;
102 | xo -= 6;
103 |
104 | let xs = 0.5; //2.5
105 | {
106 | let lightSource = new Edge(xs + xo, 12 + yo, -xs + xo, 12 + yo);
107 | let lightMaterial = new LambertEmitterMaterial({
108 | // we need to use a function as a color instead of the usual [1,1,1] array
109 | // because we've added the dispertion property to the circles materials
110 | color: function() {
111 | let w = Math.random() * 360 + 380;
112 | let it = 1;
113 | if(w < 450) it = 1.5;
114 |
115 | return {
116 | wavelength: w,
117 | intensity: 10 * it,
118 | }
119 | },
120 | });
121 |
122 | scene.add(lightSource, lightMaterial);
123 | }
124 | }
125 |
126 | function createWorldBounds(scene) {
127 | let edgeMaterial = new LambertMaterial({ opacity: 1 });
128 | let tbound = 11 * 10;
129 | let lbound = 19 * 10;
130 | let rbound = 19 * 10;
131 | let bbound = 11 * 10;
132 | let ledge = new Edge(-lbound, -bbound, -lbound, tbound, 0, 1, 0);
133 | let redge = new Edge( rbound, -bbound, rbound, tbound, 0, -1, 0);
134 | let tedge = new Edge(-lbound, tbound, rbound, tbound, 0, 0, -1);
135 | let bedge = new Edge(-lbound, -bbound, rbound, -bbound, 0, 0, 1);
136 |
137 | scene.add(ledge, edgeMaterial);
138 | scene.add(redge, edgeMaterial);
139 | scene.add(tedge, edgeMaterial);
140 | scene.add(bedge, edgeMaterial);
141 | }
142 |
143 | let sceneData = [{"x":"95.92","y":"104.16","r":"12.94"},{"x":"159.01","y":"136.85","r":"16.57"},{"x":"161.80","y":"169.90","r":"17.75"},{"x":"176.66","y":"110.01","r":"16.27"},{"x":"254.81","y":"166.83","r":"21.47"},{"x":"119.13","y":"116.32","r":"14.22"},{"x":"75.92","y":"184.40","r":"16.27"},{"x":"3.23","y":"108.13","r":"11.27"},{"x":"205.41","y":"23.46","r":"15.98"},{"x":"164.45","y":"247.42","r":"21.57"},{"x":"240.07","y":"239.80","r":"23.73"},{"x":"116.29","y":"22.51","r":"11.37"},{"x":"82.53","y":"126.38","r":"13.43"},{"x":"86.98","y":"42.78","r":"10.39"},{"x":"190.41","y":"81.65","r":"16.08"},{"x":"104.85","y":"171.38","r":"16.08"},{"x":"108.63","y":"82.85","r":"12.45"},{"x":"85.89","y":"8.81","r":"9.61"},{"x":"197.04","y":"175.67","r":"19.41"},{"x":"69.22","y":"217.20","r":"17.84"},{"x":"234.72","y":"9.37","r":"17.55"},{"x":"131.08","y":"225.81","r":"19.61"},{"x":"45.52","y":"193.87","r":"16.27"},{"x":"286.89","y":"92.88","r":"21.08"},{"x":"67.78","y":"43.20","r":"9.41"},{"x":"297.95","y":"166.72","r":"23.43"},{"x":"33.45","y":"145.52","r":"13.43"},{"x":"31.56","y":"86.59","r":"10.29"},{"x":"164.22","y":"206.30","r":"19.71"},{"x":"95.81","y":"24.98","r":"10.20"},{"x":"99.49","y":"-4.77","r":"10.20"},{"x":"76.71","y":"60.31","r":"10.49"},{"x":"98.01","y":"61.45","r":"11.47"},{"x":"96.29","y":"241.42","r":"19.51"},{"x":"64.65","y":"76.90","r":"10.59"},{"x":"192.93","y":"139.34","r":"17.94"},{"x":"229.66","y":"137.49","r":"19.51"},{"x":"53.15","y":"164.74","r":"14.80"},{"x":"145.64","y":"60.42","r":"13.63"},{"x":"73.40","y":"96.63","r":"11.76"},{"x":"138.53","y":"15.65","r":"12.45"},{"x":"209.83","y":"108.09","r":"17.75"},{"x":"200.71","y":"52.83","r":"16.08"},{"x":"273.12","y":"130.94","r":"21.27"},{"x":"258.30","y":"66.82","r":"19.22"},{"x":"108.26","y":"42.34","r":"11.37"},{"x":"300.03","y":"12.84","r":"20.98"},{"x":"156.38","y":"34.21","r":"13.53"},{"x":"198.94","y":"224.42","r":"21.57"},{"x":"120.92","y":"61.79","r":"12.35"},{"x":"246.31","y":"102.91","r":"19.22"},{"x":"172.18","y":"57.00","r":"14.71"},{"x":"264.66","y":"29.65","r":"19.22"},{"x":"223.16","y":"76.27","r":"17.65"},{"x":"51.03","y":"93.46","r":"11.18"},{"x":"132.22","y":"154.85","r":"16.37"},{"x":"160.81","y":"83.77","r":"14.80"},{"x":"231.66","y":"43.13","r":"17.65"},{"x":"105.69","y":"140.92","r":"14.71"},{"x":"119.06","y":"1.89","r":"11.37"},{"x":"57.28","y":"58.57","r":"9.61"},{"x":"132.52","y":"188.75","r":"17.75"},{"x":"133.32","y":"84.27","r":"13.63"},{"x":"229.49","y":"197.56","r":"21.57"},{"x":"16.72","y":"126.69","r":"12.25"},{"x":"177.10","y":"16.19","r":"14.51"},{"x":"101.71","y":"204.82","r":"17.75"},{"x":"86.12","y":"79.54","r":"11.57"},{"x":"131.55","y":"39.52","r":"12.45"},{"x":"146.17","y":"109.05","r":"15.10"},{"x":"-90.64","y":"-38.66","r":"9.90"},{"x":"-195.86","y":"-68.60","r":"15.69"},{"x":"-54.86","y":"125.13","r":"12.65"},{"x":"-138.70","y":"-74.02","r":"13.04"},{"x":"-147.04","y":"38.40","r":"12.94"},{"x":"18.62","y":"37.67","r":"7.45"},{"x":"-84.38","y":"95.61","r":"11.96"},{"x":"-90.83","y":"-75.03","r":"11.18"},{"x":"-97.51","y":"-20.05","r":"10.00"},{"x":"-245.36","y":"173.00","r":"21.24"},{"x":"-267.29","y":"138.14","r":"20.98"},{"x":"-107.79","y":"63.33","r":"11.57"},{"x":"-228.06","y":"-63.05","r":"17.16"},{"x":"-296.28","y":"52.44","r":"20.69"},{"x":"-287.64","y":"177.94","r":"23.04"},{"x":"-157.51","y":"-26.62","r":"13.24"},{"x":"-35.11","y":"71.39","r":"9.51"},{"x":"-149.72","y":"216.56","r":"19.51"},{"x":"-144.19","y":"14.15","r":"12.55"},{"x":"-89.42","y":"31.25","r":"9.90"},{"x":"-55.93","y":"-92.68","r":"10.59"},{"x":"-207.20","y":"166.86","r":"19.41"},{"x":"-116.12","y":"234.82","r":"19.61"},{"x":"-187.28","y":"232.53","r":"21.37"},{"x":"-342.41","y":"13.46","r":"22.84"},{"x":"-116.71","y":"198.05","r":"17.75"},{"x":"-127.37","y":"51.71","r":"12.06"},{"x":"-61.95","y":"-113.61","r":"11.96"},{"x":"-163.52","y":"-1.53","r":"13.43"},{"x":"5.18","y":"-36.30","r":"6.86"},{"x":"-128.50","y":"75.89","r":"12.84"},{"x":"-246.65","y":"103.84","r":"19.12"},{"x":"-6.67","y":"58.96","r":"8.43"},{"x":"-39.57","y":"-42.31","r":"7.75"},{"x":"-110.90","y":"-35.93","r":"10.98"},{"x":"-260.51","y":"214.25","r":"23.24"},{"x":"-212.41","y":"-9.93","r":"15.98"},{"x":"-79.03","y":"119.71","r":"12.94"},{"x":"-178.07","y":"104.27","r":"16.08"},{"x":"-371.95","y":"87.73","r":"25.00"},{"x":"-189.13","y":"8.34","r":"14.80"},{"x":"-45.29","y":"-57.12","r":"8.73"},{"x":"-105.65","y":"19.52","r":"10.49"},{"x":"-62.35","y":"-26.48","r":"8.24"},{"x":"-27.95","y":"89.51","r":"10.39"},{"x":"-6.67","y":"43.42","r":"7.55"},{"x":"-380.97","y":"38.83","r":"25.10"},{"x":"-124.64","y":"-53.97","r":"11.96"},{"x":"-36.06","y":"-88.08","r":"10.10"},{"x":"-299.84","y":"-44.31","r":"20.78"},{"x":"-40.52","y":"106.67","r":"11.47"},{"x":"-100.68","y":"-95.55","r":"12.16"},{"x":"-337.72","y":"56.32","r":"22.84"},{"x":"-146.72","y":"180.94","r":"17.75"},{"x":"-1.92","y":"-57.36","r":"7.94"},{"x":"-78.77","y":"231.04","r":"18.73"},{"x":"-192.27","y":"76.99","r":"15.98"},{"x":"-139.53","y":"-9.36","r":"12.06"},{"x":"-309.63","y":"139.05","r":"22.94"},{"x":"-61.83","y":"172.53","r":"15.20"},{"x":"-73.30","y":"41.11","r":"9.41"},{"x":"-32.98","y":"-29.69","r":"7.06"},{"x":"21.65","y":"-17.60","r":"6.47"},{"x":"-49.99","y":"-74.15","r":"9.61"},{"x":"-56.96","y":"49.06","r":"9.12"},{"x":"-39.94","y":"-108.34","r":"11.08"},{"x":"-40.16","y":"54.53","r":"8.73"},{"x":"-45.06","y":"149.53","r":"13.82"},{"x":"-205.55","y":"-39.82","r":"15.88"},{"x":"-47.08","y":"-28.65","r":"7.65"},{"x":"-144.57","y":"147.86","r":"16.27"},{"x":"40.41","y":"-4.56","r":"7.16"},{"x":"-47.96","y":"86.29","r":"10.49"},{"x":"-125.35","y":"28.35","r":"11.67"},{"x":"-215.59","y":"20.92","r":"16.18"},{"x":"-122.31","y":"5.98","r":"11.27"},{"x":"-71.37","y":"145.13","r":"14.12"},{"x":"-218.49","y":"204.81","r":"21.08"},{"x":"-89.94","y":"166.57","r":"15.39"},{"x":"-261.91","y":"69.61","r":"19.22"},{"x":"-152.43","y":"89.44","r":"14.41"},{"x":"-285.31","y":"101.20","r":"20.98"},{"x":"-123.47","y":"128.64","r":"14.71"},{"x":"-82.84","y":"196.78","r":"16.96"},{"x":"-236.96","y":"-30.60","r":"17.35"},{"x":"-226.41","y":"73.60","r":"17.45"},{"x":"-180.53","y":"194.05","r":"19.51"},{"x":"-270.21","y":"-18.51","r":"19.12"},{"x":"-205.08","y":"49.07","r":"16.08"},{"x":"-117.57","y":"-15.33","r":"10.98"},{"x":"-113.72","y":"-75.06","r":"12.06"},{"x":"-19.67","y":"-99.95","r":"10.39"},{"x":"-89.57","y":"51.69","r":"10.39"},{"x":"-84.25","y":"-6.25","r":"9.22"},{"x":"-107.39","y":"40.95","r":"10.98"},{"x":"-102.14","y":"-55.78","r":"10.98"},{"x":"-211.11","y":"104.17","r":"17.55"},{"x":"-305.33","y":"-3.95","r":"20.78"},{"x":"-67.58","y":"-10.80","r":"8.33"},{"x":"-150.58","y":"63.06","r":"13.63"},{"x":"-69.59","y":"-75.35","r":"10.39"},{"x":"-274.37","y":"20.08","r":"19.41"},{"x":"-133.17","y":"-32.02","r":"12.06"},{"x":"-77.46","y":"-94.98","r":"11.37"},{"x":"-118.72","y":"164.28","r":"16.18"},{"x":"-25.04","y":"-40.78","r":"7.35"},{"x":"-106.52","y":"86.80","r":"12.35"},{"x":"-29.23","y":"-54.34","r":"8.14"},{"x":"-127.68","y":"101.58","r":"13.82"},{"x":"-43.98","y":"38.92","r":"8.14"},{"x":"-79.12","y":"-23.72","r":"9.22"},{"x":"-71.58","y":"60.47","r":"10.10"},{"x":"-228.60","y":"136.32","r":"19.12"},{"x":"-173.20","y":"159.24","r":"17.75"},{"x":"-168.20","y":"25.03","r":"13.92"},{"x":"-32.30","y":"-70.20","r":"8.92"},{"x":"-149.26","y":"-50.84","r":"13.04"},{"x":"-183.97","y":"-19.46","r":"14.41"},{"x":"-62.48","y":"102.32","r":"11.57"},{"x":"-87.97","y":"73.08","r":"11.18"},{"x":"-53.31","y":"67.10","r":"9.71"},{"x":"-193.25","y":"132.93","r":"17.55"},{"x":"-52.41","y":"-14.47","r":"7.65"},{"x":"-13.49","y":"-48.55","r":"7.45"},{"x":"-55.21","y":"-41.87","r":"8.43"},{"x":"-242.52","y":"3.50","r":"17.55"},{"x":"-17.15","y":"73.44","r":"9.31"},{"x":"-67.92","y":"80.80","r":"10.78"},{"x":"-242.91","y":"39.08","r":"17.84"},{"x":"-102.44","y":"-0.73","r":"10.10"},{"x":"-81.69","y":"-56.46","r":"10.10"},{"x":"-102.94","y":"111.65","r":"13.33"},{"x":"-97.94","y":"138.70","r":"14.22"},{"x":"-72.11","y":"-40.43","r":"9.12"},{"x":"-176.27","y":"-46.26","r":"14.51"},{"x":"-175.42","y":"51.93","r":"14.61"},{"x":"-151.21","y":"118.36","r":"15.39"},{"x":"-87.64","y":"11.96","r":"9.51"},{"x":"-62.84","y":"-57.36","r":"9.41"},{"x":"-73.05","y":"22.77","r":"8.92"},{"x":"163.33","y":"-249.12","r":"21.18"},{"x":"189.42","y":"-153.43","r":"18.14"},{"x":"265.82","y":"-7.68","r":"19.12"},{"x":"18.63","y":"-121.43","r":"11.57"},{"x":"173.47","y":"-45.90","r":"14.51"},{"x":"207.30","y":"-316.75","r":"25.69"},{"x":"107.77","y":"-40.24","r":"10.98"},{"x":"205.82","y":"-6.37","r":"15.88"},{"x":"89.57","y":"-267.41","r":"20.49"},{"x":"129.30","y":"-272.32","r":"21.57"},{"x":"407.63","y":"-116.94","r":"27.35"},{"x":"28.69","y":"-70.09","r":"8.92"},{"x":"338.20","y":"-174.20","r":"25.29"},{"x":"208.60","y":"-185.74","r":"20.10"},{"x":"207.80","y":"-369.78","r":"28.24"},{"x":"254.38","y":"-339.12","r":"28.14"},{"x":"252.49","y":"-80.42","r":"19.02"},{"x":"44.65","y":"-155.76","r":"13.73"},{"x":"135.43","y":"-12.48","r":"12.06"},{"x":"357.05","y":"-128.82","r":"25.20"},{"x":"114.12","y":"-19.52","r":"11.18"},{"x":"-1.42","y":"-111.13","r":"11.08"},{"x":"333.35","y":"-263.41","r":"27.84"},{"x":"334.00","y":"-51.94","r":"22.92"},{"x":"146.95","y":"-53.16","r":"13.33"},{"x":"36.68","y":"-108.02","r":"11.08"},{"x":"248.92","y":"-192.67","r":"21.96"},{"x":"281.11","y":"-106.19","r":"20.98"},{"x":"20.35","y":"-145.14","r":"12.94"},{"x":"94.32","y":"-24.39","r":"10.10"},{"x":"226.83","y":"-151.61","r":"19.51"},{"x":"292.94","y":"-67.34","r":"20.88"},{"x":"21.80","y":"-171.64","r":"14.41"},{"x":"110.43","y":"-123.88","r":"14.02"},{"x":"166.74","y":"-293.16","r":"23.43"},{"x":"195.10","y":"-223.16","r":"20.98"},{"x":"48.53","y":"-184.01","r":"15.39"},{"x":"68.00","y":"-44.07","r":"9.31"},{"x":"232.80","y":"-24.25","r":"17.35"},{"x":"71.32","y":"-164.94","r":"14.61"},{"x":"216.93","y":"-89.77","r":"17.45"},{"x":"135.35","y":"-179.62","r":"17.16"},{"x":"157.03","y":"-1.55","r":"13.33"},{"x":"137.44","y":"-77.56","r":"13.33"},{"x":"32.69","y":"-87.93","r":"10.00"},{"x":"77.86","y":"-60.73","r":"10.29"},{"x":"246.34","y":"-286.51","r":"25.59"},{"x":"261.72","y":"-44.52","r":"19.02"},{"x":"129.97","y":"-35.45","r":"12.06"},{"x":"237.02","y":"-235.36","r":"23.04"},{"x":"100.66","y":"-172.01","r":"15.88"},{"x":"299.14","y":"-27.54","r":"20.88"},{"x":"122.60","y":"-150.22","r":"15.49"},{"x":"50.57","y":"-43.74","r":"8.53"},{"x":"389.67","y":"-168.66","r":"27.65"},{"x":"309.71","y":"-137.43","r":"23.04"},{"x":"110.38","y":"-202.55","r":"17.65"},{"x":"16.75","y":"-100.25","r":"10.39"},{"x":"139.65","y":"-125.55","r":"15.00"},{"x":"201.88","y":"-36.48","r":"15.88"},{"x":"163.76","y":"-341.67","r":"25.78"},{"x":"66.27","y":"-77.48","r":"10.49"},{"x":"296.06","y":"-303.20","r":"28.04"},{"x":"52.01","y":"-215.95","r":"17.16"},{"x":"194.50","y":"-66.59","r":"15.98"},{"x":"122.12","y":"-57.84","r":"12.16"},{"x":"82.84","y":"-119.62","r":"12.84"},{"x":"365.14","y":"-218.00","r":"27.75"},{"x":"77.90","y":"-194.95","r":"16.57"},{"x":"14.57","y":"-81.40","r":"9.31"},{"x":"324.62","y":"-95.39","r":"22.94"},{"x":"290.38","y":"-178.23","r":"23.33"},{"x":"313.80","y":"-216.80","r":"25.39"},{"x":"52.53","y":"-93.30","r":"10.69"},{"x":"98.89","y":"-100.41","r":"12.55"},{"x":"153.76","y":"-27.89","r":"13.24"},{"x":"119.78","y":"-236.01","r":"19.41"},{"x":"87.17","y":"-42.97","r":"10.20"},{"x":"58.35","y":"-59.68","r":"9.41"},{"x":"74.43","y":"-97.40","r":"11.67"},{"x":"203.44","y":"-266.15","r":"23.24"},{"x":"184.22","y":"-96.19","r":"16.08"},{"x":"40.69","y":"-57.47","r":"8.73"},{"x":"88.16","y":"-79.42","r":"11.37"},{"x":"170.96","y":"-125.01","r":"16.37"},{"x":"164.97","y":"-73.30","r":"14.51"},{"x":"64.91","y":"-138.36","r":"13.33"},{"x":"226.35","y":"-57.21","r":"17.35"},{"x":"40.64","y":"-130.53","r":"12.35"},{"x":"153.69","y":"-99.81","r":"14.80"},{"x":"178.74","y":"-17.64","r":"14.51"},{"x":"125.40","y":"-101.22","r":"13.63"},{"x":"281.50","y":"-252.55","r":"25.39"},{"x":"148.69","y":"-212.67","r":"19.12"},{"x":"99.15","y":"-60.22","r":"11.18"},{"x":"266.10","y":"-144.47","r":"21.18"},{"x":"170.70","y":"-183.90","r":"18.63"},{"x":"83.94","y":"-229.10","r":"18.43"},{"x":"204.80","y":"-122.12","r":"17.75"},{"x":"111.85","y":"-79.59","r":"12.35"},{"x":"91.56","y":"-144.44","r":"14.31"},{"x":"46.58","y":"-74.37","r":"9.61"},{"x":"154.73","y":"-152.90","r":"16.67"},{"x":"58.64","y":"-114.53","r":"11.96"},{"x":"240.64","y":"-115.80","r":"19.22"},{"x":"-300.16","y":"-156.29","r":"22.84"},{"x":"47.52","y":"-420.72","r":"28.33"},{"x":"-318.16","y":"-115.86","r":"22.65"},{"x":"-422.95","y":"-40.82","r":"27.16"},{"x":"-375.81","y":"-196.12","r":"27.35"},{"x":"-166.01","y":"-72.01","r":"14.41"},{"x":"54.63","y":"-251.91","r":"19.12"},{"x":"17.48","y":"-376.63","r":"25.88"},{"x":"-425.41","y":"14.31","r":"27.25"},{"x":"-77.02","y":"-250.38","r":"19.22"},{"x":"-120.51","y":"-406.64","r":"28.33"},{"x":"-125.83","y":"-96.43","r":"13.33"},{"x":"-8.17","y":"-251.33","r":"18.73"},{"x":"102.92","y":"-411.18","r":"28.33"},{"x":"22.85","y":"-201.44","r":"16.18"},{"x":"67.33","y":"-371.36","r":"25.88"},{"x":"-419.75","y":"71.49","r":"27.35"},{"x":"-85.66","y":"-116.76","r":"12.55"},{"x":"-224.95","y":"-359.84","r":"27.94"},{"x":"116.56","y":"-360.44","r":"25.88"},{"x":"-65.13","y":"-418.77","r":"28.43"},{"x":"-69.74","y":"-136.73","r":"13.04"},{"x":"-1.66","y":"-133.45","r":"12.16"},{"x":"-397.91","y":"-146.30","r":"27.25"},{"x":"-339.48","y":"-30.44","r":"22.75"},{"x":"-108.38","y":"-166.50","r":"15.69"},{"x":"125.62","y":"-313.84","r":"23.53"},{"x":"-289.93","y":"-83.28","r":"20.69"},{"x":"-2.40","y":"-158.19","r":"13.53"},{"x":"-413.68","y":"-94.27","r":"27.25"},{"x":"-262.72","y":"-55.01","r":"18.92"},{"x":"-21.56","y":"-120.85","r":"11.57"},{"x":"-183.20","y":"-96.09","r":"15.78"},{"x":"-207.13","y":"-219.12","r":"21.18"},{"x":"156.94","y":"-394.11","r":"28.33"},{"x":"23.24","y":"-235.02","r":"18.04"},{"x":"-27.62","y":"-169.45","r":"14.31"},{"x":"-5.58","y":"-216.76","r":"16.86"},{"x":"-215.88","y":"-93.93","r":"17.16"},{"x":"-324.18","y":"-197.07","r":"25.10"},{"x":"-132.66","y":"-148.78","r":"15.59"},{"x":"-179.06","y":"-334.55","r":"25.78"},{"x":"-31.74","y":"-197.95","r":"15.98"},{"x":"-67.38","y":"-215.41","r":"17.16"},{"x":"-195.40","y":"-182.55","r":"19.31"},{"x":"-41.77","y":"-263.94","r":"19.61"},{"x":"-236.45","y":"-122.76","r":"19.02"},{"x":"-331.30","y":"-73.77","r":"22.65"},{"x":"-257.22","y":"-156.21","r":"20.98"},{"x":"-51.82","y":"-332.70","r":"23.63"},{"x":"-153.22","y":"-96.60","r":"14.41"},{"x":"-96.56","y":"-323.13","r":"23.43"},{"x":"-141.54","y":"-265.28","r":"21.37"},{"x":"-381.71","y":"-11.04","r":"25.00"},{"x":"-45.28","y":"-130.82","r":"12.25"},{"x":"-174.38","y":"-386.88","r":"28.24"},{"x":"-295.73","y":"-238.13","r":"25.29"},{"x":"-346.96","y":"-153.28","r":"24.90"},{"x":"82.88","y":"-325.84","r":"23.43"},{"x":"56.75","y":"-291.79","r":"21.27"},{"x":"22.04","y":"-272.97","r":"20.00"},{"x":"-111.20","y":"-118.47","r":"13.63"},{"x":"-8.65","y":"-423.41","r":"28.43"},{"x":"-347.37","y":"-243.25","r":"27.45"},{"x":"-3.65","y":"-185.80","r":"15.10"},{"x":"-83.35","y":"-369.18","r":"25.78"},{"x":"-364.13","y":"-107.39","r":"24.90"},{"x":"-375.74","y":"-59.85","r":"24.80"},{"x":"38.06","y":"-332.00","r":"23.52"},{"x":"-125.74","y":"-193.47","r":"17.45"},{"x":"-222.46","y":"-307.80","r":"25.49"},{"x":"-96.22","y":"-140.53","r":"14.12"},{"x":"-89.65","y":"-191.23","r":"16.47"},{"x":"-51.50","y":"-155.85","r":"13.73"},{"x":"-101.46","y":"-223.13","r":"18.33"},{"x":"-277.15","y":"-194.58","r":"22.94"},{"x":"-261.57","y":"-275.41","r":"25.49"},{"x":"-234.47","y":"-189.40","r":"20.98"},{"x":"-138.87","y":"-307.92","r":"23.33"},{"x":"-13.67","y":"-291.90","r":"21.08"},{"x":"-200.21","y":"-123.59","r":"17.35"},{"x":"-275.70","y":"-120.60","r":"20.78"},{"x":"-217.83","y":"-154.03","r":"19.12"},{"x":"-270.99","y":"-326.61","r":"27.84"},{"x":"-132.36","y":"-355.23","r":"25.88"},{"x":"-179.38","y":"-287.02","r":"23.43"},{"x":"-251.47","y":"-89.70","r":"18.82"},{"x":"-167.49","y":"-122.34","r":"15.88"},{"x":"-153.37","y":"-172.90","r":"17.25"},{"x":"-249.14","y":"-229.76","r":"23.04"},{"x":"-175.58","y":"-243.86","r":"21.18"},{"x":"-65.80","y":"-292.57","r":"21.47"},{"x":"-78.75","y":"-162.48","r":"14.61"},{"x":"-24.24","y":"-143.86","r":"12.84"},{"x":"-137.44","y":"-227.15","r":"19.31"},{"x":"-58.87","y":"-184.03","r":"15.49"},{"x":"-32.96","y":"-376.43","r":"25.88"},{"x":"-179.92","y":"-151.32","r":"17.55"},{"x":"-104.28","y":"-279.69","r":"21.37"},{"x":"-216.21","y":"-260.83","r":"23.24"},{"x":"-36.75","y":"-229.41","r":"17.65"},{"x":"-138.32","y":"-120.58","r":"14.71"},{"x":"-167.16","y":"-205.59","r":"19.31"},{"x":"-6.90","y":"-334.92","r":"23.53"},{"x":"-312.27","y":"-287.27","r":"27.75"},{"x":"-237.26","y":"294.47","r":"25.78"},{"x":"-36.86","y":"296.52","r":"21.76"},{"x":"-54.70","y":"259.53","r":"20.00"},{"x":"-78.36","y":"416.55","r":"28.82"},{"x":"-140.21","y":"400.19","r":"28.63"},{"x":"-194.55","y":"376.67","r":"28.53"},{"x":"-152.97","y":"255.92","r":"21.57"},{"x":"-195.24","y":"323.73","r":"25.78"},{"x":"-116.27","y":"275.00","r":"21.65"},{"x":"-149.86","y":"348.07","r":"25.98"},{"x":"-192.57","y":"275.83","r":"23.43"},{"x":"-153.05","y":"299.89","r":"23.63"},{"x":"-98.45","y":"366.90","r":"26.27"},{"x":"-110.69","y":"319.72","r":"23.92"},{"x":"-78.32","y":"290.58","r":"21.86"},{"x":"-16.47","y":"423.56","r":"28.82"},{"x":"-42.61","y":"376.63","r":"26.37"},{"x":"-63.71","y":"332.42","r":"24.02"},{"x":"9.87","y":"377.03","r":"26.27"},{"x":"5.43","y":"296.55","r":"21.67"},{"x":"-16.18","y":"335.56","r":"23.92"},{"x":"61.56","y":"371.30","r":"26.18"},{"x":"30.93","y":"332.93","r":"23.92"},{"x":"47.37","y":"291.45","r":"21.67"},{"x":"111.74","y":"359.25","r":"26.08"},{"x":"59.88","y":"252.82","r":"19.61"},{"x":"76.92","y":"324.75","r":"23.73"},{"x":"88.21","y":"281.40","r":"21.67"},{"x":"121.29","y":"311.04","r":"23.82"},{"x":"127.40","y":"266.58","r":"21.57"},{"x":"328.61","y":"81.13","r":"23.04"},{"x":"372.56","y":"205.12","r":"27.84"},{"x":"421.23","y":"45.69","r":"27.45"},{"x":"295.84","y":"53.28","r":"20.88"},{"x":"320.86","y":"208.27","r":"25.69"},{"x":"345.15","y":"161.49","r":"25.59"},{"x":"315.95","y":"124.52","r":"23.04"},{"x":"396.30","y":"152.65","r":"27.65"},{"x":"362.18","y":"114.06","r":"25.20"},{"x":"412.07","y":"99.83","r":"27.55"},{"x":"373.36","y":"65.61","r":"25.00"},{"x":"423.56","y":"-9.03","r":"27.35"},{"x":"378.50","y":"16.51","r":"25.00"},{"x":"335.90","y":"36.95","r":"22.84"},{"x":"337.79","y":"-7.60","r":"22.84"},{"x":"377.54","y":"-32.76","r":"25.00"},{"x":"419.02","y":"-63.52","r":"27.35"},{"x":"370.37","y":"-81.46","r":"25.10"},{"x":"54.38","y":"-1.76","r":"7.94"},{"x":"75.70","y":"-27.64","r":"9.31"},{"x":"34.99","y":"-17.27","r":"7.06"},{"x":"42.80","y":"-29.67","r":"7.65"},{"x":"49.32","y":"-16.26","r":"7.75"},{"x":"34.69","y":"-42.30","r":"7.84"},{"x":"64.84","y":"-14.04","r":"8.53"},{"x":"28.35","y":"-29.03","r":"7.06"},{"x":"81.75","y":"-10.39","r":"9.31"},{"x":"58.50","y":"-29.31","r":"8.33"},{"x":"69.95","y":"1.95","r":"8.63"},{"x":"49.90","y":"42.46","r":"8.63"},{"x":"58.92","y":"28.63","r":"8.63"},{"x":"42.90","y":"27.70","r":"7.84"},{"x":"76.41","y":"25.34","r":"9.51"},{"x":"31.90","y":"5.89","r":"6.86"},{"x":"43.71","y":"12.75","r":"7.45"},{"x":"58.81","y":"13.14","r":"8.24"},{"x":"-18.00","y":"-81.12","r":"9.31"},{"x":"12.45","y":"-64.53","r":"8.43"},{"x":"-1.68","y":"-91.13","r":"9.80"},{"x":"24.61","y":"-54.08","r":"8.14"},{"x":"20.19","y":"-39.88","r":"7.25"},{"x":"-16.30","y":"-64.00","r":"8.43"},{"x":"10.01","y":"-49.26","r":"7.55"},{"x":"-1.92","y":"-73.38","r":"8.82"},{"x":"25.03","y":"171.40","r":"14.80"},{"x":"-18.62","y":"108.76","r":"11.27"},{"x":"35.37","y":"225.56","r":"17.94"},{"x":"-50.47","y":"201.19","r":"16.67"},{"x":"-30.82","y":"128.31","r":"12.45"},{"x":"-16.05","y":"262.20","r":"19.80"},{"x":"14.18","y":"199.51","r":"16.37"},{"x":"-6.87","y":"128.79","r":"12.35"},{"x":"-18.66","y":"151.05","r":"13.63"},{"x":"-17.75","y":"201.63","r":"16.37"},{"x":"-32.90","y":"175.20","r":"15.10"},{"x":"7.62","y":"149.75","r":"13.53"},{"x":"-3.74","y":"174.74","r":"14.90"},{"x":"-34.37","y":"230.11","r":"18.04"},{"x":"0.70","y":"229.80","r":"17.94"},{"x":"22.24","y":"259.79","r":"19.71"},{"x":"-228.63","y":"247.15","r":"23.43"},{"x":"-287.49","y":"311.07","r":"28.04"},{"x":"-307.30","y":"221.48","r":"25.29"},{"x":"-242.89","y":"347.06","r":"28.24"},{"x":"-274.81","y":"260.01","r":"25.49"},{"x":"-326.27","y":"270.28","r":"27.94"},{"x":"-334.55","y":"179.55","r":"25.29"},{"x":"-385.99","y":"177.02","r":"27.45"},{"x":"-326.87","y":"98.38","r":"22.94"},{"x":"-406.42","y":"125.14","r":"27.25"},{"x":"-359.18","y":"225.39","r":"27.75"},{"x":"-356.38","y":"134.88","r":"25.20"},{"x":"163.56","y":"292.08","r":"23.73"},{"x":"337.85","y":"257.53","r":"28.14"},{"x":"159.88","y":"341.00","r":"26.08"},{"x":"272.97","y":"206.44","r":"23.73"},{"x":"203.26","y":"268.32","r":"23.73"},{"x":"285.95","y":"252.73","r":"25.78"},{"x":"205.29","y":"316.91","r":"25.98"},{"x":"247.47","y":"287.35","r":"25.98"},{"x":"45.40","y":"73.27","r":"9.80"},{"x":"11.90","y":"88.96","r":"10.20"},{"x":"39.48","y":"55.93","r":"8.82"},{"x":"11.27","y":"62.85","r":"8.63"},{"x":"27.26","y":"68.59","r":"9.31"},{"x":"24.68","y":"105.41","r":"11.18"},{"x":"33.81","y":"40.45","r":"8.04"},{"x":"28.19","y":"26.97","r":"7.25"},{"x":"23.19","y":"52.01","r":"8.24"},{"x":"7.68","y":"47.92","r":"7.84"},{"x":"41.11","y":"421.12","r":"28.82"},{"x":"97.81","y":"411.35","r":"28.73"},{"x":"204.62","y":"370.50","r":"28.63"},{"x":"252.93","y":"340.19","r":"28.43"},{"x":"152.58","y":"394.37","r":"28.63"},{"x":"297.05","y":"303.91","r":"28.33"},{"x":"-23.37","y":"56.99","r":"8.57"},{"x":"-38.59","y":"-17.25","r":"6.96"},{"x":"-44.89","y":"23.76","r":"7.55"},{"x":"-58.01","y":"15.51","r":"8.04"},{"x":"-58.52","y":"31.73","r":"8.53"},{"x":"14.92","y":"-27.96","r":"6.47"},{"x":"4.54","y":"34.09","r":"7.16"},{"x":"-71.10","y":"5.54","r":"8.53"},{"x":"-19.23","y":"-29.56","r":"6.57"},{"x":"-25.86","y":"-19.40","r":"6.37"},{"x":"-55.98","y":"0.19","r":"7.75"},{"x":"-7.94","y":"-35.71","r":"6.76"},{"x":"9.15","y":"-17.54","r":"5.98"},{"x":"-31.80","y":"31.10","r":"7.35"},{"x":"27.25","y":"-6.32","r":"6.57"},{"x":"-21.31","y":"41.16","r":"7.65"},{"x":"-11.44","y":"-20.93","r":"5.98"},{"x":"-1.86","y":"-0.15","r":"5.10"},{"x":"22.99","y":"15.14","r":"6.67"},{"x":"-42.19","y":"-4.19","r":"6.96"},{"x":"-8.34","y":"29.61","r":"6.76"},{"x":"-44.23","y":"9.43","r":"7.25"},{"x":"-17.98","y":"-11.52","r":"5.78"},{"x":"19.32","y":"3.43","r":"6.27"},{"x":"14.36","y":"24.80","r":"6.67"},{"x":"-32.21","y":"17.34","r":"6.96"},{"x":"-0.26","y":"-24.80","r":"6.08"},{"x":"1.85","y":"21.44","r":"6.37"},{"x":"4.57","y":"-7.67","r":"5.39"},{"x":"10.86","y":"13.02","r":"6.08"},{"x":"-20.14","y":"-0.32","r":"5.98"},{"x":"-10.00","y":"-5.18","r":"5.31"},{"x":"-29.55","y":"-7.82","r":"6.47"},{"x":"15.21","y":"-7.52","r":"5.88"},{"x":"-3.53","y":"-13.72","r":"5.49"},{"x":"-20.28","y":"24.08","r":"6.76"},{"x":"8.04","y":"2.24","r":"5.59"},{"x":"-31.59","y":"4.42","r":"6.57"},{"x":"-0.26","y":"10.00","r":"5.78"},{"x":"-9.75","y":"17.05","r":"6.18"},{"x":"-10.60","y":"5.62","r":"5.69"},{"x":"-20.73","y":"11.46","r":"6.18"},{"x":"0.30","y":"74.63","r":"9.22"},{"x":"-8.20","y":"90.76","r":"10.20"},{"x":"60.90","y":"114.10","r":"12.25"},{"x":"58.59","y":"138.15","r":"13.43"},{"x":"80.30","y":"154.27","r":"14.80"},{"x":"39.62","y":"121.69","r":"12.25"}];
144 |
145 | export { createScene };
--------------------------------------------------------------------------------