1 | """Mesh and animation export classes. |
---|
2 | |
---|
3 | @author Michael Reimpell |
---|
4 | """ |
---|
5 | # Copyright (C) 2005 Michael Reimpell |
---|
6 | # |
---|
7 | # This library is free software; you can redistribute it and/or |
---|
8 | # modify it under the terms of the GNU Lesser General Public |
---|
9 | # License as published by the Free Software Foundation; either |
---|
10 | # version 2.1 of the License, or (at your option) any later version. |
---|
11 | # |
---|
12 | # This library is distributed in the hope that it will be useful, |
---|
13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of |
---|
14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU |
---|
15 | # Lesser General Public License for more details. |
---|
16 | # |
---|
17 | # You should have received a copy of the GNU Lesser General Public |
---|
18 | # License along with this library; if not, write to the Free Software |
---|
19 | # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA |
---|
20 | |
---|
21 | # epydoc doc format |
---|
22 | __docformat__ = "javadoc en" |
---|
23 | |
---|
24 | import base |
---|
25 | from base import * |
---|
26 | import materialexport |
---|
27 | from materialexport import * |
---|
28 | import armatureexport |
---|
29 | from armatureexport import * |
---|
30 | |
---|
31 | import Blender |
---|
32 | import Blender.Mathutils |
---|
33 | from Blender.Mathutils import * |
---|
34 | import math |
---|
35 | |
---|
36 | # OGRE_VERTEXCOLOUR_BGRA |
---|
37 | # workaround for Ogre's vertex colour conversion bug. |
---|
38 | # Set to 0 for RGBA, 1 for BGRA. |
---|
39 | OGRE_OPENGL_VERTEXCOLOUR = 1 |
---|
40 | |
---|
41 | matrixOne = [[1,0,0,0],[0,1,0,0],[0,0,1,0],[0,0,0,1]] |
---|
42 | |
---|
43 | class Vertex: |
---|
44 | """ |
---|
45 | """ |
---|
46 | THRESHOLD = 1e-6 |
---|
47 | def __init__(self, bMesh, bMFace, bIndex, index, parentTransform, armatureExporter=None): |
---|
48 | """Represents an Ogre vertex. |
---|
49 | |
---|
50 | @param bIndex Index in the vertex list of the NMFace. |
---|
51 | @param index Vertexbuffer position. |
---|
52 | @param parentTransform Additional transformation to apply to the vertex. |
---|
53 | """ |
---|
54 | self.bMesh = bMesh |
---|
55 | # imples position |
---|
56 | # vertex in basis shape |
---|
57 | self.bMVert = bMFace.v[bIndex] |
---|
58 | bKey = self.bMesh.getKey() |
---|
59 | if (bKey and len(bKey.blocks)): |
---|
60 | # first shape key is rest position |
---|
61 | self.bMVert = bKey.blocks[0].data[self.bMVert.index] |
---|
62 | ## Face properties in Blender |
---|
63 | self.normal = None |
---|
64 | self.colourDiffuse = None |
---|
65 | self.texcoord = None |
---|
66 | ## bookkeeping |
---|
67 | # vertexbuffer position in vertexbuffer |
---|
68 | self.index = index |
---|
69 | self.parentTransform = parentTransform |
---|
70 | # implies influences |
---|
71 | self.armatureExporter = armatureExporter |
---|
72 | ### populated attributes |
---|
73 | ## normal |
---|
74 | if bMFace.smooth: |
---|
75 | # key blocks don't have normals |
---|
76 | self.normal = self._applyParentTransform(bMFace.v[bIndex].no) |
---|
77 | else: |
---|
78 | # create face normal |
---|
79 | # 1 - 2 |
---|
80 | # | / |
---|
81 | # 3 |
---|
82 | # n = (v_3 - v_1) x (v_2 - v_1)/||n|| |
---|
83 | if (bKey and len(bKey.blocks)): |
---|
84 | # first shape key is rest position |
---|
85 | blockData = bKey.blocks[0].data |
---|
86 | v1 = self._applyParentTransform(blockData[bMFace.v[0].index].co) |
---|
87 | v2 = self._applyParentTransform(blockData[bMFace.v[1].index].co) |
---|
88 | v3 = self._applyParentTransform(blockData[bMFace.v[2].index].co) |
---|
89 | else: |
---|
90 | # self.normal = CrossVecs(bMFace.v[1].co - bMFace.v[0].co, bMFace.v[2].co - bMFace.v[0].co) |
---|
91 | v1 = self._applyParentTransform(bMFace.v[0].co) |
---|
92 | v2 = self._applyParentTransform(bMFace.v[1].co) |
---|
93 | v3 = self._applyParentTransform(bMFace.v[2].co) |
---|
94 | self.normal = CrossVecs(v2 - v1, v3 - v1) |
---|
95 | # self.normal.normalize() does not throw ZeroDivisionError exception |
---|
96 | normalLength = self.normal.length |
---|
97 | if (normalLength > Vertex.THRESHOLD): |
---|
98 | self.normal = Vector([coordinate/normalLength for coordinate in self.normal]) |
---|
99 | else: |
---|
100 | Log.getSingleton().logWarning("Error in normalize! Face of mesh \"%s\" too small." % bMesh.name) |
---|
101 | self.normal = Vector([0,0,0]) |
---|
102 | ## colourDiffuse |
---|
103 | #Mesh#if bMesh.vertexColors: |
---|
104 | if bMesh.hasVertexColours(): |
---|
105 | bMCol = bMFace.col[bIndex] |
---|
106 | if OGRE_OPENGL_VERTEXCOLOUR: |
---|
107 | self.colourDiffuse = (bMCol.b/255.0, bMCol.g/255.0, bMCol.r/255.0, bMCol.a/255.0) |
---|
108 | else: |
---|
109 | self.colourDiffuse = (bMCol.r/255.0, bMCol.g/255.0, bMCol.b/255.0, bMCol.a/255.0) |
---|
110 | else: |
---|
111 | # Note: hasVertexColours() always returns false when uv coordinates are present. |
---|
112 | # Therefore also check "VCol Paint" and "VCol Light" buttons as well as |
---|
113 | # try if Blender's faces provide vertex colour data. |
---|
114 | try: |
---|
115 | bMCol = bMFace.col[bIndex] |
---|
116 | except: |
---|
117 | pass |
---|
118 | else: |
---|
119 | # vertex colour data available |
---|
120 | try: |
---|
121 | bMaterial = self.bMesh.materials[bMFace.mat] |
---|
122 | except: |
---|
123 | pass |
---|
124 | else: |
---|
125 | # material assigned |
---|
126 | if ((bMaterial.mode & Blender.Material.Modes["VCOL_PAINT"]) |
---|
127 | or (bMaterial.mode & Blender.Material.Modes["VCOL_LIGHT"])): |
---|
128 | # vertex colours enabled |
---|
129 | if OGRE_OPENGL_VERTEXCOLOUR: |
---|
130 | self.colourDiffuse = (bMCol.b/255.0, bMCol.g/255.0, bMCol.r/255.0, bMCol.a/255.0) |
---|
131 | else: |
---|
132 | self.colourDiffuse = (bMCol.r/255.0, bMCol.g/255.0, bMCol.b/255.0, bMCol.a/255.0) |
---|
133 | ## texcoord |
---|
134 | # origin in OGRE is top-left |
---|
135 | #Mesh#if bMesh.faceUV: |
---|
136 | if bMesh.hasFaceUV(): |
---|
137 | self.texcoord = (bMFace.uv[bIndex][0], 1 - bMFace.uv[bIndex][1]) |
---|
138 | #Mesh#elif bMesh.vertexUV: |
---|
139 | elif bMesh.hasVertexUV(): |
---|
140 | self.texcoord = (self.bMVert.uvco[0], 1 - self.bMVert.uvco[1]) |
---|
141 | return |
---|
142 | def __eq__(self, other): |
---|
143 | """Tests if this vertex is equal to another vertex in the Ogre sense. |
---|
144 | |
---|
145 | Does no take parentTransform into account! |
---|
146 | Also, it does not compare the index. |
---|
147 | """ |
---|
148 | isEqual = 0 |
---|
149 | # compare index, normal, colourDiffuse and texcoord |
---|
150 | if (self.bMVert.index == other.bMVert.index): |
---|
151 | pass |
---|
152 | elif ((self.normal - other.normal).length > Vertex.THRESHOLD): |
---|
153 | pass |
---|
154 | elif ((self.texcoord and not(other.texcoord)) or |
---|
155 | (not(self.texcoord) and other.texcoord)): |
---|
156 | pass |
---|
157 | elif ((math.fabs(self.texcoord[0] - other.texcoord[0]) > Vertex.THRESHOLD) |
---|
158 | or (math.fabs(self.texcoord[1] - other.texcoord[1]) > Vertex.THRESHOLD)): |
---|
159 | pass |
---|
160 | elif ((self.colourDiffuse and not(other.colourDiffuse)) or |
---|
161 | (not(self.colourDiffuse) and other.colourDiffuse)): |
---|
162 | pass |
---|
163 | elif ((math.fabs(self.colourDiffuse[0] - other.colourDiffuse[0]) > Vertex.THRESHOLD) |
---|
164 | or (math.fabs(self.colourDiffuse[1] - other.colourDiffuse[1]) > Vertex.THRESHOLD) |
---|
165 | or (math.fabs(self.colourDiffuse[2] - other.colourDiffuse[2]) > Vertex.THRESHOLD) |
---|
166 | or (math.fabs(self.colourDiffuse[3] - other.colourDiffuse[3]) > Vertex.THRESHOLD)): |
---|
167 | pass |
---|
168 | else: |
---|
169 | isEqual = 1 |
---|
170 | return isEqual |
---|
171 | def hasDiffuseColours(self): |
---|
172 | available = False |
---|
173 | if self.colourDiffuse is not None: |
---|
174 | available = True |
---|
175 | return available |
---|
176 | def nTextureCoords(self): |
---|
177 | nCoords = 0 |
---|
178 | if self.texcoord is not None: |
---|
179 | nCoords += 1 |
---|
180 | return nCoords |
---|
181 | def writePosition(self, fileObject, indentation=0): |
---|
182 | fileObject.write(indent(indentation) + "<position x=\"%.6f\" y=\"%.6f\" z=\"%.6f\"/>\n" \ |
---|
183 | % tuple(self.getPosition())) |
---|
184 | return |
---|
185 | def writeNormal(self, fileObject, indentation=0): |
---|
186 | fileObject.write(indent(indentation) + "<normal x=\"%.6f\" y=\"%.6f\" z=\"%.6f\"/>\n" \ |
---|
187 | % tuple(self.normal)) |
---|
188 | return |
---|
189 | def writeColourDiffuse(self, fileObject, indentation=0): |
---|
190 | if self.colourDiffuse: |
---|
191 | fileObject.write(indent(indentation) + "<colour_diffuse value=\"%.6f %.6f %.6f %.6f\"/>\n" \ |
---|
192 | % self.colourDiffuse) |
---|
193 | return |
---|
194 | def writeTexcoord(self, fileObject, indentation=0): |
---|
195 | if self.texcoord: |
---|
196 | fileObject.write(indent(indentation) + "<texcoord u=\"%.6f\" v=\"%.6f\"/>\n"\ |
---|
197 | % self.texcoord) |
---|
198 | return |
---|
199 | def writeVertex(self, fileObject, indentation=0): |
---|
200 | fileObject.write(indent(indentation) + "<vertex>\n") |
---|
201 | self.writePosition(fileObject, indentation + 1) |
---|
202 | self.writeNormal(fileObject, indentation + 1) |
---|
203 | self.writeColourDiffuse(fileObject, indentation + 1) |
---|
204 | self.writeTexcoord(fileObject, indentation + 1) |
---|
205 | fileObject.write(indent(indentation) + "</vertex>\n") |
---|
206 | return |
---|
207 | def writeBoneAssignments(self, fileObject, indentation=0): |
---|
208 | nAssignments = 0 |
---|
209 | weightSum = 0 |
---|
210 | for groupName in self.bMesh.getVertGroupNames(): |
---|
211 | try: |
---|
212 | weight = self.bMesh.getVertsFromGroup(groupName, 1, [self.bMVert.index])[0][1] |
---|
213 | except IndexError: |
---|
214 | # vertex not in group groupName |
---|
215 | pass |
---|
216 | else: |
---|
217 | if weight > Vertex.THRESHOLD: |
---|
218 | boneIndex = self.armatureExporter.getBoneIndex(groupName) |
---|
219 | if boneIndex is not None: |
---|
220 | # group belongs to an OGRE bone |
---|
221 | fileObject.write(indent(indentation) + \ |
---|
222 | "<vertexboneassignment vertexindex=\"%d\" boneindex=\"%d\" weight=\"%.6f\"/>\n" \ |
---|
223 | % (self.index, boneIndex, weight)) |
---|
224 | nAssignments += 1 |
---|
225 | weightSum += weight |
---|
226 | # warnings |
---|
227 | if (nAssignments == 0): |
---|
228 | Log.getSingleton().logWarning("Vertex without bone assignment!") |
---|
229 | elif (nAssignments > 4): |
---|
230 | Log.getSingleton().logWarning("Vertex with more than 4 bone assignments!") |
---|
231 | # weightSum > 1.0 seems to be often the case. |
---|
232 | # TODO: check whether OGRE requires that it is <= 1.0 |
---|
233 | # TODO: check whether Blender takes this as a scale factor, i.e., |
---|
234 | # influence is sum_i bone_i*weight_i, or if weights are normalized to sum_i weight_i == 1 |
---|
235 | #if (weightSum > 1.0): |
---|
236 | # Log.getSingleton().logWarning("Vertex with sum of bone assignment weights > 1!") |
---|
237 | return |
---|
238 | def getIndex(self): |
---|
239 | return self.index |
---|
240 | def getMVert(self): |
---|
241 | return self.bMVert |
---|
242 | def getPosition(self): |
---|
243 | """Returns position vector of the rest position. |
---|
244 | """ |
---|
245 | return self._applyParentTransform(self.bMVert.co) |
---|
246 | def getCurrentFramePosition(self, bDeformedNMesh): |
---|
247 | """Returns position of this vertex in the current frame of the possibly deformed mesh. |
---|
248 | """ |
---|
249 | return self._applyParentTransform(bDeformedNMesh.verts[self.bMVert.index].co) |
---|
250 | def getCurrentFrameRelativePosition(self, bDeformedNMesh): |
---|
251 | """Returns relative position of this vertex in the current frame of the possibly deformed mesh. |
---|
252 | """ |
---|
253 | return (self.getCurrentFramePosition(bDeformedNMesh) - self.getPosition()) |
---|
254 | def _applyParentTransform(self, vector): |
---|
255 | """Applies transformation to threedimensional vector. |
---|
256 | """ |
---|
257 | vec = Vector(vector) |
---|
258 | vec.resize4D() |
---|
259 | vec = vec * self.parentTransform |
---|
260 | vec.resize3D() |
---|
261 | return vec |
---|
262 | |
---|
263 | class VertexManager: |
---|
264 | """ |
---|
265 | """ |
---|
266 | def __init__(self, bMesh, parentTransform, armatureExporter=None): |
---|
267 | self.bMesh = bMesh |
---|
268 | self.parentTransform = parentTransform |
---|
269 | # needed for boneassignments |
---|
270 | self.armatureExporter = armatureExporter |
---|
271 | # key: index, value: list of vertices with same MVert |
---|
272 | self.vertexDict = {} |
---|
273 | # vertices in ascending index order |
---|
274 | self.vertexList = [] |
---|
275 | return |
---|
276 | def __iter__(self): |
---|
277 | return VertexManager.Iterator(self) |
---|
278 | def getNumberOfVertices(self): |
---|
279 | """Returns the current number of vertices. |
---|
280 | """ |
---|
281 | return len(self.vertexList) |
---|
282 | def getVertex(self, bMFace, bIndex): |
---|
283 | """Returns possibly shared vertex. |
---|
284 | |
---|
285 | @param bMesh Blender Mesh. |
---|
286 | @param bMFace Blender Face. |
---|
287 | @param bIndex Index in the vertex list of the MFace. |
---|
288 | @return Corresponding vertex. |
---|
289 | """ |
---|
290 | vertex = Vertex(self.bMesh, bMFace, bIndex, len(self.vertexList), self.parentTransform, self.armatureExporter) |
---|
291 | if self.vertexDict.has_key(bMFace.v[bIndex].index): |
---|
292 | # check Ogre vertices for that Blender vertex |
---|
293 | vertexList = self.vertexDict[bMFace.v[bIndex].index] |
---|
294 | found = 0 |
---|
295 | listIndex = 0 |
---|
296 | while (not(found) and (listIndex < len(vertexList))): |
---|
297 | if (vertex == vertexList[listIndex]): |
---|
298 | vertex = vertexList[listIndex] |
---|
299 | found = 1 |
---|
300 | listIndex = listIndex + 1 |
---|
301 | if not(found): |
---|
302 | # create Ogre vertex for that Blender vertex |
---|
303 | self.vertexDict[bMFace.v[bIndex].index].append(vertex) |
---|
304 | self.vertexList.append(vertex) |
---|
305 | else: |
---|
306 | # create Ogre vertex for that Blender vertex |
---|
307 | self.vertexDict[bMFace.v[bIndex].index] = [vertex] |
---|
308 | self.vertexList.append(vertex) |
---|
309 | return vertex |
---|
310 | def writeGeometry(self, fileObject, indentation=0): |
---|
311 | fileObject.write(indent(indentation) + "<geometry vertexcount=\"%d\">\n" % len(self.vertexList)) |
---|
312 | # TODO: replace single vertexbuffer with separate position vertexbuffer for vertex animation |
---|
313 | fileObject.write(indent(indentation + 1) + "<vertexbuffer positions=\"true\" normals=\"true\"") |
---|
314 | ## optional attributes |
---|
315 | # query the first vertex in the buffer |
---|
316 | if (len(self.vertexList) > 0): |
---|
317 | firstVertex = self.vertexList[0] |
---|
318 | if firstVertex.hasDiffuseColours(): |
---|
319 | fileObject.write(" colours_diffuse=\"true\"") |
---|
320 | if (firstVertex.nTextureCoords() > 0): |
---|
321 | fileObject.write(" texture_coords=\"1\" texture_coord_dimensions_0=\"2\"") |
---|
322 | fileObject.write(">\n") |
---|
323 | for vertex in self.vertexList: |
---|
324 | vertex.writeVertex(fileObject, indentation + 2) |
---|
325 | fileObject.write(indent(indentation + 1) + "</vertexbuffer>\n") |
---|
326 | fileObject.write(indent(indentation) + "</geometry>\n") |
---|
327 | return |
---|
328 | def writeBoneAssignments(self, fileObject, indentation=0): |
---|
329 | if self.armatureExporter: |
---|
330 | fileObject.write(indent(indentation) + "<boneassignments>\n") |
---|
331 | for vertex in self.vertexList: |
---|
332 | vertex.writeBoneAssignments(fileObject, indentation + 1) |
---|
333 | fileObject.write(indent(indentation) + "</boneassignments>\n") |
---|
334 | return |
---|
335 | class Iterator: |
---|
336 | """Iterates over vertices in ascending index order. |
---|
337 | """ |
---|
338 | def __init__(self, vertexManager): |
---|
339 | self.vertexManager = vertexManager |
---|
340 | self.listIndex = 0 |
---|
341 | return |
---|
342 | def next(self): |
---|
343 | if self.listIndex >= len(self.vertexManager.vertexList): |
---|
344 | raise StopIteration |
---|
345 | self.listIndex = self.listIndex + 1 |
---|
346 | return self.vertexManager.vertexList[self.listIndex - 1] |
---|
347 | |
---|
348 | class Submesh: |
---|
349 | """Ogre submesh. |
---|
350 | """ |
---|
351 | def __init__(self, bMesh, material, index, parentTransform, armatureExporter = None): |
---|
352 | """Constructor. |
---|
353 | |
---|
354 | @param index Index of submesh in submeshes list. |
---|
355 | """ |
---|
356 | self.bMesh = bMesh |
---|
357 | self.materialName = material.getName() |
---|
358 | self.index = index |
---|
359 | self.parentTransform = parentTransform |
---|
360 | self.armatureExporter = armatureExporter |
---|
361 | self.vertexManager = VertexManager(self.bMesh, self.parentTransform, self.armatureExporter) |
---|
362 | # list of (tuple of vertice indices) |
---|
363 | self.faces =[] |
---|
364 | return |
---|
365 | def getIndex(self): |
---|
366 | return self.index |
---|
367 | def addFace(self, bMFace): |
---|
368 | """Adds a Blender face to the submesh. |
---|
369 | """ |
---|
370 | # vertex winding: |
---|
371 | # Blender: clockwise, Ogre: clockwise |
---|
372 | if (len(bMFace.v) == 3): |
---|
373 | v1 = self.vertexManager.getVertex(bMFace, 0) |
---|
374 | v2 = self.vertexManager.getVertex(bMFace, 1) |
---|
375 | v3 = self.vertexManager.getVertex(bMFace, 2) |
---|
376 | self.faces.append((v1.getIndex(), v2.getIndex(), v3.getIndex())) |
---|
377 | elif (len(bMFace.v) == 4): |
---|
378 | v1 = self.vertexManager.getVertex(bMFace, 0) |
---|
379 | v2 = self.vertexManager.getVertex(bMFace, 1) |
---|
380 | v3 = self.vertexManager.getVertex(bMFace, 2) |
---|
381 | v4 = self.vertexManager.getVertex(bMFace, 3) |
---|
382 | # Split face on shortest edge |
---|
383 | if ((v3.getPosition() - v1.getPosition()).length < (v4.getPosition() - v2.getPosition()).length): |
---|
384 | # 1 - 2 |
---|
385 | # | \ | |
---|
386 | # 4 - 3 |
---|
387 | self.faces.append((v1.getIndex(), v2.getIndex(), v3.getIndex())) |
---|
388 | self.faces.append((v1.getIndex(), v3.getIndex(), v4.getIndex())) |
---|
389 | else: |
---|
390 | # 1 - 2 |
---|
391 | # | / | |
---|
392 | # 4 _ 3 |
---|
393 | self.faces.append((v1.getIndex(), v2.getIndex(), v4.getIndex())) |
---|
394 | self.faces.append((v2.getIndex(), v3.getIndex(), v4.getIndex())) |
---|
395 | else: |
---|
396 | Log.getSingleton().logWarning("Ignored face with %d edges." % len(bMFace.v)) |
---|
397 | return |
---|
398 | def getVertexManager(self): |
---|
399 | return self.vertexManager |
---|
400 | def write(self, fileObject, indentation=0): |
---|
401 | fileObject.write(indent(indentation) + "<submesh") |
---|
402 | ## attributes |
---|
403 | fileObject.write(" material=\"%s\"" % self.materialName) |
---|
404 | fileObject.write(" usesharedvertices=\"false\"") |
---|
405 | if (self.vertexManager.getNumberOfVertices() > 65535): |
---|
406 | fileObject.write(" use32bitindexes=\"true\"") |
---|
407 | Log.getSingleton().logInfo("Switched to 32 bit indices for submesh \"%s\"!" % self.materialName) |
---|
408 | fileObject.write(">\n") |
---|
409 | ## elements |
---|
410 | self._writeFaces(fileObject, indentation + 1) |
---|
411 | self.vertexManager.writeGeometry(fileObject, indentation + 1) |
---|
412 | self.vertexManager.writeBoneAssignments(fileObject, indentation + 1) |
---|
413 | fileObject.write(indent(indentation) + "</submesh>\n") |
---|
414 | return |
---|
415 | def _writeFaces(self, fileObject, indentation): |
---|
416 | fileObject.write(indent(indentation) + "<faces") |
---|
417 | ## attributes |
---|
418 | fileObject.write(" count=\"%d\"" % len(self.faces)) |
---|
419 | fileObject.write(">\n") |
---|
420 | ## elements |
---|
421 | for face in self.faces: |
---|
422 | fileObject.write(indent(indentation + 1) + "<face v1=\"%d\" v2=\"%d\" v3=\"%d\"/>\n" % face) |
---|
423 | fileObject.write(indent(indentation) + "</faces>\n") |
---|
424 | return |
---|
425 | |
---|
426 | class SubmeshManager: |
---|
427 | """ |
---|
428 | """ |
---|
429 | def __init__(self, bMesh, parentTransform, armatureExporter=None): |
---|
430 | self.bMesh = bMesh |
---|
431 | self.parentTransform = parentTransform |
---|
432 | self.armatureExporter = armatureExporter |
---|
433 | # key: material name, value: Submesh |
---|
434 | self.submeshDict = {} |
---|
435 | # submeshes in ascending index order |
---|
436 | self.submeshList = [] |
---|
437 | return |
---|
438 | def __iter__(self): |
---|
439 | return SubmeshManager.Iterator(self) |
---|
440 | def getSubmesh(self, material): |
---|
441 | """Returns a Submesh for that material. |
---|
442 | """ |
---|
443 | submesh = None |
---|
444 | if self.submeshDict.has_key(material.getName()): |
---|
445 | submesh = self.submeshDict[material.getName()] |
---|
446 | else: |
---|
447 | # return new Submesh |
---|
448 | index = len(self.submeshList) |
---|
449 | submesh = Submesh(self.bMesh, material, index, self.parentTransform, self.armatureExporter) |
---|
450 | self.submeshDict[material.getName()] = submesh |
---|
451 | self.submeshList.append(submesh) |
---|
452 | return submesh |
---|
453 | def write(self, fileObject, indentation=0): |
---|
454 | if len(self.submeshList): |
---|
455 | fileObject.write(indent(indentation) + "<submeshes>\n") |
---|
456 | for submesh in self.submeshList: |
---|
457 | submesh.write(fileObject, indentation + 1) |
---|
458 | fileObject.write(indent(indentation) + "</submeshes>\n") |
---|
459 | return |
---|
460 | class Iterator: |
---|
461 | """Iterates over submeshes in ascending index order. |
---|
462 | """ |
---|
463 | def __init__(self, submeshManager): |
---|
464 | self.submeshManager = submeshManager |
---|
465 | self.listIndex = 0 |
---|
466 | return |
---|
467 | def next(self): |
---|
468 | if self.listIndex >= len(self.submeshManager.submeshList): |
---|
469 | raise StopIteration |
---|
470 | self.listIndex = self.listIndex + 1 |
---|
471 | return self.submeshManager.submeshList[self.listIndex - 1] |
---|
472 | |
---|
473 | class Pose: |
---|
474 | """ |
---|
475 | """ |
---|
476 | THRESHOLD = 1e-7 |
---|
477 | def __init__(self, bKeyBlock, submesh, index, parentTransform): |
---|
478 | """Constructor. |
---|
479 | |
---|
480 | @param index Index of pose in poses list. |
---|
481 | """ |
---|
482 | self.bKeyBlock = bKeyBlock |
---|
483 | self.submesh = submesh |
---|
484 | self.index = index |
---|
485 | self.parentTransform = parentTransform |
---|
486 | # list of pose offset tuples (vertexIndex, deltaX, deltaY, deltaZ) |
---|
487 | self.poseoffsetList = [] |
---|
488 | # calculate poseoffsets |
---|
489 | poseVertexList = self.bKeyBlock.data |
---|
490 | for vertex in self.submesh.getVertexManager(): |
---|
491 | offset = self._applyParentTransform(poseVertexList[vertex.getMVert().index]) \ |
---|
492 | - vertex.getPosition() |
---|
493 | if (offset.length > Pose.THRESHOLD): |
---|
494 | self.poseoffsetList.append((vertex.getIndex(), offset.x, offset.y, offset.z)) |
---|
495 | return |
---|
496 | def getIndex(self): |
---|
497 | return self.index |
---|
498 | def getInfluence(self): |
---|
499 | """Returns influence of this pose in the current frame. |
---|
500 | """ |
---|
501 | return self.bKeyBlock.curval |
---|
502 | def getName(self): |
---|
503 | # unique name = KeyBlock name + submesh index |
---|
504 | return self.bKeyBlock.name + "-" + str(self.submesh.getIndex()) |
---|
505 | def nPoseoffsets(self): |
---|
506 | return len(self.poseoffsetList) |
---|
507 | def write(self, fileObject, indentation=0): |
---|
508 | if len(self.poseoffsetList): |
---|
509 | fileObject.write(indent(indentation) + \ |
---|
510 | "<pose target=\"submesh\" index=\"%d\" name=\"%s\">\n" \ |
---|
511 | % (self.submesh.getIndex(), self.getName())) |
---|
512 | for poseoffset in self.poseoffsetList: |
---|
513 | fileObject.write(indent(indentation + 1) + \ |
---|
514 | "<poseoffset index=\"%d\" x=\"%.6f\" y=\"%.6f\" z=\"%.6f\"/>\n" \ |
---|
515 | % poseoffset) |
---|
516 | fileObject.write(indent(indentation) + "</pose>\n") |
---|
517 | return |
---|
518 | def _applyParentTransform(self, vector): |
---|
519 | """Applies transformation to threedimensional vector. |
---|
520 | """ |
---|
521 | vec = Vector(vector) |
---|
522 | vec.resize4D() |
---|
523 | vec = vec * self.parentTransform |
---|
524 | vec.resize3D() |
---|
525 | return vec |
---|
526 | |
---|
527 | class PoseManager: |
---|
528 | """ |
---|
529 | """ |
---|
530 | def __init__(self, bMesh, submeshManager, parentTransform): |
---|
531 | self.bMesh = bMesh |
---|
532 | self.submeshManager = submeshManager |
---|
533 | self.parentTransform = parentTransform |
---|
534 | # key: submesh, value: poseList |
---|
535 | self.poseListDict = {} |
---|
536 | self.poseList = [] |
---|
537 | # create poses |
---|
538 | # each keyblock creates a pose for every submesh |
---|
539 | bKey = self.bMesh.getKey() |
---|
540 | if bKey: |
---|
541 | for bKeyBlock in bKey.blocks: |
---|
542 | for submesh in self.submeshManager: |
---|
543 | index = len(self.poseList) |
---|
544 | pose = Pose(bKeyBlock, submesh, index, self.parentTransform) |
---|
545 | if (pose.nPoseoffsets() > 0): |
---|
546 | # add nonempty pose to list and dict |
---|
547 | self.poseList.append(pose) |
---|
548 | if self.poseListDict.has_key(submesh): |
---|
549 | self.poseListDict[submesh].append(pose) |
---|
550 | else: |
---|
551 | self.poseListDict[submesh] = [pose] |
---|
552 | return |
---|
553 | def getPoseList(self, submesh): |
---|
554 | if self.poseListDict.has_key(submesh): |
---|
555 | poseList = self.poseListDict[submesh] |
---|
556 | else: |
---|
557 | poseList = [] |
---|
558 | return poseList |
---|
559 | def nPoses(self): |
---|
560 | return len(self.poseList) |
---|
561 | def write(self, fileObject, indentation=0): |
---|
562 | if len(self.poseList): |
---|
563 | fileObject.write(indent(indentation) + "<poses>\n") |
---|
564 | for pose in self.poseList: |
---|
565 | pose.write(fileObject, indentation + 1) |
---|
566 | fileObject.write(indent(indentation) + "</poses>\n") |
---|
567 | return |
---|
568 | |
---|
569 | class MorphAnimationTrack: |
---|
570 | """ |
---|
571 | """ |
---|
572 | def __init__(self, submesh): |
---|
573 | """Constructor. |
---|
574 | |
---|
575 | @param submesh Submesh. |
---|
576 | """ |
---|
577 | self.submesh = submesh |
---|
578 | # key: time, value: list of position in same order as in the VertexManager. |
---|
579 | self.keyframeDict = {} |
---|
580 | return |
---|
581 | def addKeyframe(self, bDeformedNMesh, time): |
---|
582 | """Append current frame as keyframe at given time. |
---|
583 | """ |
---|
584 | positionList = [] |
---|
585 | for vertex in self.submesh.getVertexManager(): |
---|
586 | positionList.append(vertex.getCurrentFramePosition(bDeformedNMesh)) |
---|
587 | self.keyframeDict[time] = positionList |
---|
588 | return |
---|
589 | def write(self, fileObject, indentation): |
---|
590 | fileObject.write(indent(indentation) + "<track target=\"submesh\" index=\"%d\" type=\"morph\">\n" \ |
---|
591 | % self.submesh.getIndex()) |
---|
592 | fileObject.write(indent(indentation + 1) + "<keyframes>\n") |
---|
593 | timeList = self.keyframeDict.keys() |
---|
594 | timeList.sort() |
---|
595 | for time in timeList: |
---|
596 | fileObject.write(indent(indentation + 2) + "<keyframe time=\"%.6f\">\n" % time) |
---|
597 | for position in self.keyframeDict[time]: |
---|
598 | fileObject.write(indent(indentation + 3) + "<position x=\"%.6f\" y=\"%.6f\" z=\"%.6f\"/>\n" \ |
---|
599 | % tuple(position)) |
---|
600 | fileObject.write(indent(indentation + 2) + "</keyframe>\n") |
---|
601 | fileObject.write(indent(indentation + 1) + "</keyframes>\n") |
---|
602 | fileObject.write(indent(indentation) + "</track>\n") |
---|
603 | return |
---|
604 | |
---|
605 | class PoseAnimationTrack: |
---|
606 | """Track with a single pose as keyframes. |
---|
607 | """ |
---|
608 | THRESHOLD = 1e-6 |
---|
609 | def __init__(self, submesh, poseManager): |
---|
610 | """Constructor. |
---|
611 | |
---|
612 | @param submesh Submesh. |
---|
613 | """ |
---|
614 | self.submesh = submesh |
---|
615 | self.poseManager = poseManager |
---|
616 | # key: time, value: list of poseref tuples (poseindex, influence). |
---|
617 | self.keyframeDict = {} |
---|
618 | return |
---|
619 | def nKeyframes(self): |
---|
620 | return len(self.keyframeDict) |
---|
621 | def addKeyframe(self, time): |
---|
622 | for pose in self.poseManager.getPoseList(self.submesh): |
---|
623 | if (pose.getInfluence() > PoseAnimationTrack.THRESHOLD): |
---|
624 | poseref = (pose.getIndex(), pose.getInfluence()) |
---|
625 | if self.keyframeDict.has_key(time): |
---|
626 | self.keyframeDict[time].append(poseref) |
---|
627 | else: |
---|
628 | self.keyframeDict[time] = [poseref] |
---|
629 | return |
---|
630 | def write(self, fileObject, indentation): |
---|
631 | fileObject.write(indent(indentation) + \ |
---|
632 | "<track target=\"submesh\" index=\"%d\" type=\"pose\">\n" \ |
---|
633 | % self.submesh.getIndex()) |
---|
634 | fileObject.write(indent(indentation + 1) + "<keyframes>\n") |
---|
635 | timeList = self.keyframeDict.keys() |
---|
636 | timeList.sort() |
---|
637 | for time in timeList: |
---|
638 | fileObject.write(indent(indentation + 2) + "<keyframe time=\"%.6f\">\n" % time) |
---|
639 | for poseref in self.keyframeDict[time]: |
---|
640 | fileObject.write(indent(indentation + 3) + \ |
---|
641 | "<poseref poseindex=\"%d\" influence=\"%.6f\"/>\n" \ |
---|
642 | % poseref) |
---|
643 | fileObject.write(indent(indentation + 2) + "</keyframe>\n") |
---|
644 | fileObject.write(indent(indentation + 1) + "</keyframes>\n") |
---|
645 | fileObject.write(indent(indentation) + "</track>\n") |
---|
646 | return |
---|
647 | |
---|
648 | class VertexAnimation: |
---|
649 | """Animation base class. |
---|
650 | """ |
---|
651 | def __init__(self, name, startFrame, endFrame): |
---|
652 | self.name = name |
---|
653 | self.startFrame = startFrame |
---|
654 | self.endFrame = endFrame |
---|
655 | ## populated on export |
---|
656 | self.length = None |
---|
657 | # same order as submeshList of the SubmeshManager |
---|
658 | self.trackList = None |
---|
659 | return |
---|
660 | def getName(self): |
---|
661 | return self.name |
---|
662 | def write(self, fileObject, indentation=0): |
---|
663 | if (len(self.trackList) > 0): |
---|
664 | fileObject.write(indent(indentation) + "<animation name=\"%s\" length = \"%.6f\">\n" \ |
---|
665 | % (self.name, self.length)) |
---|
666 | fileObject.write(indent(indentation + 1) + "<tracks>\n") |
---|
667 | for track in self.trackList: |
---|
668 | track.write(fileObject, indentation + 2) |
---|
669 | fileObject.write(indent(indentation + 1) + "</tracks>\n") |
---|
670 | fileObject.write(indent(indentation) + "</animation>\n") |
---|
671 | else: |
---|
672 | Log.getSingleton().logWarning("Skipped animation \"%s\" as it has no tracks!" \ |
---|
673 | % self.name) |
---|
674 | return |
---|
675 | def _createFrameNumberDict(self): |
---|
676 | ## frames to times |
---|
677 | self.length = 0 |
---|
678 | fps = Blender.Scene.GetCurrent().getRenderingContext().framesPerSec() |
---|
679 | # frameNumberDict: key = export time, value = frame number |
---|
680 | frameNumberDict = {} |
---|
681 | if (self.startFrame <= self.endFrame): |
---|
682 | minFrame = self.startFrame |
---|
683 | maxFrame = self.endFrame |
---|
684 | else: |
---|
685 | minFrame = self.endFrame |
---|
686 | maxFrame = self.startFrame |
---|
687 | for frameNumber in range(int(minFrame), int(maxFrame+1)): |
---|
688 | if (self.startFrame <= self.endFrame): |
---|
689 | time = float(frameNumber-self.startFrame)/fps |
---|
690 | else: |
---|
691 | # backward animation |
---|
692 | time = float(self.endFrame-frameNumber)/fps |
---|
693 | # update animation duration |
---|
694 | if self.length < time: |
---|
695 | self.length = time |
---|
696 | frameNumberDict[time] = frameNumber |
---|
697 | return frameNumberDict |
---|
698 | |
---|
699 | class MorphAnimation(VertexAnimation): |
---|
700 | """Morph animation. |
---|
701 | """ |
---|
702 | def export(self, bObject, submeshManager): |
---|
703 | Log.getSingleton().logInfo("Exporting morph animation \"%s\" of mesh \"%s\"" % (self.name, bObject.getData(True))) |
---|
704 | ## submeshes to tracks |
---|
705 | self.trackList = [] |
---|
706 | for submesh in submeshManager: |
---|
707 | self.trackList.append(MorphAnimationTrack(submesh)) |
---|
708 | ## frames to times |
---|
709 | frameNumberDict = self._createFrameNumberDict() |
---|
710 | ## export |
---|
711 | timeList = frameNumberDict.keys() |
---|
712 | timeList.sort() |
---|
713 | for time in timeList: |
---|
714 | Blender.Set('curframe', frameNumberDict[time]) |
---|
715 | bDeformedNMesh = Blender.NMesh.GetRawFromObject(bObject.getName()) |
---|
716 | for track in self.trackList: |
---|
717 | track.addKeyframe(bDeformedNMesh, time) |
---|
718 | return |
---|
719 | |
---|
720 | class PoseAnimation(VertexAnimation): |
---|
721 | """Pose animation. |
---|
722 | """ |
---|
723 | def export(self, bObject, submeshManager, poseManager): |
---|
724 | Log.getSingleton().logInfo("Exporting pose animation \"%s\" of mesh \"%s\"" % (self.name, bObject.getData(True))) |
---|
725 | ## submeshes to tracks |
---|
726 | self.trackList = [] |
---|
727 | trackList = [] |
---|
728 | for submesh in submeshManager: |
---|
729 | trackList.append(PoseAnimationTrack(submesh, poseManager)) |
---|
730 | ## frames to times |
---|
731 | frameNumberDict = self._createFrameNumberDict() |
---|
732 | ## export |
---|
733 | timeList = frameNumberDict.keys() |
---|
734 | timeList.sort() |
---|
735 | for time in timeList: |
---|
736 | Blender.Set('curframe', frameNumberDict[time]) |
---|
737 | for track in trackList: |
---|
738 | track.addKeyframe(time) |
---|
739 | for track in trackList: |
---|
740 | if (track.nKeyframes() > 0): |
---|
741 | self.trackList.append(track) |
---|
742 | # at least one track? |
---|
743 | if (len(self.trackList) == 0): |
---|
744 | # no pose offsets |
---|
745 | Log.getSingleton().logWarning("Pose animation \"%s\" does not differ from restpose." % self.name) |
---|
746 | return |
---|
747 | |
---|
748 | class VertexAnimationExporter: |
---|
749 | """ |
---|
750 | """ |
---|
751 | def __init__(self, meshExporter): |
---|
752 | self.meshExporter = meshExporter |
---|
753 | self.morphAnimationList = [] |
---|
754 | self.poseAnimationList = [] |
---|
755 | self.poseManager = None |
---|
756 | return |
---|
757 | def addMorphAnimation(self, morphAnimation): |
---|
758 | """Adds a morph animation. |
---|
759 | """ |
---|
760 | self.morphAnimationList.append(morphAnimation) |
---|
761 | return |
---|
762 | def addPoseAnimation(self, poseAnimation): |
---|
763 | """Adds a pose for pose animation. |
---|
764 | """ |
---|
765 | self.poseAnimationList.append(poseAnimation) |
---|
766 | return |
---|
767 | def hasAnimation(self): |
---|
768 | return (len(self.morphAnimationList) or len(self.poseAnimationList)) |
---|
769 | def export(self, parentTransform): |
---|
770 | # generate poses |
---|
771 | self.poseManager = PoseManager(self.meshExporter.getObject().getData(), self.meshExporter.getSubmeshManager(), parentTransform) |
---|
772 | if self.hasAnimation(): |
---|
773 | # sample animations |
---|
774 | animationNameList = [] |
---|
775 | bCurrentFrame = Blender.Get('curframe') |
---|
776 | if len(self.poseAnimationList): |
---|
777 | # pose animations |
---|
778 | if (self.poseManager.nPoses() > 0): |
---|
779 | for poseAnimation in self.poseAnimationList: |
---|
780 | # warn on pose animation name clash |
---|
781 | animationName = poseAnimation.getName() |
---|
782 | if animationName in animationNameList: |
---|
783 | Log.getSingleton().logWarning("Duplicate animation name \"%s\" for mesh \"%s\"!" \ |
---|
784 | % (animationName, self.meshExporter.getName())) |
---|
785 | animationNameList.append(animationName) |
---|
786 | # export |
---|
787 | poseAnimation.export(self.meshExporter.getObject(), self.meshExporter.getSubmeshManager(), self.poseManager) |
---|
788 | else: |
---|
789 | Log.getSingleton().logWarning("Skipped pose animation export as mesh \"%s\"has no shape keys!" \ |
---|
790 | % self.meshExporter.getName()) |
---|
791 | # clear poseAnimationList to prevent writing |
---|
792 | self.poseAnimationList = [] |
---|
793 | if len(self.morphAnimationList): |
---|
794 | # morph and pose animation cannot share the same vertex data |
---|
795 | Log.getSingleton().logError("Skipping morph animations of mesh \"%s\": Cannot share vertex data with pose animation!" |
---|
796 | % self.meshExporter.getName()) |
---|
797 | self.morphAnimationList = [] |
---|
798 | elif len(self.morphAnimationList): |
---|
799 | # morph animations |
---|
800 | for morphAnimation in self.morphAnimationList: |
---|
801 | # warn on morph animation name clash |
---|
802 | animationName = morphAnimation.getName() |
---|
803 | if animationName in animationNameList: |
---|
804 | Log.getSingleton().logWarning("Duplicate animation name \"%s\" for mesh \"%s\"!" \ |
---|
805 | % (animationName, self.meshExporter.getName())) |
---|
806 | animationNameList.append(animationName) |
---|
807 | # export |
---|
808 | morphAnimation.export(self.meshExporter.bObject, self.meshExporter.getSubmeshManager()) |
---|
809 | Blender.Set('curframe', bCurrentFrame) |
---|
810 | return |
---|
811 | def write(self, fileObject, indentation=0): |
---|
812 | # poses |
---|
813 | self.poseManager.write(fileObject, indentation) |
---|
814 | if (len(self.morphAnimationList) or len(self.poseAnimationList)): |
---|
815 | fileObject.write(indent(indentation) + "<animations>\n") |
---|
816 | if len(self.poseAnimationList): |
---|
817 | # pose animations |
---|
818 | for poseAnimation in self.poseAnimationList: |
---|
819 | poseAnimation.write(fileObject, indentation + 1) |
---|
820 | elif len(self.morphAnimationList): |
---|
821 | # morph animations |
---|
822 | for morphAnimation in self.morphAnimationList: |
---|
823 | morphAnimation.write(fileObject, indentation + 1) |
---|
824 | fileObject.write(indent(indentation) + "</animations>\n") |
---|
825 | return |
---|
826 | |
---|
827 | class MeshExporter: |
---|
828 | """Exports a Blender mesh to Ogre. |
---|
829 | |
---|
830 | Exports mesh, armature and animations to Ogre XML resp. script files. Materials are |
---|
831 | exported to a MaterialManager. |
---|
832 | """ |
---|
833 | def __init__(self, bObject): |
---|
834 | """ |
---|
835 | """ |
---|
836 | # mesh |
---|
837 | self.bObject = bObject |
---|
838 | self.name = self.bObject.getData(True) |
---|
839 | # vertex animations |
---|
840 | self.vertexAnimationExporter = VertexAnimationExporter(self) |
---|
841 | # skeleton |
---|
842 | self.armatureExporter = None |
---|
843 | parent = GetArmatureObject(self.bObject) |
---|
844 | if (parent is not None): |
---|
845 | self.armatureExporter = ArmatureExporter(self.bObject, parent) |
---|
846 | # populated on export |
---|
847 | self.submeshManager = None |
---|
848 | return |
---|
849 | def export(self, dir, materialManager=MaterialManager(), parentTransform=Matrix(*matrixOne), colouredAmbient=False, gameEngineMaterials=False, convertXML=False): |
---|
850 | # leave editmode |
---|
851 | editmode = Blender.Window.EditMode() |
---|
852 | if editmode: |
---|
853 | Blender.Window.EditMode(0) |
---|
854 | Log.getSingleton().logInfo("Exporting mesh \"%s\"" % self.getName()) |
---|
855 | ## export possible armature |
---|
856 | if self.armatureExporter: |
---|
857 | self.armatureExporter.export(dir, parentTransform, convertXML) |
---|
858 | ## export meshdata |
---|
859 | self._generateSubmeshes(parentTransform, materialManager, colouredAmbient, gameEngineMaterials) |
---|
860 | ## export vertex animations |
---|
861 | self.vertexAnimationExporter.export(parentTransform) |
---|
862 | ## write files |
---|
863 | self._write(dir, convertXML) |
---|
864 | ## cleanup |
---|
865 | self.submeshManager = None |
---|
866 | # reenter editmode |
---|
867 | if editmode: |
---|
868 | Blender.Window.EditMode(1) |
---|
869 | return |
---|
870 | def getObject(self): |
---|
871 | return self.bObject |
---|
872 | def getName(self): |
---|
873 | return self.name |
---|
874 | def getVertexAnimationExporter(self): |
---|
875 | return self.vertexAnimationExporter |
---|
876 | def getArmatureExporter(self): |
---|
877 | return self.armatureExporter |
---|
878 | def getSubmeshManager(self): |
---|
879 | return self.submeshManager |
---|
880 | def _generateSubmeshes(self, parentTransform, materialManager, colouredAmbient, gameEngineMaterials): |
---|
881 | """Generates submeshes of the mesh. |
---|
882 | """ |
---|
883 | #NMesh# Blender.Mesh.Mesh does not provide access to mesh shape keys, use Blender.NMesh.NMesh |
---|
884 | bMesh = self.bObject.getData() |
---|
885 | self.submeshManager = SubmeshManager(bMesh, parentTransform, self.armatureExporter) |
---|
886 | for bMFace in bMesh.faces: |
---|
887 | faceMaterial = materialManager.getMaterial(bMesh, bMFace, colouredAmbient, gameEngineMaterials) |
---|
888 | if faceMaterial: |
---|
889 | # append face to submesh |
---|
890 | self.submeshManager.getSubmesh(faceMaterial).addFace(bMFace) |
---|
891 | return |
---|
892 | def _write(self, dir, convertXML): |
---|
893 | file = self.getName() + ".mesh.xml" |
---|
894 | Log.getSingleton().logInfo("Writing mesh file \"%s\"" % file) |
---|
895 | fileObject = open(os.path.join(dir, file), "w") |
---|
896 | fileObject.write(indent(0)+"<mesh>\n") |
---|
897 | # submeshes |
---|
898 | self.submeshManager.write(fileObject, 1) |
---|
899 | # skeleton |
---|
900 | if self.armatureExporter: |
---|
901 | fileObject.write(indent(1)+"<skeletonlink name=\"%s.skeleton\"/>\n" % self.armatureExporter.getName()) |
---|
902 | # vertex animations |
---|
903 | self.vertexAnimationExporter.write(fileObject, 1) |
---|
904 | fileObject.write(indent(0)+"</mesh>\n") |
---|
905 | fileObject.close() |
---|
906 | if convertXML: |
---|
907 | OgreXMLConverter.getSingleton().convert(os.path.join(dir, file)) |
---|
908 | return |
---|