Sunday, 16 April 2017

Loading the Levels and Texturing

Loading The Map

This will bring together the stuff we've done so far to load up the levels from the I76 mission files.

Initially we copy the contents of the installed Mission directory under the Urho3D Data directory so we can locate the target files easily. Go to the the Urho3D Data directory, create a subdirectory Imports and copy the mission files into that.

The AngelScript code is available in this zip file. Unzip the file contents, and run the viewer with a line like "Urho3DPlayer ./levelread.as -w". This code doesn't do much in the way of error checking, so make sure that under the Urho3D Data directory you have a directory "Imports" and that the mission files (.msn and .ter) are contained in it. A description of how the code works is below.

Parsing The Level

File Load

We use the standard Urho3D file dialog system to locate a target .msn file, and since this is all boilerplate there's nothing special to notice here.

However to simplify interaction with this then we isolate it to a separate class for handling, and we have the dialog generate an event when it's complete to feed back to the main code.

So we have a class "OurFileSelector" which uses the stock FileSelector to generate a pop up window. The HandleFileSelected event is used to flag a selection and we create an event using a VariantMap and SendEvent() to signal the file chosen.

In the main code we've added the call to launch the selector with fselect.PickFile(), and to pick up the completion we add an event handler:

SubscribeToEvent("FileChosen", "HandleFileChosen");
We can just define this event and handler in either order without any setup and rely on the dispatch to take care of it - this is fast when prototyping, but obviously error prone. Also we use a global signal here, since there's only one file selector at a time and we don't try and associate a handler with a specific object.

Reading and Parsing

The actual file load resembles the code we generated before for the C++ level to Image map conversion process, although a little cleaner since we know what we're doing.

In this case the Urho3D File library provides routines for loading correct data sizes from the target file. The only specific piece of code we need is the format mangling to get the heightmap information into the right format.

In this case we extract the height data as

  • Mask out the upper 4 bits for the terrain flags
  • Scale up the 12 bit range to 16 bits by left shifting 4
  • Split out the Upper and Lower 8 bits as the rough & fine height components
  • Form the final color as :
    • Blue for Flags
    • Green as Fine detail (Low Byte of height)
    • Red as Rough detail (Top Byte of height)
  • The final 32 bit value is packed as BGR

The heightmap will use R and G for determining the height, but will ignore the Blue channel, however we will use this to modify the texture later.

This code fragment is:

      uint32 heightval;
      uint32 terrain_flags;

        heightval = data[cursor++];
        terrain_flags = heightval & 0xf000;
        terrain_flags = (terrain_flags << 4) &0xff0000;
        heightval = heightval & 0x0fff;         // Mask top four terrain bits....
        heightval = (heightval << 4) & 0xfff0;  // Scale up to full 16 bit range

        uint32 heightColor_fine = heightval & 0xff;
        uint32 heightColor_rough =  (heightval >>8)& 0xff;
        uint32 heightColor = terrain_flags | (heightColor_fine << 8) | heightColor_rough;
        heightIm.SetPixelInt(y, x, heightColor); //Note X/Y transpose to rotate....

We also restrict the level heightmap size to rendering the in use zones, rather than trying to handle the full I76 80*80 zone map to keep performance sane. So combining this logic with the existing heightmap render gets us a functional level render.

Adding a Debug HUD

Urho3D has a built in debug overlay which can be used to report the current state of the engine and this is trivial to enable, with the code

  engine.CreateDebugHud();
  debugHud.defaultStyle = cache.GetResource("XMLFile", "UI/DefaultStyle.xml");
And we can then turn this on and off with debugHud.ToggleAll()

Adding a SkyBox

Again, a simple operation which adds a node with a Skybox component to the root scene. The Skybox Model and Material are the stock Urho3D versions.

    skybxnode = thescene.CreateChild();

    Skybox@ skybox = skybxnode.CreateComponent("Skybox");
    skybox.model = cache.GetResource("Model", "Models/Box.mdl");
    skybox.material = cache.GetResource("Material", "Materials/Skybox.xml");

Putting it together

You get this: From the bottom of the bowl in level m01.msn

Modifying the terrain

We've put the terrain flags in the blue channel, and we can use this information to manually tweak the texture we put on the map.

For this version then what we'll try is:

  • Create a new image matching the height map size
  • Fill in this new image with colours based on the terrain flags
Then
  • Create a default stone material
  • Get Texture used for the Diffuse layer of this material
  • Change the source Image data to our coloured bitmap
  • Update the material information

It's worth taking a moment to look at how Urho3d uses Texture Units: By default Urho3D has five predefined texture unit types: diffuse, normal, specular emissive and environment. The stone texture XML has the lines allocating these units for the diffuse and normal map.

In this case we're going to load the texture and replace the Diffuse Image with our own, and we can access the material and get a reference to the relevant texture unit. The Material class defines the textures in the material as an Array of Texture called "textures", and when we load a material then we can reference the target unit with textures[TU_DIFFUSE].

In this case we're actually going to grab it as a Texture2D rather than a Texture type: Texture2D is a subclass of Texture, but it will allow us to set a new Image reference. This is slightly confusing since the AngelScript header doesn't explicitly flag this relationship in the way that the C++ code does, but the sample code demonstrates the use of the subclass type for these cases.

(Note that additional units are available as "desktop only" features, and the units can be used by numeric references and remapped as the Terrain shader does, but that's detail we don't need to go into right now).

To get this change we will replace the terrainblock.material assignment in the level reader with this code: The code is:

Material@ renderMaterial = cache.GetResource("Material", "Materials/Stone.xml");
Texture2D@ inTex = renderMaterial.textures[TU_DIFFUSE];

  newImg.SetSize(msn.FinalHeightMap.width, msn.FinalHeightMap.height, 3);
  newImg.ClearInt(0x4f);

    for (int srcx = 0; srcx < msn.FinalHeightMap.width; srcx++)
      for (int srcy = 0; srcy < msn.FinalHeightMap.height; srcy++) {
      int v = msn.FinalHeightMap.GetPixelInt(srcx,srcy);
        if ((v & 0xff0000) != 0) {
          if ((v & 0x010000) != 0)
            newImg.SetPixelInt(srcx, srcy, 0x770000); // Rough
          else if ((v & 0x020000) != 0)
            newImg.SetPixelInt(srcx, srcy, 0x007700); // Dirt
          else if ((v & 0x040000) != 0)
            newImg.SetPixelInt(srcx, srcy, 0x333333); // Tarmac
          else 
            newImg.SetPixelInt(srcx, srcy, 0xffffff); // Other?

        }
        else {
          newImg.SetPixelInt(srcx, srcy, 0x888888); // Sand
        }
      }
  inTex.SetData(newImg);
  renderMaterial.textures[TU_DIFFUSE] = inTex;
  terrainblock.material = renderMaterial;
And this gives us the following from the training mission....
Which is less pretty, but more useful in highlighting the terrain type distribution.