├── .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 |
30 |
31 |
32 |
37 |
38 |
39 |
42 |
43 |
44 |
45 |
46 | - Press I to toggle interface
47 | - |
48 | -
49 | Source code
50 |
51 |
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 |
--------------------------------------------------------------------------------