【实时渲染】屏幕空间特效和多边形技术

前言:主要涵盖了《real time rendring 4》第12章image space affect和第13章beyond polygons的前六节内容。

目录

屏幕空间特效(Image-Space Effects)

图像处理

景深

运动模糊

bloom

降采样

Bilateral Filtering 

多边形技术(beyond polygons)

公告板技术( Billboarding )

渲染谱(The Rendering Spectrum)

固定视角效果(The Rendering Spectrum)

天空盒(sky box)

光场渲染(Light Field Rendering)

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游戏相关。如下图,后挡板在鸡的前面,鸡在驾驶室的前面,驾驶室在路和树木的前面。将场景看做一系列的层,每一层都有与之相关的深度。之后我们从后到前依次渲染场景而不需要深度值。

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值