-
什么是立即渲染模式/固定管线渲染和核心模式,区别是什么
-
立即渲染模式 / 固定管线 (Immediate Mode / Fixed-Function Pipeline) 这是早期旧版的OpenGL工作方式,开发者调用
glBegin/glEnd之类的函数,CPU逐个地将顶点数据和绘图指令发送给GPU。它的渲染流程是固定的,开发者只能通过API调用来配置光照、变换等有限的功能。这种方式易于上手,但效率很低,现已废弃。 -
核心模式 (Core Profile Mode) 是现代OpenGL(3.3版本及以后)的工作方式。它废弃了所有旧的固定管线功能,将渲染管线的控制权完全交给了开发者。开发者必须自己管理顶点数据(使用VBO、VAO),并编写着色器(Shaders)程序来精确控制顶点如何处理、像素如何着色。这种方式更灵活、性能更高。
-
区别:
- 控制权:固定管线的功能是预设好的,控制权在OpenGL API手中;核心模式通过可编程的着色器,将控制权完全交给了开发者。
- 性能:立即模式需要频繁的CPU到GPU通信,性能差;核心模式将数据批量发送到GPU并由GPU上的着色器程序处理,性能极高。
- 开发方式:固定管线是调用一系列固定功能的API;核心模式是数据管理(缓冲区对象)+ 编写着色器程序。
-
-
什么是状态机
状态机 (State Machine) 是一个抽象的数学模型,用来描述一个系统可以处于的所有状态,以及是什么事件或条件导致它从一个状态转换到另一个状态。
有限状态机 (Finite State Machine, FSM) 是状态机的一种,其核心特点是它所拥有的状态数量是有限的。它是实际应用中最常见的状态机类型。
-
OpenGL中的对象是什么
由OpenGL在内部管理的一块数据和状态的集合,不是C++语言层面上的对象,而更像一个句柄 (Handle) 或引用,通常用一个无符号整数(
GLuint)来表示。 -
GLFW,GLAD,OPENGL分别起什么作用
OpenGL (Open Graphics Library)
- 作用:图形渲染规范。
- 它是一个跨平台的图形API(应用程序编程接口),定义了上百个函数,用于告诉图形硬件(GPU)如何绘制2D和3D场景。它只负责纯粹的渲染计算,不关心窗口创建、键盘鼠标输入等任何与操作系统相关的事情。
GLFW (Graphics Library Framework)
-
作用:窗口和输入管理。
-
它是一个辅助库,专门处理OpenGL没有覆盖的操作系统层面的任务。主要工作包括:
- 创建和管理窗口。
- 创建并绑定OpenGL上下文(让OpenGL有地方可画)。
- 处理键盘、鼠标、游戏手柄的输入事件。
GLAD
- 作用:OpenGL函数地址加载器。
- OpenGL只是一个规范,具体的函数实现是由显卡驱动提供的。在程序运行时,需要向驱动查询这些函数的内存地址才能调用它们。GLAD就是这样一个工具,它能在程序初始化时自动完成这个查询和加载过程,让你可以在代码中直接调用
glGenBuffers等现代OpenGL函数。
总结:使用 GLFW 创建一个窗口,然后 GLAD 负责加载好驱动里的OpenGL函数,最后通过调用这些 OpenGL 函数在窗口里进行绘图
-
shader中如何丢弃片段
片元着色器中使用关键字
discard -
如何实现批处理,动态/静态,区别是什么
批处理是一种图形渲染中的核心优化技术。它的主要目标是减少绘图调用(Draw Call)的数量,批处理的思想就是将多个材质和顶点属性相同或相近的小物体的绘制请求,在CPU端合并成一个或少数几个大的绘制请求,然后一次性发给GPU,从而显著降低开销.
静态批处理 (Static Batching)
- 适用对象:用于场景中永远不会移动、旋转或缩放的物体,例如建筑、山脉、地面、某些装饰物等。
- 工作原理:在游戏加载或场景初始化时,它会找到所有标记为“静态”并且使用相同材质的物体,将它们的网格(Mesh)合并成一个巨大的网格。这个过程包括将所有物体的顶点坐标从局部空间转换到世界空间。之后,这些原本独立的物体就可以通过一个Draw Call全部绘制出来。
- 特点:一次性投入,后续渲染效率极高。主要缺点是会增加内存占用,因为如果一个模型被多次使用,合并后它的顶点数据也会在内存中存在多份。
动态批处理 (Dynamic Batching)
- 适用对象:用于那些会移动,但顶点数量很少的小物体,例如粒子效果、金币、子弹等。
- 工作原理:它在每一帧都会自动地、动态地将使用相同材质的小物体组合在一起。这个过程在CPU上完成,CPU需要计算每个物体变换后的顶点位置,然后把它们填充到一个共享的动态顶点缓冲区(Dynamic VBO)中,最后发起一个Draw Call。
- 特点:能为移动的小物体减少Draw Call。主要缺点是对CPU消耗较大,因为合并操作是每帧都在进行的,并且它只对顶点数很少的物体有效,否则CPU合并的开销会超过它带来的收益。
实现:
静态批处理实现:
- 确定要批处理的、使用相同材质的静态物体列表。
- 创建一个新的、空的“超级网格”(Mega Mesh)数据结构。
- 遍历列表中的每个物体: a. 获取其网格的顶点和索引数据。 b. 将其每个顶点坐标乘以该物体的世界变换矩阵,变换到世界空间。 c. 将变换后的顶点数据和调整过的索引数据追加到“超级网格”中。
- 根据这个“超级网格”创建一个最终的VBO和IBO。
- 在渲染循环中,只需要绑定这个VBO/IBO并调用一次Draw Call即可。
动态批处理实现:
- 在每一帧的开始,准备一个足够大的、可动态写入的VBO。
- 遍历场景中所有使用相同材质的可见小物体。
- 在CPU端,计算每个物体的模型-视图-投影矩阵。
- 将物体的顶点数据在CPU上进行变换,然后把结果依次拷贝到动态VBO的当前可用位置。
- 记录下所有拷贝进去的顶点数量。
- 最后调用一次Draw Call,绘制VBO中这一帧填充好的那部分数据。
-
描述一下OpenGL的渲染管线的各阶段
...TOO MUCH
-
什么是MipMap,有什么用,在放大时候用还是缩小时候用
多级渐远纹理,指的是一系列预先计算好的、分辨率由高到低逐级递减的纹理图像集合,随着物体与相机的距离越远,采用不同级别的纹理图(一般是上一级图像的1/4大小),目的是为了解决当相机离得物体很远时,物体所占的分辨率变低,但是仍采样原本的大的纹理图带来的不真实的效果(闪烁,锯齿,摩尔纹等采样失真),并且当物体很远时,GPU从一个尺寸更小、更合适的Mipmap纹理中读取数据,而不是从巨大的原始纹理中读取。这能极大地提高GPU纹理缓存的命中率,加快渲染速度。
MipMap仅被用于纹理缩小的情况,应用于纹理放大是没有意义的
-
OpenGL中的shader的数据类型有哪些
Scalars:int,float,uint ,bool
Vectors:i/u/b .. vec .. 2/3/4
Matrices:mat2/3/4
Samplers:sampler1/2/3D,samplerCube,samplerShadow
-
类似于sampler2D,这是一个什么类型(采样器句柄)
一种不透明类型 (Opaque Type) ,可以理解为一个句柄或引用,它代表了对一个2D纹理对象的访问权限。你可以通过这个变量,结合
texture()函数,在着色器中对外部(由C++代码绑定的)2D纹理进行采样,也就是根据纹理坐标读取特定点的颜色 -
什么是前向渲染和延迟渲染(GBO,GeoPass/Pass)
-
前向渲染
以物体为中心,对场景中的每个物体进行渲染计算,包括光照,例如渲染一个物体,渲染器会遍历所有能影响到物体的光源并计算光源对该物体每个像素的颜色贡献,然后应用颜色并写入像素
优点:流程简单清晰,物体按顺序逐个绘制,可以进行Alpha混合,每个物体都可以有不同的shader和光照模型,
缺点:光源数量增多会导致性能急剧下降
-
延迟渲染
为了解决前向渲染光照性能瓶颈,延迟光照计算,分为两个Pass,几何Pass和光照Pass,
- 几何Pass:遍历场景中不透明物体,不进行任何光照计算,将渲染每个像素所需要的几何信息()存储在一系列屏幕大小的纹理中,这些纹理集合成为几何缓冲区GBuffer
- 光照Pass:几何阶段结束后,GBuffer中包含屏幕上每个可见像素的全部几何信息,遍历场景中的所有光源,对于每个光源的光体积(代表光源影响范围的球体),对该范围内的像素,从GBuffer中读取数据进行光照计算并叠加得到最终颜色
优点:光照性能强,光照计算复杂度与物体数量无关,只于分辨率有关,适合大量动态光源场景,无重复光照计算
缺点:难处理透明物体的混合,因为GBuffer只存储了最终像素的信息,所以需要额外结合前向渲染处理,显存占用较大,读写带宽需求也大,材质种类受限, 所有物体的输出需要符合GBuffer格式,对于头发,皮肤等特殊材质不如前向渲染灵活,无法使用MSAA,MSAA多重采样涉及到单个像素多采样点,逐个采样点(4x)对于几何阶段会使得显存带宽加倍(x4) ,光照阶段对于每个字采样点也会计算导致计算量变多
-
-
shader是什么,会不会写shader,shader的关键字有哪些
shader是着色器,指的是在GPU上为管线的每个阶段运行的小程序,对于顶点和片段着色器是必须提供的着色器,几何着色器可选,关键字例如:uniform(统一变量,全局只读),in(输入变量),out(输出变量),还有一些内建变量:(gl_Position,gl_FragCoord等)
-
什么是双缓冲
使用两个独立的图像缓冲区解决渲染过程中的视觉瑕疵,有前后两个缓冲,前缓冲存储当前显示在屏幕上的衣服完整的图像,后缓冲在内存中,绘制操作都将绘制在这个缓冲区中,工作流程:
- 程序在后台缓冲区中将一整帧画面完全绘制好。
- 在这一帧绘制完成之后,程序会发出一个指令,将前台缓冲区和后台缓冲区进行交换 (Swap) 。
- 这个交换操作通常非常快,(在概念上)后台缓冲区瞬间变为新的前台缓冲区,而旧的前台缓冲区则变为新的后台缓冲区,等待绘制下一帧。
- 因为交换是瞬间完成的,所以用户看到的永远是一幅完整的图像,从一帧到下一帧的过渡是无缝的,这就避免了只用一个缓冲区时,因看到“正在绘制中”的不完整图像而产生的屏幕撕裂 (Tearing) 或闪烁 (Flickering) 现象。
-
什么是抗锯齿,有哪些抗锯齿方法(MSAA,SSAA),讲一下MSAA的原理
锯齿现象是因为屏幕是由离散像素点组成的网格,在屏幕上显示的图案需要使用方形像素去近似,导致边缘处出现锯齿,抗锯齿则是通过在物体的边缘的像素颜色进行与背景颜色的混合创造一种过渡的颜色,实现了抗锯齿
-
SSAA: 超采样抗锯齿
使用远高于当前分辨率的尺寸渲染场景,得到高清的图像后,将该图下采样(downsample) 到屏幕实际分辨率.
-
MSAA:多重采样抗锯齿
SSAA的高效优化,将像素的覆盖率采样与着色采样分离,以4xMSAA为例
- 创建多重采样缓冲区:启用4xMSAA,GPU为当前屏幕上每个像素点内部创建4个子采样点,每个字采样点都有自己独立的深度值和模板值,但共享一个颜色值
- 覆盖率测试:一个三角形被光栅化时,GPU检查这个三角形覆盖子采样点的个数得到覆盖掩码,而不是简单判断是否覆盖像素中心
- 着色计算:GPU对每个像素只运行一次片段着色器,是在像素中心点运行,无论有多少子采样点被覆盖等,但是光照,纹理采样只会被计算一次得到单一的颜色值.
- 颜色写入:根据覆盖掩码,将该掩码与颜色值写入到子采样点,未覆盖的子采样点保留原色
- 解析:回合平均像素内所有子采样点的颜色输出到屏幕.
-
除此之外还有后期处理抗锯齿(Post-processing Anti-Aliasing)FXAA,SMAA,TAA
-
-
混合在管线的哪个阶段,半透明物体的渲染顺序是怎样的
在测试阶段之后,被写入颜色缓冲区之前,使用glBlendFunc,混合顺序基本原则为:先绘制所有不透明物体,将所有半透明物体从后向前进行排序后在绘制
- 绘制所有不透明物体,开启深度测试和深度写入.
- 保持深度测试开启,关闭深度写入,对所有半透明物体根据它们距离摄像机的距离排序,从远到近绘制半透明物体
为什么要由远到近绘制?
混合操作的是当前片段与已存在与颜色缓冲区中的颜色进行混合,如果先绘制了近的半透明物体,再绘制远的,那么因为绘制B的时候,会把自己的颜色(源颜色)混合到已经包含A颜色的缓冲区(目标颜色)之上,导致了前后关系颠倒的错误结果.
-
什么是RenderGraph
渲染图 是一种现代图形渲染中用于管理和优化渲染流程的高级抽象系统,不再像传统渲染循环那样,让开发者手动管理状态、资源(如纹理、缓冲区)和它们之间的依赖关系,而是将整个渲染帧抽象成一个有向无环图 (DAG) 。
在这个图中:
- 节点 (Nodes) 代表一个独立的渲染任务或计算步骤,通常称为一个 "Pass" (例如:深度预处理Pass、G-Buffer生成Pass、光照Pass、后期处理Pass)。
- 边 (Edges) 代表这些Pass之间的资源依赖关系。例如,光照Pass需要读取(read) G-Buffer生成Pass写入(write)的G-Buffer纹理。
定义 (Define): 开发者首先声明这一帧需要执行哪些渲染Pass,并明确声明每个Pass会读取哪些资源,写入或创建哪些资源。开发者只需关心每个Pass自身的输入和输出,而不需要关心资源的具体创建、销毁和跨Pass传递。
构建图 (Build Graph): RenderGraph系统接收所有Pass的声明,并根据它们声明的资源读写关系,自动构建出一个依赖关系图。
编译/优化 (Compile/Optimize): 这是RenderGraph最核心和强大的步骤。在执行任何GPU命令之前,系统会分析整个图,进行一系列自动优化,例如:
- 资源生命周期管理 (Resource Lifetime Management): 自动计算出每个资源(如中间结果纹理)何时需要被创建、何时可以被安全销毁或重用 (Aliasing) ,极大地优化显存占用。例如,G-Buffer中的法线纹理在光照Pass结束后就不再需要,RenderGraph可以立即将这块显存标记为可重用,分配给后续的后期处理Pass。
- 自动同步 (Automatic Synchronization): 根据资源的读写依赖,自动在不同的Pass之间插入正确的内存屏障 (Barriers) 或信号量 (Semaphores),确保GPU在读取一个资源前,写入该资源的操作已经完成。这极大地简化了现代图形API(如Vulkan, DirectX 12)中复杂的同步管理。
- Pass合并与排序 (Pass Merging & Reordering): 分析图的依赖关系,自动合并可以并行执行的渲染通道,或者重新排序以提高GPU硬件的利用率。
执行 (Execute): 最后,RenderGraph将优化后的、具体的GPU指令(如渲染命令、状态切换、内存屏障)提交给图形API去执行。
总结: RenderGraph 是一种声明式的渲染架构。你不再告诉GPU “如何做” (命令式:绑定这个、设置那个、绘制、再设置下一个...),而是告诉RenderGraph “你想要什么结果” (声明式:我有一个Pass需要这些输入,并产生这些输出)。然后,RenderGraph系统会智能地计算出最高效的 “如何做” 的方案。
-
OpenGL多线程如何使用,为什么要多线程,不同线程之间的数据是否能同步,如何同步
OpenGL中使用多线程的核心原则是:一个线程在同一时刻只能有一个活动的OpenGL上下文 (Context)
每个线程一个上下文,共享资源,流程大致为:
- 创建共享上下文,主线程创建一个主上下文,在创建工作线程的上下文时,将气质定位与主上下文共享资源,
- 上下文被共享后,一个线程创建的OpenGL对象将对其他所有共享上下文可见,比如glGenTextures生成一个纹理ID,主线程可以直接使用他
- 主线程负责绘图指令,状态切换,窗口系统交互,工作线程负责处理CPU密集型或耗时的OpenGL任务,例如资源加载和创建,硬盘读取图片数据,解压,然后调用glTexImage2D上传到GPU,或者生成复杂的模型数据并调用glBufferData创建VBO
为什么多线程:
避免渲染线程被阻塞,保证流畅的帧率,如果所有操作都在一个线程里,当执行一个耗时任务时(比如从磁盘加载一个50MB的高清纹理),整个渲染会完全卡住,直到加载完成。这在用户看来就是一次严重的掉帧或卡顿。
通过多线程,我们可以将这些耗时任务(如资源加载、程序化网格生成等)放到后台的工作线程中去做,而渲染线程则可以继续不间断地以60FPS或更高的帧率运行,从而提供流畅的用户体验。
多线程了如何同步?
当你在工作线程调用一个OpenGL命令(如
glBufferData)时,这个命令被放入驱动的指令队列中,CPU会立即返回,但GPU可能还没有开始甚至还没有收到这个指令。如果此时主线程立刻就要使用这个VBO,GPU可能还没准备好,会导致渲染错误或未定义的行为。-
如何同步:
-
Fence Sync Objects (围栏同步对象) :这是现代OpenGL(3.2+)中处理此类问题的标准方法。
-
工作原理:
- 在工作线程提交完所有需要同步的指令后,向GPU的指令流中插入一个“围栏”对象 (
glFenceSync)。 - 当主线程需要使用这些资源时,它可以检查这个围栏的状态。最常见的做法是调用
glClientWaitSync,这个函数会阻塞主线程的CPU,直到GPU执行到围栏所在的位置,从而确保所有在围栏之前的指令都已经执行完毕。
- 在工作线程提交完所有需要同步的指令后,向GPU的指令流中插入一个“围栏”对象 (
-
-
一个4x4矩阵中,TRS分别在哪里(天空盒贴图,通过->3x3然后再回到4x4可以移除view的位移,使得天空盒始终以Camera为中心,不会有位置变化)
Translation (T - 位移): 最容易识别,它占据了最后一列的前三个元素
(Tx, Ty, Tz)。这三个值直接表示物体在X, Y, Z轴上的位移。Rotation (R - 旋转) 和 Scale (S - 缩放): 这两者共同作用于左上角的 3x3 子矩阵中。它们是相乘混合在一起的,不能简单地把某个元素看作是纯粹的旋转或缩放。
- 旋转 (R) 决定了这个 3x3 矩阵的方向(即基向量的方向)。
- 缩放 (S) 决定了这个 3x3 矩阵中三个列向量的长度。如果不存在缩放(缩放值为1),那么这三个列向量都是单位向量。如果X轴缩放了2倍,那么第一个列向量的长度就是2。
变换矩阵通常是
M = T * R * S的乘积结果。1. 位移矩阵 (Translation)
[ 1 0 0 Tx ] [ 0 1 0 Ty ] [ 0 0 1 Tz ] [ 0 0 0 1 ]2. 缩放矩阵 (Scale)
[ Sx 0 0 0 ] [ 0 Sy 0 0 ] [ 0 0 Sz 0 ] [ 0 0 0 1 ]3. 旋转矩阵 (Rotation - 以绕Z轴为例)
[ cosθ -sinθ 0 0 ] [ sinθ cosθ 0 0 ] [ 0 0 1 0 ] [ 0 0 0 1 ] -
什么是Quat,万向节死锁
四元数本质上是一个由4个分量组成的数(通常表示为 ),但你可以直观地将其理解为封装了一个旋转轴 (axis) 和一个旋转角度 (angle) 的组合,四元数也可以理解为从四维的角度将四维球进行球极投影到三维空间得到的一个数,更直观的欧拉角(Euler Angles,即分别绕X, Y, Z轴旋转一定角度)相比,四元数的主要优点是:
- 避免万向节死锁 (Gimbal Lock) 。
- 可以实现平滑、稳定的球面线性插值(SLERP),这对于动画来说至关重要。
- 组合多个旋转的计算效率更高。
万向节死锁 是在使用欧拉角表示旋转时,会遇到的一种特殊状态,在此状态下会丢失一个旋转自由度。丢失一个旋转维度
-
向量的点乘和叉乘
点乘用于衡量两个向量的“对齐”程度或方向上的相似性,两个向量的点乘的得到的是向量长度以及夹角余弦的积,所以点积为0可以判断垂直.
叉乘用于计算一个同时垂直于两个给定向量的新向量.
-
坐标系统:局部空间->世界空间->观察空间->裁剪空间->屏幕空间(NDC->Screen)
局部空间 (Local Space / 模型空间, Model Space): 所有的顶点坐标都是相对于这个模型自身的原点来定义的,用于建模和动画.
世界空间(WorldSpace) :统一的全局坐标系,描述物体间的相对位置,通过model matrix对物体操作
观察空间(ViewSpace) :以摄像机为原点的坐标系空间,通过Viewmatrix移动摄像机,简化投影计算,判断视锥体便于裁剪
裁剪空间(ClipSpace) : 与观察空间类似,但是被应用了投影矩阵,将观察空间的点变换到一个标准话的视锥体中
透视除法:进入屏幕空间之前进行透视除法,也就是将裁剪空间的坐标的前三个分量都除以第四个分量w,得到NDC坐标.
屏幕空间 (Screen Space) :通过视口变换(glViewPort),将NDC坐标映射到视口的实际像素,NDC的Z值被映射到0,1,用于深度缓冲,得到的顶点在窗口上最终的像素位置后,交给光栅化器填充像素
-
什么是深度缓冲,缓冲值是否是线性的,深度缓冲是在近处出精度大还是远处精度大,如何防止深度冲突
深度缓冲是一张与屏幕(颜色缓冲区)大小相同的二维纹理,每像素存储的是深度值[0.0-1.0]范围内.用于处理物体的前后遮挡关系,也就是深度测试.
标准的透视投影 (Perspective Projection) 下,深度缓冲中的值是非线性的,近处精度非常大,远处精度非常小。
深度冲突 (Z-Fighting) 指的是当两个物体的表面离得非常非常近,以至于深度缓冲的精度不足以区分它们谁前谁后时,在渲染时这两个物体的像素就会交错闪烁,防止或缓解深度冲突:
- 将近裁剪平面 (Near Plane) 设置得尽可能远
- 将远裁剪平面 (Far Plane) 设置得尽可能近
- 使用更高精度的深度缓冲
- 在物体层面进行微调
- 关闭对冲突物体的深度写入
-
什么是冯氏光照,和布林冯氏光照的区别
冯氏光照通过:
环境光 (Ambient) :模拟场景中经过多次反射、无特定方向的“背景光”,确保物体最暗的部分也不是纯黑色。这是一个固定的、较暗的颜色。
漫反射 (Diffuse) :模拟光线照射到粗糙表面后,向所有方向均匀散射的效果。这是构成物体基本颜色和形状的主要部分。其亮度取决于光线方向与表面法线之间的夹角。
高光 (Specular / 镜面反射) :模拟光线照射到光滑表面(如金属、塑料)后,在一个集中方向上反射所形成的“高光点”。其亮度不仅取决于光线和法线,还取决于观察者的视线方向。
来模拟光照,布林冯氏光照模型是冯氏模型的一个优化和改进,引入了半程向量,位于光线向量 (L) 和 观察向量 (V) 的正中间,通过该向量计算与表面法线的夹角,避免了普通冯氏光照的镜面反射在观察向量角度大时的锐利高光现象
-
模板测试在什么时候进行,有什么作用,创建一个物体轮廓的流程是怎样的
实现对屏幕上特定区域像素的条件性渲染。使用一个额外的缓冲区,叫做模板缓冲区 (Stencil Buffer) ,这个缓冲区为屏幕上的每个像素存储一个小的整数值(通常是8位,范围0-255),根据模板缓冲区中的值,来决定是否要丢弃一个即将被绘制的像素.
创建一个物体轮廓的流程:
启用模板测试:
glEnable(GL_STENCIL_TEST);
第一步:正常绘制物体,并“标记”模板缓冲区:
-
设置模板操作:让所有被绘制的物体像素,都在模板缓冲区对应位置写入一个特定的参考值(例如
1)。glStencilFunc(GL_ALWAYS, 1, 0xFF);// 测试总是通过glStencilOp(GL_KEEP, GL_KEEP, GL_REPLACE);// 如果深度和模板测试通过,就将模板值替换为1
-
正常绘制一遍物体。
-
执行完这一步后,屏幕上所有属于该物体的像素,其在模板缓冲区中的值都变成了
1。
第二步:绘制放大版的物体作为轮廓:
-
在绘制前,将物体的模型矩阵稍微放大一点点。
-
使用一个只输出单一轮廓颜色(比如金色、白色)的简单着色器。
-
修改模板测试的规则:只在模板缓冲区的像素值不等于
1的地方进行绘制。glStencilFunc(GL_NOTEQUAL, 1, 0xFF);// 只在模板值不为1的地方绘制
-
为了防止轮廓本身被其他物体遮挡,或者错误地写入深度,通常此时会关闭深度测试或关闭深度写入。
glDisable(GL_DEPTH_TEST);
-
绘制这个放大版的物体。由于模板测试的规则,这次绘制只会填充那些“多出来”的、模板值仍为
0的边缘像素。
最后:恢复状态:
- 重新启用深度测试,并将模板测试函数恢复到默认状态,以便后续其他物体的正常渲染。
glEnable(GL_DEPTH_TEST);glStencilFunc(GL_ALWAYS, 0, 0xFF);
-
面剔除是怎么做到的,有哪些应用(阴影走样,性能提升)
面剔除核心原理是基于多边形(通常是三角形)顶点在屏幕空间中的环绕顺序 (Winding Order) 。通过
glEnable(GL_CULL_FACE)即可开启面剔除功能,你还可以通过glCullFace()来指定剔除正面、背面还是两者都剔除,并通过glFrontFace()来改变对正面环绕顺序的定义(CW或CCW)。性能优化,丢弃不会被看到的面,渲染特殊几何体,辅助模型调试,检查模型顶点环绕顺序,处理双面材质(布料、纸张、植物叶子这样两面都可能被看到的物体)
-
讲一下帧缓冲,怎么创建一个帧缓冲,需要哪些条件,有什么用(离屏渲染,后期处理)
OpenGL中一个管理渲染目的地的对象。可以把它理解为一个自定义的、屏幕外的画布。本身只是一个“容器”或“管理器”,汇集了一系列可绘制的内存缓冲区,颜色附件 (Color Attachment) :用于存储渲染出的像素颜色,通常是一张纹理。深度附件 (Depth Attachment) :用于存储深度信息,进行深度测试。模板附件 (Stencil Attachment) :用于存储模板值,进行模板测试。
怎么创建一个帧缓冲?需要哪些条件?
创建帧缓冲对象 (FBO),创建并附加附件(RBO/Texture),指定绘制目标(帧缓冲有多个颜色附件,可以指定写入那个附件(MRTs)),检查完整性(检查帧缓冲是否完整),完整的条件:
- 必须至少附加一个颜色缓冲区。
- 所有附件本身必须是完整的(例如,纹理必须已分配内存)。
- 所有附件必须有相同的宽度和高度。
- 所有附件必须有相同的多重采样样本数。
作用:后期处理,阴影贴图,反射折射,延迟渲染,对象拾取
-
纹理和RBO的区别
纹理 (Texture) 的内容可以被着色器读取(采样) ,而渲染缓冲对象 (Renderbuffer Object, RBO) 则不行,
特性 纹理 (Texture) 渲染缓冲对象 (RBO) 着色器访问 可读可写 (主要用于读取/采样) 只写 (不能被着色器采样) 主要用途 最终需要被着色器作为输入使用的渲染目标,如后期处理、阴影贴图、反射。 无需读取的“临时”或“中间”数据存储,最典型的就是深度和模板缓冲。 性能 性能稍低,因为其存储格式必须支持采样、过滤等复杂操作。 性能更高,因为它以GPU最高效的原生格式存储,省去了纹理相关的开销。 - 如果渲染结果需要在后续的着色器中被读取和使用 -> 必须用纹理。
- 如果渲染结果只用于写入和测试(如深度测试),之后就丢弃 -> 用RBO性能更好。
-
立方体贴图中,创建一个天空盒,天空盒是否应该随着相机运动,怎么做
是的,天空盒应该永远是无限远的位置,所以相机旋转,天空盒需要旋转,但是天空盒不应该有任何相对位置的变化,所以可以渲染天空盒时对观察矩阵将其转换为3x3矩阵(丢弃位移),再转换回4x4矩阵以移除位移,并且需要进行深度优化,确保天空盒总是在所有物体的背后,所以将其Z值强制设置为w,因为深度值来源于透视除法(z/w),那么glPosition=pos.xyww后深度值总是1.0,最大值,并且深度函数改为GL_LEQUAL就可以了.
-
顶点的交错和分批是什么
交错:将一个顶点的所有属性(如位置、法线、纹理坐标)连续地存储在一起,然后再存储下一个顶点的所有属性,
[位置1, 法线1, UV1, 位置2, 法线2, UV2, 位置3, 法线3, UV3, ...]分批:将所有顶点的同一种属性连续地存储在一起,形成几个独立的属性数据块。
[位置1, 位置2, 位置3, ...] [法线1, 法线2, 法线3, ...] [UV1, UV2, UV3, ...]交错布局是更常见、通用的选择,对大部分标准渲染任务友好。分批布局则在需要对数据进行更细粒度控制的特定优化场景(如延迟渲染的G-Buffer Pass)中更有优势。
-
有没有写过UBO,什么是绑定点
uniform buffer object,一种更高级、更高效地管理统一变量 (uniforms) 的方法,与传统的、逐个设置 uniform (
glUniform*) 的方式相比,可以将大量的 uniform 变量(例如多个矩阵、光源属性、全局设置等)一次性地打包存入这个缓冲区,可以一次性更新一大块数据,而不是进行多次单独的API调用,也可以让同一个UBO可以被多个不同的着色器程序 (Shader Program) 共享和使用.绑定点 (Binding Point) 是 UBO 实现数据共享的连接机制,可以把它想象成一个插槽或接口。它是一个位于UBO和着色器之间的中间层。整个连接过程分为两步:
- 将UBO链接到绑定点:先把的UBO(比如存有矩阵数据的
matrixUBO)绑定到一个全局可用的、用索引号表示的绑定点上(例如,binding point 0)。 - 将着色器链接到绑定点:然后在着色器程序中,内部的那个uniform数据块(Uniform Block)应该从
binding point 0去获取数据。
通过这种方式,UBO和着色器程序之间解耦了。你可以让十几个不同的着色器程序都去监听
binding point 0,当你需要更换全局数据时,只需要将一个新的UBO绑定到binding point 0上,所有着色器就会立刻开始使用新的数据,极大地简化了复杂场景中全局数据的管理。 - 将UBO链接到绑定点:先把的UBO(比如存有矩阵数据的
-
OpenGL有哪些内建变量
顶点着色器 (Vertex Shader)
输入 (Inputs):
gl_VertexID: 正在处理的顶点的索引。gl_InstanceID: 正在处理的图元的实例ID(用于实例化渲染)。
输出 (Outputs):
gl_Position: (必须输出) 变换后的顶点在裁剪空间中的坐标。这是顶点着色器最重要的输出。gl_PointSize: 渲染点(Points)时,这个点的大小(以像素为单位)。
片段着色器 (Fragment Shader)
输入 (Inputs):
gl_FragCoord: 片段在窗口坐标系中的坐标(x, y, z, 1/w)。z值是它的深度值。gl_FrontFacing: 一个布尔值。如果片段属于一个正面,则为true;否则为false。gl_PointCoord: 渲染点(Points)时,片段在点内的2D坐标,范围是[0,1]。
输出 (Outputs):
gl_FragColor: (已废弃) 在旧版GLSL中,用于指定片段最终输出的颜色。out vec4 FragColor;: (现代做法) 在现代GLSL中,通过一个自定义的out变量来输出颜色。gl_FragDepth: 可选输出。允许着色器手动设置片段的深度值,而不是使用插值后的深度。
-
有没有写过几何着色器,接受什么,输出什么,有哪些功能,常见的应用场景
几何着色器位于顶点着色器和片段着色器之间。它的输入是顶点着色器处理完后组装成的一个完整的图元 (Primitive) ,以及这个图元所有顶点的属性数据。
points: 如果绘制点,则输入1个顶点。lines: 如果绘制线段,则输入2个顶点。triangles: 如果绘制三角形,则输入3个顶点。lines_adjacency,triangles_adjacency: 包含邻接信息的更复杂的图元。几何着色器的强大之处在于它可以改变图元的类型和数量。它的输出是0个或多个新的图元。它可以将输入的单个图元,转变为全新的点、线段或三角形带 (triangle strip)。
功能:图元生成,图元丢弃,图元类型转换,图层选择
应用:法线可视化,爆炸效果,毛发生成,粒子效果
-
OpenGL的实例化是什么,为什么要用实例化数组
实例化 (Instancing) 是一种极其高效的渲染技术,用于在场景中一次性绘制成千上万个外观相似、但属性(如位置、颜色、大小)不同的物体,只向GPU发送一份模型的顶点数据(例如,一棵草的模型),然后告诉GPU:“请用这份模型数据,在这些不同的位置、用这些不同的颜色,把它画10000遍”。所有这些绘制请求会合并成一个单独的绘图调用 (Draw Call) ,从而极大地减少了CPU到GPU的通信开销,性能远高于循环调用10000次
glDrawArrays。创建一个额外的VBO,里面存储的是每个实例的属性数据(例如,一个包含10000个不同位置向量的数组)。在设置顶点属性指针时 (
glVertexAttribPointer),不仅设置了它的位置和格式,还通过glVertexAttribDivisor(attributeLocation, 1)告诉OpenGL:这个属性是一个实例化数组,每个实例更新一次数据(除数为1)。在顶点着色器中,会同时接收到模型的顶点位置(每个顶点都不同)和实例的位置(当前实例的所有顶点共享同一个值)。将两者相加,就能得到每个实例在世界中正确的位置。
-
什么是MRTs(多重渲染目标)
允许 片段着色器 (Fragment Shader) 在单次渲染传递 (single render pass) 中,同时将数据输出到多个不同纹理(颜色附件)中的技术,在标准的渲染中,片段着色器只有一个输出,它将计算出的最终颜色写入到帧缓冲的第一个颜色附件(Color Attachment 0)中。常用于延迟渲染
当启用 MRT 时:
- 创建一个拥有多个颜色附件(例如,4个纹理)的帧缓冲对象 (FBO)。
- 在片段着色器中,定义多个
out变量(例如out vec4 FragData0; out vec4 FragData1;...)。 - 在着色器代码的末尾,给这些
out变量分别赋上不同的值。 - GPU会自动将
FragData0的值写入FBO的颜色附件0,FragData1的值写入颜色附件1,以此类推。
-
如何实现阴影效果,说一下思路
第一步:从光的视角渲染 (生成深度图)
-
创建帧缓冲 (FBO) :创建一个只附加了深度附件(通常是一张纹理)的帧缓冲。我们不需要颜色信息。
-
设置“光的摄像机” :
- 将一个虚拟的摄像机放置在光源的位置。
- 计算这个“摄像机”的观察矩阵 (View Matrix) 和投影矩阵 (Projection Matrix)。这套矩阵通常被称为光的空间变换矩阵 (Light-Space Matrix) 。
-
渲染场景:
- 绑定上面创建的FBO。
- 使用光的空间变换矩阵,将整个场景渲染一遍。
- 在这个阶段,我们不需要复杂的着色器,因为我们只关心深度。一个简单的、只做顶点变换的顶点着色器和一个空的片段着色器就足够了。
-
结果:执行完毕后,附加在FBO上的那张深度纹理,就是阴影贴图 (Shadow Map) 。这张图从光的视角记录了场景中离光最近的物体的深度信息。
第二步:从主摄像机的视角正常渲染 (绘制阴影)
-
绑定默认帧缓冲:切换回默认的帧缓冲,准备将最终结果画到屏幕上。
-
正常渲染:使用主摄像机的观察和投影矩阵,以及常规的光照着色器(如布林冯氏)来渲染场景。
-
在片段着色器中判断阴影:
-
对于屏幕上的每一个即将被着色的片元,我们首先计算出它在世界空间中的坐标。
-
然后,用第一步中创建的光的空间变换矩阵,将这个世界坐标变换到光空间中。
-
现在我们得到了这个片元在光视角下的坐标
(x, y, z)。其中z值就是该片元当前离光的距离。 -
我们用
(x, y)作为纹理坐标,去采样第一步生成的阴影贴图,得到一个深度值。这个值代表了在那个方向上,离光最近的物体的距离。 -
进行比较:
- 如果 当前距离 > 阴影贴图中的距离,说明有另一个物体挡在了当前片元和光源之间。因此,该片元处于阴影中。我们就不再对其进行漫反射和高光计算。
- 否则,该片元没有被遮挡,正常进行光照计算。
-
-
-
法线贴图,切线空间
- 法线贴图是一项在低多边形模型上伪造 (fake) 高精度表面细节的强大技术。它是一张特殊的纹理,但其RGB通道存储的不是颜色,而是方向向量(法线) 。在进行光照计算时,着色器会逐像素地从这张贴图中读取法线,并用这个“假的”法线来代替模型原始的、平滑的法线。这会欺骗光照系统,使其认为模型表面有着丰富的凹凸细节(如砖缝、划痕、毛孔),从而产生正确的高光和阴影,即使模型的几何体本身是完全平坦的。它的核心优势是以极低的性能开销,实现极高的视觉细节。
-
切线空间是一个建立在模型每个表面顶点上的局部坐标系。它使得法线贴图可以被正确地、独立于模型旋转和动画而应用。
这个坐标系由三个相互垂直的轴定义:
- 法线 (Normal - N) :垂直于表面,“朝上”的轴。这是这个局部空间的 Z轴。
- 切线 (Tangent - T) :沿着表面,通常指向纹理坐标U方向的轴。这是 X轴。
- 副切线 (Bitangent - B) :也沿着表面,垂直于N和T,通常指向纹理坐标V方向。这是 Y轴。
法线贴图中存储的法线向量,就是相对于这个TBN坐标系的。一个指向
(0, 0, 1)的法线(在贴图中表现为淡蓝色RGB(128, 128, 255))就代表该点法线与原始表面法线完全一致,即“没有凹凸”。切线空间为法线贴图提供了一个“上、前、左”的统一参考标准,无论这个模型表面在世界中如何旋转扭曲,着色器都能正确地解读出法线贴图所指示的凹凸方向,并将其转换到世界空间中进行正确的光照计算。
-
HDR是什么
HDR (High Dynamic Range) ,即高动态范围,在计算机图形学中,是一种旨在模拟人眼所能感知的、极其宽广的亮度范围的渲染技术。
HDR技术通过使用浮点数(如16位或32位)来存储颜色值,打破了
[0, 1](或0-255)的限制。这使得渲染出的场景可以包含“超级亮”的颜色值(例如,太阳的亮度可能是vec3(25.0, 25.0, 25.0)),同时也能保留阴影中vec3(0.001, 0.001, 0.001)的微弱细节。简单来说,LDR记录的是最终要在屏幕上显示的“照片”,而HDR记录的是场景中光线的物理真实亮度。
流程:
由于显示器本身仍然是LDR设备(它们只能显示
[0, 1]范围的颜色),直接将HDR图像发送给显示器是行不通的。因此,HDR渲染管线是一个分为两个主要步骤的过程:第一步:渲染到浮点帧缓冲 (Floating-Point Framebuffer)
- 创建浮点帧缓冲:我们不再将场景渲染到默认的8位帧缓冲,而是创建一个使用浮点纹理(例如
GL_RGB16F或GL_RGB32F)作为颜色附件的自定义帧缓冲对象 (FBO)。 - 进行光照计算:以正常方式渲染整个场景,但所有的光照计算都是基于物理上更精确的亮度值。在这个阶段,我们不进行任何颜色截断 (Clamping) 。如果一个像素因为强光照射而计算出的亮度值是
5.0,我们就原封不动地将这个5.0存入浮点帧缓冲中。 - 结果:完成这一步后,我们就得到了一张包含场景真实高动态范围光照信息的HDR纹理。
第二步:色调映射 (Tone Mapping)
这是将HDR图像转换为可在标准LDR显示器上观看的关键步骤。
色调映射是一种后期处理效果,它负责将无限范围的HDR亮度值,智能地、非线性地压缩到
[0, 1]的LDR范围内,同时尽可能地保留高光和暗部的细节与对比度,模拟人眼适应不同光照环境的效果。流程如下:
-
绘制一个铺满屏幕的四边形。
-
绑定并采样上一步生成的HDR纹理。
-
在片段着色器中,应用一个色调映射算法 (Tone Mapping Operator) 来处理采样的HDR颜色。常见的算法有:
- Reinhard:非常简单有效,将亮度值平滑地映射到LDR范围。
- ACES (Academy Color Encoding System) :电影行业标准,效果非常出色,能产生富有电影感的、自然的色彩和对比度。
- Uncharted 2:游戏《神秘海域2》使用的算法,效果也广受好评。
-
伽马校正 (Gamma Correction) :在色调映射之后,通常还需要进行伽马校正,以确保颜色在显示器上看起来是正确的。
最终,经过色调映射和伽马校正后的LDR颜色,才会被输出到默认帧缓冲并显示在屏幕上。用户看到的画面虽然是LDR的,但它保留了HDR计算带来的丰富细节和真实感,例如可以看到明亮灯泡周围的辉光,同时也能看清阴影下的物体。
- 创建浮点帧缓冲:我们不再将场景渲染到默认的8位帧缓冲,而是创建一个使用浮点纹理(例如
-
SSAO了解过吗
Screen-Space Ambient Occlusion (屏幕空间环境光遮蔽) ,用于实时近似环境光遮蔽 (Ambient Occlusion) 效果的后期处理技术。其目的是通过模拟场景中缝隙、角落、以及物体接触点由于光线被遮挡而产生的变暗效果,来增加画面的深度感、接触感和真实感。
SSAO 不去追踪真实世界的光线,而是作为一个后期处理效果,在屏幕空间(也就是基于最终渲染出来的2D图像)进行工作。它对屏幕上的每一个像素,检查其在三维空间中的邻近区域,并判断这个区域在多大程度上被周围的几何体所“遮蔽”。一个点被周围几何体遮蔽得越严重,它接收到的环境光就越少,看起来就越暗。
实现流程:
几何阶段 (G-Buffer Pass) :
- 这与延迟渲染的第一步完全相同。我们需要先将渲染场景所必需的几何信息输出到一个G-Buffer中。
- 对于SSAO来说,最关键的信息是每个像素的世界坐标位置 (Position) 和法线 (Normal) 。深度信息也可以用来重建位置。
SSAO计算阶段 (SSAO Pass) :
-
这一步是SSAO的核心。我们绘制一个铺满屏幕的四边形,对屏幕上的每一个像素(片元)执行以下操作:
- 定义采样核心 (Kernel) :在要计算的片元周围,定义一个由多个采样点组成的半球形或球形“核心”。这些采样点的位置是预先定义好的。
- 进行采样:对于核心中的每一个采样点,将其从观察空间(以当前片元为中心)变换到世界空间。
- 比较深度:将变换后的采样点坐标,投影回屏幕空间,并用这个屏幕坐标去采样G-Buffer中的深度/位置图,得到该位置上实际几何体的深度。如果采样点的深度大于实际几何体的深度,就意味着这个采样点位于某个几何体的内部或后方,即当前片元在该方向上被“遮蔽”了。
- 计算遮蔽因子 (Occlusion Factor) :统计有多少个采样点被遮蔽了。被遮蔽的采样点越多,该片元的“遮蔽因子”就越高(例如,从0.0代表完全无遮蔽,到1.0代表完全被遮蔽)。
-
为了避免采样图案重复产生条纹,通常还会引入一张小的随机旋转纹理,来让每个像素的采样核心都有一个随机的旋转。
-
结果:这一步的输出是一张只包含遮蔽信息的灰度图(SSAO贴图),其中明亮区域代表无遮蔽,黑暗区域代表有遮蔽。
模糊与应用阶段 (Blur & Composite Pass) :
- 上一步生成的SSAO贴图会有很多噪点。因此,需要对其进行一次模糊处理(通常是高斯模糊),以平滑噪点,使阴影过渡更自然。
- 最后,在进行最终的光照计算时,将场景的原始颜色与模糊后的SSAO贴图中的遮蔽因子相乘。
FinalColor = SceneColor * (1.0 - OcclusionFactor)。这样,被遮蔽越多的地方,其环境光亮度就会被削弱得越多,从而产生最终的SSAO效果。