昨天谈到了矩阵的变换, 今天开始谈一下关于顶点,面,法线,网格等基本术语。如果有时间,还会写一个stage3d的demo。
1、顶点
在3d世界里面,基本单位就是三角形,为什么会是三角形?因为三个坐标可以构成一个三角形,而一个三角形则可以表示一个面。一个四边形则是两个三角形组合,再也找不到比三角形更小的几何图形可以用来表示一个面。在想象一下,一个立方体。比如你手边的魔方,盒子等。你可以观察出来,它是由6个四边形组成,也就是由12个三角形组成。 盒子的尖角就是一个顶点。 也就是说,一个多边形两边相交的点就叫做顶点。一般我们也称定义位置的点为顶点。例如你手边的立方体盒子,就是由8个顶点定义,将这个8个顶点分别两两相连,就可以看到一个立方体由12个三角形组成。
大家在想象一下,如果是很多个三角形,分的很细,那么是否就可以组成一个模型呢?例如一个人,汽车?轮子?
大家注意看哪个透明的茶壶,你们就会发现它是由很多个三角形构成的。那么最终的这个形态呢,我们就称之为网格。
2、网格
网格的定义上面就说了。
3、面
最初就提到了,三个点构成的三角形就称之为面。
4、法线
上面提到三个点可以构成一个三角形。在基础0里面就讲到了关于向量。现在我们来看一下这三个点,假设点分别为v0, v1, v2。那么我们可以轻易的得到两个向量:A->v1-v0; B->v2-v0。AB这两个向量的起点都是v0,他们的终点为别为v1,v2。在前面就讲到,两个向量的叉积的结果也是一个向量,并且这个向量垂直于这两个向量。那么我们就可以通过A向量叉成B向量,得到一条垂直向量AB的向量N。这个向量不仅垂直向量AB,而且还垂直于AB向量夹角范围内与AB平行的所有向量。我们称这个向量N为该面的法线。法线的定义也就是:垂直与面的向量。既然向量N垂直这个面,那么这个法线其实也就代表了这个面的方向。只是面的方向和法线成90度而已。
题外话:在3d世界中,一个灯光照射到物体上面,照不到的是黑的,照的到的是亮的,这个是怎么实现的呢?可以想象一下,假设一条平行光从上往下照射到模型上。其中模型面向显示器的有一个面正好与我们的显示器平行。假设就是一个摆放的立方体。我们平行看得到的那个面,就正好和显示器平行。那么那个面的法线就刚好和显示器垂直,也刚好和那一束平行光垂直。前面就提到过了,向量点积的结果可以表示两个向量的夹角关系。那么在这里,我们将这一条法线和平行光向量进行点积,法线值为0。然后我们把这个面涂黑,然后我们按照这个思路对所有的面进行这样的操作。根据点积的结果,将那个面进行不同程度的颜色变化。咦?灯光效果就出来了。。。。
5、贴图
大家看那个茶壶,它是一个网格结构,只有一个线框,中间还是空的,假设我们用一张图片按照一定的规则给它贴上去,那么它看起来就是一个茶壶咯。
要是无法想通,就假设这个张图全黑,在给它贴上面,就有了一个黑不溜秋的茶壶了。。。那么这个图呢,我们就称之为贴图。有法线贴图。。。高光贴图。等等。
关于3d里面最基本的术语呢,就先解释这么多。下面给出一个带有详细注释的demo。这个demo是我买的那本书里面的例子。
?
{
import com.adobe.utils.*;
import flash.display.*;
import flash.display3D.*;
import flash.display3D.textures.*;
import flash.events.*;
import flash.geom.*;
import flash.utils.*;
[SWF(width="640", height="480", frameRate="60", backgroundColor="#000000")]
public class Stage3dGame extends Sprite
{
// 定义后台缓冲区的大小,一般来说这个大小也和swf的大小一样大,关于后台缓冲区可能明天就会谈到
private const swfWidth:int = 640;
private const swfHeight:int = 480;
// 这个是贴图的大小,注意贴图的大小必须为2的幂,也就是说必须为2、4、8、16、32、64、128、256、512等形式,目前贴图最大为2048(stage3d的baseline模式)
private const textureSize:int = 512;
// context3d对象,它主要负责用于和显卡通讯,并且context3d各个对象之间数据不通用,也就是说,你不能把一个context3d的数据妄图放到另一个里面去使用显示
private var context3D:Context3D;
// 着色器程序,之前就说了,将一张图贴到模型上面,这个过程就需要着色器来做,着色器根据给的的数据从贴图里面取出像素然后显示出来。
private var shaderProgramrogram3D;
// 顶点数据buffer。前面说到一个模型是由很多个三角形组成,三角形呢又是由点构成,那么这个Buffer就用来存放各个顶点数据
private var vertexBuffer:VertexBuffer3D;
// 索引buffer。上面是顶点数据的buffer,但是我们只有顶点,我们以及计算机都不知道这些顶点怎么可以构成不同的三角形。因此我们需要提供一个顶点的索引给显卡,让显卡通过索引来取出顶点的数据,也就是说,假如我们有两个三角形,那么我们的索引数据就有6个。前三个索引指定一个三角形,后三个索引指定一个三角形
private var indexBuffer:IndexBuffer3D;
// 内存里面存放的顶点数据
private var meshVertexData:Vector.
// 内存里面存放的索引数据
private var meshIndexData:Vector.
// 投影矩阵,这个矩阵后面会说到。简要说一下:我们在3d里面物体如何显示到一个2d的显示屏幕上面?并且我们看远方,会发现远方的物体会很小,那么这个过程呢,其实就是这个矩阵完成。注意这个矩阵是继承的matrix3d。
private var projectionMatrixerspectiveMatrix3D = new PerspectiveMatrix3D();
// 模型矩阵,我在矩阵变换里面就提到了,如果通过矩阵来进行平移缩放旋转。那么这个矩阵就对应了模型,如果需要对模型就行这些操作,就在这个矩阵上下手吧。
private var modelMatrix:Matrix3D = new Matrix3D();
// 相机矩阵,在3d世界中,我们并不是直接观察3d世界的,我们是通过一个相机来观察的。想象一个你拍照的时候,从相机里面看到的世界。那么这个相机我们同样可以移动,我们可以把相机摆得近一些,看到的物体就清晰一些,也可以摆的远一些,看到的物体就模糊小一些。
private var viewMatrix:Matrix3D = new Matrix3D();
// 最终的矩阵,这个矩阵是模型矩阵*相机矩阵*投影矩阵得到的。(注意这个顺序是固定的,以后会谈到一个渲染管线的东西)
private var modelViewProjection:Matrix3D = new Matrix3D();
// a simple frame counter used for animation
private var t:Number = 0;
/* TEXTURE: Pure AS3 and Flex version:
* if you are using Adobe Flash CS5 comment out the next two lines of code */
[Embed (source = "texture.jpg")] private var myTextureBitmap:Class;
private var myTextureData:Bitmap = new myTextureBitmap();
/* TEXTURE: Flash CS5 version:
* add the jpg to your library (F11)
* right click it and edit the advanced properties so
* it is exported for use in Actionscript and call it myTextureBitmap
* if using Flex/FlashBuilder/FlashDevlop comment out the next two lines of code */
//private var myBitmapDataObject:myTextureBitmapData = new myTextureBitmapData(textureSize, textureSize);
//private var myTextureData:Bitmap = new Bitmap(myBitmapDataObject);
// 这个呐,就是贴图
private var myTexture:Texture;
public function Stage3dGame()
{
if (stage != null)
init();
else
addEventListener(Event.ADDED_TO_STAGE, init);
}
private function init(e:Event = null):void
{
if (hasEventListener(Event.ADDED_TO_STAGE))
removeEventListener(Event.ADDED_TO_STAGE, init);
stage.frameRate = 60;
stage.scaleMode = StageScaleMode.NO_SCALE;
stage.align = StageAlign.TOP_LEFT;
stage.stage3Ds[0].addEventListener(
Event.CONTEXT3D_CREATE, onContext3DCreate);
// 向stage3ds[0]声请一个context3d对象,一般来说0就可以了,不同的显卡可用的stage3ds数量不同
// 上面注释提到了一个baseline的模式,这个模式呢其实就是adobe为了支持手机,移动设备,其他设备,pc等等做出来的一个限制模式,保证大家都可以用这个模式下面的东西。
// 我们在申请context3d的时候可以指定使用扩展模式,这里默认使用受限模式
// stage.stage3D[0].requestContext3D.call(this,Context3DRenderMode.AUTO,profileMode);
?
stage.stage3Ds[0].requestContext3D();
}
private function onContext3DCreate(event:Event):void
{
// 移除帧循环事件,因为有可能我们之前创建好了contex3d对象,但是当我们待机,睡眠,切换了浏览器tab或者浏览器失去了焦点,都有可能让我们的这个contex3d设备丢失。所以我们需要重新声请。
if (hasEventListener(Event.ENTER_FRAME))
removeEventListener(Event.ENTER_FRAME,enterFrame);
// context3d对象是从stage3d对象里面获取的
var t:Stage3D = event.target as Stage3D;
context3D = t.context3D;
if (context3D == null)
{
// Currently no 3d context is available (error!)
return;
}
// 开启context3d的错误检测。这个检测主要是针对显卡里面的错误检测。因为我们以后要对显卡进行编程。但是对显卡进行编程了,我们不好调试,所以adobe这煞笔提供了一个这么一个错误检测选项,开启了这个错误见检测,我们就可以看到错误消息,注意只是看得到错误消息。但是这个错误消息,我真心看不懂,写了这么久了,我也只是凭着经验来调试找bug。开启这个选项会消耗一定的性能,在产品的发布阶段这个就需要关闭了。
context3D.enableErrorChecking = true;
// 初始化顶点数据以及索引数据,这个数据目前我们是手动填入的,显示出来的也只是一个正方形平板。
initData();
// 设置后台缓冲区的大小,以及锯齿,是否启用深度以及模板测试(这个后面再谈)
context3D.configureBackBuffer(swfWidth, swfHeight, 0, true);
// 顶点着色器程序,专门用来处理顶点的
var vertexShaderAssembler:AGALMiniAssembler = new AGALMiniAssembler();
vertexShaderAssembler.assemble
(
Context3DProgramType.VERTEX,
// op是输出目标,va0是我们指定的顶点buffer中的数据,vc0是我们传入的modelViewProj矩阵。m44就是表示,用顶点乘以modelViewProj矩阵,
"m44 op, va0, vc0\n" +
// 将顶点传入中间变量寄存器,因为顶点程序和片段着色器程序是分开的,他们的数据不能通用,因此需要一个中间变量哎传递,并且这个是单向的,只能从顶点程序到片段程序
"mov v0, va0\n" +
// 将指定的buffer中的数据1,也就是uv数据(后面再谈)
"mov v1, va1\n"
);
// 片段着色器程序,专门用来渲染颜色
var fragmentShaderAssembler:AGALMiniAssembler = new AGALMiniAssembler();
fragmentShaderAssembler.assemble
(
Context3DProgramType.FRAGMENT,
// grab the texture color from texture fs0
// tex是采样命令,意思就是根据uv数据从传入的贴图fs0安照<2d,repeat,miplinear>方式进行获取颜色信息,存放在ft0中
"tex ft0, v1, fs0 <2d,repeat,miplinear>\n" +
// oc是颜色输出目标,将ft0传入oc进行输出了。
"mov oc, ft0\n"
);
// 通过context3d创建着色器程序
shaderProgram = context3D.createProgram();
// 向显卡上传指令
shaderProgram.upload(vertexShaderAssembler.agalcode, fragmentShaderAssembler.agalcode);
// 创建indexbuffer
indexBuffer = context3D.createIndexBuffer(meshIndexData.length);
// 向显卡上传索引数据,第一参数指定索引数据,第二个指定起始点,第三个指定长度,我们也可以指定某一段的索引,那么现实出来的模型也即是那一段索引对于的三角形了。也就是说我们可以把两个模型的顶点数据放到一个vertexbuffer里面,索引数据也放到一个indexbuffer里面,然后根据后面两个参数来指定索引。
indexBuffer.uploadFromVector(meshIndexData, 0, meshIndexData.length);
// 创建vertexbuffer,需要指定buffer的长度以及每一段的长度。大家可以想象成一个二维数组,一行8个表示一段数据,总共length/8段数据
vertexBuffer = context3D.createVertexBuffer(meshVertexData.length/8, 8);
vertexBuffer.uploadFromVector(meshVertexData, 0, meshVertexData.length/8);
// 根据texturesize创建texture,这个大小不一定要和贴图一样大小
myTexture = context3D.createTexture(textureSize, textureSize,
Context3DTextureFormat.BGRA, false);
// 下面这个操作是在做mip。将贴图进行不同等级的缩放,也就是当模型很远的时候,就用被缩放过的贴图贴到模型上面去。如果模型很近,就用没有缩放的贴图贴上去。
var ws:int = myTextureData.bitmapData.width;
var hs:int = myTextureData.bitmapData.height;
var level:int = 0;
var tmp:BitmapData;
var transform:Matrix = new Matrix();
tmp = new BitmapData(ws, hs, true, 0x00000000);
while ( ws >= 1 && hs >= 1 )
{
tmp.draw(myTextureData.bitmapData, transform, null, null, null, true);
// 这里就是在上传不同等级贴图,这个过程称之为Mip。level越小,贴图质量越高。
myTexture.uploadFromBitmapData(tmp, level);
transform.scale(0.5, 0.5);
level++;
ws >>= 1;
hs >>= 1;
if (hs && ws)
{
tmp.dispose();
tmp = new BitmapData(ws, hs, true, 0x00000000);
}
}
tmp.dispose();
// 将投影矩阵标准化,就是弄成单位矩阵
projectionMatrix.identity();
// 生成投影矩阵,0.01是看得最近的距离,100是看得远的距离,模型超过了就看不到了。adobe提供的这个算法有问题不靠谱,模型的面过多的时候,会造成图像剧烈抖动。以后我会提 // 供一个算法出来。这里注意看perspectiveFieldOfViewRH这个是生成右手系。
projectionMatrix.perspectiveFieldOfViewRH(45.0, swfWidth / swfHeight, 0.01, 100.0);
// create a matrix that defines the camera location
viewMatrix.identity();
// 将相机往后移动4米。右手系的y是向上,x是向右,z是朝屏幕内,详情看基础0。那么既然是z朝着屏幕内,那么将相机矩阵的z轴移动-4应该是向前面移动啊,怎么会变成往后了呢???大家注意。。。相机看到的东西是反的。。。。
viewMatrix.appendTranslation(0,0,-4);
// start animating
addEventListener(Event.ENTER_FRAME,enterFrame);
}
private function enterFrame(e:Event):void
{
// 清楚后台缓冲区,因为显卡把当前帧的所有数据都绘制到了后台缓冲区,后台缓冲区的数据才是最终显示的,这一帧绘制完成之后,下一帧到来,它需要将上一帧的数据清空掉。
context3D.clear(0,0,0);
// 设置着色器程序
context3D.setProgram ( shaderProgram );
// 之前提到的模型矩阵
modelMatrix.identity();
modelMatrix.appendRotation(t*0.7, Vector3D.Y_AXIS);
modelMatrix.appendRotation(t*0.6, Vector3D.X_AXIS);
modelMatrix.appendRotation(t*1.0, Vector3D.Y_AXIS);
modelMatrix.appendTranslation(0.0, 0.0, 0.0);
modelMatrix.appendRotation(90.0, Vector3D.X_AXIS);
// 旋转0.2
t += 2.0;
// 最终矩阵,使用前这里清空成了单位矩阵
modelViewProjection.identity();
modelViewProjection.append(modelMatrix);
modelViewProjection.append(viewMatrix);
modelViewProjection.append(projectionMatrix);
// 注意看名称,设置顶点程序常量,0表示vc0,如果是矩阵,那么会占用4个寄存器,所有会占用vc0,vc1,vc2,vc3,true是否反转矩阵,这个也以后谈,也是一个坑儿。
context3D.setProgramConstantsFromMatrix(
Context3DProgramType.VERTEX,
0, modelViewProjection, true );
// associate the vertex data with current shader program
// 设置顶点va,0表示va0,从vertexbuffer的一段里面的第0个位置开始取,取3个->顶点坐标信息
context3D.setVertexBufferAt(0, vertexBuffer, 0,
Context3DVertexBufferFormat.FLOAT_3);
// 设置va1,从veftexbuffer的一段里面的第三个位置开始取(前面三个是顶点坐标信息啦),取三个数据
context3D.setVertexBufferAt(1, vertexBuffer, 3,
Context3DVertexBufferFormat.FLOAT_3);
// 设置fs0为mytexture
context3D.setTextureAt(0, myTexture);
// 绘制三角形,传入索引buffer,并且指定绘制多少个三角形。三个索引对应一个三角形嘛。
context3D.drawTriangles(indexBuffer, 0, meshIndexData.length/3);
// 显示后台缓冲区的数据。
context3D.present();
}
private function initData():void
{
// Defines which vertex is used for each polygon
// In this example a square is made from two triangles
meshIndexData = Vector.
([
0, 1, 2, 0, 2, 3,
]);
// Raw data used for each of the 4 verteces
// Position XYZ, texture coordinate UV, normal XYZ
meshVertexData = Vector.
( [
//X, Y, Z, U, V, nX, nY, nZ
-1, -1, 1, 0, 0, 0, 0, 1,
1, -1, 1, 1, 0, 0, 0, 1,
1, 1, 1, 1, 1, 0, 0, 1,
-1, 1, 1, 0, 1, 0, 0, 1
]);
}
}
}