Unity3D Shader系列之描边

Posted WangShade

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Unity3D Shader系列之描边相关的知识,希望对你有一定的参考价值。

目录


1 引言

总结下描边效果的实现方式,主要有以下几种:
①法线外拓+ZTest Always
②法线外拓+Cull Front
③法线外拓+ZWrite Off
④法线外拓+模板测试
⑤基于屏幕后处理

2 顶点沿法线外拓方式

法线外拓的原理如下:
基本原理还是很简单的:模型渲染两次,第一次渲染时将模型的顶点沿法线方向外拓,然后绘制描边颜色,第二次渲染按正常的渲染即可。也就是用第二次渲染去覆盖掉第一次的渲染,由于第二次没有法线外拓,所以只会覆盖掉中间的部分,从而实现描边。
这个过程就像画家绘画样,对于同一个位置,用后画的颜色去覆盖掉先画的颜色。

所以这里最主要的问题在于,如何保证第二个Pass一定能覆盖第一个Pass。以下几种方法都可实现,一般方法②和方法③用得多一点:
方法①第二个Pass开启ZTest Always
方法②第一个Pass使用Cull Front
方法③第一个Pass使用ZWrite Off
方法④使用模板测试

2.1 法线外拓+ZTest Always

2.1.1 代码

要保证第二个Pass一定能覆盖掉第一个Pass,最简单的方法就是让深度测试一直通过,即使用ZTest Always。但是使用ZTest Always问题是非常多的,我们下一节再详说。

伪代码如下:

// 先用描边颜色渲染
Pass

	...
	// 顶点着色器:顶点沿着法线外拓
	v2f vert (appdata v)
    
        v2f o;
		v.vertex.xy += normalize(v.normal) * _OutlineWidth;
		o.vertex = UnityObjectToClipPos(v.vertex);
        return o;
    
	
	// 片元着色器:直接绘制描边颜色
	fixed4 frag (v2f i) : SV_Target
	
		return _OutlineColor;
	


// 再正常渲染
Pass

	// 保证此Pass一定会渲染
	ZTest Always
	// ...

完整代码如下:

Shader "LaoWang/Outline_Example01"

    Properties
    
        _MainTex ("Texture", 2D) = "white" 
		_OutlineWidth ("Outline width", Range(0.01, 4)) = 0.01
		_OutlineColor ("Outline Color", color) = (1.0, 1.0, 1.0, 1.0)
    
    SubShader
    
        Tags  "RenderType"="Opaque" "Queue"="Geometry"
        LOD 100

		Pass
		
			CGPROGRAM

			#pragma vertex vert
            #pragma fragment frag

            #include "UnityCG.cginc"

            struct appdata
            
                float4 vertex : POSITION;
				float3 normal : NORMAL;
            ;

            struct v2f
            
                float4 vertex : SV_POSITION;
            ;

			float _OutlineWidth;
			fixed4 _OutlineColor;

            v2f vert (appdata v)
            
                v2f o;
				v.vertex.xy += normalize(v.normal) * _OutlineWidth;
				o.vertex = UnityObjectToClipPos(v.vertex);
                return o;
            

            fixed4 frag (v2f i) : SV_Target
            
                return _OutlineColor;
            

			ENDCG
		

        Pass
        
        	ZTest Always
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            #include "UnityCG.cginc"
			#include "Lighting.cginc"

            struct appdata
            
                float4 vertex : POSITION;
                float2 uv : TEXCOORD0;
            ;

            struct v2f
            
                float2 uv : TEXCOORD0;
                float4 vertex : SV_POSITION;
            ;

            sampler2D _MainTex;
            float4 _MainTex_ST;

            v2f vert (appdata v)
            
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.uv = TRANSFORM_TEX(v.uv, _MainTex);
                return o;
            

            fixed4 frag (v2f i) : SV_Target
            
                fixed4 color = tex2D(_MainTex, i.uv);
                return fixed4(color.rgb, 1.0);
            
            ENDCG
        
    


2.1.2 问题点

由于第二个Pass使用了ZTest Always,会导致两个问题。
①模型自身会穿透自身
但是我们会发现只有部分网格会穿透自身。为什么只有部分网格会穿透呢?我也没弄清楚。
GPU绘制某一模型时,同一模型中的各个三角面的渲染顺序是如何控制的呢?希望知道的同学解答一下,在此谢过。

②物体将会永远再最前面

所以这种方式基本没人使用。

2.2 法线外拓+Cull Front

2.2.1 代码

原理和上一节类似,只不过不是用ZTest Always来保证第二个Pass覆盖第一个Pass,而是在第一个Pass中使用Cull Front,即第一个Pass只渲染模型的背面,然后让背面向外拓展一下,因为一般背面都在正面的后面(即背面的深度值比正面的深度值大),所以第二个Pass就会覆盖掉中间部分。

Shader "LaoWang/Outline_CullFront"

    Properties
    
        _MainTex ("Texture", 2D) = "white" 
		_OutlineWidth ("Outline width", Range(0.01, 4)) = 0.01
		_OutlineColor ("Outline Color", color) = (1.0, 1.0, 1.0, 1.0)
    
    SubShader
    
        Tags  "RenderType"="Opaque" "Queue"="Geometry"
        LOD 100

		Pass
		
			Cull Front
			CGPROGRAM

			#pragma vertex vert
            #pragma fragment frag

            #include "UnityCG.cginc"

            struct appdata
            
                float4 vertex : POSITION;
				float3 normal : NORMAL;
            ;

            struct v2f
            
                float4 vertex : SV_POSITION;
            ;

			float _OutlineWidth;
			fixed4 _OutlineColor;

            v2f vert (appdata v)
            
                v2f o;
				v.vertex.xy += normalize(v.normal) * _OutlineWidth;
				o.vertex = UnityObjectToClipPos(v.vertex);
                return o;
            

            fixed4 frag (v2f i) : SV_Target
            
                return _OutlineColor;
            

			ENDCG
		

        Pass
        
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            #include "UnityCG.cginc"
			#include "Lighting.cginc"

            struct appdata
            
                float4 vertex : POSITION;
                float2 uv : TEXCOORD0;
            ;

            struct v2f
            
                float2 uv : TEXCOORD0;
                float4 vertex : SV_POSITION;
            ;

            sampler2D _MainTex;
            float4 _MainTex_ST;

            v2f vert (appdata v)
            
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.uv = TRANSFORM_TEX(v.uv, _MainTex);
                return o;
            

            fixed4 frag (v2f i) : SV_Target
            
                fixed4 color = tex2D(_MainTex, i.uv);
                return fixed4(color.rgb, 1.0);
            
            ENDCG
        
    


效果是这样的。

虽然Robot Kyle这个模型使用这种方式效果不好,但是对于其他绝大部分模型还是够用了。
一般的描边就是使用这种方式。

2.2.2 改进点

①无论相机距离物体多远或者观察视角的变化,都让描边的宽度保持等比例。
如图。

出现这样问题的原因在于我们是在模型空间对顶点进行外拓的,外拓的距离是一样的。但是由于是透视相机,模型上离相机近的地方描边效果较粗,而远的地方描边效果较细。
解决这个问题的方法是,我们不在模型空间外拓,而在齐次裁剪空间将顶点沿法线方向进行外拓。
而这个方法最大的问题在于,如何才能求到齐次裁剪空间中的法线方向?
顶点从模型空间变换到齐次裁剪空间的变换矩阵是MVP,那法线的变换能否直接使用MVP矩阵呢?答案是不行。法线的变换应该是变换矩阵的逆转置矩阵,即我们这里将使用(MVP)-1T来进行法线变换。
为什么法线变换不能直接使用变换矩阵,而要使用逆转置矩阵呢?主要是为了保证存在非等比缩放时,变换后的法线方向依然是垂直与表面的。如果不存在非等比缩放,即只存在旋转,那么法线的变换是可以直接使用变换矩阵的。(具体描述详见《Unity Shader入门精要》4.7节 法线变换)

那MVP矩阵是只有旋转吗?不是的。P矩阵即从观察空间到齐次裁剪空间的变换矩阵一定是存在非等比缩放的。所以,我们这里需要用到MVP的逆转置矩阵。

回到最初的问题,MVP的逆转置矩阵该怎么求?很遗憾Unity的Shader中并没有直接提供相应的变量,要真正得到这个逆转置矩阵需要从C#端计算然后传递到shader。但其实我们并不需要那么高的精度,近似即可。有两种近似方式。
一是直接使用MVP矩阵来近似。

v2f vert (appdata v)

	o.vertex = UnityObjectToClipPos(v.vertex);
	float3 clipNormal = mul((float3x3) UNITY_MATRIX_VP, mul((float3x3) UNITY_MATRIX_M, v.normal));
	o.vertex.xy += normalize(clipNormal).xy * _OutlineWidth;

二是使用(MV)的逆转置矩阵* P来近似。为什么使用MV的逆转置矩阵呢,是因为Unity刚好提供了这个变量,UNITY_MATRIX_IT_MV。

v2f vert (appdata v)

	o.vertex = UnityObjectToClipPos(v.vertex);
	float3 viewNormal = mul((float3x3)UNITY_MATRIX_IT_MV, v.normal);
	float2 clipNormal = mul((float2x2)UNITY_MATRIX_P, viewNormal.xy);
	o.vertex.xy += normalize(clipNormal) * _OutlineWidth;

效果对比如下,可以看到上面两种方式的效果其实差不多。

2.3 法线外拓+ZWrite Off

2.3.1 代码

逻辑也很简单,第一个Pass由于关闭了深度写入,那么第二个Pass肯定能够通过深度测试,所以第二个Pass会覆盖掉第一个Pass。

Shader "LaoWang/Outline_ZWriteOff"

    Properties
    
        _MainTex ("Texture", 2D) = "white" 
		_OutlineWidth ("Outline width", Range(0.01, 4)) = 0.01
		_OutlineColor ("Outline Color", color) = (1.0, 1.0, 1.0, 1.0)
    
    SubShader
    
        Tags  "RenderType"="Opaque" "Queue"="Geometry"
        LOD 100

		Pass
		
			ZWrite Off

			CGPROGRAM

			#pragma vertex vert
            #pragma fragment frag

            #include "UnityCG.cginc"

            struct appdata
            
                float4 vertex : POSITION;
				float3 normal : NORMAL;
            ;

            struct v2f
            
                float4 vertex : SV_POSITION;
            ;

			float _OutlineWidth;
			fixed4 _OutlineColor;

            v2f vert (appdata v)
            
                v2f o;
				//v.vertex.xy += normalize(v.normal) * _OutlineWidth;
				//o.vertex = UnityObjectToClipPos(v.vertex);

				o.vertex = UnityObjectToClipPos(v.vertex);
				float3 clipNormal = mul((float3x3) UNITY_MATRIX_VP, mul((float3x3) UNITY_MATRIX_M, v.normal));
				o.vertex.xy += normalize(clipNormal).xy * _OutlineWidth;

				//o.vertex = UnityObjectToClipPos(v.vertex);
				//float3 viewNormal = mul((float3x3)UNITY_MATRIX_IT_MV, v.normal);
				//float2 clipNormal = mul((float2x2)UNITY_MATRIX_P, viewNormal.xy);
				//o.vertex.xy += normalize(clipNormal) * _OutlineWidth;
                return o;
            

            fixed4 frag (v2f i) : SV_Target
            
                return _OutlineColor;
            

			ENDCG
		

        Pass
        
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            #include "UnityCG.cginc"
			#include "Lighting.cginc"

            struct appdata
            
                float4 vertex : POSITION;
                float2 uv : TEXCOORD0;
            ;

            struct v2f
            
                float2 uv : TEXCOORD0;
                float4 vertex : SV_POSITION;
            ;

            sampler2D _MainTex;
            float4 _MainTex_ST;

            v2f vert (appdata v)
            
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.uv = TRANSFORM_TEX(v.uv, _MainTex);
                return o;
            

            fixed4 frag (v2f i) : SV_Target
            
                fixed4 color = tex2D(_MainTex, i.uv);
                return fixed4(color.rgb, 1.0);
            
            ENDCG
        
    


效果如下,可以看到Robot Kyle这个模型使用这种方式效果是最好的。

2.3.2 问题点

ZWrite Off关闭后会有两个问题。具体如下。
我们在场景中加上一个Ground,新建一个标准的材质球。

然后我们就会发现有地板的部分描边就消失了。

我们从Frame Debugger中可以看出地板是最后绘制的。而绘制描边时没有开启深度写入,这就导致地板的深度测试会通过。所以地板的颜色会覆盖掉描边部分的颜色。

要解决这个问题,其实也很简单,最后渲染我们的模型就行了。用什么方法控制我们的模型最后渲染呢?当然是控制渲染队列啦,这点我们在《Unity3D Shader系列之透视效果XRay》中讲过,就不再多说了。
我们将渲染队列设置为“Geometry+1”,即在所有不透明物体渲染后再渲染我们的模型。

调整之后,描边效果就正常了。
但是这样调整之后依然还有问题,比如我们再复制一个描边模型,然后一个在前一个在后。此时,我们将会发现两个模型重叠的部分没有描边了。

同样,我们去Frame Debugger中看看原因。

从上图我们可以看到,是先绘制的前面的物体,再绘制后面的物体,就导致绘制后面物体时将前面物体的描边给覆盖掉了。
要解决这个问题,我们得有一个储备知识:Unity在渲染不透明物体时,如果这两个物体的渲染队列一样(Render Queue的值一样),则按距离摄像机由近到远的顺序依次渲染。在渲染半透明物体时,如果这两个物体的渲染队列一样(Render Queue的值一样),则按距离摄像机由远到近的顺序依次渲染。
为什么要这样做?对于不透明物体,先渲染近的再渲染远的,由于硬件的Early-Z等技术,可以减少Over Draw。对于半透明物体,由于需要关闭深度写入,所以必须先渲染远的再渲染近的,这样才能保证混合后的颜色是正确的。
所以我们这里要想让两个模型重叠的部分也能绘制出描边效果,就得先渲染后面的再渲染前面的,那把渲染队列改为Transparent就可以了。但是这个办法也不是完全能解决问题的,因为我们上面给出的距离摄像机的远近其实很模糊,这个远近到底是取哪个值?是取物体的世界坐标与相机的距离呢还是物体的某个顶点距离相机的距离呢?Unity官方也没给出说明。

2.4 法线外拓+模板测试

先正常渲染物体,将模板缓冲区写为1。然后再法线外拓进行描边,当模板缓冲区值为0时绘制描边。
伪代码。

Pass

	// 将模板缓冲区写为1
	Stencil
	
		Ref 1
		Comp Always
		Pass Replace
	

	// 正常渲染
	...


Pass

	Stencil
	
		Ref 0
		Comp Equal
	
	ZWrite Off

	// 渲染描边
	// 顶点着色器法线外拓
	...


完整代码。

Shader "LaoWang/Outline_StencilTest"

    Properties
    
        _MainTex ("Texture", 2D) = "white" 
		_OutlineWidth ("Outline width", Range(0.01, 4)) = 0.01
		_OutlineColor ("Outline Color", color) = (1.0, 1.0, 1.0, 1.0)
    
    SubShader
    
        Tags  "RenderType"="Opaque" "Queue"="Geometry+1"
        LOD 100

		Pass
        
			Stencil
			
				Ref 1
				Comp Always
				Pass Replace
			

            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            #include "UnityCG.cginc"
			#include "Lighting.cginc"

            struct appdata
            
                float4 vertex : POSITION;
                float2 uv : TEXCOORD0;
            ;

            struct v2f
            
                float2 uv : TEXCOORD0;
                float4 vertex : SV_POSITION;
            ;

            sampler2D _MainTex;
            float4 _MainTex_ST;

            v2f vert (appdata v)
            
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.uv = TRANSFORM_TEX(v.uv, _MainTex);
                return o;
            

            fixed4 frag (v2f i) : SV_Target
            
                fixed4 color = tex2D(_MainTex, i.uv);
                return fixed4(color.rgb, 1.0);
            
            ENDCG
        

		Pass
		
			Stencil
			
				Ref 0
				Comp Equal
			

			ZWrite Off

			CGPROGRAM

			#pragma vertex vert
            #pragma fragment frag

            #include "UnityCG.cginc"

            struct appdata
            
                float4 vertex : POSITION;
				float3 normal : NORMAL;
            ;

            struct v2f
            
                float4 vertex : SV_POSITION;
            ;

			float _OutlineWidth;
			fixed4 _OutlineColor;

            v2f vert (appdata v)
            
                v2f o;
				//v.vertex.xy += normalize(v.normal) * _OutlineWidth;
				//o.vertex = UnityObjectToClipPos(v.vertex);

				o.vertex = UnityObjectToClipPos(v.vertex);
				float3 clipNormal = mul((float3x3) UNITY_MATRIX_VP, mul((float3x3) UNITY_MATRIX_M, v.normal));
				o.vertex.xy += normalize(clipNormal).xy * _OutlineWidth;

				//o.vertex = UnityObjectToClipPos(v.vertex);
				//float3 viewNormal = mul((float3x3)UNITY_MATRIX_IT_MV, v.normal);
				//float2 clipNormal = mul((float2x2)UNITY_MATRIX_P, viewNormal.xy);
				//o.vertex.xy += normalize(clipNormal) * _OutlineWidth;
                return o;
            

            fixed4 frag (v2f i) : SV_Target
            
                return _OutlineColor;
            

			ENDCG
		
    


效果如下。

这种方式也会有两个模型重叠部分没有描边的问题,但是由于使用的是模板测试,这个问题是无解的了。

2.5 法线外拓实现描边的问题

使用法线外拓实现描边都存在下图这样的问题,即法线不是连续的时候,描边就会中断。

要解决这个问题需要写一个工具将顶点的法线平滑一下,并将其保存在顶点的颜色数据中,然后在外拓时使用平滑后的法线来外拓。
可以参考这篇文章,里面实现了法线平滑工具。

3 屏幕后处理的方式

使用屏幕后处理实现描边一般有两种方式,一是使用Unity中Camera的着色器替代技术,二是使用Render Command。

3.1 使用Camera的着色器替代技术

这种方式我没有去具体实现,但是去了解了下,这里总结下大概的思路。

3.1.1 着色器替代技术

什么是Camera的着色器替代技术?
说起来高大上,其实本质的东西并不复杂,就是用相机重新渲染一遍场景,但是本次渲染的过程中,场景中的物体(不一定是全部物体,我们可以用代码控制只渲染某一部分物体)不再使用它自身的Shader进行着色,而是使用特定的Shader(所有要渲染的物体都是用同一个Shader)来着色。
从代码上来说,就是下面Camera类中的两个方法。

public void RenderWithShader(Shader shader, string replacementTag);
public void SetReplacementShader(Shader shader, string replacementTag);

RenderWithShader只有调用时的那一帧有效。
SetReplacementShader是调用之后Camera渲染都一直使用指定的Shader,直到代码主动调用ResetReplacementShader方法。

public void ResetReplacementShader();

说一下两个方法的参数。
第一个参数为相机渲染时将使用的Shader。
第二参数为相机

二次元卡通渲染之描边

前言

本文为“优梦创客”原创文章,您可以自由转载,但必须加入完整的版权声明

更多学习资源请加QQ:1517069595获取(企业级性能优化/热更新/Shader特效/服务器/商业项目实战/每周直播/一对一指导)

点赞、关注、分享可免费获得配套学习资源

完整视频可以点击B站链接:点击观看完整视频

请添加图片描述

各位同学大家好!欢迎大家来到我们的优梦创客,今天这节课要给大家分享的是《泛二次元卡通渲染》的技术原理与实现

大家知道,近几年加入二次元游戏的玩家越来越多,这跟游戏玩家的群体主力都是年轻人有很大关系,今天来到咱们课堂的相信有很多都是

90后或者00后的同学,同学们都是看着二次元动漫长大的,自然对二次元文化的游戏接受度更高,那么很显然手机游戏厂商也察觉到了这个

趋势,所以近几年来越来越多的手游研发公司都转向了开发二次元游戏,这其中知名度最高的无外乎“米哈游”这家公司,它的《原神》、
《崩坏》系列一直都是二次元的经典之作,当然还有其他厂商的像《阴阳师》、《明日方舟》,也都是一些非常流行的二次元手游佳作。

那么今天我要分享给大家的就是如何实现二次元的风格化渲染效果,以及对于已经在职的或者在开发独立游戏的小伙伴们,如何将这项渲染技术应用在自己的项目中。
请添加图片描述

主题

请添加图片描述

  • 二次元卡通渲染技术概览
  • 二次元卡通渲染的实现
  • 商业项目中二次元卡通渲染的重点和难点问题详解

渲染体系

请添加图片描述
二次元卡通渲染实际上是二次元卡通渲染的一个分支,而二次元卡通渲染又是风格化渲染的一个分支。

当我们打算立项制作一款游戏的时候,首先我们的美术团队就要确定整体的画面风格,偏写实还是偏风格化,偏风格化的话,那么风格化的程度如何

并且,风格化渲染又有很多种分类,常见于:水墨风、铅笔风、卡通风,卡通风格又可以分成美式动漫风格和日式二次元风格,我们平时所说的二次元卡通渲染指的就是这种日式渲染风格。

下面我们来看一看,如果我们打算立项一个二次元风格的游戏,那么在商业项目的开发中要经历几个步骤呢?

二次元卡通渲染得分类

请添加图片描述
美式卡通风格在色彩上比较连续,有渐变色,着色风格很大程度上依赖于艺术家定义的色调(tone),而在阴影和高光方面常常采取夸张和
变形的做法,比较典型的是《军团要塞2》日式卡通风格往往角色造型更写实,但在着色方面,则趋向于大片大片纯色色块,并有的明暗交
界,例如《崩坏3》。虽然这样的分类并没有清晰界限,但易于描述,接下来我们就按照美式卡通和日式卡通的分类,从光影和描边两个维度上分别列举各类技术实现。

二次元卡通渲染的步骤

请添加图片描述
那么二次元卡通渲染总体的步骤在图形学当中基本是没有什么变化的。

就是第1步先描边,第2步再着色。当然具体到描边和着色的技巧,那么这么多年就会有蛮多的演变了。

描边:轮廓的图形学定义

请添加图片描述
下面我们首先来看一下二次元卡通渲染中描边的图形学定义
大家看上面这幅图,这幅图是图形学领域的经典之作《Real time rendering》中定义的轮廓边的概念。

  • B: boundary,边界
  • C: crease,接缝
  • M:materal,外表
  • S:silhouette,外轮廓

日式卡通在描边上的特点

请添加图片描述
在二次元游戏中,我们一般需要同时绘制物体表面的外轮廓与内部轮廓,并且要灵活控制轮廓的宽度

大家看上面图中轮廓线没有变化与有变化的情况,显然角色的轮廓表现是需要有粗细变化之分的。
我们一般会在绘制角色的外轮廓时,把轮廓线描绘的粗一些,在绘制角色的转折位置时,把轮廓线描绘的粗一些,而面部的身体的一些细节位置会绘制的细一些。

通常可以把调节任务交给美工去调节,而我们ta只需要提供这样的可以自由调节粗细的美素工具即可。这也是ta所必备的技能之一,就是掌握运用C sharp脚本去开发支撑美术和程序工作流的一些工具,所以我们说ta是连接美术与程序的桥梁。

在这里再给大家科普一下ta岗位的职责,ta的职责主要是用技术的方式去展现美术效果,当当然也包括上面讲的,为了实现这些美术效果所必须提供给美术和程序使用的一些工具的开发。那么所有这些ta岗位所必须掌握的技能都会包括在我们的unity小白的ta之路课程中。

描边:轮廓的计算

请添加图片描述

  • 基于视角
  • 基于几何生成的方法
  • 基于图像处理的描边

基于视角

请添加图片描述
原理:

  • 利用模型法线向量和视向量的夹角(参考系统课程)
  • ·夹角越接近垂直,说明离轮廓线越近

缺点:

  • ·这种方法渲染的轮廓线宽度不均,实际应用不多

基于几何生成方法

请添加图片描述
原理:

  • 双pass渲染
  • 第一个pass渲染物体正面
  • 第二个pass渲染物体背面,并使轮廓可见

优点:

  • 所有顶点的处理独立
  • 渲染速度很快
  • 线条宽度可控

请添加图片描述
缺点1解决方案:

  • 方案1:强迫同一个位置的顶点具有相同的法线朝向》工具 & 美术模型工作流
  • 方案2:另一种解决方法是在这些轮廓处创建额外的网格结构(提升网格的密度);由于gap大多在模型放大的情况出现,根据到Camera- 的距离控制轮廓线的宽度,一定程度上可以减少gap的出现
  • 方案3:z-bias的方法,也是绘制背面,但不膨胀,而是把背面顶点的Z值稍微向前偏移一点点,使得背面的些许部分显示出来形成描边效果

缺点2解决方案:

  • 方案1:给backfaces设置Z-offset,使轮廓线埋没到临近的面里。
  • 方案2:修改backfaces扩张的法线,使轮廓线扁平化。

基于图像处理的描边

请添加图片描述
这类方法的实现可以说更接近于“边缘”这一概念的本质定义,什么是“边缘”呢?

边缘就是在深度或者法线上不连续的位置。因此为了获取边缘,我们只需要在图片上找到深度或者法线不连续的位置即可

因此,我们需要将深度信息和法线信息以贴图的形式传入,运用边缘检测算法去寻找这些像素

这类方法的优点是描边的线宽一致,缺点是需要额外的法线和深度信息

当然,由于近年来流行的延迟渲染框架,法线和深度本来就是G-Buffer的一部分,因此往往不需要额外绘制法线和深度的信息

Unity中的渲染路径:延迟渲染路径

请添加图片描述
G-Buffer:指Geometry Buffer,亦即“几何缓冲”。区别于普通的仅将颜色渲染到纹理中,G-Buffer指包含颜色、法线、世界空间坐标的缓冲区,亦即指包含颜色、法线、世界空间坐标的纹理。

可以看出,延迟渲染使用的Pass数目通常就是两个,这跟场景中包含的光源数目是没有关系的。换句话说,延迟渲染的效率不依赖于场景的复杂度,而是和我们使用的屏幕空间的大小有关。这是因为,我们需要的信息都存储在缓冲区中,而这些缓冲区可以理解成是一张张2D图像,我们的计算实际上就是在这些图像空间中进行的。

对于延迟渲染路径来说,它最适合在场景中光源数目很多、如果使用前向渲染会造成性能瓶颈的情况下使用。而且,延迟渲染路径中的每个光源都可以按逐像素的方式处理。但是,延迟渲染也有一些缺点。

美式卡通在描边上的特点(略谈)

请添加图片描述
美式卡通中往往倾向于使用基于图像处理的描边方法来生成均匀一致的描边效果。

在《英雄联盟》中小兵和英雄的勾边效果就是用Sobel算子对深度信息进行边缘检测来获得的。

由于游戏中只需要针对小兵和英雄勾边而不需要对场景地图进行勾边,因此在LOL中,勾边的计算并非全屏后处理,而是逐物体进行的,这

样的好处是可以随意控制哪些物体描边,每个物体可以单独指定描边颜色,缺点是当物体较多时(尤其是skinned mesh较多时)计算量会增大。

一个折衷的方案是,在进行正常绘制的阶段用stencil buffer标记出需要描边的物体,然后用一个全屏的后处理,对stencil buffer标记的像素进行边缘检测,当然这样的话,就很难给每个物体单独指定描边颜色了。

实际上,在LOL中有两种类型的描边,一种是小兵和英雄的固定描边,另一种是防御塔发出攻击警报或者某个单位被点选时才产生的红色描边,这两种描边在处理上略有差别,前者直接使用边缘检测的结果作为最终描边,而后者则是对边缘检测结果再进行一次模糊,借此来扩大和柔化描边效果。

日式卡通在描边上的特点

请添加图片描述
日式卡通中往往倾向于使用基于几何体生成的方法去描边,这类描边方法相较于另两类方法的好处在于线宽更容易为美术所控制,而在日式卡通中,往往需要粗细有变化的描边去体现角色不同部位的特征,例如在《GUILTY GEAR Xrd》(罪恶装备)中,角色的描边就是通过几何体生成的方法,并引入了逐物体的顶点色来控制描边细节,同时也是为了保证描边粗细不会随着摄像机视距发生变化。

效果展示:断边处理

请添加图片描述

描边适配视距(适配前)

请添加图片描述

描边适配视距(适配后)

请添加图片描述

性能优化

请添加图片描述
性能优化视频传送门

描边粗细控制

请添加图片描述
描边粗细控制视频传送门

思考:

请添加图片描述

  • 如何刷顶点色?
  • 如何合理分配顶点色储存的数据内容?
  • 如何让根据视距调整勾边粗细

如何学习:

请添加图片描述

  • 理解总体美术目标
  • 分析美术需求,形成Shader开发思路
  • Shader实现
  • 效果调整
  • 商业项目中的TA技巧与面试经验

更多学习资源请加QQ:1517069595获取(企业级性能优化/热更新/Shader特效/服务器/商业项目实战/每周直播/一对一指导)

点赞、关注、分享可免费获得教程配套学习资源。

完整视频可以点击B站链接:点击观看完整视频

更多知识、教程、源码请进↓↓↓
优梦创客工坊

精品内容推送请搜索↓↓↓
微信公众号:优梦创客

免费直播、VIP视频请进↓↓↓
优梦创客课堂

游戏开发交流群↓↓↓
游戏开发交流群

以上是关于Unity3D Shader系列之描边的主要内容,如果未能解决你的问题,请参考以下文章

Unity3D shader专题

二次元卡通渲染之描边

转 猫都能学会的Unity3D Shader入门指南

猫都能学会的Unity3D Shader入门指南

Unity3D Shader系列之渲染流水线

浅墨Unity3D Shader编程之十三 单色透明Shader & 标准镜面高光Shader