├── README.md ├── js ├── TouchControl.js ├── map.js ├── node.js ├── player.js ├── render.js ├── save.js └── world.js ├── media ├── crosshair.png ├── logo.png ├── pos.png └── texture.png ├── play.htm ├── screenshot.png └── style └── style.css /README.md: -------------------------------------------------------------------------------- 1 | # Cube Engine 2 | 3 | ![Cube Engine](https://raw.github.com/Nurgak/Cube-engine/master/screenshot.png) 4 | 5 | Cube Engine is an HTML5 3D engine based on canvas, absolutely no OpenGL and thus no 3D acceleration. This is a proof of concept and learning project, but it can be fun to some extent... as long as you have a ludicrously powerful computer. 6 | 7 | ## Demo 8 | Check out the [live demo](http://nurgak.github.com/Cube-engine). 9 | 10 | ## World 11 | The world resembles that of [Minecraft](http://www.minecraft.net/), a popular voxel-type 3D game based on boxes. You can add and remove everything and anything, different types of nodes are available. The borders are only limited by the size of an integer in Javascript, that means the world is generated dynamically as the player visits new areas. There is no vertical limit, but rendering distance is somewhat limited. 12 | 13 | The topography of the world is generated by a pseudo-random function. The underlying system imitates that of Minecraft: the world is made of 16 by 16 by infinity node "chuncks". The pseudo-random function generates a single height value for each chunk and the height values in between are interpolated. Everything under the height value is filled with nodes and everything above is left empty. Depending on the height different types of soil can be found (grass, dirt, sand...). 14 | 15 | Contrary to Minecraft the world renderer uses a 2D perlin-noise generator rather than 3D, there are no ores or tunnels underground, no enemies or mobs, no trees or physics. Since the renderer takes all the processing power the features must be limited. 16 | 17 | A height map and a frames per second graph can be enabled in the menu. 18 | 19 | ## Rendering 20 | 21 | The rendering is a simple painter's algorithm, everything is drawn from back to front, so nodes close to the camera are drawn over the nodes further away. As canvas doesn't have any 3D rendering capabilities the engine uses very basic 3D rendering techniques. Some optimisation techniques like back-face culling, occlusion culling and frustum culling have been implemented. Octrees aren't adapted to this game and scale, chunks are better suited. 22 | 23 | To make things go faster a lower rendering distance can be selected. 24 | 25 | Canvas doesn't support textures, so texture implementation must be made from ground up: an affine texture mapping system is used. Rendering with textures makes the game noticeably slower so a simple renderer, that uses plain colors, is also available. 26 | 27 | Textures are stored in a _png_ file, the texture placement is exactly the same as Minecraft's, so one could use any current Minecraft texture pack with this game. 28 | 29 | ## Saving 30 | One can locally save a game, this stores all nodes in a 3 by 3 chunk (with the player in the middle chunk) in the browsers local memory or in a file. The data can be loaded locally, but not from a file (for security reasons). To load a world from file the program must be run from a server. 31 | 32 | ## Problems 33 | Corner clipping isn't supported, so being too close to a node will lead to its removal and one will be able to see what's on the other side. 34 | 35 | The way Javascript handles floating point numbers makes it difficult to have exact integers, something that the collision system relies on. This was solved by "rounding" the numbers when close to an integer, but it's not a very elegant solution. 36 | 37 | Chrome has some trouble rendering in plain color, specifically it's the `context.fill()` function that doesn't work when called too much it seems. 38 | 39 | Firefox doesn't let the browser lock the pointer unless it's in full screen mode, this program should never run in full screen, unless on a super computer. 40 | 41 | ## Licence 42 | Released under the [WTFPL](http://sam.zoy.org/wtfpl/COPYING). -------------------------------------------------------------------------------- /js/TouchControl.js: -------------------------------------------------------------------------------- 1 | SQUARIFIC = {framework: {}}; 2 | SQUARIFIC.framework.TouchControl = function (elem, settings) { 3 | /* Settings: 4 | { 5 | pretendArrowKeys: boolean, //Should it simulate keypresses of the arrows 6 | } 7 | */ 8 | "use strict"; 9 | var callbackList = [], 10 | self = this, 11 | originalStyle, 12 | originalX = 0, 13 | originalY = 0, 14 | fakeKeyspressed = [], 15 | multiple = 45, 16 | angleKeys = [ 17 | {angle: 0, keyCodes: [39]}, 18 | {angle: 45, keyCodes: [39, 40]}, 19 | {angle: 90, keyCodes: [40]}, 20 | {angle: 135, keyCodes: [40, 37]}, 21 | {angle: 180, keyCodes: [37]}, 22 | {angle: -180, keyCodes: [37]}, 23 | {angle: -135, keyCodes: [37, 38]}, 24 | {angle: -90, keyCodes: [38]}, 25 | {angle: -45, keyCodes: [38, 39]} 26 | ]; //Angle is in degrees and should be a multiple of the var "multiple",x-axis to the right = 0 left = 180, y-axis down = 90 up = -90, one angle can occur multiple times, e.g. {angle: 30, keyCodes: [1]}, {angle: 30, keyCodes: [2]} will fire 1 and 2, {angle: 30, keyCodes: [1, 1]} will fire 1 twice, -180 and 180 should both be added; 27 | if (!settings) { 28 | settings = {}; 29 | } 30 | if (isNaN(settings.mindistance)) { 31 | settings.mindistance = 20; 32 | } 33 | if (isNaN(settings.middleLeft)) { 34 | settings.middleLeft = 0; 35 | } 36 | if (isNaN(settings.middleTop)) { 37 | settings.middleTop = 0; 38 | } 39 | if (!elem) { 40 | throw "Joystick Control: No element provided! Provided:" + elem; 41 | } 42 | settings.pretendArrowKeys = true; //Remove once non-pretend is implemented 43 | this.on = function (name, callback) { 44 | callbackList.sort(function (a, b) {return a.id - b.id;}); //To get a unique id we need the highest id last 45 | if (callbackList.length < 1) { 46 | var next = 0; 47 | } else { 48 | var next = callbackList[callbackList.length - 1].id + 1; 49 | } 50 | callbackList.push({id: next, name: name, cb: callback}); 51 | return next; 52 | }; 53 | this.removeOn = function (id) { 54 | var i; 55 | for (i = 0; i < callbackList.length; i++) { 56 | if (callbackList[i].id === id) { 57 | callbackList.splice(id); 58 | return true; 59 | } 60 | } 61 | return false; 62 | }; 63 | this.cb = function (name, arg) { 64 | var i; 65 | for (i = 0; i < callbackList.length; i++) { 66 | if (callbackList[i].name === name && typeof callbackList[i].cb == "function") { 67 | callbackList[i].cb(arg); 68 | } 69 | } 70 | }; 71 | this.removeNonFakedKeys = function (keys) { 72 | for (var i = 0; i < fakeKeyspressed.length; i++) { 73 | if (!self.inArray(fakeKeyspressed[i], keys)) { 74 | self.cb("pretendKeyup", {keyCode: fakeKeyspressed[i]}); 75 | } 76 | } 77 | }; 78 | this.inArray = function (el, arr) { 79 | if (!arr) { 80 | return false; 81 | } 82 | for (var i = 0; i < arr.length; i++) { 83 | if (arr[i] === el) { 84 | return true; 85 | } 86 | } 87 | return false; 88 | }; 89 | this.handleTouchStart = function (event) { 90 | if (event.changedTouches[0].target == elem) { 91 | originalStyle = {position: elem.style.position, top: elem.style.top, left: elem.style.left}; 92 | originalX = event.changedTouches[0].clientX; 93 | originalY = event.changedTouches[0].clientY; 94 | elem.style.position = "fixed"; 95 | elem.style.left = event.changedTouches[0].clientX - settings.middleLeft + "px"; 96 | elem.style.top = event.changedTouches[0].clientY - settings.middleTop + "px"; 97 | event.preventDefault(); 98 | } 99 | }; 100 | this.handleTouchStop = function (event) { 101 | if (event.changedTouches[0].target == elem) { 102 | elem.style.position = originalStyle.position; 103 | elem.style.top = originalStyle.top; 104 | elem.style.left = originalStyle.left; 105 | self.removeNonFakedKeys(); 106 | event.preventDefault(); 107 | } 108 | }; 109 | this.handleTouchMove = function (event) { 110 | if (event.changedTouches[0].target == elem) { 111 | var i, k, keys = [], angle, distance, 112 | deltaX = event.changedTouches[0].clientX - originalX, 113 | deltaY = event.changedTouches[0].clientY - originalY; 114 | elem.style.left = event.changedTouches[0].clientX - settings.middleLeft + "px"; 115 | elem.style.top = event.changedTouches[0].clientY - settings.middleTop + "px"; 116 | event.preventDefault(); 117 | distance = Math.sqrt(Math.pow(deltaX, 2) + Math.pow(deltaY, 2)); 118 | if (settings.pretendArrowKeys) { 119 | if (distance < settings.mindistance) { 120 | self.removeNonFakedKeys(); 121 | } else { 122 | angle = multiple * Math.round((Math.atan2(deltaY, deltaX) * 180 / Math.PI) / multiple); 123 | for (i = 0; i < angleKeys.length; i++) { 124 | if (angleKeys[i].angle === angle) { 125 | for (k = 0; k < angleKeys[i].keyCodes.length; k++) { 126 | keys.push(angleKeys[i].keyCodes[k]); 127 | } 128 | } 129 | } 130 | for (i = 0; i < keys.length; i++) { 131 | if (!self.inArray(keys[i], fakeKeyspressed)) { 132 | fakeKeyspressed.push(keys[i]); 133 | } 134 | self.cb("pretendKeydown", {keyCode: keys[i]}); 135 | } 136 | self.removeNonFakedKeys(keys); 137 | } 138 | } else { 139 | //Planned for later 140 | } 141 | } 142 | }; 143 | elem.addEventListener("touchstart", self.handleTouchStart); 144 | elem.addEventListener("touchend", self.handleTouchStop); 145 | elem.addEventListener("touchmove", self.handleTouchMove); 146 | }; 147 | -------------------------------------------------------------------------------- /js/map.js: -------------------------------------------------------------------------------- 1 | /* 2 | * File: map.js 3 | * 4 | * Map class: generates a continous heightmap based on a seed, one chunk at the time. 5 | * 6 | * Author: Karl Kangur 7 | * Licence: WTFPL 2.0 (http://en.wikipedia.org/wiki/WTFPL) 8 | */ 9 | 10 | function Map(seed) 11 | { 12 | this.seed = seed; 13 | this.cache = {}; 14 | } 15 | 16 | // ############################################################ PERLIN NOISE 17 | // http://freespace.virgin.net/hugo.elias/models/m_perlin.htm 18 | 19 | Map.prototype.getAbsoluteHeight = function(x, z) 20 | { 21 | return (10*this.getHeight(x, z)+5)|0; 22 | } 23 | 24 | // returns height value between 0 and 1 25 | Map.prototype.getHeight = function(x, z) 26 | { 27 | var cx = Math.floor(x/16); 28 | var cz = Math.floor(z/16); 29 | 30 | var lx = ((x%16)+16)%16; 31 | var lz = ((z%16)+16)%16; 32 | 33 | if(this.cache[cx+'_'+cz]) 34 | { 35 | return this.cache[cx+'_'+cz][lx][lz]; 36 | } 37 | 38 | this.cache[cx+'_'+cz] = []; 39 | 40 | var corners = this.getCorners(cx, cz); 41 | 42 | var a, b; 43 | for(var x = 0; x < 16; x++) 44 | { 45 | this.cache[cx+'_'+cz][x] = []; 46 | 47 | a = this.interpolate(corners[0], corners[1], x/16); 48 | b = this.interpolate(corners[2], corners[3], x/16); 49 | 50 | for(var z = 0; z < 16; z++) 51 | { 52 | this.cache[cx+'_'+cz][x][z] = this.interpolate(a, b, z/16); 53 | } 54 | } 55 | 56 | return this.cache[cx+'_'+cz][lx][lz]; 57 | } 58 | 59 | // returns corner heigt values for giver chunk 60 | Map.prototype.getCorners = function(cx, cz) 61 | { 62 | // 2-a-3 63 | // z | 64 | // 0-b-1 65 | 66 | return [ 67 | this.noise(cx*16, cz*16), 68 | this.noise(cx*16+16, cz*16), 69 | this.noise(cx*16, cz*16+16), 70 | this.noise(cx*16+16, cz*16+16) 71 | ]; 72 | } 73 | 74 | // returns a value between 0 and 1 75 | Map.prototype.noise = function(x, y) 76 | { 77 | var k = x+y*this.seed; 78 | n = (k<<13)^k; 79 | return ((n*(n*n*60493+19990303)+1376312589)&0x7fffffff)/2147483648; 80 | } 81 | 82 | Map.prototype.interpolate = function(a, b, x) 83 | { 84 | var f = (1.0-Math.cos(x*3.1415927))*0.5; 85 | return a*(1.0-f)+b*f; 86 | } 87 | -------------------------------------------------------------------------------- /js/node.js: -------------------------------------------------------------------------------- 1 | /* 2 | * File: node.js 3 | * 4 | * Contains node class and defines node types. 5 | * 6 | * Author: Karl Kangur 7 | * Licence: WTFPL 2.0 (http://en.wikipedia.org/wiki/WTFPL) 8 | */ 9 | 10 | // coordinates indexes system faces 11 | // front 12 | // 011---111 3-----7 +-----+ 13 | // /| /| /| /| /|top /| 14 | // 010+--110| 2-+---6 | y +-+---+ | right 15 | // |001--|101 | 1---|-5 | z left | +---|-/ 16 | // |/ |/ |/ |/ |/ |/back|/ 17 | // 000---100 0-----4 +----x +-----+ 18 | // bottom 19 | // 20 | // face order: front, back, right, left, top, bottom 21 | 22 | // node face constants 23 | const FACE = { 24 | FRONT: 1, 25 | BACK: 2, 26 | RIGHT: 4, 27 | LEFT: 8, 28 | TOP: 16, 29 | BOTTOM: 32 30 | } 31 | 32 | // rendering vertex order 33 | const VERTEX = { 34 | FRONT: [7, 5, 1, 3], 35 | BACK: [2, 0, 4, 6], 36 | RIGHT: [6, 4, 5, 7], 37 | LEFT: [3, 1, 0, 2], 38 | TOP: [2, 6, 7, 3], 39 | BOTTOM: [0, 1, 5, 4] 40 | } 41 | 42 | // vertex offset from node origin 43 | const OFFSET = [ 44 | {x: 0, y: 0, z: 0}, 45 | {x: 0, y: 0, z: 1}, 46 | {x: 0, y: 1, z: 0}, 47 | {x: 0, y: 1, z: 1}, 48 | {x: 1, y: 0, z: 0}, 49 | {x: 1, y: 0, z: 1}, 50 | {x: 1, y: 1, z: 0}, 51 | {x: 1, y: 1, z: 1} 52 | ]; 53 | 54 | // node definition, private: do not call directly 55 | // only the world can add a new node with addNode(x, y, z, type) 56 | function Node(x, y, z, type) 57 | { 58 | this.x = x; 59 | this.y = y; 60 | this.z = z; 61 | this.type = nodeType[type]; 62 | 63 | // at first all faces are visible 64 | this.sides = 0x3f; // = 0b111111 65 | // bit | face 66 | // ----+-------- 67 | // 0 | front 68 | // 1 | back 69 | // 2 | right 70 | // 3 | left 71 | // 4 | top 72 | // 5 | bottom 73 | } 74 | 75 | // node definitions 76 | const nodeType = {}; 77 | 78 | // restore type to loaded node using type id 79 | nodeType.getTypeName = function(id) 80 | { 81 | for(var key in nodeType) 82 | { 83 | if(typeof nodeType[key] == "object" && nodeType[key].id == id) 84 | { 85 | return key; 86 | } 87 | } 88 | } 89 | 90 | // ids are used to save data locally or to file 91 | nodeType.stone = { 92 | id: 1, 93 | color: '#828282', 94 | texture: function(face) 95 | { 96 | return [1, 0]; 97 | }, 98 | transparent: false, 99 | solid: true 100 | } 101 | 102 | nodeType.grass = { 103 | id: 2, 104 | color: '#749317', 105 | texture: function(face) 106 | { 107 | if(face == FACE.TOP) 108 | { 109 | return [0, 0]; 110 | } 111 | else if(face != FACE.BOTTOM) 112 | { 113 | return [3, 0]; 114 | } 115 | else 116 | { 117 | return [2, 0]; 118 | } 119 | }, 120 | transparent: false, 121 | solid: true 122 | } 123 | 124 | nodeType.dirt = { 125 | id: 3, 126 | color: '#703A00', 127 | texture: function(face) 128 | { 129 | return [2, 0]; 130 | }, 131 | transparent: false, 132 | solid: true 133 | } 134 | 135 | nodeType.cobblestone = { 136 | id: 4, 137 | color: '#787878', 138 | texture: function(face) 139 | { 140 | return [0, 1]; 141 | }, 142 | transparent: false, 143 | solid: true 144 | } 145 | 146 | nodeType.planks = { 147 | id: 5, 148 | color: '#E08907', 149 | texture: function(face) 150 | { 151 | return [4, 0]; 152 | }, 153 | transparent: false, 154 | solid: true 155 | } 156 | 157 | nodeType.wood = { 158 | id: 6, 159 | color: '#642E00', 160 | texture: function(face) 161 | { 162 | if(face < FACE.TOP) 163 | { 164 | return [4, 1]; 165 | } 166 | else 167 | { 168 | return [5, 1]; 169 | } 170 | }, 171 | transparent: false, 172 | solid: true 173 | } 174 | 175 | nodeType.bricks = { 176 | id: 7, 177 | color: '#D14B1B', 178 | texture: function(face) 179 | { 180 | return [7, 0]; 181 | }, 182 | transparent: false, 183 | solid: true 184 | } 185 | 186 | nodeType.gravel = { 187 | id: 8, 188 | color: '#5E6469', 189 | texture: function(face) 190 | { 191 | return [3, 1]; 192 | }, 193 | transparent: false, 194 | solid: true 195 | } 196 | 197 | 198 | nodeType.sand = { 199 | id: 9, 200 | color: '#FCE781', 201 | texture: function(face) 202 | { 203 | return [2, 1]; 204 | }, 205 | transparent: false, 206 | solid: true 207 | } 208 | 209 | nodeType.sandstone = { 210 | id: 10, 211 | color: '#FFEE88', 212 | texture: function(face) 213 | { 214 | if(face == FACE.TOP) 215 | { 216 | return [0, 11]; 217 | } 218 | else if(face != FACE.BOTTOM) 219 | { 220 | return [0, 12]; 221 | } 222 | else 223 | { 224 | return [0, 13]; 225 | } 226 | }, 227 | transparent: false, 228 | solid: true 229 | } 230 | 231 | nodeType.obsidian = { 232 | id: 11, 233 | color: '#2C202F', 234 | texture: function(face) 235 | { 236 | return [5, 2]; 237 | }, 238 | transparent: false, 239 | solid: true 240 | } 241 | 242 | nodeType.glass = { 243 | id: 12, 244 | color: '#A3D8FF', 245 | texture: function(face) 246 | { 247 | return [1, 3]; 248 | }, 249 | transparent: true, 250 | solid: true 251 | } 252 | 253 | nodeType.leaves = { 254 | id: 13, 255 | color: '#029700', 256 | texture: function(face) 257 | { 258 | return [4, 3]; 259 | }, 260 | transparent: true, 261 | solid: true 262 | } 263 | 264 | nodeType.black = { 265 | id: 14, 266 | color: '#262626', 267 | texture: function(face) 268 | { 269 | return [1, 7]; 270 | }, 271 | transparent: false, 272 | solid: true 273 | } 274 | 275 | nodeType.red = { 276 | id: 15, 277 | color: '#BE3600', 278 | texture: function(face) 279 | { 280 | return [1, 8]; 281 | }, 282 | transparent: false, 283 | solid: true 284 | } 285 | 286 | nodeType.rose = { 287 | id: 16, 288 | color: '#FC79AE', 289 | texture: function(face) 290 | { 291 | return [2, 8]; 292 | }, 293 | transparent: false, 294 | solid: true 295 | } 296 | 297 | nodeType.orange = { 298 | id: 17, 299 | color: '#C67B00', 300 | texture: function(face) 301 | { 302 | return [2, 13]; 303 | }, 304 | transparent: false, 305 | solid: true 306 | } 307 | 308 | nodeType.yellow = { 309 | id: 18, 310 | color: '#C1C600', 311 | texture: function(face) 312 | { 313 | return [2, 10]; 314 | }, 315 | transparent: false, 316 | solid: true 317 | } 318 | 319 | nodeType.white = { 320 | id: 19, 321 | color: '#E2E2E2', 322 | texture: function(face) 323 | { 324 | return [0, 4]; 325 | }, 326 | transparent: false, 327 | solid: true 328 | } 329 | 330 | nodeType.green = { 331 | id: 20, 332 | color: '#0E7000', 333 | texture: function(face) 334 | { 335 | return [1, 9]; 336 | }, 337 | transparent: false, 338 | solid: true 339 | } 340 | 341 | nodeType.cyan = { 342 | id: 21, 343 | color: '#00D0CC', 344 | texture: function(face) 345 | { 346 | return [1, 13]; 347 | }, 348 | transparent: false, 349 | solid: true 350 | } 351 | 352 | nodeType.blue = { 353 | id: 22, 354 | color: '#085CD0', 355 | texture: function(face) 356 | { 357 | return [1, 11]; 358 | }, 359 | transparent: false, 360 | solid: true 361 | } 362 | 363 | nodeType.purple = { 364 | id: 23, 365 | color: '#6E0FD3', 366 | texture: function(face) 367 | { 368 | return [1, 12]; 369 | }, 370 | transparent: false, 371 | solid: true 372 | } 373 | 374 | nodeType.water = { 375 | id: 24, 376 | color: '#5E65E1', 377 | texture: function(face) 378 | { 379 | return [14, 12]; 380 | }, 381 | transparent: true, 382 | solid: false 383 | } 384 | 385 | nodeType.lava = { 386 | id: 25, 387 | color: '#FF1408', 388 | texture: function(face) 389 | { 390 | return [14, 15]; 391 | }, 392 | transparent: false, 393 | solid: true 394 | } 395 | 396 | nodeType.darkgray = { 397 | id: 26, 398 | color: '#4B4B4B', 399 | texture: function(face) 400 | { 401 | return [2, 7]; 402 | }, 403 | transparent: false, 404 | solid: true 405 | } 406 | 407 | nodeType.gray = { 408 | id: 27, 409 | color: '#949494', 410 | texture: function(face) 411 | { 412 | return [1, 14]; 413 | }, 414 | transparent: false, 415 | solid: true 416 | } 417 | 418 | nodeType.lime = { 419 | id: 28, 420 | color: '#68D000', 421 | texture: function(face) 422 | { 423 | return [2, 9]; 424 | }, 425 | transparent: false, 426 | solid: true 427 | } 428 | 429 | nodeType.brown = { 430 | id: 29, 431 | color: '#6C360A', 432 | texture: function(face) 433 | { 434 | return [1, 10]; 435 | }, 436 | transparent: false, 437 | solid: true 438 | } 439 | 440 | nodeType.lightblue = { 441 | id: 30, 442 | color: '#4E9EFF', 443 | texture: function(face) 444 | { 445 | return [2, 11]; 446 | }, 447 | transparent: false, 448 | solid: true 449 | } 450 | 451 | nodeType.magneta = { 452 | id: 31, 453 | color: '#B956FF', 454 | texture: function(face) 455 | { 456 | return [2, 12]; 457 | }, 458 | transparent: false, 459 | solid: true 460 | } 461 | 462 | nodeType.ice = { 463 | id: 32, 464 | color: '#7CC8FF', 465 | texture: function(face) 466 | { 467 | return [3, 4]; 468 | }, 469 | transparent: true, 470 | solid: true 471 | } 472 | 473 | nodeType.snow = { 474 | id: 33, 475 | color: '#F5F9FB', 476 | texture: function(face) 477 | { 478 | return [2, 4]; 479 | }, 480 | transparent: false, 481 | solid: true 482 | } 483 | 484 | nodeType.stoneblock = { 485 | id: 34, 486 | color: '#9E9E9E', 487 | texture: function(face) 488 | { 489 | if(face < FACE.TOP) 490 | { 491 | return [5, 0]; 492 | } 493 | else 494 | { 495 | return [6, 0]; 496 | } 497 | }, 498 | transparent: false, 499 | solid: true 500 | } 501 | 502 | nodeType.cobweb = { 503 | id: 35, 504 | color: '#FFFFFF', 505 | texture: function(face) 506 | { 507 | return [11, 0]; 508 | }, 509 | transparent: true, 510 | solid: true 511 | } 512 | 513 | nodeType.bedrock = { 514 | id: 36, 515 | color: '#222222', 516 | texture: function(face) 517 | { 518 | return [1, 1]; 519 | }, 520 | transparent: false, 521 | solid: true 522 | } -------------------------------------------------------------------------------- /js/player.js: -------------------------------------------------------------------------------- 1 | /* 2 | * File: player.js 3 | * 4 | * Defines player viewpoint for rendering and does collision detection. 5 | * 6 | * Author: Karl Kangur 7 | * Licence: WTFPL 2.0 (http://en.wikipedia.org/wiki/WTFPL) 8 | */ 9 | 10 | function Player(world) 11 | { 12 | this.world = world; 13 | 14 | // player position is limited by Number.MAX_VALUE (1.798e308) and Number.MIN_VALUE (5e-324) 15 | this.position = this.world.spawn; 16 | this.rotation = {x: 0, y: 0, z: 0}; 17 | this.chunk = {x: 0, z: 0}; 18 | 19 | // player bounding box used for collision detection 20 | // +------+ 21 | // /| /| 22 | // +-+----+ | 23 | // | | | | height 24 | // | | | | 25 | // | +----| | 26 | // |/ |/ 2*size 27 | // +------+ 28 | // 2*size 29 | 30 | this.delta = {x: 0, y: 0, z: 0}; 31 | this.height = 1.7; 32 | this.size = 0.3; 33 | this.speed = 5; 34 | this.rSpeed = 2.5; 35 | this.velocity = 0; 36 | this.fallSpeed = 8; 37 | this.jumpSpeed = 8; 38 | this.acceleration = 21; 39 | this.gravity = true; 40 | this.collision = true; 41 | this.firstUpdate = true; 42 | this.lastUpdate = new Date().getTime(); 43 | this.rotationMatrix = []; 44 | this.keys = {}; 45 | this.collisionNodes = []; 46 | 47 | var player = this; 48 | document.onkeydown = function(event) 49 | { 50 | player.onKeyEvent(event.keyCode, true); 51 | } 52 | 53 | document.onkeyup = function(event) 54 | { 55 | player.onKeyEvent(event.keyCode, false); 56 | } 57 | this.joystick = new SQUARIFIC.framework.TouchControl(document.getElementById("joystick"), {pretendArrowKeys: true, mindistance: 25, middletop: 25, middleleft: 25}); 58 | this.joystick.on("pretendKeydown", 59 | function (event) { 60 | player.onKeyEvent(event.keyCode, true); 61 | } 62 | ); 63 | this.joystick.on("pretendKeyup", 64 | function (event) { 65 | player.onKeyEvent(event.keyCode, false); 66 | } 67 | ); 68 | 69 | this.spawn(); 70 | } 71 | 72 | // taken from Overv's WebCraft: https://github.com/Overv/WebCraft 73 | Player.prototype.onKeyEvent = function(keyCode, state) 74 | { 75 | var key = String.fromCharCode(keyCode).toLowerCase(); 76 | this.keys[key] = state; 77 | this.keys[keyCode] = state; 78 | } 79 | 80 | Player.prototype.spawn = function() 81 | { 82 | this.position = this.world.spawn; 83 | this.rotation = {x: 0, y: 0, z: 0}; 84 | this.chunk = { 85 | x: Math.floor(this.world.spawn.x/16), 86 | z: Math.floor(this.world.spawn.z/16) 87 | }; 88 | this.world.mapGrid9(this.chunk.x, this.chunk.z); 89 | } 90 | 91 | // ############################################################ PLAYER MOVEMENT 92 | 93 | Player.prototype.update = function() 94 | { 95 | this.elapsed = (new Date().getTime()-this.lastUpdate)/1000; 96 | this.lastUpdate = new Date().getTime(); 97 | 98 | // player rotation 99 | 100 | // [left arrow] 101 | if(this.keys[37]) 102 | { 103 | this.rotation.y += this.rSpeed*this.elapsed; 104 | } 105 | // [right arrow] 106 | if(this.keys[39]) 107 | { 108 | this.rotation.y -= this.rSpeed*this.elapsed; 109 | } 110 | // [up arrow] or [down arrow] 111 | if(this.keys[38] || this.keys[40]) 112 | { 113 | // limit pitch 114 | dy = (this.keys[38]?1:-1)*this.elapsed*this.rSpeed; 115 | 116 | if(this.rotation.x+dy <= 1.5708 && this.rotation.x+dy >= -1.5708) 117 | { 118 | this.rotation.x += dy; 119 | } 120 | else if(this.rotation.x+dy > 1.5708 || this.rotation.x+dy < -1.5708) 121 | { 122 | this.rotation.x = (dy>0?1:-1)*1.5708; 123 | } 124 | } 125 | 126 | // update rotation trigonometry 127 | this.rotTrig = { 128 | cosx: Math.cos(this.rotation.x), 129 | sinx: Math.sin(this.rotation.x), 130 | cosy: Math.cos(this.rotation.y), 131 | siny: Math.sin(this.rotation.y) 132 | }; 133 | 134 | // player movement based on player rotation 135 | var dx = this.speed*this.elapsed*this.rotTrig.siny; 136 | var dy = this.speed*this.elapsed; 137 | var dz = this.speed*this.elapsed*this.rotTrig.cosy; 138 | 139 | // reset movement delta 140 | this.delta.x = 0 141 | this.delta.z = 0; 142 | 143 | // without gravity stop falling 144 | if(!this.gravity) 145 | { 146 | this.delta.y = 0; 147 | this.velocity = 0; 148 | } 149 | 150 | // get player movement deltas with key input 151 | if(this.keys['w']) 152 | { 153 | this.delta.x -= dx; 154 | this.delta.z += dz; 155 | } 156 | if(this.keys['s']) 157 | { 158 | this.delta.x += dx; 159 | this.delta.z -= dz; 160 | } 161 | if(this.keys['d']) 162 | { 163 | this.delta.x += dz; 164 | this.delta.z += dx; 165 | } 166 | if(this.keys['a']) 167 | { 168 | this.delta.x -= dz; 169 | this.delta.z -= dx; 170 | } 171 | // [space] 172 | if(this.keys[32] && this.gravity && !this.delta.y) 173 | { 174 | this.velocity = this.jumpSpeed; 175 | } 176 | // [pg up] 177 | if(this.keys[33] && !this.gravity) 178 | { 179 | this.delta.y += dy; 180 | } 181 | // [pg down] 182 | if(this.keys[34] && !this.gravity) 183 | { 184 | this.delta.y -= dy; 185 | } 186 | 187 | // gravity and terminal velocity 188 | if(this.gravity && this.velocity > -this.fallSpeed) 189 | { 190 | this.velocity -= this.acceleration * this.elapsed; 191 | } 192 | if(this.gravity && this.velocity < -this.fallSpeed) 193 | { 194 | this.velocity = -this.acceleration; 195 | } 196 | 197 | this.delta.y = this.velocity * this.elapsed; 198 | 199 | if (this.firstUpdate) { 200 | // collision detection doesn't seem to work on the first update 201 | this.delta.y = 0; 202 | this.firstUpdate = false; 203 | } 204 | if(this.collision) 205 | { 206 | this.collisionDetection(); 207 | } 208 | 209 | // apply movement 210 | this.position.x += this.delta.x; 211 | this.position.y += this.delta.y; 212 | this.position.z += this.delta.z; 213 | 214 | // check for chunk change 215 | var cx = Math.floor(this.position.x/16); 216 | var cz = Math.floor(this.position.z/16); 217 | 218 | // update map if player has changed chunks 219 | if(cx != this.chunk.x || cz != this.chunk.z) 220 | { 221 | this.chunk.x = cx; 222 | this.chunk.z = cz; 223 | this.world.mapGrid9(this.chunk.x, this.chunk.z); 224 | } 225 | } 226 | 227 | // ############################################################ COLLISION DETECTION 228 | 229 | Player.prototype.collisionDetection = function() 230 | { 231 | var rPos = { 232 | x: Math.floor(this.position.x), 233 | y: Math.floor(this.position.y), 234 | z: Math.floor(this.position.z) 235 | }; 236 | 237 | // gather potential collision nodes 238 | for(var x = rPos.x-1; x <= rPos.x+1; x++) 239 | { 240 | for(var y = rPos.y-2; y <= rPos.y+1; y++) 241 | { 242 | for(var z = rPos.z-1; z <= rPos.z+1; z++) 243 | { 244 | if((node = this.world.getNode(x, y, z)) && node.type.solid) 245 | { 246 | this.collisionNodes.push(node); 247 | } 248 | } 249 | } 250 | } 251 | 252 | for(var i in this.collisionNodes) 253 | { 254 | var node = this.collisionNodes[i]; 255 | 256 | // collision on x axis 257 | if( 258 | this.delta.x && 259 | this.position.z+this.size > node.z && 260 | this.position.z-this.size-1 < node.z && 261 | this.position.y+this.height+0.2 > node.y && 262 | this.position.y-1 < node.y 263 | ) 264 | { 265 | if(this.position.x+this.size+this.delta.x >= node.x && this.position.x < node.x+0.5) 266 | { 267 | this.delta.x = 0; 268 | this.position.x = node.x-this.size; 269 | } 270 | else if(this.position.x-this.size+this.delta.x <= node.x+1 && this.position.x > node.x+0.5) 271 | { 272 | this.delta.x = 0; 273 | this.position.x = node.x+1+this.size; 274 | } 275 | } 276 | 277 | // collision on z axis 278 | if( 279 | this.delta.z && 280 | this.position.x+this.size > node.x && 281 | this.position.x-this.size-1 < node.x && 282 | this.position.y+this.height+0.2 > node.y && 283 | this.position.y-1 < node.y 284 | ) 285 | { 286 | // player behind the node 287 | if(this.position.z+this.size+this.delta.z >= node.z && this.position.z < node.z+0.5) 288 | { 289 | this.delta.z = 0; 290 | this.position.z = node.z-this.size; 291 | } 292 | // player in front of the node 293 | else if(this.position.z-this.size+this.delta.z <= node.z+1 && this.position.z > node.z+0.5) 294 | { 295 | this.delta.z = 0; 296 | this.position.z = node.z+1+this.size; 297 | } 298 | } 299 | 300 | // collision on y axis 301 | if( 302 | this.position.x+this.size > node.x && 303 | this.position.x-this.size-1 < node.x && 304 | this.position.z+this.size > node.z && 305 | this.position.z-this.size-1 < node.z 306 | ) 307 | { 308 | // hit the ceiling 309 | if(this.position.y+this.height+0.2+this.delta.y >= node.y && this.position.y < node.y) 310 | { 311 | this.delta.y = -0.01; 312 | this.velocity = 0; 313 | this.position.y = node.y-this.height-0.2; 314 | } 315 | 316 | // down on the floor 317 | if(this.position.y+this.delta.y <= node.y+1) 318 | { 319 | this.delta.y = 0; 320 | this.velocity = 0; 321 | this.position.y = node.y+1; 322 | } 323 | } 324 | } 325 | 326 | this.collisionNodes.length = 0; 327 | } 328 | 329 | Player.prototype.nodeCollision = function(node) 330 | { 331 | if( 332 | this.position.x+this.size > node.x && 333 | this.position.x-this.size < node.x+1 && 334 | this.position.z+this.size > node.z && 335 | this.position.z-this.size < node.z+1 && 336 | this.position.y+0.2 > node.y && 337 | this.position.y < node.y+1 338 | ) 339 | { 340 | return true; 341 | } 342 | return false; 343 | } 344 | -------------------------------------------------------------------------------- /js/render.js: -------------------------------------------------------------------------------- 1 | /* 2 | * File: render.js 3 | * 4 | * Rendering class renders world to canvas according to player's viewport. 5 | * 6 | * Author: Karl Kangur 7 | * Licence: WTFPL 2.0 (http://en.wikipedia.org/wiki/WTFPL) 8 | */ 9 | 10 | // render chunks inside the world 11 | function Renderer(canvas, world, player) 12 | { 13 | // local references 14 | this.canvas = canvas; 15 | this.world = world; 16 | this.player = player; 17 | this.camera = false; 18 | 19 | this.context = this.canvas.getContext("2d"); 20 | this.vertex = {}; 21 | 22 | // define canvas size according to window size 23 | this.canvas.width = window.innerWidth-200; 24 | this.canvas.height = window.innerHeight; 25 | 26 | // half canvas size for math later on 27 | this.w2 = (this.canvas.width/2)|0; 28 | this.h2 = (this.canvas.height/2)|0; 29 | 30 | this.focalLength = 500; 31 | 32 | // render distance, the higher the slower 33 | this.nodeRenderDist = 100; 34 | this.chunkRenderDist = 420; 35 | this.workingFace = false; 36 | this.workingNode = false; 37 | this.renderNodes = []; 38 | this.chunkCount = 0; 39 | this.nodeCount = 0; 40 | this.faceCount = 0; 41 | this.vertexCount = 0; 42 | // default starting render mode (0: plain color, 1: textured) 43 | this.renderMode = 1; 44 | this.graph = false; 45 | this.map = false; 46 | this.hud = true; 47 | this.mouselock = false; 48 | this.fps = 0; 49 | this.frames = 0; 50 | this.time = new Date().getTime(); 51 | this.frustrum = []; 52 | this.lowResChunks = []; 53 | 54 | // normal look at vector in 3 and 2 dimention 55 | this.n3d = {} 56 | this.n2d = {} 57 | 58 | // define texture (takes a couple of milliseconds to load) 59 | this.texture = new Image(); 60 | this.texture.src = "media/texture.png"; 61 | this.textureSize; 62 | this.texture.onload = function(){ 63 | renderer.textureSize = this.width/16; 64 | } 65 | 66 | this.crosshair = new Image(); 67 | this.crosshair.src = "media/crosshair.png"; 68 | 69 | // mouse click interface 70 | this.mouseClick = false; 71 | this.clickedNode = false; 72 | this.clickedFace = false; 73 | 74 | // make parent reference 75 | var renderer = this; 76 | this.canvas.onmousedown = function(event) 77 | { 78 | if(renderer.mouselock) 79 | { 80 | // when mouse is locked the click is always at the origin 81 | renderer.mouseClick = { 82 | x: 0, 83 | y: 0, 84 | button: event.button 85 | }; 86 | } 87 | else 88 | { 89 | renderer.mouseClick = { 90 | x: event.pageX-renderer.w2, 91 | y: event.pageY-renderer.h2, 92 | button: event.button 93 | }; 94 | } 95 | } 96 | 97 | // update canvas size 98 | window.onresize = function() 99 | { 100 | renderer.canvas.width = window.innerWidth-200; 101 | renderer.canvas.height = window.innerHeight; 102 | renderer.w2 = (renderer.canvas.width/2)|0; 103 | renderer.h2 = (renderer.canvas.height/2)|0; 104 | } 105 | 106 | // disable right click menu 107 | this.canvas.oncontextmenu = function(event) 108 | { 109 | return false; 110 | } 111 | 112 | // canvas always in focus 113 | this.canvas.onblur = function() 114 | { 115 | this.focus(); 116 | } 117 | this.canvas.focus(); 118 | 119 | // needed for mouse lock 120 | document.renderer = this; 121 | 122 | this.render(); 123 | } 124 | 125 | window.requestFrame = (function() 126 | { 127 | return window.requestAnimationFrame || 128 | window.webkitRequestAnimationFrame || 129 | window.mozRequestAnimationFrame || 130 | window.oRequestAnimationFrame || 131 | window.msRequestAnimationFrame || 132 | function(callback, element) 133 | { 134 | window.setTimeout(callback, 10); 135 | } 136 | })(); 137 | 138 | Renderer.prototype.lockPointer = function() 139 | { 140 | // detect feature 141 | if( 142 | !('pointerLockElement' in document) && 143 | !('mozPointerLockElement' in document) && 144 | !('webkitPointerLockElement' in document) 145 | ) 146 | { 147 | alert("Pointer lock unavailable in this browser."); 148 | return; 149 | } 150 | 151 | // firefox can only lock pointer when in full screen mode, this "program" should never run in full screen 152 | if('mozPointerLockElement' in document) 153 | { 154 | alert("Firefox needs full screen to lock mouse. Use Chrome for the time being."); 155 | return; 156 | } 157 | 158 | // when mouse us locked/unlocked callback 159 | document.addEventListener('pointerlockchange', this.mouseLockChangeCallback, false); 160 | document.addEventListener('mozpointerlockchange', this.mouseLockChangeCallback, false); 161 | document.addEventListener('mozpointerchange', this.mouseLockChangeCallback, false); 162 | document.addEventListener('webkitpointerlockchange', this.mouseLockChangeCallback, false); 163 | 164 | this.canvas.requestPointerLock = this.canvas.requestPointerLock || this.canvas.mozRequestPointerLock || this.canvas.webkitRequestPointerLock; 165 | // actually lock the mouse 166 | this.canvas.requestPointerLock(); 167 | } 168 | 169 | // called when mouse lock state changes 170 | Renderer.prototype.mouseLockChangeCallback = function() 171 | { 172 | // called from document 173 | if( 174 | document.pointerLockElement === this.renderer.canvas || 175 | document.mozPointerLockElement === this.renderer.canvas || 176 | document.webkitPointerLockElement === this.renderer.canvas 177 | ) 178 | { 179 | document.addEventListener('mousemove', this.renderer.mouseMoveCallback, false); 180 | this.renderer.mouselock = true; 181 | } 182 | else 183 | { 184 | document.removeEventListener('mousemove', this.renderer.mouseMoveCallback, false); 185 | this.renderer.mouselock = false; 186 | } 187 | } 188 | 189 | // mouse move event callback 190 | Renderer.prototype.mouseMoveCallback = function(event) 191 | { 192 | var movementX = event.movementX || event.mozMovementX || event.webkitMovementX || 0; 193 | var movementY = event.movementY || event.mozMovementY || event.webkitMovementY || 0; 194 | // this is document here 195 | this.renderer.player.rotation.x -= movementY/100; 196 | this.renderer.player.rotation.y -= movementX/100; 197 | } 198 | 199 | Renderer.prototype.changeRenderDist = function(value) 200 | { 201 | this.nodeRenderDist = parseInt(value); 202 | this.chunkRenderDist = parseInt(value)+320; 203 | } 204 | 205 | // prerender canvas element (http://kaioa.com/node/103) 206 | Renderer.prototype.prerender = function(width, height, renderFunction) 207 | { 208 | var buffer = document.createElement("canvas"); 209 | buffer.width = width; 210 | buffer.height = height; 211 | renderFunction(buffer.getContext('2d')); 212 | return buffer; 213 | } 214 | 215 | // ############################################################ FRUSTRUM 216 | 217 | Renderer.prototype.getFrustrumPlanes = function() 218 | { 219 | // http://en.wikipedia.org/wiki/Rotation_matrix 220 | 221 | // vectors corners system planes 222 | // vy|/n3d 0-----1 y +--0--+ 223 | // --+-> vx | | --+->x 3 1 224 | // | 3-----2 | +--2--+ 225 | 226 | // get the player x axis rotation unit vector 227 | var vx = { 228 | x: this.n2d.z, 229 | //y: 0, 230 | z: -this.n2d.x 231 | }; 232 | 233 | // get the player y axis rotation unit vector (cross product with vx.y == 0) 234 | var vy = { 235 | x: this.n3d.y*vx.z, 236 | y: this.n3d.z*vx.x-this.n3d.x*vx.z, 237 | z: -this.n3d.y*vx.x 238 | }; 239 | 240 | var vectors = [ 241 | { 242 | x: this.n3d.x*this.focalLength-vx.x*this.w2+vy.x*this.h2, 243 | y: this.n3d.y*this.focalLength+vy.y*this.h2, 244 | z: this.n3d.z*this.focalLength-vx.z*this.w2+vy.z*this.h2 245 | }, 246 | { 247 | x: this.n3d.x*this.focalLength+vx.x*this.w2+vy.x*this.h2, 248 | y: this.n3d.y*this.focalLength+vy.y*this.h2, 249 | z: this.n3d.z*this.focalLength+vx.z*this.w2+vy.z*this.h2 250 | }, 251 | { 252 | x: this.n3d.x*this.focalLength+vx.x*this.w2-vy.x*this.h2, 253 | y: this.n3d.y*this.focalLength-vy.y*this.h2, 254 | z: this.n3d.z*this.focalLength+vx.z*this.w2-vy.z*this.h2 255 | }, 256 | { 257 | x: this.n3d.x*this.focalLength-vx.x*this.w2-vy.x*this.h2, 258 | y: this.n3d.y*this.focalLength-vy.y*this.h2, 259 | z: this.n3d.z*this.focalLength-vx.z*this.w2-vy.z*this.h2 260 | } 261 | ]; 262 | 263 | var v1, v2, length; 264 | for(var i=0; i<4; i++) 265 | { 266 | // get planes with a cross product 267 | v1 = vectors[i]; 268 | v2 = vectors[(i+1)%4]; 269 | 270 | this.frustrum[i] = { 271 | x: v1.y*v2.z-v1.z*v2.y, 272 | y: v1.z*v2.x-v1.x*v2.z, 273 | z: v1.x*v2.y-v1.y*v2.x 274 | } 275 | 276 | // normalize vector (same length for all) 277 | if(!length) 278 | { 279 | length = 1/Math.sqrt(this.frustrum[i].x*this.frustrum[i].x+this.frustrum[i].y*this.frustrum[i].y+this.frustrum[i].z*this.frustrum[i].z); 280 | } 281 | 282 | this.frustrum[i].x *= length; 283 | this.frustrum[i].y *= length; 284 | this.frustrum[i].z *= length; 285 | } 286 | } 287 | 288 | // ############################################################ WORLD RENDERING 289 | 290 | Renderer.prototype.render = function() 291 | { 292 | this.player.update(); 293 | 294 | // 3d look-at vector 295 | this.n3d = { 296 | x: -this.player.rotTrig.cosx*this.player.rotTrig.siny, 297 | y: this.player.rotTrig.sinx, 298 | z: this.player.rotTrig.cosy*this.player.rotTrig.cosx 299 | }; 300 | 301 | // 2d look-at vector (XZ plane) 302 | this.n2d = { 303 | x: -this.player.rotTrig.siny, 304 | z: this.player.rotTrig.cosy 305 | }; 306 | 307 | this.camera = { 308 | x: this.player.position.x, 309 | y: this.player.position.y+this.player.height, 310 | z: this.player.position.z 311 | } 312 | 313 | this.chunkCount = 0; 314 | this.nodeCount = 0; 315 | this.faceCount = 0; 316 | this.vertexCount = 0; 317 | 318 | // reset vertices 319 | this.vertex = {}; 320 | 321 | this.context.clearRect(0, 0, this.canvas.width, this.canvas.height); 322 | 323 | this.getFrustrumPlanes(); 324 | 325 | // empty renderable node array from last render and low resolution chunks 326 | this.renderNodes = []; 327 | //this.lowResChunks = []; 328 | 329 | // relative chunk position 330 | var rcp, distance; 331 | for(var i in this.world.chunks) 332 | { 333 | rcp = { 334 | x: this.world.chunks[i].x*16+8-this.camera.x, 335 | z: this.world.chunks[i].z*16+8-this.camera.z 336 | }; 337 | 338 | // chunk is behind player (bounding cylinder radius: sqrt(8^2) = 11.31+margin = 13) 339 | if(this.n2d.x*rcp.x+this.n2d.z*rcp.z < -13) 340 | { 341 | continue; 342 | } 343 | 344 | // chunk too far 345 | /*distance = rcp.x*rcp.x+rcp.z*rcp.z; 346 | if(distance > this.chunkRenderDist) 347 | { 348 | this.lowResChunks.push({ 349 | chunk: this.world.chunks[i], 350 | distance: distance 351 | }); 352 | continue; 353 | }*/ 354 | 355 | // get renderable nodes from each chunk inside this.renderNodes 356 | this.getChunkNodes(this.world.chunks[i]); 357 | } 358 | 359 | // first fog layer from furthest nodes 360 | var fogDistance = 50; 361 | 362 | // render low resolution chunks according to their distance to player 363 | /*this.lowResChunks.sort(function(a, b) 364 | { 365 | return b.distance-a.distance; 366 | }); 367 | 368 | for(var i in this.lowResChunks) 369 | { 370 | this.renderLowResChunk(this.lowResChunks[i].chunk); 371 | fogDistance = this.fogLayer(fogDistance, this.lowResChunks[i].distance); 372 | }*/ 373 | 374 | // render nodes according to their distance to player 375 | this.renderNodes.sort(function(a, b) 376 | { 377 | return b.distance-a.distance; 378 | }); 379 | 380 | for(var i in this.renderNodes) 381 | { 382 | this.renderNode(this.renderNodes[i].node); 383 | fogDistance = this.fogLayer(fogDistance, this.renderNodes[i].distance); 384 | } 385 | 386 | // mouse interface 387 | if(this.mouseClick) 388 | { 389 | // left click = add new node 390 | if(this.clickedNode && this.mouseClick.button == 0) 391 | { 392 | var selectedType = document.getElementById("type").value; 393 | 394 | var newNode = {x: this.clickedNode.x, y: this.clickedNode.y, z: this.clickedNode.z}; 395 | 396 | switch(this.clickedFace) 397 | { 398 | case FACE.FRONT: newNode.z++; break; 399 | case FACE.BACK: newNode.z--; break; 400 | case FACE.RIGHT: newNode.x++; break; 401 | case FACE.LEFT: newNode.x--; break; 402 | case FACE.TOP: newNode.y++; break; 403 | case FACE.BOTTOM: newNode.y--; break; 404 | } 405 | 406 | if(!this.player.nodeCollision(newNode)) 407 | { 408 | // get node type from DOM 409 | this.world.addNode(newNode.x, newNode.y, newNode.z, selectedType); 410 | } 411 | } 412 | // right click = remove node 413 | else if(this.clickedNode && this.mouseClick.button == 2) 414 | { 415 | this.world.removeNode(this.clickedNode); 416 | } 417 | this.clickedNode = false; 418 | this.clickedFace = false; 419 | this.mouseClick = false; 420 | } 421 | 422 | if(this.mouselock) 423 | { 424 | // render crosshair 425 | this.context.drawImage(this.crosshair, this.w2-8, this.h2-8); 426 | } 427 | 428 | if(this.hud) 429 | { 430 | this.displayHud(); 431 | } 432 | 433 | if(this.graph) 434 | { 435 | this.displayPerformanceGraph(); 436 | } 437 | 438 | if(this.map) 439 | { 440 | this.displayHeightMap(); 441 | } 442 | 443 | // frames per second counter 444 | if(new Date().getTime()-this.time >= 1000) 445 | { 446 | this.fps = this.frames; 447 | this.frames = 0; 448 | this.time = new Date().getTime(); 449 | } 450 | this.frames++; 451 | 452 | window.requestFrame(this.render.bind(this)); 453 | } 454 | 455 | // pseudo fog filter renders a semi-transparent gray square over everything 456 | Renderer.prototype.fogLayer = function(fogDistance, currentDistance) 457 | { 458 | if(fogDistance < 80 && currentDistance < this.nodeRenderDist-fogDistance) 459 | { 460 | this.context.globalAlpha = 0.5; 461 | this.context.fillStyle = "#eeeeee"; 462 | this.context.beginPath(); 463 | this.context.fillRect(0, 0, this.canvas.width, this.canvas.height); 464 | this.context.closePath(); 465 | this.context.fill(); 466 | this.context.globalAlpha = 1; 467 | 468 | // next fog layer at 469 | return fogDistance+20; 470 | } 471 | 472 | return fogDistance; 473 | } 474 | 475 | Renderer.prototype.renderLowResChunk = function(chunk) 476 | { 477 | // you read it, now complete it! 478 | return; 479 | } 480 | 481 | Renderer.prototype.getChunkNodes = function(chunk) 482 | { 483 | // relative node position 484 | var rnp, distance; 485 | 486 | addSortNode: 487 | for(var i in chunk.renderNodes) 488 | { 489 | rnp = { 490 | x: chunk.renderNodes[i].x+0.5-this.camera.x, 491 | y: chunk.renderNodes[i].y+0.5-this.camera.y, 492 | z: chunk.renderNodes[i].z+0.5-this.camera.z 493 | }; 494 | 495 | distance = rnp.x*rnp.x+rnp.y*rnp.y+rnp.z*rnp.z; 496 | 497 | // node is too far or behind player (bounding sphere radius: sqrt(3*(0.5)^2) = 0.866) 498 | if(distance > this.nodeRenderDist || this.n3d.x*rnp.x+this.n3d.y*rnp.y+this.n3d.z*rnp.z < -0.866) 499 | { 500 | continue; 501 | } 502 | 503 | // check node bounding sphere against all 4 frustrum planes 504 | for(var j = 0; j < 4; j++) 505 | { 506 | if(this.frustrum[j].x*rnp.x+this.frustrum[j].y*rnp.y+this.frustrum[j].z*rnp.z > 0.866) 507 | { 508 | continue addSortNode; 509 | } 510 | } 511 | 512 | this.nodeCount++; 513 | 514 | this.renderNodes.push({ 515 | node: chunk.renderNodes[i], 516 | distance: distance 517 | }); 518 | } 519 | } 520 | 521 | Renderer.prototype.renderNode = function(node) 522 | { 523 | this.workingNode = node; 524 | 525 | // translate 526 | this.rx = node.x-this.camera.x; 527 | this.ry = node.y-this.camera.y; 528 | this.rz = node.z-this.camera.z; 529 | 530 | // projected points array 531 | this.rp = []; 532 | 533 | // node 534 | // 3-----7 535 | // /| /| 536 | // 2-+---6 | 537 | // | 1---|-5 538 | // |/ |/ 539 | // 0-----4 540 | 541 | // draw visible faces 542 | if(node.sides & FACE.FRONT && node.z+1 < this.camera.z) 543 | { 544 | this.workingFace = FACE.FRONT; 545 | this.drawRect(VERTEX.FRONT); 546 | } 547 | 548 | if(node.sides & FACE.BACK && node.z > this.camera.z) 549 | { 550 | this.workingFace = FACE.BACK; 551 | this.drawRect(VERTEX.BACK); 552 | } 553 | 554 | if(node.sides & FACE.RIGHT && node.x+1 < this.camera.x) 555 | { 556 | this.workingFace = FACE.RIGHT; 557 | this.drawRect(VERTEX.RIGHT); 558 | } 559 | 560 | if(node.sides & FACE.LEFT && node.x > this.camera.x) 561 | { 562 | this.workingFace = FACE.LEFT; 563 | this.drawRect(VERTEX.LEFT); 564 | } 565 | 566 | if(node.sides & FACE.TOP && node.y+1 < this.camera.y) 567 | { 568 | this.workingFace = FACE.TOP; 569 | this.drawRect(VERTEX.TOP); 570 | } 571 | 572 | if(node.sides & FACE.BOTTOM && node.y > this.camera.y) 573 | { 574 | this.workingFace = FACE.BOTTOM; 575 | this.drawRect(VERTEX.BOTTOM); 576 | } 577 | } 578 | 579 | Renderer.prototype.drawRect = function(p) 580 | { 581 | // process each vertex 582 | var index, x, y, z, xx, yy, zz; 583 | 584 | var offset = OFFSET; 585 | 586 | for(var i = 0; i < 4; i++) 587 | { 588 | index = (this.workingNode.x+offset[p[i]].x)+'_'+(this.workingNode.y+offset[p[i]].y)+'_'+(this.workingNode.z+offset[p[i]].z); 589 | 590 | // vertex already processed for this frame 591 | if((this.rp[p[i]] = this.vertex[index]) !== undefined) 592 | { 593 | continue; 594 | } 595 | 596 | // translate vertices 597 | x = this.rx+offset[p[i]].x; 598 | y = this.ry+offset[p[i]].y; 599 | z = this.rz+offset[p[i]].z; 600 | 601 | // clip point if behind player 602 | if(x*this.n3d.x+y*this.n3d.y+z*this.n3d.z < 0) 603 | { 604 | this.rp[p[i]] = false; 605 | this.vertex[index] = false; 606 | continue; 607 | } 608 | 609 | // rotatate vertices (http://en.wikipedia.org/wiki/Rotation_matrix) 610 | xx = this.player.rotTrig.cosy*x+this.player.rotTrig.siny*z; 611 | yy = this.player.rotTrig.sinx*this.player.rotTrig.siny*x+this.player.rotTrig.cosx*y-this.player.rotTrig.sinx*this.player.rotTrig.cosy*z; 612 | zz = -this.player.rotTrig.siny*this.player.rotTrig.cosx*x+this.player.rotTrig.sinx*y+this.player.rotTrig.cosx*this.player.rotTrig.cosy*z; 613 | 614 | // project 3d point to 2d screen (http://en.wikipedia.org/wiki/3D_projection) 615 | zz = this.focalLength/zz; 616 | 617 | // save relative point 618 | this.rp[p[i]] = {x: xx*zz, y: -yy*zz}; 619 | 620 | // save processed vertex 621 | this.vertex[index] = this.rp[p[i]]; 622 | this.vertexCount++; 623 | } 624 | 625 | // corner clipping to viewport 626 | /*if( 627 | (!this.rp[p[0]] || (this.rp[p[0]].x < -this.w2 || this.rp[p[0]].x > this.w2) || (this.rp[p[0]].y < -this.h2 || this.rp[p[0]].y > this.h2)) && 628 | (!this.rp[p[1]] || (this.rp[p[1]].x < -this.w2 || this.rp[p[1]].x > this.w2) || (this.rp[p[1]].y < -this.h2 || this.rp[p[1]].y > this.h2)) && 629 | (!this.rp[p[2]] || (this.rp[p[2]].x < -this.w2 || this.rp[p[2]].x > this.w2) || (this.rp[p[2]].y < -this.h2 || this.rp[p[2]].y > this.h2)) && 630 | (!this.rp[p[3]] || (this.rp[p[3]].x < -this.w2 || this.rp[p[3]].x > this.w2) || (this.rp[p[3]].y < -this.h2 || this.rp[p[3]].y > this.h2)) 631 | ) 632 | { 633 | return; 634 | }*/ 635 | 636 | // check for click (http://paulbourke.net/geometry/insidepoly/) 637 | if(this.mouseClick) 638 | { 639 | if( 640 | (this.mouseClick.y-this.rp[p[0]].y)*(this.rp[p[1]].x-this.rp[p[0]].x)-(this.mouseClick.x-this.rp[p[0]].x)*(this.rp[p[1]].y-this.rp[p[0]].y) < 0 && 641 | (this.mouseClick.y-this.rp[p[2]].y)*(this.rp[p[3]].x-this.rp[p[2]].x)-(this.mouseClick.x-this.rp[p[2]].x)*(this.rp[p[3]].y-this.rp[p[2]].y) < 0 && 642 | (this.mouseClick.y-this.rp[p[1]].y)*(this.rp[p[2]].x-this.rp[p[1]].x)-(this.mouseClick.x-this.rp[p[1]].x)*(this.rp[p[2]].y-this.rp[p[1]].y) < 0 && 643 | (this.mouseClick.y-this.rp[p[3]].y)*(this.rp[p[0]].x-this.rp[p[3]].x)-(this.mouseClick.x-this.rp[p[3]].x)*(this.rp[p[0]].y-this.rp[p[3]].y) < 0 644 | ) 645 | { 646 | this.clickedNode = this.workingNode; 647 | this.clickedFace = this.workingFace; 648 | } 649 | } 650 | 651 | if(this.renderMode == 0) 652 | { 653 | this.drawMonochrome(p); 654 | } 655 | else if(this.renderMode == 1) 656 | { 657 | this.drawTextured(p); 658 | } 659 | 660 | this.faceCount++; 661 | } 662 | 663 | Renderer.prototype.drawMonochrome = function(p) 664 | { 665 | var points = []; 666 | 667 | if(this.rp[p[0]]) 668 | { 669 | points.push(this.rp[p[0]]); 670 | } 671 | if(this.rp[p[1]]) 672 | { 673 | points.push(this.rp[p[1]]); 674 | } 675 | if(this.rp[p[2]]) 676 | { 677 | points.push(this.rp[p[2]]); 678 | } 679 | if(this.rp[p[3]]) 680 | { 681 | points.push(this.rp[p[3]]); 682 | } 683 | 684 | if(points.length > 1) 685 | { 686 | // reset drawing settings 687 | this.context.strokeStyle = "#000000"; 688 | this.context.lineWidth = 1; 689 | this.context.fillStyle = this.workingNode.type.color; 690 | 691 | // set transparency 692 | if(this.workingNode.type.transparent) 693 | { 694 | this.context.globalAlpha = 0.5; 695 | } 696 | 697 | // start drawing polygon 698 | this.context.beginPath(); 699 | 700 | // move to first point 701 | this.context.moveTo(points[0].x+this.w2, points[0].y+this.h2); 702 | for(var i = 1; i < points.length; i++) 703 | { 704 | this.context.lineTo(points[i].x+this.w2, points[i].y+this.h2); 705 | } 706 | // line back to first point (not needed) 707 | //this.context.lineTo(points[0].x+this.w2, points[0].y+this.h2); 708 | 709 | this.context.closePath(); 710 | 711 | // fill doesn't work properly in Chrome 20.0.1132.57 712 | this.context.fill(); 713 | this.context.stroke(); 714 | this.context.globalAlpha = 1; 715 | } 716 | } 717 | 718 | Renderer.prototype.drawTextured = function(p) 719 | { 720 | // affine texture mapping code by Andrea Griffini (http://stackoverflow.com/a/4774298/176269) 721 | var texture = this.workingNode.type.texture(this.workingFace); 722 | 723 | // 0---3 texture map corner order 724 | // | | 725 | // 1---2 726 | 727 | var size = this.textureSize; 728 | var pts = [ 729 | {x: this.rp[p[0]].x, y: this.rp[p[0]].y, u: size*texture[0], v: size*texture[1]}, 730 | {x: this.rp[p[1]].x, y: this.rp[p[1]].y, u: size*texture[0], v: size*texture[1]+size}, 731 | {x: this.rp[p[2]].x, y: this.rp[p[2]].y, u: size*texture[0]+size, v: size*texture[1]+size}, 732 | {x: this.rp[p[3]].x, y: this.rp[p[3]].y, u: size*texture[0]+size, v: size*texture[1]} 733 | ]; 734 | 735 | // triangle subdivision 736 | var tris = []; 737 | 738 | if(this.rp[p[0]] && this.rp[p[1]] && this.rp[p[2]]) 739 | { 740 | tris.push([0, 1, 2]); 741 | } 742 | else if(this.rp[p[1]] && this.rp[p[2]] && this.rp[p[3]]) 743 | { 744 | tris.push([1, 2, 3]); 745 | } 746 | 747 | if(this.rp[p[2]] && this.rp[p[3]] && this.rp[p[0]]) 748 | { 749 | tris.push([2, 3, 0]); 750 | } 751 | else if(this.rp[p[0]] && this.rp[p[1]] && this.rp[p[3]]) 752 | { 753 | tris.push([0, 1, 3]); 754 | } 755 | 756 | for(var t=0; t= 1000) 847 | { 848 | this.graph.time = new Date().getTime(); 849 | 850 | // data stack 851 | if(this.graph.fps.length > this.graph.dataPoints) 852 | { 853 | this.graph.fps.shift(); 854 | } 855 | this.graph.fps.push(this.fps); 856 | 857 | // prerender graph 858 | var graph = this.graph; 859 | this.graph.image = this.prerender(this.graph.width, this.graph.height+20, function(ctx) 860 | { 861 | ctx.drawImage(graph.base, 0, 0); 862 | 863 | // draw fps line and text 864 | ctx.strokeStyle = "#000000"; 865 | ctx.lineWidth = 2; 866 | ctx.fillStyle = "#000000"; 867 | ctx.textBaseline = "bottom"; 868 | ctx.textAlign = "right"; 869 | ctx.font = "10px sans-serif"; 870 | 871 | ctx.beginPath(); 872 | ctx.moveTo(0, graph.height-graph.fps[0]*graph.height/60); 873 | for(var i=1; i 7 | * Licence: WTFPL 2.0 (http://en.wikipedia.org/wiki/WTFPL) 8 | */ 9 | 10 | function Save(world, player) 11 | { 12 | this.world = world; 13 | this.player = player; 14 | 15 | this.getSavedWorlds(); 16 | } 17 | 18 | Save.prototype.removeLocalSave = function() 19 | { 20 | window.localStorage.removeItem(document.getElementById("load").value); 21 | this.getSavedWorlds(); 22 | } 23 | 24 | // populate saved games select input 25 | Save.prototype.getSavedWorlds = function() 26 | { 27 | var options = []; 28 | for(var key in window.localStorage) 29 | { 30 | if(key.indexOf("world") === 0) 31 | { 32 | options.push(''); 33 | } 34 | } 35 | options.sort(); 36 | document.getElementById("load").innerHTML = options.join(""); 37 | } 38 | 39 | // ############################################################ SAVE METHODS 40 | 41 | Save.prototype.saveLocally = function() 42 | { 43 | try 44 | { 45 | window.localStorage.setItem(document.getElementById("saveas").value, this.getSaveData()); 46 | } 47 | catch(e) 48 | { 49 | switch(e.code) 50 | { 51 | // data wasn't successfully saved due to quota exceed 52 | case 22: alert("Could not save world: not enough space."); break; 53 | default: alert("Could not save world. Error code "+e.code); 54 | } 55 | } 56 | 57 | this.getSavedWorlds(); 58 | } 59 | 60 | Save.prototype.saveToFile = function() 61 | { 62 | document.location = "data:text/octet-stream,"+this.getSaveData(); 63 | } 64 | 65 | Save.prototype.getSaveData = function() 66 | { 67 | var chunk_x = Math.floor(this.player.position.x/16); 68 | var chunk_z = Math.floor(this.player.position.z/16); 69 | 70 | var saveNodes = []; 71 | for(var i in this.world.chunks) 72 | { 73 | if(Math.abs(this.world.chunks[i].x-chunk_x) <= 1 && Math.abs(this.world.chunks[i].z-chunk_z) <= 1) 74 | { 75 | for(var j in this.world.chunks[i].nodes) 76 | { 77 | saveNodes.push({ 78 | x: this.world.chunks[i].nodes[j].x, 79 | y: this.world.chunks[i].nodes[j].y, 80 | z: this.world.chunks[i].nodes[j].z, 81 | t: this.world.chunks[i].nodes[j].type.id 82 | }); 83 | } 84 | } 85 | } 86 | 87 | // add all player, spawnpoint and node data 88 | var saveData = { 89 | player: { 90 | x: this.player.position.x.toFixed(2), 91 | y: this.player.position.y.toFixed(2), 92 | z: this.player.position.z.toFixed(2), 93 | rx: this.player.rotation.x.toFixed(2), 94 | ry: this.player.rotation.y.toFixed(2), 95 | rz: this.player.rotation.z.toFixed(2) 96 | }, 97 | spawn: { 98 | x: this.world.spawn.x, 99 | y: this.world.spawn.y, 100 | z: this.world.spawn.z 101 | }, 102 | seed: this.world.map.seed, 103 | nodes: saveNodes 104 | }; 105 | 106 | return JSON.stringify(saveData); 107 | } 108 | 109 | // ############################################################ LOAD METHODS 110 | 111 | Save.prototype.loadLocalSave = function() 112 | { 113 | if(worldName = document.getElementById("load").value) 114 | { 115 | this.loadWorld(window.localStorage.getItem(worldName)); 116 | } 117 | } 118 | 119 | Save.prototype.loadFromFile = function(file) 120 | { 121 | var reader = new FileReader(); 122 | 123 | reader.onload = function(e) 124 | { 125 | this.loadWorld(e.target.result); 126 | } 127 | 128 | reader.onerror = function(e) 129 | { 130 | switch(reader.error.code) 131 | { 132 | // file was uploaded with file:// protocol 133 | case 2: alert("You cannot load files when running locally due to security reasons."); break; 134 | default: alert("Could not load file. Error code: "+reader.error.code); 135 | } 136 | } 137 | 138 | reader.readAsText(file); 139 | } 140 | 141 | Save.prototype.loadWorld = function(worldData) 142 | { 143 | worldData = JSON.parse(worldData); 144 | this.world.chunks = {}; 145 | this.world.seed = parseInt(worldData.seed); 146 | for(var i in worldData.nodes) 147 | { 148 | var node = worldData.nodes[i]; 149 | this.world.addNode(parseInt(node.x), parseInt(node.y), parseInt(node.z), nodeType.getTypeName(parseInt(node.t))); 150 | } 151 | 152 | // restore player position 153 | this.player.position = { 154 | x: parseFloat(worldData.player.x), 155 | y: parseFloat(worldData.player.y), 156 | z: parseFloat(worldData.player.z), 157 | }; 158 | 159 | // restore player rotation 160 | this.player.rotation = { 161 | x: parseFloat(worldData.player.rx), 162 | y: parseFloat(worldData.player.ry), 163 | z: parseFloat(worldData.player.rz), 164 | }; 165 | } -------------------------------------------------------------------------------- /js/world.js: -------------------------------------------------------------------------------- 1 | /* 2 | * File: world.js 3 | * 4 | * Holds world and chunk classes, does map generation (but gets height from map.js). 5 | * Also loading and saving from/to files or in browser. 6 | * 7 | * Author: Karl Kangur 8 | * Licence: WTFPL 2.0 (http://en.wikipedia.org/wiki/WTFPL) 9 | */ 10 | 11 | function World(seed) 12 | { 13 | this.chunks = {}; 14 | this.newMap(seed); 15 | } 16 | 17 | World.prototype.newMap = function(seed) 18 | { 19 | this.chunks = {}; 20 | this.map = new Map(seed); 21 | this.spawn = {x: 0, y: this.map.getAbsoluteHeight(0, 0)+3, z: 0}; 22 | } 23 | 24 | // ############################################################ MAP METHODS 25 | 26 | // generate 9 chunks, the center chunk being at x, z 27 | World.prototype.mapGrid9 = function(x, z) 28 | { 29 | for(var cx = x-1; cx <= x+1; cx++) 30 | { 31 | for(var cz = z-1; cz <= z+1; cz++) 32 | { 33 | // chunk already exists 34 | if(this.chunks[cx+'_'+cz]) 35 | { 36 | continue; 37 | } 38 | 39 | this.addChunk(cx, cz); 40 | } 41 | } 42 | } 43 | 44 | // ############################################################ CHUNK METHODS 45 | 46 | function Chunk(x, z) 47 | { 48 | this.x = x; 49 | this.z = z; 50 | this.nodes = {}; 51 | this.renderNodes = {}; 52 | 53 | var map = new Map(); 54 | 55 | this.corners = [ 56 | map.getAbsoluteHeight(this.x, this.z), 57 | map.getAbsoluteHeight(this.x+16, this.z), 58 | map.getAbsoluteHeight(this.x+16, this.z+16), 59 | map.getAbsoluteHeight(this.x, this.z+16) 60 | ]; 61 | } 62 | 63 | // generates a chunk with all its nodes 64 | World.prototype.addChunk = function(x, z) 65 | { 66 | // generate the map using world heightmap 67 | for(var nx = 0; nx < 16; nx++) 68 | { 69 | for(var nz = 0; nz < 16; nz++) 70 | { 71 | var y = this.map.getAbsoluteHeight(16*x+nx, 16*z+nz); 72 | 73 | for(var ny = 0; ny < y; ny++) 74 | { 75 | var type; 76 | if(ny == 0) 77 | { 78 | type = 'bedrock'; 79 | } 80 | else if(ny*2 < y) 81 | { 82 | type = 'stone'; 83 | } 84 | else if((ny == 4 && ny == y-1) || (ny == 5 && ny == y-1) || (ny == 6 && ny == y-1)) 85 | { 86 | // water level 87 | if(ny == 4 && ny == y-1) 88 | { 89 | this.addNode(16*x+nx, 5, 16*z+nz, 'water'); 90 | this.addNode(16*x+nx, 6, 16*z+nz, 'water'); 91 | } 92 | if(ny == 5 && ny == y-1) 93 | { 94 | this.addNode(16*x+nx, 6, 16*z+nz, 'water'); 95 | } 96 | type = 'sand'; 97 | } 98 | else if(ny < y-1) 99 | { 100 | type = 'dirt'; 101 | } 102 | else 103 | { 104 | type = 'grass'; 105 | } 106 | 107 | this.addNode(16*x+nx, ny, 16*z+nz, type); 108 | } 109 | } 110 | } 111 | } 112 | 113 | // ############################################################ NODE METHODS 114 | 115 | // returns the node at x, y , z, false if there is no chunk or undefined if there is no node 116 | World.prototype.getNode = function(x, y, z) 117 | { 118 | var cx = Math.floor(x/16); 119 | var cz = Math.floor(z/16); 120 | 121 | if(this.chunks[cx+'_'+cz]) 122 | { 123 | return this.chunks[cx+'_'+cz].nodes[x+'_'+y+'_'+z]; 124 | } 125 | 126 | return false; 127 | } 128 | 129 | World.prototype.addNode = function(x, y, z, type) 130 | { 131 | var cx = Math.floor(x/16); 132 | var cz = Math.floor(z/16); 133 | var node; 134 | 135 | // space already occupied 136 | if(this.getNode(x, y, z)) 137 | { 138 | return false; 139 | } 140 | 141 | if(!this.chunks[cx+'_'+cz]) 142 | { 143 | this.chunks[cx+'_'+cz] = new Chunk(cx, cz); 144 | } 145 | 146 | node = new Node(x, y, z, type); 147 | this.chunks[cx+'_'+cz].nodes[x+'_'+y+'_'+z] = node; 148 | this.occludedFaceCulling(node); 149 | } 150 | 151 | World.prototype.removeNode = function(node) 152 | { 153 | node.removed = true; 154 | 155 | this.occludedFaceCulling(node); 156 | 157 | var cx = Math.floor(node.x/16); 158 | var cz = Math.floor(node.z/16); 159 | 160 | delete this.chunks[cx+'_'+cz].renderNodes[node.x+'_'+node.y+'_'+node.z]; 161 | delete this.chunks[cx+'_'+cz].nodes[node.x+'_'+node.y+'_'+node.z]; 162 | } 163 | 164 | World.prototype.occludedFaceCulling = function(node) 165 | { 166 | var faces = [FACE.FRONT, FACE.BACK, FACE.RIGHT, FACE.LEFT, FACE.TOP, FACE.BOTTOM]; 167 | var pos = [[0,0,1], [0,0,-1], [1,0,0], [-1,0,0], [0,1,0], [0,-1,0]]; 168 | var adjn; 169 | 170 | for(var i=0; i<3; i++) 171 | { 172 | // opposite faces 173 | var face1 = i*2; 174 | var face2 = i*2+1; 175 | 176 | // face1: front, right, top faces 177 | adjn = this.getNode(node.x+pos[face1][0], node.y+pos[face1][1], node.z+pos[face1][2]); 178 | 179 | // adjacent node doesn't exist or is a half node 180 | if(!adjn) 181 | { 182 | // add face 183 | node.sides |= faces[face1]; 184 | } 185 | else 186 | { 187 | // one of the nodes is removed 188 | if(node.removed || adjn.removed) 189 | { 190 | node.sides |= faces[face1]; 191 | adjn.sides |= faces[face2]; 192 | } 193 | // add top face 194 | else if(FACE.TOP & (1< 2 | 3 | 4 | Cube Engine v1.0 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 26 | 27 | 28 | 29 | 30 |
31 |

Cube Engine v1.0

32 | 35 |

Controls

36 |
    37 |
  • WASD: Movement
  • 38 |
  • Pg. Up: Up
  • 39 |
  • Pg. Down: Down
  • 40 |
  • Arrows: Rotation
  • 41 |
  • Space: Jump
  • 42 |
43 |

Options

44 |
    45 |
  • HUD:
  • 46 |
  • Gravity:
  • 47 |
  • Collision detection:
  • 48 |
  • Performace graph:
  • 49 |
  • Map:
  • 50 |
  • 51 |
  • 52 |
  • Focal length:
    53 | 54 |
  • 55 |
  • Render distance:
    56 | 57 |
  • 58 |
  • Render mode:
    59 | 63 |
  • 64 |
  • Type:
    65 | 106 |
  • 107 | 108 |
  • Generate world
    109 | Seed 110 | 111 |
  • 112 |
  • Save as
    113 | 120 |
    121 | 122 | 123 |
  • 124 |
  • Load
    125 |
    132 | 133 | 134 |
  • 135 |
  • Load file
    136 | 137 |
    138 |
  • 139 |
140 |
141 | 142 | 143 | 144 | 154 | 155 | 156 | 157 | -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Nurgak/Cube-engine/ba9945a71d13009926b0fa52b8c1b7b8cb20f4ef/screenshot.png -------------------------------------------------------------------------------- /style/style.css: -------------------------------------------------------------------------------- 1 | html, 2 | body 3 | { 4 | width: 100%; 5 | height: 100%; 6 | margin: 0px; 7 | font-family: sans-serif; 8 | font-size: 12px; 9 | } 10 | 11 | input, 12 | select 13 | { 14 | font-family: sans-serif; 15 | font-size: 12px; 16 | } 17 | 18 | a 19 | { 20 | text-decoration: none; 21 | border-bottom: dotted 1px #ffffff; 22 | color: #ffffff; 23 | } 24 | 25 | h1 26 | { 27 | margin: 0; 28 | font-size: 15px; 29 | text-shadow: #888 0px 1px 0; 30 | text-align: center; 31 | background: url(../media/logo.png) no-repeat top center; 32 | padding-top: 1em; 33 | margin: 0.5em 0; 34 | } 35 | 36 | h2 37 | { 38 | margin: 0; 39 | font-size: 12px; 40 | padding-left: 3px; 41 | } 42 | 43 | canvas 44 | { 45 | float: left; 46 | background-color: #7BACFF; 47 | background: -webkit-gradient(linear, 0% 0%, 0% 100%, from(#7BACFF), to(#ffffff)); 48 | background: linear-gradient(#7BACFF, #ffffff); 49 | } 50 | 51 | joystick 52 | { 53 | display:none; 54 | height: 50px; 55 | width: 50px; 56 | border-radius: 50%; 57 | } 58 | 59 | #joystick 60 | { 61 | background-color:red; 62 | position: fixed; 63 | left: 100px; 64 | bottom: 100px; 65 | } 66 | 67 | #controls 68 | { 69 | position: absolute; 70 | right: 0; 71 | top: 0; 72 | bottom: 0; 73 | background: #222; 74 | width: 200px; 75 | color: #fff; 76 | overflow-x: hidden; 77 | overflow-y: auto; 78 | } 79 | 80 | form 81 | { 82 | padding: 0; 83 | margin: 0; 84 | } 85 | 86 | ul 87 | { 88 | padding-left: 2em; 89 | } 90 | --------------------------------------------------------------------------------