1 module re.ng.camera.cam3d;
2 
3 import re.ecs;
4 import re.time;
5 import re.math;
6 import re.gfx.raytypes;
7 import re.ng.camera.base;
8 import std.math;
9 static import raylib;
10 
11 /// represents a camera for a 3D scene
12 class SceneCamera3D : SceneCamera {
13     mixin Reflect;
14     private raylib.Camera3D _camera;
15     private ProjectionType _projection;
16 
17     /// the projection used for the camera
18     public enum ProjectionType {
19         Perspective,
20         Orthographic
21     }
22 
23     this() {
24         _camera = raylib.Camera3D();
25         // default settings
26         up = Vector3(0, 1, 0); // unit vector y+
27         fov = C_PI_4; // 45 deg
28         projection = ProjectionType.Perspective;
29     }
30 
31     /// gets the underlying camera object (used internally)
32     @property ref raylib.Camera3D camera() return  {
33         return _camera;
34     }
35 
36     /// gets the projection type
37     @property ProjectionType projection() {
38         return _projection;
39     }
40 
41     /// sets the projection type
42     @property ProjectionType projection(ProjectionType value) {
43         _projection = value;
44         switch (_projection) {
45         case ProjectionType.Perspective:
46             _camera.type = raylib.CameraType.CAMERA_PERSPECTIVE;
47             break;
48         case ProjectionType.Orthographic:
49             _camera.type = raylib.CameraType.CAMERA_ORTHOGRAPHIC;
50             break;
51         default:
52             assert(0);
53         }
54         return value;
55     }
56 
57     /// gets the Y-field-of-view in radians
58     @property float fov() {
59         return _camera.fovy * C_DEG2RAD;
60     }
61 
62     /// sets the Y-field-of-view in radians
63     @property float fov(float value) {
64         return _camera.fovy = value * C_RAD2DEG;
65     }
66 
67     /// gets the direction that is up relative to this camera
68     @property Vector3 up() {
69         return _camera.up;
70     }
71 
72     /// sets the direction that is up relative to this camera
73     @property Vector3 up(Vector3 value) {
74         return _camera.up = value;
75     }
76 
77     override void update() {
78         super.update();
79 
80         // copy entity to camera transform
81         _camera.position = entity.transform.position;
82 
83         // import std.stdio : writefln;
84         // writefln("cam pos: %s", _camera.position);
85 
86         // update raylib camera
87         raylib.UpdateCamera(&_camera);
88     }
89 
90     /// orient the camera in the direction of a point
91     public void look_at(Vector3 target) {
92         _camera.target = target;
93     }
94 
95     /// orient the camera in the direction of an entity
96     public void look_at(Entity entity) {
97         look_at(entity.position);
98     }
99 }
100 
101 abstract class CameraFollow3D : Component, Updatable {
102     mixin Reflect;
103     protected SceneCamera3D cam;
104     /// the target entity
105     public Entity target;
106     protected enum third_person_dist = 1.2f;
107     protected Vector2 _angle; // xz plane camera angle
108     protected float _target_dist;
109 
110     this(Entity target) {
111         this.target = target;
112     }
113 
114     override void setup() {
115         cam = entity.get_component!SceneCamera3D();
116         cam.look_at(target); // start by looking at the target
117 
118         auto to_target = target.position - entity.position;
119 
120         _target_dist = raymath.Vector3Length(to_target);
121         _angle = Vector2(atan2(to_target.x, to_target.z), // Camera angle in plane XZ (0 aligned with Z, move positive CCW)
122                 atan2(to_target.y,
123                     sqrt(to_target.x * to_target.x + to_target.z * to_target.z))); // // Camera angle in plane XY (0 aligned with X, move positive CW)
124     }
125 }
126 
127 /// controls a camera by making it orbit an entity
128 class CameraOrbit : CameraFollow3D {
129     mixin Reflect;
130     /// the orbit speed, in radians per second
131     public float speed;
132 
133     this(Entity target, float speed) {
134         super(target);
135         this.speed = speed;
136     }
137 
138     /// based on https://github.com/raysan5/raylib/blob/6fa6757a8bf90d4b2fd0ce82dace7c7223635efa/src/camera.h#L400
139     void update() {
140         _angle.x += speed * Time.delta_time; // camera orbit angle
141 
142         // camera distance clamp
143         if (_target_dist < third_person_dist)
144             _target_dist = third_person_dist;
145 
146         // update camera position with changes
147         auto npos_x = sin(_angle.x) * _target_dist * cos(_angle.y) + target.position.x;
148         auto npos_y = ((_angle.y <= 0.0f) ? 1 : -1) * sin(
149                 _angle.y) * _target_dist * sin(_angle.y) + target.position.y;
150         auto npos_z = cos(_angle.x) * _target_dist * cos(_angle.y) + target.position.z;
151         entity.position = Vector3(npos_x, npos_y, npos_z);
152     }
153 }
154 
155 /// third person look camera
156 class CameraThirdPerson : CameraFollow3D {
157     import re.input : Keys, Input;
158 
159     mixin Reflect;
160     public float move_sensitivity = 20;
161     public float look_sensitivity = 0.003;
162     protected enum third_person_min_clamp = 5;
163     protected enum third_person_max_clamp = -85;
164 
165     this(Entity target) {
166         super(target);
167     }
168 
169     // based on https://github.com/raysan5/raylib/blob/6fa6757a8bf90d4b2fd0ce82dace7c7223635efa/src/camera.h#L458
170     void update() {
171         // bool direction[6] = {
172         //     IsKeyDown(CAMERA.moveControl[MOVE_FRONT]), IsKeyDown(CAMERA.moveControl[MOVE_BACK]),
173         //         IsKeyDown(CAMERA.moveControl[MOVE_RIGHT]), IsKeyDown(CAMERA.moveControl[MOVE_LEFT]),
174         //         IsKeyDown(CAMERA.moveControl[MOVE_UP]), IsKeyDown(CAMERA.moveControl[MOVE_DOWN])
175         // };
176         // bool[6] direction = [false, false, false, false, false, false];
177         // bool[6] direction = [
178         //     Input.is_key_down(Keys.KEY_W), Input.is_key_down(Keys.KEY_S),
179         //     Input.is_key_down(Keys.KEY_D), Input.is_key_down(Keys.KEY_A),
180         //     Input.is_key_down(Keys.KEY_E), Input.is_key_down(Keys.KEY_Q)
181         // ];
182         // enum MOVE_FRONT = 0;
183         // enum MOVE_BACK = 1;
184         // enum MOVE_RIGHT = 2;
185         // enum MOVE_LEFT = 3;
186         // enum MOVE_UP = 4;
187         // enum MOVE_DOWN = 5;
188 
189         // auto dpos_x = (sin(_angle.x) * direction[MOVE_BACK] - sin(
190         //         _angle.x) * direction[MOVE_FRONT] - cos(
191         //         _angle.x) * direction[MOVE_LEFT] + cos(_angle.x) * direction[MOVE_RIGHT]) / move_sensitivity;
192         // auto npos_x = transform.position.x + dpos_x;
193 
194         // auto dpos_y = (sin(_angle.y) * direction[MOVE_FRONT] - sin(
195         //         _angle.y) * direction[MOVE_BACK] + 1.0f * direction[MOVE_UP]
196         //         - 1.0f * direction[MOVE_DOWN]) / move_sensitivity;
197         // auto npos_y = transform.position.y + dpos_y;
198 
199         // auto dpos_z = (cos(_angle.x) * direction[MOVE_BACK] - cos(
200         //         _angle.x) * direction[MOVE_FRONT] + sin(
201         //         _angle.x) * direction[MOVE_LEFT] - sin(_angle.x) * direction[MOVE_RIGHT]) / move_sensitivity;
202         // auto npos_z = transform.position.z + dpos_z;
203 
204         // transform.position = Vector3(npos_x, npos_y, npos_z);
205 
206         auto npos_x = transform.position.x;
207         auto npos_y = transform.position.y;
208         auto npos_z = transform.position.z;
209 
210         // CAMDATA orientation calculation
211         _angle.x = _angle.x + (Input.mouse_delta.x * -look_sensitivity);
212         _angle.y = _angle.y + (Input.mouse_delta.y * -look_sensitivity);
213 
214         // Angle clamp
215         if (_angle.y > third_person_min_clamp * C_DEG2RAD)
216             _angle.y = third_person_min_clamp * C_DEG2RAD;
217         else if (_angle.y < third_person_max_clamp * C_DEG2RAD)
218             _angle.y = third_person_max_clamp * C_DEG2RAD;
219 
220         // CAMDATA zoom
221         // _target_dist -= (wheel_delta * CAMERA_MOUSE_SCROLL_SENSITIVITY);
222 
223         // CAMDATA distance clamp
224         if (_target_dist < third_person_dist)
225             _target_dist = third_person_dist;
226 
227         // TODO: It seems CAMDATA.position is not correctly updated or some rounding issue makes the CAMDATA move straight to target.transform.position...
228         npos_x = sin(_angle.x) * _target_dist * cos(_angle.y) + target.transform.position.x;
229 
230         if (_angle.y <= 0.0f)
231             npos_y = sin(_angle.y) * _target_dist * sin(_angle.y) + target.transform.position.y;
232         else
233             npos_y = -sin(_angle.y) * _target_dist * sin(_angle.y) + target.transform.position.y;
234 
235         npos_z = cos(_angle.x) * _target_dist * cos(_angle.y) + target.transform.position.z;
236 
237         transform.position = Vector3(npos_x, npos_y, npos_z);
238     }
239 }
240 
241 /// free-look camera
242 class CameraFreeLook : CameraFollow3D {
243     import re.input : Keys, MouseButton, Input;
244 
245     mixin Reflect;
246     // public float move_sensitivity = ;
247     public float look_sensitivity = 0.01;
248     public float zoom_sensitivity = 1.5;
249     public float smooth_zoom_sensitivity = 0.05;
250     protected enum free_min_clamp = 85;
251     protected enum free_max_clamp = -85;
252     protected enum free_dist_min_clamp = 0.3;
253     protected enum free_dist_max_clamp = 120;
254     protected enum free_pan_divider = 5.1;
255 
256     this(Entity target) {
257         super(target);
258     }
259 
260     // based on https://github.com/raysan5/raylib/blob/6fa6757a8bf90d4b2fd0ce82dace7c7223635efa/src/camera.h#L314
261     void update() {
262         auto npos_x = transform.position.x;
263         auto npos_y = transform.position.y;
264         auto npos_z = transform.position.z;
265 
266         auto wheel_delta = Input.scroll_delta();
267         auto mouse_delta = Input.mouse_delta;
268 
269         auto key_pan = Input.is_mouse_down(MouseButton.MOUSE_RIGHT_BUTTON) || Input.is_key_down(Keys.KEY_LEFT_ALT);
270         auto key_alternate = !Input.is_key_down(Keys.KEY_LEFT_SHIFT);
271         auto key_smooth = Input.is_key_down(Keys.KEY_LEFT_CONTROL);
272 
273         auto tpos_x = cam.camera.target.x;
274         auto tpos_y = cam.camera.target.y;
275         auto tpos_z = cam.camera.target.z;
276 
277         // Camera zoom
278         if ((_target_dist < free_dist_max_clamp) && (wheel_delta < 0)) {
279             _target_dist -= (wheel_delta * zoom_sensitivity);
280             if (_target_dist > free_dist_max_clamp)
281                 _target_dist = free_dist_max_clamp;
282         }  // Camera looking down
283         else if ((entity.position.y > tpos_y) && (_target_dist == free_dist_max_clamp)
284                 && (wheel_delta < 0)) {
285             tpos_x += wheel_delta * (tpos_x - entity.position.x) * zoom_sensitivity / _target_dist;
286             tpos_y += wheel_delta * (tpos_y - entity.position.y) * zoom_sensitivity / _target_dist;
287             tpos_z += wheel_delta * (tpos_z - entity.position.z) * zoom_sensitivity / _target_dist;
288         } else if ((entity.position.y > tpos_y) && (tpos_y >= 0)) {
289             tpos_x += wheel_delta * (tpos_x - entity.position.x) * zoom_sensitivity / _target_dist;
290             tpos_y += wheel_delta * (tpos_y - entity.position.y) * zoom_sensitivity / _target_dist;
291             tpos_z += wheel_delta * (tpos_z - entity.position.z) * zoom_sensitivity / _target_dist;
292 
293             // if (tpos_y < 0) tpos_y = -0.001;
294         } else if ((entity.position.y > tpos_y) && (tpos_y < 0) && (wheel_delta > 0)) {
295             _target_dist -= (wheel_delta * zoom_sensitivity);
296             if (_target_dist < free_dist_min_clamp)
297                 _target_dist = free_dist_min_clamp;
298         }  // Camera looking up
299         else if ((entity.position.y < tpos_y) && (_target_dist == free_dist_max_clamp)
300                 && (wheel_delta < 0)) {
301             tpos_x += wheel_delta * (tpos_x - entity.position.x) * zoom_sensitivity / _target_dist;
302             tpos_y += wheel_delta * (tpos_y - entity.position.y) * zoom_sensitivity / _target_dist;
303             tpos_z += wheel_delta * (tpos_z - entity.position.z) * zoom_sensitivity / _target_dist;
304         } else if ((entity.position.y < tpos_y) && (tpos_y <= 0)) {
305             tpos_x += wheel_delta * (tpos_x - entity.position.x) * zoom_sensitivity / _target_dist;
306             tpos_y += wheel_delta * (tpos_y - entity.position.y) * zoom_sensitivity / _target_dist;
307             tpos_z += wheel_delta * (tpos_z - entity.position.z) * zoom_sensitivity / _target_dist;
308 
309             // if (tpos_y > 0) tpos_y = 0.001;
310         } else if ((entity.position.y < tpos_y) && (tpos_y > 0) && (wheel_delta > 0)) {
311             _target_dist -= (wheel_delta * zoom_sensitivity);
312             if (_target_dist < free_dist_min_clamp)
313                 _target_dist = free_dist_min_clamp;
314         }
315 
316         // Input keys checks
317         if (key_pan) {
318             if (key_alternate) // Alternative key behaviour
319             {
320                 if (key_smooth) {
321                     // Camera smooth zoom
322                     _target_dist += (mouse_delta.y * smooth_zoom_sensitivity);
323                 } else {
324                     // Camera rotation
325                     _angle.x += mouse_delta.x * -look_sensitivity;
326                     _angle.y += mouse_delta.y * -look_sensitivity;
327 
328                     // Angle clamp
329                     if (_angle.y > free_min_clamp * C_DEG2RAD)
330                         _angle.y = free_min_clamp * C_DEG2RAD;
331                     else if (_angle.y < free_max_clamp * C_DEG2RAD)
332                         _angle.y = free_max_clamp * C_DEG2RAD;
333                 }
334             } else {
335                 // Camera panning
336                 tpos_x += ((mouse_delta.x * look_sensitivity) * cos(_angle.x) + (
337                         mouse_delta.y * look_sensitivity) * sin(_angle.x) * sin(_angle.y)) * (
338                         _target_dist / free_pan_divider);
339                 tpos_y += ((mouse_delta.y * look_sensitivity) * cos(_angle.y)) * (
340                         _target_dist / free_pan_divider);
341                 tpos_z += ((mouse_delta.x * -look_sensitivity) * sin(_angle.x) + (
342                         mouse_delta.y * look_sensitivity) * cos(_angle.x) * sin(_angle.y)) * (
343                         _target_dist / free_pan_divider);
344             }
345         }
346 
347         // Update camera position with changes
348         npos_x = -sin(_angle.x) * _target_dist * cos(_angle.y) + tpos_x;
349         npos_y = -sin(_angle.y) * _target_dist + tpos_y;
350         npos_z = -cos(_angle.x) * _target_dist * cos(_angle.y) + tpos_z;
351 
352         transform.position = Vector3(npos_x, npos_y, npos_z);
353         cam.camera.target = Vector3(tpos_x, tpos_y, tpos_z);
354     }
355 }