paint: add meshes to timeline

This commit is contained in:
luboslenco
2026-06-01 22:06:40 +02:00
parent dcc7ad0e3e
commit 680e6791d4
+338 -8
View File
@@ -16,6 +16,18 @@ typedef struct {
gpu_texture_t *texpaint_pack;
} tab_timeline_origin_t;
typedef struct {
i32 frame;
i32 mesh_index;
mat4_t transform;
bool tween;
} tab_timeline_mesh_keyframe_t;
typedef struct {
i32 mesh_index;
mat4_t transform;
} tab_timeline_mesh_origin_t;
i32 tab_timeline_selected_frame = 0;
i32 tab_timeline_selected_row = 0;
bool tab_timeline_playing = false;
@@ -31,6 +43,9 @@ static any_array_t *tab_timeline_keyframes = NULL;
static any_array_t *tab_timeline_origins = NULL;
static i32 tab_timeline_last_frame = 0;
static any_array_t *tab_timeline_mesh_keyframes = NULL;
static any_array_t *tab_timeline_mesh_origins = NULL;
static i32 tab_timeline_pending_from = -1;
static i32 tab_timeline_pending_to = -1;
static i32 tab_timeline_pending_kf_frame = -1;
@@ -38,6 +53,11 @@ static i32 tab_timeline_pending_kf_layer = -1;
static i32 tab_timeline_pending_rm_frame = -1;
static i32 tab_timeline_pending_rm_layer = -1;
static i32 tab_timeline_pending_mesh_add_frame = -1;
static i32 tab_timeline_pending_mesh_add_index = -1;
static i32 tab_timeline_pending_mesh_rm_frame = -1;
static i32 tab_timeline_pending_mesh_rm_index = -1;
static void tab_timeline_copy_tex(gpu_texture_t *dst, gpu_texture_t *src) {
draw_begin(dst, true, 0x00000000);
draw_set_pipeline(pipes_copy);
@@ -178,6 +198,179 @@ static void tab_timeline_load_from_keyframes(i32 frame) {
}
}
static i32 tab_timeline_find_mesh_keyframe(i32 frame, i32 mesh_index) {
for (i32 i = 0; i < tab_timeline_mesh_keyframes->length; i++) {
tab_timeline_mesh_keyframe_t *kf = tab_timeline_mesh_keyframes->buffer[i];
if (kf->frame == frame && kf->mesh_index == mesh_index) {
return i;
}
}
return -1;
}
static i32 tab_timeline_find_active_mesh_keyframe(i32 frame, i32 mesh_index) {
i32 best = -1;
i32 best_frame = -1;
for (i32 i = 0; i < tab_timeline_mesh_keyframes->length; i++) {
tab_timeline_mesh_keyframe_t *kf = tab_timeline_mesh_keyframes->buffer[i];
if (kf->mesh_index == mesh_index && kf->frame <= frame && kf->frame > best_frame) {
best_frame = kf->frame;
best = i;
}
}
return best;
}
static i32 tab_timeline_find_mesh_origin(i32 mesh_index) {
for (i32 i = 0; i < tab_timeline_mesh_origins->length; i++) {
if (((tab_timeline_mesh_origin_t *)tab_timeline_mesh_origins->buffer[i])->mesh_index == mesh_index) {
return i;
}
}
return -1;
}
static i32 tab_timeline_find_next_mesh_keyframe(i32 frame, i32 mesh_index) {
i32 best = -1;
i32 best_frame = tab_timeline_max_frames + 1;
for (i32 i = 0; i < tab_timeline_mesh_keyframes->length; i++) {
tab_timeline_mesh_keyframe_t *kf = tab_timeline_mesh_keyframes->buffer[i];
if (kf->mesh_index == mesh_index && kf->frame > frame && kf->frame < best_frame) {
best_frame = kf->frame;
best = i;
}
}
return best;
}
static quat_t tab_timeline_quat_slerp(quat_t a, quat_t b, float t) {
float dot = quat_dot(a, b);
if (dot < 0.0f) {
b.x = -b.x; b.y = -b.y; b.z = -b.z; b.w = -b.w;
dot = -dot;
}
if (dot > 0.9995f) {
quat_t r = {a.x + t * (b.x - a.x), a.y + t * (b.y - a.y), a.z + t * (b.z - a.z), a.w + t * (b.w - a.w)};
return quat_norm(r);
}
float theta_0 = acosf(dot);
float theta = theta_0 * t;
float sin_t = sinf(theta);
float sin_t0 = sinf(theta_0);
float s0 = cosf(theta) - dot * sin_t / sin_t0;
float s1 = sin_t / sin_t0;
return (quat_t){s0 * a.x + s1 * b.x, s0 * a.y + s1 * b.y, s0 * a.z + s1 * b.z, s0 * a.w + s1 * b.w};
}
static mat4_t tab_timeline_mat4_tween(mat4_t a, mat4_t b, float t) {
mat4_decomposed_t *da = mat4_decompose(a);
mat4_decomposed_t *db = mat4_decompose(b);
vec4_t loc = {da->loc.x + t * (db->loc.x - da->loc.x), da->loc.y + t * (db->loc.y - da->loc.y),
da->loc.z + t * (db->loc.z - da->loc.z), 1.0f};
vec4_t scl = {da->scl.x + t * (db->scl.x - da->scl.x), da->scl.y + t * (db->scl.y - da->scl.y),
da->scl.z + t * (db->scl.z - da->scl.z), 1.0f};
quat_t rot = tab_timeline_quat_slerp(da->rot, db->rot, t);
return mat4_compose(loc, rot, scl);
}
static void tab_timeline_save_mesh_origins() {
for (i32 mi = 0; mi < project_paint_objects->length; mi++) {
mesh_object_t *o = project_paint_objects->buffer[mi];
i32 oi = tab_timeline_find_mesh_origin(mi);
tab_timeline_mesh_origin_t *orig;
if (oi < 0) {
orig = GC_ALLOC_INIT(tab_timeline_mesh_origin_t, {0});
orig->mesh_index = mi;
any_array_push(tab_timeline_mesh_origins, orig);
}
else {
orig = tab_timeline_mesh_origins->buffer[oi];
}
orig->transform = o->base->transform->local;
}
}
static void tab_timeline_load_mesh_origins() {
for (i32 mi = 0; mi < project_paint_objects->length; mi++) {
i32 oi = tab_timeline_find_mesh_origin(mi);
if (oi < 0) {
continue;
}
tab_timeline_mesh_origin_t *orig = tab_timeline_mesh_origins->buffer[oi];
mesh_object_t *o = project_paint_objects->buffer[mi];
transform_set_matrix(o->base->transform, orig->transform);
}
g_context->ddirty = 2;
}
static void tab_timeline_save_mesh_to_keyframes(i32 frame) {
for (i32 mi = 0; mi < project_paint_objects->length; mi++) {
i32 kfi = tab_timeline_find_mesh_keyframe(frame, mi);
if (kfi < 0) {
continue;
}
tab_timeline_mesh_keyframe_t *kf = tab_timeline_mesh_keyframes->buffer[kfi];
mesh_object_t *o = project_paint_objects->buffer[mi];
kf->transform = o->base->transform->local;
}
}
static void tab_timeline_load_mesh_from_keyframes(float frame_f) {
bool any = false;
i32 frame_i = (i32)frame_f;
for (i32 mi = 0; mi < project_paint_objects->length; mi++) {
mesh_object_t *o = project_paint_objects->buffer[mi];
i32 act_kfi = tab_timeline_find_active_mesh_keyframe(frame_i, mi);
i32 nxt_kfi = tab_timeline_find_next_mesh_keyframe(frame_i, mi);
if (nxt_kfi >= 0) {
tab_timeline_mesh_keyframe_t *nxt = tab_timeline_mesh_keyframes->buffer[nxt_kfi];
if (nxt->tween) {
mat4_t from_mat;
i32 from_frame;
bool has_from = false;
if (act_kfi >= 0) {
tab_timeline_mesh_keyframe_t *act = tab_timeline_mesh_keyframes->buffer[act_kfi];
from_mat = act->transform;
from_frame = act->frame;
has_from = true;
}
else {
i32 oi = tab_timeline_find_mesh_origin(mi);
if (oi >= 0) {
from_mat = ((tab_timeline_mesh_origin_t *)tab_timeline_mesh_origins->buffer[oi])->transform;
from_frame = 0;
has_from = true;
}
}
if (has_from) {
float t = (frame_f - (float)from_frame) / (float)(nxt->frame - from_frame);
mat4_t result = tab_timeline_mat4_tween(from_mat, nxt->transform, t);
transform_set_matrix(o->base->transform, result);
any = true;
continue;
}
}
}
if (act_kfi >= 0) {
transform_set_matrix(o->base->transform, ((tab_timeline_mesh_keyframe_t *)tab_timeline_mesh_keyframes->buffer[act_kfi])->transform);
any = true;
}
else {
i32 oi = tab_timeline_find_mesh_origin(mi);
if (oi >= 0) {
transform_set_matrix(o->base->transform, ((tab_timeline_mesh_origin_t *)tab_timeline_mesh_origins->buffer[oi])->transform);
any = true;
}
}
}
if (any) {
g_context->ddirty = 2;
}
}
static void tab_timeline_frame_change_on_next_frame(void *_) {
i32 from = tab_timeline_pending_from;
i32 to = tab_timeline_pending_to;
@@ -186,6 +379,10 @@ static void tab_timeline_frame_change_on_next_frame(void *_) {
from == 0 ? tab_timeline_save_origins() : tab_timeline_save_to_keyframes(from);
to == 0 ? tab_timeline_load_origins() : tab_timeline_load_from_keyframes(to);
if (!tab_timeline_playing) {
from == 0 ? tab_timeline_save_mesh_origins() : tab_timeline_save_mesh_to_keyframes(from);
to == 0 ? tab_timeline_load_mesh_origins() : tab_timeline_load_mesh_from_keyframes((float)to);
}
}
static void tab_timeline_add_keyframe_on_next_frame(void *_) {
@@ -239,11 +436,55 @@ static void tab_timeline_remove_keyframe_on_next_frame(void *_) {
}
}
static void tab_timeline_add_mesh_keyframe_on_next_frame(void *_) {
i32 fr = tab_timeline_pending_mesh_add_frame;
i32 mi = tab_timeline_pending_mesh_add_index;
tab_timeline_pending_mesh_add_frame = -1;
tab_timeline_pending_mesh_add_index = -1;
if (fr <= 0 || mi < 0 || mi >= project_paint_objects->length) {
return;
}
mesh_object_t *o = project_paint_objects->buffer[mi];
i32 kfi = tab_timeline_find_mesh_keyframe(fr, mi);
tab_timeline_mesh_keyframe_t *kf;
if (kfi < 0) {
kf = GC_ALLOC_INIT(tab_timeline_mesh_keyframe_t, {0});
kf->frame = fr;
kf->mesh_index = mi;
any_array_push(tab_timeline_mesh_keyframes, kf);
}
else {
kf = tab_timeline_mesh_keyframes->buffer[kfi];
}
kf->transform = o->base->transform->local;
}
static void tab_timeline_remove_mesh_keyframe_on_next_frame(void *_) {
i32 fr = tab_timeline_pending_mesh_rm_frame;
i32 mi = tab_timeline_pending_mesh_rm_index;
tab_timeline_pending_mesh_rm_frame = -1;
tab_timeline_pending_mesh_rm_index = -1;
i32 kfi = tab_timeline_find_mesh_keyframe(fr, mi);
if (kfi < 0) {
return;
}
array_remove(tab_timeline_mesh_keyframes, tab_timeline_mesh_keyframes->buffer[kfi]);
if (fr == tab_timeline_last_frame) {
tab_timeline_load_mesh_from_keyframes((float)fr);
}
}
static void tab_timeline_clear_on_next_frame(void *_) {
gc_unroot(tab_timeline_keyframes);
tab_timeline_keyframes = any_array_create_from_raw((void *[]){}, 0);
gc_root(tab_timeline_keyframes);
gc_unroot(tab_timeline_mesh_keyframes);
tab_timeline_mesh_keyframes = any_array_create_from_raw((void *[]){}, 0);
gc_root(tab_timeline_mesh_keyframes);
tab_timeline_load_origins();
tab_timeline_load_mesh_origins();
tab_timeline_last_frame = 0;
}
@@ -255,16 +496,52 @@ static void tab_timeline_init() {
gc_root(tab_timeline_keyframes);
tab_timeline_origins = any_array_create_from_raw((void *[]){}, 0);
gc_root(tab_timeline_origins);
tab_timeline_mesh_keyframes = any_array_create_from_raw((void *[]){}, 0);
gc_root(tab_timeline_mesh_keyframes);
tab_timeline_mesh_origins = any_array_create_from_raw((void *[]){}, 0);
gc_root(tab_timeline_mesh_origins);
}
void tab_timeline_draw_frame_context_menu() {
bool has_kf = tab_timeline_keyframes != NULL && tab_timeline_selected_frame > 0 &&
tab_timeline_find_keyframe(tab_timeline_selected_frame, tab_timeline_selected_row) >= 0;
i32 layer_count = project_layers->length;
bool is_mesh = tab_timeline_selected_row >= layer_count;
bool has_kf;
i32 mesh_kfi = -1;
if (!is_mesh) {
has_kf = tab_timeline_keyframes != NULL && tab_timeline_selected_frame > 0 &&
tab_timeline_find_keyframe(tab_timeline_selected_frame, tab_timeline_selected_row) >= 0;
}
else {
i32 mi = tab_timeline_selected_row - layer_count;
mesh_kfi = tab_timeline_mesh_keyframes != NULL && tab_timeline_selected_frame > 0
? tab_timeline_find_mesh_keyframe(tab_timeline_selected_frame, mi)
: -1;
has_kf = mesh_kfi >= 0;
}
if (is_mesh && mesh_kfi >= 0) {
tab_timeline_mesh_keyframe_t *kf = tab_timeline_mesh_keyframes->buffer[mesh_kfi];
ui_handle_t *h_tween = ui_handle(__ID__);
h_tween->b = kf->tween;
ui_check(h_tween, tr("Tween"), "");
if (h_tween->changed) {
kf->tween = h_tween->b;
ui_menu_keep_open = true;
}
}
ui->enabled = has_kf;
if (ui_menu_button(tr("Remove Keyframe"), "", ICON_MINUS)) {
tab_timeline_pending_rm_frame = tab_timeline_selected_frame;
tab_timeline_pending_rm_layer = tab_timeline_selected_row;
sys_notify_on_next_frame(&tab_timeline_remove_keyframe_on_next_frame, NULL);
if (!is_mesh) {
tab_timeline_pending_rm_frame = tab_timeline_selected_frame;
tab_timeline_pending_rm_layer = tab_timeline_selected_row;
sys_notify_on_next_frame(&tab_timeline_remove_keyframe_on_next_frame, NULL);
}
else {
tab_timeline_pending_mesh_rm_frame = tab_timeline_selected_frame;
tab_timeline_pending_mesh_rm_index = tab_timeline_selected_row - layer_count;
sys_notify_on_next_frame(&tab_timeline_remove_mesh_keyframe_on_next_frame, NULL);
}
}
ui->enabled = true;
}
@@ -308,6 +585,14 @@ void tab_timeline_draw(ui_handle_t *htab) {
tab_timeline_pending_kf_layer = li;
sys_notify_on_next_frame(&tab_timeline_add_keyframe_on_next_frame, NULL);
}
else if (tab_timeline_selected_frame > 0 && li >= project_layers->length) {
i32 mi = li - project_layers->length;
if (mi < project_paint_objects->length) {
tab_timeline_pending_mesh_add_frame = tab_timeline_selected_frame;
tab_timeline_pending_mesh_add_index = mi;
sys_notify_on_next_frame(&tab_timeline_add_mesh_keyframe_on_next_frame, NULL);
}
}
}
if (ui_icon_button(tr("Edit"), ICON_EDIT, UI_ALIGN_CENTER)) {
ui_menu_draw(&tab_timeline_draw_edit, -1, -1);
@@ -358,9 +643,13 @@ void tab_timeline_draw(ui_handle_t *htab) {
if (tab_timeline_playing) {
iron_delay_idle_sleep();
f64 elapsed = sys_time() - tab_timeline_play_time;
tab_timeline_selected_frame = (i32)(elapsed * tab_timeline_fps) % tab_timeline_max_frames;
f64 elapsed = sys_time() - tab_timeline_play_time;
float frame_f = (float)fmod(elapsed * tab_timeline_fps, tab_timeline_max_frames);
tab_timeline_selected_frame = (i32)frame_f;
ui_base_hwnds->buffer[TAB_AREA_STATUS]->redraws = 2;
if (tab_timeline_mesh_keyframes != NULL) {
tab_timeline_load_mesh_from_keyframes(frame_f);
}
}
// Frame number labels every 5 frames
@@ -420,9 +709,50 @@ void tab_timeline_draw(ui_handle_t *htab) {
}
}
i32 mesh_count = project_paint_objects->length;
for (i32 mi = 0; mi < mesh_count; mi++) {
mesh_object_t *mesh = project_paint_objects->buffer[mi];
i32 ri = row_count + mi;
f32 row_y = start_y + font_h + 2 + ri * strip_h;
f32 icon_y = row_y + (strip_h - icon_size) / 2.0f;
rect_t *rect = resource_tile50(icons, ICON_CUBE);
draw_set_color(ui->ops->theme->LABEL_COL);
draw_scaled_sub_image(icons, rect->x, rect->y, rect->w, rect->h, ui->_x, icon_y, icon_size, icon_size);
draw_set_color(ui->ops->theme->LABEL_COL);
draw_string(mesh->base->name, ui->_x + icon_size + 2, row_y + (strip_h - font_h) / 2.0f);
for (i32 i = tab_timeline_scroll; i < tab_timeline_scroll + visible + 1 && i < tab_timeline_max_frames; i++) {
f32 x = start_x + (i - tab_timeline_scroll) * frame_w;
bool selected = i == tab_timeline_selected_frame && ri == tab_timeline_selected_row;
u32 col = selected ? sel_col : (i % 5 == 0) ? bright_col : base_col;
draw_set_color(col);
draw_filled_rect(x, row_y, frame_w - 1, strip_h - 1);
if (i == 0 || (tab_timeline_mesh_keyframes != NULL && tab_timeline_find_mesh_keyframe(i, mi) >= 0)) {
draw_set_color(ui->ops->theme->LABEL_COL);
draw_filled_circle(x + frame_w / 2.0f, row_y + strip_h / 2.0f, 3.0f * UI_SCALE(), 12);
}
bool in_cell = !tab_timeline_scrolling && ui->input_x > ui->_window_x + x && ui->input_x < ui->_window_x + x + frame_w &&
ui->input_y > ui->_window_y + row_y && ui->input_y < ui->_window_y + row_y + strip_h;
if (in_cell && ui->input_down) {
tab_timeline_selected_frame = i;
tab_timeline_selected_row = ri;
tab_timeline_play_time = sys_time() - (f64)i / tab_timeline_fps;
}
if (in_cell && ui->input_released_r) {
tab_timeline_selected_frame = i;
tab_timeline_selected_row = ri;
ui_menu_draw(&tab_timeline_draw_frame_context_menu, -1, -1);
}
}
}
// Scrollbar
f32 scrollbar_h = 8.0f * UI_SCALE();
f32 scrollbar_y = start_y + font_h + 2 + row_count * strip_h;
f32 scrollbar_y = start_y + font_h + 2 + (row_count + mesh_count) * strip_h;
f32 handle_w = track_w * (f32)visible / tab_timeline_max_frames;
f32 handle_x = start_x + (max_scroll > 0 ? tab_timeline_scroll * (track_w - handle_w) / max_scroll : 0);