├── .npmignore
├── .gitignore
├── media
├── bulb.png
├── dof.png
├── example.png
└── glass.png
├── src
├── glsl
│ ├── sample.vert
│ ├── display.vert
│ ├── display.frag
│ └── sample.frag
├── pingpong.js
├── camera.js
├── voxel-index.js
├── stage.js
└── render.js
├── docs
└── index.html
├── example
├── index.html
└── index.js
├── package.json
├── LICENSE
├── index.js
└── README.md
/.npmignore:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | lib
3 |
--------------------------------------------------------------------------------
/media/bulb.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wwwtyro/vixel/HEAD/media/bulb.png
--------------------------------------------------------------------------------
/media/dof.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wwwtyro/vixel/HEAD/media/dof.png
--------------------------------------------------------------------------------
/media/example.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wwwtyro/vixel/HEAD/media/example.png
--------------------------------------------------------------------------------
/media/glass.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wwwtyro/vixel/HEAD/media/glass.png
--------------------------------------------------------------------------------
/src/glsl/sample.vert:
--------------------------------------------------------------------------------
1 | precision highp float;
2 |
3 | attribute vec2 position;
4 |
5 | void main() {
6 | gl_Position = vec4(position, 0, 1);
7 | }
8 |
--------------------------------------------------------------------------------
/src/glsl/display.vert:
--------------------------------------------------------------------------------
1 | precision highp float;
2 |
3 | attribute vec2 position;
4 |
5 | varying vec2 vPos;
6 |
7 | void main() {
8 | gl_Position = vec4(position, 0, 1);
9 | vPos = 0.5 * position + 0.5;
10 | }
11 |
--------------------------------------------------------------------------------
/src/glsl/display.frag:
--------------------------------------------------------------------------------
1 | precision highp float;
2 |
3 | uniform sampler2D source;
4 |
5 | varying vec2 vPos;
6 |
7 | void main() {
8 | vec4 src = texture2D(source, vPos);
9 | vec3 color = src.rgb/max(src.a, 1.0);
10 | color = pow(color, vec3(1.0/2.2));
11 | gl_FragColor = vec4(color, 1);
12 | }
13 |
--------------------------------------------------------------------------------
/docs/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
21 |
22 |
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/example/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
21 |
22 |
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/src/pingpong.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | module.exports = function PingPong(regl, opts) {
4 | const fbos = [regl.framebuffer(opts), regl.framebuffer(opts)];
5 |
6 | let index = 0;
7 |
8 | function ping() {
9 | return fbos[index];
10 | }
11 |
12 | function pong() {
13 | return fbos[1 - index];
14 | }
15 |
16 | function swap() {
17 | index = 1 - index;
18 | }
19 |
20 | function resize(width, height) {
21 | opts.width = width;
22 | opts.height = height;
23 | ping()(opts);
24 | pong()(opts);
25 | }
26 |
27 | return {
28 | ping: ping,
29 | pong: pong,
30 | swap: swap,
31 | resize: resize
32 | };
33 | };
34 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "vixel",
3 | "version": "1.0.8",
4 | "description": "A WebGL voxel path tracer",
5 | "main": "./lib/bundle.js",
6 | "scripts": {
7 | "start": "budo -d example example/index.js:bundle.js --live -- -t glslify",
8 | "build": "browserify index.js -s Vixel -o lib/bundle.js",
9 | "build-example": "browserify example/index.js -o docs/bundle.js && cp example/index.html docs/index.html"
10 | },
11 | "keywords": [
12 | "webgl",
13 | "voxel",
14 | "path tracing"
15 | ],
16 | "author": "Rye Terrell",
17 | "browserify": {
18 | "transform": [
19 | "glslify"
20 | ]
21 | },
22 | "license": "Unlicense",
23 | "repository": {
24 | "type": "git",
25 | "url": "git+https://github.com/wwwtyro/vixel.git"
26 | },
27 | "dependencies": {
28 | "gl-matrix": "^3.3.0",
29 | "regl": "^1.7.0",
30 | "regl-atmosphere-envmap": "^1.0.5",
31 | "glslify": "^7.1.1"
32 | },
33 | "devDependencies": {
34 | "budo": "^11.6.4"
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/src/camera.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | const { mat4, vec3 } = require("gl-matrix");
4 |
5 | module.exports = class Camera {
6 | constructor(domElement) {
7 | this.domElement = domElement;
8 | this.fov = Math.PI / 6;
9 | this.eye = [0, 0, 4];
10 | this.center = [0, 0, 0];
11 | this.up = [0, 1, 0];
12 | }
13 |
14 | view() {
15 | return mat4.lookAt([], this.eye, this.center, this.up);
16 | }
17 |
18 | projection() {
19 | return mat4.perspective(
20 | [],
21 | this.fov,
22 | this.domElement.width / this.domElement.height,
23 | 0.1,
24 | 1000
25 | );
26 | }
27 |
28 | invpv() {
29 | const v = this.view();
30 | const p = this.projection();
31 | const pv = mat4.multiply([], p, v);
32 | return mat4.invert([], pv);
33 | }
34 |
35 | serialize() {
36 | return {
37 | version: 0,
38 | fov: this.fov,
39 | eye: this.eye,
40 | center: this.center,
41 | up: this.up
42 | };
43 | }
44 |
45 | deserialize(data) {
46 | // TODO: make this static & return new Camera object
47 | this.fov = data.fov;
48 | this.eye = data.eye;
49 | this.center = data.center;
50 | this.up = data.up;
51 | }
52 | };
53 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | This is free and unencumbered software released into the public domain.
2 |
3 | Anyone is free to copy, modify, publish, use, compile, sell, or
4 | distribute this software, either in source code form or as a compiled
5 | binary, for any purpose, commercial or non-commercial, and by any
6 | means.
7 |
8 | In jurisdictions that recognize copyright laws, the author or authors
9 | of this software dedicate any and all copyright interest in the
10 | software to the public domain. We make this dedication for the benefit
11 | of the public at large and to the detriment of our heirs and
12 | successors. We intend this dedication to be an overt act of
13 | relinquishment in perpetuity of all present and future rights to this
14 | software under copyright law.
15 |
16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
22 | OTHER DEALINGS IN THE SOFTWARE.
23 |
24 | For more information, please refer to
25 |
--------------------------------------------------------------------------------
/src/voxel-index.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | module.exports = class VoxelIndex {
4 | constructor() {
5 | this.aRGB = new Uint8Array(256 * 256 * 3);
6 | this.aRMET = new Float32Array(256 * 256 * 4);
7 | this.aRi = new Float32Array(256 * 256 * 4);
8 | this.clear();
9 | }
10 |
11 | clear() {
12 | this.aRGB.fill(0);
13 | this.aRMET.fill(0);
14 | this.aRi.fill(0);
15 | this.x = 1;
16 | this.y = 0;
17 | this.keys = {};
18 | }
19 |
20 | get(v) {
21 | const h = `${v.red} ${v.green} ${v.blue} ${v.rough} ${v.metal} ${v.emit} ${v.transparent} ${v.refract}`;
22 | if (this.keys[h] === undefined) {
23 | // It's cool that we're skipping the first two indices, because those will be a shortcut for air and ground.
24 | this.x++;
25 | if (this.x > 255) {
26 | this.x = 0;
27 | this.y++;
28 | if (this.y > 255) {
29 | throw new Error("Exceeded voxel type limit of 65536");
30 | }
31 | }
32 | this.keys[h] = [this.x, this.y];
33 | const i = this.y * 256 + this.x;
34 | this.aRGB[i * 3 + 0] = v.red;
35 | this.aRGB[i * 3 + 1] = v.green;
36 | this.aRGB[i * 3 + 2] = v.blue;
37 | this.aRMET[i * 4 + 0] = v.rough;
38 | this.aRMET[i * 4 + 1] = v.metal;
39 | this.aRMET[i * 4 + 2] = v.emit;
40 | this.aRMET[i * 4 + 3] = v.transparent;
41 | this.aRi[i * 4 + 0] = v.refract;
42 | }
43 | return this.keys[h];
44 | }
45 | };
46 |
--------------------------------------------------------------------------------
/example/index.js:
--------------------------------------------------------------------------------
1 | const Vixel = require("../index");
2 |
3 | const canvas = document.getElementById("render-canvas");
4 | canvas.width = 512;
5 | canvas.height = 768;
6 |
7 | const bounds = [64, 32, 22];
8 |
9 | const vixel = new Vixel(canvas, ...bounds);
10 |
11 | vixel.camera(
12 | [bounds[0] * 1.35, bounds[1] * 0.75, bounds[2] * 0.5],
13 | [bounds[0] * 0.5, bounds[1] * 0, bounds[2] * 0.5],
14 | [0, 1, 0],
15 | Math.PI / 2.8
16 | );
17 |
18 | vixel.dof(0.5, 0.25);
19 |
20 | vixel.sun(17, (1.5 * Math.PI) / 2, 1, 1);
21 |
22 | // Ground
23 | for (let x = 0; x < bounds[0]; x++) {
24 | for (let z = 0; z < bounds[2]; z++) {
25 | vixel.set(x, 0, z, {
26 | red: 0.125,
27 | green: 0.06125,
28 | blue: 0.01,
29 | });
30 | }
31 | }
32 |
33 | // Random walk
34 | let x = bounds[0] * 0.5;
35 | let z = bounds[2];
36 | for (let i = 0; i < 2 ** 11; i++) {
37 | x += Math.round(Math.random() * 2) - 1;
38 | z += Math.round(Math.random() * 2) - 1;
39 | x = ((x % bounds[0]) + bounds[0]) % bounds[0];
40 | z = ((z % bounds[2]) + bounds[2]) % bounds[2];
41 | for (let y = bounds[1] - 1; y > 0; y--) {
42 | if (vixel.get(x, y - 1, z)) {
43 | const opts = {
44 | red: 0.25,
45 | green: 0.25,
46 | blue: 0.25,
47 | };
48 | if (Math.random() < 0.03) {
49 | opts.emit = 8;
50 | }
51 | vixel.set(x, y, z, opts);
52 | break;
53 | }
54 | }
55 | }
56 |
57 | // Water
58 | for (let x = 0; x < bounds[0]; x++) {
59 | for (let y = 1; y < bounds[1] * 0.125; y++) {
60 | for (let z = 0; z < bounds[2]; z++) {
61 | if (!vixel.get(x, y, z)) {
62 | vixel.set(x, y, z, {
63 | red: 0.125,
64 | green: 1,
65 | blue: 0.75,
66 | transparent: 1,
67 | refract: 1.333,
68 | rough: 0.1,
69 | });
70 | }
71 | }
72 | }
73 | }
74 |
75 | let samples = 0;
76 |
77 | function loop() {
78 | vixel.sample(2);
79 | vixel.display();
80 | samples += 2;
81 | if (samples < 1024) {
82 | requestAnimationFrame(loop);
83 | }
84 | }
85 |
86 | loop();
87 |
--------------------------------------------------------------------------------
/src/stage.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | const { vec3 } = require("gl-matrix");
4 |
5 | const VoxelIndex = require("./voxel-index");
6 |
7 | module.exports = class Stage {
8 | constructor(regl, width, height, depth) {
9 | this.regl = regl;
10 | this.width = width;
11 | this.height = height;
12 | this.depth = depth;
13 | this.data = {};
14 | this.vIndex = new VoxelIndex();
15 | this.tIndex = regl.texture();
16 | this.tRGB = regl.texture();
17 | this.tRMET = regl.texture();
18 | this.tRi = regl.texture();
19 | }
20 |
21 | key(x, y, z) {
22 | return `${x} ${y} ${z}`;
23 | }
24 |
25 | set(
26 | x,
27 | y,
28 | z,
29 | {
30 | red = 1,
31 | green = 1,
32 | blue = 1,
33 | rough = 1,
34 | metal = 0,
35 | emit = 0,
36 | transparent = 0,
37 | refract = 1,
38 | } = {}
39 | ) {
40 | if (x < 0 || x >= this.width) throw new Error("Vixel: set out of bounds.");
41 | if (y < 0 || y >= this.height) throw new Error("Vixel: set out of bounds.");
42 | if (z < 0 || z >= this.depth) throw new Error("Vixel: set out of bounds.");
43 | this.data[this.key(x, y, z)] = {
44 | x,
45 | y,
46 | z,
47 | red: Math.round(red * 255),
48 | green: Math.round(green * 255),
49 | blue: Math.round(blue * 255),
50 | rough,
51 | metal,
52 | emit,
53 | transparent,
54 | refract,
55 | };
56 | }
57 |
58 | unset(x, y, z) {
59 | if (Object.keys(this.data).length === 1) return;
60 | delete this.data[this.key(x, y, z)];
61 | }
62 |
63 | get(x, y, z) {
64 | return this.data[this.key(x, y, z)];
65 | }
66 |
67 | clear() {
68 | this.vIndex.clear();
69 | this.data = {};
70 | }
71 |
72 | update() {
73 | this.textureSize = 1;
74 | while (
75 | this.textureSize * this.textureSize <
76 | this.width * this.height * this.depth
77 | ) {
78 | this.textureSize *= 2;
79 | }
80 | const aIndex = new Uint8Array(this.textureSize * this.textureSize * 2);
81 | aIndex.fill(0);
82 | for (let [_, v] of Object.entries(this.data)) {
83 | const vi = this.vIndex.get(v);
84 | const ai = v.y * this.width * this.depth + v.z * this.width + v.x;
85 | aIndex[ai * 2 + 0] = vi[0];
86 | aIndex[ai * 2 + 1] = vi[1];
87 | }
88 | this.tIndex({
89 | width: this.textureSize,
90 | height: this.textureSize,
91 | format: "luminance alpha",
92 | data: aIndex,
93 | });
94 | this.tRGB({
95 | width: 256,
96 | height: 256,
97 | format: "rgb",
98 | data: this.vIndex.aRGB,
99 | });
100 | this.tRMET({
101 | width: 256,
102 | height: 256,
103 | format: "rgba",
104 | type: "float",
105 | data: this.vIndex.aRMET,
106 | });
107 | this.tRi({
108 | width: 256,
109 | height: 256,
110 | format: "rgba",
111 | type: "float",
112 | data: this.vIndex.aRi,
113 | });
114 | }
115 |
116 | serialize() {
117 | const out = {
118 | version: 0,
119 | };
120 | out.width = this.width;
121 | out.height = this.height;
122 | out.depth = this.depth;
123 | out.xyz = [];
124 | out.rgb = [];
125 | out.rough = [];
126 | out.metal = [];
127 | out.emit = [];
128 | out.transparent = [];
129 | out.refract = [];
130 | for (let [_, v] of Object.entries(this.data)) {
131 | out.xyz.push(v.x, v.y, v.z);
132 | out.rgb.push(v.red, v.green, v.blue);
133 | out.rough.push(+v.rough.toFixed(3));
134 | out.metal.push(+v.metal.toFixed(3));
135 | out.emit.push(+v.emit.toFixed(3));
136 | out.transparent.push(+v.transparent.toFixed(3));
137 | out.refract.push(+v.refract.toFixed(3));
138 | }
139 | return out;
140 | }
141 |
142 | deserialize(d) {
143 | this.clear();
144 | this.width = d.width;
145 | this.height = d.height;
146 | this.depth = d.depth;
147 | for (let i = 0; i < d.xyz.length / 3; i++) {
148 | this.set(d.xyz[i * 3 + 0], d.xyz[i * 3 + 1], d.xyz[i * 3 + 2], {
149 | red: d.rgb[i * 3 + 0] / 255,
150 | green: d.rgb[i * 3 + 1] / 255,
151 | blue: d.rgb[i * 3 + 2] / 255,
152 | rough: d.rough[i],
153 | metal: d.metal[i],
154 | emit: d.emit[i],
155 | transparent: d.transparent[i],
156 | refract: d.refract[i],
157 | });
158 | }
159 | }
160 | };
161 |
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | const { vec3 } = require("gl-matrix");
2 | const Renderer = require("./src/render");
3 | const Stage = require("./src/stage");
4 | const Camera = require("./src/camera");
5 |
6 | module.exports = class Vixel {
7 | constructor(canvas, width, height, depth) {
8 | this._canvas = canvas;
9 | this._renderer = Renderer(this._canvas);
10 | this._stage = new Stage(this._renderer.context, width, height, depth);
11 | this._camera = new Camera(this._canvas);
12 | this._ground = {
13 | color: [1, 1, 1],
14 | rough: 1,
15 | metal: 0
16 | };
17 | this._sun = {
18 | time: 6,
19 | azimuth: 0,
20 | radius: 8,
21 | intensity: 1
22 | };
23 | this._dof = {
24 | distance: 0.5,
25 | magnitude: 0
26 | };
27 | this._renderDirty = true;
28 | this._stageDirty = true;
29 | this._canvas._vxOldsize = [this._canvas.width, this._canvas.height];
30 | }
31 |
32 | set(x, y, z, opts) {
33 | this._stage.set(x, y, z, opts);
34 | this._stageDirty = true;
35 | }
36 |
37 | unset(x, y, z) {
38 | this._stage.unset(x, y, z);
39 | this._stageDirty = true;
40 | }
41 |
42 | get(x, y, z) {
43 | return this._stage.get(x, y, z);
44 | }
45 |
46 | clear() {
47 | this._stage.clear();
48 | this._stageDirty = true;
49 | }
50 |
51 | serialize() {
52 | return {
53 | stage: this._stage.serialize(),
54 | camera: this._camera.serialize(),
55 | dof: JSON.parse(JSON.stringify(this._dof)),
56 | sun: JSON.parse(JSON.stringify(this._sun)),
57 | ground: JSON.parse(JSON.stringify(this._ground))
58 | };
59 | }
60 |
61 | deserialize(data) {
62 | this._stage.deserialize(data.stage);
63 | this._camera.deserialize(data.camera);
64 | this._dof = JSON.parse(JSON.stringify(data.dof));
65 | this._sun = JSON.parse(JSON.stringify(data.sun));
66 | this._ground = JSON.parse(JSON.stringify(data.ground));
67 | this._stageDirty = true;
68 | this._renderDirty = true;
69 | }
70 |
71 | get sampleCount() {
72 | return this._renderer.sampleCount();
73 | }
74 |
75 | camera(eye, center, up, fov) {
76 | if (
77 | `${eye} ${center} ${up} ${fov}` ===
78 | `${this._camera.eye} ${this._camera.center} ${this._camera.up} ${
79 | this._camera.fov
80 | }`
81 | ) {
82 | return;
83 | }
84 | this._camera.eye = eye.slice();
85 | this._camera.center = center.slice();
86 | this._camera.up = up.slice();
87 | this._camera.fov = fov;
88 | this._renderDirty = true;
89 | }
90 |
91 | ground(color, rough, metal) {
92 | if (
93 | `${color} ${rough} ${metal}` ===
94 | `${this._ground.color} ${this._ground.rough} ${this._ground.metal}`
95 | ) {
96 | return;
97 | }
98 | this._ground = {
99 | color,
100 | rough,
101 | metal
102 | };
103 | this._renderDirty = true;
104 | }
105 |
106 | sun(time, azimuth, radius, intensity) {
107 | if (
108 | `${time} ${azimuth} ${radius} ${intensity}` ===
109 | `${this._sun.time} ${this._sun.azimuth} ${this._sun.radius} ${
110 | this._sun.intensity
111 | }`
112 | ) {
113 | return;
114 | }
115 | this._sun = {
116 | time,
117 | azimuth,
118 | radius,
119 | intensity
120 | };
121 | this._renderDirty = true;
122 | }
123 |
124 | dof(distance, magnitude) {
125 | if (
126 | `${distance} ${magnitude}` ===
127 | `${this._dof.distance} ${this._dof.magnitude}`
128 | ) {
129 | return;
130 | }
131 | this._dof = {
132 | distance,
133 | magnitude
134 | };
135 | this._renderDirty = true;
136 | }
137 |
138 | sample(count) {
139 | if (
140 | this._canvas._vxOldsize[0] !== this._canvas.width ||
141 | this._canvas._vxOldsize[1] !== this._canvas.height
142 | ) {
143 | this._canvas._vxOldsize = [this._canvas.width, this._canvas.height];
144 | this._renderDirty = true;
145 | }
146 | if (this._stageDirty) {
147 | this._stage.update();
148 | this._renderDirty = true;
149 | this._stageDirty = false;
150 | }
151 | if (this._renderDirty) {
152 | this._renderer.reset();
153 | this._renderDirty = false;
154 | }
155 | this._renderer.sample(this._stage, this._camera, {
156 | groundColor: this._ground.color,
157 | groundRoughness: this._ground.rough,
158 | groundMetalness: this._ground.metal,
159 | time: this._sun.time,
160 | azimuth: this._sun.azimuth,
161 | lightRadius: this._sun.radius,
162 | lightIntensity: this._sun.intensity,
163 | dofDist: this._dof.distance,
164 | dofMag: this._dof.magnitude,
165 | count: count
166 | });
167 | }
168 |
169 | display() {
170 | this._renderer.display();
171 | }
172 | };
173 |
--------------------------------------------------------------------------------
/src/render.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | const { mat4, vec3, vec2 } = require("gl-matrix");
4 | const glsl = require("glslify");
5 | const createAtmosphereRenderer = require("regl-atmosphere-envmap");
6 | const PingPong = require("./pingpong");
7 |
8 | module.exports = function Renderer(canvas) {
9 | const regl = require("regl")({
10 | canvas: canvas,
11 | optionalExtensions: [
12 | "OES_texture_float",
13 | "OES_texture_half_float",
14 | "WEBGL_color_buffer_float",
15 | "EXT_color_buffer_half_float",
16 | ],
17 | attributes: {
18 | antialias: false,
19 | preserveDrawingBuffer: true,
20 | },
21 | });
22 |
23 | const colorType = (() => {
24 | if (
25 | regl.hasExtension("OES_texture_float") &&
26 | regl.hasExtension("WEBGL_color_buffer_float")
27 | ) {
28 | return "float";
29 | }
30 | if (
31 | regl.hasExtension("OES_texture_half_float") ||
32 | regl.hasExtension("EXT_color_buffer_half_float")
33 | ) {
34 | return "half float";
35 | }
36 | throw new Error(
37 | "Vixel requires the capability to render to floating point textures."
38 | );
39 | })();
40 |
41 | const sunDistance = 149600000000;
42 | let sunPosition = vec3.scale(
43 | [],
44 | vec3.normalize([], [1.11, -0.0, 0.25]),
45 | sunDistance
46 | );
47 |
48 | const renderAtmosphere = createAtmosphereRenderer(regl);
49 | const skyMap = renderAtmosphere({
50 | sunDirection: vec3.normalize([], sunPosition),
51 | resolution: 1024,
52 | });
53 |
54 | const pingpong = PingPong(regl, {
55 | width: canvas.width,
56 | height: canvas.height,
57 | colorType,
58 | });
59 |
60 | const ndcBox = [-1, -1, 1, -1, 1, 1, -1, -1, 1, 1, -1, 1];
61 |
62 | const tRandSize = 1024;
63 |
64 | const t2Sphere = (function () {
65 | const data = new Float32Array(tRandSize * tRandSize * 3);
66 | for (let i = 0; i < tRandSize * tRandSize; i++) {
67 | const r = vec3.random([]);
68 | data[i * 3 + 0] = r[0];
69 | data[i * 3 + 1] = r[1];
70 | data[i * 3 + 2] = r[2];
71 | }
72 | return regl.texture({
73 | width: tRandSize,
74 | height: tRandSize,
75 | format: "rgb",
76 | type: "float",
77 | data: data,
78 | wrap: "repeat",
79 | });
80 | })();
81 |
82 | const t3Sphere = (function () {
83 | const data = new Float32Array(tRandSize * tRandSize * 3);
84 | for (let i = 0; i < tRandSize * tRandSize; i++) {
85 | const r = vec3.random([], Math.random());
86 | data[i * 3 + 0] = r[0];
87 | data[i * 3 + 1] = r[1];
88 | data[i * 3 + 2] = r[2];
89 | }
90 | return regl.texture({
91 | width: tRandSize,
92 | height: tRandSize,
93 | format: "rgb",
94 | type: "float",
95 | data: data,
96 | wrap: "repeat",
97 | });
98 | })();
99 |
100 | const tUniform2 = (function () {
101 | const data = new Float32Array(tRandSize * tRandSize * 2);
102 | for (let i = 0; i < tRandSize * tRandSize; i++) {
103 | data[i * 2 + 0] = Math.random();
104 | data[i * 2 + 1] = Math.random();
105 | }
106 | return regl.texture({
107 | width: tRandSize,
108 | height: tRandSize,
109 | format: "luminance alpha",
110 | type: "float",
111 | data: data,
112 | wrap: "repeat",
113 | });
114 | })();
115 |
116 | const tUniform1 = (function () {
117 | const data = new Float32Array(tRandSize * tRandSize * 1);
118 | for (let i = 0; i < tRandSize * tRandSize; i++) {
119 | data[i] = Math.random();
120 | }
121 | return regl.texture({
122 | width: tRandSize,
123 | height: tRandSize,
124 | format: "luminance",
125 | type: "float",
126 | data: data,
127 | wrap: "repeat",
128 | });
129 | })();
130 |
131 | const cmdSample = regl({
132 | vert: glsl.file("./glsl/sample.vert"),
133 | frag: glsl.file("./glsl/sample.frag"),
134 | attributes: {
135 | position: ndcBox,
136 | },
137 | uniforms: {
138 | source: regl.prop("source"),
139 | invpv: regl.prop("invpv"),
140 | eye: regl.prop("eye"),
141 | res: regl.prop("res"),
142 | resFrag: regl.prop("resFrag"),
143 | tSky: skyMap,
144 | tUniform1: tUniform1,
145 | tUniform2: tUniform2,
146 | t2Sphere: t2Sphere,
147 | t3Sphere: t3Sphere,
148 | tOffset: regl.prop("tOffset"),
149 | tRGB: regl.prop("tRGB"),
150 | tRMET: regl.prop("tRMET"),
151 | tRi: regl.prop("tRi"),
152 | tIndex: regl.prop("tIndex"),
153 | dofDist: regl.prop("dofDist"),
154 | dofMag: regl.prop("dofMag"),
155 | resStage: regl.prop("resStage"),
156 | invResRand: [1 / tRandSize, 1 / tRandSize],
157 | lightPosition: regl.prop("lightPosition"),
158 | lightIntensity: regl.prop("lightIntensity"),
159 | lightRadius: regl.prop("lightRadius"),
160 | groundColor: regl.prop("groundColor"),
161 | groundRoughness: regl.prop("groundRoughness"),
162 | groundMetalness: regl.prop("groundMetalness"),
163 | bounds: regl.prop("bounds"),
164 | },
165 | depth: {
166 | enable: false,
167 | mask: false,
168 | },
169 | viewport: regl.prop("viewport"),
170 | framebuffer: regl.prop("destination"),
171 | count: 6,
172 | });
173 |
174 | const cmdDisplay = regl({
175 | vert: glsl.file("./glsl/display.vert"),
176 | frag: glsl.file("./glsl/display.frag"),
177 | attributes: {
178 | position: ndcBox,
179 | },
180 | uniforms: {
181 | source: regl.prop("source"),
182 | fraction: regl.prop("fraction"),
183 | tUniform1: tUniform1,
184 | tUniform1Res: [tUniform1.width, tUniform1.height],
185 | },
186 | depth: {
187 | enable: false,
188 | mask: false,
189 | },
190 | viewport: regl.prop("viewport"),
191 | count: 6,
192 | });
193 |
194 | function calculateSunPosition(time, azimuth) {
195 | const theta = (2 * Math.PI * (time - 6)) / 24;
196 | return [
197 | sunDistance * Math.cos(azimuth) * Math.cos(theta),
198 | sunDistance * Math.sin(theta),
199 | sunDistance * Math.sin(azimuth) * Math.cos(theta),
200 | ];
201 | }
202 |
203 | let sampleCount = 0;
204 |
205 | function sample(stage, camera, opts) {
206 | const sp = calculateSunPosition(opts.time, opts.azimuth);
207 | if (vec3.distance(sp, sunPosition) > 0.001) {
208 | sunPosition = sp;
209 | renderAtmosphere({
210 | sunDirection: vec3.normalize([], sunPosition),
211 | cubeFBO: skyMap,
212 | });
213 | }
214 | for (let i = 0; i < opts.count; i++) {
215 | cmdSample({
216 | eye: camera.eye,
217 | invpv: camera.invpv(),
218 | res: [canvas.width, canvas.height],
219 | tOffset: [Math.random(), Math.random()],
220 | tRGB: stage.tRGB,
221 | tRMET: stage.tRMET,
222 | tRi: stage.tRi,
223 | tIndex: stage.tIndex,
224 | resStage: stage.tIndex.width,
225 | bounds: [stage.width, stage.height, stage.depth],
226 | lightPosition: sunPosition,
227 | lightIntensity: opts.lightIntensity,
228 | lightRadius: 695508000 * opts.lightRadius,
229 | groundRoughness: opts.groundRoughness,
230 | groundColor: opts.groundColor,
231 | groundMetalness: opts.groundMetalness,
232 | dofDist: opts.dofDist,
233 | dofMag: opts.dofMag,
234 | source: pingpong.ping(),
235 | destination: pingpong.pong(),
236 | viewport: { x: 0, y: 0, width: canvas.width, height: canvas.height },
237 | });
238 | pingpong.swap();
239 | sampleCount++;
240 | }
241 | }
242 |
243 | function display() {
244 | cmdDisplay({
245 | source: pingpong.ping(),
246 | viewport: { x: 0, y: 0, width: canvas.width, height: canvas.height },
247 | });
248 | }
249 |
250 | function reset() {
251 | if (
252 | pingpong.ping().width !== canvas.width ||
253 | pingpong.ping().height !== canvas.height
254 | ) {
255 | pingpong.ping()({
256 | width: canvas.width,
257 | height: canvas.height,
258 | colorType,
259 | });
260 | pingpong.pong()({
261 | width: canvas.width,
262 | height: canvas.height,
263 | colorType,
264 | });
265 | }
266 | regl.clear({ color: [0, 0, 0, 1], framebuffer: pingpong.ping() });
267 | regl.clear({ color: [0, 0, 0, 1], framebuffer: pingpong.pong() });
268 | sampleCount = 0;
269 | }
270 |
271 | return {
272 | context: regl,
273 | sample: sample,
274 | display: display,
275 | reset: reset,
276 | sampleCount: function () {
277 | return sampleCount;
278 | },
279 | };
280 | };
281 |
--------------------------------------------------------------------------------
/src/glsl/sample.frag:
--------------------------------------------------------------------------------
1 | precision highp float;
2 | uniform highp sampler2D tRGB, tRMET, tRi, tIndex, t2Sphere, t3Sphere, tUniform2, tUniform1, source;
3 | uniform samplerCube tSky;
4 | uniform mat4 invpv;
5 | uniform vec3 eye, bounds, lightPosition, groundColor;
6 | uniform vec2 res, tOffset, invResRand;
7 | uniform float resStage, lightRadius, groundRoughness, groundMetalness, dofDist, dofMag, lightIntensity;
8 |
9 | const float epsilon = 0.0001;
10 | const int nBounces = 5;
11 |
12 | float randUniform1(inout vec2 randOffset) {
13 | float r = texture2D(tUniform1, randOffset + tOffset).r;
14 | randOffset += r;
15 | return r;
16 | }
17 |
18 | vec2 randUniform2(inout vec2 randOffset) {
19 | vec2 r = texture2D(tUniform2, randOffset + tOffset).ra;
20 | randOffset += r;
21 | return r;
22 | }
23 |
24 | vec3 rand2Sphere(inout vec2 randOffset) {
25 | vec3 r = texture2D(t2Sphere, randOffset + tOffset).xyz;
26 | randOffset += r.xy;
27 | return r;
28 | }
29 |
30 | vec3 rand3Sphere(inout vec2 randOffset) {
31 | vec3 r = texture2D(t3Sphere, randOffset + tOffset).xyz;
32 | randOffset += r.xy;
33 | return r;
34 | }
35 |
36 | bool inBounds(vec3 p) {
37 | if (p.x < 0.0 || p.y < 0.0 || p.z < 0.0) {
38 | return false;
39 | }
40 | if (p.x >= bounds.x || p.y >= bounds.y || p.z >= bounds.z) {
41 | return false;
42 | }
43 | return true;
44 | }
45 |
46 | bool rayAABB(vec3 origin, vec3 direction, vec3 bMin, vec3 bMax, out float t0) {
47 | vec3 invDir = 1.0 / direction;
48 | vec3 omin = (bMin - origin) * invDir;
49 | vec3 omax = (bMax - origin) * invDir;
50 | vec3 imax = max(omax, omin);
51 | vec3 imin = min(omax, omin);
52 | float t1 = min(imax.x, min(imax.y, imax.z));
53 | t0 = max(imin.x, max(imin.y, imin.z));
54 | t0 = max(t0, 0.0);
55 | return t1 > t0;
56 | }
57 |
58 | vec3 rayAABBNorm(vec3 p, vec3 v) {
59 | vec3 d = p - (v + 0.5);
60 | vec3 dabs = abs(d);
61 | if (dabs.x > dabs.y) {
62 | if (dabs.x > dabs.z) {
63 | return vec3(sign(d.x), 0.0, 0.0);
64 | } else {
65 | return vec3(0, 0, sign(d.z));
66 | }
67 | } else {
68 | if (dabs.y > dabs.z) {
69 | return vec3(0.0, sign(d.y), 0.0);
70 | } else {
71 | return vec3(0.0, 0.0, sign(d.z));
72 | }
73 | }
74 | }
75 |
76 | vec2 samplePoint(vec3 v) {
77 | float invResStage = 1.0 / resStage;
78 | float i = v.y * bounds.x * bounds.z + v.z * bounds.x + v.x;
79 | i = i * invResStage;
80 | float y = floor(i);
81 | float x = fract(i) * resStage;
82 | x = (x + 0.5) * invResStage;
83 | y = (y + 0.5) * invResStage;
84 | return vec2(x, y);
85 | }
86 |
87 |
88 | struct VoxelData {
89 | vec3 xyz;
90 | vec3 rgb;
91 | vec2 index;
92 | float roughness;
93 | float metalness;
94 | float emission;
95 | float transparent;
96 | float ri;
97 | };
98 |
99 | VoxelData floorData(vec3 v) {
100 | return VoxelData(v, groundColor, vec2(1.0/255.0, 0.0), groundRoughness, groundMetalness, 0.0, 0.0, 1.0);
101 | }
102 |
103 | VoxelData airData(vec3 v) {
104 | return VoxelData(v, vec3(1.0), vec2(0.0), 0.0, 0.0, 0.0, 1.0, 1.0);
105 | }
106 |
107 | VoxelData voxelData(vec3 v) {
108 | VoxelData vd;
109 | vd.xyz = v;
110 | if (v.y == -1.0) {
111 | return floorData(v);
112 | }
113 | if (!inBounds(v)) {
114 | return airData(v);
115 | }
116 | vec2 s = samplePoint(v);
117 | vd.index = texture2D(tIndex, s).ra;
118 | if (vd.index == vec2(0.0)) return airData(v);
119 | vd.rgb = texture2D(tRGB, vd.index).rgb;
120 | vec4 rmet = texture2D(tRMET, vd.index);
121 | vd.roughness = rmet.r;
122 | vd.metalness = rmet.g;
123 | vd.emission = rmet.b;
124 | vd.transparent = rmet.a;
125 | vd.ri = texture2D(tRi, vd.index).r;
126 | return vd;
127 | }
128 |
129 | VoxelData intersectFloor(vec3 r0, vec3 r) {
130 | // NOTE: Assumes this ray actually hits the floor.
131 | vec3 v = floor(r0 + r * -r0.y/r.y);
132 | v.y = -1.0;
133 | return floorData(v);
134 | }
135 |
136 | float raySphereIntersect(vec3 r0, vec3 rd, vec3 s0, float sr) {
137 | float a = dot(rd, rd);
138 | vec3 s0_r0 = r0 - s0;
139 | float b = 2.0 * dot(rd, s0_r0);
140 | float c = dot(s0_r0, s0_r0) - (sr * sr);
141 | if (b*b - 4.0*a*c < 0.0) {
142 | return -1.0;
143 | }
144 | return (-b - sqrt((b*b) - 4.0*a*c))/(2.0*a);
145 | }
146 |
147 | vec3 skyColor(vec3 r0, vec3 r, float sunScale) {
148 | if (r.y < 0.0) {
149 | return vec3(0.0);
150 | }
151 | vec3 sky = textureCube(tSky, r).rgb;
152 | if (raySphereIntersect(r0, r, lightPosition, lightRadius) > 0.0) {
153 | sky += vec3(lightIntensity) * sunScale;
154 | }
155 | return sky;
156 | }
157 |
158 | bool intersect(vec3 r0, vec3 r, inout VoxelData vd) {
159 | float tBounds = 0.0;
160 | vec3 v = vec3(0.0);
161 | if (!inBounds(r0)) {
162 | if (!rayAABB(r0, r, vec3(0.0), bounds, tBounds)) {
163 | if (r.y >= 0.0) {
164 | return false;
165 | }
166 | vd = intersectFloor(r0, r);
167 | return true;
168 | }
169 | r0 = r0 + r * tBounds + r * epsilon;
170 | }
171 | v = floor(r0);
172 | vec3 stp = sign(r);
173 | vec3 tDelta = 1.0 / abs(r);
174 | vec3 tMax = step(0.0, r) * (1.0 - fract(r0)) + (1.0 - step(0.0, r)) * fract(r0);
175 | tMax = tMax/abs(r);
176 | for (int i = 0; i < 8192; i++) {
177 | if (!inBounds(v)) {
178 | if (r.y >= 0.0) {
179 | return false;
180 | }
181 | vd = intersectFloor(r0, r);
182 | return true;
183 | }
184 | vec2 lastIndex = vd.index;
185 | vd = voxelData(v);
186 | if (lastIndex != vd.index) {
187 | return true;
188 | }
189 | vec3 s = vec3(
190 | step(tMax.x, tMax.y) * step(tMax.x, tMax.z),
191 | step(tMax.y, tMax.x) * step(tMax.y, tMax.z),
192 | step(tMax.z, tMax.x) * step(tMax.z, tMax.y)
193 | );
194 | v += s * stp;
195 | tMax += s * tDelta;
196 | }
197 | return false;
198 | }
199 |
200 |
201 | void main() {
202 |
203 | vec4 src = texture2D(source, gl_FragCoord.xy/res);
204 |
205 | vec2 randOffset = vec2(0.0);
206 |
207 | // Recover NDC
208 | vec2 jitter = randUniform2(randOffset) - 0.5;
209 | vec4 ndc = vec4(
210 | 2.0 * (gl_FragCoord.xy + jitter) / res - 1.0,
211 | 2.0 * gl_FragCoord.z - 1.0,
212 | 1.0
213 | );
214 |
215 | // Calculate clip
216 | vec4 clip = invpv * ndc;
217 |
218 | // Calculate 3D position
219 | vec3 p3d = clip.xyz / clip.w;
220 |
221 | vec3 ray = normalize(p3d - eye);
222 | vec3 r0 = eye;
223 |
224 | float ddof = dofDist * length(bounds) + length(0.5 * bounds - eye) - length(bounds) * 0.5;
225 | vec3 tdof = r0 + ddof * ray;
226 | r0 += rand2Sphere(randOffset) * dofMag;
227 | ray = normalize(tdof - r0);
228 |
229 | vec3 mask = vec3(1.0);
230 | vec3 accm = vec3(0.0);
231 |
232 | VoxelData vd = airData(floor(r0));
233 |
234 | bool reflected = false;
235 | for (int b = 0; b < nBounces; b++) {
236 | bool refracted = false;
237 | float lastRi = vd.ri;
238 | if (intersect(r0, ray, vd)) {
239 | if (vd.emission > 0.0) {
240 | accm += mask * vd.emission * vd.rgb;
241 | break;
242 | }
243 | float tVoxel = 0.0;
244 | rayAABB(r0, ray, vd.xyz, vd.xyz + 1.0, tVoxel);
245 | vec3 r1 = r0 + tVoxel * ray;
246 | vec3 n = rayAABBNorm(r1, vd.xyz);
247 | vec3 m = normalize(n + rand3Sphere(randOffset) * vd.roughness);
248 | vec3 diffuse = normalize(m + rand2Sphere(randOffset));
249 | vec3 ref = reflect(ray, m);
250 | if (randUniform1(randOffset) <= vd.metalness) {
251 | // metallic
252 | ray = ref;
253 | reflected = true;
254 | mask *= vd.rgb;
255 | } else {
256 | // nonmetallic
257 | const float F0 = 0.0;
258 | float F = F0 + (1.0 - F0) * pow(1.0 - dot(-ray, n), 5.0);
259 | if (randUniform1(randOffset) <= F) {
260 | // reflect
261 | ray = ref;
262 | reflected = true;
263 | } else {
264 | // diffuse
265 | mask *= vd.rgb;
266 | if (randUniform1(randOffset) <= vd.transparent) {
267 | // attempt refraction
268 | ray = refract(ray, m, lastRi/vd.ri);
269 | if (ray != vec3(0.0)) {
270 | // refracted
271 | ray = normalize(ray);
272 | refracted = true;
273 | reflected = false;
274 | } else {
275 | // total internal refraction, use reflection.
276 | ray = ref;
277 | refracted = false;
278 | reflected = true;
279 | }
280 | } else {
281 | // diffuse reflection
282 | ray = diffuse;
283 | reflected = false;
284 | }
285 | }
286 | }
287 | if (!refracted && dot(ray, n) < 0.0) {
288 | accm = vec3(0.0);
289 | break;
290 | }
291 | r0 = r1 + ray * epsilon;
292 | vd = voxelData(floor(r0));
293 | if (ray == diffuse) {
294 | // Perform next event estimation when a diffuse bounce occurs.
295 | vec3 pLight = lightPosition + rand2Sphere(randOffset) * lightRadius;
296 | vec3 rLight = normalize(pLight - r0);
297 | VoxelData _vd;
298 | if (!intersect(r0, rLight, _vd)) {
299 | accm += mask * skyColor(r0, rLight, 0.5) * clamp(dot(rLight, m), 0.0, 1.0);
300 | }
301 | }
302 | } else {
303 | accm += mask * skyColor(r0, ray, b == 0 ? 1.0 : 0.0).rgb;
304 | break;
305 | }
306 | }
307 |
308 | gl_FragColor = vec4(accm, 1) + src;
309 | }
310 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Vixel
2 |
3 | A WebGL path tracing voxel renderer built with [regl](https://github.com/regl-project/regl).
4 |
5 | 
6 |
7 | 
8 |
9 |
10 |
11 |
12 |
13 | ## Live demo
14 | There's a voxel scene editor you can view [here](https://wwwtyro.github.io/vixel-editor/) that shows Vixel in action.
15 |
16 | ## Example
17 |
18 | ```js
19 | const Vixel = require("vixel");
20 |
21 | const canvas = document.createElement("canvas");
22 | document.body.appendChild(canvas);
23 |
24 | // Create a vixel object with a canvas and a width (x), height (y), and depth (z).
25 | const vixel = new Vixel(canvas, 1, 1, 1);
26 |
27 | vixel.camera(
28 | [2, 2, 2], // Camera position
29 | [0.5, 0.5, 0.5], // Camera target
30 | [0, 1, 0], // Up
31 | Math.PI / 4 // Field of view
32 | );
33 |
34 | vixel.set(
35 | 0, // x
36 | 0, // y
37 | 0, // z
38 | {
39 | red: 1, // Red component
40 | green: 0.5, // Green component
41 | blue: 0.25 // Blue component
42 | }
43 | );
44 |
45 | // Take 1024 path traced samples per pixel
46 | vixel.sample(1024);
47 |
48 | // Show the result on the canvas
49 | vixel.display();
50 | ```
51 |
52 | The code in the example above will result in the following image:
53 |
54 |
55 |
56 |
57 |
58 | ## Installation
59 |
60 | `npm i --save vixel`
61 |
62 | ## API
63 |
64 | `const Vixel = require('vixel')`
65 |
66 | ### Constructor
67 |
68 | #### `const vixel = new Vixel(canvas, width, height, depth)`
69 |
70 | Returns a new `Vixel` object.
71 |
72 | | Parameter | Type | Description |
73 | | --------- | ------------- | -------------------------------------------- |
74 | | canvas | Canvas object | The canvas to render to. |
75 | | width | integer | The width of the voxel grid (x-coordinate). |
76 | | height | integer | The height of the voxel grid (y-coordinate). |
77 | | depth | integer | The depth of the voxel grid (z-coordinate). |
78 |
79 | ### Methods
80 |
81 | Methods marked with an asterisk (`*`) will reset the renderer. This will clear the collected samples and require new samples to converge the scene.
82 |
83 | Methods marked with a double asterisk (`**`) will reset both the renderer _and_ require the voxel grid to be reuploaded to the GPU on the next call to
84 | `vixel.sample`. Reuploading can be slow depending on the size of the voxel grid.
85 |
86 | #### `vixel.set(x, y, z, options)**`
87 |
88 | Sets a voxel at coordinates `x, y, z` with configuration `options`.
89 |
90 | | Parameter | Type | Description |
91 | | --------- | ------- | ------------------------------------------------ |
92 | | x | integer | The x-coordinate of the voxel. |
93 | | y | integer | The y-coordinate of the voxel. |
94 | | z | integer | The z-coordinate of the voxel. |
95 | | options | Object | Configuration of the voxel. See `options` below. |
96 |
97 | ##### `options`
98 |
99 | | Option | Type | Default | Description |
100 | | ----------- | ----- | ------- | ------------------------------------------------------------------------------------------------------------------------ |
101 | | red | float | 1.0 | The red component of the voxel color. |
102 | | green | float | 1.0 | The green component of the voxel color. |
103 | | blue | float | 1.0 | The blue component of the voxel color. |
104 | | rough | float | 1.0 | The roughness of the voxel surface. Zero is perfectly smooth, one is completely rough. |
105 | | metal | float | 0.0 | The metalness of the voxel surface. Zero is completely nonmetallic, one is fully metallic. |
106 | | transparent | float | 0.0 | The transparency of the voxel. Zero is completely opaque, one is completely clear. |
107 | | refract | float | 1.0 | The refraction index of the voxel. Air has a value of 1.0, glass is around 1.333. |
108 | | emit | float | 0.0 | The amount of light the voxel emits. If this is nonzero, `rough`, `metal`, `transparent`, and `refract` will be ignored. |
109 |
110 | #### `vixel.unset(x, y, z)**`
111 |
112 | Unsets the voxel at coordinates `x, y, z`.
113 |
114 | #### `vixel.get(x, y, z)`
115 |
116 | Returns the configuration of the voxel at coordinates `x, y, z`. See the `options` definition of `vixel.set` for information about the returned configuration
117 | object. Returns `undefined` if the voxel at the given coordinates is not set.
118 |
119 | | Parameter | Type | Description |
120 | | --------- | ------- | ------------------------------ |
121 | | x | integer | The x-coordinate of the voxel. |
122 | | y | integer | The y-coordinate of the voxel. |
123 | | z | integer | The z-coordinate of the voxel. |
124 |
125 | #### `vixel.clear()**`
126 |
127 | Clears the voxel grid.
128 |
129 | #### `vixel.camera(eye, center, up, fov)*`
130 |
131 | Configures the camera.
132 |
133 | | Parameter | Type | Description |
134 | | --------- | --------------------- | ---------------------------------------------------- |
135 | | eye | float array [x, y, z] | The position of the camera. |
136 | | center | float array [x, y, z] | The point the camera is facing. |
137 | | up | float array [x, y, z] | The up direction. Typically [0, 1, 0]. |
138 | | fov | float | The field of view in radians. Typically around PI/3. |
139 |
140 | #### `vixel.ground(color, rough, metal)*`
141 |
142 | Configures the ground. Cannot currently be disabled.
143 |
144 | | Parameter | Type | Description |
145 | | --------- | ------------------------------ | ----------------------------------------------------------------------------------- |
146 | | color | float array [red, green, blue] | The red, green, and blue color components of the ground. |
147 | | rough | float | The roughness of the ground. Zero is perfectly smooth, one is completely rough. |
148 | | metal | float | The metalness of the ground. Zero is completely nonmetallic, one is fully metallic. |
149 |
150 | #### `vixel.sun(time, azimuth, radius, intensity)*`
151 |
152 | Configures the sky and sun.
153 |
154 | | Parameter | Type | Description |
155 | | --------- | ----- | ----------------------------------------------------------------------------------------------- |
156 | | time | float | Time of day in hours, 0 to 24. 6.0 is sunrise, 18.0 is sunset. |
157 | | azimuth | float | The angle of the sun on the horizon in radians. 0.0 places the sun in the positive-x direction. |
158 | | radius | float | The radius of the sun. 1.0 represents a physically-accurate sun radius. |
159 | | intensity | float | The intensity of sunlight. |
160 |
161 | #### `vixel.dof(distance, magnitude)*`
162 |
163 | Configures depth of field.
164 |
165 | | Parameter | Type | Description |
166 | | --------- | ----- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
167 | | distance | float | The distance at which the focus plane exists, in terms of the size of the voxel grid. Zero is the nearest point, 0.5 is halfway through the grid, one is at the far side of the grid. |
168 | | magnitude | float | The magnitude of the depth of field effect. Zero disables the effect. |
169 |
170 | #### `vixel.sample(count)`
171 |
172 | Takes `count` path traced samples of the scene.
173 |
174 | #### `vixel.display()`
175 |
176 | Displays the scene sampled with `vixel.sample` on the `Canvas` provided to the constructor.
177 |
178 | #### `vixel.serialize()`
179 |
180 | Returns an object representing the populated voxel grid that can be deserialized with the `vixel.deserialize` function.
181 |
182 | #### `vixel.deserialize(data)`
183 |
184 | Populates the voxel grid with `data` from the `vixel.serialize` function.
185 |
186 | ### Members
187 |
188 | #### `vixel.sampleCount`
189 |
190 | The number of samples that have been collected since the last time the renderer was reset.
191 |
--------------------------------------------------------------------------------