]> git.lizzy.rs Git - irrlicht.git/blob - source/Irrlicht/CB3DMeshWriter.cpp
dd6031add6945794538e9fee1cab75dd69455fe8
[irrlicht.git] / source / Irrlicht / CB3DMeshWriter.cpp
1 // Copyright (C) 2014 Lauri Kasanen\r
2 // This file is part of the "Irrlicht Engine".\r
3 // For conditions of distribution and use, see copyright notice in irrlicht.h\r
4 \r
5 // TODO: replace printf's by logging messages\r
6 \r
7 #include "IrrCompileConfig.h"\r
8 \r
9 #ifdef _IRR_COMPILE_WITH_B3D_WRITER_\r
10 \r
11 #include "CB3DMeshWriter.h"\r
12 #include "os.h"\r
13 #include "ISkinnedMesh.h"\r
14 #include "IMeshBuffer.h"\r
15 #include "IWriteFile.h"\r
16 #include "ITexture.h"\r
17 #include "irrMap.h"\r
18 \r
19 \r
20 namespace irr\r
21 {\r
22 namespace scene\r
23 {\r
24 \r
25 using namespace core;\r
26 using namespace video;\r
27 \r
28 CB3DMeshWriter::CB3DMeshWriter()\r
29 {\r
30         #ifdef _DEBUG\r
31         setDebugName("CB3DMeshWriter");\r
32         #endif\r
33 }\r
34 \r
35 \r
36 //! Returns the type of the mesh writer\r
37 EMESH_WRITER_TYPE CB3DMeshWriter::getType() const\r
38 {\r
39     return EMWT_B3D;\r
40 }\r
41 \r
42 \r
43 //! writes a mesh\r
44 bool CB3DMeshWriter::writeMesh(io::IWriteFile* file, IMesh* const mesh, s32 flags)\r
45 {\r
46     if (!file || !mesh)\r
47         return false;\r
48 #ifdef __BIG_ENDIAN__\r
49     os::Printer::log("B3D export does not support big-endian systems.", ELL_ERROR);\r
50     return false;\r
51 #endif\r
52 \r
53     Size = 0;\r
54     file->write("BB3D", 4);\r
55     file->write(&Size, sizeof(u32)); // Updated later once known.\r
56 \r
57     int version = 1;\r
58     write(file, &version, sizeof(int));\r
59 \r
60     //\r
61 \r
62     const u32 numBeshBuffers = mesh->getMeshBufferCount();\r
63     array<SB3dTexture> texs;\r
64     map<ITexture *, u32> tex2id;        // TODO: texture pointer as key not sufficient as same texture can have several id's\r
65     u32 texsizes = 0;\r
66     for (u32 i = 0; i < numBeshBuffers; i++)\r
67         {\r
68         const IMeshBuffer * const mb = mesh->getMeshBuffer(i);\r
69         const SMaterial &mat = mb->getMaterial();\r
70 \r
71         for (u32 j = 0; j < MATERIAL_MAX_TEXTURES; j++)\r
72                 {\r
73             if (mat.getTexture(j))\r
74                         {\r
75                 SB3dTexture t;\r
76                                 t.TextureName = core::stringc(mat.getTexture(j)->getName().getPath());\r
77 \r
78                                 // TODO: need some description of Blitz3D texture-flags to figure this out. But Blend should likely depend on material-type.\r
79                                 t.Flags = j == 2 ? 65536 : 1;\r
80                                 t.Blend = 2;\r
81 \r
82                                 // TODO: evaluate texture matrix\r
83                                 t.Xpos = 0;\r
84                                 t.Ypos = 0;\r
85                                 t.Xscale = 1;\r
86                                 t.Yscale = 1;\r
87                                 t.Angle = 0;\r
88 \r
89                 texs.push_back(t);\r
90                 texsizes += 7*4 + t.TextureName.size() + 1;\r
91                 tex2id[mat.getTexture(j)] = texs.size() - 1;\r
92             }\r
93         }\r
94     }\r
95 \r
96     write(file, "TEXS", 4);\r
97     write(file, &texsizes, 4);\r
98 \r
99     u32 numTexture = texs.size();\r
100     for (u32 i = 0; i < numTexture; i++)\r
101         {\r
102         write(file, texs[i].TextureName.c_str(), texs[i].TextureName.size() + 1);\r
103         write(file, &texs[i].Flags, 7*4);\r
104     }\r
105 \r
106     //\r
107 \r
108     const u32 brushsize = (7 * 4 + 1) * numBeshBuffers + numBeshBuffers * 4 * MATERIAL_MAX_TEXTURES + 4;\r
109     write(file, "BRUS", 4);\r
110     write(file, &brushsize, 4);\r
111     u32 brushcheck = Size;\r
112     const u32 usedtex = MATERIAL_MAX_TEXTURES;\r
113     write(file, &usedtex, 4);\r
114 \r
115     for (u32 i = 0; i < numBeshBuffers; i++)\r
116         {\r
117         const IMeshBuffer * const mb = mesh->getMeshBuffer(i);\r
118         const SMaterial &mat = mb->getMaterial();\r
119 \r
120         write(file, "", 1);\r
121 \r
122         float f = 1;\r
123         write(file, &f, 4);\r
124         write(file, &f, 4);\r
125         write(file, &f, 4);\r
126         write(file, &f, 4);\r
127 \r
128         f = 0;\r
129         write(file, &f, 4);\r
130 \r
131         u32 tmp = 1;\r
132         write(file, &tmp, 4);\r
133         tmp = 0;\r
134         write(file, &tmp, 4);\r
135 \r
136         for (u32 j = 0; j < MATERIAL_MAX_TEXTURES; j++)\r
137                 {\r
138             if (mat.getTexture(j))\r
139                         {\r
140                 const u32 id = tex2id[mat.getTexture(j)];\r
141                 write(file, &id, 4);\r
142             }\r
143                         else\r
144                         {\r
145                 const int id = -1;\r
146                 write(file, &id, 4);\r
147             }\r
148         }\r
149     }\r
150 \r
151     // Check brushsize\r
152     brushcheck = Size - brushcheck;\r
153     if (brushcheck != brushsize)\r
154         {\r
155         printf("Failed in brush size calculation, size %u advanced %u\n",\r
156             brushsize, brushcheck);\r
157         }\r
158 \r
159     write(file, "NODE", 4);\r
160 \r
161     // Calculate node size\r
162     u32 nodesize = 41 + 8 + 4 + 8;\r
163     u32 bonesSize = 0;\r
164 \r
165     if(ISkinnedMesh *skinnedMesh = getSkinned(mesh))\r
166     {\r
167         if (!skinnedMesh->isStatic())\r
168         {\r
169             bonesSize += 20;\r
170         }\r
171 \r
172         const core::array<ISkinnedMesh::SJoint*> rootJoints = getRootJoints(skinnedMesh);\r
173         for (u32 i = 0; i < rootJoints.size(); i++)\r
174         {\r
175             bonesSize += getJointChunkSize(skinnedMesh, rootJoints[i]);\r
176         }\r
177         nodesize += bonesSize;\r
178 \r
179         // -------------------\r
180 \r
181     }\r
182 \r
183     // VERT data\r
184     nodesize += 12;\r
185 \r
186     const u32 texcoords = getUVlayerCount(mesh);\r
187     for (u32 i = 0; i < numBeshBuffers; i++)\r
188     {\r
189         nodesize += 8 + 4;\r
190         const IMeshBuffer * const mb = mesh->getMeshBuffer(i);\r
191 \r
192         nodesize += mb->getVertexCount() * 10 * 4;\r
193 \r
194         nodesize += mb->getVertexCount() * texcoords * 2 * 4;\r
195         nodesize += mb->getIndexCount() * 4;\r
196     }\r
197     write(file, &nodesize, 4);\r
198     u32 nodecheck = Size;\r
199 \r
200     // Node\r
201     write(file, "", 1);\r
202     float f = 0;\r
203     write(file, &f, 4);\r
204     write(file, &f, 4);\r
205     write(file, &f, 4);\r
206 \r
207     f = 1;\r
208     write(file, &f, 4);\r
209     write(file, &f, 4);\r
210     write(file, &f, 4);\r
211 \r
212     write(file, &f, 4);\r
213     f = 0;\r
214     write(file, &f, 4);\r
215     write(file, &f, 4);\r
216     write(file, &f, 4);\r
217 \r
218     // Mesh\r
219     write(file, "MESH", 4);\r
220     const u32 meshsize = nodesize - 41 - 8 - bonesSize;\r
221     write(file, &meshsize, 4);\r
222         s32 brushID = -1;\r
223     write(file, &brushID, 4);\r
224 \r
225 \r
226 \r
227     // Verts\r
228     write(file, "VRTS", 4);\r
229     u32 vertsize = 12;\r
230 \r
231     for (u32 i = 0; i < numBeshBuffers; i++)\r
232     {\r
233         const IMeshBuffer * const mb = mesh->getMeshBuffer(i);\r
234 \r
235         vertsize += mb->getVertexCount() * 10 * 4 +\r
236                     mb->getVertexCount() * texcoords * 2 * 4;\r
237     }\r
238     write(file, &vertsize, 4);\r
239     u32 vertcheck = Size;\r
240 \r
241     int flagsB3D = 3;\r
242     write(file, &flagsB3D, 4);\r
243 \r
244     write(file, &texcoords, 4);\r
245     flagsB3D = 2;\r
246     write(file, &flagsB3D, 4);\r
247 \r
248     for (u32 i = 0; i < numBeshBuffers; i++)\r
249     {\r
250         const IMeshBuffer * const mb = mesh->getMeshBuffer(i);\r
251         irr::u32 numVertices = mb->getVertexCount();\r
252         for (u32 j = 0; j < numVertices; j++)\r
253                 {\r
254             const vector3df &pos = mb->getPosition(j);\r
255             write(file, &pos.X, 4);\r
256             write(file, &pos.Y, 4);\r
257             write(file, &pos.Z, 4);\r
258 \r
259             const vector3df &n = mb->getNormal(j);\r
260             write(file, &n.X, 4);\r
261             write(file, &n.Y, 4);\r
262             write(file, &n.Z, 4);\r
263 \r
264             const u32 zero = 0;\r
265             switch (mb->getVertexType())\r
266                         {\r
267                 case EVT_STANDARD:\r
268                 {\r
269                     S3DVertex *v = (S3DVertex *) mb->getVertices();\r
270                     const SColorf col(v[j].Color);\r
271                     write(file, &col.r, 4);\r
272                     write(file, &col.g, 4);\r
273                     write(file, &col.b, 4);\r
274                     write(file, &col.a, 4);\r
275 \r
276                     write(file, &v[j].TCoords.X, 4);\r
277                     write(file, &v[j].TCoords.Y, 4);\r
278                     if (texcoords == 2)\r
279                     {\r
280                         write(file, &zero, 4);\r
281                         write(file, &zero, 4);\r
282                     }\r
283                 }\r
284                 break;\r
285                 case EVT_2TCOORDS:\r
286                 {\r
287                     S3DVertex2TCoords *v = (S3DVertex2TCoords *) mb->getVertices();\r
288                     const SColorf col(v[j].Color);\r
289                     write(file, &col.r, 4);\r
290                     write(file, &col.g, 4);\r
291                     write(file, &col.b, 4);\r
292                     write(file, &col.a, 4);\r
293 \r
294                     write(file, &v[j].TCoords.X, 4);\r
295                     write(file, &v[j].TCoords.Y, 4);\r
296                     write(file, &v[j].TCoords2.X, 4);\r
297                     write(file, &v[j].TCoords2.Y, 4);\r
298                 }\r
299                 break;\r
300                 case EVT_TANGENTS:\r
301                 {\r
302                     S3DVertexTangents *v = (S3DVertexTangents *) mb->getVertices();\r
303                     const SColorf col(v[j].Color);\r
304                     write(file, &col.r, 4);\r
305                     write(file, &col.g, 4);\r
306                     write(file, &col.b, 4);\r
307                     write(file, &col.a, 4);\r
308 \r
309                     write(file, &v[j].TCoords.X, 4);\r
310                     write(file, &v[j].TCoords.Y, 4);\r
311                     if (texcoords == 2)\r
312                     {\r
313                         write(file, &zero, 4);\r
314                         write(file, &zero, 4);\r
315                     }\r
316                 }\r
317                 break;\r
318             }\r
319         }\r
320     }\r
321     // Check vertsize\r
322     vertcheck = Size - vertcheck;\r
323     if (vertcheck != vertsize)\r
324         {\r
325         printf("Failed in vertex size calculation, size %u advanced %u\n",\r
326             vertsize, vertcheck);\r
327         }\r
328 \r
329     u32 currentMeshBufferIndex = 0;\r
330     // Tris\r
331     for (u32 i = 0; i < numBeshBuffers; i++)\r
332     {\r
333         const IMeshBuffer * const mb = mesh->getMeshBuffer(i);\r
334         write(file, "TRIS", 4);\r
335         const u32 trisize = 4 + mb->getIndexCount() * 4;\r
336         write(file, &trisize, 4);\r
337 \r
338         u32 tricheck = Size;\r
339 \r
340         write(file, &i, 4);\r
341 \r
342         u32 numIndices = mb->getIndexCount();\r
343         const u16 * const idx = (u16 *) mb->getIndices();\r
344         for (u32 j = 0; j < numIndices; j += 3)\r
345                 {\r
346             u32 tmp = idx[j] + currentMeshBufferIndex;\r
347             write(file, &tmp, sizeof(u32));\r
348 \r
349             tmp = idx[j + 1] + currentMeshBufferIndex;\r
350             write(file, &tmp, sizeof(u32));\r
351 \r
352             tmp = idx[j + 2] + currentMeshBufferIndex;\r
353             write(file, &tmp, sizeof(u32));\r
354         }\r
355 \r
356         // Check that tris calculation was ok\r
357         tricheck = Size - tricheck;\r
358         if (tricheck != trisize)\r
359                 {\r
360             printf("Failed in tris size calculation, size %u advanced %u\n",\r
361                 trisize, tricheck);\r
362                 }\r
363 \r
364         currentMeshBufferIndex += mb->getVertexCount();\r
365     }\r
366 \r
367     if(ISkinnedMesh *skinnedMesh = getSkinned(mesh))\r
368     {\r
369         // Write animation data\r
370         if (!skinnedMesh->isStatic())\r
371         {\r
372             write(file, "ANIM", 4);\r
373 \r
374             const u32 animsize = 12;\r
375             write(file, &animsize, 4);\r
376 \r
377             const u32 flags = 0;\r
378             const u32 frames = skinnedMesh->getFrameCount();\r
379             const f32 fps = skinnedMesh->getAnimationSpeed();\r
380 \r
381             write(file, &flags, 4);\r
382             write(file, &frames, 4);\r
383             write(file, &fps, 4);\r
384         }\r
385 \r
386         // Write joints\r
387         core::array<ISkinnedMesh::SJoint*> rootJoints = getRootJoints(skinnedMesh);\r
388 \r
389         for (u32 i = 0; i < rootJoints.size(); i++)\r
390         {\r
391             writeJointChunk(file, skinnedMesh, rootJoints[i]);\r
392         }\r
393     }\r
394 \r
395     // Check that node calculation was ok\r
396     nodecheck = Size - nodecheck;\r
397     if (nodecheck != nodesize)\r
398         {\r
399         printf("Failed in node size calculation, size %u advanced %u\n",\r
400             nodesize, nodecheck);\r
401         }\r
402     file->seek(4);\r
403     file->write(&Size, 4);\r
404 \r
405     return true;\r
406 }\r
407 \r
408 \r
409 \r
410 void CB3DMeshWriter::writeJointChunk(io::IWriteFile* file, ISkinnedMesh* mesh , ISkinnedMesh::SJoint* joint)\r
411 {\r
412     // Node\r
413     write(file, "NODE", 4);\r
414 \r
415     // Calculate node size\r
416     u32 nodesize = getJointChunkSize(mesh, joint);\r
417     nodesize -= 8; // The declaration + size of THIS chunk shouldn't be added to the size\r
418 \r
419     write(file, &nodesize, 4);\r
420 \r
421 \r
422     core::stringc name = joint->Name;\r
423     write(file, name.c_str(), name.size());\r
424     write(file, "", 1);\r
425 \r
426     core::vector3df pos = joint->Animatedposition;\r
427     // Position\r
428     write(file, &pos.X, 4);\r
429     write(file, &pos.Y, 4);\r
430     write(file, &pos.Z, 4);\r
431 \r
432     // Scale\r
433     core::vector3df scale = joint->Animatedscale;\r
434     if (scale == core::vector3df(0, 0, 0))\r
435         scale = core::vector3df(1, 1, 1);\r
436 \r
437     write(file, &scale.X, 4);\r
438     write(file, &scale.Y, 4);\r
439     write(file, &scale.Z, 4);\r
440 \r
441     // Rotation\r
442     core::quaternion quat = joint->Animatedrotation;\r
443     write(file, &quat.W, 4);\r
444     write(file, &quat.X, 4);\r
445     write(file, &quat.Y, 4);\r
446     write(file, &quat.Z, 4);\r
447 \r
448     // Bone\r
449     write(file, "BONE", 4);\r
450     u32 bonesize = 8 * joint->Weights.size();\r
451     write(file, &bonesize, 4);\r
452 \r
453     // Skinning ------------------\r
454     for (u32 i = 0; i < joint->Weights.size(); i++)\r
455     {\r
456         const u32 vertexID = joint->Weights[i].vertex_id;\r
457         const u32 bufferID = joint->Weights[i].buffer_id;\r
458         const f32 weight = joint->Weights[i].strength;\r
459 \r
460         u32 b3dVertexID = vertexID;\r
461         for (u32 j = 0; j < bufferID; j++)\r
462         {\r
463             b3dVertexID += mesh->getMeshBuffer(j)->getVertexCount();\r
464         }\r
465 \r
466         write(file, &b3dVertexID, 4);\r
467         write(file, &weight, 4);\r
468     }\r
469     // ---------------------------\r
470 \r
471     // Animation keys\r
472     if (joint->PositionKeys.size())\r
473     {\r
474         write(file, "KEYS", 4);\r
475         u32 keysSize = 4 * joint->PositionKeys.size() * 4; // X, Y and Z pos + frame\r
476         keysSize += 4;  // Flag to define the type of the key\r
477         write(file, &keysSize, 4);\r
478 \r
479         u32 flag = 1; // 1 = flag for position keys\r
480         write(file, &flag, 4);\r
481 \r
482         for (u32 i = 0; i < joint->PositionKeys.size(); i++)\r
483         {\r
484             const s32 frame = static_cast<s32>(joint->PositionKeys[i].frame);\r
485             const core::vector3df pos = joint->PositionKeys[i].position;\r
486 \r
487             write (file, &frame, 4);\r
488 \r
489             write (file, &pos.X, 4);\r
490             write (file, &pos.Y, 4);\r
491             write (file, &pos.Z, 4);\r
492 \r
493         }\r
494     }\r
495     if (joint->RotationKeys.size())\r
496     {\r
497         write(file, "KEYS", 4);\r
498         u32 keysSize = 4 * joint->RotationKeys.size() * 5; // W, X, Y and Z rot + frame\r
499         keysSize += 4; // Flag\r
500         write(file, &keysSize, 4);\r
501 \r
502         u32 flag = 4;\r
503         write(file, &flag, 4);\r
504 \r
505         for (u32 i = 0; i < joint->RotationKeys.size(); i++)\r
506         {\r
507             const s32 frame = static_cast<s32>(joint->RotationKeys[i].frame);\r
508             const core::quaternion rot = joint->RotationKeys[i].rotation;\r
509 \r
510             write (file, &frame, 4);\r
511 \r
512             write (file, &rot.W, 4);\r
513             write (file, &rot.X, 4);\r
514             write (file, &rot.Y, 4);\r
515             write (file, &rot.Z, 4);\r
516         }\r
517     }\r
518     if (joint->ScaleKeys.size())\r
519     {\r
520         write(file, "KEYS", 4);\r
521         u32 keysSize = 4 * joint->ScaleKeys.size() * 4; // X, Y and Z scale + frame\r
522         keysSize += 4; // Flag\r
523         write(file, &keysSize, 4);\r
524 \r
525         u32 flag = 2;\r
526         write(file, &flag, 4);\r
527 \r
528         for (u32 i = 0; i < joint->ScaleKeys.size(); i++)\r
529         {\r
530             const s32 frame = static_cast<s32>(joint->ScaleKeys[i].frame);\r
531             const core::vector3df scale = joint->ScaleKeys[i].scale;\r
532 \r
533             write (file, &frame, 4);\r
534 \r
535             write (file, &scale.X, 4);\r
536             write (file, &scale.Y, 4);\r
537             write (file, &scale.Z, 4);\r
538         }\r
539     }\r
540 \r
541     for (u32 i = 0; i < joint->Children.size(); i++)\r
542     {\r
543         writeJointChunk(file, mesh, joint->Children[i]);\r
544     }\r
545 }\r
546 \r
547 \r
548 ISkinnedMesh* CB3DMeshWriter::getSkinned (IMesh *mesh)\r
549 {\r
550         if (mesh->getMeshType() == EAMT_SKINNED)\r
551     {\r
552                 return static_cast<ISkinnedMesh*>(mesh);\r
553     }\r
554     return 0;\r
555 }\r
556 \r
557 u32 CB3DMeshWriter::getJointChunkSize(const ISkinnedMesh* mesh, ISkinnedMesh::SJoint* joint)\r
558 {\r
559     u32 chunkSize = 8 + 40; // Chunk declaration + chunk data\r
560     chunkSize += joint->Name.size() + 1; // the NULL character at the end of the string\r
561 \r
562     u32 boneSize = joint->Weights.size() * 8; // vertex_id + weight = 8 bits per weight block\r
563     boneSize += 8; // declaration + size of he BONE chunk\r
564 \r
565     u32 keysSize = 0;\r
566     if (joint->PositionKeys.size() != 0)\r
567     {\r
568         keysSize += 8; // KEYS + chunk size\r
569         keysSize += 4; // flags\r
570 \r
571         keysSize += (joint->PositionKeys.size() * 16);\r
572     }\r
573     if (joint->RotationKeys.size() != 0)\r
574     {\r
575         keysSize += 8; // KEYS + chunk size\r
576         keysSize += 4; // flags\r
577 \r
578         keysSize += (joint->RotationKeys.size() * 20);\r
579     }\r
580     if (joint->ScaleKeys.size() != 0)\r
581     {\r
582         keysSize += 8; // KEYS + chunk size\r
583         keysSize += 4; // flags\r
584 \r
585         keysSize += (joint->ScaleKeys.size() * 16);\r
586     }\r
587 \r
588     chunkSize += boneSize;\r
589     chunkSize += keysSize;\r
590 \r
591     for (u32 i = 0; i < joint->Children.size(); ++i)\r
592     {\r
593         chunkSize += (getJointChunkSize(mesh, joint->Children[i]));\r
594     }\r
595     return chunkSize;\r
596 }\r
597 \r
598 core::array<ISkinnedMesh::SJoint*> CB3DMeshWriter::getRootJoints(const ISkinnedMesh* mesh)\r
599 {\r
600     core::array<ISkinnedMesh::SJoint*> roots;\r
601 \r
602     core::array<ISkinnedMesh::SJoint*> allJoints = mesh->getAllJoints();\r
603     for (u32 i = 0; i < allJoints.size(); i++)\r
604     {\r
605         bool isRoot = true;\r
606         ISkinnedMesh::SJoint* testedJoint = allJoints[i];\r
607         for (u32 j = 0; j < allJoints.size(); j++)\r
608         {\r
609            ISkinnedMesh::SJoint* testedJoint2 = allJoints[j];\r
610            for (u32 k = 0; k < testedJoint2->Children.size(); k++)\r
611            {\r
612                if (testedJoint == testedJoint2->Children[k])\r
613                     isRoot = false;\r
614            }\r
615         }\r
616         if (isRoot)\r
617             roots.push_back(testedJoint);\r
618     }\r
619 \r
620     return roots;\r
621 }\r
622 \r
623 u32 CB3DMeshWriter::getUVlayerCount(IMesh* mesh)\r
624 {\r
625     const u32 numBeshBuffers = mesh->getMeshBufferCount();\r
626     for (u32 i = 0; i < numBeshBuffers; i++)\r
627     {\r
628         const IMeshBuffer * const mb = mesh->getMeshBuffer(i);\r
629 \r
630         if (mb->getVertexType() == EVT_2TCOORDS)\r
631         {\r
632             return 2;\r
633         }\r
634     }\r
635     return 1;\r
636 }\r
637 \r
638 void CB3DMeshWriter::write(io::IWriteFile* file, const void *ptr, const u32 bytes)\r
639 {\r
640         file->write(ptr, bytes);\r
641         Size += bytes;\r
642 }\r
643 \r
644 } // end namespace\r
645 } // end namespace\r
646 \r
647 #endif // _IRR_COMPILE_WITH_B3D_WRITER_\r
648 \r