1 # SPDX-License-Identifier: GPL-2.0-or-later
5 # This should work without a blender at all
9 from math
import sin
, cos
, pi
10 from itertools
import chain
15 EPSILON
= 0.0000001 # Very crude.
18 def imageConvertCompat(path
):
21 return path
# assume win32 has quicktime, dont convert
23 if path
.lower().endswith('.gif'):
24 path_to
= path
[:-3] + 'png'
30 # print('\n'+path+'\n'+path_to+'\n')
31 os
.system('convert "%s" "%s"' % (path
, path_to
)) # for now just hope we have image magick
33 if os
.path
.exists(path_to
):
39 # transform are relative
40 # order doesn't matter for loc/size/rot
41 # right handed rotation
42 # angles are in radians
43 # rotation first defines axis then amount in radians
46 # =============================== VRML Specific
48 def vrml_split_fields(value
):
50 key 0.0 otherkey 1,2,3 opt1 opt1 0.0
51 -> [key 0.0], [otherkey 1,2,3], [opt1 opt1 0.0]
54 if k
[0] != '"' and k
[0].isalpha() and k
.upper() not in {'TRUE', 'FALSE'}:
64 field_context_len
= len(field_context
)
65 if (field_context_len
> 2) and (field_context
[-2] in {'DEF', 'USE'}):
66 field_context
.append(v
)
67 elif (not iskey(field_context
[-1])) or ((field_context_len
== 3 and field_context
[1] == 'IS')):
68 # this IS a key but the previous value was not a key, or it was a defined field.
69 field_list
.append(field_context
)
72 # The last item was not a value, multiple keys are needed in some cases.
73 field_context
.append(v
)
75 # Is empty, just add this on
76 field_context
.append(v
)
78 # Add a value to the list
79 field_context
.append(v
)
82 field_list
.append(field_context
)
89 Keep this as a valid vrml file, but format in a way we can predict.
91 # Strip all comments - # not in strings - warning multiline strings are ignored.
93 #l = ' '.join(l.split())
104 # Most cases accounted for! if we have a comment at the end of the line do this...
108 if j
== -1: # simple no strings
112 for i
, c
in enumerate(l
):
122 data
= '\n'.join([strip_comment(l
) for l
in data
.split('\n')]) # remove all whitespace
124 EXTRACT_STRINGS
= True # only needed when strings or filename contains ,[]{} chars :/
128 # We need this so we can detect URL's
129 data
= '\n'.join([' '.join(l
.split()) for l
in data
.split('\n')]) # remove all whitespace
140 i
= data
.find(search
, last_i
)
143 start
= i
+ len(search
) # first char after end of search
144 end
= data
.find('"', start
)
146 item
= data
[start
:end
]
147 string_ls
.append(item
)
148 data
= data
[:start
] + data
[end
:]
149 ok
= True # keep looking
151 last_i
= (end
- len(item
)) + 1
152 # print(last_i, item, '|' + data[last_i] + '|')
154 # done with messy extracting strings part
156 # Bad, dont take strings into account
158 data = data.replace('#', '\n#')
159 data = '\n'.join([ll for l in data.split('\n') for ll in (l.strip(),) if not ll.startswith('#')]) # remove all whitespace
161 data
= data
.replace('{', '\n{\n')
162 data
= data
.replace('}', '\n}\n')
163 data
= data
.replace('[', '\n[\n')
164 data
= data
.replace(']', '\n]\n')
165 data
= data
.replace(',', ' , ') # make sure comma's separate
167 # We need to write one property (field) per line only, otherwise we fail later to detect correctly new nodes.
168 # See T45195 for details.
169 data
= '\n'.join([' '.join(value
) for l
in data
.split('\n') for value
in vrml_split_fields(l
.split())])
172 # add strings back in
174 search
= '"' # fill in these empty strings
180 i
= data
.find(search
+ '"', last_i
)
183 start
= i
+ len(search
) # first char after end of search
184 item
= string_ls
.pop(0)
186 data
= data
[:start
] + item
+ data
[start
:]
188 last_i
= start
+ len(item
) + 1
192 # More annoying obscure cases where USE or DEF are placed on a newline
193 # data = data.replace('\nDEF ', ' DEF ')
194 # data = data.replace('\nUSE ', ' USE ')
196 data
= '\n'.join([' '.join(l
.split()) for l
in data
.split('\n')]) # remove all whitespace
198 # Better to parse the file accounting for multiline arrays
200 data = data.replace(',\n', ' , ') # remove line endings with commas
201 data = data.replace(']', '\n]\n') # very very annoying - but some comma's are at the end of the list, must run this again.
204 return [l
for l
in data
.split('\n') if l
]
208 NODE_REFERENCE
= 3 # USE foobar
214 def getNodePreText(i
, words
):
217 while len(words
) < 5:
222 elif lines[i].startswith('PROTO'):
223 return NODE_PROTO, i+1
225 elif lines
[i
] == '{':
226 # words.append(lines[i]) # no need
228 return NODE_NORMAL
, i
+ 1
229 elif lines
[i
].count('"') % 2 != 0: # odd number of quotes? - part of a string.
233 new_words
= lines
[i
].split()
234 if 'USE' in new_words
:
237 words
.extend(new_words
)
240 # Check for USE node - no {
241 # USE #id - should always be on the same line.
243 # print('LINE', i, words[:words.index('USE')+2])
244 words
[:] = words
[:words
.index('USE') + 2]
245 if lines
[i
] == '{' and lines
[i
+ 1] == '}':
246 # USE sometimes has {} after it anyway
248 return NODE_REFERENCE
, i
250 # print("error value!!!", words)
254 def is_nodeline(i
, words
):
256 if not lines
[i
][0].isalpha():
259 #if lines[i].startswith('field'):
262 # Is this a prototype??
263 if lines
[i
].startswith('PROTO'):
264 words
[:] = lines
[i
].split()
265 return NODE_NORMAL
, i
+ 1 # TODO - assumes the next line is a '[\n', skip that
266 if lines
[i
].startswith('EXTERNPROTO'):
267 words
[:] = lines
[i
].split()
268 return NODE_ARRAY
, i
+ 1 # TODO - assumes the next line is a '[\n', skip that
271 proto_type, new_i = is_protoline(i, words, proto_field_defs)
273 return proto_type, new_i
276 # Simple "var [" type
277 if lines
[i
+ 1] == '[':
278 if lines
[i
].count('"') % 2 == 0:
279 words
[:] = lines
[i
].split()
280 return NODE_ARRAY
, i
+ 2
282 node_type
, new_i
= getNodePreText(i
, words
)
286 print("not node_type", lines
[i
])
289 # Ok, we have a { after some values
290 # Check the values are not fields
291 for i
, val
in enumerate(words
):
292 if i
!= 0 and words
[i
- 1] in {'DEF', 'USE'}:
293 # ignore anything after DEF, it is a ID and can contain any chars.
295 elif val
[0].isalpha() and val
not in {'TRUE', 'FALSE'}:
298 # There is a number in one of the values, therefor we are not a node.
301 #if node_type==NODE_REFERENCE:
302 # print(words, "REF_!!!!!!!")
303 return node_type
, new_i
308 Does this line start with a number?
311 # Works but too slow.
332 if l
.startswith(', '):
335 line_end
= len(l
) - 1
336 line_end_new
= l
.find(' ', line_start
) # comma's always have a space before them
338 if line_end_new
!= -1:
339 line_end
= line_end_new
342 float(l
[line_start
:line_end
]) # works for a float or int
348 class vrmlNode(object):
365 'ROUTE_IPO_NAMESPACE',
370 def __init__(self
, parent
, node_type
, lineno
):
372 self
.node_type
= node_type
374 self
.blendObject
= None
375 self
.blendData
= None
376 self
.x3dNode
= None # for x3d import only
377 self
.parsed
= None # We try to reuse objects in a smart way
379 parent
.children
.append(self
)
383 # This is only set from the root nodes.
384 # Having a filename also denotes a root node
386 self
.proto_node
= None # proto field definition eg: "field SFColor seatColor .6 .6 .1"
388 # Store in the root node because each inline file needs its own root node and its own namespace
389 self
.DEF_NAMESPACE
= None
390 self
.ROUTE_IPO_NAMESPACE
= None
392 self.FIELD_NAMESPACE = None
395 self
.PROTO_NAMESPACE
= None
397 self
.reference
= None
399 if node_type
== NODE_REFERENCE
:
400 # For references, only the parent and ID are needed
401 # the reference its self is assigned on parsing
404 self
.fields
= [] # fields have no order, in some cases rool level values are not unique so dont use a dict
406 self
.proto_field_defs
= [] # proto field definition eg: "field SFColor seatColor .6 .6 .1"
407 self
.proto_fields
= [] # proto field usage "diffuseColor IS seatColor"
409 self
.array_data
= [] # use for arrays of data - should only be for NODE_ARRAY types
411 # Only available from the root node
413 def getFieldDict(self):
414 if self.FIELD_NAMESPACE is not None:
415 return self.FIELD_NAMESPACE
417 return self.parent.getFieldDict()
419 def getProtoDict(self
):
420 if self
.PROTO_NAMESPACE
is not None:
421 return self
.PROTO_NAMESPACE
423 return self
.parent
.getProtoDict()
425 def getDefDict(self
):
426 if self
.DEF_NAMESPACE
is not None:
427 return self
.DEF_NAMESPACE
429 return self
.parent
.getDefDict()
431 def getRouteIpoDict(self
):
432 if self
.ROUTE_IPO_NAMESPACE
is not None:
433 return self
.ROUTE_IPO_NAMESPACE
435 return self
.parent
.getRouteIpoDict()
437 def setRoot(self
, filename
):
438 self
.filename
= filename
439 # self.FIELD_NAMESPACE = {}
440 self
.DEF_NAMESPACE
= {}
441 self
.ROUTE_IPO_NAMESPACE
= {}
442 self
.PROTO_NAMESPACE
= {}
445 if self
.filename
is None:
450 def getFilename(self
):
454 return self
.parent
.getFilename()
458 def getRealNode(self
):
460 return self
.reference
465 self_real
= self
.getRealNode()
467 return self_real
.id[-1] # its possible this node has no spec
471 def findSpecRecursive(self
, spec
):
472 self_real
= self
.getRealNode()
473 if spec
== self_real
.getSpec():
476 for child
in self_real
.children
:
477 if child
.findSpecRecursive(spec
):
487 def getSpecialTypeName(self
, typename
):
488 self_real
= self
.getRealNode()
490 return self_real
.id[list(self_real
.id).index(typename
) + 1]
494 def getDefName(self
):
495 return self
.getSpecialTypeName('DEF')
497 def getProtoName(self
):
498 return self
.getSpecialTypeName('PROTO')
500 def getExternprotoName(self
):
501 return self
.getSpecialTypeName('EXTERNPROTO')
503 def getChildrenBySpec(self
, node_spec
): # spec could be Transform, Shape, Appearance
504 self_real
= self
.getRealNode()
505 # using getSpec functions allows us to use the spec of USE children that dont have their spec in their ID
506 if type(node_spec
) == str:
507 return [child
for child
in self_real
.children
if child
.getSpec() == node_spec
]
509 # Check inside a list of optional types
510 return [child
for child
in self_real
.children
if child
.getSpec() in node_spec
]
512 def getChildrenBySpecCondition(self
, cond
): # spec could be Transform, Shape, Appearance
513 self_real
= self
.getRealNode()
514 # using getSpec functions allows us to use the spec of USE children that dont have their spec in their ID
515 return [child
for child
in self_real
.children
if cond(child
.getSpec())]
517 def getChildBySpec(self
, node_spec
): # spec could be Transform, Shape, Appearance
518 # Use in cases where there is only ever 1 child of this type
519 ls
= self
.getChildrenBySpec(node_spec
)
525 def getChildBySpecCondition(self
, cond
): # spec could be Transform, Shape, Appearance
526 # Use in cases where there is only ever 1 child of this type
527 ls
= self
.getChildrenBySpecCondition(cond
)
533 def getChildrenByName(self
, node_name
): # type could be geometry, children, appearance
534 self_real
= self
.getRealNode()
535 return [child
for child
in self_real
.children
if child
.id if child
.id[0] == node_name
]
537 def getChildByName(self
, node_name
):
538 self_real
= self
.getRealNode()
539 for child
in self_real
.children
:
540 if child
.id and child
.id[0] == node_name
: # and child.id[-1]==node_spec:
543 def getSerialized(self
, results
, ancestry
):
544 """ Return this node and all its children in a flat list """
545 ancestry
= ancestry
[:] # always use a copy
547 # self_real = self.getRealNode()
549 results
.append((self
, tuple(ancestry
)))
550 ancestry
.append(self
)
551 for child
in self
.getRealNode().children
:
552 if child
not in ancestry
:
553 # We dont want to load proto's, they are only references
554 # We could enforce this elsewhere
556 # Only add this in a very special case
557 # where the parent of this object is not the real parent
558 # - In this case we have added the proto as a child to a node instancing it.
559 # This is a bit arbitrary, but its how Proto's are done with this importer.
560 if child
.getProtoName() is None and child
.getExternprotoName() is None:
561 child
.getSerialized(results
, ancestry
)
565 print('getSerialized() is proto:', child
.getProtoName(), child
.getExternprotoName(), self
.getSpec())
567 self_spec
= self
.getSpec()
569 if child
.getProtoName() == self_spec
or child
.getExternprotoName() == self_spec
:
572 child
.getSerialized(results
, ancestry
)
576 def searchNodeTypeID(self
, node_spec
, results
):
577 self_real
= self
.getRealNode()
578 # print(self.lineno, self.id)
579 if self_real
.id and self_real
.id[-1] == node_spec
: # use last element, could also be only element
580 results
.append(self_real
)
581 for child
in self_real
.children
:
582 child
.searchNodeTypeID(node_spec
, results
)
585 def getFieldName(self
, field
, ancestry
, AS_CHILD
=False, SPLIT_COMMAS
=False):
586 self_real
= self
.getRealNode() # in case we're an instance
588 for f
in self_real
.fields
:
590 if f
and f
[0] == field
:
591 # print('\tfound field', f)
593 if len(f
) >= 3 and f
[1] == 'IS': # eg: 'diffuseColor IS legColor'
596 # print("\n\n\n\n\n\nFOND IS!!!")
597 f_proto_lookup
= None
598 f_proto_child_lookup
= None
603 node
= node
.getRealNode()
605 # proto settings are stored in "self.proto_node"
607 # Get the default value from the proto, this can be overwritten by the proto instance
608 # 'field SFColor legColor .8 .4 .7'
610 for child
in node
.proto_node
.children
:
611 #if child.id and len(child.id) >= 3 and child.id[2]==field_id:
612 if child
.id and ('point' in child
.id or 'points' in child
.id):
613 f_proto_child_lookup
= child
616 for f_def
in node
.proto_node
.proto_field_defs
:
618 if f_def
[0] == 'field' and f_def
[2] == field_id
:
619 f_proto_lookup
= f_def
[3:]
621 # Node instance, Will be 1 up from the proto-node in the ancestry list. but NOT its parent.
622 # This is the setting as defined by the instance, including this setting is optional,
623 # and will override the default PROTO value
624 # eg: 'legColor 1 0 0'
626 for child
in node
.children
:
627 if child
.id and child
.id[0] == field_id
:
628 f_proto_child_lookup
= child
630 for f_def
in node
.fields
:
632 if f_def
[0] == field_id
:
634 print("getFieldName(), found proto", f_def
)
635 f_proto_lookup
= f_def
[1:]
638 if f_proto_child_lookup
:
640 print("getFieldName() - AS_CHILD=True, child found")
641 print(f_proto_child_lookup
)
642 return f_proto_child_lookup
644 return f_proto_lookup
651 # print('\tfield not found', field)
653 # See if this is a proto name
655 for child
in self_real
.children
:
656 if child
.id and len(child
.id) == 1 and child
.id[0] == field
:
661 def getFieldAsInt(self
, field
, default
, ancestry
):
662 self_real
= self
.getRealNode() # in case we're an instance
664 f
= self_real
.getFieldName(field
, ancestry
)
668 f
= f
[:f
.index(',')] # strip after the comma
671 print('\t"%s" wrong length for int conversion for field "%s"' % (f
, field
))
677 print('\tvalue "%s" could not be used as an int for field "%s"' % (f
[0], field
))
680 def getFieldAsFloat(self
, field
, default
, ancestry
):
681 self_real
= self
.getRealNode() # in case we're an instance
683 f
= self_real
.getFieldName(field
, ancestry
)
687 f
= f
[:f
.index(',')] # strip after the comma
690 print('\t"%s" wrong length for float conversion for field "%s"' % (f
, field
))
696 print('\tvalue "%s" could not be used as a float for field "%s"' % (f
[0], field
))
699 def getFieldAsFloatTuple(self
, field
, default
, ancestry
):
700 self_real
= self
.getRealNode() # in case we're an instance
702 f
= self_real
.getFieldName(field
, ancestry
)
705 # if ',' in f: f = f[:f.index(',')] # strip after the comma
708 print('"%s" wrong length for float tuple conversion for field "%s"' % (f
, field
))
717 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
723 print('\tvalue "%s" could not be used as a float tuple for field "%s"' % (f
, field
))
726 def getFieldAsBool(self
, field
, default
, ancestry
):
727 self_real
= self
.getRealNode() # in case we're an instance
729 f
= self_real
.getFieldName(field
, ancestry
)
733 f
= f
[:f
.index(',')] # strip after the comma
736 print('\t"%s" wrong length for bool conversion for field "%s"' % (f
, field
))
739 if f
[0].upper() == '"TRUE"' or f
[0].upper() == 'TRUE':
741 elif f
[0].upper() == '"FALSE"' or f
[0].upper() == 'FALSE':
744 print('\t"%s" could not be used as a bool for field "%s"' % (f
[1], field
))
747 def getFieldAsString(self
, field
, default
, ancestry
):
748 self_real
= self
.getRealNode() # in case we're an instance
750 f
= self_real
.getFieldName(field
, ancestry
)
754 print('\t"%s" wrong length for string conversion for field "%s"' % (f
, field
))
758 # String may contain spaces
767 if st
[0] == '"' and st
[-1] == '"':
770 print('\tvalue "%s" could not be used as a string for field "%s"' % (f
[0], field
))
773 def getFieldAsArray(self
, field
, group
, ancestry
):
775 For this parser arrays are children
778 def array_as_number(array_string
):
781 array_data
= [int(val
, 0) for val
in array_string
]
784 array_data
= [float(val
) for val
in array_string
]
786 print('\tWarning, could not parse array data from field')
790 self_real
= self
.getRealNode() # in case we're an instance
792 child_array
= self_real
.getFieldName(field
, ancestry
, True, SPLIT_COMMAS
=True)
794 #if type(child_array)==list: # happens occasionally
795 # array_data = child_array
797 if child_array
is None:
798 # For x3d, should work ok with vrml too
799 # for x3d arrays are fields, vrml they are nodes, annoying but not too bad.
800 data_split
= self
.getFieldName(field
, ancestry
, SPLIT_COMMAS
=True)
804 array_data
= array_as_number(data_split
)
806 elif type(child_array
) == list:
808 array_data
= array_as_number(child_array
)
812 array_data
= child_array
.array_data
814 # print('array_data', array_data)
815 if group
== -1 or len(array_data
) == 0:
818 # We want a flat list
820 for item
in array_data
:
821 if type(item
) == list:
827 flat_array
= array_data
# we are already flat.
833 if type(item
) == list:
836 flat_array
.append(item
)
838 extend_flat(array_data
)
840 # We requested a flat array
847 for item
in flat_array
:
848 sub_array
.append(item
)
849 if len(sub_array
) == group
:
850 new_array
.append(sub_array
)
854 print('\twarning, array was not aligned to requested grouping', group
, 'remaining value', sub_array
)
858 def getFieldAsStringArray(self
, field
, ancestry
):
860 Get a list of strings
862 self_real
= self
.getRealNode() # in case we're an instance
865 for child
in self_real
.children
:
866 if child
.id and len(child
.id) == 1 and child
.id[0] == field
:
872 # each string gets its own list, remove ""'s
874 new_array
= [f
[0][1:-1] for f
in child_array
.fields
]
876 print('\twarning, string array could not be made')
894 level
= self
.getLevel()
896 if self
.node_type
== NODE_REFERENCE
:
898 elif self
.node_type
== NODE_NORMAL
:
904 text
= ind
+ brackets
[0] + '\n'
908 text
+= ind
+ 'ID: ' + str(self
.id) + ' ' + str(level
) + (' lineno %d\n' % self
.lineno
)
910 if self
.node_type
== NODE_REFERENCE
:
911 text
+= ind
+ "(reference node)\n"
915 text
+= ind
+ 'PROTO NODE...\n'
916 text
+= str(self
.proto_node
)
917 text
+= ind
+ 'PROTO NODE_DONE\n'
919 text
+= ind
+ 'FIELDS:' + str(len(self
.fields
)) + '\n'
921 for i
, item
in enumerate(self
.fields
):
922 text
+= ind
+ 'FIELD:\n'
923 text
+= ind
+ str(item
) + '\n'
925 text
+= ind
+ 'PROTO_FIELD_DEFS:' + str(len(self
.proto_field_defs
)) + '\n'
927 for i
, item
in enumerate(self
.proto_field_defs
):
928 text
+= ind
+ 'PROTO_FIELD:\n'
929 text
+= ind
+ str(item
) + '\n'
931 text
+= ind
+ 'ARRAY: ' + str(len(self
.array_data
)) + ' ' + str(self
.array_data
) + '\n'
932 #text += ind + 'ARRAY: ' + str(len(self.array_data)) + '[...] \n'
934 text
+= ind
+ 'CHILDREN: ' + str(len(self
.children
)) + '\n'
935 for i
, child
in enumerate(self
.children
):
936 text
+= ind
+ ('CHILD%d:\n' % i
)
939 text
+= '\n' + ind
+ brackets
[1]
943 def parse(self
, i
, IS_PROTO_DATA
=False):
944 new_i
= self
.__parse
(i
, IS_PROTO_DATA
)
946 # print(self.id, self.getFilename())
948 # Check if this node was an inline or externproto
952 if self
.node_type
== NODE_NORMAL
and self
.getSpec() == 'Inline':
953 ancestry
= [] # Warning! - PROTO's using this wont work at all.
954 url
= self
.getFieldAsString('url', None, ancestry
)
956 url_ls
= [(url
, None)]
959 elif self
.getExternprotoName():
962 for f
in self
.fields
:
968 for f_split
in ff
.split('"'):
970 # "someextern.vrml#SomeID"
973 f_split
, f_split_id
= f_split
.split('#') # there should only be 1 # anyway
975 url_ls
.append((f_split
, f_split_id
))
977 url_ls
.append((f_split
, None))
979 # Was either an Inline or an EXTERNPROTO
984 for url
, extern_key
in url_ls
:
988 urls
.append(bpy
.path
.resolve_ncase(urls
[-1]))
990 urls
.append(os
.path
.join(os
.path
.dirname(self
.getFilename()), url
))
991 urls
.append(bpy
.path
.resolve_ncase(urls
[-1]))
993 urls
.append(os
.path
.join(os
.path
.dirname(self
.getFilename()), os
.path
.basename(url
)))
994 urls
.append(bpy
.path
.resolve_ncase(urls
[-1]))
997 url
= [url
for url
in urls
if os
.path
.exists(url
)][0]
1003 print('\tWarning: Inline URL could not be found:', url
)
1005 if url
== self
.getFilename():
1006 print('\tWarning: can\'t Inline yourself recursively:', url
)
1010 data
= gzipOpen(url
)
1012 print('\tWarning: can\'t open the file:', url
)
1016 # Tricky - inline another VRML
1017 print('\tLoading Inline:"%s"...' % url
)
1019 # Watch it! - backup lines
1020 lines_old
= lines
[:]
1022 lines
[:] = vrmlFormat(data
)
1024 lines
.insert(0, '{')
1025 lines
.insert(0, 'root_node____')
1028 ff = open('/tmp/test.txt', 'w')
1029 ff.writelines([l+'\n' for l in lines])
1032 child
= vrmlNode(self
, NODE_NORMAL
, -1)
1033 child
.setRoot(url
) # initialized dicts
1036 # if self.getExternprotoName():
1037 if self
.getExternprotoName():
1038 if not extern_key
: # if none is specified - use the name
1039 extern_key
= self
.getSpec()
1043 self
.children
.remove(child
)
1046 extern_child
= child
.findSpecRecursive(extern_key
)
1049 self
.children
.append(extern_child
)
1050 extern_child
.parent
= self
1053 print("\tEXTERNPROTO ID found!:", extern_key
)
1055 print("\tEXTERNPROTO ID not found!:", extern_key
)
1057 # Watch it! - restore lines
1058 lines
[:] = lines_old
1062 def __parse(self
, i
, IS_PROTO_DATA
=False):
1064 print('parsing at', i, end="")
1065 print(i, self.id, self.lineno)
1076 node_type
, new_i
= is_nodeline(i
, words
)
1077 if not node_type
: # fail for parsing new node.
1078 print("Failed to parse new node")
1081 if self
.node_type
== NODE_REFERENCE
:
1082 # Only assign the reference and quit
1083 key
= words
[words
.index('USE') + 1]
1084 self
.id = (words
[0],)
1086 self
.reference
= self
.getDefDict()[key
]
1089 self
.id = tuple(words
)
1092 key
= self
.getDefName()
1094 self
.getDefDict()[key
] = self
1096 key
= self
.getProtoName()
1098 key
= self
.getExternprotoName()
1100 proto_dict
= self
.getProtoDict()
1102 proto_dict
[key
] = self
1104 # Parse the proto nodes fields
1105 self
.proto_node
= vrmlNode(self
, NODE_ARRAY
, new_i
)
1106 new_i
= self
.proto_node
.parse(new_i
)
1108 self
.children
.remove(self
.proto_node
)
1110 # print(self.proto_node)
1112 new_i
+= 1 # skip past the {
1114 else: # If we're a proto instance, add the proto node as our child.
1115 spec
= self
.getSpec()
1117 self
.children
.append(proto_dict
[spec
])
1132 return len(lines
) - 1
1135 # print('\tDEBUG:', i, self.node_type, l)
1141 if self
.node_type
!= NODE_NORMAL
: # also ends proto nodes, we may want a type for these too.
1142 print('wrong node ending, expected an } ' + str(i
) + ' ' + str(self
.node_type
))
1145 ### print("returning", i)
1148 if self
.node_type
!= NODE_ARRAY
:
1149 print('wrong node ending, expected a ] ' + str(i
) + ' ' + str(self
.node_type
))
1152 ### print("returning", i)
1155 node_type
, new_i
= is_nodeline(i
, [])
1156 if node_type
: # check text\n{
1157 child
= vrmlNode(self
, node_type
, i
)
1160 elif l
== '[': # some files have these anonymous lists
1161 child
= vrmlNode(self
, NODE_ARRAY
, i
)
1165 l_split
= l
.split(',')
1168 # See if each item is a float?
1170 for num_type
in (int, float):
1172 values
= [num_type(v
) for v
in l_split
]
1178 values
= [[num_type(v
) for v
in segment
.split()] for segment
in l_split
]
1183 if values
is None: # dont parse
1186 # This should not extend over multiple lines however it is possible
1187 # print(self.array_data)
1189 self
.array_data
.extend(values
)
1193 if len(words
) > 2 and words
[1] == 'USE':
1194 vrmlNode(self
, NODE_REFERENCE
, i
)
1197 # print("FIELD", i, l)
1200 ### print('\t\ttag', i)
1202 # print(words, i, l)
1205 # javastrips can exist as values.
1206 quote_count
= l
.count('"')
1207 if quote_count
% 2: # odd number?
1208 # print('MULTILINE')
1212 quote_count
= l
.count('"')
1213 if quote_count
% 2: # odd number?
1214 value
+= '\n' + l
[:l
.rfind('"')]
1219 # use shlex so we get '"a b" "b v"' --> '"a b"', '"b v"'
1220 value_all
= shlex
.split(value
, posix
=False)
1222 for value
in vrml_split_fields(value_all
):
1225 if value
[0] == 'field':
1226 # field SFFloat creaseAngle 4
1227 self
.proto_field_defs
.append(value
)
1229 self
.fields
.append(value
)
1232 # This is a prerequisite for DEF/USE-based material caching
1233 def canHaveReferences(self
):
1234 return self
.node_type
== NODE_NORMAL
and self
.getDefName()
1236 # This is a prerequisite for raw XML-based material caching.
1237 # NOTE - crude, but working implementation for
1238 # material and texture caching, based on __repr__.
1239 # Doesn't do any XML, but is better than nothing.
1241 if "material" in self
.id or "texture" in self
.id:
1242 node
= self
.reference
if self
.node_type
== NODE_REFERENCE
else self
1243 return frozenset(line
.strip() for line
in repr(node
).strip().split("\n"))
1253 data
= gzip
.open(path
, 'r').read()
1259 filehandle
= open(path
, 'r', encoding
='utf-8', errors
='surrogateescape')
1260 data
= filehandle
.read()
1264 traceback
.print_exc()
1266 data
= data
.decode(encoding
='utf-8', errors
='surrogateescape')
1271 def vrml_parse(path
):
1273 Sets up the root node and returns it so load_web3d() can deal with the blender side of things.
1274 Return root (vrmlNode, '') or (None, 'Error String')
1276 data
= gzipOpen(path
)
1279 return None, 'Failed to open file: ' + path
1282 lines
[:] = vrmlFormat(data
)
1284 lines
.insert(0, '{')
1285 lines
.insert(0, 'dymmy_node')
1287 # Use for testing our parsed output, so we can check on line numbers.
1290 ff = open('/tmp/test.txt', 'w')
1291 ff.writelines([l+'\n' for l in lines])
1296 node_type
, new_i
= is_nodeline(0, [])
1298 return None, 'Error: VRML file has no starting Node'
1300 # Trick to make sure we get all root nodes.
1301 lines
.insert(0, '{')
1302 lines
.insert(0, 'root_node____') # important the name starts with an ascii char
1305 root
= vrmlNode(None, NODE_NORMAL
, -1)
1306 root
.setRoot(path
) # we need to set the root so we have a namespace and know the path in case of inlineing
1311 # This prints a load of text
1318 # ====================== END VRML
1320 # ====================== X3d Support
1322 # Sane as vrml but replace the parser
1323 class x3dNode(vrmlNode
):
1324 def __init__(self
, parent
, node_type
, x3dNode
):
1325 vrmlNode
.__init
__(self
, parent
, node_type
, -1)
1326 self
.x3dNode
= x3dNode
1328 def parse(self
, IS_PROTO_DATA
=False):
1329 # print(self.x3dNode.tagName)
1330 self
.lineno
= self
.x3dNode
.parse_position
[0]
1332 define
= self
.x3dNode
.getAttributeNode('DEF')
1334 self
.getDefDict()[define
.value
] = self
1336 use
= self
.x3dNode
.getAttributeNode('USE')
1339 self
.reference
= self
.getDefDict()[use
.value
]
1340 self
.node_type
= NODE_REFERENCE
1342 print('\tWarning: reference', use
.value
, 'not found')
1343 self
.parent
.children
.remove(self
)
1347 for x3dChildNode
in self
.x3dNode
.childNodes
:
1348 if x3dChildNode
.nodeType
in {x3dChildNode
.TEXT_NODE
, x3dChildNode
.COMMENT_NODE
, x3dChildNode
.CDATA_SECTION_NODE
}:
1351 node_type
= NODE_NORMAL
1352 # print(x3dChildNode, dir(x3dChildNode))
1353 if x3dChildNode
.getAttributeNode('USE'):
1354 node_type
= NODE_REFERENCE
1356 child
= x3dNode(self
, node_type
, x3dChildNode
)
1362 return self
.x3dNode
.tagName
# should match vrml spec
1364 # Used to retain object identifiers from X3D to Blender
1365 def getDefName(self
):
1366 node_id
= self
.x3dNode
.getAttributeNode('DEF')
1368 return node_id
.value
1369 node_id
= self
.x3dNode
.getAttributeNode('USE')
1371 return "USE_" + node_id
.value
1374 # Other funcs operate from vrml, but this means we can wrap XML fields, still use nice utility funcs
1375 # getFieldAsArray getFieldAsBool etc
1376 def getFieldName(self
, field
, ancestry
, AS_CHILD
=False, SPLIT_COMMAS
=False):
1377 # ancestry and AS_CHILD are ignored, only used for VRML now
1379 self_real
= self
.getRealNode() # in case we're an instance
1380 field_xml
= self
.x3dNode
.getAttributeNode(field
)
1382 value
= field_xml
.value
1384 # We may want to edit. for x3d specific stuff
1385 # Sucks a bit to return the field name in the list but vrml excepts this :/
1387 value
= value
.replace(",", " ")
1388 return value
.split()
1392 def canHaveReferences(self
):
1393 return self
.x3dNode
.getAttributeNode('DEF')
1396 return self
.getRealNode().x3dNode
.toxml()
1399 def x3d_parse(path
):
1401 Sets up the root node and returns it so load_web3d() can deal with the blender side of things.
1402 Return root (x3dNode, '') or (None, 'Error String')
1404 import xml
.dom
.minidom
1406 from xml
.sax
import handler
1409 try: doc = xml.dom.minidom.parse(path)
1410 except: return None, 'Could not parse this X3D file, XML error'
1413 # Could add a try/except here, but a console error is more useful.
1414 data
= gzipOpen(path
)
1417 return None, 'Failed to open file: ' + path
1419 # Enable line number reporting in the parser - kinda brittle
1420 def set_content_handler(dom_handler
):
1421 def startElementNS(name
, tagName
, attrs
):
1422 orig_start_cb(name
, tagName
, attrs
)
1423 cur_elem
= dom_handler
.elementStack
[-1]
1424 cur_elem
.parse_position
= (parser
._parser
.CurrentLineNumber
, parser
._parser
.CurrentColumnNumber
)
1426 orig_start_cb
= dom_handler
.startElementNS
1427 dom_handler
.startElementNS
= startElementNS
1428 orig_set_content_handler(dom_handler
)
1430 parser
= xml
.sax
.make_parser()
1431 orig_set_content_handler
= parser
.setContentHandler
1432 parser
.setFeature(handler
.feature_external_ges
, False)
1433 parser
.setFeature(handler
.feature_external_pes
, False)
1434 parser
.setContentHandler
= set_content_handler
1436 doc
= xml
.dom
.minidom
.parseString(data
, parser
)
1439 x3dnode
= doc
.getElementsByTagName('X3D')[0]
1441 return None, 'Not a valid x3d document, cannot import'
1443 bpy
.ops
.object.select_all(action
='DESELECT')
1445 root
= x3dNode(None, NODE_NORMAL
, x3dnode
)
1446 root
.setRoot(path
) # so images and Inline's we load have a relative path
1451 ## f = open('/_Cylinder.wrl', 'r')
1452 # f = open('/fe/wrl/Vrml/EGS/TOUCHSN.WRL', 'r')
1453 # vrml_parse('/fe/wrl/Vrml/EGS/TOUCHSN.WRL')
1454 #vrml_parse('/fe/wrl/Vrml/EGS/SCRIPT.WRL')
1457 files = os.popen('find /fe/wrl -iname "*.wrl"').readlines()
1460 for i, f in enumerate(files):
1469 # NO BLENDER CODE ABOVE THIS LINE.
1470 # -----------------------------------------------------------------------------------
1472 from bpy_extras
import image_utils
, node_shader_utils
1473 from mathutils
import Vector
, Matrix
, Quaternion
1475 GLOBALS
= {'CIRCLE_DETAIL': 16}
1478 def translateRotation(rot
):
1480 return Matrix
.Rotation(rot
[3], 4, Vector(rot
[:3]))
1483 def translateScale(sca
):
1484 mat
= Matrix() # 4x4 default
1491 def translateTransform(node
, ancestry
):
1492 cent
= node
.getFieldAsFloatTuple('center', None, ancestry
) # (0.0, 0.0, 0.0)
1493 rot
= node
.getFieldAsFloatTuple('rotation', None, ancestry
) # (0.0, 0.0, 1.0, 0.0)
1494 sca
= node
.getFieldAsFloatTuple('scale', None, ancestry
) # (1.0, 1.0, 1.0)
1495 scaori
= node
.getFieldAsFloatTuple('scaleOrientation', None, ancestry
) # (0.0, 0.0, 1.0, 0.0)
1496 tx
= node
.getFieldAsFloatTuple('translation', None, ancestry
) # (0.0, 0.0, 0.0)
1499 cent_mat
= Matrix
.Translation(cent
)
1500 cent_imat
= cent_mat
.inverted()
1502 cent_mat
= cent_imat
= None
1505 rot_mat
= translateRotation(rot
)
1510 sca_mat
= translateScale(sca
)
1515 scaori_mat
= translateRotation(scaori
)
1516 scaori_imat
= scaori_mat
.inverted()
1518 scaori_mat
= scaori_imat
= None
1521 tx_mat
= Matrix
.Translation(tx
)
1527 mats
= [tx_mat
, cent_mat
, rot_mat
, scaori_mat
, sca_mat
, scaori_imat
, cent_imat
]
1530 new_mat
= new_mat
@ mtx
1535 def translateTexTransform(node
, ancestry
):
1536 cent
= node
.getFieldAsFloatTuple('center', None, ancestry
) # (0.0, 0.0)
1537 rot
= node
.getFieldAsFloat('rotation', None, ancestry
) # 0.0
1538 sca
= node
.getFieldAsFloatTuple('scale', None, ancestry
) # (1.0, 1.0)
1539 tx
= node
.getFieldAsFloatTuple('translation', None, ancestry
) # (0.0, 0.0)
1542 # cent is at a corner by default
1543 cent_mat
= Matrix
.Translation(Vector(cent
).to_3d())
1544 cent_imat
= cent_mat
.inverted()
1546 cent_mat
= cent_imat
= None
1549 rot_mat
= Matrix
.Rotation(rot
, 4, 'Z') # translateRotation(rot)
1554 sca_mat
= translateScale((sca
[0], sca
[1], 0.0))
1559 tx_mat
= Matrix
.Translation(Vector(tx
).to_3d())
1565 # as specified in VRML97 docs
1566 mats
= [cent_imat
, sca_mat
, rot_mat
, cent_mat
, tx_mat
]
1570 new_mat
= new_mat
@ mtx
1574 def getFinalMatrix(node
, mtx
, ancestry
, global_matrix
):
1576 transform_nodes
= [node_tx
for node_tx
in ancestry
if node_tx
.getSpec() == 'Transform']
1577 if node
.getSpec() == 'Transform':
1578 transform_nodes
.append(node
)
1579 transform_nodes
.reverse()
1584 for node_tx
in transform_nodes
:
1585 mat
= translateTransform(node_tx
, ancestry
)
1589 mtx
= global_matrix
@ mtx
1594 # -----------------------------------------------------------------------------------
1595 # Mesh import utilities
1597 # Assumes that the mesh has polygons.
1598 def importMesh_ApplyColors(bpymesh
, geom
, ancestry
):
1599 colors
= geom
.getChildBySpec(['ColorRGBA', 'Color'])
1601 if colors
.getSpec() == 'ColorRGBA':
1602 rgb
= colors
.getFieldAsArray('color', 4, ancestry
)
1604 # Array of arrays; no need to flatten
1605 rgb
= [c
+ [1.0] for c
in colors
.getFieldAsArray('color', 3, ancestry
)]
1606 lcol_layer
= bpymesh
.vertex_colors
.new()
1608 if len(rgb
) == len(bpymesh
.vertices
):
1609 rgb
= [rgb
[l
.vertex_index
] for l
in bpymesh
.loops
]
1610 rgb
= tuple(chain(*rgb
))
1611 elif len(rgb
) == len(bpymesh
.loops
):
1612 rgb
= tuple(chain(*rgb
))
1615 "WARNING not applying vertex colors, non matching numbers of vertices or loops (%d vs %d/%d)" %
1616 (len(rgb
), len(bpymesh
.vertices
), len(bpymesh
.loops
))
1620 lcol_layer
.data
.foreach_set("color", rgb
)
1623 # Assumes that the vertices have not been rearranged compared to the
1624 # source file order # or in the order assumed by the spec (e. g. in
1625 # Elevation, in rows by x).
1626 # Assumes polygons have been set.
1627 def importMesh_ApplyNormals(bpymesh
, geom
, ancestry
):
1628 normals
= geom
.getChildBySpec('Normal')
1632 per_vertex
= geom
.getFieldAsBool('normalPerVertex', True, ancestry
)
1633 vectors
= normals
.getFieldAsArray('vector', 0, ancestry
)
1635 bpymesh
.vertices
.foreach_set("normal", vectors
)
1637 bpymesh
.polygons
.foreach_set("normal", vectors
)
1640 # Reads the standard Coordinate object - common for all mesh elements
1641 # Feeds the vertices in the mesh.
1642 # Rearranging the vertex order is a bad idea - other elements
1643 # in X3D might rely on it, if you need to rearrange, please play with
1644 # vertex indices in the polygons instead.
1646 # Vertex culling that we have in IndexedFaceSet is an unfortunate exception,
1647 # brought forth by a very specific issue.
1648 def importMesh_ReadVertices(bpymesh
, geom
, ancestry
):
1649 # We want points here as a flat array, but the caching logic in
1650 # IndexedFaceSet presumes a 2D one.
1651 # The case for caching is stronger over there.
1652 coord
= geom
.getChildBySpec('Coordinate')
1653 points
= coord
.getFieldAsArray('point', 0, ancestry
)
1654 bpymesh
.vertices
.add(len(points
) // 3)
1655 bpymesh
.vertices
.foreach_set("co", points
)
1658 # Assumes that the order of vertices matches the source file.
1659 # Relies upon texture coordinates in the X3D node; if a coordinate generation
1660 # algorithm for a geometry is in the spec (e. g. for ElevationGrid), it needs
1661 # to be implemented by the geometry handler.
1663 # Texture transform is applied in ProcessObject.
1664 def importMesh_ApplyUVs(bpymesh
, geom
, ancestry
):
1665 tex_coord
= geom
.getChildBySpec('TextureCoordinate')
1669 uvs
= tex_coord
.getFieldAsArray('point', 2, ancestry
)
1673 d
= bpymesh
.uv_layers
.new().data
1674 uvs
= [i
for poly
in bpymesh
.polygons
1675 for vidx
in poly
.vertices
1677 d
.foreach_set('uv', uvs
)
1680 # Common steps for all triangle meshes once the geometry has been set:
1681 # normals, vertex colors, and UVs.
1682 def importMesh_FinalizeTriangleMesh(bpymesh
, geom
, ancestry
):
1683 importMesh_ApplyNormals(bpymesh
, geom
, ancestry
)
1684 importMesh_ApplyColors(bpymesh
, geom
, ancestry
)
1685 importMesh_ApplyUVs(bpymesh
, geom
, ancestry
)
1691 # Assumes that the mesh is stored as polygons and loops, and the premade array
1692 # of texture coordinates follows the loop array.
1693 # The loops array must be flat.
1694 def importMesh_ApplyTextureToLoops(bpymesh
, loops
):
1695 d
= bpymesh
.uv_layers
.new().data
1696 d
.foreach_set('uv', loops
)
1700 return r
if ccw
else r
[::-1]
1702 # -----------------------------------------------------------------------------------
1703 # Now specific geometry importers
1706 def importMesh_IndexedTriangleSet(geom
, ancestry
):
1708 # colorPerVertex is always true
1709 ccw
= geom
.getFieldAsBool('ccw', True, ancestry
)
1711 bpymesh
= bpy
.data
.meshes
.new(name
="XXX")
1712 importMesh_ReadVertices(bpymesh
, geom
, ancestry
)
1715 index
= geom
.getFieldAsArray('index', 0, ancestry
)
1716 num_polys
= len(index
) // 3
1718 index
= [index
[3 * i
+ j
] for i
in range(num_polys
) for j
in (1, 0, 2)]
1720 bpymesh
.loops
.add(num_polys
* 3)
1721 bpymesh
.polygons
.add(num_polys
)
1722 bpymesh
.polygons
.foreach_set("loop_start", range(0, num_polys
* 3, 3))
1723 bpymesh
.polygons
.foreach_set("vertices", index
)
1725 return importMesh_FinalizeTriangleMesh(bpymesh
, geom
, ancestry
)
1728 def importMesh_IndexedTriangleStripSet(geom
, ancestry
):
1730 # colorPerVertex is always true
1731 cw
= 0 if geom
.getFieldAsBool('ccw', True, ancestry
) else 1
1732 bpymesh
= bpy
.data
.meshes
.new(name
="IndexedTriangleStripSet")
1733 importMesh_ReadVertices(bpymesh
, geom
, ancestry
)
1736 index
= geom
.getFieldAsArray('index', 0, ancestry
)
1737 while index
[-1] == -1:
1739 ngaps
= sum(1 for i
in index
if i
== -1)
1740 num_polys
= len(index
) - 2 - 3 * ngaps
1741 bpymesh
.loops
.add(num_polys
* 3)
1742 bpymesh
.polygons
.add(num_polys
)
1743 bpymesh
.polygons
.foreach_set("loop_start", range(0, num_polys
* 3, 3))
1749 yield index
[i
+ odd
]
1750 yield index
[i
+ 1 - odd
]
1754 if i
+ 2 >= len(index
):
1756 if index
[i
+ 2] == -1:
1759 bpymesh
.polygons
.foreach_set("vertices", [f
for f
in triangles()])
1760 return importMesh_FinalizeTriangleMesh(bpymesh
, geom
, ancestry
)
1763 def importMesh_IndexedTriangleFanSet(geom
, ancestry
):
1765 # colorPerVertex is always true
1766 cw
= 0 if geom
.getFieldAsBool('ccw', True, ancestry
) else 1
1767 bpymesh
= bpy
.data
.meshes
.new(name
="IndexedTriangleFanSet")
1768 importMesh_ReadVertices(bpymesh
, geom
, ancestry
)
1771 index
= geom
.getFieldAsArray('index', 0, ancestry
)
1772 while index
[-1] == -1:
1774 ngaps
= sum(1 for i
in index
if i
== -1)
1775 num_polys
= len(index
) - 2 - 3 * ngaps
1776 bpymesh
.loops
.add(num_polys
* 3)
1777 bpymesh
.polygons
.add(num_polys
)
1778 bpymesh
.polygons
.foreach_set("loop_start", range(0, num_polys
* 3, 3))
1785 yield index
[i
+ j
+ cw
]
1786 yield index
[i
+ j
+ 1 - cw
]
1788 if i
+ j
+ 1 >= len(index
):
1790 if index
[i
+ j
+ 1] == -1:
1793 bpymesh
.polygons
.foreach_set("vertices", [f
for f
in triangles()])
1794 return importMesh_FinalizeTriangleMesh(bpymesh
, geom
, ancestry
)
1797 def importMesh_TriangleSet(geom
, ancestry
):
1799 # colorPerVertex is always true
1800 ccw
= geom
.getFieldAsBool('ccw', True, ancestry
)
1801 bpymesh
= bpy
.data
.meshes
.new(name
="TriangleSet")
1802 importMesh_ReadVertices(bpymesh
, geom
, ancestry
)
1803 n
= len(bpymesh
.vertices
)
1805 bpymesh
.loops
.add(num_polys
* 3)
1806 bpymesh
.polygons
.add(num_polys
)
1807 bpymesh
.polygons
.foreach_set("loop_start", range(0, num_polys
* 3, 3))
1810 fv
= [i
for i
in range(n
)]
1812 fv
= [3 * i
+ j
for i
in range(n
// 3) for j
in (1, 0, 2)]
1813 bpymesh
.polygons
.foreach_set("vertices", fv
)
1815 return importMesh_FinalizeTriangleMesh(bpymesh
, geom
, ancestry
)
1818 def importMesh_TriangleStripSet(geom
, ancestry
):
1820 # colorPerVertex is always true
1821 cw
= 0 if geom
.getFieldAsBool('ccw', True, ancestry
) else 1
1822 bpymesh
= bpy
.data
.meshes
.new(name
="TriangleStripSet")
1823 importMesh_ReadVertices(bpymesh
, geom
, ancestry
)
1824 counts
= geom
.getFieldAsArray('stripCount', 0, ancestry
)
1825 num_polys
= sum([n
- 2 for n
in counts
])
1826 bpymesh
.loops
.add(num_polys
* 3)
1827 bpymesh
.polygons
.add(num_polys
)
1828 bpymesh
.polygons
.foreach_set("loop_start", range(0, num_polys
* 3, 3))
1832 for i
in range(0, len(counts
)):
1833 for j
in range(0, counts
[i
] - 2):
1834 yield b
+ j
+ (j
+ cw
) % 2
1835 yield b
+ j
+ 1 - (j
+ cw
) % 2
1838 bpymesh
.polygons
.foreach_set("vertices", [x
for x
in triangles()])
1840 return importMesh_FinalizeTriangleMesh(bpymesh
, geom
, ancestry
)
1843 def importMesh_TriangleFanSet(geom
, ancestry
):
1845 # colorPerVertex is always true
1846 cw
= 0 if geom
.getFieldAsBool('ccw', True, ancestry
) else 1
1847 bpymesh
= bpy
.data
.meshes
.new(name
="TriangleStripSet")
1848 importMesh_ReadVertices(bpymesh
, geom
, ancestry
)
1849 counts
= geom
.getFieldAsArray('fanCount', 0, ancestry
)
1850 num_polys
= sum([n
- 2 for n
in counts
])
1851 bpymesh
.loops
.add(num_polys
* 3)
1852 bpymesh
.polygons
.add(num_polys
)
1853 bpymesh
.polygons
.foreach_set("loop_start", range(0, num_polys
* 3, 3))
1857 for i
in range(0, len(counts
)):
1858 for j
in range(1, counts
[i
] - 1):
1861 yield b
+ j
+ 1 - cw
1863 bpymesh
.polygons
.foreach_set("vertices", [x
for x
in triangles()])
1864 return importMesh_FinalizeTriangleMesh(bpymesh
, geom
, ancestry
)
1867 def importMesh_IndexedFaceSet(geom
, ancestry
):
1868 # Saw the following structure in X3Ds: the first mesh has a huge set
1869 # of vertices and a reasonably sized index. The rest of the meshes
1870 # reference the Coordinate node from the first one, and have their
1871 # own reasonably sized indices.
1873 # In Blender, to the best of my knowledge, there's no way to reuse
1874 # the vertex set between meshes. So we have culling logic instead -
1875 # for each mesh, only leave vertices that are used for faces.
1877 ccw
= geom
.getFieldAsBool('ccw', True, ancestry
)
1878 coord
= geom
.getChildBySpec('Coordinate')
1880 points
= coord
.getRealNode().parsed
1881 # We need unflattened coord array here, while
1882 # importMesh_ReadVertices uses flattened. Can't cache both :(
1883 # TODO: resolve that somehow, so that vertex set can be effectively
1884 # reused between different mesh types?
1886 points
= coord
.getFieldAsArray('point', 3, ancestry
)
1887 if coord
.canHaveReferences():
1888 coord
.parsed
= points
1889 index
= geom
.getFieldAsArray('coordIndex', 0, ancestry
)
1891 while index
and index
[-1] == -1:
1894 if len(points
) >= 2 * len(index
): # Need to cull
1896 cull
= {} # Maps old vertex indices to new ones
1897 uncull
= [] # Maps new indices to the old ones
1900 uncull
= cull
= None
1904 # Generate faces. Cull the vertices if necessary,
1908 faces
.append(flip(face
, ccw
))
1911 if cull
is not None:
1913 culled_points
.append(points
[i
])
1922 faces
.append(flip(face
, ccw
)) # The last face
1925 points
= culled_points
1927 bpymesh
= bpy
.data
.meshes
.new(name
="IndexedFaceSet")
1928 bpymesh
.from_pydata(points
, [], faces
)
1929 # No validation here. It throws off the per-face stuff.
1931 # Similar treatment for normal and color indices
1933 def processPerVertexIndex(ind
):
1935 # Deflatten into an array of arrays by face; the latter might
1936 # need to be flipped
1940 verts_by_face
.append(flip(ind
[i
:i
+ len(f
)], ccw
))
1942 return verts_by_face
1944 return [[uncull
[v
] for v
in f
] for f
in faces
]
1946 return faces
# Reuse coordIndex, as per the spec
1949 normals
= geom
.getChildBySpec('Normal')
1951 per_vertex
= geom
.getFieldAsBool('normalPerVertex', True, ancestry
)
1952 vectors
= normals
.getFieldAsArray('vector', 3, ancestry
)
1953 normal_index
= geom
.getFieldAsArray('normalIndex', 0, ancestry
)
1955 co
= [co
for f
in processPerVertexIndex(normal_index
)
1957 for co
in vectors
[v
]]
1958 bpymesh
.vertices
.foreach_set("normal", co
)
1960 co
= [co
for (i
, f
) in enumerate(faces
)
1962 for co
in vectors
[normal_index
[i
] if normal_index
else i
]]
1963 bpymesh
.polygons
.foreach_set("normal", co
)
1965 # Apply vertex/face colors
1966 colors
= geom
.getChildBySpec(['ColorRGBA', 'Color'])
1968 if colors
.getSpec() == 'ColorRGBA':
1969 rgb
= colors
.getFieldAsArray('color', 4, ancestry
)
1971 # Array of arrays; no need to flatten
1972 rgb
= [c
+ [1.0] for c
in colors
.getFieldAsArray('color', 3, ancestry
)]
1974 color_per_vertex
= geom
.getFieldAsBool('colorPerVertex', True, ancestry
)
1975 color_index
= geom
.getFieldAsArray('colorIndex', 0, ancestry
)
1977 d
= bpymesh
.vertex_colors
.new().data
1978 if color_per_vertex
:
1979 cco
= [cco
for f
in processPerVertexIndex(color_index
)
1982 elif color_index
: # Color per face with index
1983 cco
= [cco
for (i
, f
) in enumerate(faces
)
1985 for cco
in rgb
[color_index
[i
]]]
1986 else: # Color per face without index
1987 cco
= [cco
for (i
, f
) in enumerate(faces
)
1990 d
.foreach_set('color', cco
)
1992 # Texture coordinates (UVs)
1993 tex_coord
= geom
.getChildBySpec('TextureCoordinate')
1995 tex_coord_points
= tex_coord
.getFieldAsArray('point', 2, ancestry
)
1996 tex_index
= geom
.getFieldAsArray('texCoordIndex', 0, ancestry
)
1997 tex_index
= processPerVertexIndex(tex_index
)
1998 loops
= [co
for f
in tex_index
2000 for co
in tex_coord_points
[v
]]
2002 x_min
= y_min
= z_min
= math
.inf
2003 x_max
= y_max
= z_max
= -math
.inf
2005 # Unused vertices don't participate in size; X3DOM does so
2007 (x
, y
, z
) = points
[v
]
2008 x_min
= min(x_min
, x
)
2009 x_max
= max(x_max
, x
)
2010 y_min
= min(y_min
, y
)
2011 y_max
= max(y_max
, y
)
2012 z_min
= min(z_min
, z
)
2013 z_max
= max(z_max
, z
)
2015 mins
= (x_min
, y_min
, z_min
)
2016 deltas
= (x_max
- x_min
, y_max
- y_min
, z_max
- z_min
)
2018 axes
.sort(key
=lambda a
: (-deltas
[a
], a
))
2019 # Tuple comparison breaks ties
2020 (s_axis
, t_axis
) = axes
[0:2]
2021 s_min
= mins
[s_axis
]
2023 t_min
= mins
[t_axis
]
2026 # Avoid divide by zero T76303.
2032 def generatePointCoords(pt
):
2033 return (pt
[s_axis
] - s_min
) / ds
, (pt
[t_axis
] - t_min
) / dt
2034 loops
= [co
for f
in faces
2036 for co
in generatePointCoords(points
[v
])]
2038 importMesh_ApplyTextureToLoops(bpymesh
, loops
)
2045 def importMesh_ElevationGrid(geom
, ancestry
):
2046 height
= geom
.getFieldAsArray('height', 0, ancestry
)
2047 x_dim
= geom
.getFieldAsInt('xDimension', 0, ancestry
)
2048 x_spacing
= geom
.getFieldAsFloat('xSpacing', 1, ancestry
)
2049 z_dim
= geom
.getFieldAsInt('zDimension', 0, ancestry
)
2050 z_spacing
= geom
.getFieldAsFloat('zSpacing', 1, ancestry
)
2051 ccw
= geom
.getFieldAsBool('ccw', True, ancestry
)
2053 # The spec assumes a certain ordering of quads; outer loop by z, inner by x
2054 bpymesh
= bpy
.data
.meshes
.new(name
="ElevationGrid")
2055 bpymesh
.vertices
.add(x_dim
* z_dim
)
2056 co
= [w
for x
in range(x_dim
) for z
in range(z_dim
)
2057 for w
in (x
* x_spacing
, height
[x_dim
* z
+ x
], z
* z_spacing
)]
2058 bpymesh
.vertices
.foreach_set("co", co
)
2060 num_polys
= (x_dim
- 1) * (z_dim
- 1)
2061 bpymesh
.loops
.add(num_polys
* 4)
2062 bpymesh
.polygons
.add(num_polys
)
2063 bpymesh
.polygons
.foreach_set("loop_start", range(0, num_polys
* 4, 4))
2064 # If the ccw is off, we flip the 2nd and the 4th vertices of each face.
2065 # For quad tessfaces, it was important that the final vertex index was not 0
2066 # (Blender treated it as a triangle then).
2067 # So simply reversing the face was not an option.
2068 # With bmesh polygons, this has no importance anymore, but keep existing code for now.
2069 verts
= [i
for x
in range(x_dim
- 1) for z
in range(z_dim
- 1)
2070 for i
in (z
* x_dim
+ x
,
2071 z
* x_dim
+ x
+ 1 if ccw
else (z
+ 1) * x_dim
+ x
,
2072 (z
+ 1) * x_dim
+ x
+ 1,
2073 (z
+ 1) * x_dim
+ x
if ccw
else z
* x_dim
+ x
+ 1)]
2074 bpymesh
.polygons
.foreach_set("vertices", verts
)
2076 importMesh_ApplyNormals(bpymesh
, geom
, ancestry
)
2077 # ApplyColors won't work here; faces are quads, and also per-face
2078 # coloring should be supported
2079 colors
= geom
.getChildBySpec(['ColorRGBA', 'Color'])
2081 if colors
.getSpec() == 'ColorRGBA':
2083 in colors
.getFieldAsArray('color', 4, ancestry
)]
2084 # Array of arrays; no need to flatten
2086 rgb
= colors
.getFieldAsArray('color', 3, ancestry
)
2088 tc
= bpymesh
.vertex_colors
.new().data
2089 if geom
.getFieldAsBool('colorPerVertex', True, ancestry
):
2090 # Per-vertex coloring
2091 # Note the 2/4 flip here
2092 tc
.foreach_set("color",
2093 [c
for x
in range(x_dim
- 1)
2094 for z
in range(z_dim
- 1)
2095 for rgb_idx
in (z
* x_dim
+ x
,
2096 z
* x_dim
+ x
+ 1 if ccw
else (z
+ 1) * x_dim
+ x
,
2097 (z
+ 1) * x_dim
+ x
+ 1,
2098 (z
+ 1) * x_dim
+ x
if ccw
else z
* x_dim
+ x
+ 1)
2099 for c
in rgb
[rgb_idx
]])
2100 else: # Coloring per face
2101 tc
.foreach_set("color",
2102 [c
for x
in range(x_dim
- 1)
2103 for z
in range(z_dim
- 1)
2104 for rgb_idx
in (z
* (x_dim
- 1) + x
,) * 4
2105 for c
in rgb
[rgb_idx
]])
2107 # Textures also need special treatment; it's all quads,
2108 # and there's a builtin algorithm for coordinate generation
2109 tex_coord
= geom
.getChildBySpec('TextureCoordinate')
2111 uvs
= tex_coord
.getFieldAsArray('point', 2, ancestry
)
2113 uvs
= [(i
/ (x_dim
- 1), j
/ (z_dim
- 1))
2114 for i
in range(x_dim
)
2115 for j
in range(z_dim
)]
2117 d
= bpymesh
.uv_layers
.new().data
2118 # Rather than repeat the face/vertex algorithm from above, we read
2119 # the vertex index back from polygon. Might be suboptimal.
2120 uvs
= [i
for poly
in bpymesh
.polygons
2121 for vidx
in poly
.vertices
2123 d
.foreach_set('uv', uv
)
2130 def importMesh_Extrusion(geom
, ancestry
):
2131 # Interestingly, the spec doesn't allow for vertex/face colors in this
2132 # element, nor for normals.
2133 # Since coloring and normals are not supported here, and also large
2134 # polygons for caps might be required, we shall use from_pydata().
2136 ccw
= geom
.getFieldAsBool('ccw', True, ancestry
)
2137 begin_cap
= geom
.getFieldAsBool('beginCap', True, ancestry
)
2138 end_cap
= geom
.getFieldAsBool('endCap', True, ancestry
)
2139 cross
= geom
.getFieldAsArray('crossSection', 2, ancestry
)
2141 cross
= ((1, 1), (1, -1), (-1, -1), (-1, 1), (1, 1))
2142 spine
= geom
.getFieldAsArray('spine', 3, ancestry
)
2144 spine
= ((0, 0, 0), (0, 1, 0))
2145 orient
= geom
.getFieldAsArray('orientation', 4, ancestry
)
2147 orient
= [Quaternion(o
[:3], o
[3]).to_matrix()
2148 if o
[3] else None for o
in orient
]
2149 scale
= geom
.getFieldAsArray('scale', 2, ancestry
)
2151 scale
= [Matrix(((s
[0], 0, 0), (0, 1, 0), (0, 0, s
[1])))
2152 if s
[0] != 1 or s
[1] != 1 else None for s
in scale
]
2154 # Special treatment for the closed spine and cross section.
2155 # Let's save some memory by not creating identical but distinct vertices;
2156 # later we'll introduce conditional logic to link the last vertex with
2157 # the first one where necessary.
2158 cross_closed
= cross
[0] == cross
[-1]
2162 cross
= [Vector((c
[0], 0, c
[1])) for c
in cross
]
2163 ncf
= nc
if cross_closed
else nc
- 1
2164 # Face count along the cross; for closed cross, it's the same as the
2165 # respective vertex count
2167 spine_closed
= spine
[0] == spine
[-1]
2171 spine
= [Vector(s
) for s
in spine
]
2172 nsf
= ns
if spine_closed
else ns
- 1
2174 # This will be used for fallback, where the current spine point joins
2175 # two collinear spine segments. No need to recheck the case of the
2176 # closed spine/last-to-first point juncture; if there's an angle there,
2177 # it would kick in on the first iteration of the main loop by spine.
2178 def findFirstAngleNormal():
2179 for i
in range(1, ns
- 1):
2181 z
= (spine
[i
+ 1] - spt
).cross(spine
[i
- 1] - spt
)
2182 if z
.length
> EPSILON
:
2184 # All the spines are collinear. Fallback to the rotated source
2186 # TODO: handle the situation where the first two spine points match
2187 v
= spine
[1] - spine
[0]
2188 orig_y
= Vector((0, 1, 0))
2189 orig_z
= Vector((0, 0, 1))
2190 if v
.cross(orig_y
).length
>= EPSILON
:
2191 # Spine at angle with global y - rotate the z accordingly
2192 orig_z
.rotate(orig_y
.rotation_difference(v
))
2197 for i
, spt
in enumerate(spine
):
2198 if (i
> 0 and i
< ns
- 1) or spine_closed
:
2199 snext
= spine
[(i
+ 1) % ns
]
2200 sprev
= spine
[(i
- 1 + ns
) % ns
]
2204 try_z
= vnext
.cross(vprev
)
2205 # Might be zero, then all kinds of fallback
2206 if try_z
.length
> EPSILON
:
2207 if z
is not None and try_z
.dot(z
) < 0:
2210 elif not z
: # No z, and no previous z.
2211 # Look ahead, see if there's at least one point where
2212 # spines are not collinear.
2213 z
= findFirstAngleNormal()
2214 elif i
== 0: # And non-crossed
2215 snext
= spine
[i
+ 1]
2217 z
= findFirstAngleNormal()
2218 else: # last point and not crossed
2219 sprev
= spine
[i
- 1]
2221 # If there's more than one point in the spine, z is already set.
2222 # One point in the spline is an error anyway.
2225 m
= Matrix(((x
.x
, y
.x
, z
.x
), (x
.y
, y
.y
, z
.y
), (x
.z
, y
.z
, z
.z
)))
2226 # Columns are the unit vectors for the xz plane for the cross-section
2229 mrot
= orient
[i
] if len(orient
) > 1 else orient
[0]
2231 m
@= mrot
# Not sure about this. Counterexample???
2233 mscale
= scale
[i
] if len(scale
) > 1 else scale
[0]
2236 # First the cross-section 2-vector is scaled,
2237 # then applied to the xz plane unit vectors
2239 verts
.append((spt
+ m
@ cpt
).to_tuple())
2240 # Could've done this with a single 4x4 matrix... Oh well
2242 # The method from_pydata() treats correctly quads with final vertex
2244 # So we just flip the vertices if ccw is off.
2248 faces
.append(flip([x
for x
in range(nc
- 1, -1, -1)], ccw
))
2250 # Order of edges in the face: forward along cross, forward along spine,
2251 # backward along cross, backward along spine, flipped if now ccw.
2252 # This order is assumed later in the texture coordinate assignment;
2253 # please don't change without syncing.
2257 s
* nc
+ (c
+ 1) % nc
,
2258 (s
+ 1) * nc
+ (c
+ 1) % nc
,
2259 (s
+ 1) * nc
+ c
), ccw
) for s
in range(ns
- 1) for c
in range(ncf
)]
2262 # The faces between the last and the first spine points
2268 c
), ccw
) for c
in range(ncf
)]
2271 faces
.append(flip([(ns
- 1) * nc
+ x
for x
in range(0, nc
)], ccw
))
2273 bpymesh
= bpy
.data
.meshes
.new(name
="Extrusion")
2274 bpymesh
.from_pydata(verts
, [], faces
)
2276 # The way we deal with textures in triangular meshes doesn't apply.
2277 # The structure of the loop array goes: cap, side, cap
2278 if begin_cap
or end_cap
: # Need dimensions
2279 x_min
= x_max
= z_min
= z_max
= None
2282 if x_min
is None or x
< x_min
:
2284 if x_max
is None or x
> x_max
:
2286 if z_min
is None or z
< z_min
:
2288 if z_max
is None or z
> z_max
:
2292 cap_scale
= dz
if dz
> dx
else dx
2294 # Takes an index in the cross array, returns scaled
2295 # texture coords for cap texturing purposes
2296 def scaledLoopVertex(i
):
2298 return (c
.x
- x_min
) / cap_scale
, (c
.z
- z_min
) / cap_scale
2300 # X3DOM uses raw cap shape, not a scaled one. So we will, too.
2303 mloops
= bpymesh
.loops
2304 if begin_cap
: # vertex indices match the indices in cross
2305 # Rely on the loops in the mesh; don't repeat the face
2306 # generation logic here
2307 loops
+= [co
for i
in range(nc
)
2308 for co
in scaledLoopVertex(mloops
[i
].vertex_index
)]
2311 # Same order of vertices as in face generation
2312 # We don't rely on the loops in the mesh; instead,
2313 # we repeat the face generation logic.
2314 loops
+= [co
for s
in range(nsf
)
2316 for v
in flip(((c
/ ncf
, s
/ nsf
),
2317 ((c
+ 1) / ncf
, s
/ nsf
),
2318 ((c
+ 1) / ncf
, (s
+ 1) / nsf
),
2319 (c
/ ncf
, (s
+ 1) / nsf
)), ccw
) for co
in v
]
2322 # Base loop index for end cap
2323 lb
= ncf
* nsf
* 4 + (nc
if begin_cap
else 0)
2324 # Rely on the loops here too.
2325 loops
+= [co
for i
in range(nc
) for co
2326 in scaledLoopVertex(mloops
[lb
+ i
].vertex_index
% nc
)]
2327 importMesh_ApplyTextureToLoops(bpymesh
, loops
)
2334 # -----------------------------------------------------------------------------------
2335 # Line and point sets
2338 def importMesh_LineSet(geom
, ancestry
):
2339 # TODO: line display properties are ignored
2340 # Per-vertex color is ignored
2341 coord
= geom
.getChildBySpec('Coordinate')
2342 src_points
= coord
.getFieldAsArray('point', 3, ancestry
)
2343 # Array of 3; Blender needs arrays of 4
2344 bpycurve
= bpy
.data
.curves
.new("LineSet", 'CURVE')
2345 bpycurve
.dimensions
= '3D'
2346 counts
= geom
.getFieldAsArray('vertexCount', 0, ancestry
)
2349 sp
= bpycurve
.splines
.new('POLY')
2350 sp
.points
.add(n
- 1) # points already has one element
2353 for x
in src_points
[b
:b
+ n
]:
2358 sp
.points
.foreach_set('co', [x
for x
in points()])
2363 def importMesh_IndexedLineSet(geom
, ancestry
):
2365 # coord = geom.getChildByName('coord') # 'Coordinate'
2366 coord
= geom
.getChildBySpec('Coordinate') # works for x3d and vrml
2368 points
= coord
.getFieldAsArray('point', 3, ancestry
)
2373 print('\tWarning: IndexedLineSet had no points')
2376 ils_lines
= geom
.getFieldAsArray('coordIndex', 0, ancestry
)
2381 for il
in ils_lines
:
2386 line
.append(int(il
))
2389 # vcolor = geom.getChildByName('color')
2390 # blender doesn't have per vertex color
2392 bpycurve
= bpy
.data
.curves
.new('IndexedCurve', 'CURVE')
2393 bpycurve
.dimensions
= '3D'
2398 # co = points[line[0]] # UNUSED
2399 nu
= bpycurve
.splines
.new('POLY')
2400 nu
.points
.add(len(line
) - 1) # the new nu has 1 point to begin with
2401 for il
, pt
in zip(line
, nu
.points
):
2402 pt
.co
[0:3] = points
[il
]
2407 def importMesh_PointSet(geom
, ancestry
):
2409 coord
= geom
.getChildBySpec('Coordinate') # works for x3d and vrml
2411 points
= coord
.getFieldAsArray('point', 3, ancestry
)
2415 # vcolor = geom.getChildByName('color')
2416 # blender doesn't have per vertex color
2418 bpymesh
= bpy
.data
.meshes
.new("PointSet")
2419 bpymesh
.vertices
.add(len(points
))
2420 bpymesh
.vertices
.foreach_set("co", [a
for v
in points
for a
in v
])
2422 # No need to validate
2427 # -----------------------------------------------------------------------------------
2429 # SA: they used to use bpy.ops for primitive creation. That was
2430 # unbelievably slow on complex scenes. I rewrote to generate meshes
2434 GLOBALS
['CIRCLE_DETAIL'] = 12
2437 def importMesh_Sphere(geom
, ancestry
):
2439 # Extra field 'subdivision="n m"' attribute, specifying how many
2440 # rings and segments to use (X3DOM).
2441 r
= geom
.getFieldAsFloat('radius', 0.5, ancestry
)
2442 subdiv
= geom
.getFieldAsArray('subdivision', 0, ancestry
)
2444 if len(subdiv
) == 1:
2449 nr
= ns
= GLOBALS
['CIRCLE_DETAIL']
2450 # used as both ring count and segment count
2451 lau
= pi
/ nr
# Unit angle of latitude (rings) for the given tessellation
2452 lou
= 2 * pi
/ ns
# Unit angle of longitude (segments)
2454 bpymesh
= bpy
.data
.meshes
.new(name
="Sphere")
2456 bpymesh
.vertices
.add(ns
* (nr
- 1) + 2)
2457 # The non-polar vertices go from x=0, negative z plane counterclockwise -
2458 # to -x, to +z, to +x, back to -z
2459 co
= [0, r
, 0, 0, -r
, 0] # +y and -y poles
2460 co
+= [r
* coe
for ring
in range(1, nr
) for seg
in range(ns
)
2461 for coe
in (-sin(lou
* seg
) * sin(lau
* ring
),
2463 -cos(lou
* seg
) * sin(lau
* ring
))]
2464 bpymesh
.vertices
.foreach_set('co', co
)
2468 num_quad
= num_poly
- num_tri
2469 num_loop
= num_quad
* 4 + num_tri
* 3
2470 tf
= bpymesh
.polygons
2472 bpymesh
.loops
.add(num_loop
)
2473 bpymesh
.polygons
.foreach_set("loop_start",
2474 tuple(range(0, ns
* 3, 3)) +
2475 tuple(range(ns
* 3, num_loop
- ns
* 3, 4)) +
2476 tuple(range(num_loop
- ns
* 3, num_loop
, 3)))
2478 vb
= 2 + (nr
- 2) * ns
# First vertex index for the bottom cap
2479 fb
= (nr
- 1) * ns
# First face index for the bottom cap
2481 # Because of tricky structure, assign texture coordinates along with
2482 # face creation. Can't easily do foreach_set, 'cause caps are triangles and
2485 tex
= bpymesh
.uv_layers
.new().data
2487 # Faces go in order: top cap, sides, bottom cap.
2488 # Sides go by ring then by segment.
2491 # Top cap face vertices go in order: down right up
2492 # (starting from +y pole)
2493 # Bottom cap goes: up left down (starting from -y pole)
2494 for seg
in range(ns
):
2495 tf
[seg
].vertices
= (0, seg
+ 2, (seg
+ 1) % ns
+ 2)
2496 tf
[fb
+ seg
].vertices
= (1, vb
+ (seg
+ 1) % ns
, vb
+ seg
)
2497 for lidx
, uv
in zip(tf
[seg
].loop_indices
,
2498 (((seg
+ 0.5) / ns
, 1),
2499 (seg
/ ns
, 1 - 1 / nr
),
2500 ((seg
+ 1) / ns
, 1 - 1 / nr
))):
2502 for lidx
, uv
in zip(tf
[fb
+ seg
].loop_indices
,
2503 (((seg
+ 0.5) / ns
, 0),
2504 ((seg
+ 1) / ns
, 1 / nr
),
2505 (seg
/ ns
, 1 / nr
))):
2509 # Side face vertices go in order: down right up left
2510 for ring
in range(nr
- 2):
2512 # First vertex index for the top edge of the ring
2514 # First vertex index for the bottom edge of the ring
2515 rfb
= ns
* (ring
+ 1)
2516 # First face index for the ring
2517 for seg
in range(ns
):
2518 nseg
= (seg
+ 1) % ns
2519 tf
[rfb
+ seg
].vertices
= (tvb
+ seg
, bvb
+ seg
, bvb
+ nseg
, tvb
+ nseg
)
2520 for lidx
, uv
in zip(tf
[rfb
+ seg
].loop_indices
,
2521 ((seg
/ ns
, 1 - (ring
+ 1) / nr
),
2522 (seg
/ ns
, 1 - (ring
+ 2) / nr
),
2523 ((seg
+ 1) / ns
, 1 - (ring
+ 2) / nr
),
2524 ((seg
+ 1) / ns
, 1 - (ring
+ 1) / nr
))):
2532 def importMesh_Cylinder(geom
, ancestry
):
2534 # no ccw in this element
2535 # Extra parameter subdivision="n" - how many faces to use
2536 radius
= geom
.getFieldAsFloat('radius', 1.0, ancestry
)
2537 height
= geom
.getFieldAsFloat('height', 2, ancestry
)
2538 bottom
= geom
.getFieldAsBool('bottom', True, ancestry
)
2539 side
= geom
.getFieldAsBool('side', True, ancestry
)
2540 top
= geom
.getFieldAsBool('top', True, ancestry
)
2542 n
= geom
.getFieldAsInt('subdivision', GLOBALS
['CIRCLE_DETAIL'], ancestry
)
2545 yvalues
= (height
/ 2, -height
/ 2)
2548 # The seam is at x=0, z=-r, vertices go ccw -
2549 # to pos x, to neg z, to neg x, back to neg z
2550 verts
= [(-radius
* sin(angle
* i
), y
, -radius
* cos(angle
* i
))
2551 for i
in range(n
) for y
in yvalues
]
2554 # Order of edges in side faces: up, left, down, right.
2555 # Texture coordinate logic depends on it.
2556 faces
+= [(i
* 2 + 3, i
* 2 + 2, i
* 2, i
* 2 + 1)
2557 for i
in range(n
- 1)] + [(1, 0, nn
- 2, nn
- 1)]
2559 faces
+= [[x
for x
in range(0, nn
, 2)]]
2561 faces
+= [[x
for x
in range(nn
- 1, -1, -2)]]
2563 bpymesh
= bpy
.data
.meshes
.new(name
="Cylinder")
2564 bpymesh
.from_pydata(verts
, [], faces
)
2565 # Tried constructing the mesh manually from polygons/loops/edges,
2566 # the difference in performance on Blender 2.74 (Win64) is negligible.
2570 # The structure of the loop array goes: cap, side, cap.
2573 loops
+= [co
for i
in range(n
)
2574 for co
in ((i
+ 1) / n
, 0, (i
+ 1) / n
, 1, i
/ n
, 1, i
/ n
, 0)]
2577 loops
+= [0.5 + co
/ 2 for i
in range(n
)
2578 for co
in (-sin(angle
* i
), cos(angle
* i
))]
2581 loops
+= [0.5 - co
/ 2 for i
in range(n
- 1, -1, -1)
2582 for co
in (sin(angle
* i
), cos(angle
* i
))]
2584 importMesh_ApplyTextureToLoops(bpymesh
, loops
)
2590 def importMesh_Cone(geom
, ancestry
):
2592 # Extra parameter subdivision="n" - how many faces to use
2593 n
= geom
.getFieldAsInt('subdivision', GLOBALS
['CIRCLE_DETAIL'], ancestry
)
2594 radius
= geom
.getFieldAsFloat('bottomRadius', 1.0, ancestry
)
2595 height
= geom
.getFieldAsFloat('height', 2, ancestry
)
2596 bottom
= geom
.getFieldAsBool('bottom', True, ancestry
)
2597 side
= geom
.getFieldAsBool('side', True, ancestry
)
2603 verts
+= [(-radius
* sin(angle
* i
),
2605 -radius
* cos(angle
* i
)) for i
in range(n
)]
2608 # Side face vertices go: up down right
2610 faces
+= [(1 + (i
+ 1) % n
, 0, 1 + i
) for i
in range(n
)]
2612 faces
+= [[i
for i
in range(n
, 0, -1)]]
2614 bpymesh
= bpy
.data
.meshes
.new(name
="Cone")
2615 bpymesh
.from_pydata(verts
, [], faces
)
2620 loops
+= [co
for i
in range(n
)
2621 for co
in ((i
+ 1) / n
, 0, (i
+ 0.5) / n
, 1, i
/ n
, 0)]
2623 loops
+= [0.5 - co
/ 2 for i
in range(n
- 1, -1, -1)
2624 for co
in (sin(angle
* i
), cos(angle
* i
))]
2625 importMesh_ApplyTextureToLoops(bpymesh
, loops
)
2631 def importMesh_Box(geom
, ancestry
):
2633 # No ccw in this element
2634 (dx
, dy
, dz
) = geom
.getFieldAsFloatTuple('size', (2.0, 2.0, 2.0), ancestry
)
2639 bpymesh
= bpy
.data
.meshes
.new(name
="Box")
2640 bpymesh
.vertices
.add(8)
2642 # xz plane at +y, ccw
2643 co
= (dx
, dy
, dz
, -dx
, dy
, dz
, -dx
, dy
, -dz
, dx
, dy
, -dz
,
2645 dx
, -dy
, dz
, -dx
, -dy
, dz
, -dx
, -dy
, -dz
, dx
, -dy
, -dz
)
2646 bpymesh
.vertices
.foreach_set('co', co
)
2648 bpymesh
.loops
.add(6 * 4)
2649 bpymesh
.polygons
.add(6)
2650 bpymesh
.polygons
.foreach_set('loop_start', range(0, 6 * 4, 4))
2651 bpymesh
.polygons
.foreach_set('loop_total', (4,) * 6)
2652 bpymesh
.polygons
.foreach_set('vertices', (
2661 d
= bpymesh
.uv_layers
.new().data
2662 d
.foreach_set('uv', (
2663 1, 0, 0, 0, 0, 1, 1, 1,
2664 0, 0, 0, 1, 1, 1, 1, 0,
2665 0, 0, 0, 1, 1, 1, 1, 0,
2666 0, 0, 0, 1, 1, 1, 1, 0,
2667 0, 0, 0, 1, 1, 1, 1, 0,
2668 1, 0, 0, 0, 0, 1, 1, 1))
2673 # -----------------------------------------------------------------------------------
2674 # Utilities for importShape
2677 # Textures are processed elsewhere.
2678 def appearance_CreateMaterial(vrmlname
, mat
, ancestry
, is_vcol
):
2679 # Given an X3D material, creates a Blender material.
2680 # texture is applied later, in appearance_Create().
2681 # All values between 0.0 and 1.0, defaults from VRML docs.
2682 mat_name
= mat
.getDefName()
2683 bpymat
= bpy
.data
.materials
.new(mat_name
if mat_name
else vrmlname
)
2684 bpymat_wrap
= node_shader_utils
.PrincipledBSDFWrapper(bpymat
, is_readonly
=False)
2686 # TODO: handle 'ambientIntensity'.
2687 #ambient = mat.getFieldAsFloat('ambientIntensity', 0.2, ancestry)
2689 diff_color
= mat
.getFieldAsFloatTuple('diffuseColor', [0.8, 0.8, 0.8], ancestry
)
2690 bpymat_wrap
.base_color
= diff_color
2692 emit_color
= mat
.getFieldAsFloatTuple('emissiveColor', [0.0, 0.0, 0.0], ancestry
)
2693 bpymat_wrap
.emission_color
= emit_color
2695 # NOTE - 'shininess' is being handled as 1 - roughness for now.
2696 shininess
= mat
.getFieldAsFloat('shininess', 0.2, ancestry
)
2697 bpymat_wrap
.roughness
= 1.0 - shininess
2699 #bpymat.specular_hardness = int(1 + (510 * shininess))
2701 # TODO: handle 'specularColor'.
2702 #specular_color = mat.getFieldAsFloatTuple('specularColor',
2703 # [0.0, 0.0, 0.0], ancestry)
2705 alpha
= 1.0 - mat
.getFieldAsFloat('transparency', 0.0, ancestry
)
2706 bpymat_wrap
.alpha
= alpha
2708 bpymat
.blend_method
= "BLEND"
2709 bpymat
.shadow_method
= "HASHED"
2711 # NOTE - leaving this disabled for now
2712 if False and is_vcol
:
2713 node_vertex_color
= bpymat
.node_tree
.nodes
.new("ShaderNodeVertexColor")
2714 node_vertex_color
.location
= (-200, 300)
2716 bpymat
.node_tree
.links
.new(
2717 bpymat_wrap
.node_principled_bsdf
.inputs
["Base Color"],
2718 node_vertex_color
.outputs
["Color"]
2724 def appearance_CreateDefaultMaterial():
2725 # Just applies the X3D defaults. Used for shapes
2726 # without explicit material definition
2727 # (but possibly with a texture).
2729 bpymat
= bpy
.data
.materials
.new("Material")
2730 bpymat_wrap
= node_shader_utils
.PrincipledBSDFWrapper(bpymat
, is_readonly
=False)
2732 bpymat_wrap
.roughness
= 0.8
2733 bpymat_wrap
.base_color
= (0.8, 0.8, 0.8)
2734 #bpymat.mirror_color = (0, 0, 0)
2737 # TODO: handle 'shininess' and 'specularColor'.
2738 #bpymat.specular_hardness = 103
2740 #bpymat.specular_color = (0, 0, 0)
2742 bpymat_wrap
.alpha
= 1.0
2746 def appearance_LoadImageTextureFile(ima_urls
, node
):
2749 dirname
= os
.path
.dirname(node
.getFilename())
2750 bpyima
= image_utils
.load_image(f
, dirname
,
2753 convert_callback
=imageConvertCompat
)
2760 def appearance_LoadImageTexture(imageTexture
, ancestry
, node
):
2761 # TODO: cache loaded textures...
2762 ima_urls
= imageTexture
.getFieldAsString('url', None, ancestry
)
2764 if ima_urls
is None:
2766 ima_urls
= imageTexture
.getFieldAsStringArray('url', ancestry
)
2767 # in some cases we get a list of images.
2771 if '" "' in ima_urls
:
2772 # '"foo" "bar"' --> ['foo', 'bar']
2773 ima_urls
= [w
.strip('"') for w
in ima_urls
.split('" "')]
2775 ima_urls
= [ima_urls
]
2776 # ima_urls is a list or None
2778 if ima_urls
is None:
2779 print("\twarning, image with no URL, this is odd")
2782 bpyima
= appearance_LoadImageTextureFile(ima_urls
, node
)
2785 print("ImportX3D warning: unable to load texture", ima_urls
)
2787 # KNOWN BUG; PNGs with a transparent color are not perceived
2788 # as transparent. Need alpha channel.
2790 if bpyima
.depth
not in {32, 128}:
2791 bpyima
.alpha_mode
= 'NONE'
2795 def appearance_LoadTexture(tex_node
, ancestry
, node
):
2796 # Both USE-based caching and desc-based caching
2797 # Works for bother ImageTextures and PixelTextures
2800 if tex_node
.reference
:
2801 return tex_node
.getRealNode().parsed
2803 # Desc-based caching. It might misfire on multifile models, where the
2804 # same desc means different things in different files.
2805 # TODO: move caches to file level.
2806 desc
= tex_node
.desc()
2807 if desc
and desc
in texture_cache
:
2808 bpyima
= texture_cache
[desc
]
2809 if tex_node
.canHaveReferences():
2810 tex_node
.parsed
= bpyima
2813 # No cached texture, load it.
2814 if tex_node
.getSpec() == 'ImageTexture':
2815 bpyima
= appearance_LoadImageTexture(tex_node
, ancestry
, node
)
2816 else: # PixelTexture
2817 bpyima
= appearance_LoadPixelTexture(tex_node
, ancestry
)
2819 if bpyima
: # Loading can still fail
2820 # Update the desc-based cache
2822 texture_cache
[desc
] = bpyima
2824 # Update the USE-based cache
2825 if tex_node
.canHaveReferences():
2826 tex_node
.parsed
= bpyima
2831 def appearance_ExpandCachedMaterial(bpymat
):
2832 if 0 and bpymat
.texture_slots
[0] is not None:
2833 bpyima
= bpymat
.texture_slots
[0].texture
.image
2834 tex_has_alpha
= bpyima
.alpha_mode
not in {'NONE', 'CHANNEL_PACKED'}
2835 return (bpymat
, bpyima
, tex_has_alpha
)
2837 return (bpymat
, None, False)
2840 def appearance_MakeDescCacheKey(material
, tex_node
):
2841 mat_desc
= material
.desc() if material
else "Default"
2842 tex_desc
= tex_node
.desc() if tex_node
else "Default"
2844 if not((tex_node
and tex_desc
is None) or
2845 (material
and mat_desc
is None)):
2846 # desc not available (in VRML)
2847 # TODO: serialize VRML nodes!!!
2848 return (mat_desc
, tex_desc
)
2849 elif not tex_node
and not material
:
2850 # Even for VRML, we cache the null material
2851 return ("Default", "Default")
2853 return None # Desc-based caching is off
2856 def appearance_Create(vrmlname
, material
, tex_node
, ancestry
, node
, is_vcol
):
2857 # Creates a Blender material object from appearance
2859 tex_has_alpha
= False
2862 bpymat_wrap
= appearance_CreateMaterial(vrmlname
, material
, ancestry
, is_vcol
)
2864 bpymat_wrap
= appearance_CreateDefaultMaterial()
2866 if tex_node
: # Texture caching inside there
2867 bpyima
= appearance_LoadTexture(tex_node
, ancestry
, node
)
2870 repeatS
= tex_node
.getFieldAsBool('repeatS', True, ancestry
)
2871 repeatT
= tex_node
.getFieldAsBool('repeatT', True, ancestry
)
2873 bpymat_wrap
.base_color_texture
.image
= bpyima
2875 # NOTE - not possible to handle x and y tiling individually.
2876 extension
= "REPEAT" if repeatS
or repeatT
else "CLIP"
2877 bpymat_wrap
.base_color_texture
.extension
= extension
2879 tex_has_alpha
= bpyima
.alpha_mode
not in {'NONE', 'CHANNEL_PACKED'}
2881 bpymat_wrap
.alpha_texture
.image
= bpyima
2882 bpymat_wrap
.alpha_texture
.extension
= extension
2884 return (bpymat_wrap
.material
, bpyima
, tex_has_alpha
)
2887 def importShape_LoadAppearance(vrmlname
, appr
, ancestry
, node
, is_vcol
):
2889 Material creation takes nontrivial time on large models.
2890 So we cache them aggressively.
2891 However, in Blender, texture is a part of material, while in
2892 X3D it's not. Blender's notion of material corresponds to
2893 X3D's notion of appearance.
2895 TextureTransform is not a part of material (at least
2896 not in the current implementation).
2898 USE on an Appearance node and USE on a Material node
2899 call for different approaches.
2901 Tools generate repeating, identical material definitions.
2902 Can't rely on USE alone. Repeating texture definitions
2903 are entirely possible, too.
2905 Vertex coloring is not a part of appearance, but Blender
2906 has a material flag for it. However, if a mesh has no vertex
2907 color layer, setting use_vertex_color_paint to true has no
2908 effect. So it's fine to reuse the same material for meshes
2909 with vertex colors and for ones without.
2910 It's probably an abuse of Blender of some level.
2912 So here's the caching structure:
2913 For USE on appearance, we store the material object
2914 in the appearance node.
2916 For USE on texture, we store the image object in the tex node.
2918 For USE on material with no texture, we store the material object
2919 in the material node.
2921 Also, we store textures by description in texture_cache.
2923 Also, we store materials by (material desc, texture desc)
2926 # First, check entire-appearance cache
2927 if appr
.reference
and appr
.getRealNode().parsed
:
2928 return appearance_ExpandCachedMaterial(appr
.getRealNode().parsed
)
2930 tex_node
= appr
.getChildBySpec(('ImageTexture', 'PixelTexture'))
2931 # Other texture nodes are: MovieTexture, MultiTexture
2932 material
= appr
.getChildBySpec('Material')
2933 # We're ignoring FillProperties, LineProperties, and shaders
2935 # Check the USE-based material cache for textureless materials
2936 if material
and material
.reference
and not tex_node
and material
.getRealNode().parsed
:
2937 return appearance_ExpandCachedMaterial(material
.getRealNode().parsed
)
2939 # Now the description-based caching
2940 cache_key
= appearance_MakeDescCacheKey(material
, tex_node
)
2942 if cache_key
and cache_key
in material_cache
:
2943 bpymat
= material_cache
[cache_key
]
2944 # Still want to make the material available for USE-based reuse
2945 if appr
.canHaveReferences():
2946 appr
.parsed
= bpymat
2947 if material
and material
.canHaveReferences() and not tex_node
:
2948 material
.parsed
= bpymat
2949 return appearance_ExpandCachedMaterial(bpymat
)
2951 # Done checking full-material caches. Texture cache may still kick in.
2952 # Create the material already
2953 (bpymat
, bpyima
, tex_has_alpha
) = appearance_Create(vrmlname
, material
, tex_node
, ancestry
, node
, is_vcol
)
2956 if appr
.canHaveReferences():
2957 appr
.parsed
= bpymat
2960 material_cache
[cache_key
] = bpymat
2962 if material
and material
.canHaveReferences() and not tex_node
:
2963 material
.parsed
= bpymat
2965 return (bpymat
, bpyima
, tex_has_alpha
)
2968 def appearance_LoadPixelTexture(pixelTexture
, ancestry
):
2969 image
= pixelTexture
.getFieldAsArray('image', 0, ancestry
)
2970 (w
, h
, plane_count
) = image
[0:3]
2971 has_alpha
= plane_count
in {2, 4}
2973 if len(pixels
) != w
* h
:
2974 print("ImportX3D warning: pixel count in PixelTexture is off")
2976 bpyima
= bpy
.data
.images
.new("PixelTexture", w
, h
, has_alpha
, True)
2978 bpyima
.alpha_mode
= 'NONE'
2980 # Conditional above the loop, for performance
2981 if plane_count
== 3: # RGB
2982 bpyima
.pixels
= [(cco
& 0xff) / 255 for pixel
in pixels
2983 for cco
in (pixel
>> 16, pixel
>> 8, pixel
, 255)]
2984 elif plane_count
== 4: # RGBA
2985 bpyima
.pixels
= [(cco
& 0xff) / 255 for pixel
in pixels
2987 in (pixel
>> 24, pixel
>> 16, pixel
>> 8, pixel
)]
2988 elif plane_count
== 1: # Intensity - does Blender even support that?
2989 bpyima
.pixels
= [(cco
& 0xff) / 255 for pixel
in pixels
2990 for cco
in (pixel
, pixel
, pixel
, 255)]
2991 elif plane_count
== 2: # Intensity/alpha
2992 bpyima
.pixels
= [(cco
& 0xff) / 255 for pixel
in pixels
2994 in (pixel
>> 8, pixel
>> 8, pixel
>> 8, pixel
)]
2999 # Called from importShape to insert a data object (typically a mesh)
3001 def importShape_ProcessObject(
3002 bpycollection
, vrmlname
, bpydata
, geom
, geom_spec
, node
,
3003 bpymat
, has_alpha
, texmtx
, ancestry
,
3006 vrmlname
+= "_" + geom_spec
3007 bpydata
.name
= vrmlname
3009 if type(bpydata
) == bpy
.types
.Mesh
:
3010 # solid, as understood by the spec, is always true in Blender
3011 # solid=false, we don't support it yet.
3012 creaseAngle
= geom
.getFieldAsFloat('creaseAngle', None, ancestry
)
3013 if creaseAngle
is not None:
3014 bpydata
.auto_smooth_angle
= creaseAngle
3015 bpydata
.use_auto_smooth
= True
3017 # Only ever 1 material per shape
3019 bpydata
.materials
.append(bpymat
)
3021 if bpydata
.uv_layers
:
3022 if has_alpha
and bpymat
: # set the faces alpha flag?
3023 bpymat
.blend_method
= 'BLEND'
3024 bpymat
.shadow_method
= 'HASHED'
3027 # Apply texture transform?
3029 for l
in bpydata
.uv_layers
.active
.data
:
3033 l
.uv
[:] = (uv_copy
@ texmtx
)[0:2]
3035 # Done transforming the texture
3036 # TODO: check if per-polygon textures are supported here.
3037 elif type(bpydata
) == bpy
.types
.TextCurve
:
3038 # Text with textures??? Not sure...
3040 bpydata
.materials
.append(bpymat
)
3042 # Can transform data or object, better the object so we can instance
3044 # bpymesh.transform(getFinalMatrix(node))
3045 bpyob
= node
.blendObject
= bpy
.data
.objects
.new(vrmlname
, bpydata
)
3046 bpyob
.matrix_world
= getFinalMatrix(node
, None, ancestry
, global_matrix
)
3047 bpycollection
.objects
.link(bpyob
)
3048 bpyob
.select_set(True)
3051 bpyob
["source_line_no"] = geom
.lineno
3054 def importText(geom
, ancestry
):
3055 fmt
= geom
.getChildBySpec('FontStyle')
3056 size
= fmt
.getFieldAsFloat("size", 1, ancestry
) if fmt
else 1.
3057 body
= geom
.getFieldAsString("string", None, ancestry
)
3058 body
= [w
.strip('"') for w
in body
.split('" "')]
3060 bpytext
= bpy
.data
.curves
.new(name
="Text", type='FONT')
3061 bpytext
.offset_y
= - size
3062 bpytext
.body
= "\n".join(body
)
3067 # -----------------------------------------------------------------------------------
3070 geometry_importers
= {
3071 'IndexedFaceSet': importMesh_IndexedFaceSet
,
3072 'IndexedTriangleSet': importMesh_IndexedTriangleSet
,
3073 'IndexedTriangleStripSet': importMesh_IndexedTriangleStripSet
,
3074 'IndexedTriangleFanSet': importMesh_IndexedTriangleFanSet
,
3075 'IndexedLineSet': importMesh_IndexedLineSet
,
3076 'TriangleSet': importMesh_TriangleSet
,
3077 'TriangleStripSet': importMesh_TriangleStripSet
,
3078 'TriangleFanSet': importMesh_TriangleFanSet
,
3079 'LineSet': importMesh_LineSet
,
3080 'ElevationGrid': importMesh_ElevationGrid
,
3081 'Extrusion': importMesh_Extrusion
,
3082 'PointSet': importMesh_PointSet
,
3083 'Sphere': importMesh_Sphere
,
3084 'Box': importMesh_Box
,
3085 'Cylinder': importMesh_Cylinder
,
3086 'Cone': importMesh_Cone
,
3091 def importShape(bpycollection
, node
, ancestry
, global_matrix
):
3092 # Under Shape, we can only have Appearance, MetadataXXX and a geometry node
3093 def isGeometry(spec
):
3094 return spec
!= "Appearance" and not spec
.startswith("Metadata")
3096 bpyob
= node
.getRealNode().blendObject
3098 if bpyob
is not None:
3099 bpyob
= node
.blendData
= node
.blendObject
= bpyob
.copy()
3100 # Could transform data, but better the object so we can instance the data
3101 bpyob
.matrix_world
= getFinalMatrix(node
, None, ancestry
, global_matrix
)
3102 bpycollection
.objects
.link(bpyob
)
3103 bpyob
.select_set(True)
3106 vrmlname
= node
.getDefName()
3110 appr
= node
.getChildBySpec('Appearance')
3111 geom
= node
.getChildBySpecCondition(isGeometry
)
3113 # Oh well, no geometry node in this shape
3119 tex_has_alpha
= False
3121 is_vcol
= (geom
.getChildBySpec(['Color', 'ColorRGBA']) is not None)
3125 tex_has_alpha
) = importShape_LoadAppearance(vrmlname
, appr
,
3129 textx
= appr
.getChildBySpec('TextureTransform')
3131 texmtx
= translateTexTransform(textx
, ancestry
)
3134 geom_spec
= geom
.getSpec()
3136 # ccw is handled by every geometry importer separately; some
3137 # geometries are easier to flip than others
3138 geom_fn
= geometry_importers
.get(geom_spec
)
3139 if geom_fn
is not None:
3140 bpydata
= geom_fn(geom
, ancestry
)
3142 # There are no geometry importers that can legally return
3143 # no object. It's either a bpy object, or an exception
3144 importShape_ProcessObject(
3145 bpycollection
, vrmlname
, bpydata
, geom
, geom_spec
,
3146 node
, bpymat
, tex_has_alpha
, texmtx
,
3147 ancestry
, global_matrix
)
3149 print('\tImportX3D warning: unsupported type "%s"' % geom_spec
)
3152 # -----------------------------------------------------------------------------------
3156 def importLamp_PointLight(node
, ancestry
):
3157 vrmlname
= node
.getDefName()
3159 vrmlname
= 'PointLight'
3161 # ambientIntensity = node.getFieldAsFloat('ambientIntensity', 0.0, ancestry) # TODO
3162 # attenuation = node.getFieldAsFloatTuple('attenuation', (1.0, 0.0, 0.0), ancestry) # TODO
3163 color
= node
.getFieldAsFloatTuple('color', (1.0, 1.0, 1.0), ancestry
)
3164 intensity
= node
.getFieldAsFloat('intensity', 1.0, ancestry
) # max is documented to be 1.0 but some files have higher.
3165 location
= node
.getFieldAsFloatTuple('location', (0.0, 0.0, 0.0), ancestry
)
3166 # is_on = node.getFieldAsBool('on', True, ancestry) # TODO
3167 radius
= node
.getFieldAsFloat('radius', 100.0, ancestry
)
3169 bpylamp
= bpy
.data
.lights
.new(vrmlname
, 'POINT')
3170 bpylamp
.energy
= intensity
3171 bpylamp
.distance
= radius
3172 bpylamp
.color
= color
3174 mtx
= Matrix
.Translation(Vector(location
))
3179 def importLamp_DirectionalLight(node
, ancestry
):
3180 vrmlname
= node
.getDefName()
3182 vrmlname
= 'DirectLight'
3184 # ambientIntensity = node.getFieldAsFloat('ambientIntensity', 0.0) # TODO
3185 color
= node
.getFieldAsFloatTuple('color', (1.0, 1.0, 1.0), ancestry
)
3186 direction
= node
.getFieldAsFloatTuple('direction', (0.0, 0.0, -1.0), ancestry
)
3187 intensity
= node
.getFieldAsFloat('intensity', 1.0, ancestry
) # max is documented to be 1.0 but some files have higher.
3188 # is_on = node.getFieldAsBool('on', True, ancestry) # TODO
3190 bpylamp
= bpy
.data
.lights
.new(vrmlname
, 'SUN')
3191 bpylamp
.energy
= intensity
3192 bpylamp
.color
= color
3194 # lamps have their direction as -z, yup
3195 mtx
= Vector(direction
).to_track_quat('-Z', 'Y').to_matrix().to_4x4()
3199 # looks like default values for beamWidth and cutOffAngle were swapped in VRML docs.
3202 def importLamp_SpotLight(node
, ancestry
):
3203 vrmlname
= node
.getDefName()
3205 vrmlname
= 'SpotLight'
3207 # ambientIntensity = geom.getFieldAsFloat('ambientIntensity', 0.0, ancestry) # TODO
3208 # attenuation = geom.getFieldAsFloatTuple('attenuation', (1.0, 0.0, 0.0), ancestry) # TODO
3209 beamWidth
= node
.getFieldAsFloat('beamWidth', 1.570796, ancestry
) # max is documented to be 1.0 but some files have higher.
3210 color
= node
.getFieldAsFloatTuple('color', (1.0, 1.0, 1.0), ancestry
)
3211 cutOffAngle
= node
.getFieldAsFloat('cutOffAngle', 0.785398, ancestry
) * 2.0 # max is documented to be 1.0 but some files have higher.
3212 direction
= node
.getFieldAsFloatTuple('direction', (0.0, 0.0, -1.0), ancestry
)
3213 intensity
= node
.getFieldAsFloat('intensity', 1.0, ancestry
) # max is documented to be 1.0 but some files have higher.
3214 location
= node
.getFieldAsFloatTuple('location', (0.0, 0.0, 0.0), ancestry
)
3215 # is_on = node.getFieldAsBool('on', True, ancestry) # TODO
3216 radius
= node
.getFieldAsFloat('radius', 100.0, ancestry
)
3218 bpylamp
= bpy
.data
.lights
.new(vrmlname
, 'SPOT')
3219 bpylamp
.energy
= intensity
3220 bpylamp
.distance
= radius
3221 bpylamp
.color
= color
3222 bpylamp
.spot_size
= cutOffAngle
3223 if beamWidth
> cutOffAngle
:
3224 bpylamp
.spot_blend
= 0.0
3226 if cutOffAngle
== 0.0: # this should never happen!
3227 bpylamp
.spot_blend
= 0.5
3229 bpylamp
.spot_blend
= beamWidth
/ cutOffAngle
3233 # lamps have their direction as -z, y==up
3234 mtx
= Matrix
.Translation(location
) @ Vector(direction
).to_track_quat('-Z', 'Y').to_matrix().to_4x4()
3239 def importLamp(bpycollection
, node
, spec
, ancestry
, global_matrix
):
3240 if spec
== 'PointLight':
3241 bpylamp
, mtx
= importLamp_PointLight(node
, ancestry
)
3242 elif spec
== 'DirectionalLight':
3243 bpylamp
, mtx
= importLamp_DirectionalLight(node
, ancestry
)
3244 elif spec
== 'SpotLight':
3245 bpylamp
, mtx
= importLamp_SpotLight(node
, ancestry
)
3247 print("Error, not a lamp")
3250 bpyob
= node
.blendData
= node
.blendObject
= bpy
.data
.objects
.new(bpylamp
.name
, bpylamp
)
3251 bpycollection
.objects
.link(bpyob
)
3252 bpyob
.select_set(True)
3254 bpyob
.matrix_world
= getFinalMatrix(node
, mtx
, ancestry
, global_matrix
)
3257 # -----------------------------------------------------------------------------------
3260 def importViewpoint(bpycollection
, node
, ancestry
, global_matrix
):
3261 name
= node
.getDefName()
3265 fieldOfView
= node
.getFieldAsFloat('fieldOfView', 0.785398, ancestry
) # max is documented to be 1.0 but some files have higher.
3266 # jump = node.getFieldAsBool('jump', True, ancestry)
3267 orientation
= node
.getFieldAsFloatTuple('orientation', (0.0, 0.0, 1.0, 0.0), ancestry
)
3268 position
= node
.getFieldAsFloatTuple('position', (0.0, 0.0, 0.0), ancestry
)
3269 description
= node
.getFieldAsString('description', '', ancestry
)
3271 bpycam
= bpy
.data
.cameras
.new(name
)
3273 bpycam
.angle
= fieldOfView
3275 mtx
= Matrix
.Translation(Vector(position
)) @ translateRotation(orientation
)
3277 bpyob
= node
.blendData
= node
.blendObject
= bpy
.data
.objects
.new(name
, bpycam
)
3278 bpycollection
.objects
.link(bpyob
)
3279 bpyob
.select_set(True)
3280 bpyob
.matrix_world
= getFinalMatrix(node
, mtx
, ancestry
, global_matrix
)
3283 def importTransform(bpycollection
, node
, ancestry
, global_matrix
):
3284 name
= node
.getDefName()
3288 bpyob
= node
.blendData
= node
.blendObject
= bpy
.data
.objects
.new(name
, None)
3289 bpycollection
.objects
.link(bpyob
)
3290 bpyob
.select_set(True)
3292 bpyob
.matrix_world
= getFinalMatrix(node
, None, ancestry
, global_matrix
)
3294 # so they are not too annoying
3295 bpyob
.empty_display_type
= 'PLAIN_AXES'
3296 bpyob
.empty_display_size
= 0.2
3299 #def importTimeSensor(node):
3300 def action_fcurve_ensure(action
, data_path
, array_index
):
3301 for fcu
in action
.fcurves
:
3302 if fcu
.data_path
== data_path
and fcu
.array_index
== array_index
:
3305 return action
.fcurves
.new(data_path
=data_path
, index
=array_index
)
3308 def translatePositionInterpolator(node
, action
, ancestry
):
3309 key
= node
.getFieldAsArray('key', 0, ancestry
)
3310 keyValue
= node
.getFieldAsArray('keyValue', 3, ancestry
)
3312 loc_x
= action_fcurve_ensure(action
, "location", 0)
3313 loc_y
= action_fcurve_ensure(action
, "location", 1)
3314 loc_z
= action_fcurve_ensure(action
, "location", 2)
3316 for i
, time
in enumerate(key
):
3318 x
, y
, z
= keyValue
[i
]
3322 loc_x
.keyframe_points
.insert(time
, x
)
3323 loc_y
.keyframe_points
.insert(time
, y
)
3324 loc_z
.keyframe_points
.insert(time
, z
)
3326 for fcu
in (loc_x
, loc_y
, loc_z
):
3327 for kf
in fcu
.keyframe_points
:
3328 kf
.interpolation
= 'LINEAR'
3331 def translateOrientationInterpolator(node
, action
, ancestry
):
3332 key
= node
.getFieldAsArray('key', 0, ancestry
)
3333 keyValue
= node
.getFieldAsArray('keyValue', 4, ancestry
)
3335 rot_x
= action_fcurve_ensure(action
, "rotation_euler", 0)
3336 rot_y
= action_fcurve_ensure(action
, "rotation_euler", 1)
3337 rot_z
= action_fcurve_ensure(action
, "rotation_euler", 2)
3339 for i
, time
in enumerate(key
):
3341 x
, y
, z
, w
= keyValue
[i
]
3345 mtx
= translateRotation((x
, y
, z
, w
))
3346 eul
= mtx
.to_euler()
3347 rot_x
.keyframe_points
.insert(time
, eul
.x
)
3348 rot_y
.keyframe_points
.insert(time
, eul
.y
)
3349 rot_z
.keyframe_points
.insert(time
, eul
.z
)
3351 for fcu
in (rot_x
, rot_y
, rot_z
):
3352 for kf
in fcu
.keyframe_points
:
3353 kf
.interpolation
= 'LINEAR'
3357 def translateScalarInterpolator(node
, action
, ancestry
):
3358 key
= node
.getFieldAsArray('key', 0, ancestry
)
3359 keyValue
= node
.getFieldAsArray('keyValue', 4, ancestry
)
3361 sca_x
= action_fcurve_ensure(action
, "scale", 0)
3362 sca_y
= action_fcurve_ensure(action
, "scale", 1)
3363 sca_z
= action_fcurve_ensure(action
, "scale", 2)
3365 for i
, time
in enumerate(key
):
3367 x
, y
, z
= keyValue
[i
]
3371 sca_x
.keyframe_points
.new(time
, x
)
3372 sca_y
.keyframe_points
.new(time
, y
)
3373 sca_z
.keyframe_points
.new(time
, z
)
3376 def translateTimeSensor(node
, action
, ancestry
):
3378 Apply a time sensor to an action, VRML has many combinations of loop/start/stop/cycle times
3379 to give different results, for now just do the basics
3386 time_cu
= action
.addCurve('Time')
3387 time_cu
.interpolation
= Blender
.IpoCurve
.InterpTypes
.LINEAR
3389 cycleInterval
= node
.getFieldAsFloat('cycleInterval', None, ancestry
)
3391 startTime
= node
.getFieldAsFloat('startTime', 0.0, ancestry
)
3392 stopTime
= node
.getFieldAsFloat('stopTime', 250.0, ancestry
)
3394 if cycleInterval
is not None:
3395 stopTime
= startTime
+ cycleInterval
3397 loop
= node
.getFieldAsBool('loop', False, ancestry
)
3399 time_cu
.append((1 + startTime
, 0.0))
3400 time_cu
.append((1 + stopTime
, 1.0 / 10.0)) # annoying, the UI uses /10
3403 time_cu
.extend
= Blender
.IpoCurve
.ExtendTypes
.CYCLIC
# or - EXTRAP, CYCLIC_EXTRAP, CONST,
3406 def importRoute(node
, ancestry
):
3408 Animation route only at the moment
3411 if not hasattr(node
, 'fields'):
3414 routeIpoDict
= node
.getRouteIpoDict()
3418 action
= routeIpoDict
[act_id
]
3420 action
= routeIpoDict
[act_id
] = bpy
.data
.actions
.new('web3d_ipo')
3423 # for getting definitions
3424 defDict
= node
.getDefDict()
3426 Handles routing nodes to each other
3428 ROUTE vpPI.value_changed TO champFly001.set_position
3429 ROUTE vpOI.value_changed TO champFly001.set_orientation
3430 ROUTE vpTs.fraction_changed TO vpPI.set_fraction
3431 ROUTE vpTs.fraction_changed TO vpOI.set_fraction
3432 ROUTE champFly001.bindTime TO vpTs.set_startTime
3435 #from_id, from_type = node.id[1].split('.')
3436 #to_id, to_type = node.id[3].split('.')
3439 set_position_node
= None
3440 set_orientation_node
= None
3443 for field
in node
.fields
:
3444 if field
and field
[0] == 'ROUTE':
3446 from_id
, from_type
= field
[1].split('.')
3447 to_id
, to_type
= field
[3].split('.')
3449 print("Warning, invalid ROUTE", field
)
3452 if from_type
== 'value_changed':
3453 if to_type
== 'set_position':
3454 action
= getIpo(to_id
)
3455 set_data_from_node
= defDict
[from_id
]
3456 translatePositionInterpolator(set_data_from_node
, action
, ancestry
)
3458 if to_type
in {'set_orientation', 'rotation'}:
3459 action
= getIpo(to_id
)
3460 set_data_from_node
= defDict
[from_id
]
3461 translateOrientationInterpolator(set_data_from_node
, action
, ancestry
)
3463 if to_type
== 'set_scale':
3464 action
= getIpo(to_id
)
3465 set_data_from_node
= defDict
[from_id
]
3466 translateScalarInterpolator(set_data_from_node
, action
, ancestry
)
3468 elif from_type
== 'bindTime':
3469 action
= getIpo(from_id
)
3470 time_node
= defDict
[to_id
]
3471 translateTimeSensor(time_node
, action
, ancestry
)
3484 # Used when adding blender primitives
3485 GLOBALS
['CIRCLE_DETAIL'] = PREF_CIRCLE_DIV
3487 # NOTE - reset material cache
3488 # (otherwise we might get "StructRNA of type Material has been removed" errors)
3489 global material_cache
3492 bpyscene
= bpycontext
.scene
3493 bpycollection
= bpycontext
.collection
3494 #root_node = vrml_parse('/_Cylinder.wrl')
3495 if filepath
.lower().endswith('.x3d'):
3496 root_node
, msg
= x3d_parse(filepath
)
3498 root_node
, msg
= vrml_parse(filepath
)
3504 if global_matrix
is None:
3505 global_matrix
= Matrix()
3507 # fill with tuples - (node, [parents-parent, parent])
3508 all_nodes
= root_node
.getSerialized([], [])
3510 for node
, ancestry
in all_nodes
:
3511 #if 'castle.wrl' not in node.getFilename():
3514 spec
= node
.getSpec()
3516 prefix = node.getPrefix()
3521 if HELPER_FUNC
and HELPER_FUNC(node
, ancestry
):
3522 # Note, include this function so the VRML/X3D importer can be extended
3523 # by an external script. - gets first pick
3526 importShape(bpycollection
, node
, ancestry
, global_matrix
)
3527 elif spec
in {'PointLight', 'DirectionalLight', 'SpotLight'}:
3528 importLamp(bpycollection
, node
, spec
, ancestry
, global_matrix
)
3529 elif spec
== 'Viewpoint':
3530 importViewpoint(bpycollection
, node
, ancestry
, global_matrix
)
3531 elif spec
== 'Transform':
3532 # Only use transform nodes when we are not importing a flat object hierarchy
3533 if PREF_FLAT
== False:
3534 importTransform(bpycollection
, node
, ancestry
, global_matrix
)
3536 # These are delt with later within importRoute
3537 elif spec=='PositionInterpolator':
3538 action = bpy.data.ipos.new('web3d_ipo', 'Object')
3539 translatePositionInterpolator(node, action)
3542 # After we import all nodes, route events - anim paths
3543 for node
, ancestry
in all_nodes
:
3544 importRoute(node
, ancestry
)
3546 for node
, ancestry
in all_nodes
:
3548 # we know that all nodes referenced from will be in
3549 # routeIpoDict so no need to run node.getDefDict() for every node.
3550 routeIpoDict
= node
.getRouteIpoDict()
3551 defDict
= node
.getDefDict()
3553 for key
, action
in routeIpoDict
.items():
3555 # Assign anim curves
3557 if node
.blendData
is None: # Add an object if we need one for animation
3558 bpyob
= node
.blendData
= node
.blendObject
= bpy
.data
.objects
.new('AnimOb', None) # , name)
3559 bpycollection
.objects
.link(bpyob
)
3560 bpyob
.select_set(True)
3562 if node
.blendData
.animation_data
is None:
3563 node
.blendData
.animation_data_create()
3565 node
.blendData
.animation_data
.action
= action
3568 if PREF_FLAT
is False:
3570 for node
, ancestry
in all_nodes
:
3571 if node
.blendObject
:
3574 # Get the last parent
3578 blendObject
= ancestry
[i
].blendObject
3583 # Parent Slow, - 1 liner but works
3584 # blendObject.makeParent([node.blendObject], 0, 1)
3588 child_dict
[blendObject
].append(node
.blendObject
)
3590 child_dict
[blendObject
] = [node
.blendObject
]
3593 for parent
, children
in child_dict
.items():
3598 bpycontext
.view_layer
.update()
3602 def load_with_profiler(
3610 pro
= cProfile
.Profile()
3611 pro
.runctx("load_web3d(context, filepath, PREF_FLAT=True, "
3612 "PREF_CIRCLE_DIV=16, global_matrix=global_matrix)",
3613 globals(), locals())
3614 st
= pstats
.Stats(pro
)
3615 st
.sort_stats("time")
3617 # st.print_callers(0.1)
3626 # loadWithProfiler(operator, context, filepath, global_matrix)
3627 load_web3d(context
, filepath
,
3630 global_matrix
=global_matrix
,