├── .gitignore ├── README.md ├── css └── q3demo.css ├── demo_baseq3 ├── README.md ├── basis_convert.js └── webgl │ └── no-shader.png ├── images ├── clang_floor2.png ├── fullscreen_sm.png └── vr_goggles_sm.png ├── index.html ├── js ├── basis │ ├── basis_loader.js │ ├── basis_transcoder.js │ └── basis_transcoder.wasm ├── main.js ├── q3bsp.js ├── q3bsp_worker.js ├── q3glshader.js ├── q3movement.js ├── q3shader.js └── util │ ├── binary-file.js │ ├── game-shim.js │ ├── gl-matrix-min.js │ ├── stats.min.js │ └── webxr-polyfill.min.js ├── package.json └── server.js /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | demo_baseq3/textures/* 3 | demo_baseq3/models/* 4 | demo_baseq3/scripts/* 5 | demo_baseq3/maps/* 6 | demo_baseq3/music/* 7 | node_modules 8 | 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | WebGL Quake 3 Renderer 2 | ======================= 3 | 4 | WebGL app that renders levels from Quake 3. Quake 3 and anything related to it 5 | is the property of id Software, who does not authorize or endorse this project 6 | in any way. 7 | 8 | 9 | Related Links 10 | ------------- 11 | 12 | * [Live Demo](http://media.tojicode.com/q3bsp) 13 | * [Technical Overview](http://blog.tojicode.com/2010/08/rendering-quake-3-maps-with-webgl-tech.html) -------------------------------------------------------------------------------- /css/q3demo.css: -------------------------------------------------------------------------------- 1 | body { 2 | font: 0.8em Verdana,sans-serif; 3 | background-color: black; 4 | color: white; 5 | } 6 | 7 | a { 8 | color: #DDEEFF; 9 | text-decoration: none; 10 | } 11 | 12 | a:hover { 13 | color: #FFFFFF; 14 | text-decoration: underline; 15 | } 16 | 17 | h2 { 18 | font-size: 1.1em; 19 | text-align: center; 20 | } 21 | 22 | #stats { 23 | position: absolute; 24 | top: 5px; 25 | left: 5px; 26 | z-index: 2; 27 | pointer-events: none; 28 | } 29 | 30 | #viewport-frame, #webgl-error { 31 | position: relative; 32 | margin: auto; 33 | width: 854px; 34 | height: 480px; 35 | } 36 | 37 | canvas { 38 | width: 854px; 39 | height: 480px; 40 | cursor: move; 41 | text-align: center; 42 | background-color: black; 43 | display: none; 44 | } 45 | 46 | #viewport-info { 47 | position: relative; 48 | margin: auto; 49 | width: 854px; 50 | } 51 | 52 | #viewport-info h3 { 53 | margin: 2px; 54 | } 55 | 56 | #viewport-info ul { 57 | list-style: none; 58 | padding-right: 20px; 59 | padding-left: 20px; 60 | } 61 | 62 | #controls, #config { 63 | float: left; 64 | width: 411px; 65 | height: 120px; 66 | margin: 5px; 67 | margin-top: 10px; 68 | padding: 3px; 69 | background-image: url('../images/clang_floor2.png'); 70 | background-color: #3B3632; 71 | -moz-border-radius: 5px; 72 | -webkit-border-radius: 5px; 73 | } 74 | 75 | #config { 76 | text-align: right; 77 | } 78 | 79 | #fullscreenBtn { 80 | cursor: pointer; 81 | } 82 | 83 | #vrBtn { 84 | cursor: pointer; 85 | } 86 | 87 | #viewport-frame:-moz-full-screen, :-moz-full-screen canvas, canvas:-moz-full-screen { 88 | width: 100%; 89 | height: 100%; 90 | position: absolute; 91 | left: 0; 92 | top: 0; 93 | margin: 0; 94 | } 95 | 96 | #viewport-frame:-webkit-full-screen, :-webkit-full-screen canvas, canvas:-webkit-full-screen { 97 | width: 100%; 98 | height: 100%; 99 | position: absolute; 100 | left: 0; 101 | top: 0; 102 | margin: 0; 103 | } 104 | 105 | #loading { 106 | position: relative; 107 | margin: auto; 108 | top: 200px; 109 | padding: 10px; 110 | border: 2px solid #666; 111 | border-radius: 5px; 112 | width: 200px; 113 | z-index: 5; 114 | background-color: black; 115 | text-align: center; 116 | vertical-align: middle; 117 | pointer-events: none; 118 | } 119 | 120 | #mobileVrBtn { 121 | position: absolute; 122 | bottom: 1em; 123 | right: 4em; 124 | } 125 | 126 | #mobileFullscreenBtn { 127 | position: absolute; 128 | bottom: 1em; 129 | right: 1em; 130 | } 131 | 132 | /* Smaller than standard 960 (devices and browsers) */ 133 | @media only screen and (max-width: 959px) { 134 | h2, #viewport-info { 135 | display: none !important; 136 | } 137 | 138 | #webgl-error { 139 | width: 100%; 140 | height: 100%; 141 | } 142 | 143 | #viewport-frame 144 | { 145 | width: 100%; 146 | height: 100%; 147 | position: absolute; 148 | left: 0; 149 | top: 0; 150 | margin: 0; 151 | } 152 | 153 | canvas { 154 | position: absolute; 155 | top: 0; 156 | left: 0; 157 | cursor: move; 158 | background-color: black; 159 | display: none; 160 | text-align: center; 161 | width: 100%; 162 | height: 100%; 163 | } 164 | } 165 | 166 | @media only screen and (min-width: 960px) { 167 | #mobileVrBtn { 168 | display: none !important; 169 | } 170 | #mobileFullscreenBtn { 171 | display: none !important; 172 | } 173 | } 174 | -------------------------------------------------------------------------------- /demo_baseq3/README.md: -------------------------------------------------------------------------------- 1 | Quake 3 Resources 2 | ================= 3 | 4 | This folder is where you should extract the resources from your own copy of Quake 3 5 | to be able to load the maps. And yes, you have to provide your own resources, I'm 6 | not going to redistribute id's property. (This works just fine with the Quake 3 demo 7 | PAKs, though, if you can dig one of them up!) 8 | 9 | Resource preparation 10 | -------------------- 11 | 12 | As much as I tried to avoid it, there are some changes that you will need to make to 13 | the game resources before they can be loaded: 14 | 15 | * Any texture files that you need must be converted to PNGs. 16 | * You must make sure that all textures dimensions are a power of 2 (64, 128, 256, 1024, etc) 17 | 18 | -------------------------------------------------------------------------------- /demo_baseq3/basis_convert.js: -------------------------------------------------------------------------------- 1 | // Converts all .png files in the folder to .basis 2 | const path = require('path'); 3 | const fs = require('fs'); 4 | const { exec } = require('child_process'); 5 | 6 | const basis_path = '~/github/basis_universal/bin/basisu'; 7 | 8 | function recurse(dirPath) { 9 | // Read all the files/dirs at the specified path 10 | fs.readdir(dirPath, function (err, files) { 11 | //handling error 12 | if (err) { 13 | return console.log('Unable to scan directory: ' + err); 14 | } 15 | 16 | // Loop through all the files 17 | for (let file of files) { 18 | let fullPath = path.join(dirPath, file); 19 | let stat = fs.statSync(fullPath); 20 | if (stat.isDirectory()) { 21 | console.log ("Dir: " + file); 22 | recurse(fullPath); 23 | } else if (stat.isFile()) { 24 | if(file.endsWith('.png')) { 25 | //fs.unlinkSync(fullPath); 26 | exec(`${basis_path} ${fullPath} -uastc -uastc_level 2 -mipmap -output_path ${dirPath}`, (err, stdout, stderr) => { 27 | if (err) { 28 | console.log(`Error - ${file}:`); 29 | console.log(` - err: ${err}`); 30 | console.log(` - stdout: ${stdout}`); 31 | console.log(` - stderr: ${stderr}`); 32 | return; 33 | } else { 34 | console.log(`Success - ${file}`); 35 | } 36 | }); 37 | } 38 | } 39 | } 40 | }); 41 | } 42 | 43 | recurse(path.join(__dirname, 'models')); 44 | recurse(path.join(__dirname, 'textures')); 45 | -------------------------------------------------------------------------------- /demo_baseq3/webgl/no-shader.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toji/webgl-quake3/685002c324c251fee880360d42f4dd60aa372410/demo_baseq3/webgl/no-shader.png -------------------------------------------------------------------------------- /images/clang_floor2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toji/webgl-quake3/685002c324c251fee880360d42f4dd60aa372410/images/clang_floor2.png -------------------------------------------------------------------------------- /images/fullscreen_sm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toji/webgl-quake3/685002c324c251fee880360d42f4dd60aa372410/images/fullscreen_sm.png -------------------------------------------------------------------------------- /images/vr_goggles_sm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toji/webgl-quake3/685002c324c251fee880360d42f4dd60aa372410/images/vr_goggles_sm.png -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Quake 3 WebGL Demo 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 |

33 | 34 |
35 | 36 | Sorry, but your browser does not support WebGL or does not have it enabled. 37 | To get a WebGL-enabled browser, please see:
38 | 39 | http://get.webgl.org/ 40 | 41 |
42 |
43 | Loading... 44 |
45 |
46 | 47 | 54 | 55 | 56 | 57 | 58 | 80 | 81 | 82 | -------------------------------------------------------------------------------- /js/basis/basis_loader.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019 Brandon Jones 3 | * 4 | * This software is provided 'as-is', without any express or implied 5 | * warranty. In no event will the authors be held liable for any damages 6 | * arising from the use of this software. 7 | * 8 | * Permission is granted to anyone to use this software for any purpose, 9 | * including commercial applications, and to alter it and redistribute it 10 | * freely, subject to the following restrictions: 11 | * 12 | * 1. The origin of this software must not be misrepresented; you must not 13 | * claim that you wrote the original software. If you use this software 14 | * in a product, an acknowledgment in the product documentation would be 15 | * appreciated but is not required. 16 | * 17 | * 2. Altered source versions must be plainly marked as such, and must not 18 | * be misrepresented as being the original software. 19 | * 20 | * 3. This notice may not be removed or altered from any source 21 | * distribution. 22 | */ 23 | 24 | /* 25 | * Usage: 26 | * // basis_loader.js should be loaded from the same directory as 27 | * // basis_transcoder.js and basis_transcoder.wasm 28 | * 29 | * // Create the texture loader and set the WebGL context it should use. Spawns 30 | * // a worker which handles all of the transcoding. 31 | * let basisLoader = new BasisLoader(); 32 | * basisLoader.setWebGLContext(gl); 33 | * 34 | * // To allow separate color and alpha textures to be returned in cases where 35 | * // it would provide higher quality: 36 | * basisLoader.allowSeparateAlpha = true; 37 | * 38 | * // loadFromUrl() returns a promise which resolves to a completed WebGL 39 | * // texture or rejects if there's an error loading. 40 | * basisBasics.loadFromUrl(fullPathToTexture).then((result) => { 41 | * // WebGL color+alpha texture; 42 | * result.texture; 43 | * 44 | * // WebGL alpha texture, only if basisLoader.allowSeparateAlpha is true. 45 | * // null if alpha is encoded in result.texture or result.alpha is false. 46 | * result.alphaTexture; 47 | * 48 | * // True if the texture contained an alpha channel. 49 | * result.alpha; 50 | * 51 | * // Number of mip levels in texture/alphaTexture 52 | * result.mipLevels; 53 | * 54 | * // Dimensions of the base mip level. 55 | * result.width; 56 | * result.height; 57 | * }); 58 | */ 59 | 60 | // This file contains the code both for the main thread interface and the 61 | // worker that does the transcoding. 62 | const IN_WORKER = typeof importScripts === "function"; 63 | const SCRIPT_PATH = typeof document !== 'undefined' && document.currentScript ? document.currentScript.src : undefined; 64 | 65 | if (!IN_WORKER) { 66 | // 67 | // Main Thread 68 | // 69 | class PendingTextureRequest { 70 | constructor(gl, url) { 71 | this.gl = gl; 72 | this.url = url; 73 | this.texture = null; 74 | this.alphaTexture = null; 75 | this.promise = new Promise((resolve, reject) => { 76 | this.resolve = resolve; 77 | this.reject = reject; 78 | }); 79 | } 80 | 81 | uploadImageData(webglFormat, buffer, mipLevels) { 82 | let gl = this.gl; 83 | let texture = gl.createTexture(); 84 | gl.bindTexture(gl.TEXTURE_2D, texture); 85 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR); 86 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, mipLevels.length > 1 || webglFormat.uncompressed ? gl.LINEAR_MIPMAP_LINEAR : gl.LINEAR); 87 | 88 | let levelData = null; 89 | 90 | for (let mipLevel of mipLevels) { 91 | if (!webglFormat.uncompressed) { 92 | levelData = new Uint8Array(buffer, mipLevel.offset, mipLevel.size); 93 | gl.compressedTexImage2D( 94 | gl.TEXTURE_2D, 95 | mipLevel.level, 96 | webglFormat.format, 97 | mipLevel.width, 98 | mipLevel.height, 99 | 0, 100 | levelData); 101 | } else { 102 | switch (webglFormat.type) { 103 | case WebGLRenderingContext.UNSIGNED_SHORT_4_4_4_4: 104 | case WebGLRenderingContext.UNSIGNED_SHORT_5_5_5_1: 105 | case WebGLRenderingContext.UNSIGNED_SHORT_5_6_5: 106 | levelData = new Uint16Array(buffer, mipLevel.offset, mipLevel.size / 2); 107 | break; 108 | default: 109 | levelData = new Uint8Array(buffer, mipLevel.offset, mipLevel.size); 110 | break; 111 | } 112 | gl.texImage2D( 113 | gl.TEXTURE_2D, 114 | mipLevel.level, 115 | webglFormat.format, 116 | mipLevel.width, 117 | mipLevel.height, 118 | 0, 119 | webglFormat.format, 120 | webglFormat.type, 121 | levelData); 122 | } 123 | } 124 | 125 | if (webglFormat.uncompressed && mipLevels.length == 1) { 126 | gl.generateMipmap(gl.TEXTURE_2D); 127 | } 128 | 129 | return texture; 130 | } 131 | }; 132 | 133 | class BasisLoader { 134 | constructor() { 135 | this.gl = null; 136 | this.supportedFormats = {}; 137 | this.pendingTextures = {}; 138 | this.nextPendingTextureId = 1; 139 | this.allowSeparateAlpha = false; 140 | 141 | // Reload the current script as a worker 142 | this.worker = new Worker(SCRIPT_PATH); 143 | this.worker.onmessage = (msg) => { 144 | // Find the pending texture associated with the data we just received 145 | // from the worker. 146 | let pendingTexture = this.pendingTextures[msg.data.id]; 147 | if (!pendingTexture) { 148 | if (msg.data.error) { 149 | console.error(`Basis transcode failed: ${msg.data.error}`); 150 | } 151 | console.error(`Invalid pending texture ID: ${msg.data.id}`); 152 | return; 153 | } 154 | 155 | // Remove the pending texture from the waiting list. 156 | delete this.pendingTextures[msg.data.id]; 157 | 158 | // If the worker indicated an error has occured handle it now. 159 | if (msg.data.error) { 160 | console.error(`Basis transcode failed: ${msg.data.error}`); 161 | pendingTexture.reject(`${msg.data.error}`); 162 | return; 163 | } 164 | 165 | // Upload the image data returned by the worker. 166 | pendingTexture.texture = pendingTexture.uploadImageData( 167 | msg.data.webglFormat, 168 | msg.data.buffer, 169 | msg.data.mipLevels); 170 | 171 | if (msg.data.alphaBuffer) { 172 | pendingTexture.alphaTexture = pendingTexture.uploadImageData( 173 | msg.data.webglFormat, 174 | msg.data.alphaBuffer, 175 | msg.data.mipLevels); 176 | } 177 | 178 | pendingTexture.resolve({ 179 | mipLevels: msg.data.mipLevels.length, 180 | width: msg.data.mipLevels[0].width, 181 | height: msg.data.mipLevels[0].height, 182 | alpha: msg.data.hasAlpha, 183 | texture: pendingTexture.texture, 184 | alphaTexture: pendingTexture.alphaTexture, 185 | }); 186 | }; 187 | } 188 | 189 | setWebGLContext(gl) { 190 | if (this.gl != gl) { 191 | this.gl = gl; 192 | if (gl) { 193 | this.supportedFormats = { 194 | s3tc: !!gl.getExtension('WEBGL_compressed_texture_s3tc'), 195 | etc1: !!gl.getExtension('WEBGL_compressed_texture_etc1'), 196 | etc2: !!gl.getExtension('WEBGL_compressed_texture_etc'), 197 | pvrtc: !!gl.getExtension('WEBGL_compressed_texture_pvrtc'), 198 | astc: !!gl.getExtension('WEBGL_compressed_texture_astc'), 199 | bptc: !!gl.getExtension('EXT_texture_compression_bptc') 200 | }; 201 | } else { 202 | this.supportedFormats = {}; 203 | } 204 | } 205 | } 206 | 207 | // This method changes the active texture unit's TEXTURE_2D binding 208 | // immediately prior to resolving the returned promise. 209 | loadFromUrl(url) { 210 | let pendingTexture = new PendingTextureRequest(this.gl, url); 211 | this.pendingTextures[this.nextPendingTextureId] = pendingTexture; 212 | 213 | this.worker.postMessage({ 214 | id: this.nextPendingTextureId, 215 | url: url, 216 | allowSeparateAlpha: this.allowSeparateAlpha, 217 | supportedFormats: this.supportedFormats 218 | }); 219 | 220 | this.nextPendingTextureId++; 221 | return pendingTexture.promise; 222 | } 223 | } 224 | 225 | window.BasisLoader = BasisLoader; 226 | 227 | } else { 228 | // 229 | // Worker 230 | // 231 | importScripts('basis_transcoder.js'); 232 | 233 | let BasisFile = null; 234 | 235 | const BASIS_INITIALIZED = BASIS().then((module) => { 236 | BasisFile = module.BasisFile; 237 | module.initializeBasis(); 238 | }); 239 | 240 | // Copied from enum class transcoder_texture_format in basisu_transcoder.h with minor javascript-ification 241 | const BASIS_FORMAT = { 242 | // Compressed formats 243 | 244 | // ETC1-2 245 | cTFETC1_RGB: 0, // Opaque only, returns RGB or alpha data if cDecodeFlagsTranscodeAlphaDataToOpaqueFormats flag is specified 246 | cTFETC2_RGBA: 1, // Opaque+alpha, ETC2_EAC_A8 block followed by a ETC1 block, alpha channel will be opaque for opaque .basis files 247 | 248 | // BC1-5, BC7 (desktop, some mobile devices) 249 | cTFBC1_RGB: 2, // Opaque only, no punchthrough alpha support yet, transcodes alpha slice if cDecodeFlagsTranscodeAlphaDataToOpaqueFormats flag is specified 250 | cTFBC3_RGBA: 3, // Opaque+alpha, BC4 followed by a BC1 block, alpha channel will be opaque for opaque .basis files 251 | cTFBC4_R: 4, // Red only, alpha slice is transcoded to output if cDecodeFlagsTranscodeAlphaDataToOpaqueFormats flag is specified 252 | cTFBC5_RG: 5, // XY: Two BC4 blocks, X=R and Y=Alpha, .basis file should have alpha data (if not Y will be all 255's) 253 | cTFBC7_RGBA: 6, // RGB or RGBA, mode 5 for ETC1S, modes (1,2,3,5,6,7) for UASTC 254 | 255 | // PVRTC1 4bpp (mobile, PowerVR devices) 256 | cTFPVRTC1_4_RGB: 8, // Opaque only, RGB or alpha if cDecodeFlagsTranscodeAlphaDataToOpaqueFormats flag is specified, nearly lowest quality of any texture format. 257 | cTFPVRTC1_4_RGBA: 9, // Opaque+alpha, most useful for simple opacity maps. If .basis file doesn't have alpha cTFPVRTC1_4_RGB will be used instead. Lowest quality of any supported texture format. 258 | 259 | // ASTC (mobile, Intel devices, hopefully all desktop GPU's one day) 260 | cTFASTC_4x4_RGBA: 10, // Opaque+alpha, ASTC 4x4, alpha channel will be opaque for opaque .basis files. Transcoder uses RGB/RGBA/L/LA modes, void extent, and up to two ([0,47] and [0,255]) endpoint precisions. 261 | 262 | // Uncompressed (raw pixel) formats 263 | cTFRGBA32: 13, // 32bpp RGBA image stored in raster (not block) order in memory, R is first byte, A is last byte. 264 | cTFRGB565: 14, // 166pp RGB image stored in raster (not block) order in memory, R at bit position 11 265 | cTFBGR565: 15, // 16bpp RGB image stored in raster (not block) order in memory, R at bit position 0 266 | cTFRGBA4444: 16, // 16bpp RGBA image stored in raster (not block) order in memory, R at bit position 12, A at bit position 0 267 | 268 | cTFTotalTextureFormats: 22, 269 | }; 270 | 271 | // WebGL compressed formats types, from: 272 | // http://www.khronos.org/registry/webgl/extensions/ 273 | 274 | // https://www.khronos.org/registry/webgl/extensions/WEBGL_compressed_texture_s3tc/ 275 | const COMPRESSED_RGB_S3TC_DXT1_EXT = 0x83F0; 276 | const COMPRESSED_RGBA_S3TC_DXT1_EXT = 0x83F1; 277 | const COMPRESSED_RGBA_S3TC_DXT3_EXT = 0x83F2; 278 | const COMPRESSED_RGBA_S3TC_DXT5_EXT = 0x83F3; 279 | 280 | // https://www.khronos.org/registry/webgl/extensions/WEBGL_compressed_texture_etc1/ 281 | const COMPRESSED_RGB_ETC1_WEBGL = 0x8D64 282 | 283 | // https://www.khronos.org/registry/webgl/extensions/WEBGL_compressed_texture_etc/ 284 | const COMPRESSED_R11_EAC = 0x9270; 285 | const COMPRESSED_SIGNED_R11_EAC = 0x9271; 286 | const COMPRESSED_RG11_EAC = 0x9272; 287 | const COMPRESSED_SIGNED_RG11_EAC = 0x9273; 288 | const COMPRESSED_RGB8_ETC2 = 0x9274; 289 | const COMPRESSED_SRGB8_ETC2 = 0x9275; 290 | const COMPRESSED_RGB8_PUNCHTHROUGH_ALPHA1_ETC2 = 0x9276; 291 | const COMPRESSED_SRGB8_PUNCHTHROUGH_ALPHA1_ETC2 = 0x9277; 292 | const COMPRESSED_RGBA8_ETC2_EAC = 0x9278; 293 | const COMPRESSED_SRGB8_ALPHA8_ETC2_EAC = 0x9279; 294 | 295 | // https://www.khronos.org/registry/webgl/extensions/WEBGL_compressed_texture_astc/ 296 | const COMPRESSED_RGBA_ASTC_4x4_KHR = 0x93B0; 297 | 298 | // https://www.khronos.org/registry/webgl/extensions/WEBGL_compressed_texture_pvrtc/ 299 | const COMPRESSED_RGB_PVRTC_4BPPV1_IMG = 0x8C00; 300 | const COMPRESSED_RGB_PVRTC_2BPPV1_IMG = 0x8C01; 301 | const COMPRESSED_RGBA_PVRTC_4BPPV1_IMG = 0x8C02; 302 | const COMPRESSED_RGBA_PVRTC_2BPPV1_IMG = 0x8C03; 303 | 304 | // https://www.khronos.org/registry/webgl/extensions/EXT_texture_compression_bptc/ 305 | const COMPRESSED_RGBA_BPTC_UNORM_EXT = 0x8E8C; 306 | const COMPRESSED_SRGB_ALPHA_BPTC_UNORM_EXT = 0x8E8D; 307 | const COMPRESSED_RGB_BPTC_SIGNED_FLOAT_EXT = 0x8E8E; 308 | const COMPRESSED_RGB_BPTC_UNSIGNED_FLOAT_EXT = 0x8E8F; 309 | 310 | const BASIS_WEBGL_FORMAT_MAP = {}; 311 | // Compressed formats 312 | BASIS_WEBGL_FORMAT_MAP[BASIS_FORMAT.cTFBC1_RGB] = { format: COMPRESSED_RGB_S3TC_DXT1_EXT }; 313 | BASIS_WEBGL_FORMAT_MAP[BASIS_FORMAT.cTFBC3_RGBA] = { format: COMPRESSED_RGBA_S3TC_DXT5_EXT }; 314 | BASIS_WEBGL_FORMAT_MAP[BASIS_FORMAT.cTFBC7_RGBA] = { format: COMPRESSED_RGBA_BPTC_UNORM_EXT }; 315 | BASIS_WEBGL_FORMAT_MAP[BASIS_FORMAT.cTFETC1_RGB] = { format: COMPRESSED_RGB_ETC1_WEBGL }; 316 | BASIS_WEBGL_FORMAT_MAP[BASIS_FORMAT.cTFETC2_RGBA] = { format: COMPRESSED_RGBA8_ETC2_EAC }; 317 | BASIS_WEBGL_FORMAT_MAP[BASIS_FORMAT.cTFASTC_4x4_RGBA] = { format: COMPRESSED_RGBA_ASTC_4x4_KHR }; 318 | BASIS_WEBGL_FORMAT_MAP[BASIS_FORMAT.cTFPVRTC1_4_RGB] = { format: COMPRESSED_RGB_PVRTC_4BPPV1_IMG }; 319 | BASIS_WEBGL_FORMAT_MAP[BASIS_FORMAT.cTFPVRTC1_4_RGBA] = { format: COMPRESSED_RGBA_PVRTC_4BPPV1_IMG }; 320 | 321 | // Uncompressed formats 322 | BASIS_WEBGL_FORMAT_MAP[BASIS_FORMAT.cTFRGBA32] = { uncompressed: true, format: WebGLRenderingContext.RGBA, type: WebGLRenderingContext.UNSIGNED_BYTE }; 323 | BASIS_WEBGL_FORMAT_MAP[BASIS_FORMAT.cTFRGB565] = { uncompressed: true, format: WebGLRenderingContext.RGB, type: WebGLRenderingContext.UNSIGNED_SHORT_5_6_5 }; 324 | BASIS_WEBGL_FORMAT_MAP[BASIS_FORMAT.cTFRGBA4444] = { uncompressed: true, format: WebGLRenderingContext.RGBA, type: WebGLRenderingContext.UNSIGNED_SHORT_4_4_4_4 }; 325 | 326 | // Notifies the main thread when a texture has failed to load for any reason. 327 | function fail(id, errorMsg) { 328 | postMessage({ 329 | id: id, 330 | error: errorMsg 331 | }); 332 | } 333 | 334 | function basisFileFail(id, basisFile, errorMsg) { 335 | fail(id, errorMsg); 336 | basisFile.close(); 337 | basisFile.delete(); 338 | } 339 | 340 | // This utility currently only transcodes the first image in the file. 341 | const IMAGE_INDEX = 0; 342 | const TOP_LEVEL_MIP = 0; 343 | 344 | function transcode(id, arrayBuffer, supportedFormats, allowSeparateAlpha) { 345 | let basisData = new Uint8Array(arrayBuffer); 346 | 347 | let basisFile = new BasisFile(basisData); 348 | let images = basisFile.getNumImages(); 349 | let levels = basisFile.getNumLevels(IMAGE_INDEX); 350 | let hasAlpha = basisFile.getHasAlpha(); 351 | if (!images || !levels) { 352 | basisFileFail(id, basisFile, 'Invalid Basis data'); 353 | return; 354 | } 355 | 356 | if (!basisFile.startTranscoding()) { 357 | basisFileFail(id, basisFile, 'startTranscoding failed'); 358 | return; 359 | } 360 | 361 | let basisFormat = undefined; 362 | let needsSecondaryAlpha = false; 363 | if (hasAlpha) { 364 | if (supportedFormats.etc2) { 365 | basisFormat = BASIS_FORMAT.cTFETC2_RGBA; 366 | } else if (supportedFormats.bptc) { 367 | basisFormat = BASIS_FORMAT.cTFBC7_RGBA; 368 | } else if (supportedFormats.s3tc) { 369 | basisFormat = BASIS_FORMAT.cTFBC3_RGBA; 370 | } else if (supportedFormats.astc) { 371 | basisFormat = BASIS_FORMAT.cTFASTC_4x4_RGBA; 372 | } else if (supportedFormats.pvrtc) { 373 | if (allowSeparateAlpha) { 374 | basisFormat = BASIS_FORMAT.cTFPVRTC1_4_RGB; 375 | } else { 376 | basisFormat = BASIS_FORMAT.cTFPVRTC1_4_RGBA; 377 | } 378 | } else if (supportedFormats.etc1 && allowSeparateAlpha) { 379 | basisFormat = BASIS_FORMAT.cTFETC1_RGB; 380 | needsSecondaryAlpha = true; 381 | } else { 382 | // If we don't support any appropriate compressed formats transcode to 383 | // raw pixels. This is something of a last resort, because the GPU 384 | // upload will be significantly slower and take a lot more memory, but 385 | // at least it prevents you from needing to store a fallback JPG/PNG and 386 | // the download size will still likely be smaller. 387 | basisFormat = BASIS_FORMAT.RGBA32; 388 | } 389 | } else { 390 | if (supportedFormats.etc1) { 391 | // Should be the highest quality, so use when available. 392 | // http://richg42.blogspot.com/2018/05/basis-universal-gpu-texture-format.html 393 | basisFormat = BASIS_FORMAT.cTFETC1_RGB; 394 | } else if (supportedFormats.bptc) { 395 | basisFormat = BASIS_FORMAT.cTFBC7_RGBA; 396 | } else if (supportedFormats.s3tc) { 397 | basisFormat = BASIS_FORMAT.cTFBC1_RGB; 398 | } else if (supportedFormats.etc2) { 399 | basisFormat = BASIS_FORMAT.cTFETC2_RGBA; 400 | } else if (supportedFormats.astc) { 401 | basisFormat = BASIS_FORMAT.cTFASTC_4x4_RGBA; 402 | } else if (supportedFormats.pvrtc) { 403 | basisFormat = BASIS_FORMAT.cTFPVRTC1_4_RGB; 404 | } else { 405 | // See note on uncompressed transcode above. 406 | basisFormat = BASIS_FORMAT.cTFRGB565; 407 | } 408 | } 409 | 410 | if (basisFormat === undefined) { 411 | basisFileFail(id, basisFile, 'No supported transcode formats'); 412 | return; 413 | } 414 | 415 | let webglFormat = BASIS_WEBGL_FORMAT_MAP[basisFormat]; 416 | 417 | // If we're not using compressed textures it'll be cheaper to generate 418 | // mipmaps on the fly, so only transcode a single level. 419 | if (webglFormat.uncompressed) { 420 | levels = 1; 421 | } 422 | 423 | // Gather information about each mip level to be transcoded. 424 | let mipLevels = []; 425 | let totalTranscodeSize = 0; 426 | 427 | for (let mipLevel = 0; mipLevel < levels; ++mipLevel) { 428 | let transcodeSize = basisFile.getImageTranscodedSizeInBytes(IMAGE_INDEX, mipLevel, basisFormat); 429 | mipLevels.push({ 430 | level: mipLevel, 431 | offset: totalTranscodeSize, 432 | size: transcodeSize, 433 | width: basisFile.getImageWidth(IMAGE_INDEX, mipLevel), 434 | height: basisFile.getImageHeight(IMAGE_INDEX, mipLevel), 435 | }); 436 | totalTranscodeSize += transcodeSize; 437 | } 438 | 439 | // Allocate a buffer large enough to hold all of the transcoded mip levels at once. 440 | let transcodeData = new Uint8Array(totalTranscodeSize); 441 | let alphaTranscodeData = needsSecondaryAlpha ? new Uint8Array(totalTranscodeSize) : null; 442 | 443 | // Transcode each mip level into the appropriate section of the overall buffer. 444 | for (let mipLevel of mipLevels) { 445 | let levelData = new Uint8Array(transcodeData.buffer, mipLevel.offset, mipLevel.size); 446 | if (!basisFile.transcodeImage(levelData, IMAGE_INDEX, mipLevel.level, basisFormat, 1, 0)) { 447 | basisFileFail(id, basisFile, 'transcodeImage failed'); 448 | return; 449 | } 450 | if (needsSecondaryAlpha) { 451 | let alphaLevelData = new Uint8Array(alphaTranscodeData.buffer, mipLevel.offset, mipLevel.size); 452 | if (!basisFile.transcodeImage(alphaLevelData, IMAGE_INDEX, mipLevel.level, basisFormat, 1, 1)) { 453 | basisFileFail(id, basisFile, 'alpha transcodeImage failed'); 454 | return; 455 | } 456 | } 457 | } 458 | 459 | basisFile.close(); 460 | basisFile.delete(); 461 | 462 | // Post the transcoded results back to the main thread. 463 | let transferList = [transcodeData.buffer]; 464 | if (needsSecondaryAlpha) { 465 | transferList.push(alphaTranscodeData.buffer); 466 | } 467 | postMessage({ 468 | id: id, 469 | buffer: transcodeData.buffer, 470 | alphaBuffer: needsSecondaryAlpha ? alphaTranscodeData.buffer : null, 471 | webglFormat: webglFormat, 472 | mipLevels: mipLevels, 473 | hasAlpha: hasAlpha, 474 | }, transferList); 475 | } 476 | 477 | onmessage = (msg) => { 478 | // Each call to the worker must contain: 479 | let url = msg.data.url; // The URL of the basis image OR 480 | let buffer = msg.data.buffer; // An array buffer with the basis image data 481 | let allowSeparateAlpha = msg.data.allowSeparateAlpha; 482 | let supportedFormats = msg.data.supportedFormats; // The formats this device supports 483 | let id = msg.data.id; // A unique ID for the texture 484 | 485 | if (url) { 486 | // Make the call to fetch the basis texture data 487 | fetch(`../../${url}`).then(function(response) { 488 | if (response.ok) { 489 | response.arrayBuffer().then((arrayBuffer) => { 490 | if (BasisFile) { 491 | transcode(id, arrayBuffer, supportedFormats, allowSeparateAlpha); 492 | } else { 493 | BASIS_INITIALIZED.then(() => { 494 | transcode(id, arrayBuffer, supportedFormats, allowSeparateAlpha); 495 | }); 496 | } 497 | }); 498 | } else { 499 | fail(id, `Fetch failed: ${response.status}, ${response.statusText}`); 500 | } 501 | }); 502 | } else if (buffer) { 503 | if (BasisFile) { 504 | transcode(id, buffer, supportedFormats, allowSeparateAlpha); 505 | } else { 506 | BASIS_INITIALIZED.then(() => { 507 | transcode(id, buffer, supportedFormats, allowSeparateAlpha); 508 | }); 509 | } 510 | } else { 511 | fail(id, `No url or buffer specified`); 512 | } 513 | }; 514 | } -------------------------------------------------------------------------------- /js/basis/basis_transcoder.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toji/webgl-quake3/685002c324c251fee880360d42f4dd60aa372410/js/basis/basis_transcoder.wasm -------------------------------------------------------------------------------- /js/main.js: -------------------------------------------------------------------------------- 1 | /* 2 | * main.js - Setup for Quake 3 WebGL demo 3 | */ 4 | 5 | /* 6 | * Copyright (c) 2011 Brandon Jones 7 | * 8 | * This software is provided 'as-is', without any express or implied 9 | * warranty. In no event will the authors be held liable for any damages 10 | * arising from the use of this software. 11 | * 12 | * Permission is granted to anyone to use this software for any purpose, 13 | * including commercial applications, and to alter it and redistribute it 14 | * freely, subject to the following restrictions: 15 | * 16 | * 1. The origin of this software must not be misrepresented; you must not 17 | * claim that you wrote the original software. If you use this software 18 | * in a product, an acknowledgment in the product documentation would be 19 | * appreciated but is not required. 20 | * 21 | * 2. Altered source versions must be plainly marked as such, and must not 22 | * be misrepresented as being the original software. 23 | * 24 | * 3. This notice may not be removed or altered from any source 25 | * distribution. 26 | */ 27 | 28 | var polyfill = new WebXRPolyfill(); 29 | 30 | // The bits that need to change to load different maps are right here! 31 | // =========================================== 32 | 33 | var mapName = 'q3tourney2'; 34 | 35 | // If you're running from your own copy of Quake 3, you'll want to use these shaders 36 | /*var mapShaders = [ 37 | 'scripts/base.shader', 'scripts/base_button.shader', 'scripts/base_floor.shader', 38 | 'scripts/base_light.shader', 'scripts/base_object.shader', 'scripts/base_support.shader', 39 | 'scripts/base_trim.shader', 'scripts/base_wall.shader', 'scripts/common.shader', 40 | 'scripts/ctf.shader', 'scripts/eerie.shader', 'scripts/gfx.shader', 41 | 'scripts/gothic_block.shader', 'scripts/gothic_floor.shader', 'scripts/gothic_light.shader', 42 | 'scripts/gothic_trim.shader', 'scripts/gothic_wall.shader', 'scripts/hell.shader', 43 | 'scripts/liquid.shader', 'scripts/menu.shader', 'scripts/models.shader', 44 | 'scripts/organics.shader', 'scripts/sfx.shader', 'scripts/shrine.shader', 45 | 'scripts/skin.shader', 'scripts/sky.shader', 'scripts/test.shader' 46 | ];*/ 47 | 48 | // For my demo, I compiled only the shaders the map used into a single file for performance reasons 49 | var mapShaders = ['scripts/web_demo.shader']; 50 | 51 | // =========================================== 52 | // Everything below here is common to all maps 53 | var leftViewMat, rightViewMat, projMat; 54 | var leftViewport, rightViewport; 55 | var activeShader; 56 | var map, playerMover; 57 | var mobileSite = false; 58 | 59 | var zAngle = 3; 60 | var xAngle = 0; 61 | var cameraPosition = [0, 0, 0]; 62 | var onResize = null; 63 | 64 | // WebXR Globals 65 | var xrDevice = null; 66 | var xrSession = null; 67 | var xrReferenceSpace = null; 68 | var xrPose = null; 69 | var xrViews = []; 70 | 71 | // These values are in meters 72 | var playerHeight = 57; // Roughly where my eyes sit (1.78 meters off the ground) 73 | var xrIPDScale = 32.0; // There are 32 units per meter in Quake 3 74 | 75 | var xrDrawMode = 0; 76 | 77 | var SKIP_FRAMES = 0; 78 | var REPEAT_FRAMES = 1; 79 | 80 | function isXRPresenting() { 81 | return !!xrSession; 82 | } 83 | 84 | function getQueryVariable(variable) { 85 | var query = window.location.search.substring(1); 86 | var vars = query.split("&"); 87 | for (var i = 0; i < vars.length; i++) { 88 | var pair = vars[i].split("="); 89 | if (pair[0] == variable) { 90 | return unescape(pair[1]); 91 | } 92 | } 93 | return null; 94 | } 95 | 96 | // Set up basic GL State up front 97 | function initGL(gl, canvas) { 98 | gl.clearColor(0.0, 0.0, 0.0, 1.0); 99 | gl.clearDepth(1.0); 100 | 101 | gl.enable(gl.DEPTH_TEST); 102 | gl.enable(gl.BLEND); 103 | gl.enable(gl.CULL_FACE); 104 | 105 | leftViewMat = mat4.create(); 106 | rightViewMat = mat4.create(); 107 | projMat = mat4.create(); 108 | 109 | leftViewport = { x: 0, y: 0, width: 0, height: 0 }; 110 | rightViewport = { x: 0, y: 0, width: 0, height: 0 }; 111 | 112 | initMap(gl); 113 | } 114 | 115 | // Load the map 116 | function initMap(gl) { 117 | var titleEl = document.getElementById("mapTitle"); 118 | titleEl.innerHtml = mapName.toUpperCase(); 119 | 120 | var tesselation = getQueryVariable("tesselate"); 121 | if(tesselation) { 122 | tesselation = parseInt(tesselation, 10); 123 | } 124 | 125 | var xrMode = getQueryVariable("vrDrawMode"); 126 | if (xrMode) { 127 | xrDrawMode = parseInt(xrMode, 10); 128 | } 129 | 130 | map = new q3bsp(gl); 131 | map.onentitiesloaded = initMapEntities; 132 | map.onbsp = initPlayerMover; 133 | //map.onsurfaces = initSurfaces; 134 | map.loadShaders(mapShaders); 135 | map.load('maps/' + mapName +'.bsp', tesselation); 136 | } 137 | 138 | // Process entities loaded from the map 139 | function initMapEntities(entities) { 140 | respawnPlayer(0); 141 | } 142 | 143 | function initPlayerMover(bsp) { 144 | playerMover = new q3movement(bsp); 145 | respawnPlayer(0); 146 | document.getElementById('viewport').style.display = 'block'; 147 | onResize(); 148 | } 149 | 150 | var lastIndex = 0; 151 | // "Respawns" the player at a specific spawn point. Passing -1 will move the player to the next spawn point. 152 | function respawnPlayer(index) { 153 | if(map.entities && playerMover) { 154 | if(index == -1) { 155 | index = (lastIndex+1)% map.entities.info_player_deathmatch.length; 156 | } 157 | lastIndex = index; 158 | 159 | var spawnPoint = map.entities.info_player_deathmatch[index]; 160 | playerMover.position = [ 161 | spawnPoint.origin[0], 162 | spawnPoint.origin[1], 163 | spawnPoint.origin[2]+30 // Start a little ways above the floor 164 | ]; 165 | 166 | playerMover.velocity = [0,0,0]; 167 | 168 | zAngle = -(spawnPoint.angle || 0) * (3.1415/180) + (3.1415*0.5); // Negative angle in radians + 90 degrees 169 | xAngle = 0; 170 | } 171 | } 172 | 173 | function eulerFromQuaternion(out, q, order) { 174 | function clamp(value, min, max) { 175 | return (value < min ? min : (value > max ? max : value)); 176 | } 177 | // Borrowed from Three.JS :) 178 | // q is assumed to be normalized 179 | // http://www.mathworks.com/matlabcentral/fileexchange/20696-function-to-convert-between-dcm-euler-angles-quaternions-and-euler-vectors/content/SpinCalc.m 180 | var sqx = q[0] * q[0]; 181 | var sqy = q[1] * q[1]; 182 | var sqz = q[2] * q[2]; 183 | var sqw = q[3] * q[3]; 184 | 185 | if ( order === 'XYZ' ) { 186 | out[0] = Math.atan2( 2 * ( q[0] * q[3] - q[1] * q[2] ), ( sqw - sqx - sqy + sqz ) ); 187 | out[1] = Math.asin( clamp( 2 * ( q[0] * q[2] + q[1] * q[3] ), -1, 1 ) ); 188 | out[2] = Math.atan2( 2 * ( q[2] * q[3] - q[0] * q[1] ), ( sqw + sqx - sqy - sqz ) ); 189 | } else if ( order === 'YXZ' ) { 190 | out[0] = Math.asin( clamp( 2 * ( q[0] * q[3] - q[1] * q[2] ), -1, 1 ) ); 191 | out[1] = Math.atan2( 2 * ( q[0] * q[2] + q[1] * q[3] ), ( sqw - sqx - sqy + sqz ) ); 192 | out[2] = Math.atan2( 2 * ( q[0] * q[1] + q[2] * q[3] ), ( sqw - sqx + sqy - sqz ) ); 193 | } else if ( order === 'ZXY' ) { 194 | out[0] = Math.asin( clamp( 2 * ( q[0] * q[3] + q[1] * q[2] ), -1, 1 ) ); 195 | out[1] = Math.atan2( 2 * ( q[1] * q[3] - q[2] * q[0] ), ( sqw - sqx - sqy + sqz ) ); 196 | out[2] = Math.atan2( 2 * ( q[2] * q[3] - q[0] * q[1] ), ( sqw - sqx + sqy - sqz ) ); 197 | } else if ( order === 'ZYX' ) { 198 | out[0] = Math.atan2( 2 * ( q[0] * q[3] + q[2] * q[1] ), ( sqw - sqx - sqy + sqz ) ); 199 | out[1] = Math.asin( clamp( 2 * ( q[1] * q[3] - q[0] * q[2] ), -1, 1 ) ); 200 | out[2] = Math.atan2( 2 * ( q[0] * q[1] + q[2] * q[3] ), ( sqw + sqx - sqy - sqz ) ); 201 | } else if ( order === 'YZX' ) { 202 | out[0] = Math.atan2( 2 * ( q[0] * q[3] - q[2] * q[1] ), ( sqw - sqx + sqy - sqz ) ); 203 | out[1] = Math.atan2( 2 * ( q[1] * q[3] - q[0] * q[2] ), ( sqw + sqx - sqy - sqz ) ); 204 | out[2] = Math.asin( clamp( 2 * ( q[0] * q[1] + q[2] * q[3] ), -1, 1 ) ); 205 | } else if ( order === 'XZY' ) { 206 | out[0] = Math.atan2( 2 * ( q[0] * q[3] + q[1] * q[2] ), ( sqw - sqx + sqy - sqz ) ); 207 | out[1] = Math.atan2( 2 * ( q[0] * q[2] + q[1] * q[3] ), ( sqw + sqx - sqy - sqz ) ); 208 | out[2] = Math.asin( clamp( 2 * ( q[2] * q[3] - q[0] * q[1] ), -1, 1 ) ); 209 | } else { 210 | console.log('No order given for quaternion to euler conversion.'); 211 | return; 212 | } 213 | } 214 | 215 | var lastMove = 0; 216 | 217 | function onFrame(gl, event) { 218 | if(!map || !playerMover) { return; } 219 | 220 | // Update player movement @ 60hz 221 | // The while ensures that we update at a fixed rate even if the rendering bogs down 222 | while(event.elapsed - lastMove >= 16) { 223 | updateInput(16); 224 | lastMove += 16; 225 | } 226 | 227 | // For great laggage! 228 | for (var i = 0; i < REPEAT_FRAMES; ++i) 229 | drawFrame(gl); 230 | } 231 | 232 | var poseMatrix = mat4.create(); 233 | function getViewMatrix(out, pose, view) { 234 | mat4.identity(out); 235 | 236 | mat4.translate(out, out, playerMover.position); 237 | if (!pose) 238 | mat4.translate(out, out, [0, 0, playerHeight]); 239 | mat4.rotateZ(out, out, -zAngle); 240 | mat4.rotateX(out, out, Math.PI/2); 241 | 242 | if (view) { 243 | /*var orientation = pose.orientation; 244 | var position = pose.position; 245 | if (!orientation) { orientation = [0, 0, 0, 1]; } 246 | if (!position) { position = [0, 0, 0]; } 247 | 248 | mat4.fromRotationTranslation(poseMatrix, orientation, [ 249 | position[0] * vrIPDScale, 250 | position[1] * vrIPDScale, 251 | position[2] * vrIPDScale 252 | ]);*/ 253 | /*if (vrDisplay.stageParameters) { 254 | mat4.multiply(poseMatrix, vrDisplay.stageParameters.sittingToStandingTransform, out); 255 | }*/ 256 | 257 | /*if (eye) { 258 | mat4.translate(poseMatrix, poseMatrix, [eye.offset[0] * vrIPDScale, eye.offset[1] * vrIPDScale, eye.offset[2] * vrIPDScale]); 259 | }*/ 260 | 261 | mat4.scale(poseMatrix, view.transform.inverse.matrix, [1/xrIPDScale, 1/xrIPDScale, 1/xrIPDScale]); 262 | mat4.invert(poseMatrix, poseMatrix); 263 | mat4.multiply(out, out, poseMatrix); 264 | } 265 | 266 | mat4.rotateX(out, out, -xAngle); 267 | 268 | mat4.invert(out, out); 269 | } 270 | 271 | // Draw a single frame 272 | function drawFrame(gl) { 273 | // Clear back buffer but not color buffer (we expect the entire scene to be overwritten) 274 | gl.depthMask(true); 275 | 276 | if(!map || !playerMover) { return; } 277 | 278 | if (!xrPose) { 279 | // Standard rendering path. 280 | 281 | // Matrix setup 282 | getViewMatrix(leftViewMat); 283 | 284 | gl.bindFramebuffer(gl.FRAMEBUFFER, null); 285 | gl.clear(gl.DEPTH_BUFFER_BIT); 286 | 287 | // Here's where all the magic happens... 288 | map.draw(leftViewMat, projMat); 289 | } else { 290 | // WebXR rendering path. 291 | 292 | // If the number of views has changed since the last frame the rebuild the 293 | // list. 294 | if (xrViews.length != xrPose.views.length) { 295 | xrViews = []; 296 | } 297 | 298 | for (var v = 0; v < xrPose.views.length; ++v) { 299 | if (xrViews.length <= v) { 300 | xrViews.push({ 301 | viewMat: mat4.create(), 302 | projMat: null, 303 | viewport: null, 304 | }); 305 | } 306 | var view = xrViews[v]; 307 | getViewMatrix(view.viewMat, xrPose, xrPose.views[v]); 308 | view.projMat = xrPose.views[v].projectionMatrix; 309 | view.viewport = xrSession.renderState.baseLayer.getViewport(xrPose.views[v]); 310 | } 311 | 312 | gl.bindFramebuffer(gl.FRAMEBUFFER, xrSession.renderState.baseLayer.framebuffer); 313 | gl.clear(gl.DEPTH_BUFFER_BIT); 314 | 315 | map.drawViews(xrViews); 316 | } 317 | } 318 | 319 | var pressed = new Array(128); 320 | var cameraMat = mat4.create(); 321 | 322 | function moveLookLocked(xDelta, yDelta) { 323 | zAngle += xDelta*0.0025; 324 | while (zAngle < 0) 325 | zAngle += Math.PI*2; 326 | while (zAngle >= Math.PI*2) 327 | zAngle -= Math.PI*2; 328 | 329 | if (!isXRPresenting()) { 330 | xAngle += yDelta*0.0025; 331 | while (xAngle < -Math.PI*0.5) 332 | xAngle = -Math.PI*0.5; 333 | while (xAngle > Math.PI*0.5) 334 | xAngle = Math.PI*0.5; 335 | } 336 | } 337 | 338 | function filterDeadzone(value) { 339 | return Math.abs(value) > 0.35 ? value : 0; 340 | } 341 | 342 | var xrOrientation = quat.create(); 343 | var xrEuler = vec3.create(); 344 | function moveViewOriented(dir, frameTime) { 345 | if(dir[0] !== 0 || dir[1] !== 0 || dir[2] !== 0) { 346 | mat4.identity(cameraMat); 347 | if (xrPose) { 348 | mat4.getRotation(xrOrientation, xrPose.transform.matrix); 349 | eulerFromQuaternion(xrEuler, xrOrientation, 'YXZ'); 350 | mat4.rotateZ(cameraMat, cameraMat, zAngle - xrEuler[1]); 351 | } else { 352 | mat4.rotateZ(cameraMat, cameraMat, zAngle); 353 | } 354 | mat4.invert(cameraMat, cameraMat); 355 | 356 | vec3.transformMat4(dir, dir, cameraMat); 357 | } 358 | 359 | // Send desired movement direction to the player mover for collision detection against the map 360 | playerMover.move(dir, frameTime); 361 | } 362 | 363 | function updateInput(frameTime) { 364 | if(!playerMover) { return; } 365 | 366 | var dir = [0, 0, 0]; 367 | 368 | // This is our first person movement code. It's not really pretty, but it works 369 | if(pressed['W'.charCodeAt(0)]) { 370 | dir[1] += 1; 371 | } 372 | if(pressed['S'.charCodeAt(0)]) { 373 | dir[1] -= 1; 374 | } 375 | if(pressed['A'.charCodeAt(0)]) { 376 | dir[0] -= 1; 377 | } 378 | if(pressed['D'.charCodeAt(0)]) { 379 | dir[0] += 1; 380 | } 381 | 382 | if (!xrSession) { 383 | var gamepads = []; 384 | if (navigator.getGamepads) { 385 | gamepads = navigator.getGamepads(); 386 | } else if (navigator.webkitGetGamepads) { 387 | gamepads = navigator.webkitGetGamepads(); 388 | } 389 | 390 | for (var i = 0; i < gamepads.length; ++i) { 391 | var pad = gamepads[i]; 392 | if(pad) { 393 | dir[0] += filterDeadzone(pad.axes[0]); 394 | dir[1] -= filterDeadzone(pad.axes[1]); 395 | 396 | moveLookLocked( 397 | filterDeadzone(pad.axes[2]) * 25.0, 398 | filterDeadzone(pad.axes[3]) * 25.0 399 | ); 400 | 401 | for(var j = 0; j < Math.min(pad.buttons.length, 4); ++j) { 402 | var button = pad.buttons[j]; 403 | if (typeof(button) == "number" && button == 1.0) { 404 | playerMover.jump(); 405 | } else if (button.pressed) { 406 | playerMover.jump(); 407 | } 408 | } 409 | } 410 | } 411 | } 412 | 413 | moveViewOriented(dir, frameTime); 414 | } 415 | 416 | // Set up event handling 417 | function initEvents() { 418 | var movingModel = false; 419 | var lastX = 0; 420 | var lastY = 0; 421 | var lastMoveX = 0; 422 | var lastMoveY = 0; 423 | var viewport = document.getElementById("viewport"); 424 | var viewportFrame = document.getElementById("viewport-frame"); 425 | 426 | document.addEventListener("keydown", function(event) { 427 | if(event.keyCode == 32 && !pressed[32]) { 428 | playerMover.jump(); 429 | } 430 | pressed[event.keyCode] = true; 431 | if ((event.keyCode == 'W'.charCodeAt(0) || 432 | event.keyCode == 'S'.charCodeAt(0) || 433 | event.keyCode == 'A'.charCodeAt(0) || 434 | event.keyCode == 'D'.charCodeAt(0) || 435 | event.keyCode == 32) && !event.ctrlKey) { 436 | event.preventDefault(); 437 | } 438 | }, false); 439 | 440 | document.addEventListener("keypress", function(event) { 441 | if(event.charCode == 'R'.charCodeAt(0) || event.charCode == 'r'.charCodeAt(0)) { 442 | respawnPlayer(-1); 443 | } 444 | }, false); 445 | 446 | document.addEventListener("keyup", function(event) { 447 | pressed[event.keyCode] = false; 448 | }, false); 449 | 450 | function startLook(x, y) { 451 | movingModel = true; 452 | 453 | lastX = x; 454 | lastY = y; 455 | } 456 | 457 | function endLook() { 458 | movingModel = false; 459 | } 460 | 461 | function moveLook(x, y) { 462 | var xDelta = x - lastX; 463 | var yDelta = y - lastY; 464 | lastX = x; 465 | lastY = y; 466 | 467 | if (movingModel) { 468 | moveLookLocked(xDelta, yDelta); 469 | } 470 | } 471 | 472 | function startMove(x, y) { 473 | lastMoveX = x; 474 | lastMoveY = y; 475 | } 476 | 477 | function moveUpdate(x, y, frameTime) { 478 | var xDelta = x - lastMoveX; 479 | var yDelta = y - lastMoveY; 480 | lastMoveX = x; 481 | lastMoveY = y; 482 | 483 | var dir = [xDelta, yDelta * -1, 0]; 484 | 485 | moveViewOriented(dir, frameTime*2); 486 | } 487 | 488 | viewport.addEventListener("click", function(event) { 489 | viewport.requestPointerLock(); 490 | }, false); 491 | 492 | // Mouse handling code 493 | // When the mouse is pressed it rotates the players view 494 | viewport.addEventListener("mousedown", function(event) { 495 | if(event.which == 1) { 496 | startLook(event.pageX, event.pageY); 497 | } 498 | }, false); 499 | viewport.addEventListener("mouseup", function(event) { 500 | endLook(); 501 | }, false); 502 | viewportFrame.addEventListener("mousemove", function(event) { 503 | if(document.pointerLockElement) { 504 | moveLookLocked(event.movementX, event.movementY); 505 | } else { 506 | moveLook(event.pageX, event.pageY); 507 | } 508 | }, false); 509 | 510 | // Touch handling code 511 | viewport.addEventListener('touchstart', function(event) { 512 | var touches = event.touches; 513 | switch(touches.length) { 514 | case 1: // Single finger looks around 515 | startLook(touches[0].pageX, touches[0].pageY); 516 | break; 517 | case 2: // Two fingers moves 518 | startMove(touches[0].pageX, touches[0].pageY); 519 | break; 520 | case 3: // Three finger tap jumps 521 | playerMover.jump(); 522 | break; 523 | default: 524 | return; 525 | } 526 | event.stopPropagation(); 527 | event.preventDefault(); 528 | }, false); 529 | viewport.addEventListener('touchend', function(event) { 530 | endLook(); 531 | return false; 532 | }, false); 533 | viewport.addEventListener('touchmove', function(event) { 534 | var touches = event.touches; 535 | switch(touches.length) { 536 | case 1: 537 | moveLook(touches[0].pageX, touches[0].pageY); 538 | break; 539 | case 2: 540 | moveUpdate(touches[0].pageX, touches[0].pageY, 16); 541 | break; 542 | default: 543 | return; 544 | } 545 | event.stopPropagation(); 546 | event.preventDefault(); 547 | }, false); 548 | } 549 | 550 | // Utility function that tests a list of webgl contexts and returns when one can be created 551 | // Hopefully this future-proofs us a bit 552 | function getAvailableContext(canvas, contextList) { 553 | if (canvas.getContext) { 554 | for(var i = 0; i < contextList.length; ++i) { 555 | try { 556 | var context = canvas.getContext(contextList[i], { 557 | antialias:false, 558 | xrCompatible: true 559 | }); 560 | if(context !== null) 561 | return context; 562 | } catch(ex) { } 563 | } 564 | } 565 | return null; 566 | } 567 | 568 | var rafCallback = null; 569 | 570 | function renderLoop(gl, stats) { 571 | var startTime = new Date().getTime(); 572 | var lastTimestamp = startTime; 573 | var lastFps = startTime; 574 | 575 | var frameId = 0; 576 | 577 | function onRequestedFrame(t, frame){ 578 | timestamp = new Date().getTime(); 579 | 580 | if (xrSession) { 581 | xrSession.requestAnimationFrame(onRequestedFrame); 582 | } else { 583 | window.requestAnimationFrame(onRequestedFrame); 584 | } 585 | 586 | if (xrSession && frame) { 587 | xrPose = frame.getViewerPose(xrReferenceSpace); 588 | } else { 589 | xrPose = null; 590 | } 591 | 592 | frameId++; 593 | if (SKIP_FRAMES != 0 && frameId % SKIP_FRAMES != 0) 594 | return; 595 | 596 | stats.begin(); 597 | 598 | onFrame(gl, { 599 | timestamp: timestamp, 600 | elapsed: timestamp - startTime, 601 | frameTime: timestamp - lastTimestamp 602 | }); 603 | 604 | stats.end(); 605 | } 606 | window.requestAnimationFrame(onRequestedFrame); 607 | rafCallback = onRequestedFrame; 608 | } 609 | 610 | function main() { 611 | var stats = new Stats(); 612 | document.getElementById("viewport-frame").appendChild( stats.domElement ); 613 | 614 | var canvas = document.getElementById("viewport"); 615 | 616 | // Get the GL Context (try 'webgl2' first, then fallback) 617 | var gl = getAvailableContext(canvas, ['webgl2', 'webgl', 'experimental-webgl']); 618 | 619 | onResize = function() { 620 | if (!isXRPresenting()) { 621 | var devicePixelRatio = window.devicePixelRatio || 1; 622 | 623 | if(document.fullscreenElement) { 624 | canvas.width = screen.width * devicePixelRatio; 625 | canvas.height = screen.height * devicePixelRatio; 626 | } else { 627 | canvas.width = canvas.clientWidth * devicePixelRatio; 628 | canvas.height = canvas.clientHeight * devicePixelRatio; 629 | } 630 | 631 | gl.viewport(0, 0, canvas.width, canvas.height); 632 | mat4.perspective(projMat, 45.0, canvas.width/canvas.height, 1.0, 4096.0); 633 | } 634 | } 635 | 636 | if(!gl) { 637 | document.getElementById('viewport-frame').style.display = 'none'; 638 | document.getElementById('webgl-error').style.display = 'block'; 639 | } else { 640 | document.getElementById('viewport-info').style.display = 'block'; 641 | initEvents(); 642 | initGL(gl, canvas); 643 | renderLoop(gl, stats); 644 | } 645 | 646 | onResize(); 647 | window.addEventListener("resize", onResize, false); 648 | 649 | var showFPS = document.getElementById("showFPS"); 650 | showFPS.addEventListener("change", function() { 651 | stats.domElement.style.display = showFPS.checked ? "block" : "none"; 652 | }); 653 | 654 | /*var playMusic = document.getElementById("playMusic"); 655 | playMusic.addEventListener("change", function() { 656 | if(map) { 657 | map.playMusic(playMusic.checked); 658 | } 659 | });*/ 660 | 661 | // Handle fullscreen transition 662 | var viewportFrame = document.getElementById("viewport-frame"); 663 | var viewport = document.getElementById("viewport"); 664 | document.addEventListener("fullscreenchange", function() { 665 | if(document.fullscreenElement) { 666 | viewport.requestPointerLock(); // Attempt to lock the mouse automatically on fullscreen 667 | } 668 | onResize(); 669 | }, false); 670 | 671 | // Fullscreen 672 | function goFullscreen() { 673 | viewportFrame.requestFullScreen(); 674 | } 675 | var fullscreenButton = document.getElementById('fullscreenBtn'); 676 | var mobileFullscreenBtn = document.getElementById("mobileFullscreenBtn"); 677 | fullscreenButton.addEventListener('click', goFullscreen, false); 678 | mobileFullscreenBtn.addEventListener('click', goFullscreen, false); 679 | 680 | // XR 681 | function presentXR() { 682 | if (xrSession) { 683 | xrSession.end(); 684 | } else { 685 | xAngle = 0.0; 686 | navigator.xr.requestSession('immersive-vr', { 687 | optionalFeatures: ['local-floor'] 688 | }).then(function(session) { 689 | session.addEventListener('end', function() { 690 | xrSession = null; 691 | xrPose = null; 692 | onResize(); 693 | }); 694 | 695 | session.addEventListener('select', function(evt) { 696 | // ? 697 | }); 698 | 699 | session.addEventListener('selectstart', function(evt) { 700 | pressed['W'.charCodeAt(0)] = true; 701 | }); 702 | 703 | session.addEventListener('selectend', function(evt) { 704 | pressed['W'.charCodeAt(0)] = false; 705 | }); 706 | 707 | session.requestReferenceSpace('local-floor').then(function(refSpace) { 708 | xrReferenceSpace = refSpace; 709 | 710 | session.updateRenderState({ 711 | depthNear: 1.0, 712 | depthFar: 4096.0, 713 | baseLayer: new XRWebGLLayer(session, gl) 714 | }); 715 | xrSession = session; 716 | xrSession.requestAnimationFrame(rafCallback); 717 | }); 718 | }); 719 | } 720 | } 721 | var vrBtn = document.getElementById("vrBtn"); 722 | var mobileVrBtn = document.getElementById("mobileVrBtn"); 723 | vrBtn.addEventListener("click", presentXR, false); 724 | mobileVrBtn.addEventListener("click", presentXR, false); 725 | 726 | } 727 | 728 | // Fire this once the page is loaded up 729 | window.addEventListener("load", function() { 730 | function OnVRSupported() { 731 | var vrToggle = document.getElementById("vrToggle"); 732 | vrToggle.style.display = "block"; 733 | var mobileVrBtn = document.getElementById("mobileVrBtn"); 734 | mobileVrBtn.style.display = "block"; 735 | } 736 | 737 | if (navigator.xr) { 738 | navigator.xr.supportsSession('immersive-vr').then(OnVRSupported); 739 | } 740 | 741 | main(); 742 | }); 743 | -------------------------------------------------------------------------------- /js/q3bsp.js: -------------------------------------------------------------------------------- 1 | /* 2 | * q3bsp.js - Parses Quake 3 Maps (.bsp) for use in WebGL 3 | */ 4 | 5 | /* 6 | * Copyright (c) 2009 Brandon Jones 7 | * 8 | * This software is provided 'as-is', without any express or implied 9 | * warranty. In no event will the authors be held liable for any damages 10 | * arising from the use of this software. 11 | * 12 | * Permission is granted to anyone to use this software for any purpose, 13 | * including commercial applications, and to alter it and redistribute it 14 | * freely, subject to the following restrictions: 15 | * 16 | * 1. The origin of this software must not be misrepresented; you must not 17 | * claim that you wrote the original software. If you use this software 18 | * in a product, an acknowledgment in the product documentation would be 19 | * appreciated but is not required. 20 | * 21 | * 2. Altered source versions must be plainly marked as such, and must not 22 | * be misrepresented as being the original software. 23 | * 24 | * 3. This notice may not be removed or altered from any source 25 | * distribution. 26 | */ 27 | 28 | // Constants 29 | q3bsp_vertex_stride = 56; 30 | q3bsp_sky_vertex_stride = 20; 31 | 32 | q3bsp_base_folder = 'demo_baseq3'; 33 | 34 | /* 35 | * q3bsp 36 | */ 37 | 38 | q3bsp = function(gl) { 39 | // gl initialization 40 | this.gl = gl; 41 | this.onload = null; 42 | this.onbsp = null; 43 | this.onentitiesloaded = null; 44 | 45 | var map = this; 46 | 47 | this.showLoadStatus(); 48 | 49 | // Spawn the web worker 50 | this.worker = new Worker('js/q3bsp_worker.js'); 51 | this.worker.onmessage = function(msg) { 52 | map.onMessage(msg); 53 | }; 54 | this.worker.onerror = function(msg) { 55 | console.error('Line: ' + msg.lineno + ', ' + msg.message); 56 | }; 57 | 58 | // Map elements 59 | this.skyboxBuffer = null; 60 | this.skyboxIndexBuffer = null; 61 | this.skyboxIndexCount = 0; 62 | this.skyboxMat = mat4.create(); 63 | 64 | this.vertexBuffer = null; 65 | this.indexBuffer = null; 66 | this.indexCount = 0; 67 | this.lightmap = q3glshader.createSolidTexture(gl, [255,255,255,255]); 68 | this.surfaces = null; 69 | this.shaders = {}; 70 | 71 | this.highlighted = null; 72 | 73 | // Sorted draw elements 74 | this.skyShader = null; 75 | this.unshadedSurfaces = []; 76 | this.defaultSurfaces = []; 77 | this.modelSurfaces = []; 78 | this.effectSurfaces = []; 79 | 80 | // BSP Elements 81 | this.bspTree = null; 82 | 83 | // Effect elements 84 | this.startTime = new Date().getTime(); 85 | this.bgMusic = null; 86 | }; 87 | 88 | q3bsp.prototype.highlightShader = function(name) { 89 | this.highlighted = name; 90 | }; 91 | 92 | q3bsp.prototype.playMusic = function(play) { 93 | if(!this.bgMusic) { return; } 94 | 95 | if(play) { 96 | this.bgMusic.play(); 97 | } else { 98 | this.bgMusic.pause(); 99 | } 100 | }; 101 | 102 | q3bsp.prototype.onMessage = function(msg) { 103 | switch(msg.data.type) { 104 | case 'entities': 105 | this.entities = msg.data.entities; 106 | this.processEntities(this.entities); 107 | break; 108 | case 'geometry': 109 | this.buildBuffers(msg.data.vertices, msg.data.indices); 110 | this.surfaces = msg.data.surfaces; 111 | this.bindShaders(); 112 | break; 113 | case 'lightmap': 114 | this.buildLightmaps(msg.data.size, msg.data.lightmaps); 115 | break; 116 | case 'shaders': 117 | this.buildShaders(msg.data.shaders); 118 | break; 119 | case 'bsp': 120 | this.bspTree = new q3bsptree(msg.data.bsp); 121 | if(this.onbsp) { 122 | this.onbsp(this.bspTree); 123 | } 124 | this.clearLoadStatus(); 125 | break; 126 | case 'visibility': 127 | this.setVisibility(msg.data.visibleSurfaces); 128 | break; 129 | case 'status': 130 | this.onLoadStatus(msg.data.message); 131 | break; 132 | default: 133 | throw 'Unexpected message type: ' + msg.data.type; 134 | } 135 | }; 136 | 137 | q3bsp.prototype.showLoadStatus = function() { 138 | // Yeah, this shouldn't be hardcoded in here 139 | var loading = document.getElementById('loading'); 140 | loading.style.display = 'block'; 141 | }; 142 | 143 | q3bsp.prototype.onLoadStatus = function(message) { 144 | // Yeah, this shouldn't be hardcoded in here 145 | var loading = document.getElementById('loading'); 146 | loading.innerHTML = message; 147 | }; 148 | 149 | q3bsp.prototype.clearLoadStatus = function() { 150 | // Yeah, this shouldn't be hardcoded in here 151 | var loading = document.getElementById('loading'); 152 | loading.style.display = 'none'; 153 | }; 154 | 155 | q3bsp.prototype.load = function(url, tesselationLevel) { 156 | if(!tesselationLevel) { 157 | tesselationLevel = 5; 158 | } 159 | this.worker.postMessage({ 160 | type: 'load', 161 | url: '../' + q3bsp_base_folder + '/' + url, 162 | tesselationLevel: tesselationLevel 163 | }); 164 | }; 165 | 166 | q3bsp.prototype.loadShaders = function(sources) { 167 | var map = this; 168 | 169 | for(var i = 0; i < sources.length; ++i) { 170 | sources[i] = q3bsp_base_folder + '/' + sources[i]; 171 | } 172 | 173 | q3shader.loadList(sources, function(shaders) { 174 | map.buildShaders(shaders); 175 | }); 176 | }; 177 | 178 | q3bsp.prototype.processEntities = function(entities) { 179 | if(this.onentitiesloaded) { 180 | this.onentitiesloaded(entities); 181 | } 182 | 183 | // Background music 184 | /*if(entities.worldspawn[0].music) { 185 | this.bgMusic = new Audio(q3bsp_base_folder + '/' + entities.worldspawn[0].music.replace('.wav', '.ogg')); 186 | // TODO: When can we change this to simply setting the 'loop' property? 187 | this.bgMusic.addEventListener('ended', function(){ 188 | this.currentTime = 0; 189 | }, false); 190 | this.bgMusic.play(); 191 | }*/ 192 | 193 | // It would be relatively easy to do some ambient sound processing here, but I don't really feel like 194 | // HTML5 audio is up to the task. For example, lack of reliable gapless looping makes them sound terrible! 195 | // Look into this more when browsers get with the program. 196 | /*var speakers = entities.target_speaker; 197 | for(var i = 0; i < 1; ++i) { 198 | var speaker = speakers[i]; 199 | q3bspCreateSpeaker(speaker); 200 | }*/ 201 | }; 202 | 203 | function q3bspCreateSpeaker(speaker) { 204 | speaker.audio = new Audio(q3bsp_base_folder + '/' + speaker.noise.replace('.wav', '.ogg')); 205 | 206 | // TODO: When can we change this to simply setting the 'loop' property? 207 | speaker.audio.addEventListener('ended', function(){ 208 | this.currentTime = 0; 209 | }, false); 210 | speaker.audio.play(); 211 | }; 212 | 213 | q3bsp.prototype.buildBuffers = function(vertices, indices) { 214 | var gl = this.gl; 215 | 216 | this.vertexBuffer = gl.createBuffer(); 217 | gl.bindBuffer(gl.ARRAY_BUFFER, this.vertexBuffer); 218 | gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices), gl.STATIC_DRAW); 219 | 220 | this.indexBuffer = gl.createBuffer(); 221 | gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this.indexBuffer); 222 | gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint16Array(indices), gl.STATIC_DRAW); 223 | 224 | this.indexCount = indices.length; 225 | 226 | var skyVerts = [ 227 | -128, 128, 128, 0, 0, 228 | 128, 128, 128, 1, 0, 229 | -128, -128, 128, 0, 1, 230 | 128, -128, 128, 1, 1, 231 | 232 | -128, 128, 128, 0, 1, 233 | 128, 128, 128, 1, 1, 234 | -128, 128, -128, 0, 0, 235 | 128, 128, -128, 1, 0, 236 | 237 | -128, -128, 128, 0, 0, 238 | 128, -128, 128, 1, 0, 239 | -128, -128, -128, 0, 1, 240 | 128, -128, -128, 1, 1, 241 | 242 | 128, 128, 128, 0, 0, 243 | 128, -128, 128, 0, 1, 244 | 128, 128, -128, 1, 0, 245 | 128, -128, -128, 1, 1, 246 | 247 | -128, 128, 128, 1, 0, 248 | -128, -128, 128, 1, 1, 249 | -128, 128, -128, 0, 0, 250 | -128, -128, -128, 0, 1 251 | ]; 252 | 253 | var skyIndices = [ 254 | 0, 1, 2, 255 | 1, 2, 3, 256 | 257 | 4, 5, 6, 258 | 5, 6, 7, 259 | 260 | 8, 9, 10, 261 | 9, 10, 11, 262 | 263 | 12, 13, 14, 264 | 13, 14, 15, 265 | 266 | 16, 17, 18, 267 | 17, 18, 19 268 | ]; 269 | 270 | this.skyboxBuffer = gl.createBuffer(); 271 | gl.bindBuffer(gl.ARRAY_BUFFER, this.skyboxBuffer); 272 | gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(skyVerts), gl.STATIC_DRAW); 273 | 274 | this.skyboxIndexBuffer = gl.createBuffer(); 275 | gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this.skyboxIndexBuffer); 276 | gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint16Array(skyIndices), gl.STATIC_DRAW); 277 | 278 | this.skyboxIndexCount = skyIndices.length; 279 | }; 280 | 281 | q3bsp.prototype.buildLightmaps = function(size, lightmaps) { 282 | var gl = this.gl; 283 | 284 | gl.bindTexture(gl.TEXTURE_2D, this.lightmap); 285 | gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, size, size, 0, gl.RGBA, gl.UNSIGNED_BYTE, null); 286 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR); 287 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR_MIPMAP_LINEAR); 288 | 289 | for(var i = 0; i < lightmaps.length; ++i) { 290 | gl.texSubImage2D( 291 | gl.TEXTURE_2D, 0, lightmaps[i].x, lightmaps[i].y, lightmaps[i].width, lightmaps[i].height, 292 | gl.RGBA, gl.UNSIGNED_BYTE, new Uint8Array(lightmaps[i].bytes) 293 | ); 294 | } 295 | 296 | gl.generateMipmap(gl.TEXTURE_2D); 297 | 298 | q3glshader.init(gl, this.lightmap); 299 | }; 300 | 301 | q3bsp.prototype.buildShaders = function(shaders) { 302 | var gl = this.gl; 303 | 304 | for(var i = 0; i < shaders.length; ++i) { 305 | var shader = shaders[i]; 306 | var glShader = q3glshader.build(gl, shader); 307 | this.shaders[shader.name] = glShader; 308 | } 309 | }; 310 | 311 | q3bsp.prototype.bindShaders = function() { 312 | if(!this.surfaces) { return; } 313 | 314 | if(this.onsurfaces) { 315 | this.onsurfaces(this.surfaces); 316 | } 317 | 318 | for(var i = 0; i < this.surfaces.length; ++i) { 319 | var surface = this.surfaces[i]; 320 | if(surface.elementCount === 0 || surface.shader || surface.shaderName == 'noshader') { continue; } 321 | this.unshadedSurfaces.push(surface); 322 | } 323 | 324 | var map = this; 325 | 326 | var interval = setInterval(function() { 327 | if(map.unshadedSurfaces.length === 0) { // Have we processed all surfaces? 328 | // Sort to ensure correct order of transparent objects 329 | map.effectSurfaces.sort(function(a, b) { 330 | var order = a.shader.sort - b.shader.sort; 331 | // TODO: Sort by state here to cut down on changes? 332 | return order; //(order == 0 ? 1 : order); 333 | }); 334 | 335 | clearInterval(interval); 336 | return; 337 | } 338 | 339 | var surface = map.unshadedSurfaces.shift(); 340 | 341 | var shader = map.shaders[surface.shaderName]; 342 | if(!shader) { 343 | surface.shader = q3glshader.buildDefault(map.gl, surface); 344 | if(surface.geomType == 3) { 345 | surface.shader.model = true; 346 | map.modelSurfaces.push(surface); 347 | } else { 348 | map.defaultSurfaces.push(surface); 349 | } 350 | } else { 351 | surface.shader = shader; 352 | if(shader.sky) { 353 | map.skyShader = shader; // Sky does not get pushed into effectSurfaces. It's a separate pass 354 | } else { 355 | map.effectSurfaces.push(surface); 356 | } 357 | q3glshader.loadShaderMaps(map.gl, surface, shader); 358 | } 359 | }, 10); 360 | }; 361 | 362 | // Update which portions of the map are visible based on position 363 | 364 | q3bsp.prototype.updateVisibility = function(pos) { 365 | this.worker.postMessage({ 366 | type: 'visibility', 367 | pos: pos 368 | }); 369 | }; 370 | 371 | q3bsp.prototype.setVisibility = function(visibilityList) { 372 | if(this.surfaces.length > 0) { 373 | for(var i = 0; i < this.surfaces.length; ++i) { 374 | this.surfaces[i].visible = (visibilityList[i] === true); 375 | } 376 | } 377 | }; 378 | 379 | // Draw the map 380 | 381 | q3bsp.prototype.bindShaderMatrix = function(shader, modelViewMat, projectionMat) { 382 | var gl = this.gl; 383 | 384 | // Set uniforms 385 | gl.uniformMatrix4fv(shader.uniform.modelViewMat, false, modelViewMat); 386 | gl.uniformMatrix4fv(shader.uniform.projectionMat, false, projectionMat); 387 | } 388 | 389 | q3bsp.prototype.bindShaderAttribs = function(shader) { 390 | var gl = this.gl; 391 | 392 | // Setup vertex attributes 393 | gl.enableVertexAttribArray(shader.attrib.position); 394 | gl.vertexAttribPointer(shader.attrib.position, 3, gl.FLOAT, false, q3bsp_vertex_stride, 0); 395 | 396 | if(shader.attrib.texCoord !== undefined) { 397 | gl.enableVertexAttribArray(shader.attrib.texCoord); 398 | gl.vertexAttribPointer(shader.attrib.texCoord, 2, gl.FLOAT, false, q3bsp_vertex_stride, 3*4); 399 | } 400 | 401 | if(shader.attrib.lightCoord !== undefined) { 402 | gl.enableVertexAttribArray(shader.attrib.lightCoord); 403 | gl.vertexAttribPointer(shader.attrib.lightCoord, 2, gl.FLOAT, false, q3bsp_vertex_stride, 5*4); 404 | } 405 | 406 | if(shader.attrib.normal !== undefined) { 407 | gl.enableVertexAttribArray(shader.attrib.normal); 408 | gl.vertexAttribPointer(shader.attrib.normal, 3, gl.FLOAT, false, q3bsp_vertex_stride, 7*4); 409 | } 410 | 411 | if(shader.attrib.color !== undefined) { 412 | gl.enableVertexAttribArray(shader.attrib.color); 413 | gl.vertexAttribPointer(shader.attrib.color, 4, gl.FLOAT, false, q3bsp_vertex_stride, 10*4); 414 | } 415 | } 416 | 417 | q3bsp.prototype.bindSkyMatrix = function(shader, modelViewMat, projectionMat) { 418 | var gl = this.gl; 419 | 420 | mat4.copy(this.skyboxMat, modelViewMat); 421 | // Clear out the translation components 422 | this.skyboxMat[12] = 0; 423 | this.skyboxMat[13] = 0; 424 | this.skyboxMat[14] = 0; 425 | 426 | // Set uniforms 427 | gl.uniformMatrix4fv(shader.uniform.modelViewMat, false, this.skyboxMat); 428 | gl.uniformMatrix4fv(shader.uniform.projectionMat, false, projectionMat); 429 | }; 430 | 431 | q3bsp.prototype.bindSkyAttribs = function(shader) { 432 | var gl = this.gl; 433 | 434 | // Setup vertex attributes 435 | gl.enableVertexAttribArray(shader.attrib.position); 436 | gl.vertexAttribPointer(shader.attrib.position, 3, gl.FLOAT, false, q3bsp_sky_vertex_stride, 0); 437 | 438 | if(shader.attrib.texCoord !== undefined) { 439 | gl.enableVertexAttribArray(shader.attrib.texCoord); 440 | gl.vertexAttribPointer(shader.attrib.texCoord, 2, gl.FLOAT, false, q3bsp_sky_vertex_stride, 3*4); 441 | } 442 | }; 443 | 444 | q3bsp.prototype.setViewport = function(viewport) { 445 | if (viewport) { 446 | this.gl.viewport(viewport.x, viewport.y, viewport.width, viewport.height); 447 | } 448 | } 449 | 450 | q3bsp.prototype.draw = function(viewMat, projMat) { 451 | this.drawViews([{viewMat: viewMat, projMat: projMat}]); 452 | } 453 | 454 | q3bsp.prototype.drawViews = function(views) { 455 | var viewCount = views.length; 456 | if (viewCount == 1 && views[0].viewport) { 457 | this.setViewport(views[0].viewport); 458 | } 459 | 460 | if(this.vertexBuffer === null || this.indexBuffer === null) { return; } // Not ready to draw yet 461 | 462 | var gl = this.gl; // Easier to type and potentially a bit faster 463 | 464 | // Seconds passed since map was initialized 465 | var time = (new Date().getTime() - this.startTime)/1000.0; 466 | var i = 0; 467 | 468 | // Loop through all shaders, drawing all surfaces associated with them 469 | if(this.surfaces.length > 0) { 470 | 471 | // If we have a skybox, render it first 472 | if(this.skyShader) { 473 | // SkyBox Buffers 474 | gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this.skyboxIndexBuffer); 475 | gl.bindBuffer(gl.ARRAY_BUFFER, this.skyboxBuffer); 476 | 477 | // Render Skybox 478 | if(q3glshader.setShader(gl, this.skyShader)) { 479 | for(var j = 0; j < this.skyShader.stages.length; ++j) { 480 | var stage = this.skyShader.stages[j]; 481 | 482 | var shaderProgram = q3glshader.setShaderStage(gl, this.skyShader, stage, time); 483 | if(!shaderProgram) { continue; } 484 | this.bindSkyAttribs(shaderProgram); 485 | 486 | // Draw Sky geometry 487 | for (var v = 0; v < viewCount; ++v) { 488 | if (viewCount > 1) 489 | this.setViewport(views[v].viewport); 490 | this.bindSkyMatrix(shaderProgram, views[v].viewMat, views[v].projMat); 491 | gl.drawElements(gl.TRIANGLES, this.skyboxIndexCount, gl.UNSIGNED_SHORT, 0); 492 | } 493 | } 494 | } 495 | } 496 | 497 | // Map Geometry buffers 498 | gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this.indexBuffer); 499 | gl.bindBuffer(gl.ARRAY_BUFFER, this.vertexBuffer); 500 | 501 | // Default shader surfaces (can bind shader once and draw all of them very quickly) 502 | if(this.defaultSurfaces.length > 0 || this.unshadedSurfaces.length > 0) { 503 | // Setup State 504 | var shader = q3glshader.defaultShader; 505 | q3glshader.setShader(gl, shader); 506 | var shaderProgram = q3glshader.setShaderStage(gl, shader, shader.stages[0], time); 507 | this.bindShaderAttribs(shaderProgram); 508 | 509 | for (var v = 0; v < viewCount; ++v) { 510 | if (viewCount > 1) 511 | this.setViewport(views[v].viewport); 512 | this.bindShaderMatrix(shaderProgram, views[v].viewMat, views[v].projMat); 513 | 514 | gl.activeTexture(gl.TEXTURE0); 515 | gl.bindTexture(gl.TEXTURE_2D, q3glshader.defaultTexture); 516 | for(i = 0; i < this.unshadedSurfaces.length; ++i) { 517 | var surface = this.unshadedSurfaces[i]; 518 | gl.drawElements(gl.TRIANGLES, surface.elementCount, gl.UNSIGNED_SHORT, surface.indexOffset); 519 | } 520 | 521 | for(i = 0; i < this.defaultSurfaces.length; ++i) { 522 | var surface = this.defaultSurfaces[i]; 523 | var stage = surface.shader.stages[0]; 524 | gl.bindTexture(gl.TEXTURE_2D, stage.texture); 525 | gl.drawElements(gl.TRIANGLES, surface.elementCount, gl.UNSIGNED_SHORT, surface.indexOffset); 526 | } 527 | } 528 | } 529 | 530 | // Model shader surfaces (can bind shader once and draw all of them very quickly) 531 | if(this.modelSurfaces.length > 0) { 532 | // Setup State 533 | var shader = this.modelSurfaces[0].shader; 534 | q3glshader.setShader(gl, shader); 535 | var shaderProgram = q3glshader.setShaderStage(gl, shader, shader.stages[0], time); 536 | this.bindShaderAttribs(shaderProgram); 537 | gl.activeTexture(gl.TEXTURE0); 538 | 539 | for (var v = 0; v < viewCount; ++v) { 540 | if (viewCount > 1) 541 | this.setViewport(views[v].viewport); 542 | this.bindShaderMatrix(shaderProgram, views[v].viewMat, views[v].projMat); 543 | for(i = 0; i < this.modelSurfaces.length; ++i) { 544 | var surface = this.modelSurfaces[i]; 545 | var stage = surface.shader.stages[0]; 546 | gl.bindTexture(gl.TEXTURE_2D, stage.texture); 547 | gl.drawElements(gl.TRIANGLES, surface.elementCount, gl.UNSIGNED_SHORT, surface.indexOffset); 548 | } 549 | } 550 | } 551 | 552 | // Effect surfaces 553 | for(var i = 0; i < this.effectSurfaces.length; ++i) { 554 | var surface = this.effectSurfaces[i]; 555 | if(surface.elementCount == 0 || surface.visible !== true) { continue; } 556 | 557 | // Bind the surface shader 558 | var shader = surface.shader; 559 | 560 | if(this.highlighted && this.highlighted == surface.shaderName) { 561 | shader = q3glshader.defaultShader; 562 | } 563 | 564 | if(!q3glshader.setShader(gl, shader)) { continue; } 565 | 566 | for(var j = 0; j < shader.stages.length; ++j) { 567 | var stage = shader.stages[j]; 568 | 569 | var shaderProgram = q3glshader.setShaderStage(gl, shader, stage, time); 570 | if(!shaderProgram) { continue; } 571 | this.bindShaderAttribs(shaderProgram); 572 | 573 | for (var v = 0; v < viewCount; ++v) { 574 | if (viewCount > 1) 575 | this.setViewport(views[v].viewport); 576 | this.bindShaderMatrix(shaderProgram, views[v].viewMat, views[v].projMat); 577 | // Draw all geometry that uses this textures 578 | gl.drawElements(gl.TRIANGLES, surface.elementCount, gl.UNSIGNED_SHORT, surface.indexOffset); 579 | } 580 | } 581 | } 582 | } 583 | }; 584 | 585 | 586 | 587 | // 588 | // BSP Tree Collision Detection 589 | // 590 | q3bsptree = function(bsp) { 591 | this.bsp = bsp; 592 | }; 593 | 594 | q3bsptree.prototype.trace = function(start, end, radius) { 595 | var output = { 596 | allSolid: false, 597 | startSolid: false, 598 | fraction: 1.0, 599 | endPos: end, 600 | plane: null 601 | }; 602 | 603 | if(!this.bsp) { return output; } 604 | if(!radius) { radius = 0; } 605 | 606 | this.traceNode(0, 0, 1, start, end, radius, output); 607 | 608 | if(output.fraction != 1.0) { // collided with something 609 | for (var i = 0; i < 3; i++) { 610 | output.endPos[i] = start[i] + output.fraction * (end[i] - start[i]); 611 | } 612 | } 613 | 614 | return output; 615 | }; 616 | 617 | var q3bsptree_trace_offset = 0.03125; 618 | 619 | q3bsptree.prototype.traceNode = function(nodeIdx, startFraction, endFraction, start, end, radius, output) { 620 | if (nodeIdx < 0) { // Leaf node? 621 | var leaf = this.bsp.leaves[-(nodeIdx + 1)]; 622 | for (var i = 0; i < leaf.leafBrushCount; i++) { 623 | var brush = this.bsp.brushes[this.bsp.leafBrushes[leaf.leafBrush + i]]; 624 | var surface = this.bsp.surfaces[brush.shader]; 625 | if (brush.brushSideCount > 0 && surface.contents & 1) { 626 | this.traceBrush(brush, start, end, radius, output); 627 | } 628 | } 629 | return; 630 | } 631 | 632 | // Tree node 633 | var node = this.bsp.nodes[nodeIdx]; 634 | var plane = this.bsp.planes[node.plane]; 635 | 636 | var startDist = vec3.dot(plane.normal, start) - plane.distance; 637 | var endDist = vec3.dot(plane.normal, end) - plane.distance; 638 | 639 | if (startDist >= radius && endDist >= radius) { 640 | this.traceNode(node.children[0], startFraction, endFraction, start, end, radius, output ); 641 | } else if (startDist < -radius && endDist < -radius) { 642 | this.traceNode(node.children[1], startFraction, endFraction, start, end, radius, output ); 643 | } else { 644 | var side; 645 | var fraction1, fraction2, middleFraction; 646 | var middle = [0, 0, 0]; 647 | 648 | if (startDist < endDist) { 649 | side = 1; // back 650 | var iDist = 1 / (startDist - endDist); 651 | fraction1 = (startDist - radius + q3bsptree_trace_offset) * iDist; 652 | fraction2 = (startDist + radius + q3bsptree_trace_offset) * iDist; 653 | } else if (startDist > endDist) { 654 | side = 0; // front 655 | var iDist = 1 / (startDist - endDist); 656 | fraction1 = (startDist + radius + q3bsptree_trace_offset) * iDist; 657 | fraction2 = (startDist - radius - q3bsptree_trace_offset) * iDist; 658 | } else { 659 | side = 0; // front 660 | fraction1 = 1; 661 | fraction2 = 0; 662 | } 663 | 664 | if (fraction1 < 0) fraction1 = 0; 665 | else if (fraction1 > 1) fraction1 = 1; 666 | if (fraction2 < 0) fraction2 = 0; 667 | else if (fraction2 > 1) fraction2 = 1; 668 | 669 | middleFraction = startFraction + (endFraction - startFraction) * fraction1; 670 | 671 | for (var i = 0; i < 3; i++) { 672 | middle[i] = start[i] + fraction1 * (end[i] - start[i]); 673 | } 674 | 675 | this.traceNode(node.children[side], startFraction, middleFraction, start, middle, radius, output ); 676 | 677 | middleFraction = startFraction + (endFraction - startFraction) * fraction2; 678 | 679 | for (var i = 0; i < 3; i++) { 680 | middle[i] = start[i] + fraction2 * (end[i] - start[i]); 681 | } 682 | 683 | this.traceNode(node.children[side===0?1:0], middleFraction, endFraction, middle, end, radius, output ); 684 | } 685 | }; 686 | 687 | q3bsptree.prototype.traceBrush = function(brush, start, end, radius, output) { 688 | var startFraction = -1; 689 | var endFraction = 1; 690 | var startsOut = false; 691 | var endsOut = false; 692 | var collisionPlane = null; 693 | 694 | for (var i = 0; i < brush.brushSideCount; i++) { 695 | var brushSide = this.bsp.brushSides[brush.brushSide + i]; 696 | var plane = this.bsp.planes[brushSide.plane]; 697 | 698 | var startDist = vec3.dot( start, plane.normal ) - (plane.distance + radius); 699 | var endDist = vec3.dot( end, plane.normal ) - (plane.distance + radius); 700 | 701 | if (startDist > 0) startsOut = true; 702 | if (endDist > 0) endsOut = true; 703 | 704 | // make sure the trace isn't completely on one side of the brush 705 | if (startDist > 0 && endDist > 0) { return; } 706 | if (startDist <= 0 && endDist <= 0) { continue; } 707 | 708 | if (startDist > endDist) { // line is entering into the brush 709 | var fraction = (startDist - q3bsptree_trace_offset) / (startDist - endDist); 710 | if (fraction > startFraction) { 711 | startFraction = fraction; 712 | collisionPlane = plane; 713 | } 714 | } else { // line is leaving the brush 715 | var fraction = (startDist + q3bsptree_trace_offset) / (startDist - endDist); 716 | if (fraction < endFraction) 717 | endFraction = fraction; 718 | } 719 | } 720 | 721 | if (startsOut === false) { 722 | output.startSolid = true; 723 | if (endsOut === false) 724 | output.allSolid = true; 725 | return; 726 | } 727 | 728 | if (startFraction < endFraction) { 729 | if (startFraction > -1 && startFraction < output.fraction) { 730 | output.plane = collisionPlane; 731 | if (startFraction < 0) 732 | startFraction = 0; 733 | output.fraction = startFraction; 734 | } 735 | } 736 | 737 | return; 738 | }; -------------------------------------------------------------------------------- /js/q3bsp_worker.js: -------------------------------------------------------------------------------- 1 | /* 2 | * q3bsp_worker.js - Parses Quake 3 Maps (.bsp) for use in WebGL 3 | * This file is the threaded backend that does the main parsing and processing 4 | */ 5 | 6 | /* 7 | * Copyright (c) 2009 Brandon Jones 8 | * 9 | * This software is provided 'as-is', without any express or implied 10 | * warranty. In no event will the authors be held liable for any damages 11 | * arising from the use of this software. 12 | * 13 | * Permission is granted to anyone to use this software for any purpose, 14 | * including commercial applications, and to alter it and redistribute it 15 | * freely, subject to the following restrictions: 16 | * 17 | * 1. The origin of this software must not be misrepresented; you must not 18 | * claim that you wrote the original software. If you use this software 19 | * in a product, an acknowledgment in the product documentation would be 20 | * appreciated but is not required. 21 | * 22 | * 2. Altered source versions must be plainly marked as such, and must not 23 | * be misrepresented as being the original software. 24 | * 25 | * 3. This notice may not be removed or altered from any source 26 | * distribution. 27 | */ 28 | 29 | importScripts('./util/binary-file.js'); 30 | importScripts('./util/gl-matrix-min.js'); 31 | 32 | onmessage = function(msg) { 33 | switch(msg.data.type) { 34 | case 'load': 35 | q3bsp.load(msg.data.url, msg.data.tesselationLevel, function() { 36 | // Fallback to account for Opera handling URLs in a worker 37 | // differently than other browsers. 38 | q3bsp.load("../" + msg.data.url, msg.data.tesselationLevel); 39 | }); 40 | break; 41 | case 'loadShaders': 42 | q3shader.loadList(msg.data.sources); 43 | break; 44 | case 'trace': 45 | q3bsp.trace(msg.data.traceId, msg.data.start, msg.data.end, msg.data.radius, msg.data.slide); 46 | break; 47 | case 'visibility': 48 | q3bsp.buildVisibleList(q3bsp.getLeaf(msg.data.pos)); 49 | break; 50 | default: 51 | throw 'Unexpected message type: ' + msg.data; 52 | } 53 | }; 54 | 55 | // BSP Elements 56 | var planes, nodes, leaves, faces; 57 | var brushes, brushSides; 58 | var leafFaces, leafBrushes; 59 | var visBuffer, visSize; 60 | var shaders; // This needs to be kept here for collision detection (indicates non-solid surfaces) 61 | 62 | q3bsp = {}; 63 | 64 | q3bsp.load = function(url, tesselationLevel, errorCallback) { 65 | var request = new XMLHttpRequest(); 66 | 67 | request.addEventListener("load", function () { 68 | q3bsp.parse(new BinaryFile(request.responseText), tesselationLevel); 69 | }, false); 70 | 71 | request.open('GET', url, true); 72 | request.overrideMimeType('text/plain; charset=x-user-defined'); 73 | request.setRequestHeader('Content-Type', 'text/plain'); 74 | request.send(null); 75 | }; 76 | 77 | // Parses the BSP file 78 | q3bsp.parse = function(src, tesselationLevel) { 79 | postMessage({ 80 | type: 'status', 81 | message: 'Map downloaded, parsing level geometry...' 82 | }); 83 | 84 | var header = q3bsp.readHeader(src); 85 | 86 | // Check for appropriate format 87 | if(header.tag != 'IBSP' || header.version != 46) { 88 | postMessage({ 89 | type: 'status', 90 | message: 'Incompatible BSP version.' 91 | }); 92 | 93 | return; 94 | } 95 | 96 | // Read map entities 97 | q3bsp.readEntities(header.lumps[0], src); 98 | 99 | // Load visual map components 100 | shaders = q3bsp.readShaders(header.lumps[1], src); 101 | var lightmaps = q3bsp.readLightmaps(header.lumps[14], src); 102 | var verts = q3bsp.readVerts(header.lumps[10], src); 103 | var meshVerts = q3bsp.readMeshVerts(header.lumps[11], src); 104 | faces = q3bsp.readFaces(header.lumps[13], src); 105 | 106 | q3bsp.compileMap(verts, faces, meshVerts, lightmaps, shaders, tesselationLevel); 107 | 108 | postMessage({ 109 | type: 'status', 110 | message: 'Geometry compiled, parsing collision tree...' 111 | }); 112 | 113 | // Load bsp components 114 | planes = q3bsp.readPlanes(header.lumps[2], src); 115 | nodes = q3bsp.readNodes(header.lumps[3], src); 116 | leaves = q3bsp.readLeaves(header.lumps[4], src); 117 | leafFaces = q3bsp.readLeafFaces(header.lumps[5], src); 118 | leafBrushes = q3bsp.readLeafBrushes(header.lumps[6], src); 119 | brushes = q3bsp.readBrushes(header.lumps[8], src); 120 | brushSides = q3bsp.readBrushSides(header.lumps[9], src); 121 | var visData = q3bsp.readVisData(header.lumps[16], src); 122 | visBuffer = visData.buffer; 123 | visSize = visData.size; 124 | 125 | postMessage({ 126 | type: 'bsp', 127 | bsp: { 128 | planes: planes, 129 | nodes: nodes, 130 | leaves: leaves, 131 | leafFaces: leafFaces, 132 | leafBrushes: leafBrushes, 133 | brushes: brushes, 134 | brushSides: brushSides, 135 | surfaces: shaders, 136 | visBuffer: visBuffer, 137 | visSize: visSize 138 | } 139 | }); 140 | 141 | 142 | }; 143 | 144 | // Read all lump headers 145 | q3bsp.readHeader = function(src) { 146 | // Read the magic number and the version 147 | var header = { 148 | tag: src.readString(4), 149 | version: src.readULong(), 150 | lumps: [] 151 | }; 152 | 153 | // Read the lump headers 154 | for(var i = 0; i < 17; ++i) { 155 | var lump = { 156 | offset: src.readULong(), 157 | length: src.readULong() 158 | }; 159 | header.lumps.push(lump); 160 | } 161 | 162 | return header; 163 | }; 164 | 165 | // Read all entity structures 166 | q3bsp.readEntities = function(lump, src) { 167 | src.seek(lump.offset); 168 | var entities = src.readString(lump.length); 169 | 170 | var elements = { 171 | targets: {} 172 | }; 173 | 174 | entities.replace(/\{([^}]*)\}/mg, function($0, entitySrc) { 175 | var entity = { 176 | classname: 'unknown' 177 | }; 178 | entitySrc.replace(/"(.+)" "(.+)"$/mg, function($0, key, value) { 179 | switch(key) { 180 | case 'origin': 181 | value.replace(/(.+) (.+) (.+)/, function($0, x, y, z) { 182 | entity[key] = [ 183 | parseFloat(x), 184 | parseFloat(y), 185 | parseFloat(z) 186 | ]; 187 | }); 188 | break; 189 | case 'angle': 190 | entity[key] = parseFloat(value); 191 | break; 192 | default: 193 | entity[key] = value; 194 | break; 195 | } 196 | }); 197 | 198 | if(entity['targetname']) { 199 | elements.targets[entity['targetname']] = entity; 200 | } 201 | 202 | if(!elements[entity.classname]) { elements[entity.classname] = []; } 203 | elements[entity.classname].push(entity); 204 | }); 205 | 206 | // Send the compiled vertex/index data back to the render thread 207 | postMessage({ 208 | type: 'entities', 209 | entities: elements 210 | }); 211 | }; 212 | 213 | // Read all shader structures 214 | q3bsp.readShaders = function(lump, src) { 215 | var count = lump.length / 72; 216 | var elements = []; 217 | 218 | src.seek(lump.offset); 219 | for(var i = 0; i < count; ++i) { 220 | var shader = { 221 | shaderName: src.readString(64), 222 | flags: src.readLong(), 223 | contents: src.readLong(), 224 | shader: null, 225 | faces: [], 226 | indexOffset: 0, 227 | elementCount: 0, 228 | visible: true 229 | }; 230 | 231 | elements.push(shader); 232 | } 233 | 234 | return elements; 235 | }; 236 | 237 | // Scale up an RGB color 238 | q3bsp.brightnessAdjust = function(color, factor) { 239 | var scale = 1.0, temp = 0.0; 240 | 241 | color[0] *= factor; 242 | color[1] *= factor; 243 | color[2] *= factor; 244 | 245 | if(color[0] > 255 && (temp = 255/color[0]) < scale) { scale = temp; } 246 | if(color[1] > 255 && (temp = 255/color[1]) < scale) { scale = temp; } 247 | if(color[2] > 255 && (temp = 255/color[2]) < scale) { scale = temp; } 248 | 249 | color[0] *= scale; 250 | color[1] *= scale; 251 | color[2] *= scale; 252 | 253 | return color; 254 | }; 255 | 256 | q3bsp.brightnessAdjustVertex = function(color, factor) { 257 | var scale = 1.0, temp = 0.0; 258 | 259 | color[0] *= factor; 260 | color[1] *= factor; 261 | color[2] *= factor; 262 | 263 | if(color[0] > 1 && (temp = 1/color[0]) < scale) { scale = temp; } 264 | if(color[1] > 1 && (temp = 1/color[1]) < scale) { scale = temp; } 265 | if(color[2] > 1 && (temp = 1/color[2]) < scale) { scale = temp; } 266 | 267 | color[0] *= scale; 268 | color[1] *= scale; 269 | color[2] *= scale; 270 | 271 | return color; 272 | }; 273 | 274 | // Read all lightmaps 275 | q3bsp.readLightmaps = function(lump, src) { 276 | var lightmapSize = 128 * 128; 277 | var count = lump.length / (lightmapSize*3); 278 | 279 | var gridSize = 2; 280 | 281 | while(gridSize * gridSize < count) { 282 | gridSize *= 2; 283 | } 284 | 285 | var textureSize = gridSize * 128; 286 | 287 | var xOffset = 0; 288 | var yOffset = 0; 289 | 290 | var lightmaps = []; 291 | var lightmapRects = []; 292 | var rgb = [ 0, 0, 0 ]; 293 | 294 | src.seek(lump.offset); 295 | for(var i = 0; i < count; ++i) { 296 | var elements = new Array(lightmapSize*4); 297 | 298 | for(var j = 0; j < lightmapSize*4; j+=4) { 299 | rgb[0] = src.readUByte(); 300 | rgb[1] = src.readUByte(); 301 | rgb[2] = src.readUByte(); 302 | 303 | q3bsp.brightnessAdjust(rgb, 4.0); 304 | 305 | elements[j] = rgb[0]; 306 | elements[j+1] = rgb[1]; 307 | elements[j+2] = rgb[2]; 308 | elements[j+3] = 255; 309 | } 310 | 311 | lightmaps.push({ 312 | x: xOffset, y: yOffset, 313 | width: 128, height: 128, 314 | bytes: elements 315 | }); 316 | 317 | lightmapRects.push({ 318 | x: xOffset/textureSize, 319 | y: yOffset/textureSize, 320 | xScale: 128/textureSize, 321 | yScale: 128/textureSize 322 | }); 323 | 324 | xOffset += 128; 325 | if(xOffset >= textureSize) { 326 | yOffset += 128; 327 | xOffset = 0; 328 | } 329 | } 330 | 331 | // Send the lightmap data back to the render thread 332 | postMessage({ 333 | type: 'lightmap', 334 | size: textureSize, 335 | lightmaps: lightmaps 336 | }); 337 | 338 | return lightmapRects; 339 | }; 340 | 341 | q3bsp.readVerts = function(lump, src) { 342 | var count = lump.length/44; 343 | var elements = []; 344 | 345 | src.seek(lump.offset); 346 | for(var i = 0; i < count; ++i) { 347 | elements.push({ 348 | pos: [ src.readFloat(), src.readFloat(), src.readFloat() ], 349 | texCoord: [ src.readFloat(), src.readFloat() ], 350 | lmCoord: [ src.readFloat(), src.readFloat() ], 351 | lmNewCoord: [ 0, 0 ], 352 | normal: [ src.readFloat(), src.readFloat(), src.readFloat() ], 353 | color: q3bsp.brightnessAdjustVertex(q3bsp.colorToVec(src.readULong()), 4.0) 354 | }); 355 | } 356 | 357 | return elements; 358 | }; 359 | 360 | q3bsp.readMeshVerts = function(lump, src) { 361 | var count = lump.length/4; 362 | var meshVerts = []; 363 | 364 | src.seek(lump.offset); 365 | for(var i = 0; i < count; ++i) { 366 | meshVerts.push(src.readLong()); 367 | } 368 | 369 | return meshVerts; 370 | }; 371 | 372 | // Read all face structures 373 | q3bsp.readFaces = function(lump, src) { 374 | var faceCount = lump.length / 104; 375 | var faces = []; 376 | 377 | src.seek(lump.offset); 378 | for(var i = 0; i < faceCount; ++i) { 379 | var face = { 380 | shader: src.readLong(), 381 | effect: src.readLong(), 382 | type: src.readLong(), 383 | vertex: src.readLong(), 384 | vertCount: src.readLong(), 385 | meshVert: src.readLong(), 386 | meshVertCount: src.readLong(), 387 | lightmap: src.readLong(), 388 | lmStart: [ src.readLong(), src.readLong() ], 389 | lmSize: [ src.readLong(), src.readLong() ], 390 | lmOrigin: [ src.readFloat(), src.readFloat(), src.readFloat() ], 391 | lmVecs: [[ src.readFloat(), src.readFloat(), src.readFloat() ], 392 | [ src.readFloat(), src.readFloat(), src.readFloat() ]], 393 | normal: [ src.readFloat(), src.readFloat(), src.readFloat() ], 394 | size: [ src.readLong(), src.readLong() ], 395 | indexOffset: -1 396 | }; 397 | 398 | faces.push(face); 399 | } 400 | 401 | return faces; 402 | }; 403 | 404 | // Read all Plane structures 405 | q3bsp.readPlanes = function(lump, src) { 406 | var count = lump.length / 16; 407 | var elements = []; 408 | 409 | src.seek(lump.offset); 410 | for(var i = 0; i < count; ++i) { 411 | elements.push({ 412 | normal: [ src.readFloat(), src.readFloat(), src.readFloat() ], 413 | distance: src.readFloat() 414 | }); 415 | } 416 | 417 | return elements; 418 | }; 419 | 420 | // Read all Node structures 421 | q3bsp.readNodes = function(lump, src) { 422 | var count = lump.length / 36; 423 | var elements = []; 424 | 425 | src.seek(lump.offset); 426 | for(var i = 0; i < count; ++i) { 427 | elements.push({ 428 | plane: src.readLong(), 429 | children: [ src.readLong(), src.readLong() ], 430 | min: [ src.readLong(), src.readLong(), src.readLong() ], 431 | max: [ src.readLong(), src.readLong(), src.readLong() ] 432 | }); 433 | } 434 | 435 | return elements; 436 | }; 437 | 438 | // Read all Leaf structures 439 | q3bsp.readLeaves = function(lump, src) { 440 | var count = lump.length / 48; 441 | var elements = []; 442 | 443 | src.seek(lump.offset); 444 | for(var i = 0; i < count; ++i) { 445 | elements.push({ 446 | cluster: src.readLong(), 447 | area: src.readLong(), 448 | min: [ src.readLong(), src.readLong(), src.readLong() ], 449 | max: [ src.readLong(), src.readLong(), src.readLong() ], 450 | leafFace: src.readLong(), 451 | leafFaceCount: src.readLong(), 452 | leafBrush: src.readLong(), 453 | leafBrushCount: src.readLong() 454 | }); 455 | } 456 | 457 | return elements; 458 | }; 459 | 460 | // Read all Leaf Faces 461 | q3bsp.readLeafFaces = function(lump, src) { 462 | var count = lump.length / 4; 463 | var elements = []; 464 | 465 | src.seek(lump.offset); 466 | for(var i = 0; i < count; ++i) { 467 | elements.push(src.readLong()); 468 | } 469 | 470 | return elements; 471 | }; 472 | 473 | // Read all Brushes 474 | q3bsp.readBrushes = function(lump, src) { 475 | var count = lump.length / 12; 476 | var elements = []; 477 | 478 | src.seek(lump.offset); 479 | for(var i = 0; i < count; ++i) { 480 | elements.push({ 481 | brushSide: src.readLong(), 482 | brushSideCount: src.readLong(), 483 | shader: src.readLong() 484 | }); 485 | } 486 | 487 | return elements; 488 | }; 489 | 490 | // Read all Leaf Brushes 491 | q3bsp.readLeafBrushes = function(lump, src) { 492 | var count = lump.length / 4; 493 | var elements = []; 494 | 495 | src.seek(lump.offset); 496 | for(var i = 0; i < count; ++i) { 497 | elements.push(src.readLong()); 498 | } 499 | 500 | return elements; 501 | }; 502 | 503 | // Read all Brush Sides 504 | q3bsp.readBrushSides = function(lump, src) { 505 | var count = lump.length / 8; 506 | var elements = []; 507 | 508 | src.seek(lump.offset); 509 | for(var i = 0; i < count; ++i) { 510 | elements.push({ 511 | plane: src.readLong(), 512 | shader: src.readLong() 513 | }); 514 | } 515 | 516 | return elements; 517 | }; 518 | 519 | // Read all Vis Data 520 | q3bsp.readVisData = function(lump, src) { 521 | src.seek(lump.offset); 522 | var vecCount = src.readLong(); 523 | var size = src.readLong(); 524 | 525 | var byteCount = vecCount * size; 526 | var elements = new Array(byteCount); 527 | 528 | for(var i = 0; i < byteCount; ++i) { 529 | elements[i] = src.readUByte(); 530 | } 531 | 532 | return { 533 | buffer: elements, 534 | size: size 535 | }; 536 | }; 537 | 538 | q3bsp.colorToVec = function(color) { 539 | return[ 540 | (color & 0xFF) / 0xFF, 541 | ((color & 0xFF00) >> 8) / 0xFF, 542 | ((color & 0xFF0000) >> 16) / 0xFF, 543 | 1 544 | ]; 545 | }; 546 | 547 | 548 | // 549 | // Compile the map into a stream of WebGL-compatible data 550 | // 551 | 552 | q3bsp.compileMap = function(verts, faces, meshVerts, lightmaps, shaders, tesselationLevel) { 553 | postMessage({ 554 | type: 'status', 555 | message: 'Map geometry parsed, compiling...' 556 | }); 557 | 558 | // Find associated shaders for all clusters 559 | 560 | // Per-face operations 561 | for(var i = 0; i < faces.length; ++i) { 562 | var face = faces[i]; 563 | 564 | if(face.type==1 || face.type==2 || face.type==3) { 565 | // Add face to the appropriate texture face list 566 | var shader = shaders[face.shader]; 567 | shader.faces.push(face); 568 | var lightmap = lightmaps[face.lightmap]; 569 | 570 | if(!lightmap) { 571 | lightmap = lightmaps[0]; 572 | } 573 | 574 | if(face.type==1 || face.type==3) { 575 | shader.geomType = face.type; 576 | // Transform lightmap coords to match position in combined texture 577 | for(var j = 0; j < face.meshVertCount; ++j) { 578 | var vert = verts[face.vertex + meshVerts[face.meshVert + j]]; 579 | 580 | vert.lmNewCoord[0] = (vert.lmCoord[0] * lightmap.xScale) + lightmap.x; 581 | vert.lmNewCoord[1] = (vert.lmCoord[1] * lightmap.yScale) + lightmap.y; 582 | } 583 | } else { 584 | postMessage({ 585 | type: 'status', 586 | message: 'Tesselating face ' + i + " of " + faces.length 587 | }); 588 | // Build Bezier curve 589 | q3bsp.tesselate(face, verts, meshVerts, tesselationLevel); 590 | for(var j = 0; j < face.vertCount; ++j) { 591 | var vert = verts[face.vertex + j]; 592 | 593 | vert.lmNewCoord[0] = (vert.lmCoord[0] * lightmap.xScale) + lightmap.x; 594 | vert.lmNewCoord[1] = (vert.lmCoord[1] * lightmap.yScale) + lightmap.y; 595 | } 596 | } 597 | } 598 | } 599 | 600 | // Compile vert list 601 | var vertices = new Array(verts.length*14); 602 | var offset = 0; 603 | for(var i = 0; i < verts.length; ++i) { 604 | var vert = verts[i]; 605 | 606 | vertices[offset++] = vert.pos[0]; 607 | vertices[offset++] = vert.pos[1]; 608 | vertices[offset++] = vert.pos[2]; 609 | 610 | vertices[offset++] = vert.texCoord[0]; 611 | vertices[offset++] = vert.texCoord[1]; 612 | 613 | vertices[offset++] = vert.lmNewCoord[0]; 614 | vertices[offset++] = vert.lmNewCoord[1]; 615 | 616 | vertices[offset++] = vert.normal[0]; 617 | vertices[offset++] = vert.normal[1]; 618 | vertices[offset++] = vert.normal[2]; 619 | 620 | vertices[offset++] = vert.color[0]; 621 | vertices[offset++] = vert.color[1]; 622 | vertices[offset++] = vert.color[2]; 623 | vertices[offset++] = vert.color[3]; 624 | } 625 | 626 | // Compile index list 627 | var indices = new Array(); 628 | for(var i = 0; i < shaders.length; ++i) { 629 | var shader = shaders[i]; 630 | if(shader.faces.length > 0) { 631 | shader.indexOffset = indices.length * 2; // Offset is in bytes 632 | 633 | for(var j = 0; j < shader.faces.length; ++j) { 634 | var face = shader.faces[j]; 635 | face.indexOffset = indices.length * 2; 636 | for(var k = 0; k < face.meshVertCount; ++k) { 637 | indices.push(face.vertex + meshVerts[face.meshVert + k]); 638 | } 639 | shader.elementCount += face.meshVertCount; 640 | } 641 | } 642 | shader.faces = null; // Don't need to send this to the render thread. 643 | } 644 | 645 | // Send the compiled vertex/index data back to the render thread 646 | postMessage({ 647 | type: 'geometry', 648 | vertices: vertices, 649 | indices: indices, 650 | surfaces: shaders 651 | }); 652 | }; 653 | 654 | // 655 | // Curve Tesselation 656 | // 657 | 658 | q3bsp.getCurvePoint3 = function(c0, c1, c2, dist) { 659 | var a, b = 1.0 - dist; 660 | 661 | return vec3.add( 662 | a = vec3.add( 663 | a = vec3.scale([0, 0, 0], c0, (b*b)), 664 | a, 665 | vec3.scale([0, 0, 0], c1, (2*b*dist)) 666 | ), 667 | a, 668 | vec3.scale([0, 0, 0], c2, (dist*dist)) 669 | ); 670 | }; 671 | 672 | // This is kinda ugly. Clean it up at some point? 673 | q3bsp.getCurvePoint2 = function(c0, c1, c2, dist) { 674 | var a, b = 1.0 - dist; 675 | 676 | c30 = [c0[0], c0[1], 0]; 677 | c31 = [c1[0], c1[1], 0]; 678 | c32 = [c2[0], c2[1], 0]; 679 | 680 | var res = vec3.add( 681 | a = vec3.add( 682 | a = vec3.scale([0, 0, 0], c30, (b*b)), 683 | a, 684 | vec3.scale([0, 0, 0], c31, (2*b*dist)) 685 | ), 686 | a, 687 | vec3.scale([0, 0, 0], c32, (dist*dist)) 688 | ); 689 | 690 | return [res[0], res[1]]; 691 | }; 692 | 693 | q3bsp.tesselate = function(face, verts, meshVerts, level) { 694 | var off = face.vertex; 695 | var count = face.vertCount; 696 | 697 | var L1 = level + 1; 698 | 699 | face.vertex = verts.length; 700 | face.meshVert = meshVerts.length; 701 | 702 | face.vertCount = 0; 703 | face.meshVertCount = 0; 704 | 705 | for(var py = 0; py < face.size[1]-2; py += 2) { 706 | for(var px = 0; px < face.size[0]-2; px += 2) { 707 | 708 | var rowOff = (py*face.size[0]); 709 | 710 | // Store control points 711 | var c0 = verts[off+rowOff+px], c1 = verts[off+rowOff+px+1], c2 = verts[off+rowOff+px+2]; 712 | rowOff += face.size[0]; 713 | var c3 = verts[off+rowOff+px], c4 = verts[off+rowOff+px+1], c5 = verts[off+rowOff+px+2]; 714 | rowOff += face.size[0]; 715 | var c6 = verts[off+rowOff+px], c7 = verts[off+rowOff+px+1], c8 = verts[off+rowOff+px+2]; 716 | 717 | var indexOff = face.vertCount; 718 | face.vertCount += L1 * L1; 719 | 720 | // Tesselate! 721 | for(var i = 0; i < L1; ++i) { 722 | var a = i / level; 723 | 724 | var pos = q3bsp.getCurvePoint3(c0.pos, c3.pos, c6.pos, a); 725 | var lmCoord = q3bsp.getCurvePoint2(c0.lmCoord, c3.lmCoord, c6.lmCoord, a); 726 | var texCoord = q3bsp.getCurvePoint2(c0.texCoord, c3.texCoord, c6.texCoord, a); 727 | var color = q3bsp.getCurvePoint3(c0.color, c3.color, c6.color, a); 728 | 729 | var vert = { 730 | pos: pos, 731 | texCoord: texCoord, 732 | lmCoord: lmCoord, 733 | color: [color[0], color[1], color[2], 1], 734 | lmNewCoord: [ 0, 0 ], 735 | normal: [0, 0, 1] 736 | }; 737 | 738 | verts.push(vert); 739 | } 740 | 741 | for(var i = 1; i < L1; i++) { 742 | var a = i / level; 743 | 744 | var pc0 = q3bsp.getCurvePoint3(c0.pos, c1.pos, c2.pos, a); 745 | var pc1 = q3bsp.getCurvePoint3(c3.pos, c4.pos, c5.pos, a); 746 | var pc2 = q3bsp.getCurvePoint3(c6.pos, c7.pos, c8.pos, a); 747 | 748 | var tc0 = q3bsp.getCurvePoint3(c0.texCoord, c1.texCoord, c2.texCoord, a); 749 | var tc1 = q3bsp.getCurvePoint3(c3.texCoord, c4.texCoord, c5.texCoord, a); 750 | var tc2 = q3bsp.getCurvePoint3(c6.texCoord, c7.texCoord, c8.texCoord, a); 751 | 752 | var lc0 = q3bsp.getCurvePoint3(c0.lmCoord, c1.lmCoord, c2.lmCoord, a); 753 | var lc1 = q3bsp.getCurvePoint3(c3.lmCoord, c4.lmCoord, c5.lmCoord, a); 754 | var lc2 = q3bsp.getCurvePoint3(c6.lmCoord, c7.lmCoord, c8.lmCoord, a); 755 | 756 | var cc0 = q3bsp.getCurvePoint3(c0.color, c1.color, c2.color, a); 757 | var cc1 = q3bsp.getCurvePoint3(c3.color, c4.color, c5.color, a); 758 | var cc2 = q3bsp.getCurvePoint3(c6.color, c7.color, c8.color, a); 759 | 760 | for(j = 0; j < L1; j++) 761 | { 762 | var b = j / level; 763 | 764 | var pos = q3bsp.getCurvePoint3(pc0, pc1, pc2, b); 765 | var texCoord = q3bsp.getCurvePoint2(tc0, tc1, tc2, b); 766 | var lmCoord = q3bsp.getCurvePoint2(lc0, lc1, lc2, b); 767 | var color = q3bsp.getCurvePoint3(cc0, cc1, cc2, a); 768 | 769 | var vert = { 770 | pos: pos, 771 | texCoord: texCoord, 772 | lmCoord: lmCoord, 773 | color: [color[0], color[1], color[2], 1], 774 | lmNewCoord: [ 0, 0 ], 775 | normal: [0, 0, 1] 776 | }; 777 | 778 | verts.push(vert); 779 | } 780 | } 781 | 782 | face.meshVertCount += level * level * 6; 783 | 784 | for(var row = 0; row < level; ++row) { 785 | for(var col = 0; col < level; ++col) { 786 | meshVerts.push(indexOff + (row + 1) * L1 + col); 787 | meshVerts.push(indexOff + row * L1 + col); 788 | meshVerts.push(indexOff + row * L1 + (col+1)); 789 | 790 | meshVerts.push(indexOff + (row + 1) * L1 + col); 791 | meshVerts.push(indexOff + row * L1 + (col+1)); 792 | meshVerts.push(indexOff + (row + 1) * L1 + (col+1)); 793 | } 794 | } 795 | 796 | } 797 | } 798 | }; 799 | 800 | // 801 | // BSP Collision Detection 802 | // 803 | 804 | q3bsp.trace = function(traceId, start, end, radius, slide) { 805 | if(!radius) { radius = 0; } 806 | if(!slide) { slide = false; } 807 | 808 | if (!brushSides) { return end; } 809 | 810 | var output = { 811 | startsOut: true, 812 | allSolid: false, 813 | plane: null, 814 | fraction: 1 815 | }; 816 | 817 | q3bsp.traceNode(0, 0, 1, start, end, radius, output); 818 | 819 | if(output.fraction != 1) { // collided with something 820 | if(slide && output.plane) { 821 | var endDist = Math.abs(vec3.dot( end, output.plane.normal ) - (output.plane.distance + radius + 0.03125)); 822 | for (var i = 0; i < 3; i++) { 823 | end[i] = end[i] + endDist * (output.plane.normal[i]); 824 | } 825 | } else { 826 | for (var i = 0; i < 3; i++) { 827 | end[i] = start[i] + output.fraction * (end[i] - start[i]); 828 | } 829 | } 830 | } 831 | 832 | postMessage({ 833 | type: 'trace', 834 | traceId: traceId, 835 | end: end 836 | }); 837 | }; 838 | 839 | q3bsp.traceNode = function(nodeIdx, startFraction, endFraction, start, end, radius, output) { 840 | if (nodeIdx < 0) { // Leaf node? 841 | var leaf = leaves[-(nodeIdx + 1)]; 842 | for (var i = 0; i < leaf.leafBrushCount; i++) { 843 | var brush = brushes[leafBrushes[leaf.leafBrush + i]]; 844 | var shader = shaders[brush.shader]; 845 | if (brush.brushSideCount > 0 && (shader.contents & 1)) { 846 | q3bsp.traceBrush(brush, start, end, radius, output); 847 | } 848 | } 849 | return; 850 | } 851 | 852 | // Tree node 853 | var node = nodes[nodeIdx]; 854 | var plane = planes[node.plane]; 855 | 856 | var startDist = vec3.dot(plane.normal, start) - plane.distance; 857 | var endDist = vec3.dot(plane.normal, end) - plane.distance; 858 | 859 | if (startDist >= radius && endDist >= radius) { 860 | q3bsp.traceNode(node.children[0], startFraction, endFraction, start, end, radius, output ); 861 | } else if (startDist < -radius && endDist < -radius) { 862 | q3bsp.traceNode(node.children[1], startFraction, endFraction, start, end, radius, output ); 863 | } else { 864 | var side; 865 | var fraction1, fraction2, middleFraction; 866 | var middle = [0, 0, 0]; 867 | 868 | if (startDist < endDist) { 869 | side = 1; // back 870 | var iDist = 1 / (startDist - endDist); 871 | fraction1 = (startDist - radius + 0.03125) * iDist; 872 | fraction2 = (startDist + radius + 0.03125) * iDist; 873 | } else if (startDist > endDist) { 874 | side = 0; // front 875 | var iDist = 1 / (startDist - endDist); 876 | fraction1 = (startDist + radius + 0.03125) * iDist; 877 | fraction2 = (startDist - radius - 0.03125) * iDist; 878 | } else { 879 | side = 0; // front 880 | fraction1 = 1; 881 | fraction2 = 0; 882 | } 883 | 884 | if (fraction1 < 0) fraction1 = 0; 885 | else if (fraction1 > 1) fraction1 = 1; 886 | if (fraction2 < 0) fraction2 = 0; 887 | else if (fraction2 > 1) fraction2 = 1; 888 | 889 | middleFraction = startFraction + (endFraction - startFraction) * fraction1; 890 | 891 | for (var i = 0; i < 3; i++) { 892 | middle[i] = start[i] + fraction1 * (end[i] - start[i]); 893 | } 894 | 895 | q3bsp.traceNode(node.children[side], startFraction, middleFraction, start, middle, radius, output ); 896 | 897 | middleFraction = startFraction + (endFraction - startFraction) * fraction2; 898 | 899 | for (var i = 0; i < 3; i++) { 900 | middle[i] = start[i] + fraction2 * (end[i] - start[i]); 901 | } 902 | 903 | q3bsp.traceNode(node.children[side===0?1:0], middleFraction, endFraction, middle, end, radius, output ); 904 | } 905 | }; 906 | 907 | q3bsp.traceBrush = function(brush, start, end, radius, output) { 908 | var startFraction = -1; 909 | var endFraction = 1; 910 | var startsOut = false; 911 | var endsOut = false; 912 | var collisionPlane = null; 913 | 914 | for (var i = 0; i < brush.brushSideCount; i++) { 915 | var brushSide = brushSides[brush.brushSide + i]; 916 | var plane = planes[brushSide.plane]; 917 | 918 | var startDist = vec3.dot( start, plane.normal ) - (plane.distance + radius); 919 | var endDist = vec3.dot( end, plane.normal ) - (plane.distance + radius); 920 | 921 | if (startDist > 0) startsOut = true; 922 | if (endDist > 0) endsOut = true; 923 | 924 | // make sure the trace isn't completely on one side of the brush 925 | if (startDist > 0 && endDist > 0) { return; } 926 | if (startDist <= 0 && endDist <= 0) { continue; } 927 | 928 | if (startDist > endDist) { // line is entering into the brush 929 | var fraction = (startDist - 0.03125) / (startDist - endDist); 930 | if (fraction > startFraction) { 931 | startFraction = fraction; 932 | collisionPlane = plane; 933 | } 934 | } else { // line is leaving the brush 935 | var fraction = (startDist + 0.03125) / (startDist - endDist); 936 | if (fraction < endFraction) 937 | endFraction = fraction; 938 | } 939 | } 940 | 941 | if (startsOut === false) { 942 | output.startsOut = false; 943 | if (endsOut === false) 944 | output.allSolid = true; 945 | return; 946 | } 947 | 948 | if (startFraction < endFraction) { 949 | if (startFraction > -1 && startFraction < output.fraction) { 950 | output.plane = collisionPlane; 951 | if (startFraction < 0) 952 | startFraction = 0; 953 | output.fraction = startFraction; 954 | } 955 | } 956 | 957 | return; 958 | }; 959 | 960 | // 961 | // Visibility Checking 962 | // 963 | 964 | var lastLeaf = -1; 965 | 966 | q3bsp.checkVis = function(visCluster, testCluster) { 967 | if(visCluster == testCluster || visCluster == -1) { return true; } 968 | var i = (visCluster * visSize) + (testCluster >> 3); 969 | var visSet = visBuffer[i]; 970 | return (visSet & (1 << (testCluster & 7)) !== 0); 971 | }; 972 | 973 | q3bsp.getLeaf = function(pos) { 974 | var index = 0; 975 | 976 | var node = null; 977 | var plane = null; 978 | var distance = 0; 979 | 980 | while (index >= 0) { 981 | node = nodes[index]; 982 | plane = planes[node.plane]; 983 | distance = vec3.dot(plane.normal, pos) - plane.distance; 984 | 985 | if (distance >= 0) { 986 | index = node.children[0]; 987 | } else { 988 | index = node.children[1]; 989 | } 990 | } 991 | 992 | return -(index+1); 993 | }; 994 | 995 | q3bsp.buildVisibleList = function(leafIndex) { 996 | // Determine visible faces 997 | if(leafIndex == lastLeaf) { return; } 998 | lastLeaf = leafIndex; 999 | 1000 | var curLeaf = leaves[leafIndex]; 1001 | 1002 | var visibleShaders = new Array(shaders.length); 1003 | 1004 | for(var i = 0; i < leaves.length; ++i) { 1005 | var leaf = leaves[i]; 1006 | if(q3bsp.checkVis(curLeaf.cluster, leaf.cluster)) { 1007 | for(var j = 0; j < leaf.leafFaceCount; ++j) { 1008 | var face = faces[leafFaces[[j + leaf.leafFace]]]; 1009 | if(face) { 1010 | visibleShaders[face.shader] = true; 1011 | } 1012 | } 1013 | } 1014 | } 1015 | 1016 | var ar = new Array(visSize); 1017 | 1018 | for(var i = 0; i < visSize; ++i) { 1019 | ar[i] = visBuffer[(curLeaf.cluster * visSize) + i]; 1020 | } 1021 | 1022 | postMessage({ 1023 | type: 'visibility', 1024 | visibleSurfaces: visibleShaders 1025 | }); 1026 | }; -------------------------------------------------------------------------------- /js/q3glshader.js: -------------------------------------------------------------------------------- 1 | /* 2 | * q3glshader.js - Transforms a parsed Q3 shader definition into a set of WebGL compatible states 3 | */ 4 | 5 | /* 6 | * Copyright (c) 2009 Brandon Jones 7 | * 8 | * This software is provided 'as-is', without any express or implied 9 | * warranty. In no event will the authors be held liable for any damages 10 | * arising from the use of this software. 11 | * 12 | * Permission is granted to anyone to use this software for any purpose, 13 | * including commercial applications, and to alter it and redistribute it 14 | * freely, subject to the following restrictions: 15 | * 16 | * 1. The origin of this software must not be misrepresented; you must not 17 | * claim that you wrote the original software. If you use this software 18 | * in a product, an acknowledgment in the product documentation would be 19 | * appreciated but is not required. 20 | * 21 | * 2. Altered source versions must be plainly marked as such, and must not 22 | * be misrepresented as being the original software. 23 | * 24 | * 3. This notice may not be removed or altered from any source 25 | * distribution. 26 | */ 27 | 28 | // 29 | // Default Shaders 30 | // 31 | 32 | q3bsp_default_vertex = '\ 33 | #ifdef GL_ES \n\ 34 | precision highp float; \n\ 35 | #endif \n\ 36 | attribute vec3 position; \n\ 37 | attribute vec3 normal; \n\ 38 | attribute vec2 texCoord; \n\ 39 | attribute vec2 lightCoord; \n\ 40 | attribute vec4 color; \n\ 41 | \n\ 42 | varying vec2 vTexCoord; \n\ 43 | varying vec2 vLightmapCoord; \n\ 44 | varying vec4 vColor; \n\ 45 | \n\ 46 | uniform mat4 modelViewMat; \n\ 47 | uniform mat4 projectionMat; \n\ 48 | \n\ 49 | void main(void) { \n\ 50 | vec4 worldPosition = modelViewMat * vec4(position, 1.0); \n\ 51 | vTexCoord = texCoord; \n\ 52 | vColor = color; \n\ 53 | vLightmapCoord = lightCoord; \n\ 54 | gl_Position = projectionMat * worldPosition; \n\ 55 | } \n\ 56 | '; 57 | 58 | q3bsp_default_fragment = '\ 59 | #ifdef GL_ES \n\ 60 | precision highp float; \n\ 61 | #endif \n\ 62 | varying vec2 vTexCoord; \n\ 63 | varying vec2 vLightmapCoord; \n\ 64 | uniform sampler2D texture; \n\ 65 | uniform sampler2D lightmap; \n\ 66 | \n\ 67 | void main(void) { \n\ 68 | vec4 diffuseColor = texture2D(texture, vTexCoord); \n\ 69 | vec4 lightColor = texture2D(lightmap, vLightmapCoord); \n\ 70 | gl_FragColor = vec4(diffuseColor.rgb * lightColor.rgb, diffuseColor.a); \n\ 71 | } \n\ 72 | '; 73 | 74 | q3bsp_model_fragment = '\ 75 | #ifdef GL_ES \n\ 76 | precision highp float; \n\ 77 | #endif \n\ 78 | varying vec2 vTexCoord; \n\ 79 | varying vec4 vColor; \n\ 80 | uniform sampler2D texture; \n\ 81 | \n\ 82 | void main(void) { \n\ 83 | vec4 diffuseColor = texture2D(texture, vTexCoord); \n\ 84 | gl_FragColor = vec4(diffuseColor.rgb * vColor.rgb, diffuseColor.a); \n\ 85 | } \n\ 86 | '; 87 | 88 | var q3glshader = {} 89 | 90 | q3glshader.lightmap = null; 91 | q3glshader.white = null; 92 | q3glshader.defaultShader = null; 93 | q3glshader.defaultTexture = null; 94 | q3glshader.texMat = mat4.create(); 95 | q3glshader.defaultProgram = null; 96 | 97 | q3glshader.s3tcExt = null; 98 | 99 | q3glshader.init = function(gl, lightmap) { 100 | q3glshader.lightmap = lightmap; 101 | q3glshader.white = q3glshader.createSolidTexture(gl, [255,255,255,255]); 102 | 103 | q3glshader.defaultProgram = q3glshader.compileShaderProgram(gl, q3bsp_default_vertex, q3bsp_default_fragment); 104 | q3glshader.modelProgram = q3glshader.compileShaderProgram(gl, q3bsp_default_vertex, q3bsp_model_fragment); 105 | 106 | var image = new Image(); 107 | q3glshader.defaultTexture = gl.createTexture(); 108 | image.onload = function() { 109 | gl.bindTexture(gl.TEXTURE_2D, q3glshader.defaultTexture); 110 | gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image); 111 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR); 112 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR_MIPMAP_NEAREST); 113 | gl.generateMipmap(gl.TEXTURE_2D); 114 | } 115 | image.src = q3bsp_base_folder + '/webgl/no-shader.png'; 116 | 117 | // Load default stage 118 | q3glshader.defaultShader = q3glshader.buildDefault(gl); 119 | } 120 | 121 | // 122 | // Shader building 123 | // 124 | 125 | q3glshader.build = function(gl, shader) { 126 | var glShader = { 127 | cull: q3glshader.translateCull(gl, shader.cull), 128 | sort: shader.sort, 129 | sky: shader.sky, 130 | blend: shader.blend, 131 | name: shader.name, 132 | stages: [] 133 | }; 134 | 135 | for(var j = 0; j < shader.stages.length; ++j) { 136 | var stage = shader.stages[j]; 137 | var glStage = stage; 138 | 139 | glStage.texture = null; 140 | glStage.blendSrc = q3glshader.translateBlend(gl, stage.blendSrc); 141 | glStage.blendDest = q3glshader.translateBlend(gl, stage.blendDest); 142 | glStage.depthFunc = q3glshader.translateDepthFunc(gl, stage.depthFunc); 143 | 144 | glShader.stages.push(glStage); 145 | } 146 | 147 | return glShader; 148 | } 149 | 150 | q3glshader.buildDefault = function(gl, surface) { 151 | var diffuseStage = { 152 | map: (surface ? surface.shaderName + '.png' : null), 153 | isLightmap: false, 154 | blendSrc: gl.ONE, 155 | blendDest: gl.ZERO, 156 | depthFunc: gl.LEQUAL, 157 | depthWrite: true 158 | }; 159 | 160 | if(surface) { 161 | q3glshader.loadTexture(gl, surface, diffuseStage); 162 | } else { 163 | diffuseStage.texture = q3glshader.defaultTexture; 164 | } 165 | 166 | var glShader = { 167 | cull: gl.FRONT, 168 | blend: false, 169 | sort: 3, 170 | stages: [ diffuseStage ] 171 | }; 172 | 173 | return glShader; 174 | } 175 | 176 | q3glshader.translateDepthFunc = function(gl, depth) { 177 | if(!depth) { return gl.LEQUAL; } 178 | switch(depth.toLowerCase()) { 179 | case 'gequal': return gl.GEQUAL; 180 | case 'lequal': return gl.LEQUAL; 181 | case 'equal': return gl.EQUAL; 182 | case 'greater': return gl.GREATER; 183 | case 'less': return gl.LESS; 184 | default: return gl.LEQUAL; 185 | } 186 | }; 187 | 188 | q3glshader.translateCull = function(gl, cull) { 189 | if(!cull) { return gl.FRONT; } 190 | switch(cull.toLowerCase()) { 191 | case 'disable': 192 | case 'none': return null; 193 | case 'front': return gl.BACK; 194 | case 'back': 195 | default: return gl.FRONT; 196 | } 197 | }; 198 | 199 | q3glshader.translateBlend = function(gl, blend) { 200 | if(!blend) { return gl.ONE; } 201 | switch(blend.toUpperCase()) { 202 | case 'GL_ONE': return gl.ONE; 203 | case 'GL_ZERO': return gl.ZERO; 204 | case 'GL_DST_COLOR': return gl.DST_COLOR; 205 | case 'GL_ONE_MINUS_DST_COLOR': return gl.ONE_MINUS_DST_COLOR; 206 | case 'GL_SRC_ALPHA ': return gl.SRC_ALPHA; 207 | case 'GL_ONE_MINUS_SRC_ALPHA': return gl.ONE_MINUS_SRC_ALPHA; 208 | case 'GL_SRC_COLOR': return gl.SRC_COLOR; 209 | case 'GL_ONE_MINUS_SRC_COLOR': return gl.ONE_MINUS_SRC_COLOR; 210 | default: return gl.ONE; 211 | } 212 | }; 213 | 214 | // 215 | // Texture loading 216 | // 217 | 218 | q3glshader.loadShaderMaps = function(gl, surface, shader) { 219 | for(var i = 0; i < shader.stages.length; ++i) { 220 | var stage = shader.stages[i]; 221 | if(stage.map) { 222 | q3glshader.loadTexture(gl, surface, stage); 223 | } 224 | 225 | if(stage.shaderSrc && !stage.program) { 226 | stage.program = q3glshader.compileShaderProgram(gl, stage.shaderSrc.vertex, stage.shaderSrc.fragment); 227 | } 228 | } 229 | }; 230 | 231 | q3glshader.loadTexture = function(gl, surface, stage) { 232 | if(!stage.map) { 233 | stage.texture = q3glshader.white; 234 | return; 235 | } else if(stage.map == '$lightmap') { 236 | stage.texture = (surface.geomType != 3 ? q3glshader.lightmap : q3glshader.white); 237 | return; 238 | } else if(stage.map == '$whiteimage') { 239 | stage.texture = q3glshader.white; 240 | return; 241 | } 242 | 243 | stage.texture = q3glshader.defaultTexture; 244 | 245 | if(stage.map == 'anim') { 246 | stage.animTexture = []; 247 | for(var i = 0; i < stage.animMaps.length; ++i) { 248 | var animLoad = function(i) { 249 | stage.animTexture[i] = q3glshader.defaultTexture; 250 | q3glshader.loadTextureUrl(gl, stage, stage.animMaps[i], function(texture) { 251 | stage.animTexture[i] = texture; 252 | }); 253 | }; 254 | animLoad(i); 255 | } 256 | stage.animFrame = 0; 257 | } else { 258 | q3glshader.loadTextureUrl(gl, stage, stage.map, function(texture) { 259 | stage.texture = texture; 260 | }); 261 | } 262 | }; 263 | 264 | let basisLoader = new BasisLoader(); 265 | 266 | q3glshader.loadTextureUrlBasisWorker = function(gl, stage, url, onload) { 267 | // Swap out the file extension 268 | url = url.replace(/.png/, '.basis'); 269 | 270 | basisLoader.setWebGLContext(gl); 271 | basisLoader.loadFromUrl(`${q3bsp_base_folder}/${url}`).then((result) => { 272 | if(stage.clamp) { 273 | gl.bindTexture(gl.TEXTURE_2D, result.texture); 274 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); 275 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); 276 | 277 | if (result.alphaTexture) { 278 | gl.bindTexture(gl.TEXTURE_2D, result.alphaTexture); 279 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); 280 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); 281 | } 282 | } 283 | // TODO: If there's an alpha texture need to surface it here. 284 | onload(result.texture); 285 | }); 286 | } 287 | 288 | // PNG variant 289 | q3glshader.loadTextureUrlImg = function(gl, stage, url, onload) { 290 | var image = new Image(); 291 | image.onload = function() { 292 | var texture = gl.createTexture(); 293 | gl.bindTexture(gl.TEXTURE_2D, texture); 294 | gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image); 295 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR); 296 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR_MIPMAP_LINEAR); 297 | if(stage.clamp) { 298 | gl.texParameterf(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE ); 299 | gl.texParameterf(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE ); 300 | } 301 | gl.generateMipmap(gl.TEXTURE_2D); 302 | 303 | onload(texture); 304 | } 305 | image.src = q3bsp_base_folder + '/' + url; 306 | } 307 | 308 | if (window.location.search.indexOf('png') >= 0) { 309 | q3glshader.loadTextureUrl = q3glshader.loadTextureUrlImg; 310 | } else { 311 | q3glshader.loadTextureUrl = q3glshader.loadTextureUrlBasisWorker; 312 | } 313 | 314 | q3glshader.createSolidTexture = function(gl, color) { 315 | var data = new Uint8Array(color); 316 | var texture = gl.createTexture(); 317 | gl.bindTexture(gl.TEXTURE_2D, texture); 318 | gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGB, 1, 1, 0, gl.RGB, gl.UNSIGNED_BYTE, data); 319 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST); 320 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST); 321 | return texture; 322 | } 323 | 324 | // 325 | // Render state setup 326 | // 327 | 328 | q3glshader.setShader = function(gl, shader) { 329 | if(!shader) { 330 | gl.enable(gl.CULL_FACE); 331 | gl.cullFace(gl.BACK); 332 | } else if(shader.cull && !shader.sky) { 333 | gl.enable(gl.CULL_FACE); 334 | gl.cullFace(shader.cull); 335 | } else { 336 | gl.disable(gl.CULL_FACE); 337 | } 338 | 339 | return true; 340 | } 341 | 342 | q3glshader.setShaderStage = function(gl, shader, shaderStage, time) { 343 | var stage = shaderStage; 344 | if(!stage) { 345 | stage = q3glshader.defaultShader.stages[0]; 346 | } 347 | 348 | if(stage.animFreq) { 349 | // Texture animation seems like a natural place for setInterval, but that approach has proved error prone. 350 | // It can easily get out of sync with other effects (like rgbGen pulses and whatnot) which can give a 351 | // jittery or flat out wrong appearance. Doing it this way ensures all effects are synced. 352 | var animFrame = Math.floor(time*stage.animFreq) % stage.animTexture.length; 353 | stage.texture = stage.animTexture[animFrame]; 354 | } 355 | 356 | gl.blendFunc(stage.blendSrc, stage.blendDest); 357 | 358 | if(stage.depthWrite && !shader.sky) { 359 | gl.depthMask(true); 360 | } else { 361 | gl.depthMask(false); 362 | } 363 | 364 | gl.depthFunc(stage.depthFunc); 365 | 366 | var program = stage.program; 367 | if(!program) { 368 | if(shader.model) { 369 | program = q3glshader.modelProgram; 370 | } else { 371 | program = q3glshader.defaultProgram; 372 | } 373 | } 374 | 375 | gl.useProgram(program); 376 | 377 | var texture = stage.texture; 378 | if(!texture) { texture = q3glshader.defaultTexture; } 379 | 380 | gl.activeTexture(gl.TEXTURE0); 381 | gl.uniform1i(program.uniform.texture, 0); 382 | gl.bindTexture(gl.TEXTURE_2D, texture); 383 | 384 | if(program.uniform.lightmap) { 385 | gl.activeTexture(gl.TEXTURE1); 386 | gl.uniform1i(program.uniform.lightmap, 1); 387 | gl.bindTexture(gl.TEXTURE_2D, q3glshader.lightmap); 388 | } 389 | 390 | if(program.uniform.time) { 391 | gl.uniform1f(program.uniform.time, time); 392 | } 393 | 394 | return program; 395 | }; 396 | 397 | // 398 | // Shader program compilation 399 | // 400 | 401 | q3glshader.compileShaderProgram = function(gl, vertexSrc, fragmentSrc) { 402 | var fragmentShader = gl.createShader(gl.FRAGMENT_SHADER); 403 | gl.shaderSource(fragmentShader, fragmentSrc); 404 | gl.compileShader(fragmentShader); 405 | 406 | if (!gl.getShaderParameter(fragmentShader, gl.COMPILE_STATUS)) { 407 | /*console.debug(gl.getShaderInfoLog(fragmentShader)); 408 | console.debug(vertexSrc); 409 | console.debug(fragmentSrc);*/ 410 | gl.deleteShader(fragmentShader); 411 | return null; 412 | } 413 | 414 | var vertexShader = gl.createShader(gl.VERTEX_SHADER); 415 | gl.shaderSource(vertexShader, vertexSrc); 416 | gl.compileShader(vertexShader); 417 | 418 | if (!gl.getShaderParameter(vertexShader, gl.COMPILE_STATUS)) { 419 | /*console.debug(gl.getShaderInfoLog(vertexShader)); 420 | console.debug(vertexSrc); 421 | console.debug(fragmentSrc);*/ 422 | gl.deleteShader(vertexShader); 423 | return null; 424 | } 425 | 426 | var shaderProgram = gl.createProgram(); 427 | gl.attachShader(shaderProgram, vertexShader); 428 | gl.attachShader(shaderProgram, fragmentShader); 429 | gl.linkProgram(shaderProgram); 430 | 431 | if (!gl.getProgramParameter(shaderProgram, gl.LINK_STATUS)) { 432 | gl.deleteProgram(shaderProgram); 433 | gl.deleteShader(vertexShader); 434 | gl.deleteShader(fragmentShader); 435 | /*console.debug('Could not link shaders'); 436 | console.debug(vertexSrc); 437 | console.debug(fragmentSrc);*/ 438 | return null; 439 | } 440 | 441 | var i, attrib, uniform; 442 | var attribCount = gl.getProgramParameter(shaderProgram, gl.ACTIVE_ATTRIBUTES); 443 | shaderProgram.attrib = {}; 444 | for (i = 0; i < attribCount; i++) { 445 | attrib = gl.getActiveAttrib(shaderProgram, i); 446 | shaderProgram.attrib[attrib.name] = gl.getAttribLocation(shaderProgram, attrib.name); 447 | } 448 | 449 | var uniformCount = gl.getProgramParameter(shaderProgram, gl.ACTIVE_UNIFORMS); 450 | shaderProgram.uniform = {}; 451 | for (i = 0; i < uniformCount; i++) { 452 | uniform = gl.getActiveUniform(shaderProgram, i); 453 | shaderProgram.uniform[uniform.name] = gl.getUniformLocation(shaderProgram, uniform.name); 454 | } 455 | 456 | return shaderProgram; 457 | } -------------------------------------------------------------------------------- /js/q3movement.js: -------------------------------------------------------------------------------- 1 | /* 2 | * q3movement.js - Handles player movement through a bsp structure 3 | */ 4 | 5 | /* 6 | * Copyright (c) 2009 Brandon Jones 7 | * 8 | * This software is provided 'as-is', without any express or implied 9 | * warranty. In no event will the authors be held liable for any damages 10 | * arising from the use of this software. 11 | * 12 | * Permission is granted to anyone to use this software for any purpose, 13 | * including commercial applications, and to alter it and redistribute it 14 | * freely, subject to the following restrictions: 15 | * 16 | * 1. The origin of this software must not be misrepresented; you must not 17 | * claim that you wrote the original software. If you use this software 18 | * in a product, an acknowledgment in the product documentation would be 19 | * appreciated but is not required. 20 | * 21 | * 2. Altered source versions must be plainly marked as such, and must not 22 | * be misrepresented as being the original software. 23 | * 24 | * 3. This notice may not be removed or altered from any source 25 | * distribution. 26 | */ 27 | 28 | // Much of this file is a simplified/dumbed-down version of the Q3 player movement code 29 | // found in bg_pmove.c and bg_slidemove.c 30 | 31 | // Some movement constants ripped from the Q3 Source code 32 | var q3movement_stopspeed = 100.0; 33 | var q3movement_duckScale = 0.25; 34 | var q3movement_jumpvelocity = 50; 35 | 36 | var q3movement_accelerate = 10.0; 37 | var q3movement_airaccelerate = 0.1; 38 | var q3movement_flyaccelerate = 8.0; 39 | 40 | var q3movement_friction = 6.0; 41 | var q3movement_flightfriction = 3.0; 42 | 43 | var q3movement_frameTime = 0.30; 44 | var q3movement_overclip = 0.501; 45 | var q3movement_stepsize = 18; 46 | 47 | var q3movement_gravity = 20.0; 48 | 49 | var q3movement_playerRadius = 10.0; 50 | var q3movement_scale = 50; 51 | 52 | q3movement = function(bsp) { 53 | this.bsp = bsp; 54 | 55 | this.velocity = [0, 0, 0]; 56 | this.position = [0, 0, 0]; 57 | this.onGround = false; 58 | 59 | this.groundTrace = null; 60 | }; 61 | 62 | q3movement.prototype.applyFriction = function() { 63 | if(!this.onGround) { return; } 64 | 65 | var speed = vec3.length(this.velocity); 66 | 67 | var drop = 0; 68 | 69 | var control = speed < q3movement_stopspeed ? q3movement_stopspeed : speed; 70 | drop += control*q3movement_friction*q3movement_frameTime; 71 | 72 | var newSpeed = speed - drop; 73 | if (newSpeed < 0) { 74 | newSpeed = 0; 75 | } 76 | if(speed !== 0) { 77 | newSpeed /= speed; 78 | vec3.scale(this.velocity, this.velocity, newSpeed); 79 | } else { 80 | this.velocity = [0, 0, 0]; 81 | } 82 | }; 83 | 84 | q3movement.prototype.groundCheck = function() { 85 | var checkPoint = [this.position[0], this.position[1], this.position[2] - q3movement_playerRadius - 0.25]; 86 | 87 | this.groundTrace = this.bsp.trace(this.position, checkPoint, q3movement_playerRadius); 88 | 89 | if(this.groundTrace.fraction == 1.0) { // falling 90 | this.onGround = false; 91 | return; 92 | } 93 | 94 | if ( this.velocity[2] > 0 && vec3.dot( this.velocity, this.groundTrace.plane.normal ) > 10 ) { // jumping 95 | this.onGround = false; 96 | return; 97 | } 98 | 99 | if(this.groundTrace.plane.normal[2] < 0.7) { // steep slope 100 | this.onGround = false; 101 | return; 102 | } 103 | 104 | this.onGround = true; 105 | }; 106 | 107 | q3movement.prototype.clipVelocity = function(velIn, normal) { 108 | var backoff = vec3.dot(velIn, normal); 109 | 110 | if ( backoff < 0 ) { 111 | backoff *= q3movement_overclip; 112 | } else { 113 | backoff /= q3movement_overclip; 114 | } 115 | 116 | var change = vec3.scale([0,0,0], normal, backoff); 117 | return vec3.subtract(change, velIn, change); 118 | }; 119 | 120 | q3movement.prototype.accelerate = function(dir, speed, accel) { 121 | var currentSpeed = vec3.dot(this.velocity, dir); 122 | var addSpeed = speed - currentSpeed; 123 | if (addSpeed <= 0) { 124 | return; 125 | } 126 | 127 | var accelSpeed = accel*q3movement_frameTime*speed; 128 | if (accelSpeed > addSpeed) { 129 | accelSpeed = addSpeed; 130 | } 131 | 132 | var accelDir = vec3.scale([0,0,0], dir, accelSpeed); 133 | vec3.add(this.velocity, this.velocity, accelDir); 134 | }; 135 | 136 | q3movement.prototype.jump = function() { 137 | if(!this.onGround) { return false; } 138 | 139 | this.onGround = false; 140 | this.velocity[2] = q3movement_jumpvelocity; 141 | 142 | //Make sure that the player isn't stuck in the ground 143 | var groundDist = vec3.dot( this.position, this.groundTrace.plane.normal ) - this.groundTrace.plane.distance - q3movement_playerRadius; 144 | vec3.add(this.position, this.position, vec3.scale([0, 0, 0], this.groundTrace.plane.normal, groundDist + 5)); 145 | 146 | return true; 147 | }; 148 | 149 | q3movement.prototype.move = function(dir, frameTime) { 150 | q3movement_frameTime = frameTime*0.0075; 151 | 152 | this.groundCheck(); 153 | 154 | vec3.normalize(dir, dir); 155 | 156 | if(this.onGround) { 157 | this.walkMove(dir); 158 | } else { 159 | this.airMove(dir); 160 | } 161 | 162 | return this.position; 163 | }; 164 | 165 | q3movement.prototype.airMove = function(dir) { 166 | var speed = vec3.length(dir) * q3movement_scale; 167 | 168 | this.accelerate(dir, speed, q3movement_airaccelerate); 169 | 170 | this.stepSlideMove( true ); 171 | }; 172 | 173 | q3movement.prototype.walkMove = function(dir) { 174 | this.applyFriction(); 175 | 176 | var speed = vec3.length(dir) * q3movement_scale; 177 | 178 | this.accelerate(dir, speed, q3movement_accelerate); 179 | 180 | this.velocity = this.clipVelocity(this.velocity, this.groundTrace.plane.normal); 181 | 182 | if(!this.velocity[0] && !this.velocity[1]) { return; } 183 | 184 | this.stepSlideMove( false ); 185 | }; 186 | 187 | q3movement.prototype.slideMove = function(gravity) { 188 | var bumpcount; 189 | var numbumps = 4; 190 | var planes = []; 191 | var endVelocity = [0,0,0]; 192 | 193 | if ( gravity ) { 194 | vec3.copy(endVelocity, this.velocity); 195 | endVelocity[2] -= q3movement_gravity * q3movement_frameTime; 196 | this.velocity[2] = ( this.velocity[2] + endVelocity[2] ) * 0.5; 197 | 198 | if ( this.groundTrace && this.groundTrace.plane ) { 199 | // slide along the ground plane 200 | this.velocity = this.clipVelocity(this.velocity, this.groundTrace.plane.normal); 201 | } 202 | } 203 | 204 | // never turn against the ground plane 205 | if ( this.groundTrace && this.groundTrace.plane ) { 206 | planes.push(vec3.copy([0,0,0], this.groundTrace.plane.normal)); 207 | } 208 | 209 | // never turn against original velocity 210 | planes.push(vec3.normalize([0,0,0], this.velocity)); 211 | 212 | var time_left = q3movement_frameTime; 213 | var end = [0,0,0]; 214 | for(bumpcount=0; bumpcount < numbumps; ++bumpcount) { 215 | 216 | // calculate position we are trying to move to 217 | vec3.add(end, this.position, vec3.scale([0,0,0], this.velocity, time_left)); 218 | 219 | // see if we can make it there 220 | var trace = this.bsp.trace(this.position, end, q3movement_playerRadius); 221 | 222 | if (trace.allSolid) { 223 | // entity is completely trapped in another solid 224 | this.velocity[2] = 0; // don't build up falling damage, but allow sideways acceleration 225 | return true; 226 | } 227 | 228 | if (trace.fraction > 0) { 229 | // actually covered some distance 230 | vec3.copy(this.position, trace.endPos); 231 | } 232 | 233 | if (trace.fraction == 1) { 234 | break; // moved the entire distance 235 | } 236 | 237 | time_left -= time_left * trace.fraction; 238 | 239 | planes.push(vec3.copy([0,0,0], trace.plane.normal)); 240 | 241 | // 242 | // modify velocity so it parallels all of the clip planes 243 | // 244 | 245 | // find a plane that it enters 246 | for(var i = 0; i < planes.length; ++i) { 247 | var into = vec3.dot(this.velocity, planes[i]); 248 | if ( into >= 0.1 ) { continue; } // move doesn't interact with the plane 249 | 250 | // slide along the plane 251 | var clipVelocity = this.clipVelocity(this.velocity, planes[i]); 252 | var endClipVelocity = this.clipVelocity(endVelocity, planes[i]); 253 | 254 | // see if there is a second plane that the new move enters 255 | for (var j = 0; j < planes.length; j++) { 256 | if ( j == i ) { continue; } 257 | if ( vec3.dot( clipVelocity, planes[j] ) >= 0.1 ) { continue; } // move doesn't interact with the plane 258 | 259 | // try clipping the move to the plane 260 | clipVelocity = this.clipVelocity( clipVelocity, planes[j] ); 261 | endClipVelocity = this.clipVelocity( endClipVelocity, planes[j] ); 262 | 263 | // see if it goes back into the first clip plane 264 | if ( vec3.dot( clipVelocity, planes[i] ) >= 0 ) { continue; } 265 | 266 | // slide the original velocity along the crease 267 | var dir = [0,0,0]; 268 | vec3.cross(dir, planes[i], planes[j]); 269 | vec3.normalize(dir, dir); 270 | var d = vec3.dot(dir, this.velocity); 271 | vec3.scale(clipVelocity, dir, d); 272 | 273 | vec3.cross(dir, planes[i], planes[j]); 274 | vec3.normalize(dir, dir); 275 | d = vec3.dot(dir, endVelocity); 276 | vec3.scale(endClipVelocity, dir, d); 277 | 278 | // see if there is a third plane the the new move enters 279 | for(var k = 0; k < planes.length; ++k) { 280 | if ( k == i || k == j ) { continue; } 281 | if ( vec3.dot( clipVelocity, planes[k] ) >= 0.1 ) { continue; } // move doesn't interact with the plane 282 | 283 | // stop dead at a tripple plane interaction 284 | this.velocity = [0,0,0]; 285 | return true; 286 | } 287 | } 288 | 289 | // if we have fixed all interactions, try another move 290 | vec3.copy(this.velocity, clipVelocity); 291 | vec3.copy(endVelocity, endClipVelocity); 292 | break; 293 | } 294 | } 295 | 296 | if ( gravity ) { 297 | vec3.copy(this.velocity, endVelocity); 298 | } 299 | 300 | return ( bumpcount !== 0 ); 301 | }; 302 | 303 | q3movement.prototype.stepSlideMove = function(gravity) { 304 | var start_o = vec3.copy([0,0,0], this.position); 305 | var start_v = vec3.copy([0,0,0], this.velocity); 306 | 307 | if ( this.slideMove( gravity ) === 0 ) { return; } // we got exactly where we wanted to go first try 308 | 309 | var down = vec3.copy([0,0,0], start_o); 310 | down[2] -= q3movement_stepsize; 311 | var trace = this.bsp.trace(start_o, down, q3movement_playerRadius); 312 | 313 | var up = [0,0,1]; 314 | 315 | // never step up when you still have up velocity 316 | if ( this.velocity[2] > 0 && (trace.fraction == 1.0 || vec3.dot(trace.plane.normal, up) < 0.7)) { return; } 317 | 318 | var down_o = vec3.copy([0,0,0], this.position); 319 | var down_v = vec3.copy([0,0,0], this.velocity); 320 | 321 | vec3.copy(up, start_o); 322 | up[2] += q3movement_stepsize; 323 | 324 | // test the player position if they were a stepheight higher 325 | trace = this.bsp.trace(start_o, up, q3movement_playerRadius); 326 | if ( trace.allSolid ) { return; } // can't step up 327 | 328 | var stepSize = trace.endPos[2] - start_o[2]; 329 | // try slidemove from this position 330 | vec3.copy(this.position, trace.endPos); 331 | vec3.copy(this.velocity, start_v); 332 | 333 | this.slideMove( gravity ); 334 | 335 | // push down the final amount 336 | vec3.copy(down, this.position); 337 | down[2] -= stepSize; 338 | trace = this.bsp.trace(this.position, down, q3movement_playerRadius); 339 | if ( !trace.allSolid ) { 340 | vec3.copy(this.position, trace.endPos); 341 | } 342 | if ( trace.fraction < 1.0 ) { 343 | this.velocity = this.clipVelocity( this.velocity, trace.plane.normal ); 344 | } 345 | }; -------------------------------------------------------------------------------- /js/q3shader.js: -------------------------------------------------------------------------------- 1 | /* 2 | * q3shader.js - Parses Quake 3 shader files (.shader) 3 | */ 4 | 5 | /* 6 | * Copyright (c) 2009 Brandon Jones 7 | * 8 | * This software is provided 'as-is', without any express or implied 9 | * warranty. In no event will the authors be held liable for any damages 10 | * arising from the use of this software. 11 | * 12 | * Permission is granted to anyone to use this software for any purpose, 13 | * including commercial applications, and to alter it and redistribute it 14 | * freely, subject to the following restrictions: 15 | * 16 | * 1. The origin of this software must not be misrepresented; you must not 17 | * claim that you wrote the original software. If you use this software 18 | * in a product, an acknowledgment in the product documentation would be 19 | * appreciated but is not required. 20 | * 21 | * 2. Altered source versions must be plainly marked as such, and must not 22 | * be misrepresented as being the original software. 23 | * 24 | * 3. This notice may not be removed or altered from any source 25 | * distribution. 26 | */ 27 | 28 | // 29 | // Shader Tokenizer 30 | // 31 | 32 | shaderTokenizer = function(src) { 33 | // Strip out comments 34 | src = src.replace(/\/\/.*$/mg, ''); // C++ style (//...) 35 | src = src.replace(/\/\*[^*\/]*\*\//mg, ''); // C style (/*...*/) (Do the shaders even use these?) 36 | this.tokens = src.match(/[^\s\n\r\"]+/mg); 37 | 38 | this.offset = 0; 39 | }; 40 | 41 | shaderTokenizer.prototype.EOF = function() { 42 | if(this.tokens === null) { return true; } 43 | var token = this.tokens[this.offset]; 44 | while(token === '' && this.offset < this.tokens.length) { 45 | this.offset++; 46 | token = this.tokens[this.offset]; 47 | } 48 | return this.offset >= this.tokens.length; 49 | }; 50 | 51 | shaderTokenizer.prototype.next = function() { 52 | if(this.tokens === null) { return ; } 53 | var token = ''; 54 | while(token === '' && this.offset < this.tokens.length) { 55 | token = this.tokens[this.offset++]; 56 | } 57 | return token; 58 | }; 59 | 60 | shaderTokenizer.prototype.prev = function() { 61 | if(this.tokens === null) { return ; } 62 | var token = ''; 63 | while(token === '' && this.offset >= 0) { 64 | token = this.tokens[this.offset--]; 65 | } 66 | return token; 67 | }; 68 | 69 | // 70 | // Shader Loading 71 | // 72 | 73 | q3shader = {}; 74 | 75 | q3shader.loadList = function(sources, onload) { 76 | for(var i = 0; i < sources.length; ++i) { 77 | q3shader.load(sources[i], onload); 78 | } 79 | }; 80 | 81 | q3shader.load = function(url, onload) { 82 | var request = new XMLHttpRequest(); 83 | 84 | request.onreadystatechange = function () { 85 | if (request.readyState == 4 && request.status == 200) { 86 | q3shader.parse(url, request.responseText, onload); 87 | } 88 | }; 89 | 90 | request.open('GET', url, true); 91 | request.setRequestHeader('Content-Type', 'text/plain'); 92 | request.send(null); 93 | }; 94 | 95 | q3shader.parse = function(url, src, onload) { 96 | var shaders = []; 97 | 98 | var tokens = new shaderTokenizer(src); 99 | 100 | // Parse a shader 101 | while(!tokens.EOF()) { 102 | var name = tokens.next(); 103 | var shader = q3shader.parseShader(name, tokens); 104 | if(shader) { 105 | shader.url = url; 106 | 107 | if(shader.stages) { 108 | for(var i = 0; i < shader.stages.length; ++i) { 109 | // Build a WebGL shader program out of the stage parameters set here 110 | shader.stages[i].shaderSrc = q3shader.buildShaderSource(shader, shader.stages[i]); 111 | } 112 | } 113 | } 114 | shaders.push(shader); 115 | } 116 | 117 | // Send shaders to gl Thread 118 | onload(shaders); 119 | }; 120 | 121 | q3shader.parseShader = function(name, tokens) { 122 | var brace = tokens.next(); 123 | if(brace != '{') { 124 | return null; 125 | } 126 | 127 | var shader = { 128 | name: name, 129 | cull: 'back', 130 | sky: false, 131 | blend: false, 132 | opaque: false, 133 | sort: 0, 134 | vertexDeforms: [], 135 | stages: [] 136 | }; 137 | 138 | // Parse a shader 139 | while(!tokens.EOF()) { 140 | var token = tokens.next().toLowerCase(); 141 | if(token == '}') { break; } 142 | 143 | switch (token) { 144 | case '{': { 145 | var stage = q3shader.parseStage(tokens); 146 | 147 | // I really really really don't like doing this, which basically just forces lightmaps to use the 'filter' blendmode 148 | // but if I don't a lot of textures end up looking too bright. I'm sure I'm jsut missing something, and this shouldn't 149 | // be needed. 150 | if(stage.isLightmap && (stage.hasBlendFunc)) { 151 | stage.blendSrc = 'GL_DST_COLOR'; 152 | stage.blendDest = 'GL_ZERO'; 153 | } 154 | 155 | // I'm having a ton of trouble getting lightingSpecular to work properly, 156 | // so this little hack gets it looking right till I can figure out the problem 157 | if(stage.alphaGen == 'lightingspecular') { 158 | stage.blendSrc = 'GL_ONE'; 159 | stage.blendDest = 'GL_ZERO'; 160 | stage.hasBlendFunc = false; 161 | stage.depthWrite = true; 162 | shader.stages = []; 163 | } 164 | 165 | if(stage.hasBlendFunc) { shader.blend = true; } else { shader.opaque = true; } 166 | 167 | shader.stages.push(stage); 168 | } break; 169 | 170 | case 'cull': 171 | shader.cull = tokens.next(); 172 | break; 173 | 174 | case 'deformvertexes': 175 | var deform = { 176 | type: tokens.next().toLowerCase() 177 | }; 178 | 179 | switch(deform.type) { 180 | case 'wave': 181 | deform.spread = 1.0 / parseFloat(tokens.next()); 182 | deform.waveform = q3shader.parseWaveform(tokens); 183 | break; 184 | default: deform = null; break; 185 | } 186 | 187 | if(deform) { shader.vertexDeforms.push(deform); } 188 | break; 189 | 190 | case 'sort': 191 | var sort = tokens.next().toLowerCase(); 192 | switch(sort) { 193 | case 'portal': shader.sort = 1; break; 194 | case 'sky': shader.sort = 2; break; 195 | case 'opaque': shader.sort = 3; break; 196 | case 'banner': shader.sort = 6; break; 197 | case 'underwater': shader.sort = 8; break; 198 | case 'additive': shader.sort = 9; break; 199 | case 'nearest': shader.sort = 16; break; 200 | default: shader.sort = parseInt(sort); break; 201 | }; 202 | break; 203 | 204 | case 'surfaceparm': 205 | var param = tokens.next().toLowerCase(); 206 | 207 | switch(param) { 208 | case 'sky': 209 | shader.sky = true; 210 | break; 211 | default: break; 212 | } 213 | break; 214 | 215 | default: break; 216 | } 217 | } 218 | 219 | if(!shader.sort) { 220 | shader.sort = (shader.opaque ? 3 : 9); 221 | } 222 | 223 | return shader; 224 | }; 225 | 226 | q3shader.parseStage = function(tokens) { 227 | var stage = { 228 | map: null, 229 | clamp: false, 230 | tcGen: 'base', 231 | rgbGen: 'identity', 232 | rgbWaveform: null, 233 | alphaGen: '1.0', 234 | alphaFunc: null, 235 | alphaWaveform: null, 236 | blendSrc: 'GL_ONE', 237 | blendDest: 'GL_ZERO', 238 | hasBlendFunc: false, 239 | tcMods: [], 240 | animMaps: [], 241 | animFreq: 0, 242 | depthFunc: 'lequal', 243 | depthWrite: true 244 | }; 245 | 246 | // Parse a shader 247 | while(!tokens.EOF()) { 248 | var token = tokens.next(); 249 | if(token == '}') { break; } 250 | 251 | switch(token.toLowerCase()) { 252 | case 'clampmap': 253 | stage.clamp = true; 254 | case 'map': 255 | stage.map = tokens.next().replace(/(\.jpg|\.tga)/, '.png'); 256 | break; 257 | 258 | case 'animmap': 259 | stage.map = 'anim'; 260 | stage.animFreq = parseFloat(tokens.next()); 261 | var nextMap = tokens.next(); 262 | while(nextMap.match(/(\.jpg|\.tga)/)) { 263 | stage.animMaps.push(nextMap.replace(/(\.jpg|\.tga)/, '.png')); 264 | nextMap = tokens.next(); 265 | } 266 | tokens.prev(); 267 | break; 268 | 269 | case 'rgbgen': 270 | stage.rgbGen = tokens.next().toLowerCase();; 271 | switch(stage.rgbGen) { 272 | case 'wave': 273 | stage.rgbWaveform = q3shader.parseWaveform(tokens); 274 | if(!stage.rgbWaveform) { stage.rgbGen == 'identity'; } 275 | break; 276 | }; 277 | break; 278 | 279 | case 'alphagen': 280 | stage.alphaGen = tokens.next().toLowerCase(); 281 | switch(stage.alphaGen) { 282 | case 'wave': 283 | stage.alphaWaveform = q3shader.parseWaveform(tokens); 284 | if(!stage.alphaWaveform) { stage.alphaGen == '1.0'; } 285 | break; 286 | default: break; 287 | }; 288 | break; 289 | 290 | case 'alphafunc': 291 | stage.alphaFunc = tokens.next().toUpperCase(); 292 | break; 293 | 294 | case 'blendfunc': 295 | stage.blendSrc = tokens.next(); 296 | stage.hasBlendFunc = true; 297 | if(!stage.depthWriteOverride) { 298 | stage.depthWrite = false; 299 | } 300 | switch(stage.blendSrc) { 301 | case 'add': 302 | stage.blendSrc = 'GL_ONE'; 303 | stage.blendDest = 'GL_ONE'; 304 | break; 305 | 306 | case 'blend': 307 | stage.blendSrc = 'GL_SRC_ALPHA'; 308 | stage.blendDest = 'GL_ONE_MINUS_SRC_ALPHA'; 309 | break; 310 | 311 | case 'filter': 312 | stage.blendSrc = 'GL_DST_COLOR'; 313 | stage.blendDest = 'GL_ZERO'; 314 | break; 315 | 316 | default: 317 | stage.blendDest = tokens.next(); 318 | break; 319 | } 320 | break; 321 | 322 | case 'depthfunc': 323 | stage.depthFunc = tokens.next().toLowerCase(); 324 | break; 325 | 326 | case 'depthwrite': 327 | stage.depthWrite = true; 328 | stage.depthWriteOverride = true; 329 | break; 330 | 331 | case 'tcmod': 332 | var tcMod = { 333 | type: tokens.next().toLowerCase() 334 | } 335 | switch(tcMod.type) { 336 | case 'rotate': 337 | tcMod.angle = parseFloat(tokens.next()) * (3.1415/180); 338 | break; 339 | case 'scale': 340 | tcMod.scaleX = parseFloat(tokens.next()); 341 | tcMod.scaleY = parseFloat(tokens.next()); 342 | break; 343 | case 'scroll': 344 | tcMod.sSpeed = parseFloat(tokens.next()); 345 | tcMod.tSpeed = parseFloat(tokens.next()); 346 | break; 347 | case 'stretch': 348 | tcMod.waveform = q3shader.parseWaveform(tokens); 349 | if(!tcMod.waveform) { tcMod.type == null; } 350 | break; 351 | case 'turb': 352 | tcMod.turbulance = { 353 | base: parseFloat(tokens.next()), 354 | amp: parseFloat(tokens.next()), 355 | phase: parseFloat(tokens.next()), 356 | freq: parseFloat(tokens.next()) 357 | }; 358 | break; 359 | default: tcMod.type == null; break; 360 | } 361 | if(tcMod.type) { 362 | stage.tcMods.push(tcMod); 363 | } 364 | break; 365 | case 'tcgen': 366 | stage.tcGen = tokens.next(); 367 | break; 368 | default: break; 369 | } 370 | } 371 | 372 | if(stage.blendSrc == 'GL_ONE' && stage.blendDest == 'GL_ZERO') { 373 | stage.hasBlendFunc = false; 374 | stage.depthWrite = true; 375 | } 376 | 377 | stage.isLightmap = stage.map == '$lightmap' 378 | 379 | return stage; 380 | }; 381 | 382 | q3shader.parseWaveform = function(tokens) { 383 | return { 384 | funcName: tokens.next().toLowerCase(), 385 | base: parseFloat(tokens.next()), 386 | amp: parseFloat(tokens.next()), 387 | phase: parseFloat(tokens.next()), 388 | freq: parseFloat(tokens.next()) 389 | }; 390 | }; 391 | 392 | // 393 | // WebGL Shader creation 394 | // 395 | 396 | // This whole section is a bit ugly, but it gets the job done. The job, in this case, is translating 397 | // Quake 3 shaders into GLSL shader programs. We should probably be doing a bit more normalization here. 398 | 399 | q3shader.buildShaderSource = function(shader, stage) { 400 | return { 401 | vertex: q3shader.buildVertexShader(shader, stage), 402 | fragment: q3shader.buildFragmentShader(shader, stage) 403 | }; 404 | } 405 | 406 | q3shader.buildVertexShader = function(stageShader, stage) { 407 | var shader = new shaderBuilder(); 408 | 409 | shader.addAttribs({ 410 | position: 'vec3', 411 | normal: 'vec3', 412 | color: 'vec4', 413 | }); 414 | 415 | shader.addVaryings({ 416 | vTexCoord: 'vec2', 417 | vColor: 'vec4', 418 | }); 419 | 420 | shader.addUniforms({ 421 | modelViewMat: 'mat4', 422 | projectionMat: 'mat4', 423 | time: 'float', 424 | }); 425 | 426 | if(stage.isLightmap) { 427 | shader.addAttribs({ lightCoord: 'vec2' }); 428 | } else { 429 | shader.addAttribs({ texCoord: 'vec2' }); 430 | } 431 | 432 | shader.addLines(['vec3 defPosition = position;']); 433 | 434 | for(var i = 0; i < stageShader.vertexDeforms.length; ++i) { 435 | var deform = stageShader.vertexDeforms[i]; 436 | 437 | switch(deform.type) { 438 | case 'wave': 439 | var name = 'deform' + i; 440 | var offName = 'deformOff' + i; 441 | 442 | shader.addLines([ 443 | 'float ' + offName + ' = (position.x + position.y + position.z) * ' + deform.spread.toFixed(4) + ';' 444 | ]); 445 | 446 | var phase = deform.waveform.phase; 447 | deform.waveform.phase = phase.toFixed(4) + ' + ' + offName; 448 | shader.addWaveform(name, deform.waveform); 449 | deform.waveform.phase = phase; 450 | 451 | shader.addLines([ 452 | 'defPosition += normal * ' + name + ';' 453 | ]); 454 | break; 455 | default: break; 456 | } 457 | } 458 | 459 | shader.addLines(['vec4 worldPosition = modelViewMat * vec4(defPosition, 1.0);']); 460 | shader.addLines(['vColor = color;']); 461 | 462 | if(stage.tcGen == 'environment') { 463 | shader.addLines([ 464 | 'vec3 viewer = normalize(-worldPosition.xyz);', 465 | 'float d = dot(normal, viewer);', 466 | 'vec3 reflected = normal*2.0*d - viewer;', 467 | 'vTexCoord = vec2(0.5, 0.5) + reflected.xy * 0.5;' 468 | ]); 469 | } else { 470 | // Standard texturing 471 | if(stage.isLightmap) { 472 | shader.addLines(['vTexCoord = lightCoord;']); 473 | } else { 474 | shader.addLines(['vTexCoord = texCoord;']); 475 | } 476 | } 477 | 478 | // tcMods 479 | for(var i = 0; i < stage.tcMods.length; ++i) { 480 | var tcMod = stage.tcMods[i]; 481 | switch(tcMod.type) { 482 | case 'rotate': 483 | shader.addLines([ 484 | 'float r = ' + tcMod.angle.toFixed(4) + ' * time;', 485 | 'vTexCoord -= vec2(0.5, 0.5);', 486 | 'vTexCoord = vec2(vTexCoord.s * cos(r) - vTexCoord.t * sin(r), vTexCoord.t * cos(r) + vTexCoord.s * sin(r));', 487 | 'vTexCoord += vec2(0.5, 0.5);', 488 | ]); 489 | break; 490 | case 'scroll': 491 | shader.addLines([ 492 | 'vTexCoord += vec2(' + tcMod.sSpeed.toFixed(4) + ' * time, ' + tcMod.tSpeed.toFixed(4) + ' * time);' 493 | ]); 494 | break; 495 | case 'scale': 496 | shader.addLines([ 497 | 'vTexCoord *= vec2(' + tcMod.scaleX.toFixed(4) + ', ' + tcMod.scaleY.toFixed(4) + ');' 498 | ]); 499 | break; 500 | case 'stretch': 501 | shader.addWaveform('stretchWave', tcMod.waveform); 502 | shader.addLines([ 503 | 'stretchWave = 1.0 / stretchWave;', 504 | 'vTexCoord *= stretchWave;', 505 | 'vTexCoord += vec2(0.5 - (0.5 * stretchWave), 0.5 - (0.5 * stretchWave));', 506 | ]); 507 | break; 508 | case 'turb': 509 | var tName = 'turbTime' + i; 510 | shader.addLines([ 511 | 'float ' + tName + ' = ' + tcMod.turbulance.phase.toFixed(4) + ' + time * ' + tcMod.turbulance.freq.toFixed(4) + ';', 512 | 'vTexCoord.s += sin( ( ( position.x + position.z )* 1.0/128.0 * 0.125 + ' + tName + ' ) * 6.283) * ' + tcMod.turbulance.amp.toFixed(4) + ';', 513 | 'vTexCoord.t += sin( ( position.y * 1.0/128.0 * 0.125 + ' + tName + ' ) * 6.283) * ' + tcMod.turbulance.amp.toFixed(4) + ';' 514 | ]); 515 | break; 516 | default: break; 517 | } 518 | } 519 | 520 | switch(stage.alphaGen) { 521 | case 'lightingspecular': 522 | shader.addAttribs({ lightCoord: 'vec2' }); 523 | shader.addVaryings({ vLightCoord: 'vec2' }); 524 | shader.addLines([ 'vLightCoord = lightCoord;' ]); 525 | break; 526 | default: 527 | break; 528 | } 529 | 530 | shader.addLines(['gl_Position = projectionMat * worldPosition;']); 531 | 532 | return shader.getSource(); 533 | 534 | } 535 | 536 | q3shader.buildFragmentShader = function(stageShader, stage) { 537 | var shader = new shaderBuilder(); 538 | 539 | shader.addVaryings({ 540 | vTexCoord: 'vec2', 541 | vColor: 'vec4', 542 | }); 543 | 544 | shader.addUniforms({ 545 | texture: 'sampler2D', 546 | time: 'float', 547 | }); 548 | 549 | shader.addLines(['vec4 texColor = texture2D(texture, vTexCoord.st);']); 550 | 551 | switch(stage.rgbGen) { 552 | case 'vertex': 553 | shader.addLines(['vec3 rgb = texColor.rgb * vColor.rgb;']); 554 | break; 555 | case 'wave': 556 | shader.addWaveform('rgbWave', stage.rgbWaveform); 557 | shader.addLines(['vec3 rgb = texColor.rgb * rgbWave;']); 558 | break; 559 | default: 560 | shader.addLines(['vec3 rgb = texColor.rgb;']); 561 | break; 562 | } 563 | 564 | switch(stage.alphaGen) { 565 | case 'wave': 566 | shader.addWaveform('alpha', stage.alphaWaveform); 567 | break; 568 | case 'lightingspecular': 569 | // For now this is VERY special cased. May not work well with all instances of lightingSpecular 570 | shader.addUniforms({ 571 | lightmap: 'sampler2D' 572 | }); 573 | shader.addVaryings({ 574 | vLightCoord: 'vec2', 575 | vLight: 'float' 576 | }); 577 | shader.addLines([ 578 | 'vec4 light = texture2D(lightmap, vLightCoord.st);', 579 | 'rgb *= light.rgb;', 580 | 'rgb += light.rgb * texColor.a * 0.6;', // This was giving me problems, so I'm ignorning an actual specular calculation for now 581 | 'float alpha = 1.0;' 582 | ]); 583 | break; 584 | default: 585 | shader.addLines(['float alpha = texColor.a;']); 586 | break; 587 | } 588 | 589 | if(stage.alphaFunc) { 590 | switch(stage.alphaFunc) { 591 | case 'GT0': 592 | shader.addLines([ 593 | 'if(alpha == 0.0) { discard; }' 594 | ]); 595 | break; 596 | case 'LT128': 597 | shader.addLines([ 598 | 'if(alpha >= 0.5) { discard; }' 599 | ]); 600 | break; 601 | case 'GE128': 602 | shader.addLines([ 603 | 'if(alpha < 0.5) { discard; }' 604 | ]); 605 | break; 606 | default: 607 | break; 608 | } 609 | } 610 | 611 | shader.addLines(['gl_FragColor = vec4(rgb, alpha);']); 612 | 613 | return shader.getSource(); 614 | } 615 | 616 | // 617 | // WebGL Shader builder utility 618 | // 619 | 620 | shaderBuilder = function() { 621 | this.attrib = {}; 622 | this.varying = {}; 623 | this.uniform = {}; 624 | 625 | this.functions = {}; 626 | 627 | this.statements = []; 628 | } 629 | 630 | shaderBuilder.prototype.addAttribs = function(attribs) { 631 | for (var name in attribs) { 632 | this.attrib[name] = 'attribute ' + attribs[name] + ' ' + name + ';' 633 | } 634 | } 635 | 636 | shaderBuilder.prototype.addVaryings = function(varyings) { 637 | for (var name in varyings) { 638 | this.varying[name] = 'varying ' + varyings[name] + ' ' + name + ';' 639 | } 640 | } 641 | 642 | shaderBuilder.prototype.addUniforms = function(uniforms) { 643 | for (var name in uniforms) { 644 | this.uniform[name] = 'uniform ' + uniforms[name] + ' ' + name + ';' 645 | } 646 | } 647 | 648 | shaderBuilder.prototype.addFunction = function(name, lines) { 649 | this.functions[name] = lines.join('\n'); 650 | } 651 | 652 | shaderBuilder.prototype.addLines = function(statements) { 653 | for(var i = 0; i < statements.length; ++i) { 654 | this.statements.push(statements[i]); 655 | } 656 | } 657 | 658 | shaderBuilder.prototype.getSource = function() { 659 | var src = '\ 660 | #ifdef GL_ES \n\ 661 | precision highp float; \n\ 662 | #endif \n'; 663 | 664 | for(var i in this.attrib) { 665 | src += this.attrib[i] + '\n'; 666 | } 667 | 668 | for(var i in this.varying) { 669 | src += this.varying[i] + '\n'; 670 | } 671 | 672 | for(var i in this.uniform) { 673 | src += this.uniform[i] + '\n'; 674 | } 675 | 676 | for(var i in this.functions) { 677 | src += this.functions[i] + '\n'; 678 | } 679 | 680 | src += 'void main(void) {\n\t'; 681 | src += this.statements.join('\n\t'); 682 | src += '\n}\n'; 683 | 684 | return src; 685 | } 686 | 687 | // q3-centric functions 688 | 689 | shaderBuilder.prototype.addWaveform = function(name, wf, timeVar) { 690 | if(!wf) { 691 | this.statements.push('float ' + name + ' = 0.0;'); 692 | return; 693 | } 694 | 695 | if(!timeVar) { timeVar = 'time'; } 696 | 697 | if(typeof(wf.phase) == "number") { 698 | wf.phase = wf.phase.toFixed(4) 699 | } 700 | 701 | switch(wf.funcName) { 702 | case 'sin': 703 | this.statements.push('float ' + name + ' = ' + wf.base.toFixed(4) + ' + sin((' + wf.phase + ' + ' + timeVar + ' * ' + wf.freq.toFixed(4) + ') * 6.283) * ' + wf.amp.toFixed(4) + ';'); 704 | return; 705 | case 'square': funcName = 'square'; this.addSquareFunc(); break; 706 | case 'triangle': funcName = 'triangle'; this.addTriangleFunc(); break; 707 | case 'sawtooth': funcName = 'fract'; break; 708 | case 'inversesawtooth': funcName = '1.0 - fract'; break; 709 | default: 710 | this.statements.push('float ' + name + ' = 0.0;'); 711 | return; 712 | } 713 | this.statements.push('float ' + name + ' = ' + wf.base.toFixed(4) + ' + ' + funcName + '(' + wf.phase + ' + ' + timeVar + ' * ' + wf.freq.toFixed(4) + ') * ' + wf.amp.toFixed(4) + ';'); 714 | } 715 | 716 | shaderBuilder.prototype.addSquareFunc = function() { 717 | this.addFunction('square', [ 718 | 'float square(float val) {', 719 | ' return (mod(floor(val*2.0)+1.0, 2.0) * 2.0) - 1.0;', 720 | '}', 721 | ]); 722 | } 723 | 724 | shaderBuilder.prototype.addTriangleFunc = function() { 725 | this.addFunction('triangle', [ 726 | 'float triangle(float val) {', 727 | ' return abs(2.0 * fract(val) - 1.0);', 728 | '}', 729 | ]); 730 | } -------------------------------------------------------------------------------- /js/util/binary-file.js: -------------------------------------------------------------------------------- 1 | /* 2 | * binFile.js - Binary Stream Reader 3 | * version 1.0 4 | */ 5 | 6 | /* 7 | * Copyright (c) 2011 Brandon Jones 8 | * 9 | * This software is provided 'as-is', without any express or implied 10 | * warranty. In no event will the authors be held liable for any damages 11 | * arising from the use of this software. 12 | * 13 | * Permission is granted to anyone to use this software for any purpose, 14 | * including commercial applications, and to alter it and redistribute it 15 | * freely, subject to the following restrictions: 16 | * 17 | * 1. The origin of this software must not be misrepresented; you must not 18 | * claim that you wrote the original software. If you use this software 19 | * in a product, an acknowledgment in the product documentation would be 20 | * appreciated but is not required. 21 | * 22 | * 2. Altered source versions must be plainly marked as such, and must not 23 | * be misrepresented as being the original software. 24 | * 25 | * 3. This notice may not be removed or altered from any source 26 | * distribution. 27 | */ 28 | 29 | BinaryFile = function(data) { 30 | this.buffer = data; 31 | this.length = data.length; 32 | this.offset = 0; 33 | }; 34 | 35 | // This is the result of an interesting trick that Google does in their 36 | // GWT port of Quake 2. (For floats, anyway...) Rather than parse and 37 | // calculate the values manually they share the contents of a byte array 38 | // between several types of buffers, which allows you to push into one and 39 | // read out the other. The end result is, effectively, a typecast! 40 | 41 | var bf_byteBuff = new ArrayBuffer(4); 42 | 43 | var bf_wba = new Int8Array(bf_byteBuff); 44 | var bf_wuba = new Uint8Array(bf_byteBuff); 45 | 46 | var bf_wsa = new Int16Array(bf_byteBuff); 47 | var bf_wusa = new Uint16Array(bf_byteBuff); 48 | 49 | var bf_wia = new Int32Array(bf_byteBuff); 50 | var bf_wuia = new Uint32Array(bf_byteBuff); 51 | 52 | var bf_wfa = new Float32Array(bf_byteBuff); 53 | 54 | BinaryFile.prototype.eof = function() { 55 | this.offset >= this.length; 56 | } 57 | 58 | // Seek to the given byt offset within the stream 59 | BinaryFile.prototype.seek = function(offest) { 60 | this.offset = offest; 61 | }; 62 | 63 | // Seek to the given byt offset within the stream 64 | BinaryFile.prototype.tell = function() { 65 | return this.offset; 66 | }; 67 | 68 | // Read a signed byte from the stream 69 | BinaryFile.prototype.readByte = function() { 70 | var b0 = this.buffer.charCodeAt(this.offset) & 0xff; 71 | this.offset += 1; 72 | return b0 - (b0 & 0x80); 73 | }; 74 | 75 | // Read an unsigned byte from the stream 76 | BinaryFile.prototype.readUByte = function() { 77 | var b0 = this.buffer.charCodeAt(this.offset) & 0xff; 78 | this.offset += 1; 79 | return b0; 80 | }; 81 | 82 | // Read a signed short (2 bytes) from the stream 83 | BinaryFile.prototype.readShort = function() { 84 | var off = this.offset; 85 | var buf = this.buffer; 86 | bf_wuba[0] = buf.charCodeAt(off) & 0xff; 87 | bf_wuba[1] = buf.charCodeAt(off+1) & 0xff; 88 | this.offset += 2; 89 | return bf_wsa[0]; 90 | }; 91 | 92 | // Read an unsigned short (2 bytes) from the stream 93 | BinaryFile.prototype.readUShort = function() { 94 | var off = this.offset; 95 | var buf = this.buffer; 96 | bf_wuba[0] = buf.charCodeAt(off) & 0xff; 97 | bf_wuba[1] = buf.charCodeAt(off+1) & 0xff; 98 | this.offset += 2; 99 | return bf_wusa[0]; 100 | }; 101 | 102 | // Read a signed long (4 bytes) from the stream 103 | BinaryFile.prototype.readLong = function() { 104 | var off = this.offset; 105 | var buf = this.buffer; 106 | bf_wuba[0] = buf.charCodeAt(off) & 0xff; 107 | bf_wuba[1] = buf.charCodeAt(off+1) & 0xff; 108 | bf_wuba[2] = buf.charCodeAt(off+2) & 0xff; 109 | bf_wuba[3] = buf.charCodeAt(off+3) & 0xff; 110 | this.offset += 4; 111 | return bf_wia[0]; 112 | }; 113 | 114 | // Read an unsigned long (4 bytes) from the stream 115 | BinaryFile.prototype.readULong = function() { 116 | var off = this.offset; 117 | var buf = this.buffer; 118 | bf_wuba[0] = buf.charCodeAt(off) & 0xff; 119 | bf_wuba[1] = buf.charCodeAt(off+1) & 0xff; 120 | bf_wuba[2] = buf.charCodeAt(off+2) & 0xff; 121 | bf_wuba[3] = buf.charCodeAt(off+3) & 0xff; 122 | this.offset += 4; 123 | return bf_wuia[0]; 124 | }; 125 | 126 | // Read a float (4 bytes) from the stream 127 | BinaryFile.prototype.readFloat = function() { 128 | var off = this.offset; 129 | var buf = this.buffer; 130 | bf_wuba[0] = buf.charCodeAt(off) & 0xff; 131 | bf_wuba[1] = buf.charCodeAt(off+1) & 0xff; 132 | bf_wuba[2] = buf.charCodeAt(off+2) & 0xff; 133 | bf_wuba[3] = buf.charCodeAt(off+3) & 0xff; 134 | this.offset += 4; 135 | return bf_wfa[0]; 136 | }; 137 | 138 | BinaryFile.prototype.expandHalf = function(h) { 139 | var s = (h & 0x8000) >> 15; 140 | var e = (h & 0x7C00) >> 10; 141 | var f = h & 0x03FF; 142 | 143 | if(e == 0) { 144 | return (s?-1:1) * Math.pow(2,-14) * (f/Math.pow(2, 10)); 145 | } else if (e == 0x1F) { 146 | return f?NaN:((s?-1:1)*Infinity); 147 | } 148 | 149 | return (s?-1:1) * Math.pow(2, e-15) * (1+(f/Math.pow(2, 10))); 150 | }; 151 | 152 | BinaryFile.prototype.readHalf = function() { 153 | var h = this.readUShort(); 154 | return this.expandHalf(h); 155 | } 156 | 157 | // Read an ASCII string of the given length from the stream 158 | BinaryFile.prototype.readString = function(length) { 159 | var str = this.buffer.substr(this.offset, length).replace(/\0+$/,''); 160 | this.offset += length; 161 | return str; 162 | }; 163 | 164 | -------------------------------------------------------------------------------- /js/util/game-shim.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview game-shim - Shims to normalize gaming-related APIs to their respective specs 3 | * @author Brandon Jones 4 | * @version 0.6 5 | */ 6 | 7 | /* 8 | * Copyright (c) 2011 Brandon Jones 9 | * 10 | * This software is provided 'as-is', without any express or implied 11 | * warranty. In no event will the authors be held liable for any damages 12 | * arising from the use of this software. 13 | * 14 | * Permission is granted to anyone to use this software for any purpose, 15 | * including commercial applications, and to alter it and redistribute it 16 | * freely, subject to the following restrictions: 17 | * 18 | * 1. The origin of this software must not be misrepresented; you must not 19 | * claim that you wrote the original software. If you use this software 20 | * in a product, an acknowledgment in the product documentation would be 21 | * appreciated but is not required. 22 | * 23 | * 2. Altered source versions must be plainly marked as such, and must not 24 | * be misrepresented as being the original software. 25 | * 26 | * 3. This notice may not be removed or altered from any source 27 | * distribution. 28 | */ 29 | 30 | (function(global) { 31 | "use strict"; 32 | 33 | var elementPrototype = (global.HTMLElement || global.Element)["prototype"]; 34 | var getter; 35 | 36 | var GameShim = global.GameShim = { 37 | supports: { 38 | fullscreen: true, 39 | pointerLock: true 40 | } 41 | }; 42 | 43 | //===================== 44 | // Fullscreen 45 | //===================== 46 | 47 | if(!("fullscreenElement" in document)) { 48 | getter = (function() { 49 | // These are the functions that match the spec, and should be preferred 50 | if("webkitFullscreenElement" in document) { 51 | return function() { return document.webkitFullscreenElement; }; 52 | } 53 | if("mozFullScreenElement" in document) { 54 | return function() { return document.mozFullScreenElement; }; 55 | } 56 | return function() { return null; }; // not supported 57 | })(); 58 | 59 | Object.defineProperty(document, "fullscreenElement", { 60 | enumerable: true, configurable: false, writeable: false, 61 | get: getter 62 | }); 63 | } 64 | 65 | // Document event: fullscreenchange 66 | function fullscreenchange(oldEvent) { 67 | var newEvent = document.createEvent("CustomEvent"); 68 | newEvent.initCustomEvent("fullscreenchange", true, false, null); 69 | // TODO: Any need for variable copy? 70 | document.dispatchEvent(newEvent); 71 | } 72 | document.addEventListener("webkitfullscreenchange", fullscreenchange, false); 73 | document.addEventListener("mozfullscreenchange", fullscreenchange, false); 74 | 75 | // Document event: fullscreenerror 76 | function fullscreenerror(oldEvent) { 77 | var newEvent = document.createEvent("CustomEvent"); 78 | newEvent.initCustomEvent("fullscreenerror", true, false, null); 79 | // TODO: Any need for variable copy? 80 | document.dispatchEvent(newEvent); 81 | } 82 | document.addEventListener("webkitfullscreenerror", fullscreenerror, false); 83 | document.addEventListener("mozfullscreenerror", fullscreenerror, false); 84 | 85 | // element.requestFullScreen 86 | if(!("requestFullScreen" in elementPrototype)) { 87 | elementPrototype.requestFullScreen = (function() { 88 | if("webkitRequestFullScreen" in elementPrototype) { 89 | return elementPrototype.webkitRequestFullScreen; 90 | } 91 | 92 | if("mozRequestFullScreen" in elementPrototype) { 93 | return elementPrototype.mozRequestFullScreen; 94 | } 95 | 96 | return function(){ /* unsupported, fail silently */ }; 97 | })(); 98 | } 99 | 100 | // document.exitFullScreen 101 | if(!("exitFullScreen" in document)) { 102 | document.exitFullScreen = (function() { 103 | if("webkitExitFullScreen" in document) { 104 | return document.webkitExitFullScreen; 105 | } 106 | 107 | if("mozExitFullScreen" in document) { 108 | return document.mozExitFullScreen; 109 | } 110 | 111 | return function(){ /* unsupported, fail silently */ }; 112 | })(); 113 | } 114 | 115 | //===================== 116 | // Pointer Lock 117 | //===================== 118 | 119 | var mouseEventPrototype = global.MouseEvent.prototype; 120 | 121 | if(!("movementX" in mouseEventPrototype)) { 122 | Object.defineProperty(mouseEventPrototype, "movementX", { 123 | enumerable: true, configurable: false, writeable: false, 124 | get: function() { return this.webkitMovementX || this.mozMovementX || 0; } 125 | }); 126 | } 127 | 128 | if(!("movementY" in mouseEventPrototype)) { 129 | Object.defineProperty(mouseEventPrototype, "movementY", { 130 | enumerable: true, configurable: false, writeable: false, 131 | get: function() { return this.webkitMovementY || this.mozMovementY || 0; } 132 | }); 133 | } 134 | 135 | // Navigator pointer is not the right interface according to spec. 136 | // Here for backwards compatibility only 137 | if(!navigator.pointer) { 138 | navigator.pointer = navigator.webkitPointer || navigator.mozPointer; 139 | } 140 | 141 | // Document event: pointerlockchange 142 | function pointerlockchange(oldEvent) { 143 | var newEvent = document.createEvent("CustomEvent"); 144 | newEvent.initCustomEvent("pointerlockchange", true, false, null); 145 | document.dispatchEvent(newEvent); 146 | } 147 | document.addEventListener("webkitpointerlockchange", pointerlockchange, false); 148 | document.addEventListener("webkitpointerlocklost", pointerlockchange, false); 149 | document.addEventListener("mozpointerlockchange", pointerlockchange, false); 150 | document.addEventListener("mozpointerlocklost", pointerlockchange, false); 151 | 152 | // Document event: pointerlockerror 153 | function pointerlockerror(oldEvent) { 154 | var newEvent = document.createEvent("CustomEvent"); 155 | newEvent.initCustomEvent("pointerlockerror", true, false, null); 156 | document.dispatchEvent(newEvent); 157 | } 158 | document.addEventListener("webkitpointerlockerror", pointerlockerror, false); 159 | document.addEventListener("mozpointerlockerror", pointerlockerror, false); 160 | 161 | // document.pointerLockEnabled 162 | if(!("pointerLockEnabled" in document)) { 163 | getter = (function() { 164 | // These are the functions that match the spec, and should be preferred 165 | if("webkitPointerLockEnabled" in document) { 166 | return function() { return document.webkitPointerLockEnabled; }; 167 | } 168 | if("mozPointerLockEnabled" in document) { 169 | return function() { return document.mozPointerLockEnabled; }; 170 | } 171 | 172 | GameShim.supports.pointerLock = false; 173 | return function() { return false; }; // not supported, never locked 174 | })(); 175 | 176 | Object.defineProperty(document, "pointerLockEnabled", { 177 | enumerable: true, configurable: false, writeable: false, 178 | get: getter 179 | }); 180 | } 181 | 182 | if(!("pointerLockElement" in document)) { 183 | getter = (function() { 184 | // These are the functions that match the spec, and should be preferred 185 | if("webkitPointerLockElement" in document) { 186 | return function() { return document.webkitPointerLockElement; }; 187 | } 188 | if("mozPointerLockElement" in document) { 189 | return function() { return document.mozPointerLockElement; }; 190 | } 191 | 192 | return function() { return null; }; // not supported 193 | })(); 194 | 195 | Object.defineProperty(document, "pointerLockElement", { 196 | enumerable: true, configurable: false, writeable: false, 197 | get: getter 198 | }); 199 | } 200 | 201 | // element.requestPointerLock 202 | if(!("requestPointerLock" in elementPrototype)) { 203 | elementPrototype.requestPointerLock = (function() { 204 | if("webkitRequestPointerLock" in elementPrototype) { 205 | return elementPrototype.webkitRequestPointerLock; 206 | } 207 | 208 | if("mozRequestPointerLock" in elementPrototype) { 209 | return elementPrototype.mozRequestPointerLock; 210 | } 211 | 212 | return function() { /* unsupported, fail silently */ }; 213 | })(); 214 | } 215 | 216 | // document.exitPointerLock 217 | if(!("exitPointerLock" in document)) { 218 | document.exitPointerLock = (function() { 219 | if("webkitExitPointerLock" in elementPrototype) { 220 | return document.webkitExitPointerLock; 221 | } 222 | 223 | if("mozExitPointerLock" in elementPrototype) { 224 | return document.mozExitPointerLock; 225 | } 226 | 227 | return function() { /* unsupported, fail silently */ }; 228 | })(); 229 | } 230 | 231 | })((typeof(exports) != 'undefined') ? global : window); // Account for CommonJS environments 232 | -------------------------------------------------------------------------------- /js/util/stats.min.js: -------------------------------------------------------------------------------- 1 | // stats.js - http://github.com/mrdoob/stats.js 2 | var Stats=function(){var l=Date.now(),m=l,g=0,n=Infinity,o=0,h=0,p=Infinity,q=0,r=0,s=0,f=document.createElement("div");f.id="stats";f.addEventListener("mousedown",function(b){b.preventDefault();t(++s%2)},!1);f.style.cssText="width:80px;opacity:0.9;cursor:pointer";var a=document.createElement("div");a.id="fps";a.style.cssText="padding:0 0 3px 3px;text-align:left;background-color:#002";f.appendChild(a);var i=document.createElement("div");i.id="fpsText";i.style.cssText="color:#0ff;font-family:Helvetica,Arial,sans-serif;font-size:9px;font-weight:bold;line-height:15px"; 3 | i.innerHTML="FPS";a.appendChild(i);var c=document.createElement("div");c.id="fpsGraph";c.style.cssText="position:relative;width:74px;height:30px;background-color:#0ff";for(a.appendChild(c);74>c.children.length;){var j=document.createElement("span");j.style.cssText="width:1px;height:30px;float:left;background-color:#113";c.appendChild(j)}var d=document.createElement("div");d.id="ms";d.style.cssText="padding:0 0 3px 3px;text-align:left;background-color:#020;display:none";f.appendChild(d);var k=document.createElement("div"); 4 | k.id="msText";k.style.cssText="color:#0f0;font-family:Helvetica,Arial,sans-serif;font-size:9px;font-weight:bold;line-height:15px";k.innerHTML="MS";d.appendChild(k);var e=document.createElement("div");e.id="msGraph";e.style.cssText="position:relative;width:74px;height:30px;background-color:#0f0";for(d.appendChild(e);74>e.children.length;)j=document.createElement("span"),j.style.cssText="width:1px;height:30px;float:left;background-color:#131",e.appendChild(j);var t=function(b){s=b;switch(s){case 0:a.style.display= 5 | "block";d.style.display="none";break;case 1:a.style.display="none",d.style.display="block"}};return{REVISION:12,domElement:f,setMode:t,begin:function(){l=Date.now()},end:function(){var b=Date.now();g=b-l;n=Math.min(n,g);o=Math.max(o,g);k.textContent=g+" MS ("+n+"-"+o+")";var a=Math.min(30,30-30*(g/200));e.appendChild(e.firstChild).style.height=a+"px";r++;b>m+1E3&&(h=Math.round(1E3*r/(b-m)),p=Math.min(p,h),q=Math.max(q,h),i.textContent=h+" FPS ("+p+"-"+q+")",a=Math.min(30,30-30*(h/100)),c.appendChild(c.firstChild).style.height= 6 | a+"px",m=b,r=0);return b},update:function(){l=this.end()}}};"object"===typeof module&&(module.exports=Stats); 7 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "webgl-quake3", 3 | "version": "0.0.0", 4 | "description": "WebGL Quake 3 Renderer", 5 | "main": "server.js", 6 | "scripts": { 7 | "start": "node server.js" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/toji/webgl-quake3.git" 12 | }, 13 | "author": "Brandon Jones", 14 | "bugs": { 15 | "url": "https://github.com/toji/webgl-quake3/issues" 16 | }, 17 | "dependencies": { 18 | "express": "4.17.1", 19 | "serve-index": "^1.9.1", 20 | "serve-static": "^1.14.1" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | // Simple Node.js server that I use to test my projects. 2 | // To run, you need to have node and the express package installed 3 | // http://nodejs.org/ 4 | // http://expressjs.com/ 5 | 6 | // Then simply run "node server" from the command line in this directory 7 | // at that point you can view the demo by visiting http://localhost:9000/index.html 8 | 9 | var express = require('express'); 10 | var serveStatic = require('serve-static'); 11 | var serveIndex = require('serve-index'); 12 | 13 | var app = express(); 14 | app.use(serveStatic(__dirname)); 15 | app.use(serveIndex(__dirname)); 16 | app.listen(9000); 17 | 18 | console.log('Server is now listening on port 9000'); 19 | --------------------------------------------------------------------------------