Chapter14 非真实感渲染 NPR
一、卡通风格渲染 1.渲染轮廓线 2.添加高光 3.ToonShadingMat
二、素描风格渲染
一、卡通风格渲染
特点:物体被黑色线条描边,分明的明暗变化等 方法:其中之一就是 基于色调的着色技术
1.渲染轮廓线
方法
基于观察角度和表面法线 的轮廓线渲染:使用视角方向和表面法线的点乘结果来得到轮廓线信息,快但效果不好过程式几何 轮廓线渲染:使用两个Pass,第一个Pass渲染背面的面片,使用某些技术让其轮廓可见,第二个Pass再渲染正面面片。快速,但不适合立方体这样平整的模型基于图像处理 的轮廓线渲染:12章13章介绍的,适用于所有类型的模型,但一些深度和法线变化小的就不太行(类似于桌上的纸张)基于轮廓边检测 的轮廓线渲染:可以控制轮廓线的风格渲染,动画连贯性差
检测一条边是否为轮廓线,只需检测这条边相邻的两个三角形面片是否满足:
(
n
0
⋅
v
>
0
)
≠
(
n
1
⋅
v
>
0
)
(n_{0} \cdot v >0) \neq (n_{1} \cdot v >0)
( n 0 ⋅ v > 0 ) = ( n 1 ⋅ v > 0 )
n
0
n_{0}
n 0 和
n
1
n_{1}
n 1 表示两个三角形面片法向,
v
v
v 是从视角到该边上任意顶点的方向本质在于检查两个三角形是否一个朝正面一个朝背面 混合以上方法的轮廓线渲染 本节使用: 过程式几何 轮廓线渲染
第一个Pass中会使用轮廓线颜色渲染整个背面面片,并在是视角空间 下把模型顶点 沿着法线方向向外扩张一段距离 —— 让背部轮廓线可见 viewPos = viewPos + viewNormal * _Outline
但是对于内凹的模型,就可能会有背部轮廓遮挡正面的情况 在扩张之前,先对顶点法线z分量进行处理,使它们等于一个定值,再把法线归一化后再扩张 —— 背面更加扁平化,降低了遮挡正面的可能性
viewNormal. z = - 0.5 ;
viewNormal = normalize ( viewNormal) ;
viewPos = viewPos + viewNormal * _Outline;
2.添加高光
卡通风格的高光是一块纯色区域 Blinn-Phong 模型实现高光:float spec = pow(max(0,dot(normal, halfDir)),_Gloss);
卡通渲染:要把
n
o
r
m
a
l
⋅
h
a
l
f
D
i
r
normal \cdot halfDir
n or ma l ⋅ ha l f D i r 与阈值进行比较,小于的高光系数设为0,否则返回1。利用CG里的step函数实现与阈值比较的目的,参数1是参考值,参数2是待比较值,若参数2 > 参数1,就返回1。这种方法可能会有锯齿边缘
float spec = dot ( worldNormal, worldHalfDir) ;
spec = step ( threshold, spec) ;
在上方法的基础上进行抗锯齿平滑处理。
使用了CG的smoothstep函数 w是一个很小的值,当spec-threshold < -w 返回0,spec-threshold > w 时返回1,否则在0-1之间插值,实现平滑效果
float spec = dot ( worldNormal, worldHalfDir) ;
spec = lerp ( 0 , 1 , smoothstep ( - w, w, spec- threshold) ) ;
3.ToonShadingMat
Properties
{
_Color ( "Color" , Color) = ( 1, 1, 1, 1)
_MainTex ( "Main Tex" , 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
}
Pass
{
NAME "OUTLINE"
Cull Front
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
float _Outline;
fixed4 _OutlineColor;
struct a2v {
float4 vertex: POSITION;
float3 normal: NORMAL;
} ;
struct v2f {
float4 pos : SV_POSITION;
} ;
这个pass只渲染背面的三角形面片,设置Cull Front
把正面三角形剔除
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 ;
pos = pos + float4 ( normalize ( normal) , 0 ) * _Outline;
o. pos = mul ( UNITY_MATRIX_P, pos) ;
return o;
}
float4 frag ( v2f i) : SV_Target
{
return float4 ( _OutlineColor. rgb, 1 ) ;
}
在顶点着色器中,先把顶点和法线变换到视角空间下 ,这是为了让描边可以在观察空间达到最好的效果 把法线z分量设为定量 把法线归一化后再向外扩张,得到扩张后的顶点坐标 pos 再把顶点变换到裁剪空间 片元着色器只需要用轮廓线颜色 渲染整个背面就好
Pass{
Tags { "LightMode" = "ForwardBase" }
Cull Back
.. .. ..
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 )
} ;
#pragma multi_compile_fwdbase
:用途是告诉Unity编译器为不同的光照模型和阴影类型生成多个变体(variants)的Shader,使得Shader能够更好地适应前向渲染路径下的不同光照和阴影条件
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;
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 ) ;
}
首先计算了光照模型中所需的各个方向矢量,并进行归一化处理 计算了材质反射率 albedo(基础颜色)和环境光照 ambient UNITY_LIGHT_ATTENUATION(atten,i,i.worldPos);
:计算当前世界坐标下的阴影值又计算了 HalfLambert 漫反射系数 diff,并与阴影值 atten 相乘 用漫反射系数对渐变纹理 _Ramp 采样 又计算了高光反射,与14.1.2方法一致,使用 fwidth 函数 对高光区域的边界进行抗锯齿处理 计算高光时还使用了step(0.0001, _SpecularScale)
,因为为了在_SpecularScale 为0时,可以完全消除高光反射的光照
二、素描风格渲染
本节中不考虑多级渐变纹理的生成,直接使用6张像素纹理进行渲染 首先在顶点着色器阶段计算 逐顶点光照 ,根据光照结果来决定6张纹理的权重,并传递给片元着色器 在片元着色器中根据权重来混合6张图的采样结果
Properties
{
_Color ( "Color Tint" , Color) = ( 1, 1, 1, 1)
_TileFactor ( "Tile Factor" , Float) = 1
_Outline ( "Outline" , Range ( 0 , 1 ) ) = 0.1
_Hatch0 ( "Hatch 0" , 2D ) = "white" { }
_Hatch1 ( "Hatch 1" , 2D ) = "white" { }
_Hatch2 ( "Hatch 2" , 2D ) = "white" { }
_Hatch3 ( "Hatch 3" , 2D ) = "white" { }
_Hatch4 ( "Hatch 4" , 2D ) = "white" { }
_Hatch5 ( "Hatch 5" , 2D ) = "white" { }
}
_Color 用于控制模型的颜色 _TileFactor 是纹理的平铺系数,数越大,模型上的线条越密 _Hatch0-_Hatch5 是渲染时使用的六张纹理
SubShader
{
Tags { "RenderType" = "Opaque" "Queue" = "Geometry" }
UsePass "Custom/Chapter14-ToonShading/OUTLINE"
struct a2v {
float4 vertex : POSITION;
float3 normal : NORMAL;
float4 tangent : TANGENT;
float2 texcoord : TEXCOORD0;
} ;
struct v2f {
float4 pos : SV_POSITION;
float2 uv : TEXCOORD0;
fixed3 hatchWeights0 : TEXCOORD1;
fixed3 hatchWeights1 : TEXCOORD2;
float3 worldPos : TEXCOORD3;
SHADOW_COORDS ( 4 )
} ;
由于声明了六张纹理,所以需要6个混合权重,所以存储在两个fixed3 变量 hatchWeights0 和 hatchWeights1 中 为了添加阴影效果,还声明了 worldPos 变量,并使用 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 = 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 ;
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;
}
先对顶点进行基本坐标变换 使用_TileFactor 得到纹理采样坐标 在计算6张纹理的混合权重之前,要先进行 逐顶点光照 使用世界空间下的光照方向和法线方向得到漫反射系数 diff 把diff 缩放到 [0,7]之间,得到 hatchFactor 把[0,7] 区间均匀划分为7个子区间,通过判断 hatchFactor 所处的子区间来计算对应的纹理权重
fixed4 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-i.hatchWeights0.x - i.hatchWeights0.y - i.hatchWeights0.z-i.hatchWeights1.x - i.hatchWeights1.y - i.hatchWeights1.z)