#include "environment_builder.h" #include "asset_io.h" #include "dxt.h" #include #include #include #include #include #include #include #include #include #include #include using namespace godot; namespace mtgodot { 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 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 image = godot::Image::create_from_data( 1, 1, false, godot::Image::FORMAT_RGBA8, pixels); image->generate_mipmaps(); return ImageTexture::create_from_image(image); } Ref load_texture(const std::string &name, const fmt::AssetResolver *resolver) { if (name.empty() || resolver == nullptr) return Ref(); const std::string path = resolver->resolve(name, nullptr); if (path.empty()) return Ref(); Ref 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(); 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::load_from_file cannot open the pack:// virtual filesystem. if (godot_path.begins_with("pack://")) { PackedByteArray bytes = mtgodot::read_file(godot_path); image.instantiate(); if (godot_path.get_extension().to_lower() == "tga") image->load_tga_from_buffer(bytes); else if (godot_path.get_extension().to_lower() == "png") image->load_png_from_buffer(bytes); else if (godot_path.get_extension().to_lower() == "jpg") image->load_jpg_from_buffer(bytes); } else { image = godot::Image::load_from_file(godot_path); } } if (image.is_null() || image->is_empty()) return Ref(); image->generate_mipmaps(); return ImageTexture::create_from_image(image); } } // namespace EnvNodes apply_environment(const fmt::Environment &env, Node *parent, const fmt::AssetResolver *resolver) { EnvNodes out; // --- DirectionalLight (Background) --- out.sun = Object::cast_to(parent->get_node_or_null(NodePath("Sun"))); if (!out.sun) { out.sun = memnew(DirectionalLight3D); 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]); Vector3 fwd(d.x, d.y, d.z); if (fwd.length() < 1e-4f) fwd = Vector3(-0.4f, -0.7f, -0.55f); fwd.normalize(); // Godot 光沿自身 -Z 照射 -> basis 的 -Z = fwd Basis b = Basis::looking_at(fwd, Vector3(0, 1, 0)); out.sun->set_transform(Transform3D(b, Vector3(0, 0, 0))); if (env.dir_light.bg_enable || luma(env.dir_light.bg_diffuse) > 0.01f) { out.sun->set_color(rgba(env.dir_light.bg_diffuse)); out.sun->set_param(Light3D::PARAM_ENERGY, std::clamp(0.9f + luma(env.dir_light.bg_diffuse) * 0.4f, 0.7f, 1.6f)); } out.sun->set_shadow(true); out.sun->set_param(Light3D::PARAM_SHADOW_NORMAL_BIAS, 2.0f); out.sun->set_param(Light3D::PARAM_SHADOW_BIAS, 0.06f); out.sun->set_param(Light3D::PARAM_SHADOW_MAX_DISTANCE, 500.0f); out.sun->set_param(Light3D::PARAM_SHADOW_SPLIT_1_OFFSET, 0.08f); out.sun->set_param(Light3D::PARAM_SHADOW_SPLIT_2_OFFSET, 0.22f); out.sun->set_param(Light3D::PARAM_SHADOW_SPLIT_3_OFFSET, 0.5f); out.sun->set_shadow_mode(DirectionalLight3D::SHADOW_PARALLEL_4_SPLITS); // 角色/物体的间接补光:暖色,能量取自 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( 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(parent->get_node_or_null(NodePath("WorldEnv"))); if (!out.world_env) { out.world_env = memnew(WorldEnvironment); out.world_env->set_name("WorldEnv"); parent->add_child(out.world_env); } Ref e = out.world_env->get_environment(); if (e.is_null()) e.instantiate(); // 天空: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 psm; psm.instantiate(); const auto &g = env.sky.gradient; if (g.size() >= 2) { psm->set_sky_top_color(rgba(g.front())); psm->set_sky_horizon_color(rgba(g[g.size() / 2])); psm->set_ground_horizon_color(rgba(g.back())); Color gb = rgba(g.back()); psm->set_ground_bottom_color(Color(gb.r * 0.6f, gb.g * 0.6f, gb.b * 0.65f)); } psm->set_sun_angle_max(6.0f); Ref sky; sky.instantiate(); std::array, 6> faces; std::array 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 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.instantiate(); shader->set_code(String(SRC_SKYBOX)); Ref 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); } // 环境光:Material.Ambient 定色 + 亮度(Metin2 用暖色环境光提亮阴影面) e->set_ambient_source(godot::Environment::AMBIENT_SOURCE_COLOR); Color amb = rgba(env.material.ambient); e->set_ambient_light_color(amb); e->set_ambient_light_energy(std::clamp(0.5f + luma(env.material.ambient) * 0.4f, 0.35f, 0.95f)); e->set_ambient_light_sky_contribution(0.35f); // Emissive 当作全局轻微自发光提亮(避免死黑) e->set_bg_energy_multiplier(1.0f); // 雾:优先用 .msenv 的 NearDistance/FarDistance(cm,A1 = 5000/20000 -> 50/200m // 太近,客户端 D3DFOG 实际按更大的世界尺度;乘一个系数放到远景轻霭区)。没给 // 距离就退回 foglevel 启发式。参考端是线性远景雾 + 天空同色,不是浓雾。 if (env.fog.enable) { e->set_fog_enabled(true); e->set_fog_light_color(rgba(env.fog.color)); e->set_fog_mode(godot::Environment::FOG_MODE_DEPTH); float begin_m, end_m; if (env.fog.near_distance > 1.0f && env.fog.far_distance > env.fog.near_distance) { begin_m = env.fog.near_distance * (float)fmt::m2coord::CM_TO_M; end_m = env.fog.far_distance * (float)fmt::m2coord::CM_TO_M; // 客户端摄距比我们远:把近雾往后推一截,别糊住中景 begin_m = std::max(begin_m, 120.0f); end_m = std::max(end_m, begin_m + 400.0f); } else { float fl = env.fog.fog_level > 0 ? float(env.fog.fog_level) : 4.0f; begin_m = std::clamp((11.0f - fl) * 45.0f, 60.0f, 500.0f); end_m = begin_m + 700.0f; } e->set_fog_depth_begin(begin_m); e->set_fog_depth_end(end_m); e->set_fog_depth_curve(0.5f); e->set_fog_density(0.0f); e->set_fog_sky_affect(0.0f); // 天空自己是渐变,不要被雾再洗一层 e->set_fog_sun_scatter(0.05f); } else { e->set_fog_enabled(false); } // 色调:参考端是 DX9 定功能、LDR、无 tonemap/HDR。用 LINEAR + 曝光 1 最贴近, // Filmic 会抬黑、降饱和 -> 画面发灰。轻微加饱和/对比补回胶片感。 e->set_tonemapper(godot::Environment::TONE_MAPPER_LINEAR); e->set_tonemap_exposure(1.0f); e->set_adjustment_enabled(true); e->set_adjustment_saturation(1.12f); e->set_adjustment_contrast(1.05f); e->set_adjustment_brightness(1.0f); // glow:参考端没有 bloom。留极轻的,只有真过曝才溢。 e->set_glow_enabled(true); e->set_glow_intensity(0.06f); e->set_glow_strength(0.7f); e->set_glow_bloom(0.0f); e->set_glow_hdr_bleed_threshold(1.6f); // SSAO:参考端把接触阴影烘进地形阴影贴图。这里用一点实时 SSAO 代替。 e->set_ssao_enabled(true); 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; } } // namespace mtgodot