画完灰姑娘,我们终于要开始聊一聊 2001 版的调色板了,年末工作比较多,而且我不得不说, 2001 的调色板要比原版更加复杂。好在我还没有会一直敲碗催更的读者,我还可以慢慢研究。
本系列已完结,以下是各章节说明,17 之前是 dos 版相关,之后是 2001 版:
之前我们从 ShowEvent
那里取出了事件图的像素,由于没有识别出调色板,
所以我们只能随便定义一个调色板,看看图形是不是正确。现在图形的问题已经解决了,
接下来我们要确认事件图的调色板在哪里。
另外我们前面也提到,程序通过调色板也做出了一些动画效果,比如切换事件图时的全白渐变,
全白时,无论什么颜色索引,取出的颜色都是 0x7fff
:
看懂这个日志可能需要解释一下,这一步 EBX
保存了颜色索引,取出的颜色放在 AX
中,而 ollydbg 一般的做法是当寄存器的值有变化时,会注释一个新的变化值,上面这段日志,只有第一行有注释,表示取出了 0x7fff
,其他都没有注释,表示 EAX
的值没有发生变化。
从上面开始出发,我们知道 EDX
的内容,应该就是调色板的位置,然后每两个 byte 代表一个颜色,所以这应该是一个 16 位调色板。
上面我抓到的调色板明显是一个全白色的调色板了,这个对我们来说没什么意义,我们要抓到原始颜色的调色板,经过日志、调试和 ghidra 的三管齐下,我们觉得,调色板的位置应该是在这里生出来的:
结合 ghidra 的函数定义:
void __cdecl FUN_004119e0(uint *param_1,uint *param_2,ushort param_3,uint param_4)
第一个参数是输出的调色板,是一个全局变量 0058DC50
,第二个参数是「事件一」固定的「调色板数据」:
const DATA: [u32; 512] = [
0x7AB50000, 0x5A316A73, 0x39AC49EF, 0x1CC62D4A, 0x7B927FD6, 0x770A7B4E, 0x524C66AC,
0x2D6A41CB, 0x77BD7FFF, 0x67396F7B, 0x52945EF7, 0x42104A52, 0x318C39CE, 0x2108294A,
...
];
为什么说是「调色板数据」,这段数据是固定的,但是又不能直接当作调色板来使用, 同时这段数据也不能从文件中直接搜到,有一个解密的过程,详细的情况,我们可能要放到下一篇来说明,不过我们可以认为这段是来自文件,因为读取文件的日志中,我们可以找到这些数据:
第三个参数可以认为是常数 0x7fff
,第四个参数则是可以代表数量的 0x200
。
而 FUN_004119e0
做的事情,主要就是使用固定调色板数据和常数 0x7fff
,结合另一个逐步变化的全局变量 DAT_005aec70
去计算实际的调色板,这个变量指向不同的数组,所以在其他输入保持不变的情况下,画出的图像会逐步变白,或是逐步从白色复原。
所以如果我们要抓到画图用的调色板,我们只要时刻注意 FUN_004119e0
执行后 0058DC50
中数据的变化情况,就可以提取出事件图的真实调色板:
let pals = [
0x0C63, 0x7ED6, 0x6EB5, 0x5E73, 0x5231, 0x41EE, 0x358C, 0x2929, 0x7FF7, 0x7FB4, 0x7F70,
0x7B2C, 0x6ACE, 0x5A8E, 0x4A0D, 0x35AC, 0x7FFF, 0x7BDE, 0x739C, 0x6B5A, 0x6318, 0x5AD6,
...
];
我们可以看出一些端倪,最大的值是 0x7FFF
,也就是说 RGB 各用了 5 个 bit,有一位没有使用,那么 RGB 三色是如何排列的呢,不想动脑子的话可以像我一样,反正只有六种组合,画出来用眼睛判断即可:
for i in 0..6 {
paf_event_1_by_pal(&pals, i);
}
...
match index {
0 => {
*pixel = Rgb([r as u8, g as u8, b as u8]);
}
1 => {
*pixel = Rgb([r as u8, b as u8, g as u8]);
}
...
}
不需要卖关子,其实就是按 RGB 的顺序排列的,结合灰姑娘的数据,我们又可以画出宝钗了:
所以调色板我们找到了,但是它是从哪里来的呢,我们知道有三个输入条件决定调色板的实际颜色,图像的「调色板数据」,常量,以及一个全局变量指向的索引字典,这个索引字典我是从内存中直接 dump 出来的:
那么这段内容从哪里来,我目前有一些推测,还没有验证,我认为是一段程序生成的,不过今天内容已经不少了, 我们留到下次验证这个猜测,顺带证实一下对第二幅图是否有效吧。最后我们说说调色板是怎么生出来的:
let c1 = ((crt & 0x7c007c00) >> 8) | ((magic & 0x7c007c00) >> 3);
let c2 = ((crt & 0x03e003e0) >> 3) | ((magic & 0x03e003e0) << 2);
let c3 = ((crt & 0x001f001f) << 2) | ((magic & 0x001f001f) << 7);
let (parsed_c1_low, parsed_c1_high) = get_dict_parsed(&dict_ram, c1);
let (parsed_c2_low, parsed_c2_high) = get_dict_parsed(&dict_ram, c2);
let (parsed_c3_low, parsed_c3_high) = get_dict_parsed(&dict_ram, c3);
let mut parsed_c = (parsed_c1_high << 0x10) | parsed_c1_low;
parsed_c = parsed_c << 5;
parsed_c |= (parsed_c2_high << 0x10) | parsed_c2_low;
parsed_c = parsed_c << 5;
parsed_c |= (parsed_c3_high << 0x10) | parsed_c3_low;
parsed[i] = parsed_c;
fn get_dict_parsed(data: &Vec<u8>, c: u32) -> (u32, u32) {
let dict_start = 0x56c50;
let c1_offset1 = dict_start + (c & 0x0000ffff) as usize;
let c1_offset2 = dict_start + (c >> 0x10) as usize;
//println!("offset1: {:#10x}, offset2: {:#10x}", c1_offset1, c1_offset2);
let parsed_c1 = u32_from_vec_u8(&data, c1_offset1);
let parsed_c2 = u32_from_vec_u8(&data, c1_offset2);
//println!("parsed1: {:#10x}, parsed2: {:#10x}", parsed_c1, parsed_c2);
(parsed_c1, parsed_c2)
}
简单解释一下逻辑,程序先按位取出每种颜色,然后与常量对应位或,这一步像是在补齐高位的 1
,
然后查表,取出的值再合并成 4 个 byte。这个做法每次取出 4 个 byte,生成 4 个 byte,
由于每个颜色两个 byte,所以循环只要 128 次。
虽说画出了第一张图,但是其实我还是一头雾水,实在是想不出来为什么调色板要这样子设计,分析 DOS 版的时候,我们基本没在调色板上画什么时间,直接用猜的就解决了,而这次绕了很多弯路,一般说来,游戏程序的设计都是直来直去的,不会故意绕什么弯子,因为会有性能损失,所以这里面应该是有我们还不知道的缘由吧。
话说回来,由于 dos 版的调色板我印象中每个颜色是 6 位(0-63),而 2001 版的颜色只有 5 位 (0-31),两个版本用的颜色数也是差不多的,所以我觉得图像的粒度会比原版更大一些,当然这是我一个美术外行人的看法,就不再详细展开了。