Merge pull request #337 from kichikuou/flat

Parts: Stop motion support and rendering fixes
This commit is contained in:
Nunuhara Cabbage
2026-05-10 09:58:02 -07:00
committed by GitHub
3 changed files with 171 additions and 54 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);
+76 -49
View File
@@ -30,15 +30,6 @@
#include "parts_internal.h"
#ifndef M_PI
#define M_PI (3.14159265358979323846)
#endif
static inline float deg2rad(float deg)
{
return deg * (M_PI / 180.0);
}
static struct {
struct shader shader;
GLint blend_rate;
@@ -97,7 +88,7 @@ static void parts_render_texture(struct texture *texture, mat4 mw_transform, Rec
// Calculate the inverse of the clipper's world matrix.
mat4 clip_mw = GLM_MAT4_IDENTITY_INIT;
glm_translate(clip_mw, (vec3) { clipper->global.pos.x, clipper->global.pos.y, 0 });
glm_rotate_z(clip_mw, clipper->local.rotation.z * (M_PI/180.0), clip_mw);
glm_rotate_z(clip_mw, glm_rad(clipper->local.rotation.z), clip_mw);
glm_scale(clip_mw, (vec3){ clipper->global.scale.x, clipper->global.scale.y, 1.0 });
glm_translate(clip_mw, (vec3){ c_common->origin_offset.x, c_common->origin_offset.y, 0 });
glm_scale(clip_mw, (vec3){ c_common->w, c_common->h, 1.0 });
@@ -157,7 +148,7 @@ static void parts_render_cg(struct parts *parts, struct parts_common *common)
// FIXME: need perspective for 3D rotate
//glm_rotate_x(mw_transform, parts->rotation.x, mw_transform);
//glm_rotate_y(mw_transform, parts->rotation.y, mw_transform);
glm_rotate_z(mw_transform, parts->local.rotation.z * (M_PI/180.0), mw_transform);
glm_rotate_z(mw_transform, glm_rad(parts->local.rotation.z), mw_transform);
glm_scale(mw_transform, (vec3){ parts->global.scale.x, parts->global.scale.y, 1.0 });
glm_translate(mw_transform, (vec3){ common->origin_offset.x, common->origin_offset.y, 0 });
glm_scale(mw_transform, (vec3){ common->w, common->h, 1.0 });
@@ -185,14 +176,49 @@ static void parts_render_cg(struct parts *parts, struct parts_common *common)
static void render_flat_layer(struct parts *parts, struct parts_flat *f,
struct flat_layer_state *state,
struct flat_timeline *timelines, size_t nr_timelines,
mat4 parent, float parent_alpha);
mat4 parent, float parent_alpha, int parent_draw_filter);
static void render_flat_cg(struct parts *parts, Texture *tex,
struct flat_key_data_graphic *key, mat4 combined, float alpha, int draw_filter)
{
if (!tex->handle)
return;
set_draw_filter_blend_func(draw_filter);
mat4 render_m;
glm_mat4_copy(combined, render_m);
// Kill the Z-output row to pin clip_z at the near plane, avoiding
// near/far clipping of 3D-rotated sprites.
render_m[0][2] = render_m[1][2] = render_m[2][2] = render_m[3][2] = 0.0f;
// area_x/area_y select a sub-rectangle of the texture atlas, but
// should not shift the on-screen position. This translation cancels
// the offset that the sub-rect's top-left would otherwise introduce.
glm_translate(render_m, (vec3){ -(float)key->area_x, -(float)key->area_y, 0.0f });
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,
draw_filter, parts->alpha_clipper_parts_no);
if (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,
mat4 parent, float parent_alpha)
struct flat_timeline *tl, int local,
mat4 parent, float parent_alpha, int parent_draw_filter)
{
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;
@@ -202,56 +228,58 @@ static void render_flat_item(struct parts *parts, struct parts_flat *f,
float pos_x = (f->flat->hdr.version > 4) ? key->pos_x.f : (float)key->pos_x.i;
float pos_y = (f->flat->hdr.version > 4) ? key->pos_y.f : (float)key->pos_y.i;
// Per-key local matrix:
// M_layer = T(pos) * Rz * Rx * Ry * Scale * T(-origin) * ReverseScale
// The original engine projects 3D-rotated sprites through a proper
// perspective, but we use an orthographic approximation for simplicity.
mat4 layer_m = GLM_MAT4_IDENTITY_INIT;
glm_translate(layer_m, (vec3){ pos_x, pos_y, 0 });
glm_rotate_z(layer_m, deg2rad(key->angle_z), layer_m);
if (key->angle_z != 0)
glm_rotate_z(layer_m, glm_rad(key->angle_z), layer_m);
if (key->angle_x != 0)
glm_rotate_x(layer_m, glm_rad(-key->angle_x), layer_m);
if (key->angle_y != 0)
glm_rotate_y(layer_m, glm_rad(key->angle_y), layer_m);
glm_scale(layer_m, (vec3){ key->scale_x, key->scale_y, 1.0f });
glm_translate(layer_m, (vec3){ -(float)key->origin_x, -(float)key->origin_y, 0 });
if (key->reverse_lr || key->reverse_tb) {
glm_scale(layer_m, (vec3){
key->reverse_lr ? -1.0f : 1.0f,
key->reverse_tb ? -1.0f : 1.0f,
1.0f });
}
mat4 combined;
glm_mat4_mul(parent, layer_m, combined);
float alpha = parent_alpha * key->alpha / 255.0f;
// Effective draw filter: this key's own filter overrides for its subtree;
// otherwise the cascading parent filter persists.
int draw_filter = key->draw_filter != PARTS_DRAW_FILTER_NORMAL
? key->draw_filter : parent_draw_filter;
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, draw_filter);
break;
}
case FLAT_LIB_TIMELINE: {
struct flat_layer_state *child = state->children[tl_idx];
if (child) {
render_flat_layer(parts, f, child,
lib->timeline.timelines,
lib->timeline.nr_timelines,
combined, alpha);
combined, alpha, draw_filter);
}
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, draw_filter);
break;
}
// TODO: support FLAT_LIB_EMITTER
default:
break;
}
@@ -260,7 +288,7 @@ static void render_flat_item(struct parts *parts, struct parts_flat *f,
static void render_flat_layer(struct parts *parts, struct parts_flat *f,
struct flat_layer_state *state,
struct flat_timeline *timelines, size_t nr_timelines,
mat4 parent, float parent_alpha)
mat4 parent, float parent_alpha, int parent_draw_filter)
{
// reverse order for correct z-ordering
for (size_t i = nr_timelines; i-- > 0;) {
@@ -273,9 +301,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, parent_draw_filter);
}
}
@@ -286,12 +313,12 @@ static void parts_render_flat(struct parts *parts, struct parts_flat *f)
mat4 base = GLM_MAT4_IDENTITY_INIT;
glm_translate(base, (vec3){ parts->global.pos.x, parts->global.pos.y, 0 });
glm_rotate_z(base, deg2rad(parts->local.rotation.z), base);
glm_rotate_z(base, glm_rad(parts->local.rotation.z), base);
glm_scale(base, (vec3){ parts->global.scale.x, parts->global.scale.y, 1.0f });
render_flat_layer(parts, f, f->root_state,
f->flat->timelines, f->flat->nr_timelines,
base, parts->global.alpha / 255.0f);
base, parts->global.alpha / 255.0f, PARTS_DRAW_FILTER_NORMAL);
}
static void parts_render_flash_shape(struct parts *parts, struct parts_flash *f, struct parts_flash_object *obj, struct swf_tag_define_shape *tag)