SealEngine: Add .mpr (motion material keyframes) support

.mpr is an optional sidecar to .mot used by SealEngine, replacing .txa
used in ReignEngine / TapirEngine. While .mot drives bone poses, .mpr
drives material properties on a per-frame timeline.

Supported keys, all keyed by frame number:

  Object{MulAlpha,MulDiffuse,AddAmbient} : applied to the whole model
  Mesh{MulAlpha,MulDiffuse,AddAmbient}   : applied per named mesh
  MeshTextureAnime                       : swap diffuse texture index

Colors and alpha use linear interpolation between keys. Out-of-range
frames hold the nearest endpoint.

Mesh* tracks act as per-mesh tints and do not leak to other meshes
that happen to share a material. MeshTextureAnime is the exception:
it rewrites the shared material's diffuse slot, so all meshes using
that material animate in sync (e.g. in Rance 9, hanny_black_move.mpr
lists only hibana_R but hibana_L animates together).
This commit is contained in:
kichikuou
2026-05-22 13:57:02 +09:00
parent ced4daa331
commit c01a27583c
8 changed files with 388 additions and 17 deletions
+1
View File
@@ -79,6 +79,7 @@ target_sources(xsystem4 PRIVATE
src/3d/collision.c
src/3d/debug.c
src/3d/model.c
src/3d/mpr.c
src/3d/parser.c
src/3d/particle.c
src/3d/reign.c
+4 -3
View File
@@ -87,6 +87,7 @@ uniform bool use_normal_map;
uniform sampler2D normal_texture;
uniform vec3 instance_ambient;
uniform vec3 diffuse_mod;
uniform int diffuse_type;
uniform float shadow_darkness;
uniform float shadow_bias;
@@ -120,20 +121,20 @@ void main() {
vec4 texel;
if (diffuse_type == DIFFUSE_ENV_MAP) {
texel = texture(tex, (vec2(normal.x, -normal.y) + 1.0) / 2.0);
frag_rgb = texel.rgb;
frag_rgb = texel.rgb * diffuse_mod;
} else {
texel = texture(tex, tex_coord);
if (use_blend_texture) {
texel = mix(texel, texture(blend_tex, blend_tex_coord), blend_weight);
}
if (diffuse_type == DIFFUSE_EMISSIVE) {
frag_rgb = texel.rgb;
frag_rgb = texel.rgb * diffuse_mod;
} else {
vec3 diffuse = dir_lights_diffuse(norm);
if (diffuse_type == DIFFUSE_LIGHT_MAP) {
diffuse *= texture(light_texture, light_tex_coord).rgb;
}
frag_rgb += texel.rgb * diffuse * color_mod.rgb;
frag_rgb += texel.rgb * diffuse * color_mod.rgb * diffuse_mod;
}
}
+42
View File
@@ -155,6 +155,7 @@ struct RE_renderer {
GLint has_bones;
GLint global_ambient;
GLint instance_ambient;
GLint diffuse_mod;
struct {
GLint dir;
GLint diffuse;
@@ -515,6 +516,7 @@ struct mot {
uint32_t nr_bones;
uint32_t nr_texture_indices;
uint32_t *texture_indices; // texture index for each frame
struct mpr *mpr; // optional
struct mot_bone *motions[];
};
@@ -568,6 +570,46 @@ struct amt_material *amt_find_material(struct amt *amt, const char *name);
void opr_load(uint8_t *data, size_t size, struct pol *pol);
void txa_load(uint8_t *data, size_t size, struct mot *mot);
// mpr.c
struct mpr_float_key { int frame; float v; };
struct mpr_vec3_key { int frame; vec3 v; };
struct mpr_int_key { int frame; int v; };
struct mpr_track_set {
int nr_mul_alpha;
struct mpr_float_key *mul_alpha;
int nr_mul_diffuse;
struct mpr_vec3_key *mul_diffuse;
int nr_add_ambient;
struct mpr_vec3_key *add_ambient;
int nr_texture_anime;
struct mpr_int_key *texture_anime; // mesh-scope only
};
struct mpr {
int nr_meshes; // == model->nr_meshes
struct mpr_track_set **mesh_tracks; // sparse: indexed by mesh index, NULL if absent
struct mpr_track_set object;
bool has_mesh_alpha; // any mesh_tracks[i]->nr_mul_alpha > 0
};
// Per-draw material modulation.
struct mpr_modulation {
float alpha;
vec3 ambient;
vec3 diffuse;
};
struct mpr *mpr_load(uint8_t *data, size_t size, struct model *model);
void mpr_free(struct mpr *mpr);
void mpr_evaluate_object(const struct mpr *mpr, float frame,
const struct RE_instance *inst, struct mpr_modulation *out);
void mpr_evaluate_mesh(const struct mpr_track_set *mt, float frame,
const struct mpr_modulation *obj, struct mpr_modulation *out);
void mpr_build_mat_tex_index(const struct model *model, const struct RE_instance *inst,
const struct mpr *mpr, float frame, int *mat_tex_index);
// collision.c
struct collider_triangle;
+13 -5
View File
@@ -783,11 +783,19 @@ static struct mot *mot_load(const char *name, struct model *model, struct archiv
}
qsort(mot->motions, mot->nr_bones, sizeof(struct mot_bone *), cmp_motions_by_bone_id);
// Load optional .txa file.
struct archive_data *txa_file = RE_get_aar_entry(aar, model->path, name, ".txa");
if (txa_file) {
txa_load(txa_file->data, txa_file->size, mot);
archive_free_data(txa_file);
// Load optional sidecar file.
if (re_plugin_version <= RE_TAPIR_PLUGIN) {
struct archive_data *txa_file = RE_get_aar_entry(aar, model->path, name, ".txa");
if (txa_file) {
txa_load(txa_file->data, txa_file->size, mot);
archive_free_data(txa_file);
}
} else {
struct archive_data *mpr_file = RE_get_aar_entry(aar, model->path, name, ".mpr");
if (mpr_file) {
mot->mpr = mpr_load(mpr_file->data, mpr_file->size, model);
archive_free_data(mpr_file);
}
}
return mot;
+294
View File
@@ -0,0 +1,294 @@
/* 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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <cglm/cglm.h>
#include "system4.h"
#include "3d_internal.h"
// .mpr parser.
static int resolve_mesh_index(struct model *model, const char *name)
{
for (int i = 0; i < model->nr_meshes; i++) {
if (!strcmp(model->meshes[i].name, name))
return i;
}
return -1;
}
static void append_float_key(struct mpr_track_set *ts, int frame, float v)
{
ts->mul_alpha = xrealloc(ts->mul_alpha, (ts->nr_mul_alpha + 1) * sizeof(struct mpr_float_key));
ts->mul_alpha[ts->nr_mul_alpha].frame = frame;
ts->mul_alpha[ts->nr_mul_alpha].v = v;
ts->nr_mul_alpha++;
}
static void append_vec3_key(struct mpr_vec3_key **arr, int *nr, int frame, float r, float g, float b)
{
*arr = xrealloc(*arr, (*nr + 1) * sizeof(struct mpr_vec3_key));
(*arr)[*nr].frame = frame;
(*arr)[*nr].v[0] = r;
(*arr)[*nr].v[1] = g;
(*arr)[*nr].v[2] = b;
(*nr)++;
}
static void append_int_key(struct mpr_int_key **arr, int *nr, int frame, int v)
{
*arr = xrealloc(*arr, (*nr + 1) * sizeof(struct mpr_int_key));
(*arr)[*nr].frame = frame;
(*arr)[*nr].v = v;
(*nr)++;
}
// All mpr_*_key structs have `int frame` as their first field.
static int cmp_key_by_frame(const void *a, const void *b)
{
return *(const int *)a - *(const int *)b;
}
static void sort_track_set(struct mpr_track_set *ts)
{
if (ts->nr_mul_alpha)
qsort(ts->mul_alpha, ts->nr_mul_alpha, sizeof(struct mpr_float_key), cmp_key_by_frame);
if (ts->nr_mul_diffuse)
qsort(ts->mul_diffuse, ts->nr_mul_diffuse, sizeof(struct mpr_vec3_key), cmp_key_by_frame);
if (ts->nr_add_ambient)
qsort(ts->add_ambient, ts->nr_add_ambient, sizeof(struct mpr_vec3_key), cmp_key_by_frame);
if (ts->nr_texture_anime)
qsort(ts->texture_anime, ts->nr_texture_anime, sizeof(struct mpr_int_key), cmp_key_by_frame);
}
struct mpr *mpr_load(uint8_t *data, size_t size, struct model *model)
{
struct mpr *mpr = xcalloc(1, sizeof(struct mpr));
mpr->nr_meshes = model->nr_meshes;
mpr->mesh_tracks = xcalloc(model->nr_meshes, sizeof(struct mpr_track_set *));
struct mpr_track_set *current = NULL; // current Mesh-scope track set
while (size > 0) {
char line[200];
uint8_t *nl = memchr(data, '\n', size);
if (nl)
nl++;
else
nl = data + size;
int copylen = min(nl - data, sizeof(line) - 1);
memcpy(line, (char *)data, copylen);
line[copylen] = '\0';
size -= nl - data;
data = nl;
char s[200];
int frame, ti;
float a, r, g, b;
if (sscanf(line, "Mesh = \"%[^\"]\"", s) == 1) {
int mi = resolve_mesh_index(model, s);
if (mi < 0) {
WARNING("mpr: unknown mesh \"%s\"", s);
current = NULL;
} else {
if (!mpr->mesh_tracks[mi])
mpr->mesh_tracks[mi] = xcalloc(1, sizeof(struct mpr_track_set));
current = mpr->mesh_tracks[mi];
}
} else if (sscanf(line, "MeshMulAlpha = ( %d , %f )", &frame, &a) == 2) {
if (current)
append_float_key(current, frame, a);
else
WARNING("mpr: MeshMulAlpha outside Mesh scope");
} else if (sscanf(line, "MeshMulDiffuse = ( %d , %f , %f , %f )", &frame, &r, &g, &b) == 4) {
if (current)
append_vec3_key(&current->mul_diffuse, &current->nr_mul_diffuse, frame, r, g, b);
else
WARNING("mpr: MeshMulDiffuse outside Mesh scope");
} else if (sscanf(line, "MeshAddAmbient = ( %d , %f , %f , %f )", &frame, &r, &g, &b) == 4) {
if (current)
append_vec3_key(&current->add_ambient, &current->nr_add_ambient, frame, r, g, b);
else
WARNING("mpr: MeshAddAmbient outside Mesh scope");
} else if (sscanf(line, "MeshTextureAnime = ( %d , %d )", &frame, &ti) == 2) {
if (current)
append_int_key(&current->texture_anime, &current->nr_texture_anime, frame, ti);
else
WARNING("mpr: MeshTextureAnime outside Mesh scope");
} else if (sscanf(line, "ObjectMulAlpha = ( %d , %f )", &frame, &a) == 2) {
append_float_key(&mpr->object, frame, a);
} else if (sscanf(line, "ObjectMulDiffuse = ( %d , %f , %f , %f )", &frame, &r, &g, &b) == 4) {
append_vec3_key(&mpr->object.mul_diffuse, &mpr->object.nr_mul_diffuse, frame, r, g, b);
} else if (sscanf(line, "ObjectAddAmbient = ( %d , %f , %f , %f )", &frame, &r, &g, &b) == 4) {
append_vec3_key(&mpr->object.add_ambient, &mpr->object.nr_add_ambient, frame, r, g, b);
} else if (strchr(line, '=')) {
WARNING("mpr: unknown field: %s", line);
}
}
for (int i = 0; i < mpr->nr_meshes; i++) {
if (!mpr->mesh_tracks[i])
continue;
sort_track_set(mpr->mesh_tracks[i]);
if (mpr->mesh_tracks[i]->nr_mul_alpha > 0)
mpr->has_mesh_alpha = true;
}
sort_track_set(&mpr->object);
return mpr;
}
static void destroy_track_set(struct mpr_track_set *ts)
{
free(ts->mul_alpha);
free(ts->mul_diffuse);
free(ts->add_ambient);
free(ts->texture_anime);
}
void mpr_free(struct mpr *mpr)
{
if (!mpr)
return;
for (int i = 0; i < mpr->nr_meshes; i++) {
if (mpr->mesh_tracks[i]) {
destroy_track_set(mpr->mesh_tracks[i]);
free(mpr->mesh_tracks[i]);
}
}
free(mpr->mesh_tracks);
destroy_track_set(&mpr->object);
free(mpr);
}
// Keyframe evaluation.
// Returns the largest index i with keys[i].frame <= frame.
// Returns -1 if frame < keys[0].frame.
#define DEFINE_FIND_KEY(NAME, KEY_TYPE) \
static int NAME(const KEY_TYPE *keys, int nr, float frame) \
{ \
if (frame < (float)keys[0].frame) \
return -1; \
int lo = 0, hi = nr - 1; \
while (lo < hi) { \
int mid = (lo + hi + 1) / 2; \
if ((float)keys[mid].frame <= frame) \
lo = mid; \
else \
hi = mid - 1; \
} \
return lo; \
}
DEFINE_FIND_KEY(find_float_key, struct mpr_float_key)
DEFINE_FIND_KEY(find_vec3_key, struct mpr_vec3_key)
DEFINE_FIND_KEY(find_int_key, struct mpr_int_key)
static float eval_float(const struct mpr_float_key *keys, int nr, float frame, float fallback)
{
if (nr == 0)
return fallback;
int i = find_float_key(keys, nr, frame);
if (i < 0)
return keys[0].v;
if (i >= nr - 1)
return keys[nr - 1].v;
float t = (frame - keys[i].frame) / (float)(keys[i + 1].frame - keys[i].frame);
return glm_lerp(keys[i].v, keys[i + 1].v, t);
}
static void eval_vec3(const struct mpr_vec3_key *keys, int nr, float frame, const vec3 fallback, vec3 out)
{
if (nr == 0) {
glm_vec3_copy((float *)fallback, out);
return;
}
int i = find_vec3_key(keys, nr, frame);
if (i < 0) {
glm_vec3_copy((float *)keys[0].v, out);
return;
}
if (i >= nr - 1) {
glm_vec3_copy((float *)keys[nr - 1].v, out);
return;
}
float t = (frame - keys[i].frame) / (float)(keys[i + 1].frame - keys[i].frame);
glm_vec3_lerp((float *)keys[i].v, (float *)keys[i + 1].v, t, out);
}
static int eval_int(const struct mpr_int_key *keys, int nr, float frame, int fallback)
{
if (nr == 0)
return fallback;
int i = find_int_key(keys, nr, frame);
if (i < 0)
return keys[0].v;
return keys[i].v; // step
}
// Combine inst defaults with .mpr Object-scope keyframes.
void mpr_evaluate_object(const struct mpr *mpr, float frame,
const struct RE_instance *inst, struct mpr_modulation *out)
{
out->alpha = inst->alpha;
glm_vec3_copy((float *)inst->ambient, out->ambient);
glm_vec3_one(out->diffuse);
if (!mpr)
return;
out->alpha *= eval_float(mpr->object.mul_alpha, mpr->object.nr_mul_alpha, frame, 1.f);
vec3 add;
eval_vec3(mpr->object.add_ambient, mpr->object.nr_add_ambient, frame, (vec3){0,0,0}, add);
glm_vec3_add(out->ambient, add, out->ambient);
eval_vec3(mpr->object.mul_diffuse, mpr->object.nr_mul_diffuse, frame, (vec3){1,1,1}, out->diffuse);
}
// Combine the object-scope values with .mpr Mesh-scope keyframes for one mesh.
void mpr_evaluate_mesh(const struct mpr_track_set *mt, float frame,
const struct mpr_modulation *obj, struct mpr_modulation *out)
{
*out = *obj;
if (!mt)
return;
out->alpha *= eval_float(mt->mul_alpha, mt->nr_mul_alpha, frame, 1.f);
vec3 add;
eval_vec3(mt->add_ambient, mt->nr_add_ambient, frame, (vec3){0,0,0}, add);
glm_vec3_add(out->ambient, add, out->ambient);
vec3 mul;
eval_vec3(mt->mul_diffuse, mt->nr_mul_diffuse, frame, (vec3){1,1,1}, mul);
glm_vec3_mul(out->diffuse, mul, out->diffuse);
}
// MeshTextureAnime writes to the shared material slot, so all meshes
// referencing the same material pick up the animation even if .mpr only names
// one of them. Caller supplies the output buffer sized to model->nr_materials.
void mpr_build_mat_tex_index(const struct model *model, const struct RE_instance *inst,
const struct mpr *mpr, float frame, int *mat_tex_index)
{
for (int m = 0; m < model->nr_materials; m++)
mat_tex_index[m] = inst->texture_animation_index;
if (!mpr)
return;
for (int i = 0; i < mpr->nr_meshes; i++) {
const struct mpr_track_set *t = mpr->mesh_tracks[i];
if (!t || t->nr_texture_anime == 0)
continue;
int mat = model->meshes[i].material;
mat_tex_index[mat] = eval_int(t->texture_anime, t->nr_texture_anime, frame, mat_tex_index[mat]);
}
}
+1
View File
@@ -491,6 +491,7 @@ void mot_free(struct mot *mot)
if (mot->texture_indices) {
free(mot->texture_indices);
}
mpr_free(mot->mpr);
free(mot);
}
+32 -9
View File
@@ -225,6 +225,7 @@ struct RE_renderer *RE_renderer_new(void)
glUniformBlockBinding(r->program, bone_transforms, BONE_TRANSFORMS_BINDING);
r->global_ambient = glGetUniformLocation(r->program, "global_ambient");
r->instance_ambient = glGetUniformLocation(r->program, "instance_ambient");
r->diffuse_mod = glGetUniformLocation(r->program, "diffuse_mod");
for (int i = 0; i < NR_DIR_LIGHTS; i++) {
char buf[64];
sprintf(buf, "dir_lights[%d].dir", i);
@@ -353,18 +354,27 @@ static void render_model(struct RE_instance *inst, struct RE_renderer *r, enum d
struct model *model = inst->model;
if (!inst->model)
return;
bool all_transparent = inst->alpha < 1.0f;
bool all_opaque = !model->has_transparent_mesh && inst->alpha >= 1.0f;
// NOTE: next_motion's .mpr is not sampled.
const struct mpr *mpr = (inst->motion && inst->motion->mot) ? inst->motion->mot->mpr : NULL;
float frame = inst->motion ? inst->motion->current_frame : 0.f;
struct mpr_modulation obj_mod;
mpr_evaluate_object(mpr, frame, inst, &obj_mod);
bool all_transparent = obj_mod.alpha < 1.0f;
bool all_opaque = !model->has_transparent_mesh && obj_mod.alpha >= 1.0f
&& !(mpr && mpr->has_mesh_alpha);
if ((phase == DRAW_OPAQUE && all_transparent) || (phase == DRAW_TRANSPARENT && all_opaque))
return;
int mat_tex_index[model->nr_materials];
mpr_build_mat_tex_index(model, inst, mpr, frame, mat_tex_index);
if (inst->local_transform_needs_update)
RE_instance_update_local_transform(inst);
glUniformMatrix4fv(r->local_transform, 1, GL_FALSE, inst->local_transform[0]);
glUniformMatrix3fv(r->normal_transform, 1, GL_FALSE, inst->normal_transform[0]);
glUniform1f(r->alpha_mod, inst->alpha);
glUniform3fv(r->instance_ambient, 1, inst->ambient);
bool draw_shadow = inst->draw_shadow && inst->plugin->shadow_mode;
if (draw_shadow) {
@@ -378,10 +388,21 @@ static void render_model(struct RE_instance *inst, struct RE_renderer *r, enum d
if (mesh->hidden)
continue;
struct material *material = &model->materials[mesh->material];
bool is_transparent = mesh->is_transparent || inst->alpha < 1.0f;
const struct mpr_track_set *mt = mpr ? mpr->mesh_tracks[i] : NULL;
struct mpr_modulation mesh_mod;
mpr_evaluate_mesh(mt, frame, &obj_mod, &mesh_mod);
if (mesh_mod.alpha <= 0.0f)
continue;
bool is_transparent = mesh->is_transparent || mesh_mod.alpha < 1.0f;
if (phase != (is_transparent ? DRAW_TRANSPARENT : DRAW_OPAQUE))
continue;
glUniform1f(r->alpha_mod, mesh_mod.alpha);
glUniform3fv(r->instance_ambient, 1, mesh_mod.ambient);
glUniform3fv(r->diffuse_mod, 1, mesh_mod.diffuse);
if (mesh->flags & MESH_BLEND_ADDITIVE) {
glBlendFuncSeparate(GL_SRC_ALPHA, GL_ONE, GL_ZERO, GL_ONE);
} else {
@@ -411,8 +432,8 @@ static void render_model(struct RE_instance *inst, struct RE_renderer *r, enum d
glUniform3fv(r->rim_color, 1, material->rim_color);
glActiveTexture(GL_TEXTURE0 + COLOR_TEXTURE_UNIT);
int animation_index = inst->texture_animation_index < material->nr_color_maps
? inst->texture_animation_index : 0;
int tex_index = mat_tex_index[mesh->material];
int animation_index = tex_index < material->nr_color_maps ? tex_index : 0;
glBindTexture(GL_TEXTURE_2D, material->color_maps[animation_index]);
glUniform1i(r->texture, COLOR_TEXTURE_UNIT);
@@ -444,12 +465,12 @@ static void render_model(struct RE_instance *inst, struct RE_renderer *r, enum d
}
if (material->alpha_map) {
glUniform1i(r->alpha_mode, mesh->is_transparent ? ALPHA_MAP_BLEND : ALPHA_MAP_TEST);
glUniform1i(r->alpha_mode, is_transparent ? ALPHA_MAP_BLEND : ALPHA_MAP_TEST);
glActiveTexture(GL_TEXTURE0 + ALPHA_TEXTURE_UNIT);
glBindTexture(GL_TEXTURE_2D, material->alpha_map);
glUniform1i(r->alpha_texture, ALPHA_TEXTURE_UNIT);
} else {
glUniform1i(r->alpha_mode, mesh->is_transparent ? ALPHA_BLEND : ALPHA_TEST);
glUniform1i(r->alpha_mode, is_transparent ? ALPHA_BLEND : ALPHA_TEST);
}
if (material->blend_texture) {
@@ -535,6 +556,7 @@ static void render_billboard(struct RE_instance *inst, struct RE_renderer *r, ma
glm_mat4_pick3(local_transform, normal_transform);
glUniform3fv(r->instance_ambient, 1, inst->ambient);
glUniform3f(r->diffuse_mod, 1.f, 1.f, 1.f);
glUniformMatrix4fv(r->local_transform, 1, GL_FALSE, local_transform[0]);
glUniformMatrix3fv(r->normal_transform, 1, GL_FALSE, normal_transform[0]);
@@ -659,6 +681,7 @@ static void render_particle_effect(struct RE_instance *inst, struct RE_renderer
return;
glUniform3fv(r->instance_ambient, 1, inst->ambient);
glUniform3f(r->diffuse_mod, 1.f, 1.f, 1.f);
glUniform1i(r->has_bones, GL_FALSE);
glUniform1f(r->specular_strength, 0.0);
+1
View File
@@ -40,6 +40,7 @@ xsystem4 = [version_h,
'3d/collision.c',
'3d/debug.c',
'3d/model.c',
'3d/mpr.c',
'3d/parser.c',
'3d/particle.c',
'3d/reign.c',