前言:主要涵盖了《real time rendring 4》第12章image space affect和第13章beyond polygons的前六节内容。
目录
固定视角效果(The Rendering Spectrum)
sprite和层 (Sprite s and Layers)
屏幕空间特效(Image-Space Effects)
图像处理
图像处理类似于PS,我们输入一张图片,之后以多种方式对其修饰。可编程着色器和使用输出图像作为输入纹理为使用GPU实现多种图像处理效果提供了途径。在渲染结束后对生成的图像进行处理成为后处理(post-processing)。一个场景可以被渲染成多种形式到一个离屏缓冲,比如color buffer,z-depth buffer。这些结果会被看成一个纹理贴图,布满整个屏幕四边形。
不过在实际应用中,一个满屏的三角形会比四边形更有效 ,因为一个生成一个四边形至少需要两个三角形,而满屏三角形只需要一个。但是不论如何,他们的本质都是一致的:使pixel shader 计算屏幕上的每一个像素,并作出相应的处理。
景深
物体在一个范围之内能够清晰成像,在那个范围之外则成像模糊,这种效果就是景深。在摄影和电影中,景深常用来只是对场景的注意范围,并提供场景深度的感觉。
景深效果由透镜的物理性质产生,若要穿过透镜的光汇聚到胶片上的一个点,光源必须与透镜有特定的距离,在这个距离上的平面称为焦平面。不在这个精确距离上的任何东西,投影到胶片上的区域成为模糊圈(Coc,circle of confusion)。Coc的直径与透镜尺寸和偏离焦平面的距离成正比,偏离距离小到一定程度,Coc会变得比较偏的分辨率更小,在这个范围之内的便称为聚焦,否则则是模糊的。
景深的实现有多种方式,在《ray tracing in one weekend》中,作者通过光线追踪的方式实现了该效果。而本文则是基于图像后处理和模糊圈来模拟该效果。
上图中Coc=abs(A*F*(P-D)/(D*(P-F)))。
物距能通过z缓冲区的Z值来计算:D=-z*znear/(z*(zfar-znear)-zfar)。
模糊圈也能通过z缓冲值,综合摄像机参数的缩放项CS,和偏置项CB来计算:
float Coc=abs(z*CS+CB);
用摄像机参数计算缩放和偏置项:
float P, F, A;
float Zf, Zn;
float CS = (A*F*P*(Zf - Zn)) / ((P - F)*Zn*Zf);
float CB = (A*F*(Zn - P)) / (P*F*Zn);
为了模拟模糊圈的效果,我们可以通过计算模糊圈的直径,将其颜色值分摊到以该像素为中心的圆内所有像素中,进而求其平均值来产生模糊效果。但是这种方法实现起来可能比较困难,所以我们采用了另一种方法:
以某像素为中心,以所有像素最大模糊圈的值为半径,遍历该圆内部所有像素,如果遍历的该像素的模糊圈可以覆盖中心像素,那么color+=pixel.color,最后除以像素数量来获得平均值。
vec3 color=texture(colorTex,texcoords).rgb;
for(int i=0;i<sampleNum;i++)
{
vec2 offset=vec2(sampleX[i],sampleY[i]);
float depth=texture(depthTex,texcoords+offset*tex_offset).r;
if(depth<0.8)
{
float D=abs(depth*CS+CB);
if(D>2*sampleR[i])
{
color+=texture(colorTex,texcoords+offset*tex_offset).rgb;
count++;
}
}
}
FragColor=vec4(color/(count+1),1.0);
注意起初要设置color的颜色为中心像素的颜色,否则如果该像素对应世界空间中的点位于焦平面上的话,其模糊圈的值将会为0,不会有任何其他模糊圈覆盖该像素,那么该像素最后会错误的显示为黑色。
对于采样点的生成,为了省事我采用了均匀采样,首先在一个正方形内均匀的生成采样点,进而通过同心映射将这些采样点映射到圆上。
同心圆映射的代码如下:
vector<float>samplexc;
vector<float>sampleyc;
vector<float>sampleR;
float R, phi;
const float PI = 3.1415926535l;
int sampleNum = (r * 2) * (r * 2);
for (int i = 0; i < sampleNum; i++)
{
float x = samplexf[i]/r;
float y = sampleyf[i]/r;
if (x > y)
{
if (x > -y)
{
R = x;
phi = (PI*y) / (4 * x);
sampleR.push_back(r*R);
sampleyc.push_back(R*sin(phi)*r);
samplexc.push_back(R*cos(phi)*r);
}
else
{
R = -y;
phi = PI / 4*(6 - x / y);
sampleR.push_back(r*R);
sampleyc.push_back(R*sin(phi)*r);
samplexc.push_back(R*cos(phi)*r);
}
}
else
{
if (x > -y)
{
R = y;
phi = PI / 4 * (2 - x / y);
sampleR.push_back(r*R);
sampleyc.push_back(R*sin(phi)*r);
samplexc.push_back(R*cos(phi)*r);
}
else
{
R = -x;
phi = PI / 4 * (4 + y / x);
sampleR.push_back(r*R);
sampleyc.push_back(R*sin(phi)*r);
samplexc.push_back(R*cos(phi)*r);
}
}
}
最后是效果图:
运动模糊
运动模糊是模拟速度的一种方法,它可以使游戏更加的平滑。
刺客信条中运动模糊的运用
有多种方法来实现运动模糊。第一种是通过累计缓冲来实现,即储存多帧的缓冲图像最后通过取平均值来实现,但是这种方法的消耗比较大。还有一种方法根据某点像素速度的方向和大小,来确定模糊的方向和大小。那么如何获取像素的运动方向和大小?
我们通过相邻两帧间物体在NDC中的位置距离差来获得像素的运动大小和方向。
首先根据像素的坐标和z值我们可以获得该像素对应NDC中的位置H,这一段可以参考我的一篇之前关于光栅化的博客。
float zOverw=texture(depthTexture,TexCoords).x;
vec4 H=vec4(TexCoords.x*2-1,TexCoords.y*2-1,zOverw,1);
那么现在该像素在NDC中的位置就是H
vec4 currPos=H;
由于本文中的物体在世界空间中的坐标并没有移动,仅仅是相机的视角发生了变化,所以我们可以据此获得前一帧中物体在世界空间中的坐标:
vec4 D=inverse(projectionView)*H;
vec4 worldPos=D/D.w;
之后我们需要获得前一帧的透视投影矩阵。
glm::mat4 preProjectionView = glm::mat4(1.0);
while (!glfwWindowShouldClose(window))
{
float currentFrame = glfwGetTime();
deltaTime = currentFrame - lastFrame;
lastFrame = currentFrame;
processInput(window);
glClearColor(0.8f, 0.8f, 0.8f, 1.0f);
glBindFramebuffer(GL_FRAMEBUFFER, framebuffer);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
firstShader.use();
glm::mat4 view = camera.GetViewMatrix();
glm::mat4 model = glm::mat4(1.0f);
model = glm::scale(model, glm::vec3(0.5, 0.5, 0.5));
glm::mat4 projection = glm::perspective(camera.Zoom, (float)SCR_WIDTH / (float)SCR_HEIGHT, 0.1f, 100.0f);
firstShader.setMat4("view", view);
firstShader.setMat4("model", model);
firstShader.setMat4("projection", projection);
firstShader.setVec3("viewPos", camera.Position);
firstShader.setVec3("lightPos", lightPos);
glActiveTexture(GL_TEXTURE2);
glBindTexture(GL_TEXTURE_2D, cubeTex);
firstShader.setMat4("model", model);
renderCube();
model = glm::translate(model, glm::vec3(1, 2, -1));
firstShader.setMat4("model", model);
renderCube();
glm::mat4 tempMat = projection * view;
glBindFramebuffer(GL_FRAMEBUFFER, 0);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
secShader.use();
secShader.setMat4("projectionView", tempMat);
secShader.setMat4("preProjectionView", preProjectionView);
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, textureColorbuffer);
glActiveTexture(GL_TEXTURE1);
glBindTexture(GL_TEXTURE_2D, depthTexture);
renderQuad();
preProjectionView = tempMat;
glfwSwapBuffers(window);
glfwPollEvents();
}
注意在渲染循环前我声明了一个preProjectionView矩阵用来存储前一帧的ProjectionView矩阵,之后在渲染循环中我声明了一个tempMat矩阵来存储当前帧的ProjectionView矩阵,在第二个shader我将tempMat的值赋给了preProjectionView,如果在第一个shader后赋值的话,那么preProjectionView就是当前帧的ProjectionView矩阵,就不会产生运动模糊的效果。
获取前一帧的位置:
vec4 prePos=preProjectionView*worldPos;
prePos/=prePos.w;
获取像素的方向和大小 :
vec2 velocity=vec2(currPos-prePos)/8.0f;
对图像在速度方向上进行模糊:
vec3 color=texture(colorTexture,TexCoords).xyz;
vec2 texCoords=TexCoords+velocity;
for(int i=1;i<15;i++,texCoords+=velocity)
{
color+=texture(colorTexture,texCoords).xyz;
}
vec3 result=color/15;
renderdoc捕捉的效果:
bloom
这个部分略过,在LearnOpenGL中有相应介绍。
降采样
降采样(Downsampling部分书籍会翻译成向下采样,景深一节中应用了该技术,但本文并未使用)是一个模糊时常用的GPU相关的技术。其原理是生成一个smaller image,比如,沿坐标轴轴将分辨率减半,制作一个四分之一屏幕的图像。基于采样技术和所需的算法,原始的图像会产生一个低分辨率的图像。当这个图像被混合到最终的全屏满分辨率的图像时 ,放大的贴图将会使用双线性插值混合两个样本,这就造成了更强的模糊效果。在低分辨率的图像上采样明显的减少了访问纹素的数量。例如,在smaller image 中应用宽度为5的kernel的效果和在原始图像中应用宽度为9的kernel的效果相似。虽然效果会有一点下降,但是在模糊有着大面积相似的区域时,消耗相对较少。降采样技术可以用于其他缓慢变化的现象,比如大部分的粒子系统可以已一半的像素渲染。降采样也可以用来产生mipmap。
Bilateral Filtering
Upsampling或者其他图像处理操作可以通过Bilateral Filtering的形式来提升效果。丢弃或弱化与我们采样中心无关表面的样本的影响。这种过滤方法常用作保护边界。
想象一下,在一个灰色背景的场景中,有一个蓝色的三角形,它的前面有一个我们聚焦的红色三角形。这个蓝色的三角形应该被模糊,而这个红色的三角形应该保持锋利。Bilateral filter将会检测像素的颜色,如果是蓝色将会被模糊,否则,如果是红色,将不会进行操作而保持锋利。除了颜色值之外,我们也可以用其他额外的信息来来判断某像素是否应该被忽略,比如深度值,法线值。现在假设场景中有两个物体,其中一个物体的阴影很靠近另一个物体但不会混合在一起,经过普通的模糊之后,这块阴影很可能和另一个物体混合在一起,这个时候我们就可以使用bilateral filter来对其进行处理。
多边形技术(beyond polygons)
公告板技术( Billboarding )
公告板技术会根据视角方向来旋转多边形,使得公告板看起来始终朝向观察者,公告板技术有很多的应用,比如小的公告板可以代表雪花,表面和人物:
还可以用作粒子系统,比如下面unity中的一个粒子特效其实就是由许多小的billboard组成的:
为了实现该效果我们要构造一组正交基:
1.根据该点在世界空间中的位置和相机坐标获得normal向量。
vec3 center=vec3(0,0,0);
vec3 wolrdPos=vec3(model*vec4(aPos,1.0));
vec3 normalDir=normalize(viewPos-center);
2.up向量设为vec3(0,1,0)。如果normal向量和up向量相等的话它们叉乘会产生错误的向量,所以如果我们从正上或者正下方观察该公告板的话将up向量设为vec3(0,0,1)。
if(abs(normalDir.y)>0.99)
up=vec3(0,0,1);
else
up=vec3(0,1,0);
3.由up向量和normal向量叉乘得到right向量。
vec3 right=normalize(cross(up,normalDir));
4.由right向量和normal向量叉乘得到相对视角方向的up向量u'。
up=normalize(cross(normalDir,right));
获取视角下的正交基后我们将公告板从世界空间转换到用户视角下:
1.获得原坐标点相对世界空间原点的位移。
vec3 offset=wolrdPos-center;
2.根据offset和正交基将其变换到用户视角下。
vec3 viewSpacePos=center+right*offset.x+up*offset.y+normalDir*offset.z;
以下是截图,因为没有视频不容易看出效果,但是无论怎么移动视角这个公告板总是会正对用户:
渲染谱(The Rendering Spectrum)
当相机靠近一个物体时,为了提升该物体的品质,我们希望用更高细节的模型去替换它,相反,当我们远离该模型,我们则期望使用简化的模型。这就叫做Level Of Detail(LOD)技术,它可以让让场景更快地显示。
固定视角效果(The Rendering Spectrum)
对于复杂的几何图形和着色模型,每一帧都重新渲染整个场景的话消耗自然会很巨大,但是当视角并不移动,或者只移动一点点的话,那么很多东西可以只渲染一次。
这种技术在CAD比如maya,zb之类的软件中应用广泛,因为这里面的物体一般都是静态的。一旦用户的视角发生了移动,对应视角的color和z-buffer就会存储起来以便重用。当然还可以存储其他信息。比如在一个三维绘画软件中可以存储一个物体的ID,normal,给定视角纹理坐标,当用户进行了某些改动时,可以把根据改动信息对相应的数据进行调整。
另一个相关概念adaptive refinement /progressive refinement,即当观察者的视角和场景不移动的时候,计算机会产生质量越来越高的图像,这在CAD中非常有用。
Relight就是当用户确定了一个视角后,就会用该视角下的数据进行离线处理,进而产生一系列的缓冲或者更精细的结构来呈现该场景。这种方法和延迟渲染很相似,不同点在于,延迟渲染是在单独一帧中处理场景,而relight则将渲染任务分配到多个帧中。
天空盒(sky box)
即立方体贴图,略。
光场渲染(Light Field Rendering)
存储各个视角和角度场景的渲染图像,当给定一个新的视角,这种技术会根据存储的图像通过插值方法来产生新的图像,类似于全息投影,在交互式渲染中应用较少。
sprite和层 (Sprite s and Layers)
主要与2D游戏相关。如下图,后挡板在鸡的前面,鸡在驾驶室的前面,驾驶室在路和树木的前面。将场景看做一系列的层,每一层都有与之相关的深度。之后我们从后到前依次渲染场景而不需要深度值。