/** * * 翻译力求准确,信达雅谈不上,如有错误或者不准确的地方欢迎指出 * @see http://ltslashgt.com/2011/08/07/rendering-models-with-molehill/ * */我很想接着我 上一篇文章的步伐来点高级的例子,我终于抽出时间来搞了。这是我有关在 Molehill中加载和渲染各种模型系列文章的第一篇(事实上也是最后一篇,之后作者就没更新过)。
模型格式可能很令人抓狂。大多数格式很古老(推出的时间比较早)或者编写时没有考虑到硬件加速。你常常需要对其数据做做调整使其可用。在随着时间的演变硬件加速渲染成为标准的过程中,看看格式的随之变迁很有趣。我们从一些较老的格式开始之后过渡到新的格式。
这是本篇文章的实战 代码,展示了一个由 Bobo the Seal制作的可爱模型 Bunker。它是一个基于文本的模型格式。它支持很多高深的东西,但是(本文中)这个加载类只支持最常用的:顶点位置,法线和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文件。
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 line line = line.replace(/^\s*|\s*$/g, ''); if (line === '' || line.charAt(0) === '#') { // Blank line or comment, ignore it continue; } // TODO: parse the line }
上段代码的TODO,你需要用空格将行拆分开,然后检测它是那种命令。你可以这样做,如下:
// Split line into fields on whitespace var fields:Array=line.split(/\s+/); switch (fields[0].toLowerCase()) { case 'v': // TODO: parse vertex position break; case 'vn': // TODO: parse vertex normal break; case 'vt': // TODO: parse vertex uv break; case 'f': // TODO: parse face break; case 'g': // TODO: parse group break; case 'o': // TODO: parse object break; case 'usemtl': // TODO: parse material break; }顶点位置(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;
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 index return _tupleIndices[tuple]; } else { var faceIndices:Array=tuple.split('/'); // Position index var index:uint=parseInt(faceIndices[0], 10) - 1; _vertices.push(positions[index * 3 + 0], positions[index * 3 + 1], positions[index * 3 + 2]); // Normal index if (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 index if (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 again return _tupleIndices[tuple]=_tupleIndex++; } }这个函数承担了OBJ 加载器的大部分工作,如果元组已经存在于我们的元组缓存中,那么返回已经被合并进去的索引(index),否则,我们将面指向的定点数据(vertex Data)拷贝到合并数组中,并返回新的索引(index)。
vertexBuffer=context.createVertexBuffer(_vertices.length / 8, 8); vertexBuffer.uploadFromVector(_vertices, 0, _vertices.length / 8);
// 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文件。这是另一个很神秘的格式,但它是二进制的并且支持动画,所以有更多可学的东西。