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

601d70b282e9e8faa19de2499208e8a6.png

零、前言

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

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

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

一、实现思路

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

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

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

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

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

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

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

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

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

103ded8c901798cf8b904d45d6cb788e.png

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

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

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

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

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

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

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

二、具体实现

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

1、被镜面反射的物体 Shader

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

e5153fe601368b7bde9b028c5be8a0e8.png

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

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

{

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

}Pass

{

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

}}}

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

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

0fdbf6ebe68ddd75d82280aefd017bad.png

并在 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;//这里渲染虚像的 PassPass{//这里是一些设置..//顶点函数
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 EqualPass keepZFail keep}//因为虚像经过镜像反转,位置也发生了变换,陷入了镜子世界中。所以势必会深度测试失败。

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

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

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

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

Stencil{Ref [_RefValue]Comp EqualPass keepZFail keep}//因为虚像经过镜像反转,位置也发生了变换,陷入了镜子世界中。所以势必会深度测试失败。

Ref [_RefValue]Comp EqualPass keepZFail 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 AlwaysPass Replace}

Tags { "RenderType"="Opaque" "Queue"="Geometry-1" } //[_RefValue]就是我们自己设置的参考值//Always 表示了无论如何都通过模板测试//Replace 表示通过模板测试后用参考值替换掉 Stencil Buffer 中此像素原有的 stencil 值(缓冲值)Stencil{Ref [_RefValue]Comp AlwaysPass Replace}Pass{//这里镜子的正常渲染(默认我使用 Unlit 的代码}

}

}

三、效果展示

5340108224591aeffd0783a3d3a582db.png

参考资料:

  • https://blog.csdn.net/MQLCSDN/article/details/96352876

  • https://blog.csdn.net/liu_if_else/article/details/86316361

  • https://docs.unity3d.com/ScriptReference/Transform-localToWorldMatrix.html

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

四、下一章预告

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

db8eda696098b3849d29543cacc35aab.png

a32267a2581dcd525e9a966ebe795031.png

bc88aab22d13016fa7fd5b196070d941.png 9d0c678dae4d331647bab06fd063dd2a.png

dcbbbbdda24e0dc7476b99b5f34203cc.png

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值