Stage3D大冒险
——《Stage3D Game Programming Beginner’s Guide》阅读笔记
Molehill是什么?
Molehill是Flash Stage3D技术的开发代号(现在正式命名Stage3D)。它能让下一代Flash游戏通过显卡GPU硬件加速去渲染3D图形(或成千上万个“精灵”),从而降低CPU负载。
Molehill不是什么?
Molehill是非常低层的API,只提供基本的3D渲染功能。它不是一个游戏引擎(如Unity3d或者Unreal又或者ID Tech),一个游戏引擎还包含物理模拟,音频,音乐,碰撞检测等等补充的游戏函数。取而代之的是,你可使用Molehill这样的低层API去制作你自己的游戏引擎。与OpenGL或Direct3D相似,Molehill是一些非常基本的函数集合,是游戏和游戏引擎的基本构件。
Game Start!
基本3D术语
常见3D内容名词
Mesh 网格
3D模型在游戏里通常称为网格。这些网格是用3D建模软件(Maya,3DsMax,Blender等)制作。定义一物体的三维形状,通常也会给它上色。
Polygon 多边形
一个网格可以由成千上万个多边形组成。任何多边形都能分解成一个三角形的集合,所以显卡会将3D几何图形当成一大堆三角形去处理。(Triangle 三角形, Quad 四边形, Polys 多边形数, Tris 三角形数)
Vertex 顶点
一个多边形由三个或多个“角”的位置决定的,这些“角”叫做顶点。比如一个四方形有4个顶点,一个3D盒有6个面,8个顶点。
Texture 材质/纹理
为了绘制一个网格(除非你只要渲染线框),你需要一张或多张图片去上色,这些图片叫做材质(或纹理)。可以将它们想象成墙纸:贴材质到网格上,将网格包起来,而且图片可以拉伸去适应网格形状。(俗称“贴图”)
Shaders 着色器
着色器是用来定义视觉风格细节的,定义“如何画”一些东西。比如某些着色器可能用于渲染亮金属的,又比如有些用来表现爆炸的。
在Stage3D的API中,着色器保存在Program3D类里面,为了创建program3D你需要创建一个fragment program和一个vertex program,两个加起来就是一个Shader(着色器)。
Vertex Program 顶点程序
每渲染一次,顶点程序会对网格各个顶点运算一次,然后在渲染之前得出“最终位置”。除了计算位置可以附带其他信息比如颜色。凡是渲染网格都需要一个顶点程序,无论是非常简单的(按原位置绘制每一个顶点),还是复杂的(将整个网格变形去完成一个步行循环动画)。
Fragment Program 片元程序
在Molehill中,片元程序是用来定义怎么处理一个特定的网格的视觉渲染。描述在给定一个表面/材质对光的反应。不同的物质对光的反应不一样(取决于物体的反射率和透明度),你需要用不同的代码去模拟它们。片元程序通常使用材质通过复杂的函数去决定屏幕上每个像素显示什么颜色,有时候会为了合成特效使用一个以上的材质。渲染网格都要需要一个片元程序,可能是简单的(只用材质原本)或者复杂的(混合两个材质并染成半透明泛光的红色)。
Level up!
常见3D编程名词
Vector 向量/矢量
Vector3D是一个含有x,y,z值的对象,用来描述3D空间里面的位置。
Normal 标准化向量/单位向量/法向量/法线
标准化向量是一种通常用来描述方向的特殊向量,长度总是等于1,所以也称为单位向量。
Matrix 矩阵
如果要只通过一步就对某个对象完成移动、旋转、缩放操作,就需要用到矩阵。Matrix3D是一个4×4矩阵,内建了平移、旋转、缩放函数。
Level up!
总结
Vector3D: 含有x, y, z的组件
Normal: 一个长度为1的Vector3D
Matrix: 一个4×4的表示位置、旋转和缩放的向量组
Vertex: 表示多边形的一个“角”的空间中的一点
Polygon: 一个由多个顶点坐标定义的形状
Mesh: 一组多边形,组成一个3D模型
Texture: 一个位图图像,像是Mesh的壁纸
Shader: vertex program与fragment program混合而成结果
Vertex program: 影响Mesh形状的一组命令
Fragment program: 影响Mesh外观的一组命令
Level 2 Molehill的蓝图
Molehill应用的结构
Stage
Stage3D
Context3D
VertexBuffer3D
IndexBuffer3D
Program3D
Molehill程序流程图
只需设定一次的东西
1. 从Stage3D中请求一个Context3D实例
2. 设置Context3D (大小是多少之类)
3. 创建一个VertexBuffer (每一个Mesh分配一个)
4. 创建一个IndexBuffer (每一个对应一个VertexBuffer)
5. 上传这些数据到Context3D (即是传到GPU)
6. “编译”一个VertexProgram (将你的着色器代码转换成字节码)
7. “编译”一个FragmentProgram (将你的着色器代码转换成字节码)
8. 创建一个将要使用上面提到的着色器的Program3D (可以被一个以上的Mesh使用)
需要反复做的东西
1. 清理Context3D (擦除旧视图)
2. 告诉Context3D用哪一个Program3D(比如”rusty metal”着色器)
3. 告诉Context3D用哪一个VertexBuffer(例如, 一个宇宙飞船)
4. 设置你的着色器数据 (一些变量, 比如你的飞船的新位置)
5. 绘制一些三角形(告诉Context3D绘制一些东西)
6. 更新屏幕(给用户展现刚才渲染的新视图)
Level 3 (1st Boss Battle) 启动引擎!
代码见附件:BasicQuad.as
Level 4 基本着色器: 能看见一些东西了!
一行AGAL是什么样子的?
<opcode> <destination> <source1> <source2>
每一行代码,第一个位置叫做opcode,表示用什么命令或函数;
第二个位置叫做destination,表示结果输出;
接着的是源数据列表source1, source2, …——即是命令执行时的参数。
例如:
mov v0, va1
意思就是va1的值复制到v0
AGAL大概只有30种opcode,这里有一些比较普通的:
mov (将source1的数据复制到destination,)
add (destination = source1 + source2)
sub (destination = source1 – source2)
mul (destination = source1 * source2)
div (destination = source1 / source2)
还有其他更复杂的命令,用于游戏编程常用的复杂计算,比如点积和矩阵乘法。
寄存器是什么?
上面例子提到的source和destination就叫寄存器。你可以把他们看做变量、用来储存数据的地址。因为AGAL对寄存器有一定限制,所以制作复杂的着色器的时候需要重用寄存器。
分量(component)是什么?
在寄存器里面的四个值各自叫做分量。你可以通过x,y,z,w或者r,g,b,a访问每一个分量。比如va1.x就引用va1的第一个分量,va1.r也如此。至于用哪一组名字随你喜好,通常好的习惯是在顶点程序中用x,y,z,w,在片元程序中用r,g,b,a。
四个分量是同时运算的。比如add操作,如果source1是Vector3D值为(10,100,50,0)而source2为(5,1,3,0),destination则等于(15,101,53,0)。
不同的寄存器做不同的工作
在AGAL代码里面,寄存器是通过由2个字母组成的关键字来引用的,若有多个,后面接数字。例如,有8个va寄存器,命名为va0, va1, va2, va3, va4, va5, va6, va7。
顶点程序用到的寄存器
va = Vertex Attribute
vc = Vertex Constant
vt = Vertex Temporary
op = Vertex Output Position
片元程序用到的寄存器
fc = Fragment Constant
ft = Fragment Temporary
fs = Fragment Texture Sampler
oc = Fragment Output Color
顶点属性寄存器(Vertex Attribute registers): va0..va7
这些寄存器关联到VertexBuffer里的指定位置,只给顶点着色器用。当上传网格数据时,除了可以包含x,y,z坐标之外,通常还可以包含材质uv坐标,法线,rgb颜色和其它数据。分配VertexBuffer到指定属性寄存器,可用Context3D.setVertexBufferAt()函数。
语法:
va<n>, 这里<n>就是属性寄存器的索引号。
常量寄存器(Constant registers):vc0..vc127 与 fc0..fc27
这些寄存器为从ActionScript传参到着色器而服务。常量寄存器不存储在VertexBuffer中,他们被顶点和片元共享。可以把他们当成“全局变量”。通过Context3D.setProgramConstants()函数。
语法:
vc<n>, 用于顶点着色器;fc<n>, 用于片元着色器。
临时寄存器(Temporary registers):vt0..vt7 与 ft0..ft7
这些寄存器用于临时计算。计算每一个顶点之前他们是未初始化的,所以你可以把他们当成局部的、临时变量。
语法:
vt<n>,用于顶点着色器;ft<n>, 用于片元着色器。
输出寄存器(Output registers):op 与 oc
op输出位置,oc输出颜色,是顶点着色器和片元着色器储存计算的最终结果的地方。
变化寄存器(Varying registers):v0..v7
这些寄存器是用于从顶点着色器向片元着色器传递数据的。被传递的数据会被GPU适当地插值。
例如,在顶点程序里面,你可能会以与摄像机的距离去计算每个顶点的颜色,以达到模拟雾的效果。通过向片元着色器传值(各顶点各一个),片元着色器会对任何一个多变形的三个顶点中的三个颜色进行插值,使得一个颜色到另一个颜色平滑过渡。
这里的重点要记住的是变化寄存器可以返回稍微不同的值给屏幕上各个像素:片元程序每一次迭代,它们会一个接一个地平滑插值。片元着色器接收到的值,是由组成三角形的三个顶点之间插值得来的。
材质取样器(Texture samplers):fs0..fs7
8个材质取样器寄存器是用于提取材质的颜色,基于UV坐标。只用于片元程序。材质通过ActionScript调用Context3D.setTextureAt()来定义。
例如,一个标准的2D材质不带mip mapping和linear filtering的可以取样到临时寄存器ft1,假设变化寄存器v0存有材质UV的插值,那么AGAL代码如下:
tex ft1, v0, fs0 <2d,linear,nomip>
语法:
fs<n> <flag>, <n>是取样器的索引,<flag>是定义怎样取样的一些标记,由逗号隔开的字符串,定义如下:
1. 材质维度(texture dimension): 2d, 3d, cube.
2D材质是最常用的,方形位图;
3D材质较少用,有长度、宽度和深度,占用很多材质内存,但对木纹、粗糙面、大理石等非常棒;
cube材质是由六个方形位图编成一组的,通常用于反射,可以指定贴在盒子内面。
2. MIP映射法(mip mapping): nomip, mipone, mipnearest, miplinear.
如果材质有MipMap,可以让Stage3D通过它们平滑地插值,提高渲染质量而避免“摩尔纹现象”。
3. 材质过滤(texture filtering): nearest, linear.
If you prefer a retro, then pixilated look you can use nearest here to tell Stage3D not to interpolate the values smoothly, resulting in blocky textures when viewed up close, for example.
If you use linear, when zoomed into a texture it will be blurry and will have smoother edges.
4. 材质重复(texture repeat): repeat, wrap, clamp.
If you want the texture tile properly, then you use repeat here.
If you are not tiling the texture and run into problems with edge pixels being the color of the opposite edge (which often happens in transparent billboard particles and when rendering bitmap fonts in 3d), then specify clamp to force Stage3D not to blur adjacent edge pixels.
一个基本的AGAL着色器例子
一个着色器是由一个顶点程序和片元程序组成的,存放于program3D对象里。
简单的顶点程序
// the simplest possible vertex program
m44 op, va0, vc0 // pos to clipspace
mov v0, va1 // copy uv
步骤:
1. 改变每一个顶点的坐标以符合当前摄像机角度
2. 复制这个值到”output position”寄存器
3. 将UV材质坐标传到片元程序
简单的片元程序
// a simple fragment program
tex ft1, v0, fs0 <2d,linear,nomip>
mov oc, ft1
步骤:
1. 使用顶点程序输出来的UV坐标对材质进行取样
2. 复制这些颜色值到”output color”寄存器
编译AGAL代码
vertexShaderAssembler. assemble(
Context3DProgramType. VERTEX,
"m44 op, va0, vc0\n " +
"mov v0, va1"
) ;
var fragmentShaderAssembler: AGALMiniAssembler = new AGALMiniAssembler( ) ;
fragmentShaderAssembler. assemble(
Context3DProgramType. FRAGMENT,
"tex ft1, v0, fs0 <2d,linear, nomip>;\n " +
"mov oc, ft1"
) ;
var program: Program3D = context3D. createProgram( ) ;
program. upload (
vertexShaderAssembler. agalcode,
fragmentShaderAssembler. agalcode
) ;
为了编译AGAL源代码,只要简单地:
1. 创建一个新的AGALMiniAssembler对象
2. 告诉它编译你的AGAL源码
3. 合并编译后的顶点程序和片元程序生成一个着色器(shader)
4. 渲染过程中上传这个东西
Time to Render!
你只需有游戏初始化时编译你的AGAL代码一次。之后,你只需简单地指示Stage3D API你想要给哪一个网格使用哪一个着色器程序。为此,做到一下步骤:
1. 清空Context3D,擦除上一帧
2. 选择使用哪一个VertexBuffer
3. 选择使用哪一个材质
4. 设定当前着色器程序
5. 在着色器里应用一个矩阵去计算定位
6. 渲染网格
7. 对每一个网格重复2到6步
8. 呈现最终图像给用户
代码见附件:BasicShader.as
Level up!