From 39f63c8c086dd9dbe286924909e400d0d47b186c Mon Sep 17 00:00:00 2001 From: Robin Hohnsbeen Date: Tue, 14 Feb 2023 15:29:27 +0100 Subject: [PATCH] Sculpting: Vector displacement for the sculpting draw brush Sculpt: Added vector displacement for the sculpting draw brush (area plane mapping only for now) Vector displacement maps (VDM) provide a way to create complex displacements that can have overhangs in one brush dab. This is unlike standard displacement with height maps that only displace in the normal direction. Forms like ears, curled horns, etc can be created in one click if VMDs are used. There is a checkbox on the draw brush in the texture settings "Vector Displacement" that enables/disables this feature. Technical description: The RGB channels of a texture in a brush stroke are read and interpreted as individual vectors, that are used to offset vertices. As of now, this is only working for the draw brush using the area plane mapping. Symmetry and radial symmetry are working. A few things to consider when making VD-Maps: * UVs need to stay intact for the bake mesh (e.g. voxel remeshing can't be used to create VD Meshes) * When exporting a VD Map it should be in the file format OpenEXR (for positive and negative floating point values). * Export resolution can be 512x512 or lower (EXR files can get very large, but VDM brushes don't need a high resolution) And when using them: * Inside Blender clamping needs to be unchecked on the texture * The brush falloff should be set to constant (or nearly constant) This patch was inspired by this [right-click-select proposal](https://blender.community/c/rightclickselect/WqWx/) Thanks for the post! (Moved [this patch](https://archive.blender.org/developer/D17080) to here.) Co-authored-by: Robin Hohnsbeen Pull Request #104481 --- .../startup/bl_ui/properties_paint_common.py | 5 + .../editors/sculpt_paint/paint_cursor.cc | 17 +- .../editors/sculpt_paint/paint_intern.h | 21 +- .../editors/sculpt_paint/paint_utils.c | 50 ++-- source/blender/editors/sculpt_paint/sculpt.cc | 244 ++++++++++++------ .../sculpt_paint/sculpt_brush_types.cc | 41 ++- .../editors/sculpt_paint/sculpt_intern.hh | 31 ++- source/blender/makesdna/DNA_brush_enums.h | 1 + source/blender/makesrna/intern/rna_brush.c | 8 + 9 files changed, 281 insertions(+), 137 deletions(-) diff --git a/release/scripts/startup/bl_ui/properties_paint_common.py b/release/scripts/startup/bl_ui/properties_paint_common.py index c912de8f3e4..63dd18cb703 100644 --- a/release/scripts/startup/bl_ui/properties_paint_common.py +++ b/release/scripts/startup/bl_ui/properties_paint_common.py @@ -1141,6 +1141,11 @@ def brush_texture_settings(layout, brush, sculpt): # texture_sample_bias layout.prop(brush, "texture_sample_bias", slider=True, text="Sample Bias") + if brush.sculpt_tool == 'DRAW': + col = layout.column() + col.active = tex_slot.map_mode == 'AREA_PLANE' + col.prop(brush, "use_color_as_displacement", text="Vector Displacement") + def brush_mask_texture_settings(layout, brush): mask_tex_slot = brush.mask_texture_slot diff --git a/source/blender/editors/sculpt_paint/paint_cursor.cc b/source/blender/editors/sculpt_paint/paint_cursor.cc index a2fa4745f74..53b56397adb 100644 --- a/source/blender/editors/sculpt_paint/paint_cursor.cc +++ b/source/blender/editors/sculpt_paint/paint_cursor.cc @@ -37,6 +37,7 @@ #include "WM_api.h" #include "wm_cursors.h" +#include "IMB_colormanagement.h" #include "IMB_imbuf_types.h" #include "ED_image.h" @@ -200,10 +201,18 @@ static void load_tex_task_cb_ex(void *__restrict userdata, y = len * sinf(angle); } - if (col) { - float rgba[4]; + float avg; + float rgba[4]; + paint_get_tex_pixel(mtex, x, y, pool, thread_id, &avg, rgba); - paint_get_tex_pixel_col(mtex, x, y, rgba, pool, thread_id, convert_to_linear, colorspace); + if (col) { + if (convert_to_linear) { + IMB_colormanagement_colorspace_to_scene_linear_v3(rgba, colorspace); + } + + linearrgb_to_srgb_v3_v3(rgba, rgba); + + clamp_v4(rgba, 0.0f, 1.0f); buffer[index * 4] = rgba[0] * 255; buffer[index * 4 + 1] = rgba[1] * 255; @@ -211,8 +220,6 @@ static void load_tex_task_cb_ex(void *__restrict userdata, buffer[index * 4 + 3] = rgba[3] * 255; } else { - float avg = paint_get_tex_pixel(mtex, x, y, pool, thread_id); - avg += br->texture_sample_bias; /* Clamp to avoid precision overflow. */ diff --git a/source/blender/editors/sculpt_paint/paint_intern.h b/source/blender/editors/sculpt_paint/paint_intern.h index ee07f7cc26d..312d93051dc 100644 --- a/source/blender/editors/sculpt_paint/paint_intern.h +++ b/source/blender/editors/sculpt_paint/paint_intern.h @@ -352,16 +352,17 @@ void paint_calc_redraw_planes(float planes[4][4], float paint_calc_object_space_radius(struct ViewContext *vc, const float center[3], float pixel_radius); -float paint_get_tex_pixel( - const struct MTex *mtex, float u, float v, struct ImagePool *pool, int thread); -void paint_get_tex_pixel_col(const struct MTex *mtex, - float u, - float v, - float rgba[4], - struct ImagePool *pool, - int thread, - bool convert, - struct ColorSpace *colorspace); + +/** + * Returns true when a color was sampled and false when a value was sampled. + */ +bool paint_get_tex_pixel(const struct MTex *mtex, + float u, + float v, + struct ImagePool *pool, + int thread, + float *r_intensity, + float r_rgba[4]); /** * Used for both 3D view and image window. diff --git a/source/blender/editors/sculpt_paint/paint_utils.c b/source/blender/editors/sculpt_paint/paint_utils.c index 98fefd86d95..ef794ea6a39 100644 --- a/source/blender/editors/sculpt_paint/paint_utils.c +++ b/source/blender/editors/sculpt_paint/paint_utils.c @@ -146,45 +146,29 @@ float paint_calc_object_space_radius(ViewContext *vc, const float center[3], flo return len_v3(delta) / scale; } -float paint_get_tex_pixel(const MTex *mtex, float u, float v, struct ImagePool *pool, int thread) -{ - float intensity; - float rgba_dummy[4]; - const float co[3] = {u, v, 0.0f}; - - RE_texture_evaluate(mtex, co, thread, pool, false, false, &intensity, rgba_dummy); - - return intensity; -} - -void paint_get_tex_pixel_col(const MTex *mtex, - float u, - float v, - float rgba[4], - struct ImagePool *pool, - int thread, - bool convert_to_linear, - struct ColorSpace *colorspace) +bool paint_get_tex_pixel(const MTex *mtex, + float u, + float v, + struct ImagePool *pool, + int thread, + /* Return arguments. */ + float *r_intensity, + float r_rgba[4]) { const float co[3] = {u, v, 0.0f}; float intensity; + const bool has_rgb = RE_texture_evaluate( + mtex, co, thread, pool, false, false, &intensity, r_rgba); + *r_intensity = intensity; - const bool hasrgb = RE_texture_evaluate(mtex, co, thread, pool, false, false, &intensity, rgba); - - if (!hasrgb) { - rgba[0] = intensity; - rgba[1] = intensity; - rgba[2] = intensity; - rgba[3] = 1.0f; + if (!has_rgb) { + r_rgba[0] = intensity; + r_rgba[1] = intensity; + r_rgba[2] = intensity; + r_rgba[3] = 1.0f; } - if (convert_to_linear) { - IMB_colormanagement_colorspace_to_scene_linear_v3(rgba, colorspace); - } - - linearrgb_to_srgb_v3_v3(rgba, rgba); - - clamp_v4(rgba, 0.0f, 1.0f); + return has_rgb; } void paint_stroke_operator_properties(wmOperatorType *ot) diff --git a/source/blender/editors/sculpt_paint/sculpt.cc b/source/blender/editors/sculpt_paint/sculpt.cc index ebb8490276d..59bf3f04dd6 100644 --- a/source/blender/editors/sculpt_paint/sculpt.cc +++ b/source/blender/editors/sculpt_paint/sculpt.cc @@ -54,6 +54,7 @@ #include "BKE_scene.h" #include "BKE_subdiv_ccg.h" #include "BKE_subsurf.h" +#include "BLI_math_vector.hh" #include "NOD_texture.h" @@ -2555,80 +2556,12 @@ static float brush_strength(const Sculpt *sd, } } -float SCULPT_brush_strength_factor(SculptSession *ss, - const Brush *br, - const float brush_point[3], - float len, - const float vno[3], - const float fno[3], - float mask, - const PBVHVertRef vertex, - const int thread_id, - AutomaskingNodeData *automask_data) +static float sculpt_apply_hardness(const SculptSession *ss, const float input_len) { - StrokeCache *cache = ss->cache; - const Scene *scene = cache->vc->scene; - const MTex *mtex = BKE_brush_mask_texture_get(br, OB_MODE_SCULPT); - float avg = 1.0f; - float rgba[4]; - float point[3]; - - sub_v3_v3v3(point, brush_point, cache->plane_offset); - - if (!mtex->tex) { - avg = 1.0f; - } - else if (mtex->brush_map_mode == MTEX_MAP_MODE_3D) { - /* Get strength by feeding the vertex location directly into a texture. */ - avg = BKE_brush_sample_tex_3d(scene, br, mtex, point, rgba, 0, ss->tex_pool); - } - else { - float symm_point[3], point_2d[2]; - /* Quite warnings. */ - float x = 0.0f, y = 0.0f; - - /* If the active area is being applied for symmetry, flip it - * across the symmetry axis and rotate it back to the original - * position in order to project it. This insures that the - * brush texture will be oriented correctly. */ - if (cache->radial_symmetry_pass) { - mul_m4_v3(cache->symm_rot_mat_inv, point); - } - flip_v3_v3(symm_point, point, cache->mirror_symmetry_pass); - - ED_view3d_project_float_v2_m4(cache->vc->region, symm_point, point_2d, cache->projection_mat); - - /* Still no symmetry supported for other paint modes. - * Sculpt does it DIY. */ - if (mtex->brush_map_mode == MTEX_MAP_MODE_AREA) { - /* Similar to fixed mode, but projects from brush angle - * rather than view direction. */ - - mul_m4_v3(cache->brush_local_mat, symm_point); - - x = symm_point[0]; - y = symm_point[1]; - - x *= mtex->size[0]; - y *= mtex->size[1]; - - x += mtex->ofs[0]; - y += mtex->ofs[1]; - - avg = paint_get_tex_pixel(mtex, x, y, ss->tex_pool, thread_id); - - avg += br->texture_sample_bias; - } - else { - const float point_3d[3] = {point_2d[0], point_2d[1], 0.0f}; - avg = BKE_brush_sample_tex_3d(scene, br, mtex, point_3d, rgba, 0, ss->tex_pool); - } - } - - /* Hardness. */ - float final_len = len; + const StrokeCache *cache = ss->cache; + float final_len = input_len; const float hardness = cache->paint_brush.hardness; - float p = len / cache->radius; + float p = input_len / cache->radius; if (p < hardness) { final_len = 0.0f; } @@ -2640,9 +2573,100 @@ float SCULPT_brush_strength_factor(SculptSession *ss, final_len = p * cache->radius; } + return final_len; +} + +static void sculpt_apply_texture(const SculptSession *ss, + const Brush *brush, + const float brush_point[3], + const int thread_id, + float *r_value, + float r_rgba[4]) +{ + StrokeCache *cache = ss->cache; + const Scene *scene = cache->vc->scene; + const MTex *mtex = BKE_brush_mask_texture_get(brush, OB_MODE_SCULPT); + + if (!mtex->tex) { + *r_value = 1.0f; + copy_v4_fl(r_rgba, 1.0f); + return; + } + + float point[3]; + sub_v3_v3v3(point, brush_point, cache->plane_offset); + + if (mtex->brush_map_mode == MTEX_MAP_MODE_3D) { + /* Get strength by feeding the vertex location directly into a texture. */ + *r_value = BKE_brush_sample_tex_3d(scene, brush, mtex, point, r_rgba, 0, ss->tex_pool); + } + else { + float symm_point[3]; + + /* If the active area is being applied for symmetry, flip it + * across the symmetry axis and rotate it back to the original + * position in order to project it. This insures that the + * brush texture will be oriented correctly. */ + if (cache->radial_symmetry_pass) { + mul_m4_v3(cache->symm_rot_mat_inv, point); + } + flip_v3_v3(symm_point, point, cache->mirror_symmetry_pass); + + /* Still no symmetry supported for other paint modes. + * Sculpt does it DIY. */ + if (mtex->brush_map_mode == MTEX_MAP_MODE_AREA) { + /* Similar to fixed mode, but projects from brush angle + * rather than view direction. */ + + mul_m4_v3(cache->brush_local_mat, symm_point); + + float x = symm_point[0]; + float y = symm_point[1]; + + x *= mtex->size[0]; + y *= mtex->size[1]; + + x += mtex->ofs[0]; + y += mtex->ofs[1]; + + paint_get_tex_pixel(mtex, x, y, ss->tex_pool, thread_id, r_value, r_rgba); + + add_v3_fl(r_rgba, brush->texture_sample_bias); // v3 -> Ignore alpha + *r_value -= brush->texture_sample_bias; + } + else { + float point_2d[2]; + ED_view3d_project_float_v2_m4( + cache->vc->region, symm_point, point_2d, cache->projection_mat); + const float point_3d[3] = {point_2d[0], point_2d[1], 0.0f}; + *r_value = BKE_brush_sample_tex_3d(scene, brush, mtex, point_3d, r_rgba, 0, ss->tex_pool); + } + } +} + +float SCULPT_brush_strength_factor(SculptSession *ss, + const Brush *brush, + const float brush_point[3], + float len, + const float vno[3], + const float fno[3], + float mask, + const PBVHVertRef vertex, + int thread_id, + AutomaskingNodeData *automask_data) +{ + StrokeCache *cache = ss->cache; + + float avg = 1.0f; + float rgba[4]; + sculpt_apply_texture(ss, brush, brush_point, thread_id, &avg, rgba); + + /* Hardness. */ + const float final_len = sculpt_apply_hardness(ss, len); + /* Falloff curve. */ - avg *= BKE_brush_curve_strength(br, final_len, cache->radius); - avg *= frontface(br, cache->view_normal, vno, fno); + avg *= BKE_brush_curve_strength(brush, final_len, cache->radius); + avg *= frontface(brush, cache->view_normal, vno, fno); /* Paint mask. */ avg *= 1.0f - mask; @@ -2653,6 +2677,69 @@ float SCULPT_brush_strength_factor(SculptSession *ss, return avg; } +void SCULPT_brush_strength_color(SculptSession *ss, + const Brush *brush, + const float brush_point[3], + float len, + const float vno[3], + const float fno[3], + float mask, + const PBVHVertRef vertex, + int thread_id, + AutomaskingNodeData *automask_data, + float r_rgba[4]) +{ + StrokeCache *cache = ss->cache; + + float avg = 1.0f; + sculpt_apply_texture(ss, brush, brush_point, thread_id, &avg, r_rgba); + + /* Hardness. */ + const float final_len = sculpt_apply_hardness(ss, len); + + /* Falloff curve. */ + const float falloff = BKE_brush_curve_strength(brush, final_len, cache->radius) * + frontface(brush, cache->view_normal, vno, fno); + + /* Paint mask. */ + const float paint_mask = 1.0f - mask; + + /* Auto-masking. */ + const float automasking_factor = SCULPT_automasking_factor_get( + cache->automasking, ss, vertex, automask_data); + + const float masks_combined = falloff * paint_mask * automasking_factor; + + mul_v4_fl(r_rgba, masks_combined); +} + +void SCULPT_calc_vertex_displacement(SculptSession *ss, + const Brush *brush, + float rgba[4], + float out_offset[3]) +{ + mul_v3_fl(rgba, ss->cache->bstrength); + /* Handle brush inversion */ + if (ss->cache->bstrength < 0) { + rgba[0] *= -1; + rgba[1] *= -1; + } + + /* Apply texture size */ + for (int i = 0; i < 3; ++i) { + rgba[i] *= blender::math::safe_divide(1.0f, pow2f(brush->mtex.size[i])); + } + + /* Transform vector to object space */ + mul_mat3_m4_v3(ss->cache->brush_local_mat_inv, rgba); + + /* Handle symmetry */ + if (ss->cache->radial_symmetry_pass) { + mul_m4_v3(ss->cache->symm_rot_mat, rgba); + } + flip_v3_v3(out_offset, rgba, ss->cache->mirror_symmetry_pass); +} + bool SCULPT_search_sphere_cb(PBVHNode *node, void *data_v) { SculptSearchSphereData *data = static_cast(data_v); @@ -2920,7 +3007,10 @@ static void calc_local_y(ViewContext *vc, const float center[3], float y[3]) mul_m4_v3(ob->world_to_object, y); } -static void calc_brush_local_mat(const MTex *mtex, Object *ob, float local_mat[4][4]) +static void calc_brush_local_mat(const MTex *mtex, + Object *ob, + float local_mat[4][4], + float local_mat_inv[4][4]) { const StrokeCache *cache = ob->sculpt->cache; float tmat[4][4]; @@ -2966,6 +3056,8 @@ static void calc_brush_local_mat(const MTex *mtex, Object *ob, float local_mat[4 scale_m4_fl(scale, radius); mul_m4_m4m4(tmat, mat, scale); + /* Return tmat as is (for converting from local area coords to model-space coords). */ + copy_m4_m4(local_mat_inv, tmat); /* Return inverse (for converting from model-space coords to local area coords). */ invert_m4_m4(local_mat, tmat); } @@ -3000,7 +3092,7 @@ static void update_brush_local_mat(Sculpt *sd, Object *ob) if (cache->mirror_symmetry_pass == 0 && cache->radial_symmetry_pass == 0) { const Brush *brush = BKE_paint_brush(&sd->paint); const MTex *mask_tex = BKE_brush_mask_texture_get(brush, OB_MODE_SCULPT); - calc_brush_local_mat(mask_tex, ob, cache->brush_local_mat); + calc_brush_local_mat(mask_tex, ob, cache->brush_local_mat, cache->brush_local_mat_inv); } } diff --git a/source/blender/editors/sculpt_paint/sculpt_brush_types.cc b/source/blender/editors/sculpt_paint/sculpt_brush_types.cc index 3ae892b6a9f..9e6ce8131ef 100644 --- a/source/blender/editors/sculpt_paint/sculpt_brush_types.cc +++ b/source/blender/editors/sculpt_paint/sculpt_brush_types.cc @@ -265,18 +265,35 @@ static void do_draw_brush_task_cb_ex(void *__restrict userdata, SCULPT_automasking_node_update(ss, &automask_data, &vd); /* Offset vertex. */ - const float fade = SCULPT_brush_strength_factor(ss, - brush, - vd.co, - sqrtf(test.dist), - vd.no, - vd.fno, - vd.mask ? *vd.mask : 0.0f, - vd.vertex, - thread_id, - &automask_data); - - mul_v3_v3fl(proxy[vd.i], offset, fade); + if (ss->cache->brush->flag2 & BRUSH_USE_COLOR_AS_DISPLACEMENT && + (brush->mtex.brush_map_mode == MTEX_MAP_MODE_AREA)) { + float r_rgba[4]; + SCULPT_brush_strength_color(ss, + brush, + vd.co, + sqrtf(test.dist), + vd.no, + vd.fno, + vd.mask ? *vd.mask : 0.0f, + vd.vertex, + thread_id, + &automask_data, + r_rgba); + SCULPT_calc_vertex_displacement(ss, brush, r_rgba, proxy[vd.i]); + } + else { + float fade = SCULPT_brush_strength_factor(ss, + brush, + vd.co, + sqrtf(test.dist), + vd.no, + vd.fno, + vd.mask ? *vd.mask : 0.0f, + vd.vertex, + thread_id, + &automask_data); + mul_v3_v3fl(proxy[vd.i], offset, fade); + } if (vd.is_mesh) { BKE_pbvh_vert_tag_update_normal(ss->pbvh, vd.vertex); diff --git a/source/blender/editors/sculpt_paint/sculpt_intern.hh b/source/blender/editors/sculpt_paint/sculpt_intern.hh index 37e7a522ccf..5117b49407f 100644 --- a/source/blender/editors/sculpt_paint/sculpt_intern.hh +++ b/source/blender/editors/sculpt_paint/sculpt_intern.hh @@ -586,8 +586,13 @@ struct StrokeCache { float sculpt_normal_symm[3]; /* Used for area texture mode, local_mat gets calculated by - * calc_brush_local_mat() and used in tex_strength(). */ + * calc_brush_local_mat() and used in sculpt_apply_texture(). + * Transforms from model-space coords to local area coords. + */ float brush_local_mat[4][4]; + /* The matrix from local area coords to model-space coords is used to calculate the vector + * displacement in area plane mode. */ + float brush_local_mat_inv[4][4]; float plane_offset[3]; /* used to shift the plane around when doing tiled strokes */ int tile_pass; @@ -1241,6 +1246,30 @@ float SCULPT_brush_strength_factor(SculptSession *ss, int thread_id, AutomaskingNodeData *automask_data); +/** + * Return a color of a brush texture on a particular vertex multiplied by active masks. + */ +void SCULPT_brush_strength_color(SculptSession *ss, + const Brush *brush, + const float brush_point[3], + float len, + const float vno[3], + const float fno[3], + float mask, + const PBVHVertRef vertex, + int thread_id, + AutomaskingNodeData *automask_data, + float r_rgba[4]); + +/** + * Calculates the vertex offset for a single vertex depending on the brush setting rgb as vector + * displacement. + */ +void SCULPT_calc_vertex_displacement(SculptSession *ss, + const Brush *brush, + float rgba[3], + float out_offset[3]); + /** * Tilts a normal by the x and y tilt values using the view axis. */ diff --git a/source/blender/makesdna/DNA_brush_enums.h b/source/blender/makesdna/DNA_brush_enums.h index 72357ea6734..3ea2c3d70b7 100644 --- a/source/blender/makesdna/DNA_brush_enums.h +++ b/source/blender/makesdna/DNA_brush_enums.h @@ -412,6 +412,7 @@ typedef enum eBrushFlags2 { BRUSH_CLOTH_USE_COLLISION = (1 << 6), BRUSH_AREA_RADIUS_PRESSURE = (1 << 7), BRUSH_GRAB_SILHOUETTE = (1 << 8), + BRUSH_USE_COLOR_AS_DISPLACEMENT = (1 << 9), } eBrushFlags2; typedef enum { diff --git a/source/blender/makesrna/intern/rna_brush.c b/source/blender/makesrna/intern/rna_brush.c index 8e6908841a6..13832457c23 100644 --- a/source/blender/makesrna/intern/rna_brush.c +++ b/source/blender/makesrna/intern/rna_brush.c @@ -2899,6 +2899,14 @@ static void rna_def_brush(BlenderRNA *brna) RNA_def_property_ui_text(prop, "Texture Sample Bias", "Value added to texture samples"); RNA_def_property_update(prop, 0, "rna_Brush_update"); + prop = RNA_def_property(srna, "use_color_as_displacement", PROP_BOOLEAN, PROP_NONE); + RNA_def_property_boolean_sdna(prop, NULL, "flag2", BRUSH_USE_COLOR_AS_DISPLACEMENT); + RNA_def_property_ui_text(prop, + "Vector Displacement", + "Handles each pixel color as individual vector for displacement. Works " + "only with area plane mapping"); + RNA_def_property_update(prop, 0, "rna_Brush_update"); + prop = RNA_def_property(srna, "normal_weight", PROP_FLOAT, PROP_FACTOR); RNA_def_property_float_sdna(prop, NULL, "normal_weight"); RNA_def_property_float_default(prop, 0);