WIP: Brush assets project #106303

Draft
Julian Eisel wants to merge 354 commits from brush-assets-project into main

When changing the target branch, be careful to rebase the branch in your fork to match. See documentation.
18 changed files with 384 additions and 299 deletions
Showing only changes of commit c5243b0e0b - Show all commits

View File

@ -49,6 +49,11 @@ class DrawingRuntime {
*/
mutable SharedCache<Vector<uint3>> triangles_cache;
/**
* Normal vector cache for every stroke. Computed using Newell's method.
*/
mutable SharedCache<Vector<float3>> curve_plane_normals_cache;
/**
* Number of users for this drawing. The users are the frames in the Grease Pencil layers.
* Different frames can refer to the same drawing, so we need to make sure we count these users
@ -69,6 +74,10 @@ class Drawing : public ::GreasePencilDrawing {
* The triangles for all the fills in the geometry.
*/
Span<uint3> triangles() const;
/**
* Normal vectors for a plane that fits the stroke.
*/
Span<float3> curve_plane_normals() const;
void tag_positions_changed();
void tag_topology_changed();

View File

@ -293,6 +293,7 @@ Drawing::Drawing(const Drawing &other)
this->runtime = MEM_new<bke::greasepencil::DrawingRuntime>(__func__);
this->runtime->triangles_cache = other.runtime->triangles_cache;
this->runtime->curve_plane_normals_cache = other.runtime->curve_plane_normals_cache;
}
Drawing::~Drawing()
@ -357,6 +358,51 @@ Span<uint3> Drawing::triangles() const
return this->runtime->triangles_cache.data().as_span();
}
Span<float3> Drawing::curve_plane_normals() const
{
this->runtime->curve_plane_normals_cache.ensure([&](Vector<float3> &r_data) {
const CurvesGeometry &curves = this->strokes();
const Span<float3> positions = curves.positions();
const OffsetIndices<int> points_by_curve = curves.points_by_curve();
r_data.reinitialize(curves.curves_num());
threading::parallel_for(curves.curves_range(), 512, [&](const IndexRange range) {
for (const int curve_i : range) {
const IndexRange points = points_by_curve[curve_i];
if (points.size() < 2) {
r_data[curve_i] = float3(1.0f, 0.0f, 0.0f);
continue;
}
/* Calculate normal using Newell's method. */
float3 normal(0.0f);
float3 prev_point = positions[points.last()];
for (const int point_i : points) {
const float3 curr_point = positions[point_i];
add_newell_cross_v3_v3v3(normal, prev_point, curr_point);
prev_point = curr_point;
}
float length;
normal = math::normalize_and_get_length(normal, length);
/* Check for degenerate case where the points are on a line. */
if (math::is_zero(length)) {
for (const int point_i : points.drop_back(1)) {
float3 segment_vec = math::normalize(positions[point_i] - positions[point_i + 1]);
if (math::length_squared(segment_vec) != 0.0f) {
normal = float3(segment_vec.y, -segment_vec.x, 0.0f);
break;
}
}
}
r_data[curve_i] = normal;
}
});
});
return this->runtime->curve_plane_normals_cache.data().as_span();
}
const bke::CurvesGeometry &Drawing::strokes() const
{
return this->geometry.wrap();
@ -409,6 +455,7 @@ void Drawing::tag_positions_changed()
{
this->strokes_for_write().tag_positions_changed();
this->runtime->triangles_cache.tag_dirty();
this->runtime->curve_plane_normals_cache.tag_dirty();
}
void Drawing::tag_topology_changed()

View File

@ -999,7 +999,6 @@ static bNodeTreeInterfaceSocket *make_socket(const int uid,
const StringRef socket_type,
const NodeTreeInterfaceSocketFlag flag)
{
BLI_assert(!name.is_empty());
BLI_assert(!socket_type.is_empty());
const char *idname = socket_types::try_get_supported_socket_type(socket_type);

View File

@ -1732,7 +1732,7 @@ void BKE_pbvh_node_mark_redraw(PBVHNode *node)
void BKE_pbvh_node_mark_normals_update(PBVHNode *node)
{
node->flag |= PBVH_UpdateNormals | PBVH_UpdateDrawBuffers | PBVH_UpdateRedraw;
node->flag |= PBVH_UpdateNormals | PBVH_UpdateDrawBuffers | PBVH_UpdateRedraw | PBVH_UpdateBB;
}
void BKE_pbvh_node_fully_hidden_set(PBVHNode *node, int fully_hidden)

View File

@ -25,6 +25,7 @@ void KuwaharaNode::convert_to_operations(NodeConverter &converter,
switch (data->variation) {
case CMP_NODE_KUWAHARA_CLASSIC: {
KuwaharaClassicOperation *kuwahara_classic = new KuwaharaClassicOperation();
kuwahara_classic->set_data(data);
converter.add_operation(kuwahara_classic);
converter.map_input_socket(get_input_socket(0), kuwahara_classic->get_input_socket(0));
converter.map_input_socket(get_input_socket(1), kuwahara_classic->get_input_socket(1));

View File

@ -54,8 +54,10 @@ void KuwaharaClassicOperation::execute_pixel_sampled(float output[4],
size_reader_->read_sampled(size, x, y, sampler);
const int kernel_size = int(math::max(0.0f, size[0]));
/* Naive implementation is more accurate for small kernel sizes. */
if (kernel_size >= 4) {
/* For high radii, we accelerate the filter using a summed area table, making the filter
* execute in constant time as opposed to having quadratic complexity. Except if high precision
* is enabled, since summed area tables are less precise. */
if (!data_->high_precision && size[0] > 5.0f) {
for (int q = 0; q < 4; q++) {
/* A fancy expression to compute the sign of the quadrant q. */
int2 sign = int2((q % 2) * 2 - 1, ((q / 2) * 2 - 1));
@ -172,10 +174,13 @@ void KuwaharaClassicOperation::update_memory_buffer_partial(MemoryBuffer *output
float4 mean_of_squared_color[4] = {float4(0.0f), float4(0.0f), float4(0.0f), float4(0.0f)};
int quadrant_pixel_count[4] = {0, 0, 0, 0};
const int kernel_size = int(math::max(0.0f, *size_image->get_elem(x, y)));
const float size = *size_image->get_elem(x, y);
const int kernel_size = int(math::max(0.0f, size));
/* Naive implementation is more accurate for small kernel sizes. */
if (kernel_size >= 4) {
/* For high radii, we accelerate the filter using a summed area table, making the filter
* execute in constant time as opposed to having quadratic complexity. Except if high precision
* is enabled, since summed area tables are less precise. */
if (!data_->high_precision && size > 5.0f) {
for (int q = 0; q < 4; q++) {
/* A fancy expression to compute the sign of the quadrant q. */
int2 sign = int2((q % 2) * 2 - 1, ((q / 2) * 2 - 1));

View File

@ -9,6 +9,7 @@
namespace blender::compositor {
class KuwaharaClassicOperation : public MultiThreadedOperation {
const NodeKuwaharaData *data_;
SocketReader *image_reader_;
SocketReader *size_reader_;
SocketReader *sat_reader_;
@ -17,6 +18,11 @@ class KuwaharaClassicOperation : public MultiThreadedOperation {
public:
KuwaharaClassicOperation();
void set_data(const NodeKuwaharaData *data)
{
data_ = data;
}
void init_execution() override;
void deinit_execution() override;
void execute_pixel_sampled(float output[4], float x, float y, PixelSampler sampler) override;

View File

@ -35,6 +35,7 @@
#include "ED_grease_pencil.hh"
#include "ED_screen.hh"
#include "GEO_smooth_curves.hh"
#include "GEO_subdivide_curves.hh"
#include "WM_api.hh"
@ -112,191 +113,6 @@ static void keymap_grease_pencil_painting(wmKeyConfig *keyconf)
/** \name Smooth Stroke Operator
* \{ */
template<typename T>
static void gaussian_blur_1D(const Span<T> src,
const int64_t iterations,
const float influence,
const bool smooth_ends,
const bool keep_shape,
const bool is_cyclic,
MutableSpan<T> dst)
{
/**
* 1D Gaussian-like smoothing function.
*
* NOTE: This is the algorithm used by #BKE_gpencil_stroke_smooth_point (legacy),
* but generalized and written in C++.
*
* This function uses a binomial kernel, which is the discrete version of gaussian blur.
* The weight for a value at the relative index is:
* `w = nCr(n, j + n/2) / 2^n = (n/1 * (n-1)/2 * ... * (n-j-n/2)/(j+n/2)) / 2^n`.
* All weights together sum up to 1.
* This is equivalent to doing multiple iterations of averaging neighbors,
* where: `n = iterations * 2 and -n/2 <= j <= n/2`.
*
* Now the problem is that `nCr(n, j + n/2)` is very hard to compute for `n > 500`, since even
* double precision isn't sufficient. A very good robust approximation for `n > 20` is:
* `nCr(n, j + n/2) / 2^n = sqrt(2/(pi*n)) * exp(-2*j*j/n)`.
*
* `keep_shape` is a new option to stop the points from severely deforming.
* It uses different partially negative weights.
* `w = 2 * (nCr(n, j + n/2) / 2^n) - (nCr(3*n, j + n) / 2^(3*n))`
* ` ~ 2 * sqrt(2/(pi*n)) * exp(-2*j*j/n) - sqrt(2/(pi*3*n)) * exp(-2*j*j/(3*n))`
* All weights still sum up to 1.
* Note that these weights only work because the averaging is done in relative coordinates.
*/
BLI_assert(!src.is_empty());
BLI_assert(src.size() == dst.size());
/* Avoid computation if the there is just one point. */
if (src.size() == 1) {
return;
}
/* Weight Initialization. */
const int64_t n_half = keep_shape ? (iterations * iterations) / 8 + iterations :
(iterations * iterations) / 4 + 2 * iterations + 12;
double w = keep_shape ? 2.0 : 1.0;
double w2 = keep_shape ?
(1.0 / M_SQRT3) * exp((2 * iterations * iterations) / double(n_half * 3)) :
0.0;
Array<double> total_weight(src.size(), 0.0);
const int64_t total_points = src.size();
const int64_t last_pt = total_points - 1;
auto is_end_and_fixed = [smooth_ends, is_cyclic, last_pt](int index) {
return !smooth_ends && !is_cyclic && ELEM(index, 0, last_pt);
};
/* Initialize at zero. */
threading::parallel_for(dst.index_range(), 256, [&](const IndexRange range) {
for (const int64_t index : range) {
if (!is_end_and_fixed(index)) {
dst[index] = T(0);
}
}
});
for (const int64_t step : IndexRange(iterations)) {
const int64_t offset = iterations - step;
threading::parallel_for(dst.index_range(), 256, [&](const IndexRange range) {
for (const int64_t index : range) {
/* Filter out endpoints. */
if (is_end_and_fixed(index)) {
continue;
}
double w_before = w - w2;
double w_after = w - w2;
/* Compute the neighboring points. */
int64_t before = index - offset;
int64_t after = index + offset;
if (is_cyclic) {
before = (before % total_points + total_points) % total_points;
after = after % total_points;
}
else {
if (!smooth_ends && (before < 0)) {
w_before *= -before / float(index);
}
before = math::max(before, int64_t(0));
if (!smooth_ends && (after > last_pt)) {
w_after *= (after - (total_points - 1)) / float(total_points - 1 - index);
}
after = math::min(after, last_pt);
}
/* Add the neighboring values. */
const T bval = src[before];
const T aval = src[after];
const T cval = src[index];
dst[index] += (bval - cval) * w_before;
dst[index] += (aval - cval) * w_after;
/* Update the weight values. */
total_weight[index] += w_before;
total_weight[index] += w_after;
}
});
w *= (n_half + offset) / double(n_half + 1 - offset);
w2 *= (n_half * 3 + offset) / double(n_half * 3 + 1 - offset);
}
/* Normalize the weights. */
threading::parallel_for(dst.index_range(), 256, [&](const IndexRange range) {
for (const int64_t index : range) {
if (!is_end_and_fixed(index)) {
total_weight[index] += w - w2;
dst[index] = src[index] + influence * dst[index] / total_weight[index];
}
}
});
}
void gaussian_blur_1D(const GSpan src,
const int64_t iterations,
const float influence,
const bool smooth_ends,
const bool keep_shape,
const bool is_cyclic,
GMutableSpan dst)
{
bke::attribute_math::convert_to_static_type(src.type(), [&](auto dummy) {
using T = decltype(dummy);
/* Reduces unnecessary code generation. */
if constexpr (std::is_same_v<T, float> || std::is_same_v<T, float2> ||
std::is_same_v<T, float3>)
{
gaussian_blur_1D(src.typed<T>(),
iterations,
influence,
smooth_ends,
keep_shape,
is_cyclic,
dst.typed<T>());
}
});
}
static void smooth_curve_attribute(const OffsetIndices<int> points_by_curve,
const VArray<bool> &point_selection,
const VArray<bool> &cyclic,
const IndexMask &curves_to_smooth,
const int64_t iterations,
const float influence,
const bool smooth_ends,
const bool keep_shape,
GMutableSpan data)
{
curves_to_smooth.foreach_index(GrainSize(512), [&](const int curve_i) {
Vector<std::byte> orig_data;
const IndexRange points = points_by_curve[curve_i];
IndexMaskMemory memory;
const IndexMask selection_mask = IndexMask::from_bools(points, point_selection, memory);
if (selection_mask.is_empty()) {
return;
}
selection_mask.foreach_range([&](const IndexRange range) {
GMutableSpan dst_data = data.slice(range);
orig_data.resize(dst_data.size_in_bytes());
dst_data.type().copy_assign_n(dst_data.data(), orig_data.data(), range.size());
const GSpan src_data(dst_data.type(), orig_data.data(), range.size());
gaussian_blur_1D(
src_data, iterations, influence, smooth_ends, keep_shape, cyclic[curve_i], dst_data);
});
});
}
static int grease_pencil_stroke_smooth_exec(bContext *C, wmOperator *op)
{
const Scene *scene = CTX_data_scene(C);
@ -340,43 +156,43 @@ static int grease_pencil_stroke_smooth_exec(bContext *C, wmOperator *op)
if (smooth_position) {
bke::GSpanAttributeWriter positions = attributes.lookup_for_write_span("position");
smooth_curve_attribute(points_by_curve,
point_selection,
cyclic,
strokes,
iterations,
influence,
smooth_ends,
keep_shape,
positions.span);
geometry::smooth_curve_attribute(strokes,
points_by_curve,
point_selection,
cyclic,
iterations,
influence,
smooth_ends,
keep_shape,
positions.span);
positions.finish();
changed = true;
}
if (smooth_opacity && info.drawing.opacities().is_span()) {
bke::GSpanAttributeWriter opacities = attributes.lookup_for_write_span("opacity");
smooth_curve_attribute(points_by_curve,
point_selection,
cyclic,
strokes,
iterations,
influence,
smooth_ends,
false,
opacities.span);
geometry::smooth_curve_attribute(strokes,
points_by_curve,
point_selection,
cyclic,
iterations,
influence,
smooth_ends,
false,
opacities.span);
opacities.finish();
changed = true;
}
if (smooth_radius && info.drawing.radii().is_span()) {
bke::GSpanAttributeWriter radii = attributes.lookup_for_write_span("radius");
smooth_curve_attribute(points_by_curve,
point_selection,
cyclic,
strokes,
iterations,
influence,
smooth_ends,
false,
radii.span);
geometry::smooth_curve_attribute(strokes,
points_by_curve,
point_selection,
cyclic,
iterations,
influence,
smooth_ends,
false,
radii.span);
radii.finish();
changed = true;
}

View File

@ -181,61 +181,6 @@ void DrawingPlacement::project(const Span<float2> src, MutableSpan<float3> dst)
});
}
static float3 drawing_origin(const Scene *scene, const Object *object, char align_flag)
{
BLI_assert(object != nullptr && object->type == OB_GREASE_PENCIL);
if (align_flag & GP_PROJECT_VIEWSPACE) {
if (align_flag & GP_PROJECT_CURSOR) {
return float3(scene->cursor.location);
}
/* Use the object location. */
return float3(object->object_to_world[3]);
}
return float3(scene->cursor.location);
}
static float3 screen_space_to_3d(
const Scene *scene, const ARegion *region, const View3D *v3d, const Object *object, float2 co)
{
float3 origin = drawing_origin(scene, object, scene->toolsettings->gpencil_v3d_align);
float3 r_co;
ED_view3d_win_to_3d(v3d, region, origin, co, r_co);
return r_co;
}
float brush_radius_world_space(bContext &C, int x, int y)
{
ARegion *region = CTX_wm_region(&C);
View3D *v3d = CTX_wm_view3d(&C);
Scene *scene = CTX_data_scene(&C);
Object *object = CTX_data_active_object(&C);
Brush *brush = scene->toolsettings->gp_paint->paint.brush;
/* Default radius. */
float radius = 2.0f;
if (brush == nullptr || object->type != OB_GREASE_PENCIL) {
return radius;
}
/* Use an (arbitrary) screen space offset in the x direction to measure the size. */
const int x_offest = 64;
const float brush_size = float(BKE_brush_size_get(scene, brush));
/* Get two 3d coordinates to measure the distance from. */
const float2 screen1(x, y);
const float2 screen2(x + x_offest, y);
const float3 pos1 = screen_space_to_3d(scene, region, v3d, object, screen1);
const float3 pos2 = screen_space_to_3d(scene, region, v3d, object, screen2);
/* Clip extreme zoom level (and avoid division by zero). */
const float distance = math::max(math::distance(pos1, pos2), 0.001f);
/* Calculate the radius of the brush in world space. */
radius = (1.0f / distance) * (brush_size / 64.0f);
return radius;
}
static Array<int> get_frame_numbers_for_layer(const bke::greasepencil::Layer &layer,
const int current_frame,
const bool use_multi_frame_editing)

View File

@ -152,8 +152,6 @@ bool has_any_frame_selected(const bke::greasepencil::Layer &layer);
void create_keyframe_edit_data_selected_frames_list(KeyframeEditData *ked,
const bke::greasepencil::Layer &layer);
float brush_radius_world_space(bContext &C, int x, int y);
bool active_grease_pencil_poll(bContext *C);
bool editable_grease_pencil_poll(bContext *C);
bool editable_grease_pencil_point_selection_poll(bContext *C);
@ -204,14 +202,6 @@ void create_blank(Main &bmain, Object &object, int frame_number);
void create_stroke(Main &bmain, Object &object, float4x4 matrix, int frame_number);
void create_suzanne(Main &bmain, Object &object, float4x4 matrix, int frame_number);
void gaussian_blur_1D(const GSpan src,
int64_t iterations,
float influence,
bool smooth_ends,
bool keep_shape,
bool is_cyclic,
GMutableSpan dst);
int64_t ramer_douglas_peucker_simplify(IndexRange range,
float epsilon,
FunctionRef<float(int64_t, int64_t, int64_t)> dist_function,

View File

@ -21,6 +21,8 @@
#include "ED_grease_pencil.hh"
#include "ED_view3d.hh"
#include "GEO_smooth_curves.hh"
#include "WM_api.hh"
#include "WM_types.hh"
@ -278,13 +280,13 @@ struct PaintOperationExecutor {
* stable) fit. */
Array<float2> coords_pre_blur(smooth_window.size());
const int pre_blur_iterations = 3;
ed::greasepencil::gaussian_blur_1D(coords_to_smooth,
pre_blur_iterations,
settings_->active_smooth,
true,
true,
false,
coords_pre_blur.as_mutable_span());
geometry::gaussian_blur_1D(coords_to_smooth,
pre_blur_iterations,
settings_->active_smooth,
true,
true,
false,
coords_pre_blur.as_mutable_span());
/* Curve fitting. The output will be a set of handles (float2 triplets) in a flat array. */
const float max_error_threshold_px = 5.0f;

View File

@ -38,6 +38,7 @@ set(SRC
intern/resample_curves.cc
intern/reverse_uv_sampler.cc
intern/set_curve_type.cc
intern/smooth_curves.cc
intern/subdivide_curves.cc
intern/trim_curves.cc
intern/uv_pack.cc
@ -66,6 +67,7 @@ set(SRC
GEO_resample_curves.hh
GEO_reverse_uv_sampler.hh
GEO_set_curve_type.hh
GEO_smooth_curves.hh
GEO_subdivide_curves.hh
GEO_trim_curves.hh
GEO_uv_pack.hh

View File

@ -0,0 +1,42 @@
/* SPDX-FileCopyrightText: 2024 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#pragma once
#include "BLI_generic_span.hh"
#include "BLI_index_mask.hh"
#include "BLI_offset_indices.hh"
namespace blender::geometry {
/**
* 1D Gaussian-like smoothing function.
*
* \param iterations: Number of times to repeat the smoothing.
* \param smooth_ends: Smooth the first and last value.
* \param keep_shape: Changes the gaussian kernal to avoid severe deformations.
* \param is_cyclic: Propagate smoothing across the ends of the input as if they were connected.
*/
void gaussian_blur_1D(const GSpan src,
int iterations,
const float influence,
const bool smooth_ends,
const bool keep_shape,
const bool is_cyclic,
GMutableSpan dst);
/**
* Smoothes the \a attribute_data using a 1D gaussian blur.
*/
void smooth_curve_attribute(const IndexMask &curves_to_smooth,
const OffsetIndices<int> points_by_curve,
const VArray<bool> &point_selection,
const VArray<bool> &cyclic,
int iterations,
float influence,
bool smooth_ends,
bool keep_shape,
GMutableSpan attribute_data);
} // namespace blender::geometry

View File

@ -0,0 +1,205 @@
/* SPDX-FileCopyrightText: 2024 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include "BKE_attribute_math.hh"
#include "BLI_array.hh"
#include "BLI_generic_span.hh"
#include "BLI_index_mask.hh"
#include "BLI_index_range.hh"
#include "BLI_vector.hh"
#include "BLI_virtual_array.hh"
#include "GEO_smooth_curves.hh"
namespace blender::geometry {
template<typename T>
static void gaussian_blur_1D(const Span<T> src,
const int iterations,
const float influence,
const bool smooth_ends,
const bool keep_shape,
const bool is_cyclic,
MutableSpan<T> dst)
{
/**
* 1D Gaussian-like smoothing function.
*
* NOTE: This is the algorithm used by #BKE_gpencil_stroke_smooth_point (legacy),
* but generalized and written in C++.
*
* This function uses a binomial kernel, which is the discrete version of gaussian blur.
* The weight for a value at the relative index is:
* `w = nCr(n, j + n/2) / 2^n = (n/1 * (n-1)/2 * ... * (n-j-n/2)/(j+n/2)) / 2^n`.
* All weights together sum up to 1.
* This is equivalent to doing multiple iterations of averaging neighbors,
* where: `n = iterations * 2 and -n/2 <= j <= n/2`.
*
* Now the problem is that `nCr(n, j + n/2)` is very hard to compute for `n > 500`, since even
* double precision isn't sufficient. A very good robust approximation for `n > 20` is:
* `nCr(n, j + n/2) / 2^n = sqrt(2/(pi*n)) * exp(-2*j*j/n)`.
*
* `keep_shape` is a new option to stop the points from severely deforming.
* It uses different partially negative weights.
* `w = 2 * (nCr(n, j + n/2) / 2^n) - (nCr(3*n, j + n) / 2^(3*n))`
* ` ~ 2 * sqrt(2/(pi*n)) * exp(-2*j*j/n) - sqrt(2/(pi*3*n)) * exp(-2*j*j/(3*n))`
* All weights still sum up to 1.
* Note that these weights only work because the averaging is done in relative coordinates.
*/
BLI_assert(!src.is_empty());
BLI_assert(src.size() == dst.size());
/* Avoid computation if the there is just one point. */
if (src.size() == 1) {
return;
}
/* Weight Initialization. */
const int n_half = keep_shape ? (iterations * iterations) / 8 + iterations :
(iterations * iterations) / 4 + 2 * iterations + 12;
double w = keep_shape ? 2.0 : 1.0;
double w2 = keep_shape ?
(1.0 / M_SQRT3) * exp((2 * iterations * iterations) / double(n_half * 3)) :
0.0;
Array<double> total_weight(src.size(), 0.0);
const int64_t total_points = src.size();
const int64_t last_pt = total_points - 1;
auto is_end_and_fixed = [smooth_ends, is_cyclic, last_pt](int index) {
return !smooth_ends && !is_cyclic && ELEM(index, 0, last_pt);
};
/* Initialize at zero. */
threading::parallel_for(dst.index_range(), 1024, [&](const IndexRange range) {
for (const int64_t index : range) {
if (!is_end_and_fixed(index)) {
dst[index] = T(0);
}
}
});
/* Compute weights. */
for (const int64_t step : IndexRange(iterations)) {
const int64_t offset = iterations - step;
threading::parallel_for(dst.index_range(), 1024, [&](const IndexRange range) {
for (const int64_t index : range) {
/* Filter out endpoints. */
if (is_end_and_fixed(index)) {
continue;
}
double w_before = w - w2;
double w_after = w - w2;
/* Compute the neighboring points. */
int64_t before = index - offset;
int64_t after = index + offset;
if (is_cyclic) {
before = (before % total_points + total_points) % total_points;
after = after % total_points;
}
else {
if (!smooth_ends && (before < 0)) {
w_before *= -before / float(index);
}
before = math::max(before, int64_t(0));
if (!smooth_ends && (after > last_pt)) {
w_after *= (after - (total_points - 1)) / float(total_points - 1 - index);
}
after = math::min(after, last_pt);
}
/* Add the neighboring values. */
const T bval = src[before];
const T aval = src[after];
const T cval = src[index];
dst[index] += (bval - cval) * w_before;
dst[index] += (aval - cval) * w_after;
/* Update the weight values. */
total_weight[index] += w_before;
total_weight[index] += w_after;
}
});
w *= (n_half + offset) / double(n_half + 1 - offset);
w2 *= (n_half * 3 + offset) / double(n_half * 3 + 1 - offset);
}
/* Normalize the weights. */
threading::parallel_for(dst.index_range(), 1024, [&](const IndexRange range) {
for (const int64_t index : range) {
if (!is_end_and_fixed(index)) {
total_weight[index] += w - w2;
dst[index] = src[index] + influence * dst[index] / total_weight[index];
}
}
});
}
void gaussian_blur_1D(const GSpan src,
const int iterations,
const float influence,
const bool smooth_ends,
const bool keep_shape,
const bool is_cyclic,
GMutableSpan dst)
{
bke::attribute_math::convert_to_static_type(src.type(), [&](auto dummy) {
using T = decltype(dummy);
/* Only allow smoothing of float, float2, or float3. */
/* Reduces unnecessary code generation. */
if constexpr (std::is_same_v<T, float> || std::is_same_v<T, float2> ||
std::is_same_v<T, float3>)
{
gaussian_blur_1D(src.typed<T>(),
iterations,
influence,
smooth_ends,
keep_shape,
is_cyclic,
dst.typed<T>());
}
});
}
void smooth_curve_attribute(const IndexMask &curves_to_smooth,
const OffsetIndices<int> points_by_curve,
const VArray<bool> &point_selection,
const VArray<bool> &cyclic,
const int iterations,
const float influence,
const bool smooth_ends,
const bool keep_shape,
GMutableSpan attribute_data)
{
curves_to_smooth.foreach_index(GrainSize(512), [&](const int curve_i) {
Vector<std::byte> orig_data;
const IndexRange points = points_by_curve[curve_i];
IndexMaskMemory memory;
const IndexMask selection_mask = IndexMask::from_bools(points, point_selection, memory);
if (selection_mask.is_empty()) {
return;
}
selection_mask.foreach_range([&](const IndexRange range) {
GMutableSpan dst_data = attribute_data.slice(range);
orig_data.resize(dst_data.size_in_bytes());
dst_data.type().copy_assign_n(dst_data.data(), orig_data.data(), range.size());
const GSpan src_data(dst_data.type(), orig_data.data(), range.size());
gaussian_blur_1D(
src_data, iterations, influence, smooth_ends, keep_shape, cyclic[curve_i], dst_data);
});
});
}
} // namespace blender::geometry

View File

@ -366,14 +366,14 @@ GPUTexture *IMB_create_gpu_texture(const char *name,
bool freebuf = false;
/* Create Texture. */
tex = GPU_texture_create_2d(
name, UNPACK2(size), 9999, tex_format, GPU_TEXTURE_USAGE_SHADER_READ, nullptr);
/* Create Texture. Specifiy read usage to allow both shader and host reads, the latter is needed
* by the GPU compositor. */
const eGPUTextureUsage usage = GPU_TEXTURE_USAGE_SHADER_READ | GPU_TEXTURE_USAGE_HOST_READ;
tex = GPU_texture_create_2d(name, UNPACK2(size), 9999, tex_format, usage, nullptr);
if (tex == nullptr) {
size[0] = max_ii(1, size[0] / 2);
size[1] = max_ii(1, size[1] / 2);
tex = GPU_texture_create_2d(
name, UNPACK2(size), 9999, tex_format, GPU_TEXTURE_USAGE_SHADER_READ, nullptr);
tex = GPU_texture_create_2d(name, UNPACK2(size), 9999, tex_format, usage, nullptr);
do_rescale = true;
}
BLI_assert(tex != nullptr);

View File

@ -1067,6 +1067,8 @@ typedef struct NodeKuwaharaData {
int uniformity;
float sharpness;
float eccentricity;
char high_precision;
char _pad[3];
} NodeKuwaharaData;
typedef struct NodeAntiAliasingData {

View File

@ -8582,6 +8582,14 @@ static void def_cmp_kuwahara(StructRNA *srna)
RNA_def_property_ui_text(prop, "", "Variation of Kuwahara filter to use");
RNA_def_property_update(prop, NC_NODE | NA_EDITED, "rna_Node_update");
prop = RNA_def_property(srna, "use_high_precision", PROP_BOOLEAN, PROP_NONE);
RNA_def_property_boolean_sdna(prop, nullptr, "high_precision", 1);
RNA_def_property_ui_text(
prop,
"High Precision",
"Uses a more precise but slower method. Use if the output contains undesirable noise");
RNA_def_property_update(prop, NC_NODE | NA_EDITED, "rna_Node_update");
prop = RNA_def_property(srna, "uniformity", PROP_INT, PROP_NONE);
RNA_def_property_int_sdna(prop, nullptr, "uniformity");
RNA_def_property_range(prop, 0.0, 50.0);

View File

@ -57,7 +57,10 @@ static void node_composit_buts_kuwahara(uiLayout *layout, bContext * /*C*/, Poin
const int variation = RNA_enum_get(ptr, "variation");
if (variation == CMP_NODE_KUWAHARA_ANISOTROPIC) {
if (variation == CMP_NODE_KUWAHARA_CLASSIC) {
uiItemR(col, ptr, "use_high_precision", UI_ITEM_NONE, nullptr, ICON_NONE);
}
else if (variation == CMP_NODE_KUWAHARA_ANISOTROPIC) {
uiItemR(col, ptr, "uniformity", UI_ITEM_NONE, nullptr, ICON_NONE);
uiItemR(col, ptr, "sharpness", UI_ITEM_NONE, nullptr, ICON_NONE);
uiItemR(col, ptr, "eccentricity", UI_ITEM_NONE, nullptr, ICON_NONE);
@ -88,9 +91,12 @@ class ConvertKuwaharaOperation : public NodeOperation {
void execute_classic()
{
/* 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. */
* execute in constant time as opposed to having quadratic complexity. Except if high precision
* is enabled, since summed area tables are less precise. */
Result &size_input = get_input("Size");
if (size_input.is_single_value() && size_input.get_float_value() > 5.0f) {
if (!node_storage(bnode()).high_precision &&
(size_input.is_texture() || size_input.get_float_value() > 5.0f))
{
execute_classic_summed_area_table();
return;
}