vrshoot

annotate libs/assimp/JoinVerticesProcess.cpp @ 0:b2f14e535253

initial commit
author John Tsiombikas <nuclear@member.fsf.org>
date Sat, 01 Feb 2014 19:58:19 +0200
parents
children
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 Implementation of the post processing step to join identical vertices
nuclear@0 43 * for all imported meshes
nuclear@0 44 */
nuclear@0 45
nuclear@0 46 #include "AssimpPCH.h"
nuclear@0 47 #ifndef ASSIMP_BUILD_NO_JOINVERTICES_PROCESS
nuclear@0 48
nuclear@0 49 #include "JoinVerticesProcess.h"
nuclear@0 50 #include "ProcessHelper.h"
nuclear@0 51 #include "Vertex.h"
nuclear@0 52 #include "TinyFormatter.h"
nuclear@0 53
nuclear@0 54 using namespace Assimp;
nuclear@0 55 // ------------------------------------------------------------------------------------------------
nuclear@0 56 // Constructor to be privately used by Importer
nuclear@0 57 JoinVerticesProcess::JoinVerticesProcess()
nuclear@0 58 {
nuclear@0 59 // nothing to do here
nuclear@0 60 }
nuclear@0 61
nuclear@0 62 // ------------------------------------------------------------------------------------------------
nuclear@0 63 // Destructor, private as well
nuclear@0 64 JoinVerticesProcess::~JoinVerticesProcess()
nuclear@0 65 {
nuclear@0 66 // nothing to do here
nuclear@0 67 }
nuclear@0 68
nuclear@0 69 // ------------------------------------------------------------------------------------------------
nuclear@0 70 // Returns whether the processing step is present in the given flag field.
nuclear@0 71 bool JoinVerticesProcess::IsActive( unsigned int pFlags) const
nuclear@0 72 {
nuclear@0 73 return (pFlags & aiProcess_JoinIdenticalVertices) != 0;
nuclear@0 74 }
nuclear@0 75 // ------------------------------------------------------------------------------------------------
nuclear@0 76 // Executes the post processing step on the given imported data.
nuclear@0 77 void JoinVerticesProcess::Execute( aiScene* pScene)
nuclear@0 78 {
nuclear@0 79 DefaultLogger::get()->debug("JoinVerticesProcess begin");
nuclear@0 80
nuclear@0 81 // get the total number of vertices BEFORE the step is executed
nuclear@0 82 int iNumOldVertices = 0;
nuclear@0 83 if (!DefaultLogger::isNullLogger()) {
nuclear@0 84 for( unsigned int a = 0; a < pScene->mNumMeshes; a++) {
nuclear@0 85 iNumOldVertices += pScene->mMeshes[a]->mNumVertices;
nuclear@0 86 }
nuclear@0 87 }
nuclear@0 88
nuclear@0 89 // execute the step
nuclear@0 90 int iNumVertices = 0;
nuclear@0 91 for( unsigned int a = 0; a < pScene->mNumMeshes; a++)
nuclear@0 92 iNumVertices += ProcessMesh( pScene->mMeshes[a],a);
nuclear@0 93
nuclear@0 94 // if logging is active, print detailed statistics
nuclear@0 95 if (!DefaultLogger::isNullLogger())
nuclear@0 96 {
nuclear@0 97 if (iNumOldVertices == iNumVertices)
nuclear@0 98 {
nuclear@0 99 DefaultLogger::get()->debug("JoinVerticesProcess finished ");
nuclear@0 100 } else
nuclear@0 101 {
nuclear@0 102 char szBuff[128]; // should be sufficiently large in every case
nuclear@0 103 sprintf(szBuff,"JoinVerticesProcess finished | Verts in: %i out: %i | ~%.1f%%",
nuclear@0 104 iNumOldVertices,
nuclear@0 105 iNumVertices,
nuclear@0 106 ((iNumOldVertices - iNumVertices) / (float)iNumOldVertices) * 100.f);
nuclear@0 107 DefaultLogger::get()->info(szBuff);
nuclear@0 108 }
nuclear@0 109 }
nuclear@0 110
nuclear@0 111 pScene->mFlags |= AI_SCENE_FLAGS_NON_VERBOSE_FORMAT;
nuclear@0 112 }
nuclear@0 113
nuclear@0 114 // ------------------------------------------------------------------------------------------------
nuclear@0 115 // Unites identical vertices in the given mesh
nuclear@0 116 int JoinVerticesProcess::ProcessMesh( aiMesh* pMesh, unsigned int meshIndex)
nuclear@0 117 {
nuclear@0 118 BOOST_STATIC_ASSERT( AI_MAX_NUMBER_OF_COLOR_SETS == 8);
nuclear@0 119 BOOST_STATIC_ASSERT( AI_MAX_NUMBER_OF_TEXTURECOORDS == 8);
nuclear@0 120
nuclear@0 121 // Return early if we don't have any positions
nuclear@0 122 if (!pMesh->HasPositions() || !pMesh->HasFaces()) {
nuclear@0 123 return 0;
nuclear@0 124 }
nuclear@0 125
nuclear@0 126 // We'll never have more vertices afterwards.
nuclear@0 127 std::vector<Vertex> uniqueVertices;
nuclear@0 128 uniqueVertices.reserve( pMesh->mNumVertices);
nuclear@0 129
nuclear@0 130 // For each vertex the index of the vertex it was replaced by.
nuclear@0 131 // Since the maximal number of vertices is 2^31-1, the most significand bit can be used to mark
nuclear@0 132 // whether a new vertex was created for the index (true) or if it was replaced by an existing
nuclear@0 133 // unique vertex (false). This saves an additional std::vector<bool> and greatly enhances
nuclear@0 134 // branching performance.
nuclear@0 135 BOOST_STATIC_ASSERT(AI_MAX_VERTICES == 0x7fffffff);
nuclear@0 136 std::vector<unsigned int> replaceIndex( pMesh->mNumVertices, 0xffffffff);
nuclear@0 137
nuclear@0 138 // A little helper to find locally close vertices faster.
nuclear@0 139 // Try to reuse the lookup table from the last step.
nuclear@0 140 const static float epsilon = 1e-5f;
nuclear@0 141 // float posEpsilonSqr;
nuclear@0 142 SpatialSort* vertexFinder = NULL;
nuclear@0 143 SpatialSort _vertexFinder;
nuclear@0 144
nuclear@0 145 typedef std::pair<SpatialSort,float> SpatPair;
nuclear@0 146 if (shared) {
nuclear@0 147 std::vector<SpatPair >* avf;
nuclear@0 148 shared->GetProperty(AI_SPP_SPATIAL_SORT,avf);
nuclear@0 149 if (avf) {
nuclear@0 150 SpatPair& blubb = (*avf)[meshIndex];
nuclear@0 151 vertexFinder = &blubb.first;
nuclear@0 152 // posEpsilonSqr = blubb.second;
nuclear@0 153 }
nuclear@0 154 }
nuclear@0 155 if (!vertexFinder) {
nuclear@0 156 // bad, need to compute it.
nuclear@0 157 _vertexFinder.Fill(pMesh->mVertices, pMesh->mNumVertices, sizeof( aiVector3D));
nuclear@0 158 vertexFinder = &_vertexFinder;
nuclear@0 159 // posEpsilonSqr = ComputePositionEpsilon(pMesh);
nuclear@0 160 }
nuclear@0 161
nuclear@0 162 // Squared because we check against squared length of the vector difference
nuclear@0 163 static const float squareEpsilon = epsilon * epsilon;
nuclear@0 164
nuclear@0 165 // Again, better waste some bytes than a realloc ...
nuclear@0 166 std::vector<unsigned int> verticesFound;
nuclear@0 167 verticesFound.reserve(10);
nuclear@0 168
nuclear@0 169 // Run an optimized code path if we don't have multiple UVs or vertex colors.
nuclear@0 170 // This should yield false in more than 99% of all imports ...
nuclear@0 171 const bool complex = ( pMesh->GetNumColorChannels() > 0 || pMesh->GetNumUVChannels() > 1);
nuclear@0 172
nuclear@0 173 // Now check each vertex if it brings something new to the table
nuclear@0 174 for( unsigned int a = 0; a < pMesh->mNumVertices; a++) {
nuclear@0 175 // collect the vertex data
nuclear@0 176 Vertex v(pMesh,a);
nuclear@0 177
nuclear@0 178 // collect all vertices that are close enough to the given position
nuclear@0 179 vertexFinder->FindIdenticalPositions( v.position, verticesFound);
nuclear@0 180 unsigned int matchIndex = 0xffffffff;
nuclear@0 181
nuclear@0 182 // check all unique vertices close to the position if this vertex is already present among them
nuclear@0 183 for( unsigned int b = 0; b < verticesFound.size(); b++) {
nuclear@0 184
nuclear@0 185 const unsigned int vidx = verticesFound[b];
nuclear@0 186 const unsigned int uidx = replaceIndex[ vidx];
nuclear@0 187 if( uidx & 0x80000000)
nuclear@0 188 continue;
nuclear@0 189
nuclear@0 190 const Vertex& uv = uniqueVertices[ uidx];
nuclear@0 191 // Position mismatch is impossible - the vertex finder already discarded all non-matching positions
nuclear@0 192
nuclear@0 193 // We just test the other attributes even if they're not present in the mesh.
nuclear@0 194 // In this case they're initialized to 0 so the comparision succeeds.
nuclear@0 195 // By this method the non-present attributes are effectively ignored in the comparision.
nuclear@0 196 if( (uv.normal - v.normal).SquareLength() > squareEpsilon)
nuclear@0 197 continue;
nuclear@0 198 if( (uv.texcoords[0] - v.texcoords[0]).SquareLength() > squareEpsilon)
nuclear@0 199 continue;
nuclear@0 200 if( (uv.tangent - v.tangent).SquareLength() > squareEpsilon)
nuclear@0 201 continue;
nuclear@0 202 if( (uv.bitangent - v.bitangent).SquareLength() > squareEpsilon)
nuclear@0 203 continue;
nuclear@0 204
nuclear@0 205 // Usually we won't have vertex colors or multiple UVs, so we can skip from here
nuclear@0 206 // Actually this increases runtime performance slightly, at least if branch
nuclear@0 207 // prediction is on our side.
nuclear@0 208 if (complex){
nuclear@0 209 // manually unrolled because continue wouldn't work as desired in an inner loop,
nuclear@0 210 // also because some compilers seem to fail the task. Colors and UV coords
nuclear@0 211 // are interleaved since the higher entries are most likely to be
nuclear@0 212 // zero and thus useless. By interleaving the arrays, vertices are,
nuclear@0 213 // on average, rejected earlier.
nuclear@0 214
nuclear@0 215 if( (uv.texcoords[1] - v.texcoords[1]).SquareLength() > squareEpsilon)
nuclear@0 216 continue;
nuclear@0 217 if( GetColorDifference( uv.colors[0], v.colors[0]) > squareEpsilon)
nuclear@0 218 continue;
nuclear@0 219
nuclear@0 220 if( (uv.texcoords[2] - v.texcoords[2]).SquareLength() > squareEpsilon)
nuclear@0 221 continue;
nuclear@0 222 if( GetColorDifference( uv.colors[1], v.colors[1]) > squareEpsilon)
nuclear@0 223 continue;
nuclear@0 224
nuclear@0 225 if( (uv.texcoords[3] - v.texcoords[3]).SquareLength() > squareEpsilon)
nuclear@0 226 continue;
nuclear@0 227 if( GetColorDifference( uv.colors[2], v.colors[2]) > squareEpsilon)
nuclear@0 228 continue;
nuclear@0 229
nuclear@0 230 if( (uv.texcoords[4] - v.texcoords[4]).SquareLength() > squareEpsilon)
nuclear@0 231 continue;
nuclear@0 232 if( GetColorDifference( uv.colors[3], v.colors[3]) > squareEpsilon)
nuclear@0 233 continue;
nuclear@0 234
nuclear@0 235 if( (uv.texcoords[5] - v.texcoords[5]).SquareLength() > squareEpsilon)
nuclear@0 236 continue;
nuclear@0 237 if( GetColorDifference( uv.colors[4], v.colors[4]) > squareEpsilon)
nuclear@0 238 continue;
nuclear@0 239
nuclear@0 240 if( (uv.texcoords[6] - v.texcoords[6]).SquareLength() > squareEpsilon)
nuclear@0 241 continue;
nuclear@0 242 if( GetColorDifference( uv.colors[5], v.colors[5]) > squareEpsilon)
nuclear@0 243 continue;
nuclear@0 244
nuclear@0 245 if( (uv.texcoords[7] - v.texcoords[7]).SquareLength() > squareEpsilon)
nuclear@0 246 continue;
nuclear@0 247 if( GetColorDifference( uv.colors[6], v.colors[6]) > squareEpsilon)
nuclear@0 248 continue;
nuclear@0 249
nuclear@0 250 if( GetColorDifference( uv.colors[7], v.colors[7]) > squareEpsilon)
nuclear@0 251 continue;
nuclear@0 252 }
nuclear@0 253
nuclear@0 254 // we're still here -> this vertex perfectly matches our given vertex
nuclear@0 255 matchIndex = uidx;
nuclear@0 256 break;
nuclear@0 257 }
nuclear@0 258
nuclear@0 259 // found a replacement vertex among the uniques?
nuclear@0 260 if( matchIndex != 0xffffffff)
nuclear@0 261 {
nuclear@0 262 // store where to found the matching unique vertex
nuclear@0 263 replaceIndex[a] = matchIndex | 0x80000000;
nuclear@0 264 }
nuclear@0 265 else
nuclear@0 266 {
nuclear@0 267 // no unique vertex matches it upto now -> so add it
nuclear@0 268 replaceIndex[a] = (unsigned int)uniqueVertices.size();
nuclear@0 269 uniqueVertices.push_back( v);
nuclear@0 270 }
nuclear@0 271 }
nuclear@0 272
nuclear@0 273 if (!DefaultLogger::isNullLogger() && DefaultLogger::get()->getLogSeverity() == Logger::VERBOSE) {
nuclear@0 274 DefaultLogger::get()->debug((Formatter::format(),
nuclear@0 275 "Mesh ",meshIndex,
nuclear@0 276 " (",
nuclear@0 277 (pMesh->mName.length ? pMesh->mName.data : "unnamed"),
nuclear@0 278 ") | Verts in: ",pMesh->mNumVertices,
nuclear@0 279 " out: ",
nuclear@0 280 uniqueVertices.size(),
nuclear@0 281 " | ~",
nuclear@0 282 ((pMesh->mNumVertices - uniqueVertices.size()) / (float)pMesh->mNumVertices) * 100.f,
nuclear@0 283 "%"
nuclear@0 284 ));
nuclear@0 285 }
nuclear@0 286
nuclear@0 287 // replace vertex data with the unique data sets
nuclear@0 288 pMesh->mNumVertices = (unsigned int)uniqueVertices.size();
nuclear@0 289
nuclear@0 290 // ----------------------------------------------------------------------------
nuclear@0 291 // NOTE - we're *not* calling Vertex::SortBack() because it would check for
nuclear@0 292 // presence of every single vertex component once PER VERTEX. And our CPU
nuclear@0 293 // dislikes branches, even if they're easily predictable.
nuclear@0 294 // ----------------------------------------------------------------------------
nuclear@0 295
nuclear@0 296 // Position
nuclear@0 297 delete [] pMesh->mVertices;
nuclear@0 298 pMesh->mVertices = new aiVector3D[pMesh->mNumVertices];
nuclear@0 299 for( unsigned int a = 0; a < pMesh->mNumVertices; a++)
nuclear@0 300 pMesh->mVertices[a] = uniqueVertices[a].position;
nuclear@0 301
nuclear@0 302 // Normals, if present
nuclear@0 303 if( pMesh->mNormals)
nuclear@0 304 {
nuclear@0 305 delete [] pMesh->mNormals;
nuclear@0 306 pMesh->mNormals = new aiVector3D[pMesh->mNumVertices];
nuclear@0 307 for( unsigned int a = 0; a < pMesh->mNumVertices; a++) {
nuclear@0 308 pMesh->mNormals[a] = uniqueVertices[a].normal;
nuclear@0 309 }
nuclear@0 310 }
nuclear@0 311 // Tangents, if present
nuclear@0 312 if( pMesh->mTangents)
nuclear@0 313 {
nuclear@0 314 delete [] pMesh->mTangents;
nuclear@0 315 pMesh->mTangents = new aiVector3D[pMesh->mNumVertices];
nuclear@0 316 for( unsigned int a = 0; a < pMesh->mNumVertices; a++) {
nuclear@0 317 pMesh->mTangents[a] = uniqueVertices[a].tangent;
nuclear@0 318 }
nuclear@0 319 }
nuclear@0 320 // Bitangents as well
nuclear@0 321 if( pMesh->mBitangents)
nuclear@0 322 {
nuclear@0 323 delete [] pMesh->mBitangents;
nuclear@0 324 pMesh->mBitangents = new aiVector3D[pMesh->mNumVertices];
nuclear@0 325 for( unsigned int a = 0; a < pMesh->mNumVertices; a++) {
nuclear@0 326 pMesh->mBitangents[a] = uniqueVertices[a].bitangent;
nuclear@0 327 }
nuclear@0 328 }
nuclear@0 329 // Vertex colors
nuclear@0 330 for( unsigned int a = 0; pMesh->HasVertexColors(a); a++)
nuclear@0 331 {
nuclear@0 332 delete [] pMesh->mColors[a];
nuclear@0 333 pMesh->mColors[a] = new aiColor4D[pMesh->mNumVertices];
nuclear@0 334 for( unsigned int b = 0; b < pMesh->mNumVertices; b++) {
nuclear@0 335 pMesh->mColors[a][b] = uniqueVertices[b].colors[a];
nuclear@0 336 }
nuclear@0 337 }
nuclear@0 338 // Texture coords
nuclear@0 339 for( unsigned int a = 0; pMesh->HasTextureCoords(a); a++)
nuclear@0 340 {
nuclear@0 341 delete [] pMesh->mTextureCoords[a];
nuclear@0 342 pMesh->mTextureCoords[a] = new aiVector3D[pMesh->mNumVertices];
nuclear@0 343 for( unsigned int b = 0; b < pMesh->mNumVertices; b++) {
nuclear@0 344 pMesh->mTextureCoords[a][b] = uniqueVertices[b].texcoords[a];
nuclear@0 345 }
nuclear@0 346 }
nuclear@0 347
nuclear@0 348 // adjust the indices in all faces
nuclear@0 349 for( unsigned int a = 0; a < pMesh->mNumFaces; a++)
nuclear@0 350 {
nuclear@0 351 aiFace& face = pMesh->mFaces[a];
nuclear@0 352 for( unsigned int b = 0; b < face.mNumIndices; b++) {
nuclear@0 353 face.mIndices[b] = replaceIndex[face.mIndices[b]] & ~0x80000000;
nuclear@0 354 }
nuclear@0 355 }
nuclear@0 356
nuclear@0 357 // adjust bone vertex weights.
nuclear@0 358 for( int a = 0; a < (int)pMesh->mNumBones; a++)
nuclear@0 359 {
nuclear@0 360 aiBone* bone = pMesh->mBones[a];
nuclear@0 361 std::vector<aiVertexWeight> newWeights;
nuclear@0 362 newWeights.reserve( bone->mNumWeights);
nuclear@0 363
nuclear@0 364 for( unsigned int b = 0; b < bone->mNumWeights; b++)
nuclear@0 365 {
nuclear@0 366 const aiVertexWeight& ow = bone->mWeights[b];
nuclear@0 367 // if the vertex is a unique one, translate it
nuclear@0 368 if( !(replaceIndex[ow.mVertexId] & 0x80000000))
nuclear@0 369 {
nuclear@0 370 aiVertexWeight nw;
nuclear@0 371 nw.mVertexId = replaceIndex[ow.mVertexId];
nuclear@0 372 nw.mWeight = ow.mWeight;
nuclear@0 373 newWeights.push_back( nw);
nuclear@0 374 }
nuclear@0 375 }
nuclear@0 376
nuclear@0 377 if (newWeights.size() > 0) {
nuclear@0 378 // kill the old and replace them with the translated weights
nuclear@0 379 delete [] bone->mWeights;
nuclear@0 380 bone->mNumWeights = (unsigned int)newWeights.size();
nuclear@0 381
nuclear@0 382 bone->mWeights = new aiVertexWeight[bone->mNumWeights];
nuclear@0 383 memcpy( bone->mWeights, &newWeights[0], bone->mNumWeights * sizeof( aiVertexWeight));
nuclear@0 384 }
nuclear@0 385 else {
nuclear@0 386
nuclear@0 387 /* NOTE:
nuclear@0 388 *
nuclear@0 389 * In the algorithm above we're assuming that there are no vertices
nuclear@0 390 * with a different bone weight setup at the same position. That wouldn't
nuclear@0 391 * make sense, but it is not absolutely impossible. SkeletonMeshBuilder
nuclear@0 392 * for example generates such input data if two skeleton points
nuclear@0 393 * share the same position. Again this doesn't make sense but is
nuclear@0 394 * reality for some model formats (MD5 for example uses these special
nuclear@0 395 * nodes as attachment tags for its weapons).
nuclear@0 396 *
nuclear@0 397 * Then it is possible that a bone has no weights anymore .... as a quick
nuclear@0 398 * workaround, we're just removing these bones. If they're animated,
nuclear@0 399 * model geometry might be modified but at least there's no risk of a crash.
nuclear@0 400 */
nuclear@0 401 delete bone;
nuclear@0 402 --pMesh->mNumBones;
nuclear@0 403 for (unsigned int n = a; n < pMesh->mNumBones; ++n) {
nuclear@0 404 pMesh->mBones[n] = pMesh->mBones[n+1];
nuclear@0 405 }
nuclear@0 406
nuclear@0 407 --a;
nuclear@0 408 DefaultLogger::get()->warn("Removing bone -> no weights remaining");
nuclear@0 409 }
nuclear@0 410 }
nuclear@0 411 return pMesh->mNumVertices;
nuclear@0 412 }
nuclear@0 413
nuclear@0 414 #endif // !! ASSIMP_BUILD_NO_JOINVERTICES_PROCESS