卡通渲染這方面,Arc System Works 無疑是我目前見過的做的最好的
“NPR就是各種Trick”
怼 Real-Time Rendering 的第7天,寫一篇NPR緩一緩,這幾天就是看書,然後忙裡偷閑玩幾把Gwent,聽說移動端快出了,舒服
上一篇關于卡通渲染的文章實作了一些卡通渲染基礎操作 梯度漫反射、頂點擴張描邊、邊緣光、各向異性高光;
本篇繼續對卡通渲染基本操作進行補充。
實驗素材來自Toony Colors Pro
由程式控制的多梯度
1. 對光影比例的控制
diffuse的經驗模型一般都包含,
我們可以通過一個量來控制光影的比例
2. 對色階層數控制
“劃分”最常用的方法就是 先乘上一個數,再取整
這裡 / _ToonSteps 目的是 在 0 - 1 範圍内梯度
3. 控制色階之間的平滑程度
float interval = 1 / _ToonSteps;
ramp = interval * smoothstep(level - _RampSmooth * interval * 0.5, level + _RampSmooth * interval * 0.5, diff) + level - interval;
ramp = max(0, ramp);
因為smoothstep本身是平滑內插補點,當RampSmooth = 1,應該是完全平滑,就是說線性,是以這裡可以用step或者if處理一下,建議step
陰影部分的顔色控制
1. 增加顔色系數,控制陰影部分顔色
Toony Colors Pro 2 中的做法是用 “_ShadowColor” 和 “_HightLIghtColor” 內插補點,把“_ShadowColor”的a通道作為內插補點系數,控制疊加在片段上的高光和陰影顔色
_SColor = lerp(_HColor, _SColor, _SColor.a);
float3 rampColor = lerp(_SColor.rgb, _HColor.rgb, ramp);
......
float3 diffuse = albedo * lightColor * rampColor;
2.額外的陰影紋理
雖然需要額外的采樣,但是可以增加陰影的層次感,對于不同部分添加不同的陰影顔色,美術更容易控制,不用調半天材質
float4 shadowTex = tex2D(_ShadowTex,i.uv);
albedo = lerp(shadowTex.rgb,albedo.rgb,ramp);
對于描邊的控制
日式卡渲趨向于基于幾何體生成的方法去描邊,角色不同部位的描邊粗細和顔色往往更容易控制
比如上圖中 頭發的描邊和其他部位描邊顔色, 衣服描邊寬度的粗細變化,甚至有的地方出現了“斷線”。
可以通過額外的紋理控制,充分利用各個通道,NPR沒有統一标準,這裡舉一個例子:
A通道,控制描邊的粗細,比如 0.0,就能做出斷線的效果
RGB,控制描邊顔色
還可以更複雜,比如:
A通道,描邊的可見性
R通道,控制 頂點 到 錄影機 的擴張系數,說白了就是根據距離遠近控制(乘)描邊粗細
G通道,描邊的粗細
B通道,可以預留,也可用來做陰影的遮罩,我們一般需要面部或其它一些部位顔色亮一些
局部曝光
曝光是提升視覺效果的常用手段
比如說,我們可以設定邊緣光設定更高的強度,根據需求控制人物不同位置的曝光程度。
對邊緣光的控制
基本和梯度漫反射一樣,可以提供對邊緣光面積和平滑程度的控制。原理也基本同上
對于高光的控制
增加額外的控制高光的灰階圖或通道,或者各向異性高光,在上一篇文章已經提到
支援多光源的卡通渲染
以前向渲染為前提
除了不同光源的衰減計算之外,在ForwardBase中計算過的東西,是完全搬運到ForwardAdd中,還是有選擇性的去掉一些東西。 這取決于Shader的情況和需求。 NPR和BPR在這一方面是完全不同的,效果好看就完事兒了。
舉個例子
實驗素材來自MiHoYo的崩壞,它的人物渲染和罪惡裝備Xrd極為相似。
前幾天還原了MiHoYo的一個人物,好像叫白練?,除了頭發對于面部的陰影,其他地方大體還原了(頭發對面部的陰影按理說也該有了,就是沒效果,我以為是暗面計算有問題,可是換成兩個片面又沒問題了,目前沒找出來錯誤在哪)
順便提一下,很牛批的軟體,就是目前導出圖檔的時候沒Alpha,采樣a全是1.0,導緻我還得用PS自己加通道,我感覺有必要回報一下。
MainTex: rgb 顔色, a 是否受光照影響
LightMap:r控制高光顔色, g 控制固定陰影 b 控制高光範圍。這是一張RGB圖,a沒用
高光和陰影控制什麼的其實上面也都有提到,就是利用通道控制,隻不過用的Trick不一樣罷了,很多部落客都講了,我就不再贅述了。
最後奉上一個很普通的卡通渲染Shader
有3個Pass,ForwardBase,ForwardAdd,ShadowCaster。
卡通渲染不一定要OutLine,這個Shader隻是用來看着玩,NPR務必根據需求自己 寫 (借鑒+增删改)。
Shader "GaoShuai/MultiLightItem" {
Properties {
//顔色
_Color ("Color", Color) = (1, 1, 1, 1)
_HColor ("Highlight Color", Color) = (1.0, 1.0, 1.0, 1.0)
_SColor ("Shadow Color", Color) = (0.8, 0.8, 0.8, 1.0)
//主紋理
_MainTex ("Main Texture", 2D) = "white" {}
//梯度
_ToonSteps ("Steps of Toon", range(1, 9)) = 2
_RampThreshold ("Ramp Threshold", Range(0.1, 1)) = 0.5
_RampSmooth ("Ramp Smooth", Range(0, 1)) = 0.1
//高光
_SpecColor ("Specular Color", Color) = (0.5, 0.5, 0.5, 1)
_SpecSmooth ("Specular Smooth", Range(0, 1)) = 0.1
_Shininess ("Shininess", Range(0.001, 10)) = 0.2
_Gloss("Gloss",Range(0.0,1.0))=1.0
// 邊緣光
_RimColor ("Rim Color", Color) = (0.8, 0.8, 0.8, 0.6)
_RimThreshold ("Rim Threshold", Range(0, 1)) = 0.5
_RimSmooth ("Rim Smooth", Range(0, 1)) = 0.1
}
SubShader {
Tags { "RenderType"="Opaque" }
Pass {
Tags { "LightMode"="ForwardBase" }
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#pragma multi_compile_fog
#pragma multi_compile_fwdbase
#pragma multi_compile_instancing
#pragma enable_d3d11_debug_symbols
#include "Lighting.cginc"
#include "AutoLight.cginc"
float4 _Color;
float4 _HColor;
float4 _SColor;
sampler2D _MainTex;
float4 _MainTex_ST;
float _RampThreshold;
float _RampSmooth;
float _ToonSteps;
float _SpecSmooth;
float _Shininess;
float _Gloss;
float4 _RimColor;
float _RimThreshold;
float _RimSmooth;
struct a2v {
float4 vertex : POSITION;
float3 normal : NORMAL;
float4 texcoord : TEXCOORD0;
float4 tangent : TANGENT;
UNITY_VERTEX_INPUT_INSTANCE_ID
};
struct v2f {
float4 pos : SV_POSITION;
float2 uv : TEXCOORD0;
float3 worldNormal : TEXCOORD1;
float3 worldPos : TEXCOORD2;
SHADOW_COORDS(3)
UNITY_FOG_COORDS(4)
};
UNITY_INSTANCING_BUFFER_START(Props)
UNITY_INSTANCING_BUFFER_END(Props)
v2f vert (a2v v) {
v2f o;
UNITY_SETUP_INSTANCE_ID(v);
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);
UNITY_TRANSFER_FOG(o,o.pos);
return o;
}
float4 frag(v2f i) : SV_Target {
float3 worldNormal = normalize(i.worldNormal);
float3 worldLightDir = normalize(UnityWorldSpaceLightDir(i.worldPos));
float3 worldViewDir = normalize(UnityWorldSpaceViewDir(i.worldPos));
float3 lightColor = _LightColor0.rgb;
float4 c = tex2D (_MainTex, i.uv);
float3 albedo = c.rgb * _Color.rgb;
float Alpha = c.a*_Color.a;
float Specular = _Shininess;
UNITY_LIGHT_ATTENUATION(atten, i, i.worldPos);
float3 floatDir = normalize(worldLightDir + worldViewDir);
float3 ndl = max(0,dot(worldNormal, worldLightDir)); //光照系數
float ndh = max(0, dot(worldNormal, floatDir)); //高光系數
float ndv = max(0, dot(worldNormal, worldViewDir)); //邊緣光系數
//控制光影比例
float diff = smoothstep(_RampThreshold - ndl, _RampThreshold + ndl, ndl);
float interval = 1 / _ToonSteps;
//簡化顔色,劃分色階
float level = round(diff * _ToonSteps) / _ToonSteps;
float ramp ;
//平滑過渡色階
ramp = interval * smoothstep(level - _RampSmooth * interval * 0.5, level + _RampSmooth * interval * 0.5, diff) + level - interval;
ramp = max(0, ramp);
ramp *= atten;
//使用顔色疊加,對高光部分和陰影部分疊色
_SColor = lerp(_HColor, _SColor, _SColor.a);
float3 rampColor = lerp(_SColor.rgb, _HColor.rgb, ramp);
//高光計算
float spec = pow(ndh, Specular * 128.0) * _Gloss;
spec *= atten;
spec = smoothstep(0.5 - _SpecSmooth * 0.5, 0.5 + _SpecSmooth * 0.5, spec);
//邊緣光計算
float rim = (1.0 - ndv) * ndl;
rim *= atten;
rim = smoothstep(_RimThreshold - _RimSmooth * 0.5, _RimThreshold + _RimSmooth * 0.5, rim);
float4 color;
float3 diffuse = albedo * lightColor * rampColor; //疊加顔色
float3 specular = _SpecColor.rgb * lightColor * spec;
float3 rimColor = _RimColor.rgb * lightColor * _RimColor.a * rim;
color.rgb = diffuse + specular + rimColor;
color.a = Alpha;
UNITY_APPLY_FOG(i.fogCoord, color);
return color;
}
ENDCG
}
Pass {
Tags { "LightMode"="ForwardAdd" }
Blend One One
CGPROGRAM
#pragma multi_compile_fwdadd
#pragma multi_compile_instancing
#pragma enable_d3d11_debug_symbols
#pragma vertex vert
#pragma fragment frag
#include "Lighting.cginc"
#include "AutoLight.cginc"
float4 _Color;
float4 _HColor;
float4 _SColor;
sampler2D _MainTex;
float4 _MainTex_ST;
float _RampThreshold;
float _RampSmooth;
float _ToonSteps;
float _SpecSmooth;
float _Shininess;
float _Gloss;
float4 _RimColor;
float _RimThreshold;
float _RimSmooth;
struct a2v {
float4 vertex : POSITION;
float3 normal : NORMAL;
float4 texcoord : TEXCOORD0;
float4 tangent : TANGENT;
UNITY_VERTEX_INPUT_INSTANCE_ID
};
struct v2f {
float4 pos : SV_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;
}
UNITY_INSTANCING_BUFFER_START(Props)
UNITY_INSTANCING_BUFFER_END(Props)
float4 frag(v2f i) : SV_Target {
float3 worldNormal = normalize(i.worldNormal);
#ifdef USING_DIRECTIONAL_LIGHT
fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz);
#else
fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz - i.worldPos.xyz);
#endif
float3 worldViewDir = normalize(UnityWorldSpaceViewDir(i.worldPos));
float3 lightColor = _LightColor0.rgb;
float4 c = tex2D (_MainTex, i.uv);
float3 albedo = c.rgb * _Color.rgb;
float Alpha = c.a*_Color.a;
float Specular = _Shininess;
#ifdef USING_DIRECTIONAL_LIGHT
fixed atten = 1.0;
#else
#if defined (POINT)
float3 lightCoord = mul(unity_WorldToLight, float4(i.worldPos, 1)).xyz;
fixed atten = tex2D(_LightTexture0, dot(lightCoord, lightCoord).rr).UNITY_ATTEN_CHANNEL;
#elif defined (SPOT)
float4 lightCoord = mul(unity_WorldToLight, float4(i.worldPos, 1));
fixed atten = (lightCoord.z > 0) * tex2D(_LightTexture0, lightCoord.xy / lightCoord.w + 0.5).w * tex2D(_LightTextureB0, dot(lightCoord, lightCoord).rr).UNITY_ATTEN_CHANNEL;
#else
fixed atten = 1.0;
#endif
#endif
float3 floatDir = normalize(worldLightDir + worldViewDir);
float3 ndl = max(0,dot(worldNormal, worldLightDir));
float ndh = max(0, dot(worldNormal, floatDir));
float diff = smoothstep(_RampThreshold - ndl, _RampThreshold + ndl, ndl);
float interval = 1 / _ToonSteps;
float level = round(diff * _ToonSteps) / _ToonSteps;
float ramp = interval * smoothstep(level - _RampSmooth * interval * 0.5, level + _RampSmooth * interval * 0.5, diff) + level - interval;
ramp = max(0, ramp);
ramp *=atten;
float spec = pow(ndh, Specular * 128.0) * _Gloss;
spec *= atten;
spec = smoothstep(0.5 - _SpecSmooth * 0.5, 0.5 + _SpecSmooth * 0.5, spec);
float4 color;
float3 diffuse = albedo * lightColor * ramp;
float3 specular = _SpecColor.rgb * lightColor * spec;
color.rgb = specular + diffuse;
color.a = Alpha;
return color;
}
ENDCG
}
pass
{
Tags{ "LightMode" = "ShadowCaster"}
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#pragma multi_compile_shadowcaster
#pragma enable_d3d11_debug_symbols
#include "UnityCG.cginc"
struct v2f
{
V2F_SHADOW_CASTER;
};
v2f vert(appdata_full v)
{
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
TRANSFER_SHADOW_CASTER_NORMALOFFSET(o)
return o;
}
float4 frag(v2f o) :SV_Target
{
SHADOW_CASTER_FRAGMENT(o)
}
ENDCG
}
}
}