├── .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 | ![vixel screenshot](media/screenshot-000.png) 6 | 7 | ![vixel screenshot](media/screenshot-001.png) 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 | ![thing](media/material-000.png) | 22 | | 1.0 | 0.0 | Chalk | ![thing](media/material-001.png) | 23 | | 0.0 | 1.0 | Mirror | ![thing](media/material-002.png) | 24 | | 1.0 | 0.05 | Unpolished metal | ![thing](media/material-003.png) | 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 | 94 | 95 | 96 | 97 | 98 | 99 |
Left mouse buttonPlace voxel
Shift + left mouse buttonRemove voxel
Control + left mouse buttonCopy voxel
Right mouse button + dragRotate camera
Mouse wheelZoom
H keyHide/show controls
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 | --------------------------------------------------------------------------------