unity Shader 后处理实现边缘检测
本文记录用sobel算子进行边缘检测,实现unity描边屏幕后处理效果的过程(Learn by 《unity shader 入门精要》)
unity实现屏幕后处理效果过程如下:
1、首先在摄像机中添加一个用于屏幕后处理的脚本,该脚本需要先检测一系列条件是否满足 如当前平台是否支持渲染纹理和屏幕特效,是否支持当前的unity shader。为了提高代码复用性,我们还创建了一个基类用于检测条件,将具体实现效果的脚本继承自该基类。
基类PostEffectBase.cs
using UnityEngine;
using System.Collections;
[ExecuteInEditMode]//支持脚本在编辑模式下运行
[RequireComponent (typeof(Camera))]
public class PostEffectsBase : MonoBehaviour {
//protected void Start()
//{
// CheckResources();
//}
// Called when start
//protected void CheckResources()
//{
// bool isSupported = CheckSupport();
// if (isSupported == false)
// {
// NotSupported();
// }
//}
// 检查平台支持 现已无需检测,始终返回true
//protected bool CheckSupport() {
// if (SystemInfo.supportsImageEffects == false) {
// Debug.LogWarning("This platform does not support image effects.");
// return false;
// }
// return true;
//}
// Called when the platform doesn't support this effect
//protected void NotSupported() {
// enabled = false;
//}
// Called when need to create the material used by this effect
protected Material CheckShaderAndCreateMaterial(Shader shader, Material material) {
if (shader == null) {
return null;
}
//if (shader.isSupported && material && material.shader == shader)
// return material;
if (material && material.shader == shader)
return material;
//if (!shader.isSupported) {
// return null;
//}
else {
material = new Material(shader);
material.hideFlags = HideFlags.DontSave;
if (material)
return material;
else
return null;
}
}
}
边缘检测脚本 EdgeDetection.cs
using UnityEngine;
using System.Collections;
public class EdgeDetection : PostEffectsBase {
public Shader edgeDetectShader;
private Material edgeDetectMaterial = null;
public Material material {
get {
edgeDetectMaterial = CheckShaderAndCreateMaterial(edgeDetectShader, edgeDetectMaterial);
return edgeDetectMaterial;
}
}
[Range(0.0f, 1.0f)]
public float edgesOnly = 0.0f; //0——1 原图像——边缘
public Color edgeColor = Color.black; //边缘颜色
public Color backgroundColor = Color.white; //背景颜色
//src——源纹理 dest——目标纹理
void OnRenderImage (RenderTexture src, RenderTexture dest) {
if (material != null) {
material.SetFloat("_EdgeOnly", edgesOnly);
material.SetColor("_EdgeColor", edgeColor);
material.SetColor("_BackgroundColor", backgroundColor);
//当前渲染图像存储到第一个参数,将第二个参数对应的纹理传递给材质
Graphics.Blit(src, dest, material);
} else {
Graphics.Blit(src, dest);
}
}
}
edgesOnly:用于调整边缘与源图像的混合权重 当edgesOnly为1时,则只会显示边缘,为0时则会叠加在源渲染图像上
OnRenderImage:是unity提供的接口,方便我们直接抓取渲染后的屏幕图像。在该函数中,我们通常利用Graphics.Blit函数来完成对渲染纹理的处理
Blit:其声明如下 public static void Blit(Texture src,RenderTexture dest,Material mat,int pass=-1)
src对应源纹理,
dest是目标纹理,
mat是我们使用的材质,他会将src纹理传递给Shader中名为_MainTex的纹理属性
pass 默认-1,表示他会依次调用Shader内所有Pass
2、创建一个Shader用于处理渲染纹理,基类提供的CheckShaderAndCreateMaterial方法会自动返回一个使用该shader的材质
Edge Detection.shader
Shader "MyShader/Edge Detection" {
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)
}
SubShader {
Pass {
//屏幕后处理渲染设置的标配
//关闭深度写入,防止挡住在其后面被渲染的物体
ZTest Always Cull Off ZWrite Off
CGPROGRAM
#include "UnityCG.cginc"
#pragma vertex vert
#pragma fragment fragSobel
sampler2D _MainTex;
uniform half4 _MainTex_TexelSize;//访问某纹理对应的每个纹素大小。通过其计算各个相邻区域的纹理坐标
fixed _EdgeOnly;
fixed4 _EdgeColor;
fixed4 _BackgroundColor;
struct v2f {
float4 pos : SV_POSITION;
half2 uv[9] : TEXCOORD0;
};
//appdata_img为unity内置结构体 包含一个顶点和一个纹理信息
v2f vert(appdata_img v) {
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
half2 uv = v.texcoord;
//定义维数为9的纹理数组,对应使用Sobel算子采样时需要的9个纹理坐标
o.uv[0] = uv + _MainTex_TexelSize.xy * half2(-1, -1);
o.uv[1] = uv + _MainTex_TexelSize.xy * half2(0, -1);
o.uv[2] = uv + _MainTex_TexelSize.xy * half2(1, -1);
o.uv[3] = uv + _MainTex_TexelSize.xy * half2(-1, 0);
o.uv[4] = uv + _MainTex_TexelSize.xy * half2(0, 0);
o.uv[5] = uv + _MainTex_TexelSize.xy * half2(1, 0);
o.uv[6] = uv + _MainTex_TexelSize.xy * half2(-1, 1);
o.uv[7] = uv + _MainTex_TexelSize.xy * half2(0, 1);
o.uv[8] = uv + _MainTex_TexelSize.xy * half2(1, 1);
return o;
}
//亮度信息
fixed luminance(fixed4 color) {
return 0.2125 * color.r + 0.7154 * color.g + 0.0721 * color.b;
}
half Sobel(v2f i) {
const half Gx[9] = {-1, 0, 1,
-2, 0, 2,
-1, 0, 1};
const half Gy[9] = {-1, -2, -1,
0, 0, 0,
1, 2, 1};
half texColor;
half edgeX = 0;
half edgeY = 0;
//在卷积运算中,依次对9个像素进行采样,计算他们的亮度值,再与卷积核Gx Gy中对应的权重相乘后,叠加到各自的梯度上
for (int it = 0; it < 9; it++) {
texColor = luminance(tex2D(_MainTex, i.uv[it]));
edgeX += texColor * Gx[it];
edgeY += texColor * Gy[it];
}
//1减去水平方向和竖直方向的梯度值的绝对值,得到edge edge越小越可能是边缘
half edge = 1 - abs(edgeX) - abs(edgeY);
return edge;
}
fixed4 fragSobel(v2f i) : SV_Target {
half edge = Sobel(i);
//lerp(from,to,t) = from + (to - from) *t
fixed4 withEdgeColor = lerp(_EdgeColor, tex2D(_MainTex, i.uv[4]), edge);
fixed4 onlyEdgeColor = lerp(_EdgeColor, _BackgroundColor, edge);
return lerp(withEdgeColor, onlyEdgeColor, _EdgeOnly);
}
ENDCG
}
}
FallBack Off
}
最后片元着色fragSobel的3个Lerp操作有点难理解
首先搞清楚lerp的数学意义
lerp(from,to,t)=from + (to - from)*t 也就是随t 从0到1 输出结果从 from 到 to
从前面卷积得到的边缘来看,边缘edge越小越有可能是边缘
第一个withEdgeColor的函数lerp的参数是_EdgeColor和屏幕源图像的采样,也就是lerp从0—1的变化时从带边缘的图到不带边缘的图
第二个onlyEdgeColor的函数lerp的第二个参数变换为了背景色(默认为白色),也就是lerp从0——1的变化时从只有边缘的图到仅有背景色的图
最后return的lerp参数是前两个lerp的返回值,lerp从0——1变化时从带边缘的图到只有边缘的图
3、编写完代码返回编辑器后,在EdgeDetection.cs脚本面板将edgeDetectShader拖拽到公开变量中
展示如下:
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 分享4款.NET开源、免费、实用的商城系统
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 记一次.NET内存居高不下排查解决与启示
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了