forked from godotengine/godot-blender-exporter
-
Notifications
You must be signed in to change notification settings - Fork 0
/
structures.py
486 lines (392 loc) · 16.4 KB
/
structures.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
"""The Godot file format has several concepts such as headings and subresources
This file contains classes to help dealing with the actual writing to the file
"""
import os
import math
import copy
import collections
import mathutils
class ValidationError(Exception):
"""An error type for explicitly delivering error messages to user."""
class ESCNFile:
"""The ESCN file consists of three major sections:
- paths to external resources
- internal resources
- nodes
Because the write order is important, you have to know all the resources
before you can start writing nodes. This class acts as a container to store
the file before it can be written out in full
Things appended to this file should have the method "to_string()" which is
used when writing the file
"""
def __init__(self, heading):
self.heading = heading
self.nodes = []
self.internal_resources = []
self._internal_hashes = {}
self.external_resources = []
self._external_hashes = {}
def get_external_resource(self, hashable):
"""Searches for existing external resources, and returns their
resource ID. Returns None if it isn't in the file"""
return self._external_hashes.get(hashable)
def add_external_resource(self, item, hashable):
"""External resources are indexed by ID. This function ensures no
two items have the same ID. It returns the index to the resource.
An error is thrown if the hashable matches an existing resource. You
should check get_external_resource before converting it into godot
format
The resource is not written to the file until the end, so you can
modify the resource after adding it to the file"""
if self.get_external_resource(hashable) is not None:
raise Exception("Attempting to add object to file twice")
self.external_resources.append(item)
index = len(self.external_resources)
item.heading['id'] = index
self._external_hashes[hashable] = index
return index
def get_internal_resource(self, hashable):
"""Searches for existing internal resources, and returns their
resource ID"""
return self._internal_hashes.get(hashable)
def add_internal_resource(self, item, hashable):
"""See comment on external resources. It's the same"""
if self.get_internal_resource(hashable) is not None:
raise Exception("Attempting to add object to file twice")
resource_id = self.force_add_internal_resource(item)
self._internal_hashes[hashable] = resource_id
return resource_id
def force_add_internal_resource(self, item):
"""Add an internal resource without providing an hashable,
ATTENTION: it should not be called unless an hashable can not
be found"""
self.internal_resources.append(item)
index = len(self.internal_resources)
item.heading['id'] = index
return index
def add_node(self, item):
"""Adds a node to this file. Nodes aren't indexed, so none of
the complexity of the other resource types"""
self.nodes.append(item)
def fix_paths(self, export_settings):
"""Ensures all external resource paths are relative to the exported
file"""
for res in self.external_resources:
res.fix_path(export_settings)
def to_string(self):
"""Serializes the file ready to dump out to disk"""
sections = (
self.heading.to_string(),
'\n\n'.join(i.to_string() for i in self.external_resources),
'\n\n'.join(e.to_string() for e in self.internal_resources),
'\n\n'.join(n.to_string() for n in self.nodes)
)
return "\n\n".join([s for s in sections if s]) + "\n"
class FileEntry(collections.OrderedDict):
'''Everything inside the file looks pretty much the same. A heading
that looks like [type key=val key=val...] and contents that is newline
separated key=val pairs. This FileEntry handles the serialization of
on entity into this form'''
def __init__(self, entry_type, heading_dict=(), values_dict=()):
self.entry_type = entry_type
self.heading = collections.OrderedDict(heading_dict)
# This string is copied verbaitum, so can be used for custom writing
self.contents = ''
super().__init__(values_dict)
def generate_heading_string(self):
"""Convert the heading dict into [type key=val key=val ...]"""
out_str = '[{}'.format(self.entry_type)
for var in self.heading:
val = self.heading[var]
if isinstance(val, str):
val = '"{}"'.format(val)
out_str += " {}={}".format(var, val)
out_str += ']'
return out_str
def generate_body_string(self):
"""Convert the contents of the super/internal dict into newline
separated key=val pairs"""
lines = []
for var in self:
val = self[var]
val = to_string(val)
lines.append('{} = {}'.format(var, val))
return "\n".join(lines)
def to_string(self):
"""Serialize this entire entry"""
heading = self.generate_heading_string()
body = self.generate_body_string()
if body and self.contents:
return "{}\n\n{}\n{}".format(heading, body, self.contents)
if body:
return "{}\n\n{}".format(heading, body)
return heading
class NodeTemplate(FileEntry):
"""Most things inside the escn file are Nodes that make up the scene tree.
This is a template node that can be used to contruct nodes of any type.
It is not intended that other classes in the exporter inherit from this,
but rather that all the exported nodes use this template directly."""
def __init__(self, name, node_type, parent_node):
# set child, parent relation
self.children = []
self.parent = parent_node
# filter out special character
invalid_chs = ('.', '\\', '/', ':')
node_name = ''.join(filter(lambda ch: ch not in invalid_chs, name))
if parent_node is not None:
# solve duplication
counter = 1
child_name_set = {c.get_name() for c in self.parent.children}
node_name_base = node_name
while node_name in child_name_set:
node_name = node_name_base + str(counter).zfill(3)
counter += 1
parent_node.children.append(self)
super().__init__(
"node",
collections.OrderedDict((
("name", node_name),
("type", node_type),
("parent", parent_node.get_path())
))
)
else:
# root node
super().__init__(
"node",
collections.OrderedDict((
("type", node_type),
("name", node_name)
))
)
def get_name(self):
"""Get the name of the node in Godot scene"""
return self.heading['name']
def get_path(self):
"""Get the node path in the Godot scene"""
# root node
if 'parent' not in self.heading:
return '.'
# children of root node
if self.heading['parent'] == '.':
return self.heading['name']
return self.heading['parent'] + '/' + self.heading['name']
def get_type(self):
"""Get the node type in Godot scene"""
return self.heading["type"]
class ExternalResource(FileEntry):
"""External Resouces are references to external files. In the case of
an escn export, this is mostly used for images, sounds and so on"""
def __init__(self, path, resource_type):
super().__init__(
'ext_resource',
collections.OrderedDict((
# ID is overwritten by ESCN_File.add_external_resource
('id', None),
('path', path),
('type', resource_type)
))
)
def fix_path(self, export_settings):
"""Makes the resource path relative to the exported file"""
# The replace line is because godot always works in linux
# style slashes, and python doing relpath uses the one
# from the native OS
self.heading['path'] = os.path.relpath(
self.heading['path'],
os.path.dirname(export_settings["path"]),
).replace('\\', '/')
class InternalResource(FileEntry):
""" A resource stored internally to the escn file, such as the
description of a material """
def __init__(self, resource_type, name):
super().__init__(
'sub_resource',
collections.OrderedDict((
# ID is overwritten by ESCN_File.add_internal_resource
('id', None),
('type', resource_type)
))
)
self['resource_name'] = '"{}"'.format(
name.replace('.', '').replace('/', '')
)
class Array(list):
"""In the escn file there are lots of arrays which are defined by
a type (eg Vector3Array) and then have lots of values. This helps
to serialize that sort of array. You can also pass in custom separators
and suffixes.
Note that the constructor values parameter flattens the list using the
add_elements method
"""
def __init__(self, prefix, seperator=', ', suffix=')', values=()):
self.prefix = prefix
self.seperator = seperator
self.suffix = suffix
super().__init__()
self.add_elements(values)
self.__str__ = self.to_string
def add_elements(self, list_of_lists):
"""Add each element from a list of lists to the array (flatten the
list of lists)"""
for lis in list_of_lists:
self.extend(lis)
def to_string(self):
"""Convert the array to serialized form"""
return "{}{}{}".format(
self.prefix,
self.seperator.join([to_string(v) for v in self]),
self.suffix
)
class Map(collections.OrderedDict):
"""An ordered dict, used to serialize to a dict to escn file. Note
that the key should be string, but for the value will be applied
with to_string() method"""
def __init__(self):
super().__init__()
self.__str__ = self.to_string
def to_string(self):
"""Convert the map to serialized form"""
return ("{\n\t" +
',\n\t'.join(['"{}":{}'.format(k, to_string(v))
for k, v in self.items()]) +
"\n}")
class NodePath:
"""Node in scene points to other node or node's attribute,
for example, a MeshInstane points to a Skeleton. """
def __init__(self, from_here, to_there, attribute_pointed=''):
self.relative_path = os.path.normpath(
os.path.relpath(to_there, from_here)
)
if os.sep == '\\':
# Ensure node path use '/' on windows as well
self.relative_path = self.relative_path.replace('\\', '/')
self.attribute_name = attribute_pointed
def new_copy(self, attribute=None):
"""Return a new instance of the current NodePath and
able to change the attribute pointed"""
new_node_path = copy.deepcopy(self)
if attribute is not None:
new_node_path.attribute_name = attribute
return new_node_path
def to_string(self):
"""Serialize a node path"""
return 'NodePath("{}:{}")'.format(
self.relative_path,
self.attribute_name
)
class RGBA:
"""Color with an Alpha channel.
Use when you need to export a color with alpha, as mathutils.Color lacks
an alpha channel.
See https://developer.blender.org/T53540
"""
def __init__(self, values):
self.values = values
def to_string(self):
"""Convert the color to serialized form"""
return color_to_string(self.values)
def fix_matrix(mtx):
""" Shuffles a matrix to change from y-up to z-up"""
# TODO: can this be replaced my a matrix multiplcation?
trans = mathutils.Matrix(mtx)
up_axis = 2
for i in range(3):
trans[1][i], trans[up_axis][i] = trans[up_axis][i], trans[1][i]
for i in range(3):
trans[i][1], trans[i][up_axis] = trans[i][up_axis], trans[i][1]
trans[1][3], trans[up_axis][3] = trans[up_axis][3], trans[1][3]
trans[up_axis][0] = -trans[up_axis][0]
trans[up_axis][1] = -trans[up_axis][1]
trans[0][up_axis] = -trans[0][up_axis]
trans[1][up_axis] = -trans[1][up_axis]
trans[up_axis][3] = -trans[up_axis][3]
return trans
_AXIS_CORRECT = mathutils.Matrix.Rotation(math.radians(-90), 4, 'X')
def fix_directional_transform(mtx):
"""Used to correct spotlights and cameras, which in blender are
Z-forwards and in Godot are Y-forwards"""
return mtx @ _AXIS_CORRECT
def fix_bone_attachment_transform(attachment_obj, blender_transform):
"""Godot and blender bone children nodes' transform relative to
different bone joints, so there is a difference of bone_length
along bone direction axis"""
armature_obj = attachment_obj.parent
bone_length = armature_obj.data.bones[attachment_obj.parent_bone].length
mtx = mathutils.Matrix(blender_transform)
mtx[1][3] += bone_length
return mtx
def fix_bone_attachment_location(attachment_obj, location_vec):
"""Fix the bone length difference in location vec3 of
BoneAttachment object"""
armature_obj = attachment_obj.parent
bone_length = armature_obj.data.bones[attachment_obj.parent_bone].length
vec = mathutils.Vector(location_vec)
vec[1] += bone_length
return vec
def gamma_correct(color):
"""Apply sRGB color space gamma correction to the given color"""
if isinstance(color, float):
# seperate color channel
return color ** (1 / 2.2)
# mathutils.Color does not support alpha yet, so just use RGB
# see: https://developer.blender.org/T53540
color = color[0:3]
# note that here use a widely mentioned sRGB approximation gamma = 2.2
# it is good enough, the exact gamma of sRGB can be find at
# https://en.wikipedia.org/wiki/SRGB
if len(color) > 3:
color = color[:3]
return mathutils.Color(tuple([x ** (1 / 2.2) for x in color]))
# ------------------ Implicit Conversions of Blender Types --------------------
def mat4_to_string(mtx, prefix='Transform(', suffix=')'):
"""Converts a matrix to a "Transform" string that can be parsed by Godot"""
mtx = fix_matrix(mtx)
array = Array(prefix, suffix=suffix)
for row in range(3):
for col in range(3):
array.append(mtx[row][col])
# Export the basis
for axis in range(3):
array.append(mtx[axis][3])
return array.to_string()
def color_to_string(rgba):
"""Converts an RGB colors in range 0-1 into a fomat Godot can read. Accepts
iterables of 3 or 4 in length, but is designed for mathutils.Color"""
alpha = 1.0 if len(rgba) < 4 else rgba[3]
col = list(rgba[0:3]) + [alpha]
return Array('Color(', values=[col]).to_string()
def vector_to_string(vec):
"""Encode a mathutils.vector. actually, it accepts iterable of any length,
but 2, 3 are best...."""
return Array('Vector{}('.format(len(vec)), values=[vec]).to_string()
def float_to_string(num):
"""Intelligently rounds float numbers"""
if abs(num) < 1e-15:
# This should make floating point errors round sanely. It does mean
# that if you have objects with large scaling factors and tiny meshes,
# then the object may "collapse" to zero.
# There are still some e-8's that appear in the file, but I think
# people would notice it collapsing.
return '0.0'
return '{:.6}'.format(num)
def to_string(val):
"""Attempts to convert any object into a string using the conversions
table, explicit conversion, or falling back to the str() method"""
if hasattr(val, "to_string"):
val = val.to_string()
else:
converter = CONVERSIONS.get(type(val))
if converter is not None:
val = converter(val)
else:
val = str(val)
return val
# Finds the correct conversion function for a datatype
CONVERSIONS = {
float: float_to_string,
bool: lambda x: 'true' if x else 'false',
mathutils.Matrix: mat4_to_string,
mathutils.Color: color_to_string,
mathutils.Vector: vector_to_string
}