Pixel World

it's better be burning out than to fade away.

Geometry Shader

in/out layout qualifier

几何着色器需要定义输入的图元以及输出的图元信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#version 460 core
layout (points) in;
layout (line_strip, max_vertices = 2) out;

void main()
{
gl_Position = gl_in[0].gl_Position + vec4(-0.1, 0.0, 0.0, 0.0);
EmitVertex();

gl_Position = gl_in[0].gl_Position + vec4( 0.1, 0.0, 0.0, 0.0);
EmitVertex();

EndPrimitive();
}

输入的图元可以使用以下任意一种类型:

  • points: when drawing GL_POINTS primitives (1).
  • lines: when drawing GL_LINES or GL_LINE_STRIP (2).
  • lines_adjacency: GL_LINES_ADJACENCY or GL_LINE_STRIP_ADJACENCY (4).
  • triangles: GL_TRIANGLES, GL_TRIANGLE_STRIP or GL_TRIANGLE_FAN (3).
  • triangles_adjacency : GL_TRIANGLES_ADJACENCY or GL_TRIANGLE_STRIP_ADJACENCY (6).

输出的图元仅可以使用以下三种类型:

  • points
  • line_strip
  • triangle_strip

几何着色器允许在渲染管线中动态的生成几何体。即使绘制命令只指定了最基础的图元(比如4个点),几何着色器也能将这些点实时扩展为更复杂的形状,从而用非常简单的输入创造出丰富的视觉效果。
glDrawArrays/glDrawElements中传入的图元类型对应了几何着色器中输入的图元类型(in)

阅读全文 »

glProvokingVertex

用于控制Flat Shading(平面着色)时,整个三角形使用哪个顶点的属性值。当使用flat修饰符的插值方式时,三角形内部所有片段都使用同一个顶点的属性值,而不是插值。provokingVertexMode决定选用哪个顶点。

1
void glProvokingVertex(GLenum provokeMode);

provokeMode有且仅有两个枚举值:GL_FIRST_VERTEX_CONVENTION or GL_LAST_VERTEX_CONVENTION

阅读全文 »

Pick

GPU Pick

为场景中的每个对象分配一个唯一的颜色,将所有对象渲染到一个FBO中,拾取时基于鼠标指针在屏幕上的位置索引FBO中的颜色值。如果该颜色与某个对象匹配,则该对象被选中。以下是绘制到SelectionBuffer和从SelectionBuffer读取的详细步骤:

Drawing to Selection Buffer

首先需要一个单的pass将物体分配的颜色写入单独的FBO,这里需要处理几件事:

步骤 操作 详细说明
1 创建帧缓冲区 创建一个与渲染屏幕分辨率相同的帧缓冲区
2 分配唯一索引 为每个对象分配一个唯一的索引值(通常是从 1 开始的整数)
3 索引转颜色 将索引值转换为 32 位颜色值 (r, g, b, a)
4 渲染 用索引转换得到的颜色将对象绘制到帧缓冲区

Reading from Selection Buffer

步骤 操作 详细说明
1 获取鼠标位置 从屏幕获取鼠标坐标 (x, y)
2 读取像素 使用 glReadPixels() 读取鼠标位置的颜色值
3 颜色转索引 将帧缓冲区中的颜色值转换回对象索引值
4 命中检测 如果索引值非零,表示选中了对象
5 未命中检测 如果索引值为零,表示未选中任何对象

IndexToColor & ColorToIndex

阅读全文 »

Advanced GLSL

gl_PointSize

GLSL定义了一个叫做gl_PointSize输出变量,它是一个float变量,可以使用它来设置点的大小。在顶点着色器中修改点的大小的话,就可以对每个顶点设置不同的值了。
在顶点着色器中修改点大小的功能默认是禁用的,如果需要使用它的话,需要启用OpenGL的GL_PROGRAM_POINT_SIZE:

1
glEnable(GL_PROGRAM_POINT_SIZE);

gl_VertexID

整型变量gl_VertexID储存了正在绘制顶点的当前ID。当使用glDrawElements命令进行渲染的时候,这个变量会存储正在绘制顶点的当前索引。当使用glDrawArrays命令绘制的时候,这个变量会储存从渲染调用开始的已处理顶点数量。

gl_FragCoord

gl_FragCoord的x和y分量是片段的屏幕坐标,其原点为窗口的左下角.z分量等于对应片段的深度值.

1
2
3
4
5
6
7
void main()
{
if(gl_FragCoord.x < 400)
FragColor = vec4(1.0, 0.0, 0.0, 1.0);
else
FragColor = vec4(0.0, 1.0, 0.0, 1.0);
}
阅读全文 »

Deferred Shading

Combining deferred rendering with forward rendering

  1. 延迟渲染 GBufferPass + lightingPass
  2. 2.将zBuffer通过blitFrameBuffer拷贝到新的FBO
  3. 基于deferred Shading生成的zBuffer执行前向渲染
1
2
3
4
5
6
7
8
9
GBufferPass();
LightingPass();
glBindFramebuffer(GL_READ_FRAMEBUFFER, gBuffer);
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0); // write to default framebuffer
glBlitFramebuffer(
0, 0, SCR_WIDTH, SCR_HEIGHT, 0, 0, SCR_WIDTH, SCR_HEIGHT, GL_DEPTH_BUFFER_BIT, GL_NEAREST
);
glBindFramebuffer(GL_FRAMEBUFFER, 0);
ForwardRenderingPass();

A larger number of lights

通常情况下,当我们渲染一个复杂光照场景下的片段着色器时,我们会计算场景中每一个光源的贡献,不管它们离这个片段有多远。很大一部分的光源根本就不会到达这个片段,所以为什么我们还要浪费这么多光照运算呢?
隐藏在光体积背后的核心思想就是计算光源的半径,或是体积,也就是光能够到达片段的范围。由于大部分光源都使用了某种形式的衰减(Attenuation),我们可以用它来计算光源能够到达的最远距离,或者说是半径。接下来只需要对那些在一个或多个光体积内的片段进行繁重的光照运算就行了。这可以给我们省下来很可观的计算量,因为我们现在只在需要的情况下计算光照。
这个方法的难点就是需要计算出一个光源光体积的大小,或者是半径。

1
2
3
4
5
6
7
float constant  = 1.0; 
float linear = 0.7;
float quadratic = 1.8;
float lightMax = std::fmaxf(std::fmaxf(lightColor.r, lightColor.g), lightColor.b);
float radius =
(-linear + std::sqrtf(linear * linear - 4 * quadratic * (constant - (256.0 / 5.0) * lightMax)))
/ (2 * quadratic);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
struct Light {
[...]
float Radius;
};

void main()
{
[...]
for(int i = 0; i < NR_LIGHTS; ++i)
{
// calculate distance between light source and current fragment
float distance = length(lights[i].Position - FragPos);
if(distance < lights[i].Radius)
{
// do expensive lighting
[...]
}
}
}

上面那个片段着色器在实际情况下不能真正地工作,并且它只演示了我们可以不知怎样能使用光体积减少光照运算。然而事实上,你的GPU和GLSL并不擅长优化循环和分支。这一缺陷的原因是GPU中着色器的运行是高度并行的,大部分的架构要求对于一个大的线程集合,GPU需要对它运行完全一样的着色器代码从而获得高效率。这通常意味着一个着色器运行时总是执行一个if语句所有的分支从而保证着色器运行都是一样的,这使得我们之前的半径检测优化完全变得无用,我们仍然在对所有光源计算光照!
使用光体积更好的方法是渲染一个实际的球体,并根据光体积的半径缩放。这些球的中心放置在光源的位置,由于它是根据光体积半径缩放的,这个球体正好覆盖了光的可视体积。这就是我们的技巧:我们使用大体相同的延迟片段着色器来渲染球体。因为球体产生了完全匹配于受影响像素的着色器调用,我们只渲染了受影响的像素而跳过其它的像素。下面这幅图展示了这一技巧:

阅读全文 »

GPU Culling

基于computeShader实现GPU的视锥剔除和遮挡剔除.

frustumCulling

GPU提出的第一步先判断包围盒是否完全位于视锥体外部,需要构造frustum的六个平面以及场景包围盒,循环frustum的6个面分别与包围盒求交,如果包围盒在任意一个平面的外部,则直接剔除。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
bool frustumCulling(mat4 mvp, BoundingBox bbox)
{
vec3 center = (bbox.min.xyz + bbox.max.xyz) * 0,5;
vec3 extents = bbox.max.xyz - center;
for(int i = 0; i < 6; ++i)
{
vec3 normal = frustumPlanes[i].xyz;
float d = frustumPlanes[i].w;
float radius = dot(extents, abs(normal));
float distance = dot(center, normal) + d;
if(distance < -radius)
{
return true;
}
}
return false;
}
阅读全文 »

Gamma Correction

人类视觉与物理亮度

人类的眼睛对暗部变化更敏感,对亮部变化较迟钝。显示器利用这一特性,在有限的位深(通常是 8 位)下,将更多精度分配给暗部,从而在视觉上提供更均匀的亮度分布。

显示器的非线性

CRT 显示器(以及现代 LCD 模拟其行为)具有非线性响应。这种非线性特性实际上与人类视觉系统相匹配。输入电压与输出亮度之间的关系大致为:

阅读全文 »
0%