GPv3: Draw Tool: Jitter option

This adds the `Jitter` draw tool option in the randomize panel.

To make this work in combination with the active smoothing, the jitter of the positions is applied
after active smoothing as an effect on top.
This means that the active smooth will not smooth the jittered points.

In addition, it is also now allowed to raise the `Jitter` factor above 1 for a more extreme effect.

Note: The jittering worked a bit differently in GPv2 (probably because of a bug). In GPv3 we compute the cotangent using the smoothed stroke direction (over time), which is a lot more acurate then using the previous and current position for the direction.

Pull Request: https://projects.blender.org/blender/blender/pulls/123680
This commit is contained in:
Falk David
2024-06-24 16:32:13 +02:00
committed by Falk David
parent 3e28b6c7f1
commit 9ffb277e01
2 changed files with 191 additions and 126 deletions

View File

@@ -18,6 +18,7 @@
#include "BLI_math_base.hh"
#include "BLI_math_color.h"
#include "BLI_math_geom.h"
#include "BLI_rand.hh"
#include "DEG_depsgraph_query.hh"
@@ -125,15 +126,114 @@ static void morph_points_to_curve(Span<float2> src, Span<float2> target, Mutable
dst.last() = src.last();
}
/**
* Creates a new curve with one point at the beginning or end.
* \note Does not initialize the new curve or points.
*/
static void create_blank_curve(bke::CurvesGeometry &curves, const bool on_back)
{
if (!on_back) {
const int num_old_points = curves.points_num();
curves.resize(curves.points_num() + 1, curves.curves_num() + 1);
curves.offsets_for_write().last(1) = num_old_points;
return;
}
curves.resize(curves.points_num() + 1, curves.curves_num() + 1);
MutableSpan<int> offsets = curves.offsets_for_write();
offsets.first() = 0;
/* Loop through backwards to not overwrite the data. */
for (int i = curves.curves_num() - 2; i >= 0; i--) {
offsets[i + 1] = offsets[i] + 1;
}
bke::MutableAttributeAccessor attributes = curves.attributes_for_write();
attributes.for_all(
[&](const bke::AttributeIDRef &id, const bke::AttributeMetaData /*meta_data*/) {
bke::GSpanAttributeWriter dst = attributes.lookup_for_write_span(id);
GMutableSpan attribute_data = dst.span;
bke::attribute_math::convert_to_static_type(attribute_data.type(), [&](auto dummy) {
using T = decltype(dummy);
MutableSpan<T> span_data = attribute_data.typed<T>();
/* Loop through backwards to not overwrite the data. */
for (int i = span_data.size() - 2; i >= 0; i--) {
span_data[i + 1] = span_data[i];
}
});
dst.finish();
return true;
});
}
/**
* Extends the first or last curve by `new_points_num` number of points.
* \note Does not initialize the new points.
*/
static void extend_curve(bke::CurvesGeometry &curves, const bool on_back, const int new_points_num)
{
if (!on_back) {
curves.resize(curves.points_num() + new_points_num, curves.curves_num());
curves.offsets_for_write().last() = curves.points_num();
return;
}
const int last_active_point = curves.points_by_curve()[0].last();
curves.resize(curves.points_num() + new_points_num, curves.curves_num());
MutableSpan<int> offsets = curves.offsets_for_write();
for (const int src_curve : curves.curves_range().drop_front(1)) {
offsets[src_curve] = offsets[src_curve] + new_points_num;
}
offsets.last() = curves.points_num();
bke::MutableAttributeAccessor attributes = curves.attributes_for_write();
attributes.for_all([&](const bke::AttributeIDRef &id, const bke::AttributeMetaData meta_data) {
if (meta_data.domain != bke::AttrDomain::Point) {
return true;
}
bke::GSpanAttributeWriter dst = attributes.lookup_for_write_span(id);
GMutableSpan attribute_data = dst.span;
bke::attribute_math::convert_to_static_type(attribute_data.type(), [&](auto dummy) {
using T = decltype(dummy);
MutableSpan<T> span_data = attribute_data.typed<T>();
/* Loop through backwards to not overwrite the data. */
for (int i = (span_data.size() - 1) - new_points_num; i >= last_active_point; i--) {
span_data[i + new_points_num] = span_data[i];
}
});
dst.finish();
return true;
});
curves.tag_topology_changed();
}
class PaintOperation : public GreasePencilStrokeOperation {
private:
/* Screen space coordinates from input samples. */
Vector<float2> screen_space_coords_orig_;
/* Temporary vector of curve fitted screen space coordinates per input sample from the active
* smoothing window. */
* smoothing window. The length of this depends on `active_smooth_start_index_`. */
Vector<Vector<float2>> screen_space_curve_fitted_coords_;
/* Temporary vector of screen space offsets */
Vector<float2> screen_space_jitter_offsets_;
/* Screen space coordinates after smoothing. */
Vector<float2> screen_space_smoothed_coords_;
/* Screen space coordinates after smoothing and jittering. */
Vector<float2> screen_space_final_coords_;
/* The start index of the smoothing window. */
int active_smooth_start_index_ = 0;
blender::float4x2 texture_space_ = float4x2::identity();
@@ -141,8 +241,10 @@ class PaintOperation : public GreasePencilStrokeOperation {
/* Helper class to project screen space coordinates to 3d. */
ed::greasepencil::DrawingPlacement placement_;
/* Angle factor smoothed over time. */
float smoothed_angle_factor_ = 1.0f;
/* Direction the pen is moving in smoothed over time. */
float2 smoothed_pen_direction_ = float2(0.0f);
RandomNumberGenerator rng;
friend struct PaintOperationExecutor;
@@ -167,6 +269,8 @@ struct PaintOperationExecutor {
std::optional<ColorGeometry4f> fill_color_;
float softness_;
bool use_settings_random_;
bke::greasepencil::Drawing *drawing_;
PaintOperationExecutor(const bContext &C)
@@ -179,6 +283,7 @@ struct PaintOperationExecutor {
brush_ = BKE_paint_brush(paint);
settings_ = brush_->gpencil_settings;
use_settings_random_ = (settings_->flag & GP_BRUSH_GROUP_RANDOM) != 0;
const bool use_vertex_color = (scene_->toolsettings->gp_paint->mode ==
GPPAINT_FLAG_USE_VERTEXCOLOR);
if (use_vertex_color) {
@@ -203,100 +308,6 @@ struct PaintOperationExecutor {
BLI_assert(drawing_ != nullptr);
}
/**
* Creates a new curve with one point at the beginning or end.
* \note Does not initialize the new curve or points.
*/
static void create_blank_curve(bke::CurvesGeometry &curves, const bool on_back)
{
if (!on_back) {
const int num_old_points = curves.points_num();
curves.resize(curves.points_num() + 1, curves.curves_num() + 1);
curves.offsets_for_write().last(1) = num_old_points;
return;
}
curves.resize(curves.points_num() + 1, curves.curves_num() + 1);
MutableSpan<int> offsets = curves.offsets_for_write();
offsets.first() = 0;
/* Loop through backwards to not overwrite the data. */
for (int i = curves.curves_num() - 2; i >= 0; i--) {
offsets[i + 1] = offsets[i] + 1;
}
bke::MutableAttributeAccessor attributes = curves.attributes_for_write();
attributes.for_all(
[&](const bke::AttributeIDRef &id, const bke::AttributeMetaData /*meta_data*/) {
bke::GSpanAttributeWriter dst = attributes.lookup_for_write_span(id);
GMutableSpan attribute_data = dst.span;
bke::attribute_math::convert_to_static_type(attribute_data.type(), [&](auto dummy) {
using T = decltype(dummy);
MutableSpan<T> span_data = attribute_data.typed<T>();
/* Loop through backwards to not overwrite the data. */
for (int i = span_data.size() - 2; i >= 0; i--) {
span_data[i + 1] = span_data[i];
}
});
dst.finish();
return true;
});
}
/**
* Extends the first or last curve by `new_points_num` number of points.
* \note Does not initialize the new points.
*/
static void extend_curve(bke::CurvesGeometry &curves,
const bool on_back,
const int new_points_num)
{
if (!on_back) {
curves.resize(curves.points_num() + new_points_num, curves.curves_num());
curves.offsets_for_write().last() = curves.points_num();
return;
}
const int last_active_point = curves.points_by_curve()[0].last();
curves.resize(curves.points_num() + new_points_num, curves.curves_num());
MutableSpan<int> offsets = curves.offsets_for_write();
for (const int src_curve : curves.curves_range().drop_front(1)) {
offsets[src_curve] = offsets[src_curve] + new_points_num;
}
offsets.last() = curves.points_num();
bke::MutableAttributeAccessor attributes = curves.attributes_for_write();
attributes.for_all([&](const bke::AttributeIDRef &id, const bke::AttributeMetaData meta_data) {
if (meta_data.domain != bke::AttrDomain::Point) {
return true;
}
bke::GSpanAttributeWriter dst = attributes.lookup_for_write_span(id);
GMutableSpan attribute_data = dst.span;
bke::attribute_math::convert_to_static_type(attribute_data.type(), [&](auto dummy) {
using T = decltype(dummy);
MutableSpan<T> span_data = attribute_data.typed<T>();
/* Loop through backwards to not overwrite the data. */
for (int i = (span_data.size() - 1) - new_points_num; i >= last_active_point; i--) {
span_data[i + new_points_num] = span_data[i];
}
});
dst.finish();
return true;
});
curves.tag_topology_changed();
}
/* Attributes that are defined explicitly and should not be copied from original geometry. */
Set<std::string> skipped_attribute_ids(const bke::AttrDomain domain) const
{
@@ -352,7 +363,9 @@ struct PaintOperationExecutor {
self.screen_space_coords_orig_.append(start_coords);
self.screen_space_curve_fitted_coords_.append(Vector<float2>({start_coords}));
self.screen_space_jitter_offsets_.append(float2(0.0f));
self.screen_space_smoothed_coords_.append(start_coords);
self.screen_space_final_coords_.append(start_coords);
/* Resize the curves geometry so there is one more curve with a single point. */
bke::CurvesGeometry &curves = drawing_->strokes_for_write();
@@ -419,9 +432,7 @@ struct PaintOperationExecutor {
drawing_->tag_topology_changed();
}
void active_smoothing(PaintOperation &self,
const IndexRange smooth_window,
MutableSpan<float3> curve_positions)
void active_smoothing(PaintOperation &self, const IndexRange smooth_window)
{
const Span<float2> coords_to_smooth = self.screen_space_coords_orig_.as_span().slice(
smooth_window);
@@ -468,7 +479,6 @@ struct PaintOperationExecutor {
MutableSpan<float2> window_coords = self.screen_space_smoothed_coords_.as_mutable_span().slice(
smooth_window);
MutableSpan<float3> positions_slice = curve_positions.slice(smooth_window);
const float converging_threshold_px = 0.1f;
bool stop_counting_converged = false;
int num_converged = 0;
@@ -492,7 +502,6 @@ struct PaintOperationExecutor {
/* Update the positions in the current cache. */
window_coords[window_i] = new_pos;
positions_slice[window_i] = self.placement_.project(new_pos);
}
/* Remove all the converged points from the active window and shrink the window accordingly. */
@@ -502,15 +511,48 @@ struct PaintOperationExecutor {
}
}
void active_jitter(PaintOperation &self,
const int new_points_num,
const float brush_radius_px,
const float pressure,
const IndexRange active_window,
MutableSpan<float3> curve_positions)
{
float jitter_factor = 1.0f;
if (settings_->flag & GP_BRUSH_USE_JITTER_PRESSURE) {
jitter_factor = BKE_curvemapping_evaluateF(settings_->curve_jitter, 0, pressure);
}
const float2 tangent = math::normalize(self.smoothed_pen_direction_);
const float2 cotangent = float2(-tangent.y, tangent.x);
for ([[maybe_unused]] const int _ : IndexRange(new_points_num)) {
const float rand = self.rng.get_float() * 2.0f - 1.0f;
const float factor = rand * settings_->draw_jitter * jitter_factor;
self.screen_space_jitter_offsets_.append(cotangent * factor * brush_radius_px);
}
const Span<float2> jitter_slice = self.screen_space_jitter_offsets_.as_mutable_span().slice(
active_window);
MutableSpan<float2> smoothed_coords =
self.screen_space_smoothed_coords_.as_mutable_span().slice(active_window);
MutableSpan<float2> final_coords = self.screen_space_final_coords_.as_mutable_span().slice(
active_window);
MutableSpan<float3> positions_slice = curve_positions.slice(active_window);
for (const int64_t window_i : active_window.index_range()) {
final_coords[window_i] = smoothed_coords[window_i] + jitter_slice[window_i];
positions_slice[window_i] = self.placement_.project(final_coords[window_i]);
}
}
void process_extension_sample(PaintOperation &self,
const bContext &C,
const InputSample &extension_sample)
{
const float2 coords = extension_sample.mouse_position;
Scene *scene = CTX_data_scene(&C);
const RegionView3D *rv3d = CTX_wm_region_view3d(&C);
const ARegion *region = CTX_wm_region(&C);
const bool on_back = (scene->toolsettings->gpencil_flags & GP_TOOL_FLAG_PAINT_ONBACK) != 0;
const float3 position = self.placement_.project(coords);
const float2 coords = extension_sample.mouse_position;
float3 position = self.placement_.project(coords);
float radius = ed::greasepencil::radius_from_input_sample(rv3d,
region,
brush_,
@@ -518,17 +560,18 @@ struct PaintOperationExecutor {
position,
self.placement_.to_world_space(),
settings_);
const float brush_radius_px = brush_radius_to_pixel_radius(
rv3d, brush_, math::transform_point(self.placement_.to_world_space(), position));
const float opacity = ed::greasepencil::opacity_from_input_sample(
extension_sample.pressure, brush_, settings_);
Scene *scene = CTX_data_scene(&C);
const bool on_back = (scene->toolsettings->gpencil_flags & GP_TOOL_FLAG_PAINT_ONBACK) != 0;
bke::CurvesGeometry &curves = drawing_->strokes_for_write();
OffsetIndices<int> points_by_curve = curves.points_by_curve();
bke::MutableAttributeAccessor attributes = curves.attributes_for_write();
const int active_curve = on_back ? curves.curves_range().first() :
curves.curves_range().last();
const IndexRange curve_points = curves.points_by_curve()[active_curve];
const IndexRange curve_points = points_by_curve[active_curve];
const int last_active_point = curve_points.last();
const float2 prev_coords = self.screen_space_coords_orig_.last();
@@ -536,29 +579,31 @@ struct PaintOperationExecutor {
const float prev_opacity = drawing_->opacities()[last_active_point];
const ColorGeometry4f prev_vertex_color = drawing_->vertex_colors()[last_active_point];
/* Use the vector from the previous to the next point and then interpolate it with the previous
* direction to get a smoothed value over time. */
self.smoothed_pen_direction_ = math::interpolate(
self.smoothed_pen_direction_, coords - self.screen_space_coords_orig_.last(), 0.1f);
/* Approximate brush with non-circular shape by changing the radius based on the angle. */
if (settings_->draw_angle_factor > 0.0f) {
const float angle = settings_->draw_angle;
const float2 angle_vec = float2(math::cos(angle), math::sin(angle));
const float2 vec = coords - self.screen_space_coords_orig_.last();
/* `angle_factor` is the angle to the horizontal line in screen space. */
const float angle_factor = 1.0f - math::abs(math::dot(angle_vec, math::normalize(vec)));
/* Smooth the angle factor over time. */
self.smoothed_angle_factor_ = math::interpolate(
self.smoothed_angle_factor_, angle_factor, 0.1f);
const float angle_factor =
1.0f - math::abs(math::dot(angle_vec, math::normalize(self.smoothed_pen_direction_)));
/* Influence is controlled by `draw_angle_factor`. */
const float radius_factor = math::interpolate(
1.0f, self.smoothed_angle_factor_, settings_->draw_angle_factor);
1.0f, angle_factor, settings_->draw_angle_factor);
radius *= radius_factor;
}
/* Overwrite last point if it's very close. */
const IndexRange points_range = curves.points_by_curve()[curves.curves_range().last()];
const bool is_first_sample = (points_range.size() == 1);
const bool is_first_sample = (curve_points.size() == 1);
constexpr float point_override_threshold_px = 2.0f;
if (math::distance(coords, prev_coords) < point_override_threshold_px) {
const float distance_px = math::distance(coords, prev_coords);
if (distance_px < point_override_threshold_px) {
/* Don't move the first point of the stroke. */
if (!is_first_sample) {
curves.positions_for_write()[last_active_point] = position;
@@ -568,22 +613,19 @@ struct PaintOperationExecutor {
return;
}
/* If the next sample is far away, we subdivide the segment to add more points. */
const float distance_px = math::distance(coords, prev_coords);
const float brush_radius_px = brush_radius_to_pixel_radius(
rv3d, brush_, math::transform_point(self.placement_.to_world_space(), position));
/* Clamp the number of points within a pixel in screen space. */
constexpr int max_points_per_pixel = 4;
/* The value `brush_->spacing` is a percentage of the brush radius in pixels. */
const float max_spacing_px = math::max((float(brush_->spacing) / 100.0f) *
float(brush_radius_px),
1.0f / float(max_points_per_pixel));
/* If the next sample is far away, we subdivide the segment to add more points. */
const int new_points_num = (distance_px > max_spacing_px) ?
int(math::floor(distance_px / max_spacing_px)) :
1;
/* Resize the curves geometry. */
extend_curve(curves, on_back, new_points_num);
/* Subdivide stroke in new_points. */
const IndexRange new_points = curves.points_by_curve()[active_curve].take_back(new_points_num);
Array<float2> new_screen_space_coords(new_points_num);
@@ -604,6 +646,7 @@ struct PaintOperationExecutor {
/* Update screen space buffers with new points. */
self.screen_space_coords_orig_.extend(new_screen_space_coords);
self.screen_space_smoothed_coords_.extend(new_screen_space_coords);
self.screen_space_final_coords_.extend(new_screen_space_coords);
for (float2 new_position : new_screen_space_coords) {
self.screen_space_curve_fitted_coords_.append(Vector<float2>({new_position}));
}
@@ -617,8 +660,29 @@ struct PaintOperationExecutor {
}
else {
/* Active smoothing is done in a window at the end of the new stroke. */
this->active_smoothing(
self, smooth_window, positions.slice(curves.points_by_curve()[active_curve]));
this->active_smoothing(self, smooth_window);
}
MutableSpan<float3> curve_positions = positions.slice(curves.points_by_curve()[active_curve]);
if (use_settings_random_ && settings_->draw_jitter > 0.0f) {
this->active_jitter(self,
new_points_num,
brush_radius_px,
extension_sample.pressure,
smooth_window,
curve_positions);
}
else {
MutableSpan<float2> smoothed_coords =
self.screen_space_smoothed_coords_.as_mutable_span().slice(smooth_window);
MutableSpan<float2> final_coords = self.screen_space_final_coords_.as_mutable_span().slice(
smooth_window);
/* Not jitter, so we just copy the positions over. */
final_coords.copy_from(smoothed_coords);
MutableSpan<float3> curve_positions_slice = curve_positions.slice(smooth_window);
for (const int64_t window_i : smooth_window.index_range()) {
curve_positions_slice[window_i] = self.placement_.project(final_coords[window_i]);
}
}
/* Initialize the rest of the attributes with default values. */
@@ -953,7 +1017,7 @@ void PaintOperation::on_stroke_done(const bContext &C)
}
if (settings->simplify_px > 0.0f) {
simplify_stroke(drawing,
this->screen_space_smoothed_coords_.as_span().drop_back(num_points_removed),
this->screen_space_final_coords_.as_span().drop_back(num_points_removed),
settings->simplify_px,
active_curve);
}

View File

@@ -1490,8 +1490,9 @@ static void rna_def_gpencil_options(BlenderRNA *brna)
/* Jitter factor for new strokes */
prop = RNA_def_property(srna, "pen_jitter", PROP_FLOAT, PROP_FACTOR);
RNA_def_property_float_sdna(prop, nullptr, "draw_jitter");
RNA_def_property_range(prop, 0.0f, 1.0f);
RNA_def_property_ui_text(prop, "Jitter", "Jitter factor for new strokes");
RNA_def_property_range(prop, 0.0f, 100.0f);
RNA_def_property_ui_range(prop, 0.0f, 1.0f, 0.001, 3);
RNA_def_property_ui_text(prop, "Jitter", "Jitter factor of brush radius for new strokes");
RNA_def_property_translation_context(prop, BLT_I18NCONTEXT_ID_BRUSH);
RNA_def_property_clear_flag(prop, PROP_ANIMATABLE);
RNA_def_property_update(prop, NC_GPENCIL | ND_DATA, nullptr);