1 module isodi.raylib.resources.bone; 2 3 import raylib; 4 5 import std.meta; 6 import std.math; 7 import std.math : PI; 8 import std.string; 9 import std.random; 10 11 import isodi.pack; 12 import isodi.model; 13 import isodi.resource; 14 import isodi.raylib.model; 15 import isodi.raylib.internal; 16 17 18 @safe: 19 20 21 /// A bone resource. 22 struct Bone { 23 24 /// Enable debugging bone ends 25 private enum BoneDebug = false; 26 27 // Data 28 public { 29 30 /// Owner object. 31 RaylibModel model; 32 33 /// Skeleton node represented by this bone. 34 SkeletonNode node; 35 36 } 37 38 // Translation matrixes and related data for this bone. 39 public { 40 41 /// Matrix corresponding to the bone start. 42 Matrix boneStart; 43 44 /// Matrix corresponding to the bone end. 45 /// 46 /// Children will inherit their `boneStart` matrixes from this property. 47 Matrix boneEnd; 48 49 /// Local rotation of this bone, in radians. 50 Vector3 boneRotation; 51 52 /// Global rotation of the bone. 53 private Vector3 globalRotation; 54 55 } 56 57 // Other data 58 private { 59 60 /// If true, this node has a parent 61 bool hasParent; 62 63 /// Original scale of the texture. 64 float originalScale; 65 66 /// Scale to be applied to textures. 67 float scale; 68 69 /// Width of a single angle on the texture atlas. 70 uint atlasWidth; 71 72 /// Texture of the bone. 73 Texture2D texture; 74 75 /// Options of the resource. 76 const(ResourceOptions)* options; 77 78 } 79 80 /// Create the bone and load resources. 81 this(RaylibModel model, SkeletonNode node, Pack.Resource!string resource) { 82 83 // Set parameters 84 this.model = model; 85 this.node = node; 86 87 // Ignore the rest if not displaying 88 if (node.hidden) return; 89 90 // Load the texture 91 this.texture = model.display.loadTexture(resource.match); 92 this.options = resource.options; 93 94 // Get the scale 95 this.scale = cast(float) model.display.cellSize / options.tileSize; 96 this.originalScale = this.scale; 97 this.atlasWidth = texture.width / options.angles; 98 99 // Check if this node has a parent 100 this.hasParent = cast(bool) model.bones.length; 101 102 } 103 104 @property { 105 106 /// Scale applied to this bone. 107 float boneScale() const { 108 109 return scale / originalScale; 110 111 } 112 113 /// Ditto 114 float boneScale(float value) { 115 116 return scale = originalScale * value; 117 118 } 119 120 } 121 122 /// 123 void draw() const @trusted { 124 125 // Ignore if not displaying 126 if (node.hidden) return; 127 128 rlPushMatrix(); 129 scope (exit) rlPopMatrix(); 130 131 import std.conv : to; 132 133 // Get the current atlas frame 134 const rotationX = 360 - model.display.camera.angle.x; 135 const frameDelimiter = 360.0 / options.angles; 136 const atlasFrame = to!uint(rotationX / frameDelimiter + 0.5 + 1e-7) % options.angles; 137 138 /// Get the matrix 139 auto matrixf = localMatrix(atlasFrame).MatrixToFloat; 140 141 // Apply the matrix 142 rlMultMatrixf(&matrixf[0]); 143 144 // Scale appropriately 145 rlScalef(scale, scale, scale); 146 147 // Snap to frame 148 frameSnap(atlasFrame, frameDelimiter); 149 150 // Push a matrix if debugging bones 151 static if (BoneDebug) rlPushMatrix(); 152 153 // Translate the texture 154 rlTranslatef( 155 node.texturePosition[0], 156 node.texturePosition[1] - texture.height, 157 node.texturePosition[2] + 1, 158 ); 159 160 // Check for mirroring 161 const textureFrame = node.mirror ? atlasFrame : options.angles - atlasFrame; 162 163 // Draw the texture 164 texture.DrawTextureRec( 165 Rectangle( 166 atlasWidth * textureFrame, 0, 167 -mirrorScale * cast(int) atlasWidth, -texture.height 168 ), 169 Vector2(), 170 Colors.WHITE 171 ); 172 173 // Draw debug points 174 static if (BoneDebug) { 175 176 // Draw texture debug 177 DrawCircle3D( 178 Vector3(0, 0, -1), 0.2, 179 Vector3(), 1, 180 Colors.BLUE 181 ); 182 183 // Remove texture transform 184 rlPopMatrix(); 185 186 // Draw node debug 187 DrawCircle3D( 188 Vector3(0, 0, 0), 0.4, 189 Vector3(), 1, 190 Colors.GREEN 191 ); 192 193 } 194 195 } 196 197 /// Get local matrix for bone start. 198 /// 199 /// Params: 200 /// atlasFrame = Current atlas frame of the texture. 201 private Matrix localMatrix(float atlasFrame) const @trusted { 202 203 immutable rad = std.math.PI / 180; 204 205 const camAngle = model.display.camera.angle; 206 return mult( 207 208 // Move the bone to its position in the model 209 boneStart, 210 211 // Negate camera vertical rotation 212 MatrixRotateY(camAngle.x * rad), 213 MatrixRotateX(camAngle.y * rad), 214 MatrixRotateY(-camAngle.x * rad), 215 216 // Move to the tile 217 MatrixTranslate( 218 model.visualPosition.toTuple3(model.display.cellSize, CellPoint.center).expand 219 ) 220 221 ); 222 223 } 224 225 /// Returns -1 if mirroring, -1 if not. 226 private int mirrorScale() const { 227 228 return node.mirror ? -1 : 1; 229 230 } 231 232 private void frameSnap(float atlasFrame, float frameDelimiter) const @trusted { 233 234 // Note: still requires more testing, especially for models with more than 4 angles 235 236 const snapAngle = cast(int) atlasFrame * frameDelimiter; 237 238 // Rounding for floating point precision 239 const piAbove = PI_2 + 1e-6; 240 const piBelow = PI_2 - 1e-6; 241 242 // Bone is above 90° 243 if (globalRotation.x >= piAbove || globalRotation.z > piAbove) { 244 245 rlRotatef(snapAngle, 0, 1, 0); 246 247 } 248 249 // Bone is exactly on 90° 250 else if (globalRotation.x >= piBelow || globalRotation.z >= piBelow) { 251 252 // TODO, both cases would probably be different 253 254 } 255 256 // Bone is below 257 else { 258 259 rlRotatef(180 - snapAngle, 0, 1, 0); 260 261 } 262 263 } 264 265 /// Calculate matrixes for this node. 266 void updateMatrixes() @trusted { 267 268 // If there is a parent 269 if (hasParent) { 270 271 // Inherit start from parent 272 const parent = model.bones[node.parent]; 273 boneStart = parent.boneEnd; 274 globalRotation = parent.globalRotation; 275 276 } 277 278 // For the root, create an identity matrix 279 else boneStart = MatrixIdentity; 280 281 /// Prepare a scale matrix based on bone position 282 Matrix boneTranslate(float[3] array) const { 283 284 return MatrixTranslate( 285 array[0] * scale, 286 array[1] * scale, 287 array[2] * scale, 288 ); 289 290 } 291 292 const PI2 = PI * 2; 293 294 // Calculate points 295 boneStart = mult( 296 MatrixRotateXYZ(boneRotation), 297 boneTranslate(node.boneStart), 298 boneStart, 299 ); 300 boneEnd = mult( 301 boneTranslate(node.boneEnd), 302 boneStart, 303 ); 304 globalRotation = Vector3( 305 (globalRotation.x + boneRotation.x) % PI2, 306 (globalRotation.y + boneRotation.y) % PI2, 307 (globalRotation.z + boneRotation.z) % PI2, 308 ); 309 310 } 311 312 } 313 314 /// Multiply matrixes. 315 private Matrix mult(Matrix[] matrixes...) @trusted { 316 317 auto result = MatrixIdentity; 318 foreach (matrix; matrixes) { 319 320 result = MatrixMultiply(result, matrix); 321 322 } 323 324 return result; 325 326 }