Tag Archives: 技术

2011 – 中

好吧,首先,现在已经不是2011年中了,时间嗖嗖的。看pangwa搞的这blog主题不错,杠杠的。

然后,必须要好好搞搞技术了。直接原因是,最近部门考虑重新评估长期技术路线,准备把从前台到后台的整套系统都好好折腾下。于是就有很多的所谓technical workshop / discussion,于是就有很多良性的非良性的技术的非技术的架吵,于是就发现现在忽悠,尤其是在技术方面的忽悠能力下降很多。归根结底,不是忽悠能力的下降,而是技术能力的下降。反思一下,自从从Autodesk跳槽之后,对技术的关注少了很多。前两天买了本久违的程序员,满篇尽是新词语(好吧,各种NoSQL,各种BigTable,各种Cluster),情何以堪啊情何以堪。对新的存储技术、分布式技术的认识,落后了不少。

回头评价一下从Microsoft跳槽Cisco这件事情。在Microsoft做的是Visual Studio里支持Business Application开发的功能和模块。所谓Business Application,无非就是围绕数据、流程、分布式、更上层更傻瓜化(好吧,智能化)的开发。在Cisco做的事情其实某种意义上来说一脉相承,做的就是支持Manufactoring的Business Application / System。从技术上讲,需要处理高并发、海量数据,需要保证接近100%的系统可靠性,需要支持复杂的业务逻辑,需要支持客户端用户开发一些数据处理、测试、仿真的程序和脚本。从行业上讲,和之前做的产品研发不同,而更接近信息系统开发,某种意义上讲也更接近互联网行业。总体来讲,能接触新的技术领域、能接触新的流程,我认为利还是远大于弊的。当然,毫无疑问,频繁跳槽是不对的。。。

好了,CHEER UP!

Bresenham算法

上回说到, 在看一本书《Windows游戏编程大师技巧》 (Tricks of Windows Game Programming Gurus). 这次继续书里的内容: 直线光栅化的Bresenham算法. 书上讲的比较含糊, 没有讲算法的推导过程, 更没讲算法是怎么想出来的. 所以我们只好自己动手, 丰衣足食…

直线光栅化

直线光栅化是指用像素点来模拟直线. 比如下图中用蓝色的像素点来模拟红色的直线. 图中坐标系是显示器上的坐标系: x轴向右, y轴向下.

bresenham

设deltaX = endX – startX, deltaY = endY – startY. 那么斜率为k = deltaY / deltaX. 我们先考虑简单的情况: 当 0 < k < 1即直线更贴近x轴. 在这种情况下deltaY < deltaX, 所以在光栅化的过程中, 在y轴上描的点比在x轴上描点少. 那么就有一个很直观的光栅化算法:

line_bresenham(startX, startY, endX, endY)
{
    deltaX = endX - startX;
    deltaY = endY - startY;
    k = deltaY / deltaX;
 
    <span style="color: #0000ff">for</span> (x = startX, y = startY; x &lt;= endX; ++x)
    {
        <span style="color: #0000ff">if</span> (满足一定条件)
        {
            ++y;
        }
        drawPixel(x, y);
    }
}

基于斜率 / 距离的两个简单直线光栅化算法

好了,貌似很简单, 就剩一个问题: “满足一定条件”是什么? 可以用斜率判断, 也可以用上图中直线与光栅线交点 (红点) 光栅点 (蓝点) 的距离来判断. 继续用伪代码说话:

<span style="color: #008000">// 算法1: 用斜率判断</span>
<span style="color: #0000ff">void</span> line_bresenham_k(startX, startY, endX, endY)
{
    deltaX = endX - startX;
    deltaY = endY - startY;
    k = deltaY / deltaX;
 
    <span style="color: #0000ff">for</span> (x = startX, y = startY; x &lt;= endX; ++x)
    {
        <span style="color: #0000ff">if</span> (x - startX != 0)
        {
            <span style="color: #008000">// 计算当前斜率</span>
            currentK = (y - startY) / (x - startX);
 
            <span style="color: #008000">// 如果当前斜率 &lt; k, 则增加y坐标</span>
            <span style="color: #0000ff">if</span> (currentK &lt; k)
            {
                ++y
            }
        }
        drawPixel(x, y);
    }
}
 
<span style="color: #008000">// 算法2: 用距离判断. 计算直线与光栅线交点y坐标我们需要用到</span>
<span style="color: #008000">// 直线方程 y = k (x - startX) + startY</span>
line_bresenham_dist(startX, startY, endX, endY)
{
    deltaX = endX - startX;
    deltaY = endY - startY;
    k = deltaY / deltaX;
 
    <span style="color: #0000ff">for</span> (x = startX, y = startY; x &lt;= endX; ++x)
    {
        <span style="color: #008000">// 计算直线与光栅线交点的y坐标, 以及与光栅点的距离</span>
        ptY = k * (x - startX) + startY;
        dist = ptY - y;
 
        <span style="color: #008000">// 如果距离 &gt; 0.5或者 &lt; -0.5, 说明我们需要增加y以</span>
        <span style="color: #008000">// 将距离的绝对值控制在0.5之类</span>
        <span style="color: #0000ff">if</span> (dist &gt; 0.5 || dist &lt; -0.5)
        {
            ++y;
        }
        drawPixel(x, y);
    }
}

消灭浮点数!

以上都是很直观的算法, 下面不直观的来了 – 上面的算法都需要在循环体内执行乘法, 准确的说, 是进行浮点数的乘法. 我们怎么能减少这些浮点数的乘法开销呢? 以基于距离的算法2为例: 首先, k是一个浮点数, 0.5也是浮点数. 我们可以通过将这些表达式都乘以2 * deltaX (整数) 来解决浮点数的问题. 伪代码:

<span style="color: #008000">// 算法3: 在算法2的基础上消灭浮点数!</span>
line_bresenham_dist(startX, startY, endX, endY)
{
    deltaX = endX - startX;
    deltaY = endY - startY;
 
    <span style="color: #0000ff">for</span> (x = startX, y = startY; x &lt;= endX; ++x)
    {
        <span style="color: #008000">// 计算直线与光栅线交点的y坐标, 以及与光栅点的距离</span>
        ptY1 = deltaY * (x - startX) + startY * deltaX;
        dist1 = ptY1 - y * deltaX;
        dist1 = dist1 &lt;&lt; 1; <span style="color: #008000">// dist1 = dist1 * 2</span>
 
        <span style="color: #008000">// 如果距离 &gt; 0.5或者 &lt; -0.5, 说明我们需要增加y以</span>
        <span style="color: #008000">// 将距离的绝对值控制在0.5之类</span>
        <span style="color: #0000ff">if</span> (dist1 &gt; deltaX || dist &lt; -deltaX)
        {
            ++y;
        }
        drawPixel(x, y);
    }
}

消灭乘法!

圆满解决浮点数运算问题! 不过…乘法运算还在. 消灭乘法问题的办法比较不直观, 让我们想一想: 还有什么办法能简化运算. 直线方程已经不能再简化, 所以唯一的突破口就是能不能利用递推 / 用上一次循环的计算结果推导下一次循环的计算结果.

首先我们来看看在算法2的基础上 (因为算法2计算红点蓝点之间的距离, 比较直观), 怎么通过第n – 1次循环计算出的dist值 (设为d1) 来推导出第n次循环的dist值 (设为d2). 先回顾一下: dist = 直线与光栅线交点的y坐标 – 相应光栅点的y坐标. 我们从几何上直观地考虑: 在第n次循环中, 我们先根据上一次循环所计算出来的d1, 暂时令d2 = d1 + k, 因为我们要保证-0.5 < d2 < 0.5, 而d1 + k满足d1 + k > –0.5, 所以我们只需要考虑当d1 + k > 0.5时, 我们需要将光栅点y坐标增加1, 并且将d2减去1. 显然, 设y1是第n – 1次循环中光栅点的y坐标, y2是第n次循环中光栅点的y坐标. 我们有
1) d2 = d1 + k –  (y2 – y1)
2) 当d1 + k > 0.5时y2 = y1 + 1, 否则y2 = y1
我们已经能根据上面的两个关系式写出算法, 不过为了消除乘法和浮点数, 我们将这两个关系式两端同时乘以2 * deltaX, 并且设e = 2 * deltaX * d, 则我们有
3) e2 =  e1 + 2 * deltaY – 2 * deltaX * (y2 – y1)
4) 当e1 + 2 * deltaY > deltaX时y2 = y1 + 1, 否则y2 = y1
终于, 没有了乘法 (2 * deltaY在循环体外计算且被简化为左移一位的运算), 没有了浮点数, 根据关系式3) 和 4), 写出算法:

<span style="color: #008000">// 算法4: 在算法2, 3的基础上利用递推消灭乘法和浮点数!</span>
line_bresenham(startX, startY, endX, endY)
{
    deltaX = endX - startX;
    deltaY = endY - startY;
    e = 0;
    deltaX2 = deltaX &lt;&lt; 1;
    deltaY2 = deltaY &lt;&lt; 1;
 
    drawPixel(startX, startY);
 
    <span style="color: #0000ff">for</span> (x = startX + 1, y = startY; x &lt;= endX; ++x)
    {
        <span style="color: #008000">// 关系式3) e2 =  e1 + 2 * deltaY – 2 * deltaX * (y2 – y1)</span>
        <span style="color: #008000">// 关系式4) 当e2 + 2 * deltaY &gt; deltaX时y2 = y1 + 1, 否则y2 = y1</span>
        e += deltaY2;
        <span style="color: #0000ff">if</span> (e &gt; deltaX)
        {
            e -= deltaX2;
            ++y;
        }
        drawPixel(x, y);
    }
}

消灭浮点数! 代数推导

上面递推关系的推导过程是从图形上”直观”地分析得来的, 但是不严密. 我们能不能形式化地证明关系式1), 2), 3), 4)呢? 因为关系式3), 4)和1), 2)能互相推导, 我们只证明3), 4)如下:

在算法3的基础上设第n – 1次循环计算出的dist1值为e1, 对应的y值为y1, 第n次循环计算出的dist1值为e2, 对应的y值为y2. 根据算法3,
dist1 = 2 * deltaY * (x – startX) + 2 * startY * deltaX – 2 * y * deltaX, 则
e2 – e1
= 2 * deltaY * (x – startX) + 2 * startY * deltaX – 2 * y2 * deltaX – [2 * deltaY * (x – 1 – startX) + 2 * startY * deltaX – 2 * y1 * deltaX ]
=  – 2 * y2 * deltaX + 2 * deltaY + 2 * y1 * deltaX
= 2 * deltaY – 2 * deltaX * (y2 – y1)
所以e2 = e1 + deltaY – deltaX * (y2 – y1). 所以我们有关系式
1) e2 = e1 + 2 * deltaY – 2 * deltaX * (y2 – y1)
2) –deltaX <  e1 < deltaX
3) –deltaX < e2 < deltaX
4)  y2 – y1 = 0 或者 1 
我们根据e1 + 2 * deltaY的取值范围进行讨论. 首先, 因为不等式6), 我们有
2 * deltaY – deltaX < e1 + 2 * deltaY < 2 * deltaY + deltaX

情况1: 如果2 * deltaY – deltaX < e1 + 2 * deltaY < deltaX, 则
2 * deltaY – deltaX – 2 * deltaX * (y2 – y1) < e2 < deltaX– 2 * deltaX * (y2 – y1) 
证: 若y2 – y1 = 1, 则 2 * deltaY – deltaX – 2 * deltaX < e2 < deltaX – 2 * deltaX = -deltaX, 所以y2 – y1 = 1不成立. 即情况1中y2 = y1.

情况2:  如果 deltaX < e1 + 2 * deltaY < 2 * deltaY + deltaX, 则
deltaX – 2 * deltaX * (y2 – y1) < e2 < 2 * deltaY + deltaX – 2 * deltaX * (y2 – y1)
反证: 若y2 – y1 = 0, 则 deltaX < e2 < 2 * deltaY + deltaX 所以y2 – y1 = 0不成立. 即情况2中y2 = y1 + 1.

打了这么多字, 累…以上就是当0 < k < 1的情况, 剩余几种情况 (k > 1, –1 < k < 0, k < –1. 不要挑剔我不用”>=”这种符号…) 都可以通过简单的x, y交换以及正负交换来搞定.

Makefile 自动生成头文件的依赖关系

最近在看一本书《Windows游戏编程大师技巧》 (Tricks of Windows Game Programming Gurus). 第一章给出了一个打砖块小游戏的示例程序. 包括三个文件: blackbox.h, blackbox.cpp和freakout.cpp (600行代码, 对于Windows C++程序来说还好, 没有让我freak out…). blackbox.cpp封装了部分DirectDraw, 提供了一些更傻瓜化的初始化DirectDraw, 画点, 画方框的工具函数. blackbox.h包括了这些函数的声明. freakout.cpp引用了blackbox.h文件, 包括WinMain主函数和主要的游戏逻辑.

How to build?

和我一样还在使用N年前的垃圾电脑的看官们, 多半也不愿意用Visual Studio来挑战自己的耐心. 但是所以让我们选择小米加步枪: GCC (MinGW) + Makefile. 写个简单的makefile例如:

all:    freakout.exe
 
freakout.exe: freakout.o blackbox.o
    g++ freakout.o blackbox.o -o stupid.exe -L D:gamedevdx81sdkDXFDXSDKlib -lddraw -mwindows
 
freakout.o: freakout.cpp <strong><span style="color: #ff0000;">blackbox.h</span></strong>
    g++ -c freakout.cpp -o freakout.o -I D:gamedevdx81sdkDXFDXSDKinclude
 
blackbox.o: blackbox.cpp <strong><span style="color: #ff0000;">blackbox.h</span></strong>
    g++ -c blackbox.cpp -o blackbox.o -I D:gamedevdx81sdkDXFDXSDKinclude

Problem?

上面的代码当然有很多问题, 比如”-L <DX lib path>”, “-I <DX inc path>”可以被定义在类似$(LIB), $(INC)的变量里面, 比如我没有按照makefile的习惯写一个clean目标清理生成的东东…不过我的重点是在红色高亮的blackbox.h: 两个.o文件都需要依赖于blackbox.h.

假设我们修改freakout.cpp引用更多的头文件, 每加一条#include “somefile.h”指令, 就需要相应地在makefile里为freakout.o增加一个依赖项somefile.h.

假设我们修改stupid.h文件, 让stupid.h也引用一个头文件someotherfile.h, 那么我们也需要相应地在makefile里为所有依赖stupid.h的.o文件 (在这个例子中是freakout.o和blackbox.o) 增加依赖项someotherfile.h.

所以问题是: 在.cpp和.h被修改的情况下, 怎么维护.o目标文件和.h头文件的依赖关系.

Solution – 自动生成依赖关系

Google一下”makefile 头文件 依赖”会发现大多数编译器都提供了一个选项生成.o目标文件所依赖的文件列表. 比如GCC的”-MM”选项. 运行GCC –MM freakout.cpp blackbox.cpp <库文件和头文件选项>得到输出:

freakout.o: freakout.cpp blackbox.h D:/gamedev/dx81sdk/DXF/DXSDK/include/ddraw.h
blackbox.o: blackbox.cpp blackbox.h D:/gamedev/dx81sdk/DXF/DXSDK/include/ddraw.h

所以一个简单的处理依赖关系的办法是把这些编译器生成的依赖关系写入一个文件里, 然后在makefile中用include指令包含这个文件:

CPP = g++
OBJ = freakout.o blackbox.o
LIB = -L D:gamedevdx81sdkDXFDXSDKlib -mwindows -l ddraw
INC = -I D:gamedevdx81sdkDXFDXSDKinclude
 
all: freakout.exe
 
freakout.exe: ${OBJ}
    ${CPP} ${OBJ} -o freakout.exe ${LIB}
 
<span style="color: #ff0000;"><strong>include depend</strong></span>
 
# note the symbols: $&lt; and '$@
%.o: %.cpp
    ${CPP} -c $&lt; -o $@ ${INC}
 
# generate depend file
<span style="color: #ff0000;"><strong>depend:</strong></span>
    <span style="color: #ff0000;"><strong>${CPP} -MM ${OBJ:.o=.cpp} ${INC} &gt; depend</strong></span>

注意红色的部分, depend是GCC生成的包含了依赖关系的文件 (也注意上面的makefile中有”$<”, “$@”这种perl风格的bt匹配字符…) . 有了这样的makefile, 我们就能用两个步骤来build项目:

1) 在需要更新依赖关系的时候 (比如在某个文件里多加了一条#include指令) 运行make depend.

2) 运行make.

Makefile Auto Dependency – 只运行一次make

懒惰是程序员的一大美德, 所以GNU make的手册里提供了一个运行一次make就能更新所有依赖关系并且按照依赖关系build的办法 (http://www.gnu.org/software/make/manual/make.html#Automatic-Prerequisites): 与整体引入一个depend目标不同, 我们为每一个.o/.cpp文件引入一个.d依赖关系文件. 依赖关系文件由GCC的”-MM”选项生成, 并且在GCC输出的基础上把自己本身加入到目标列表中. 比如:

# freakout.d
freakout.o <strong><span style="color: #ff0000;">freakout.d</span></strong>: freakout.cpp D:/gamedev/dx81sdk/DXF/DXSDK/include/ddraw.h blackbox.h
 
# blackbox.d
blackbox.o <strong><span style="color: #ff0000;">blackbox.d</span></strong>: blackbox.cpp D:/gamedev/dx81sdk/DXF/DXSDK/include/ddraw.h blackbox.h

注意红色的部份: .d文件被加入到了目标列表, 依赖相关的.h和.cpp文件. 那么怎样自动生成这些.d文件呢? 类似的, makefile:

CPP = g++
OBJ = freakout.o blackbox.o
LIB = -L D:gamedevdx81sdkDXFDXSDKlib -mwindows -l ddraw
INC = -I D:gamedevdx81sdkDXFDXSDKinclude
 
all: freakout.exe
 
freakout.exe: ${OBJ}
    ${CPP} ${OBJ} -o freakout.exe ${LIB}
 
<span style="color: #ff0000;"><strong>include ${OBJ:.o=.d}</strong></span>
 
%.o: %.cpp
    ${CPP} -c $&lt; -o $@ ${INC}
 
<strong><span style="color: #ff0000;">%.d: %.cpp</span></strong>
    rm -f $@ &amp; 
    ${CPP} -MM $&lt; ${INC} &gt; $@.$$ &amp; 
    insertdfile.exe $&lt; $@.$$ &gt; $@ &amp; 
    rm -f $@.$$

这个makefile和上一节给出的makefile大致差不多, 不同的是两个红色的部分: 第一部分我们用include指令include相关的所有.d文件; 第二部分定义了生成.d文件的规则 (又是$@, $<符号…): 第一行首先删除原有的.d文件; 第二行运行g++ -MM生成依赖关系写入一个临时文件($@.$$)里; 第三行把.d文件加入到第二行生成的依赖关系中并把最终结果写到.d文件中; 第四行删除临时文件.

P.S. insertdfile.exe是我自己写的一个小程序, 用来把.d文件加入到第二行生成的依赖关系中, 例如把”blackbox.o: blackbox.cpp blackbox.h”转换为”blackbox.o blackbox.d: blackbox.cpp blackbox.h”. 我为什么要自己写一个字符串替换的程序? 因为我没有装sed工具…如果有, 当然用官方推荐的办法, 把第三行换成神奇的符咒: sed ‘s,($*).o[ :]*,1.o $@ : ,g’ < $@.$$ > $@ .

好了, 运行make:

1) make尝试去包含blackbox.d和freakout.d文件, 没有发现, 所以检查有没有规则可以生成这些.d文件, 发现我们%.d: %.cpp的这条规则, 于是运行这条规则生成所有的.d文件并且包含进来.
2) 按照正常规则生成.o文件, 然后生成.exe.

然后我们做一些修改, 比如增加一个stupid.h文件, 然后修改blackbox.h文件以包含stupid.h. 运行make:

1) make把第一个目标all加入到需要生成的目标列表中.
2) make包含blackbox.d和freakout.d文件. 注意在这个过程中这两个.d文件也会被加入到make的需要生成的目标列表中 – 因为可能有规则能更新这两个.d文件.
3) 根据blackbox.d和freakout.d包含的规则: .d文件 (已经被入到了make的需要生成目标列表中) 需要被更新, 于是相应地运行%.d: %.cpp规则更新.d文件: 新加入的 stupid.h文件被加入到两个.d文件的依赖项中.
4) make发现包含的两个.d文件在第2)步中都被更新, 于是重新包含这两个.d文件.
5) make根据在第2)步生成在第3)步被包含进来的规则, 发现stupid.h的日期比两个.o文件的日期都要新, 于是重新生成.o文件.
6) 根据规则生成.exe.