====== NervLand: A simple camera implementation ======
{{tag>dev cpp vulkan nervland}}
Hello world! Continuing our journey with vulkan I feel I'm now ready to build a dedicated "camera" class, and then figure out how to send user inputs to that camera: after all, all we really need here are the mouse events from the SDL window, right ? So, can't be that terrible to implement 🤣 Let's get started!
====== ======
===== Initial Camera class =====
* Continuing with the previous article on this topic, what I need here is a "Camera class" from which I could just retrieve the view matrix and the projection matrix, or the combination of both (with the view matrix inverted).
* Naturally I will simply call this class "Camera" and here is the initial version of it: class NVVULKAN_EXPORT Camera : public nv::RefObject {
NV_DECLARE_NO_COPY(Camera)
NV_DECLARE_NO_MOVE(Camera)
public:
/** Camera constructor */
explicit Camera();
/** Camera destructor */
~Camera() override;
/** Set the view matrix as a look at matrix */
void set_look_at(const nv::Vec3f& eye, const nv::Vec3f& target,
const nv::Vec3f& up) {
_view.make_look_at(eye, target, up);
_viewInvDirty = true;
_viewProjDirty = true;
}
/** Set the projection as a perspective */
void set_perspective(float fovy, float aspect, float znear, float zfar) {
_proj.make_perspective(fovy, aspect, znear, zfar);
_projInvDirty = true;
_viewProjDirty = true;
}
/** Get the view matrix */
auto get_view() const -> const nv::Mat4f& { return _view; }
/** Get the view inverse matrix */
auto get_view_inverse() -> const nv::Mat4f&;
/** Get the projection matrix */
auto get_projection() const -> const nv::Mat4f& { return _proj; }
/** Get the projection inverse matrix */
auto get_projection_inverse() -> const nv::Mat4f&;
/** Get view projection matrix: this is computed internally as
proj*viewInv */
auto get_view_projection() -> const nv::Mat4f&;
protected:
/** Storage for the view matrix */
nv::Mat4f _view;
bool _viewInvDirty{true};
nv::Mat4f _viewInv;
/** Storage for the projection matrix */
nv::Mat4f _proj;
bool _projInvDirty{true};
nv::Mat4f _projInv;
/** Storage for view projection matrix */
nv::Mat4f _viewProj;
bool _viewProjDirty{true};
};
===== Using the camera class =====
* Next, in our cube rotator updater, let's try to use this new camera class instead of providing the projection and view matrices manually.
* => Here is the updated implementation for the SimpleCubeRotator: struct SimpleCubeRotator : public GraphUpdater {
nv::RefPtr pushc{nullptr};
U32 offset{0};
Vec3f axis;
float speed;
float startTime{-1.0F};
nv::RefPtr camera;
SimpleCubeRotator(PushConstants* pushc, const Vec3f& axis, float speed,
Camera* cam, U32 offset)
: pushc(pushc), offset(offset), axis(axis), speed(speed), camera(cam){};
void update(FrameDesc& fdesc) override;
};
void SimpleCubeRotator::update(FrameDesc& fdesc) {
auto& barr = pushc->get_bytearray();
// compute the rotation angle:
if (startTime < 0.0) {
startTime = (float)fdesc.frameTime;
}
float elapsed = (float)fdesc.frameTime - startTime;
float angle = toRad(elapsed * speed);
// Create the rotation matrix:
auto rot = Mat4f::rotate(angle, axis);
// Compute the final transform matrix:
auto mat = camera->get_view_projection() * rot;
// Write the matrix into the push constant buffer:
barr.write_mat4f(mat, offset);
}
auto create_simple_cube_rotator(PushConstants* pushc, const Vec3f& axis,
float speed, Camera* cam, U32 offset)
-> nv::RefPtr {
return nv::create_ref_object(pushc, axis, speed, cam,
offset);
}
* Next we should create the camera object in lua and pass it to the updater: -- Create on simple cube rotator:
local axis = nv.Vec3f(1.0, 1.0, 1.0)
local speed = 30.0
local view = nv.Mat4f.look_at(nv.Vec3f(0.0, 0.0, -50.0), nv.Vec3f(0.0, 0.0, 0.0), nv.Vec3f(0.0, -1.0, 0.0))
local proj = nv.Mat4f.perspective(math.rad(60.0), 1.333, 1.0, 100.0)
-- local proj = nv.Mat4f.ortho(-30, 30, -30, 30, 0.0, 100.0)
-- Create our camera object:
local cam = nvk.Camera()
cam:set_view(view)
cam:set_projection(proj)
local updater = nvk.create_simple_cube_rotator(pushc, axis, speed, cam, 0)
rgraph:add_updater(updater)
* And with that I should get the same visual results as before... checking... And no LOL... just a completely gray background 🤣 So I messed something up. Let's see...
* **OK**: fixed now: this was a stupid typo in ''Camera::get_view_projection()''
===== Designing a camera controller =====
* For our camear controller we need to intercept mouse events from the window, so the controller should accept the SDL window as parameter.
* **Note**: currently our handlers in the window class are defined to be a simple ''std::function'': let's turn these into dedicated functors instead: I think this would be more flexible on the long run.
* Okay so I got a bit side-tracked lately, but now let's try to focus on the camera controller again...
* So here is the initial version of a TargetCamerController I created: namespace nvk {
TargetCameraController::TargetCameraController(Camera* cam, nv::Window* window,
nv::Vec3f target, nv::Vec3f pos,
float viewDist)
: CameraController(cam), _targetPosition(target), _cameraPosition(pos),
_viewDistance(viewDist) {
logDEBUG("Creating TargetCameraController object.");
connect_handlers(window);
}
TargetCameraController::~TargetCameraController() {
logDEBUG("Deleting TargetCameraController object.");
// Disconnect the event handlers:
disconnect_handlers();
}
void TargetCameraController::disconnect_handlers() {
if (_window == nullptr) {
// nothing to do:
return;
}
for (auto* h : _handlers) {
_window->remove_event_handler(h);
}
_handlers.clear();
}
void TargetCameraController::connect_handlers(nv::Window* win) {
if (win == _window) {
// Nothing to do:
return;
}
// Disconnect any previous handlers:
disconnect_handlers();
if (win == nullptr) {
return;
}
// Connect the handlers:
auto* h = win->add_mouse_motion_handler([this](int x, int y, int state) {
// logDEBUG("Camera Controller: mouse motion event: {}, {}, {}", x, y,
// state);
// if the state is 1, then our mouse button is pressed, and we want to
// perform a rotation: use the position offset from the last time:
if (!_dragging && state == 1) {
_dragging = true;
_mouseX = x;
_mouseY = y;
}
if (_dragging && state == 0) {
_dragging = false;
}
if (!_dragging) {
return false;
}
I32 dx = x - _mouseX;
I32 dy = y - _mouseY;
_mouseX = x;
_mouseY = y;
// Update the theta/phi angles:
_theta -= dx * 0.004;
_phi -= dy * 0.004;
return true;
});
_handlers.push_back(h);
}
void TargetCameraController::update() {
// Update the camera matrix here:
// We should update the camera position using the current orientation:
auto att =
nv::Quatd(_phi, nv::VEC3D_RIGHT) * nv::Quatd(_theta, nv::VEC3D_UP);
;
nv::Vec3f forward(att * nv::VEC3D_FWD);
// logDEBUG("Forward vector is: {}", forward);
// logDEBUG("Forward vector length: {}", forward.length());
_cameraPosition = _targetPosition - forward * _viewDistance;
// logDEBUG("Updated camera position is: {}", _cameraPosition);
_camera->set_look_at(_cameraPosition, _targetPosition, _upDirection);
}
} // namespace nvk /*//*/
===== Conclusion =====
* I then started to use this target camera controller in lua as follow: -- Create our camera object:
local cam = nvk.Camera()
cam:set_view(view)
cam:set_projection(proj)
local updater = nvk.create_simple_cube_rotator(pushc, axis, speed, cam, 0)
rgraph:add_updater(updater)
-- Also create a camera target controller here:
local ctrl = nvk.TargetCameraController(cam, self:getMainWindow(), nv.Vec3f(0.0, 0.0, 0.0), nv.Vec3f(0.0, 0.0, -50.0)
, 50.0)
self.camController = ctrl
local updater = nvk.create_lua_graph_updater(function(fdesc)
-- logDEBUG("Current frame index is: ", fdesc.frameNumber)
ctrl:update()
end)
rgraph:add_updater(updater)
* And with that change I can now control the camera orientation with the mouse:
{{ blog:2023:0221:target_cam_ctrl.gif?600 }}