游戏编程算法与技巧 Game Programming Algorithms and Techniques (Sanjay Madhav 著)

http://gamealgorithms.net

第1章 游戏编程概述 (已看)

第2章 2D图形 (已看)

第3章 游戏中的线性代数 (已看)

第4章 3D图形 (已看)

第5章 游戏输入 (已看)

第6章 声音 (已看)

第7章 物理 (已看)

第8章 摄像机 (已看)

第9章 人工智能 (已看)

第10章 用户界面 (已看)

第11章 脚本语言和数据格式 (已看)

第12章 网络游戏 (已看)

第13章 游戏示例:横向滚屏者(iOS)

第14章 游戏示例:塔防(PC/Mac)

第1章 游戏编程概述

  游戏编程的发展

世界上第一款商业游戏Computer Space在1971年推出.是后来Atari的创办人Nolan Bushnell 和 Ted Dabney 开发的.这款游戏当时并不是运行在传统计算机上的.它的硬件甚至没有处理器和内存.它只是一个简单的状态机,在多个状态中转换.Computer Space的所有逻辑必须在硬件上完成.

随着"Atari 2600"在1977年推出,开发者们有了一个开发游戏的平台.这正是游戏开发变得更加软件化的时候,再也不用设计复杂的硬件了.从Atari时期一直到现在,仍然有一些游戏技术保留着

家用机推出的时候,它的硬件就会被锁定5年多,称为一个"世代".家用机的优点也在于其锁定了硬件,使得程序员可以有效利用机能.

    Atari时期(1977-1985年)

这个时期的程序员需要对底层硬件有一定的理解.CPU运行在1.1MHz,内存只有128字节.这些限制使得用C语言开发不太实际.大多数游戏都是完全用汇编语言开发的. 更糟糕的是,调试是完全看个人能力的.没有任何开发工具和SDK.

    NES和SNES时期(1985-1995年)

然而到了1983年,北美游戏市场崩溃了.主要原因在于,市场上充斥着大量质量低下的游戏.

直到1985年推出的红白机(NES)才把产业带回正轨.

到了超级任天堂(SNES)时代,开发团队进一步扩大.随着开发团队的扩大,不可避免地会变得更加专业化.

NES和SNES的游戏仍然完全用汇编语言开发,因为内存依然不足.幸运的是任天堂有提供SDK和开发工具,开发者不再像Atari时期那么痛苦.

    PS和PS2时期(1995-2005年)

由于高级语言带来了生产力提升,开发团队规模的增长在这个时期刚开始有所减缓.大部分早期游戏仍然只需要8~10位程序员.即使最复杂的游戏,比如2001年推出的GTA3,工程师团队也是那样的规模

虽然本时期早期开发团队规模跟NES时期差不多,可是到了后期就变庞大了不少.比如2004年在Xbox推出的Full Spectrum Warrior 总共有15名程序员参与开发,大部分都是专门开发某个功能的.但这个增加比起下个时期可以说微不足道

    Xbox360,PS3和Wii时期(2005-2013年)

家用机的高画质表现导致游戏开发进入了两种境地.AAA游戏变得更加庞大,也有着相应庞大的团队和费用.而独立游戏则跟过去的小团队开发规模相仿

游戏编程的另一个大的开发趋势就是中间件及开源的出现.有的中间件是完整的游戏引擎,比如Unreal,Unity.有的则是专门做某个系统,比如物理引擎Havok Physics. 这样就节省了大量的人力,财力.但是缺点就是某个策划功能可能会受到中间件的限制

    游戏的未来

尽管游戏开发多年来有许多变迁,有趣的是,许多早期概念到现在仍然适用,20年都没变的核心概念的基础部分: 游戏循环, 游戏时间管理和游戏对象模型

  游戏循环

整个游戏程序的核心流程控制称为游戏循环.之所以是一个循环,是因为游戏总是不断地执行一系列动作直到玩家退出.每迭代一次游戏循环称为1帧.大部分实时游戏每秒钟更新30~60帧.如果一个游戏跑60FPS(帧/秒),那么这个游戏循环每秒要执行60次.

游戏开发中有着各种各样的游戏循环,选择上需要考虑许多因素,其中以硬件因素为主

    传统的游戏循环

一个传统的游戏循环可以分成3部分: 处理输入,更新游戏世界,生成输出.一个基本的游戏循环可能是这样的:

while game is running
    process inputs
    update game world
    generate outputs
loop
View Code

process inputs会检查各种输入设备,比如键盘,鼠标,手柄.但是这些输入设备并不只输入已经想到的,任何外部的输入在这一阶段都要处理完成

update game world 会执行所有激活并且需要更新的对象.这可能会有成千上万个游戏对象.

对于generate outputs, 最耗费计算量的输出通常就是图形渲染成2D或3D.另外还有其他一些输出,比如音频,涵盖了音效,背景音乐,对话,跟图形输出同样重要.

while player.lives > 0
    // 处理输入
    JoystickData j = grab raw data from joystick

    // 游戏世界更新
    update player.position based on j
    foreach Ghost g in world
        if player colliders with g
            kill either player or g
        else 
            update AI for g based on player.position
        end
    loop

    // Pac-Man 吃到药丸
    ...

    // 输出结果
    draw graphics
    update audio
loop
View Code

    多线程下的游戏循环

  时间和游戏

大多数游戏都有关于时间进展(progression of time)的概念.对于实时游戏,时间进展通常都很快.比如30FPS(Frame Per Second)的游戏,每帧大约用时33ms.即使是回合制游戏也是通过时间进展来完成功能的,只不过它们使用回合来计数

    真实时间和游戏时间

真实时间,就是从真实世界流逝的时间;游戏时间,就是从游戏世界流逝的时间.区分它们很重要.虽然通常都是1:1,但不总是这样.比如说,游戏处于暂停状态.虽然真实时间流逝了,但是游戏时间没有发生变化

马克思 佩恩 运用了"子弹时间"的概念去减慢游戏时间.这种情况下,游戏时间比实际时间要慢得多.与减速相反的例子就是,很多体育类游戏都提升了游戏速度.在足球游戏中,不要求玩家完完全全地经历15分钟,而是通常都会让时钟拨快1倍.还有一些游戏甚至会有时间倒退的情形,比如<<波斯王子:时之沙>>就可以让玩家回到之前的时刻

    通过处理时间增量来表示游戏逻辑

对于 enemy.position.x += 5   16MHz的处理器比8MHz的处理器要快1倍.

为了解决这样的问题,需要引入增量时间的概念: 从上一帧起流逝的时间 enemy.position.x += 150 * deltaTime

while game is running
    realDeltaTime = time since last frame
    gameDeltaTime = realDeltaTime * gameTimeFactor
    
    // 进程输入
    ...
    update game world with gameDeltaTime
    
    // 渲染输出
    ...
loop
View Code
targetFrameTime = 33.3f
while game is running
    realDeltaTime = time since last frame
    gameDeltaTime = realDeltaTime * gameTimeFactor

    // 处理输入
    ...
    
    update game world with gameDeltaTime

    // 渲染输出
    ...

    while (time spent this frame) < targetFrameTime
        // 做一些事情将多出的时间用完
        ...
    loop
loop
View Code

  游戏对象

广义上的游戏对象是每一帧都需要更新或者绘制的对象.虽然叫作"游戏对象",但并不意味着就必须用传统的面向对象.有的游戏采用传统的对象,也有的用组合或者其他复杂的方法.不管如何实现,游戏都需要跟踪管理这些对象,在游戏循环中把它们整合起来.

    游戏对象的类型

更新和绘制都需要的对象.  任何角色,生物或者可以移动的物体都需要在游戏循环中的 update game world 阶段更新,还要在 generate outputs 阶段渲染.在<<超级马里奥兄弟>>中,任何敌人及所有会动的方块都是这种游戏对象

只绘制不更新的对象,  称为静态对象. 这些对象就是那些玩家可以看到,但是永远不需要更新的对象.他可以是游戏背景中的建筑.一栋建筑不会移动也不会攻击玩家,但是需要绘制

需要更新但不需要绘制的对象.  一个例子就是摄像机.技术上来讲,你是看不到摄像机的,但是很多游戏都会移动摄像机.另一个例子就是触发器.触发器会检测玩家的位置,然后触发合适的行为.所以触发器j就是一个看出见的检测与玩家碰撞的盒子.

    游戏循环中的游戏对象

class GameObject
    // 成员变量/函数
    ...
end

interface Drawable
    function Draw()
end

interface Updateable
    function Update(float deltaTime)
end

// 只更新的游戏对象
class UGameObject inherits GameObject, implements Updateable
    // 重载更新函数
    ...
end

// 只渲染的游戏对象
class DGameObject inherits GameObject, implements Drawable
    // 重载绘制函数
    ...
end

class DUGameObject inherits UGameObject, implements Drawable
    // 重载绘制和更新函数
    ...
end

class GameWorld
    List updateableObjects
    List drawableObjects
end

while game is running
    realDeltaTime = time since last frame
    gameDeltaTime = realDeltaTime * gameTimeFactor
    
    // 处理输入
    ...
    
    // 更新游戏世界
    foreach Updateable o in GameWorld.updateableObjects
        o.Update(gameDeltaTime)
    loop

    // 渲染输出
    foreach Drawable o in GameWorld.drawableObjects
        o.Draw()
    loop

    // 帧数限制代码
    ...
loop
View Code

  相关资料

    游戏编程的发展

Crane,David. "GDC 2011 Classic Postmortem on Pitfall!" (https://www.youtube.com/watch?v=tfAnxaWiSeE). Pitfall! 的作者 David Crane 的1小时演讲,关于在Atari上开发的心得.

    游戏循环

Gregory,Jason. Game Engine Architecture. Boca Raton: A K Peters, 2009. 这本书用了几节篇幅讲了多种多线程下的游戏循环,包括在PS3的非对称CPU架构上使用的情况.

West,Mick "Programming Responsiveness" 和 "Measuring Responsiveness"(http://tinyurl.com/594f6r和http://tinyurl.com/5qv5zt).这些是Mick West在Gamasutra上些的文章,讨论了那些导致输入延迟的因素,同时也对游戏中的输入延迟进行了测量

    游戏对象

Dickheiser, Michael,Ed. Game Programming Gems 6. Boston: Charles River Media, 2006. 这卷书中的一篇文章 "Game Object Component System" 讲了一种与传统面向对象游戏对象模型所不同的方法.虽然实现上有点复杂,但是越来越多的商业游戏中的游戏对象通过组合的形式来实现.

第2章 2D图形

   2D渲染基础

    CRT显示器基础

在多年前,阴极射线管(Cathode Ray Tube)显示器是显示器的主流.CRT里图像的元素就是像素.对于彩色显示器,每个颜色由红,绿,蓝组成.显示器的分辨率决定了像素的数量.比如一个300x200的显示器由200行像素,叫作扫描线.每个扫描线可以有300个元素,所以总共有60000个像素之多.位于(0, 0)的像素通常在左上角,但不是所有显示器都这样.

CRT内部,绘制图像是通过电子枪发射电子流完成的.这把枪从左上角开始沿第一条扫描线进行绘制.当它完成之后就继续下一条扫描线,然后不断地重复,直到所有扫描都画完

当电子枪刚刚完成一帧的绘制的时候,它的枪头在右下角.喷枪从右下角移动到左上角所花费的时间,我们称为场消隐期(VBLANK).这个间隔以ms计,间隔不是由CRT,计算机或者电视机决定的,而是由用途决定的

    像素缓冲区和垂直同步

新的硬件使得由足够的内存将所有颜色保存在像素缓冲区中.但这不是说游戏循环就可以完全无视CRT喷枪.假设喷枪在屏幕上绘制到一半时,刚好游戏循环到了"generate outputs"阶段.它开始为新的一帧往像素缓冲区写像素时,CRT还在上一帧的绘制过程中.这就导致了屏幕撕裂,具体表现就是屏幕上同时显示了不同的两帧的各自一半画面.更糟糕的是,新一帧的数据提交时,上一帧还没开始.这就不仅是一帧中有两半不同的画面了,而是连画面都没有

一个解决方案就是同步游戏循环,等到场消隐期再开始渲染.这样会消除分裂图像的问题,但是它限制了游戏循环的间隔,只有场消隐期期间才能进行渲染,对于现在的游戏来说是不行的

另一个解决方案叫作双缓冲技术.双缓冲技术里,有两块像素缓冲区.游戏交替地绘制在这两块缓冲区里.在1帧内,游戏循环可能将颜色写入缓冲区A,而CRT正在显示缓冲区B.到了下一帧,CRT显示缓冲区A,而游戏循环写入缓冲区B.由于CRT和游戏循环都在使用不同的缓冲区,所以没有CRT绘制不完整的风险

为了完全消灭屏幕撕裂,缓冲区交换必须在场消隐期进行.这就是游戏中常见的垂直同步设置.技术上来讲这是不恰当的,因为垂直同步是显示器在场消隐期刚结束时才告诉你的信号.不管怎样,缓冲区交换是y一个相对快速的操作,游戏渲染一帧花费的时间则长的多(尽管理想中要比CRT绘制一帧快).所以在场消隐期交换缓冲区完全消除了屏幕撕裂风险

有些游戏确实允许缓冲区交换在绘制完成前尽快进行,这就会导致屏幕撕裂的可能.这种情况通常是因为玩家想要获得远比屏幕刷新速度快的频率.如果一款显示器有60Hz的刷新率,同步缓冲区交换到场消隐期最多只有60Hz.但是玩家为了减少输入延迟(或者有很快的机器相应速度),可能会消除同步以达到更高的帧率

function RenderWorld() {
    // 绘制游戏世界中所有对象
    ...
    wait for VBLANK
    swap color buffers
end
View Code

虽然CRT显示器今天几乎不再使用,但是双缓冲技术在正确的时间交换还是能在LCD上消除屏幕撕裂的问题.一些游戏甚至使用了三缓冲技术,使用3个缓冲区而不是两个.三缓冲区能使帧率在一些特殊情况下更加平滑,但也增加了输入延迟

  精灵

精灵是使用图片中的一个方块绘制而成的2D图像.通常精灵用来表示角色和其他动态对象.对于简单的游戏来讲,精灵也可能用于背景,虽然有更加有效的方式来完成,特别是静态的背景.大多数2D游戏运用大量的精灵,对于移动端来说,精灵通常就是游戏体积(磁盘空间占用)的主要部分.所以,高效利用精灵是非常重要的

stb_image.c

    绘制精灵

最简单的绘制场景的方式是,先画背景后画角色.这就像画家在画布上画画一样,也因为这样,这个算法叫作画家算法

class Sprite
    ImageFile image
    int drawOrder
    int x, y
    function Draw()
        // 把图片在正确的(x, y)上绘制出来
        ...
    end
end

SortedList spriteList


// 创建新的精灵...
Sprite newSprite = specify image and desired x/y
newSprite.drawOrder = set desired draw order value
// 根据渲染顺序添加到排序列表
spriteList.Add(newSprite.drawOrder, newSprite)
// 准备绘制
foreach Sprite s in spriteList
    s.Draw()
loop
View Code

画家算法也可以运用在3D环境下,但它有很多缺陷.而在2D场景中,画家算法工作得很号

    动画精灵

struct AnimFrameData
    // 第1帧动画的索引
    int startFrame
    // 动画的所有帧数
    int numFrames
end

struct AnimData
    // 所有动画用到的图片
    ImageFile images[]
    // 所有动画用到的帧
    AnimFrameData frameInfo[]
end

class AnimatedSprite inherits Sprite
    // 所有动画数据(包括ImageFiles和FrameData)
    AnimData animData
    // 当前运行中的动画
    int animNum
    // 当前运行中的动画的帧数
    int frameNum
    // 当前帧播放了多长时间
    float frameTime
    // 动画的FPS(默认24FPS)
    float animFPS = 24.0f

    function Initialize(AnimData myData, int startingAnimNum)
    function UpdateAnim(float deltaTime)
    function ChangeAnim(int num)
end

function AnimatedSprite.Initialize(AnimData myData, int startingAnimNum)
    animData = myData
    ChangeAnim(startingAnimNum)
end

function AnimatedSprite.ChangeAnim(int num)
    animNum = num
    // 当前动画为第0帧的0.0f时间
    frameNum = 0
    animTime = 0.0f
    // 设置当前图像, 设置为startFrame
    int imageNum = animData.frameInfo[animNum].startFrame
    image = animData.images[imageNum]
end

function AnimatedSprite.UpdateAnim(float deltaTime)
    // 更新当前帧播放时间
    frameTime += deltaTime
    
    // 根据frameTime判断是否播放下一帧
    if frameTime > (1 / animFPS)
        // 更新当前播放到第几帧
        // frameTime / (1 / animFPS)就相当于frameTime * animFPS
        frameNum += frameTime * animFPS
    
        // 检查是否跳过最后一帧
        if frameNum >= animData.frameInfo[animNum].numFrames
            // 取模能保证帧数循环正确
            // (比如, If numFrames == 10 and frameNum == 11
            // frameNum会得到11 % 10 = 1)
            frameNum = frameNum % animData.frameInfo[animNum].numFrames
        end
        
        // 更新当前显示图片
        // (startFrame是相对于所有图片来决定的,而frameNum是相对于某个动画来决定的)
        int imageNum = animData.frameInfo[animNum].startFrame + frameNum
        image  = animData.images[imageNum]

        // 我们用fmod(浮点数运算),相当于取模运算
        frameTime = fmod(frameTime, 1 / animFPS)
    end
end
View Code

    精灵表单(SpriteSheet)

TexturePacker

使用单张图片区存储所有精灵,称之为精灵表单.

在精灵表单中,可以让精灵打包尽可能地靠近,从而减少浪费的无用空间

精灵表单的另一个优势就是很多GPU要纹理加载后才能绘制.如果绘制过程中频繁地切换纹理,会造成相当多的性能损耗,特别是对于大一点的精灵.但是如果所有精灵到在一张纹理中,是可以消除切换产生的损耗的

取决与于游戏中精灵的数量,把所有的精灵都放入一张纹理是不现实的.大多数硬件都有纹理最大尺寸限制

  滚屏

    单轴滚屏

在单轴滚屏游戏中,游戏只沿x轴或者y轴滚动.

最简单的方法j就是把关卡背景按屏幕大小进行切割

// 一台iPhone 4/4S 屏幕大小为960 x 640
const int screenWidth = 960;
// 所有屏幕大小的背景图
string backgrounds[] = { "bg1.png", "bg2.png", /* ... */ }
// 所有水平摆放的屏幕大小的背景图数量
int hCount = 0;
foreach string s in backgrounds
    Sprite bgSprite
    bgSprite.image.Load(s)
    // 第1个屏幕在 x = 0处,第2个在x = 960处, 第3个在 x = 1920处...
    bgSprite.x = hCount * screenWidth
    bgSprite.y = 0
    bgSpriteList.Add(bgSprite)
    hCount++
loop
View Code
// camera.x就是player.x在区间中经过clamp的值
camera.x = clamp(player.x, screenWidth / 2, hCount * screenWidth - screenWidth / 2)

Iterator i = bgSpriteList.begin()
while i != bgSpriteList.end()
    Sprite s = i.value()
    // 找到第1张图片来绘制
    if (camera.x - s.x) < screenWidth
        // 第1张图: s.x = 0, camera.x = 480, screenWidth / 2 = 480
        // 0 - 480 + 480 = 0
        draw s at (s.x - camera.x + screenWidth / 2, 0)
        i++
        s = i.value()
        draw s at (s.x - camera.x + screenWidth / 2, 0)
        break
    end
    i++
loop
View Code

    无限滚屏

无限滚屏就是当玩家失败才停止滚屏的游戏.当然,这里不可能有无限多个背景来滚屏.因此游戏中的背景会重复出现.当然,大部分无限滚屏游戏都拥有大量的背景图片和很好的随机性来产生丰富的背景.通常游戏都会用连续的四五张图片组成序列,有了不同系列的图片可选后再打乱重组

    平行滚屏

在平行滚屏中,背景拆分成几个不同深度的层级.每一层都用不同的速度来滚动以制造不同深度的假象

    四向滚屏

在四向滚屏中,游戏世界会在水平和垂直方向上滚动

// 这里假设用二维数据array[row][column]记录所有片段
for int i = 0, i < vCount, i++
    // 这是正确的行吗
    if (camera.y - segments[i][0].y) < screenHeight
        for int j = 0, j < hCount, j++
            // 这是正确的列吗
            if (camera.x - segments[i][j].x) < screenWidth
                // 这是左上角的可见片段
            end
        loop
    end
loop
View Code

  砖块地图(TileMap)

砖块地图通过把游戏世界分割成等分的方块(或者其他多边形)来解决这个问题.每个方块代表的精灵占据着某一块网格位置.这些引用的精灵可以放在多张或者一张砖块集合里.所以如果树木在砖块集合中的索引号为0,每个表示树木的方块都可以用0表示.虽然正方形是砖块地图的最常见形式,但这不是必须的.一些游戏采用六边形,有的则采用平行四边形.这主要取决于你希望的视角

不管什么情况,砖块地图是一种很好的节省内存的方式,这让策划和美工更容易工作.那些动态创建内容的2D游戏,比如Spelunky,没有砖块地图将很难实现

    简单的砖块地图

Tiled Map Editor

// 基本关卡文件格式 5 x 5
5, 5
0, 0, 1, 0, 0
0, 1, 1, 1, 0
1, 1, 2, 1, 1
0, 1, 1, 1, 0
0, 0, 1, 0, 0

class Level
    const int tileSize = 32
    int width, height
    int tiles[][]
    function Draw()
end

function Draw()
    for int row = 0, row < height, row++
        for int col = 0, col < width, col++
            // 在 (col * tileSize, row * tileSize)绘制tiles[row][col]
        loop
    loop
end
View Code

虽然这种基于文本的砖块地图在简单关卡中运行顺利,但是在商业游戏中,会采用更加健壮的格式

    斜视等视角砖块地图

在斜视等视角中,视角通过旋转,让场景更具深度感

  相关资料

    Cocos2D

iOS游戏开发最流行的2D代码库,在http://cocos2d.org/可以获取

Itterheim, Stephen. Learn Cocos2d 2: Game Development for iOS. New York: Apress, 2012. 虽然有很多本书讲Cocos2D,但这本书是相对强的

    SDL

Simple DirectMedia Layer(SDL) 是另一种可供选择的代码库.这是用C语言开发的流行的跨平台2D游戏代码库,同时也可移植到其他语言.SDL可以在www.libsdl.org获取.

第3章 游戏中的线性代数

   向量

向量表示了n维空间下的长度和方向,每个维度都用一个实数去表示

当解决向量问题的时候,你会发现在不同位置都可以绘制向量是非常有帮助的.因为改变向量绘制的位置并不会改变向量本身,这个常用的技巧要铭记在心

    加法

    减法

    长度,单位向量和正规化

向量长度等于1的时候就是单位向量.单位向量(unit vector)的符号就是上方加一顶帽子,就像这样

把非单位向量转换成单位向量,这个转换叫作正规化(normalize).

一个经验法则就是,那些只关心方向的向量,你可以将它们正规化.如果你关心方向和长度,那么向量不应该正规化

    标量乘积

    点乘

    问题举例:向量反射

    叉乘

两个向量叉乘会得到第3个向量.给定两个向量,可以确定一个平面.叉乘得到的向量就会垂直于这个平面,称之为平面的法线

因为它能得到平面的法线,所以叉乘只能在3D向量中使用.这就意味着,为了在2D向量中使用叉乘,要先通过将z分量设为0的方式转换成3D向量

值得注意的是,技术上来讲,平面的垂直向量有两个:与c反方向的向量.所以你怎么知道叉乘结果向量的朝向?结果取决于坐标系的手系

0

你可能会注意到,如果你讲将食指对准b,中指对准a,拇指的朝向就会是反方向.这是因为叉乘不满足交换律,实际上它满足反交换律:

跟点乘一样,叉乘也有要注意的特殊情况.如果叉乘返回的3个分量都为0,意味着两个向量共线,也就是在一条直线上.两个共线的向量不能确定一个平面,这就是为什么无法返回该平面的法线.

    问题举例:旋转一个2D角色

    线性插值

线性插值能够计算两个值中间的数值.举个例子,如果a = 0而且b = 10, 从a到b线性插值20%就是2.线性插值不仅作用在实数上,它能够作用在任意维度的值上.可以对点,向量,矩阵,四元数等数值进行插值.不管值的维度是什么,都能用一个公式表达:

Lerp(a, b, f) = (1 - f) * a + f * b 在公式中,a 和 b都是即将插值的变量,而f则是介于[0, 1]的因子

在游戏中,插值的常见应用就是将两个顶点插值.假设有一个角色在a点,他需要平滑地移动到b点.Lerp通过f值从0增加到1,既可做到将a平滑过渡到b点

    坐标系

有时候x轴,y轴和z轴用轴向量.轴向量用于定义坐标系

  矩阵

    加法/减法

    标量乘法

    乘法

    逆矩阵

    转置

    用矩阵变换3D向量

  相关资料

Lengyel, Eric. Mathematics for 3D Game Programming and Computer Graphics (Third Edition). Boston: Course Technology, 2012. 这本书讨论了本章很多概念的细节,有完整的计算和证明.它也涵盖了超出本书范畴的更加复杂的数学,但是对于一些游戏程序员还是很有用的(特别是专注于图形学领域的程序员).

第4章 3D图形

   基础

第一款3D游戏中的渲染是完全以软件方式实现的(即没有硬件支持).这意味着即使是画线这种基础功能都要图形程序员去完成.这套3D模型正确渲染到2D颜色缓冲的算法称为软件光栅化,大部分计算机图形学会花费大量时间在这些算法上.但是现代的计算机已经有了称之为图形处理单元(GPU)的图形硬件,这些硬件实现了绘制点,线,三角形等功能

由此,现代游戏不再需要开发实现软件光栅化了.而焦点则转变为将需要渲染的3D场景数据以正确的方式传递给显卡,一般都通过像OpenGL和DirectX这样的库完成.如果需要进一步自定义这个需求,可以编写一段运行在显卡上称之为着色器的小程序来应用传入的数据.在这些数据都传递完成之后,显卡就会将这些数据绘制在屏幕上.编写Bresenham画线算法的日子一去不复返

在3D图形中需要注意的是,经常需要计算近似值.这只是因为计算机没有足够的时间去计算真实的光照.游戏不像CG电影那样,可以花上几个小时就为了渲染一帧的画面.一个游戏通常需要每秒画30帧或者60帧,所以精确度需要在性能上做出折中.由于近似模拟而产生的显示错误称之为图形失真,没有游戏可以完全避免失真

    多边形

3D对象在计算机程序中有多种显示方法,在游戏中最广泛应用的就是通过多边形显示,更具体一点来说是三角形

为什么是三角形?首先,它们是最简单的多边形,它们可以仅用3个顶点表示.第二点就是三角形总是在一个平面上,而多个顶点的多边形则有可能在多个平面上.最后,任何3D对象都k可以简单地用细分三角面表示,且不会l留下漏洞或者进行变形

单个模型,我们称为网格,是由多个三角片组成

  坐标系

一个坐标系空间有不同的参考系.比如说,在笛卡尔坐标系中,原点在世界的中间,所有坐标都相对于中心点.与之类似,还有很多坐标系有不同的原点.在3D渲染管线中,渲染3D模型到2D显示器,必须经历4个主要的坐标系空间

  模型坐标系/局部坐标系

  世界坐标系

  视角坐标系/摄像机坐标系

  投影坐标系

    模型坐标系

当我们在建模的时候,比如像在Maya这样的软件里面,所有模型顶点的表示都是相对于模型原点的.模型坐标系就是那个相对于模型自身的坐标系.在模型坐标系中,原点通常就在模型中心,角色模型的原点在角色两脚之间.这是因为对象的中心点会更好处理

现在假设游戏场景中有100个不同的对象.如果游戏只是加载它们然后以模型坐标系绘制会发生什么?由于所有对象都在模型空间创建,所有对象,包括玩家,都会在原点.相信这样的关卡会很无趣.为了让这个关卡加载正确,需要另一个坐标系

    世界坐标系

有一个新得坐标系称为世界坐标系.在世界坐标系中,所有对象都相对于世界的原点偏移

就像之前说过的,经常会有3D游戏使用4D向量.当4D坐标系应用在3D空间中时,它们被称为齐次坐标,而第4个分量被称为w分量

在大多数情况下,w分量要么是0,要么是1.如果w=0,表示这个齐次坐标是3D向量.而w=1,则表示齐次坐标是3D的点.但很容易让人疑惑的是,Vector4类同时用于表示向量和顶点.因此,通过命名规范来保持语义是很重要的

Vector4 playerPosition  // 这是个点
Vector4 playerFacing  // 这是个向量

用于变换的矩阵通常是4 x 4矩阵.为了与4 x 4矩阵相乘,同时也需要4D向量.

矩阵变换就是矩阵用某种方法来影响向量或者顶点.矩阵变换使得我们可以将模型坐标系变换为世界坐标系

WorldTransform = Scale x Rotation x Translation

    视角/摄像机坐标系

在所有对象放置到世界坐标系上正确的位置之后,下一件要考虑的事情就是摄像机的位置.一个场景或者关卡可以完全静止,但是如果摄像机的位置改变,就完全改变了屏幕上的显示.这个称为视角/摄像机坐标系

所以还需要另外一个矩阵告诉显卡如何将世界坐标系的模型变换到相对于摄像机的位置上.最常见的矩阵是观察矩阵.在观察矩阵当中,摄像机的位置通过3个轴的额外分量来表示

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值