使用Molehill渲染3D模型

来源:互联网 发布:win7如何禁止安装软件 编辑:程序博客网 时间:2024/05/12 02:40
/** *  * 翻译力求准确,信达雅谈不上,如有错误或者不准确的地方欢迎指出 * @see http://ltslashgt.com/2011/08/07/rendering-models-with-molehill/ *  */
我很想接着我上一篇文章的步伐来点高级的例子,我终于抽出时间来搞了。这是我有关在Molehill中加载和渲染各种模型系列文章的第一篇(事实上也是最后一篇,之后作者就没更新过)。

模型格式可能很令人抓狂。大多数格式很古老(推出的时间比较早)或者编写时没有考虑到硬件加速。你常常需要对其数据做做调整使其可用。在随着时间的演变硬件加速渲染成为标准的过程中,看看格式的随之变迁很有趣。我们从一些较老的格式开始之后过渡到新的格式。

这是本篇文章的实战代码,展示了一个由Bobo the Seal制作的可爱模型Bunker。
//
//演示到原网站上看看吧
//

Wavefront OBJ

Wavefront OBJ格式是为上世纪80年代Wavefront公司的Advanced Visualizer(一个3D软件)创建的。据我所知,它自从上世纪90年代以来一直没有更新(对于模型格式来说,这是常态)。今天他仍旧被用来展示静态物体,因为其格式简单所以几乎所有的建模软件都支持它。

它是一个基于文本的模型格式。它支持很多高深的东西,但是(本文中)这个加载类只支持最常用的:顶点位置,法线和UV坐标以及具有3个或更多顶点的面此外还有材质组。OBJ格式不支持动画

代码

[Embed(source="../res/bunker/bunker.obj", mimeType="application/octet-stream")]static protected const BUNKER_OBJ:Class;[Embed(source="../res/bunker/fidget_head.png")]static protected const BUNKER_HEAD:Class;[Embed(source="../res/bunker/fidget_body.png")]static protected const BUNKER_BODY:Class;
创建之后像这样加载
// Load the model, and set the material textures_obj = new OBJ();_obj.readBytes(new BUNKER_OBJ(), _context);_obj.setMaterial('h_head', _headTexture);_obj.setMaterial('u_torso', _bodyTexture);_obj.setMaterial('l_legs', _bodyTexture);
这里你会注意到我手动设置了材质纹理,因为loader不处理MTL文件。
OBJ文件自身在OBJ.as中的readBytes()方法中被解析。为OBJ(提供方便)的字节数组(ByteArray)被传了进来,它必须被转化成文本,然后一行一行的读。任何空行或者以#开头的行都会被忽略,代码如下:
var text:String = bytes.readUTFBytes(bytes.bytesAvailable);var lines:Array = text.split(/[\r\n]+/);for each (var line:String in lines){// Trim whitespace from the lineline = line.replace(/^\s*|\s*$/g, '');if (line === '' || line.charAt(0) === '#'){// Blank line or comment, ignore itcontinue;}// TODO: parse the line}

上段代码的TODO,你需要用空格将行拆分开,然后检测它是那种命令。你可以这样做,如下:

// Split line into fields on whitespacevar fields:Array=line.split(/\s+/);switch (fields[0].toLowerCase()){case 'v':// TODO: parse vertex positionbreak;case 'vn':// TODO: parse vertex normalbreak;case 'vt':// TODO: parse vertex uvbreak;case 'f':// TODO: parse facebreak;case 'g':// TODO: parse groupbreak;case 'o':// TODO: parse objectbreak;case 'usemtl':// TODO: parse materialbreak;}

顶点位置(v命令)只是3个浮点数。字段都从字符串转化为数字,并push进了positions数组
case 'v':positions.push(parseFloat(fields[1]), parseFloat(fields[2]), parseFloat(fields[3]));break;
顶点法线(vn命令)工作方式相同:
case 'vn':normals.push(parseFloat(fields[1]), parseFloat(fields[2]), parseFloat(fields[3]));break;
顶点UV(vt命令)只是两个浮点数。OBJ有一个翻转的V轴纹理坐标,所以你需要将其翻转回正常的:
case 'vt':uvs.push(parseFloat(fields[1]), 1.0 - parseFloat(fields[2]));break;
对于组(g命令),创建了一个新的OBJGroup对象并将其添加到组列表中。组有几个属性(name,material和face),因此OBJGroup对象对于跟踪那些东西很有用。
case 'g':group = new OBJGroup(fields[1], materialName);groups.push(group);break;
材质名称(usemtl命令)仅被保存下来并赋给当前的组(如果有的话)。默认清空下任何后续的组都将被赋予当前的材质,除非它们有自己的usemtl命令
case 'usemtl':materialName = fields[1];if (group !== null){group.materialName = materialName;}break;
如前所述,面组(f命令)是一系列的索引元祖。创建一个新的vector来保存面的索引元祖,并且面被添加到当前的组当中。后续会处理它。
case 'f':face = new Vector.<String>();for each (var tuple:String in fields.slice(1)){face.push(tuple);}if (group === null){group = new OBJGroup(null, materialName);groups.push(group);}group._faces.push(face);break;

Fixing up the data(数据整理)

这是所有我们需要处理的命令。这个循环将对文件中所有的行进行复制。一旦完成后我们将会有几个分开的顶点数据流(位置,法线和UV)。我们也会有组的列表,每个都有其面的列表,他们有进入这些分流的索引(indice)。
这是个问题。OBJ为位置、法线和UV指定了分开的索引,但是现代的硬件渲染不知那些。我们只能有一个顶点流(index Stream)。要修正它,我们需要将这三个顶点流合并成一个顶点流。面的顶点(face indices)也需要更新以便在这个流中指定正确的偏移量。
要做到这点,每个组得到一个新的索引流(index stream)。然后对于面中的每一个索引元祖我在合并后的流中写入一个新的vertex,如果在别的面中已经有那个顶点元组,则使用已被合并进去的索引(index)。
我们所面临的的另一个问题是OBJ允许多边形面。也就是说,面不必是三角形。这就是问题所在:Context3D只支持绘制三角形。要修正这,我们需要将非三角形转化成三角形。
这一切循环如下:
for each (group in groups){group._indices.length=0;for each (face in group._faces){var il:int=face.length - 1;for (var i:int=1; i < il; ++i){group._indices.push(mergeTuple(face[i], positions, normals, uvs));group._indices.push(mergeTuple(face[0], positions, normals, uvs));group._indices.push(mergeTuple(face[i + 1], positions, normals, uvs));}}group.indexBuffer=context.createIndexBuffer(group._indices.length);group.indexBuffer.uploadFromVector(group._indices, 0, group._indices.length);group._faces=null;}
上述循环对面中的每一个索引元祖(mergeTuple)调用了mergeTuple方法。该方法如下:
protected function mergeTuple(tuple:String, positions:Vector.<Number>, normals:Vector.<Number>, uvs:Vector.<Number>):uint{if (_tupleIndices[tuple] !== undefined){// Already merged, return the merged indexreturn _tupleIndices[tuple];}else{var faceIndices:Array=tuple.split('/');// Position indexvar index:uint=parseInt(faceIndices[0], 10) - 1;_vertices.push(positions[index * 3 + 0], positions[index * 3 + 1], positions[index * 3 + 2]);// Normal indexif (faceIndices.length > 2 && faceIndices[2].length > 0){index=parseInt(faceIndices[2], 10) - 1;_vertices.push(normals[index * 3 + 0], normals[index * 3 + 1], normals[index * 3 + 2]);}else{// Face doesn't have a normal_vertices.push(0, 0, 0);}// UV indexif (faceIndices.length > 1 && faceIndices[1].length > 0){index=parseInt(faceIndices[1], 10) - 1;_vertices.push(uvs[index * 2 + 0], uvs[index * 2 + 1]);}else{// Face doesn't have a UV_vertices.push(0, 0);}// Cache the merged tuple index in case it's used againreturn _tupleIndices[tuple]=_tupleIndex++;}}
这个函数承担了OBJ 加载器的大部分工作,如果元组已经存在于我们的元组缓存中,那么返回已经被合并进去的索引(index),否则,我们将面指向的定点数据(vertex Data)拷贝到合并数组中,并返回新的索引(index)。
OBJ无需指定法线和UV,因此为了使与其之前状态一致,我们只在那里填充几个0。现在的顶点缓冲是0索引,但是OBJ不是。所以我们还需要从所有的顶点里减去1(indices)。
最后但同样重要的是,我们需要为新的流创建vertex Buffer:
vertexBuffer=context.createVertexBuffer(_vertices.length / 8, 8);vertexBuffer.uploadFromVector(_vertices, 0, _vertices.length / 8);

Rendering the model(渲染模型)

解决了模型加载,渲染那就是一气呵成。DemoOBJ.as文件中的update()函数做了些设置,然后如下渲染OBJ:
// Draw the model_context.setVertexBufferAt(0,_obj.vertexBuffer,0,Context3DVertexBufferFormat.FLOAT_3);_context.setVertexBufferAt(1, _obj.vertexBuffer, 3, Context3DVertexBufferFormat.FLOAT_3);_context.setVertexBufferAt(2, _obj.vertexBuffer, 6, Context3DVertexBufferFormat.FLOAT_2);for each (var group:OBJGroup in _obj.groups){_context.setTextureAt(0, _obj.getMaterial(group.materialName));_context.drawTriangles(group.indexBuffer);}
这为在OBJ Buffer中的vertex Buffer设置了的位置、法线和UV。然后遍历每个组并为其设置材料并绘制与组相关的三角形。

我希望这有助于展示在Molehill(Stage3D)如何加载和渲染模型.在本文中我不能覆盖所有的代码,所以如果你有问题可以随意发表评论或者给我发邮件。如果你想找到更多的OBJ文件来折腾,我推荐你去Polycount上的SDK master thread看看

在我的下一篇文章中,我将看看Quake MDL文件。这是另一个很神秘的格式,但它是二进制的并且支持动画,所以有更多可学的东西。

原创粉丝点击