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