
概要:
卡渲是在一般的光照模型上,如Lambert、Phong…进行改造,记录这些光照模型的结果,作为uv的一个坐标采样RampTex;或使用 色调映射技术(Tonal Art Map,TAM),作为不同风格、不同灰度的纹理的权值…以此来体现有风格的明暗面
- 一般卡渲:
- 素描效果:
第十四章 · 卡渲:
是在一般的光照模型上,如Lambert、Phong…进行改造,记录这些光照模型的结果,作为uv的一个坐标采样RampTex;或使用 色调映射技术(Tonal Art Map,TAM),作为不同风格、不同灰度的纹理的权值…以此来体现有风格的明暗面
14.1卡通风格的渲染
实现卡通渲染的方式之一:基于色调着色技术(tone-based shading)
- 使用漫反射系数对一张一维纹理RampTex进行采样,以控制漫反射的色调
- 对模型进行描边
14.1.1 渲染轮廓线
《Real Time Rendering, third edition》将描边方法分为5种类型:
- 基于观察角度和表面法线渲染(类似于Fresnel):使用视角方向和表面法线的点乘来得到轮廓信息特点:快速(一个Pass即可),但效果不好
- **过程式几何轮廓渲染:**第一个Pass渲染背面,并将顶点放大,让其轮廓显示;第二个Pass渲染正面的面片。优点:快速有效、适用于绝大多数表面平滑的模型缺点:不适合类似于立方体这样平整的模型
- 基于图像处理的轮廓线渲染(第12、13章的边缘检测就属于这一类别)优点:适用于任何种类的模型缺点:一些深度和法线变化很小的轮廓无法被检测,如桌子上的纸张
- 基于轮廓边检测的轮廓线渲染:检测一条边为轮廓边的公式:检测 这条边相邻的两个三角形面片是否一个朝正面,一个朝背面即(n_0 \cdot v >0) ≠ (n_1 \cdot v >0) ,其中n_0 和n_1 是2相邻三角形面片的法向, v是视角到改边任意顶点的方向检测过程由 **几何着色器(Geometry Shader)**完成优点:可以渲染出独特的轮廓线,找到精确的轮廓边缺点:逐帧单独提取轮廓,在帧与帧之间会有跳跃性
- 混合上边的方法: 本节使用法2(过程式几何轮廓渲染):使用两个Pass
-
Pass1:使用轮廓线颜色渲染整个背面面片,并在视角空间下把模型顶点朝法向方向向外扩张一段距离,以此让背部轮廓线可见。代码如下:
viewPos = viewPos + viewNormal * _Outline; -
**Pass1改进:**直接使用法线对顶点扩展,对于内凹的模型,可能出现背面面片遮住正面面片的情况。为了尽可能防止出现这种情况,进行以下改进: 这样的好处:扩展的背面更加扁平化,从而降低了挡住正面面片的可能性
viewNormal.z = -0.5; viewNormal = normalize(viewNormal); viewPos = viewPos + viewNormal * _Outline;
14.1.2 添加高光
卡渲的高光往往是一块分界明显的色块
将之前的 Blinn-Phong模型进行改造,将值和一个阈值进行比较,如果小于阈值,则高光反射系数为0,反之为1(和阈值的做比较可以用 step函数)
1.step函数: 参数2 > 参数1 时,输出1,反之输出0
float spec = dot(worldNormal , worldHalfDir); //法向点乘半角方向
spec = step(threshold , spec); //和阈值做比较
此方法的缺点,从0到1会有很大锯齿
2.方法改进:lerp+smoothstep
**smoothstep(a , b , x)函数:**当 a<x<b时,输出值在(0,1)之间平滑曲线过渡;其他情况输出0或者1
本例代码:
w取很小的值,这里用相邻像素之间的近似导数值算出w,用 fwidth函数 实现
**fwidth函数 :**计算屏幕空间相邻两像素之间某一值的插值
**当年清风:常用Shader内置函数及使用总结(持续更新)**51 赞同 · 4 评论文章
关于fwidth函数的示例
当spec-threshold 很小时,即处于轮廓边缘,则进行平滑过渡
float spec = dot(worldNormal , worldHalfDir); //法向点乘半角方向
spec = lerp(0 , 1 , smoothstep(-w, w, spec - threshold)); //其实把lerp去了也可以
14.1.3 实现
Pass1:渲染背面,将模型顶点沿着法线放大,渲染出描边
Pass2:正常的卡渲
- **Diff:**用RampTex
- Spec:用色块,smoothstep函数作用 bulling-Phong模型,smoothstep函数的w值,用fwidth(bulling) 计算出
- 本次是对之前光照模型的回顾,尤其是shadow的实现不要忘了:头文件、Tags…
Shader "Unlit/C14_ToonShading"
{
Properties
{
_Color ("Color Tint" ,Color) = (1.0, 1.0, 1.0, 1.0)
_MainTex ("MainTex", 2D) = "white" {}
_RampTex ("RampTex", 2D) = "white" {}
_OutLine ("OutLine", Range(0.0, 1.0)) = 0.1
_OutLineCol ("OutLineCol", Color) = (0.0, 0.0, 0.0, 1.0)
_SpecularCol ("SpecularCol", Color) = (1.0, 1.0, 1.0, 1.0)
_SpecularScale ("SpecularScale", Range(0, 0.1)) = 0.01 //高光反射时的阈值
}
SubShader
{
Tags { "RenderType"="Opaque" }
//【Pass1 渲染背面,得到轮廓】
Pass
{
NAME "OUTLINE"
Cull Front //关闭正面渲染
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
half _OutLine;
half4 _OutLineCol;
struct appdata
{
float4 vertex : POSITION;
float3 normal : NORMAL;
};
struct v2f
{
float4 pos : SV_POSITION;
};
v2f vert (appdata v)
{
v2f o;
float4 posVS = float4(UnityObjectToViewPos(v.vertex) , 1.0); // 将顶点变换到视角空间
float3 nDirVS = mul((float3x3)UNITY_MATRIX_IT_MV , v.normal); // 将法线变换到视角空间
nDirVS.z = -0.5; // 法线z值 设为常数
posVS = posVS + float4(normalize(nDirVS) , 0.0) * _OutLine; // 改变顶点位置
o.pos = UnityViewToClipPos(posVS); // 将顶点变换到裁剪空间
return o;
}
half4 frag (v2f i) : SV_Target
{
return _OutLineCol;
}
ENDCG
}
//【Pass2 卡渲正面】
Pass
{
Tags {"LightMode" = "ForwardBase"} // 使用Unity的光照信息
Cull Back
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
#include "Lighting.cginc" //阴影所需
#include "AutoLight.cginc" //阴影所需
#pragma multi_compile_fwdbase_fullshadows //让Shader的光照变量可以被正确赋值,并能产生阴影
half4 _Color;
sampler2D _MainTex;
sampler2D _RampTex;
half4 _SpecularCol;
half _SpecularScale;
struct appdata
{
float4 vertex : POSITION;
float2 uv0 : TEXCOORD0;
float3 normal : NORMAL;
};
struct v2f
{
float4 pos : SV_POSITION;
float2 uv0 : TEXCOORD0;
float3 nDirWS : TEXCOORD1;
float3 posWS : TEXCOORD2;
LIGHTING_COORDS(3, 4) //阴影
};
v2f vert (appdata v)
{
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
o.uv0 = v.uv0;
o.nDirWS = UnityObjectToWorldNormal(v.normal);
o.posWS = mul(unity_ObjectToWorld, v.vertex).xyz;
TRANSFER_VERTEX_TO_FRAGMENT(o) //阴影
return o;
}
half4 frag (v2f i) : SV_Target
{
//【贴图采样】
half4 var_MainTex = tex2D(_MainTex, i.uv0);
//【向量准备】
half3 nDirWS = normalize(i.nDirWS);
half3 lDirWS = normalize(UnityWorldSpaceLightDir(i.posWS));
half3 vDirWS = normalize(UnityWorldSpaceViewDir(i.posWS));
half3 hDirWS = normalize(vDirWS + lDirWS);
//【Ambient】
half3 albedo = (_Color * var_MainTex).rgb;
half3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * albedo; // 环境光
//【Shadow】
half shadow = LIGHT_ATTENUATION(i);
//【Diff】
half halfLambert = dot(nDirWS, lDirWS) * 0.5 + 0.5;
half4 var_RampTex = tex2D(_RampTex, float2(halfLambert * shadow, 0.2)); // shadow * halflambert 再采样,使得阴影是RampTex的暗面
half3 diffuse = var_RampTex.rgb * albedo * _LightColor0.rgb;
//【Spec】
half bulling_withoutMax0 = dot(nDirWS , lDirWS);
half w = fwidth(bulling_withoutMax0) * 2.0; // 高光边缘处,值越大
half3 specular = _SpecularCol.rgb * smoothstep(-w, w, bulling_withoutMax0 + _SpecularScale -1) ; //-1是为了缩小高光点
//【最终输出】
half3 finalCol = diffuse + specular + ambient;
return half4(finalCol, 1.0);
}
ENDCG
}
}
Fallback "Diffuse"
}
最终效果
14.2 素描风格的渲染
微软研究院发表了一篇关于素描风格的渲染文章,通过提前生成的素描纹理实现素描风格渲染,这些素描纹理组成了一个 色调艺术映射(Tonal Art Map,TAM)
素描纹理:
- 从左到右,纹理的笔触逐渐增多,用于模拟不同光照下的漫反射
- 从上到下,每张纹理的多级渐变纹理(mipmaps),此mipmaps不是简单对上一级纹理进行降采样,而是需要保持笔触之间的间隔,以便得到更真实的素描效果 本节课,我们只使用6张素描纹理,不考虑mipmaps
14.2.1实现:
- Properties:
- 用UsePass调用边缘线Pass
- 顶点着色器:
- 片元着色器:
Shader "Unlit/C14_Hatching"
{
Properties
{
_Color ("Color Tint", Color) = (1.0, 1.0, 1.0, 1.0)
_MainTex ("Texture", 2D) = "white" {}
_TileFactor ("平铺系数", Float) = 1.0 // 值越大,素描线条越密;_TileFactor缩放uv坐标,控制纹理大小
_OutLine ("OutLine", Range(0.0, 1.0)) = 0.1 // 边缘线,颜色就不追加了,默认黑色
_HatchMap0 ("HatchMap 0", 2D) = "white" {}
_HatchMap1 ("HatchMap 1", 2D) = "white" {}
_HatchMap2 ("HatchMap 2", 2D) = "white" {}
_HatchMap3 ("HatchMap 3", 2D) = "white" {}
_HatchMap4 ("HatchMap 4", 2D) = "white" {}
_HatchMap5 ("HatchMap 5", 2D) = "white" {}
}
SubShader
{
Tags { "RenderType"="Opaque" }
//【调用14.1节的描边Pass】
UsePass "Unlit/C14_ToonShading/OUTLINE"
//【素描Pass】
Pass
{
Tags {"LightMode" = "ForwardBase"}
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
#include "Lighting.cginc" //阴影所需
#include "AutoLight.cginc" //阴影所需
#pragma multi_compile_fwdbase_fullshadows //阴影所需
half4 _Color;
sampler2D _MainTex;
float _TileFactor;
half _OutLine;
sampler2D _HatchMap0;
sampler2D _HatchMap1;
sampler2D _HatchMap2;
sampler2D _HatchMap3;
sampler2D _HatchMap4;
sampler2D _HatchMap5;
struct appdata
{
float4 vertex : POSITION;
float2 uv0 : TEXCOORD0;
float3 normal : NORMAL;
};
struct v2f
{
float4 pos : SV_POSITION;
float2 uv0 : TEXCOORD0;
float3 posWS : TEXCOORD1;
half3 hatchWeights0 : TEXCOORD2; //纹理012的权值————权值当作亮度
half3 hatchWeights1 : TEXCOORD3; //纹理345的权值————权值当作亮度
LIGHTING_COORDS(4, 5)
};
v2f vert (appdata v)
{
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
o.uv0 = v.uv0 * _TileFactor; // _TileFactor缩放uv坐标,控制纹理大小
o.posWS = mul(unity_ObjectToWorld , v.vertex).xyz;
float3 nDirWS = normalize(UnityObjectToWorldNormal(v.normal));
float3 lDirWS = normalize(UnityWorldSpaceLightDir(o.posWS));
//【光照模型:Lambert】
half lambert = max(0.0, dot(nDirWS, lDirWS));
o.hatchWeights0 = half3(0.0, 0.0, 0.0);
o.hatchWeights1 = half3(0.0, 0.0, 0.0);
float hatchFactor = lambert * 7.0; // 通过Lambert决定不同纹理的权值
//【权值给予,对于每个顶点】
if (hatchFactor > 6.0) { // 白色权值 = 1 - 所有纹理的权值之和,这里最亮,所以白色权值为1.
// pure white ,do nothing
}
else if (hatchFactor > 5.0) { // 纹理0权值;此时所有权值相加不为1,所以是白色和纹理0混合
o.hatchWeights0.x = 6.0 - hatchFactor;
}
else if (hatchFactor > 4.0) { // 纹理0权值, 纹理1权值。 纹理0权值+ 纹理1权值=1 ,所以是纹理0和纹理1混合
o.hatchWeights0.x = hatchFactor - 4.0;
o.hatchWeights0.y = 1 - o.hatchWeights0.x;
}
else if (hatchFactor > 3.0) {
o.hatchWeights0.y = hatchFactor - 3.0;
o.hatchWeights0.z = 1 - o.hatchWeights0.y;
}
else if (hatchFactor > 2.0) {
o.hatchWeights0.z = hatchFactor - 2.0;
o.hatchWeights1.x = 1 - o.hatchWeights0.z;
}
else if (hatchFactor > 1.0) {
o.hatchWeights1.x = hatchFactor - 1.0;
o.hatchWeights1.y = 1 - o.hatchWeights1.x;
}
else {
o.hatchWeights1.y = hatchFactor - 0.0;
o.hatchWeights1.z = 1 - o.hatchWeights1.y;
}
TRANSFER_VERTEX_TO_FRAGMENT(o) //阴影
return o;
}
half4 frag (v2f i) : SV_Target
{
//【采样纹理,获得每个像素对应的颜色】
half4 hatchMap0 = tex2D(_HatchMap0, i.uv0) * i.hatchWeights0.x; //纹理0的值
half4 hatchMap1 = tex2D(_HatchMap1, i.uv0) * i.hatchWeights0.y; //纹理1的值
half4 hatchMap2 = tex2D(_HatchMap2, i.uv0) * i.hatchWeights0.z; //纹理2的值
half4 hatchMap3 = tex2D(_HatchMap3, i.uv0) * i.hatchWeights1.x; //纹理3的值
half4 hatchMap4 = tex2D(_HatchMap4, i.uv0) * i.hatchWeights1.y; //纹理4的值
half4 hatchMap5 = tex2D(_HatchMap5, i.uv0) * i.hatchWeights1.z; //纹理5的值
half4 sumAllHatchWeights = i.hatchWeights0.x + i.hatchWeights0.y + i.hatchWeights0.z +
i.hatchWeights1.x + i.hatchWeights1.y + i.hatchWeights1.z; //得到所有权值之和,用于计算纯白色
half4 whiteCol = half4(1.0, 1.0, 1.0, 1.0) * (1.0 - sumAllHatchWeights); //计算白色
half4 hatchCol = whiteCol + hatchMap0 + hatchMap1 + hatchMap2 + hatchMap3 + hatchMap4 + hatchMap5; //素描颜色
//【shadow】
half shadow = LIGHT_ATTENUATION(i);
//【finalCol】
half3 finalCol = (hatchCol * _Color ).rgb;
return half4(finalCol, 1.0);
}
ENDCG
}
}
Fallback "Diffuse"
}
最终效果