1 /*
2 Copyright (c) 2017-2018 Timur Gafarov
3 
4 Boost Software License - Version 1.0 - August 17th, 2003
5 Permission is hereby granted, free of charge, to any person or organization
6 obtaining a copy of the software and accompanying documentation covered by
7 this license (the "Software") to use, reproduce, display, distribute,
8 execute, and transmit the Software, and to prepare derivative works of the
9 Software, and to permit third-parties to whom the Software is furnished to
10 do so, all subject to the following:
11 
12 The copyright notices in the Software and this entire statement, including
13 the above license grant, this restriction and the following disclaimer,
14 must be included in all copies of the Software, in whole or in part, and
15 all derivative works of the Software, unless such copies or derivative
16 works are solely in the form of machine-executable object code generated by
17 a source language processor.
18 
19 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
20 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
21 FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT
22 SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE
23 FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE,
24 ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
25 DEALINGS IN THE SOFTWARE.
26 */
27 
28 module dagon.resource.iqm;
29 
30 import std.stdio;
31 import std.math;
32 import std.string;
33 import std.path;
34 
35 import dlib.core.memory;
36 import dlib.core.stream;
37 import dlib.container.array;
38 import dlib.container.dict;
39 import dlib.filesystem.filesystem;
40 import dlib.math.vector;
41 import dlib.math.matrix;
42 import dlib.math.quaternion;
43 import dlib.math.interpolation;
44 
45 import dagon.core.ownership;
46 import dagon.graphics.animmodel;
47 import dagon.graphics.texture;
48 import dagon.resource.asset;
49 import dagon.resource.serialization;
50 import dagon.resource.textureasset;
51 
52 enum IQM_VERSION = 2;
53 
54 struct IQMHeader
55 {
56     ubyte[16] magic;
57     uint ver;
58     uint filesize;
59     uint flags;
60     uint numText, ofsText;
61     uint numMeshes, ofsMeshes;
62     uint numVertexArrays, numVertices, ofsVertexArrays;
63     uint numTriangles, ofsTriangles, ofsAdjacency;
64     uint numJoints, ofsJoints;
65     uint numPoses, ofsPoses;
66     uint numAnims, ofsAnims;
67     uint numFrames, numFrameChannels, ofsFrames, ofsBounds;
68     uint numComment, ofsComment;
69     uint numExtensions, ofsExtensions;
70 }
71 
72 struct IQMVertexArray
73 {
74     uint type;
75     uint flags;
76     uint format;
77     uint size;
78     uint offset;
79 }
80 
81 alias uint[3] IQMTriangle;
82 
83 struct IQMJoint
84 {
85     uint name;
86     int parent;
87     Vector3f translation;
88     Quaternionf rotation;
89     Vector3f scaling;
90 }
91 
92 struct IQMMesh
93 {
94     uint name;
95     uint material;
96     uint firstVertex, numVertices;
97     uint firstTriangle, numTriangles;
98 }
99 
100 alias ubyte[4] IQMBlendIndex;
101 alias ubyte[4] IQMBlendWeight;
102 
103 enum
104 {
105     IQM_POSITION     = 0,
106     IQM_TEXCOORD     = 1,
107     IQM_NORMAL       = 2,
108     IQM_TANGENT      = 3,
109     IQM_BLENDINDEXES = 4,
110     IQM_BLENDWEIGHTS = 5,
111     IQM_COLOR        = 6,
112     IQM_CUSTOM       = 0x10
113 }
114 
115 struct IQMPose
116 {
117     int parent;
118     uint mask;
119     float[10] channelOffset;
120     float[10] channelScale;
121 }
122 
123 struct IQMAnim
124 {
125     uint name;
126     uint firstFrame;
127     uint numFrames;
128     float framerate;
129     uint flags;
130 }
131 
132 //version = IQMDebug;
133 
134 class IQMModel: AnimatedModel
135 {
136     DynamicArray!Vector3f vertices;
137     DynamicArray!Vector3f normals;
138     DynamicArray!Vector2f texcoords;
139     // TODO: tangents
140 
141     DynamicArray!IQMBlendIndex blendIndices;
142     DynamicArray!IQMBlendWeight blendWeights;
143 
144     IQMTriangle[] tris;
145     IQMVertexArray[] vas;
146     IQMMesh[] meshes;
147     AnimationFacegroup[] facegroups;
148 
149     DynamicArray!IQMJoint joints;
150 
151     Matrix4x4f[] baseFrame;
152     Matrix4x4f[] invBaseFrame;
153     
154     Matrix4x4f[] frames;
155     
156     uint numFrames;
157 
158     ubyte[] textBuffer;
159 
160     Dict!(IQMAnim, string) animations;
161     
162     this(InputStream istrm, ReadOnlyFileSystem rofs, AssetManager mngr)
163     {
164         load(istrm, rofs, mngr);
165     }
166 
167     ~this()
168     {
169         release();
170     }
171 
172     void release()
173     {
174         vertices.free();
175         normals.free();
176         texcoords.free();
177 
178         blendIndices.free();
179         blendWeights.free();
180 
181         if (tris.length) Delete(tris);
182         if (vas.length) Delete(vas);
183         if (meshes.length) Delete(meshes);
184 
185         joints.free();
186 
187         if (baseFrame.length) Delete(baseFrame);
188         if (invBaseFrame.length) Delete(invBaseFrame);
189         if (frames.length) Delete(frames);
190 
191         if (textBuffer.length) Delete(textBuffer);
192 
193         if (animations) Delete(animations);
194 
195         if (facegroups) Delete(facegroups);
196     }
197     
198     void load(InputStream istrm, ReadOnlyFileSystem rofs, AssetManager mngr)
199     {
200         // Header part
201         IQMHeader hdr = istrm.read!(IQMHeader, true);
202 
203         version(IQMDebug)
204         {
205             writefln("hdr.magic: %s", cast(string)hdr.magic);
206             writefln("hdr.ver: %s", hdr.ver);
207         }
208         assert(cast(string)hdr.magic == "INTERQUAKEMODEL\0");
209         assert(hdr.ver == IQM_VERSION);
210 
211         version(IQMDebug)
212         {
213             writefln("hdr.numText: %s", hdr.numText);
214             writefln("hdr.ofsText: %s", hdr.ofsText);
215         }
216        
217         istrm.setPosition(hdr.ofsText);
218         textBuffer = New!(ubyte[])(hdr.numText);
219         istrm.fillArray(textBuffer);
220         version(IQMDebug)
221             writefln("text:\n%s", cast(string)textBuffer);
222 
223         // FIXME: 
224         //Delete(buf);
225 
226         // Vertex data part
227         version(IQMDebug)
228         {
229             writefln("hdr.numVertexArrays: %s", hdr.numVertexArrays);
230             writefln("hdr.ofsVertexArrays: %s", hdr.ofsVertexArrays);
231         }
232 
233         vas = New!(IQMVertexArray[])(hdr.numVertexArrays);
234         istrm.setPosition(hdr.ofsVertexArrays);
235         foreach(i; 0..hdr.numVertexArrays)
236         {
237             vas[i] = istrm.read!(IQMVertexArray, true);
238         }
239 
240         // FIXME:
241         //Delete(vas);
242 
243         foreach(i, va; vas)
244         {
245             version(IQMDebug)
246             {
247                 writefln("Vertex array %s:", i);
248                 writefln("va.type: %s", va.type);
249                 writefln("va.flags: %s", va.flags);
250                 writefln("va.format: %s", va.format);
251                 writefln("va.size: %s", va.size);
252                 writefln("va.offset: %s", va.offset);
253                 writeln("---------------");
254             }
255 
256             if (va.type == IQM_POSITION)
257             {
258                 assert(va.size == 3);
259                 // TODO: format asserion
260                 auto verts = New!(Vector3f[])(hdr.numVertices);
261                 istrm.setPosition(va.offset);
262                 istrm.fillArray(verts);
263                 vertices.append(verts);
264                 Delete(verts);
265             }
266             else if (va.type == IQM_NORMAL)
267             {
268                 assert(va.size == 3);
269                 // TODO: format asserion
270                 auto norms = New!(Vector3f[])(hdr.numVertices);
271                 istrm.setPosition(va.offset);
272                 istrm.fillArray(norms);
273                 normals.append(norms);
274                 Delete(norms);
275             }
276             else if (va.type == IQM_TEXCOORD)
277             {
278                 assert(va.size == 2);
279                 // TODO: format asserion
280                 auto texs = New!(Vector2f[])(hdr.numVertices);
281                 istrm.setPosition(va.offset);
282                 istrm.fillArray(texs);
283                 texcoords.append(texs);
284                 Delete(texs);
285             }
286             /* TODO: IQM_TANGENT */ 
287             else if (va.type == IQM_BLENDINDEXES)
288             {
289                 assert(va.size == 4);
290                 // TODO: format asserion
291                 auto bi = New!(IQMBlendIndex[])(hdr.numVertices);
292                 istrm.setPosition(va.offset);
293                 istrm.fillArray(bi);
294                 blendIndices.append(bi);
295                 Delete(bi);
296             }
297             else if (va.type == IQM_BLENDWEIGHTS)
298             {
299                 assert(va.size == 4);
300                 // TODO: format asserion
301                 auto bw = New!(IQMBlendWeight[])(hdr.numVertices);
302                 istrm.setPosition(va.offset);
303                 istrm.fillArray(bw);
304                 blendWeights.append(bw);
305                 Delete(bw);
306             }
307         }
308 
309         version(IQMDebug)
310         {
311             writefln("numVertices: %s", vertices.length);
312             writefln("numNormals: %s", normals.length);
313             writefln("numTexcoords: %s", texcoords.length);
314 
315             writefln("hdr.numTriangles: %s", hdr.numTriangles);
316             writefln("hdr.ofsTriangles: %s", hdr.ofsTriangles);
317         }
318 
319         tris = New!(IQMTriangle[])(hdr.numTriangles);
320         istrm.setPosition(hdr.ofsTriangles);
321         foreach(i; 0..hdr.numTriangles)
322         {
323             tris[i] = istrm.read!IQMTriangle;
324             uint tmp = tris[i][0];
325             tris[i][0] = tris[i][2];
326             tris[i][2] = tmp;
327         }
328 
329         version(IQMDebug)
330             writefln("hdr.ofsAdjacency: %s", hdr.ofsAdjacency);
331 
332         // Skeleton part
333         version(IQMDebug)
334         {
335             writefln("hdr.numJoints: %s", hdr.numJoints);
336             writefln("hdr.ofsJoints: %s", hdr.ofsJoints);
337         }
338 
339         baseFrame = New!(Matrix4x4f[])(hdr.numJoints);
340         invBaseFrame = New!(Matrix4x4f[])(hdr.numJoints);
341         istrm.setPosition(hdr.ofsJoints);
342         foreach(i; 0..hdr.numJoints)
343         {
344             IQMJoint j = istrm.read!(IQMJoint, true);
345 
346             j.rotation.normalize();
347             baseFrame[i] = transformationMatrix(j.rotation, j.translation, j.scaling);
348             invBaseFrame[i] = baseFrame[i].inverse;
349 
350             if (j.parent >= 0)
351             {
352                 baseFrame[i] = baseFrame[j.parent] * baseFrame[i];
353                 invBaseFrame[i] = invBaseFrame[i] * invBaseFrame[j.parent];
354             }
355 
356             assert(validMatrix(baseFrame[i]));
357             assert(validMatrix(invBaseFrame[i]));
358 
359             assert(baseFrame[i].isAffine);
360             assert(invBaseFrame[i].isAffine);
361 
362             joints.append(j);
363         }
364 
365         // Meshes part
366         version(IQMDebug)
367         {
368             writefln("hdr.numMeshes: %s", hdr.numMeshes);
369             writefln("hdr.ofsMeshes: %s", hdr.ofsMeshes);
370         }
371         meshes = New!(IQMMesh[])(hdr.numMeshes);
372 
373         facegroups = New!(AnimationFacegroup[])(meshes.length);
374 
375         istrm.setPosition(hdr.ofsMeshes);
376         foreach(i; 0..hdr.numMeshes)
377         {
378             meshes[i] = istrm.read!(IQMMesh, true);
379 
380             // Load texture
381             uint matIndex = meshes[i].material;
382             version(IQMDebug)
383                 writefln("matIndex: %s", matIndex);
384                 
385             if (matIndex > 0)
386             {
387                 char* texFilenamePtr = cast(char*)&textBuffer[matIndex];
388                 string texFilename = cast(string)fromStringz(texFilenamePtr);
389                 version(IQMDebug)
390                     writefln("material: %s", texFilename);
391 
392                 facegroups[i].firstTriangle = meshes[i].firstTriangle;
393                 facegroups[i].numTriangles = meshes[i].numTriangles;
394                 facegroups[i].textureName = texFilename;
395 
396                 if (!mngr.assetExists(texFilename))
397                 {
398                     auto texAsset = New!TextureAsset(mngr.imageFactory, mngr.hdrImageFactory, mngr);
399                     mngr.addAsset(texAsset, texFilename);
400                     texAsset.threadSafePartLoaded = mngr.loadAssetThreadSafePart(texAsset, texFilename);
401                     facegroups[i].texture = texAsset.texture;
402                 }
403                 else
404                     facegroups[i].texture = (cast(TextureAsset)mngr.getAsset(texFilename)).texture;
405             }
406         }
407     
408         // Animation part
409     
410         // Number of poses should be the same as bindpose joints
411         version(IQMDebug)
412             writefln("hdr.numPoses: %s", hdr.numPoses);
413         assert(hdr.numPoses == hdr.numJoints);
414 
415         version(IQMDebug)
416         {
417             writefln("hdr.numFrames: %s", hdr.numFrames);
418             writefln("hdr.numFrameChannels: %s", hdr.numFrameChannels);
419         }
420         
421         // Read poses
422         istrm.setPosition(hdr.ofsPoses);
423         IQMPose[] poses = New!(IQMPose[])(hdr.numPoses);
424         foreach(i; 0..hdr.numPoses)
425         {
426             poses[i] = istrm.read!(IQMPose, true);
427         }
428 
429         // Read frames
430         numFrames = hdr.numFrames;
431         frames = New!(Matrix4x4f[])(hdr.numFrames * hdr.numPoses);
432         istrm.setPosition(hdr.ofsFrames);
433         uint fi = 0;
434         foreach(i; 0..hdr.numFrames)
435         foreach(j; 0..hdr.numPoses)
436         {
437             auto p = &poses[j];
438             
439             Vector3f trans, scale;
440             Quaternionf rot;
441             trans.x = p.channelOffset[0]; if (p.mask & 0x01) trans.x += istrm.read!(ushort, true) * p.channelScale[0];
442             trans.y = p.channelOffset[1]; if (p.mask & 0x02) trans.y += istrm.read!(ushort, true) * p.channelScale[1];
443             trans.z = p.channelOffset[2]; if (p.mask & 0x04) trans.z += istrm.read!(ushort, true) * p.channelScale[2];
444             rot.x = p.channelOffset[3]; if(p.mask&0x08) rot.x += istrm.read!(ushort, true) * p.channelScale[3];
445             rot.y = p.channelOffset[4]; if(p.mask&0x10) rot.y += istrm.read!(ushort, true) * p.channelScale[4];
446             rot.z = p.channelOffset[5]; if(p.mask&0x20) rot.z += istrm.read!(ushort, true) * p.channelScale[5];
447             rot.w = p.channelOffset[6]; if(p.mask&0x40) rot.w += istrm.read!(ushort, true) * p.channelScale[6];
448             scale.x = p.channelOffset[7]; if(p.mask&0x80) scale.x += istrm.read!(ushort, true) * p.channelScale[7];
449             scale.y = p.channelOffset[8]; if(p.mask&0x100) scale.y += istrm.read!(ushort, true) * p.channelScale[8];
450             scale.z = p.channelOffset[9]; if(p.mask&0x200) scale.z += istrm.read!(ushort, true) * p.channelScale[9];
451             
452             rot.normalize();            
453             Matrix4x4f m = transformationMatrix(rot, trans, scale);
454             assert(validMatrix(m));
455             
456             // Concatenate each pose with the inverse base pose to avoid doing this at animation time.
457             // If the joint has a parent, then it needs to be pre-concatenated with its parent's base pose.
458             if (p.parent >= 0)
459                 frames[i * hdr.numPoses + j] = 
460                     baseFrame[p.parent] * m * invBaseFrame[j];
461             else 
462                 frames[i * hdr.numPoses + j] = m * invBaseFrame[j];
463         }
464     
465         // Read animations
466         animations = New!(Dict!(IQMAnim, string));
467         istrm.setPosition(hdr.ofsAnims);
468         foreach(i; 0..hdr.numAnims)
469         {
470             IQMAnim anim = istrm.read!(IQMAnim, true);
471         
472             char* namePtr = cast(char*)&textBuffer[anim.name];
473             string name = cast(string)fromStringz(namePtr);
474             version(IQMDebug)
475             {
476                 writefln("anim.name: %s", name);
477             }
478         
479             animations[name] = anim;
480             
481             version(IQMDebug)
482             {
483                 writefln("anim.firstFrame: %s", anim.firstFrame);
484                 writefln("anim.numFrames: %s", anim.numFrames);
485             }
486         }
487 
488         if (poses.length) Delete(poses);
489     }
490 
491     Vector3f[] getVertices()
492     {
493         return vertices.data;
494     }
495 
496     Vector3f[] getNormals()
497     {
498         return normals.data;
499     }
500 
501     Vector2f[] getTexcoords()
502     {
503         return texcoords.data;
504     }
505 
506     uint[3][] getTriangles()
507     {
508         return tris;
509     }
510 
511     size_t numBones()
512     {
513         return joints.length;
514     }
515 
516     AnimationFacegroup[] getFacegroups()
517     {
518         return facegroups;
519     }
520 
521     uint numAnimationFrames()
522     {
523         return numFrames;
524     }
525 
526     void calcBindPose(AnimationFrameData* data)
527     {
528         foreach(i, ref j; joints)
529         {
530             data.frame[i] = baseFrame[i] * invBaseFrame[i];
531         }
532 
533         foreach(i, v; vertices)
534         {
535             auto bi = blendIndices[i];
536             auto bw = blendWeights[i];
537 
538             float w = (cast(float)bw[0])/255.0f;
539             Matrix4x4f mat = multScalarAffine(data.frame[bi[0]], w);
540 
541             for (uint j = 1; j < 4 && bw[j] > 0.0; j++)
542             {
543                 w = (cast(float)bw[j])/255.0f;
544                 auto tmp = multScalarAffine(data.frame[bi[j]], w);
545                 mat = addMatrixAffine(mat, tmp);
546             }
547 
548             assert(validMatrix(mat));
549             assert(mat.isAffine);
550 
551             data.vertices[i] = vertices[i] * mat;
552             data.normals[i] = normals[i] * matrix4x4to3x3(mat);
553             data.normals[i].normalize();
554         }
555     }
556 
557     void calcFrame(
558         uint f1, 
559         uint f2, 
560         float t, 
561         AnimationFrameData* data)
562     {            
563         Matrix4x4f* mat1 = &frames[f1 * joints.length];
564         Matrix4x4f* mat2 = &frames[f2 * joints.length];
565         
566         // Interpolate between two frames
567         foreach(i, ref j; joints)
568         {
569             Matrix4x4f mat = mat1[i] * (1.0f - t) + mat2[i] * t;
570             if (j.parent >= 0)
571                 data.frame[i] = data.frame[j.parent] * mat;
572             else
573                 data.frame[i] = mat;
574         }
575         
576         // Update vertex data
577         foreach(i, v; vertices)
578         {
579             auto bi = blendIndices[i];
580             auto bw = blendWeights[i];
581 
582             float w = (cast(float)bw[0])/255.0f;
583             Matrix4x4f mat = multScalarAffine(data.frame[bi[0]], w);
584             
585             for (uint j = 1; j < 4 && bw[j] > 0.0; j++)
586             {
587                 w = (cast(float)bw[j])/255.0f;
588                 auto tmp = multScalarAffine(data.frame[bi[j]], w);
589                 mat = addMatrixAffine(mat, tmp);
590             }
591 
592             assert(validMatrix(mat));
593             assert(mat.isAffine);
594 
595             data.vertices[i] = vertices[i] * mat;
596             data.normals[i] = normals[i] * matrix4x4to3x3(mat);
597             data.normals[i].normalize();
598         }
599     }
600 
601     void blendFrame(
602         uint f1, 
603         uint f2, 
604         float t, 
605         AnimationFrameData* data,
606         float blendFactor)
607     {
608         Matrix4x4f* mat1 = &frames[f1 * joints.length];
609         Matrix4x4f* mat2 = &frames[f2 * joints.length];
610         
611         // Interpolate between two frames
612         foreach(i, ref j; joints)
613         {
614             Matrix4x4f mat = mat1[i] * (1.0f - t) + mat2[i] * t;
615             if (j.parent >= 0)
616                 data.frame[i] = data.frame[j.parent] * mat;
617             else
618                 data.frame[i] = mat;
619         }
620         
621         // Update vertex data
622         foreach(i, v; vertices)
623         {
624             auto bi = blendIndices[i];
625             auto bw = blendWeights[i];
626 
627             float w = (cast(float)bw[0])/255.0f;
628             Matrix4x4f mat = multScalarAffine(data.frame[bi[0]], w);
629             
630             for (uint j = 1; j < 4 && bw[j] > 0.0; j++)
631             {
632                 w = (cast(float)bw[j])/255.0f;
633                 auto tmp = multScalarAffine(data.frame[bi[j]], w);
634                 mat = addMatrixAffine(mat, tmp);
635             }
636 
637             assert(validMatrix(mat));
638             assert(mat.isAffine);
639 
640             data.vertices[i] = lerp(data.vertices[i], vertices[i] * mat, blendFactor);
641             data.normals[i] = lerp(data.normals[i], normals[i] * matrix4x4to3x3(mat), blendFactor);
642             //data.normals[i].normalize();
643         }
644     }
645 
646     bool getAnimation(string name, AnimationData* data)
647     {
648         if (!name.length)
649         {
650             data.firstFrame = 0;
651             data.numFrames = numFrames;
652             return true;
653         }
654 
655         if (!(name in animations))
656             return false;
657 
658         auto anim = animations[name];
659         data.firstFrame = anim.firstFrame;
660         data.numFrames = anim.numFrames;
661         data.framerate = anim.framerate;
662         return true;
663     }
664 }
665 
666 class IQMAsset: Asset
667 {
668     IQMModel model;
669 
670     this(Owner o)
671     {
672         super(o);
673     }
674 
675     ~this()
676     {
677         if (model)
678             Delete(model);
679     }
680 
681     override bool loadThreadSafePart(string filename, InputStream istrm, ReadOnlyFileSystem fs, AssetManager mngr)
682     {
683         model = New!IQMModel(istrm, fs, mngr);
684         return true;
685     }
686 
687     override bool loadThreadUnsafePart()
688     {
689         return true;
690     }
691 
692     override void release()
693     {
694         if (model)
695             model.release();
696     }
697 }
698 
699 bool fileExists(ReadOnlyFileSystem rofs, string filename)
700 {
701     FileStat stat;
702     return rofs.stat(filename, stat);
703 }
704 
705 Matrix4x4f transformationMatrix(Quaternionf r, Vector3f t, Vector3f s)
706 {
707     Matrix4x4f res = Matrix4x4f.identity;
708     Matrix3x3f rm = r.toMatrix3x3;
709     res.a11 = rm.a11 * s.x; res.a12 = rm.a12 * s.x; res.a13 = rm.a13 * s.x;
710     res.a21 = rm.a21 * s.y; res.a22 = rm.a22 * s.y; res.a23 = rm.a23 * s.y;
711     res.a31 = rm.a31 * s.z; res.a32 = rm.a32 * s.z; res.a33 = rm.a33 * s.z;
712     res.a14 = t.x;
713     res.a24 = t.y;
714     res.a34 = t.z;
715     return res;
716 }
717 
718 Matrix4x4f multScalarAffine(Matrix4x4f m, float s)
719 {
720     Matrix4x4f res = m;
721     res.a11 *= s; res.a12 *= s; res.a13 *= s;
722     res.a21 *= s; res.a22 *= s; res.a23 *= s;
723     res.a31 *= s; res.a32 *= s; res.a33 *= s;
724     res.a14 *= s;
725     res.a24 *= s;
726     res.a34 *= s;
727     return res;
728 }
729 
730 Matrix4x4f addMatrixAffine(Matrix4x4f m1, Matrix4x4f m2)
731 {
732     Matrix4x4f res = m1;
733     res.a11 += m2.a11; res.a12 += m2.a12; res.a13 += m2.a13;
734     res.a21 += m2.a21; res.a22 += m2.a22; res.a23 += m2.a23;
735     res.a31 += m2.a31; res.a32 += m2.a32; res.a33 += m2.a33;
736     res.a14 += m2.a14;
737     res.a24 += m2.a24;
738     res.a34 += m2.a34;
739     return res;
740 }
741 
742 bool validMatrix(T, size_t N)(Matrix!(T, N) m)
743 {
744     foreach (v; m.arrayof)
745         if (isNaN(v))
746             return false;
747     return true;
748 }
749 
750 bool validVector(T, size_t N)(Vector!(T, N) vec)
751 {
752     foreach (v; vec.arrayof)
753         if (isNaN(v))
754             return false;
755     return true;
756 }
757 
758