使用Molehill渲染3D模型

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

你可能感兴趣的:(游戏开发,stage3d,Flash3D,Molehill,模型解析)