├── .gitignore
├── LICENSE
├── README.md
├── images
└── anim.gif
├── noise.js
├── package.json
├── wireframe_world.js
└── www
├── demo.html
└── demo.js
/.gitignore:
--------------------------------------------------------------------------------
1 | lib-cov
2 | *.seed
3 | *.log
4 | *.csv
5 | *.dat
6 | *.out
7 | *.pid
8 | *.gz
9 |
10 | pids
11 | logs
12 | results
13 |
14 | npm-debug.log
15 | node_modules/*
16 | *.DS_Store
17 |
18 | *.mp4
19 | *.jpg
20 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 |
2 |
3 | The MIT License (MIT)
4 |
5 | Copyright (c) 2016 Eric Arnebäck
6 |
7 | Permission is hereby granted, free of charge, to any person obtaining a copy
8 | of this software and associated documentation files (the "Software"), to deal
9 | in the Software without restriction, including without limitation the rights
10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11 | copies of the Software, and to permit persons to whom the Software is
12 | furnished to do so, subject to the following conditions:
13 |
14 | The above copyright notice and this permission notice shall be included in
15 | all copies or substantial portions of the Software.
16 |
17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
23 | THE SOFTWARE.
24 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # wireframe-world
2 |
3 | This demo draws an infinite, vaporwave-like world using the WebGL
4 | framework [regl](https://github.com/mikolalysenko/regl). A link to the
5 | demo is [here](http://erkaman.github.io/wireframe-world/www/demo.html). It should look
6 | like the below:
7 |
8 | 
9 |
10 | And click below for a longer video of the demo:
11 |
12 | [](https://www.youtube.com/watch?v=tE9geTQxgZc)
13 |
14 |
15 | ## Implementation Details
16 |
17 | As for the implementation, it is not very difficult stuff; I divide up
18 | the world into chunks(just like in Minecraft), and as the camera
19 | traverses the world, the chunks that become out of range are thrown
20 | away and are no longer rendered. And in the far away horizon I keep adding
21 | new chunks, to give the illusion that the world is infinite.
22 |
23 | ## Build
24 |
25 | To run the demo locally on your computer, first change your directory to the directory of the project, then run
26 |
27 | ```bash
28 | npm install
29 | ```
30 |
31 | To then run the demo, do
32 |
33 | ```bash
34 | npm start
35 | ```
36 |
37 |
38 | ## TODO
39 |
40 | Port the program into screensavers for OS X, Windows and Linux.
41 |
--------------------------------------------------------------------------------
/images/anim.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Erkaman/wireframe-world/d1a305dae50f45125ac152b7bc76ffba0c095667/images/anim.gif
--------------------------------------------------------------------------------
/noise.js:
--------------------------------------------------------------------------------
1 | /*
2 | This code was taken from here:
3 | https://github.com/josephg/noisejs
4 | */
5 |
6 | function Grad(x, y, z) {
7 | this.x = x; this.y = y; this.z = z;
8 | }
9 |
10 | Grad.prototype.dot2 = function(x, y) {
11 | return this.x * x + this.y * y;
12 | };
13 |
14 | var grad3 = [new Grad(1, 1, 0), new Grad(-1, 1, 0), new Grad(1, -1, 0), new Grad(-1, -1, 0),
15 | new Grad(1, 0, 1), new Grad(-1, 0, 1), new Grad(1, 0, -1), new Grad(-1, 0, -1),
16 | new Grad(0, 1, 1), new Grad(0, -1, 1), new Grad(0, 1, -1), new Grad(0, -1, -1)];
17 |
18 | var p = [151, 160, 137, 91, 90, 15,
19 | 131, 13, 201, 95, 96, 53, 194, 233, 7, 225, 140, 36, 103, 30, 69, 142, 8, 99, 37, 240, 21, 10, 23,
20 | 190, 6, 148, 247, 120, 234, 75, 0, 26, 197, 62, 94, 252, 219, 203, 117, 35, 11, 32, 57, 177, 33,
21 | 88, 237, 149, 56, 87, 174, 20, 125, 136, 171, 168, 68, 175, 74, 165, 71, 134, 139, 48, 27, 166,
22 | 77, 146, 158, 231, 83, 111, 229, 122, 60, 211, 133, 230, 220, 105, 92, 41, 55, 46, 245, 40, 244,
23 | 102, 143, 54, 65, 25, 63, 161, 1, 216, 80, 73, 209, 76, 132, 187, 208, 89, 18, 169, 200, 196,
24 | 135, 130, 116, 188, 159, 86, 164, 100, 109, 198, 173, 186, 3, 64, 52, 217, 226, 250, 124, 123,
25 | 5, 202, 38, 147, 118, 126, 255, 82, 85, 212, 207, 206, 59, 227, 47, 16, 58, 17, 182, 189, 28, 42,
26 | 223, 183, 170, 213, 119, 248, 152, 2, 44, 154, 163, 70, 221, 153, 101, 155, 167, 43, 172, 9,
27 | 129, 22, 39, 253, 19, 98, 108, 110, 79, 113, 224, 232, 178, 185, 112, 104, 218, 246, 97, 228,
28 | 251, 34, 242, 193, 238, 210, 144, 12, 191, 179, 162, 241, 81, 51, 145, 235, 249, 14, 239, 107,
29 | 49, 192, 214, 31, 181, 199, 106, 157, 184, 84, 204, 176, 115, 121, 50, 45, 127, 4, 150, 254,
30 | 138, 236, 205, 93, 222, 114, 67, 29, 24, 72, 243, 141, 128, 195, 78, 66, 215, 61, 156, 180];
31 | // To remove the need for index wrapping, double the permutation table length
32 | var perm = new Array(512);
33 | var gradP = new Array(512);
34 |
35 | // This isn't a very good seeding function, but it works ok. It supports 2^16
36 | // different seed values. Write something better if you need more seeds.
37 | seed = function(seed) {
38 | if(seed > 0 && seed < 1) {
39 | // Scale the seed out
40 | seed *= 65536;
41 | }
42 |
43 | seed = Math.floor(seed);
44 | if(seed < 256) {
45 | seed |= seed << 8;
46 | }
47 |
48 | for(var i = 0; i < 256; i++) {
49 | var v;
50 | if (i & 1) {
51 | v = p[i] ^ (seed & 255);
52 | } else {
53 | v = p[i] ^ ((seed>>8) & 255);
54 | }
55 |
56 | perm[i] = perm[i + 256] = v;
57 | gradP[i] = gradP[i + 256] = grad3[v % 12];
58 | }
59 | };
60 |
61 | seed(0);
62 |
63 | function fade(t) {
64 | // return t * t * t * (t * (t * 6-15)+10);
65 | return t
66 | }
67 |
68 | function lerp(a, b, t) {
69 | return (1-t) * a + t * b;
70 | }
71 |
72 | // 2D Perlin Noise
73 | module.exports = function(x, y) {
74 | // Find unit grid cell containing point
75 | var X = Math.floor(x), Y = Math.floor(y);
76 | // Get relative xy coordinates of point within that cell
77 | x = x - X; y = y - Y;
78 | // Wrap the integer cells at 255 (smaller integer period can be introduced here)
79 | X = X & 255; Y = Y & 255;
80 |
81 | // Calculate noise contributions from each of the four corners
82 | var n00 = gradP[X+perm[Y]].dot2(x, y);
83 | var n01 = gradP[X+perm[Y+1]].dot2(x, y-1);
84 | var n10 = gradP[X+1+perm[Y]].dot2(x-1, y);
85 | var n11 = gradP[X+1+perm[Y+1]].dot2(x-1, y-1);
86 |
87 | // Compute the fade curve value for x
88 | var u = fade(x);
89 |
90 | // Interpolate the four results
91 | return lerp(
92 | lerp(n00, n10, u),
93 | lerp(n01, n11, u),
94 | fade(y));
95 | };
96 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "wireframe-world",
3 | "version": "0.1.0",
4 | "description": "Demo that draws an infinite world",
5 | "main": "index.js",
6 | "directories": {
7 | "example": "example"
8 | },
9 | "dependencies": {},
10 | "devDependencies": {
11 | "budo": "^8.3.0",
12 | "canvas-fit": "^1.5.0",
13 | "canvas-orbit-camera": "^1.0.2",
14 | "gl": "^4.0.2",
15 | "gl-camera-pos-from-view-matrix": "^1.0.1",
16 | "gl-mat4": "^1.1.4",
17 | "regl": "^0.10.0",
18 | "regl-recorder": "^0.2.0",
19 | "standard": "^7.1.2"
20 | },
21 | "scripts": {
22 | "example": "node example/index.js",
23 | "test": "standard",
24 | "start": "budo --live --open wireframe_world.js"
25 | },
26 | "repository": {
27 | "type": "git",
28 | "url": "git+https://github.com/Erkaman/wireframe-world.git"
29 | },
30 | "keywords": [
31 | "movie",
32 | "recorder",
33 | "animation",
34 | "regl"
35 | ],
36 | "author": "Eric Arnebäck",
37 | "license": "MIT",
38 | "bugs": {
39 | "url": "https://github.com/Erkaman/wireframe-world/issues"
40 | },
41 | "homepage": "https://github.com/Erkaman/wireframe-world"
42 | }
43 |
--------------------------------------------------------------------------------
/wireframe_world.js:
--------------------------------------------------------------------------------
1 | const canvas = document.body.appendChild(document.createElement('canvas'))
2 | const fit = require('canvas-fit')
3 |
4 | var str = ``
5 |
6 | const regl = require('regl')({
7 | canvas: canvas,
8 | onDone: (err, regl) => {
9 | if (err) {
10 | document.body.innerHTML = `
11 | Failed to initialize the demo because:
12 |
` + err + '
' +
13 | `
14 | But you can find a recorded gif of the demo by clicking here.
15 | `
16 | throw err
17 | }
18 | }
19 | })
20 |
21 | var container = document.createElement('div')
22 | container.innerHTML = str
23 | document.body.appendChild(container)
24 |
25 | window.addEventListener('resize', fit(canvas), false)
26 |
27 | const mat4 = require('gl-mat4')
28 | const noise2 = require('./noise.js')
29 | var cameraPosFromViewMatrix = require('gl-camera-pos-from-view-matrix')
30 |
31 | // projection matrix settings.
32 | const Z_FAR = 120000
33 | const Z_NEAR = 0.01
34 | const FOV = Math.PI / 4
35 |
36 | // these variables are used all over the place. Declare them here,
37 | // once and for all.
38 | var x
39 | var z
40 | var y
41 | var r
42 | var i
43 |
44 | function makeWireframeTexture () {
45 | var texData = []
46 |
47 | //
48 | // make base image,
49 | //
50 | var lw = 10 // line width
51 | for (y = 0; y < 256; y++) {
52 | r = []
53 | for (x = 0; x < 256; x++) {
54 | if (y < lw || y > (256 - lw) || x < lw || x > (256 - lw)) {
55 | r.push([255, 255, 255, 255])
56 | } else {
57 | r.push([0, 0, 0, 255])
58 | }
59 | }
60 | texData.push(r)
61 | }
62 |
63 | //
64 | // do box filter blur on the base image:
65 | //
66 | var tempTexData = []
67 | for (y = 0; y < 256; y++) {
68 | r = []
69 | for (x = 0; x < 256; x++) {
70 | var c = [0, 0, 0, 0]
71 |
72 | for (var ax = -3; ax <= +3; ax++) {
73 | for (var ay = -3; ay <= +3; ay++) {
74 | var wy = y + ay
75 | var wx = x + ax
76 |
77 | if (wy < 0 || wx < 0 || wy > 255 || wx > 255) {
78 | // avoid out-of-range access.
79 | continue
80 | }
81 |
82 | var d = texData[wy][wx]
83 | c = [
84 | c[0] + d[0],
85 | c[1] + d[1],
86 | c[2] + d[2],
87 | c[3] + d[3]
88 | ]
89 | }
90 | }
91 |
92 | var u = 49.0
93 | r.push([c[0] / u, c[1] / u, c[2] / u, c[3] / u])
94 | }
95 | tempTexData.push(r)
96 | }
97 | texData = tempTexData
98 |
99 | return texData
100 | }
101 |
102 | // lerp between two colors
103 | function lerp (c0, c1, x) {
104 | return [
105 | c1[0] * x + c0[0] * (1.0 - x),
106 | c1[1] * x + c0[1] * (1.0 - x),
107 | c1[2] * x + c0[2] * (1.0 - x),
108 | c1[3] * x + c0[3] * (1.0 - x)
109 | ]
110 | }
111 |
112 | function makeSunTexture () {
113 | var texData = []
114 |
115 | // the color of the circle is based on this palette.
116 | // and the palette uses the distance from the center to
117 | // smoothly interpolate between colors.
118 | var palette = [
119 | [0.0, [246.0, 125.0, 202.0, 255.0]],
120 | [0.6, [247.0, 27.0, 111.0, 255.0]],
121 | [0.9, [247.0, 27.0, 111.0, 255.0]],
122 | [1.0, [0.0, 0.0, 0.0, 255.0]]
123 | ]
124 |
125 | for (y = 0; y < 256; y++) {
126 | r = [] // row of pixel data
127 | for (x = 0; x < 256; x++) {
128 | // convert (x,y) to range [-1, +1]
129 | var ox = (x - 128) / 127
130 | var oy = (y - 128) / 127
131 |
132 | var R = Math.sqrt(ox * ox + oy * oy) // distance from center.
133 | var c
134 |
135 | if (R >= 1.0) {
136 | c = [0.0, 0.0, 0.0, 0.0]
137 | } else {
138 | var ip
139 | // find the two colors in the palette, which we should
140 | // interpolate between.
141 | for (ip = 0; ip < palette.length - 1; ip++) {
142 | if (palette[ip][0] <= R && palette[ip + 1][0] >= R) {
143 | break
144 | }
145 | }
146 |
147 | var c0 = palette[ip + 0]
148 | var c1 = palette[ip + 1]
149 | c = lerp(c0[1], c1[1], (R - c0[0]) / (c1[0] - c0[0]))
150 | }
151 | r.push(c)
152 | }
153 | texData.push(r)
154 | }
155 |
156 | return texData
157 | }
158 |
159 | const elements = [] // faces
160 | var texCoord = [] // texCoords
161 |
162 | const H = 80 // number of squares on the height
163 | const W = 60 // number of squares on the width
164 |
165 | var size = 100.0 // the sidelength of a square.
166 | var xmin = -(W / 2.0) * size
167 | var xmax = +(W / 2.0) * size
168 | var zmin = -(H / 2.0) * size
169 | var zmax = +(H / 2.0) * size
170 |
171 | var row
172 | var col
173 |
174 | function Chunk () {
175 | this.position = []
176 | this.positionBuffer = regl.buffer({
177 | length: (H + 1) * (W + 1) * 3 * 4,
178 | type: 'float',
179 | usage: 'dynamic'
180 | })
181 | }
182 | var chunkPool = []
183 | function freeChunk (chunk) {
184 | chunkPool.push(chunk)
185 | }
186 |
187 | // every time we add a new chunk, we increment this number.
188 | // it is used to determine the z-position of the chunk.
189 | var N = 0
190 |
191 | function makeChunk () {
192 | // retrieve chunk from the pool, or create one if necessary.
193 | var chunk = chunkPool.pop() || new Chunk()
194 |
195 | var j = 0
196 | for (row = 0; row <= H; ++row) {
197 | z = (row / H) * (zmax - zmin) + zmin
198 | // If N==0, then this is the first chunk that we see.
199 | // If N==1, it is the second chunk that we see, and so on.
200 | z += (zmax - zmin) * -N
201 |
202 | for (col = 0; col <= W; ++col) {
203 | x = (col / W) * (xmax - xmin) + xmin
204 |
205 | var f = 0.0015974
206 | var amp = 100.0
207 | var n = 0
208 |
209 | // FBM of two octaves.
210 | for (var i = 0; i < 2; i++) {
211 | n += amp * noise2(x * f, z * f)
212 |
213 | amp *= 6.0
214 | f *= 0.5
215 | }
216 |
217 | // make the terrain less smooth looking.
218 | y = Math.round(n / 60) * 60
219 |
220 | chunk.position[j++] = [x, y, z]
221 | }
222 | }
223 | // upload vertex data to the GPU.
224 | chunk.positionBuffer.subdata(chunk.position)
225 |
226 | chunk.N = N
227 |
228 | N++
229 | return chunk
230 | }
231 |
232 | // render distance of chunks.
233 | var RENDER_N = 10
234 | var chunks = []
235 |
236 | // create all the chunks we need.
237 | for (i = 0; i < RENDER_N; i++) {
238 | chunks[i] = makeChunk()
239 | }
240 |
241 | // create texCoords.
242 | for (row = 0; row <= H; ++row) {
243 | z = (row)
244 | for (col = 0; col <= W; ++col) {
245 | x = (col)
246 | texCoord.push([x, z])
247 | }
248 | }
249 |
250 | // create faces.
251 | for (row = 0; row <= (H - 1); ++row) {
252 | for (col = 0; col <= (W - 1); ++col) {
253 | i = row * (W + 1) + col
254 |
255 | var i0 = i + 0
256 | var i1 = i + 1
257 | var i2 = i + (W + 1) + 0
258 | var i3 = i + (W + 1) + 1
259 |
260 | elements.push([i3, i1, i0])
261 | elements.push([i0, i2, i3])
262 | }
263 | }
264 |
265 | // this global scope encapsulates all state common to all drawCommands.
266 | const globalScope = regl({
267 | uniforms: {
268 | projection: ({viewportWidth, viewportHeight}) => {
269 | return mat4.perspective([], FOV, viewportWidth / viewportHeight, Z_NEAR, Z_FAR)
270 | }
271 | },
272 | cull: {
273 | enable: true
274 | }
275 | })
276 |
277 | // encapsulates state needed for drawing chunks.
278 | const chunkScope = regl({
279 | uniforms: {
280 | view: (_, props) => props.view,
281 |
282 | tex: regl.texture({
283 | min: 'linear mipmap linear',
284 | mag: 'linear',
285 | wrap: 'repeat',
286 | data: makeWireframeTexture()
287 | }),
288 | cameraPos: (_, props) => {
289 | return cameraPosFromViewMatrix([], props.view)
290 | },
291 | tick: ({tick}) => tick
292 | },
293 |
294 | frag: `
295 | precision mediump float;
296 |
297 | varying vec2 vTexCoord;
298 | varying vec3 vPosition;
299 |
300 | uniform sampler2D tex;
301 | uniform vec3 cameraPos;
302 | uniform float tick;
303 |
304 | void main () {
305 | vec3 d = vec3(
306 | (sin(tick*0.02 + 0.0) + 1.0) * 0.5 + 0.5,
307 | (sin(tick*0.02 + 2.0) + 1.0) * 0.5 + 0.5,
308 | (sin(tick*0.01 + 4.0) + 1.0) * 0.5 + 0.5
309 | );
310 |
311 | vec3 c = texture2D(tex, vTexCoord).x * d;
312 | gl_FragColor = vec4(c.xyz, 1.0);
313 | }`,
314 | vert: `
315 | precision mediump float;
316 |
317 | attribute vec3 position;
318 | attribute vec2 texCoord;
319 |
320 | varying vec2 vTexCoord;
321 | varying vec3 vPosition;
322 |
323 | uniform mat4 projection, view;
324 | uniform vec3 cameraPos;
325 |
326 | void main() {
327 | vTexCoord = texCoord;
328 | vPosition = position.xyz;
329 |
330 | float dist = distance(cameraPos.xz, vPosition.xz);
331 | float curveAmount = 0.3;
332 |
333 | // we lower all vertices down a bit, to create a slightly curved horizon.
334 | gl_Position = projection * view * vec4(position - vec3(0.0, dist*curveAmount * 0.0, 0.0), 1);
335 | }`,
336 |
337 | attributes: {
338 | texCoord: texCoord
339 | },
340 | elements: elements
341 | })
342 |
343 | const drawSun = regl({
344 | uniforms: {
345 | view: (_, props) => {
346 | var m = mat4.copy([], props.view)
347 | // the sun should always stay where it is, so do this:
348 | m[12] = 0
349 | m[13] = 0
350 | m[14] = 0
351 | return m
352 | },
353 | tex: regl.texture({
354 | data: makeSunTexture(),
355 | mag: 'linear'
356 | })
357 | },
358 |
359 | frag: `
360 | precision mediump float;
361 |
362 | varying vec2 vTexCoord;
363 |
364 | uniform sampler2D tex;
365 |
366 | void main () {
367 | gl_FragColor = vec4(texture2D(tex, vTexCoord).xyz, 1.0);
368 | }`,
369 | vert: `
370 | precision mediump float;
371 |
372 | attribute vec3 position;
373 | attribute vec2 texCoord;
374 |
375 | uniform mat4 projection, view;
376 | uniform vec3 cameraPos;
377 |
378 | varying vec2 vTexCoord;
379 |
380 | void main() {
381 | vec3 q = position;
382 | // scale and translate the sun:
383 | q += vec3(0.0, 0.1, 0.0);
384 | q *= vec3(vec2(0.4), -1.0);
385 | vec4 p = view * vec4(q, 1);
386 |
387 | vTexCoord = texCoord;
388 | gl_Position = projection * p;
389 | }`,
390 |
391 | attributes: {
392 | position: [
393 | [-0.5, -0.5, 1.0],
394 | [+0.5, -0.5, 1.0],
395 | [+0.5, +0.5, 1.0],
396 |
397 | [+0.5, +0.5, 1.0],
398 | [-0.5, +0.5, 1.0],
399 | [-0.5, -0.5, 1.0]
400 | ],
401 | texCoord: [
402 | [0.0, 0.0],
403 | [1.0, 0.0],
404 | [1.0, 1.0],
405 |
406 | [1.0, 1.0],
407 | [0.0, 1.0],
408 | [0.0, 0.0]
409 | ]
410 | },
411 |
412 | count: 6,
413 | depth: {
414 | enable: false // the sun will be behind everything else.
415 | }
416 | })
417 |
418 | // used for drawing a single chunk.
419 | const drawChunk = regl({
420 | attributes: {
421 | position: regl.prop('pos')
422 | }
423 | })
424 |
425 | // make sure that we actually upload all the vertex-data before starting.
426 | regl._gl.flush()
427 | regl._gl.finish()
428 |
429 | regl.frame(({tick, viewportWidth, viewportHeight}) => {
430 | regl.clear({color: [0.0, 0.0, 0.0, 1.0], depth: 1})
431 |
432 | // create a moving camera.
433 | var view = []
434 | var speed = 40.0
435 | var startZ = 5100
436 | var down = -1000
437 | var cameraPos = [0, 410, startZ - tick * speed]
438 | mat4.lookAt(view, cameraPos, [0, down, -startZ - tick * speed], [0, 1, 0])
439 |
440 | globalScope(() => {
441 | drawSun({view: view})
442 |
443 | chunkScope({view: view}, () => {
444 | for (i = 0; i < chunks.length; i++) {
445 | drawChunk({pos: {buffer: chunks[i].positionBuffer}})
446 | }
447 | })
448 | })
449 |
450 | // If the first chunk can't be seen anymore, remove it.
451 | // Then way back in the horizon we place a new chunk,
452 | // so that the world goes on forever.
453 | if (chunks.length > 0) {
454 | z = zmin + (zmax - zmin) * -chunks[0].N
455 | if (cameraPos[2] < z) {
456 | freeChunk(chunks.shift())
457 | chunks.push(makeChunk())
458 | }
459 | }
460 | })
461 |
--------------------------------------------------------------------------------
/www/demo.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |