零、前言
镜面反射是游戏里十分常见又比较麻烦的需求,大多情况都需要额外创建一个摄像机,根据镜面镜像反转位置来渲染镜子中的内容。
不过我们如果基于Stencil原理来操作,就可以不需要额外创建摄像机就可以实现镜面效果了噢!
相信大家都看了前几章后(应该),对于模板Stencil作用会有个感性的理解:遮罩作用。那这篇文章将使用模板Stencil进行镜面区域限定,配合模型顶点镜面反转,来实现镜面反射的效果。
一、实现思路
我们先来想一想真实世界中镜子成像的原理 :太阳或者灯的光照射到人或物体的身上,随后人或物体又反射这些光(大部分是漫反射)射向到镜面上。平面镜又将光镜面反射到人的眼睛里,因此我们看到了自己或物体在平面镜中的虚像。
我们分析得出一下三点特征:
一,假设镜子光滑的是完美镜面反射(即光只改变方向不改变光的颜色),在镜子里可以看到的物体(虚像)和实际的物体(实体)外观细节(纹理颜色)是一模一样的,因为都是漫反射光的结果。
二,因为是镜面反射成像,虚像和实体之间会关于镜子平面互相对称。
三,镜子成的像只能在镜子里面看到(看起来是废话哈哈,不过这正是Stencil模板发挥作用的地方噢)。
第一点,对于我们来说是个好消息。既然纹理颜色是一样的,我们可以使用相同内容的两个Pass将物体渲染两遍就好了。
第二点,是一个比较难搞又重要的问题。我们需要让它们关于镜子平面互相对称才行。这怎么做呢?
(这里要十分感谢群里Colin和其他大佬们提供的思路)
贴张图来展示一下:关于镜子平面互相对称,只需要构建一个“World” To “MirrorWorld” Matrix(世界转换到镜子世界的矩阵)将物体关于镜子Y轴对称反转就可以了。
“World” To “MirrorWorld” Matrix(世界转换到镜子世界的矩阵)具体构建思路如下:
- 在镜子表面的中心放一个新的空 GameObject ,让其 Local坐标系 下的 Y轴 指向镜子外面。
- 用其 Transform 的 worldToLocalMatrix 矩阵将物体从世界坐标系转换至以镜子为中心的本地坐标系;
- 然后构建一个 Y轴反转矩阵(即Y变成-Y)左乘上面得到的 worldToLocalMatrix 矩阵;
- 最后再用其 Transform的 localToWorldMatrix 矩阵左乘以上的矩阵。
第三点,相信大家都看过前面几篇文章后,可能会有个体会:模板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传递“World” 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; //”World“ 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、应用镜面对称反转矩阵
这时我们被镜面反射的物体Shader代码也要更新一下,来接收与使用脚本传递来的矩阵。
我们声明了 float4x4 类型的 _WtoMW 矩阵,来接受脚本传递来的矩阵。
并在渲染虚像Pass里的顶点着色器使用此矩阵,将顶点从世界空间转换至镜子空间。
具体看代码注释:
Shader "Custom/StencilBufferTwoPassReflection" { Properties { _MainTex ("Texture", 2D) = "white" {} } SubShader { Tags { "RenderType"="Opaque" "LightMode"="ForwardBase" "Queue"="Geometry" } //这里是其他变量的声明.. //声明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); 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添加 Stencil 、ZTest Always 、Cull 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 //这里是其他变量的声明和设置.... //声明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); 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 } Pass{ //这里镜子的正常渲染(默认我使用Unlit的代码 } } }
三、效果展示
参考资料:
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原理的屏幕后处理,局部描边:
暂无关于此日志的评论。