/** * * 翻译力求准确,信达雅谈不上,如有错误或者不准确的地方欢迎指出 * @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 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;
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 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)。
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文件。这是另一个很神秘的格式,但它是二进制的并且支持动画,所以有更多可学的东西。