GPv3: Fill texture coordinates system

This is implements the system texture coordinates for GPv3.

This pull request adds:
- System for storing and viewing texture coordinates.
- Texture coordinates are convert when covering from legacy to GPv3,
   (Tested with object and layer transformation)
- Textures are set to the drawing plane.

Pull Request: https://projects.blender.org/blender/blender/pulls/119303
This commit is contained in:
casey bianco-davis
2024-03-21 16:07:18 +01:00
committed by Falk David
parent d5cf430a54
commit 20b614ab8e
5 changed files with 399 additions and 18 deletions

View File

@@ -46,6 +46,8 @@ class DrawingRuntime {
*/
mutable SharedCache<Vector<float3>> curve_plane_normals_cache;
mutable SharedCache<Vector<float4x2>> curve_texture_matrices;
/**
* 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
@@ -70,9 +72,21 @@ class Drawing : public ::GreasePencilDrawing {
* Normal vectors for a plane that fits the stroke.
*/
Span<float3> curve_plane_normals() const;
void tag_texture_matrices_changed();
void tag_positions_changed();
void tag_topology_changed();
/*
* Returns the matrices that transform from a 3D point in layer-space to a 2D point in
* texture-space.
*/
Span<float4x2> texture_matrices() const;
/*
* Sets the matrices the that transform from a 3D point in layer-space to a 2D point in
* texture-space
*/
void set_texture_matrices(Span<float4x2> matrices, const IndexMask &selection);
/**
* Radii of the points. Values are expected to be in blender units.
*/

View File

@@ -307,6 +307,7 @@ Drawing::Drawing(const Drawing &other)
this->runtime->triangles_cache = other.runtime->triangles_cache;
this->runtime->curve_plane_normals_cache = other.runtime->curve_plane_normals_cache;
this->runtime->curve_texture_matrices = other.runtime->curve_texture_matrices;
}
Drawing::~Drawing()
@@ -426,6 +427,207 @@ Span<float3> Drawing::curve_plane_normals() const
return this->runtime->curve_plane_normals_cache.data().as_span();
}
/*
* Returns the matrix that transforms from a 3D point in layer-space to a 2D point in
* stroke-space for the stroke `curve_i`
*/
static float4x2 get_local_to_stroke_matrix(const Span<float3> positions, const float3 normal)
{
using namespace blender::math;
if (positions.size() <= 2) {
return float4x2::identity();
}
const float3 point_0 = positions[0];
const float3 point_1 = positions[1];
/* Local X axis (p0 -> p1) */
const float3 local_x = normalize(point_1 - point_0);
/* Local Y axis (cross to normal/x axis). */
const float3 local_y = normalize(cross(normal, local_x));
if (length_squared(local_x) == 0.0f || length_squared(local_y) == 0.0f) {
return float4x2::identity();
}
/* Get local space using first point as origin. */
const float4x2 mat = transpose(
float2x4(float4(local_x, -dot(point_0, local_x)), float4(local_y, -dot(point_0, local_y))));
return mat;
}
/*
* Returns the matrix that transforms from a 2D point in stroke-space to a 2D point in
* texture-space for a stroke `curve_i`
*/
static float3x2 get_stroke_to_texture_matrix(const float uv_rotation,
const float2 uv_translation,
const float2 uv_scale)
{
using namespace blender::math;
const float2 uv_scale_inv = safe_rcp(uv_scale);
const float s = sin(uv_rotation);
const float c = cos(uv_rotation);
const float2x2 rot = float2x2(float2(c, s), float2(-s, c));
float3x2 texture_matrix = float3x2::identity();
/*
* The order in which the three transforms are applied has been carefully chosen to be easy to
* invert.
*
* The translation is applied last so that the origin goes to `uv_translation`
* The rotation is applied after the scale so that the `u` direction's angle is `uv_rotation`
* Scale is the only transform that changes the length of the basis vectors and if it is applied
* first it's independent of the other transforms.
*
* These properties are not true with a different order.
*/
/* Apply scale. */
texture_matrix = from_scale<float2x2>(uv_scale_inv) * texture_matrix;
/* Apply rotation. */
texture_matrix = rot * texture_matrix;
/* Apply translation. */
texture_matrix[2] += uv_translation;
return texture_matrix;
}
static float4x3 expand_4x2_mat(float4x2 strokemat)
{
float4x3 strokemat4x3 = float4x3(strokemat);
/*
* We need the diagonal of ones to start from the bottom right instead top left to properly
* apply the two matrices.
*
* i.e.
* # # # # # # # #
* We need # # # # Instead of # # # #
* 0 0 0 1 0 0 1 0
*
*/
strokemat4x3[2][2] = 0.0f;
strokemat4x3[3][2] = 1.0f;
return strokemat4x3;
}
Span<float4x2> Drawing::texture_matrices() const
{
this->runtime->curve_texture_matrices.ensure([&](Vector<float4x2> &r_data) {
const CurvesGeometry &curves = this->strokes();
const AttributeAccessor attributes = curves.attributes();
const VArray<float> uv_rotations = *attributes.lookup_or_default<float>(
"uv_rotation", AttrDomain::Curve, 0.0f);
const VArray<float2> uv_translations = *attributes.lookup_or_default<float2>(
"uv_translation", AttrDomain::Curve, float2(0.0f, 0.0f));
const VArray<float2> uv_scales = *attributes.lookup_or_default<float2>(
"uv_scale", AttrDomain::Curve, float2(1.0f, 1.0f));
const OffsetIndices<int> points_by_curve = curves.points_by_curve();
const Span<float3> positions = curves.positions();
const Span<float3> normals = this->curve_plane_normals();
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];
const float3 normal = normals[curve_i];
const float4x2 strokemat = get_local_to_stroke_matrix(positions.slice(points), normal);
const float3x2 texture_matrix = get_stroke_to_texture_matrix(
uv_rotations[curve_i], uv_translations[curve_i], uv_scales[curve_i]);
const float4x2 texspace = texture_matrix * expand_4x2_mat(strokemat);
r_data[curve_i] = texspace;
}
});
});
return this->runtime->curve_texture_matrices.data().as_span();
}
void Drawing::set_texture_matrices(Span<float4x2> matrices, const IndexMask &selection)
{
using namespace blender::math;
CurvesGeometry &curves = this->strokes_for_write();
MutableAttributeAccessor attributes = curves.attributes_for_write();
SpanAttributeWriter<float> uv_rotations = attributes.lookup_or_add_for_write_span<float>(
"uv_rotation", AttrDomain::Curve);
SpanAttributeWriter<float2> uv_translations = attributes.lookup_or_add_for_write_span<float2>(
"uv_translation", AttrDomain::Curve);
SpanAttributeWriter<float2> uv_scales = attributes.lookup_or_add_for_write_span<float2>(
"uv_scale",
AttrDomain::Curve,
AttributeInitVArray(VArray<float2>::ForSingle(float2(1.0f, 1.0f), curves.curves_num())));
const OffsetIndices<int> points_by_curve = curves.points_by_curve();
const Span<float3> positions = curves.positions();
const Span<float3> normals = this->curve_plane_normals();
selection.foreach_index(GrainSize(256), [&](const int64_t curve_i, const int64_t pos) {
const IndexRange points = points_by_curve[curve_i];
const float3 normal = normals[curve_i];
const float4x2 strokemat = get_local_to_stroke_matrix(positions.slice(points), normal);
const float4x2 texspace = matrices[pos];
/* We do the computation using doubles to avoid numerical precision errors. */
double4x3 strokemat4x3 = double4x3(expand_4x2_mat(strokemat));
/*
* We want to solve for `texture_matrix` in the equation: `texspace = texture_matrix *
* strokemat4x3` Because these matrices are not square we can not use a standard inverse.
*
* Our problem has the form of: `X = A * Y`
* We can solve for `A` using: `A = X * B`
*
* Where `B` is the Right-sided inverse or Moore-Penrose pseudo inverse.
* Calculated as:
*
* |--------------------------|
* | B = T(Y) * (Y * T(Y))^-1 |
* |--------------------------|
*
* And `T()` is transpose and `()^-1` is the inverse.
*/
const double3x4 transpose_strokemat = transpose(strokemat4x3);
const double3x4 right_inverse = transpose_strokemat *
invert(strokemat4x3 * transpose_strokemat);
const float3x2 texture_matrix = float3x2(double4x2(texspace) * right_inverse);
/* Solve for translation, the translation is simply the origin. */
const float2 uv_translation = texture_matrix[2];
/* Solve rotation, the angle of the `u` basis is the rotation. */
const float uv_rotation = atan2(texture_matrix[0][1], texture_matrix[0][0]);
/* Calculate the determinant to check if the `v` scale is negative. */
const float det = determinant(float2x2(texture_matrix));
/* Solve scale, scaling is the only transformation that changes the length, so scale factor
* is simply the length. And flip the sign of `v` if the determinant is negative. */
const float2 uv_scale = safe_rcp(
float2(length(texture_matrix[0]), sign(det) * length(texture_matrix[1])));
uv_rotations.span[curve_i] = uv_rotation;
uv_translations.span[curve_i] = uv_translation;
uv_scales.span[curve_i] = uv_scale;
});
uv_rotations.finish();
uv_translations.finish();
uv_scales.finish();
this->tag_texture_matrices_changed();
}
const bke::CurvesGeometry &Drawing::strokes() const
{
return this->geometry.wrap();
@@ -474,11 +676,17 @@ MutableSpan<ColorGeometry4f> Drawing::vertex_colors_for_write()
ColorGeometry4f(0.0f, 0.0f, 0.0f, 0.0f));
}
void Drawing::tag_texture_matrices_changed()
{
this->runtime->curve_texture_matrices.tag_dirty();
}
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();
this->tag_texture_matrices_changed();
}
void Drawing::tag_topology_changed()

View File

@@ -156,6 +156,113 @@ static void find_used_vertex_groups(const bGPDframe &gpf,
}
}
/*
* This takes the legacy uv tranforms and returns the stroke-space to texture-space matrix.
*/
static float3x2 get_legacy_stroke_to_texture_matrix(const float2 uv_translation,
const float uv_rotation,
const float2 uv_scale)
{
using namespace blender;
/* Bounding box data. */
const float2 minv = float2(-1.0f, -1.0f);
const float2 maxv = float2(1.0f, 1.0f);
/* Center of rotation. */
const float2 center = float2(0.5f, 0.5f);
const float2 uv_scale_inv = math::safe_rcp(uv_scale);
const float2 diagonal = maxv - minv;
const float sin_rotation = sin(uv_rotation);
const float cos_rotation = cos(uv_rotation);
const float2x2 rotation = float2x2(float2(cos_rotation, sin_rotation),
float2(-sin_rotation, cos_rotation));
float3x2 texture_matrix = float3x2::identity();
/* Apply bounding box rescaling. */
texture_matrix[2] -= minv;
texture_matrix = math::from_scale<float2x2>(1.0f / diagonal) * texture_matrix;
/* Apply translation. */
texture_matrix[2] += uv_translation;
/* Apply rotation. */
texture_matrix[2] -= center;
texture_matrix = rotation * texture_matrix;
texture_matrix[2] += center;
/* Apply scale. */
texture_matrix = math::from_scale<float2x2>(uv_scale_inv) * texture_matrix;
return texture_matrix;
}
/*
* This gets the legacy layer-space to stroke-space matrix.
*/
static blender::float4x2 get_legacy_layer_to_stroke_matrix(bGPDstroke *gps)
{
using namespace blender;
using namespace blender::math;
const bGPDspoint *points = gps->points;
const int totpoints = gps->totpoints;
if (totpoints < 2) {
return float4x2::identity();
}
const bGPDspoint *point0 = &points[0];
const bGPDspoint *point1 = &points[1];
const bGPDspoint *point3 = &points[int(totpoints * 0.75f)];
const float3 pt0 = float3(point0->x, point0->y, point0->z);
const float3 pt1 = float3(point1->x, point1->y, point1->z);
const float3 pt3 = float3(point3->x, point3->y, point3->z);
/* Local X axis (p0 -> p1) */
const float3 local_x = normalize(pt1 - pt0);
/* Point vector at 3/4 */
const float3 local_3 = (totpoints == 2) ? (pt3 * 0.001f) - pt0 : pt3 - pt0;
/* Vector orthogonal to polygon plane. */
const float3 normal = cross(local_x, local_3);
/* Local Y axis (cross to normal/x axis). */
const float3 local_y = normalize(cross(normal, local_x));
/* Get local space using first point as origin. */
const float4x2 mat = transpose(
float2x4(float4(local_x, -dot(pt0, local_x)), float4(local_y, -dot(pt0, local_y))));
return mat;
}
static blender::float4x2 get_legacy_texture_matrix(bGPDstroke *gps)
{
const float3x2 texture_matrix = get_legacy_stroke_to_texture_matrix(
float2(gps->uv_translation), gps->uv_rotation, float2(gps->uv_scale));
const float4x2 strokemat = get_legacy_layer_to_stroke_matrix(gps);
float4x3 strokemat4x3 = float4x3(strokemat);
/*
* We need the diagonal of ones to start from the bottom right instead top left to properly apply
* the two matrices.
*
* i.e.
* # # # # # # # #
* We need # # # # Instead of # # # #
* 0 0 0 1 0 0 1 0
*
*/
strokemat4x3[2][2] = 0.0f;
strokemat4x3[3][2] = 1.0f;
return texture_matrix * strokemat4x3;
}
void legacy_gpencil_frame_to_grease_pencil_drawing(const bGPDframe &gpf,
const ListBase &vertex_group_names,
GreasePencilDrawing &r_drawing)
@@ -259,17 +366,13 @@ void legacy_gpencil_frame_to_grease_pencil_drawing(const bGPDframe &gpf,
"hardness", AttrDomain::Curve);
SpanAttributeWriter<float> stroke_point_aspect_ratios =
attributes.lookup_or_add_for_write_span<float>("aspect_ratio", AttrDomain::Curve);
SpanAttributeWriter<float2> stroke_fill_translations =
attributes.lookup_or_add_for_write_span<float2>("fill_translation", AttrDomain::Curve);
SpanAttributeWriter<float> stroke_fill_rotations =
attributes.lookup_or_add_for_write_span<float>("fill_rotation", AttrDomain::Curve);
SpanAttributeWriter<float2> stroke_fill_scales = attributes.lookup_or_add_for_write_span<float2>(
"fill_scale", AttrDomain::Curve);
SpanAttributeWriter<ColorGeometry4f> stroke_fill_colors =
attributes.lookup_or_add_for_write_span<ColorGeometry4f>("fill_color", AttrDomain::Curve);
SpanAttributeWriter<int> stroke_materials = attributes.lookup_or_add_for_write_span<int>(
"material_index", AttrDomain::Curve);
Array<float4x2> legacy_texture_matrices(num_strokes);
int stroke_i = 0;
LISTBASE_FOREACH_INDEX (bGPDstroke *, gps, &gpf.strokes, stroke_i) {
stroke_cyclic.span[stroke_i] = (gps->flag & GP_STROKE_CYCLIC) != 0;
@@ -280,9 +383,6 @@ void legacy_gpencil_frame_to_grease_pencil_drawing(const bGPDframe &gpf,
stroke_hardnesses.span[stroke_i] = gps->hardness;
stroke_point_aspect_ratios.span[stroke_i] = gps->aspect_ratio[0] /
max_ff(gps->aspect_ratio[1], 1e-8);
stroke_fill_translations.span[stroke_i] = float2(gps->uv_translation);
stroke_fill_rotations.span[stroke_i] = gps->uv_rotation;
stroke_fill_scales.span[stroke_i] = float2(gps->uv_scale);
stroke_fill_colors.span[stroke_i] = ColorGeometry4f(gps->vert_color_fill);
stroke_materials.span[stroke_i] = gps->mat_nr;
@@ -367,8 +467,15 @@ void legacy_gpencil_frame_to_grease_pencil_drawing(const bGPDframe &gpf,
/* Unknown curve type. */
BLI_assert_unreachable();
}
const float4x2 legacy_texture_matrix = get_legacy_texture_matrix(gps);
legacy_texture_matrices[stroke_i] = legacy_texture_matrix;
}
/* Ensure that the normals are up to date. */
curves.tag_normals_changed();
drawing.set_texture_matrices(legacy_texture_matrices.as_span(), curves.curves_range());
delta_times.finish();
rotations.finish();
vertex_colors.finish();
@@ -380,9 +487,6 @@ void legacy_gpencil_frame_to_grease_pencil_drawing(const bGPDframe &gpf,
stroke_end_caps.finish();
stroke_hardnesses.finish();
stroke_point_aspect_ratios.finish();
stroke_fill_translations.finish();
stroke_fill_rotations.finish();
stroke_fill_scales.finish();
stroke_fill_colors.finish();
stroke_materials.finish();
}

View File

@@ -510,6 +510,7 @@ static void grease_pencil_geom_batch_ensure(Object &object,
const ed::greasepencil::DrawingInfo &info = drawings[drawing_i];
const Layer &layer = *grease_pencil.layers()[info.layer_index];
const float4x4 layer_space_to_object_space = layer.to_object_space(object);
const float4x4 object_space_to_layer_space = math::invert(layer_space_to_object_space);
const bke::CurvesGeometry &curves = info.drawing.strokes();
const bke::AttributeAccessor attributes = curves.attributes();
const OffsetIndices<int> points_by_curve = curves.points_by_curve();
@@ -538,6 +539,7 @@ static void grease_pencil_geom_batch_ensure(Object &object,
const VArray<int> materials = *attributes.lookup_or_default<int>(
"material_index", bke::AttrDomain::Curve, 0);
const Span<uint3> triangles = info.drawing.triangles();
const Span<float4x2> texture_matrices = info.drawing.texture_matrices();
const Span<int> verts_start_offsets = verts_start_offsets_per_visible_drawing[drawing_i];
const Span<int> tris_start_offsets = tris_start_offsets_per_visible_drawing[drawing_i];
IndexMaskMemory memory;
@@ -553,10 +555,11 @@ static void grease_pencil_geom_batch_ensure(Object &object,
int point_i,
int idx,
float length,
const float4x2 &texture_matrix,
GreasePencilStrokeVert &s_vert,
GreasePencilColorVert &c_vert) {
copy_v3_v3(s_vert.pos,
math::transform_point(layer_space_to_object_space, positions[point_i]));
const float3 pos = math::transform_point(layer_space_to_object_space, positions[point_i]);
copy_v3_v3(s_vert.pos, pos);
s_vert.radius = radii[point_i] * ((end_cap == GP_STROKE_CAP_TYPE_ROUND) ? 1.0f : -1.0f);
/* Convert to legacy "pixel" space. The shader expects the values to be in this space.
* Otherwise the values will get clamped. */
@@ -570,8 +573,7 @@ static void grease_pencil_geom_batch_ensure(Object &object,
s_vert.packed_asp_hard_rot = pack_rotation_aspect_hardness(
rotations[point_i], stroke_point_aspect_ratios[curve_i], stroke_hardnesses[curve_i]);
s_vert.u_stroke = length;
/* TODO: Populate fill UVs. */
s_vert.uv_fill[0] = s_vert.uv_fill[1] = 0;
copy_v2_v2(s_vert.uv_fill, texture_matrix * float4(pos, 1.0f));
copy_v4_v4(c_vert.vcol, vertex_colors[point_i]);
copy_v4_v4(c_vert.fcol, stroke_fill_colors[curve_i]);
@@ -591,6 +593,7 @@ static void grease_pencil_geom_batch_ensure(Object &object,
IndexRange verts_range = IndexRange(verts_start_offset, num_verts);
MutableSpan<GreasePencilStrokeVert> verts_slice = verts.slice(verts_range);
MutableSpan<GreasePencilColorVert> cols_slice = cols.slice(verts_range);
const float4x2 texture_matrix = texture_matrices[curve_i] * object_space_to_layer_space;
const Span<float> lengths = curves.evaluated_lengths_for_curve(curve_i, is_cyclic);
@@ -619,6 +622,7 @@ static void grease_pencil_geom_batch_ensure(Object &object,
points[i],
idx,
length,
texture_matrix,
verts_slice[idx],
cols_slice[idx]);
}
@@ -633,6 +637,7 @@ static void grease_pencil_geom_batch_ensure(Object &object,
points[0],
idx,
length,
texture_matrix,
verts_slice[idx],
cols_slice[idx]);
}

View File

@@ -9,6 +9,7 @@
#include "BKE_curves.hh"
#include "BKE_grease_pencil.hh"
#include "BKE_material.h"
#include "BKE_scene.hh"
#include "BLI_length_parameterize.hh"
#include "BLI_math_color.h"
@@ -117,6 +118,7 @@ class PaintOperation : public GreasePencilStrokeOperation {
Vector<float2> screen_space_smoothed_coords_;
/* The start index of the smoothing window. */
int active_smooth_start_index_ = 0;
blender::float4x2 texture_space_ = float4x2::identity();
/* Helper class to project screen space coordinates to 3d. */
ed::greasepencil::DrawingPlacement placement_;
@@ -437,6 +439,9 @@ struct PaintOperationExecutor {
bke::AttrDomain::Point,
{"position", "radius", "opacity", "vertex_color"},
curves.points_range().take_back(1));
drawing_->set_texture_matrices(Span<float4x2>(&(self.texture_space_), 1),
IndexMask(IndexRange(curves.curves_range().last(), 1)));
}
void execute(PaintOperation &self, const bContext &C, const InputSample &extension_sample)
@@ -473,9 +478,9 @@ void PaintOperation::on_stroke_begin(const bContext &C, const InputSample &start
BKE_curvemapping_init(brush->gpencil_settings->curve_rand_saturation);
BKE_curvemapping_init(brush->gpencil_settings->curve_rand_value);
const bke::greasepencil::Layer layer = *grease_pencil->get_active_layer();
/* Initialize helper class for projecting screen space coordinates. */
placement_ = ed::greasepencil::DrawingPlacement(
*scene, *region, *view3d, *eval_object, *grease_pencil->get_active_layer());
placement_ = ed::greasepencil::DrawingPlacement(*scene, *region, *view3d, *eval_object, layer);
if (placement_.use_project_to_surface()) {
placement_.cache_viewport_depths(CTX_data_depsgraph_pointer(&C), region, view3d);
}
@@ -484,6 +489,48 @@ void PaintOperation::on_stroke_begin(const bContext &C, const InputSample &start
placement_.set_origin_to_nearest_stroke(start_sample.mouse_position);
}
float3 u_dir;
float3 v_dir;
/* Set the texture space origin to be the first point. */
float3 origin = placement_.project(start_sample.mouse_position);
/* Align texture with the drawing plane. */
switch (scene->toolsettings->gp_sculpt.lock_axis) {
case GP_LOCKAXIS_VIEW:
u_dir = math::normalize(
placement_.project(float2(region->winx, 0.0f) + start_sample.mouse_position) - origin);
v_dir = math::normalize(
placement_.project(float2(0.0f, region->winy) + start_sample.mouse_position) - origin);
break;
case GP_LOCKAXIS_Y:
u_dir = float3(1.0f, 0.0f, 0.0f);
v_dir = float3(0.0f, 0.0f, 1.0f);
break;
case GP_LOCKAXIS_X:
u_dir = float3(0.0f, 1.0f, 0.0f);
v_dir = float3(0.0f, 0.0f, 1.0f);
break;
case GP_LOCKAXIS_Z:
u_dir = float3(1.0f, 0.0f, 0.0f);
v_dir = float3(0.0f, 1.0f, 0.0f);
break;
case GP_LOCKAXIS_CURSOR: {
float3x3 mat;
BKE_scene_cursor_rot_to_mat3(&scene->cursor, mat.ptr());
u_dir = mat * float3(1.0f, 0.0f, 0.0f);
v_dir = mat * float3(0.0f, 1.0f, 0.0f);
origin = float3(scene->cursor.location);
break;
}
}
this->texture_space_ = math::transpose(float2x4(float4(u_dir, -math::dot(u_dir, origin)),
float4(v_dir, -math::dot(v_dir, origin))));
/* `View` is already stored in object space but all others are in layer space. */
if (scene->toolsettings->gp_sculpt.lock_axis != GP_LOCKAXIS_VIEW) {
this->texture_space_ = this->texture_space_ * layer.to_object_space(*object);
}
Material *material = BKE_grease_pencil_object_material_ensure_from_active_input_brush(
CTX_data_main(&C), object, brush);
const int material_index = BKE_object_material_index_get(object, material);
@@ -591,6 +638,9 @@ void PaintOperation::process_stroke_end(const bContext &C, bke::greasepencil::Dr
}
selection.finish();
drawing.set_texture_matrices(Span<float4x2>(&(this->texture_space_), 1),
IndexMask(IndexRange(curves.curves_range().last(), 1)));
}
void PaintOperation::on_stroke_done(const bContext &C)