├── .gitignore ├── README.md ├── ffmpeg.txt ├── fractals ├── cityscape │ └── frag.glsl ├── klein │ ├── frag.glsl │ └── thumbnail.png ├── mandelbox │ ├── frag.glsl │ └── thumbnail.png ├── mandelbulb │ ├── frag.glsl │ └── thumbnail.png ├── menger │ ├── frag.glsl │ └── thumbnail.png ├── plant │ └── plant.glsl ├── shaded-mandelbulb │ └── frag.glsl └── spheres │ └── spheres.glsl ├── get-speed.js ├── headless.js ├── index.html ├── main.js ├── package-lock.json ├── package.json ├── pass-through-vert.glsl ├── player-controls.js ├── readme.gif ├── renderer.js ├── server └── main.js ├── thumbnail.png ├── upsample.glsl ├── viewer.html └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /dist 3 | /.cache 4 | /render-results 5 | /server/dist 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 |

Fractal.garden

3 |

4 | Explore fractals in your browser in real time 5 |
6 |
7 | animated 8 |

9 | 10 | Fractal.garden is an interactive 3d fractal explorer. The fractals are rendered using a technique called [raymarching](https://www.iquilezles.org/www/articles/raymarchingdf/raymarchingdf.htm). The images are rendered at mostly acceptable framerates using WebGL (through a library called [regl](https://github.com/regl-project/regl)). 11 | 12 | 13 | This project also contains a headless rendering server, which can turn a set of frame states (each of which contains exactly the state needed to render an image) into a set of png images, one per frame. These can then be combined into a video (or the gif in this readme) using ffmpeg. Note that this server is quite unoptimized, so it takes a long time to render everything (i.e. much longer than the renderer in the browser would). 14 | 15 | ## Installation 16 | 17 | To install, you need a system with node installed (tested on 10.x.x, anything above that should definitely be okay). Run `npm install` and then any of the folling 18 | 19 | ``` 20 | # will start a dev server (parcel) 21 | npm run dev 22 | 23 | # will build the project 24 | npm run build 25 | 26 | # will start a render server that renders pngs to the filesystem 27 | npm run server 28 | ``` 29 | 30 | ## Usage 31 | 32 | Running `npm run dev` will start a development server at `http://localhost:1234` by default. When you open this in your local browser, you'll see effectively the same as you would at `https://fractal.garden`. The page itself provides some further instruction on how to actually use the application. 33 | 34 | ### Recording 35 | 36 | Locally, you can run `npm run server` to start a render server alongside the dev server. Once this server is running, you can head back to your browser tab that's running your local fractal explorer. When you press `r` once while flying around, your browser will start recording all of the updates to your position, direction, and any other relevant piece of state needed to render a frame. After pressing `r` a second time, the browser sends this collection of frames to the server, which proceeds to render all of the frames required to individual PNGs inside the `render-results` folder. 37 | 38 | Notice that this is a somewhat janky approach; There's no preview of what has been recorded, and no animation tools to speak of. You pretty much just get what you see as you fly through the fractal. The upside is that the server gets more time than the browser to render each frame, so it always renders each frame at a nice resolution (1080p) and at a constant framerate. The browser may not send its frames at a constant framerate because there may be hiccups during rendering, so the server also interpolates between the frames to smoothen things out. 39 | 40 | Also notice that this doesn't actually give you a full video, it just gives you a set of pngs. The file `ffmpeg.txt` has some convenient commands for ffmpeg that will convert the PNGs into a video, and subsequently flip the video vertically because for some reason, our headless webgl environment has a flipped y-axis compared to the browser, and I've been too lazy to just flip it in code. 41 | 42 | -------------------------------------------------------------------------------- /ffmpeg.txt: -------------------------------------------------------------------------------- 1 | Render video from PNGs: 2 | ffmpeg -i frame-%1d.png -pix_fmt yuv420p out.mp4 3 | 4 | Flip video: 5 | ffmpeg -i out.mp4 -vf vflip -c:a copy out-flip.mp4 -------------------------------------------------------------------------------- /fractals/cityscape/frag.glsl: -------------------------------------------------------------------------------- 1 | precision highp float; 2 | uniform vec2 screenSize; 3 | uniform float time; 4 | uniform vec3 cameraPosition; 5 | uniform mat4 cameraDirection; 6 | 7 | const float hitThreshold = 0.00045; 8 | 9 | const int CAMERA_ITERATIONS = 180; 10 | const int LIGHT_ITERATIONS= 30; 11 | 12 | const vec3 spaceRepetition = vec3(2, 100, 5); 13 | 14 | vec3 getRay(vec2 xy) { 15 | vec2 normalizedCoords = xy - vec2(0.5); 16 | vec2 pixel = (normalizedCoords - 0.5 * screenSize) / min(screenSize.x, screenSize.y); 17 | 18 | // normalize to get unit vector 19 | return normalize(cameraDirection * (vec4(pixel.x, pixel.y, 1, 0))).xyz; 20 | } 21 | 22 | vec3 opRepeat(vec3 p, vec3 distance) { 23 | return mod(p + 0.5 * distance, distance) - 0.5 * distance; 24 | } 25 | 26 | float smin( float a, float b, float k ) 27 | { 28 | float h = max( k-abs(a-b), 0.0 )/k; 29 | return min( a, b ) - h*h*k*(1.0/4.0); 30 | } 31 | 32 | float sphere(vec3 p, vec3 center, float r) { 33 | return distance(p, center) - r; 34 | } 35 | 36 | float box(vec3 p, vec3 boxdim) { 37 | vec3 dist = abs(p) - boxdim; 38 | return length(max(dist,0.0)) - min(max(dist.x, max(dist.y, dist.z)), 0.); 39 | } 40 | 41 | const float rx = 13.; 42 | const float ry = 62.; 43 | const float rz = 75.; 44 | 45 | const vec3 building1Repeat = vec3(.4, 100, 1.); 46 | const vec3 building2Repeat = vec3(.5, 100, 2.81); 47 | 48 | float doModel(vec3 p) { 49 | float building1 = box(opRepeat(p, building1Repeat), vec3(.5, 1., .5) / 5.); 50 | float building2 = box(opRepeat(p, building2Repeat), vec3(.9, 1.5, .4) / 5.); 51 | return min(min(building1, building2), p.y + 2.); // floor 52 | } 53 | 54 | vec3 calcNormal(vec3 p, float h) { 55 | const vec2 k = vec2(1,-1); 56 | return normalize( k.xyy*doModel( p + k.xyy*h ) + 57 | k.yyx*doModel( p + k.yyx*h ) + 58 | k.yxy*doModel( p + k.yxy*h ) + 59 | k.xxx*doModel( p + k.xxx*h ) ); 60 | } 61 | 62 | vec3 light = normalize(vec3(0, 1, -.5)); 63 | const float mint = 5. * hitThreshold; 64 | const float maxt = 0.5; 65 | const float k = 4.; 66 | const float fogNear = 0.; 67 | const float fogFar = 150.; 68 | // this is kinda contrived and does a bunch of stuff I'm not using right now, but I'll leave it like this for now 69 | float trace(vec3 origin, vec3 direction, out vec3 collision, out int iterations, out float fog) { 70 | vec3 position = origin; 71 | float distanceTraveled = 0.; 72 | float d = 0.; 73 | float h = hitThreshold; 74 | for(int i = 0; i <= CAMERA_ITERATIONS; i++) { 75 | iterations = i; 76 | d = doModel(position); 77 | h = max(hitThreshold * distanceTraveled, hitThreshold / 20.); 78 | if (d < h) break; 79 | position += d * direction; 80 | distanceTraveled += d; 81 | if (distanceTraveled > fogFar) break; 82 | } 83 | fog = max(0., (distance(position, origin) - fogNear) / (fogFar - fogNear)); 84 | if (iterations == CAMERA_ITERATIONS || distanceTraveled > fogFar) { 85 | iterations = 0; 86 | fog = 1.; 87 | return dot(direction, light); 88 | } 89 | collision = position; 90 | vec3 n = calcNormal(collision, h); 91 | float t = mint; 92 | float res = 1.0; 93 | float pd = 1e1; 94 | for (int i = 0; i < LIGHT_ITERATIONS; i++) { 95 | position = collision + light * t; 96 | d = doModel(position); 97 | if (d < hitThreshold){ 98 | return 0.; 99 | // return (t - mint) / (maxt - mint); 100 | }; 101 | if (t > maxt) { 102 | res = 1.; 103 | break; 104 | } 105 | float y = d*d/(2.0*pd); 106 | float h = sqrt(d*d-y*y); 107 | res = min( res, k*h/max(0.,t-y) ); 108 | pd = d; 109 | t += d; 110 | } 111 | return max(0., res * dot(n, light)); 112 | } 113 | 114 | float occlusion(int iterations) { 115 | float occlusionLight = 1. - float(iterations) / float(CAMERA_ITERATIONS); 116 | return occlusionLight; 117 | } 118 | 119 | void main() { 120 | vec3 direction = getRay(gl_FragCoord.xy); 121 | 122 | int iterations; 123 | vec3 collision; 124 | float fog; 125 | float lightStrength = trace(cameraPosition, direction, collision, iterations, fog); 126 | 127 | float d = distance(collision, cameraPosition); 128 | float ol = .20; 129 | gl_FragColor = vec4( 130 | vec3((ol * occlusion(iterations) + (1. - ol) * lightStrength)), 131 | 1. 132 | ); 133 | // gl_FragColor = vec4(direction * 1., 1.); 134 | } -------------------------------------------------------------------------------- /fractals/klein/frag.glsl: -------------------------------------------------------------------------------- 1 | precision highp float; 2 | uniform vec2 screenSize; 3 | uniform vec2 offset; 4 | uniform vec2 repeat; 5 | uniform float time; 6 | uniform vec3 cameraPosition; 7 | uniform mat4 cameraDirection; 8 | uniform bool onlyDistance; 9 | uniform float scrollX; 10 | uniform float scrollY; 11 | 12 | const float hitThreshold = 0.00003; 13 | 14 | const int CAMERA_ITERATIONS = 240; 15 | const int LIGHT_ITERATIONS= 0; 16 | 17 | const vec3 spaceRepetition = vec3(12, 5.15, 6); 18 | 19 | const float theta = 0.5 * 3.14; 20 | // rotation matrix used to rotate the scene 90deg around x axis 21 | const mat3 rotmat = mat3( 22 | 1, 0, 0, 23 | 0, cos(theta), -sin(theta), 24 | 0, sin(theta), cos(theta) 25 | ); 26 | 27 | vec3 getRay(vec2 xy) { 28 | vec2 normalizedCoords = xy - vec2(0.5) + (offset / repeat); 29 | vec2 pixel = (normalizedCoords - 0.5 * screenSize) / min(screenSize.x, screenSize.y); 30 | 31 | // normalize to get unit vector 32 | return normalize((cameraDirection * vec4(pixel.x, pixel.y, 1, 0)).xyz); 33 | } 34 | 35 | vec3 opRepeat(vec3 p, vec3 distance) { 36 | return mod(p + 0.5 * distance, distance) - 0.5 * distance; 37 | } 38 | 39 | // see e.g. http://blog.hvidtfeldts.net/index.php/2012/05/distance-estimated-3d-fractals-part-viii-epilogue/ 40 | // for more info 41 | 42 | const vec4 param_min = vec4(-0.8323, -0.694, -0.1045, 0.8067); 43 | const vec4 param_max = vec4(0.85, 2.0, 0.9, 0.93); 44 | const int FOLDING_NUMBER = 9; 45 | float doModel(vec3 p) 46 | { 47 | p = opRepeat(p, spaceRepetition); 48 | float k1, k2, rp2, rq2; 49 | float scale = 1.0; 50 | float orb = 1e4; 51 | vec3 q = p; 52 | for (int i = 0; i < FOLDING_NUMBER; i++) 53 | { 54 | p = (1.9 + .1 * sin(scrollY + .5)) * clamp(p, param_min.xyz, param_max.xyz) - p; 55 | q = 2. * fract(0.5 * q + 0.5) - 1.0; 56 | rp2 = dot(p, p); 57 | rq2 = dot(q, q); 58 | k1 = max(param_min.w / rp2, 1.0); 59 | k2 = max(param_min.w / rq2, 1.0); 60 | p *= k1; 61 | q *= k2; 62 | scale *= k1; 63 | orb = min(orb, rq2); 64 | } 65 | float lxy = length(p.xy); 66 | return abs(0.5 * max(param_max.w - lxy, lxy * p.z / length(p)) / scale); 67 | } 68 | 69 | vec3 calcNormal(vec3 p, float h) { 70 | const vec2 k = vec2(1,-1); 71 | return normalize( k.xyy*doModel( p + k.xyy*h ) + 72 | k.yyx*doModel( p + k.yyx*h ) + 73 | k.yxy*doModel( p + k.yxy*h ) + 74 | k.xxx*doModel( p + k.xxx*h ) ); 75 | } 76 | 77 | vec3 light = rotmat * normalize(vec3(sin(scrollX - 1.6), 3, cos(scrollX))); 78 | const float minDistance = 0.03; 79 | const float k = 8.; 80 | const float fogNear = 1.; 81 | const float fogFar = 100.; 82 | // this is kinda contrived and does a bunch of stuff I'm not using right now, but I'll leave it like this for now 83 | float trace(vec3 origin, vec3 direction, out vec3 collision, out int iterations, out float fog) { 84 | float distanceTraveled = minDistance; 85 | vec3 position = origin + minDistance * direction; 86 | float d = 0.; 87 | float h = hitThreshold; 88 | for(int i = 0; i <= CAMERA_ITERATIONS; i++) { 89 | iterations = i; 90 | d = doModel(position); 91 | h = max(hitThreshold * distanceTraveled * distanceTraveled, hitThreshold); 92 | if (d < h) break; 93 | position += d * direction; 94 | distanceTraveled += d; 95 | if (distanceTraveled > fogFar) break; 96 | } 97 | float iterationFog = float(iterations) / float(CAMERA_ITERATIONS); 98 | fog = max(iterationFog, (distance(position, origin) - fogNear) / (fogFar - fogNear)); 99 | if (iterations == CAMERA_ITERATIONS || distanceTraveled > fogFar) { 100 | iterations = 0; 101 | fog = 1.; 102 | return dot(direction, light); 103 | } 104 | collision = position; 105 | vec3 n = calcNormal(collision, h); 106 | return max(0., dot(n, light)); 107 | } 108 | 109 | float occlusion(int iterations) { 110 | float occlusionLight = 1. - float(iterations) / float(CAMERA_ITERATIONS); 111 | return occlusionLight; 112 | } 113 | 114 | // const float col = 0.05; // amount of coloring 115 | 116 | vec3 hsl2rgb( in vec3 c ) { 117 | vec3 rgb = clamp( abs(mod(c.x*6.0+vec3(0.0,4.0,2.0),6.0)-3.0)-1.0, 0.0, 1.0 ); 118 | return c.z + c.y * (rgb-0.5)*(1.0-abs(2.0*c.z-1.0)); 119 | } 120 | 121 | vec3 getColor(float it, float d) { 122 | return hsl2rgb(vec3( 123 | d, 124 | 0.6, 125 | pow(it, 0.8) 126 | )); 127 | } 128 | 129 | vec3 a = vec3(0.5, 0.5, 0.7); 130 | vec3 b = vec3(0.5, 0.5, 1.0); 131 | vec3 c = vec3(6.0, 1.0, 0.0); 132 | vec3 d = vec3(1.1, 1.0, 1.); 133 | vec3 color(in float t) 134 | { 135 | return a + b * cos(6.28318 * (c * t + d)); 136 | } 137 | 138 | void main() { 139 | vec3 direction = rotmat * getRay(gl_FragCoord.xy); 140 | 141 | int iterations; 142 | vec3 collision; 143 | float fog; 144 | float lightStrength = trace(rotmat * (cameraPosition * 2.) + vec3(1.4, 9.5, 1.1), direction, collision, iterations, fog); 145 | 146 | vec3 fogColor = vec3(dot(direction, light)); 147 | 148 | vec3 normal = calcNormal(collision, hitThreshold); 149 | 150 | // float d = distance(collision, cameraPosition); 151 | float ol = .25; 152 | gl_FragColor = vec4( 153 | color(normal.x * normal.y * normal.z) * mix(vec3(occlusion(iterations) * (2. - ol) * lightStrength), 2. * fogColor, fog), 154 | 1. 155 | ); 156 | // gl_FragColor = vec4(vec3(fog), 1.); 157 | } 158 | -------------------------------------------------------------------------------- /fractals/klein/thumbnail.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ath92/fractal-garden/0b53370de317765a2b72a319c52cd544d70ebea5/fractals/klein/thumbnail.png -------------------------------------------------------------------------------- /fractals/mandelbox/frag.glsl: -------------------------------------------------------------------------------- 1 | precision highp float; 2 | uniform vec2 screenSize; 3 | uniform vec2 offset; 4 | uniform vec2 repeat; 5 | uniform float time; 6 | uniform vec3 cameraPosition; 7 | uniform mat4 cameraDirection; 8 | uniform bool onlyDistance; 9 | uniform float scrollX; 10 | uniform float scrollY; 11 | 12 | const float hitThreshold = 0.0005; 13 | 14 | const int CAMERA_ITERATIONS = 140; 15 | const int LIGHT_ITERATIONS= 60; 16 | 17 | const vec3 spaceRepetition = vec3(12); 18 | 19 | vec3 getRay(vec2 xy) { 20 | vec2 normalizedCoords = xy - vec2(0.5) + (offset / repeat); 21 | vec2 pixel = (normalizedCoords - 0.5 * screenSize) / min(screenSize.x, screenSize.y); 22 | 23 | // normalize to get unit vector 24 | return (cameraDirection * normalize(vec4(pixel.x, pixel.y, 1, 0))).xyz; 25 | } 26 | 27 | vec3 opRepeat(vec3 p, vec3 distance) { 28 | return mod(p + 0.5 * distance, distance) - 0.5 * distance; 29 | } 30 | 31 | // from https://strangerintheq.github.io/sdf.html 32 | float fixed_radius2 = 5.5; 33 | float min_radius2 = 0.5; 34 | float folding_limit = 1.5 + sin(scrollY) * .8; 35 | float scale = 24.; 36 | 37 | void sphere_fold(inout vec3 p, inout float dp) { 38 | float r2 = dot(p, p); 39 | if(r2 < min_radius2) { 40 | float temp = (fixed_radius2 / min_radius2); 41 | p *= temp; 42 | dp *= temp; 43 | } else if(r2 < fixed_radius2) { 44 | float temp = (fixed_radius2 / r2); 45 | p *= temp; 46 | dp *= temp; 47 | } 48 | } 49 | 50 | void box_fold(inout vec3 p, inout float dp) { 51 | p = clamp(p, -folding_limit, folding_limit) * 2.0 - p; 52 | } 53 | 54 | float mandelbox(vec3 p) { 55 | float scale = 4.; 56 | float dr = 1.; 57 | vec3 offset = p; 58 | for (int n = 0; n < 10; ++n) { 59 | sphere_fold(p, dr); 60 | box_fold(p, dr); 61 | p = scale * p + offset; 62 | dr = dr * abs(scale) + 1.0; 63 | offset = vec3( 0.1 - sin(scrollY) * cos(scrollY) * 0.1) ; 64 | } 65 | float r = length(p); 66 | return r / abs(dr); 67 | } 68 | 69 | float ground(vec3 p, float y) { 70 | return p.y - y; 71 | } 72 | 73 | float doModel(vec3 p) { 74 | return mandelbox(opRepeat(p, vec3(10., 0., 10.))); 75 | // return menger(opRepeat(p, vec3(10., 0., 5.)), 3., 1. / 2.)); 76 | } 77 | 78 | vec3 calcNormal(vec3 p, float h) { 79 | const vec2 k = vec2(1,-1); 80 | return normalize( k.xyy*doModel( p + k.xyy*h ) + 81 | k.yyx*doModel( p + k.yyx*h ) + 82 | k.yxy*doModel( p + k.yxy*h ) + 83 | k.xxx*doModel( p + k.xxx*h ) ); 84 | } 85 | 86 | vec3 light = normalize(vec3(sin(scrollX), 3, cos(scrollX))); 87 | const float mint = 5. * hitThreshold; 88 | const float maxt = 1.; 89 | const float k = 8.; 90 | const float fogNear = 10.; 91 | const float fogFar = 20.; 92 | // this is kinda contrived and does a bunch of stuff I'm not using right now, but I'll leave it like this for now 93 | float trace(vec3 origin, vec3 direction, out vec3 collision, out int iterations, out float fog) { 94 | vec3 position = origin; 95 | float distanceTraveled = 0.; 96 | float d = 0.; 97 | float h = hitThreshold; 98 | for(int i = 0; i <= CAMERA_ITERATIONS; i++) { 99 | iterations = i; 100 | d = doModel(position); 101 | h = max(hitThreshold * distanceTraveled, hitThreshold / 20.); 102 | if (d < h) break; 103 | position += d * direction; 104 | distanceTraveled += d; 105 | if (distanceTraveled > fogFar) break; 106 | } 107 | fog = max(0., (distance(position, origin) - fogNear) / (fogFar - fogNear)); 108 | if (iterations == CAMERA_ITERATIONS || distanceTraveled > fogFar) { 109 | iterations = 0; 110 | fog = 1.; 111 | return dot(direction, light); 112 | } 113 | collision = position; 114 | vec3 n = calcNormal(collision, h); 115 | float t = mint; 116 | float res = 1.0; 117 | float pd = 1e1; 118 | for (int i = 0; i < LIGHT_ITERATIONS; i++) { 119 | position = collision + light * t; 120 | d = doModel(position); 121 | if (d < hitThreshold){ 122 | return 0.; 123 | // return (t - mint) / (maxt - mint); 124 | }; 125 | if (t > maxt) { 126 | res = 1.; 127 | break; 128 | } 129 | float y = d*d/(2.0*pd); 130 | float h = sqrt(d*d-y*y); 131 | res = min( res, k*h/max(0.,t-y) ); 132 | pd = d; 133 | t += d; 134 | } 135 | return max(0., res * dot(n, light)); 136 | } 137 | 138 | float occlusion(int iterations) { 139 | float occlusionLight = 1. - float(iterations) / float(CAMERA_ITERATIONS); 140 | return occlusionLight; 141 | } 142 | 143 | // const float col = 0.05; // amount of coloring 144 | 145 | vec3 hsl2rgb( in vec3 c ) { 146 | vec3 rgb = clamp( abs(mod(c.x*6.0+vec3(0.0,4.0,2.0),6.0)-3.0)-1.0, 0.0, 1.0 ); 147 | return c.z + c.y * (rgb-0.5)*(1.0-abs(2.0*c.z-1.0)); 148 | } 149 | 150 | vec3 getColor(float it, float d) { 151 | return hsl2rgb(vec3( 152 | d, 153 | 0.6, 154 | pow(it, 0.8) 155 | )); 156 | } 157 | 158 | void main() { 159 | vec3 direction = getRay(gl_FragCoord.xy); 160 | 161 | int iterations; 162 | vec3 collision; 163 | float fog; 164 | float lightStrength = trace(cameraPosition * 10. + vec3(0, 2, 7.7), direction, collision, iterations, fog); 165 | 166 | float fogColor = dot(direction, light); 167 | 168 | float d = distance(collision, cameraPosition); 169 | float ol = .25; 170 | gl_FragColor = vec4( 171 | vec3((ol * occlusion(iterations) + (1. - ol) * lightStrength) * (1. - fog) + fog * fogColor), 172 | 1. 173 | ); 174 | } -------------------------------------------------------------------------------- /fractals/mandelbox/thumbnail.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ath92/fractal-garden/0b53370de317765a2b72a319c52cd544d70ebea5/fractals/mandelbox/thumbnail.png -------------------------------------------------------------------------------- /fractals/mandelbulb/frag.glsl: -------------------------------------------------------------------------------- 1 | precision highp float; 2 | uniform vec2 screenSize; 3 | uniform vec2 offset; 4 | uniform vec2 repeat; 5 | uniform float time; 6 | uniform vec3 cameraPosition; 7 | uniform mat4 cameraDirection; 8 | uniform bool onlyDistance; 9 | uniform float scrollX; 10 | uniform float scrollY; 11 | 12 | const float hitThreshold = 0.00015; 13 | const int MAX_ITER = 200; 14 | 15 | const vec3 spaceRepetition = vec3(3.5); 16 | 17 | vec3 getRay() { 18 | vec2 normalizedCoords = gl_FragCoord.xy - vec2(0.5) + (offset / repeat); 19 | vec2 pixel = (normalizedCoords - 0.5 * screenSize) / min(screenSize.x, screenSize.y); 20 | 21 | // as if the higher the pixel value, the more the offset is being applied 22 | // normalize to get unit vector 23 | return (cameraDirection * normalize(vec4(pixel.x, pixel.y, 1, 0))).xyz; 24 | } 25 | 26 | vec3 opRepeat(vec3 p, vec3 distance) { 27 | return mod(p + 0.5 * distance, distance) - 0.5 * distance; 28 | } 29 | 30 | float doModel(vec3 p) { 31 | vec3 pos = opRepeat(p, spaceRepetition); 32 | vec3 z = pos; 33 | float dr = 1.; 34 | float r = 0.0; 35 | for (int i = 0; i < 10; i++) { 36 | r = length(z); 37 | if (r > 4.) break; 38 | 39 | // convert to polar coordinates 40 | float theta = acos(z.z / r); 41 | float phi = atan(z.y, z.x); 42 | float power = 12. + sin(scrollY) * 10.; 43 | dr = pow(r, power - 1.) * power * dr + 1.5; 44 | 45 | // scale and rotate the point 46 | float zr = pow(r, power); 47 | theta = theta * power; 48 | phi = phi * power; 49 | 50 | // convert back to cartesian coordinates 51 | z = zr * vec3(sin(theta) * cos(phi), sin(phi) * sin(theta), cos(theta)); 52 | z += pos; 53 | } 54 | return abs(0.5 * log(r) * r / dr); 55 | } 56 | // this is kinda contrived and does a bunch of stuff I'm not using right now, but I'll leave it like this for now 57 | vec3 trace(vec3 origin, vec3 direction, out int iterations) { 58 | vec3 position = origin; 59 | float distanceTraveled = 0.; 60 | mat3 scrollXRotate = mat3( 61 | 1, sin(scrollX) * 0.05, 0, 62 | -sin(scrollX) * 0.05, 1, 0, 63 | 0, 0, 1 64 | ); 65 | for(int i = 0; i < MAX_ITER; i++) { 66 | iterations = i; 67 | float d = doModel(position); 68 | if (d < hitThreshold * distanceTraveled) break; 69 | position += d * direction; 70 | direction = scrollXRotate * direction; 71 | distanceTraveled += d; 72 | } 73 | return position; 74 | } 75 | 76 | float getIllumination(vec3 collision, int iterations) { 77 | float occlusionLight = 1. - float(iterations) / float(MAX_ITER); 78 | return occlusionLight; 79 | } 80 | 81 | // const float col = 0.05; // amount of coloring 82 | 83 | vec3 hsl2rgb( in vec3 c ) { 84 | vec3 rgb = clamp( abs(mod(c.x*6.0+vec3(0.0,4.0,2.0),6.0)-3.0)-1.0, 0.0, 1.0 ); 85 | return c.z + c.y * (rgb-0.5)*(1.0-abs(2.0*c.z-1.0)); 86 | } 87 | 88 | vec3 getColor(float it, float d) { 89 | return hsl2rgb(vec3( 90 | d, 91 | 0.5, 92 | 1. - pow(it, 0.8) 93 | )); 94 | } 95 | 96 | void main() { 97 | vec3 direction = getRay(); 98 | // gl_FragColor = vec4(offset / (repeat - vec2(1)), 0, 1); 99 | // return; 100 | 101 | // gl_FragColor = vec4(opRepeat(cameraPosition, vec3(2.5)), 1); 102 | // return; 103 | 104 | int iterations; 105 | vec3 collision = trace(cameraPosition, direction, iterations); 106 | gl_FragColor = vec4( 107 | getColor(float(iterations) / float(MAX_ITER), distance(collision, spaceRepetition / 2.)), 108 | 1. 109 | ); 110 | // gl_FragColor = vec4(1., 0, 0, 1); 111 | // return; 112 | } -------------------------------------------------------------------------------- /fractals/mandelbulb/thumbnail.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ath92/fractal-garden/0b53370de317765a2b72a319c52cd544d70ebea5/fractals/mandelbulb/thumbnail.png -------------------------------------------------------------------------------- /fractals/menger/frag.glsl: -------------------------------------------------------------------------------- 1 | precision highp float; 2 | uniform vec2 screenSize; 3 | uniform vec2 offset; 4 | uniform vec2 repeat; 5 | uniform vec3 cameraPosition; 6 | uniform mat4 cameraDirection; 7 | uniform float scrollX; 8 | uniform float scrollY; 9 | 10 | const int MAX_ITER = 128; 11 | const float HIT_THRESHOLD = 0.0001; 12 | const float variance = 0.01; 13 | // const float PI = 3.14159265359; 14 | 15 | mat3 rotmat = mat3( 16 | 1, 0, 0, 17 | 0, cos(scrollX), sin(scrollX), 18 | 0, -sin(scrollX), cos(scrollX) 19 | ); 20 | 21 | vec3 getRay() { 22 | vec2 normalizedCoords = gl_FragCoord.xy - vec2(0.5) + (offset / repeat); 23 | vec2 pixel = (normalizedCoords - 0.5 * screenSize) / min(screenSize.x, screenSize.y); 24 | 25 | // as if the higher the pixel value, the more the offset is being applied 26 | // normalize to get unit vector 27 | return (cameraDirection * normalize(vec4(pixel.x, pixel.y, 1, 0))).xyz; 28 | } 29 | 30 | float makeHoles(vec3 p, float h) { 31 | p = min(abs(p) - h, 0.); 32 | return max(max(-max(p.z, p.y), -max(p.x, p.z)), -max(p.x, p.y)); 33 | } 34 | 35 | float box(vec3 p, float b) { 36 | p = abs(p) - b; 37 | return length(max(p, 0.0)) + min(max(p.x, max(p.y, p.z)), 0.0); 38 | } 39 | 40 | vec3 opRepeat(vec3 p, vec3 distance) { 41 | return mod(p + 0.5 * distance, distance) - 0.5 * distance; 42 | } 43 | 44 | const int MENGER_ITERATIONS = 7; 45 | float menger(vec3 p, float b, float h) { 46 | float box = box(p, b); 47 | float holes = makeHoles(p, h); 48 | float scale = h; 49 | for (int i = 0; i < MENGER_ITERATIONS; i++) { 50 | p = rotmat * p + vec3(-2. * scale, -2. * scale, -2. * scale); 51 | holes = max(holes, makeHoles(opRepeat(p, vec3(2. * scale)), h * scale)); 52 | scale = scale * h; 53 | } 54 | return max(box, holes); 55 | } 56 | 57 | float doModel(vec3 p) { 58 | return menger( 59 | opRepeat(p, vec3(10.)), 60 | 3., 61 | 1. / 3. + scrollY / 10. 62 | ); 63 | } 64 | // this is kinda contrived and does a bunch of stuff I'm not using right now, but I'll leave it like this for now 65 | vec3 trace(vec3 origin, vec3 direction, out int iterations) { 66 | vec3 position = origin; 67 | for(int i = 0; i < MAX_ITER; i++) { 68 | iterations = i; 69 | float d = doModel(position); 70 | if (d < HIT_THRESHOLD) break; 71 | position += d * direction; 72 | } 73 | return position; 74 | } 75 | 76 | // vec3 lightDirection = normalize(vec3(1, -1, -1)); 77 | float getIllumination(vec3 collision, int iterations) { 78 | // vec3 n = calcNormal(collision); 79 | float occlusionLight = 1. - float(iterations) / float(MAX_ITER); 80 | return occlusionLight; 81 | // return dot(n, lightDirection); 82 | } 83 | 84 | // const float col = 0.05; // amount of coloring 85 | 86 | void main() { 87 | vec3 direction = getRay(); 88 | // gl_FragColor = vec4(offset / (repeat - vec2(1)), 0, 1); 89 | // return; 90 | 91 | float brightness = 0.; 92 | int iterations; 93 | vec3 collision = trace(cameraPosition * 20., direction, iterations); 94 | if (iterations < MAX_ITER - 1) { // actual collision 95 | brightness = getIllumination(collision, iterations); 96 | } 97 | gl_FragColor = vec4( 98 | brightness, 99 | brightness, 100 | brightness, 101 | 1. 102 | ); 103 | } -------------------------------------------------------------------------------- /fractals/menger/thumbnail.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ath92/fractal-garden/0b53370de317765a2b72a319c52cd544d70ebea5/fractals/menger/thumbnail.png -------------------------------------------------------------------------------- /fractals/plant/plant.glsl: -------------------------------------------------------------------------------- 1 | precision highp float; 2 | uniform vec2 screenSize; 3 | uniform vec2 offset; 4 | uniform vec2 repeat; 5 | uniform float time; 6 | uniform vec3 cameraPosition; 7 | uniform mat4 cameraDirection; 8 | 9 | const int MAX_ITER = 128; 10 | const float HIT_THRESHOLD = 0.001; 11 | const float variance = 0.01; 12 | // const float PI = 3.14159265359; 13 | 14 | 15 | vec3 getRay() { 16 | vec2 normalizedCoords = gl_FragCoord.xy - vec2(0.5) + (offset / repeat); 17 | vec2 pixel = (normalizedCoords - 0.5 * screenSize) / min(screenSize.x, screenSize.y); 18 | 19 | // as if the higher the pixel value, the more the offset is being applied 20 | // normalize to get unit vector 21 | return (cameraDirection * normalize(vec4(pixel.x, pixel.y, 1, 0))).xyz; 22 | } 23 | 24 | vec3 zRotate(vec3 p, float zRotationRads) { 25 | float cosTheta = cos(zRotationRads); 26 | float sinTheta = sin(zRotationRads); 27 | mat3 rotation = mat3( 28 | 1, 0, 0, 29 | 0, cosTheta, -sinTheta, 30 | 0, sinTheta, cosTheta 31 | ); 32 | return rotation * p; 33 | } 34 | 35 | vec3 xMirror(vec3 p) { 36 | return vec3(abs(p.x), p.y, p.z); 37 | } 38 | 39 | vec3 yMirror(vec3 p) { 40 | return vec3(p.x, abs(p.y), p.z); 41 | } 42 | 43 | vec3 zMirror(vec3 p) { 44 | return vec3(p.x, p.y, abs(p.z)); 45 | } 46 | 47 | 48 | vec3 opRepeat(vec3 p, vec3 distance) { 49 | return mod(p + 0.5 * distance, distance) - 0.5 * distance; 50 | } 51 | 52 | float trunk(vec3 p, vec2 xz, float b) { 53 | vec3 nearest = vec3(xz.x, p.y, xz.y); 54 | return distance(nearest, p) - b; 55 | } 56 | 57 | vec3 curl(vec3 p, float k) { 58 | float c = cos(k*p.y); 59 | float s = sin(k*p.y); 60 | mat2 m = mat2(c,-s,s,c); 61 | return vec3(m*p.xz,p.y); 62 | } 63 | 64 | float doModel(vec3 p) { 65 | vec3 repeated = opRepeat(p, vec3(15., 30., 199.)); 66 | vec3 transformed = zRotate(zMirror(yMirror(repeated)), 1.); 67 | return trunk( 68 | curl(p, 0.5), 69 | vec2(1., 1.), 70 | 1. 71 | ); 72 | } 73 | // this is kinda contrived and does a bunch of stuff I'm not using right now, but I'll leave it like this for now 74 | vec3 trace(vec3 origin, vec3 direction, out int iterations) { 75 | vec3 position = origin; 76 | for(int i = 0; i < MAX_ITER; i++) { 77 | iterations = i; 78 | float d = doModel(position); 79 | if (d < HIT_THRESHOLD) break; 80 | position += d * direction; 81 | } 82 | return position; 83 | } 84 | 85 | // vec3 lightDirection = normalize(vec3(1, -1, -1)); 86 | float getIllumination(vec3 collision, int iterations) { 87 | // vec3 n = calcNormal(collision); 88 | float occlusionLight = 1. - float(iterations) / float(MAX_ITER); 89 | return occlusionLight; 90 | // return dot(n, lightDirection); 91 | } 92 | 93 | // const float col = 0.05; // amount of coloring 94 | 95 | void main() { 96 | vec3 direction = getRay(); 97 | // gl_FragColor = vec4(offset / (repeat - vec2(1)), 0, 1); 98 | // return; 99 | 100 | float brightness = 0.; 101 | int iterations; 102 | vec3 collision = trace(cameraPosition, direction, iterations); 103 | if (iterations < MAX_ITER - 1) { // actual collision 104 | brightness = getIllumination(collision, iterations); 105 | } 106 | gl_FragColor = vec4( 107 | brightness, 108 | brightness, 109 | brightness, 110 | 1. 111 | ); 112 | } -------------------------------------------------------------------------------- /fractals/shaded-mandelbulb/frag.glsl: -------------------------------------------------------------------------------- 1 | precision highp float; 2 | uniform vec2 screenSize; 3 | uniform vec2 offset; 4 | uniform vec2 repeat; 5 | uniform float time; 6 | uniform vec3 cameraPosition; 7 | uniform mat4 cameraDirection; 8 | uniform bool onlyDistance; 9 | uniform float scrollX; 10 | uniform float scrollY; 11 | 12 | const float hitThreshold = 0.00025; 13 | 14 | const int CAMERA_ITERATIONS = 140; 15 | const int LIGHT_ITERATIONS= 60; 16 | 17 | const vec3 spaceRepetition = vec3(12); 18 | 19 | vec3 getRay(vec2 xy) { 20 | vec2 normalizedCoords = xy - vec2(0.5) + (offset / repeat); 21 | vec2 pixel = (normalizedCoords - 0.5 * screenSize) / min(screenSize.x, screenSize.y); 22 | 23 | // normalize to get unit vector 24 | return (cameraDirection * normalize(vec4(pixel.x, pixel.y, 1, 0))).xyz; 25 | } 26 | 27 | vec3 opRepeat(vec3 p, vec3 distance) { 28 | return mod(p + 0.5 * distance, distance) - 0.5 * distance; 29 | } 30 | 31 | float doModel(vec3 p) { 32 | vec3 pos = opRepeat(p, spaceRepetition); 33 | vec3 z = pos; 34 | float dr = 1.; 35 | float r = 0.0; 36 | for (int i = 0; i < 10; i++) { 37 | r = length(z); 38 | if (r > 2.) break; 39 | 40 | // convert to polar coordinates 41 | float theta = acos(z.z / r); 42 | float phi = atan(z.y, z.x); 43 | float power = 12. + sin(scrollY) * 10.; 44 | dr = pow(r, power - 1.) * power * dr + 1.5; 45 | 46 | // scale and rotate the point 47 | float zr = pow(r, power); 48 | theta = theta * power; 49 | phi = phi * power; 50 | 51 | // convert back to cartesian coordinates 52 | z = zr * vec3(sin(theta) * cos(phi), sin(phi) * sin(theta), cos(theta)); 53 | z += pos; 54 | } 55 | return abs(0.5 * log(r) * r / dr); 56 | } 57 | 58 | vec3 calcNormal(vec3 p, float h) { 59 | const vec2 k = vec2(1,-1); 60 | return normalize( k.xyy*doModel( p + k.xyy*h ) + 61 | k.yyx*doModel( p + k.yyx*h ) + 62 | k.yxy*doModel( p + k.yxy*h ) + 63 | k.xxx*doModel( p + k.xxx*h ) ); 64 | } 65 | 66 | vec3 light = normalize(vec3(sin(scrollX), 3, cos(scrollX))); 67 | const float mint = 5. * hitThreshold; 68 | const float maxt = 1.; 69 | const float k = 8.; 70 | const float fogNear = 10.; 71 | const float fogFar = 20.; 72 | // this is kinda contrived and does a bunch of stuff I'm not using right now, but I'll leave it like this for now 73 | float trace(vec3 origin, vec3 direction, out vec3 collision, out int iterations, out float fog) { 74 | vec3 position = origin; 75 | float distanceTraveled = 0.; 76 | float d = 0.; 77 | float h = hitThreshold; 78 | for(int i = 0; i <= CAMERA_ITERATIONS; i++) { 79 | iterations = i; 80 | d = doModel(position); 81 | h = max(hitThreshold * distanceTraveled, hitThreshold / 20.); 82 | if (d < h) break; 83 | position += d * direction; 84 | distanceTraveled += d; 85 | if (distanceTraveled > fogFar) break; 86 | } 87 | fog = max(0., (distance(position, origin) - fogNear) / (fogFar - fogNear)); 88 | if (iterations == CAMERA_ITERATIONS || distanceTraveled > fogFar) { 89 | iterations = 0; 90 | fog = 1.; 91 | return dot(direction, light); 92 | } 93 | collision = position; 94 | vec3 n = calcNormal(collision, h); 95 | float t = mint; 96 | float res = 1.0; 97 | float pd = 1e1; 98 | for (int i = 0; i < LIGHT_ITERATIONS; i++) { 99 | position = collision + light * t; 100 | d = doModel(position); 101 | if (d < hitThreshold * distanceTraveled){ 102 | return 0.; 103 | // return (t - mint) / (maxt - mint); 104 | }; 105 | if (t > maxt) { 106 | res = 1.; 107 | break; 108 | } 109 | float y = d*d/(2.0*pd); 110 | float h = sqrt(d*d-y*y); 111 | res = min( res, k*h/max(0.,t-y) ); 112 | pd = d; 113 | t += d; 114 | } 115 | return max(0., sqrt(res) * dot(n, light)); 116 | } 117 | 118 | float occlusion(int iterations) { 119 | float occlusionLight = 1. - float(iterations) / float(CAMERA_ITERATIONS); 120 | return occlusionLight; 121 | } 122 | 123 | // const float col = 0.05; // amount of coloring 124 | 125 | vec3 hsl2rgb( in vec3 c ) { 126 | vec3 rgb = clamp( abs(mod(c.x*6.0+vec3(0.0,4.0,2.0),6.0)-3.0)-1.0, 0.0, 1.0 ); 127 | return c.z + c.y * (rgb-0.5)*(1.0-abs(2.0*c.z-1.0)); 128 | } 129 | 130 | vec3 getColor(float it, float d) { 131 | return hsl2rgb(vec3( 132 | d, 133 | 0.6, 134 | pow(it, 0.8) 135 | )); 136 | } 137 | 138 | void main() { 139 | vec3 direction = getRay(gl_FragCoord.xy); 140 | 141 | int iterations; 142 | vec3 collision; 143 | float fog; 144 | float lightStrength = trace(cameraPosition + vec3(0, 0.7, 7.7), direction, collision, iterations, fog); 145 | 146 | float fogColor = dot(direction, light); 147 | 148 | float d = distance(collision, cameraPosition); 149 | float ol = .25; 150 | gl_FragColor = vec4( 151 | vec3((ol * occlusion(iterations) + (1. - ol) * lightStrength) * (1. - fog) + fog * fogColor), 152 | 1. 153 | ); 154 | } -------------------------------------------------------------------------------- /fractals/spheres/spheres.glsl: -------------------------------------------------------------------------------- 1 | precision highp float; 2 | uniform vec2 screenSize; 3 | uniform vec2 offset; 4 | uniform vec2 repeat; 5 | uniform float time; 6 | uniform vec3 cameraPosition; 7 | uniform mat4 cameraDirection; 8 | 9 | const int MAX_ITER = 40; 10 | const float HIT_THRESHOLD = 0.000001; 11 | const float variance = 0.01; 12 | // const float PI = 3.14159265359; 13 | 14 | 15 | vec3 getRay() { 16 | vec2 normalizedCoords = gl_FragCoord.xy - vec2(0.5) + (offset / repeat); 17 | vec2 pixel = (normalizedCoords - 0.5 * screenSize) / min(screenSize.x, screenSize.y); 18 | 19 | // as if the higher the pixel value, the more the offset is being applied 20 | // normalize to get unit vector 21 | return (cameraDirection * normalize(vec4(pixel.x, pixel.y, 1, 0))).xyz; 22 | } 23 | 24 | float sphere(vec3 p, float radius) { 25 | return length(p) - radius; 26 | } 27 | 28 | float hollowSphere(vec3 p, float radius, float thickness) { 29 | float outer = sphere(p, radius); 30 | float inner = sphere(p, radius - thickness); 31 | // inner will be bigger outside itself 32 | // and bigger inside itself 33 | // we only want to use it when it's smaller than zero 34 | float combined = max(outer, -inner); 35 | return combined; 36 | } 37 | 38 | float box(vec3 p, vec3 box) { 39 | vec3 q = abs(p) - box; 40 | return length(max(q,0.0)) + min(max(q.x,max(q.y,q.z)),0.0); 41 | } 42 | 43 | vec3 yRotate(vec3 p, float a) { 44 | float cosa = cos(a); 45 | float sina = sin(a); 46 | return vec3( 47 | cosa * p.x + -sina * p.z, 48 | p.y, 49 | cosa * p.z + sina * p.x 50 | ); 51 | } 52 | 53 | float doModel(vec3 p) { 54 | float distance = 99999.; 55 | for (float i = 0.; i < 6.; i++) { 56 | vec3 p2 = yRotate(p, i / 3. + 0.5 * cos(time / 1000. + i / 0.1 * i) - 1.5); 57 | float r = 3. - i / 2.; 58 | float s = hollowSphere(p2, r, 0.2); 59 | float b = box(p2 - vec3(0,0,r), vec3(r)); 60 | distance = min(distance, max(b, s)); 61 | } 62 | return distance; 63 | } 64 | // this is kinda contrived and does a bunch of stuff I'm not using right now, but I'll leave it like this for now 65 | vec3 trace(vec3 origin, vec3 direction, out int iterations) { 66 | vec3 position = origin; 67 | for(int i = 0; i < MAX_ITER; i++) { 68 | iterations = i; 69 | float d = doModel(position); 70 | if (d < HIT_THRESHOLD) break; 71 | position += d * direction; 72 | } 73 | return position; 74 | } 75 | 76 | // vec3 lightDirection = normalize(vec3(1, -1, -1)); 77 | float getIllumination(vec3 collision, int iterations) { 78 | // vec3 n = calcNormal(collision); 79 | float occlusionLight = 1. - float(iterations) / float(MAX_ITER); 80 | return occlusionLight; 81 | // return dot(n, lightDirection); 82 | } 83 | 84 | // const float col = 0.05; // amount of coloring 85 | 86 | void main() { 87 | vec3 direction = getRay(); 88 | // gl_FragColor = vec4(offset / (repeat - vec2(1)), 0, 1); 89 | // return; 90 | 91 | float brightness = 0.; 92 | int iterations; 93 | vec3 collision = trace(cameraPosition, direction, iterations); 94 | if (iterations < MAX_ITER - 1) { // actual collision 95 | brightness = getIllumination(collision, iterations); 96 | } 97 | gl_FragColor = vec4( 98 | brightness, 99 | brightness, 100 | brightness, 101 | 1. 102 | ); 103 | } -------------------------------------------------------------------------------- /get-speed.js: -------------------------------------------------------------------------------- 1 | import { vec3 } from 'gl-matrix'; 2 | 3 | const spaceRepeat = vec3.fromValues(2.5, 2.5, 2.5); 4 | 5 | /** 6 | * glsl mod 7 | * @param {number} x 8 | * @param {number} y 9 | */ 10 | const mod = (x, y) => x - y * Math.floor(x/y); 11 | 12 | /** 13 | * space repetition 14 | * @param {vec3} p 15 | * @param {vec3} distance 16 | */ 17 | function opRepeat(p, distance) { 18 | return p.map((n, i) => { 19 | const d = distance[i]; 20 | return mod((n + 0.5 * d), d) - 0.5 * d; 21 | }); 22 | } 23 | 24 | /** 25 | * js implementation of mandelbulb distance function 26 | * @param {vec3} p 27 | */ 28 | const getCurrentDistance = (p) => { 29 | return 50; 30 | const Power = 12.0; 31 | const pos = opRepeat(p, spaceRepeat); 32 | let z = vec3.clone(pos); 33 | let dr = 1.0; 34 | let r = 0.0; 35 | for (let i = 0; i < 10; i++) { 36 | r = vec3.length(z); 37 | if (r > 4) break; 38 | 39 | // convert to polar coordinates 40 | let theta = Math.acos(z[2] / r); 41 | let phi = Math.atan(z[1], z[0]); 42 | dr = Math.pow(r, Power - 1) * Power * dr + 1.0; 43 | 44 | // scale and rotate the point 45 | const zr = Math.pow(r, Power); 46 | theta = theta * Power; 47 | phi = phi * Power; 48 | 49 | // convert back to cartesian coordinates 50 | vec3.scale(z, vec3.fromValues(Math.sin(theta) * Math.cos(phi), Math.sin(phi) * Math.sin(theta), Math.cos(theta)), zr); 51 | vec3.add(z, z, pos); 52 | } 53 | return 0.5 * Math.log(r) * r / dr; 54 | } 55 | 56 | export default getCurrentDistance; -------------------------------------------------------------------------------- /headless.js: -------------------------------------------------------------------------------- 1 | import Regl from "regl"; 2 | import fragmentShaders from './fractals/**/frag.glsl'; 3 | // import frag from './mandelbulb.glsl'; 4 | import passThroughVert from './pass-through-vert.glsl'; 5 | import headlessGL from "gl"; 6 | import { vec3, mat4, quat } from 'gl-matrix'; 7 | 8 | const setup = (fractal = 'mandelbulb', width = 1000, height = 1000) => { 9 | const fragmentShader = fragmentShaders[fractal]; 10 | 11 | const repeat = [1, 1]; 12 | const offsets = []; 13 | 14 | // const renderFrame = (state) => { 15 | // const steps = renderer.generateRenderSteps(state); 16 | // let step = steps.next(); 17 | // while (!step.done) { // this shouldn't be necessary since we shouldn't be generating more than one render step 18 | // step = steps.next(); 19 | // } 20 | // const fbo = step.value; 21 | // renderer.drawToCanvas({ texture: fbo }); 22 | // console.log(renderer.regl.read()); 23 | 24 | // return renderer.regl.read(); 25 | // } 26 | const context = headlessGL(width, height, { preserveDrawingBuffer: true }); 27 | const regl = Regl(context); 28 | 29 | // screen-filling rectangle 30 | const position = regl.buffer([ 31 | [-1, -1], 32 | [1, -1], 33 | [1, 1], 34 | [-1, -1], 35 | [1, 1,], 36 | [-1, 1] 37 | ]); 38 | 39 | const renderSDF = regl({ 40 | frag: fragmentShader, 41 | vert: passThroughVert.replace("#define GLSLIFY 1", ""), 42 | uniforms: { 43 | screenSize: regl.prop('screenSize'), 44 | cameraPosition: regl.prop('cameraPosition'), 45 | cameraDirection: regl.prop('cameraDirection'), 46 | offset: regl.prop('offset'), 47 | repeat: regl.prop('repeat'), 48 | scrollX: regl.prop('scrollX'), 49 | scrollY: regl.prop('scrollY'), 50 | }, 51 | attributes: { 52 | position 53 | }, 54 | count: 6, 55 | }); 56 | 57 | const renderFrame = (state) => { 58 | regl.clear({ 59 | depth: 1, 60 | }); 61 | 62 | renderSDF({ 63 | ...state, 64 | repeat, 65 | offset: [0, 0], 66 | screenSize: [width, height] 67 | }); 68 | return regl.read(); 69 | } 70 | 71 | function* renderFrames(frames, fps = 30) { 72 | const first = frames[0]; 73 | const last = frames[frames.length - 1]; 74 | const timespan = (last.time - first.time); 75 | const numFrames = Math.ceil(timespan / (1000 / fps)); 76 | // for (let frame of frames) { 77 | // yield renderFrame(frame.state); 78 | // }; 79 | console.log("timespan:", timespan, "at", numFrames); 80 | /** 81 | * frame 3 82 | * 48 + firstime 83 | * 84 | */ 85 | for (let i = 0; i < numFrames; i++) { 86 | const time = i * (1000 / fps) + first.time; 87 | const endFrameIndex = frames.findIndex((frame) => frame.time >= time); 88 | if (endFrameIndex === -1) break; 89 | console.log("State frame", endFrameIndex); 90 | const endFrame = frames[endFrameIndex]; 91 | 92 | // now interpolate all state 93 | const startFrame = endFrameIndex === 0 ? endFrame : frames[endFrameIndex - 1]; 94 | const progress = (time - startFrame.time) / (endFrame.time - startFrame.time); 95 | console.log("Video frame", progress) 96 | const scrollX = (1 - progress) * startFrame.state.scrollX + progress * endFrame.state.scrollX; 97 | const scrollY = (1 - progress) * startFrame.state.scrollY + progress * endFrame.state.scrollY; 98 | const cameraPosition = vec3.add(vec3.create(), vec3.scale(vec3.create(), startFrame.state.cameraPosition, 1 - progress), vec3.scale(vec3.create(), endFrame.state.cameraPosition, progress)); 99 | const cameraDirectionQuat = quat.add(quat.create(), quat.scale(quat.create(), startFrame.state.cameraDirectionQuat, 1 - progress), quat.scale(quat.create(), endFrame.state.cameraDirectionQuat, progress)); 100 | const cameraDirection = mat4.fromQuat(mat4.create(), cameraDirectionQuat); 101 | const interpolatedState = { 102 | scrollX, 103 | scrollY, 104 | cameraPosition, 105 | cameraDirection, 106 | }; 107 | yield renderFrame(interpolatedState); 108 | } 109 | } 110 | 111 | return { 112 | renderFrame, 113 | renderFrames, 114 | }; 115 | }; 116 | 117 | export default setup; 118 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Fractal garden 5 | 6 | 7 | 8 | 93 | 94 | 95 |
96 |
97 |

Fractal.garden

98 |

99 | Explore 3d fractals in real time in your web browser. 100 |

101 | 108 | Star 109 | 110 |
111 |
112 | 113 | Menger sponge 114 | 115 | 116 | Mandelbulb 117 | 118 | 119 | Mandelbox 120 | 121 | 122 | Pseudo-Kleinian 123 | 124 |
125 | 128 | 129 | 130 | 131 | -------------------------------------------------------------------------------- /main.js: -------------------------------------------------------------------------------- 1 | import Regl from "regl"; 2 | import fragmentShaders from './fractals/**/frag.glsl'; 3 | import PlayerControls from './player-controls'; 4 | import setupRenderer from './renderer'; 5 | import 'setimmediate'; 6 | 7 | const urlParams = new URLSearchParams(window.location.search); 8 | const fractal = urlParams.get('fractal') || 'mandelbulb'; 9 | 10 | const fragmentShader = fragmentShaders[fractal]; 11 | 12 | const controlsMap = { 13 | klein: [0.0005] 14 | } 15 | 16 | const currentControls = controlsMap[fractal] 17 | let controller; 18 | if (currentControls){ 19 | controller = new PlayerControls(currentControls) 20 | } else { 21 | controller = new PlayerControls() 22 | } 23 | 24 | const getRenderSettings = (performance) => { 25 | // For larger screens 26 | // The render function is divided into a certain number of steps. This is done horizontally and vertically; 27 | // In each step 1/(x*y)th (1/x horizontal and 1/y vertical) of all pixels on the screen are rendered 28 | // If there is not enough time left to maintain a reasonable FPS, the renderer can bail at any time after the first step. 29 | // On small screens we do no upsampling, to reduce the amount of overhead introduced 30 | if (performance === 1 || window.innerWidth <= 800) return { 31 | repeat: [1, 1], 32 | offsets: [], 33 | }; 34 | 35 | if (performance === 2) return { 36 | repeat: [2, 2], 37 | offsets: [ 38 | [1, 1], 39 | [0, 1], 40 | [1, 0] 41 | ], 42 | }; 43 | 44 | // Each render step gets an offset ([0, 0] in the first, mandatory step) 45 | // This controls what pixels are used to draw each render step 46 | if (performance === 3) return { 47 | repeat: [3, 3], 48 | offsets: [ 49 | [2, 2], 50 | [0, 2], 51 | [2, 0], 52 | [1, 1], 53 | [1, 0], 54 | [0, 1], 55 | [2, 1], 56 | [1, 2] 57 | ], 58 | }; 59 | 60 | return { 61 | repeat: [4, 4], 62 | offsets: [ 63 | [2, 2], 64 | [3, 0], 65 | [0, 3], 66 | [1, 1], 67 | [3, 3], 68 | [2, 1], 69 | [1, 2], 70 | [1, 0], 71 | [3, 1], 72 | [2, 3], 73 | [0, 2], 74 | [2, 0], 75 | [3, 2], 76 | [1, 3], 77 | [0, 1] 78 | ], 79 | } 80 | } 81 | 82 | const init = (performance) => { 83 | const { repeat, offsets } = getRenderSettings(performance); 84 | let canvas = document.querySelector('canvas'); 85 | if (canvas) { 86 | canvas.remove(); 87 | } 88 | canvas = document.createElement('canvas'); 89 | document.querySelector('.container').appendChild(canvas); 90 | // resize to prevent rounding errors 91 | let width = window.innerWidth; 92 | let height = Math.min(window.innerHeight, Math.floor(width * (window.innerHeight / window.innerWidth))); 93 | while (width % repeat[0]) width--; 94 | while (height % repeat[1]) height--; 95 | canvas.width = width; 96 | canvas.height = height; 97 | const context = canvas.getContext("webgl", { 98 | preserveDrawingBuffer: true, 99 | desynchronized: true, 100 | }); 101 | 102 | const regl = Regl(context); // no params = full screen canvas 103 | 104 | const renderer = setupRenderer({ 105 | frag: fragmentShader, 106 | regl, 107 | repeat, 108 | offsets, 109 | width, 110 | height, 111 | }); 112 | 113 | // This controls the FPS (not in an extremely precise way, but good enough) 114 | // 60fps + 4ms timeslot for drawing to canvas and doing other things 115 | const threshold = 1000 / 120; 116 | 117 | // This essentially checks if the state has changed by doing a deep equals 118 | // If there are changes, it returns a new object so in other places, we can just check if the references are the same 119 | const getCurrentState = (() => { 120 | let current; 121 | return () => { 122 | const newState = controller.state; 123 | if (JSON.stringify(current) !== JSON.stringify(newState)) { 124 | current = newState; 125 | } 126 | return current; 127 | } 128 | })(); 129 | 130 | // In order to check if the state has changes, we poll the player controls every frame. 131 | // TODO: refactor this so state changes automatically schedules re-render 132 | function pollForChanges(callbackIfChanges, lastFBO) { 133 | const currentState = getCurrentState(); 134 | (function checkForChanges() { 135 | // TODO: not sure why it is necessary to re-draw the last fbo here. 136 | // Sometimes the last FBO is not drawn in the render step. 137 | renderer.drawToCanvas({ texture: lastFBO }); 138 | const newState = getCurrentState(); 139 | if (newState !== currentState) { 140 | callbackIfChanges(newState); 141 | } else { 142 | requestAnimationFrame(checkForChanges); 143 | } 144 | })(); 145 | } 146 | 147 | let bail = false; 148 | let frameCallback = null; 149 | function onEnterFrame(state) { 150 | if (bail) { 151 | renderer.regl.destroy(); 152 | return; 153 | } 154 | if (frameCallback) { 155 | frameCallback(state); 156 | } 157 | const start = Date.now(); 158 | const render = renderer.generateRenderSteps(state); 159 | let i = 0; 160 | (function step() { 161 | regl.clear({ 162 | depth: 1, 163 | }); 164 | i++; 165 | const { value: fbo, done } = render.next(); 166 | const now = Date.now(); 167 | 168 | if (done) { 169 | console.log("frametime",now-start) 170 | renderer.drawToCanvas({ texture: fbo }); 171 | pollForChanges(onEnterFrame, fbo); 172 | return; 173 | } 174 | 175 | const newState = getCurrentState(); 176 | const stateHasChanges = newState !== state; 177 | if (now - start > threshold) { 178 | // out of time, draw to screen 179 | renderer.drawToCanvas({ texture: fbo }); 180 | // console.log(i); // amount of render steps completed 181 | if (stateHasChanges) { 182 | requestAnimationFrame(() => onEnterFrame(newState)); 183 | return; 184 | } 185 | } 186 | setImmediate(step, 0); 187 | })(); 188 | } 189 | onEnterFrame(getCurrentState()); 190 | 191 | return { 192 | stop: () => { 193 | bail = true; 194 | }, 195 | getFrameStates: callback => { 196 | frameCallback = callback; 197 | }, 198 | } 199 | } 200 | 201 | 202 | let perf = 2; 203 | let instance = init(perf); 204 | // reinit on resize 205 | window.addEventListener('resize', () => { 206 | instance.stop(); 207 | instance = init(perf); 208 | }); 209 | 210 | let recording = false; 211 | let frames = []; 212 | document.addEventListener('keydown', e => { 213 | if (['1', '2', '3', '4'].some(p => p === e.key)) { 214 | instance.stop(); 215 | perf = parseInt(e.key); 216 | instance = init(perf); 217 | } 218 | if (e.key === 'r') { 219 | // record 220 | // renderSingleFrame(controller.state); 221 | if (!recording) { 222 | frames = []; 223 | instance.getFrameStates((state) => frames.push({ time: Date.now(), state })); 224 | } else { 225 | console.log(frames); 226 | fetch(`http://localhost:3000/render/${fractal}`, { 227 | headers: { 228 | 'Accept': 'application/json', 229 | 'Content-Type': 'application/json' 230 | }, 231 | method: 'post', 232 | body: JSON.stringify({ frames }), 233 | }).then(console.log); 234 | frames = []; 235 | } 236 | recording = !recording; 237 | } 238 | }) 239 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "raymarching", 3 | "version": "1.0.0", 4 | "description": "raymarching with regl attempt", 5 | "scripts": { 6 | "dev": "parcel index.html", 7 | "build": "parcel build index.html", 8 | "server": "parcel build server/main.js --out-dir server/dist --target node && node server/dist/main.js" 9 | }, 10 | "author": "Tom Hutman", 11 | "license": "MIT", 12 | "dependencies": { 13 | "cors": "^2.8.5", 14 | "cuid": "^2.1.8", 15 | "express": "^4.17.1", 16 | "gl": "^4.8.0", 17 | "gl-matrix": "^3.2.1", 18 | "parcel-bundler": "^1.12.5", 19 | "regl": "^1.4.2", 20 | "setimmediate": "^1.0.5", 21 | "sharp": "^0.26.2" 22 | }, 23 | "devDependencies": { 24 | "@babel/cli": "^7.11.6", 25 | "@babel/core": "^7.11.6", 26 | "@babel/plugin-proposal-class-properties": "^7.10.4", 27 | "@babel/plugin-syntax-async-generators": "^7.8.4", 28 | "@babel/preset-env": "^7.11.5", 29 | "babel-core": "^6.26.3", 30 | "babel-plugin-transform-class-properties": "^6.24.1", 31 | "glslify-bundle": "^5.1.1", 32 | "glslify-deps": "^1.3.1", 33 | "parcel": "^1.12.4" 34 | }, 35 | "browserslist": [ 36 | "last 1 Chrome versions" 37 | ] 38 | } 39 | -------------------------------------------------------------------------------- /pass-through-vert.glsl: -------------------------------------------------------------------------------- 1 | precision highp float; 2 | attribute vec2 position; 3 | varying vec2 uv; 4 | void main() { 5 | uv = position; 6 | gl_Position = vec4(position, 0, 1); 7 | } -------------------------------------------------------------------------------- /player-controls.js: -------------------------------------------------------------------------------- 1 | import { vec3, mat4, quat } from 'gl-matrix'; 2 | import getCurrentDistance from './get-speed'; 3 | 4 | const forward = vec3.fromValues(0, 0, 1); 5 | const backward = vec3.fromValues(0, 0, -1); 6 | const left = vec3.fromValues(-1, 0, 0); 7 | const right = vec3.fromValues(1, 0, 0); 8 | 9 | const minSpeed = 0.00005; 10 | 11 | function getTouchEventCoordinates(touchEvent) { 12 | const lastTouch = touchEvent.touches[touchEvent.touches.length - 1]; 13 | return { 14 | x: lastTouch.clientX, 15 | y: lastTouch.clientY, 16 | } 17 | } 18 | 19 | export default class PlayerControls { 20 | constructor(acceleration = 0.00010, friction = 0.12, mouseSensitivity = 0.15, touchSensitivity = 0.012) { 21 | // TODO: cleanup event listeners 22 | this.acceleration = acceleration; 23 | this.friction = friction; 24 | this.speed = vec3.fromValues(0, 0, 0.01); 25 | this.mouseSensitivity = mouseSensitivity; 26 | this.touchSensitivity = touchSensitivity; 27 | this.position = vec3.fromValues(0, 0, -9); 28 | this.direction = quat.create(); 29 | this.isPanning = false; 30 | this.mouseX = 0; 31 | this.mouseY = 0; 32 | this.touchX = 0; 33 | this.touchY = 0; 34 | this.touchStartX = window.innerWidth / 2; 35 | this.touchStartY = window.innerHeight / 2; 36 | this.scrollX = 0; 37 | this.scrollY = 0; 38 | this.directionKeys = { 39 | forward: false, 40 | backward: false, 41 | left: false, 42 | right: false, 43 | }; 44 | this.sprintMode = false; 45 | this.isTouching = false; 46 | 47 | this.onPointerLock = () => {}; 48 | 49 | this.handleKeyboardEvent = keyboardEvent => { 50 | const { code, type, shiftKey } = keyboardEvent; 51 | const value = type === 'keydown'; 52 | if (code === 'KeyW' || code === 'ArrowUp') this.directionKeys.forward = value; 53 | if (code === 'KeyS' || code === 'ArrowDown') this.directionKeys.backward = value; 54 | if (code === 'KeyA' || code === 'ArrowLeft') this.directionKeys.left = value; 55 | if (code === 'KeyD' || code === 'ArrowRight') this.directionKeys.right = value; 56 | this.sprintMode = shiftKey; 57 | 58 | if (type === 'keydown' && code === 'KeyF') { 59 | if (!!document.pointerLockElement) { 60 | document.exitPointerLock(); 61 | } else { 62 | document.querySelector('body').requestPointerLock(); 63 | } 64 | } 65 | }; 66 | 67 | document.addEventListener('keydown', this.handleKeyboardEvent); 68 | document.addEventListener('keyup', this.handleKeyboardEvent); 69 | 70 | document.addEventListener('mousedown', (e) => { 71 | if (e.target.tagName !== 'CANVAS') { 72 | return; 73 | } 74 | this.isPanning = true; 75 | }); 76 | 77 | document.addEventListener('mouseup', (e) => { 78 | this.isPanning = false; 79 | }); 80 | 81 | document.addEventListener('pointerlockchange', () => { 82 | this.isPanning = !!document.pointerLockElement; 83 | }, false); 84 | 85 | document.addEventListener('mousemove', e => { 86 | if (!this.isPanning && !this.isTouching) return; 87 | this.hasMovedSinceMousedown = true; 88 | this.mouseX += e.movementX * this.mouseSensitivity; 89 | this.mouseY += e.movementY * this.mouseSensitivity; 90 | }); 91 | 92 | document.addEventListener('touchstart', e => { 93 | this.directionKeys.forward = true; 94 | this.isTouching = true; 95 | const { x, y } = getTouchEventCoordinates(e); 96 | this.touchX = x; 97 | this.touchY = y; 98 | this.touchStartX = x; 99 | this.touchStartY = y; 100 | }); 101 | 102 | document.addEventListener('touchmove', e => { 103 | const { x, y } = getTouchEventCoordinates(e); 104 | this.touchX = x; 105 | this.touchY = y; 106 | }); 107 | 108 | const onTouchOver = () => { 109 | this.directionKeys.forward = false; 110 | this.isTouching = false; 111 | } 112 | 113 | window.addEventListener("wheel", e => { 114 | this.scrollY += e.deltaY / 5000; 115 | this.scrollX += e.deltaX / 5000; 116 | }); 117 | 118 | document.addEventListener('touchend', onTouchOver); 119 | document.addEventListener('touchcancel', onTouchOver); 120 | document.addEventListener('mouseup', onTouchOver); 121 | 122 | requestAnimationFrame(() => this.loop()); 123 | } 124 | 125 | loop() { 126 | if (this.isTouching) { 127 | this.mouseX += (this.touchX - this.touchStartX) * this.touchSensitivity; 128 | this.mouseY += (this.touchY - this.touchStartY) * this.touchSensitivity; 129 | } 130 | this.mouseY = Math.min(this.mouseY, 90); 131 | this.mouseY = Math.max(this.mouseY, -90); 132 | 133 | quat.fromEuler( 134 | this.direction, 135 | this.mouseY, 136 | this.mouseX, 137 | 0 138 | ); 139 | 140 | // strafing with keys 141 | const diff = vec3.create(); 142 | if (this.directionKeys.forward) vec3.add(diff, diff, forward); 143 | if (this.directionKeys.backward) vec3.add(diff, diff, backward); 144 | if (this.directionKeys.left) vec3.add(diff, diff, left); 145 | if (this.directionKeys.right) vec3.add(diff, diff, right); 146 | if (typeof autoHideMessage === "function" && vec3.length(diff) > 1.1) autoHideMessage(); 147 | // vec3.normalize(diff, diff); 148 | vec3.transformQuat(diff, diff, this.direction); 149 | vec3.scale(diff, diff, (this.sprintMode ? 4 : 1) * this.acceleration); 150 | // const currentDistance = getCurrentDistance(this.position) 151 | vec3.scale(this.speed, this.speed, 1 - this.friction); 152 | if (vec3.length(this.speed) < minSpeed) { 153 | vec3.set(this.speed, 0, 0, 0); 154 | } 155 | vec3.add(this.speed, this.speed, diff); 156 | vec3.add(this.position, this.position, this.speed); 157 | 158 | requestAnimationFrame(() => this.loop()); 159 | } 160 | 161 | get state() { 162 | return { 163 | scrollX: this.scrollX, 164 | scrollY: this.scrollY, 165 | cameraPosition: [...this.position], 166 | cameraDirection: mat4.fromQuat(mat4.create(), this.direction), 167 | cameraDirectionQuat: [...this.direction], 168 | } 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /readme.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ath92/fractal-garden/0b53370de317765a2b72a319c52cd544d70ebea5/readme.gif -------------------------------------------------------------------------------- /renderer.js: -------------------------------------------------------------------------------- 1 | import passThroughVert from './pass-through-vert.glsl'; 2 | import upSampleFrag from './upsample.glsl'; 3 | 4 | function setupRenderer({ 5 | frag, 6 | regl, 7 | repeat = [1, 1], 8 | offsets = [], 9 | width, 10 | height, 11 | }) { 12 | // The FBO the actual SDF samples are rendered into 13 | let sdfTexture = regl.texture({ 14 | width: Math.round(width / repeat[0]), 15 | height: Math.round(height / repeat[1]) 16 | }); 17 | const sdfFBO = regl.framebuffer({ color: sdfTexture }); 18 | const getSDFFBO = () => sdfFBO({ color: sdfTexture }); 19 | 20 | // We need a double buffer in order to progressively add samples for each render step 21 | const createPingPongBuffers = textureOptions => { 22 | const tex1 = regl.texture(textureOptions); 23 | const tex2 = regl.texture(textureOptions); 24 | const one = regl.framebuffer({ 25 | color: tex1 26 | }); 27 | const two = regl.framebuffer({ 28 | color: tex2 29 | }); 30 | let counter = 0; 31 | return () => { 32 | counter++; 33 | if (counter % 2 === 0) { 34 | return one({ color: tex1 }); 35 | } 36 | return two({ color: tex2 }); 37 | } 38 | }; 39 | 40 | let getScreenFBO = createPingPongBuffers({ 41 | width, 42 | height, 43 | }); 44 | 45 | // screen-filling rectangle 46 | const position = regl.buffer([ 47 | [-1, -1], 48 | [1, -1], 49 | [1, 1], 50 | [-1, -1], 51 | [1, 1,], 52 | [-1, 1] 53 | ]); 54 | 55 | const renderSDF = regl({ 56 | frag, 57 | vert: passThroughVert.replace("#define GLSLIFY 1", ""), 58 | uniforms: { 59 | screenSize: regl.prop('screenSize'), 60 | cameraPosition: regl.prop('cameraPosition'), 61 | cameraDirection: regl.prop('cameraDirection'), 62 | worldMat: regl.prop('worldMat'), 63 | offset: regl.prop('offset'), 64 | repeat: regl.prop('repeat'), 65 | scrollX: regl.prop('scrollX'), 66 | scrollY: regl.prop('scrollY'), 67 | }, 68 | attributes: { 69 | position 70 | }, 71 | count: 6, 72 | }); 73 | 74 | // render texture to screen 75 | const drawToCanvas = regl({ 76 | vert: passThroughVert.replace("#define GLSLIFY 1\n", ""), 77 | frag: ` 78 | precision highp float; 79 | uniform sampler2D inputTexture; 80 | varying vec2 uv; 81 | 82 | void main () { 83 | vec4 color = texture2D(inputTexture, uv * 0.5 + 0.5); 84 | // vec4 color = vec4(uv.x, uv.y, 0, 1); 85 | gl_FragColor = color; 86 | } 87 | `, 88 | uniforms: { 89 | inputTexture: regl.prop('texture'), 90 | }, 91 | attributes: { 92 | position 93 | }, 94 | count: 6, 95 | }); 96 | 97 | const upSample = regl({ 98 | vert: passThroughVert.replace("#define GLSLIFY 1\n", ""), 99 | frag: upSampleFrag.replace("#define GLSLIFY 1\n", ""), 100 | uniforms: { 101 | inputSample: regl.prop('sample'), // sampler2D 102 | previous: regl.prop('previous'), // sampler2D 103 | repeat: regl.prop('repeat'), // vec2 104 | offset: regl.prop('offset'), // vec2 105 | screenSize: regl.prop('screenSize'), // vec2 106 | }, 107 | attributes: { 108 | position 109 | }, 110 | count: 6, 111 | }); 112 | 113 | // This generates each of the render steps, to be used in the main animation loop 114 | // By pausing the execution of this function, we can let the main thread handle events, gc, etc. between steps 115 | // It also allows us to bail early in case we ran out of time 116 | function* generateRenderSteps(renderState){ 117 | const fbo = getSDFFBO(); 118 | fbo.use(() => { 119 | renderSDF({ 120 | screenSize: [width / repeat[0], height / repeat[1]], 121 | offset: [0,0], 122 | repeat, 123 | ...renderState 124 | }); 125 | // console.log("hier moet het eigenlijk 255 zijn", regl.read()); 126 | }); 127 | yield fbo; 128 | 129 | let currentScreenBuffer = getScreenFBO(); 130 | currentScreenBuffer.use(() => { 131 | drawToCanvas({ texture: fbo }); 132 | }); 133 | 134 | const performUpSample = (previousScreenBuffer, offset) => { 135 | const newSampleFBO = getSDFFBO(); 136 | newSampleFBO.use(() => { 137 | renderSDF({ 138 | screenSize: [width / repeat[0], height / repeat[1]], 139 | offset, 140 | repeat, 141 | ...renderState 142 | }); 143 | }); 144 | 145 | const newScreenBuffer = getScreenFBO(); 146 | newScreenBuffer.use(() => { 147 | upSample({ 148 | sample: newSampleFBO, 149 | previous: previousScreenBuffer, 150 | repeat, 151 | offset, 152 | screenSize: [width, height], 153 | }); 154 | }); 155 | return newScreenBuffer; 156 | } 157 | 158 | for (let offset of offsets) { 159 | currentScreenBuffer = performUpSample(currentScreenBuffer, offset); 160 | yield currentScreenBuffer; 161 | } 162 | // also return the current screenbuffer so the last next() on the generator still gives a reference to what needs to be drawn 163 | return currentScreenBuffer; 164 | }; 165 | 166 | return { 167 | regl, 168 | drawToCanvas, // will draw fbo to canvas (or whatever was given as regl context) 169 | generateRenderSteps, // generator that yields FBOs, that can be drawn to the canvas 170 | } 171 | } 172 | 173 | export default setupRenderer; 174 | -------------------------------------------------------------------------------- /server/main.js: -------------------------------------------------------------------------------- 1 | import headlessRenderer from '../headless.js'; 2 | import express from "express"; 3 | import cors from "cors"; 4 | import sharp from "sharp"; 5 | import fs from "fs"; 6 | import cuid from "cuid"; 7 | const app = express(); 8 | app.use(cors()); 9 | app.use(express.json({ limit: "50mb" })); 10 | const port = 3000; 11 | 12 | const width = 1920; 13 | const height = 1080; 14 | 15 | const transformFrames = (frames) => { 16 | return frames.map(frame => { 17 | return { 18 | ...frame, 19 | state: { 20 | ...frame.state, 21 | cameraDirection: Object.values(frame.state.cameraDirection), // turn from object with string indices into array 22 | } 23 | } 24 | }) 25 | } 26 | 27 | app.get('/', (req, res) => { 28 | 29 | res.send('Hello World!') 30 | }); 31 | 32 | app.post('/render/:fractal', async (req, res) => { 33 | console.log(req.params); 34 | const headless = headlessRenderer(req.params.fractal, width, height); 35 | // console.log(req.body); 36 | const frames = headless.renderFrames(transformFrames(req.body?.frames)); 37 | const dir = `./render-results/${cuid()}`; 38 | 39 | console.log("going to render", frames.length, "frames"); 40 | 41 | fs.mkdirSync(dir); 42 | (async function step(i = 0) { 43 | const start = Date.now(); 44 | console.log("start frame", Date.now() - start); 45 | let { value: frame, done } = frames.next(); 46 | console.log("done rendering", Date.now() - start); 47 | if (done) return; 48 | const data = Buffer.from(frame); 49 | console.log("created buffer", Date.now() - start); 50 | console.log(data); 51 | try { 52 | const outputInfo = await sharp(data, { 53 | raw: { 54 | width, 55 | height, 56 | channels: 4, 57 | }, 58 | }).toFile(`${dir}/frame-${i}.png`); 59 | console.log("wrote file", outputInfo, Date.now() - start); 60 | } catch (e) { 61 | console.warn(e); 62 | } 63 | step(i + 1); 64 | })(); 65 | 66 | console.log("finished rendering images"); 67 | 68 | res.send('Great success!'); 69 | }) 70 | 71 | app.listen(port, () => { 72 | console.log(`Example app listening at http://localhost:${port}`) 73 | }) -------------------------------------------------------------------------------- /thumbnail.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ath92/fractal-garden/0b53370de317765a2b72a319c52cd544d70ebea5/thumbnail.png -------------------------------------------------------------------------------- /upsample.glsl: -------------------------------------------------------------------------------- 1 | precision highp float; 2 | precision mediump sampler2D; 3 | uniform sampler2D inputSample; 4 | uniform sampler2D previous; 5 | uniform vec2 offset; 6 | uniform vec2 repeat; 7 | uniform vec2 screenSize; 8 | 9 | const vec2 pixelOffset = vec2(0.499); 10 | 11 | vec2 modulo (vec2 a, vec2 b) { 12 | vec2 d = floor(a / b); 13 | vec2 q = d * b; 14 | return a - q; 15 | } 16 | 17 | float getMixFactor (vec2 position) { 18 | vec2 rest = modulo(position, repeat); 19 | vec2 diff = abs(rest - (offset)); 20 | return 1. - min(max(diff.x, diff.y), 1.); 21 | } 22 | 23 | void main () { 24 | vec2 position = gl_FragCoord.xy - pixelOffset; 25 | vec2 pixel = position / screenSize; 26 | 27 | vec4 previousColor = texture2D(previous, pixel); 28 | vec4 newColor = texture2D(inputSample, pixel); 29 | 30 | gl_FragColor = mix(previousColor, newColor, getMixFactor(position)); 31 | } 32 | 33 | // 1, 3 position 34 | // 1, 0 offset 35 | // 3, 3 repeat 36 | 37 | // 1, 0 rest 38 | // 0, 0 diff 39 | // 0 mix factor 40 | 41 | // 1 - 3 * floor(1/3) = 0 -> want it to be 2 42 | // 3 - 3 43 | 44 | // 0, 3 = 0 -> 0 - 3 * floor (0 / 3) = 0 45 | // 1, 3 = 1 -> 1 - 3 * floor (1 / 3) = 1; 46 | // 2, 3 = 2 -> 2 - 3 * floor (2 / 3) = 2 47 | // 3, 3 = 0 -> 3 - 3 * floor (3 / 3) = 0 48 | // 4, 3 = 1 -> 4 - 3 * floor (4 / 3) = 1 49 | // 5, 3 = 2 50 | // 6, 3 = 0 51 | // 7, 3 = 1 52 | 53 | // 1, 3 -> 1, 0 54 | 55 | 56 | // 2, 3 position 57 | // 2, 0 offset 58 | // 3, 3 repeat 59 | 60 | // restX = mod(2, 3) = 2 - 3 * floor(2 / 3) = 2 61 | // restY = mod(3, 3) = 3 - 3 * floor(3 / 3) = 0 62 | // 2, 0 rest 63 | // diff = (2, 0) - (2, 0) = (0, 0) 64 | // 1 - min(max(0,0), 1) = 0 -------------------------------------------------------------------------------- /viewer.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Fractal garden 5 | 6 | 7 | 94 | 95 | 96 | 97 | 98 | 99 | 100 |
101 |
102 |
103 |
104 |
105 | Click and drag to look around. Scroll to change shape.
106 | WASD / Arrow keys to move around. Hold shift key to go faster.
107 | Press 1, 2, 3, or 4 to select performance / quality.
108 | Press F to hide the cursor. 109 | View source on Github 110 |
111 |
112 | Touch and hold to move around
113 | View source on Github 114 |
115 |
116 | 119 |
120 | 121 | 147 | 148 | 149 | 150 | --------------------------------------------------------------------------------