Saturday, 15 April 2017

3D Scene Setup and Heightmaps

Minimal 3D application

This is a very (very) simple 3D application - we create a simple plane, a camera and a light and then set it up to render.

The Code

Scene@ thescene;
Node@ cameraNode;

void Start() {
  thescene = Scene();
  thescene.CreateComponent("Octree");

  cameraNode = thescene.CreateChild();
  cameraNode.CreateComponent("Camera");
  cameraNode.position = Vector3(0.0f, 5.0f, 0.0f);

Node@ planeNode = thescene.CreateChild();
  planeNode.scale = Vector3(100.0f, 1.0f, 100.0f);
StaticModel@ planeObject = planeNode.CreateComponent("StaticModel");
  planeObject.model = cache.GetResource("Model", "Models/Plane.mdl");
  planeObject.material = cache.GetResource("Material", "Materials/StoneTiled.xml");

Node@ lightNode = thescene.CreateChild();
  lightNode.direction = Vector3(0.6f, -1.0f, 0.8f);
Light@ light = lightNode.CreateComponent("Light");
  light.lightType = LIGHT_DIRECTIONAL;

  Viewport@ viewport = Viewport(thescene, cameraNode.GetComponent("Camera"));
  renderer.viewports[0] = viewport;

  SubscribeToEvent("KeyDown", "HandleKeyPress");
}

void HandleKeyPress(StringHash eventType, VariantMap& eventData) {
  if (eventData["Key"].GetInt() == KEY_ESCAPE) {
    engine.Exit();
  }
}

Taking This Line by Line

Initially we create a reference to the Scene object - every node in 3d space is a child of this. We also create an Octree Component at this level, which must be added to the root scene for the render to work.
The Octree is responsible for accelerating the render by subdividing the global 3D space and rapidly determining the renderable objects. In theory we could optimise this for a given scene, but we need at least one to render at all, so we add a default version to the root scene node.

Next we create the basic objects that will populate the scene: A plane, a light, and a camera. A camera so we we have something to view the scene through, a light so we can see things, a plane so we have something to actually see.

Our node graph is this:

          [Scene]
             |
    _________|_________  
   |         |         |
   |         |         |
[Plane]   [Light]   [Camera]   

And for the three children we have a two part process which is:

  • Create a Node, as a child of the Scene
  • Create the Component within the Node
In these cases we apply the basic 3D transforms (scaling, direction, position) to the Node, and the Component has any specific operations for the type of thing we create (Apply a model, Set the Type of Light, etc). The Material and Model for the plane are taken straight from the default Urho3D resources, available with the distribution.

The Material is provided as an XML file describing the texturing in terms of textures, shaders and rendering passes, and in this case references images which are stored in the DDS format; (a DirectDraw Surface) for the underlying texture images.

Finally we set up a Viewport, passing it the Scene and Camera reference, and then hand it to the global renderer, and it all goes. The KeyDown handler is our basic exit.

We make the Scene and Camera nodes file globals in this example. We have to do this for the Scene node, otherwise the reference drop when we leave Start() will result in deletion and a blank screen. We make the Camera node global so that we can access it from the event handler for the next example.

Spinning The Camera

Although this gives us a 3D view the fixed image does not prove much, but we can spin the camera using a simple event handler. In the Start() function then add an event call on each frame (the Update event).

SubscribeToEvent("Update", "HandleUpdate");
And in the update then we can call the camera Rotate() method:
void HandleUpdate(StringHash eventType, VariantMap& eventData) {
    cameraNode.Rotate(Quaternion(0.0, 0.2, 0.0));
}
And that'll perform a slow spin

Adding a Terrain Map

This is easy to do using a supplied heightmap image and texture from the Urho3D distribution: Simply remove the planeNode and planeObject and replace these with a call to MakeTerrain() and fill that function in as:

void MakeTerrain()
{
    Node@ terrainNode = thescene.CreateChild();
    terrainNode.position = Vector3(0.0f, 0.0f, 0.0f);

    Terrain@ terrain = terrainNode.CreateComponent("Terrain");
    terrain.patchSize = 64;
    terrain.spacing = Vector3(2.0f, 0.1f, 2.0f);
    terrain.smoothing = true;
    terrain.heightMap = cache.GetResource("Image", "Textures/HeightMap.png");
    terrain.material = cache.GetResource("Material", "Materials/Terrain.xml");
    terrain.occluder = true;
}

This creates a Node (terrainNode) and adds a Terrain Component object reference. The Terrain Component has a couple of simple parameters supplied:

  • patchSize: Must be a power of 2 between 4 and 128. This controls the level of Terrain detail generated by the engine, with lower values generating more (finer) data.
  • spacing: The scaling of the map to the world: the Y scales the map vertically (screen height) with X & Z as Width & Height
  • smoothing: Flag to smooth the output Terrain
  • occluder: Use Terrain when calculating Occlusion
  • material: This is a stock texture provided in the Urho3D resources, which consists of a mix of 3 textures
  • heightmap: In this case an 8 bit greyscale image

The source heightmap for Urho3D's terrain system must be square and be of a size of "2^n+1". For the default resource it's a 1025x1025 pixel 8 bit greyscale. Although this case is an 8 bit greyscale map, if the provided image has more channels it will be treated as a 16 bit heightmap with the Red and Green channels providing the 16 bit value when concatenated.

This material actually uses four images - The TerrainDetail1 to TerrainDetail3 DDS files provide the texture image and the TerrainWeights DDS file selects the texture at a location based on the R, G or B channel. In this case we're not too worried about this since we're just using it as a placeholder.

Generating a Higher Resolution Heightmap

This is actually fairly straightforward; generate an Image() instance, set the image to be 3 channel (although anything higher than 2 will work) and set the values in the pixel map on the Red and Green channel for height.

In setting the pixel values R is the lowest byte of the value, and G is the next, so setting up a manual stepping in the output image can be done as follows.

Image@ heightIm;

uint32 height1 = 0x000f;
uint32 height2 = 0x00ff;
uint32 height3 = 0x01ff;
uint32 height4 = 0x02ff;

void MakeHeightImage(int imgsz)
{
uint length = imgsz + 1;
  heightIm = Image();

  heightIm.SetSize(length, length, 3);
  uint32 heightColor_fine = 0;
  uint32 heightColor_rough = 0;

  for (int y = 0; y < heightIm.height; ++y) {
    for (int x = 0; x < heightIm.width; ++x) {
    uint32 heightval;

      if (y > (7* heightIm.height/10)) {
        heightval = height4;
      }
      else if (y > (6* heightIm.height/10)) {
        heightval = height3;
      }
      else if (y > (5* heightIm.height/10)) {
        heightval = height2;
      }
      else if (y > (3* heightIm.height/10)) {
        heightval = height1;
      }
      else {
        heightval = 0;
      }

      heightColor_fine = heightval & 0xff;
      heightColor_rough =  (heightval >>8)& 0xff;
      uint32 heightColor = (heightColor_fine << 8) | heightColor_rough;
      heightIm.SetPixelInt(x, y, heightColor);
    }
  }
}
This generates the image which we can request with a call like:
  MakeHeightImage(64);
And assign to the material used for the heightmap.
  terrain.heightMap = heightIm; 
And that's it...

Adding Fly Through Controls

This is very straightforward, and this is lifted directly from the sample code: It adds a basic Mouse look and WASD movement. It's invoked during the frame update, rather than the Key Down handler, and uses the input global to scan the keys and mouse and translate this to camera node rotation and position directly.

float yaw = 0;
float pitch =0;
void MoveCamera(float timeStep)
{
  const float MOVE_SPEED = 50.0f;
  const float MOUSE_SENSITIVITY = 0.5f;

  IntVector2 mouseMove = input.mouseMove;
  yaw += MOUSE_SENSITIVITY * mouseMove.x;
  pitch += MOUSE_SENSITIVITY * mouseMove.y;
  pitch = Clamp(pitch, -90.0f, 90.0f);

  cameraNode.rotation = Quaternion(pitch, yaw, 0.0f);

  if (input.keyDown[KEY_W])
      cameraNode.Translate(Vector3(0.0f, 0.0f, 1.0f) * MOVE_SPEED * timeStep);
  if (input.keyDown[KEY_S])
      cameraNode.Translate(Vector3(0.0f, 0.0f, -1.0f) * MOVE_SPEED * timeStep);
  if (input.keyDown[KEY_A])
      cameraNode.Translate(Vector3(-1.0f, 0.0f, 0.0f) * MOVE_SPEED * timeStep);
  if (input.keyDown[KEY_D])
      cameraNode.Translate(Vector3(1.0f, 0.0f, 0.0f) * MOVE_SPEED * timeStep);
}
This really is as simple as it looks: grab the mouseMove value and use it to calculate the value we put into camera rotation, then look for W,A,S and D and call cameraNode.Translate() to move the camera position. Timestep is pulled from the event information to ensure smooth movement but that's it. Next up we'll import some actual levels...