Creating a Shader class

Started by
3 comments, last by Seabolt 10 years, 6 months ago

Hello! I am working on a class for my game engine called NXShader which will allow me to load my shaders easily like this:


NXShader(HWND*, WCHAR*, const string&, const string&);

The next thing I would like to add is the ability to dynamically set shader parameters if that makes any sense. Kind of similar to how shader parameters were handled in XNA:


Effect.Parameters["parameter"].SetValue(value); 

I want to be able to do something like this with my NXShader class:


NXShader* shader = new NXShader(nullptr, L"Texture.fx", "VS", "PS");
shader->SetParameter(parameter name, value);

But I am not actually sure about to do this. The class is currently set up like this:

Header:


struct MatrixBufferType
	{
		XMMATRIX World;
		XMMATRIX View;
		XMMATRIX Projection;
	};

	struct NEXENGINE_API NXShader
	{
		public:
			NXShader(){}
			NXShader(HWND*, WCHAR*, const string&, const string&);
			HRESULT Initialize(HWND*);
			bool SetShaderParameters(XMMATRIX&, XMMATRIX&, XMMATRIX&);
			bool SetShaderParameters(XMMATRIX&, XMMATRIX&, XMMATRIX&, NXTexture2D*);
			void Dispose();

			HRESULT CompileShaderFromFile(WCHAR*, LPCSTR, LPCSTR, ID3DBlob**);

			WCHAR* TargetProfile = L"_4_0";
			std::string VertexShaderEntry;
			std::string PixelShaderEntry;

			WCHAR* Name;
			ID3D11VertexShader* VertexShader;
			ID3D11PixelShader* PixelShader;
			ID3D11InputLayout* Layout;
			ID3D11Buffer* MatrixBuffer;
	};

Source:


#include "stdafx.h"
#include "NXShader.h"
#include <d3dcompiler.h>
#include "NXTypes.h"

#pragma comment(lib, "d3d11.lib")
#pragma comment(lib, "d3dcompiler.lib")

namespace NexEngine
{
	NXShader::NXShader(HWND* wnd, WCHAR* shaderName, const string& vsEntry, const string& psEntry)
	{
		Name = shaderName;
		VertexShaderEntry = vsEntry;
		VertexShader = 0;
		PixelShaderEntry = psEntry;
		PixelShader = 0;
		Layout = 0;
		MatrixBuffer = 0;

		HRESULT result;

		result = Initialize(wnd);
		if (FAILED(result))
			return;
	}

	HRESULT NXShader::Initialize(HWND* hwnd)
	{
		HRESULT result;
		D3D11_BUFFER_DESC matrixBufferDesc;
		ID3DBlob* pVSBlob = nullptr;
		result = CompileShaderFromFile(Name, VertexShaderEntry.c_str(), "vs_4_0", &pVSBlob);
		if (FAILED(result))
		{
			MessageBox(nullptr,
				L"The FX file cannot be compiled.  Please run this executable from the directory that contains the FX file.", L"Error", MB_OK);
			exit(0);
			return result;
		}

		result = NXGPU::GetDevice()->CreateVertexShader(pVSBlob->GetBufferPointer(), pVSBlob->GetBufferSize(), nullptr, &VertexShader);
		if (FAILED(result))
		{
			pVSBlob->Release();
			MessageBox(nullptr, L"Failed To Create Vertex Shader.", L"Error", MB_OK);
			return result;
		}

		ID3DBlob* pPSBlob = nullptr;
		result = CompileShaderFromFile(Name, PixelShaderEntry.c_str(), "ps_4_0", &pPSBlob);
		if (FAILED(result))
		{
			MessageBox(nullptr,
				L"The FX file cannot be compiled.  Please run this executable from the directory that contains the FX file.", L"Error", MB_OK);
			exit(0);
			return result;
		}

		result = NXGPU::GetDevice()->CreatePixelShader(pPSBlob->GetBufferPointer(), pPSBlob->GetBufferSize(), nullptr, &PixelShader);
		pPSBlob->Release();
		if (FAILED(result))
		{
			MessageBox(nullptr, L"Failed To Create Pixel Shader.", L"Error", MB_OK);
			return result;
		}
		pPSBlob = 0;

		result = NXGPU::GetDevice()->CreateInputLayout(NXVertex::InputElements, NXVertex::InputElementCount, pVSBlob->GetBufferPointer(),
			pVSBlob->GetBufferSize(), &Layout);


		pVSBlob->Release();
		if (FAILED(result))
		{
			MessageBox(nullptr, L"Failed To Create Input Layout.", L"Error", MB_OK);
			exit(0);
			return result;
		}
		pVSBlob = 0;
		
		matrixBufferDesc.Usage = D3D11_USAGE_DYNAMIC;
		matrixBufferDesc.ByteWidth = sizeof(MatrixBufferType);
		matrixBufferDesc.BindFlags = D3D11_BIND_CONSTANT_BUFFER;
		matrixBufferDesc.CPUAccessFlags = D3D11_CPU_ACCESS_WRITE;
		matrixBufferDesc.MiscFlags = 0;
		matrixBufferDesc.StructureByteStride = 0;

		result = NXGPU::GetDevice()->CreateBuffer(&matrixBufferDesc, NULL, &MatrixBuffer);
		if (FAILED(result))
		{
			MessageBox(nullptr, L"Faield to Create Matrix Buffer", L"Error", MB_OK);
			exit(0);
			return result;
		}

		return S_OK;
	}

	bool NXShader::SetShaderParameters(XMMATRIX& view, XMMATRIX& world, XMMATRIX& proj)
	{
		HRESULT result;
		D3D11_MAPPED_SUBRESOURCE mappedResource;
		MatrixBufferType* dataPtr;
		unsigned int bufferNumber;

		world = XMMatrixTranspose(world);
		view = XMMatrixTranspose(view);
		proj = XMMatrixTranspose(proj);

		result = NXGPU::GetDeviceContext()->Map(MatrixBuffer, 0, D3D11_MAP_WRITE_DISCARD, 0, &mappedResource);
		if (FAILED(result))
		{
			return false;
		}

		dataPtr = (MatrixBufferType*) mappedResource.pData;

		dataPtr->World = world;
		dataPtr->View = view;
		dataPtr->Projection = proj;

		NXGPU::GetDeviceContext()->Unmap(MatrixBuffer, 0);

		bufferNumber = 0;

		NXGPU::GetDeviceContext()->VSSetConstantBuffers(bufferNumber, 1, &MatrixBuffer);

		return true;
	}

	bool NXShader::SetShaderParameters(XMMATRIX& view, XMMATRIX& world, XMMATRIX& proj, NXTexture2D* texture)
	{
		HRESULT result;
		D3D11_MAPPED_SUBRESOURCE mappedResource;
		MatrixBufferType* dataPtr;
		unsigned int bufferNumber;

		world = XMMatrixTranspose(world);
		view = XMMatrixTranspose(view);
		proj = XMMatrixTranspose(proj);

		result = NXGPU::GetDeviceContext()->Map(MatrixBuffer, 0, D3D11_MAP_WRITE_DISCARD, 0, &mappedResource);
		if (FAILED(result))
		{
			return false;
		}

		dataPtr = (MatrixBufferType*) mappedResource.pData;

		dataPtr->World = world;
		dataPtr->View = view;
		dataPtr->Projection = proj;

		NXGPU::GetDeviceContext()->Unmap(MatrixBuffer, 0);

		bufferNumber = 0;

		NXGPU::GetDeviceContext()->VSSetConstantBuffers(bufferNumber, 1, &MatrixBuffer);

		NXGPU::GetDeviceContext()->PSSetShaderResources(0, 1, &texture->Texture);

		return true;
	}

	void NXShader::Dispose()
	{
		if (MatrixBuffer)
		{
			MatrixBuffer->Release();
			MatrixBuffer = 0;
		}

		if (Layout)
		{
			Layout->Release();
			Layout = 0;
		}

		if (PixelShader)
		{
			PixelShader->Release();
			PixelShader = 0;
		}

		if (VertexShader)
		{
			VertexShader->Release();
			VertexShader = 0;
		}

		return;
	}

	HRESULT NXShader::CompileShaderFromFile(WCHAR* szFileName, LPCSTR szEntryPoint, LPCSTR szShaderModel, ID3DBlob** ppBlobOut)
	{
		HRESULT hr = S_OK;

		DWORD dwShaderFlags = D3DCOMPILE_ENABLE_STRICTNESS;
#if defined( DEBUG ) || defined( _DEBUG )
		// Set the D3DCOMPILE_DEBUG flag to embed debug information in the shaders.
		// Setting this flag improves the shader debugging experience, but still allows 
		// the shaders to be optimized and to run exactly the way they will run in 
		// the release configuration of this program.
		dwShaderFlags |= D3DCOMPILE_DEBUG;
#endif

		ID3DBlob* pErrorBlob;
		hr = D3DCompileFromFile(szFileName, nullptr, nullptr, szEntryPoint, szShaderModel,
			dwShaderFlags, 0, ppBlobOut, &pErrorBlob);
		if (FAILED(hr))
		{
			if (pErrorBlob != nullptr)
				OutputDebugStringA((char*) pErrorBlob->GetBufferPointer());
			if (pErrorBlob) pErrorBlob->Release();
			return hr;
		}
		if (pErrorBlob) pErrorBlob->Release();

		return S_OK;
	}
} 

I would like to remove the SetShaderParameter bool methods above, and replace them with something like this:


void SetParameter(const string&, float);
void SetParameter(const string&, int);
void SetParameter(const string&, XMMatrix*)
void SetParameter...etc.

This would really help me out. The class can currently compile and load Pixel/Vertex shaders correctly:

kum0.png

But I seem to limited to setting parameters by creating a bunch of constant buffers for each cbuffer in my shaders. Is there a way to do what I am trying to do in DX11? Thanks! smile.png

Advertisement
Sure, there's a way. You could create a mapping from string to a structure member.

There's really no reason to go that route though. Event the DX9 effects framework essentially uses cbuffers on the back end, with the strings just being mappings.

You really shouldn't be using strings for this though, its not a particularly efficient idea.

In time the project grows, the ignorance of its devs it shows, with many a convoluted function, it plunges into deep compunction, the price of failure is high, Washu's mirth is nigh.

The way shader constants have changed from D3D9 to 11, and the fact that D3D, even in version 9, never really had per-shader constants (they were always global state, the old Effects framework just never exposed that well), means that you're probably better off separating your constants from your shaders, otherwise you've got an artificial coupling that's only going to hinder you as you go forward.

As an example, what would you propose to do if you had two different vertex shaders but they both needed to use the same matrices? Your current design doesn't handle this requirement, instead forcing you to upload the matrices twice. However, D3D itself doesn't (and never did) require you to do that.

If what you really want is just a D3D11 version of the old Effects framework (which it looks like is what you're designing towards), there is one provided with the DirectX SDK (I don't know about newer Windows SDK versions) in source-code-only form; you'll find it in C:\Program Files (x86)\Microsoft DirectX SDK (June 2010)\Samples\C++\Effects11 and you can compile it and link to it.

Personally I've just got a much more lightweight shader class that just contains creation and destruction code for the types of shaders (and input layouts) I'm interested in, using a hash of the bytecode and a global shader cache to avoid creating the same object more than once, some simple state change management, and otherwise I handle cbuffer updates completely separately and via the raw API.

Direct3D has need of instancing, but we do not. We have plenty of glVertexAttrib calls.

Hey, thanks for the replies!

Sure, there's a way. You could create a mapping from string to a structure member.

There's really no reason to go that route though. Event the DX9 effects framework essentially uses cbuffers on the back end, with the strings just being mappings.

You really shouldn't be using strings for this though, its not a particularly efficient idea.

What would you recommend to replace the strings? Is WCHAR or char good?

The way shader constants have changed from D3D9 to 11, and the fact that D3D, even in version 9, never really had per-shader constants (they were always global state, the old Effects framework just never exposed that well), means that you're probably better off separating your constants from your shaders, otherwise you've got an artificial coupling that's only going to hinder you as you go forward.

As an example, what would you propose to do if you had two different vertex shaders but they both needed to use the same matrices? Your current design doesn't handle this requirement, instead forcing you to upload the matrices twice. However, D3D itself doesn't (and never did) require you to do that.

If what you really want is just a D3D11 version of the old Effects framework (which it looks like is what you're designing towards), there is one provided with the DirectX SDK (I don't know about newer Windows SDK versions) in source-code-only form; you'll find it in C:\Program Files (x86)\Microsoft DirectX SDK (June 2010)\Samples\C++\Effects11 and you can compile it and link to it.

Personally I've just got a much more lightweight shader class that just contains creation and destruction code for the types of shaders (and input layouts) I'm interested in, using a hash of the bytecode and a global shader cache to avoid creating the same object more than once, some simple state change management, and otherwise I handle cbuffer updates completely separately and via the raw API.

Hey! Thanks for the info! It is really helpful. smile.png

The way I do it in my engine is that I add meta data for my shaders, describing the size of the constant, it's register, it's name, whatever information I may need. Then in my game I have a SetUniform call that takes in a name or an enum that corresponds to known constants, a void*, and the size of the uniform. Then I map the name/id to a global shader cache, and apply it from there. It takes a bit of setup to work, but that's all handled by tools now. And I can create custom uniforms without having to cram it into some sort of pre-defined register index.

Perception is when one imagination clashes with another

This topic is closed to new replies.

Advertisement