【Shader笔记】【NPR】卡通渲染-轮廓线的渲染

写在前面

本文借鉴大佬的《【NPR】漫谈轮廓线的渲染》
地址:https://blog.csdn.net/candycat1992/article/details/45577749

【NPR】轮廓线的渲染

Surface Angle Silhouette

利用viewpoint和surface normal的点乘结果得到轮廓线信息,结果越接近0,说明离轮廓线越近。在实际应用中,我们通常使用一张一维纹理来模拟,即使用视角方向和顶点法向的点乘对该纹理进行采样。使用了两种方法实现这种技术:

  • 一种是使用一个参数_Outline来控制轮廓线的宽度

  • 另一种方式是使用了一张一维纹理来控制
    在这里插入图片描述

参数&纹理控制

使用纹理为:
在这里插入图片描述
使用纹理控制的轮廓效果很难控制,有的地方轮廓很宽,有些地方又捕捉不到。
这种方法的优点在于简单快速,可以在一个Pass里得到结果,而且还可以使用texture filtering对轮廓线进行抗锯齿。
不过也有很多的局限性,比如只适用于某些模型,对于像cube这样的模型就会有问题。虽然我们可以使用变量来控制轮廓线的宽度(如果使用纹理的话就是纹理中黑色的宽度),但实际的效果是依赖于表面的曲率(curvature)的。对于像cube这样表面非常平坦的物体,它的轮廓线回发生突变,要么没有,要么全黑。
在这里插入图片描述

Shader "NPR/Tex_Surface Angle Sihouetting"
{
    Properties
    {	
        _MainTex ("Base(RGB)", 2D) = "white" {}
		_Outline("Outline",Range(0,1))=0.4
		_SilhouetteTex("Silhouette Texture",2D)="white"{} //一维纹理
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" }
        LOD 200

        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

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

			sampler2D _MainTex;
			float _Outline;
			sampler2D _SilhouetteTex;

            struct v2f
            {
				float4 pos : SV_POSITION;
                float2 uv : TEXCOORD0;
				float3 worldLightDir:TEXCOORD1;
				float3 worldNormal:TEXCOORD2;
				float3 worldViewDir:TEXCOORD3;
            };
            v2f vert (appdata_full v) //appdata_full:包含位置、法线、切线、顶点色和两个纹理坐标
            {
                v2f o;
                o.pos = UnityObjectToClipPos(v.vertex);
				o.uv=v.texcoord;

				o.worldLightDir = UnityWorldSpaceLightDir(v.vertex);
				o.worldNormal = UnityObjectToWorldNormal(v.normal);
				o.worldViewDir = UnityWorldSpaceViewDir(v.vertex);

				TRANSFER_VERTEX_TO_FRAGMENT(o);  //宏,在顶点着色器中计算上一步中声明的阴影纹理坐标
				//TRANSFER_SHADOW(o); 好像现在都是这样了
                return o;
            }

			//用view和normal点乘计算轮廓线的函数
			fixed3 GetSilhouetteUseConstant(fixed3 normal , fixed3 vierDir)
			{
			//saturate将数控制在【0,1】内,大于1为1,小于0为0,0-1则为本身
			fixed edge = saturate ( dot (normal ,vierDir)); 
			edge =edge < _Outline ? edge/4 :1;  
			return fixed3 (edge ,edge,edge);
			}
			//使用view和normal的点乘,对一维纹理进行采样的函数
			fixed3 GetSilhouetteUseTexture(fixed3 normal , fixed3 vierDir)
			{
			fixed edge = dot(normal ,vierDir);
			edge = edge *0.5 +0.5;
			return tex2D(_SilhouetteTex , fixed2(edge,edge)).rgb;
			}

            fixed4 frag (v2f i) : SV_Target
            {
				fixed3 worldNormal = normalize(i.worldNormal);
				fixed3 worldLightDir = normalize(i.worldLightDir);
				fixed3 worldViewDir = normalize(i.worldViewDir);
				fixed3 col = tex2D(_MainTex , i.uv).rgb;

				//Use a constant to render Silhouette使用参数来控制轮廓线的宽度
				//fixed3 silhouetteColor = GetSilhouetteUseConstant (worldNormal ,worldViewDir);

				//Or use a one dime silhouette Texture使用了一维纹理来控制轮廓线
				fixed3 silhouetteColor = GetSilhouetteUseTexture( worldNormal ,worldViewDir);

				fixed4 fragColor;
				fragColor.rgb = col * silhouetteColor ;
				fragColor.a =1.0;  // 等于return fixed4(fragColor , 1.0)吧

				return fragColor;
            }
            ENDCG
        }
    }
}

Procedural Geometry Silhouette

这种方法的核心是用两个Pass渲染:

  • 第一个Pass中正常渲染frontfaces
  • 第二个Pass中再渲染backfaces,并使用某些技术来让它的轮廓可见。
    当然,渲染背面的方法有很多,比如shell or halo method,即沿着法线方向移动backfaces中的顶点
    下面列举一些其他渲染背面的方法:
  • 只渲染backfaces的edges(可以理解把渲染模式设置为DRAW_EDGE),然后使用一些biasing等技术来保证这些线会在frontfaces的前面渲染。
  • Z-bias方法。把backfaces渲染成黑色,然后在屏幕空间的z方向上向前移动它们,使其可见。移动的距离可以是一个固定值,或其他适应后的值。
    缺点:不能创建宽度相同的轮廓,因为frontface和backface的夹角不一样,可控性很弱。
  • Triangle Fatting。把每个backface triangle的edges都“变胖”一定程度,使其在视角空间中看起来宽度是一致的。
    缺点:对于一些瘦长的triangles(三角形)来说,它的corner(角)也会变得很细长,一种解决的方法就是把拓展后的edges链接在一起形成斜接在一起的corners。
    而且这种方法也无法应用在GPU生成的一些curved surfaces上(因为是弯曲的面,没有edges)
    在这里插入图片描述
  • Shell or halo method,把backface的顶点沿着顶点法向向外扩张。
    优点:很快速,可以在vertex shader中就完成,而且具有一定的健壮性。不需要任何关于相邻顶点/边等信息,所有的处理都是独立的,因此从速度上来说很快。
    缺点:像cube这样的模型,它的同一个顶点在不同面上具有不同的顶点法向,所以向外扩张后会形成一个gaps(间隙),一种解决方法是,强迫同一个位置的顶点具有相同的法向。另一个方法是在这些轮廓处创建额外的网格结构。

上面所列出的所有方法都有一个共同的缺点,那就是对轮廓线的外观可控性很少,而且如果没有进行一些反锯齿操作,轮廓线看起来锯齿比较严重。

z-bias和vertex normal的方法

有两个地方需要注意

  • 首先对顶点只会在xy方向上扩张,这种操作在模型全是外凸的情况下基本没有什么问题,但是一个模型有内凹的部分就有可能会出现轮廓线挡住frontfaces的情况;
  • 另一点是,在扩张顶点时考虑了顶点在投影矩阵中的深度值,这意味着模型轮廓线的宽度会随摄像机移动而改变
// Upgrade NOTE: replaced 'mul(UNITY_MATRIX_MVP,*)' with 'UnityObjectToClipPos(*)'

Shader "NPR/Procedural Geometry Silhouette"
{
    Properties
    {
        _MainTex ("Base(RGB)", 2D) = "white" {}
		_Outline("Outline",Range(0,1))=0.1
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" }
        LOD 200

		Pass
        {
			Tags{"LightMode" = "ForwardBase"}
			Cull Back
			Lighting On

            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

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

			 sampler2D _MainTex;

            struct v2f
            {
                float2 uv : TEXCOORD0;
                float4 pos : SV_POSITION;
            };

            v2f vert (appdata_full v) //appdata_full:包含位置、法线、切线、顶点色和两个纹理坐标
            {
                v2f o;
                o.pos = UnityObjectToClipPos(v.vertex);
                o.uv = v.texcoord;

				TRANSFER_VERTEX_TO_FRAGMENT(o);
                return o;
            }

            fixed4 frag (v2f i) : COLOR
            {
				fixed3 col = tex2D(_MainTex ,i.uv).rgb;

				fixed4 fragColor;
				fragColor.rgb =col;
				fragColor.a = 1.0;

                return fragColor;
            }
            ENDCG
        }

        Pass
        {
			Tags{"LightMode" = "ForwardBase"}
			Cull Front
			Lighting Off

            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

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

			 sampler2D _MainTex;
			 float _Outline;

            struct v2f
            {
                float2 uv : TEXCOORD0;
                float4 pos : SV_POSITION;
            };

			void ZBiaMethod(appdata_full i , inout v2f o) //顶点沿着法线方向偏移方法一
			{
				//简单粗暴,直接与参数_Outline相加
				float4 viewPos = mul(UNITY_MATRIX_MV , i.vertex);
				viewPos.z +=_Outline;

				o.pos = mul(UNITY_MATRIX_P , viewPos);
			}

			void VertexNormalMethod0(appdata_full i , inout v2f o) //顶点沿着法线方向偏移方法二
			{	
				//将法线变换到视角空间后,再计算offset的程度,之后与顶点相加
				//缺点:如果直接使用顶点法线进行拓展,对于一些内凹的模型,就可能发生背面面片遮挡正面面片的情况
				o.pos =UnityObjectToClipPos( i.vertex);

				//法线转换到视角空间,为了让描边可以在观察空间达到最好的效果
				float3 normal = mul((float3x3)UNITY_MATRIX_IT_MV , i.normal);
				//将视空间法线xy坐标转化到投影空间,只有xy需要,z深度不需要了 
				float2 offset = TransformViewToProjection(normal.xy); 

				//在最终投影阶段输出进行偏移操作
				o.pos.xy +=offset * o.pos.z * _Outline; 
			}

			void VertexNormalMethod1(appdata_full i , inout v2f o)  //顶点沿着法线方向偏移方法三
			{	
				//同理法线转换到视角空间,利用法线的方向与_Outline相乘,得出偏移程度
				//首先对顶点法线的z分量进行处理,使它们等于一个定值
				//然后把法线归一化后再对顶点进行扩张。这样的好处在于,拓展后的背面更加扁平化,从而降低了遮挡正面面片的可能性。
				float4 viewPos = mul(UNITY_MATRIX_MV , i.vertex);
				float3 normal = mul ( (float3x3) UNITY_MATRIX_IT_MV , i.normal);

				//设置法线的z分量为定值,对其归一化后再将顶点沿其方向扩张
				normal.z = -1.0;
				viewPos = viewPos + float4 (normalize ( normal) ,0) * _Outline;

				o.pos = mul(UNITY_MATRIX_P , viewPos);
			}

			v2f vert ( appdata_full i )
            {
				v2f o;
				//ZBiaMethod (i , o);
				//VertexNormalMethod0 (i , o);
				VertexNormalMethod1 (i ,o);

				o.uv = i.texcoord;
				TRANSFER_VERTEX_TO_FRAGMENT(o); 
				return o;
            }

            fixed4 frag (v2f i) : COLOR
            {
				return fixed4 (0,0,0,1);
            }
            ENDCG
        }
    }
	FallBack "DIFFUSE"
}

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值