1 //  SuperTuxKart - a fun racing game with go-kart
2 //  Copyright (C) 2017 SuperTuxKart-Team
3 //
4 //  This program is free software; you can redistribute it and/or
5 //  modify it under the terms of the GNU General Public License
6 //  as published by the Free Software Foundation; either version 3
7 //  of the License, or (at your option) any later version.
8 //
9 //  This program is distributed in the hope that it will be useful,
10 //  but WITHOUT ANY WARRANTY; without even the implied warranty of
11 //  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12 //  GNU General Public License for more details.
13 //
14 //  You should have received a copy of the GNU General Public License
15 //  along with this program; if not, write to the Free Software
16 //  Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
17 
18 #include "graphics/sp_mesh_loader.hpp"
19 
20 #include "graphics/sp/sp_mesh.hpp"
21 #include "graphics/sp/sp_mesh_buffer.hpp"
22 #include "graphics/central_settings.hpp"
23 #include "graphics/material_manager.hpp"
24 #include "graphics/stk_tex_manager.hpp"
25 #include "utils/constants.hpp"
26 #include "utils/mini_glm.hpp"
27 
28 #include "../../lib/irrlicht/source/Irrlicht/CSkinnedMesh.h"
29 const uint8_t VERSION_NOW = 1;
30 
31 #include <algorithm>
32 #include <cmath>
33 #include <IVideoDriver.h>
34 #include <IFileSystem.h>
35 
36 // ----------------------------------------------------------------------------
isALoadableFileExtension(const io::path & filename) const37 bool SPMeshLoader::isALoadableFileExtension(const io::path& filename) const
38 {
39     return core::hasFileExtension(filename, "spm");
40 }   // isALoadableFileExtension
41 
42 // ----------------------------------------------------------------------------
createMesh(io::IReadFile * f)43 scene::IAnimatedMesh* SPMeshLoader::createMesh(io::IReadFile* f)
44 {
45 #ifndef SERVER_ONLY
46     const bool real_spm = CVS->isGLSL();
47 #else
48     const bool real_spm = false;
49 #endif
50     if (!IS_LITTLE_ENDIAN)
51     {
52         Log::error("SPMeshLoader", "Not little endian machine.");
53         return NULL;
54     }
55     if (f == NULL)
56     {
57         return NULL;
58     }
59     m_bind_frame = 0;
60     m_joint_count = 0;
61     m_frame_count = 0;
62     m_mesh = NULL;
63     m_mesh = real_spm ? new SP::SPMesh() : m_scene_manager->createSkinnedMesh();
64     io::IFileSystem* fs = m_scene_manager->getFileSystem();
65     std::string base_path = fs->getFileDir(f->getFileName()).c_str();
66     std::string header;
67     header.resize(2);
68     f->read(&header.front(), 2);
69     if (header != "SP")
70     {
71         Log::error("SPMeshLoader", "Not a spm file.");
72         m_mesh->drop();
73         return NULL;
74     }
75     uint8_t byte = 0;
76     f->read(&byte, 1);
77     uint8_t version = byte >> 3;
78     if (version != VERSION_NOW)
79     {
80         Log::error("SPMeshLoader", "Version mismatch, file %d SP %d", version,
81             VERSION_NOW);
82         m_mesh->drop();
83         return NULL;
84     }
85     byte &= ~0x08;
86     header = byte == 0 ? "SPMS" : byte == 1 ? "SPMA" : "SPMN";
87     if (header == "SPMS")
88     {
89         Log::error("SPMeshLoader", "Space partitioned mesh not supported.");
90         m_mesh->drop();
91         return NULL;
92     }
93     f->read(&byte, 1);
94     bool read_normal = byte & 0x01;
95     bool read_vcolor = byte >> 1 & 0x01;
96     bool read_tangent = byte >> 2 & 0x01;
97     const bool is_skinned = header == "SPMA";
98     const SPVertexType vt = is_skinned ? SPVT_SKINNED : SPVT_NORMAL;
99     float bbox[6];
100     f->read(bbox, 24);
101     uint16_t size_num = 0;
102     f->read(&size_num, 2);
103     unsigned id = 0;
104     std::unordered_map<unsigned, std::tuple<video::SMaterial, bool,
105         bool> > mat_map;
106     std::unordered_map<unsigned, std::tuple<Material*, bool,
107         bool> > sp_mat_map;
108     while (size_num != 0)
109     {
110         uint8_t tex_size;
111         std::string tex_name_1, tex_name_2;
112         f->read(&tex_size, 1);
113         if (tex_size > 0)
114         {
115             tex_name_1.resize(tex_size);
116             f->read(&tex_name_1.front(), tex_size);
117         }
118         f->read(&tex_size, 1);
119         if (tex_size > 0)
120         {
121             tex_name_2.resize(tex_size);
122             f->read(&tex_name_2.front(), tex_size);
123         }
124         if (real_spm)
125         {
126             if (!tex_name_1.empty())
127             {
128                 std::string full_path = base_path + "/" + tex_name_1;
129                 if (fs->existFile(full_path.c_str()))
130                 {
131                     tex_name_1 = full_path;
132                 }
133             }
134             sp_mat_map[id] =
135                 std::make_tuple(
136                 material_manager->getMaterialSPM(tex_name_1, tex_name_2),
137                 !tex_name_1.empty(), !tex_name_2.empty());
138         }
139         else
140         {
141             video::ITexture* textures[2] = { NULL, NULL };
142             if (!tex_name_1.empty())
143             {
144                 std::string full_path = base_path + "/" + tex_name_1;
145                 if (fs->existFile(full_path.c_str()))
146                 {
147                     tex_name_1 = full_path;
148                 }
149                 video::ITexture* tex = STKTexManager::getInstance()
150                     ->getTexture(tex_name_1);
151                 if (tex != NULL)
152                 {
153                     textures[0] = tex;
154                 }
155             }
156             if (!tex_name_2.empty())
157             {
158                 std::string full_path = base_path + "/" + tex_name_2;
159                 if (fs->existFile(full_path.c_str()))
160                 {
161                     tex_name_2 = full_path;
162                 }
163                 textures[1] = STKTexManager::getInstance()->getTexture
164                     (tex_name_2);
165             }
166 
167             video::SMaterial m;
168             m.MaterialType = video::EMT_SOLID;
169             if (textures[0] != NULL)
170             {
171                 m.setTexture(0, textures[0]);
172             }
173             if (textures[1] != NULL)
174             {
175                 m.setTexture(1, textures[1]);
176             }
177             mat_map[id] =
178                 std::make_tuple(m, !tex_name_1.empty(), !tex_name_2.empty());
179         }
180         size_num--;
181         id++;
182     }
183     f->read(&size_num, 2);
184     while (size_num != 0)
185     {
186         uint16_t mat_size;
187         f->read(&mat_size, 2);
188         while (mat_size != 0)
189         {
190             uint32_t vertices_count, indices_count;
191             uint16_t mat_id;
192             f->read(&vertices_count, 4);
193             if (vertices_count > 65535)
194             {
195                 Log::error("SPMeshLoader", "32bit index not supported.");
196                 m_mesh->drop();
197                 return NULL;
198             }
199             f->read(&indices_count, 4);
200             f->read(&mat_id, 2);
201             if (real_spm)
202             {
203                 assert(mat_id < sp_mat_map.size());
204                 decompressSPM(f, vertices_count, indices_count, read_normal,
205                     read_vcolor, read_tangent, std::get<1>(sp_mat_map[mat_id]),
206                     std::get<2>(sp_mat_map[mat_id]), vt,
207                     std::get<0>(sp_mat_map[mat_id]));
208             }
209             else
210             {
211                 assert(mat_id < mat_map.size());
212                 decompress(f, vertices_count, indices_count, read_normal,
213                     read_vcolor, read_tangent, std::get<1>(mat_map[mat_id]),
214                     std::get<2>(mat_map[mat_id]), vt,
215                     std::get<0>(mat_map[mat_id]));
216             }
217             mat_size--;
218         }
219         if (header == "SPMS")
220         {
221             // Reserved, never used
222             assert(false);
223             f->read(bbox, 24);
224         }
225         size_num--;
226     }
227     if (header == "SPMA")
228     {
229         createAnimationData(f);
230         convertIrrlicht();
231     }
232     else if (header == "SPMS")
233     {
234         // Reserved, never used
235         assert(false);
236         uint16_t pre_computed_size = 0;
237         f->read(&pre_computed_size, 2);
238     }
239     const bool has_armature = !m_all_armatures.empty();
240     if (real_spm)
241     {
242         SP::SPMesh* spm = static_cast<SP::SPMesh*>(m_mesh);
243         spm->m_bind_frame = m_bind_frame;
244         spm->m_joint_using = m_joint_count;
245         // Because the last frame in spm is usable
246         if (has_armature)
247         {
248             spm->m_frame_count = m_frame_count + 1;
249         }
250         for (unsigned i = 0; i < m_all_armatures.size(); i++)
251         {
252             // This is diffferent from m_joint_using
253             spm->m_total_joints +=
254                 (unsigned)m_all_armatures[i].m_joint_names.size();
255         }
256         spm->m_all_armatures = std::move(m_all_armatures);
257     }
258     m_mesh->finalize();
259     if (!real_spm && has_armature)
260     {
261         // Because the last frame in spm is usable
262         static_cast<scene::CSkinnedMesh*>(m_mesh)->AnimationFrames =
263             (float)m_frame_count + 1.0f;
264     }
265     m_all_armatures.clear();
266     m_to_bind_pose_matrices.clear();
267     m_joints.clear();
268     return m_mesh;
269 }   // createMesh
270 
271 // ----------------------------------------------------------------------------
decompressSPM(irr::io::IReadFile * spm,unsigned vertices_count,unsigned indices_count,bool read_normal,bool read_vcolor,bool read_tangent,bool uv_one,bool uv_two,SPVertexType vt,Material * m)272 void SPMeshLoader::decompressSPM(irr::io::IReadFile* spm,
273                                  unsigned vertices_count,
274                                  unsigned indices_count, bool read_normal,
275                                  bool read_vcolor, bool read_tangent,
276                                  bool uv_one, bool uv_two, SPVertexType vt,
277                                  Material* m)
278 {
279     assert(vertices_count != 0);
280     assert(indices_count != 0);
281 
282     using namespace SP;
283     SPMeshBuffer* mb = new SPMeshBuffer();
284     static_cast<SPMesh*>(m_mesh)->m_buffer.push_back(mb);
285     const unsigned idx_size = vertices_count > 255 ? 2 : 1;
286     for (unsigned i = 0; i < vertices_count; i++)
287     {
288         video::S3DVertexSkinnedMesh vertex = {};
289         // 3 * float position
290         spm->read(&vertex.m_position, 12);
291         if (read_normal)
292         {
293             spm->read(&vertex.m_normal, 4);
294         }
295         else
296         {
297             // 0, 1, 0
298             vertex.m_normal = 0x1FF << 10;
299         }
300         if (read_vcolor)
301         {
302             // Color identifier
303             uint8_t ci;
304             spm->read(&ci, 1);
305             if (ci == 128)
306             {
307                 // All white
308                 vertex.m_color = video::SColor(255, 255, 255, 255);
309             }
310             else
311             {
312                 uint8_t r, g, b;
313                 spm->read(&r, 1);
314                 spm->read(&g, 1);
315                 spm->read(&b, 1);
316                 vertex.m_color = video::SColor(255, r, g, b);
317             }
318         }
319         else
320         {
321             vertex.m_color = video::SColor(255, 255, 255, 255);
322         }
323         if (uv_one)
324         {
325             spm->read(&vertex.m_all_uvs[0], 4);
326             if (uv_two)
327             {
328                 spm->read(&vertex.m_all_uvs[2], 4);
329             }
330             if (read_tangent)
331             {
332                 spm->read(&vertex.m_tangent, 4);
333             }
334             else
335             {
336                 vertex.m_tangent = MiniGLM::quickTangent(vertex.m_normal);
337             }
338         }
339         if (vt == SPVT_SKINNED)
340         {
341             spm->read(&vertex.m_joint_idx[0], 16);
342             if (vertex.m_joint_idx[0] == -1 ||
343                 vertex.m_weight[0] == 0 ||
344                 // -0.0 in half float (16bit)
345                 vertex.m_weight[0] == -32768)
346             {
347                 // For the skinned mesh shader
348                 vertex.m_joint_idx[0] = -32767;
349                 // 1.0 in half float (16bit)
350                 vertex.m_weight[0] = 15360;
351             }
352         }
353         mb->addSPMVertex(vertex);
354     }
355 
356     std::vector<uint16_t> indices;
357     indices.resize(indices_count);
358     if (idx_size == 2)
359     {
360         spm->read(indices.data(), indices_count * 2);
361     }
362     else
363     {
364         std::vector<uint8_t> tmp_idx;
365         tmp_idx.resize(indices_count);
366         spm->read(tmp_idx.data(), indices_count);
367         for (unsigned i = 0; i < indices_count; i++)
368         {
369             indices[i] = tmp_idx[i];
370         }
371     }
372     mb->setIndices(indices);
373     mb->setSTKMaterial(m);
374 
375 }   // decompressSPM
376 
377 // ----------------------------------------------------------------------------
decompress(irr::io::IReadFile * spm,unsigned vertices_count,unsigned indices_count,bool read_normal,bool read_vcolor,bool read_tangent,bool uv_one,bool uv_two,SPVertexType vt,const video::SMaterial & m)378 void SPMeshLoader::decompress(irr::io::IReadFile* spm, unsigned vertices_count,
379                               unsigned indices_count, bool read_normal,
380                               bool read_vcolor, bool read_tangent, bool uv_one,
381                               bool uv_two, SPVertexType vt,
382                               const video::SMaterial& m)
383 {
384     assert(vertices_count != 0);
385     assert(indices_count != 0);
386     scene::SSkinMeshBuffer* mb = m_mesh->addMeshBuffer();
387     if (uv_two)
388     {
389         mb->convertTo2TCoords();
390     }
391     using namespace MiniGLM;
392     const unsigned idx_size = vertices_count > 255 ? 2 : 1;
393     char tmp[8] = {};
394     std::vector<std::pair<std::array<short, 4>, std::array<float, 4> > >
395         cur_joints;
396     for (unsigned i = 0; i < vertices_count; i++)
397     {
398         video::S3DVertex2TCoords vertex;
399         // 3 * float position
400         spm->read(&vertex.Pos, 12);
401         if (read_normal)
402         {
403             // 3 10 + 2 bits normal
404             uint32_t packed;
405             spm->read(&packed, 4);
406             vertex.Normal = decompressVector3(packed);
407         }
408         if (read_vcolor)
409         {
410             // Color identifier
411             uint8_t ci;
412             spm->read(&ci, 1);
413             if (ci == 128)
414             {
415                 // All white
416                 vertex.Color = video::SColor(255, 255, 255, 255);
417             }
418             else
419             {
420                 uint8_t r, g, b;
421                 spm->read(&r, 1);
422                 spm->read(&g, 1);
423                 spm->read(&b, 1);
424                 vertex.Color = video::SColor(255, r, g, b);
425             }
426         }
427         else
428         {
429             vertex.Color = video::SColor(255, 255, 255, 255);
430         }
431         if (uv_one)
432         {
433             short hf[2];
434             spm->read(hf, 4);
435             vertex.TCoords.X = toFloat32(hf[0]);
436             vertex.TCoords.Y = toFloat32(hf[1]);
437             assert(!std::isnan(vertex.TCoords.X));
438             assert(!std::isnan(vertex.TCoords.Y));
439             if (uv_two)
440             {
441                 spm->read(hf, 4);
442                 vertex.TCoords2.X = toFloat32(hf[0]);
443                 vertex.TCoords2.Y = toFloat32(hf[1]);
444                 assert(!std::isnan(vertex.TCoords2.X));
445                 assert(!std::isnan(vertex.TCoords2.Y));
446             }
447             if (read_tangent)
448             {
449                 uint32_t packed;
450                 spm->read(&packed, 4);
451             }
452         }
453         if (vt == SPVT_SKINNED)
454         {
455             std::array<short, 4> joint_idx;
456             spm->read(joint_idx.data(), 8);
457             spm->read(tmp, 8);
458             std::array<float, 4> joint_weight = {};
459             for (int j = 0; j < 8; j += 2)
460             {
461                 short hf;
462                 memcpy(&hf, tmp + j, 2);
463                 const unsigned idx = j >> 1;
464                 joint_weight[idx] = toFloat32(hf);
465                 assert(!std::isnan(joint_weight[idx]));
466             }
467             cur_joints.emplace_back(joint_idx, joint_weight);
468         }
469         if (uv_two)
470         {
471             mb->Vertices_2TCoords.push_back(vertex);
472         }
473         else
474         {
475             mb->Vertices_Standard.push_back(vertex);
476         }
477     }
478     if (vt == SPVT_SKINNED)
479     {
480         m_joints.emplace_back(std::move(cur_joints));
481     }
482     if (m.TextureLayer[0].Texture != NULL)
483     {
484         mb->Material = m;
485     }
486     mb->Indices.set_used(indices_count);
487     if (idx_size == 2)
488     {
489         spm->read(mb->Indices.pointer(), indices_count * 2);
490     }
491     else
492     {
493         std::vector<uint8_t> tmp_idx;
494         tmp_idx.resize(indices_count);
495         spm->read(tmp_idx.data(), indices_count);
496         for (unsigned i = 0; i < indices_count; i++)
497         {
498             mb->Indices[i] = tmp_idx[i];
499         }
500     }
501 
502     if (!read_normal)
503     {
504         for (unsigned i = 0; i < mb->Indices.size(); i += 3)
505         {
506             core::plane3df p(mb->getVertex(mb->Indices[i])->Pos,
507                 mb->getVertex(mb->Indices[i + 1])->Pos,
508                 mb->getVertex(mb->Indices[i + 2])->Pos);
509             mb->getVertex(mb->Indices[i])->Normal += p.Normal;
510             mb->getVertex(mb->Indices[i + 1])->Normal += p.Normal;
511             mb->getVertex(mb->Indices[i + 2])->Normal += p.Normal;
512         }
513         for (unsigned i = 0; i < mb->getVertexCount(); i++)
514         {
515             mb->getVertex(i)->Normal.normalize();
516         }
517     }
518 }   // decompress
519 
520 // ----------------------------------------------------------------------------
createAnimationData(irr::io::IReadFile * spm)521 void SPMeshLoader::createAnimationData(irr::io::IReadFile* spm)
522 {
523     uint8_t armature_size = 0;
524     spm->read(&armature_size, 1);
525     assert(armature_size > 0);
526     m_bind_frame = 0;
527     spm->read(&m_bind_frame, 2);
528     m_all_armatures.resize(armature_size);
529     for (unsigned i = 0; i < armature_size; i++)
530     {
531         m_all_armatures[i].read(spm);
532     }
533     for (unsigned i = 0; i < armature_size; i++)
534     {
535         m_frame_count = std::max(m_frame_count,
536             (unsigned)m_all_armatures[i].m_frame_pose_matrices.back().first);
537         m_joint_count += m_all_armatures[i].m_joint_used;
538     }
539 
540     m_to_bind_pose_matrices.resize(m_joint_count);
541     unsigned accumulated_joints = 0;
542     for (unsigned i = 0; i < armature_size; i++)
543     {
544         m_all_armatures[i].getPose((float)m_bind_frame,
545             &m_to_bind_pose_matrices[accumulated_joints]);
546         accumulated_joints += m_all_armatures[i].m_joint_used;
547     }
548 
549     // Only for legacy device
550     if (m_joints.empty())
551     {
552         return;
553     }
554     assert(m_joints.size() == m_mesh->getMeshBufferCount());
555     for (unsigned i = 0; i < m_to_bind_pose_matrices.size(); i++)
556     {
557         m_to_bind_pose_matrices[i].makeInverse();
558     }
559     for (unsigned i = 0; i < m_mesh->getMeshBufferCount(); i++)
560     {
561         for (unsigned j = 0; j < m_joints[i].size(); j++)
562         {
563             if (!(m_joints[i][j].first[0] == -1 ||
564                 m_joints[i][j].second[0] == 0.0f))
565             {
566                 core::vector3df bind_pos, bind_nor;
567                 for (unsigned k = 0; k < 4; k++)
568                 {
569                     if (m_joints[i][j].second[k] == 0.0f)
570                     {
571                         break;
572                     }
573                     core::vector3df cur_pos, cur_nor;
574                     m_to_bind_pose_matrices[m_joints[i][j].first[k]]
575                         .transformVect(cur_pos,
576                         m_mesh->getMeshBuffers()[i]->getVertex(j)->Pos);
577                     bind_pos += cur_pos * m_joints[i][j].second[k];
578                     m_to_bind_pose_matrices[m_joints[i][j].first[k]]
579                         .rotateVect(cur_nor,
580                         m_mesh->getMeshBuffers()[i]->getVertex(j)->Normal);
581                     bind_nor += cur_nor * m_joints[i][j].second[k];
582                 }
583                 m_mesh->getMeshBuffers()[i]->getVertex(j)->Pos = bind_pos;
584                 m_mesh->getMeshBuffers()[i]->getVertex(j)->Normal = bind_nor;
585             }
586         }
587     }
588 }   // createAnimationData
589 
590 // ----------------------------------------------------------------------------
convertIrrlicht()591 void SPMeshLoader::convertIrrlicht()
592 {
593     // Only for legacy device
594     if (m_joints.empty())
595     {
596         return;
597     }
598     unsigned total_joints = 0;
599     for (unsigned i = 0; i < m_all_armatures.size(); i++)
600     {
601         total_joints += (unsigned)m_all_armatures[i].m_joint_names.size();
602     }
603     for (unsigned i = 0; i < total_joints; i++)
604     {
605         m_mesh->addJoint(NULL);
606     }
607     core::array<scene::ISkinnedMesh::SJoint*>& joints = m_mesh->getAllJoints();
608     std::vector<int> used_joints_map;
609     used_joints_map.resize(m_joint_count);
610     total_joints = 0;
611     unsigned used_joints = 0;
612     for (unsigned i = 0; i < m_all_armatures.size(); i++)
613     {
614         for (unsigned j = 0; j < m_all_armatures[i].m_joint_names.size(); j++)
615         {
616             if (m_all_armatures[i].m_joint_used > j)
617             {
618                 used_joints_map[used_joints + j] = total_joints + j;
619             }
620             joints[total_joints + j]->Name =
621                 m_all_armatures[i].m_joint_names[j].c_str();
622             const int p_id = m_all_armatures[i].m_parent_infos[j];
623             core::matrix4 tmp;
624             if (p_id != -1)
625             {
626                 joints[total_joints + p_id]->Children.push_back
627                     (joints[total_joints + j]);
628                 m_all_armatures[i].m_joint_matrices[j].getInverse(tmp);
629                 tmp = m_all_armatures[i].m_joint_matrices[p_id] * tmp;
630             }
631             else
632             {
633                 m_all_armatures[i].m_joint_matrices[j].getInverse(tmp);
634             }
635             joints[total_joints + j]->LocalMatrix = tmp;
636             for (unsigned k = 0; k <
637                 m_all_armatures[i].m_frame_pose_matrices.size(); k++)
638             {
639                 float frame = (float)
640                     m_all_armatures[i].m_frame_pose_matrices[k].first;
641                 core::vector3df pos = m_all_armatures[i]
642                     .m_frame_pose_matrices[k].second[j].m_loc;
643                 core::quaternion q = m_all_armatures[i]
644                     .m_frame_pose_matrices[k].second[j].m_rot;
645                 core::vector3df scl = m_all_armatures[i]
646                     .m_frame_pose_matrices[k].second[j].m_scale;
647                 joints[total_joints + j]->PositionKeys.push_back({frame, pos});
648                 joints[total_joints + j]->RotationKeys.push_back({frame,
649                     // Reverse for broken irrlicht quaternion
650                     core::quaternion(q.X, q.Y, q.Z, -q.W)});
651                 joints[total_joints + j]->ScaleKeys.push_back({frame, scl});
652             }
653         }
654         total_joints += (unsigned)m_all_armatures[i].m_joint_names.size();
655         used_joints += m_all_armatures[i].m_joint_used;
656     }
657 
658     for (unsigned i = 0; i < m_joints.size(); i++)
659     {
660         for (unsigned j = 0; j < m_joints[i].size(); j++)
661         {
662             for (unsigned k = 0; k < 4; k++)
663             {
664                 if (m_joints[i][j].first[k] == -1 ||
665                     m_joints[i][j].second[k] == 0.0f)
666                 {
667                     break;
668                 }
669                 scene::ISkinnedMesh::SWeight* w = m_mesh->addWeight
670                     (joints[used_joints_map[m_joints[i][j].first[k]]]);
671                 w->buffer_id = (uint16_t)i;
672                 w->vertex_id = j;
673                 w->strength = m_joints[i][j].second[k];
674             }
675         }
676     }
677 
678 }   // convertIrrlicht
679