├── .gitignore ├── LICENSE ├── README.md ├── gl-matrix.js ├── gl-util.js ├── index.html ├── loaddata.js ├── main.js ├── rw.js ├── rwrender.js ├── rwstream.js ├── shaders.js ├── ui.js └── webgl.css /.gitignore: -------------------------------------------------------------------------------- 1 | iii/* 2 | vc/* 3 | sa/* 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 GTA modding 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Does not come with data. 2 | See it in action [here](http://gta.rockstarvision.com/vehicleviewer/). 3 | -------------------------------------------------------------------------------- /gl-util.js: -------------------------------------------------------------------------------- 1 | var gl; 2 | 3 | const ATTRIB_POS = 0; 4 | const ATTRIB_NORMAL = 1; 5 | const ATTRIB_COLOR = 2; 6 | const ATTRIB_TEXCOORDS0 = 3; 7 | const ATTRIB_TEXCOORDS1 = 4; 8 | 9 | function 10 | loadTexture(url) 11 | { 12 | if(gl === undefined) 13 | return null; 14 | 15 | const texid = gl.createTexture(); 16 | 17 | gl.bindTexture(gl.TEXTURE_2D, texid); 18 | gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, 19 | 1, 1, 0, gl.RGBA, gl.UNSIGNED_BYTE, 20 | new Uint8Array([255, 255, 255, 255])); 21 | 22 | const image = new Image(); 23 | image.onload = function() { 24 | gl.bindTexture(gl.TEXTURE_2D, texid); 25 | gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, 26 | gl.RGBA, gl.UNSIGNED_BYTE, image); 27 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.REPEAT); 28 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.REPEAT); 29 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR); 30 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR); 31 | }; 32 | image.src = url; 33 | 34 | return texid; 35 | } 36 | 37 | function 38 | loadShaders(vs, fs) 39 | { 40 | const shaderProgram = initShaderProgram(vs, fs); 41 | 42 | programInfo = { 43 | program: shaderProgram, 44 | a: [ 45 | gl.getAttribLocation(shaderProgram, 'in_pos'), 46 | gl.getAttribLocation(shaderProgram, 'in_normal'), 47 | gl.getAttribLocation(shaderProgram, 'in_color'), 48 | gl.getAttribLocation(shaderProgram, 'in_tex0'), 49 | gl.getAttribLocation(shaderProgram, 'in_tex1'), 50 | ], 51 | u: { 52 | u_proj: gl.getUniformLocation(shaderProgram, 'u_proj'), 53 | u_view: gl.getUniformLocation(shaderProgram, 'u_view'), 54 | u_world: gl.getUniformLocation(shaderProgram, 'u_world'), 55 | u_env: gl.getUniformLocation(shaderProgram, 'u_env'), 56 | u_matColor: gl.getUniformLocation(shaderProgram, 'u_matColor'), 57 | u_surfaceProps: gl.getUniformLocation(shaderProgram, 'u_surfaceProps'), 58 | u_ambLight: gl.getUniformLocation(shaderProgram, 'u_ambLight'), 59 | u_lightDir: gl.getUniformLocation(shaderProgram, 'u_lightDir'), 60 | u_lightCol: gl.getUniformLocation(shaderProgram, 'u_lightCol'), 61 | u_alphaRef: gl.getUniformLocation(shaderProgram, 'u_alphaRef'), 62 | u_tex0: gl.getUniformLocation(shaderProgram, 'tex0'), 63 | u_tex1: gl.getUniformLocation(shaderProgram, 'tex1'), 64 | u_tex2: gl.getUniformLocation(shaderProgram, 'tex2'), 65 | }, 66 | }; 67 | console.log(programInfo.u.u_alphaRef); 68 | return programInfo; 69 | } 70 | 71 | function 72 | initShaderProgram(vsSource, fsSource) 73 | { 74 | const vertexShader = loadShader(gl.VERTEX_SHADER, vsSource); 75 | const fragmentShader = loadShader(gl.FRAGMENT_SHADER, fsSource); 76 | 77 | const shaderProgram = gl.createProgram(); 78 | gl.attachShader(shaderProgram, vertexShader); 79 | gl.attachShader(shaderProgram, fragmentShader); 80 | gl.linkProgram(shaderProgram); 81 | 82 | if(!gl.getProgramParameter(shaderProgram, gl.LINK_STATUS)){ 83 | alert('Unable to initialize the shader program: ' + gl.getProgramInfoLog(shaderProgram)); 84 | return null; 85 | } 86 | 87 | return shaderProgram; 88 | } 89 | 90 | function 91 | loadShader(type, source) 92 | { 93 | const shader = gl.createShader(type); 94 | 95 | gl.shaderSource(shader, source); 96 | gl.compileShader(shader); 97 | 98 | if(!gl.getShaderParameter(shader, gl.COMPILE_STATUS)){ 99 | alert('An error occurred compiling the shaders: ' + gl.getShaderInfoLog(shader)); 100 | gl.deleteShader(shader); 101 | return null; 102 | } 103 | 104 | return shader; 105 | } 106 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | GTA Model Viewer 5 | 6 | 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | 15 |
16 | 17 |
18 | 21 |
22 | 23 |
24 | 25 | 26 | 27 | 28 |
29 |
30 | 31 |
32 |
33 | III | 34 | VC | 35 | SA 36 |
37 | 38 |
39 | 42 |
43 |
44 | 45 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 167 | 168 | -------------------------------------------------------------------------------- /loaddata.js: -------------------------------------------------------------------------------- 1 | var VehicleColours = []; 2 | var ModelInfos = {}; 3 | var ModelInfosName = {}; 4 | var CurrentModel = null; 5 | 6 | function 7 | SetCarColors(cols) 8 | { 9 | carColors = [ 10 | [ 0, 0, 0, 255 ], 11 | [ 0, 0, 0, 255 ], 12 | [ 0, 0, 0, 255 ], 13 | [ 0, 0, 0, 255 ] 14 | ]; 15 | if(cols !== undefined) 16 | for(let i = 0; i < cols.length; i++){ 17 | // GTA color ID 18 | let colorID = cols[i]; 19 | 20 | // RGB color 21 | let col = VehicleColours[colorID]; 22 | if(!col) { 23 | continue; 24 | } 25 | 26 | carColors[i][0] = col[0]; 27 | carColors[i][1] = col[1]; 28 | carColors[i][2] = col[2]; 29 | } 30 | } 31 | 32 | function 33 | LoadColors(cols) 34 | { 35 | SetCarColors(cols); 36 | setVehicleColors(modelinfo, carColors[0], carColors[1], carColors[2], carColors[3]); 37 | } 38 | 39 | function 40 | SelectModel(model) 41 | { 42 | let colortable = document.getElementById('colors'); 43 | removeChildren(colortable); 44 | 45 | CurrentModel = ModelInfosName[model]; 46 | let col1 = [ 0, 0, 0, 255 ]; 47 | let col2 = [ 0, 0, 0, 255 ]; 48 | for(let i = 0; i < CurrentModel.colors.length; i++){ 49 | let c = CurrentModel.colors[i]; 50 | let c1 = VehicleColours[c[0]]; 51 | let c2 = VehicleColours[c[1]]; 52 | if(i == 0){ 53 | col1[0] = c1[0]; 54 | col1[1] = c1[1]; 55 | col1[2] = c1[2]; 56 | col2[0] = c2[0]; 57 | col2[1] = c2[1]; 58 | col2[2] = c2[2]; 59 | } 60 | let tr = document.createElement('tr'); 61 | for(let j = 0; j < c.length; j++){ 62 | let td = document.createElement('td'); 63 | td.width = "16px"; 64 | td.height = "16px"; 65 | 66 | // GTA color ID 67 | let colorID = c[j]; 68 | 69 | // RGB color 70 | let col = VehicleColours[colorID]; 71 | if(!col) { 72 | continue; 73 | } 74 | 75 | td.style = "background-color: rgb("+col[0]+","+col[1]+","+col[2]+")"; 76 | tr.appendChild(td); 77 | } 78 | tr.onclick = function() { LoadColors(c); }; 79 | colortable.appendChild(tr); 80 | } 81 | 82 | camDist = 5.0; 83 | camPitch = 0.3; 84 | camYaw = 1.0; 85 | SetCarColors(CurrentModel.colors[0]); 86 | 87 | loadCar(model + ".dff"); 88 | 89 | window.location.hash = currentGame + '/' + model; 90 | } 91 | 92 | function 93 | SelectModelByID(modelID) { 94 | model = ModelInfos[modelID]; 95 | SelectModel(model.model); 96 | } 97 | 98 | function 99 | StartUI() 100 | { 101 | let objects = document.getElementById('objects'); 102 | removeChildren(objects); 103 | for(let model in ModelInfosName){ 104 | let option = document.createElement('option'); 105 | option.innerHTML = model; 106 | option.onclick = function(){ SelectModel(model); }; 107 | objects.appendChild(option); 108 | } 109 | } 110 | 111 | function 112 | LoadVehicle(fields) 113 | { 114 | let id = Number(fields[0]); 115 | let mi = {}; 116 | mi.id = id; 117 | mi.type = "vehicle"; 118 | mi.model = fields[1]; 119 | mi.txd = fields[2]; 120 | mi.vehtype = fields[3]; 121 | mi.handling = fields[4]; 122 | if(mi.vehtype == "car"){ 123 | // TODO: check SA 124 | mi.wheelId = Number(fields[11]); 125 | mi.wheelScale = Number(fields[12]); 126 | } 127 | mi.colors = []; 128 | ModelInfos[mi.id] = mi; 129 | ModelInfosName[mi.model] = mi; 130 | } 131 | 132 | function 133 | LoadColour(fields) 134 | { 135 | let r = Number(fields[0]); 136 | let g = Number(fields[1]); 137 | let b = Number(fields[2]); 138 | VehicleColours.push([r, g, b]); 139 | } 140 | 141 | function 142 | LoadVehicleColour(fields) 143 | { 144 | let mi = ModelInfosName[fields[0]]; 145 | for(let i = 1; i < fields.length; i += 2){ 146 | let c1 = Number(fields[i]); 147 | let c2 = Number(fields[i+1]); 148 | mi.colors.push([c1, c2]); 149 | } 150 | } 151 | 152 | function 153 | LoadVehicleColour4(fields) 154 | { 155 | let mi = ModelInfosName[fields[0]]; 156 | for(let i = 1; i < fields.length; i += 4){ 157 | let c1 = Number(fields[i]); 158 | let c2 = Number(fields[i+1]); 159 | let c3 = Number(fields[i+2]); 160 | let c4 = Number(fields[i+3]); 161 | mi.colors.push([c1, c2, c3, c4]); 162 | } 163 | } 164 | 165 | function 166 | LoadObjectTypes(text) 167 | { 168 | LoadSectionedFile(text, { 169 | "cars": LoadVehicle 170 | }); 171 | } 172 | 173 | function 174 | LoadVehicleColours(text) 175 | { 176 | LoadSectionedFile(text, { 177 | "col": LoadColour, 178 | "car": LoadVehicleColour, 179 | "car4": LoadVehicleColour4 180 | }); 181 | } 182 | 183 | function 184 | LoadSectionedFile(text, sections) 185 | { 186 | let section = "end"; 187 | let lines = text.split("\n"); 188 | for(let i = 0; i < lines.length; i++){ 189 | let line = lines[i].replace(/,/g, " ").replace(/#.*/g, "").trim().toLowerCase(); 190 | if(line.length == 0) 191 | continue; 192 | let fields = line.split(/[\t ]+/); 193 | 194 | if(section == "end"){ 195 | section = fields[0]; 196 | continue; 197 | } 198 | if(fields[0] == "end"){ 199 | section = "end"; 200 | continue; 201 | } 202 | 203 | if(section in sections) 204 | sections[section](fields); 205 | } 206 | } 207 | 208 | function 209 | loadText(filename, cb) 210 | { 211 | let req = new XMLHttpRequest(); 212 | req.open("GET", DataDirPath + "/" + filename, true); 213 | req.responseType = "text"; 214 | 215 | req.onload = function(oEvent){ 216 | cb(req.response); 217 | }; 218 | 219 | req.send(null); 220 | } 221 | 222 | function 223 | loadVehicleViewer(idefile, CB) 224 | { 225 | VehicleColours = []; 226 | ModelInfos = {}; 227 | ModelInfosName = {}; 228 | CurrentModel = null; 229 | 230 | loadText(idefile, function(text){ 231 | LoadObjectTypes(text); 232 | loadText("carcols.dat", function(text){ 233 | LoadVehicleColours(text); 234 | StartUI(); 235 | CB(); 236 | }); 237 | }); 238 | } 239 | -------------------------------------------------------------------------------- /main.js: -------------------------------------------------------------------------------- 1 | var DataDirPath; 2 | var ModelsDirPath; 3 | var TexturesDirPath; 4 | 5 | // init info 6 | var isIIICar; 7 | var isSACar; 8 | var carColors; 9 | 10 | // the scene 11 | var running = false; 12 | var myclump; 13 | var modelinfo; 14 | var camPitch; 15 | var camYaw; 16 | var camDist; 17 | 18 | // gl things 19 | var state = {}; 20 | var whitetex; 21 | var camera; 22 | 23 | var envFrame; 24 | var defaultProgram; 25 | var envMapProgram; 26 | var carPS2Program; 27 | 28 | var backgroundColor = [0, 0, 0, 0]; 29 | 30 | function deg2rad(d) { return d / 180.0 * Math.PI; } 31 | 32 | var rotating, zooming; 33 | 34 | function 35 | mouseDown(e) 36 | { 37 | if(e.button == 0) 38 | rotating = true; 39 | else if(e.button == 1) 40 | zooming = true; 41 | old_x = e.pageX; 42 | old_y = e.pageY; 43 | e.preventDefault(); 44 | } 45 | 46 | function 47 | mouseUp(e) 48 | { 49 | rotating = false; 50 | zooming = false; 51 | } 52 | 53 | function 54 | mouseMove(e) 55 | { 56 | let dX, dY; 57 | if(rotating){ 58 | dX = (e.pageX-old_x)*2*Math.PI/gl.canvas.width, 59 | dY = (e.pageY-old_y)*2*Math.PI/gl.canvas.height; 60 | 61 | camYaw -= dX; 62 | camPitch += dY; 63 | if(camPitch > Math.PI/2 - 0.01) camPitch = Math.PI/2 - 0.01 64 | if(camPitch < -Math.PI/2 + 0.01) camPitch = -Math.PI/2 + 0.01 65 | 66 | old_x = e.pageX; 67 | old_y = e.pageY; 68 | e.preventDefault(); 69 | } 70 | if(zooming){ 71 | dY = (e.pageY-old_y)/gl.canvas.height; 72 | 73 | camDist += dY; 74 | if(camDist < 0.1) camDist = 0.1; 75 | 76 | old_x = e.pageX; 77 | e.preventDefault(); 78 | } 79 | }; 80 | 81 | function 82 | InitRW() 83 | { 84 | console.log("InitRW()"); 85 | let canvas = document.querySelector('#glcanvas'); 86 | canvas.width = window.innerWidth; 87 | canvas.height = window.innerHeight; 88 | 89 | // Get background color from stylesheet 90 | var bgColorStr = window.getComputedStyle(canvas, null).getPropertyValue("background-color"); 91 | bgColorStr = bgColorStr.substring(4, bgColorStr.length-1); 92 | backgroundColor = bgColorStr.replace(" ", "").split(","); 93 | backgroundColor = [parseFloat(backgroundColor[0])/255, parseFloat(backgroundColor[1])/255, parseFloat(backgroundColor[2])/255, 1.0]; 94 | 95 | gl = canvas.getContext('webgl'); 96 | 97 | if(!gl){ 98 | alert('Unable to initialize WebGL. Your browser or machine may not support it.'); 99 | return; 100 | } 101 | 102 | canvas.addEventListener("mousedown", mouseDown, false); 103 | canvas.addEventListener("mouseup", mouseUp, false); 104 | canvas.addEventListener("mouseout", mouseUp, false); 105 | canvas.addEventListener("mousemove", mouseMove, false); 106 | 107 | whitetex = loadTexture("textures/white.png"); 108 | 109 | defaultProgram = loadShaders(defaultVS, defaultFS); 110 | envMapProgram = loadShaders(envVS, envFS); 111 | carPS2Program = loadShaders(carPS2VS, carPS2FS); 112 | 113 | state.alphaRef = 0.1; 114 | state.projectionMatrix = mat4.create(); 115 | state.viewMatrix = mat4.create(); 116 | state.worldMatrix = mat4.create(); 117 | state.envMatrix = mat4.create(); 118 | state.matColor = vec4.create(); 119 | state.surfaceProps = vec4.create(); 120 | state.ambLight = vec3.fromValues(0.4, 0.4, 0.4); 121 | const alpha = 45; 122 | const beta = 45; 123 | state.lightDir = vec3.fromValues( 124 | -Math.cos(deg2rad(beta))*Math.cos(deg2rad(alpha)), 125 | -Math.sin(deg2rad(beta))*Math.cos(deg2rad(alpha)), 126 | -Math.sin(deg2rad(alpha)) 127 | ); 128 | state.lightCol = vec3.fromValues(1.0, 1.0, 1.0); 129 | 130 | 131 | AttachPlugins(); 132 | 133 | camera = RwCameraCreate(); 134 | camera.nearPlane = 0.1; 135 | camera.farPlane = 100.0; 136 | let frm = RwFrameCreate(); 137 | RwCameraSetFrame(camera, frm); 138 | 139 | const fov = deg2rad(70); 140 | const aspect = gl.canvas.clientWidth / gl.canvas.clientHeight; 141 | camera.viewWindow[1] = Math.tan(fov / 2); 142 | camera.viewWindow[0] = camera.viewWindow[1]*aspect; 143 | 144 | envFrame = RwFrameCreate(); 145 | mat4.rotateX(envFrame.matrix, envFrame.matrix, deg2rad(60)); 146 | rwFrameSynchLTM(envFrame); 147 | } 148 | 149 | function 150 | displayFrames(frame, parelem) 151 | { 152 | let li = document.createElement('li'); 153 | li.innerHTML = frame.name; 154 | for(let i = 0; i < frame.objects.length; i++){ 155 | let o = frame.objects[i]; 156 | if(o.type == rwID_ATOMIC){ 157 | let checkbox = document.createElement('input'); 158 | checkbox.type = "checkbox"; 159 | checkbox.onclick = function() { o.visible = checkbox.checked; }; 160 | checkbox.checked = o.visible; 161 | li.appendChild(checkbox); 162 | } 163 | } 164 | parelem.appendChild(li); 165 | if(frame.child){ 166 | let ul = document.createElement('ul'); 167 | parelem.appendChild(ul); 168 | for(let c = frame.child; c != null; c = c.next) 169 | displayFrames(c, ul); 170 | } 171 | } 172 | 173 | function 174 | loadCarIII(filename) 175 | { 176 | loadDFF(filename, function(clump){ 177 | myclump = clump; 178 | modelinfo = processVehicle(myclump); 179 | setupIIICar(myclump); 180 | setVehicleColors(modelinfo, carColors[0], carColors[1]); 181 | main(); 182 | }); 183 | } 184 | 185 | function 186 | loadCarVC(filename) 187 | { 188 | loadDFF(filename, function(clump){ 189 | myclump = clump; 190 | modelinfo = processVehicle(myclump); 191 | setVehicleColors(modelinfo, carColors[0], carColors[1]); 192 | main(); 193 | }); 194 | } 195 | 196 | function 197 | loadCarSA(filename) 198 | { 199 | loadDFF(filename, function(clump){ 200 | myclump = clump; 201 | modelinfo = processVehicle(myclump); 202 | setupSACar(myclump); 203 | setVehicleColors(modelinfo, 204 | carColors[0], carColors[1], carColors[2], carColors[3]); 205 | setVehicleLightColors(modelinfo, 206 | [ 255, 255, 255, 255 ], 207 | [ 255, 255, 255, 255 ], 208 | [ 255, 255, 255, 255 ], 209 | [ 255, 255, 255, 255 ]); 210 | main(); 211 | }); 212 | } 213 | 214 | function 215 | loadModel(filename) 216 | { 217 | loadDFF(filename, function(clump){ 218 | myclump = clump; 219 | main(); 220 | }); 221 | } 222 | 223 | function 224 | removeChildren(x) 225 | { 226 | while(x.firstChild) 227 | x.removeChild(x.firstChild); 228 | } 229 | 230 | function 231 | main() 232 | { 233 | let ul = document.getElementById('frames'); 234 | removeChildren(ul); 235 | displayFrames(myclump.frame, ul); 236 | 237 | if(!running){ 238 | running = true; 239 | 240 | let then = 0; 241 | function render(now){ 242 | now *= 0.001; // convert to seconds 243 | const deltaTime = now - then; 244 | then = now; 245 | 246 | drawScene(deltaTime); 247 | 248 | requestAnimationFrame(render); 249 | } 250 | requestAnimationFrame(render); 251 | } 252 | } 253 | 254 | function 255 | setupIIICar(clump) 256 | { 257 | for(let i = 0; i < clump.atomics.length; i++){ 258 | let a = clump.atomics[i]; 259 | a.pipeline = matFXPipe; 260 | for(let j = 0; j < a.geometry.materials.length; j++){ 261 | m = a.geometry.materials[j]; 262 | if(m.surfaceProperties[1] <= 0.0) 263 | continue; 264 | m.matfx = { 265 | type: 2, 266 | envCoefficient: m.surfaceProperties[1], 267 | envTex: RwTextureRead("reflection01", "") 268 | }; 269 | } 270 | } 271 | } 272 | 273 | function 274 | setupSACar(clump) 275 | { 276 | for(let i = 0; i < clump.atomics.length; i++){ 277 | let a = clump.atomics[i]; 278 | a.pipeline = carPipe; 279 | for(let j = 0; j < a.geometry.materials.length; j++){ 280 | m = a.geometry.materials[j]; 281 | m.fxFlags = 0; 282 | if(!m.matfx || m.matfx.type != 2) continue; 283 | 284 | if(m.matfx.envTex && m.envMap && m.envMap.shininess != 0){ 285 | m.envMap.texture = m.matfx.envTex; 286 | if(m.envMap.texture.name[0] == 'x') 287 | m.fxFlags |= 2; 288 | else 289 | m.fxFlags |= 1; 290 | } 291 | 292 | if(m.specMap && m.specMap.specularity != 0) 293 | m.fxFlags |= 4; 294 | } 295 | } 296 | } 297 | 298 | function 299 | setVehicleColors(vehinfo, c1, c2, c3, c4) 300 | { 301 | for(let i = 0; i < vehinfo.firstMaterials.length; i++) 302 | vehinfo.firstMaterials[i].color = c1; 303 | for(let i = 0; i < vehinfo.secondMaterials.length; i++) 304 | vehinfo.secondMaterials[i].color = c2; 305 | for(let i = 0; i < vehinfo.thirdMaterials.length; i++) 306 | vehinfo.thirdMaterials[i].color = c3; 307 | for(let i = 0; i < vehinfo.fourthMaterials.length; i++) 308 | vehinfo.fourthMaterials[i].color = c4; 309 | 310 | if(c1) document.getElementById("custom-color0").value = RGB2HTML(c1); 311 | if(c2) document.getElementById("custom-color1").value = RGB2HTML(c2); 312 | if(c3) document.getElementById("custom-color2").value = RGB2HTML(c3); 313 | if(c4) document.getElementById("custom-color3").value = RGB2HTML(c4); 314 | } 315 | 316 | function 317 | setVehicleLightColors(vehinfo, c1, c2, c3, c4) 318 | { 319 | for(let i = 0; i < vehinfo.firstLightMaterials.length; i++) 320 | vehinfo.firstLightMaterials[i].color = c1; 321 | for(let i = 0; i < vehinfo.secondLightMaterials.length; i++) 322 | vehinfo.secondLightMaterials[i].color = c2; 323 | for(let i = 0; i < vehinfo.thirdLightMaterials.length; i++) 324 | vehinfo.thirdLightMaterials[i].color = c3; 325 | for(let i = 0; i < vehinfo.fourthLightMaterials.length; i++) 326 | vehinfo.fourthLightMaterials[i].color = c4; 327 | } 328 | 329 | function 330 | findEditableMaterials(geo, vehinfo) 331 | { 332 | for(let i = 0; i < geo.materials.length; i++){ 333 | m = geo.materials[i]; 334 | if(m.color[0] == 0x3C && m.color[1] == 0xFF && m.color[2] == 0) 335 | vehinfo.firstMaterials.push(m); 336 | else if(m.color[0] == 0xFF && m.color[1] == 0 && m.color[2] == 0xAF) 337 | vehinfo.secondMaterials.push(m); 338 | else if(m.color[0] == 0 && m.color[1] == 0xFF && m.color[2] == 0xFF) 339 | vehinfo.thirdMaterials.push(m); 340 | else if(m.color[0] == 0xFF && m.color[1] == 0x00 && m.color[2] == 0xFF) 341 | vehinfo.fourthMaterials.push(m); 342 | else if(m.color[0] == 0xFF && m.color[1] == 0xAF && m.color[2] == 0) 343 | vehinfo.firstLightMaterials.push(m); 344 | else if(m.color[0] == 0 && m.color[1] == 0xFF && m.color[2] == 0xC8) 345 | vehinfo.secondLightMaterials.push(m); 346 | else if(m.color[0] == 0xB9 && m.color[1] == 0xFF && m.color[2] == 0) 347 | vehinfo.thirdLightMaterials.push(m); 348 | else if(m.color[0] == 0xFF && m.color[1] == 0x3C && m.color[2] == 0) 349 | vehinfo.fourthLightMaterials.push(m); 350 | } 351 | } 352 | 353 | function 354 | processVehicle(clump) 355 | { 356 | let vehicleInfo = { 357 | firstMaterials: [], 358 | secondMaterials: [], 359 | thirdMaterials: [], 360 | fourthMaterials: [], 361 | firstLightMaterials: [], // front left 362 | secondLightMaterials: [], // front right 363 | thirdLightMaterials: [], // back left 364 | fourthLightMaterials: [], // back right 365 | clump: clump 366 | }; 367 | 368 | // Wheel atomic to clone 369 | let wheel = null; 370 | 371 | for(let i = 0; i < clump.atomics.length; i++){ 372 | a = clump.atomics[i]; 373 | f = a.frame; 374 | if(f.name.endsWith("_dam") || 375 | f.name.endsWith("_lo") || 376 | f.name.endsWith("_vlo")) 377 | a.visible = false; 378 | 379 | if(!wheel && f.name.startsWith("wheel")) { 380 | wheel = a; 381 | } 382 | 383 | findEditableMaterials(a.geometry, vehicleInfo); 384 | } 385 | 386 | // Clone wheels 387 | let frame = clump.frame.child; 388 | while(wheel && frame) { 389 | if(["wheel_rb_dummy", "wheel_rm_dummy", "wheel_lf_dummy", "wheel_lb_dummy", "wheel_lm_dummy"].includes(frame.name)) { 390 | let wheel2 = RpAtomicClone(wheel); 391 | mat4.copy(wheel2.frame.ltm, frame.ltm); 392 | if(["wheel_lf_dummy", "wheel_lb_dummy", "wheel_lm_dummy"].includes(frame.name)) { 393 | // Rotate cloned wheel 394 | mat4.rotate(wheel2.frame.ltm, wheel2.frame.ltm, Math.PI, [0, 0, 1]); 395 | } 396 | frame.child = wheel2.frame; 397 | clump.atomics.push(wheel2); 398 | } 399 | frame = frame.next; 400 | } 401 | 402 | return vehicleInfo; 403 | } 404 | 405 | function 406 | drawScene(deltaTime) 407 | { 408 | if(window.autoRotateCamera) { 409 | camYaw += deltaTime * 0.9; 410 | } 411 | 412 | gl.clearColor(backgroundColor[0], backgroundColor[1], backgroundColor[2], 1.0); 413 | gl.clearDepth(1.0); 414 | gl.enable(gl.DEPTH_TEST); 415 | gl.depthFunc(gl.LEQUAL); 416 | 417 | gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); 418 | 419 | let x = camDist * Math.cos(camYaw)* Math.cos(camPitch); 420 | let y = camDist * Math.sin(camYaw)* Math.cos(camPitch); 421 | let z = camDist * Math.sin(camPitch); 422 | RwFrameLookAt(camera.frame, 423 | [ x, y, z ], 424 | [ 0.0, 0.0, 0.0 ], 425 | [ 0.0, 0.0, 1.0 ]); 426 | rwFrameSynchLTM(camera.frame); 427 | 428 | RwCameraBeginUpdate(camera); 429 | 430 | RenderPass = 0; 431 | RpClumpRender(myclump); 432 | RenderPass = 1; 433 | RpClumpRender(myclump); 434 | RenderPass = -1; 435 | } 436 | 437 | 438 | function 439 | loadDFF(filename, cb) 440 | { 441 | let req = new XMLHttpRequest(); 442 | req.open("GET", ModelsDirPath + "/" + filename, true); 443 | req.responseType = "arraybuffer"; 444 | 445 | req.onload = function(oEvent){ 446 | let arrayBuffer = req.response; 447 | if(arrayBuffer){ 448 | stream = RwStreamCreate(arrayBuffer); 449 | 450 | if(RwStreamFindChunk(stream, rwID_CLUMP)){ 451 | let c = RpClumpStreamRead(stream); 452 | if(c != null) 453 | cb(c); 454 | } 455 | return null; 456 | } 457 | }; 458 | 459 | req.send(null); 460 | } 461 | 462 | function 463 | rgbToCSSString(r, g, b) 464 | { 465 | return ["rgb(",r,",",g,",",b,")"].join(""); 466 | } 467 | 468 | function 469 | RGB2HTML(color) 470 | { 471 | let decColor = 0x1000000 + color[2] + 0x100 * color[1] + 0x10000 * color[0]; 472 | return '#' + decColor.toString(16).substr(1); 473 | } 474 | -------------------------------------------------------------------------------- /rw.js: -------------------------------------------------------------------------------- 1 | // core 2 | var rwID_STRUCT = 0x01; 3 | var rwID_STRING = 0x02; 4 | var rwID_EXTENSION = 0x03; 5 | var rwID_CAMERA = 0x05; 6 | var rwID_TEXTURE = 0x06; 7 | var rwID_MATERIAL = 0x07; 8 | var rwID_MATLIST = 0x08; 9 | var rwID_FRAMELIST = 0x0E; 10 | var rwID_GEOMETRY = 0x0F; 11 | var rwID_CLUMP = 0x10; 12 | var rwID_LIGHT = 0x12; 13 | var rwID_ATOMIC = 0x14; 14 | var rwID_GEOMETRYLIST = 0x1A; 15 | // tk 16 | var rwID_HANIMPLUGIN = 0x11E; 17 | var rwID_MATERIALEFFECTSPLUGIN = 0x120; 18 | // world 19 | var rwID_BINMESHPLUGIN = 0x50E; 20 | // R* 21 | var rwID_NODENAME = 0x0253F2FE; 22 | var rwID_ENVMAT = 0x0253F2FC; 23 | var rwID_SPECMAT = 0x0253F2F6; 24 | 25 | 26 | var frameTKList = {}; 27 | var textureTKList = {}; 28 | var materialTKList = {}; 29 | var geometryTKList = {}; 30 | var atomicTKList = {}; 31 | var clumpTKList = {}; 32 | 33 | /* RwFrame */ 34 | 35 | function 36 | rwSetHierarchyRoot(frame, root) 37 | { 38 | frame.root = root; 39 | for(frame = frame.child; frame != null; frame = frame.next) 40 | rwSetHierarchyRoot(frame, root); 41 | } 42 | 43 | function 44 | RwFrameRemoveChild(c) 45 | { 46 | let f = c.parent.child; 47 | // remove as child 48 | if(f == c) 49 | c.parent.child = c.next; 50 | else{ 51 | while(f.next != c) 52 | f = f.next; 53 | f.next = c.next; 54 | } 55 | // now make this the root of a new hierarchy 56 | c.parent = null; 57 | c.next = null; 58 | rwSetHierarchyRoot(c, c); 59 | } 60 | 61 | function 62 | RwFrameAddChild(p, child) 63 | { 64 | if(child.parent != null) 65 | RwFrameRemoveChild(child); 66 | // append as child of p 67 | if(p.child == null) 68 | p.child = child; 69 | else{ 70 | let c; 71 | for(c = p.child; c.next != null; c = c.next); 72 | c.next = child; 73 | } 74 | child.next = null; 75 | 76 | child.parent = p; 77 | rwSetHierarchyRoot(child, p.root); 78 | } 79 | 80 | function 81 | rwFrameSynchLTM(f) 82 | { 83 | if(f.parent == null) 84 | mat4.copy(f.ltm, f.matrix); 85 | else 86 | mat4.multiply(f.ltm, f.matrix, f.parent.ltm); 87 | for(let c = f.child; c != null; c = c.next) 88 | rwFrameSynchLTM(c); 89 | } 90 | 91 | function 92 | RwFrameLookAt(frm, pos, target, up) 93 | { 94 | let at = vec3.create(); 95 | vec3.subtract(at, target, pos); 96 | vec3.normalize(at, at); 97 | let left = vec3.create(); 98 | vec3.cross(left, up, at); 99 | vec3.normalize(left, left); 100 | vec3.cross(up, at, left); 101 | m = frm.matrix; 102 | m[0] = left[0]; 103 | m[1] = left[1]; 104 | m[2] = left[2]; 105 | m[4] = up[0]; 106 | m[5] = up[1]; 107 | m[6] = up[2]; 108 | m[8] = at[0]; 109 | m[9] = at[1]; 110 | m[10] = at[2]; 111 | m[12] = pos[0]; 112 | m[13] = pos[1]; 113 | m[14] = pos[2]; 114 | } 115 | 116 | function 117 | RwFrameCreate() 118 | { 119 | let f = { 120 | parent: null, 121 | root: null, 122 | child: null, 123 | next: null, 124 | matrix: mat4.create(), 125 | ltm: mat4.create(), 126 | objects: [], 127 | name: "" 128 | }; 129 | f.root = f; 130 | mat4.copy(f.ltm, f.matrix); 131 | return f; 132 | } 133 | 134 | /* RwCamera */ 135 | 136 | function 137 | RwCameraCreate() 138 | { 139 | cam = { 140 | type: rwID_CAMERA, 141 | frame: null, 142 | viewWindow: [ 1.0, 1.0 ], 143 | viewOffset: [ 0.0, 0.0 ], 144 | nearPlane: [ 0.5 ], 145 | farPlane: [ 10.0 ], 146 | fogPlane: [ 5.0 ], 147 | projmat: mat4.create() 148 | }; 149 | return cam; 150 | } 151 | 152 | function 153 | RwCameraSetFrame(c, f) 154 | { 155 | c.frame = f; 156 | f.objects.push(c); 157 | } 158 | 159 | function 160 | RwCameraBeginUpdate(cam) 161 | { 162 | mat4.invert(state.viewMatrix, camera.frame.ltm); 163 | state.viewMatrix[0] = -state.viewMatrix[0]; 164 | state.viewMatrix[4] = -state.viewMatrix[4]; 165 | state.viewMatrix[8] = -state.viewMatrix[8]; 166 | state.viewMatrix[12] = -state.viewMatrix[12]; 167 | 168 | p = cam.projmat; 169 | let xscl = 1.0/cam.viewWindow[0]; 170 | let yscl = 1.0/cam.viewWindow[1]; 171 | let zscl = 1.0/(cam.farPlane-cam.nearPlane); 172 | 173 | p[0] = xscl; 174 | p[1] = 0; 175 | p[2] = 0; 176 | p[3] = 0; 177 | 178 | p[4] = 0; 179 | p[5] = yscl; 180 | p[6] = 0; 181 | p[7] = 0; 182 | 183 | p[8] = cam.viewOffset[0]*xscl; 184 | p[9] = cam.viewOffset[1]*yscl; 185 | p[12] = -p[8]; 186 | p[13] = -p[9]; 187 | 188 | p[10] = (cam.farPlane+cam.nearPlane)*zscl; 189 | p[11] = 1.0; 190 | p[14] = -2.0*cam.nearPlane*cam.farPlane*zscl; 191 | p[15] = 0.0; 192 | 193 | mat4.copy(state.projectionMatrix, p); 194 | } 195 | 196 | /* RwTexture */ 197 | 198 | function 199 | RwTextureRead(name, mask) 200 | { 201 | return { 202 | name: name, 203 | mask: mask, 204 | tex: loadTexture(TexturesDirPath + "/" + name + ".png") 205 | }; 206 | } 207 | 208 | /* RpMaterial */ 209 | 210 | function 211 | RpMaterialCreate() 212 | { 213 | let mat = { 214 | color: [ 255, 255, 255, 255 ], 215 | surfaceProperties: [ 1.0, 1.0, 1.0 ] 216 | }; 217 | return mat; 218 | } 219 | 220 | /* RpGeometry */ 221 | 222 | function 223 | RpGeometryCreate(flags, numMorphTargets) 224 | { 225 | let geo = { 226 | numVertices: 0, // used for instancing 227 | triangles: [], 228 | morphTargets: [], 229 | materials: [], 230 | meshtype: 0, 231 | meshes: [], 232 | totalMeshIndices: 0 // used for instancing 233 | }; 234 | 235 | let numTexCoords = (flags >> 16) & 0xFF; 236 | if(numTexCoords == 0){ 237 | if(flags & 0x04) numTexCoords = 1; 238 | if(flags & 0x80) numTexCoords = 2; 239 | } 240 | geo.texCoords = []; 241 | if(numTexCoords > 0) 242 | while(numTexCoords--) 243 | geo.texCoords.push([]); 244 | 245 | if(flags & 0x08) 246 | geo.prelit = []; 247 | 248 | while(numMorphTargets--){ 249 | let mt = { vertices: [] }; 250 | if(flags & 0x10) 251 | mt.normals = []; 252 | geo.morphTargets.push(mt); 253 | } 254 | 255 | return geo; 256 | } 257 | 258 | /* RpAtomic */ 259 | 260 | function 261 | RpAtomicCreate() 262 | { 263 | return { 264 | type: rwID_ATOMIC, 265 | frame: null, 266 | geometry: null, 267 | visible: true, 268 | pipeline: defaultPipe 269 | }; 270 | } 271 | 272 | function 273 | RpAtomicClone(a) 274 | { 275 | let a2 = RpAtomicCreate(); 276 | a2.type = a.type; 277 | a2.visible = a.visible; 278 | a2.frame = RwFrameClone(a.frame); 279 | a2.geometry = a.geometry; 280 | a2.pipeline = a.pipeline; 281 | a2.frame.objects = []; 282 | a2.frame.objects[0] = a2; 283 | return a2; 284 | } 285 | 286 | function 287 | RpAtomicSetFrame(a, f) 288 | { 289 | a.frame = f; 290 | f.objects.push(a); 291 | } 292 | 293 | function 294 | RpAtomicRender(atomic) 295 | { 296 | if(atomic.geometry.instData === undefined) 297 | instanceGeo(atomic.geometry); 298 | atomic.pipeline.renderCB(atomic); 299 | } 300 | 301 | /* RpClump */ 302 | 303 | function 304 | RpClumpCreate() 305 | { 306 | return { 307 | type: rwID_CLUMP, 308 | frame: null, 309 | atomics: [] 310 | }; 311 | } 312 | 313 | function RpClumpSetFrame(c, f) { c.frame = f; } 314 | 315 | function 316 | RpClumpRender(clump) 317 | { 318 | for(let i = 0; i < clump.atomics.length; i++) 319 | RpAtomicRender(clump.atomics[i]); 320 | } 321 | 322 | 323 | /* Instancing */ 324 | 325 | function 326 | instanceGeo(geo) 327 | { 328 | let header = { 329 | prim: gl.TRIANGLES, 330 | totalNumVertices: geo.numVertices, 331 | totalNumIndices: geo.totalMeshIndices, 332 | vbo: gl.createBuffer(), 333 | ibo: gl.createBuffer(), 334 | inst: [], 335 | attribs: [] 336 | }; 337 | if(geo.meshtype == 1) 338 | header.prim = gl.TRIANGLE_STRIP; 339 | 340 | // instance indices 341 | let buffer = new ArrayBuffer(header.totalNumIndices*2); 342 | offset = 0; 343 | for(let i = 0; i < geo.meshes.length; i++){ 344 | m = geo.meshes[i]; 345 | inst = { 346 | material: m.material, 347 | numIndices: m.indices.length, 348 | offset: offset 349 | }; 350 | let indices = new Uint16Array(buffer, inst.offset, inst.numIndices); 351 | indices.set(m.indices); 352 | offset += inst.numIndices*2; 353 | header.inst.push(inst); 354 | } 355 | gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, header.ibo); 356 | gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, buffer, gl.STATIC_DRAW); 357 | geo.instData = header; 358 | 359 | instanceVertices(geo); 360 | } 361 | 362 | function 363 | instanceVertices(geo) 364 | { 365 | let i; 366 | let stride = 0; 367 | let attribs = []; 368 | 369 | attribs.push({ 370 | index: ATTRIB_POS, 371 | size: 3, 372 | type: gl.FLOAT, 373 | normalized: false, 374 | offset: stride 375 | }); 376 | stride += 12; 377 | 378 | if(geo.morphTargets[0].normals){ 379 | attribs.push({ 380 | index: ATTRIB_NORMAL, 381 | size: 3, 382 | type: gl.FLOAT, 383 | normalized: false, 384 | offset: stride 385 | }); 386 | stride += 12; 387 | } 388 | 389 | if(geo.prelit){ 390 | attribs.push({ 391 | index: ATTRIB_COLOR, 392 | size: 4, 393 | type: gl.UNSIGNED_BYTE, 394 | normalized: true, 395 | offset: stride 396 | }); 397 | stride += 4; 398 | } 399 | 400 | for(i = 0; i < geo.texCoords.length; i++){ 401 | attribs.push({ 402 | index: ATTRIB_TEXCOORDS0 + i, 403 | size: 2, 404 | type: gl.FLOAT, 405 | normalized: false, 406 | offset: stride 407 | }); 408 | stride += 8; 409 | } 410 | 411 | for(i = 0; i < attribs.length; i++) 412 | attribs[i].stride = stride; 413 | 414 | header = geo.instData; 415 | header.attribs = attribs; 416 | let buffer = new ArrayBuffer(header.totalNumVertices*stride); 417 | 418 | // instance verts 419 | for(i = 0; attribs[i].index != ATTRIB_POS; i++); 420 | let a = attribs[i]; 421 | instV3d(buffer, a.offset, a.stride, geo.morphTargets[0].vertices, header.totalNumVertices); 422 | 423 | if(geo.morphTargets[0].normals){ 424 | for(i = 0; attribs[i].index != ATTRIB_NORMAL; i++); 425 | let a = attribs[i]; 426 | instV3d(buffer, a.offset, a.stride, geo.morphTargets[0].normals, header.totalNumVertices); 427 | } 428 | 429 | if(geo.prelit){ 430 | for(i = 0; attribs[i].index != ATTRIB_COLOR; i++); 431 | let a = attribs[i]; 432 | instRGBA(buffer, a.offset, a.stride, geo.prelit, header.totalNumVertices); 433 | } 434 | 435 | for(i = 0; i < geo.texCoords.length; i++){ 436 | for(j = 0; attribs[j].index != ATTRIB_TEXCOORDS0 + i; j++); 437 | let a = attribs[j]; 438 | instV2d(buffer, a.offset, a.stride, geo.texCoords[i], header.totalNumVertices); 439 | } 440 | 441 | gl.bindBuffer(gl.ARRAY_BUFFER, header.vbo); 442 | gl.bufferData(gl.ARRAY_BUFFER, buffer, gl.STATIC_DRAW); 443 | } 444 | 445 | function 446 | instV3d(buffer, offset, stride, src, n) 447 | { 448 | let view = new DataView(buffer); 449 | let o = offset; 450 | for(let i = 0; i < n; i++){ 451 | view.setFloat32(o+0, src[i][0], true); 452 | view.setFloat32(o+4, src[i][1], true); 453 | view.setFloat32(o+8, src[i][2], true); 454 | o += stride; 455 | } 456 | } 457 | 458 | function 459 | instV2d(buffer, offset, stride, src, n) 460 | { 461 | let view = new DataView(buffer); 462 | let o = offset; 463 | for(let i = 0; i < n; i++){ 464 | view.setFloat32(o+0, src[i][0], true); 465 | view.setFloat32(o+4, src[i][1], true); 466 | o += stride; 467 | } 468 | } 469 | 470 | function 471 | instRGBA(buffer, offset, stride, src, n) 472 | { 473 | let view = new DataView(buffer); 474 | let o = offset; 475 | for(let i = 0; i < n; i++){ 476 | view.setUint8(o+0, src[i][0]); 477 | view.setUint8(o+1, src[i][1]); 478 | view.setUint8(o+2, src[i][2]); 479 | view.setUint8(o+3, src[i][3]); 480 | o += stride; 481 | } 482 | } 483 | 484 | /* The Init functions are for when we 485 | * read a json structure produced by convdff */ 486 | 487 | function 488 | rwFrameInit(f) 489 | { 490 | f.parent = null; 491 | f.root = f; 492 | f.child = null; 493 | f.next = null; 494 | f.objects = []; 495 | m = f.matrix; 496 | f.matrix = mat4.fromValues( 497 | m[0], m[1], m[2], 0, 498 | m[3], m[4], m[5], 0, 499 | m[6], m[7], m[8], 0, 500 | m[9], m[10], m[11], 1); 501 | f.ltm = mat4.create(); 502 | mat4.copy(f.ltm, f.matrix); 503 | } 504 | 505 | function 506 | RwFrameClone(f) { 507 | let f2 = RwFrameCreate(); 508 | mat4.copy(f2.matrix, f.matrix); 509 | mat4.copy(f2.ltm, f.ltm); 510 | f2.child = f.child; 511 | f2.name = f.name; 512 | f2.parent = f.parent; 513 | f2.root = f.root; 514 | return f2; 515 | } 516 | 517 | function 518 | rpMaterialInit(m) 519 | { 520 | if(m.texture) 521 | m.texture = RwTextureRead(m.texture.name, m.texture.mask); 522 | if(m.matfx && m.matfx.envTex) 523 | m.matfx.envTex = RwTextureRead(m.matfx.envTex.name, m.matfx.envTex.mask); 524 | if(m.specMat) 525 | m.specMat.texture = RwTextureRead(m.specMat.texture, ""); 526 | } 527 | 528 | function 529 | rpGeometryInit(g) 530 | { 531 | for(let i = 0; i < g.materials.length; i++){ 532 | m = g.materials[i]; 533 | rpMaterialInit(m); 534 | } 535 | for(let i = 0; i < g.meshes.length; i++){ 536 | g.meshes[i].material = g.materials[g.meshes[i].matId]; 537 | delete g.meshes[i].matId; 538 | } 539 | g.numVertices = g.morphTargets[0].vertices.length; 540 | g.totalMeshIndices = 0; 541 | for(let i = 0; i < g.meshes.length; i++) 542 | g.totalMeshIndices += g.meshes[i].indices.length; 543 | } 544 | 545 | function 546 | rpAtomicInit(atomic) 547 | { 548 | atomic.type = rwID_ATOMIC; 549 | atomic.frame = null; 550 | atomic.visible = true; 551 | atomic.pipeline = defaultPipe; 552 | atomic.objects = []; 553 | if(atomic.matfx) 554 | atomic.pipeline = matFXPipe; 555 | } 556 | 557 | function 558 | rpClumpInit(clump) 559 | { 560 | for(let i = 0; i < clump.atomics.length; i++){ 561 | let a = clump.atomics[i]; 562 | let f = clump.frames[a.frame]; 563 | rpAtomicInit(a); 564 | RpAtomicSetFrame(a, f); 565 | 566 | rpGeometryInit(a.geometry); 567 | instanceGeo(a.geometry) 568 | } 569 | clump.frame = null; 570 | for(let i = 0; i < clump.frames.length; i++){ 571 | let f = clump.frames[i]; 572 | p = f.parent; 573 | rwFrameInit(f); 574 | if(p >= 0) 575 | RwFrameAddChild(clump.frames[p], f); 576 | else 577 | RpClumpSetFrame(clump, f); 578 | } 579 | delete clump.frames; 580 | 581 | rwFrameSynchLTM(clump.frame); 582 | } 583 | -------------------------------------------------------------------------------- /rwrender.js: -------------------------------------------------------------------------------- 1 | var RenderPass = -1; 2 | 3 | var defaultPipe = { 4 | renderCB: defaultRenderCB 5 | }; 6 | var matFXPipe = { 7 | renderCB: matfxRenderCB 8 | }; 9 | var carPipe = { 10 | renderCB: carRenderCB 11 | }; 12 | 13 | function 14 | RenderThisPass(mat) 15 | { 16 | switch(RenderPass){ 17 | case 0: // opaque 18 | return mat.color[3] == 255; 19 | case 1: // transparent 20 | return mat.color[3] != 255; 21 | } 22 | return true; 23 | } 24 | 25 | 26 | function 27 | uploadState(proginfo) 28 | { 29 | gl.uniformMatrix4fv(proginfo.u.u_proj, false, state.projectionMatrix); 30 | gl.uniformMatrix4fv(proginfo.u.u_view, false, state.viewMatrix); 31 | gl.uniformMatrix4fv(proginfo.u.u_world, false, state.worldMatrix); 32 | if(proginfo.u.u_env) 33 | gl.uniformMatrix4fv(proginfo.u.u_env, false, state.envMatrix); 34 | 35 | gl.uniform3fv(proginfo.u.u_ambLight, state.ambLight); 36 | gl.uniform3fv(proginfo.u.u_lightDir, state.lightDir); 37 | gl.uniform3fv(proginfo.u.u_lightCol, state.lightCol); 38 | 39 | gl.uniform1i(proginfo.u.u_tex0, 0); 40 | gl.uniform1i(proginfo.u.u_tex1, 1); 41 | gl.uniform1i(proginfo.u.u_tex2, 2); 42 | 43 | gl.uniform1f(proginfo.u.u_alphaRef, state.alphaRef); 44 | } 45 | 46 | function 47 | setAttributes(attribs, proginfo) 48 | { 49 | for(let i = 0; i < attribs.length; i++){ 50 | a = attribs[i]; 51 | if(proginfo.a[a.index] < 0) 52 | continue; 53 | gl.vertexAttribPointer(proginfo.a[a.index], 54 | a.size, a.type, 55 | a.normalized, 56 | a.stride, a.offset); 57 | gl.enableVertexAttribArray(proginfo.a[a.index]); 58 | } 59 | } 60 | 61 | function 62 | resetAttributes(attribs, proginfo) 63 | { 64 | for(let i = 0; i < attribs.length; i++){ 65 | a = attribs[i]; 66 | if(proginfo.a[a.index] >= 0) 67 | gl.disableVertexAttribArray(proginfo.a[a.index]); 68 | } 69 | } 70 | 71 | function 72 | defaultRenderCB(atomic) 73 | { 74 | if(!atomic.visible) 75 | return; 76 | 77 | mat4.copy(state.worldMatrix, atomic.frame.ltm); 78 | 79 | let header = atomic.geometry.instData; 80 | gl.bindBuffer(gl.ARRAY_BUFFER, header.vbo); 81 | gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, header.ibo); 82 | 83 | let prg = defaultProgram; 84 | gl.useProgram(prg.program); 85 | 86 | setAttributes(header.attribs, prg); 87 | uploadState(prg); 88 | 89 | for(let i = 0; i < header.inst.length; i++){ 90 | inst = header.inst[i]; 91 | m = inst.material; 92 | 93 | if(!RenderThisPass(m)) 94 | continue; 95 | 96 | gl.activeTexture(gl.TEXTURE0); 97 | if(m.texture) 98 | gl.bindTexture(gl.TEXTURE_2D, m.texture.tex); 99 | else 100 | gl.bindTexture(gl.TEXTURE_2D, whitetex); 101 | 102 | vec4.scale(state.matColor, m.color, 1.0/255.0); 103 | state.surfaceProps[0] = m.surfaceProperties[0]; 104 | state.surfaceProps[1] = m.surfaceProperties[1]; 105 | state.surfaceProps[2] = m.surfaceProperties[2]; 106 | 107 | gl.uniform4fv(prg.u.u_matColor, state.matColor); 108 | gl.uniform4fv(prg.u.u_surfaceProps, state.surfaceProps); 109 | 110 | if(m.color[3] != 255 || m.texture){ 111 | gl.enable(gl.BLEND); 112 | gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA); 113 | }else 114 | gl.disable(gl.BLEND); 115 | 116 | gl.drawElements(header.prim, inst.numIndices, gl.UNSIGNED_SHORT, inst.offset); 117 | } 118 | 119 | resetAttributes(header.attribs, programInfo); 120 | } 121 | 122 | var envMatScale = mat4.fromValues( 123 | -0.5, 0.0, 0.0, 0.0, 124 | 0.0, -0.5, 0.0, 0.0, 125 | 0.0, 0.0, 1.0, 0.0, 126 | 0.5, 0.5, 0.0, 1.0 127 | ); 128 | 129 | function 130 | matfxRenderCB(atomic) 131 | { 132 | if(!atomic.visible) 133 | return; 134 | 135 | mat4.copy(state.worldMatrix, atomic.frame.ltm); 136 | 137 | let tmp = mat4.create(); 138 | mat4.invert(tmp, envFrame.ltm); 139 | mat4.multiply(tmp, tmp, state.viewMatrix); 140 | tmp[12] = tmp[13] = tmp[14] = 0.0; 141 | 142 | mat4.multiply(state.envMatrix, envMatScale, tmp); 143 | 144 | let header = atomic.geometry.instData; 145 | gl.bindBuffer(gl.ARRAY_BUFFER, header.vbo); 146 | gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, header.ibo); 147 | 148 | let prg = envMapProgram; 149 | gl.useProgram(prg.program); 150 | 151 | setAttributes(header.attribs, prg); 152 | uploadState(prg); 153 | 154 | for(let i = 0; i < header.inst.length; i++){ 155 | inst = header.inst[i]; 156 | m = inst.material; 157 | 158 | if(!RenderThisPass(m)) 159 | continue; 160 | 161 | gl.activeTexture(gl.TEXTURE0); 162 | if(m.texture) 163 | gl.bindTexture(gl.TEXTURE_2D, m.texture.tex); 164 | else 165 | gl.bindTexture(gl.TEXTURE_2D, whitetex); 166 | 167 | gl.activeTexture(gl.TEXTURE1); 168 | envcoef = 0.0; 169 | if(m.matfx && m.matfx.envTex){ 170 | envcoef = m.matfx.envCoefficient; 171 | gl.bindTexture(gl.TEXTURE_2D, m.matfx.envTex.tex); 172 | }else 173 | gl.bindTexture(gl.TEXTURE_2D, null); 174 | 175 | vec4.scale(state.matColor, m.color, 1.0/255.0); 176 | state.surfaceProps[0] = m.surfaceProperties[0]; 177 | state.surfaceProps[1] = envcoef; 178 | state.surfaceProps[2] = m.surfaceProperties[2]; 179 | 180 | gl.uniform4fv(prg.u.u_matColor, state.matColor); 181 | gl.uniform4fv(prg.u.u_surfaceProps, state.surfaceProps); 182 | 183 | if(m.color[3] != 255 || m.texture){ 184 | gl.enable(gl.BLEND); 185 | gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA); 186 | }else 187 | gl.disable(gl.BLEND); 188 | 189 | gl.drawElements(header.prim, inst.numIndices, gl.UNSIGNED_SHORT, inst.offset); 190 | } 191 | 192 | resetAttributes(header.attribs, programInfo); 193 | } 194 | 195 | function 196 | carRenderCB(atomic) 197 | { 198 | if(!atomic.visible) 199 | return; 200 | 201 | mat4.copy(state.worldMatrix, atomic.frame.ltm); 202 | 203 | let header = atomic.geometry.instData; 204 | gl.bindBuffer(gl.ARRAY_BUFFER, header.vbo); 205 | gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, header.ibo); 206 | 207 | let prg = carPS2Program; 208 | gl.useProgram(prg.program); 209 | 210 | setAttributes(header.attribs, prg); 211 | uploadState(prg); 212 | 213 | for(let i = 0; i < header.inst.length; i++){ 214 | inst = header.inst[i]; 215 | m = inst.material; 216 | 217 | if(!RenderThisPass(m)) 218 | continue; 219 | 220 | gl.activeTexture(gl.TEXTURE0); 221 | if(m.texture) 222 | gl.bindTexture(gl.TEXTURE_2D, m.texture.tex); 223 | else 224 | gl.bindTexture(gl.TEXTURE_2D, whitetex); 225 | 226 | let shininess = 0.0; 227 | let specularity = 0.0; 228 | 229 | if(m.fxFlags & 3){ 230 | shininess = m.envMap.shininess; 231 | gl.activeTexture(gl.TEXTURE1); 232 | gl.bindTexture(gl.TEXTURE_2D, m.envMap.texture.tex); 233 | } 234 | 235 | if(m.fxFlags & 4){ 236 | specularity = m.specMap.specularity; 237 | gl.activeTexture(gl.TEXTURE2); 238 | gl.bindTexture(gl.TEXTURE_2D, m.specMap.texture.tex); 239 | } 240 | 241 | vec4.scale(state.matColor, m.color, 1.0/255.0); 242 | state.surfaceProps[0] = m.surfaceProperties[0]; 243 | state.surfaceProps[1] = specularity; 244 | state.surfaceProps[2] = m.surfaceProperties[2]; 245 | state.surfaceProps[3] = shininess; 246 | 247 | gl.uniform4fv(prg.u.u_matColor, state.matColor); 248 | gl.uniform4fv(prg.u.u_surfaceProps, state.surfaceProps); 249 | 250 | if(m.color[3] != 255 || m.texture){ 251 | gl.enable(gl.BLEND); 252 | gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA); 253 | }else 254 | gl.disable(gl.BLEND); 255 | 256 | gl.drawElements(header.prim, inst.numIndices, gl.UNSIGNED_SHORT, inst.offset); 257 | } 258 | 259 | resetAttributes(header.attribs, programInfo); 260 | } 261 | -------------------------------------------------------------------------------- /rwstream.js: -------------------------------------------------------------------------------- 1 | function 2 | AttachPlugins() 3 | { 4 | frameTKList[rwID_NODENAME] = { streamRead: NodeNameStreamRead }; 5 | geometryTKList[rwID_BINMESHPLUGIN] = { streamRead: rpMeshRead }; 6 | materialTKList[rwID_MATERIALEFFECTSPLUGIN] = { streamRead: rpMatfxMaterialStreamRead }; 7 | materialTKList[rwID_ENVMAT] = { streamRead: envMatStreamRead }; 8 | materialTKList[rwID_SPECMAT] = { streamRead: specMatStreamRead }; 9 | atomicTKList[rwID_MATERIALEFFECTSPLUGIN] = { streamRead: rpMatfxAtomicStreamRead }; 10 | } 11 | 12 | function 13 | RwStreamCreate(buffer) 14 | { 15 | return { 16 | buffer: buffer, 17 | view: new DataView(buffer), 18 | offset: 0, 19 | eof: false 20 | }; 21 | } 22 | 23 | function 24 | RwStreamSkip(stream, len) 25 | { 26 | stream.offset += len; 27 | } 28 | 29 | function 30 | RwStreamReadUInt8(stream) 31 | { 32 | if(stream.offset >= stream.buffer.byteLength){ 33 | stream.eof = true; 34 | return null; 35 | } 36 | let v = stream.view.getUint8(stream.offset, true); 37 | stream.offset += 1; 38 | return v; 39 | } 40 | 41 | function 42 | RwStreamReadUInt16(stream) 43 | { 44 | if(stream.offset >= stream.buffer.byteLength){ 45 | stream.eof = true; 46 | return null; 47 | } 48 | let v = stream.view.getUint16(stream.offset, true); 49 | stream.offset += 2; 50 | return v; 51 | } 52 | 53 | function 54 | RwStreamReadUInt32(stream) 55 | { 56 | if(stream.offset >= stream.buffer.byteLength){ 57 | stream.eof = true; 58 | return null; 59 | } 60 | let v = stream.view.getUint32(stream.offset, true); 61 | stream.offset += 4; 62 | return v; 63 | } 64 | 65 | function 66 | RwStreamReadInt32(stream) 67 | { 68 | if(stream.offset >= stream.buffer.byteLength){ 69 | stream.eof = true; 70 | return null; 71 | } 72 | let v = stream.view.getInt32(stream.offset, true); 73 | stream.offset += 4; 74 | return v; 75 | } 76 | 77 | function 78 | RwStreamReadReal(stream) 79 | { 80 | if(stream.offset >= stream.buffer.byteLength){ 81 | stream.eof = true; 82 | return null; 83 | } 84 | let v = stream.view.getFloat32(stream.offset, true); 85 | stream.offset += 4; 86 | return v; 87 | } 88 | 89 | function 90 | RwStreamReadString(stream, length) 91 | { 92 | if(stream.offset >= stream.buffer.byteLength){ 93 | stream.eof = true; 94 | return null; 95 | } 96 | let a = new Uint8Array(stream.buffer, stream.offset, length); 97 | let s = ""; 98 | for(let i = 0; i < length && a[i] != 0; i++) 99 | s += String.fromCharCode(a[i]); 100 | stream.offset += length; 101 | return s; 102 | } 103 | 104 | function 105 | rwStreamReadChunkHeader(stream) 106 | { 107 | let t = RwStreamReadUInt32(stream); 108 | let l = RwStreamReadUInt32(stream); 109 | let id = RwStreamReadUInt32(stream); 110 | if(stream.eof) 111 | return null; 112 | let version = 0; 113 | let build = 0; 114 | if((id & 0xFFFF0000) == 0){ 115 | version = id<<8; 116 | build = 0; 117 | }else{ 118 | version = ((id>>14) & 0x3FF00) + 0x30000 | 119 | ((id>>16) & 3); 120 | build = id & 0xFFFF; 121 | } 122 | return { 123 | type: t, 124 | length: l, 125 | version: version, 126 | build: build, 127 | }; 128 | } 129 | 130 | function 131 | RwStreamFindChunk(stream, type) 132 | { 133 | let header; 134 | while(header = rwStreamReadChunkHeader(stream)){ 135 | if(header.type == type) 136 | return header; 137 | RwStreamSkip(stream, header.length); 138 | } 139 | return null; 140 | } 141 | 142 | function 143 | rwPluginRegistryReadDataChunks(tklist, stream, object) 144 | { 145 | let header; 146 | if((header = RwStreamFindChunk(stream, rwID_EXTENSION)) == null) 147 | return null; 148 | let end = stream.offset + header.length; 149 | while(stream.offset < end){ 150 | header = rwStreamReadChunkHeader(stream); 151 | if(header.type in tklist && tklist[header.type].streamRead){ 152 | if(!tklist[header.type].streamRead(stream, object, header.length)) 153 | return null; 154 | }else 155 | RwStreamSkip(stream, header.length); 156 | } 157 | return 1; 158 | } 159 | 160 | function 161 | rwStringStreamFindAndRead(stream) 162 | { 163 | let header; 164 | if((header = RwStreamFindChunk(stream, rwID_STRING)) == null) 165 | return null; 166 | return RwStreamReadString(stream, header.length); 167 | } 168 | 169 | function 170 | rwFrameListStreamRead(stream) 171 | { 172 | let header; 173 | if((header = RwStreamFindChunk(stream, rwID_STRUCT)) == null) 174 | return null; 175 | let numFrames = RwStreamReadInt32(stream); 176 | let frames = []; 177 | for(let i = 0; i < numFrames; i++){ 178 | let xx = RwStreamReadReal(stream); 179 | let xy = RwStreamReadReal(stream); 180 | let xz = RwStreamReadReal(stream); 181 | let yx = RwStreamReadReal(stream); 182 | let yy = RwStreamReadReal(stream); 183 | let yz = RwStreamReadReal(stream); 184 | let zx = RwStreamReadReal(stream); 185 | let zy = RwStreamReadReal(stream); 186 | let zz = RwStreamReadReal(stream); 187 | let wx = RwStreamReadReal(stream); 188 | let wy = RwStreamReadReal(stream); 189 | let wz = RwStreamReadReal(stream); 190 | 191 | frame = RwFrameCreate(); 192 | mat4.set(frame.matrix, 193 | xx, xy, xz, 0, 194 | yx, yy, yz, 0, 195 | zx, zy, zz, 0, 196 | wx, wy, wz, 1); 197 | frames.push(frame); 198 | let parent = RwStreamReadInt32(stream); 199 | RwStreamReadInt32(stream); // unused 200 | if(parent >= 0) 201 | RwFrameAddChild(frames[parent], frame); 202 | } 203 | for(let i = 0; i < numFrames; i++) 204 | if(!rwPluginRegistryReadDataChunks(frameTKList, stream, frames[i])) 205 | return null; 206 | return frames; 207 | } 208 | 209 | function 210 | RwTextureStreamRead(stream) 211 | { 212 | let header; 213 | if((header = RwStreamFindChunk(stream, rwID_STRUCT)) == null) 214 | return null; 215 | let flags = RwStreamReadUInt32(stream); // we ignore this 216 | let name = rwStringStreamFindAndRead(stream); 217 | if(name == null) return null; 218 | let mask = rwStringStreamFindAndRead(stream); 219 | if(mask == null) return null; 220 | let tex = RwTextureRead(name, mask); 221 | if(!rwPluginRegistryReadDataChunks(textureTKList, stream, tex)) 222 | return null; 223 | return tex; 224 | } 225 | 226 | function 227 | RpMaterialStreamRead(stream) 228 | { 229 | let header; 230 | if((header = RwStreamFindChunk(stream, rwID_STRUCT)) == null) 231 | return null; 232 | let mat = RpMaterialCreate(); 233 | RwStreamReadInt32(stream); // flags, unused 234 | mat.color[0] = RwStreamReadUInt8(stream); 235 | mat.color[1] = RwStreamReadUInt8(stream); 236 | mat.color[2] = RwStreamReadUInt8(stream); 237 | mat.color[3] = RwStreamReadUInt8(stream); 238 | RwStreamReadInt32(stream); // unused 239 | let textured = RwStreamReadInt32(stream); 240 | mat.surfaceProperties[0] = RwStreamReadReal(stream); 241 | mat.surfaceProperties[1] = RwStreamReadReal(stream); 242 | mat.surfaceProperties[2] = RwStreamReadReal(stream); 243 | if(textured){ 244 | if((header = RwStreamFindChunk(stream, rwID_TEXTURE)) == null) 245 | return null; 246 | mat.texture = RwTextureStreamRead(stream); 247 | if(mat.texture == null) 248 | return null; 249 | } 250 | if(!rwPluginRegistryReadDataChunks(materialTKList, stream, mat)) 251 | return null; 252 | return mat; 253 | } 254 | 255 | function 256 | rpMaterialListStreamRead(stream) 257 | { 258 | let header; 259 | if((header = RwStreamFindChunk(stream, rwID_STRUCT)) == null) 260 | return null; 261 | let numMaterials = RwStreamReadInt32(stream); 262 | let indices = []; 263 | while(numMaterials--) 264 | indices.push(RwStreamReadInt32(stream)); 265 | let materials = [] 266 | for(let i = 0; i < indices.length; i++){ 267 | if(indices[i] >= 0) 268 | materials.push(materials[indices[i]]); 269 | else{ 270 | if((header = RwStreamFindChunk(stream, rwID_MATERIAL)) == null) 271 | return null; 272 | let m = RpMaterialStreamRead(stream); 273 | if(m == null) 274 | return null; 275 | materials.push(m); 276 | } 277 | } 278 | return materials; 279 | } 280 | 281 | function 282 | RpGeometryStreamRead(stream) 283 | { 284 | let header; 285 | if((header = RwStreamFindChunk(stream, rwID_STRUCT)) == null) 286 | return null; 287 | let flags = RwStreamReadUInt32(stream); 288 | let numTriangles = RwStreamReadInt32(stream); 289 | let numVertices = RwStreamReadInt32(stream); 290 | let numMorphTargets = RwStreamReadInt32(stream); 291 | if(header.version < 0x34000) 292 | RwStreamSkip(stream, 12); 293 | if(flags & 0x01000000) return null; // native geometry not supported 294 | 295 | let geo = RpGeometryCreate(flags, numMorphTargets); 296 | geo.numVertices = numVertices; 297 | 298 | if(geo.prelit) 299 | for(let i = 0; i < numVertices; i++){ 300 | let r = RwStreamReadUInt8(stream); 301 | let g = RwStreamReadUInt8(stream); 302 | let b = RwStreamReadUInt8(stream); 303 | let a = RwStreamReadUInt8(stream); 304 | geo.prelit.push([r, g, b, a]); 305 | } 306 | 307 | for(let i = 0; i < geo.texCoords.length; i++){ 308 | let texCoords = geo.texCoords[i]; 309 | for(let j = 0; j < numVertices; j++){ 310 | let u = RwStreamReadReal(stream); 311 | let v = RwStreamReadReal(stream); 312 | texCoords.push([u, v]); 313 | } 314 | } 315 | 316 | for(let i = 0; i < numTriangles; i++){ 317 | let w1 = RwStreamReadUInt32(stream); 318 | let w2 = RwStreamReadUInt32(stream); 319 | let v1 = w1>>16 & 0xFFFF; 320 | let v2 = w1 & 0xFFFF; 321 | let v3 = w2>>16 & 0xFFFF; 322 | let matid = w2 & 0xFFFF; 323 | geo.triangles.push([v1, v2, v3, matid]); 324 | } 325 | 326 | for(let i = 0; i < numMorphTargets; i++){ 327 | let mt = geo.morphTargets[i]; 328 | 329 | RwStreamSkip(stream, 4*4 + 4 + 4); // ignore bounding sphere and flags 330 | for(let j = 0; j < numVertices; j++){ 331 | let x = RwStreamReadReal(stream); 332 | let y = RwStreamReadReal(stream); 333 | let z = RwStreamReadReal(stream); 334 | mt.vertices.push([x, y, z]); 335 | } 336 | if(mt.normals) 337 | for(let j = 0; j < numVertices; j++){ 338 | let x = RwStreamReadReal(stream); 339 | let y = RwStreamReadReal(stream); 340 | let z = RwStreamReadReal(stream); 341 | mt.normals.push([x, y, z]); 342 | } 343 | } 344 | 345 | if((header = RwStreamFindChunk(stream, rwID_MATLIST)) == null) 346 | return null; 347 | geo.materials = rpMaterialListStreamRead(stream); 348 | if(geo.materials == null) 349 | return null; 350 | 351 | if(!rwPluginRegistryReadDataChunks(geometryTKList, stream, geo)) 352 | return null; 353 | 354 | return geo; 355 | } 356 | 357 | function 358 | rpMeshRead(stream, geo, length) 359 | { 360 | geo.meshtype = RwStreamReadInt32(stream); 361 | let numMeshes = RwStreamReadInt32(stream); 362 | geo.totalMeshIndices = RwStreamReadInt32(stream); 363 | while(numMeshes--){ 364 | let numIndices = RwStreamReadInt32(stream); 365 | let matid = RwStreamReadInt32(stream); 366 | let m = { 367 | indices: [], 368 | material: geo.materials[matid] 369 | }; 370 | while(numIndices--) 371 | m.indices.push(RwStreamReadInt32(stream)); 372 | geo.meshes.push(m); 373 | } 374 | return geo; 375 | } 376 | 377 | function 378 | rpGeometryListStreamRead(stream) 379 | { 380 | let header; 381 | if((header = RwStreamFindChunk(stream, rwID_STRUCT)) == null) 382 | return null; 383 | let numGeoms = RwStreamReadInt32(stream); 384 | let geoms = [] 385 | while(numGeoms--){ 386 | if((header = RwStreamFindChunk(stream, rwID_GEOMETRY)) == null) 387 | return null; 388 | let g = RpGeometryStreamRead(stream); 389 | if(g == null) 390 | return null; 391 | geoms.push(g); 392 | } 393 | return geoms; 394 | } 395 | 396 | function 397 | rpClumpAtomicStreamRead(stream, frames, geos) 398 | { 399 | let header; 400 | if((header = RwStreamFindChunk(stream, rwID_STRUCT)) == null) 401 | return null; 402 | let atomic = RpAtomicCreate(); 403 | let frame = RwStreamReadInt32(stream); 404 | let geometry = RwStreamReadInt32(stream); 405 | let flags = RwStreamReadInt32(stream); // ignored 406 | RwStreamReadInt32(stream); // unused 407 | RpAtomicSetFrame(atomic, frames[frame]); 408 | atomic.geometry = geos[geometry]; 409 | 410 | if(!rwPluginRegistryReadDataChunks(atomicTKList, stream, atomic)) 411 | return null; 412 | 413 | return atomic; 414 | } 415 | 416 | function 417 | RpClumpStreamRead(stream) 418 | { 419 | let header; 420 | if((header = RwStreamFindChunk(stream, rwID_STRUCT)) == null) 421 | return null; 422 | 423 | let numAtomics = RwStreamReadInt32(stream); 424 | let numLights = 0; 425 | let numCameras = 0; 426 | if(header.version > 0x33000){ 427 | numLights = RwStreamReadInt32(stream); 428 | numCameras = RwStreamReadInt32(stream); 429 | } 430 | 431 | if((header = RwStreamFindChunk(stream, rwID_FRAMELIST)) == null) 432 | return null; 433 | frames = rwFrameListStreamRead(stream); 434 | if(frames == null) 435 | return null; 436 | 437 | clump = RpClumpCreate(); 438 | RpClumpSetFrame(clump, frames[0]); 439 | 440 | if((header = RwStreamFindChunk(stream, rwID_GEOMETRYLIST)) == null) 441 | return null; 442 | geos = rpGeometryListStreamRead(stream); 443 | if(geos == null) 444 | return null; 445 | 446 | while(numAtomics--){ 447 | if((header = RwStreamFindChunk(stream, rwID_ATOMIC)) == null) 448 | return null; 449 | let a = rpClumpAtomicStreamRead(stream, frames, geos); 450 | if(a == null) 451 | return null; 452 | clump.atomics.push(a); 453 | } 454 | 455 | if(!rwPluginRegistryReadDataChunks(clumpTKList, stream, clump)) 456 | return null; 457 | 458 | rwFrameSynchLTM(clump.frame); 459 | 460 | // TODO? lights, cameras 461 | 462 | return clump; 463 | } 464 | 465 | 466 | /* 467 | * Plugins 468 | */ 469 | 470 | /* MatFX */ 471 | 472 | var rpMATFXEFFECTBUMPMAP = 1; 473 | var rpMATFXEFFECTENVMAP = 2; 474 | var rpMATFXEFFECTBUMPENVMAP = 3; 475 | var rpMATFXEFFECTDUAL = 4; 476 | var rpMATFXEFFECTUVTRANSFORM = 5; 477 | var rpMATFXEFFECTDUALUVTRANSFORM = 6; 478 | 479 | function 480 | RpMatFXMaterialSetEffects(mat, effects) 481 | { 482 | mat.matfx = { 483 | type: effects, 484 | bump: false, 485 | env: false, 486 | dual: false, 487 | uvxform: false 488 | }; 489 | // TODO: init the relevant fields here 490 | switch(effects){ 491 | case rpMATFXEFFECTBUMPMAP: 492 | mat.matfx.bump = true; 493 | break; 494 | case rpMATFXEFFECTENVMAP: 495 | mat.matfx.env = true; 496 | break; 497 | case rpMATFXEFFECTBUMPENVMAP: 498 | mat.matfx.bump = true; 499 | mat.matfx.env = true; 500 | break; 501 | case rpMATFXEFFECTDUAL: 502 | mat.matfx.dual = true; 503 | break; 504 | case rpMATFXEFFECTUVTRANSFORM: 505 | mat.matfx.uvxform = true; 506 | break; 507 | case rpMATFXEFFECTDUALUVTRANSFORM: 508 | mat.matfx.dual = true; 509 | mat.matfx.uvxform = true; 510 | break; 511 | } 512 | } 513 | 514 | function 515 | rpMatfxMaterialStreamRead(stream, mat, length) 516 | { 517 | let header; 518 | 519 | let effects = RwStreamReadInt32(stream); 520 | RpMatFXMaterialSetEffects(mat, effects); 521 | let mfx = mat.matfx; 522 | 523 | for(let i = 0; i < 2; i++){ 524 | let type = RwStreamReadInt32(stream); 525 | switch(type){ 526 | case rpMATFXEFFECTBUMPMAP: 527 | mfx.bumpCoefficient = RwStreamReadReal(stream); 528 | if(RwStreamReadInt32(stream)){ 529 | if((header = RwStreamFindChunk(stream, rwID_TEXTURE)) == null) 530 | return null; 531 | mfx.bumpedTex = RwTextureStreamRead(stream); 532 | if(mfx.bumpedTex == null) 533 | return null; 534 | } 535 | if(RwStreamReadInt32(stream)){ 536 | if((header = RwStreamFindChunk(stream, rwID_TEXTURE)) == null) 537 | return null; 538 | mfx.bumpTex = RwTextureStreamRead(stream); 539 | if(mfx.bumpTex == null) 540 | return null; 541 | } 542 | break; 543 | case rpMATFXEFFECTENVMAP: 544 | mfx.envCoefficient = RwStreamReadReal(stream); 545 | mfx.envFBalpha = RwStreamReadInt32(stream); 546 | if(RwStreamReadInt32(stream)){ 547 | if((header = RwStreamFindChunk(stream, rwID_TEXTURE)) == null) 548 | return null; 549 | mfx.envTex = RwTextureStreamRead(stream); 550 | if(mfx.envTex == null) 551 | return null; 552 | } 553 | break; 554 | case rpMATFXEFFECTDUAL: 555 | mfs.srcBlend = RwStreamReadInt32(stream); 556 | mfs.dstBlend = RwStreamReadInt32(stream); 557 | if(RwStreamReadInt32(stream)){ 558 | if((header = RwStreamFindChunk(stream, rwID_TEXTURE)) == null) 559 | return null; 560 | mfx.dualTex = RwTextureStreamRead(stream); 561 | if(mfx.dualTex == null) 562 | return null; 563 | } 564 | break; 565 | } 566 | } 567 | 568 | return mat; 569 | } 570 | 571 | function 572 | rpMatfxAtomicStreamRead(stream, atomic, length) 573 | { 574 | atomic.matfx = RwStreamReadInt32(stream); 575 | if(atomic.matfx) 576 | atomic.pipeline = matFXPipe; 577 | return atomic; 578 | } 579 | 580 | 581 | /* GTA Node Name */ 582 | 583 | function 584 | NodeNameStreamRead(stream, frame, length) 585 | { 586 | frame.name = RwStreamReadString(stream, length); 587 | return frame; 588 | } 589 | 590 | /* GTA Env Map */ 591 | 592 | function 593 | envMatStreamRead(stream, mat, length) 594 | { 595 | let sclX = RwStreamReadReal(stream); 596 | let sclY = RwStreamReadReal(stream); 597 | let transSclX = RwStreamReadReal(stream); 598 | let transSclY = RwStreamReadReal(stream); 599 | let shininess = RwStreamReadReal(stream); 600 | RwStreamReadInt32(stream); // ignore 601 | 602 | mat.envMap = { 603 | scale: [ sclX, sclY ], 604 | transScale: [ transSclX, transSclY ], 605 | shininess: shininess 606 | }; 607 | return mat; 608 | } 609 | 610 | /* GTA Spec Map */ 611 | 612 | function 613 | specMatStreamRead(stream, mat, length) 614 | { 615 | let specularity = RwStreamReadReal(stream); 616 | let texname = RwStreamReadString(stream, 24); 617 | 618 | mat.specMap = { 619 | specularity: specularity, 620 | texture: RwTextureRead(texname, "") 621 | }; 622 | return mat; 623 | } 624 | -------------------------------------------------------------------------------- /shaders.js: -------------------------------------------------------------------------------- 1 | const defaultVS = ` 2 | attribute vec3 in_pos; 3 | attribute vec3 in_normal; 4 | attribute vec4 in_color; 5 | attribute vec2 in_tex0; 6 | 7 | uniform mat4 u_world; 8 | uniform mat4 u_view; 9 | uniform mat4 u_proj; 10 | 11 | uniform vec4 u_matColor; 12 | uniform vec4 u_surfaceProps; 13 | 14 | uniform vec3 u_ambLight; 15 | uniform vec3 u_lightDir; 16 | uniform vec3 u_lightCol; 17 | 18 | varying highp vec4 v_color; 19 | varying highp vec2 v_tex0; 20 | 21 | void main() { 22 | gl_Position = u_proj * u_view * u_world * vec4(in_pos, 1.0); 23 | v_tex0 = in_tex0; 24 | 25 | v_color = in_color; 26 | 27 | v_color.rgb += u_ambLight*u_surfaceProps.x; 28 | vec3 N = mat3(u_world) * in_normal; 29 | float L = max(0.0, dot(N, -normalize(u_lightDir))); 30 | v_color.rgb += L*u_lightCol*u_surfaceProps.z; 31 | v_color = clamp(v_color, 0.0, 1.0); 32 | v_color *= u_matColor; 33 | } 34 | `; 35 | 36 | const defaultFS = ` 37 | uniform sampler2D tex; 38 | 39 | uniform highp float u_alphaRef; 40 | 41 | varying highp vec4 v_color; 42 | varying highp vec2 v_tex0; 43 | 44 | void main() { 45 | gl_FragColor = v_color*texture2D(tex, v_tex0); 46 | if(gl_FragColor.a < u_alphaRef) 47 | discard; 48 | } 49 | `; 50 | 51 | 52 | const envVS = ` 53 | attribute vec3 in_pos; 54 | attribute vec3 in_normal; 55 | attribute vec4 in_color; 56 | attribute vec2 in_tex0; 57 | 58 | uniform mat4 u_world; 59 | uniform mat4 u_view; 60 | uniform mat4 u_proj; 61 | uniform mat4 u_env; 62 | 63 | uniform vec4 u_matColor; 64 | uniform vec4 u_surfaceProps; 65 | 66 | uniform vec3 u_ambLight; 67 | uniform vec3 u_lightDir; 68 | uniform vec3 u_lightCol; 69 | 70 | varying highp vec4 v_color0; 71 | varying highp vec4 v_color1; 72 | varying highp vec2 v_tex0; 73 | varying highp vec2 v_tex1; 74 | 75 | void main() { 76 | gl_Position = u_proj * u_view * u_world * vec4(in_pos, 1.0); 77 | v_tex0 = in_tex0; 78 | 79 | v_color0 = in_color; 80 | 81 | v_color0.rgb += u_ambLight*u_surfaceProps.x; 82 | vec3 N = mat3(u_world) * in_normal; 83 | float L = max(0.0, dot(N, -normalize(u_lightDir))); 84 | v_color0.rgb += L*u_lightCol*u_surfaceProps.z; 85 | v_color0 = clamp(v_color0, 0.0, 1.0); 86 | v_color0 *= u_matColor; 87 | 88 | v_color1 = v_color0*u_surfaceProps.y; 89 | 90 | v_tex1 = (u_env*vec4(N, 1.0)).xy; 91 | } 92 | `; 93 | 94 | const envFS = ` 95 | uniform sampler2D tex0; 96 | uniform sampler2D tex1; 97 | 98 | uniform highp float u_alphaRef; 99 | 100 | varying highp vec4 v_color0; 101 | varying highp vec4 v_color1; 102 | varying highp vec2 v_tex0; 103 | varying highp vec2 v_tex1; 104 | 105 | void main() { 106 | gl_FragColor = v_color0*texture2D(tex0, v_tex0); 107 | if(gl_FragColor.a < u_alphaRef) 108 | discard; 109 | gl_FragColor.rgb += (v_color1*texture2D(tex1, v_tex1)).rgb; 110 | } 111 | `; 112 | 113 | 114 | const carPS2VS = ` 115 | attribute vec3 in_pos; 116 | attribute vec3 in_normal; 117 | attribute vec4 in_color; 118 | attribute vec2 in_tex0; 119 | attribute vec2 in_tex1; 120 | 121 | uniform mat4 u_world; 122 | uniform mat4 u_view; 123 | uniform mat4 u_proj; 124 | uniform mat4 u_env; 125 | 126 | uniform vec4 u_matColor; 127 | uniform vec4 u_surfaceProps; 128 | 129 | uniform vec3 u_ambLight; 130 | uniform vec3 u_lightDir; 131 | uniform vec3 u_lightCol; 132 | 133 | varying highp vec4 v_color0; 134 | varying highp vec4 v_color1; 135 | varying highp vec4 v_color2; 136 | varying highp vec2 v_tex0; 137 | varying highp vec2 v_tex1; 138 | varying highp vec2 v_tex2; 139 | 140 | void main() { 141 | gl_Position = u_proj * u_view * u_world * vec4(in_pos, 1.0); 142 | v_tex0 = in_tex0; 143 | 144 | v_color0 = in_color; 145 | 146 | v_color0.rgb += u_ambLight*u_surfaceProps.x; 147 | vec3 N = mat3(u_world) * in_normal; 148 | float L = max(0.0, dot(N, -normalize(u_lightDir))); 149 | v_color0.rgb += L*u_lightCol*u_surfaceProps.z; 150 | v_color0 = clamp(v_color0, 0.0, 1.0); 151 | v_color0 *= u_matColor; 152 | 153 | v_tex1 = in_tex1; 154 | v_color1 = vec4(1.5*u_surfaceProps.w); 155 | 156 | 157 | N = mat3(u_view) * N; 158 | vec3 D = mat3(u_view) * u_lightDir; 159 | N = D - 2.0*N*dot(N, D); 160 | v_tex2 = (N.xy + vec2(1.0, 1.0))/2.0; 161 | if(N.z < 0.0) 162 | v_color2 = vec4(0.75*u_surfaceProps.y); 163 | else 164 | v_color2 = vec4(0.0); 165 | } 166 | `; 167 | 168 | const carPS2FS = ` 169 | uniform sampler2D tex0; 170 | uniform sampler2D tex1; 171 | uniform sampler2D tex2; 172 | 173 | uniform highp float u_alphaRef; 174 | 175 | varying highp vec4 v_color0; 176 | varying highp vec4 v_color1; 177 | varying highp vec4 v_color2; 178 | varying highp vec2 v_tex0; 179 | varying highp vec2 v_tex1; 180 | varying highp vec2 v_tex2; 181 | 182 | void main() { 183 | gl_FragColor = v_color0*texture2D(tex0, v_tex0); 184 | if(gl_FragColor.a < u_alphaRef) 185 | discard; 186 | gl_FragColor.rgb += (v_color1*texture2D(tex1, v_tex1)).rgb; 187 | gl_FragColor.rgb += (v_color2*texture2D(tex2, v_tex2)).rgb; 188 | } 189 | `; 190 | -------------------------------------------------------------------------------- /ui.js: -------------------------------------------------------------------------------- 1 | showInterface = true; 2 | autoRotateCamera = false; 3 | 4 | function 5 | hex2rgb(hex) { 6 | let r = parseInt(hex.slice(1, 3), 16); 7 | let g = parseInt(hex.slice(3, 5), 16); 8 | let b = parseInt(hex.slice(5, 7), 16); 9 | return [r, g, b]; 10 | } 11 | 12 | function 13 | updateVehicleCustomColors() { 14 | let colors = []; 15 | for(let i = 0; i < 4; i++) { 16 | let cStr = document.getElementById("custom-color" + i).value; 17 | let c = hex2rgb(cStr); 18 | c[3] = 255; 19 | colors[i] = c; 20 | } 21 | setVehicleColors(modelinfo, colors[0], colors[1], colors[2], colors[3]); 22 | } 23 | 24 | for(let i = 0; i < 4; i++) { 25 | document.getElementById("custom-color" + i).addEventListener("input", updateVehicleCustomColors, false); 26 | } 27 | 28 | document.addEventListener("keypress", 29 | function(e) { 30 | if(e.key === "i") { 31 | showInterface = !showInterface; 32 | document.querySelectorAll(".ui").forEach((v) => { 33 | v.style.visibility = showInterface ? "unset" : "hidden"; 34 | }); 35 | } 36 | 37 | else if(e.key === "r") { 38 | autoRotateCamera = !autoRotateCamera; 39 | } 40 | }, 41 | false); 42 | 43 | document.getElementById("objects").addEventListener("keypress", 44 | function(e) { 45 | e.preventDefault(); 46 | return false; 47 | }, 48 | false); 49 | 50 | var lastModelChangeViaKey = 0; 51 | document.getElementById("objects").addEventListener("keydown", 52 | function(e) { 53 | if(e.keyCode !== 38 && e.keyCode !== 40) { 54 | return true; 55 | } 56 | 57 | if(Date.now() - lastModelChangeViaKey < 750) { 58 | e.preventDefault(); 59 | return false; 60 | } 61 | 62 | lastModelChangeViaKey = Date.now(); 63 | }, 64 | false); 65 | 66 | 67 | document.getElementById("objects").addEventListener("keyup", 68 | function(e) { 69 | if(e.keyCode !== 38 && e.keyCode !== 40) { 70 | return true; 71 | } 72 | 73 | lastModelChangeViaKey -= 400; 74 | 75 | let model = document.getElementById("objects").value; 76 | if(model !== CurrentModel.model) { 77 | SelectModel(model); 78 | } 79 | }, 80 | false); 81 | 82 | function 83 | uiSetCurrentGame(game) { 84 | document.querySelectorAll("#control a").forEach((v) => { 85 | v.classList.remove("active"); 86 | }); 87 | 88 | let gameSelect = document.getElementById("game-select-" + game); 89 | gameSelect.classList.add("active"); 90 | } 91 | 92 | function 93 | uiSetCurrentModel(model) { 94 | let l = document.querySelectorAll("#objects option"); 95 | for(let i = 0; i < l.length; i++) { 96 | if(l[i].value === model) { 97 | l[i].selected = 'selected'; 98 | break; 99 | } 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /webgl.css: -------------------------------------------------------------------------------- 1 | html { 2 | height: 100%; 3 | } 4 | 5 | body { 6 | margin: 0; 7 | padding: 0; 8 | height: 100%; 9 | overflow: hidden; 10 | } 11 | 12 | a { 13 | color: black; 14 | } 15 | 16 | body, canvas { 17 | background-color: #808080; 18 | } 19 | 20 | video { 21 | display: none; 22 | } 23 | 24 | #objects { 25 | outline: none; 26 | height: calc(100% - 18.4px); 27 | width: 110px; 28 | border: unset; 29 | } 30 | 31 | .select-panel { 32 | position: absolute; 33 | float: left; 34 | height: 100%; 35 | } 36 | 37 | #control { 38 | text-align: center; 39 | background-color: white; 40 | color: black; 41 | font-family: Arial; 42 | font-size: 18px; 43 | font-weight: bold; 44 | padding-top: 5px; 45 | padding-bottom: 5px; 46 | } 47 | 48 | .objects-wrapper { 49 | float: left; 50 | height: calc(100% - 15px); 51 | width: calc(100%); 52 | } 53 | 54 | .viewer-panel { 55 | position: absolute; 56 | top: 0; 57 | left: 0; 58 | } 59 | 60 | option { 61 | padding-left: 5px; 62 | font-size: 14px; 63 | padding-top: 3px; 64 | padding-bottom: 3px; 65 | } 66 | 67 | option:hover { 68 | cursor: pointer; 69 | } 70 | 71 | #colors { 72 | position: absolute; 73 | bottom: 20px; 74 | left: 130px; 75 | 76 | border-radius: 2px; 77 | padding: 5px; 78 | } 79 | 80 | #colors td { 81 | border: 1px solid white; 82 | } 83 | 84 | #colors td:hover { 85 | cursor: pointer; 86 | } 87 | 88 | .frames-wrapper { 89 | position: absolute; 90 | top: 0; 91 | right: 100px; 92 | } 93 | 94 | .bottom-links { 95 | position: absolute; 96 | bottom: 10px; 97 | right: 10px; 98 | margin: 0; 99 | } 100 | 101 | .bottom-links ul { 102 | list-style-type: none; 103 | } 104 | 105 | .bottom-links li { 106 | float: left; 107 | margin-left: 10px; 108 | list-style-type: none; 109 | } 110 | 111 | .custom-colors { 112 | position: absolute; 113 | bottom: 210px; 114 | left: 130px; 115 | } 116 | 117 | .custom-colors input:hover { 118 | cursor: pointer; 119 | } 120 | 121 | #control a { 122 | text-decoration: none; 123 | } 124 | 125 | #control a:hover, #control a.active { 126 | text-decoration: underline; 127 | } 128 | 129 | @media (prefers-color-scheme: dark) 130 | { 131 | body, canvas { 132 | background-color: #181818; 133 | } 134 | 135 | body, a, #objects, option { 136 | color: #f5f5f5 !important; 137 | } 138 | 139 | #objects { 140 | background-color: #292929; 141 | } 142 | 143 | option:checked, option:hover, .option:active { 144 | background-color: #4f4f4f !important; 145 | } 146 | 147 | #colors, .custom-colors input { 148 | background-color: #343434; 149 | } 150 | 151 | #colors td { 152 | border: 1px solid white; 153 | } 154 | 155 | .custom-colors input { 156 | border-radius: 2px; 157 | } 158 | 159 | #objects option:nth-child(2n) { 160 | background-color: #2e2e2e; 161 | } 162 | 163 | #control { 164 | background-color: #292929; 165 | color: #f5f5f5; 166 | } 167 | } 168 | --------------------------------------------------------------------------------