UnityShader[1]光照模型

目录

    • 计算机图形学介绍
      • 计算机图形学的主要研究内容
      • 图形显示系统
      • 可编程渲染流水线
    • 图形学流水线
      • 渲染管线的三个阶段
      • *渲染流程
    • 坐标空间与变换
      • 模型空间
      • 世界空间
      • 观察空间
      • 裁剪空间
      • 屏幕空间
    • 何为BRDF
    • UnityShader编程
      • 概念一览
      • Shader前准备
      • 漫反射与镜面反射
      • 了解光在模型上的成色原理
      • ShaderForge插件基础
        • ShaderForge实现光照模型
        • 镜面反射光照模型
        • 映射
        • 卡通渲染和RampTex
        • 法线偏移和高光、菲涅尔
        • 假的环境反射光照模型
        • UV、深度
        • Halftone
        • 关于Properties外部参数
        • 次表面散射
        • 环境光与环境光遮蔽
        • 投影
        • 为渲染添加法线贴图
      • Shader代码编写基础
        • Cg中的变量
        • UnityLab的函数
        • 手写兰伯特光照模型
        • 镜面反射光照模型
        • 环境光遮蔽
        • 投影
        • OldShoolPlus
        • 法线贴图模板
      • Matcap
      • 立方体纹理

计算机图形学介绍

计算机图形学(ComputerGraphics,CG)是使用数学算法,将二维/三维图形转化为计算机显示设备的栅格形式的学科。

二维/三维图形:贴图,模型等 显示设备的栅格:显示器上的一个个像素

计算机图形学研究的内容是将结构特征变为图像信号

UnityShader[1]光照模型_第1张图片

平直的横线经过旋转后发生走样:

![](https://img-blog.csdnimg.cn/675d12518fe046eb9557f077f25010e7.png)
UnityShader[1]光照模型_第2张图片
从图像信号变为结构特征,还有另外一个学科名为计算机视觉,与计算机图形学相反。计算机视觉的主要功用:人脸识别,车牌识别等。

图像信号的改变:数字图像处理,数字图像处理的主要功用:Ps、滤镜等。


计算机图形学的主要研究内容

建模(Modeling)、渲染(Rendering)、动画(Animation)、人机交互

建模:用点线面来表达一个物体的几何形态。(传统3D建模/雕刻软件(次世代)/扫描建模/程序化建模(类似SubstanceDesigner程序化贴图))

渲染:将图形数据利用数学算法转换成3D空间图像的操作,分为离线渲染和实时渲染

离线渲染:为达到画面真实,不计渲染成本,不考虑流畅性的复杂渲染,一般渲染时间较长(一般用于CG电影)

实时渲染:要保证图形数据的实时计算和输出,画面帧率要流畅,因此必须在画质上妥协。

动画:

序列帧动画:每一帧都需要绘制的帧动画

蒙皮动画:在建模软件中,使用骨骼蒙皮,创造骨骼动画

视效模拟:烟雾,爆炸,落雨,流体等视觉特效

计算机图形学的应用:

电子游戏、CG电影、动画、计算机辅助设计(CAD)等。


图形显示系统

计算机图形学将二维/三维图形经过一系列渲染管线转换为显示器上的栅格显示。渲染好的栅格画面作为一组数据被显示器读取,或被帧缓存读取。帧缓存存储了每一帧的画面信息,经由显示控制器,投放到显示器上。

早期的帧缓存是存放在内存中的,这使得显示控制器想要获取帧缓存中的帧信息就需要经过总线,导致画面加载变慢。而现在,在系统总线中加入图形显示处理器(显卡),将帧缓存存放到图形显示处理器中,并且进行显示处理,提高了渲染效率。

现在的图形显示,一般首先从硬盘中读取渲染所需信息加载到内存(RAM),然后一些数据又被加载到显卡的存储单元,即显存(VRAM)。大多数显卡对内存没有直接访问权力。

GPU:显卡的心脏

GPU(GraphicalProgressingUnit),又称显卡核心、显示芯片,是一种在移动设备上进行图像运算的微处理器。GPU的核处理器多,适合进行浮点运算和并行运算。但是管理控制能力较弱,功耗大。是大体上与CPU相反的处理器。一般的显卡商:因特尔集成显卡芯片,NVIDIA,AMD。

对显卡的了解可以优化Shader和图形处理的工作流程。


可编程渲染流水线

早期的图形编程是开发人员直接对底层的GPU进行操作,然后显示到图形设备上的。这样的图形编程虽然对GPU的可控制性更好,但是费时费力,而且因为面向硬件编程,它的跨平台性不好。

固定功能渲染流水线:
UnityShader[1]光照模型_第3张图片
标准图形函数库中封装了很多固定的算法。这种固定功能渲染流水线简单方便,可以跨平台(与硬件无关),但是控制权限低(因为从硬件函数库中抽象,封装出来,不能编写)。
UnityShader[1]光照模型_第4张图片
可编程渲染流水线:

使用编程功能干涉标准图形函数库。这种可编程渲染流水线简单方便,可以跨平台(与硬件无关),控制权限高(编程)

UnityShader[1]光照模型_第5张图片

图形学流水线

流水线:一种工业生产方式,每个生产单位只专注处理某一部分工作,快速稳固的完成总体工作,以提高工作效率与产量。

左图:一般生产方式 右图:流水线生产方式。同样的生产周期内,流水线生产比一般生产多生产了一倍的产品。

在这里插入图片描述

渲染流水线/渲染管线:通过引擎内部的算法,将三维场景中的物件展现到显示设备上成为二维画面的过程

主要功能:在给定相机、三维物体、光源、材质等诸多条件下,生成一幅二维图像


渲染管线的三个阶段

应用程序阶段Application 几何阶段Geometry 光栅化阶段Rasterizer

应用程序阶段(不可操控)

应用程序阶段的最主要目的有三个:

1,将渲染所需要的场景数据准备好,如顶点,模型,光照,材质;然后交给显存。

2,为了节省性能,可能还需要进行粗粒度剔除(culling)操作,将不可见的物体剔除出去不做渲染。

3,设置每个模型的渲染状态。如shader、纹理、贴图。

应用程序阶段最主要的输出是渲染所需的全部几何信息,即渲染图元,渲染图元可以是点、线、三角面等,会被输出给下一阶段。


几何阶段

几何阶段往往在GPU上进行,对每个顶点、多边形进行操作,几何阶段的重要目标就是把顶点坐标变换到屏幕空间中,交给光栅化处理。

几何阶段输出每个顶点在屏幕空间的二维坐标,以及它们的深度值、着色等相关信息给下一阶段。

顶点着色(可操控):对模型顶点进行颜色赋予,空间变换(模型变换视图变换)后,输出到下一阶段

​ 模型变换:通过对模型进行平移,缩放,旋转,镜像,错切等操作来调整模型的过程,以合理指定场景中模型的位置等信息

​ 视图变换:在场景中放置摄像机的过程,调整拍摄的位置,拍摄角度,焦距,拍摄

几何曲面细分(可选)(可操控):为得到更加光滑的模型,而进行顶点的增加(细分图元),如果API不支持,这一步可以移到应用程序阶段,即建模时建成高模

裁剪(可配置):通过已经通过视图变换处理好的顶点信息,放入裁剪空间,进行投影变换,将所有顶点转入一个规范化的空间内,方便下一步操作

​ 投影变换:将图片投影到一个新的视平面的过程

屏幕映射(不可操控):将裁剪得到的图形映射到屏幕,将几何阶段的结果输出到下阶段


光栅化阶段

光栅化阶段使用上一阶段传递的数据产生屏幕上的像素,并渲染最终图像,往往在GPU上进行。它需要对上一阶段得到的逐顶点数据进行插值运算,再进行逐像素处理。

三角形设置(不可操控):通过顶点着色得到的信息,将所有顶点拼接成一个个面片,供光栅化使用

三角形遍历(不可操控):遍历每个像素点,判断是否为三角形面片的所占位置,如果是,则存储,不是则剔除

片断着色/片元着色器(可选)(可操控):对遍历出来的每一个像素进行着色,可以自己定义不同的算法对此进行操控

合并/逐片元操作(可配置):某物体的某像素可能被另一物体遮挡或穿透(玻璃、滤镜),所以需要进行Alpha测试、模板测试、深度测试等一系列复杂操作,决定片元的可见性,通过所有测试的片元会被提取颜色信息,与已经存放到颜色缓冲区的颜色进行混合


*渲染流程

一般的渲染过程为: 模型->输入数据结构->顶点Shader->输出数据结构->像素Shader->渲染输出

输入数据结构 的过程:

主要功能为数据的选取与采集.下图为一般obj格式的模型模型文件内的数据结构

UnityShader[1]光照模型_第6张图片

UnityShader[1]光照模型_第7张图片
模型数据文件中包含的基本信息:

顶点的坐标信息(v开头,三维变量,意为XYZ三轴向坐标,顶点编号为代码顺序从上到下)

三角面信息(f开头,三维变量,意为构成三角面的三个顶点的编号,)

0 Polygons - 12 triangles 代表0个四边面,12个三角面.不使用四边面的原因:

UnityShader[1]光照模型_第8张图片

描述一个四边面需要四个三维变量,而描述一个三角面只需要一个三维变量

额外可能包含的信息:UV信息,顶点色信息,法线的向量信息…

这些信息经过输入数据结构的转换后,变为各种结构体存储到程序中,供后续渲染使用

相当于渲染管线的应用程序阶段


顶点Shader 的过程:

1,将模型的顶点信息转变为该顶点对应显示器屏幕中的位置信息(透视理论).

2,计算并赋予顶点的其他信息,如UV,顶点色,法线.

在代码中具体的表现形式为InputShader结构体经过顶点Shader后变为OutputShader的过程.

相当于渲染管线的几何阶段


输出数据结构 的过程:

将透视变换后的几何体构成信息推广至块面,这个过程主要是三角形顶点的方向信息推广到整个三角形块面上。三个相邻顶点的方向一致,说明这三个顶点构成一个三角形面,且这个三角形的朝向与这三个顶点的朝向相同


像素Shader 的过程:

将得到的输出数据结构信息,给定的材质信息,光照信息,摄像机信息整合后,通过在显示器上逐像素扫描的方式,将图像以它该有的颜色表达到它该出现的像素上.(深入刻画每一个块面)

相当于渲染管线的光栅化阶段


坐标空间与变换

渲染游戏的过程相当于把一个个顶点经过层层处理最终转换到屏幕上的过程。换句话说,顶点经过许多坐标空间最终被渲染到屏幕上。何为坐标空间?在Unity中,被挂载到Hierarchy下的对象都是主要以世界空间(WorldSpace)为坐标空间的,而被挂载到这些对象上的子对象,都是主要以其父对象为坐标空间的。坐标空间会形成一个层次结构,每个坐标空间都是另一个坐标空间的子空间,每个坐标空间都有个父坐标空间,对坐标空间的变换实际上是在父空间和子空间之间对点和矢量进行变换,这其中涉及到矩阵运算。我们需要在不同情况下使用不同坐标空间,因为一些概念只在特定坐标空间下才有意义。坐标空间的变换贯穿整个图形学流水线的几何阶段。

模型空间

模型空间(ModelSpace)也被叫做对象空间(ObjectSpace)、局部空间(LocalSpace)。每个游戏对象都有自己的模型空间,当它移动或旋转的时候,模型空间也会随着它移动旋转。在Unity中,模型普遍使用左手坐标系,即+x为右、+y为上、+z为前。而前后左右上下这些方向代名词一般被称为自然方向。在模型制作时,如果成果需要导入Unity中,一般会约定将z轴正方向作为模型的前方向导出模型

UnityShader[1]光照模型_第9张图片

此外,制作模型的过程也包括规定模型上每个顶点在模型空间中的坐标位置、规定模型原点位置和三轴向

世界空间

世界空间(WorldSpace)是Unity中的最大空间,是最外层的坐标系。世界空间可以用来描述绝对位置。一个物体的Transform组件中的PRS属性都基于它的父节点,如果这个物体没有父节点,那么Transform组件中的PRS属性代表该物体在世界空间下的PRS属性

UnityShader[1]光照模型_第10张图片

顶点着色(顶点变换)的第一步就是将顶点坐标从模型空间变换到世界空间中,这个变换通常被称为模型变换(ModelTransform)。即模型空间中每个顶点的坐标信息和世界空间中该模型空间的坐标信息,通过矩阵运算得到每个顶点在世界空间中的坐标信息

观察空间

观察空间(ViewSpace)指的是摄像机的模型空间,属于模型空间中比较特殊的一种。观察空间特殊在它的+z方向为后方,即唯独观察空间使用了右手坐标系,这是为了符合OpenGL传统。这样的差异一般不会对编程有影响。

观察空间与屏幕空间是不同的,观察空间是一个三维空间,而屏幕空间是一个二维空间,从观察空间到屏幕空间的转换需要经过投影操作。

顶点着色(顶点变换)的第二部就是将顶点坐标从世界空间变换到观察空间,这个变换被称为观察变换(ViewTransform也叫视图变换),即世界空间中每个顶点的坐标信息,通过矩阵运算得到每个顶点在观察空间中的坐标信息

裁剪空间

在观察变换后,顶点需要从观察空间变换到裁剪空间(ClipSpace齐次裁剪空间)中,用于变换的矩阵被称为裁剪矩阵(ClipMatrix)或投影矩阵(ProjectionMatrix)。裁剪空间的目的是为了方便对渲染图元(面片)进裁剪,这块空间是由视锥体确定的,完全位于这块空间外部的图元会被剔除,完全位于其内部的图元会被保留,而与这块空间相交的图元会被裁剪

UnityShader[1]光照模型_第11张图片

视锥体的6块平面决定了裁剪空间的大小,而有两块裁剪平面比较特殊:近裁剪平面和远裁剪平面。它们决定了摄像机可以看到的深度范围,下图的立方体超出远裁剪平面外的部分被裁剪掉了。

UnityShader[1]光照模型_第12张图片

对于视锥体来说,判断一个点是否在视锥体内部很麻烦,更加简洁的办法是:使用投影矩阵把顶点转换到裁剪空间中。经过投影矩阵变换后,顶点坐标xyzw的w分量有确定范围的作用。在投影矩阵前,顶点的w为1,矢量的w为0。投影矩阵后,w有决定该顶点是否在裁剪空间外的作用,如果顶点的x、y、z都在这个范围内,说明该顶点位于裁剪空间内。

在Unity中,六个裁剪平面由Camera组件中的参数和视图的纵横比确定,我们可以通过Camera组件中的Projection确定摄像机使用透视视图还是正交视图,FOV(FieldOfView)属性改变视锥体的纵向角度,ClippingPanes决定视锥体的近裁剪平面和远裁剪平面的距离。而视锥体的横向信息由视图的纵横比与ViewPortRect共同决定。

UnityShader[1]光照模型_第13张图片

如果一个顶点在视锥体内,它变换后的坐标必须满足:-w <= x <= w;-w <= y <= w;-w <= z <= w

屏幕空间

在完成投影矩阵变换、且完成裁剪操作后,就需要进行真正的投影。需要将视锥体投影到屏幕空间(ScreenSpace)中,经过这一变换后,我们就能得到真正的像素位置。

首先,我们需要进行标准齐次除法/透视除法,就是用齐次坐标系的w分量除以xyz分量,在OpenGL中,这个过程得到的坐标位置被称为归一化的设备坐标(NDC)。经过透视投影的裁剪空间坐标,继续经过齐次除法后会变换到一个立方体内,在OpenGL中这个立方体的范围是[-1,1],在DirectX中这个立方体的范围是[0,1]。可以根据变换后的xy坐标来映射到输出窗口的对应像素坐标上。


何为BRDF

假设一束激光照射在桌面上,当人移动观察位置时,桌上激光的成像亮度会发生变换。观察位置不动,转而移动激光方向,成像亮度同样会发生变化。光方向和视线方向都不动,只移动桌子倾斜角度,成像亮度也会发生变化。也就是说桌面对于不同的入射角和反射角的组合,拥有不同的反射率,这个反射率决定了成像的亮度。

UnityShader[1]光照模型_第14张图片

为了在计算机中还原这个现象,出现了BRDF(bidirectional reflectance distribution function 双向反射分布函数)。该函数需要外部给定的参数有三项:vDir(视线方向),lDir(光线方向),nDir(物体表面法线方向)。拥有这三项数据后,可以计算得出当前入射角和反射角组合下的反射率。每当我们移动上述三个参数时,反射率都会随之改变。BRDF是所有光照模型的理论基础。诸如金属度、粗糙度这种对光照模型进行微调的参数不会修改物体的nDir,只是在其上对反射率进行二次调整,如反射率的密集程度、扩散广度、整体亮度等。


UnityShader编程

概念一览

可编程渲染流水线中的标准图形函数库:OpenGL、DirectX

OpenGL是在驱动层之上提供跨平台的图形标准API,高版本的OpenGL,如果想要向下兼容,需要考虑语法,API上的支持与否。

DirectX(DirectEXtension简称DX)是由微软创建的多媒体编程接口。

UnityShader的编程要依赖以上两种API进行。

GLSL:OpenGL着色器编程语言(OpenGLShaderingLanguage),用来在OpenGL中编写着色器的语言,是一种具有C/C++风格的高级语言

HLSL:高级着色器语言(HighLevelShaderLanguage),由微软拥有及开发的一种着色器语言,主要用于Direct3D,与OpenGL标准不兼容

Cg:CforGraphic,Cg是由NVIDIA与微软相互协作开发的一种高级着色器语言,与HLSL非常相似

ShaderLab:Unity中编写着色器的一种语言,是Unity在图形标准API的上一层再封装一层的标准语言。在 CGPROGRAM 与 ENDCG 之间可以使用 HLSL/Cg 编写。

计算机图形学:一门研究通过计算机显示二维/三维图形的学科

GPU:用于渲染二维画面的硬件

OpenGL/DirectX:与显卡驱动交互的图形标准API函数库

Unity:用于编写游戏客户端的引擎软件

Shaderlab:Unity中的Shader编程语言,是OpenGL/DirectX图形标准API的进一步封装

Unity中编写Shaderlab来通过OpenGL/DirectX告诉显卡驱动要做什么、怎么做,然后显卡驱动指挥GPU进行计算并输出到显示器


Shader前准备

Shader(着色器)是用来渲染图形的一种技术,通过shader,可以自定义显卡渲染画面的算法,使画面达到想要的效果。Shader是一段代码,用于告诉GPU如何绘制每个顶点的颜色和屏幕上每个像素显示的颜色

shader编程的两种方法:

代码编写:自由灵活,功能强大,性能可控,但上手困难。

可视化节点编辑:容易上手,无需代码,能快速出效果,但功能有限,性能很难最优。

可视化节点编辑的软件:

ShaderForge 已停止更新 最后支持2019版本

AmplifyShaderEditor ASE倾向于表面着色器

ShaderGraph Unity2018后内置的Shader编辑器,只支持自定义渲染管线(SRP)

Shaderlab的几种形式:

固定管线着色器(FixedFunctionShader) 对应固定功能渲染流水线 淘汰

表面着色器(SurfaceShader) 代码简洁 倾向于人类思维(颜色-法线-透明度) 来自于对图形标准API的封装,底层是顶点片断着色器

顶点片断着色器(Vertex/FragmentShader) Unity中最根本的着色器形式 功能最强大 分为顶点着色器和片断着色器

来自Shaderlab的模板

StandardSurfaceShader 表面着色器模板

UnlitShader 无光照效果着色器模板 用于在此之上添加光照效果 底层是顶点片断着色器

ImageEffectShader 后处理着色器模板 用于屏幕后处理(整体调色,bloom等)

ComputeShader GPU着色器模板 独立于普通渲染管线外 通常用于大量的并行计算

RayTracingShader 光线追踪着色器模板 暂不了解

模板只是初始代码,本质相同

编程环境IDE:VSCode

编写Shader推荐的Unity视图方式:

UnityShader[1]光照模型_第15张图片

漫反射与镜面反射

初中物理学过,光线与物体表面发生碰撞会发生反射,反射方向为:物体表面方向为法线,入射方向的对向。而由于光源大小的原因,向物体表面发射的平行光线肯定不止一条,此时反射光就会根据物体表面的粗糙程度改变效果。

通常情况下如果物体极度粗糙(如石膏像、投影屏),则反射光为漫反射(Emission)。漫无目的、四面八方、均匀散射

UnityShader[1]光照模型_第16张图片

如果物体极度光滑(如镜子,车漆),则反射光为镜面反射(Specular)。有目的,镜面方向,不均匀散射

]

Unity图形渲染中常用的向量及其通用称呼:

lDir:光方向。表示物体表面指向光源的方向。简称l。

nDir:法线方向。表示物体表面的垂直方向。简称n。

vDir:视线方向。表示物体表面指向摄像机的方向。简称v。

rDir:反射方向。表示光线碰撞到物体表面发生反射的方向。简称r。

hDir:半角方向。lDir和vDir的中间角方向。简称h。h代表Halfway。

Unity中的空间坐标系及其通用称呼:

OS:ObjectSpace 物体空间,本地空间。

WS:WorldSpace 世界空间。

VS:ViewSpace 观察空间。

CS:HomogenousClipSpace 齐次裁剪空间。

TS:TangentSpace 切线空间。

TXS:TextureSpace 纹理空间。

一般情况下,nDirWS代表世界空间下的法线方向

在UnityShader中,漫反射的实现方式为Lambert光照模型。不谈论光的强度问题,lDir与nDir夹角越小,物体表面的漫反射强度越大。因为物体表面的漫反射强度不随vDir的变化而变化,所以无论怎么观察,在光方向不变的前提下物体表面的指定位置漫反射强度都是不变的。

镜面反射的实现方式为Phone光照模型和Blinn-Phone光照模型。在镜面反射中,观察者的视角决定了反射光线的有无以及明暗,Phone光照模型:rDir与vDir夹角越小,物体表面的指定位置镜面反射强度越大(实际上是镜头承接到的镜面反射光越多)。且vDir必须在rDir的圆锥形范围内才能观测到镜面反射。Blinn-Phone光照模型:nDir与hDir夹角越小,物体表面的镜面反射强度越大(镜面反射光线最强的时候,rDir会与vDir重合,此时lDir与rDir的中线hDir刚好会与nDir相重合)。


了解光在模型上的成色原理

两种数据类型:

标量(Scalar),只有大小没有方向的量,如重量体积等;

向量(Vector),既有大小也有方向的量,如速度和力等;

向量的运算方法:

点乘/点积/Dot:两向量相运算,结果为标量,意为一个向量在另一个向量上的投影长度。图形学中,定义两向量点乘结果为+1时,两个向量方向相同。点乘结果为0时,两个向量方向相互垂直。点乘结果为-1时,两向量方向相反。

所以默认Shader下在计算光时,Shader先获取模型网格的法线方向(nDir,NormanDirection),光照方向的反方向(lDir,LightDirection);将法线方向与光源方向的反方向(保证结果为+1的位置是最亮的)进行点乘运算(nDir * lDir),结果为:法线方向与光源方向相反的地方最亮,相互垂直的地方最暗。

如下图,模型最亮部分点乘结果为+1,模型最暗部分点乘结果为0~-1。

负数是无意义的亮度,所以将所有负数的值都改为0。

计算方法为:对得到的点乘结果进行Max(0,nDir * lDir)运算,让所有负值结果变成0,非负值结果还是原结果

兰伯特光照模型(Lambert):


UnityShader[1]光照模型_第17张图片

但是显然这样的光照模型有多一半的面积是暗色的,一半的面积是纯黑色的。虽然具有真实感,但是却过于单调。所以有了半兰伯特光照模型

计算方法为:对得到的点乘结果进行 [(nDir * lDir) * 0.5 + 0.5] 运算,让-1变成0,0变成+0.5

这样的模型比原来的模型过渡更平缓,色彩更丰富,增加了透气感。


ShaderForge插件基础

前往GitHub获取ShaderForge工程文件,最高支持版本为Unity2019,下载后打开:

UnityShader[1]光照模型_第18张图片

本质上相当于一个Unity项目文件,但是在已经有现成项目的情况下不需要全部使用.打开Assets文件夹将ShaderForge包拖进Unity项目文件中,此时可以在顶层栏中的Tools中直接找到ShaderForge工具.

New Shader新建无光照Shader(Unlit),这些选项也只是模板

UnityShader[1]光照模型_第19张图片

右键创建好的Shader,新建一个Material,该Material使用的Shader就是新建好的Shader.将它挂载到场景中新建的球上,现在场景中的材质球与Shader Forge中显示的一样了.

UnityShader[1]光照模型_第20张图片

通过这种方式,可以将Shader Forge制作的Shader实时显示在场景中的一个物件的材质上,也反映了Shader的最终目的就是调整出一个符合需求的材质.

在ShaderForge的官网有节点文档:https://acegikmo.com/shaderforge/nodes/


ShaderForge实现光照模型

默认情况下ShaderForge中,空Shader的情况:

UnityShader[1]光照模型_第21张图片

视图中有两个节点,一个连线,分别代表一个颜色节点,主材质节点。它们之间的连线用于规定主材质当前显示该颜色。

在ShaderSetting中,可以在Path中设置该Shader的路径,用于分组。这个路径与Unity显示界面中的路径是不相同的,仅用来分组,而Unity中的路径为实际资源保存路径。在新建一个Shader时,应首先对Shader进行分组。

UnityShader[1]光照模型_第22张图片

对Shader的分组会体现在制作材质时,方便进行分组筛选:

UnityShader[1]光照模型_第23张图片

右键视图中的任意位置可以弹出创建新节点的对话框,可以按输出类型筛选需要的节点。

UnityShader[1]光照模型_第24张图片

进入VectorOperation(向量运算) / DotProduct(点乘运算),在视图中加入该节点。节点的左半边为输入接口,用来从其他节点接收参数;右半边为输出接口,用于将节点处理过的数据进行输出(return)。节点在刚被创建时,必须被赋值的输入接口会被打上感叹号,意为该接口为必填项。Dot节点用于输入两个向量信息,输出它们的点乘积信息。

UnityShader[1]光照模型_第25张图片

计算光照模型需要的两个向量,是法线向量和光照向量,右键新建节点GeometryData(几何体参数)/NormalDir.(法线向量);Lighting(光照)/LightDir.(光照向量)。进行连接后:

UnityShader[1]光照模型_第26张图片

Emission接口代表模型的自发光属性,代表了模型最终会发出什么颜色的像素;如果在默认情况下,蓝色Color接口接入,模型的每个像素发出的颜色都是蓝色。

此时被渲染出来的模型:

UnityShader[1]光照模型_第27张图片


之后进行兰伯特光照模型化操作:

新建节点Arithmetic/Clamp 01,用于将输入的值规定在01范围内,小于0的部分赋值为0,大于1的部分赋值为1。连接在Dot与Emission之间。输出结果与原先一样,这是因为默认情况下点积结果小于0处显示亮度本来就为0

UnityShader[1]光照模型_第28张图片


进行半兰伯特光照模型化操作:

根据之前推导的公式,需要对点积结果进行 [(nDir * lDir) * 0.5 + 0.5] ,此时需要Multiply节点和Add节点,并且需要一个静态值 0.5 ,用来参与乘法加法运算。在ConstantVectors(静态参数)组中可以找到各种数据结构的静态值,包括三维变量,四维变量,使用Value规定一个静态值,得到一个半兰伯特光照模型:

UnityShader[1]光照模型_第29张图片


镜面反射光照模型

Phone光照模型的实现:首先Phone光照模型的成像原理:光与物体表面发生碰撞,从碰撞点向反射光方向散射出一系列光线,如果反射光方向rDir与视线方向vDir越接近,反射光强度越大,反之越弱。

UnityShader[1]光照模型_第30张图片

Phone光照模型:首先通过lDir(光方向的反方向)与-1相乘得到光方向,使用Reflect结点,计算光方向在法线方向下的反射方向rDir。rDir与vDir点乘得到基本光照模型,然后使用Max限制最小值为0,最后使用Power(开方,相当于正片叠底次数)结点+滑块灵活控制反射光强度。

UnityShader[1]光照模型_第31张图片

Blinn-Phone光照模型则更加简单,不需要计算rDir,使用现成的hDir结点即可。

UnityShader[1]光照模型_第32张图片

UnityShader[1]光照模型_第33张图片

Phone与Blinn-Phone的区别:因为计算方式不同,效果也有一定区别,当光方向与视线方向几乎平行的这种极端情况下,Phone要比Blinn-Phone更加真实:左为Blinn-Phone,右为Phone。但是Phone的计算量要比Blinn-Phone大一些。

UnityShader[1]光照模型_第34张图片


映射

映射是指两个元素集之间相互对应的关系,如投影仪射影口的小型长方体框对应投影的大型长方体框,它们属于映射关系。通过映射,我们可以将兰伯特光照模型的色调进行改变,使它不再被局限于灰度值,而是上升到RGB。如将下图的纯灰度图形,作为UV中的U坐标,任取一常量为V坐标,从给定渐变色贴图(RampTex)进行采样,对每个像素进行重新着色。下面进行模拟:

[]

自制的渐变色(RampTex),为方便观察,V坐标方向上没有设置颜色变化:

UnityShader[1]光照模型_第35张图片

预测进行映射后得到的结果:


进行实操:

首先,需要构建一个UV坐标系,以原先计算得出的半兰伯特光照模型的点积结果作为U坐标(代表不同点积结果会映射不同的颜色),给定一个**固定值0.4(任意)**作为V坐标:

UnityShader[1]光照模型_第36张图片

然后将在PhotoShop中制作好的RampTex(渐变色贴图)导入进来。

构建UV坐标系的方法:

使用VectorOperation/Append(附加)操作,相当于为一个自变量附加一个应变量构成坐标系:

UnityShader[1]光照模型_第37张图片

从Unity资源栏中拖入任意贴图就能完成2D贴图节点的导入(外部可控节点的方框是浅绿色的):

UnityShader[1]光照模型_第38张图片

将得到的UV给RampTex后接入Emission:

UnityShader[1]光照模型_第39张图片

最终结果:

UnityShader[1]光照模型_第40张图片


对于VectorOperations/Append操作,也可以给一个二维变量附加应变量成为三维变量,提升数据的维度;最高能够提升到四维,也就是RGBA变量。也可以使用VectorOperations/ComponentMask操作从一个变量中提取它的所有子变量

UnityShader[1]光照模型_第41张图片

点击节点左下方的按钮修改输出的子变量是哪个。


渲染模型出现斑点的解决方法:

UnityShader[1]光照模型_第42张图片

在模型的末端或首端出现小斑点,这是模型对贴图进行采样时浮点数精度造成的误差,在渲染半兰伯特光照模型时,规定的灰度范围应当在0~1之间,但由于浮点数精度的影响,会在端点处出现误差,如在1位置出现1.0001;在0位置出现-0.0001,此时会去掉整数部分,变成0.0001,造成颜色突然变化。解决方法为将贴图的WrapMode(衔接方式)改为Clamp(紧凑)


卡通渲染和RampTex

使用半兰伯特光照模型+描边+卡硬过渡贴图模拟卡渲:

UnityShader[1]光照模型_第43张图片

使用的RampTex(卡渲3cut):

UnityShader[1]光照模型_第44张图片

其中包含的美术技法:

1,过渡处卡硬模拟卡通阴影和正面的反光效果;

2,暗部色相变化大,亮度变化小,模拟卡通的高饱和度效果;

3,亮部加入少许白斑,模拟高光(实际情况下不这么加);


炫彩猴头:

UnityShader[1]光照模型_第45张图片

可以使用下图这样的RampTex,发现不仅是U轴向,V轴向也有了颜色变化,这方便我们对RampTex的采样进行动态处理

UnityShader[1]光照模型_第46张图片

如:以点积结果为U轴,以游戏时间的小数部分为V轴(Time节点输出游戏时间;Frac节点让得到的值仅包含小数),模拟0~1的无限循环。配合上下相循环的RampTex,可以模拟虹色效果。

UnityShader[1]光照模型_第47张图片

UnityShader[1]光照模型_第48张图片


法线偏移和高光、菲涅尔

UnityShader[1]光照模型_第49张图片

只需要在设计RampTex时将亮部与暗部颠倒,就能模拟玉石的透光性。

UnityShader[1]光照模型_第50张图片

但是这样模拟的玉石效果不是很真实,需要点缀一些高光。使用法线偏移操作,可以让物体表面的高光位置发生偏移。首先定义一个简单的兰伯特光照模型:

UnityShader[1]光照模型_第51张图片

然后添加Properties/Vector4(四维变量参数),在Properties中的参数都可以在使用该Shader建立的材质中直接进行修改(外部可控),这个Vector4形的参数用于控制法线在三个轴向上的偏移量。将这个偏移量与从NormalDir中获取的法线方向相加(达到法线整体方向发生固定角度的偏移),进行归一化(将所有法线向量变为单位向量),然后再与光线方向进行点乘。

UnityShader[1]光照模型_第52张图片

这样就能得到一个光线方向可控(实际上是法线方向可控,当然也可以直接给光线方向加入偏移量)的高光模板了,使用法线偏移就能使高光在物体表面进行相对位移。使用点乘结果附加一个RampTex,并将得到的高光与原本的玉石效果相Add,就能得到:

UnityShader[1]光照模型_第53张图片

也可以使用一些新方法:

为点乘结果加入If判断,如果点乘结果大于0.9,则显示纯白,如果点乘结果小于0.9,则显示纯黑,这样的二值性也可以用来加高光。

UnityShader[1]光照模型_第54张图片

UnityShader[1]光照模型_第55张图片

使用同样的方法制作第二个高光后,可以使用Arithmetic/Max(两输入中每个像素取最大值输出)节点将两个高光合在一起,不使用Add的原因:使用Add可能出现0以下,1以上的值。最后使用Arithmetic/Lerp(蒙版)节点,以高光为蒙版,将玉石节点和新定义的高光色节点相运算(Lerp结点的A:底部;B:顶部;T:蒙版),得到最终结果:

UnityShader[1]光照模型_第56张图片

UnityShader[1]光照模型_第57张图片

也可以给在Shader中增加一菲涅尔反射效果,ShaderForge自带菲涅尔节点:GeometryData/Fresnel节点。该节点的输入Exp可以设置菲涅尔节点的衰减强度,给菲涅尔节点Multiply乘以一个颜色值,这个颜色就是菲涅尔效应的颜色,然后将其与原结果进行混合Blend,混合模式选择Screen(按透明度覆盖)。

UnityShader[1]光照模型_第58张图片

菲涅尔效应:视线与较光滑平面的夹角越大,平面折射外部光线的效果越强。放在图形学中的解释为:视线方向与法线方向的夹角越趋近于-1(反向),物体表面的环境光反射越弱,反之如果视线方向与法线方向的夹角越趋近于垂直,物体表面的环境光反射越强。反映到物体表面的现象是:物体边缘变亮。菲涅尔效应常出现在水面、玻璃、宝石等光滑表面。

UnityShader[1]光照模型_第59张图片

如上图所示:比较光滑的平面(水面)视线与水平面越垂直(1),来自水面的反射效果越弱,反之(3)越强。将Fresnel节点拆开是如下结构。不进行除负数操作的原因:因为点积结果的负值都在模型背面,不会被摄像机捕捉,此外如果选择了剔除模型背面的话则更好。

UnityShader[1]光照模型_第60张图片


假的环境反射光照模型

在漫反射与镜面反射中,光来自光源,但是在实际环境中,光不会只来自于光源,更多的来自于环境中的其他物品,它们吸收来自光源的光,通过漫反射或镜面反射发出加工后的光,这些光普遍是多且复杂的,并且实现起来开销很大,而如果少了这些光,会让场景中缺少真实感。当物体的粗糙度够高时,环境反射光不会很显眼。如果物体比较光滑,环境反射光就很重要,尤其在一些金属表面,如果没有合适的环境反射光会显得很奇怪。现在可以利用所学到的Phone+RampTex的方式,简单制作一些假的环境反射光照模型,开销不大,并且如果RampTex合适的话,看起来不会很违和。

UnityShader[1]光照模型_第61张图片

使用Phone光照模型进行两次使用。使用的RampTex:

UnityShader[1]光照模型_第62张图片

这样的环境反射光照模型虽然简单,但是实现效果不是很好,这个光照模型对模型起伏要求比较高,模型不规则时能达到更好的效果,而模型表面平滑时,效果就不那么好了:

UnityShader[1]光照模型_第63张图片

UnityShader[1]光照模型_第64张图片


UV、深度

UV实际上相当于一个坐标系,横坐标为U,纵坐标为V,UV代表了计算机显示器的屏幕坐标系,以屏幕中心为坐标系原点,U与V最大值都为1,最小值都为-1。因为UV会输出一个二位变量,所以显示出来的图像为RG图像(U为红值,V为绿值)。

UnityShader[1]光照模型_第65张图片

使用UV可以做到一些视觉效果:最简单如GeometryData/ScreenPosition就可以获取物体表面某点在UV坐标系中的位置。获取到该位置后,可以显示一张图片(最好是首尾相接的图片):

UnityShader[1]光照模型_第66张图片

UnityShader[1]光照模型_第67张图片


深度是摄像机相对于物体的一个属性,摄像机离物体越接近,这个值越小,反之越大。深度类似于距离;可以使用深度来对同一物体表面距离摄像机远近不同的地方进行视觉操控

在GeometryData/Depth获取深度结点,输出当前物体表面距离摄像机距离。

但是可以看到物体是全白的,这是因为该物体表面就算距离摄像机最近的地方深度也大于1。为此要减去一个常量:
UnityShader[1]光照模型_第68张图片

通过深度测量可以看到摄像机距离物体大约4个单位长度。(数值小于1的地方灰度下降)

将UV坐标与深度进行乘法运算,得到的结果为:图片在深度方向上发生偏移(越靠近摄像机的部位图片越大,越远离摄像机的部位图片缩小),这样能让图片随着摄像机远近发生缩放,产生近大远小效果,在视觉上形成图片的立体感。

UnityShader[1]光照模型_第69张图片
UnityShader[1]光照模型_第70张图片

一些美化处理:

将深度+UV图片算法得到的结果与正常的兰伯特光照模型使用Athemetic/Step(A<=B)结点(B小于A的部分为0,大于A的部分为1),将结果二值化:

UnityShader[1]光照模型_第71张图片
UnityShader[1]光照模型_第72张图片

由于兰伯特光照模型的暗部有负值,所以暗部一定小于深度+UV图片算法结果。如果亮部不够亮,可能是因为图片的线条间隔比较大,在兰伯特光照模型上增加一个值来增大亮部。

将结果作为遮罩附加颜色,BaseColor(A)在下,LineColor(B)在上

UnityShader[1]光照模型_第73张图片

UnityShader[1]光照模型_第74张图片

此外,还要添加一次颜色,这个颜色来自兰伯特光照模型,使用光照模型来控制颜色的明度。为点乘结果乘以一个颜色,这个操作与映射不同,映射根据点乘结果作为U坐标,根据灰度变化产生不同的过渡效果。而光照模型直接乘以一个颜色则会直接改变光照模型的颜色(改变色阶)。

UnityShader[1]光照模型_第75张图片

然后将其与结果相加,得到一个颜色错落有致的模型,在其上添加描边也能增加动感。最终结果:

UnityShader[1]光照模型_第76张图片
UnityShader[1]光照模型_第77张图片


Halftone

Halftone(半色调/灰度级)是一种反映图像亮度层次,黑白对比变化的指标,它通过网点的大小、密集程度区分明暗。生活中的图像分为两类,连续调图像(Continuous-Tone Image)和半色调图像(Halftone Image),连续调图像就是常见的由淡到浓/由深到浅的物质颗粒密度决定的图像。而半色调图像则是通过网点面积、覆盖率表现(网点可以是任何形状,一般是圆点和正方形)。半色调图像在美术方面也有比较好的视觉效果。

UnityShader[1]光照模型_第78张图片

为了制作Halftone图像,首先要对UV进行一些拓展。我们知道UV最大值为1,最小值为-1。

如果分别输出U值(横坐标)和V值(纵坐标)就能分别得到在横坐标和纵坐标上的-1~1的灰度渐变(也可以对UV图像使用VectorOperations/ComponentMask结点对RG两轴上的数值进行分别提取):

UnityShader[1]光照模型_第79张图片

那么如果将UV坐标乘以任意整数,就能将UV的大小扩大任意倍,因为只有0~1的部分回有灰度渐变,所以这个渐变就变得微乎其微了:

UnityShader[1]光照模型_第80张图片

此时使用Athematic/Frac进行取余操作,就能得到余数渐变构成的图像了:

UnityShader[1]光照模型_第81张图片

直接对UV图像进行取余可以得到栅格化图像(可以将每个栅格看成一个小型坐标系,横纵坐标最小值为0,最大值为1):
UnityShader[1]光照模型_第82张图片

拥有这样的栅格化图像后,我们需要将每一个栅格映射为一个圆点,方法:使用Athematic/Remap(Simple)结点将栅格的数据范围映射为(-0.5~0.5),然后使用VectorOperations/Length结点将数据距离0的长度计算出来(内部将RG两值所在位置距离0的距离使用勾股定理求出),输出得到一维数。

UnityShader[1]光照模型_第83张图片
得到了点阵图像,将它输出到物体表面:
UnityShader[1]光照模型_第84张图片

显然还需要进行光照映射,对Halftone的光照要求:暗部全暗,亮部全亮,过度部分要使用Halftone点阵特有的效果,越亮的部分点的面积越小,密度越小;越暗的部分点的面积越大,密度越大。问题是:怎么同时将亮部的点阵变小,将暗部的点阵变大。现在的图像的最大值为1,最小值为0,我们知道小数进行平方将变小,进行开放将变大,而平方与开放的区别为指数的正负。结论:在亮部对图像进行开方,在暗部对图像进行平方,为此我们需要将兰伯特光照模型的值颠倒后作为指数与栅格进行Power

我们选择将兰伯特光照模型进行反色映射,具体操作为Remap时将From数值进行反向映射。然后连接Athematic/power结点,这个结点的功能为:以Val为底数,Exp为指数进行平方,若Exp为负数则进行开方。

UnityShader[1]光照模型_第85张图片
UnityShader[1]光照模型_第86张图片
此时的视觉效果不够强烈,需要对图像的对比度进行强化,因为图像只有灰度,没有其他颜色,所以我们只需要简单进行四舍五入就能将图像二值化,可以使用Athematic/Round结点直接进行四舍五入,或使用Step(A <= B)也可以。最后加入边线为图像增加动感。成品:
UnityShader[1]光照模型_第87张图片
Halftone的shader在经过少量修改后可以有很多泛用性。


关于Properties外部参数

在描绘一个物体的材质时,并不是每个材质都需要一个shader,同一个shader可以向外部发出多个参数控件,有可能是贴图、三维向量、颜色、开关、滑块等。不同的材质在使用同一shader时,使用不同的参数就能达到不同的效果。外部参数表达了Shader的可拓展性

在ShaderForge中,绿色外边框的结点代表外部参数引入结点,被包含在Properties中:

UnityShader[1]光照模型_第88张图片

使用外部参数全局调控的PaintOff模型(Lerp同一个Noise完成底部与漆面Specular和Color的分离调控):

UnityShader[1]光照模型_第89张图片
UnityShader[1]光照模型_第90张图片


次表面散射

此表面散射 (SubSurfaceScattering,SSS)代表光线在物体内散射而形成的半透明效果,如在较暗的环境中用手将光源包裹住,手此时变成发红光的半透明状态,内部的血管隐约可见。红色光的散射更加剧烈,而绿色和蓝色光则更加集中。所以在美术上有时会在皮肤明暗交界处进行些许红色处理。

UnityShader[1]光照模型_第91张图片

次表面散射的目的是模拟材质的层级关系,达到半透明效果。最简单的次表面散射效果为修改RampTex的颜色渐变,在暗部加入红色。下图为次表面散射的RampTex:

UnityShader[1]光照模型_第92张图片

但是为了让次表面散射的程度可控,还需要修改RampTex的V轴向上的渐变,使得V轴值用来控制次表面散射的强度,并用滑块控制。这样在V轴向上有变化的RampTex被称为查找纹理 (LookUpTexture)。

UnityShader[1]光照模型_第93张图片

预积分算法为查找纹理的理论推导过程。实际效果仅在RampTex的基础上添加一个维度。因为有些算法在实际结点运算方面过于复杂,所以此时的做法是将其反映到一张图片上,根据较简单的公式计算UV坐标查找颜色

Lut图的功能不仅是SSS效果的表达,还可以运用在查找其他信息。

结点连接与成品:

UnityShader[1]光照模型_第94张图片
UnityShader[1]光照模型_第95张图片

预积分皮肤着色器(Pre-IntergratedSkinShading)插件将预积分算法写入Shader源码中,次表面散射通常用在皮肤,玉石等表面。

UnityShader[1]光照模型_第96张图片

环境光与环境光遮蔽

环境光(EnviomentLight)是图形学中光的一种,不同于其他光源,环境光没有具体的位置和方向等,它是环境中来自四面八方的不同颜色的光,在物理学中环境光是来自点光源,经过多次其它物体的吸收和反射后打到物体表面的光,在计算机图形学中这个过程是难以计算的,所以需要采用模拟的办法实现。最简单的方法就是在物体表面添加一个或多个颜色表示环境光。

环境光遮蔽(AmbicientOcclusion,AO)是模型基于UV的一种贴图,意在模拟模型与模型间的狭小空隙造成的采光度不好的闭塞阴影。我们知道环境光是从四面八方照射而来的,而模型某些部位因为四周被遮挡导致采光不好,接收到的光不多。因而时常处于黑色状态(闭塞)。引擎中为了模拟闭塞效果的影响,产生了AO。
UnityShader[1]光照模型_第97张图片
可以明显看出头部缝隙和底座缝隙的闭塞阴影得到了表现。

首先需要使用建模或贴图制作软件从模型上生成一张AO贴图,然后在Shader中将这张贴图贴到模型表面即可。如下图从SubstancePainter中生成一张2048x的AO贴图(AO贴图的生成来自于模型的顶点间间距),在Shader中将AO贴图与漫反射光照模型相乘即可得到 漫反射+闭塞光照模型
UnityShader[1]光照模型_第98张图片
UnityShader[1]光照模型_第99张图片
环境中的环境光不可能只有一种,设想以下情况中,环境光该如何表达:
UnityShader[1]光照模型_第100张图片

环境光中大体包含三种光色,且这三种光色来自不同方向,从上方而来的蓝色光,侧边而来的绿色光,以及底部的黄色光。此时为了获得上中下三种不同的光色效果,首先需要用Shader将模型进行三分,可以使用法线高度进行三分。Shader中获取的法线是根据模型角度计算过的世界空间法线

法线有RGB三通道,分别对应三个轴向的法线方向,每个通道都有从-11的值表示向量角度的-180180。其中G通道对应的是纵向法线角度,且无论怎么旋转,模型顶部永远为1,模型底部永远为-1,即物体某点的法线方向会随着模型在世界坐标中的角度变化而变化。此外,R通道对应模型从左(-1)到右(1)的法线方向,B通道对应模型从后(-1)到前(1)的法线方向。
UnityShader[1]光照模型_第101张图片
利用法线G通道对模型高度进行三分的操作很常见。节点实现:
UnityShader[1]光照模型_第102张图片

将三个分段分别加入颜色后相加、并加入AO贴图:
UnityShader[1]光照模型_第103张图片
某些效果可能需要对模型的多种贴图进行采样,如法线贴图、曲率、AO、厚度、粗糙度、金属度等,其中,大部分贴图仅需要一个维度就能进行描述(法线贴图为RGB三维),即单通道贴图。如果目标效果需要多个同模型的单通道贴图,可以在PhotoShop中将这几个贴图混合为同一张,其中RGBA通道包含不同贴图信息,如R通道为曲率、G通道为AO、B通道为厚度、A通道为粗糙度,然后在Shader中对这几张贴图进行拆分,这样即可省去很多次采样开销。


投影

在美术中,物体投影与物体漫反射效果一般是一起考虑的,漫反射的阴影是直接遮挡,而物体投影则是间接遮挡。物体某一位置到光源的路径上出现其他模型,从而发生光线遮挡导致物体该位置呈现阴影的效果被称为投影。在图形学中,投影与漫反射阴影需要分开考虑。

在ShaderForge中,有专门控制投影的节点:Lighting/LightAttenuation,其中记录了物体与光源的遮挡关系点光源的衰减信息
UnityShader[1]光照模型_第104张图片

实际上投影属于光源的遮挡信息,而AO属于环境光的遮挡信息,两者相结合能够构成较为完整的光照模型。尝试将上述内容进行结合:

UnityShader[1]光照模型_第105张图片
UnityShader[1]光照模型_第106张图片

使用这种渲染方式可以制作出很好看的底色材质。


为渲染添加法线贴图

到目前为止我们使用的光照模型大部分离不开法线,最简单的兰伯特光照模型会需要将法线与光方向进行点积得到结果。可以人为干涉法线。干涉法线的目的主要是为了让光照模型的结果更符合预期,当我们需要制作一些比较粗糙凹凸多变的表面,如砂岩、红砖等,此时增加模型的顶点,手动制作凹凸、或修改AO,让表面产生斑点等操作均可行,但是修改模型开销太大,修改AO效果不好,于是我们可以选择修改法线,让本来平缓的法线承接凹凸信息变成贴图,从而达到模型表面出现一些虚假纹理的效果,模型面数不变,而且纹理也会模拟光照效果。

UnityShader[1]光照模型_第107张图片

为此我们需要获取一张从模型上生成的法线贴图,可以在SubstancePainter中制作一些法线贴图,此处选择模拟石块。生成的法线贴图会需要在PhotoShop中将绿通道进行反向,否则效果发生偏差(这是SubstancePainter与UnityShader读取法线的差异造成的)。

UnityShader[1]光照模型_第108张图片

将法线贴图在Unity中完成设置,进入ShaderForge操作:需要在其中加入Code代码块进行操作(因为ShaderForge中没有UnpackNormal操作),这个操作可以对输入的NormalMap进行解码,赋予模型uv后输出模型切线空间法线信息。模型每个顶点都有自己的切线空间,毕竟法线贴图中包含的信息就是全部顶点在其切线空间内的朝向信息,切线空间法线信息是属于模型本身的信息,不会随着引擎中模型的旋转同步改变方向,因而无法达到作为光照模型使用的目的。
UnityShader[1]光照模型_第109张图片
然后就需要将切线空间法线信息(nDirTS)转换到世界坐标下,转换到世界坐标中的法线信息会随着模型旋转而同步改变方向,从而实现光照模型所用。为此我们需要获取切线空间到世界空间的转换矩阵(TBN)。TBN指代切线空间的三个轴向,分别为:TangentDir(tDir,切向,x轴)、BitangentDir(bDir,辅切向,y轴)、NormalDir(nDir,法向,z轴)。每个顶点都有自己的切线空间。到目前为止我们所使用的法线方向都是某顶点的切线空间的n轴正向。
UnityShader[1]光照模型_第110张图片

在ShaderForge中,可以在GeometryData中获取这三个信息。然后将这三个向量合成为3x3的TBN转换矩阵,这个矩阵就是将切线空间法线信息转换为世界空间法线信息的凭证,将其与切线空间法线信息做矩阵乘法后归一化即可作为法线信息使用。

UnityShader[1]光照模型_第111张图片

float3 nDirTS = UnpackNormal(tex2D(NormalMap, uv));
float3x3 TBN = float3x3(tDir, bDir, nDir);
float3 nDirWS = normalize(mul(nDirTS, TBN));
return nDirWS;

左图为切线空间法线信息、中图为世界空间法线信息、右图为使用自定义法线信息的兰伯特光照模型
UnityShader[1]光照模型_第112张图片


Shader代码编写基础

类似于学习一般编程语言时的HelloWorld,在学习shader时也要从最简单的代码开始,首先要使用ShaderForge获取最简易的Shader代码,进行以下设置以得到最简洁代码:

在创建Shader时选择最简单的Unlit模板,这个模板里没有任何黑盒操作,属于最简单模板。

结点处只使用一个三维变量作为颜色输出

ShaderSetting中将路径重命名

在Lighting中将LightingCount改为SingleDirectional,这是最简单的光照模式

在Geometry中将NormalQuality改为Interpolated(插值),不选Normalized(归一化)的原因是代码更简洁

UnityShader[1]光照模型_第113张图片
设置完毕后点击编译代码,双击Shader文件打开Shader代码:

UnityShader[1]光照模型_第114张图片
红色框选部分为ShaderForge识别用代码,直接删去即可;蓝色部分为形式段暂不需要去理解,需要时Copy即可;重点在剩余部分。
UnityShader[1]光照模型_第115张图片
通过面向对象编程的思想去理解:

Shader路径名为一个,它定义了ShaderCode/HelloWorld路径下的一个Shader

材质面板参数(Properties)为一个输入,由外部美术人员给予贴图、数值的接口

输入结构(VertexInput)为一个结构体,它表示由应用程序阶段输入的各种数据的数据结构,如顶点

输出结构(VertexOutput)为一个结构体,它表示输入数据经由顶点Shader处理后输出的数据

顶点Shader(vert)为一个方法,它进行了输入结构->顶点Shader->输出结构的过程

像素Shader(frag)为一个方法,它进行了输出结构->像素Shader->屏幕的过程

VertexInput输入结构实际上是appdata(应用阶段数据),方便记忆和分类也可以叫做VertexInput(顶点着色阶段输入数据)。VertexOutput输出结构实际上是v2f(VertToFrag顶点着色到像素着色数据),方便记忆和分类被叫做VertexOutput(顶点着色阶段输出数据)。

在VertexInput和VertexOutput中取出的数据一般都有自己的后缀,如:

float4 vertex : POSITION;
float4 normal : NORMAL;
float2 uv0 : TEXCOORD0;
...

在VertexInput中,前缀与后缀是固定匹配的。POSITION为后缀的变量一定是顶点信息,NORMAL为后缀的变量一定是法线信息等。在VertexOutput中,某些前缀不需要匹配后缀

代码里的float4类型的参数一般指代RGBA四值,也可用xyzw进行操作。

上述代码实现了一个给模型表面平涂绿色的Shader,效果如下:

UnityShader[1]光照模型_第116张图片

研究代码:

UnityShader[1]光照模型_第117张图片

Shader路径名内的路径为在Material中选择使用Shader时需要寻找的路径。

平涂绿色Shader不需要材质面板参数,所以内部为空。

输入结构中只需要模型的顶点信息,因为表面是统一绿色所以不需要输入表面法线信息。

输出结构中相应的只会输出顶点在屏幕上的信息,类似于输出一个画好模型的二维图片。

顶点Shader中只进行三维物体在二维平面上的换算操作。

在像素Shader中,会进行每个屏幕像素点上的着色,阅读像素Shader的代码,因为ShaderForge习惯使用结点的连接,在代码中的体现就是封装好的函数的调用,使得代码看起来很不直观,如下图:

UnityShader[1]光照模型_第118张图片

对上面代码进行一些修改:

UnityShader[1]光照模型_第119张图片

也能达到同样的效果:

UnityShader[1]光照模型_第120张图片

最简单的顶点/片元着色器的源代码:

Shader "ShaderCodeTest/SimpleShader"
{
    Properties
    {
    }
    SubShader
    {
        //适用于显卡1的Cg
        Pass
        {
            //Cg代码片段
            CGPROGRAM

            //此处编写Cg代码

            ENDCG

            //其他设置
        }
        //其他需要的Pass
    }
    SubShader
    {
        //适用于显卡2的Cg   
    }
    Fallback "Diffuse"
}

Cg中的变量

Cg语言中有7种数据类型。

float //32位高精度浮点数
half //16位浮点数
int //32位整形数 一般用于循环(减少使用)和数组索引
fixed //12位定点数
bool //布尔数据
sampler //纹理对象句柄 共有sampler sampler1D sampler2D sampler3D samplerCUBE samplerRECT六种
string //字符串,cg中基本不使用

此外cg中支持矩阵数据类型。

float2x4 matrix //2x4的矩阵,包含8个float类型数据

一般而言精度够用就行,例如颜色和单位向量这种数据,使用fixed(2-2,精确度为1/256)**就可以。其余尽量使用**half(6w-6w,精确到小数点后3.3位)。否则使用float。然而在PC平台不管Shader中写的是half还是fixed,都会被当作float来处理。half与fixed仅在一些移动设备上有效。

float、half、fixed这三个基本数据类型可以再组合成vector(向量)、matrix(矩阵)数据类型。如float4代表由4个float类型变量组成,fixed2x4代表由8个fixed组成。

在处理图形运算,特别是3D图形生成运算时,往往要定义一个fixed数据类型,我称它为定点数,定点数其实就是一个整形数据类型,他的作用就是把所有数进行转换,从而得到相应类型的整型表达,然后使用定点数进行整行运算,取到最终值并将其转换回实际的基本数据类型。因此它是通过避免大量的浮点运算来加快图形处理的一个方式

纹理sampler:默认情况下在移动设备上纹理会被自动转换为低精度纹理,如有需要可以使用以下方式声明:

sampler2D_float //高精度二维纹理
sampler2D_half //中精度二维纹理
sampler3D_float //高精度三维纹理
sampler3D_half //中精度三维纹理

在Properties中,我们会经常引入一些Shader外部数据/贴图,这些值是这样对应的:

int/float/range //浮点值表示,也就是float、half或者fixed,根据自己需要的精度来定义。
Vector/Color //float4、half4或者fixed4表示。
2D类型 //sampler2D表示。
3D类型 //sampler3D表示。
CUBE类型 //samplerCUBE表示。

例如,在Properties中声明了一个颜色和二维贴图(淡蓝色部分代表Shader内变量名、括号内橘色字符串代表材质处显示变量名、绿色部分代表材质处数据接口方式如颜色贴图滑块)。

UnityShader[1]光照模型_第121张图片

在Cg/HLSL中需要同样声明一次才能使用(默认位置在输入结构定义前),声明前可以加修饰符:

uniform:共享于vert、frac

attribute:仅用于vert

varying:用于vert、frac传数据

可以通过rgba来访问其四个分量,也可以使用xyzw访问,意义相同。通常情况下通过rgba访问颜色的四个分量,通过xyzw访问向量的四个分量。


UnityLab的函数

在进行图形学计算时,难免运用到矩阵运算,要用到线性代数的知识,但UnityLab中封装了一些来自OpenGL的接口,将其制成了库函数和关键字,专门用于一些必要且容易出错的矩阵运算,大大缩减了代码的体积,同时让一些矩阵知识不那么好的人也能使用UnityLab进行Shader编写,这也是使用Unity进行学习的一大原因。

此处记录本文用到的UnityLab的库函数和关键字便于查阅:

//空间变换算法:
float4 UnityObjectToClipPos(float4 vertex) // OS顶点位置 > CS顶点位置
float3 UnityObjectToWorldNormal(float3 normal) // OS法线方向 > WS法线方向
//常用算法:
float3 mul(float3 vertex_a, float3 vertex_b) // 向量乘法
float3 reflect(float3 direction, float3 normal) // 向量反射算法
float3 cross(float3 xDir, float3 yDir) // 叉乘算法,获取第三个轴向zDir的向量信息
float3 normalize(float3 direction) // 向量归一化(单位向量)
float4 tex2D(sampler2D tex, float2 uv) // 贴图采样函数,将tex按照uv贴到模型上,输出的是tex的RGBA信息
float4 texCUBElod(samplerCUBE cubeTex, float4(float3 dir, float mip)) //立方体纹理采样函数,将cubeTex以dir向量映射到模型上
float3 UnpackNormal(float4 normal) // 将贴图当作法线贴图进行解码,输出贴图内包含的法线信息
//关键字:
float3 _WorldSpaceLightPos0 // WS光源位置
float3 _WorldSpaceCameraPos // WS摄像机位置
float3 unity_ObjectToWorld // OS向量 > WS向量 转换矩阵
float4 UNITY_MATRIX_V // WS向量 > VS向量 转换矩阵 四维向量,使用时需要维度匹配

值得注意的是,ShaderLab中Cg的代码部分大部分都是需要封号结尾的,而在ShaderLab的其他部分,如Properties是不需要加封号的,因为这部分属于Unity在OpenGL函数库的基础上再次封装的信息。


手写兰伯特光照模型

分析兰伯特光照模型的制作过程:兰伯特光照模型需要模型法线方向和光照方向;不需要任何外部输入;每个像素点的颜色都为灰度,值为模型法线方向和光照方向的反方向的点积

//Shader保存路径
Shader "ShaderCode/Lambert" 
{
    //外部输入
    Properties 
    {
    }
    //形式代码
    SubShader 
    {
        Pass 
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #include "UnityCG.cginc"
            #pragma multi_compile_fwdbase_fullshadows
            #pragma target 3.0
            //输入结构
            struct VertexInput 
            {
                //输入模型顶点信息
                float4 vertex : POSITION; 
                //输入模型法线信息
                float3 normal : NORMAL;     
            };
            //输出结构
            struct VertexOutput 
            {
                //由模型顶点信息换算而来的顶点屏幕位置
                float4 pos : SV_POSITION;
                //由模型法线信息换算来的世界空间法线信息
                float3 nDirWS : TEXCOORD0;  
            };
            //输入结构->顶点Shader->输出结构
            VertexOutput vert (VertexInput v) 
            {
                //新建输出结构
                VertexOutput o = (VertexOutput)0;               
                //变换顶点信息到裁剪空间,并将其交给输出结构
                o.pos = UnityObjectToClipPos( v.vertex );
                //变换模型法线信息到世界空间法线信息,并将其交给输出结构
                //nDirWS代表NormalDirectionWorldSpace
                o.nDirWS = UnityObjectToWorldNormal(v.normal);  
                //将输出结构输出                                                
                return o;                                       
            }
            //输出结构->像素Shader
            float4 frag(VertexOutput i) : COLOR 
            {
                //获取法线方向nDir
                float3 nDir = i.nDirWS;
                //获取光方向lDir
                float3 lDir = _WorldSpaceLightPos0.xyz;
                //nDir点积lDir,点乘结果为两向量的夹角大小(-1~1)
                float nDotl = dot(i.nDirWS,lDir);
                //将负值归0
                float lambert = max(0.0, nDotl);
                //输出像素
                return float4(lambert,lambert,lambert,1);   
            }
            ENDCG
        }
    }
    FallBack "Diffuse"
}

因为不进行任何外部输入,所以材质面板参数Properties不进行任何操作。

因为需要获取模型法线方向,在输入结构中加入模型法线信息、在输出结构中加入由模型法线信息换算而来的世界空间法线信息、在顶点Shader中加入将模型法线信息换算成世界空间法线信息的算法

因为需要获取光方向,在像素Shader时通过**_WorldSpaceLightPos0.xyz**获取光方向的三维向量。

在像素Shader中要完成每个像素的点积运算,输出时将结果输出到RGB的每个通道上。
UnityShader[1]光照模型_第122张图片


镜面反射光照模型

Phone:需要获取摄像机到某顶点的方向vDir,所以首先需要在Shader中得到某顶点在世界坐标中的位置信息,然后与摄像机在世界坐标中的的位置信息进行向量计算得到vDir;此外需要向材质暴露一个接口,用于控制高光强度(正片叠底次数)。

Properties
{
	_LightPow ("LightPow",Range(0,90)) = 30 //对外接口(滑块0~90)
}
...
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
#pragma target 3.0
float _LightPow; //获取接口数据
struct VertexInput
{
	float4 vertex : POSITION;
	float3 normal : NORMAL;
};
struct VertexOutput
{
	float4 posCS : SV_POSITION; //顶点裁剪空间坐标信息
	float4 posWS : TEXCOORD0; //顶点世界空间坐标信息
	float3 nDirWS : TEXCOORD1; //顶点世界空间法线信息
};
VertexOutput vert (VertexInput v)
{
	VertexOutput o;
	o.posCS = UnityObjectToClipPos(v.vertex);
	o.posWS = mul(unity_ObjectToWorld,v.vertex); //mul()矩阵乘法
	o.nDirWS = UnityObjectToWorldNormal(v.normal);
	return o;
}
fixed4 frag (VertexOutput i) : COLOR
{
    float3 nDir = i.nDirWS;
    float3 lDir = _WorldSpaceLightPos0.xyz;
    float3 lDirRev = reflect(lDir*(-1.0),nDir); //光方向反方向
    float3 vDir = normalize(_WorldSpaceCameraPos.xyz - i.posWS.xyz); //摄像机朝向信息
    float3 phone = pow(max(dot(lDirRev,vDir),0.0),_LightPow); //计算高光(幂计算)
    return fixed4(phone,1.0);
}
ENDCG

UnityShader[1]光照模型_第123张图片
OldShool:一种很早期的光照模型,相当于将兰伯特光照模型和镜面反射光照模型进行结合。在Phone光照模型的代码基础上对片元着色器进行修改、添加颜色接口并调用。这里引入片元着色器的一般代码结构,四段法:准备向量 – 准备点积结果 – 准备光照模型 – 后处理+返回结果

Properties 
{
    _MainCol ("Color", Color) = (1.0, 1.0, 1.0, 1.0)
    _PowIndex ("LightPow", Range(0,90)) = 30
}
...
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
#pragma target 3.0

//输入结构
struct VertexInput
{
    float4 vertex : POSITION;
    float3 normal : NORMAL;
    float2 uv0 : TEXCOORD0;     //获取模型UV信息(0通道,共4通道)
};
//输出结构
struct VertexOutput
{
    float4 pos : SV_POSITION;
    float4 posWS : TEXCOORD0;
    float3 nDirWS : TEXCOORD1;
    float2 uv : TEXCOORD2;	//获取模型UV信息
};
//顶点Shader
VertexOutput vert (VertexInput v)
{
    VertexOutput o;
    o.pos = UnityObjectToClipPos(v.vertex);
    o.posWS = normalize(mul(unity_ObjectToWorld, v.vertex));
    o.nDirWS = UnityObjectToWorldNormal(v.normal);
    o.uv = v.uv0;   //输出UV信息
    return o;
}
float4 frag(VertexOutput i) : COLOR 
{
    //准备向量
    float3 nDir = i.nDirWS;
    float3 lDir = _WorldSpaceLightPos0.xyz;
    float3 vDir = normalize(_WorldSpaceCameraPos.xyz - i.posWS);
    float3 hDir = normalize(vDir + lDir);
    //准备点积结果
    float nDoth = max(dot(hDir,nDir),0.0);
    float nDotl = max(dot(lDir,nDir),0.0);
    //准备光照模型
    float lambert = nDotl;
    float blinnPhone = pow(nDoth,_PowIndex);
    float3 finalRGB = _MainCol.rgb * lambert + blinnPhone;
    //后处理+返回结果
    return fixed4(finalRGB,1.0);
}
ENDCG

巧克力质感:
UnityShader[1]光照模型_第124张图片


环境光遮蔽

环境光遮蔽的重点在于它需要一张基于UV的贴图,为此我们需要在代码中获取模型的uv信息,也要在外部面板参数部分加入2d贴图接口作为AO贴图,此处使用代码实现AO+Env3Col效果

Properties
{
    _Occlusion  ("环境光遮蔽", 2d)    = "white" {}	//获取外部纹理贴图
    _EnvUpCol   ("顶部环境光", COLOR) = (1.0,1.0,1.0,1.0)
    _EnvSideCol ("侧边环境光", COLOR) = (0.5,0.5,0.5,1.0)
    _EnvDownCol ("底部环境光", COLOR) = (0.0,0.0,0.0,1.0)
}
...
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
#pragma target 3.0

uniform sampler2D _Occlusion;	//内部定义纹理贴图
uniform float4 _EnvUpCol;
uniform float4 _EnvSideCol;
uniform float4 _EnvDownCol;
            
//输入结构
struct VertexInput
{
    float4 vertex : POSITION;
    float3 normal : NORMAL;
    float2 uv0 : TEXCOORD0;     //获取模型UV信息(0通道,共4通道)
};
//输出结构
struct VertexOutput
{
    float4 pos : SV_POSITION;
    float3 nDirWS : TEXCOORD0;
    float2 uv : TEXCOORD1;	//获取模型UV信息
};
//顶点Shader
VertexOutput vert (VertexInput v)
{
    VertexOutput o;
    o.pos = UnityObjectToClipPos(v.vertex);
    o.nDirWS = UnityObjectToWorldNormal(v.normal);
    o.uv = v.uv0;   //输出UV信息
    return o;
}
//像素Shader
float4 frag (VertexOutput i) : COLOR
{
    float nDirG = i.nDirWS.g;

    float maskUp = max(nDirG, 0.0);
    float maskDown = max(-1 * nDirG, 0.0);
    float maskSide = 1.0 - maskUp - maskDown;
    float4 var_Occlusion = tex2D(_Occlusion, i.uv);  //根据uv与环境光遮蔽贴图获取遮挡关系

    float3 finalCol = (_EnvUpCol*maskUp + _EnvSideCol*maskSide + _EnvDownCol*maskDown) * var_Occlusion.r;
    return float4(finalCol,1.0);
}
ENDCG

UnityShader[1]光照模型_第125张图片

代码中使用了函数:tex2D(),用来进行贴图采样。


投影

在Shader中进行投影代码的编写:

CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
#include "AutoLight.cginc"  			  //追加Unity投影必要的前缀
#include "Lighting.cginc"   			  //同上
#pragma multi_compile_fwdbase_fullshadows  //同上
#pragma target 3.0
struct VertexInput 
{
    float4 vertex : POSITION;
};
struct VertexOutput 
{
    float4 pos : SV_POSITION;
    LIGHTING_COORDS(0,1)   //投影用坐标信息,(0,1)代表占用 COORDS0 和 COORDS1
};
VertexOutput vert (VertexInput v) 
{
    VertexOutput o = (VertexOutput)0;
    o.pos = UnityObjectToClipPos( v.vertex );
    TRANSFER_VERTEX_TO_FRAGMENT(o)  //必要函数,将顶点转换为片段
    return o;
}
float4 frag(VertexOutput i) : COLOR 
{
    float shadow = LIGHT_ATTENUATION(i);    //获取投影函数
    return float4(shadow,shadow,shadow,1.0);
}
ENDCG

UnityShader[1]光照模型_第126张图片
其中运用到了多个Unity已经封装好的函数。要注意的是,如果需要使用这些函数,VertexOutput中的裁剪空间坐标必须命名为pos,否则会报错:

LIGHTING_COORDS(0,1) //用模型的第一和第二套纹理坐标填充光照纹理坐标
TRANSFER_VERTEX_TO_FRAGMENT(o) //根据光源类型处理光源坐标的具体值
LIGHT_ATTENUATION(i) //用于计算光照衰减系数(单向光无衰减)

OldShoolPlus

这个OldShoolPlus会将Lambert、Phone、Shadow、AO、3CutEnvCol进行综合,制成效果可观的光照模型。

Properties 
{
    _Occlusion ("环境光遮蔽", 2d) = "white" {}
    _LightCol ("光色", COLOR) = (1.0, 1.0, 1.0, 1.0)
    _BaseCol ("基底色", COLOR) = (1.0, 1.0, 1.0, 1.0)
    _HighLightPow ("高光扩散程度", Range(1,100)) = 30
    _EnvColUp ("顶部环境光色", COLOR) = (1.0, 1.0, 1.0, 1.0)
    _EnvColSide ("侧边环境光色", COLOR) = (1.0, 1.0, 1.0, 1.0)
    _EnvColDown ("底部环境光色", COLOR) = (1.0, 1.0, 1.0, 1.0)
    _EnvColPow ("环境光强度", Range(0,1)) = 0.1
}
...
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
#include "AutoLight.cginc"
#include "Lighting.cginc"
#pragma multi_compile_fwdbase_fullshadows
#pragma target 3.0

sampler2D _Occlusion;
float4 _LightCol;
float4 _BaseCol;
float _HighLightPow;
float4 _EnvColUp;
float4 _EnvColSide;
float4 _EnvColDown;
float _EnvColPow;
//输入结构
struct VertexInput 
{
	float4 vertex : POSITION;
	float3 normal : NORMAL;
	float2 uv0 : TEXCOORD0;
	};
//输出结构
struct VertexOutput 
{
	float4 pos : SV_POSITION;
	float4 posWS : TEXCOORD0;
	float3 nDirWS : TEXCOORD1;
	float2 uv : TEXCOORD2;
	LIGHTING_COORDS(3,4)
};
//顶点着色
VertexOutput vert (VertexInput v) 
{
	VertexOutput o = (VertexOutput)0;
	o.pos = UnityObjectToClipPos( v.vertex );
	o.posWS = mul(unity_ObjectToWorld,v.vertex);
	o.nDirWS = UnityObjectToWorldNormal(v.normal);
	TRANSFER_VERTEX_TO_FRAGMENT(o)
	o.uv = v.uv0;
	return o;
}
//片断着色
float4 frag(VertexOutput i) : COLOR 
{
	//准备向量
	float3 nDir = i.nDirWS;
	float3 lDir = _WorldSpaceLightPos0.xyz;
	float3 vDir = normalize(_WorldSpaceCameraPos.xyz - i.posWS);

	//准备点积结果
	float nDotl = max(0.0, dot(nDir, lDir));
	float vDotl = max(0.0,dot(vDir, reflect(-1 * lDir, nDir)));

	//光源效果 lambert + phone + shadow
	float3 lambert = _BaseCol.rgb * nDotl;
	float phone = pow(vDotl,_HighLightPow);
	float shadow = LIGHT_ATTENUATION(i);
	float3 lightResult = (lambert + phone) * shadow * _LightCol;
                
	//环境光效果 3CutEnvCol + AO
	float maskUp = max(0.0, nDir.g);
	float maskDown = max(0.0, (-1.0 * nDir.g));
	float maskSide = 1.0 - maskUp - maskDown;
	float3 envColUp = maskUp * _EnvColUp;
	float3 envColSide = maskSide * _EnvColSide;
	float3 envColDown = maskDown * _EnvColDown;
	float4 var_Occlusion = tex2D(_Occlusion, i.uv);
	float3 envLighting = (envColUp+envColSide+envColDown)* _BaseCol * _EnvColPow * var_Occlusion; 
			
	float3 finalRGB = dirLighting + envLighting;
	return float4(lightResult + envColResult, 1.0);
}
ENDCG

UnityShader[1]光照模型_第127张图片


法线贴图模板

当我们需要使用自定义的法线贴图时,会需要将代码进行大修改。

Properties 
{
    _NormalMap ("法线贴图", 2D) = "bump" {}
}
...
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
#pragma target 3.0
//获取参数
uniform sampler2D _NormalMap;
//输入结构
struct VertexInput 
{
    float4 vertex : POSITION;
    float3 normal : NORMAL;
    float4 tangent : TANGENT;
    float2 uv0 : TEXCOORD0;
};
//输出结构
struct VertexOutput 
{
    float4 pos : SV_POSITION;
    float4 posWS : TEXCOORD0;
    float3 tDirWS : TEXCOORD1;
    float3 bDirWS : TEXCOORD2;
    float3 nDirWS : TEXCOORD3;
    float2 uv : TEXCOORD4;
};
//顶点着色
VertexOutput vert (VertexInput v) 
{
    VertexOutput o = (VertexOutput)0;
    o.uv = v.uv0;
    o.pos = UnityObjectToClipPos( v.vertex );
    o.posWS = mul(unity_ObjectToWorld, v.vertex);
    o.nDirWS = UnityObjectToWorldNormal(v.normal);
    //获取tDir
    o.tDirWS = normalize(mul(unity_ObjectToWorld, float4(v.tangent.xyz, 0.0)).xyz);
    //获取bDir
    o.bDirWS = normalize(cross(o.nDirWS, o.tDirWS) * v.tangent.w);
    return o;
}
//片段着色
float4 frag(VertexOutput i) : COLOR 
{
    //获取与解码法线贴图
    float3 var_NormalMap = UnpackNormal(tex2D(_NormalMap, i.uv));
    //组合TBN矩阵
    float3x3 TBN = float3x3(i.tDirWS, i.bDirWS, i.nDirWS);
    float3 nDir = normalize(mul(var_NormalMap, TBN));
    float3 lDir = _WorldSpaceLightPos0.xyz;

    float nDotl = max(0.0, dot(nDir, lDir));

    float3 lambert = float3(nDotl, nDotl, nDotl);
    return float4(lambert, 1.0);
}
ENDCG

在输入结构中引入了一个新参数Tangent,代表切线空间信息,合成法线信息时需要获取某顶点切线坐标下的TBN三向量。在输出结构中除去nDirWS外,还需要获取tDirWS和bDirWS。顶点着色器中,推导这两个向量的方法很重要可以经常复制粘贴,tDirWS需要通过矩阵乘法将其从对象坐标系中取出,而bDirWS则需要使用nDirWS与tDirWS进行叉乘得到 (计算完整坐标系的一般方法)。

在片段着色器中使用UnpackNormal方法对切线空间法线信息(nDirTS)进行解码操作,然后使用三个向量计算TBN矩阵,对nDirTS与TBN进行矩阵乘法,得到自定义法线信息nDir,然后进行兰伯特光照模型化操作。
UnityShader[1]光照模型_第128张图片


Matcap

Matcap常被用于一些建模、雕刻、渲染软件的预览方法,它不同于PBR但又接近于PBR。Matcap胜在可以用来模拟环境光的镜面反射效果且不需要很大开销。Matcap所实现的环境光镜面反射是在BRDF所得到的光照模型的基础上对一张环境贴图进行采样,绕过了BRDF高额开销的环境光镜面反射算法,从而模拟光滑表面在环境中的表现形式,相比于PBR,它的缺点在于从头到尾只能使用一张环境贴图,一旦模型开始运动,就会穿帮。

Matcap材质制作有两个重点,一个是模型对贴图的采样方法,另一个是环境贴图的制作方法。首先拿ShaderForge进行实验:

首先在SubstancePainter中从模型上获取一张法线贴图,可以使用ShaderForge内置的VectorOperations/Transform节点快速对法线贴图进行空间转换,空间转换后的法线贴图会随着该空间下原点与模型的角度变换而同步旋转,从而达到一些奇特效果。
UnityShader[1]光照模型_第129张图片
需要将模型法线(可自定义)从切线空间转移到观察空间,以达到通过视角控制法线方向的效果,然后获取法线方向的RG两轴,代表该顶点法线方向的横向坐标(-1,+1)和纵向坐标(-1,+1)的组合(偏移屏幕中心的横纵方向的角度),使用这两轴信息Remap到(0,1)当作UV信息对EnvTex进行采样(贴图的坐标空间为0~1,负值无意义)。
UnityShader[1]光照模型_第130张图片
使用的EnvTex是一张球形贴图,它是1024x的一张采样贴图,并不是真正的三维贴图,它可以在Unity中给定6张天空盒贴图生成,也可以在其他软件中生成后直接截图。在PBR中,每个模型的环境光镜面反射由真正的天空盒与环境决定。至于为何要使用球形贴图:我们知道刚刚用于采样的uv来自法线向量的横纵两轴,这两轴的范围为-1~1,可以将其看作半径为1的圆形的数学公式:
x 2 + y 2 = 1 x^2+y^2 = 1 x2+y2=1
也就能够得到实际采样面积了(建议球形贴图的直径稍稍大于1.0,否则可能出现浮点数误差造成的无效采样)。使用这个流水线,可以从大多数软件中提取材质球,直接当作环境贴图使用,达到很多效果。由于Mapcap只会在一张球状贴图中进行采样,所以当视角旋转时,法线跟着旋转,采样不会旋转,导致不管从什么方向看模型,采样贴图都是一样的,比较容易穿帮,所以Mapcap适合比较粗糙表面的环境光镜面反射,和不精细的采样贴图。

UnityShader[1]光照模型_第131张图片

代码实现:

Properties 
{
    _NormalMap  ("法线贴图", 2D) = "bump" {}
    _EnvTex     ("环境贴图", 2D) = "gray" {}
    _FresnelPow ("菲涅尔次幂", Range(0, 3)) = 1
    _EnvSpecInt ("环境光强度", Range(0, 2)) = 1
}
...
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
#pragma target 3.0

uniform sampler2D _NormalMap;
uniform sampler2D _EnvTex;
uniform float _FresnelPow;
uniform float _EnvSpecInt;

struct VertexInput
{
    float4 vertex : POSITION;
    float3 normal : NORMAL;
    float4 tangent : TANGENT;
    float2 uv0 : TEXCOORD0;
};
struct VertexOutput
{
    float4 pos : SV_POSITION;
    float4 posWS : TEXCOORD0;
    float2 uv0 : TEXCOORD1;
    float3 tDirWS : TEXCOORD2;
    float3 bDirWS : TEXCOORD3;
    float3 nDirWS : TEXCOORD4;
};
VertexOutput vert (VertexInput v)
{
    VertexOutput o = (VertexOutput)0;
    o.pos = UnityObjectToClipPos( v.vertex );
    o.posWS = mul(unity_ObjectToWorld, v.vertex);
    o.uv0 = v.uv0;
    o.nDirWS = UnityObjectToWorldNormal(v.normal);
    o.tDirWS = normalize(mul(unity_ObjectToWorld, float4(v.tangent.xyz, 0.0)).xyz);
    o.bDirWS = normalize(cross(o.nDirWS, o.tDirWS) * v.tangent.w);
    return o;
}
float4 frag(VertexOutput i) : COLOR
{
    // 准备向量
    float3 nDirTS = UnpackNormal(tex2D(_NormalMap, i.uv0)).xyz;
    float3x3 TBN = float3x3(i.tDirWS, i.bDirWS, i.nDirWS); // TS->WS 转换矩阵
    float3 nDirWS = normalize(mul(nDirTS, TBN));
    float3 nDirVS = mul(UNITY_MATRIX_V, float4(nDirWS, 0.0)); // nDirWS -> nDirVS
    float3 vDirWS = normalize(_WorldSpaceCameraPos.xyz - i.posWS.xyz);
    // 中间量
    float2 matcapUV = nDirVS.rg * 0.5 + 0.5; // 采样EnvTex用UV
    float nDotv = max(0.0, dot(nDirWS, vDirWS));
    // 光照模型
    float fresnel = pow(1.0 - nDotv, _FresnelPow);
    float3 matcap = tex2D(_EnvTex, matcapUV); // 采样EnvTex
    float3 envSpecLighting = matcap * fresnel * _EnvSpecInt;
    // 返回值
    return float4(envSpecLighting, 1.0);
}
ENDCG

UNITY_MATRIX_V关键字为将坐标从世界空间转换到观察空间的转换矩阵,为float4类型。


立方体纹理

立方体纹理(CubeMap)属于贴图的一种,代表立方体贴图,主要用于天空盒与环境映射。CubeMap主流有三种,有六面体式(Six-Frame)、球面镜式(Mirrored-Ball)、经纬式(Latitude-Longitude)
UnityShader[1]光照模型_第132张图片
三种CubeMap均可在Unity中进行导入,只需稍微设置即可。ConvolutionType代表CubeMap的mip方式,有根据贴图反射强度区分位置和根据贴图色块区分位置两种mip方式,mipMap是贴图的底分辨率模式,会用于远距离渲染时候切换,方便节省内存。
UnityShader[1]光照模型_第133张图片
CubeMap也能用来制作环境光镜面反射效果,在ShaderForge中进行实验:ShaderForge中的CubeMap节点需要方向信息进行采样(相当于这个方向指向的CubeMap的位置),也可以给定MIP系数决定使用哪张MipMap,通过这个方式模拟操控粗糙度。我们希望物体表面有环境光镜面反射效果,所以将视线方向通过模型世界空间法线(可自定义)进行扰动,然后使用扰动过的视线方向进行采样。
UnityShader[1]光照模型_第134张图片
使用CubeMap制作而成的环境光镜面反射比较于MatCap拥有了更多的自由度,它可以从整个天空盒中进行采样,而不是只从一张球形贴图中进行采样,更适合比较光滑的平面和精细的天空盒。

关于CubeMap的问题:CubeMap有很多种类,但是在外部看来CubeMap无非两种形式:单张贴图或六张贴图。六张贴图一定是SixSided形式,而单张贴图可能是MirroredBall或LL式,单张贴图的CubeMap需要包含的信息除去天空盒颜色外,还可以包含天空盒的明亮度,也就是RGB值大于1的颜色(高动态范围贴图 High-Dynamic Range Image,HDRI),主要用于模拟发光效果,但是在一些设备上不支持HDR,所以有时需要将CubeMap进行变换成为LDRI:

在PhotoShop中,右上角 图像 / 模式 / 8位通道 进行映射,将图像最亮的地方改为RGB1,最暗的地方改为RGB0。下图左为HDR贴图,右为LDR贴图
UnityShader[1]光照模型_第135张图片
CubeMap的代码实现:

Properties 
{
    _NormalMap  ("法线贴图", 2D) = "bump" {}
    _CubeMap    ("立方体纹理", Cube) = "_Skybox" {} // 立方体纹理获取方法
    _MipPow     ("Mip级别", Range(0,8)) = 0
    _FresnelPow ("菲涅尔次幂", Range(0, 3)) = 1
    _EnvSpecInt ("环境光强度", Range(0, 2)) = 1
}
...
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
#pragma target 3.0
uniform sampler2D _NormalMap;
uniform samplerCUBE _CubeMap; // 立方体纹理获取方法
uniform float _MipPow;
uniform float _FresnelPow;
uniform float _EnvSpecInt;
struct VertexInput
{
    float4 vertex : POSITION;
    float3 normal : NORMAL;
    float4 tangent : TANGENT;
    float2 uv0 : TEXCOORD0;
};
struct VertexOutput
{
    float4 pos : SV_POSITION;
    float4 posWS : TEXCOORD0;
    float2 uv0 : TEXCOORD1;
    float3 tDirWS : TEXCOORD2;
    float3 bDirWS : TEXCOORD3;
    float3 nDirWS : TEXCOORD4;
};
VertexOutput vert (VertexInput v)
{
    VertexOutput o = (VertexOutput)0;
    o.pos = UnityObjectToClipPos( v.vertex );
    o.posWS = mul(unity_ObjectToWorld, v.vertex);
    o.uv0 = v.uv0;
    o.nDirWS = UnityObjectToWorldNormal(v.normal);
    o.tDirWS = normalize(mul(unity_ObjectToWorld, float4(v.tangent.xyz, 0.0)).xyz);
    o.bDirWS = normalize(cross(o.nDirWS, o.tDirWS) * v.tangent.w);
    return o;
}
float4 frag(VertexOutput i) : COLOR
{
    // 准备向量
    float3x3 TBN = float3x3(i.tDirWS, i.bDirWS, i.nDirWS);
    float3 nDirTS = normalize(UnpackNormal(tex2D(_NormalMap, i.uv0)));
    float3 nDirWS = mul(nDirTS, TBN);
    float3 vDirWS = normalize(_WorldSpaceCameraPos.xyz - i.posWS.xyz);
    float3 vrDirWS = reflect(-vDirWS, nDirWS); // 从模型表面根据法线反射出视线方向,用来模拟环境反射
    // 中间量
    float vDotn = max(0.0, dot(vDirWS, nDirWS)); // Fresnel用
    // 光照模型
    float3 cubeMap = texCUBElod(_CubeMap, float4(vrDirWS, _MipPow)); // 立方体纹理采样方法
    float fresnel = pow((1.0 - vDotn), _FresnelPow);
    float3 envSpecLighting = cubeMap * fresnel * _EnvSpecInt;
    // 返回值
    return float4(envSpecLighting, 1.0);
}
ENDCG

其中使用了一些立方体采样需要的方法,首先立方体纹理的外部参数格式为Cube;初始值为"_Skybox";代码内部承接的格式为samplerCUBE;片段着色中进行采样的方法为texCUBElod(cubeTex, float4(float3 dir, float mip)),这个方法需要了解立方体纹理采样的原理:
UnityShader[1]光照模型_第136张图片
将立方体纹理想象为一个盒子或一个球体,采样原点就在这个几何体的内部中心处,dir代表从这个原点向外发射的射线向量,这个射线最终达到盒子或球体的哪个像素,代表dir采样采到了这个像素的颜色。而mip代表当前cubeTex的mip等级,等级越高,tex分辨率越低,越模糊。

在之前的OldShoolPlus基础上添加CubeMap作为环境镜面反射,即可做出相对完整的材质OldShoolPro:

UnityShader[1]光照模型_第137张图片
代码实现:

Properties 
{
    _NormalMap ("法线贴图", 2D) = "bump" {}
    _Occlusion ("环境光遮蔽", 2d) = "white" {}
    _CubeMap ("天空盒", Cube) = "_Skybox" {}
    _MipInt ("Mip强度", Range(0,9)) = 0
    _BaseCol ("基底色", COLOR) = (1.0, 1.0, 1.0, 1.0)
    _LightCol ("光色", COLOR) = (1.0, 1.0, 1.0, 1.0)
    _SpecPow ("高光扩散程度", Range(1,100)) = 30
    _EnvColUp ("顶部环境光色", COLOR) = (1.0, 1.0, 1.0, 1.0)
    _EnvColSide ("侧边环境光色", COLOR) = (1.0, 1.0, 1.0, 1.0)
    _EnvColDown ("底部环境光色", COLOR) = (1.0, 1.0, 1.0, 1.0)
    _FresnelPow ("菲涅尔次幂", Range(0,2)) = 0
    _EnvSpecInt ("环境镜面反射强度", Range(0,1)) = 1
    _EnvColPow ("环境光强度", Range(0,1)) = 0.5
}
...
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
#include "AutoLight.cginc"
#include "Lighting.cginc"
#pragma multi_compile_fwdbase_fullshadows
#pragma target 3.0

uniform sampler2D _NormalMap;
uniform sampler2D _Occlusion;
uniform samplerCUBE _CubeMap;
uniform float _MipInt;
uniform float4 _BaseCol;
uniform float4 _LightCol;
uniform float _SpecPow;
uniform float4 _EnvColUp;
uniform float4 _EnvColSide;
uniform float4 _EnvColDown;
uniform float _EnvColPow;
uniform float _FresnelPow;
uniform float _EnvSpecInt;
//输入结构
struct VertexInput 
{
    float4 vertex : POSITION;
    float3 normal : NORMAL;
    float4 tangent : TANGENT;
    float2 uv0 : TEXCOORD0;
};
//输出结构
struct VertexOutput 
{
    float4 pos : SV_POSITION;
    float4 posWS : TEXCOORD0;
    float3 nDirWS : TEXCOORD1;
    float3 tDirWS : TEXCOORD2;
    float3 bDirWS : TEXCOORD3;
    float2 uv0 : TEXCOORD4;
    LIGHTING_COORDS(5,6)
};
//顶点着色
VertexOutput vert (VertexInput v) 
{
    VertexOutput o = (VertexOutput)0;
    o.pos = UnityObjectToClipPos( v.vertex );
    o.posWS = mul(unity_ObjectToWorld,v.vertex);
    o.nDirWS = UnityObjectToWorldNormal(v.normal);
    o.tDirWS = normalize(mul(unity_ObjectToWorld, float4(v.tangent.xyz, 0.0)).xyz);
    o.bDirWS = normalize(cross(o.nDirWS, o.tDirWS) * v.tangent.w);
    o.uv0 = v.uv0;
    TRANSFER_VERTEX_TO_FRAGMENT(o)
    return o;
}
//片断着色
float4 frag(VertexOutput i) : COLOR 
{
    //准备向量
    float3x3 TBN = float3x3(i.tDirWS, i.bDirWS, i.nDirWS);
    float3 nDirWS = normalize(mul(UnpackNormal(tex2D(_NormalMap, i.uv0)), TBN));
    float3 lDirWS = _WorldSpaceLightPos0.xyz;
    float3 vDirWS = normalize(_WorldSpaceCameraPos.xyz - i.posWS);
    float3 vrDirWS = reflect(-vDirWS, nDirWS);
    //准备点积结果
    float nDotl = max(0.0, dot(nDirWS, lDirWS));
    float nDotv = max(0.0, dot(nDirWS, vDirWS));
    float vrDotl = max(0.0,dot(vrDirWS, lDirWS));
    //直接光照效果 lambert + phone + shadow
    float3 lambert = _BaseCol.rgb * nDotl;
    float phone = pow(vrDotl,_SpecPow);
    float shadow = LIGHT_ATTENUATION(i);
    float3 dirLighting = (lambert + phone) * shadow * _LightCol;
    //环境光照效果 3CutEnvCol + CubeMap + AO
    float maskUp = max(0.0, nDirWS.g);
    float maskDown = max(0.0, (-1.0 * nDirWS.g));
    float maskSide = 1.0 - maskUp - maskDown;
    float3 envColUp = maskUp * _EnvColUp;
    float3 envColSide = maskSide * _EnvColSide;
    float3 envColDown = maskDown * _EnvColDown;
    float3 env3Col = (envColUp + envColSide + envColDown) * _BaseCol * _EnvColPow;

    float3 cubeMap = texCUBElod(_CubeMap, float4(vrDirWS, _MipInt)); // CubeMap采样
    float fresnel = pow(1.0 - nDotv, _FresnelPow); // 菲涅尔
    float4 occlusion = tex2D(_Occlusion, i.uv0); // AO
    float3 envLighting = (env3Col + cubeMap)* occlusion * fresnel * _EnvSpecInt;
                
    float3 finalRGB = dirLighting + envLighting;
    return float4(finalRGB, 1.0);
}
ENDCG

到目前位置OldShoolPro可以作为阶段性学习成果。

你可能感兴趣的:(unity,材质,着色器,技术美术,图形渲染)