Ballance自制图的不断柱子问题
Ballance自制图中断裂的柱子自2011年以来(或更早。由于贴吧无了,已经找不到最早记录了)就已被吧友发现并一直困惑吧友十数年。如今,这一问题终于有了科学合理的解释。算是为在不同时期关注过,并尝试过解决的各位吧友有了一个交代。

历史
由于各路制图人对于柱子断裂问题的持续不断地努力,多数吧友自入吧时期就从未意识到自制地图有这样一段柱子不能透明的时光,再加上当初知晓这一缺陷的吧友大多已离开贴吧,请允许我在这里对柱子问题做一个简短的回顾。
自制地图的柱子问题,又或者:柱子不断,柱子不透明,所谈及的都是同一个问题,即图中的所有柱子,比如常见的灯柱,或者终点处的钢柱,其下端本应平滑渐变至透明,而实际上透明度却戛然而止,呈现柱子唐突消失的现象。这种现象经过吧友不断摸索之后总结出了规律:无论是自制地图还是原版地图,只要是经过Virtools重新保存的文件均会发生此种情况。而没有经过重新保存的原版地图则不会。
自制地图的柱子问题,常与另外几个制图问题,比如路灯发光问题,以及路面阴影问题,可被视为当时Ballance制图界最难以对付的几个问题。其中尤其以柱子问题最为严重,甚至到了吧里当时为了判断竞速使用的地图是否被修改,以柱子是否断裂为标准。以上的这些制图问题中,路灯发光问题是由Ballance脚本硬编码造成的;路面阴影问题也已在2020年7月后有了科学的解释和解决方案。唯独剩下自制地图柱子问题这一片乌云。
2013年2月,勇往直前3地图发布,此地图实现了一种看起来似乎是不断的柱子,并且作为后续一系列地图的不断柱子的范例。然而仔细分析后可以发现,其实际上是将透明度断裂的柱子贴图的透明度突变附近的部分拉伸至整个模型,利用渲染过程中的插值过程产生渐变效果。但是这样会导致贴图的水平纹理丢失,并且整个柱子渐变部分被无限地平滑,看不出任何纹理,所以也不是完美的解决方案。
2018年10月,chirs241097就地图灯影及柱子问题发表了一篇帖子,详细地阐明了路灯发光问题的原因。而对于柱子问题却仍未得出结论。只能找到替代方案,即修改CK2_3D.ini的TextureVideoFormat字段,让Ballance默认使用32位ARGB8888格式去读取贴图。但在结尾chirs241097仍然给出了可能的原因,并且与5年后的结论不谋而合,即文档格式略有区别,导致Desired Video Format选项无法被读取,游戏回退到了默认格式,即会导致柱子断裂的16位ARGB1555格式。
2019年1月,chirs241097发布了有关地图内嵌脚本的相关技术。此技术可以实现运行游戏之外的脚本的功能,而无需加载或修改任何其他文件。此项技术很快被应用于修正地图断裂的柱子这一问题中。通过在内嵌脚本中,为那些透明度错误的贴图,强制指定他们的Video Format,可以使得柱子不再断裂,可以正常渐变到透明。也就是优化了2018年的妥协方案,使得不再需要去修改游戏文件来达成柱子不断的效果。
时至今日,多数自制地图的柱子都已通过内嵌脚本技术不再断裂。很多新入坑的玩家都不再在意柱子的事情。但柱子断裂的问题并不是不存在了,他只是被各种技术压制了,不再表现出来罢了,压制并不等于不存在,它还在那,还是一朵飘在制图人头顶的乌云。
本文的目的,就是彻底击碎这最后一块压在制图人心头的巨石。还柱子问题一个科学,清晰的解释。并给出一些现阶段可行的解决方案。本文的最终目的是取代地图内嵌脚本实现的柱子不断,转而通过原生方式实现。这样以来,许多地图就不再需要内嵌脚本来强制保证柱子不断了,也降低了新手制图人入门的门槛,毕竟不是所有人都会制作内嵌脚本。

科学解释
在Virtools升级换代过程中,一个CKStateChunk(Virtools底层存储格式)中的Identifier(标识符)被修改了,从0x002ff000(Ballance可以读取此Identifier)被修改为0x00fff000(Ballance不可读取此Identifier)。这导致其随后的数据块不可被读取。此数据块存有CKTexture(贴图)的Video Format数据。丢失了Video Format数据的CKTexture被迫回退为CK2_3D.ini中指定的默认格式,而默认格式又不能正确地处理透明度,最终导致了柱子的断裂。

研究与验证
本次研究主要是基于doyaGu所作的有关CK2及CK2_3D的IDA反编译文档展开的。没有doyaGu的这些文档,一些工作开展起来就会非常困难,亦或者无法开展。在此郑重地感谢doyaGu及其所作的工作。
那么在验证前,首先还需要介绍一下我所用的工具LibCmo和Unvirt。由于Virtools SDK屏蔽了很多技术细节以及Virtools底层代码,导致我不能随心所欲地去自由地分析Virtools文件。于是我开发了LibCmo,一个着重复现Virtools 2.1版本文件读写功能的静态库。LibCmo基于doyaGu的反编译结果,基本完整地重新实现了Virtools文件,以及作为Virtools内部底层数据存储单元CKStateChunk的读写操作。而Unvirt则是一款借助LibCmo提供的函数,可以交互式的处理用户需求的命令行程序,例如加载和保存Virtools文件,显示CKStateChunk的核心数据等。Unvirt的名称来源于Luigi Auriemma开发的同名软件,其可以粗略地解析Virtools文件,但其深度不够,不足以解析到我需要的阶段。LibCmo和Unvirt目前仍处于开发阶段,但对于现阶段的研究需求来说足矣。
在前人研究的基础上,我们不难总结出是由于Virtools版本更迭造成的柱子断裂,而且很有可能是因为消失了某些必须的东西造成的。那么既然我们有了这一猜想,接下来就是找所谓的“某些东西”是什么,以及验证它确实有影响。先前各路人马的研究已经几乎排除了大部分的可能,只剩下CKStateChunk这一Virtools严密保护,不对外公开具体内部结构的黑盒未被探索。因此在研究开始,我就将柱子断裂的研究方向集中于CKStateChunk。
首先我们需要一个参照,我这里使用的是原版关卡第二关,其名为Level_02.NMO。将其加载入游戏,传送至关卡结束部分,我们可以看到关卡结束部分的铁柱子和普通灯柱均有平滑的透明渐变。这是我们之后进行对比的地方。

接下来就是利用Virtools Dev 3.5读取原版第二关,然后保存它。这里我采用的是右键Level - Save as这一传统保存方式。保存后的文件命名为Level_02.new.NMO。此时将其加载进游戏,可以发现关卡末尾部分,无论是铁柱子,还是普通灯柱,均没有透明度了。这是符合柱子断裂的描述的。

接下来我使用了Unvirt加载了Level_02.new.NMO,即被Virtools 3.5保存的第二关。具体操作是先使用encoding 1252
设置文档编码,然后使用load shallow Level_02.new.NMO
指令加载文件。接下来通过使用ls obj 页码
的方式浏览该文件内存储的物体数据,找到名为Column_beige_fade,类型为CK_TEXTURE的物体。这个物体是普通灯柱下半透明部分的贴图。这里我找到的编号是320。然后使用chunk obj 320
显示它的底层存储结构CKStateChunk中的数据,如下图所示。该指令为我们列出此物体具有3个Identifiers。

为了有对比性,我使用unload
指令卸载当前加载的文件。然后再加载原生的第二关Level_02.NMO。加载方式如上文所述。然后按照同样的方式找到同一个物体。我这里找到的编号是319。用chunk obj 319
显示其底层数据,如下图所示。可以发现其也具有3个Identifiers,且与上文的输出极其相似。

上文输出的两个数据极其相似,只有一点不同,即其中一个Identifier从0x002ff000变为了0x00fff000。那么到这里我们只能说这个Identifier有很大嫌疑会造成这个bug,但并不能绝对肯定,接下来我们需要继续找证据佐证它。
首先我查阅了Virtools文档中有关此Identifier的定义。此Identifier被定义在CKdefines2.h中。在Virtools 2.5 SDK的这个文件中,如下图所示,可以观察到对于0x002ff000的注解是“Kept for compatibility”,而对于0x00fff000的注解是“Save Only Texture Data”,显而易见,0x00fff000取代了0x002ff000。这可能会导致兼容问题,我对于它会造成此bug更加坚信了。

接下来我打开了doyaGu分析完毕的反编译IDA文档。CKTexture的相关定义都在CK2_3D里面,只需直接进去查找CKTexturex::Load()
这一虚函数的定义。在打开定义后,可以很轻松地找到Identifier常量0x002ff000,如下图所示。在其相关数据解析部分中,可以找到对Video
Format相关的设置,具体的情况是在Identifier后随的数据区块中的第三个DWORD位存放了VX_PIXELFORMAT,该数值指示了该以何种Video
Format加载此CKTexture。

回到我们打开了Level_02.NMO的Unvirt中,Unvirt不仅为我们列出了Identifier,还提供了其对应数据区的内存指针和大小,在这里我们可以找到0x002ff000对应数据区的指针是0x00cf6830,大小是3 DWORD。现在我们需要一款内存编辑器,因为Unvirt暂时还没有直接修改内存的功能。我这里用的是Cheat Engine,你也可以用你自己习惯的软件。首先用CE打开Unvirt,然后转到内存视图,复制Unvirt提供的内存地址,在内存视图中跳转到该地址,如下图所示。然后向后阅读到第三个DWORD,其数值为2。通过查阅有关VX_PIXELFORMAT的定义,如下图所示,可快速得知此值代表的正是32位ARGB8888格式。


卸载当前加载文件,并用Unvirt加载回Level_02.new.NMO,再次用CE查看Identifier 0x00fff000对应的数据区内容,如下图所示。注意到这两个Identifier所引导的数据区块内容完全一致。那么至此可以基本确定就是这个Identifier导致了柱子断裂的bug。接下来就是要验证我们的猜想。方法也很简单,就是针对Level_02.new.NMO,将其0x00fff000改回为0x002ff000。观察是否解决柱子断裂问题即可验证。

仍然是借助CE的修改内存功能,首先复制Unvirt提供的0x00fff000对应数据的地址,然后在CE中向前找2个DWORD,即可找到我们的Identifier本身0x00fff000,如下图所示。用CE将其修改为0x002ff000。然后再用Unvirt打印当前CKTexture的Identifiers。可以发现其Identifier已改变,如下图所示。


接下来使用save Level_02.mod.NMO
将修改后的所有物体数据保存到一个新的Virtools文件,Level_02.mod.NMO。然后进入游戏将其加载,导航到关卡末尾,可以观察到我们的普通灯柱部分已经具有渐变的透明,然铁柱子部分则仍然断裂,如下图所示。这是因为我们只处理了普通柱子的数据,并没有处理铁柱子的,二者用的不是同一个贴CKTexture。这也变相地进行了修改数据与没修改数据的同台对比,证明了我们猜想的正确性。至此柱子断裂问题宣告解决。


解决方案
理论上而言,可以通过编写一些代码,来实现批量地修正贴图问题。然目前我需要一些时间修整,写这些代码耗费了我大量精力,我无暇再去顾及这些了。故此处介绍2个目前暂时可行的解决方案。
基础解决方案
首先你需要在你的Virtools Dev的偏好设置中,将你输出的文件格式设置为不压缩。因为经过Virtools压缩后的数据完全不可读,你将无从操作。
然后保存你的地图文件。接着使用任意十六进制编辑器,例如HxD等打开它,全局搜索0x00fff000,并将它们替换为0x002ff000。然后保存。
这种方案的优点显而易见,那就是非常容易操作。但缺点也是非常明显的,你非常有可能将一些重要数据误替换。因为并不是只有Identifier处会出现0x00fff000,可能文件的别处也会恰巧出现0x00fff000的匹配项且恰好是某个关键数据。倘若误替换会导致最后文件无法打开。
进阶解决方案
进阶解决方案需要使用Unvirt及配套的内存编辑器(此处以CE为例),按照类似上文研究与验证章节相似的手段进行操作。
具体方法则是,先使用encoding 1252
设定文档编码格式,1252是Windows用于西欧字符的代码页,设置此项后,将解决读取文档时可能会抛出的编码错误提示。
然后使用load shallow file.NMO
加载文件。其中file.NMO
是你的地图文件。而shallow代表着浅加载,即只加载到CKStateChunk阶段。与之相对的是deep,即深加载,会加载到CKObject阶段。目前由于LibCmo完全没有实现各个CKObject的加载机制,所以深加载和浅加载效果基本一致。但按目的而言,这种修改只需要浅加载即可。
接着你可以使用ls obj 页面
的命令来浏览当前文件包含的项目。此外如果觉得默认一页显示的项目数过少,可以通过items 个数
指令改变一页显示的项目个数。
当找到目标CKTexture后,记下其序号,序号在列表指令中以#
前缀进行输出。以210举例,此时执行chunk obj 210
显示其底层数据,找到0x00fff000对应的数据地址,在CE内存视图中打开,向前找2个DWORD,找到Identifier本身,然后修改其为0x002ff000。然后再次运行一遍chunk obj 210
,确认修改成功。如法炮制对所有需要修改的CKTexture进行此操作。
操作完毕后,执行save new.NMO
将当前加载的文件保存为一个新文件new.NMO
。至此修改已完成,可以执行unload
和exit
指令退出Unvirt。
这样的修改操作固然麻烦,但不会出错,且可以处理压缩的Virtools文件,不需要去调整Virtools Dev的压缩设置。
有关Unvirt可进行的更多操作,可以使用help
指令查看。还请注意,Unvirt目前仍在开发阶段,可能会存在崩溃或缺失功能的情况。

结语
本次的研究是建立在Ballance的逆向工作极度丰富的情况下的。手握如此巨量的逆向工作成果,让人很难不想试一试搞出一些成果。自2jjy与chirs241097联合反混淆了Ballance的各个核心脚本文件后,针对Ballance的各类逆向工作层出不穷。Gamepiaynmo逆向了一个可用的Virtools 2.1库并在此基础上写出了BallanceModLoader,结束了Ballance没有SDK的一大痛苦历史。在这之后doyaGu完成了对Player.exe本体,以及各类Virtools核心文件的逆向,包括CK2,CK2_3D等,并在此基础上创作出了新的Virtools 2.1 SDK,新Player,新BallanceModLoader,CKRasteriazer等一系列新事物。Ballance的开发从未像现在这样如此简单过。几年前我们还在为修改Ballance而犯难,现在则可以对Ballance做我们想做的任何修改了。
我坚信,所有有关Ballance的难题,最终都会有一个科学的解释,只是过程中我们需要付出很多。试想在十年前,没有这些便利的工具的情况下,想搞明白柱子断裂的原理,是要花费很多很多时间的。正是有一波波热爱Ballance制图与技术的吧友,不断奉献,才能有我们今天有关原理的揭示。
为科学技术献上崇高的敬意与赞美。