渲染剔除技术
剔除技术
一、视锥体剔除
1.1 背景
视锥体剔除(Frustum Culling) 是一种基于相机可视范围的空间裁剪技术:相机在三维场景中形成一个四棱锥形状的可视区域即视锥体,只有位于该锥体内部的物体才会被渲染,外部物体直接跳过绘制;实现时通常先为物体计算包围球或包围盒,再通过矩阵运算将其与视锥体六个裁剪平面做相交检测,判断物体是否完全不可见,以此决定是否送入渲染管线;该技术的关键在于准确的包围体计算、高效的平面相交测试以及层级空间划分(如八叉树、BVH)以减少检测次数;其优点是实现简单、性能提升显著、几乎适配所有三维场景且无明显视觉损耗,缺点是无法处理物体被前方几何体遮挡的情况,仅能排除视野外对象,对密集遮挡场景优化有限,且复杂场景下单纯逐物体检测仍存在一定计算开销。

1.2 AABB包围盒
AABB(Axis-Aligned Bounding Box,轴对齐包围盒是三维图形中最常用的简单包围体,它是一个各边严格平行于世界坐标系或局部坐标系 X/Y/Z 轴的长方体盒子,用于包裹一个模型、网格或场景物体,以简化复杂几何体的空间判断;AABB 包围盒由 最小点 min(xmin, ymin, zmin)和最大点 max(xmax, ymax, zmax) 两个顶点定义,盒内所有点坐标都介于这两个极值之间。
1.3 BoundingShpere包围球
BoundingSphere是用于三维空间包围球计算的核心类,私有成员变量包含Small::Vec3 m_vCenter(包围球球心坐标)与float m_fRadius(包围球半径),用于存储包围球的基础几何信息;公共接口提供完整的包围球管理功能:默认构造与带参构造可初始化包围球,MakeEmpty和IsEmpty用于清空与判空操作,Center、Radius等方法可获取球心、半径、半径平方、直径等属性,SetCenter和SetRadius支持动态修改包围球参数,ExpandByPoint、ExpandBySphere、ExpandByAABB可分别通过点、包围球、轴对齐包围盒扩展当前包围球。 静态方法Union实现两个包围球的合并,Contains判断一个包围球是否完全包含另一个,Intersects检测两个包围球是否相交,FromAABB可由轴对齐包围盒生成适配的包围球,外部函数,TransformBoundingSphere则支持通过 4x4 矩阵对包围球进行空间变换,整体满足三维图形渲染、碰撞检测、视锥体裁剪等场景下的包围球运算需求。
1.4 BVH树
BVH 即层次包围盒树,是一种把空间物体用包围盒层层包裹、组织成二叉树结构的空间索引算法,目的是在碰撞、射线检测等查询时快速排除不相关对象,大幅减少计算量;而经典 Box2D BVH 是专门针对动态物理场景优化的动态 BVH,它以 AABB 包围盒包裹每个物体,自底向上构建二叉树,每个内部节点存储两个子节点的合并包围盒,查询时仅遍历与目标区域相交的分支,实现快速剔除,其核心优势是支持高效局部更新,物体移动时无需重建整棵树,仅更新受影响节点。 以 1000 个物体为例,构建时先为每个物体生成独立 AABB 作为叶子节点,再通过 Young-Latimer 分割策略,按节点包围盒最长轴对物体集合划分左右子集,同时结合 SAH 表面积启发式评估不同分割方式,选择使分割后子树总表面积最小、重叠最少的方案,逐层向上合并生成父节点直至形成根节点,最终完成树构建;Young-Latimer 分割侧重沿最长轴划分以保证树结构平衡,SAH 则通过量化包围盒表面积代价优化分割,减少查询时的无效相交检测。 相比传统离线 BVH,Box2D BVH 更新成本极低、适配动态物体,相较于 KD-Tree、八叉树与网格划分,它自适应空间分布,无固定分辨率限制,避免网格过大漏检或过小内存爆炸问题,查询效率更高,但在静态场景密集空间划分上,其构建开销略高于 KD-Tree,极端不规则分布下树的平衡性也可能略逊于严格空间分割的结构。
1.4.1 DynamicCullingBVH类
DynamicCullingBVH 是一个基于球体层次包围盒(Sphere-BVH)类,采用双包围体设计:叶节点同时存储精确的轴对齐包围盒(AABB)用于视锥剔除和保守的包围球(BoundingSphere)用于树构建与平衡,内部节点仅维护包围球以降低内存开销并简化 SAH 代价计算。核心功能包括对象插入InsertObject、基于 Fat-AABB 的惰性更新(UpdateObject仅在超出扩展边界时触发重插)、以及支持深度键计算的 GPU 友好型遍历ComputeNodeDepthKey。树维护采用自顶向下的 SAH 启发式插入(FindBestSibling)与 AVL 式旋转平衡(Balance),通过对象池(m_freeList)管理节点内存,支持运行时动态增删与批量重建,适用于大规模动态场景的视锥剔除和遮挡查询。
1.5 视锥体
1.5.1 视锥体生成
可以通过 View-Projection 矩阵的列线性组合提取视锥体 6 个裁剪平面,利用裁剪空间(clip space)到观察空间(view space)的变换关系:在齐次裁剪空间中,视锥体被变换为标准立方体 ,因此该立方体的 6 个边界平面在裁剪空间中的方程为、、,将这些边界条件通过 VP 矩阵的逆变换映射回世界空间,即得到世界空间中的 6 个锥面方程。具体实现上,将 VP 矩阵的第 3 列(对应w分量)分别与第 0/1/2 列(对应 x/y/z 分量)进行加减组合(例如左平面为 col3 + col0(即 w + x = 0)、右平面为 col3 - col0(即 w - x = 0))从而提取出左/右、下/上、近/远 6 个平面,其中法向量 norm 表示平面的单位朝向(指向视锥体内部为正),距离 distance 表示原点到平面的有符号距离(满足平面方程),两者共同定义了半空间测试的边界,用于后续的视锥体剔除(Frustum Culling)时判断物体 AABB 是否完全位于某一平面的外侧。

1.5.2 剔除算法
基于层次包围盒(BVH)的高效视锥体剔除算法,其核心流程采用非递归的深度优先遍历策略,通过父节点状态继承和子树快速标记优化剔除效率。算法首先获取 BVH 根节点并初始化显式栈,栈元素包含节点索引和父节点的视锥分类结果(Inside/Intersect/Outside)两种信息。遍历过程中,对于每个出栈节点,算法优先利用父节点状态进行快速决策:若父节点已完全位于视锥体内(Inside),则当前节点直接继承该状态而跳过昂贵的平面测试;若父节点完全在外(Outside),同样直接继承以加速剔除;仅当父节点与视锥体相交时,才调用 ClassifyAABBFrustum 对当前节点的 AABB 执行完整的 6 平面分离轴测试。 该测试通过 p-vertex/n-vertex 技巧分别计算盒体在平面法线方向的最远点和最近点,根据两点与平面的有符号距离判定整体在内、在外或相交。 对于分类结果,算法实施差异化处理:若节点为叶节点,则直接根据可见性状态(考虑配置是否启用反向剔除)设置对应Object可见标志;若节点为内部节点且判定为完全在内,则调用 MarkSubtreeVisible 快速标记整个子树所有叶节点可见,避免逐节点测试的线性开销;若为相交或在外状态,则将子节点压栈继续递归测试。这种设计充分利用了 BVH 的空间连贯性,通过父节点状态传播将 O(n) 的线性测试降为 O(log n) 的树遍历,同时 Inside 状态的子树短路机制显著减少了底层叶节点的平面测试次数,使其能够高效处理大规模动态场景的视锥体剔除需求。
二、小特征剔除
2.1 背景
小特征剔除是一种针对微小渲染对象的屏幕空间剔除技术,其技术背景源于远距离或视角下大量微小图元(如点、短线、细小模型)会产生大量无效绘制与像素闪烁问题,通过提前剔除可减少绘制压力。 该技术特点是基于物体包围体在屏幕上的投影尺寸判断可见性,而非空间位置或遮挡关系,属于轻量化剔除手段,不依赖复杂层级计算与 GPU 查询。实现的关键参数为剔除模式开关与屏幕像素阈值,通过设定最小像素尺寸控制剔除强度。相比视锥剔除仅判断空间范围、遮挡剔除依赖遮挡体与 GPU 查询、LOD 侧重多精度模型切换,其优点是计算开销极低、不占用额外渲染资源、对细小物体闪烁抑制效果明显,缺点是仅针对屏幕尺寸剔除、无法处理中等大小被遮挡物体,阈值设置不当易导致必要细小特征误剔除,且无法替代视锥与遮挡剔除的作用。
2.2 剔除算法
具体算法实现上:针对透视与正交两种投影类型的几何特性采用差异化的屏幕半径计算策略:透视投影遵循近大远小的透视规律,需先将球心变换到视图空间获取深度值,再利用相似三角形原理——屏幕半径与世界半径之比等于焦距(像素单位)与视图空间深度之比,因此通过反推投影矩阵得到垂直FOV并计算像素焦距 focalPixels = 0.5 * viewportHeight / tan(fovy/2),最终得到 screenRadius = radius / depth * focalPixels,深度越大屏幕半径越小;正交投影则无透视收缩效应,物体屏幕尺寸与深度无关,仅取决于正交投影的视锥体世界高度与视口像素高度的线性映射关系,因此通过投影矩阵元素 mat(1,1) = 2/(top-bottom) 反推世界高度 orthoHeightWorld = 2 / mat(1,1),再按 screenRadius = radius * (viewportHeight / orthoHeightWorld) 线性缩放,两种投影方式最终都将世界空间半径转换为统一的像素单位以便与阈值比较,实现投影无关的微小物体剔除。

三、遮挡剔除
3.1 硬件 Occlusion Query技术
3.1.1 背景
硬件 Occlusion Query(遮挡查询)利用 GPU 在光栅阶段统计某个物体(通常用其包围盒或简化代理几何)是否有片元通过深度测试,从而判断该物体是否被遮挡;实现步骤通常是:先正常渲染已知可见或“可能遮挡他物”的几何以填充深度缓冲,然后对待测物体关闭颜色写入、仅保留深度测试并开启查询(如 ANY_SAMPLES_PASSED),绘制其包围体,GPU 记录是否有片元通过,CPU 在随后一帧(或延迟若干帧以避免阻塞)读取结果并决定是否真正绘制该物体;优点是判定较精确、实现相对直接、可复用现有渲染管线;缺点是存在 GPU→CPU 同步开销(易造成 pipeline stall)、结果有帧延迟(可能引入闪烁)、当查询数量很大时开销显著(需要层级化/批处理缓解);适用场景包括中等规模场景、可接受一帧延迟的可见性判定、以及配合层级结构(BVH/Octree)的分层查询,而对于像 CAD 这类 10 万级细粒度对象的超大场景,通常需要与层级剔除或改用 Hi-Z 等方案结合以获得更稳定的性能。
3.1.2 实现流程
a. 深度预填充(Depth Pre-pass)
先画“已知可见 or 默认全部画一遍”(第一帧)
glColorMask(GL_FALSE, GL_FALSE, GL_FALSE, GL_FALSE);
glDepthMask(GL_TRUE);
glEnable(GL_DEPTH_TEST);
glDepthFunc(GL_LESS);
// 使用简单shader(只输出深度)
depthShader.Use();
for (auto& obj : objects)
{
if (obj.visible) // 第一帧可以全 true
{
glBindVertexArray(obj.vao);
glDrawElements(GL_TRIANGLES, obj.indexCount, GL_UNSIGNED_INT, 0);
}
}
//恢复颜色写入
glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE);
b. 对每个物体做 Occlusion Query(画包围体)
用包围盒,而不是原始 mesh
glDepthMask(GL_FALSE); // 不写深度(只测试)
for (auto& obj : objects)
{
glBeginQuery(GL_ANY_SAMPLES_PASSED, obj.queryId);
glBindVertexArray(obj.bboxVao);
glDrawElements(GL_TRIANGLES, obj.bboxIndexCount, GL_UNSIGNED_INT, 0);
glEndQuery(GL_ANY_SAMPLES_PASSED);
}
c. 读取上一帧 Query 结果(避免 stall)
不要直接 glGetQueryObject,用非阻塞方式
for (auto& obj : objects)
{
GLuint available = 0;
glGetQueryObjectuiv(obj.queryId, GL_QUERY_RESULT_AVAILABLE, &available);
if (available)
{
GLuint passed = 0;
glGetQueryObjectuiv(obj.queryId, GL_QUERY_RESULT, &passed);
obj.visible = (passed != 0);
}
// 如果没ready,保持上一帧visible
}
d. 根据结果决定是否绘制真实 mesh
正式渲染(只画可见物体)
glDepthMask(GL_TRUE);
glEnable(GL_DEPTH_TEST);
mainShader.Use();
for (auto& obj : objects)
{
if (!obj.visible)
continue;
glBindVertexArray(obj.vao);
glDrawElements(GL_TRIANGLES, obj.indexCount, GL_UNSIGNED_INT, 0);
}
3.2 软件遮挡剔除(Software Rasterizer)技术
3.2.1 背景
软件遮挡剔除(Software Rasterizer Occlusion Culling)是在 CPU 端用一个低分辨率深度缓冲(software depth buffer)模拟光栅化过程,将场景中“可能成为遮挡物”的几何(通常用简化网格或 AABB/OBB)投影并栅格化到该深度缓冲中,然后对待测物体的包围体进行深度测试(如投影后取屏幕矩形或采样点,与当前最小深度比较),从而判断其是否被遮挡;
实现步骤一般为:
① 对可见候选(通常先做 Frustum + BVH)按前到后排序;
② 清空一个较小分辨率(如 256×144/512×288)的 CPU 深度缓冲;
③ 将前景遮挡体逐个进行视图投影并在 CPU 上光栅化写入深度(可用三角形或保守光栅化的 AABB);
④ 对每个待测物体,将其包围体投影到屏幕,做块/点采样深度比较(若其最前深度大于缓冲中的深度则判为被遮挡);
⑤ 将通过测试的物体加入渲染列表并可选择继续更新深度以增强后续遮挡;
优点是完全避免 GPU→CPU 同步、结果稳定无帧延迟、易于多线程扩展、对 CAD 这类视角变化相对平滑的场景很友好;缺点是占用 CPU(尤其在高分辨率或复杂遮挡体下)、实现复杂度高(需要高效的投影与光栅化、SIMD/分块优化)、精度依赖分辨率与保守策略(可能过度剔除或漏剔);适用场景包括大规模静态或半静态场景(如 CAD/数字孪生)、需要确定性和稳定性的编辑器/工具链,以及 GPU 资源紧张或不希望引入查询延迟的系统,常与 BVH/层级剔除结合以控制成本。
3.2.2 实现流程
a. 定义数据结构
struct SWDepthBuffer
{
int width = 256;
int height = 144;
std::vector<float> depth;
void Init(int w, int h)
{
width = w;
height = h;
depth.resize(w * h);
}
void Clear()
{
std::fill(depth.begin(), depth.end(), 1.0f); // far depth
}
inline float& At(int x, int y)
{
return depth[y * width + x];
}
};
b. 实现投影函数
bool ProjectAABBToScreen(
const AxisAlignedBoundingBox& box,
const Mat44& viewProj,
int screenW,
int screenH,
ScreenRect& out)
{
Vec3 corners[8];
box.GetCorners(corners);
float minX = 1e9f, minY = 1e9f;
float maxX = -1e9f, maxY = -1e9f;
float minZ = 1e9f;
for (int i = 0; i < 8; ++i)
{
Vec4 clip = viewProj * Vec4(corners[i], 1.0f);
if (clip.w() <= 0.0f)
continue;
float invW = 1.0f / clip.w();
float ndcX = clip.x() * invW;
float ndcY = clip.y() * invW;
float ndcZ = clip.z() * invW;
minX = std::min(minX, ndcX);
minY = std::min(minY, ndcY);
maxX = std::max(maxX, ndcX);
maxY = std::max(maxY, ndcY);
minZ = std::min(minZ, ndcZ);
}
if (minX > maxX || minY > maxY)
return false;
// NDC → screen
out.x0 = int((minX * 0.5f + 0.5f) * screenW);
out.y0 = int((minY * 0.5f + 0.5f) * screenH);
out.x1 = int((maxX * 0.5f + 0.5f) * screenW);
out.y1 = int((maxY * 0.5f + 0.5f) * screenH);
out.minDepth = minZ * 0.5f + 0.5f;
return true;
}
c. 判断是否剔除并写入深度
bool IsOccluded(
const ScreenRect& rect,
const SWDepthBuffer& depth)
{
int x0 = std::max(0, rect.x0);
int y0 = std::max(0, rect.y0);
int x1 = std::min(depth.width - 1, rect.x1);
int y1 = std::min(depth.height - 1, rect.y1);
int visibleSamples = 0;
for (int y = y0; y <= y1; y += 2) // ↓ 降采样(性能关键)
{
for (int x = x0; x <= x1; x += 2)
{
if (rect.minDepth < depth.depth[y * depth.width + x])
{
return false; // 有一个点可见 → 不被遮挡
}
}
}
return true; // 全部被挡
}
void RasterizeRect(
const ScreenRect& rect,
SWDepthBuffer& depth)
{
int x0 = std::max(0, rect.x0);
int y0 = std::max(0, rect.y0);
int x1 = std::min(depth.width - 1, rect.x1);
int y1 = std::min(depth.height - 1, rect.y1);
for (int y = y0; y <= y1; ++y)
{
for (int x = x0; x <= x1; ++x)
{
float& d = depth.depth[y * depth.width + x];
d = std::min(d, rect.minDepth);
}
}
}
3.3 Hi-Z / 深度金字塔技术
3.3.1 背景
Hi-Z / 深度金字塔(Hierarchical Z-Buffer)遮挡剔除是在 GPU 上基于已渲染的深度缓冲构建一组逐级降采样的 mipmap 层级深度纹理(Hi-Z),每一层存储对应区域的“最小深度(或最大深度,取决于约定)”,从而可以用较少采样快速判断一个物体(通常用 AABB/包围球投影后的屏幕区域)是否完全被前方几何遮挡;
实现步骤通常为:
① 先进行深度预填充(Depth Pre-pass 或主渲染早期阶段)得到完整深度;
② 通过 compute shader 或多次 downsample 构建深度金字塔(每层为上一层的 2×2 合并);
③ 对待测物体将其包围体投影到屏幕得到矩形区域和最近深度;
④ 依据屏幕尺寸选择合适的 mip 层,在该层对覆盖区域进行少量采样并与 Hi-Z 深度比较;
⑤ 将可见结果写入可见列表(常配合 GPU Driven / Indirect Draw);
优点是完全在 GPU 侧完成、无 CPU-GPU 同步开销、可大规模并行、适合实例化和批处理、稳定性好;缺点是实现复杂(需要深度金字塔构建与采样策略)、对精度与保守性有权衡(可能轻微漏剔或过剔)、依赖较新的 GPU 管线能力(compute/纹理访问效率);适用场景包括大规模场景(如 CAD 大装配、开放世界)、需要高吞吐和低延迟的现代渲染管线(Forward+/Deferred、GPU Driven),尤其在物体数量达到数万至百万级时效果显著,是当前主流引擎(如 UE、Frostbite)常用的遮挡剔除方案。
3.3.2 实现流程
a. 深度预填充(Depth Pre-pass)
glBindFramebuffer(GL_FRAMEBUFFER, depthFBO);
glColorMask(GL_FALSE, GL_FALSE, GL_FALSE, GL_FALSE);
glDepthMask(GL_TRUE);
glEnable(GL_DEPTH_TEST);
glDepthFunc(GL_LESS);
depthShader.Use();
for (auto obj : candidates) // FrustumCull结果
{
DrawMesh(obj);
}
//恢复颜色写入
glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE);
b. 创建 Hi-Z 纹理
创建 mipmap depth texture
GLuint hizTex;
glGenTextures(1, &hizTex);
glBindTexture(GL_TEXTURE_2D, hizTex);
glTexImage2D(GL_TEXTURE_2D, 0, GL_R32F,
width, height, 0,
GL_RED, GL_FLOAT, nullptr);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST_MIPMAP_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
glGenerateMipmap(GL_TEXTURE_2D);
c. 构建 Hi-Z(金字塔)
用 FBO + fullscreen pass实现,降采样shader
#version 330 core
out float FragDepth;
uniform sampler2D u_depth;
uniform vec2 u_texelSize;
void main()
{
vec2 uv = gl_FragCoord.xy * u_texelSize;
float d0 = texture(u_depth, uv).r;
float d1 = texture(u_depth, uv + vec2(u_texelSize.x, 0)).r;
float d2 = texture(u_depth, uv + vec2(0, u_texelSize.y)).r;
float d3 = texture(u_depth, uv + u_texelSize).r;
// 取最小深度(更保守)
FragDepth = min(min(d0, d1), min(d2, d3));
}
构建mipmap
int mipWidth = width;
int mipHeight = height;
for (int level = 1; level < maxMipLevels; ++level)
{
mipWidth /= 2;
mipHeight /= 2;
glBindFramebuffer(GL_FRAMEBUFFER, fbo);
glFramebufferTexture2D(GL_FRAMEBUFFER,
GL_COLOR_ATTACHMENT0,
GL_TEXTURE_2D,
hizTex,
level);
glViewport(0, 0, mipWidth, mipHeight);
downsampleShader.Use();
glUniform1i(glGetUniformLocation(..., "u_depth"), 0);
glUniform2f(..., 1.0f / mipWidth, 1.0f / mipHeight);
glBindTexture(GL_TEXTURE_2D, hizTex);
glDrawArrays(GL_TRIANGLES, 0, 3); // fullscreen triangle
}
d. CPU 侧读取 Hi-Z
int mipLevel = 4;
int w = width >> mipLevel;
int h = height >> mipLevel;
std::vector<float> hizData(w * h);
glBindTexture(GL_TEXTURE_2D, hizTex);
glGetTexImage(GL_TEXTURE_2D, mipLevel, GL_RED, GL_FLOAT, hizData.data());
e. 判断是否剔除
bool IsOccludedHiZ(
const ScreenRect& rect,
const std::vector<float>& hiz,
int width, int height)
{
int x0 = std::max(0, rect.x0);
int y0 = std::max(0, rect.y0);
int x1 = std::min(width - 1, rect.x1);
int y1 = std::min(height - 1, rect.y1);
for (int y = y0; y <= y1; ++y)
{
for (int x = x0; x <= x1; ++x)
{
float depth = hiz[y * width + x];
if (rect.minDepth <= depth)
{
return false; // 有一个点可见
}
}
}
return true;
}
3.4 GPU Culling技术
3.4.1 背景
GPU Driven(Indirect Draw + Occlusion)方案是将可见性判定与绘制调度从 CPU 转移到 GPU,由 Compute Shader 在 GPU 侧并行完成 Frustum/Hi-Z 遮挡剔除,并直接生成 间接绘制命令(Indirect Draw Command Buffer),随后通过 glDraw*Indirect 一次或少量调用驱动整批物体渲染;
实现步骤通常为:
① CPU 仅上传静态/半静态数据(实例数据、包围体、变换、BVH/Cluster 索引等)到 SSBO;
② 进行 Depth Pre-pass 并构建 Hi-Z(深度金字塔);
③ 运行 Compute Shader,对每个实例/cluster 执行 Frustum + Hi-Z 测试,将通过的实例写入可见列表或直接填充 Indirect Command(同时用原子计数器/前缀和控制写入位置);
④(可选)对可见列表做排序/分桶(如不透明前到后、按材质分组);
⑤ 调用 glMultiDraw*Indirect 批量渲染;
优点是几乎消除 CPU-GPU 同步与 draw call 开销、可大规模并行(适合 10万~百万实例)、与 Instancing/Cluster/Meshlet 高度契合、吞吐量和稳定性最佳;缺点是实现复杂(需要 Compute/SSBO/同步屏障/调试困难)、对硬件与 API 要求高(OpenGL 4.3+/Vulkan/D3D12)、在 GL3.3 等旧管线下难以完整落地;适用场景包括超大规模场景(CAD 大装配、开放世界、海量实例化植被/城市)、现代引擎的 GPU Driven 管线(Forward+/Deferred + Hi-Z),以及对 CPU 预算敏感、需要极低 draw call 的高性能渲染系统。
3.4.2 实现流程
a. 定义数据结构,创建实例
struct InstanceData
{
glm::mat4 model;
glm::vec4 bounding; // xyz = center, w = radius
uint meshIndex; // 0=cube, 1=sphere
};
struct DrawElementsIndirectCommand
{
uint count;
uint instanceCount;
uint firstIndex;
uint baseVertex;
uint baseInstance;
};
std::vector<InstanceData> instances;
for (int i = 0; i < 10; ++i)
{
InstanceData d{};
d.model = glm::translate(glm::mat4(1.0f), glm::vec3(i * 2.0f, 0, 0));
d.bounding = glm::vec4(i * 2.0f, 0, 0, 1.0f); // 半径1
d.meshIndex = 0;
instances.push_back(d);
}
for (int i = 0; i < 5; ++i)
{
InstanceData d{};
d.model = glm::translate(glm::mat4(1.0f), glm::vec3(i * 2.0f, 3, 0));
d.bounding = glm::vec4(i * 2.0f, 3, 0, 1.0f);
d.meshIndex = 1;
instances.push_back(d);
}
b. 创建SSBO和Indirect Buffer
GLuint instanceSSBO;
glGenBuffers(1, &instanceSSBO);
glBindBuffer(GL_SHADER_STORAGE_BUFFER, instanceSSBO);
glBufferData(GL_SHADER_STORAGE_BUFFER,
instances.size() * sizeof(InstanceData),
instances.data(),
GL_STATIC_DRAW);
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 0, instanceSSBO);
GLuint indirectBuffer;
glGenBuffers(1, &indirectBuffer);
glBindBuffer(GL_DRAW_INDIRECT_BUFFER, indirectBuffer);
glBufferData(GL_DRAW_INDIRECT_BUFFER,
sizeof(DrawElementsIndirectCommand) * 15,
nullptr,
GL_DYNAMIC_DRAW);
c. 创建可见计数(Atomic Counter)
GLuint atomicBuffer;
glGenBuffers(1, &atomicBuffer);
glBindBuffer(GL_ATOMIC_COUNTER_BUFFER, atomicBuffer);
glBufferData(GL_ATOMIC_COUNTER_BUFFER, sizeof(GLuint), nullptr, GL_DYNAMIC_DRAW);
glBindBufferBase(GL_ATOMIC_COUNTER_BUFFER, 1, atomicBuffer);
d. 实现compute shader
#version 430
layout(local_size_x = 64) in;
struct InstanceData
{
mat4 model;
vec4 bounding; // xyz center, w radius
uint meshIndex;
};
struct DrawCmd
{
uint count;
uint instanceCount;
uint firstIndex;
uint baseVertex;
uint baseInstance;
};
layout(std430, binding = 0) buffer Instances
{
InstanceData instances[];
};
layout(std430, binding = 2) buffer Commands
{
DrawCmd cmds[];
};
layout(binding = 1, offset = 0) uniform atomic_uint visibleCount;
// Hi-Z
uniform sampler2D hizTex;
// Camera
uniform mat4 viewProj;
bool FrustumTest(vec3 center, float radius)
{
vec4 clip = viewProj * vec4(center, 1.0);
float w = clip.w;
return abs(clip.x) <= w + radius &&
abs(clip.y) <= w + radius &&
clip.z >= -radius &&
clip.z <= w + radius;
}
// 简化版 Hi-Z(只查 mip0)
bool Occluded(vec3 center, float radius)
{
vec4 clip = viewProj * vec4(center, 1.0);
vec3 ndc = clip.xyz / clip.w;
vec2 uv = ndc.xy * 0.5 + 0.5;
float depth = texture(hizTex, uv).r;
return depth < ndc.z - 0.001;
}
void main()
{
uint id = gl_GlobalInvocationID.x;
if (id >= instances.length()) return;
InstanceData inst = instances[id];
if (!FrustumTest(inst.bounding.xyz, inst.bounding.w))
return;
if (Occluded(inst.bounding.xyz, inst.bounding.w))
return;
uint index = atomicCounterIncrement(visibleCount);
cmds[index].count = (inst.meshIndex == 0) ? 36 : 240; // cube/sphere
cmds[index].instanceCount = 1;
cmds[index].firstIndex = 0;
cmds[index].baseVertex = 0;
cmds[index].baseInstance = id;
}
e. 构建Hi-Z
创建 mipmap depth texture
GLuint hizTex;
glGenTextures(1, &hizTex);
glBindTexture(GL_TEXTURE_2D, hizTex);
glTexStorage2D(GL_TEXTURE_2D, 5, GL_R32F, width, height);
从 depth buffer 拷贝
glBindTexture(GL_TEXTURE_2D, hizTex);
glCopyTexImage2D(GL_TEXTURE_2D, 0, GL_DEPTH_COMPONENT32F, 0, 0, width, height, 0);
生成 mipmap
glGenerateMipmap(GL_TEXTURE_2D);
f. 执行compute shader和最终绘制
// 清零 atomic
GLuint zero = 0;
glBindBuffer(GL_ATOMIC_COUNTER_BUFFER, atomicBuffer);
glBufferSubData(GL_ATOMIC_COUNTER_BUFFER, 0, sizeof(GLuint), &zero);
// 绑定
glUseProgram(computeProgram);
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 0, instanceSSBO);
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 2, indirectBuffer);
glBindBufferBase(GL_ATOMIC_COUNTER_BUFFER, 1, atomicBuffer);
glBindTextureUnit(0, hizTex);
// Dispatch
glDispatchCompute((15 + 63) / 64, 1, 1);
// 同步
glMemoryBarrier(GL_COMMAND_BARRIER_BIT | GL_SHADER_STORAGE_BARRIER_BIT);
glBindBuffer(GL_DRAW_INDIRECT_BUFFER, indirectBuffer);
// cube VAO
glBindVertexArray(cubeVAO);
glMultiDrawElementsIndirect(
GL_TRIANGLES,
GL_UNSIGNED_INT,
0,
15,
0
);

浙公网安备 33010602011771号