1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
|
#pragma BLENDER_REQUIRE(gpu_shader_compositor_texture_utilities.glsl)
/* The Morphological Distance Threshold operation is effectively three consecutive operations
* implemented as a single operation. The three operations are as follows:
*
* .-----------. .--------------. .----------------.
* | Threshold |-->| Dilate/Erode |-->| Distance Inset |
* '-----------' '--------------' '----------------'
*
* The threshold operation just converts the input into a binary image, where the pixel is 1 if it
* is larger than 0.5 and 0 otherwise. Pixels that are 1 in the output of the threshold operation
* are said to be masked. The dilate/erode operation is a dilate or erode morphological operation
* with a circular structuring element depending on the sign of the distance, where it is a dilate
* operation if the distance is positive and an erode operation otherwise. This is equivalent to
* the Morphological Distance operation, see its implementation for more information. Finally, the
* distance inset is an operation that converts the binary image into a narrow band distance field.
* That is, pixels that are unmasked will remain 0, while pixels that are masked will start from
* zero at the boundary of the masked region and linearly increase until reaching 1 in the span of
* a number pixels given by the inset value.
*
* As a performance optimization, the dilate/erode operation is omitted and its effective result is
* achieved by slightly adjusting the distance inset operation. The base distance inset operation
* works by computing the signed distance from the current center pixel to the nearest pixel with a
* different value. Since our image is a binary image, that means that if the pixel is masked, we
* compute the signed distance to the nearest unmasked pixel, and if the pixel unmasked, we compute
* the signed distance to the nearest masked pixel. The distance is positive if the pixel is masked
* and negative otherwise. The distance is then normalized by dividing by the given inset value and
* clamped to the [0, 1] range. Since distances larger than the inset value are eventually clamped,
* the distance search window is limited to a radius equivalent to the inset value.
*
* To archive the effective result of the omitted dilate/erode operation, we adjust the distance
* inset operation as follows. First, we increase the radius of the distance search window by the
* radius of the dilate/erode operation. Then we adjust the resulting narrow band signed distance
* field as follows.
*
* For the erode case, we merely subtract the erode distance, which makes the outermost erode
* distance number of pixels zero due to clamping, consequently achieving the result of the erode,
* while retaining the needed inset because we increased the distance search window by the same
* amount we subtracted.
*
* Similarly, for the dilate case, we add the dilate distance, which makes the dilate distance
* number of pixels just outside of the masked region positive and part of the narrow band distance
* field, consequently achieving the result of the dilate, while at the same time, the innermost
* dilate distance number of pixels become 1 due to clamping, retaining the needed inset because we
* increased the distance search window by the same amount we added.
*
* Since the erode/dilate distance is already signed appropriately as described before, we just add
* it in both cases. */
void main()
{
ivec2 texel = ivec2(gl_GlobalInvocationID.xy);
/* Apply a threshold operation on the center pixel, where the threshold is currently hard-coded
* at 0.5. The pixels with values larger than the threshold are said to be masked. */
bool is_center_masked = texture_load(input_tx, texel).x > 0.5;
/* Since the distance search window will access pixels outside of the bounds of the image, we use
* a texture loader with a fallback value. And since we don't want those values to affect the
* result, the fallback value is chosen such that the inner condition fails, which is when the
* sampled pixel and the center pixel are the same, so choose a fallback that will be considered
* masked if the center pixel is masked and unmasked otherwise. */
vec4 fallback = vec4(is_center_masked ? 1.0 : 0.0);
/* Since the distance search window is limited to the given radius, the maximum possible squared
* distance to the center is double the squared radius. */
int minimum_squared_distance = radius * radius * 2;
/* Find the squared distance to the nearest different pixel in the search window of the given
* radius. */
for (int y = -radius; y <= radius; y++) {
for (int x = -radius; x <= radius; x++) {
bool is_sample_masked = texture_load(input_tx, texel + ivec2(x, y), fallback).x > 0.5;
if (is_center_masked != is_sample_masked) {
minimum_squared_distance = min(minimum_squared_distance, x * x + y * y);
}
}
}
/* Compute the actual distance from the squared distance and assign it an appropriate sign
* depending on whether it lies in a masked region or not. */
float signed_minimum_distance = sqrt(minimum_squared_distance) * (is_center_masked ? 1.0 : -1.0);
/* Add the erode/dilate distance and divide by the inset amount as described in the discussion,
* then clamp to the [0, 1] range. */
float value = clamp((signed_minimum_distance + distance) / inset, 0.0, 1.0);
imageStore(output_img, texel, vec4(value));
}
|