Jump to content
  • entries
    944
  • comments
    5,899
  • views
    929,506

Streaming Terrain in Leadwerks Game Engine 5


Josh

1,472 views

 Share

The terrain system in Leadwerks Game Engine 4 allows terrains up to 64 square kilometers in size. This is big enough for any game where you walk and most driving games, but is not sufficient for flight simulators or space simulations. For truly massive terrain, we need to be able to dynamically stream data in and out of memory, at multiple resolutions, so we can support terrains bigger than what would otherwise fit in memory all at once.

The next update of Leadwerks Game Engine 5 beta supports streaming terrain, using the following command:

shared_ptr<StreamingTerrain> CreateStreamingTerrain(shared_ptr<World> world, const int resolution, const int patchsize, const std::wstring& datapath, const int atlassize = 1024, void FetchPatchInfo(TerrainPatchInfo*) = NULL)

Let's looks at the parameters:

  • resolution: Number of terrain points along one edge of the terrain, should be power-of-two.
  • patchsize: The number of tiles along one edge of a terrain piece, should be a power-of-two number, probably 64 or 128.
  • datapath: By default this indicates a file path and name but can be customized.
  • atlassize: Width and height of texture atlas texture data is copied into. 1024 is usually fine.
  • FetchPatchInfo: Optional user-defined callback to override the default data handler.

A new Lua sample is included that creates a streaming terrain:

local terrain = CreateStreamingTerrain(world, 32768, 64, "Terrain/32768/32768")
terrain:SetScale(1,1000,1)

The default fetch patch function can be used to make your own data handler. Here is the default function, which is probably more complex than what you need for streaming GIS data. The key parts to note are:

  • The TerrainPatchInfo structure contains the patch X and Y position and the level of detail.
  • The member patch->heightmap should be set to a pixmap with format TEXTURE_R16.
  • The member patch->normalmap should be set to a pixmap with format TEXTURE_RGBA (for now). You can generate this from the heightmap using MakeNormalMap().
  • The scale value input into the MakeNormalMap() should be the terrain vertical scale you intend to use, times two, divided by the mipmap level. This ensures normals are calculated correctly at each LOD.
  • For height and normal data, which is all that is currently supported, you should use the dimensions patchsize + 1, because the a 64x64 patch, for example, uses 65x65 vertices.
  • Don't forget to call patch->UpdateBounds() to calculate the AABB for this patch.
  • The function must be thread-safe, as it will be called from many different threads, simultaneously.
	void StreamingTerrain::FetchPatchInfo(TerrainPatchInfo* patch)
	{
		//User-defined callback
		if (FP_FETCH_PATCH_INFO != nullptr)
		{
			FP_FETCH_PATCH_INFO(patch);
			return;
		}

		auto stream = this->stream[TEXTURE_DISPLACEMENT];
		if (stream == nullptr) return;
		int countmips = 1;
		int mw = this->resolution.x;
		while (mw > this->patchsize)
		{
			countmips++;
			mw /= 2;
		}
		int miplevel = countmips - 1 - patch->level;
		Assert(miplevel >= 0);
		uint64_t mipmapsize = Round(this->resolution.x * 1.0f / pow(2.0f, miplevel));
		auto pos = mipmappos[TEXTURE_DISPLACEMENT][miplevel];
		uint64_t rowpos;
		patch->heightmap = CreatePixmap(patchsize + 1, patchsize + 1, TEXTURE_R16);
		uint64_t px = patch->x * patchsize;
		uint64_t py = patch->y * patchsize;
		int rowwidth = patchsize + 1;
		for (int ty = 0; ty < patchsize + 1; ++ty)
		{
			if (py + ty >= mipmapsize)
			{
				patch->heightmap->CopyRect(0, patch->heightmap->size.y - 2, patch->heightmap->size.x, 1, patch->heightmap, 0, patch->heightmap->size.y - 1);
				continue;
			}
			if (px + rowwidth > mipmapsize) rowwidth = mipmapsize - px;
			rowpos = pos + ((py + ty) * mipmapsize + px) * 2;
			streammutex[TEXTURE_DISPLACEMENT]->Lock();
			stream->Seek(rowpos);
			stream->Read(patch->heightmap->pixels->data() + (ty * (patchsize + 1) * 2), rowwidth * 2);
			streammutex[TEXTURE_DISPLACEMENT]->Unlock();
			if (rowwidth < patchsize + 1)
			{
				patch->heightmap->WritePixel(patch->heightmap->size.x - 1, ty, patch->heightmap->ReadPixel(patch->heightmap->size.x - 2, ty));
			}
		}
		patch->UpdateBounds();

		stream = this->stream[TEXTURE_NORMAL];
		if (stream == nullptr)
		{
			patch->normalmap = patch->heightmap->MakeNormalMap(scale.y * 2.0f / float(1 + miplevel), TEXTURE_RGBA);
		}
		else
		{
			pos = mipmappos[TEXTURE_NORMAL][miplevel];
			Assert(pos < stream->GetSize());
			patch->normalmap = CreatePixmap(patchsize + 1, patchsize + 1, TEXTURE_RGBA);
			rowwidth = patchsize + 1;
			for (int ty = 0; ty < patchsize + 1; ++ty)
			{
				if (py + ty >= mipmapsize)
				{
					patch->normalmap->CopyRect(0, patch->normalmap->size.y - 2, patch->normalmap->size.x, 1, patch->normalmap, 0, patch->normalmap->size.y - 1);
					continue;
				}
				if (px + rowwidth > mipmapsize) rowwidth = mipmapsize - px;
				rowpos = pos + ((py + ty) * mipmapsize + px) * 4;
				Assert(rowpos < stream->GetSize());
				streammutex[TEXTURE_NORMAL]->Lock();
				stream->Seek(rowpos);
				stream->Read(patch->normalmap->pixels->data() + uint64_t(ty * (patchsize + 1) * 4), rowwidth * 4);
				streammutex[TEXTURE_NORMAL]->Unlock();
				if (rowwidth < patchsize + 1)
				{
					patch->normalmap->WritePixel(patch->normalmap->size.x - 1, ty, patch->normalmap->ReadPixel(patch->normalmap->size.x - 2, ty));
				}
			}
		}
	}

There are some really nice behaviors that came about naturally as a consequence of the design.

  • Because the culling algorithm works its way down the quad tree with only known patches of data, the lower-resolution sections of terrain will display first and then be replaced with higher-resolution patches as they are loaded in.
  • If the cache gets filled up, low-resolution patches will be displayed until the cache clears up and more detailed patches are loaded in.
  • If all the patches in one draw call have not yet been loaded, the previous draw call's contents will be rendered instead.

As a result, the streaming terrain is turning out to be far more robust than I was initially expecting. I can fly close to the ground at 650 MPH (the speed of some fighter jets) with no problems at all.

There are also some issues to note:

  • Terrain still has cracks in it.
  • Seams in the normal maps will appear along the edges, for now.
  • Streaming terrain does not display material layers at all, just height and normal.

But there is enough to start working with it and piping data into the system.

 

 Share

0 Comments


Recommended Comments

There are no comments to display.

Guest
Add a comment...

×   Pasted as rich text.   Paste as plain text instead

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.

×
×
  • Create New...