1 /*
2 Open Asset Import Library (assimp)
3 ----------------------------------------------------------------------
4 
5 Copyright (c) 2006-2016, assimp team
6 All rights reserved.
7 
8 Redistribution and use of this software in source and binary forms,
9 with or without modification, are permitted provided that the
10 following conditions are met:
11 
12 * Redistributions of source code must retain the above
13   copyright notice, this list of conditions and the
14   following disclaimer.
15 
16 * Redistributions in binary form must reproduce the above
17   copyright notice, this list of conditions and the
18   following disclaimer in the documentation and/or other
19   materials provided with the distribution.
20 
21 * Neither the name of the assimp team, nor the names of its
22   contributors may be used to endorse or promote products
23   derived from this software without specific prior
24   written permission of the assimp team.
25 
26 THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
27 "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
28 LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
29 A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
30 OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
31 SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
32 LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
33 DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
34 THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
35 (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
36 OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
37 
38 ----------------------------------------------------------------------
39 */
40 
41 
42 #ifndef ASSIMP_BUILD_NO_EXPORT
43 #ifndef ASSIMP_BUILD_NO_3DS_EXPORTER
44 
45 #include "3DSExporter.h"
46 #include "3DSLoader.h"
47 #include "SceneCombiner.h"
48 #include "SplitLargeMeshes.h"
49 #include "StringComparison.h"
50 #include <assimp/IOSystem.hpp>
51 #include <assimp/DefaultLogger.hpp>
52 #include <assimp/Exporter.hpp>
53 #include <memory>
54 
55 using namespace Assimp;
56 namespace Assimp    {
57 
58 namespace {
59 
60     //////////////////////////////////////////////////////////////////////////////////////
61     // Scope utility to write a 3DS file chunk.
62     //
63     // Upon construction, the chunk header is written with the chunk type (flags)
64     // filled out, but the chunk size left empty. Upon destruction, the correct chunk
65     // size based on the then-position of the output stream cursor is filled in.
66     class ChunkWriter {
67         enum {
68               CHUNK_SIZE_NOT_SET = 0xdeadbeef
69             , SIZE_OFFSET        = 2
70         };
71     public:
72 
ChunkWriter(StreamWriterLE & writer,uint16_t chunk_type)73         ChunkWriter(StreamWriterLE& writer, uint16_t chunk_type)
74             : writer(writer)
75         {
76             chunk_start_pos = writer.GetCurrentPos();
77             writer.PutU2(chunk_type);
78             writer.PutU4(CHUNK_SIZE_NOT_SET);
79         }
80 
~ChunkWriter()81         ~ChunkWriter() {
82             std::size_t head_pos = writer.GetCurrentPos();
83 
84             ai_assert(head_pos > chunk_start_pos);
85             const std::size_t chunk_size = head_pos - chunk_start_pos;
86 
87             writer.SetCurrentPos(chunk_start_pos + SIZE_OFFSET);
88             writer.PutU4(chunk_size);
89             writer.SetCurrentPos(head_pos);
90         }
91 
92     private:
93         StreamWriterLE& writer;
94         std::size_t chunk_start_pos;
95     };
96 
97 
98     // Return an unique name for a given |mesh| attached to |node| that
99     // preserves the mesh's given name if it has one. |index| is the index
100     // of the mesh in |aiScene::mMeshes|.
GetMeshName(const aiMesh & mesh,unsigned int index,const aiNode & node)101     std::string GetMeshName(const aiMesh& mesh, unsigned int index, const aiNode& node) {
102         static const std::string underscore = "_";
103         char postfix[10] = {0};
104         ASSIMP_itoa10(postfix, index);
105 
106         std::string result = node.mName.C_Str();
107         if (mesh.mName.length > 0) {
108             result += underscore + mesh.mName.C_Str();
109         }
110         return result + underscore + postfix;
111     }
112 
113     // Return an unique name for a given |mat| with original position |index|
114     // in |aiScene::mMaterials|. The name preserves the original material
115     // name if possible.
GetMaterialName(const aiMaterial & mat,unsigned int index)116     std::string GetMaterialName(const aiMaterial& mat, unsigned int index) {
117         static const std::string underscore = "_";
118         char postfix[10] = {0};
119         ASSIMP_itoa10(postfix, index);
120 
121         aiString mat_name;
122         if (AI_SUCCESS == mat.Get(AI_MATKEY_NAME, mat_name)) {
123             return mat_name.C_Str() + underscore + postfix;
124         }
125 
126         return "Material" + underscore + postfix;
127     }
128 
129     // Collect world transformations for each node
CollectTrafos(const aiNode * node,std::map<const aiNode *,aiMatrix4x4> & trafos)130     void CollectTrafos(const aiNode* node, std::map<const aiNode*, aiMatrix4x4>& trafos) {
131         const aiMatrix4x4& parent = node->mParent ? trafos[node->mParent] : aiMatrix4x4();
132         trafos[node] = parent * node->mTransformation;
133         for (unsigned int i = 0; i < node->mNumChildren; ++i) {
134             CollectTrafos(node->mChildren[i], trafos);
135         }
136     }
137 
138     // Generate a flat list of the meshes (by index) assigned to each node
CollectMeshes(const aiNode * node,std::multimap<const aiNode *,unsigned int> & meshes)139     void CollectMeshes(const aiNode* node, std::multimap<const aiNode*, unsigned int>& meshes) {
140         for (unsigned int i = 0; i < node->mNumMeshes; ++i) {
141             meshes.insert(std::make_pair(node, node->mMeshes[i]));
142         }
143         for (unsigned int i = 0; i < node->mNumChildren; ++i) {
144             CollectMeshes(node->mChildren[i], meshes);
145         }
146     }
147 }
148 
149 // ------------------------------------------------------------------------------------------------
150 // Worker function for exporting a scene to 3DS. Prototyped and registered in Exporter.cpp
ExportScene3DS(const char * pFile,IOSystem * pIOSystem,const aiScene * pScene,const ExportProperties * pProperties)151 void ExportScene3DS(const char* pFile, IOSystem* pIOSystem, const aiScene* pScene, const ExportProperties* pProperties)
152 {
153     std::shared_ptr<IOStream> outfile (pIOSystem->Open(pFile, "wb"));
154     if(!outfile) {
155         throw DeadlyExportError("Could not open output .3ds file: " + std::string(pFile));
156     }
157 
158     // TODO: This extra copy should be avoided and all of this made a preprocess
159     // requirement of the 3DS exporter.
160     //
161     // 3DS meshes can be max 0xffff (16 Bit) vertices and faces, respectively.
162     // SplitLargeMeshes can do this, but it requires the correct limit to be set
163     // which is not possible with the current way of specifying preprocess steps
164     // in |Exporter::ExportFormatEntry|.
165     aiScene* scenecopy_tmp;
166     SceneCombiner::CopyScene(&scenecopy_tmp,pScene);
167     std::unique_ptr<aiScene> scenecopy(scenecopy_tmp);
168 
169     SplitLargeMeshesProcess_Triangle tri_splitter;
170     tri_splitter.SetLimit(0xffff);
171     tri_splitter.Execute(scenecopy.get());
172 
173     SplitLargeMeshesProcess_Vertex vert_splitter;
174     vert_splitter.SetLimit(0xffff);
175     vert_splitter.Execute(scenecopy.get());
176 
177     // Invoke the actual exporter
178     Discreet3DSExporter exporter(outfile, scenecopy.get());
179 }
180 
181 } // end of namespace Assimp
182 
183 // ------------------------------------------------------------------------------------------------
Discreet3DSExporter(std::shared_ptr<IOStream> outfile,const aiScene * scene)184 Discreet3DSExporter:: Discreet3DSExporter(std::shared_ptr<IOStream> outfile, const aiScene* scene)
185 : scene(scene)
186 , writer(outfile)
187 {
188     CollectTrafos(scene->mRootNode, trafos);
189     CollectMeshes(scene->mRootNode, meshes);
190 
191     ChunkWriter chunk(writer, Discreet3DS::CHUNK_MAIN);
192 
193     {
194         ChunkWriter chunk(writer, Discreet3DS::CHUNK_OBJMESH);
195         WriteMaterials();
196         WriteMeshes();
197 
198         {
199             ChunkWriter chunk(writer, Discreet3DS::CHUNK_MASTER_SCALE);
200             writer.PutF4(1.0f);
201         }
202     }
203 
204     {
205         ChunkWriter chunk(writer, Discreet3DS::CHUNK_KEYFRAMER);
206         WriteHierarchy(*scene->mRootNode, -1, -1);
207     }
208 }
209 
210 // ------------------------------------------------------------------------------------------------
WriteHierarchy(const aiNode & node,int seq,int sibling_level)211 int Discreet3DSExporter::WriteHierarchy(const aiNode& node, int seq, int sibling_level)
212 {
213     // 3DS scene hierarchy is serialized as in http://www.martinreddy.net/gfx/3d/3DS.spec
214     {
215         ChunkWriter chunk(writer, Discreet3DS::CHUNK_TRACKINFO);
216         {
217             ChunkWriter chunk(writer, Discreet3DS::CHUNK_TRACKOBJNAME);
218 
219             // Assimp node names are unique and distinct from all mesh-node
220             // names we generate; thus we can use them as-is
221             WriteString(node.mName);
222 
223             // Two unknown int16 values - it is even unclear if 0 is a safe value
224             // but luckily importers do not know better either.
225             writer.PutI4(0);
226 
227             int16_t hierarchy_pos = static_cast<int16_t>(seq);
228             if (sibling_level != -1) {
229                 hierarchy_pos = sibling_level;
230             }
231 
232             // Write the hierarchy position
233             writer.PutI2(hierarchy_pos);
234         }
235     }
236 
237     // TODO: write transformation chunks
238 
239     ++seq;
240     sibling_level = seq;
241 
242     // Write all children
243     for (unsigned int i = 0; i < node.mNumChildren; ++i) {
244         seq = WriteHierarchy(*node.mChildren[i], seq, i == 0 ? -1 : sibling_level);
245     }
246 
247     // Write all meshes as separate nodes to be able to reference the meshes by name
248     for (unsigned int i = 0; i < node.mNumMeshes; ++i) {
249         const bool first_child = node.mNumChildren == 0 && i == 0;
250 
251         const unsigned int mesh_idx = node.mMeshes[i];
252         const aiMesh& mesh = *scene->mMeshes[mesh_idx];
253 
254         ChunkWriter chunk(writer, Discreet3DS::CHUNK_TRACKINFO);
255         {
256             ChunkWriter chunk(writer, Discreet3DS::CHUNK_TRACKOBJNAME);
257             WriteString(GetMeshName(mesh, mesh_idx, node));
258 
259             writer.PutI4(0);
260             writer.PutI2(static_cast<int16_t>(first_child ? seq : sibling_level));
261             ++seq;
262         }
263     }
264     return seq;
265 }
266 
267 // ------------------------------------------------------------------------------------------------
WriteMaterials()268 void Discreet3DSExporter::WriteMaterials()
269 {
270     for (unsigned int i = 0; i < scene->mNumMaterials; ++i) {
271         ChunkWriter chunk(writer, Discreet3DS::CHUNK_MAT_MATERIAL);
272         const aiMaterial& mat = *scene->mMaterials[i];
273 
274         {
275             ChunkWriter chunk(writer, Discreet3DS::CHUNK_MAT_MATNAME);
276             const std::string& name = GetMaterialName(mat, i);
277             WriteString(name);
278         }
279 
280         aiColor3D color;
281         if (mat.Get(AI_MATKEY_COLOR_DIFFUSE, color) == AI_SUCCESS) {
282             ChunkWriter chunk(writer, Discreet3DS::CHUNK_MAT_DIFFUSE);
283             WriteColor(color);
284         }
285 
286         if (mat.Get(AI_MATKEY_COLOR_SPECULAR, color) == AI_SUCCESS) {
287             ChunkWriter chunk(writer, Discreet3DS::CHUNK_MAT_SPECULAR);
288             WriteColor(color);
289         }
290 
291         if (mat.Get(AI_MATKEY_COLOR_AMBIENT, color) == AI_SUCCESS) {
292             ChunkWriter chunk(writer, Discreet3DS::CHUNK_MAT_AMBIENT);
293             WriteColor(color);
294         }
295 
296         if (mat.Get(AI_MATKEY_COLOR_EMISSIVE, color) == AI_SUCCESS) {
297             ChunkWriter chunk(writer, Discreet3DS::CHUNK_MAT_SELF_ILLUM);
298             WriteColor(color);
299         }
300 
301         aiShadingMode shading_mode = aiShadingMode_Flat;
302         if (mat.Get(AI_MATKEY_SHADING_MODEL, shading_mode) == AI_SUCCESS) {
303             ChunkWriter chunk(writer, Discreet3DS::CHUNK_MAT_SHADING);
304 
305             Discreet3DS::shadetype3ds shading_mode_out;
306             switch(shading_mode) {
307             case aiShadingMode_Flat:
308             case aiShadingMode_NoShading:
309                 shading_mode_out = Discreet3DS::Flat;
310                 break;
311 
312             case aiShadingMode_Gouraud:
313             case aiShadingMode_Toon:
314             case aiShadingMode_OrenNayar:
315             case aiShadingMode_Minnaert:
316                 shading_mode_out = Discreet3DS::Gouraud;
317                 break;
318 
319             case aiShadingMode_Phong:
320             case aiShadingMode_Blinn:
321             case aiShadingMode_CookTorrance:
322             case aiShadingMode_Fresnel:
323                 shading_mode_out = Discreet3DS::Phong;
324                 break;
325 
326             default:
327                 shading_mode_out = Discreet3DS::Flat;
328                 ai_assert(false);
329             };
330             writer.PutU2(static_cast<uint16_t>(shading_mode_out));
331         }
332 
333 
334         float f;
335         if (mat.Get(AI_MATKEY_SHININESS, f) == AI_SUCCESS) {
336             ChunkWriter chunk(writer, Discreet3DS::CHUNK_MAT_SHININESS);
337             WritePercentChunk(f);
338         }
339 
340         if (mat.Get(AI_MATKEY_SHININESS_STRENGTH, f) == AI_SUCCESS) {
341             ChunkWriter chunk(writer, Discreet3DS::CHUNK_MAT_SHININESS_PERCENT);
342             WritePercentChunk(f);
343         }
344 
345         int twosided;
346         if (mat.Get(AI_MATKEY_TWOSIDED, twosided) == AI_SUCCESS && twosided != 0) {
347             ChunkWriter chunk(writer, Discreet3DS::CHUNK_MAT_TWO_SIDE);
348             writer.PutI2(1);
349         }
350 
351         WriteTexture(mat, aiTextureType_DIFFUSE, Discreet3DS::CHUNK_MAT_TEXTURE);
352         WriteTexture(mat, aiTextureType_HEIGHT, Discreet3DS::CHUNK_MAT_BUMPMAP);
353         WriteTexture(mat, aiTextureType_OPACITY, Discreet3DS::CHUNK_MAT_OPACMAP);
354         WriteTexture(mat, aiTextureType_SHININESS, Discreet3DS::CHUNK_MAT_MAT_SHINMAP);
355         WriteTexture(mat, aiTextureType_SPECULAR, Discreet3DS::CHUNK_MAT_SPECMAP);
356         WriteTexture(mat, aiTextureType_EMISSIVE, Discreet3DS::CHUNK_MAT_SELFIMAP);
357         WriteTexture(mat, aiTextureType_REFLECTION, Discreet3DS::CHUNK_MAT_REFLMAP);
358     }
359 }
360 
361 // ------------------------------------------------------------------------------------------------
WriteTexture(const aiMaterial & mat,aiTextureType type,uint16_t chunk_flags)362 void Discreet3DSExporter::WriteTexture(const aiMaterial& mat, aiTextureType type, uint16_t chunk_flags)
363 {
364     aiString path;
365     aiTextureMapMode map_mode[2] = {
366         aiTextureMapMode_Wrap, aiTextureMapMode_Wrap
367     };
368     float blend = 1.0f;
369     if (mat.GetTexture(type, 0, &path, NULL, NULL, &blend, NULL, map_mode) != AI_SUCCESS || !path.length) {
370         return;
371     }
372 
373     // TODO: handle embedded textures properly
374     if (path.data[0] == '*') {
375         DefaultLogger::get()->error("Ignoring embedded texture for export: " + std::string(path.C_Str()));
376         return;
377     }
378 
379     ChunkWriter chunk(writer, chunk_flags);
380     {
381         ChunkWriter chunk(writer, Discreet3DS::CHUNK_MAPFILE);
382         WriteString(path);
383     }
384 
385     WritePercentChunk(blend);
386 
387     {
388         ChunkWriter chunk(writer, Discreet3DS::CHUNK_MAT_MAP_TILING);
389         uint16_t val = 0; // WRAP
390         if (map_mode[0] == aiTextureMapMode_Mirror) {
391             val = 0x2;
392         }
393         else if (map_mode[0] == aiTextureMapMode_Decal) {
394             val = 0x10;
395         }
396         writer.PutU2(val);
397     }
398     // TODO: export texture transformation (i.e. UV offset, scale, rotation)
399 }
400 
401 // ------------------------------------------------------------------------------------------------
WriteMeshes()402 void Discreet3DSExporter::WriteMeshes()
403 {
404     // NOTE: 3DS allows for instances. However:
405     //   i)  not all importers support reading them
406     //   ii) instances are not as flexible as they are in assimp, in particular,
407     //        nodes can carry (and instance) only one mesh.
408     //
409     // This exporter currently deep clones all instanced meshes, i.e. for each mesh
410     // attached to a node a full TRIMESH chunk is written to the file.
411     //
412     // Furthermore, the TRIMESH is transformed into world space so that it will
413     // appear correctly if importers don't read the scene hierarchy at all.
414     for (MeshesByNodeMap::const_iterator it = meshes.begin(); it != meshes.end(); ++it) {
415         const aiNode& node = *(*it).first;
416         const unsigned int mesh_idx = (*it).second;
417 
418         const aiMesh& mesh = *scene->mMeshes[mesh_idx];
419 
420         // This should not happen if the SLM step is correctly executed
421         // before the scene is handed to the exporter
422         ai_assert(mesh.mNumVertices <= 0xffff);
423         ai_assert(mesh.mNumFaces <= 0xffff);
424 
425         const aiMatrix4x4& trafo = trafos[&node];
426 
427         ChunkWriter chunk(writer, Discreet3DS::CHUNK_OBJBLOCK);
428 
429         // Mesh name is tied to the node it is attached to so it can later be referenced
430         const std::string& name = GetMeshName(mesh, mesh_idx, node);
431         WriteString(name);
432 
433 
434         // TRIMESH chunk
435         ChunkWriter chunk2(writer, Discreet3DS::CHUNK_TRIMESH);
436 
437         // Vertices in world space
438         {
439             ChunkWriter chunk(writer, Discreet3DS::CHUNK_VERTLIST);
440 
441             const uint16_t count = static_cast<uint16_t>(mesh.mNumVertices);
442             writer.PutU2(count);
443             for (unsigned int i = 0; i < mesh.mNumVertices; ++i) {
444                 const aiVector3D& v = trafo * mesh.mVertices[i];
445                 writer.PutF4(v.x);
446                 writer.PutF4(v.y);
447                 writer.PutF4(v.z);
448             }
449         }
450 
451         // UV coordinates
452         if (mesh.HasTextureCoords(0)) {
453             ChunkWriter chunk(writer, Discreet3DS::CHUNK_MAPLIST);
454             const uint16_t count = static_cast<uint16_t>(mesh.mNumVertices);
455             writer.PutU2(count);
456 
457             for (unsigned int i = 0; i < mesh.mNumVertices; ++i) {
458                 const aiVector3D& v = mesh.mTextureCoords[0][i];
459                 writer.PutF4(v.x);
460                 writer.PutF4(v.y);
461             }
462         }
463 
464         // Faces (indices)
465         {
466             ChunkWriter chunk(writer, Discreet3DS::CHUNK_FACELIST);
467 
468             ai_assert(mesh.mNumFaces <= 0xffff);
469 
470             // Count triangles, discard lines and points
471             uint16_t count = 0;
472             for (unsigned int i = 0; i < mesh.mNumFaces; ++i) {
473                 const aiFace& f = mesh.mFaces[i];
474                 if (f.mNumIndices < 3) {
475                     continue;
476                 }
477                 // TRIANGULATE step is a pre-requisite so we should not see polys here
478                 ai_assert(f.mNumIndices == 3);
479                 ++count;
480             }
481 
482             writer.PutU2(count);
483             for (unsigned int i = 0; i < mesh.mNumFaces; ++i) {
484                 const aiFace& f = mesh.mFaces[i];
485                 if (f.mNumIndices < 3) {
486                     continue;
487                 }
488 
489                 for (unsigned int j = 0; j < 3; ++j) {
490                     ai_assert(f.mIndices[j] <= 0xffff);
491                     writer.PutI2(static_cast<uint16_t>(f.mIndices[j]));
492                 }
493 
494                 // Edge visibility flag
495                 writer.PutI2(0x0);
496             }
497 
498             // TODO: write smoothing groups (CHUNK_SMOOLIST)
499 
500             WriteFaceMaterialChunk(mesh);
501         }
502 
503         // Transformation matrix by which the mesh vertices have been pre-transformed with.
504         {
505             ChunkWriter chunk(writer, Discreet3DS::CHUNK_TRMATRIX);
506             for (unsigned int r = 0; r < 4; ++r) {
507                 for (unsigned int c = 0; c < 3; ++c) {
508                     writer.PutF4(trafo[r][c]);
509                 }
510             }
511         }
512     }
513 }
514 
515 // ------------------------------------------------------------------------------------------------
WriteFaceMaterialChunk(const aiMesh & mesh)516 void Discreet3DSExporter::WriteFaceMaterialChunk(const aiMesh& mesh)
517 {
518     ChunkWriter chunk(writer, Discreet3DS::CHUNK_FACEMAT);
519     const std::string& name = GetMaterialName(*scene->mMaterials[mesh.mMaterialIndex], mesh.mMaterialIndex);
520     WriteString(name);
521 
522     // Because assimp splits meshes by material, only a single
523     // FACEMAT chunk needs to be written
524     ai_assert(mesh.mNumFaces <= 0xffff);
525     const uint16_t count = static_cast<uint16_t>(mesh.mNumFaces);
526     writer.PutU2(count);
527 
528     for (unsigned int i = 0; i < mesh.mNumFaces; ++i) {
529         writer.PutU2(static_cast<uint16_t>(i));
530     }
531 }
532 
533 // ------------------------------------------------------------------------------------------------
WriteString(const std::string & s)534 void Discreet3DSExporter::WriteString(const std::string& s) {
535     for (std::string::const_iterator it = s.begin(); it != s.end(); ++it) {
536         writer.PutI1(*it);
537     }
538     writer.PutI1('\0');
539 }
540 
541 // ------------------------------------------------------------------------------------------------
WriteString(const aiString & s)542 void Discreet3DSExporter::WriteString(const aiString& s) {
543     for (std::size_t i = 0; i < s.length; ++i) {
544         writer.PutI1(s.data[i]);
545     }
546     writer.PutI1('\0');
547 }
548 
549 // ------------------------------------------------------------------------------------------------
WriteColor(const aiColor3D & color)550 void Discreet3DSExporter::WriteColor(const aiColor3D& color) {
551     ChunkWriter chunk(writer, Discreet3DS::CHUNK_RGBF);
552     writer.PutF4(color.r);
553     writer.PutF4(color.g);
554     writer.PutF4(color.b);
555 }
556 
557 // ------------------------------------------------------------------------------------------------
WritePercentChunk(float f)558 void Discreet3DSExporter::WritePercentChunk(float f) {
559     ChunkWriter chunk(writer, Discreet3DS::CHUNK_PERCENTF);
560     writer.PutF4(f);
561 }
562 
563 
564 #endif // ASSIMP_BUILD_NO_3DS_EXPORTER
565 #endif // ASSIMP_BUILD_NO_EXPORT
566