再谈边缘检测【Unity Shader入门精要13.4】

12.3节中的边缘检测使用的是Sobel算子,但是这种直接对颜色信息进行边缘检测的方法会产生很多我们不希望得到的边缘线,

本节学习如何在深度和法线纹理上进行边缘检测,这些图像不会受纹理和光照影响,而仅仅保存了当前渲染物体的模型信息。

Roberts算子的卷积核

-1 0
0 1
0

-1

1

0

本质上是计算左上角和右下角的差值,乘以右上角和左下角的差值,作为评估边缘的依据,。

取对角方向的深度和法线值,比较他们之间的差值,如果超过某个阀值(可自由控制阀值),就认为他们之间存在一条边

//13.4 再谈边缘检测

// Upgrade NOTE: replaced 'mul(UNITY_MATRIX_MVP,*)' with 'UnityObjectToClipPos(*)'
Shader "Unlit/Chapter13-EdgeDeterNormalAndDepth"
{
	Properties{
		_MainTex("Base (RGB)", 2D) = "white" {}
	_EdgeOnly("Edge Only", Float) = 1.0
		_EdgeColor("Edge Color", Color) = (0, 0, 0, 1)
		_BackgroundColor("Background Color", Color) = (1, 1, 1, 1)
		_SampleDistance("Sample Distance", Float) = 1.0//样本距离
		_Sensitivity("Sensitivity", Vector) = (1, 1, 1, 1)//XY分量分别对应了法线和深度的检测灵敏度,ZW分量择没有实际作用

	}
		SubShader{
		CGINCLUDE

#include "UnityCG.cginc"

	sampler2D _MainTex;
	half4 _MainTex_TexelSize;//存储纹素的大小
	fixed _EdgeOnly;
	fixed4 _EdgeColor;
	fixed4 _BackgroundColor;
	float _SampleDistance;
	half4 _Sensitivity;

	sampler2D _CameraDepthNormalsTexture;//获取深度+纹理

	struct v2f {
		float4 pos : SV_POSITION;
		half2 uv[5]: TEXCOORD0;
	};

	/*定义了一个维度为5的纹理坐标数组,这个数组的第一个坐标储存了屏幕颜色图像的采样纹理,
	我们对深度纹理的采样坐标进行了平台化差异处理,在必要情况下对它的竖坐标进行翻转,
	数组中剩余的4个坐标则储存了使用Roberts算子需要采样的纹理坐标。
	_SampleDistance 用来控制采样距离。*/
	
	v2f vert(appdata_img v) {
		v2f o;
		o.pos = UnityObjectToClipPos(v.vertex);

		half2 uv = v.texcoord;
		o.uv[0] = uv;

#if UNITY_UV_STARTS_AT_TOP
		if (_MainTex_TexelSize.y < 0)
			uv.y = 1 - uv.y;
#endif
		/*
		【-1.1】3-----(0.1)------【1.1】1
		   |             |            |
		(-1.0)------(0.0)------(0.1)
		   |             |            |
		【-1.-1】2---(0.-1)-----【1.-1】4
		
		*/
		/*
		Roberts算子
		*/
		o.uv[1] = uv + _MainTex_TexelSize.xy * half2(1,1) * _SampleDistance;
		o.uv[2] = uv + _MainTex_TexelSize.xy * half2(-1,-1) * _SampleDistance;
		o.uv[3] = uv + _MainTex_TexelSize.xy * half2(-1,1) * _SampleDistance;
		o.uv[4] = uv + _MainTex_TexelSize.xy * half2(1,-1) * _SampleDistance;

		return o;
	}



	/*首先用四个纹理坐标对深度进行采样*/

	/*再调用CheckSame函数来分别计算对角线上两个纹理值的差值。
	CheckSame的返回值要么是0要么是1,返回0时表明这两个点之间存在一条边界,反之则返回1。
	*/

	half CheckSame(half4 center, half4 sample) {

		/*CheckSame首先对输入参数进行处理,得到两个采样点的法线和深度值
		值得注意的是,这里并没有解码得到真正的法线值,而是直接使用了XY分量,这是因为我们只需要比较两个采样值之间的差异度,
		而并不需要知道他们的真正法线*/
		half2 centerNormal = center.xy;
		float centerDepth = DecodeFloatRG(center.zw);
		half2 sampleNormal = sample.xy;
		float sampleDepth = DecodeFloatRG(sample.zw);


		
		// difference in normals  法线的差
		// do not bother decoding normals - there's no need here 不要费心去解码法线——这里没有必要
		/*然后我们把两个采样点的对应值相减并取绝对值,再乘以灵敏度的参数*/
		half2 diffNormal = abs(centerNormal - sampleNormal) * _Sensitivity.x;

		/*把差异值的每个分量相加再和一个阀值比较,
		如果他们的和小于阀值,则返回1,说明差异不明显,不存一条边界,否则返回0*/
		int isSameNormal = (diffNormal.x + diffNormal.y) < 0.1;


		// difference in depth  不同的深度
		float diffDepth = abs(centerDepth - sampleDepth) * _Sensitivity.y;

		// scale the required threshold by the distance   按距离缩放所需的阈值
		int isSameDepth = diffDepth < 0.1 * centerDepth;

		/*最后我们把法线和深度的检查结果相乘,作为组合后的返回值*/
		// return:
		// 1 - if normals and depth are similar enough  如果法线和深度足够相似
		// 0 - otherwise
		return isSameNormal * isSameDepth ? 1.0 : 0.0;
	}


	/*
	通过把计算采样的纹理坐标从片元着色器中转移到顶点着色器中,可以减少运算,提高性能。
	由于从顶点着色器到片元着色器的插值是线性的,因此这样的转移并不会影响纹理坐标的计算结果
	*/   
	//片元着色器
	fixed4 fragRobertsCrossDepthAndNormal(v2f i) : SV_Target{
		half4 sample1 = tex2D(_CameraDepthNormalsTexture, i.uv[1]);
		half4 sample2 = tex2D(_CameraDepthNormalsTexture, i.uv[2]);
		half4 sample3 = tex2D(_CameraDepthNormalsTexture, i.uv[3]);
		half4 sample4 = tex2D(_CameraDepthNormalsTexture, i.uv[4]);
		half edge = 1.0;

		edge *= CheckSame(sample1, sample2);
		edge *= CheckSame(sample3, sample4);

		fixed4 withEdgeColor = lerp(_EdgeColor, tex2D(_MainTex, i.uv[0]), edge);
		fixed4 onlyEdgeColor = lerp(_EdgeColor, _BackgroundColor, edge);

		return lerp(withEdgeColor, onlyEdgeColor, _EdgeOnly);
	}

		ENDCG

		Pass {
		ZTest Always Cull Off ZWrite Off

			CGPROGRAM

#pragma vertex vert  
#pragma fragment fragRobertsCrossDepthAndNormal

			ENDCG
	}
	}
		FallBack Off
}
//13.4 再谈边缘检测

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class EdgeDetectNormalsAndDepth : PostEffectsBase  {
    public Shader edgeDetectShader;
    private Material edgeDetectMaterial;
    private Material material
    {
        get
        {
            edgeDetectMaterial = CheckShaderAndCreateMaterial(edgeDetectShader, edgeDetectMaterial);
            return edgeDetectMaterial;
        }
    }

    [Range(0.0f, 1.0f)]
    public float edgesOnly = 0.0f;
    public Color edgeColor = Color.black;
    public Color backgroundColor = Color.white;
    public float sampleDistance = 1.0f;//用于控制对深度+法线纹理采样时,使用的采样距离
    public float sensitivityDepth = 1.0f;//影响当邻域的深度值或法线的深度值相差多少时,会被认为存在一条边界
    public float sensitivityNormals = 1.0f;//同上

    //由于本例需要获取摄像机的深度+法线纹理,在脚本的OnEnable函数中设置摄像机的相应状态
    private void OnEnable()
    {
        GetComponent<Camera>().depthTextureMode |= DepthTextureMode.DepthNormals;
    }

    //实现OnRender函数,把各个参数传递给材质

    [ImageEffectOpaque]
    /*在默认情况下,OnRender函数会在所有的不透明的Pass
     * (即渲染队列小于等于2500的PSS,内置的back,Geometry,Alpha渲染队列均在此范围内)
     * 执行完毕后立即调用该函数而不对透明物体(渲染队列为Transform)产生影响,
     * 此时可以在OnRenderImage函数前添加ImageEffectOpaque属性来实现*/
    private void OnRenderImage(RenderTexture source, RenderTexture destination)
    {
        if (material !=null)
        {
            material.SetFloat("_EdgeOnly", edgesOnly);
            material.SetColor("_EdgeColor", edgeColor);
            material.SetColor("_BackgroundColor", backgroundColor);
            material.SetFloat("_SampleDistance", sampleDistance);
            material.SetVector("_Sensitivity", new Vector4(sensitivityNormals, sensitivityDepth, 0.0f, 0.0f));

            Graphics.Blit(source, destination, material);
        }else
        {
            Graphics.Blit(source, destination);
        }
    }
}

扫描二维码关注公众号,回复: 4818123 查看本文章

本节实现的描边效果是基于整个屏幕空间进行,例如当玩家选中某个物体后,我们想要在该物体周围添加一层描边效果,这时可以使用Unity提供的Graphics.DrawMesh或者Graphics.DrawMeshNow 函数把需要描边的物体再渲染一遍(在所有不透明物体渲染完毕以后),然后再使用本节提到的边缘检测算法计算深度或法线纹理中每个像素的梯度值,判断他们是否小于某个阀值如果是,就在shader中使用Clip()函数将该像素剔除,从而显示出原来的颜色

。。。。稍微试了一下。。没试出来,撸第二遍的时候在来补上吧


扩展阅读;

我们可以在Unity中创建任何需要的缓存纹理,这可以通过使用Unity的着色器转换(ShaderReplacement)功能(即调用Camera。RenderWithShader(shader,replacementTag)函数)把整个场景再渲染一遍来得到,而在很多时候,这实际也是Unity创建深度和法线纹理时使用的方法

P287


猜你喜欢

转载自blog.csdn.net/ABigDeal/article/details/84563092
今日推荐