- 三种着色处理方法。
- 抗锯齿总结
- 透明排序
- 伽马校正
- 三种着色总结:
1.平滑着色(Flat shading):简单来讲,就是一个三角面用同一个颜色。如果一个三角面的代表顶点(也许是按在 index 中的第一个顶点),恰好被光照成了白色,那么整个面都会是白的。
2.高洛德着色(Gouraud shading):每顶点求值后的线性插值结果通常称为高洛德着色。在高洛德着色的实现中,顶点着色器传递世界空间的顶点法线和位置到 Shade( )函数(首先确保法线矢量长度为 1),然后将结果写入内插值。像素着色器将获取内插值并将其直接写入输出。 高洛德着色可以为无光泽表面产生合理的结果,但是对于强高光反射的表面,可能会产生失(artifacts)。3.冯氏着色(Phong shading):冯氏着色是对着色方程进行完全的像素求值。在冯氏着色实现中,顶点着色器将世界空间法线和位置写入内插值,此值通过像素着色器传递给 Shade( )函数。而将 Shade( )函数返回值写入到输出中。请注意,即使表面法线在顶点着色器中缩放为长度 1,插值也可以改变其长度,因此可能需要在像素着色器中再次执行此归一化操作。
注意 Phong Shading 和 Phong Lighting Model 的区别,前者是考虑如何在三个顶点中填充颜色,而后者表示的是物体被光照产生的效果。
注意冯氏着色可以说是三者中最接近真实的着色效果,当然开销也是最大的。因为高洛德着色是每个顶点(vertex)计算一次光照,冯氏着色是每个片元(fragment)或者说每像素计算一次光照,点的法向量是通过顶点的法向量插值得到的。所以说不会出现高洛德着色也许会遇到的失真问题
2、抗锯齿总结:
抗锯齿(英语:anti-aliasing,简称 AA),也译为边缘柔化、消除混叠、抗图像折叠有损,反走样等。它是一种消除显示器输出的画面中图物边缘出现凹凸锯齿的技术,那些凹凸的锯齿通常因为高分辨率的信号以低分辨率表示或无法准确运算出 3D 图形坐标定位时所导致的图形混叠(aliasing)而产生的,抗锯齿技术能有效地解决这些问题。
锯齿的产生:
几何失真
这应该算是最常见的失真类型。当一些场景的几何体的边缘(一般为三角形)和一个像素部分覆盖的时候,由于对非完全覆盖的采样过于粗糙(1像素1采样点),栅格化后就出现了锯齿。
透明失真
当一张贴图包含部分透明(镂空)图案的时候(例如上图左上角的铁丝网),由于贴图本身就是由个数有限的像素阵列所组成,当判断被渲染几何体透明与否的时候会将贴图的栅格化特性带入最后的渲染结果。这样一来,就算是几何体内部也可能会有锯齿。
子像素失真
当栅格化后的物体小于一像素时,就产生了子像素失真。子像素失真最常见于非常细小的物体,例如场景中的塔尖、电话线或电线,甚至是距离屏幕足够远的一把剑。虽然这类失真也可以算是几何失真的一种,但在反锯齿算法的设计中需要被特殊对待,因而另归一类。
纹理失真
当包含大量高频信号的纹理未被充分采样的时候(特别是在各向异性采样的场景中,最典型的就是延伸到屏幕深处的一个平面,例如地面),则会出现纹理失真。通常这种失真并无法从静态的截图中体现出来,但在下面的动态图中可以看到,当纹理移动的时候出现了像素闪烁的现象
渲染失真
像素渲染器在对每个像素进行颜色填充时产生的失真称为渲染失真。这种现象通常在硬光照渲染中出现,例如基于法线贴图的镜面高光,卡通渲染,以及边缘光照等等。
所有的3D纹理都是基于贴图吗?显然不是的,除了通过贴图给3D骨架上色外也能通过像素着色器辅以相应的数学方法来填充物体的颜色。例如我有一个立方体,我希望它每一面都是一半蓝一半白,那么我就可以写一个Shader Program来进行坐标的判断并填入相应的色彩(例如大于0.5填蓝,小于等于0.5填白,而卡通渲染(Toon Shader)也只不过是在这个基础上多加几个分界点罢了)。不出意外的是,由于像素着色是在像素阵列上执行,立方体的颜色界线会有锯齿
抗锯齿太多了,挑选几个经典的总结一下
SSAA:
超级采样抗锯齿(Super-Sampling Anti-Aliasing,简称 SSAA)是比较早期的抗锯齿方法,比较消耗资源,但简单直接。这种抗锯齿方法先把图像映射到缓存并把它放大,再用超级采样把放大后的图像像素进行采样,一般选取 2 个或 4 个邻近像素,把这些采样混合起来后,生成的最终像素,令每个像素拥有邻近像素的特征,像素与像素之间的过渡色彩,就变得近似,令图形的边缘色彩过渡趋于平滑。再把最终像素还原回原来大小的图像,并保存到帧缓存也就是显存中,替代原图像存储起来,最后输出到显示器,显示出一帧画面。这样就等于把一幅模糊的大图,通过细腻化后再缩小成清晰的小图。如果每帧都进行抗锯齿处理,游戏或视频中的所有画面都带有抗锯齿效果。 超级采样抗锯齿中使用的采样法一般有两种:
• OGSS,顺序栅格超级采样(Ordered Grid Super-Sampling,简称 OGSS),采样时选取 2 个邻近像素。
• RGSS,旋转栅格超级采样(Rotated Grid Super-Sampling,简称 RGSS),采样时选取 4 个邻近像素。
另外,作为概念上最简单的一种超采样方法,全场景抗锯齿(Full-Scene Antialiasing,FSAA)以较高的分辨率对场景进行绘制,然后对相邻的采样样本进行平均,从而生成一幅新的图像。
MSAA:
多重采样抗锯齿(Multi Sampling Anti-Aliasing,简称 MSAA),是一种特殊的超级采样抗锯齿(SSAA)。MSAA 首先来自于 OpenGL。具体是 MSAA 只对 Z 缓存(Z-Buffer)和模板缓存(Stencil Buffer)中的数据进行超级采样抗锯齿的处理。可以简单理解为只对多边形的边缘进行抗锯齿处理。这样的话,相比 SSAA 对画面中所有数据进行处理,MSAA 对资源的消耗需求大大减弱,不过在画质上可能稍有不如 SSAA。同时MSAA提取边缘是在图形管线的前段,是一种硬件AA。
在unity中开启:
FXAA:
FXAA全称为“Fast Approximate Anti-Aliasing”,翻译成中文就是“快速近似抗锯齿”。它是传统MSAA(多重采样抗锯齿)效果的一种高性能近似值,相比于MSAA,FXAA的目标是速度更快、显存占用更低,还有着不会造成镜面模糊和亚像素模糊(表面渲染不足一个像素时的闪烁现象)的优势,而代价就是精度和质量上的损失
Unity 抗锯齿的例子:
主要概括为两步
1. 查找边缘
2. 模糊边缘
边缘检测可以参考candycat。
模糊边缘:网格,随机,与旋转采样。
网格采样比较简单,但是因为太规则了,模糊的效果可能不够好,
显获取周围的点,然后进行混色
float4 c0 = tex2D(_MainTex, i.uv_MainTex + fixed2(0.5, 1) / _Size);
float4 c1 = tex2D(_MainTex, i.uv_MainTex + fixed2(-0.5, 1) / _Size);
float4 c2 = tex2D(_MainTex, i.uv_MainTex + fixed2(0.5, -1) / _Size);
float4 c3 = tex2D(_MainTex, i.uv_MainTex + fixed2(-0.5, -1) / _Size);
然后就是随机,个人认为随机的效果不太好,因为像是边缘被噪波了一样,有像素点扩散的痕迹
float2 randUV = 0;
randUV = rand(float2(n.x, n.y));
float4 c0 = tex2D(_MainTex, i.uv_MainTex + float2(randUV.x / 2, randUV.y) / _Size);
randUV = rand(float2(-n.x, n.y));
float4 c1 = tex2D(_MainTex, i.uv_MainTex + float2(randUV.x / 2, randUV.y) / _Size);
randUV = rand(float2(n.x, -n.y));
float4 c2 = tex2D(_MainTex, i.uv_MainTex + float2(randUV.x / 2, randUV.y) / _Size);
randUV = rand(float2(-n.x, -n.y));
float4 c3 = tex2D(_MainTex, i.uv_MainTex + float2(randUV.x / 2, randUV.y) / _Size);
然后就是旋转网格采样,最佳的旋转角度是arctan (1/2) (大约 26.6°),这里偷个懒,也省去了旋转计算的消耗,大概个位置进行采样,效果还算好。
float4 c0 = tex2D(_MainTex, i.uv_MainTex + fixed2(0.2 / 2, 0.8) / _Size);
float4 c1 = tex2D(_MainTex, i.uv_MainTex + fixed2(0.8 / 2, -0.2) / _Size);
float4 c2 = tex2D(_MainTex, i.uv_MainTex + fixed2(-0.2 / 2, -0.8) / _Size);
float4 c3 = tex2D(_MainTex, i.uv_MainTex + fixed2(-0.8 / 2, 0.2) / _Size);
接下来是ShaderLab部分:
3、透明排序
透明度测试:比较极端,开启深度写入,只要深度值小于某个值,都算透明,该片元都会被舍弃
透明度混合:得到半透明效果,关闭深度写入,没有关闭深度测试,所以可以进行比较,当更近的时候可以进行混合操作
在渲染透明物体时需要关闭深度写入,一个半透明表面背后的物体本来是可以被看到的,但是在深度测试时发现半透明物体的深度值更小,会导致后面的被剔除,我们就无法透过半透明表面看到后面的物体了。
渲染顺序的重要性:A半透明,B不透明,先渲染B再渲染A
另外附上Unity渲染顺序:
Background:队列通常被最先渲染,一般是用来渲染那些需要绘制在背景上的物体。
Geometry:默认的渲染队列。它被用于绝大多数对象。不透明几何体使用该队列。
AlphaTest:通道检查的几何体使用该队列。它和Geometry队列不同,需要透明度测试的物体使用这个队列
Transparent:这个是在Geometry和AlphaTest物体渲染后,再按从后往前的顺序进行渲染
Overlay :一般是用来实现一些叠加效果
4、伽马校正
Gamma校正从何而来
有一种常见的说法,gamma来源于眼睛对光感受。我也曾经错误地采用了这种说法。在wikipedia上查到了gamma的真正来源:开发gamma编码是用来抵消阴极射线管(CRT)显示器的输入和输出特性。电子枪的电流,也就是光的亮度,与输入的正极电压的变化是非线性的。通过gamma压缩来改变输入信号抵消了这个非线性,因此输出图像就能有预期的亮度。
所以,gamma校正和人眼特性无关,仅仅和CRT有关。更新的显示方法,比如LCD和等离子之类,为了保证兼容,也都选择了和当年CRT一样的非线性特性。(其实和系统有关,Mac OS X 10.6就用的1.8,其他系统,包括电视,都用的2.2)Gamma计算很简单,只是个power而已,也就是:
其中的γ就是用来校正的gamma值。
输入和输出
现在让我们来看看一个输入输出的例子。假设相机是线性的,显示器也是线性的,那么输入和输出的关系就是:
也就是通过相机拍照后,在显示器上看到的和真实场景的色彩一样。
可惜,现实是残酷的,显示器的gamma为2.2,所以如果相机仍然是线性的,那么结果就会变成:
这样在显示器上看到的就会有明显的色彩失真。解决方法是把相机的gamma设成1/2.2,这样两次调整之后又能得到真实场景的色彩了:
其实从这个过程也可以看出,gamma校正是为了在输入和输出的环节中保证能和真实场景一致,而眼睛不在这个环节中,所以和眼睛对亮度的感受没有直接关系了。
对渲染的意义
前面讲的输入是对相机拍的照片而言。而对渲染来说,情况又如何呢?渲染中用到的光照都是在线性空间的。因为在设计光照的时候都是认为1的亮度是0.5的2倍。光照如此,texture又如何呢?渲染中用到的 texture一般有两个来源,一个是照片,一个是artist手工画的。前文提到了,照片是gamma = 1/2.2的。一般图象处理软件也都是在gamma空间工作的,所以artist画的图一般也可以认为是gamma = 1/2.2的。所以,我们在pixel shader常可以见到这样的代码:
float4 diff = tex2D(diffuse_texture, uv);
return diff * max(0, dot(light_dir, normal));
这样的代码对吗?不对也对。
说其不对,是因为这里没显式地做gamma校正。做校正的话应该是这样的:
float4 diff = pow(tex2D(diffuse_texture, uv), 2.2f);
return pow(diff * max(0, dot(light_dir, normal)), 1 / 2.2f);
也就是说,gamma校正的过程就是把输入的texture都转换到线性空间,并把输出的调整到gamma = 1/2.2的空间。
说其对,是因为如果diffuse texture如果是sRGB格式的,那么再读取的时候硬件会把它自动转到线性空间。如果render target的texture也是sRGB格式的,在输出的时候硬件也会把它自动转到线性空间。所以,如果输入和输出纹理都是sRGB,那么原先那段shader就是正确的。对于不支持sRGB的老硬件,就必须自己做pow了。
除了渲染,另一个需要注意gamma的地方就是mipmap。如果原texture是gamma =1/2.2的,那么在建立mipmap chain的时候,每一层都必须和渲染一样,先转到线性空间,计算之后再转到gamma = 1/2.2的。否则,255和0混合得到的是51,而不是128。
Unity中的线性空间与Gamma校正
分析下目前Unity中的线性空间和Gamma校正。
目前还是很多游戏是用的Gamma,主要原因是目前市面上还是有一批不支持linear的设备(iphone5及以下,android4.3以下都不支持OpenGL ES3.0,而3.0才开始支持texture和RBO的sRGB),而效果上调调系数也都没问题,但是未来这批手机被放弃了之后,linear空间因为更准确一些,所以移动平台还是有几率会使用linear的当color space为gamma的时候,纹理设置sRGB是没有意义的,因为sRGB是设置给linear color space看的,linear color space中,sRGB的纹理会在shader运算前通过power 2.2切换到线性空间,然后再计算完毕后,通过power 1/2.2再切换回gamma空间。非sRGB纹理则会直接在shader中进行运算,比如normal map、mask、GUI。其中legacy GUI比较特殊,虽然这个纹理是gamma space的,理论上应该标记为sRGB,但是它在shader中的计算不需要linear,所以它不会在shader运算前,从gamma转linear,在运算结束后,从linear转gamma,而是会直接去运算,假装它是linear space的。当color space为gamma的时候,纹理被采样后直接去进行运算,虽然采样的值是gamma空间的,但是没办法。将color space从gamma切换到linear的时候,很多参数(材质、光照)需要调整才能得到原来的效果,但是应该是值得的。
例如在gamma空间下:
可以看到两个图片是没有什么变化的,但是我切换到linear space来看
勾选sRGB的图片在下面的信息中显示是sRGB——它被作为一张sRGB纹理来看待,需要进行gamma校正;
Lightmap是按照gamma空间存放。所以当linear color space的时候,Lightmap会从gamma空间转到线性空间再去计算,而Gamma color space,就不转了。所以如果切换color space,需要rebake lightmap。如果从外界导入lightmap,需要选择纹理类型:lightmap。确保lightmap按照gamma空间存放。