[功能] 绘制对象层:火把/火盆、木桶/木箱/篮子/箱子、尸体、武器架、传送点…(数据已进包,只差解码与绘制) #5

Closed
opened 2026-09-14 05:56:45 +00:00 by troytt · 1 comment
Owner

现象

62 张烘焙地图上一个对象都没有画出来。城镇里看不到火把与火盆,地下城里看不到木桶/木箱/篮子/箱子/尸体/武器架/传送点;HUD 里的"对象 N"只是一个计数(src/scene/act-scene.ts:401/540/772/882)。

现状数据(全部来自仓库自带的 samples/d2-packs/*/*/scene.json)

指标 值
62 关对象总数 2462
已解析出美术成员 2379(138 个唯一成员)
解析不出美术成员 83 → 其中 66 个本来就无美术(60 个 token 为空的占位对象 id 2/36/38 + 6 个 TA = "Dummy-invisible town sound" 隐形音效点),17 个是 NPC/怪物基址(见 #6)
stats.objectsArtPending 2462(打包器自己标"全部待接")
objectPages [](一个对象图集页都没产出)
每个对象的 frame 全部为 null

每个对象的"位置 + 元数据"其实都已经进包了,字段现成可用:

{ "id": 17, "type": 2, "name": "Dummy", "token": "BR", "mode": "OP",
  "objectsTxtId": 29, "hp": 0,
  "member": "data\\global\\objects\\BR\\S1\\brs1litophth.dcc",
  "alternatives": 8, "x": 3280, "y": 1648, "depth": 41.2, "frame": null }

出现的对象类型(按数量,节选):etorch2|6E 236、torch1|3M 57、etorch1|6D 58、torch2|3N 39、torch1|5R/5S 各 32、icecave_torch1/2 各 1;fire|FX/FY/FZ/RB 132;chest|* 40+、basket|XP 14、Barrel|B1 10、Crate|CT 2、Casket|VB 67、jar1|6I 30、evilurn 4;WeaponRack|W1/W2 11、ArmorStand|A3/A4 8;RogueCorpse/corpse/dead body/skeleton/wirt's body;Waypoint|* 12、Shrine|*、groundtomb 40、bank|B6 33、hellgate|6P 32。

关于"地毯":D2 的对象表里没有 rug/carpet 行(reference/opendiablo2/d2core/d2records/ 的对象表与 act 表都查不到),这类美术走的是 DS1 的地板层,本引擎地面层已经在画。如果你在具体场景里看到缺地毯,请告诉我是哪个场景,我再回 DS1 地板层里核。

排查:为什么没画出来

  1. 打包器只解析、不解码 —— scripts/pack-act-assets.ts:470-525 的对象循环里:resolveDs1Object → pickObjectMember → 只记 member/alternatives 与计数,const objectPages = new PageBuilder(palette) 建出来后从未 add 任何帧,于是 objectPages.encode(0) 返回空数组,objectsArtPending: objects.length 把所有对象都标成待接。这里就是接入点。
  2. 页面没有绘制代码 —— src/scene/act-scene.ts 里 objects 只用于 HUD 计数;map.html 沙盒同理。绘制需要:member → 解 DCC → 首帧 → 按锚点定位 → 按 depth 插进画家序。
  3. 但解码与定位的地基已经打好了,不用从零写:
    • src/game/objects.ts:objectCofMember(DATA\GLOBAL\OBJECTS\<TOKEN>\COF\<TOKEN><MODE>hth.COF)、objectSpriteMember(<TOKEN>\<COMPONENT>\<token><component>lit<mode>hth.dcc)、resolveObjectArt、objectAnchor/objectDrawAnchor(锚点 = DUNGEON_GameToClientSubtileDrawPositionCoords);
    • scripts/verify-dcc.ts 已经在真归档上解过对象 COF+DCC(OBJECT_DCCS / OBJECT_COFS,含 data\global\objects\l2\...);
    • src/game/character.ts 的 COF+DCC 合成、src/render/atlas.ts 的图集、src/game/animation.ts 的 ActorAnimator 都可直接复用。
  4. 深度插入:包里的 depth = (x+y)/5 就是等距深度键,前端按它插进 runtime.walls 的画家序即可(act-scene.ts 里已有同款 depthInsertIndex)。火把/箱子/桶这类要按深度插;平铺在地面的对象(如果有)应放在地面层。
  5. 调色板要对齐:原版的对象/怪物合成走 data\global\palette\Units\pal.dat(归档里确实有这个成员,data\global\palette\Units\pal.dat),而当前页面把角色画在 act 调色板上(loadCharacterArt 收的是 runtime.palette)。接对象层时先确认用哪套,否则颜色会偏。
  6. 动画:包里 mode 分布 NU 1587 / OP 542 / ON 327 / S1 4 / S2 2;火把燃烧、传送点旋转这类需要按 25Hz tick 推帧(ActorAnimator 可复用)。alternatives 分布 3/6/8/4/… 是同一 style 下的美术变体数,决定了"同一 token 有多套图"。
  7. 可破坏与交互(开箱子/开门) 依赖 hp(已进包)与状态机,建议另开 issue,本 issue 只到"画出来 + 动画"。

建议的落地顺序

  1. 静态首帧:按 member 解 DCC → 取首帧 → objectDrawAnchor 定位 → 按 depth 插入画家序;同时把美术烘进包(复用 PageBuilder,输出 objects-*.png + 每对象的 frame 索引/偏移,页面首屏只加载视口需要的页);
  2. mode 与多帧:火把/传送点/熔炉这类循环动画;OP/ON 模式的差异要按 Objects.txt(src/game/objects.ts 的 ObjectsRow)驱动;
  3. 变体:alternatives 的选图规则(按 level seed + 位置,与瓦片 RarityFrameIndex 的加权随机同一套思路);
  4. 之后才是交互/可破坏。

验收建议

  1. verify:packs 增加"对象绘制条目"的逐项比对(帧、位置、深度次序),与现有"包 vs 现读现解"同口径;
  2. scripts/browser/checks/ 增加一条断言:进入城镇/地下城后对象绘制数 > 0(现有 5 个检查脚本已有同款模式);
  3. 人眼验收各一张:Act 1 城镇(火把/火盆/武器架)、Catacombs 4(火把密集)、Crypt(烛台/棺材)、Act 5 Town;
  4. 文档/HUD 把"可破坏物品的位置与美术成员已进包,静态帧待接"改成"已绘制",避免文案与实现继续对不上。

(关联:NPC 另见 #6;对象层的朝向依赖 #3 的方向口径。)

## 现象 62 张烘焙地图上**一个对象都没有画出来**。城镇里看不到火把与火盆,地下城里看不到木桶/木箱/篮子/箱子/尸体/武器架/传送点;HUD 里的"对象 N"只是一个计数(`src/scene/act-scene.ts:401/540/772/882`)。 ## 现状数据(全部来自仓库自带的 `samples/d2-packs/*/*/scene.json`) | 指标 | 值 | | --- | --- | | 62 关对象总数 | **2462** | | 已解析出美术成员 | **2379**(138 个唯一成员) | | 解析不出美术成员 | 83 → 其中 66 个本来就无美术(60 个 token 为空的占位对象 id 2/36/38 + 6 个 `TA` = "Dummy-invisible town sound" 隐形音效点),**17 个是 NPC/怪物基址(见 #6)** | | `stats.objectsArtPending` | **2462**(打包器自己标"全部待接") | | `objectPages` | **[]**(一个对象图集页都没产出) | | 每个对象的 `frame` | **全部为 null** | 每个对象的"位置 + 元数据"其实**都已经进包了**,字段现成可用: ```json { "id": 17, "type": 2, "name": "Dummy", "token": "BR", "mode": "OP", "objectsTxtId": 29, "hp": 0, "member": "data\\global\\objects\\BR\\S1\\brs1litophth.dcc", "alternatives": 8, "x": 3280, "y": 1648, "depth": 41.2, "frame": null } ``` 出现的对象类型(按数量,节选):`etorch2|6E` 236、`torch1|3M` 57、`etorch1|6D` 58、`torch2|3N` 39、`torch1|5R/5S` 各 32、`icecave_torch1/2` 各 1;`fire|FX/FY/FZ/RB` 132;`chest|*` 40+、`basket|XP` 14、`Barrel|B1` 10、`Crate|CT` 2、`Casket|VB` 67、`jar1|6I` 30、`evilurn` 4;`WeaponRack|W1/W2` 11、`ArmorStand|A3/A4` 8;`RogueCorpse`/`corpse`/`dead body`/`skeleton`/`wirt's body`;`Waypoint|*` 12、`Shrine|*`、`groundtomb` 40、`bank|B6` 33、`hellgate|6P` 32。 > 关于"地毯":D2 的对象表里**没有** rug/carpet 行(`reference/opendiablo2/d2core/d2records/` 的对象表与 act 表都查不到),这类美术走的是 DS1 的**地板层**,本引擎地面层已经在画。如果你在具体场景里看到缺地毯,请告诉我是哪个场景,我再回 DS1 地板层里核。 ## 排查:为什么没画出来 1. **打包器只解析、不解码** —— `scripts/pack-act-assets.ts:470-525` 的对象循环里:`resolveDs1Object` → `pickObjectMember` → 只记 `member`/`alternatives` 与计数,`const objectPages = new PageBuilder(palette)` 建出来后**从未 add 任何帧**,于是 `objectPages.encode(0)` 返回空数组,`objectsArtPending: objects.length` 把所有对象都标成待接。这里就是接入点。 2. **页面没有绘制代码** —— `src/scene/act-scene.ts` 里 `objects` 只用于 HUD 计数;`map.html` 沙盒同理。绘制需要:`member` → 解 DCC → 首帧 → 按锚点定位 → 按 `depth` 插进画家序。 3. **但解码与定位的地基已经打好了**,不用从零写: - `src/game/objects.ts`:`objectCofMember`(`DATA\GLOBAL\OBJECTS\<TOKEN>\COF\<TOKEN><MODE>hth.COF`)、`objectSpriteMember`(`<TOKEN>\<COMPONENT>\<token><component>lit<mode>hth.dcc`)、`resolveObjectArt`、`objectAnchor`/`objectDrawAnchor`(锚点 = `DUNGEON_GameToClientSubtileDrawPositionCoords`); - `scripts/verify-dcc.ts` 已经在真归档上解过对象 COF+DCC(`OBJECT_DCCS` / `OBJECT_COFS`,含 `data\global\objects\l2\...`); - `src/game/character.ts` 的 COF+DCC 合成、`src/render/atlas.ts` 的图集、`src/game/animation.ts` 的 `ActorAnimator` 都可直接复用。 4. **深度插入**:包里的 `depth = (x+y)/5` 就是等距深度键,前端按它插进 `runtime.walls` 的画家序即可(`act-scene.ts` 里已有同款 `depthInsertIndex`)。火把/箱子/桶这类要按深度插;平铺在地面的对象(如果有)应放在地面层。 5. **调色板要对齐**:原版的对象/怪物合成走 `data\global\palette\Units\pal.dat`(归档里确实有这个成员,`data\global\palette\Units\pal.dat`),而当前页面把角色画在 act 调色板上(`loadCharacterArt` 收的是 `runtime.palette`)。接对象层时先确认用哪套,否则颜色会偏。 6. **动画**:包里 mode 分布 `NU` 1587 / `OP` 542 / `ON` 327 / `S1` 4 / `S2` 2;火把燃烧、传送点旋转这类需要按 25Hz tick 推帧(`ActorAnimator` 可复用)。`alternatives` 分布 3/6/8/4/… 是同一 style 下的美术变体数,决定了"同一 token 有多套图"。 7. **可破坏与交互(开箱子/开门)** 依赖 `hp`(已进包)与状态机,建议**另开 issue**,本 issue 只到"画出来 + 动画"。 ## 建议的落地顺序 1. **静态首帧**:按 `member` 解 DCC → 取首帧 → `objectDrawAnchor` 定位 → 按 `depth` 插入画家序;同时把美术烘进包(复用 `PageBuilder`,输出 `objects-*.png` + 每对象的 frame 索引/偏移,页面首屏只加载视口需要的页); 2. **mode 与多帧**:火把/传送点/熔炉这类循环动画;`OP/ON` 模式的差异要按 `Objects.txt`(`src/game/objects.ts` 的 `ObjectsRow`)驱动; 3. **变体**:`alternatives` 的选图规则(按 level seed + 位置,与瓦片 `RarityFrameIndex` 的加权随机同一套思路); 4. 之后才是交互/可破坏。 ## 验收建议 1. `verify:packs` 增加"对象绘制条目"的逐项比对(帧、位置、深度次序),与现有"包 vs 现读现解"同口径; 2. `scripts/browser/checks/` 增加一条断言:进入城镇/地下城后对象绘制数 > 0(现有 5 个检查脚本已有同款模式); 3. 人眼验收各一张:Act 1 城镇(火把/火盆/武器架)、Catacombs 4(火把密集)、Crypt(烛台/棺材)、Act 5 Town; 4. 文档/HUD 把"可破坏物品的位置与美术成员已进包,静态帧待接"改成"已绘制",避免文案与实现继续对不上。 (关联:NPC 另见 #6;对象层的朝向依赖 #3 的方向口径。)
Author
Owner

Issue #5 解决报告:对象层美术解码、图集打包与场景深度排序绘制完成

1. 根本原因排查

  1. 打包器仅解析未解码(scripts/pack-act-assets.ts):
    • 原打包器在遍历 DS1 对象时,虽然成功调用 resolveDs1Object / pickObjectMember 匹配出 2379 个美术成员路径,但未对 DCC/DC6 文件调用解码器提取像素数据;
    • objectPages = new PageBuilder(palette) 实例化后未加入任何帧,最终生成空图集 [],导致所有对象的 frame 为 null,并将全部 2462 个对象标记为 objectsArtPending。
  2. 原版数据源笔误(src/game/object-lookup-data.ts):
    • 查表数据中 Act 3 行 104 错录入导致墨菲斯托红门(row 342:1Y:ON)遗失且总记录数跌至 3614 条,修正后恢复 3615 条完整记录,Objects.txt 精确匹配提升至 528 项。
  3. 缺少对象绘制管线与深度插值(src/scene/act-scene.ts):
    • 原场景仅在 HUD 中统计对象总数,缺少针对 scene.objectPages 的纹理加载;
    • 未将对象纳入等轴测深度排序管线:暗黑2 子瓦片深度为 (subX + subY) / 5,与墙体深度 cellX + cellY 线性尺度一致,未按该尺度排序混合绘制;
    • 页面卸载生命周期缺少 renderer.deleteAtlas,存在图集显存泄露隐患。

2. 核心架构与修改实现

  1. 查表与规则修复 (src/game/object-lookup-data.ts):
    • 恢复 Act 3 第 104 项为墨菲斯托红门 (342:1Y:ON);
    • 过滤 COF 非精灵体文件,优先选取 body 部件 (tr),按 .dcc > .dc6 规则挑选第一帧。
  2. 资产解码与图集打包 (scripts/pack-act-assets.ts):
    • 引入全局跨地图解码缓存 decodedMemberCache,62 张地图去重解码 DCC/DC6,显著加快烘焙速度;
    • 货架装箱(Shelf Packing)打包生成每关的 objects-*.png;
    • 计算精确的直角坐标锚点偏移:
      • DCC: offsetX = -16 + dir.box.left, offsetY = 16 + dir.box.top
      • DC6: offsetX = -16 + f.offsetX, offsetY = 16 + (f.offsetY - f.height + 1)
    • 填充每个有效对象的 frame 字段,并将 objectsArtPending 准确清零(0 待接)。
  3. 场景深度渲染管线 (src/scene/act-scene.ts):
    • 定义 PackObjectFrame / ObjectDrawable 类型,扩展 MapRuntime 支持对象层;
    • loadPages 异步载入所有 scene.objectPages 并登记到 WebGL 渲染器;
    • 主渲染循环 runScene 实现单趟双指针深度遍历 drawUpToDepth(maxDepth):将 runtime.walls 与 runtime.objectDrawables 严格按深度交织排序,先画角色后方物体与墙体,再画角色与随从,后画遮挡前景物体与屋顶;
    • beforeunload 生命周期完整释放 runtime.objectPages 纹理显存;
    • HUD 状态更新为实时绘制对象数量。
  4. 全套自动化测试验证:
    • 更新 scripts/verify-packs.ts:增加对全部 62 张地图 objectPages 存在性、尺寸有效性、帧边界以及 pendingArt === 0 的强一致性断言,1302/1302 个断言全部通过;
    • scripts/verify-object-lookup.ts:更新 3615 条记录验证与红门抽检,20/20 项测试通过;
    • scripts/verify-renderer.ts:26/26 渲染器单元测试通过;
    • npm run typecheck & npm run build:0 错误 0 告警顺利编译。

3. 提交与同步

  • 代码已推送到 diablo2-web 主干:72731bb 与 9475886;
  • 62 张地图全量烘焙的资源包(含 objects-*.png 与全新 scene.json)已同步至 diablo2-web-assets 主干:20fd786。
## Issue #5 解决报告:对象层美术解码、图集打包与场景深度排序绘制完成 ### 1. 根本原因排查 1. **打包器仅解析未解码**(`scripts/pack-act-assets.ts`): - 原打包器在遍历 DS1 对象时,虽然成功调用 `resolveDs1Object` / `pickObjectMember` 匹配出 2379 个美术成员路径,但未对 DCC/DC6 文件调用解码器提取像素数据; - `objectPages = new PageBuilder(palette)` 实例化后未加入任何帧,最终生成空图集 `[]`,导致所有对象的 `frame` 为 `null`,并将全部 2462 个对象标记为 `objectsArtPending`。 2. **原版数据源笔误**(`src/game/object-lookup-data.ts`): - 查表数据中 Act 3 行 104 错录入导致墨菲斯托红门(row 342:1Y:ON)遗失且总记录数跌至 3614 条,修正后恢复 3615 条完整记录,Objects.txt 精确匹配提升至 528 项。 3. **缺少对象绘制管线与深度插值**(`src/scene/act-scene.ts`): - 原场景仅在 HUD 中统计对象总数,缺少针对 `scene.objectPages` 的纹理加载; - 未将对象纳入等轴测深度排序管线:暗黑2 子瓦片深度为 `(subX + subY) / 5`,与墙体深度 `cellX + cellY` 线性尺度一致,未按该尺度排序混合绘制; - 页面卸载生命周期缺少 `renderer.deleteAtlas`,存在图集显存泄露隐患。 --- ### 2. 核心架构与修改实现 1. **查表与规则修复** (`src/game/object-lookup-data.ts`): - 恢复 Act 3 第 104 项为墨菲斯托红门 (`342:1Y:ON`); - 过滤 COF 非精灵体文件,优先选取 body 部件 (`tr`),按 `.dcc` > `.dc6` 规则挑选第一帧。 2. **资产解码与图集打包** (`scripts/pack-act-assets.ts`): - 引入全局跨地图解码缓存 `decodedMemberCache`,62 张地图去重解码 DCC/DC6,显著加快烘焙速度; - 货架装箱(Shelf Packing)打包生成每关的 `objects-*.png`; - 计算精确的直角坐标锚点偏移: - DCC: `offsetX = -16 + dir.box.left`, `offsetY = 16 + dir.box.top` - DC6: `offsetX = -16 + f.offsetX`, `offsetY = 16 + (f.offsetY - f.height + 1)` - 填充每个有效对象的 `frame` 字段,并将 `objectsArtPending` 准确清零(0 待接)。 3. **场景深度渲染管线** (`src/scene/act-scene.ts`): - 定义 `PackObjectFrame` / `ObjectDrawable` 类型,扩展 `MapRuntime` 支持对象层; - `loadPages` 异步载入所有 `scene.objectPages` 并登记到 WebGL 渲染器; - 主渲染循环 `runScene` 实现单趟双指针深度遍历 `drawUpToDepth(maxDepth)`:将 `runtime.walls` 与 `runtime.objectDrawables` 严格按深度交织排序,先画角色后方物体与墙体,再画角色与随从,后画遮挡前景物体与屋顶; - `beforeunload` 生命周期完整释放 `runtime.objectPages` 纹理显存; - HUD 状态更新为实时绘制对象数量。 4. **全套自动化测试验证**: - 更新 `scripts/verify-packs.ts`:增加对全部 62 张地图 `objectPages` 存在性、尺寸有效性、帧边界以及 `pendingArt === 0` 的强一致性断言,**1302/1302** 个断言全部通过; - `scripts/verify-object-lookup.ts`:更新 3615 条记录验证与红门抽检,**20/20** 项测试通过; - `scripts/verify-renderer.ts`:**26/26** 渲染器单元测试通过; - `npm run typecheck` & `npm run build`:0 错误 0 告警顺利编译。 --- ### 3. 提交与同步 - 代码已推送到 `diablo2-web` 主干:`72731bb` 与 `9475886`; - 62 张地图全量烘焙的资源包(含 `objects-*.png` 与全新 `scene.json`)已同步至 `diablo2-web-assets` 主干:`20fd786`。
Sign in to join this conversation.
No Label
No Milestone
No project
No Assignees
1 Participants
Notifications
Due Date
The due date is invalid or out of range. Please use the format 'yyyy-mm-dd'.

No due date set.

Dependencies

No dependencies set.

Reference: troytt/diablo2-web#5
No description provided.