走进 Stencil Buffer 系列 2:非欧空间

作者:阿创
2020-05-06
17 9 1

零、遗留问题

首先让我们解答上一篇中所遗留的问题吧。

上一篇所谓的“边界融合”指的是:在我们视角空间下,相同描边材质的两个物体挨在一起的时候,两个物体中间视线遮挡处的描边边界不见了,看起来就像是给一个物体描边一样。

就如下面这张↓图

这是为什么呢?

答案其实上一篇已经解释过了噢,在上一篇文章里我们假设描边处参考值是1,物体原本模型处参考值是 0

又因为他们所用同一个材质,那么进行模板测试的时候,较前的物体边框参考值 1 与较后物体原本模型参考值 0 进行比较。

10 显然不一样,那么就抛弃较前物体边框的颜色,保留原有较后物体原本模型(纹理)颜色。就是描边边界消融的效果啦。

一、非欧几里得空间

什么是非欧几里得空间?

那就得先解释什么是欧几里得空间。简单来说:我们现实所处在的三维立体空间就是欧几里得空间。

那非欧几里得空间,又简单来说:违反现实三维空间几何规律的空间就可以认为是非欧几里得空间辣。

比如大名鼎鼎的《传送门(Portal)》,还有近期的《笼中窥梦(Moncage)》都是很典型的例子。

《笼中窥梦(Moncage)》游戏

《笼中窥梦(Moncage)》游戏

二、实现原理

1. 我们首先来做个拆解,将上图正方体拆成 6 个面世界(后面也有三个面没画出来)。一个面显示一个世界。


2. 我们再单独拿出一个面世界来说明:

一个面世界在游戏引擎里其实由一个四边形面片(Quad)一个(组)三维物体(GameObjects)组成。如下图:

想要达成非欧几里得的效果,只需要如下设置:

  1. 一个面世界中,只有通过这个四边形面片(Quad),才能看到这个里面的三维物体(GameObjects)。
  2. 各个面世界不相互干扰,一个面只负责显示一个世界。


我们首先解决第一点问题:单独一个面世界的显示

我们如何达到上图的效果呢?

首先在上图中我们发现:四边形面片(Quad)是没有颜色的。像全透明的一样。

在 ShaderLab 中,我们可以使用 ColorMask 指令来控制 Pass 渲染颜色的输出

ColorMask RGBA     // 默认输出 Pass 计算出的所有通道颜色
ColorMask R        // 只输出 Pass 计算出的 R 通道颜色 
    ...
ColorMask 0        // 不输出任何通道颜色

这里我们使用 ColorMask 0 指令 就不会输出任何颜色,就好像全透明一样。


其次,也是最最最重要的设置:只有通过这个四边形面片(Quad),才能看到这个里面的三维物体(GameObjects)

(不用想,这系列都是讲 Stencil Test 模板测试的,绝对和它脱离不了关系 ←_←)。

没有错,又双叒叕是 Stencil Test 模板测试哈哈哈。

其实和轮廓描边本质上原理是一样的:

  1. 首先渲染四边形面片(Quad),并且向 Stencil Buffer 中写入一个 Ref 参考值,假设是 1吧;
  2. 然后为面世界中的所有物体们设置一个 Ref 参考值也是 1,并且使用 Comp Equal 进行模板测试比较。又因为只有在四边形面片(Quad)渲染过的地方,Stencil Buffer 中缓冲值才会 1,使其相等,才能通过模板测试,保留模型本身渲染的颜色(即正常显示出来);
  3. 在其余地方,因为 Ref 参考值和缓冲值不相等,物体渲染出颜色将会被抛弃(即不能显示出来)。


我们再来解决第二点问题:面世界之间互不干扰

为了更好的区分各个面世界之间的边界。我们简单地做了上面↑这样的模型。

要想让面世界之间互不干扰:你显示你的,我显示我的。就像上图所显示那样。

其实很简单,只需要为每个面世界设置不同的 Ref 参考值就好了。

比如左边显示圆球的面世界中,四边形面片(Quad)与其中的物体们(GameObjects)的参考值都设置为 1

右边显示圆柱的面世界中,四边形面片(Quad)与其中的物体们(GameObjects)的参考值都设置为 2

原理和上面是一样的,只有相同参考值参会被显示。就不再赘述了。

三、具体实现

1. 首先创建两个 Shader 文件

StencilGeometry 是给面世界中的物体们(GameObjects)的,

StencilMask 是给面世界的四边形面片(Quad)的。(PS:从名字 Mask 遮罩看出,四边形面片的作用就像遮罩一样)


2. StencilMask 的核心代码

Shader "Unlit/StencilMask"
{
    Properties
    {
        _RefValue("Stencil RefValue",Int) = 0
    }
    SubShader
    {
        //Queue 渲染队列设置到 Geometry-1 是因为想在被遮挡物体渲染之前就进行渲染,写入 stencil 值
        Tags { "RenderType"="Opaque" "Queue"="Geometry-1"}

        //[_RefValue] 就是我们自己设置的参考值
        //Always 表示了无论如何都通过模板测试
        //Replace 表示通过模板测试后用参考值替换掉 Stencil Buffer 中此像素原有的 stencil 值(缓冲值)
        Stencil{
            Ref [_RefValue]
            Comp Always
            Pass Replace
        }

        //关闭深度写入,因为是四边形面片 Queue 较小,较先渲染
        //如果还开启深度写入,后续的面世界内的物体都不能通过深度测试(Depth Test),就都不会被显示出来了。
        ZWrite Off

        //关闭颜色写入
        ColorMask 0

        Pass{
        //普通正常 Unlit 的 Pass
        }
    }
}

以上基本有注释的地方,都是重点。具体原理就不再赘述了。

额外需要注意的地方:

  1. 渲染顺序 Queue 标签。
  2. 关闭 Zwrite 深度写入。


3. StencilGeometry 核心代码

Shader "Unlit/StencilGeometry"
{
    Properties
    {
        _MainTex ("Texture", 2D) = "white" {}
        _Diffuse("Color Tint",Color) = (1,1,1,1)
        _RefValue("Stencil RefValue",Int) = 0
    }
    SubShader
    {
        //除了要注意 Queue 要比 StencilMask 的要大,正常设置就好。
        Tags { "RenderType"="Opaque"  "Queue"="Geometry"}

        //[_RefValue] 就是我们自己设置的参考值
        //Equal 表示了只有和缓冲值相等才通过测试,物体才能被显示出来
        //Keep 表示通过模板测试后,保留原有缓冲值。
        Stencil{
            Ref [_RefValue]
            Comp Equal
            Pass Keep
        }

        Pass{
            //面世界内物体正常的渲染
        }

    }
}

同上 基本有注释的地方,都是重点。具体原理就不再赘述了。


四、配置与展示

1. 各面世界的材质

我们需要为每个面世界单独制作不同 Ref 材质。

就比如其中一个面世界的 Geometry 和 Mask 材质中的 Stencil RefValue 参考值参数是 2


其他各个面世界的参考值都设置为不一样的,就可以得到以下效果辣~


本章参考资料:

  1. https://www.alanzucconi.com/2015/12/09/3873/

五、下一章预告

Stencil 原理的镜面反射!!!