IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 游戏开发 -> Shader攻占笔记(四)卡通着色器 -> 正文阅读

[游戏开发]Shader攻占笔记(四)卡通着色器

卡通光照

课本上介绍的卡通着色器有使用坡度图(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-1
            half 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 200
        CGPROGRAM
        #pragma surface surf MyLighting
        #pragma target 3.0

        sampler2D _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-1
            half 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 剔除面向视点的部分,再将原模型渲染一次盖住中间部分。

float3 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 Front
            ZWrite On
            CGPROGRAM
            #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 direction
                float3 dirN = v.normal;                      //Normal direction
                float3 dir = lerp(dirV, dirN, _Factor);      //Adjust _Factor depends on the actural model
                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;
                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_Toon2"
{
    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, 1)) = 0.1
        _SpecTex("高光贴图", 2D) = "white"{}
        _SpecPower("高光密度" , Range(0, 0.7)) = 0.2
        _SpecPower2("高光范围" , Range(0.4, 1)) = 0.5
    }
    SubShader
    {
        Tags { "RenderType"="Opaque"  }
        LOD 200
        CGPROGRAM
        #pragma surface surf MyLighting
        #pragma target 3.0

        sampler2D _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));

            //根据法线和视角刻画描边范围
            half NdotV = dot(o.Normal, IN.viewDir);
            NdotV = pow(saturate(1- NdotV), 5);
            NdotV = step((1-_Thickness), NdotV);//outline range
            
            //根据范围上色
            o.Albedo = lerp(c.rgb, _OutlineColor * _LightColor0.rgb, NdotV);
            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-1
            half cel =max(0.2, floor(NdotL * _LightVal) / _LightVal);    //颜色阶梯

            //衰减因子处理---------------------
            atten = max(0.2, smoothstep(0.3, 0.7, atten));                //处理阴影边缘使其更加锋利
            
            half3 col = saturate(((1 + spec) * s.Albedo.rgb* _LightColor0.rgb * cel) * atten);
            half4 c = half4(col, s.Alpha);
            return c;
        }
        ENDCG
    }
    FallBack "Diffuse"
}

参数:
在这里插入图片描述

后附:由于描边与光照角度并没有关系,所以在旋转的时候并不会变化,背光的情况下有穿帮风险(如下)。以后有时间的时候再改进叭。

在这里插入图片描述

  游戏开发 最新文章
6、英飞凌-AURIX-TC3XX: PWM实验之使用 GT
泛型自动装箱
CubeMax添加Rtthread操作系统 组件STM32F10
python多线程编程:如何优雅地关闭线程
数据类型隐式转换导致的阻塞
WebAPi实现多文件上传,并附带参数
from origin ‘null‘ has been blocked by
UE4 蓝图调用C++函数(附带项目工程)
Unity学习笔记(一)结构体的简单理解与应用
【Memory As a Programming Concept in C a
上一篇文章           查看所有文章
加:2021-10-07 14:07:54  更:2021-10-07 14:09:28 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/28 2:28:43-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码