====== 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 }}