Progress Update

Published March 02, 2017
Advertisement

tl;dr: My level editor lets you assign a texture and/or normal map to each face of the level geometry and the game dynamically generates pixel shaders to support the required combinations.

rpg.png

Progress Report:

I've updated the level editor now to support setting an optional diffuse texture and/or normal map on each face of the geometry brushes. These are located on the "Assets" path you set as a level global property.

boxes.png

Each face also has properties for the UV scale and offset, so you have a lot of flexibility here.

When generating the vertex buffer for the solid geometry, the editor now groups all the triangles that share the same textures and keeps track of the offsets into the buffer for each group, so that when we render, we can now draw each group separately, setting the relevant samplers before each call to [font='courier new']DrawPrimitive[/font].

This is achived by using a simple [font='courier new']RenderKey[/font] class that provides [font='courier new']operator==[/font] and [font='courier new']qHash[/font] so it can be used with a [font='courier new']QHash[/font] or [font='courier new']QSet:[/font]

class RenderKey{public: RenderKey(){ } RenderKey(const QString &texture, const QString &normalMap) : texture(texture), normalMap(normalMap) { } bool operator==(const RenderKey &key) const { return texture == key.texture && normalMap == key.normalMap; } QString texture; QString normalMap;};inline uint qHash(const RenderKey &key){ return qHash(key.texture) ^ qHash(key.normalMap);}When generating the preview buffer, we first run over the entities and collect a [font='courier new']QPair[/font] representing an entity index and a face index into a [font='courier new']QHash > >[/font] mapping structure that is then walked to generate the vertices into the buffer so that they are grouped correctly.

When the editor exports the level to the file format supported by the game, it generates a separate mesh per RenderKey. Later we can add more attributes to [font='courier new']RenderKey[/font], such as a manual [font='courier new']Group[/font] attribute that will allow the level desiger to sub-divide the level geometry into geographical groups for the purposes of frustrum culling and so on.

In theory, we can just update [font='courier new']RenderKey[/font] now with any additional attributes and everything else will "just work".

Back to the game and the almost immediate discovery that I have run into the issue of shader permutations - the great unsolved problem in graphics programming according to the internet. I guess everyone stumbles across this eventually.

I want to support both textures and normal maps being option on faces and to even support having a normal map but no texture. My main pixel shader was doing both texture and normal map sampling for everything as was, because everything was using a generic texture and normal map so this was fine. But now, of course, I need versions of the pixel shader to support:

- no texture or normal map
- texture only
- normal map only
- texture and noraml map

And so the fun begins.

I was pleased to discover that it is pretty easy to feed into the HLSL preprocessor with [font='courier new']D3DXCompileShaderFromFile[/font] by passing in an array of [font='courier new']D3DXMACRO[/font] structures. Readers are probably already groaning as they can see the insanity ahead :)

So game side, we now have a [font='courier new']RenderParams[/font] structure that each scene node provides, declaring a [font='courier new']Gx::GraphicsResourceId[/font] (which can be null) for the texture and normal map. An [font='courier new']operator<[/font] is provided so we can use this in a [font='courier new']std::set[/font]. I'll change this to support hashing into a [font='courier new']std::unordered_set[/font] later on.

class RenderParams{public: enum { Texture = 1, NormalMap = 2 }; RenderParams() : features(0) { } Gx::GraphicsResourceId texture; Gx::GraphicsResourceId normalMap; Gx::Index features;};Gx::Index renderFeatures(const RenderParams &params);bool operator==(const RenderParams &a, const RenderParams &b);bool operator!=(const RenderParams &a, const RenderParams &b);bool operator<(const RenderParams &a, const RenderParams &b);[font='courier new']Gx::Index[/font] is just a typedef for [font='courier new']unsigned int[/font]. Once we have set the texture and normalMap IDs in the level loader or wherever, we set up the [font='courier new']features[/font] property as a bitwise combination of [font='courier new']Feature[/font] flags based on the [font='courier new']valid()[/font] states of the resource IDs.


Gx::Index renderFeatures(const RenderParams &params){ Gx::Index features = 0; if(params.texture.valid()) features |= RenderParams::Texture; if(params.normalMap.valid()) features |= RenderParams::NormalMap; return features;}Trivial of course, but thinking ahead for future expansion.

The [font='courier new']Scene[/font] class [font='courier new']render()[/font] method now takes the node's [font='courier new']RenderParams[/font] into account now when deciding when it needs to do a state change, so as long as we sort the scene nodes correctly, we can minimise the number of state changes as we render the scene.

In the level loading code now, as we load the mesh instances from the file, we insert their features value into a [font='courier new']std::set[/font] so we have a list of the possible permutations. This gets fed into [font='courier new']loadShaderSet():[/font]

The features attribute handily maps to an integer between 0 and (at the moment) 3 which we can append to the .dat file name to distinguish each one from the others.

We can also store the IDs of the shaders in an array and we can then just use the features attribute of a given [font='courier new']RenderParams[/font] as an index into the array to select the correct shader when we are setting up the state.

The [font='courier new']DBEUGLOAD[/font] define can be disabled in the distributed version of the game. I had some issues on a friend's PC once that was missing the shader compiler runtime so I prefer to ship the precompiled shader binaries only and just invoke the compiler locally. My shader loading code uses the timestamps of the source .txt and the binary .dat to decide whether to recompile or not.

Obviously when distributing, we have to make sure we ship every possible permutation but the numeric suffix makes this easy to confirm and the game will still only load the shaders it requires for each level.

bool loadShaderSet(Gx::Shader::Type type, const std::string &id, const std::set &permutations, Gx::Graphics &graphics, Gx::GraphicsResourceId *ids){ std::string ts = (type == Gx::Shader::Type::Vertex ? "vertex" : "pixel"); std::string url = id + ts + "shader"; std::string file = "shaders/" + id + ts; for(auto &ps: permutations) { std::string datFile = Gx::stringFormat(file, "_", ps, ".dat");#ifdef DEBUGLOAD Gx::PodVector macros; if(ps & RenderParams::Texture) macros.push_back({ "TEXTURE", "1" }); if(ps & RenderParams::NormalMap) macros.push_back({ "NORMAL", "1" }); macros.push_back({ NULL, NULL }); if(needsCompiling(file + ".txt", datFile)) { if(!compileShader(type, file + ".txt", datFile, macros)) return Gx::GraphicsResourceId(); }#endif Gx::DataInFileStream ds(datFile); if(ds.fail()) return false; Gx::GraphicsResourceId id = makeShader(type, Gx::stringFormat(url, "_", ps), ds.get(), graphics); if(!id) { return false; } ids[ps] = id; } return true;}Here we set up the macros based on the permutations that have been generated when loading the level and we generate a different shader binary for each permutation.

The shader then uses the preprocessor to include/exclude features as required, and we only generate the shaders we actually need for the particular level.

So yet another wheel thoroughly reinvented. I'm not completely sure I've finished this yet in terms of how it is structured, but it should allow me to expand with other optional features if I need them painlessly enough.

In other news, I did an experiment yesterday to see what the performance gains of using a position-only mesh structure for rendering the depth cubemap would be. I currently just use the same [font='courier new']GeneralVertex[/font] structure for all the rendering which includes UV, tangent, diffuse and a bunch of other attributes that aren't required by the depth render.

First up, I generated some timing data based on something I know has a big effect on performance to ensure my timing code was working properly - namely the size of the cube map used to render the depth. The graph below shows the times to render frames 100 to 199 using a cube map with dimensions 512, 1024 and 2048 respectively.

shadowcubesizes.png

This is pretty much what I would expect so I was confident the timing code was correct.

I then set things up so that, for each mesh, I generated a clone of the mesh containing only the position for each vertex, set up an equivalent vertex declaration and set up the scene node rendering to use the position-only mesh when rendering the depth to the cube map. The graph below shows the results for frames 100 to 199:

positiononly.png

So, makes no apparent difference. I guess when the levels get larger, it is possible we will start having the vertex buffers getting paged on and off the GPU, in which case this might become more effective so perhaps it is a bit premature to be testing this while the level geometry is so simple and small. Can always revisit this later on.

4 likes 0 comments

Comments

Nobody has left a comment. You can be the first!
You must log in to join the conversation.
Don't have a GameDev.net account? Sign up!
Profile
Author
Advertisement
Advertisement