├── .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 |
48 | Sorry, but your browser does not support WebGL or does not have it enabled.
49 | To get a WebGL-enabled browser, please see:
50 |
51 | http://get.webgl.org/
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
Controls
61 |
62 | Look: Click and Drag
63 | Movement: W,A,S,D
64 | Jump: Space
65 | Respawn: R
66 |
67 |
68 |
69 |
78 |
See more WebGL demo's at my blog
79 |
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 |
--------------------------------------------------------------------------------