OpenGL GLFW OIT 实现

OIT

LearnOpenGL - IntroductionLearn OpenGL . com provides good and clear modern 3.3+ OpenGL tutorials with clear examples. A great resource to learn modern OpenGL aimed at beginners.https://learnopengl.com/Guest-Articles/2020/OIT/Introduction

这里主要是介绍了两类实现透明物体混合的方法。

  • 基于排序的透明渲染 ordered transparency
  • 顺序无关的透明渲染 order-independent transparency

Odered Transparency

顾名思义,这种混合方法就以物体为单位,先利用CPU对画面中的目标进行排序,然后从远往近逐个渲染目标,早期的3D游戏很多都是使用这种方式来实现混合。但如果画面中的物体数量非常庞大的时候,在每一帧都用CPU在渲染前先对所有对象做一次排序,渲染性能下降会很明显。同时,在自遮挡、互相遮挡等情况下,混合依然会出现问题。

👇下面这种情况就是以立方体为单位排序后按序渲染,当发生互相遮挡的情况时,结果就出错了。

Order-Independent Transparency

为了提升混合速度和效果,就不能再用CPU排序的简单方法实现混合,这类不使用CPU排序混合的方法可以称作为OIT,顺序无关只是说不需要在渲染前用CPU排序了,倒不是意味着真的不考虑渲染的顺序了。又根据具体实现的方法可以分成两个大类,精准OIT和近似OIT。这里只实现以下精准OIT。

Exact OIT

exact OIT通常实现是通过在GPU中对片元进行排序来准确计算正确的混合结果。排序阶段需要在着色器中使用相对较大的临时内存,而这些内存通常会被保守地分配到最大值。经典的exact OIT方法有Depth Peeling,Per Pixel Linked List。

Depth Peeling

Depth Peeling的介绍在这:OpenGL GLFW 深度/模版测试 面剔除 混合与帧缓冲-CSDN博客,效果如下。

Per Pixel Linked List

参考: vulkan_顺序无关的半透明混合(OIT)_adaptive transparency opengl-CSDN博客

Depth Peeling中剥离几次就需要几个PASS,如果能够在一个PASS中直接准备好排序需要的所有内容,就可以节省掉后续几个PASS的性能消耗。Per Pixel Linked List通过静态链表实现在一个PASS中,为每个像素维护一个链表,再对每个像素的链表排序,以此正确混合颜色。

  • 使用SSBO传输静态链表到GPU中并在着色器中修改(UBO在着色器中是只读的,虽然访问速度更快,但有大小限制,通常的UBO的大小在16KB,所以不能用在这里),在我的机器上SSBO的最大容量是2048MB。
  • SSBO中使用到结构体时需要注意字节对齐,字节对齐的一个重要原因是为了使机器访问更迅速。例如我用的内存布局方式为std430,结构体中有三个变量,vec4,float,int。vec4的需要4N对齐(基础值N为4个字节),float和int的对齐基数为N,可知float和int总是字节对齐的,而vec4没有16字节对齐,如果直接使用就会如下图一样出错。所以还需要增加8个字节的数据作填充。对于其他布局方式,如std140,同理参考对应的字节对齐要求即可。
  • SSBO更具体的部分可看interface block 及 UBO、SSBO 详解 -- gleam
  • GLSL 4.30版本新增了SSBO原子操作函数 ,后者的出现就是由于在shader可以对SSBO执行写入操作,而GPU是并行计算的,可想而知如果对SSBO中的数据写入没有保证原子性,数据肯定就全乱了套了。对此可以使用4.30及以上版本提供的原子性函数操作SSBO中的数据,atomicAdd, atomicAnd, atomicOr, atomicXor, atomicMin, atomicMax, atomicCompSwap
  • 静态数组需要预分配足够的空间,对1000*800的分辨率设置的静态数组大小为3倍分辨率大小时,静态数组需要32 * 1000 * 800 * 3 = 76,800,000字节,也就是73.24MB的内存空间。VS2019中默认栈大小为1MB,只能在堆中为静态数组分配内存。静态数组设置太大会影响到性能,太小可能无法应对复杂的混合场景。
  • 注意每帧开始时都需要清空静态数组。通过glMapBufferRange映射缓冲区对象实现。
  • gl_FragCoord.xy表示的是像素的中心位置,例如最左下角的像素就为(0.5,0.5),如果需要,可以设置布局限定词为pixel_center_integer,会将坐标设置为整数值,使得片段的中心精确地对应到整数坐标。Layout Qualifier (GLSL) - OpenGL Wiki
  • 实现用了三个SSBO,一个用作静态链表,存储PixelNode。一个大小等同于分辨率,存储每个像素在链表中的首个元素的位置。一个用于设置当前计数,被所有fragment shader共享,用来选择链表中的空闲结点。

生成像素链表的片元着色器:

cpp 复制代码
#version 430 core

layout(pixel_center_integer) in vec4 gl_FragCoord;

struct PixelNode {
    vec4 rgba;
    float depth;
    int next;
    float padding[2];
};

layout(std430, binding = 0) buffer PixelBuffer{
    PixelNode node[];
};

layout(std430, binding = 1) buffer PixelCount{
    int curCount;
    int maxCount;
};

layout(std430, binding = 2) buffer StartIndexTexture{
    int startIndexTexture[];
};

out vec4 FragColor;

uniform vec4 Color;
uniform int width;
uniform int height;

void main(){
    int curIndex = atomicAdd(curCount, 1);
	if(curIndex < maxCount){
        int oldind = atomicExchange(startIndexTexture[int(gl_FragCoord.x) * height + int(gl_FragCoord.y)], curIndex);
        node[curIndex].rgba = Color;
        node[curIndex].depth = gl_FragCoord.z;
        node[curIndex].next = oldind;
    }
    FragColor = vec4(1.0, 1.0, 1.0, 1.0);
}

生成像素链表的顶点着色器:

cpp 复制代码
#version 430 core

layout(location = 0) in vec3 aPos;
layout(location = 1) in vec2 aUV;

uniform mat4 model;
uniform mat4 view;
uniform mat4 projection;

void main(){
	gl_Position = projection * view * model * vec4(aPos, 1.0);
}

排序像素链表并混合颜色输出的片元着色器:

cpp 复制代码
#version 430 core
#define MAX_TREANSPARNTS_LAYER 128

layout(pixel_center_integer) in vec4 gl_FragCoord;

struct PixelNode {
    vec4 rgba;
    float depth;
    int next;
    float padding[2];
};

in vec2 oTexture;
out vec4 FragColor;

uniform sampler2D colorTexture;
uniform sampler2D depthTexture;
uniform int width;
uniform int height;

layout(std430, binding = 0) buffer PixelBuffer{
    PixelNode node[];
};

layout(std430, binding = 2) buffer StartIndexTexture{
    int startIndexTexture[];
};

void main(){
	vec4 color = texture(colorTexture, oTexture);
	float depth = texture(depthTexture, oTexture).x;
    int ind = startIndexTexture[int(gl_FragCoord.x) * height + int(gl_FragCoord.y)];
    int plength = 0;

    if(ind != -1){
        PixelNode fragment[MAX_TREANSPARNTS_LAYER];
        while(ind != -1){
            PixelNode cNode = node[ind];
            ind = cNode.next;
            plength += 1;
            for(int i = 0; i < plength; ++i){
                if(i == plength - 1) fragment[i] = cNode;
                else if(fragment[i].depth < cNode.depth){
                    for(int j = plength - 1; j > i; --j){
                        fragment[j] = fragment[j - 1];
                    }
                    fragment[i] = cNode;
                    break;
                }
            }
        }
        for(int i = 0; i < plength; ++i){
            if(fragment[i].depth >= depth) continue;
            color = mix(color, fragment[i].rgba, fragment[i].rgba.a);
        }
    }

	FragColor = vec4(color.rgb, 1.0);
}

排序像素链表并混合颜色输出的顶点着色器:

cpp 复制代码
#version 430 core

layout(location = 0) in vec2 aPos;
layout(location = 1) in vec2 aTexture;

out vec2 oTexture;

void main(){
	oTexture = aTexture;
	gl_Position = vec4(aPos, 0.0, 1.0);
}

附上Per-Pixel Linked List的渲染效果:

教训😇:因为一个uniform值没传进去,导致结果出大问题,还误以为是自己的程序处理的不对。

相关推荐
dayouziei40 分钟前
java的类加载机制的学习
java·学习
捕鲸叉2 小时前
创建线程时传递参数给线程
开发语言·c++·算法
A charmer2 小时前
【C++】vector 类深度解析:探索动态数组的奥秘
开发语言·c++·算法
Peter_chq2 小时前
【操作系统】基于环形队列的生产消费模型
linux·c语言·开发语言·c++·后端
aloha_7893 小时前
从零记录搭建一个干净的mybatis环境
java·笔记·spring·spring cloud·maven·mybatis·springboot
青花瓷4 小时前
C++__XCode工程中Debug版本库向Release版本库的切换
c++·xcode
dsywws4 小时前
Linux学习笔记之vim入门
linux·笔记·学习
晨曦_子画5 小时前
3种最难学习和最容易学习的 3 种编程语言
学习
UTwelve5 小时前
【UE5】一种老派的假反射做法,可以用于移动端,或对反射的速度、清晰度有需求的地方
ue5·虚幻引擎·着色器·虚幻4
幺零九零零5 小时前
【C++】socket套接字编程
linux·服务器·网络·c++