现在的位置: 首页 > 综合 > 正文

Flash Stage3D 学习笔记(二)

2017年12月24日 ⁄ 综合 ⁄ 共 7050字 ⁄ 字号 评论关闭

本文参考来源:

http://www.adobe.com/devnet/flashplayer/articles/hello-triangle.html

http://www.adobe.com/devnet/flashplayer/articles/what-is-agal.html

http://www.adobe.com/devnet/flashplayer/articles/vertex-fragment-shaders.html


1.重温GPU渲染过程

上一篇文章初略的描述了GPU管道渲染过程,现在我们重新温习一下。


图1 GPU可编程管道渲染过程

将顶点流(Vertex Stream)输入顶点着色器,经过矩阵变换等处理后,开始三角形组合,接着进入视图窗口进行裁剪,获得当前显示的范围数据,接着开始栅格化处理,之后进入片段着色器开始材质纹理或颜色的渲染,最后形成一帧帧图像缓冲并输出到屏幕显示。

这里的第2步,第3步,第4步和第6步将被渲染管道自动处理,而需要数据输入的则是顶点着色器VertexShader和片段着色器FragmentShader。于此同时,Stage3D也将这两个着色器开放给ActionScript程序控制。因此VertexShader和FragmentShader一直贯穿于Stage3D的整个过程中,构成了3D API的程序基石。换句话来讲,我们必须要时刻将这两个着色器牢记在心。

VertexShader:顶点着色器,最基本的功能是用来定位顶点,当我们旋转,平移,扭曲或做任何其他的3D对象操作时,顶点在场景中的输出位置始终由顶点着色器完成处理,因此这个着色器一直在做大量的矩阵变换的运算。

FragmentShader:片段着色器,最基本的功能是用来进行颜色或纹理的渲染,换句话来说,就是计算每个三角形面上的每一点对应的颜色,一旦3D对象在场景中的所有点的颜色全部算出,整个图像也就得到了。对三角形面的栅格化处理,对颜色的渐变处理等,都会涉及到对原始顶点的位置和颜色的插值处理,因此这个着色器一直在做大量的“扫描”工作。


2.神秘的AGAL语言

我们先来看一个官方的Stage3D的“Hello World”例子,链接地址为:

http://www.adobe.com/devnet/flashplayer/articles/hello-triangle.html

在阅读源码的时候,在其他类似的案例教程中,这样的代码随处可见,

var vertexShaderAssembler : AGALMiniAssembler = new AGALMiniAssembler();
vertexShaderAssembler.assemble( Context3DProgramType.VERTEX,
"m44 op, va0, vc0\n" + // pos to clipspace
"mov v0, va1" // copy color
);

这看起来很像汇编语言,不是吗?没错,它们正是汇编语言,这是由Adobe专门开发的调用GPU的汇编语言AGAL(Adobe Graphics Assembly Language)。看起来很神秘吗?有点,毕竟在ActionScript语言中写上这样一段代码,读起来是相当的纠结。汇编语言难吗?就单行单行的代码来看,不难,无非就是几个寄存器之间的数据在进行运算;但是如果要完成一段独立完整的功能的汇编代码,轮到要写AGAL汇编代码了,我表示鸭梨很大。


在继续深入讨论之前,我不得不提醒读者,您需要激活这样一些概念知识。

(1)了解xyz坐标系和uv坐标系,特别是uv坐标,用于材质贴图的定位,不清楚可以搜索一下。我这里简单的说一下,uv贴图就跟我们设置桌面壁纸很相似,我们是左上对齐还是居中对齐,我们是平铺还是拉伸,换句话来说,设置壁纸就是材质贴图,不是吗?

(2)弄清什么是点乘,什么是叉乘,在AGAL的教程中,点乘(dot product)和叉乘(cross product)经常会一步小心就冒出来,因为图形渲染中涉及到大量的矩阵运算,所以我们需要将大学中线性代数捡起来,怎么进行矩阵相乘,怎样计算行列式的值,这些需要唤醒记忆,唤不醒的就去搜索一下。简单的说一下,点乘就是坐标点的同维度的乘积求和,比如点P1(x1, y1, z1)与点P2(x2,
y2, z2)的点乘就是x1*x2+y1*y2+z1*z2。叉乘则是两个矢量构成平面的垂直乘积,想想电磁感应中的右手法则,具体则是两点与左边维度构成的矩阵对应的行列式的值,公式不方便打出来,请搜索一下。

(3)为什么要用到矩阵乘法,对一个物体进行旋转,物体上原来每点的位置经过旋转之后的新位置,怎么算?具体推导就是一个与旋转角度相关的矩阵变换,具体公式请搜索。同样平移,扭曲,拉伸,无一不是矩阵变换,无一不是矩阵乘法,GPU所做的大量工作离不开矩阵运算,而且往往是硬件电路来做这样的运算,比起软件做矩阵运算,速度当然提高不少。


涉及到汇编,一个是操作码,另一个则是寄存器。操作码就是加减乘除等运算,在汇编中是add,sub,mul,div等,寄存器是用来临时存取数据的最小单元,数量多,类型杂,不好记。我这里抄写一下参考文献里面的东西,AGAL汇编指令的基本格式是这样的,

<opcode> <destination>, <source 1>, <source 2 or sampler>

比如

add v0, va0, va1

就是va0寄存器里面的数据加上va1里面的数据,结果存放到v0寄存器,即:

v0 = va0 + va1

就是这么个意思,明白了吧。

AGAL常用指令(也是原封不动抄来的):

  • mov: moves data from source1 to destination, component-wise
  • add: destination = source1 + source2, component-wise
  • sub: destination = source1 – source2, component-wise
  • mul: destination = source1 * source2, component-wise
  • div: destination = source1 / source2, component-wise
  • dp3: dot product (3 components) between source1 and source2
  • dp4: dot product (4 components) between source1 and source2
  • m44: multiplication between 4 components vector in source1 and 4×4 matrix in source2
  • tex: texture sample. Load from texture at source2 at coordinates source1.

看到这里,dot product冒出来了,点乘啊。component-wise,什么意思?clockwise,顺时针,这里顺着“组件(component)”,这是什么意思?感觉怪怪的,一会再做解释,OK?还有一大堆汇编指令,请参考这里面的两张大图,静下心来看,大部分可以看得懂,不难,哦也~

http://www.adobe.com/devnet/flashplayer/articles/what-is-agal.html


再回到GPU中的寄存器,一大堆,总共分了六大类,这里不想抄原文了,我自己画了张图,再按图说话,也许能帮助看官您更好的消化理解。

图2 GPU寄存器与Stage3D初始化数据API

从左上角看起,每个寄存器为128比特位,按每32位分成4段,每一段因为是32比特位,刚好是一个单精度浮点数的尺寸,因此可以放一个小数进去。这里的每一段,被称为一个component,翻译成“组”比“组件”要妥当一点,当然聪明的您会想到更好的翻译。每一组可以塞进一个小数,4个组刚好可以放进x,y,z,w是个数,也就是可以表示一个坐标点P(x, y, z),这里w往往用不到。同样也可以放入一个颜色值rgba,rgb是三原色,a是指通道透明值。也就是说一个寄存器,分成了四段后,刚好可以放一个坐标位置,也可以放一个颜色值,这两样正是显卡所需要的东西。换句话来讲,一个寄存器设计成128位的大小真是好!

一个寄存器里面放点P1(x1, y1, z1),另一个寄存器里面放点P2(x2, y2, z2),这样两个寄存器如果做加法,得到第3个点P3就是(x1+x2, y1+y2, z1+z2),这就是component-wise的意思,也就是说按分段来进行运算,各得其所。如果不按分段运算,结果会怎样?会进位,z1+z2会进位给y1+y2,那就完蛋了。

接下来再看下面的图,看到“vc x128”了,这是什么意思?看右上角的解释,这里的vc不是风险投资,vc是常量寄存器,constant嘛,常量啊。“x128”表示数量128个,也就是说这种常量寄存器有128个,其他地方的数字都是这个意思,表示寄存器的具体个数。再看右边还有一个“fc x28”,表示还有28个常量寄存器。看虚线框,左边的这128个是给顶点着色器用的,右边的这28个是给片段着色器用的。常量寄存器是干什么用的呢?用来放一个常量,0啊,1啊,你想放什么数字都行,但是这些寄存器通常会放一个矩阵或者向量。

“va x8”,表示8个属性(attribute)寄存器,什么属性呢?就是坐标点位置和颜色值。va被划分到顶点着色器的框框里面了,只给顶点着色器用的。片段着色器难道不需要属性数据吗?当然需要,但是它是从“v x8”中拿到属性数据的,v是变量寄存器,英文叫Verying Register,翻译成变量寄存器并不是十分的准确,但是大家都这样翻,我也随大流了,总而言之,变量寄存器v是专门用来从va属性寄存器取数据,传给片段着色器使用。

“fs xn”,表示纹理采样寄存器,n表示数目不详,我确实不知道有多少个,看官有清楚的不妨告诉我,呵呵。纹理寄存器,专门用于放置贴图纹理的信息数据。具体的参数这里不详细阐述了,比如2D还是3D方式贴图,是平铺不平铺,等等。

再看下面蓝色的一层,t表示临时寄存器(Temporary Register),vt表示给VertexShader用的,ft表示给FragmentShader用的,都是用来放计算中的中间结果,因此临时寄存器可以当做变量来使用,各8个。

最底下一层是输出寄存器,都只有一个,左边的op是用来输出位置(position)的,右边的oc是用来输出颜色值(color)的。不管程序千算万算,多少波折,最终结果只有两个,一个是位置,一个是颜色,这两个属性对于渲染图像而言足矣,难道我们要的不是这两个东西吗?实话实说,我们输入进来的也是这两个东西,顶点和纹理,但是多了一个常量,用来做运算的。进来是这两样东西,出去还是这两样东西,中间却进行了大量的运算。

把这个图摆出来,还特意画了两个框框,把这些寄存器人为的划分成两部分,强行拨给顶点着色器和片段着色器,对吗?不对,只是为了在写AGAL时流程清楚一些,事实上,这里除了输出寄存器,其余的寄存器相互之间都可以参与运算,顶点着色器所属的vt临时寄存器,难道不能跟片段着色器的v变量寄存器一起参与运算吗?当然可以啊,程序想怎么写就怎么写,只要不死机就行。这么多寄存器摆在那里,再加上一大堆操作指令,特别是矩阵运算,作为编程人员来讲,实在是太灵活了,再复杂的渲染算法,再曲折的运算过程,都可以实现出来,这就是AGAL的力量!


3.Stage3D API

从图2可以看到,参与AGAL运算的过程首先是将数据传给属性寄存器va、纹理寄存器fs、常量寄存器vc这三个寄存器,这是第一步,数据的输入。等到数据都传给了寄存器,接下来就要调用AGAL汇编语言进行运算,这就需要给GPU传递指令了,因为GPU是硬件,只能认识机器码,因此需要将AGAL汇编语言编译成目标语言,Adobe提供了这样一个类AGALMiniAssembler.as来完成这个编译过程。最后GPU将输入到寄存器中的数据,根据汇编指令(准确的说是机器码)进行运算,得到输出结果,然后存放在输出寄存器op和oc中。根据三角形面的渲染,GPU会逐点计算面上每个像素的颜色值,然后将所有运算好的像素点进行缓冲,形成一帧图像,此时即可渲染输出整个图像到显示器了。下一帧继续重复以上的全部过程,通过改变va,fs,vc中的数据,重新得到一个新的图像,这相当于逐帧渲染。


(1)数据初始化

在将数据传给寄存器之前,需要先产生和获得数据,也就是数据的初始化。根据三种输入寄存器,我们同样需要初始化三种数据:属性数据(即Vertex,包含顶点位置和颜色值/uv材质坐标),纹理数据(即Texture,包括纹理图像,贴图方式等),常量数据(即Constants,参与运算的参量,包括数字,向量,矩阵等数据)。现在我们来看对应的Stage3D的API,然后解释一下“hello
triangle”中的代码。注意大部分的API都是由Context3D这个类完成的,下面直接用类名引用只是为了习惯,不要当成静态方法!

属性数据初始化:Context3D.createVertexBuffer()

纹理数据初始化:Context3D.createTexture()

常量数据初始化:我们自己任意定义一个ByteArray,Vector,或Matrix


永远要记住的是,GPU是以三角形为单元进行渲染的,比如有一个3D几何体被分成了10个三角形的面,每个三角形都有三个顶点,因为渲染三角形有个先后顺序,这样这些顶点就会以渲染的顺序形成一个队列,GPU会以三个为一单元进行组合,所以这个顶点队列的元素个数必须是3的倍数。因此也需要对顶点队列进行初始化,

顶点索引数据初始化:Context3D.createIndexBuffer()

在初始化数据的同时,可以将属性数据和索引数据输入到GPU缓冲区中存储起来

缓冲顶点属性数据:VertexBuffer3D.uploadFromXXX()

缓冲顶点索引数据:IndexBuffer3D.uploadFromXXX()


(2)输入寄存器

参看图2,

输入属性寄存器va:Context3D.setVertexBufferAt()

输入纹理寄存器fs:Context3D.setTextureAt()

输入常量寄存器vc:Context3D.setProgramConstantsFromXXX()

输入常量寄存器稍微有点复杂,输入的类型可以是ByteArray,Vector和Matrix;同时要根据programType来区别输入给顶点着色器或片段着色器。


(3)运算指令

分别写好输入给顶点着色器和片段着色器的汇编代码,然后利用AGALMiniAssembler.as编译成字节码,

程序初始化:Context3D.createProgram()

获取指令:Program3D.upload(vertexProgram, fragmentProgram)

缓冲程序至GPU:Context3D.setProgram(Program3D)


4.Stage3D渲染流程

上面讲的只是一个基本的过程,实际的渲染流程会多出来一些。

(1)数据初始化:包括属性、纹理、常量、顶点、程序等,同时上传属性队列、顶点队列、程序到GPU缓冲区,参考3.1和3.3部分

(2)场景初始化:Context3D.configureBackBuffer(),设置待渲染的视图区域的宽度和高度,渲染质量等


以上的初始化可以一次性完成,也可以在每次渲染之前重新设置,下面是每次渲染需要做的动作,

(3)清空场景:Context3D.clear()

(4)设置呈现状态:将属性、纹理、常量等数据输入寄存器,为GPU运算做准备,参考3.2部分

(5)启动渲染:Context3D.drawTriangles(),根据顶点队列一次画一组三角形,如果有多组顶点,则可以反复调用此方法

(6)显示图像:Context3D.present()

在逐帧渲染时,一般会重复上面的第3步~第6步。在第5步的渲染过程中,是逐个三角形依次渲染,每画一组三角形,都可以设置不同寄存器输入参数,这非常类似我们在Graphics中一系列的绘图中,反复不断的设置笔触和填充,反复不断的移动绘点位置。也就是第4步和第5步的重复。


关于渲染流程,看看官方帮助文档是怎么说的,

若要呈现并显示某个场景(在获取 Context3D 对象后),下面是典型的步骤:

  1. 通过调用 configureBackBuffer() 来配置主显示缓冲区属性。
  2. 创建并初始化您的呈现资源,包括:
    • 定义场景几何的顶点和索引缓冲区
    • 用于呈现场景的顶点和像素程序(着色器)
    • 纹理
  3. 呈现帧:
    • 为场景中的一个对象或一组对象设置适当的呈现状态。
    • 调用 drawTriangles() 方法可以呈现一组三角形。
    • 更改下一组对象的呈现状态。
    • 调用 drawTriangles() 可以绘制定义对象的三角形。
    • 重复直至场景全部呈现。
    • 调用 present() 方法可以在舞台上显示呈现的场景。

最后请读者自己去看一下“hello triangle”中的代码示例http://www.adobe.com/devnet/flashplayer/articles/hello-triangle.html,这里就不贴出来了。


抱歉!评论已关闭.