Handle late main-character data, guarded model construction, bounded retries and map correction. Validate player model sources and add lifecycle and real-asset visual regressions. Include pending material and character-selection changes, updated A1 screenshot, and the detailed rendering repair plan. Hair occlusion, full UI parity and final macOS package acceptance remain unfinished.
277 lines
9.8 KiB
C++
277 lines
9.8 KiB
C++
#include "m2_material.h"
|
|
|
|
#include <godot_cpp/classes/image_texture.hpp>
|
|
#include <godot_cpp/classes/shader.hpp>
|
|
#include <godot_cpp/classes/shader_material.hpp>
|
|
#include <godot_cpp/classes/texture2d.hpp>
|
|
#include <godot_cpp/variant/color.hpp>
|
|
|
|
using namespace godot;
|
|
|
|
namespace mtgodot {
|
|
namespace {
|
|
|
|
// blend_mix shader: covers opaque (mode 0, alpha-scissor) and alpha (mode 1).
|
|
const char *SRC_MIX = R"(shader_type spatial;
|
|
render_mode blend_mix, depth_draw_opaque, cull_back, diffuse_lambert, specular_disabled;
|
|
|
|
uniform sampler2D albedo_tex : source_color, filter_linear_mipmap, repeat_enable;
|
|
uniform bool use_texture = true;
|
|
uniform vec4 modulate : source_color = vec4(1.0);
|
|
uniform float alpha_scissor : hint_range(0.0, 1.0) = 0.5;
|
|
uniform int mode = 0; // 0 opaque | 1 alpha-blend | 2 alpha-test (cutout)
|
|
// sphere-map specular (EterGrnLib/Material.cpp:305 __ApplySpecularRenderState)
|
|
uniform sampler2D spec_map : source_color, filter_linear_mipmap, repeat_enable;
|
|
uniform float spec_power = 0.0;
|
|
uniform bool spec_enable = false;
|
|
uniform float lod_fade = 1.0; // LOD crossfade multiplier (1 = fully shown)
|
|
|
|
void fragment() {
|
|
vec4 c = use_texture ? texture(albedo_tex, UV) : vec4(1.0);
|
|
float spec_mask = c.a; // tex.a before modulate == D3DTA_TEXTURE alpha
|
|
c *= modulate; // no vertex color: Metin2 PC meshes carry none (ARRAY_COLOR absent)
|
|
if (mode == 2 && c.a < alpha_scissor) {
|
|
discard;
|
|
}
|
|
ALBEDO = c.rgb;
|
|
ALPHA = ((mode == 1) ? c.a : 1.0) * lod_fade;
|
|
// Opaque only: client early-outs to plain diffuse when D3DRS_ALPHABLENDENABLE.
|
|
// stage1 COLOROP MODULATEALPHA_ADDCOLOR = CURRENT.rgb + CURRENT.a*sphere.rgb,
|
|
// CURRENT.a = tex.a * D3DRS_TEXTUREFACTOR.a (= spec_power). texcoord =
|
|
// D3DTSS_TCI_CAMERASPACEREFLECTIONVECTOR -> view-space reflect(); .xy as UV.
|
|
if (spec_enable && spec_power > 0.0 && mode == 0) {
|
|
// Sphere-map metallic sheen (EterGrnLib/Material.cpp:305). The client masks
|
|
// this with the armor texture's alpha (metal=1, cloth=0); our loose-file
|
|
// texture resolution can't be trusted for that alpha, so bias it to
|
|
// grazing angles (Fresnel) and knock the level down — reads as an edge
|
|
// sheen instead of a full-body chrome mirror.
|
|
vec3 vdir = normalize(VERTEX);
|
|
vec3 ndir = normalize(NORMAL);
|
|
float fres = pow(clamp(1.0 - abs(dot(ndir, vdir)), 0.0, 1.0), 3.0);
|
|
vec3 refl = reflect(vdir, ndir);
|
|
EMISSION = texture(spec_map, refl.xy * 0.5 + 0.5).rgb
|
|
* (spec_mask * spec_power * fres * 0.5);
|
|
}
|
|
}
|
|
)";
|
|
|
|
// Skinned variant of SRC_MIX: LBS in vertex() with FULL per-bone matrices from a
|
|
// float texture (no Skeleton3D). bones_tex is RGBAF, 3 x bone_count; row = bone,
|
|
// texel j = column j of the row-vector skin matrix (skinned = [pos 1] * M).
|
|
const char *SRC_SKIN = R"(shader_type spatial;
|
|
render_mode blend_mix, depth_draw_opaque, cull_back, diffuse_lambert, specular_disabled;
|
|
|
|
uniform sampler2D albedo_tex : source_color, filter_linear_mipmap, repeat_enable;
|
|
uniform bool use_texture = true;
|
|
uniform vec4 modulate : source_color = vec4(1.0);
|
|
uniform float alpha_scissor : hint_range(0.0, 1.0) = 0.5;
|
|
uniform int mode = 0;
|
|
uniform sampler2D bones_tex : filter_nearest; // RGBAF, 3 x bone_count
|
|
uniform sampler2D spec_map : source_color, filter_linear_mipmap, repeat_enable;
|
|
uniform float spec_power = 0.0;
|
|
uniform bool spec_enable = false;
|
|
uniform float lod_fade = 1.0; // LOD crossfade multiplier (1 = fully shown)
|
|
|
|
void vertex() {
|
|
vec4 p = vec4(VERTEX, 1.0);
|
|
vec3 sp = vec3(0.0);
|
|
vec3 sn = vec3(0.0);
|
|
ivec4 bi = ivec4(BONE_INDICES);
|
|
vec4 bw = BONE_WEIGHTS;
|
|
float wsum = bw.x + bw.y + bw.z + bw.w;
|
|
if (wsum <= 0.0) { bw = vec4(1.0, 0.0, 0.0, 0.0); wsum = 1.0; }
|
|
for (int k = 0; k < 4; k++) {
|
|
float w = bw[k] / wsum;
|
|
if (w <= 0.0) { continue; }
|
|
int r = bi[k];
|
|
vec4 c0 = texelFetch(bones_tex, ivec2(0, r), 0);
|
|
vec4 c1 = texelFetch(bones_tex, ivec2(1, r), 0);
|
|
vec4 c2 = texelFetch(bones_tex, ivec2(2, r), 0);
|
|
sp += w * vec3(dot(p, c0), dot(p, c1), dot(p, c2));
|
|
sn += w * vec3(dot(NORMAL, c0.xyz), dot(NORMAL, c1.xyz), dot(NORMAL, c2.xyz));
|
|
}
|
|
VERTEX = sp;
|
|
NORMAL = normalize(sn);
|
|
}
|
|
|
|
void fragment() {
|
|
vec4 c = use_texture ? texture(albedo_tex, UV) : vec4(1.0);
|
|
float spec_mask = c.a;
|
|
c *= modulate;
|
|
if (mode == 2 && c.a < alpha_scissor) { discard; }
|
|
ALBEDO = c.rgb;
|
|
ALPHA = ((mode == 1) ? c.a : 1.0) * lod_fade;
|
|
if (spec_enable && spec_power > 0.0 && mode == 0) {
|
|
// Sphere-map metallic sheen (EterGrnLib/Material.cpp:305). The client masks
|
|
// this with the armor texture's alpha (metal=1, cloth=0); our loose-file
|
|
// texture resolution can't be trusted for that alpha, so bias it to
|
|
// grazing angles (Fresnel) and knock the level down — reads as an edge
|
|
// sheen instead of a full-body chrome mirror.
|
|
vec3 vdir = normalize(VERTEX);
|
|
vec3 ndir = normalize(NORMAL);
|
|
float fres = pow(clamp(1.0 - abs(dot(ndir, vdir)), 0.0, 1.0), 3.0);
|
|
vec3 refl = reflect(vdir, ndir);
|
|
EMISSION = texture(spec_map, refl.xy * 0.5 + 0.5).rgb
|
|
* (spec_mask * spec_power * fres * 0.5);
|
|
}
|
|
}
|
|
)";
|
|
|
|
// blend_add shader: additive glow (effects, enchant overlays).
|
|
const char *SRC_ADD = R"(shader_type spatial;
|
|
render_mode blend_add, depth_draw_opaque, depth_test_disabled, cull_back, unshaded;
|
|
|
|
uniform sampler2D albedo_tex : source_color, filter_linear_mipmap, repeat_enable;
|
|
uniform bool use_texture = true;
|
|
uniform vec4 modulate : source_color = vec4(1.0);
|
|
|
|
void fragment() {
|
|
vec4 c = use_texture ? texture(albedo_tex, UV) : vec4(1.0);
|
|
c *= modulate; // no vertex color: Metin2 PC meshes carry none (ARRAY_COLOR absent)
|
|
ALBEDO = c.rgb * c.a;
|
|
ALPHA = 1.0;
|
|
}
|
|
)";
|
|
|
|
// Additive surfaces need the same vertex deformation as opaque/alpha surfaces.
|
|
// Keeping this as a separate shader preserves blend_add/unshaded render modes.
|
|
const char *SRC_ADD_SKIN = R"(shader_type spatial;
|
|
render_mode blend_add, depth_draw_opaque, depth_test_disabled, cull_back, unshaded;
|
|
|
|
uniform sampler2D albedo_tex : source_color, filter_linear_mipmap, repeat_enable;
|
|
uniform bool use_texture = true;
|
|
uniform vec4 modulate : source_color = vec4(1.0);
|
|
uniform sampler2D bones_tex : filter_nearest;
|
|
|
|
void vertex() {
|
|
vec4 p = vec4(VERTEX, 1.0);
|
|
vec3 sp = vec3(0.0);
|
|
vec3 sn = vec3(0.0);
|
|
ivec4 bi = ivec4(BONE_INDICES);
|
|
vec4 bw = BONE_WEIGHTS;
|
|
float wsum = bw.x + bw.y + bw.z + bw.w;
|
|
if (wsum <= 0.0) { bw = vec4(1.0, 0.0, 0.0, 0.0); wsum = 1.0; }
|
|
for (int k = 0; k < 4; k++) {
|
|
float w = bw[k] / wsum;
|
|
if (w <= 0.0) { continue; }
|
|
int r = bi[k];
|
|
vec4 c0 = texelFetch(bones_tex, ivec2(0, r), 0);
|
|
vec4 c1 = texelFetch(bones_tex, ivec2(1, r), 0);
|
|
vec4 c2 = texelFetch(bones_tex, ivec2(2, r), 0);
|
|
sp += w * vec3(dot(p, c0), dot(p, c1), dot(p, c2));
|
|
sn += w * vec3(dot(NORMAL, c0.xyz), dot(NORMAL, c1.xyz), dot(NORMAL, c2.xyz));
|
|
}
|
|
VERTEX = sp;
|
|
NORMAL = normalize(sn);
|
|
}
|
|
|
|
void fragment() {
|
|
vec4 c = use_texture ? texture(albedo_tex, UV) : vec4(1.0);
|
|
c *= modulate;
|
|
ALBEDO = c.rgb * c.a;
|
|
ALPHA = 1.0;
|
|
}
|
|
)";
|
|
|
|
Ref<Shader> s_mix;
|
|
Ref<Shader> s_add;
|
|
Ref<Shader> s_skin;
|
|
Ref<Shader> s_add_skin;
|
|
// cull_disabled variants for two-sided parts (hair / cloth / foliage; client
|
|
// ExtendedData "Two-sided" -> D3DRS_CULLMODE = D3DCULL_NONE).
|
|
Ref<Shader> s_mix_2s;
|
|
Ref<Shader> s_add_2s;
|
|
Ref<Shader> s_skin_2s;
|
|
Ref<Shader> s_add_skin_2s;
|
|
Ref<Shader> s_opaque[4];
|
|
|
|
Ref<Shader> shader_for(bool additive, bool skinned, bool two_sided, bool opaque) {
|
|
if (opaque) {
|
|
Ref<Shader> &solid = s_opaque[(skinned ? 2 : 0) + (two_sided ? 1 : 0)];
|
|
if (solid.is_null()) {
|
|
String code = skinned ? SRC_SKIN : SRC_MIX;
|
|
// Writing ALPHA puts even alpha=1 into Godot's transparent pass.
|
|
// Solid faces must write depth before the hair cutout is drawn.
|
|
code = code.replace("ALPHA = ((mode == 1) ? c.a : 1.0) * lod_fade;",
|
|
"if (lod_fade < fract(sin(dot(FRAGCOORD.xy, vec2(12.9898, 78.233))) * 43758.5453)) { discard; }");
|
|
if (two_sided)
|
|
code = code.replace("cull_back", "cull_disabled");
|
|
solid.instantiate();
|
|
solid->set_code(code);
|
|
}
|
|
return solid;
|
|
}
|
|
Ref<Shader> &slot = two_sided
|
|
? (additive ? (skinned ? s_add_skin_2s : s_add_2s) : (skinned ? s_skin_2s : s_mix_2s))
|
|
: (additive ? (skinned ? s_add_skin : s_add) : (skinned ? s_skin : s_mix));
|
|
if (slot.is_null()) {
|
|
slot.instantiate();
|
|
const char *base =
|
|
additive ? (skinned ? SRC_ADD_SKIN : SRC_ADD) : (skinned ? SRC_SKIN : SRC_MIX);
|
|
slot->set_code(two_sided ? String(base).replace("cull_back", "cull_disabled")
|
|
: String(base));
|
|
}
|
|
return slot;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
Ref<ShaderMaterial> make_material(const MaterialDesc &d) {
|
|
Ref<ShaderMaterial> m;
|
|
m.instantiate();
|
|
|
|
const bool additive = (d.blend == BlendMode::Add);
|
|
const bool skinned = d.skinned;
|
|
m->set_shader(shader_for(additive, skinned, d.two_sided,
|
|
d.blend == BlendMode::Opaque || d.blend == BlendMode::AlphaTest));
|
|
if (skinned && d.bones_tex.is_valid()) {
|
|
m->set_shader_parameter("bones_tex", d.bones_tex);
|
|
}
|
|
|
|
const bool has_tex = d.albedo.is_valid();
|
|
m->set_shader_parameter("use_texture", has_tex);
|
|
if (has_tex) {
|
|
m->set_shader_parameter("albedo_tex", d.albedo);
|
|
}
|
|
m->set_shader_parameter("modulate", has_tex ? Color(1, 1, 1, 1) : Color(0.8, 0.8, 0.82, 1));
|
|
|
|
if (!additive) {
|
|
// sphere-map specular (dormant unless spec_power > 0 and a map is bound)
|
|
const bool spec = d.spec_power > 0.0f && d.spec_map.is_valid();
|
|
m->set_shader_parameter("spec_enable", spec);
|
|
m->set_shader_parameter("spec_power", d.spec_power);
|
|
if (spec) {
|
|
m->set_shader_parameter("spec_map", d.spec_map);
|
|
}
|
|
m->set_shader_parameter("alpha_scissor", d.alpha_scissor);
|
|
int mode = 0;
|
|
if (d.blend == BlendMode::Alpha) {
|
|
mode = 1;
|
|
} else if (d.blend == BlendMode::AlphaTest) {
|
|
mode = 2;
|
|
}
|
|
m->set_shader_parameter("mode", mode);
|
|
}
|
|
if (d.blend == BlendMode::Alpha) {
|
|
m->set_render_priority(1);
|
|
} else if (additive) {
|
|
m->set_render_priority(2);
|
|
}
|
|
return m;
|
|
}
|
|
|
|
void cleanup_material_shaders() {
|
|
for (auto &shader : s_opaque)
|
|
shader.unref();
|
|
s_mix.unref();
|
|
s_add.unref();
|
|
s_skin.unref();
|
|
s_add_skin.unref();
|
|
s_mix_2s.unref();
|
|
s_add_2s.unref();
|
|
s_skin_2s.unref();
|
|
s_add_skin_2s.unref();
|
|
}
|
|
|
|
} // namespace mtgodot
|