├── .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 |
96 |
111 |
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 |
115 |
116 |
117 | ✕
118 |
119 |
120 |
?
121 |
147 |
148 |
149 |
150 |
--------------------------------------------------------------------------------