1/*
2Copyright (c) 2012, Broadcom Europe Ltd
3All rights reserved.
4
5Redistribution and use in source and binary forms, with or without
6modification, are permitted provided that the following conditions are met:
7 * Redistributions of source code must retain the above copyright
8 notice, this list of conditions and the following disclaimer.
9 * Redistributions in binary form must reproduce the above copyright
10 notice, this list of conditions and the following disclaimer in the
11 documentation and/or other materials provided with the distribution.
12 * Neither the name of the copyright holder nor the
13 names of its contributors may be used to endorse or promote products
14 derived from this software without specific prior written permission.
15
16THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
17ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
18WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
19DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY
20DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
21(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
22LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
23ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
24(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
25SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
26*/
27
28#include <math.h>
29#include <stdio.h>
30#include <stdint.h>
31
32#include "GLES/gl.h"
33#include <GLES/glext.h>
34#include "EGL/egl.h"
35#include "EGL/eglext.h"
36#include "models.h"
37
38#define VMCS_RESOURCE(a,b) (b)
39
40/******************************************************************************
41Private typedefs, macros and constants
42******************************************************************************/
43
44enum {VBO_VERTEX, VBO_NORMAL, VBO_TEXTURE, VBO_MAX};
45#define MAX_MATERIALS 4
46#define MAX_MATERIAL_NAME 32
47
48typedef struct wavefront_material_s {
49 GLuint vbo[VBO_MAX];
50 int numverts;
51 char name[MAX_MATERIAL_NAME];
52 GLuint texture;
53} WAVEFRONT_MATERIAL_T;
54
55typedef struct wavefront_model_s {
56 WAVEFRONT_MATERIAL_T material[MAX_MATERIALS];
57 int num_materials;
58 GLuint texture;
59} WAVEFRONT_MODEL_T;
60
61
62/******************************************************************************
63Static Data
64******************************************************************************/
65
66/******************************************************************************
67Static Function Declarations
68******************************************************************************/
69
70/******************************************************************************
71Static Function Definitions
72******************************************************************************/
73
74static void create_vbo(GLenum type, GLuint *vbo, int size, void *data)
75{
76 glGenBuffers(1, vbo);
77 vc_assert(*vbo);
78 glBindBuffer(type, *vbo);
79 glBufferData(type, size, data, GL_STATIC_DRAW);
80 glBindBuffer(type, 0);
81}
82
83
84static void destroy_vbo(GLuint *vbo)
85{
86 glDeleteBuffers(1, vbo);
87 *vbo = 0;
88}
89
90#define MAX_VERTICES 100000
91static void *allocbuffer(int size)
92{
93 return malloc(size);
94}
95
96static void freebuffer(void *p)
97{
98 free (p);
99}
100
101static void centre_and_rescale(float *verts, int numvertices)
102{
103 float cx=0.0f, cy=0.0f, cz=0.0f, scale=0.0f;
104 float minx=0.0f, miny=0.0f, minz=0.0f;
105 float maxx=0.0f, maxy=0.0f, maxz=0.0f;
106 int i;
107 float *v = verts;
108 minx = maxx = verts[0];
109 miny = maxy = verts[1];
110 minz = maxz = verts[2];
111 for (i=0; i<numvertices; i++) {
112 float x = *v++;
113 float y = *v++;
114 float z = *v++;
115 minx = vcos_min(minx, x);
116 miny = vcos_min(miny, y);
117 minz = vcos_min(minz, z);
118 maxx = vcos_max(maxx, x);
119 maxy = vcos_max(maxy, y);
120 maxz = vcos_max(maxz, z);
121 cx += x;
122 cy += y;
123 cz += z;
124 }
125 cx /= (float)numvertices;
126 cy /= (float)numvertices;
127 cz /= (float)numvertices;
128 scale = 3.0f / (maxx-minx + maxy-miny + maxz-minz);
129 v = verts;
130 for (i=0; i<numvertices; i++) {
131 *v = (*v-cx) * scale; v++;
132 *v = (*v-cy) * scale; v++;
133 *v = (*v-cz) * scale; v++;
134 }
135}
136
137static void renormalise(float *verts, int numvertices)
138{
139 int i;
140 float *v = verts;
141 for (i=0;i<numvertices; i++) {
142 float x = v[0];
143 float y = v[1];
144 float z = v[2];
145 float scale = 1.0f/sqrtf(x*x + y*y + z*z);
146 *v++ = x * scale;
147 *v++ = y * scale;
148 *v++ = z * scale;
149 }
150}
151
152static void deindex(float *dst, const float *src, const unsigned short *indexes, GLsizei size, GLsizei count)
153{
154 int i;
155 for (i=0; i<count; i++) {
156 int ind = size * (indexes[0]-1);
157 *dst++ = src[ind + 0];
158 *dst++ = src[ind + 1];
159 // todo: optimise - move out of loop
160 if (size >= 3) *dst++ = src[ind + 2];
161 indexes += 3;
162 }
163}
164
165int draw_wavefront(MODEL_T m, GLuint texture)
166{
167 int i;
168 WAVEFRONT_MODEL_T *model = (WAVEFRONT_MODEL_T *)m;
169
170 for (i=0; i<model->num_materials; i++) {
171 WAVEFRONT_MATERIAL_T *mat = model->material + i;
172 if (mat->texture == -1) continue;
173
174 if (mat->texture)
175 glBindTexture(GL_TEXTURE_2D, mat->texture);
176 else
177 glBindTexture(GL_TEXTURE_EXTERNAL_OES, texture);
178
179 if (mat->vbo[VBO_VERTEX]) {
180 glBindBuffer(GL_ARRAY_BUFFER, mat->vbo[VBO_VERTEX]);
181 glVertexPointer(3, GL_FLOAT, 0, NULL);
182 }
183 if (mat->vbo[VBO_NORMAL]) {
184 glEnableClientState(GL_NORMAL_ARRAY);
185 glBindBuffer(GL_ARRAY_BUFFER, mat->vbo[VBO_NORMAL]);
186 glNormalPointer(GL_FLOAT, 0, NULL);
187 } else {
188 glDisableClientState(GL_NORMAL_ARRAY);
189 }
190 if (mat->vbo[VBO_TEXTURE]) {
191 glEnableClientState(GL_TEXTURE_COORD_ARRAY);
192 glBindBuffer(GL_ARRAY_BUFFER, mat->vbo[VBO_TEXTURE]);
193 glTexCoordPointer(2, GL_FLOAT, 0, NULL);
194 } else {
195 glDisableClientState(GL_TEXTURE_COORD_ARRAY);
196 }
197 glDrawArrays(GL_TRIANGLES, 0, mat->numverts);
198 }
199 glBindBuffer(GL_ARRAY_BUFFER, 0);
200 return 0;
201}
202
203struct wavefront_model_loading_s {
204 unsigned short material_index[MAX_MATERIALS];
205 int num_materials;
206 int numv, numt, numn, numf;
207 unsigned int data[0];
208};
209
210static int load_wavefront_obj(const char *modelname, WAVEFRONT_MODEL_T *model, struct wavefront_model_loading_s *m)
211{
212 char line[256+1];
213 unsigned short pp[54+1];
214 FILE *fp;
215 int i, valid;
216 float *qv = (float *)m->data;
217 float *qt = (float *)m->data + 3 * MAX_VERTICES;
218 float *qn = (float *)m->data + (3+2) * MAX_VERTICES;
219 unsigned short *qf = (unsigned short *)((float *)m->data + (3+2+3) * MAX_VERTICES);
220 float *pv = qv, *pt = qt, *pn = qn;
221 unsigned short *pf = qf;
222 fp = fopen(modelname, "r");
223 if (!fp) return -1;
224
225 m->num_materials = 0;
226 m->material_index[0] = 0;
227
228 valid = fread(line, 1, sizeof(line)-1, fp);
229
230 while (valid > 0) {
231 char *s, *end = line;
232
233 while((end-line < valid) && *end != '\n' && *end != '\r')
234 end++;
235 *end++ = 0;
236
237 if((end-line < valid) && *end != '\n' && *end != '\r')
238 *end++ = 0;
239
240 s = line;
241
242 if (s[strlen(s)-1] == 10) s[strlen(s)-1]=0;
243 switch (s[0]) {
244 case '#': break; // comment
245 case '\r': case '\n': case '\0': break; // blank line
246 case 'm': vc_assert(strncmp(s, "mtllib", sizeof "mtllib"-1)==0); break;
247 case 'o': break;
248 case 'u':
249 if (sscanf(s, "usemtl %s", /*MAX_MATERIAL_NAME-1, */model->material[m->num_materials].name) == 1) {
250 if (m->num_materials < MAX_MATERIALS) {
251 if (m->num_materials > 0 && ((pf-qf)/3 == m->material_index[m->num_materials-1] || strcmp(model->material[m->num_materials-1].name, model->material[m->num_materials].name)==0)) {
252 strcpy(model->material[m->num_materials-1].name, model->material[m->num_materials].name);
253 m->num_materials--;
254 } else
255 m->material_index[m->num_materials] = (pf-qf)/3;
256 m->num_materials++;
257 }
258 } else { printf("%s", s); vc_assert(0); }
259 break;
260 case 'g': vc_assert(strncmp(s, "g ", sizeof "g "-1)==0); break;
261 case 's': vc_assert(strncmp(s, "s ", sizeof "s "-1)==0); break;
262 case 'v': case 'f':
263 if (sscanf(s, "v %f %f %f", pv+0, pv+1, pv+2) == 3) {
264 pv += 3;
265 } else if (sscanf(s, "vt %f %f", pt+0, pt+1) == 2) {
266 pt += 2;
267 } else if (sscanf(s, "vn %f %f %f", pn+0, pn+1, pn+2) == 3) {
268 pn += 3;
269 } else if (i = sscanf(s, "f"" %hu//%hu %hu//%hu %hu//%hu %hu//%hu %hu//%hu %hu//%hu"
270 " %hu//%hu %hu//%hu %hu//%hu %hu//%hu %hu//%hu %hu//%hu"
271 " %hu//%hu %hu//%hu %hu//%hu %hu//%hu %hu//%hu %hu//%hu %hu",
272 pp+ 0, pp+ 1, pp+ 2, pp+ 3, pp+ 4, pp+ 5, pp+ 6, pp+ 7, pp+ 8, pp+ 9, pp+10, pp+11,
273 pp+12, pp+13, pp+14, pp+15, pp+16, pp+17, pp+18, pp+19, pp+20, pp+21, pp+22, pp+23,
274 pp+24, pp+25, pp+26, pp+27, pp+28, pp+29, pp+30, pp+32, pp+32, pp+33, pp+34, pp+35, pp+36), i >= 6) {
275 int poly = i/2;
276 //vc_assert(i < countof(pp)); // may need to increment poly count and pp array
277 for (i=1; i<poly-1; i++) {
278 *pf++ = pp[0]; *pf++ = 0; *pf++ = pp[1];
279 *pf++ = pp[2*i+0]; *pf++ = 0; *pf++ = pp[2*i+1];
280 *pf++ = pp[2*(i+1)+0]; *pf++ = 0; *pf++ = pp[2*(i+1)+1];
281 }
282 } else if (i = sscanf(s, "f"" %hu/%hu %hu/%hu %hu/%hu %hu/%hu %hu/%hu %hu/%hu"
283 " %hu/%hu %hu/%hu %hu/%hu %hu/%hu %hu/%hu %hu/%hu"
284 " %hu/%hu %hu/%hu %hu/%hu %hu/%hu %hu/%hu %hu/%hu %hu",
285 pp+ 0, pp+ 1, pp+ 2, pp+ 3, pp+ 4, pp+ 5, pp+ 6, pp+ 7, pp+ 8, pp+ 9, pp+10, pp+11,
286 pp+12, pp+13, pp+14, pp+15, pp+16, pp+17, pp+18, pp+19, pp+20, pp+21, pp+22, pp+23,
287 pp+24, pp+25, pp+26, pp+27, pp+28, pp+29, pp+30, pp+32, pp+32, pp+33, pp+34, pp+35, pp+36), i >= 6) {
288 int poly = i/2;
289 //vc_assert(i < countof(pp); // may need to increment poly count and pp array
290 for (i=1; i<poly-1; i++) {
291 *pf++ = pp[0]; *pf++ = pp[1]; *pf++ = 0;
292 *pf++ = pp[2*i+0]; *pf++ = pp[2*i+1]; *pf++ = 0;
293 *pf++ = pp[2*(i+1)+0]; *pf++ = pp[2*(i+1)+1]; *pf++ = 0;
294 }
295 } else if (i = sscanf(s, "f"" %hu/%hu/%hu %hu/%hu/%hu %hu/%hu/%hu %hu/%hu/%hu %hu/%hu/%hu %hu/%hu/%hu"
296 " %hu/%hu/%hu %hu/%hu/%hu %hu/%hu/%hu %hu/%hu/%hu %hu/%hu/%hu %hu/%hu/%hu"
297 " %hu/%hu/%hu %hu/%hu/%hu %hu/%hu/%hu %hu/%hu/%hu %hu/%hu/%hu %hu/%hu/%hu %hu",
298 pp+ 0, pp+ 1, pp+ 2, pp+ 3, pp+ 4, pp+ 5, pp+ 6, pp+ 7, pp+ 8, pp+ 9, pp+10, pp+11, pp+12, pp+13, pp+14, pp+15, pp+16, pp+17,
299 pp+18, pp+19, pp+20, pp+21, pp+22, pp+23, pp+24, pp+25, pp+26, pp+27, pp+28, pp+29, pp+30, pp+32, pp+32, pp+33, pp+34, pp+35,
300 pp+36, pp+37, pp+38, pp+39, pp+40, pp+41, pp+42, pp+43, pp+44, pp+45, pp+46, pp+47, pp+48, pp+49, pp+50, pp+51, pp+52, pp+53, pp+54), i >= 9) {
301 int poly = i/3;
302 //vc_assert(i < countof(pp); // may need to increment poly count and pp array
303 for (i=1; i<poly-1; i++) {
304 *pf++ = pp[0]; *pf++ = pp[1]; *pf++ = pp[2];
305 *pf++ = pp[3*i+0]; *pf++ = pp[3*i+1]; *pf++ = pp[3*i+2];
306 *pf++ = pp[3*(i+1)+0]; *pf++ = pp[3*(i+1)+1]; *pf++ = pp[3*(i+1)+2];
307 }
308 } else { printf("%s", s); vc_assert(0); }
309 break;
310 default:
311 printf("%02x %02x %s", s[0], s[1], s); vc_assert(0); break;
312 }
313
314 // shift down read characters and read some more into the end
315 // if we didn't find a newline, then end is one off the end of our
316 // line, so end-line will be valid+1
317 i = end-line > valid ? valid : end-line;
318 memmove(line, end, valid - i);
319 valid -= i;
320 valid += fread(line+valid, 1, sizeof(line)-1-valid, fp);
321 }
322 fclose(fp);
323
324 if (m->num_materials==0) m->material_index[m->num_materials++] = 0;
325
326 centre_and_rescale(qv, (pv-qv)/3);
327 renormalise(qn, (pn-qn)/3);
328 //centre_and_rescale2(qt, (pt-qt)/2);
329
330 m->numv = pv-qv;
331 m->numt = pt-qt;
332 m->numn = pn-qn;
333 m->numf = pf-qf;
334
335 // compress array
336 //memcpy((float *)m->data, (float *)m->data, m->numv * sizeof *qv); - nop
337 memcpy((float *)m->data + m->numv, (float *)m->data + 3 * MAX_VERTICES, m->numt * sizeof *qt);
338 memcpy((float *)m->data + m->numv + m->numt,(float *) m->data + (3 + 2) * MAX_VERTICES, m->numn * sizeof *qn);
339 memcpy((float *)m->data + m->numv + m->numt + m->numn, (float *)m->data + (3 + 2 + 3) * MAX_VERTICES, m->numf * sizeof *qf);
340
341 return 0;
342}
343
344static int load_wavefront_dat(const char *modelname, WAVEFRONT_MODEL_T *model, struct wavefront_model_loading_s *m)
345{
346 FILE *fp;
347 int s;
348 const int size = sizeof *m +
349 sizeof(float)*(3+2+3)*MAX_VERTICES + // 3 vertices + 2 textures + 3 normals
350 sizeof(unsigned short)*3*MAX_VERTICES; //each face has 9 vertices
351
352 fp = fopen(modelname, "r");
353 if (!fp) return -1;
354 s = fread(m, 1, size, fp);
355 if (s < 0) return -1;
356 fclose(fp);
357 return 0;
358}
359
360MODEL_T load_wavefront(const char *modelname, const char *texturename)
361{
362 WAVEFRONT_MODEL_T *model;
363 float *temp, *qv, *qt, *qn;
364 unsigned short *qf;
365 int i;
366 int numverts = 0, offset = 0;
367 struct wavefront_model_loading_s *m;
368 int s=-1;
369 char modelname_obj[128];
370 model = malloc(sizeof *model);
371 if (!model || !modelname) return NULL;
372 memset (model, 0, sizeof *model);
373 model->texture = 0; //load_texture(texturename);
374 m = allocbuffer(sizeof *m +
375 sizeof(float)*(3+2+3)*MAX_VERTICES + // 3 vertices + 2 textures + 3 normals
376 sizeof(unsigned short)*3*MAX_VERTICES); //each face has 9 vertices
377 if (!m) return 0;
378
379 if (strlen(modelname) + 5 <= sizeof modelname_obj) {
380 strcpy(modelname_obj, modelname);
381 strcat(modelname_obj, ".dat");
382 s = load_wavefront_dat(modelname_obj, model, m);
383 }
384 if (s==0) {}
385 else if (strncmp(modelname + strlen(modelname) - 4, ".obj", 4) == 0) {
386 #ifdef DUMP_OBJ_DAT
387 int size;
388 FILE *fp;
389 #endif
390 s = load_wavefront_obj(modelname, model, m);
391 #ifdef DUMP_OBJ_DAT
392 strcpy(modelname_obj, modelname);
393 strcat(modelname_obj, ".dat");
394 size = sizeof *m +
395 sizeof(float)*(3*m->numv+2*m->numt+3*m->numn) + // 3 vertices + 2 textures + 3 normals
396 sizeof(unsigned short)*3*m->numf; //each face has 9 vertices
397 fp = host_file_open(modelname_obj, "w");
398 fwrite(m, 1, size, fp);
399 fclose(fp);
400 #endif
401 } else if (strncmp(modelname + strlen(modelname) - 4, ".dat", 4) == 0) {
402 s = load_wavefront_dat(modelname, model, m);
403 }
404 if (s != 0) return 0;
405
406 qv = (float *)(m->data);
407 qt = (float *)(m->data + m->numv);
408 qn = (float *)(m->data + m->numv + m->numt);
409 qf = (unsigned short *)(m->data + m->numv + m->numt + m->numn);
410
411 numverts = m->numf/3;
412 vc_assert(numverts <= MAX_VERTICES);
413
414 temp = allocbuffer(3*numverts*sizeof *temp);
415 for (i=0; i<m->num_materials; i++) {
416 WAVEFRONT_MATERIAL_T *mat = model->material + i;
417 mat->numverts = i < m->num_materials-1 ? m->material_index[i+1]-m->material_index[i] : numverts - m->material_index[i];
418 // vertex, texture, normal
419 deindex(temp, qv, qf+3*offset+0, 3, mat->numverts);
420 create_vbo(GL_ARRAY_BUFFER, mat->vbo+VBO_VERTEX, 3 * mat->numverts * sizeof *qv, temp); // 3
421
422 deindex(temp, qt, qf+3*offset+1, 2, mat->numverts);
423 create_vbo(GL_ARRAY_BUFFER, mat->vbo+VBO_TEXTURE, 2 * mat->numverts * sizeof *qt, temp); // 2
424
425 deindex(temp, qn, qf+3*offset+2, 3, mat->numverts);
426 create_vbo(GL_ARRAY_BUFFER, mat->vbo+VBO_NORMAL, 3 * mat->numverts * sizeof *qn, temp); // 3
427 offset += mat->numverts;
428 mat->texture = model->texture;
429 }
430 model->num_materials = m->num_materials;
431 vc_assert(offset == numverts);
432 freebuffer(temp);
433 freebuffer(m);
434 return (MODEL_T)model;
435}
436
437void unload_wavefront(MODEL_T m)
438{
439 WAVEFRONT_MODEL_T *model = (WAVEFRONT_MODEL_T *)m;
440 int i;
441 for (i=0; i<model->num_materials; i++) {
442 WAVEFRONT_MATERIAL_T *mat = model->material + i;
443 if (mat->vbo[VBO_VERTEX])
444 destroy_vbo(mat->vbo+VBO_VERTEX);
445 if (mat->vbo[VBO_TEXTURE])
446 destroy_vbo(mat->vbo+VBO_TEXTURE);
447 if (mat->vbo[VBO_NORMAL])
448 destroy_vbo(mat->vbo+VBO_NORMAL);
449 }
450}
451
452// create a cube model that looks like a wavefront model,
453MODEL_T cube_wavefront(void)
454{
455 static const float qv[] = {
456 -0.5f, -0.5f, 0.5f,
457 -0.5f, -0.5f, -0.5f,
458 0.5f, -0.5f, -0.5f,
459 0.5f, -0.5f, 0.5f,
460 -0.5f, 0.5f, 0.5f,
461 0.5f, 0.5f, 0.5f,
462 0.5f, 0.5f, -0.5f,
463 -0.5f, 0.5f, -0.5f,
464 };
465
466 static const float qn[] = {
467 0.0f, -1.0f, -0.0f,
468 0.0f, 1.0f, -0.0f,
469 0.0f, 0.0f, 1.0f,
470 1.0f, 0.0f, -0.0f,
471 0.0f, 0.0f, -1.0f,
472 -1.0f, 0.0f, -0.0f,
473 };
474
475 static const float qt[] = {
476 1.0f, 0.0f,
477 1.0f, 1.0f,
478 0.0f, 1.0f,
479 0.0f, 0.0f,
480 };
481
482 static const unsigned short qf[] = {
483 1,1,1, 2,2,1, 3,3,1,
484 3,3,1, 4,4,1, 1,1,1,
485 5,4,2, 6,1,2, 7,2,2,
486 7,2,2, 8,3,2, 5,4,2,
487 1,4,3, 4,1,3, 6,2,3,
488 6,2,3, 5,3,3, 1,4,3,
489 4,4,4, 3,1,4, 7,2,4,
490 7,2,4, 6,3,4, 4,4,4,
491 3,4,5, 2,1,5, 8,2,5,
492 8,2,5, 7,3,5, 3,4,5,
493 2,4,6, 1,1,6, 5,2,6,
494 5,2,6, 8,3,6, 2,4,6,
495 };
496 WAVEFRONT_MODEL_T *model = malloc(sizeof *model);
497 if (model) {
498 WAVEFRONT_MATERIAL_T *mat = model->material;
499 float *temp;
500 const int offset = 0;
501 memset(model, 0, sizeof *model);
502
503 temp = allocbuffer(3*MAX_VERTICES*sizeof *temp);
504 mat->numverts = countof(qf)/3;
505 // vertex, texture, normal
506 deindex(temp, qv, qf+3*offset+0, 3, mat->numverts);
507 create_vbo(GL_ARRAY_BUFFER, mat->vbo+VBO_VERTEX, 3 * mat->numverts * sizeof *qv, temp); // 3
508
509 deindex(temp, qt, qf+3*offset+1, 2, mat->numverts);
510 create_vbo(GL_ARRAY_BUFFER, mat->vbo+VBO_TEXTURE, 2 * mat->numverts * sizeof *qt, temp); // 2
511
512 deindex(temp, qn, qf+3*offset+2, 3, mat->numverts);
513 create_vbo(GL_ARRAY_BUFFER, mat->vbo+VBO_NORMAL, 3 * mat->numverts * sizeof *qn, temp); // 3
514
515 freebuffer(temp);
516 model->num_materials = 1;
517 }
518 return (MODEL_T)model;
519}
520
521
522