目录

  • 写在前面
  • Unity Chan使用的Shader
    • 写给自己
  • CharaOutline:描边
  • CharaMain:衣服和头发
    • 光照衰减
    • 高光反射
    • 反射
    • 阴影
    • 边缘高光
  • CharaSkin:皮肤

写在前面

分析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 );  //最大通道减1float3_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点乘hfloat_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),输出的是xzyfloat3_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 );
#endifcombinedColor *= _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倍。除此之外,皮肤使用的漫反射衰减纹理也与衣服等使用的纹理不同:

【Shader】解读Unity Chan的卡通材质相关推荐

  1. 【Unity Shader】Unity Chan的卡通材质

    写在前面 时隔两个月我终于来更新博客了,之前一直在学东西,做一些项目,感觉没什么可以分享的就一直没写.本来之前打算写云彩渲染或是Compute Shader的,觉得时间比较长所以打算先写个简单的. 今 ...

  2. Unity Shader学习:将mmd人物更新卡通材质

    最近图形学看到了shader,虽然很早前就学过但是一直没实操过,最近自己正好也在看Unity Shaderlab这本书,顺手解决了一个小问题,记录一下. mmd中人物材质接近PBR,这部分光照不太满意 ...

  3. Unity Shader 学习笔记(5)Shader变体、Shader属性定义技巧、自定义材质面板

    写在之前 Shader变体.Shader属性定义技巧.自定义材质面板,这三个知识点任何一个单拿出来都是一套知识体系,不能一概而论,本文章目的在于将学习和实际工作中遇见的问题进行总结,类似于网络笔记之用 ...

  4. 【Unity3D】 Unity Chan项目分享

    写在前面 之前的一个博文里分享了日本Unity酱的项目,如果大家有去仔细搜Unity酱的话,就会发现日本Unity官方还放出了一个更完整的Unity酱的项目,感觉被萌化了!(事实上,Unity日本经常 ...

  5. unity 线程断点时卡机_Compute Shader在Unity和UE4中的应用

    该文档为学习文档,如有错误欢迎指正. 1. D3D11 Compute Shader概述 Compute Shader 是一个通用计算 Stage.它利用了GPU的并行处理器,实现大量线程并发执行.它 ...

  6. unity再战PBR材质流程与材质制作实践

    版权声明:本文为博主原创文章,未经博主允许不得转载. 这篇在上一篇的基础上增加了对PBR的认识,主要包括了金属度和粗糙度(光滑度)的测试 unity里PBR流程,PBR材质属性具体分析 传统模型到PB ...

  7. 【Unity Shader】Unity中利用GrabPass实现玻璃效果

    <入门精要>中模拟玻璃是用了Unity里的一个特殊的Pass来实现的,这个Pass就是GrabPass,比起上一篇博客实现镜子的方法,这个方法我认为相对复杂,因此在实现之前需要对GrabP ...

  8. Blender简单卡通材质体现

    1.灯光,阴影设置 (1)为了更好的阴影效果,灯光选择日光. (2)灯光强度可自行调整. (3)勾选阴影,打开 "级联阴影图" 根据模型调整最大距离来获取最好的阴影效果. 2.卡通 ...

  9. Unity 在设备上材质显示正常,但是Editor下材质显示为紫色

    windows 下面bundle模式紫色图片解决   运行平台还是原来的 android 或 ios, 但是 PC Editor 的渲染引擎要跟平台的一致起来 Unity 在设备上材质显示正常,但是E ...

最新文章

  1. HarmonyOS ScrollView 使用
  2. 人人都能看懂的 Python 装饰器入门教程
  3. proget Android代码混淆
  4. SAP ABAP SM50事务码和Hybris Commerce的线程管理器
  5. 一个nodejs里日志文件的实现
  6. unity读取Text
  7. python基础知识-8-三元和一行代码(推导式)
  8. vivado 仿真_提高Vivado效率一种自研工具介绍
  9. 【HDU - 6290】 奢侈的旅行 (对题目预处理 + DIjkstra最短路)
  10. 开源开放 | 开源网络通信行业知识图谱(新华三)
  11. Linux配置yum源(离线和在线)
  12. 《我也能做CTO之程序员职业规划》和《.NET软件设计新思维——像搭积木一样搭建软件》新书发布会 回顾
  13. centos 打包某个目录_Linux(CentOS)下目录档案管理以及档案文件系统打包压缩
  14. unit系统与linux系统区别,python+unittet在linux与windows使用的区别
  15. app store connect
  16. 1-15 Burpsuite Sequencer介绍
  17. MyBatis_Plus(Spring版本笔记)
  18. 原来发朋友圈还有这讲究,难怪我的朋友圈没人看
  19. U盘的两个文件夹不见了,但它还是占着我的空间,为什么?
  20. jdm分布式架构框架

热门文章

  1. java获取origin,java – 通过环境变量指定@CrossOrigin orgins
  2. springclound
  3. ES5和ES6介绍及新增内容用法讲解
  4. 背包问题动态规划matlab,01背包问题动态规划详解
  5. 架设个人FTP服务器的三种方法
  6. springboot 后台把数据制作成excel表格并打成压缩包下载
  7. 零窗口探测怎么抓包_窗口信息探测(Spy4Win) v0.20b 中文版
  8. js运行机制,宏观任务和微观任务
  9. fts16_FT230XS-xxxx
  10. 简单实现对象调用:创建一个LOL中的英雄类和怪物类