ICode9

精准搜索请尝试: 精确搜索
首页 > 其他分享> 文章详细

【Shader】解读Unity Chan的卡通材质

2019-09-21 17:43:25  阅读:465  来源: 互联网

标签:贴图 高光 颜色 Chan Shader 阴影 Unity rgb 衰减


目录

写在前面

分析Unity Chan所用的Shader,并加些注释
本文借鉴的是大佬的《【Unity Shader】Unity Chan的卡通材质》
地址:https://blog.csdn.net/candycat1992/article/details/51050591
《使用CgInclude让你的Shader模块化——使用#define指令创建Shader》
地址:https://blog.csdn.net/candycat1992/article/details/38961411
还有另一位大佬的详细注释《【卡通渲染】 解讀Unity Chan》
地址:https://www.twblogs.net/a/5c0a6dbfbd9eee6fb21348f2

Unity Chan使用的Shader

Unity Chan包含了3个Shader(CG)文件:

名字 用途
CharaOutline 包含了最通用的shader,即绘制描边效果。
CharaMain 角色使用的最主要的Shader,包含了一些漫反射、阴影、高光、边缘高光、反射的通用的vertex shader和fragma shader的实现。用于渲染衣服和头发。
CharaSkin 皮肤使用的shader,包含了漫反射、边缘高光和阴影的实现(相较于CharaMain,没有计算高光和反射)。用于渲染皮肤、眼镜、脸颊、睫毛。

写给自己

UnityChan用了很多的shader,比如有衣服,皮肤的shader,但是打开之后法线跟平时写的不一样,代码量怎么会这么少,后面发现其实代码都写在一些主要的shader里,里面定义了很多自己写的宏,也就是模块化的代码,之后只要在相应的shader里调用相应的模块definition就行
在这里插入图片描述

CharaOutline:描边

卡通效果需要高光、漫反射等,还要描边,Unity Chan描边的实现也是把顶点沿着法线方向扩张后得到的。

上面的实现,就是把顶点和法线变换到裁剪空间后,把顶点沿着法线方向进行扩张。法线的z分量增加了0.0001,为了稍微防止一下描边挡住正常渲染。不过这个方法有弊端:当描边宽度很大时,会有穿帮镜头。

思路:

  • Vert
    • 转化法线和顶点;
    • 扩大法线,并进行缩放;
      SN = Edge * DIVISOR * n (描边宽度* 轮廓厚度乘数* 法线)
    • 法线的z分量稍稍增加一下,然后和顶点相加
  • Frag
    • 选出最大通道
    • 其他不符合的通道颜色加深
      • 加深通道
        newMapColor = lerp (SATURATION_FACTOR * diff,diff ,lerpVals)
        (饱和系数 * 采样贴图,采样贴图,最大通道为1其他通道为0)
    • 混合
      • float4 ( 亮度系数 * newMapColor * diff , diff.a) * 轮廓颜色定义 * 灯光色

CharaOutline的代码如下

// Upgrade NOTE: replaced 'mul(UNITY_MATRIX_MVP,*)' with 'UnityObjectToClipPos(*)'

// Outline shader

// Material parameters
float4 _Color;
float4 _LightColor0;
float _EdgeThickness = 1.0;  //描边宽度
float4 _MainTex_ST;

// Textures
sampler2D _MainTex;

// Structure from vertex shader to fragment shader
struct v2f
{
	float4 pos : SV_POSITION;
	float2 uv : TEXCOORD0;
};

// Float types
#define float_t  half
#define float2_t half2
#define float3_t half3
#define float4_t half4

//definition定义,创建自己的CgInlude去保存光照模型、变量和辅助函数,可以使得代码更加模块化
//告诉Unity去查找这个名称的定义
//Outline thickness multiplier 轮廓厚度乘数
#define INV_EDGE_THICKNESS_DIVISOR 0.00285
// Outline color parameters 轮廓的颜色参数
#define SATURATION_FACTOR 0.6
#define BRIGHTNESS_FACTOR 0.8

// Vertex shader
v2f vert( appdata_base v )  //包含顶点位置,法线和一个纹理坐标。
{
	v2f o;
	o.uv = TRANSFORM_TEX( v.texcoord.xy, _MainTex );
	//将顶点和法线变换到裁剪空间,然后把顶点沿着法线方向进行扩张
	half4 projSpacePos = UnityObjectToClipPos( v.vertex );
	half4 projSpaceNormal = normalize( UnityObjectToClipPos( half4( v.normal, 0 ) ) );

	half4 scaledNormal = _EdgeThickness * INV_EDGE_THICKNESS_DIVISOR * projSpaceNormal; // * projSpacePos.w;

	scaledNormal.z += 0.00001;
	o.pos = projSpacePos + scaledNormal;

	return o;
}

// Fragment shader
float4 frag( v2f i ) : SV_Target
{
	//使得描边的颜色暗于正常渲染的颜色,起到强调边缘的结果
	//亮度系数BRIGHTNESS_FACTOR,用于控制整体变暗的程度
	float4_t diffuseMapColor = tex2D( _MainTex, i.uv );

	//最大通道,比较原贴图的三大通道,取值最大的
	float_t maxChan = max( max( diffuseMapColor.r, diffuseMapColor.g ), diffuseMapColor.b );
	float4_t newMapColor = diffuseMapColor;

	//值最高的分量颜色保持不变,因为此时lerpVals=1,
	//而其他分量只要比最高值小,lerpVals就会取0,需要乘以变暗系数SATURATION_FACTOR 
	maxChan -= ( 1.0 / 255.0 );  //最大通道减1
	float3_t lerpVals = saturate( ( newMapColor.rgb - float3( maxChan, maxChan, maxChan ) ) * 255.0 );
	newMapColor.rgb = lerp( SATURATION_FACTOR * newMapColor.rgb, newMapColor.rgb, lerpVals ); //插值

	return float4( BRIGHTNESS_FACTOR * newMapColor.rgb * diffuseMapColor.rgb, diffuseMapColor.a ) * _Color * _LightColor0;
}

CharaMain:衣服和头发

CharaMain主要用于渲染角色的衣服和头发,使用了这个CharaMain的shader有:Unitychan_chara_hair,Unitychan_chara_hair_ds…后面跟有_ds的时表示是不时双面渲染:没有_ds的在渲染时剔除了背面(Cull Back),而有_ds的则关闭了剔除(Cull off)。
CharaMain 里面具体包括了一堆vert和frag的实现:

  • vert:顶点变换。计算主纹理(_MainTex)的采样坐标,计算世界空间下的法线方向、视角方向、光照方向等
  • frag:主要完成5个工作:
    在这里插入图片描述

光照衰减

通常计算漫反射是通过对贴图采样后再乘以漫反射系数(n点乘l),不过在这里用的却是法线和观察方向(n点乘v),采样一张衰减贴图,得出衰减值,然后将衰减值去混合原贴图颜色和带阴影的原贴图颜色。不过这样的效果没有考虑到光照方向,而是使用了类似边缘高光的方法计算光照衰减。

	//衰减的光照颜色
	// Falloff. Convert the angle between the normal and the camera direction into a lookup for the gradient

	//【漫反射系数】n*v(实际上应该是法线和光照方向,这里改了)
	float_t normalDotEye = dot( normalVec, i.eyeDir.xyz );

	//【截取漫反射系数】
	//float The float result between the min and max values,将数返回在max和min之间
	//Mathf.abs 取反
	float_t falloffU = clamp( 1.0 - abs( normalDotEye ), 0.02, 0.98 );

	//【衰减纹理采样】用上面的漫反射系数去采样一张衰减纹理
	//FALLOFF_POWER 衰减程度
	float4_t falloffSamplerColor = FALLOFF_POWER * tex2D( _FalloffSampler, float2( falloffU, 0.25f ) );

	//【阴影颜色】平方加深原贴图颜色,作为阴影
	float3_t shadowColor = diffSamplerColor.rgb * diffSamplerColor.rgb;

	//【c=混合了阴影的原贴图】用采样后的衰减纹理R通道 插值 原贴图和阴影颜色
	float3_t combinedColor = lerp( diffSamplerColor.rgb, shadowColor, falloffSamplerColor.r );

	//【c=有阴影、衰减度的原贴图】带阴影的原贴图 * (1+带透明度的衰减纹理)
	combinedColor *= ( 1.0 + falloffSamplerColor.rgb * falloffSamplerColor.a );

高光反射

利用法线与观察方向的点乘结果,n点乘v来获得高光反射系数,然后与漫反射系数以及高光反射的指数部分传给CG的lit函数,计算各个光照系数(返回一个四元向量)。也可以自己写代码计算高光反射光照,这么写应该是为了利用GPU,提高一些性能。

	// Specular高光反射
	// Use the eye vector as the light vector

#ifdef ENABLE_SPECULAR
	//【采样高光反射贴图】
	float4_t reflectionMaskColor = tex2D( _SpecularReflectionSampler, i.uv.xy );

	//【高光反射系数】这里用了n点乘v,实际上是n点乘h
	float_t specularDot = dot( normalVec, i.eyeDir.xyz );

	//【计算各个光照系数】将高光反射系数,还有上面的漫反射系数,还有高光强度代入lit函数里,得到高光反射光照
	//返回一个(光照)4元向量(环境, 漫反射 , 高光 ,1)
	float4_t lighting = lit( normalDotEye, specularDot, _SpecularPower );

	//【高光反射颜色】光照系数的z分量与原贴图颜色、高光反射贴图颜色混合
	float3_t specularColor = saturate( lighting.z ) * reflectionMaskColor.rgb * diffSamplerColor.rgb;

	//【c= 带阴影、衰减度、高光的原贴图】
	combinedColor += specularColor;
#endif

反射

没有使用环境贴图,而是使用了一张普通的二维纹理;采样坐标是通过把反射方向从[-1, 1]映射到[0, 1]来实现的,得到初始的反射颜色;调用GetOverlayColor函数来计算原光照结果和反射颜色混合后的结果;使用反射遮罩值来混合之前的计算结果和反射结果,并和颜色属性以及光源颜色相乘得到结果。
最后还计算了该像素的透明度,也就是漫反射贴图、颜色属性和光源颜色的透明度的乘积,它会作为输出像素的透明通道值。

	//反射
	// Reflection

#ifdef ENABLE_REFLECTION
	//【反射方向】reflect(-v·n),输出的是xzy
	float3_t reflectVector = reflect( -i.eyeDir.xyz, normalVec ).xzy;

	//【坐标采样映射】坐标的xy加1并乘以0.5,目的是把反射方向从【-1,1】映射到【0,1】,得到初始的反射颜色
	float2_t sphereMapCoords = 0.5 * ( float2_t( 1.0, 1.0 ) + reflectVector.xy );

	//【二维纹理采样】
	float3_t reflectColor = tex2D( _EnvMapSampler, sphereMapCoords ).rgb;

	//【混合颜色】GetOverlayColor()函数
	reflectColor = GetOverlayColor( reflectColor, combinedColor );

	//【c=带阴影、衰减度、高光、反射的原贴图】 反射遮罩的透明通道 插值 c和反射颜色
	combinedColor = lerp( combinedColor, reflectColor, reflectionMaskColor.a );
#endif

	combinedColor *= _Color.rgb * _LightColor0.rgb;
	float opacity = diffSamplerColor.a * _Color.a * _LightColor0.a;

阴影

没有使用LIGHT_ATTENUATION和之前的结果计算,而是使用了阴影衰减值插值阴影颜色(漫反射纹理采样结果的平方)和现有颜色,得到的阴影效果就是在阴影覆盖的地方就是变暗了的纹理颜色。(就是把纹理变暗当阴影处理了)

//阴影
#ifdef ENABLE_CAST_SHADOWS //如果开启了投影
	// Cast shadows

	//【阴影颜色】自定义的阴影颜色属性 * c(c包含了阴影、衰减、高光、反射原贴图)
	shadowColor = _ShadowColor.rgb * combinedColor;

	//【阴影衰减值】映射到【-1,1】之间,然后只取【0,1】
	float_t attenuation = saturate( 2.0 * LIGHT_ATTENUATION( i ) - 1.0 );

	//【c】用阴影衰减值 插值 阴影颜色和c 
	combinedColor = lerp( shadowColor, combinedColor, attenuation );
#endif

边缘高光

边缘高光是卡通效果的必备效果,这里也使用了n·l来和n·v相乘,计算边缘高光的衰减,然后用它对一张边缘高光纹理采样,得到真正的边缘高光衰减值。

	//边缘高光
	// Rimlight

#ifdef ENABLE_RIMLIGHT
	//【边缘高光方向】
	float_t rimlightDot = saturate( 0.5 * ( dot( normalVec, i.lightDir ) + 1.0 ) );
	//【边缘高光大小】方向相乘,为采样作为x轴使用
	falloffU = saturate( rimlightDot * falloffU );
	//在边缘高光贴图上采样
	falloffU = tex2D( _RimLightSampler, float2( falloffU, 0.25f ) ).r;
	//与原贴图颜色相乘,之后与c相加
	float3_t lightColor = diffSamplerColor.rgb; // * 2.0;
	combinedColor += falloffU * lightColor;
#endif

大大总结的一些trick:

  • 计算了一个全局的shadowColor,它其实就是漫反射纹理采样结果的平方,效果就是比原贴图颜色暗了一点。
  • 漫反射计算不需要考虑光照方向,而是使用n和v的点乘来计算衰减,这个衰减将会混合上面的shadowColor和正常的颜色贴图。得到的效果是模型边缘部分会较暗
  • 高光反射的部分同样不考虑光照方向,而是使用n和v的点乘。得到的效果是正对视角方向的部分高光越明显,和光源无光
  • 计算环境反射时使用普通的二维纹理来代替环境贴图
  • 使用阴影衰减值来混合shadowColor,这样阴影区域会保留角色的纹理细节
  • 边缘高光系数是NdotL和NdotV的共同结果,即那些和光照方向一致、且在模型边缘的地方高光越明显

CharaSkin:皮肤

CharaSkin主要用于渲染皮肤、眼睛、脸蛋、睫毛,这些部分。CharaSkin使用的代码和CharaMain中基本一样,只是精简了一些部分,它去掉了计算环境反射、高光反射的部分,只保留漫反射、边缘高光、和阴影的计算部分。而且,在计算边缘高光时,高光颜色也比CharaMain中的暗了一倍,即只去源颜色的0.5倍。除此之外,皮肤使用的漫反射衰减纹理也与衣服等使用的纹理不同:
在这里插入图片描述

标签:贴图,高光,颜色,Chan,Shader,阴影,Unity,rgb,衰减
来源: https://blog.csdn.net/kiritoV/article/details/100849551

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有