Tutorial #00: Moving Camera

In the first 3d tutorial we show how to create a very simple 3d scene surrounded by a skybox and how to move the camera in the scene.

Skybox

To keep the example easy, we create a simple scene with merely a cube on a plane. Additionally we enclose the scene with a skybox.

A skybox is a popular technique to create a background for a 3d scene. The whole scene is wrapped inside a textured cuboid. Each plane of the cuboid is textured with a different image (cube mapping). Beside the sky, the skybox textures may also contain distant mountains or buildings, thus making the scene look bigger and more impressive.

Note
We will use the cube mapping technique not only for the skybox but also for the cube.

CubemapTexture

The node Graph::CubemapTexture can be used to create a texture object which contains all six texture images for the cuboid.

Firstly we specify the individual image resources in the file package.xml.

    <!-- Image resources -->
    <Resource id="gfx_plane"          fileName="plane.png"/>
    <Resource id="gfx_cube_nx"        fileName="cube_neg_x.png"/>
    <Resource id="gfx_cube_px"        fileName="cube_pos_x.png"/>
    <Resource id="gfx_cube_ny"        fileName="cube_neg_y.png"/>
    <Resource id="gfx_cube_py"        fileName="cube_pos_y.png"/>
    <Resource id="gfx_cube_nz"        fileName="cube_neg_z.png"/>
    <Resource id="gfx_cube_pz"        fileName="cube_pos_z.png"/>
    <Resource id="gfx_sky_up"         fileName="sky1_up.jpg"/>
    <Resource id="gfx_sky_down"       fileName="sky1_down.jpg"/>
    <Resource id="gfx_sky_left"       fileName="sky1_left.jpg"/>
    <Resource id="gfx_sky_right"      fileName="sky1_right.jpg"/>
    <Resource id="gfx_sky_front"      fileName="sky1_front.jpg"/>
    <Resource id="gfx_sky_back"       fileName="sky1_back.jpg"/>

Next we create a CubemapTexture object and define a suitable textureSlot with a TextureState node.

    <FlatTexture id="texture_plane" imageResourceId="gfx_plane"/>

    <CubemapTexture id="texture_cube"
                    imageResourceIdPosX="gfx_cube_nx" imageResourceIdNegX="gfx_cube_px"
                    imageResourceIdPosY="gfx_cube_ny" imageResourceIdNegY="gfx_cube_py"
                    imageResourceIdPosZ="gfx_cube_nz" imageResourceIdNegZ="gfx_cube_pz"
                    wrapModeX="CLAMP_TO_EDGE"
                    wrapModeY="CLAMP_TO_EDGE"
    />
    
    <CubemapTexture id="texture_sky"
                    imageResourceIdPosX="gfx_sky_right" imageResourceIdNegX="gfx_sky_left"
                    imageResourceIdPosY="gfx_sky_up"    imageResourceIdNegY="gfx_sky_down"
                    imageResourceIdPosZ="gfx_sky_front" imageResourceIdNegZ="gfx_sky_back"
                    wrapModeX="CLAMP_TO_EDGE"
                    wrapModeY="CLAMP_TO_EDGE"
    />
    
    <TextureState slot="0" textureId="texture_plane"/>
    <TextureState slot="2" textureId="texture_cube"/>
    <TextureState slot="3" textureId="texture_sky"/>

Changing the attribute wrapModeX and wrapModeY from default value REPEAT to CLAMP_TO_EDGE prevents unaesthetic artifacts on the edges.

Shader

As there is no FixedProgram which can be used to draw cube maps, we need a ShaderProgram.

For this, we first need to define the shader code as XML shader resource in the file shader_skybox.xml:

<?xml version="1.0" ?>

<Shader languages="GLSL_ES_120,HLSL_30,HLSL_40_93">

    <Attributes>
        <Attribute item="COORD" type="FLOAT_VECTOR_4"/>
    </Attributes>

    <ConstantBuffers>
        <ConstantBuffer item="MODEL"/>
        <ConstantBuffer item="CAMERA"/>
    </ConstantBuffers>

    <Varyings>
        <Varying name="vCubeCoord" type="FLOAT_VECTOR_3" precision="MEDIUM"/>
    </Varyings>

    <Textures>
        <Texture unit="0" type="CUBE" precision="LOW"/>
    </Textures>
    
    <VertexSource>
        <![CDATA[
            void main()
            {
                gl_Position = uCameraViewProjectionMatrix * (uModelMatrix * aPosition);
                vCubeCoord = aPosition.xyz;
            }
        ]]>
    </VertexSource>
    
    <FragmentSource>
        <![CDATA[
            void main()
            {
                gl_FragColor = textureCube(uTexture0, vCubeCoord);
            }
        ]]>
    </FragmentSource>
    
</Shader>

The new file needs to be listed as a resource in the package.xml file before it can be used.

    <!-- Shader resources -->
    <Resource id="shader_skybox_res"  fileName="shader_skybox.xml"/>

Next, we define a Shader node and a ShaderProgram node in the materials.xml file.

<Shader id="shader_skybox"
    shaderResourceId="shader_skybox_res"
/>
<ShaderProgram
    id="prg_cube"
    vertexShaderId="shader_skybox"
    fragmentShaderId="shader_skybox"
/>

The ShaderProgram can then be used to create the Material nodes.

<Material
    id="mat_cube"
    visibleFaces="FRONT_AND_BACK"
    programId="prg_cube"
/>
<Material
    id="mat_sky"
    visibleFaces="BACK_ONLY"
    programId="prg_cube"
/>

Now we are able to define the nodes for the skybox, the cube and the plane.

<MaterialState slot="11" materialId="/material/mat_cube"/>
<MaterialState slot="12" materialId="/material/mat_sky"/>

<PlaneGeometry id="plane" materialSlot="3" scaleFactor="10000" posX="0" posY="-75" angleX="90deg" texCoordX2="8" texCoordY2="8"/>
<CubeGeometry  id="cube" materialSlot="11" textureSlot="2" scaleFactor="100" posY="-25"/>
<CubeGeometry  id="skybox" materialSlot="12" textureSlot="3" scaleFactor="10000" />
tut0300_moving_camera.png
Moving camera scene

Moving Camera

We define the position of the camera (as for every other object) by a number of transformations. The primary position is defined by the CameraTransform node. Additional transform operations can be defined by nesting the CameraTransform node within additional Transform nodes.

In computer graphics a transformation is usually done by multiplying the vertex data with a four-dimensional transformation matrix (see also https://en.wikipedia.org/wiki/Transformation_matrix). Each Transform node contains such a 4x4 transformation matrix.

We will move the camera by directly manipulating the transformation matrix of the CameraTransform node. When doing so, we have to keep the order of the operations in mind:

Within one Transform node the transformation order is:

  • Scale
  • Rotate
  • Translate

The rotation order of the Euler angles within one Transform node is:

  • Z
  • Y
  • X

The execution order for nested Transform nodes is from inner to outer. So the transformation of the most inner Transform node is applied first.

There are, of course, various possibilities for the implementation of the camera movement. Three different methods are outlined below.

Initialize

In the header file we define a reference to the CameraTransform node, two member variables to store the rotation and the position vector and an additional member variable to store the current mode.

    Murl::Logic::TransformNode mCameraTransform;
    Vector mRot;
    Vector mPos;
    UInt32 mMode;

The variables will be initialized in the constructor and in the OnInit method:

App::MovingCameraLogic::MovingCameraLogic(Logic::IFactory* factory)
: BaseProcessor(factory)
, mRot(Vector::ZERO_POSITION)
, mPos(Vector::ZERO_POSITION)
, mMode(1)
{
}
Bool App::MovingCameraLogic::OnInit(const Logic::IState* state)
{
    state->GetLoader()->UnloadPackage("startup");

    Graph::IRoot* root = state->GetGraphRoot();
    AddGraphNode(mCameraTransform.GetReference(root, "main_camera_transform"));

    if (!AreGraphNodesValid())
    {
        return false;
    }

    mPos = mCameraTransform->GetPosition();
    state->SetUserDebugMessage("Mode "+Util::UInt32ToString(mMode));
    return true;
}

Now we are able to change the position of the camera in the OnProcessTick method by altering the mCameraTransform values. We use the keyboard to control the camera:

R Reset
1/2/3 Mode 1/2/3
LEFT/RIGHT/UP/DOWN Bewegung nach links/rechts/vor/zurück
SHIFT/STRG Bewegung nach oben/unten
Q/A Rotation um X
W/S Rotation um Y
E/D Rotation um Z

    // get translation key input
    moveLeft     = deviceHandler->IsRawKeyPressed(RAWKEY_LEFT_ARROW);
    moveRight    = deviceHandler->IsRawKeyPressed(RAWKEY_RIGHT_ARROW);
    moveForward  = deviceHandler->IsRawKeyPressed(RAWKEY_UP_ARROW);
    moveBackward = deviceHandler->IsRawKeyPressed(RAWKEY_DOWN_ARROW);
    moveUp       = deviceHandler->IsRawKeyPressed(RAWKEY_LEFT_SHIFT);
    moveDown     = deviceHandler->IsRawKeyPressed(RAWKEY_LEFT_CONTROL);

    // get rotation key input
    if (deviceHandler->IsRawKeyPressed(RAWKEY_Q))
        rotDiffX = 1;
    else if (deviceHandler->IsRawKeyPressed(RAWKEY_A))
        rotDiffX = -1;

    if (deviceHandler->IsRawKeyPressed(RAWKEY_W))
        rotDiffY = 1;
    else if (deviceHandler->IsRawKeyPressed(RAWKEY_S))
        rotDiffY = -1;

    if (deviceHandler->IsRawKeyPressed(RAWKEY_E))
        rotDiffZ = 1;
    else if (deviceHandler->IsRawKeyPressed(RAWKEY_D))
        rotDiffZ = -1;
        

Reset

To reset the positon of the camera, we create an identity matrix and adjust the translation values to the initial camera position (0/0/512). The method SetTransform() is then used to overwrite the values of the transformation matrix of the CameraTransform node.

    // RESET
    if (deviceHandler->WasRawKeyPressed(RAWKEY_R))
    {
        mPos.x = 0;
        mPos.y = 0;
        mPos.z = 512;
        mRot = Vector::ZERO_POSITION;
        Matrix transformMatrix(Matrix::IDENTITY);
        transformMatrix.SetTranslationComponent(mPos);
        mCameraTransform->SetTransform(transformMatrix);
    }

Mode 1

This method directly sets the rotation and translation parameter of the transformation matrix. The rotation values are set as Euler angles (see also en.wikipedia.org/wiki/Euler_angles).

At first we calculate a direction vector depending on the currently pressed keys. This direction vector will also be used by the other methods.

The member variable mPos and mRot are used to store the current translation and rotation vector, respectively. We use the method GetTransform() to get the transformation matrix and use the methods SetRotationComponent and SetTranslationComponent to update the rotation values and translation values, respectively.

    // calculate translation vector
    Vector direction(Vector::ZERO_DIRECTION);
    if (moveForward)
        direction.z = -walkingSpeed;
    else if (moveBackward)
        direction.z =  walkingSpeed;
    if (moveLeft)
        direction.x = -walkingSpeed;
    else if (moveRight)
        direction.x =  walkingSpeed;
    if (moveUp)
        direction.y =  walkingSpeed;
    else if (moveDown)
        direction.y = -walkingSpeed;

    if (mMode == 1)
    {
        // directly set rotation parameter
        mRot.x += rotDiffX * rotSpeed;
        mRot.y += rotDiffY * rotSpeed;
        mRot.z += rotDiffZ * rotSpeed;
        mCameraTransform->GetTransform().SetRotationComponent(mRot.x, mRot.y, mRot.z);

        // move along the x/y/z axis
        mPos += direction;
        mCameraTransform->GetTransform().SetTranslationComponent(mPos);
    }

There are some disadvantages associated with this method:

  • The camera moves always along the x/y/z axis of the world and not along the viewing direction.
  • Due to the rotation order z, y, x, it is not possible to control the rotation independently e.g. if your viewing direction is along the x axis, it is not possible to roll. Changing the values for x and z would have the same affect (see also en.wikipedia.org/wiki/Gimbal_lock).

The best way to understand the limitations is to try it out and experiment.

Note
Hint! It is also possible to set the rotation by using the method SetRotationComponentAxisAngle or by using quaternions.

Mode 2

This method solves the previous problems. We don't store and set the rotation and translation components of the matrix but instead directly apply every change by a matrix multiplication.

    if (mMode == 2)
    {
        // multiply by rotation/translation matrix
        Matrix transformMatrix(Matrix::IDENTITY);
        transformMatrix.SetRotationComponent(rotDiffX*rotSpeed, rotDiffY*rotSpeed, rotDiffZ*rotSpeed);
        transformMatrix.SetTranslationComponent(direction);
        mCameraTransform->SetTransform(mCameraTransform->GetTransform() * transformMatrix);
    }

The camera moves always along the viewing direction and you can control the roll, pitch and yaw angle independently. You can fly around.

Mode 3

The third method demonstrates a FPS style mode. For this mode we allow to control the rotation with the mouse but move always on the plane.

    if (mMode == 3)
    {
        //get rotation mouse input
        if (deviceHandler->IsRawMouseAvailable())
        {
            deviceHandler->GetRawMouseDelta(rotDiffY, rotDiffX);
            Real dummy;
            deviceHandler->GetRawWheelDelta(dummy, rotDiffZ);
            rotDiffY *= -1.0;
        }

        // FPS mode
        mRot.x += rotDiffX * rotSpeed;
        mRot.y += rotDiffY * rotSpeed;
        mRot.z += rotDiffZ * rotSpeed;
        mCameraTransform->GetTransform().SetRotationComponent(mRot.x, mRot.y, 0);

        Matrix directionTransform(Matrix::IDENTITY);
        directionTransform.SetRotationComponent(0.0, mRot.y, 0.0);
        mPos += directionTransform.Multiply(direction);
        mPos.y = 0;
        mCameraTransform->GetTransform().SetTranslationComponent(mPos);
    }

The camera moves always along the viewing direction but the y position stays always the same (or would typically be defined by the terrain). You can walk around and look up and down.


Copyright © 2011-2024 Spraylight GmbH.