diff --git a/CMakeLists.txt b/CMakeLists.txt index 1bcd818..8447a89 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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 diff --git a/shaders/reign.f.glsl b/shaders/reign.f.glsl index 2299ac5..e09c804 100644 --- a/shaders/reign.f.glsl +++ b/shaders/reign.f.glsl @@ -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; } } diff --git a/src/3d/3d_internal.h b/src/3d/3d_internal.h index 8fdb9ed..798cdc7 100644 --- a/src/3d/3d_internal.h +++ b/src/3d/3d_internal.h @@ -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; diff --git a/src/3d/model.c b/src/3d/model.c index 5c9c6ea..34b46f8 100644 --- a/src/3d/model.c +++ b/src/3d/model.c @@ -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; diff --git a/src/3d/mpr.c b/src/3d/mpr.c new file mode 100644 index 0000000..0de766a --- /dev/null +++ b/src/3d/mpr.c @@ -0,0 +1,294 @@ +/* Copyright (C) 2026 kichikuou + * + * 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 . + */ + +#include +#include +#include +#include + +#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(¤t->mul_diffuse, ¤t->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(¤t->add_ambient, ¤t->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(¤t->texture_anime, ¤t->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]); + } +} diff --git a/src/3d/parser.c b/src/3d/parser.c index a9c057e..08eba45 100644 --- a/src/3d/parser.c +++ b/src/3d/parser.c @@ -491,6 +491,7 @@ void mot_free(struct mot *mot) if (mot->texture_indices) { free(mot->texture_indices); } + mpr_free(mot->mpr); free(mot); } diff --git a/src/3d/renderer.c b/src/3d/renderer.c index 5097204..6500d6e 100644 --- a/src/3d/renderer.c +++ b/src/3d/renderer.c @@ -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); diff --git a/src/meson.build b/src/meson.build index a1a2a2d..0f57f4c 100644 --- a/src/meson.build +++ b/src/meson.build @@ -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',