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.projection = raylib.CameraProjection.CAMERA_PERSPECTIVE;
47             break;
48         case ProjectionType.Orthographic:
49             _camera.projection = raylib.CameraProjection.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     public bool pause = false;
133 
134     this(Entity target, float speed) {
135         super(target);
136         this.speed = speed;
137     }
138 
139     public void set_xz_angle(float val) {
140         _angle.x = val;
141     }
142 
143     /// based on https://github.com/raysan5/raylib/blob/6fa6757a8bf90d4b2fd0ce82dace7c7223635efa/src/camera.h#L400
144     void update() {
145         if (!pause) {
146             _angle.x += speed * Time.delta_time; // camera xz orbit angle
147         }
148 
149         // camera distance clamp
150         if (_target_dist < third_person_dist)
151             _target_dist = third_person_dist;
152 
153         // update camera position with changes
154         auto npos_x = sin(_angle.x) * _target_dist * cos(_angle.y) + target.position.x;
155         auto npos_y = ((_angle.y <= 0.0f) ? 1 : -1) * sin(
156                 _angle.y) * _target_dist * sin(_angle.y) + target.position.y;
157         auto npos_z = cos(_angle.x) * _target_dist * cos(_angle.y) + target.position.z;
158         entity.position = Vector3(npos_x, npos_y, npos_z);
159     }
160 }
161 
162 /// third person look camera
163 class CameraThirdPerson : CameraFollow3D {
164     import re.input : Keys, Input;
165 
166     mixin Reflect;
167     public float move_sensitivity = 20;
168     public float look_sensitivity = 0.003;
169     protected enum third_person_min_clamp = 5;
170     protected enum third_person_max_clamp = -85;
171 
172     this(Entity target) {
173         super(target);
174     }
175 
176     // based on https://github.com/raysan5/raylib/blob/6fa6757a8bf90d4b2fd0ce82dace7c7223635efa/src/camera.h#L458
177     void update() {
178         // bool direction[6] = {
179         //     IsKeyDown(CAMERA.moveControl[MOVE_FRONT]), IsKeyDown(CAMERA.moveControl[MOVE_BACK]),
180         //         IsKeyDown(CAMERA.moveControl[MOVE_RIGHT]), IsKeyDown(CAMERA.moveControl[MOVE_LEFT]),
181         //         IsKeyDown(CAMERA.moveControl[MOVE_UP]), IsKeyDown(CAMERA.moveControl[MOVE_DOWN])
182         // };
183         // bool[6] direction = [false, false, false, false, false, false];
184         // bool[6] direction = [
185         //     Input.is_key_down(Keys.KEY_W), Input.is_key_down(Keys.KEY_S),
186         //     Input.is_key_down(Keys.KEY_D), Input.is_key_down(Keys.KEY_A),
187         //     Input.is_key_down(Keys.KEY_E), Input.is_key_down(Keys.KEY_Q)
188         // ];
189         // enum MOVE_FRONT = 0;
190         // enum MOVE_BACK = 1;
191         // enum MOVE_RIGHT = 2;
192         // enum MOVE_LEFT = 3;
193         // enum MOVE_UP = 4;
194         // enum MOVE_DOWN = 5;
195 
196         // auto dpos_x = (sin(_angle.x) * direction[MOVE_BACK] - sin(
197         //         _angle.x) * direction[MOVE_FRONT] - cos(
198         //         _angle.x) * direction[MOVE_LEFT] + cos(_angle.x) * direction[MOVE_RIGHT]) / move_sensitivity;
199         // auto npos_x = transform.position.x + dpos_x;
200 
201         // auto dpos_y = (sin(_angle.y) * direction[MOVE_FRONT] - sin(
202         //         _angle.y) * direction[MOVE_BACK] + 1.0f * direction[MOVE_UP]
203         //         - 1.0f * direction[MOVE_DOWN]) / move_sensitivity;
204         // auto npos_y = transform.position.y + dpos_y;
205 
206         // auto dpos_z = (cos(_angle.x) * direction[MOVE_BACK] - cos(
207         //         _angle.x) * direction[MOVE_FRONT] + sin(
208         //         _angle.x) * direction[MOVE_LEFT] - sin(_angle.x) * direction[MOVE_RIGHT]) / move_sensitivity;
209         // auto npos_z = transform.position.z + dpos_z;
210 
211         // transform.position = Vector3(npos_x, npos_y, npos_z);
212 
213         auto npos_x = transform.position.x;
214         auto npos_y = transform.position.y;
215         auto npos_z = transform.position.z;
216 
217         // CAMDATA orientation calculation
218         _angle.x = _angle.x + (Input.mouse_delta.x * -look_sensitivity);
219         _angle.y = _angle.y + (Input.mouse_delta.y * -look_sensitivity);
220 
221         // Angle clamp
222         if (_angle.y > third_person_min_clamp * C_DEG2RAD)
223             _angle.y = third_person_min_clamp * C_DEG2RAD;
224         else if (_angle.y < third_person_max_clamp * C_DEG2RAD)
225             _angle.y = third_person_max_clamp * C_DEG2RAD;
226 
227         // CAMDATA zoom
228         // _target_dist -= (wheel_delta * CAMERA_MOUSE_SCROLL_SENSITIVITY);
229 
230         // CAMDATA distance clamp
231         if (_target_dist < third_person_dist)
232             _target_dist = third_person_dist;
233 
234         // TODO: It seems CAMDATA.position is not correctly updated or some rounding issue makes the CAMDATA move straight to target.transform.position...
235         npos_x = sin(_angle.x) * _target_dist * cos(_angle.y) + target.transform.position.x;
236 
237         if (_angle.y <= 0.0f)
238             npos_y = sin(_angle.y) * _target_dist * sin(_angle.y) + target.transform.position.y;
239         else
240             npos_y = -sin(_angle.y) * _target_dist * sin(_angle.y) + target.transform.position.y;
241 
242         npos_z = cos(_angle.x) * _target_dist * cos(_angle.y) + target.transform.position.z;
243 
244         transform.position = Vector3(npos_x, npos_y, npos_z);
245     }
246 }
247 
248 /// free-look camera
249 class CameraFreeLook : CameraFollow3D {
250     import re.input : Keys, MouseButton, Input;
251 
252     mixin Reflect;
253     // public float move_sensitivity = ;
254     public float look_sensitivity = 0.01;
255     public float zoom_sensitivity = 1.5;
256     public float smooth_zoom_sensitivity = 0.05;
257     protected enum free_min_clamp = 85;
258     protected enum free_max_clamp = -85;
259     protected enum free_dist_min_clamp = 0.3;
260     protected enum free_dist_max_clamp = 120;
261     protected enum free_pan_divider = 5.1;
262 
263     this(Entity target) {
264         super(target);
265     }
266 
267     // based on https://github.com/raysan5/raylib/blob/6fa6757a8bf90d4b2fd0ce82dace7c7223635efa/src/camera.h#L314
268     void update() {
269         auto npos_x = transform.position.x;
270         auto npos_y = transform.position.y;
271         auto npos_z = transform.position.z;
272 
273         auto wheel_delta = Input.scroll_delta();
274         auto mouse_delta = Input.mouse_delta;
275 
276         auto key_pan = Input.is_mouse_down(MouseButton.MOUSE_BUTTON_RIGHT)
277             || Input.is_key_down(Keys.KEY_LEFT_ALT);
278         auto key_alternate = !Input.is_key_down(Keys.KEY_LEFT_SHIFT);
279         auto key_smooth = Input.is_key_down(Keys.KEY_LEFT_CONTROL);
280 
281         auto tpos_x = cam.camera.target.x;
282         auto tpos_y = cam.camera.target.y;
283         auto tpos_z = cam.camera.target.z;
284 
285         // Camera zoom
286         if ((_target_dist < free_dist_max_clamp) && (wheel_delta < 0)) {
287             _target_dist -= (wheel_delta * zoom_sensitivity);
288             if (_target_dist > free_dist_max_clamp)
289                 _target_dist = free_dist_max_clamp;
290         }  // Camera looking down
291         else if ((entity.position.y > tpos_y)
292                 && (_target_dist == free_dist_max_clamp) && (wheel_delta < 0)) {
293             tpos_x += wheel_delta * (tpos_x - entity.position.x) * zoom_sensitivity / _target_dist;
294             tpos_y += wheel_delta * (tpos_y - entity.position.y) * zoom_sensitivity / _target_dist;
295             tpos_z += wheel_delta * (tpos_z - entity.position.z) * zoom_sensitivity / _target_dist;
296         } else if ((entity.position.y > tpos_y) && (tpos_y >= 0)) {
297             tpos_x += wheel_delta * (tpos_x - entity.position.x) * zoom_sensitivity / _target_dist;
298             tpos_y += wheel_delta * (tpos_y - entity.position.y) * zoom_sensitivity / _target_dist;
299             tpos_z += wheel_delta * (tpos_z - entity.position.z) * zoom_sensitivity / _target_dist;
300 
301             // if (tpos_y < 0) tpos_y = -0.001;
302         } else if ((entity.position.y > tpos_y) && (tpos_y < 0) && (wheel_delta > 0)) {
303             _target_dist -= (wheel_delta * zoom_sensitivity);
304             if (_target_dist < free_dist_min_clamp)
305                 _target_dist = free_dist_min_clamp;
306         }  // Camera looking up
307         else if ((entity.position.y < tpos_y)
308                 && (_target_dist == free_dist_max_clamp) && (wheel_delta < 0)) {
309             tpos_x += wheel_delta * (tpos_x - entity.position.x) * zoom_sensitivity / _target_dist;
310             tpos_y += wheel_delta * (tpos_y - entity.position.y) * zoom_sensitivity / _target_dist;
311             tpos_z += wheel_delta * (tpos_z - entity.position.z) * zoom_sensitivity / _target_dist;
312         } else if ((entity.position.y < tpos_y) && (tpos_y <= 0)) {
313             tpos_x += wheel_delta * (tpos_x - entity.position.x) * zoom_sensitivity / _target_dist;
314             tpos_y += wheel_delta * (tpos_y - entity.position.y) * zoom_sensitivity / _target_dist;
315             tpos_z += wheel_delta * (tpos_z - entity.position.z) * zoom_sensitivity / _target_dist;
316 
317             // if (tpos_y > 0) tpos_y = 0.001;
318         } else if ((entity.position.y < tpos_y) && (tpos_y > 0) && (wheel_delta > 0)) {
319             _target_dist -= (wheel_delta * zoom_sensitivity);
320             if (_target_dist < free_dist_min_clamp)
321                 _target_dist = free_dist_min_clamp;
322         }
323 
324         // Input keys checks
325         if (key_pan) {
326             if (key_alternate) // Alternative key behaviour
327             {
328                 if (key_smooth) {
329                     // Camera smooth zoom
330                     _target_dist += (mouse_delta.y * smooth_zoom_sensitivity);
331                 } else {
332                     // Camera rotation
333                     _angle.x += mouse_delta.x * -look_sensitivity;
334                     _angle.y += mouse_delta.y * -look_sensitivity;
335 
336                     // Angle clamp
337                     if (_angle.y > free_min_clamp * C_DEG2RAD)
338                         _angle.y = free_min_clamp * C_DEG2RAD;
339                     else if (_angle.y < free_max_clamp * C_DEG2RAD)
340                         _angle.y = free_max_clamp * C_DEG2RAD;
341                 }
342             } else {
343                 // Camera panning
344                 tpos_x += ((mouse_delta.x * look_sensitivity) * cos(_angle.x) + (
345                         mouse_delta.y * look_sensitivity) * sin(_angle.x) * sin(_angle.y)) * (
346                         _target_dist / free_pan_divider);
347                 tpos_y += ((mouse_delta.y * look_sensitivity) * cos(_angle.y)) * (
348                         _target_dist / free_pan_divider);
349                 tpos_z += ((mouse_delta.x * -look_sensitivity) * sin(_angle.x) + (
350                         mouse_delta.y * look_sensitivity) * cos(_angle.x) * sin(_angle.y)) * (
351                         _target_dist / free_pan_divider);
352             }
353         }
354 
355         // Update camera position with changes
356         npos_x = -sin(_angle.x) * _target_dist * cos(_angle.y) + tpos_x;
357         npos_y = -sin(_angle.y) * _target_dist + tpos_y;
358         npos_z = -cos(_angle.x) * _target_dist * cos(_angle.y) + tpos_z;
359 
360         transform.position = Vector3(npos_x, npos_y, npos_z);
361         cam.camera.target = Vector3(tpos_x, tpos_y, tpos_z);
362     }
363 }