Filament渲染引擎剖析 之 通过图元构建几何体
什么是图元
图元是构成图形实体的最小单元,可见物体的表面可以由数量不等的三角形拟合而成,常见的图元类型有点、线、三角形等,无论多么复杂的物体外观,一般都可以用这三类基础图元拟合而成。
OpenGL 常见的图元类型: 点、线、条带线、循环线、三角形、三角形条带、三角形扇面。在OpenGL早期版本中多边形与四边形也是OpenGL 基本图元类型,但在OpenGL3.0 后被废弃了。可以看出OpenGL 图元类型越来越简单易用,OpenGL3.0版本可以看作OpenGL崛起的分水岭,逐步向DirextX 靠齐,化繁为简, 把精力重点放在了渲染上。
DirextX 基本图元类型也类似,有 点、线、条带线、循环线、三角形、三角形条带、三角形扇面这几种类型。
filament可绘制的图元类型
/**
* Primitive types
*/
enum class PrimitiveType : uint8_t {
// don't change the enums values (made to match GL)
POINTS = 0, //!< points
LINES = 1, //!< lines
TRIANGLES = 4, //!< triangles
NONE = 0xFF
};
filament 支持 点、线、三角形这三种基础图元的绘制。
构建图元的工具
先用最简单的例子说明 OpenGL 绘制实体的过程,加深点印象,重点看设置渲数据部分,省略shader相关的代码 。虽然filament 支持多种图形API, 但内部的表示大同小异,OpenGL 也不例外。
上面的代码,首先使用glGenvertexArrays()生成 VAO 对象,然后生成跟顶点数组关联的缓存对象, 并用顶点数据初始化缓存对象, 加载并编译shader 并使之生效, 使用 glVertexAttribPointer()设置顶点属性,顶点属性可以是位置、颜色、法相等信息,最后启用顶点数组对象VAO。 使用顶点数组对象时,它会记录渲染相关的信息,比如顶点数据源在哪里,纹理设置信息等等。在绘制时不需要反复调用相关的图形API, 只需要启用顶点数据,调用绘制函数即可完成实体的绘制。
glClear()清屏,启用顶点数组对象,调用glDrawArrays() 命令绘制实体。glFlush 会刷新客户端的渲染命令队列,将命令队列拷贝至GPU服务器端的命令队列中,并立即返回。 OpenGL 还有一个函数 glFinish(), 该函数会等待GPU 直到所有绘制命令执行完毕才返回。
除了glDrawArrays() 按顶点顺序绘制的方式,为了复用顶点,OpenGL 还提供了索引形式的绘制方式:
void
display(void)
{
glClear(GL_COLOR_BUFFER_BITS);
glBindVertexArray(VAO[Triangles]);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, Buffers[IndexBuffer]);
glDrawElements(GL_TRIANGLES,6, GL_UNSIGNED_SHORT, NULL);
glFlush();
}
filament 使用索引模式(第二种模式)绘制图元, 那么在filament 渲染引擎中用什么来表示以顶点数组、索引数组,图元组成的可绘制对象呢?三个关键类: VertexBuffer, IndexBuffer, Primitive。
我们看看 Cube 这个基体,filament 是如何绘制的:
Cube::Cube(Engine& engine, filament::Material const* material, float3 linearColor, bool culling) :
mEngine(engine),
mMaterial(material) {
mVertexBuffer = VertexBuffer::Builder()
.vertexCount(8)
.bufferCount(1)
.attribute(VertexAttribute::POSITION, 0, VertexBuffer::AttributeType::FLOAT3)
.build(engine);
mIndexBuffer = IndexBuffer::Builder()
.indexCount(12*2 + 3*2*6)
.build(engine);
if (mMaterial) {
mMaterialInstanceSolid = mMaterial->createInstance();
mMaterialInstanceWireFrame = mMaterial->createInstance();
mMaterialInstanceSolid->setParameter("color", RgbaType::LINEAR,
LinearColorA{linearColor.r, linearColor.g, linearColor.b, 0.05f});
mMaterialInstanceWireFrame->setParameter("color", RgbaType::LINEAR,
LinearColorA{linearColor.r, linearColor.g, linearColor.b, 0.25f});
}
mVertexBuffer->setBufferAt(engine, 0,
VertexBuffer::BufferDescriptor(
mVertices, mVertexBuffer->getVertexCount() * sizeof(mVertices[0])));
mIndexBuffer->setBuffer(engine,
IndexBuffer::BufferDescriptor(
mIndices, mIndexBuffer->getIndexCount() * sizeof(uint32_t)));
utils::EntityManager& em = utils::EntityManager::get();
mSolidRenderable = em.create();
RenderableManager::Builder(1)
.boundingBox({{ 0, 0, 0 },
{ 1, 1, 1 }})
.material(0, mMaterialInstanceSolid)
.geometry(0, RenderableManager::PrimitiveType::TRIANGLES, mVertexBuffer, mIndexBuffer, 0, 3*2*6)
.priority(7)
.culling(culling)
.build(engine, mSolidRenderable);
mWireFrameRenderable = em.create();
RenderableManager::Builder(1)
.boundingBox({{ 0, 0, 0 },
{ 1, 1, 1 }})
.material(0, mMaterialInstanceWireFrame)
.geometry(0, RenderableManager::PrimitiveType::LINES, mVertexBuffer, mIndexBuffer, WIREFRAME_OFFSET, 24)
.priority(6)
.culling(culling)
.build(engine, mWireFrameRenderable);
}
从以上代码可以看出,使用构建者模式创建 VertexBuffer 和 IndexBuffer 缓存,再通过RenderableManager 的 geometry 方法构建 primitive, 一个geometry 对应一个primitive 对象。
VertexBuffer 类的接口,关键是看构造器类:
VertexBuffer
class UTILS_PUBLIC VertexBuffer : public FilamentAPI {
struct BuilderDetails;
public:
using AttributeType = backend::ElementType;
using BufferDescriptor = backend::BufferDescriptor;
class Builder : public BuilderBase<BuilderDetails> {
friend struct BuilderDetails;
public:
Builder() noexcept;
Builder(Builder const& rhs) noexcept;
Builder(Builder&& rhs) noexcept;
~Builder() noexcept;
Builder& operator=(Builder const& rhs) noexcept;
Builder& operator=(Builder&& rhs) noexcept;
/**
* Defines how many buffers will be created in this vertex buffer set. These buffers are
* later referenced by index from 0 to \p bufferCount - 1.
*
* This call is mandatory. The default is 0.
*
* @param bufferCount Number of buffers in this vertex buffer set. The maximum value is 8.
* @return A reference to this Builder for chaining calls.
*/
Builder& bufferCount(uint8_t bufferCount) noexcept;
/**
* Size of each buffer in the set in vertex.
*
* @param vertexCount Number of vertices in each buffer in this set.
* @return A reference to this Builder for chaining calls.
*/
Builder& vertexCount(uint32_t vertexCount) noexcept;
/**
* Sets up an attribute for this vertex buffer set.
*
* Using \p byteOffset and \p byteStride, attributes can be interleaved in the same buffer.
*
* @param attribute The attribute to set up.
* @param bufferIndex The index of the buffer containing the data for this attribute. Must
* be between 0 and bufferCount() - 1.
* @param attributeType The type of the attribute data (e.g. byte, float3, etc...)
* @param byteOffset Offset in *bytes* into the buffer \p bufferIndex
* @param byteStride Stride in *bytes* to the next element of this attribute. When set to
* zero the attribute size, as defined by \p attributeType is used.
*
* @return A reference to this Builder for chaining calls.
*
* @warning VertexAttribute::TANGENTS must be specified as a quaternion and is how normals
* are specified.
*
* @see VertexAttribute
*
* This is a no-op if the \p attribute is an invalid enum.
* This is a no-op if the \p bufferIndex is out of bounds.
*
*/
Builder& attribute(VertexAttribute attribute, uint8_t bufferIndex,
AttributeType attributeType,
uint32_t byteOffset = 0, uint8_t byteStride = 0) noexcept;
/**
* Sets whether a given attribute should be normalized. By default attributes are not
* normalized. A normalized attribute is mapped between 0 and 1 in the shader. This applies
* only to integer types.
*
* @param attribute Enum of the attribute to set the normalization flag to.
* @param normalize true to automatically normalize the given attribute.
* @return A reference to this Builder for chaining calls.
*
* This is a no-op if the \p attribute is an invalid enum.
*/
Builder& normalized(VertexAttribute attribute, bool normalize = true) noexcept;
/**
* Creates the VertexBuffer object and returns a pointer to it.
*
* @param engine Reference to the filament::Engine to associate this VertexBuffer with.
*
* @return pointer to the newly created object or nullptr if exceptions are disabled and
* an error occurred.
*
* @exception utils::PostConditionPanic if a runtime error occurred, such as running out of
* memory or other resources.
* @exception utils::PreConditionPanic if a parameter to a builder function was invalid.
*/
VertexBuffer* build(Engine& engine);
private:
friend class details::FVertexBuffer;
};
/**
* Returns the vertex count.
* @return Number of vertices in this vertex buffer set.
*/
size_t getVertexCount() const noexcept;
/**
* Asynchronously copy-initializes the specified buffer from the given buffer data.
*
* @param engine Reference to the filament::Engine to associate this VertexBuffer with.
* @param bufferIndex Index of the buffer to initialize. Must be between 0
* and Builder::bufferCount() - 1.
* @param buffer A BufferDescriptor representing the data used to initialize the buffer at
* index \p bufferIndex. BufferDescriptor points to raw, untyped data that will
* be copied as-is into the buffer.
* @param byteOffset Offset in *bytes* into the buffer at index \p bufferIndex of this vertex
* buffer set.
*/
void setBufferAt(Engine& engine, uint8_t bufferIndex, BufferDescriptor&& buffer,
uint32_t byteOffset = 0);
/**
* Specifies the quaternion type for the "populateTangentQuaternions" utility.
*/
enum QuatType {
HALF4, //!< 2 bytes per component as half-floats (8 bytes per quat)
SHORT4, //!< 2 bytes per component as normalized integers (8 bytes per quat)
FLOAT4, //!< 4 bytes per component as floats (16 bytes per quat)
};
/**
* Specifies the parameters for the "populateTangentQuaternions" utility.
*/
struct QuatTangentContext {
QuatType quatType; //!< desired quaternion type (required)
size_t quatCount; //!< number of quaternions (required)
void* outBuffer; //!< pre-allocated output buffer (required)
size_t outStride; //!< desired stride in bytes (optional)
const math::float3* normals; //!< source normals (required)
size_t normalsStride; //!< normals stride in bytes (optional)
const math::float4* tangents; //!< source tangents (optional)
size_t tangentsStride; //!< tangents stride in bytes (optional)
};
/**
* Convenience function that consumes normal vectors (and, optionally, tangent vectors) and
* produces quaternions that can be passed into a TANGENTS buffer.
*
* The given output buffer must be preallocated with at least quatCount * outStride bytes.
*
* Normals are required but tangents are optional, in which case this function tries to generate
* reasonable tangents. The given normals should be unit length.
*
* If supplied, the tangent vectors should be unit length and should be orthogonal to the
* normals. The w component of the tangent is a sign (-1 or +1) indicating handedness of the
* basis.
*
* @param ctx An initialized QuatTangentContext structure.
*
* @deprecated Instead please use filament::geometry::SurfaceOrientation from libgeometry, it
* has additional capabilities and a daisy-chain API. Be sure to explicitly link libgeometry
* since its dependency might be removed in future versions of libfilament.
*/
static void populateTangentQuaternions(const QuatTangentContext& ctx);
};
} // namespace filament
bufferCount(), vertexCount() , attribute() … ,下面说明这些接口表示的含义。
首先,我们看创建一个cube 顶点应该有哪些属性:
Vertex 属性:
Position0, Position1,Position2 … PositionN;
Color0, Color1, Color2 … ColorN;
Normal0, Normal1, Normal2… NormalN;
…
如果不考虑优化, 每个顶点属性可以关联一个VBO 缓存对象,按这种形式, 当我们创建 VertexBuffer 对象时,bufferCount(3) , 顶点个数为 VertexCount(N), 使用 attribute 接口描述顶点属性:
enum VertexAttribute : uint8_t {
// Update hasIntegerTarget() in VertexBuffer when adding an attribute that will
// be read as integers in the shaders
POSITION = 0, //!< XYZ position (float3)
TANGENTS = 1, //!< tangent, bitangent and normal, encoded as a quaternion (float4)
COLOR = 2, //!< vertex color (float4)
UV0 = 3, //!< texture coordinates (float2)
UV1 = 4, //!< texture coordinates (float2)
BONE_INDICES = 5, //!< indices of 4 bones, as unsigned integers (uvec4)
BONE_WEIGHTS = 6, //!< weights of the 4 bones (normalized float4)
// -- we have 1 unused slot here --
CUSTOM0 = 8,
CUSTOM1 = 9,
CUSTOM2 = 10,
CUSTOM3 = 11,
CUSTOM4 = 12,
CUSTOM5 = 13,
CUSTOM6 = 14,
CUSTOM7 = 15,
// Aliases for vertex morphing.
MORPH_POSITION_0 = CUSTOM0,
MORPH_POSITION_1 = CUSTOM1,
MORPH_POSITION_2 = CUSTOM2,
MORPH_POSITION_3 = CUSTOM3,
MORPH_TANGENTS_0 = CUSTOM4,
MORPH_TANGENTS_1 = CUSTOM5,
MORPH_TANGENTS_2 = CUSTOM6,
MORPH_TANGENTS_3 = CUSTOM7,
// this is limited by driver::MAX_VERTEX_ATTRIBUTE_COUNT
};
enum class ElementType : uint8_t {
BYTE,
BYTE2,
BYTE3,
BYTE4,
UBYTE,
UBYTE2,
UBYTE3,
UBYTE4,
SHORT,
SHORT2,
SHORT3,
SHORT4,
USHORT,
USHORT2,
USHORT3,
USHORT4,
INT,
UINT,
FLOAT,
FLOAT2,
FLOAT3,
FLOAT4,
HALF,
HALF2,
HALF3,
HALF4,
};
Builder& attribute(VertexAttribute attribute, uint8_t bufferIndex,
ElementType attributeType,
uint32_t byteOffset = 0, uint8_t byteStride = 0) noexcept;
可以看到VertexAttribute 是顶点属性的索引, bufferIndex 是 VBO 的索引,attributeType 是数据类型, byteOffset 是取数据的偏移量, byteStride 是相邻顶点数据的偏移量。 那么针对上面三个顶点属性,
attribute 属性可以这样设置:
attribute(POSITION, 0, FLOAT3, 0, 0);
attribute(COLOR, 1,FLOAT3, 0, 0);
attribute(TANGENTS, 2, FLOAT3, 0, 0);
....
filament 在创建VertexBuffer 时 如何解析并生成VBO :
FVertexBuffer::FVertexBuffer(FEngine& engine, const VertexBuffer::Builder& builder)
: mVertexCount(builder->mVertexCount), mBufferCount(builder->mBufferCount) {
std::copy(std::begin(builder->mAttributes), std::end(builder->mAttributes), mAttributes.begin());
mDeclaredAttributes = builder->mDeclaredAttributes;
uint8_t attributeCount = (uint8_t) mDeclaredAttributes.count();
AttributeArray attributeArray;
static_assert(attributeArray.size() == MAX_VERTEX_ATTRIBUTE_COUNT,
"Attribute and Builder::Attribute arrays must match");
static_assert(sizeof(Attribute) == sizeof(AttributeData),
"Attribute and Builder::Attribute must match");
auto const& declaredAttributes = mDeclaredAttributes;
auto const& attributes = mAttributes;
#pragma nounroll
for (size_t i = 0, n = attributeArray.size(); i < n; ++i) {
if (declaredAttributes[i]) {
attributeArray[i].offset = attributes[i].offset;
attributeArray[i].stride = attributes[i].stride;
attributeArray[i].buffer = attributes[i].buffer;
attributeArray[i].type = attributes[i].type;
attributeArray[i].flags = attributes[i].flags;
}
}
// Backends do not (and should not) know the semantics of each vertex attribute, but they
// need to know whether the vertex shader consumes them as integers or as floats.
// NOTE: This flag needs to be set regardless of whether the attribute is actually declared.
attributeArray[BONE_INDICES].flags |= Attribute::FLAG_INTEGER_TARGET;
FEngine::DriverApi& driver = engine.getDriverApi();
mHandle = driver.createVertexBuffer(
mBufferCount, attributeCount, mVertexCount, attributeArray, backend::BufferUsage::STATIC);
}
void OpenGLDriver::createVertexBufferR(
Handle<HwVertexBuffer> vbh,
uint8_t bufferCount,
uint8_t attributeCount,
uint32_t elementCount,
AttributeArray attributes,
BufferUsage usage) {
DEBUG_MARKER()
auto& gl = mContext;
GLVertexBuffer* vb = construct<GLVertexBuffer>(vbh,
bufferCount, attributeCount, elementCount, attributes);
GLsizei n = GLsizei(vb->bufferCount);
assert(n <= (GLsizei)vb->gl.buffers.size());
glGenBuffers(n, vb->gl.buffers.data());
for (GLsizei i = 0; i < n; i++) {
// figure out the size needed for each buffer
size_t size = 0;
for (auto const& item : attributes) {
if (item.buffer == i) {
size_t end = item.offset + elementCount * item.stride;
size = std::max(size, end);
}
}
gl.bindBuffer(GL_ARRAY_BUFFER, vb->gl.buffers[i]);
glBufferData(GL_ARRAY_BUFFER, size, nullptr, getBufferUsage(usage));
}
CHECK_GL_ERROR(utils::slog.e)
}
可以看到createVertexBufferR 根据attribute偏移属性,大小, 累加VBO 数组的大小,最终创建符合真正大小的VBO 数组。
每个 VertexBuffer 、 IndexBuffer 最终实例化的时候都会生成一个驱动相关的硬件资源。filament 为了实现并行渲染,解耦了渲染逻辑部分与调用图形API具体绘制部分,在CPU端调用的绘制函数时,只是生成硬件资源句柄,在渲染线程中才真正调用图形API 进行绘制。所以当你阅读代码时经常看到调用 CommandStream 做为绘制命令的转发接口。CommandStream对一般对图形API 的调用封装成两部分, 函数尾带S 只是创建并初始化硬件相关的资源,并将资源内存转换成句柄的形式返回,这一步在调用时立即执行。 当在渲染线程中执行渲染时,调用带R(reality?)的具体函数。
GLVertexBuffer 是 OpenGL 版本的VBO 版本:
struct HwVertexBuffer : public HwBase {
AttributeArray attributes{}; // 8 * MAX_VERTEX_ATTRIBUTE_COUNT
uint32_t vertexCount{}; // 4
uint8_t bufferCount{}; // 1
uint8_t attributeCount{}; // 1
uint8_t padding[2]{}; // 2 -> total struct is 136 bytes
HwVertexBuffer() noexcept = default;
HwVertexBuffer(uint8_t bufferCount, uint8_t attributeCount, uint32_t elementCount,
AttributeArray const& attributes) noexcept
: attributes(attributes),
vertexCount(elementCount),
bufferCount(bufferCount),
attributeCount(attributeCount) {
}
};
struct GLVertexBuffer : public backend::HwVertexBuffer {
using HwVertexBuffer::HwVertexBuffer;
struct {
// 4 * MAX_VERTEX_ATTRIBUTE_COUNT bytes
std::array<GLuint, backend::MAX_VERTEX_ATTRIBUTE_COUNT> buffers{}; // OpenGL VBOs...
} gl;
};
mHandle = driver.createVertexBuffer( mBufferCount, attributeCount, mVertexCount,
attributeArray, backend::BufferUsage::STATIC);
以上创建多个VBO缓存对应不同的顶点属性的方式,虽然意义明确、直观,但效率不高。一般我们会通过偏移量来创建交错数组的形式来创建顶点数据,这种形式更高效,而且减少 VBO的创建个数。
用交错数组的形式定义 VertexBuffer :
Vertex:
POSITION0, COLOR0, NORMAL0, POSITION1, COLOR1, NORMAL1....POSITIONN,COLORN, NORMALN
按这种形式, 当我们创建 VertexBuffer 对象时,bufferCount(1) , 顶点个数为 VertexCount(N), 使用 attribute 接口描述顶点属性:
attribute(POSITION, 0, FLOAT3, 0, sizeof(FLOAT3)*3);
attribute(COLOR, 0,FLOAT3, sizeof(FLOAT3), sizeof(FLOAT3)*3);
attribute(TANGENTS, 0, FLOAT3, sizeof(FLOAT3)*2, sizeof(FLOAT3)*3);
....
或者 做成分段数据的形式存储为一个VBO
POSITION0, COLOR0, NORMAL0, POSITION1, COLOR1, NORMAL1…POSITIONN,COLORN, NORMALN
P0, P1... PN, C0, C1... CN, N0, N1... NN
attribute(POSITION, 0, FLOAT3, 0, 0);
attribute(COLOR, 0,FLOAT3, color_offset, 0);
attribute(TANGENTS, 0, FLOAT3, normal_offset, 0);
....
IndexBuffer
IndexBuffer 的构建相对于VertexBuffer 的构建简单的多,因为IndexBuffer 只有一个数组,只要指定绘制顶点的个数和数据类型即可计算出ElementBuffer 的大小。
class UTILS_PUBLIC IndexBuffer : public FilamentAPI {
struct BuilderDetails;
public:
using BufferDescriptor = backend::BufferDescriptor;
/**
* Type of the index buffer
*/
enum class IndexType : uint8_t {
USHORT = uint8_t(backend::ElementType::USHORT), //!< 16-bit indices
UINT = uint8_t(backend::ElementType::UINT), //!< 32-bit indices
};
class Builder : public BuilderBase<BuilderDetails> {
friend struct BuilderDetails;
public:
Builder() noexcept;
Builder(Builder const& rhs) noexcept;
Builder(Builder&& rhs) noexcept;
~Builder() noexcept;
Builder& operator=(Builder const& rhs) noexcept;
Builder& operator=(Builder&& rhs) noexcept;
/**
* Size of the index buffer in elements.
* @param indexCount Number of indices the IndexBuffer can hold.
* @return A reference to this Builder for chaining calls.
*/
Builder& indexCount(uint32_t indexCount) noexcept;
/**
* Type of the index buffer, 16-bit or 32-bit.
* @param indexType Type of indices stored in the IndexBuffer.
* @return A reference to this Builder for chaining calls.
*/
Builder& bufferType(IndexType indexType) noexcept;
/**
* Creates the IndexBuffer object and returns a pointer to it. After creation, the index
* buffer is uninitialized. Use IndexBuffer::setBuffer() to initialized the IndexBuffer.
*
* @param engine Reference to the filament::Engine to associate this IndexBuffer with.
*
* @return pointer to the newly created object or nullptr if exceptions are disabled and
* an error occurred.
*
* @exception utils::PostConditionPanic if a runtime error occurred, such as running out of
* memory or other resources.
* @exception utils::PreConditionPanic if a parameter to a builder function was invalid.
*
* @see IndexBuffer::setBuffer
*/
IndexBuffer* build(Engine& engine);
private:
friend class details::FIndexBuffer;
};
/**
* Asynchronously copy-initializes a region of this IndexBuffer from the data provided.
*
* @param engine Reference to the filament::Engine to associate this IndexBuffer with.
* @param buffer A BufferDescriptor representing the data used to initialize the IndexBuffer.
* BufferDescriptor points to raw, untyped data that will be interpreted as
* either 16-bit or 32-bits indices based on the Type of this IndexBuffer.
* @param byteOffset Offset in *bytes* into the IndexBuffer
*/
void setBuffer(Engine& engine, BufferDescriptor&& buffer, uint32_t byteOffset = 0);
/**
* Returns the size of this IndexBuffer in elements.
* @return The number of indices the IndexBuffer holds.
*/
size_t getIndexCount() const noexcept;
};
以上构建出了VBO, EBO, 图元如何构建出来呢?我们看看RenderableManger 构建器geometry 接口:
Primitive
RenderableManager::Builder& RenderableManager::Builder::geometry(size_t index,
PrimitiveType type, VertexBuffer* vertices, IndexBuffer* indices) noexcept {
return geometry(index, type, vertices, indices,
0, 0, vertices->getVertexCount() - 1, indices->getIndexCount());
}
RenderableManager::Builder& RenderableManager::Builder::geometry(size_t index,
PrimitiveType type, VertexBuffer* vertices, IndexBuffer* indices,
size_t offset, size_t count) noexcept {
return geometry(index, type, vertices, indices, offset,
0, vertices->getVertexCount() - 1, count);
}
RenderableManager::Builder& RenderableManager::Builder::geometry(size_t index,
PrimitiveType type, VertexBuffer* vertices, IndexBuffer* indices,
size_t offset, size_t minIndex, size_t maxIndex, size_t count) noexcept {
std::vector<Entry>& entries = mImpl->mEntries;
if (index < entries.size()) {
entries[index].vertices = vertices;
entries[index].indices = indices;
entries[index].offset = offset;
entries[index].minIndex = minIndex;
entries[index].maxIndex = maxIndex;
entries[index].count = count;
entries[index].type = type;
}
return *this;
}
看上去比较直观,每个geometry 填充一个entry , 描述primitive 的绘制引用的vbo, ebo , 偏移量,绘制索引范围, 绘制顶点个数,绘制的图元类型。我们再看看entries 是如何展开的,那我们就去构造函数和驱动类driver 中去找具体的实现:
void FRenderPrimitive::init(backend::DriverApi& driver,
const RenderableManager::Builder::Entry& entry) noexcept {
assert(entry.materialInstance);
mHandle = driver.createRenderPrimitive();
mMaterialInstance = upcast(entry.materialInstance);
mBlendOrder = entry.blendOrder;
if (entry.indices && entry.vertices) {
FVertexBuffer* vertexBuffer = upcast(entry.vertices);
FIndexBuffer* indexBuffer = upcast(entry.indices);
AttributeBitset enabledAttributes = vertexBuffer->getDeclaredAttributes();
auto const& ebh = vertexBuffer->getHwHandle();
auto const& ibh = indexBuffer->getHwHandle();
driver.setRenderPrimitiveBuffer(mHandle, ebh, ibh, (uint32_t)enabledAttributes.getValue());
driver.setRenderPrimitiveRange(mHandle, entry.type,
(uint32_t)entry.offset, (uint32_t)entry.minIndex, (uint32_t)entry.maxIndex,
(uint32_t)entry.count);
mPrimitiveType = entry.type;
mEnabledAttributes = enabledAttributes;
}
}
驱动层创建图元接口的实现:
void OpenGLDriver::setRenderPrimitiveBuffer(Handle<HwRenderPrimitive> rph,
Handle<HwVertexBuffer> vbh, Handle<HwIndexBuffer> ibh,
uint32_t enabledAttributes) {
DEBUG_MARKER()
auto& gl = mContext;
if (rph) {
GLRenderPrimitive* const rp = handle_cast<GLRenderPrimitive*>(rph);
GLVertexBuffer const* const eb = handle_cast<const GLVertexBuffer*>(vbh);
GLIndexBuffer const* const ib = handle_cast<const GLIndexBuffer*>(ibh);
assert(ib->elementSize == 2 || ib->elementSize == 4);
gl.bindVertexArray(&rp->gl);
CHECK_GL_ERROR(utils::slog.e)
rp->gl.indicesType = ib->elementSize == 4 ? GL_UNSIGNED_INT : GL_UNSIGNED_SHORT;
rp->maxVertexCount = eb->vertexCount;
for (size_t i = 0, n = eb->attributes.size(); i < n; i++) {
if (enabledAttributes & (1U << i)) {
uint8_t bi = eb->attributes[i].buffer;
assert(bi != 0xFF);
gl.bindBuffer(GL_ARRAY_BUFFER, eb->gl.buffers[bi]);
if (UTILS_UNLIKELY(eb->attributes[i].flags & Attribute::FLAG_INTEGER_TARGET)) {
glVertexAttribIPointer(GLuint(i),
getComponentCount(eb->attributes[i].type),
getComponentType(eb->attributes[i].type),
eb->attributes[i].stride,
(void*) uintptr_t(eb->attributes[i].offset));
} else {
glVertexAttribPointer(GLuint(i),
getComponentCount(eb->attributes[i].type),
getComponentType(eb->attributes[i].type),
getNormalization(eb->attributes[i].flags & Attribute::FLAG_NORMALIZED),
eb->attributes[i].stride,
(void*) uintptr_t(eb->attributes[i].offset));
}
gl.enableVertexAttribArray(GLuint(i));
} else {
// In some OpenGL implementations, we must supply a properly-typed placeholder for
// every integer input that is declared in the vertex shader.
if (UTILS_UNLIKELY(eb->attributes[i].flags & Attribute::FLAG_INTEGER_TARGET)) {
glVertexAttribI4ui(GLuint(i), 0, 0, 0, 0);
} else {
glVertexAttrib4f(GLuint(i), 0, 0, 0, 0);
}
gl.disableVertexAttribArray(GLuint(i));
}
}
// this records the index buffer into the currently bound VAO
gl.bindBuffer(GL_ELEMENT_ARRAY_BUFFER, ib->gl.buffer);
CHECK_GL_ERROR(utils::slog.e)
}
}
void OpenGLDriver::setRenderPrimitiveRange(Handle<HwRenderPrimitive> rph,
PrimitiveType pt, uint32_t offset,
uint32_t minIndex, uint32_t maxIndex, uint32_t count) {
DEBUG_MARKER()
if (rph) {
GLRenderPrimitive* const rp = handle_cast<GLRenderPrimitive*>(rph);
rp->type = pt;
rp->offset = offset * ((rp->gl.indicesType == GL_UNSIGNED_INT) ? 4 : 2);
rp->count = count;
rp->minIndex = minIndex;
rp->maxIndex = maxIndex > minIndex ? maxIndex : rp->maxVertexCount - 1; // sanitize max index
}
}
注意 handle_cast 这个函数的妙用, 将句柄转换成资源内存地址!
最后我们看看filament 绘制时如何利用这些信息:
void OpenGLDriver::draw(PipelineState state, Handle<HwRenderPrimitive> rph) {
DEBUG_MARKER()
auto& gl = mContext;
OpenGLProgram* p = handle_cast<OpenGLProgram*>(state.program);
// If the material debugger is enabled, avoid fatal (or cascading) errors and that can occur
// during the draw call when the program is invalid. The shader compile error has already been
// dumped to the console at this point, so it's fine to simply return early.
if (FILAMENT_ENABLE_MATDBG && UTILS_UNLIKELY(!p->isValid())) {
return;
}
useProgram(p);
const GLRenderPrimitive* rp = handle_cast<const GLRenderPrimitive *>(rph);
gl.bindVertexArray(&rp->gl);
setRasterState(state.rasterState);
gl.polygonOffset(state.polygonOffset.slope, state.polygonOffset.constant);
setViewportScissor(state.scissor);
glDrawRangeElements(GLenum(rp->type), rp->minIndex, rp->maxIndex, rp->count,
rp->gl.indicesType, reinterpret_cast<const void*>(rp->offset));
CHECK_GL_ERROR(utils::slog.e)
}
这是多么熟悉的场景, 到此filament 创建图元的整个过程剖析完毕。有了这些知识,我们再去创建几何图元就轻车熟路了。
那么我们准备的几何数据是如何装填到VBO , EBO 这些缓存对象中去呢?
mVertexBuffer->setBufferAt(engine, 0,
VertexBuffer::BufferDescriptor(
mVertices, mVertexBuffer->getVertexCount() * sizeof(mVertices[0])));
mIndexBuffer->setBuffer(engine,
IndexBuffer::BufferDescriptor(
mIndices, mIndexBuffer->getIndexCount() * sizeof(uint32_t)));
VertexBuffer 可以根据顶点属性的不同创建多个缓冲区VBO,所以更新数据时,要指定更新哪个缓冲区,调用setBufferAt()方法。 索引缓冲区只有一个,直接调用 setBuffer() 方法即可。