1 /// Optional helper utility for managing the camera.
2 module isodi.camera;
3 
4 import raylib;
5 
6 import std.conv;
7 import std.math;
8 import std.meta;
9 import std.traits;
10 
11 private alias PI = std.math.PI;
12 
13 
14 @safe:
15 
16 
17 // Helper UDAs for metaprogramming
18 private {
19 
20     struct Affects(string what) {
21         static immutable name = what;
22     }
23     struct Change(short by) {
24         static immutable value = by;
25     }
26     enum Speed;
27 
28 }
29 
30 /// Keys to connect to camera actions.
31 struct CameraKeybindings {
32 
33     @Affects!"distance" {
34 
35         /// Zoom the camera in.
36         @Change!(-1)
37         KeyboardKey zoomIn;
38 
39         /// Zoom the camera out
40         @Change!(+1)
41         KeyboardKey zoomOut;
42 
43     }
44 
45     @Affects!"yaw" {
46 
47         /// Rotate the camera around the Y axis clockwise.
48         @Change!(-1)
49         KeyboardKey rotateRight;
50 
51         /// Rotate the camera around the Y axis counter-clockwise.
52         @Change!(+1)
53         KeyboardKey rotateLeft;
54 
55     }
56 
57     @Affects!"pitch" {
58 
59         /// Pitch the camera down.
60         @Change!(-1)
61         KeyboardKey rotateDown;
62 
63         /// Pitch the camera up.
64         @Change!(+1)
65         KeyboardKey rotateUp;
66 
67     }
68 
69     @Affects!"offsetScreenX" {
70 
71         /// Move the camera to the left.
72         @Change!(-1)
73         KeyboardKey moveLeft;
74 
75         /// Move the camera to the right.
76         @Change!(+1)
77         KeyboardKey moveRight;
78 
79     }
80 
81     @Affects!"offsetScreenZ" {
82 
83         /// Move the camera backward.
84         @Change!(+1)
85         KeyboardKey moveBackward;
86 
87         /// Move the camera forward.
88         @Change!(-1)
89         KeyboardKey moveForward;
90 
91     }
92 
93     @Affects!"offset.y" {
94 
95         /// Move the camera downwards.
96         @Change!(-1)
97         KeyboardKey moveDown;
98 
99         /// Move the camera upwards.
100         @Change!(+1)
101         KeyboardKey moveUp;
102 
103     }
104 
105     @Speed {
106 
107         /// Zoom speed, cell per second.
108         @Affects!"distance"
109         float zoomSpeed = 15;
110 
111         /// Rotation speed, radians per second.
112         @Affects!"yaw"
113         @Affects!"pitch"
114         float rotateSpeed = PI_2;
115 
116         /// Movement speed, cells per second.
117         @Affects!"offsetScreenX"
118         @Affects!"offsetScreenZ"
119         @Affects!"offset.y"
120         float movementSpeed = 8;
121 
122     }
123 
124     /// Maximum zoom in.
125     float zoomInLimit = 5;
126 
127     /// Maximum zoom out. This should be greater than `zoomInLimit`.
128     float zoomOutLimit = 50;
129 
130 }
131 
132 struct CameraController {
133 
134     /// Camera yaw.
135     float yaw = PI_4;
136 
137     /// Camera pitch.
138     ///
139     /// Must be between 0° (side view) and 90° (top-down), defaults to 45°
140     float pitch = PI_4;
141 
142     /// Convience function to determine the camera's position.
143     ///
144     /// This can be null. If so, follows position (0, 0, 0).
145     Vector3 delegate() @safe follow;
146 
147     /// Distance between the camera and the followed object.
148     float distance = 15;
149 
150     /// Distance to FOV ratio, used to correct projection parameters.
151     float distanceFOVRatio = 5;
152 
153     /// Offset between camera and the followed object.
154     Vector3 offset;
155 
156     /// Change the offset relative to the screen
157     void offsetScreenX(float value) {
158 
159         offset.x += value * yaw.cos;
160         offset.z += value * yaw.sin;
161 
162     }
163 
164     /// Change the offset relative to the screen
165     void offsetScreenZ(float value) {
166 
167         offset.x += value * yaw.sin;
168         offset.z += value * yaw.cos;
169 
170     }
171 
172     /// Get input and update the camera.
173     void update(CameraKeybindings keybinds, ref Camera camera) {
174 
175         input(keybinds);
176         output(camera);
177 
178     }
179 
180     /// ditto
181     Camera update(CameraKeybindings keybinds) {
182 
183         Camera camera = {
184             up: Vector3(0, 1, 0),
185             projection: CameraProjection.CAMERA_ORTHOGRAPHIC,
186         };
187 
188         update(keybinds, camera);
189 
190         return camera;
191 
192     }
193 
194     /// Move the camera using bound keys.
195     void input(CameraKeybindings keybinds) @trusted {
196 
197         alias SpeedFields = getSymbolsByUDA!(CameraKeybindings, Speed);
198 
199         const delta = GetFrameTime();
200 
201         // Check each field
202         static foreach (actionName; FieldNameTuple!CameraKeybindings) {{
203 
204             alias BindField = mixin("CameraKeybindings." ~ actionName);
205 
206             // This is a key field
207             static if (hasUDA!(BindField, Change)) {
208 
209                 alias ChangeDeco = getUDAs!(BindField, Change)[0];
210                 enum direction = ChangeDeco.value;
211 
212                 // Pressed
213                 if (mixin("keybinds." ~ actionName).IsKeyDown) {
214 
215                     // Get the affected field
216                     alias AffectDeco = getUDAs!(BindField, Affects)[0];
217                     enum controllerField = "this." ~ AffectDeco.name;
218                     alias ControllerField = typeof(mixin(controllerField));
219 
220                     // Get the speed field for the deco
221                     alias HasAffectDeco = ApplyRight!(hasUDA, AffectDeco);
222                     alias speedSymbol = Filter!(HasAffectDeco, SpeedFields)[0];
223                     enum speedField = __traits(identifier, speedSymbol);
224 
225                     // Get the total change
226                     const change = direction * delta * mixin("keybinds." ~ speedField);
227 
228                     // Check if the given type is a field or property
229                     enum IsField(alias T) = !isFunction!T || functionAttributes!T & FunctionAttribute.property;
230 
231                     // If so
232                     static if (IsField!(mixin(controllerField))) {
233 
234                         import std.algorithm : clamp;
235 
236                         // Get the new value
237                         auto newValue = mixin(controllerField) + change;
238 
239                         // Clamp camera distance
240                         static if (AffectDeco.name == "distance") {
241 
242                             newValue = newValue.clamp(keybinds.zoomInLimit, keybinds.zoomOutLimit);
243 
244                         }
245 
246                         // Clamp pitch
247                         else static if (AffectDeco.name == "pitch") {
248 
249                             newValue = newValue.clamp(0, PI_2 - 2e-6);
250 
251                         }
252 
253                         // Update the field
254                         mixin(controllerField) = newValue;
255 
256                     }
257 
258                     // Call the function otherwise
259                     else mixin(controllerField)(change);
260 
261                 }
262 
263             }
264 
265         }}
266 
267     }
268 
269     /// Update the camera based on controller data.
270     void output(ref Camera camera) const {
271 
272         // Calculate the target
273         const target = follow is null
274             ? Vector3()
275             : follow();
276 
277         // Update the camera
278         // not sure how to get the correct fovy from distance, this is just close to the expected result.
279         // TODO: figure it out.
280         camera.fovy = distance;
281 
282         // Get the target
283         camera.target = target + offset;
284 
285         // And place the camera
286         camera.position = camera.target + Vector3(
287             pitch.cos * yaw.sin,
288             pitch.sin,
289             pitch.cos * yaw.cos,
290         ) * (distance * distanceFOVRatio);
291 
292     }
293 
294 }