├── .gitignore
├── LICENSE.md
├── README.md
├── index.html
├── media
├── material-000.png
├── material-001.png
├── material-002.png
├── material-003.png
├── screenshot-000.png
└── screenshot-001.png
├── package-lock.json
├── package.json
├── src
├── camera.js
├── glsl
│ ├── display.frag
│ ├── display.vert
│ ├── frag.frag
│ ├── frag.vert
│ ├── sample.frag
│ └── sample.vert
├── index.js
├── pingpong.js
├── render.js
├── stage.js
└── voxel-index.js
└── static
└── bundle.js
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
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 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Vixel
2 |
3 | A javascript & webgl voxel path tracer. See it live [here](https://wwwtyro.github.io/vixel-editor).
4 |
5 | 
6 |
7 | 
8 |
9 | ## Materials
10 |
11 | - **Color** is the material's base color.
12 |
13 | - **Roughness** describes how randomly specular light is reflected from the surface.
14 |
15 | - **Metalness** describes how much of the light it reflects is diffusive. A purely metallic surface reflects zero light diffusively.
16 |
17 | - **Emission** is how much light the material emits. If this value is greater than zero, only the **color** component of the material is used.
18 |
19 | | Roughness | Metalness | Real world analogue | Rendered example |
20 | | --------- | --------- | ------------------- | -------------------------------- |
21 | | 0.0 | 0.0 | Smooth plastic |  |
22 | | 1.0 | 0.0 | Chalk |  |
23 | | 0.0 | 1.0 | Mirror |  |
24 | | 1.0 | 0.05 | Unpolished metal |  |
25 |
26 | ## Ground
27 |
28 | The **color**, **roughness**, and **metalness** properties can also be set for the ground plane, and are identical in meaning.
29 |
30 | ## Sky
31 |
32 | - **Time** is simply the time of day on a 24-hour clock. The sun rises at 6:00 and sets at 18:00.
33 |
34 | - **Azimuth** is the direction of the sun _around_ the up/down axis.
35 |
36 | ## Rendering
37 |
38 | - **Width** and **Height** define the resolution of your rendered image.
39 |
40 | - **DOF Distance** is how far into your scene the focus plane lies.
41 |
42 | - **DOF Magnitude** is how strong the DOF effect is.
43 |
44 | - **Samples/Frame** describes how many samples are taken per frame. `1` is one sample per pixel, per frame. If the interactivity of the editor is slow or
45 | choppy, you can reduce this to improve your framerate. Similarly, if you want to converge the scene faster, you can increase it (though increasing it is only
46 | effective until you're GPU bound).
47 |
48 | - **Take Screenshot** will download a screenshot.
49 |
50 | ## Scene
51 |
52 | - **Copy URL** copies the current scene to the clipboard and updates the URL. For now, this is the only way to save and share your scene. Feel free to paste it
53 | into a text file to save it longer term. Yes, there are absolutely plans to improve this.
54 |
55 | - **Clear Scene** clears the scene of all voxels save one.
56 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
✖
89 |
90 | Controls
91 |
92 |
93 | Left mouse button | Place voxel |
94 | Shift + left mouse button | Remove voxel |
95 | Control + left mouse button | Copy voxel |
96 | Right mouse button + drag | Rotate camera |
97 | Mouse wheel | Zoom |
98 | H key | Hide/show controls |
99 |
100 |
101 |
More information
102 |
103 | help
104 |
105 |
106 |
--------------------------------------------------------------------------------
/media/material-000.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wwwtyro/vixel-editor/337d22e584b522a2cdb094bee975ab93d4fa5eb0/media/material-000.png
--------------------------------------------------------------------------------
/media/material-001.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wwwtyro/vixel-editor/337d22e584b522a2cdb094bee975ab93d4fa5eb0/media/material-001.png
--------------------------------------------------------------------------------
/media/material-002.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wwwtyro/vixel-editor/337d22e584b522a2cdb094bee975ab93d4fa5eb0/media/material-002.png
--------------------------------------------------------------------------------
/media/material-003.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wwwtyro/vixel-editor/337d22e584b522a2cdb094bee975ab93d4fa5eb0/media/material-003.png
--------------------------------------------------------------------------------
/media/screenshot-000.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wwwtyro/vixel-editor/337d22e584b522a2cdb094bee975ab93d4fa5eb0/media/screenshot-000.png
--------------------------------------------------------------------------------
/media/screenshot-001.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wwwtyro/vixel-editor/337d22e584b522a2cdb094bee975ab93d4fa5eb0/media/screenshot-001.png
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "vixel",
3 | "version": "1.0.0",
4 | "description": "A javascript & webgl voxel path tracer.",
5 | "main": "src/index.js",
6 | "scripts": {
7 | "start": "budo src/index.js:static/bundle.js --live -- -t glslify",
8 | "build": "browserify -t glslify src/index.js | uglifyjs > static/bundle.js"
9 | },
10 | "keywords": [
11 | "webgl",
12 | "voxel",
13 | "path tracing",
14 | "ray tracing"
15 | ],
16 | "author": "Rye Terrell",
17 | "license": "Unlicense",
18 | "dependencies": {
19 | "camera-picking-ray": "^1.0.1",
20 | "copy-to-clipboard": "^3.0.8",
21 | "dat.gui": "^0.7.3",
22 | "download-canvas": "^1.0.2",
23 | "gl-matrix": "^2.8.1",
24 | "jcb64": "^1.1.5",
25 | "regl": "^1.3.9",
26 | "regl-atmosphere-envmap": "^1.0.1"
27 | },
28 | "devDependencies": {
29 | "budo": "^11.5.0",
30 | "glslify": "^7.0.0",
31 | "uglify-es": "^3.3.9"
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/src/camera.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | const { mat4, vec3 } = require("gl-matrix");
4 |
5 | module.exports = class TrackballCamera {
6 | constructor(domElement) {
7 | this._domElement = domElement;
8 | this._rotation = mat4.create();
9 | this.fov = Math.PI / 6;
10 | this.center = [0, 0, 0];
11 | this.radius = 10.0;
12 | this.near = 0.1;
13 | this.far = 1000;
14 | }
15 |
16 | rotate(dx, dy) {
17 | mat4.rotateY(this._rotation, this._rotation, -dx);
18 | mat4.rotateX(this._rotation, this._rotation, -dy);
19 | }
20 |
21 | up() {
22 | const u = [0, 1, 0];
23 | vec3.transformMat4(u, u, this._rotation);
24 | return u;
25 | }
26 |
27 | right() {
28 | const r = [1, 0, 0];
29 | vec3.transformMat4(r, r, this._rotation);
30 | return r;
31 | }
32 |
33 | eye() {
34 | const e = [0, 0, this.radius];
35 | vec3.transformMat4(e, e, this._rotation);
36 | vec3.add(e, e, this.center);
37 | return e;
38 | }
39 |
40 | view() {
41 | const up = [0, 1, 0];
42 | vec3.transformMat4(up, up, this._rotation);
43 | const e = this.eye();
44 | return mat4.lookAt([], e, this.center, up);
45 | }
46 |
47 | projection() {
48 | return mat4.perspective(
49 | [],
50 | this.fov,
51 | this._domElement.clientWidth / this._domElement.clientHeight,
52 | this.near,
53 | this.far
54 | );
55 | }
56 |
57 | invpv() {
58 | const v = this.view();
59 | const p = this.projection();
60 | const pv = mat4.multiply([], p, v);
61 | return mat4.invert([], pv);
62 | }
63 |
64 | serialize() {
65 | return {
66 | version: 0,
67 | rotation: this._rotation,
68 | fov: this.fov,
69 | center: this.center,
70 | radius: this.radius,
71 | near: this.near,
72 | far: this.far
73 | };
74 | }
75 |
76 | deserialize(data) {
77 | this._rotation = data.rotation;
78 | this.fov = data.fov;
79 | this.center = data.center;
80 | this.radius = data.radius;
81 | this.near = data.near;
82 | this.far = data.far;
83 | }
84 | };
85 |
--------------------------------------------------------------------------------
/src/glsl/display.frag:
--------------------------------------------------------------------------------
1 | precision highp float;
2 |
3 | uniform sampler2D source, preview, tUniform1;
4 | uniform vec2 tUniform1Res;
5 | uniform float fraction;
6 |
7 | varying vec2 vPos;
8 |
9 | void main() {
10 | vec4 src = texture2D(source, vPos);
11 | vec4 prv = texture2D(preview, vPos);
12 | vec3 color = mix(prv.rgb, src.rgb/max(src.a, 1.0), fraction);
13 | color = pow(color, vec3(1.0/2.2));
14 | float r = texture2D(tUniform1, gl_FragCoord.xy/tUniform1Res).r;
15 | color += mix(-0.5/255.0, 0.5/255.0, r);
16 | gl_FragColor = vec4(color, 1);
17 | }
18 |
--------------------------------------------------------------------------------
/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/frag.frag:
--------------------------------------------------------------------------------
1 | precision highp float;
2 |
3 | uniform sampler2D tUniform2;
4 | uniform vec2 resFrag, resTarget, randOffset;
5 | uniform float resRand;
6 |
7 | void main() {
8 | vec2 p0 = floor(gl_FragCoord.xy)/resFrag;
9 | vec2 r = texture2D(tUniform2, randOffset + gl_FragCoord.xy/resRand).ra;
10 | vec2 pr = p0 + r/resFrag;
11 | vec2 fc = floor(pr * resTarget);
12 | gl_FragColor = vec4(fc + 0.5, 1, 1);
13 | }
14 |
--------------------------------------------------------------------------------
/src/glsl/frag.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/sample.frag:
--------------------------------------------------------------------------------
1 | precision highp float;
2 | uniform 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 | uniform bool renderPreview;
9 |
10 | const float epsilon = 0.0001;
11 | const int nBounces = 5;
12 |
13 | float randUniform1(inout vec2 randOffset) {
14 | float r = texture2D(tUniform1, randOffset + tOffset).r;
15 | randOffset += r;
16 | return r;
17 | }
18 |
19 | vec2 randUniform2(inout vec2 randOffset) {
20 | vec2 r = texture2D(tUniform2, randOffset + tOffset).ra;
21 | randOffset += r;
22 | return r;
23 | }
24 |
25 |
26 | vec3 rand2Sphere(inout vec2 randOffset) {
27 | vec3 r = texture2D(t2Sphere, randOffset + tOffset).xyz;
28 | randOffset += r.xy;
29 | return r;
30 | }
31 |
32 | vec3 rand3Sphere(inout vec2 randOffset) {
33 | vec3 r = texture2D(t3Sphere, randOffset + tOffset).xyz;
34 | randOffset += r.xy;
35 | return r;
36 | }
37 |
38 | bool inBounds(vec3 p) {
39 | return all(greaterThanEqual(p, vec3(0.0))) && all(lessThan(p, bounds));
40 | }
41 |
42 | bool rayAABB(vec3 origin, vec3 direction, vec3 bMin, vec3 bMax, out float t0) {
43 | vec3 invDir = 1.0 / direction;
44 | vec3 omin = (bMin - origin) * invDir;
45 | vec3 omax = (bMax - origin) * invDir;
46 | vec3 imax = max(omax, omin);
47 | vec3 imin = min(omax, omin);
48 | float t1 = min(imax.x, min(imax.y, imax.z));
49 | t0 = max(imin.x, max(imin.y, imin.z));
50 | t0 = max(t0, 0.0);
51 | return t1 > t0;
52 | }
53 |
54 | vec3 rayAABBNorm(vec3 p, vec3 v) {
55 | vec3 d = p - (v + 0.5);
56 | vec3 dabs = abs(d);
57 | if (dabs.x > dabs.y) {
58 | if (dabs.x > dabs.z) {
59 | return vec3(sign(d.x), 0.0, 0.0);
60 | } else {
61 | return vec3(0, 0, sign(d.z));
62 | }
63 | } else {
64 | if (dabs.y > dabs.z) {
65 | return vec3(0.0, sign(d.y), 0.0);
66 | } else {
67 | return vec3(0.0, 0.0, sign(d.z));
68 | }
69 | }
70 | }
71 |
72 | vec2 samplePoint(vec3 v) {
73 | float invResStage = 1.0 / resStage;
74 | float i = v.y * bounds.x * bounds.z + v.z * bounds.x + v.x;
75 | i = i * invResStage;
76 | float y = floor(i);
77 | float x = fract(i) * resStage;
78 | x = (x + 0.5) * invResStage;
79 | y = (y + 0.5) * invResStage;
80 | return vec2(x, y);
81 | }
82 |
83 |
84 | struct VoxelData {
85 | vec3 xyz;
86 | vec3 rgb;
87 | vec2 index;
88 | float roughness;
89 | float metalness;
90 | float emission;
91 | float transparent;
92 | float ri;
93 | };
94 |
95 | VoxelData floorData(vec3 v) {
96 | return VoxelData(v, groundColor, vec2(1.0/255.0, 0.0), groundRoughness, groundMetalness, 0.0, 0.0, 1.0);
97 | }
98 |
99 | VoxelData airData(vec3 v) {
100 | return VoxelData(v, vec3(1.0), vec2(0.0), 0.0, 0.0, 0.0, 1.0, 1.0);
101 | }
102 |
103 | VoxelData voxelData(vec3 v) {
104 | VoxelData vd;
105 | vd.xyz = v;
106 | if (v.y == -1.0) {
107 | return floorData(v);
108 | }
109 | if (!inBounds(v)) {
110 | return airData(v);
111 | }
112 | vec2 s = samplePoint(v);
113 | vd.index = texture2D(tIndex, s).ra;
114 | if (vd.index == vec2(0.0)) return airData(v);
115 | vd.rgb = texture2D(tRGB, vd.index).rgb;
116 | vec4 rmet = texture2D(tRMET, vd.index);
117 | vd.roughness = rmet.r;
118 | vd.metalness = rmet.g;
119 | vd.emission = rmet.b;
120 | vd.transparent = rmet.a;
121 | vd.ri = texture2D(tRi, vd.index).r;
122 | return vd;
123 | }
124 |
125 | VoxelData intersectFloor(vec3 r0, vec3 r) {
126 | // NOTE: Assumes this ray actually hits the floor.
127 | vec3 v = floor(r0 + r * -r0.y/r.y);
128 | v.y = -1.0;
129 | return floorData(v);
130 | }
131 |
132 | float raySphereIntersect(vec3 r0, vec3 rd, vec3 s0, float sr) {
133 | float a = dot(rd, rd);
134 | vec3 s0_r0 = r0 - s0;
135 | float b = 2.0 * dot(rd, s0_r0);
136 | float c = dot(s0_r0, s0_r0) - (sr * sr);
137 | if (b*b - 4.0*a*c < 0.0) {
138 | return -1.0;
139 | }
140 | return (-b - sqrt((b*b) - 4.0*a*c))/(2.0*a);
141 | }
142 |
143 | vec3 skyColor(vec3 r0, vec3 r, float sunScale) {
144 | if (r.y < 0.0) {
145 | return vec3(0.0);
146 | }
147 | vec3 sky = textureCube(tSky, r).rgb;
148 | if (raySphereIntersect(r0, r, lightPosition, lightRadius) > 0.0) {
149 | sky += vec3(lightIntensity) * sunScale;
150 | }
151 | return sky;
152 | }
153 |
154 | bool intersect(vec3 r0, vec3 r, inout VoxelData vd) {
155 | float tBounds = 0.0;
156 | vec3 v = vec3(0.0);
157 | if (!inBounds(r0)) {
158 | if (!rayAABB(r0, r, vec3(0.0), bounds, tBounds)) {
159 | if (r.y >= 0.0) {
160 | return false;
161 | }
162 | vd = intersectFloor(r0, r);
163 | return true;
164 | }
165 | r0 = r0 + r * tBounds + r * epsilon;
166 | }
167 | v = floor(r0);
168 | vec3 stp = sign(r);
169 | vec3 tDelta = 1.0 / abs(r);
170 | vec3 tMax = step(0.0, r) * (1.0 - fract(r0)) + (1.0 - step(0.0, r)) * fract(r0);
171 | tMax = tMax/abs(r);
172 | for (int i = 0; i < 8192; i++) {
173 | if (!inBounds(v)) {
174 | if (r.y >= 0.0) {
175 | return false;
176 | }
177 | vd = intersectFloor(r0, r);
178 | return true;
179 | }
180 | vec2 lastIndex = vd.index;
181 | vd = voxelData(v);
182 | if (lastIndex != vd.index) {
183 | return true;
184 | }
185 | vec3 s = vec3(
186 | step(tMax.x, tMax.y) * step(tMax.x, tMax.z),
187 | step(tMax.y, tMax.x) * step(tMax.y, tMax.z),
188 | step(tMax.z, tMax.x) * step(tMax.z, tMax.y)
189 | );
190 | v += s * stp;
191 | tMax += s * tDelta;
192 | }
193 | return false;
194 | }
195 |
196 |
197 | vec3 preview() {
198 | vec4 ndc = vec4(
199 | 2.0 * gl_FragCoord.xy / res - 1.0,
200 | 2.0 * gl_FragCoord.z - 1.0,
201 | 1.0
202 | );
203 | vec4 clip = invpv * ndc;
204 | vec3 p3d = clip.xyz / clip.w;
205 | vec3 ray = normalize(p3d - eye);
206 | vec3 r0 = eye;
207 |
208 | VoxelData vd = airData(floor(r0));
209 |
210 | if (intersect(r0, ray, vd)) {
211 | if (vd.emission > 0.0) {
212 | return vd.rgb;
213 | }
214 | float tVoxel = 0.0;
215 | rayAABB(r0, ray, vd.xyz, vd.xyz + 1.0, tVoxel);
216 | vec3 r1 = r0 + tVoxel * ray;
217 | vec3 n = rayAABBNorm(r1, vd.xyz);
218 | vec3 rLight = normalize(lightPosition - r1);
219 | vec3 color = vd.rgb * (skyColor(r1, n, 0.0) + 0.25);
220 | r1 -= ray * epsilon;
221 | vd = voxelData(floor(r1));
222 | if (intersect(r1, rLight, vd)) {
223 | if (vd.xyz.y != -1.0) {
224 | color *= 0.5;
225 | }
226 | }
227 | return color;
228 | }
229 | return skyColor(r0, ray, 1.0);
230 | }
231 |
232 |
233 | void main() {
234 |
235 | vec4 src = texture2D(source, gl_FragCoord.xy/res);
236 |
237 | if (renderPreview) {
238 | gl_FragColor = vec4(preview(), 1) + src;
239 | return;
240 | }
241 |
242 | vec2 randOffset = vec2(0.0);
243 |
244 | // Recover NDC
245 | vec2 jitter = randUniform2(randOffset) - 0.5;
246 | vec4 ndc = vec4(
247 | 2.0 * (gl_FragCoord.xy + jitter) / res - 1.0,
248 | 2.0 * gl_FragCoord.z - 1.0,
249 | 1.0
250 | );
251 |
252 | // Calculate clip
253 | vec4 clip = invpv * ndc;
254 |
255 | // Calculate 3D position
256 | vec3 p3d = clip.xyz / clip.w;
257 |
258 | vec3 ray = normalize(p3d - eye);
259 | vec3 r0 = eye;
260 |
261 | float ddof = dofDist * length(bounds) + length(0.5 * bounds - eye) - length(bounds) * 0.5;
262 | vec3 tdof = r0 + ddof * ray;
263 | r0 += rand2Sphere(randOffset) * dofMag;
264 | ray = normalize(tdof - r0);
265 |
266 | vec3 mask = vec3(1.0);
267 | vec3 accm = vec3(0.0);
268 |
269 | VoxelData vd = airData(floor(r0));
270 |
271 | bool reflected = false;
272 | for (int b = 0; b < nBounces; b++) {
273 | bool refracted = false;
274 | float lastRi = vd.ri;
275 | if (intersect(r0, ray, vd)) {
276 | if (vd.emission > 0.0) {
277 | accm += mask * vd.emission * vd.rgb;
278 | break;
279 | }
280 | float tVoxel = 0.0;
281 | rayAABB(r0, ray, vd.xyz, vd.xyz + 1.0, tVoxel);
282 | vec3 r1 = r0 + tVoxel * ray;
283 | vec3 n = rayAABBNorm(r1, vd.xyz);
284 | vec3 m = normalize(n + rand3Sphere(randOffset) * vd.roughness);
285 | vec3 diffuse = normalize(m + rand2Sphere(randOffset));
286 | vec3 ref = reflect(ray, m);
287 | if (randUniform1(randOffset) <= vd.metalness) {
288 | // metallic
289 | ray = ref;
290 | reflected = true;
291 | mask *= vd.rgb;
292 | } else {
293 | // nonmetallic
294 | const float F0 = 0.0;
295 | float F = F0 + (1.0 - F0) * pow(1.0 - dot(-ray, n), 5.0);
296 | if (randUniform1(randOffset) <= F) {
297 | // reflect
298 | ray = ref;
299 | reflected = true;
300 | } else {
301 | // diffuse
302 | mask *= vd.rgb;
303 | if (randUniform1(randOffset) <= vd.transparent) {
304 | // attempt refraction
305 | ray = refract(ray, m, lastRi/vd.ri);
306 | if (ray != vec3(0.0)) {
307 | // refracted
308 | ray = normalize(ray);
309 | refracted = true;
310 | reflected = false;
311 | } else {
312 | // total internal refraction, use reflection.
313 | ray = ref;
314 | refracted = false;
315 | reflected = true;
316 | }
317 | } else {
318 | // diffuse reflection
319 | ray = diffuse;
320 | reflected = false;
321 | }
322 | }
323 | }
324 | if (!refracted && dot(ray, n) < 0.0) {
325 | accm = vec3(0.0);
326 | break;
327 | }
328 | r0 = r1 + ray * epsilon;
329 | vd = voxelData(floor(r0));
330 | if (ray == diffuse) {
331 | // Perform next event estimation when a diffuse bounce occurs.
332 | vec3 pLight = lightPosition + rand2Sphere(randOffset) * lightRadius;
333 | vec3 rLight = normalize(pLight - r0);
334 | VoxelData _vd;
335 | if (!intersect(r0, rLight, _vd)) {
336 | accm += mask * skyColor(r0, rLight, 0.5) * clamp(dot(rLight, m), 0.0, 1.0);
337 | }
338 | }
339 | } else {
340 | accm += mask * skyColor(r0, ray, b == 0 ? 1.0 : 0.0).rgb;
341 | break;
342 | }
343 | }
344 |
345 | gl_FragColor = vec4(accm, 1) + src;
346 | }
347 |
--------------------------------------------------------------------------------
/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/index.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | const { mat4, vec3, vec2 } = require("gl-matrix");
4 | const dat = require("dat.gui");
5 | const pick = require("camera-picking-ray");
6 | const downloadCanvas = require("download-canvas").downloadCanvas;
7 | const clip = require("copy-to-clipboard");
8 | const jcb64 = require("jcb64");
9 |
10 | const Renderer = require("./render");
11 | const Stage = require("./stage");
12 | const Camera = require("./camera");
13 |
14 | const canvas = document.getElementById("render-canvas");
15 | canvas.width = canvas.clientWidth;
16 | canvas.height = canvas.clientHeight;
17 |
18 | const renderer = Renderer(canvas);
19 | const stage = new Stage(renderer.context);
20 | const camera = new Camera(canvas);
21 |
22 | camera.rotate(1, 0.2);
23 |
24 | const mouse = {
25 | left: false,
26 | right: false,
27 | x: null,
28 | y: null
29 | };
30 |
31 | function toggleHelp() {
32 | const h = document.getElementById("help");
33 | const hb = document.getElementById("help-button");
34 | if (hb.style.display === "none") {
35 | hb.style.display = "inline";
36 | h.style.display = "none";
37 | } else {
38 | hb.style.display = "none";
39 | h.style.display = "inline";
40 | }
41 | }
42 |
43 | toggleHelp();
44 |
45 | document.getElementById("help").addEventListener("click", toggleHelp);
46 | document.getElementById("help-button").addEventListener("click", toggleHelp);
47 |
48 | window.addEventListener("contextmenu", e => {
49 | e.preventDefault();
50 | return false;
51 | });
52 |
53 | canvas.addEventListener("mousedown", e => {
54 | if (e.button === 0) {
55 | mouse.left = true;
56 | mouse.x = e.clientX;
57 | mouse.y = e.clientY;
58 | const b = stage.bounds;
59 | camera.center = [
60 | b.width / 2 + b.min.x,
61 | b.height / 2 + b.min.y,
62 | b.depth / 2 + b.min.z
63 | ];
64 | camera.radius = vec3.length([b.width, b.height, b.depth]) * 1.5;
65 | const r = [];
66 | const r0 = [];
67 | pick(
68 | r0,
69 | r,
70 | [
71 | (canvas.width * e.offsetX) / canvas.clientWidth,
72 | (canvas.height * e.offsetY) / canvas.clientHeight
73 | ],
74 | [0, 0, canvas.width, canvas.height],
75 | camera.invpv()
76 | );
77 | const v = stage.intersect(r0, r);
78 | if (v === undefined && r[1] < 0 && r0[1] > b.min.y) {
79 | if (!e.shiftKey && !e.ctrlKey) {
80 | const p = vec3.add(
81 | [],
82 | r0,
83 | vec3.scale([], r, -(r0[1] - b.min.y) / r[1] - 0.001)
84 | );
85 | let n = p.map(Math.floor);
86 | stage.set(
87 | n[0],
88 | n[1],
89 | n[2],
90 | controls.color[0] / 255,
91 | controls.color[1] / 255,
92 | controls.color[2] / 255,
93 | controls.roughness,
94 | controls.metalness,
95 | controls.emission,
96 | controls.transparent,
97 | controls.ri
98 | );
99 | stage.update();
100 | renderer.reset();
101 | }
102 | } else if (v !== undefined) {
103 | if (e.shiftKey) {
104 | stage.unset(v.voxel[0], v.voxel[1], v.voxel[2]);
105 | stage.update();
106 | renderer.reset();
107 | } else if (e.ctrlKey) {
108 | const vd = stage.get(v.voxel[0], v.voxel[1], v.voxel[2]);
109 | controls.roughness = vd.rough;
110 | controls.metalness = vd.metal;
111 | controls.emission = vd.emit;
112 | controls.color = [vd.red, vd.green, vd.blue];
113 | controls.transparent = vd.transparent;
114 | controls.ri = vd.ri;
115 | gui.updateDisplay();
116 | } else {
117 | const p = vec3.add([], r0, vec3.scale([], r, v.t - 0.001));
118 | let n = p.map(Math.floor);
119 | stage.set(
120 | n[0],
121 | n[1],
122 | n[2],
123 | controls.color[0] / 255,
124 | controls.color[1] / 255,
125 | controls.color[2] / 255,
126 | controls.roughness,
127 | controls.metalness,
128 | controls.emission,
129 | controls.transparent,
130 | controls.ri
131 | );
132 | stage.update();
133 | renderer.reset();
134 | }
135 | }
136 | }
137 | if (e.button === 2) {
138 | mouse.right = true;
139 | mouse.x = e.clientX;
140 | mouse.y = e.clientY;
141 | }
142 | });
143 |
144 | window.addEventListener("mouseup", e => {
145 | if (e.button === 0) {
146 | mouse.left = false;
147 | }
148 | if (e.button === 2) {
149 | mouse.right = false;
150 | }
151 | });
152 |
153 | window.addEventListener("mousemove", e => {
154 | if (!mouse.right) return;
155 | const dx = e.clientX - mouse.x;
156 | const dy = e.clientY - mouse.y;
157 | mouse.x = e.clientX;
158 | mouse.y = e.clientY;
159 | camera.rotate(dx * 0.003, dy * 0.003);
160 | renderer.reset();
161 | });
162 |
163 | window.addEventListener("wheel", e => {
164 | camera.fov *= 1 + Math.sign(e.deltaY) * 0.1;
165 | camera.fov = Math.max(Math.PI / 32, Math.min(Math.PI / 1.1, camera.fov));
166 | renderer.reset();
167 | });
168 |
169 | const controls = new function() {
170 | this.color = [255, 255, 255];
171 | this.roughness = 0.0;
172 | this.metalness = 0.0;
173 | this.emission = 0.0;
174 | this.transparent = 0.0;
175 | this.ri = 1.0;
176 | this.groundColor = [80, 80, 80];
177 | this.groundRoughness = 1;
178 | this.groundMetalness = 0.0;
179 | this.time = 6.1;
180 | this.azimuth = 0.0;
181 | this.lightRadius = 8.0;
182 | this.lightIntensity = 1.0;
183 | this.width = 1280;
184 | this.height = 720;
185 | this.dofDist = 0.5;
186 | this.dofMag = 0.0;
187 | this.autoSample = true;
188 | this.samplesPerFrame = 1;
189 | this.screenshot = function() {
190 | downloadCanvas("render-canvas", {
191 | name: "voxel",
192 | type: "png",
193 | quality: 1
194 | });
195 | };
196 | this.save = function() {
197 | const hash = `#${pack()}`;
198 | clip(location.href + hash);
199 | location.hash = hash;
200 | };
201 | this.clear = function() {
202 | stage.clear();
203 | stage.set(0, 0, 0, 0.5, 0.5, 0.5, 0, 0, 0, 0, 1);
204 | stage.update();
205 | renderer.reset();
206 | };
207 | }();
208 |
209 | const gui = new dat.GUI();
210 |
211 | gui.fMaterial = gui.addFolder("Material");
212 | gui.fGround = gui.addFolder("Ground");
213 | gui.fSky = gui.addFolder("Sky");
214 | gui.fRender = gui.addFolder("Rendering");
215 | gui.fScene = gui.addFolder("Scene");
216 |
217 | gui.fMaterial.open();
218 | gui.fSky.open();
219 | gui.fGround.open();
220 | gui.fRender.open();
221 | gui.fScene.open();
222 |
223 | gui.fMaterial.addColor(controls, "color").name("Color");
224 | gui.fMaterial
225 | .add(controls, "roughness")
226 | .name("Roughness")
227 | .min(0.0)
228 | .max(1.0)
229 | .step(0.01);
230 | gui.fMaterial
231 | .add(controls, "metalness")
232 | .name("Metalness")
233 | .min(0.0)
234 | .max(1.0)
235 | .step(0.01);
236 | gui.fMaterial
237 | .add(controls, "transparent")
238 | .name("Transparency")
239 | .min(0.0)
240 | .max(1.0)
241 | .step(0.01);
242 | gui.fMaterial
243 | .add(controls, "ri")
244 | .name("Refractive Index")
245 | .min(1.0)
246 | .max(3.0)
247 | .step(0.01);
248 | gui.fMaterial
249 | .add(controls, "emission")
250 | .name("Emission")
251 | .min(0.0)
252 | .step(0.1);
253 |
254 | gui.fGround
255 | .addColor(controls, "groundColor")
256 | .name("Color")
257 | .onChange(renderer.reset);
258 | gui.fGround
259 | .add(controls, "groundRoughness")
260 | .name("Roughness")
261 | .min(0.0)
262 | .max(1.0)
263 | .step(0.01)
264 | .onChange(renderer.reset);
265 | gui.fGround
266 | .add(controls, "groundMetalness")
267 | .name("Metalness")
268 | .min(0.0)
269 | .max(1.0)
270 | .step(0.01)
271 | .onChange(renderer.reset);
272 |
273 | gui.fSky
274 | .add(controls, "time")
275 | .name("Time")
276 | .min(0.0)
277 | .max(24.0)
278 | .step(0.01)
279 | .onChange(function() {
280 | renderer.reset();
281 | });
282 | gui.fSky
283 | .add(controls, "azimuth")
284 | .name("Azimuth")
285 | .min(0.0)
286 | .max(2 * Math.PI)
287 | .step(0.01)
288 | .onChange(function() {
289 | renderer.reset();
290 | });
291 | gui.fSky
292 | .add(controls, "lightRadius")
293 | .name("Sun Radius")
294 | .min(0.0)
295 | .onChange(function() {
296 | renderer.reset();
297 | });
298 | gui.fSky
299 | .add(controls, "lightIntensity")
300 | .name("Sun Intensity")
301 | .min(0.0)
302 | .onChange(function() {
303 | renderer.reset();
304 | });
305 |
306 | gui.fRender
307 | .add(controls, "width")
308 | .name("Width")
309 | .min(1.0)
310 | .step(1)
311 | .onFinishChange(reflow);
312 | gui.fRender
313 | .add(controls, "height")
314 | .name("Height")
315 | .min(1.0)
316 | .step(1)
317 | .onFinishChange(reflow);
318 | gui.fRender
319 | .add(controls, "dofDist")
320 | .name("DOF Distance")
321 | .min(0.0)
322 | .max(1.0)
323 | .step(0.001)
324 | .onChange(renderer.reset);
325 | gui.fRender
326 | .add(controls, "dofMag")
327 | .name("DOF Magnitude")
328 | .min(0.0)
329 | .step(0.01)
330 | .onChange(renderer.reset);
331 | gui.fRender.add(controls, "autoSample");
332 | gui.fRender
333 | .add(controls, "samplesPerFrame")
334 | .name("Samples/Frame")
335 | .min(1)
336 | .listen();
337 | gui.fRender.add(controls, "screenshot").name("Take Screenshot");
338 |
339 | gui.fScene.add(controls, "save").name("Copy URL");
340 | gui.fScene.add(controls, "clear").name("Clear Scene");
341 |
342 | const dg = document.getElementsByClassName("dg");
343 | Array.prototype.forEach.call(dg, function(el, i) {
344 | el.style.userSelect = "none";
345 | el.style.webkitUserSelect = "none";
346 | el.style.webkitTouchCallout = "none";
347 | el.style.msUserSelect = "none";
348 | el.style.mozUserSelect = "none";
349 | el.style.oUserSelect = "none";
350 | });
351 |
352 | function reflow() {
353 | if (canvas.width !== controls.width || canvas.height !== controls.height) {
354 | canvas.width = controls.width;
355 | canvas.height = controls.height;
356 | renderer.reset();
357 | }
358 | const aspect0 = canvas.width / canvas.height;
359 | const aspect1 = window.innerWidth / window.innerHeight;
360 | if (aspect0 > aspect1) {
361 | canvas.style.width = `${window.innerWidth}px`;
362 | canvas.style.height = `${Math.floor(window.innerWidth / aspect0)}px`;
363 | } else {
364 | canvas.style.height = `${window.innerHeight}px`;
365 | canvas.style.width = `${Math.floor(aspect0 * window.innerHeight)}px`;
366 | }
367 | }
368 |
369 | function pack() {
370 | const data = {
371 | model: stage.serialize(),
372 | camera: camera.serialize(),
373 | ground: {
374 | version: 0,
375 | color: controls.groundColor,
376 | roughness: controls.groundRoughness,
377 | metalness: controls.groundMetalness
378 | },
379 | sky: {
380 | version: 0,
381 | time: controls.time,
382 | azimuth: controls.azimuth,
383 | radius: controls.lightRadius,
384 | intensity: controls.lightIntensity
385 | },
386 | dof: {
387 | dist: controls.dofDist,
388 | mag: controls.dofMag
389 | }
390 | };
391 | return jcb64.pack(data);
392 | }
393 |
394 | function unpack(d) {
395 | const data = jcb64.unpack(d);
396 | stage.deserialize(data.model);
397 | camera.deserialize(data.camera);
398 | controls.time = data.sky.time;
399 | controls.azimuth = data.sky.azimuth;
400 | controls.lightRadius = data.sky.radius;
401 | controls.lightIntensity = data.sky.intensity;
402 | controls.groundColor = data.ground.color;
403 | controls.groundRoughness = data.ground.roughness;
404 | controls.groundMetalness = data.ground.metalness;
405 | controls.dofDist = data.dof.dist;
406 | controls.dofMag = data.dof.mag;
407 | gui.updateDisplay();
408 | }
409 |
410 | reflow();
411 |
412 | window.addEventListener("resize", reflow);
413 |
414 | if (location.hash) {
415 | unpack(location.hash.slice(1));
416 | } else {
417 | let x = 0;
418 | let y = 0;
419 | let z = 0;
420 | stage.set(x, y, z, 1, 1, 1, 1, 0, 0, 0, 1);
421 | let transparent = 1;
422 | let ri = 1.5;
423 | let rgb = [1, 1, 1];
424 | let rough = 0.1;
425 | for (let i = 0; i < 200; i++) {
426 | const n = [[1, 0], [-1, 0], [0, 1], [0, -1]][Math.floor(Math.random() * 4)];
427 | const x1 = x + n[0];
428 | const z1 = z + n[1];
429 | while (stage.get(x1, y, z1)) y++;
430 | x = x1;
431 | z = z1;
432 | let emit = Math.random() < 0.1 && !transparent ? 2 : 0;
433 | if (Math.random() < 0.1) {
434 | if (transparent) {
435 | transparent = 0;
436 | ri = 1;
437 | rgb = [1, 1, 1];
438 | rough = 1;
439 | } else {
440 | emit = 0;
441 | transparent = 1.0;
442 | ri = Math.random() + 1;
443 | rough = Math.random() * 0.1;
444 | rgb = [Math.random(), Math.random(), Math.random()];
445 | }
446 | }
447 | stage.set(x, y, z, ...rgb, 1, 0, emit, transparent, ri);
448 | while (stage.get(x, y - 1, z) === undefined && y > 0) {
449 | y--;
450 | stage.set(x, y, z, ...rgb, 1, 0, 0, 0, 1);
451 | }
452 | }
453 | }
454 |
455 | renderer.reset();
456 |
457 | stage.update();
458 |
459 | let tLast = 0;
460 |
461 | function loop() {
462 | if (controls.autoSample) {
463 | const dt = performance.now() - tLast;
464 | tLast = performance.now();
465 |
466 | if (dt > 1000 / 30) {
467 | controls.samplesPerFrame = Math.max(1, controls.samplesPerFrame - 1);
468 | } else if (dt < 1000 / 60) {
469 | if (renderer.sampleCount() > 1) controls.samplesPerFrame++;
470 | }
471 | // gui.updateDisplay();
472 | }
473 | const b = stage.bounds;
474 | camera.center = vec3.scale([], [b.width, b.height, b.depth], 0.5);
475 | camera.radius = vec3.length([b.width, b.height, b.depth]) * 1.5;
476 | renderer.sample(stage, camera, controls);
477 | renderer.display();
478 |
479 | document.getElementById(
480 | "stats"
481 | ).innerText = `${renderer.sampleCount().toFixed(2)} samples`;
482 |
483 | requestAnimationFrame(loop);
484 | }
485 |
486 | loop();
487 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 | extensions: ["OES_texture_float"],
12 | attributes: {
13 | antialias: false,
14 | preserveDrawingBuffer: true
15 | }
16 | });
17 |
18 | const sunDistance = 149600000000;
19 | let sunPosition = vec3.scale(
20 | [],
21 | vec3.normalize([], [1.11, -0.0, 0.25]),
22 | sunDistance
23 | );
24 |
25 | const renderAtmosphere = createAtmosphereRenderer(regl);
26 | const skyMap = renderAtmosphere({
27 | sunDirection: vec3.normalize([], sunPosition),
28 | resolution: 1024
29 | });
30 |
31 | const pingpong = PingPong(regl, {
32 | width: canvas.width,
33 | height: canvas.height,
34 | colorType: "float"
35 | });
36 |
37 | const fboPreview = regl.framebuffer({
38 | width: canvas.width,
39 | height: canvas.height,
40 | colorType: "float"
41 | });
42 |
43 | const ndcBox = [-1, -1, 1, -1, 1, 1, -1, -1, 1, 1, -1, 1];
44 |
45 | const tRandSize = 1024;
46 |
47 | const t2Sphere = (function() {
48 | const data = new Float32Array(tRandSize * tRandSize * 3);
49 | for (let i = 0; i < tRandSize * tRandSize; i++) {
50 | const r = vec3.random([]);
51 | data[i * 3 + 0] = r[0];
52 | data[i * 3 + 1] = r[1];
53 | data[i * 3 + 2] = r[2];
54 | }
55 | return regl.texture({
56 | width: tRandSize,
57 | height: tRandSize,
58 | format: "rgb",
59 | type: "float",
60 | data: data,
61 | wrap: "repeat"
62 | });
63 | })();
64 |
65 | const t3Sphere = (function() {
66 | const data = new Float32Array(tRandSize * tRandSize * 3);
67 | for (let i = 0; i < tRandSize * tRandSize; i++) {
68 | const r = vec3.random([], Math.random());
69 | data[i * 3 + 0] = r[0];
70 | data[i * 3 + 1] = r[1];
71 | data[i * 3 + 2] = r[2];
72 | }
73 | return regl.texture({
74 | width: tRandSize,
75 | height: tRandSize,
76 | format: "rgb",
77 | type: "float",
78 | data: data,
79 | wrap: "repeat"
80 | });
81 | })();
82 |
83 | const tUniform2 = (function() {
84 | const data = new Float32Array(tRandSize * tRandSize * 2);
85 | for (let i = 0; i < tRandSize * tRandSize; i++) {
86 | data[i * 2 + 0] = Math.random();
87 | data[i * 2 + 1] = Math.random();
88 | }
89 | return regl.texture({
90 | width: tRandSize,
91 | height: tRandSize,
92 | format: "luminance alpha",
93 | type: "float",
94 | data: data,
95 | wrap: "repeat"
96 | });
97 | })();
98 |
99 | const tUniform1 = (function() {
100 | const data = new Float32Array(tRandSize * tRandSize * 1);
101 | for (let i = 0; i < tRandSize * tRandSize; i++) {
102 | data[i] = Math.random();
103 | }
104 | return regl.texture({
105 | width: tRandSize,
106 | height: tRandSize,
107 | format: "luminance",
108 | type: "float",
109 | data: data,
110 | wrap: "repeat"
111 | });
112 | })();
113 |
114 | const cmdSample = regl({
115 | vert: glsl.file("./glsl/sample.vert"),
116 | frag: glsl.file("./glsl/sample.frag"),
117 | attributes: {
118 | position: ndcBox
119 | },
120 | uniforms: {
121 | source: regl.prop("source"),
122 | invpv: regl.prop("invpv"),
123 | eye: regl.prop("eye"),
124 | res: regl.prop("res"),
125 | resFrag: regl.prop("resFrag"),
126 | tSky: skyMap,
127 | tUniform1: tUniform1,
128 | tUniform2: tUniform2,
129 | t2Sphere: t2Sphere,
130 | t3Sphere: t3Sphere,
131 | tOffset: regl.prop("tOffset"),
132 | tRGB: regl.prop("tRGB"),
133 | tRMET: regl.prop("tRMET"),
134 | tRi: regl.prop("tRi"),
135 | tIndex: regl.prop("tIndex"),
136 | dofDist: regl.prop("dofDist"),
137 | dofMag: regl.prop("dofMag"),
138 | resStage: regl.prop("resStage"),
139 | invResRand: [1 / tRandSize, 1 / tRandSize],
140 | lightPosition: regl.prop("lightPosition"),
141 | lightIntensity: regl.prop("lightIntensity"),
142 | lightRadius: regl.prop("lightRadius"),
143 | groundColor: regl.prop("groundColor"),
144 | groundRoughness: regl.prop("groundRoughness"),
145 | groundMetalness: regl.prop("groundMetalness"),
146 | bounds: regl.prop("bounds"),
147 | renderPreview: regl.prop("renderPreview")
148 | },
149 | depth: {
150 | enable: false,
151 | mask: false
152 | },
153 | viewport: regl.prop("viewport"),
154 | framebuffer: regl.prop("destination"),
155 | count: 6
156 | });
157 |
158 | const cmdDisplay = regl({
159 | vert: glsl.file("./glsl/display.vert"),
160 | frag: glsl.file("./glsl/display.frag"),
161 | attributes: {
162 | position: ndcBox
163 | },
164 | uniforms: {
165 | source: regl.prop("source"),
166 | preview: regl.prop("preview"),
167 | fraction: regl.prop("fraction"),
168 | tUniform1: tUniform1,
169 | tUniform1Res: [tUniform1.width, tUniform1.height]
170 | },
171 | depth: {
172 | enable: false,
173 | mask: false
174 | },
175 | viewport: regl.prop("viewport"),
176 | count: 6
177 | });
178 |
179 | function calculateSunPosition(time, azimuth) {
180 | const theta = (2 * Math.PI * (time - 6)) / 24;
181 | return [
182 | sunDistance * Math.cos(azimuth) * Math.cos(theta),
183 | sunDistance * Math.sin(theta),
184 | sunDistance * Math.sin(azimuth) * Math.cos(theta)
185 | ];
186 | }
187 |
188 | let sampleCount = 0;
189 |
190 | function sample(stage, camera, controls) {
191 | const sp = calculateSunPosition(controls.time, controls.azimuth);
192 | if (vec3.distance(sp, sunPosition) > 0.001) {
193 | sunPosition = sp;
194 | renderAtmosphere({
195 | sunDirection: vec3.normalize([], sunPosition),
196 | cubeFBO: skyMap
197 | });
198 | }
199 | const b = stage.bounds;
200 | for (let i = 0; i < controls.samplesPerFrame; i++) {
201 | cmdSample({
202 | eye: camera.eye(),
203 | invpv: camera.invpv(),
204 | res: [canvas.width, canvas.height],
205 | tOffset: [Math.random(), Math.random()],
206 | tRGB: stage.tRGB,
207 | tRMET: stage.tRMET,
208 | tRi: stage.tRi,
209 | tIndex: stage.tIndex,
210 | resStage: stage.tIndex.width,
211 | bounds: [b.width, b.height, b.depth],
212 | lightPosition: sunPosition,
213 | lightIntensity: controls.lightIntensity,
214 | lightRadius: 695508000 * controls.lightRadius,
215 | groundRoughness: controls.groundRoughness,
216 | groundColor: controls.groundColor.map(c => c / 255),
217 | groundMetalness: controls.groundMetalness,
218 | dofDist: controls.dofDist,
219 | dofMag: controls.dofMag,
220 | renderPreview: sampleCount === 0,
221 | source: pingpong.ping(),
222 | destination: sampleCount === 0 ? fboPreview : pingpong.pong(),
223 | viewport: { x: 0, y: 0, width: canvas.width, height: canvas.height }
224 | });
225 | if (sampleCount > 0) {
226 | pingpong.swap();
227 | }
228 | sampleCount++;
229 | if (sampleCount === 1) break;
230 | }
231 | }
232 |
233 | function display() {
234 | cmdDisplay({
235 | source: pingpong.ping(),
236 | preview: fboPreview,
237 | fraction: Math.min(1.0, sampleCount / 128),
238 | viewport: { x: 0, y: 0, width: canvas.width, height: canvas.height }
239 | });
240 | }
241 |
242 | function reset() {
243 | if (
244 | pingpong.ping().width !== canvas.width ||
245 | pingpong.ping().height !== canvas.height
246 | ) {
247 | pingpong.ping()({
248 | width: canvas.width,
249 | height: canvas.height,
250 | colorType: "float"
251 | });
252 | pingpong.pong()({
253 | width: canvas.width,
254 | height: canvas.height,
255 | colorType: "float"
256 | });
257 | fboPreview({
258 | width: canvas.width,
259 | height: canvas.height,
260 | colorType: "float"
261 | });
262 | }
263 | regl.clear({ color: [0, 0, 0, 0], framebuffer: pingpong.ping() });
264 | regl.clear({ color: [0, 0, 0, 0], framebuffer: pingpong.pong() });
265 | regl.clear({ color: [0, 0, 0, 0], framebuffer: fboPreview });
266 | sampleCount = 0;
267 | }
268 |
269 | return {
270 | context: regl,
271 | sample: sample,
272 | display: display,
273 | reset: reset,
274 | sampleCount: function() {
275 | return sampleCount;
276 | }
277 | };
278 | };
279 |
--------------------------------------------------------------------------------
/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) {
9 | this.regl = regl;
10 | this.data = {};
11 | this.vIndex = new VoxelIndex();
12 | this.tIndex = regl.texture();
13 | this.tRGB = regl.texture();
14 | this.tRMET = regl.texture();
15 | this.tRi = regl.texture();
16 | }
17 |
18 | key(x, y, z) {
19 | return `${x} ${y} ${z}`;
20 | }
21 |
22 | set(x, y, z, red, green, blue, rough, metal, emit, transparent, ri) {
23 | this.data[this.key(x, y, z)] = {
24 | x,
25 | y,
26 | z,
27 | red: Math.round(red * 255),
28 | green: Math.round(green * 255),
29 | blue: Math.round(blue * 255),
30 | rough,
31 | metal,
32 | emit,
33 | transparent,
34 | ri
35 | };
36 | }
37 |
38 | unset(x, y, z) {
39 | if (Object.keys(this.data).length === 1) return;
40 | delete this.data[this.key(x, y, z)];
41 | }
42 |
43 | get(x, y, z) {
44 | return this.data[this.key(x, y, z)];
45 | }
46 |
47 | clear() {
48 | this.vIndex.clear();
49 | this.data = {};
50 | }
51 |
52 | updateBounds() {
53 | const b = {
54 | min: {
55 | x: Infinity,
56 | y: Infinity,
57 | z: Infinity
58 | },
59 | max: {
60 | x: -Infinity,
61 | y: -Infinity,
62 | z: -Infinity
63 | }
64 | };
65 | for (let [_, v] of Object.entries(this.data)) {
66 | b.min.x = Math.min(b.min.x, v.x);
67 | b.min.y = Math.min(b.min.y, v.y);
68 | b.min.z = Math.min(b.min.z, v.z);
69 | b.max.x = Math.max(b.max.x, v.x);
70 | b.max.y = Math.max(b.max.y, v.y);
71 | b.max.z = Math.max(b.max.z, v.z);
72 | }
73 | b.width = 1 + b.max.x - b.min.x;
74 | b.height = 1 + b.max.y - b.min.y;
75 | b.depth = 1 + b.max.z - b.min.z;
76 | this.bounds = b;
77 | }
78 |
79 | update() {
80 | this.updateBounds();
81 | let size = 1;
82 | while (
83 | size * size <
84 | this.bounds.width * this.bounds.height * this.bounds.depth
85 | ) {
86 | size *= 2;
87 | }
88 | const shiftX = -this.bounds.min.x;
89 | const shiftY = -this.bounds.min.y;
90 | const shiftZ = -this.bounds.min.z;
91 | const aIndex = new Uint8Array(size * size * 2);
92 | aIndex.fill(0);
93 | for (let [_, v] of Object.entries(this.data)) {
94 | const vi = this.vIndex.get(v);
95 | const ai =
96 | (shiftY + v.y) * this.bounds.width * this.bounds.depth +
97 | (shiftZ + v.z) * this.bounds.width +
98 | (shiftX + v.x);
99 | aIndex[ai * 2 + 0] = vi[0];
100 | aIndex[ai * 2 + 1] = vi[1];
101 | }
102 | this.tIndex({
103 | width: size,
104 | height: size,
105 | format: "luminance alpha",
106 | data: aIndex
107 | });
108 | this.tRGB({
109 | width: 256,
110 | height: 256,
111 | format: "rgb",
112 | data: this.vIndex.aRGB
113 | });
114 | this.tRMET({
115 | width: 256,
116 | height: 256,
117 | format: "rgba",
118 | type: "float",
119 | data: this.vIndex.aRMET
120 | });
121 | this.tRi({
122 | width: 256,
123 | height: 256,
124 | format: "rgba",
125 | type: "float",
126 | data: this.vIndex.aRi
127 | });
128 | }
129 |
130 | serialize() {
131 | const out = {
132 | version: 0
133 | };
134 | out.xyz = [];
135 | out.rgb = [];
136 | out.rough = [];
137 | out.metal = [];
138 | out.emit = [];
139 | out.transparent = [];
140 | out.ri = [];
141 | for (let [_, v] of Object.entries(this.data)) {
142 | out.xyz.push(v.x, v.y, v.z);
143 | out.rgb.push(v.red, v.green, v.blue);
144 | out.rough.push(+v.rough.toFixed(3));
145 | out.metal.push(+v.metal.toFixed(3));
146 | out.emit.push(+v.emit.toFixed(3));
147 | out.transparent.push(+v.transparent.toFixed(3));
148 | out.ri.push(+v.ri.toFixed(3));
149 | }
150 | return out;
151 | }
152 |
153 | deserialize(d) {
154 | this.clear();
155 | for (let i = 0; i < d.xyz.length / 3; i++) {
156 | this.set(
157 | d.xyz[i * 3 + 0],
158 | d.xyz[i * 3 + 1],
159 | d.xyz[i * 3 + 2],
160 | d.rgb[i * 3 + 0] / 255,
161 | d.rgb[i * 3 + 1] / 255,
162 | d.rgb[i * 3 + 2] / 255,
163 | d.rough[i],
164 | d.metal[i],
165 | d.emit[i],
166 | d.transparent[i],
167 | d.ri[i]
168 | );
169 | }
170 | }
171 |
172 | rayAABB(r0, r, v) {
173 | const bMin = v.slice();
174 | const bMax = vec3.add([], v, [1, 1, 1]);
175 | const invr = r.map(e => 1 / e);
176 | const omax = vec3.mul([], vec3.sub([], bMin, r0), invr);
177 | const omin = vec3.mul([], vec3.sub([], bMax, r0), invr);
178 | const imax = vec3.max([], omax, omin);
179 | const imin = vec3.min([], omax, omin);
180 | const t1 = Math.min(imax[0], Math.min(imax[1], imax[2]));
181 | const t0 = Math.max(0, Math.max(imin[0], Math.max(imin[1], imin[2])));
182 | if (t1 > t0) {
183 | return t0;
184 | }
185 | return false;
186 | }
187 |
188 | intersect(r0, r) {
189 | const v = r0.map(Math.floor);
190 | const stp = r.map(Math.sign);
191 | const tDelta = r.map(e => 1.0 / Math.abs(e));
192 | const tMax = [
193 | r[0] < 0 ? r0[0] - Math.floor(r0[0]) : Math.ceil(r0[0]) - r0[0],
194 | r[1] < 0 ? r0[1] - Math.floor(r0[1]) : Math.ceil(r0[1]) - r0[1],
195 | r[2] < 0 ? r0[2] - Math.floor(r0[2]) : Math.ceil(r0[2]) - r0[2]
196 | ];
197 | tMax[0] /= Math.abs(r[0]);
198 | tMax[1] /= Math.abs(r[1]);
199 | tMax[2] /= Math.abs(r[2]);
200 | for (let i = 0; i < 8192; i++) {
201 | if (tMax[0] < tMax[1]) {
202 | if (tMax[0] < tMax[2]) {
203 | v[0] += stp[0];
204 | tMax[0] += tDelta[0];
205 | } else {
206 | v[2] += stp[2];
207 | tMax[2] += tDelta[2];
208 | }
209 | } else {
210 | if (tMax[1] <= tMax[2]) {
211 | v[1] += stp[1];
212 | tMax[1] += tDelta[1];
213 | } else {
214 | v[2] += stp[2];
215 | tMax[2] += tDelta[2];
216 | }
217 | }
218 | const gv = this.get(v[0], v[1], v[2]);
219 | if (gv) {
220 | return {
221 | voxel: v,
222 | t: this.rayAABB(r0, r, v)
223 | };
224 | }
225 | }
226 | return undefined;
227 | }
228 | };
229 |
--------------------------------------------------------------------------------
/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} ${
22 | v.transparent
23 | } ${v.ri}`;
24 | if (this.keys[h] === undefined) {
25 | // It's cool that we're skipping the first two indices, because those will be a shortcut for air and ground.
26 | this.x++;
27 | if (this.x > 255) {
28 | this.x = 0;
29 | this.y++;
30 | if (this.y > 255) {
31 | throw new Error("Exceeded voxel type limit of 65536");
32 | }
33 | }
34 | this.keys[h] = [this.x, this.y];
35 | const i = this.y * 256 + this.x;
36 | this.aRGB[i * 3 + 0] = v.red;
37 | this.aRGB[i * 3 + 1] = v.green;
38 | this.aRGB[i * 3 + 2] = v.blue;
39 | this.aRMET[i * 4 + 0] = v.rough;
40 | this.aRMET[i * 4 + 1] = v.metal;
41 | this.aRMET[i * 4 + 2] = v.emit;
42 | this.aRMET[i * 4 + 3] = v.transparent;
43 | this.aRi[i * 4 + 0] = v.ri;
44 | }
45 | return this.keys[h];
46 | }
47 | };
48 |
--------------------------------------------------------------------------------