非真实感渲染(Non Photorealistic Rendering,NPR),用于卡通,水彩风格渲染。
卡通风格渲染(黑色描边,分明的明暗变化)
基于色调的着色技术(tone-based shading),使用蓝色来模拟冷色调,使用黄色来模拟黄色调,来实现从冷到暖的色调变化,为了更加真实的模拟,还可以模型本身的漫反射颜色进行混合来得到最终的冷暖色调。
实现中一般使用漫反射系数对一张一维纹理进行采样,以控制漫反射色调。
1.渲染轮廓线(5种方式)
- 基于观察角度和表面法线的轮廓线渲染。这种方法使用视角方向和表面法线的点乘结果来得到轮廓线的信息。快速简单,使用一个Pass就可得到结果。但是描边效果不好。
- 过程式几何轮廓线渲染。使用两个Pass渲染,第一个Pass渲染背面,并使用它的轮廓可见。第二个Pass渲染正面,这种方法快速,效果好,但是不适合类似立方体这样的平整模型。
- 基于图像处理的轮廓线渲染。屏幕后处理。适用于任何模型,但是深度和法线变化比较小的轮廓无法检测出来。比如桌子上的纸张。
- 基于轮廓边检测的轮廓线渲染。本质是判断相邻两个三角面片是否朝一个朝正面,一个朝背面。由于是逐帧提取轮廓线,帧与帧之间会出现跳跃性。
- 基于上述几种渲染方法,先找到轮廓边,把模型和轮廓渲染到纹理中。再使用图像处理方法识别出轮廓线,并在图像空间进行风格化渲染。
基于过程式几何轮廓线渲染:
第一个Pass中,使用轮廓线颜色渲染整个背面的面片,并在视角空间下把模型顶点沿着法线方向向外扩张一段距离,以此让背部轮廓线可见。代码如下:
viewPos = viewPos + viewNormal * _Outline
对于内凹模型,直接用顶点法线进行扩展,可能会背面面片遮挡正面面片,为了防止出现这种情况,扩张背面顶点,先把顶点法线等于一个定值,然后归一化法线,再对顶点进行扩张。是扩张后的背面更加扁平化,降低遮挡正面面片的可能性。
viewNormal.z = -0.5
viewNormal = normalize(viewNormal)
viewPos = viewPos + viewNormal * _Outline;
尽量让顶点沿xy平面扩张,不扩张z,然后背面面片z值大于前面面片,导致背面面片,遮住前面面片。
2.添加高光
为了让模型有分界明显的纯色区域,使用法线点乘(光照方向+视角方向)的一半,再和一个阈值进行比较,小于该阈值,高光反射系数为0,大於则为1。
float spec = dot(worldNormal,worldHalfDir);
spec = step(阈值,spec);// step函数 spec小于阈值返回0,大于等于返回1
这种方式会让高光区域有锯齿,因为高光区域边缘从0突变到1,没有渐变。因此高光边缘区域加个渐变,进行抗锯齿处理。
float spec = dot(worldNormal,worldHalfDir);
spec = lerp(0,1,smothstep(-w,w,spec - 阈值)) //spec - 阈值 < -w,返回0,大于w返回1,否则在0到1之间进行插值。
这样[-w,w]区间内,就有个渐变插值,消除锯齿。
Shader "Chan/Chapter14_ToonShading" {
Properties
{
_Color("Color Tint",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) = (1,1,1,1)
//高光反射颜色
_Specular("Specular",Color) = (1,1,1,1)
//高光反射计算使用到的阈值
_SpecularScale("Specular Scale",Range(0,0.1)) = 0.01
}
SubShader
{
Tags { "RenderType"="Opaque" "Queue"="Geometry"}
Pass
{
//定义此Pass名称,其他Shader可复用
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;
};
v2f vert(a2v v)
{
v2f o;
//顶点从模型空间变换到摄像机空间
float4 pos = mul(UNITY_MATRIX_MV,v.vertex);
//法线从模型空间变换到摄像机空间 为什么不能用UNITY_MATRIX_MV?
//https://blog.csdn.net/a133900029/article/details/80558765
float3 normal = mul((float3x3)UNITY_MATRIX_IT_MV,v.normal);
//防止背后面片顶点扩张,z值比前面面片顶点的z值大,出现遮挡情况
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);
}
ENDCG
}
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"
fixed4 _Color;
sampler2D _MainTex;
float4 _MainTex_ST;
sampler2D _Ramp;
fixed4 _Specular;
fixed _SpecularScale;
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)
};
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 + worldViewDir);
fixed4 c = tex2D(_MainTex,i.uv);
//材质反射率albedo
fixed3 albedo = c.rgb * _Color.rgb;
//环境光
fixed ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * albedo;
//对阴影纹理进行采样
UNITY_LIGHT_ATTENUATION(atten,i,i.worldPos);
//漫反射光照计算
fixed diff = dot(worldNormal,worldLightDir);
//半兰伯特漫反射光照模型计算漫反射系数diff,使diff在【0,1】区间内。
//渐变纹理纵轴颜色不变,直接用[diff,diff]对渐变纹理采样
diff = (diff * 0.5 + 0.5) * atten;
//半兰伯特漫反射 * 阴影 = 最终的漫反射
fixed3 diffuse = _LightColor0.rgb * albedo * tex2D(_Ramp,float2(diff,diff)).rgb;
//高光反射
fixed3 spec = dot(worldNormal,worldHalfDir);
//邻域像素之间的近似导数值
fixed w = fwidth(spec) * 2.0;
//高光颜色 * 高光反射系数 = 高光反射颜色
//smoothstep在[-w,w]之间进行插值
//step(0.0001,_SpecularScale)) _SpecularScale < 0.0001,返回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
}
}
Fallback "Diffuse"
}
注解:
第一个Pass,在摄像机空间下,将顶点从处理过的法线方向,外扩。
第二个Pass,计算 环境光+漫反射+高光反射 = 最终颜色。
素描风格的渲染
Shader "Chan/Chapter14_Hatching" {
Properties
{
_Color("Color Tint",Color) = (1,1,1,1)
//模型上的纹理平铺系数,值越大,模型素描线条越密
_TileFactor("Tile Factor",Float) = 1
//轮廓线线控参数
_Outline("Outline",Range(0,1)) = 0.1
//素描渲染使用的6张纹理
_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"{}
}
SubShader
{
Tags{"RenderType" = "Opaque" "Queue" = "Geometry"}
//使用Chan/Chapter14_ToonShading Shader中的Pass渲染模型轮廓
UsePass "Chan/Chapter14_ToonShading/OUTLINE"
Pass
{
//为了获得各个光照变量,需要设置Pass的标签和相关编译指令
Tags{"lightMode" = "ForwardBase"}
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#pragma multi_compile_fwdbase
#include "UnityCG.cginc"
#include "Lighting.cginc"
#include "AutoLight.cginc"
#include "UnityShaderVariables.cginc"
fixed4 _Color;
float _TileFactor;
sampler2D _Hatch0;
sampler2D _Hatch1;
sampler2D _Hatch2;
sampler2D _Hatch3;
sampler2D _Hatch4;
sampler2D _Hatch5;
struct a2v
{
float4 vertex:POSITION;
float4 tangent:TANGENT;
float3 normal:NORMAL;
float2 texcoord:TEXCOORD0;
};
struct v2f
{
float4 pos:SV_POSITION;
float2 uv:TEXCOORD0;
//6张纹理 需要6个混合权重,存储在以下两个变量中
fixed3 hatchWeights0:TEXCOORD1;
fixed3 hatchWeights1:TEXCOORD2;
//为了添加阴影效果,还声明了worldPos变量
float3 worldPos:TEXCOORD3;
//申明阴影纹理的采样座标 (其实就是TEXCOORD4)
SHADOW_COORDS(4)
};
v2f vert(a2v v)
{
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
//
o.uv = v.texcoord.xy * _TileFactor;
//世界空间下,dot(光照方向,法线方向) = 漫反射系数
fixed3 worldLightDir = normalize(WorldSpaceLightDir(v.vertex));
fixed3 worldNormal = UnityObjectToWorldNormal(v.normal);
fixed diff = max(0,dot(worldLightDir,worldNormal));
//6张纹理权重初始化为0
o.hatchWeights0 = fixed3(0,0,0);
o.hatchWeights1 = fixed3(0,0,0);
float hatchFactor = diff * 7.0;
//根据hatchFactor决定纹理混合权重
if(hatchFactor > 6.0)
{
//不处理,光照最亮部分 后边做纯白处理
//六个纹理的权重,存储在hatchWeights0 hatchWeights1中的xyz分量中
}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;
//计算阴影纹理的采样座标(TEXCOORD4)
TRANSFER_SHADOW(o);
return o;
}
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);
//经过权重采样后的6张纹理采样颜色 和 纯白颜色
fixed4 hatchColor = hatchTex0 + hatchTex1 + hatchTex2 + hatchTex3 + hatchTex4 + hatchTex5 + whiteColor;
//计算阴影和光照衰减
UNITY_LIGHT_ATTENUATION(atten,i,i.worldPos);
return fixed4(hatchColor.xyz * _Color.rgb * atten,1.0);
}
ENDCG
}
}
FallBack "Diffuse"
}
注解:
//uv范围放大,然后跟每个纹理对应的权重对比,在权重范围内,采样对应的纹理
o.uv = v.texcoord.xy * _TileFactor;