前言

周末通关了一个小游戏,流程很短,6个小时左右就通关,但是游戏的画风,视角,玩法都比较新奇,对了,游戏的名字也很奇特《12 Is Better Than 6》(12比6好是有什么梗吗?)。

游戏采用的是俯视角,人物在活着的时候基本只能看到个帽子,玩法类似很早玩的《夺宝奇兵》《盟军敢死队》《1937特种兵》,但是游戏是西部牛仔的背景,画风采用的黑白素描的风格。

游戏玩法虽然简单,主要是射击+暗杀,但是非常硬核。前期有左轮手枪的时候,需要开一枪,按一下右键转动左轮再开下一枪,上弹也是需要手动按。有几关过了很久才过去。后期拿到双左轮之后难度就大大降低啦。

游戏通关之后,留给我印象最深刻的还是游戏本身的渲染风格,这种类似铅笔描绘的风格看起来也挺舒服的。也不由得让我想尝试一下类似的效果,今天主要来玩一下基于后处理的边缘检测效果。

简介

边缘检测,在图像处理,计算机视觉中都是很重要的一个概念。在图像处理领域,由于输入只有一张图片,所以一般是将图片转成灰度,然后判断图片像素间的梯度来判断图像中的边界的;而在3D渲染领域,除了场景渲染结果图片外,我们还可以得到场景的深度以及法线等信息,让我们可以得到更加精确的边缘检测结果。边缘检测在渲染中虽然可能没有图像处理领域那样出名,但是也是可以用来实现一些特殊渲染风格,渲染效果,以及后处理AA等功能。

边缘检测的方式是使用一些边缘检测的算子对图像进行卷积操作,和之前玩过的高斯模糊,双边滤波类似,都是通过当前像素点及其周围像素点按照一定的规则权重计算得到结果。

本文的效果实际上也算是一种描边效果的实现,关于其他类型的描边效果,可以参考本人之前的blog-《Unity Shader-描边效果》。

基于图像的边缘检测

首先,我们看一下基于图像的边缘检测。也就是只在后处理阶段使用边缘检测算子针对图像的灰度计算梯度。我们能看到图像的边界,在于图像中的亮度等因素有明显差异,我们可以用梯度来表示这种边界的权重,梯度越大,边缘就越明显。在图像处理领域已经有了很成熟的边缘检测卷积方式,比如Roberts算子和Sobel算子。主要的思想就是使用横竖两个方向的两个矩阵对原图进行卷积运算,得到两个方向的亮度的梯度,两个算子如下(与常见的图像处理中定义可能稍微有一些区别,主要在于行矩阵和列矩阵的差异,表现的结果是一样的):

我们在Shader中同时包含两种边缘检测的算子,对比效果。Shader代码如下:

/********************************************************************FileName: EdgeEffect.shaderDescription: 后处理描边效果,使用Roberts和Sobel算子,可调强度&检测距离history: 11:11:2018 by puppet_masterhttps://blog.csdn.net/puppet_master
*********************************************************************/
Shader "Edge/EdgeEffect"
{Properties{_MainTex ("Texture", 2D) = "white" {}}CGINCLUDE#include "UnityCG.cginc"struct appdata{float4 vertex : POSITION;float2 uv : TEXCOORD0;};struct v2f{float2 uvRoberts[5] : TEXCOORD0;float2 uvSobel[9] : TEXCOORD5;float4 vertex : SV_POSITION;};sampler2D _MainTex;float4 _MainTex_TexelSize;fixed4 _EdgeColor;fixed4 _NonEdgeColor;float _EdgePower;float _SampleRange;float Sobel(v2f i){const float Gx[9] = {-1, -2, -1,0,  0,  0,1,  2,  1};const float Gy[9] ={1, 0, -1,2, 0, -2,1, 0, -1};float edgex, edgey;for(int j = 0; j < 9; j++){fixed4 col = tex2D(_MainTex, i.uvSobel[j]);float lum = Luminance(col.rgb);edgex += lum * Gx[j];edgey += lum * Gy[j];}return 1 - abs(edgex) - abs(edgey);}float Roberts(v2f i){const float Gx[4] = {-1,  0,0,  1};const float Gy[4] ={0, -1,1,  0};float edgex, edgey;for(int j = 0; j < 4; j++){fixed4 col = tex2D(_MainTex, i.uvRoberts[j]);float lum = Luminance(col.rgb);edgex += lum * Gx[j];edgey += lum * Gy[j];}return 1 - abs(edgex) - abs(edgey);}v2f vert_Sobel (appdata v){v2f o;o.vertex = UnityObjectToClipPos(v.vertex);o.uvSobel[0] = v.uv + float2(-1, -1) * _MainTex_TexelSize * _SampleRange;o.uvSobel[1] = v.uv + float2( 0, -1) * _MainTex_TexelSize * _SampleRange;o.uvSobel[2] = v.uv + float2( 1, -1) * _MainTex_TexelSize * _SampleRange;o.uvSobel[3] = v.uv + float2(-1,  0) * _MainTex_TexelSize * _SampleRange;o.uvSobel[4] = v.uv + float2( 0,  0) * _MainTex_TexelSize * _SampleRange;o.uvSobel[5] = v.uv + float2( 1,  0) * _MainTex_TexelSize * _SampleRange;o.uvSobel[6] = v.uv + float2(-1,  1) * _MainTex_TexelSize * _SampleRange;o.uvSobel[7] = v.uv + float2( 0,  1) * _MainTex_TexelSize * _SampleRange;o.uvSobel[8] = v.uv + float2( 1,  1) * _MainTex_TexelSize * _SampleRange;return o;}fixed4 frag_Sobel (v2f i) : SV_Target{fixed4 col = tex2D(_MainTex, i.uvSobel[4]);float g = Sobel(i);g = pow(g, _EdgePower);col.rgb = lerp(_EdgeColor, _NonEdgeColor, g);return col;}v2f vert_Roberts (appdata v){v2f o;o.vertex = UnityObjectToClipPos(v.vertex);o.uvRoberts[0] = v.uv + float2(-1, -1) * _MainTex_TexelSize * _SampleRange;o.uvRoberts[1] = v.uv + float2( 1, -1) * _MainTex_TexelSize * _SampleRange;o.uvRoberts[2] = v.uv + float2(-1,  1) * _MainTex_TexelSize * _SampleRange;o.uvRoberts[3] = v.uv + float2( 1,  1) * _MainTex_TexelSize * _SampleRange;o.uvRoberts[4] = v.uv;return o;}fixed4 frag_Roberts (v2f i) : SV_Target{fixed4 col = tex2D(_MainTex, i.uvRoberts[4]);float g = Roberts(i);g = pow(g, _EdgePower);col.rgb = lerp(_EdgeColor, _NonEdgeColor, g);return col;}ENDCGSubShader{// No culling or depthCull Off ZWrite Off ZTest Always//Pass 0 Sobel OperatorPass{CGPROGRAM#pragma vertex vert_Sobel#pragma fragment frag_SobelENDCG}//Pass 1 Roberts OperatorPass{CGPROGRAM#pragma vertex vert_Roberts#pragma fragment frag_RobertsENDCG}}
}

C#代码如下:

/********************************************************************FileName: EdgeEffect.csDescription: 后处理描边效果,使用Roberts和Sobel算子,可调强度&检测距离history: 11:11:2018 by puppet_masterhttps://blog.csdn.net/puppet_master
*********************************************************************/
using UnityEngine;[ExecuteInEditMode]
public class EdgeEffect : MonoBehaviour
{public enum EdgeOperator{Sobel = 0,Roberts = 1,}private Material edgeEffectMaterial = null;public Color edgeColor = Color.black;public Color nonEdgeColor = Color.white;[Range(1.0f, 10.0f)]public float edgePower = 1.0f;[Range(1, 5)]public int sampleRange = 1;public EdgeOperator edgeOperator = EdgeOperator.Sobel;private void Awake(){var shader = Shader.Find("Edge/EdgeEffect");edgeEffectMaterial = new Material(shader);}private void OnRenderImage(RenderTexture source, RenderTexture destination){edgeEffectMaterial.SetColor("_EdgeColor", edgeColor);edgeEffectMaterial.SetColor("_NonEdgeColor", nonEdgeColor);edgeEffectMaterial.SetFloat("_EdgePower", edgePower);edgeEffectMaterial.SetFloat("_SampleRange", sampleRange);Graphics.Blit(source, destination, edgeEffectMaterial, (int)edgeOperator);}
}

依然是本人最常用的场景,原始的场景效果如下:

使用Roberts算子的边缘检测效果如下,x1 Power:

效果不是很清晰,而使用Sobel算子的边缘检测效果,x1 Power:

很明显,Sobel算子的效果会更好一些,但是我们如果将其都乘以一定的Power,实际上二者可以达到接近的效果,而Roberts的性能是要由于Sobel的。下面为Sobel x10Power后的效果:

颇有一些《12 is Better than 6》风格化的感觉了,我们再换个颜色,调整一下检测的半径,使线条变粗,则又是一种宣纸毛笔画的风格:

基于深度法线的边缘检测

基于颜色的边缘检测的主要优点在于无需额外信息,只需要场景图本身,但是也有一定的缺点,如果两个对象的颜色差异不明显,即使有边界也检测不出来,可能出现一些瑕疵。如果我们想要纯正的边缘的效果的话,就需要用另一种更加准确的边缘检测方式。3D渲染相对于普通的二维图像处理的优势就在于我们还可以得到一些其他的信息,比如场景的深度,场景的法线,通过这两者,我们可以在当前采样点的周围像素点计算法线的差异以及深度的差异,如果超过一定的阈值,就认为是边界。

Shader代码如下:

/********************************************************************FileName: EdgeEffectDepthNormal.shaderDescription: 后处理描边效果,使用DepthNormalTexture检测history: 13:11:2018 by puppet_masterhttps://blog.csdn.net/puppet_master
*********************************************************************/
Shader "Edge/EdgeEffectDepthNormal"
{Properties{_MainTex ("Texture", 2D) = "white" {}}CGINCLUDE#include "UnityCG.cginc"struct appdata{float4 vertex : POSITION;float2 uv : TEXCOORD0;};struct v2f{float2 uv[5] : TEXCOORD0;float4 vertex : SV_POSITION;};sampler2D _MainTex;float4 _MainTex_TexelSize;sampler2D _CameraDepthNormalsTexture;fixed4 _EdgeColor;fixed4 _NonEdgeColor;float _SampleRange;float _NormalDiffThreshold;float _DepthDiffThreshold;float CheckEdge(fixed4 s1, fixed4 s2){float2 normalDiff = abs(s1.xy - s2.xy);float normalEdgeVal = (normalDiff.x + normalDiff.y) < _NormalDiffThreshold;float s1Depth = DecodeFloatRG(s1.zw);float s2Depth = DecodeFloatRG(s2.zw);float depthEdgeVal = abs(s1Depth - s2Depth) < 0.1 * s1Depth * _DepthDiffThreshold;return depthEdgeVal * normalEdgeVal;}v2f vert (appdata v){v2f o;o.vertex = UnityObjectToClipPos(v.vertex);o.uv[0] = v.uv + float2(-1, -1) * _MainTex_TexelSize * _SampleRange;o.uv[1] = v.uv + float2( 1, -1) * _MainTex_TexelSize * _SampleRange;o.uv[2] = v.uv + float2(-1,  1) * _MainTex_TexelSize * _SampleRange;o.uv[3] = v.uv + float2( 1,  1) * _MainTex_TexelSize * _SampleRange;o.uv[4] = v.uv;return o;}fixed4 frag (v2f i) : SV_Target{fixed4 col = tex2D(_MainTex, i.uv[4]);fixed4 s1 = tex2D(_CameraDepthNormalsTexture, i.uv[0]);fixed4 s2 = tex2D(_CameraDepthNormalsTexture, i.uv[1]);fixed4 s3 = tex2D(_CameraDepthNormalsTexture, i.uv[2]);fixed4 s4 = tex2D(_CameraDepthNormalsTexture, i.uv[3]);float result = 1.0;result *= CheckEdge(s1, s4);result *= CheckEdge(s2, s3);col.rgb = lerp(_EdgeColor, _NonEdgeColor, result);return col;}ENDCGSubShader{// No culling or depthCull Off ZWrite Off ZTest Always//Pass 0 Roberts OperatorPass{CGPROGRAM#pragma vertex vert#pragma fragment fragENDCG}}
}

C#代码如下:

/********************************************************************FileName: EdgeEffectDepthNormal.csDescription: 后处理描边效果,使用DepthNormalTexture进行检测history: 13:11:2018 by puppet_masterhttps://blog.csdn.net/puppet_master
*********************************************************************/
using UnityEngine;[ExecuteInEditMode]
public class EdgeEffectDepthNormal : MonoBehaviour
{private Material edgeEffectMaterial = null;public Color edgeColor = Color.black;public Color nonEdgeColor = Color.white;[Range(1, 5)]public int sampleRange = 1;[Range(0, 1.0f)]public float normalDiffThreshold = 0.2f;[Range(0, 5.0f)]public float depthDiffThreshold = 2.0f;private void Awake(){var shader = Shader.Find("Edge/EdgeEffectDepthNormal");edgeEffectMaterial = new Material(shader);}private void OnEnable(){var cam = GetComponent<Camera>();cam.depthTextureMode |= DepthTextureMode.DepthNormals;}private void OnDisable(){var cam = GetComponent<Camera>();cam.depthTextureMode = DepthTextureMode.None;}private void OnRenderImage(RenderTexture source, RenderTexture destination){edgeEffectMaterial.SetColor("_EdgeColor", edgeColor);edgeEffectMaterial.SetColor("_NonEdgeColor", nonEdgeColor);edgeEffectMaterial.SetFloat("_SampleRange", sampleRange);edgeEffectMaterial.SetFloat("_NormalDiffThreshold", normalDiffThreshold);edgeEffectMaterial.SetFloat("_DepthDiffThreshold", depthDiffThreshold);Graphics.Blit(source, destination, edgeEffectMaterial);}
}

还是之前的场景图:

使用边缘检测的效果如下(Depth + Normal同时检测):

单独使用Depth检测的效果:

单独使用Normal检测的效果:

实际上,单独使用Depth和单独使用Normal都可以实现边缘检测,但是二者结合起来使用可能效果更好一些,正好CameraDepthNormalTexture中二者都包含,索性一起用啦。

边缘检测实现高亮流光效果

实现了基本的边缘检测效果,我们除了可以用这个技术做一些特殊的渲染风格外,还可以实现一些特殊的效果。比如加一个Flash贴图,类似之前的流光效果:

float v = tex2D(_FlashTexture, i.uvSobel[4] + float2(_EffectPercentage * _Time.y, 0.0)).r * 10;
fixed3 edge = lerp(_EdgeColor, _NonEdgeColor, g);
col.rgb = lerp(edge, col.rgb, saturate(v));

效果如下:

反过来也可以:

当然,我们也可以只让描边本身和原始效果融合,达到仅显示高两部分边缘的效果:

float v = tex2D(_FlashTexture, i.uv[4] + float2(_EffectPercentage * _Time.y, 0.0)).r;
col.rgb = v * (1 - result) * _EdgeColor + col.rgb;

效果如下:

如果使用DepthNormalMap检测,可以获得更精准的边缘流动效果:

边缘检测实现过渡效果

有了边缘检测的基本效果,下面就是发挥想象力的时间了。我们可以再做一些其他的效果,比如转场的效果,把边缘效果和场景原始效果做一个基本的插值,实现一个最基本的转场:

fixed3 edge = lerp(_EdgeColor, _NonEdgeColor, g);
col.rgb = lerp(edge, col.rgb, _EffectPercentage);

效果动图如下:

不够酷炫,那么我们就让这个转场实现一个按照方向来的渐变,根据uv控制渐变的方向,再用噪声添加一些随机效果:

fixed3 edge = lerp(_EdgeColor, _NonEdgeColor, g);
float noise = tex2D(_FlashTexture, i.uvSobel[4]).r * _NoiseFactor;
float control = _EffectPercentage > (i.uvSobel[4].x + noise);
control = saturate(control);
col.rgb = lerp(edge, col.rgb, control);

效果如下:

总结

本文主要实现了基于颜色以及基于深度和法线的边缘检测效果,然后使用边缘检测实验了一些特殊的渲染风格,以及流光,转场等特殊效果。

最后,最近通关了《What Remains of Edith Finch》(艾迪芬奇的记忆),神作啊!!下篇的开头继续安利!

Unity Shader-边缘检测效果(基于颜色,基于深度法线,边缘流光效果,转场效果)相关推荐

  1. Unity Shader 之 实现简单的动态过场切换图片的效果

    Unity Shader 之 实现简单的动态过场切换图片的效果 目录 Unity Shader 之 实现简单的动态过场切换图片的效果 一.简单介绍

  2. html5 css3鼠标滑过效果,纯CSS3鼠标滑过按钮流光效果

    这是一款效果非常炫酷的纯CSS3鼠标滑过按钮流光效果.当用户用鼠标滑过按钮的时候,一道流光会瞬间滑过按钮,就像玻璃的反光效果,非常漂亮. 使用方法 HTML结构 该效果中的按钮是一个超链接元素. Li ...

  3. Unity Shader - 实现简单水体 - 浅水到深水颜色控制

    文章目录 制作步骤 准备好水体网格 扰动水体网格 添加水体网格色调,纹理 放置海上放哨点(一些随便放的立方体) 添加水的深浅透视效果 添加水光效 重构水顶点法线 正交的相机的深度需要注意 改进 Pro ...

  4. 【Unity Shader】(六) ------ 复杂的光照(上)

    笔者使用的是 Unity 2018.2.0f2 + VS2017,建议读者使用与 Unity 2018 相近的版本,避免一些因为版本不一致而出现的问题.              [Unity Sha ...

  5. 【Unity Shader】 CubeMap(立方体贴图)

    Unity Shader 立方体贴图 一.介绍CubeMap Shader中对CubeMap采样 Unity中如何制作CubeMap 二.Reflect CubeMap(反射立方体纹理用于环境映射) ...

  6. 【Unity Shader】用Cubemap实现天空盒和环境映射

    1 关于Cubemap Cubemap在实时渲染中有很多应用,最常见的就是实现天空盒(Skybox)和环境映射(Environment Mapping). 2 实现天空盒 2.1 实现原理 天空盒不陌 ...

  7. Unity Shader - Noise 噪点图 - 实现简单山脉

    学习记录一下噪点应用 噪点相关知识,可以看文章最下面的:References 后面有基于这篇文章重构过:Unity Shader - 简单山脉 - 顶点着色器重构法线 运行效果 噪点图 可以写了个C# ...

  8. Unity Shader 布料渲染(丝袜)

    Unity Shader 布料渲染(丝袜) 现实中的丝袜效果 丹尼尔值 纤维的特性 分析 效果截图: Weak: Normal: Strong: 属性值定义: 丹尼尔值与拉伸程度 边缘度的计算 完整S ...

  9. 【Unity Shader】Unity Chan的卡通材质

    写在前面 时隔两个月我终于来更新博客了,之前一直在学东西,做一些项目,感觉没什么可以分享的就一直没写.本来之前打算写云彩渲染或是Compute Shader的,觉得时间比较长所以打算先写个简单的. 今 ...

最新文章

  1. 一流科技完成5000万人民币A轮融资,高瓴创投独家领投
  2. ASP.NET MVC 过滤器(三)
  3. 如何遍历一个JS对象中的所有属性,输出键值对--我居然犯错半个小时
  4. 数据库-优化-从慢查询日志中分析索引使用情况及pt-find
  5. Iphone NSMutableArray,NSMutableDictionary AND 动态添加按钮
  6. Android Studio新建工程syncing失败;Android studio Connection timed out: connect
  7. JavaScript的单线程性质以及定时器的工作原理
  8. java script 下载_JavaScript下载
  9. 泛微OA7.0下载7.1下载
  10. HTML入门与进阶以及HTML5
  11. 自定义View实现雨点洒落效果
  12. js基础知识(第二篇)
  13. 怎样实现服务器远程操作系统,如何远程控制家里的电脑或服务器?
  14. 目标检测 | RCNN算法系列汇总+详解(包括Fast, Faster)
  15. 常见博客Blog托管提供商评测
  16. 数据存储大讲堂:谈磁盘列阵与RAID技巧
  17. QQ空间打不开,IE里无法运行脚本的解决方案 转自:spookfox.cublog.cn
  18. 领英达到每周好友邀请上限怎么办?领英加好友时要注意哪些细节?置顶推荐
  19. 【微电子】半导体器件物理:0-2半导体器件基本架构与类型、半导体器件与电路技术之发展
  20. 指定文件打成jra包

热门文章

  1. 【VMware】KMODE EXCEPTION NOT HANDLED
  2. 中台产品经理宝典读后感(9):从零开始中台商品中心搭建(上)
  3. Blender Shading 节点材质编辑器着色、添加动画
  4. 外汇交易MT4是什么软件?MT4与MT5有何区别?下载MT4要注意什么?
  5. UE4渲染目标开发实战
  6. 阿里云服务器实现域名解析步骤(入门级教程)
  7. 模板-测试报告-AAA系统android应用V1.1.0测试报告
  8. Q版疯狂大炮游戏android×××
  9. 全电发票这么好,但如何做才能真正享受其释放的价值效能?
  10. PM和PMO到底有什么区别?如何提升组织效能?