3 Name: 'X3D & VRML97 (.x3d / wrl)...'
6 Tooltip: 'Load an X3D or VRML97 file'
9 # ***** BEGIN GPL LICENSE BLOCK *****
11 # (C) Copyright 2008 Paravizion
12 # Written by Campbell Barton aka Ideasman42
14 # This program is free software; you can redistribute it and/or
15 # modify it under the terms of the GNU General Public License
16 # as published by the Free Software Foundation; either version 2
17 # of the License, or (at your option) any later version.
19 # This program is distributed in the hope that it will be useful,
20 # but WITHOUT ANY WARRANTY; without even the implied warranty of
21 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
22 # GNU General Public License for more details.
24 # You should have received a copy of the GNU General Public License
25 # along with this program; if not, write to the Free Software Foundation,
26 # Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
28 # ***** END GPL LICENCE BLOCK *****
29 # --------------------------------------------------------------------------
31 __author__ = "Campbell Barton"
32 __url__ = ['www.blender.org', 'blenderartists.org', 'http://wiki.blender.org/index.php/Scripts/Manual/Import/X3D_VRML97']
36 This script is an importer for the X3D and VRML97 file formats.
39 # This should work without a blender at all
41 from Blender.sys import exists
43 from os.path import exists
46 return path.split('/')[-1].split('\\')[-1]
49 return path[:-len(baseName(path))]
51 def imageConvertCompat(path):
57 if path.endswith('.gif'):
58 path_to = path[:-3] + 'png'
64 # print '\n'+path+'\n'+path_to+'\n'
65 os.system('convert "%s" "%s"' % (path, path_to)) # for now just hope we have image magick
73 # transform are relative
74 # order dosnt matter for loc/size/rot
75 # right handed rotation
76 # angles are in radians
77 # rotation first defines axis then ammount in radians
81 # =============================== VRML Spesific
86 Keep this as a valid vrml file, but format in a way we can pradict.
88 # Strip all commends - # not in strings - warning multiline strings are ignored.
90 #l = ' '.join(l.split())
101 # Most cases accounted for! if we have a comment at the end of the line do this...
105 if j == -1: # simple no strings
109 for i,c in enumerate(l):
119 data = '\n'.join([strip_comment(l) for l in data.split('\n') ]) # remove all whitespace
121 EXTRACT_STRINGS = True # only needed when strings or filesnames containe ,[]{} chars :/
125 # We need this so we can detect URL's
126 data = '\n'.join([' '.join(l.split()) for l in data.split('\n')]) # remove all whitespace
137 i = data.find(search, last_i)
140 start = i + len(search) # first char after end of search
141 end = data.find('"', start)
143 item = data[start:end]
144 string_ls.append( item )
145 data = data[:start] + data[end:]
146 ok = True # keep looking
148 last_i = end - len(item) + 1
149 # print last_i, item, '|' + data[last_i] + '|'
151 # done with messy extracting strings part
155 # Bad, dont take strings into account
157 data = data.replace('#', '\n#')
158 data = '\n'.join([ll for l in data.split('\n') for ll in (l.strip(),) if not ll.startswith('#')]) # remove all whitespace
160 data = data.replace('{', '\n{\n')
161 data = data.replace('}', '\n}\n')
162 data = data.replace('[', '\n[\n')
163 data = data.replace(']', '\n]\n')
164 data = data.replace(',', ' , ') # make sure comma's seperate
167 # add strings back in
169 search = '"' # fill in these empty strings
175 i = data.find(search + '"', last_i)
178 start = i + len(search) # first char after end of search
179 item = string_ls.pop(0)
180 data = data[:start] + item + data[start:]
182 last_i = start + len(item)
187 # More annoying obscure cases where USE or DEF are placed on a newline
188 # data = data.replace('\nDEF ', ' DEF ')
189 # data = data.replace('\nUSE ', ' USE ')
191 data = '\n'.join([' '.join(l.split()) for l in data.split('\n')]) # remove all whitespace
193 # Better to parse the file accounting for multiline arrays
195 data = data.replace(',\n', ' , ') # remove line endings with commas
196 data = data.replace(']', '\n]\n') # very very annoying - but some comma's are at the end of the list, must run this again.
199 return [l for l in data.split('\n') if l]
203 NODE_REFERENCE = 3 # USE foobar
207 def getNodePreText(i, words):
210 while len(words) < 5:
215 # words.append(lines[i]) # no need
217 return NODE_NORMAL, i+1
218 elif lines[i].count('"') % 2 != 0: # odd number of quotes? - part of a string.
222 new_words = lines[i].split()
223 if 'USE' in new_words:
226 words.extend(new_words)
229 # Check for USE node - no {
230 # USE #id - should always be on the same line.
232 # print 'LINE', i, words[:words.index('USE')+2]
233 words[:] = words[:words.index('USE')+2]
234 if lines[i] == '{' and lines[i+1] == '}':
235 # USE sometimes has {} after it anyway
237 return NODE_REFERENCE, i
239 # print "error value!!!", words
242 def is_nodeline(i, words):
244 if not lines[i][0].isalpha():
247 # Simple "var [" type
248 if lines[i+1] == '[':
249 if lines[i].count('"') % 2 == 0:
250 words[:] = lines[i].split()
251 return NODE_ARRAY, i+2
253 node_type, new_i = getNodePreText(i, words)
258 # Ok, we have a { after some values
259 # Check the values are not fields
260 for i, val in enumerate(words):
261 if i != 0 and words[i-1] in ('DEF', 'USE'):
262 # ignore anything after DEF, it is a ID and can contain any chars.
264 elif val[0].isalpha() and val not in ('TRUE', 'FALSE'):
267 # There is a number in one of the values, therefor we are not a node.
270 #if node_type==NODE_REFERENCE:
271 # print words, "REF_!!!!!!!"
272 return node_type, new_i
276 Does this line start with a number?
279 # Works but too slow.
300 if l.startswith(', '):
304 line_end_new = l.find(' ', line_start) # comma's always have a space before them
306 if line_end_new != -1:
307 line_end = line_end_new
310 float(l[line_start:line_end]) # works for a float or int
316 class vrmlNode(object):
317 __slots__ = 'id', 'fields', 'node_type', 'parent', 'children', 'parent', 'array_data', 'reference', 'lineno', 'filename', 'blendObject', 'DEF_NAMESPACE', 'ROUTE_IPO_NAMESPACE', 'FIELD_NAMESPACE', 'x3dNode'
318 def __init__(self, parent, node_type, lineno):
320 self.node_type = node_type
322 self.blendObject = None
323 self.x3dNode = None # for x3d import only
325 parent.children.append(self)
329 # This is only set from the root nodes.
330 # Having a filename also denotes a root node
333 # Store in the root node because each inline file needs its own root node and its own namespace
334 self.DEF_NAMESPACE = None
335 self.ROUTE_IPO_NAMESPACE = None
336 self.FIELD_NAMESPACE = None
338 self.reference = None
340 if node_type==NODE_REFERENCE:
341 # For references, only the parent and ID are needed
342 # the reference its self is assigned on parsing
345 self.fields = [] # fields have no order, in some cases rool level values are not unique so dont use a dict
347 self.array_data = [] # use for arrays of data - should only be for NODE_ARRAY types
350 # Only available from the root node
351 def getFieldDict(self):
352 if self.FIELD_NAMESPACE != None:
353 return self.FIELD_NAMESPACE
355 return self.parent.getFieldDict()
357 def getDefDict(self):
358 if self.DEF_NAMESPACE != None:
359 return self.DEF_NAMESPACE
361 return self.parent.getDefDict()
363 def getRouteIpoDict(self):
364 if self.ROUTE_IPO_NAMESPACE != None:
365 return self.ROUTE_IPO_NAMESPACE
367 return self.parent.getRouteIpoDict()
369 def setRoot(self, filename):
370 self.filename = filename
371 self.FIELD_NAMESPACE = {}
372 self.DEF_NAMESPACE = {}
373 self.ROUTE_IPO_NAMESPACE = {}
376 if self.filename == None:
381 def getFilename(self):
385 return self.parent.getFilename()
389 def getRealNode(self):
391 return self.reference
396 self_real = self.getRealNode()
398 return self_real.id[-1] # its possible this node has no spec
407 def getDefName(self):
408 self_real = self.getRealNode()
410 if 'DEF' in self_real.id:
412 return self_real.id[ list(self_real.id).index('DEF')+1 ]
417 def getChildrenBySpec(self, node_spec): # spec could be Transform, Shape, Appearance
418 self_real = self.getRealNode()
419 # using getSpec functions allows us to use the spec of USE children that dont have their spec in their ID
420 if type(node_spec) == str:
421 return [child for child in self_real.children if child.getSpec()==node_spec]
423 # Check inside a list of optional types
424 return [child for child in self_real.children if child.getSpec() in node_spec]
426 def getChildBySpec(self, node_spec): # spec could be Transform, Shape, Appearance
427 # Use in cases where there is only ever 1 child of this type
428 ls = self.getChildrenBySpec(node_spec)
432 def getChildrenByName(self, node_name): # type could be geometry, children, appearance
433 self_real = self.getRealNode()
434 return [child for child in self_real.children if child.id if child.id[0]==node_name]
436 def getChildByName(self, node_name):
437 self_real = self.getRealNode()
438 for child in self_real.children:
439 if child.id and child.id[0]==node_name: # and child.id[-1]==node_spec:
442 def getSerialized(self, results, ancestry):
443 ''' Return this node and all its children in a flat list '''
444 ancestry = ancestry[:] # always use a copy
446 # self_real = self.getRealNode()
448 results.append((self, tuple(ancestry)))
449 ancestry.append(self)
450 for child in self.getRealNode().children:
451 if child not in ancestry:
452 child.getSerialized(results, ancestry)
456 def searchNodeTypeID(self, node_spec, results):
457 self_real = self.getRealNode()
458 # print self.lineno, self.id
459 if self_real.id and self_real.id[-1]==node_spec: # use last element, could also be only element
460 results.append(self_real)
461 for child in self_real.children:
462 child.searchNodeTypeID(node_spec, results)
465 def getFieldName(self, field):
466 self_real = self.getRealNode() # incase we're an instance
468 for f in self_real.fields:
470 if f and f[0] == field:
471 # print '\tfound field', f
474 # print '\tfield not found', field
477 def getFieldAsInt(self, field, default):
478 self_real = self.getRealNode() # incase we're an instance
480 f = self_real.getFieldName(field)
481 if f==None: return default
482 if ',' in f: f = f[:f.index(',')] # strip after the comma
485 print '\t"%s" wrong length for int conversion for field "%s"' % (f, field)
491 print '\tvalue "%s" could not be used as an int for field "%s"' % (f[0], field)
494 def getFieldAsFloat(self, field, default):
495 self_real = self.getRealNode() # incase we're an instance
497 f = self_real.getFieldName(field)
498 if f==None: return default
499 if ',' in f: f = f[:f.index(',')] # strip after the comma
502 print '\t"%s" wrong length for float conversion for field "%s"' % (f, field)
508 print '\tvalue "%s" could not be used as a float for field "%s"' % (f[0], field)
511 def getFieldAsFloatTuple(self, field, default):
512 self_real = self.getRealNode() # incase we're an instance
514 f = self_real.getFieldName(field)
515 if f==None: return default
516 # if ',' in f: f = f[:f.index(',')] # strip after the comma
519 print '"%s" wrong length for float tuple conversion for field "%s"' % (f, field)
525 try: ret.append(float(v))
526 except: break # quit of first non float, perhaps its a new field name on the same line? - if so we are going to ignore it :/ TODO
532 print '\tvalue "%s" could not be used as a float tuple for field "%s"' % (f, field)
535 def getFieldAsBool(self, field, default):
536 self_real = self.getRealNode() # incase we're an instance
538 f = self_real.getFieldName(field)
539 if f==None: return default
540 if ',' in f: f = f[:f.index(',')] # strip after the comma
543 print '\t"%s" wrong length for bool conversion for field "%s"' % (f, field)
546 if f[0].upper()=='"TRUE"' or f[0].upper()=='TRUE':
548 elif f[0].upper()=='"FALSE"' or f[0].upper()=='FALSE':
551 print '\t"%s" could not be used as a bool for field "%s"' % (f[1], field)
554 def getFieldAsString(self, field, default=None):
555 self_real = self.getRealNode() # incase we're an instance
557 f = self_real.getFieldName(field)
558 if f==None: return default
560 print '\t"%s" wrong length for string conversion for field "%s"' % (f, field)
564 # String may contain spaces
573 if st[0]=='"' and st[-1]=='"':
576 print '\tvalue "%s" could not be used as a string for field "%s"' % (f[0], field)
579 def getFieldAsArray(self, field, group):
581 For this parser arrays are children
583 self_real = self.getRealNode() # incase we're an instance
586 for child in self_real.children:
587 # print "ID IS", child.id
588 if child.id and len(child.id) == 1 and child.id[0] == field:
592 if child_array==None:
594 # For x3d, should work ok with vrml too
595 # for x3d arrays are fields, vrml they are nodes, annoying but not tooo bad.
596 data_split = self.getFieldName(field)
599 array_data = ' '.join(data_split)
600 if array_data == None:
603 array_data = array_data.replace(',', ' ')
604 data_split = array_data.split()
606 array_data = [int(val) for val in data_split]
609 array_data = [float(val) for val in data_split]
611 print '\tWarning, could not parse array data from field'
616 array_data = child_array.array_data
619 # print 'array_data', array_data
621 if group==-1 or len(array_data)==0:
624 # We want a flat list
626 for item in array_data:
627 if type(item) == list:
633 flat_array = array_data # we are alredy flat.
639 if type(item)==list: extend_flat(item)
640 else: flat_array.append(item)
642 extend_flat(array_data)
645 # We requested a flat array
652 for item in flat_array:
653 sub_array.append(item)
654 if len(sub_array)==group:
655 new_array.append(sub_array)
659 print '\twarning, array was not aligned to requested grouping', group, 'remaining value', sub_array
663 def getFieldAsStringArray(self, field):
665 Get a list of strings
667 self_real = self.getRealNode() # incase we're an instance
670 for child in self_real.children:
671 if child.id and len(child.id) == 1 and child.id[0] == field:
677 # each string gets its own list, remove ""'s
679 new_array = [f[0][1:-1] for f in child_array.fields]
681 print '\twarning, string array could not be made'
699 level = self.getLevel()
702 if self.node_type==NODE_REFERENCE:
704 elif self.node_type==NODE_NORMAL:
710 text = ind + brackets[0] + '\n'
714 text += ind + 'ID: ' + str(self.id) + ' ' + str(level) + (' lineno %d\n' % self.lineno)
716 if self.node_type==NODE_REFERENCE:
717 text += ind + "(reference node)\n"
720 text += ind + 'FIELDS:\n'
722 for i,item in enumerate(self.fields):
723 text += ind + 'FIELD:\n'
724 text += ind + str(item) +'\n'
726 #text += ind + 'ARRAY: ' + str(len(self.array_data)) + ' ' + str(self.array_data) + '\n'
727 text += ind + 'ARRAY: ' + str(len(self.array_data)) + '[...] \n'
729 text += ind + 'CHILDREN: ' + str(len(self.children)) + '\n'
730 for i, child in enumerate(self.children):
731 text += ind + ('CHILD%d:\n' % i)
734 text += '\n' + ind + brackets[1]
739 new_i = self.__parse(i)
741 # print self.id, self.getFilename()
743 # If we were an inline then try load the file
744 if self.node_type == NODE_NORMAL and self.getSpec() == 'Inline':
746 url = self.getFieldAsString('url', None)
751 urls.append( BPySys.caseInsensitivePath(urls[-1]) )
753 urls.append( dirName(self.getFilename()) + baseName(url) )
754 urls.append( BPySys.caseInsensitivePath(urls[-1]) )
757 url = [url for url in urls if exists(url)][0]
763 print '\tWarning: Inline URL could not be found:', url
765 if url==self.getFilename():
766 print '\tWarning: cant Inline yourself recursively:', url
772 print '\tWarning: cant open the file:', url
776 # Tricky - inline another VRML
777 print '\tLoading Inline:"%s"...' % url
779 # Watch it! - backup lines
783 lines[:] = vrmlFormat( data )
786 lines.insert(0, 'root_node____')
789 ff = open('/test.txt', 'w')
790 ff.writelines([l+'\n' for l in lines])
793 child = vrmlNode(self, NODE_NORMAL, -1)
794 child.setRoot(url) # initialized dicts
797 # Watch it! - restore lines
803 def __parse(self, i):
804 # print 'parsing at', i,
805 # print i, self.id, self.lineno
814 node_type, new_i = is_nodeline(i, words)
815 if not node_type: # fail for parsing new node.
818 if self.node_type==NODE_REFERENCE:
819 # Only assign the reference and quit
820 key = words[words.index('USE')+1]
821 self.id = (words[0],)
823 self.reference = self.getDefDict()[key]
826 self.id = tuple(words)
829 key = self.getDefName()
832 self.getDefDict()[ key ] = self
846 if self.node_type != NODE_NORMAL:
847 print 'wrong node ending, expected an } ' + str(i)
849 ### print "returning", i
852 if self.node_type != NODE_ARRAY:
853 print 'wrong node ending, expected a ] ' + str(i)
855 ### print "returning", i
858 node_type, new_i = is_nodeline(i, [])
859 if node_type: # check text\n{
860 ### print '\t\tgroup', i
861 child = vrmlNode(self, node_type, i)
863 # print child.id, 'YYY'
865 elif l=='[': # some files have these anonymous lists
866 child = vrmlNode(self, NODE_ARRAY, i)
870 l_split = l.split(',')
873 # See if each item is a float?
875 for num_type in (int, float):
877 values = [num_type(v) for v in l_split ]
884 values = [[num_type(v) for v in segment.split()] for segment in l_split ]
889 if values == None: # dont parse
892 # This should not extend over multiple lines however it is possible
893 # print self.array_data
895 self.array_data.extend( values )
899 if len(words) > 2 and words[1] == 'USE':
900 vrmlNode(self, NODE_REFERENCE, i)
903 # print "FIELD", i, l
906 ### print '\t\ttag', i
911 # javastrips can exist as values.
912 quote_count = l.count('"')
913 if quote_count % 2: # odd number?
918 quote_count = l.count('"')
919 if quote_count % 2: # odd number?
920 value += '\n'+ l[:l.rfind('"')]
925 value_all = value.split()
928 if k[0] != '"' and k[0].isalpha() and k.upper() not in ('TRUE', 'FALSE'):
932 def split_fields(value):
934 key 0.0 otherkey 1,2,3 opt1 opt1 0.0
935 -> [key 0.0], [otherkey 1,2,3], [opt1 opt1 0.0]
940 for j in xrange(len(value)):
943 # this IS a key but the previous value was not a key, ot it was a defined field.
944 if (not iskey(field_context[-1])) or ((len(field_context)==3 and field_context[1]=='IS')):
945 field_list.append(field_context)
946 field_context = [value[j]]
948 # The last item was not a value, multiple keys are needed in some cases.
949 field_context.append(value[j])
951 # Is empty, just add this on
952 field_context.append(value[j])
954 # Add a value to the list
955 field_context.append(value[j])
958 field_list.append(field_context)
963 for value in split_fields(value_all):
966 if value[0]=='field':
967 # field SFFloat creaseAngle 4
968 self.getFieldDict()[value[2]] = value[3:] # skip the first 3 values
970 # Get referenced field
971 if len(value) >= 3 and value[1]=='IS':
973 value = [ value[0] ] + self.getFieldDict()[ value[2] ]
975 print '\tWarning, field could not be found:', value, 'TODO add support for exposedField'
976 print '\t', self.getFieldDict()
977 self.fields.append(value)
979 self.fields.append(value)
988 try: data = gzip.open(path, 'r').read()
991 print '\tNote, gzip module could not be imported, compressed files will fail to load'
994 try: data = open(path, 'rU').read()
999 def vrml_parse(path):
1001 Sets up the root node and returns it so load_web3d() can deal with the blender side of things.
1002 Return root (vrmlNode, '') or (None, 'Error String')
1004 data = gzipOpen(path)
1007 return None, 'Failed to open file: ' + path
1010 lines[:] = vrmlFormat( data )
1012 lines.insert(0, '{')
1013 lines.insert(0, 'dymmy_node')
1015 # Use for testing our parsed output, so we can check on line numbers.
1018 ff = open('/test.txt', 'w')
1019 ff.writelines([l+'\n' for l in lines])
1023 node_type, new_i = is_nodeline(0, [])
1025 return None, 'Error: VRML file has no starting Node'
1027 # Trick to make sure we get all root nodes.
1028 lines.insert(0, '{')
1029 lines.insert(0, 'root_node____') # important the name starts with an ascii char
1032 root = vrmlNode(None, NODE_NORMAL, -1)
1033 root.setRoot(path) # we need to set the root so we have a namespace and know the path incase of inlineing
1038 # This prints a load of text
1046 # ====================== END VRML
1050 # ====================== X3d Support
1052 # Sane as vrml but replace the parser
1053 class x3dNode(vrmlNode):
1054 def __init__(self, parent, node_type, x3dNode):
1055 vrmlNode.__init__(self, parent, node_type, -1)
1056 self.x3dNode = x3dNode
1059 # print self.x3dNode.tagName
1061 define = self.x3dNode.getAttributeNode('DEF')
1063 self.getDefDict()[define.value] = self
1065 use = self.x3dNode.getAttributeNode('USE')
1068 self.reference = self.getDefDict()[use.value]
1069 self.node_type = NODE_REFERENCE
1071 print '\tWarning: reference', use.value, 'not found'
1072 self.parent.children.remove(self)
1076 for x3dChildNode in self.x3dNode.childNodes:
1077 if x3dChildNode.nodeType in (x3dChildNode.TEXT_NODE, x3dChildNode.COMMENT_NODE, x3dChildNode.CDATA_SECTION_NODE):
1080 node_type = NODE_NORMAL
1081 # print x3dChildNode, dir(x3dChildNode)
1082 if x3dChildNode.getAttributeNode('USE'):
1083 node_type = NODE_REFERENCE
1085 child = x3dNode(self, node_type, x3dChildNode)
1091 return self.x3dNode.tagName # should match vrml spec
1093 def getDefName(self):
1094 data = self.x3dNode.getAttributeNode('DEF')
1098 # Other funcs operate from vrml, but this means we can wrap XML fields, still use nice utility funcs
1099 # getFieldAsArray getFieldAsBool etc
1100 def getFieldName(self, field):
1101 self_real = self.getRealNode() # incase we're an instance
1102 field_xml = self.x3dNode.getAttributeNode(field)
1104 value = field_xml.value
1106 # We may want to edit. for x3d spesific stuff
1107 # Sucks a bit to return the field name in the list but vrml excepts this :/
1108 return value.split()
1112 def x3d_parse(path):
1114 Sets up the root node and returns it so load_web3d() can deal with the blender side of things.
1115 Return root (x3dNode, '') or (None, 'Error String')
1119 import xml.dom.minidom
1121 return None, 'Error, import XML parsing module (xml.dom.minidom) failed, install python'
1124 try: doc = xml.dom.minidom.parse(path)
1125 except: return None, 'Could not parse this X3D file, XML error'
1128 # Could add a try/except here, but a console error is more useful.
1129 data = gzipOpen(path)
1132 return None, 'Failed to open file: ' + path
1134 doc = xml.dom.minidom.parseString(data)
1138 x3dnode = doc.getElementsByTagName('X3D')[0]
1140 return None, 'Not a valid x3d document, cannot import'
1142 root = x3dNode(None, NODE_NORMAL, x3dnode)
1143 root.setRoot(path) # so images and Inline's we load have a relative path
1150 ## f = open('/_Cylinder.wrl', 'r')
1151 # f = open('/fe/wrl/Vrml/EGS/TOUCHSN.WRL', 'r')
1152 # vrml_parse('/fe/wrl/Vrml/EGS/TOUCHSN.WRL')
1153 #vrml_parse('/fe/wrl/Vrml/EGS/SCRIPT.WRL')
1157 files = os.popen('find /fe/wrl -iname "*.wrl"').readlines()
1160 for i, f in enumerate(files):
1169 # NO BLENDER CODE ABOVE THIS LINE.
1170 # -----------------------------------------------------------------------------------
1177 from Blender import Texture, Material, Mathutils, Mesh, Types, Window
1178 from Blender.Mathutils import TranslationMatrix
1179 from Blender.Mathutils import RotationMatrix
1180 from Blender.Mathutils import Vector
1181 from Blender.Mathutils import Matrix
1183 RAD_TO_DEG = 57.29578
1185 GLOBALS = {'CIRCLE_DETAIL':16}
1187 def translateRotation(rot):
1189 return RotationMatrix(rot[3]*RAD_TO_DEG, 4, 'r', Vector(rot[:3]))
1191 def translateScale(sca):
1192 mat = Matrix() # 4x4 default
1198 def translateTransform(node):
1199 cent = node.getFieldAsFloatTuple('center', None) # (0.0, 0.0, 0.0)
1200 rot = node.getFieldAsFloatTuple('rotation', None) # (0.0, 0.0, 1.0, 0.0)
1201 sca = node.getFieldAsFloatTuple('scale', None) # (1.0, 1.0, 1.0)
1202 scaori = node.getFieldAsFloatTuple('scaleOrientation', None) # (0.0, 0.0, 1.0, 0.0)
1203 tx = node.getFieldAsFloatTuple('translation', None) # (0.0, 0.0, 0.0)
1206 cent_mat = TranslationMatrix(Vector(cent)).resize4x4()
1207 cent_imat = cent_mat.copy().invert()
1209 cent_mat = cent_imat = None
1211 if rot: rot_mat = translateRotation(rot)
1212 else: rot_mat = None
1214 if sca: sca_mat = translateScale(sca)
1215 else: sca_mat = None
1218 scaori_mat = translateRotation(scaori)
1219 scaori_imat = scaori_mat.copy().invert()
1221 scaori_mat = scaori_imat = None
1223 if tx: tx_mat = TranslationMatrix(Vector(tx)).resize4x4()
1228 mats = [tx_mat, cent_mat, rot_mat, scaori_mat, sca_mat, scaori_imat, cent_imat]
1231 new_mat = mtx * new_mat
1235 def translateTexTransform(node):
1236 cent = node.getFieldAsFloatTuple('center', None) # (0.0, 0.0)
1237 rot = node.getFieldAsFloat('rotation', None) # 0.0
1238 sca = node.getFieldAsFloatTuple('scale', None) # (1.0, 1.0)
1239 tx = node.getFieldAsFloatTuple('translation', None) # (0.0, 0.0)
1243 # cent is at a corner by default
1244 cent_mat = TranslationMatrix(Vector(cent).resize3D()).resize4x4()
1245 cent_imat = cent_mat.copy().invert()
1247 cent_mat = cent_imat = None
1249 if rot: rot_mat = RotationMatrix(rot*RAD_TO_DEG, 4, 'z') # translateRotation(rot)
1250 else: rot_mat = None
1252 if sca: sca_mat = translateScale((sca[0], sca[1], 0.0))
1253 else: sca_mat = None
1255 if tx: tx_mat = TranslationMatrix(Vector(tx).resize3D()).resize4x4()
1260 # as specified in VRML97 docs
1261 mats = [cent_imat, sca_mat, rot_mat, cent_mat, tx_mat]
1265 new_mat = mtx * new_mat
1271 def getFinalMatrix(node, mtx, ancestry):
1273 transform_nodes = [node_tx for node_tx in ancestry if node_tx.getSpec() == 'Transform']
1274 if node.getSpec()=='Transform':
1275 transform_nodes.append(node)
1276 transform_nodes.reverse()
1281 for node_tx in transform_nodes:
1282 mat = translateTransform(node_tx)
1287 def importMesh_IndexedFaceSet(geom, bpyima):
1288 # print geom.lineno, geom.id, vrmlNode.DEF_NAMESPACE.keys()
1290 ccw = geom.getFieldAsBool('ccw', True)
1291 ifs_colorPerVertex = geom.getFieldAsBool('colorPerVertex', True) # per vertex or per face
1292 ifs_normalPerVertex = geom.getFieldAsBool('normalPerVertex', True)
1294 # This is odd how point is inside Coordinate
1297 #coord = geom.getChildByName('coord') # 'Coordinate'
1299 coord = geom.getChildBySpec('Coordinate') # works for x3d and vrml
1301 if coord: ifs_points = coord.getFieldAsArray('point', 3)
1305 print '\tWarnint: IndexedFaceSet has no points'
1308 ifs_faces = geom.getFieldAsArray('coordIndex', 0)
1311 if ifs_faces: # In rare cases this causes problems - no faces but UVs???
1314 # coords_tex = geom.getChildByName('texCoord')
1315 coords_tex = geom.getChildBySpec('TextureCoordinate')
1318 ifs_texpoints = coords_tex.getFieldAsArray('point', 2)
1319 ifs_texfaces = geom.getFieldAsArray('texCoordIndex', 0)
1321 if not ifs_texpoints:
1322 # IF we have no coords, then dont bother
1327 # vcolor = geom.getChildByName('color')
1328 vcolor = geom.getChildBySpec('Color')
1329 vcolor_spot = None # spot color when we dont have an array of colors
1332 ifs_vcol = [[int(c*256) for c in col] for col in vcolor.getFieldAsArray('color', 3)]
1333 ifs_color_index = geom.getFieldAsArray('colorIndex', 0)
1336 vcolor_spot = [int(c*256) for c in vcolor.getFieldAsFloatTuple('color', [])]
1338 # Convert faces into somthing blender can use
1341 # All lists are aligned!
1343 faces_uv = [] # if ifs_texfaces is empty then the faces_uv will match faces exactly.
1344 faces_orig_index = [] # for ngons, we need to know our original index
1346 if coords_tex and ifs_texfaces:
1351 # current_face = [0] # pointer anyone
1353 def add_face(face, fuvs, orig_index):
1357 # faces_orig_index.append(current_face[0])
1359 faces_uv.append(fuvs)
1361 faces_orig_index.append(orig_index)
1362 elif l==2: edges.append(face)
1364 for i in xrange(2, len(face)):
1365 faces.append([face[0], face[i-1], face[i]])
1367 faces_uv.append([fuvs[0], fuvs[i-1], fuvs[i]])
1368 faces_orig_index.append(orig_index)
1370 # faces with 1 verts? pfft!
1371 # still will affect index ordering
1377 for i, fi in enumerate(ifs_faces):
1378 # ifs_texfaces and ifs_faces should be aligned
1380 # face.append(int(fi)) # in rare cases this is a float
1382 # Annoyance where faces that have a zero index vert get rotated. This will then mess up UVs and VColors
1383 face.append(int(fi)+1) # in rare cases this is a float, +1 because of stupid EEKADOODLE :/
1386 if i >= len(ifs_texfaces):
1387 print '\tWarning: UV Texface index out of range'
1388 fuvs.append(ifs_texfaces[0])
1390 fuvs.append(ifs_texfaces[i])
1392 add_face(face, fuvs, orig_index)
1398 add_face(face, fuvs, orig_index)
1399 del add_face # dont need this func anymore
1401 bpymesh = bpy.data.meshes.new()
1403 bpymesh.verts.extend([(0,0,0)]) # EEKADOODLE
1404 bpymesh.verts.extend(ifs_points)
1406 # print len(ifs_points), faces, edges, ngons
1409 bpymesh.faces.extend(faces, smooth=True, ignoreDups=True)
1411 print "one or more vert indicies out of range. corrupt file?"
1413 # bpymesh.faces.extend(faces, smooth=True)
1415 bpymesh.calcNormals()
1417 if len(bpymesh.faces) != len(faces):
1418 print '\tWarning: adding faces did not work! file is invalid, not adding UVs or vcolors'
1421 # Apply UVs if we have them
1423 faces_uv = faces # fallback, we didnt need a uvmap in the first place, fallback to the face/vert mapping.
1425 #print ifs_texpoints
1427 bpymesh.faceUV = True
1428 for i,f in enumerate(bpymesh.faces):
1430 fuv = faces_uv[i] # uv indicies
1431 for j,uv in enumerate(f.uv):
1432 # print fuv, j, len(ifs_texpoints)
1434 uv[:] = ifs_texpoints[fuv[j]]
1436 print '\tWarning: UV Index out of range'
1437 uv[:] = ifs_texpoints[0]
1439 elif bpyima and len(bpymesh.faces):
1440 # Oh Bugger! - we cant really use blenders ORCO for for texture space since texspace dosnt rotate.
1441 # we have to create VRML's coords as UVs instead.
1445 If the texCoord field is NULL, a default texture coordinate mapping is calculated using the local
1446 coordinate system bounding box of the shape. The longest dimension of the bounding box defines the S coordinates,
1447 and the next longest defines the T coordinates. If two or all three dimensions of the bounding box are equal,
1448 ties shall be broken by choosing the X, Y, or Z dimension in that order of preference.
1449 The value of the S coordinate ranges from 0 to 1, from one end of the bounding box to the other.
1450 The T coordinate ranges between 0 and the ratio of the second greatest dimension of the bounding box to the greatest dimension.
1454 # U gets longest, V gets second longest
1455 xmin, ymin, zmin = ifs_points[0]
1456 xmax, ymax, zmax = ifs_points[0]
1457 for co in ifs_points:
1459 if x < xmin: xmin = x
1460 if y < ymin: ymin = y
1461 if z < zmin: zmin = z
1463 if x > xmax: xmax = x
1464 if y > ymax: ymax = y
1465 if z > zmax: zmax = z
1471 depth_min = xmin, ymin, zmin
1472 depth_list = [xlen, ylen, zlen]
1473 depth_sort = depth_list[:]
1476 depth_idx = [depth_list.index(val) for val in depth_sort]
1478 axis_u = depth_idx[-1]
1479 axis_v = depth_idx[-2] # second longest
1481 # Hack, swap these !!! TODO - Why swap??? - it seems to work correctly but should not.
1482 # axis_u,axis_v = axis_v,axis_u
1484 min_u = depth_min[axis_u]
1485 min_v = depth_min[axis_v]
1486 depth_u = depth_list[axis_u]
1487 depth_v = depth_list[axis_v]
1491 if axis_u == axis_v:
1492 # This should be safe because when 2 axies have the same length, the lower index will be used.
1495 bpymesh.faceUV = True
1497 # HACK !!! - seems to be compatible with Cosmo though.
1498 depth_v = depth_u = max(depth_v, depth_u)
1500 for f in bpymesh.faces:
1504 for i,v in enumerate(f):
1506 fuv[i][:] = (co[axis_u]-min_u) / depth_u, (co[axis_v]-min_v) / depth_v
1511 bpymesh.vertexColors = True
1513 for f in bpymesh.faces:
1515 if ifs_colorPerVertex:
1517 for i,c in enumerate(fcol):
1518 color_index = fv[i].index # color index is vert index
1519 if ifs_color_index: color_index = ifs_color_index[color_index]
1521 if len(ifs_vcol) < color_index:
1522 c.r, c.g, c.b = ifs_vcol[color_index]
1524 #print '\tWarning: per face color index out of range'
1527 if vcolor_spot: # use 1 color, when ifs_vcol is []
1529 c.r, c.g, c.b = vcolor_spot
1531 color_index = faces_orig_index[f.index] # color index is face index
1532 #print color_index, ifs_color_index
1534 if color_index <= len(ifs_color_index):
1535 print '\tWarning: per face color index out of range'
1538 color_index = ifs_color_index[color_index]
1541 col = ifs_vcol[color_index]
1542 for i,c in enumerate(fcol):
1545 bpymesh.verts.delete([0,]) # EEKADOODLE
1549 def importMesh_IndexedLineSet(geom):
1551 #coord = geom.getChildByName('coord') # 'Coordinate'
1552 coord = geom.getChildBySpec('Coordinate') # works for x3d and vrml
1553 if coord: points = coord.getFieldAsArray('point', 3)
1557 print '\tWarning: IndexedLineSet had no points'
1560 ils_lines = geom.getFieldAsArray('coordIndex', 0)
1565 for il in ils_lines:
1570 line.append(int(il))
1573 # vcolor = geom.getChildByName('color') # blender dosnt have per vertex color
1575 bpycurve = bpy.data.curves.new('IndexedCurve', 'Curve')
1585 co = points[line[0]]
1586 bpycurve.appendNurb([co[0], co[1], co[2], w, t])
1587 bpycurve[curve_index].type= 0 # Poly Line
1591 bpycurve.appendPoint(curve_index, [co[0], co[1], co[2], w])
1599 def importMesh_PointSet(geom):
1601 #coord = geom.getChildByName('coord') # 'Coordinate'
1602 coord = geom.getChildBySpec('Coordinate') # works for x3d and vrml
1603 if coord: points = coord.getFieldAsArray('point', 3)
1606 # vcolor = geom.getChildByName('color') # blender dosnt have per vertex color
1608 bpymesh = bpy.data.meshes.new()
1609 bpymesh.verts.extend(points)
1610 bpymesh.calcNormals() # will just be dummy normals
1613 GLOBALS['CIRCLE_DETAIL'] = 12
1615 MATRIX_Z_TO_Y = RotationMatrix(90, 4, 'x')
1617 def importMesh_Sphere(geom):
1618 # bpymesh = bpy.data.meshes.new()
1619 diameter = geom.getFieldAsFloat('radius', 0.5) * 2 # * 2 for the diameter
1620 bpymesh = Mesh.Primitives.UVsphere(GLOBALS['CIRCLE_DETAIL'], GLOBALS['CIRCLE_DETAIL'], diameter)
1621 bpymesh.transform(MATRIX_Z_TO_Y)
1624 def importMesh_Cylinder(geom):
1625 # bpymesh = bpy.data.meshes.new()
1626 diameter = geom.getFieldAsFloat('radius', 1.0) * 2 # * 2 for the diameter
1627 height = geom.getFieldAsFloat('height', 2)
1628 bpymesh = Mesh.Primitives.Cylinder(GLOBALS['CIRCLE_DETAIL'], diameter, height)
1629 bpymesh.transform(MATRIX_Z_TO_Y)
1631 # Warning - Rely in the order Blender adds verts
1632 # not nice design but wont change soon.
1634 bottom = geom.getFieldAsBool('bottom', True)
1635 side = geom.getFieldAsBool('side', True)
1636 top = geom.getFieldAsBool('top', True)
1638 if not top: # last vert is top center of tri fan.
1639 bpymesh.verts.delete([(GLOBALS['CIRCLE_DETAIL']+GLOBALS['CIRCLE_DETAIL'])+1])
1641 if not bottom: # second last vert is bottom of triangle fan
1642 bpymesh.verts.delete([GLOBALS['CIRCLE_DETAIL']+GLOBALS['CIRCLE_DETAIL']])
1646 bpymesh.faces.delete(1, [f for f in bpymesh.faces if len(f)==4])
1650 def importMesh_Cone(geom):
1651 # bpymesh = bpy.data.meshes.new()
1652 diameter = geom.getFieldAsFloat('bottomRadius', 1.0) * 2 # * 2 for the diameter
1653 height = geom.getFieldAsFloat('height', 2)
1654 bpymesh = Mesh.Primitives.Cone(GLOBALS['CIRCLE_DETAIL'], diameter, height)
1655 bpymesh.transform(MATRIX_Z_TO_Y)
1657 # Warning - Rely in the order Blender adds verts
1658 # not nice design but wont change soon.
1660 bottom = geom.getFieldAsBool('bottom', True)
1661 side = geom.getFieldAsBool('side', True)
1663 if not bottom: # last vert is on the bottom
1664 bpymesh.verts.delete([GLOBALS['CIRCLE_DETAIL']+1])
1665 if not side: # second last vert is on the pointy bit of the cone
1666 bpymesh.verts.delete([GLOBALS['CIRCLE_DETAIL']])
1670 def importMesh_Box(geom):
1671 # bpymesh = bpy.data.meshes.new()
1673 size = geom.getFieldAsFloatTuple('size', (2.0, 2.0, 2.0))
1674 bpymesh = Mesh.Primitives.Cube(1.0)
1676 # Scale the box to the size set
1677 scale_mat = Matrix([size[0],0,0], [0, size[1], 0], [0, 0, size[2]])
1678 bpymesh.transform(scale_mat.resize4x4())
1682 def importShape(node, ancestry):
1683 vrmlname = node.getDefName()
1684 if not vrmlname: vrmlname = 'Shape'
1686 # works 100% in vrml, but not x3d
1687 #appr = node.getChildByName('appearance') # , 'Appearance'
1688 #geom = node.getChildByName('geometry') # , 'IndexedFaceSet'
1690 # Works in vrml and x3d
1691 appr = node.getChildBySpec('Appearance')
1692 geom = node.getChildBySpec(['IndexedFaceSet', 'IndexedLineSet', 'PointSet', 'Sphere', 'Box', 'Cylinder', 'Cone'])
1694 # For now only import IndexedFaceSet's
1700 depth = 0 # so we can set alpha face flag later
1704 #mat = appr.getChildByName('material') # 'Material'
1705 #ima = appr.getChildByName('texture') # , 'ImageTexture'
1706 #if ima and ima.getSpec() != 'ImageTexture':
1707 # print '\tWarning: texture type "%s" is not supported' % ima.getSpec()
1709 # textx = appr.getChildByName('textureTransform')
1711 mat = appr.getChildBySpec('Material')
1712 ima = appr.getChildBySpec('ImageTexture')
1714 textx = appr.getChildBySpec('TextureTransform')
1717 texmtx = translateTexTransform(textx)
1725 mat = ima # This is a bit dumb, but just means we use default values for all
1727 # all values between 0.0 and 1.0, defaults from VRML docs
1728 bpymat = bpy.data.materials.new()
1729 bpymat.amb = mat.getFieldAsFloat('ambientIntensity', 0.2)
1730 bpymat.rgbCol = mat.getFieldAsFloatTuple('diffuseColor', [0.8, 0.8, 0.8])
1732 # NOTE - blender dosnt support emmisive color
1733 # Store in mirror color and approximate with emit.
1734 emit = mat.getFieldAsFloatTuple('emissiveColor', [0.0, 0.0, 0.0])
1735 bpymat.mirCol = emit
1736 bpymat.emit = (emit[0]+emit[1]+emit[2])/3.0
1738 bpymat.hard = int(1+(510*mat.getFieldAsFloat('shininess', 0.2))) # 0-1 -> 1-511
1739 bpymat.specCol = mat.getFieldAsFloatTuple('specularColor', [0.0, 0.0, 0.0])
1740 bpymat.alpha = 1.0 - mat.getFieldAsFloat('transparency', 0.0)
1741 if bpymat.alpha < 0.999:
1742 bpymat.mode |= Material.Modes.ZTRANSP
1747 ima_url = ima.getFieldAsString('url')
1750 try: ima_url = ima.getFieldAsStringArray('url')[0] # in some cases we get a list of images.
1751 except: ima_url = None
1754 print "\twarning, image with no URL, this is odd"
1756 bpyima= BPyImage.comprehensiveImageLoad(ima_url, dirName(node.getFilename()), PLACE_HOLDER= False, RECURSIVE= False, CONVERT_CALLBACK= imageConvertCompat)
1758 texture= bpy.data.textures.new()
1759 texture.setType('Image')
1760 texture.image = bpyima
1762 # Adds textures for materials (rendering)
1763 try: depth = bpyima.depth
1768 bpymat.setTexture(0, texture, Texture.TexCo.UV, Texture.MapTo.COL | Texture.MapTo.ALPHA)
1769 texture.setImageFlags('MipMap', 'InterPol', 'UseAlpha')
1770 bpymat.mode |= Material.Modes.ZTRANSP
1773 bpymat.setTexture(0, texture, Texture.TexCo.UV, Texture.MapTo.COL)
1775 ima_repS = ima.getFieldAsBool('repeatS', True)
1776 ima_repT = ima.getFieldAsBool('repeatT', True)
1778 # To make this work properly we'd need to scale the UV's too, better to ignore th
1779 # texture.repeat = max(1, ima_repS * 512), max(1, ima_repT * 512)
1781 if not ima_repS: bpyima.clampX = True
1782 if not ima_repT: bpyima.clampY = True
1785 geom_spec = geom.getSpec()
1787 if geom_spec == 'IndexedFaceSet':
1788 bpydata, ccw = importMesh_IndexedFaceSet(geom, bpyima)
1789 elif geom_spec == 'IndexedLineSet':
1790 bpydata = importMesh_IndexedLineSet(geom)
1791 elif geom_spec == 'PointSet':
1792 bpydata = importMesh_PointSet(geom)
1793 elif geom_spec == 'Sphere':
1794 bpydata = importMesh_Sphere(geom)
1795 elif geom_spec == 'Box':
1796 bpydata = importMesh_Box(geom)
1797 elif geom_spec == 'Cylinder':
1798 bpydata = importMesh_Cylinder(geom)
1799 elif geom_spec == 'Cone':
1800 bpydata = importMesh_Cone(geom)
1802 print '\tWarning: unsupported type "%s"' % geom_spec
1806 vrmlname = vrmlname + geom_spec
1808 bpydata.name = vrmlname
1810 bpyob = node.blendObject = bpy.data.scenes.active.objects.new(bpydata)
1812 if type(bpydata) == Types.MeshType:
1813 is_solid = geom.getFieldAsBool('solid', True)
1814 creaseAngle = geom.getFieldAsFloat('creaseAngle', None)
1816 if creaseAngle != None:
1817 bpydata.maxSmoothAngle = 1+int(min(79, creaseAngle * RAD_TO_DEG))
1818 bpydata.mode |= Mesh.Modes.AUTOSMOOTH
1820 # Only ever 1 material per shape
1821 if bpymat: bpydata.materials = [bpymat]
1825 if depth==32: # set the faces alpha flag?
1826 transp = Mesh.FaceTranspModes.ALPHA
1827 for f in bpydata.faces:
1831 # Apply texture transform?
1833 for f in bpydata.faces:
1838 uv.x, uv.y = (uv_copy * texmtx)[0:2]
1839 # Done transforming the texture
1842 # Must be here and not in IndexedFaceSet because it needs an object for the flip func. Messy :/
1843 if not ccw: bpydata.flipNormals()
1846 # else could be a curve for example
1850 # Can transform data or object, better the object so we can instance the data
1851 #bpymesh.transform(getFinalMatrix(node))
1852 bpyob.setMatrix( getFinalMatrix(node, None, ancestry) )
1855 def importLamp_PointLight(node):
1856 vrmlname = node.getDefName()
1857 if not vrmlname: vrmlname = 'PointLight'
1859 # ambientIntensity = node.getFieldAsFloat('ambientIntensity', 0.0) # TODO
1860 # attenuation = node.getFieldAsFloatTuple('attenuation', (1.0, 0.0, 0.0)) # TODO
1861 color = node.getFieldAsFloatTuple('color', (1.0, 1.0, 1.0))
1862 intensity = node.getFieldAsFloat('intensity', 1.0) # max is documented to be 1.0 but some files have higher.
1863 location = node.getFieldAsFloatTuple('location', (0.0, 0.0, 0.0))
1864 # is_on = node.getFieldAsBool('on', True) # TODO
1865 radius = node.getFieldAsFloat('radius', 100.0)
1867 bpylamp = bpy.data.lamps.new()
1868 bpylamp.setType('Lamp')
1869 bpylamp.energy = intensity
1870 bpylamp.dist = radius
1873 mtx = TranslationMatrix(Vector(location))
1877 def importLamp_DirectionalLight(node):
1878 vrmlname = node.getDefName()
1879 if not vrmlname: vrmlname = 'DirectLight'
1881 # ambientIntensity = node.getFieldAsFloat('ambientIntensity', 0.0) # TODO
1882 color = node.getFieldAsFloatTuple('color', (1.0, 1.0, 1.0))
1883 direction = node.getFieldAsFloatTuple('direction', (0.0, 0.0, -1.0))
1884 intensity = node.getFieldAsFloat('intensity', 1.0) # max is documented to be 1.0 but some files have higher.
1885 # is_on = node.getFieldAsBool('on', True) # TODO
1887 bpylamp = bpy.data.lamps.new(vrmlname)
1888 bpylamp.setType('Sun')
1889 bpylamp.energy = intensity
1892 # lamps have their direction as -z, yup
1893 mtx = Vector(direction).toTrackQuat('-z', 'y').toMatrix().resize4x4()
1897 # looks like default values for beamWidth and cutOffAngle were swapped in VRML docs.
1899 def importLamp_SpotLight(node):
1900 vrmlname = node.getDefName()
1901 if not vrmlname: vrmlname = 'SpotLight'
1903 # ambientIntensity = geom.getFieldAsFloat('ambientIntensity', 0.0) # TODO
1904 # attenuation = geom.getFieldAsFloatTuple('attenuation', (1.0, 0.0, 0.0)) # TODO
1905 beamWidth = node.getFieldAsFloat('beamWidth', 1.570796) * RAD_TO_DEG # max is documented to be 1.0 but some files have higher.
1906 color = node.getFieldAsFloatTuple('color', (1.0, 1.0, 1.0))
1907 cutOffAngle = node.getFieldAsFloat('cutOffAngle', 0.785398) * RAD_TO_DEG # max is documented to be 1.0 but some files have higher.
1908 direction = node.getFieldAsFloatTuple('direction', (0.0, 0.0, -1.0))
1909 intensity = node.getFieldAsFloat('intensity', 1.0) # max is documented to be 1.0 but some files have higher.
1910 location = node.getFieldAsFloatTuple('location', (0.0, 0.0, 0.0))
1911 # is_on = node.getFieldAsBool('on', True) # TODO
1912 radius = node.getFieldAsFloat('radius', 100.0)
1914 bpylamp = bpy.data.lamps.new(vrmlname)
1915 bpylamp.setType('Spot')
1916 bpylamp.energy = intensity
1917 bpylamp.dist = radius
1919 bpylamp.spotSize = cutOffAngle
1920 if beamWidth > cutOffAngle:
1921 bpylamp.spotBlend = 0.0
1923 if cutOffAngle==0.0: #@#$%^&*(!!! - this should never happen
1924 bpylamp.spotBlend = 0.5
1926 bpylamp.spotBlend = beamWidth / cutOffAngle
1930 # lamps have their direction as -z, y==up
1931 mtx = Vector(direction).toTrackQuat('-z', 'y').toMatrix().resize4x4() * TranslationMatrix(Vector(location))
1936 def importLamp(node, spec, ancestry):
1937 if spec=='PointLight':
1938 bpylamp,mtx = importLamp_PointLight(node)
1939 elif spec=='DirectionalLight':
1940 bpylamp,mtx = importLamp_DirectionalLight(node)
1941 elif spec=='SpotLight':
1942 bpylamp,mtx = importLamp_SpotLight(node)
1944 print "Error, not a lamp"
1947 bpyob = node.blendObject = bpy.data.scenes.active.objects.new(bpylamp)
1948 bpyob.setMatrix( getFinalMatrix(node, mtx, ancestry) )
1951 def importViewpoint(node, ancestry):
1952 name = node.getDefName()
1953 if not name: name = 'Viewpoint'
1955 fieldOfView = node.getFieldAsFloat('fieldOfView', 0.785398) * RAD_TO_DEG # max is documented to be 1.0 but some files have higher.
1956 # jump = node.getFieldAsBool('jump', True)
1957 orientation = node.getFieldAsFloatTuple('orientation', (0.0, 0.0, 1.0, 0.0))
1958 position = node.getFieldAsFloatTuple('position', (0.0, 0.0, 0.0))
1959 description = node.getFieldAsString('description', '')
1961 bpycam = bpy.data.cameras.new(name)
1963 bpycam.angle = fieldOfView
1965 mtx = translateRotation(orientation) * TranslationMatrix(Vector(position))
1968 bpyob = node.blendObject = bpy.data.scenes.active.objects.new(bpycam)
1969 bpyob.setMatrix( getFinalMatrix(node, mtx, ancestry) )
1972 def importTransform(node, ancestry):
1973 name = node.getDefName()
1974 if not name: name = 'Transform'
1976 bpyob = node.blendObject = bpy.data.scenes.active.objects.new('Empty', name) # , name)
1977 bpyob.setMatrix( getFinalMatrix(node, None, ancestry) )
1980 #def importTimeSensor(node):
1983 def translatePositionInterpolator(node, ipo):
1984 key = node.getFieldAsArray('key', 0)
1985 keyValue = node.getFieldAsArray('keyValue', 3)
1987 loc_x = ipo.addCurve('LocX')
1988 loc_y = ipo.addCurve('LocY')
1989 loc_z = ipo.addCurve('LocZ')
1991 loc_x.interpolation = loc_y.interpolation = loc_z.interpolation = Blender.IpoCurve.InterpTypes.LINEAR
1993 for i, time in enumerate(key):
1996 loc_x.append((time,x))
1997 loc_y.append((time,y))
1998 loc_z.append((time,z))
2000 def translateOrientationInterpolator(node, ipo):
2001 key = node.getFieldAsArray('key', 0)
2002 keyValue = node.getFieldAsArray('keyValue', 4)
2004 rot_x = ipo.addCurve('RotX')
2005 rot_y = ipo.addCurve('RotY')
2006 rot_z = ipo.addCurve('RotZ')
2008 rot_x.interpolation = rot_y.interpolation = rot_z.interpolation = Blender.IpoCurve.InterpTypes.LINEAR
2010 for i, time in enumerate(key):
2012 mtx = translateRotation(keyValue[i])
2014 rot_x.append((time,eul.x/10.0))
2015 rot_y.append((time,eul.y/10.0))
2016 rot_z.append((time,eul.z/10.0))
2019 def translateScalarInterpolator(node, ipo):
2020 key = node.getFieldAsArray('key', 0)
2021 keyValue = node.getFieldAsArray('keyValue', 4)
2023 sca_x = ipo.addCurve('SizeX')
2024 sca_y = ipo.addCurve('SizeY')
2025 sca_z = ipo.addCurve('SizeZ')
2027 sca_x.interpolation = sca_y.interpolation = sca_z.interpolation = Blender.IpoCurve.InterpTypes.LINEAR
2029 for i, time in enumerate(key):
2031 sca_x.append((time,x/10.0))
2032 sca_y.append((time,y/10.0))
2033 sca_z.append((time,z/10.0))
2035 def translateTimeSensor(node, ipo):
2037 Apply a time sensor to an IPO, VRML has many combinations of loop/start/stop/cycle times
2038 to give different results, for now just do the basics
2041 time_cu = ipo.addCurve('Time')
2042 time_cu.interpolation = Blender.IpoCurve.InterpTypes.LINEAR
2044 cycleInterval = node.getFieldAsFloat('cycleInterval', None)
2046 startTime = node.getFieldAsFloat('startTime', 0.0)
2047 stopTime = node.getFieldAsFloat('stopTime', 250.0)
2049 if cycleInterval != None:
2050 stopTime = startTime+cycleInterval
2052 loop = node.getFieldAsBool('loop', False)
2054 time_cu.append((1+startTime, 0.0))
2055 time_cu.append((1+stopTime, 1.0/10.0))# anoying, the UI uses /10
2059 time_cu.extend = Blender.IpoCurve.ExtendTypes.CYCLIC # or - EXTRAP, CYCLIC_EXTRAP, CONST,
2062 def importRoute(node):
2064 Animation route only at the moment
2067 routeIpoDict = node.getRouteIpoDict()
2070 try: ipo = routeIpoDict[id]
2071 except: ipo = routeIpoDict[id] = bpy.data.ipos.new('web3d_ipo', 'Object')
2074 # for getting definitions
2075 defDict = node.getDefDict()
2077 Handles routing nodes to eachother
2079 ROUTE vpPI.value_changed TO champFly001.set_position
2080 ROUTE vpOI.value_changed TO champFly001.set_orientation
2081 ROUTE vpTs.fraction_changed TO vpPI.set_fraction
2082 ROUTE vpTs.fraction_changed TO vpOI.set_fraction
2083 ROUTE champFly001.bindTime TO vpTs.set_startTime
2086 #from_id, from_type = node.id[1].split('.')
2087 #to_id, to_type = node.id[3].split('.')
2090 set_position_node = None
2091 set_orientation_node = None
2094 for field in node.fields:
2095 if field and field[0]=='ROUTE':
2096 from_id, from_type = field[1].split('.')
2097 to_id, to_type = field[3].split('.')
2099 if from_type == 'value_changed':
2100 if to_type == 'set_position':
2102 set_data_from_node = defDict[from_id]
2103 translatePositionInterpolator(set_data_from_node, ipo)
2105 if to_type == 'set_orientation':
2107 set_data_from_node = defDict[from_id]
2108 translateOrientationInterpolator(set_data_from_node, ipo)
2110 if to_type == 'set_scale':
2112 set_data_from_node = defDict[from_id]
2113 translateScalarInterpolator(set_data_from_node, ipo)
2115 elif from_type == 'bindTime':
2116 ipo = getIpo(from_id)
2117 time_node = defDict[to_id]
2118 translateTimeSensor(time_node, ipo)
2123 def load_web3d(path, PREF_FLAT=False, PREF_CIRCLE_DIV=16, HELPER_FUNC = None):
2125 # Used when adding blender primitives
2126 GLOBALS['CIRCLE_DETAIL'] = PREF_CIRCLE_DIV
2128 #root_node = vrml_parse('/_Cylinder.wrl')
2129 if path.lower().endswith('.x3d'):
2130 root_node, msg = x3d_parse(path)
2132 root_node, msg = vrml_parse(path)
2135 if Blender.mode == 'background':
2138 Blender.Draw.PupMenu(msg)
2142 # fill with tuples - (node, [parents-parent, parent])
2143 all_nodes = root_node.getSerialized([], [])
2145 for node, ancestry in all_nodes:
2146 #if 'castle.wrl' not in node.getFilename():
2149 spec = node.getSpec()
2151 importShape(node, ancestry)
2152 elif spec in ('PointLight', 'DirectionalLight', 'SpotLight'):
2153 importLamp(node, spec, ancestry)
2154 elif spec=='Viewpoint':
2155 importViewpoint(node, ancestry)
2156 elif spec=='Transform':
2157 # Only use transform nodes when we are not importing a flat object hierarchy
2158 if PREF_FLAT==False:
2159 importTransform(node, ancestry)
2161 # These are delt with later within importRoute
2162 elif spec=='PositionInterpolator':
2163 ipo = bpy.data.ipos.new('web3d_ipo', 'Object')
2164 translatePositionInterpolator(node, ipo)
2168 # Note, include this function so the VRML/X3D importer can be extended
2169 # by an external script.
2171 HELPER_FUNC(node, ancestry)
2175 # After we import all nodes, route events - anim paths
2176 for node, ancestry in all_nodes:
2179 for node, ancestry in all_nodes:
2181 # we know that all nodes referenced from will be in
2182 # routeIpoDict so no need to run node.getDefDict() for every node.
2183 routeIpoDict = node.getRouteIpoDict()
2184 defDict = node.getDefDict()
2186 for key, ipo in routeIpoDict.iteritems():
2188 # Assign anim curves
2190 if node.blendObject==None: # Add an object if we need one for animation
2191 node.blendObject = bpy.data.scenes.active.objects.new('Empty', 'AnimOb') # , name)
2193 node.blendObject.setIpo(ipo)
2198 if PREF_FLAT==False:
2200 for node, ancestry in all_nodes:
2201 if node.blendObject:
2204 # Get the last parent
2208 blendObject = ancestry[i].blendObject
2213 # Parent Slow, - 1 liner but works
2214 # blendObject.makeParent([node.blendObject], 0, 1)
2217 try: child_dict[blendObject].append(node.blendObject)
2218 except: child_dict[blendObject] = [node.blendObject]
2221 for parent, children in child_dict.iteritems():
2222 parent.makeParent(children, 0, 1)
2225 bpy.data.scenes.active.update(1)
2231 PREF_HIERARCHY= Draw.Create(0)
2232 PREF_CIRCLE_DIV= Draw.Create(16)
2237 ('Hierarchy', PREF_HIERARCHY, 'Import transform nodes as empties to create a parent/child hierarchy'),\
2238 ('Circle Div:', PREF_CIRCLE_DIV, 3, 128, 'Number of divisions to use for circular primitives')
2241 if not Draw.PupBlock('Import X3D/VRML...', pup_block):
2244 Window.WaitCursor(1)
2247 (not PREF_HIERARCHY.val),\
2248 PREF_CIRCLE_DIV.val,\
2251 Window.WaitCursor(0)
2254 if __name__ == '__main__':
2255 Window.FileSelector(load_ui, 'Import X3D/VRML97')
2260 # load_web3d('/test.x3d')
2261 # load_web3d('/_Cylinder.x3d')
2264 # load_web3d('m:\\root\\Desktop\\_Cylinder.wrl')
2265 # load_web3d('/_Cylinder.wrl')
2266 # load_web3d('/fe/wrl/Vrml/EGS/BCKGD.WRL')
2268 # load_web3d('/fe/wrl/Vrml/EGS/GRNDPLNE.WRL')
2269 # load_web3d('/fe/wrl/Vrml/EGS/INDEXFST.WRL')
2270 # load_web3d('/fe/wrl/panel1c.wrl')
2271 # load_web3d('/test.wrl')
2272 # load_web3d('/fe/wrl/dulcimer.wrl')
2273 # load_web3d('/fe/wrl/rccad/Ju-52.wrl') # Face index out of range
2274 # load_web3d('/fe/wrl/16lat.wrl') # spotlight
2275 # load_web3d('/fe/wrl/Vrml/EGS/FOG.WRL') # spotlight
2276 # load_web3d('/fe/wrl/Vrml/EGS/LOD.WRL') # vcolor per face
2278 # load_web3d('/fe/wrl/new/daybreak_final.wrl') # no faces in mesh, face duplicate error
2279 # load_web3d('/fe/wrl/new/earth.wrl')
2280 # load_web3d('/fe/wrl/new/hendrix.ei.dtu.dk/vrml/talairach/fourd/TalaDruryRight.wrl') # define/use fields
2281 # load_web3d('/fe/wrl/new/imac.wrl') # extrusion and define/use fields, face index is a float somehow
2282 # load_web3d('/fe/wrl/new/www.igs.net/~mascott/vrml/vrml2/mcastle.wrl')
2283 # load_web3d('/fe/wrl/new/www.igs.net/~mascott/vrml/vrml2/tower.wrl')
2284 # load_web3d('/fe/wrl/new/www.igs.net/~mascott/vrml/vrml2/temple.wrl')
2285 # load_web3d('/fe/wrl/brain.wrl') # field define test 'a IS b'
2286 # load_web3d('/fe/wrl/new/coaster.wrl') # fields that are confusing to read.
2290 # load_web3d('/fe/x3d/www.web3d.org/x3d/content/examples/Basic/StudentProjects/PlayRoom.x3d') # invalid UVs
2294 # files = os.popen('find /fe/wrl -iname "*.wrl"').readlines()
2295 # files = os.popen('find /fe/x3d -iname "*.x3d"').readlines()
2296 files = os.popen('find /fe/x3d/X3dExamplesSavage -iname "*.x3d"').readlines()
2300 for i, f in enumerate(files):
2301 if i < 12803 or i > 1000000:
2308 sce = bpy.data.scenes.new(f.split('/')[-1])
2309 bpy.data.scenes.active = sce
2311 load_web3d(f, PREF_FLAT=True)