走进 Stencil Buffer 系列 4:Stencil 后处理局部描边
cac55 2024-11-15 16:39 13 浏览 0 评论
一、前言
我们之前已经介绍了一种几何过程式描边方法了。几何过程式描边可以很好的为不同模型设置不同的描边参数(描边颜色,宽度等等),不过也正是如此,要为每个模型都额外渲染一遍描边模型,性能上花费比较多。而有另外一种描边方法就是基于屏幕图像后处理描边方法,它只需要对一张屏幕图像进行边缘检测,无论模型多么复杂,计算量也是恒定的,也就节省了性能开销。
屏幕图形后处理比较常见的是在渲染的最后的阶段,拿到屏幕已经渲染的结果(一张 2D 图像),再对其进行图像处理,这也是“后处理”的这个名字来源。不过这样一来对整一张屏幕图像进行处理,有些地方我们不太希望被处理的地方也会被“误操作”了。比如在下图《英雄联盟(LOL)》游戏里,我们只想对英雄与小兵进行描边,而场景背景保持不变。那我们该怎么办呢?
上图没有描边,下图只针对小兵描边
没错,这时又需要请出我们的 Stencil Test 啦![1]
注意因为这是 Stencil
系列的文章,对于涉及到的屏幕后处理和图像边缘检测算法,不会太过于全面地介绍的相关知识。如果大家有看不太懂的地方,可能需要去查找一些屏幕后处理相关的资料了。
二、实现思路
我们主要思路是:首先让所有需要描边的物体在渲染的时候,将 Stencil 参考值写入 Stencil Buffer 中。全部写入完成之后,我们就把 Stencil Buffer 提取出来转换成一直图像,并使得图像上只有 Stencil 值的地方有颜色。然后把这张图像传入屏幕后处理所用自定义提取 Shader 中,根据 Sobel 边缘检测算法对其边缘检测,检测出边缘后与原屏幕图像进行叠加就完成了。
我们再来分析一下其中的技术细节。
1、对于 Stencil
参考值写入用一个Stencil
指令就 ok 了。
2、将 Stencil Buffer 提取并转换成图像。我们需要借助一张渲染纹理 RenderTexture [2],渲染纹理这个名字和“渲染到纹理”技术相关。通常渲染结果都是直接输出到屏幕窗口帧缓冲中,而渲染到纹理技术,可以把渲染结果渲染到一张纹理中(即渲染纹理)。这也是屏幕后处理的核心技术。
通常需要借助 Graphics.Blit (Texture source, RenderTexture dest,Material mat) 函数将屏幕渲染结果通过某个材质的 Shader 处理后搬运到目标渲染纹理中,其中 Blit
函数会把source
设置为材质的 Shader 中的_MainTex
。而这个 Shader 就是我们提取 StencilBuffer 为图像的关键。我们可以对屏幕图像里每一个像素检测 Stencil 值,如果相等就渲染一个固定颜色(比如白色RBGA(1,1,1,1)
),否者就不进行任何渲染(RBGA(0,0,0,0)
),由此渲染到一张渲染纹理中就完成对 StencilBuffer 提取转换图像 [3]。
3、 Sobel 边缘检测算法。边缘检测的目的是标识数字图像中亮度变化明显的点,即对图像用 Soebl 卷积核进行卷积运算 [4]。A
代表原始图像,Gx
和Gy
分别代表经横向及纵向边缘检测的图像,通过以上公式就可以分别计算出横向 和 纵向 的梯度值,即Gx
和Gy
,梯度值越大,边缘就越明显。
Sobel 卷积核算子
三、具体实现
首先建一个场景,放一个可爱的小兔子 bunny 还有一个立方体 cube,并使 bunny 的材质 Shader 中写入 Stencil
参考值2
,但 cube 不写入参考值。
bunny 材质 Shader 中写入 Stencil 参考值 2,cube 不写入参考值
然后创建后处理 StencilOutlinePostProcessing.cs脚本。
在脚本里我们声明两个材质,一个用于后处理提取 Stencil
并转换为图像的材质StencilProcessMat,一个用于后处理边缘检测的描边材质OutlinePostProcessByStencilMat;
还有两个渲染纹理,一个用于承接屏幕渲染结果图像的 cameraRenderTexture,一个用于承接颜色图像形式StencilBuffer
的stencilBufferToColor。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class StencilOutlinePostProcessing : MonoBehaviour
{
//用于后处理描边的材质
public Material OutlinePostProcessByStencilMat;
//用于提取出纯颜色形式的 StencilBuffer 的材质
public Material StencilProcessMat;
//屏幕图像的渲染纹理
private RenderTexture cameraRenderTexture;
//纯颜色形式的 StencilBuffer
private RenderTexture stencilBufferToColor;private Camera mainCamera;
}
然后,就是初始化部分,两个渲染纹理都设置为一个深度缓冲区中的位数是 24 位的渲染纹理,(可选 0,16,24;但只有 24 位具有模板缓冲区),是因为 24 位缓冲区里包括了 16 为的深度缓冲 depthBuffer
,和 8 位的模板缓冲stencilBuffer
。并且对用于边缘检测的OutlinePostProcessByStencilMat材质传入了stencilBufferToColor即后面用来承载颜色图像形式的StencilBuffer
渲染纹理。
void Start
{
mainCamera = GameObject.FindWithTag("MainCamera").GetComponent<Camera>;
//创建一个深度缓冲区中的位数是 24 位的渲染纹理,(可选 0,16,24;但只有 24 位具有模板缓冲区)
cameraRenderTexture = new RenderTexture(Screen.width,Screen.height,24);
//因为无法直接获得 Stencil Buffer,
//将 renderTexture 中的被 Stencil 标记的像素转换成一张纯颜色的渲染纹理
stencilBufferToColor = new RenderTexture(Screen.width,Screen.height,24);OutlinePostProcessByStencilMat.SetTexture("_StencilBufferToColor",stencilBufferToColor);
}
然后脚本的后处理部分。这里要特别注意一下,通常情况后处理下都是在 void OnRenderImage(RenderTexture src, RenderTexture dest) 函数 内操作的,不过经过实验和资料查询 [5],在调用 OnRenderImage
之前,就已经把src
中的 Stencil buffer 清除掉了。这真是一个致命伤啊...那我们该怎么办呢?
我们来看看 Unity 生命周期的 Scene rendering 渲染阶段 [6]
在 OnRenderImage函数前还有OnPostRender函数,那我们的逻辑可以放到OnPostRender函数里,从而实现屏幕后处理效果。还要注意一点的是OnPostRender函数是没有参数的,即意味着我们要自己去获得屏幕图像。而OnPreRender函数在照相机开始渲染场景之前调用,我们可以在OnPreRender中就设置摄像机渲染的屏幕图像目标是我们设定创建的cameraRenderTexture。
好的,接下来就是我们的后处理部分代码。
void OnPreRender
{
//将摄像机的渲染结果传到 cameraRenderTexture 中mainCamera.targetTexture = cameraRenderTexture;
}
void OnPostRender
{
// 意味着 camera 渲染结果直接交付给 FramBuffer
mainCamera.targetTexture = ;
//设置 Graphics 的渲染操作目标为 stencilBufferToColor
//即 Graphics 的 activeColorBuffer 和 activeDepthBuffer 都是 stencilBufferToColor 里的
Graphics.SetRenderTarget(stencilBufferToColor);
//清除 stencilBufferToColor 里的颜色和深度缓冲区内容,并设置默认颜色为(0,0,0,0)
GL.Clear(true,true,new Color(0,0,0,0));
//设置 Graphics 的渲染操作目标
//即 Graphics 的 activeColorBuffer 是 stencilBufferToColor 的 ColorBuffer
//Graphics 的 activeDepthBuffer 是 cameraRenderTexture 的 depthBuffer
Graphics.SetRenderTarget(stencilBufferToColor.colorBuffer,cameraRenderTexture.depthBuffer);
//提取出纯颜色形式的 StencilBuffer:
//将 cameraRenderTexture 通过 StencilProcessMat 材质提取出到 Graphics.activeColorBuffer
//即提取到 stencilBufferToColor 中
Graphics.Blit(cameraRenderTexture,StencilProcessMat);
//将 cameraRenderTexture 通过 OutlinePostProcessMat 材质
//并与材质中的 _StencilBufferToColor 进行边缘检测操作
//最后输出到 FrameBuffer( 意味着直接交付给 FramBuffer)Graphics.Blit(cameraRenderTexture, as RenderTexture,OutlinePostProcessByStencilMat);
}
在 OnPreRender
中我们设置了摄像机的渲染目标纹理。
而后处理的重点在 OnPostRender中,首先我们把Graphics
的渲染激活操作目标为stencilBufferToColor,并清除stencilBufferToColor
里的颜色和深度缓冲区内容,并设置默认颜色为RGBA(0,0,0,0)
。随后又设置Graphics
的激活操作目标,写入color
的目标是stencilBufferToColor.colorBuffer,测试使用的 depth buffer 的数据来源是cameraRenderTexture.depthBuffer。
接下来就是提取出纯颜色形式的 StencilBuffer
了,用Blit
函数将cameraRenderTexture通过StencilProcessMat模板测试材质把StencilBuffer
提取出到stencilBufferToColor.colorBuffer 中。
StencilProcessMat的作用就是对cameraRenderTexture.depthBuffer进行模板测试 Stencil Test,如果相等才写入我们自定义的_StencilColor颜色 (白色),否者为RGBA(0,0,0,0)
。
StencilProcessMat的代码如下:
Shader "Unlit/StencilProcess"
{
Properties
{
_MainTex ("Texture", 2D) = "white" {}
_StencilColor("StencilBuffer Color",Color)=(1,1,1,1)
_RefValue("Ref Value",Int)=2
}
SubShader
{
Stencil{
Ref [_RefValue]
Comp Equal
}
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
struct appdata
{
float4 vertex : POSITION;
};
struct v2f
{
float4 vertex : SV_POSITION;
};
sampler2D _MainTex;
fixed4 _StencilColor;
v2f vert (appdata v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
return o;
}
fixed4 frag (v2f i) : SV_Target
{
return _StencilColor;
}
ENDCG
}}
}
我们在 Frame Debugger 中可以查看到这个颜色图像形式的 StencilBuffer
:
颜色图像形式的 StencilBuffer
随后就到边缘检测和原图像叠加了,将 cameraRenderTexture 通过OutlinePostProcessMat 材质处理,并与材质中的_StencilBufferToColor 进行边缘检测操作。
//将 cameraRenderTexture 通过 OutlinePostProcessMat 材质//并与材质中的 _StencilBufferToColor 进行边缘检测操作//最后输出到 FrameBuffer( 意味着直接交付给 FramBuffer)Graphics.Blit(cameraRenderTexture, as RenderTexture,OutlinePostProcessByStencilMat);
用于边缘检测和原屏幕图像叠加的 OutlinePostProcessMat材质 Shader 代码如下:
Shader "Unlit/OutlinePostProcessByStencil"
{
Properties
{
_MainTex ("Texture", 2D) = "white" {}
_EdgeColor("Edge Color",Color)= (1,1,1,1)
}
SubShader
{
ZTest Always Cull Off ZWrite Off
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
struct v2f
{
float2 uv[9] : TEXCOORD0;
float4 pos : SV_POSITION;
};
sampler2D _MainTex;
sampler2D _StencilBufferToColor;
float4 _StencilBufferToColor_TexelSize;
float4 _EdgeColor;
v2f vert (appdata_img v)
{
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
half2 uv = v.texcoord;
o.uv[0] = uv + _StencilBufferToColor_TexelSize.xy * half2(-1, -1);
o.uv[1] = uv + _StencilBufferToColor_TexelSize.xy * half2(0, -1);
o.uv[2] = uv + _StencilBufferToColor_TexelSize.xy * half2(1, -1);
o.uv[3] = uv + _StencilBufferToColor_TexelSize.xy * half2(-1, 0);
o.uv[4] = uv + _StencilBufferToColor_TexelSize.xy * half2(0, 0);
o.uv[5] = uv + _StencilBufferToColor_TexelSize.xy * half2(1, 0);
o.uv[6] = uv + _StencilBufferToColor_TexelSize.xy * half2(-1, 1);
o.uv[7] = uv + _StencilBufferToColor_TexelSize.xy * half2(0, 1);
o.uv[8] = uv + _StencilBufferToColor_TexelSize.xy * half2(1, 1);
return o;
}
float SobelEdge(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};
float edge = 0;
float edgeY = 0;
float edgeX = 0;
float luminance =0;
for(int it=0; it<9; it++){
luminance = tex2D(_StencilBufferToColor,i.uv[it]).a;
edgeX += luminance*Gx[it];
edgeY += luminance*Gy[it];
}
edge = 1 - abs(edgeX) - abs(edgeY);
return edge;
}
fixed4 frag (v2f i) : SV_Target
{
fixed4 sourceColor = tex2D(_MainTex, i.uv[4]);
float edge = SobelEdge(i);
return lerp(_EdgeColor,sourceColor,edge);
}
ENDCG
}}
}
在 Shader 最后根据边缘检测出来的 edge,对原图像和边缘描边颜色进行插值,我们就搞定了。
只针对 Stencil 参考值为 2 的 bunny 描边
四、其他效果展示
如果我们让 cube 的材质 Shader 也写入 Stencil
值,并且是和小兔子 bunny 的Stencil
值不同(比如是1
),但用于StencilBuffer
提取的材质 Shader 还是用和 bunny 相同的2
进行模板测试的话,提取出来的颜色图像形式的StencilBuffer
长这样:
cube 写入值 1,bunny 写入 2, StencilProcessMat 模板测试值为 2 的 Stencil Buffer
描边效果长这样:
cube 写入值 1,bunny 写入 2,StencilProcessMat 模板测试值为 2 的描边效果
为啥会这样?有知道的同学欢迎在评论区留言噢~~(看看能钓到多少活鱼儿)
五、下一章预告
Stencil 后处理原理的传送门视觉效果!!!
参考资料和引用
[1] 《英雄联盟 LoL》中后备的小兵英雄后处理 Stencil 描边方法https://technology.riotgames.com/news/trip-down-lol-graphics-pipeline
[2] Unity 手册渲染纹理介绍https://docs.unity3d.com/Manual/class-RenderTexture.html
[3] 乐园:利用 StencilBuffer 实现局部后处理描边https://zhuanlan.zhihu.com/p/95747680
[4] Unity Shader - 边缘检测https://zhuanlan.zhihu.com/p/138561005
[5] UWA:OnRenderImage 提问https://answer.uwa4d.com/question/5e153e7afd2e373ffa7eaae5
[6] Unity 生命周期的 Scene rendering 渲染阶段https://docs.unity3d.com/Manual/ExecutionOrder.html
其他比较杂的,算是收集资料的时候顺带补充了知识
有讲到 depth/stencil buffer 的关系
https://blog.csdn.net/weixin_34112900/article/details/86272901
CommandBuffer.Blit isn't stencil buffer friendly
https://forum.unity.com/threads/commandbuffer-blit-isnt-stencil-buffer-friendly.432776/
有讲到 Graphics 的 activeXXXBuffer 和 SetRenderTarget 用法
https://www.jianshu.com/p/4e8162ed0c8d
口袋妖怪 X/Y 制作技法
https://www.cnblogs.com/TracePlus/p/4299428.html
Unity 后处理 性能优化
https://zhuanlan.zhihu.com/p/39850106
结尾碎碎念
啊,这篇好长,写了两天好久。看了一下之前的文章排版也是惨不忍睹,瞎琢磨了一下下排版(感觉还行吧。。吧)。希望到时候投稿不用麻烦小编操心改排版就好了。
后续可能做做其他系列 Shader 文章,但也不一定,有可能是零碎的 Shader 效果。
临近学期末,作业也越来越多,当初定下一星期一篇真是越来越难了/(ㄒoㄒ)/~~(咕咕咕
相关推荐
- Mac电脑强制删除任何软件方法-含自启动应用
-
对于打工者来说,进入企业上班使用的电脑大概率是会被监控起来,比如各种流行的数据防泄漏DLP,奇安信天擎,甚至360安全卫士,这些安全软件你想卸载是非常困难的,甚至卸载后它自己又安装回来了,并且还在你不...
- Linux基础知识 | 文件与目录大全讲解
-
1.linux文件权限与目录配置1.文件属性Linux一般将文件可存取的身份分为三个类别,分别是owner/group/others,且三种身份各read/write/execute等权限文...
- 文件保护不妥协:2025 年 10 款顶级加密工具推荐
-
数据安全无小事,2025年这10款加密工具凭借独特功能脱颖而出,从个人到企业场景全覆盖,第一款为Ping32,其余为国外英文软件。1.Ping32企业级加密核心工具,支持200+文件格...
- 省心省力 一个软件搞定系统维护_省心安装在哪里能找到
-
◆系统类似于我们居住的房间,需要经常打理才能保持清洁、高效。虽然它本身也自带一些清理和优化的工具,但借助于好用的第三方工具来执行这方面的任务,会更让人省心省力。下面笔者就为大家介绍一款集多项功能于一身...
- JAVA程序员常用的几个工具类_java程序员一般用什么软件写程序
-
好的工具做起事来常常事半功倍,下面介绍几个开发中常用到的工具类,收藏一下,也许后面真的会用到。字符串处理:org.apache.commons.lang.StringUtilsisBlank(Char...
- 手工解决Windows10的若干难题_windows10系统卡顿怎么解决
-
【电脑报在线】很多朋友已经开始使用Win10,估计还只是测试版本的原因,使用过程中难免会出现一些问题,这里介绍解决一些解决难题的技巧。技巧1:让ProjectSpartan“重归正途”从10074...
- System32文件夹千万不能删除,看完这篇你就知道为什么了
-
C:\Windows\System32目录是Windows操作系统的关键部分,重要的系统文件存储在该目录中。网上的一些恶作剧者可能会告诉你删除它,但你不应该尝试去操作,如果你尝试的话,我们会告诉你会发...
- Windows.old 文件夹:系统备份的解析与安全删除指南
-
Windows.old是Windows系统升级(如Win10升Win11)或重装时,系统自动在C盘创建的备份文件夹,其核心作用是保留旧系统的文件、程序与配置,为“回退旧系统”提供保...
- 遇到疑难杂症?Windows 10回收站问题巧解决
-
回收站是Windows10的一个重要组件。然而,我们在使用过程中,可能会遇到一些问题。例如,不论回收站里有没有文件,都显示同一个图标,让人无法判别回收站的空和满的真实情况;没有了像Windows7...
- 卸载软件怎么彻底删掉?简单几个步骤彻底卸载,电脑小白看过来
-
日常工作学习生活中,我们需要在安装一些软件程序,但随着软件的更新迭代速度,很多时候我们需要重新下载安装新的程序,这时就需要将旧的一些软件程序进行卸载。但是卸载软件虽然很简单,但是很多小伙伴们表示卸载不...
- 用不上就删!如何完全卸载OneDrive?
-
作为Windows10自带的云盘,OneDrive为资料的自动备份和同步提供了方便。然而,从隐私或其他方面考虑,有些人不愿意使用OneDrive。但Windows10本身不提供直接卸载OneDri...
- 【Linux知识】Linux下快速删除大量文件/文件夹方法
-
在Linux下,如果需要快速删除大量文件或文件夹,可以使用如下方法:使用rm命令删除文件:可以使用rm命令删除文件,例如:rm-rf/path/to/directory/*这个命令会递...
- 清理系统不用第三方工具_清理系统垃圾用什么软件
-
清理优化系统一定要借助于优化工具吗?其实,手动优化系统也没有那么神秘,掌握了方法和技巧,系统清理也是一件简单和随心的事。一方面要为每一个可能产生累赘的文件找到清理的方法,另一方面要寻找能够提高工作效率...
- 系统小技巧:软件卸载不了?这里办法多
-
在正常情况下,我们都是通过软件程序组中的卸载图标,或利用控制面板中的“程序和功能”模块来卸载软件的。但有时,我们也会发现利用卸载图标无法卸载软件或者卸载图标干脆丢失找不到了,甚至控制面板中卸载软件的功...
- 麒麟系统无法删除文件夹_麒麟系统删除文件权限不够
-
删除文件夹方法例:sudorm-rf文件夹名称。删除文件方法例:sudorm-r文件名包括扩展名。如果没有权限,给文件夹加一下权限再删。加最高权限chmod775文件名加可执行权限...
你 发表评论:
欢迎- 一周热门
- 最近发表
- 标签列表
-
- 如何绘制折线图 (52)
- javaabstract (48)
- 新浪微博头像 (53)
- grub4dos (66)
- s扫描器 (51)
- httpfile dll (48)
- ps实例教程 (55)
- taskmgr (51)
- s spline (61)
- vnc远程控制 (47)
- 数据丢失 (47)
- wbem (57)
- flac文件 (72)
- 网页制作基础教程 (53)
- 镜像文件刻录 (61)
- ug5 0软件免费下载 (78)
- debian下载 (53)
- ubuntu10 04 (60)
- web qq登录 (59)
- 笔记本变成无线路由 (52)
- flash player 11 4 (50)
- 右键菜单清理 (78)
- cuteftp 注册码 (57)
- ospf协议 (53)
- ms17 010 下载 (60)