Documentation (as of 06-Apr-2020)

Basic loop

Here's a basic gradient which uses hardstops.

1     - type: gradient
2       bounds: 50 0 100 50
3       start: 0 0
4       end: 100 0
5       repeat: false
6       stops: [0.0, red , 0.5, yellow,
7               0.5, blue, 1.0, green ]
doc_repeat_basic_gradient.png

Because of the hard stops, these 4 stops cannot be baked into a gradient cache, since upscaling the cached strip would blur the transition from yellow to blue.

Thus emit_segments will detect this and in effect treat this as two separate gradients, side by side; each strip gets its own gradient cache strip, and its own quad primitive. The iterations inside emit_segments will look as follows:

1 emit_segments call:
2         start_offset: 0, end_offset: 1
3         prim_start_offset: 0, prim_end_offset: 1
4         gradient_offset_base: 0
5                 caching from offset 0 to 0.5
6                 and blitting to Rect(50.0×50.0 at (50.0,0.0))
7                 caching from offset 0.5 to 1
8                 and blitting to Rect(50.0×50.0 at (100.0,0.0))

Resulting in these draw calls...

doc_repeat_basic_gradient_drawcalls.png

... from this texture cache:

doc_repeat_basic_gradient_cache.png

Clamping

If the above gradient is in a primitive that is 300 pixels wide, with a start at 50 pixels and an end at 150 pixels, then the result is that red and green get clamped:

1     - type: gradient
2       bounds: 0 0 300 50
3       start: 50 0
4       end: 150 0
5       repeat: false
6       stops: [0.0, red  , 0.5, yellow,
7               0.5, blue, 1.0, green ]
doc_repeat_off.png

emit_segments is still called only once, but now the values of prim_start_offset and prim_end_offset are a bit more interesting. Measured in "gradient offsets", the primitive stretches from -0.5 to 2.5.

For simplicity, clamping is implemented by simply duplicating the first stop, on the primitive's left border (with a negative offset) and/or duplicating the last stop, on the primitive's right border (with an offset > 1):

1 emit_segments call:
2         start_offset: -0.5, end_offset: 2.5
3         prim_start_offset: -0.5, prim_end_offset: 2.5
4         gradient_offset_base: 0
5                 caching from offset -0.5 to 0.5
6                 and blitting to Rect(100.0×50.0 at (0.0,0.0))
7                 caching from offset 0.5 to 2.5
8                 and blitting to Rect(200.0×50.0 at (100.0,0.0))

Resulting in these draw calls...

doc_repeat_off_drawcalls.png

... from this texture cache:

doc_repeat_off_cache.png

Repeat

To repeat a gradient, emit_segments is called multiple times to march across the primitive. Each of these calls will be very similar to the "Basic Loop" case: no clamping, no duplicated stops -- just a gradient that we draw as if the primitive that contains it had been shrunk until the gradient no longer repeats.

In other words, emit_segments will pretend that the gradient is made up of multiple gradients side by side, to get around the limitation of "max 4 stops and no hard stops per cache". Then, an outer loop around that will pretend that the primitive is made up of multiple primitives side by side, each containing exactly one non-repeating gradient:

1     - type: gradient
2       bounds: 0 0 300 50
3       start: 50 0
4       end: 150 0
5       repeat: true
6       stops: [0.0, red  , 0.5, yellow,
7               0.5, blue, 1.0, green ]

Visual result:

doc_repeat.png

As we march along,

  • start_ and end_offset contain the gradient offsets of the "sub primitive" that we are drawing. Most of the time this will be 0.0 and 1.0 as we're drawing another full copy, except at the borders where the offsets get clipped;
  • prim_start_ and prim_end_offset never change and always contain the offset of the primitive's left and right most edge;
  • gradient_offset_base is the gradient offset of the left most edge of the unclipped "sub-primitive". See the illustrations further below.

The example gradient results in these calls to emit_segments:

 1 emit_segments call:
 2         start_offset: 0.5, end_offset: 1
 3         prim_start_offset: -0.5, prim_end_offset: 2.5
 4         gradient_offset_base: -1
 5                 caching from offset 0.5 to 1
 6                 and blitting to Rect(50.0×50.0 at (0.0,0.0))
 7 emit_segments call:
 8         start_offset: 0, end_offset: 1
 9         prim_start_offset: -0.5, prim_end_offset: 2.5
10         gradient_offset_base: 0
11                 caching from offset 0 to 0.5
12                 and blitting to Rect(50.0×50.0 at (50.0,0.0))
13                 caching from offset 0.5 to 1
14                 and blitting to Rect(50.0×50.0 at (100.0,0.0))
15 emit_segments call:
16         start_offset: 0, end_offset: 1
17         prim_start_offset: -0.5, prim_end_offset: 2.5
18         gradient_offset_base: 1
19                 caching from offset 0 to 0.5
20                 and blitting to Rect(50.0×50.0 at (150.0,0.0))
21                 caching from offset 0.5 to 1
22                 and blitting to Rect(50.0×50.0 at (200.0,0.0))
23 emit_segments call:
24         start_offset: 0, end_offset: 0.5
25         prim_start_offset: -0.5, prim_end_offset: 2.5
26         gradient_offset_base: 2
27                 caching from offset 0 to 0.5
28                 and blitting to Rect(50.0×50.0 at (250.00002,0.0))

... and these drawcalls (a single DrawElementsInstanced(6,6)):

doc_repeat_drawcalls.png

There are 4 calls to emit_segments, here's the second one:

doc_repeat_drawcalls_annotate.png

... and the first one:

doc_repeat_drawcalls_annotateB.png

gradient_offset_base is effectively the counter that we need to figure out the sub-primitive's position relative to the real primitive. By not folding this offset into start_offset and end_offset, each copy will use the same offsets to build a cache key, so we end up baking only a single red-to-orange and a single blue-to-green into the gradient cache.

doc_repeat_cache.png doc_repeat_drawcalls_annotateC.png ∎ QED