走进 Stencil Buffer 系列 3:镜面反射

走进 Stencil Buffer 系列 3:镜面反射

首页休闲益智镜面灯光反射更新时间:2024-04-30

零、前言

镜面反射是游戏里十分常见又比较麻烦的需求,大多情况都需要额外创建一个摄像机,根据镜面镜像反转位置来渲染镜子中的内容。

不过我们如果基于 Stencil原理来操作,就可以不需要额外创建摄像机就可以实现镜面效果了噢!

相信大家都看了前几章后(应该)(文章链接),对于模板 Stencil作用会有个感性的理解:遮罩作用。那这篇文章将使用模板Stencil进行镜面区域限定,配合模型顶点镜面反转,来实现镜面反射的效果。

一、实现思路

我们先来想一想真实世界中镜子成像的原理 :太阳或者灯的光照射到人或物体的身上,随后人或物体又反射这些光(大部分是漫反射)射向到镜面上。平面镜又将光镜面反射到人的眼睛里,因此我们看到了自己或物体在平面镜中的虚像。

我们分析得出一下三点特征:

  1. 假设镜子光滑的是完美镜面反射(即光只改变方向不改变光的颜色),在镜子里可以看到的物体(虚像)和实际的物体(实体)外观细节(纹理颜色)是一模一样的,因为都是漫反射光的结果。

  2. 因为是镜面反射成像,虚像和实体之间会关于镜子平面互相对称。

  3. 镜子成的像只能在镜子里面看到(看起来是废话哈哈,不过这正是 Stencil模板发挥作用的地方噢)。

第一点,对于我们来说是个好消息。既然纹理颜色是一样的,我们可以使用相同内容的两个 Pass将物体渲染两遍就好了。

第二点,是一个比较难搞又重要的问题。我们需要让它们关于镜子平面互相对称才行。这怎么做呢?

(这里要十分感谢群里 Colin 和其他大佬们提供的思路)

贴张图来展示一下:关于镜子平面互相对称,只需要构建一个”Wrold“ To ”MirrorWorld“ Matrix(世界转换到镜子世界的矩阵)将物体关于镜子 Y轴对称反转就可以了。

“Wrold” To “MirrorWorld” Matrix(世界转换到镜子世界的矩阵)具体构建思路如下:

  1. 在镜子表面的中心放一个新的空 GameObject,让其 Local 坐标系下的 Y轴指向镜子外面。

  2. 用其 TransformworldToLocalMatrix矩阵将物体从世界坐标系转换至以镜子为中心的本地坐标系;

  3. 然后构建一个 Y轴反转矩阵(即Y变成-Y)左乘上面得到的worldToLocalMatrix矩阵;

  4. 最后再用其 TransformlocalToWorldMatrix矩阵左乘以上的矩阵。

第三点,相信大家都看过前面几篇文章后,可能会有个体会:模板 Stencil 的效果可以大致理解为一个遮罩效果,使用遮罩来限制某些区域(像素)的显示。

那我们的镜子模型就是这个遮罩,限制虚像也就是我们反转后的模型显示的区域。

二、具体实现

由上面所说的思路,我们来搭个框架,讲讲核心代码。

1、被镜面反射的物体 Shader

创建 Shader 和材质给到要被镜面反射的物体身上

然后就像上面所说的,在 Shader 代码中虚像和实体先是一样的 Pass。(其实顶点着色器有点不一样,后面有提到)

Shader "Custom/StencilBufferTwoPassReflection"
{
Properties
{
_MainTex ("Texture", 2D) = "white" {}
}
SubShader
{
Tags { "RenderType"="Opaque" }
LOD 100

Pass

{

//这里渲染虚像的 Pass,正常的渲染

}

Pass

{

//这里渲染实像的 Pass,正常的渲染

}
}
}

2、虚像模型关于镜面对称反转

我们先再镜子表面中心创建一个空物体命名 WtoMW_Object,并使其 Local 坐标系下Y轴朝向镜面外部。

并在 WtoMW_Object上挂一个脚本,来构建并向 Shader 传递“Wrold” To “MirrorWorld” Matrix(世界转换到镜子世界的矩阵)。

具体脚本代码如下:

using System.Collections;

using System.Collections.Generic;

using UnityEngine;

//Set World to Mirror World Matrix

public class SetWtoMWMatrix : MonoBehaviour

{

//WtoMW_Object 的 transform;
Transform refTransform;
//”Wrold“ To ”MirrorWorld“ Matrix(世界转换到镜子世界的矩阵)
Matrix4x4 WtoMW;
Material material;
//Y 轴对称反转矩阵
Matrix4x4 YtoNegativeY = new Matrix4x4(
new Vector4(1, 0, 0, 0),
new Vector4(0, -1, 0, 0),
new Vector4(0, 0, 1, 0),
new Vector4(0, 0, 0, 1));

private void Start
{
material = GetComponent<MeshRenderer>.sharedMaterial;
refTransform = GameObject.Find("WtoMW_Object").transform;

}

void Update
{
WtoMW = refTransform.localToWorldMatrix * YtoNegativeY * refTransform.worldToLocalMatrix;
material.SetMatrix("_WtoMW", WtoMW);
}}

3、应用镜面对称反转矩阵

这时我们被镜面反射的物体 ShaderShader代码也要更新一下,来接收与使用脚本传递来的矩阵。

我们声明了 float4x4类型的_WtoMW矩阵,来接受脚本传递来的矩阵。

并在渲染虚像 Pass里的顶点着色器使用此矩阵,将顶点从世界空间转换至镜子空间。

具体看代码注释:

Shader "Custom/StencilBufferTwoPassReflection"

{

Properties
{
_MainTex ("Texture", 2D) = "white" {}
}
SubShader
{
Tags { "RenderType"="Opaque" "LightMode"="ForwardBase" "Queue"="Geometry" }

//这里是其他变量的声明..

//声明 float4x4 类型的 _WtoMW 矩阵,来接受脚本传递来的矩阵 float4x4 _WtoMW;

float4x4 _WtoMW;

//这里渲染虚像的 Pass
Pass
{
//这里是一些设置..

//顶点函数
v2f vert (appdata v)
{

v2f o;

//首先将模型顶点转换至世界空间坐标系

float4 worldPos = mul(unity_ObjectToWorld,v.vertex);

//再把顶点从世界空间转换至镜子空间

float4 mirrorWorldPos = mul(_WtoMW,worldPos);

//最后就后例行把顶点从世界空间转换至裁剪空间

o.vertex = mul(UNITY_MATRIX_VP,mirrorWorldPos);

//再把顶点从世界空间转换至镜子空间
float4 mirrorWorldPos = mul(_WtoMW,worldPos);

//最后就后例行把顶点从世界空间转换至裁剪空间

o.vertex = mul(UNITY_MATRIX_VP,mirrorWorldPos);


o.uv = TRANSFORM_TEX(v.uv, _MainTex);
// Transform the normal from object space to world space
o.worldNormal = mul(v.normal, (float3x3)unity_WorldToObject);
return o;
}

//frag 函数和实体的是一样的..

}

Pass
{
//这里渲染实体的 Pass
}

}

}

4、为虚像的 Pass添加指令

我们更新一下 StencilBufferTwoPassReflection被镜面反射的物体 Shader 代码:

为虚像的 Pass添加StencilZTest AlwaysCull Front指令。

Stencil里边的指令老生常谈了,原理和上一章的非欧世界内的物体一模一样,虚像在其余地方时,因为Ref参考值和缓冲值不相等,物体渲染出颜色将会被抛弃(即不能显示出来)。注释里也有详细解释。

需要注意的是经过镜像反转,位置发生了变换,位置上陷入了镜子世界中。所以默认情况下深度测试会失败。

虚像模型正反面也发生了变换,原来模型的正面现在变成虚像的背面,模型的背面现在变成虚像的正面,而恰恰 Unity 默认会剔除掉模型的背面,只显示模型的正面。也就是说,虚像的正面将会被剔除掉,只显示背面,这显然是不正确的。

所以我们通过以下两个指令修复这些错误:

ZTest Always指令作用是:无论深度测试是什么结果都算通过深度测试。这样就避免了因为深度测试失败而不能显示。

Cull Front指令的作用是 :剔除掉模型的正面(即虚像的背面),显示模型的反面(即虚像的正面)。

Shader "Unlit/StencilBufferTwoPassReflection"

{

Properties
{
_MainTex("Main Tex",2D)= "white"{}
_Color("Color Diffuse",Color) = (1,1,1,1)
_RefValue("Ref Value",Int) = 0
}
SubShader
{
Tags { "RenderType"="Opaque" "LightMode"="ForwardBase" "Queue"="Geometry" }

//这里是虚像的渲染
Pass

{

//[_RefValue] 就是我们自己设置的参考值

//Equal 表示了只有和缓冲值相等才通过测试,物体才能被显示出来

//Keep 表示通过模板测试或深度测试失败后,都保留原有缓冲值.

Stencil{

Ref [_RefValue]

Comp Equal

Pass keep

ZFail keep

}

//因为虚像经过镜像反转,位置也发生了变换,陷入了镜子世界中。所以势必会深度测试失败。

//作用无论深度测试是什么结果都算通过深度测试。

ZTest Always

//剔除掉模型的正面(即虚像的背面),显示模型的反面(即虚像的正面)。

Cull Front

//Equal 表示了只有和缓冲值相等才通过测试,物体才能被显示出来

//Keep 表示通过模板测试或深度测试失败后,都保留原有缓冲值.

Stencil{

Ref [_RefValue]

Comp Equal

Pass keep

ZFail keep

}

//因为虚像经过镜像反转,位置也发生了变换,陷入了镜子世界中。所以势必会深度测试失败。

Ref [_RefValue]
Comp Equal
Pass keep
ZFail keep
}

//因为虚像经过镜像反转,位置也发生了变换,陷入了镜子世界中。所以势必会深度测试失败。

//作用无论深度测试是什么结果都算通过深度测试。

ZTest Always

//剔除掉模型的正面(即虚像的背面),显示模型的反面(即虚像的正面)。

Cull Front


//剔除掉模型的正面(即虚像的背面),显示模型的反面(即虚像的正面)。
Cull Front

//这里是其他变量的声明和设置....

//声明 float4x4 类型的 _WtoMW 矩阵,来接受脚本传递来的矩阵
float4x4 _WtoMW;

//顶点函数
v2f vert (appdata v)
{

v2f o;

//首先将模型顶点转换至世界空间坐标系

float4 worldPos = mul(unity_ObjectToWorld,v.vertex);

//再把顶点从世界空间转换至镜子空间

float4 mirrorWorldPos = mul(_WtoMW,worldPos);

//最后就后例行把顶点从世界空间转换至裁剪空间

o.vertex = mul(UNITY_MATRIX_VP,mirrorWorldPos);

float4 worldPos = mul(unity_ObjectToWorld,v.vertex);
//再把顶点从世界空间转换至镜子空间
float4 mirrorWorldPos = mul(_WtoMW,worldPos);
//最后就后例行把顶点从世界空间转换至裁剪空间
o.vertex = mul(UNITY_MATRIX_VP,mirrorWorldPos);

o.uv = TRANSFORM_TEX(v.uv, _MainTex);
// Transform the normal from object space to world space
o.worldNormal = mul(v.normal, (float3x3)unity_WorldToObject);
return o;
}

//frag 函数和实体的是一样的..

}

Pass
{
//这里渲染实体的 Pass
}

}

}

5、镜子的 Shader :限制虚像只在镜面中显示

在创建一个 Shader 和材质给到镜子物体身上

并在镜子的 Shader 中写入 Stencil指令:(和上一章的非欧世界面片 Quad 原理一模一样,就是起到遮罩作用,限定虚像显示区域。

细节看注释:

Shader "Unlit/StencilBufferMirror"

{

Properties
{

_MainTex ("Texture", 2D) = "white" {}

_RefValue("Ref Value",Int) = 0

_Color("Color Tint",Color) = (0,0,0,1)
}
SubShader

{

//Queue 渲染队列设置到 Geometry-1 是因为想在被反射的物体渲染之前就进行渲染,写入 stencil 值

Tags { "RenderType"="Opaque" "Queue"="Geometry-1" }

//[_RefValue]就是我们自己设置的参考值

//Always 表示了无论如何都通过模板测试

//Replace 表示通过模板测试后用参考值替换掉 Stencil Buffer 中此像素原有的 stencil 值(缓冲值)

Stencil{

Ref [_RefValue]

Comp Always

Pass Replace

}

Tags { "RenderType"="Opaque" "Queue"="Geometry-1" }

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

Pass{
//这里镜子的正常渲染(默认我使用 Unlit 的代码
}

}

}

三、效果展示

参考资料:

(再次感谢群里 Colin 和其他大佬们提供的思路)

四、下一章预告

Stencil 原理的屏幕后处理,局部描边:

,
大家还看了
也许喜欢
更多游戏

Copyright © 2024 妖气游戏网 www.17u1u.com All Rights Reserved