走进 Stencil Buffer 系列 4:Stencil 后处理局部描边
cac55 2024-11-15 16:39 11 浏览 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ㄒ)/~~(咕咕咕
相关推荐
- MIRIX重塑AI记忆:超Gemini 410%,节省99.9%内存,APP同步上线
-
MIRIX,一个由UCSD和NYU团队主导的新系统,正在重新定义AI的记忆格局。在过去的十年里,我们见证了大型语言模型席卷全球,从写作助手到代码生成器,无所不能。然而,即使最强大的模型依...
- 硬盘坏了怎么把数据弄出来对比10种硬盘数据恢复软件
-
机械硬盘或固态硬盘损坏导致数据丢失时,应立即停止对硬盘的读写操作,并根据损坏类型选择逻辑层恢复工具或专业物理恢复服务。紧急处置措施立即停止通电使用:发现硬盘异响、无法识别或数据异常时,需立即断开连接,...
- 蓝宝石B850A WIFI主板新玩法:内存小参调节体验
-
蓝宝石前段时间发布了一款性价比极高的主板:NITRO氮动B850AWIFI主板。这款主板的售价只要1349元,相比普遍1500元以上的B850主板,确实极具竞争力。虽然价格实惠,蓝宝石NITR...
- 内存卡损坏读不出怎么修复?这5个数据恢复工具汇总,3秒挽回!
-
在数字化生活的浪潮中,内存卡凭借小巧便携与大容量存储的特性,成为相机、手机、行车记录仪等设备存储数据的得力助手,承载着无数珍贵回忆与重要文件。然而,当内存卡突然损坏无法读取,无论是误删、格式化、病毒入...
- 内存卡修复不再难,2025年必学的6款软件工具
-
内存卡出现问题时,通常是因为文件系统损坏、物理损坏或病毒感染。通过专业的修复工具,我们可以尝试恢复数据并修复内存卡。内存卡修复利器:万兴恢复专家万兴恢复专家是一款功能强大的数据恢复软件,支持多种设备和...
- 有5款内存卡修复工具汇总,内存卡数据轻松找回!
-
在如今的数字时代,内存卡作为不可或缺的存储介质,广泛应用于相机、手机、行车记录仪等各类设备中,承载着我们珍贵的照片、视频以及重要文件。然而,数据丢失的风险却如影随形,误删、格式化、病毒入侵、硬件故障等...
- 揭秘:如何通过多种方式精准查询内存条型号及规避风险?
-
以下是内存条型号查询的常用方法及注意事项,综合了物理查看、软件检测、编码解析等多种方式:一、物理标签查看法1.拆机查看标签打开电脑主机/笔记本后盖找到内存条,观察标签上的型号标识。例如内存标签通常标...
- 内存卡数据恢复5个工具汇总推荐,轻松找回珍贵记忆!
-
在这个数字化时代,内存卡作为我们存储珍贵照片、重要文件的常用载体,广泛应用于手机、相机、平板电脑等设备。但数据丢失的意外却常常不期而至,误删除、格式化、病毒攻击,甚至内存卡的物理损坏,都可能让辛苦保存...
- 电脑内存智能监控清理,优化性能的实用软件
-
软件介绍Memorycleaner是一款内存清理软件。功能很强,效果很不错。Memorycleaner会在内存用量超出80%时,自动执行“裁剪进程工作集”“清理系统缓存”以及“用全部可能的方法清理...
- TechPowerUp MemTest64:内存稳定性测试利器
-
TechPowerUpMemTest64:内存稳定性测试利器一、软件简介TechPowerUpMemTest64,由知名硬件信息工具GPU-Z的出品公司TechPowerUp发布,是一款专为64位...
- 微软推出AI恶意软件检测智能体Project Ire,精确度高达98%
-
IT之家8月6日消息,当地时间周二,微软宣布推出可自主分析恶意软件的AI检测系统原型——ProjectIre。该项目由微软研究院、Defender研究团队及Discovery&a...
- 农村老木匠常用的20种老工具,手艺人靠它养活一家人,你认识几种
-
生活中的手艺老匠人是非常受到尊敬和崇拜的,特别是在农村曾经的老匠人都是家里的“座上宾”。对于民间传统的手艺人,有一种说法就是传统的八大匠:木匠、泥匠、篾匠、铁匠、船匠、石匠、油匠和剃头匠。木匠的祖始爷...
- 恶意木马新变种伪装成聊天工具诱人点击
-
国家计算机病毒应急处理中心通过对互联网监测发现,近期出现一种恶意木马程序变种Trojan_FakeQQ.CTU。该变种通过伪装成即时聊天工具,诱使计算机用户点击运行。该变种运行后,将其自身复制到受感染...
- 学习网络安全 这些工具你知道吗?
-
工欲善其事必先利其器,在新入门网络安全的小伙伴而言。这些工具你必须要有所了解。本文我们简单说说这些网络安全工具吧!Web安全类web类工具主要是通过各种扫描工具,发现web站点存在的各种漏洞...
- 5分钟盗走你的隐私照片,这个全球性漏洞到底有多可怕?
-
这个时代,大家对电脑出现漏洞,可能已经习以为常。但如果机哥告诉大家,这个漏洞能够在5分钟内,破解并盗取你所有加密文件,而且还无法通过软件和补丁修复...这可就有点吓人啦。事情是酱婶的。来自荷兰埃因...
你 发表评论:
欢迎- 一周热门
- 最近发表
- 标签列表
-
- 如何绘制折线图 (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)