01 一般的渲染过程#
渲染流程#
flowchart LR 模型 --> 输入结构 输入结构 --> 顶点Shader 顶点Shader --> 输出结构 输出结构 --> 像素Shader
组件 | 功能 |
---|---|
模型 | 包含顶点信息 (如:v 1.0 1.0 -1.0 , ID 由顺序决定)、三角面信息 (如:f 5 3 1 , 数字为顶点 ID), 以及 UV、法线、顶点色等 |
输入结构 | 选择需要用到的模型信息 |
顶点Shader | 处理输入信息,将每个顶点的位置换算到屏幕空间,并计算/赋值其他逐顶点信息 (如 UV、顶点色、法线等) |
输出结构 | 输出指定的顶点信息 |
像素Shader | 结合环境、光照、摄像机等,输出最终渲染结果 |
向量#
关于向量的说明,这一份文章讲解得比较详细
兰伯特#
它是主光源向量和法线向量的点乘结果, 值域在[-1,1]区间, 视觉上表现出明暗关系, max(0, nDir·lDir)
, 只取正值. Lambert * 0.5 + 0.5
被称为半兰伯特, 暗部更柔和.
代码#
float lambert = max(0.0, nDotl);
float2 uv = float2(lambert, 0.0);
float4 var_MainTex = tex2D(_MainTex, uv);
return float4(var_MainTex);
02 案例讲解#
复数高光#
利用法线的偏移,制造出多个高光区域,丰富物体表面的高光表现.
屏幕UV#
通过将屏幕 UV 与深度值相乘,可以让纹理依附于物体表面,并始终面向摄像机.
算法组合#
纹理节点#
通过屏幕坐标系乘以 tiling 数量,取小数后限定在 [-0.5, 0.5] 区间,再接入 Length 节点.
Power#
对值进行幂运算,调节高光或其他效果的形状.
Length#
Length = √(x^2 + y^2 + z^2),x=y 时,x=-0.5 或 0.5 时结果为 √0.5,x=0 时结果为 0,形成周期性循环.
03 FlatCol#
FlatCol 表现的是一个单色的渲染结果.之前的案例都是用 ShaderForge 节点方式制作,这里开始用代码实现材质.代码实现的材质在性能和灵活性上有优势,因为最终输出的指令更精简.
代码示例#
Shader "Zhuangdong/AP1/L02/Lambert_U_01" {
Properties{}
SubShader{
Tags { "RenderType" = "Opaque" }
Pass {
Name "FORWARD"
Tags { "LightMode" = "ForwardBase" }
CGPROGRAM//着色器编译指令
#pragma vertex vert//声明顶点着色器
#pragma fragment frag//声明片段着色器
#include "UnityCG.cginc"//引入 Unity 内置 CG 库
#pragma multi_compile_fwdbase_fullshadows//启用阴影功能
#pragma target 3.0//指定 shader 模型版本
//上面这些类似上下文,但功能更多.
//声明顶点着色器和片段着色器,下面的 vert,frag 函数才能启用.
//还可以选择开启和关闭功能,比如启用阴影功能.以及指定 shader 模型版本
struct VertexInput {
//导入模型顶点信息和法线信息
//POSITION NORMAL 这类词可以看 UNITY 官方 Document 查看意思
float4 vertex : POSITION;
float3 normal : NORMAL;
};
struct VertexOutput {
//通过上面的模型顶点信息换算成顶点屏幕位置
//模型法线信息换算成世界空间法线信息
float4 pos : SV_POSITION;
float3 nDirWS : TEXCOORD0;
};
VertexOutput vert(VertexInput v) {
//顶点着色器固定写法 v2f vert (appdata v), Output vert (input 变量名)
VertexOutput o = (VertexOutput)0;
o.pos = UnityObjectToClipPos(v.vertex);//模型顶点位置转换成顶点屏幕位置
o.nDirWS = UnityObjectToWorldNormal(v.normal);//法线转成世界法线
return o;
}
float4 frag(VertexOutput i) : COLOR{
//片段着色器固定写法
float3 nDir = i.nDirWS;
float3 lDir = normalize(_WorldSpaceLightPos0.xyz);
//0 代表平行光,后面 xyz 代表方向,1 代表点光,xyz 代表点光坐标
//normalize 归一化保证渲染结果不出错
float nDotl = (dot(nDir, lDir)*0.5+0.5);//halflambert
float lambert = max(0.0, nDotl);
//有的手机机型会把 0 视为 0.1 这类小数,所以最好写 0.0,不然会出错.
//使用 max 而不是 clamp,是因为 clamp 限制两头,max 限制一头,更省性能,因为点乘不会大于 1
return float4(lambert, lambert, lambert, 1.0);
}
ENDCG
}
}
FallBack "Diffuse"
}
其他案例:#
- 用 float4 自定义光源方向,实现自定义兰伯特模型
- 兰伯特模型乘以 float4 可自定义光源颜色
- 兰伯特模型乘以 float 可自定义光强
04 案例讲解#
SSS 材质#
这个案例的核心思路是用贴图和参数一起控制明暗交界的颜色和范围.贴图 U 轴前半段是黑色,后半段从暗到亮渐变.通过调整参数,可以让明暗交界线附近的颜色和范围随意变化.这种效果常用于模拟生物皮肤的半透明感,叫做 SSS 效果
Step遮罩#
该案例使用 step 函数对光照模型进行分层.step 函数可以把灰度图变成黑白分明的遮罩.使用自定义颜色,R/G/B 通道各自和 Lambert 混合,生成不同遮罩,最后混合所有遮罩输出
特殊案例#
该案例混合了贴图和光照模型,再通过 Round 对混合结果进行分区,从而得到自定义的结果.
- 使用两组 Tiling 值不同的贴图进行混合,再调整灰度,得到最终贴图
- 用 Half Dir 和法线点乘,得到特殊的光照模型,能让光照随摄像机变化
- 灰度图和光照模型混合,结果再用 round 节点处理,得到只有黑白两色的分界
- 最后给黑白区域分别赋予不同颜色
LUT#
LUT(Look Up Texture) 原理简述
$$ D(\theta) = \frac{\int_{-\pi}^{\pi} \cos(\theta + x) \cdot R(2\sin(x/2))dx}{\int_{-\pi}^{\pi} R(2\sin(x/2))dx} $$
该公式是理论推导, 及理论生成LUT图的过程. Pre-Integrated Skin Shading 是理论生成LUT图的工具, 并且附带源码
05 漫反射与镜面反射#
漫反射#
Lambert,nDotl, 方向无关.现实中的案例有电影屏幕
镜面反射#
Phong/Blinn-Phong,rDotv/nDoth, 观察方向相关.现实中的案例有车漆
常用向量#
向量 | 功能 |
---|---|
nDir | 法线方向 |
lDir | 光源方向 |
vDir | 视方向 |
rDir | 光的反射方向,r = reflect(-l, n) |
hDir | 半角方向,ldir 和 vdir 中间的向量 |
nDoth | hDir 和 nDir 越接近,输出的值越接近 1 |
高光调节#
Power = Value^Exp
常用于 Phong 模型的公式,通过 Exp 控制高光的范围
案例#
漫反射和镜面反射结合
// 片段着色器示例
float3 nDir = normalize(i.nDirWS);
float3 lDir = _WorldSpaceLightPos0.xyz;
float3 vDir = normalize(_WorldSpaceCameraPos.xyz - i.posWS.xyz);
float3 rDir = reflect(-lDir, nDir);
float rDotv = dot(rDir, vDir);
float nDotl = dot(nDir, lDir);
float Lambert = nDotl * 0.5 + 0.5;
float Phong = pow(max(0.0, rDotv), _SpecPow);
float3 FinalColor = _BaseColor * Lambert + Phong;
return float4(FinalColor, 1.0);
TEXCOORD 寄存器占位符,相当于 GPU 管线中预留给开发者使用的寄存器空位,默认是 float4. 作用是标记顶点数据在着色器中的输入位置,用于传递 UV 和 自定义向量数据
06 案例讲解#
FakeEnvReflect#
该案例把 cloud2 贴图的灰度值映射到 U 坐标,实现将随机灰度分布应用到光照效果上.
葡萄#
案例使用兰伯特光照模型和 LUTRampTex 作为漫反射,高光部分则使用 Phong 模型.最后通过 cloud2 贴图调整颜色和高光变化.
漆面金属#
该案例用 cloud2 生成遮罩,lerp 混合不同材质表现.一部分有镜面反射,一部分没有.
BRDF#
可以理解为一个函数,输入光线、视角和表面参数,输出反射分布.Lambert 和 Phong 都是常见的 BRDF 模型.
BRDFExplorer#
可以自定义参数,制作自己的 BRDF,并查看常见 Shader 的源码.
07 闭塞与投影#
三色环境光#
该案例利用 normal 的三个通道,制作 Top/Side/Bottom 三层遮罩.用于模拟三种不同方向的环境光对光照模型的影响
float3 nDir = i.nDirWS;
float TopMask = max(0.0, nDir.g);
float BotMask = max(0.0, -nDir.g);
float MidMask = 1 - TopMask - BotMask;
float3 EnvCol = _TopCol * TopMask + _MidCol * MidMask + _BotCol * BotMask;
float AO = tex2D(_AoMap, i.uv);
float3 EnvLighting = EnvCol * AO;
return float4(EnvLighting, 1.0);
Shadow#
给光照模型添加投影
//给 Shader 增加投影
struct VertexOutput {
...
LIGHTING_COORDS(3,4)};//在 VertexOutput 加入 LIGHT_COORDS
VertexOutput vert(VertexInput v) {
VertexOutput o = (VertexOutput)0;
...
TRANSFER_VERTEX_TO_FRAGMENT(o)//顶点着色器加入 TRANSFER_VERTEX_TO_FRAGMENT
...
return o;}
float4 frag(VertexOutput i) : COLOR{
float shadow = LIGHT_ATTENUATION(i);//片段着色器加入 LIGHT_ATTENUATION
...}
光照构成#
flowchart LR 光照 --> 光源 光照 --> 环境 光源 --> 漫反射Lambert 光源 --> 镜面反射Phong 漫反射Lambert --> 遮挡Shadow 镜面反射Phong --> 遮挡Shadow 遮挡Shadow --> 结果 环境 --> 漫反射3Col 环境 --> 镜面反射Cubemap 漫反射3Col --> 遮挡AO 镜面反射Cubemap --> 遮挡AO 遮挡AO --> 结果
OldSchool 案例是上述光照构成的输出结果
08 TBN矩阵#
TBN: Tangent(Red), Bitangent(Blue), Normal(Green)
法线贴图记录的是在模型空间的法线朝向,需要转换到世界空间才能正常显示.使用 TBN 矩阵,将法线贴图转换到世界空间.转换步骤如下:
- 采样法线贴图并解码
- 构建 TBN 矩阵
- 切线空间法线转世界空间
- 输出世界空间法线
VertexOutput vert(VertexInput v) {
VertexOutput o = (VertexOutput)0;
o.pos = UnityObjectToClipPos(v.vertex);
o.uv0 = v.uv0;
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 );//切线方向 OStoWS
o.bDirWS = normalize(cross(o.nDirWS, o.tDirWS) * v.tangent.w);//根据 nDir tDir 求 bDir
TRANSFER_VERTEX_TO_FRAGMENT(o)
return o;
}
float4 frag(VertexOutput i) : COLOR{
//向量
float3 nDirTS = UnpackNormal(tex2D(_NormalMap, i.uv0)).rgb;
float3x3 TBN = float3x3(i.tDirWS, i.bDirWS, i.nDirWS);//TBN 矩阵
float3 nDirWS = normalize(mul(nDirTS, TBN));//转世界空间
09 环境反射#
菲涅尔#
边缘发光现象,公式为 nDotv, nDir 和 vDir 垂直时输出 1, 重合时输出 0, 视觉表现为模型边缘高亮
Matcap#
view 空间法线映射 BRDF 的渲染结果,只适合静态展示,Zbrush 预览界面使用的就是 Matcap.
贴图为圆形贴图,这是因为 MatcapMap 取 nDirVS 的 RG 通道为映射值,且法线向量满足以下公式
$$ x^2 + y^2 + z^2 = 1 $$
而在只取 xy 的情况下,映射出来的结果会是个圆形
Cubemap#
全景图,将摄像机周边的环境映射到一个 Cube 上.Mipmap 展示不同阶级的清晰度
float3 vrDirWS = reflect(-vDirWS, nDirWS);
float3 cubemap = texCUBElod(_CubeMap, float4(vrDirWS, _CubemapMip));
10 直播答疑#
魔改 PBR#
PBR 的由来之一就是对表面参数的概括,Shader 功能加的越多,面板参数也就越多,通过寻找参数之间的物理关系来减少面板参数就是 PBR 的内容之一.以 PBR 基于物理为核修改,叫魔改 PBR. 把一些 PBR 的 BRDF 或纹理名称移植到传统模型,那还是传统模型,不叫魔改 PBR
手游 PBS 基于物理为核的意义:
- 基于物理的光能守恒
- 基于物理的表面属性归纳
- 微表面理论
改变 UV#
法线贴图记录的是在切线空间的法线信息,切线空间的主副切线方向可直观理解为,贴图在表面处的 UV 轴方向,模型不变,切线空间变了,记录的法线信息也会变
11 常用参数#
常用参数和作用#
属性类型 | 语法格式 | 说明 |
---|---|---|
数值,范围 | _Name ("名称", float) = defaultVal | 浮点数参数 |
_Name ("名称", range(min, max)) = defaultVal | 范围参数 | |
_Name ("名称", int) = defaultVal | 整数参数 | |
位置,向量,颜色 | _Name ("名称", vector) = (xVal, yVal, zVal, wVal) | 向量参数 |
_Name ("名称", color) = (rVal, gVal, bVal, aVal) | 颜色参数 | |
2D, 3D 纹理,环境球 | _Name ("名称", 2d) = "defaultVal" {} | 2D纹理 |
_Name ("名称", 3d) = "defaultVal" {} | 3D纹理 | |
_Name ("名称", cube) = "defaultVal" {} | 立方体贴图 | |
[HideInInspect] | [HideInInspect] _FakeLightDir("伪光方向", vector) = (0.0, 1.0, 0.0, 1.0) | 在面板上隐藏该参数 |
[NoScaleOffset] | [NoScaleOffset] _MainTex ("主贴图", 2d) = "white" {} | 禁用纹理的 TilingOffset 面板 |
[Normal] | [Normal] _NormTex ("法线贴图", 2d) = "bump" {} | 标示该纹理参数为法线贴图,以激活相关自检功能 |
[HDR] | [HDR] _EmitCol ("自发光颜色", color) = (1.0, 1.0, 1.0, 1.0) | 用于设置高动态范围颜色值; 如:灯光颜色,自发光颜色等 |
[Gamma] | [Gamma] _EmitCol ("自发光颜色", color) = (1.0, 1.0, 1.0, 1.0) | 用于颜色参数的色彩空间的转换; 一般用于颜色空间为 Linear 的项目 |
[PowerSlider(value)] | [PowerSlider(0.5)] _SpecPow ("高光次幂", range(1,90)) = 30 | 对范围参数做 Power 处理后再传入 Shader; 纠正部分参数调节手感 |
[Header(Label)] | [Header(Texture)] | 标签,用于排版 |
[Space(value)] | [Space(50)] | 空行,用于排版 |
常用参数类型#
数据类型 | 精度和范围 | 说明 |
---|---|---|
fixed | 11位定点数, -2.0 ~ 2.0, 精度 1/256 | 低精度定点数 |
half | 16位浮点数, -60000 ~ 60000, 精度约 3 位小数 | 中等精度浮点数 |
float | 32位浮点数, -3.4E38 ~ 3.4E28, 精度约 6, 7 位小数 | 高精度浮点数 |
int | 32位整数 | 整数类型,较少使用 |
bool | 布尔型数 | 布尔类型,较少使用 |
矩阵 | float2x2, float3x3, float4x4, float2x3 诸如此类格式 | 浮点矩阵类型 |
half2x2, half3x3, half4x4, half2x3 诸如此类格式 | 半精度矩阵类型 | |
纹理对象 | sampler2D | 2D纹理采样器 |
sampler3D | 3D纹理采样器 | |
samplerCUBE | Cube纹理采样器 |
参数使用方法#
原则上优先使用精度最低的数据类型
经验#
- 世界空间位置和 UV 坐标,使用 float
- 向量,HDR 颜色,使用 half; 视情况升到 float
- LDR 颜色,简单乘子,可使用 fixed
要点#
- 不同平台对数据类型的支持情况不同; 一般会自动转换,极少数情况自动转换会带来问题
- 部分平台上,数据类型精度转换消耗也不小; 所以 fixed 也是慎用
- 多和图形开发商量
顶点 Input 数据#
顶点Input数据 | 数据类型 | 说明 |
---|---|---|
POSITION | float3, float4 | 顶点位置 |
TEXCOORD0 | float2, float3, float4 | UV通道1 |
TEXCOORD1 | float2, float3, float4 | UV通道2 |
TEXCOORD2 | float2, float3, float4 | UV通道3 |
TEXCOORD3 | float2, float3, float4 | UV通道4 |
NORMAL | float3 | 法线方向 |
TANGENT | float4 | 切线方向 |
COLOR | float4 | 顶点色 |
顶点 Output 数据#
顶点Output数据 | 数据类型 | 说明 |
---|---|---|
pos | float4 | 顶点位置(裁剪空间) |
uv0 | float2 | 一般纹理UV |
uv1 | float2 | LightmapUV |
posWS | float3 | 顶点位置(世界空间) |
nDirWS | half3 | 法线方向(世界空间) |
tDirWS | half3 | 切线方向(世界空间) |
bDirWS | half3 | 副切线方向(世界空间) |
color | fixed4 | 顶点色 |
常用顶点Shader操作#
注: Unity2019.3.2f1 版本
顶点操作 | 代码示例 | 说明 |
---|---|---|
pos | UnityObjectToClipPos(v.vertex); | 将顶点从对象空间转换到裁剪空间 |
uv0 | o.uv0 = v.uv1; o.uv0 = TRANSFORM_TEX(v.uv0, _MainTex_ST); | 设置一般纹理UV坐标 |
uv1 | o.uv1 = v.uv1; o.uv1 = v.uv1 * unity_LightmapST.xy + unity_LightmapST.zw; | 设置LightmapUV坐标 |
posWS | mul(unity_ObjectToWorld, v.vertex); | 将顶点从对象空间转换到世界空间 |
nDirWS | UnityObjectToWorldNormal(v.normal); | 将法线从对象空间转换到世界空间 |
tDirWS | normalize(mul(unity_ObjectToWorld, float4(v.tangent.xyz, 0.0)).xyz); | 将切线从对象空间转换到世界空间并归一化 |
bDirWS | normalize(cross(o.nDirWS, o.tDirWS) * v.tangent.w); | 计算副切线方向(法线和切线的叉积) |
color | o.color = v.color; | 传递顶点色 |
贴图tiling#
uniform sampler2D _MainTex;
uniform float4 _MainTex_ST
VertexOutput vert(VertexInput v) {
VertexOutput o = (VertexOutput)0;
// 使用TRANSFORM_TEX宏或手动计算
o.uv0 = TRANSFORM_TEX(v.uv0, _MainTex);
// 等价于: o.uv0 = v.uv0 * _MainTex_ST.xy + _MainTex_ST.zw;
return o;
}
功能模块化#
原写法:
float TopMask = max(0.0, nDirWS.g);
float BotMask = max(0.0, -nDirWS.g);
float MidMask = 1 - TopMask - BotMask;
float3 EnvCol = _TopCol * TopMask +
_MidCol * MidMask +
_BotCol * BotMask;
修改后:
float3 TriColAmbient (float3 n, float3 uCol, float3 sCol, float3 dCol){
float uMask = max(0.0, n.g);
float dMask = max(0.0, -n.g);
float sMask = 1.0 - uMask - dMask;
float3 envCol = uCol * uMask +
sCol * sMask +
dCol * dMask;
return envCol;
}
// 使用函数
float3 envCol = TriColAmbient(nDirWS, _EnvUpCol, _EnvSideCol, _EnvDownCol);
模块化 - 创建独立文件:
创建 Assets\Cgnic\MyCgnic.cgnic
:
#ifndef MY_CGINC
#define MY_CGINC
float3 TriColAmbient (float3 n, float3 uCol, float3 sCol, float3 dCol){
float uMask = max(0.0, n.g);
float dMask = max(0.0, -n.g);
float sMask = 1.0 - uMask - dMask;
float3 envCol = uCol * uMask +
sCol * sMask +
dCol * dMask;
return envCol;
}
#endif
在 Shader CGPROGRAM 下新增路径来引用:
CGPROGRAM
// ... 其他代码 ...
#include "../cginc/MyCginc.cginc"
// ... 其他代码 ...
float3 envCol = TriColAmbient(nDirWS, _EnvUpCol, _EnvSideCol, _EnvDownCol);
12 Ogre 案例#
本案例参考了 https://support.steampowered.com/kb/3081-QUXN-6209/dota-2-workshop-item-shader-masks?l=finnish , 结合实际资源,制作了一个典型的角色 Shader.
代码示例#
Shader "Zhuangdong/AP1/L06/L06_ogre_Feedback" {
Properties{
[Header(Texture)]
_MainTex ("RGB:基础颜色 A:透明贴图", 2D) = "White" {}
[Normal] _NormalMap ("RGB:法线贴图", 2D) = "bump" {}
_DetailMap ("RGB: Detail 细节贴图 A: 细节遮罩", 2D) = "black" {}
_MetalnessMask ("金属遮罩", 2D) = "black" {}
_SelfIllMask ("SelfIll 自发光遮罩", 2D) = "black" {}
_SpecTex ("RGB:Spec 高光贴图", 2D) = "gray" {}
_RimLight ("Rim 边缘光遮罩", 2D) = "black" {}
_BaseTintMask ("Tint 底色遮罩", 2D) = "White" {}
_SpecularExponent ("SpecExpo 镜面反射指数", 2D) = "White" {}
_DiffuseWarp ("Diffuse 扩散遮罩", 2D) = "black" {}
_CubeMap ("RGB:Cube 环境贴图", Cube) = "_Skybox" {}
_FresnelWarp("菲涅尔贴图 R:FCol G:FRim B:FSpec", 2D) = "black" {}
[Header(Diffuse)]
_LightCol ("主光颜色", Color) = (1.0,1.0,1.0,1.0)
[Space(10)]
_TopCol ("顶部颜色", Color) = (0.47,0.96,1,1)
_MidCol ("中部颜色", Color) = (0.46,0.7,0.45,1)
_BotCol ("底部颜色", Color) = (0.75,0.39,0.39,1)
_EnvDiffInt ("EnvDiff 环境漫反射强度", Range(0.0, 5.0)) = 0.2
[Header(Specular)]
_SpecPow("高光次幂", Range(0.0, 90.0)) = 5
_SpecInt("高光强度", Range(0.0, 10.0)) = 5
[Space(10)]
_EnvSpecint ("Envspec 环境镜反强度", Range(0.0, 10.0)) = 0.2
[Header(SelfIll)]
_SelfIllInt ("SelfIll 自发光强度", Range(0, 10)) = 1
[HDR]_RimCol ("轮廓光颜色", Color) = (1.0,1.0,1.0,1.0)
_RimInt ("轮廓光强度", Range(0.0, 3.0)) = 1.0
[HideInInspector]
_Cutoff ("Alpha cutoff", Range(0,1)) = 0.5
[HideInInspector]
_Color ("Main Color", Color) = (1,1,1,1)//Fallback Require
}
SubShader{
Tags {
"RenderType" = "Opaque"
}
Pass {
Name "FORWARD"
Tags {
"LightMode" = "ForwardBase"
}
Cull Off
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
//Texture
uniform sampler2D _MainTex;
uniform sampler2D _NormalMap;
uniform sampler2D _DetailMap;
uniform sampler2D _MetalnessMask;
uniform sampler2D _SelfIllMask;
uniform sampler2D _SpecTex;
uniform sampler2D _RimLight;
uniform sampler2D _BaseTintMask;
uniform sampler2D _SpecularExponent;
uniform sampler2D _DiffuseWarp;
uniform samplerCUBE _CubeMap;
uniform sampler2D _FresnelWarp;
//Diffuse
uniform half3 _LightCol;
uniform fixed3 _TopCol;
uniform fixed3 _MidCol;
uniform fixed3 _BotCol;
uniform fixed _EnvDiffInt;
//Specular
uniform half _SpecPow;
uniform half _SpecInt;
uniform half _EnvSpecint;
//SelfIll
uniform fixed _SelfIllInt;
uniform half3 _RimCol;
uniform half _RimInt;
uniform half _Cutoff;
struct VertexInput {
Document查看意思
float4 vertex : POSITION;
float2 uv0 : TEXCOORD0;
float3 normal : NORMAL;
float4 tangent : TANGENT;
};
struct VertexOutput {
法线信息
float4 pos : SV_POSITION;
float2 uv0 : TEXCOORD0;
float3 posWS : TEXCOORD1;
float3 nDirWS : TEXCOORD2;
float3 tDirWS : TEXCOORD3;
float3 bDirWS : TEXCOORD4;
LIGHTING_COORDS(5,6)
};
VertexOutput vert(VertexInput v) {
VertexOutput o = (VertexOutput)0;
o.pos = UnityObjectToClipPos(v.vertex);
o.uv0 = v.uv0;
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);
TRANSFER_VERTEX_TO_FRAGMENT(o)
return o;
}
float4 frag(VertexOutput i) : COLOR{
//向量
half3 nDirTS = UnpackNormal(tex2D(_NormalMap, i.uv0)).rgb;
half3x3 TBN = float3x3(i.tDirWS, i.bDirWS, i.nDirWS);
half3 nDirWS = normalize(mul(nDirTS, TBN));
half3 vDirWS = normalize(_WorldSpaceCameraPos.xyz - i.posWS.xyz);
half3 vrDirWS = reflect(-vDirWS, nDirWS);
half3 lDirWS = _WorldSpaceLightPos0.xyz;
表点光坐标,归一化比较安全,点乘结果会比较正确点
half3 lrDirWS = reflect(-lDirWS, nDirWS);
float shadow = LIGHT_ATTENUATION(i);
//点乘
half rDotv = dot(vDirWS, lrDirWS);//Phong
half nDotl = dot(nDirWS, lDirWS);//Lambert
half ndotv = dot(nDirWS, vDirWS);
//采样纹理
half4 var_MainTex = tex2D(_MainTex, i.uv0);
half4 var_DetailMap = tex2D(_DetailMap, i.uv0);
half var_MetalnessMask = tex2D(_MetalnessMask, i.uv0);
half var_SelfIllMask = tex2D(_SelfIllMask, i.uv0);
half var_SpecTex = tex2D(_SpecTex, i.uv0);//specInt
half var_RimLight = tex2D(_RimLight, i.uv0);
half var_BaseTintMask = tex2D(_BaseTintMask, i.uv0);
half var_SpecularExponent = tex2D(_SpecularExponent, i.uv0);//specSize
half3 var_Cubemap = texCUBElod(_CubeMap, float4(vrDirWS, lerp(8.0, 0.0,
var_MetalnessMask))).rgb;
half3 var_FresnelWarp = tex2D(_FresnelWarp, ndotv);
//提取信息
half3 BaseCol = var_MainTex.rgb;
half Opacity = var_MainTex.a;
half MetalMask = var_MetalnessMask;
half RimLightInt = var_RimLight;
half TintMask = var_BaseTintMask;
half SpecExp = var_SpecularExponent;
half3 EnvCube = var_Cubemap;
half SpecInt = var_SpecTex;
half EmitInt = var_SelfIllMask;
//光照模型
half3 DiffCol = lerp(BaseCol, half3(0.0,0.0,0.0), MetalMask);
//越接近金属,漫反射越弱
half3 SpecCol = lerp(BaseCol, half3(0.3,0.3,0.3), TintMask) * SpecInt;
//根据 TintMask 决定高光颜色
//0.3 是经验值,这个值乘以高光强度 specInt 得到的高光颜色贴图比较舒服
//菲涅尔
half3 Fresnel = lerp(var_FresnelWarp, 0.0, MetalMask);
//金属度越高,菲涅尔现象越不明显;
half FreCol = Fresnel.r; //无实际用途
half FreRim = Fresnel.g; //轮廓光用 Fresnel
half FreSpec = Fresnel.b; //镜面反射用 Fresnel
//主光漫反射
half HalfLambert = nDotl * 0.5 + 0.5;//Halflambert
half3 var_DiffuseWarp = tex2D(_DiffuseWarp, half2(HalfLambert, 0.2));
//对 Ramptexture 进行采样
half3 DirDiff = DiffCol * var_DiffuseWarp * _LightCol;
//主光镜面反射
half Phong = pow(max(0.0, rDotv), SpecExp * _SpecPow);
half Spec = Phong * max(0.0,nDotl);
Spec = max(Spec, FreSpec);
//会有个油亮的视觉效果,强烈的菲涅尔现象和 Phong 混合在一起
Spec = Spec * _SpecInt;
//乘以 SpecInt 后,大部分之前 Spec 的效果会消失,只有 SpecInt 指定的范围才有亮点
//原 Shader 是将所有镜面反射整合在一起最后再 max(flMetalnessMask, flSpecWarp)
//这里作者对镜面反射进行拆分,所以两边最后结算都需要进行一次 max
half3 DirSpec = SpecCol * Spec * _LightCol;
//环境漫反射
float TopMask = max(0.0, nDirWS.g);
float BotMask = max(0.0, -nDirWS.g);
float MidMask = 1 - TopMask - BotMask;
float3 EnvCol = _TopCol * TopMask +
_MidCol * MidMask +
_BotCol * BotMask;
float3 EnvDiff = DiffCol * EnvCol * _EnvDiffInt;
//视频作者只用了单色,这里我是直接复制之前的案例代码没有改
//环境镜面反射
half ReflectInt = max(FreSpec, MetalMask) * SpecInt;
//金属部分 MetalMask 最大,非金属 FreSpec 最大
//这样非金属有很强的菲涅尔现象,金属部分则很少,取而代之的有很强的反射现象
half3 EnvSpec = SpecCol * ReflectInt * EnvCube * _EnvSpecint;
//轮廓光
half3 RimLight = _RimCol * FreRim * RimLightInt * max(0.0, nDirWS.g) * _RimInt;
//轮廓光只出现在顶部,所以需要用 normal.g
//FreRim 定义菲涅尔现象,RimLightInt 定义强度范围,RimInt 定义强度
//自发光
float3 emission = EmitInt * DiffCol * _SelfIllInt;
//Final
half3 FinalRGB = (DirDiff + DirSpec) * shadow + EnvDiff + EnvSpec + RimLight +
emission;
clip(Opacity - _Cutoff);//小于_Cutoff 全部删去,大于的保留
return float4(FinalRGB, 1.0);
}
ENDCG
}
}
Fallback "Legacy Shaders/Transparent/Cutout/VertexLit"
//注:使用 FallBack 必须声明一个_Color: 颜色,即使你用不到
}
主要问题#
- Fallback 设置:指定到支持透明贴图的 Shader(如 Legacy Shaders/Transparent/Cutout/VertexLit).
- Cull Off:关闭背面剔除,防止模型背面被裁剪.
贴图讲解#
贴图类型 | 贴图名称 | 说明 |
---|---|---|
非共用贴图 | Color | 基础颜色贴图 |
MatelnessMask | 金属遮罩,控制金属区域 | |
Normal | 法线贴图 | |
RimMask | 边缘光遮罩 | |
SelfIllumMask | 自发光遮罩 | |
SpecularExponent | 高光指数贴图 | |
SpecularMask | 高光强度遮罩 | |
TintByBaseMask | 高光染色遮罩,金属高光颜色由ColorMap决定 | |
Translucency | 半透明贴图 | |
共用贴图 | Cubemap | 立方体贴图 |
DiffuseWarp RampTex | 漫反射扭曲渐变贴图 | |
FresnelWarpColor | 菲涅尔颜色扭曲贴图 | |
FresnelWarpRim | 菲涅尔边缘光扭曲贴图 | |
FresnelWarpSpec | 菲涅尔高光扭曲贴图 | |
贴图合并 | Color: RGB: Color A: Opacity | RGB通道存储颜色,A通道存储透明度 |
Mask1: R: SpecInt G: RimInt B: TintMask A: SpecPow | R通道高光强度,G通道边缘光强度,B通道染色遮罩,A通道高光指数 | |
Mask2: R:FresnelCol G: FresnelRim B: FresnelSpec | R通道菲涅尔颜色,G通道菲涅尔边缘光,B通道菲涅尔高光 |
其他贴图如 ColorMap, Tramsparency, NormalMap, SelfIlluminationMask, SpecularExponent, DiffuseMask, DetailMask 之前案例接触过,或该案例不使用,故不说明
源代码说明#
RimLightScale、SpecScale:为固定值,不同角色设定不同,主要用于边缘光和高光计算.
SpecularScale, RimLightScale 和其他源代码有的参数,作者在不影响最终效果的情况下移除了,并对代码进行了简化
菲涅尔与边缘光:不同角色 RimLightScale 设定不同,大部分是大于 1 的数字,在这基础下,通过 FresnelWarp 贴图和 RimMask 控制边缘高光的强度和范围,能实现夸张的卡通边缘效果.
FresnelWarpSpec 在原 Shader 被称为 flSpecWarp, 用于定义角色的环境镜面反射
高光与金属控制:金属部分用 cubemap 反射,非金属部分用高光贴图控制.通过 cSpecular *= max(flMetalnessMask, flSpecWarp);
实现金属和非金属的高光切换.
主光镜面反射:Dota2 采用了更适合俯视视角的算法,核心代码如下:
```c
vec3 R = reflect (V, N);
float RdotL = clamp(dot(L, -R), 0, 1);
```
由此我们可能知道,R 和 L 越接近,光越亮.相比传统 Phong,更适合 Dota2 这种上帝视角.
光照构成#
flowchart LR 光照 --> 光源 光源 --> 漫反射HalfLambert+WarpTex/Lambert 光源 --> 镜面反射Phong+FresnelSpecWarp 漫反射HalfLambert+WarpTex/Lambert --> 遮挡Shadow 镜面反射Phong+FresnelSpecWarp --> 遮挡Shadow 遮挡Shadow --> 结果 光照 --> 环境 环境 --> 漫反射1Col 环境 --> 镜面反射Cubemap 漫反射1Col --> 无AO,部分成分用RimMask代替 镜面反射Cubemap --> 无AO,部分成分用RimMask代替 无AO,部分成分用RimMask代替 --> 结果 轮廓光FresnelRimWarp --> 结果 自发光 --> 结果
经验总结#
视频案例将原代码拆分并融入自己的体系,便于理解和扩展.初学时容易忽略 Fresnel 贴图的作用,建议多参考 Dota2 原始 Shader 和社区资源,理解每张贴图的实际用途.