天天看点

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

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