# VRML import prototype # # strubi@blender.nl # """VRML import module This is a prototype for VRML97 file import Supported: - Object hierarchies, transform collapsing (optional) - Meshes (IndexedFaceSet, no Basic primitives yet) - Materials - Textures (jpg, tga), conversion option from alien formats """ import Blender.sys as os # Blender os emulation from beta import Scenegraph Transform = Scenegraph.Transform import beta.Objects _b = beta.Objects #from Blender import Mesh Color = _b.Color DEFAULTFLAGS = _b.DEFAULTFLAGS FACEFLAGS = _b.FACEFLAGS shadowNMesh = _b.shadowNMesh quat = Scenegraph.quat # quaternion math vect = quat.vect # vector math module from vrml import loader #### GLOBALS OB = Scenegraph.Object.Types # CONST values LA = Scenegraph.Lamp.Types g_level = 1 g_supported_fileformats = ["jpg", "jpeg", "tga"] #### OPTIONS OPTIONS = {'cylres' : 16, # resolution of cylinder 'flipnormals' : 0, # flip normals (force) 'mat_as_vcol' : 0, # material as vertex color - warning, this increases mem usage drastically on big files 'notextures' : 0, # no textures - saves some memory 'collapseDEFs' : 0, # collapse DEF nodes 'collapseTF' : 0, # collapse Transforms (as far as possible, # i.e. currently to Object transform level) } #### CONSTANTS LAYER_EMPTY = (1 << 2) LAYER_LAMP = (1 << 4) LAYER_CAMERA = 1 + (1 << 4) CREASE_ANGLE_THRESHOLD = 0.45 # radians PARSE_TIME = (loader.parser.IMPORT_PARSE_TIME ) PROCESS_TIME = (1.0 - PARSE_TIME ) PROGRESS_DEPTH = loader.parser.PROGRESS_DEPTH VERBOSE_DEPTH = PROGRESS_DEPTH #### DEBUG def warn(text): print "###", text def debug2(text): print (g_level - 1) * 4 * " " + text def verbose(text): print text def quiet(text): pass debug = quiet #### ERROR message filtering: g_error = {} # dictionary for non-fatal errors to mark whether an error # was already reported def clrError(): global g_error g_error['toomanyfaces'] = 0 def isError(name): return g_error[name] def setError(name): global g_error g_error[name] = 1 #### ERROR handling class baseError: def __init__(self, value): self.value = value def __str__(self): return `self.value` class MeshError(baseError): pass UnfinishedError = loader.parser.UnfinishedError ########################################################## # HELPER ROUTINES def assignImage(f, img): f.image = img def assignUV(f, uv): if len(uv) != len(f.v): uv = uv[:len(f.v)] #raise MeshError, "Number of UV coordinates does not match number of vertices in face" f.uv = [] for u in uv: f.uv.append((u[0], u[1])) # make sure it's a tuple #### VRML STUFF # this is used for transform collapsing class TransformStack: def __init__(self): self.stack = [Transform()] def push(self, t): self.stack.append(t) def pop(self): return self.stack.pop() def last(self): return self.stack[-1] def fromVRMLTransform(tfnode): t = Transform() s = tfnode.scale t.scale = (s[0], s[1], s[2]) r = tfnode.rotation if r[0] == 0.0 and r[1] == 0.0 and r[2] == 0.0: rotaxis = (0.0, 0.0, 1.0) ang = 0.0 else: rotaxis = vect.norm3(r[:3]) ang = r[3] #t.rotation = (rotaxis, ang) t.calcRotfromAxis((rotaxis, ang)) tr = tfnode.translation t.translation = (tr[0], tr[1], tr[2]) # XXX more to come.. return t ### TODO: enable material later on #class dummyMaterial: #def setMode(self, *args): #pass def fromVRMLMaterial(mat): name = mat.DEF from Blender import Material m = Material.New(name) m.rgbCol = mat.diffuseColor m.alpha = 1.0 - mat.transparency m.emit = vect.len3(mat.emissiveColor) if m.Emit > 0.01: if vect.cross(mat.diffuseColor, mat.emissiveColor) > 0.01 * m.Emit: m.rgbCol = mat.emissiveColor m.ref = 1.0 m.spec = mat.shininess m.specCol = mat.specularColor m.amb = mat.ambientIntensity return m # override: #def fromVRMLMaterial(mat): # return dummyMaterial() def buildVRMLTextureMatrix(tr): from math import sin, cos newMat = vect.Matrix newVec = vect.Vector # rotmatrix s = tr.scale t = tr.translation c = tr.center phi = tr.rotation SR = newMat() C = newMat() C[2] = newVec(c[0], c[1], 1.0) if abs(phi) > 0.00001: SR[0] = newVec(s[0] * cos(phi), s[1] * sin(phi), 0.0) SR[1] = newVec(-s[0] * sin(phi), s[1] * cos(phi), 0.0) else: SR[0] = newVec(s[0], 0.0, 0.0) SR[1] = newVec(0.0, s[1], 0.0) SR = C * SR * C.inverse() # rotate & scale about rotation center T = newMat() T[2] = newVec(t[0], t[1], 1.0) return SR * T # texture transform matrix def imageConvert(fromfile, tofile): """This should convert from a image file to another file, type is determined automatically (on extension). It's currently just a stub - users can override this function to implement their own converters""" return 0 # we just fail in general def addImage(path, filename): "returns a possibly existing image which is imported by Blender" from Blender import Image img = None try: r = filename.rindex('.') except: return None naked = filename[:r] ext = filename[r+1:].lower() if path: name = os.sep.join([path, filename]) file = os.sep.join([path, naked]) else: name = filename file = naked if not ext in g_supported_fileformats: tgafile = file + '.tga' jpgfile = file + '.jpg' for f in tgafile, jpgfile: # look for jpg, tga try: img = Image.Load(f) if img: verbose("couldn't load %s (unsupported).\nFound %s instead" % (name, f)) return img except IOError, msg: pass try: imgfile = open(name, "rb") imgfile.close() except IOError, msg: warn("Image %s not found" % name) return None verbose("Format unsupported, trying to convert to %s" % tgafile) if not imageConvert(name, tgafile): warn("image conversion failed") return None else: return Image.Load(tgafile) return None # failed try: img = Image.Load(name) except IOError, msg: warn("Image %s not found" % name) return img # ok, is supported def callMethod(_class, method, vnode, newnode, warn = 1): meth = None try: meth = getattr(_class, method) except AttributeError: if warn: unknownType(method) return None, None if meth: return meth(vnode, parent = newnode) def unknownType(type): warn("unsupported:" + repr(type)) def getChildren(vnode): try: children = vnode.children except: children = None return children def getNodeType(vnode): return vnode.__gi__ GroupingNodeTypes = ["Group", "Collision", "Anchor", "Billboard", "Inline", "LOD", "Switch", "Transform"] ################################################################################ # #### PROCESSING CLASSES class NullProcessor: def __init__(self, tstack = TransformStack()): self.stack = tstack self.walker = None self.mesh = None self.ObjectNode = Scenegraph.NodefromData # may be altered... self.MaterialCache = {} self.ImageCache = {} # This is currently not used XXX class DEFcollapser(NullProcessor): """This is for collapsing DEF Transform nodes into a single object""" def __init__(self): self.collapsedNodes = [] def Transform(self, curnode, parent, **kw): name = curnode.DEF if not name: # node is a DEF node return None, None return children, None class Processor(NullProcessor): """The processor class defines the handler for a VRML Scenegraph node. Definition of a handler method simply happens by use of the VRML Scenegraph entity name. A handler usually creates a new Scenegraph node in the target scenegraph, converting the data from the given VRML node. A handler takes the arguments: curnode: the currently visited VRML node parent: the previously generated target scenegraph parent node **kw: additional keywords It MUST return: (children, newBnode) where: children: the children of the current VRML node. These will be further processed by the processor. If this is not wanted (because they might have been processed by the handler), None must be returned. newBnode: the newly created target node or None. """ def _handleProto(self, curnode, parent, **kw): p = curnode.PROTO if not p.sceneGraph: print curnode.__gi__, "unsupported" return None, None def _dummy(self, curnode, parent, **kw): print curnode.sceneGraph return None, None #def __getattr__(self, name): #"""If method is not statically defined, look up prototypes""" #return self._handleProto def _currentTransform(self): return self.stack.last() def _parent(self, curnode, parent, trans): name = curnode.DEF children = getChildren(curnode) debug("children: %s" % children) objects = [] transforms = [] groups = [] isempty = 0 for c in children: type = getNodeType(c) if type == 'Transform': transforms.append(c) elif type in GroupingNodeTypes: groups.append(c) #else: elif hasattr(self, type): objects.append(c) if transforms or groups or len(objects) != 1: # it's an empty if not name: name = 'EMPTY' Bnode = self.ObjectNode(None, OB.EMPTY, name) # empty Blender Object node if options['layers']: Bnode.object.Layer = LAYER_EMPTY Bnode.transform = trans Bnode.update() isempty = 1 parent.insert(Bnode) else: # don't insert extra empty if only one object has children Bnode = parent for node in objects: c, new = self.walker.walk(node, Bnode) if not isempty: # only apply transform if no extra transform empty in hierarchy new.transform = trans Bnode.insert(new) for node in transforms: self.walker.walk(node, Bnode) for node in groups: self.walker.walk(node, Bnode) return None, None def sceneGraph(self, curnode, parent, **kw): parent.type = 'ROOT' return curnode.children, None def Transform(self, curnode, parent, **kw): # we support 'center' and 'scaleOrientation' by inserting # another Empty in between the Transforms t = fromVRMLTransform(curnode) cur = self._currentTransform() chainable = 0 if OPTIONS['collapseTF']: try: cur = cur * t # chain transforms except: cur = self._currentTransform() chainable = 1 self.stack.push(cur) # here comes the tricky hacky transformation conversion # TODO: SR not supported yet if chainable == 1: # collapse, but not chainable # insert extra transform: Bnode = self.ObjectNode(None, OB.EMPTY, 'Transform') # Empty Bnode.transform = cur parent.insert(Bnode) parent = Bnode c = curnode.center if c != [0.0, 0.0, 0.0]: chainable = 1 trans = Transform() trans.translation = (-c[0], -c[1], -c[2]) tr = t.translation t.translation = (tr[0] + c[0], tr[1] + c[1], tr[2] + c[2]) Bnode = self.ObjectNode(None, OB.EMPTY, 'C') # Empty Bnode.transform = t parent.insert(Bnode) parent = Bnode else: trans = t if chainable == 2: # collapse and is chainable # don't parent, insert into root node: for c in getChildren(curnode): dummy, node = self.walker.walk(c, parent) # skip transform node, insert into parent if node: # a valid Blender node node.transform = cur else: self._parent(curnode, parent, trans) self.stack.pop() return None, None def Switch(self, curnode, parent, **kw): return None, None def Group(self, curnode, parent, **kw): if OPTIONS['collapseTF']: cur = self._currentTransform() # don't parent, insert into root node: children = getChildren(curnode) for c in children: dummy, node = self.walker.walk(c, parent) # skip transform node, insert into parent if node: # a valid Blender node node.transform = cur else: t = Transform() self._parent(curnode, parent, t) return None, None def Collision(self, curnode, parent, **kw): return self.Group(curnode, parent) # def LOD(self, curnode, parent, **kw): # c, node = self.walker.walk(curnode.level[0], parent) # parent.insert(node) # return None, None def Appearance(self, curnode, parent, **kw): # material colors: mat = curnode.material self.curColor = mat.diffuseColor name = mat.DEF if name: if self.MaterialCache.has_key(name): self.curmaterial = self.MaterialCache[name] else: m = fromVRMLMaterial(mat) self.MaterialCache[name] = m self.curmaterial = m else: if curnode.DEF: name = curnode.DEF if self.MaterialCache.has_key(name): self.curmaterial = self.MaterialCache[name] else: m = fromVRMLMaterial(mat) self.MaterialCache[name] = m self.curmaterial = m else: self.curmaterial = fromVRMLMaterial(mat) try: name = curnode.texture.url[0] except: name = None if name: if self.ImageCache.has_key(name): self.curImage = self.ImageCache[name] else: self.ImageCache[name] = self.curImage = addImage(self.curpath, name) else: self.curImage = None tr = curnode.textureTransform if tr: self.curtexmatrix = buildVRMLTextureMatrix(tr) else: self.curtexmatrix = None return None, None def Shape(self, curnode, parent, **kw): name = curnode.DEF debug(name) #self.mesh = Mesh.rawMesh() self.mesh = shadowNMesh() self.mesh.name = name # don't mess with the order of these.. if curnode.appearance: self.walker.preprocess(curnode.appearance, self.walker.preprocessor) else: # no appearance, get colors from shape (vertex colors) self.curColor = None self.curImage = None self.walker.preprocess(curnode.geometry, self.walker.preprocessor) if hasattr(self, 'curmaterial'): self.mesh.assignMaterial(self.curmaterial) meshobj = self.mesh.write() # write mesh del self.mesh bnode = Scenegraph.ObjectNode(meshobj, OB.MESH, name) if name: curnode.setTargetnode(bnode) # mark as already processed return None, bnode def Box(self, curnode, parent, **kw): col = apply(Color, self.curColor) faces = [] x, y, z = curnode.size x *= 0.5; y *= 0.5; z *= 0.5 name = curnode.DEF m = self.mesh v0 = m.addVert((-x, -y, -z)) v1 = m.addVert(( x, -y, -z)) v2 = m.addVert(( x, y, -z)) v3 = m.addVert((-x, y, -z)) v4 = m.addVert((-x, -y, z)) v5 = m.addVert(( x, -y, z)) v6 = m.addVert(( x, y, z)) v7 = m.addVert((-x, y, z)) flags = DEFAULTFLAGS if not self.curImage: uvflag = 1 else: uvflag = 0 m.addFace([v3, v2, v1, v0], flags, uvflag) m.addFace([v0, v1, v5, v4], flags, uvflag) m.addFace([v1, v2, v6, v5], flags, uvflag) m.addFace([v2, v3, v7, v6], flags, uvflag) m.addFace([v3, v0, v4, v7], flags, uvflag) m.addFace([v4, v5, v6, v7], flags, uvflag) for f in m.faces: f.col = [col, col, col, col] return None, None def Viewpoint(self, curnode, parent, **kw): t = Transform() r = curnode.orientation name = 'View_' + curnode.description t.calcRotfromAxis((r[:3], r[3])) t.translation = curnode.position Bnode = self.ObjectNode(None, OB.CAMERA, name) # Empty Bnode.object.Layer = LAYER_CAMERA Bnode.transform = t return None, Bnode def DirectionalLight(self, curnode, parent, **kw): loc = (0.0, 10.0, 0.0) l = self._lamp(curnode, loc) l.object.data.type = LA.SUN return None, l def PointLight(self, curnode, parent, **kw): l = self._lamp(curnode, curnode.location) l.object.data.type = LA.LOCAL return None, l def _lamp(self, curnode, location): t = Transform() name = curnode.DEF energy = curnode.intensity t.translation = location Bnode = self.ObjectNode(None, OB.LAMP, "Lamp") Bnode.object.data.energy = energy * 5.0 if options['layers']: Bnode.object.Layer = LAYER_LAMP Bnode.transform = t return Bnode def IndexedFaceSet(self, curnode, **kw): matxvec = vect.matxvec mesh = self.mesh debug("IFS, read mesh") texcoo = curnode.texCoord uvflag = 0 if curnode.color: colors = curnode.color.color if curnode.colorIndex: # we have color indices colindex = curnode.colorIndex else: colindex = curnode.coordIndex if not texcoo: uvflag = 1 else: colors = None faceflags = DEFAULTFLAGS if not texcoo and OPTIONS['mat_as_vcol'] and self.curColor: uvflag = 1 col = apply(Color, self.curColor) elif self.curImage: faceflags += FACEFLAGS.TEX # MAKE VERTICES coo = curnode.coord ncoo = len(coo.point) if curnode.normal: # normals defined normals = curnode.normal.vector if curnode.normalPerVertex and len(coo.point) == len(normals): self.mesh.recalc_normals = 0 normindex = curnode.normalIndex i = 0 for v in coo.point: newv = mesh.addVert(v) n = newv.no n[0], n[1], n[2] = normals[normindex[i]] i += 1 else: for v in coo.point: mesh.addVert(v) else: for v in coo.point: mesh.addVert(v) if curnode.creaseAngle < CREASE_ANGLE_THRESHOLD: self.mesh.smooth = 1 nvertices = len(mesh.vertices) if nvertices != ncoo: print "todo: %d, done: %d" % (ncoo, nvertices) raise RuntimeError, "FATAL: could not create all vertices" # MAKE FACES index = curnode.coordIndex vlist = [] flip = OPTIONS['flipnormals'] facecount = 0 vertcount = 0 cols = [] if curnode.colorPerVertex: # per vertex colors for i in index: if i == -1: if flip or (curnode.ccw == 0 and not flip): # counterclockwise face def vlist.reverse() f = mesh.addFace(vlist, faceflags, uvflag) if uvflag or colors: f.col = cols cols = [] vlist = [] else: if colors: col = apply(Color, colors[colindex[vertcount]]) cols.append(col) vertcount += 1 v = mesh.vertices[i] vlist.append(v) else: # per face colors for i in index: if i == -1: if flip or (curnode.ccw == 0 and not flip): # counterclockwise face def vlist.reverse() f = mesh.addFace(vlist, faceflags, uvflag) facecount += 1 if colors: col = apply(Color, colors[colindex[facecount]]) cols = len(f.v) * [col] if uvflag or colors: f.col = cols vlist = [] else: v = mesh.vertices[i] vlist.append(v) # TEXTURE COORDINATES if not texcoo: return None, None self.curmaterial.setMode("traceable", "shadow", "texFace") m = self.curtexmatrix if m: # texture transform exists: for uv in texcoo.point: v = (uv[0], uv[1], 1.0) v1 = matxvec(m, v) uv[0], uv[1] = v1[0], v1[1] UVindex = curnode.texCoordIndex if not UVindex: UVindex = curnode.coordIndex # go assign UVs self.mesh.hasFaceUV(1) j = 0 uv = [] for i in UVindex: if i == -1: # flush if not curnode.ccw: uv.reverse() assignUV(f, uv) assignImage(f, self.curImage) uv = [] j +=1 else: f = mesh.faces[j] uv.append(texcoo.point[i]) return None, None class PostProcessor(NullProcessor): def Shape(self, curnode, **kw): pass return None, None def Transform(self, curnode, **kw): return None, None class Walker: """The node visitor (walker) class for VRML nodes""" def __init__(self, pre, post = NullProcessor(), progress = None): self.scene = Scenegraph.BScene() self.preprocessor = pre self.postprocessor = post pre.walker = self # processor knows about walker post.walker = self self.nodes = 1 self.depth = 0 self.progress = progress self.processednodes = 0 def walk(self, vnode, parent): """Essential walker routine. It walks along the scenegraph nodes and processes them according to its pre/post processor methods. The preprocessor methods return the children of the node remaining to be processed or None. Also, a new created target node is returned. If the target node is == None, the current node will be skipped in the target scenegraph generation. If it is a valid node, the walker routine inserts it into the 'parent' node of the target scenegraph, which must be a valid root node on first call, leading us to the example usage: p = Processor() w = Walker(p, PostProcessor()) root = Scenegraph.RootNode() w.walk(SG, root) # SG is a VRML scenegraph """ global g_level #XXX self.depth += 1 g_level = self.depth if self.depth < PROGRESS_DEPTH: self.processednodes += 1 if self.progress: ret = self.progress(PARSE_TIME + PROCESS_TIME * float(self.processednodes) / self.nodes) if not ret: progress(1.0) raise UnfinishedError, "User cancelled conversion" # if vnode has already been processed, call Linker method, Processor method otherwise id = vnode.DEF # get name if not id: id = 'Object' processed = vnode.getTargetnode() if processed: # has been processed ? debug("linked obj: %s" % id) children, bnode = self.link(processed, parent) else: children, bnode = self.preprocess(vnode, parent) if not bnode: bnode = parent # pass on else: parent.insert(bnode) # insert into SG if children: for c in children: self.walk(c, bnode) if not processed: self.postprocess(vnode, bnode) self.depth -= 1 return children, bnode def link(self, bnode, parent): """Link already processed data""" # link data: new = bnode.clone() if not new: raise RuntimeError, "couldn't clone object" return None, new def preprocess(self, vnode, newnode = None): """Processes a VRML node 'vnode' and returns a custom node. The processor must be specified in 'p'. Optionally, a custom parent node (previously created) is passed as 'newnode'.""" pre = "pre" nodetype = vnode.__gi__ debug(pre + "process:" + repr(nodetype) + " " + vnode.DEF) return callMethod(self.preprocessor, nodetype, vnode, newnode) def postprocess(self, vnode, newnode = None): """Postprocessing of a VRML node, see Walker.preprocess()""" nodetype = vnode.__gi__ pre = "post" debug(pre + "process:" + repr(nodetype) + " " + vnode.DEF) return callMethod(self.postprocessor, nodetype, vnode, newnode, 0) testfile2 = '/home/strubi/exotic/wrl/BrownTrout1.wrl' testfile = '/home/strubi/exotic/wrl/examples/VRML_Model_HSL.wrl' def fix_VRMLaxes(root, scale): from Blender import Object, Scene q = quat.fromRotAxis((1.0, 0.0, 0.0), 1.57079) empty = Object.New(OB.EMPTY) empty.layer = LAYER_EMPTY Scene.getCurrent().link(empty) node = Scenegraph.ObjectNode(empty, None, "VRMLscene") node.transform.rotation = q if scale: node.transform.scale = (0.01, 0.01, 0.01) for c in root.children: node.insert(c) node.update() root.children = [node] ################################################################# # these are the routines that must be provided for the importer # interface in blender def checkmagic(name): "check for file magic" f = open(name, "r") magic = loader.getFileType(f) f.close() if magic == 'vrml': return 1 elif magic == 'gzip': verbose("gzipped file detected") try: import gzip except ImportError, value: warn("Importing gzip module: %s" % value) return 0 f = gzip.open(name, 'rb') header = f.readline() f.close() if header[:10] == "#VRML V2.0": return 1 else: return 0 print "unknown file" return 0 g_infotxt = "" def progress(done): from Blender import Window ret = Window.draw_progressbar(done, g_infotxt) return ret class Counter: def __init__(self): self._count = 0 self.depth = 0 def count(self, node): if self.depth >= PROGRESS_DEPTH: return 0 self.depth += 1 self._count += 1 if not getChildren(node): self.depth -= 1 return 0 else: for c in node.children: self.count(c) self.depth -= 1 return self._count ################################################################################ # MAIN ROUTINE def importfile(name): global g_infotxt global options global DEFAULTFLAGS # from Blender import Get # XXX options = Get('vrmloptions') DEFAULTFLAGS = FACEFLAGS.LIGHT + FACEFLAGS.DYNAMIC if options['twoside']: print "TWOSIDE" DEFAULTFLAGS |= FACEFLAGS.TWOSIDE clrError() g_infotxt = "load & parse file..." progress(0.0) root = Scenegraph.RootNode() try: l = loader.Loader(name, progress) SG = l.load() p = Processor() w = Walker(p, PostProcessor(), progress) g_infotxt = "convert data..." p.curpath = os.path.dirname(name) print "counting nodes...", c = Counter() nodes = c.count(SG) print "done." w.nodes = nodes # let walker know about number of nodes parsed # XXX w.walk(SG, root) except UnfinishedError, msg: print msg progress(1.0) fix_VRMLaxes(root, options['autoscale']) # rotate coordinate system: in VRML, y is up! root.update() # update baselist for proper display return root