2
\$\begingroup\$

I know that branching is an expensive operation on GPU (not as much as it used to be, but still).

The most common situation where I use branching is when I have both textured and non-textured models/objects rendered with single shader (switching between shaders is also quite expensive and it only differs in 1-2 small parts).

For example, here's some pixel shader for rendering 2D objects:

constbuffer constBufferPerObject{
    float4 textureCoordORColor; //hasTexture is true => it's texCoord(x,y), color otherwise
    bool hasTexture;
};

...

float4 PS(VS_OUTPUT input) : SV_TARGET{
    if(hasTexture){
        //input.TexCoord was calculated in VS (it's meaningless if hasTexture is false)
        return ObjTexture.Sample(ObjSamplerState, input.TexCoord); 
    }else{
        return textureCoordORColor; 
    }
}

What would be a smart way to re-write the PS in order to avoid branching here?

\$\endgroup\$
  • \$\begingroup\$ Switching shaders may be expensive, but for the vast majority of geometry (opaque) you only have to do it once. Render nontextured geometry. Switch shaders. Render textured geometry. \$\endgroup\$ – bcrist Dec 14 '14 at 9:03
  • \$\begingroup\$ I am already switching shaders about 10 times per frame due to other more complicated effects. I would like to keep one shader in case of textured/not-textured objects (layers are involved - I would end up switching it for each layer in each frame). And same problem occurs several times (regardless of what shader do, most of them at some point have to render textured / not textured object). It's a case of 5-10 more switches per frame. Also I cannot just sort objects as: textured and not textured, because I use sorting for other cases and it would interrupt that (think about 3D case as well). \$\endgroup\$ – PolGraphic Dec 14 '14 at 9:09
  • 2
    \$\begingroup\$ use return textureCoordORColor * ObjTexture.Sample(ObjSamplerState, input.TexCoord); for everything and for objects that you don't want texture , you can assign a small 4x4 square white texture. \$\endgroup\$ – Raxvan Dec 14 '14 at 9:30
  • 1
    \$\begingroup\$ @PolGraphic For performance on PC it's questionable, modern hardware is very powerful so you have to test. On a mobile device should be more performant. Also 4x4 because i experienced issues with texture compressors in the past regarding small textures. Also mip mapping has to be taken into account and other factors (clamp/repeat texture, and if texture is in an atlas or not). \$\endgroup\$ – Raxvan Dec 14 '14 at 10:03
  • 1
    \$\begingroup\$ To add to Raxvan comment on 4x4: most compressed texture formats use a 4x4 cell size. 4x4 is a best-bet minimum size. ASTC has different sizes from 4x4 (minimum) up to 12x12, S3TC (DXT1) and ETC1 use 4x4 cells. GPU texture caches cells are often 4x4 in size (or larger) to accommodate the compressed formats so using a smaller texture will not gain you anything in performance anyway as the GPU needs to load the full 4x4 into its cache to read a single pixel. \$\endgroup\$ – Stephane Hockenhull Dec 15 '14 at 2:14
4
\$\begingroup\$

For best performance you should avoid changing uniforms as well as shaders by using vertex attributes.

Ideally pack all your 2D object textures into 1 single large texture (called an atlas) so you can render all your 2D objects in one call using a single vertex buffer.

In your large texture you also put a 4x4 white area and map the texture coordinates of all your color-only elements to the center of that white square.

Pixel shader:

float4 PS(VS_OUTPUT input) : SV_TARGET{
  return ObjTexture.Sample(ObjSamplerState, input.TexCoord) * input.Color; 
}

If instead you were fetching at garbage texture coordinates randomly all over the place and ignoring it you'd be trashing the texture cache needlessly.

Fetching the white texture area lets the input.Color pass through, conversely using a white color lets the texture pass through.

Your vertex format needs UV coordinates (2 unsigned shorts should be enough precision), and 1 RGBA color (4 unsigned bytes) as well as the vertex coordinates (2 floats), for a total of 16 bytes per vertex, a good vertex size and alignment. If you need your 2D elements in 3D space then you'll need a larger vertex format.

With that you can draw all your 2D elements in one single draw call.

And on top of this by using pre-multiplied alpha you can even combine alpha blending and additive blending (glowing particles) in one single draw call.

Blend = SRC * ONE + DST * ONE_MINUS_SRC_ALPHA;

By using an alpha of 0 in SRC and a color value > 0 you get an additive blending. To get regular blending the color has to be pre-multiplied with its alpha value so that when alpha is 0 then color in the texture and/or vertex is also 0.

\$\endgroup\$
  • 1
    \$\begingroup\$ Using unsigned short (16 bits) texture coordinates on a pixel scale lets you have an atlas of up to 65536 x 65536, if you use 4x4 pixel units then you could go up to 262144 x 262144 (if GPUs support this size already). Compacting vertices improve performance on mobile GPUs. On desktops other things tend to be the bottleneck. \$\endgroup\$ – Stephane Hockenhull Dec 15 '14 at 2:22
  • \$\begingroup\$ Fantastic, thank you :) and of course, texture atlas is a way to go, but somehow I did not think about including that white texture there. \$\endgroup\$ – PolGraphic Dec 15 '14 at 4:24

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.