WIP: Brush assets project #106303
|
@ -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();
|
||||
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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);
|
||||
|
|
|
@ -1067,6 +1067,8 @@ typedef struct NodeKuwaharaData {
|
|||
int uniformity;
|
||||
float sharpness;
|
||||
float eccentricity;
|
||||
char high_precision;
|
||||
char _pad[3];
|
||||
} NodeKuwaharaData;
|
||||
|
||||
typedef struct NodeAntiAliasingData {
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue