Permalink
Cannot retrieve contributors at this time
Join GitHub today
GitHub is home to over 28 million developers working together to host and review code, manage projects, and build software together.
Sign up
Fetching contributors…
| <!DOCTYPE html> | |
| <html> | |
| <head> | |
| <title>Voxel Space project demonstration</title> | |
| <meta charset="UTF-8"> | |
| <meta name="description" content="Demonstration of the Voxel Space technique"> | |
| <meta name="author" content="Sebastian Macke"> | |
| <meta name="keywords" content="Voxel, VoxelSpace, Voxel Space, Comanche, landscape, rendering"> | |
| <style> | |
| html, body {margin: 0; height: 100%; overflow: hidden} | |
| canvas { width: 100%; height: 100%; } | |
| a { color: white; } | |
| #info { | |
| position: absolute; | |
| top: 0px; | |
| width: 100%; | |
| padding: 5px; | |
| z-index:100; | |
| color: white; | |
| font-family: "Arial", Times, serif; | |
| font-size: 120%; | |
| } | |
| #fps { | |
| float: right; | |
| position: absolute; | |
| top: 0px; | |
| right: 10px; | |
| z-index:100; | |
| padding: 5px; | |
| color: white; | |
| font-family: "Arial", Times, serif; | |
| font-size: 120%; | |
| } | |
| </style> | |
| </head> | |
| <body scroll="no"> | |
| <div id="fps"> | |
| </div> | |
| <div id="info"> | |
| Fly controls | |
| <b>WASD</b> or <b>Cursor Keys</b> or <b>left click</b> move, <b>R|F</b> up | down, <b>Q|E</b> pitch, | |
| <br> | |
| <select name="Mapselector" size="1" onchange="LoadMap(this.value);" value="C1W;D1"> | |
| <option value="C1W;D1">Map C1W</option> | |
| <option value="C2W;D2">Map C2W</option> | |
| <option value="C3;D3">Map C3</option> | |
| <option value="C4;D4">Map C4</option> | |
| <option value="C5W;D5">Map C5W</option> | |
| <option value="C6W;D6">Map C6W</option> | |
| <option value="C7W;D7">Map C7W</option> | |
| <option value="C8;D6">Map C8</option> | |
| <option value="C9W;D9">Map C9W</option> | |
| <option value="C10W;D10">Map C10W</option> | |
| <option value="C11W;D11">Map C11W</option> | |
| <option value="C12W;D11">Map C12W</option> | |
| <option value="C13;D13">Map C13</option> | |
| <option value="C14;D14">Map C14</option> | |
| <option value="C14W;D14">Map C14W</option> | |
| <option value="C15;D15">Map C15</option> | |
| <option value="C16W;D16">Map C16W</option> | |
| <option value="C17W;D17">Map C17W</option> | |
| <option value="C18W;D18">Map C18W</option> | |
| <option value="C19W;D19">Map C19W</option> | |
| <option value="C20W;D20">Map C20W</option> | |
| <option value="C21;D21">Map C21</option> | |
| <option value="C22W;D22">Map C22W</option> | |
| <option value="C23W;D21">Map C23W</option> | |
| <option value="C24W;D24">Map C24W</option> | |
| <option value="C25W;D25">Map C25W</option> | |
| <option value="C26W;D18">Map C26W</option> | |
| <option value="C27W;D15">Map C27W</option> | |
| <option value="C28W;D25">Map C28W</option> | |
| <option value="C29W;D16">Map C29W</option> | |
| </select> | |
| <label for="distancerange">Distance</label> | |
| <input id="distancerange" type="range" min="100" max="2000" step="1" onchange="camera.distance = this.value"> | |
| <a href="https://github.com/s-macke/VoxelSpace">Github project page</a> | |
| </div> | |
| <canvas id="fullscreenCanvas" width="800" height="400"> | |
| Your browser does not support the canvas element. | |
| </canvas> | |
| <script> | |
| "use strict"; | |
| // --------------------------------------------- | |
| // Viewer information | |
| var camera = | |
| { | |
| x: 512., // x position on the map | |
| y: 800., // y position on the map | |
| height: 78., // height of the camera | |
| angle: 0., // direction of the camera | |
| horizon: 100., // horizon position (look up and down) | |
| distance: 800 // distance of map | |
| }; | |
| // --------------------------------------------- | |
| // Landscape data | |
| var map = | |
| { | |
| width: 1024, | |
| height: 1024, | |
| shift: 10, // power of two: 2^10 = 1024 | |
| altitude: new Uint8Array(1024*1024), // 1024 * 1024 byte array with height information | |
| color: new Uint32Array(1024*1024) // 1024 * 1024 int array with RGB colors | |
| }; | |
| // --------------------------------------------- | |
| // Screen data | |
| var screendata = | |
| { | |
| canvas: null, | |
| context: null, | |
| imagedata: null, | |
| bufarray: null, // color data | |
| buf8: null, // the same array but with bytes | |
| buf32: null, // the same array but with 32-Bit words | |
| backgroundcolor: 0xFFE09090 | |
| }; | |
| // --------------------------------------------- | |
| // Keyboard and mouse interaction | |
| var input = | |
| { | |
| forwardbackward: 0, | |
| leftright: 0, | |
| updown: 0, | |
| lookup: false, | |
| lookdown: false, | |
| mouseposition: null, | |
| keypressed: false | |
| } | |
| var updaterunning = false; | |
| var time = new Date().getTime(); | |
| // for fps display | |
| var timelastframe = new Date().getTime(); | |
| var frames = 0; | |
| // Update the camera for next frame. Dependent on keypresses | |
| function UpdateCamera() | |
| { | |
| var current = new Date().getTime(); | |
| input.keypressed = false; | |
| if (input.leftright != 0) | |
| { | |
| camera.angle += input.leftright*0.1*(current-time)*0.03; | |
| input.keypressed = true; | |
| } | |
| if (input.forwardbackward != 0) | |
| { | |
| camera.x -= input.forwardbackward * Math.sin(camera.angle) * (current-time)*0.03; | |
| camera.y -= input.forwardbackward * Math.cos(camera.angle) * (current-time)*0.03; | |
| input.keypressed = true; | |
| } | |
| if (input.updown != 0) | |
| { | |
| camera.height += input.updown * (current-time)*0.03; | |
| input.keypressed = true; | |
| } | |
| if (input.lookup) | |
| { | |
| camera.horizon += 2 * (current-time)*0.03; | |
| input.keypressed = true; | |
| } | |
| if (input.lookdown) | |
| { | |
| camera.horizon -= 2 * (current-time)*0.03; | |
| input.keypressed = true; | |
| } | |
| // Collision detection. Don't fly below the surface. | |
| var mapoffset = ((Math.floor(camera.y) & (map.width-1)) << map.shift) + (Math.floor(camera.x) & (map.height-1))|0; | |
| if ((map.altitude[mapoffset]+10) > camera.height) camera.height = map.altitude[mapoffset] + 10; | |
| time = current; | |
| } | |
| // --------------------------------------------- | |
| // Keyboard and mouse event handlers | |
| // --------------------------------------------- | |
| // Keyboard and mouse event handlers | |
| function GetMousePosition(e) | |
| { | |
| // fix for Chrome | |
| if (e.type.startsWith('touch')) | |
| { | |
| return [e.targetTouches[0].pageX, e.targetTouches[0].pageY]; | |
| } else | |
| { | |
| return [e.pageX, e.pageY]; | |
| } | |
| } | |
| function DetectMouseDown(e) | |
| { | |
| input.forwardbackward = 3.; | |
| input.mouseposition = GetMousePosition(e); | |
| time = new Date().getTime(); | |
| if (!updaterunning) Draw(); | |
| return; | |
| } | |
| function DetectMouseUp() | |
| { | |
| input.mouseposition = null; | |
| input.forwardbackward = 0; | |
| input.leftright = 0; | |
| input.updown = 0; | |
| return; | |
| } | |
| function DetectMouseMove(e) | |
| { | |
| e.preventDefault(); | |
| if (input.mouseposition == null) return; | |
| if (input.forwardbackward == 0) return; | |
| var currentMousePosition = GetMousePosition(e); | |
| input.leftright = (input.mouseposition[0] - currentMousePosition[0]) / window.innerWidth * 2; | |
| camera.horizon = 100 + (input.mouseposition[1] - currentMousePosition[1]) / window.innerHeight * 500; | |
| input.updown = (input.mouseposition[1] - currentMousePosition[1]) / window.innerHeight * 10; | |
| } | |
| function DetectKeysDown(e) | |
| { | |
| switch(e.keyCode) | |
| { | |
| case 37: // left cursor | |
| case 65: // a | |
| input.leftright = +1.; | |
| break; | |
| case 39: // right cursor | |
| case 68: // d | |
| input.leftright = -1.; | |
| break; | |
| case 38: // cursor up | |
| case 87: // w | |
| input.forwardbackward = 3.; | |
| break; | |
| case 40: // cursor down | |
| case 83: // s | |
| input.forwardbackward = -3.; | |
| break; | |
| case 82: // r | |
| input.updown = +2.; | |
| break; | |
| case 70: // f | |
| input.updown = -2.; | |
| break; | |
| case 69: // e | |
| input.lookup = true; | |
| break; | |
| case 81: //q | |
| input.lookdown = true; | |
| break; | |
| default: | |
| return; | |
| break; | |
| } | |
| if (!updaterunning) { | |
| time = new Date().getTime(); | |
| Draw(); | |
| } | |
| return false; | |
| } | |
| function DetectKeysUp(e) | |
| { | |
| switch(e.keyCode) | |
| { | |
| case 37: // left cursor | |
| case 65: // a | |
| input.leftright = 0; | |
| break; | |
| case 39: // right cursor | |
| case 68: // d | |
| input.leftright = 0; | |
| break; | |
| case 38: // cursor up | |
| case 87: // w | |
| input.forwardbackward = 0; | |
| break; | |
| case 40: // cursor down | |
| case 83: // s | |
| input.forwardbackward = 0; | |
| break; | |
| case 82: // r | |
| input.updown = 0; | |
| break; | |
| case 70: // f | |
| input.updown = 0; | |
| break; | |
| case 69: // e | |
| input.lookup = false; | |
| break; | |
| case 81: //q | |
| input.lookdown = false; | |
| break; | |
| default: | |
| return; | |
| break; | |
| } | |
| return false; | |
| } | |
| // --------------------------------------------- | |
| // Fast way to draw vertical lines | |
| function DrawVerticalLine(x, ytop, ybottom, col) | |
| { | |
| x = x|0; | |
| ytop = ytop|0; | |
| ybottom = ybottom|0; | |
| col = col|0; | |
| var buf32 = screendata.buf32; | |
| var screenwidth = screendata.canvas.width|0; | |
| if (ytop < 0) ytop = 0; | |
| if (ytop > ybottom) return; | |
| // get offset on screen for the vertical line | |
| var offset = ((ytop * screenwidth) + x)|0; | |
| for (var k = ytop|0; k < ybottom|0; k=k+1|0) | |
| { | |
| buf32[offset|0] = col|0; | |
| offset = offset + screenwidth|0; | |
| } | |
| } | |
| // --------------------------------------------- | |
| // Basic screen handling | |
| function DrawBackground() | |
| { | |
| var buf32 = screendata.buf32; | |
| var color = screendata.backgroundcolor|0; | |
| for (var i = 0; i < buf32.length; i++) buf32[i] = color|0; | |
| } | |
| // Show the back buffer on screen | |
| function Flip() | |
| { | |
| screendata.imagedata.data.set(screendata.buf8); | |
| screendata.context.putImageData(screendata.imagedata, 0, 0); | |
| } | |
| // --------------------------------------------- | |
| // The main render routine | |
| function Render() | |
| { | |
| var mapwidthperiod = map.width - 1; | |
| var mapheightperiod = map.height - 1; | |
| var screenwidth = screendata.canvas.width|0; | |
| var sinang = Math.sin(camera.angle); | |
| var cosang = Math.cos(camera.angle); | |
| var hiddeny = new Int32Array(screenwidth); | |
| for(var i=0; i<screendata.canvas.width|0; i=i+1|0) | |
| hiddeny[i] = screendata.canvas.height; | |
| var deltaz = 1.; | |
| // Draw from front to back | |
| for(var z=1; z<camera.distance; z+=deltaz) | |
| { | |
| // 90 degree field of view | |
| var plx = -cosang * z - sinang * z; | |
| var ply = sinang * z - cosang * z; | |
| var prx = cosang * z - sinang * z; | |
| var pry = -sinang * z - cosang * z; | |
| var dx = (prx - plx) / screenwidth; | |
| var dy = (pry - ply) / screenwidth; | |
| plx += camera.x; | |
| ply += camera.y; | |
| var invz = 1. / z * 240.; | |
| for(var i=0; i<screenwidth|0; i=i+1|0) | |
| { | |
| var mapoffset = ((Math.floor(ply) & mapwidthperiod) << map.shift) + (Math.floor(plx) & mapheightperiod)|0; | |
| var heightonscreen = (camera.height - map.altitude[mapoffset]) * invz + camera.horizon|0; | |
| DrawVerticalLine(i, heightonscreen|0, hiddeny[i], map.color[mapoffset]); | |
| if (heightonscreen < hiddeny[i]) hiddeny[i] = heightonscreen; | |
| plx += dx; | |
| ply += dy; | |
| } | |
| deltaz += 0.005; | |
| } | |
| } | |
| // --------------------------------------------- | |
| // Draw the next frame | |
| function Draw() | |
| { | |
| updaterunning = true; | |
| UpdateCamera(); | |
| DrawBackground(); | |
| Render(); | |
| Flip(); | |
| frames++; | |
| if (!input.keypressed) | |
| { | |
| updaterunning = false; | |
| } else | |
| { | |
| window.setTimeout(Draw, 0); | |
| } | |
| } | |
| // --------------------------------------------- | |
| // Init routines | |
| // Util class for downloading the png | |
| function DownloadImagesAsync(urls) { | |
| return new Promise(function(resolve, reject) { | |
| var pending = urls.length; | |
| var result = []; | |
| if (pending === 0) { | |
| resolve([]); | |
| return; | |
| } | |
| urls.forEach(function(url, i) { | |
| var image = new Image(); | |
| //image.addEventListener("load", function() { | |
| image.onload = function() { | |
| var tempcanvas = document.createElement("canvas"); | |
| var tempcontext = tempcanvas.getContext("2d"); | |
| tempcanvas.width = map.width; | |
| tempcanvas.height = map.height; | |
| tempcontext.drawImage(image, 0, 0, map.width, map.height); | |
| result[i] = tempcontext.getImageData(0, 0, map.width, map.height).data; | |
| pending--; | |
| if (pending === 0) { | |
| resolve(result); | |
| } | |
| }; | |
| image.src = url; | |
| }); | |
| }); | |
| } | |
| function LoadMap(filenames) | |
| { | |
| var files = filenames.split(";"); | |
| DownloadImagesAsync(["maps/"+files[0]+".png", "maps/"+files[1]+".png"]).then(OnLoadedImages); | |
| } | |
| function OnLoadedImages(result) | |
| { | |
| var datac = result[0]; | |
| var datah = result[1]; | |
| for(var i=0; i<map.width*map.height; i++) | |
| { | |
| map.color[i] = 0xFF000000 | (datac[(i<<2) + 2] << 16) | (datac[(i<<2) + 1] << 8) | datac[(i<<2) + 0]; | |
| map.altitude[i] = datah[i<<2]; | |
| } | |
| Draw(); | |
| } | |
| function OnResizeWindow() | |
| { | |
| screendata.canvas = document.getElementById('fullscreenCanvas'); | |
| var aspect = window.innerWidth / window.innerHeight; | |
| screendata.canvas.width = window.innerWidth<800?window.innerWidth:800; | |
| screendata.canvas.height = screendata.canvas.width / aspect; | |
| if (screendata.canvas.getContext) | |
| { | |
| screendata.context = screendata.canvas.getContext('2d'); | |
| screendata.imagedata = screendata.context.createImageData(screendata.canvas.width, screendata.canvas.height); | |
| } | |
| screendata.bufarray = new ArrayBuffer(screendata.imagedata.width * screendata.imagedata.height * 4); | |
| screendata.buf8 = new Uint8Array(screendata.bufarray); | |
| screendata.buf32 = new Uint32Array(screendata.bufarray); | |
| Draw(); | |
| } | |
| function Init() | |
| { | |
| for(var i=0; i<map.width*map.height; i++) | |
| { | |
| map.color[i] = 0xFF007050; | |
| map.altitude[i] = 0; | |
| } | |
| LoadMap("C1W;D1"); | |
| OnResizeWindow(); | |
| // set event handlers for keyboard, mouse, touchscreen and window resize | |
| var canvas = document.getElementById("fullscreenCanvas"); | |
| canvas.onkeydown = DetectKeysDown; | |
| canvas.onkeyup = DetectKeysUp; | |
| canvas.onmousedown = DetectMouseDown; | |
| canvas.onmouseup = DetectMouseUp; | |
| canvas.onmousemove = DetectMouseMove; | |
| canvas.ontouchstart = DetectMouseDown; | |
| canvas.ontouchend = DetectMouseUp; | |
| canvas.ontouchmove = DetectMouseMove; | |
| window.onresize = OnResizeWindow; | |
| window.setInterval(function(){ | |
| var current = new Date().getTime(); | |
| document.getElementById('fps').innerText = (frames / (current-timelastframe) * 1000).toFixed(1) + " fps"; | |
| frames = 0; | |
| timelastframe = current; | |
| }, 2000); | |
| } | |
| Init(); | |
| </script> | |
| </body> |