Rendering Terrain Part 3 – Drawing the Height map in 2D

Edit

It’s been brought to my attention that I changed my code to display shadows and have not yet changed it back. If you download the code and it displays something that isn’t a height map, go to RenderTerrain2dPS.hlsl and you’ll probably see something like this:

Texture2D<float> heightmap : register(t0);
Texture2D<float> shadowmap : register(t1);
SamplerState hmsampler : register(s0);

struct VS_OUTPUT {
	float4 pos : SV_POSITION;
	float2 tex : TEXCOORD;
};

float4 main(VS_OUTPUT input) : SV_TARGET {
//	float height = heightmap.Sample(hmsampler, input.tex);
//	return float4(height, height, height, 1);
	float shadow = shadowmap.Sample(hmsampler, input.tex);
	return float4(shadow, 0, 0, 1);
}

Just remove the comments and comment out or delete the part about shadows to get back to rendering the height map.

End of Edit

Drawing

Now that we’ve initialized the Device and the Pipeline, we’re free to start rendering frames.
In our Scene object’s Draw method, we will need to let the Graphics object know to reset the pipeline in preparation for the next frame.

mpGFX->ResetPipeline();

Before it can do that, the Graphics object has to check the fence we mentioned last time to confirm that the last frame rendered to our current back buffer is actually done. If we are rendering fast enough, it may not be and we may need to wait.

void ResetPipeline() {
	NextFrame(); // this was WaitOnBackBuffer in the Braynzar Soft Tutorials.

	// reset command allocator and command list memory.
	if (FAILED(maCmdAllocators[mBufferIndex]->Reset())) {
		throw GFX_Exception("CommandAllocator Reset failed on UpdatePipeline.");
	}
	if (FAILED(mpCmdList->Reset(maCmdAllocators[mBufferIndex], NULL))) {
		throw GFX_Exception("CommandList Reset failed on UpdatePipeline.");
	}
}

void NextFrame() {
	// set the buffer index to point to the current back buffer.
	mBufferIndex = mpSwapChain->GetCurrentBackBufferIndex();

	// if the current value returned by the fence is less than the current fence value for this buffer, then we know the GPU is not done with the buffer, so wait.
	if (maFences[mBufferIndex]->GetCompletedValue() < maFenceValues[mBufferIndex]) { if (FAILED(maFences[mBufferIndex]->SetEventOnCompletion(maFenceValues[mBufferIndex], mFenceEvent))) {
			throw GFX_Exception("Failed to SetEventOnCompletion for fence in WaitOnBackBuffer.");
		}

		WaitForSingleObject(mFenceEvent, INFINITE);
	}

	++maFenceValues[mBufferIndex];
}

We then need to ready the back buffer to render. The commands for this are just commands that can be added directly to the Command List, which the Scene has, but they require knowledge of which buffer we’re on, so the methods are part of the Graphics object. Unfortunately, so does setting the clear colour.

// set the back buffer as the render target for the provided command list.
void SetBackBufferRender(ID3D12GraphicsCommandList* cmdList, const float clearColor[4]) {
	// set back buffer to render target.
	cmdList->ResourceBarrier(1, &CD3DX12_RESOURCE_BARRIER::Transition(maBackBuffers[mBufferIndex], D3D12_RESOURCE_STATE_PRESENT, D3D12_RESOURCE_STATE_RENDER_TARGET));

	// get the handle to the back buffer and set as render target.
	CD3DX12_CPU_DESCRIPTOR_HANDLE rtvHandle(mpRTVHeap->GetCPUDescriptorHandleForHeapStart(), mBufferIndex, mRTVDescSize);
	cmdList->OMSetRenderTargets(1, &rtvHandle, false, NULL);

	cmdList->ClearRenderTargetView(rtvHandle, clearColor, 0, NULL);
}

We can now set the viewport and scissor rectangle. These together define the area of the window where we’ll actually be rendering our frame. So we can actually define different viewports and rectangles and render different things in them. For instance, if we wanted a mini-map in the corner. A HUD could potentially be rendered separately from the scene in a series of viewports and those images would then be blended on top of the main scene rendering. We’re just rendering once per frame to the entire screen, so I define the viewport on initialization. It still needs to be set every frame though. This is done by the scene as we’re now at the bit that doesn’t require anything other than the command list.

void SetViewport() {
	ID3D12GraphicsCommandList* cmdList = mpGFX->GetCommandList();
	cmdList->RSSetViewports(1, &mViewport);
	cmdList->RSSetScissorRects(1, &mScissorRect);
}

Next, we draw our objects. Since we only have our terrain, this is pretty straight forward:

void Draw2D(ID3D12GraphicsCommandList* cmdList) {
	cmdList->SetPipelineState(mpPSO);
	cmdList->SetGraphicsRootSignature(mpRootSig);
	cmdList->IASetPrimitiveTopology(D3D_PRIMITIVE_TOPOLOGY_TRIANGLELIST); // describe how to read the vertex buffer.
	ID3D12DescriptorHeap* heaps[] = { mpSRVHeap };
	cmdList->SetDescriptorHeaps(1, heaps);
	cmdList->SetGraphicsRootDescriptorTable(0, mpSRVHeap->GetGPUDescriptorHandleForHeapStart());
	cmdList->DrawInstanced(3, 1, 0, 0);
}

In a larger application, we probably wouldn’t want to set the PSO for each object. If there are multiple objects that use the same shader pipeline, we would set the PSO once and then pass each object in a batch. If the textures are different but the pipeline is otherwise the same, then we’d just have to change the Root Signature per object. If they are identical objects, we wouldn’t even need to do that.
If we had vertex or index buffers, they’d need to be bound here, before the DrawInstanced() call. In our case, we don’t need to send any vertex data to the GPU, so there are no vertex buffers or index buffers. We simply tell the GPU that we want 3 vertices in a triangle list layout. So basically we want 1 triangle. We’ll talk about the shaders soon, but let’s finish up the Direct3D draw calls before we move to HLSL.

Once we’ve finished adding all of the object render commands to the command list, we need to add the commands to swap buffers and present the buffer containing this frame. As with setting the back buffer to render, to present will require data we’ve limited to the Graphics object, so this method is called by Scene.

// set the back buffer as presenting for the provided command list.
void SetBackBufferPresent(ID3D12GraphicsCommandList* cmdList) {
	// switch back buffer to present state.
	cmdList->ResourceBarrier(1, &CD3DX12_RESOURCE_BARRIER::Transition(maBackBuffers[mBufferIndex], D3D12_RESOURCE_STATE_RENDER_TARGET, D3D12_RESOURCE_STATE_PRESENT));
}

We now need to close the command list and signal to the Graphics object that it may send the list to the GPU for rendering.

/* In Scene Object. */
// close the command list.
if (FAILED(mpGFX->GetCommandList()->Close())) {
	throw GFX_Exception("CommandList Close failed.");
}

/* Graphics object method */
void Graphics::Render() {
	// load the command list.
	ID3D12CommandList* lCmds[] = { mpCmdList };
		
	// execute
	mpCmdQ->ExecuteCommandLists(__crt_countof(lCmds), lCmds);

	// Add Signal command to set fence to the fence value that indicates the GPU is done with that buffer. maFenceValues[i] contains the frame count for that buffer.
	if (FAILED(mpCmdQ->Signal(maFences[mBufferIndex], maFenceValues[mBufferIndex]))) {
		throw GFX_Exception("CommandQueue Signal Fence failed on Render.");
	}

	// swap the back buffers.
	if (FAILED(mpSwapChain->Present(0, 0))) {
		throw GFX_Exception("SwapChain Present failed on Render.");
	}
}

If we had multiple threads, we would need each thread to fill up it’s own command list. We’d then need to make sure that the command lists are added to the array in the correct order to render correctly. Perhaps we have one thread loading a shader for shadow maps, another for an environment map to create reflective surfaces, and the last one to put it all together. We could potentially build the command lists in parallel, but they need to be run in the correct order or our program will break1.

The Shaders

Our shaders are super simple right now. Our vertex shader just needs to define the locations for our 3 vertices such that we wind up with a triangle that completely covers the entire view. It will also generate texture coordinates so that the height map is mapped exactly to the visible area of the window. It will wind up a bit stretched unless our height map is exactly the same dimensions as our window. I’m ok with that.

struct VS_OUTPUT {
	float4 pos : SV_POSITION;
	float2 tex : TEXCOORD;
};

VS_OUTPUT main(uint input : SV_VERTEXID) {
	VS_OUTPUT output;

	output.pos = float4(float2((input << 1) & 2, input == 0) * float2(2.0f, -4.0f) + float2(-1.0f, 1.0f), 0.0f, 1.0f);
	output.tex = float2((output.pos.x + 1) / 2, (output.pos.y + 1) / 2);

	return output;
}

Our pixel shader is, perhaps, even simpler. We’ve passed it a height map and texture coordinates so we know exactly where in the texture this pixel is located. We just have to look up the value and return it.

Texture2D<float4> heightmap : register(t0);
SamplerState hmsampler : register(s0);

struct VS_OUTPUT {
	float4 pos : SV_POSITION;
	float2 tex : TEXCOORD;
};

float4 main(VS_OUTPUT input) : SV_TARGET {
	return heightmap.Sample(hmsampler, input.tex);
}

Here’s some example shots with different heightmaps:
2dscreenshot3

2dscreenshot4

2dscreenshot
So that pretty much wraps up rendering our height maps in 2D. The code could be a bit cleaner. I’ll work on that. But my next goal is to get the terrain rendered in 3D, with the camera back far enough to see the entire thing.

The latest code for this project can be found on GitHub.

Traagen