stage3D 搭建2d图形引擎 (三)透明度(alpha混合)
目前为止我们绘制的显示对象的颜色都还是没有考虑透明度的,为了给我们的显示对象增加透明度这一特性,我们需要深入了解透明度的本质。
所谓的透明度并不是真实世界中的那种透明,真实世界中的透明是物体发射或者反射的光线透过了物体和我们之间的障碍物,从而我们能够看见障碍物另一侧的物体。但是计算机世界中一切的显示都是虚拟的,所以不存在光线透过的问题。计算机图形学中的透明度实际上是通过两者的颜色混合而得到的一种模拟,参见alpha混合。具体的原理这里不多做解释,但是需要强调几点:
1.alpha混合的操作对象数量为2,需要明确区分两者,可以通俗的称他们为背景层与前景层,但在很多技术文档中,他们的名称更加晦涩,比如输入(source)颜色和输出(destination)颜色。
2.混合公式为Cresult=Csource × alphasource + Cdestination × alphadestination(1-alphasource);
在stage3D中我们通过
1 Context3D::setBlendFactors(sourceFactor:String, destinationFactor:String):void
这个方法来设置混合模式。这个方法需要两个参数sourceFactor,destinationFactor源因子和目标因子,可选值为Context3DBlendFactor类中的枚举常量。可以任意搭配这些枚举常量,事实上,通过一些不同的搭配我们能够设计出多种混合模式,但在这里我们只对一种混合模式感兴趣,那就是alpha混合。
为了得到alpha混合,需要将两个参数分别设置为Context3DBlendFactor.SOURCE_ALPHA,Context3DBlendFactor.ONE_MINUS_SOURCE_ALPHA。但仅仅是这些还是不够的,我们前面说,alpha混合有两个操作对象,前景层或者背景层抑或源颜色与目标颜色注。在计算机内部目标颜色是缓存中已经存在的颜色,而源颜色则是刚刚被计算出来的颜色还没有到达缓存中,虽然被计算出来的时间是有先后的,但是最终呈现在计算机上的颜色并不一定会遵循这种先后,即后被绘制的颜色不一定能够覆盖先被绘制的颜色(虽然似乎应该这样),这里面还关系到另外一种计算,即深度测试,深度测试是根据z坐标的大小关系来决定是丢弃新颜色还是旧颜色。在stage3D中通过接口:
1 Context3D::setDepthTest(depthMask:Boolean, passCompareMode:String):void
来设置深度比较的类型,如果第一个参数为true并且,通过第二个参数设置的比较类型的比较结果为false时,那么源颜色将被丢弃,如果为true则进行后续的像素处理(这其中就包括颜色混合)。我们的研究的问题是2d的,为了将问题限定在这个范围内,我们将z坐标都处理成统一的值:0。理论上只要我们设置的比较类型中含有equal(less_equal,equal,greater_equal),那么比较结果就会为true,但事实上只有less_equal才是有效的,对这个问题我们之后会持续的研究。
当然还有一种更容易理解的简单方式,那就是关闭深度测试:
1 Context3D::configureBackBuffer(width:int, height:int, antiAlias:int, enableDepthAndStencil:Boolean = true, wantsBestResolution:Boolean = false):void
将第四个参数设置为false就同时关闭了深度和印模测试(模板测试)。
问题并没有结束,深度测试我们基本解决了,但是如何规定绘制的先后顺序呢?
虽然目前我们还没有引入显示列表的概念,但是一旦引入,我们如何保证显示列表上层的显示对象能够盖在下层的现实对象之上呢?事实上这就需要我们深入了解
1 Context3D::drawTriangles(indexBuffer:IndexBuffer3D, firstIndex:int = 0, numTriangles:int = -1):void
这一接口背后的东西。虽然这个接口执行一次就可以画出大量的三角形,但是在其内部这些三角形并不是同时被绘制的,实际上越靠近indexBuffer前部,三角形将越先被绘制。因此为了最终达到我们需要的alpha混合效果,需要将“底部”的显示对象的顶点放在“顶部”的显示对象的顶点之后。
下面我们给出修改后的类:
首先是QuadVertex:
package psw2d { import flash.geom.Matrix; public class QuadVertex { public static const ELEMENTS_PER_VERTEX:int = 7; private var _rawData:Vector.<Number>; public function QuadVertex() { _rawData = new Vector.<Number>(4*ELEMENTS_PER_VERTEX,true); } public function setPosition(index:uint,x:Number,y:uint):void { var offset:int = ELEMENTS_PER_VERTEX * index; _rawData[offset] = x; _rawData[offset+1] = y; _rawData[offset+2] = 0; } public function setColor(index:uint,color:uint):void { var offset:int = ELEMENTS_PER_VERTEX * index; var alpha:Number = (color >> 24 ) & 0xFF; var r:Number = (color >> 16) & 0xFF var g:Number = (color >> 8) & 0xFF; var b:Number = (color & 0xFF); r/=0xFF; g/=0xFF; b/=0xFF; alpha/=0xFF; _rawData[offset+3] = r; _rawData[offset+4] = g; _rawData[offset+5] = b; _rawData[offset+6] = alpha; } public function get rawData():Vector.<Number> { return _rawData; } public function set rawData(value:Vector.<Number>):void { _rawData = value; } public function transformVertex(modelMatix:Matrix):void { var x:Number,y:Number; for(var i:int=0; i<4; ++i) { x = _rawData[i*ELEMENTS_PER_VERTEX]; y = _rawData[i*ELEMENTS_PER_VERTEX+1]; _rawData[i*ELEMENTS_PER_VERTEX] = modelMatix.a * x + modelMatix.c * y + modelMatix.tx; _rawData[i*ELEMENTS_PER_VERTEX+1] = modelMatix.b * x + modelMatix.d * y + modelMatix.ty; } } public function copyTo(target:QuadVertex):void { for(var i:uint;i<_rawData.length;++i) { target.rawData[i] = _rawData[i]; } } } }
改动的部分已经突出显示,首先是顶点数据的个数增加到了7位,新增加的一位是给alpha值得,由于增加了alpha通道,相关的一些算法也要改变。值得注意的是alpha通道是加在rgb之后的,这同他在颜色值中的位置恰好相反。
在QuadRender中改动很小,主要在两个方法中:
1 public function rebuildBuffer():void 2 { 3 _vertexBuffer && _vertexBuffer.dispose(); 4 _indexBuffer && _indexBuffer.dispose(); 5 var numQuads:uint = _quads.length; 6 if(!numQuads) return; 7 var vertexData:Vector.<Number>=new Vector.<Number>(); 8 var indexData:Vector.<uint>=new Vector.<uint>(); 9 _vertexData = new QuadVertex(); 10 for(var i:int=0;i<numQuads;++i) 11 { 12 _quads[i].vertexData.copyTo(_vertexData) 13 _vertexData.transformVertex(_quads[i].modelMatrix); 14 vertexData = vertexData.concat(_vertexData.rawData); 15 indexData.push(i*4+0,i*4+1,i*4+2,i*4+0,i*4+2,i*4+3); 16 } 17 _vertexBuffer = _context3D.createVertexBuffer(numQuads * 4,QuadVertex.ELEMENTS_PER_VERTEX); 18 _indexBuffer = _context3D.createIndexBuffer(numQuads * 6); 19 20 _vertexBuffer.uploadFromVector(vertexData,0,numQuads * 4); 21 _indexBuffer.uploadFromVector(indexData,0,indexData.length); 22 23 _context3D.setVertexBufferAt(0,_vertexBuffer,0,Context3DVertexBufferFormat.FLOAT_3); 24 _context3D.setVertexBufferAt(1,_vertexBuffer,3,Context3DVertexBufferFormat.FLOAT_4); 25 }
1 public function setProgram():void 2 { 3 var vertexSrc:String = "m44 op,va0,vc0 \n" + 4 "mov v0,va1"; 5 var fragmentSrc:String = "mov oc,v0"; 6 7 var vertexAssembler:AGALMiniAssembler = new AGALMiniAssembler(); 8 var fragmentAssembler:AGALMiniAssembler = new AGALMiniAssembler(); 9 vertexAssembler.assemble(Context3DProgramType.VERTEX,vertexSrc); 10 fragmentAssembler.assemble(Context3DProgramType.FRAGMENT,fragmentSrc); 11 var program:Program3D = _context3D.createProgram(); 12 program.upload(vertexAssembler.agalcode,fragmentAssembler.agalcode); 13 _context3D.setDepthTest(true,Context3DCompareMode.LESS_EQUAL);//在开启深度测试的情况下,可通过这样的设置启动alpha混合 14 _context3D.setBlendFactors(Context3DBlendFactor.SOURCE_ALPHA,Context3DBlendFactor.ONE_MINUS_SOURCE_ALPHA); 15 _context3D.setProgram(program); 16 }
rebuildBuffer()这一方法中是为了传递新增的alpha分量而做的同步改变。setProgram()方法中则是开启alpha混合的关键。
附一张demo图
注:实际上,后面的称呼更恰当,因为他们更接近问题的本质,而前一种称呼,虽然形象,但是不够严谨,因为前与后这种视觉上的关系实际上是在操作执行之后才呈现的,在这之前没有所谓的前与后