rev |
line source |
nuclear@0
|
1 /*
|
nuclear@0
|
2 ---------------------------------------------------------------------------
|
nuclear@0
|
3 Open Asset Import Library (assimp)
|
nuclear@0
|
4 ---------------------------------------------------------------------------
|
nuclear@0
|
5
|
nuclear@0
|
6 Copyright (c) 2006-2012, assimp team
|
nuclear@0
|
7
|
nuclear@0
|
8 All rights reserved.
|
nuclear@0
|
9
|
nuclear@0
|
10 Redistribution and use of this software in source and binary forms,
|
nuclear@0
|
11 with or without modification, are permitted provided that the following
|
nuclear@0
|
12 conditions are met:
|
nuclear@0
|
13
|
nuclear@0
|
14 * Redistributions of source code must retain the above
|
nuclear@0
|
15 copyright notice, this list of conditions and the
|
nuclear@0
|
16 following disclaimer.
|
nuclear@0
|
17
|
nuclear@0
|
18 * Redistributions in binary form must reproduce the above
|
nuclear@0
|
19 copyright notice, this list of conditions and the
|
nuclear@0
|
20 following disclaimer in the documentation and/or other
|
nuclear@0
|
21 materials provided with the distribution.
|
nuclear@0
|
22
|
nuclear@0
|
23 * Neither the name of the assimp team, nor the names of its
|
nuclear@0
|
24 contributors may be used to endorse or promote products
|
nuclear@0
|
25 derived from this software without specific prior
|
nuclear@0
|
26 written permission of the assimp team.
|
nuclear@0
|
27
|
nuclear@0
|
28 THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
nuclear@0
|
29 "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
nuclear@0
|
30 LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
nuclear@0
|
31 A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
nuclear@0
|
32 OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
nuclear@0
|
33 SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
nuclear@0
|
34 LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
nuclear@0
|
35 DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
nuclear@0
|
36 THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
nuclear@0
|
37 (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
nuclear@0
|
38 OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
nuclear@0
|
39 ---------------------------------------------------------------------------
|
nuclear@0
|
40 */
|
nuclear@0
|
41
|
nuclear@0
|
42 /** @file PretransformVertices.cpp
|
nuclear@0
|
43 * @brief Implementation of the "PretransformVertices" post processing step
|
nuclear@0
|
44 */
|
nuclear@0
|
45
|
nuclear@0
|
46 #include "AssimpPCH.h"
|
nuclear@0
|
47 #include "PretransformVertices.h"
|
nuclear@0
|
48 #include "ProcessHelper.h"
|
nuclear@0
|
49 #include "SceneCombiner.h"
|
nuclear@0
|
50
|
nuclear@0
|
51 using namespace Assimp;
|
nuclear@0
|
52
|
nuclear@0
|
53 // some array offsets
|
nuclear@0
|
54 #define AI_PTVS_VERTEX 0x0
|
nuclear@0
|
55 #define AI_PTVS_FACE 0x1
|
nuclear@0
|
56
|
nuclear@0
|
57 // ------------------------------------------------------------------------------------------------
|
nuclear@0
|
58 // Constructor to be privately used by Importer
|
nuclear@0
|
59 PretransformVertices::PretransformVertices()
|
nuclear@0
|
60 : configKeepHierarchy (false)
|
nuclear@0
|
61 {
|
nuclear@0
|
62 }
|
nuclear@0
|
63
|
nuclear@0
|
64 // ------------------------------------------------------------------------------------------------
|
nuclear@0
|
65 // Destructor, private as well
|
nuclear@0
|
66 PretransformVertices::~PretransformVertices()
|
nuclear@0
|
67 {
|
nuclear@0
|
68 // nothing to do here
|
nuclear@0
|
69 }
|
nuclear@0
|
70
|
nuclear@0
|
71 // ------------------------------------------------------------------------------------------------
|
nuclear@0
|
72 // Returns whether the processing step is present in the given flag field.
|
nuclear@0
|
73 bool PretransformVertices::IsActive( unsigned int pFlags) const
|
nuclear@0
|
74 {
|
nuclear@0
|
75 return (pFlags & aiProcess_PreTransformVertices) != 0;
|
nuclear@0
|
76 }
|
nuclear@0
|
77
|
nuclear@0
|
78 // ------------------------------------------------------------------------------------------------
|
nuclear@0
|
79 // Setup import configuration
|
nuclear@0
|
80 void PretransformVertices::SetupProperties(const Importer* pImp)
|
nuclear@0
|
81 {
|
nuclear@0
|
82 // Get the current value of AI_CONFIG_PP_PTV_KEEP_HIERARCHY and AI_CONFIG_PP_PTV_NORMALIZE
|
nuclear@0
|
83 configKeepHierarchy = (0 != pImp->GetPropertyInteger(AI_CONFIG_PP_PTV_KEEP_HIERARCHY,0));
|
nuclear@0
|
84 configNormalize = (0 != pImp->GetPropertyInteger(AI_CONFIG_PP_PTV_NORMALIZE,0));
|
nuclear@0
|
85 }
|
nuclear@0
|
86
|
nuclear@0
|
87 // ------------------------------------------------------------------------------------------------
|
nuclear@0
|
88 // Count the number of nodes
|
nuclear@0
|
89 unsigned int PretransformVertices::CountNodes( aiNode* pcNode )
|
nuclear@0
|
90 {
|
nuclear@0
|
91 unsigned int iRet = 1;
|
nuclear@0
|
92 for (unsigned int i = 0;i < pcNode->mNumChildren;++i)
|
nuclear@0
|
93 {
|
nuclear@0
|
94 iRet += CountNodes(pcNode->mChildren[i]);
|
nuclear@0
|
95 }
|
nuclear@0
|
96 return iRet;
|
nuclear@0
|
97 }
|
nuclear@0
|
98
|
nuclear@0
|
99 // ------------------------------------------------------------------------------------------------
|
nuclear@0
|
100 // Get a bitwise combination identifying the vertex format of a mesh
|
nuclear@0
|
101 unsigned int PretransformVertices::GetMeshVFormat(aiMesh* pcMesh)
|
nuclear@0
|
102 {
|
nuclear@0
|
103 // the vertex format is stored in aiMesh::mBones for later retrieval.
|
nuclear@0
|
104 // there isn't a good reason to compute it a few hundred times
|
nuclear@0
|
105 // from scratch. The pointer is unused as animations are lost
|
nuclear@0
|
106 // during PretransformVertices.
|
nuclear@0
|
107 if (pcMesh->mBones)
|
nuclear@0
|
108 return (unsigned int)(uint64_t)pcMesh->mBones;
|
nuclear@0
|
109
|
nuclear@0
|
110
|
nuclear@0
|
111 const unsigned int iRet = GetMeshVFormatUnique(pcMesh);
|
nuclear@0
|
112
|
nuclear@0
|
113 // store the value for later use
|
nuclear@0
|
114 pcMesh->mBones = (aiBone**)(uint64_t)iRet;
|
nuclear@0
|
115 return iRet;
|
nuclear@0
|
116 }
|
nuclear@0
|
117
|
nuclear@0
|
118 // ------------------------------------------------------------------------------------------------
|
nuclear@0
|
119 // Count the number of vertices in the whole scene and a given
|
nuclear@0
|
120 // material index
|
nuclear@0
|
121 void PretransformVertices::CountVerticesAndFaces( aiScene* pcScene, aiNode* pcNode, unsigned int iMat,
|
nuclear@0
|
122 unsigned int iVFormat, unsigned int* piFaces, unsigned int* piVertices)
|
nuclear@0
|
123 {
|
nuclear@0
|
124 for (unsigned int i = 0; i < pcNode->mNumMeshes;++i)
|
nuclear@0
|
125 {
|
nuclear@0
|
126 aiMesh* pcMesh = pcScene->mMeshes[ pcNode->mMeshes[i] ];
|
nuclear@0
|
127 if (iMat == pcMesh->mMaterialIndex && iVFormat == GetMeshVFormat(pcMesh))
|
nuclear@0
|
128 {
|
nuclear@0
|
129 *piVertices += pcMesh->mNumVertices;
|
nuclear@0
|
130 *piFaces += pcMesh->mNumFaces;
|
nuclear@0
|
131 }
|
nuclear@0
|
132 }
|
nuclear@0
|
133 for (unsigned int i = 0;i < pcNode->mNumChildren;++i)
|
nuclear@0
|
134 {
|
nuclear@0
|
135 CountVerticesAndFaces(pcScene,pcNode->mChildren[i],iMat,
|
nuclear@0
|
136 iVFormat,piFaces,piVertices);
|
nuclear@0
|
137 }
|
nuclear@0
|
138 }
|
nuclear@0
|
139
|
nuclear@0
|
140 // ------------------------------------------------------------------------------------------------
|
nuclear@0
|
141 // Collect vertex/face data
|
nuclear@0
|
142 void PretransformVertices::CollectData( aiScene* pcScene, aiNode* pcNode, unsigned int iMat,
|
nuclear@0
|
143 unsigned int iVFormat, aiMesh* pcMeshOut,
|
nuclear@0
|
144 unsigned int aiCurrent[2], unsigned int* num_refs)
|
nuclear@0
|
145 {
|
nuclear@0
|
146 // No need to multiply if there's no transformation
|
nuclear@0
|
147 const bool identity = pcNode->mTransformation.IsIdentity();
|
nuclear@0
|
148 for (unsigned int i = 0; i < pcNode->mNumMeshes;++i)
|
nuclear@0
|
149 {
|
nuclear@0
|
150 aiMesh* pcMesh = pcScene->mMeshes[ pcNode->mMeshes[i] ];
|
nuclear@0
|
151 if (iMat == pcMesh->mMaterialIndex && iVFormat == GetMeshVFormat(pcMesh))
|
nuclear@0
|
152 {
|
nuclear@0
|
153 // Decrement mesh reference counter
|
nuclear@0
|
154 unsigned int& num_ref = num_refs[pcNode->mMeshes[i]];
|
nuclear@0
|
155 ai_assert(0 != num_ref);
|
nuclear@0
|
156 --num_ref;
|
nuclear@0
|
157
|
nuclear@0
|
158 if (identity) {
|
nuclear@0
|
159 // copy positions without modifying them
|
nuclear@0
|
160 ::memcpy(pcMeshOut->mVertices + aiCurrent[AI_PTVS_VERTEX],
|
nuclear@0
|
161 pcMesh->mVertices,
|
nuclear@0
|
162 pcMesh->mNumVertices * sizeof(aiVector3D));
|
nuclear@0
|
163
|
nuclear@0
|
164 if (iVFormat & 0x2) {
|
nuclear@0
|
165 // copy normals without modifying them
|
nuclear@0
|
166 ::memcpy(pcMeshOut->mNormals + aiCurrent[AI_PTVS_VERTEX],
|
nuclear@0
|
167 pcMesh->mNormals,
|
nuclear@0
|
168 pcMesh->mNumVertices * sizeof(aiVector3D));
|
nuclear@0
|
169 }
|
nuclear@0
|
170 if (iVFormat & 0x4)
|
nuclear@0
|
171 {
|
nuclear@0
|
172 // copy tangents without modifying them
|
nuclear@0
|
173 ::memcpy(pcMeshOut->mTangents + aiCurrent[AI_PTVS_VERTEX],
|
nuclear@0
|
174 pcMesh->mTangents,
|
nuclear@0
|
175 pcMesh->mNumVertices * sizeof(aiVector3D));
|
nuclear@0
|
176 // copy bitangents without modifying them
|
nuclear@0
|
177 ::memcpy(pcMeshOut->mBitangents + aiCurrent[AI_PTVS_VERTEX],
|
nuclear@0
|
178 pcMesh->mBitangents,
|
nuclear@0
|
179 pcMesh->mNumVertices * sizeof(aiVector3D));
|
nuclear@0
|
180 }
|
nuclear@0
|
181 }
|
nuclear@0
|
182 else
|
nuclear@0
|
183 {
|
nuclear@0
|
184 // copy positions, transform them to worldspace
|
nuclear@0
|
185 for (unsigned int n = 0; n < pcMesh->mNumVertices;++n) {
|
nuclear@0
|
186 pcMeshOut->mVertices[aiCurrent[AI_PTVS_VERTEX]+n] = pcNode->mTransformation * pcMesh->mVertices[n];
|
nuclear@0
|
187 }
|
nuclear@0
|
188 aiMatrix4x4 mWorldIT = pcNode->mTransformation;
|
nuclear@0
|
189 mWorldIT.Inverse().Transpose();
|
nuclear@0
|
190
|
nuclear@0
|
191 // TODO: implement Inverse() for aiMatrix3x3
|
nuclear@0
|
192 aiMatrix3x3 m = aiMatrix3x3(mWorldIT);
|
nuclear@0
|
193
|
nuclear@0
|
194 if (iVFormat & 0x2)
|
nuclear@0
|
195 {
|
nuclear@0
|
196 // copy normals, transform them to worldspace
|
nuclear@0
|
197 for (unsigned int n = 0; n < pcMesh->mNumVertices;++n) {
|
nuclear@0
|
198 pcMeshOut->mNormals[aiCurrent[AI_PTVS_VERTEX]+n] =
|
nuclear@0
|
199 (m * pcMesh->mNormals[n]).Normalize();
|
nuclear@0
|
200 }
|
nuclear@0
|
201 }
|
nuclear@0
|
202 if (iVFormat & 0x4)
|
nuclear@0
|
203 {
|
nuclear@0
|
204 // copy tangents and bitangents, transform them to worldspace
|
nuclear@0
|
205 for (unsigned int n = 0; n < pcMesh->mNumVertices;++n) {
|
nuclear@0
|
206 pcMeshOut->mTangents [aiCurrent[AI_PTVS_VERTEX]+n] = (m * pcMesh->mTangents[n]).Normalize();
|
nuclear@0
|
207 pcMeshOut->mBitangents[aiCurrent[AI_PTVS_VERTEX]+n] = (m * pcMesh->mBitangents[n]).Normalize();
|
nuclear@0
|
208 }
|
nuclear@0
|
209 }
|
nuclear@0
|
210 }
|
nuclear@0
|
211 unsigned int p = 0;
|
nuclear@0
|
212 while (iVFormat & (0x100 << p))
|
nuclear@0
|
213 {
|
nuclear@0
|
214 // copy texture coordinates
|
nuclear@0
|
215 memcpy(pcMeshOut->mTextureCoords[p] + aiCurrent[AI_PTVS_VERTEX],
|
nuclear@0
|
216 pcMesh->mTextureCoords[p],
|
nuclear@0
|
217 pcMesh->mNumVertices * sizeof(aiVector3D));
|
nuclear@0
|
218 ++p;
|
nuclear@0
|
219 }
|
nuclear@0
|
220 p = 0;
|
nuclear@0
|
221 while (iVFormat & (0x1000000 << p))
|
nuclear@0
|
222 {
|
nuclear@0
|
223 // copy vertex colors
|
nuclear@0
|
224 memcpy(pcMeshOut->mColors[p] + aiCurrent[AI_PTVS_VERTEX],
|
nuclear@0
|
225 pcMesh->mColors[p],
|
nuclear@0
|
226 pcMesh->mNumVertices * sizeof(aiColor4D));
|
nuclear@0
|
227 ++p;
|
nuclear@0
|
228 }
|
nuclear@0
|
229 // now we need to copy all faces. since we will delete the source mesh afterwards,
|
nuclear@0
|
230 // we don't need to reallocate the array of indices except if this mesh is
|
nuclear@0
|
231 // referenced multiple times.
|
nuclear@0
|
232 for (unsigned int planck = 0;planck < pcMesh->mNumFaces;++planck)
|
nuclear@0
|
233 {
|
nuclear@0
|
234 aiFace& f_src = pcMesh->mFaces[planck];
|
nuclear@0
|
235 aiFace& f_dst = pcMeshOut->mFaces[aiCurrent[AI_PTVS_FACE]+planck];
|
nuclear@0
|
236
|
nuclear@0
|
237 const unsigned int num_idx = f_src.mNumIndices;
|
nuclear@0
|
238
|
nuclear@0
|
239 f_dst.mNumIndices = num_idx;
|
nuclear@0
|
240
|
nuclear@0
|
241 unsigned int* pi;
|
nuclear@0
|
242 if (!num_ref) { /* if last time the mesh is referenced -> no reallocation */
|
nuclear@0
|
243 pi = f_dst.mIndices = f_src.mIndices;
|
nuclear@0
|
244
|
nuclear@0
|
245 // offset all vertex indices
|
nuclear@0
|
246 for (unsigned int hahn = 0; hahn < num_idx;++hahn){
|
nuclear@0
|
247 pi[hahn] += aiCurrent[AI_PTVS_VERTEX];
|
nuclear@0
|
248 }
|
nuclear@0
|
249 }
|
nuclear@0
|
250 else {
|
nuclear@0
|
251 pi = f_dst.mIndices = new unsigned int[num_idx];
|
nuclear@0
|
252
|
nuclear@0
|
253 // copy and offset all vertex indices
|
nuclear@0
|
254 for (unsigned int hahn = 0; hahn < num_idx;++hahn){
|
nuclear@0
|
255 pi[hahn] = f_src.mIndices[hahn] + aiCurrent[AI_PTVS_VERTEX];
|
nuclear@0
|
256 }
|
nuclear@0
|
257 }
|
nuclear@0
|
258
|
nuclear@0
|
259 // Update the mPrimitiveTypes member of the mesh
|
nuclear@0
|
260 switch (pcMesh->mFaces[planck].mNumIndices)
|
nuclear@0
|
261 {
|
nuclear@0
|
262 case 0x1:
|
nuclear@0
|
263 pcMeshOut->mPrimitiveTypes |= aiPrimitiveType_POINT;
|
nuclear@0
|
264 break;
|
nuclear@0
|
265 case 0x2:
|
nuclear@0
|
266 pcMeshOut->mPrimitiveTypes |= aiPrimitiveType_LINE;
|
nuclear@0
|
267 break;
|
nuclear@0
|
268 case 0x3:
|
nuclear@0
|
269 pcMeshOut->mPrimitiveTypes |= aiPrimitiveType_TRIANGLE;
|
nuclear@0
|
270 break;
|
nuclear@0
|
271 default:
|
nuclear@0
|
272 pcMeshOut->mPrimitiveTypes |= aiPrimitiveType_POLYGON;
|
nuclear@0
|
273 break;
|
nuclear@0
|
274 };
|
nuclear@0
|
275 }
|
nuclear@0
|
276 aiCurrent[AI_PTVS_VERTEX] += pcMesh->mNumVertices;
|
nuclear@0
|
277 aiCurrent[AI_PTVS_FACE] += pcMesh->mNumFaces;
|
nuclear@0
|
278 }
|
nuclear@0
|
279 }
|
nuclear@0
|
280
|
nuclear@0
|
281 // append all children of us
|
nuclear@0
|
282 for (unsigned int i = 0;i < pcNode->mNumChildren;++i) {
|
nuclear@0
|
283 CollectData(pcScene,pcNode->mChildren[i],iMat,
|
nuclear@0
|
284 iVFormat,pcMeshOut,aiCurrent,num_refs);
|
nuclear@0
|
285 }
|
nuclear@0
|
286 }
|
nuclear@0
|
287
|
nuclear@0
|
288 // ------------------------------------------------------------------------------------------------
|
nuclear@0
|
289 // Get a list of all vertex formats that occur for a given material index
|
nuclear@0
|
290 // The output list contains duplicate elements
|
nuclear@0
|
291 void PretransformVertices::GetVFormatList( aiScene* pcScene, unsigned int iMat,
|
nuclear@0
|
292 std::list<unsigned int>& aiOut)
|
nuclear@0
|
293 {
|
nuclear@0
|
294 for (unsigned int i = 0; i < pcScene->mNumMeshes;++i)
|
nuclear@0
|
295 {
|
nuclear@0
|
296 aiMesh* pcMesh = pcScene->mMeshes[ i ];
|
nuclear@0
|
297 if (iMat == pcMesh->mMaterialIndex) {
|
nuclear@0
|
298 aiOut.push_back(GetMeshVFormat(pcMesh));
|
nuclear@0
|
299 }
|
nuclear@0
|
300 }
|
nuclear@0
|
301 }
|
nuclear@0
|
302
|
nuclear@0
|
303 // ------------------------------------------------------------------------------------------------
|
nuclear@0
|
304 // Compute the absolute transformation matrices of each node
|
nuclear@0
|
305 void PretransformVertices::ComputeAbsoluteTransform( aiNode* pcNode )
|
nuclear@0
|
306 {
|
nuclear@0
|
307 if (pcNode->mParent) {
|
nuclear@0
|
308 pcNode->mTransformation = pcNode->mParent->mTransformation*pcNode->mTransformation;
|
nuclear@0
|
309 }
|
nuclear@0
|
310
|
nuclear@0
|
311 for (unsigned int i = 0;i < pcNode->mNumChildren;++i) {
|
nuclear@0
|
312 ComputeAbsoluteTransform(pcNode->mChildren[i]);
|
nuclear@0
|
313 }
|
nuclear@0
|
314 }
|
nuclear@0
|
315
|
nuclear@0
|
316 // ------------------------------------------------------------------------------------------------
|
nuclear@0
|
317 // Apply the node transformation to a mesh
|
nuclear@0
|
318 void PretransformVertices::ApplyTransform(aiMesh* mesh, const aiMatrix4x4& mat)
|
nuclear@0
|
319 {
|
nuclear@0
|
320 // Check whether we need to transform the coordinates at all
|
nuclear@0
|
321 if (!mat.IsIdentity()) {
|
nuclear@0
|
322
|
nuclear@0
|
323 if (mesh->HasPositions()) {
|
nuclear@0
|
324 for (unsigned int i = 0; i < mesh->mNumVertices; ++i) {
|
nuclear@0
|
325 mesh->mVertices[i] = mat * mesh->mVertices[i];
|
nuclear@0
|
326 }
|
nuclear@0
|
327 }
|
nuclear@0
|
328 if (mesh->HasNormals() || mesh->HasTangentsAndBitangents()) {
|
nuclear@0
|
329 aiMatrix4x4 mWorldIT = mat;
|
nuclear@0
|
330 mWorldIT.Inverse().Transpose();
|
nuclear@0
|
331
|
nuclear@0
|
332 // TODO: implement Inverse() for aiMatrix3x3
|
nuclear@0
|
333 aiMatrix3x3 m = aiMatrix3x3(mWorldIT);
|
nuclear@0
|
334
|
nuclear@0
|
335 if (mesh->HasNormals()) {
|
nuclear@0
|
336 for (unsigned int i = 0; i < mesh->mNumVertices; ++i) {
|
nuclear@0
|
337 mesh->mNormals[i] = (m * mesh->mNormals[i]).Normalize();
|
nuclear@0
|
338 }
|
nuclear@0
|
339 }
|
nuclear@0
|
340 if (mesh->HasTangentsAndBitangents()) {
|
nuclear@0
|
341 for (unsigned int i = 0; i < mesh->mNumVertices; ++i) {
|
nuclear@0
|
342 mesh->mTangents[i] = (m * mesh->mTangents[i]).Normalize();
|
nuclear@0
|
343 mesh->mBitangents[i] = (m * mesh->mBitangents[i]).Normalize();
|
nuclear@0
|
344 }
|
nuclear@0
|
345 }
|
nuclear@0
|
346 }
|
nuclear@0
|
347 }
|
nuclear@0
|
348 }
|
nuclear@0
|
349
|
nuclear@0
|
350 // ------------------------------------------------------------------------------------------------
|
nuclear@0
|
351 // Simple routine to build meshes in worldspace, no further optimization
|
nuclear@0
|
352 void PretransformVertices::BuildWCSMeshes(std::vector<aiMesh*>& out, aiMesh** in,
|
nuclear@0
|
353 unsigned int numIn, aiNode* node)
|
nuclear@0
|
354 {
|
nuclear@0
|
355 // NOTE:
|
nuclear@0
|
356 // aiMesh::mNumBones store original source mesh, or UINT_MAX if not a copy
|
nuclear@0
|
357 // aiMesh::mBones store reference to abs. transform we multiplied with
|
nuclear@0
|
358
|
nuclear@0
|
359 // process meshes
|
nuclear@0
|
360 for (unsigned int i = 0; i < node->mNumMeshes;++i) {
|
nuclear@0
|
361 aiMesh* mesh = in[node->mMeshes[i]];
|
nuclear@0
|
362
|
nuclear@0
|
363 // check whether we can operate on this mesh
|
nuclear@0
|
364 if (!mesh->mBones || *reinterpret_cast<aiMatrix4x4*>(mesh->mBones) == node->mTransformation) {
|
nuclear@0
|
365 // yes, we can.
|
nuclear@0
|
366 mesh->mBones = reinterpret_cast<aiBone**> (&node->mTransformation);
|
nuclear@0
|
367 mesh->mNumBones = UINT_MAX;
|
nuclear@0
|
368 }
|
nuclear@0
|
369 else {
|
nuclear@0
|
370
|
nuclear@0
|
371 // try to find us in the list of newly created meshes
|
nuclear@0
|
372 for (unsigned int n = 0; n < out.size(); ++n) {
|
nuclear@0
|
373 aiMesh* ctz = out[n];
|
nuclear@0
|
374 if (ctz->mNumBones == node->mMeshes[i] && *reinterpret_cast<aiMatrix4x4*>(ctz->mBones) == node->mTransformation) {
|
nuclear@0
|
375
|
nuclear@0
|
376 // ok, use this one. Update node mesh index
|
nuclear@0
|
377 node->mMeshes[i] = numIn + n;
|
nuclear@0
|
378 }
|
nuclear@0
|
379 }
|
nuclear@0
|
380 if (node->mMeshes[i] < numIn) {
|
nuclear@0
|
381 // Worst case. Need to operate on a full copy of the mesh
|
nuclear@0
|
382 DefaultLogger::get()->info("PretransformVertices: Copying mesh due to mismatching transforms");
|
nuclear@0
|
383 aiMesh* ntz;
|
nuclear@0
|
384
|
nuclear@0
|
385 const unsigned int tmp = mesh->mNumBones; //
|
nuclear@0
|
386 mesh->mNumBones = 0;
|
nuclear@0
|
387 SceneCombiner::Copy(&ntz,mesh);
|
nuclear@0
|
388 mesh->mNumBones = tmp;
|
nuclear@0
|
389
|
nuclear@0
|
390 ntz->mNumBones = node->mMeshes[i];
|
nuclear@0
|
391 ntz->mBones = reinterpret_cast<aiBone**> (&node->mTransformation);
|
nuclear@0
|
392
|
nuclear@0
|
393 out.push_back(ntz);
|
nuclear@0
|
394 }
|
nuclear@0
|
395 }
|
nuclear@0
|
396 }
|
nuclear@0
|
397
|
nuclear@0
|
398 // call children
|
nuclear@0
|
399 for (unsigned int i = 0; i < node->mNumChildren;++i)
|
nuclear@0
|
400 BuildWCSMeshes(out,in,numIn,node->mChildren[i]);
|
nuclear@0
|
401 }
|
nuclear@0
|
402
|
nuclear@0
|
403 // ------------------------------------------------------------------------------------------------
|
nuclear@0
|
404 // Reset transformation matrices to identity
|
nuclear@0
|
405 void PretransformVertices::MakeIdentityTransform(aiNode* nd)
|
nuclear@0
|
406 {
|
nuclear@0
|
407 nd->mTransformation = aiMatrix4x4();
|
nuclear@0
|
408
|
nuclear@0
|
409 // call children
|
nuclear@0
|
410 for (unsigned int i = 0; i < nd->mNumChildren;++i)
|
nuclear@0
|
411 MakeIdentityTransform(nd->mChildren[i]);
|
nuclear@0
|
412 }
|
nuclear@0
|
413
|
nuclear@0
|
414 // ------------------------------------------------------------------------------------------------
|
nuclear@0
|
415 // Build reference counters for all meshes
|
nuclear@0
|
416 void PretransformVertices::BuildMeshRefCountArray(aiNode* nd, unsigned int * refs)
|
nuclear@0
|
417 {
|
nuclear@0
|
418 for (unsigned int i = 0; i< nd->mNumMeshes;++i)
|
nuclear@0
|
419 refs[nd->mMeshes[i]]++;
|
nuclear@0
|
420
|
nuclear@0
|
421 // call children
|
nuclear@0
|
422 for (unsigned int i = 0; i < nd->mNumChildren;++i)
|
nuclear@0
|
423 BuildMeshRefCountArray(nd->mChildren[i],refs);
|
nuclear@0
|
424 }
|
nuclear@0
|
425
|
nuclear@0
|
426 // ------------------------------------------------------------------------------------------------
|
nuclear@0
|
427 // Executes the post processing step on the given imported data.
|
nuclear@0
|
428 void PretransformVertices::Execute( aiScene* pScene)
|
nuclear@0
|
429 {
|
nuclear@0
|
430 DefaultLogger::get()->debug("PretransformVerticesProcess begin");
|
nuclear@0
|
431
|
nuclear@0
|
432 // Return immediately if we have no meshes
|
nuclear@0
|
433 if (!pScene->mNumMeshes)
|
nuclear@0
|
434 return;
|
nuclear@0
|
435
|
nuclear@0
|
436 const unsigned int iOldMeshes = pScene->mNumMeshes;
|
nuclear@0
|
437 const unsigned int iOldAnimationChannels = pScene->mNumAnimations;
|
nuclear@0
|
438 const unsigned int iOldNodes = CountNodes(pScene->mRootNode);
|
nuclear@0
|
439
|
nuclear@0
|
440 // first compute absolute transformation matrices for all nodes
|
nuclear@0
|
441 ComputeAbsoluteTransform(pScene->mRootNode);
|
nuclear@0
|
442
|
nuclear@0
|
443 // Delete aiMesh::mBones for all meshes. The bones are
|
nuclear@0
|
444 // removed during this step and we need the pointer as
|
nuclear@0
|
445 // temporary storage
|
nuclear@0
|
446 for (unsigned int i = 0; i < pScene->mNumMeshes;++i) {
|
nuclear@0
|
447 aiMesh* mesh = pScene->mMeshes[i];
|
nuclear@0
|
448
|
nuclear@0
|
449 for (unsigned int a = 0; a < mesh->mNumBones;++a)
|
nuclear@0
|
450 delete mesh->mBones[a];
|
nuclear@0
|
451
|
nuclear@0
|
452 delete[] mesh->mBones;
|
nuclear@0
|
453 mesh->mBones = NULL;
|
nuclear@0
|
454 }
|
nuclear@0
|
455
|
nuclear@0
|
456 // now build a list of output meshes
|
nuclear@0
|
457 std::vector<aiMesh*> apcOutMeshes;
|
nuclear@0
|
458
|
nuclear@0
|
459 // Keep scene hierarchy? It's an easy job in this case ...
|
nuclear@0
|
460 // we go on and transform all meshes, if one is referenced by nodes
|
nuclear@0
|
461 // with different absolute transformations a depth copy of the mesh
|
nuclear@0
|
462 // is required.
|
nuclear@0
|
463 if( configKeepHierarchy ) {
|
nuclear@0
|
464
|
nuclear@0
|
465 // Hack: store the matrix we're transforming a mesh with in aiMesh::mBones
|
nuclear@0
|
466 BuildWCSMeshes(apcOutMeshes,pScene->mMeshes,pScene->mNumMeshes, pScene->mRootNode);
|
nuclear@0
|
467
|
nuclear@0
|
468 // ... if new meshes have been generated, append them to the end of the scene
|
nuclear@0
|
469 if (apcOutMeshes.size() > 0) {
|
nuclear@0
|
470 aiMesh** npp = new aiMesh*[pScene->mNumMeshes + apcOutMeshes.size()];
|
nuclear@0
|
471
|
nuclear@0
|
472 memcpy(npp,pScene->mMeshes,sizeof(aiMesh*)*pScene->mNumMeshes);
|
nuclear@0
|
473 memcpy(npp+pScene->mNumMeshes,&apcOutMeshes[0],sizeof(aiMesh*)*apcOutMeshes.size());
|
nuclear@0
|
474
|
nuclear@0
|
475 pScene->mNumMeshes += apcOutMeshes.size();
|
nuclear@0
|
476 delete[] pScene->mMeshes; pScene->mMeshes = npp;
|
nuclear@0
|
477 }
|
nuclear@0
|
478
|
nuclear@0
|
479 // now iterate through all meshes and transform them to worldspace
|
nuclear@0
|
480 for (unsigned int i = 0; i < pScene->mNumMeshes; ++i) {
|
nuclear@0
|
481 ApplyTransform(pScene->mMeshes[i],*reinterpret_cast<aiMatrix4x4*>( pScene->mMeshes[i]->mBones ));
|
nuclear@0
|
482
|
nuclear@0
|
483 // prevent improper destruction
|
nuclear@0
|
484 pScene->mMeshes[i]->mBones = NULL;
|
nuclear@0
|
485 pScene->mMeshes[i]->mNumBones = 0;
|
nuclear@0
|
486 }
|
nuclear@0
|
487 }
|
nuclear@0
|
488 else {
|
nuclear@0
|
489
|
nuclear@0
|
490 apcOutMeshes.reserve(pScene->mNumMaterials<<1u);
|
nuclear@0
|
491 std::list<unsigned int> aiVFormats;
|
nuclear@0
|
492
|
nuclear@0
|
493 std::vector<unsigned int> s(pScene->mNumMeshes,0);
|
nuclear@0
|
494 BuildMeshRefCountArray(pScene->mRootNode,&s[0]);
|
nuclear@0
|
495
|
nuclear@0
|
496 for (unsigned int i = 0; i < pScene->mNumMaterials;++i) {
|
nuclear@0
|
497 // get the list of all vertex formats for this material
|
nuclear@0
|
498 aiVFormats.clear();
|
nuclear@0
|
499 GetVFormatList(pScene,i,aiVFormats);
|
nuclear@0
|
500 aiVFormats.sort();
|
nuclear@0
|
501 aiVFormats.unique();
|
nuclear@0
|
502 for (std::list<unsigned int>::const_iterator j = aiVFormats.begin();j != aiVFormats.end();++j) {
|
nuclear@0
|
503 unsigned int iVertices = 0;
|
nuclear@0
|
504 unsigned int iFaces = 0;
|
nuclear@0
|
505 CountVerticesAndFaces(pScene,pScene->mRootNode,i,*j,&iFaces,&iVertices);
|
nuclear@0
|
506 if (0 != iFaces && 0 != iVertices)
|
nuclear@0
|
507 {
|
nuclear@0
|
508 apcOutMeshes.push_back(new aiMesh());
|
nuclear@0
|
509 aiMesh* pcMesh = apcOutMeshes.back();
|
nuclear@0
|
510 pcMesh->mNumFaces = iFaces;
|
nuclear@0
|
511 pcMesh->mNumVertices = iVertices;
|
nuclear@0
|
512 pcMesh->mFaces = new aiFace[iFaces];
|
nuclear@0
|
513 pcMesh->mVertices = new aiVector3D[iVertices];
|
nuclear@0
|
514 pcMesh->mMaterialIndex = i;
|
nuclear@0
|
515 if ((*j) & 0x2)pcMesh->mNormals = new aiVector3D[iVertices];
|
nuclear@0
|
516 if ((*j) & 0x4)
|
nuclear@0
|
517 {
|
nuclear@0
|
518 pcMesh->mTangents = new aiVector3D[iVertices];
|
nuclear@0
|
519 pcMesh->mBitangents = new aiVector3D[iVertices];
|
nuclear@0
|
520 }
|
nuclear@0
|
521 iFaces = 0;
|
nuclear@0
|
522 while ((*j) & (0x100 << iFaces))
|
nuclear@0
|
523 {
|
nuclear@0
|
524 pcMesh->mTextureCoords[iFaces] = new aiVector3D[iVertices];
|
nuclear@0
|
525 if ((*j) & (0x10000 << iFaces))pcMesh->mNumUVComponents[iFaces] = 3;
|
nuclear@0
|
526 else pcMesh->mNumUVComponents[iFaces] = 2;
|
nuclear@0
|
527 iFaces++;
|
nuclear@0
|
528 }
|
nuclear@0
|
529 iFaces = 0;
|
nuclear@0
|
530 while ((*j) & (0x1000000 << iFaces))
|
nuclear@0
|
531 pcMesh->mColors[iFaces++] = new aiColor4D[iVertices];
|
nuclear@0
|
532
|
nuclear@0
|
533 // fill the mesh ...
|
nuclear@0
|
534 unsigned int aiTemp[2] = {0,0};
|
nuclear@0
|
535 CollectData(pScene,pScene->mRootNode,i,*j,pcMesh,aiTemp,&s[0]);
|
nuclear@0
|
536 }
|
nuclear@0
|
537 }
|
nuclear@0
|
538 }
|
nuclear@0
|
539
|
nuclear@0
|
540 // If no meshes are referenced in the node graph it is possible that we get no output meshes.
|
nuclear@0
|
541 if (apcOutMeshes.empty()) {
|
nuclear@0
|
542 throw DeadlyImportError("No output meshes: all meshes are orphaned and are not referenced by any nodes");
|
nuclear@0
|
543 }
|
nuclear@0
|
544 else
|
nuclear@0
|
545 {
|
nuclear@0
|
546 // now delete all meshes in the scene and build a new mesh list
|
nuclear@0
|
547 for (unsigned int i = 0; i < pScene->mNumMeshes;++i)
|
nuclear@0
|
548 {
|
nuclear@0
|
549 aiMesh* mesh = pScene->mMeshes[i];
|
nuclear@0
|
550 mesh->mNumBones = 0;
|
nuclear@0
|
551 mesh->mBones = NULL;
|
nuclear@0
|
552
|
nuclear@0
|
553 // we're reusing the face index arrays. avoid destruction
|
nuclear@0
|
554 for (unsigned int a = 0; a < mesh->mNumFaces; ++a) {
|
nuclear@0
|
555 mesh->mFaces[a].mNumIndices = 0;
|
nuclear@0
|
556 mesh->mFaces[a].mIndices = NULL;
|
nuclear@0
|
557 }
|
nuclear@0
|
558
|
nuclear@0
|
559 delete mesh;
|
nuclear@0
|
560
|
nuclear@0
|
561 // Invalidate the contents of the old mesh array. We will most
|
nuclear@0
|
562 // likely have less output meshes now, so the last entries of
|
nuclear@0
|
563 // the mesh array are not overridden. We set them to NULL to
|
nuclear@0
|
564 // make sure the developer gets notified when his application
|
nuclear@0
|
565 // attempts to access these fields ...
|
nuclear@0
|
566 mesh = NULL;
|
nuclear@0
|
567 }
|
nuclear@0
|
568
|
nuclear@0
|
569 // It is impossible that we have more output meshes than
|
nuclear@0
|
570 // input meshes, so we can easily reuse the old mesh array
|
nuclear@0
|
571 pScene->mNumMeshes = (unsigned int)apcOutMeshes.size();
|
nuclear@0
|
572 for (unsigned int i = 0; i < pScene->mNumMeshes;++i) {
|
nuclear@0
|
573 pScene->mMeshes[i] = apcOutMeshes[i];
|
nuclear@0
|
574 }
|
nuclear@0
|
575 }
|
nuclear@0
|
576 }
|
nuclear@0
|
577
|
nuclear@0
|
578 // remove all animations from the scene
|
nuclear@0
|
579 for (unsigned int i = 0; i < pScene->mNumAnimations;++i)
|
nuclear@0
|
580 delete pScene->mAnimations[i];
|
nuclear@0
|
581 delete[] pScene->mAnimations;
|
nuclear@0
|
582
|
nuclear@0
|
583 pScene->mAnimations = NULL;
|
nuclear@0
|
584 pScene->mNumAnimations = 0;
|
nuclear@0
|
585
|
nuclear@0
|
586 // --- we need to keep all cameras and lights
|
nuclear@0
|
587 for (unsigned int i = 0; i < pScene->mNumCameras;++i)
|
nuclear@0
|
588 {
|
nuclear@0
|
589 aiCamera* cam = pScene->mCameras[i];
|
nuclear@0
|
590 const aiNode* nd = pScene->mRootNode->FindNode(cam->mName);
|
nuclear@0
|
591 ai_assert(NULL != nd);
|
nuclear@0
|
592
|
nuclear@0
|
593 // multiply all properties of the camera with the absolute
|
nuclear@0
|
594 // transformation of the corresponding node
|
nuclear@0
|
595 cam->mPosition = nd->mTransformation * cam->mPosition;
|
nuclear@0
|
596 cam->mLookAt = aiMatrix3x3( nd->mTransformation ) * cam->mLookAt;
|
nuclear@0
|
597 cam->mUp = aiMatrix3x3( nd->mTransformation ) * cam->mUp;
|
nuclear@0
|
598 }
|
nuclear@0
|
599
|
nuclear@0
|
600 for (unsigned int i = 0; i < pScene->mNumLights;++i)
|
nuclear@0
|
601 {
|
nuclear@0
|
602 aiLight* l = pScene->mLights[i];
|
nuclear@0
|
603 const aiNode* nd = pScene->mRootNode->FindNode(l->mName);
|
nuclear@0
|
604 ai_assert(NULL != nd);
|
nuclear@0
|
605
|
nuclear@0
|
606 // multiply all properties of the camera with the absolute
|
nuclear@0
|
607 // transformation of the corresponding node
|
nuclear@0
|
608 l->mPosition = nd->mTransformation * l->mPosition;
|
nuclear@0
|
609 l->mDirection = aiMatrix3x3( nd->mTransformation ) * l->mDirection;
|
nuclear@0
|
610 }
|
nuclear@0
|
611
|
nuclear@0
|
612 if( !configKeepHierarchy ) {
|
nuclear@0
|
613
|
nuclear@0
|
614 // now delete all nodes in the scene and build a new
|
nuclear@0
|
615 // flat node graph with a root node and some level 1 children
|
nuclear@0
|
616 delete pScene->mRootNode;
|
nuclear@0
|
617 pScene->mRootNode = new aiNode();
|
nuclear@0
|
618 pScene->mRootNode->mName.Set("<dummy_root>");
|
nuclear@0
|
619
|
nuclear@0
|
620 if (1 == pScene->mNumMeshes && !pScene->mNumLights && !pScene->mNumCameras)
|
nuclear@0
|
621 {
|
nuclear@0
|
622 pScene->mRootNode->mNumMeshes = 1;
|
nuclear@0
|
623 pScene->mRootNode->mMeshes = new unsigned int[1];
|
nuclear@0
|
624 pScene->mRootNode->mMeshes[0] = 0;
|
nuclear@0
|
625 }
|
nuclear@0
|
626 else
|
nuclear@0
|
627 {
|
nuclear@0
|
628 pScene->mRootNode->mNumChildren = pScene->mNumMeshes+pScene->mNumLights+pScene->mNumCameras;
|
nuclear@0
|
629 aiNode** nodes = pScene->mRootNode->mChildren = new aiNode*[pScene->mRootNode->mNumChildren];
|
nuclear@0
|
630
|
nuclear@0
|
631 // generate mesh nodes
|
nuclear@0
|
632 for (unsigned int i = 0; i < pScene->mNumMeshes;++i,++nodes)
|
nuclear@0
|
633 {
|
nuclear@0
|
634 aiNode* pcNode = *nodes = new aiNode();
|
nuclear@0
|
635 pcNode->mParent = pScene->mRootNode;
|
nuclear@0
|
636 pcNode->mName.length = ::sprintf(pcNode->mName.data,"mesh_%i",i);
|
nuclear@0
|
637
|
nuclear@0
|
638 // setup mesh indices
|
nuclear@0
|
639 pcNode->mNumMeshes = 1;
|
nuclear@0
|
640 pcNode->mMeshes = new unsigned int[1];
|
nuclear@0
|
641 pcNode->mMeshes[0] = i;
|
nuclear@0
|
642 }
|
nuclear@0
|
643 // generate light nodes
|
nuclear@0
|
644 for (unsigned int i = 0; i < pScene->mNumLights;++i,++nodes)
|
nuclear@0
|
645 {
|
nuclear@0
|
646 aiNode* pcNode = *nodes = new aiNode();
|
nuclear@0
|
647 pcNode->mParent = pScene->mRootNode;
|
nuclear@0
|
648 pcNode->mName.length = ::sprintf(pcNode->mName.data,"light_%i",i);
|
nuclear@0
|
649 pScene->mLights[i]->mName = pcNode->mName;
|
nuclear@0
|
650 }
|
nuclear@0
|
651 // generate camera nodes
|
nuclear@0
|
652 for (unsigned int i = 0; i < pScene->mNumCameras;++i,++nodes)
|
nuclear@0
|
653 {
|
nuclear@0
|
654 aiNode* pcNode = *nodes = new aiNode();
|
nuclear@0
|
655 pcNode->mParent = pScene->mRootNode;
|
nuclear@0
|
656 pcNode->mName.length = ::sprintf(pcNode->mName.data,"cam_%i",i);
|
nuclear@0
|
657 pScene->mCameras[i]->mName = pcNode->mName;
|
nuclear@0
|
658 }
|
nuclear@0
|
659 }
|
nuclear@0
|
660 }
|
nuclear@0
|
661 else {
|
nuclear@0
|
662 // ... and finally set the transformation matrix of all nodes to identity
|
nuclear@0
|
663 MakeIdentityTransform(pScene->mRootNode);
|
nuclear@0
|
664 }
|
nuclear@0
|
665
|
nuclear@0
|
666 if (configNormalize) {
|
nuclear@0
|
667 // compute the boundary of all meshes
|
nuclear@0
|
668 aiVector3D min,max;
|
nuclear@0
|
669 MinMaxChooser<aiVector3D> ()(min,max);
|
nuclear@0
|
670
|
nuclear@0
|
671 for (unsigned int a = 0; a < pScene->mNumMeshes; ++a) {
|
nuclear@0
|
672 aiMesh* m = pScene->mMeshes[a];
|
nuclear@0
|
673 for (unsigned int i = 0; i < m->mNumVertices;++i) {
|
nuclear@0
|
674 min = std::min(m->mVertices[i],min);
|
nuclear@0
|
675 max = std::max(m->mVertices[i],max);
|
nuclear@0
|
676 }
|
nuclear@0
|
677 }
|
nuclear@0
|
678
|
nuclear@0
|
679 // find the dominant axis
|
nuclear@0
|
680 aiVector3D d = max-min;
|
nuclear@0
|
681 const float div = std::max(d.x,std::max(d.y,d.z))*0.5f;
|
nuclear@0
|
682
|
nuclear@0
|
683 d = min+d*0.5f;
|
nuclear@0
|
684 for (unsigned int a = 0; a < pScene->mNumMeshes; ++a) {
|
nuclear@0
|
685 aiMesh* m = pScene->mMeshes[a];
|
nuclear@0
|
686 for (unsigned int i = 0; i < m->mNumVertices;++i) {
|
nuclear@0
|
687 m->mVertices[i] = (m->mVertices[i]-d)/div;
|
nuclear@0
|
688 }
|
nuclear@0
|
689 }
|
nuclear@0
|
690 }
|
nuclear@0
|
691
|
nuclear@0
|
692 // print statistics
|
nuclear@0
|
693 if (!DefaultLogger::isNullLogger())
|
nuclear@0
|
694 {
|
nuclear@0
|
695 char buffer[4096];
|
nuclear@0
|
696
|
nuclear@0
|
697 DefaultLogger::get()->debug("PretransformVerticesProcess finished");
|
nuclear@0
|
698
|
nuclear@0
|
699 sprintf(buffer,"Removed %i nodes and %i animation channels (%i output nodes)",
|
nuclear@0
|
700 iOldNodes,iOldAnimationChannels,CountNodes(pScene->mRootNode));
|
nuclear@0
|
701 DefaultLogger::get()->info(buffer);
|
nuclear@0
|
702
|
nuclear@0
|
703 sprintf(buffer,"Kept %i lights and %i cameras",
|
nuclear@0
|
704 pScene->mNumLights,pScene->mNumCameras);
|
nuclear@0
|
705 DefaultLogger::get()->info(buffer);
|
nuclear@0
|
706
|
nuclear@0
|
707 sprintf(buffer,"Moved %i meshes to WCS (number of output meshes: %i)",
|
nuclear@0
|
708 iOldMeshes,pScene->mNumMeshes);
|
nuclear@0
|
709 DefaultLogger::get()->info(buffer);
|
nuclear@0
|
710 }
|
nuclear@0
|
711 }
|
nuclear@0
|
712
|