Android OpenGL ES详解——模板Stencil

目录

一、概念

1、模板测试

2、模板缓冲

二、模板测试如何使用

1、开启和关闭模板测试

2、开启/禁止模板缓冲区写入

3、模板测试策略函数

4、更新模板缓冲

5、模板测试应用------物体轮廓

三、模板缓冲如何使用

1、创建模板缓冲

2、使用模板缓冲

3、模板缓冲应用------描边

四、源码下载


一、概念

1、模板测试

在OpenGL中,模板测试(Stencil Testing)是一种用于控制像素绘制的方式。它允许开发者根据模板缓冲区(Stencil Buffer)中的值来决定是否绘制某个像素。模板测试通常用于实现一些高级的图形效果,如阴影、反射、折射等。

当片段着色器处理完片段之后,模板测试(Stencil Test) 就开始执行了,和深度测试一样,它能丢弃一些片段。仍然保留下来的片段进入深度测试阶段,深度测试可能丢弃更多。模板测试基于另一个缓冲,这个缓冲叫做模板缓冲(Stencil Buffer),我们被允许在渲染时更新它来获取有意思的效果。

模板测试与深度测试类似,但在渲染管线中发生在深度测试之前。模板测试也会丢弃掉一些片段,只是丢弃的片段数量比深度测试少。

2、模板缓冲

模板测试是基于一个缓冲区 -- 模板缓冲区(stenci buffer),同理该缓冲区也是由我们创建窗口库创建的,我使用的库是GLFW库。

模板缓冲中的**模板值(Stencil Value)**通常是8位的,因此每个片段/像素共有256种不同的模板值(译注:8位就是1字节大小,因此和char的容量一样是256个不同值)。这样我们就能将这些模板值设置为我们链接的,然后在模板测试时根据这个模板值,我们就可以决定丢弃或保留它了。

StencilBuffer是模板缓冲,可以用来实现一些诸如描边,遮罩之类的操作。

模板测试和深度测试作用类似,模板测试主要是通过对比模板缓冲区来决定是否需要对片段进行丢弃。

在渲染时更新模板缓冲区可以获取很多有趣的效果。

一个模板缓冲区通常使用8位来表示,因此一个像素可以有256中表示方法。

每个窗口库都需要为你设置模板缓冲。GLFW自动做了这件事,所以你不必告诉GLFW去创建它,但是其他库可能没默认创建模板库,所以一定要查看你使用的库的文档。(GLFW 是一个 OpenGL 的应用框架,支持 Linux 和 Windows。GLFW 主要用来处理特定操作系统下的特定任务,例如 OpenGL 窗口管理、分辨率切换、键盘、鼠标以及游戏手柄、定时器输入、线程创建等等。)

上面图示中:首先要使用模板测试,必须先开启glEnabIe(GL STENCIL TEST),然后先使用0来清空模板缓冲区,然后开启矩形片段用1填充,此时我们将1写入模板缓冲区,绘制出来的图形只会显示模板缓冲区为1的像素图形。

模板测试就是通过模板缓冲中记录的模板信息实现的,具体来说就是渲染管线在模板缓冲区中为每个位置的片元保存了一个"模板值",当像素需要进行模板测试时,将设定的模板参考值与该片元对应位置的模板值进行比较,符合条件的片元通过测试,不符合条件的则被丢弃,不进行渲染;

二、模板测试如何使用

无论我们在渲染哪里的片段,模板缓冲操作都允许我们把模板缓冲设置为一个特定值。改变模板缓冲的内容实际上就是对模板缓冲进行写入。在同一次(或接下来的)渲染迭代我们可以读取这些值来决定丢弃还是保留这些片段。模板缓冲区允许我们使用指定的值去设置缓冲区,通过改变模板缓冲区的值实现一些特殊场景,这个一般步骤如下

  • 先开启模板测试,开启模板缓冲写入。
  • 绘制源物体,并改变模板缓冲中的值。
  • 禁止模板缓冲写入。
  • 绘制其他物体,此时可以使用对应的策略:如绘制的时候,丢弃和源物体重合的部分(如,绘制描边)。

使用模板缓冲我们可以基于场景中已经绘制的片段,来决定是否丢弃特定的片段。

1、开启和关闭模板测试

你可以开启GL_STENCIL_TEST来开启模板测试。接着所有渲染函数调用都会以这样或那样的方式影响到模板缓冲。

java 复制代码
glEnable(GL_STENCIL_TEST);

注意:在Android中,只使用这句是无法启用模板测试的,还需要使用下面代码配置GLSurfaceView,因为默认的配置是没有配置模板测试的。

要注意的是,像颜色和深度缓冲一样,在每次循环,你也得清空模板缓冲。

glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);

2、开启/禁止模板缓冲区写入

同时,和深度测试的glDepthMask函数一样,模板缓冲也有一个相似函数。glStencilMask允许我们给模板值设置一个位掩码(Bitmask, 掩码 ‌是一种用于对数据进行编码和解码的技术,通过二进制代码对目标字段进行位与运算,屏蔽当前的输入位,从而实现数据的部分或全部隐藏,以保护敏感信息。掩码在计算机科学和数据处理中有着广泛的应用,包括数据压缩、权限控制、网络编程等‌,掩码在图像处理和对象分割等领域中用于标识和处理特定的图像区域‌。) ,它与模板值进行按位与(AND)运算决定缓冲是否可写。默认设置的位掩码都是1,这样就不会影响输出,但是如果我们设置为0x00,所有写入深度缓冲最后都是0。这和深度缓冲的glDepthMask(GL_FALSE)很类似:

Kotlin 复制代码
// 0xFF == 0b11111111
//此时,模板值与它进行按位与运算结果是模板值,模板缓冲可写
// 每一位写入模板缓冲时都保持原样
glStencilMask(0xFF); 

// 0x00 == 0b00000000 == 0
//此时,模板值与它进行按位与运算结果是0,模板缓冲不可写
// 每一位在写入模板缓冲时都会变成0(禁用写入)
//注意:他不是关闭模板,而是不让模板被写入,关闭模板的话连判断都没了。
glStencilMask(0x00); 

大多数情况你的模板遮罩(stencil mask)写为0x00或0xFF就行,但是最好知道有一个选项可以自定义位遮罩。

3、模板测试策略函数

和深度测试类似,模板测试也有对应的api函数来设置模板测试的策略,如测试失败或者成功的情况下如何处理模板缓冲区。glStencilFunc函数决定一个片段的去留------是否会显示在屏幕上。通过测试则显示,否则不显示。

模板测试提供了两个函数:glStencilFunc and glStencilOp.

java 复制代码
void glStencilFunc(GLenum func, GLint ref, GLuint mask)

函数有三个参数:

  • func :设置模板测试操作。这个测试操作应用到已经储存的模板值和 giStenciFunc 的 ref 值上即判断模板测试func:通过的条件,例如我func设置为GL EQUAL,ref为1,那么模板测试通过的条件就是模板值等于1,可用的选项是:GL_NEVERGL_LEQUALGL_GREATERGL_GEQUALGL_EQUALGL_NOTEQUALGL_ALWAYS。它们的语义和深度缓冲的相似。func设置模板测试成功与否的策略。包括下面几种情况:
func策略 描述
GL_NEVER 每次模板测试都失败
GL_LESS 比参考值更小就通过测试
GL_LEQUAL 小余或等于参考值就通过测试
GL_GREATER 比参考值更大就通过测试
GL_GEQUAL 大于或等于参考值就通过测试
GL_EQUAL 缓冲区的值和参数值ref相等
GL_NOTEQUAL 缓冲区的值和参数值ref不相等
GL_ALWAYS 每次模板测试都成功
  • ref:指定模板测试的参考值,这个值会用来和模板缓冲区中的值进行比较,来判断是否丢弃片段(按照上述func参数设置的类型进行比较)。
  • mask:设置一个掩码,在模板缓冲区和ref参考值进行比较值前,会使用mask做一个按位与的操作,初始化当前值,一般是在为0xFF即可,保留原值。

在上面简单模板的例子里,方程应该设置为:

java 复制代码
glStencilFunc(GL_EQUAL, 1, 0xFF)

它会告诉OpenGL,无论何时,一个片段模板值等于(GL_EQUAL)引用值1,片段就能通过测试被绘制了,否则就会被丢弃。

但是glStencilFunc只描述了OpenGL对模板缓冲做什么,而不是描述我们如何更新缓冲。这就需要glStencilOp登场了。

4、更新模板缓冲

glStencilOp函数决定片段如何改变模板。判断达成某些条件的片段,要如何更新模板。这个函数决定一个片段,是否并如何改变一个模板上的值。

java 复制代码
void glStencilOp(GLenum sfail, GLenum zfail, GLenum szpass)

函数包含三个选项,我们可以指定每个选项的动作:

  • sfail: 如果模板测试失败将采取的动作。
  • zfail: 如果模板测试通过,但是深度测试失败时采取的动作。
  • szppass: 如果深度测试和模板测试都通过,将采取的动作。

每个选项都可以使用下列任何一个动作。

操作 描述
GL_KEEP 保持现有的模板值
GL_ZERO 将模板值置为0
GL_REPLACE 将模板值设置为用glStencilFunc函数设置的ref
GL_INCR 如果模板值不是最大值就将模板值+1
GL_INCR_WRAP GL_INCR一样将模板值+1,如果模板值已经是最大值则设为0
GL_DECR 如果模板值不是最小值就将模板值-1
GL_DECR_WRAP GL_DECR一样将模板值-1,如果模板值已经是最小值则设为最大值
GL_INVERT 按位反转当前模板缓冲区的值

如设置:

glStencilOp(GL_KEEP,GL_KEEP,GL_REPLACE);

则表示模板测试失败时,保持原有模板值,模板测试成功但是深度测试失败时保持原有模板值,两种都成功,则更新模板值为参考值。

glStencilOp函数默认设置为 (GL_KEEP, GL_KEEP, GL_KEEP) ,所以任何测试的任何结果,模板缓冲都会保留它的值。默认行为不会更新模板缓冲,所以如果你想写入模板缓冲的话,你必须像任意选项指定至少一个不同的动作。

使用glStencilFuncglStencilOp,我们就可以指定在什么时候以及我们打算怎么样去更新模板缓冲了,我们也可以指定何时让测试通过或不通过。什么时候片段会被抛弃。glStencilOp一般只设置一次,而glStencilFunc在绘制每个模型前都会设置一次。

5、模板测试应用------物体轮廓

下面我们使用一个案例来分析模板测试。

看了前面的部分你未必能理解模板测试是如何工作的,所以我们会展示一个用模板测试实现的一个特别的和有用的功能,叫做物体轮廓(Object Outlining)

物体轮廓就像它的名字所描述的那样,它能够给每个(或一个)物体创建一个有颜色的边。在策略游戏中当你打算选择一个单位的时候它特别有用。给物体加上轮廓的步骤如下:

  1. 首先开启模板测试,清空模板缓冲区,开启模板缓冲区写入
  2. 在绘制源物体之前设置模板测试操作为GL_ALWAYS,设置参考值为1,掩码为0xFF,这样可以在每次绘制的时候都可以将模板缓冲区写入1。//使用gIStencil0p选项中的GL_REPLACE将模板值设置成glStencilFunc中的ref值
  3. 绘制源物体,写入模板缓冲(源物体对应的模板缓冲区位置写入1)
  4. 禁止模板缓冲区写入并禁止深度测试。
  5. 将源物体放大一点。(即,x,y,z坐标等比放大后的坐标位置,作为源物体的外围描边坐标。)
  6. 使用一个纯颜色的片段着色器对源物体放大后的位置坐标进行绘制。
  7. 绘制源物体,但是此时使用GL_NOTEQUAL 的方式进行模板缓冲区绘制策略
  8. 重新开启深度缓冲区和模板可写入操作。

这个过程将每个物体的片段模板缓冲设置为1,当我们绘制边框的时候,我们基本上绘制的是放大版本的物体的通过测试的地方,放大的版本绘制后物体就会有一个边。我们基本会使用模板缓冲丢弃所有的不是原来物体的片段的放大的版本内容。

场景中的物体边框的绘制方法最后看起来像这样:

glEnable(GL_DEPTH_TEST);
glStencilOp(GL_KEEP, GL_KEEP, GL_REPLACE);  

glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);

glStencilMask(0x00); // 绘制地板时确保关闭模板缓冲的写入
normalShader.Use();
DrawFloor()  

glStencilFunc(GL_ALWAYS, 1, 0xFF);
glStencilMask(0xFF);
DrawTwoContainers();

glStencilFunc(GL_NOTEQUAL, 1, 0xFF);
glStencilMask(0x00);
glDisable(GL_DEPTH_TEST);
shaderSingleColor.Use();
DrawTwoScaledUpContainers();
glStencilMask(0xFF);
glEnable(GL_DEPTH_TEST);

理解这段代码后面的模板测试的思路并不难以理解。如果还不明白尝试再仔细阅读上面的部分,尝试理解每个函数的作用,现在你已经看到了它的使用方法的例子。

这个边框的算法的结果在深度测试教程的那个场景中,看起来像这样:

三、模板缓冲如何使用

使用模板缓存,分为两大部分:

1、创建模板缓冲

  • 创建Stencil,用其他模型,遮罩等的片段,去创建模板缓冲。(遮罩也完全可以不被显示出来,只更新模板)
  • 打开模板缓冲的写入 glStencilMask(0xFF) ,用某些模型,或遮罩图片,写入缓冲。

2、使用模板缓冲

利用创建好的Stencil去和需要被遮罩处理的片段比较,去决定目标片段是否被保留。

3、模板缓冲应用------描边

描边的例子步骤如下:

1、创建模板缓冲

打开模板缓冲的写入和深度测试,以正常的绘制模型和写入模板。

写入原始模型的片段(绘图),同时写入模板缓冲(创建Mask)

2、使用缓冲的部分

关闭模板缓冲写入,防止模板为污染

关闭Z-test深度测试,将描边画在最上面。

将原始模型放大一点,改用单色shader(描边色)

当模板值不是1的时候,写入单色(中间有原模型的部位,不会被写入。没有原模型的部位,被写入。)

注意整体绘制顺序,因为关闭ZBuffer深度缓冲之后,后画的一定会覆盖先画的。

四、源码下载

模板测试StencilTest功能应用演示demo源码下载:

https://download.csdn.net/download/github_27263697/89953195

demo样图如下:

推荐文章

android opengl测试demo opengl模板测试_mob64ca14150f43的技术博客_51CTO博客

https://learnopengl-cn.readthedocs.io/zh/latest/04%20Advanced%20OpenGL/02%20Stencil%20testing/#_1https://zhuanlan.zhihu.com/p/620697689

OpenGL学习笔记------StencilBuffer-腾讯游戏学堂

学习OpenGL ES for Android(十九)--- 模板测试_gles20-CSDN博客

相关推荐
长亭外的少年12 分钟前
Kotlin 编译失败问题及解决方案:从守护进程到 Gradle 配置
android·开发语言·kotlin
JIAY_WX15 分钟前
kotlin
开发语言·kotlin
建群新人小猿3 小时前
会员等级经验问题
android·开发语言·前端·javascript·php
1024小神4 小时前
tauri2.0版本开发苹果ios和安卓android应用,环境搭建和最后编译为apk
android·ios·tauri
兰琛4 小时前
20241121 android中树结构列表(使用recyclerView实现)
android·gitee
Y多了个想法5 小时前
RK3568 android11 适配敦泰触摸屏 FocalTech-ft5526
android·rk3568·触摸屏·tp·敦泰·focaltech·ft5526
NotesChapter6 小时前
Android吸顶效果,并有着ViewPager左右切换
android
_祝你今天愉快7 小时前
分析android :The binary version of its metadata is 1.8.0, expected version is 1.5.
android
暮志未晚Webgl7 小时前
109. UE5 GAS RPG 实现检查点的存档功能
android·java·ue5
麦田里的守望者江7 小时前
KMP 中的 expect 和 actual 声明
android·ios·kotlin