卡通着色器

  • 卡通光照
  • 描边 —— 全息描边
  • 描边 —— 顶点膨胀描边
    • 沿着法线
    • 在顶点与法线之间抉择
  • 高光改进

卡通光照

课本上介绍的卡通着色器有使用坡度图(ramp map)和直接程序截断两种方法。因为前者不太方便在编辑器中根据实际模型调整,所以个人比较偏向后一种方法。本文中的光照也是使用后者进行渲染。

//以下是光照处理部分伪码
float3 N = normalize(法线方向);
float3 L = normalize(光照方向);
float NdotL = dot(N, L);
NdotL = max(0.1, floor(NdotL * 层数参数)/层数参数);
最终光照 = _lightColor0.rgb * NdotL * 衰减因子;

这样就可以得到分层的龙:

乘上 Albedo 后就可以了
下面是光照部分(形参不要写反了!)

half4 LightingMyLighting (SurfaceOutput s, half3 lightDir, half3 viewDir, half atten){//光照分层处理---------------------half NdotL = dot(s.Normal, lightDir);NdotL = (NdotL + 1) * 0.5;                          //将范围映射至0-1half cel = floor(NdotL * _LightVal) / _LightVal;    //颜色阶梯atten = smoothstep(0.2, 0.8, atten);                //处理阴影边缘使其更加锋利//赋值输出-------------------------half3 col = (s.Albedo * _LightColor0.rgb * atten * cel).xyz;half4 c = half4(col, s.Alpha);return c;}

描边 —— 全息描边

全息描边的大致思想是计算出法线和视角较小的点(即视口中模型的边缘部分)进行染色,并且需要急剧变化的边缘。有两个语句都可以达到区分边缘效果:

float NdotV = dot(normalize(normalDir), normalize(viewDir);
//第一句处理使得 NdotV 反相,并限制在[0,1];pow控制数值急剧变化
//第二句使得 NdotV 舍弃掉小于_Val2的部分,且让筛选剩余的部分为1
NdotV = pow(saturate(1- NdotV), _Val1);
NdotV = step(_Val2, NdotV);


但是由于模型表面法线并非都是平滑连续变化的,这种描边算法不仅会粗细不均,而且使用 NdotV 的算法对正方体一类的模型很不友好。

Shader "Custom/sd_Toon"
{Properties{_MainTex ("Albedo (RGB)", 2D) = "white" {}_Alpha("Tex Alpha",Range(0,1)) = 0.5_Color("Main Color", Color) = (1,1,1,1)_LightVal("光照区分度",Range(0.1 , 10)) = 0.5_OutlineColor("边线颜色", color) = (0,0,0,0)_TestVal("边缘粗细", Range(0, 1)) = 0.1}SubShader{Tags { "RenderType"="Opaque"  }LOD 200CGPROGRAM#pragma surface surf MyLighting#pragma target 3.0sampler2D _MainTex;struct Input{float2 uv_MainTex;float3 viewDir;float3 worldNormal;};half4 _Color;float _LightVal;half _Alpha;fixed _TestVal;fixed4 _OutlineColor;UNITY_INSTANCING_BUFFER_START(Props)UNITY_INSTANCING_BUFFER_END(Props)void surf (Input IN, inout SurfaceOutput o){//图片取样half3 c;c = tex2D (_MainTex, IN.uv_MainTex).rgb * _Color;//根据法线和视角刻画描边范围half NdotV = dot(o.Normal, IN.viewDir);NdotV = pow(saturate(1- NdotV), 3);NdotV = step((1-_TestVal), NdotV);//outline range 筛除掉乘积小于_TestVal的部分(原色为0,上色部分为1)//根据范围上色c.rgb = lerp(_Color.rgb, c.rgb, _Alpha);o.Albedo = lerp(c.rgb, _OutlineColor, NdotV);o.Alpha = 1;}half4 LightingMyLighting (SurfaceOutput s, half3 lightDir, half3 viewDir, half atten){//光照分层处理---------------------half NdotL = dot(s.Normal, lightDir);NdotL = (NdotL + 1) * 0.5;                          //将范围映射至0-1half cel = floor(NdotL * _LightVal) / _LightVal;    //颜色阶梯//衰减因子处理---------------------atten = smoothstep(0.2, 0.8, atten);                //处理阴影边缘使其更加锋利half3 col = (s.Albedo * _LightColor0.rgb * atten * cel).xyz;half4 c = half4(col, s.Alpha);return c;}ENDCG}FallBack "Diffuse"
}

描边 —— 顶点膨胀描边

沿着法线

描边是否还有其他方案?回想起2D描边,会发现大部分的处理方案实际上是向外描边,而在shader中可以通过“膨胀”访问到模型外的一部分区域。

整理一下思路:通过顶点沿着【法线】方向膨胀,可以建立比原模型大一点的模型,将此模型上色为描边颜色。通过 Cull Front 剔除面向视点的部分,再将原模型渲染一次盖住中间部分。

dir = mul((float3x3)UNITY_MATRIX_MV, dir);   //Convert the final direction to viewspace
float2 offset = TransformViewToProjection(dir.xy);//Convert visual space direction xy coordinates to projection space
o.pos.xy += offset * o.pos.z * _Outline;

可能听起来不错?但是有些模型的模型空间原点不在几何中心,而是在底部或者顶部,导致膨胀出的轮廓不是均匀套在物体周围的。(而我刚好有个这样的模型)

在顶点与法线之间抉择

如何打造同时适配两种情况的着色器呢?
这种情况下,可以通过在顶点方向和法线方向之间插值,根据具体的模型作相应手动调整。
_Factor用于调整:float3 dir = lerp(dirV, dirN, _Factor);

方块采用沿顶点方向膨胀方式,适合有硬边且模型空间中心与几何中心基本重合的模型;鲸采用沿法线方向膨胀方式,适合法线变化比较平缓的模型。
完整代码:

Shader "Lesson/sd_OutlineTest3"
{Properties{_MainTex("Main Tex", 2D) = "white"{} //_MainTex("Main Tex", 2D) = ""{} 这个开启之后会抓取屏幕 不知道怎么回事_Color("Tint of MainTex", Color) = (1,1,1,1)_Outline ("Outline Range", Range(0, 0.3)) = 0.02_OutlineColor("Out line Color", Color) = (1,1,1,1)_Factor("Factor of dirV and dirN", Range(0, 1)) = 1_LightVal("Layering Val of Light", Range(0.001, 6)) = 0.5}SubShader{Pass{//本Pass用于描边Tags { "LightMode"="Always" }Cull FrontZWrite OnCGPROGRAM#pragma vertex vert#pragma fragment frag#include "UnityCG.cginc"fixed _Outline;fixed4 _OutlineColor;fixed _Factor;struct v2f{float4 pos : SV_POSITION;};v2f vert (appdata_full v){v2f o;o.pos = UnityObjectToClipPos(v.vertex);float3 dirV = normalize(v.vertex.xyz);       //Vertex directionfloat3 dirN = v.normal;                      //Normal directionfloat3 dir = lerp(dirV, dirN, _Factor);      //Adjust _Factor depends on the actural modeldir = mul((float3x3)UNITY_MATRIX_MV, dir);   //Convert the final direction to viewspace  float2 offset = TransformViewToProjection(dir.xy);//Convert visual space direction xy coordinates to projection spaceo.pos.xy += offset * o.pos.z * _Outline;return o;}fixed4 frag (v2f i) : SV_Target{fixed4 c = _OutlineColor;return c;}ENDCG}Pass{//本Pass用于模型基础着色Tags { "LightMode"="ForwardBase" }CGPROGRAM#pragma vertex vert#pragma fragment frag#include "UnityCG.cginc"float4 _LightColor0;sampler2D _MainTex;fixed4 _Color;fixed _LightVal;struct appdata{float4 pos:POSITION;float4 texcoord:TEXCOORD0;float3 normal:TEXCOORD1;};struct v2f{float4 pos : SV_POSITION;float3 lightDir : TEXCOORD0;float3 viewDir:TEXCOORD1;float3 normal:TEXCOORD2;float4 mainTex:TEXCOORD3;};v2f vert (appdata_full v){v2f o;o.pos = UnityObjectToClipPos(v.vertex);o.normal = v.normal;o.lightDir = ObjSpaceLightDir(v.vertex);o.viewDir = ObjSpaceViewDir(v.vertex);o.mainTex = v.texcoord;return o;}fixed4 frag (v2f i) : COLOR{float4 c = 1;float3 N = normalize(i.normal);float3 viewDir = normalize(i.viewDir);fixed NdotL = dot(N, i.lightDir);NdotL = max(0.1, floor(NdotL * _LightVal)/ _LightVal);float diff = max(0, dot(N, viewDir));diff = (diff + 1)/2;diff = smoothstep(0, 1, diff);fixed4 col = tex2D(_MainTex, i.mainTex);c = _LightColor0 * diff * NdotL * col * _Color;return c;}ENDCG}}
}

后附:
在一般情况下这种情况是适用的,但总有一些比较极端的模型(如下),并非所有部分都是均匀需要从法线/顶点方向膨胀的。这种情况下可以尝试通过自动插值逐点判断到底是要使用顶点还是法线方向,通过点积确定该点背离几何中心的程度,然后通过lerp函数确定最终dir。但是测试了一下,效果不是很好,于是在这里不作展示。

高光改进

上面展示的程序化的卡通上色采用均分色阶,要想获得足够亮的表面只能调高色彩分阶;但是那样又丧失了卡通分解光照的简洁感。如何人工加一个风格化的高光?
突然想到,荒野之息的卡通着色方式十分廉价,但是又能取得比较好的效果。简单分析了一下可以得出以下信息:

  1. 拉线风格高光 —— 各向异性高光叠加应用
  2. 两层颜色 —— 卡通光照分层
  3. 光色细边 ——全息描边

好耶!三个都学过诶!
第一点:需要调整(或者直接更换)各向异性应用的贴图。因为已经不需要密度那么大的拉丝效果了,游戏中更像是素描高光的表现方式。在这里我随手拿了一张电视波纹的贴图(下图)。

避免过于突兀的高光颜色,直接采用场景中 _LightColor0 的颜色对高光线进行上色。

第二点:需要控制分出的层数保持2层(或者3层),直接观察材质下部的视窗来调整会比较明显。调整数值,使得阴影部分占的区域比较小。


第三点:因为模型边缘的轮廓主要是内描边,所以用到了全息描边的方式。将粗细度调得细一些;描边颜色同样为 _LightColor0 的颜色。

组合拼装一下,可以得到以下效果:

(…好像害行?)

完整shader存档:

Shader "Lesson/sd_Toon3"
{Properties{_MainTex ("Albedo (RGB)", 2D) = "white" {}_Alpha("Tex Alpha",Range(0,1)) = 0.5_Color("Main Color", Color) = (1,1,1,1)_LightVal("光照区分度",Range(0.1 , 10)) = 0.5_OutlineColor("边线颜色", color) = (0,0,0,0)_Thickness("边缘粗细", Range(0, 0.3)) = 0.1_SpecTex("高光贴图", 2D) = "white"{}_SpecPower("高光密度" , Range(0, 0.7)) = 0.2_SpecPower2("高光范围" , Range(0.4, 1)) = 0.5}SubShader{Tags { "RenderType"="Opaque"  }LOD 200CGPROGRAM#pragma surface surf MyLighting#pragma target 3.0sampler2D _MainTex;sampler2D _SpecTex;struct Input{float2 uv_MainTex;float2 uv_SpecTex;float3 viewDir;float3 worldNormal;};half4 _Color;float _LightVal;half _Alpha;fixed _Thickness;fixed4 _OutlineColor;fixed _SpecPower;fixed _SpecPower2;struct surfOutput{fixed3 Albedo;fixed3 Normal;fixed3 Emission;fixed3 SpecDirection;fixed Alpha;};UNITY_INSTANCING_BUFFER_START(Props)UNITY_INSTANCING_BUFFER_END(Props)void surf (Input IN, inout surfOutput o){half3 c;c = tex2D (_MainTex, IN.uv_MainTex).rgb * _Color;o.SpecDirection = UnpackNormal(tex2D(_SpecTex, IN.uv_SpecTex));o.Albedo = c.rgb;o.Alpha = 1;}half4 LightingMyLighting (surfOutput s, half3 lightDir, half3 viewDir, half atten){//高光处理--------------------------fixed3 halfVector = normalize(lightDir + viewDir);halfVector = max(0, dot(s.Normal, halfVector));halfVector = pow(halfVector, _SpecPower2 * 17);                   //幂数描述单片高光面积fixed HdotS = dot(normalize(s.SpecDirection + s.Normal), halfVector);   //条纹高光HdotS = step(_SpecPower, (1 - HdotS)); float spec = normalize(HdotS);spec = saturate(spec* 0.2);//光照分层处理---------------------half NdotL = dot(s.Normal, lightDir);NdotL = (NdotL + 1) * 0.5;                                   //将范围映射至0-1half cel =max(0.2, floor(NdotL * _LightVal) / _LightVal);    //颜色阶梯//衰减因子 atten 处理---------------------atten = max(0.2, smoothstep(0.3, 0.7, atten));                //处理阴影边缘使其更加锋利//边缘光---------------------------half NdotV = dot(s.Normal, viewDir);half NdVdL = step(_Thickness, dot(NdotV, lightDir));NdVdL = saturate(1-NdVdL);//最终上色half3 col = saturate((((1 + spec) * s.Albedo.rgb + NdVdL)* _LightColor0.rgb * cel) * atten);half4 c = half4(col, s.Alpha);return c;}ENDCG}FallBack "Diffuse"
}

参数:

Shader攻占笔记(四)卡通着色器相关推荐

  1. Unity Toon Shader 卡通着色器(一):卡通着色

    一直对非真实感渲染 (Non-Photorealistic Rendering) 很感兴趣,正好发现某社出的新游戏中可以选择真实质感或卡通质感,所以想试试在 Unity 里实现一下卡通着色器. 卡通渲 ...

  2. Shader攻占笔记(九)结课作业小记

    视频链接:Decryption card 解密卡 因为用到的基本上都是前八周的学习内容,不作赘述,逐一简单说说要求对应的画面呈现吧 1.有山有水有角色 2.有静有动有交互 3.顶点片段不能少(Ligh ...

  3. threejs 源码解析_ThreeJS 物理材质shader源码分析(顶点着色器)

    ThreeJS 物理材质shader源码分析(顶点着色器) Threejs将shader代码分为ShaderLib和ShaderChunk两部分,ShaderLib通过组合ShaderChunk的代码 ...

  4. OpenGL蓝宝书第九章学习笔记:片段着色器和帧缓存

    前言 本篇在讲什么 OpenGL蓝宝书第九章学习笔记之片段着色器和帧缓存 本篇适合什么 适合初学OpenGL的小白 本篇需要什么 对C++语法有简单认知 对OpenGL有简单认知 最好是有OpenGL ...

  5. Shader学习_曲面细分着色器

    曲面细分 详细介绍 当然除了细分与简化之外,还有另外一种同属一类的操作叫做==曲面规则化(Mesh Regularization)==其所作的便是将三角面都变的尽可能相同,从而也达到提升模型效果的目的 ...

  6. opengl 设置每个点的颜色_OpenGL学习笔记(四)着色器

    本文为学习LearnOpenGL的学习笔记,如有书写和理解错误还请大佬扶正: 教程链接: 着色器 - LearnOpenGL CN​learnopengl-cn.github.io 一,基础概念 1, ...

  7. LearnOpenGL学习笔记(四)——着色器类编写

    之前我们将着色器的代码用glsl写好之后,保存为字符串指针,然后用一个函数去编译它,这是一种手段,对于简单的着色器代码可以这样.但当我们针对复杂的着色器,我们发现编写.编译.管理着色器是一件麻烦事.我 ...

  8. Unity 3D 图形学 Shader之顶点与片段着色器(五)

    通过实现一个只有颜色属性可调节的简单材质效果更好的了解顶点与片段着色器 一.顶点着色器 顶点着色器就是处理顶点的着色器,每个顶点都会执行一次顶点着色器.我们先认识下顶点函数的结构: 顶点着色器函数的名 ...

  9. 【OpenGL】笔记三、着色器

    1. 流程 1.1 图形颜色随时间变化 接上回生成矩形,本次我们要对矩形颜色做一定改变,让它在渲染过程中随时间而变化,但是目前来说我们好像无法在着色器源码中获取时间,那么剩下的方法应该就是在程序中对片 ...

最新文章

  1. 使用wsdl2java命令生成webservice本地调用代码
  2. 定制AjaxControlToolkit(1):使一个CalendarExtender对应N个TextBox
  3. 百度Java电面一面面经
  4. 最全整理浏览器兼容性问题与解决方案(转)
  5. 大气压随温度变化表_空气密度随温度的变化
  6. adb 命令获取ip和mac地址
  7. 怎么提高win服务器性能,电脑升级win10后,必做的40项性能优化,值得收藏
  8. 2020-2021 ICPC - Gran Premio de Mexico - Repechaje
  9. 为什么Pod突然就不见了?
  10. 见猎心喜 浅尝辄止 偶有所得 不足为法
  11. SAP-MM知识精解-计划协议-01
  12. 排序方法基本介绍(1)
  13. 在Unity3D中使用Mixamo动画资源
  14. 美国泛达网络:新一代通用型数据中心机柜
  15. python爬虫----简单的抓取斗鱼弹幕
  16. 外夹式超声波流量计工作原理及优缺点
  17. 明亮如星研旅(4)—— Online and offline handwritten Chinese character recognition
  18. 笔记本连接手机热点通过网线给其他电脑联网
  19. X-Forwarded-For注入漏洞实战
  20. pymssql mysql_Python数据库模块pymssql连接SQLServer数据库操作详解

热门文章

  1. Springboot毕设项目电子营业执照系统t4g7g(java+VUE+Mybatis+Maven+Mysql)
  2. charCodeAt()和charAt()的使用及区别
  3. P5.js创意编程之自我介绍
  4. 正则表达式中的贪婪模式
  5. 重庆计算机应用学校,重庆财政学校-计算机应用
  6. 回收站清空数据恢复,原来是这么简单
  7. 回首昨天,继往开来!
  8. 利用svg实现鼠标绘制箭头
  9. jQuery思维框架
  10. “我还是想做医生。”于是这群北大清华学霸开了家烧烤店。