百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 编程字典 > 正文

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

toyiye 2024-07-06 00:22 16 浏览 0 评论

零、前言

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

不过我们如果基于 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 的代码
}

}

}

三、效果展示

参考资料:

  • 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 原理的屏幕后处理,局部描边:

相关推荐

为何越来越多的编程语言使用JSON(为什么编程)

JSON是JavascriptObjectNotation的缩写,意思是Javascript对象表示法,是一种易于人类阅读和对编程友好的文本数据传递方法,是JavaScript语言规范定义的一个子...

何时在数据库中使用 JSON(数据库用json格式存储)

在本文中,您将了解何时应考虑将JSON数据类型添加到表中以及何时应避免使用它们。每天?分享?最新?软件?开发?,Devops,敏捷?,测试?以及?项目?管理?最新?,最热门?的?文章?,每天?花?...

MySQL 从零开始:05 数据类型(mysql数据类型有哪些,并举例)

前面的讲解中已经接触到了表的创建,表的创建是对字段的声明,比如:上述语句声明了字段的名称、类型、所占空间、默认值和是否可以为空等信息。其中的int、varchar、char和decimal都...

JSON对象花样进阶(json格式对象)

一、引言在现代Web开发中,JSON(JavaScriptObjectNotation)已经成为数据交换的标准格式。无论是从前端向后端发送数据,还是从后端接收数据,JSON都是不可或缺的一部分。...

深入理解 JSON 和 Form-data(json和formdata提交区别)

在讨论现代网络开发与API设计的语境下,理解客户端和服务器间如何有效且可靠地交换数据变得尤为关键。这里,特别值得关注的是两种主流数据格式:...

JSON 语法(json 语法 priority)

JSON语法是JavaScript语法的子集。JSON语法规则JSON语法是JavaScript对象表示法语法的子集。数据在名称/值对中数据由逗号分隔花括号保存对象方括号保存数组JS...

JSON语法详解(json的语法规则)

JSON语法规则JSON语法是JavaScript对象表示法语法的子集。数据在名称/值对中数据由逗号分隔大括号保存对象中括号保存数组注意:json的key是字符串,且必须是双引号,不能是单引号...

MySQL JSON数据类型操作(mysql的json)

概述mysql自5.7.8版本开始,就支持了json结构的数据存储和查询,这表明了mysql也在不断的学习和增加nosql数据库的有点。但mysql毕竟是关系型数据库,在处理json这种非结构化的数据...

JSON的数据模式(json数据格式示例)

像XML模式一样,JSON数据格式也有Schema,这是一个基于JSON格式的规范。JSON模式也以JSON格式编写。它用于验证JSON数据。JSON模式示例以下代码显示了基本的JSON模式。{"...

前端学习——JSON格式详解(后端json格式)

JSON(JavaScriptObjectNotation)是一种轻量级的数据交换格式。易于人阅读和编写。同时也易于机器解析和生成。它基于JavaScriptProgrammingLa...

什么是 JSON:详解 JSON 及其优势(什么叫json)

现在程序员还有谁不知道JSON吗?无论对于前端还是后端,JSON都是一种常见的数据格式。那么JSON到底是什么呢?JSON的定义...

PostgreSQL JSON 类型:处理结构化数据

PostgreSQL提供JSON类型,以存储结构化数据。JSON是一种开放的数据格式,可用于存储各种类型的值。什么是JSON类型?JSON类型表示JSON(JavaScriptO...

JavaScript:JSON、三种包装类(javascript 包)

JOSN:我们希望可以将一个对象在不同的语言中进行传递,以达到通信的目的,最佳方式就是将一个对象转换为字符串的形式JSON(JavaScriptObjectNotation)-JS的对象表示法...

Python数据分析 只要1分钟 教你玩转JSON 全程干货

Json简介:Json,全名JavaScriptObjectNotation,JSON(JavaScriptObjectNotation(记号、标记))是一种轻量级的数据交换格式。它基于J...

比较一下JSON与XML两种数据格式?(json和xml哪个好)

JSON(JavaScriptObjectNotation)和XML(eXtensibleMarkupLanguage)是在日常开发中比较常用的两种数据格式,它们主要的作用就是用来进行数据的传...

取消回复欢迎 发表评论:

请填写验证码