Gx::Camera wafflings

Published January 25, 2017
Advertisement

Time for the next gripping installment of the development of my [font='courier new']Gx[/font] game framework library. The latest addition I'll waffle about today is the [font='courier new']Gx::Camera[/font] class from [font='courier new']GxMaths[/font].

The requirement I have for the camera is that it can either be controlled directly by the user, for debugging and development purposes and also run in a sort of tracking mode, where you set a target position and rotation each frame and the camera smoothly moves towards this target.

As a result, it is necessary for me to store the rotation of the camera as a quaternion, so I can easily do smooth interpolations. However, I find quaternions hard to visualise, so I also want to be able to talk to the camera in terms of a 2D vector of angles.

With all this in mind, the place to start is (most of) the public interface to [font='courier new']Gx::Camera[/font].

namespace Gx{class Camera{public: enum class Mode { Manual, Tracking }; Camera(); Camera(const ViewTransform &transform); Camera(const Vec3 &position, const Vec2 &rotation); void store(); void update(float delta); void update(float trackingSpeed, float delta); ViewTransform transform() const; ViewTransform transform(float blend) const; void setTransform(const ViewTransform &transform); void setTarget(const ViewTransform &transform); Matrix viewMatrix(float blend) const; Vec3 position(float blend) const; Mode mode() const;};So what is a [font='courier new']Gx::ViewTransform[/font] when it is at home? It is a simple enough class that aggregates a position and a quaternion representation of a rotation, but also provides an interface to express the rotation in terms of two angles.

namespace Gx{class ViewTransform{public: ViewTransform(); ViewTransform(const Vec3 &position, const Quaternion &rotation); ViewTransform(const Vec3 &position, const Vec2 &angle); Vec3 position() const; void setPosition(const Vec3 &value); Quaternion rotation() const; void setRotation(const Quaternion &value); Vec2 angle() const; void setAngle(const Vec2 &value); void flatVectors(Vec3 &look, Vec3 &right) const;private: Vec3 pos; Quaternion rot;};ViewTransform interpolate(const ViewTransform &a, const ViewTransform &b, float t);}All pretty self-explanatory. Internally we just use a method from [font='courier new']Gx::Quaternion[/font]'s home file to change the angle into a quaternion, for example:

void Gx::ViewTransform::setAngle(const Vec2 &value){ rot = axisRotationQuaternion(Vec3(value.y, value.x, 0));}This just translates into a call to [font='courier new']D3DXQuaternionRotationYawPitchRoll[/font] internally.

The next question you may be asking (and I appreciate you are probably not but let's pretend you are) is what the [font='courier new']blend[/font] parameter is all about in the various getter methods on [font='courier new']Gx::Camera[/font].

My game loops are always based on the Gaffer fix-your-timestep approach, where we render as fast as possible but call the update loop with a fixed timestep (normally 1 / 60 but can be set to anything). To keep everything looking smooth in such a system, when you render, the game loop produces a [font='courier new']blend[/font] value between 0 and 1, which is a kind of representation of the accumulated error between the actual framerate and the physics step.

The results are quite striking in terms of how much smoother everything looks, especially when you have a lower framerate or a lot of variation in how long the render takes from frame to frame.

To support this, I have a simple but widely-used [font='courier new']Gx::BlendValue[/font] class, which takes care of storing and interpolating between a value and its previous state.

namespace Gx{template class BlendValue{public: BlendValue(const T &t = T()) : v(t), o(v) { } template BlendValue(Args... args) : v(args...), o(v) { } void store(){ o = v; } void set(const T &t){ v = t; } void add(const T &t){ v += t; } T value() const { return v; } T value(float blend) const { return interpolate(o, v, blend); }private: T v; T o;};typedef BlendValue BlendFloat;typedef BlendValue BlendVec2;typedef BlendValue BlendVec3;typedef BlendValue BlendQuaternion;typedef BlendValue BlendViewTransform;}This can be instantiated with any type for which a free [font='courier new']interpolate[/font] method is defined. You call [font='courier new']store()[/font] somewhere at the start of the frame, then use [font='courier new']set()[/font] or [font='courier new']add()[/font] to modify the value. Then when you are rendering, you can just call the version of [font='courier new']value()[/font] which takes the [font='courier new']blend[/font] parameter to get the interpolated value for the current frame.

Looking at a few of the [font='courier new']Gx::Camera[/font]'s methods with this in mind:

void Gx::Camera::store(){ frame.store();}void Gx::Camera::update(float delta){ update(8, delta);}void Gx::Camera::update(float trackingSpeed, float delta){ if(m == Mode::Tracking) { t += delta * trackingSpeed; frame.set(interpolate(cur, tar, t)); if(t >= 1) { cur = tar; frame.set(cur); } } else { frame.set(cur); }}Gx::Matrix Gx::Camera::viewMatrix(float blend) const{ Vec3 look(0, 0, 1); Vec3 up(0, 1, 0); ViewTransform tr = frame.value(blend); Quaternion r = tr.rotation(); look = transformNormal(look, r); up = transformNormal(up, r); Vec3 p = tr.position(); return lookAtMatrix(p, p + look, up);}This ultimately gives us a camera that works within the game loop system we are using.

I wanted to have a fairly easy way to control the camera with user-input as well from the game project using the library but wanted to decouple the camera from the input events system I have. [font='courier new']Gx::ApplicationEvents[/font] is a class that takes care of informing the application about input events.

class ApplicationEvents{public: ApplicationEvents(); bool isKeyDown(int key) const; Vec2 rawMouseDelta() const; Signal activated; Signal keyDown; Signal keyUp; Signal graphicsDeviceReset;};We won't bother going into details of how all this is implemented. Suffice to say it has a [font='courier new']wndProc[/font] at its heart and uses [font='courier new']WM_INPUT[/font] messages to get raw mouse information.

So [font='courier new']Gx::Camera[/font] has a chid class - [font='courier new']Gx::Camera::Step[/font] and a [font='courier new']step()[/font] method:

namespace Gx{class Camera{public: class Step { public: Step(); bool forward; bool backward; bool left; bool right; bool rise; bool fall; float speed; Vec2 rawMouseDelta; }; void step(const Step &step, float delta);};This is nice and easy to use from a host project as long as we have access to the [font='courier new']Gx::ApplicationEvents[/font] instance, which is a member of [font='courier new']Gx::Application[/font]. I just bind references to this when I need it in other classes. So in my game application, we just call this method each frame:

void GameMode::updateCamera(float delta){ camera.store(); Gx::Camera::Step step; step.forward = events.isKeyDown('W'); step.backward = events.isKeyDown('S'); step.left = events.isKeyDown('A'); step.right = events.isKeyDown('D'); step.rise = events.isKeyDown('R'); step.fall = events.isKeyDown('F'); step.speed = 10; step.rawMouseDelta = events.rawMouseDelta(); camera.step(step, delta);}Once we get to the point where we have a central character running around the level, we can instead run the camera in its tracking mode, where we just work out the point above and behind the player's head, work out the angle that points the camera in the appropriate direction, then call the [font='courier new']setTarget()[/font] method each frame.

We then just have to call the [font='courier new']update()[/font] method of the camera each frame and the camera will tracking smoothly in a third-person style.

So, that is the [font='courier new']Gx::Camera[/font] class.

On a personal note, I'm going to scrap my car tonight and do without transport for a while. Seems one of my front suspension springs broke yesterday. It really was the suspension spring that broke the camel's back as far as throwing any more money at that rusty old heap goes. It has passed the end of its useful life and I'd be a fool to throw another penny at it.

It has cost me a fortune to keep it on the road and MOT-ed over the last couple of years. Enough is enough.

I walked from home to where I work (I technically work as a C++ developer from home, but can't stand working at home so work from a local Wetherspoons pub every day, taking advantage of the free coffee refills and wi-fi) carrying my laptop bag this morning. Took about 45 minutes, which is bearable.

Not having the car will be a pain in some ways, but it also kind of feels like I'm taking a massive weight off my shoulders. Even without the constant repair bills, I reckon fuel, insurance, tax and car-parking are costing me between GBP200 and GBP300 a month, so when the walking is bothering me, I can just think about my bank balance to cheer myself up. Quite like walking anyway. It's good for my sanity.

Thanks for stopping by.

2 likes 0 comments

Comments

Nobody has left a comment. You can be the first!
You must log in to join the conversation.
Don't have a GameDev.net account? Sign up!
Profile
Author
Advertisement
Advertisement