blog:2023:0221_nervland_simple_camera

NervLand: A simple camera implementation

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!

  • 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};
    };
  • 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<PushConstants> pushc{nullptr};
        U32 offset{0};
        Vec3f axis;
        float speed;
        float startTime{-1.0F};
        nv::RefPtr<Camera> 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<GraphUpdater> {
        return nv::create_ref_object<SimpleCubeRotator>(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()
  • 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
  • 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_nervland_simple_camera.txt
  • Last modified: 2023/02/21 17:04
  • by 127.0.0.1