unreal引擎官方给出了unreal-4如何渲染一帧的介绍,结合renderDoc插件,分析渲染过程,并且给出各个环节的性能瓶颈与解决方案。

本文用以记录,学习,分享。如有错误,烦请指正。

原文链接

An In-Depth Look at Real-Time Rendering

概述

概述之概述

这个系列课程旨在揭示和可视化整个渲染过程,对unreal所有的特性、挑战以及解决方案做一个全局的探讨,使得读者能理解unreal的设计蓝图,也能理解这一套工作流的利弊。

当什么都没有的时候,实时渲染RTR是最高效的。RTR主要讨论的是一种平衡,性能与渲染质量的平衡。站在项目角度,应当在开发前明确目标帧率,在目标帧率的约束下,尽量用最少的性能换取最高的画面表现。画面表现,性能,渲染特性,三者有得必有失。

现实是非常复杂:
1.所有细节都需要被高效的绘制
2.需要一个稳定的工作流程和限制条件
3.结合使用预计算来离线渲染以提高效率
4.多种方法的耦合

unreal用到的特性有:

其中可见哪些是预计算的,哪些是实时计算的。几种计算反射与光照的方法在离线阶段完成。

由点到面

一切渲染的源头,是由三点构成的三角面片,点标志着三角形的位置,面片显示三角形的颜色与图案,虚幻世界的一切模型都是由这样的三角面片构成的。

计算机的两个核心元件,CPU与GPU共同肩负着渲染的责任。他们处理渲染流水的不同工作,都具有其瓶颈。

unreal采用的渲染流水线

unreal游戏场景视口的渲染主要采用延迟渲染管线(Deffered Rendering),在这个流程中的某些环节采用前向渲染管线(Forward Rendering)。

前向渲染

每进行一次完整的前向渲染,都需要渲染该对象的渲染图元,并计算两个缓冲区的信息,一是颜色缓冲区,二是深度缓冲区。利用深度缓冲区来决定一个片元是否可见,如果可见即更新颜色缓冲区中的颜色值。乐乐用下面的伪代码解释这一过程[1]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Pass{
for(each primitive in this model){
for(each fragment covered by this primitive){
if(failed in depth test){
//如果没有通过深度测试,则该片元不可见
discard;
}
else{
//如果可见则进行光照计算
float4 color = Shadeing();
writeFrameBuffer();
}
}
}
}

延迟渲染

延迟渲染包括两个过程,在第一部分,不进行任何光照计算,仅仅计算哪些片元是可见的(主要通过深度缓冲实现)。如果发现一个片元可见,则把相关信息存储到G缓冲区(信息包括表面法线,视角方向,漫反射系数等),然后在第二部分中,利用Gbuffer中的信息进行真正的光照计算[1]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
Pass1{
for(each primitive in this model){
for(each fragment covered fy this primitive){
if(failed in depth test){
//如果没有通过深度测试,则该片元不可见
discard;
}
else{
//如果可见则写进GBuffer
writeGBuffer(materialInfo, norml, pos);
}
}
}
}
Pass2{
for(each pixel in the screen){
if(the pixel i valid){
readGBuffer();
float4 color = shading();
writeFrameBuffer(pixel ,color);
}
}
}

利弊

延迟渲染与前向渲染


渲染准备

要将画面从数据呈现到屏幕中,需要CPU与GPU的交替配合,流水线使得工作效率更高。

CPU与GPU的逐帧执行流水线

在Frame0时:
完成场景中所有物体的位置计算;

在Frame1时:
依据远近、相机视锥盒子、遮挡关系进行剔除,得到最终需要渲染的物体,并将渲染信息发送给GPU;

在Frame2时:
GPU进行绘制,先计算深度缓冲Zbuffer,再进行GBuffer的计算,渲染各种特性。理解draw call,GPU的性能与draw call紧密联系。通常场景中的同种材质的图元 在一个draw call中调用渲染。


几何渲染

draw call对性能的影响

Drawcall就是一个命令,cpu发起,gpu接收,这个命令仅仅指向一个需要被渲染的图元列表。DrawCall发出后,一般影响的是GPU端的绘制,因为draw的过程是图形的准备及绘制过程,大部分在GPU中处理(设置绘制上下文--绘图方式--顶点坐标--绘制)。

draw call调用次数对游戏性能的影响,大于模型面数对性能的影响。比如有时候 ,50000个三角形会比5000 00000个三角形渲染起来更慢(如果draw call次数过多的话)。因为draw call有着基本的性能消耗,所以将一个低量级的三角形数量优化到一个超低量级的数量往往对性能提升没什么用。

如果一个actor有多个component,那么每个component单独作一次draw call,一个接着一个的渲染,所以把很多component从多个actor,集合到一个actor中,并不会提升性能

降低draw call的一种方法是使用更少的大模型代替很多的小模型。于是推荐使用 Modular Meshes 技术合并网格,同时利用Statistics工具和Stat命令,但是一旦合并就很难再恢复原样,所以尽量在项目的后期优化时再合并网格。如果面对目标机型为低端机的,可以利用合并网格技术合并几乎所有网格以提升性能。
另一种降低draw call的方法是Level of Detail(LOD):给定条件下简化模型,比如随着与相机的距离变化简化模型。unreal采用了HLOD,成组的简化模型。

Shaders之始,Vertex shader

shader(着色器)是跑在GPU上的小程序,贯穿渲染流水线的整个过程,shader的种类有很多,渲染流水线执行的第一个shader便是 vertex shader。

主要作用是将输入的顶点从局部坐标系,变换到世界空间坐标系。还有别的用途,比如处理顶点着色,也用作顶点位置的额外偏移,比如草地与水的偏移。

值得注意的是,顶点坐标便宜并不会改变其实际的坐标信息,只是视觉上的一种变幻而已,

vertex shader作为渲染的一部分,也对性能有着影响。着色器越复杂,运行越慢;顶点越多运行越慢。因此高量级的模型只应该参与简单的vertex shader。

光栅化与GBuffer

光栅化与OverShading

光栅化就是将图元渲染到像素构成的网格中。 每次draw call都要完成一次光栅化。
一个像素点只能代表一个三角形。同时一个像素点甚至可以代替一个面数非常大的模型(模型离相机非常非常远的情况下)

当光栅化结束,每个像素点都使用pixel shader来进行更精确的像素计算,这个过程需要更多的图元信息,比如纹理。(OpenGL中,使用fragment shader,Direct中叫pixel shader),将在下文介绍。

OverShading在两个地方出现。第一处是光栅化的时候,unreal对像素点的管理采用以2X2的模块单位进行分组,这意味着如果三角形某个点只占用2X2中的1个的时候,也会会整个2X2进行计算。第二处是当模型分布不合理时,导致某一处像素组,发生大量重复的计算与替换。

Gbuffer

GBuffer是延迟渲染管线中的关键概念之一。在这一过程中,将画面的多种信息计算加载到不同的缓存中,最后合成一帧。

GBuffer包含的信息图:Normal(A), metalic value(B-r), specular value(B-g), roughness value(B-b), 
without lighting value(C), special pixels(D), depth buffer(E)

unreal游戏场景视口中可看见Gbuffer信息

像素着色器和材质

纹理

说到纹理,就要说纹理压缩技术,每个平台的压缩技术都不一样,DXTC(又叫BC)常见于PC平台。法线贴图常使用BC5压缩技术。

纹理影响存储和带宽,而并不影响渲染性能,于是要考虑压缩技术。

为了最大化纹理的利用效率,大部分引擎包括虚幻,都是用mipmaps技术(MIPS),即多级纹理。与LOD原理差不多,MIPS会依据距离加载同一张纹理的不同的级别显示。

pixel shader

像素着色器是渲染管线的核心,是GPU运行的对像素进行着色的一系列计算。像素着色器驱动着虚幻引擎整个材质系统,也驱动着光线,雾,反射,屏幕后处理,等特性。

像素着色器可以自由选择屏幕中需要着色的像素点,依据mask图像。

像素着色器用shader语言书写,每个平台的语言都不一样,比如DirectX API使用的着色器语言是HLSL

材质

在ue中,材质系统是除了几何体数据外的所有其他数据。包括光照模型,分布函数,各种渲染状态,各种渲染分支,以及一个提供给用户的节点图等等。
材质系统的很大一部分特性是基于PBR的。PBR使用Specular/Metallic/Roughness来处理所有的着色。

材质系统有个设置,shading model,是一些mask用于确定哪些像素使用非PBR着色模型,然后这些像素点采用另外的渲染路径。

UE4 Shader生成分两部分,第一部分是把材质编辑器中的节点图编译成HLSL代码,这一部分是通过FHLSLMaterialTranslator来完成的。
UE4 Shader生成的第二部分是把HLSL生成多平台的Shader代码,如Windows上的HLSL,Android上的GLSL,IOS上的MetalShader。

一个材质/着色器的最大纹理采样器使用数量通常是16,其中通常有13个可以使用。开发者可以使用128个共享采样器(DX1以上);

纹理大小主要导致延迟,而不是帧率损失;

像素着色器影响很大,因其对游戏运行十分重要;

分辨率越高,复杂材质对性能的影响越大

一个材质会编译生成很多个shader

从render的角度, 具有同样材质实例的mesh 会在同一批次渲染,即在一个draw call被绘制,即使是不同skeletal mesh 上的材质。但就算是同一个skeletal mesh的不同材质,他们母材值一样,但是材质实例不一样,也是会调用不同的draw call。所以为了尽量减少draw call,合并texture使得不同的mesh用同一个材质实例是一个方法。


反射

实时计算反射非常难,unreal提供了三种工具来计算反射,皆有其利弊,但是好好利用这三种技术,将反射与渲染结果相结合的话,会提升效果表现。

反射捕捉

  • 在一个特定的位置捕捉一张静态的cubemap
  • 预计算
  • 非常快
  • 不精确
  • 只能够用于捕捉点位置的反射效果

屏幕反射 Planar Reflection

Screen Space Reflections

  • 并不常用,发生在平面上的捕捉
  • 非常耗
  • 对于需要精确反射效果的光滑平面适用,其他情况就不太适用
  • 只在有限区域适用

屏幕空间反射SSR

  • 虚幻默认的反射系统
  • 实时计算,影响场景中的所有地方
  • 准确的反射
  • 输出信息有噪声
  • 中等消耗
  • 只能显示 当前可见的集合体的反射信息
  • 在”post process volume”组件中

总结

所有这三种反射一起影响最终的反射环境

如果项目没有为分发进行烘培的话,反射捕捉会在关卡加载的时候进行。因此太多的反射捕捉会降低loading的速度(反射捕捉超过千个的话,引擎直到最后烘培前都不well)

反射捕捉的精度可以在引擎中调整

天空盒是一个性价比很高的选择

平面反射仅推荐在非常需要的时候使用

当项目对硬件的要求不高时,建议关闭SSR;如果电脑性能跟得上,那么SSR质量调整随意


静态光源与静态阴影

虚幻主要有两种方式处理光照和阴影,针对静态光照的方式 与 针对动态光照的方式,或者说是预计算与实时 两种方式。光照和阴影一般是分开处理的。

方法的利弊

处理静态光照,是在编辑器中预计算,然后存在光照贴图中;

非常快,但是消耗存储

预计算时候花费较长的时间

只要场景中有东西改变,都要重新渲染一遍

模型需要光照贴图的UV,这个额外的映射也需要时间

质量的利弊

有效的处理辐射和全局光照

产生具有软阴影的物理阴影

质量依赖于lightmap的分辨率和UV映射

lightmap分辨率是有上限的

非常大的模型不会有足够的lightmap uv 空间

一旦预计算完成,光线和阴影不会实时的变化与更换

Lightmass

Lightmass是生成Lightmaps的进程,是一个独立的工具,支持基于网络的分布式渲染。烘培质量由Light Build Quality还有每个关卡的Lightmass决定。

Indirect Lighting Cache

为了解决动态模型的光线预计算。ILC在场景中基于光线缓存,体在每个单位上放置了向量。每个向量都存了当前位置的光线强度。在运行时,距离动态模型最近的5X5X5个向量将会被考虑。

静态光源的表现及影响

静态光源经常以用同一种速度渲染

烘培之后,一个光源 还是五万个光源,对于性能表现不大

lightmap分辨率影响的是存储和文件大小,并不影响帧率

提升烘培时间可以考虑:

 a. lightmap 分辨率
 b. 模型/光源的数量
 c. 具有大衰减半径或源半径的光

动态光源与实时阴影

unreal项目中对性能影响最大的几个因素:

  1. drawcall的数量,几何体渲染
  2. 透明物体渲染
  3. 像素着色器
  4. 动态阴影

方法的利弊

  1. 使用GBuffer进行实时渲染
  2. 光源可以被任意更改/移动/替换
  3. 不需要任何特别的模型上的准备
  4. 阴影是最耗的
  5. 有多种方法渲染动态阴影,依据使用场景的不同来设计应用阴影的解决方案

质量的利弊

  1. 因为阴影对性能影响很大,所以经常损失质量来弥补性能
  2. 对于大部分资源,都不会进行辐射计算和全局光照计算
  3. 动态光源比静态光源更尖锐和显眼
  4. 动态阴影通常是大小中立的,不像静态阴影计算方法那样依赖模型大小
  5. 动态阴影很难做好

实时阴影

为了计算阴影,需要知道两点的位置,为了得到距离集合体表面的距离,需要查询和比较。这是很慢的。

实时阴影对性能影响明显,关掉一些光源的”shadow casting”选项对性能有显而易见的提升。

unreal引擎中主要有四种动态阴影的方法,以及一些不常用的方法:

  1. Regular Dynamic Shadows - 最常用的方法
  2. Per Object Shadows - stationary light shadows
  3. cascaded shadow maps (CSM) - Directional light shadowing
  4. Distance field shadows - 使用距离场信息进行计算,而不是实时追踪集合体

不常用的有:

  1. inset shaodows
  2. contact shadows
  3. capsule shadows

渲染过程

在像素着色器中计算,动态光源作为一个球体来渲染,这个球的作用就像mask一样。任何在球内的物体都需要一个像素着色器操作来混合动态光源信息。

实时光照的影响

  1. 实时光源在延迟渲染管线中影响不大,但在前向渲染管线中影响很大

  2. 实时光源的计算,会激发更多的像素着色器,而成本取决于像素着色器操作,像素越多,速度就越慢

  3. 光源离相机越近,照亮的像素就越多,运行就越慢

  4. 光源的半径最好尽可能的小

  5. 避免重复的光范围覆盖,引起更多的像素着色器计算。

  6. 如果不是一定需要的话,把阴影关掉

  7. 几何体的三角面片数量也影响动态阴影

  8. 尽量考虑用距离场来简化计算

  9. 距离场最适合用于具有硬直边几何形状的实体模型

  10. 当距离较远时,淡化或关闭阴影

对于阴影方案的选择

  1. 静态阴影与动态阴影相结合是最好的方案
    1. 弱光 离得比较远的光源 用静态
    2. 相机附近的间接光照 用静态
    3. 在静态灯光之上使用动态灯光,以更好地突出阴影和阴影,并在静态结果之上提供一个互动层

综合,有两个基础准则

  1. 只有在需要尽可能高的性能时,才使用静态
  2. 只有当你需要能够在任何时候自由地修改灯光时,才使用动态

雾与透明物体

fog

透明物体

延迟渲染管线在处理透明物体时显现了弊端(延迟渲染只有GBuffer信息,而计算透明物体需要更多的表面信息),因此透明物体都是在最后一个stage再开始处理,或者单独用前向渲染计算,最后与延迟渲染的结果合并。

透明效果的影响:

  1. 当以最好的质量渲染时,透明物体的需要计算更多的像素着色器
  2. 当许多层覆盖同一个像素时,透明物体的计算更加高昂。
  3. 除了像素着色器的消耗,透明物体的渲染顺序也是个麻烦的事情,很容易出错

建议:

  1. 如果非得使用透明物体的话,建议不要把view mode 设置为 default lit,改为 Unlit,会节约很多性能。如果非得需要绝佳的性能,把lighting mode设置为 Surface ForwardShading
  2. 预计透明材料覆盖的像素越多,一般来说,它的材料应该更简单
  3. 讲光线考虑进效果的透明材质,比不发光unlit的透明材质,消耗更多性能。因此,透明物体,能避免光线就避免光线。

更多

  • 次表面渲染
  • 折射
  • displacement mapping
  • 屏幕环境遮罩 SSAO
  • 交互界面 UI
  • 贴花 Decals

后处理

后处理在渲染流水线的最后计算,再次依赖于像素着色器,基于合成并重用GBuffer来计算其效果。

常见的后处理效果:

  • light bloom
  • depth of field/Blurring
  • 镜头光斑
  • 光束
  • vignette 晕影相机?
  • 颜色矫正
  • 曝光
  • motion blur

参考及推荐阅读

1. Unity Shader 入门精要

2.《大象无形-虚幻引擎程序设计浅析-罗丁力》

最近在YouTube中看了些在Unreal材质蓝图中制作火焰的例子,感觉很有趣,学习记录一下。

最终效果

因为美术风格的不同,火焰也有非常多种不同的风格,卡通,炫酷,写实等等,卡通风格中又包括二次元,美漫等风格。这里记一下简单卡通风格与炫酷风格的火焰,效果均在Unreal5.1中实现,

CartoonFire
FireBall

卡通火焰

完整材质图

full material graph

火焰颜色插值

full material graph
为了模拟出火焰颜色的层次感, 要使得火焰外层与内层的颜色有差异。这里进行纹理采样后,火焰边缘透明度接近0,火焰中心透明度接近1,正好利用透明度进行插值,在黄色与红色之间进行插值即可。

火焰主体纹理映射

full material graph
火焰顶端和底端,需要淡化,这样更符合现实中的火焰形态。淡化用渐变来处理。渐变利用纹理坐标y值的0-1映射来控制。

火焰主体的纹理,由两张噪声图混合,噪声图选择时图像块要贴合火焰边缘的形状。并且利用Panner节点设定从下往上的运动。

同时利用Mask纹理来裁剪出最后的火焰轮廓。

两种扰动思路

前面呈现的效果图中没有加入扰动。“扰动”是一种常见的效果,可以将直线变为曲线,就是在原来采样的基础上,增加一些随机效果,使得整体效果更加不规则(在边缘处比较明显可以看见扰动的效果)。比如

实现扰动一般是读噪声图,将噪声图中对应的值叠加到纹理坐标上,再用新的坐标值对纹理进行采样。

炫酷火焰

炫酷火焰比卡通火焰更贴近现实,但是又超出现实……

完整材质图

这个效果参考链接3

  1. UE4 - Ring of fire on a sphere effect - Test (Material bp shown)

火焰主体纹理映射

对火焰纹理进行采样并混合噪声图,依然利用Panner节点添加滚动效果。利用3PointLevels节点对值重新映射。

菲涅尔效应

主要理解Fresnel节点。

所谓菲涅尔效应,是指光照基于观察者角度的不同形成不同强度反射的现象。比如,低头垂直视线看水面,能看见水底(光线折射多),平行水面看远方水面能看见倒映的天空(光线反射多)。

unreal提供的简单Fresnel节点,有三个参数:

  • ExponentIn:控制菲涅尔效果的衰减
  • BaseReflectFractionIn:指定从正面观察表面时镜面反射的小数,为1时将禁用菲涅尔
  • Normal:可在此处输入法线,影响菲涅尔效果的渲染方式

利用Fresnel节点,将效果控制在物体的边缘轮廓上。

添加火舌

这里通过添加“顶点扰动”从而形成“火舌”,本文顶点扰动用PerlinNoise来做。

因为这里希望火舌向上运动,于是依据顶点法线的方向判断是否需要扰动。法线向下的不需要添加扰动。顶点法线向上还是向下,用法线与物体世界坐标系中up向量点乘来判断(材质编辑器中的ObjectOrientation节点即为up向量)。

Unreal其他实现火焰的方案

  1. 利用材质编辑器中FlipBook节点对动画纹理进行随时偏移采样形成动画。
  2. 粒子系统也是常见的制作火焰的方式,尤其是对火焰移动的方向、速度、形态有较高要求时。

关于unreal材质的消耗

Unreal中材质节点到shader代码分为两个过程,材质编辑器中的节点图编译成HLSL代码,然后再把HLSL生成多平台的shader代码,每一个材质节点其实都是一片类HLSL代码的封装。一个材质会编译生成大量的shader源码来匹配不同光照不同材质特性的需求。游戏引擎为了设计师能便捷的进行设计,对于效率做出了些牺牲,虚幻材质系统一方面由于其可达丰富的材质类型而著名,又因为其缓慢的编译过程与冗杂的shader体量而诟病。因此为了游戏能具有更加流畅的表现,在使用虚幻材质时一定要注定材质优化。参考中给了一些材质优化的方法链接,之后我再渐渐总结一些材质优化方法。

参考

  1. Unreal Tournament Fire Material: Set (virtual) things ON FIRE!
  2. how video games make FIRE
  3. UE4 - Ring of fire on a sphere effect - Test (Material bp shown)
  4. [UE4]性能优化指南(程序向)
  5. UE4材质编译加速
  6. UE4 Shader 编译以及变种实现

在阅读《Unity Shader入门精要》时,跟着此书实现了案例shader,今天把第十四章介绍的卡通风格渲染复现了一遍。这是一个简单但被广泛运用的效果。记录一下。感谢乐乐女神~

思路

渲染效果如下图所示:

书中给出的思路是:1.双通道描边;2.对高光反射进行处理,使得出现颜色统一的色块,而不是真实感渲染中的均匀高光反射区域,

描边

在Pass1中将所有顶点沿着外法线方向向外扩大一点,只渲染内表面。值得注意的是,书中源码在摄像机坐标系中扩大顶点。Pass1中将扩大后的内表面渲染成黑色。

在Pass2中正常渲染对象,结合先渲染的Pass1中的扩大版黑色内表面,以形成黑色描边。

Unity以三角形的顶点顺序区分正面还是反面(外面or内面),顺时针为外表面,逆时针为内表面。Cull Off就是正反面都看见,Cull Front就是只看见内表面,Cull Back只看见外表面(这个比较常见)

在Pass1中实现描边。实现代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
Pass
{
NAME "OUTLINE"
Cull Front

CGPROGRAM
#pragma vertex vert
#pragma fragment frag

#include"UnityCG.cginc"
struct a2v
{
float4 vertex : POSITION;
float3 normal : NORMAL;
};

struct v2f
{
float4 pos : SV_POSITION;
};
float _Outline;
fixed4 _OutlineColor;

v2f vert (a2v v)
{
v2f o;

float4 pos = mul(UNITY_MATRIX_MV, v.vertex);
float3 normal = mul((float3x3)UNITY_MATRIX_IT_MV, v.normal);
normal.z = -0.5;
normal = normalize(normal);

pos = pos + float4(normal,0)* _Outline;
o.pos = mul(UNITY_MATRIX_P, pos);

return o;
}

fixed4 frag (v2f i) : SV_Target
{
// sample the texture
fixed4 col = fixed4(_OutlineColor.rgb,1.0);
return col;
}
ENDCG
}

注意内置矩阵UNITY_MATRIX_MV,UNITY_MATRIX_P矩阵的使用。且法线的在坐标系之间的转换与点、向量的转换不一样,需要对矩阵进行逆、转置等变换。UNITY_MATRIX_IT_MV指的是modelview矩阵的逆转矩阵。且对于法线的坐标系间的变换,应使用33的矩阵,4*4的矩阵常用于对点进行变换。在摄像机坐标系中,固定法线的z值,可以使得外轮廓点朝着扁平的方向变化避免出现穿透。

均匀色块

漫反射项与高光项

在Pass2中实现卡通风格的色块,代替真实感渲染的漫反射与高光项。Pass2的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
 Pass{
...
struct a2v {
float4 vertex : POSITION;
float3 normal : NORMAL;
float4 texcoord : TEXCOORD0;
float4 tangent : TANGENT;
};

struct v2f {
float4 pos : POSITION;
float2 uv : TEXCOORD0;
float3 worldNormal : TEXCOORD1;
float3 worldPos : TEXCOORD2;
SHADOW_COORDS(3)
};

v2f vert (a2v v) {
v2f o;

o.pos = UnityObjectToClipPos( v.vertex);
o.uv = TRANSFORM_TEX (v.texcoord, _MainTex);
o.worldNormal = UnityObjectToWorldNormal(v.normal);
o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;

TRANSFER_SHADOW(o);

return o;
}

float4 frag(v2f i) : SV_Target {
fixed3 worldNormal = normalize(i.worldNormal);
fixed3 worldLightDir = normalize(UnityWorldSpaceLightDir(i.worldPos));
fixed3 worldViewDir = normalize(UnityWorldSpaceViewDir(i.worldPos));
fixed3 worldHalfDir = normalize(worldLightDir + worldViewDir);

fixed4 c = tex2D (_MainTex, i.uv);
fixed3 albedo = c.rgb * _Color.rgb;

fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * albedo;

UNITY_LIGHT_ATTENUATION(atten, i, i.worldPos);

fixed diff = dot(worldNormal, worldLightDir);
diff = (diff * 0.5 + 0.5) * atten;

//利用计算得到的漫反射光照值diff在_Ramp纹理中采样,_Ramp纹理只呈现三种颜色。
fixed3 diffuse = _LightColor0.rgb * albedo * tex2D(_Ramp, float2(diff, diff)).rgb;

fixed spec = dot(worldNormal, worldHalfDir);
fixed w = fwidth(spec) * 2.0;
//对计算得到的高光反射值spec进行处理,使大部分区域其变为非0即1的高光项,接近阈值的区域平滑从0-1过渡
fixed3 specular = _Specular.rgb * lerp(0, 1, smoothstep(-w, w, spec + _SpecularScale - 1)) * step(0.0001, _SpecularScale);
//fixed3 specular = _Specular.rgb * step(1-_SpecularScale, spec) * step(0.0001, _SpecularScale);

return fixed4(ambient + diffuse + specular, 1.0);
}

...
}

记录两个函数,step,smoothstep:

Unity中生成阴影

经典的生成阴影的方法是shadow map方法,Unity已经维护了屏幕空间的阴影映射纹理,作者学会利用Unity的技术的话,就可以非常迅速的得到阴影了……开启光源的shadow type,开启模型的Lighting选项中的阴影选项(receive shadow+cast shadow),注意渲染模型的shader里要能访问到ShadowCaster Pass(更新光源的阴影映射纹理从而更新屏幕的阴影映射纹理)。
在接收shadow的渲染对象的shader中,要想阴影显现,那要在fragment中,申明Unity和阴影相关的宏(如小节“漫反射项与高光项”中的代码)。

参考文献

  1. Unity Shader 入门精要
  2. Built-in shader variables - Unity官方文档
  3. GAMES101课件

卡通渲染中,常常需要描边的效果。这里记录一下遇到的几种产生描边效果的方法。包括《Unity Shader入门精要》12、13章提到的两种方法,与另两种离线的方法。

描边效果

美术风格有很多种,其中日式和美式有种完全不同的风格,国风更有其独一无二的特点。美式动画的代表风格之一为皮克斯风格(如下图右所示),日式动画的代表风格之一为宫崎骏风格(如下图左所示)。

日式动画常基于二维美术风格表示,在色块外有着明显的边缘来区分形状,描边是这种风格的特点。当然,随着各种不同风格的碰撞与发展,描边成为各种不同美术风格的重要元素了(如下图所示),具体的描边效果,随着风格的不同而有区别。本文记录一下遇到的几种实现不同风格描边的方法。

法一:屏幕后处理中进行边缘检测

利用图像处理中的边缘检测算子,检测图像中的边(像素值跳变大的点视作边)。

法二:屏幕后处理中进行边缘检测feat.深度纹理

在法一中,边缘检测算子只在原渲染图像中卷积,采集的边只是色块相交处的边缘,而不是实际物体的边缘。改用深度纹理(或者深度法线纹理)进行卷积,能够检测出深度跳变大的点,而往往是物体的实际边缘(如下图所示,图片来自乐乐的教程)。因此这种方法更为常见。

法三:双通道渲染

这种方法思路比较简单,通过两个Pass实现描边的效果,详见这篇blog。第一个Pass在摄像机空间中沿着法线方向扩大顶点坐标,将对象渲染成黑色,且只渲染背面;第二个Pass正常渲染,且只渲染正面。这样叠加两个通道就得到了具有描边的效果。

法四:提取轮廓边单独渲染

这种常用于离线的边缘检测,对模型的所有边进行判断,判断是否是边缘(边的相邻三角形与视角的点乘正负,一正一负则为边)。这种方法常用于一些需要风格化边缘的情况。因为将边的信息提取出来后,可以用shader对边进行风格渲染。

在阅读《Unity Shader入门精要》时,跟着此书实现了案例shader,今天把第十三章介绍的屏幕后处理之全局雾效复现了一遍。记录一下。感谢乐乐女神~

雾效效果

添加雾效前的场景:

添加雾效后的场景:

该雾效脚本中FogDensity为0.72

思路

在屏幕后处理阶段添加雾效,在camera中添加雾效脚本,使得在Y轴方向根据位置高低,将原渲染结果与雾以不同系数混合输出最终效果。

雾效混合系数与其计算公式

需要一个混合系数f,来混合原始rgb值与雾rgb值:

1
float3 afterFog = f * FogColor + (1-f) * origColor;

书中采用类似线性的雾效公式来计算f:
$$
f ={H_{end}-h}\over{H_{end}-H_{start}}
$$
如果h是指世界空间Y轴坐标的话,可见随着海拔增高,f越小,则雾越淡。

世界坐标的求解

因为雾效混合系数的计算需要渲染位置的世界坐标,因此在后处理shader中计算像素的世界坐标是必须的一步。很容易想到用View矩阵和Projection矩阵的逆矩阵来计算屏幕像素的世界坐标,但这种需要在fragment shader中进行矩阵运算的方法性能不佳,因此需要另外一种方法。

另一种方法,就是利用相机的世界坐标与像素位置相对于相机的偏移来计算像素的实际世界坐标,即根据公式:

1
float4 worldPos=_WorldSpaceCameraPos+lineDepth*interpolatedRay;

其中lineDepth可以由相机的深度纹理直接得到,interpolatedRay是每个像素与相机相关的射线向量,该向量记录了位置与方向信息。
且interpolatedRay是由硬件插值计算来的,使得消耗更小的算力。

camera深度纹理的获取

在脚本中设置camera的深度纹理类型:

1
camera.depthTextureMode |= DepthTextureMode.Depth;

在shader中布置深度纹理变量,并获取对应像素位置的深度值:

1
2
3
4
5
...
sampler2D _CameraDepthTexture;
...
float linearDepth = LinearEyeDepth(SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture,i.uv_depth));
...

shader

关键代码如下:

1
2
3
4
5
6
7
8
9
10
11
fixed4 frag(v2f i) :SV_Target{
float linearDepth = LinearEyeDepth(SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture,i.uv_depth));
float3 worldPos = _WorldSpaceCameraPos + linearDepth * i.interpolatedRay.xyz;
float fogDensity = (_FogEnd - worldPos.y) / (_FogEnd - _FogStart);
fogDensity = saturate(fogDensity * _FogDensity);//Clamps x to the [0, 1] range.

fixed4 finalColor = tex2D(_MainTex, i.uv);
finalColor.rgb = lerp(finalColor.rgb, _FogColor.rgb, fogDensity);

return finalColor;
}

记住个shader中常用的函数saturate:

参考文献

Unity Shader 入门精要

《Unity Shader入门精要》第十二章屏幕后处理技巧中介绍了运动模糊的实现,因为涉及的知识点较多,因此单记一篇笔记,如有错误的理解,望各位指正。感谢乐乐女神~

透明度测试 与 透明度混合

实现透明效果主要有两种方法,一种是透明度测试,一种是透明度混合。透明度测试即在shader中加入clip()函数,裁剪掉小于透明度阈值的片元,无法实现半透明的效果。透明度用来实现半透明的效果,更加复杂。

使用透明度混合,必须要添加ZWrite Off与Blend语义,告诉Unity不将透明物体的深度写入深度缓冲,并且要混合当前片元与颜色缓冲中的片元RGB值,利用Shader中frag返回的alpha通道值混合。混合的模式有多种方式:

1
2
3
4
Blend Off //不混合,直接用当前片元覆盖颜色缓冲区
Blend SrcFactor DstFactor //利用当前片元的透明度混合 颜色缓冲区的rgb与当前片元的rgb
Blend SrcFactor DstFactor,SrcFactorA DstFactorA //赋予新的透明度因子SrcFactorA DstFactorA进行混合
BlendOp BlendOperation //指定别的方式BlendOperation进行混合

其中第二种混合方式公式如下(当SrcFactor为SrcAlpha ,DstFactor为OneMinusSrcalpha时):
$$
DstColor_{new}=SrcAlpha \times SrcColor + (1-SrcAlpha) \times DstColor_{old}
$$
其中$DstColor$为颜色缓冲区中的值。

ColorMask A指的是只对alpha通道进行写值。

屏幕后处理脚本系统

屏幕后处理,就是对执行完所有透明与不透明的Pass后的场景进行抓取,将抓取到的屏幕存在纹理中,对抓取的纹理进行一些处理,将变化后的纹理再显示再屏幕中。unity已经提供了抓取屏幕的接口—–OnRenderImage,函数声明如下:

1
MonoBehaviour.OnRenderImage(RenderTexture source,RenderTexture destination);

当在脚本中声明此函数时,Unity会将当前渲染得到的屏幕图像存储在参数source中,此函数中的实现对source的操作,最终处理后的屏幕图像保存在destination中,Unity会将destination绘制在屏幕中。在OnRenderImage中我们通常用Graphics.Blit函数实现对纹理的变换,Blit函数声明如下:

1
2
public static void Blit(Texture src,RnederTexture dest);
public static void Blit(Texture src,RnederTexture dest,Material mat,int pass=-1);

Unity设计Blit函数帮助我们变换屏幕纹理,Blit中会将src传给mat中的“_MainTex”,利用mat进行修改,将结果返回给dest,pass默认为-1表示会依次执行mat中所有的pass,否则就会调用指定的Pass。

运动模糊法1

该效果主要思路就是:当前帧的rgb值取决于上一帧与实际当前帧的混合。

让画面运动起来

设置相机运动脚本,让画面动起来,再混合每一帧造成运动模糊的效果。在书中,想要获得相机绕固定点旋转的视角,改变相机的transform,lookAt矩阵即可。像机脚本的关键代码如下:

1
2
3
4
5
6
7
8
9
void Update () {
transform.position = Vector3.Slerp(transform.position, curEndPoint, Time.deltaTime * speed);//利用Slerp球形插值函数,随着Time而移动
transform.LookAt(lookAt);//改变像机视角矩阵 View
if (pingpong) {
if (Vector3.Distance(transform.position, curEndPoint) < 0.001f) {
curEndPoint = Vector3.Distance(curEndPoint, endPoint) < Vector3.Distance(curEndPoint, startPoint) ? startPoint : endPoint;
}
}
}

shader

在shader中开启ZWrite Off与Blend语义,以混合颜色缓冲区中上一帧的RGB与当前渲染帧的RGB以达到模糊的效果。设置两个Pass,Pass1往颜色缓冲中写RGB值,并且利用_BlurAmount混合前后帧率,Pass2往颜色缓冲区中写入原有屏幕的Alpha值。

1
2
3
fixed4 fragRGB (v2f i) : SV_Target {
return fixed4(tex2D(_MainTex, i.uv).rgb, _BlurAmount);
}

运动模糊法2

在书的第三章中介绍了另外一种实现运动模糊的方法。是作者受《GPU GEMS3》速度映射图启发的方法。利用深度图来得到屏幕所有像素的世界坐标(经过VP逆矩阵的转换),再用上一帧的VP矩阵得到当前像素点在上一帧的像素位置。在屏幕空间坐标系中计算速度,沿着速度路径平均采样。关键代码如下:

在后处理脚本中,需要拿到当前帧VP矩阵,和上一帧的VP逆矩阵:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void OnRenderImage (RenderTexture src, RenderTexture dest) {
if (material != null) {
material.SetFloat("_BlurSize", blurSize);

material.SetMatrix("_PreviousViewProjectionMatrix", previousViewProjectionMatrix);
Matrix4x4 currentViewProjectionMatrix = camera.projectionMatrix * camera.worldToCameraMatrix;//VP矩阵
Matrix4x4 currentViewProjectionInverseMatrix = currentViewProjectionMatrix.inverse;//逆矩阵
material.SetMatrix("_CurrentViewProjectionInverseMatrix", currentViewProjectionInverseMatrix);
previousViewProjectionMatrix = currentViewProjectionMatrix;

Graphics.Blit (src, dest, material);
} else {
Graphics.Blit(src, dest);
}
}

shader中关键代码在fragment shader中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
fixed4 frag(v2f i) : SV_Target {
//从深度图缓冲中得到当前采样点的深度值
float d = SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, i.uv_depth);
//H是映射到-1~1坐标系中的坐标
float4 H = float4(i.uv.x * 2 - 1, i.uv.y * 2 - 1, d * 2 - 1, 1);
//利用VP逆矩阵得到世界空间中坐标
float4 D = mul(_CurrentViewProjectionInverseMatrix, H);
//齐次除法
float4 worldPos = D / D.w;

//当前坐标
float4 currentPos = H;
//因为书中的所有物体的世界空间坐标不会变化,变得只是摄像机的世界空间位置与朝向, 因此可以根据VP矩阵计算出准确的上一帧屏幕像素坐标
float4 previousPos = mul(_PreviousViewProjectionMatrix, worldPos);
//齐次除法
previousPos /= previousPos.w;

//利用-1~1坐标系中的位置计算速度
float2 velocity = (currentPos.xy - previousPos.xy)/2.0f;

//沿着速度方向进行均值采样
float2 uv = i.uv;
float4 c = tex2D(_MainTex, uv);
uv += velocity * _BlurSize;
for (int it = 1; it < 3; it++, uv += velocity * _BlurSize) {
float4 currentColor = tex2D(_MainTex, uv);
c += currentColor;
}
c /= 3;

return fixed4(c.rgb, 1.0);
}

参考文献

Unity Shader 入门精要

ShaderLab基础

shader语法基本框架

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
Shader"ShaderName"{//定义Shader名字
Properties{//属性,cpu传给shader的值,出现在unity材质面板中,在Pass中要重新声明

}
SubShader{
//针对显卡A的shader
//此处设置标签1
//此处设置状态
Pass{
//此处也可以设置标签2
//此处设置状态
CGPROGRAM

//下面是两条关键的编译指令,告诉unity哪两个函数最最重要的
#pragma vertex vertname
#pragma fragment fragname

//此处定义cg代码

ENDCG

}
Pass{

}
}
SubShader{
//针对显卡B的shader
}
}

Pass的执行

Pass译为通道,一般渲染对象只经过一次Pass,效果是单薄的,多Pass渲染会让画面更加有趣。Pass可以命名,可以设置标签,可以指定某个Pass对对象进行渲染,也可以指定多个Pass进行渲染。

关于标签1常出现的字符串:

1
2
3
4
SubShader{
Tags{"RenderType"="Opaque" "Queue"="Geometry"}
...
}

RenderType

其中RenderType使用的值通常包括Opaque、Transparent、Background等,实际上是一种约定,用来区别这个Shader要渲染的对象。并且可以使用自定义值,影响不大,只是在代替渲染时,定义者要区分不同RenderType类型的Shader。

代替渲染解释:

1
2
Camera.SetReplacementShader("shader1","") //场景中所有的渲染对象的shader都替换成shader1进行渲染
Camera.SetReplacementShader("shader1","RenderType") //场景中所有RenderType类型与shader1的RenderType一致的渲染对象,其shader都替换为shader1,再渲染

Queue

Queue渲染队列,用来指定当前shader渲染对象的渲染顺序,其值有Background、Geometry、AlphaTest、Transparent等,
乐乐在书中介绍,队列索引号越小说明越先被渲染:

关于标签2中常出现的字符串:

1
2
3
4
Pass{
Tags{"LightMode"="ForwardBase" }
...
}

程序员指定渲染路径LightMode,是想配置光照属性与渲染流程。这是程序员与Unity的一个暗号。详见这里

状态设置

SubShader或Pass中常出现一些状态设置:
: Cull
: ZTest
: Zwrite
Blend
在SubShader中出现,表示该SubShader中所有的Pass都受状态设置的影响;在Pass中出现,说明只有当前Pass受状态设置的影响。

Shader中的系统语义

在定义输入struct的时,常遇到POSITION、SV_POSITION、SV_Target、NORMAL等语义,这些语义告诉Unity此处变量的输入输出类型,例如变量后有”:POSITION”指示Unity把模型的顶点坐标存到变量里,变量后有“:SV_POSITION”指示该变量存的值是裁剪空间中的模型坐标,“SV_Target”指示将输出输入到渲染缓冲中,一般为帧缓冲。

shader不报错

参考

  1. RenderType与Queue的理解
  2. Unity Shader 入门精要

在阅读《Unity Shader入门精要》时,跟着此书实现了案例shader,今天把第十二章介绍的屏幕后处理复现了一遍。包括屏幕明暗+饱和度+对比度、高斯模糊、运动模糊、Bloom、边缘检测等。记录一下。感谢乐乐女神~

调整屏幕的亮度、饱和度、对比度

书中对于三种效果的调整分别为:对于像素点pixel(R,G,B,A),调整亮度,将四个对应的分量按倍数调整即可;饱和度是色彩的一种属性,表示颜色的鲜艳程度、纯度,色彩越高越纯。用CG中的lerp函数对饱和度为0的颜色与原颜色进行插值,lerp函数定义为:

对于对比度的话,对平均颜色(0.5,0.5,0.5)与原色彩进行插值。

至于为什么要这么调,要了解一下色相饱和度和HSL色彩模型….

关键shader

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

fixed4 frag (v2f i) : SV_Target
{
// sample the texture
fixed4 col = tex2D(_MainTex, i.uv);
fixed3 finalcol = col.rgb * _Brightness;//调整亮度

fixed luminance= 0.2125 * col.r + 0.7154 * col.g + 0.0721 * col.b;
fixed3 luminanceCol = fixed3(luminance, luminance, luminance);
finalcol = lerp(luminanceCol,finalcol,_Saturation);//调整饱和度

fixed3 avg = fixed3(0.5, 0.5, 0.5);
finalcol = lerp(avg, finalcol, _Contrast);//调整对比度
return fixed4(finalcol,col.a);
}

调整亮度的效果如:

调整饱和度的效果如:

调整对比度的效果如:

边缘检测

将图片中梯度变化大的像素点,染上黑色。样例中检测边缘,用的是边缘检测算子Sobel。

在vertex中计算检测算子覆盖的图像块像素值half2 uv[9]:TEXCOORD0;

1
2
3
4
5
6
7
8
9
10
11
12
 half2 uv = v.uv;

o.uv[0] = uv + _MainTex_TexelSize.xy * half2(-1, -1);
o.uv[1] = uv + _MainTex_TexelSize.xy * half2(0, -1);
o.uv[2] = uv + _MainTex_TexelSize.xy * half2(1, -1);
o.uv[3] = uv + _MainTex_TexelSize.xy * half2(-1, 0);
o.uv[4] = uv + _MainTex_TexelSize.xy * half2(0, 0);
o.uv[5] = uv + _MainTex_TexelSize.xy * half2(1, 0);
o.uv[6] = uv + _MainTex_TexelSize.xy * half2(-1, 1);
o.uv[7] = uv + _MainTex_TexelSize.xy * half2(0, 1);
o.uv[8] = uv + _MainTex_TexelSize.xy * half2(1, 1);

在fragment中利用检测算子对像素点卷积。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
fixed luminance(fixed4 color) {
return 0.2125 * color.r + 0.7154 * color.g + 0.0721 * color.b;
}

half Sobel(v2f i) {
const half Gx[9] = {-1, 0, 1,
-2, 0, 2,
-1, 0, 1};
const half Gy[9] = {-1, -2, -1,
0, 0, 0,
1, 2, 1};

half texColor;
half edgeX = 0;
half edgeY = 0;
for (int it = 0; it < 9; it++) {
texColor = luminance(tex2D(_MainTex, i.uv[it]));
edgeX += texColor * Gx[it];
edgeY += texColor * Gy[it];
}

half edge = 1 - abs(edgeX) - abs(edgeY);

return edge;//越小越是边缘
}

fixed4 fragSobel(v2f i) : SV_Target {
half edge = Sobel(i);

fixed4 withEdgeColor = lerp(_EdgeColor, tex2D(_MainTex, i.uv[4]), edge);
fixed4 onlyEdgeColor = lerp(_EdgeColor, _BackgroundColor, edge);//不是边缘的地方设为白色
return lerp(withEdgeColor, onlyEdgeColor, _EdgeOnly);
}

边缘检测效果如(分别是在原图做边缘检测,原图非边缘区域设为白色):

高斯模糊

对图片进行模糊有多种方法,其中比较常用的是高斯模糊。同上文说的边缘检测一样,高斯模糊也用到了卷积,卷积核被称为高斯核。利用高斯方程计算高斯核中每一个位置的值。高斯方程如下所示:

$$
G(x,y) = {e^{ (x^2+y^2) \over {2\sigma ^2}} \over {2\pi \sigma ^2}}
$$

其中$\sigma$一般取为1,$x$和$y$分别对应到卷积核的整数距离。计算出高斯核中每个位置的高斯值后,要对所有的权值除以权值和,避免图片变暗。因为大小为$M*M$的图片用一个$N*N$的高斯核进行卷积需要计算,采样次数达到$M*M*N*N$,当利用高斯核的可分离性质时,只需要计算$M*M*2*N$即可。

书中shader代码设置了两个Pass,分别计算两个高斯核。在script中设置了高斯模糊的迭代次数。script关键代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
private void OnRenderImage(RenderTexture source, RenderTexture destination)
{
if (material != null)
{
int rtW=source.width/downSample;
int rtH= source.height / downSample;
RenderTexture buffer0 = RenderTexture.GetTemporary(rtW, rtH, 0);//利用RenderTexture记录中间屏幕模糊结果
buffer0.filterMode = FilterMode.Bilinear;

Graphics.Blit(source, buffer0);//RenderTexture初始化

for(int i = 0; i < iterations; i++)
{
material.SetFloat("_BlurSize", 1.0f + i * blurSpread);
RenderTexture buffer1 = RenderTexture.GetTemporary(rtW, rtH, 0);
Graphics.Blit(buffer0, buffer1, material, 0);//指定material的Pass 0进行计算

RenderTexture.ReleaseTemporary(buffer0);
buffer0 = buffer1;

buffer1 = RenderTexture.GetTemporary(rtW, rtH, 0);
Graphics.Blit(buffer0, buffer1, material, 1);//指定Material的Pass 1进行计算

RenderTexture.ReleaseTemporary(buffer0);
buffer0 = buffer1;
}

Graphics.Blit(buffer0, destination);
RenderTexture.ReleaseTemporary(buffer0);

}
else
{
Graphics.Blit(source, destination);
}
}


shader中的关键代码为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
v2f vertBlurVertical(appdata v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
float2 uv = v.uv;
o.uv[0] = uv;
o.uv[1] = uv+float2(0.0, _MainTex_TexelSize.y * 1.0) * _BlurSize;
o.uv[2] = uv-float2(0.0, _MainTex_TexelSize.y * 1.0)*_BlurSize;
o.uv[3] = uv + float2(0.0, _MainTex_TexelSize.y* 2.0) * _BlurSize;
o.uv[4] = uv - float2(0.0, _MainTex_TexelSize.y * 2.0) * _BlurSize;

return o;
}

v2f vertBlurHorizon(appdata v) {

v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
float2 uv = v.uv;
o.uv[0] = uv;
o.uv[1] = uv + float2(_MainTex_TexelSize.x * 1.0, 0.0) * _BlurSize;
o.uv[2] = uv - float2(_MainTex_TexelSize.x * 1.0, 0.0) * _BlurSize;
o.uv[3] = uv + float2(_MainTex_TexelSize.x * 2.0, 0.0) * _BlurSize;
o.uv[4] = uv - float2(_MainTex_TexelSize.x * 2.0, 0.0) * _BlurSize;

return o;
}

fixed4 fragBlur(v2f i) : SV_Target
{
float weights[3] = {0.4026, 0.2442, 0.0545};
fixed3 sum = tex2D(_MainTex, i.uv[0]).rgb * weights[0];
for (int it = 1; it < 3; it++) {
//sum += tex2D(_MainTex, i.uv[i * 2 - 1]).rgb * weights[i];
//sum += tex2D(_MainTex, i.uv[i * 2]).rgb * weights[i];
sum += tex2D(_MainTex, i.uv[it * 2 - 1]).rgb * weights[it];
sum += tex2D(_MainTex, i.uv[it * 2]).rgb * weights[it];

}
return fixed4(sum, 1.0);
}

模糊效果如下图

bloom效果

bloom效果,会使画面很亮的地方变的更亮了,并且这些地方的外缘地区也会变亮,看起来就像漏光一样。实现的主要思路是:将图片(最后的屏幕其实也是图片)中较亮的地方挖出来,高斯模糊一下,然后与原图片叠加。shader中就是使用四个Pass来进行操作,第一个Pass挖亮区域,第二个Pass竖直卷积模糊,第三个Pass水平卷积模糊,第四个Pass叠加。关键代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
...
fixed luminance(fixed4 col) {
return 0.2125 * col.r + 0.7154 * col.g + 0.0721 * col.b;
}

fixed4 fragExtractBright(v2f i) : SV_Target
{
// sample the texture
fixed4 col = tex2D(_MainTex, i.uv);
fixed val = clamp(luminance(col) - _LuminanceThreshold,0.0, 1.0);//clamp是CG中的截取函数,这里依据第一个参数的正负值使返回的值非0即1,详见下文解释
return col*val;
}
...
fixed4 fragBloom(v2fBloom i) : SV_Target {
return tex2D(_MainTex, i.uv.xy) + tex2D(_Bloom, i.uv.zw);//相加即混合
}


代码中用到了clamp函数,cg library中对clamp解释如下:

bloom效果如下图:

参考文献

Unity Shader 入门精要

0%