Compositor: Allow variable size Kuwahara

This patch changes the size property of the Kuwahara into a node socket
to allow variable size Kuwahara.

Pull Request: https://projects.blender.org/blender/blender/pulls/112946
This commit is contained in:
Omar Emara
2023-10-10 10:10:18 +02:00
committed by Omar Emara
parent 1a234805d0
commit 203559757a
11 changed files with 160 additions and 70 deletions

View File

@@ -24,10 +24,10 @@ void KuwaharaNode::convert_to_operations(NodeConverter &converter,
switch (data->variation) {
case CMP_NODE_KUWAHARA_CLASSIC: {
KuwaharaClassicOperation *operation = new KuwaharaClassicOperation();
operation->set_kernel_size(data->size);
converter.add_operation(operation);
converter.map_input_socket(get_input_socket(0), operation->get_input_socket(0));
converter.map_input_socket(get_input_socket(1), operation->get_input_socket(1));
converter.map_output_socket(get_output_socket(0), operation->get_output_socket());
break;
}
@@ -68,8 +68,10 @@ void KuwaharaNode::convert_to_operations(NodeConverter &converter,
converter.add_operation(kuwahara_anisotropic_operation);
converter.map_input_socket(get_input_socket(0),
kuwahara_anisotropic_operation->get_input_socket(0));
converter.map_input_socket(get_input_socket(1),
kuwahara_anisotropic_operation->get_input_socket(1));
converter.add_link(blur_y_operation->get_output_socket(0),
kuwahara_anisotropic_operation->get_input_socket(1));
kuwahara_anisotropic_operation->get_input_socket(2));
converter.map_output_socket(get_output_socket(0),
kuwahara_anisotropic_operation->get_output_socket(0));

View File

@@ -16,6 +16,7 @@ namespace blender::compositor {
KuwaharaAnisotropicOperation::KuwaharaAnisotropicOperation()
{
this->add_input_socket(DataType::Color);
this->add_input_socket(DataType::Value);
this->add_input_socket(DataType::Color);
this->add_output_socket(DataType::Color);
this->flags_.is_fullframe_operation = true;
@@ -24,12 +25,14 @@ KuwaharaAnisotropicOperation::KuwaharaAnisotropicOperation()
void KuwaharaAnisotropicOperation::init_execution()
{
image_reader_ = this->get_input_socket_reader(0);
structure_tensor_reader_ = this->get_input_socket_reader(1);
size_reader_ = this->get_input_socket_reader(1);
structure_tensor_reader_ = this->get_input_socket_reader(2);
}
void KuwaharaAnisotropicOperation::deinit_execution()
{
image_reader_ = nullptr;
size_reader_ = nullptr;
structure_tensor_reader_ = nullptr;
}
@@ -87,14 +90,18 @@ void KuwaharaAnisotropicOperation::execute_pixel_sampled(float output[4],
float eigenvalue_difference = first_eigenvalue - second_eigenvalue;
float anisotropy = eigenvalue_sum > 0.0f ? eigenvalue_difference / eigenvalue_sum : 0.0f;
float4 size;
size_reader_->read(size, x, y, nullptr);
float radius = max(0.0f, size.x);
/* Compute the width and height of an ellipse that is more width-elongated for high anisotropy
* and more circular for low anisotropy, controlled using the eccentricity factor. Since the
* anisotropy is in the [0, 1] range, the width factor tends to 1 as the eccentricity tends to
* infinity and tends to infinity when the eccentricity tends to zero. This is based on the
* equations in section "3.2. Anisotropic Kuwahara Filtering" of the paper. */
float ellipse_width_factor = (get_eccentricity() + anisotropy) / get_eccentricity();
float ellipse_width = ellipse_width_factor * data.size;
float ellipse_height = data.size / ellipse_width_factor;
float ellipse_width = ellipse_width_factor * radius;
float ellipse_height = radius / ellipse_width_factor;
/* Compute the cosine and sine of the angle that the eigenvector makes with the x axis. Since
* the eigenvector is normalized, its x and y components are the cosine and sine of the angle
@@ -126,7 +133,7 @@ void KuwaharaAnisotropicOperation::execute_pixel_sampled(float output[4],
* section "3 Alternative Weighting Functions" of the polynomial weights paper. More on this
* later in the code. */
const int number_of_sectors = 8;
float sector_center_overlap_parameter = 2.0f / data.size;
float sector_center_overlap_parameter = 2.0f / radius;
float sector_envelope_angle = ((3.0f / 2.0f) * M_PI) / number_of_sectors;
float cross_sector_overlap_parameter = (sector_center_overlap_parameter +
cos(sector_envelope_angle)) /
@@ -307,7 +314,7 @@ void KuwaharaAnisotropicOperation::update_memory_buffer_partial(MemoryBuffer *ou
for (BuffersIterator<float> it = output->iterate_with(inputs, area); !it.is_end(); ++it) {
/* The structure tensor is encoded in a float4 using a column major storage order, as can be
* seen in the KuwaharaAnisotropicStructureTensorOperation. */
float4 encoded_structure_tensor = float4(inputs[1]->get_elem(it.x, it.y));
float4 encoded_structure_tensor = float4(inputs[2]->get_elem(it.x, it.y));
float dxdx = encoded_structure_tensor.x;
float dxdy = encoded_structure_tensor.y;
float dydy = encoded_structure_tensor.w;
@@ -334,14 +341,16 @@ void KuwaharaAnisotropicOperation::update_memory_buffer_partial(MemoryBuffer *ou
float eigenvalue_difference = first_eigenvalue - second_eigenvalue;
float anisotropy = eigenvalue_sum > 0.0f ? eigenvalue_difference / eigenvalue_sum : 0.0f;
float radius = max(0.0f, *inputs[1]->get_elem(it.x, it.y));
/* Compute the width and height of an ellipse that is more width-elongated for high anisotropy
* and more circular for low anisotropy, controlled using the eccentricity factor. Since the
* anisotropy is in the [0, 1] range, the width factor tends to 1 as the eccentricity tends to
* infinity and tends to infinity when the eccentricity tends to zero. This is based on the
* equations in section "3.2. Anisotropic Kuwahara Filtering" of the paper. */
float ellipse_width_factor = (get_eccentricity() + anisotropy) / get_eccentricity();
float ellipse_width = ellipse_width_factor * data.size;
float ellipse_height = data.size / ellipse_width_factor;
float ellipse_width = ellipse_width_factor * radius;
float ellipse_height = radius / ellipse_width_factor;
/* Compute the cosine and sine of the angle that the eigenvector makes with the x axis. Since
* the eigenvector is normalized, its x and y components are the cosine and sine of the angle
@@ -374,7 +383,7 @@ void KuwaharaAnisotropicOperation::update_memory_buffer_partial(MemoryBuffer *ou
* section "3 Alternative Weighting Functions" of the polynomial weights paper. More on this
* later in the code. */
int number_of_sectors = 8;
float sector_center_overlap_parameter = 2.0f / data.size;
float sector_center_overlap_parameter = 2.0f / radius;
float sector_envelope_angle = ((3.0f / 2.0f) * M_PI) / number_of_sectors;
float cross_sector_overlap_parameter = (sector_center_overlap_parameter +
cos(sector_envelope_angle)) /

View File

@@ -12,6 +12,7 @@ namespace blender::compositor {
class KuwaharaAnisotropicOperation : public MultiThreadedOperation {
SocketReader *image_reader_;
SocketReader *size_reader_;
SocketReader *structure_tensor_reader_;
public:

View File

@@ -2,6 +2,7 @@
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include "BLI_math_base.hh"
#include "BLI_math_vector.hh"
#include "BLI_math_vector_types.hh"
@@ -14,8 +15,8 @@ namespace blender::compositor {
KuwaharaClassicOperation::KuwaharaClassicOperation()
{
this->add_input_socket(DataType::Color);
this->add_input_socket(DataType::Value);
this->add_output_socket(DataType::Color);
this->set_kernel_size(4);
this->flags_.is_fullframe_operation = true;
}
@@ -23,11 +24,13 @@ KuwaharaClassicOperation::KuwaharaClassicOperation()
void KuwaharaClassicOperation::init_execution()
{
image_reader_ = this->get_input_socket_reader(0);
size_reader_ = this->get_input_socket_reader(1);
}
void KuwaharaClassicOperation::deinit_execution()
{
image_reader_ = nullptr;
size_reader_ = nullptr;
}
void KuwaharaClassicOperation::execute_pixel_sampled(float output[4],
@@ -39,9 +42,13 @@ void KuwaharaClassicOperation::execute_pixel_sampled(float output[4],
float4 mean_of_squared_color[] = {float4(0.0f), float4(0.0f), float4(0.0f), float4(0.0f)};
int quadrant_pixel_count[] = {0, 0, 0, 0};
float4 size;
size_reader_->read_sampled(size, x, y, sampler);
const int kernel_size = int(math::max(0.0f, size[0]));
/* Split surroundings of pixel into 4 overlapping regions. */
for (int dy = -kernel_size_; dy <= kernel_size_; dy++) {
for (int dx = -kernel_size_; dx <= kernel_size_; dx++) {
for (int dy = -kernel_size; dy <= kernel_size; dy++) {
for (int dx = -kernel_size; dx <= kernel_size; dx++) {
int xx = x + dx;
int yy = y + dy;
@@ -102,21 +109,12 @@ void KuwaharaClassicOperation::execute_pixel_sampled(float output[4],
output[3] = mean_of_color[min_index].w; /* Also apply filter to alpha channel. */
}
void KuwaharaClassicOperation::set_kernel_size(int kernel_size)
{
kernel_size_ = kernel_size;
}
int KuwaharaClassicOperation::get_kernel_size()
{
return kernel_size_;
}
void KuwaharaClassicOperation::update_memory_buffer_partial(MemoryBuffer *output,
const rcti &area,
Span<MemoryBuffer *> inputs)
{
MemoryBuffer *image = inputs[0];
MemoryBuffer *size_image = inputs[1];
for (BuffersIterator<float> it = output->iterate_with(inputs, area); !it.is_end(); ++it) {
const int x = it.x;
@@ -126,9 +124,11 @@ void KuwaharaClassicOperation::update_memory_buffer_partial(MemoryBuffer *output
float4 mean_of_squared_color[] = {float4(0.0f), float4(0.0f), float4(0.0f), float4(0.0f)};
int quadrant_pixel_count[] = {0, 0, 0, 0};
const int kernel_size = int(math::max(0.0f, *size_image->get_elem(x, y)));
/* Split surroundings of pixel into 4 overlapping regions. */
for (int dy = -kernel_size_; dy <= kernel_size_; dy++) {
for (int dx = -kernel_size_; dx <= kernel_size_; dx++) {
for (int dy = -kernel_size; dy <= kernel_size; dy++) {
for (int dx = -kernel_size; dx <= kernel_size; dx++) {
int xx = x + dx;
int yy = y + dy;

View File

@@ -10,8 +10,7 @@ namespace blender::compositor {
class KuwaharaClassicOperation : public MultiThreadedOperation {
SocketReader *image_reader_;
int kernel_size_;
SocketReader *size_reader_;
public:
KuwaharaClassicOperation();
@@ -20,9 +19,6 @@ class KuwaharaClassicOperation : public MultiThreadedOperation {
void deinit_execution() override;
void execute_pixel_sampled(float output[4], float x, float y, PixelSampler sampler) override;
void set_kernel_size(int kernel_size);
int get_kernel_size();
void update_memory_buffer_partial(MemoryBuffer *output,
const rcti &area,
Span<MemoryBuffer *> inputs) override;

View File

@@ -56,6 +56,12 @@ void main()
float eigenvalue_difference = first_eigenvalue - second_eigenvalue;
float anisotropy = eigenvalue_sum > 0.0 ? eigenvalue_difference / eigenvalue_sum : 0.0;
#if defined(VARIABLE_SIZE)
float radius = max(0.0, texture_load(size_tx, texel).x);
#elif defined(CONSTANT_SIZE)
float radius = max(0.0, size);
#endif
/* Compute the width and height of an ellipse that is more width-elongated for high anisotropy
* and more circular for low anisotropy, controlled using the eccentricity factor. Since the
* anisotropy is in the [0, 1] range, the width factor tends to 1 as the eccentricity tends to

View File

@@ -10,6 +10,12 @@ void main()
{
ivec2 texel = ivec2(gl_GlobalInvocationID.xy);
#if defined(VARIABLE_SIZE)
int radius = max(0, int(texture_load(size_tx, texel).x));
#elif defined(CONSTANT_SIZE)
int radius = max(0, size);
#endif
vec4 mean_of_squared_color_of_quadrants[4] = vec4[](vec4(0.0), vec4(0.0), vec4(0.0), vec4(0.0));
vec4 mean_of_color_of_quadrants[4] = vec4[](vec4(0.0), vec4(0.0), vec4(0.0), vec4(0.0));

View File

@@ -6,20 +6,41 @@
GPU_SHADER_CREATE_INFO(compositor_kuwahara_classic_shared)
.local_group_size(16, 16)
.push_constant(Type::INT, "radius")
.image(0, GPU_RGBA16F, Qualifier::WRITE, ImageType::FLOAT_2D, "output_img")
.compute_source("compositor_kuwahara_classic.glsl");
GPU_SHADER_CREATE_INFO(compositor_kuwahara_classic)
GPU_SHADER_CREATE_INFO(compositor_kuwahara_classic_convolution_shared)
.additional_info("compositor_kuwahara_classic_shared")
.sampler(0, ImageType::FLOAT_2D, "input_tx")
.sampler(0, ImageType::FLOAT_2D, "input_tx");
GPU_SHADER_CREATE_INFO(compositor_kuwahara_classic_convolution_constant_size)
.additional_info("compositor_kuwahara_classic_convolution_shared")
.push_constant(Type::INT, "size")
.define("CONSTANT_SIZE")
.do_static_compilation(true);
GPU_SHADER_CREATE_INFO(compositor_kuwahara_classic_summed_area_table)
GPU_SHADER_CREATE_INFO(compositor_kuwahara_classic_convolution_variable_size)
.additional_info("compositor_kuwahara_classic_convolution_shared")
.sampler(1, ImageType::FLOAT_2D, "size_tx")
.define("VARIABLE_SIZE")
.do_static_compilation(true);
GPU_SHADER_CREATE_INFO(compositor_kuwahara_classic_summed_area_table_shared)
.additional_info("compositor_kuwahara_classic_shared")
.define("SUMMED_AREA_TABLE")
.sampler(0, ImageType::FLOAT_2D, "table_tx")
.sampler(1, ImageType::FLOAT_2D, "squared_table_tx")
.sampler(1, ImageType::FLOAT_2D, "squared_table_tx");
GPU_SHADER_CREATE_INFO(compositor_kuwahara_classic_summed_area_table_constant_size)
.additional_info("compositor_kuwahara_classic_summed_area_table_shared")
.push_constant(Type::INT, "size")
.define("CONSTANT_SIZE")
.do_static_compilation(true);
GPU_SHADER_CREATE_INFO(compositor_kuwahara_classic_summed_area_table_variable_size)
.additional_info("compositor_kuwahara_classic_summed_area_table_shared")
.sampler(2, ImageType::FLOAT_2D, "size_tx")
.define("VARIABLE_SIZE")
.do_static_compilation(true);
GPU_SHADER_CREATE_INFO(compositor_kuwahara_anisotropic_compute_structure_tensor)
@@ -29,13 +50,23 @@ GPU_SHADER_CREATE_INFO(compositor_kuwahara_anisotropic_compute_structure_tensor)
.compute_source("compositor_kuwahara_anisotropic_compute_structure_tensor.glsl")
.do_static_compilation(true);
GPU_SHADER_CREATE_INFO(compositor_kuwahara_anisotropic)
GPU_SHADER_CREATE_INFO(compositor_kuwahara_anisotropic_shared)
.local_group_size(16, 16)
.push_constant(Type::INT, "radius")
.push_constant(Type::FLOAT, "eccentricity")
.push_constant(Type::FLOAT, "sharpness")
.sampler(0, ImageType::FLOAT_2D, "input_tx")
.sampler(1, ImageType::FLOAT_2D, "structure_tensor_tx")
.image(0, GPU_RGBA16F, Qualifier::WRITE, ImageType::FLOAT_2D, "output_img")
.compute_source("compositor_kuwahara_anisotropic.glsl")
.compute_source("compositor_kuwahara_anisotropic.glsl");
GPU_SHADER_CREATE_INFO(compositor_kuwahara_anisotropic_constant_size)
.additional_info("compositor_kuwahara_anisotropic_shared")
.define("CONSTANT_SIZE")
.push_constant(Type::FLOAT, "size")
.do_static_compilation(true);
GPU_SHADER_CREATE_INFO(compositor_kuwahara_anisotropic_variable_size)
.additional_info("compositor_kuwahara_anisotropic_shared")
.define("VARIABLE_SIZE")
.sampler(2, ImageType::FLOAT_2D, "size_tx")
.do_static_compilation(true);

View File

@@ -1043,7 +1043,7 @@ typedef struct NodeBilateralBlurData {
} NodeBilateralBlurData;
typedef struct NodeKuwaharaData {
short size;
short size DNA_DEPRECATED;
short variation;
int uniformity;
float sharpness;

View File

@@ -8432,14 +8432,6 @@ static void def_cmp_kuwahara(StructRNA *srna)
{0, nullptr, 0, nullptr, nullptr},
};
prop = RNA_def_property(srna, "size", PROP_INT, PROP_NONE);
RNA_def_property_int_sdna(prop, nullptr, "size");
RNA_def_property_range(prop, 1.0, 100.0);
RNA_def_property_ui_range(prop, 1, 100, 1, -1);
RNA_def_property_ui_text(
prop, "Size", "Size of filter. Larger values give stronger stylized effect");
RNA_def_property_update(prop, NC_NODE | NA_EDITED, "rna_Node_update");
prop = RNA_def_property(srna, "variation", PROP_ENUM, PROP_NONE);
RNA_def_property_enum_sdna(prop, nullptr, "variation");
RNA_def_property_enum_items(prop, variation_items);

View File

@@ -32,6 +32,7 @@ static void cmp_node_kuwahara_declare(NodeDeclarationBuilder &b)
b.add_input<decl::Color>("Image")
.default_value({1.0f, 1.0f, 1.0f, 1.0f})
.compositor_domain_priority(0);
b.add_input<decl::Float>("Size").default_value(6.0f).compositor_domain_priority(1);
b.add_output<decl::Color>("Image");
}
@@ -41,7 +42,6 @@ static void node_composit_init_kuwahara(bNodeTree * /*ntree*/, bNode *node)
node->storage = data;
/* Set defaults. */
data->size = 6;
data->uniformity = 4;
data->eccentricity = 1.0;
data->sharpness = 0.5;
@@ -54,7 +54,6 @@ static void node_composit_buts_kuwahara(uiLayout *layout, bContext * /*C*/, Poin
col = uiLayoutColumn(layout, false);
uiItemR(col, ptr, "variation", UI_ITEM_NONE, nullptr, ICON_NONE);
uiItemR(col, ptr, "size", UI_ITEM_NONE, nullptr, ICON_NONE);
const int variation = RNA_enum_get(ptr, "variation");
@@ -90,19 +89,25 @@ class ConvertKuwaharaOperation : public NodeOperation {
{
/* For high radii, we accelerate the filter using a summed area table, making the filter
* execute in constant time as opposed to the trivial quadratic complexity. */
if (node_storage(bnode()).size > 5) {
Result &size_input = get_input("Size");
if (size_input.is_single_value() && size_input.get_float_value() > 5.0f) {
execute_classic_summed_area_table();
return;
}
GPUShader *shader = shader_manager().get("compositor_kuwahara_classic");
GPUShader *shader = shader_manager().get(get_classic_convolution_shader_name());
GPU_shader_bind(shader);
GPU_shader_uniform_1i(shader, "radius", node_storage(bnode()).size);
const Result &input_image = get_input("Image");
input_image.bind_as_texture(shader, "input_tx");
if (size_input.is_single_value()) {
GPU_shader_uniform_1i(shader, "size", int(size_input.get_float_value()));
}
else {
size_input.bind_as_texture(shader, "size_tx");
}
const Domain domain = compute_domain();
Result &output_image = get_result("Image");
output_image.allocate_texture(domain);
@@ -125,10 +130,16 @@ class ConvertKuwaharaOperation : public NodeOperation {
summed_area_table(
context(), get_input("Image"), squared_table, SummedAreaTableOperation::Square);
GPUShader *shader = shader_manager().get("compositor_kuwahara_classic_summed_area_table");
GPUShader *shader = shader_manager().get(get_classic_summed_area_table_shader_name());
GPU_shader_bind(shader);
GPU_shader_uniform_1i(shader, "radius", node_storage(bnode()).size);
Result &size_input = get_input("Size");
if (size_input.is_single_value()) {
GPU_shader_uniform_1i(shader, "size", int(size_input.get_float_value()));
}
else {
size_input.bind_as_texture(shader, "size_tx");
}
table.bind_as_texture(shader, "table_tx");
squared_table.bind_as_texture(shader, "squared_table_tx");
@@ -164,16 +175,23 @@ class ConvertKuwaharaOperation : public NodeOperation {
float2(node_storage(bnode()).uniformity));
structure_tensor.release();
GPUShader *shader = shader_manager().get("compositor_kuwahara_anisotropic");
GPUShader *shader = shader_manager().get(get_anisotropic_shader_name());
GPU_shader_bind(shader);
GPU_shader_uniform_1i(shader, "radius", node_storage(bnode()).size);
GPU_shader_uniform_1f(shader, "eccentricity", get_eccentricity());
GPU_shader_uniform_1f(shader, "sharpness", get_sharpness());
Result &input = get_input("Image");
input.bind_as_texture(shader, "input_tx");
Result &size_input = get_input("Size");
if (size_input.is_single_value()) {
GPU_shader_uniform_1f(shader, "size", size_input.get_float_value());
}
else {
size_input.bind_as_texture(shader, "size_tx");
}
smoothed_structure_tensor.bind_as_texture(shader, "structure_tensor_tx");
const Domain domain = compute_domain();
@@ -214,15 +232,44 @@ class ConvertKuwaharaOperation : public NodeOperation {
return structure_tensor;
}
/* The sharpness controls the sharpness of the transitions between the kuwahara sectors, which is
* controlled by the weighting function pow(standard_deviation, -sharpness) as can be seen in the
* shader. The transition is completely smooth when the sharpness is zero and completely sharp
* when it is infinity. But realistically, the sharpness doesn't change much beyond the value of
* 16 due to its exponential nature, so we just assume a maximum sharpness of 16.
const char *get_classic_convolution_shader_name()
{
if (is_constant_size()) {
return "compositor_kuwahara_classic_convolution_constant_size";
}
return "compositor_kuwahara_classic_convolution_variable_size";
}
const char *get_classic_summed_area_table_shader_name()
{
if (is_constant_size()) {
return "compositor_kuwahara_classic_summed_area_table_constant_size";
}
return "compositor_kuwahara_classic_summed_area_table_variable_size";
}
const char *get_anisotropic_shader_name()
{
if (is_constant_size()) {
return "compositor_kuwahara_anisotropic_constant_size";
}
return "compositor_kuwahara_anisotropic_variable_size";
}
bool is_constant_size()
{
return get_input("Size").is_single_value();
}
/* The sharpness controls the sharpness of the transitions between the kuwahara sectors, which
* is controlled by the weighting function pow(standard_deviation, -sharpness) as can be seen
* in the shader. The transition is completely smooth when the sharpness is zero and completely
* sharp when it is infinity. But realistically, the sharpness doesn't change much beyond the
* value of 16 due to its exponential nature, so we just assume a maximum sharpness of 16.
*
* The stored sharpness is in the range [0, 1], so we multiply by 16 to get it in the range
* [0, 16], however, we also square it before multiplication to slow down the rate of change near
* zero to counter its exponential nature for more intuitive user control. */
* [0, 16], however, we also square it before multiplication to slow down the rate of change
* near zero to counter its exponential nature for more intuitive user control. */
float get_sharpness()
{
const float sharpness_factor = node_storage(bnode()).sharpness;
@@ -236,12 +283,12 @@ class ConvertKuwaharaOperation : public NodeOperation {
* (eccentricity + anisotropy) / eccentricity
*
* Since the anisotropy is in the [0, 1] range, the factor tends to 1 as the eccentricity tends
* to infinity and tends to infinity when the eccentricity tends to zero. The stored eccentricity
* is in the range [0, 2], we map that to the range [infinity, 0.5] by taking the reciprocal,
* satisfying the aforementioned limits. The upper limit doubles the computed default
* eccentricity, which users can use to enhance the directionality of the filter. Instead of
* actual infinity, we just use an eccentricity of 1 / 0.01 since the result is very similar to
* that of infinity. */
* to infinity and tends to infinity when the eccentricity tends to zero. The stored
* eccentricity is in the range [0, 2], we map that to the range [infinity, 0.5] by taking the
* reciprocal, satisfying the aforementioned limits. The upper limit doubles the computed
* default eccentricity, which users can use to enhance the directionality of the filter.
* Instead of actual infinity, we just use an eccentricity of 1 / 0.01 since the result is very
* similar to that of infinity. */
float get_eccentricity()
{
return 1.0f / math::max(0.01f, node_storage(bnode()).eccentricity);