Created
April 4, 2013 17:59
-
-
Save nocksock/5312590 to your computer and use it in GitHub Desktop.
A CodePen by Nils Riedemann. RayCasting Part2 - Using the basis of my previous RayCasting implementation to create the classic 3D simulation.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<h1>Use the Cursor Keys!</h1> | |
<p>… after you clicked on the canvas</p> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
(function(doc) { | |
var canvas = document.createElement('canvas') | |
, mapData = [ | |
[1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1], | |
[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1], | |
[1,0,2,2,2,0,0,0,0,0,0,0,0,0,0,0,1,1,1,0,1], | |
[1,0,2,2,2,0,0,0,0,0,0,0,0,0,0,0,1,1,1,0,1], | |
[1,0,2,0,2,0,0,0,0,1,0,1,0,0,0,0,1,0,1,0,1], | |
[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1], | |
[1,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,1], | |
[1,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,1], | |
[1,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,1], | |
[1,0,0,0,1,0,1,1,0,1,1,1,0,1,1,0,1,0,0,0,1], | |
[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1], | |
[1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1] | |
] | |
, context = canvas.getContext("2d") | |
, screenStrips = [] | |
, screen = doc.createElement("canvas") | |
, screenCtx = screen.getContext("2d") | |
, player | |
, map | |
, options = { | |
scale: 16, | |
screenWidth : 320, | |
screenHeight : 200, | |
stripWidth : 3, | |
rayCount : 120, | |
} | |
, colors = [ "#aaa", "#red" ] | |
, fov = 70 * Math.PI / 180 | |
, numRays = Math.ceil(options.screenWidth / options.stripWidth) | |
, viewDistance = (options.screenWidth/2) / Math.tan((fov / 2)) | |
; | |
// Initialize //{{{ | |
window.onload = function init() { | |
map = new Map(mapData); | |
player = new Player(); | |
canvas.width = map.width * options.scale; | |
canvas.height = map.height * options.scale; | |
// setup screen | |
screen.id = "screen"; | |
screen.style.width = options.screenWidth + "px"; | |
screen.style.height = options.screenHeight + "px"; | |
// doc.body.appendChild(canvas); | |
doc.body.appendChild(screen); | |
player.turnDirection = 1; | |
setTimeout(function() { | |
player.turnDirection = 0; | |
}, 1500); | |
window.requestAnimFrame(mainLoop); | |
};//}}} | |
function mainLoop(){//{{{ | |
// context.clearRect(0,0,canvas.width, canvas.height); | |
context.fillStyle="black"; | |
context.clearRect(0,0, canvas.width, canvas.height); | |
screenCtx.clearRect(0,0, canvas.width, canvas.height); | |
player.update(); | |
map.draw(); | |
player.draw(); | |
raycaster.castAll(); | |
window.requestAnimFrame(mainLoop); | |
};//}}} | |
// Keybindings{{{ | |
doc.onkeydown = function(e) { | |
e = e || window.event; | |
switch (e.keyCode) { // which key was pressed? | |
case 38: player.speed = 1; break; // up | |
case 40: player.speed = -1; break; // down | |
case 37: player.turnDirection = -1; break; // left | |
case 39: player.turnDirection = 1; break; // right | |
} | |
} | |
doc.onkeyup = function(e) { | |
e = e || window.event; | |
switch (e.keyCode) { | |
case 38: // fall through | |
case 40: player.speed = 0; break; | |
case 37: // fall through | |
case 39: player.turnDirection = 0; break; | |
} | |
} | |
// }}} | |
// Player {{{ | |
function Player() { | |
this.position = [11.4,1.4]; | |
this.turnDirection = 0; | |
this.rotation = 0.73; | |
this.speed = 0; | |
this.moveSpeed = 0.05; | |
this.rotationSpeed = 2 * Math.TAU / 180; | |
} | |
Player.prototype = { | |
update: function () { | |
var step = this.speed * this.moveSpeed | |
, x, y | |
; | |
this.rotation += this.turnDirection * this.rotationSpeed; | |
this.rotation = normalizeAngle(this.rotation); | |
x = this.position[0] + (Math.cos(this.rotation) * step); | |
y = this.position[1] + (Math.sin(this.rotation) * step); | |
if(!map.isPassableAt(x,y)){ | |
// not passable | |
return; | |
} | |
this.position[0] = x; | |
this.position[1] = y; | |
}, | |
draw: function drawPlayer() { | |
context.fillStyle = "red"; | |
context.beginPath(); | |
context.arc( | |
this.position[0]*options.scale, this.position[1]*options.scale, | |
2, 0, Math.TAU | |
); | |
context.fill(); | |
} | |
};//}}} | |
function renderStrip(stripID, distance, angle){ | |
var height = Math.round(viewDistance/distance) | |
, topOffset = ( ( options.screenHeight - height )/2 ) | |
, leftOffset = stripID*options.stripWidth | |
, alpha = ( 0.5/distance )*6 | |
; | |
screenCtx.fillStyle = "hsla(198, 50%, 50%,"+alpha+")"; | |
screenCtx.fillRect( | |
fround( leftOffset ), | |
fround( topOffset ), | |
fround( options.stripWidth ), | |
fround( height ) | |
); | |
}; | |
// Map {{{ | |
function Map(map){ | |
this.map = map; | |
this.height = map.length; | |
this.width = map[0].length; | |
} | |
Map.prototype = { | |
isPassableAt: function isPassableAt(x,y) { | |
return this.isInScope(x,y) && this.hasSpaceAt(x,y); | |
}, | |
hasSpaceAt: function hasSpaceAt(x,y) { | |
return this.map[Math.floor(y)][Math.floor(x)] == 0; | |
}, | |
isInScope: function(x,y) { | |
return !( x < 0 || y < 0 || y > this.height || x > this.width ); | |
}, | |
draw: function() { | |
context.fillStyle = "hsla(0, 0%, 5%, 1)"; | |
for (var y = 0; y < this.height; y++) { | |
for (var x = 0; x < this.width; x++) { | |
if(this.map[y][x] != 0) { | |
context.fillRect( | |
x*options.scale, | |
y*options.scale, | |
options.scale, | |
options.scale | |
); | |
} | |
}; | |
}; | |
} | |
};//}}} | |
// raycaster {{{ | |
raycaster = { | |
castAll: function castAll() { | |
var strip = 0; | |
for (var i = 0; i < options.rayCount; i++) { | |
var rayPosition = (-options.rayCount/2 + i)*options.stripWidth | |
, rayViewDist = pythagoras(rayPosition, viewDistance) | |
, rayAngle = Math.asin(rayPosition / rayViewDist) | |
; | |
this.cast(player.rotation + rayAngle, i); | |
}; | |
}, | |
cast: function(_angle, stripID) {//{{{ | |
var angle = normalizeAngle(_angle) | |
, right = (angle > Math.TAU * 0.75 || angle < Math.TAU * 0.25) | |
, up = (angle < 0 || angle > Math.PI) | |
, angleSin = Math.sin(angle) | |
, angleCos = Math.cos(angle) | |
, distanceVertical = 0 | |
, distanceHorizontal = 0 | |
, distance | |
, hit = [0,0] | |
, texture = [0,0] | |
, wall = [0,0] | |
; | |
// check vertical walls {{{ | |
var slope = angleSin / angleCos | |
, _x = right ? 1 : -1 | |
, _y = _x * slope | |
, x = right ? Math.ceil(player.position[0]) : Math.floor(player.position[0]) | |
, y = player.position[1] + (x - player.position[0]) * slope | |
; | |
while(x >= 0 && map.width && y > 0 && y < map.height) { | |
if(!map.hasSpaceAt( x + (right ? 0 : -1), y)) { | |
distance = distanceVertical = pythagorasSquared( | |
x - player.position[0], | |
y - player.position[1] | |
); | |
hit = [x ,y]; | |
break; | |
} | |
x += _x; | |
y += _y; | |
}//}}} | |
// check horizontal walls {{{ | |
slope = angleCos / angleSin; | |
_y = up ? -1 : 1; | |
_x = _y * slope; | |
y = up ? Math.floor(player.position[1]) : Math.ceil(player.position[1]); | |
x = player.position[0] + (y - player.position[1]) * slope; | |
while(x >= 0 && x < map.width && y >= 0 && y < map.height) { | |
if(!map.hasSpaceAt( x, y + (up ? -1 : 0) )) { | |
distanceHorizontal = pythagorasSquared( | |
x - player.position[0], | |
y - player.position[1] | |
); | |
if(!distanceVertical || distanceHorizontal < distanceVertical) { | |
distance = distanceHorizontal; | |
hit = [x, y]; | |
} | |
break; | |
} | |
x += _x; | |
y += _y; | |
} | |
//}}} | |
if(distance){ | |
renderStrip(stripID,perpendicularDistance( | |
Math.sqrt(distance), player.rotation - angle | |
), texture[0]); | |
this.draw(hit); | |
} | |
},//}}} | |
draw: function(ray) { | |
context.strokeStyle = "yellow"; | |
context.lineWidth = 0.5; | |
context.beginPath(); | |
context.moveTo(player.position[0]*options.scale, player.position[1]*options.scale); | |
context.lineTo( | |
ray[0] * options.scale, | |
ray[1] * options.scale | |
); | |
context.closePath(); | |
context.stroke(); | |
} | |
}//}}} | |
// convenience methods {{{ | |
function fround (v) { return (0.5+v) | 0; }; | |
function normalizeAngle(angle) { | |
angle %= Math.TAU; | |
if(angle<0) angle+=Math.TAU; | |
return angle; | |
} | |
function perpendicularDistance(distance, angle) { | |
return distance * Math.cos(angle); | |
} | |
function pythagorasSquared(a, b){ | |
return ( a*a ) + (b * b); | |
} | |
function pythagoras(a,b){ | |
return Math.sqrt(pythagorasSquared(a,b)); | |
} | |
Math.TAU = Math.PI * 2; | |
window.requestAnimFrame=function(){return window.requestAnimationFrame||window.webkitRequestAnimationFrame||window.mozRequestAnimationFrame||window.oRequestAnimationFrame||window.msRequestAnimationFrame||function(a){window.setTimeout(a,1E3/60)}}();//}}} | |
}(document)) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@import "compass"; | |
body, html { | |
background: black; | |
text-align:center; | |
color: white; | |
font-family: sans-serif; | |
} | |
h1 { | |
margin-bottom: 0; | |
} | |
p { | |
color: #444; | |
font-size: 10pt; | |
margin-top:0; | |
margin-bottom: 10px; | |
} | |
canvas { | |
margin: 10px auto; | |
display:block; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment