·本专栏文章记录笔者阅读学习《Unity Shader入门精要》的感想与笔记,方便日后复习与查找
一、什么是非真实风格渲染
我们之前学习的渲染都是希望画面越像真实世界中越好,但是有的时候我们希望渲染的画面不是偏向于真实,而是偏向于某种卡通动漫、水墨画等特殊风格的渲染。这些就是非真实风格的渲染。


比如以上两个就是典型的非真实感渲染
二、卡通风格渲染
1.卡通风格渲染的特点
- 【描边】物体都有黑色的线条描边
- 【漫反射】物体都有分明的明暗变化
- 【高光反射】模型上分辨明显的纯色区域的高光
实现卡通风格渲染的方法之一是:基于色调的着色技术(tone-based shading),即用漫反射系数对一张一维纹理进行采样,以控制漫反射的色调。(主要用于漫反射光照的卡通化)
2.渲染轮廓线
轮廓线渲染一般有以下几种方法:
- 基于观察角度和表面法线的轮廓线渲染:通过视角方向和表面法线的点乘结果来得到轮廓线的信息(可以在一个Pass中完成)
- 过程式几何轮廓线渲染:一个Pass渲染模型背面并让他能被看到,另一个Pass渲染模型的正面
- 基于图像处理的的轮廓线渲染:通过屏幕后处理+深度法线纹理
【之前的笔记中我们有所提到的《Unity Shader入门精要》学习--高级篇--深度和法线纹理:“我来助你”
- 基于轮廓线边检测的轮廓线渲染:检查出一个边是否为轮廓边,然后直接渲染它
- 综合渲染处理:①找到轮廓边 ②模型和轮廓边渲染到纹理中 ③图像处理分辨出轮廓线 ④在图像空间下进行风格化渲染
3.高光
卡通风格的高光往往是模型上一块分别明显的纯色区域
这里我们在Bling-Phong模型中计算得到的高光反射率之后需要去根据一个阈值来决定它是1还是0:
float spec = dot(worldNormal, worldHalfDir);
spec = step(threshold, spec);
①step(比较值,被比较的值)是一个内置函数,如果被比较值 > 比较值 则 返回 1 否则 返回 0
②但是只是这样的话会形成锯齿
我们可以用如下的方法来消除这个锯齿
float spec = dot(worldNormal, worldHalfDir);
float w = fwidth(spec);
spec = lerp(0, 1, stepSmooth(-w, w, spec - threshold));
①这里用到的stempSmooth(-w, w, spec - threshold)会返回[0,1]之间的值
- spec - threshold < -w 则返回0
- spec - threshold > w 则返回1
- 其余情况返回的是[0, 1]之间的值
②w可以设为一个很小的值,这里我们用相邻像素的近似导数值(也就是 w = fwidth(spec))
4.实现一个卡通风格的渲染
这里我们用过程式几何轮廓线的方式来实现(就是要用两个Pass的那个)
4.1.属性添加
Properties
{
_Color ("Color Tint", Color) = (1, 1, 1, 1) //主颜色
_MainTex ("Texture", 2D) = "white" {} //主纹理
_Ramp ("Ramp Texture", 2D) = "white" {} //渐变纹理
_Outline("Outline", Range(0, 1)) = 0.1 //边缘线大小
_OutlineColor("Outline Color", Color) = (0, 0, 0, 1) //边缘线颜色
_Specular("Specular", Color) = (1, 1, 1, 1) //高光反射颜色
_SpecularScale("Specular Scale", Range(0, 0.1)) = 0.01 //高光反射阈值
}
①这里模型主体需要主纹理和主颜色
②对于轮廓线渲染需要线的粗细和颜色
③对于漫反射卡通风格的渲染需要用到渐变纹理
④对于高光反射的卡通风格需要用到高光颜色和高光反射的阈值
4.2.实现第一个Pass
Pass
{
//第一个Pass用于渲染外轮廓线
NAME "OUTLINE" //Pass Name which can be call in other Shader
Cull Front //this pass only shader back whith outline COLOR
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
float _Outline;
float4 _OutlineColor;
struct appdata
{
float4 vertex : POSITION;
float3 normal : NORMAL; //need normal to caculate lightColor in viewSpace
};
struct v2f
{
float4 pos : SV_POSITION;
};
v2f vert (appdata v)
{
//vert shader need to extend the back vertex to be the outline
v2f o;
float4 pos = mul(UNITY_MATRIX_MV, v.vertex);
float3 normal = mul((float3x3)UNITY_MATRIX_IT_MV, v.normal); //calulate normal in viewSpace with inverse-transpose matrix;
normal.z = -0.5;
pos = pos + float4(normalize(normal), 0) * _Outline; //Let the vertices expand
o.pos = mul(UNITY_MATRIX_P, pos);
return o;
}
float4 frag (v2f i) : SV_Target
{
return float4(_OutlineColor.rgb, 1.0); //frag shader need to return the outline COLOR
}
ENDCG
}
①第一个Pass的工作主要是渲染模型背面用某种方式偏移到外面形成可见的轮廓
②同时整个模型的背面都是渲染呈线的颜色
③渲染轮廓线是非真实感渲染的一种常用的Pass,所以我们给他取了个名字,这样可以以后随时来这里调用在
④让normal.z = -0.5(观察空间中)是为了保证法线统一朝向有一个远离摄像机方向的方向,防止有的背面凹陷的顶点刺破到正面去
④对于背部顶点的偏移都是在观察空间中实现的,让模型的顶点在观察空间下沿着法线法线方向偏移
4.3.实现第二个Pass
Pass{
Tags { "LightMode" = "ForwardBase"}
Cull Back
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#pragma multi_compile_fwdbase
#include "UnityCG.cginc"
#include "Lighting.cginc"
#include "AutoLight.cginc"
#include "UnityShaderVariables.cginc"
float4 _Color;
sampler2D _MainTex;
float4 _MainTex_ST;
sampler2D _Ramp;
float4 _Ramp_ST;
float4 _Specular;
float _SpecularScale;
struct a2v{
float4 vertex : POSITION;
float3 normal : NORMAL; //need normal to caculate lightColor in viewSpace
float2 texcoord : TEXCOORD0;
};
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 + worldNormal);
//采样获取颜色,计算环境光
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(worldLightDir, worldNormal);
diff = (diff * 0.5 + 0.5) * atten;
//让漫反射系数与主纹理颜色、以及渐变纹理颜色进行进一步相乘处理
fixed3 diffuse = _LightColor0.rgb * albedo * tex2D(_Ramp, float2(diff, diff)).rgb;
//计算高光反射率
fixed spec = dot(worldNormal, worldHalfDir);
fixed w = fwidth(spec) * 2.0;
fixed3 specular = _Specular.rgb * lerp(0, 1, smoothstep(-w, w, spec + _SpecularScale - 1) * step(0.0001, _SpecularScale));
return fixed4(ambient + diffuse + specular, 1.0);
}
ENDCG
}
①这个Pass负责渲染正面的模型:普通环境光 + 卡通风格漫反射 + 卡通风格高光反射
②顶点着色器只负责把世界空间法线、世界空间顶点、纹理坐标传送给片元着色器
③片元着色器中:
- 首先对主纹理进行采样,并与主颜色相乘得到反射率;
- 之后计算环境光部分(按老样子计算);
- 再计算漫反射率,然后用漫反射率在渐变纹理中采样的结果和光照颜色以及反射率进行漫反射颜色计算;
- 然后再计算高光反射率、计算像素的近似倒数,用这两个去插值得到的值乘以高光反射的颜色
- 最后环境光 + 漫反射颜色 + 高光反射颜色 作为返回值即可
④高光反射的计算中最后有一项是 step(0.0001, _SpecularScale)是为了保证_SpecularScale为0的时候,高光反射消失
4.4.查看最终效果


可以看出它们的特点就是有轮廓线、颜色明暗变化明显、高光反射是纯色
三、素描风格渲染
素描风格的的渲染是通过提前生成的素描纹理来实现实时渲染的素描风格的。这些纹理组成了一个色调艺术映射(Tonal Art Map, TAM)
下面我们会通过最简单的方式来对它进行实现
1.属性添加与CG定义
Properties
{
_Color ("Color Tint", Color) = (1, 1, 1, 1)
_TileFactor ("Tile Factor", Float) = 1
_Outline ("Outline", Range(0, 1)) = 0.1
_Hatch0 ("Hactch 0", 2D) = "white" {}
_Hatch1 ("Hactch 1", 2D) = "white" {}
_Hatch2 ("Hactch 2", 2D) = "white" {}
_Hatch3 ("Hactch 3", 2D) = "white" {}
_Hatch4 ("Hactch 4", 2D) = "white" {}
_Hatch5 ("Hactch 5", 2D) = "white" {}
}
float4 _Color;
float _TileFactor;
float _Outline;
sampler2D _Hatch0;
sampler2D _Hatch1;
sampler2D _Hatch2;
sampler2D _Hatch3;
sampler2D _Hatch4;
sampler2D _Hatch5;
①对于主颜色提供主颜色属性
②对于素描纹理,我们提供了六张、并且提供控制它们平铺大小的系数
③这个也需要描边所以我们定义了描边的大小
2.顶点着色器中计算光照与权重计算
struct v2f{
float4 pos : POSITION;
float2 uv : TEXCOORD0;
fixed3 hatchWeights0 : TEXCOORD1;
fixed3 hatchWeights1 : TEXCOORD2;
float3 worldPos : TEXCOORD3;
SHADOW_COORDS(4)
};
v2f vert (a2v v) {
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
//然后要对其的纹理坐标进行缩放
o.uv = v.texcoord.xy * _TileFactor;
//进行逐逐顶点的光照计算(计算漫反射系数)
fixed3 worldLightDir = normalize(WorldSpaceLightDir(v.vertex));
fixed3 worldNormal = normalize(UnityObjectToWorldNormal(v.normal));
fixed diff = max(0, dot(worldLightDir, worldNormal));
//混合权重
o.hatchWeights0 = fixed3(0, 0, 0);
o.hatchWeights1 = fixed3(0, 0, 0);
float hatchFactor = diff * 7.0; //把漫反射系数映射到[0, 7]的区间里
if (hatchFactor > 6.0){
//纯白色,什么都不做
}
else if(hatchFactor > 5.0){
o.hatchWeights0.x = hatchFactor - 5.0;
}
else if(hatchFactor > 4.0){
o.hatchWeights0.x = hatchFactor - 4.0;
o.hatchWeights0.y = 1.0 - o.hatchWeights0.x;
}
else if(hatchFactor > 3.0){
o.hatchWeights0.y = hatchFactor - 3.0;
o.hatchWeights0.z = 1.0 - o.hatchWeights0.y;
}
else if(hatchFactor > 2.0){
o.hatchWeights0.z = hatchFactor - 2.0;
o.hatchWeights1.x = 1.0 - o.hatchWeights0.z;
}
else if(hatchFactor > 1.0){
o.hatchWeights1.x = hatchFactor - 1.0;
o.hatchWeights1.y = 1.0 - o.hatchWeights1.x;
}
else{
o.hatchWeights1.y = hatchFactor;
o.hatchWeights1.z = 1.0 - o.hatchWeights1.y;
}
o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
TRANSFER_SHADOW(o);
return o;
}
①我们这里逐顶点计算漫反射系数,并把漫反射系数映射到[0,7]这个区间之间(我们有六个纹理+1个完全空白),然后根据获取的漫反射系数所处的是哪一个纹理的对应范围去计算对应的权重值。
②对于权重,我们有六张纹理,所以需要有六个权重值,我们通过两个TEXCOORD传入float3来存储它们
3.片元着色器中根据权重对纹理采样混合
float4 frag(v2f i) : SV_TARGET {
fixed4 hatchTex0 = tex2D(_Hatch0, i.uv) * i.hatchWeights0.x;
fixed4 hatchTex1 = tex2D(_Hatch1, i.uv) * i.hatchWeights0.y;
fixed4 hatchTex2 = tex2D(_Hatch2, i.uv) * i.hatchWeights0.z;
fixed4 hatchTex3 = tex2D(_Hatch3, i.uv) * i.hatchWeights1.x;
fixed4 hatchTex4 = tex2D(_Hatch4, i.uv) * i.hatchWeights1.y;
fixed4 hatchTex5 = tex2D(_Hatch5, i.uv) * i.hatchWeights1.z;
fixed4 whiteColor = fixed4(1, 1, 1, 1) * (1 - i.hatchWeights0.x - i.hatchWeights0.y - i.hatchWeights0.z - i.hatchWeights1.x - i.hatchWeights1.y - i.hatchWeights1.z);
fixed4 hatchColor = hatchTex0 + hatchTex1 + hatchTex2 + hatchTex3 + hatchTex4 + hatchTex5 + whiteColor;
UNITY_LIGHT_ATTENUATION(atten, i, i.worldPos);
return fixed4(hatchColor.rgb * _Color.rgb * atten, 1.0);
}
①片元着色器中我们只需要对六个纹理根据对应权重值进行采样,获取对应的颜色值
②空白颜色的权重值就是1-六个权重值,所以也可以求空白颜色值
③把采样获得的6个颜色值 和 一个空白颜色值 进行相加后,乘以阴影光照衰减值,作为最终的颜色值返回。
4.最终效果


可以看的出来很有素描的感觉
四、总结
①非真实感渲染常见的类型有卡通风格的渲染、和素描风格的渲染。
②卡通风格的渲染的特征是有明显轮廓线、明显的颜色变化和交替、纯色的高光区域。轮廓线我们在这里通过过程式几何轮廓渲染实现(两个Pass,一个负责渲染背面【观察空间中背面法线远离摄像机方向,顶点沿着法线方向平移轮廓线粗度个单位】,一个Pass负责渲染正面【渐变纹理处理漫反射率、高光反射阈值判断处理高光反射率(高光反射颜色不需要参杂光照颜色)】)
③素描风格的渲染的简单实现则是通过在顶点着色器中,计算六个素描纹理在当前顶点的漫反射率的权重值,从而在片元着色器中对应采样与混合,并最终返回颜色得到素描风格的效果