最终效果展示

里世界效果

光照变化效果

里世界效果

效果设计

考虑制作一个通过面片可以看见不一样的世界的“里世界”效果,只有透过特定的“窗户”才能看到那个世界。在这里,这个窗户就是我们的画。其中里面的物体受内部光照影响,内外世界互不影响。

这里就将正常的世界称为外部世界,透过“窗户”看到的世界叫里世界。

制作思路

首先是渲染效果,所谓透过窗户,也就是说让让一些东西只在某一个面片覆盖的地方才渲染。所以这里使用Stencil Buffer这个存在来实现这个效果。

我们首先让场景中其他不透明 (Opaque) 与不透明度检测 (Alpha Test) 的物品渲染完(这里暂时不考虑透明物体,因为透明物体涉及到复杂的排序问题)。然后渲染那个面片。在渲染面片的时候,让所有面片的像素除了输出颜色外,还固定往Stencil Buffer里面的对应像素点写一个值,比如说写入1,同时面片不写入任何深度。然后渲染所有里世界物品,里世界的物体都要经过Stencil Buffer检测,只有当当前像素的Stencil Buffer值为1的时候才能通过检测。也就是说只有被面片覆盖的像素才会渲染里世界的模型。

这里就出现了一个问题,因为没开启面片的深度写入,所以说面片所覆盖的像素的深度值还是外部世界的深度值。而里世界因为要互相遮挡,必须要开启深度检测,所以说我们需要先渲染一个里世界天空盒,开启Stencil Buffer检测单不开启深度测试(也就是说只要是被面片覆盖的像素都一定会绘制),进行深度覆盖,之后在渲染其他里世界物品。

所以说,这里要注意严格控制渲染次序防止出现排序错误。所有的里世界效果都要手动修改渲染次序。

整个渲染的次序如下:

  1. 场景内其他不透明、透明度检测物体都渲染完成;
  2. 渲染面片,正常深度检测但不写入深度(被外部世界中不透明物体遮挡),将通过深度检测的每个像素的Stencil Buffer设置为1,并且输出颜色作为Clear Color;
  3. 渲染里世界天空盒,当 Stencil Buffer 的值等于1的时候通过检测,不做深度检测但写入深度值,将整个面片内的深度覆盖为一个较大的值;
  4. 渲染里世界的物体,当 Stencil Buffer 的值等于1的时候通过检测,正常深度检测正常深度写入。

下面进行一个简单的效果实现。

具体实现

先新建一个场景,并且摆放一些东西(如图)。其中黑色的是上面说的平面,白色的是现实世界的物品,蓝色的是里世界的物品。

场景

我这里所有 shader 都使用 Amplify Shader Editor(ASE)插件进行编写,不过代码部分的实现很简单,我也会放上关键部分的代码实现

先制作平面的shader:

  • 处理渲染次序

    Unity中,不透明 (Opaque) 渲染队列为2000,透明度测试 (Alpha Test) 为2450,透明度混合 (Alpha Blend) 为3000。我们要将平面的渲染队列放在 Alpha Test 之后,所以我们将渲染队列设置为 Alpha Test + 10 ,也就是2460。

    调整渲染队列

    代码实现:

    1
    Tags{ "RenderType" = "Opaque"  "Queue" = "AlphaTest+10" }

    现在在材质中可以看到渲染队列已经正确的变成了2460:

    渲染队列变为2460

  • 进行 Stencil Buffer 的操作

    现在我们要开启 Stencil Buffer 写入,将其的参考值设置为我们想写入的值,这里是 1 。然后将 Comparison 设置为 always ,意思是不进行 Stencil 测试。将 Pass Front 设置为替换,其他都设置为保持。这样只要深度测试通过,就一定会向 Stencil Buffer 中写入 1 这个值。

    代码实现:

    1
    2
    3
    4
    5
    6
    7
    8
    Stencil
    {
    Ref 1
    Comp Always
    Pass Replace
    Fail Keep
    ZFail Keep
    }
  • 处理深度测试

    这里要正常深度测试,但是不进行深度写入:

    深度测试设置

    代码实现:

    1
    2
    ZWrite Off
    ZTest Less
  • 输出颜色

    输出一个固定颜色就可以,这个是作为,类似清屏颜色使用的(虽然说后面会绘制天空盒)。

然后在制作里世界shader:

  • 处理渲染次序

    这里设置成 Alpha Test + 20即可,设置方法跟上面一样。

  • Stancil Buffer 操作

    这里将参考值也设置为1,只有在相等的时候才通过测试,其他情况都不对 Buffer 内容做任何修改。这样就只会渲染在面片后的东西了。

    里世界 Stencil Buffer 设置

    代码实现与上面相同。

  • 处理深度测试

    深度测试不用改,正常测试,开启深度写入。

  • 输出颜色

    正常输出颜色,我这里直接用的表面着色器的 Standard 光照模型。

现在就可以正常制作出 Stencil 的效果了,也会正确被外部世界遮挡。

效果

不过现在还有一个问题就是,处于屏幕后面的里世界物体还是会被外部世界遮挡,这是因为里世界物体是正常做深度测试的,这里下面的球体被地面遮挡了。这就是我前面说要加天空盒覆盖原有深度信息的原因。

深度错误

下面来制作天空盒 shader ,其他的设置都与里世界 shader 一样,只是渲染队列变成了 Alpha Test + 15(平面之后,里世界其他物体之前),关闭深度测试:

里世界天空盒的深度测试设置

这里我为了方便,直接制作 Unlit Shader 输出一个颜色,如果有需求采样一个 Cubemap 即可。然后放一个半球形模型,将天空盒材质附上去。现在就没有任何问题了。这里记得吧天空盒的阴影之类的全关掉。

加了天空盒后

光照设置

现在里世界还是会收到外部世界的光照,并且投射阴影,这是我们不想要的。我们直接新建一个层,将里世界所有物品加加进去。

设置层

然后设置光照中的 Culling Mask 就可以控制光照的影响层了,外部世界的光源取消对 Inner World 层的影响,然后制作里世界光照只影响 Inner world 层即可。

设置光照

至此,里世界这个魔法画作效果就可以制作了。

光照变化效果

效果设计

想要一个效果,就是一个材质,被光照到与平常是两种纹理。可以做魔法画作效果,也可以做类似紫外线照射的效果。

制作思路

有以下两种思路:

  1. 玩家有手电筒,手电筒光照范围是个锥形。则可以把玩家手电筒位置,手电筒方向,聚光角度这三个参数传入 shader ,进行一些计算就可以得知该点是否在锥形内。
  2. Shader 内可以轻松地得到光源的各种属性,而且 shader 会默认为每个光源添加一个Forward Add Pass进行渲染,直接在这个pass上进行操作即可。

很容易看出,思路2不仅可以支持多光源,且计算也少很多,制作调试方便,性能好,因此选择思路2。

在 Unity 的前向渲染管线中,一个物体的 shader 会每个光照都分别执行一遍,其中最主要的平行光照走 Forward Base Pass,其他光照则走 Forward Add Pass。那这样就很简单了,只要让所有走 Forward Add Pass 光照都会更改其的纹理就好了,而且 shader 内也很好获取到光照衰减,思路一下子清晰了起来。

具体实现

一般来讲,Forward Add Pass 一般会设置为 Blend One One 也就是叠加,因为光照本身就是做颜色叠加。但是我们这里并不把他当个光照看,我们要通过光照的衰减值确定哪里需要使用贴图2的信息,所以需要把光照衰减数据作为 Alpha 值进行混合,所以要设置成 Blend DstAlpha OneMinusSrcAlpha 。

这里因为改动比较大,表面着色器不支持,只能用传统的顶点片元进行编写。

最终要的 Forward Add Pass 的片元着色器代码:

1
2
3
4
5
6
7
8
9
10
fixed4 frag(v2f i) : SV_Target
{
float3 worldPos = float3(i.TtoW0.w, i.TtoW1.w, i.TtoW2.w);
fixed3 diffuse = tex2D(_LightTex, i.uv.zw).rgb;

UNITY_LIGHT_ATTENUATION(atten, i, worldPos);
atten = smoothstep(atten, 0, 0.2);

return fixed4(diffuse * atten, atten);
}

这样除了主要的平行光外,其他灯光都会让这这个材质采用 _LightTex 这张贴图进行覆盖。光照变化的效果就制作完成了。