Files
shenandClaude Sonnet 5 47baf6c0c6 Metin2 game client (P0–P11) + mobile asset pipeline
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
2026-08-31 20:02:12 +09:00

14 KiB
Raw Permalink Blame History

M1 · macOS 静态渲染

总纲:../PLAN.md §03 / §04 / §06。演示切片见 ../DEMO-PLAN.md D0–D4。 本文件是 M1 的详细施工文档。


目标

sokol_app/SDL2 窗口 + bgfx(Metal),把 M0 读出的 gr2 静态网格(绑定姿势)+ DDS 贴图渲染出来, 轨道相机可转。同时验证 bgfx ↔ 窗口层的交接PLAN §03 的高风险点)。

前置

  • M0 完成:libgr2 能出骨架 + 网格;test/assets.list 产出。
  • oracle 的 T6(确定态截图)—— M1 视觉对拍要它。
  • EterImageLibCDXTCImage 冻结拷进 reuse/

交付物

产物 位置
xrender-demo 可执行(macOS build/app/
engine/rhi.{h,cpp} bgfx 薄封装 engine/
engine/scene.{h,cpp} camera.{h,cpp} engine/
着色器:vs_pnt / fs_pnt / fs_normalviz + texture_stage.sh app/shaders/
交接方案决议(sokol_app 还是 SDL2 记进 app/README.md

任务分解(按 DEMO-PLAN 的 D0D4 切)

T0a · toolchain 骨架(对应 D0a

  • CMakeLists.txtadd_subdirectory(third_party/bgfx.cmake)pin bgfx 到与 .sc 草稿相近版本。
  • 窗口层默认用 bgfx 自带 entry 或 SDL2(零风险,bgfx 所有 example 用的)——不是 sokol_app。
  • 一个清屏 appbgfx::setViewClear(0x303030ff) + bgfx::setDebug(BGFX_DEBUG_STATS) + bgfx::frame()
  • 编一个 dummy .scvs_flat.sc / fs_flat.sc),过 shaderc -p metal --platform osx + bin2c,断言 *.sc.bin.h 生成 —— 提前验证 shaderc 链路(否则要到 T2 才触发)。
  • hi-dpiresize 事件里 bgfx::reset(fbWidth, fbHeight)retina 上用 framebuffer 像素,不是点)。
  • 完成判据:窗口出现纯色背景 + stats 角标;*.sc.bin.h 已生成。

T0b · sokol_app ↔ bgfx 交接实验(对应 D0b,不阻塞

  • 另开一个 target,用 sokol_app.hsapp_metal_get_layer()CAMetalLayer*)填 bgfx::PlatformData::nwhinit.type = Metal
  • 已知障碍:sokol_app 会自建 MTKView + 自己 present,和 bgfx 抢 layerPLAN §03)。
  • 成了 → M1 之后统一切 sokol_app(为移动端生命周期铺路)。没成 → demo 全程用 T0a 的 SDL2/entrysokol_app 留到 M3 再单独攻。
  • 完成判据:二选一有明确结论并记进 app/README.md

T1 · engine/rhi —— bgfx 薄封装(≈150 行)

  • 接口签名见 DEMO-PLAN §5「engine/rhi」。语义对齐 StateManager
  • 关键:init(nativeHandle,w,h)createVB/IB/Tex2D/ProgramsetModel/setBones/setTexture/setStageUniformssubmitbeginFrame/endFrame
  • 深度范围 / Y 翻转:用 bgfx::getCaps()->homogeneousDepth / originBottomLeft 决定 bx::mtxProj 参数,别手写。

T2 · engine/scene + vs_pnt/fs_pnt —— 白模上屏(对应 D2

  • scenelibgr2::Mesh → 打包顶点 → bgfx::VertexLayout(映射表见 DEMO-PLAN §7.3)→ createVB;索引 → createIB;每 TriGroup 一个 draw item。
  • vs_pnt.sc / fs_pnt.sc:从 shaders.rar 拷改。vs_pnta_color0fog 先关。
  • camera.{h,cpp}:轨道相机(鼠标拖 = 绕轨道、滚轮 = 距离)。
  • 坐标系:加固定 basisFixGranny art-tool basis → 左手 Y-up),用朝向明确的资产手调到"warrior 正着站、面朝 +Z",写进 scene.cpp 注释。
  • 完成判据:可转的白模,轮廓 = Blender 里同模型。

T3 · DXT 解码 + 贴图(对应 D3)

  • reuse/EterImageLibCDXTCImageLoadHeaderFromMemory + LoadFromMemory + Decompress(0, rgba)
  • warrior_cheongrin.dds 实测 512×512 DXT3 5mip —— 先只传 level 0BGFX_SAMPLER_MIN_POINT 关 mip;T3 过后再补全 mip 链。
  • texture_stage.shshaders.rar 原样拷,喂"单 stage MODULATE(TEXTURE, DIFFUSE)"的 uniform(等价 tex * vertexColor)。
  • T 切贴图 / 白模。
  • 完成判据:UV 无错位、无镜像;和 oracle 确定态截图目视一致。

T4 · 自检工具(对应 D4

  • fs_normalviz.sc10 行):gl_FragColor = vec4(v_normal*0.5+0.5, 1)。按 N 切。
  • 线框:BGFX_STATE_PT_LINESbgfx::setDebug(BGFX_DEBUG_WIREFRAME)。按 W 切。
  • 完成判据:法线朝外;三角组分段可见且正确。

T5 · 多 stage 材质专项(PLAN §04 要求)

  • test/assets.list 挑一个用了 D3DTOP_MODULATE2XADDSIGNED 的材质,单独渲一帧和 oracle 确定态截图对拍。
  • 不能只靠整场景 SSIM 兜 texture_stage.sh 的正确性。
  • 完成判据:该材质的着色结果与 oracle 一致(差异分类内)。

门禁(go / no-go

  • 子门禁 1(交接)bgfx 经窗口层 handle 在 macOS 出画面 + 输入可用。sokol_app 啃不下 → 用 SDL2/entry,本门禁照样算过(方案已决议)。
  • 子门禁 2(几何):网格拓扑、UV 正确;无背面剔除错误 / 法线翻转。
  • 子门禁 3(材质):一个多 stage 材质的着色结果与 oracle 对上。

验证

  • 对比场景两侧都喂同一张预解码 RGBA(绕开 DXT),截图 diff 只反映几何 / 光照 / texture-stage,不被"软解 vs 硬件 S3TC"的 bit 级差异污染。
  • oracle 侧进入确定态(T6:注入相机矩阵、固定光、绑定姿势、无程序化摇摆)。
  • 与 oracle 截图像素 diff + SSIM;差异分类见 PLAN §07 视觉层。
  • 法线可视化模式自检。

本步风险(从 PLAN §08 筛)

风险 状态 / 缓解
sokol_app + bgfx 抢 context 规避M1 用 GLFWGLFW_NO_API + glfwGetCocoaWindowPlatformData.nwh),零冲突。sokol_app 留到 M3。
.sc 草稿针对某 bgfx 版本,shaderc 编不过 已解vs_pnt/fs_pntshaders.rar 改写对上当前 pin 的 bgfx_shader.shshaderc -p metal 编过。
reuse/EterImageLib 从没链接过 规避DXT1/3/5 自研 engine/dxt.cpp~180 行),完全不碰 EterImageLib。
坐标系 basiswarrior 躺着 / 镜像 / 巨大 已解basis_fix = rotX(-90°)·scale(0.01)warrior 正着站、比例约 1.7m。写死在 scene.cpp
DXT 软解 vs 客户端 GPU S3TC 色差 对拍时两侧都用软解 RGBAengine/dxt.cpp 产出,喂给 oracle 侧同一张)。
hi-dpi 视口 on_fb_sizeglfwGetFramebufferSizerhi::resetframebuffer 像素)。

DoD 清单

  • xrender-demo 在 macOS 起窗口(GLFW+ bgfx Metal init 成功(renderer: Metal)(T0a
  • 交接方案定为 GLFW,记进 app/README.mdT0b)。sokol_app 不用(抢 layer),留到 M3。
  • warrior 绑定姿势上屏,正着站、basis_fix = rotX(-90°)·scale(0.01)Granny Z-up cm → Y-up m),轨道相机 + 键位 T/N/W/RT2
  • DXT1 / DXT3 软解(engine/dxt.cpp,绕开 reuse/EterImageLib),贴图 + UV 正确、无镜像(warrior_cheongrin.dds 512² DXT3、warrior_face.dds 256² DXT1)(T3
  • N 法线可视化(朝外、平滑)/ W 线框(拓扑正确)自检(T4
  • 一个多 stage 材质与 oracle 对拍通过(T5)—— 待 oracletexture_stage.sh 单独走 fs_stage.scM1 主路径用固定 MODULATE(TEXTURE,DIFFUSE)
  • 与 oracle 确定态截图 SSIM ≥ 0.98 或差异分类通过 —— 待 oracle

M1 实现结果

结果
依赖 bgfx.cmake + bx/bimg/bgfx + glm + glfwthird_party/pin 见 VERSIONS.md)。tools/bootstrap-submodules.sh 拉。
构建 cmake -B build -DXRENDER_BUILD_DEMO=ON -DXRENDER_BUILD_TOOLS=OFFcmake --build build --target xrender-demo。着色器经 bgfx 的 shaderc 编成 metal .bincmake/xrender-shaders.cmake),运行时加载。
无头验证 XR_HIDDEN=1(隐藏窗口)+ XR_SCREENSHOT=<path>bgfx requestScreenShot → TGArhi 自带 CallbackI+ `XR_MODE=normals
参考截图 test/golden/m1-warrior_cheongrin-bindpose-textured.png-normals.png
几何 5 meshObject16 / face / Object03 / Object09 / body),顶点/索引数与 M0 一致;bind pose = Granny T-pose;朝向、比例正确
① 近期 bgfx 把 platform.h 并进 bgfx.h;② bgfx::setUniform 要在每次 submit 前调(frame 头单调一次不可靠)→ rhi 缓存 light/stage 每 draw 重设;③ 截图 TGA 带 alpha 通道,shader 输出非 1 的 alpha 会让 PNG 查看器合成成白 → 不透明物体 shader 固定 gl_FragColor.a = 1,截图 writer 也强制 alpha 255

画质改进 pass(档 1 + 档 2)

用户反馈"渲染毛糙"后做的一轮画质提升。基线:无 MSAA / 无 mip / 平光 / 贴图靠文件名瞎猜 / 无 sRGB。

档 1 —— 采样与着色正确性(engine/ + app/shaders/

做法
MSAA rhi.cpp init/resetBGFX_RESET_MSAA_X4state 加 BGFX_STATE_MSAA
mip 链 engine/dxt.{h,cpp} 重写:decode()dwMipMapCount(off 28) 循环解每一级;Image.mips 存 level0..N。scene.cpp 把各级拼成一块传 rhi::create_tex2d(packed, …, mips)bgfx hasMips = mips>1
sRGB 正确性 create_tex2dBGFX_TEXTURE_SRGB(采样自动 sRGB→linear);fs_pnt.sc 线性空间着色,末尾 pow(col, 1/2.2) 编回显示空间
各向异性过滤 create_tex2dBGFX_SAMPLER_{MIN,MAG}_ANISOTROPIC
半球环境光 fs_pnt.scmix(u_ambientGround, u_ambientSky, n.y*0.5+0.5) 代替常数 ambient
3 盏方向光 LightDescdir[3]/color[3]key/fill/顶光),.w = 强度;u_lightDir/u_lightColorVec4,3 uniform
双面光照 fs_pnt.scabs(dot(n,l))(薄片/头发/飘带两面都受光,和 Metin2 一致)
法线 renormalize vs 输出前 + fs normalize(v_normal)

档 2 —— 材质绑定(libgr2 + engine/

这是"毛糙"的最大来源:原来整个模型套一张 <gr2 名>.ddssura_lord 根本没有 sura_lord.dds → 纯黑。

做法
libgr2 解材质 gr2_mesh.cpp material_texture_name()granny_material → 直接 .Texture.FromFileName,或递归 .Maps[].Map注意:本版 Granny 里 granny_material_map 的子材质成员名是 Map 不是 Material)。gr2_fileinfo.cppFileInfo.materials(顶层表,调试用)。
每网格贴图表 Mesh.material_textures(与 granny_mesh.MaterialBindings 平行);tri_group.material_index 索引它
逐 tri_group 上贴图 scene.cppSubMesh.rangesDrawRange{ib,index_count,tex,alpha_cutout}),一个网格按 tri_group 切多段,每段切出独立 IB,各自贴图。贴图按 FromFileName 的 basename 在 gr2 同目录里大小写不敏感查找,带缓存。找不到时回落老的 stem 猜测。
bind pose 也要蒙皮 很多 Metin2 模型(shaman_lord 等)的 raw 顶点不在 bind 空间,直接上 raw 会整块错位。Scene::set_bind_pose() 有骨架时走 File::bind_pose(0) 的蒙皮矩阵(= set_pose);bounds/相机框选也按 bind 姿势的顶点算。
alpha-test(镂空) 解贴图时看 mip0 的 alpha 分布:同时有 >5% 近 0 且 >20% 近 255(双峰)才判 cutout(头发/飘带/树叶)。只看"低 alpha 比例"会误伤 alpha 平面全 0 的不透明 DXT3(整块被 discard)。rhi::set_alpha_test 逐 draw 开,fs_pnt.sctexColor.a < ref → discard

排障中发现并修掉的两个真 bug

bug 现象 根因 / 修复
多材质网格第 2 段起整块飞出视锥 shaman_lord(1 网格 2 材质组)只渲出头顶一小撮;任何多组网格丢掉第一组之后的内容 Scene::drawrhi::set_model()(→ bgfx::setTransform)每网格只调一次,但 bgfx 每次 submit 消费一次 transform。第 2 个 draw range 没设 transform → 用单位阵 → 顶点(~120 单位的 skin 空间坐标)画在 basis_fix 之外。修复set_model 移进 range 循环,逐 submit 重设。
bind pose 直接上 raw 顶点 shaman_lord 等模型整体错位、相机框选发飞(模型变成一个远处的点) raw 顶点不在 bind 空间。修复set_bind_pose 走蒙皮矩阵(见上表)。

调试加了 XR_CAM_YAW / XR_CAM_PITCH / XR_CAM_DIST(乘子)env 覆盖初始相机,XR_VERBOSE 打 bounds / 相机参数,方便无头抽查各角度。

结果

  • warrior_cheongrin / warrior_novice / sura_lord / assassin / shaman_lord / snakeman 等正/背/动画各角度截图:贴图正确、比例朝向对、边缘平滑、明暗有层次(见 test/render-samples/
  • 无回归gr2fuzz 9166/9166、render_fuzz 9166/91660 crash/empty/nan)、oracle 逐字段对拍 23/23(≤6.5e-5)、iOS 交叉编译通过

明确不做(本 POC 范围外,记档)

原因
多 stage 材质混合(fs_stage.sc Metin2 角色基本是单 stage MODULATE(TEXTURE,DIFFUSE);多 stage 主要用于地形。texture_stage.sh 已备,未接主路径
alpha blend(半透明排序) cutoutalpha-test)已覆盖头发/飘带主要场景;真半透明要 OIT 或按深度排序,收益低
顶点色 语料里角色网格全是 PNT332(无 color 分量)
.msm 装配(换发型/换肤) formats/msm.cpp 已能解析,装配是模型组合子系统,非渲染画质
法线贴图 / 切线帧 PNT332 无切线;Metin2 资产也没有法线贴图
阴影 / IBL / FXAA / LOD 选择 已有 MSAA x4;阴影/IBL 是独立子系统,env map 资产缺失;FXAA 在 MSAA 之上边际收益小
特效 / 粒子 / 地形 / 水 / SpeedTree 独立子系统,POC(读+渲+动 .gr2 骨骼模型)范围外