Unity&Shader案例篇-镜子2

时间:2023-02-06 22:19:11

一、前言

上一篇介绍了有关镜子的制作,有关理论部分的内容我会在后续相关的文章中陆续介绍,莫急,我先趁着自己脑子还是对此技术比较热,趁热打铁尽早把实现部分先写出来。上一章的介绍制作的镜子其实只是一个取巧的方法,并不能做到实时的反射出实物,但是思路还是比较有意思的。

 

还是废话不多说先上图,我用的Unity版本5.3.3Unity&Shader案例篇-镜子2

二、Unity中制作原理

1、简单说明:其实这个原理就是用一个摄像机去拍镜子上面的物体将得到的图像投影给Plane,最后主摄像机就能看到Plane上物体的镜像,所以关键的部分就是计算摄像机上的投影矩阵和主摄像机的投影矩阵的关系,因为站在不同的角度看(主摄像机转动或移动)镜像是要跟着偏移的

2、创建一个Camera作为镜像摄像机,将下面计算摄像机的投影平面的脚本代码拖到这个Camera上

using UnityEngine;
using System.Collections;
[ExecuteInEditMode]
public class ViewPlane : MonoBehaviour {
public GameObject mirrorPlane; //镜子屏幕

public bool estimateViewFrustum = true;
public bool setNearClipPlane = false; //是否设置近剪切平面

public float nearClipDistanceOffset = -0.01f; //近剪切平面的距离

private Camera mirrorCamera; //镜像摄像机
// Use this for initialization
void Start () {
mirrorCamera = GetComponent<Camera>();

}

// Update is called once per frame
void Update () {

if (null != mirrorPlane && null != mirrorCamera)
{
//世界坐标系的左下角
Vector3 pa = mirrorPlane.transform.TransformPoint(new Vector3(-5.0f, 0.0f, -5.0f));

//世界坐标系的右下角
Vector3 pb = mirrorPlane.transform.TransformPoint(new Vector3(5.0f, 0.0f, -5.0f));

//世界坐标系的左上角
Vector3 pc = mirrorPlane.transform.TransformPoint(new Vector3(-5.0f, 0.0f, 5.0f));

//镜像观察角度的世界坐标位置
Vector3 pe = transform.position;

//镜像摄像机的近剪切面的距离
float n = mirrorCamera.nearClipPlane;

//镜像摄像机的远剪切面的距离
float f = mirrorCamera.farClipPlane;

//从镜像摄像机到左下角
Vector3 va = pa - pe;

//从镜像摄像机到右下角
Vector3 vb = pb - pe;

//从镜像摄像机到左上角
Vector3 vc = pc - pe;

//屏幕的右侧旋转轴
Vector3 vr = pb - pa;

//屏幕的上侧旋转轴
Vector3 vu = pc - pa;

//屏幕的法线
Vector3 vn;

//到屏幕左边缘的距离
float l;

//到屏幕右边缘的距离
float r;

//到屏幕下边缘的距离
float b;

//到屏幕上边缘的距离
float t;

//从镜像摄像机到屏幕的距离
float d;

//如果看向镜子的背面
if (Vector3.Dot(-Vector3.Cross(va, vc), vb) < 0.0f)
{
//
vu = -vu;
pa = pc;
pb = pa + vr;
pc = pa + vu;
va = pa - pe;
vb = pb - pe;
vc = pc - pe;
}

vr.Normalize();
vu.Normalize();

//两个向量的叉乘,最后在取负,因为Unity是使用左手坐标系
vn = -Vector3.Cross(vr, vu);

vn.Normalize();

d = -Vector3.Dot(va, vn);
if (setNearClipPlane)
{
n = d + nearClipDistanceOffset;
mirrorCamera.nearClipPlane = n;
}
l = Vector3.Dot(vr, va) * n / d;
r = Vector3.Dot(vr, vb) * n / d;
b = Vector3.Dot(vu, va) * n / d;
t = Vector3.Dot(vu, vc) * n / d;

//投影矩阵
Matrix4x4 p = new Matrix4x4();
p[0, 0] = 2.0f * n / (r - l);
p[0, 1] = 0.0f;
p[0, 2] = (r + l) / (r - l);
p[0, 3] = 0.0f;

p[1, 0] = 0.0f;
p[1, 1] = 2.0f * n / (t - b);
p[1, 2] = (t + b) / (t - b);
p[1, 3] = 0.0f;

p[2, 0] = 0.0f;
p[2, 1] = 0.0f;
p[2, 2] = (f + n) / (n - f);
p[2, 3] = 2.0f * f * n / (n - f);

p[3, 0] = 0.0f;
p[3, 1] = 0.0f;
p[3, 2] = -1.0f;
p[3, 3] = 0.0f;

//旋转矩阵
Matrix4x4 rm = new Matrix4x4();
rm[0, 0] = vr.x;
rm[0, 1] = vr.y;
rm[0, 2] = vr.z;
rm[0, 3] = 0.0f;

rm[1, 0] = vu.x;
rm[1, 1] = vu.y;
rm[1, 2] = vu.z;
rm[1, 3] = 0.0f;

rm[2, 0] = vn.x;
rm[2, 1] = vn.y;
rm[2, 2] = vn.z;
rm[2, 3] = 0.0f;

rm[3, 0] = 0.0f;
rm[3, 1] = 0.0f;
rm[3, 2] = 0.0f;
rm[3, 3] = 1.0f;

Matrix4x4 tm = new Matrix4x4();
tm[0, 0] = 1.0f;
tm[0, 1] = 0.0f;
tm[0, 2] = 0.0f;
tm[0, 3] = -pe.x;

tm[1, 0] = 0.0f;
tm[1, 1] = 1.0f;
tm[1, 2] = 0.0f;
tm[1, 3] = -pe.y;

tm[2, 0] = 0.0f;
tm[2, 1] = 0.0f;
tm[2, 2] = 1.0f;
tm[2, 3] = -pe.z;

tm[3, 0] = 0.0f;
tm[3, 1] = 0.0f;
tm[3, 2] = 0.0f;
tm[3, 3] = 1.0f;

//矩阵组
//
mirrorCamera.projectionMatrix = p;
mirrorCamera.worldToCameraMatrix = rm * tm;


if (estimateViewFrustum)
{
//旋转摄像机
Quaternion q = new Quaternion();
q.SetLookRotation((0.5f * (pb + pc) - pe), vu);
//聚焦到屏幕的中心点
mirrorCamera.transform.rotation = q;

//保守估计fieldOfView的值
if (mirrorCamera.aspect >= 1.0)
{
mirrorCamera.fieldOfView = Mathf.Rad2Deg *
Mathf.Atan(((pb - pa).magnitude + (pc - pa).magnitude)
/ va.magnitude);
}
else
{
//在摄像机角度考虑,保证视锥足够宽
mirrorCamera.fieldOfView =
Mathf.Rad2Deg / mirrorCamera.aspect *
Mathf.Atan(((pb - pa).magnitude + (pc - pa).magnitude)
/ va.magnitude);
}
}
}
}
}
3、创建一个Plane作为镜子,这个Plane的Shader必须要是一个能接受贴图的,所以这里可以自行使用Unity自带的Shader,我选择了Unlit/Texture

4、将下面的代码赋给2中创建的Camera,将主摄像机给MainCamrea变量,镜子Plane赋值给MirrorPlane变量,

using UnityEngine;
using System.Collections;
[ExecuteInEditMode]
public class Mirrors2 : MonoBehaviour {

public GameObject mirrorPlane;//镜子
public Camera mainCamera;//主摄像机
private Camera mirrorCamera;//镜像摄像机
// Use this for initialization
void Start () {
mirrorCamera = GetComponent<Camera>();

}

// Update is called once per frame
void Update () {

if(null!=mirrorPlane&&null!=mirrorCamera&&null!=mainCamera)
{
//将主摄像机的世界坐标位置转换为镜子的局部坐标位置
Vector3 postionInMirrorSpace = mirrorPlane.transform.InverseTransformPoint(mainCamera.transform.position);

//一般y为镜面的法线方向
postionInMirrorSpace.y = -postionInMirrorSpace.y;

//转回到世界坐标系的位置
mirrorCamera.transform.position = mirrorPlane.transform.TransformPoint(postionInMirrorSpace);
}
}
}
5、如果是刚创建的Camera的投影梯形一定是非常规则的如图所示,勾选这个Camera

Unity&Shader案例篇-镜子2 上的ViewPlane脚本上的setNearClipPlane,这个时候其实是在设置这个Camera的近剪切屏幕,使得这个平面尽量与镜子Plane平面重合,如图所示,这个还不是完全的重合,可以通过调整参数nearClipDistanceOffset来调整,默认的-0.01其实就是我已经调
Unity&Shader案例篇-镜子2
整好了的参数,只要勾选setNearClipPlane就会自动调整到与镜子平面重合,如图所示
Unity&Shader案例篇-镜子2

6、最后创建一个RenderTexture,这个Texture就是用来将镜像摄像机投影出来的图像信息传递给镜子Plane的中间变量,所以将这个Texture分别托给Plane的Shader材质中的Texture和镜像摄像机中的TargetTexture。最后设置这个Texture的分辨率,我设置成了1024×1024,默认的是256×256,这样的会造成镜像模糊,如图所示

Unity&Shader案例篇-镜子2

设置成1024×1024之后就如第一章图所示,清晰度比较高。

三、总结

1、缺点:镜子不能有自己的贴图、不能实现多面镜子同时相互反射(还有待发现)

2、相比上一篇的优点:可以实时的反射所有在镜面上的物体,可以改变物体的光照和贴图

3、什么不是Shader,一句Shader代码都没有怎么还是Shader的案例,跟Shader有毛关系?是的,的确没有一句Shader的代码,然而使用C#代码控制其实都是Shader里面的参数,比如投影矩阵的计算,尤其是此处的投影区域有不规则倾斜的情况。

后续待...

原文转载请注明出处点击打开链接

工程文件下载地址点击打开链接