Grease Pencil: Convert from mesh to Grease Pencil

Convert mesh objects to grease pencil, optionally preserve mesh faces as
filled shapes, and convert crease edges into strokes. Following options
are available:

- Whether to convert faces to filled strokes.
- Stroke thickness.
- Stroke offset (Using normal direction to lift strokes out of mesh
  surfaces).

Note that "Crease Angle" option from legacy grease pencil is not ported
to this implementation. This option is deemed more suitable for using
geometry nodes to achieve.

Resolves #126480

Pull Request: https://projects.blender.org/blender/blender/pulls/131854
This commit is contained in:
YimingWu 2025-01-14 08:28:52 +01:00 committed by YimingWu
parent d675e886d4
commit 9d509de52a
2 changed files with 149 additions and 20 deletions

View file

@ -35,6 +35,7 @@
#include "DNA_scene_types.h"
#include "DNA_vfont_types.h"
#include "BLI_array_utils.hh"
#include "BLI_ghash.h"
#include "BLI_listbase.h"
#include "BLI_math_matrix.h"
@ -2874,6 +2875,7 @@ struct ObjectConversionInfo {
Object *obact;
bool keep_original;
bool do_merge_customdata;
PointerRNA *op_props;
ReportList *reports;
};
@ -3086,6 +3088,145 @@ static Object *convert_mesh_to_mesh(Base &base, ObjectConversionInfo &info, Base
return newob;
}
static int mesh_to_grease_pencil_add_material(Main &bmain,
Object &ob_grease_pencil,
const StringRefNull name,
const std::optional<float4> &stroke_color,
const std::optional<float4> &fill_color)
{
int index;
Material *ma = BKE_grease_pencil_object_material_ensure_by_name(
&bmain, &ob_grease_pencil, DATA_(name.c_str()), &index);
if (stroke_color.has_value()) {
copy_v4_v4(ma->gp_style->stroke_rgba, stroke_color.value());
srgb_to_linearrgb_v4(ma->gp_style->stroke_rgba, ma->gp_style->stroke_rgba);
}
if (fill_color.has_value()) {
copy_v4_v4(ma->gp_style->fill_rgba, fill_color.value());
srgb_to_linearrgb_v4(ma->gp_style->fill_rgba, ma->gp_style->fill_rgba);
}
SET_FLAG_FROM_TEST(ma->gp_style->flag, stroke_color.has_value(), GP_MATERIAL_STROKE_SHOW);
SET_FLAG_FROM_TEST(ma->gp_style->flag, fill_color.has_value(), GP_MATERIAL_FILL_SHOW);
return index;
}
static void mesh_data_to_grease_pencil(const Mesh &mesh_eval,
GreasePencil &grease_pencil,
const int current_frame,
const bool generate_faces,
const float stroke_radius,
const float offset)
{
grease_pencil.flag |= GREASE_PENCIL_STROKE_ORDER_3D;
if (mesh_eval.edges_num <= 0) {
return;
}
bke::greasepencil::Layer &layer_line = grease_pencil.add_layer(DATA_("Lines"));
bke::greasepencil::Drawing *drawing_line = grease_pencil.insert_frame(layer_line, current_frame);
constexpr int face_mat_index = 1;
const Span<float3> mesh_positions = mesh_eval.vert_positions();
const Span<float3> vert_normals = mesh_eval.vert_normals();
const Span<int2> edges = mesh_eval.edges();
const OffsetIndices<int> faces = mesh_eval.faces();
Span<int> faces_span = faces.data();
const Span<int> corner_verts = mesh_eval.corner_verts();
if (generate_faces && !faces.is_empty()) {
bke::greasepencil::Layer &layer_fill = grease_pencil.add_layer(DATA_("Fills"));
bke::greasepencil::Drawing *drawing_fill = grease_pencil.insert_frame(layer_fill,
current_frame);
const int fills_num = faces.size();
const int fills_points_num = corner_verts.size();
drawing_fill->strokes_for_write().resize(fills_points_num, fills_num);
bke::CurvesGeometry &curves_fill = drawing_fill->strokes_for_write();
MutableSpan<float3> positions_fill = curves_fill.positions_for_write();
MutableSpan<int> offsets_fill = curves_fill.offsets_for_write();
MutableSpan<bool> cyclic_fill = curves_fill.cyclic_for_write();
bke::SpanAttributeWriter<int> stroke_materials_fill =
curves_fill.attributes_for_write().lookup_or_add_for_write_span<int>(
"material_index", bke::AttrDomain::Curve);
curves_fill.fill_curve_types(CURVE_TYPE_POLY);
array_utils::gather(mesh_positions, corner_verts, positions_fill);
array_utils::copy(faces_span, offsets_fill);
cyclic_fill.fill(true);
stroke_materials_fill.span.fill(face_mat_index);
stroke_materials_fill.finish();
}
const int edges_num = edges.size();
const int points_num = edges_num * 2;
bke::CurvesGeometry &curves = drawing_line->strokes_for_write();
curves.resize(points_num, edges_num);
MutableSpan<float3> positions = curves.positions_for_write();
MutableSpan<int> offsets = curves.offsets_for_write();
MutableSpan<float> radii = curves.radius_for_write();
curves.fill_curve_types(CURVE_TYPE_POLY);
for (const int edge_i : edges.index_range()) {
const int2 edge = edges[edge_i];
const int point_i = edge_i * 2;
positions[point_i] = mesh_positions[edge[0]] + offset * vert_normals[edge[0]];
positions[point_i + 1] = mesh_positions[edge[1]] + offset * vert_normals[edge[1]];
radii[point_i] = radii[point_i + 1] = stroke_radius;
}
radii.fill(stroke_radius);
offset_indices::fill_constant_group_size(2, 0, offsets);
}
static Object *convert_mesh_to_grease_pencil(Base &base,
ObjectConversionInfo &info,
Base **r_new_base)
{
Object *ob = base.object;
ob->flag |= OB_DONE;
Object *newob = get_object_for_conversion(base, info, r_new_base);
const bool generate_faces = RNA_boolean_get(info.op_props, "faces");
const int thickness = RNA_int_get(info.op_props, "thickness");
const float offset = RNA_float_get(info.op_props, "offset");
/* To be compatible with the thickness value prior to Grease Pencil v3. */
const float stroke_radius = float(thickness) / 1000.0f;
const Object *ob_eval = DEG_get_evaluated_object(info.depsgraph, ob);
const Mesh *mesh_eval = BKE_object_get_evaluated_mesh(ob_eval);
BKE_object_free_derived_caches(newob);
BKE_object_free_modifiers(newob, 0);
GreasePencil *grease_pencil = BKE_grease_pencil_add(info.bmain, BKE_id_name(mesh_eval->id));
newob->data = grease_pencil;
newob->type = OB_GREASE_PENCIL;
/* Reset `ob->totcol` since currently the generic / grease pencil material functions still
* depend on this value being coherent (The same value as `GreasePencil::material_array_num`).
*/
short *totcol = BKE_object_material_len_p(newob);
newob->totcol = *totcol;
mesh_to_grease_pencil_add_material(
*info.bmain, *newob, DATA_("Stroke"), float4(0.0f, 0.0f, 0.0f, 1.0f), {});
if (generate_faces) {
mesh_to_grease_pencil_add_material(*info.bmain, *newob, DATA_("Fill"), {}, float4(1.0f));
}
mesh_data_to_grease_pencil(
*mesh_eval, *grease_pencil, info.scene->r.cfra, generate_faces, stroke_radius, offset);
return newob;
}
static Object *convert_mesh(Base &base,
const ObjectType target,
ObjectConversionInfo &info,
@ -3100,6 +3241,8 @@ static Object *convert_mesh(Base &base,
return convert_mesh_to_point_cloud(base, info, r_new_base);
case OB_MESH:
return convert_mesh_to_mesh(base, info, r_new_base);
case OB_GREASE_PENCIL:
return convert_mesh_to_grease_pencil(base, info, r_new_base);
default:
/* Current logic does convert mesh to mesh for any other target types. This would change
* after other types of conversion are designed and implemented. */
@ -3728,6 +3871,7 @@ static int object_convert_exec(bContext *C, wmOperator *op)
info.obact = obact;
info.keep_original = keep_original;
info.do_merge_customdata = do_merge_customdata;
info.op_props = op->ptr;
info.reports = op->reports;
Base *act_base = nullptr;
@ -3913,11 +4057,9 @@ static void object_convert_ui(bContext * /*C*/, wmOperator *op)
if (target == OB_MESH) {
uiItemR(layout, op->ptr, "merge_customdata", UI_ITEM_NONE, std::nullopt, ICON_NONE);
}
else if (target == OB_GPENCIL_LEGACY) {
else if (target == OB_GREASE_PENCIL) {
uiItemR(layout, op->ptr, "thickness", UI_ITEM_NONE, std::nullopt, ICON_NONE);
uiItemR(layout, op->ptr, "angle", UI_ITEM_NONE, std::nullopt, ICON_NONE);
uiItemR(layout, op->ptr, "offset", UI_ITEM_NONE, std::nullopt, ICON_NONE);
uiItemR(layout, op->ptr, "seams", UI_ITEM_NONE, std::nullopt, ICON_NONE);
uiItemR(layout, op->ptr, "faces", UI_ITEM_NONE, std::nullopt, ICON_NONE);
}
}
@ -3959,20 +4101,7 @@ void OBJECT_OT_convert(wmOperatorType *ot)
"Merge UVs",
"Merge UV coordinates that share a vertex to account for imprecision in some modifiers");
prop = RNA_def_float_rotation(ot->srna,
"angle",
0,
nullptr,
DEG2RADF(0.0f),
DEG2RADF(180.0f),
"Threshold Angle",
"Threshold to determine ends of the strokes",
DEG2RADF(0.0f),
DEG2RADF(180.0f));
RNA_def_property_float_default(prop, DEG2RADF(70.0f));
RNA_def_int(ot->srna, "thickness", 5, 1, 100, "Thickness", "", 1, 100);
RNA_def_boolean(ot->srna, "seams", false, "Only Seam Edges", "Convert only seam edges");
RNA_def_boolean(ot->srna, "faces", true, "Export Faces", "Export faces as filled strokes");
RNA_def_float_distance(ot->srna,
"offset",

View file

@ -136,10 +136,10 @@ def main():
ConversionPair('Curves 6', 'BezierCircle', 'CURVES', 'CURVES'),
ConversionPair('Curves 7', 'BezierCurve', 'CURVES', 'CURVES'),
ConversionPair('Curves 8', 'Text', 'CURVES', 'CURVES'),
ConversionPair('GreasePencil 1', 'Cube', 'GREASEPENCIL', 'MESH'),
ConversionPair('GreasePencil 2', 'CubeWithEdges', 'GREASEPENCIL', 'MESH'),
ConversionPair('GreasePencil 3', 'Plane', 'GREASEPENCIL', 'MESH'),
ConversionPair('GreasePencil 4', 'HollowPlane', 'GREASEPENCIL', 'MESH'),
ConversionPair('GreasePencil 1', 'Cube', 'GREASEPENCIL', 'GREASEPENCIL'),
ConversionPair('GreasePencil 2', 'CubeWithEdges', 'GREASEPENCIL', 'GREASEPENCIL'),
ConversionPair('GreasePencil 3', 'Plane', 'GREASEPENCIL', 'GREASEPENCIL'),
ConversionPair('GreasePencil 4', 'HollowPlane', 'GREASEPENCIL', 'GREASEPENCIL'),
ConversionPair('GreasePencil 5', 'Suzanne', 'GREASEPENCIL', 'GREASEPENCIL'),
ConversionPair('GreasePencil 6', 'BezierCircle', 'GREASEPENCIL', 'GREASEPENCIL'),
ConversionPair('GreasePencil 7', 'BezierCurve', 'GREASEPENCIL', 'GREASEPENCIL'),