Networked client on the existing Godot 4.7 + libgr2 renderer:
- net: m2dev wire protocol (libsodium KX + XChaCha20), auth/select/game
phases, EntityStore world model, ~all GC/CG headers. char create/delete,
private shop / mall / cube, SHOP_GC_START_EX, guild, party (+ CG_PARTY_SET_STATE),
quests, dragon soul, refine, safebox, exchange.
- UI: in-game windows migrated 1:1 from the reference uiscript/root .py —
char status (/stat), inventory+equipment, select-item ([SELECT_ITEM] quest
token), system-option + game-option + ESC system menu, private-shop 39-grid,
party info board, shop tabs, atlas, minimap, quickbar, chat, …
- EterGrnLib polish: GR2 material blend/two-sided, LOD crossfade, motion-event
dispatch, contact shadow, ray-AABB picking, weapon grip pre-transform.
Portable asset IO (A1) — all extension/libgr2/formats/mtproto reads routed
through godot::FileAccess (res:// PCK works on iOS/Android); standalone-lib
*_path() kept for the non-Godot CTests. AssetResolver + PropertyRegistry
switched to a baked index (bake_asset_index.gd) instead of std::filesystem.
Mobile builds: build-{android,ios}.sh, export-android.sh, pack-assets.sh,
gen-debug-keystore.sh. Assets ship as a zip mounted at runtime by
project/asset_pack.gd (adb push now; HTTP download is a drop-in later).
ctest 10/10, 34 GDScript suites, macOS/iOS/Android all build.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013EJxkHiNKS4kybHS3XKyAJ
258 lines
9.1 KiB
C++
258 lines
9.1 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> shader_for(bool additive, bool skinned, bool two_sided) {
|
|
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));
|
|
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
|