Sign in to follow this  
WFP

Propagating data through an engine to constant buffers

Recommended Posts

WFP    2781

I was hoping to get some advice about engine design, and more specifically, about passing data that models need to exist in a constant buffer at render time.  The below is based on an entity-component framework, in case that helps clarify where data should be coming from.

 

For probably >90% of the content in my scene (basic models with a vertex shader, pixel shader, and only really need a WVP transform) I have a reserved (in the context of my engine) constant buffer slot I'm using to set data that comes from the Entity's corresponding TransformComponent combined with the view and projection matrix that comes from the current camera being used.  For such a simple case, this is easy and straightforward enough that I haven't really thought to revisit it.

 

Recently, I've started adding tesselated heightmap-based terrain to the engine, and unlike the common entities, it also requires an additional constant buffer that houses things like min and max tesselation factors, camera position, frustum planes (for culling), and a view matrix (used to move generated normals to view space for the GBuffer).  I haven't done a good job in building flexibility into the current pipeline to accomodate the need for anything outside of the standard constant buffer described above which, again, houses mostly the WVP transformation matrix.

 

When I started thinking longer term, I realized I was going to run into the same issues for things like ocean rendering, volumetric fog, or really anything that is "non-standard" in that it's not just a model with a straightforward Vertex Shader -> Pixel Shader -> Done type of setup.  I'll go over below what I have existing right now to band-aid this situation, but I would really appreciate input about how to better be able to get at the data I need for a specific model's constant buffer requirements without the model having to know about upstream objects (for example, without the model being able to query the scene for the current camera being used to get the view matrix).

 

Current solution:

In my render component, which houses a pointer to a model (which contains a vertex buffer, index buffer, and subset table describing offsets and shaders/texture per subset - ideally where a would like constant buffer data to live since models depend on it), I have added a std::function member to allow for "extra work" and a boolean flag to acknowledge its presence.  The gist is that during setup if a renderable entity (one with a RenderComponent) needs to perform extra setup work, it can define what work needs to be done in that std::function member and the main render loop will check if it's flag is set during each iteration.  So, like below:

// during scene setup - create a render component with the provided model
RenderComponent* pRC = Factory<RenderComponent>.create(pTerrainModel);
pRC->setExtraWork = [&](DeviceContext& deviceContext, FrameRenderData& frameRenderData)
{
  // do the additional work here - in the case above, retrieve extra data needed
  // for constant buffer from the frameRenderData and store and enable that to a
  // shader constant buffer slot
}


/////// later in rendering loop
if(pCurrent->hasExtraWork())
{
  pCurrent->getExtraWork()(deviceContext, frameRenderData);
}


//////// and the way the extra work member is defined in RenderComponent
std::function<void(DeviceContext& deviceContext, FrameRenderData& frameRenderData)> m_extraWork;

The FrameRenderData is just a generated struct of references to the data relevant to any given frame - the current camera, the current entities to be rendered, etc.

 

The other thought I had would be to trigger an event at the start of each frame containing the FrameRenderData and let anything that wants to know about it listen for it, but then I feel like my models or render components would need to have event listeners attached, which also seems like iffy design at best.

 

While the above technically works, I feel like it's kludgy and was wondering if anyone had thoughts on a better way to get data to dependent constant buffers in a system setup similar to what's above.

 

Thanks for your time and help.

Share this post


Link to post
Share on other sites
Seabolt    781

I have tortured myself over this decision in several of my engines that I've written.

 

Generally I have a couple of things that I look up. Usually I'll have my materials have some way of knowing what constants are in my shaders then doing a lookup for the size of the constant, register location, etc. Then the gameplay side just knows the name of the constant and provides a void* to the material.

 

Now the downside is that every shader needs to have additional information to tell the game what constants it is looking for and other information regarding it. This can be a pain, and I just do it by hand. 

Share this post


Link to post
Share on other sites
WFP    2781

@Seabolt Thanks for the input.  It's definitely a challenging design to get just right :)

 

@Irlan Also, thank you for your comments.  I think you and I are actually on the same page in a lot of places, and maybe something like the shader manager is the missing piece for me.

 

To clarify a few things that may help the discussion:

 

 

Models are not render components.

 

Correct!  Render components own a pointer to a model instance, that's all.  A model actually knows nothing about render components or anything in a higher level system, and that's the aim of the post - to keep it that way :)

 

Your and my concepts of models and graphics libraries and the separations between the two are matched, also.  My graphics library has no concept of a model.  In fact, it's mostly just a thin wrapper around DirectX 11 functions so hopefully when the time comes, porting to other API backends (OpenGL/DirectX 12) isn't too much of a hassle.  The graphics library can be used to create various buffers (vertex, index, constant), textures, device contexts, etc. (you get the picture), and has the ability to set data to the actual pipeline slots, but that's about the extent of it.

 

A model simply contains a mesh and the data needed to render that mesh (save for the constant buffers).  A mesh just contains a vertex buffer, an index buffer, and topology descriptions, but has no knowledge of anything higher level than that.

 

 

 

Terrains aren't models.

 

True.  My terrain class has all sorts of data about the terrain instance, including methods for getting the height at a specific coordinate, etc., that have nothing to do with actually rendering the terrain.  But it also necessarily contains a model as described above (with a mesh containing a vertex and index buffer), and that model can be added to an entity as its render component so it will get drawn in the same rendering loop as every other entity.

 

And that's where the current hiccup in my design is, and possibly something the shader manager will solve for after I've thought it through a little more.  A model is as simple as any simple thing can be, so when its any particular model's turn to be rendered (be it a house, a wizard, or a terrain), I need a way upstream of the model to ensure that it's is getting everything it needs for drawing itself.  For the most part, it contains this - a model has a mesh which has the vertex and index buffers and topology requirements to send to the graphics context.  It's shaders and shader resources are stored as part of its subset descriptions.  It's just the dynamically updating things that I'm still trying to get figured in, like a constant buffer that has a view matrix, a camera position, and frustum planes, which depends on the specific frame.

 

Does that help clarify where I currently am with this?

 

Thanks,

WFP

Share this post


Link to post
Share on other sites
L. Spiro    25638

But it also necessarily contains a model

Terrain necessarily does not contain a model/mesh. It constructs primitives through basic math for the X and Z, and via a heightmap for the Z. It also has specialized LOD methods, and if you are using GeoClipmap terrain there is a special update required for the heightmap texture itself.

Vegetation necessarily does not contain a model or mesh.
Volumetric fog. Water.


Your problem is that you have designed a system that acts like a funnel. At the end of the day, your system requires a certain type of data in a certain format to create a render, which is a design flaw.
Things can render vastly differently from each other, which is why you always let the objects render themselves. Parts of the pipeline can be shared, such as data for sorting in a render-queue, but at the end of the day it needs to be the actual object that does the render. This allows full flexibility and also solves your problem, since objects will be able to upload to shaders whatever data they need, and only that data.


L. Spiro Edited by L. Spiro

Share this post


Link to post
Share on other sites
Vilem Otte    2941

 

But it also necessarily contains a model

...
Volumetric fog. Water.
...

 

Actually any volumetric effect doesn't really need to contain a model.

 

Also, I'd like to note that you might be trying to generalize materials possibly into single shader. As long as we will have hacks in computer graphics (which means as long as we are not able to run real time bidirectional path tracer without any noise in output image and generate those images at 100 fps with highly complex BSDF based materials), you shouldn't do that.

 

Just a little explanation - there is a generalized mathematical model describing how the light scatters around surface (e.g. incoming light vs. outgoing light), which is called BSDF (Bidirectional Scattering Distribution Function) - there are even some generic BSDF already implemented in some offline rendering packages (these materials are most generic, you can describe almost any material using it, and they are terribly slow) - BSDF is a superset of less complicated and faster set of functions - like BRDF for example (I guess you've heard about BRDF already). Using correct, physically based implementation of BRDF is still not possible currently (you need ray tracer for that ~ although I get quite good results with my ray tracer - for simple scenes (Sponza, and such) it even gets realtime on solid hardware)

 

BSDF is quite far future (for interactive rendering), because I don't see a way how you could work with it inside a rasterizer-based renderer and still produce at least semi-accurate results.

Share this post


Link to post
Share on other sites
L. Spiro    25638

Actually any volumetric effect doesn't really need to contain a model.

Nor does terrain or water or vegetation. That’s why I listed them there together.
Hard to tell if your emphasis is on “any”, which would in that case mean that you are just expanding upon my list.
And indeed, no volumetric effect needs a model/mesh.


L. Spiro Edited by L. Spiro

Share this post


Link to post
Share on other sites
Irlan    4067


Also, thank you for your comments.  I think you and I are actually on the same page in a lot of places, and maybe something like the shader manager is the missing piece for me.

 

Just remember of the SRP (Single Responsability Principle): if you're doing something that is unrelated to a class you need to create another class and give that this responsibility. So, dividing tasks it can be a great start.

 


But it also necessarily contains a model as described above (with a mesh containing a vertex and index buffer), and that model can be added to an entity as its render component so it will get drawn in the same rendering loop as every other entity.

 

The more abstracted you do things that don't necessarely need to be, more you tend to write unmaintainable code. A terrain is so specialized that you find books out there like "how to render terrains", "shader guide for terrains", "pratical rendering with terrains", etc. A terrain doesn't have a mesh. It has buffers, a lot of texture layers (to do texture blending, etc.); do not mess up with the models.

 


Correct!  Render components own a pointer to a model instance, that's all.

 

Looks like your "render component" is a model instance. If it is then it isn't a problem, but a render component can be anything that can be rendered at the game-side.

 

If you're not confortable with the entity-component-system I'd recommend creating a simple hierarchy such game entities (used in CryEngine, ID Tech, etc.) and keep sub-dividing the entities responsibility so do the components. I don't use ECS myself because I think that for one single person to manage that in a engine it's asking for unmaintainable code.

Share this post


Link to post
Share on other sites
WFP    2781

Hi all,

Thanks for the additional comments.  I think it's starting to become more clear how I'm going to need to restructure a few things, but I have a few more questions that might help me out a little.

 

 

But it also necessarily contains a model

This was mainly in the context to how I have my heightmap-based terrain setup.  As stated, in my engine a model is just a vertex buffer, index buffer, and a subset table.  With my approach, I'm storing all of these in a single model that the terrain class builds during initialization and owns and submitting it to the input assembler as a control patch (D3D11_PRIMITIVE_TOPOLOGY_4_CONTROL_POINT_PATCHLIST).  Are you saying instead that the Terrain class itself should just directly own the necessary buffers and subset table?  This isn't a huge refactoring effort by any means to pull out of the model class and give to the terrain directly, it just seems like it's duplicating code.

 

I guess I see two possible approaches emerging.  One is to loosen up exactly what a render component owns, and instead have it be more like below:

// anything that a scene needs to render must implement this interface
class IRenderable
{
public:
  void render(GraphicsContext& deviceContext, FrameRenderData& frameRenderData) = 0;
};

class RenderComponent: public BaseComponent
{
public:
  // basic interface
private:
  IRenderable* pRenderable;
};

In this setup, whether I'm using my current simple heightmap terrain or later on adopt geoclipmapping, for example, as long as the terrain class implements IRenderable, it can be used as a render component in the scene and drawn during the main render loop to the GBuffer (or whatever render target it needs).  This would also mean that a model class would need to implement the IRenderable interface, and therefore know how to draw itself.  I don't hate the idea too much, but it seems like a model should contain data about what's needed to draw itself, but not actually do the drawing.  Maybe I'm just trying to over-complicate and over-abstract here, though.

 

The other approach would be to have specialized renderers for different types of objects.  One would exists for models in their current context, and there would also be a specialized one for terrain, water, vegetation, etc.  For example:

class IRenderer
{
  void drawScene(GraphicsContext& context, FrameRenderData& renderData) = 0;
};

class ModelRenderer: IRenderer
{
  void drawScene(GraphicsContext& context, FrameRenderData& renderData) override;
};

class TerrainRenderer: IRenderer
{
  void drawScene(GraphicsContext& context, FrameRenderData& renderData) override;
};
// more specialized renderers
...
//
RenderSystem
{
  std::vector<IRenderer*> m_renderers;
  // scenes can add only the renderers they need
  void addRenderer(IRenderer* pRenderer);
};

With this setup, the renderers themselves are specialized and know only how to draw objects that "match" them.  So for example, if I were to later down the road create a new terrain class based on geoclipmapping, I would create a specialized renderer class for it and register that with the render system during scene setup.  Regardless of whether or not it has a model, the specialized renderer will know enough about whatever it is it's supposed to be drawing to set the appropriate data to its constant buffers and get it into the GBuffer or whatever other render target it may happen to need.

 

Does this make sense, or am I still missing it?

 

Thanks for your help, all.

Share this post


Link to post
Share on other sites
haegarr    7372

My approach is a bit different in that low-level rendering is not directly related to an entity.

 

Low-level rendering means to execute the stages in a graphic pipeline. Each stage has its specific task within the pipeline. A stage that uses the GPU requires a GPU program. From all available GPU programs left after filtering by the platform and user settings, at most a few of them are suitable for a specific stage because they implement the solution to the stage's task.

 

A renderer as a component of an entity (be it a game object in ECS or an entity in its own) specifies which one of the remaining GPU programs is to be used. In the end it is the interface of the GPU program that need to be fulfilled, e.g. to set all constant blocks as expected and to stream in all vertex attributes as expected. Some of this stuff is provided by the entity via its renderer, probably stored in some other components. Some is from elsewhere, e.g. the camera or the view settings from the viewing system or stage specific settings from the stage.

 

Collecting this stuff is the task of the stage when building the rendering jobs. In this sense a renderer component is not an active component. Maybe that this gives a bit of flexibility away (although I have not hit such a problem yet), but it relates the responsibility of what to do within stage processing to the stage itself.

Share this post


Link to post
Share on other sites
Irlan    4067


Are you saying instead that the Terrain class itself should just directly own the necessary buffers and subset table?  This isn't a huge refactoring effort by any means to pull out of the model class and give to the terrain directly, it just seems like it's duplicating code.

 

A terrain should not be rendered in the same way of a model. A terrain also does not has sub-sets; it contains what a terrain needs to have: vertex buffers, index buffers, textures, etc.—specific functionality. 

 

For more that the terrain graphics data looks like the model data (buffers and resources) you don't need to explicity let a terrain be dependent of a model.

 

Further, you may want to do specific things with a terrain such terrain rendering, etc. Remove any dependency that you're having with the model and the terrain and you'll be doing in the correct way. This is not duplicated code (since the objects characteristics are totally unrelated and well defined).

 

Don't be afraid of expanding 10 classes to 100 when it make sense.

 

Later, you will see that complexity depends of the pre-defined requisites, and in your case the requisites are: terrain, models, and your entity-component system, and you should separate that until no dependency is noticed.

 

A terrain knows how to render itself, so you can cut-off your TerrainRenderer for a while.

 

A model knows how to render itself, so you can cut-off your ModelRenderer for a while.

 

Your "Render System" it is a scene. Yet you said that on the comments but since you're limited to call everything a "system" you're limited also to create objects that can be managed by that system you didn't change the class name and its characteristics tongue.png . 

 

A scene knows what it contains, so you can cut-off the RenderSystem for a while.

 

The final result would be something like that (other objects such entites, actors are removed from the example for simplicity purposes):

class CScene {
protected :
     CVector<CRenderableModel*> m_vRenderableModels;
     CTerrainManager m_tmTerrainManager; //It has a lot of terrains, and terrains has specific functionalities of terrains.
};

Simple as that!

 

Now you know that you can call CRenderableModel.Render() and CTerrain.Render(), etc. in the game-engine side at any time you want. Just make sure you've loaded the resource first laugh.png .

Share this post


Link to post
Share on other sites
WFP    2781

@haegarr and @Irlan

 

Thanks for the further explanations.  I'm definitely starting to see now what you all mean about terrain not having a model (as defined in my overall structure), but instead just having the very specific data it needs.  It also makes it more clear how things like volumetric effects would fit in (as you all mentioned above - similar things that don't have a model).  That's helping to clarify a lot and I think with a little bit of restructuring and refactoring I can get this thing a bit more manageable.

 

I'd like to leave the thread open for a while longer to allow a few others to reply and gain as much perspective as I can before I start the refactoring effort, but again - thanks to all who have contributed!

Share this post


Link to post
Share on other sites
Seabolt    781

I'm curious about the approaches listed here.

 

So if we make it to where each object is able to render itself, it makes threading your rendering a lot more difficult because now the render components need to be thread safe, and it also puts the burden of knowing how to draw on the game sim side.

 

Do you guys just accept that cost, or is there something I'm overlooking?

Share this post


Link to post
Share on other sites
haegarr    7372


I'm curious about the approaches listed here.
 
So if we make it to where each object is able to render itself, it makes threading your rendering a lot more difficult because now the render components need to be thread safe, and it also puts the burden of knowing how to draw on the game sim side.

Err ... no. The approach I've mentioned does not do so. The render component contributes information of how to render in form of selection criteria, and provides access to other rendering related data, but rendering is executed by the graphic pipeline stages. Further, the stages generate rendering jobs which are enqueued for execution by the low-level rendering backend (the well known "order your draw call" thing).

Share this post


Link to post
Share on other sites
Seabolt    781

Oh I see what you're saying. So instead of sending down the components, (mesh, materials), you add a render to a higher level that will send down more specific information like (vert buffers, index buffers, textures, shader constants), and then allow the render system to process those elements?

Share this post


Link to post
Share on other sites
WFP    2781

Hi again,

Glad to see the conversation is still going on.  I think I'm really getting things hammered out now, but just wanted to go ahead and post a few more questions and see how they should fit into the new design.

I've just finished reading this post by L. Spiro http://lspiroengine.com/?p=96 and that combined with the thread above has gotten things pretty much cleared up.  The last few questions I have are below.

 

1)  My rendering pipeline is structured such that it is based on a deferred shading renderer and backed by a forward renderer for transparent objects.  The issue I see here is for objects like, for example, a house that is mostly opaque but may have semi-transparent windows.  When I'm building out the render queue, should I just make sure that any subset of an object that is transparent is added to a special post-gbuffer / delayed render queue?  Or is there a better way to handle these cases?

 

2)  Does shadow mapping fall under the same overall advice as earlier in the thread?  Obviously, most objects simply need a super simple vertex shader to generate their data, but there's also the case for objects with alpha clip and/or tesselation that will need their own methods of getting to the shadow map.  In addition to rendering itself, should a model/terrain/object of whatever type also know how to draw itself into a shadow map?

 

I think with those two items buttoned up I'll be set to knock this out.

 

Thanks,

 

WFP

Edited by WFP

Share this post


Link to post
Share on other sites
haegarr    7372


Oh I see what you're saying. So instead of sending down the components, (mesh, materials), you add a render to a higher level that will send down more specific information like (vert buffers, index buffers, textures, shader constants), and then allow the render system to process those elements?

Close to, but not exactly. In fact, a renderer component is not an active component, i.e. it does not send something. Instead, the pipeline stage requests data from the renderer component. This is so because the structure of the pipeline (does it forward or deferred shading? can that stage be processed in a single pass on the given GPU or not?) is not a problem of the renderer component. The renderer component marks an entity as being renderable, denotes a graphic technique to use (e.g. physical lighting) as a criterion for the pipeline stages to pick the correct shaders, and grants an access point for the stages to get vertex data and constant blocks.

 

The following is related to the process of rendering as an example of what happens in detail behind the scenes. But it is not essential to the approach I've mentioned. You are welcome to read on, but notice that it might be more details than you want to know ;) As a background information, the engine is extremely data driven. Even the graphic pipeline is. So ...

 

A stage in the pipeline is responsible for visibility culling. It iterates the pool of provided renderer components (this is typically the total set of render components of the scene, but may be already filtered by a previously executed stage, e.g. by object layering). Any render component passing this stage is meant to be rendered. The following stage will apply some shader script. So it asks the render component for the technique to use. The result is used to determine the shader program that implements that technique (with respect to the functionality of the current stage, of course).

 

Let's use the vertex data as an example of what happens further. The shader program provides an interface declaration, from which a VertexInterface may be part of. A VertexInterface is a declaration what vertex attributes are expected at what slots, i.e. it describes the vertex input assembly in some way. Because a VertexInterface is attached to the shader program, the program obviously expects vertex data to be streamed in. So the pipeline stage requests a VertexConfiguration from the render component, where the VertexConfiguration has to met the given VertexInterface.

 

Ugh, what? To understand this, one need to know that a vertex source (be it a model's mesh, mesh generator, sprite batcher, or whatever) provides a VertexSignature. That is a description of which groups of vertex attributes exists, where a group is an interleaved structure of attributes with a common update frequency (immutable, static, transient, temporary are examples from the literature), and a attribute is basically an enum-value for a semantic and an enum-value for the data type. Now, using such a VertexSignature together with a wanted vertex capacity allows to allocate buffers from a graphics device. Because more than one buffer may be needed for a given VertexSignature (namely due to different update frequencies), a new structure is build which is named VertexCompilation. It says basically how many vertices with a defined set of attributes of defined data types can be stored by using which buffers. Notice that another vertex source with the same VertexSignature is allowed to use the same VertexCompilation, which effectively means vertex batching.

 

For now, we have a VertexCompilation which is a total set of vertex attribute data available from a vertex source. However, a specific shader program needs a subset of this (not necessarily a real subset, of course) and the attributes streamed into specific vertex inputs. So for every unique VertexInterface used in a graphic pipeline when using a VertexCompilation, a match of the VertexInterface with the VertexCompilation is done and remembered as so-called VertexConfiguration.

 

Well, all this VertexSomething things are structured blobs, so to say. They are addressed using indices; no pointers or something. And they are stored once they are created or read in as resource descriptors. That's make their use very efficient.

 

When the pipeline stage has requested a VertexConfiguration from the render component, and got an index number as result, it puts this into a graphic rendering job as state parameter like "activate vertex configuration #5". When the low-level graphic renderer interprets this, it makes a look up in its table of vertex configurations and finds a, say we're running OpenGL, name of a VAO associated with the index. If not, then the original VertexConfiguration is fetched by its index from the graphics support, and a new VAO is build.

Share this post


Link to post
Share on other sites
Seabolt    781

Okay let me make sure I've got this, you generate a list of RenderComponents, pass it to the RenderSystem, which when running it's rendering pass will query each component to determine whatever relevant information for each stage?

 

But then you run into the issue that your RenderComponents have to be locked when being processed by the RenderSystem, (assuming that there is the potential for multi-threaded rendering), where in my experience the RenderComponent is used as the interface between the Game and the RenderSystem.

 

Is that right?

Share this post


Link to post
Share on other sites
haegarr    7372
But then you run into the issue that your RenderComponents have to be locked when being processed by the RenderSystem, (assuming that there is the potential for multi-threaded rendering), where in my experience the RenderComponent is used as the interface between the Game and the RenderSystem.

With fine grained multi-threading: The pool of render components is a view onto the scene. If you want to make e.g. the visibility culling multi-threaded, then partition the pool into non-overlapping sub-pools and give each thread such a sub-pool. Then there is no need for locking. A stage that generates graphic jobs is different in that a common queue of jobs is wanted. So locking has to happen on writing a job into the queue, or else copy sections of sorting IDs together later on.

 

With "system based" multi-threading: In fact, the low-level renderer in my system runs in an own thread. Rendering is a multi-tier system. The mid-level rendering requests a GraphicFrameContext from the graphic device. The GraphicFrameContext is triple buffered. The request is blocked if the low-level rendering thread is two frames behind, otherwise a context is available and the mid-level rendering continues. Part of the context is a graphic jobs queue which is hence also triple buffered. So blocking may happen when the mid-level requests a context or commits a context for the low-level, or the low-level requests a context or grants a context for re-use. This happens with the frame rate, obviously. Also no need to lock render components, because the components play no role as soon as the graphic jobs are generated.

Edited by haegarr

Share this post


Link to post
Share on other sites
Seabolt    781

Interesting approach. I've written a couple of multi-threaded renderers, and they way I avoided duplicate render components was to create my own command buffer for communication between the game side and the low level renderer. So I would batch commands by draw calls, creating commands like SetVertexInput, SetRenderState, SetTexture, SetShader, SetShaderConstants, etc and send down those commands. 

 

I liked this approach because I could make the game side of the rendering system completely platform agnostic, and the renderer doesn't know anything about the game and can be platform independent.

 

But you're absolutely right that this pattern makes it difficult to alter the renderer functionality. 

 

I might try to make a combination of the two where the game side renderer process the list and then sends the command to the low level renderer.

 

Sorry I'm somewhat on a tangent, but I like discussing strategies rolleyes.gif

Share this post


Link to post
Share on other sites
WFP    2781

Hi all,

 

I was still hoping to get a little help with the items mentioned in Post #16 and thought of another issue to think about.

 

When building render queues, a lot of places I've been looking seem to recommend building out something of a descriptor key for each object - for example, a 64 bit unsigned integer where certain bits are reserved for shader id, texture id, pass type, depth, etc.

 

What should be responsible for filling this data out?  Should the model/terrain/water/whatever be able to fill this information out about themselves (and have to know what a RenderQueueKey is, or at least its format)?  When the setup is like my existing implementation and everything's kind of homogeneous (everything's a model) it seems a little clearer, but what's the best way to be able to do this once the render system is taking over after a scene update for everything else (including models, but also extending into terrain and anything else)?

Thanks for your help.

Share this post


Link to post
Share on other sites
L. Spiro    25638

a lot of places I've been looking seem to recommend building out something of a descriptor key for each object - for example, a 64 bit unsigned integer where certain bits are reserved for shader id, texture id, pass type, depth, etc.

This usually comes from this site, and while he has the right general idea, and while I have his invaluable book, with all due respect, he is not a graphics programmer.
In practice, you can almost never fit things into a single 64-bit integer, especially when you find 2 objects that have the same shader and vertex buffer and set of textures, etc. (which would make them a great candidate for instancing) but need to decide between them which to draw first, which of course always goes to depth (opaque objects should always be drawn front-to-back).  A sort key cannot be fewer than 96 bits, practically speaking.
 
By now it should be obvious that if you are thinking, “How can I make this work?”, it isn’t because you aren’t getting something, it is because what they’ve described simply doesn’t suit what you need.  It’s a fallacy to read a paper or a site and then get muddled in the details wondering why it won’t work for you.  It won’t work for you because it wasn’t designed by you for you.  If you need more bits, you need more bits.  Just because he suggested it, 64 bits is not the standard, and if you need more then you need more.  The idea, however, is still to reduce the bits you need to as few as possible.  Don’t feel bad if you can’t get it down to 64.  It has to be 96 anyway, and I myself always use 128 bits.
 
 

What should be responsible for filling this data out?

Should the model/terrain/water/whatever be able to fill this information out about themselves (and have to know what a RenderQueueKey is, or at least its format)?

Why not?
The graphics module doesn’t know what a model is etc. And a render-queue doesn’t need to care about those details either. It just needs to provide a structure and sort it.

If the model library, terrain library, etc. are all getting vertex buffers from the graphics library, there’s no reason they can’t also know the details they need to pass into a render-queue, also provided by the graphics library.

 

what's the best way to be able to do this once the render system is taking over after a scene

Trick question.
The graphics library is not providing any form of “render system”.
The scene manager orchestrates how objects are rendered. It relies on helpers such as render queues to determine the order in which to render, and each individual render is up to each individual object, so that terrain can do by itself vastly different operations from what standard models are doing.


L. Spiro Edited by L. Spiro

Share this post


Link to post
Share on other sites
WFP    2781

Hi L. Spiro,

 

Thanks for the great reply.  My issue was that I was thinking the RenderQueue was higher up in the hierarchy than it really needed to be.  Moving it down to part of the graphics module makes the picture a lot clearer.  And that's good to hear that going up to 128 bits for a key isn't uncommon - that seems to provide a lot more flexibility.

 

The only question I have left at this point is when it comes to shadow mapping.  Basically, should models, terrain, etc., all know how to draw themselves to a shadow map, as well.  That is, should these objects have both a renderStandard and renderShadowMap type of function that gets called at the appropriate time?  The main reason this is an issue is that, while for most objects a simple transform vertex shader and no pixel shader works great, it obviously doesn't handle cases where features like alpha clip (needs a pixel shader), or tesselation (hull + domain shaders) are required.

 

I have a method of working around this in my current implementation, but it's kludgy and while I'm redesigning this part of my engine to be more flexible, I'd like to update as much as possible in one go.  I think with that question answered, I'll be in good shape.

 

EDIT:  Thought of another question that someone may be able to clear up.  With the render queue items example explained thus far - how does the texture ID portion of it adapt to cases where an object uses more than one texture and/or a blend map?  Should the render queue item just base its texture ID on the first one used, since that's the most common case, or should it somehow be able to store the IDs of all textures used?

 

Thanks for your help!

Edited by WFP

Share this post


Link to post
Share on other sites
L. Spiro    25638

With the render queue items example explained thus far - how does the texture ID portion of it adapt to cases where an object uses more than one texture and/or a blend map?  Should the render queue item just base its texture ID on the first one used, since that's the most common case, or should it somehow be able to store the IDs of all textures used?

When you keep in mind that a render-queue is just for optimization, and the worst-case scenario is that things be drawn less optimally then they should be, you are free to make very big assumptions without worrying too much if they are correct for every single possible situation (plus you have the luxury of benchmarking to verify your assumptions).

You can assume safely that if any 2 objects are using the same diffuse texture, they are also using the exact same set of accompanying textures, such as normal maps etc. In practice, I’ve never heard of the case where 2 objects are using the same diffuse texture but different normal maps, and if such a rare case ever does appear, it gets drawn less-than-optimally. That’s obviously a better case than always slowing down every render by checking 128 textures on every single object.

In short, it is enough to test only the 1st texture.


The only question I have left at this point is when it comes to shadow mapping.  Basically, should models, terrain, etc., all know how to draw themselves to a shadow map, as well.  That is, should these objects have both a renderStandard and renderShadowMap type of function that gets called at the appropriate time?

The best solution is as you would expect the most time-consuming. Your graphics engine really should be fully data-driven, and allow shadow-map-creation to be just another data-defined pass.
In practice, this is almost never the case because people just don’t have that kind of time to wait to see anything drawn on the screen.

Until you can get things fully data-driven, yes, making a “hard-coded” pass for shadow-map-creation is typical.


And once again objects are deciding for themselves how to draw shadows. Opaque objects just return black, foliage has to do some discarding, and alpha objects need to run full shaders to get color information.


L. Spiro

Share this post


Link to post
Share on other sites
WFP    2781

Thanks again, for the well thought-out response.  I've already started building this out a little, and came to the same conclusion that you mentioned about texture IDs - if they're really varying after the first one (a very uncommon case), then it'll just have to be a sub-optimal draw.

 

Here's what I've come up with so far to describe RenderItems - items that act as the keys in the RenderQueue.  I've gone with a 128-bit approach like you suggested, and while I currently have a lot of empty space, I think it's good to have that extra room to grow into as the engine matures.

/*
 * HIGHSET - Bits 128 to 65
 *
 * Bits [128 - 125] - 1111
 *   Pass type - Opaque, Transparent, Shadow (Other ?)
 *
 * if Opaque or Shadow
 *   Bits [124 - 109] - 1111 1111 1111 1111
 *     Shader technique ID
 *   Bits [108 - 93] - 1111 1111 1111 1111
 *     Texture ID
 *   Bits [92 - 87] - 1111 11
 *     Subset
 *   Bits [86 - 65] - unused
 *    
 * else if Transparent (these switch because transparency has to be sorted back to front)
 *   Bits [124 - 93] - 1111 1111 1111 1111 1111 1111 1111 1111
 *     Depth
 *   Bits [92 - 77] - 1111 1111 1111 1111
 *     Shader technique ID
 *   Bits [76 - 65] - unused
 *
 * LOWSET - Bits 64 - 1
 * 
 * if Opaque or Shadow
 *   Bits [64 - 33] - 1111 1111 1111 1111 1111 1111 1111 1111
 *     Depth
 *   Bits [32 - 1] - unused
 *
 * else if Transparent
 *   Bits [64 - 49] - 1111 1111 1111 1111
 *     Texture ID
 *   Bits [48 - 43] - 1111 11
 *     Subset
 *   Bits [42 - 1] - unused
 *
 */

        class RenderItem
	{
	public:
		uint64 highSet;
		uint64 lowSet;
	};

	inline bool operator<(const RenderItem& lhs, const RenderItem& rhs)
	{
		return lhs.highSet < rhs.highSet ? true : lhs.highSet > rhs.highSet ? false :
			// highSets are equal so compare low sets
			lhs.lowSet < rhs.lowSet ? true : false;
	}

	inline bool operator>(const RenderItem& lhs, const RenderItem& rhs)
	{
		return rhs < lhs;
	}

Is there anything you see that's definitely missing that should be there before going any further?  One thing I know you've mentioned in a few posts is having a vertex buffer ID included.  Currently my vertex buffer object is just a very thin wrapper around the underlying ID3D11Buffer object holding the actual data.  It wouldn't be too much of a hassle to add an ID to each new buffer as they're created, though.  Is this something that should definitely be added?  Are there any other critiques, as well?

 

Thanks for all your help on this!

 

WFP

 

EDIT:  Corrected the math around the bit layouts.  More edits for formatting.

Edited by WFP

Share this post


Link to post
Share on other sites

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now

Sign in to follow this