1 /** 2 * Renderer object. 3 * 4 * @author Andrew J. Baker 5 */ 6 7 /** @namespace Steppe */ 8 var Steppe = (function(Steppe) { 9 /** @class */ 10 Steppe.Renderer = function(canvas) { 11 var _CANVAS_WIDTH = 320, // 320 pixels 12 _CANVAS_HEIGHT = 200, // 200 pixels 13 _ANGLE_OF_VIEW = 60, // 60 degrees 14 _ONE_DEGREE_ANGLE = 1 / _ANGLE_OF_VIEW * _CANVAS_WIDTH, 15 _THIRTY_DEGREE_ANGLE = _ONE_DEGREE_ANGLE * 30, 16 _THREE_HUNDRED_AND_SIXTY_DEGREE_ANGLE = _ONE_DEGREE_ANGLE * 360, 17 _ANGULAR_INCREMENT = _ANGLE_OF_VIEW / _CANVAS_WIDTH, 18 _DEGREES_TO_RADIANS = Math.PI / 180, 19 _FAKE_DEGREES_TO_RADIANS = (2 * Math.PI) / 20 ((360 / _ANGLE_OF_VIEW) * _CANVAS_WIDTH), 21 _RADIANS_TO_DEGREES = 180 / Math.PI, 22 _RADIANS_TO_FAKE_DEGREES = ((360 / _ANGLE_OF_VIEW) * 23 _CANVAS_WIDTH) / (2 * Math.PI), 24 _SCALE_FACTOR = 35, 25 _CAMERA_Y = 200, 26 _DISTANCE = 75, 27 _MAXIMUM_ROW = _CANVAS_HEIGHT + _CANVAS_HEIGHT / 2 - 1, 28 _WATER_HEIGHT = 64; 29 30 var _FASTEST = 4, 31 _DONT_CARE = 2, 32 _NICEST = 1; 33 34 var _camera = { angle: 0, x: 0, y: _CAMERA_Y, z: 0 }, 35 _cosineLookupTable = [], 36 _fogColor = 0x7f7f7fff, 37 _framebuffer, 38 _heightmap = [], 39 _inverseDistortionLookupTable = [], 40 _outOfBoundsHeightmap = [], 41 _outOfBoundsTexturemap, 42 _rayLengthLookupTable = [], 43 _sineLookupTable = [], 44 _sky, 45 _skyData, 46 _spriteList = [], 47 _temporaryFramebuffer, 48 _texturemap, 49 _visibleSpriteList = [], 50 _zBuffer = []; 51 52 var _fog = false, // disabled (default) 53 _quality = _DONT_CARE, // medium quality (default) 54 _smooth = 0, // disabled (default) 55 _waterHeight = -1; // disabled (default) 56 57 /** 58 * Blend two colours together using an alpha value. 59 * 60 * @param {number} firstColor First, or source, colour (RGBA). 61 * @param {number} secondColor Second, or destination, colour (RGBA). 62 * @param {number} alpha Alpha value in the range 0..255. 63 * @return {number} Mixed colour (RGBA). 64 */ 65 var _alphaBlend = function(firstColor, secondColor, alpha) { 66 if (alpha < 0) { 67 alpha = 0; 68 } else if (alpha > 255) { 69 alpha = 255; 70 } 71 72 var normalisedAlpha = alpha / 255, 73 adjustedAlpha = 1 - normalisedAlpha; 74 75 var mixedRed = ((firstColor >> 24) & 0xff) * normalisedAlpha | 0, 76 mixedGreen = ((firstColor >> 16) & 0xff) * normalisedAlpha | 0, 77 mixedBlue = ((firstColor >> 8) & 0xff) * normalisedAlpha | 0; 78 79 mixedRed += Math.floor(((secondColor >> 24) & 0xff) * 80 adjustedAlpha); 81 mixedGreen += Math.floor(((secondColor >> 16) & 0xff) * 82 adjustedAlpha); 83 mixedBlue += Math.floor(((secondColor >> 8) & 0xff) * 84 adjustedAlpha); 85 86 return (mixedRed << 24) | 87 (mixedGreen << 16) | 88 (mixedBlue << 8) | 0xff; 89 }; 90 91 /** 92 * Get a pixel from the out-of-bounds texturemap. 93 * 94 * @param {number} x The x-coordinate; must be in the range 95 * 0..out-of-bounds-texturemap-width - 1. 96 * @param {number} y The y-coordinate; must be in the range 97 * 0..out-of-bounds-texturemap-height - 1. 98 * @return {number} An integer composed of RGBA components for the 99 * corresponding pixel. 100 */ 101 var _getPixelFromOutOfBoundsTexturemap = function(x, y) { 102 if (typeof _outOfBoundsTexturemap !== 'undefined') { 103 var index = (y << 12) + (x << 2); 104 105 return (_outOfBoundsTexturemap[index] << 24) | 106 (_outOfBoundsTexturemap[index + 1] << 16) | 107 (_outOfBoundsTexturemap[index + 2] << 8) | 0xff; 108 } else { 109 return 0x7f7f7fff; 110 } 111 }; 112 113 /** 114 * Get a pixel from the sky canvas. 115 * 116 * @param {number} x The x-coordinate; should be in the range 117 * 0..sky-width - 1. 118 * @param {number} y The y-coordinate; should be in the range 119 * 0..sky-height - 1. 120 * @return {number} An integer composed of RGBA components for the 121 * corresponding pixel. 122 */ 123 var _getPixelFromSky = function(x, y) { 124 var currentAngle = _camera.angle - _THIRTY_DEGREE_ANGLE; 125 126 if (currentAngle < 0) { 127 currentAngle += _THREE_HUNDRED_AND_SIXTY_DEGREE_ANGLE; 128 } 129 130 if (y < 0) { 131 y = 0; 132 } else if (y >= 100) { 133 y = 100 - 1; 134 } 135 136 var index = (y * 1920 + (currentAngle + x | 0) % 1920) * 4; 137 138 return (_skyData[index] << 24) | 139 (_skyData[index + 1] << 16) | 140 (_skyData[index + 2] << 8) | 0xff; 141 }; 142 143 /** 144 * Get a pixel from the texturemap. 145 * 146 * @param {number} x The x-coordinate; must be in the range 147 * 0..texturemap-width - 1. 148 * @param {number} y The y-coordinate; must be in the range 149 * 0..texturemap-width - 1. 150 * @return {number} An integer composed of RGBA components for the 151 * corresponding pixel. 152 */ 153 var _getPixelFromTexturemap = function(x, y) { 154 if (typeof _outOfBoundsTexturemap !== 'undefined') { 155 var index = (y << 12) + (x << 2); 156 157 return (_texturemap[index] << 24) | 158 (_texturemap[index + 1] << 16) | 159 (_texturemap[index + 2] << 8) | 0xff; 160 } else { 161 return 0xffffffff; 162 } 163 }; 164 165 /** 166 * Get the row at which a sprite should be rendered. This is a private 167 * helper method. 168 * 169 * @param {number} x ... 170 * @param {number} z ... 171 * @param {number} ray ... 172 * @return {number} ... 173 */ 174 var _getRow = function(x, z, ray) { 175 var cameraVectorX = Math.cos(_camera.angle * _ANGULAR_INCREMENT * 176 _DEGREES_TO_RADIANS); 177 var cameraVectorZ = Math.sin(_camera.angle * _ANGULAR_INCREMENT * 178 _DEGREES_TO_RADIANS); 179 180 var spriteVectorX = x - _camera.x; 181 var spriteVectorZ = z - _camera.z; 182 183 var vectorLength = Math.sqrt(spriteVectorX * spriteVectorX + 184 spriteVectorZ * spriteVectorZ); 185 186 var newVectorLength = vectorLength / 187 _inverseDistortionLookupTable[ray]; 188 189 var y = Math.round(_DISTANCE * _camera.y / newVectorLength); 190 var row = y + _CANVAS_HEIGHT - 1 - _camera.y; 191 192 return row; 193 }; 194 195 /** 196 * Initialise the inverse distortion lookup table (for removing 197 * fisheye). 198 */ 199 var _initInverseDistortionLookupTable = function() { 200 for (var angleOfRotation = 0; 201 angleOfRotation < _THIRTY_DEGREE_ANGLE; 202 ++angleOfRotation) { 203 var angleOfRotationInRadians = angleOfRotation * 204 _ANGULAR_INCREMENT * _DEGREES_TO_RADIANS; 205 206 _inverseDistortionLookupTable[angleOfRotation + 207 _THIRTY_DEGREE_ANGLE] = 1 / 208 Math.cos(angleOfRotationInRadians); 209 210 var cosine = Math.cos(angleOfRotationInRadians); 211 if (cosine !== 0) { 212 _inverseDistortionLookupTable[ 213 _THIRTY_DEGREE_ANGLE - angleOfRotation] = 1 / cosine; 214 } 215 } 216 217 _inverseDistortionLookupTable[0] = 2; 218 _inverseDistortionLookupTable[160] = 1; 219 }; 220 221 /** 222 * Initialise (or recalculate) the ray-length lookup table. 223 * 224 * @param {number} distance The distance from the camera to the 225 * projection plane. 226 */ 227 var _initRayLengthLookupTable = function(distance) { 228 for (var y = 200; y <= 300; ++y) { 229 _rayLengthLookupTable[y] = []; 230 231 for (var ray = 1; ray < _CANVAS_WIDTH; ++ray) { 232 for (var row = 0; row <= _MAXIMUM_ROW; ++row) { 233 var invertedRow = _CANVAS_HEIGHT - 1 - row; 234 235 var rayLength = _inverseDistortionLookupTable[ray] * 236 ((distance * y) / (y - invertedRow)); 237 238 _rayLengthLookupTable[y][row * _CANVAS_WIDTH + ray] = 239 rayLength; 240 } 241 } 242 } 243 }; 244 245 /** 246 * Initialise the sine and cosine lookup tables. 247 */ 248 var _initSineAndCosineLookupTables = function() { 249 for (var angleOfRotation = 0; 250 angleOfRotation < _THREE_HUNDRED_AND_SIXTY_DEGREE_ANGLE; 251 ++angleOfRotation) { 252 var angleOfRotationInRadians = angleOfRotation * 253 _ANGULAR_INCREMENT * _DEGREES_TO_RADIANS; 254 255 _sineLookupTable[angleOfRotation] = Math.sin( 256 angleOfRotationInRadians); 257 _cosineLookupTable[angleOfRotation] = Math.cos( 258 angleOfRotationInRadians); 259 } 260 }; 261 262 /** 263 * Render the terrain (landscape). 264 * 265 * @param {number} initialAngle ... 266 */ 267 var _renderFrontToBack = function(initialAngle) { 268 var currentAngle = initialAngle; 269 270 var framebufferImageData = _temporaryFramebuffer.createImageData( 271 320, 200); 272 var framebufferData = framebufferImageData.data; 273 274 for (var ray = _quality; ray < _CANVAS_WIDTH; ray += _quality) { 275 var previousTop = _MAXIMUM_ROW; 276 277 for (var row = _MAXIMUM_ROW; row >= 0; --row) { 278 var rayLength = _rayLengthLookupTable[_camera.y][ 279 (row << 8) + (row << 6) + ray]; 280 281 var rayX = _camera.x + rayLength * 282 _cosineLookupTable[currentAngle] | 0, 283 rayZ = _camera.z + rayLength * 284 _sineLookupTable[currentAngle] | 0; 285 286 var u = rayX & 1023; 287 var v = rayZ & 1023; 288 289 var height; 290 if ((rayX < 1024 || rayX >= 1024 + 1024 || 291 rayZ < 1024 || rayZ >= 1024 + 1024) && 292 _outOfBoundsHeightmap.length > 0) { 293 height = _outOfBoundsHeightmap[(v << 10) + u]; 294 } else { 295 height = _heightmap[(v << 10) + u]; 296 } 297 298 var scale = height * _SCALE_FACTOR / (rayLength + 1) | 0; 299 300 var top = (_CANVAS_HEIGHT >> 1) - 301 (_camera.y - _CANVAS_HEIGHT) + row - scale, 302 bottom = top + scale; 303 304 if (top < previousTop) { 305 bottom = previousTop; 306 previousTop = top; 307 308 var color = 0x000000ff; 309 310 var texel; 311 if (rayX < 1024 || rayX >= 1024 + 1024 || 312 rayZ < 1024 || rayZ >= 1024 + 1024) { 313 texel = 314 _getPixelFromOutOfBoundsTexturemap(u, v); 315 316 color = texel; 317 } else { 318 if (height < _waterHeight) { 319 var data = _getPixelFromSky(ray, 320 200 - top); 321 322 texel = 323 _getPixelFromTexturemap(u, v); 324 325 var mixedColor = _alphaBlend(data, 326 texel, (_waterHeight - height) / 327 _waterHeight * 255 * 2 | 0); 328 329 texel = mixedColor; 330 331 height = _waterHeight; 332 333 color = texel; 334 } else { 335 texel = 336 _getPixelFromTexturemap(u, v); 337 338 color = texel; 339 } 340 } 341 342 if (_fog) { 343 var foggedTexel = _alphaBlend(texel, 344 _fogColor, row / 100 * 255 | 0); 345 346 color = foggedTexel; 347 } 348 349 if (bottom > 199) { 350 bottom = 199; 351 } 352 353 var index, i, j; 354 if (ray > _quality) { 355 // Not the left-most ray... 356 index = 357 (top * (framebufferImageData.width << 2)) + 358 (ray << 2); 359 360 var red = (color >> 24) & 0xff, 361 green = (color >> 16) & 0xff, 362 blue = (color >> 8) & 0xff; 363 364 for (j = 0; j < bottom - top + 1; ++j) { 365 for (i = 0; i < _quality; ++i) { 366 framebufferData[index++] = red; 367 framebufferData[index++] = green; 368 framebufferData[index++] = blue; 369 framebufferData[index++] = 0xff; 370 } 371 372 index += (framebufferImageData.width << 2) - 373 (_quality << 2); 374 } 375 } else { 376 // Left-most ray: we don't cast rays for column 0! 377 index = 378 (top * (framebufferImageData.width << 2)) + 379 (ray << 2); 380 381 red = (color >> 24) & 0xff; 382 green = (color >> 16) & 0xff; 383 blue = (color >> 8) & 0xff; 384 385 for (j = 0; j < bottom - top + 1; ++j) { 386 for (i = 0; i < _quality; ++i) { 387 framebufferData[index - 388 (_quality << 2)] = red; 389 framebufferData[index - 390 (_quality << 2) + 1] = green; 391 framebufferData[index - 392 (_quality << 2) + 2] = blue; 393 framebufferData[index - 394 (_quality << 2) + 3] = 395 0xff; 396 397 framebufferData[index++] = red; 398 framebufferData[index++] = green; 399 framebufferData[index++] = blue; 400 framebufferData[index++] = 0xff; 401 } 402 403 index += (framebufferImageData.width << 2) - 404 (_quality << 2); 405 } 406 } 407 } 408 } 409 410 currentAngle += _quality; 411 if (currentAngle >= _THREE_HUNDRED_AND_SIXTY_DEGREE_ANGLE) { 412 currentAngle = 0; 413 } 414 } 415 416 _temporaryFramebuffer.putImageData(framebufferImageData, 0, 0); 417 418 _framebuffer.drawImage(_temporaryFramebuffer.canvas, 419 0, 0 - _smooth, 420 _temporaryFramebuffer.canvas.width, 421 _temporaryFramebuffer.canvas.height); 422 }; 423 424 /** 425 * Render the 360-degree panoramic sky based on the camera's angle 426 * of rotation. 427 */ 428 var _renderSky = function() { 429 var angleOfRotation = _camera.angle - _THIRTY_DEGREE_ANGLE; 430 431 if (angleOfRotation < 0) { 432 angleOfRotation += _THREE_HUNDRED_AND_SIXTY_DEGREE_ANGLE; 433 } 434 435 angleOfRotation |= 0; 436 437 var skyWidth = _sky.width; 438 var skyHeight = (_CANVAS_HEIGHT >> 1) - 439 (_camera.y - _CANVAS_HEIGHT); 440 441 if (skyHeight > _sky.height) { 442 skyHeight = _sky.height; 443 } 444 445 var sy = _camera.y - _CANVAS_HEIGHT; 446 447 if (sy < 0) { 448 sy = 0; 449 } 450 451 if (angleOfRotation + 320 <= skyWidth) { 452 _framebuffer.drawImage(_sky, angleOfRotation, sy, 320, 453 skyHeight, 0, 0, 320, skyHeight); 454 } else { 455 _framebuffer.drawImage(_sky, angleOfRotation, sy, 456 skyWidth - angleOfRotation, skyHeight, 0, 0, 457 skyWidth - angleOfRotation, skyHeight); 458 _framebuffer.drawImage(_sky, 0, sy, 459 320 - (skyWidth - angleOfRotation), 460 skyHeight, skyWidth - angleOfRotation, 0, 461 320 - (skyWidth - angleOfRotation), skyHeight); 462 } 463 464 if (_fog) { 465 var skyContext = _sky.getContext('2d'); 466 467 var skyGradient = skyContext.createLinearGradient(0, 0, 0, 468 skyHeight - 1); 469 skyGradient.addColorStop(0, 'rgba(' + 470 ((_fogColor >> 24) & 0xff) + ', ' + 471 ((_fogColor >> 16) & 0xff) + ', ' + 472 ((_fogColor >> 8) & 0xff) + ', ' + 473 (1 - (1 / (100 / skyHeight))) + ')'); 474 skyGradient.addColorStop(1, 'rgba(' + 475 ((_fogColor >> 24) & 0xff) + ', ' + 476 ((_fogColor >> 16) & 0xff) + ', ' + 477 ((_fogColor >> 8) & 0xff) + ', 1)'); 478 479 _framebuffer.fillStyle = skyGradient; 480 _framebuffer.fillRect(0, 0, 320, skyHeight); 481 } 482 }; 483 484 /** 485 * ... 486 */ 487 var _renderSprites = function() { 488 // For each visible sprite... 489 for (var i = 0; i < _visibleSpriteList.length; ++i) { 490 // If the current sprite has been removed... 491 if (typeof _visibleSpriteList[i] === 'undefined') { 492 // Move to the next sprite. 493 continue; 494 } 495 496 var sprite = _visibleSpriteList[i]; 497 498 // Draw the sprite. 499 _framebuffer.drawImage( 500 sprite.image, 501 sprite.x, 502 sprite.y - _smooth, 503 sprite.width, 504 sprite.height); 505 506 // Remove the sprite from the list of visible sprites. 507 delete _visibleSpriteList[i]; 508 } 509 }; 510 511 /** 512 * Sort the list of currently visible sprites. 513 */ 514 var _sortVisibleSpriteList = function() { 515 var length = _visibleSpriteList.length; 516 517 for (var i = 0; i < length - 1; ++i) { 518 for (var j = i + 1; j < length; ++j) { 519 if (_visibleSpriteList[j].vectorLength > 520 _visibleSpriteList[i].vectorLength) { 521 var temp = _visibleSpriteList[i]; 522 _visibleSpriteList[i] = _visibleSpriteList[j]; 523 _visibleSpriteList[j] = temp; 524 } 525 } 526 } 527 }; 528 529 if (arguments.length > 1) { 530 throw('Too many arguments passed to constructor'); 531 } 532 533 if (canvas.width !== _CANVAS_WIDTH) { 534 throw('Canvas width not equal to ' + _CANVAS_WIDTH); 535 } 536 if (canvas.height !== _CANVAS_HEIGHT) { 537 throw('Canvas height not equal to ' + _CANVAS_HEIGHT); 538 } 539 540 _framebuffer = canvas.getContext('2d'); 541 542 var temporaryFramebufferCanvas = document.createElement('canvas'); 543 temporaryFramebufferCanvas.width = canvas.width; 544 temporaryFramebufferCanvas.height = canvas.height; 545 546 _temporaryFramebuffer = temporaryFramebufferCanvas.getContext('2d'); 547 548 _initSineAndCosineLookupTables(); 549 _initInverseDistortionLookupTable(); 550 _initRayLengthLookupTable(_DISTANCE); 551 552 return { 553 /** 554 * Add a 2D sprite, at the specified world coords, to the sprite 555 * list. 556 * 557 * @param {HTMLImageElement} image The 2D sprite as an image. 558 * @param {number} x The x-coordinate in world space. 559 * @param {number} y The y-coordinate in world space. 560 * @param {number} z The z-coordinate in world space. 561 * @return {Renderer} This (chainable). 562 */ 563 addSprite: function(image, x, y, z) { 564 if ( !(image instanceof HTMLImageElement)) { 565 throw('Invalid image: not an instance of HTMLImageElement'); 566 } 567 if (typeof(x) != 'number') { 568 throw('Invalid x: not a number'); 569 } 570 if (typeof(y) != 'number') { 571 throw('Invalid y: not a number'); 572 } 573 if (typeof(z) != 'number') { 574 throw('Invalid z: not a number'); 575 } 576 577 if (x < 1024 || x >= 1024 + 1024) { 578 throw('Invalid x: must be in the range 1024..2047'); 579 } 580 if (y < 0 || y >= 1024) { 581 throw('Invalid y: must be in the range 0..1023'); 582 } 583 if (z < 1024 || z >= 1024 + 1024) { 584 throw('Invalid z: must be in the range 1024..2047'); 585 } 586 587 _spriteList.push({ 588 image: image, 589 x: x, 590 y: y & 1023, 591 z: z 592 }); 593 594 return this; 595 }, 596 597 /** 598 * Disable a Steppe capability. 599 * 600 * @param {string} capability Specifies a string indicating a 601 * Steppe capability; 'fog', 602 * 'reflection-map' and 'smooth' are 603 * currently implemented. 604 * @return {Renderer} This (chainable). 605 */ 606 disable: function(capability) { 607 if (capability === 'fog') { 608 _fog = false; 609 } else if (capability === 'reflection-map') { 610 _waterHeight = -1; 611 } else if (capability === 'smooth') { 612 _smooth = 0; 613 } else { 614 throw("Can't disable unknown capability"); 615 } 616 617 return this; 618 }, 619 620 /** 621 * Enable a Steppe capability. 622 * 623 * @param {string} capability Specifies a string indicating a 624 * Steppe capability; 'fog', 625 * 'reflection-map' and 'smooth' are 626 * currently implemented. 627 * @return {Renderer} This (chainable). 628 */ 629 enable: function(capability) { 630 if (capability === 'fog') { 631 _fog = true; 632 } else if (capability === 'reflection-map') { 633 _waterHeight = _WATER_HEIGHT; 634 } else if (capability === 'smooth') { 635 _smooth = 0.5; 636 } else { 637 throw("Can't enable unknown capability"); 638 } 639 640 return this; 641 }, 642 643 /** 644 * Get the current camera. 645 * 646 * @return {object} An object composed of an angle-of-rotation (in 647 * degrees about the y-axis) and a 3D point in 648 * world space. 649 */ 650 getCamera: function() { 651 return { 652 angle: _camera.angle / 653 _THREE_HUNDRED_AND_SIXTY_DEGREE_ANGLE * 360 + 654 0.5 | 0, 655 x: _camera.x, 656 y: _camera.y, 657 z: _camera.z 658 }; 659 }, 660 661 /** 662 * Get the height (from the heightmap) of a single unit of terrain, 663 * in world space. 664 * 665 * @param {number} x The x-coordinate of the unit of terrain. 666 * @param {number} z The z-coordinate of the unit of terrain. 667 * @return {number} The corresponding y-coordinate of the specified 668 * unit of terrain. 669 */ 670 getHeight: function(x, z) { 671 if (typeof(x) != 'number') { 672 throw('Invalid x: not a number'); 673 } 674 if (typeof(z) != 'number') { 675 throw('Invalid z: not a number'); 676 } 677 678 if (x < 1024 || x >= 1024 + 1024) { 679 throw('Invalid x: must be in the range 1024..2047'); 680 } 681 if (z < 1024 || z >= 1024 + 1024) { 682 throw('Invalid z: must be in the range 1024..2047'); 683 } 684 685 var u = x & 1023; 686 var v = z & 1023; 687 688 return _heightmap[(v << 10) + u]; 689 }, 690 691 /** 692 * Test whether a capability is enabled. 693 * 694 * @param {string} capability Specifies a string indicating a 695 * Steppe capability; 'smooth' and 696 * 'reflection-map' are currently 697 * implemented. 698 * @return {boolean} Returns true if capability is an enabled 699 * capability and returns false otherwise. 700 */ 701 isEnabled: function(capability) { 702 if (capability === 'fog') { 703 return _fog; 704 } else if (capability === 'reflection-map') { 705 return (_waterHeight > -1); 706 } else if (capability === 'smooth') { 707 return (_smooth === 0.5); 708 } 709 throw('Unknown capability'); 710 }, 711 712 /** 713 * Render the terrain (landscape) including the sky and any visible 714 * sprites. 715 */ 716 render: function() { 717 // Fill the upper region of the framebuffer with the 718 // fog-colour. 719 _framebuffer.fillStyle = '#7f7f7f'; 720 _framebuffer.fillRect(0, 100, 320, 25); 721 722 _renderSky(); 723 724 // Empty the list of visible sprites. 725 _visibleSpriteList.length = 0; 726 727 if (_spriteList.length > 0) { 728 // Calculate the unit vector of the camera. 729 var cameraVectorX = Math.cos(_camera.angle * 730 _ANGULAR_INCREMENT * _DEGREES_TO_RADIANS); 731 var cameraVectorZ = Math.sin(_camera.angle * 732 _ANGULAR_INCREMENT * _DEGREES_TO_RADIANS); 733 734 // For each sprite... 735 for (var i = 0; i < _spriteList.length; ++i) { 736 var sprite = _spriteList[i]; 737 738 // Calculate the vector of the sprite. 739 var spriteVectorX = sprite.x - _camera.x, 740 spriteVectorZ = sprite.z - _camera.z; 741 742 // Calculate the magnitude (length of the vector) to 743 // determine the distance from the camera to the 744 // sprite. 745 var vectorLength = Math.sqrt( 746 spriteVectorX * spriteVectorX + 747 spriteVectorZ * spriteVectorZ); 748 749 // If the distance from the camera to the sprite is 750 // outside the viewing frustum... 751 if (vectorLength > 400) { 752 // Move to the next sprite. 753 continue; 754 } 755 756 // Normalise the sprite vector to become the unit 757 // vector. 758 spriteVectorX /= vectorLength; 759 spriteVectorZ /= vectorLength; 760 761 // Calculate the dot product of the camera and sprite 762 // vectors. 763 var dotProduct = cameraVectorX * spriteVectorX + 764 cameraVectorZ * spriteVectorZ; 765 766 // If the dot product is negative... 767 if (dotProduct < 0) { 768 // The sprite is behind the camera, so clearly not 769 // in view. Move to the next sprite. 770 continue; 771 } 772 773 // Calculate the angle (theta) between the camera vector 774 // and the sprite vector. 775 var theta = Math.acos(dotProduct); 776 777 // If the angle (theta) is less than or equal to 30 778 // degrees... 779 // NOTE: We do NOT need to check the lower bound (-30 780 // degrees) because theta will /never/ be negative. 781 if (theta <= _THIRTY_DEGREE_ANGLE * 782 _FAKE_DEGREES_TO_RADIANS) { 783 var scale = _SCALE_FACTOR / (vectorLength + 1); 784 785 // Scale the projected sprite. 786 var width = scale * sprite.image.width | 0; 787 var height = scale * sprite.image.height | 0; 788 789 // Calculate the cross product. The cross product 790 // differs from the dot product in a crucial way: 791 // the result is *signed*! 792 var crossProduct = cameraVectorX * spriteVectorZ - 793 spriteVectorX * cameraVectorZ; 794 795 // Calculate the projected x coord relative to the 796 // horizontal centre of the canvas. We add or 797 // subtract the value dependent on the sign of the 798 // cross product. 799 var x; 800 if (crossProduct < 0) { 801 x = _CANVAS_WIDTH / 2 - theta * 802 _RADIANS_TO_FAKE_DEGREES | 0; 803 } else { 804 x = _CANVAS_WIDTH / 2 + theta * 805 _RADIANS_TO_FAKE_DEGREES | 0; 806 } 807 808 // Calculate the 3D coords of the sprite. 809 var spriteX = sprite.x, 810 spriteY = _heightmap[((sprite.z & 1023) << 10) + 811 (sprite.x & 1023)], 812 spriteZ = sprite.z; 813 814 var row = _getRow(spriteX, spriteZ, x); 815 816 // Centre the scaled sprite. 817 x -= (width >> 1); 818 819 var rayX = spriteX, 820 rayZ = spriteZ; 821 822 var u = rayX & 1023, 823 v = rayZ & 1023; 824 825 var projectedHeight; 826 if ((rayX < 1024 || rayX >= 1024 + 1024 || 827 rayZ < 1024 || rayZ >= 1024 + 1024) && 828 _outOfBoundsHeightmap.length > 0) { 829 projectedHeight = _outOfBoundsHeightmap[ 830 (v << 10) + u]; 831 } else { 832 projectedHeight = _heightmap[(v << 10) + u]; 833 } 834 835 var projectedScale = projectedHeight * scale; 836 837 var top = (_CANVAS_HEIGHT >> 1) - 838 (_camera.y - _CANVAS_HEIGHT) + row - 839 projectedScale, 840 bottom = top + projectedScale; 841 842 // Add the projected sprite to the list of visible 843 // sprites. 844 _visibleSpriteList.push({ 845 height: height, 846 image: sprite.image, 847 row: bottom | 0, 848 vectorLength: vectorLength, 849 width: width, 850 x: x | 0, 851 y: top - height | 0 852 }); 853 } 854 } 855 } 856 857 _sortVisibleSpriteList(); 858 // for (var k = 0; k < _visibleSpriteList.length; ++k) { 859 // console.log(_visibleSpriteList[k].vectorLength); 860 // } 861 862 var initialAngle = _camera.angle - _THIRTY_DEGREE_ANGLE; 863 864 if (initialAngle < 0) { 865 initialAngle += _THREE_HUNDRED_AND_SIXTY_DEGREE_ANGLE; 866 } 867 868 initialAngle |= 0; 869 870 // var date = new Date(); 871 // var startTime = date.getTime(); 872 873 /* 874 * 1. Construct a list of visible sprites; sprites are visible 875 * where they fall within the -30..30 (60-degree) horizontal 876 * field-of-view based on the direction that the camera is 877 * pointing in. 878 * 2. For each visible sprite, determine its projected 2D 879 * coords (x and y) and its scaled width and height based on 880 * distance from the camera. The y coord corresponds to the 881 * bottom of the sprite and the x coord is the centre of the 882 * width of the sprite. 883 * 3. After drawing each row of terrain, draw any sprites where 884 * the sprite's 'projected' row equals the current row. 885 * Remove the sprite from the list of visible sprites. 886 * 4. Rinse and repeat. 887 */ 888 889 // Render the terrain front-to-back. 890 _renderFrontToBack(initialAngle); 891 892 // If there are sprites in view... 893 if (_visibleSpriteList.length > 0) { 894 _renderSprites(); 895 } 896 897 // var date2 = new Date(); 898 // var endTime = date2.getTime(); 899 // console.log(endTime - startTime); 900 }, 901 902 /** 903 * Set the current camera. 904 * 905 * @param {object} camera The object representing the current 906 * camera. 907 * @return {Renderer} This (chainable). 908 */ 909 setCamera: function(camera) { 910 if (typeof(camera) != 'object') { 911 throw('Invalid camera: not an object'); 912 } 913 914 _camera.angle = (typeof camera.angle === 'number') ? 915 (Math.abs(~~(camera.angle + 0.5)) % 360 / 916 _ANGLE_OF_VIEW * 320) : (_camera.angle); 917 _camera.x = (typeof camera.x === 'number') ? 918 (~~(camera.x + 0.5)) : (_camera.x); 919 if (typeof camera.y === 'number' && camera.y >= 200 && 920 camera.y <= 300) { 921 _camera.y = ~~(camera.y + 0.5); 922 } 923 _camera.z = (typeof camera.z === 'number') ? 924 (~~(camera.z + 0.5)) : (_camera.z); 925 926 return this; 927 }, 928 929 /** 930 * Set the fog colour. 931 * 932 * @param {string} cssColor ... 933 * @return {Renderer} This (chainable). 934 */ 935 setFogColor: function(cssColor) { 936 if ( !_fog) { 937 throw('Capability not enabled'); 938 } 939 940 var re = /#([0-9A-Fa-f]{2})([0-9A-Fa-f]{2})([0-9A-Fa-f]{2})/; 941 var matches = re.exec(cssColor); 942 943 if (matches.length != 4) { 944 throw('Invalid cssColor: must be in fully-qualified ' + 945 'hexadecimal CSS format (#rrggbb)'); 946 } 947 948 var red = matches[1], 949 green = matches[2], 950 blue = matches[3]; 951 952 _fogColor = ((parseInt(red, 16) << 24) | 953 (parseInt(green, 16) << 16) | 954 (parseInt(blue, 16) << 8) | 0xff); 955 956 return this; 957 }, 958 959 /** 960 * Set the heightmap to use for terrain rendering. 961 * 962 * @param {array} heightmap The heightmap canvas as an array of 963 * values in the range 0..255. 964 * @return {Renderer} This (chainable). 965 */ 966 setHeightmap: function(heightmap) { 967 if ( !(heightmap instanceof Array)) { 968 throw('Invalid heightmap: not an array'); 969 } 970 971 if (heightmap.length != 1024 * 1024) { 972 throw('Invalid heightmap: number of array elements ' + 973 'incorrect'); 974 } 975 976 _heightmap = heightmap; 977 978 return this; 979 }, 980 981 /** 982 * Set the out-of-bounds heightmap. 983 * 984 * @param {array} outOfBoundsHeightmap The out-of-bounds heightmap 985 * canvas as an array. 986 * @return {Renderer} This (chainable). 987 */ 988 setOutOfBoundsHeightmap: function(outOfBoundsHeightmap) { 989 if ( !(outOfBoundsHeightmap instanceof Array)) { 990 throw('Invalid outOfBoundsHeightmap: not an array'); 991 } 992 993 if (outOfBoundsHeightmap.length != 1024 * 1024) { 994 throw('Invalid outOfBoundsHeightmap: number of array ' + 995 'elements incorrect'); 996 } 997 998 _outOfBoundsHeightmap = outOfBoundsHeightmap; 999 1000 return this; 1001 }, 1002 1003 /** 1004 * Set the out-of-bounds texturemap. 1005 * 1006 * @param {HTMLCanvasElement} outOfBoundsTexturemapCanvas The 1007 * out-of-bounds texturemap canvas. 1008 * @return {Renderer} This (chainable). 1009 */ 1010 setOutOfBoundsTexturemap: function( 1011 outOfBoundsTexturemapCanvas) { 1012 if ( !(outOfBoundsTexturemapCanvas instanceof 1013 HTMLCanvasElement)) { 1014 throw('Invalid outOfBoundsTexturemapCanvas: not an ' + 1015 'instance of HTMLCanvasElement'); 1016 } 1017 1018 if (outOfBoundsTexturemapCanvas.width != 1024) { 1019 throw('outOfBoundsTexturemapCanvas width not equal ' + 1020 'to 1024'); 1021 } 1022 if (outOfBoundsTexturemapCanvas.height != 1024) { 1023 throw('outOfBoundsTexturemapCanvas height not equal ' + 1024 'to 1024'); 1025 } 1026 1027 _outOfBoundsTexturemap = 1028 outOfBoundsTexturemapCanvas.getContext('2d') 1029 .getImageData(0, 0, outOfBoundsTexturemapCanvas.width, 1030 outOfBoundsTexturemapCanvas.height).data; 1031 1032 return this; 1033 }, 1034 1035 /** 1036 * Set render quality. 1037 * 1038 * @param {string} quality Specifies a string indicating the 1039 * render quality from 'low', through 1040 * 'medium', to 'high'. 1041 * @return {Renderer} This (chainable). 1042 */ 1043 setQuality: function(quality) { 1044 if (quality === 'medium') { 1045 _quality = _DONT_CARE; 1046 } else if (quality === 'low') { 1047 _quality = _FASTEST; 1048 } else if (quality === 'high') { 1049 _quality = _NICEST; 1050 } else { 1051 throw("Invalid quality: must be 'low', 'medium', " + 1052 "or 'high'"); 1053 } 1054 1055 return this; 1056 }, 1057 1058 /** 1059 * Set the canvas to use for 360-degree panoramic sky. 1060 * 1061 * @param {HTMLCanvasElement} skyCanvas The sky canvas; must be 1062 * 1920x100. 1063 * @return {Renderer} This (chainable). 1064 */ 1065 setSky: function(skyCanvas) { 1066 if ( !(skyCanvas instanceof HTMLCanvasElement)) { 1067 throw('Invalid skyCanvas: not an instance of ' + 1068 'HTMLCanvasElement'); 1069 } 1070 1071 if (skyCanvas.width != 1920) { 1072 throw('skyCanvas width not equal to 1920'); 1073 } 1074 if (skyCanvas.height != 100) { 1075 throw('skyCanvas height not equal to 100'); 1076 } 1077 1078 _sky = skyCanvas; 1079 _skyData = skyCanvas.getContext('2d').getImageData(0, 0, 1080 skyCanvas.width, skyCanvas.height).data; 1081 1082 return this; 1083 }, 1084 1085 /** 1086 * Set the texturemap. 1087 * 1088 * @param {HTMLCanvasElement} texturemapCanvas The texturemap 1089 * canvas. 1090 * @return {Renderer} This (chainable). 1091 */ 1092 setTexturemap: function(texturemapCanvas) { 1093 if ( !(texturemapCanvas instanceof HTMLCanvasElement)) { 1094 throw('Invalid texturemapCanvas: not an instance of ' + 1095 'HTMLCanvasElement'); 1096 } 1097 1098 if (texturemapCanvas.width != 1024) { 1099 throw('texturemapCanvas width not equal to 1024'); 1100 } 1101 if (texturemapCanvas.height != 1024) { 1102 throw('texturemapCanvas height not equal to 1024'); 1103 } 1104 1105 _texturemap = texturemapCanvas.getContext('2d') 1106 .getImageData(0, 0, texturemapCanvas.width, 1107 texturemapCanvas.height).data; 1108 1109 return this; 1110 }, 1111 1112 /** 1113 * Set height of the reflection-mapped water. 1114 * 1115 * @param {number} height Globally-defined height of the 1116 * reflection-mapped water. It must be 1117 * in the range 0..255. 1118 * @return {Renderer} This (chainable). 1119 */ 1120 setWaterHeight: function(height) { 1121 if (_waterHeight == -1) { 1122 throw('Capability not enabled'); 1123 } 1124 1125 if (typeof(height) != 'number') { 1126 throw('Invalid height: not a number'); 1127 } 1128 1129 if (height < 0 || height > 255) { 1130 throw('Invalid height: must be in the range 0..255'); 1131 } 1132 1133 _waterHeight = height; 1134 1135 return this; 1136 } 1137 }; 1138 }; 1139 1140 return Steppe; 1141 } (Steppe || { }) ); 1142