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 }