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 }