天天看點

人物渲染篇(二) —— 基礎卡通渲染 下

卡通渲染這方面,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
		}
	}
}