1 module isodi.raylib.model;
2 
3 import raylib;
4 
5 import std.math;
6 import std.array;
7 import std.typecons;
8 import std.algorithm;
9 
10 import isodi.model : Model;
11 import isodi.display;
12 import isodi.resource;
13 import isodi.raylib.internal;
14 import isodi.raylib.resources.bone;
15 
16 
17 @safe:
18 
19 
20 /// `Model` implementation for Raylib.
21 final class RaylibModel : Model, WithDrawableResources {
22 
23     package {
24 
25         Bone*[] bones;
26         Bone*[string] bonesID;
27 
28     }
29 
30     ///
31     this(Display display, const string type) {
32 
33         super(display, type);
34         reload();
35 
36     }
37 
38     override void changeVariant(string id, string variant) {
39 
40         //auto bone = bonesID[id].changeVariant(variant);
41         assert(0, "unimplemented");
42 
43     }
44 
45     ///
46     void reload() {
47 
48         // Clear the array first
49         bones = [];
50 
51         // Get the bones
52         auto skeleton = display.packs.getSkeleton(type);
53         foreach (node; skeleton.match) {
54 
55             // Create a bone
56             auto bone = new Bone(this, node, getBone(node));
57             bones ~= bone;
58 
59             // Save by ID
60             assert(bone.node.id !in bonesID);
61             bonesID[bone.node.id] = bone;
62 
63         }
64 
65     }
66 
67     ///
68     void draw() @trusted {
69 
70         const rad = display.camera.angle.x * std.math.PI / 180;
71 
72         runAnimations();
73 
74         rlPushMatrix();
75         scope(exit) rlPopMatrix();
76 
77         // Sort the bones
78         bones.map!((a) => cameraDistance(a, rad))
79             .array
80             .sort!((a, b) => a[1] > b[1])
81 
82             // Draw them
83             .each!(a => a[0].draw());
84 
85     }
86 
87     private void runAnimations() {
88 
89         foreach_reverse (i, ref animation; animations) {
90 
91             const previousFrame = animation.frame;
92             const delta = (() @trusted => animation.fps * GetFrameTime)();
93 
94             // Increment frame time
95             animation.frame += delta;
96 
97             // Run the animation
98             while (true) {
99 
100                 // Get the next part
101                 const part = animation.parts[animation.current];
102 
103                 // Frame delta / Frames until the end. Used to calculate changes in animated values
104                 const progress = min(1, delta / (part.length - previousFrame));
105 
106                 // Run the animation
107                 runAnimationPart(part, progress);
108 
109                 // Stop if the part didn't end
110                 if (progress != 1) break;
111 
112                 // Decrease frame by part length
113                 animation.frame -= part.length;
114 
115                 // Advance to the next part
116                 if (animation.advance == Yes.ended) {
117 
118                     // The animation ended, remove it
119                     animations = animations.remove(i);
120 
121                     // Continue to the next animation
122                     break;
123 
124                 }
125 
126                 // Break if this is an infinite loop
127                 else if (animation.times == 0) break;
128 
129             }
130 
131         }
132 
133     }
134 
135     /// Run the current animation part.
136     private void runAnimationPart(const AnimationPart part, float progress) {
137 
138         // TODO: offset
139 
140         // Check the bones
141         foreach (bone, target; part.bone) {
142 
143             // Get the node for this bone
144             auto node = bonesID.get(bone, null);
145 
146             // Ignore if the node doesn't exist
147             // This is because models and animations might be provided by different packs and different models may
148             // have different complexity level — less complex models should still work with animations designed for
149             // advanced ones, assuming that basic naming is kept the same.
150             // Detecting mispellings and unknown bones is the job of a resource editor.
151             if (node is null) continue;
152 
153             // Changing rotation
154             {
155 
156                 import std.range : enumerate;
157 
158                 // Tween each value
159                 auto newValues = [node.boneRotation.tupleof]
160                     .enumerate
161                     .map!((item) {
162 
163                         const targetRad = target.rotate[item.index] * std.math.PI / 180;
164 
165                         return tweenAngle(progress, item.value, targetRad);
166 
167                     });
168 
169                 // Assign it
170                 node.boneRotation = Vector3(newValues[0], newValues[1], newValues[2]);
171 
172             }
173 
174             // Changing scale
175             if (target.scale != 0) {
176 
177                 node.boneScale = tween(progress, node.boneScale, target.scale);
178 
179             }
180 
181         }
182 
183     }
184 
185     /// Animate the given rotation property.
186     private T tweenAngle(T)(float progress, T currentValue, T target) {
187 
188         // TODO: make this consider wrapping
189         return currentValue + progress * (target - currentValue);
190 
191     }
192 
193     /// Animate the given property.
194     private T tween(T)(float progress, T currentValue, T target) {
195 
196         return currentValue + progress * (target - currentValue);
197 
198     }
199 
200     private Tuple!(Bone*, float) cameraDistance(Bone* bone, real rad) {
201 
202         // Get new matrixes
203         bone.updateMatrixes();
204 
205         const vec = (() @trusted => Vector3Transform(Vector3Zero, bone.boneStart))();
206 
207         return Tuple!(Bone*, float)(
208             bone,
209             -vec.x * sin(rad)
210               - vec.z * cos(rad),
211         );
212 
213     }
214 
215 }