【Shader实战】卡通风格的Shader(一)

写在前面

呜,其实很早就看到了这类Shader,实现方法很多,效果也有些许不一样。从这篇开始,陆续学习一下接触到的卡通类型Shader的编写。

本篇的最后效果如下(只有怪物和苹果部分):

本篇文章里指的卡通效果具有如下特点:

  • 简化了模型中使用的颜色
  • 简化光照,使模型具有明确的明暗区域
  • 在模型边缘部分绘制轮廓(也就是描边)

我们再来回顾一下Unity Surface Shader的pipeline。(来源:Unity Gems

由上图可以看出,我们一共有4个可修改渲染结果的机会(绿色方框中的代码)。在理解这个的基础上,我们来真正学习如何实现上述效果。

简化颜色

在第一步中,我们只实现一个最常见的Bump Diffuse Shader,在这个基础上添加一点其他的技巧来实现简化颜色的目的。Unity的内置Shader也包含了Bump Diffuse Shader,它的作用很简单,就是用一张贴图(也叫法线贴图)记录了模型上的凹凸情况,以此来作为顶点的法线信息,渲染出来的模型也就有了凹凸不平的感觉(详情可见Unity官网)。

基本的Bump Diffuse Shader代码如下:

Shader "Example/Diffuse Bump" {
    Properties {
      _MainTex ("Texture", 2D) = "white" {}
      _BumpMap ("Bumpmap", 2D) = "bump" {}
    }
    SubShader {
      Tags { "RenderType" = "Opaque" }
      CGPROGRAM
      #pragma surface surf Lambert
      struct Input {
        float2 uv_MainTex;
        float2 uv_BumpMap;
      };
      sampler2D _MainTex;
      sampler2D _BumpMap;
      void surf (Input IN, inout SurfaceOutput o) {
        o.Albedo = tex2D (_MainTex, IN.uv_MainTex).rgb;
        o.Normal = UnpackNormal (tex2D (_BumpMap, IN.uv_BumpMap));
      }
      ENDCG
    }
    Fallback "Diffuse"
  }

效果如下:

接下来,我们进行以下步骤:

  1. 在Properties块中添加如下新的属性:

    _Tooniness ("Tooniness", Range(0.1,20)) = 4
  2. 在SubShader块中添加对应的引用:
    float _Tooniness;
  3. 给#pragma添加新的指令final:
    #pragma surface surf Lambert finalcolor:final

    解释:由之前pipeline的图可知,我们有最后一次修改像素的机会,就是使用finalcolor:your function。finalcolor后面紧跟就是我们的函数名,Unity将调用该函数进行最后的修改。其他可供选择的准则可见官网

  4. 实现final函数:
            void final(Input IN, SurfaceOutput o, inout fixed4 color) {
                color = floor(color * _Tooniness)/_Tooniness;
            }

    解释:我们把颜色值乘以_Tooniness,向下取整后再除以_Tooniness。由于color的范围是0到1,乘以_Tooniness再取整将会得到一定范围内的特定整数,这样就使得所有的颜色都被归入到一个已知的集合中,达到了简化颜色的目的。_Tooniness越小,输出的颜色种类越少。

完整代码如下:

Shader "Custom/Toon" {
    Properties {
        _MainTex ("Base (RGB)", 2D) = "white" {}
        _Bump ("Bump", 2D) = "bump" {}
        _Tooniness ("Tooniness", Range(0.1,20)) = 4
    }
    SubShader {
        Tags { "RenderType"="Opaque" }
        LOD 200

        CGPROGRAM
        #pragma surface surf Lambert finalcolor:final

        sampler2D _MainTex;
        sampler2D _Bump;
        float _Tooniness;

        struct Input {
            float2 uv_MainTex;
            float2 uv_Bump;
        };

        void surf (Input IN, inout SurfaceOutput o) {
            half4 c = tex2D (_MainTex, IN.uv_MainTex);
            o.Normal = UnpackNormal( tex2D(_Bump, IN.uv_Bump));
            o.Albedo = c.rgb;
            o.Alpha = c.a;
        }

        void final(Input IN, SurfaceOutput o, inout fixed4 color) {
            color = floor(color * _Tooniness)/_Tooniness;
        }

        ENDCG
    }
    FallBack "Diffuse"
}

效果如下:

卡通光照

除了上述使用取整的方法简化颜色,更常见的是使用一张渐变贴图(ramp texture)来模拟卡通光照达到目的。下图是我们为怪兽使用的渐变贴图(PS里面画的):

这张图的特点就是边界明显,而不像其他渐变图那样是缓慢渐变的。正如卡通风格里面经常有分界明显的明暗变化一样。

我们按如下步骤添加光照函数:

  1. 在Properties块中添加渐变图属性:

    _Ramp ("Ramp Texture", 2D) = "white" {}
  2. 在SubShader块中添加对应的引用:
    sampler2D _Ramp;
  3. 给#pragma添加新的指令:
    #pragma surface surf Toon

    解释:我们去掉了final函数,将其功能移到了后面的surf函数中。这样允许我们有更多的可变性。上述语句说明我们将使用名称为Toon的光照函数。

  4. 修改surf函数:
            void surf (Input IN, inout SurfaceOutput o) {
                half4 c = tex2D (_MainTex, IN.uv_MainTex);
                o.Normal = UnpackNormal( tex2D(_Bump, IN.uv_Bump));
                o.Albedo = (floor(c.rgb * _Tooniness)/_Tooniness);
                o.Alpha = c.a;
            }
  5. 实现Toon光照函数:
            half4 LightingToon(SurfaceOutput s, fixed3 lightDir, half3 viewDir, fixed atten)
            {
            	float difLight = max(0, dot (s.Normal, lightDir));
            	float dif_hLambert = difLight * 0.5 + 0.5; 
    
            	float rimLight = max(0, dot (s.Normal, viewDir));
            	float rim_hLambert = rimLight * 0.5 + 0.5; 
    
            	float3 ramp = tex2D(_Ramp, float2(rim_hLambert, dif_hLambert)).rgb;   
    
     		float4 c;
                    c.rgb = s.Albedo * _LightColor0.rgb * ramp;
                    c.a = s.Alpha;
                    return c;
            }

    解释:上述最重要的部分就是如何在ramp中采样,我们使用了两个值:漫反射光照方向和边缘光照方向。max是为了防止明暗突变的区域产生奇怪的现象,0.5的相关操作则是为了改变光照区间,进一步提高整体亮度。具体可参加之前的文章

完整代码如下:

Shader "Custom/Toon" {
    Properties {
        _MainTex ("Base (RGB)", 2D) = "white" {}
        _Bump ("Bump", 2D) = "bump" {}
        _Ramp ("Ramp Texture", 2D) = "white" {}
        _Tooniness ("Tooniness", Range(0.1,20)) = 4
    }
    SubShader {
        Tags { "RenderType"="Opaque" }
        LOD 200

        CGPROGRAM
        #pragma surface surf Toon

        sampler2D _MainTex;
        sampler2D _Bump;
        sampler2D _Ramp;
        float _Tooniness;
        float _Outline;

        struct Input {
            float2 uv_MainTex;
            float2 uv_Bump;
        };

        void surf (Input IN, inout SurfaceOutput o) {
            half4 c = tex2D (_MainTex, IN.uv_MainTex);
            o.Normal = UnpackNormal( tex2D(_Bump, IN.uv_Bump));
            o.Albedo = (floor(c.rgb * _Tooniness)/_Tooniness);
            o.Alpha = c.a;
        }

        half4 LightingToon(SurfaceOutput s, fixed3 lightDir, half3 viewDir, fixed atten)
        {
            float difLight = max(0, dot (s.Normal, lightDir));
            float dif_hLambert = difLight * 0.5 + 0.5; 

            float rimLight = max(0, dot (s.Normal, viewDir));
            float rim_hLambert = rimLight * 0.5 + 0.5; 

            float3 ramp = tex2D(_Ramp, float2(rim_hLambert, dif_hLambert)).rgb;   

 	    float4 c;
            c.rgb = s.Albedo * _LightColor0.rgb * ramp;
            c.a = s.Alpha;
            return c;
        }

        ENDCG
    }
    FallBack "Diffuse"
}

效果如下:

添加描边

最后,我们给模型添加描边效果。这是通过边缘光照(rim lighting)来实现的,在本例中我们将边缘渲染成黑色来实现描边。边缘光照找到那些和观察方向接近90°的像素,再把他们变成黑色。你大概也想到了边缘光照使用的方法了:点乘。

我们按如下步骤实现:

  1. 首先为描边的宽度在Properties块中添加属性:

    _Outline ("Outline", Range(0,1)) = 0.4
  2. 在SubShader块中添加对应的引用:
    float _Outline;
  3. 前面说了,边缘光照需要使用观察方向,因此我们修改Input结构体:
            struct Input {
                float2 uv_MainTex;
                float2 uv_Bump;
                float3 viewDir;
            };

    解释:viewDir也是Unity的内置参数,其他内置参数可在官网找到。

  4. 我们在surf函数中使用如下方法检测那些边:
                half edge = saturate(dot (o.Normal, normalize(IN.viewDir)));
                edge = edge < _Outline ? edge/4 : 1;
    
                o.Albedo = (floor(c.rgb * _Tooniness)/_Tooniness) * edge;

    解释:我们首先得到该像素的法线方向和观察方向的点乘结果。如果该结果小于我们的阈值,我们认为这就是我们要找的那些边缘点,并除以4(一个实验值)来减少它的值得到黑色;否则,让它等于1,即没有任何效果。

整体代码如下:

Shader "Custom/Toon" {
    Properties {
        _MainTex ("Base (RGB)", 2D) = "white" {}
        _Bump ("Bump", 2D) = "bump" {}
        _Ramp ("Ramp Texture", 2D) = "white" {}
        _Tooniness ("Tooniness", Range(0.1,20)) = 4
        _Outline ("Outline", Range(0,1)) = 0.4
    }
    SubShader {
        Tags { "RenderType"="Opaque" }
        LOD 200

        CGPROGRAM
        #pragma surface surf Toon

        sampler2D _MainTex;
        sampler2D _Bump;
        sampler2D _Ramp;
        float _Tooniness;
        float _Outline;

        struct Input {
            float2 uv_MainTex;
            float2 uv_Bump;
            float3 viewDir;
        };

        void surf (Input IN, inout SurfaceOutput o) {
            half4 c = tex2D (_MainTex, IN.uv_MainTex);
            o.Normal = UnpackNormal( tex2D(_Bump, IN.uv_Bump));

            half edge = saturate(dot (o.Normal, normalize(IN.viewDir)));
			edge = edge < _Outline ? edge/4 : 1;

            o.Albedo = (floor(c.rgb * _Tooniness)/_Tooniness) * edge;
            o.Alpha = c.a;
        }

        half4 LightingToon(SurfaceOutput s, fixed3 lightDir, half3 viewDir, fixed atten)
        {
        	float difLight = max(0, dot (s.Normal, lightDir));
        	float dif_hLambert = difLight * 0.5 + 0.5; 

        	float rimLight = max(0, dot (s.Normal, viewDir));
        	float rim_hLambert = rimLight * 0.5 + 0.5; 

        	float3 ramp = tex2D(_Ramp, float2(rim_hLambert, dif_hLambert)).rgb;   

 			float4 c;
            c.rgb = s.Albedo * _LightColor0.rgb * ramp;
            c.a = s.Alpha;
            return c;
        }

        ENDCG
    }
    FallBack "Diffuse"
}

最后效果如下:

结束语

本篇一开始是参考了Unity Gems的一篇文章,但在学习过程中发现了一面一些错误和改善的地方,例如里面对光照函数的解释,以及渐变贴图的实现。以后的学习还是要多思考,去其糟粕取其精华啊。

在后面的卡通Shader系列,我会首先学习Unity Gems里面用Fragment Shader实现的方法,最后,再学习一下Unity一个资源包里面的卡通效果实现方法。

欢迎交流和指教!

【Shader实战】卡通风格的Shader(一)

时间: 2024-10-05 22:39:51

【Shader实战】卡通风格的Shader(一)的相关文章

【Unity Shader实战】卡通风格的Shader(二)

写在前面 好久没写博客了,一定是因为课程作业比较多,一定不是因为我懒,恩恩. 三个月以前,在一篇讲卡通风格的Shader的最后,我们说到在Surface Shader中实现描边效果的弊端,也就是只对表面平缓的模型有效.这是因为我们是依赖法线和视角的点乘结果来进行描边判断的,因此,对于那些平整的表面,它们的法线通常是一个常量或者会发生突变(例如立方体的每个面),这样就会导致最后的效果并非如我们所愿.如下图所示: 因此,我们有一个更好的方法来实现描边效果,也就是通过两个pass进行渲染--首先渲染对

Unity3d shader之卡通着色Toon Shading

卡通着色的目的是为了让被着色物体显得过渡的不那么好,明暗交界线很明显,等等卡通风格的一系列特征, 也叫Non-photorealisticrendering非真实渲染 重点要做到两点: 1.    描边 2.    着色 另:本片中cg函数均用绿色标明,想了解函数作用和函数内部构成请看这篇文章NVIDIA CG语言 函数之所有数学类函数(Mathematical Functions) 就从最初的描边开始 首先声明变量_Outline挤出描边的粗细_Factor挤出多远 Properties {

【Unity Shaders】Mobile Shader Adjustment —— 为手机定制Shader

本系列主要参考<Unity Shaders and Effects Cookbook>一书(感谢原书作者),同时会加上一点个人理解或拓展. 这里是本书所有的插图.这里是本书所需的代码和资源(当然你也可以从官网下载). ========================================== 分割线 ========================================== 写在前面 在上一篇里,我们学习了一些技巧来初步优化Shader.这次,我们学习更多的技术来实现一个更

Unity3D教程宝典之Shader篇:第一讲Shader总篇

原创文章如需转载请注明:转载自风宇冲Unity3D教程学院 引言,在Unity3d里,所有的图形绘制都必须通过Shader,即着色器.一般的使用过程中,我们用到的都是unity自带的Shader,即build-in shader.学习Shader后,自己写的Shader能做出很多你想要的特殊效果,增强游戏的画面表现.例如 水的倒影. LOGO的光影闪过等等效果等等. Shader的分类 Shader按管线分类一般分为固定渲染管线与可编程渲染管线 1)固定渲染管线 ——这是标准的几何&光照(Tra

Unity shader教程-第二课:Shader的框架和Properties详细介绍

本文首发地址:http://98jy.net/article/17 更多更及时的文章可在上述地址看到 一.Shader的框架 shader由关键字Shader 加上后面的用双引号括起来的字串开始,字串里面可以用上/表示在Inspector中显示出来的分类.整个shader代码都包在这个部分后面的{}中.举例来说,一个典型的shader会是这样 Shader "Name" { } 或者 Shader "Category / Name" { } 二.Shader中的属性

临摹一个像素风格高楼shader

原始效果地址:http://glslsandbox.com/e#40050.0 是一个的城市高楼感的shader,比较像素风 可以拿来做游戏背景,或者基于这个思路做一些别的效果 这个是我后来找的版本,因为最早那个版本没存,所以大体上有些区别. 下面开始分解这个的做法 大体思路是用一种噪波作为因子,可以是sin,cos或者别的.然后用floor把他转成int形.这样生成出来的图案就是方块 最后加一层循环来做到层叠效果 step1.用sin作为因子,加上简单的时间偏移 fixed4 frag (v2

shader forge卡通渲染!

自从用了shader forge,妈妈我再也不写shader了...... 写了3种,分别用的顶点法线.法线贴图.顶点法线+法线贴图,然后还有自发光和受光两种模式,那就是6种了吧... 最后来一张shader forge的node graph:

Unity3D Shader官方教程翻译(十一)----Shader语法:Pass的Blending(混合)

ShaderLab syntax: Blending 混合 Blending is used to make transparent objects. 混合是用来制作透明物体的. When graphics are rendered, after all shaders have executed and all textures have been applied, the pixels are written to the screen. How they are combined with

cocos2d Shader系列1:cocos2d-js Shader和OpenGL ES2.0

cocos2d的Shader也就是差不多直接跟GPU打交道了,跟Flash的Stage3D(AGAL)类似,不过没有AGAL这么恶心,不需要直接编写汇编语言.而Fragment Shader又跟Flash的pixelbender类似. 本文以cocos2d-js为例,但cocos2dx其他版本也是同理的,只是函数名略有不同而已. 当然还是得先复习或者学习一下GPU的原理,至少得知道vertex shader和fragment shader的作用和区别. 详细可以看看大神的说明: http://w