Game engine architecture

Started by
14 comments, last by EarthBanana 9 years, 9 months ago

I'm sure you can find useful help/tips in Jason's Game Engine Architecture book. It's a good way to begin and even master.

Advertisement

"A mesh represents a geometry or model with a single material."

While it is true that below the level of a “mesh” there are no common or official terms, a single mesh can definitely have more than 1 material and render call.
I outlined this on my site, where I referred to the sub-mesh level as “render part” for lack of a better term at the time (since then I have found “sub-mesh” to be better).
LSModelBreakdown1.png

In such a simple case as this you could possibly draw the red and green parts in one draw call (though it would be non-trivial in any generic sense), but the fact is that the red and green areas are 2 different materials on the same mesh and it is meant to illustrate that in a more advanced case you will have no choice but to draw the red, switch materials and shaders, and then draw the green.

If Assimp says that the red, green, and blue are 3 different meshes, it’s wrong.
If it says there are 2 meshes but a mesh can have only 1 material, it’s wrong.

I have never used Assimp and don’t know its limitations, but if it says a mesh can have only 1 material then that sounds like a pretty bad limitation.


would this be the way:

Yes, but that’s the active way.
A better way is the last-minute way.
Basic Graphics Implementation


L. Spiro

I read your site and liked it, however I couldn't fully understand how to implement what you call the last-minute way. For example, I don't understand what CStd::Min( LSG_MAX_TEXTURE_UNITS, CFndBase::m_mMetrics.ui32MaxTexSlot ); does.

Am I right in understanding you have two arrays : an array of last active states (for example m_psrvLastActiveTextures ) and the ones that are active at the time of the render call (for example m_psrvActiveTextures)?

And then you set the last active to the current one if they are not already equal?

I am still fuzzy on the details, though, it seems similar to the active way because you are swapping a current sate and a last one? Whats the difference?

Thanks!

For example, I don't understand what CStd::Min( LSG_MAX_TEXTURE_UNITS, CFndBase::m_mMetrics.ui32MaxTexSlot ); does.

The API may support 128 texture slots (CFndBase::m_mMetrics.ui32MaxTexSlot), but the engine only supports 32 (LSG_MAX_TEXTURE_UNITS).


Am I right in understanding you have two arrays

Not directly.

I have a structure which encapsulates all state information.
/** The render-state copy that allows us to know which states have changed since the last system update. */
struct LSG_RENDER_STATE {
	/** Shader resources. */
	LSG_SHADER_RESOURCE *				psrShaderResources[LSG_MAX_TEXTURE_UNITS];

	/** Sampler states. */
	LSG_SAMPLER_STATE *				pssSamplers[LSG_MAX_SHADER_SAMPLERS];

	/** Render targets. */
	LSG_RENDER_TARGET *				prtRenderTargets[LSG_MAX_RENDER_TARGET_COUNT];

	/** Input layouts. */
	const LSG_INPUT_LAYOUT *			pilInputLayout;

	/** Vertex buffers. */
	LSG_VERTEX_BUFFER				pvbVertexBuffers[LSG_MAX_VERTEX_STREAMS];

	/** Depth/stencil buffer. */
	LSG_DEPTH_STENCIL *				pdsDepthStencil;

	/** Rasterizer state. */
	LSG_RASTERIZER_STATE *				prsRasterState;

	/** Blend state. */
	LSG_BLEND_STATE *				pbsBlendState;

	/** Depth/stencil state. */
	LSG_DEPTH_STENCIL_STATE *			pdssDepthStencilState;

	/** Viewports. */
	LSG_VIEWPORT					vViewports[LSG_MAX_VIEWPORTS_SCISSORS];

	/** Scissors. */
	LSG_RECT					rScissors[LSG_MAX_VIEWPORTS_SCISSORS];

	/** Blend factors. */
	LSGREAL						fBlendFactors[4];

	/** Clear color. */
	LSGREAL						fClearColor[4];

	/** Clear depth. */
	LSGREAL						fClearDepth;

	/** Stencil reference value. */
	LSUINT32					ui32StencilRef;

	/** 32-bit sample coverage for the output-merger state. */
	LSUINT32					ui32SampleMask;

	/** Total active vertex buffers. */
	LSUINT32					ui32TotalVertexBuffers;

	/** Stencil clear value. */
	LSUINT8						ui8ClearStencil;

	/** Total viewports/scissors. */
	LSUINT8						ui8TotalViewportsAndScissors;

	/** Total render targets. */
	LSUINT8						ui8TotalRenderTargets;

};
Since my engine is cross-platform, LSG_SHADER_RESOURCE, LSG_SAMPLER_STATE, etc., are all typedefs.
LSGDirectX11.h:
	/** Sampler description. */
	typedef D3D11_SAMPLER_DESC			LSG_SAMPLER_DESC;

	/** Rasterizer description. */
	typedef D3D11_RASTERIZER_DESC			LSG_RASTERIZER_DESC;

	/** Render-target blend description. */
	typedef D3D11_RENDER_TARGET_BLEND_DESC		LSG_RENDER_TARGET_BLEND_DESC;

	/** Blend description. */
	typedef D3D11_BLEND_DESC			LSG_BLEND_DESC;

	/** Depth/stencil operation description. */
	typedef D3D11_DEPTH_STENCILOP_DESC		LSG_DEPTH_STENCILOP_DESC;

	/** Depth/stencil description. */
	typedef D3D11_DEPTH_STENCIL_DESC		LSG_DEPTH_STENCIL_DESC;

	/** The 1D texture type. */
	typedef ID3D11Texture1D				LSG_TEXTURE_1D;

	/** The 2D texture type. */
	typedef ID3D11Texture2D				LSG_TEXTURE_2D;

	/** The 3D texture type. */
	typedef ID3D11Texture3D				LSG_TEXTURE_3D;

	/** The sampler type. */
	typedef ID3D11SamplerState			LSG_SAMPLER_STATE;

	/** The shader resource view type. */
	typedef ID3D11ShaderResourceView		LSG_SHADER_RESOURCE;

	/** The render target type. */
	typedef ID3D11RenderTargetView			LSG_RENDER_TARGET;

	/** The depth/stencil type. */
	typedef ID3D11DepthStencilView			LSG_DEPTH_STENCIL;

	/** The rasterizer state type. */
	typedef ID3D11RasterizerState			LSG_RASTERIZER_STATE;

	/** The blend state type. */
	typedef ID3D11BlendState			LSG_BLEND_STATE;

	/** The depth/stencil state. */
	typedef ID3D11DepthStencilState			LSG_DEPTH_STENCIL_STATE;

	/** The viewport type. */
	typedef D3D11_VIEWPORT				LSG_VIEWPORT;

	/** The scissor rectangle type. */
	typedef D3D11_RECT				LSG_RECT;

	/** The input layout type. */
	typedef ID3D11InputLayout			LSG_INPUT_LAYOUT;

	/** The vertex buffer type. */
	typedef struct LSG_VERTEX_BUFFER {
		const CVertexBuffer *			pvbVertBuffer;			/**< The vertex buffer pointer. */
		LSUINT32				ui32Offset;			/**< Offset of the vertex buffer. */
	} * LPLSG_VERTEX_BUFFER, * const LPCLSG_VERTEX_BUFFER;
[spoiler]LSGDirectX9.h:
#include "../Gfx/LSGDescStructs.h"

	/** The 1D texture type. */
	typedef IDirect3DTexture9			LSG_TEXTURE_1D;

	/** The 2D texture type. */
	typedef IDirect3DTexture9			LSG_TEXTURE_2D;

	/** The 3D texture type. */
	typedef IDirect3DTexture9			LSG_TEXTURE_3D;

	/** The sampler type. */
	typedef LSG_SAMPLER_DESC			LSG_SAMPLER_STATE;

	/** The shader resource view type. */
	typedef IDirect3DTexture9			LSG_SHADER_RESOURCE;

	/** The render target type. */
	typedef IDirect3DSurface9			LSG_RENDER_TARGET;

	/** The depth/stencil type. */
	typedef IDirect3DSurface9			LSG_DEPTH_STENCIL;

	/** The rasterizer state type. */
	typedef LSG_RASTERIZER_DESC			LSG_RASTERIZER_STATE;

	/** The blend state type. */
	typedef LSG_BLEND_DESC				LSG_BLEND_STATE;

	/** The depth/stencil state. */
	typedef LSG_DEPTH_STENCIL_DESC			LSG_DEPTH_STENCIL_STATE;

	/** The viewport type. */
	typedef D3DVIEWPORT9				LSG_VIEWPORT;

	/** The scissor rectangle type. */
	typedef RECT					LSG_RECT;

	/** The input layout type. */
	typedef IDirect3DVertexDeclaration9		LSG_INPUT_LAYOUT;

	/** The vertex buffer type. */
	typedef struct LSG_VERTEX_BUFFER {
		const CVertexBuffer *			pvbVertBuffer;			/**< The vertex buffer pointer. */
		LSUINT32				ui32Offset;			/**< Offset of the vertex buffer. */
	} * LPLSG_VERTEX_BUFFER, * const LPCLSG_VERTEX_BUFFER;
LSGOpenGL.h:
#include "../Gfx/LSGDescStructs.h"

	/** The 1D texture type. */
	typedef GLuint					LSG_TEXTURE_1D;

	/** The 2D texture type. */
	typedef GLuint					LSG_TEXTURE_2D;

	/** The 3D texture type. */
	typedef GLuint					LSG_TEXTURE_3D;

	/** The sampler type. */
	typedef LSG_SAMPLER_DESC			LSG_SAMPLER_STATE;

	/** The shader resource view type. */
	typedef GLuint					LSG_SHADER_RESOURCE;

	/** The render target type. */
	typedef GLuint					LSG_RENDER_TARGET;

	/** The depth/stencil type. */
	typedef GLuint					LSG_DEPTH_STENCIL;

	/** The rasterizer state type. */
	typedef LSG_RASTERIZER_DESC			LSG_RASTERIZER_STATE;

	/** The blend state type. */
	typedef LSG_BLEND_DESC				LSG_BLEND_STATE;

	/** The depth/stencil state. */
	typedef LSG_DEPTH_STENCIL_DESC			LSG_DEPTH_STENCIL_STATE;

	/** The viewport type. */
	typedef struct LSG_VIEWPORT {
		GLint					iX;				/**< The left coordinate of the viewport in pixels. */
		GLint					iY;				/**< The upper coordinate of the viewport in pixels. */
		GLsizei					sWidth;				/**< Width of the viewport in pixels. */
		GLsizei					sHeight;			/**< Height of the viewport in pixels. */
		GLfloat					fMinZ;				/**< Together with MaxZ, value describing the range of depth values into which a scene is to be rendered, the minimum and maximum values of the clip volume. */
		GLfloat					fMaxZ;				/**< Together with MinZ, value describing the range of depth values into which a scene is to be rendered, the minimum and maximum values of the clip volume. */
	} * LPLSG_VIEWPORT, * const LPCLSG_VIEWPORT;

	/** The scissor rectangle type. */
	typedef struct LSG_RECT {
		GLint					left;				/**< The x-coordinate of the upper-left corner of the rectangle. */
		GLint					top;				/**< The y-coordinate of the upper-left corner of the rectangle. */
		GLsizei					right;				/**< The x-coordinate of the lower-right corner of the rectangle. */
		GLsizei					bottom;				/**< The y-coordinate of the lower-right corner of the rectangle. */
	} * LPLSG_RECT, * const LPCLSG_RECT;

	/** The input layout type. */
	typedef LSVOID *				LSG_INPUT_LAYOUT;

	/** The vertex buffer type. */
	typedef struct LSG_VERTEX_BUFFER {
		const CVertexBuffer *			pvbVertBuffer;			/**< The vertex buffer pointer. */
		LSUINT32				ui32Offset;			/**< Offset of the vertex buffer. */
	} * LPLSG_VERTEX_BUFFER, * const LPCLSG_VERTEX_BUFFER;
[/spoiler]

The graphics device has 2 LSG_RENDER_STATE objects.
		/** The last render state. */
		LSG_RENDER_STATE CDirectX11::m_rsLastRenderState;

		/** The current render state. */
		LSG_RENDER_STATE CDirectX11::m_rsCurRenderState;
When you want to set a viewport (for example):
	LSE_INLINE LSVOID LSE_CALL CDirectX11::SetViewport( LSREAL _fX, LSREAL _fY,
		LSREAL _fWidth, LSREAL _fHeight, LSUINT32 _ui32Target ) {
		assert( _ui32Target < LSG_MAX_VIEWPORTS_SCISSORS );

		assert( static_cast<FLOAT>(_fX) >= static_cast<FLOAT>(LSG_VIEWPORT_MIN) && static_cast<FLOAT>(_fX) <= static_cast<FLOAT>(LSG_VIEWPORT_MAX) );
		assert( static_cast<FLOAT>(_fY) >= static_cast<FLOAT>(LSG_VIEWPORT_MIN) && static_cast<FLOAT>(_fY) <= static_cast<FLOAT>(LSG_VIEWPORT_MAX) );

		assert( static_cast<FLOAT>(_fWidth) >= 0.0f && static_cast<FLOAT>(_fX + _fWidth) <= static_cast<FLOAT>(LSG_VIEWPORT_MAX) );
		assert( static_cast<FLOAT>(_fHeight) >= 0.0f && static_cast<FLOAT>(_fY + _fHeight) <= static_cast<FLOAT>(LSG_VIEWPORT_MAX) );

		m_rsCurRenderState.vViewports[_ui32Target].TopLeftX = static_cast<FLOAT>(_fX);
		m_rsCurRenderState.vViewports[_ui32Target].TopLeftY = static_cast<FLOAT>(_fY);
		m_rsCurRenderState.vViewports[_ui32Target].Width = static_cast<FLOAT>(_fWidth);
		m_rsCurRenderState.vViewports[_ui32Target].Height = static_cast<FLOAT>(_fHeight);
	}
m_rsCurRenderState is the current state and you can modify each part of it all you want. Redundant or not, it just updates the local copy.


At some point you are going to want to make a draw call. This is when you check for redundancies.

	LSVOID LSE_CALL CDirectX11::ApplyRenderStates( LSBOOL _bForce ) {
		const LSG_RENDER_STATE & rsCurState = m_rsCurRenderState;
		LSG_RENDER_STATE & rsLastState = m_rsLastRenderState;

#define LSG_CHANGED( MEMBER )	((rsCurState.MEMBER != rsLastState.MEMBER) || _bForce)
#define LSG_UPDATE( MEMBER )	rsLastState.MEMBER = rsCurState.MEMBER
…

		// Rasterizer state.
		if ( LSG_CHANGED( prsRasterState ) ) {
			GetDirectX11Context()->RSSetState( rsCurState.prsRasterState );
			LSG_UPDATE( prsRasterState );
		}

		// Blend state.
		if ( LSG_CHANGED( pbsBlendState ) ||
			LSG_CHANGED( fBlendFactors[0] ) || LSG_CHANGED( fBlendFactors[1] ) || LSG_CHANGED( fBlendFactors[2] ) || LSG_CHANGED( fBlendFactors[3] ) ||
			LSG_CHANGED( ui32SampleMask ) ) {
			GetDirectX11Context()->OMSetBlendState( rsCurState.pbsBlendState,
				rsCurState.fBlendFactors,
				rsCurState.ui32SampleMask );
			LSG_UPDATE( pbsBlendState );
			LSG_UPDATE( fBlendFactors[0] );
			LSG_UPDATE( fBlendFactors[1] );
			LSG_UPDATE( fBlendFactors[2] );
			LSG_UPDATE( fBlendFactors[3] );
			LSG_UPDATE( ui32SampleMask );
		}

		// Depth/stencil state.
		if ( LSG_CHANGED( pdssDepthStencilState ) || LSG_CHANGED( ui32StencilRef ) ) {
			GetDirectX11Context()->OMSetDepthStencilState( rsCurState.pdssDepthStencilState,
				rsCurState.ui32StencilRef );
			LSG_UPDATE( pdssDepthStencilState );
			LSG_UPDATE( ui32StencilRef );
		}

…

		// Viewports.
		LSBOOL bSetViewports = LSG_CHANGED( ui8TotalViewportsAndScissors );
		LSBOOL bSetScissors = bSetViewports;
		if ( !bSetViewports ) {
			// Check for changes in each viewport.
			for ( LSUINT32 I = m_rsCurRenderState.ui8TotalViewportsAndScissors; I--; ) {
				if ( LSG_CHANGED( vViewports[I].TopLeftX ) || LSG_CHANGED( vViewports[I].TopLeftY ) ||
					LSG_CHANGED( vViewports[I].Width ) || LSG_CHANGED( vViewports[I].Height ) ||
					LSG_CHANGED( vViewports[I].MinDepth ) || LSG_CHANGED( vViewports[I].MaxDepth ) ) {
						bSetViewports = true;
						break;
				}
			}
		}
		if ( bSetViewports ) {
			GetDirectX11Context()->RSSetViewports( rsCurState.ui8TotalViewportsAndScissors,
				rsCurState.vViewports );
			// Update our copies of what we just sent to the hardware.
			for ( LSUINT32 I = m_rsCurRenderState.ui8TotalViewportsAndScissors; I--; ) {
				LSG_UPDATE( vViewports[I] );
			}
		}

…

#undef LSG_UPDATE
#undef LSG_CHANGED
}
	LSVOID LSE_CALL CGfx::Render( void * _pibIndexBuffer,
		LSUINT32 _ui32StartVertex,
		LSUINT32 _ui32PrimitiveCount ) {
…

		// Apply render states.
		ApplyRenderStates();

		// Render.
		psShader->PreRender();

…

		if ( _pibIndexBuffer ) {
			VertexBuffers()[0].pvbVertBuffer->RenderApi( 0, 0 );		// Makes it apply certain necessary settings before rendering.
			pibIndexBuffer->RenderApi( _ui32StartVertex, _ui32PrimitiveCount );
		}
		else {
			VertexBuffers()[0].pvbVertBuffer->RenderApi( _ui32StartVertex, _ui32PrimitiveCount );
		}

…
}

Redundancies are checked at the last minute, just before rendering. Hence it is last-minute.


L. Spiro

I restore Nintendo 64 video-game OST’s into HD! https://www.youtube.com/channel/UCCtX_wedtZ5BoyQBXEhnVZw/playlists?view=1&sort=lad&flow=grid

In order to create a model system, I need to choose a file format I am going to primarily support. I couldn't find too much information on which ones are used in the game industry, but it would seem .fbx is a good choice? Would it be possible to export models as compiled vertex and index buffers and then load them into the engines buffers? That would be very fast and efficient and it would make loading quite simple on the engine side (albeit not the exporter side, fbx is quite undocumented and I don't know where to start).

What should my hierarchy look like?

CModel

{

std::vector<CMesh*>mMeshes;

XMFLOAT3 mPos;

// what else?

};

CMesh

{

std::vector<CSubMesh*>mSubMeshes;

// what else?

};

CSubMesh

{

CMaterial* mMaterial;

vbuffer

ibuffer

pshader

vshader

// what else?

};

Thank you !

I couldn't find too much information on which ones are used in the game industry, but it would seem .fbx is a good choice?

Most engines use custom formats to store and load meshes at runtime. FBX is an interchange file format not optimized for runtime loading. Try to create a (binary) format specific to the needs of your engine that makes loading ultra fast, something like: <N><array of N vertices><...>, so when you load the model you can simply read the number of vertices and memcpy N*sizeof(Vertex) directly to the correct place (eg: graphics API vertex buffer).

My engine uses an hierarchy like this:


Actor
{
    Model
    Position
    Actor specific constants
}

Model
{
    Mesh
    vector<Subset>
}

Mesh
{
    Vertex Buffer
    Index Buffer
}

Subset
{
    Start index
    Number of indices
    Material
}

Material
{
    Textures
    Shader
    Other data
}

This way you can have multiple Actors at different positions using the same model, and multiple models using the same mesh but with different materials, etc

Assimp does not limit 1 material per mesh - very roughly speaking what Assimp calls a Scene is what LSpiro is referring to as a mesh, and what Assimp calls a Mesh is what LSpiro is calling a Subset

Assimp loads the "Scene" as an array of "Meshes" which each have an index in to the array of Materials for the scene

This translates, in my engine, as a Mesh which contains an array of Submeshes, each with integer handles to the material.. Each Submesh can refer to only 1 material.. Each Material then contains an integer handle to the Shader responsible for drawing the material

When using components, don't call your resources components - anything that you have to load could be considered a resource - textures, meshes, animations, audio clips, shaders, etc

You only want to load these things once - and either pass around handles or pointers to these within the engine

IE if you want to have a Render component, it should contain a pointer or handle to a mesh rather than the mesh itself

This topic is closed to new replies.

Advertisement