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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WaHYEY9rwLWt21PULiYjeJ
This commit is contained in:
Claude
2026-08-29 09:09:55 +09:00
commit c52b8657c4
21 changed files with 839 additions and 0 deletions
+16
View File
@@ -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
+4
View File
@@ -0,0 +1,4 @@
[submodule "extension/godot-cpp"]
path = extension/godot-cpp
url = https://github.com/godotengine/godot-cpp.git
branch = master
+21
View File
@@ -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)
+111
View File
@@ -0,0 +1,111 @@
# mtgodot-poc
「**Godot 4 + 自研资源 loader**」跨平台方案的渲染 Demoroute ①)。
用一个 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 <xrender-poc url> 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 代码。本仓库内部研究用途,不对外公开。
Executable
+19
View File
@@ -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)"
+298
View File
@@ -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 1macOS**(当前)→ Phase 2iOS · 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 脚本层、eterpackPhase 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/ GDExtensionC++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;跨文件 retargetmotion 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 描述 → ShaderMaterialM2.5
libgr2/ submodule → ../xrender-poc/libgr2
formats/ submodule → ../xrender-poc/formatsmsa/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 macOSPhase 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 skyM2.5 接入 | 成本近零,Mac 观感必要 |
| 资产来源 | 从 `m2dev-client-main/assets`**warrior + 另 2 种骨架** 的 gr2/dds 到 `project/assets/`,散文件 | 不接 eterpackPhase 2 / M4 |
| 真值源 | `xrender-poc/oracle` golden + bgfx demo 同机位 / pose / t 截图 | 复用现成对拍基建 |
| 仓库 | 新建 `mtgodot-poc/``libgr2` / `formats` submodule 指向 `xrender-poc` | 隔离,不污染 bgfx route |
---
## 04 · 里程碑
### M0' · 脚手架(macOS12 天)
**目标**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 · 静态渲染(11.5 周)
**目标**warrior + 另 2 种骨架的 bind pose + 贴图在 Godot 里正确显示,多部件组装。
对标 [`steps/M1-static-render.md`](./steps/M1-static-render.md) T2T5。
| 任务 | 完成判据 |
|---|---|
| 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.52 周)
**目标**:从 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 · 材质与观感保真(11.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 1macOS
| 里程碑 | 单人 | 2 人并行 |
|---|---|---|
| M0' | 12 天 | 1 天 |
| M1 | 11.5 周 | 1 周(网格 / 骨架分工) |
| M2 | 1.52 周 | 11.5 周(动画 / 对拍分工) |
| M2.5 | 11.5 周 | 1 周(shader / 材质映射分工) |
| **Phase 1 合计** | **≈ 3.55 周** | **≈ 2.53.5 周** |
前置依赖:`xrender-poc``libgr2`(已完成)、`oracle` goldenM2 对拍要)、bgfx demoM1/M2/M2.5 截图对拍要,已可跑)、Godot 4.7.1 export templates(需在编辑器内下载)。
---
## 06 · 风险登记册
### Phase 1macOS
| 风险 | 触发信号 | 缓解 / 退路 |
|---|---|---|
| `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-<case>.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 2Godot 版本钉死 4.7.1 / godot-cpp 4.7。
- *(待 M1 T1.3 填写:贴图走 `.dds` 原生导入 还是运行时 `dds_loader`)*
- *(待 M2.5 T2.5.6 填写:材质无法 1:1 还原项清单)*
+37
View File
@@ -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.$<IF:$<CONFIG:Release>,template_release,template_debug>"
LIBRARY_OUTPUT_DIRECTORY "${CMAKE_SOURCE_DIR}/project/bin"
)
Submodule extension/godot-cpp added at 101ae38034
+39
View File
@@ -0,0 +1,39 @@
#include "metin2_model.h"
#include <godot_cpp/core/class_db.hpp>
#include <godot_cpp/variant/utility_functions.hpp>
#include <string>
#include <gr2/gr2.h> // 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
+31
View File
@@ -0,0 +1,31 @@
#pragma once
#include <godot_cpp/classes/node3d.hpp>
#include <godot_cpp/variant/string.hpp>
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
+33
View File
@@ -0,0 +1,33 @@
#include "register_types.h"
#include <gdextension_interface.h>
#include <godot_cpp/core/defs.hpp>
#include <godot_cpp/godot.hpp>
#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();
}
}
+6
View File
@@ -0,0 +1,6 @@
#pragma once
#include <godot_cpp/core/class_db.hpp>
void initialize_mtgodot_module(godot::ModuleInitializationLevel p_level);
void uninitialize_mtgodot_module(godot::ModuleInitializationLevel p_level);
View File
+10
View File
@@ -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"
+1
View File
@@ -0,0 +1 @@
uid://hc7fxxrjvpr8
+61
View File
@@ -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
+125
View File
@@ -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))
+1
View File
@@ -0,0 +1 @@
uid://clyxqop8cvbbe
+6
View File
@@ -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")
+19
View File
@@ -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
Binary file not shown.

After

Width:  |  Height:  |  Size: 176 KiB