Rendering Terrain

Sky

As it looks now, the scenario is hard on the eyes.

Provide proper sky color by defining a sky color callback:

// Asks the scenario for sky colors according to current time of day and weather.
void UAW_CALL getScenarioSkyColors(
	UAW_Extension_Scenario const * scenario,
	UAW_Scenario_SkyColors *       result,
	UAW_Clock                      utc
) {
	// HINT: Use the “utc” parameter for day-night cycles!
	result->whitePoint   = { 1.f, 1.f, 1.f };
	result->zenith       = { 0.f, 0.f, 0.05f };   // zenith is dark blue
	result->horizon      = { 0.02f, 0.f, 0.04f }; // horizon is dark purple
	result->horizonSunny = { 0.1f, 0.f, 0.2f };   // brighter so towards sun
}

UAW_Extension_Scenario * UAW_CALL createScenario(
	void *                       extensionData,
	UAW_API_ScenarioPopulate *   uaw,
	UAW_Extension_ScenarioID     id,
	UAW_Extension_Scenario_API * methods
) try {
	// Pass our callbacks:
	methods->destroy          = &destroyScenario;
	methods->prepareRendering = &prepareScenarioRendering;
	methods->getSkyColors     = &getScenarioSkyColors;
	return new UAW_Extension_Scenario;
} catch(...) {
	return nullptr;
}

Easier on the eyes:

Simple Graphics

The easiest graphics API in UAW is Simple Graphics:

This is enough for 90s-level graphics, as in many classic games with baked lighting. It’s sufficient if you don’t need dynamic day/night cycles in your scenario.

According types in the API have Simple in their name.

Static vs. Dynamic Geometry

UAW supports static and dynamic graphics. Static graphics is for things that don’t change – terrain, typically. The graphics callback is called once before the graphics is displayed to capture whatever is shown, and UAW will replay this later on.

RationaleThis allows UAW to optimize the draw process, e.g. via removing redundant state changes or via sorting polygons by texture (thus reducing state changes even further). In fact, all modern graphics APIs are based on command lists.

Dynamic graphics is for things that change constantly – flags waving in the wind, water surfaces, etc. The graphics callback is called every time that thing is displayed on the screen.

Both can be combined: Parts of the terrain can be static, others can be dynamic.

In UAW, static terrain geometry is what you draw when a terrain view changes. This gives you a possibility to determine the correct levels of detail and the amount of data to render. Dynamic terrain geometry, on the other hand, is drawn during the terrain rendering callback.

Vertex Buffer

In order to get familiar with the rendering API, we will draw a black quad below the viewer.

Rendering a quad requires a vertex buffer. It is used for the whole lifetime of the terrain and for every terrain view, so add it to the definition of UAW_Extension_Terrain:

struct UAW_Extension_Terrain {
	UAW_Graphics_SimpleVertices3D * tileVertices;
};

Initialize it when creating the terrain:

UAW_Extension_Terrain * UAW_CALL loadTerrain(
	UAW_Extension *             extension,
	UAW_API_TerrainLoad *       uaw,
	UAW_Extension_TerrainID     id,
	UAW_Extension_Terrain_API * result
) try {

	…

	// Generate a quad that will be used as tile.

	UAW_Graphics_SimpleVertex3D const vertices[] = {
		{ { -tileSize / 2, -tileSize / 2, 0.f }, { 255, 255, 255, 255 }, { 0.f, 0.f } },
		{ { -tileSize / 2,  tileSize / 2, 0.f }, { 255, 255, 255, 255 }, { 0.f, 1.f } },
		{ {  tileSize / 2, -tileSize / 2, 0.f }, { 255, 255, 255, 255 }, { 1.f, 0.f } },
		{ {  tileSize / 2,  tileSize / 2, 0.f }, { 255, 255, 255, 255 }, { 1.f, 1.f } }
	};

	UAW_Graphics_SimpleVertex3D const quad[] = {
		vertices[0], vertices[2], vertices[1],
		vertices[1], vertices[2], vertices[3]
	};

	auto tileVertexBuffer = uaw->buffer(&quad[0], &quad[0] + 6);

	return new UAW_Extension_Terrain{ tileVertexBuffer };
} catch(...) {
	return nullptr;
}

When the terrain view changes, draw this quad. This requires specifying a transformation matrix from whatever space the vertices are in to world coordinates:

void UAW_CALL onTerrainViewChange(
	UAW_Extension_Terrain *     terrain,
	UAW_Extension_TerrainView * view,
	UAW_API_TerrainGraphics *   uaw,
	int                         tileIndexNorth,
	int                         tileIndexEast,
	int                         rangeInTiles
) {
	UAW_Matrix_4x3_float const identity = { {
		{ 1, 0, 0 },
		{ 0, 1, 0 },
		{ 0, 0, 1 },
		{ 0, 0, 0 }
	} };
	uaw->setTransformation(identity);
	uaw->bind(terrain->tileVertices);
	uaw->renderTriangles(0, 2);
}

UAW will capture this command and repeat it whenever the view is drawn. As a result, a black quad can be seen:

Note that this quad follows the viewer in huge leaps. This is because UAW moves the coordinate origin of the world to always match the tile below the viewer.

Texture

The tile is black even though vertex colors define the quad as white. This is because vertex color is multiplied with texture color, but no texture is set.

Create a texture in addition to the vertex buffer:

struct UAW_Extension_Terrain {
	UAW_Graphics_SimpleVertices3D * tileVertices;
	UAW_UI_Texture *                tileTexture;
};
	auto tileVertexBuffer = uaw->buffer(&quad[0], &quad[0] + 6);

	// Generate a texture consisting of only one reddish pixel.
	unsigned int pixel = 0xffee7744; // AARRGGBB – opaque orange

	auto tileTexture = uaw->createTexture(1, 1, false, &pixel, sizeof pixel);

	return new UAW_Extension_Terrain{ tileVertexBuffer };
	return new UAW_Extension_Terrain{ tileVertexBuffer, tileTexture };

Use this texture while rendering:

	uaw->bind(terrain->tileVertices);
	uaw->bind(terrain->tileTexture);
	uaw->setTransformation(identity);
	uaw->renderTriangles(0, 2);

Create a larger texture with some kind of grid on it. In a real extension, it would of course be loaded from a file or created procedurally.

	// Generate a texture consisting of only one reddish pixel.
	unsigned int pixel = 0xffee7744; // AARRGGBB – opaque orange

	auto tileTexture = uaw->createTexture(1, 1, false, &pixel, sizeof pixel);

	// Generate a black texture with green grid lines.
	unsigned int pixels[256 * 256];
	for(auto & pixel : pixels)
		pixel = 0xff000000; // AARRGGBB – opaque black
	for(auto i = 0; i < 256; ++i) {
		pixels[i      ] = 0xff00ff44;
		pixels[i * 256] = 0xff00ff44;
	}

	auto tileTexture = uaw->createTexture(256, 256, true, &pixels[0], 256 * sizeof pixels[0]);

It looks like this:

When tiled, the lines meet and a grid is formed.

Rendering the Whole Terrain

Upscaling is simple: Instead of drawing just the tile below the viewer, draw every tile in sight. The transformation matrix can be used to place the quad wherever we need it.

void UAW_CALL onTerrainViewChange(
	UAW_Extension_Terrain *     terrain,
	UAW_Extension_TerrainView * view,
	UAW_API_TerrainGraphics *   uaw,
	int                         tileIndexNorth,
	int                         tileIndexEast,
	int                         rangeInTiles
) {
	uaw->bind(terrain->tileVertices);
	uaw->bind(terrain->tileTexture);
	UAW_Matrix_4x3_float const identity = { {
		{ 1, 0, 0 },
		{ 0, 1, 0 },
		{ 0, 0, 1 },
		{ 0, 0, 0 }
	} };
	uaw->setTransformation(identity);
	uaw->renderTriangles(0, 2);
	for(int offsetN = -rangeInTiles; offsetN <= rangeInTiles; ++offsetN) {
		for(int offsetE = -rangeInTiles; offsetE <= rangeInTiles; ++offsetE) {
			UAW_Matrix_4x3_float const transformation = { {
				{ 1, 0, 0 },
				{ 0, 1, 0 },
				{ 0, 0, 1 },
				{ offsetN * tileSize, offsetE * tileSize, 0 }
			} };
			uaw->setTransformation(transformation);
			uaw->renderTriangles(0, 2);
		}
	}
}

Further Steps

Extensions may want to create different geometry for tiles. The exact way to it depends on the use case: If tiles often repeat in the terrain, it’s probably best to create additional vertex buffers and textures in the terrain structure.

If, on the other hand, every tile has unique geometry and texture, it is best to store it in UAW_Extension_TerrainTile instead. Create the vertices/texture during loadTerrainTileAt() and release it during releaseTerrainTile().

Finding out which tile to draw in which place during onTerrainViewChange() – e.g. for looking up a tile map – is straightforward: The viewer’s tile is given in tileIndexNorth and tileIndexEast.

The Ace Combat 3 extension is built exclusively on the API presented in this chapter.

Sample Project

Download an updated sample project here.