mirror of
https://github.com/nunuhara/xsystem4.git
synced 2026-09-22 23:18:01 +03:00
Flat: Add particle emitter support
This implements rendering of FLAT_LIB_EMITTER libraries: timed spawning, per-particle motion, fade, and 3-axis rotation. For each visible emitter, render_flat_emitter() builds an emitter base matrix from the ancestor key stack so particles follow their parent timelines. parent_key_mode chooses between filtering ancestor properties via the emitter's inherit_* flags (SELECTIVE) and using ancestor poses wholesale (ANCESTOR_CURRENT / ANCESTOR_AT_BIRTH). For each birth frame in the lifetime window, parts_flat_foreach_emitter_particle() reseeds the RNG from (rand_seed, birth_frame), making particles deterministic across re-renders, and yields each particle's pose, which is composed with the emitter transform and submitted as a quad through the standard parts texture pipeline.
This commit is contained in:
@@ -15,8 +15,12 @@
|
||||
*/
|
||||
|
||||
#include <limits.h>
|
||||
#include <math.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <cglm/cglm.h>
|
||||
|
||||
#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)
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
+146
-34
@@ -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)
|
||||
|
||||
+1
-1
Submodule subprojects/libsys4 updated: 3b909ac215...ed74c9e48e
Reference in New Issue
Block a user