├── 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 }; --------------------------------------------------------------------------------