commit c52b8657c41435f481eada954acb8ef71786be69 Author: Claude Date: Sat Aug 29 09:09:55 2026 +0900 M0' scaffold: Godot 4.7 + GDExtension + libgr2 wired up (macOS) - extension/: godot-cpp submodule (master, API 4.7), Metin2Model node registered, links xrender-poc/libgr2 via sibling-dir reference. - project/: Godot 4.7 project — orbit camera + DirectionalLight3D (shadows) + WorldEnvironment/procedural sky + placeholder cube; main.gd probes the extension and can run libgr2 on a real .gr2 (MTGODOT_PROBE_GR2). - build.sh one-shot build into project/bin/. - docs/GODOT-POC-PLAN.md (phased: Phase 1 macOS -> M0'/M1/M2/M2.5). Verified on this machine: [mtgodot] Metin2Model registered OK — libgr2 linked probe_gr2(warrior_cheongrin.gr2) -> gr2 OK: format_version=6 sections=8 Metal Forward+ renders the scene (test/golden/m0-scaffold.png). Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01WaHYEY9rwLWt21PULiYjeJ diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..de6ffb26 --- /dev/null +++ b/.gitignore @@ -0,0 +1,16 @@ +# build +/build/ +/cmake-build-*/ +compile_commands.json + +# godot +/project/.godot/ +/project/bin/*.dylib +/project/bin/*.dSYM/ +/project/bin/*.so +/project/bin/*.dll +/project/bin/*.wasm + +# misc +.DS_Store +*.tmp diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 00000000..1cc4f385 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,4 @@ +[submodule "extension/godot-cpp"] + path = extension/godot-cpp + url = https://github.com/godotengine/godot-cpp.git + branch = master diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 00000000..06511a81 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,21 @@ +# mtgodot-poc — top level +# Phase 1 (macOS) scaffolding. See docs/GODOT-POC-PLAN.md §M0'. +cmake_minimum_required(VERSION 3.20) +project(mtgodot_poc CXX C) + +set(CMAKE_CXX_STANDARD 20) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_C_STANDARD 17) + +if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) + set(CMAKE_BUILD_TYPE Debug CACHE STRING "" FORCE) +endif() + +set(CMAKE_EXPORT_COMPILE_COMMANDS ON) + +# Phase 1 is macOS-only; fail early anywhere else so nobody wastes time. +if(NOT APPLE) + message(FATAL_ERROR "mtgodot-poc Phase 1 targets macOS only (see docs/GODOT-POC-PLAN.md §00).") +endif() + +add_subdirectory(extension) diff --git a/README.md b/README.md new file mode 100644 index 00000000..0e27d169 --- /dev/null +++ b/README.md @@ -0,0 +1,111 @@ +# mtgodot-poc + +「**Godot 4 + 自研资源 loader**」跨平台方案的渲染 Demo(route ①)。 +用一个 C++ GDExtension 复用 [`xrender-poc`](../xrender-poc) 的 `libgr2`,把 Metin2 的 +`.gr2` 骨骼资源渲染进 Godot,用 Godot 内置渲染器。 + +与 `xrender-poc`(自研引擎 + bgfx RHI)是**两条并行的跨平台基座候选**,共用 `libgr2`。 + +判定分两阶段: + +| 阶段 | 平台 | 里程碑 | 产出 | +|---|---|---|---| +| **Phase 1**(当前) | macOS | M0' · M1 · M2 · M2.5 | 中期评审(不下 go/no-go) | +| Phase 2 | iOS · Android | M3 | go / no-go | + +完整计划见 [`docs/GODOT-POC-PLAN.md`](docs/GODOT-POC-PLAN.md)。 + +--- + +## 现状 + +- [~] **M0'** — 脚手架。GDExtension 骨架(`Metin2Model` 空节点)+ `libgr2` 静态库集成 + Godot 4.7 工程(orbit 相机 + 方向光阴影 + sky + 占位 cube)。macOS only。 + +--- + +## 环境 + +| 组件 | 版本 / 位置 | 说明 | +|---|---|---| +| Godot | **4.7.1**(`/opt/homebrew/bin/godot`,Homebrew cask) | 编辑器 + headless | +| godot-cpp | submodule `extension/godot-cpp` @ `master`(pin `101ae38`) | master 默认 targets Godot 4.7 API(`GODOTCPP_DEFAULT_API_VERSION=4.7`);无 `4.6/4.7` 分支,只有 bundled `extension_api-4-7.json` | +| Xcode | 26.4.1 | macOS 构建 | +| CMake | 已装;**不需要 SCons** | godot-cpp 走 CMake 路径 | +| libgr2 | `../xrender-poc/libgr2`(sibling-dir 引用) | 见下「libgr2 依赖」 | + +**导出模板**(M0' T0.4 / 真正 export 才需要):编辑器内 `Editor → Manage Export Templates → Download`,或 `godot --headless --install-export-templates`。~600MB,版本须与编辑器一致(4.7.1)。 + +--- + +## 构建 + +```bash +git submodule update --init --recursive # 拉 godot-cpp(首次) +./build.sh # Debug;产物 → project/bin/libmtgodot.macos.template_debug.dylib +./build.sh Release # template_release +``` + +首次会编译 godot-cpp(几分钟)。 + +## 运行 + +```bash +# 编辑器打开 +godot -e --path project + +# headless 冒烟测试(验证 GDExtension 加载 + 类注册) +godot --headless --path project --quit-after 3 + +# 跑一个真实 gr2 过 libgr2(可选) +MTGODOT_PROBE_GR2="$PWD/../m2dev-client-main/assets/PC/ymir work/pc/warrior/warrior_cheongrin.gr2" \ + godot --headless --path project --quit-after 3 +``` + +控制台出现 `[mtgodot] Metin2Model registered OK — libgr2 linked; ...` = M0' 的 T0.1/T0.2 通过。 + +窗口内:拖拽 = 轨道,滚轮 = 缩放,`F2` = 截图(→ `user://`)。 + +--- + +## libgr2 依赖 + +M0' 直接用 **sibling-directory 引用**:`extension/CMakeLists.txt` 里 +`XRENDER_POC_DIR` 默认 `../../xrender-poc`,`add_subdirectory` 进 `libgr2`。 +两个 repo 都在 `.../mt/` 下时开箱即用。 + +换机 / CI 时二选一: +- `git submodule add third_party/xrender-poc` 后传 `-DXRENDER_POC_DIR=third_party/xrender-poc` +- vendor 一份 `libgr2/` 进本仓库 + +`formats/`(msa/msm)到 **M2** 才需要,届时同样方式接入。 + +--- + +## 目录 + +``` +mtgodot-poc/ + CMakeLists.txt 顶层(macOS only 守卫) + build.sh 便捷构建 + extension/ + CMakeLists.txt mtgodot SHARED + godot-cpp + libgr2 + godot-cpp/ submodule @ master + src/ + register_types.{h,cpp} GDExtension 入口,注册 Metin2Model + metin2_model.{h,cpp} M0' 占位节点(证明类注册 + libgr2 链接) + project/ Godot 4.7 工程 + project.godot + main.tscn 根 Node3D + main.gd + main.gd M0' harness:建 env/light/cam/cube + 探测扩展 + assets/.gdignore 让 Godot 不 import .gr2/.dds + bin/mtgodot.gdextension 扩展描述符(dylib 构建产物落这里) + export_presets.cfg macOS preset + docs/ + GODOT-POC-PLAN.md 开发计划(M0'–M4 + 风险 + 验收) + steps/ (按需) + test/golden/ 对拍截图(M1 起) +``` + +## 授权 + +`libgr2` 全自研,不含 Granny SDK 代码。本仓库内部研究用途,不对外公开。 diff --git a/build.sh b/build.sh new file mode 100755 index 00000000..e3fc7113 --- /dev/null +++ b/build.sh @@ -0,0 +1,19 @@ +#!/usr/bin/env bash +# Build the mtgodot GDExtension into project/bin/. macOS only (Phase 1). +set -euo pipefail +cd "$(dirname "$0")" + +CONFIG="${1:-Debug}" # Debug | Release +JOBS="${JOBS:-$(sysctl -n hw.ncpu)}" + +if [ ! -f extension/godot-cpp/CMakeLists.txt ]; then + echo "godot-cpp submodule missing — run: git submodule update --init --recursive" >&2 + exit 1 +fi + +cmake -S . -B build -DCMAKE_BUILD_TYPE="$CONFIG" +cmake --build build --config "$CONFIG" -j "$JOBS" + +echo +echo "Built libs in project/bin/:" +ls -la project/bin/*.dylib 2>/dev/null || echo " (none — check build output above)" diff --git a/docs/GODOT-POC-PLAN.md b/docs/GODOT-POC-PLAN.md new file mode 100644 index 00000000..8192e193 --- /dev/null +++ b/docs/GODOT-POC-PLAN.md @@ -0,0 +1,298 @@ +# Godot 渲染 Demo — 开发计划 + +> 内部研究方案,不对外公开。 +> 与 [`PLAN.md`](./PLAN.md) 是**两条并行的跨平台基座候选**:本文件评估「Godot 4 + 自研资源 loader」,`PLAN.md` 评估「自研引擎 + bgfx RHI」。 +> 复用 [`../libgr2`](../libgr2)、[`../formats`](../formats) 与 [`../oracle`](../oracle) 的真值数据。 + +| | | +|---|---| +| 状态 | PoC 规划 | +| 技术栈 | Godot **4.7.1** stable · GDExtension(`godot-cpp` `4.7` 分支)· C++20 | +| 目标平台 | **Phase 1:macOS**(当前)→ Phase 2:iOS · Android | +| 判定周期 | Phase 1 ≈ 3.5–5 周(单人)→ **中期评审**(不下结论);Phase 2 后 → **go / no-go** | +| 复用 | `libgr2`(gr2 v6 读取器)· `formats/`(textscript · msa · msm)· `oracle/` golden · bgfx demo 截图 | +| 自研 | `Metin2Model` GDExtension 节点(gr2 → `Skeleton3D` + `ArrayMesh` + 材质) | + +--- + +## 00 · 阶段划分 + +| 阶段 | 平台 | 里程碑 | 回答什么 | 产出 | +|---|---|---|---|---| +| **Phase 1** | macOS | M0' · M1 · M2 · **M2.5** | ① gr2 → Godot 场景对象能否正确映射;② 动画运行时(采样 / retarget / 事件)能否移植;③ **Metin2 观感能否在 Godot 里还原** | 中期评审报告:桥接层工作量实测 + 观感对比 + 剩余风险 | +| **Phase 2** | iOS · Android | M3(·M4 可选) | ④ 跨平台三端一致;⑤ 移动端性能 / 生命周期 | **go / no-go 结论** | + +**关键澄清(写给评审)**:Godot 在 macOS 上渲染一个蒙皮 gr2 **几乎不可能失败**。Phase 1 不是用来判断"该不该押 Godot"的——那个只有 Phase 2 能答。Phase 1 的价值是把**平台无关**的 GDExtension 工程(占本方案工程量的大头)做完,并确认视觉保真度可达,从而让 Phase 2 只剩"移动端 + 性能"这一个真未知。 + +**进入 Phase 2 的前置**:M2.5 门禁通过,且中期评审判定"桥接层工作量与观感均可接受"。否则在此转回 bgfx route,损失仅为 GDExtension 桥接层(≈ glTF importer 量级),`libgr2` 完整保留。 + +--- + +## 01 · 目标与判定标准 + +### Phase 1 要证明的三件事(macOS) + +1. **gr2 → Godot 场景对象** — `libgr2` 读出的骨架 / 蒙皮网格 / 权重能否正确映射到 `Skeleton3D` + `ArrayMesh` + `Skin`,覆盖多种骨架(不止 warrior)。 +2. **动画正确** — 每帧从 `libgr2::sample_pose` 驱动 `Skeleton3D`,结果与 bgfx demo / Granny oracle 逐帧一致;跨文件 retarget、motion event 成立。 +3. **观感可还原** — 多 texture-stage 材质、alpha test、顶点色、additive 混合、fog、方向光阴影在 Godot 里做出来,与原客户端目视接近。 + +### Phase 2 要证明的两件事(移动端,本轮不做) + +4. **跨平台一致** — 同一 GDExtension + Godot 工程在 iOS 真机 + Android 真机(Vulkan + Compatibility)渲染与桌面一致。 +5. **性能 / 生命周期达标** — 中端机单角色 / 多角色帧率达标,后台 / 恢复稳,Godot 节点开销可控。 + +### 明确排除(两个阶段都不做) + +SpeedTree 植被、地形(Metin2 户外地图)、UI / Python 脚本层、eterpack(Phase 1 用散文件)、GPU 蒙皮优化、LOD 策略、多角色 AI、网络、玩法。 + +> 地形与 `.mse` 特效在 Phase 1 也不做,但 **M2.5 会做通用 `ShaderMaterial` + 混合模式的地基**,为后续正式移植时的地形 / 特效渲染铺路。 + +### 判定口径 + +- **M2.5 门禁通过 + 中期评审"可接受"** = 进入 Phase 2。 +- **门禁失败区分两类**(同 `PLAN.md` §01 口径): + - **工程性延期** — 材质差色、某曲线子类型没实现、某 API 用错。有明确修法,记下继续。 + - **方案性死路** — 例如 Godot 节点 / 骨骼模型无法表达 Metin2 的多部件挂点体系、或多 stage 固定管线在 `ShaderMaterial` 里无法近似到可接受、或每帧写骨骼在合理规模下 Mac 上就已经卡。这才触发"转回 bgfx route"。 +- **最终交付(Phase 2 末)** 一页纸对比结论:与 bgfx route 在 **工作量 / 性能 / 视觉保真度 / 长期维护成本** 四轴上的取舍。 + +### 与 bgfx route 的关系 + +| | bgfx route(`PLAN.md`) | Godot route(本文件) | +|---|---|---| +| 渲染器 | 自研(bgfx 薄封装 + `.sc` 着色器) | Godot 内置渲染器 | +| 平台壳 / 输入 / 音频 / 打包 | 自己拼(SDL3 / 薄壳 + 各库) | Godot 全包 + 编辑器 | +| gr2 解析 | `libgr2` | `libgr2`(同一份) | +| 动画运行时 | 自研(复用 EterGrnLib 逻辑) | 映射到 `Skeleton3D` / `AnimationMixer` | +| 阴影 / 环境 / sky | 自研 | Godot 内置(M2.5 直接用) | +| 长期 | 自己养一个引擎 | 跟随 Godot 版本(GDExtension ABI 绑 minor) | + +**`libgr2` 在两条路里都是核心,完整复用。** + +--- + +## 02 · 架构总览 + +``` +mtgodot-poc/ + extension/ GDExtension(C++,godot-cpp 4.7) + src/ + register_types.cpp 注册 Metin2Model / Metin2AnimPlayer + metin2_model.{h,cpp} load_gr2 → Skeleton3D + ArrayMesh + Skin + material + metin2_anim.{h,cpp} 每帧 libgr2::sample_pose → Skeleton3D bone pose;跨文件 retarget;motion event + gr2_bridge.{h,cpp} libgr2 POD 视图 → Godot 数组打包;basis + 单位换算(集中一处) + dds_loader.{h,cpp} DDS → Godot Image(散文件路径用;res:// 里的 .dds 交 Godot 原生导入) + m2_material.{h,cpp} texture-stage 描述 → ShaderMaterial(M2.5) + libgr2/ submodule → ../xrender-poc/libgr2 + formats/ submodule → ../xrender-poc/formats(msa/msm) + SConstruct / CMakeLists godot-cpp 构建(Phase 1 只出 macOS arm64) + project/ Godot 4.7.1 demo 工程 + main.tscn orbit 相机 + DirectionalLight3D(+shadow) + WorldEnvironment(sky) + Metin2Model + main.gd 加载资产、切模型 / 动画、截图、性能 HUD + assets/ warrior + 另 2 种骨架的 gr2/dds + .gdignore ← 让 Godot 不去 import .gr2(见下) + export_presets.cfg macOS(Phase 1) + test/ + golden/ godot-macos-*.png + compare.py 与 ../xrender-poc bgfx demo / oracle 对拍 + docs/ + GODOT-POC-PLAN.md 本文件 + steps/ 按需拆 +``` + +### 分层(Phase 1) + +| 层 | 内容 | +|---|---| +| 平台 / 窗口 / 输入 / 打包 | **Godot**(macOS) | +| 渲染器 | **Godot 内置** Forward+(macOS / Metal) | +| 资源运行时 | **`Metin2Model` GDExtension**:gr2 → `Skeleton3D` / `ArrayMesh` / `Skin` / `ShaderMaterial` | +| 动画 | 每帧 `libgr2::sample_pose` 写 `Skeleton3D`(**B 方案**);A/B 决策**推迟到 Phase 2**(Mac 上信号不足),M2 只做一次 50 角色压力采样留档 | +| 解析 | **`libgr2`(复用)** · `formats/`(msa/msm,复用) · DDS | +| 数学 / 坐标 | Godot `Transform3D` / `Basis`;gr2 art-tool basis → Godot 右手 Y-up + 单位缩放,在 `gr2_bridge` 一次性处理,约定见 §03 | + +### 两个实操约定(Phase 1 T1 前定死,写进 `gr2_bridge` 注释) + +- **`.gr2` 不进 import 流程**:`project/assets/` 放 `.gdignore`,`.gr2` 由 `Metin2Model` 在运行时用绝对路径 / `user://` 直接读;`.dds` 可留在 `res://` 交 Godot 原生导入,或也走运行时 `dds_loader`(二选一,T1.3 决定并记录)。 +- **单位缩放**:定 `M2_TO_GODOT = 0.01`(Metin2 约 100 unit ≈ 1 Godot m),相机 near/far、`DirectionalLight3D` 阴影范围按此设。若与资产实测不符,在 T1.1 调整并记录。 + +--- + +## 03 · 技术选型 + +| 项 | 选择 | 理由 | +|---|---|---| +| 引擎版本 | **Godot 4.7.1 stable**(本机已装),锁死不随手升 | GDExtension ABI 绑 minor;升级作独立任务 | +| 扩展机制 | `godot-cpp` **`4.7` 分支**,GDExtension(非模块编译) | 用官方导出模板,不自定义 Godot 构建 | +| 构建工具 | `godot-cpp` 的 **CMake 路径**(本机已有 cmake;不装 SCons)或 `brew install scons` 二选一 | Phase 1 只出 macOS arm64,摩擦最小 | +| gr2 解析 | `xrender-poc/libgr2` 静态库嵌入 extension | 与 bgfx route 同一份,已过 9166 fuzz + oracle | +| 动画方案 | **B(每帧写骨骼)**;A(烘 `Animation`)**决策推迟到 Phase 2** | B 直接复用 `libgr2` 采样、最保真;A 的动机(多角色 CPU)在 Mac 上压不出来 | +| 骨骼权重 | `ArrayMesh` 4 权重优先;>4 影响则 `ARRAY_FLAG_USE_8_BONE_WEIGHTS` | M1 T1.2 早验,三种骨架都查 | +| 贴图 | 见 §02 实操约定(`.dds` 原生导入 vs 运行时 `dds_loader`,T1.3 定) | 两条都留 | +| 材质 | **自定义 `ShaderMaterial`**(`light()` 自定义着色贴 Metin2 观感);M1 先 `StandardMaterial3D` 上屏,M2.5 换 | 多 stage 只做近似,列差距清单 | +| 渲染器 | Forward+(macOS) | Phase 1 不涉及 Compatibility | +| 阴影 / sky | Godot 内置 `DirectionalLight3D` shadow + `WorldEnvironment` procedural sky,M2.5 接入 | 成本近零,Mac 观感必要 | +| 资产来源 | 从 `m2dev-client-main/assets` 拷 **warrior + 另 2 种骨架** 的 gr2/dds 到 `project/assets/`,散文件 | 不接 eterpack(Phase 2 / M4) | +| 真值源 | `xrender-poc/oracle` golden + bgfx demo 同机位 / pose / t 截图 | 复用现成对拍基建 | +| 仓库 | 新建 `mtgodot-poc/`,`libgr2` / `formats` submodule 指向 `xrender-poc` | 隔离,不污染 bgfx route | + +--- + +## 04 · 里程碑 + +### M0' · 脚手架(macOS,1–2 天) + +**目标**:GDExtension 骨架 + `libgr2` 集成 + Godot 工程能跑,仅 macOS。 + +| 任务 | 完成判据 | +|---|---| +| T0.1 `godot-cpp` 4.7 GDExtension 骨架,注册空节点 `Metin2Model`(继承 `Node3D`) | Godot 4.7.1 编辑器里能添加该节点、无报错 | +| T0.2 构建集成 `libgr2` 静态库(CMake 或 SCons),仅 macOS arm64 | `libgr2` 符号进 extension,`.dylib` 加载成功 | +| T0.3 Godot 工程骨架:`main.tscn`(orbit 相机 + `DirectionalLight3D` + `WorldEnvironment` + 空 `Metin2Model`),`main.gd`(orbit 控制 + 截图键) | 编辑器运行出带 sky 的场景 + 一个 cube,鼠标可轨道 | +| T0.4 `project/assets/.gdignore`;`export_presets.cfg` 加 macOS preset | 导出一次 macOS `.app`,空场景能跑 | + +**门禁 M0'**:macOS 上跑起「Godot 场景 + sky + cube + orbit 相机」,导出的 `.app` 也能跑。 + +--- + +### M1 · 静态渲染(1–1.5 周) + +**目标**:warrior + 另 2 种骨架的 bind pose + 贴图在 Godot 里正确显示,多部件组装。 + +对标 [`steps/M1-static-render.md`](./steps/M1-static-render.md) T2–T5。 + +| 任务 | 完成判据 | +|---|---| +| T1.1 `gr2_bridge`:`libgr2::Skeleton` → `Skeleton3D`(bone 名 / parent / rest `Transform3D`);basis + 单位缩放在此处理,约定写进注释 | 骨架层级树与 `gr2dump --members` 一致;三种骨架的骨骼数都正确 | +| T1.2 `libgr2::Mesh` → `ArrayMesh`(`ARRAY_VERTEX/NORMAL/TANGENT/TEX_UV/BONES/WEIGHTS`)+ `Skin`(bind 矩阵);处理 >4 权重 | 白模轮廓 = Blender 里同模型;无破面;三种骨架都过 | +| T1.3 贴图路径决策(`.dds` 原生导入 vs 运行时 `dds_loader`),实现选定方案;warrior 贴图(实测 512×512 DXT3 5mip) | UV 无错位 / 无镜像;决策写进 `extension/README.md` | +| T1.4 材质:`StandardMaterial3D`(albedo 贴图 + unshaded 先看轮廓),可切白模 | 贴图目视正确 | +| T1.5 多部件(body / weapon / hair)组装,武器挂 `BoneAttachment3D` | 部件数正确、武器跟手骨 | +| T1.6 `test/compare.py`:与 bgfx demo 同机位截图 | 几何一致(允许色彩空间 / 光照差异) | + +**门禁 M1**:warrior + 2 种骨架 bind pose 下与 bgfx demo 同机位截图**几何像素级接近**;顶点数、部件数、骨骼数与 oracle dump 一致。 + +--- + +### M2 · 骨骼动画(1.5–2 周) + +**目标**:从 GDExtension 每帧驱动 `Skeleton3D` 播动画,含跨文件 retarget 与 motion event。 + +对标 [`steps/M2-anim-skinning.md`](./steps/M2-anim-skinning.md)。 + +| 任务 | 完成判据 | +|---|---| +| T2.1 `Metin2AnimPlayer`:`_process` 里 `libgr2::sample_pose(t)` → 写 `Skeleton3D` 每骨 local `Transform3D` | 单个动画连续播放、不抖 | +| T2.2 跨文件 retarget:骨架 gr2 + 独立动画 gr2 / `.msa` 按骨名对齐(必要时 `RetargetModifier3D` / 骨名映射表) | warrior 骨架播 `dance_1`(独立文件)姿势正确 | +| T2.3 motion event:`.msa`/`.msm` 里的帧事件(放音 / 特效锚点 / 震屏)解析并派发(Phase 1 只需回调打点,不接实际特效) | 事件在正确帧触发,日志可见 | +| T2.4 `main.gd`:idle + 一个动作,按键 / HUD 按钮切换 | 切换无爆姿势 | +| T2.5 逐帧对拍:同 `t` 的 `Skeleton3D` 世界骨骼矩阵 vs bgfx demo / oracle | 差值在坐标系 / 单位换算误差内(沿用 M2 的 ~6.5e-5 量级口径);全程无 NaN | +| T2.6 全量采样自检:181 个 warrior 动画逐个 load + sample | 0 崩溃、0 NaN | +| T2.7 留档:50 角色同播动画的帧时间 / `_process` 耗时采样(**不作门禁**,给 Phase 2 A/B 决策用) | 数据写进 `test/godot-macos-stress.json` | + +**门禁 M2**:181 动画全部加载采样无 NaN;选定动作与 bgfx demo 同 `t` 视觉一致、数值在换算误差内;motion event 触发帧正确。 + +--- + +### M2.5 · 材质与观感保真(1–1.5 周)★ Phase 1 的核心门禁 + +**目标**:把 Metin2 的固定管线材质在 `ShaderMaterial` 里近似出来,让 Mac 版渲染"看起来做好了"。 + +| 任务 | 完成判据 | +|---|---| +| T2.5.1 `m2_material`:Metin2 材质描述(texture-stage op / arg / alpha op)→ 自定义 `ShaderMaterial`(Godot shading language,`light()` 自定义) | 单 stage `MODULATE(TEXTURE, DIFFUSE)` = `tex * vertex_color` 与 bgfx demo `texture_stage.sh` 输出一致 | +| T2.5.2 多 stage 近似:`MODULATE2X` / `ADD` / `BLENDDIFFUSEALPHA` 等常见组合 | warrior 全套贴图正确显示,与原客户端目视接近 | +| T2.5.3 alpha test(`D3DCMP_*`)+ alpha blend + additive(装备发光) | 半透明 / 发光部件正确,排序无明显错误 | +| T2.5.4 顶点色 + fog(线性 / exp2) | 与 bgfx demo 对拍一致 | +| T2.5.5 接 Godot `DirectionalLight3D` 阴影 + `WorldEnvironment`(procedural sky + ambient) | 角色有投影、受环境光;整体画面完整 | +| T2.5.6 差距清单:列出无法 1:1 还原的项(排序 corner case、特定 blend op、mip / 各向异性差异) | 写进中期评审报告 | + +**门禁 M2.5**:warrior 完整着装 + 动画,在 Godot 里与原客户端 / bgfx demo **目视接近**(结构化对比 + 直方图,不做像素比);差距清单里没有"方案性死路"项。 + +--- + +### M3 ·(Phase 2,本轮不做)iOS + Android + 性能 + +移动端真机、三端一致、性能、生命周期、Compatibility 渲染器、每帧写骨骼的 A/B 决策 —— **go / no-go 结论在此产生**。详见后续 `steps/M3-mobile.md`(届时再写)。 + +### M4 ·(Phase 2 可选拉伸) + +接真实 eterpack、试水一个 `.mse` 特效、多 texture-stage 精确对拍。 + +--- + +## 05 · 工作量(Phase 1,macOS) + +| 里程碑 | 单人 | 2 人并行 | +|---|---|---| +| M0' | 1–2 天 | 1 天 | +| M1 | 1–1.5 周 | 1 周(网格 / 骨架分工) | +| M2 | 1.5–2 周 | 1–1.5 周(动画 / 对拍分工) | +| M2.5 | 1–1.5 周 | 1 周(shader / 材质映射分工) | +| **Phase 1 合计** | **≈ 3.5–5 周** | **≈ 2.5–3.5 周** | + +前置依赖:`xrender-poc` 的 `libgr2`(已完成)、`oracle` golden(M2 对拍要)、bgfx demo(M1/M2/M2.5 截图对拍要,已可跑)、Godot 4.7.1 export templates(需在编辑器内下载)。 + +--- + +## 06 · 风险登记册 + +### Phase 1(macOS) + +| 风险 | 触发信号 | 缓解 / 退路 | +|---|---|---| +| `godot-cpp` + `libgr2` 构建集成摩擦 | M0' 链接 / 加载失败 | 只出 macOS arm64,摩擦面最小;CMake 路径优先,SCons 备选 | +| `.gr2` 被 Godot import 流程扫到报错 | 打开工程时一堆 import 错误 | `project/assets/.gdignore`;`.gr2` 走运行时绝对路径 / `user://`,T1 前定死 | +| gr2 basis / 单位换算到 Godot(左手 → 右手 Y-up + 缩放) | M1 模型躺 / 镜像 / 骨骼反向 / 大小离谱 | `gr2_bridge` 一次性处理,定 `M2_TO_GODOT`,用朝向明确资产手校,写注释 | +| `Skeleton3D` 骨骼数上限 / 8 权重路径行为 | M1 T1.2 蒙皮爆开 / 部位错位 | 三种骨架都查 `gr2dump`;>4 权重走 8-bone flag | +| **多 stage 固定管线材质无法在 `ShaderMaterial` 近似到可接受** | M2.5 T2.5.2/3 关键装备观感明显错 | **这是 Phase 1 主要方案性风险**;差距清单区分"可延期"vs"死路",后者触发转回 bgfx route | +| 半透明 / additive 排序 corner case | M2.5 T2.5.3 发光部件穿插 | 记入差距清单;Godot 的 `render_priority` / `depth_draw` 调,不追求 100% | +| motion event 语义(`.msa`/`.msm` 帧事件类型) | M2 T2.3 事件类型没覆盖全 | Phase 1 只需打点回调,实际特效 Phase 2;未覆盖类型列清单 | + +### Phase 2(移动端,本轮不激活,仅登记) + +| 风险 | 缓解 / 退路 | +|---|---| +| 每帧 GDExtension 写骨骼的 CPU 成本(多角色) | M2 T2.7 已留档;转 A 方案(烘 `Animation` + `AnimationMixer`) | +| Godot Compatibility(GLES3) 老安卓 shader 兼容 | M2.5 的 `ShaderMaterial` 写保守,避开 Compatibility 不支持特性 | +| Godot 节点开销(大场景规模) | `MultiMesh` / `RenderingServer` 直调兜底 | +| GDExtension ABI 绑 Godot minor | 锁 4.7.1,升级作独立任务 | +| iOS 交叉编译 `libgr2` 静态库 | 提前出 iOS `.a` 预编译产物 | + +--- + +## 07 · 验证方法论 + +- **真值源**:`xrender-poc/oracle` 的 Granny golden 数据 + `xrender-poc` bgfx demo(同机位 / pose / `t` 截图与数值 dump,Metal 后端在同一台 Mac 上跑)。 +- **每个门禁**存 `test/golden/godot-macos-.png` + 骨骼矩阵 / 顶点 dump,跑 `test/compare.py` 与 bgfx route 对拍。 +- **资产集**:`xrender-poc/test/assets.list` 里的 warrior 相关文件 + 另选 2 种骨架(记入本仓库 `test/assets.list`)。 +- **对拍容差**:几何 / 骨骼矩阵按坐标系 + 单位换算误差内严格比;颜色 / 光照因渲染器不同**不做像素比**,只做结构化目视 + 直方图。 + +--- + +## 08 · 交付物 + +### Phase 1(本轮) + +| 产物 | 位置 | +|---|---| +| `mtgodot-poc/` 仓库:GDExtension 源 + Godot 4.7.1 demo 工程 | 新仓库 | +| macOS 导出配置 | `project/export_presets.cfg` | +| 与 bgfx demo 的几何 / 动画对拍报告 | `test/godot-snapshot.json` | +| 材质差距清单 | 中期评审报告内 | +| 50 角色压力采样(供 Phase 2 A/B 决策) | `test/godot-macos-stress.json` | +| **中期评审报告** | 桥接层工作量实测 + 观感对比截图 + 剩余风险 + "是否进入 Phase 2"建议 | + +### Phase 2(后续) + +| 产物 | +|---| +| iOS `.app` + Android APK/AAB + 真机性能 / 生命周期报告 | +| **go/no-go 对比结论(一页纸)**:与 bgfx route 在工作量 / 性能 / 视觉保真度 / 长期维护四轴对比,给出客户端跨平台基座选型建议 | + +--- + +## 选型变更记录 + +- 2026-08-29 · 范围收敛为 **Phase 1 = macOS only**;新增 **M2.5 材质与观感保真** 作为 Phase 1 核心门禁;动画 A/B 决策推迟到 Phase 2;Godot 版本钉死 4.7.1 / godot-cpp 4.7。 +- *(待 M1 T1.3 填写:贴图走 `.dds` 原生导入 还是运行时 `dds_loader`)* +- *(待 M2.5 T2.5.6 填写:材质无法 1:1 还原项清单)* diff --git a/extension/CMakeLists.txt b/extension/CMakeLists.txt new file mode 100644 index 00000000..d605009d --- /dev/null +++ b/extension/CMakeLists.txt @@ -0,0 +1,37 @@ +# mtgodot GDExtension — M0' skeleton (registers Metin2Model, links libgr2). + +# --- godot-cpp (submodule @ master) --- +# master has no per-minor branch; it ships bundled extension_api-4-*.json. +# Pin to 4.7 to match the installed editor (4.7.1; patch-level ABI is compatible). +set(GODOTCPP_API_VERSION "4.7" CACHE STRING "Target Godot API version" FORCE) +add_subdirectory(godot-cpp) + +# --- libgr2 from the sibling xrender-poc repo --- +# M0': plain sibling-directory reference (both repos live under .../mt/). +# For a portable checkout, either git-submodule xrender-poc or vendor libgr2, +# then pass -DXRENDER_POC_DIR=/path/to/xrender-poc. +set(XRENDER_POC_DIR "${CMAKE_CURRENT_SOURCE_DIR}/../../xrender-poc" + CACHE PATH "Path to the xrender-poc repo (provides libgr2)") +if(NOT EXISTS "${XRENDER_POC_DIR}/libgr2/CMakeLists.txt") + message(FATAL_ERROR + "libgr2 not found at '${XRENDER_POC_DIR}/libgr2'. " + "Pass -DXRENDER_POC_DIR=/path/to/xrender-poc") +endif() +add_subdirectory("${XRENDER_POC_DIR}/libgr2" "${CMAKE_CURRENT_BINARY_DIR}/libgr2") + +# --- the extension shared library --- +add_library(mtgodot SHARED + src/register_types.cpp + src/metin2_model.cpp +) +target_compile_features(mtgodot PRIVATE cxx_std_20) +target_link_libraries(mtgodot PRIVATE godot::cpp xrender::libgr2) + +# Drop the dylib straight into the Godot project where the .gdextension expects it: +# project/bin/libmtgodot.macos.template_debug.dylib (Debug) +# project/bin/libmtgodot.macos.template_release.dylib (Release) +set_target_properties(mtgodot PROPERTIES + PREFIX "lib" + OUTPUT_NAME "mtgodot.macos.$,template_release,template_debug>" + LIBRARY_OUTPUT_DIRECTORY "${CMAKE_SOURCE_DIR}/project/bin" +) diff --git a/extension/godot-cpp b/extension/godot-cpp new file mode 160000 index 00000000..101ae380 --- /dev/null +++ b/extension/godot-cpp @@ -0,0 +1 @@ +Subproject commit 101ae38034304346a46ea9ea84ae156d3e860496 diff --git a/extension/src/metin2_model.cpp b/extension/src/metin2_model.cpp new file mode 100644 index 00000000..55dc5f8d --- /dev/null +++ b/extension/src/metin2_model.cpp @@ -0,0 +1,39 @@ +#include "metin2_model.h" + +#include +#include + +#include + +#include // libgr2 (xrender-poc) + +using namespace godot; + +namespace mtgodot { + +Metin2Model::Metin2Model() = default; +Metin2Model::~Metin2Model() = default; + +void Metin2Model::_bind_methods() { + ClassDB::bind_method(D_METHOD("libgr2_info"), &Metin2Model::libgr2_info); + ClassDB::bind_method(D_METHOD("probe_gr2", "path"), &Metin2Model::probe_gr2); +} + +String Metin2Model::libgr2_info() const { + // Forces a reference to a libgr2 symbol so the link is real, not dead-stripped. + return String("libgr2 linked; gr2::Magic enum size=") + itos((int)sizeof(gr2::Magic)); +} + +String Metin2Model::probe_gr2(const String &path) const { + gr2::LoadError err; + const std::string p(path.utf8().get_data()); + auto file = gr2::File::load_path(p, &err); + if (!file) { + return vformat("gr2 load FAILED [%s]: %s", + String(err.stage.c_str()), String(err.message.c_str())); + } + return vformat("gr2 OK: format_version=%d sections=%d", + (int)file->format_version(), (int)file->sections().size()); +} + +} // namespace mtgodot diff --git a/extension/src/metin2_model.h b/extension/src/metin2_model.h new file mode 100644 index 00000000..adcb2084 --- /dev/null +++ b/extension/src/metin2_model.h @@ -0,0 +1,31 @@ +#pragma once + +#include +#include + +namespace mtgodot { + +// M0' placeholder node. +// +// Purpose at this milestone: prove the GDExtension registers a class the Godot +// editor can instantiate, and that libgr2 is linked in and callable from here. +// M1 replaces the body with real Skeleton3D / ArrayMesh construction +// (see docs/GODOT-POC-PLAN.md §M1). +class Metin2Model : public godot::Node3D { + GDCLASS(Metin2Model, godot::Node3D) + +public: + Metin2Model(); + ~Metin2Model() override; + + // One-line tag proving libgr2 symbols resolved at link time. + godot::String libgr2_info() const; + + // Parse a .gr2 with libgr2 and report a couple of counts (no scene build yet). + godot::String probe_gr2(const godot::String &path) const; + +protected: + static void _bind_methods(); +}; + +} // namespace mtgodot diff --git a/extension/src/register_types.cpp b/extension/src/register_types.cpp new file mode 100644 index 00000000..bec16a51 --- /dev/null +++ b/extension/src/register_types.cpp @@ -0,0 +1,33 @@ +#include "register_types.h" + +#include +#include +#include + +#include "metin2_model.h" + +using namespace godot; + +void initialize_mtgodot_module(ModuleInitializationLevel p_level) { + if (p_level != MODULE_INITIALIZATION_LEVEL_SCENE) { + return; + } + GDREGISTER_CLASS(mtgodot::Metin2Model); +} + +void uninitialize_mtgodot_module(ModuleInitializationLevel p_level) { + (void)p_level; +} + +extern "C" { +GDExtensionBool GDE_EXPORT mtgodot_library_init( + GDExtensionInterfaceGetProcAddress p_get_proc_address, + const GDExtensionClassLibraryPtr p_library, + GDExtensionInitialization *r_initialization) { + godot::GDExtensionBinding::InitObject init_obj(p_get_proc_address, p_library, r_initialization); + init_obj.register_initializer(initialize_mtgodot_module); + init_obj.register_terminator(uninitialize_mtgodot_module); + init_obj.set_minimum_library_initialization_level(MODULE_INITIALIZATION_LEVEL_SCENE); + return init_obj.init(); +} +} diff --git a/extension/src/register_types.h b/extension/src/register_types.h new file mode 100644 index 00000000..6c942e64 --- /dev/null +++ b/extension/src/register_types.h @@ -0,0 +1,6 @@ +#pragma once + +#include + +void initialize_mtgodot_module(godot::ModuleInitializationLevel p_level); +void uninitialize_mtgodot_module(godot::ModuleInitializationLevel p_level); diff --git a/project/assets/.gdignore b/project/assets/.gdignore new file mode 100644 index 00000000..e69de29b diff --git a/project/bin/mtgodot.gdextension b/project/bin/mtgodot.gdextension new file mode 100644 index 00000000..fa0bfdd4 --- /dev/null +++ b/project/bin/mtgodot.gdextension @@ -0,0 +1,10 @@ +[configuration] + +entry_symbol = "mtgodot_library_init" +compatibility_minimum = "4.7" +reloadable = true + +[libraries] + +macos.debug = "res://bin/libmtgodot.macos.template_debug.dylib" +macos.release = "res://bin/libmtgodot.macos.template_release.dylib" diff --git a/project/bin/mtgodot.gdextension.uid b/project/bin/mtgodot.gdextension.uid new file mode 100644 index 00000000..8e6f1ae3 --- /dev/null +++ b/project/bin/mtgodot.gdextension.uid @@ -0,0 +1 @@ +uid://hc7fxxrjvpr8 diff --git a/project/export_presets.cfg b/project/export_presets.cfg new file mode 100644 index 00000000..253ace6a --- /dev/null +++ b/project/export_presets.cfg @@ -0,0 +1,61 @@ +[preset.0] + +name="macOS" +platform="macOS" +runnable=true +advanced_options=false +dedicated_server=false +custom_features="" +export_filter="all_resources" +include_filter="" +exclude_filter="" +export_path="../build/export/mtgodot-poc.app" +patches=PackedStringArray() +encryption_include_filters="" +encryption_exclude_filters="" +seed=0 +encrypt_pck=false +encrypt_directory=false +script_export_mode=2 + +[preset.0.options] + +export/distribution_type=1 +binary_format/architecture="arm64" +custom_template/debug="" +custom_template/release="" +debug/export_console_wrapper=1 +application/icon="" +application/icon_interpolation=4 +application/bundle_identifier="org.internal.mtgodotpoc" +application/signature="" +application/app_category="Games" +application/short_version="1.0" +application/version="1.0" +application/copyright="" +display/high_res=true +xcode/platform_build="14C18" +xcode/sdk_version="13.1" +xcode/sdk_path="/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk" +xcode/sdk_name="macosx13.1" +codesign/codesign=0 +codesign/identity="" +codesign/certificate_file="" +codesign/certificate_password="" +codesign/entitlements/allow_jit_code_execution=false +codesign/entitlements/allow_unsigned_executable_memory=false +codesign/entitlements/allow_dyld_environment_variables=false +codesign/entitlements/disable_library_validation=true +notarization/notarization=0 +privacy/microphone_usage_description="" +privacy/camera_usage_description="" +privacy/location_usage_description="" +privacy/address_book_usage_description="" +privacy/calendar_usage_description="" +privacy/photos_library_usage_description="" +privacy/desktop_folder_usage_description="" +privacy/documents_folder_usage_description="" +privacy/downloads_folder_usage_description="" +privacy/network_volumes_usage_description="" +privacy/removable_volumes_usage_description="" +ssh_remote_deploy/enabled=false diff --git a/project/main.gd b/project/main.gd new file mode 100644 index 00000000..89d4576a --- /dev/null +++ b/project/main.gd @@ -0,0 +1,125 @@ +extends Node3D +## mtgodot-poc — M0' harness. +## +## Builds the minimal scene the plan asks for (orbit camera + DirectionalLight3D +## with shadows + WorldEnvironment/sky + placeholder cube + an empty Metin2Model), +## then reports whether the GDExtension loaded. +## +## M1 will replace the cube with Metin2Model.load_gr2(...) output. + +const ORBIT_SPEED := 0.01 +const ZOOM_STEP := 0.3 +const PITCH_LIMIT := 1.45 + +var _cam: Camera3D +var _yaw := 0.6 +var _pitch := 0.45 +var _dist := 4.0 +var _target := Vector3(0.0, 1.0, 0.0) +var _dragging := false + + +func _ready() -> void: + _build_environment() + _build_sun() + _build_camera() + _build_placeholder() + _probe_extension() + print("[mtgodot] M0' harness ready — drag: orbit, wheel: zoom, F2: screenshot") + # Batch capture: MTGODOT_AUTOSHOT=/abs/path.png godot --path project + var autoshot := OS.get_environment("MTGODOT_AUTOSHOT") + if autoshot != "": + await get_tree().create_timer(0.8).timeout + var img := get_viewport().get_texture().get_image() + img.save_png(autoshot) + print("[mtgodot] autoshot -> ", autoshot) + get_tree().quit() + + +func _build_environment() -> void: + var we := WorldEnvironment.new() + var env := Environment.new() + var sky := Sky.new() + sky.sky_material = ProceduralSkyMaterial.new() + env.background_mode = Environment.BG_SKY + env.sky = sky + env.ambient_light_source = Environment.AMBIENT_SOURCE_SKY + env.tonemap_mode = Environment.TONE_MAPPER_FILMIC + we.environment = env + add_child(we) + + +func _build_sun() -> void: + var sun := DirectionalLight3D.new() + sun.rotation_degrees = Vector3(-45.0, -35.0, 0.0) + sun.shadow_enabled = true + add_child(sun) + + +func _build_camera() -> void: + _cam = Camera3D.new() + _cam.current = true + _cam.fov = 55.0 + add_child(_cam) + _update_camera() + + +func _build_placeholder() -> void: + var cube := MeshInstance3D.new() + cube.name = "Placeholder" + cube.mesh = BoxMesh.new() + cube.position = _target + add_child(cube) + + +func _probe_extension() -> void: + if not ClassDB.class_exists("Metin2Model"): + push_error("[mtgodot] Metin2Model NOT registered — GDExtension failed to load") + return + var m := ClassDB.instantiate("Metin2Model") as Node3D + m.name = "Metin2Model" + add_child(m) + print("[mtgodot] Metin2Model registered OK — ", m.call("libgr2_info")) + # Optional: point MTGODOT_PROBE_GR2 at a real .gr2 to exercise libgr2 end to end. + var probe := OS.get_environment("MTGODOT_PROBE_GR2") + if probe != "": + print("[mtgodot] probe_gr2(", probe, ") -> ", m.call("probe_gr2", probe)) + + +func _update_camera() -> void: + var offset := Vector3( + cos(_pitch) * sin(_yaw), + sin(_pitch), + cos(_pitch) * cos(_yaw)) * _dist + _cam.position = _target + offset + _cam.look_at(_target, Vector3.UP) + + +func _unhandled_input(event: InputEvent) -> void: + if event is InputEventMouseButton: + var mb := event as InputEventMouseButton + match mb.button_index: + MOUSE_BUTTON_LEFT: + _dragging = mb.pressed + MOUSE_BUTTON_WHEEL_UP: + _dist = maxf(0.5, _dist - ZOOM_STEP) + _update_camera() + MOUSE_BUTTON_WHEEL_DOWN: + _dist = minf(50.0, _dist + ZOOM_STEP) + _update_camera() + elif event is InputEventMouseMotion and _dragging: + var mm := event as InputEventMouseMotion + _yaw -= mm.relative.x * ORBIT_SPEED + _pitch = clampf(_pitch + mm.relative.y * ORBIT_SPEED, -PITCH_LIMIT, PITCH_LIMIT) + _update_camera() + elif event is InputEventKey: + var k := event as InputEventKey + if k.pressed and k.keycode == KEY_F2: + _screenshot() + + +func _screenshot() -> void: + var img := get_viewport().get_texture().get_image() + var path := "user://shot_%d.png" % Time.get_ticks_msec() + img.save_png(path) + print("[mtgodot] screenshot -> ", ProjectSettings.globalize_path(path)) diff --git a/project/main.gd.uid b/project/main.gd.uid new file mode 100644 index 00000000..e3d067fe --- /dev/null +++ b/project/main.gd.uid @@ -0,0 +1 @@ +uid://clyxqop8cvbbe diff --git a/project/main.tscn b/project/main.tscn new file mode 100644 index 00000000..a2d273d3 --- /dev/null +++ b/project/main.tscn @@ -0,0 +1,6 @@ +[gd_scene load_steps=2 format=3] + +[ext_resource type="Script" path="res://main.gd" id="1_main"] + +[node name="Main" type="Node3D"] +script = ExtResource("1_main") diff --git a/project/project.godot b/project/project.godot new file mode 100644 index 00000000..95e64813 --- /dev/null +++ b/project/project.godot @@ -0,0 +1,19 @@ +; Godot 4.7 project — mtgodot-poc Phase 1 (macOS) +; See ../docs/GODOT-POC-PLAN.md + +config_version=5 + +[application] + +config/name="mtgodot-poc" +run/main_scene="res://main.tscn" +config/features=PackedStringArray("4.7", "Forward Plus") + +[debug] + +gdscript/warnings/untyped_declaration=1 + +[rendering] + +renderer/rendering_method="forward_plus" +anti_aliasing/quality/msaa_3d=2 diff --git a/test/golden/m0-scaffold.png b/test/golden/m0-scaffold.png new file mode 100644 index 00000000..f41e0f08 Binary files /dev/null and b/test/golden/m0-scaffold.png differ