Unity用ShaderGraph做全套画面效果

内容纲要

暑期课报了陈老师的一个vr内容创作,然后开课组队,JeremyGuo发了一波个人简历,我于是蹭了一波啊嘿嘿嘿嘿,开了一波会确定了我们要做vr小游戏,建模美工文案都不缺,我们主要负责搭地图写程序,我主要负责画面效果和地图搭建 (且兼职设计...和优化) 。

本文接下来主要讲地图shader的制作。

由于能力有限,准备做一个低多边形卡通风格的画面效果。不会Cg语言,但是shadergraph很好用,快速上手之后也可以达到不错的效果。

文章末尾会附带所有shadergraph,先来几张目前的效果图。

接下来详细讲解一下我具体实现了那些效果,每个效果又是怎么用shadergraph实现的。

环境:Unity 2019.4 LTS

总览

需求大概有这些:

  1. 适用于低多边形风格,简单但能把相对单调的色彩渲染得更加鲜艳。
  2. 游戏需求中有实现“变更季节”的要求,需要提供丰富的色彩调节选项。
  3. 一个比较牛X的水的效果。
  4. VR一体机需要高性能的shader。

首先第一和第二条,我想要获得更多对画面的控制权,但奈何我不会Cg语言(也不会搞渲染管线,才给我一个周的时间,从零学习,我™还能能搞出点什么玩意...)。于是使用最基础的Unlit作为ShaderGraph的Master节点(最终渲染节点),这样我能控制更多画面元素,同时这也比较符合第四条的要求,因为Unlit渲染速度最快,但相对的,Unlit由于非常简单,基础的画面就很渣。

梦开始的地方

怎么办呢?由于Hineven有一定的涂鸦经验,决定做一个可以丰富自定义的shader,支持这些功能:

  • 亮面增色
  • 暗面增色
  • 明暗交界线增色
  • 阴影增色
  • 空气透视
  • 阴影边缘增色
  • 边缘光(反光)增色

然后按照套路(暗处和反光天光增强,亮处主光增强,明暗交界线以及阴影边界线饱和度提高并稍稍偏移色相来达到色彩丰富的效果)对shader的参数进行调整:

梦结束的地方

同样的,水面shader也单独制作:

基础shader

水面支持:

  • 双色的波动波纹
  • 顶点波(水面多面体的顶点会上下摆动)
  • 接收阴影
  • 太阳高光
  • 镜面反射
  • 水中物体随水面扭曲

然后调调参数:

加特技!加特技!

然后套全局反光bloom效果,让亮爆(Value大于1)的地方光线能溢出来。后期处理、还有一些小的特别shader,之后再提,先写一下怎么实现上面所说的通用shader和水面shader。

通用shader

通用shader是用来一般渲染非透明物体的shader,由于lowpoly的模型极度简单,通用shader不需要支持闭塞阴影贴图、法线贴图、自发光贴图等各种乱七八糟的玩意儿,由于游戏没有第二光源,我们只考虑主光源。

为了方便调色,通用shader还应该是一个可以高度自定义的shader。

固有色

固有色渲染没啥好说的,如果是纯色模式直接上用户定义的纯色,否然采样固有色贴图然后上菜即可。搞定之后输出的RGB值会接着进入下面的流程继续处理。

物体三大调分离

我们暂时忽略物体的投影,专注于单个物体。

三大调子在这里并不是亮灰暗,而是亮、暗、和明暗交界线。使用主光源方向向量和物体表面法向量点乘可以得到一个[-1, 1]之间的浮点数,那么区分这三大调子就很简单了,点乘结果靠近1的部分是亮面,结果靠近-1的部分是暗面,而在0附近的则处于明暗交界线之中,通过一些简单的运算就可以将物体三个面的mask分离。

由于我们需要更加丰富的色彩变换,因此尽管我们的渲染目标是“卡通”,但依然不会对这三个mask进行二值化。我们反而会对明暗交界线的mask进行平滑处理。本身明暗交界线的mask形状类似于(x-1)^2,我们需要套一个类似于x^2的下凸函数来解决掉它在原点的尖锐波峰(不然会出现一条难看的线)。出于某种玄学直觉我觉得1-cos(x)是一个很棒的函数,然后就给它套上去了。

既然处理处了mask,那么接下来就很简单,我让shader能利用这三个mask让用户可以对一个物体的三种不同区域(亮面、暗面、明暗交界线)分别进行调色,即,分别调整这些部分中混入的色彩以及混入的量,以及HSV(色相、饱和度、明度)的偏移值,然后在亮面塞一坨主光源的颜色,那么shader的第一部分就完成了。

阴影处理

接下来要处理的是物体的投影。Unity中物体投影的渲染非常粗暴。流程差不多是从平行主光源方向放一个(关闭透视的)摄像头,照一张照片,摄像头照的到的地方是亮的,其他地方是暗的,亮为1,暗为0。

然后通过一个shadergraph里的自定义函数节点,可以采样到某个渲染点阴影的亮暗值,由于实现上的困难采样的原始投影图会有很多噪点,为了祛除这些噪点做了一个区域取平均的模糊然后(凭借玄学直觉)套了一个三角函数,由于是卡通风,对投影直接做平滑二值化(套一个类似于sigmoid的跃变函数,只不过是从[0, 1]映射到[0, 1]),这样一搞,投影的mask也被咱弄出来了,有了mask之后一切按照上面继续:投影可自定义地增加色彩倾向,以及进行HSV平移。

但是有了投影之后,我们还需要处理一些其他问题。首先,物体的暗面和明暗交界线与阴影本来是不应该叠加的,但是现在我们的处理方式让这三个东西可以互相叠加(具体会不会叠加还和模型有关,这里不细说)。我们让暗面的亮度-0.2,再给投影的亮度-0.2,叠起来部分又在暗面又在投影的物体亮度-0.4,直接黑成一坨,非常难看。但是如果使用投影的mask将暗面的mask完全排除,又无法保留附加在物体暗面的色彩倾向以及投影效果。于是使用了一个这种的做法,从暗面mask中排除部分投影,这个排除的程度可以由用户自定义的一个浮点数调整。

明暗交界线面临类似的问题,按照以上方法类似处理。

接下来考虑投影边缘的问题,部分半透明材质(如草叶、皮肤和血肉)在光的照射下、明暗交界线以及投影的边缘会出现高饱和色彩,而将一些奇怪的色彩混在明暗交界线和阴影边缘中来提高色彩鲜艳的程度也是部分大佬的惯用操作。于是我们需要将阴影边缘提取出来单独调色。

提取阴影边缘的mask也很简单,上文说到为了给阴影图去噪点需要进行一个区域平均,这样非0即1的值就变成了一个平滑的值,取一段[0, 1]之间的较小的区间过滤阴影值,当成阴影边缘处理,类似的加上色彩倾向、HSV偏移以及去除阴影的模块即可。

边缘光

FresnelEffect是一圈围绕物体向内散发的光,不知道怎么搞出来的(或许是视角方向和物体法向量点乘?),但反正能拿来用。利用物体亮面的mask平滑一下去减Fresnel光,就能获得一个只存在于物体暗面的类似于反光的东西。出于渲染管线限制无法向这种地方塞环境光,于是塞个天光作为劣等替代品,然后套HSV偏移调色组件。

空气透视

据说一个比较科学的空气透视公式是1-(1/e^(x^2)),但是实测很难看,于是x/c作为空气透视的不透明度。越远的物体所受到的影响越大,很简单,完事。

调色

shader已经写好了,现在需要调整各类参数。我遵循的规则很简单,主光源统治的地方往主光源调,不受主光源统治的地方往天光调整,然后光越弱的地方固有色和饱和度越高,空气透视的颜色在天光和天空盒之间调和,而反光在天光和主光源之间选取一个极亮的低饱和颜色并调低透明度。

shader就完工了。

水面Shader

Lowpoly模型中不同的物体与光交互发生的反应都差不太多,所以一个通用Shader就能处理绝大多数的东西。但是水面材质很特殊,而漂亮的水面又可以缓解画面的单调性,于是给水面单独做一个Shader。

基础波纹

水本身没有固有色,但是受到天光四面八方的影响它会带有一点固有色。选择一个低饱和偏青的亮色作为主色,选择一个高饱和偏紫的暗色作为次色,然后搞一个GradientNoise

GradientNoise差不多长这样

在亮的地方填充主色,暗的地方填充次要色,中间过渡,同时让GradientNoise随着时间变化而在二维转圈圈、平移(随便套几个平滑的周期函数,作为位移来实现这一点),就做成了一个基本的水的“固有色”。

接下来使用刚刚的GradientNoise当成高度图来生成一个水的法线贴图,这样能更好的做出“带有许多褶皱”的波纹迭起的水面。生成的法线贴图先放在一边,等会会参与反光、高光和水底景物扭曲的计算。这张法线贴图可以代表水的基础波纹。

顶点波

为了更好模拟水的波动效果,顶点也会随着水波上下波动,我们选择在渲染顶点时为y高度附加一个随时间变化的值。为了避免两块水面的边缘出现裂痕的情况,必须使用一个二维周期函数套在世界xz坐标之和+时间上来附加到顶点高度y上。由于Hineven数学渣,二维函数只想到了三角函数,于是套了一个三角函数。

结果JeremyGuo看外效果演示:“你这个顶点是不是套了一个三角函数啊”...居然被他一眼看出来了,不愧是大佬。

接收阴影

直接在投影材质上采样,然后得到的mask加一固有的个平移值saturate乘到最后输出的rgb上即可。

太阳高光

太阳高光是由水面将主光源的光线镜面反射到玩家(摄像机)眼中形成的亮爆的高光。这个也比较简单,首先我们从基础波纹步骤拿到了水面的法线(波纹法线和模型法线混合而成的法线),然后用这个法线和主光源的方向算一个反射,把反射向量和视角向量(摄像机到物体的连线的归一化向量)点乘一下,小于一个阈值(比如-0.97)就认为是高光,mask上设为1。

但是这样的高光mask是比较简陋的。首先水进行镜面反射的能力和光线射入水面与水法向量的夹角有关,如果光线几乎垂直地射入水面,那么水最多进行折射而不是镜面反射了。这个和金属不一样,PBR中有一个材质属性叫做“金度”(metallic,我的垃圾翻译),如果金度接近1,意味着无论光线入射角度和模型法线夹角如何,都不会发生漫反射(或者折射),而是完全的镜面反射(这在物理世界中和金属的光电效应有关,具体不深究)。水当然不是金属,于是在光线入射角和水面发现夹角大道一定程度时是不会出现高光的。这俩向量点积一下,加上一,调调权重乘到高光的mask上去,去除不会出现的高光。

其次,在投影中显然是不会出现太阳高光,乘以一个阴影的mask减去阴影。

这里高光是最后加到RGB上的,且不用saturate,因为这样高光会“亮爆”,在bloom处理时会溢出来,更自然。

镜面反射

这个操作比较骚,是我从youtube上学过来的。

镜面反射相当于光线被反射了一次,如果按着光线走,镜面反射所呈现的图像所对应的相机位置应该是玩家的位置以水平面为轴对称过去,在地底下的一个相机位置。我们把镜面反射产生的图像理解为一个水面的动态固有色贴图,写一个脚本在地底下那个相机机位上绑定一个Reflection Probe(这玩意可以理解为一个低分辨率的全景相机),跟着玩家一起移动,实时渲染,渲染水面镜面反射的时候直接从这个Reflection Probe上采样,按照上文所说的反射规律乘以一个mask混合到固有色上去渲染即可。

缺点就是动态渲染Reflection Probe太™吃资源了,我们要做到VR平台上的话应该会考虑多搞几个Reflection Probe烘焙一下,做一个假的动态镜面反射欺骗一下玩家,反正没人会在意这些细节的,大致好看就可。

水中物体扭曲

这个东西比较好玩。

首先Unity的渲染方式很有趣,它优先渲染Opaque(不透明)的物体,然后你可以设置一下相机是否“Use Opaque Texture”,这样它把不透明物体渲染完后会将屏幕空间写入一个缓冲区里,接下来渲染Transparent(透明)物体时就可以从这个缓冲区里采样,做出各种效果。

我们把水面标注成Transparent,然后按照水的反射规律,视角与水法线夹角越小就越不容易镜面反射,剩下的除了一丁丁漫反射和吸收以外其它的光线都被折射了。UV坐标按照一个二维函数偏移一下直接从缓冲区采样,然后乘以视角与水面法向量点乘之后搞搞搞搞弄出来的一个mask,混合到最终输出结果上,就能做出水底物品扭曲效果了。

其它shader

还写了一个草的shader...让它能随风飘扬,主要是在一个低模十字上套半透明材质,然后改写顶点渲染器,让顶点能“随风飘扬”,比较naive就没啥好说的。

很恶心的一点是它比较吃性能...不敢放太多。

  • 上帝光

上帝光(God Light)其实就是体积光。体积雾用JeremyGuo搞的粒子系统代替,然后体积光,emmm,做个假的出来吧...

首先体积光实际上是一个套了特殊shader的圆柱,在圆柱表面渲染一个半透明的动态光束波纹,然后渲染一些亮爆(会bloom)的明灭相间不断移动粒子来代表灰尘,然后搞定了之后套两层,调一下参数,就好了。

  • 环境光

难道unlit加上shadergraph不烘焙能做出环境光吗?当然不可以!于是使用了一个非常劣质的粒子(随距离衰减)贴图贴在路灯上代替灯光...

后期处理

Bloom是一个很重要的效果,翻译的话叫做“全局泛光”。它的原理很简单,从渲染成品上滤出那些亮度大于一个阈值的颜色,跑一遍高斯模糊(这个模糊不一定高斯,反正就是模糊),然后叠加到原来的图像上。这样的话原图亮度地方的光线就会从边缘溢出来,很棒。

此条目发表在小项目分类目录,贴了标签。将固定链接加入收藏夹。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注