完善客户端功能并同步差距文档

This commit is contained in:
shenlei
2026-09-03 18:40:01 +09:00
parent f93a172296
commit 13ccb8d02d
135 changed files with 21881 additions and 1259 deletions
+290 -3
View File
@@ -1,8 +1,16 @@
#include "environment_builder.h"
#include "asset_io.h"
#include "dxt.h"
#include <godot_cpp/classes/environment.hpp>
#include <godot_cpp/classes/image.hpp>
#include <godot_cpp/classes/image_texture.hpp>
#include <godot_cpp/classes/procedural_sky_material.hpp>
#include <godot_cpp/classes/shader.hpp>
#include <godot_cpp/classes/shader_material.hpp>
#include <godot_cpp/classes/sky.hpp>
#include <godot_cpp/classes/texture2d.hpp>
#include <godot_cpp/core/object.hpp>
#include <m2_coord.h>
@@ -18,9 +26,158 @@ namespace {
Color rgba(const fmt::Rgba &c) { return Color(c[0], c[1], c[2], c[3]); }
float luma(const fmt::Rgba &c) { return 0.2126f * c[0] + 0.7152f * c[1] + 0.0722f * c[2]; }
const char *SRC_SKYBOX = R"(shader_type sky;
render_mode use_debanding;
uniform sampler2D front_tex : source_color, filter_linear_mipmap_anisotropic;
uniform sampler2D back_tex : source_color, filter_linear_mipmap_anisotropic;
uniform sampler2D left_tex : source_color, filter_linear_mipmap_anisotropic;
uniform sampler2D right_tex : source_color, filter_linear_mipmap_anisotropic;
uniform sampler2D top_tex : source_color, filter_linear_mipmap_anisotropic;
uniform sampler2D bottom_tex : source_color, filter_linear_mipmap_anisotropic;
uniform bool has_front = false;
uniform bool has_back = false;
uniform bool has_left = false;
uniform bool has_right = false;
uniform bool has_top = false;
uniform bool has_bottom = false;
uniform vec4 gradient_top = vec4(0.2, 0.3, 0.6, 1.0);
uniform vec4 gradient_horizon = vec4(0.5, 0.6, 0.8, 1.0);
uniform vec4 gradient_bottom = vec4(0.2, 0.2, 0.25, 1.0);
uniform sampler2D cloud_tex : source_color, repeat_enable, filter_linear_mipmap_anisotropic;
uniform bool has_cloud = false;
uniform vec2 cloud_scale = vec2(2000.0, 2000.0);
uniform float cloud_height = 300.0;
uniform vec2 cloud_texture_scale = vec2(4.0, 4.0);
uniform vec2 cloud_speed = vec2(0.001, 0.001);
uniform vec4 cloud_tint = vec4(0.0, 0.0, 0.0, 0.0);
vec3 gradient_color(vec3 direction) {
float height = clamp(direction.y * 0.5 + 0.5, 0.0, 1.0);
if (height < 0.5)
return mix(gradient_bottom.rgb, gradient_horizon.rgb, height * 2.0);
return mix(gradient_horizon.rgb, gradient_top.rgb, (height - 0.5) * 2.0);
}
vec3 apply_cloud(vec3 color, vec3 direction) {
if (!has_cloud || direction.y <= 0.001 || cloud_height <= 0.0 ||
cloud_scale.x <= 0.001 || cloud_scale.y <= 0.001)
return color;
// ClientVS22 renders a finite horizontal quad at camera.z + CloudHeight.
// After the Metin2 -> Godot conversion this is camera.y + height. The
// source quad maps source +Y -> U=0 and source +X -> V=1; source Y is
// Godot -Z, so keep that orientation here.
float distance_to_cloud = cloud_height / direction.y;
vec2 plane = direction.xz * distance_to_cloud;
if (abs(plane.x) > cloud_scale.x || abs(plane.y) > cloud_scale.y)
return color;
vec2 uv = vec2(
0.5 + 0.5 * plane.y / cloud_scale.y,
0.5 + 0.5 * plane.x / cloud_scale.x);
uv = fract(uv * cloud_texture_scale + TIME * cloud_speed);
// SkyBox.cpp uses MODULATEINVALPHA_ADDCOLOR followed by ONE /
// INVSRCCOLOR blending: cloud.rgb + sky * (1 - cloud.rgb).
vec4 texel = texture(cloud_tex, uv);
vec3 cloud_rgb = clamp(texel.rgb * (1.0 - cloud_tint.a) + cloud_tint.rgb,
0.0, 1.0);
return cloud_rgb + color * (1.0 - cloud_rgb);
}
void sky() {
vec3 direction = normalize(EYEDIR);
vec3 color = gradient_color(direction);
float ax = abs(direction.x);
float ay = abs(direction.y);
float az = abs(direction.z);
vec2 uv;
// ClientVS22 uses Metin2 Z-up faces. After x,z,-y conversion:
// front=+Z, back=-Z, left=+X, right=-X, top=+Y, bottom=-Y.
if (az >= ax && az >= ay) {
if (direction.z > 0.0) {
uv = vec2(0.5 - 0.5 * direction.x / az,
0.5 - 0.5 * direction.y / az);
if (has_front)
color = texture(front_tex, uv).rgb;
} else {
uv = vec2(0.5 + 0.5 * direction.x / az,
0.5 - 0.5 * direction.y / az);
if (has_back)
color = texture(back_tex, uv).rgb;
}
} else if (ax >= ay) {
if (direction.x > 0.0) {
uv = vec2(0.5 + 0.5 * direction.z / ax,
0.5 - 0.5 * direction.y / ax);
if (has_left)
color = texture(left_tex, uv).rgb;
} else {
uv = vec2(0.5 - 0.5 * direction.z / ax,
0.5 - 0.5 * direction.y / ax);
if (has_right)
color = texture(right_tex, uv).rgb;
}
} else if (direction.y > 0.0) {
uv = vec2(0.5 - 0.5 * direction.x / ay,
0.5 - 0.5 * direction.z / ay);
if (has_top)
color = texture(top_tex, uv).rgb;
} else {
uv = vec2(0.5 - 0.5 * direction.x / ay,
0.5 - 0.5 * direction.z / ay);
if (has_bottom)
color = texture(bottom_tex, uv).rgb;
}
COLOR = apply_cloud(color, direction);
}
)";
Ref<ImageTexture> solid_texture(const Color &color) {
PackedByteArray pixels;
pixels.resize(4);
uint8_t *p = pixels.ptrw();
p[0] = (uint8_t)std::round(std::clamp(color.r, 0.0f, 1.0f) * 255.0f);
p[1] = (uint8_t)std::round(std::clamp(color.g, 0.0f, 1.0f) * 255.0f);
p[2] = (uint8_t)std::round(std::clamp(color.b, 0.0f, 1.0f) * 255.0f);
p[3] = (uint8_t)std::round(std::clamp(color.a, 0.0f, 1.0f) * 255.0f);
Ref<godot::Image> image = godot::Image::create_from_data(
1, 1, false, godot::Image::FORMAT_RGBA8, pixels);
image->generate_mipmaps();
return ImageTexture::create_from_image(image);
}
Ref<ImageTexture> load_texture(const std::string &name, const fmt::AssetResolver *resolver) {
if (name.empty() || resolver == nullptr)
return Ref<ImageTexture>();
const std::string path = resolver->resolve(name, nullptr);
if (path.empty())
return Ref<ImageTexture>();
Ref<godot::Image> image;
const String godot_path(path.c_str());
if (godot_path.get_extension().to_lower() == "dds") {
const mtgodot::Image decoded = mtgodot::dds_from_file(godot_path);
if (!decoded.ok())
return Ref<ImageTexture>();
PackedByteArray pixels;
pixels.resize((int64_t)decoded.rgba.size());
std::copy(decoded.rgba.begin(), decoded.rgba.end(), pixels.ptrw());
image = godot::Image::create_from_data(decoded.w, decoded.h, false,
godot::Image::FORMAT_RGBA8, pixels);
} else {
image = godot::Image::load_from_file(godot_path);
}
if (image.is_null() || image->is_empty())
return Ref<ImageTexture>();
image->generate_mipmaps();
return ImageTexture::create_from_image(image);
}
} // namespace
EnvNodes apply_environment(const fmt::Environment &env, Node *parent) {
EnvNodes apply_environment(const fmt::Environment &env, Node *parent,
const fmt::AssetResolver *resolver) {
EnvNodes out;
// --- DirectionalLight (Background) ---
@@ -30,6 +187,8 @@ EnvNodes apply_environment(const fmt::Environment &env, Node *parent) {
out.sun->set_name("Sun");
parent->add_child(out.sun);
}
out.sun->set_cull_mask(1u << 0); // background / terrain layer
out.sun->set_visible(env.dir_light.bg_enable);
// .msenv Direction 是 Metin2 Z-up 向量(光传播方向)。
fmt::m2coord::Vec3 d = fmt::m2coord::direction_to_godot(
env.dir_light.direction[0], env.dir_light.direction[1], env.dir_light.direction[2]);
@@ -56,6 +215,26 @@ EnvNodes apply_environment(const fmt::Environment &env, Node *parent) {
// 角色/物体的间接补光:暖色,能量取自 Material.Ambient
out.sun->set_param(Light3D::PARAM_SPECULAR, 0.4f);
// ClientVS22 keeps the character light separate from the background light
// (MapOutdoor::OnBeginEnvironment -> SpeedTree::SetLight). Godot's light
// cull mask is the closest exact scene-level equivalent: static world
// geometry is layer 1, Metin2Model geometry is layer 2.
out.character_light = Object::cast_to<DirectionalLight3D>(
parent->get_node_or_null(NodePath("CharacterLight")));
if (!out.character_light) {
out.character_light = memnew(DirectionalLight3D);
out.character_light->set_name("CharacterLight");
parent->add_child(out.character_light);
}
out.character_light->set_cull_mask(1u << 1);
out.character_light->set_visible(env.dir_light.ch_enable);
out.character_light->set_transform(Transform3D(b, Vector3(0, 0, 0)));
out.character_light->set_color(rgba(env.dir_light.ch_diffuse));
out.character_light->set_param(Light3D::PARAM_ENERGY,
std::clamp(0.9f + luma(env.dir_light.ch_diffuse) * 0.4f, 0.7f, 1.6f));
out.character_light->set_shadow(false);
out.character_light->set_param(Light3D::PARAM_SPECULAR, 0.4f);
// --- WorldEnvironment ---
out.world_env =
Object::cast_to<WorldEnvironment>(parent->get_node_or_null(NodePath("WorldEnv")));
@@ -68,7 +247,12 @@ EnvNodes apply_environment(const fmt::Environment &env, Node *parent) {
if (e.is_null())
e.instantiate();
// 天空:SkyBox Gradient -> ProceduralSky 的三段色(zenith / horizon / ground)
// 天空:SkyBox Gradient -> ProceduralSky 的三段色(zenith / horizon / ground)。
// 参考端的 texture mode 是独立六面 quad;Godot Sky shader 用 EYEDIR 采样
// 同一套六面图,坐标和 UV 依照 EterLib/SkyBox.cpp 的面顶点顺序转换。
int sky_face_count = 0;
bool sky_texture_applied = false;
bool sky_cloud_applied = false;
{
Ref<ProceduralSkyMaterial> psm;
psm.instantiate();
@@ -83,7 +267,79 @@ EnvNodes apply_environment(const fmt::Environment &env, Node *parent) {
psm->set_sun_angle_max(6.0f);
Ref<Sky> sky;
sky.instantiate();
sky->set_material(psm);
std::array<Ref<ImageTexture>, 6> faces;
std::array<bool, 6> face_loaded{};
for (int i = 0; i < 6; ++i) {
if (env.sky.face_textures[i].empty())
continue;
faces[i] = load_texture(env.sky.face_textures[i], resolver);
face_loaded[i] = faces[i].is_valid();
if (face_loaded[i])
++sky_face_count;
}
Ref<ImageTexture> cloud_texture;
if (!env.sky.cloud_texture.empty())
cloud_texture = load_texture(env.sky.cloud_texture, resolver);
const bool use_texture_sky = env.sky.texture_render_mode && sky_face_count > 0;
sky_texture_applied = use_texture_sky;
if (use_texture_sky) {
Ref<Shader> shader;
shader.instantiate();
shader->set_code(String(SRC_SKYBOX));
Ref<ShaderMaterial> material;
material.instantiate();
material->set_shader(shader);
Color fallback = g.empty() ? Color(0.35f, 0.45f, 0.7f, 1.0f) : rgba(g.front());
Color horizon = g.size() < 2 ? fallback : rgba(g[g.size() / 2]);
Color ground = g.empty() ? Color(0.2f, 0.2f, 0.25f, 1.0f) : rgba(g.back());
material->set_shader_parameter("gradient_top", fallback);
material->set_shader_parameter("gradient_horizon", horizon);
material->set_shader_parameter("gradient_bottom", ground);
static constexpr const char *kFaceParams[6] = {
"front_tex", "back_tex", "left_tex", "right_tex", "top_tex", "bottom_tex"};
static constexpr const char *kFaceFlags[6] = {
"has_front", "has_back", "has_left", "has_right", "has_top", "has_bottom"};
for (int i = 0; i < 6; ++i) {
if (!faces[i].is_valid())
faces[i] = solid_texture(i == 5 ? ground : horizon);
material->set_shader_parameter(kFaceParams[i], faces[i]);
material->set_shader_parameter(kFaceFlags[i], face_loaded[i]);
}
if (cloud_texture.is_valid()) {
material->set_shader_parameter("cloud_tex", cloud_texture);
material->set_shader_parameter("has_cloud", true);
material->set_shader_parameter("cloud_scale", Vector2(
std::max(env.sky.cloud_scale[0] * (float)fmt::m2coord::CM_TO_M, 0.01f),
std::max(env.sky.cloud_scale[1] * (float)fmt::m2coord::CM_TO_M, 0.01f)));
material->set_shader_parameter("cloud_height",
std::max(env.sky.cloud_height * (float)fmt::m2coord::CM_TO_M, 0.01f));
material->set_shader_parameter("cloud_texture_scale", Vector2(
env.sky.cloud_texture_scale[0], env.sky.cloud_texture_scale[1]));
material->set_shader_parameter("cloud_speed", Vector2(
env.sky.cloud_speed[0], env.sky.cloud_speed[1]));
Color tint = Color(0, 0, 0, 0);
if (!env.sky.cloud_color.empty())
tint = rgba(env.sky.cloud_color.front());
material->set_shader_parameter("cloud_tint", tint);
sky_cloud_applied = true;
}
sky->set_material(material);
} else {
sky->set_material(psm);
}
// Reference SkyBox scrolls clouds in its own renderer. Godot's procedural
// sky has no equivalent UV-speed control, but sky_cover preserves the real
// cloud asset and alpha/color instead of silently dropping CloudTextureFileName.
if (!env.sky.cloud_texture.empty() && !use_texture_sky) {
if (cloud_texture.is_valid()) {
psm->set_sky_cover(cloud_texture);
Color tint = Color(1, 1, 1, 1);
if (!env.sky.cloud_color.empty())
tint = rgba(env.sky.cloud_color.front());
psm->set_sky_cover_modulate(tint);
sky_cloud_applied = true;
}
}
e->set_sky(sky);
e->set_background(godot::Environment::BG_SKY);
}
@@ -145,6 +401,37 @@ EnvNodes apply_environment(const fmt::Environment &env, Node *parent) {
e->set_ssao_radius(1.2f);
e->set_ssao_intensity(0.9f);
// Keep parsed reference values visible to the runtime/debug probe even where
// Godot has no 1:1 mapping (six cube faces, D3D blend factors, cloud motion,
// and lens flare). This also prevents a future builder from silently losing
// the fields while the renderer equivalent is being implemented.
out.world_env->set_meta("msenv_sky_texture_mode", env.sky.texture_render_mode);
out.world_env->set_meta("msenv_sky_face_count", sky_face_count);
out.world_env->set_meta("msenv_sky_texture_mode_applied", sky_texture_applied);
out.world_env->set_meta("msenv_sky_cloud_applied", sky_cloud_applied);
out.world_env->set_meta("msenv_sky_scale",
Vector3(env.sky.scale[0], env.sky.scale[1], env.sky.scale[2]));
out.world_env->set_meta("msenv_cloud_scale",
Vector2(env.sky.cloud_scale[0], env.sky.cloud_scale[1]));
out.world_env->set_meta("msenv_cloud_height", env.sky.cloud_height);
out.world_env->set_meta("msenv_cloud_texture_scale",
Vector2(env.sky.cloud_texture_scale[0], env.sky.cloud_texture_scale[1]));
out.world_env->set_meta("msenv_cloud_speed",
Vector2(env.sky.cloud_speed[0], env.sky.cloud_speed[1]));
out.world_env->set_meta("msenv_filter_enabled", env.filter.enable);
out.world_env->set_meta("msenv_filter_color", rgba(env.filter.color));
out.world_env->set_meta("msenv_filter_alpha_src", env.filter.alpha_src);
out.world_env->set_meta("msenv_filter_alpha_dest", env.filter.alpha_dest);
out.world_env->set_meta("msenv_lens_flare_enabled", env.lens_flare.enable);
out.world_env->set_meta("msenv_lens_flare_texture", env.lens_flare.main_flare_texture.c_str());
out.world_env->set_meta("msenv_background_light_enabled", env.dir_light.bg_enable);
out.world_env->set_meta("msenv_character_light_enabled", env.dir_light.ch_enable);
out.world_env->set_meta("msenv_background_light_ambient", rgba(env.dir_light.bg_ambient));
out.world_env->set_meta("msenv_character_light_ambient", rgba(env.dir_light.ch_ambient));
// MapUtil.cpp defaults; Environment_Load has no wind token to override these.
out.world_env->set_meta("msenv_wind_strength", 0.2f);
out.world_env->set_meta("msenv_wind_random", 0.0f);
out.world_env->set_environment(e);
return out;
}