#include "m2_material.h" #include #include #include #include #include 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 s_mix; Ref s_add; Ref s_skin; Ref s_add_skin; // cull_disabled variants for two-sided parts (hair / cloth / foliage; client // ExtendedData "Two-sided" -> D3DRS_CULLMODE = D3DCULL_NONE). Ref s_mix_2s; Ref s_add_2s; Ref s_skin_2s; Ref s_add_skin_2s; Ref shader_for(bool additive, bool skinned, bool two_sided) { Ref &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 make_material(const MaterialDesc &d) { Ref 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)); 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() { 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