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 }