Descent3/legacy/editor/Read3ds.cpp

862 lines
20 KiB
C++
Raw Normal View History

/*
* Descent 3
* Copyright (C) 2024 Parallax Software
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
--- HISTORICAL COMMENTS FOLLOW ---
* $Logfile: /DescentIII/Main/editor/Read3ds.cpp $
* $Revision: 1.1.1.1 $
* $Date: 2003-08-26 03:57:38 $
* $Author: kevinb $
*
* Code to read .p3d files generated in 3DS Max
*
* $Log: not supported by cvs2svn $
*
* 26 10/15/99 12:26p Matt
* Added error checking for too many verts & faces when importing a room.
* Also, now allow more than the max number of faces, as long as there is
* a valid number after combining faces.
*
* 25 4/13/99 11:23a Jason
* check for degenerate faces on import
*
* 24 2/25/99 10:35a Jason
* added removal of redundant verts
*
* 23 2/04/99 4:40p Matt
* Added some error checking
*
* 22 1/21/99 11:15p Jeff
* pulled out some structs and defines from header files and moved them
* into seperate header files so that multiplayer dlls don't require major
* game headers, just those new headers. Side effect is a shorter build
* time. Also cleaned up some header file #includes that weren't needed.
* This affected polymodel.h, object.h, player.h, vecmat.h, room.h,
* manage.h and multi.h
*
* 21 12/22/98 2:03p Matt
* Added room names, and made rooms not compress so that room numbers are
* suitable for persistant uses.
*
* 20 5/01/98 5:55p Jason
* made combine faces much stricter
*
* 19 4/02/98 12:23p Jason
* trimmed some fat from our structures
*
* 18 3/31/98 3:49p Jason
* added memory lib
*
* 17 2/02/98 5:14p Matt
* Check for bad normals when importing room
*
* 16 1/22/98 2:56p Brent
* Define default textures to unassigned faces
*
* 15 1/19/98 2:55p Jason
* added the ability to have the importer keep the textures on the faces
* if they are already present in memory
*
* 14 12/23/97 11:06a Samir
* Added pserror.h
*
* 13 12/10/97 5:20p Jason
* set alphas to 1 when importing rooms
*
* 12 12/10/97 4:26p Jason
* don't assign default UVs to room
*
* 11 8/21/97 5:57p Matt
* Use new & modified functions from erooms.cpp
*
* 10 8/01/97 3:16p Chris
*
* 9 7/21/97 12:11p Matt
* Fixed stupid bug in concavity check
*
* 8 7/21/97 11:39a Jason
* checked in for matt to debug
*
* 7 7/17/97 11:13a Jason
* fixed bug with reading in rooms - somehow it got broken
*
* 7 7/17/97 10:51a Jason
* fixed bug with normals
*
* $NoKeywords: $
*/
#include "read3ds.h"
#include "cfile.h"
#include "room.h"
#include "erooms.h"
#include "gametexture.h"
#include "ddio.h"
#include "pserror.h"
#include <string.h>
#include <stdlib.h>
#include "mem.h"
#include "vecmat.h"
// 3ds MAX id's
#define ID_3DS_MODIFIED 0xbeef
#define ID_OBJ_PROPS 0xdead
#define ID_SPLINE_PATH 0x8001
#define ID_3DSM 0xcfd0
#define ID_3DS 0x4d4d
#define ID_OBJECT 0x4000
#define ID_UNKNOWN 0x3d3d
#define ID_TRI_MESH 0x4100
#define ID_VERTLIST 0x4110
#define ID_FACELIST 0x4120
#define ID_MAT_APP 0x4130
#define ID_VERTEX_MAPPING 0x4140
#define ID_SMOOTH 0x4150
#define ID_MATRIX 0x4160
#define ID_MATERIAL 0xafff
#define ID_MAT_NAME 0xa000
#define ID_MAT_DSIDED 0xa081
#define ID_MAT_DIFFUSE_COLOR 0xa020
#define ID_MAT_TEXTURE 0xa200
#define ID_DIRECT_LIGHT 0x4600
#define OBJECT_NODE_TAG 0xB002
char Reading_properties[255];
#define MAX_MATERIALS 100
struct material
{
char name[PAGENAME_LEN];
int texhandle;
};
int Num_materials=0;
material Materials[MAX_MATERIALS];
struct reading_face
{
2024-05-24 03:07:26 +00:00
uint8_t flags; // flags for this face (see above)
int16_t portal_num; // which portal this face is part of, or -1 if none
2024-05-24 03:07:26 +00:00
uint8_t num_verts; // how many vertices in this face
int16_t face_verts[MAX_VERTS_PER_FACE]; // index into list of vertices for this face
roomUVL face_uvls[MAX_VERTS_PER_FACE]; // index into list of uvls for this face
vector normal; // the surface normal of this face
int16_t tmap; // texture numbers for this face
};
struct reading_room {
char name[PAGENAME_LEN];
int flags; // various room flags
int num_faces; // how many poygons in this room
int num_portals; // how many connections in this room
int num_verts; // how many verts in the room
reading_face *faces; // pointer to list of faces
vector *verts; // array of vertices for this room
int objects; // index of first object in this room
float static_light; // the amount of light in this room
};
reading_room Reading_room; // the global that we use to keep a temp copy of the room while
// we're reading it in
int CombineFaces (reading_face *,reading_face *,reading_face *);
// Our nest level
int Nest_level=0;
extern void AssignDefaultUVsToRoom(room *rp);
int DeleteUnusedRoomVerts(room *);
int RemoveDuplicatePoints(room *);
int RemoveDuplicateFacePoints(room *);
#define MAX_READING_ROOM_FACES (MAX_FACES_PER_ROOM * 2)
// Opens and reads a 3dsmax file for our rooms. Allocs a room to carry the data
// Returns the index into the Rooms[] array if successful
// Return -1 on fail
int Read3DSMaxFile(char *filename)
{
uint16_t id;
int len;
CFILE *fp;
int i;
fp=(CFILE *)cfopen (filename,"rb");
if (!fp)
{
2024-05-24 15:46:07 +00:00
mprintf(0,"Couldn't open 3dsmax file %s!\n",filename);
return -1;
}
Nest_level=0;
// Alloc space for reading stuff in
Reading_room.faces=(reading_face *)mem_malloc (MAX_READING_ROOM_FACES*sizeof(reading_face));
Reading_room.verts=(vector *)mem_malloc (MAX_VERTS_PER_ROOM*sizeof(vector));
Reading_room.num_faces=0;
Reading_room.num_verts=0;
id=cf_ReadShort (fp);
len=cf_ReadInt(fp);
Num_materials=0;
if (id == ID_3DS_MODIFIED)
Parse3DSMaxChunk (fp, len-6);
else
{
2024-05-24 15:46:07 +00:00
mprintf(0,"This file is not a 3ds max file!\n");
cfclose (fp);
return -1;
}
cfclose (fp);
if ((Reading_room.num_verts == 0) || (Reading_room.num_faces == 0)) {
OutrageMessageBox("The imported room has %d verts and %d faces. Aborting import.",Reading_room.num_faces,Reading_room.num_faces);
return -1;
}
if (Reading_room.num_verts > MAX_VERTS_PER_ROOM) {
OutrageMessageBox("The imported room has %d verts. The limit is %d. Aborting import.",Reading_room.num_verts,MAX_VERTS_PER_ROOM);
return -1;
}
if (Reading_room.num_faces > MAX_READING_ROOM_FACES) {
OutrageMessageBox("The imported room has %d faces. The limit is %d. Aborting import.",Reading_room.num_faces,MAX_READING_ROOM_FACES);
return -1;
}
if (Reading_room.num_faces > MAX_FACES_PER_ROOM) {
OutrageMessageBox("The imported room has %d faces. The limit after combining faces is %d.\n\nIf there are too many faces after combining, this room will not be imported.",Reading_room.num_faces,MAX_FACES_PER_ROOM);
}
// Convert our points to left handed space
for (i=0;i<Reading_room.num_verts;i++)
ConvertHandiness (&Reading_room.verts[i]);
// calculate normals
int bad_normals = 0;
for (i=0;i<Reading_room.num_faces;i++)
{
reading_room *rp=&Reading_room;
reading_face *mfp=&rp->faces[i];
//vm_GetNormal(&Reading_room.faces[i].normal,&rp->verts[mfp->face_verts[0]],&rp->verts[mfp->face_verts[1]],&rp->verts[mfp->face_verts[2]]);
if (! ComputeNormal(&Reading_room.faces[i].normal,Reading_room.faces[i].num_verts,mfp->face_verts,rp->verts)) {
2024-05-24 15:46:07 +00:00
mprintf(1,"Warning: Low precision normal for face %d\n",i);
bad_normals++;
}
}
if (bad_normals) {
OutrageMessageBox("Warning: The loaded room has %d faces with bad normals -- see the mono screen for details.\n\n"
"Coplanar faces in this room have NOT been combined.\n\n"
"It is STRONGLY recommended that you fix this room before using it.",bad_normals);
goto skip_combine;
}
// Now make a copy of the relevant Reading_room data into our destination
int t;
reading_face destface;
2024-05-24 15:46:07 +00:00
mprintf(0,"Combining faces, please wait...\n");
TryAgain:
for (i=0;i<Reading_room.num_faces;i++)
{
reading_face *a=&Reading_room.faces[i];
for (t=0;t<Reading_room.num_faces;t++)
{
reading_face *b=&Reading_room.faces[t];
if (a==b)
continue;
int retval=CombineFaces (&destface,a,b);
if (retval)
{
// Copy the new face in the place of face a,
// remove face b and then start over!
Reading_room.faces[i]=destface;
int k;
for (k=t;k<Reading_room.num_faces-1;k++)
Reading_room.faces[k]=Reading_room.faces[k+1];
Reading_room.num_faces--;
goto TryAgain;
}
}
}
skip_combine:;
2024-05-24 15:46:07 +00:00
mprintf(0,"Total faces=%d\n",Reading_room.num_faces);
if (Reading_room.num_faces > MAX_FACES_PER_ROOM) {
OutrageMessageBox("The imported room has %d faces. The limit is %d. Aborting import.",Reading_room.num_faces,MAX_FACES_PER_ROOM);
return -1;
}
2024-05-24 15:46:07 +00:00
mprintf(0,"Trying to allocate a room for %d verts, %d faces!\n",Reading_room.num_verts,Reading_room.num_faces);
// int n=AllocRoom (Reading_room.num_verts,Reading_room.num_faces);
room *rp = CreateNewRoom(Reading_room.num_verts,Reading_room.num_faces,1);
if (rp != NULL)
{
for (i=0;i<Reading_room.num_verts;i++)
rp->verts[i]=Reading_room.verts[i];
for (i=0;i<Reading_room.num_faces;i++)
{
InitRoomFace(&rp->faces[i],Reading_room.faces[i].num_verts);
rp->faces[i].normal=Reading_room.faces[i].normal;
rp->faces[i].num_verts=Reading_room.faces[i].num_verts;
rp->faces[i].tmap=Reading_room.faces[i].tmap;
for (t=0;t<Reading_room.faces[i].num_verts;t++)
{
rp->faces[i].face_verts[t]=Reading_room.faces[i].face_verts[t];
rp->faces[i].face_uvls[t]=Reading_room.faces[i].face_uvls[t];
}
}
// Reset UV's
//AssignDefaultUVsToRoom (rp);
// Remove redundant verts
DeleteUnusedRoomVerts(rp);
RemoveDuplicatePoints(rp);
RemoveDuplicateFacePoints(rp);
char name[255];
char path[255];
char extension[255];
char roomname[255];
// Find a unique name for this room
ddio_SplitPath (filename,path,name,extension);
int done=0;
int count=1;
sprintf (roomname,"%s.ORF",name);
while (!done)
{
int val=FindRoomName (roomname);
if (val==-1)
{
done=1;
continue;
}
count++;
sprintf (roomname,"%s%d.ORF",name,count);
}
ASSERT(rp->name == NULL);
rp->name = (char *) mem_malloc(strlen(roomname)+1);
strcpy (rp->name,roomname);
// Save it out to disk (locally)
ddio_MakePath (name,LocalRoomsDir,roomname,NULL);
SaveRoom (ROOMNUM(rp),name);
}
// Free our verts
if (Reading_room.faces)
mem_free (Reading_room.faces);
if (Reading_room.verts)
mem_free (Reading_room.verts);
return ROOMNUM(rp);
}
// Converts the 3dsmax coordinate space into our left-handed coordinate space
void ConvertHandiness( vector * v )
{
vector v1 = *v;
v->x = -1.0f*v1.x;
v->y = v1.z;
v->z = -1.0f*v1.y;
}
#define skip(f,n) cfseek(f,n,SEEK_CUR)
// Parses a chunk of a 3dsmax file - this function calls itself
void Parse3DSMaxChunk (CFILE *fp, int size)
{
uint16_t id;
int len;
int level=Nest_level;
int i;
float scale_factor = 0.0254f; // 0.0254 inches/meter
Nest_level++;
while (size && !cfeof(fp))
{
id=cf_ReadShort(fp);
len=cf_ReadInt(fp);
if (size<0)
{
2024-05-24 15:46:07 +00:00
mprintf(0,"%d:chunk error\n",level);
exit (1);
}
switch (id)
{
case ID_UNKNOWN:
Parse3DSMaxChunk(fp,len-6);
break;
case ID_MAT_NAME:
{
char material_name[PAGENAME_LEN];
cf_ReadString(material_name,PAGENAME_LEN,fp);
strcpy (Materials[Num_materials].name,material_name);
Num_materials++;
ASSERT (Num_materials<MAX_MATERIALS);
break;
}
case ID_MATERIAL:
{
Parse3DSMaxChunk(fp,len-6);
break;
}
case ID_MAT_TEXTURE:
{
int i;
char texture_name [PAGENAME_LEN];
// Read in Unknown field
for (i=0; i<6; i++ )
cf_ReadByte (fp);
cf_ReadShort(fp);
// Read in Unknown field
for (i=0; i<6; i++ )
cf_ReadByte (fp);
cf_ReadString(texture_name,PAGENAME_LEN,fp);
// Find the texture that has this bitmap as a name
int texlen=strlen(texture_name);
texture_name[texlen-4]=0;
strcat( texture_name, ".OGF" );
int ret=FindTextureBitmapName(texture_name);
if (ret==-1)
{
2024-05-24 15:46:07 +00:00
mprintf(0,"Couldn't find bitmap %s!\n",texture_name);
ret=GetNextTexture(0);
}
Materials[Num_materials-1].texhandle=ret;
skip(fp,len-6-6-2-6-(strlen(texture_name)+1));
break;
}
case ID_MAT_APP:
{
char material_name[PAGENAME_LEN];
int16_t n_faces,face;
int done=0;
int texnum;
cf_ReadString(material_name,PAGENAME_LEN,fp);
n_faces=cf_ReadShort (fp);
for (int i=0;i<Num_materials && !done;i++)
{
if (!stricmp (Materials[i].name,material_name))
{
texnum=Materials[i].texhandle;
done=1;
}
}
if (!done)
{
2024-05-24 15:46:07 +00:00
mprintf(0,"Couldn't find material named %s!\n",material_name);
texnum=GetNextTexture(0);
}
while (n_faces--)
{
face=cf_ReadShort(fp);
Reading_room.faces[face].tmap=texnum;
}
break;
}
case ID_OBJECT:
{
cf_ReadString (Reading_room.name,PAGENAME_LEN,fp);
Parse3DSMaxChunk(fp,len-6-(strlen(Reading_room.name)+1));
break;
}
// Special properties for this object/room
case ID_OBJ_PROPS:
{
cf_ReadString (Reading_properties,255,fp);
// Skip next two ints
cf_ReadInt(fp);
cf_ReadInt(fp);
Parse3DSMaxChunk(fp, len-6-4-4-(strlen(Reading_properties)+1));
break;
}
case ID_TRI_MESH:
2024-05-24 15:46:07 +00:00
mprintf(0,"Found 3dsmax TRI_MESH chunk!\n");
Parse3DSMaxChunk(fp,len-6);
break;
// Vertex list
case ID_VERTLIST:
{
uint16_t num_verts=cf_ReadShort(fp);
int i;
if (num_verts > MAX_VERTS_PER_ROOM)
return;
2024-05-24 15:46:07 +00:00
mprintf(0,"Reading in %d verts from room!\n",num_verts);
// Make room for these verts
Reading_room.num_verts=num_verts;
for (i=0;i<num_verts;i++)
{
Reading_room.verts[i].x=cf_ReadFloat(fp);
Reading_room.verts[i].y=cf_ReadFloat(fp);
Reading_room.verts[i].z=cf_ReadFloat(fp);
Reading_room.verts[i]*=scale_factor;
}
break;
}
case ID_FACELIST:
{
uint16_t num_faces=cf_ReadShort(fp);
uint16_t a,b,c,flags;
int i,t, j, this_size;
if (num_faces > MAX_READING_ROOM_FACES)
return;
2024-05-24 15:46:07 +00:00
mprintf(0,"Reading in %d faces!\n",num_faces);
Reading_room.num_faces=num_faces;
ASSERT (Reading_room.faces!=NULL);
for (i=0;i<num_faces;i++)
{
// a,b, and c are indices into the list of vertices for this room
Reading_room.faces[i].num_verts=3;
a = cf_ReadShort(fp);
b = cf_ReadShort(fp);
c = cf_ReadShort(fp);
flags = cf_ReadShort(fp);
// Set our pointers accordingly
// We must reverse the ordering of the verts because
// 3ds has a right handed coordinate system
Reading_room.faces[i].face_verts[0]=c;
Reading_room.faces[i].face_verts[1]=b;
Reading_room.faces[i].face_verts[2]=a;
Reading_room.faces[i].tmap=GetNextTexture(0);
Reading_room.faces[i].flags=flags;
// Read UVs
float u[3],v[3];
for (t=0; t<3; t++ )
{
u[t]=cf_ReadFloat (fp);
v[t]=cf_ReadFloat (fp);
}
for (j = 0; j < 3; j++)
{
Reading_room.faces[i].face_uvls[j].u = u[2-j];
Reading_room.faces[i].face_uvls[j].v = -v[2-j];
Reading_room.faces[i].face_uvls[j].alpha = 255;
}
}
//get whatever is left in this segment
this_size = num_faces * ((4 * sizeof(int16_t)) + (6*sizeof(float)));
if (len-6-this_size )
Parse3DSMaxChunk(fp,len-6-2-this_size);
break;
}
case ID_MATRIX:
{
float room_matrix[12];
int16_t i;
for (i=0; i<12; i++ )
room_matrix[i] = cf_ReadFloat(fp);
break;
}
case OBJECT_NODE_TAG:
{
//printf( "=================== OBJECT_NODE_TAG! =======================\n" );
Parse3DSMaxChunk(fp,len-6);
break;
}
default:
// Skip this stuff
for (i=0;i<len-6;i++)
cf_ReadByte (fp);
break;
}
size -= len;
}
Nest_level--;
}
int next_vertex( reading_face *f, int v )
{
return f->face_verts[(v+1)%f->num_verts];
}
int this_vertex( reading_face *f, int v )
{
return f->face_verts[v];
}
int compute_faces_mapping( reading_face * f, vector *out_norm, float *out_d )
{
int i;
float ut, vt, len;
vector point, u, v;
vector normal;
vm_MakeZero(&point );
vm_MakeZero( &normal );
ut = vt = 0.0f;
for (i=0; i<f->num_verts; i++ )
{
u.x = f->face_uvls[i].u;
u.y = f->face_uvls[i].v;
u.z = 1.0f;
v.x = f->face_uvls[(i+1) % f->num_verts].u;
v.y = f->face_uvls[(i+1) % f->num_verts].v;
v.z = 1.0f;
ut += (float)fabs( u.x - v.x );
vt += (float)fabs( u.y - v.y );
normal.x += (u.y-v.y) * (u.z + v.z);
normal.y += (u.z-v.z) * (u.x + v.x);
normal.z += (u.x-v.x) * (u.y + v.y);
point+=u;
}
len = vm_GetMagnitude( &normal );
out_norm->x = normal.x / len;
out_norm->y = normal.y / len;
out_norm->z = normal.z / len;
len *= f->num_verts;
*out_d = (vm_DotProduct( &point, &normal ) / len);
point.x /= f->num_verts;
point.y /= f->num_verts;
point.z /= f->num_verts;
return 0;
}
// Returns 1 if the uv's match
int uvs_match( reading_face * a, int va, reading_face * b, int vb )
{
int f1, f2, flag;
vector n1, n2;
float d1, d2;
float cosTheta;
float u_err, v_err;
u_err = (float)fabs( (a->face_uvls[va].u-b->face_uvls[vb].u) / ((a->face_uvls[va].u+b->face_uvls[vb].u)/2.0f) );
v_err = (float)fabs( (a->face_uvls[va].v-b->face_uvls[vb].v) / ((a->face_uvls[va].v+b->face_uvls[vb].v)/2.0f) );
if ( u_err+v_err > 0.00001f ) return 0;
f1 = compute_faces_mapping( a, &n1, &d1 );
f2 = compute_faces_mapping( b, &n2, &d2 );
if ( f1 || f2 ) return 0;
cosTheta = vm_DotProduct( &n1, &n2 );
if ( (cosTheta < 0.99f) || (fabs( d2-d1 ) > 0.0001f) ) {
flag = 0;
} else {
flag = 1;
}
return flag;
}
#define MAX_POINT_DISTANCE_FROM_PLANE .1
int CombineFaces( reading_face *dest,reading_face * a, reading_face * b )
{
int starta, startb, i;
int va;
if (! NormalsAreSame(&b->normal,&a->normal))
return 0;
if (a->tmap!=b->tmap)
return 0;
ASSERT (a->num_verts > 2 );
ASSERT (b->num_verts > 2 );
// Compare points to plane
vector vec=Reading_room.verts[a->face_verts[0]];
vector norm=a->normal;
float plane_dist=-(vec.x*norm.x+vec.y*norm.y+vec.z*norm.z);
for (i=0;i<b->num_verts;i++)
{
vec=Reading_room.verts[b->face_verts[i]];
float dist = vec.x*norm.x+vec.y*norm.y+vec.z*norm.z+plane_dist;
if (fabs(dist)>MAX_POINT_DISTANCE_FROM_PLANE)
return 0;
}
// Go through each vertex and get a match
for (starta=0; starta<a->num_verts; starta++ )
{
for (startb=0; startb<b->num_verts; startb++ )
{
if ( (this_vertex(a,starta)==next_vertex(b,startb)) && (next_vertex(a,starta)==this_vertex(b,startb)) &&
uvs_match(a,starta,b,(startb+1)%b->num_verts) && uvs_match(a,(starta+1)%a->num_verts,b,startb) )
{
//MATCH!!!!!!!!
dest->num_verts = 0;
dest->flags = a->flags;
dest->normal = a->normal; //normal of this face
dest->tmap = a->tmap;
for (i=1; i<a->num_verts; i++ )
{
ASSERT(dest->num_verts < MAX_VERTS_PER_FACE);
dest->face_verts[dest->num_verts] = a->face_verts[(starta+i)%a->num_verts];
dest->face_uvls[dest->num_verts] = a->face_uvls[(starta+i)%a->num_verts];
va = dest->face_verts[dest->num_verts];
dest->num_verts++;
}
if ( (va==b->face_verts[(startb+2)%b->num_verts]))
2024-05-24 15:46:07 +00:00
mprintf(0, "WARNING!!! Faces were combined that caused the loss of a vertex!\n");
for (i=1; i<b->num_verts; i++ )
{
ASSERT(dest->num_verts < MAX_VERTS_PER_FACE );
if ( (i==1) && (va==b->face_verts[(startb+i+1)%b->num_verts]))
continue;
else if ( (i==2) && (va==b->face_verts[(startb+i)%b->num_verts]))
continue;
else
{
dest->face_verts[dest->num_verts] = b->face_verts[(startb+i)%b->num_verts];
dest->face_uvls[dest->num_verts] = b->face_uvls[(startb+i)%b->num_verts];
dest->num_verts++;
}
}
ASSERT( dest->num_verts > 2 );
if ((CheckFaceConcavity (dest->num_verts,dest->face_verts,&dest->normal,Reading_room.verts))>=0)
return 0;
// Now check for degenerate face
for (int v=0;v<dest->num_verts;v++)
{
if (dest->face_verts[v] == dest->face_verts[(v+2)%dest->num_verts])
{
return 0;
}
}
return 1;
}
}
}
return 0;
}