NPR 是 Non-Photorealistic Rendering 的简称,也就是图形渲染中的非真实感渲染,常见的 NPR 渲染包括卡通渲染、油画渲染、像素感渲染、素描画、水墨画等类型,
卡通渲染 是非真实感渲染中应用最广的渲染技术,在游戏和影视领域都是非常常见的。它主要是通过简化并剔除画面原本所包含的混杂部分,给人以独特的感染力和童趣,通常来说卡通渲染有4个要素 轮廓描边、色阶、高光、边缘光
轮廓描边:
渲染轮廓线的方式有很多种, 在这里带大家熟悉其中最简单的一种, 对物体做两次渲染, 第二次渲染时开启正面剔除,将顶点沿法线向外延深一段距离,(放大物体),实现轮廓线,这里就用到我们之前提到的多Pass渲染。
打开Shader,首先在Properties语块中声明轮廓线相关的两个属性,方便我们进行之后的调整。原本的pass我们暂时先不用动,直接新增一个Pass来做轮廓线的渲染,记得定义一下我们刚刚声明的宽度和颜色
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv = TRANSFORM_TEX(v.uv, _MainTex);
UNITY_TRANSFER_FOG(o,o.vertex);
return o;
}
fixed4 frag (v2f i) : SV_Target
{
// sample the texture
fixed4 col = tex2D(_MainTex, i.uv);
// apply fog
UNITY_APPLY_FOG(i.fogCoord, col);
return col;
}
ENDCG
}
Pass
{
// 开启前向剔除 表示剔除前面 只显示背面
Cull Front
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
// 线条宽度
float _OutlineWidth;
// 线条颜色
float4 _OutLineColor;
struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
// 法线
float3 normal : NORMAL;
};
struct v2f
{
float2 uv : TEXCOORD0;
float4 vertex : SV_POSITION;
};
v2f vert (appdata v)
{
v2f o;
// 顶点沿着法线方向外扩(放大模型)
float4 newVertex = float4(v.vertex.xyz + v.normal * _OutlineWidth * 0.01 ,1);
// UnityObjectToClipPos(v.vertex) 将模型空间下的顶点转换到齐次裁剪空间
o.vertex = UnityObjectToClipPos(newVertex);
return o;
}
half4 frag(v2f i) : SV_TARGET
{
// 返回线条色彩
return _OutLineColor;
}
ENDCG
}
}
}
色阶:
通常来说都是由它来决定画面色彩的丰富度饱满度精细度,而大部分卡通渲染习惯降低色阶,用简单的明暗关系来描述世界,使画面扁平又不失层次感,这里还是用上节讲的half Lambert光照模型,不过这看起来一点都不卡通,我们需要让它明暗分明一点
// 得到顶点法线
float3 normal = normalize(i.worldNormal);
// 得到光照方向
float3 worldLightDir = UnityWorldSpaceLightDir(i.worldPos);
// NoL代表表面接受的能量大小
float NoL = dot(i.worldNormal, worldLightDir);
// 计算half-lambert亮度值
float halfLambert = NoL * 0.5 + 0.5;
// 通过亮度值计算线性ramp
float ramp = linearstep(_RampStart, _RampStart + _RampSize, halfLambert);
float step = ramp * _RampStep; // 使每个色阶大小为1, 方便计算
float gridStep = floor(step); // 得到当前所处的色阶
float smoothStep = smoothstep(gridStep, gridStep + _RampSmooth, step) + gridStep;
ramp = smoothStep / _RampStep; // 回到原来的空间
// 得到最终的ramp色彩
float3 rampColor = lerp(_DarkColor, _LightColor, ramp);
rampColor *= col;
高光:
用相机的位置减去世界位置得到视向量,也就是当前物体表面指向摄像机的方向,由于反射不太好算,所以这里通过 视向量 和 光照方向 得到角平分线,也就是半程向量。通过 法线方向 点乘 半程向量 就可以得到 法线 和 半程向量 的 夹角,由此就 可以推断出 视向量 和 反射向量 的 接近程度,用 noh 来 计算高光 的 亮度值,而这个参数 SpecPow 则是 控制高光的 光泽度,也就是 高光 亮斑的 范围,和色阶同样,用smoothStep来做个柔边的效果再把高光颜色和强度值加上,最后我们把漫反射和高光混合,就可以来调试效果了。
// 得到视向量
float3 viewDir = normalize(_WorldSpaceCameraPos.xyz - i.worldPos.xyz);
// 计算half向量, 使用Blinn-phone计算高光
float3 halfDir = normalize(viewDir + worldLightDir);
// 计算NoH用于计算高光
float NoH = dot(normal, halfDir);
// 计算高光亮度值
float blinnPhone = pow(max(0, NoH), _SpecPow * 128.0);
// 计算高光色彩
float3 specularColor = smoothstep(0.7 - _SpecSmooth / 2, 0.7 + _SpecSmooth / 2, blinnPhone)
* _SpecularColor * _SpecIntensity;
边缘光:
首先我们需要得知哪里是我们看到的边缘,当我们的视向量和法线向量的夹角越接近直角时它就越靠近边缘,先拿到视向量和法向量的夹角,就可以看到,越是接近边缘的地方越暗,但边缘光一般都是越接近边缘越亮,所以给 1- 反转一下,但正常来说阴影部分是不应该有边缘光的,所以要把漫反射加一下,那到至此边缘光就正确了
// 计算NoV用于计算边缘光
float NoV = dot(i.worldNormal, viewDir);
// 计算边缘光亮度值
float rim = (1 - max(0, NoV)) * NoL;
// 计算边缘光颜色
float3 rimColor = smoothstep(_RimThreshold - _RimSmooth / 2, _RimThreshold + _RimSmooth / 2, rim) * _RimColor;
完整代码:
Shader "Custom/ToonShader"
{
Properties
{
_MainTex ("Texture", 2D) = "white" {}
_OutlineWidth ("Outline Width", Range(0.01, 1)) = 0.01
_OutLineColor ("OutLine Color", Color) = (0.5,0.5,0.5,1)
_RampStart ("交界起始 RampStart", Range(0.1, 1)) = 0.3
_RampSize ("交界大小 RampSize", Range(0, 1)) = 0.1
[IntRange] _RampStep("交界段数 RampStep", Range(1,10)) = 1
_RampSmooth ("交界柔和度 RampSmooth", Range(0.01, 1)) = 0.1
_DarkColor ("暗面 DarkColor", Color) = (0.4, 0.4, 0.4, 1)
_LightColor ("亮面 LightColor", Color) = (0.8, 0.8, 0.8, 1)
_SpecPow("SpecPow 光泽度", Range(0, 1)) = 0.1
_SpecularColor ("SpecularColor 高光", Color) = (1.0, 1.0, 1.0, 1)
_SpecIntensity("SpecIntensity 高光强度", Range(0, 1)) = 0
_SpecSmooth("SpecSmooth 高光柔和度", Range(0, 0.5)) = 0.1
_RimColor ("RimColor 边缘光", Color) = (1.0, 1.0, 1.0, 1)
_RimThreshold("RimThreshold 边缘光阈值", Range(0, 1)) = 0.45
_RimSmooth("RimSmooth 边缘光柔和度", Range(0, 0.5)) = 0.1
}
SubShader
{
Tags { "RenderType"="Opaque" }
LOD 100
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
float3 normal: NORMAL; // 计算光照需要用到模型法线
};
struct v2f
{
float2 uv : TEXCOORD0;
float4 vertex : SV_POSITION;
// 计算光照需要用到法线和世界位置
float3 worldNormal: TEXCOORD1;
float3 worldPos:TEXCOORD2;
};
sampler2D _MainTex;
float4 _MainTex_ST;
float _RampStart;
float _RampSize;
float _RampStep;
float _RampSmooth;
float3 _DarkColor;
float3 _LightColor;
float _SpecPow;
float3 _SpecularColor;
float _SpecIntensity;
float _SpecSmooth;
float3 _RimColor;
float _RimThreshold;
float _RimSmooth;
float linearstep (float min, float max, float t)
{
return saturate((t - min) / (max - min));
}
v2f vert (appdata v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv = TRANSFORM_TEX(v.uv, _MainTex);
// 向下传输这些数据
o.worldNormal = normalize(UnityObjectToWorldNormal(v.normal));
o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
return o;
}
fixed4 frag (v2f i) : SV_Target
{
// sample the texture
fixed4 col = tex2D(_MainTex, i.uv);
//------------------------ 漫反射 ------------------------
// 得到顶点法线
float3 normal = normalize(i.worldNormal);
// 得到光照方向
float3 worldLightDir = UnityWorldSpaceLightDir(i.worldPos);
// NoL代表表面接受的能量大小
float NoL = dot(i.worldNormal, worldLightDir);
// 计算half-lambert亮度值
float halfLambert = NoL * 0.5 + 0.5;
//------------------------ 高光 ------------------------
// 得到视向量
float3 viewDir = normalize(_WorldSpaceCameraPos.xyz - i.worldPos.xyz);
// 计算half向量, 使用Blinn-phone计算高光
float3 halfDir = normalize(viewDir + worldLightDir);
// 计算NoH用于计算高光
float NoH = dot(normal, halfDir);
// 计算高光亮度值
float blinnPhone = pow(max(0, NoH), _SpecPow * 128.0);
// 计算高光色彩
float3 specularColor = smoothstep(0.7 - _SpecSmooth / 2, 0.7 + _SpecSmooth / 2, blinnPhone)
* _SpecularColor * _SpecIntensity;
//------------------------ 边缘光 ------------------------
// 计算NoV用于计算边缘光
float NoV = dot(i.worldNormal, viewDir);
// 计算边缘光亮度值
float rim = (1 - max(0, NoV)) * NoL;
// 计算边缘光颜色
float3 rimColor = smoothstep(_RimThreshold - _RimSmooth / 2, _RimThreshold + _RimSmooth / 2, rim) * _RimColor;
//------------------------ 色阶 ------------------------
// 通过亮度值计算线性ramp
float ramp = linearstep(_RampStart, _RampStart + _RampSize, halfLambert);
float step = ramp * _RampStep; // 使每个色阶大小为1, 方便计算
float gridStep = floor(step); // 得到当前所处的色阶
float smoothStep = smoothstep(gridStep, gridStep + _RampSmooth, step) + gridStep;
ramp = smoothStep / _RampStep; // 回到原来的空间
// 得到最终的ramp色彩
float3 rampColor = lerp(_DarkColor, _LightColor, ramp);
rampColor *= col;
// 混合颜色
float3 finalColor = saturate(rampColor + specularColor + rimColor);
return float4(finalColor,1);
}
ENDCG
}
Pass
{
Cull Front
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
// 法线
float3 normal : NORMAL;
};
struct v2f
{
float2 uv : TEXCOORD0;
float4 vertex : SV_POSITION;
};
sampler2D _MainTex;
float4 _MainTex_ST;
// 线条宽度
float _OutlineWidth;
// 线条颜色
float4 _OutLineColor;
v2f vert (appdata v)
{
v2f o;
float4 newVertex = float4(v.vertex.xyz + normalize(v.normal) * _OutlineWidth * 0.05,1);
o.vertex = UnityObjectToClipPos(newVertex);
return o;
}
fixed4 frag (v2f i) : SV_Target
{
return _OutLineColor;
}
ENDCG
}
}
fallback"Diffuse"
}