#ifdef USE_MODEL /* * Read and display 3-D models from .obj/.ma files * (wavefront .obj geometry, Maya 2.x/3.x .ma Maya ASCII material properties). * * Stuart Levy, slevy@ncsa.uiuc.edu * National Center for Supercomputing Applications, * University of Illinois 2001. * This file is part of partiview, released under the * Illinois Open Source License; see the file LICENSE.partiview for details. */ #include <stdio.h> #include <stdlib.h> #include <string.h> #include <time.h> #include <sys/stat.h> #ifdef WIN32 # include "winjunk.h" #else # include <unistd.h> #endif #include <ctype.h> #undef isspace #ifdef __APPLE__ #include <OpenGL/gl.h> #else #include <GL/gl.h> /* for GLuint */ #endif #include "cat_model.h" #include "findfile.h" #include "partiviewc.h" #include "specks.h" /* just for tokenize() -- ugh */ /* Instantiate the templates we'll need */ #include "cat_modelutil.cc" template class vvec<int>; template class vvec<Point>; void CAT_model_preinit() { Model::preinit(); } /* * linked list of all Models. * We don't expect there'll be a huge number of these. */ Model **Model::allModels = NULL; Model **Model::trashModels = NULL; char **Model::pathdirs = NULL; void Model::preinit() { allModels = NewN( Model *, 1 ); *allModels = NULL; trashModels = NewN( Model *, 1 ); *trashModels = NULL; } Model::Model() { init(); } void Model::init() { name = fname = NULL; next = NULL; defined = 0; } void Model::add() { Model *alias, **aliasp; if(this->name == NULL) { msg("Model::add(): Can't add a nameless Model!"); return; } for(aliasp = allModels; (alias = *aliasp) != NULL; aliasp = &alias->next) { if(!strcmp(this->name, alias->name)) { /* We already have a model named that. */ /* Remove it from main list, and add to our "trash" list. */ Model *succ = alias->next; *aliasp = succ; /* Add it to our "trash" list. */ alias->discard(); break; } } this->next = *allModels; *allModels = this; } void Model::discard() { this->next = *trashModels; *trashModels = this; this->unused = 0; /* set purge clock */ } void Model::purge() { Model *m, **mp = trashModels; for(mp = trashModels; (m = *mp) != NULL; ) { if(m->unused > 2) { *mp = m->next; delete m; } else { m->unused++; mp = &m->next; } } } /* purge the model, but leave it in the list */ Model::~Model() { name = fname = NULL; /* XXX leak memory */ defined = 0; } Model *Model::find( const char *name ) { Model *m; if(name == NULL) return NULL; int len = strlen(name); Model *maybe = NULL; for(m = *allModels; m != NULL; m = m->next) { if(m->name != NULL) { if(!strcmp(m->name, name)) break; if(!memcmp(m->name, name, len) && m->name[len] == ':') maybe = m; } } return m ? m : maybe; } time_t fmodtime(const char *fname) { struct stat st; if(stat(fname, &st) < 0) return 0; return st.st_mtime; } /* * Do we have an up-to-date copy of "file"? * Returns 1 if yes, 0 if not seen, -1 if out-of-date. */ enum Model::fstatus Model::seenFile( const char *fname ) { Model *m; if(fname == NULL) return NONESUCH; time_t mtime = fmodtime(fname); if(mtime == 0) return NONESUCH; for(m = *allModels; m != NULL; m = m->next) { if(m->fname && strcmp(m->fname, fname) == 0) { return m->fmtime < mtime ? OUTDATED : READIT; } } return UNREAD; } /* * .obj file reader */ Appearance::Appearance() { init(); } void Appearance::init() { memset(this, 0, sizeof(*this)); defined = 0; use = NULL; txfname = NULL; name = NULL; txloaded = 0; hookfunc = NULL; hookdata = NULL; } Appearance::~Appearance() { if(txfname) Free(txfname); if(txdir) free((void *)txdir); if(name) Free(name); // if(txloaded) IMAGEbuff::schedule_expunge(txim); } void Appearance::add( Appearance **list ) { if(list) { next = *list; *list = this; } } Appearance *Appearance::find( Appearance **list, const char *name ) { Appearance *ap; if(list == NULL || name == NULL) return NULL; for(ap = *list; ap != NULL; ap = ap->next) if(!strcmp(ap->name, name)) break; return ap; } Appearance *Appearance::findCreate( Appearance **list, const char *name ) { Appearance *ap; if(list == NULL || name == NULL) return NULL; if((ap = find(list, name)) != NULL) return ap; ap = new Appearance; ap->name = shmstrdup(name); ap->defined = 0; ap->add( list ); return ap; } int Appearance::HookTextures( TextureHookFunc hook, void *data, const char *txnametag ) { int any = 0; if(txnametag == NULL || (this->txfname != NULL && strstr(this->txfname, txnametag) != NULL) ) { this->hookfunc = hook; this->hookdata = data; any = 1; } if(this->use != NULL && this->use != this && (any || use->HookTextures(hook,data,txnametag))) { this->use->hookfunc = hook; this->use->hookdata = data; any = 1; } return any; } WavObj::WavObj() { init(); } void WavObj::init() { pt.init(); tx.init(); norm.init(); scene = NULL; faces = NULL; } // If you say // wavobj->HookTextures( NULL, NULL, "" ) // it will remove any hooks on any textures in that model. int WavObj::HookTextures( TextureHookFunc hook, void *data, const char *txnametag ) { int total = 0; if(this->scene == NULL) return 0; for(Appearance *ap = this->scene->aps; ap != NULL; ap = ap->next) total += ap->HookTextures( hook, data, txnametag ); return total; } WavObj::~WavObj() { } WavObj *WavObj::create() { WavObj *obj = NewN( WavObj, 1 ); obj->init(); return obj; } WavFaces *WavFaces::create() { WavFaces *wf = NewN( WavFaces, 1 ); wf->init(); return wf; } void WavFaces::init() { nfv.init(); fv.init(); ap = NULL; obj = NULL; next = NULL; } int agetfloats( float *v, int max, int a0, int ac, char **av ) { int i; char *cp; float tv; for(i = 0; i < max && a0+i < ac; i++) { tv = strtod(av[a0+i], &cp); if(*cp != '\0') break; v[i] = tv; } return i; } WavObj *WavObj::addObj( const char *name, const char *group ) { WavObj *obj; char fullname[256]; obj = new WavObj(); *obj = *this; sprintf(fullname, group ? "%.127s:%.127s" : "%.127s", name, group); obj->name = shmstrdup(fullname); obj->pt.trim(0); obj->tx.trim(0); obj->norm.trim(0); obj->add(); /* Add new object to public list */ for(WavFaces *wf = obj->faces; wf != NULL; wf = wf->next) { wf->nfv.trim(0); wf->fv.trim(0); wf->obj = obj; /* * Check that we found all the material-types we need. * Also load any textures that we're actually using. */ if(!wf->ap) { msg("Warning: %s: no material for group of %d faces?", fname, wf->nfv.count); } else if(!wf->ap->defined) { msg("Warning: %s: found no def'n for material %s", fname, wf->ap->name ); wf->ap->defined = 1; wf->ap->lighted = 0; wf->ap->Kd = 1; wf->ap->Cs[0] = .3; /* puke green! */ wf->ap->Cs[1] = .5; wf->ap->Cs[2] = .15; } else { wf->ap->loadTextures(); } } return obj; } int WavObj::readFile( const char *name, const char *fname, const char *scenename, int complain ) { FILE *inf = fopen(fname, "r"); char line[1024], tline[1024], *s; int ac; char *av[32]; Appearance *ap = NULL; int k; int lno = 0; const char *err = NULL; char errbuf[80]; const char *group = NULL; WavFaces *curfaces = NULL; Point tpt[4000], ttx[4000], tnorm[4000]; int tnfv[250], tfv[1000]; int ok = 1; Point scaleby; WavObj me; if(inf == NULL) { if(complain) msg( "Couldn't open model file %s", fname ); return 0; } me.fname = shmstrdup(fname); me.scene = new MayaScene; me.scene->fname = shmstrdup(scenename); me.scene->aps = NULL; /* * Inhale corresponding ".scene" file to read all those appearances */ me.scene->readScene( scenename, complain ); /* * Use auto vars for default space. */ me.pt.use( tpt, COUNT(tpt) ); me.tx.use( ttx, COUNT(ttx) ); me.norm.use( tnorm, COUNT(tnorm) ); me.pt.count = me.tx.count = me.norm.count = 0; scaleby.x[0] = scaleby.x[1] = scaleby.x[2] = 1; while(fgets(line, sizeof(line), inf) != NULL) { lno++; ac = tokenize( line, tline, COUNT(av), av, NULL ); if(ac <= 0) continue; s = av[0]; if(!strcmp(s, "v")) { Point *cv = me.pt.append(); if(agetfloats( &cv->x[0], 3, 1, ac, av ) < 3) err = "v: expected 3 floats"; cv->x[0] *= scaleby.x[0]; cv->x[1] *= scaleby.x[1]; cv->x[2] *= scaleby.x[2]; } else if(!strcmp(s, "vt")) { if(agetfloats( &me.tx.append()->x[0], 2, 1, ac, av ) < 2) err = "vt: expected 2 floats"; } else if(!strcmp(s, "vn")) { if(agetfloats( &me.norm.append()->x[0], 3, 1, ac, av ) < 3) err = "vn: expected 3 floats"; } else if(!strcmp(s, "s")) { /* Not sure what this new "s" directive means. * Could it be related to smoothing? * Anyway, let's ignore it. */ } else if(!strcmp(s, "scale")) { int ns = agetfloats( &scaleby.x[0], 3, 1, ac, av ); switch(ns) { case 1: scaleby.x[1] = scaleby.x[2] = scaleby.x[0]; break; case 3: break; default: err = "scale: expected 1 or 3 floats"; break; } } else if(!strcmp(s, "g")) { /* ignore "g" directives. Hmm, wonder if they're * related to Maya layers? */ } else if(!strcmp(s, "group")) { if(me.pt.count > 0) { me.addObj( name, group ); me.faces = NULL; me.pt.count = me.tx.count = me.norm.count = 0; me.pt.use( tpt, COUNT(tpt) ); me.tx.use( ttx, COUNT(ttx) ); me.norm.use( tnorm, COUNT(tnorm) ); } if(group != NULL) Free(group); group = shmstrdup(av[1]); curfaces = NULL; } else if(!strcmp(s, "usemtl")) { if(ac < 2) err = "usemtl: expected material name"; else { Appearance *nap = NULL; for(k = ac; --k > 0; ) { nap = Appearance::find( &me.scene->aps, av[k] ); if(nap != NULL && nap->defined) break; } if(nap == NULL || !nap->defined) { err = "usemtl: no known appearances listed"; } else if(nap->defined < 0) { err = "usemtl: no layered shaders; try multilister's Convert Solid Texture"; nap->defined = 1; /* emit above message only once */ } else if(ap != nap) { if(curfaces) { curfaces->nfv.trim( curfaces->nfv.room ); curfaces->fv.trim( curfaces->fv.room ); } /* Do we already have some faces with this appearance? * Add more to the same group. */ for(curfaces = me.faces; curfaces != NULL; curfaces = curfaces->next) { if(curfaces->ap == nap) break; } /* If not, curfaces is now NULL, so we'll create a new * clump of faces when needed. */ ap = nap; } } } else if(!strcmp(s, "f")) { if(curfaces == NULL) { curfaces = WavFaces::create(); curfaces->obj = NULL; curfaces->ap = ap; curfaces->next = me.faces; me.faces = curfaces; curfaces->nfv.use( tnfv, COUNT(tnfv) ); curfaces->fv.use( tfv, COUNT(tfv) ); } /* How many verts on this face? One per arg after the "f" */ int mynfv = (ac - 1); /* face description in fv[] begins at current pos'n */ int fv0 = curfaces->fv.count; /* preallocate enough space in fv[], and adjust count. */ curfaces->fv.needs( fv0 + mynfv*3 ); curfaces->fv.count = fv0 + mynfv*3; /* each face takes two entries in nfv[]: */ *curfaces->nfv.append() = mynfv; /* count */ *curfaces->nfv.append() = fv0; /* fv[] offset */ /* and 3*N entries in fv[]: */ int *fvp = curfaces->fv.val(fv0); for(k = 0; k < mynfv; k++, fvp += 3) { /* Parse the NN/NN/NN vertex description */ char *cp = av[k+1]; fvp[0] = strtol(cp, &cp, 0) - 1; if(*cp == '/') cp++; fvp[1] = strtol(cp, &cp, 0) - 1; if(*cp == '/') cp++; fvp[2] = strtol(cp, &cp, 0) - 1; if(*cp != '\0') { err = "f: expected series of NN/NN/NN (or just NN) fields for each vertex"; } else { if(fvp[0] >= me.pt.count) { sprintf(errbuf, "There's no vertex number %d! Only have %d", fvp[0]+1, me.pt.count); err = errbuf; ok = 0; } else if(fvp[1] >= me.tx.count) { sprintf(errbuf, "There's no texture-vertex number %d! Only have %d", fvp[1]+1, me.tx.count); err = errbuf; ok = 0; } else if(fvp[2] >= me.norm.count) { sprintf(errbuf, "There's no surface-normal number %d! Only have %d", fvp[2]+1, me.norm.count); err = errbuf; ok = 0; } } } } else { err = "Surprise tag: expected \"f\" or \"v\" or \"vt\" or \"vn\" or \"usemtl\""; } if(err != NULL) { msg("Reading .obj file %s line %d: %s", me.fname, lno, err); msg(" %s", line); // ok = 0; // Let's make all errors non-fatal for now. err = NULL; } } fclose(inf); if(ok) { me.fmtime = fmodtime(me.fname); me.defined = 1; me.addObj( name, group ); } return ok; } MayaScene::MayaScene() { aps = NULL; partials = NULL; fname = NULL; } #define LINESIZE 1024 const char *MayaScene::parseFileNode( const char *name, FILE *f, char *reline, int *lno ) { char line[LINESIZE+1], tline[LINESIZE+1]; char *av[8]; Appearance *ap = Appearance::findCreate( &this->partials, name ); while(fgets(line, LINESIZE, f) != NULL) { if(!isspace(line[0])) { /* Must not be our kind of "file" node -- rescan */ // delete ap; Not if we add with findCreate()! strcpy(reline, line); return NULL; /* we don't consider this an error */ } ++*lno; int ac = tokenize( line, tline, COUNT(av), av, NULL ); if(ac <= 4 || strcmp(av[0], "setAttr") != 0) return "createNode file: expected setAttr"; if(strcmp(av[1], ".ftn") != 0 || strcmp(av[2], "-type") != 0) continue; ap->txfname = shmstrdup( av[4] ); if(this->fname != NULL) { const char *tail = strrchr( this->fname, '/' ); if(tail) { int len = tail - this->fname + 1; ap->txdir = static_cast<char *>(malloc( len+1 )); memcpy( ap->txdir, this->fname, len ); ap->txdir[len] = '\0'; } else { ap->txdir = strdup("./"); } } return NULL; } // delete ap; Not if we add with findCreate()! reline[0] = '\0'; return "Surprise EOF"; } const char *MayaScene::parseMatNode( const char *name, FILE *f, char *reline, int *lno, const char *kind ) { char line[LINESIZE+1], tline[LINESIZE+1]; char *av[16]; Appearance *ap = Appearance::findCreate( &this->partials, name ); ap->defined = 1; /* * Fill in all our defaults */ ap->shiny = (strcmp(kind, "lambert") != 0); ap->lighted = 1; ap->smooth = 0; ap->textured = 0; ap->Cs[0] = ap->Cs[1] = ap->Cs[2] = 0.5; ap->Ca[0] = ap->Ca[1] = ap->Ca[2] = 0; // ap->Ka = 0; ap->Kd = .8; // ap->Ks = .5; ap->Cspec[0] = ap->Cspec[1] = ap->Cspec[2] = .5; ap->shininess = 20.; while(fgets(line, LINESIZE, f) != NULL) { if(!isspace(line[0])) { /* End of material node */ strcpy(reline, line); return NULL; /* we don't consider this an error */ } ++*lno; int ac = tokenize( line, tline, COUNT(av), av, NULL ); if(ac <= 2 || strcmp(av[0], "setAttr") != 0) return "material node: expected setAttr"; if(!strcmp(av[1], ".dc")) ap->Kd = atof(av[2]); else if(!strcmp(av[1], ".sro")) ap->shininess = 1 / (1 - atof(av[2])); else if(!strcmp(av[1], ".cp")) ap->shininess = atof(av[2]); else if(ac >= 7 && !strcmp(av[2],"-type") && !strcmp(av[3],"float3")) { if(!strcmp(av[1], ".c")) agetfloats( &ap->Cs[0], 3, 4, ac, av ); if(!strcmp(av[1], ".ambc")) agetfloats( &ap->Ca[0], 3, 4, ac, av ); else if(!strcmp(av[1], ".sc")) agetfloats( &ap->Cspec[0], 3, 4, ac, av ); } } reline[0] = '\0'; return NULL; } const char *MayaScene::parseScrapNode( const char *name, FILE *f, char *reline, int *lno ) { Appearance *ap = Appearance::findCreate( &this->partials, name ); fgets(reline, sizeof(reline), f); ap->defined = -1; return NULL; } const char *MayaScene::parseEngineNode( const char *name, FILE *f, char *reline, int *lno ) { Appearance::findCreate( &this->aps, name ); /* just ensure node on list! */ fgets(reline, sizeof(reline), f); return NULL; } /* * if str ends with suffix suf, return index in str where suf begins, * else -1. */ int endsWith( const char *str, const char *suf ) { if(str == NULL) return 0; int at = strlen(str) - strlen(suf); if(at < 0) return 0; return strcmp( str+at, suf ) == 0 ? at : -1; } const char *MayaScene::parseConnect( char *from, char *to ) { int efrom, eto; Appearance *afrom, *ato; if((efrom = endsWith( from, ".oc" ))>0 && (eto = endsWith( to, ".ss" ))>0) { /* material -> engine */ from[efrom] = '\0'; to[eto] = '\0'; /* Expect to find "from" on the "partials" list (as a lamb/blinn/phong node) * and "to" on the "aps" list (as a final shadingEngine node). */ afrom = Appearance::find( &this->partials, from ); ato = Appearance::find( &this->aps, to ); if(afrom && ato) { ato->use = afrom; ato->defined = 1; } } else if((efrom = endsWith( from, ".oc" ))>0 && (eto = endsWith( to, ".c" ))>0) { /* texture-file -> material */ from[efrom] = '\0'; to[eto] = '\0'; afrom = Appearance::find( &this->partials, from ); ato = Appearance::find( &this->partials, to ); if(afrom && afrom->txfname && ato) ato->use = afrom; } return NULL; } int MayaScene::readScene( const char *scenefname, int complain ) { char line[LINESIZE+1], tline[LINESIZE+1]; int ac; char *av[32]; int lno = 0; int ok = 1; int rescan = 0; const char *err = NULL; if(scenefname == NULL) return 0; FILE *f = fopen(scenefname, "r"); if(f == NULL) { msg( "Can't open Maya scene file %s", scenefname); return 0; } while(rescan || fgets(line, sizeof(line), f) != NULL) { rescan = 0; lno++; if(strcmp(line, "FOR4") == 0) { msg( "Hmm, is %s a \"Maya binary\" file? Needs to be saved as \"Maya ASCII\".", scenefname); } else if(strncmp(line, "createNode", 10) == 0) { ac = tokenize( line, tline, COUNT(av), av, NULL ); if(ac <= 1) continue; const char *kind = av[1]; const char *name = ""; int i; for(i = 2; i < ac-1; i++) { if(!strcmp(av[i], "-n")) { name = av[i+1]; break; } } if(!strcmp(kind, "file")) err = parseFileNode(name, f, line, &lno); else if(!strcmp(kind, "lambert") || !strcmp(kind, "blinn") || !strcmp(kind, "phong")) err = parseMatNode(name, f, line, &lno, kind); else if(!strcmp(kind, "shadingEngine")) err = parseEngineNode(name, f, line, &lno); else if(!strcmp(kind, "layeredShader")) err = parseScrapNode(name, f, line, &lno); else continue; /* Ignore other createNode types */ rescan = 1; } else if(strncmp(line, "connectAttr", 11) == 0) { ac = tokenize( line, tline, COUNT(av), av, NULL ); if(ac >= 3) err = parseConnect(av[1], av[2]); continue; } else continue; /* Ignore other stuff in file */ if(err) { msg("Reading Maya file %s line %d: %s", fname, lno, err); msg(" %s", line); ok = 0; break; } } fclose(f); if(ok) { /* Tie partial information into top-level Appearance's */ Appearance *ap, *mat; for(ap = this->aps; ap != NULL; ap = ap->next) { if((mat = ap->use) != NULL && mat->defined) { ap->defined = 1; if(mat->use && mat->use->txfname) { mat->txfname = mat->use->txfname; if(mat->use->txdir) mat->txdir = mat->use->txdir; } } } } return ok; } void Appearance::loadTextures() { if(txfname != NULL && txloaded == 0) { const char *cp = strrchr(txfname, '/'); if(cp == NULL) cp = txfname; else cp++; char *fullname = findfile( txdir, cp ); if(fullname == NULL) { msg( "Can't find texture image %s", cp); txloaded = -1; /* Failed */ return; } txim = txmake( fullname, TXF_DECAL, TXF_SCLAMP|TXF_TCLAMP, 7 ); if(txload(txim) <= 0) { msg( "Can't load texture (must be SGI image) %s", fullname); txloaded = -1; /* Failed */ return; } txloaded = 1; textured = 1; } else if(this->use != NULL) { use->loadTextures(); } } void WavObj::render() { WavFaces *wf; int i; int *fvp, *nfvp; int token = 0; int prevtoken = 0; glMatrixMode(GL_TEXTURE); glPushMatrix(); glMatrixMode(GL_MODELVIEW); for(wf = faces; wf != NULL; wf = wf->next) { if(wf->ap) wf->ap->applyOGL(wf); nfvp = wf->nfv.v; for(i = 0, nfvp = wf->nfv.v; i < wf->nfv.count; i += 2, nfvp += 2) { int nverts = nfvp[0]; int fvbase = nfvp[1]; switch(nfvp[0]) { case 1: token = GL_POINTS; break; case 2: token = GL_LINES; break; case 3: token = GL_TRIANGLES; break; case 4: token = GL_QUADS; break; default: token = GL_POLYGON; if(prevtoken != 0) prevtoken = -1; break; } if(token != prevtoken) { if(prevtoken != 0) glEnd(); glBegin( token ); } for(fvp = &wf->fv.v[ fvbase ]; --nverts >= 0; fvp += 3) { if(fvp[2]>=0) glNormal3fv( norm.v[ fvp[2] ].x ); if(fvp[1]>=0) glTexCoord2fv( tx.v[ fvp[1] ].x ); glVertex3fv( pt.v[ fvp[0] ].x ); } prevtoken = token; } if(prevtoken != 0) { glEnd(); prevtoken = 0; } } txbind(NULL, NULL); glMatrixMode(GL_TEXTURE); glPopMatrix(); glMatrixMode(GL_MODELVIEW); } void Appearance::applyOGL( WavFaces *wf ) { static GLfloat zero[3] = {0,0,0}; GLfloat cd[3]; if(!defined) return; Appearance *ap = this; if(this->use) ap = this->use; glDisable(GL_LIGHTING); glDisable(GL_COLOR_MATERIAL); if(!ap->textured) { // If this node is flagged as having no texture, // just apply all our other material properties (color, shininess, ...) glMaterialf( GL_FRONT_AND_BACK, GL_SHININESS, ap->shiny ? ap->shininess : 10 ); glMaterialfv( GL_FRONT_AND_BACK, GL_AMBIENT, ap->Ca ); glMaterialfv( GL_FRONT_AND_BACK, GL_SPECULAR, ap->shiny ? ap->Cspec : zero ); cd[0] = ap->Kd * ap->Cs[0]; cd[1] = ap->Kd * ap->Cs[1]; cd[2] = ap->Kd * ap->Cs[2]; if(ap->lighted) glMaterialfv( GL_FRONT_AND_BACK, GL_DIFFUSE, cd ); else glColor3fv( cd ); } else { // We have a texture, or at least a pointer to an image file. // Select white material with appropriate shininess. static GLfloat white[] = {1,1,1,1}; glMaterialf( GL_FRONT_AND_BACK, GL_SHININESS, ap->shiny ? 10 : 10 ); glMaterialfv( GL_FRONT_AND_BACK, GL_AMBIENT_AND_DIFFUSE, &white[0] ); glMaterialfv( GL_FRONT_AND_BACK, GL_SPECULAR, ap->shiny ? &white[0] : zero ); glMatrixMode( GL_TEXTURE ); glLoadIdentity(); glMatrixMode( GL_MODELVIEW ); } if(ap->use && ap->use->textured && ap->use->txloaded > 0) ap = ap->use; if(ap->lighted) glEnable(GL_LIGHTING); int enabled = -1; //jjm-hook if(ap->hookfunc != NULL) { if( (*ap->hookfunc)( &enabled, ap->hookdata, ap, wf ) ) return; } if(ap->textured && ap->txloaded > 0) { txbind( ap->txim, &enabled ); /* Bind tx object, enable texturing. */ // glEnable( GL_ALPHA_TEST ); Something's wrong on the Octane?? XXX glAlphaFunc( GL_GREATER, 0.5 ); glTexEnvf( GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_DECAL ); } else { txbind( NULL, NULL ); glDisable( GL_TEXTURE_2D ); glDisable( GL_ALPHA_TEST ); } } void survey( Appearance *ap ) { while(ap != NULL) { printf("%p: %s[%p]%d ", ap, ap->name, ap->name, ap->defined); ap = ap->next; } printf("\n"); } #endif /*USE_MODEL*/