diff --git a/src/parts/flat.c b/src/parts/flat.c index dfa88ae..852d5bd 100644 --- a/src/parts/flat.c +++ b/src/parts/flat.c @@ -15,8 +15,12 @@ */ #include +#include #include #include +#include + +#include "system4/mt19937int.h" #include "system4.h" #include "system4/archive.h" @@ -30,6 +34,46 @@ #include "parts_internal.h" #include "xsystem4.h" +enum emitter_direction_type { + EMITTER_DIRECTION_RANDOM = 0, + EMITTER_DIRECTION_FIXED = 1, + EMITTER_DIRECTION_PARENT = 2, + EMITTER_DIRECTION_PARENT_REVERSE = 3, + EMITTER_DIRECTION_DISK = 4, +}; + +enum emitter_create_pos_type { + EMITTER_CREATE_POS_NONE = 0, + EMITTER_CREATE_POS_SPHERE = 1, + EMITTER_CREATE_POS_CIRCLE = 2, + EMITTER_CREATE_POS_RECT = 3, +}; + +enum emitter_parent_key_mode { + EMITTER_PARENT_KEY_SELECTIVE = 0, + EMITTER_PARENT_KEY_ANCESTOR_CURRENT = 1, + EMITTER_PARENT_KEY_ANCESTOR_AT_BIRTH = 2, +}; + +static inline float mt_next(struct mt19937 *mt) +{ + return mt19937_genrand(mt) / 4294967296.0f; +} + +static inline float mt_next_signed(struct mt19937 *mt) +{ + return mt_next(mt) - 0.5f; +} + +// Approximately uniform random unit 3D vector. +static void random_unit_vec3(struct mt19937 *mt, vec3 v) +{ + v[0] = mt_next_signed(mt); + v[1] = mt_next_signed(mt); + v[2] = mt_next_signed(mt); + glm_vec3_normalize(v); +} + static struct flat_layer_state *flat_layer_state_new(size_t nr_timelines) { struct flat_layer_state *s = xcalloc(1, sizeof(struct flat_layer_state)); @@ -315,6 +359,17 @@ bool parts_flat_load(struct parts *parts, struct parts_flat *f, struct string *f if (f->flat->libraries[i].type == FLAT_LIB_STOP_MOTION) build_stop_motion_frames(f, (int)i); } + for (size_t i = 0; i < f->flat->nr_libraries; i++) { + if (f->flat->libraries[i].type != FLAT_LIB_EMITTER) + continue; + struct flat_emitter *em = &f->flat->libraries[i].emitter; + if (em->end_pos_type != 0) + WARNING("flat: emitter '%s': unsupported end_pos_type %d", + display_sjis0(em->library_name->text), em->end_pos_type); + if (em->pos_track_mode == 0) + WARNING("flat: emitter '%s': unsupported pos_track_mode %d", + display_sjis0(em->library_name->text), em->pos_track_mode); + } // TODO: The original engine performs per-frame hit testing against // the flat's visible sprites (with full transform chain), rather than @@ -383,6 +438,430 @@ bool parts_flat_update(struct parts_flat *f, int passed_time) return any_changed; } +// Build a layer matrix from a single key's properties. +// The sequence is: T(pos) * Rz * Rx * Ry * S(scale) * T(-origin) * Reverse. +// Components can be selectively enabled; pos is resolved by the caller. +void parts_flat_build_layer_matrix(const struct flat_key_data_graphic *key, + vec2 pos, + bool use_rotation, bool use_scale, bool use_origin, + bool reverse_lr, bool reverse_tb, + mat4 out) +{ + glm_mat4_identity(out); + glm_translate(out, (vec3){ pos[0], pos[1], 0 }); + // The original engine projects 3D-rotated sprites through a proper + // perspective, but we use an orthographic approximation for simplicity. + if (use_rotation) { + if (key->angle_z != 0) glm_rotate_z(out, glm_rad(key->angle_z), out); + if (key->angle_x != 0) glm_rotate_x(out, glm_rad(-key->angle_x), out); + if (key->angle_y != 0) glm_rotate_y(out, glm_rad(key->angle_y), out); + } + if (use_scale) + glm_scale(out, (vec3){ key->scale_x, key->scale_y, 1.0f }); + if (use_origin) + glm_translate(out, (vec3){ -(float)key->origin_x, -(float)key->origin_y, 0 }); + if (reverse_lr || reverse_tb) { + glm_scale(out, (vec3){ + reverse_lr ? -1.0f : 1.0f, + reverse_tb ? -1.0f : 1.0f, + 1.0f }); + } +} + +// Build the emitter's particle base matrix from the recorded ancestor key +// chain. +// +// SELECTIVE mode: each ancestor contributes a fresh key with defaults +// (scale=1, rotation=0, origin=0, reverse=false), overriding only the +// properties listed in the emitter's inherit_* flags. +// +// ANCESTOR_CURRENT / ANCESTOR_AT_BIRTH modes: the keys from the entire +// parent view chain are used as-is. inherit_* flags are ignored; all +// ancestor properties affect the particle base matrix. +void parts_flat_build_emitter_base_matrix(const struct flat_emitter *em, + const struct flat_key_stack *stack, mat4 out) +{ + bool connected = em->parent_key_mode != EMITTER_PARENT_KEY_SELECTIVE; + glm_mat4_identity(out); + for (int i = 0; i < stack->count; i++) { + const struct flat_key_data_graphic *key = stack->keys[i]; + vec2 pos = { key->pos_x, key->pos_y }; + + mat4 layer_m; + parts_flat_build_layer_matrix(key, pos, + connected || em->inherit_rotation, + connected || em->inherit_scale, + connected, + (connected || em->inherit_reverse_lr) && key->reverse_lr, + (connected || em->inherit_reverse_tb) && key->reverse_tb, + layer_m); + + glm_mat4_mul(out, layer_m, out); + } +} + +// Returns how many particles should be born on birth_frame, distributing +// create_count particles evenly across active_frames. +static int emitter_get_birth_count(int birth_frame, int create_count, + int frame_count, int particle_lifetime) +{ + int active_frames = frame_count - particle_lifetime + 1; + if (active_frames <= 1) + return create_count; + if (create_count == 1) + return birth_frame == 0 ? 1 : 0; + + if (create_count >= active_frames) { + // Dense case: distribute uniformly so each frame gets floor or ceil of + // the average rate. + float rate = (float)create_count / active_frames; + return (int)((birth_frame + 1) * rate) - (int)(birth_frame * rate); + } + + // Sparse case: pin one particle to frame 0, then space the remaining + // create_count-1 particles evenly from frame 1 to active_frames-1 (so + // frame active_frames-1 also always gets one particle). + if (birth_frame == 0) + return 1; + float rate = (float)(create_count - 1) / (active_frames - 1); + return (int)(birth_frame * rate) - (int)((birth_frame - 1) * rate); +} + +static float emitter_fade_alpha(int age, struct flat_emitter *em) +{ + if (em->fade_in_frame > 0 && age < em->fade_in_frame) + return (float)age / em->fade_in_frame; + if (em->fade_out_frame > 0 && age > em->particle_lifetime - em->fade_out_frame) + return (float)(em->particle_lifetime - age) / em->fade_out_frame; + return 1.0f; +} + +static float emitter_gravity_displacement(int age, struct flat_emitter *em, int fps) +{ + if (!em->is_fall) + return 0; + const float G = 9.8f; + float t = (float)age / fps; + if (em->width != 0 && em->air_resistance != 0) { + float k = em->air_resistance / em->width; + float exp_term = 1 - expf(-k * t); + return (t - exp_term / k) * em->width * G / em->air_resistance; + } + return 0.5f * G * t * t; +} + +// Lerp from `begin` to `end` at `t`, with per-endpoint random jitter scaled +// by begin_rand / end_rand. When `sync` is true, both endpoints share the +// same jitter draw (but the second draw is still consumed, to keep the RNG +// sequence stable across the sync flag). +static float lerp_with_jitter(struct mt19937 *mt, + float begin, float begin_rand, float end, float end_rand, + bool sync, float t) +{ + float r1 = mt_next_signed(mt) * begin_rand; + float r2 = mt_next_signed(mt) * end_rand; + float b = begin + r1; + float e = end + (sync ? r1 : r2); + return b + (e - b) * t; +} + +static void emitter_interpolate_scale(float t, struct flat_emitter *em, + struct mt19937 *mt, vec2 out) +{ + float overall = lerp_with_jitter(mt, em->begin_scale, em->begin_scale_rand, + em->end_scale, em->end_scale_rand, em->sync_scale_rand, t); + float x = lerp_with_jitter(mt, em->begin_x_scale, em->begin_x_scale_rand, + em->end_x_scale, em->end_x_scale_rand, em->sync_scale_rand, t); + float y = lerp_with_jitter(mt, em->begin_y_scale, em->begin_y_scale_rand, + em->end_y_scale, em->end_y_scale_rand, em->sync_scale_rand, t); + out[0] = x * overall; + out[1] = y * overall; +} + +static void emitter_interpolate_rotation(float t, struct flat_emitter *em, + struct mt19937 *mt, vec3 out) +{ + out[0] = lerp_with_jitter(mt, em->begin_x_angle, em->begin_x_angle_rand, + em->end_x_angle, em->end_x_angle_rand, em->sync_rotation_rand, t); + out[1] = lerp_with_jitter(mt, em->begin_y_angle, em->begin_y_angle_rand, + em->end_y_angle, em->end_y_angle_rand, em->sync_rotation_rand, t); + out[2] = lerp_with_jitter(mt, em->begin_z_angle, em->begin_z_angle_rand, + em->end_z_angle, em->end_z_angle_rand, em->sync_rotation_rand, t); +} + +static void emitter_calc_create_position(struct flat_emitter *em, + struct mt19937 *mt, vec2 out) +{ + switch (em->create_pos_type) { + case EMITTER_CREATE_POS_RECT: + out[0] = mt_next_signed(mt) * em->create_pos_length; + out[1] = mt_next_signed(mt) * em->create_pos_length2; + return; + case EMITTER_CREATE_POS_SPHERE: { + vec3 v; + random_unit_vec3(mt, v); + out[0] = v[0]; + out[1] = v[1]; + break; + } + case EMITTER_CREATE_POS_CIRCLE: + out[0] = mt_next_signed(mt); + out[1] = mt_next_signed(mt); + glm_vec2_normalize(out); + break; + default: + glm_vec2_zero(out); + break; + } + float dist = mt_next(mt) * (em->create_pos_length - em->create_pos_length2) + + em->create_pos_length2; + glm_vec2_scale(out, dist, out); +} + +// 3D rejection sampling: +// Picks a random unit vector c with dot(v, c) > cos(rand * angle/2), i.e. +// constrained to within `rand * angle/2` of the input direction. +static void randomize_direction_within_cone(vec3 v, float angle_deg, struct mt19937 *mt) +{ + glm_vec3_normalize(v); + float theta = mt_next(mt) * glm_rad(angle_deg) * 0.5f; + if (!(theta > 0)) + return; + float cos_t = cosf(theta); + for (int i = 0; i < 1000; i++) { + vec3 c; + random_unit_vec3(mt, c); + if (glm_vec3_dot(v, c) > cos_t) { + glm_vec3_copy(c, v); + return; + } + } +} + +static void emitter_calc_direction(struct flat_emitter *em, + struct mt19937 *mt, vec2 parent_vel, vec2 out) +{ + vec3 dir; + switch (em->direction_type) { + case EMITTER_DIRECTION_RANDOM: + random_unit_vec3(mt, dir); + break; + case EMITTER_DIRECTION_PARENT: + dir[0] = parent_vel[0]; + dir[1] = parent_vel[1]; + dir[2] = 0; + break; + case EMITTER_DIRECTION_PARENT_REVERSE: + dir[0] = -parent_vel[0]; + dir[1] = -parent_vel[1]; + dir[2] = 0; + break; + case EMITTER_DIRECTION_DISK: { + // direction_x/y/z is the normal of the emission disk. Pick a + // random unit vector on that disk; randomize_direction_within_cone + // below thickens it into a band of half-width direction_angle/2. + vec3 normal = { em->direction_x, em->direction_y, em->direction_z }; + if (glm_vec3_norm2(normal) < 1e-12f) + glm_vec3_copy((vec3){ 0, 0, 1 }, normal); + glm_vec3_ortho(normal, dir); + float phi = mt_next(mt) * 2 * GLM_PIf; + glm_vec3_rotate(dir, phi, normal); + break; + } + case EMITTER_DIRECTION_FIXED: + default: + glm_vec3_copy((vec3){ em->direction_x, em->direction_y, em->direction_z }, dir); + break; + } + randomize_direction_within_cone(dir, em->direction_angle, mt); + // Project to 2D by dropping Z without renormalizing in 2D, so + // directions tilted out of the XY plane translate to slower + // on-screen motion. + glm_vec3_normalize(dir); + out[0] = dir[0]; + out[1] = dir[1]; +} + +static void emitter_calc_trajectory(int age, struct flat_emitter *em, + vec2 dir, int fps, float move_rand_factor, vec2 out) +{ + float t = (float)age / fps; + float accel = em->acceleration * move_rand_factor; + float speed = em->speed * move_rand_factor; + float move_length = em->move_length * move_rand_factor; + float curve = em->move_curve * move_rand_factor; + + if (accel < 0 && speed != 0) { + float t_max = fabsf(speed / accel); + if (t > t_max) + t = t_max; + } + + float displacement = 0.5f * accel * t * t + speed * t; + + if (move_length != 0) { + float norm_t = em->particle_lifetime > 0 ? (float)age / em->particle_lifetime : 0; + if (curve > 1.0f) + displacement += powf(norm_t, curve) * move_length; + else if (curve < -1.0f) + displacement += (1 - powf(1 - norm_t, -curve)) * move_length; + else + displacement += move_length * norm_t; + } + glm_vec2_scale(dir, displacement, out); +} + +// Resolve the per-frame emitter layer properties (pos, alpha, colors, reverse +// flags, draw_filter) into `out`, applying the emitter's inherit_* flags. +void parts_flat_emitter_resolve_layer( + const struct flat_emitter *em, + const struct flat_key_data_graphic *key, + float parts_alpha, float layer_alpha, + struct flat_emitter_layer_effective *out) +{ + bool connected = em->parent_key_mode != EMITTER_PARENT_KEY_SELECTIVE; + out->use_origin = connected; + out->use_scale = connected || em->inherit_scale; + out->use_rotation = connected || em->inherit_rotation; + out->pos[0] = key->pos_x; + out->pos[1] = key->pos_y; + out->reverse_lr = (connected || em->inherit_reverse_lr) && key->reverse_lr; + out->reverse_tb = (connected || em->inherit_reverse_tb) && key->reverse_tb; + out->alpha = (connected || em->inherit_alpha) ? layer_alpha : parts_alpha; + if (connected || em->inherit_add_color) { + out->add_color[0] = key->add_r / 255.0f; + out->add_color[1] = key->add_g / 255.0f; + out->add_color[2] = key->add_b / 255.0f; + } else { + glm_vec3_zero(out->add_color); + } + if (connected || em->inherit_mul_color) { + out->mul_color[0] = key->mul_r / 255.0f; + out->mul_color[1] = key->mul_g / 255.0f; + out->mul_color[2] = key->mul_b / 255.0f; + } else { + glm_vec3_one(out->mul_color); + } + int draw_filter = (connected || em->inherit_draw_filter) ? key->draw_filter + : PARTS_DRAW_FILTER_NORMAL; + // Emitter's own draw_filter overrides the inherited one. + out->draw_filter = em->draw_filter != PARTS_DRAW_FILTER_NORMAL + ? em->draw_filter : draw_filter; +} + +// Compute the alignment origin offset for particle_align (1-9 numpad layout). +bool parts_flat_emitter_get_align_offset(struct parts_flat *f, int emitter_lib_idx, vec2 out) +{ + struct flat_emitter *em = &f->flat->libraries[emitter_lib_idx].emitter; + if (em->particle_lifetime <= 0 || em->create_count <= 0) + return false; + if (f->flat->hdr.fps <= 0) + return false; + + int cg_lib_idx = parts_flat_find_library(f->flat, em->library_name->text); + if (cg_lib_idx < 0 || (size_t)cg_lib_idx >= f->flat->nr_libraries) + return false; + if (f->flat->libraries[cg_lib_idx].type == FLAT_LIB_STOP_MOTION) { + cg_lib_idx = parts_flat_stop_motion_get_cg_lib(f, cg_lib_idx, 0); + if (cg_lib_idx < 0) + return false; + } + + Texture *tex = &f->textures[cg_lib_idx]; + if (!tex->handle) + return false; + + int align = em->particle_align; + if (align < 1 || align > 9) align = 5; + int col = (align - 1) % 3; + int row = (align - 1) / 3; + out[0] = tex->w * col / 2.0f; + out[1] = tex->h * row / 2.0f; + return true; +} + +// Enumerate the particles spawned on `birth_frame` for this emitter, computing +// each particle's pose at the given `age` (in frames since its birth) and +// invoking `fn` with the result. The RNG is seeded from the emitter's +// rand_seed and birth_frame, so the same birth_frame always yields the same +// particles regardless of `age`. +void parts_flat_foreach_emitter_particle(struct parts_flat *f, int emitter_lib_idx, + const struct flat_key_data_graphic *keys, + int birth_frame, int age, int frame_count, + flat_emitter_particle_fn fn, void *ud) +{ + struct flat *fl = f->flat; + struct flat_emitter *em = &fl->libraries[emitter_lib_idx].emitter; + + int count = emitter_get_birth_count(birth_frame, em->create_count, + frame_count, em->particle_lifetime); + if (count == 0) + return; + + float fade_alpha = emitter_fade_alpha(age, em); + if (fade_alpha <= 0.f) + return; + + // Resolve the particle CG. + int lib_idx = parts_flat_find_library(fl, em->library_name->text); + if (lib_idx < 0) + return; + if (fl->libraries[lib_idx].type == FLAT_LIB_STOP_MOTION) { + lib_idx = parts_flat_stop_motion_get_cg_lib(f, lib_idx, age); + if (lib_idx < 0) + return; + } + + // Parent velocity at birth_frame (only consumed when direction_type + // is PARENT or PARENT_REVERSE). Backward difference at frame > 0, + // forward difference at frame 0, zero otherwise. + vec2 parent_vel = { 0, 0 }; + if (em->direction_type == EMITTER_DIRECTION_PARENT + || em->direction_type == EMITTER_DIRECTION_PARENT_REVERSE) { + const struct flat_key_data_graphic *cur = &keys[birth_frame]; + if (birth_frame > 0) { + const struct flat_key_data_graphic *prev = &keys[birth_frame - 1]; + parent_vel[0] = cur->pos_x - prev->pos_x; + parent_vel[1] = cur->pos_y - prev->pos_y; + } else if (birth_frame + 1 < frame_count) { + const struct flat_key_data_graphic *next = &keys[birth_frame + 1]; + parent_vel[0] = next->pos_x - cur->pos_x; + parent_vel[1] = next->pos_y - cur->pos_y; + } + } + + struct mt19937 mt; + mt19937_init(&mt, em->rand_seed * (birth_frame + 1)); + + int fps = fl->hdr.fps; + float pixels_per_meter = (float)fl->hdr.game_view_width / fl->hdr.meter; + float t = (float)age / em->particle_lifetime; + float gravity_y = emitter_gravity_displacement(age, em, fps); + + for (int i = 0; i < count; i++) { + struct flat_emitter_particle p; + emitter_interpolate_scale(t, em, &mt, p.scale); + vec2 create_pos; + emitter_calc_create_position(em, &mt, create_pos); + emitter_interpolate_rotation(t, em, &mt, p.rot); + vec2 dir; + emitter_calc_direction(em, &mt, parent_vel, dir); + float move_rand_factor = 1.0f - (mt_next(&mt) - 0.5f) * em->move_rand * 0.01f; + vec2 traj; + emitter_calc_trajectory(age, em, dir, fps, move_rand_factor, traj); + + if (em->align_to_direction && (dir[0] != 0 || dir[1] != 0)) + p.rot[2] += glm_deg(atan2f(dir[1], dir[0])) + 90; + + p.pos[0] = (create_pos[0] + traj[0]) * pixels_per_meter; + p.pos[1] = (create_pos[1] + traj[1] + gravity_y) * pixels_per_meter; + p.fade_alpha = fade_alpha; + p.cg_lib_idx = lib_idx; + + fn(&p, ud); + } +} + bool PE_ExistsFlatFile(struct string *filename) { if (!filename) diff --git a/src/parts/parts_internal.h b/src/parts/parts_internal.h index 9fb09fc..af63a1b 100644 --- a/src/parts/parts_internal.h +++ b/src/parts/parts_internal.h @@ -570,6 +570,56 @@ 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); +struct flat_emitter; +struct flat_key_data_graphic; + +struct flat_emitter_particle { + vec2 pos; // emitter-space position (pixels) + vec2 scale; + vec3 rot; // degrees (x, y, z) + float fade_alpha; // 0-1.0 + int cg_lib_idx; // CG library index of the texture for this particle +}; + +// Per-key emitter properties after applying the emitter's inherit_* flags. +struct flat_emitter_layer_effective { + vec2 pos; + bool reverse_lr, reverse_tb; + float alpha; + vec3 add_color; + vec3 mul_color; + int draw_filter; + bool use_scale; + bool use_rotation; + bool use_origin; +}; + +#define FLAT_MAX_ANCESTOR_DEPTH 32 +struct flat_key_stack { + const struct flat_key_data_graphic *keys[FLAT_MAX_ANCESTOR_DEPTH]; + int count; +}; + +typedef void (*flat_emitter_particle_fn)(const struct flat_emitter_particle *p, + void *ud); +bool parts_flat_emitter_get_align_offset(struct parts_flat *f, int emitter_lib_idx, vec2 out); +void parts_flat_foreach_emitter_particle(struct parts_flat *f, int emitter_lib_idx, + const struct flat_key_data_graphic *keys, + int birth_frame, int age, int frame_count, + flat_emitter_particle_fn fn, void *ud); +void parts_flat_build_layer_matrix(const struct flat_key_data_graphic *key, + vec2 pos, + bool use_rotation, bool use_scale, bool use_origin, + bool reverse_lr, bool reverse_tb, + mat4 out); +void parts_flat_build_emitter_base_matrix(const struct flat_emitter *em, + const struct flat_key_stack *stack, mat4 out); +void parts_flat_emitter_resolve_layer( + const struct flat_emitter *em, + const struct flat_key_data_graphic *key, + float parts_alpha, float layer_alpha, + struct flat_emitter_layer_effective *out); + // layoutbox.c void parts_do_layout(struct parts *parts); diff --git a/src/parts/render.c b/src/parts/render.c index e39944f..570c4e9 100644 --- a/src/parts/render.c +++ b/src/parts/render.c @@ -175,6 +175,118 @@ static void parts_render_cg(struct parts *parts, struct parts_common *common) glBlendFuncSeparate(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, GL_ONE, GL_ZERO); } +struct emitter_render_ud { + struct parts_flat *f; + mat4 transform; // root * base * layer for this emitter+birth_frame + float parent_alpha; + vec2 align; + int alpha_clipper; + int draw_filter; + vec3 add_color; + vec3 mul_color; +}; + +static void render_emitter_particle_cb(const struct flat_emitter_particle *p, + void *ud) +{ + struct emitter_render_ud *d = ud; + if (p->cg_lib_idx < 0 || (size_t)p->cg_lib_idx >= d->f->nr_libraries) + return; + Texture *tex = &d->f->textures[p->cg_lib_idx]; + if (!tex->handle) + return; + + mat4 m = GLM_MAT4_IDENTITY_INIT; + glm_translate(m, (vec3){ p->pos[0], p->pos[1], 0 }); + if (p->rot[2] != 0) + glm_rotate_z(m, glm_rad(p->rot[2]), m); + if (p->rot[0] != 0) + glm_rotate_x(m, glm_rad(-p->rot[0]), m); + if (p->rot[1] != 0) + glm_rotate_y(m, glm_rad(p->rot[1]), m); + glm_scale(m, (vec3){ p->scale[0], p->scale[1], 1.0f }); + glm_translate(m, (vec3){ -d->align[0], -d->align[1], 0 }); + + // Bring particle into screen space, then kill the Z-output row so + // clip_z stays at the near plane. + glm_mat4_mul(d->transform, m, m); + m[0][2] = m[1][2] = m[2][2] = m[3][2] = 0.0f; + glm_scale(m, (vec3){ tex->w, tex->h, 1.0f }); + + if (d->draw_filter != PARTS_DRAW_FILTER_NORMAL) + set_draw_filter_blend_func(d->draw_filter); + + float blend_rate = d->parent_alpha * p->fade_alpha; + Rectangle rect = { 0, 0, tex->w, tex->h }; + parts_render_texture(tex, m, &rect, blend_rate, + d->add_color, d->mul_color, d->draw_filter, d->alpha_clipper); + + if (d->draw_filter != PARTS_DRAW_FILTER_NORMAL) + glBlendFuncSeparate(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, GL_ONE, GL_ZERO); +} + +static void render_flat_emitter(struct parts *parts, struct parts_flat *f, + int emitter_lib_idx, int local, int frame_count, + struct flat_key_data_graphic *keys, + mat4 root, float parent_alpha, + struct flat_key_stack *key_stack) +{ + vec2 align; + if (!parts_flat_emitter_get_align_offset(f, emitter_lib_idx, align)) + return; + + struct flat_emitter *em = &f->flat->libraries[emitter_lib_idx].emitter; + int active_frames = max(1, frame_count - em->particle_lifetime + 1); + float parts_alpha = parts->global.alpha / 255.0f; + + // TODO: key_stack reflects the current render frame, but each particle + // should see the ancestor keyframes at its birth frame instead. + // Currently all particles of this emitter share the same ancestor pose, + // which is noticeable only on `猿玉/取得済みマーク.flat` in Rance 9. + // Fixing this requires per-emitter state in parts_flat_update to record + // ancestor keys at each birth frame; base would then depend on birth_frame. + mat4 base; + parts_flat_build_emitter_base_matrix(em, key_stack, base); + + int min_birth = max(0, local - em->particle_lifetime + 1); + int max_birth = min(active_frames - 1, local); + for (int birth_frame = min_birth; birth_frame <= max_birth; birth_frame++) { + int age = local - birth_frame; + + // Use the birth frame's key transform so particles stay at their + // birth position while the emitter moves. + struct flat_key_data_graphic *birth_key = &keys[birth_frame]; + float layer_alpha = parent_alpha * birth_key->alpha / 255.0f; + + struct flat_emitter_layer_effective eff; + parts_flat_emitter_resolve_layer(em, birth_key, + parts_alpha, layer_alpha, &eff); + + // Build per-birth-frame layer matrix from birth_key and the emitter's + // inherit_* flags. + mat4 layer_m; + parts_flat_build_layer_matrix(birth_key, eff.pos, + eff.use_rotation, eff.use_scale, eff.use_origin, + eff.reverse_lr, eff.reverse_tb, + layer_m); + + struct emitter_render_ud ud = { + .f = f, + .parent_alpha = eff.alpha, + .alpha_clipper = parts->alpha_clipper_parts_no, + .draw_filter = eff.draw_filter, + }; + glm_vec2_copy(align, ud.align); + glm_vec3_copy(eff.add_color, ud.add_color); + glm_vec3_copy(eff.mul_color, ud.mul_color); + glm_mat4_mul(base, layer_m, ud.transform); + glm_mat4_mul(root, ud.transform, ud.transform); + parts_flat_foreach_emitter_particle(f, emitter_lib_idx, keys, + birth_frame, age, frame_count, + render_emitter_particle_cb, &ud); + } +} + struct flat_draw_ctx { mat4 matrix; float alpha; @@ -186,7 +298,8 @@ struct flat_draw_ctx { 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, - struct flat_draw_ctx *ctx); + struct flat_draw_ctx *ctx, mat4 root, + struct flat_key_stack *key_stack); static void render_flat_cg(struct parts *parts, Texture *tex, struct flat_key_data_graphic *key, struct flat_draw_ctx *ctx) @@ -224,39 +337,29 @@ static void render_flat_cg(struct parts *parts, Texture *tex, 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, int local, - struct flat_draw_ctx *parent) + struct flat_draw_ctx *parent, mat4 root, + struct flat_key_stack *key_stack) { - 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; struct flat_library *lib = &f->flat->libraries[lib_idx]; - - 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 }); - 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 }); + if (lib->type == FLAT_LIB_EMITTER) { + render_flat_emitter(parts, f, lib_idx, local, tl->frame_count, + tl->graphic.keys, + root, parent->alpha, key_stack); + return; } + struct flat_key_data_graphic *key = &tl->graphic.keys[local]; + mat4 layer_m; + vec2 pos = { key->pos_x, key->pos_y }; + parts_flat_build_layer_matrix(key, pos, + true, true, true, + key->reverse_lr, key->reverse_tb, + layer_m); + struct flat_draw_ctx ctx; glm_mat4_mul(parent->matrix, layer_m, ctx.matrix); ctx.alpha = parent->alpha * key->alpha / 255.0f; @@ -273,10 +376,12 @@ static void render_flat_item(struct parts *parts, struct parts_flat *f, break; case FLAT_LIB_TIMELINE: { struct flat_layer_state *child = state->children[tl_idx]; - if (child) { + if (child && key_stack->count < FLAT_MAX_ANCESTOR_DEPTH) { + key_stack->keys[key_stack->count++] = key; render_flat_layer(parts, f, child, - lib->timeline.timelines, - lib->timeline.nr_timelines, &ctx); + lib->timeline.timelines, lib->timeline.nr_timelines, + &ctx, root, key_stack); + key_stack->count--; } break; } @@ -286,8 +391,11 @@ static void render_flat_item(struct parts *parts, struct parts_flat *f, render_flat_cg(parts, &f->textures[cg_idx], key, &ctx); break; } - // TODO: support FLAT_LIB_EMITTER - default: + case FLAT_LIB_EMITTER: + // cannot happen, handled above + break; + case FLAT_LIB_MEMORY: + // not implemented break; } } @@ -295,7 +403,8 @@ 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, - struct flat_draw_ctx *ctx) + struct flat_draw_ctx *ctx, mat4 root, + struct flat_key_stack *key_stack) { // reverse order for correct z-ordering for (size_t i = nr_timelines; i-- > 0;) { @@ -309,7 +418,7 @@ static void render_flat_layer(struct parts *parts, struct parts_flat *f, if (local >= (int)tl->graphic.count) continue; - render_flat_item(parts, f, state, i, tl, local, ctx); + render_flat_item(parts, f, state, i, tl, local, ctx, root, key_stack); } } @@ -327,8 +436,11 @@ static void parts_render_flat(struct parts *parts, struct parts_flat *f) glm_vec3_zero(ctx.add_color); glm_vec3_one(ctx.mul_color); ctx.draw_filter = PARTS_DRAW_FILTER_NORMAL; + + struct flat_key_stack key_stack = { .count = 0 }; render_flat_layer(parts, f, f->root_state, - f->flat->timelines, f->flat->nr_timelines, &ctx); + f->flat->timelines, f->flat->nr_timelines, + &ctx, ctx.matrix, &key_stack); } static void parts_render_3dlayer(struct parts *parts, struct parts_3dlayer *l) diff --git a/subprojects/libsys4 b/subprojects/libsys4 index 3b909ac..ed74c9e 160000 --- a/subprojects/libsys4 +++ b/subprojects/libsys4 @@ -1 +1 @@ -Subproject commit 3b909ac2159e321250d465f380382f44637b4836 +Subproject commit ed74c9e48e39cadab2b5d9c4adf0f0a3f336d1ce