Flat: Add support for FLAT_LIB_STOP_MOTION

FLAT_LIB_STOP_MOTION represents a sprite animation as a sequence of CG
frames. A stop_motion library specifies a base CG file (e.g. "foo.png");
at load time, parts_flat_load() discovers numbered frame variants
("foo_01.png", "foo_02.png", ...) and builds a frame list.

At render time, the active frame is resolved from the local timeline
clock using three parameters:
  - span: number of timeline ticks each frame is displayed
  - loop_type: 0 = play once and stop at last frame,
               1 = loop forward,
               2 = ping-pong (forward then backward)
  - local: the timeline's current local time
This commit is contained in:
kichikuou
2026-05-07 11:23:53 +09:00
parent 07bd9c7438
commit a2f087b7f9
3 changed files with 135 additions and 36 deletions
+84 -4
View File
@@ -62,10 +62,15 @@ void parts_flat_free(struct parts_flat *f)
free_string(f->name);
flat_layer_state_free(f->root_state);
if (f->textures) {
for (size_t i = 0; i < f->nr_textures; i++)
for (size_t i = 0; i < f->nr_libraries; i++)
gfx_delete_texture(&f->textures[i]);
free(f->textures);
}
if (f->stop_motion_frames) {
for (size_t i = 0; i < f->nr_libraries; i++)
free(f->stop_motion_frames[i].lib_indices);
free(f->stop_motion_frames);
}
}
int parts_flat_find_library(struct flat *fl, const char *name)
@@ -192,6 +197,75 @@ static bool flat_advance_children(struct flat *fl,
return changed;
}
// Build the CG list for a STOP_MOTION library: frame 0 is the base
// library named in `sm->library_name`; subsequent frames are numbered
// variants `${stem}_NN.${ext}`, terminated by the first missing entry.
static void build_stop_motion_frames(struct parts_flat *f, int sm_lib_idx)
{
struct flat_stop_motion *sm = &f->flat->libraries[sm_lib_idx].stop_motion;
const char *base = sm->library_name->text;
int base_idx = parts_flat_find_library(f->flat, base);
if (base_idx < 0)
return;
const char *dot = strrchr(base, '.');
if (!dot) {
WARNING("flat: invalid STOP_MOTION library name '%s'", base);
return;
}
int *indices = xmalloc(sizeof(int));
indices[0] = base_idx;
int count = 1;
char name_buf[512];
for (int i = 1; ; i++) {
snprintf(name_buf, sizeof(name_buf), "%.*s_%02d.%s",
(int)(dot - base), base, i, dot + 1);
int idx = parts_flat_find_library(f->flat, name_buf);
if (idx < 0)
break;
indices = xrealloc(indices, (count + 1) * sizeof(int));
indices[count++] = idx;
}
f->stop_motion_frames[sm_lib_idx].lib_indices = indices;
f->stop_motion_frames[sm_lib_idx].count = count;
}
static int stop_motion_frame_index(int loop_type, int span, int local, int total)
{
if (total <= 1)
return 0;
int idx = (span > 0) ? local / span : 0;
switch (loop_type) {
case 0: // stop at last frame
return min(idx, total - 1);
case 1: // loop
return idx % total;
case 2: { // ping-pong
int period = total * 2 - 2;
idx = idx % period;
return idx < total ? idx : period - idx;
}
default:
return 0;
}
}
int parts_flat_stop_motion_get_cg_lib(struct parts_flat *f, int sm_lib_idx, int local)
{
if (sm_lib_idx < 0 || (size_t)sm_lib_idx >= f->nr_libraries)
return -1;
struct flat_stop_motion_frames *frames = &f->stop_motion_frames[sm_lib_idx];
if (frames->count <= 0)
return -1;
struct flat_stop_motion *sm = &f->flat->libraries[sm_lib_idx].stop_motion;
int frame = stop_motion_frame_index(sm->loop_type, sm->span, local, frames->count);
return frames->lib_indices[frame];
}
bool parts_flat_load(struct parts *parts, struct parts_flat *f, struct string *filename)
{
if (f->flat) {
@@ -221,8 +295,10 @@ bool parts_flat_load(struct parts *parts, struct parts_flat *f, struct string *f
f->root_state = flat_layer_state_new(f->flat->nr_timelines);
// Load textures for CG libraries.
f->nr_textures = f->flat->nr_libraries;
f->textures = xcalloc(f->nr_textures, sizeof(Texture));
f->nr_libraries = f->flat->nr_libraries;
f->textures = xcalloc(f->nr_libraries, sizeof(Texture));
f->stop_motion_frames = xcalloc(f->nr_libraries,
sizeof(struct flat_stop_motion_frames));
for (size_t i = 0; i < f->flat->nr_libraries; i++) {
struct flat_library *lib = &f->flat->libraries[i];
if (lib->type != FLAT_LIB_CG)
@@ -235,6 +311,10 @@ bool parts_flat_load(struct parts *parts, struct parts_flat *f, struct string *f
gfx_init_texture_with_cg(&f->textures[i], cg);
cg_free(cg);
}
for (size_t i = 0; i < f->flat->nr_libraries; i++) {
if (f->flat->libraries[i].type == FLAT_LIB_STOP_MOTION)
build_stop_motion_frames(f, (int)i);
}
// TODO: The original engine performs per-frame hit testing against
// the flat's visible sprites (with full transform chain), rather than
@@ -244,7 +324,7 @@ bool parts_flat_load(struct parts *parts, struct parts_flat *f, struct string *f
if (w <= 0 || h <= 0) {
w = 0;
h = 0;
for (size_t i = 0; i < f->nr_textures; i++) {
for (size_t i = 0; i < f->nr_libraries; i++) {
if (f->textures[i].w > w)
w = f->textures[i].w;
if (f->textures[i].h > h)
+11 -1
View File
@@ -310,6 +310,13 @@ struct flat_layer_state {
size_t nr_timelines;
};
// Per-frame CG list for a STOP_MOTION library. lib_indices[k] is
// the library index of the CG to display at frame k.
struct flat_stop_motion_frames {
int *lib_indices;
int count;
};
struct parts_flat {
struct parts_common common;
struct string *name;
@@ -320,8 +327,10 @@ struct parts_flat {
int end_frame;
int pending_seek_delta;
struct flat_layer_state *root_state;
size_t nr_libraries;
Texture *textures; // indexed by library index (only CG libs have valid textures)
size_t nr_textures;
// Indexed by library index. Only entries for STOP_MOTION libraries have lib_indices populated.
struct flat_stop_motion_frames *stop_motion_frames;
};
struct parts_movie {
@@ -559,6 +568,7 @@ void parts_flat_free(struct parts_flat *f);
bool parts_flat_load(struct parts *parts, struct parts_flat *f, struct string *filename);
bool parts_flat_update(struct parts_flat *f, int passed_time);
int parts_flat_find_library(struct flat *fl, const char *name);
int parts_flat_stop_motion_get_cg_lib(struct parts_flat *f, int sm_lib_idx, int local);
// layoutbox.c
void parts_do_layout(struct parts *parts);
+40 -31
View File
@@ -187,12 +187,40 @@ static void render_flat_layer(struct parts *parts, struct parts_flat *f,
struct flat_timeline *timelines, size_t nr_timelines,
mat4 parent, float parent_alpha);
static void render_flat_cg(struct parts *parts, Texture *tex,
struct flat_key_data_graphic *key, mat4 combined, float alpha)
{
if (!tex->handle)
return;
set_draw_filter_blend_func(key->draw_filter);
mat4 render_m;
glm_mat4_copy(combined, render_m);
glm_scale(render_m, (vec3){ tex->w, tex->h, 1.0f });
Rectangle rect;
if (key->area_width && key->area_height) {
rect = (Rectangle){ key->area_x, key->area_y, key->area_width, key->area_height };
} else {
rect = (Rectangle){ 0, 0, tex->w, tex->h };
}
vec3 add_color = { key->add_r / 255.0f, key->add_g / 255.0f, key->add_b / 255.0f };
vec3 mul_color = { key->mul_r / 255.0f, key->mul_g / 255.0f, key->mul_b / 255.0f };
parts_render_texture(tex, render_m, &rect, alpha, add_color, mul_color,
key->draw_filter, parts->alpha_clipper_parts_no);
if (key->draw_filter != PARTS_DRAW_FILTER_NORMAL)
glBlendFuncSeparate(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, GL_ONE, GL_ZERO);
}
static void render_flat_item(struct parts *parts, struct parts_flat *f,
struct flat_layer_state *state, size_t tl_idx,
struct flat_timeline *tl,
struct flat_key_data_graphic *key,
struct flat_timeline *tl, int local,
mat4 parent, float parent_alpha)
{
struct flat_key_data_graphic *key = &tl->graphic.keys[local];
int lib_idx = parts_flat_find_library(f->flat, tl->library_name->text);
if (lib_idx < 0 || (size_t)lib_idx >= f->flat->nr_libraries)
return;
@@ -214,33 +242,9 @@ static void render_flat_item(struct parts *parts, struct parts_flat *f,
float alpha = parent_alpha * key->alpha / 255.0f;
switch (lib->type) {
case FLAT_LIB_CG: {
if ((size_t)lib_idx >= f->nr_textures || !f->textures[lib_idx].handle)
return;
Texture *tex = &f->textures[lib_idx];
set_draw_filter_blend_func(key->draw_filter);
mat4 render_m;
glm_mat4_copy(combined, render_m);
glm_scale(render_m, (vec3){ tex->w, tex->h, 1.0f });
Rectangle rect;
if (key->area_width && key->area_height) {
rect = (Rectangle){ key->area_x, key->area_y, key->area_width, key->area_height };
} else {
rect = (Rectangle){ 0, 0, tex->w, tex->h };
}
vec3 add_color = { key->add_r / 255.0f, key->add_g / 255.0f, key->add_b / 255.0f };
vec3 mul_color = { key->mul_r / 255.0f, key->mul_g / 255.0f, key->mul_b / 255.0f };
parts_render_texture(tex, render_m, &rect, alpha, add_color, mul_color,
key->draw_filter, parts->alpha_clipper_parts_no);
if (key->draw_filter != PARTS_DRAW_FILTER_NORMAL)
glBlendFuncSeparate(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, GL_ONE, GL_ZERO);
case FLAT_LIB_CG:
render_flat_cg(parts, &f->textures[lib_idx], key, combined, alpha);
break;
}
case FLAT_LIB_TIMELINE: {
struct flat_layer_state *child = state->children[tl_idx];
if (child) {
@@ -251,7 +255,13 @@ static void render_flat_item(struct parts *parts, struct parts_flat *f,
}
break;
}
// TODO: support FLAT_LIB_STOP_MOTION and FLAT_LIB_EMITTER
case FLAT_LIB_STOP_MOTION: {
int cg_idx = parts_flat_stop_motion_get_cg_lib(f, lib_idx, local);
if (cg_idx >= 0 && (size_t)cg_idx < f->nr_libraries)
render_flat_cg(parts, &f->textures[cg_idx], key, combined, alpha);
break;
}
// TODO: support FLAT_LIB_EMITTER
default:
break;
}
@@ -273,9 +283,8 @@ static void render_flat_layer(struct parts *parts, struct parts_flat *f,
if (local >= (int)tl->graphic.count)
continue;
struct flat_key_data_graphic *key = &tl->graphic.keys[local];
render_flat_item(parts, f, state, i, tl, key, parent, parent_alpha);
render_flat_item(parts, f, state, i, tl, local, parent, parent_alpha);
}
}