Blending Demystified
Sorry, I'm terrible at updating my blog, but I had an interesting debugging session learning about D3D Blend states. The MSDN documentation is severely lacking in my opinion, so I'm going to try and explain it here.
Suppose I have an image I'm about to draw to the screen. Let's say this image has the RGBA color of (1.0, 0.5, 0.0, 0.5) which looks like so:
And suppose I want to blend this with the background (0.0, 0.0, 1.0, 1.0) which might be something like the following:
How do I blend them together?
D3D has this nice option in the output merger stage, that allows you to blend what your draw did with whatever was previously in your render target. You do this by calling OMSetBlendState which takes an ID3D11BlendState object.
You set your blend state object with a series of flags that looks like so:
D3D11_BLEND_DESC bs;
ZeroMemory(&bs, sizeof(bs));
for (int i=0; i<8; i++)
{
bs.RenderTarget[i].BlendEnable = TRUE;
bs.RenderTarget[i].BlendOp = D3D11_BLEND_OP_ADD;
bs.RenderTarget[i].BlendOpAlpha = D3D11_BLEND_OP_ADD;
bs.RenderTarget[i].RenderTargetWriteMask = D3D11_COLOR_WRITE_ENABLE_ALL;
bs.RenderTarget[i].SrcBlend = D3D11_BLEND_ONE;
bs.RenderTarget[i].DestBlend = D3D11_BLEND_ZERO;
bs.RenderTarget[i].SrcBlendAlpha = D3D11_BLEND_ONE;
bs.RenderTarget[i].DestBlendAlpha = D3D11_BLEND_ONE;
}
What do all those flags mean?
This is where I feel like the documentation is lacking. There are 2 enums you have to deal with here:
- D3D11_BLEND_OP
- D3D11_BLEND_MODE
Let’s talk about each one separately.
BLEND_OP
The BLEND_OP is a little bit easier to understand. Basically when you are blending, you are combining the original color (the one already in the render target) with a new color (the one you are outputting on your current draw). The BLEND_OP specifies how you combine these colors after applying the blend mode. It’s one of:
- ADD
- SUBTRACT output from original
- SUBTRACT original from output
- MIN
- MAX
BLEND_MODE
BLEND_MODE is a little more complicated. This is where the confusion set in for me. BLEND_MODE has a bunch of different values, like SRC_COLOR, SRC_ALPHA etc. What these mean is where to get our multiplier from. The general formula for the blend is like so:
FinalColor = (SourceColor * Source BlendFactor) Blend Op (DestinationColor * Destination BlendFactor)
BLEND_MODE just lets us decide how to compute the BlendFactor in that equation above.
This makes a big matrix of possibilities. So what might the results be? I created an example table using just the R value from each of my colors:
|
Blend Multiplier (factor) |
Src Color ( just R) |
Dest Color (just R) |
Blend op |
Final Color |
BLEND_ZERO | 0 | 1.0 * 0.0 | 0.0 * 0.0 | OP_ADD | 0.0 |
BLEND_ONE | 1 | 1.0 * 1.0 | 0.0 * 1.0 | OP_ADD | 1.0 |
BLEND_SRC_COLOR | 1.0 | 1.0 * 1.0 | 0.0 * 0.0 | OP_ADD | 1.0 |
BLEND_INV_SRC_COLOR | 1.0 – 1.0 | 1.0 * (1.0 – 1.0) | 0.0 * (1.0 – 1.0) | OP_ADD | 0.0 |
BLEND_SRC_ALPHA | 0.5 | 1.0 * 0.5 | 0.0 * 0.5 | OP_ADD | 0.5 |
BLEND_INV_SRC_ALPHA | 1.0 – 0.5 | 1.0 * (1.0 – 0.5) | 0.0 * (1.0 – 0.5) | OP_ADD | 0.5 |
BLEND_DEST_ALPHA | 1.0 | 1.0 * 1.0 | 0.0 * 1.0 | OP_ADD | 1.0 |
BLEND_DEST_INV_ALPHA | 1.0 – 1.0 | 1.0 * (1.0 – 1.0) | 0.0 * (1.0 – 1.0) | OP_ADD | 0.0 |
BLEND_DEST_COLOR | 0.0 | 1.0 * 0.0 | 0.0 * 0.0 | OP_ADD | 0.0 |
BLEND_DEST_INV_COLOR | 1.0 – 0.0 | 1.0 * (1.0 – 0.0) | 0.0 * (1.0 – 0.0) | OP_ADD | 1.0 |
BLEND_SRC_ALPHA_SAT | min(0.5, 1-1) | 1.0 * 0.0 | 0.0 * 0.0 | OP_ADD | 0.0 |
BLEND_BLEND_FACTOR | blend factor on OMSetBlendState | 1.0 * f | 0.0 * f | OP_ADD | f |
BLEND_INV_BLEND_FACTOR | 1- blend factor set | 1.0 * (1.0 –f) | 0.0 * (1.0 –f) | OP_ADD | 1-f |
BLEND_SRC1_COLOR | color data from Pixel shader | 1.0 * c | 0.0 * c | OP_ADD | c |
BLEND_INV_SRC1_COLOR | 1- color data from pixel shader | 1.0 * (1-c) | 0.0 * (1-c) | OP_ADD | 1-c |
BLEND_SRC1_ALPHA | alpha from pixel shader | 1.0 * (a) | 0.0 * (a) | OP_ADD | a |
BLEND_INV_SRC1_ALPHA | 1- alpha from pixel shader | 1.0 * (1- a) | 0.0 * (1- a) | OP_ADD | 1- a |
Now this table is certainly not the limit of what the matrix might be, this table is simply specifying OP_ADD and using the same blend mode for both the source and destination, In a blend desc, this would be set like so:
D3D11_BLEND_DESC bs;
ZeroMemory(&bs, sizeof(bs));
for (int i=0; i<8; i++)
{
bs.RenderTarget[i].BlendEnable = TRUE;
bs.RenderTarget[i].BlendOp = D3D11_BLEND_OP_ADD;
bs.RenderTarget[i].BlendOpAlpha = D3D11_BLEND_OP_ADD;
bs.RenderTarget[i].RenderTargetWriteMask = D3D11_COLOR_WRITE_ENABLE_ALL;
bs.RenderTarget[i].SrcBlend = D3D11_BLEND_ONE;
bs.RenderTarget[i].DestBlend = D3D11_BLEND_ONE;
bs.RenderTarget[i].SrcBlendAlpha = D3D11_BLEND_ONE;
bs.RenderTarget[i].DestBlendAlpha = D3D11_BLEND_ONE;
}
This example is probably a silly idea, as you want to combine the destination and source in different ways. This blend mode above is just adding up all the colors together, more than likely getting something close to white each time.
The default (meaning no blending) would effectively be like so:
D3D11_BLEND_DESC bs;
ZeroMemory(&bs, sizeof(bs));
for (int i=0; i<8; i++)
{
bs.RenderTarget[i].BlendEnable = TRUE;
bs.RenderTarget[i].BlendOp = D3D11_BLEND_OP_ADD;
bs.RenderTarget[i].BlendOpAlpha = D3D11_BLEND_OP_ADD;
bs.RenderTarget[i].RenderTargetWriteMask = D3D11_COLOR_WRITE_ENABLE_ALL;
bs.RenderTarget[i].SrcBlend = D3D11_BLEND_ONE;
bs.RenderTarget[i].DestBlend = D3D11_BLEND_ZERO;
bs.RenderTarget[i].SrcBlendAlpha = D3D11_BLEND_ONE;
bs.RenderTarget[i].DestBlendAlpha = D3D11_BLEND_ZERO;
}
Although this is just the same as setting BlendEnable to false. Most of the time what people want to do is actually blend using the alpha component. This produces a nice effect where you can sort of see the stuff behind what you are drawing. A common way to do that would be to pick the following:
D3D11_BLEND_DESC bs;
ZeroMemory(&bs, sizeof(bs));
for (int i=0; i<8; i++)
{
bs.RenderTarget[i].BlendEnable = TRUE;
bs.RenderTarget[i].BlendOp = D3D11_BLEND_OP_ADD;
bs.RenderTarget[i].BlendOpAlpha = D3D11_BLEND_OP_MAX;
bs.RenderTarget[i].RenderTargetWriteMask = D3D11_COLOR_WRITE_ENABLE_ALL;
bs.RenderTarget[i].SrcBlend = D3D11_BLEND_SRC_ALPHA;
bs.RenderTarget[i].DestBlend = D3D11_BLEND_INV_SRC_ALPHA;
bs.RenderTarget[i].SrcBlendAlpha = D3D11_BLEND_ONE;
bs.RenderTarget[i].DestBlendAlpha = D3D11_BLEND_ONE;
}
What this does is makes the src alpha component more important in the final output, and destination color less important (the inverse applied). For our image above this would be:
Final Color = (Source Color * Source Alpha) + (Destination Color * Inverse Source Alpha)
Final ColorR = (1.0 * 0.5) + (0.0 * (1.0 – 0.5))
Final ColorG = (0.5 * 0.5) + (0.0 * (1.0 – 0.5))
Final ColorB = (0.0 * 0.5) + (1.0 * (1.0 – 0.5))
Final Color = (0.5, 0.25, 0.5)
This ends up looking like so when we overlay: