From a07d8c6d51d59a1ae77be4653823a675b70b5239 Mon Sep 17 00:00:00 2001 From: luboslenco Date: Sat, 30 May 2026 19:45:52 +0200 Subject: [PATCH] paint: add texture mesh node --- paint/sources/context.c | 2 +- paint/sources/functions.h | 1 + paint/sources/main.c | 1 + .../sources/neural_nodes/texture_mesh_node.c | 409 ++++++++++++++++++ paint/sources/nodes_material.c | 3 + paint/sources/types.h | 1 + 6 files changed, 416 insertions(+), 1 deletion(-) create mode 100644 paint/sources/neural_nodes/texture_mesh_node.c diff --git a/paint/sources/context.c b/paint/sources/context.c index a4c6061b..fbca0904 100644 --- a/paint/sources/context.c +++ b/paint/sources/context.c @@ -425,7 +425,7 @@ bool context_is_decal_mask() { } bool context_is_decal_camera_align() { - return context_is_decal() && operator_shortcut(any_map_get(config_keymap, "decal_camera_align"), SHORTCUT_TYPE_DOWN); + return context_is_decal() && (g_context->decal_camera_align || operator_shortcut(any_map_get(config_keymap, "decal_camera_align"), SHORTCUT_TYPE_DOWN)); } bool context_is_decal_mask_paint() { diff --git a/paint/sources/functions.h b/paint/sources/functions.h index f2983b7c..49d8e89c 100644 --- a/paint/sources/functions.h +++ b/paint/sources/functions.h @@ -692,6 +692,7 @@ void upscale_image_node_init(); void image_to_pbr_node_init(); void inpaint_image_node_init(); void text_to_image_node_init(); +void texture_mesh_node_init(); void text_to_text_node_clear(); void text_to_text_node_run(char *prompt, void (*done)(char *)); void outpaint_image_node_init(); diff --git a/paint/sources/main.c b/paint/sources/main.c index 576e7e09..8c3a6d6c 100644 --- a/paint/sources/main.c +++ b/paint/sources/main.c @@ -89,6 +89,7 @@ #include "neural_nodes/repeat_node.c" #include "neural_nodes/save_image_node.c" #include "neural_nodes/text_to_image_node.c" +#include "neural_nodes/texture_mesh_node.c" #include "neural_nodes/text_to_text_node.c" #include "neural_nodes/tile_image_node.c" #include "neural_nodes/upscale_image_node.c" diff --git a/paint/sources/neural_nodes/texture_mesh_node.c b/paint/sources/neural_nodes/texture_mesh_node.c new file mode 100644 index 00000000..2e7cf18a --- /dev/null +++ b/paint/sources/neural_nodes/texture_mesh_node.c @@ -0,0 +1,409 @@ + +#include "../global.h" + +#define TEXTURE_MESH_NODE_NUM_VIEWS 6 +#define TEXTURE_MESH_NODE_GRID_COLS 3 +#define TEXTURE_MESH_NODE_GRID_ROWS 2 +#define TEXTURE_MESH_NODE_CELL 512 +#define TEXTURE_MESH_NODE_GRID_W (TEXTURE_MESH_NODE_CELL * TEXTURE_MESH_NODE_GRID_COLS) +#define TEXTURE_MESH_NODE_GRID_H (TEXTURE_MESH_NODE_CELL * TEXTURE_MESH_NODE_GRID_ROWS) + +static ui_node_t *texture_mesh_node_current = NULL; +static i32 texture_mesh_node_step = 0; +static i32 texture_mesh_node_model = 0; + +static void texture_mesh_node_run_sd(ui_node_t *node); +static void texture_mesh_node_project(ui_node_t *node); +buffer_t *export_arm_rgba64_to_rgba32(buffer_t *buffer); + +static void texture_mesh_node_set_view(i32 view) { + f32 h = math_pi() / 2.0; + f32 p = math_pi(); + switch (view) { + case 0: + viewport_set_view(0, 0, -1, p, 0, p); + break; // bottom + case 1: + viewport_set_view(0, 0, 1, 0, 0, 0); + break; // top + case 2: + viewport_set_view(0, 1, 0, h, 0, p); + break; // back + case 3: + viewport_set_view(-1, 0, 0, h, 0, -h); + break; // left + case 4: + viewport_set_view(1, 0, 0, h, 0, h); + break; // right + case 5: + viewport_set_view(0, -1, 0, h, 0, 0); + break; // front + } +} + +static void texture_mesh_node_restore_camera() { + camera_reset(g_context->view_index_last); +} + +static void texture_mesh_node_grab_view(i32 view_idx) { + char *dir = neural_node_dir(); + render_target_t *rt = any_map_get(render_path_render_targets, "last"); + gpu_texture_t *tex = rt->_image; + + iron_write_png(string("%s%sinput_%d.png", dir, PATH_SEP, view_idx), export_arm_rgba64_to_rgba32(gpu_get_texture_pixels(tex)), tex->width, tex->height, 0); +} + +static void texture_mesh_node_capture(void *_) { + if (texture_mesh_node_step == 0) { + texture_mesh_node_set_view(0); + texture_mesh_node_step = 1; + sys_notify_on_next_frame(texture_mesh_node_capture, NULL); + return; + } + + i32 view = texture_mesh_node_step - 1; + texture_mesh_node_grab_view(view); + + if (texture_mesh_node_step < TEXTURE_MESH_NODE_NUM_VIEWS) { + texture_mesh_node_set_view(texture_mesh_node_step); + texture_mesh_node_step++; + sys_notify_on_next_frame(texture_mesh_node_capture, NULL); + } + else { + texture_mesh_node_restore_camera(); + texture_mesh_node_run_sd(texture_mesh_node_current); + } +} + +static void texture_mesh_node_build_grid(char *dir) { + gpu_texture_t *grid = gpu_create_render_target(TEXTURE_MESH_NODE_GRID_W, TEXTURE_MESH_NODE_GRID_H, GPU_TEXTURE_FORMAT_RGBA32); + draw_begin(grid, true, 0xff000000); + for (i32 v = 0; v < TEXTURE_MESH_NODE_NUM_VIEWS; v++) { + char *input = string("%s%sinput_%d.png", dir, PATH_SEP, v); + gpu_texture_t *tex = iron_load_texture(input); + f32 dx = (v % TEXTURE_MESH_NODE_GRID_COLS) * TEXTURE_MESH_NODE_CELL; + f32 dy = (v / TEXTURE_MESH_NODE_GRID_COLS) * TEXTURE_MESH_NODE_CELL; + draw_scaled_image(tex, dx, dy, TEXTURE_MESH_NODE_CELL, TEXTURE_MESH_NODE_CELL); + } + draw_end(); + iron_write_png(string("%s%sinput_grid.png", dir, PATH_SEP), gpu_get_texture_pixels(grid), grid->width, grid->height, 0); +} + +static void texture_mesh_node_split_grid(char *dir) { + char *grid_path = string("%s%soutput_grid.png", dir, PATH_SEP); + gpu_texture_t *grid = iron_load_texture(grid_path); + f32 cw = grid->width / (f32)TEXTURE_MESH_NODE_GRID_COLS; + f32 ch = grid->height / (f32)TEXTURE_MESH_NODE_GRID_ROWS; + for (i32 v = 0; v < TEXTURE_MESH_NODE_NUM_VIEWS; v++) { + f32 sx = (v % TEXTURE_MESH_NODE_GRID_COLS) * cw; + f32 sy = (v / TEXTURE_MESH_NODE_GRID_COLS) * ch; + gpu_texture_t *cell = gpu_create_render_target((i32)cw, (i32)ch, GPU_TEXTURE_FORMAT_RGBA32); + draw_begin(cell, false, 0); + draw_scaled_sub_image(grid, sx, sy, cw, ch, 0, 0, cw, ch); + draw_end(); + iron_write_png(string("%s%soutput_%d.png", dir, PATH_SEP, v), gpu_get_texture_pixels(cell), cell->width, cell->height, 0); + } +} + +static char *texture_mesh_node_grid_prompt(char *prompt) { + return string("%s Keep the grid layout unchanged.", prompt, TEXTURE_MESH_NODE_GRID_COLS, TEXTURE_MESH_NODE_GRID_ROWS, TEXTURE_MESH_NODE_NUM_VIEWS); +} + +static string_array_t *texture_mesh_node_flux_klein_args(char *dir, char *prompt) { + any_array_t *argv = any_array_create(0); + any_array_push(argv, string("%s/%s", dir, neural_node_sd_bin())); + any_array_push(argv, "--diffusion-model"); + any_array_push(argv, string("%s/flux-2-klein-4b-Q8_0.gguf", dir)); + any_array_push(argv, "--vae"); + any_array_push(argv, string("%s/flux_ae.safetensors", dir)); + any_array_push(argv, "--llm"); + any_array_push(argv, string("%s/Qwen3-4B-Q8_0.gguf", dir)); + any_array_push(argv, "--cfg-scale"); + any_array_push(argv, "1.0"); + any_array_push(argv, "--sampling-method"); + any_array_push(argv, "euler"); + any_array_push(argv, "--diffusion-fa"); + any_array_push(argv, "--offload-to-cpu"); + any_array_push(argv, "--steps"); + any_array_push(argv, "4"); + any_array_push(argv, "-s"); + any_array_push(argv, "-1"); + any_array_push(argv, "-W"); + any_array_push(argv, string("%d", TEXTURE_MESH_NODE_GRID_W)); + any_array_push(argv, "-H"); + any_array_push(argv, string("%d", TEXTURE_MESH_NODE_GRID_H)); + any_array_push(argv, "-r"); + any_array_push(argv, string("%s%sinput_grid.png", dir, PATH_SEP)); + any_array_push(argv, "-p"); + any_array_push(argv, texture_mesh_node_grid_prompt(prompt)); + any_array_push(argv, "-o"); + any_array_push(argv, string("%s%soutput_grid.png", dir, PATH_SEP)); + any_array_push(argv, NULL); + return argv; +} + +static string_array_t *texture_mesh_node_qwen_args(char *dir, char *prompt) { + any_array_t *argv = any_array_create(0); + any_array_push(argv, string("%s/%s", dir, neural_node_sd_bin())); + any_array_push(argv, "--diffusion-model"); + any_array_push(argv, string("%s/qwen-image-edit-2511-Q4_K_S.gguf", dir)); + any_array_push(argv, "--vae"); + any_array_push(argv, string("%s/Qwen_Image-VAE.safetensors", dir)); + any_array_push(argv, "--llm"); + any_array_push(argv, string("%s/Qwen2.5-VL-7B-Instruct-Q4_K_S.gguf", dir)); + any_array_push(argv, "--llm_vision"); + any_array_push(argv, string("%s/mmproj-F16.gguf", dir)); + any_array_push(argv, "--offload-to-cpu"); + any_array_push(argv, "--diffusion-fa"); + any_array_push(argv, "--steps"); + any_array_push(argv, "30"); + any_array_push(argv, "-s"); + any_array_push(argv, "-1"); + any_array_push(argv, "--cfg-scale"); + any_array_push(argv, "2.5"); + any_array_push(argv, "--flow-shift"); + any_array_push(argv, "3"); + any_array_push(argv, "--qwen-image-zero-cond-t"); + any_array_push(argv, "-W"); + any_array_push(argv, string("%d", TEXTURE_MESH_NODE_GRID_W / 2)); + any_array_push(argv, "-H"); + any_array_push(argv, string("%d", TEXTURE_MESH_NODE_GRID_H / 2)); + any_array_push(argv, "-p"); + any_array_push(argv, texture_mesh_node_grid_prompt(prompt)); + any_array_push(argv, "-r"); + any_array_push(argv, string("%s%sinput_grid.png", dir, PATH_SEP)); + any_array_push(argv, "-o"); + any_array_push(argv, string("%s%soutput_grid.png", dir, PATH_SEP)); + any_array_push(argv, NULL); + return argv; +} + +static void texture_mesh_node_run_upscale(char *dir) { + char *grid = string("%s%soutput_grid.png", dir, PATH_SEP); + string_array_t *argv = any_array_create_from_raw( + (void *[]){ + string("%s/%s", dir, neural_node_sd_bin()), + "-M", + "upscale", + "--upscale-model", + string("%s/RealESRGAN_x4plus.pth", dir), + "-i", + grid, + "-o", + grid, + NULL, + }, + 10); + iron_exec_async(argv->buffer[0], argv->buffer); +} + +static void texture_mesh_node_check_upscale(ui_node_t *node) { + gc_unroot(neural_node_current); + neural_node_current = node; + gc_root(neural_node_current); + iron_delay_idle_sleep(); + if (iron_exec_async_done == 1) { + sys_remove_update(texture_mesh_node_check_upscale); + texture_mesh_node_split_grid(neural_node_dir()); + texture_mesh_node_project(node); + } +} + +static void texture_mesh_node_check_sd(ui_node_t *node) { + gc_unroot(neural_node_current); + neural_node_current = node; + gc_root(neural_node_current); + iron_delay_idle_sleep(); + if (iron_exec_async_done == 1) { + sys_remove_update(texture_mesh_node_check_sd); + texture_mesh_node_run_upscale(neural_node_dir()); + sys_notify_on_update(texture_mesh_node_check_upscale, node); + } +} + +static void texture_mesh_node_run_sd(ui_node_t *node) { + char *node_name = parser_material_node_name(node, NULL); + ui_handle_t *h = ui_handle(node_name); + char *prompt = ui_nest(h, 1)->text; + char *dir = neural_node_dir(); + + if (prompt == NULL || string_equals(prompt, "")) { + prompt = "."; + } + + texture_mesh_node_build_grid(dir); + + string_array_t *argv = texture_mesh_node_model == 0 ? texture_mesh_node_qwen_args(dir, prompt) : texture_mesh_node_flux_klein_args(dir, prompt); + iron_exec_async(argv->buffer[0], argv->buffer); + sys_notify_on_update(texture_mesh_node_check_sd, node); +} + +static void texture_mesh_node_project(ui_node_t *node) { + char *dir = neural_node_dir(); + char *node_name = parser_material_node_name(node, NULL); + + // Save state + tool_type_t _tool = g_context->tool; + f32 _brush_radius = g_context->brush_radius; + f32 _brush_opacity = g_context->brush_opacity; + f32 _brush_angle = g_context->brush_angle; + f32 _brush_scale = g_context->brush_scale; + f32 _brush_scale_x = g_context->brush_scale_x; + + // Project a camera-aligned decal + g_context->tool = TOOL_TYPE_DECAL; + g_context->decal_camera_align = true; + g_context->brush_opacity = 1.0; + g_context->brush_angle = 0.0; + g_context->brush_scale = 1.0; + g_context->brush_scale_x = (f32)sys_w() / (f32)sys_h(); + g_context->brush_directional = false; + g_context->decal_x = 0.5; + g_context->decal_y = 0.5; + + // viewport_update_camera_type(CAMERA_TYPE_ORTHOGRAPHIC); + + for (i32 v = 0; v < TEXTURE_MESH_NODE_NUM_VIEWS; v++) { + char *output = string("%s%soutput_%d.png", dir, PATH_SEP, v); + if (!iron_file_exists(output)) { + continue; + } + + gpu_texture_t *sd_tex = iron_load_texture(output); + any_imap_set(neural_node_results, node->id, sd_tex); + any_map_set(data_cached_images, node_name, sd_tex); + + texture_mesh_node_set_view(v); + camera_object_build_mat(scene_camera); + render_path_base_draw_gbuffer(); + + mat4_t vp = scene_camera->vp; + vec4_t clip_c = vec4_apply_mat4((vec4_t){0.0, 0.0, 0.0, 1.0}, vp); + vec4_t cam_up = camera_object_up_world(scene_camera); + vec4_t up_clip = vec4_apply_mat4((vec4_t){cam_up.x, cam_up.y, cam_up.z, 0.0}, vp); + f32 m = math_abs(up_clip.y) > 0.00001 ? clip_c.w / up_clip.y : 1.0; + f32 scale2d = (900.0 / (f32)base_h()) * g_config->window_scale; + g_context->brush_radius = (math_abs(m) * 15.0) / (scale2d * 2.0); + + make_material_parse_paint_material(false); + + g_context->paint_vec.x = 0.5; + g_context->paint_vec.y = 0.5; + g_context->last_paint_vec_x = 0.5; + g_context->last_paint_vec_y = 0.5; + g_context->pdirty = 1; + render_path_paint_commands_paint(false); + } + + render_path_paint_dilate(true, true); + + // Restore + texture_mesh_node_restore_camera(); + camera_object_build_mat(scene_camera); + + g_context->pdirty = 0; + g_context->tool = _tool; + g_context->decal_camera_align = false; + g_context->brush_radius = _brush_radius; + g_context->brush_opacity = _brush_opacity; + g_context->brush_angle = _brush_angle; + g_context->brush_scale = _brush_scale; + g_context->brush_scale_x = _brush_scale_x; + + make_material_parse_paint_material(false); + render_path_base_draw_gbuffer(); + + g_context->rtdirty = 1; + g_context->rdirty = 2; + ui_nodes_hwnd->redraws = 2; + ui_view2d_hwnd->redraws = 2; + + texture_mesh_node_step = 0; + gc_unroot(texture_mesh_node_current); + texture_mesh_node_current = NULL; +} + +void texture_mesh_node_button(i32 node_id) { + ui_node_t *node = ui_get_node(ui_nodes_get_canvas(true)->nodes, node_id); + char *node_name = parser_material_node_name(node, NULL); + ui_handle_t *h = ui_handle(node_name); + string_array_t *models = any_array_create_from_raw( + (void *[]){ + "Qwen Image Edit", + "FLUX 2 klein", + }, + 2); + i32 model = ui_combo(ui_nest(h, 0), models, tr("Model"), false, UI_ALIGN_LEFT, true); + char *prompt = ui_text_area(ui_nest(h, 1), UI_ALIGN_LEFT, true, tr("prompt"), true); + node->buttons->buffer[0]->height = string_split(prompt, "\n")->length + 2; + + if (neural_node_button(node, models->buffer[model])) { + texture_mesh_node_model = model; + g_context->capturing_screenshot = true; + texture_mesh_node_step = 0; + + gc_unroot(texture_mesh_node_current); + texture_mesh_node_current = node; + gc_root(texture_mesh_node_current); + + sys_notify_on_next_frame(texture_mesh_node_capture, NULL); + } +} + +void texture_mesh_node_init() { + + ui_node_t *texture_mesh_node_def = + GC_ALLOC_INIT(ui_node_t, {.id = 0, + .name = _tr("Texture Mesh"), + .type = "NEURAL_TEXTURE_MESH", + .x = 0, + .y = 0, + .color = 0xff4982a0, + .inputs = any_array_create_from_raw( + (void *[]){ + GC_ALLOC_INIT(ui_node_socket_t, {.id = 0, + .node_id = 0, + .name = _tr("In"), + .type = "BOOL", + .color = 0xff6363c7, + .default_value = f32_array_create_xyz(0.0, 0.0, 0.0), + .min = 0.0, + .max = 1.0, + .precision = 100, + .display = 0}), + }, + 1), + .outputs = any_array_create_from_raw( + (void *[]){ + GC_ALLOC_INIT(ui_node_socket_t, {.id = 0, + .node_id = 0, + .name = _tr("Color"), + .type = "RGBA", + .color = 0xffc7c729, + .default_value = f32_array_create_xyzw(0.0, 0.0, 0.0, 1.0), + .min = 0.0, + .max = 1.0, + .precision = 100, + .display = 0}), + }, + 1), + .buttons = any_array_create_from_raw( + (void *[]){ + GC_ALLOC_INIT(ui_node_button_t, {.name = "texture_mesh_node_button", + .type = "CUSTOM", + .output = -1, + .default_value = f32_array_create_x(0), + .data = NULL, + .min = 0.0, + .max = 1.0, + .precision = 100, + .height = 1}), + }, + 1), + .width = 0, + .flags = 0}); + + any_array_push(nodes_material_neural, texture_mesh_node_def); + any_map_set(parser_material_node_vectors, "NEURAL_TEXTURE_MESH", neural_node_vector); + any_map_set(ui_nodes_custom_buttons, "texture_mesh_node_button", texture_mesh_node_button); +} diff --git a/paint/sources/nodes_material.c b/paint/sources/nodes_material.c index 114ff80c..5d57385c 100644 --- a/paint/sources/nodes_material.c +++ b/paint/sources/nodes_material.c @@ -96,6 +96,9 @@ void nodes_material_init() { repeat_node_init(); save_image_node_init(); text_to_image_node_init(); + if (g_config->experimental) { + texture_mesh_node_init(); + } tile_image_node_init(); upscale_image_node_init(); vary_image_node_init(); diff --git a/paint/sources/types.h b/paint/sources/types.h index a865e0a3..b8534216 100644 --- a/paint/sources/types.h +++ b/paint/sources/types.h @@ -292,6 +292,7 @@ typedef struct context { bool decal_preview; f32 decal_x; f32 decal_y; + bool decal_camera_align; bool write_icon_on_export; struct gpu_texture *text_tool_image; char *text_tool_text;