笔者介绍:姜雪伟,IT公司技术合伙人,IT高级讲师,CSDN社区专家,特邀编辑,畅销书作者,国家专利发明人;已出版书籍:《手把手教你架构3D游戏引擎》电子工业出版社和《Unity3D实战核心技术详解》电子工业出版社等。
顶点索引数据是游戏开发中经常需要加载处理的,游戏中的顶点数据最终是通过流传出去的,类中定义了一个结构体,专用于对指定的顶点属性进行绘制,它与glVertexAttribPointer是相关的。这个结构体是Cocos2d-x引擎自己定义的,结构体定义代码如下所示:
struct CC_DLL VertexStreamAttribute
{
/**
构造函数.
*/
VertexStreamAttribute()
: _normalize(false),_offset(0),_semantic(0),_type(0),_size(0)
{
}
/**
构造函数
@参数offset 属性的便宜
@参数semantic 属性的(位置, 纹理, 颜色等).
@参数type属性的类型, 可以是GL_FLOAT类型.
@参数size 描述属性有多少参数类型.
*/
VertexStreamAttribute(int offset, int semantic, int type, int size)
: _normalize(false),_offset(offset),_semantic(semantic),_type(type),_size(size)
{
}
/**
构造函数
@参数 offset 属性的便宜
@参数 semantic 属性的(位置, 纹理, 颜色等).
@参数 type 属性的类型, 可以是GL_FLOAT类型.
@参数 size 描述属性有多少参数类型.
@参数normalize 如果是true, 数据将被255除 */
VertexStreamAttribute(int offset, int semantic, int type, int size, bool normalize)
: _normalize(normalize),_offset(offset),_semantic(semantic),_type(type),_size(size)
{
}
/**
属性是否被标准化
*/
bool _normalize;
/**
在属性缓存中的偏移值
*/
int _offset;
/**
描述属性的用处, 可能是位置,颜色等.
*/
int _semantic;
/**
属性类型的描述, 可能是 GL_FLOAT, GL_UNSIGNED_BYTE 等.
*/
int _type;
/**
描述在属性中有多少元素的类型
*/
int _size;
};
以上定义的是用于传输的数据流,这个也是引擎底层对数据的处理方式。另外还定义了一个VertexData类,该类主要是用于指定输入流到GPU渲染管道,一个VertexData数据是由几个流数据组成的,每个流包含一个VertexStreamAttribute和绑定的顶点缓存。
如果要把定义好的VertexData数据输入到GPU中,首先要做的事情是创建VertexData数据,函数代码如下:
VertexData* VertexData::create()
{
VertexData* result = new (std::nothrow) VertexData();
if(result)
{
result->autorelease();
return result;
}
CC_SAFE_DELETE(result);
return nullptr;
}
VertexData 数据创建好后,需要将其设置到流里面,调用函数 setStream 如下所示:
bool VertexData::setStream(VertexBuffer* buffer, const VertexStreamAttribute& stream)
{
if( buffer == nullptr ) return false;
auto iter = _vertexStreams.find(stream._semantic);
if(iter == _vertexStreams.end())
{
buffer->retain();
auto& bufferAttribute = _vertexStreams[stream._semantic];
bufferAttribute._buffer = buffer;
bufferAttribute._stream = stream;
}
else
{
buffer->retain();
iter->second._buffer->release();
iter->second._stream = stream;
iter->second._buffer = buffer;
}
return true;
}
函数实现了数据流的设置,缓存的设置操作,这些接口都是引擎底层实现的,写逻辑时一般不会直接使用。开发者编写逻辑时,直接填充数据就可以了,引擎底层会将其转成数据流的形式传到GPU中处理。
引擎提供了两个类用于实现模型顶点数据处理:
VertexIndexData类与MeshVertexIndexData类,二者是不同的,前者主要是针对数据流的定义,而后者是具体到模型Mesh的顶点索引数据定义。MeshVertexIndexData类主要是针对模型Mesh的顶点和索引信息的定义,在它里面定义了一个MeshIndexData类,这个类包含了mesh所有的索引数据,与MeshIndexData类对应的是MeshVertexData类,这个类包含了mesh所需要的顶点数据。这两个类对于模型来说非常重要,Unity引擎中也有相关定义,下面具体说一下其内部实现,在MeshVertexData类中有create函数,用于创建模型顶点数据,以及申请顶点缓存用于存放顶点数据,具体实现函数如下所示:
MeshVertexData* MeshVertexData::create(const MeshData& meshdata)
{
auto vertexdata = new (std::nothrow) MeshVertexData();
int pervertexsize = meshdata.getPerVertexSize();
vertexdata->_vertexBuffer = VertexBuffer::create(pervertexsize, (int)(meshdata.vertex.size() / (pervertexsize / 4)));
vertexdata->_vertexData = VertexData::create();
CC_SAFE_RETAIN(vertexdata->_vertexData);
CC_SAFE_RETAIN(vertexdata->_vertexBuffer);
int offset = 0;
for (const auto&it : meshdata.attribs) {
vertexdata->_vertexData->setStream(vertexdata->_vertexBuffer, VertexStreamAttribute(offset, it.vertexAttrib, it.type, it.size));
offset += it.attribSizeBytes;
}
vertexdata->_attribs = meshdata.attribs;
if(vertexdata->_vertexBuffer)
{
vertexdata->_vertexBuffer->updateVertices((void*)&meshdata.vertex[0], (int)meshdata.vertex.size() * 4 / vertexdata->_vertexBuffer->getSizePerVertex(), 0);
}
bool needCalcAABB = (meshdata.subMeshAABB.size() != meshdata.subMeshIndices.size());
for (size_t i = 0; i <meshdata.subMeshIndices.size(); i++) {
auto& index = meshdata.subMeshIndices[i];
auto indexBuffer = IndexBuffer::create(IndexBuffer::IndexType::INDEX_TYPE_SHORT_16, (int)(index.size()));
indexBuffer->updateIndices(&index[0], (int)index.size(), 0);
std::string id = (i < meshdata.subMeshIds.size() ? meshdata.subMeshIds[i] : "");
MeshIndexData* indexdata = nullptr;
if (needCalcAABB)
{
auto aabb = Bundle3D::calculateAABB(meshdata.vertex, meshdata.getPerVertexSize(), index);
indexdata = MeshIndexData::create(id, vertexdata, indexBuffer, aabb);
}
else
indexdata = MeshIndexData::create(id, vertexdata, indexBuffer, meshdata.subMeshAABB[i]);
vertexdata->_indexs.pushBack(indexdata);
}
vertexdata->autorelease();
return vertexdata;
}
在函数中,创建了顶点数据缓存和索引数据缓存,最终要将其加入到vertexdata表中,其中函数的参数MeshData是在类文件CCBundle3DData.h中定义的模型结构体数据, MeshData结构体网格数据定义如下所示:
struct MeshData
{
typedef std::vector<unsigned short> IndexArray;
std::vector<float> vertex;
int vertexSizeInFloat;
std::vector<IndexArray> subMeshIndices;
std::vector<std::string> subMeshIds; //子网格名字
std::vector<AABB> subMeshAABB;
int numIndex;
std::vector<MeshVertexAttrib> attribs;
int attribCount;
public:
/**
* 获取到每个顶点的尺寸大小
* @return 返回所有属性大小的总和
*/
int getPerVertexSize() const
{
int vertexsize = 0;
for(const auto& attrib : attribs)
{
vertexsize += attrib.attribSizeBytes;
}
return vertexsize;
}
/**
* 重置数据
*/
void resetData()
{
vertex.clear();
subMeshIndices.clear();
subMeshAABB.clear();
attribs.clear();
vertexSizeInFloat = 0;
numIndex = 0;
attribCount = 0;
}
/**
* 构造函数
*/
MeshData()
: vertexSizeInFloat(0)
, numIndex(0)
, attribCount(0)
{
}
~MeshData()
{
resetData();
}
};
存放的模型顶点是用vector向量列表定义的,另外子网格索引也是用vector定义的。vector在引擎开发中使用还是非常广泛的,引擎中的MeshData结构体是针对单一的模型顶点定义的,而模型包含的数据是非常多的,举个例子,一个模型包含很多个顶点,给读者展示一下c3t模型内容如下图:
图中只截取了一小部分数据只是为了告诉读者引擎中定义的MeshData结构体对应的就是图中vetices一行的数据,正如读者所看到的文中有很多行数据,所以需要定义一个可以填充整个模型数据的结构体,其实就是把MeshData重新封装了一个vector,引擎中针对模型数据定义的结构体如下所示:
struct MeshDatas
{
std::vector<MeshData*> meshDatas;
void resetData()
{
for(auto& it : meshDatas)
{
delete it;
}
meshDatas.clear();
}
~MeshDatas()
{
resetData();
}
};
结构体用于存储模型的所有数据信息,它也是引擎底层的实现方式。有了数据后,还需要定义缓存用于存放,VertexIndexBuffer类主要是解决这个问题,将在后面介绍。