Merge branch 'main' of http://192.168.21.200:8418/4v5u09Z5a4Yuc/metin2-client
# Conflicts: # docs/RENDERING-REPAIR-PLAN.md
This commit is contained in:
@@ -415,7 +415,20 @@ void Metin2AnimPlayer::apply_pose(double t) {
|
||||
tt = std::fmax(0.0, std::fmin(t, duration));
|
||||
}
|
||||
|
||||
gr2::sample_pose(*sk, an, (float)tt, world_buf, skin_buf);
|
||||
// Offset4x4 = identity, NOT the model's exported InitialPlacement.
|
||||
//
|
||||
// gr2::sample_pose defaults to sk.initial_placement because that is what the
|
||||
// oracle passes (GrannyGetModelInitialPlacement4x4) and what the bind pose /
|
||||
// InverseWorldTransform pair is self-consistent with. Metin2's animation .gr2
|
||||
// carries the root bone's pelvis height inside its own track, so composing it
|
||||
// onto InitialPlacement counts that placement twice and lifts the whole actor
|
||||
// by one pelvis height (warrior 99.4 cm, assassin 92.6, sura 103.7, shaman
|
||||
// 95.5 — exactly each model's IP.z). That was the "人物悬空" report.
|
||||
// Measured with tools/rendering foot probe: with identity the lowest skinned
|
||||
// vertex sits within a few mm of z=0 for wait/walk on all four classes, and
|
||||
// only leaves the ground during run's airborne frames.
|
||||
static const gr2::Mat4 kNoOffset{ 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1 };
|
||||
gr2::sample_pose(*sk, an, (float)tt, world_buf, skin_buf, &kNoOffset);
|
||||
|
||||
// Crossfade: blend the frozen outgoing-clip pose into this one, then rebuild
|
||||
// skin_buf = invWorld · world_blended (PARITY §2.9).
|
||||
@@ -423,7 +436,7 @@ void Metin2AnimPlayer::apply_pose(double t) {
|
||||
const gr2::FileInfo &pfi = prev_anim_file->file_info();
|
||||
if (!pfi.animations.empty()) {
|
||||
gr2::sample_pose(*sk, pfi.animations[0], (float)prev_anim_time, prev_world_buf,
|
||||
prev_skin_buf);
|
||||
prev_skin_buf, &kNoOffset);
|
||||
double w = blend_elapsed / blend_time;
|
||||
// ease-in on the incoming clip, matching the client's
|
||||
// GrannySetControlEaseInCurve(t0,t1, 0,0,1,1) Hermite (p0=0,m0=0,
|
||||
|
||||
@@ -564,12 +564,6 @@ void Metin2Model::_load_hair() {
|
||||
hair_parts = mtgodot::build_parts(hfi);
|
||||
|
||||
// hair 贴图:同名 .dds,alpha-scissor 镂空
|
||||
Ref<StandardMaterial3D> hm;
|
||||
hm.instantiate();
|
||||
hm->set_roughness(1.0f);
|
||||
hm->set_transparency(StandardMaterial3D::TRANSPARENCY_ALPHA_SCISSOR);
|
||||
hm->set_alpha_scissor_threshold(0.5f);
|
||||
hm->set_cull_mode(StandardMaterial3D::CULL_DISABLED);
|
||||
// SourceSkin (gr2 sibling .dds) -> TargetSkin override (§2.4)
|
||||
String hdds = hair_skin.is_empty() ? (hair_gr2.get_basename() + ".dds") : hair_skin;
|
||||
Ref<ImageTexture> ht = _load_dds(hdds);
|
||||
@@ -577,10 +571,20 @@ void Metin2Model::_load_hair() {
|
||||
UtilityFunctions::push_warning(String("[Metin2Model] hair_skin not found: ") + hair_skin);
|
||||
ht = _load_dds(hair_gr2.get_basename() + ".dds"); // fall back to SourceSkin
|
||||
}
|
||||
if (ht.is_valid())
|
||||
hm->set_texture(BaseMaterial3D::TEXTURE_ALBEDO, ht);
|
||||
else
|
||||
hm->set_albedo(Color(0.18f, 0.12f, 0.08f));
|
||||
// R2-02: the same forward material the body/weapon surfaces use, not a
|
||||
// StandardMaterial3D. The old one ran Godot's PBR specular at roughness 1.0
|
||||
// and washed the fringe out into flat cream patches (bisection: roughness 0
|
||||
// or unshaded clean, alpha/cull/metallic changes not). make_material's
|
||||
// shaders are `specular_disabled`, so the hair now lights like the body.
|
||||
// CPU path: skinning already happened on the CPU, so skinned = false.
|
||||
mtgodot::MaterialDesc hd;
|
||||
hd.albedo = ht;
|
||||
hd.blend = mtgodot::BlendMode::AlphaTest; // discard < 0.5, still depth-opaque
|
||||
hd.alpha_scissor = 0.5f;
|
||||
hd.two_sided = true; // hair cards are single-sided geometry
|
||||
Ref<ShaderMaterial> hm = mtgodot::make_material(hd);
|
||||
if (ht.is_null()) // keep the untextured fallback colour (make_material's is pale grey)
|
||||
hm->set_shader_parameter("modulate", Color(0.18f, 0.12f, 0.08f, 1.0f));
|
||||
hair_mat = hm;
|
||||
hair_tex = ht; // reused by the GPU-skin path (§2.2)
|
||||
|
||||
|
||||
@@ -119,11 +119,24 @@ void EntityStore::mut_spawn_full(const Entity &src) {
|
||||
// caller ever passes `store.get(vid)` back in.
|
||||
Entity e = src;
|
||||
e.vid = src.vid;
|
||||
m_ents[src.vid] = e;
|
||||
if (e.is_main) {
|
||||
m_main_vid = src.vid;
|
||||
// Reference CNetworkActorManager::__AppendCharacterManagerActor():
|
||||
// kCreateData.m_isMain = __IsMainActorVID(dwVID);
|
||||
// The main VID is owned by the manager, not by the actor row, so an actor
|
||||
// re-added under the main VID becomes the main instance again. The 40250
|
||||
// server does exactly this at spawn-in: GC_CHARACTER_DEL + GC_CHARACTER_ADD
|
||||
// on our own VID moments after GC_MAIN_CHARACTER. Without this, the re-added
|
||||
// row is a plain PC and the local player stays the fallback capsule.
|
||||
if (e.vid == m_main_vid) {
|
||||
e.is_main = true;
|
||||
}
|
||||
m_ents[src.vid] = e;
|
||||
m_changes.push_back({ChangeKind::Spawn, src.vid});
|
||||
if (e.is_main) {
|
||||
// __AppendCharacterManagerActor() re-runs SetMainCharacterIndex(dwVID)
|
||||
// on every main re-add, so re-announce it here too.
|
||||
m_main_vid = src.vid;
|
||||
m_changes.push_back({ChangeKind::MainSet, src.vid});
|
||||
}
|
||||
}
|
||||
|
||||
void EntityStore::mut_ownership(uint32_t vid, uint32_t owner_vid) {
|
||||
@@ -170,12 +183,14 @@ void EntityStore::mut_map_bgm(const std::string &name, float volume) {
|
||||
}
|
||||
|
||||
void EntityStore::mut_despawn(uint32_t vid) {
|
||||
// Reference CNetworkActorManager::RemoveActor() drops the actor row and the
|
||||
// character instance but never touches m_dwMainVID — that field is written
|
||||
// only by SetMainActorVID() (GC_MAIN_CHARACTER) and cleared only when the
|
||||
// manager is destroyed (our reset_for_map_change()). Clearing it here made a
|
||||
// routine GC_CHARACTER_DEL on our own VID permanently un-main the player.
|
||||
if (m_ents.erase(vid)) {
|
||||
m_changes.push_back({ChangeKind::Despawn, vid});
|
||||
}
|
||||
if (vid == m_main_vid) {
|
||||
m_main_vid = 0;
|
||||
}
|
||||
}
|
||||
|
||||
// CLIENT-GAP §3.2: GC_MOVE no longer mutates position directly. It appends a
|
||||
@@ -1658,12 +1673,11 @@ void EntityStore::apply(uint16_t header, const void *body, uint16_t len) {
|
||||
}
|
||||
GCCharacterDel p;
|
||||
std::memcpy(&p, body, sizeof(p));
|
||||
// Same rule as mut_despawn(): the main VID survives a
|
||||
// GC_CHARACTER_DEL and is re-bound by the matching re-add.
|
||||
if (m_ents.erase(p.vid)) {
|
||||
m_changes.push_back({ChangeKind::Despawn, p.vid});
|
||||
}
|
||||
if (p.vid == m_main_vid) {
|
||||
m_main_vid = 0;
|
||||
}
|
||||
return;
|
||||
}
|
||||
case GC_MOVE: {
|
||||
|
||||
Reference in New Issue
Block a user