├── README └── obj2egg.py /README: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/treeform/obj2egg/2439769af58be9e2588753df0a2bbe5c8199c85e/README -------------------------------------------------------------------------------- /obj2egg.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | """ 3 | This Version: $Id: obj2egg.py,v 1.7 2008/05/26 17:42:53 andyp Exp $ 4 | Info: info >at< pfastergames.com 5 | 6 | Extended from: http://panda3d.org/phpbb2/viewtopic.php?t=3378 7 | .___..__ .___.___.___.__..__ . . 8 | | [__)[__ [__ [__ | |[__)|\/| 9 | | | \[___[___| |__|| \| | 10 | obj2egg.py [n##][b][t][s] filename1.obj ... 11 | -n regenerate normals with # degree smoothing 12 | exaple -n30 (normals at less 30 degrees will be smoothed) 13 | -b make binarmals 14 | -t make tangents 15 | -s show in pview 16 | 17 | licensed under WTFPL (http://sam.zoy.org/wtfpl/) 18 | """ 19 | 20 | from pandac.PandaModules import * 21 | import math 22 | import string 23 | import getopt 24 | import sys, os 25 | 26 | 27 | def floats(float_list): 28 | """coerce a list of strings that represent floats into a list of floats""" 29 | return [ float(number) for number in float_list ] 30 | 31 | def ints(int_list): 32 | """coerce a list of strings that represent integers into a list of integers""" 33 | return [ int(number) for number in int_list ] 34 | 35 | 36 | class ObjMaterial: 37 | """a wavefront material""" 38 | def __init__(self): 39 | self.filename = None 40 | self.name = "default" 41 | self.eggdiffusetexture = None 42 | self.eggmaterial = None 43 | self.attrib = {} 44 | self.attrib["Ns"] = 100.0 45 | self.attrib["d"] = 1.0 46 | self.attrib["illum"] = 2 47 | # "magenta" 48 | self.attrib["Kd"] = [1.0, 0.0, 1.0] 49 | self.attrib["Ka"] = [0.0, 0.0, 0.0] 50 | self.attrib["Ks"] = [0.0, 0.0, 0.0] 51 | self.attrib["Ke"] = [0.0, 0.0, 0.0] 52 | 53 | def put(self, key, value): 54 | self.attrib[key] = value 55 | return self 56 | 57 | def get(self, key): 58 | if self.attrib.has_key(key): 59 | return self.attrib[key] 60 | return None 61 | 62 | def has_key(self, key): 63 | return self.attrib.has_key(key) 64 | 65 | def isTextured(self): 66 | # for k in ("map_Kd", "map_Bump", "map_Ks"): <-- NOT YET 67 | if self.attrib.has_key("map_Kd"): 68 | return True; 69 | return False; 70 | 71 | def getEggTexture(self): 72 | if self.eggdiffusetexture: 73 | return self.eggdiffusetexture 74 | if not self.isTextured(): 75 | return none 76 | m = EggTexture(self.name + "_diffuse", self.get("map_Kd")) 77 | m.setFormat(EggTexture.FRgb) 78 | m.setMagfilter(EggTexture.FTLinearMipmapLinear) 79 | m.setMinfilter(EggTexture.FTLinearMipmapLinear) 80 | m.setWrapU(EggTexture.WMRepeat) 81 | m.setWrapV(EggTexture.WMRepeat) 82 | self.eggdiffusetexture = m 83 | return self.eggdiffusetexture 84 | 85 | def getEggMaterial(self): 86 | if self.eggmaterial: 87 | return self.eggmaterial 88 | m = EggMaterial(self.name + "_mat") 89 | # XXX TODO: add support for specular, and obey illum setting 90 | # XXX as best as we can 91 | rgb = self.get("Kd") 92 | if rgb is not None: 93 | m.setDiff(Vec4(rgb[0], rgb[1], rgb[2], 1.0)) 94 | rgb = self.get("Ka") 95 | if rgb is not None: 96 | m.setAmb(Vec4(rgb[0], rgb[1], rgb[2], 1.0)) 97 | rgb = self.get("Ks") 98 | if rgb is not None: 99 | m.setSpec(Vec4(rgb[0], rgb[1], rgb[2], 1.0)) 100 | ns = self.get("Ns") 101 | if ns is not None: 102 | m.setShininess(ns) 103 | self.eggmaterial = m 104 | return self.eggmaterial 105 | 106 | class MtlFile: 107 | """an object representing all Wavefront materials in a .mtl file""" 108 | def __init__(self, filename=None): 109 | self.filename = None 110 | self.materials = {} 111 | self.comments = {} 112 | if filename is not None: 113 | self.read(filename) 114 | 115 | def read(self, filename, verbose=False): 116 | self.filename = filename 117 | self.materials = {} 118 | self.comments = {} 119 | try: 120 | file = open(filename) 121 | except: 122 | return self 123 | linenumber = 0 124 | mat = None 125 | for line in file.readlines(): 126 | line = line.strip() 127 | linenumber = linenumber + 1 128 | if not line: 129 | continue 130 | if line[0] == '#': 131 | self.comments[linenumber] = line 132 | print line 133 | continue 134 | tokens = line.split() 135 | if not tokens: 136 | continue 137 | if verbose: print "tokens[0]:", tokens 138 | if tokens[0] == "newmtl": 139 | mat = ObjMaterial() 140 | mat.filename = filename 141 | mat.name = tokens[1] 142 | self.materials[mat.name] = mat 143 | if verbose: print "newmtl:", mat.name 144 | continue 145 | if tokens[0] in ("Ns", "d", "Tr"): 146 | # "d factor" - specifies the dissovle for the current material, 147 | # 1.0 is full opaque 148 | # "Ns exponent" - specifies the specular exponent. A high exponent 149 | # results in a tight, concentrated highlight. 150 | mat.put(tokens[0], float(tokens[1])) 151 | continue 152 | if tokens[0] in ("illum"): 153 | # according to http://www.fileformat.info/format/material/ 154 | # 0 = Color on and Ambient off 155 | # 1 = Color on and Ambient on 156 | # 2 = Highlight on 157 | # 3 = Reflection on and Ray trace on 158 | # 4 = Transparency: Glass on, Reflection: Ray trace on 159 | # 5 = Reflection: Fesnel on and Ray trace on 160 | # 6 = Transparency: Refraction on, Reflection: Fresnel off and Ray trace on 161 | # 7 = Transparency: Refraction on, Refelction: Fresnel on and Ray Trace on 162 | # 8 = Reflection on and Ray trace off 163 | # 9 = Transparency: Glass on, Reflection: Ray trace off 164 | # 10 = Casts shadows onto invisible surfaces 165 | mat.put(tokens[0], int(tokens[1])) 166 | continue 167 | if tokens[0] in ("Kd", "Ka", "Ks", "Ke"): 168 | mat.put(tokens[0], floats(tokens[1:])) 169 | continue 170 | if tokens[0] in ("map_Kd", "map_Bump", "map_Ks", "map_bump", "bump"): 171 | # Ultimate Unwrap 3D Pro emits these: 172 | # map_Kd == diffuse 173 | # map_Bump == bump 174 | # map_Ks == specular 175 | mat.put(tokens[0], pathify(tokens[1])) 176 | if verbose: print "map:", mat.name, tokens[0], mat.get(tokens[0]) 177 | continue 178 | if tokens[0] in ("Ni"): 179 | # blender's .obj exporter can emit this "Ni 1.000000" 180 | mat.put(tokens[0], float(tokens[1])) 181 | continue 182 | print "file \"%s\": line %d: unrecognized:" % (filename, linenumber), tokens 183 | file.close() 184 | if verbose: print "%d materials" % len(self.materials), "loaded from", filename 185 | return self 186 | 187 | class ObjFile: 188 | """a representation of a wavefront .obj file""" 189 | def __init__(self, filename=None): 190 | self.filename = None 191 | self.objects = ["defaultobject"] 192 | self.groups = ["defaultgroup"] 193 | self.points = [] 194 | self.uvs = [] 195 | self.normals = [] 196 | self.faces = [] 197 | self.polylines = [] 198 | self.matlibs = [] 199 | self.materialsbyname = {} 200 | self.comments = {} 201 | self.currentobject = self.objects[0] 202 | self.currentgroup = self.groups[0] 203 | self.currentmaterial = None 204 | if filename is not None: 205 | self.read(filename) 206 | 207 | def read(self, filename, verbose=False): 208 | if verbose: print "ObjFile.read:", "filename:", filename 209 | self.filename = filename 210 | self.objects = ["defaultobject"] 211 | self.groups = ["defaultgroup"] 212 | self.points = [] 213 | self.uvs = [] 214 | self.normals = [] 215 | self.faces = [] 216 | self.polylines = [] 217 | self.matlibs = [] 218 | self.materialsbyname = {} 219 | self.comments = {} 220 | self.currentobject = self.objects[0] 221 | self.currentgroup = self.groups[0] 222 | self.currentmaterial = None 223 | try: 224 | file = open(filename) 225 | except: 226 | return self 227 | linenumber = 0 228 | for line in file.readlines(): 229 | line = line.strip() 230 | linenumber = linenumber + 1 231 | if not line: 232 | continue 233 | if line[0] == '#': 234 | self.comments[linenumber] = line 235 | print line 236 | continue 237 | tokens = line.split() 238 | if not tokens: 239 | continue 240 | if tokens[0] == "mtllib": 241 | if verbose: print "mtllib:", tokens[1:] 242 | mtllib = MtlFile(tokens[1]) 243 | # if verbose: print mtllib 244 | self.matlibs.append(mtllib) 245 | self.indexmaterials(mtllib) 246 | continue 247 | if tokens[0] == "g": 248 | if verbose: print "g:", tokens[1:] 249 | self.__newgroup("".join(tokens[1:])) 250 | continue 251 | if tokens[0] == "o": 252 | if verbose: print "o:", tokens[1:] 253 | self.__newobject("".join(tokens[1:])) 254 | continue 255 | if tokens[0] == "usemtl": 256 | if verbose: print "usemtl:", tokens[1:] 257 | self.__usematerial(tokens[1]) 258 | continue 259 | if tokens[0] == "v": 260 | if verbose: print "v:", tokens[1:] 261 | self.__newv(tokens[1:]) 262 | continue 263 | if tokens[0] == "vn": 264 | if verbose: print "vn:", tokens[1:] 265 | self.__newnormal(tokens[1:]) 266 | continue 267 | if tokens[0] == "vt": 268 | if verbose: print "vt:", tokens[1:] 269 | self.__newuv(tokens[1:]) 270 | continue 271 | if tokens[0] == "f": 272 | if verbose: print "f:", tokens[1:] 273 | self.__newface(tokens[1:]) 274 | continue 275 | if tokens[0] == "s": 276 | # apparently, this enables/disables smoothing 277 | print "%s:%d:" % (filename, linenumber), "ignoring:", tokens 278 | continue 279 | if tokens[0] == "l": 280 | if verbose: print "l:", tokens[1:] 281 | self.__newpolyline(tokens[1:]) 282 | continue 283 | print "%s:%d:" % (filename, linenumber), "unknown:", tokens 284 | file.close() 285 | return self 286 | 287 | def __vertlist(self, lst): 288 | res = [] 289 | for vert in lst: 290 | vinfo = vert.split("/") 291 | vlen = len(vinfo) 292 | vertex = {'v':None, 'vt':None, 'vn':None} 293 | if vlen == 1: 294 | vertex['v'] = int(vinfo[0]) 295 | elif vlen == 2: 296 | if vinfo[0] != '': 297 | vertex['v'] = int(vinfo[0]) 298 | if vinfo[1] != '': 299 | vertex['vt'] = int(vinfo[1]) 300 | elif vlen == 3: 301 | if vinfo[0] != '': 302 | vertex['v'] = int(vinfo[0]) 303 | if vinfo[1] != '': 304 | vertex['vt'] = int(vinfo[1]) 305 | if vinfo[2] != '': 306 | vertex['vn'] = int(vinfo[2]) 307 | else: 308 | print "aborting..." 309 | raise UNKNOWN, res 310 | res.append(vertex) 311 | if False: print res 312 | return res 313 | 314 | def __enclose(self, lst): 315 | mdata = (self.currentobject, self.currentgroup, self.currentmaterial) 316 | return (lst, mdata) 317 | 318 | def __newpolyline(self, l): 319 | polyline = self.__vertlist(l) 320 | if False: print "__newline:", polyline 321 | self.polylines.append(self.__enclose(polyline)) 322 | return self 323 | 324 | def __newface(self, f): 325 | face = self.__vertlist(f) 326 | if False: print face 327 | self.faces.append(self.__enclose(face)) 328 | return self 329 | 330 | def __newuv(self, uv): 331 | self.uvs.append(floats(uv)) 332 | return self 333 | 334 | def __newnormal(self, normal): 335 | self.normals.append(floats(normal)) 336 | return self 337 | 338 | def __newv(self, v): 339 | # capture the current metadata with vertices 340 | vdata = floats(v) 341 | mdata = (self.currentobject, self.currentgroup, self.currentmaterial) 342 | vinfo = (vdata, mdata) 343 | self.points.append(vinfo) 344 | return self 345 | 346 | def indexmaterials(self, mtllib, verbose=False): 347 | # traverse the materials defined in mtllib, indexing 348 | # them by name. 349 | for mname in mtllib.materials: 350 | mobj = mtllib.materials[mname] 351 | self.materialsbyname[mobj.name] = mobj 352 | if verbose: 353 | print "indexmaterials:", mtllib.filename, "materials:", self.materialsbyname.keys() 354 | return self 355 | 356 | def __closeobject(self): 357 | self.currentobject = "defaultobject" 358 | return self 359 | 360 | def __newobject(self, object): 361 | self.__closeobject() 362 | if False: print "__newobject:", "object:", object 363 | self.currentobject = object 364 | self.objects.append(object) 365 | return self 366 | 367 | def __closegroup(self): 368 | self.currentgroup = "defaultgroup" 369 | return self 370 | 371 | def __newgroup(self, group): 372 | self.__closegroup() 373 | if False: print "__newgroup:", "group:", group 374 | self.currentgroup = group 375 | self.groups.append(group) 376 | return self 377 | 378 | def __usematerial(self, material): 379 | if False: print "__usematerial:", "material:", material 380 | if self.materialsbyname.has_key(material): 381 | self.currentmaterial = material 382 | else: 383 | print "warning:", "__usematerial:", "unknown material:", material 384 | return self 385 | 386 | def __itemsby(self, itemlist, objname, groupname): 387 | res = [] 388 | for item in itemlist: 389 | vlist, mdata = item 390 | wobj, wgrp, wmat = mdata 391 | if (wobj == objname) and (wgrp == groupname): 392 | res.append(item) 393 | return res 394 | 395 | def __facesby(self, objname, groupname): 396 | return self.__itemsby(self.faces, objname, groupname) 397 | 398 | def __linesby(self, objname, groupname): 399 | return self.__itemsby(self.polylines, objname, groupname) 400 | 401 | def __eggifyverts(self, eprim, evpool, vlist): 402 | for vertex in vlist: 403 | ixyz = vertex['v'] 404 | vinfo = self.points[ixyz-1] 405 | vxyz, vmeta = vinfo 406 | ev = EggVertex() 407 | ev.setPos(Point3D(vxyz[0], vxyz[1], vxyz[2])) 408 | iuv = vertex['vt'] 409 | if iuv is not None: 410 | vuv = self.uvs[iuv-1] 411 | ev.setUv(Point2D(vuv[0], vuv[1])) 412 | inormal = vertex['vn'] 413 | if inormal is not None: 414 | vn = self.normals[inormal-1] 415 | ev.setNormal(Vec3D(vn[0], vn[1], vn[2])) 416 | evpool.addVertex(ev) 417 | eprim.addVertex(ev) 418 | return self 419 | 420 | def __eggifymats(self, eprim, wmat): 421 | if self.materialsbyname.has_key(wmat): 422 | mtl = self.materialsbyname[wmat] 423 | if mtl.isTextured(): 424 | eprim.setTexture(mtl.getEggTexture()) 425 | # NOTE: it looks like you almost always want to setMaterial() 426 | # for textured polys.... [continued below...] 427 | eprim.setMaterial(mtl.getEggMaterial()) 428 | rgb = mtl.get("Kd") 429 | if rgb is not None: 430 | # ... and some untextured .obj's store the color of the 431 | # material # in the Kd settings... 432 | eprim.setColor(Vec4(rgb[0], rgb[1], rgb[2], 1.0)) 433 | # [continued...] but you may *not* always want to assign 434 | # materials to untextured polys... hmmmm. 435 | if False: 436 | eprim.setMaterial(mtl.getEggMaterial()) 437 | return self 438 | 439 | def __facestoegg(self, egg, objname, groupname): 440 | selectedfaces = self.__facesby(objname, groupname) 441 | if len(selectedfaces) == 0: 442 | return self 443 | eobj = EggGroup(objname) 444 | egg.addChild(eobj) 445 | egrp = EggGroup(groupname) 446 | eobj.addChild(egrp) 447 | evpool = EggVertexPool(groupname) 448 | egrp.addChild(evpool) 449 | for face in selectedfaces: 450 | vlist, mdata = face 451 | wobj, wgrp, wmat = mdata 452 | epoly = EggPolygon() 453 | egrp.addChild(epoly) 454 | self.__eggifymats(epoly, wmat) 455 | self.__eggifyverts(epoly, evpool, vlist) 456 | #; each matching face 457 | return self 458 | 459 | def __polylinestoegg(self, egg, objname, groupname): 460 | selectedlines = self.__linesby(objname, groupname) 461 | if len(selectedlines) == 0: 462 | return self 463 | eobj = EggGroup(objname) 464 | egg.addChild(eobj) 465 | egrp = EggGroup(groupname) 466 | eobj.addChild(egrp) 467 | evpool = EggVertexPool(groupname) 468 | egrp.addChild(evpool) 469 | for line in selectedlines: 470 | vlist, mdata = line 471 | wobj, wgrp, wmat = mdata 472 | eline = EggLine() 473 | egrp.addChild(eline) 474 | self.__eggifymats(eline, wmat) 475 | self.__eggifyverts(eline, evpool, vlist) 476 | #; each matching line 477 | return self 478 | 479 | def toEgg(self, verbose=True): 480 | if verbose: print "converting..." 481 | # make a new egg 482 | egg = EggData() 483 | # convert polygon faces 484 | if len(self.faces) > 0: 485 | for objname in self.objects: 486 | for groupname in self.groups: 487 | self.__facestoegg(egg, objname, groupname) 488 | # convert polylines 489 | if len(self.polylines) > 0: 490 | for objname in self.objects: 491 | for groupname in self.groups: 492 | self.__polylinestoegg(egg, objname, groupname) 493 | return egg 494 | 495 | def pathify(path): 496 | if os.path.isfile(path): 497 | return path 498 | # if it was written on win32, it may have \'s in it, and 499 | # also a full rather than relative pathname (Hexagon does this... ick) 500 | orig = path 501 | path = path.lower() 502 | path = path.replace("\\", "/") 503 | h, t = os.path.split(path) 504 | if os.path.isfile(t): 505 | return t 506 | print "warning: can't make sense of this map file name:", orig 507 | return t 508 | 509 | def main(argv=None): 510 | if argv is None: 511 | argv = sys.argv 512 | try: 513 | opts, args = getopt.getopt(argv[1:], "hn:bs", ["help", "normals", "binormals", "show"]) 514 | except getopt.error, msg: 515 | print msg 516 | print __doc__ 517 | return 2 518 | show = False 519 | for o, a in opts: 520 | if o in ("-h", "--help"): 521 | print __doc__ 522 | return 0 523 | elif o in ("-s", "--show"): 524 | show = True 525 | for infile in args: 526 | try: 527 | if ".obj" not in infile: 528 | print "WARNING", finfile, "does not look like a valid obj file" 529 | continue 530 | obj = ObjFile(infile) 531 | egg = obj.toEgg() 532 | f, e = os.path.splitext(infile) 533 | outfile = f + ".egg" 534 | for o, a in opts: 535 | if o in ("-n", "--normals"): 536 | egg.recomputeVertexNormals(float(a)) 537 | elif o in ("-b", "--binormals"): 538 | egg.recomputeTangentBinormal(GlobPattern("")) 539 | egg.removeUnusedVertices(GlobPattern("")) 540 | if True: 541 | egg.triangulatePolygons(EggData.TConvex & EggData.TPolygon) 542 | if True: 543 | egg.recomputePolygonNormals() 544 | egg.writeEgg(Filename(outfile)) 545 | if show: 546 | os.system("pview " + outfile) 547 | except Exception,e: 548 | print e 549 | return 0 550 | 551 | if __name__ == "__main__": 552 | sys.exit(main()) 553 | 554 | 555 | --------------------------------------------------------------------------------