Ogre参考手册(九) 5 硬件缓冲区
5 硬件缓冲区
顶点、索引和像素缓冲区继承了大部分的HardwareBuffer类特征。硬件缓冲区的基本前提是那一一块可以任意处理的内存,没有格式(顶点等),完全依赖于使用时的解释方法。从这方面,硬件缓冲区就像你通过malloc分配的内存,区别在于可能位于GPU或者AGP存储中。
5.1硬件缓冲区管理
HardwareBufferManager类是几何系统中所有对象创建的工厂中心。你创建和销毁的大部分物体的几何定义都通过这个类。它是单例的,可以通过HardwareBufferManager::getSingleton()访问,不过需要在渲染系统初始化之后(调用完Root::initalise),这是因为不同的API下物体的创建方式不同,虽然使用的是公共接口。
例:
VertexDeclaration* decl = HardwareBufferManager::getSingleton().createVertexDeclaration();
HardwareVertexBufferSharedPtr vbuf = HardwareBufferManager::getSingleton().createVertexBuffer(
3*sizeof(Real), // size of one whole vertex
numVertices, // number of vertices
HardwareBuffer::HBU_STATIC_WRITE_ONLY, // usage
false); // no shadow buffer
5.2缓冲区用途
硬件缓冲区的内存在渲染场景时竞争非常激烈,因而缓冲区在时间上的使用方式很重要;是否需要经常更新缓冲区的内容,是否需要从中读取数据,都是影响图形卡管理缓冲区的很重要因素。最佳的缓冲区类型是不需要经常更新且不需要读取。创建顶点和索引缓冲区方法和参数不同,但有一个共同的参数usage:
HBU_STATIC 不需要经常更新,但需要读取
HBU_STATIC_WRITE_ONLY 不需要经常更新,也不需要读取。不过,如果你可以从其影子缓冲区读取,如果设置了的话(参考:影子缓冲区)。这是最优的用途选项。
HBU_DYNAMIC 需要经常更新,而且需要读取。这是最差的设置
HBU_DYNAMIC_WRITE_ONLY 经常更新,但不需要读取。不过,如果你可以从其影子缓冲区读取,如果设置了的话(参考:影子缓冲区)。如果你使用这个选项,而且每帧都要替换全部内容,可以使用HBU_DYNAMIC_WRITE_ONLY_DISCARDABLE代替,可以在某些平台上取得更好的性能。
HBU_DYNAMIC_WRITE_ONLY_DISCARDABLE 需要经常替换缓冲区的全部内容,特别是每帧。使用此选项,系统不用关心现有内容的丢失,因为丢失后你很快会再放进去。有些平台上可以获得显著的性能。如果你使用这个方式,在锁定以便写入内容时,需要使用HBL_DISCARD标志。
如果你需要经常更新顶点缓冲区,考虑你真正需要更新全部还是部分。如果是后者,考虑使用多个缓冲区,仅将需要修改的数据缓冲区标记为HBU_DYNAMIC
尽可能尝试_WRITE_ONLY形式。这意味着你不能直接从硬件缓冲区中读取,这是很好的实践,因为从硬件缓冲区读取速度很慢。如果你确实需要读取数据,可以使用下一节介绍的影子缓冲区。
5.3影子缓冲区(shadow buffer)
从硬件缓冲区读取数据很慢,如果你的确需要从顶点缓冲区读取数据,你可以设置createVertexBuffer或者createIndexBuffer的shadowBuffer参数为true。这将会在系统内存内为硬件缓冲区创建一份拷贝,你可以得到读取普通内存一样的性能。代价是写入时,将会首先更新系统内存,然后再进行单独拷贝过程更新硬件缓冲区。因此在写数据时会产生额外的开销。除非确实需要使用,否则不要使用它。
5.4锁定缓冲区
如果读取或更新硬件缓冲区,你需要先锁定缓冲区。这会做两件事:告诉图形卡你需要访问缓冲区(会影响渲染队列),并获取操纵数据的指针。注:如果你需要读取缓冲区,硬件缓冲区的内容将拷贝到系统内存以便访问(你不应该这么做,除非你为缓冲区设置了影子缓冲区)。使用后同样需要解锁,如果你锁定缓冲区进行更新,此时修改的信息将更新到图形硬件。
锁定参数
锁定缓冲区有两种方法:
// 锁定整个缓冲区
pBuffer->lock(lockType);
// 锁定部分
pBuffer->lock(start, length, lockType);
锁定部分缓冲区快于锁定全部,因为需要更小的数据传输,以小的批次锁定不能使用HBL_DISCARD标记。
锁定类型lockType对性能有很大影响,特别是没有使用影子缓冲区的时候,说明:
HBL_NORMAL 允许读写,这是最差的性能。如果你没有使用影子缓冲区,这将会导致数据从图形卡传输到系统内存并再传输回去。如果使用影子缓冲区,影响就很小。
HBL_READ_ONLY 只读。在使用了影子缓冲区时效果最好(仅当前的读效果好,缓冲区创建时的写很慢),因为不需要从图形卡下载数据。
HBL_DISCARD 参考HBU_DYNAMIC_WRITE_ONLY_DISCARDABLE,这表示你并不介意图形卡丢弃整个缓冲区的内容。这隐含着你不会从中读取数据,同时意味着图形卡可以避免在此缓冲区正在渲染时产生堵塞(渲染时也可以直接锁定),因为它实际上给了你完全不同的数据(即未初始化的内存,并未使用缓冲区内容覆盖)。未使用影子缓冲区时尽可能使用此选项。如果你使用了影子缓冲区,这起的作用很小,尽管使用影子缓冲区最好是一次锁定整个缓冲区,从而允许通过使用HBL_DISCARD标记更新修改数据到缓冲区。
HBL_NO_OVERWRITE 在仅锁定了部分因而不能使用HBL_DISCARD标记时很有用。这告诉显卡你不会修改任何当前帧渲染操作使用的部分(未锁定的部分?)。仅在没有影子缓冲区时有效。
锁定缓冲区之后,你可以使用返回的指针做任何操作(不要自找麻烦去读取使用HBL_DISCARD锁定的数据或写入HBL_READ_ONLY数据)。内容的修改依赖于缓冲区的类型,参考顶点缓冲区、索引缓冲区
5.5实用的缓冲区提示
1、尽可能使用通过HBL_STATIC_WRITE_ONLY标识创建的缓冲区(不使用影子缓冲区),并通过HBL_DISCARD一次锁定生成数据。之后再不访问它
2、如果需要经常更新缓冲区,创建时使用HBU_DYNAMIC_WRITE_ONLY(同样无影子缓冲区),使用HBL_DISCARD锁定整个缓冲区,或者可以的话使用HBL_NO_OVERWRITE锁定部分
3、如果你确实需要从缓冲区读取数据,创建影子缓冲区。锁定仅用于读取时使用HBL_READ_ONLY,以避免解锁时上传数据。你可以同时混合使用前面说的的两点,尽可能使用static。_WRITE_ONLY可以安全的用于读取。
4、当顶点不同要素使用方式不同时可划分为多个缓冲区。不要为顶点数据使用一个巨大的可更新的缓冲区,比如如果你只需要更新纹理坐标,可以将纹理坐标化为单独的缓冲区,其它的使用HBU_STATIC_WRITE_ONLY
附:
从硬件缓冲区直接读取性能很慢(对写入做了优化),如果需要读取应该使用影子缓冲区,这实际上放了两份,系统内存中的影子缓冲区用于读取。因此应尽量避免读取。
相比CPU性能,内存访问也很慢,因此应使用HBL_DISCARD(不需要初始填充内存)或者使用HBL_NO_OVERWRITE(部分锁定),以减少内存访问。
5.6硬件顶点缓冲区
本章节特殊说明硬件顶点缓冲区,关于硬件缓冲区的一般说明以及相应的创建、锁定,参考硬件缓冲区
5.6.1 VertexData 类
VertexData类汇集了用于几何体渲染的所有顶点相关信息。
新的RenderOperation类需要一个VertexData指针,同时还用于Mesh和SubMesh以存储顶点位置、法线、纹理坐标等。VertexData既可以独立使用(渲染非索引的几何体),也可以和IndexData混合使用,此时三角面由索引定义。
你并不需要使用VertexData存储几何数据,你只需要在渲染的时候提供VertexData即可。这比较容易,因为VertexData所有成员都是指针,因此你可以使用替代的结构维护自己的顶点缓冲区,只要在渲染时转换为VertexData即可。
VertexData中重要的成员:
vertexStart 顶点起始范围,可支持多个renderable共享同一个缓冲区
vertexCount 顶点个数
vertexDeclaration 定义顶点格式的VertexDelaration对象指针。注:该对象由VertexData为你创建,参考顶点声明
vertexBufferBinding 定义顶点缓冲区绑定的VertexBufferBinding对象,同样由VertexData为你创建,参考顶点缓冲区绑定
5.6.2 顶点声明
顶点声明定义了渲染几何体的顶点输入数据。顶点声明允许定义多个数据项(顶点元素,VertexElement类)以支持任意数目的缓冲区,多个共享的或用于单个元素。你需要保证缓冲区的内容与顶点声明所指示的用途一致
可使用VertexDelcaration::addElement增加元素,参数包括:
source 元素所使用的缓冲区索引,从0到顶点数据所使用的缓冲区个数-1。通过索引而不是直接使用缓冲区指针是为了使重新绑定顶点数据源更容易,避免改动顶点格式声明。
- offset 元素相对于整个顶点数据的偏移字节数。如果只有一个元素偏移为0,多个元素时为前面的各元素大小之和
type 元素数据类型(同时决定了大小)。这个很重要,因为GPU越来越先进,可以不再假设顶点位置必须是3个浮点数,因为可编程顶点管线提供了对输入输出的全面控制。这部分定义了元素的类型和大小,比如VET_FLOAT3表示3个浮点数,其意义由semantic参数指定
semantic 元素的含义,GPU通过这个决定输入数据的使用,使用可编程管线时,这表明输入数据映射的语义。这可以标识元素是位置、法线、纹理坐标数据等
index 当同一顶点声明含多个相同的语义时使用。比如使用了超过一组纹理坐标时,第一组为0,第二组为1
可通过多次调用addElement增加多个语义,同时VertexDelaration还提供了定位元素的方法
重要的考虑
理论上你可以完全控制顶点的格式,但实际上有一些限制。老的DirectX硬件要求以下限制
1 顶点元素必须按以下顺序:
位置、混合权重、法线、漫反射色、镜面色、纹理坐标
2 缓冲区不能存在不被顶点元素引用的缝隙
3 不能使offest设置将两个顶点元素重叠
5.6.3 顶点缓冲区绑定
顶点缓冲区绑定用于将顶点缓冲区关联到顶点声明的源数据索引(source)
创建顶点缓冲区
HardwareVertexBufferSharedPtr vbuf = HardwareBufferManager::getSingleton().createVertexBuffer(
3*sizeof(Real), // size of one whole vertex
numVertices, // number of vertices
HardwareBuffer::HBU_STATIC_WRITE_ONLY, // usage
false); // no shadow buffer
顶点缓冲区通过HardwareVertexBufferSharedPtr以引用计数法方式管理,以用于多个几何体共享时可正确销毁
参数说明:
vertexSize 单个顶点的字节大小,一个顶点可包含多个元素。
numVertices 顶点个数。并不需要一次使用所有顶点,可以创建大的缓冲区用于多个几何体共享,因为改变顶点缓冲区绑定属于渲染状态切换,应尽量最小化
usage 参考缓冲区用途
useShadowBuffer 是否使用影子缓冲区
绑定顶点缓冲区
vertextData->vertexBufferBinding->setBinding(0, vbuf);
将缓冲区绑定到索引0
5.6.4 更新顶点缓冲区
首先锁定,获取指针:
Real* pReal = static_cast<Real*>(vbuf->lock(HardwareBuffer::HBL_DISCARD)); //overwrite only
注:Real应为float,否则使用双精度编译ogre时有问题
依据获取的指针直接写入或读取数据,Ogre提供了一些辅助的方法, 例:
// Get base pointer
unsigned char* pVert = static_cast<unsigned char*>(vbuf->lock(HardwareBuffer::HBL_READ_ONLY));
Real* pReal;
for (size_t v = 0; v < vertexCount; ++v)
{
// Get elements
VertexDeclaration::VertexElementList elems = decl->findElementsBySource(bufferIdx);
VertexDeclaration::VertexElementList::iterator i, iend;
for (i = elems.begin(); i != elems.end(); ++i)
{
VertexElement& elem = *i;
if (elem.getSemantic() == VES_POSITION)
{
elem.baseVertexPointerToElement(pVert, &pReal);
// write position using pReal
}
}
pVert += vbuf->getVertexSize();
}
vbuf->unlock();
decl->findElementsBySource 获取缓冲区关联的元素列表
elem.baseVertexPointerToElement(pVert, &pReal) 根据pVert计算elem对应的数据偏移指针pReal
注:例子是HBL_READ_ONLY实际是不能写得(不会更新到硬件缓冲区),应该是HBL_DISCARD
5.7硬件索引缓冲区
索引缓冲区通过索引引用间接顶点的方式构造三角形,和顶点缓冲区一样可以用于多个几何体共享
5.7.1 IndexData类
类汇总了使用索引绘制几何体的信息,主要成员包括:
indexStart 起始索引,多个几何体共享同一索引缓冲区时有用
indexCount 索引个数
indexBuffer 放置索引数据的缓冲区
创建索引缓冲区
和顶点缓冲区类似
HardwareIndexBufferSharedPtr ibuf = HardwareBufferManager::getSingleton().createIndexBuffer(
HardwareIndexBuffer::IT_16BIT, // type of index
numIndexes, // number of indexes
HardwareBuffer::HBU_STATIC_WRITE_ONLY, // usage
false); // no shadow buffer
参数说明:
indexType 索引类型,有两种:16bit和32bit,当顶点超多65526时需要使用32bit索引。32bit索引在一些老的图形卡上不支持。另外DirectX/Opengl一个批次只支持65536个基本图元,使用32bit时最多也只能支持65536*3个索引(一个图元最多3个顶点索引)。
numIndexes 索引个数
usage 缓冲区用途
useShadowBuffer 是否使用影子缓冲区
5.7.2 更新索引缓冲区
需首先锁定以获取数据指针,16位索引为unsigned short*, 32位为unsigned long*
unsigned short* pIdx = static_cast<unsigned short*>(ibuf->lock(HardwareBuffer::HBL_DISCARD))
更新后调用unlock已提交数据。
5.8 硬件像素缓冲区
硬件像素缓冲区是图形卡一种特殊的存放图形数据的缓冲区,普遍用于纹理。像素缓冲区可以表示1维、二维、三维图像。一个纹理可以由过个缓冲区组成。
相对于顶点和索引缓冲区,像素缓冲区不能直接构造。创建纹理时,会自动构造必要的像素缓冲区。
5.8.1 纹理
纹理指可以应用到三维模型表面的图像。Ogre中,纹理通过Texture类体现
创建纹理
纹理通过TextureManger类创建,大多数时候由Ogre资源系统通过图像文件直接创建。你也可以自己手动创建
ptex = TextureManager::getSingleton().createManual(
"MyManualTexture", // Name of texture
"General", // Name of resource group in which the texture should be created
TEX_TYPE_2D, // Texture type
256, // Width
256, // Height
1, // Depth (Must be 1 for two dimensional textures)
0, // Number of mipmaps
PF_A8R8G8B8, // Pixel format
TU_DYNAMIC_WRITE_ONLY // usage
);
纹理用途
除了硬件缓冲区的用途外,纹理附加了一些用途:
TU_AUTOMIPMAP 由图形硬件自动产生mipmap,具体的算法未定义,你可以假定为2*2盒过滤器
TU_RENDERTARGET 纹理作为渲染目标,用于渲染到纹理,使用此标记将忽略除TU_AUTOMIPMAP外的标记
TU_DEFAULT 等价于TY_AUTOMIPMAP | TU_STATIC_WRITE_ONLY。资源系统使用此标记加载图形文件
获取像素缓冲区
纹理可能由多个像素缓冲区组成,每个mipmap和面组合对应一个像素缓冲区,可通过Texture::getBuffer ( face, mipmap)获取:
对于立方体纹理,face表示对应的面,否则face为0。例:
// Get the PixelBuffer for face 0, mipmap 0.
HardwarePixelBufferSharedPtr ptr = tex->getBuffer(0,0);
5.8.2 更新像素缓冲区
有两种方法可以更新像素缓冲区,都需要使用PixelBox对象
blitFromMemory
// Manually loads an image and puts the contents in a manually created texture
Image img;
img.load("elephant.png", "General");
// Create RGB texture with 5 mipmaps
TexturePtr tex = TextureManager::getSingleton().createManual(
"elephant",
"General",
TEX_TYPE_2D,
img.getWidth(), img.getHeight(),
5, PF_X8R8G8B8);
// Copy face 0 mipmap 0 of the image to face 0 mipmap 0 of the texture.
tex->getBuffer(0,0)->blitFromMemory(img.getPixelBox(0,0));
这是最简单的方式,blitFromMemory自动完成格式转换和缩放
直接内存锁定
通过锁定,直接访问缓冲区的内容, 例:
/// Lock the buffer so we can write to it
buffer->lock(HardwareBuffer::HBL_DISCARD);
const PixelBox &pb = buffer->getCurrentLock();
/// Image data starts at pb.data and has format pb.format
/// Here we assume data.format is PF_X8R8G8B8 so we can address pixels as uint32.
uint32 *data = static_cast<uint32*>(pb.data);
size_t height = pb.getHeight();
size_t width = pb.getWidth();
size_t pitch = pb.rowPitch; // Skip between rows of image
for(size_t y=0; y<height; ++y)
{
for(size_t x=0; x<width; ++x)
{
// 0xRRGGBB -> fill the buffer with yellow pixels
data[pitch*y + x] = 0x00FFFF00;
}
}
/// Unlock the buffer again (frees it for use by the GPU)
buffer->unlock();
5.8.3 纹理类型
TEX_TYPE_1D
TEX_TYPE_2D
TEX_TYPE_3D 体纹理,通过三维纹理坐标访问
TEX_TYPE_CUBE_MAP 立方体映射纹理,由6个二维纹理组成,通过三维纹理坐标访问
立方体映射纹理
六个面分别为
+X (face 0) (right).
-X (face 1) (left).
+Y (face 2) (top).
-Y (face 3) (bottom).
+Z (face 4) (front!).
-Z (face 5) (back!)
5.8.4 像素格式
像素格式定义了像素内存中的编码方式:
本地端序格式内存中的本地端序整数(16、24、32bit)。即格式为PF_A8R8G8B8的图像可以看做一个32位数组,定义为十六进制0xAARRGGBB.
Byte格式(PF_BYTE_*)
Short 格式(PF_SHORT_*)
Float16(PF_FLOAT16_*) 仅5位指数,10位尾数。没有对应的C++/CPU数据类型支持,但GPU上可以更快的支持
Float32格式(PF_FLOAT32_*)
压缩格式 PF_DXT[1-5] s3公司的压缩纹理格式(s3tc)
颜色通道
RGB 红绿蓝
A 透明度alpha
L 亮度 Luminance
X 忽略
5.8.5 像素盒(pixel box)
Ogre所有方法通过PixcelBox处理原始图像数据,主要成员包括:
data 图像数据内存指针
format PixelFormat
rowPitch 行块数,压缩纹理与getWidth相同
slicePitch 页(面)块数,压缩纹理为getWidth()*getHeight()
left, top, right, bottom, front, back
常用方法
getWidth() 宽
getHeight() 高
getDepth() 高、深度
getRowSkip() 行空隙 rowPictch – getWidth()
getSliceSkip() 页空隙 slicePitch - (getHeight() * rowPitch)
isConsecutive()是否连续
setConsecutive() 设置为连续(并不处理实际数据)