Fix #127856: GPv3 sculpt mode delta projection (lock axis) without DrawingPlacement

Sculpt mode in Grease Pencil should not use the origin point of
`DrawingPlacement` and only apply the plane projection to relative
offsets.

A new variant of the `foreach_editable_drawing` function is introduced
that provides a `DeltaProjectionFunc` instead of `DrawingPlacement`.
This projection callback takes a layer-space position and a screen-space
delta and returns a layer-space position with the constrained delta
applied. The projection function is based on the tools settings'
"lock axis", like `DrawingPlacement`, but ignores the origin point.

Exception is the smoothing tool, which does not restrict movement along
the lock axis. This can be implemented if desired, but for a
non-directional tool it seems the lock axis isn't very meaningful.

Pull Request: https://projects.blender.org/blender/blender/pulls/128022
This commit is contained in:
Lukas Tönne
2024-09-23 19:46:22 +02:00
parent 3f6e36bdda
commit cbe2bb6755
8 changed files with 151 additions and 80 deletions

View File

@@ -4,6 +4,8 @@
#pragma once
#include <functional>
#include "BLI_color.hh"
#include "BLI_task.hh"
@@ -29,6 +31,14 @@ struct GeometryDeformation;
namespace blender::ed::sculpt_paint {
/**
* Projects a screen-space displacement vector into layer space.
* Current position (in layer space) is used to compute the perspective distance (zfac).
* Returns the new layer space position with the projected delta applied.
*/
using DeltaProjectionFunc =
std::function<float3(const float3 position, const float2 &screen_delta)>;
struct InputSample {
float2 mouse_position;
float pressure;
@@ -85,6 +95,8 @@ bool is_brush_inverted(const Brush &brush, BrushStrokeMode stroke_mode);
struct GreasePencilStrokeParams {
const ToolSettings &toolsettings;
const ARegion &region;
const RegionView3D &rv3d;
const Scene &scene;
Object &ob_orig;
Object &ob_eval;
const bke::greasepencil::Layer &layer;
@@ -98,6 +110,7 @@ struct GreasePencilStrokeParams {
static GreasePencilStrokeParams from_context(const Scene &scene,
Depsgraph &depsgraph,
ARegion &region,
RegionView3D &rv3d,
Object &object,
int layer_index,
int frame_number,
@@ -125,6 +138,12 @@ Array<float2> calculate_view_positions(const GreasePencilStrokeParams &params,
Array<float> calculate_view_radii(const GreasePencilStrokeParams &params,
const IndexMask &selection);
/* Get an appropriate projection function from screen space to layer space.
* This is an alternative to using the DrawingPlacement. */
DeltaProjectionFunc get_screen_projection_fn(const GreasePencilStrokeParams &params,
const Object &object,
const bke::greasepencil::Layer &layer);
bool do_vertex_color_points(const Brush &brush);
bool do_vertex_color_fill(const Brush &brush);
@@ -158,9 +177,10 @@ class GreasePencilStrokeOperationCommon : public GreasePencilStrokeOperation {
const bContext &C,
GrainSize grain_size,
FunctionRef<bool(const GreasePencilStrokeParams &params)> fn) const;
void foreach_editable_drawing(const bContext &C,
FunctionRef<bool(const GreasePencilStrokeParams &params,
const DrawingPlacement &placement)> fn) const;
void foreach_editable_drawing(
const bContext &C,
FunctionRef<bool(const GreasePencilStrokeParams &params,
const DeltaProjectionFunc &projection_fn)> fn) const;
};
/* Operations */

View File

@@ -194,10 +194,80 @@ bool is_brush_inverted(const Brush &brush, const BrushStrokeMode stroke_mode)
return bool(brush.flag & BRUSH_DIR_IN) ^ (stroke_mode == BrushStrokeMode::BRUSH_STROKE_INVERT);
}
DeltaProjectionFunc get_screen_projection_fn(const GreasePencilStrokeParams &params,
const Object &object,
const bke::greasepencil::Layer &layer)
{
const float4x4 view_to_world = float4x4(params.rv3d.viewinv);
const float4x4 layer_to_world = layer.to_world_space(object);
const float4x4 world_to_layer = math::invert(layer_to_world);
auto screen_to_world = [=](const float3 &world_pos, const float2 &screen_delta) {
const float zfac = ED_view3d_calc_zfac(&params.rv3d, world_pos);
float3 world_delta;
ED_view3d_win_to_delta(&params.region, screen_delta, zfac, world_delta);
return world_delta;
};
switch (params.toolsettings.gp_sculpt.lock_axis) {
case GP_LOCKAXIS_VIEW: {
const float3 world_normal = view_to_world.z_axis();
return [=](const float3 &position, const float2 &screen_delta) {
const float3 world_pos = math::transform_point(layer_to_world, position);
const float3 world_delta = screen_to_world(world_pos, screen_delta);
const float3 layer_delta = math::transform_direction(
world_to_layer, world_delta - world_normal * math::dot(world_delta, world_normal));
return position + layer_delta;
};
}
case GP_LOCKAXIS_X: {
return [=](const float3 &position, const float2 &screen_delta) {
const float3 world_pos = math::transform_point(layer_to_world, position);
const float3 world_delta = screen_to_world(world_pos, screen_delta);
const float3 layer_delta = math::transform_direction(
world_to_layer, float3(0.0f, world_delta.y, world_delta.z));
return position + layer_delta;
};
}
case GP_LOCKAXIS_Y: {
return [=](const float3 &position, const float2 &screen_delta) {
const float3 world_pos = math::transform_point(layer_to_world, position);
const float3 world_delta = screen_to_world(world_pos, screen_delta);
const float3 layer_delta = math::transform_direction(
world_to_layer, float3(world_delta.x, 0.0f, world_delta.z));
return position + layer_delta;
};
}
case GP_LOCKAXIS_Z: {
return [=](const float3 &position, const float2 &screen_delta) {
const float3 world_pos = math::transform_point(layer_to_world, position);
const float3 world_delta = screen_to_world(world_pos, screen_delta);
const float3 layer_delta = math::transform_direction(
world_to_layer, float3(world_delta.x, world_delta.y, 0.0f));
return position + layer_delta;
};
}
case GP_LOCKAXIS_CURSOR: {
const float3 world_normal = params.scene.cursor.matrix<float3x3>().z_axis();
return [=](const float3 &position, const float2 &screen_delta) {
const float3 world_pos = math::transform_point(layer_to_world, position);
const float3 world_delta = screen_to_world(world_pos, screen_delta);
const float3 layer_delta = math::transform_direction(
world_to_layer, world_delta - world_normal * math::dot(world_delta, world_normal));
return position + layer_delta;
};
}
}
BLI_assert_unreachable();
return [](const float3 &, const float2 &) { return float3(); };
}
GreasePencilStrokeParams GreasePencilStrokeParams::from_context(
const Scene &scene,
Depsgraph &depsgraph,
ARegion &region,
RegionView3D &rv3d,
Object &object,
const int layer_index,
const int frame_number,
@@ -210,6 +280,8 @@ GreasePencilStrokeParams GreasePencilStrokeParams::from_context(
const bke::greasepencil::Layer &layer = grease_pencil.layer(layer_index);
return {*scene.toolsettings,
region,
rv3d,
scene,
object,
ob_eval,
layer,
@@ -327,6 +399,7 @@ void GreasePencilStrokeOperationCommon::foreach_editable_drawing(
const Scene &scene = *CTX_data_scene(&C);
Depsgraph &depsgraph = *CTX_data_depsgraph_pointer(&C);
ARegion &region = *CTX_wm_region(&C);
RegionView3D &rv3d = *CTX_wm_region_view3d(&C);
Object &object = *CTX_data_active_object(&C);
GreasePencil &grease_pencil = *static_cast<GreasePencil *>(object.data);
@@ -338,6 +411,7 @@ void GreasePencilStrokeOperationCommon::foreach_editable_drawing(
scene,
depsgraph,
region,
rv3d,
object,
info.layer_index,
info.frame_number,
@@ -364,6 +438,7 @@ void GreasePencilStrokeOperationCommon::foreach_editable_drawing(
const Scene &scene = *CTX_data_scene(&C);
Depsgraph &depsgraph = *CTX_data_depsgraph_pointer(&C);
ARegion &region = *CTX_wm_region(&C);
RegionView3D &rv3d = *CTX_wm_region_view3d(&C);
Object &object = *CTX_data_active_object(&C);
GreasePencil &grease_pencil = *static_cast<GreasePencil *>(object.data);
@@ -376,6 +451,7 @@ void GreasePencilStrokeOperationCommon::foreach_editable_drawing(
scene,
depsgraph,
region,
rv3d,
object,
info.layer_index,
info.frame_number,
@@ -395,15 +471,15 @@ void GreasePencilStrokeOperationCommon::foreach_editable_drawing(
void GreasePencilStrokeOperationCommon::foreach_editable_drawing(
const bContext &C,
FunctionRef<bool(const GreasePencilStrokeParams &params, const DrawingPlacement &placement)>
fn) const
FunctionRef<bool(const GreasePencilStrokeParams &params,
const DeltaProjectionFunc &projection_fn)> fn) const
{
using namespace blender::bke::greasepencil;
const Scene &scene = *CTX_data_scene(&C);
Depsgraph &depsgraph = *CTX_data_depsgraph_pointer(&C);
View3D &view3d = *CTX_wm_view3d(&C);
ARegion &region = *CTX_wm_region(&C);
RegionView3D &rv3d = *CTX_wm_region_view3d(&C);
Object &object = *CTX_data_active_object(&C);
Object &object_eval = *DEG_get_evaluated_object(&depsgraph, &object);
GreasePencil &grease_pencil = *static_cast<GreasePencil *>(object.data);
@@ -413,25 +489,18 @@ void GreasePencilStrokeOperationCommon::foreach_editable_drawing(
threading::parallel_for_each(drawings, [&](const MutableDrawingInfo &info) {
const Layer &layer = grease_pencil.layer(info.layer_index);
DrawingPlacement placement(scene, region, view3d, object_eval, &layer);
if (placement.use_project_to_surface()) {
placement.cache_viewport_depths(&depsgraph, &region, &view3d);
}
else if (placement.use_project_to_nearest_stroke()) {
placement.cache_viewport_depths(&depsgraph, &region, &view3d);
placement.set_origin_to_nearest_stroke(this->start_mouse_position);
}
GreasePencilStrokeParams params = GreasePencilStrokeParams::from_context(
const GreasePencilStrokeParams params = GreasePencilStrokeParams::from_context(
scene,
depsgraph,
region,
rv3d,
object,
info.layer_index,
info.frame_number,
info.multi_frame_falloff,
info.drawing);
if (fn(params, placement)) {
const DeltaProjectionFunc projection_fn = get_screen_projection_fn(params, object_eval, layer);
if (fn(params, projection_fn)) {
changed = true;
}
});

View File

@@ -47,15 +47,14 @@ void CloneOperation::on_stroke_begin(const bContext &C, const InputSample &start
*
* Here we only have the GPv2 behavior that actually works for now. */
this->foreach_editable_drawing(
C,
[&](const GreasePencilStrokeParams &params,
const ed::greasepencil::DrawingPlacement &placement) {
C, [&](const GreasePencilStrokeParams &params, const DeltaProjectionFunc &projection_fn) {
const IndexRange pasted_curves = ed::greasepencil::clipboard_paste_strokes(
bmain, object, params.drawing, false);
if (pasted_curves.is_empty()) {
return false;
}
const bke::crazyspace::GeometryDeformation deformation = get_drawing_deformation(params);
bke::CurvesGeometry &curves = params.drawing.strokes_for_write();
const OffsetIndices<int> pasted_points_by_curve = curves.points_by_curve().slice(
pasted_curves);
@@ -73,7 +72,7 @@ void CloneOperation::on_stroke_begin(const bContext &C, const InputSample &start
MutableSpan<float3> positions = curves.positions_for_write();
threading::parallel_for(pasted_points, 4096, [&](const IndexRange range) {
for (const int point_i : range) {
positions[point_i] = placement.project(view_positions[point_i] + mouse_delta);
positions[point_i] = projection_fn(deformation.positions[point_i], mouse_delta);
}
});
params.drawing.tag_positions_changed();

View File

@@ -31,7 +31,6 @@ class GrabOperation : public GreasePencilStrokeOperationCommon {
public:
using GreasePencilStrokeOperationCommon::GreasePencilStrokeOperationCommon;
using MutableDrawingInfo = blender::ed::greasepencil::MutableDrawingInfo;
using DrawingPlacement = ed::greasepencil::DrawingPlacement;
/* Cached point mask and influence for a particular drawing. */
struct PointWeights {
@@ -51,12 +50,11 @@ class GrabOperation : public GreasePencilStrokeOperationCommon {
/* Cached point data for each affected drawing. */
Array<PointWeights> drawing_data;
void foreach_grabbed_drawing(
const bContext &C,
FunctionRef<bool(const GreasePencilStrokeParams &params,
const ed::greasepencil::DrawingPlacement &placement,
const IndexMask &mask,
Span<float> weights)> fn) const;
void foreach_grabbed_drawing(const bContext &C,
FunctionRef<bool(const GreasePencilStrokeParams &params,
const DeltaProjectionFunc &projection_fn,
const IndexMask &mask,
Span<float> weights)> fn) const;
void on_stroke_begin(const bContext &C, const InputSample &start_sample) override;
void on_stroke_extended(const bContext &C, const InputSample &extension_sample) override;
@@ -66,7 +64,7 @@ class GrabOperation : public GreasePencilStrokeOperationCommon {
void GrabOperation::foreach_grabbed_drawing(
const bContext &C,
FunctionRef<bool(const GreasePencilStrokeParams &params,
const ed::greasepencil::DrawingPlacement &placement,
const DeltaProjectionFunc &projection_fn,
const IndexMask &mask,
Span<float> weights)> fn) const
{
@@ -76,7 +74,7 @@ void GrabOperation::foreach_grabbed_drawing(
const Scene &scene = *CTX_data_scene(&C);
Depsgraph &depsgraph = *CTX_data_depsgraph_pointer(&C);
ARegion &region = *CTX_wm_region(&C);
View3D &view3d = *CTX_wm_view3d(&C);
RegionView3D &rv3d = *CTX_wm_region_view3d(&C);
Object &object = *CTX_data_active_object(&C);
Object &object_eval = *DEG_get_evaluated_object(&depsgraph, &object);
GreasePencil &grease_pencil = *static_cast<GreasePencil *>(object.data);
@@ -94,25 +92,18 @@ void GrabOperation::foreach_grabbed_drawing(
return;
}
ed::greasepencil::DrawingPlacement placement(scene, region, view3d, object_eval, &layer);
if (placement.use_project_to_surface()) {
placement.cache_viewport_depths(&depsgraph, &region, &view3d);
}
else if (placement.use_project_to_nearest_stroke()) {
placement.cache_viewport_depths(&depsgraph, &region, &view3d);
placement.set_origin_to_nearest_stroke(this->start_mouse_position);
}
GreasePencilStrokeParams params = GreasePencilStrokeParams::from_context(
scene,
depsgraph,
region,
rv3d,
object,
data.layer_index,
data.frame_number,
data.multi_frame_falloff,
*drawing);
if (fn(params, placement, data.point_mask, data.weights)) {
DeltaProjectionFunc projection_fn = get_screen_projection_fn(params, object_eval, layer);
if (fn(params, projection_fn, data.point_mask, data.weights)) {
changed = true;
}
});
@@ -154,6 +145,8 @@ void GrabOperation::on_stroke_begin(const bContext &C, const InputSample &start_
GreasePencilStrokeParams params = {*scene.toolsettings,
region,
rv3d,
scene,
ob_orig,
ob_eval,
layer,
@@ -195,38 +188,22 @@ void GrabOperation::on_stroke_begin(const bContext &C, const InputSample &start_
void GrabOperation::on_stroke_extended(const bContext &C, const InputSample &extension_sample)
{
const ARegion &region = *CTX_wm_region(&C);
const RegionView3D &rv3d = *CTX_wm_region_view3d(&C);
this->foreach_grabbed_drawing(
C,
[&](const GreasePencilStrokeParams &params,
const ed::greasepencil::DrawingPlacement &placement,
const DeltaProjectionFunc &projection_fn,
const IndexMask &mask,
const Span<float> weights) {
/* Crazy-space deformation. */
bke::crazyspace::GeometryDeformation deformation = get_drawing_deformation(params);
/* Transform mouse delta into layer space. */
const float2 mouse_delta_win = this->mouse_delta(extension_sample);
const float3 layer_origin = params.layer.to_world_space(params.ob_eval).location();
const float zfac = ED_view3d_calc_zfac(&rv3d, layer_origin);
float3 mouse_delta;
ED_view3d_win_to_delta(&region, mouse_delta_win, zfac, mouse_delta);
const float4x4 &world_to_layer = math::invert(params.layer.to_world_space(params.ob_eval));
mouse_delta = math::transform_direction(world_to_layer, mouse_delta);
bke::CurvesGeometry &curves = params.drawing.strokes_for_write();
bke::crazyspace::GeometryDeformation deformation = get_drawing_deformation(params);
MutableSpan<float3> positions = curves.positions_for_write();
mask.foreach_index(GrainSize(1024), [&](const int point_i, const int index) {
mask.foreach_index(GrainSize(4096), [&](const int point_i, const int index) {
/* Translate the point with the influence factor. */
const float3 new_pos_layer = deformation.positions[point_i] +
mouse_delta * weights[index];
const float3 new_pos_world = math::transform_point(
params.layer.to_world_space(params.ob_eval), new_pos_layer);
float2 new_pos_view;
ED_view3d_project_float_global(&region, new_pos_world, new_pos_view, V3D_PROJ_TEST_NOP);
positions[point_i] = placement.project(new_pos_view);
positions[point_i] = projection_fn(deformation.positions[point_i],
mouse_delta_win * weights[index]);
});
params.drawing.tag_positions_changed();

View File

@@ -3,6 +3,7 @@
* SPDX-License-Identifier: GPL-2.0-or-later */
#include "BKE_context.hh"
#include "BKE_crazyspace.hh"
#include "BKE_curves.hh"
#include "BKE_grease_pencil.hh"
#include "BKE_paint.hh"
@@ -42,15 +43,14 @@ void PinchOperation::on_stroke_extended(const bContext &C, const InputSample &ex
eGP_Sculpt_SelectMaskFlag(scene.toolsettings->gpencil_selectmode_sculpt));
this->foreach_editable_drawing(
C,
[&](const GreasePencilStrokeParams &params,
const ed::greasepencil::DrawingPlacement &placement) {
C, [&](const GreasePencilStrokeParams &params, const DeltaProjectionFunc &projection_fn) {
IndexMaskMemory selection_memory;
const IndexMask selection = point_selection_mask(params, is_masking, selection_memory);
if (selection.is_empty()) {
return false;
}
bke::crazyspace::GeometryDeformation deformation = get_drawing_deformation(params);
Array<float2> view_positions = calculate_view_positions(params, selection);
bke::CurvesGeometry &curves = params.drawing.strokes_for_write();
MutableSpan<float3> positions = curves.positions_for_write();
@@ -65,9 +65,11 @@ void PinchOperation::on_stroke_extended(const bContext &C, const InputSample &ex
return;
}
const float scale_offset = influence * influence / 25.0f;
const float scale = invert ? 1.0 + scale_offset : 1.0f - scale_offset;
positions[point_i] = placement.project(target + (co - target) * scale);
const float influence_squared = influence * influence / 25.0f;
const float influence_final = invert ? 1.0 + influence_squared :
1.0f - influence_squared;
positions[point_i] = projection_fn(deformation.positions[point_i],
(target - co) * (1.0f - influence_final));
});
params.drawing.tag_positions_changed();

View File

@@ -3,6 +3,7 @@
* SPDX-License-Identifier: GPL-2.0-or-later */
#include "BKE_context.hh"
#include "BKE_crazyspace.hh"
#include "BKE_curves.hh"
#include "BKE_grease_pencil.hh"
#include "BKE_paint.hh"
@@ -42,15 +43,14 @@ void PushOperation::on_stroke_extended(const bContext &C, const InputSample &ext
eGP_Sculpt_SelectMaskFlag(scene.toolsettings->gpencil_selectmode_sculpt));
this->foreach_editable_drawing(
C,
[&](const GreasePencilStrokeParams &params,
const ed::greasepencil::DrawingPlacement &placement) {
C, [&](const GreasePencilStrokeParams &params, const DeltaProjectionFunc &projection_fn) {
IndexMaskMemory selection_memory;
const IndexMask selection = point_selection_mask(params, is_masking, selection_memory);
if (selection.is_empty()) {
return false;
}
bke::crazyspace::GeometryDeformation deformation = get_drawing_deformation(params);
Array<float2> view_positions = calculate_view_positions(params, selection);
bke::CurvesGeometry &curves = params.drawing.strokes_for_write();
MutableSpan<float3> positions = curves.positions_for_write();
@@ -65,7 +65,8 @@ void PushOperation::on_stroke_extended(const bContext &C, const InputSample &ext
return;
}
positions[point_i] = placement.project(co + mouse_delta * influence);
positions[point_i] = projection_fn(deformation.positions[point_i],
mouse_delta * influence);
});
params.drawing.tag_positions_changed();

View File

@@ -8,6 +8,7 @@
#include "BLI_task.hh"
#include "BKE_context.hh"
#include "BKE_crazyspace.hh"
#include "BKE_curves.hh"
#include "BKE_grease_pencil.hh"
#include "BKE_paint.hh"
@@ -62,9 +63,7 @@ void RandomizeOperation::on_stroke_extended(const bContext &C, const InputSample
eGP_Sculpt_SelectMaskFlag(scene.toolsettings->gpencil_selectmode_sculpt));
this->foreach_editable_drawing(
C,
[&](const GreasePencilStrokeParams &params,
const ed::greasepencil::DrawingPlacement &placement) {
C, [&](const GreasePencilStrokeParams &params, const DeltaProjectionFunc &projection_fn) {
const uint32_t seed = this->unique_seed();
IndexMaskMemory selection_memory;
@@ -73,6 +72,7 @@ void RandomizeOperation::on_stroke_extended(const bContext &C, const InputSample
return false;
}
bke::crazyspace::GeometryDeformation deformation = get_drawing_deformation(params);
Array<float2> view_positions = calculate_view_positions(params, selection);
bke::CurvesGeometry &curves = params.drawing.strokes_for_write();
bke::MutableAttributeAccessor attributes = curves.attributes_for_write();
@@ -93,7 +93,8 @@ void RandomizeOperation::on_stroke_extended(const bContext &C, const InputSample
return;
}
const float noise = 2.0f * hash_rng(seed, 5678, point_i) - 1.0f;
positions[point_i] = placement.project(co + sideways * influence * noise);
positions[point_i] = projection_fn(deformation.positions[point_i],
sideways * influence * noise);
});
params.drawing.tag_positions_changed();

View File

@@ -5,6 +5,7 @@
#include "BLI_math_rotation.h"
#include "BKE_context.hh"
#include "BKE_crazyspace.hh"
#include "BKE_curves.hh"
#include "BKE_grease_pencil.hh"
#include "BKE_paint.hh"
@@ -52,15 +53,14 @@ void TwistOperation::on_stroke_extended(const bContext &C, const InputSample &ex
eGP_Sculpt_SelectMaskFlag(scene.toolsettings->gpencil_selectmode_sculpt));
this->foreach_editable_drawing(
C,
[&](const GreasePencilStrokeParams &params,
const ed::greasepencil::DrawingPlacement &placement) {
C, [&](const GreasePencilStrokeParams &params, const DeltaProjectionFunc &projection_fn) {
IndexMaskMemory selection_memory;
const IndexMask selection = point_selection_mask(params, is_masking, selection_memory);
if (selection.is_empty()) {
return false;
}
bke::crazyspace::GeometryDeformation deformation = get_drawing_deformation(params);
Array<float2> view_positions = calculate_view_positions(params, selection);
bke::CurvesGeometry &curves = params.drawing.strokes_for_write();
MutableSpan<float3> positions = curves.positions_for_write();
@@ -76,8 +76,10 @@ void TwistOperation::on_stroke_extended(const bContext &C, const InputSample &ex
}
const float angle = DEG2RADF(invert ? -1.0f : 1.0f) * influence;
positions[point_i] = placement.project(rotate_by_angle(co - mouse_pos, angle) +
mouse_pos);
const float2 radial_offset = co - mouse_pos;
positions[point_i] = projection_fn(deformation.positions[point_i],
rotate_by_angle(radial_offset, angle) -
radial_offset);
});
params.drawing.tag_positions_changed();