SealEngine: Add .3de parser, effect framework, camera, and sound

.3de is the particle/effect format used by SealEngine. Like the
ReignEngine's .pae, it is a text format describing an effect as a set of
emitters, but the two differ enough in design that .3de lives in its own
file (s3de.c) rather than being folded into particle.c.

This commit lays the groundwork:

- Parse .3de text.
- Integrate effect loading, playback, and caching into the existing
  infrastructure.
- Apply the camera object's per-frame override to RE_camera.
- Play the companion sound asset.

Billboard and polygon-object rendering will be added separately.
This commit is contained in:
kichikuou
2026-05-31 08:22:01 +09:00
parent 1fcccb766c
commit bf0b2bb44f
7 changed files with 1057 additions and 34 deletions
+1
View File
@@ -84,6 +84,7 @@ target_sources(xsystem4 PRIVATE
src/3d/particle.c
src/3d/reign.c
src/3d/renderer.c
src/3d/s3de.c
src/dungeon/dgn.c
src/dungeon/dtx.c
+8 -1
View File
@@ -84,6 +84,12 @@ struct RE_camera {
vec3 pos;
float pitch, roll, yaw; // in degrees
float quake_pitch, quake_yaw;
// Per-frame full override from an 3de "camera" object. Cleared at the
// start of every RE_build_model tick and re-asserted by s3de_effect_update
// if any active camera object selects this frame.
bool override_active;
vec3 override_pos;
float override_pitch, override_yaw, override_roll;
};
struct RE_back_cg {
@@ -104,7 +110,7 @@ struct RE_plugin {
struct RE_instance **instances;
struct archive *aar;
struct hash_table *model_cache;
struct hash_table *pae_cache;
struct hash_table *effect_cache;
struct RE_renderer *renderer;
struct RE_camera camera;
mat4 proj_transform;
@@ -158,6 +164,7 @@ struct RE_instance {
struct RE_plugin *plugin;
struct model *model;
struct particle_effect *effect;
struct s3de_effect *s3de_effect;
struct motion *motion;
struct motion *next_motion;
struct height_detector *height_detector;
+172
View File
@@ -402,6 +402,178 @@ void particle_effect_free(struct particle_effect *effect);
void particle_effect_update(struct RE_instance *inst);
void particle_object_calc_local_transform(struct RE_instance *inst, struct particle_object *po, struct particle_instance *pi, float frame, mat4 dest);
// s3de.c
// .3de effect system. The 's' prefix is just to avoid an identifier starting
// with a digit; it stands for SealEngine.
enum s3de_object_type {
S3DE_OBJ_BILLBOARD,
S3DE_OBJ_POLYGON,
S3DE_OBJ_CAMERA,
};
enum s3de_move_type {
S3DE_MOVE_LINEAR,
S3DE_MOVE_BEZIER,
};
enum s3de_spawn_position_type {
S3DE_SPAWN_NORMAL,
S3DE_SPAWN_SPHERE,
S3DE_SPAWN_HORIZONTAL,
};
enum s3de_blend_type {
S3DE_BLEND_NORMAL,
S3DE_BLEND_ADDITIVE,
};
enum s3de_emitter_link_type {
S3DE_LINK_NONE = 0, // frozen at spawn-time world pos
S3DE_LINK_FOLLOW = 1, // particle follows current emitter pos
S3DE_LINK_ROTATE_AT_SPAWN = 2, // emitter rotation around spawn pos
S3DE_LINK_ROTATE_AT_CURRENT = 3, // emitter rotation around current pos
S3DE_LINK_MAX = S3DE_LINK_ROTATE_AT_CURRENT
};
enum s3de_direction_type {
S3DE_DIR_RANDOM,
S3DE_DIR_SPECIFIED,
S3DE_DIR_EMITTER,
S3DE_DIR_EMITTER_REVERSE,
S3DE_DIR_EMITTER_COORD,
S3DE_DIR_ARBITRARY_PLANE,
S3DE_DIR_SPAWN,
};
enum s3de_posture_type {
S3DE_POSE_NONE,
S3DE_POSE_CAMERA,
S3DE_POSE_MOVE_DIR,
S3DE_POSE_MOVE_AND_FLY,
S3DE_POSE_FLY_AND_EMITTER_MOVE,
};
enum s3de_interp_type {
S3DE_INTERP_NONE,
S3DE_INTERP_LINEAR,
S3DE_INTERP_SLERP,
};
struct s3de_range {
float base;
float random;
};
struct s3de_scalar_entry {
int frame;
float value;
enum s3de_interp_type interp;
};
struct s3de_vec3_entry {
int frame;
vec3 v;
enum s3de_interp_type interp;
};
struct s3de_position_entry {
int frame;
vec3 pos;
enum s3de_interp_type interp;
float spawn_radius;
float spawn_angle_deg;
};
struct s3de_object {
char *name;
enum s3de_object_type type;
enum s3de_move_type movement_type;
enum s3de_spawn_position_type spawn_position_type;
float spawn_distance;
int particle_count;
char *texture;
enum s3de_blend_type blend_type;
char *polygon_name;
enum s3de_emitter_link_type emitter_link_type;
bool soft_fog_edge;
enum s3de_posture_type posture_type;
enum s3de_direction_type direction_type;
vec3 direction;
float direction_angle;
struct s3de_range start_size, end_size;
struct s3de_range start_x_size, end_x_size;
struct s3de_range start_y_size, end_y_size;
struct s3de_range offset_x, offset_y, offset_z;
struct s3de_range speed, acceleration;
struct s3de_range movement_distance, movement_curve;
bool free_fall;
float mass, air_resistance;
struct s3de_range start_x_rotation, end_x_rotation;
struct s3de_range start_y_rotation, end_y_rotation;
struct s3de_range start_z_rotation, end_z_rotation;
float x_revolution_angle[2], y_revolution_angle[2], z_revolution_angle[2];
float x_revolution_distance[2], y_revolution_distance[2], z_revolution_distance[2];
int child_frame;
float texture_anime_frame;
struct s3de_range alpha_fade_in_frame, alpha_fade_out_frame;
int *damages;
int nr_damages;
struct s3de_position_entry *position_list;
int nr_position_entries;
struct s3de_scalar_entry *size_list;
int nr_size_entries;
struct s3de_scalar_entry *x_size_list;
int nr_x_size_entries;
struct s3de_scalar_entry *y_size_list;
int nr_y_size_entries;
struct s3de_scalar_entry *alpha_list;
int nr_alpha_entries;
struct s3de_vec3_entry *rotation_list;
int nr_rotation_entries;
struct s3de_vec3_entry *multiply_color_list;
int nr_multiply_color_entries;
struct s3de_vec3_entry *additive_color_list;
int nr_additive_color_entries;
struct model *model;
};
struct s3de {
char *path;
int loop_start_frame;
int loop_end_frame;
struct s3de_object *objects;
int nr_objects;
};
struct s3de *s3de_load(struct archive *aar, const char *path);
void s3de_free(struct s3de *s);
struct s3de_effect {
struct s3de *s3de;
int wav_channel; // -1 if no sound
bool sound_started;
float last_frame;
};
struct s3de_effect *s3de_effect_create(struct s3de *s, struct archive *aar);
void s3de_effect_free(struct s3de_effect *eff);
void s3de_effect_update(struct RE_instance *inst);
void s3de_calc_frame_range(struct s3de *s, struct motion *motion);
// parser.c
struct pol {
+55 -25
View File
@@ -77,6 +77,10 @@ static void unload_instance(struct RE_instance *instance)
particle_effect_free(instance->effect);
instance->effect = NULL;
}
if (instance->s3de_effect) {
s3de_effect_free(instance->s3de_effect);
instance->s3de_effect = NULL;
}
if (instance->bone_transforms) {
glDeleteBuffers(1, &instance->bone_transforms_ubo);
free(instance->bone_transforms);
@@ -238,7 +242,7 @@ struct RE_plugin *RE_plugin_new(enum RE_plugin_version version)
plugin->instances = xcalloc(plugin->nr_instances, sizeof(struct RE_instance *));
plugin->aar = aar;
plugin->model_cache = ht_create(32);
plugin->pae_cache = ht_create(32);
plugin->effect_cache = ht_create(32);
for (int i = 0; i < RE_NR_BACK_CGS; i++)
RE_back_cg_init(&plugin->back_cg[i]);
plugin->mag_speed = 1;
@@ -260,8 +264,9 @@ void RE_plugin_free(struct RE_plugin *plugin)
archive_free(plugin->aar);
ht_foreach_value(plugin->model_cache, (void(*)(void*))model_free);
ht_free(plugin->model_cache);
ht_foreach_value(plugin->pae_cache, (void(*)(void*))pae_free);
ht_free(plugin->pae_cache);
ht_foreach_value(plugin->effect_cache, re_plugin_version < RE_SEAL_PLUGIN ?
(void(*)(void*))pae_free : (void(*)(void*))s3de_free);
ht_free(plugin->effect_cache);
for (int i = 0; i < RE_NR_BACK_CGS; i++)
RE_back_cg_destroy(&plugin->back_cg[i]);
xfree_aligned(plugin);
@@ -321,6 +326,7 @@ bool RE_build_model(struct RE_plugin *plugin, int elapsed_ms)
return false;
plugin->camera.quake_pitch = plugin->camera.quake_yaw = 0.0;
plugin->camera.override_active = false;
for (int i = 0; i < plugin->nr_instances; i++) {
struct RE_instance *inst = plugin->instances[i];
@@ -342,7 +348,11 @@ bool RE_build_model(struct RE_plugin *plugin, int elapsed_ms)
// update_bones().
for (int i = 0; i < plugin->nr_instances; i++) {
struct RE_instance *inst = plugin->instances[i];
if (inst && inst->type == RE_ITYPE_PARTICLE_EFFECT)
if (!inst || inst->type != RE_ITYPE_PARTICLE_EFFECT)
continue;
if (inst->s3de_effect)
s3de_effect_update(inst);
else
particle_effect_update(inst);
}
return true;
@@ -469,7 +479,6 @@ bool RE_instance_load(struct RE_instance *instance, const char *name)
if (name[0] == '\0')
return true; // empty name just unloads the instance
struct pae *pae;
switch (instance->type) {
case RE_ITYPE_STATIC:
case RE_ITYPE_SKINNED:
@@ -488,18 +497,34 @@ bool RE_instance_load(struct RE_instance *instance, const char *name)
}
return !!instance->model;
case RE_ITYPE_PARTICLE_EFFECT:
pae = ht_get(instance->plugin->pae_cache, name, NULL);
if (!pae) {
pae = pae_load(instance->plugin->aar, name);
if (!pae)
return false;
ht_put(instance->plugin->pae_cache, name, pae);
}
instance->effect = particle_effect_create(pae);
if (!instance->motion) {
instance->motion = xcalloc(1, sizeof(struct motion));
instance->motion->instance = instance;
pae_calc_frame_range(instance->effect->pae, instance->motion);
if (re_plugin_version >= RE_SEAL_PLUGIN) {
struct s3de *s3de = ht_get(instance->plugin->effect_cache, name, NULL);
if (!s3de) {
s3de = s3de_load(instance->plugin->aar, name);
if (!s3de)
return false;
ht_put(instance->plugin->effect_cache, name, s3de);
}
instance->s3de_effect = s3de_effect_create(s3de, instance->plugin->aar);
if (!instance->motion) {
instance->motion = xcalloc(1, sizeof(struct motion));
instance->motion->instance = instance;
s3de_calc_frame_range(instance->s3de_effect->s3de, instance->motion);
}
} else {
struct pae *pae = ht_get(instance->plugin->effect_cache, name, NULL);
if (!pae) {
pae = pae_load(instance->plugin->aar, name);
if (!pae)
return false;
ht_put(instance->plugin->effect_cache, name, pae);
}
instance->effect = particle_effect_create(pae);
if (!instance->motion) {
instance->motion = xcalloc(1, sizeof(struct motion));
instance->motion->instance = instance;
pae_calc_frame_range(instance->effect->pae, instance->motion);
}
}
return true;
default:
@@ -670,7 +695,7 @@ bool RE_instance_find_path(struct RE_instance *inst, vec3 start, vec3 goal)
if (!inst || !inst->model->collider)
return false;
mat4 vp_transform;
RE_calc_view_matrix(&inst->plugin->camera, GLM_YUP, vp_transform);
RE_calc_view_matrix(&inst->plugin->camera, NULL, vp_transform);
glm_mat4_mul(inst->plugin->proj_transform, vp_transform, vp_transform);
return collider_find_path(inst->model->collider, start, goal, vp_transform);
}
@@ -701,7 +726,7 @@ bool RE_instance_calc_path_finder_intersect_eye_vec(struct RE_instance *inst, in
vec4 far = { ndc_x, ndc_y, 1.f, 1.f };
mat4 inv_vp;
RE_calc_view_matrix(&inst->plugin->camera, GLM_YUP, inv_vp);
RE_calc_view_matrix(&inst->plugin->camera, NULL, inv_vp);
glm_mat4_mul(inst->plugin->proj_transform, inv_vp, inv_vp);
glm_mat4_inv(inv_vp, inv_vp);
@@ -724,7 +749,7 @@ bool RE_plugin_transform_pos_to_view_pos(struct RE_plugin *plugin, float x, floa
return false;
mat4 vp;
RE_calc_view_matrix(&plugin->camera, GLM_YUP, vp);
RE_calc_view_matrix(&plugin->camera, NULL, vp);
glm_mat4_mul(plugin->proj_transform, vp, vp);
vec4 pos = { x, y, z, 1.f };
@@ -746,11 +771,16 @@ bool RE_plugin_get_camera_z_vector(struct RE_plugin *plugin, vec3 out)
if (!plugin)
return false;
vec3 euler = {
glm_rad(plugin->camera.pitch + plugin->camera.quake_pitch),
glm_rad(plugin->camera.yaw + plugin->camera.quake_yaw),
glm_rad(plugin->camera.roll)
};
vec3 euler;
if (plugin->camera.override_active) {
euler[0] = glm_rad(plugin->camera.override_pitch);
euler[1] = glm_rad(plugin->camera.override_yaw);
euler[2] = glm_rad(plugin->camera.override_roll);
} else {
euler[0] = glm_rad(plugin->camera.pitch + plugin->camera.quake_pitch);
euler[1] = glm_rad(plugin->camera.yaw + plugin->camera.quake_yaw);
euler[2] = glm_rad(plugin->camera.roll);
}
mat4 rot;
glm_euler_yxz(euler, rot);
glm_mat4_mulv3(rot, GLM_FORWARD, 0.f, out);
+24 -8
View File
@@ -332,15 +332,30 @@ void RE_renderer_free(struct RE_renderer *r)
void RE_calc_view_matrix(struct RE_camera *camera, vec3 up, mat4 out)
{
vec3 front = { 0.0, 0.0, -1.0 };
vec3 euler = {
glm_rad(camera->pitch + camera->quake_pitch),
glm_rad(camera->yaw + camera->quake_yaw),
glm_rad(camera->roll)
};
vec3 euler;
float *pos;
if (camera->override_active) {
euler[0] = glm_rad(camera->override_pitch);
euler[1] = glm_rad(camera->override_yaw);
euler[2] = glm_rad(camera->override_roll);
pos = camera->override_pos;
} else {
euler[0] = glm_rad(camera->pitch + camera->quake_pitch);
euler[1] = glm_rad(camera->yaw + camera->quake_yaw);
euler[2] = glm_rad(camera->roll);
pos = camera->pos;
}
mat4 rot;
glm_euler_yxz(euler, rot);
glm_mat4_mulv3(rot, front, 0.0, front);
glm_look(camera->pos, front, up, out);
vec3 up_v;
if (up) {
glm_vec3_copy(up, up_v); // Use the provided up vector.
} else {
// Derive the camera's up by rotating world-Y with the same orientation.
glm_mat4_mulv3(rot, GLM_YUP, 0.0, up_v);
}
glm_look(pos, front, up_v, out);
}
static bool should_draw_shadow(struct mesh *mesh, struct material *material)
@@ -892,7 +907,8 @@ static void render_back_cg(struct texture *dst, struct RE_back_cg *bcg, struct R
static void setup_lights(struct RE_plugin *plugin)
{
struct RE_renderer *r = plugin->renderer;
glUniform3fv(r->camera_pos, 1, plugin->camera.pos);
glUniform3fv(r->camera_pos, 1, plugin->camera.override_active
? plugin->camera.override_pos : plugin->camera.pos);
glUniform3fv(r->global_ambient, 1, plugin->global_ambient);
int light_index = 0;
for (int i = 0; i < plugin->nr_instances; i++) {
@@ -1000,7 +1016,7 @@ void RE_render(struct sact_sprite *sp)
glEnable(GL_CULL_FACE);
mat4 view_transform;
RE_calc_view_matrix(&plugin->camera, GLM_YUP, view_transform);
RE_calc_view_matrix(&plugin->camera, NULL, view_transform);
// Tweak the projection transform so that the rendering result is vertically
// flipped. If we render the scene normally, the resulting image will be
+796
View File
@@ -0,0 +1,796 @@
/* Copyright (C) 2026 kichikuou <KichikuouChrome@gmail.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, see <http://gnu.org/licenses/>.
*/
#include <ctype.h>
#include <math.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <cglm/cglm.h>
#include "system4.h"
#include "system4/aar.h"
#include "system4/utfsjis.h"
#include "3d_internal.h"
#include "audio.h"
static void parse_error(const char *msg, char *input)
{
char *eol = strchr(input, '\n');
if (eol) *eol = '\0';
WARNING("Parse error: %s Near: \"%s\"", msg, input);
}
static enum s3de_object_type parse_object_type(const char *s)
{
if (!strcmp(s, "ビルボード"))
return S3DE_OBJ_BILLBOARD;
if (!strcmp(s, "ポリゴンオブジェクト"))
return S3DE_OBJ_POLYGON;
if (!strcmp(s, "カメラ"))
return S3DE_OBJ_CAMERA;
WARNING("unknown object type \"%s\"", s);
return (enum s3de_object_type)-1;
}
static enum s3de_move_type parse_move_type(const char *s)
{
if (!strcmp(s, "直線"))
return S3DE_MOVE_LINEAR;
if (!strcmp(s, "ベジェ"))
return S3DE_MOVE_BEZIER;
WARNING("unknown move type \"%s\"", s);
return (enum s3de_move_type)-1;
}
static enum s3de_spawn_position_type parse_spawn_position_type(const char *s)
{
if (!strcmp(s, "標準"))
return S3DE_SPAWN_NORMAL;
if (!strcmp(s, "球"))
return S3DE_SPAWN_SPHERE;
if (!strcmp(s, "水平面"))
return S3DE_SPAWN_HORIZONTAL;
WARNING("unknown spawn position type \"%s\"", s);
return (enum s3de_spawn_position_type)-1;
}
static enum s3de_blend_type parse_blend_type(const char *s)
{
if (!strcmp(s, "標準"))
return S3DE_BLEND_NORMAL;
if (!strcmp(s, "加算"))
return S3DE_BLEND_ADDITIVE;
WARNING("unknown blend type \"%s\"", s);
return (enum s3de_blend_type)-1;
}
static enum s3de_direction_type parse_direction_type(const char *s)
{
if (!strcmp(s, "ランダム"))
return S3DE_DIR_RANDOM;
if (!strcmp(s, "指定方向"))
return S3DE_DIR_SPECIFIED;
if (!strcmp(s, "エミッタ方向"))
return S3DE_DIR_EMITTER;
if (!strcmp(s, "エミッタ逆方向"))
return S3DE_DIR_EMITTER_REVERSE;
if (!strcmp(s, "エミッタ座標系"))
return S3DE_DIR_EMITTER_COORD;
if (!strcmp(s, "任意平面"))
return S3DE_DIR_ARBITRARY_PLANE;
if (!strcmp(s, "生成方向"))
return S3DE_DIR_SPAWN;
WARNING("unknown direction type \"%s\"", s);
return (enum s3de_direction_type)-1;
}
static enum s3de_posture_type parse_posture_type(const char *s)
{
if (!strcmp(s, "なし"))
return S3DE_POSE_NONE;
if (!strcmp(s, "カメラ"))
return S3DE_POSE_CAMERA;
if (!strcmp(s, "移動方向"))
return S3DE_POSE_MOVE_DIR;
if (!strcmp(s, "移動&飛行方向"))
return S3DE_POSE_MOVE_AND_FLY;
if (!strcmp(s, "飛行&エミッタ移動方向"))
return S3DE_POSE_FLY_AND_EMITTER_MOVE;
WARNING("unknown posture type \"%s\"", s);
return (enum s3de_posture_type)-1;
}
static enum s3de_interp_type parse_interp_type(const char *s)
{
if (!strcmp(s, "なし"))
return S3DE_INTERP_NONE;
if (!strcmp(s, "線形"))
return S3DE_INTERP_LINEAR;
if (!strcmp(s, "球面線形"))
return S3DE_INTERP_SLERP;
WARNING("unknown interp type \"%s\"", s);
return (enum s3de_interp_type)-1;
}
#define MATCH(fmt, ...) (len = 0, sscanf(text, fmt " %n", ##__VA_ARGS__, &len), text += len, len)
#define STRING_BUF_SIZE 256
#define KEYWORD " %255[^, \t\r\n}]"
#define QUOTED_STRING "\"%255[^\"]\""
static char *parse_quoted_string(char **ptext)
{
char *text = *ptext;
while (isspace((unsigned char)*text)) text++;
if (*text++ != '"') {
parse_error("quoted string expected.", text);
return NULL;
}
char *p = strchr(text, '"');
if (!p) {
parse_error("unfinished string.", text);
return NULL;
}
*p = '\0';
*ptext = p + 1;
return utf2sjis(text, 0);
}
// range := float [',' float]
static bool parse_range(char **ptext, struct s3de_range *r)
{
char *text = *ptext;
int len;
float a, b;
if (!MATCH("%f", &a)) {
parse_error("float expected.", text);
return false;
}
r->base = a;
r->random = 0.0f;
if (MATCH(", %f", &b))
r->random = b;
*ptext = text;
return true;
}
static bool parse_two_floats(char **ptext, float out[2])
{
char *text = *ptext;
int len;
if (!MATCH("%f , %f", &out[0], &out[1])) {
parse_error("two floats expected.", text);
return false;
}
*ptext = text;
return true;
}
static bool parse_spin_angle(char **ptext, struct s3de_range *start, struct s3de_range *end)
{
float v[2];
if (!parse_two_floats(ptext, v))
return false;
start->base = v[0];
start->random = 0.0f;
end->base = v[1];
end->random = 0.0f;
return true;
}
static bool parse_vec3(char **ptext, vec3 out)
{
char *text = *ptext;
int len;
if (!MATCH("%f , %f , %f", &out[0], &out[1], &out[2])) {
parse_error("three floats expected.", text);
return false;
}
*ptext = text;
return true;
}
static int *parse_int_list(char **ptext, int *nr)
{
char *text = *ptext;
int len, n;
if (!MATCH("%d", &n)) {
parse_error("int expected.", text);
return NULL;
}
int cap = 16;
int *v = xmalloc(cap * sizeof(int));
v[0] = n;
*nr = 1;
while (MATCH(", %d", &n)) {
if (*nr == cap) {
cap *= 2;
v = xrealloc(v, cap * sizeof(int));
}
v[(*nr)++] = n;
}
*ptext = text;
return v;
}
// Parses a comma-separated list of quoted strings, storing the first non-empty
// one in *out and skipping the rest. Rance 9 only uses the first name.
static bool parse_texture_name(char **ptext, char **out)
{
char *text = *ptext;
int len;
char *result = NULL;
do {
char *s = parse_quoted_string(&text);
if (!s) {
free(result);
return false;
}
if (result || s[0] == '\0')
free(s);
else
result = s;
} while (MATCH(", "));
*ptext = text;
*out = result;
return true;
}
// Parses a list of scalar entries of the form "{ frame, value, interp }, ...".
static bool parse_scalar_entries(char **ptext, struct s3de_scalar_entry **out, int *nr_out)
{
char *text = *ptext;
int len;
int cap = 8, nr = 0;
char buf[STRING_BUF_SIZE];
struct s3de_scalar_entry *v = xmalloc(cap * sizeof(*v));
for (;;) {
if (!MATCH("{ %d , %f , " KEYWORD, &v[nr].frame, &v[nr].value, buf)) {
parse_error("scalar list entry expected.", text);
goto err;
}
int interp = parse_interp_type(buf);
if (interp < 0) goto err;
v[nr].interp = interp;
if (!MATCH("}")) { parse_error("'}' expected.", text); goto err; }
nr++;
if (nr == cap) {
cap *= 2;
v = xrealloc(v, cap * sizeof(*v));
}
if (!MATCH(",")) break;
}
*out = v;
*nr_out = nr;
*ptext = text;
return true;
err:
free(v);
return false;
}
// Parses a list of vec3 entries of the form "{ frame, x, y, z, interp }, ...".
static bool parse_vec3_entries(char **ptext, struct s3de_vec3_entry **out, int *nr_out)
{
char *text = *ptext;
int len;
int cap = 8, nr = 0;
char buf[STRING_BUF_SIZE];
struct s3de_vec3_entry *v = xmalloc(cap * sizeof(*v));
for (;;) {
if (!MATCH("{ %d , %f , %f , %f , " KEYWORD,
&v[nr].frame, &v[nr].v[0], &v[nr].v[1], &v[nr].v[2], buf)) {
parse_error("vec3 list entry expected.", text);
goto err;
}
int interp = parse_interp_type(buf);
if (interp < 0) goto err;
v[nr].interp = interp;
if (!MATCH("}")) { parse_error("'}' expected.", text); goto err; }
nr++;
if (nr == cap) {
cap *= 2;
v = xrealloc(v, cap * sizeof(*v));
}
if (!MATCH(",")) break;
}
*out = v;
*nr_out = nr;
*ptext = text;
return true;
err:
free(v);
return false;
}
// Parses a list of position entries of the form
// "{ frame, x, y, z, interp, [spawn_radius, spawn_angle_deg] }, ...".
static bool parse_position_entries(char **ptext, struct s3de_position_entry **out, int *nr_out)
{
char *text = *ptext;
int len;
int cap = 8, nr = 0;
char buf[STRING_BUF_SIZE];
struct s3de_position_entry *v = xmalloc(cap * sizeof(*v));
for (;;) {
memset(&v[nr], 0, sizeof(v[nr]));
if (!MATCH("{ %d , %f , %f , %f , " KEYWORD,
&v[nr].frame, &v[nr].pos[0], &v[nr].pos[1], &v[nr].pos[2], buf)) {
parse_error("position list entry expected.", text);
goto err;
}
// Convert from left-handed coords to right-handed coords.
v[nr].pos[2] = -v[nr].pos[2];
int interp = parse_interp_type(buf);
if (interp < 0) goto err;
v[nr].interp = interp;
(void)MATCH(", %f , %f", &v[nr].spawn_radius, &v[nr].spawn_angle_deg);
if (!MATCH("}")) { parse_error("'}' expected.", text); goto err; }
nr++;
if (nr == cap) {
cap *= 2;
v = xrealloc(v, cap * sizeof(*v));
}
if (!MATCH(",")) break;
}
*out = v;
*nr_out = nr;
*ptext = text;
return true;
err:
free(v);
return false;
}
static struct s3de *parse_3de(char *text)
{
struct s3de *s = xcalloc(1, sizeof(struct s3de));
s->loop_start_frame = -1;
s->loop_end_frame = -1;
struct s3de_object *current = NULL;
for (;;) {
// Skip whitespace and // comments.
for (;;) {
while (isspace((unsigned char)*text)) text++;
if (text[0] == '/' && text[1] == '/') {
char *nl = strchr(text, '\n');
text = nl ? nl + 1 : text + strlen(text);
} else {
break;
}
}
int len;
int n;
float f;
char buf[STRING_BUF_SIZE];
if (!current) {
if (!*text) return s;
if (MATCH("ループ開始フレーム = %d", &n)) {
s->loop_start_frame = n;
} else if (MATCH("ループ終了フレーム = %d", &n)) {
s->loop_end_frame = n;
} else if (MATCH("オブジェクト " QUOTED_STRING " = {", buf)) {
s->objects = xrealloc_array(s->objects, s->nr_objects,
s->nr_objects + 1, sizeof(struct s3de_object));
current = &s->objects[s->nr_objects++];
current->name = utf2sjis(buf, 0);
current->texture_anime_frame = 1.0f;
} else {
parse_error("unknown toplevel token.", text);
goto err;
}
continue;
}
if (!*text) {
WARNING("unexpected EOF inside object");
goto err;
}
if (MATCH("}")) {
current = NULL;
} else if (MATCH("種類 = " KEYWORD, buf)) {
int v = parse_object_type(buf);
if (v < 0) goto err;
current->type = v;
} else if (MATCH("移動タイプ = " KEYWORD, buf)) {
int v = parse_move_type(buf);
if (v < 0) goto err;
current->movement_type = v;
} else if (MATCH("生成位置タイプ = " KEYWORD, buf)) {
int v = parse_spawn_position_type(buf);
if (v < 0) goto err;
current->spawn_position_type = v;
} else if (MATCH("生成距離 = %f", &f)) {
current->spawn_distance = f;
} else if (MATCH("個数 = %d", &n)) {
current->particle_count = n;
} else if (MATCH("テクスチャ = ")) {
if (!parse_texture_name(&text, &current->texture)) goto err;
} else if (MATCH("ブレンドタイプ = " KEYWORD, buf)) {
int v = parse_blend_type(buf);
if (v < 0) goto err;
current->blend_type = v;
} else if (MATCH("テクスチャアニメフレーム = %f", &f)) {
current->texture_anime_frame = f;
} else if (MATCH("ポリゴン = ")) {
current->polygon_name = parse_quoted_string(&text);
if (!current->polygon_name) goto err;
} else if (MATCH("フレーム参照 = " KEYWORD, buf)) {
// Every emitter in Rance 9 uses "エフェクト".
if (strcmp(buf, "エフェクト") != 0)
WARNING("unhandled frame_reference \"%s\"", buf);
} else if (MATCH("エミッタ連動タイプ = %d", &n)) {
if (n < 0 || n > S3DE_LINK_MAX) {
WARNING("unknown emitter_link_type %d", n);
n = S3DE_LINK_NONE;
}
current->emitter_link_type = n;
} else if (MATCH("ソフトフォグエッジ = %d", &n)) {
current->soft_fog_edge = (n != 0);
} else if (MATCH("姿勢タイプ = " KEYWORD, buf)) {
int v = parse_posture_type(buf);
if (v < 0) goto err;
current->posture_type = v;
} else if (MATCH("ターゲット名 = ")) {
// Unused.
char *s = parse_quoted_string(&text);
if (!s) goto err;
free(s);
} else if (MATCH("方向タイプ = " KEYWORD, buf)) {
int v = parse_direction_type(buf);
if (v < 0) goto err;
current->direction_type = v;
} else if (MATCH("方向 = ")) {
if (!parse_vec3(&text, current->direction))
goto err;
current->direction[2] = -current->direction[2];
} else if (MATCH("方向角度 = %f", &f)) {
current->direction_angle = f;
}
else if (MATCH("開始サイズ = ")) { if (!parse_range(&text, &current->start_size)) goto err; }
else if (MATCH("終了サイズ = ")) { if (!parse_range(&text, &current->end_size)) goto err; }
else if (MATCH("開始Xサイズ = ")) { if (!parse_range(&text, &current->start_x_size)) goto err; }
else if (MATCH("終了Xサイズ = ")) { if (!parse_range(&text, &current->end_x_size)) goto err; }
else if (MATCH("開始Yサイズ = ")) { if (!parse_range(&text, &current->start_y_size)) goto err; }
else if (MATCH("終了Yサイズ = ")) { if (!parse_range(&text, &current->end_y_size)) goto err; }
else if (MATCH("オフセットX = ")) { if (!parse_range(&text, &current->offset_x)) goto err; }
else if (MATCH("オフセットY = ")) { if (!parse_range(&text, &current->offset_y)) goto err; }
else if (MATCH("オフセットZ = ")) {
if (!parse_range(&text, &current->offset_z))
goto err;
current->offset_z.base = -current->offset_z.base;
}
else if (MATCH("速度 = ")) { if (!parse_range(&text, &current->speed)) goto err; }
else if (MATCH("加速度 = ")) { if (!parse_range(&text, &current->acceleration)) goto err; }
else if (MATCH("移動距離 = ")) { if (!parse_range(&text, &current->movement_distance)) goto err; }
else if (MATCH("移動カーブ = ")) { if (!parse_range(&text, &current->movement_curve)) goto err; }
else if (MATCH("自由落下 = %d", &n)) { current->free_fall = (n != 0); }
else if (MATCH("質量 = %f", &f)) { current->mass = f; }
else if (MATCH("空気抵抗係数 = %f", &f)) { current->air_resistance = f; }
else if (MATCH("開始X回転角度 = ")) { if (!parse_range(&text, &current->start_x_rotation)) goto err; }
else if (MATCH("終了X回転角度 = ")) { if (!parse_range(&text, &current->end_x_rotation)) goto err; }
else if (MATCH("開始Y回転角度 = ")) { if (!parse_range(&text, &current->start_y_rotation)) goto err; }
else if (MATCH("終了Y回転角度 = ")) { if (!parse_range(&text, &current->end_y_rotation)) goto err; }
else if (MATCH("開始Z回転角度 = ")) { if (!parse_range(&text, &current->start_z_rotation)) goto err; }
else if (MATCH("終了Z回転角度 = ")) { if (!parse_range(&text, &current->end_z_rotation)) goto err; }
else if (MATCH("X軸自転角度 = ")) { if (!parse_spin_angle(&text, &current->start_x_rotation, &current->end_x_rotation)) goto err; }
else if (MATCH("Y軸自転角度 = ")) { if (!parse_spin_angle(&text, &current->start_y_rotation, &current->end_y_rotation)) goto err; }
else if (MATCH("Z軸自転角度 = ")) { if (!parse_spin_angle(&text, &current->start_z_rotation, &current->end_z_rotation)) goto err; }
else if (MATCH("X軸公転角度 = ")) { if (!parse_two_floats(&text, current->x_revolution_angle)) goto err; }
else if (MATCH("Y軸公転角度 = ")) { if (!parse_two_floats(&text, current->y_revolution_angle)) goto err; }
else if (MATCH("Z軸公転角度 = ")) { if (!parse_two_floats(&text, current->z_revolution_angle)) goto err; }
else if (MATCH("X軸公転距離 = ")) { if (!parse_two_floats(&text, current->x_revolution_distance)) goto err; }
else if (MATCH("Y軸公転距離 = ")) { if (!parse_two_floats(&text, current->y_revolution_distance)) goto err; }
else if (MATCH("Z軸公転距離 = ")) { if (!parse_two_floats(&text, current->z_revolution_distance)) goto err; }
else if (MATCH("カーブX距離 = %f", &f)) { /* unused */ }
else if (MATCH("カーブY距離 = %f", &f)) { /* unused */ }
else if (MATCH("カーブZ距離 = %f", &f)) { /* unused */ }
else if (MATCH("子フレーム = %d", &n)) { current->child_frame = n; }
else if (MATCH("子傾き = %f", &f)) { /* unused */ }
else if (MATCH("子傾き2 = %f", &f)) { /* unused */ }
else if (MATCH("子移動方向タイプ = %d", &n)) { /* unused */ }
else if (MATCH("アップベクトルタイプ = %d", &n)) { /* unused */ }
else if (MATCH("停止フレーム = %d", &n)) { /* unused */ }
else if (MATCH("アルファフェードインフレーム = ")) { if (!parse_range(&text, &current->alpha_fade_in_frame)) goto err; }
else if (MATCH("アルファフェードアウトフレーム = ")) { if (!parse_range(&text, &current->alpha_fade_out_frame)) goto err; }
else if (MATCH("ダメージ = ")) {
current->damages = parse_int_list(&text, &current->nr_damages);
if (!current->damages)
goto err;
}
else if (MATCH("位置リスト = ")) {
if (!parse_position_entries(&text, &current->position_list, &current->nr_position_entries))
goto err;
}
else if (MATCH("サイズリスト = ")) {
if (!parse_scalar_entries(&text, &current->size_list, &current->nr_size_entries))
goto err;
}
else if (MATCH("Xサイズリスト = ")) {
if (!parse_scalar_entries(&text, &current->x_size_list, &current->nr_x_size_entries))
goto err;
}
else if (MATCH("Yサイズリスト = ")) {
if (!parse_scalar_entries(&text, &current->y_size_list, &current->nr_y_size_entries))
goto err;
}
else if (MATCH("アルファリスト = ")) {
if (!parse_scalar_entries(&text, &current->alpha_list, &current->nr_alpha_entries))
goto err;
}
else if (MATCH("回転リスト = ")) {
if (!parse_vec3_entries(&text, &current->rotation_list, &current->nr_rotation_entries))
goto err;
}
else if (MATCH("乗算カラーリスト = ")) {
if (!parse_vec3_entries(&text, &current->multiply_color_list, &current->nr_multiply_color_entries))
goto err;
}
else if (MATCH("加算カラーリスト = ")) {
if (!parse_vec3_entries(&text, &current->additive_color_list, &current->nr_additive_color_entries))
goto err;
}
else {
parse_error("unknown object field.", text);
goto err;
}
}
err:
s3de_free(s);
return NULL;
}
struct s3de *s3de_load(struct archive *aar, const char *path)
{
const char *basename = strrchr(path, '\\');
basename = basename ? basename + 1 : path;
struct archive_data *dfile = RE_get_aar_entry(aar, path, basename, ".3de");
if (!dfile) {
WARNING("%s\\%s.3de: not found", path, basename);
return NULL;
}
char *sjis_text = xmalloc(dfile->size + 1);
memcpy(sjis_text, dfile->data, dfile->size);
sjis_text[dfile->size] = '\0';
char *text = sjis2utf(sjis_text, dfile->size);
free(sjis_text);
struct s3de *s = parse_3de(text);
free(text);
if (!s) {
WARNING("could not parse %s", dfile->name);
archive_free_data(dfile);
return NULL;
}
archive_free_data(dfile);
s->path = strdup(path);
return s;
}
void s3de_free(struct s3de *s)
{
if (!s) return;
for (int i = 0; i < s->nr_objects; i++) {
struct s3de_object *o = &s->objects[i];
free(o->name);
free(o->texture);
free(o->polygon_name);
free(o->damages);
free(o->position_list);
free(o->size_list);
free(o->x_size_list);
free(o->y_size_list);
free(o->alpha_list);
free(o->rotation_list);
free(o->multiply_color_list);
free(o->additive_color_list);
}
free(s->objects);
free(s->path);
free(s);
}
static void interp_vec3(struct s3de_vec3_entry *list, int n, float frame, vec3 dest, vec3 def)
{
if (n == 0) {
glm_vec3_copy(def, dest);
return;
}
if (frame <= list[0].frame) {
glm_vec3_copy(list[0].v, dest);
return;
}
if (frame >= list[n - 1].frame) {
glm_vec3_copy(list[n - 1].v, dest);
return;
}
int k = 0;
while (k + 1 < n && list[k + 1].frame <= frame)
k++;
struct s3de_vec3_entry *a = &list[k];
struct s3de_vec3_entry *b = &list[k + 1];
if (a->interp == S3DE_INTERP_NONE) {
glm_vec3_copy(a->v, dest);
return;
}
float t = (frame - a->frame) / (float)(b->frame - a->frame);
glm_vec3_lerp(a->v, b->v, t, dest);
}
static void interp_pos(struct s3de_position_entry *list, int n, float frame, vec3 dest)
{
if (n == 0) {
glm_vec3_zero(dest);
return;
}
if (frame <= list[0].frame) {
glm_vec3_copy(list[0].pos, dest);
return;
}
if (frame >= list[n - 1].frame) {
glm_vec3_copy(list[n - 1].pos, dest);
return;
}
int k = 0;
while (k + 1 < n && list[k + 1].frame <= frame)
k++;
struct s3de_position_entry *a = &list[k];
struct s3de_position_entry *b = &list[k + 1];
if (a->interp == S3DE_INTERP_NONE) {
glm_vec3_copy(a->pos, dest);
return;
}
float t = (frame - a->frame) / (float)(b->frame - a->frame);
glm_vec3_lerp(a->pos, b->pos, t, dest);
}
struct s3de_effect *s3de_effect_create(struct s3de *s, struct archive *aar)
{
struct s3de_effect *eff = xcalloc(1, sizeof(struct s3de_effect));
eff->s3de = s;
eff->wav_channel = -1;
const char *basename = strrchr(s->path, '\\');
basename = basename ? basename + 1 : s->path;
struct archive_data *dfile = RE_get_aar_entry(aar, s->path, basename, ".wav");
if (dfile) {
int ch = wav_get_unused_channel();
if (wav_prepare_from_archive_data(ch, dfile)) {
eff->wav_channel = ch;
} else {
WARNING("wav_prepare_from_archive_data failed for %s\\%s.wav", s->path, basename);
}
}
return eff;
}
void s3de_effect_free(struct s3de_effect *eff)
{
if (!eff)
return;
if (eff->wav_channel >= 0) {
wav_stop(eff->wav_channel);
wav_unprepare(eff->wav_channel);
}
free(eff);
}
// Pick the camera object whose position_list frame range contains `frame`.
// If `frame` is past every range, pick the camera with the latest last_frame.
// If `frame` precedes every range, return -1.
static int select_active_camera_obj(struct s3de_effect *eff, float frame)
{
int best = -1;
float best_last = -INFINITY, best_first = -INFINITY;
for (int i = 0; i < eff->s3de->nr_objects; i++) {
struct s3de_object *obj = &eff->s3de->objects[i];
if (obj->type != S3DE_OBJ_CAMERA || obj->nr_position_entries == 0)
continue;
float first = obj->position_list[0].frame;
float last = obj->position_list[obj->nr_position_entries - 1].frame;
if (first > frame)
continue;
if (frame < last)
return i;
if (last > best_last || (last == best_last && first > best_first)) {
best = i;
best_last = last;
best_first = first;
}
}
return best;
}
static void update_sound(struct s3de_effect *eff, struct motion *m)
{
if (eff->wav_channel < 0)
return;
bool stopped = (m->state == RE_MOTION_STATE_STOP);
float frame = m->current_frame;
if (eff->sound_started && (stopped || frame + 0.5f < eff->last_frame)) {
wav_stop(eff->wav_channel);
eff->sound_started = false;
}
if (!eff->sound_started && !stopped) {
int ms = (int)(frame * 1000.0f / 30.0f);
if (ms > 0)
wav_seek(eff->wav_channel, ms);
wav_play(eff->wav_channel);
eff->sound_started = true;
}
eff->last_frame = frame;
}
static void update_camera_override(struct RE_instance *inst, struct s3de_effect *eff, float frame)
{
int cam_idx = select_active_camera_obj(eff, frame);
if (cam_idx < 0 || !inst->plugin)
return;
struct s3de_object *obj = &eff->s3de->objects[cam_idx];
vec3 emitter_pos, emitter_rotation_deg;
interp_pos(obj->position_list, obj->nr_position_entries, frame, emitter_pos);
interp_vec3(obj->rotation_list, obj->nr_rotation_entries, frame,
emitter_rotation_deg, GLM_VEC3_ZERO);
if (inst->local_transform_needs_update)
RE_instance_update_local_transform(inst);
vec4 local_pos = { emitter_pos[0], emitter_pos[1], emitter_pos[2], 1.0f };
vec4 world_pos;
glm_mat4_mulv(inst->local_transform, local_pos, world_pos);
struct RE_camera *cam = &inst->plugin->camera;
glm_vec3_copy(world_pos, cam->override_pos);
// XXX: Compose the instance and camera-object rotations by summing Euler
// angles directly. This is not equivalent to true rotation composition in
// general, but it is exact here: in Rance 9 non-camera instances are only
// ever rotated about yaw. Since yaw is the outermost axis in the renderer's
// YXZ convention, the two yaw rotations combine exactly by addition.
cam->override_pitch = inst->pitch + emitter_rotation_deg[0];
cam->override_yaw = inst->yaw + emitter_rotation_deg[1];
cam->override_roll = -(inst->roll + emitter_rotation_deg[2]);
cam->override_active = true;
}
void s3de_effect_update(struct RE_instance *inst)
{
struct s3de_effect *eff = inst->s3de_effect;
if (!eff || !inst->motion)
return;
update_sound(eff, inst->motion);
float frame = inst->motion->current_frame;
update_camera_override(inst, eff, frame);
}
void s3de_calc_frame_range(struct s3de *s, struct motion *motion)
{
int begin = s->loop_start_frame >= 0 ? s->loop_start_frame : 0;
int end = s->loop_end_frame >= 0 ? s->loop_end_frame : begin;
motion->frame_begin = motion->loop_frame_begin = begin;
motion->frame_end = motion->loop_frame_end = end;
}
+1
View File
@@ -45,6 +45,7 @@ xsystem4 = [version_h,
'3d/particle.c',
'3d/reign.c',
'3d/renderer.c',
'3d/s3de.c',
'dungeon/dgn.c',
'dungeon/dtx.c',