Efficient rendering of a dynamic grid

Started by
10 comments, last by dpadam450 8 years, 7 months ago

I'm trying to render a grid similar to Starcraft 2's build grid. I want each tile to change its color as efficiently as possible using a single shader for the whole grid.

OmFT6.jpg

On the CPU side, I have a byte[] (C#) which contain the state of each tile (free, occupied, conflict, etc.). These states change each time the player moves a building over the grid or builds it. My idea right now is to send this data to the GPU through a StructuredBuffer, submit a procedural drawcall, and somehow change the color of the tiles based on the data and the vertexID.

I have two potential problems with this:

  • There is no 8 bit scalar type in HLSL (like byte). Should I convert my byte[] to a uint[] and pass that to the buffer instead? I don't like the idea of having a duplicate of my data array. Is there a way to "fake" an 8 bit scalar in HLSL?
  • I'm using Unity, and the only way to update a buffer on the CPU side is to set the entire array of data. This could be done frequently (each time the player moves the building to another position). I wonder if there is a more GPU-centric approach to fetch the state of each tile in my grid.

I know this all sounds like premature optimization, but my main goal here is to learn new ways to think about these kinds of problems.

Thanks for any ideas.

Advertisement
Instead of a structured buffer, can you can use a "regular" buffer with DXGI_FORMAT_R8_UINT? If you use this, then it will work in the shader just as if it were a byte[] array, except that during the load the 8-bit value will be cast to a 32-bit uint (which is fine). If you can't do that with Unity, then you can still do it with a structured buffer. You'll just need to load 4 bytes at a time, and mask and shift the uint result in order to get the byte that you're looking for. Like this:


uint elemIdx = byteIdx / 4;
uint bufferValue = GridBuffer[elemIdx];
uint gridValue = (bufferValue >> (byteIdx % 4)) & 0xFF;
As for updating the buffer, how big is the buffer that you're updating? The drivers are definitely optimized for the case of updating GPU resources from the CPU, and it shouldn't be an issue unless your buffer is really big. Depending on how many tile states you have, you could also possibly reduce your memory requirements by packing your tile state into less than 8 bits.

This is way I would look at this, assuming you have your world grid evenly spaced across the map, and the state of each cell flagged as open or closed and the info for whether the current structure can be placed there (refinery) and whether its blocked by a unit, what you really want to draw is an WxH planar grid mesh of quads spaced the same as the world grid, which you can generate once at load time. Each quad would have its own 0.0 -> 1.0 texture space, to place the little faded corner texture on, and you could just change the vertex colors of the cells (quads) to match their state (yellow = neutral, green = ok, red = blocked), the whole thing would then look like a simple blended mesh draw, where you update the vertex colors when needed (when the grid is visible).

So you wouldnt exactly draw the cells as you have them stored in the world grid. You just draw a planar grid over top, blended, textured, with vertex colors matching the state based on the selected building/structure and your game logic.

@MJP The buffer would typically be 100x100 elements. I'll try packing the states in a uint[], with 16 2-bit states per uint. Then I'll send that to the StructuredBuffer.

@NumberXaero Correct me if I'm wrong, but I don't see how storing and updating vertex colors would be more efficient than sending states as a bye[] in a buffer. In both cases I need to update a buffer, which would be larger if I choose vertex colors.

You are doing this completely different from what I would do. If you know the size of that projected decal in world space, you can create a projection matrix and modelview matrix much like any shadow mapping or projective texturing. Then just render to another texture those surrounding terrain cells as a bunch of quads in the texture. Apply that overlay texture that has those "+" shapes in that decal you see. Then just apply the decal as projective texturing. Why you would need to update vertex colors and write a shader etc is not only going to be much more of a pain, but also you can see in SC2 they are doing it my way because you have those "+" grids, which are definitely in a texture. You can't get those shapes from a vertex color.

NBA2K, Madden, Maneater, Killing Floor, Sims http://www.pawlowskipinball.com/pinballeternal

@dpadam450 I think we're not talking about the same thing here. My question is not about the appearance of the grid. In fact, I do have a texture with the + shape and everything. What I need is an efficient way to send each tile's state (green, yellow, red) to the shader and THEN use this for shading. Or maybe I misunderstood your solution?

Edit: Right now I do have the same exact grid as SC2 visually. What I don't have are the tile colors matching the states (free, obstructed, etc).

If you want to have a color overlay I would just render the grid separately after you have already rendered the ground. To render the grid, I would just have one repeating texture for the tile and then either use vertex colors or a low res texture to define what each tile color should be.

My current game project Platform RPG

I ended up following MJP's advice, but with very tight packing inside a uint. This creates a very small buffer at the cost of code complexity (lots of bitwise operations). I'll benchmark my implementation against a more naive approach using uints for states to see if there are real benefits with packing.

Here's a glimpse of the vertex shader code to unpack the uint buffer:

3300c6aeb65ccbb495013e8366ea2578.png

Benchmark results to come.

Well then you are doing this badly/inefficiently.

If you are already projective texturing. I was suggested not manipulating vertex data and modulating that in the shader, I was saying create a new texture/FBO the size of your "+" texture. Grab the closest 10x10 tiles or whatever, do a for loop drawing green/red quads. Then draw your "+" texture overlayed on top of those red/green quads. Then just do your projective texturing that you said you already have. Case closed.

Why do you need to send /manipulate vertex data. Are you multiplying all vertices by "0" for all the squares that are outside of the grid? It all seems like a waste if you already have the projective grid texture.

NBA2K, Madden, Maneater, Killing Floor, Sims http://www.pawlowskipinball.com/pinballeternal

Grab the closest 10x10 tiles or whatever, do a for loop drawing green/red quads.

That's the part I don't understand. My grid can be up to 200x200 visible tiles. Are you suggesting I do 40k draw calls?

I'm not trying to challenge your idea here, I just genuinely want to understand your approach.

This topic is closed to new replies.

Advertisement