1 module isodi.skeleton;
2 
3 import raylib;
4 
5 import std.math;
6 import std.array;
7 import std.format;
8 import std.algorithm;
9 
10 import isodi.utils;
11 import isodi.properties;
12 import isodi.isodi_model;
13 
14 private alias PI = std.math.PI;
15 
16 
17 @safe:
18 
19 
20 /// Skeleton.
21 ///
22 /// TODO documentation
23 struct Skeleton {
24 
25     public {
26 
27         /// Properties for the skeleton.
28         Properties properties;
29 
30         /// Seed to use to generate variants on the skeleton.
31         ulong seed;
32 
33         /// Mapping of bone types to their positions in the texture.
34         BoneUV[BoneType] atlas;
35 
36         /// All bones in the skeleton.
37         ///
38         /// Note: A child bone should never come before its parent.
39         Bone[] bones;
40 
41     }
42 
43     /// Add a bone from the given bone set.
44     /// Params:
45     ///     type = Type of the bone.
46     ///     parentIndex = Index of the parent, if any.
47     ///     parent = Parent node, if any.
48     ///     matrix = Transform for the bone.
49     ///     vector = Vector the bone will align to.
50     Bone* addBone(BoneType type, size_t parentIndex, Matrix matrix, Vector3 vector) return {
51 
52         bones ~= Bone(bones.length, type, parentIndex, matrix, vector);
53         return &bones[$-1];
54 
55     }
56 
57     /// ditto
58     Bone* addBone(BoneType type, const Bone* parent, Matrix matrix, Vector3 vector) return
59         => addBone(type, parent.index, matrix, vector);
60 
61     /// ditto
62     Bone* addBone(BoneType type, Matrix matrix, Vector3 vector) return
63         => addBone(type, 0, matrix, vector);
64 
65     /// ditto
66     Bone* addBone(BoneType type, size_t parentIndex) @trusted return
67         => addBone(type, parentIndex, MatrixIdentity, Vector3(0, 1, 0));
68 
69     /// ditto
70     Bone* addBone(BoneType type, const Bone* parent) @trusted return
71         => addBone(type, parent.index, MatrixIdentity, Vector3(0, 1, 0));
72 
73     /// ditto
74     Bone* addBone(BoneType type) @trusted return
75         => addBone(type, MatrixIdentity, Vector3(0, 1, 0));
76 
77     /// Make a model out of the skeleton.
78     ///
79     /// Note: `texture` and `matrixImage` will not be automatically freed. This must be done manually.
80     IsodiModel makeModel(return Texture2D texture, return Texture2D matrixTexture = Texture2D.init) const @trusted
81     in (matrixTexture.height == 0 || matrixTexture.height == bones.length,
82         format!"Matrix texture height (%s) must match bone count (%s) or be 0"(matrixTexture.height, bones.length))
83     in (matrixTexture.height == 0 || matrixTexture.width == 4,  // note: width == 0 isn't valid if height != 0
84         format!"Matrix texture width (%s) must be 4"(matrixTexture.width))
85     do {
86 
87         const atlasSize = Vector2(texture.width, texture.height);
88 
89         // Model data
90         const vertexCount = bones.length * 4;
91         const triangleCount = bones.length * 2;
92 
93         // Create a model
94         IsodiModel model = {
95             properties: properties,
96             texture: texture,
97             matrixTexture: matrixTexture,
98             flatten: true,
99             showBackfaces: true,
100         };
101         model.vertices.reserve = vertexCount;
102         model.variants.length = vertexCount;
103         model.texcoords.reserve = vertexCount;
104         model.bones.length = matrixTexture.height * 4;
105         model.triangles.reserve = triangleCount;
106 
107         // Add each bone
108         foreach (i, const bone; bones) with (model) {
109 
110             const boneUV = bone.type in atlas;
111             assert(boneUV, format!"%s not present in skeleton atlas"(bone.type));
112             // TODO This shouldn't be an error; We should allow bones to be less detailed than expected by the
113             // skeletons.
114 
115             // Get the variant
116             auto boneVariant = boneUV.getBone(seed + i).toShader(atlasSize);
117             boneVariant.width /= 4;  // TODO use variant count instead
118 
119             // Get the size of the bone
120             Vector2 boneSize;
121             boneSize.y = Vector3Length(bone.vector);
122             boneSize.x = boneSize.y * boneVariant.width / boneVariant.height;
123 
124             bool invertX, invertY;
125 
126             Vector3 makeVertex(bool start, int sign, bool adjust = false) {
127 
128                 // Revert start and left, if needed, to make the bone face the right direction
129                 if (!adjust) {
130                     sign  *= invertX ? -1 : 1;
131                     start ^= invertY;
132                 }
133 
134                 /// Matrix to adjust the horizontal position of the vertex
135                 const translation = MatrixTranslate(sign * boneSize.x / 2, 0, 0);
136 
137                 /// Vector to use for transformation — one for start, one for end
138                 const vector = start
139                     ? Vector3()
140                     : bone.vector;
141 
142                 return vector.Vector3Transform(translation);
143 
144             }
145 
146             // Check inverts
147             {
148 
149                 const xstart = makeVertex(true, -1, true);
150                 const xend   = makeVertex(true, +1, true);
151 
152                 const ystart = makeVertex(true,  +1, true);
153                 const yend   = makeVertex(false, +1, true);
154 
155                 invertX = xstart.x > xend.x;
156                 invertY = ystart.y > yend.y;
157 
158             }
159 
160             // Vertices
161             vertices ~= [
162                 makeVertex(false, -1),
163                 makeVertex(false, +1),
164                 makeVertex(true,  +1),
165                 makeVertex(true,  -1),
166             ];
167 
168             // UVs
169             texcoords ~= [
170                 Vector2( invertX,  invertY),
171                 Vector2(!invertX,  invertY),
172                 Vector2(!invertX, !invertY),
173                 Vector2( invertX, !invertY),
174             ];
175 
176             // TODO verify this
177             const normalMatrix = MatrixMultiply(
178                 MatrixRotate(Vector3(-1, 0, 0), PI / 2),
179                 bone.transform,
180             );
181 
182             Vector2 anchor(bool start) {
183                 return Vector2(
184                     makeVertex(start, 0).x.round,
185                     makeVertex(start, 0).z.round,
186                 );
187             }
188 
189             // Variants
190             variants.assign(i, 4, boneVariant);
191 
192             // Bones
193             if (bones.length != 0) {
194                 bones.assign(i, 4, cast(float) i / this.bones.length);
195             }
196 
197             ushort[3] value(int[] offsets) => [
198                 cast(ushort) (i*4 + offsets[0]),
199                 cast(ushort) (i*4 + offsets[1]),
200                 cast(ushort) (i*4 + offsets[2]),
201             ];
202 
203             // Triangles
204             triangles ~= map!value([
205                 [0, 1, 2],
206                 [0, 2, 3],
207             ]).array;
208 
209         }
210 
211         // Upload the model
212         model.upload();
213 
214         return model;
215 
216     }
217 
218     /// Generate a matrix image for the skeleton.
219     ///
220     /// The resulting image will be freed by the GC. Do not use `UnloadImage` on it.
221     Image matrixImage() const
222     in (bones.length <= int.max, "There are too many bones to fit in an image")
223     do {
224 
225         auto data = matrixImageData;
226 
227         Image result = {
228             data: &data[0],
229             width: 4,
230             height: cast(int) data.length,
231             mipmaps: 1,
232             format: PixelFormat.PIXELFORMAT_UNCOMPRESSED_R32G32B32A32,
233         };
234 
235         return result;
236 
237     }
238 
239     /// Generate a matrix image data for the skeleton.
240     ///
241     /// Both buffer parameters are optional, but can be provided to prevent allocation on subsequent calls.
242     ///
243     /// Params:
244     ///     buffer = Buffer for use in the image. Filled with bone-start matrices.
245     ///     matrices = Array filled with bone-end matrices. Byproduct of the matrix image generation.
246     Vector4[4][] matrixImageData() const {
247 
248         // Create a buffer
249         auto result = new Vector4[4][bones.length];
250 
251         // Write data to it
252         matrixImageData(result);
253 
254         return result;
255 
256     }
257 
258     /// ditto
259     void matrixImageData(Vector4[4][] buffer) const
260     in (bones.length > 0, "Cannot generate image for an empty model")
261     in (buffer.length == bones.length, "Buffer height differs from bone count")
262     do {
263 
264         // Create a matrix array for the bones
265         auto matrices = new Matrix[bones.length];
266 
267         matrixImageData(buffer, matrices);
268 
269     }
270 
271     /// ditto
272     void matrixImageData(Vector4[4][] buffer, Matrix[] matrices) const
273     in (buffer.length == bones.length, "Buffer count differs from bone count")
274     in (matrices.length == bones.length, "Matrix count differs from bone count")
275     do {
276 
277         size_t i;
278 
279         // Convert the matrices
280         globalMatrices(matrices, (matrix) {
281 
282             // Write to the buffer with a column-first layout
283             buffer[i++] = [
284                 Vector4(matrix.m0,  matrix.m1,  matrix.m2,  matrix.m3),
285                 Vector4(matrix.m4,  matrix.m5,  matrix.m6,  matrix.m7),
286                 Vector4(matrix.m8,  matrix.m9,  matrix.m10, matrix.m11),
287                 Vector4(matrix.m12, matrix.m13, matrix.m14, matrix.m15),
288             ].staticArray;
289 
290         });
291 
292     }
293 
294     /// Fill the given array with matrices each transforming a zero vector to the end of corresponding bones, relative
295     /// to the model.
296     ///
297     /// Given delegate will be called, for each bone, with the current matrix but pointing to the *start* of the bone.
298     void globalMatrices(Matrix[] matrices, void delegate(Matrix) @safe startCb = null) const @trusted
299     in (matrices.length == bones.length, "Length of the given matrix buffer doesn't match bones count")
300     do {
301 
302         // Prepare matrices for each bone
303         foreach (i, bone; bones) {
304 
305             // Get the bone's relative transform
306             Matrix matrix = bone.transform;
307 
308             // If there's a parent
309             if (i) {
310 
311                 // Get the parent transform
312                 const parent = matrices[bone.parent];
313 
314                 // Inherit it
315                 matrix = MatrixMultiply(matrix, parent);
316 
317             }
318 
319             // Finally, transform the matrix to the bone's end
320             matrices[i] = MatrixMultiply(MatrixTranslate(bone.vector.tupleof), matrix);
321 
322             // Emit the matrix
323             if (startCb) startCb(matrix);
324 
325         }
326 
327     }
328 
329     /// Draw lines for each bone in the skeleton.
330     /// Params:
331     ///     buffer = Optional matrix buffer for the function to operate on. Can be specified to prevent memory
332     ///         allocation on subsequent calls to this function.
333     void drawBoneLines() const {
334 
335         auto buffer = new Matrix[bones.length];
336         drawBoneLines(buffer);
337 
338     }
339 
340     /// ditto
341     void drawBoneLines(Matrix[] buffer) const @trusted
342     in (buffer.length == bones.length, "Buffer length must equal bone count")
343     do {
344 
345         size_t i;
346 
347         globalMatrices(buffer, (matrix) @trusted {
348 
349             scope (success) i++;
350 
351             matrix = MatrixMultiply(matrix, properties.transform);
352 
353             const start = Vector3().Vector3Transform(matrix);
354             const end = bones[i].vector.Vector3Transform(matrix);
355 
356             DrawCylinderEx(start, end, 0.03, 0.03, 3, boneColor(i));
357 
358         });
359 
360     }
361 
362     /// Draw normals for each bone.
363     void drawBoneNormals(Matrix[] buffer) const @trusted
364     in (buffer.length == bones.length, "Buffer length must equal bone count")
365     do {
366 
367         size_t i;
368 
369         globalMatrices(buffer, (matrix) @trusted {
370 
371             scope (success) i++;
372 
373             matrix = mul(
374                 MatrixTranslate(Vector3Divide(bones[i].vector, Vector3(2, 2, 2)).tupleof),
375                 matrix,
376                 properties.transform,
377             );
378 
379             const start = Vector3().Vector3Transform(matrix);
380             const end = Vector3(0, 0, -0.1).Vector3Transform(matrix);
381 
382             DrawCylinderEx(start, end, 0.02, 0.02, 3, boneColor(i, 0.4));
383 
384         });
385 
386     }
387 
388     /// Get a representative color for the bone based on its index. Useful for debugging.
389     static Color boneColor(size_t i, float saturation = 0.6, float value = 0.9) @trusted
390 
391         => ColorFromHSV(i * 35 % 360, saturation, value);
392 
393 }
394 
395 struct BoneType {
396 
397     ulong typeID;
398 
399 }
400 
401 struct BoneUV {
402 
403     RectangleI boneAreas;
404 
405     // TODO support variants
406 
407     /// Get random bone variant within the UV.
408     RectangleI getBone(ulong seed) const => boneAreas;
409 
410 }
411 
412 struct Bone {
413 
414     const size_t index;
415     BoneType type;
416     size_t parent;
417     Matrix transform;
418     Vector3 vector;
419 
420 }
421 
422 /// Polyfill from Raylib master branch.
423 private float Vector3Angle(Vector3 v1, Vector3 v2) @nogc pure {
424 
425     float result = 0.0f;
426 
427     auto cross = Vector3(v1.y*v2.z - v1.z*v2.y, v1.z*v2.x - v1.x*v2.z, v1.x*v2.y - v1.y*v2.x);
428     float len = sqrt(cross.x*cross.x + cross.y*cross.y + cross.z*cross.z);
429     float dot = (v1.x*v2.x + v1.y*v2.y + v1.z*v2.z);
430     result = atan2(len, dot);
431 
432     return result;
433 
434 }