// libgr2 —— gr2 v6 只读读取器(demo 子集)。 // 施工文档:docs/reference/steps/M0-gr2-reader.md #pragma once #include "types.h" #include #include #include #include #include #include #include namespace gr2 { enum class Magic { Unknown, Bit32_LE, Bit32_BE, Bit64_LE, Bit64_BE }; struct LoadError { std::string stage; // "header" / "section" / "typetree" / "fileinfo" / ... std::string message; }; // T3:通用类型树遍历(gr2dump 打印成员用;不硬编码 struct 布局) struct MemberDump { std::string name; int member_type; int array_width; }; class File; std::vector dump_root_members(const File&); // 材质视图(类型树遍历 root.Materials → granny_material)。 // 只读文件名,不解码贴图。diffuse_texture 取 Usage 含 "diffuse" 的 map 的 // 贴图,退而取第一张;all_textures 是从该材质可达的全部 FromFileName。 struct MaterialInfo { std::string name; std::string diffuse_texture; std::vector all_textures; // Metin2/EterGrnLib 渲染态推断(gr2 不带 D3DRS,靠名字 + map 数 + ExtendedData): int map_count = 0; // granny_material.MapCount(可达子材质的 Texture 数) bool alpha_blend = false;// Name 以 "Blend" 开头且 map_count>1(EterGrnLib Material.cpp:233) bool two_sided = false; // 名字含 2side/twoside/double/leaf/tree/ivy/fence(ExtendedData "Two-sided" 的近似) }; std::vector dump_materials(const File&); // 已解析、fixup 后的文件。析构释放所有 section 缓冲。 class File { public: static std::optional load(const uint8_t* bytes, size_t len, LoadError* err); static std::optional load_path(const std::string& path, LoadError* err); // T1 Magic magic() const { return magic_; } uint32_t format_version()const { return version_; } // 期望 6 uint32_t total_size() const { return total_size_; } const std::vector& sections() const { return sections_; } // T4 —— 完整汇总(内部会按需触发 T5/T6/T7a) const FileInfo& file_info() const { return info_; } // 展开后的 section 字节(T2 产出)。空 section 返回空 span。调试 / 校验用。 std::span section_bytes(size_t i) const; // T5 —— bind pose 自洽检查(不依赖 oracle)。返回 max|world_bind·invBind − I| // 阈值策略见 M0-gr2-reader.md T5(noise_floor 前用 1e-3) double bind_pose_self_check(int skeleton = 0) const; // M2 便捷封装(同文件内的 skeleton + animation)。跨文件(model gr2 + anim gr2) // 用自由函数 gr2::sample_pose(skeleton, animation, t, ...)。anim/skeleton 是 file_info() 索引。 bool sample_pose(int anim, float t, int skeleton, std::vector& world_pose, std::vector& skin) const; bool bind_pose(int skeleton, std::vector& world_pose, std::vector& skin) const; private: File() = default; Magic magic_ = Magic::Unknown; uint32_t version_ = 0; uint32_t total_size_ = 0; std::vector sections_; FileInfo info_; // 内部:section 缓冲、root object 指针、类型树 —— 放 .cpp 的 Impl struct Impl; std::shared_ptr impl_; friend std::vector dump_root_members(const File&); friend std::vector dump_materials(const File&); }; // ── M2:姿势采样(model 的 skeleton + anim 的 tracks 可来自不同 gr2)────── // Granny 语义、行主序、平移在 elem 12..14: // world[i] = Composite(local[i]) · (parent<0 ? root_offset : world[parent]) // skin[i] = sk.bones[i].inverse_world · world[i] // tracks 按骨骼名匹配 sk;无匹配的骨骼用 sk 的 bind LocalTransform。 // // root_offset = GrannyBuildWorldPose 的 Offset4x4。默认(nullptr)用 // sk.initial_placement —— 与 oracle/oracle.c 的 GrannyGetModelInitialPlacement4x4 // 一致,是 bind pose 与 inverse_world 自洽所要求的那一个(bind_pose 只用这一种)。 // 渲染动画时客户端另有取法:EterGrnLib 对 Offset4x4 传身位矩阵而不是导出期的 // InitialPlacement,而 Metin2 的 .gr2 动画 track 里根骨的位移已经含了骨盆高度, // 所以再乘一次 InitialPlacement 会把整个人抬高一个骨盆高。要那条语义就显式传 // 单位阵(见 extension/src/metin2_anim.cpp 与 test/rendering/README.md 的实测)。 void sample_pose(const Skeleton& sk, const Animation& an, float t, std::vector& world, std::vector& skin, const Mat4* root_offset = nullptr); void bind_pose(const Skeleton& sk, std::vector& world, std::vector& skin); // 打印辅助 const char* magic_name(Magic); const char* compression_name(uint32_t section_format); // 0=none 1=Oodle0 2=Oodle1 } // namespace gr2