nds有完整的矩阵运算体系,这些矩阵不仅将不同的坐标系进行转换,缩放、旋转、平移等,均可以通过矩阵实现,还有专门的矩阵栈用来存储矩阵
矩阵是相当重要的一块,掌握好这一块,可以做到很多事情,本文将在编写的过程中穿插一些hack实例
定点数
nds的3D引擎不采用浮点数,而采用嵌入式系统常见的定点数,这里有必要做个介绍
定点数以固定的位保存小数,固定的位保存整数的方式存储一个小数,3D矩阵的元素用的定点数是
1bit符号 19bit整数 12bit小数
这样的一种格式
*如果你知道定点数如何转换等问题,下面请直接pass
引用
复制内容到剪贴板
代码:
#define fix2float(v) (((float)((s32)(v))) / (float)(1<<12))
这是desmume中用于将定点数转换为浮点数的表达式,我觉得这个挺赞的,稍微讲解一下
我们都知道,10进制中,你把一个数除以10,就相当于小数点向前移动1位,比如53,除以10,就是5.3,除以100就是移动2位
在二进制中也满足这个规律,除以(1<<12),就相当于把小数点向前移动12位
*如果这部分看懂了,下面请直接pass
接下来是手动计算,以win7的计算器为例,因为win7的计算器中,程序员模式是不支持小数运算的,只有标准型跟科学型支持,所以第一步就是将16进制转换成10进制了
在转换的时候,请把左下角的格式设置为“双字”,这样诸如0xFFFFFFFF的数字,可以被正确地转换为-1
接着把值复制到粘贴板上(编辑->复制),计算器的模式转成科学型,把这个数值粘贴进去,除以4096,即可得到转换后的小数了
比如0xFFFFFC00,转成10进制,就是-1024,除以4096,就是-0.25
*如果这部分看不懂的话,请重复阅读直到看懂为止,谢谢
将小数转为定点数存储
首先要把小数部分跟整数部分分开,然后小数部分,连续12次乘以2,在连乘的过程中,整数部分舍弃,如果遇到小数部分刚好为0,则停止运算,后面自动补0
每连乘一次,根据运算结果,将二进制数0/1添加到小数点后面,如果运算结果超过1,添加1,没有超过,添加0
举个例子,0.25
0.25 * 2 = 0.5 -> 小数点后面添加0
0.5 * 2 = 1.0 -> 小数点后面添加1,因为小数点后面为0,停止运算,接着补0
所以小数部分是010000000000b,整数部分是0,所以最后的结果就是0x400
这个还是要掌握的,游戏内的相关API,会要求你用定点数作为参数传递
3D矩阵简介
NDS的3D矩阵采用4x4矩阵,以行优先的方式存储,矩阵的乘法都是右乘,如果记原矩阵为C,要乘以的目标矩阵记为M,则有M*C
引用
复制内容到剪贴板
代码:
_ 4x4 矩阵 _
| m[0] m[1] m[2] m[3] |
| m[4] m[5] m[6] m[7] |
| m[8] m[9] m[10] m[11] |
|_m[12] m[13] m[14] m[15]_|
接着有几种特殊的矩阵
引用
复制内容到剪贴板
代码:
_ 单位矩阵 _
| 1.0 0 0 0 |
| 0 1.0 0 0 |
| 0 0 1.0 0 |
|_ 0 0 0 1.0 _|
_ 4x3 矩阵 _ _ 平移矩阵 _
| m[0] m[1] m[2] 0 | | 1.0 0 0 0 |
| m[3] m[4] m[5] 0 | | 0 1.0 0 0 |
| m[6] m[7] m[8] 0 | | 0 0 1.0 0 |
|_m[9] m[10] m[11] 1.0 _| |_m[0] m[1] m[2] 1.0 _|
_ 3x3 矩阵 _ _ 缩放矩阵 _
| m[0] m[1] m[2] 0 | | m[0] 0 0 0 |
| m[3] m[4] m[5] 0 | | 0 m[1] 0 0 |
| m[6] m[7] m[8] 0 | | 0 0 m[2] 0 |
|_ 0 0 0 1.0 _| |_ 0 0 0 1.0 _|
这几个矩阵,NDS的3D引擎有提供相应的I/O寄存器以简化运算
然后NDS的3D引擎中,存在着4种矩阵,分别是
1.投影矩阵
2.position矩阵(ModelView矩阵)
3.directional矩阵
4.纹理矩阵
其中,投影矩阵将世界坐标系的点变换到camera坐标系的CVV中,position矩阵将模型坐标系的点变换到世界坐标系中,
directional矩阵似乎是用在光照特效中,用来调整光线的方向等,这个系列的教程不进行讨论,
纹理矩阵是把输入进去的坐标转换成完全与纹理相对应的纹理坐标,很多游戏中这个矩阵都是单位矩阵,可以无视,NDS的纹理坐标可以做到±2048的寻址范围,所以一般也用不到这个
一般一个点的坐标要乘以position矩阵,然后再乘以投影矩阵,将坐标变换到CVV,然后CVV再进一步变换就是我们在屏幕上看到的图像了
坐标的平移、旋转、缩放等变换,都可以通过构造特定的矩阵,接着通过矩阵的乘法运算完成
因为矩阵的乘法满足结合律,记点的齐次坐标为C,平移矩阵为T,position矩阵为P,(C * T) * P = C *(T * P)
上文提到,NDS的矩阵乘法是右乘,因此在实际应用中,可以先把坐标变换的矩阵乘以position矩阵,然后坐标再统一乘过去
这几个矩阵可以在nds的I/O map窗口中的LCD-3D'中确认到
矩阵运算的I/O寄存器
引用
复制内容到剪贴板
代码:
4000440h 10h MTX_MODE - 设置矩阵模式(W)
4000454h 15h MTX_IDENTITY - 读取单位矩阵到当前矩阵(W)
4000458h 16h MTX_LOAD_4x4 - 读取4x4矩阵到当前矩阵(W)
400045Ch 17h MTX_LOAD_4x3 - 读取4x3矩阵到当前矩阵(W)
4000460h 18h MTX_MULT_4x4 - 将4x4矩阵乘以当前矩阵(W)
4000464h 19h MTX_MULT_4x3 - 将4x3矩阵乘以当前矩阵(W)
4000468h 1Ah MTX_MULT_3x3 - 将3x3矩阵乘以当前矩阵(W)
400046Ch 1Bh MTX_SCALE - 将缩放矩阵乘以当前矩阵(W)
4000470h 1Ch MTX_TRANS - 将平移矩阵乘以当前矩阵(W)
注意,这些I/O寄存器都是只写的,矩阵运算可以针对上文提到的4个矩阵进行,由MTX_MODE进行设置
MTX_MODE的参数如下
引用
复制内容到剪贴板
代码:
0-1 矩阵模式 (0..3)
0 投影矩阵
1 Position矩阵(Modelview矩阵)
2 似乎是运算同时作用于position矩阵与directional矩阵的模式
3 纹理矩阵
2-31 未使用
关于模式2的话,我自己也做了一些测试,这是白金版中模式2的情况
这是模式1的情况
可以看出,模式1的颜色明显偏暗,由于对directional矩阵没有研究,在这里还不能下任何结论,现阶段知道它对光照特效有影响就行
好了,接下来讲解一下几个矩阵运算的I/O寄存器的用法
比如MTX_LOAD_4x4,读取一个4x4到当前矩阵,例子如下
代码
汇编语言
复制内容到剪贴板
代码:
ldr r0,=0x4000458
ldr r1,=p_MTX_4x4//指向一个有效的4x4矩阵的指针
mov r2,0x0
loop:
ldr r3,[r1],0x4
str r3,[r0]
add r2,r2,0x1
cmp r2,0x10
blt loop
C语言
复制内容到剪贴板
代码:
for(int i = 0;i < 0x10;i++ )
*((int*)0x4000458) = *(p_MTX_4x4++);
其他几个都是一样的,就是矩阵格式变了一下,格式请参考3D矩阵简介那部分
关于MTX_IDENTITY,这个比较特殊,写入任意值即可完成操作,毕竟单位矩阵是固定的
相关的API
复制内容到剪贴板
代码:
void G3_MtxMode(GXMtxMode mode);
void G3_Identity();
void G3_LoadMtx43(const MtxFx43* m);
void G3_LoadMtx44(cosnt G3Mtx44* m);
void G3_MultMtx43(const MtxFx43* m);
void G3_MultMtx44(const G3Mtx44* m);
void G3_MultMtx33(const MtxFx33* m);
void G3_MultTransMtx33(const MtxFx33* m);
void G3_Scale(fx32 x, fx32 y, fx32 z);
void G3_Translate(fx32 x, fx32 y, fx32 z);//这个跟G3_MultTransMtx33的作用是一样,这里的fx32指的是定点数
有人想必注意到了,为什么没有旋转矩阵?nds的3D I/O寄存器不直接提供关于旋转矩阵的接口,旋转矩阵需要通过构造一个3x3矩阵来完成
引用
复制内容到剪贴板
代码:
绕X轴旋转 绕Y轴旋转 绕Z轴旋转
| 1.0 0 0 | | cos 0 sin | | cos sin 0 |
| 0 cos sin | | 0 1.0 0 | | -sin cos 0 |
| 0 -sin cos | | -sin 0 cos | | 0 0 1.0 |
这里的正弦与余弦,必须对应同一个角度的
很幸运的是,SDK里有提供相关的API
复制内容到剪贴板
代码:
void G3_RotX(fx32 s, fx32 c);//绕X轴旋转
void G3_RotY(fx32 s, fx32 c);//绕Y轴旋转
void G3_RotZ(fx32 s, fx32 c);//绕Z轴旋转
参数s是目标角度的正弦,参数c是目标角度的余弦
简析投影矩阵与position矩阵
先来投影矩阵,投影矩阵的主要任务,就是将camera坐标系的视锥体(view volume)中的坐标变换到camera坐标系中的正规化可视空间上(canonical view volume,缩写CVV)
视堆体可以理解为我们用肉眼观察到的空间,CVV是个长宽高均为2的正方体,其中2个点位于(1,1,1)跟(-1,-1,-1),下面的图并没有体现出CVV,请脑补
投影有2种,一种是正交投影,一种是透视投影,2种投影有不同的视堆体
引用
复制内容到剪贴板
代码:
透视投影 正交投影
___ __________
top ___---- | top | |
| view | | view |
Eye ----|--------->| Eye ----|--------->|
|__volume | | volume |
bottom ----___| bottom|__________|
near far near far
透视投影
正交投影
透视投影比较符合人的肉眼所观察到的图像,远小近大,3D游戏基本上用这个,正交投影常见于某些2D游戏,比如pokemon ranger系列,用正交投影的经典例子
视堆体中,靠近camera的那一面叫做近截面(near clip plane),离camera最远的那一面叫做远截面(far clip planes),
camera与近截面的垂直距离记为n,与远截面的垂直距离记为f,近截面面向camera方向的右上角坐标记为(r,t,n),这里的r t分别为right与top的首字母
近截面面向camera方向的左下角坐标记为(l,b,n),l b分别为left与bottom的首字母
对于正交投影,存在如下矩阵将视锥体的坐标转换到CVV中
复制内容到剪贴板
代码:
| (2.0)/(r-l) 0 0 0 |
| 0 (2.0)/(t-b) 0 0 |
| 0 0 (2.0)/(n-f) 0 |
| (l+r)/(l-r) (b+t)/(b-t) (n+f)/(n-f) 1.0 |
对于透视投影,存在如下矩阵将视锥体的坐标转换到CVV中
复制内容到剪贴板
代码:
| (2*n)/(r-l) 0 0 0 |
| 0 (2*n)/(t-b) 0 0 |
| (r+l)/(r-l) (t+b)/(t-b) (n+f)/(n-f) -1.0 |
| 0 0 (2*n*f)/(n-f) 0 |
具体的推导方式请查阅文末的参考资料
此外,对于透视投影,上面那种可以理解为透视投影的一般形式,记做Frustum透视投影(Frustum有平截头体的意思,但连起来的中文我不知道)
此外还有种Perspective透视投影(你说Perspective就是透视的意思,没错啊,很别扭是吧,我也觉得,GBATEK就是这么描述,我也费解)的特殊形式的投影
SDK内是把一般形式的透视投影记为Frustum投影,而把特殊形式的透视投影记为Perspective投影
Perspective透视投影有如下特征,r = -l,t = -b,Z轴正好垂直并穿过近截面跟远截面的中心
如图所示
这是侧面看过去的样子,注意θ,下面的运算要用到它
接着把r = -l t = -b,代入到上面的式子内,得到
复制内容到剪贴板
代码:
| n/r 0 0 0 |
| 0 n/t 0 0 |
| 0 0 (n+f)/(n-f) -1.0 |
| 0 0 (2*n*f)/(n-f) 0 |
先考虑n/t,根据三角函数的定义有,n/t = cotθ = cosθ/sinθ
然后是n/r,r跟t的关系,可以用近截面的宽高比来转换,记高宽比asp = height/width,r = t * asp
代入进去,n/r = n/(t *asp) = (n/t) * (1/asp),n/t的话,上面有了,就是cosθ/sinθ,所以最终的式子就是cosθ/(asp*sinθ)
最终的矩阵
复制内容到剪贴板
代码:
| cos/(asp*sin) 0 0 0 |
| 0 cos/sin 0 0 |
| 0 0 (n+f)/(n-f) -1.0 |
| 0 0 (2*n*f)/(n-f) 0 |
好了,终于可以介绍相关的API了,上面写那么多,都是为了这个
复制内容到剪贴板
代码:
void G3_Frustum(fx32 t,fx32 b,fx32 l,fx32 r,fx32 n,fx32 f,MtxFx44 * mtx);
void G3_Perspective(fx32 fovySin,fx32 fovyCos,fx32 aspect,fx32 n,fx32 f,MtxFx44 * mtx);
void G3_Ortho(fx32 t,fx32 b,fx32 l,fx32 r,fx32 n,fx32 f,MtxFx44 * mtx);
void G3_FrustumW(fx32 t, fx32 b, fx32 l, fx32 r, fx32 n, fx32 f, fx32 scaleW, MtxFx44 *mtx);
void G3_PerspectiveW(fx32 fovySin, fx32 fovyCos, fx32 aspect, fx32 n, fx32 f, fx32 scaleW, MtxFx44 *mtx);
void G3_OrthoW(fx32 t, fx32 b, fx32 l, fx32 r, fx32 n, fx32 f, fx32 scaleW, MtxFx44 *mtx);
其中t b l r n f,这6个参数,没错,跟上面提到的定义完全一样,mtx存储计算完毕的矩阵
fovySin fovyCos aspect这几个就是上文提到的sinθ cosθ 高宽比
带W后缀的API表示按参数scaleW进行缩放,实质上是把投影矩阵乘以一个缩放矩阵
好了,做点实际的hack,巩固一下这部分知识吧,rom是白金版,游戏运行到大地图中,观察它的投影矩阵,可以发现它的投影矩阵属于Perspective投影
观察可知,修改第一列的第一个元素与第二列的第二个元素,可以调整近截面的大小,从而调整整个视野
先在内存里搜索到这个矩阵,地址在0x21c4e94(我这是日版,其他版本可能不一样),把那2个元素改成0x3333
然后,可以发现,视野被调整了
接着是camera的调整,camera的调整通过把一个平移矩阵或者旋转矩阵乘以投影矩阵进行,下面通过调用G3_RotZ将camera绕Z轴旋转90°
示例中的代码是通过hook函数sub_20B1EEC完成的,代码从0x20AEC28跳转过来的
汇编代码,thumb下的
复制内容到剪贴板
代码:
push r4, lr
blx 0x20B1EEC//调用原函数
ldr r4,=0x4000440
mov r1,0x0
str r1,[r4]
ldr r0,=0x1000//sin90°= 1;cos90°= 0
blx 0x20BF808//调用G3_RotZ
mov r1,0x2
str r1,[r4]
pop r4,pc
C代码
复制内容到剪贴板
代码:
void hookSub_20B1EEC(int unk_1,int unk_2,int unk_3)//代码从0x20AEC28跳
{
void (*unk_fun)(int,int,int) = 0x20B1EEC;
unk_fun(unk_1,unk_2,unk_3);
*((int*)0x4000440) = 0;//设置当前矩阵
void (*g3_rotZ)(int,int) = 0x20BF808;
g3_rotZ(0x1000,0);//让它绕Z轴转个90°
*((int*)0x4000440) = 2;//还原
}
结果
有没有想到啥?对,就是反转世界
再来谈谈position矩阵,这个矩阵比上面的投影矩阵简单多了
position矩阵将模型坐标系的点变换到世界坐标系中,这里简单地用一个hack介绍一下,这个hack跟上面那个相比,意义并不是很大,随便看看就好
以空之探险队为例,空之探险队的对话框是用3D引擎渲染的,在3D查看器中可以观察到,每次绘制对话框的多边形之前,都会有一个缩放矩阵乘以position矩阵
因为矩阵的乘法满足结合律,修改这个缩放矩阵的话,就可以调整对话框的比例,跟踪到这个缩放矩阵,然后修改成0x20000,0x20000,0x1000
剩下2个矩阵,directional矩阵不会介绍,纹理矩阵将在纹理贴图篇进行介绍
[
本帖最后由 enler 于 2013-3-15 21:53 编辑 ]