Merge sculpt

This commit is contained in:
luboslenco
2025-10-08 00:06:10 +02:00
parent 7563d8fc32
commit a1742047f7
10 changed files with 352 additions and 6 deletions
Binary file not shown.
@@ -181,6 +181,7 @@ function brush_output_paint(self: brush_output_node_t) {
if (context_raw.painted <= 1) {
context_raw.pdirty = 1;
context_raw.rdirty = 2;
sculpt_push_undo = true;
}
}
+5 -1
View File
@@ -5,7 +5,6 @@ let history_undos: i32 = 0; // Undos available
let history_redos: i32 = 0; // Redos available
let history_push_undo: bool = false; // Store undo on next paint
let history_undo_layers: slot_layer_t[] = null;
let history_push_undo2: bool = false;
function history_undo() {
if (history_undos > 0) {
@@ -668,6 +667,11 @@ function history_copy_to_undo(from_id: i32, to_id: i32, is_mask: bool) {
// render_path_draw_shader("Scene/copy_pass/copyR8_pass");
render_path_draw_shader("Scene/copy_pass/copy_pass");
}
else if (context_raw.layer.texpaint_sculpt != null) {
render_path_set_target("texpaint_sculpt_undo" + to_id);
render_path_bind_target("texpaint_sculpt" + from_id, "tex");
render_path_draw_shader("Scene/copy_pass/copyRGBA128_pass");
}
else {
let additional: string[] = ["texpaint_nor_undo" + to_id, "texpaint_pack_undo" + to_id];
render_path_set_target("texpaint_undo" + to_id, additional);
+7
View File
@@ -80,6 +80,13 @@ function make_mesh_run(data: material_t, layer_pass: i32 = 0): node_shader_conte
kong.frag_n = true;
node_shader_add_function(kong, str_pack_float_int16);
for (let i: i32 = 0; i < project_layers.length; ++i) {
let l: slot_layer_t = project_layers[i];
if (l.texpaint_sculpt != null) {
sculpt_make_mesh_run(kong, l);
}
}
if (context_raw.tool == tool_type_t.COLORID) {
texture_count++;
node_shader_add_texture(kong, "texcolorid", "_texcolorid");
+4
View File
@@ -40,6 +40,10 @@ function make_paint_color_attachments(): string[] {
}
function make_paint_run(data: material_t, matcon: material_context_t): node_shader_context_t {
if (context_raw.layer.texpaint_sculpt != null) {
return sculpt_make_sculpt_run(data, matcon);
}
let context_id: string = "paint";
let props: shader_context_t = {
name: context_id,
+301
View File
@@ -0,0 +1,301 @@
let sculpt_push_undo: bool = false;
function sculpt_import_mesh_pack_to_texture(mesh: mesh_data_t, l: slot_layer_t) {
let b: buffer_t = buffer_create(config_get_texture_res_x() * config_get_texture_res_y() * 4 * 4);
for (let i: i32 = 0; i < math_floor(mesh.index_array.length); ++i) {
let index: i32 = mesh.index_array[i];
buffer_set_f32(b, 4 * i * 4, mesh.vertex_arrays[0].values[index * 4] / 32767);
buffer_set_f32(b, 4 * i * 4 + 1 * 4, mesh.vertex_arrays[0].values[index * 4 + 1] / 32767);
buffer_set_f32(b, 4 * i * 4 + 2 * 4, mesh.vertex_arrays[0].values[index * 4 + 2] / 32767);
buffer_set_f32(b, 4 * i * 4 + 3 * 4, 1.0);
}
let imgmesh: gpu_texture_t = gpu_create_texture_from_bytes(b, config_get_texture_res_x(), config_get_texture_res_y(), tex_format_t.RGBA128);
draw_begin(l.texpaint_sculpt);
draw_set_pipeline(pipes_copy128);
draw_scaled_image(imgmesh, 0, 0, config_get_texture_res_x(), config_get_texture_res_y());
draw_set_pipeline(null);
draw_end();
}
function sculpt_make_sculpt_run(data: material_t, matcon: material_context_t): node_shader_context_t {
let context_id: string = "paint";
let props: shader_context_t = {
name: context_id,
depth_write: false,
compare_mode: "always",
cull_mode: "none",
vertex_elements: [
{
name: "pos",
data: "float2"
}
],
color_attachments: [
"RGBA128",
"R8"
]
};
let con_paint: node_shader_context_t = node_shader_context_create(data, props);
con_paint.data.color_writes_red = [true, true, true, true];
con_paint.data.color_writes_green = [true, true, true, true];
con_paint.data.color_writes_blue = [true, true, true, true];
con_paint.data.color_writes_alpha = [true, true, true, true];
let kong: node_shader_t = node_shader_context_make_kong(con_paint);
let decal: bool = context_is_decal();
node_shader_add_out(kong, "tex_coord: float2");
node_shader_write_vert(kong, "var madd: float2 = float2(0.5, 0.5);");
node_shader_write_vert(kong, "output.tex_coord = input.pos.xy * madd + madd;");
node_shader_write_vert(kong, "output.tex_coord.y = 1.0 - output.tex_coord.y;");
node_shader_write_vert(kong, "output.pos = float4(input.pos.xy, 0.0, 1.0);");
node_shader_write_attrib_frag(kong, "var tex_coord: float2 = input.tex_coord;");
node_shader_add_constant(kong, "inp: float4", "_input_brush");
node_shader_add_constant(kong, "inplast: float4", "_input_brush_last");
node_shader_add_texture(kong, "gbufferD");
kong.frag_out = "float4[2]";
node_shader_add_constant(kong, "brush_radius: float", "_brush_radius");
node_shader_add_constant(kong, "brush_opacity: float", "_brush_opacity");
node_shader_add_constant(kong, "brush_hardness: float", "_brush_hardness");
if (context_raw.tool == tool_type_t.BRUSH ||
context_raw.tool == tool_type_t.ERASER ||
context_raw.tool == tool_type_t.CLONE ||
context_raw.tool == tool_type_t.BLUR ||
context_raw.tool == tool_type_t.SMUDGE ||
context_raw.tool == tool_type_t.PARTICLE ||
decal) {
node_shader_write_frag(kong, "var dist: float = 0.0;");
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;");
node_shader_add_constant(kong, "W: float4x4", "_world_matrix");
// node_shader_add_constant(kong, "texpaint_undo_size: float2", "_size(texpaint_sculpt_undo)"); ////
// node_shader_write_attrib_frag(kong, "var wposition: float3 = (constants.W * texpaint_sculpt_undo[uint2(uint(tex_coord.x * constants.texpaint_undo_size.x), uint(tex_coord.y * constants.texpaint_undo_size.y))]).xyz;");
node_shader_write_attrib_frag(kong, "var wposition: float3 = (constants.W * texpaint_sculpt_undo[uint2(uint(tex_coord.x * 2048.0), uint(tex_coord.y * 2048.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;");
node_shader_write_frag(kong, "dist = distance(wposition.xyz, winp.xyz);");
node_shader_write_frag(kong, "if (dist > constants.brush_radius) { discard; }");
}
node_shader_write_frag(kong, "var basecol: float3 = float3(1.0, 1.0, 1.0);");
node_shader_write_frag(kong, "var opacity: float = 1.0;");
node_shader_write_frag(kong, "if (opacity == 0.0) { discard; }");
node_shader_write_frag(kong, "var str: float = clamp((constants.brush_radius - dist) * constants.brush_hardness * 1.0, 0.0, 1.0) * opacity;");
node_shader_add_texture(kong, "texpaint_sculpt_undo", "_texpaint_sculpt_undo");
node_shader_write_frag(kong, "var sample_undo: float4 = sample_lod(texpaint_sculpt_undo, sampler_linear, tex_coord, 0.0);");
node_shader_write_frag(kong, "if (sample_undo.r == 0.0 && sample_undo.g == 0.0 && sample_undo.b == 0.0) { discard; }");
node_shader_add_function(kong, str_octahedron_wrap);
node_shader_add_texture(kong, "gbuffer0_undo");
node_shader_write_frag(kong, "var g0_undo: float2 = sample_lod(gbuffer0_undo, sampler_linear, constants.inp.xy, 0.0).rg;");
node_shader_write_frag(kong, "var wn: float3;");
node_shader_write_frag(kong, "wn.z = 1.0 - abs(g0_undo.x) - abs(g0_undo.y);");
// node_shader_write_frag(kong, "wn.xy = wn.z >= 0.0 ? g0_undo.xy : octahedron_wrap(g0_undo.xy);");
node_shader_write_frag(kong, "if (wn.z >= 0.0) { wn.xy = g0_undo.xy; } else { wn.xy = octahedron_wrap(g0_undo.xy); }");
node_shader_write_frag(kong, "var n: float3 = normalize(wn);");
node_shader_write_frag(kong, "output[0] = float4(sample_undo.rgb + n * 0.1 * str, 1.0);");
node_shader_write_frag(kong, "output[1] = float4(str, 0.0, 0.0, 1.0);");
parser_material_finalize(con_paint);
con_paint.data.shader_from_source = true;
gpu_create_shaders_from_kong(node_shader_get(kong), ADDRESS(con_paint.data.vertex_shader), ADDRESS(con_paint.data.fragment_shader), ADDRESS(con_paint.data._.vertex_shader_size), ADDRESS(con_paint.data._.fragment_shader_size));
return con_paint;
}
function sculpt_make_mesh_run(kong: node_shader_t, l: slot_layer_t) {
node_shader_add_constant(kong, "WVP: float4x4", "_world_view_proj_matrix");
let lid: i32 = l.id;
node_shader_add_texture(kong, "texpaint_sculpt", "_texpaint_sculpt" + lid);
node_shader_add_constant(kong, "texpaint_sculpt_size: float2", "_size(_texpaint_sculpt" + lid + ")");
// node_shader_write_vert(kong, "var meshpos: float3 = sample_lod(texpaint_sculpt, sampler_linear, uint2(vertex_id() % constants.texpaint_sculpt_size.x, vertex_id() / constants.texpaint_sculpt_size.y), 0.0).xyz;");
node_shader_write_vert(kong, "var meshpos: float4 = texpaint_sculpt[uint2(uint(float(vertex_id()) % constants.texpaint_sculpt_size.x), uint(float(vertex_id()) / constants.texpaint_sculpt_size.y))];");
// + input.pos.xyz * 0.000001
node_shader_write_vert(kong, "output.pos = constants.WVP * float4(meshpos.xyz, 1.0);");
kong.frag_n = false;
node_shader_add_constant(kong, "N: float3x3", "_normal_matrix");
node_shader_add_out(kong, "wnormal: float3");
node_shader_write_attrib_vert(kong, "var base_vertex0: float = float(vertex_id()) - (float(vertex_id()) % float(3));");
node_shader_write_attrib_vert(kong, "var base_vertex1: float = base_vertex0 + 1.0;");
node_shader_write_attrib_vert(kong, "var base_vertex2: float = base_vertex0 + 2.0;");
// node_shader_write_attrib_vert(kong, "var meshpos0: float3 = sample_lod(texpaint_sculpt, sampler_linear, uint2(base_vertex0 % constants.texpaint_sculpt_size.x, base_vertex0 / constants.texpaint_sculpt_size.y), 0.0).xyz;");
// node_shader_write_attrib_vert(kong, "var meshpos1: float3 = sample_lod(texpaint_sculpt, sampler_linear, uint2(base_vertex1 % constants.texpaint_sculpt_size.x, base_vertex1 / constants.texpaint_sculpt_size.y), 0.0).xyz;");
// node_shader_write_attrib_vert(kong, "var meshpos2: float3 = sample_lod(texpaint_sculpt, sampler_linear, uint2(base_vertex2 % constants.texpaint_sculpt_size.x, base_vertex2 / constants.texpaint_sculpt_size.y), 0.0).xyz;");
node_shader_write_attrib_vert(kong, "var meshpos0: float4 = texpaint_sculpt[uint2(uint(base_vertex0 % constants.texpaint_sculpt_size.x), uint(base_vertex0 / constants.texpaint_sculpt_size.y))];");
node_shader_write_attrib_vert(kong, "var meshpos1: float4 = texpaint_sculpt[uint2(uint(base_vertex1 % constants.texpaint_sculpt_size.x), uint(base_vertex1 / constants.texpaint_sculpt_size.y))];");
node_shader_write_attrib_vert(kong, "var meshpos2: float4 = texpaint_sculpt[uint2(uint(base_vertex2 % constants.texpaint_sculpt_size.x), uint(base_vertex2 / constants.texpaint_sculpt_size.y))];");
node_shader_write_attrib_vert(kong, "var meshnor: float3 = normalize(cross(meshpos2.xyz - meshpos1.xyz, meshpos0.xyz - meshpos1.xyz));");
node_shader_write_attrib_vert(kong, "output.wnormal = constants.N * meshnor;");
node_shader_write_attrib_frag(kong, "var n: float3 = normalize(input.wnormal);");
}
function sculpt_layers_create_sculpt_layer() {
let l: slot_layer_t = layers_new_layer();
let id: i32 = l.id;
{
let t: render_target_t = render_target_create();
t.name = "texpaint_sculpt" + id;
t.width = config_get_texture_res_x();
t.height = config_get_texture_res_y();
t.format = "RGBA128";
l.texpaint_sculpt = render_path_create_render_target(t)._image;
}
// util_mesh_merge();
// let md: mesh_data_t = context_raw.merged_object.data;
let md: mesh_data_t = context_raw.paint_object.data;
sculpt_import_mesh_pack_to_texture(md, l);
let posa: i16_array_t = i16_array_create(md.index_array.length * 4);
let nora: i16_array_t = i16_array_create(md.index_array.length * 2);
let texa: i16_array_t = i16_array_create(md.index_array.length * 2);
for (let i: i32 = 0; i < posa.length; ++i) {
posa[i] = 32767;
}
for (let i: i32 = 0; i < md.index_array.length; ++i) {
let index: i32 = md.index_array[i];
posa[i * 4 ] = md.vertex_arrays[0].values[index * 4 ];
posa[i * 4 + 1] = md.vertex_arrays[0].values[index * 4 + 1];
posa[i * 4 + 2] = md.vertex_arrays[0].values[index * 4 + 2];
posa[i * 4 + 3] = md.vertex_arrays[0].values[index * 4 + 3];
nora[i * 2 ] = md.vertex_arrays[1].values[index * 2 ];
nora[i * 2 + 1] = md.vertex_arrays[1].values[index * 2 + 1];
texa[i * 2 ] = md.vertex_arrays[2].values[index * 2 ];
texa[i * 2 + 1] = md.vertex_arrays[2].values[index * 2 + 1];
}
let inda: u32_array_t = u32_array_create(md.index_array.length);
for (let i: i32 = 0; i < inda.length; ++i) {
inda[i] = i;
}
let raw: mesh_data_t = {
name: md.name,
vertex_arrays: [
{
values: posa,
attrib: "pos",
data: "short4norm"
},
{
values: nora,
attrib: "nor",
data: "short2norm"
},
{
values: texa,
attrib: "tex",
data: "short2norm"
}
],
index_array: inda,
scale_pos: 1.0,
scale_tex: 1.0
};
md = mesh_data_create(raw);
mesh_object_set_data(context_raw.paint_object, md);
make_material_parse_paint_material();
make_material_parse_mesh_material();
{
let t: render_target_t = render_target_create();
t.name = "gbuffer0_undo";
t.width = 0;
t.height = 0;
t.format = "RGBA64";
t.scale = render_path_base_get_super_sampling();
render_path_create_render_target(t);
}
{
let t: render_target_t = render_target_create();
t.name = "gbufferD_undo";
t.width = 0;
t.height = 0;
t.format = "R32";
t.scale = render_path_base_get_super_sampling();
render_path_create_render_target(t);
}
render_path_load_shader("Scene/copy_pass/copyR32_pass");
for (let i: i32 = 0; i < history_undo_layers.length; ++i) {
let len: i32 = history_undo_layers.length;
let ext: string = "_undo" + i;
let l: slot_layer_t = history_undo_layers[i];
{
let t: render_target_t = render_target_create();
t.name = "texpaint_sculpt" + ext;
t.width = config_get_texture_res_x();
t.height = config_get_texture_res_y();
t.format = "RGBA128";
l.texpaint_sculpt = render_path_create_render_target(t)._image;
}
}
}
function render_path_sculpt_commands() {
if (context_raw.pdirty <= 0) {
return;
}
let tid: i32 = context_raw.layer.id;
let texpaint_sculpt: string = "texpaint_sculpt" + tid;
render_path_set_target("texpaint_blend1");
render_path_bind_target("texpaint_blend0", "tex");
render_path_draw_shader("Scene/copy_pass/copyR8_pass");
let additional: string[] = ["texpaint_blend0"];
render_path_set_target(texpaint_sculpt, additional);
render_path_bind_target("gbufferD_undo", "gbufferD");
if (context_raw.xray || config_raw.brush_angle_reject) {
render_path_bind_target("gbuffer0", "gbuffer0");
}
render_path_bind_target("texpaint_blend1", "paintmask");
render_path_bind_target("gbuffer0_undo", "gbuffer0_undo");
let material_context: material_context_t = null;
let shader_context: shader_context_t = null;
let mat: material_data_t = project_paint_objects[0].material;
for (let j: i32 = 0; j < mat.contexts.length; ++j) {
if (mat.contexts[j].name == "paint") {
shader_context = shader_data_get_context(mat._.shader, "paint");
material_context = mat.contexts[j];
break;
}
}
gpu_set_pipeline(shader_context._.pipe);
uniforms_set_context_consts(shader_context, _render_path_bind_params);
uniforms_set_obj_consts(shader_context, project_paint_objects[0].base);
uniforms_set_material_consts(shader_context, material_context);
gpu_set_vertex_buffer(const_data_screen_aligned_vb);
gpu_set_index_buffer(const_data_screen_aligned_ib);
gpu_draw();
render_path_end();
}
function render_path_sculpt_begin() {
if (!render_path_paint_paint_enabled()) {
return;
}
render_path_paint_push_undo_last = history_push_undo;
if (history_push_undo && history_undo_layers != null) {
history_paint();
render_path_set_target("gbuffer0_undo");
render_path_bind_target("gbuffer0", "tex");
render_path_draw_shader("Scene/copy_pass/copyRGBA64_pass");
render_path_set_target("gbufferD_undo");
render_path_bind_target("main", "tex");
render_path_draw_shader("Scene/copy_pass/copyR32_pass");
}
if (sculpt_push_undo && history_undo_layers != null) {
history_paint();
}
}
+15 -1
View File
@@ -116,6 +116,11 @@ function render_path_paint_draw_fullscreen_triangle(context: string) {
function render_path_paint_commands_paint(dilation: bool = true) {
let tid: i32 = context_raw.layer.id;
if (context_raw.layer.texpaint_sculpt != null) {
render_path_sculpt_commands();
return;
}
if (context_raw.pdirty > 0) {
if (context_raw.tool == tool_type_t.COLORID) {
render_path_set_target("texpaint_colorid", null, null, clear_flag_t.COLOR, 0xff000000);
@@ -514,6 +519,11 @@ function render_path_paint_live_brush_dirty() {
}
function render_path_paint_begin() {
if (context_raw.layer.texpaint_sculpt != null) {
render_path_sculpt_begin();
return;
}
if (!render_path_paint_dilated) {
render_path_paint_dilate(false, true);
render_path_paint_dilated = true;
@@ -796,6 +806,10 @@ function render_path_paint_bind_layers() {
render_path_bind_target("texpaint_nor" + l.id, "texpaint_nor" + l.id);
render_path_bind_target("texpaint_pack" + l.id, "texpaint_pack" + l.id);
}
if (l.texpaint_sculpt != null) {
render_path_bind_target("texpaint_sculpt" + l.id, "texpaint_sculpt" + l.id);
}
}
}
@@ -815,7 +829,7 @@ function render_path_paint_dilate(base: bool, nor_pack: bool) {
let format: string = base_bits_handle.i == texture_bits_t.BITS8 ? "RGBA32" :
base_bits_handle.i == texture_bits_t.BITS16 ? "RGBA64" :
"RGBA128";
"RGBA128";
let copy_pass: string = format == "RGBA64" ? "copyRGBA64_pass" :
format == "RGBA128" ? "copyRGBA128_pass" :
+5 -4
View File
@@ -29,6 +29,7 @@ type slot_layer_t = {
paint_emis?: bool;
paint_subs?: bool;
decal_mat?: mat4_t; // Decal layer
texpaint_sculpt?: gpu_texture_t;
};
function slot_layer_create(ext: string = "", type: layer_slot_type_t = layer_slot_type_t.LAYER, parent: slot_layer_t = null): slot_layer_t {
@@ -78,7 +79,7 @@ function slot_layer_create(ext: string = "", type: layer_slot_type_t = layer_slo
raw.name = "Layer " + id;
let format: string = base_bits_handle.i == texture_bits_t.BITS8 ? "RGBA32" :
base_bits_handle.i == texture_bits_t.BITS16 ? "RGBA64" :
"RGBA128";
"RGBA128";
{
let t: render_target_t = render_target_create();
@@ -361,7 +362,7 @@ function slot_layer_resize_and_set_bits(raw: slot_layer_t) {
let format: tex_format_t =
base_bits_handle.i == texture_bits_t.BITS8 ? tex_format_t.RGBA32 :
base_bits_handle.i == texture_bits_t.BITS16 ? tex_format_t.RGBA64 :
tex_format_t.RGBA128;
tex_format_t.RGBA128;
let pipe: gpu_pipeline_t = format == tex_format_t.RGBA32 ? pipes_copy :
format == tex_format_t.RGBA64 ? pipes_copy64 :
@@ -871,7 +872,7 @@ function layers_make_temp_img() {
if (layers_temp_image == null) {
let format: string = base_bits_handle.i == texture_bits_t.BITS8 ? "RGBA32" :
base_bits_handle.i == texture_bits_t.BITS16 ? "RGBA64" :
"RGBA128";
"RGBA128";
let t: render_target_t = render_target_create();
t.name = "temptex0";
@@ -914,7 +915,7 @@ function layers_make_export_img() {
if (layers_expa == null) {
let format: string = base_bits_handle.i == texture_bits_t.BITS8 ? "RGBA32" :
base_bits_handle.i == texture_bits_t.BITS16 ? "RGBA64" :
"RGBA128";
"RGBA128";
{
+5
View File
@@ -94,6 +94,11 @@ function tab_layers_button_new(text: string) {
if (ui_menu_button(tr("Decal Layer"))) {
layers_create_fill_layer(uv_type_t.PROJECT);
}
// if (ui_menu_button(tr("Sculpt Layer"))) {
// sys_notify_on_next_frame(function () {
// sculpt_layers_create_sculpt_layer();
// });
// }
if (ui_menu_button(tr("Black Mask"))) {
if (slot_layer_is_mask(l)) {
context_set_layer(l.parent);
+9
View File
@@ -322,6 +322,11 @@ function uniforms_ext_tex_link(object: object_t, mat: material_data_t, link: str
let rt: render_target_t = map_get(render_path_render_targets, "texpaint_pack_undo" + i);
return rt._image;
}
else if (link == "_texpaint_sculpt_undo") {
let i: i32 = history_undo_i - 1 < 0 ? config_raw.undo_steps - 1 : history_undo_i - 1;
let rt: render_target_t = map_get(render_path_render_targets, "texpaint_sculpt_undo" + i);
return rt._image;
}
else if (link == "_texcolorid") {
if (project_assets.length == 0) {
let rt: render_target_t = map_get(render_path_render_targets, "empty_white");
@@ -389,6 +394,10 @@ function uniforms_ext_tex_link(object: object_t, mat: material_data_t, link: str
let tid: i32 = parse_int(substring(link, link.length - 1, link.length));
return tid < project_layers.length ? project_layers[tid].texpaint_pack : null;
}
if (starts_with(link, "_texpaint_sculpt")) {
let tid: i32 = parse_int(substring(link, link.length - 1, link.length));
return tid < project_layers.length ? project_layers[tid].texpaint_sculpt : null;
}
if (starts_with(link, "_texpaint")) {
let tid: i32 = parse_int(substring(link, link.length - 1, link.length));
return tid < project_layers.length ? project_layers[tid].texpaint : null;