Source code

Revision control

Copy as Markdown

Other Tools

# Text Rendering
This document describes the details of how WebRender renders text, particularly the blending stage of text rendering.
We will go into grayscale text blending, subpixel text blending, and "subpixel text with background color" blending.
### Prerequisites
The description below assumes you're familiar with regular rgba compositing, operator over,
and the concept of premultiplied alpha.
### Not covered in this document
We are going to treat the origin of the text mask as a black box.
We're also going to assume we can blend text in the device color space and will not go into the gamma correction and linear pre-blending that happens in some of the backends that produce the text masks.
## Grayscale Text Blending
Grayscale text blending is the simplest form of text blending. Our blending function has three inputs:
- The text color, as a premultiplied rgba color.
- The text mask, as a single-channel alpha texture.
- The existing contents of the framebuffer that we're rendering to, the "destination". This is also a premultiplied rgba buffer.
Note: The word "grayscale" here does *not* mean that we can only draw gray text.
It means that the mask only has a single alpha value per pixel, so we can visualize
the mask in our minds as a grayscale image.
### Deriving the math
We want to mask our text color using the single-channel mask, and composite that to the destination.
This compositing step uses operator "over", just like regular compositing of rgba images.
I'll be using GLSL syntax to describe the blend equations, but please consider most of the code below pseudocode.
We can express the blending described above as the following blend equation:
```glsl
vec4 textblend(vec4 text_color, vec4 mask, vec4 dest) {
return over(in(text_color, mask), dest);
}
```
with `over` being the blend function for (premultiplied) operator "over":
```glsl
vec4 over(vec4 src, vec4 dest) {
return src + (1.0 - src.a) * dest;
}
```
and `in` being the blend function for (premultiplied) operator "in", i.e. the masking operator:
```glsl
vec4 in(vec4 src, vec4 mask) {
return src * mask.a;
}
```
So the complete blending function is:
```glsl
result.r = text_color.r * mask.a + (1.0 - text_color.a * mask.a) * dest.r;
result.g = text_color.g * mask.a + (1.0 - text_color.a * mask.a) * dest.g;
result.b = text_color.b * mask.a + (1.0 - text_color.a * mask.a) * dest.b;
result.a = text_color.a * mask.a + (1.0 - text_color.a * mask.a) * dest.a;
```
### Rendering this with OpenGL
In general, a fragment shader does not have access to the destination.
So the full blend equation needs to be expressed in a way that the shader only computes values that are independent of the destination,
and the parts of the equation that use the destination values need to be applied by the OpenGL blend pipeline itself.
The OpenGL blend pipeline can be tweaked using the functions `glBlendEquation` and `glBlendFunc`.
In our example, the fragment shader can output just `text_color * mask.a`:
```glsl
oFragColor = text_color * mask.a;
```
and the OpenGL blend pipeline can be configured like so:
```rust
pub fn set_blend_mode_premultiplied_alpha(&self) {
self.gl.blend_func(gl::ONE, gl::ONE_MINUS_SRC_ALPHA);
self.gl.blend_equation(gl::FUNC_ADD);
}
```
This results in an overall blend equation of
```
result.r = 1 * oFragColor.r + (1 - oFragColor.a) * dest.r;
^ ^ ^^^^^^^^^^^^^^^^^
| | |
+--gl::ONE | +-- gl::ONE_MINUS_SRC_ALPHA
|
+-- gl::FUNC_ADD
= 1 * (text_color.r * mask.a) + (1 - (text_color.a * mask.a)) * dest.r
= text_color.r * mask.a + (1 - text_color.a * mask.a) * dest.r
```
which is exactly what we wanted.
### Differences to the actual WebRender code
There are two minor differences between the shader code above and the actual code in the text run shader in WebRender:
```glsl
oFragColor = text_color * mask.a; // (shown above)
// vs.
oFragColor = vColor * mask * alpha; // (actual webrender code)
```
`vColor` is set to the text color. The differences are:
- WebRender multiplies with all components of `mask` instead of just with `mask.a`.
However, our font rasterization code fills the rgb values of `mask` with the value of `mask.a`,
so this is completely equivalent.
- WebRender applies another alpha to the text. This is coming from the clip.
You can think of this alpha to be a pre-adjustment of the text color for that pixel, or as an
additional mask that gets applied to the mask.
## Subpixel Text Blending
Now that we have the blend equation for single-channel text blending, we can look at subpixel text blending.
The main difference between subpixel text blending and grayscale text blending is the fact that,
for subpixel text, the text mask contains a separate alpha value for each color component.
### Component alpha
Regular painting uses four values per pixel: three color values, and one alpha value. The alpha value applies to all components of the pixel equally.
Imagine for a second a world in which you have *three alpha values per pixel*, one for each color component.
- Old world: Each pixel has four values: `color.r`, `color.g`, `color.b`, and `color.a`.
- New world: Each pixel has *six* values: `color.r`, `color.a_r`, `color.g`, `color.a_g`, `color.b`, and `color.a_b`.
In such a world we can define a component-alpha-aware operator "over":
```glsl
vec6 over_comp(vec6 src, vec6 dest) {
vec6 result;
result.r = src.r + (1.0 - src.a_r) * dest.r;
result.g = src.g + (1.0 - src.a_g) * dest.g;
result.b = src.b + (1.0 - src.a_b) * dest.b;
result.a_r = src.a_r + (1.0 - src.a_r) * dest.a_r;
result.a_g = src.a_g + (1.0 - src.a_g) * dest.a_g;
result.a_b = src.a_b + (1.0 - src.a_b) * dest.a_b;
return result;
}
```
and a component-alpha-aware operator "in":
```glsl
vec6 in_comp(vec6 src, vec6 mask) {
vec6 result;
result.r = src.r * mask.a_r;
result.g = src.g * mask.a_g;
result.b = src.b * mask.a_b;
result.a_r = src.a_r * mask.a_r;
result.a_g = src.a_g * mask.a_g;
result.a_b = src.a_b * mask.a_b;
return result;
}
```
and even a component-alpha-aware version of `textblend`:
```glsl
vec6 textblend_comp(vec6 text_color, vec6 mask, vec6 dest) {
return over_comp(in_comp(text_color, mask), dest);
}
```
This results in the following set of equations:
```glsl
result.r = text_color.r * mask.a_r + (1.0 - text_color.a_r * mask.a_r) * dest.r;
result.g = text_color.g * mask.a_g + (1.0 - text_color.a_g * mask.a_g) * dest.g;
result.b = text_color.b * mask.a_b + (1.0 - text_color.a_b * mask.a_b) * dest.b;
result.a_r = text_color.a_r * mask.a_r + (1.0 - text_color.a_r * mask.a_r) * dest.a_r;
result.a_g = text_color.a_g * mask.a_g + (1.0 - text_color.a_g * mask.a_g) * dest.a_g;
result.a_b = text_color.a_b * mask.a_b + (1.0 - text_color.a_b * mask.a_b) * dest.a_b;
```
### Back to the real world
If we want to transfer the component alpha blend equation into the real world, we need to make a few small changes:
- Our text color only needs one alpha value.
So we'll replace all instances of `text_color.a_r/g/b` with `text_color.a`.
- We're currently not making use of the mask's `r`, `g` and `b` values, only of the `a_r`, `a_g` and `a_b` values.
So in the real world, we can use the rgb channels of `mask` to store those component alphas and
replace `mask.a_r/g/b` with `mask.r/g/b`.
These two changes give us:
```glsl
result.r = text_color.r * mask.r + (1.0 - text_color.a * mask.r) * dest.r;
result.g = text_color.g * mask.g + (1.0 - text_color.a * mask.g) * dest.g;
result.b = text_color.b * mask.b + (1.0 - text_color.a * mask.b) * dest.b;
result.a_r = text_color.a * mask.r + (1.0 - text_color.a * mask.r) * dest.a_r;
result.a_g = text_color.a * mask.g + (1.0 - text_color.a * mask.g) * dest.a_g;
result.a_b = text_color.a * mask.b + (1.0 - text_color.a * mask.b) * dest.a_b;
```
There's a third change we need to make:
- We're rendering to a destination surface that only has one alpha channel instead of three.
So `dest.a_r/g/b` and `result.a_r/g/b` will need to become `dest.a` and `result.a`.
This creates a problem: We're currently assigning different values to `result.a_r`, `result.a_g` and `result.a_b`.
Which of them should we use to compute `result.a`?
This question does not have an answer. One alpha value per pixel is simply not sufficient
to express the same information as three alpha values.
However, see what happens if the destination is already opaque:
We have `dest.a_r == 1`, `dest.a_g == 1`, and `dest.a_b == 1`.
```
result.a_r = text_color.a * mask.r + (1 - text_color.a * mask.r) * dest.a_r
= text_color.a * mask.r + (1 - text_color.a * mask.r) * 1
= text_color.a * mask.r + 1 - text_color.a * mask.r
= 1
same for result.a_g and result.a_b
```
In other words, for opaque destinations, it doesn't matter what which channel of the mask we use when computing `result.a`, the result will always be completely opaque anyways. In WebRender we just pick `mask.g` (or rather,
have font rasterization set `mask.a` to the value of `mask.g`) because it's as good as any.
The takeaway here is: **Subpixel text blending is only supported for opaque destinations.** Attempting to render subpixel
text into partially transparent destinations will result in bad alpha values. Or rather, it will result in alpha values which
are not anticipated by the r, g, and b values in the same pixel, so that subsequent blend operations, which will mix r and a values
from the same pixel, will produce incorrect colors.
Here's the final subpixel blend function:
```glsl
vec4 subpixeltextblend(vec4 text_color, vec4 mask, vec4 dest) {
vec4 result;
result.r = text_color.r * mask.r + (1.0 - text_color.a * mask.r) * dest.r;
result.g = text_color.g * mask.g + (1.0 - text_color.a * mask.g) * dest.g;
result.b = text_color.b * mask.b + (1.0 - text_color.a * mask.b) * dest.b;
result.a = text_color.a * mask.a + (1.0 - text_color.a * mask.a) * dest.a;
return result;
}
```
or for short:
```glsl
vec4 subpixeltextblend(vec4 text_color, vec4 mask, vec4 dest) {
return text_color * mask + (1.0 - text_color.a * mask) * dest;
}
```
To recap, here's what we gained and lost by making the transition from the full-component-alpha world to the
regular rgba world: All colors and textures now only need four values to be represented, we still use a
component alpha mask, and the results are equivalent to the full-component-alpha result assuming that the
destination is opaque. We lost the ability to draw to partially transparent destinations.
### Making this work in OpenGL
We have the complete subpixel blend function.
Now we need to cut it into pieces and mix it with the OpenGL blend pipeline in such a way that
the fragment shader does not need to know about the destination.
Compare the equation for the red channel and the alpha channel between the two ways of text blending:
```
single-channel alpha:
result.r = text_color.r * mask.a + (1.0 - text_color.a * mask.a) * dest.r
result.a = text_color.a * mask.a + (1.0 - text_color.a * mask.a) * dest.r
component alpha:
result.r = text_color.r * mask.r + (1.0 - text_color.a * mask.r) * dest.r
result.a = text_color.a * mask.a + (1.0 - text_color.a * mask.a) * dest.r
```
Notably, in the single-channel alpha case, all three destination color channels are multiplied with the same thing:
`(1.0 - text_color.a * mask.a)`. This factor also happens to be "one minus `oFragColor.a`".
So we were able to take advantage of OpenGL's `ONE_MINUS_SRC_ALPHA` blend func.
In the component alpha case, we're not so lucky: Each destination color channel
is multiplied with a different factor. We can use `ONE_MINUS_SRC_COLOR` instead,
and output `text_color.a * mask` from our fragment shader.
But then there's still the problem that the first summand of the computation for `result.r` uses
`text_color.r * mask.r` and the second summand uses `text_color.a * mask.r`.
There are multiple ways to deal with this. They are:
1. Making use of `glBlendColor` and the `GL_CONSTANT_COLOR` blend func.
2. Using a two-pass method.
3. Using "dual source blending".
Let's look at them in order.
#### 1. Subpixel text blending in OpenGL using `glBlendColor`
In this approach we return `text_color.a * mask` from the shader.
Then we set the blend color to `text_color / text_color.a` and use `GL_CONSTANT_COLOR` as the source blendfunc.
This results in the following blend equation:
```
result.r = (text_color.r / text_color.a) * oFragColor.r + (1 - oFragColor.r) * dest.r;
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ^ ^^^^^^^^^^^^^^^^^
| | |
+--gl::CONSTANT_COLOR | +-- gl::ONE_MINUS_SRC_COLOR
|
+-- gl::FUNC_ADD
= (text_color.r / text_color.a) * (text_color.a * mask.r) + (1 - (text_color.a * mask.r)) * dest.r
= text_color.r * mask.r + (1 - text_color.a * mask.r) * dest.r
```
At the very beginning of this document, we defined `text_color` as the *premultiplied* text color.
So instead of actually doing the calculation `text_color.r / text_color.a` when specifying the blend color,
we really just want to use the *unpremultiplied* text color in that place.
That's usually the representation we start with anyway.
#### 2. Two-pass subpixel blending in OpenGL
The `glBlendColor` method has the disadvantage that the text color is part of the OpenGL state.
So if we want to draw text with different colors, we have two use separate batches / draw calls
to draw the differently-colored parts of text.
Alternatively, we can use a two-pass method which avoids the need to use the `GL_CONSTANT_COLOR` blend func:
- The first pass outputs `text_color.a * mask` from the fragment shader and
uses `gl::ZERO, gl::ONE_MINUS_SRC_COLOR` as the glBlendFuncs. This achieves:
```
oFragColor = text_color.a * mask;
result_after_pass0.r = 0 * oFragColor.r + (1 - oFragColor.r) * dest.r
= (1 - text_color.a * mask.r) * dest.r
result_after_pass0.g = 0 * oFragColor.g + (1 - oFragColor.g) * dest.r
= (1 - text_color.a * mask.r) * dest.r
...
```
- The second pass outputs `text_color * mask` from the fragment shader and uses
`gl::ONE, gl::ONE` as the glBlendFuncs. This results in the correct overall blend equation.
```
oFragColor = text_color * mask;
result_after_pass1.r
= 1 * oFragColor.r + 1 * result_after_pass0.r
= text_color.r * mask.r + result_after_pass0.r
= text_color.r * mask.r + (1 - text_color.a * mask.r) * dest.r
```
#### 3. Dual source subpixel blending in OpenGL
The third approach is similar to the second approach, but makes use of the [`ARB_blend_func_extended`](https://www.khronos.org/registry/OpenGL/extensions/ARB/ARB_blend_func_extended.txt) extension
in order to fold the two passes into one:
Instead of outputting the two different colors in two separate passes, we output them from the same pass,
as two separate fragment shader outputs.
Those outputs can then be treated as two different sources in the blend equation.