百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 技术文章 > 正文

走进 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代表原始图像,GxGy分别代表经横向及纵向边缘检测的图像,通过以上公式就可以分别计算出横向 和 纵向 的梯度值,即GxGy,梯度值越大,边缘就越明显。

Sobel 卷积核算子

三、具体实现

首先建一个场景,放一个可爱的小兔子 bunny 还有一个立方体 cube,并使 bunny 的材质 Shader 中写入 Stencil参考值2,但 cube 不写入参考值。

bunny 材质 Shader 中写入 Stencil 参考值 2,cube 不写入参考值

然后创建后处理 StencilOutlinePostProcessing.cs脚本。

在脚本里我们声明两个材质,一个用于后处理提取 Stencil并转换为图像的材质StencilProcessMat,一个用于后处理边缘检测的描边材质OutlinePostProcessByStencilMat

还有两个渲染纹理,一个用于承接屏幕渲染结果图像的 cameraRenderTexture,一个用于承接颜色图像形式StencilBufferstencilBufferToColor

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

其他比较杂的,算是收集资料的时候顺带补充了知识

  1. 有讲到 depth/stencil buffer 的关系

    https://blog.csdn.net/weixin_34112900/article/details/86272901

  2. CommandBuffer.Blit isn't stencil buffer friendly

    https://forum.unity.com/threads/commandbuffer-blit-isnt-stencil-buffer-friendly.432776/

  3. 有讲到 Graphics 的 activeXXXBuffer 和 SetRenderTarget 用法

    https://www.jianshu.com/p/4e8162ed0c8d

  4. 口袋妖怪 X/Y 制作技法

    https://www.cnblogs.com/TracePlus/p/4299428.html

  5. 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分钟内,破解并盗取你所有加密文件,而且还无法通过软件和补丁修复...这可就有点吓人啦。事情是酱婶的。来自荷兰埃因...

取消回复欢迎 发表评论: