├── img ├── sky.jpg ├── wood.png ├── barrel.png ├── floor.png ├── pillar.png ├── painting.png ├── scaletest.png ├── barrel_mask.png ├── bricks-gray.png ├── pillar_mask.png └── bricks-brown.png ├── css └── main.css ├── js ├── raycaster.constants.js ├── raycaster.drawing.js ├── raycaster.js ├── raycaster.objects.js ├── raycaster.classes.js ├── raycaster.objects.level.js ├── raycaster.movement.js ├── raycaster.renderengine.js └── raycaster.raycasting.js ├── readme.txt └── default.html /img/sky.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DotTech/HTML5-Raycaster/HEAD/img/sky.jpg -------------------------------------------------------------------------------- /img/wood.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DotTech/HTML5-Raycaster/HEAD/img/wood.png -------------------------------------------------------------------------------- /img/barrel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DotTech/HTML5-Raycaster/HEAD/img/barrel.png -------------------------------------------------------------------------------- /img/floor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DotTech/HTML5-Raycaster/HEAD/img/floor.png -------------------------------------------------------------------------------- /img/pillar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DotTech/HTML5-Raycaster/HEAD/img/pillar.png -------------------------------------------------------------------------------- /img/painting.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DotTech/HTML5-Raycaster/HEAD/img/painting.png -------------------------------------------------------------------------------- /img/scaletest.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DotTech/HTML5-Raycaster/HEAD/img/scaletest.png -------------------------------------------------------------------------------- /img/barrel_mask.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DotTech/HTML5-Raycaster/HEAD/img/barrel_mask.png -------------------------------------------------------------------------------- /img/bricks-gray.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DotTech/HTML5-Raycaster/HEAD/img/bricks-gray.png -------------------------------------------------------------------------------- /img/pillar_mask.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DotTech/HTML5-Raycaster/HEAD/img/pillar_mask.png -------------------------------------------------------------------------------- /img/bricks-brown.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DotTech/HTML5-Raycaster/HEAD/img/bricks-brown.png -------------------------------------------------------------------------------- /css/main.css: -------------------------------------------------------------------------------- 1 | body { 2 | background: #000; 3 | font-family: "Trebuchet Ms", Helvetica, Arial, Verdana; 4 | font-size: 13px; 5 | color: #fff; 6 | } 7 | 8 | #wrapper { 9 | margin: 0 auto; 10 | width: 640px; 11 | } 12 | 13 | h1 { 14 | text-align: center; 15 | } 16 | 17 | p { 18 | text-align: center; 19 | line-height: 18px; 20 | } 21 | 22 | footer { 23 | text-align: center; 24 | margin-top: 10px; 25 | border-top: 1px solid #CCCCCC; 26 | padding-top: 10px; 27 | clear: both; 28 | } 29 | 30 | a { 31 | color: #ccc; 32 | } 33 | 34 | #settings { 35 | float: left; 36 | margin-top: 8px; 37 | width: 350px; 38 | padding: 0; 39 | margin: 0 0 20px 0; 40 | } 41 | 42 | #settings li { 43 | float: left; 44 | margin-right: 10px; 45 | list-style: none; 46 | margin-bottom: 5px; 47 | } 48 | 49 | #settings li input { 50 | float: left; 51 | margin: 0 3px 0 0 ; 52 | } 53 | 54 | #settings li label { 55 | float: left; 56 | } 57 | 58 | label[for=ddlQuality] { 59 | margin-left: 20px; 60 | } 61 | 62 | #keys { 63 | float: right; 64 | margin-bottom: 10px; 65 | } 66 | 67 | div.keys { 68 | width: 32px; 69 | height: 32px; 70 | background: #fff; 71 | cursor: pointer; 72 | float: left; 73 | margin-left: 10px; 74 | text-align: center; 75 | font-weight: bold; 76 | color: #000; 77 | line-height: 29px; 78 | } -------------------------------------------------------------------------------- /js/raycaster.constants.js: -------------------------------------------------------------------------------- 1 | /* 2 | // Namespace: Raycaster.Constants 3 | // Description: Definition of engine parameters and other constant values 4 | */ 5 | Raycaster.Constants = 6 | { 7 | fieldOfView: 66, // Field of view of the player (in degrees) 8 | screenWidth: 640, // Width of the viewport 9 | screenHeight: 480, // Height of the viewport 10 | angleBetweenRays: Number(66 / 640), // Angle between casted rays 11 | movementStep: 9, // How much the player moves each step 12 | turningStep: 3, // How fast the player turns 13 | startFadingAt: 100, // At what distance to start fading visibility 14 | playerStartAngle: 273, // Angle player is viewing at on startup 15 | playerStartPos: { // Location of player on startup 16 | x: 80, 17 | y: 160, 18 | z: 0 19 | }, 20 | distanceToViewport: 0, // This value is calculated once one startup 21 | noClipping: false, // Allows player to walk through walls and objects 22 | displayDebugInfo: false, 23 | runningLocal: location.href.indexOf("file://") > -1, 24 | texturesFiles: [ // Image files for wall textures 25 | "img/bricks-brown.png", 26 | "img/bricks-gray.png", 27 | "img/painting.png", 28 | "img/wood.png", 29 | "img/floor.png", 30 | ], 31 | spriteFiles: [ // Image files for sprites 32 | "img/barrel.png", 33 | "img/barrel_mask.png", 34 | "img/pillar.png", 35 | "img/pillar_mask.png", 36 | ], 37 | skyImage: "img/sky.jpg", // Background image (sky) 38 | debugFont: "bold 12px arial", // Font for debug info 39 | glIntervalTimeout: 50 // Gameloop interval timeout 40 | }; -------------------------------------------------------------------------------- /js/raycaster.drawing.js: -------------------------------------------------------------------------------- 1 | /* 2 | // Namespace: Raycaster.Drawing 3 | // Description: Basic canvas drawing functions 4 | */ 5 | Raycaster.Drawing = 6 | { 7 | // Clear the screen 8 | clear: function() 9 | { 10 | var context = Raycaster.Objects.context; 11 | context.clearRect(0, 0, context.canvas.width, context.canvas.height); 12 | }, 13 | 14 | // Get rgb() color string 15 | colorRgb: function(r, g, b) 16 | { 17 | return "rgb(" + r + ", " + g + ", " + b + ")"; 18 | }, 19 | 20 | // Get rgba() color string 21 | colorRgba: function(r, g, b, a) 22 | { 23 | return "rgba(" + r + ", " + g + ", " + b + ", " + a + ")"; 24 | }, 25 | 26 | // Draws a circle 27 | circle: function(x, y, radius, color) 28 | { 29 | var context = Raycaster.Objects.context; 30 | context.beginPath(); 31 | context.fillStyle = color; 32 | context.arc(x, y, radius, 0, Math.PI*2, true); 33 | context.fill(); 34 | context.closePath(); 35 | }, 36 | 37 | // Draws a square 38 | square: function(x, y, width, height, color) 39 | { 40 | var context = Raycaster.Objects.context; 41 | context.beginPath(); 42 | context.fillStyle = color; 43 | context.fillRect(x, y, width, height); 44 | context.closePath(); 45 | }, 46 | 47 | filledPath: function(vectorArray, color, shrinkFactor) 48 | { 49 | var context = Raycaster.Objects.context; 50 | context.beginPath(); 51 | 52 | for (var i in vectorArray) { 53 | var vector = vectorArray[i], 54 | x1 = shrinkFactor ? vector.x1 / shrinkFactor : vector.x1, 55 | x2 = shrinkFactor ? vector.x2 / shrinkFactor : vector.x2, 56 | y1 = shrinkFactor ? vector.y1 / shrinkFactor : vector.y1, 57 | y2 = shrinkFactor ? vector.y2 / shrinkFactor : vector.y2; 58 | 59 | if (i == 0) { 60 | context.moveTo(x1, y1); 61 | } 62 | context.lineTo(x2, y2); 63 | } 64 | 65 | context.fillStyle = color; 66 | context.fill(); 67 | 68 | context.closePath(); 69 | }, 70 | 71 | // Draws a line 72 | // Note: for some reason lineTo() and stroke() result in a semi-transparent line 73 | // If you want to be sure the line is of solid color, use lineSquare() instead 74 | line: function(x, y, x2, y2, color) 75 | { 76 | var context = Raycaster.Objects.context; 77 | context.beginPath(); 78 | context.moveTo(x, y); 79 | context.lineTo(x2, y2); 80 | context.strokeStyle = color; 81 | context.stroke(); 82 | context.closePath(); 83 | }, 84 | 85 | lineSquare: function(x, y, x2, y2, color) 86 | { 87 | Raycaster.Drawing.square(x, y, x2, y2 - y, color); 88 | }, 89 | 90 | // Draw text 91 | text: function(text, x, y, color, font) 92 | { 93 | var context = Raycaster.Objects.context; 94 | context.fillStyle = color; 95 | context.font = font; 96 | context.textBaseline = "top"; 97 | context.fillText(text, x, y); 98 | } 99 | }; -------------------------------------------------------------------------------- /js/raycaster.js: -------------------------------------------------------------------------------- 1 | /* 2 | HTML5 Raycaster Demo 3 | 4 | Author: Ruud van Falier (ruud@dottech.nl) 5 | Version: 0.9.1 6 | Released: 20 october 2011 7 | 8 | Demo: http://www.dottech.nl/raycaster/ 9 | Git: https://github.com/Stribe/HTML5-Raycaster 10 | 11 | This is a very basic raycasting engine running on a HTML5 canvas. 12 | Currently supports non-orthogonal walls with variable height, 13 | texture mapping and sprite rendering. 14 | FPS has dropped quite severely since more features were added and i hope to be able 15 | to improve on this after planned analysis and implemention of sectors. 16 | The old (orthogonal walls) version is available from the v0.3 branch. 17 | 18 | Feel free to use it for whatever you need it for. 19 | 20 | See readme.txt for revision log! 21 | */ 22 | 23 | /* 24 | // Namespace: Raycaster 25 | // Description: All parts of the application are stored in the Raycaster namespace 26 | // Each child namespace has its own js file which is included on startup 27 | // Properties: engine 28 | // Methods: start(canvasId) (void) 29 | */ 30 | var Raycaster = function() 31 | { 32 | // Define include files 33 | var jsfolder = "js/", 34 | includes = [ 35 | "raycaster.constants.js", "raycaster.classes.js", "raycaster.objects.js", 36 | "raycaster.objects.level.js", "raycaster.drawing.js", 37 | "raycaster.raycasting.js", "raycaster.renderengine.js", "raycaster.movement.js" 38 | ]; 39 | 40 | // Include all our javascript files 41 | for (var i in includes) { 42 | document.write(''); 43 | } 44 | 45 | // Public reference to the RenderEngine instance 46 | var engine = null; 47 | 48 | // Initialization method of the Raycaster application 49 | var start = function(canvasId) 50 | { 51 | // Hide introduction text and show debug info if running local 52 | if (Raycaster.Constants.runningLocal) { 53 | document.getElementById("introduction").style.display = "none"; 54 | Raycaster.Constants.displayDebugInfo = true; 55 | } 56 | 57 | // Set resolution 58 | var res = Raycaster.Objects.settings.selectedResolution(); 59 | Raycaster.Constants.screenWidth = res.w; 60 | Raycaster.Constants.screenHeight = res.h; 61 | 62 | // Setup the canvas 63 | var canvas = document.getElementById(canvasId); 64 | canvas.width = Raycaster.Constants.screenWidth; 65 | canvas.height = Raycaster.Constants.screenHeight; 66 | Raycaster.Objects.context = canvas.getContext("2d"); 67 | 68 | // Call level init (to allow runtime modifications to level) 69 | var level = Raycaster.Objects.settings.selectedLevel(); 70 | Raycaster.Objects.Level = demoLevels[level]; 71 | document.getElementById("ddlLevel").selectedIndex = level; 72 | 73 | Raycaster.Objects.Level.init(); 74 | 75 | // Load texture and sprite files 76 | Raycaster.Objects.loadResources(); 77 | 78 | // Create an instance of the RenderEngine 79 | Raycaster.engine = new Raycaster.RenderEngine(); 80 | 81 | // Bind keyboard events 82 | Raycaster.Movement.init(); 83 | 84 | // Gameloop 85 | Raycaster.Objects.gameloopInterval = setInterval(function() { 86 | Raycaster.Movement.update(); 87 | Raycaster.engine.update(); 88 | }, Raycaster.Constants.glIntervalTimeout); 89 | }; 90 | 91 | return { 92 | engine: engine, 93 | start: start 94 | }; 95 | }(); 96 | 97 | document.addEventListener("DOMContentLoaded", function() { 98 | Raycaster.start("raycaster"); 99 | }, true); 100 | 101 | -------------------------------------------------------------------------------- /readme.txt: -------------------------------------------------------------------------------- 1 | HTML5 Raycaster Demo 2 | 3 | Author: Ruud van Falier (ruud@dottech.nl) 4 | Version: 0.9.1 5 | Released: 20 october 2011 6 | 7 | Demo: http://www.dottech.nl/raycaster/ 8 | Git: https://github.com/Stribe/HTML5-Raycaster 9 | 10 | NOTE (15 may 2012): I have discontinued the development of this engine. 11 | This was my first raycasting engine and it solves some problems in undesirable ways. 12 | By now i have gotten many new insights in 3D programming and i actually did start developing an entirely new engine at some point, but i never finished to a point that it was worth releasing the code. 13 | Maybe i will in the future. 14 | 15 | 16 | DESCRIPTION: 17 | This is a very basic raycasting engine running on a HTML5 canvas. 18 | Currently supports non-orthogonal walls with variable height, 19 | texture mapping and sprite rendering. 20 | FPS has dropped quite severely since more features were added and i hope to be able 21 | to improve on this after planned analysis and implemention of sectors. 22 | The old (orthogonal walls) version is available from the v0.3 branch. 23 | 24 | Feel free to use it for whatever you need it for. 25 | 26 | Planned features: 27 | - Sectors 28 | - Elevated floors definitions should be more flexible 29 | - Repeating textures (instead of being stretched) 30 | 31 | Optimizations/fixes planned: 32 | - When drawing a wall slice, remember intersection and use it during sprite rendering to avoid having to search for blocking walls 33 | - Build some kind of intersection buffer, maybe pre-calculate them when loading the level 34 | - Sprites are not visible behind walls with lower height than the sprite 35 | - getIntersection should check for Z-coord 36 | 37 | Revision log: 38 | (0.2) - Initial release. 39 | Uses "wolfenstein" technique to render the world. 40 | Has a grid-bases level and supports only orthogonal walls. 41 | (0.3) - Redraw only when player has moved or settings have changed. 42 | - Removed jQuery dependency (thanks to James Abley, https://github.com/jabley) 43 | (0.4) - Attempted to implement non-orthogonal walls, but it was a failed attempt 44 | Never released this version 45 | (0.5) - Raycasting engine is rewritten and now supports non-orthogonal walls. 46 | - Strafing implemented. 47 | (0.5.1) - Added FPS counter 48 | (0.5.2) - Experimented with floor casting, but it costs too much performance. 49 | For that reason a gradient is used as floor. 50 | - Added sky background 51 | (0.6) - Refactored the code so that it could be stored in seperate files and added more comments 52 | - Implemented sprite rendering 53 | (0.7) - Fixed bug that caused incorrect rendering of walls that are very close to the player 54 | - Sprites rendering optimized and now supports vertically positioning of sprites 55 | - Variable wall height implemented 56 | Also support angled height (a different start and end height) 57 | (0.8) - Tried several techniques for speeding up drawing, but couldn't find anything faster. 58 | Also turns out that the big performance eater is not drawing, but calculating. 59 | - Implemented a few performance tweaks: 60 | * VSliceDrawParams and Intersection are now returning objects and dont need to be instantiated 61 | * Math.round() replaced for bitwise hack in functions that are frequently called 62 | - Fixed bug in Raycasting.correctQuadrant() which returned incorrect result at straight angles 63 | - Fixed texturing bug at 0 degrees (turns out i should not ignore angle=360 degrees, doh) 64 | - Experimental floorcasting implemented, its way too slow though 65 | - Player Z coord added and implemented elevation 66 | - Movement key bindings got a different setup 67 | - Texture mapping procedure is improved and now stretches correctly. 68 | Textures now always stretch in height to fit the wall and repeat over the width. 69 | (0.9) - Elevatated area support added (floor rendering not implemented) 70 | - Demo levels added and level switch implemented 71 | (0.9.1) - Slight performance tweak. 72 | - Added resolution switch with 320x240 option (since 640x480 is quite slow at this point). 73 | 74 | -------------------------------------------------------------------------------- /default.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | HTML5 Raycaster Demo 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 |

HTML5 Raycaster Demo

19 |

20 | This is a basic raycasting engine rendered on an HTML5 canvas.
21 | See readme.txt for more information.
22 |
23 | Code is available on GitHub
24 | Old, simpler, version with grid-based engine still available here.
25 |
26 | Runs fastest in Chrome or IE9 and pretty slow in FF. Not yet tested in other browsers.
27 | If you don't see anything, you should update your browser...
28 |
29 | Usage: W/S = walk, Q/E = turn, A/D = strafe.
30 |

31 | 32 | 33 | 34 | 76 | 77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 | 86 | 89 | 90 |
91 | 92 | 103 | 104 | 105 | -------------------------------------------------------------------------------- /js/raycaster.objects.js: -------------------------------------------------------------------------------- 1 | /* 2 | // Namespace: Raycaster.Objects 3 | // Description: Instances of global objects and variables used throughout the application 4 | */ 5 | Raycaster.Objects = 6 | { 7 | // Defines player parameters 8 | player: 9 | { 10 | x: Raycaster.Constants.playerStartPos.x, 11 | y: Raycaster.Constants.playerStartPos.y, 12 | z: Raycaster.Constants.playerStartPos.z, 13 | angle: new Raycaster.Classes.Angle(Raycaster.Constants.playerStartAngle), 14 | height: 48, 15 | width: 0 16 | }, 17 | 18 | // Settings checkboxes states 19 | settings: 20 | { 21 | renderTextures: function() { 22 | return document.getElementById("chkTextures").checked; 23 | }, 24 | renderLighting: function() { 25 | return document.getElementById("chkLighting").checked; 26 | }, 27 | renderMiniMap: function() { 28 | return document.getElementById("chkMiniMap").checked; 29 | }, 30 | renderSky: function() { 31 | return document.getElementById("chkSky").checked; 32 | }, 33 | renderFloor: function() { 34 | return false; //document.getElementById("chkFloor").checked; 35 | }, 36 | renderSprites: function() { 37 | return document.getElementById("chkSprites").checked; 38 | }, 39 | selectedLevel: function() { 40 | var selected = 0; 41 | 42 | if (location.hash) { 43 | var settings = location.hash.split("#")[1]; 44 | var index = parseInt(settings.split(",")[0]); 45 | 46 | if (index) { 47 | selected = index; 48 | } 49 | } 50 | 51 | return selected; 52 | }, 53 | selectedResolution: function() { 54 | var selected = { w: 640, h: 480 }; 55 | 56 | if (location.hash) { 57 | var settings = location.hash.split("#")[1]; 58 | var res = parseInt(settings.split(",")[1]); 59 | 60 | if (res == 320) { 61 | selected = { w: 320, h: 240 }; 62 | document.getElementById("ddlSize").selectedIndex = 1; 63 | } 64 | else { 65 | document.getElementById("ddlSize").selectedIndex = 0; 66 | } 67 | } 68 | 69 | return selected; 70 | } 71 | }, 72 | 73 | // Definition of keyboard buttons 74 | keys: { 75 | arrowLeft: Raycaster.Classes.KeyButton(37), 76 | arrowUp: Raycaster.Classes.KeyButton(38), 77 | arrowRight: Raycaster.Classes.KeyButton(39), 78 | arrowDown: Raycaster.Classes.KeyButton(40), 79 | lessThan: Raycaster.Classes.KeyButton(188), 80 | greaterThan: Raycaster.Classes.KeyButton(190), 81 | esc: Raycaster.Classes.KeyButton(27), 82 | shift: Raycaster.Classes.KeyButton(16), 83 | charR: Raycaster.Classes.KeyButton(82), 84 | charA: Raycaster.Classes.KeyButton(65), 85 | charZ: Raycaster.Classes.KeyButton(90), 86 | charQ: Raycaster.Classes.KeyButton(81), 87 | charW: Raycaster.Classes.KeyButton(87), 88 | charE: Raycaster.Classes.KeyButton(69), 89 | charS: Raycaster.Classes.KeyButton(83), 90 | charD: Raycaster.Classes.KeyButton(68), 91 | charX: Raycaster.Classes.KeyButton(88) 92 | }, 93 | 94 | context: null, // Reference to the canvas context 95 | bufferctx: null, 96 | gameloopInterval: null, // Reference to the interval that triggers the update functions 97 | redrawScreen: true, // Wether it is necessary to redraw the scene 98 | textures: null, // Array with texture Image objects 99 | sprites: null, // Array with sprite Image objects 100 | skyImage: new Image(), // Holds the image that is used for the sky background 101 | 102 | /* 103 | // Method: loadResources 104 | // Description: Loads the texture and sprite images in memory 105 | // Parameters: - 106 | // Returns: - 107 | */ 108 | loadResources: function() 109 | { 110 | //need this when creating images in a different canvas 111 | //netscape.security.PrivilegeManager.enablePrivilege("UniversalBrowserRead"); 112 | 113 | // Load texture images 114 | Raycaster.Objects.textures = new Array(); 115 | for (var i = 0; i < Raycaster.Constants.texturesFiles.length; i++) { 116 | Raycaster.Objects.textures[i] = new Image(); 117 | Raycaster.Objects.textures[i].src = Raycaster.Constants.texturesFiles[i]; 118 | Raycaster.Objects.textures[i].onload = function() { 119 | Raycaster.Objects.redrawScreen = true; 120 | }; 121 | } 122 | 123 | // Load sprite images 124 | Raycaster.Objects.sprites = new Array(); 125 | for (var i = 0; i < Raycaster.Constants.spriteFiles.length; i++) { 126 | Raycaster.Objects.sprites[i] = new Image(); 127 | Raycaster.Objects.sprites[i].src = Raycaster.Constants.spriteFiles[i]; 128 | } 129 | 130 | // Load sky background image 131 | Raycaster.Objects.skyImage.src = Raycaster.Constants.skyImage; 132 | Raycaster.Objects.skyImage.onload = function() { 133 | Raycaster.Objects.redrawScreen = true; 134 | }; 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /js/raycaster.classes.js: -------------------------------------------------------------------------------- 1 | /* 2 | // Namespace: Raycaster.Classes 3 | // Description: Definition of classes 4 | */ 5 | Raycaster.Classes = 6 | { 7 | Point: function(x, y) { 8 | return { 9 | x: x, 10 | y: y 11 | }; 12 | }, 13 | 14 | Vector: function(x1, y1, x2, y2) { 15 | return { 16 | x1: x1, 17 | y1: y1, 18 | x2: x2, 19 | y2: y2 20 | }; 21 | }, 22 | 23 | /* 24 | // Class: Raycaster.Classes.Sprite 25 | // Description: Parameters that define a sprite in the game world 26 | */ 27 | Sprite: function(x, y, z, id, yoff) { 28 | return { 29 | x: x, // x,y location of sprite in the game world 30 | y: y, 31 | z: z, 32 | yoff: yoff, // Offset for Y coordinate, to make it possible to place things on the floor or ceiling 33 | id: id // index of sprite image in Constants.spriteFiles array 34 | }; 35 | }, 36 | 37 | /* 38 | // Class: Raycaster.Classes.Wall 39 | // Description: Parameters that define a wall in the game world 40 | */ 41 | Wall: function(x1, y1, x2, y2, z1, z2, h1, h2, textureId) { 42 | return { 43 | x1: x1, // x1, y1: wall start point 44 | y1: y1, 45 | x2: x2, // x2, y2: wall end point 46 | y2: y2, 47 | z1: z1, // wall elevation at start 48 | z2: z2, // wall elevation at end 49 | h1: h1, // wall height at start 50 | h2: h2, // wall height at end 51 | textureId: textureId, // id (index in Constants.texturesFiles array) of texture to use on this wall 52 | maxHeight: (h1 > h2 ? h1: h2) 53 | }; 54 | }, 55 | 56 | /* 57 | // Class: Raycaster.Classes.Elevation 58 | // Description: Defines an elevation in the floor 59 | */ 60 | Elevation: function(height, area) { 61 | return { 62 | area: area, // One Vector defining the size of the elevated area. 63 | // Currently limits to elevations being squared and angled straight 64 | height: height // Height of the elevation 65 | }; 66 | }, 67 | 68 | /* 69 | // Class: Raycaster.Classes.VSliceDrawParams 70 | // Description: Drawing parameters for a vertical slice (scanline) of an object 71 | */ 72 | VSliceDrawParams: function() { 73 | return { 74 | dy1: 0, // Destination start Y coord 75 | dy2: 0, // Destination end Y coord 76 | sy1: 0, // Source image start Y coord 77 | sy2: 0, // Source image end Y coord 78 | texture: null // Image object containing the texture or sprite to draw 79 | }; 80 | }, 81 | 82 | Intersection: function() { 83 | return { 84 | x: 0, // X coordinate of this intersection 85 | y: 0, // Y coordinate of this intersection 86 | distance: 0, // Distance to the intersection 87 | resourceId: 0, // index of texture or sprite image in Objects namespace 88 | levelObjectId: 0, // index of texture or sprite in Objects.Level namespace 89 | textureX: 0, // X coordinate of the texture scanline to draw 90 | isSprite: false, // true if intersection if for a sprite, otherwise its for a wall 91 | drawParams: null // VSliceDrawParams object for this intersection 92 | }; 93 | }, 94 | 95 | KeyButton: function(code) { 96 | return { 97 | code: code, 98 | pressed: false 99 | }; 100 | }, 101 | 102 | Angle: function(degrees) { 103 | 104 | var self = this; 105 | this.degrees = degrees; 106 | this.radians = 0; 107 | 108 | // Set the value of this angle 109 | // Corrects negative values or values greater than 360 degrees 110 | this.setValue = function(v) { 111 | self.degrees = Number(v); 112 | 113 | if (self.degrees >= 360) { 114 | self.degrees -= 360; 115 | } 116 | if (self.degrees < 0) { 117 | self.degrees += 360; 118 | } 119 | 120 | self.radians = self.toRadians(); 121 | }; 122 | 123 | // Converts the angle from degrees to radians 124 | this.toRadians = function() { 125 | if (self.degrees == 90) { 126 | return Math.PI / 2; 127 | } 128 | else if (self.degrees == 270) { 129 | return 3 * Math.PI / 2; 130 | } 131 | return self.degrees * (Math.PI / 180); 132 | }; 133 | 134 | // Turn the angle with n degrees 135 | this.turn = function(degrees) { 136 | self.setValue(self.degrees + degrees); 137 | }; 138 | 139 | // Determine to which quadrant of a circle the angle is facing 140 | this.getQuadrant = function() { 141 | var rounded = ~~ (0.5 + self.degrees); 142 | 143 | if ((rounded >= 0 || rounded == 360) && rounded < 90) { 144 | return 1; 145 | } 146 | if (rounded >= 90 && rounded < 180) { 147 | return 2; 148 | } 149 | if (rounded >= 180 && rounded < 270) { 150 | return 3; 151 | } 152 | if (rounded >= 270 && rounded < 360) { 153 | return 4; 154 | } 155 | }; 156 | 157 | this.setValue(degrees); 158 | } 159 | }; -------------------------------------------------------------------------------- /js/raycaster.objects.level.js: -------------------------------------------------------------------------------- 1 | // Hard-coded level definition 2 | var demoLevels = new Array(); 3 | 4 | demoLevels[0] = { 5 | walls: [ 6 | // Walls surrounding the level 7 | Raycaster.Classes.Wall(100, 10, 700, 10, 0, 0, 200, 200, 1), 8 | Raycaster.Classes.Wall(10, 100, 10, 700, 0, 0, 200, 200, 1), 9 | Raycaster.Classes.Wall(10, 100, 100, 10, 0, 0, 200, 200, 1), 10 | Raycaster.Classes.Wall(790, 100, 790, 700, 0, 0, 200, 200, 1), 11 | Raycaster.Classes.Wall(700, 10, 790, 100, 0, 0, 200, 200, 1), 12 | Raycaster.Classes.Wall(100, 790, 700, 790, 0, 0, 200, 200, 1), 13 | Raycaster.Classes.Wall(790, 700, 700, 790, 0, 0, 200, 200, 1), 14 | Raycaster.Classes.Wall(10, 700, 100, 790, 0, 0, 200, 200, 1), 15 | 16 | // Walls in the middle 17 | Raycaster.Classes.Wall(340, 300, 380, 320, 0, 0, 64, 64, 0), 18 | Raycaster.Classes.Wall(380, 320, 420, 360, 0, 0, 64, 64, 0), 19 | Raycaster.Classes.Wall(420, 360, 420, 424, 0, 0, 64, 64, 2), 20 | Raycaster.Classes.Wall(420, 424, 400, 440, 0, 0, 64, 64, 0), 21 | Raycaster.Classes.Wall(400, 440, 400, 470, 0, 0, 64, 64, 0), 22 | Raycaster.Classes.Wall(400, 470, 430, 510, 0, 0, 64, 64, 0), 23 | Raycaster.Classes.Wall(430, 510, 500, 530, 0, 0, 64, 64, 0), 24 | Raycaster.Classes.Wall(500, 530, 560, 530, 0, 0, 64, 64, 0), 25 | Raycaster.Classes.Wall(560, 530, 580, 500, 0, 0, 64, 64, 0), 26 | Raycaster.Classes.Wall(580, 500, 560, 280, 0, 0, 64, 64, 0), 27 | Raycaster.Classes.Wall(560, 280, 500, 220, 0, 0, 64, 64, 0), 28 | Raycaster.Classes.Wall(500, 220, 340, 190, 0, 0, 64, 64, 0), 29 | Raycaster.Classes.Wall(340, 190, 340, 300, 0, 0, 64, 64, 0), 30 | 31 | Raycaster.Classes.Wall(240, 10, 300, 84, 0, 0, 128, 128, 1), 32 | Raycaster.Classes.Wall(300, 84, 410, 84, 0, 0, 128, 128, 1), 33 | Raycaster.Classes.Wall(410, 84, 470, 10, 0, 0, 128, 128, 1) 34 | ], 35 | 36 | sprites: [ 37 | Raycaster.Classes.Sprite(355, 171, 0, 2), 38 | Raycaster.Classes.Sprite(355, 137, 0, 2), 39 | Raycaster.Classes.Sprite(355, 103, 0, 2), 40 | Raycaster.Classes.Sprite(320, 230, 0, 0), 41 | Raycaster.Classes.Sprite(320, 247, 30, 0), 42 | Raycaster.Classes.Sprite(320, 265, 0, 0) 43 | ], 44 | 45 | // Define floor elevations 46 | // Make sure the vectors appear in the correct order (connecting lines) and the area is closed 47 | // Must be a straight square!! 48 | elevations: [ 49 | ], 50 | 51 | floorTextureId: 4, 52 | 53 | init: function() { 54 | Raycaster.Objects.player.angle.setValue(340); 55 | } 56 | }; 57 | 58 | demoLevels[1] = 59 | { 60 | walls: [ 61 | // Walls surrounding the level 62 | Raycaster.Classes.Wall(100, 10, 700, 10, 0, 0, 200, 200, 1), 63 | Raycaster.Classes.Wall(10, 100, 10, 700, 0, 0, 200, 200, 1), 64 | Raycaster.Classes.Wall(10, 100, 100, 10, 0, 0, 200, 200, 1), 65 | Raycaster.Classes.Wall(790, 100, 790, 700, 0, 0, 200, 200, 1), 66 | Raycaster.Classes.Wall(700, 10, 790, 100, 0, 0, 200, 200, 1), 67 | Raycaster.Classes.Wall(100, 790, 700, 790, 0, 0, 200, 200, 1), 68 | Raycaster.Classes.Wall(790, 700, 700, 790, 0, 0, 200, 200, 1), 69 | Raycaster.Classes.Wall(10, 700, 100, 790, 0, 0, 200, 200, 1), 70 | 71 | // Stairways 72 | Raycaster.Classes.Wall(59, 240, 10, 240, 0, 0, 200, 200, 0), 73 | Raycaster.Classes.Wall(59, 240, 59, 600, 0, 0, 200, 200, 0), 74 | Raycaster.Classes.Wall(59, 601, 400, 601, 0, 0, 200, 200, 0), 75 | Raycaster.Classes.Wall(400, 601, 400, 650, 0, 0, 200, 200, 0), 76 | Raycaster.Classes.Wall(399, 650, 10, 650, 0, 0, 200, 200, 0), 77 | Raycaster.Classes.Wall(181, 240, 180, 480, 0, 0, 200, 200, 0), 78 | Raycaster.Classes.Wall(181, 480, 400, 480, 0, 0, 200, 200, 0), 79 | Raycaster.Classes.Wall(400, 481, 400, 600, 0, 0, 45, 45, 0), 80 | Raycaster.Classes.Wall(181, 240, 400, 480, 0, 0, 200, 200, 0) 81 | ], 82 | 83 | sprites: [ 84 | ], 85 | 86 | // Define floor elevations 87 | // Make sure the vectors appear in the correct order (connecting lines) and the area is closed 88 | // Must be a straight square!! 89 | elevations: [ 90 | // Elevate the floor inside the square 91 | Raycaster.Classes.Elevation(45, Raycaster.Classes.Vector(180, 480, 400, 600)) 92 | ], 93 | 94 | floorTextureId: 4, 95 | 96 | // Generates a stairway in the level 97 | init: function() { 98 | Raycaster.Objects.player.angle.setValue(280); 99 | 100 | var level = Raycaster.Objects.Level, 101 | classes = Raycaster.Classes, 102 | stairwidth = 120, 103 | stepwidth = 120, 104 | startX = 60, 105 | startY = 240; 106 | 107 | // Generate walls for stairway 108 | for (var i = 0; i < 3; i++) { 109 | var stepheight = 15, 110 | z = i * 15, 111 | y = startY + i * stepwidth; 112 | 113 | level.walls[level.walls.length] = classes.Wall(startX, y, startX + stairwidth, y, z, z, stepheight, stepheight, 0); 114 | if (i < 2) { 115 | level.walls[level.walls.length] = classes.Wall(startX + stairwidth, y, startX + stairwidth, y + stepwidth, z, z, stepheight, stepheight, 0); 116 | level.walls[level.walls.length] = classes.Wall(startX + stairwidth, y + stepwidth, startX, y + stepwidth, z, z, stepheight, stepheight, 0); 117 | level.walls[level.walls.length] = classes.Wall(startX, y + stepwidth, startX, y, z, z, stepheight, stepheight, 0); 118 | } 119 | level.elevations[level.elevations.length] = Raycaster.Classes.Elevation(z + stepheight, Raycaster.Classes.Vector(startX, y, startX + stairwidth, y + stepwidth)); 120 | } 121 | } 122 | }; 123 | 124 | demoLevels[2] = 125 | { 126 | walls: [ 127 | // Walls surrounding the level 128 | Raycaster.Classes.Wall(100, 10, 700, 10, 0, 0, 200, 200, 1), 129 | Raycaster.Classes.Wall(10, 100, 10, 700, 0, 0, 200, 200, 1), 130 | Raycaster.Classes.Wall(10, 100, 100, 10, 0, 0, 200, 200, 1), 131 | Raycaster.Classes.Wall(790, 100, 790, 700, 0, 0, 200, 200, 1), 132 | Raycaster.Classes.Wall(700, 10, 790, 100, 0, 0, 200, 200, 1), 133 | Raycaster.Classes.Wall(100, 790, 700, 790, 0, 0, 200, 200, 1), 134 | Raycaster.Classes.Wall(790, 700, 700, 790, 0, 0, 200, 200, 1), 135 | Raycaster.Classes.Wall(10, 700, 100, 790, 0, 0, 200, 200, 1), 136 | 137 | // Angled and elevated walls 138 | Raycaster.Classes.Wall(300, 10, 300, 90, 0, 0, 120, 120, 0), 139 | Raycaster.Classes.Wall(300, 90, 300, 130, 40, 120, 80, 80, 0), 140 | Raycaster.Classes.Wall(300, 130, 300, 170, 120, 120, 80, 80, 0), 141 | Raycaster.Classes.Wall(300, 170, 300, 210, 120, 40, 80, 80, 0), 142 | Raycaster.Classes.Wall(300, 210, 300, 290, 0, 0, 120, 120, 0) 143 | ], 144 | 145 | sprites: [ 146 | Raycaster.Classes.Sprite(455, 171, 0, 2), 147 | Raycaster.Classes.Sprite(455, 137, 0, 2), 148 | Raycaster.Classes.Sprite(455, 103, 0, 2), 149 | Raycaster.Classes.Sprite(420, 230, 0, 0), 150 | Raycaster.Classes.Sprite(420, 247, 30, 0), 151 | Raycaster.Classes.Sprite(420, 265, 0, 0) 152 | ], 153 | 154 | elevations: [ 155 | ], 156 | 157 | floorTextureId: 4, 158 | 159 | // Generates a stairway in the level 160 | init: function() { 161 | Raycaster.Objects.player.angle.setValue(4); 162 | } 163 | }; 164 | 165 | Raycaster.Objects.Level = demoLevels[0]; -------------------------------------------------------------------------------- /js/raycaster.movement.js: -------------------------------------------------------------------------------- 1 | /* 2 | // Namespace: Raycaster.Drawing 3 | // Description: Contains all movement related functions 4 | // 5 | // Public methods: init() 6 | // update() 7 | */ 8 | Raycaster.Movement = function() 9 | { 10 | var player = Raycaster.Objects.player; 11 | 12 | /****************** / Private methods / *****************/ 13 | // Returns closest intersection (wall or sprite) at specified angle. 14 | // This is used for collision detection. 15 | var findIntersection = function(angle) 16 | { 17 | if (Raycaster.Constants.noClipping) { 18 | return false; 19 | } 20 | 21 | var objects = Raycaster.Raycasting.findObjects(angle); 22 | 23 | if (objects.length > 0) { 24 | return objects[objects.length - 1]; 25 | } 26 | 27 | return false; 28 | } 29 | 30 | // Make the player turn by increasing its viewing angle 31 | var turn = function(angle) 32 | { 33 | player.angle.turn(angle); 34 | Raycaster.engine.redraw(); 35 | }; 36 | 37 | // Make the player walk forward or backwards 38 | var walk = function(forward) 39 | { 40 | step = forward ? Raycaster.Constants.movementStep : -Raycaster.Constants.movementStep; 41 | 42 | var delta = Raycaster.Raycasting.getDeltaXY(player.angle, step); 43 | 44 | var angle = forward 45 | ? player.angle 46 | : new Raycaster.Classes.Angle(player.angle.degrees + 180); 47 | 48 | var intersection = findIntersection(angle); 49 | 50 | if (!intersection || intersection.distance > 50) { 51 | player.x = Math.round(player.x + delta.x); 52 | player.y = Math.round(player.y - delta.y); 53 | } 54 | 55 | Raycaster.engine.redraw(); 56 | }; 57 | 58 | // Elevate player up or down 59 | var elevate = function(up) 60 | { 61 | step = up ? Raycaster.Constants.movementStep : -Raycaster.Constants.movementStep; 62 | player.z += step; 63 | 64 | // Prevent player from going under ground 65 | if (player.z < 0) { 66 | player.z = 0; 67 | } 68 | 69 | Raycaster.engine.redraw(); 70 | }; 71 | 72 | // Make player strafe left or right 73 | var strafe = function(left) 74 | { 75 | var angle = left 76 | ? new Raycaster.Classes.Angle(player.angle.degrees + 90) 77 | : new Raycaster.Classes.Angle(player.angle.degrees - 90); 78 | 79 | var delta = Raycaster.Raycasting.getDeltaXY(angle, Raycaster.Constants.movementStep); 80 | intersection = findIntersection(angle); 81 | 82 | if (!intersection || intersection.distance > 20) { 83 | player.x = Math.round(player.x + delta.x); 84 | player.y = Math.round(player.y - delta.y); 85 | } 86 | 87 | Raycaster.engine.redraw(); 88 | }; 89 | 90 | /****************** / Public methods / *****************/ 91 | // Update movement 92 | var update = function() 93 | { 94 | // Turn left 95 | if (Raycaster.Objects.keys.arrowLeft.pressed || Raycaster.Objects.keys.charQ.pressed) { 96 | turn(Raycaster.Constants.turningStep); 97 | } 98 | // Turn right 99 | else if (Raycaster.Objects.keys.arrowRight.pressed || Raycaster.Objects.keys.charE.pressed) { 100 | turn(-Raycaster.Constants.turningStep); 101 | } 102 | 103 | // Walk forward 104 | if (Raycaster.Objects.keys.arrowUp.pressed || Raycaster.Objects.keys.charW.pressed) { 105 | walk(true); 106 | } 107 | // Walk backward 108 | else if (Raycaster.Objects.keys.arrowDown.pressed || Raycaster.Objects.keys.charS.pressed) { 109 | walk(false); 110 | } 111 | 112 | // Strafe left 113 | if (Raycaster.Objects.keys.charA.pressed) { 114 | strafe(true); 115 | } 116 | // Strafe right 117 | else if (Raycaster.Objects.keys.charD.pressed) { 118 | strafe(false); 119 | } 120 | 121 | // Stop gameloop interval 122 | if (Raycaster.Objects.keys.esc.pressed) { 123 | clearInterval(Raycaster.Objects.gameloopInterval); 124 | } 125 | 126 | // Reverse player angle 127 | if (Raycaster.Objects.keys.charR.pressed) { 128 | Raycaster.Objects.player.angle.setValue(Raycaster.Objects.player.angle.degrees - 180); 129 | Raycaster.Objects.keys.charR.pressed = false; 130 | Raycaster.engine.redraw(); 131 | } 132 | 133 | if (Raycaster.Objects.keys.charZ.pressed) { 134 | elevate(true); 135 | } 136 | 137 | if (Raycaster.Objects.keys.charX.pressed) { 138 | elevate(false); 139 | } 140 | } 141 | 142 | // Bind the arrow keydown events 143 | var init = function() 144 | { 145 | // Prevents default event handling 146 | var preventDefault = function(e) 147 | { 148 | e.preventDefault 149 | ? e.preventDefault() 150 | : e.returnValue = false; 151 | }; 152 | 153 | // Handler for keydown events 154 | var keyDownHandler = function (e) 155 | { 156 | var keyCode = e.keyCode || e.which; 157 | 158 | for (var name in Raycaster.Objects.keys) { 159 | if (Raycaster.Objects.keys[name].code == keyCode) { 160 | Raycaster.Objects.keys[name].pressed = true; 161 | preventDefault(e); 162 | } 163 | } 164 | }; 165 | 166 | // Handler for keyup events 167 | var keyUpHandler = function (e) 168 | { 169 | var keyCode = e.keyCode || e.which; 170 | 171 | for (var name in Raycaster.Objects.keys) { 172 | if (Raycaster.Objects.keys[name].code == keyCode) { 173 | Raycaster.Objects.keys[name].pressed = false; 174 | preventDefault(e); 175 | } 176 | } 177 | }; 178 | 179 | window.addEventListener("keydown", keyDownHandler, false); 180 | window.addEventListener("keyup", keyUpHandler, false); 181 | 182 | // Bind key icons to mobile devices which have no keyboard 183 | var keys = document.getElementsByClassName("keys"); 184 | 185 | for (var i = 0, n = keys.length; i < n; ++i) { 186 | var key = keys[i]; 187 | var keyCode = parseInt(key.getAttribute("data-code"), 0); 188 | 189 | (function(k, kc) { 190 | k.addEventListener("mouseover", function() { 191 | keyDownHandler({ keyCode: kc }); 192 | return false; 193 | }, false); 194 | k.addEventListener("mouseout", function() { 195 | keyUpHandler({ keyCode: kc }); 196 | return false; 197 | }, false); 198 | }(key, keyCode)); 199 | } 200 | 201 | // Redraw when settings change 202 | document.getElementById("chkTextures").addEventListener('change', Raycaster.engine.redraw); 203 | document.getElementById("chkLighting").addEventListener('change', Raycaster.engine.redraw); 204 | document.getElementById("chkMiniMap").addEventListener('change', Raycaster.engine.redraw); 205 | document.getElementById("chkSky").addEventListener('change', Raycaster.engine.redraw); 206 | //document.getElementById("chkFloor").addEventListener('change', Raycaster.engine.redraw); 207 | document.getElementById("chkSprites").addEventListener('change', Raycaster.engine.redraw); 208 | 209 | // Level and size selection 210 | document.getElementById("ddlLevel").addEventListener('change', function() { 211 | location.hash = document.getElementById("ddlLevel").selectedIndex + "," + Raycaster.Constants.screenWidth; 212 | location.reload(); 213 | }); 214 | document.getElementById("ddlSize").addEventListener('change', function() { 215 | location.hash = document.getElementById("ddlLevel").selectedIndex + "," + 216 | document.getElementById("ddlSize").options[document.getElementById("ddlSize").selectedIndex].value; 217 | location.reload(); 218 | }); 219 | }; 220 | 221 | return { 222 | update: update, 223 | init: init 224 | }; 225 | }(); -------------------------------------------------------------------------------- /js/raycaster.renderengine.js: -------------------------------------------------------------------------------- 1 | /* 2 | // Namespace: Raycaster.RenderEngine 3 | // Description: The render engine class contains all methods required for drawing the 3D world and related stuff. 4 | // Reasons for making this non-static is that the instance needs to be created after the canvas context is created. 5 | // Raycaster.start() creates the RenderEngine instances which is accessible through Raycaster.engine 6 | // 7 | // Public methods: redraw() 8 | // update() 9 | */ 10 | Raycaster.RenderEngine = function() 11 | { 12 | var context = Raycaster.Objects.context, 13 | constants = Raycaster.Constants, 14 | objects = Raycaster.Objects, 15 | drawing = Raycaster.Drawing, 16 | classes = Raycaster.Classes, 17 | raycasting = Raycaster.Raycasting, 18 | lastFpsUpdate = new Date().getTime(); 19 | 20 | // Calculate viewport distance and angle between casted rays 21 | constants.distanceToViewport = Math.round(constants.screenWidth / 2 / Math.tan(constants.fieldOfView / 2 * (Math.PI / 180))); 22 | constants.angleBetweenRays = Number(constants.fieldOfView / constants.screenWidth); 23 | 24 | 25 | /************* / Rendering of secondary stuff (sky, floor, map) / *****************/ 26 | 27 | // Writes debug information to the screen 28 | var drawDebugInfo = function() 29 | { 30 | if (constants.displayDebugInfo) { 31 | var elapsed = new Date().getTime() - lastFpsUpdate, 32 | fps = Math.round(1000 / elapsed), 33 | fpsText = fps + " fps"; 34 | 35 | Raycaster.Drawing.text(fpsText, 590, 10, Raycaster.Drawing.colorRgb(255, 255, 255), Raycaster.Constants.debugFont); 36 | lastFpsUpdate = new Date().getTime(); 37 | 38 | Raycaster.Drawing.text("X: " + Math.round(objects.player.x) + " Y: " + Math.round(objects.player.y) + " Z: " + Math.round(objects.player.z), 530, 30, Raycaster.Drawing.colorRgb(255, 255, 255), Raycaster.Constants.debugFont); 39 | Raycaster.Drawing.text("A: " + objects.player.angle.degrees, 590, 50, Raycaster.Drawing.colorRgb(255, 255, 255), Raycaster.Constants.debugFont); 40 | } 41 | } 42 | 43 | // Draw the 2D top-view of the level 44 | var drawMiniMap = function() 45 | { 46 | if (objects.settings.renderMiniMap()) { 47 | // Map is smaller than world, determine shrink factor 48 | var shrinkFactor = 5, 49 | mapOffsetX = 0, 50 | mapOffsetY = 0, 51 | odd = false; 52 | 53 | context.globalAlpha = 0.6; 54 | 55 | // Draw white background 56 | drawing.square(mapOffsetX, mapOffsetY, 57 | 160, 160, 58 | drawing.colorRgb(255, 255, 255)); 59 | 60 | // Draw elevations 61 | for (var i in Raycaster.Objects.Level.elevations) { 62 | var elevation = Raycaster.Objects.Level.elevations[i], 63 | area = elevation.area; 64 | 65 | drawing.square(area.x1 / shrinkFactor, area.y1 / shrinkFactor, (area.x2 - area.x1) / shrinkFactor, (area.y2 - area.y1) / shrinkFactor, drawing.colorRgb(255, 0, 0)); 66 | } 67 | 68 | // Draw the walls 69 | for (var i in Raycaster.Objects.Level.walls) { 70 | var wall = Raycaster.Objects.Level.walls[i]; 71 | drawing.line(mapOffsetX + wall.x1 / shrinkFactor, mapOffsetY + wall.y1 / shrinkFactor, 72 | mapOffsetX + wall.x2 / shrinkFactor, mapOffsetY + wall.y2 / shrinkFactor, drawing.colorRgb(0, 0, 0)); 73 | } 74 | 75 | // Draw sprites 76 | for (var i in Raycaster.Objects.Level.sprites) { 77 | var sprite = Raycaster.Objects.Level.sprites[i]; 78 | drawing.circle(mapOffsetX + sprite.x / shrinkFactor, mapOffsetY + sprite.y / shrinkFactor, 2, drawing.colorRgb(0, 255, 0)); 79 | } 80 | 81 | // Draw player 82 | var playerX = Math.floor(objects.player.x / shrinkFactor), 83 | playerY = Math.floor(objects.player.y / shrinkFactor); 84 | 85 | drawing.circle(mapOffsetX + playerX, mapOffsetY + playerY, 3, drawing.colorRgb(255, 0, 0)); 86 | 87 | // Visualize the viewing range on the map 88 | var angle = new classes.Angle(objects.player.angle.degrees + constants.fieldOfView / 2), 89 | rayStep = 10; 90 | 91 | for (var i = 0; i < constants.screenWidth; i += rayStep) 92 | { 93 | var deltaX = Math.floor(Math.cos(angle.radians) * (Math.abs(200) / shrinkFactor)), 94 | deltaY = Math.floor(Math.sin(angle.radians) * (Math.abs(200) / shrinkFactor)); 95 | 96 | drawing.line(mapOffsetX + playerX, mapOffsetY + playerY, 97 | playerX + deltaX, playerY - deltaY, drawing.colorRgb(200, 200, 0)); 98 | 99 | angle.turn(-constants.angleBetweenRays * rayStep); 100 | } 101 | 102 | context.globalAlpha = 1; 103 | } 104 | }; 105 | 106 | // Draw sky background 107 | var drawSky = function() 108 | { 109 | if (objects.settings.renderSky()) { 110 | var skyX = objects.skyImage.width - parseInt(objects.player.angle.degrees * (objects.skyImage.width / 360)), 111 | skyWidth = constants.screenWidth, 112 | leftOverWidth = 0; 113 | 114 | if (skyX + skyWidth > objects.skyImage.width) { 115 | leftOverWidth = skyX + skyWidth - objects.skyImage.width; 116 | skyWidth -= leftOverWidth; 117 | } 118 | 119 | if (skyWidth > 0) { 120 | context.drawImage(objects.skyImage, 121 | skyX, 0, skyWidth, constants.screenHeight / 2, 122 | 0, 0, skyWidth, constants.screenHeight / 2); 123 | } 124 | 125 | if (leftOverWidth > 0) { 126 | context.drawImage(objects.skyImage, 127 | 0, 0, leftOverWidth, constants.screenHeight / 2, 128 | skyWidth, 0, leftOverWidth, constants.screenHeight / 2); 129 | } 130 | } 131 | } 132 | 133 | // Draws the gradient for the floor 134 | var drawFloorGradient = function() 135 | { 136 | var context = Raycaster.Objects.context, 137 | gradient = context.createLinearGradient(0, constants.screenHeight / 2, 0, constants.screenHeight); 138 | 139 | gradient.addColorStop(0, drawing.colorRgb(20, 20, 20)); 140 | gradient.addColorStop(0.25, drawing.colorRgb(40, 40, 40)); 141 | gradient.addColorStop(0.6, drawing.colorRgb(100, 100, 100)); 142 | gradient.addColorStop(1, drawing.colorRgb(130, 130, 130)); 143 | 144 | context.fillStyle = gradient; 145 | context.fillRect(0, constants.screenHeight / 2, constants.screenWidth, constants.screenHeight / 2); 146 | } 147 | 148 | // Perform floor casting for scanline 149 | // Floor casting is too slow at this point.. 150 | // The calculations alone lose us a lot of FPS, not even to speak about drawing the pixels... 151 | // (thats why there is drawFloorGradient() to use as alternative) 152 | var drawFloor = function(vscan, startY, intersection) 153 | { 154 | var step = 1; 155 | 156 | // Formula from: http://lodev.org/cgtutor/raycasting2.html 157 | if (objects.settings.renderFloor() && vscan % step == 0) { 158 | 159 | var floorTexture = objects.textures[Raycaster.Objects.Level.floorTextureId]; 160 | 161 | for (var y = startY; y < constants.screenHeight; y += step) { 162 | var curdist = constants.screenHeight / (2 * y - constants.screenHeight); 163 | 164 | var weight = curdist / intersection.distance, 165 | floorX = weight * intersection.x + (1 - weight) * objects.player.x, 166 | floorY = weight * intersection.y + (1 - weight) * objects.player.y, 167 | textureX = parseInt(floorX * floorTexture.width) % floorTexture.width, 168 | textureY = parseInt(floorY * floorTexture.height) % floorTexture.height; 169 | 170 | context.drawImage(floorTexture, 171 | textureX, textureY, 1, 1, 172 | vscan, y, step, step); 173 | 174 | //if (objects.settings.renderLighting() && curdist > 100) { 175 | // drawing.lineSquare(vscan, y, vscan + 1, y + 1, drawing.colorRgba(0, 0, 0, calcDistanceOpacity(curdist))) 176 | //} 177 | } 178 | } 179 | }; 180 | 181 | /****************** / Core rendering methods (walls, sprites) / *****************/ 182 | 183 | // Search for objects in given direction and draw the vertical scanline for them. 184 | // All objects in visible range will be drawn in order of distance. 185 | var drawObjects = function(vscan, angle) 186 | { 187 | var intersections = raycasting.findObjects(angle, vscan); 188 | 189 | // Draw walls for each found intersection 190 | for (var i = 0; i < intersections.length; i++) { 191 | var intersection = intersections[i]; 192 | if (intersection.isSprite) { 193 | drawSprite(vscan, intersection); 194 | } 195 | else { 196 | drawWall(vscan, intersection); 197 | } 198 | } 199 | 200 | // Floor casting (still way too slow) 201 | //drawFloor(vscan, drawParams.dy2, intersection); 202 | } 203 | 204 | // Draw the vertical slice for a wall 205 | var drawWall = function(vscan, intersection) 206 | { 207 | var drawParams = intersection.drawParams; 208 | 209 | if (objects.settings.renderTextures()) { 210 | // Draw wall slice with texture 211 | context.drawImage(drawParams.texture, 212 | intersection.textureX, drawParams.sy1, 1, drawParams.sy2 - drawParams.sy1, 213 | vscan, drawParams.dy1, 1, drawParams.dy2 - drawParams.dy1); 214 | } 215 | else { 216 | // Draw without textures 217 | drawing.lineSquare(vscan, drawParams.dy1, 1, drawParams.dy2, drawing.colorRgb(128, 0, 0)); 218 | } 219 | 220 | // Make walls in the distance appear darker 221 | if (objects.settings.renderLighting() && intersection.distance > constants.startFadingAt) { 222 | drawing.lineSquare(vscan, drawParams.dy1, 1, drawParams.dy2, drawing.colorRgba(0, 0, 0, calcDistanceOpacity(intersection.distance))) 223 | } 224 | } 225 | 226 | // Draw the vertical slice for a sprite 227 | var drawSprite = function(vscan, intersection) 228 | { 229 | if (objects.settings.renderSprites()) { 230 | var drawParams = intersection.drawParams; 231 | 232 | context.drawImage(drawParams.texture, 233 | intersection.textureX, drawParams.sy1, 1, drawParams.sy2 - drawParams.sy1, 234 | vscan, drawParams.dy1, 1, drawParams.dy2 - drawParams.dy1); 235 | 236 | // Make sprites in the distance appear darker 237 | if (objects.settings.renderLighting() && intersection.distance > constants.startFadingAt) { 238 | var opacity = calcDistanceOpacity(intersection.distance); 239 | 240 | // There is a black image mask of every sprite located in the sprites array, one index after the original sprite. 241 | // Draw the mask over the sprite using the calculated opacity 242 | if (opacity > 0) { 243 | context.globalAlpha = opacity; 244 | context.drawImage(objects.sprites[intersection.resourceIndex + 1], 245 | intersection.textureX, drawParams.sy1, 1, drawParams.sy2 - drawParams.sy1, 246 | vscan, drawParams.dy1, 1, drawParams.dy2 - drawParams.dy1); 247 | context.globalAlpha = 1; 248 | } 249 | } 250 | } 251 | } 252 | 253 | // Calculates the opacity for the black overlay image that is used to make objects in the distance appear darker 254 | var calcDistanceOpacity = function(distance) 255 | { 256 | var colorDivider = Number(distance / (constants.startFadingAt * 1.5)); 257 | colorDivider = (colorDivider > 5) ? 5 : colorDivider; 258 | 259 | return Number(1 - 1 / colorDivider); 260 | }; 261 | 262 | // Draw 3D representation of the world 263 | var drawWorld = function() 264 | { 265 | // Draw solid floor and ceiling 266 | drawing.clear(); 267 | drawing.square(0, 0, constants.screenWidth, constants.screenHeight / 2, drawing.colorRgb(60, 60, 60)); 268 | drawing.square(0, constants.screenHeight / 2, constants.screenWidth, constants.screenHeight / 2, drawing.colorRgb(120, 120, 120)); 269 | 270 | // Draw sky and floor (floorcasting is disabled) 271 | drawSky(); 272 | drawFloorGradient(); 273 | 274 | var angle = new classes.Angle(objects.player.angle.degrees + constants.fieldOfView / 2); 275 | 276 | // Render the walls 277 | for (var vscan = 0; vscan < constants.screenWidth; vscan++) { 278 | drawObjects(vscan, angle); 279 | angle.turn(-constants.angleBetweenRays); 280 | } 281 | }; 282 | 283 | // Calculates if the player is standing inside an elevated area and returns the elevation 284 | // If elevation has changed we need to update the player's Z coord 285 | var updateElevation = function() 286 | { 287 | for (var i in Raycaster.Objects.Level.elevations) { 288 | var elevation = Raycaster.Objects.Level.elevations[i], 289 | area = elevation.area; 290 | 291 | if (objects.player.x >= area.x1 && objects.player.x <= area.x2 && 292 | objects.player.y >= area.y1 && objects.player.y <= area.y2) 293 | { 294 | objects.player.z = elevation.height; 295 | return; 296 | } 297 | } 298 | 299 | objects.player.z = 0; 300 | }; 301 | 302 | /****************** / Public methods / *****************/ 303 | // Tell renderengine that a redraw is required 304 | var redraw = function() 305 | { 306 | objects.redrawScreen = true; 307 | } 308 | 309 | // Execute all rendering tasks 310 | var update = function() 311 | { 312 | if (objects.redrawScreen) { 313 | updateElevation(); 314 | drawWorld(); 315 | drawMiniMap(); 316 | drawDebugInfo(); 317 | 318 | objects.redrawScreen = false; 319 | } 320 | } 321 | 322 | // Expose public members 323 | return { 324 | redraw: redraw, 325 | update: update 326 | }; 327 | }; -------------------------------------------------------------------------------- /js/raycaster.raycasting.js: -------------------------------------------------------------------------------- 1 | /* 2 | // Namespace: Raycaster.Raycasting 3 | // Description: Functionality required for raycasting 4 | // 5 | // Public methods: findWall(angle): returns Intersection object or false 6 | // findSprite(angle): returns Intersection object or false 7 | // getWallHeight: returns height for wall at specified intersection 8 | // getDeltaXY: returns difference in x and y for a distance at specified angle 9 | */ 10 | Raycaster.Raycasting = function() 11 | { 12 | var objects = Raycaster.Objects, 13 | player = objects.player, 14 | classes = Raycaster.Classes, 15 | constants = Raycaster.Constants, 16 | fishbowlFixValue = 0; 17 | 18 | /****************** / Private methods / *****************/ 19 | // Determine wether an intersection is located in the quadrant we are looking at 20 | var correctQuadrant = function(intersection, angle) 21 | { 22 | var deltaX = player.x - intersection.x, 23 | deltaY = player.y - intersection.y, 24 | quadrant = 0; 25 | 26 | var roundedAngle = ~~ (0.5 + angle.degrees); 27 | if (roundedAngle == 0 || roundedAngle == 360) { 28 | return deltaX < 0; 29 | } 30 | else if (roundedAngle == 90) { 31 | return deltaY > 0; 32 | } 33 | else if (roundedAngle == 180) { 34 | return deltaX > 0; 35 | } 36 | else if (roundedAngle == 270) { 37 | return deltaY < 0; 38 | } 39 | 40 | if (deltaX < 0 && deltaY >= 0) quadrant = 1; 41 | if (deltaX >= 0 && deltaY > 0) quadrant = 2; 42 | if (deltaX > 0 && deltaY <= 0) quadrant = 3; 43 | if (deltaX <= 0 && deltaY < 0) quadrant = 4; 44 | 45 | return quadrant == angle.getQuadrant(); 46 | } 47 | 48 | // Calculates the length of the hypotenuse side (angled side) of a triangle 49 | // adjacentLength: length of the side adjacent to the angle 50 | // oppositeLength: length of the side opposite of the angle 51 | var getHypotenuseLength = function(adjacentLength, oppositeLength) 52 | { 53 | return Math.sqrt(Math.pow(Math.abs(adjacentLength), 2) + Math.pow(Math.abs(oppositeLength), 2)); 54 | } 55 | 56 | // Calculate value needed to manipulate distance to counter "fishbowl effect" 57 | var setFishbowlFixValue = function(vscan) 58 | { 59 | var distortRemove = new classes.Angle(constants.fieldOfView / 2); 60 | distortRemove.turn(-constants.angleBetweenRays * vscan); 61 | 62 | fishbowlFixValue = Math.cos(distortRemove.radians); 63 | }; 64 | 65 | // Calculate intersection point on a line (wall) 66 | // Formula found here: http://paulbourke.net/geometry/lineline2d/ 67 | var getIntersection = function(line, angle, dontRoundCoords) 68 | { 69 | // Ray line 70 | var px1 = player.x, 71 | py1 = player.y, 72 | px2 = px1 + Math.cos(angle.radians), 73 | py2 = py1 - Math.sin(angle.radians); 74 | 75 | // Some number we need to solve our equation 76 | var f1 = ((line.x2 - line.x1) * (py1 - line.y1) - (line.y2 - line.y1) * (px1 - line.x1)) / 77 | ((line.y2 - line.y1) * (px2 - px1) - (line.x2 - line.x1) * (py2 - py1)); 78 | 79 | // Calculate where the ray intersects with the line 80 | var i = classes.Intersection(); 81 | i.x = px1 + f1 * (px2 - px1), 82 | i.y = py1 + f1 * (py2 - py1); 83 | 84 | // Check if intersection is located on the line 85 | var hit = true, 86 | intersX = i.x, 87 | intersY = i.y, 88 | linex1 = line.x1, 89 | linex2 = line.x2, 90 | liney1 = line.y1, 91 | liney2 = line.y2; 92 | 93 | // When looking for walls we want to round the intersection coordinates before comparing them 94 | // When looking for sprites we dont want those coords rounded, otherwise the result is not exact enough 95 | if (!dontRoundCoords) { 96 | // Round the numbers using bitwise rounding hack 97 | intersX = ~~ (0.5 + i.x); 98 | intersY = ~~ (0.5 + i.y); 99 | } 100 | 101 | hit = (linex1 >= linex2) 102 | ? intersX <= linex1 && intersX >= linex2 103 | : intersX >= linex1 && intersX <= linex2; 104 | if (hit) { 105 | hit = (liney1 >= liney2) 106 | ? intersY <= liney1 && intersY >= liney2 107 | : intersY >= liney1 && intersY <= liney2; 108 | } 109 | 110 | // The formula will also return the intersections that are behind the player 111 | // Only return it if it's located in the correct quadrant 112 | if (!correctQuadrant(i, angle) || !hit) { 113 | return false; 114 | } 115 | 116 | // Calculate distance to the intersection 117 | var deltaX = player.x - i.x, 118 | deltaY = player.y - i.y; 119 | 120 | if (Math.abs(deltaX) > Math.abs(deltaY)) { 121 | i.distance = Math.abs(deltaX / Math.cos(angle.radians)); 122 | } 123 | else { 124 | i.distance = Math.abs(deltaY / Math.sin(angle.radians)); 125 | } 126 | 127 | if (!dontRoundCoords) { 128 | //i.x = intersX; 129 | //i.y = intersY; 130 | } 131 | 132 | return i; 133 | }; 134 | 135 | // Texture mapping routine for walls 136 | // Determines which scanline of the texture to draw for this intersection 137 | var setTextureParams = function(intersection) 138 | { 139 | if (objects.settings.renderTextures()) { 140 | var wall = Raycaster.Objects.Level.walls[intersection.levelObjectId], 141 | length = getHypotenuseLength(wall.x1 - wall.x2, wall.y1 - wall.y2), 142 | lengthToIntersection = getHypotenuseLength(wall.x1 - intersection.x, wall.y1 - intersection.y); 143 | 144 | intersection.resourceIndex = Raycaster.Objects.Level.walls[intersection.levelObjectId].textureId; 145 | 146 | var textureWidth = objects.textures[intersection.resourceIndex].width, 147 | textureHeight = objects.textures[intersection.resourceIndex].height; 148 | 149 | // Wall textures are stretched in height and repeated over the width 150 | if (wall.maxHeight != textureHeight) { 151 | lengthToIntersection *= textureHeight / wall.maxHeight; 152 | } 153 | 154 | intersection.textureX = parseInt(lengthToIntersection % objects.textures[intersection.resourceIndex].width); 155 | } 156 | }; 157 | 158 | /* 159 | // Method: Raycaster.Raycasting.setVSliceDrawParams 160 | // Description: 161 | // Once we know the distance to a wall or sprite, this function calculates the parameters 162 | // that are required to draw the vertical slice for it. 163 | // It also accounts for leaving away pixels of the object if it exceeds the size of the viewport. 164 | // Returns FALSE if the intersection is not visible to the player 165 | // 166 | // Input parameters: 167 | // - intersection: intersection with the object to draw 168 | // 169 | // The following parameters are calculated: 170 | // - dy1: Starting point of the slice on the destination (the screen) 171 | // - dy2: End point of the slice on the destination 172 | // - sy1: Starting point of the slice on the source (the texture image) 173 | // - sy2: End point of the slice on the source 174 | // - texture: Image object containing the texture to draw 175 | */ 176 | var setVSliceDrawParams = function(intersection) 177 | { 178 | var scanlineOffsY = 0, // Additional Y-offset for the scanline (used in sprites) 179 | distance = intersection.distance * fishbowlFixValue, // Distance to the intersection 180 | rindex = intersection.resourceIndex, 181 | lindex = intersection.levelObjectId, 182 | levelObject = intersection.isSprite // Level object definition (wall or sprite) 183 | ? Raycaster.Objects.Level.sprites[lindex] 184 | : Raycaster.Objects.Level.walls[lindex], 185 | texture = intersection.isSprite // Image object containing the texture to draw 186 | ? objects.sprites[rindex] 187 | : objects.textures[rindex], 188 | objHeight = intersection.isSprite // Original height of the object at current intersection 189 | ? texture.height 190 | : getWallHeight(intersection), 191 | objMaxHeight = intersection.isSprite // Maximum possible height of the object (used in walls with angled height) 192 | ? objHeight 193 | : levelObject.maxHeight, 194 | objectZ = intersection.isSprite // Z-position of the object at current intersection 195 | ? levelObject.z 196 | : getWallZ(intersection), 197 | height = Math.floor(objHeight / distance * constants.distanceToViewport); // Height of the object on screen 198 | 199 | // horizonOffset is used for aligning walls and objects correctly on the horizon. 200 | // Without this value, everything would always be vertically centered. 201 | var eyeHeight = player.height * 0.75, 202 | base = (eyeHeight + player.z - objectZ) * 2, 203 | horizonOffset = (height - Math.floor(base / distance * constants.distanceToViewport)) / 2; 204 | 205 | // Determine where to start and end the scanline on the screen 206 | var scanlineEndY = parseInt((constants.screenHeight / 2 - horizonOffset) + height / 2), 207 | scanlineStartY = scanlineEndY - height; 208 | 209 | // Prevent the coordinates from being off-screen 210 | intersection.drawParams = classes.VSliceDrawParams(); 211 | intersection.drawParams.dy1 = scanlineStartY < 0 ? 0 : scanlineStartY; 212 | intersection.drawParams.dy2 = scanlineEndY > constants.screenHeight ? constants.screenHeight : scanlineEndY; 213 | intersection.drawParams.texture = texture; 214 | 215 | if (intersection.drawParams.dy2 < 0 || intersection.drawParams.dy1 > constants.screenHeight) { 216 | return false; 217 | } 218 | 219 | // Now that we've determined the size and location of the scanline, 220 | // we calculate which part of the texture image we need to render onto the scanline 221 | // When part of the object is located outside of the screen we dont need to copy that part of the texture image. 222 | if ((!intersection.isSprite && objects.settings.renderTextures()) 223 | || (intersection.isSprite && objects.settings.renderSprites())) 224 | { 225 | var scale = height / texture.height, // Height ratio of the object compared to its original size 226 | srcStartY = 0, // Start Y coord of source image data 227 | srcEndY = texture.height; // End y coord of source image data 228 | 229 | // Compensate for bottom part being offscreen 230 | if (scanlineEndY > constants.screenHeight) { 231 | var remove = (scanlineEndY - constants.screenHeight) / scale; 232 | srcEndY -= remove; 233 | } 234 | 235 | // Compensate for top part being offscreen 236 | if (scanlineStartY < 0) { 237 | var remove = Math.abs(scanlineStartY) / scale; 238 | srcStartY += remove; 239 | } 240 | 241 | // Prevent the texture from appearing skewed when wall height is angled 242 | // This is bugged: 243 | /*if (objMaxHeight > objHeight) { 244 | var maxHeight = Math.floor(objMaxHeight / distance * constants.distanceToViewport), 245 | diff = Math.abs(maxHeight - height), 246 | scale = maxHeight / texture.height; 247 | 248 | srcStartY += Math.floor(diff / scale); 249 | }*/ 250 | 251 | intersection.drawParams.sy1 = srcStartY; 252 | intersection.drawParams.sy2 = srcEndY; 253 | 254 | if (intersection.drawParams.sy2 <= intersection.drawParams.sy1) { 255 | return false; 256 | } 257 | } 258 | 259 | return true; 260 | } 261 | 262 | // Walls have a start and end height 263 | // This method calculates the height of a wall at a specific intersection 264 | var getWallHeight = function(intersection) 265 | { 266 | var wall = Raycaster.Objects.Level.walls[intersection.levelObjectId]; 267 | 268 | if (wall.h1 == wall.h2) { 269 | return wall.h1; 270 | } 271 | 272 | var length = getHypotenuseLength(wall.x1 - wall.x2, wall.y1 - wall.y2), 273 | slope = (wall.h2 - wall.h1) / length, 274 | lengthToIntersection = getHypotenuseLength(wall.x1 - intersection.x, wall.y1 - intersection.y), 275 | height = wall.h1 + (lengthToIntersection * slope); 276 | 277 | return height; 278 | } 279 | 280 | // Walls have a start and end Z position 281 | // This method calculates the Z pos of a wall at a specific intersection 282 | var getWallZ = function(intersection) 283 | { 284 | var wall = Raycaster.Objects.Level.walls[intersection.levelObjectId]; 285 | 286 | if (wall.z1 == wall.z2) { 287 | return wall.z1; 288 | } 289 | 290 | var length = getHypotenuseLength(wall.x1 - wall.x2, wall.y1 - wall.y2), 291 | slope = (wall.z2 - wall.z1) / length, 292 | lengthToIntersection = getHypotenuseLength(wall.x1 - intersection.x, wall.y1 - intersection.y), 293 | z = wall.z1 + (lengthToIntersection * slope); 294 | 295 | return z; 296 | } 297 | 298 | // Find intersection on a specific sprite that is in the players field of view 299 | var findSprite = function(angle, spriteId) 300 | { 301 | // Create a imaginary plane on which the sprite is drawn 302 | // That way we can check for sprites in exactly the same way we check for walls 303 | var planeAngle = new classes.Angle(angle.degrees - 90), 304 | x = Raycaster.Objects.Level.sprites[spriteId].x, 305 | y = Raycaster.Objects.Level.sprites[spriteId].y, 306 | sprite = objects.sprites[Raycaster.Objects.Level.sprites[spriteId].id], 307 | delta = getDeltaXY(planeAngle, (sprite.width - 1) / 2), 308 | plane = classes.Vector(x - delta.x, y + delta.y, 309 | x + delta.x, y - delta.y); 310 | 311 | // Find intersection point on the plane 312 | var intersection = getIntersection(plane, angle, true); 313 | 314 | if (intersection) { 315 | // Determine which scanline of the sprite image to draw for this intersection 316 | var lengthToIntersection = getHypotenuseLength(plane.x1 - intersection.x, plane.y1 - intersection.y); 317 | 318 | intersection.textureX = Math.floor(lengthToIntersection); 319 | intersection.resourceIndex = Raycaster.Objects.Level.sprites[spriteId].id; 320 | intersection.levelObjectId = spriteId; 321 | intersection.isSprite = true; 322 | 323 | // Calculate the drawing parameters for the vertical scanline for this sprite 324 | // If the sprite is not visible for the player the method returns false 325 | if (!setVSliceDrawParams(intersection)) { 326 | return false; 327 | } 328 | } 329 | 330 | return intersection; 331 | }; 332 | 333 | // Find intersection on a specific wall that is in the players field of view 334 | var findWall = function(angle, wallId) 335 | { 336 | // Find intersection point on current wall 337 | var intersection = getIntersection(Raycaster.Objects.Level.walls[wallId], angle); 338 | 339 | if (intersection) { 340 | intersection.levelObjectId = wallId; 341 | setTextureParams(intersection); 342 | 343 | // Calculate the drawing parameters for the vertical scanline for this wall 344 | // If the wall is not visible (elevation is too high or low) the method returns false 345 | if (!setVSliceDrawParams(intersection)) { 346 | return false; 347 | } 348 | } 349 | 350 | return intersection; 351 | }; 352 | 353 | 354 | /****************** / Public methods / *****************/ 355 | // Find intersection for all the walls and sprites that are in viewing range. 356 | // Returns an array of intersection objects, sorted descending by distance 357 | var findObjects = function(angle, vscan) 358 | { 359 | var intersections = new Array(); 360 | 361 | if (vscan) { 362 | setFishbowlFixValue(vscan); 363 | } 364 | 365 | // Find walls 366 | for (var i = 0; i < Raycaster.Objects.Level.walls.length; i++) { 367 | var intersection = findWall(angle, i); 368 | if (intersection) { 369 | intersections[intersections.length] = intersection; 370 | } 371 | } 372 | 373 | // Find sprites 374 | for (var i = 0; i < Raycaster.Objects.Level.sprites.length; i++) { 375 | var intersection = findSprite(angle, i); 376 | if (intersection) { 377 | intersections[intersections.length] = intersection; 378 | } 379 | } 380 | 381 | // Sort the objects by distance so that the once further away are drawn first 382 | intersections.sort(function(i1, i2) { 383 | return i2.distance - i1.distance; 384 | }); 385 | 386 | return intersections; 387 | }; 388 | 389 | // Calculate difference in X and Y for a distance at a specific angle 390 | var getDeltaXY = function(angle, distance) 391 | { 392 | return classes.Point( 393 | Math.cos(angle.radians) * distance, 394 | Math.sin(angle.radians) * distance 395 | ); 396 | } 397 | 398 | // Expose public members 399 | return { 400 | findObjects : findObjects, 401 | getDeltaXY: getDeltaXY, 402 | }; 403 | }(); --------------------------------------------------------------------------------