sculpt: add grab mode

This commit is contained in:
luboslenco
2026-07-11 16:07:22 +02:00
parent 394029483b
commit a0e13782bf
5 changed files with 93 additions and 30 deletions
+5
View File
@@ -125,6 +125,11 @@ typedef enum {
UV_TYPE_PROJECT = 2,
} uv_type_t;
typedef enum {
SCULPT_TYPE_DRAW = 0,
SCULPT_TYPE_GRAB = 1,
} sculpt_type_t;
typedef enum {
PICKER_MASK_NONE = 0,
PICKER_MASK_MATERIAL = 1,
+58 -20
View File
@@ -113,16 +113,16 @@ node_shader_context_t *sculpt_make_sculpt_run(material_t *data, material_context
.compare_mode = "always",
.cull_mode = "none",
.vertex_elements = any_array_create_from_raw(
(void *[]){
GC_ALLOC_INIT(vertex_element_t, {.name = "pos", .data = "float2"}),
},
1),
(void *[]){
GC_ALLOC_INIT(vertex_element_t, {.name = "pos", .data = "float2"}),
},
1),
.color_attachments = any_array_create_from_raw(
(void *[]){
"RGBA128",
"R8",
},
2)});
(void *[]){
"RGBA128",
"R8",
},
2)});
node_shader_context_t *con_paint = node_shader_context_create(data, props);
con_paint->data->color_writes_red = u8_array_create_from_raw(
(u8[]){
@@ -160,8 +160,10 @@ node_shader_context_t *sculpt_make_sculpt_run(material_t *data, material_context
node_shader_t *kong = node_shader_context_make_kong(con_paint);
bool decal = context_is_decal();
bool particle = g_context->tool == TOOL_TYPE_PARTICLE;
// Grab drags the vertices along with the cursor instead of displacing along the normal
bool grab = g_context->tool == TOOL_TYPE_BRUSH && g_context->brush_sculpt == SCULPT_TYPE_GRAB;
// Decal fill layer: displacement must be confined to the decal box projection
bool decal_layer = g_context->layer->fill_material != NULL && g_context->layer->uv_type == UV_TYPE_PROJECT && g_context->tool == TOOL_TYPE_FILL;
bool decal_layer = g_context->layer->fill_material != NULL && g_context->layer->uv_type == UV_TYPE_PROJECT && g_context->tool == TOOL_TYPE_FILL;
bool has_wposition = g_context->tool == TOOL_TYPE_BRUSH || g_context->tool == TOOL_TYPE_ERASER || g_context->tool == TOOL_TYPE_CLONE ||
g_context->tool == TOOL_TYPE_BLUR || g_context->tool == TOOL_TYPE_PARTICLE || g_context->tool == TOOL_TYPE_FILL || decal;
@@ -197,20 +199,51 @@ node_shader_context_t *sculpt_make_sculpt_run(material_t *data, material_context
if (g_context->tool == TOOL_TYPE_BRUSH || g_context->tool == TOOL_TYPE_ERASER || g_context->tool == TOOL_TYPE_CLONE || g_context->tool == TOOL_TYPE_BLUR ||
g_context->tool == TOOL_TYPE_PARTICLE || decal) {
node_shader_write_frag(kong, "var depth: float = sample_lod(gbufferD, sampler_linear, constants.inp.xy, 0.0).r;");
node_shader_add_constant(kong, "invVP: float4x4", "_inv_view_proj_matrix");
// node_shader_write_frag(kong, "var winp: float4 = float4(float2(constants.inp.x, 1.0 - constants.inp.y) * 2.0 - 1.0, depth * 2.0 - 1.0, 1.0);");
node_shader_write_frag(kong, "var winp: float4 = float4(float2(constants.inp.x, 1.0 - constants.inp.y) * 2.0 - 1.0, depth, 1.0);");
node_shader_write_frag(kong, "winp = constants.invVP * winp;");
node_shader_write_frag(kong, "winp.xyz = winp.xyz / winp.w;");
if (grab) {
// Anchor a camera-facing plane at the grab_start surface point
node_shader_add_constant(kong, "grab_start: float2", "_grab_start");
node_shader_write_frag(kong, "var grab_ndc: float2 = float2(constants.grab_start.x, 1.0 - constants.grab_start.y) * 2.0 - 1.0;");
node_shader_write_frag(kong, "var grab_adepth: float = sample_lod(gbufferD, sampler_linear, constants.grab_start, 0.0).r;");
node_shader_write_frag(kong, "var grab_anchor4: float4 = constants.invVP * float4(grab_ndc, grab_adepth, 1.0);");
node_shader_write_frag(kong, "var grab_anchor: float3 = grab_anchor4.xyz / grab_anchor4.w;");
node_shader_write_frag(kong, "var grab_near4: float4 = constants.invVP * float4(grab_ndc, 0.0, 1.0);");
node_shader_write_frag(kong, "var grab_far4: float4 = constants.invVP * float4(grab_ndc, 1.0, 1.0);");
node_shader_write_frag(kong, "var grab_plane_n: float3 = normalize(grab_far4.xyz / grab_far4.w - grab_near4.xyz / grab_near4.w);");
// Intersect the cursor ray with the grab plane
node_shader_write_frag(kong, "var winp_ndc: float2 = float2(constants.inp.x, 1.0 - constants.inp.y) * 2.0 - 1.0;");
node_shader_write_frag(kong, "var winp_o4: float4 = constants.invVP * float4(winp_ndc, 0.0, 1.0);");
node_shader_write_frag(kong, "var winp_f4: float4 = constants.invVP * float4(winp_ndc, 1.0, 1.0);");
node_shader_write_frag(kong, "var winp_o: float3 = winp_o4.xyz / winp_o4.w;");
node_shader_write_frag(kong, "var winp_d: float3 = normalize(winp_f4.xyz / winp_f4.w - winp_o);");
node_shader_write_frag(kong,
"var winp: float4 = float4(winp_o + winp_d * (dot(grab_anchor - winp_o, grab_plane_n) / dot(winp_d, grab_plane_n)), 1.0);");
}
else {
node_shader_write_frag(kong, "var depth: float = sample_lod(gbufferD, sampler_linear, constants.inp.xy, 0.0).r;");
node_shader_write_frag(kong, "var winp: float4 = float4(float2(constants.inp.x, 1.0 - constants.inp.y) * 2.0 - 1.0, depth, 1.0);");
node_shader_write_frag(kong, "winp = constants.invVP * winp;");
node_shader_write_frag(kong, "winp.xyz = winp.xyz / winp.w;");
}
node_shader_add_constant(kong, "W: float4x4", "_world_matrix");
node_shader_write_attrib_frag(kong, "var read_undo: float4 = texpaint_sculpt_undo[uint2(uint(tex_coord.x * constants.texpaint_undo_size.x), "
"uint(tex_coord.y * constants.texpaint_undo_size.y))];");
node_shader_write_attrib_frag(kong, "var wposition: float3 = (constants.W * float4(read_undo.xyz, 1.0)).xyz;");
node_shader_write_frag(kong, "var depthlast: float = sample_lod(gbufferD, sampler_linear, constants.inplast.xy, 0.0).r;");
node_shader_write_frag(kong, "var winplast: float4 = float4(float2(constants.inplast.x, 1.0 - constants.inplast.y) * 2.0 - 1.0, depthlast, 1.0);");
node_shader_write_frag(kong, "winplast = constants.invVP * winplast;");
node_shader_write_frag(kong, "winplast.xyz = winplast.xyz / winplast.w;");
if (grab) {
node_shader_write_frag(kong, "var winpl_ndc: float2 = float2(constants.inplast.x, 1.0 - constants.inplast.y) * 2.0 - 1.0;");
node_shader_write_frag(kong, "var winpl_o4: float4 = constants.invVP * float4(winpl_ndc, 0.0, 1.0);");
node_shader_write_frag(kong, "var winpl_f4: float4 = constants.invVP * float4(winpl_ndc, 1.0, 1.0);");
node_shader_write_frag(kong, "var winpl_o: float3 = winpl_o4.xyz / winpl_o4.w;");
node_shader_write_frag(kong, "var winpl_d: float3 = normalize(winpl_f4.xyz / winpl_f4.w - winpl_o);");
node_shader_write_frag(
kong, "var winplast: float4 = float4(winpl_o + winpl_d * (dot(grab_anchor - winpl_o, grab_plane_n) / dot(winpl_d, grab_plane_n)), 1.0);");
}
else {
node_shader_write_frag(kong, "var depthlast: float = sample_lod(gbufferD, sampler_linear, constants.inplast.xy, 0.0).r;");
node_shader_write_frag(kong, "var winplast: float4 = float4(float2(constants.inplast.x, 1.0 - constants.inplast.y) * 2.0 - 1.0, depthlast, 1.0);");
node_shader_write_frag(kong, "winplast = constants.invVP * winplast;");
node_shader_write_frag(kong, "winplast.xyz = winplast.xyz / winplast.w;");
}
if (particle) {
node_shader_add_constant(kong, "particle_hit: float3", "_particle_hit");
@@ -616,6 +649,10 @@ node_shader_context_t *sculpt_make_sculpt_run(material_t *data, material_context
else if (g_context->tool == TOOL_TYPE_ERASER) {
node_shader_write_frag(kong, "output[0] = float4(sample_undo.rgb - n * disp * str, raw_undo.a);");
}
else if (grab) {
node_shader_write_frag(kong, "var grab_delta: float3 = (winp.xyz - winplast.xyz) * falloff * opacity;");
node_shader_write_frag(kong, "output[0] = float4(sample_undo.rgb + grab_delta, raw_undo.a);");
}
else {
node_shader_write_frag(kong, string("var sculpt_disp: float = %s;", sculpt_blend_mode(kong, g_context->brush_blending, "0.0", "disp.x", "str")));
node_shader_write_frag(kong, "output[0] = float4(sample_undo.rgb + n * sculpt_disp, raw_undo.a);");
@@ -1043,7 +1080,6 @@ void render_path_sculpt_commands() {
}
void render_path_sculpt_snapshot_gbuffer() {
// Freeze the current surface (depth + packed normal) as the reference the displacement pass reads
render_path_set_target("gbuffer0_undo", NULL, NULL, GPU_CLEAR_NONE, 0, 0.0);
render_path_bind_target("gbuffer0", "tex");
render_path_draw_shader("Scene/copy_pass/copyRGBA64_pass");
@@ -1060,6 +1096,8 @@ void render_path_sculpt_begin() {
if (history_push_undo && history_undo_layers != NULL) {
history_paint();
render_path_sculpt_snapshot_gbuffer();
g_context->grab_start_x = g_context->paint_vec.x;
g_context->grab_start_y = g_context->paint_vec.y;
}
sculpt_push_undo = false;
}
+3
View File
@@ -239,6 +239,8 @@ typedef struct context {
f32 clone_start_y;
f32 clone_delta_x;
f32 clone_delta_y;
f32 grab_start_x;
f32 grab_start_y;
bool show_compass;
i32 project_type;
f32 last_paint_vec_x;
@@ -362,6 +364,7 @@ typedef struct context {
f32 brush_lazy_x;
f32 brush_lazy_y;
uv_type_t brush_paint;
sculpt_type_t brush_sculpt;
f32 brush_angle_reject_dot;
bake_type_t bake_type;
bake_axis_t bake_axis;
+16 -2
View File
@@ -384,7 +384,7 @@ void ui_header_draw_tool_properties() {
g_context->brush_hardness = ui_slider(h, tr("Hardness"), 0.0, 1.0, true, 100.0, true, UI_ALIGN_RIGHT, true);
}
if (g_context->tool != TOOL_TYPE_ERASER) {
if (g_context->tool != TOOL_TYPE_ERASER && g_config->workflow != WORKFLOW_SCULPT) {
ui_handle_t *brush_blending_handle = ui_handle(__ID__);
brush_blending_handle->i = g_context->brush_blending;
string_array_t *brush_blending_combo = any_array_create_from_raw(
@@ -415,7 +415,7 @@ void ui_header_draw_tool_properties() {
}
}
if (g_context->tool == TOOL_TYPE_BRUSH || g_context->tool == TOOL_TYPE_FILL) {
if ((g_context->tool == TOOL_TYPE_BRUSH || g_context->tool == TOOL_TYPE_FILL) && g_config->workflow != WORKFLOW_SCULPT) {
ui_handle_t *paint_handle = ui_handle(__ID__);
string_array_t *texcoord_combo = any_array_create_from_raw(
(void *[]){
@@ -430,6 +430,20 @@ void ui_header_draw_tool_properties() {
}
}
if (g_context->tool == TOOL_TYPE_BRUSH && g_config->workflow == WORKFLOW_SCULPT) {
ui_handle_t *sculpt_handle = ui_handle(__ID__);
string_array_t *mode_combo = any_array_create_from_raw(
(void *[]){
tr("Draw"),
tr("Grab"),
},
2);
g_context->brush_sculpt = ui_combo(sculpt_handle, mode_combo, tr("Mode"), false, UI_ALIGN_LEFT, true);
if (sculpt_handle->changed) {
make_material_parse_paint_material(true);
}
}
if (g_context->tool == TOOL_TYPE_TEXT) {
ui_handle_t *h = ui_handle(__ID__);
h->text = string_copy(g_context->text_tool_text);
+11 -8
View File
@@ -173,6 +173,9 @@ vec2_t uniforms_ext_vec2_link(object_t *object, material_data_t *mat, char *link
else if (string_equals(link, "_clone_delta")) {
return (vec2_t){g_context->clone_delta_x, g_context->clone_delta_y};
}
else if (string_equals(link, "_grab_start")) {
return (vec2_t){g_context->grab_start_x, g_context->grab_start_y};
}
else if (string_equals(link, "_texpaint_size")) {
return (vec2_t){config_get_texture_res_x(), config_get_texture_res_y()};
}
@@ -202,10 +205,10 @@ vec4_t uniforms_ext_vec3_link(object_t *object, material_data_t *mat, char *link
// Discard first paint for directional brush (no prev position yet)
bool allow_paint = g_context->prev_paint_vec_x > 0 && g_context->prev_paint_vec_y > 0 &&
(g_context->prev_paint_vec_x != g_context->paint_vec.x || g_context->prev_paint_vec_y != g_context->paint_vec.y);
f32 x = g_context->paint_vec.x;
f32 y = g_context->paint_vec.y;
f32 lastx = g_context->prev_paint_vec_x;
f32 lasty = g_context->prev_paint_vec_y;
f32 x = g_context->paint_vec.x;
f32 y = g_context->paint_vec.y;
f32 lastx = g_context->prev_paint_vec_x;
f32 lasty = g_context->prev_paint_vec_y;
if (g_context->paint2d) {
x = uniforms_ext_vec2d(x);
lastx = uniforms_ext_vec2d(lastx);
@@ -311,10 +314,10 @@ mat4_t uniforms_ext_mat4_link(object_t *object, material_data_t *mat, char *link
transform_t *t = object->transform;
mat4_t W = t->world;
vec4_t axes[3] = {
vec4_norm((vec4_t){W.m00, W.m10, W.m20, 0.0}),
vec4_norm((vec4_t){W.m01, W.m11, W.m21, 0.0}),
vec4_norm((vec4_t){W.m02, W.m12, W.m22, 0.0}),
};
vec4_norm((vec4_t){W.m00, W.m10, W.m20, 0.0}),
vec4_norm((vec4_t){W.m01, W.m11, W.m21, 0.0}),
vec4_norm((vec4_t){W.m02, W.m12, W.m22, 0.0}),
};
f32 scale[3] = {t->scale.x, t->scale.y, t->scale.z};
mat4_t F = mat4_identity();
for (i32 i = 0; i < 3; ++i) {