分析对象:Torchlight2.exe(32 位客户端,基址 0x400000)
核心定位:解析掉落装备从生成到落地稳定期间的表现层管线,涵盖逐件飞出动画、稀有度粒子布局选择与三阶段音效调度。

0. 结论速览 ​

原版装备掉落并非简单的物理刚体抛射。引擎首先依据逐件延迟将模型置为隐藏,延迟耗尽后由 sub_58EB70 构造名为 DROP 的三控制点 CPath;随后 sub_57D2A0 逐帧将路径进度递增 dt × 20,经 Catmull–Rom 曲线插值计算世界位置,到达路径末端时触发落地音效并结束飞行动画。

品质光晕也并非单一光柱仅靠着色器换色,sub_5808F0 在四套独立的粒子布局资源间做离散选择:

选择分支布局资源核心呈现元素
魔法 / 附魔装备MAGIC-ITEM.LAYOUT蓝色直立 Light、蓝色碎片、火花、gleam
普通白色装备GENERIC-ITEM.LAYOUTgleam 与垂直上升火花(无直立光柱)
任务专属物品QUEST-ITEM.LAYOUT黄色光晕、尖刺、爆闪、火花与具名 Beam 节点
暗金 / 唯一装备UNIQUE-ITEM.LAYOUT橙色碎片、橙色 LightFloor、火花、gleam

数据定义侧亦支持以 NODROPPARTICLES 彻底关停掉落粒子,或通过 DROPPARTICLE 指定自定义 LAYOUT 覆盖默认规则。

音效管线分为三个正交阶段:FALL_SOUND、LAND_SOUND、TAKE_SOUND 分别写入声音库槽位 25、26、27,在起飞开始、飞行结束落地、以及玩家拾取时各触发一次。

1. CEquipment 表现层虚表偏移校正 ​

CEquipment 主虚表起始于 0x2174864。直接解析虚表槽位指针可得:

虚表偏移指针位置目标函数实际职责
+4600x2174A30sub_5A4C00模型/部件显隐状态切换
+4680x2174A38sub_5A51D0依据地面法线调整模型静态朝向姿态
+5320x2174A78sub_58EB70完整装备掉落表现初始化入口

对应虚表字节序列自 0x2174A30 起为:

text
00 4C 5A 00  30 D4 57 00  D0 51 5A 00  ...  70 EB 58 00

此前的推测将 +460 标注为落地弹跳、+532 仅负责延迟显形,经底层指令核对均不准确:+460 本质是部件显隐切换,+532 则是集成了路径曲线、姿态矩阵、粒子挂接与起飞音效的完整掉落表现初始化函数。

2. 延迟与飞出状态 ​

与表现直接相关的 CEquipment 字段如下:

偏移类型用途指令证据
+0x4ACfloat逐件掉落延迟0x583017: fcom [esi+4ACh];0x583032: fst [esi+4ACh]
+0x288CPath*DROP 路径0x57D2B6: mov ecx,[ebx+288h]
+0x28Cfloat路径进度0x57D2E4: fadd [ebx+28Ch]
+0x290byte飞行/延迟状态0x58300C: cmp byte ptr [esi+290h],0
+0x360Matrix4旋转所用起始姿态0x57D3F3: lea ecx,[ebx+360h]
+0x43C粒子实例指针掉落品质粒子0x580A9E: mov ecx,[esi+43Ch]
+0x1F0声音库25/26/27 三声音槽0x592B29: mov ecx,[esi+1F0h]

sub_582FF0 是逐帧外层更新。其关键控制流是:

cpp
if (equipment->drop_active) {                    // +0x290
    if (0.0f <= equipment->drop_delay) {         // +0x4AC
        equipment->drop_delay -= dt;
        if (equipment->drop_delay < 0.0f)
            equipment->vftable_plus_532();       // sub_58EB70
        else
            equipment->set_parts_visible(0, 1);  // vtable+460
    }
    equipment_update_drop_flight(equipment, dt); // sub_57D2A0
}

调用 +532 的原始指令为:

text
0x583041  8B 82 14 02 00 00    mov eax,[edx+214h]
0x583047  FF D0                call eax

0x214 即十进制 532。延迟尚未结束时则从 [edx+0x1CC] 取 +460,传入 (0,1)。

3. sub_58EB70:装备掉落初始化 ​

3.1 延迟分支 ​

若 +0x4AC > 0,函数只调用 vtable+460(0,1) 隐藏模型,将 +0x290 置为 1, 然后返回。真正的飞出初始化由上一节的逐帧函数在延迟变为负数时再次调用。

延迟由 C-0178 已确认的发放循环按“第几件装备”递增;装备和金币各自使用独立计数器。 因此同一点生成的多件装备虽然逻辑落点重合,视觉上仍会依次飞出。

3.2 初始化步骤 ​

延迟到期后的顺序是:

  1. 调用 sub_58DEB0 整理渲染状态。
  2. 在 [0,360] 内随机初始 yaw,并乘 0.017453292 转为弧度。
  3. 通过 vtable+460(1,1) 显示模型,调用 sub_5808F0 附着品质粒子。
  4. 创建或重置名为 DROP 的 CPath,清 +0x28C,置 +0x290=1。
  5. 向路径加入起点、中点和落点三个控制点;中间弧度带 3.5..4.5 的随机抬升/偏转, 每点以 -60/+60 为角度边界传给 sub_675690。
  6. 调 sub_5F5240 向地面扫描并求法线,再经 vtable+468/sub_5A51D0 贴合斜坡。
  7. 调 sub_577610 对武器类别作额外展示姿态校正,保存起始矩阵到 +0x360。
  8. 播放声音槽 25,即 FALL_SOUND。

默认分支的落点 Y 为地表再抬 1.51;另一分支抬 1.01。若是武器再加 0.05, unit type 188 还会再加 0.25。这些是展示高度,不应回写成 C-0178 的逻辑散落坐标。

3.3 曲线类型 ​

sub_674C40(path, progress) 的展开公式含标准的 0.5 系数和相邻四点组合,是 Catmull–Rom spline 取样。这里没有重力积分、速度碰撞或刚体反弹;看起来像抛物线, 实质是预先构造的样条路径。

4. sub_57D2A0:每帧推进与落地 ​

函数先要求:

text
+0x290 != 0
+0x288 != nullptr
+0x4AC <= 0

之后执行:

cpp
progress += dt * 20.0f;
if (progress >= path->total_length) {
    progress = path->total_length;
    virtual_plus_48(1);
    drop_active = false;
    play_sound_slot(26);              // LAND_SOUND
}
position = path->sample(progress);     // sub_674C40

20.0f 取自 0x2142624。关键指令:

text
0x57D2D7  fld  [esp+arg_0]
0x57D2DE  fmul ds:flt_2142624       ; 20.0
0x57D2E4  fadd [ebx+28Ch]
0x57D2EA  fst  [ebx+28Ch]
0x57D2F0  fcomp [ecx+24h]           ; 路径总长
0x57D32D  push 1Ah                  ; 槽 26
0x57D32F  call sub_664F10
0x57D344  mov  ecx,[ebx+288h]
0x57D34D  call sub_674C40

unit type 8 / 21 / 33 / 42 还会按 min(progress / total_length, 1) × 2π 在整段飞行中旋转一周。本文不强行给这四个数补 未证实的正式枚举名;控制流本身已钉死。

5. sub_5808F0:品质粒子选择 ​

5.1 数据覆盖优先级 ​

函数首先读取装备持有的原始 DAT group(CEquipment+0x1B0):

  1. NODROPPARTICLES=true:跳过全部掉落粒子。
  2. DROPPARTICLE 非空:装入指定 LAYOUT,覆盖默认品质选择。
  3. 否则进入默认四分支。

发行 MEDIA/UNITS 普查结果:NODROPPARTICLES 只有 1 个 DAT;DROPPARTICLE 出现于 31 个 DAT,共 5 个不同路径,其中 27 个使用任务物品的 greenlights 布局。

5.2 默认四分支 ​

cpp
if (sub_512D80(item))
    index = 23;                         // QUEST-ITEM
else if (hasUnitType(0x36) || hasUnitType(0xF1))
    index = 24;                         // UNIQUE-ITEM
else
    index = 22 - virtual_plus_448();    // true -> 21 MAGIC, false -> 22 GENERIC

直接证据:

text
0x580A54  call sub_512D80
0x580A5D  mov edi,17h                  ; 23
0x580A64  push 36h
0x580A71  push 0F1h
0x580A83  mov eax,[edx+1C0h]           ; vtable+448
0x580A94  add edi,16h                  ; 得 21 或 22
0x580A99  mov edi,18h                  ; 24

主虚表的 +448 指向 sub_57EB60。它不仅检查某个“稀有度整数”,还会看 unit type、 affix/effect 向量并回退到 sub_577DB0;因此最稳妥的移植语义是“需要魔法掉落表现的 装备谓词”,不要简化成 rarity >= rare。

四个索引对应的路径由静态初始化函数 sub_107BE50 直接写死:

text
0x107C01C  MAGIC-ITEM.LAYOUT     -> 静态表项 21
0x107C031  GENERIC-ITEM.LAYOUT   -> 静态表项 22
0x107C046  QUEST-ITEM.LAYOUT     -> 静态表项 23
0x107C05B  UNIQUE-ITEM.LAYOUT    -> 静态表项 24

5.3 “光柱”的真实构成 ​

  • GENERIC-ITEM:XING6.DDS 的 Gleam 和 Rising sparkles。没有 Beam,也没有直立 Light。
  • MAGIC-ITEM:GLOWBLUE.DDS 的 Billboard Up、IS LIGHT=true 的 Light,另有蓝色 碎片、BLUEPARTICLE 上升火花与 gleam。
  • QUEST-ITEM:GLOWYELLOW、BLUESTREAK、STAR3、MULTI-SPARKLE CLEAR、 FLASH09,再加一个明确名为 Beam 的 Particle。Beam 使用 RAY33.DDS, RENDER TYPE=Billboard Forward Camera、ORIGIN=Bottom Center、HEIGHT=10。
  • UNIQUE-ITEM:STREAK、FRAGMENT_4X4ORANGE、FAST 火花、XING6,以及 GLOWORANGE.DDS 的 Billboard Up LightFloor。这是橙色地面亮光,不是同一个 Beam。

6. 三阶段声音 ​

装备 DAT 加载函数 CEquipment__loadFromDat/sub_5923E0 读取三个键:

DAT 键声音槽装载指令播放时机
FALL_SOUND250x592B31: push 19hsub_58EB70 飞出开始
LAND_SOUND260x592BCD: push 1Ahsub_57D2A0 路径结束
TAKE_SOUND270x592C69: push 1Bhsub_577830 拾取

三处随后均调用 sub_665FB0 将解析出的声音装进 CEquipment+0x1F0 的 sound bank。 实际播放通过 sub_664F10 按槽位、位置与声音配置分派。

有一个容易复刻错的时序:sub_53B110 在把装备加入背包之前就调用 sub_577830(item,0)。若背包已满,物品随后会被放回拾取者脚下,但拾取音已经播放。 所以声音对应“拾取动作被接受并从地面摘除”,不对应“背包最终成功容纳”。

发行 MEDIA/UNITS 有 624 个 DAT 显式写 TAKE_SOUND,共 16 种值;最高频是 ClothGet=279、GemDrop=117、LEATHERGET=69、Pickup=64。ITEMS.DAT 中通用 Pickup 与许多 *Get 最终使用 media/sounds/ui/pickup.wav。

7. 金币表现 ​

金币类使用同样的三声音阶段,但有自己的实现:

  • sub_5A7D90:播放槽 25,创建 DROP 路径,附着 MEDIA/PARTICLES/DROPS/GOLD_TRAIL.LAYOUT。
  • sub_5A7900:到达落点时播放槽 26。
  • sub_5A78B0:拾取时播放槽 27。

GOLD_TRAIL.LAYOUT 用 MULTIPARTICLE/COINS.DDS,渲染方式 Alpha,粒子寿命 0.15,发射率 3,所以金币飞行时会留下短促硬币拖尾。玩家 BASE.DAT 的 GOLD_SOUND 为 GOLDPICKUP;MEDIA/SOUNDS/ITEMS.DAT 中 GoldPickup 指向 media/sounds/item/drop/dropgold.wav,音量 0.15、频率变化 5000、半径 10。

8. 重制实现与表现层建议 ​

在重制或独立运行时实现中,建议将逻辑结算落点与表现层动画路径严格解耦:

  1. 逻辑散点依据确定性掉落公式计算世界坐标,装备实体生成时不应施加随机力;
  2. 装备与金币各自维护独立计数器累加生成延迟,延迟生效期间隐藏渲染模型;
  3. 延迟结束时刻构建三控制点 Catmull–Rom 路径,并以标定速率(20 units/s)沿曲线步进;
  4. 落地判定点依据地面碰撞网格法线校正模型的静止旋转姿态,但不改变底层所属权及交互包围盒;
  5. 优先响应数据中的 NODROPPARTICLES 与 DROPPARTICLE 显式配置;无自定义覆盖时再执行基于稀有度的四套默认布局分派;
  6. 粒子效果应完整还原各 LAYOUT 预设内的多发射器与贴图图元,避免简化为单一颜色光柱;
  7. 严格在 launch / land / take 三个生命周期事件点触发音效,保持与原版一致的事件时序。

9. 自动化验证覆盖与后续边界 ​

配套自动化验证用例覆盖以下关键项:

  • 客户端发行版特征字节与虚表指针;
  • 逐件延迟与路径推进相关的字段偏移及浮点运算指令;
  • 四类默认粒子布局与覆盖字段解析;
  • 声音槽位映射与资源文件路径。

以下边界逻辑保留后续深入追踪:

  • unit+0x8A3 决定的 0.25 秒 stagger 触发来源;
  • NetPickupItemMsg(49) 与 NetPickupNewItemMsg(50) 网络消息包结构;
  • 掉落物在场景中的生命周期与超时清理机制;
  • 飞行旋转判定中涉及的 unit type 8/21/33/42 具体枚举名称。

上述未决项不影响本文关于飞出曲线、粒子布局与音效阶段的核心结论。