存档

2010年4月 的存档

从零到点 – Chapter 0

2010年4月29日 Huang Donghai 没有评论

有“高手”说,考试新人,考矩阵求逆就够了。这么“高深”的东西,我怎么能懂呢。我是只知道点积和叉积的。这个话恐怕也说满了,积和积,我也不能说我就知道了。我只知道了一点点。

关于矩阵求逆,我们知道求逆的定义,就是求 M-1= I / M。对于4\times 4的矩阵,不过是 16 个线性联立方程,有16个未知数,这个只是体力活,就算你未尝先把公式背下来,硬算也是初中生可解的问题,并且这个只是代数问题,跟几何没有关系。而点积和叉积则不同,点积和叉积是 3D 解析几何,3D 图形学的数学基础,如何让扁平的代数立体起来,靠的就是这个。大多数引擎程序员会自认为自己很了解点积和叉积。但是实际上据我观察,却不是这样。99%的人没搞懂这个。

我们的教科书,倾向于直接把结果含糊不清的告诉学生,而没有把其中的过程,思辨教给学生,而这个反而是精华。我说他们含糊不清,已经是个很轻的说法。实际上,我发现我们的教科书,采用循环定义,想当然的推理的地方很多。我尝试用一种不同的途径来讲述这个。你看了之后,可能会更明白了,哦,原来是这样。但是很有可能 (或者说是更有可能) 是更迷糊了。很多你认为理所当然的东西一下子朦胧了起来。

自然数 Natural number

皮亚诺像我们从最简单的开始。老王是卖白菜的,他问,一斤白菜 2 块钱,请问 3 斤白菜多少钱?虽然你家里不是卖白菜的,我想你也能得出答案,6 块钱。我如果问为什么呢?因为 2*3 = 6。如果我问为什么 2*3 = 6 呢?这个就有点难度了。九九乘法表里就是这样的,我们从小就背下来了。其实很简单,乘法是加法的简化写法,2*3 等于 3 个 2 相加,所以 2*3 = 2+2+2=6。那为什么 2+2+2=6 呢?这就很难答上来了。如果你孩子问你,你会跟他搬指头。搬指头是没错的,但是如果是 20,200,2000, 你恐怕就没这么多指头可以搬(蜈蚣除外)。

要解答这个问题,其实是没有可能的。我们要搬出公理,公理,就是公认的道理,不证自明的道理,其实就是没有办法证明的道理。我们前面说的,其实是自然数,或者说,我们叫它自然数,因为它很“自然而然”。关于自然数,我们有皮亚诺公理来描述这个问题。

皮亚诺的这五条公理用非形式化的方法叙述如下:

  1. 1 是自然数;
  2. 每一个确定的自然数a,都有一个确定的后继数 a’a’  也是自然数(一个数的后继数就是紧接在这个数后面的数,例如,1的后继数是2,2的后继数是3,等等);
  3. 如果自然数 bc 的后继数都是自然数 a,那么 b = c
  4. 1 不是任何自然数的后继数;
  5. 任意关于自然数的命题,如果证明了它对自然数1是对的,又假定它对自然数 n 为真时,可以证明它对 n’  也真,那么,命题对所有自然数都真。(这条公理保证了数学归纳法的正确性)

皮亚诺公理里的第 2 项所谓的后继,其实定义了加法,后继,就是搬一个指头,就是 +1 的得到的数。后继的后继,就是 + 1 + 1。我们知道 1 的后继那个数,我们写作 2,意思就是 1 + 1 = 2。所以加法就是多次搬指头的简写

a+b 定义为 a 后面第 b 个后继,也就是 a 之后再搬 b 次指头。

我们先证明证明加法结合律:

(a+b)+c=a+(b+c)

首先证明 c=1 时上式成立, (a+b)+1 是 (a+b) 的后继,而 a+b 是 a 后的第 b 个后继,那么 a+(b+1) 也就是 (a+b) 的后继。所以

公式1: (a+b)+1 = a+(b+1)

成立。

假设 c=m 时成立,我们证明 c=m+1 时也成立。

如果

公式2:(a+b)+m=a+(b+m)

成立,

那么 根据公式1有

(a+b)+(m+1) = (a+b+m) + 1

根据公式2有

(a+b+m) + 1 = ( a+(b+m))+1

再根据公式1(用了两次公式1)

( a+(b+m))+1 = a+( (b+m)+1) = a+(b+(m+1))

证毕。

加法交换律也可以用归纳法类似的证明。

那么 2+2 等于多少呢?2+2 = (1+1) + (1+1) = (1+1+1) + 1 = 3+1 = 4

2+2+2 等于多少呢?按上面的方法,不难证明 2+2+2=6

但是2+2+2写起来太难看了,如果你买100斤白菜,那岂不是要写老长?我们定义一个新的简写方法,a多次累加b次,我们写成a\times b乘法就是多次加法的简写。就得到 2\times 3=6

乘法也可以证明符合结合律和交换律。证明就不写了。自己可以去试试。

另外,对于一个自然数,乘以1等于多少呢?这个很容易理解,还是等于自身。一斤白菜2块钱,你买一斤,当然还是2块钱。所以我们有:

公式3: 1 \times n = n

整数 Integer

卖白菜是件很高深的工作,跟掏粪不相上下,也需要大学生才能搞定。老王大学毕业后,卖白菜卖的很有心得。有个问题,今天进了100斤白菜的货,卖出了85斤,问,还剩多少斤呢?

所以我们要定义一个搬指头的逆运算,反向扳指头,先拿出100根指头,然后拿掉85个指头,这个运算我们叫减法,减法是加法的逆运算

100-85=15

如果卖了100斤呢?你发现啥都没剩下。这时候我们定义一个新的数 0,表示没有,空。0算不算自然数,有的领域倾向于算是,比如集合论和计算机科学里。有的领域一般不算,比如数论。数论里面算0,很多东西就不好搞了。比如0算不算偶数?算不算质数?不好搞。我们不讨论这个。卖白菜虽然高深,但是也没到要去证明哥德巴赫猜想的地步。

所以,卖完100斤后,老王今天收入了200块钱。老王的白菜生意做得很好,又很有信誉,所以,白菜供应商可以让老王先拿货,后付款。第二天老王又拿了100斤白菜,但是挂了帐,拿货价是1元每斤(白菜的利润没这么高,否则就不是叫白菜,但是我们还没学过实数,所以暂时这么定)。结果这天生意不好,大家都喜欢吃点好的(豆腐就不错),只卖掉了30斤,老王今天收入多少钱呢?

30\times2=60

要还供应商100块,这个帐一算,老王亏了40块。如何计帐这个亏了的40块钱(统计局不叫这个叫亏损,叫赤字,老王收入下降也不是下降,叫负增长),我们扩展自然数支持这个记法,叫负数。-1就是0前继,-40就是0往前数40。

我们不难得到,0-40 = -40,0-n=-n;

负整数乘以正整数是正整数还是负整数呢。当然是负整数。我欠你10块钱,是-10。如果是欠10倍的10块钱,当然是 –100。如果是正数就麻烦了,你要倒找我钱。即, 假设a, b是正整数:

(-a)\times b=-(a\times b)

负整数乘以负整数是负整数还是正整数呢?

假设a, b是正整数,那么

\begin{array}{rl} (-a)\times(-b) & =-1\times a\times(-b)\\ & =-(a\times(-b))\\ & =0-(a\times(-b))\\ & =0-(-(a\times b))\\ & =a\times b\end{array}

显然结果为正整数。

小结

我们初步从一无所有开始,得到了整数的概念和一些基本的运算。后面我们要进入实数和几何,然后接近点积和叉积。

分类: 基础理论 标签: ,

Ubuntu 9.10下Firefox字体发虚的问题

2010年4月27日 Huang Donghai 1 条评论

Ubuntu 9.10 下如果你出现了 Firefox 字体始终发虚(启动了ClearType),并且在“外观”里修改而 Firefox 始终发虚(其他程序正常),如这个案例:

    Ubuntu中文论坛 • 查看主题 – ubuntu 9.10下firefox 3.5 的字体平滑方式如何解决?

那么你可以这样做:

sudo rm /etc/fonts/conf.d/10*
sudo dpkg-reconfigure fontconfig

重启 Firefox 就好了。

分类: Tips & Tricks 标签: , ,

ZT:建立只能使用ssh“转发”功能的系统账户

2010年4月24日 Huang Donghai 没有评论

为了满足“翻Wall”的需要,在国外的 Linux主机上或 VPS 上建个可 ssh登录的用户,使用 ssh 的 Tunnel 来作代理是十分常见的方法。

但是主人往往又想最小化用户权限,以避免对系统造成影响。最简单的办法就是,禁止用户登录。

其实 ssh 可以连接到 sshd 但是不执行远程命令(默认是启动用户设定的 shell ),使用 -N 参数即可。

在服务器上建一个 username :

$ useradd -s /bin/false username

将用户的 shell 设置成 /bin/false。这样用户就无法与系统进行交互。设置密码:

$ passwd username

补充一下:对已有帐号禁止其shell交互使用:

$ usermod -s /bin/false username

小技巧:

也可以使用 /usr/bin/passwd 作为用户的 shell ,这样用户就可以通过登录而来自主修改密码。需要注意的是,需要将 /usr/bin/passwd 这一行写进 /etc/shells 文件。

sshd 认证通后之后,会检查设定的 shell 是否登记在 /etc/shells 文件中,若已经登记,则 fork自己,然后 fork 出来的子进程再 exec 设定的 shell 。而 ssh 的 -N 参数,则是告诉 sshd 不需要执行 shell。

建立Tunnel:

$ ssh -D 1080 -qfnN username@hostname

输入密码即可使用(也可以用key认证)。

Windows的话,可以使用 plink.exe 或者 MyEnTunnel(MyEnTunnel 本质上也是使用 plink.exe 来建立Tunnel)。

此时账号 username 可以通过 sshd 的认证使用 TcpForwarding ,但是不能运行 shell,不能与系统交互。刚好可以用来为朋友提供国外的代理翻Wall。

参数详解:
-D 1080 建立动态Tunnel,监听在本地1080端口。
-q 安静模式。
-f ssh在后台运行,即认证之后,ssh退居后台。
-n 将 stdio 重定向到 /dev/null,与-f配合使用。
-N 不运行远程程序。即通知 sshd 不运行设定的 shell。

source: http://www.bsdmap.com/2010/02/22/create-tunnel-user/

分类: 系统网络 标签: ,

用 fail2ban 来保护 VPS

2010年4月23日 Huang Donghai 没有评论

总是有无聊的人来扫描 ssh 的密码,看看 /var/log/auth.log 就可以看到。为了安全, 可以用 fail2ban 来保护, 这个软件会自动屏蔽一段时间错误尝试的 ip。直接从源里安装:

$ apt-get install fail2ban

就行了。

我用的时候碰到了奇怪的问题,发现这个软件不能发挥功用,经过检查,原来是我因为我设定了新的服务器时区。设定服务器时区,在 Ubuntu上 可以用命令:

$ dpkg-reconfigure tzdata

但是系统没有重启,所以 log 系统还是用老时区时间,但是 fail2ban 用的是新时区的时间,而 fail2ban 是靠查 /var/log/auth.log 来判断是否侵入的,所以就无法正常工作。

重新启动之后问题消失。

分类: 系统网络 标签: , ,

由 Z buffer 得到 View Space Z

2010年4月23日 Huang Donghai 没有评论

现在几乎没有不用某种程度的延迟模式渲染,我们经常需要在 GBuffer 里保存 Z 信息,然后可以反算出视点坐标系或者世界坐标系的坐标。如果你直接保存 ViewSpace 的 Z,那没有这篇所说的问题。但是保存 ViewSpace Z,需要 DepthBuffer 之外的地方来保存。有的引擎直接用 Hardware ZBuffer,OpenGL 上这个并不难实现,D3D 上需要些 Tricks,要用 vendor 提供的特殊的 FOURCC 格式,比如 nVidia 的 RAWZ、INTZ,ATi 的 DF16、DF24,好像最新的 ATi 显卡也支持 INTZ 了。

由 ZBuffer Z 得到 ViewSpace Z 是件很简单的事情,我们先看 Direct3D 和 OpenGL 上的变换矩阵:

OpenGL:

\mathrm{P}_{OpenGL}=\left(\begin{array}{cccc} {\displaystyle \frac{2n}{r-l}} & 0 & {\displaystyle \frac{r+l}{r-l}} & 0\\ 0 & {\displaystyle \frac{2n}{t-b}} & {\displaystyle \frac{t+b}{t-b}} & 0\\ 0 & 0 & {\displaystyle -\frac{f+n}{f-n}} & {\displaystyle -\frac{2fn}{f-n}}\\ 0 & 0 & -1 & 0\end{array}\right)

z\in\left[-1,1\right]

D3D:

\mathrm{P}_{D3D}=\left(\begin{array}{cccc} {\displaystyle \frac{2n}{r-l}} & 0 & {\displaystyle \frac{r+l}{r-l}} & 0\\ 0 & {\displaystyle \frac{2n}{t-b}} & {\displaystyle \frac{t+b}{t-b}} & 0\\ 0 & 0 & {\displaystyle \frac{f}{f-n}} & {\displaystyle -\frac{fn}{f-n}}\\ 0 & 0 & 1 & 0\end{array}\right)

z\in\left[0,1\right]

OpenGL 的投影变换矩阵变换后,z 的值在 [-1, 1] 范围,而 D3D 的值在 [0, 1] 范围。新手混淆不清的是,OpenGL 的 z 实际上是在 [1, -1],因为他的右手坐标系,z 是指向你的眼睛的,所以,远的地方 z 值反而较小,以至于为负。

但是当你写到硬件 ZBuffer 以后,读出来的时候,无论是 OpenGL 还是 D3D,无论是那个格式(RAWZ, INTZ, DF16, DF24, GL_DEPTH_COMPONENTxx),都是 [0, 1] 范围,0 是最近点,既是 near plane 的 z,1 是最远点,far plane 的 z。这也是经常让人混淆的东西。因为这个原因,我们可以值用 D3D 的公式来算。

有了这些信息,我们不难推导出求 ViewSpace Z 的公式。

因为:

\left(\begin{array}{cccc} {\displaystyle \frac{2n}{r-l}} & 0 & {\displaystyle \frac{r+l}{r-l}} & 0\\ 0 & {\displaystyle \frac{2n}{t-b}} & {\displaystyle \frac{t+b}{t-b}} & 0\\ 0 & 0 & {\displaystyle \frac{f}{f-n}} & {\displaystyle -\frac{fn}{f-n}}\\ 0 & 0 & 1 & 0\end{array}\right)\left(\begin{array}{c} x_{v}\\ y_{v}\\ z_{v}\\ w_{v}\end{array}\right)=\left(\begin{array}{c} x_{c}\\ y_{c}\\ z_{c}\\ w_{c}\end{array}\right)

我们只关心 Z 的值:

\begin{cases} \begin{array}{l} {\displaystyle \frac{f}{f-n}}z_{v}-{\displaystyle \frac{fn}{f-n}=z_{c}}\\ \\w_{c}=z_{v}\end{array}\end{cases}

根据上两式我们得到:

\begin{array}{rcl} z_{buf} & = & {\displaystyle \frac{z_{c}}{z_{v}}}\\ & = & \frac{{\displaystyle \frac{f}{f-n}}z_{v}-{\displaystyle \frac{fn}{f-n}}}{{\displaystyle z_{v}}}\\ & = & {\displaystyle \frac{f}{f-n}}-{\displaystyle \frac{fn}{z_{v}\left(f-n\right)}}\end{array}

移动一下位置:

{\displaystyle \frac{fn}{z_{v}\left(f-n\right)}}={\displaystyle \frac{f}{f-n}}-z_{buf}

两边乘以\left(f-n\right)

{\displaystyle \frac{fn}{z_{v}}}=f-z_{buf}\left(f-n\right)

最终得到:

\mathrm{z}_{view}=\frac{fn}{f-\mathrm{z}_{buf}(f-n)}

神奇的是,这个公式反而不分 OpenGL 和 D3D 了。这是因为,虽然 OpenGL 和 D3D 的透视变换矩阵虽然不同,定义的 clip space 的 z 也不同,但是,实际上写入物理 zbuf 的值,却是完全一样的,所以一个公式可以搞定。

具体实现上,可以传入 f*n, f, f-n 三个值,以减少 shader 的计算,如果这样的话,HLSL 代码大概是这样的:

// zparam, for recover view space z, Zview = 2*f*n/((f+n)-Zbuf(f-n)), where Zbuf is [-1,1]
// if Zbuf is [0,1], acultly we use, the equation should be
//
//              f * n
// Zview = -------------------
//           f - Zbuf(f-n)
// faster than use matrix multiply, but only for perspective projection
 
struct ZrecoverParam {
    float near, far, farXnear, farSUBnear;
};
 
const ZrecoverParam g_zrecoverParam;
 
// return n~f
float ZR_getViewSpace(float zbuf) {
    return g_zrecoverParam.farXnear / (g_zrecoverParam.far - zbuf * g_zrecoverParam.farXnear);
}
// return 0~1 view space
float ZR_getViewSpaceNormalized(float zbuf) {
    return 1.0 / (zbuf * g_zrecoverParam.far + g_zrecoverParam.farXnear);
}

Switch to our mobile site