之前我们学习过环境反射,是一种基于理想情况下的实现,根据反射向量采样环境盒子的纹理像素,这是一种向运行性能妥协的折中办法,同时采样的像素颜色值也只局限于预先设置好的环境盒子的六面贴图,并非实时反射。
这次我们就来学习一种较为真实的实时反射,为什么说较为真实呢?因为在计算机图形学中,非光线追踪的渲染都是“部分真实”的渲染,只有一部分使用了真实物理模型公式或者说只是用了繁杂物理学的一部分。
想一想现实中的实时反射,或者说镜面反射,物体反射太阳光,经过镜面反射,射入我们的人眼,这就是一次镜面反射的过程,此时镜面中和真实物体对称的虚假物体就叫虚像,数学关系和我们之前学习的两种点与平面点与直线的对称数学计算一样,当然这里我们只采用三维空间中点与平面的对称计算。
那么在计算机图形学中,怎么表现现实中的实时镜面反射呢?我们可以假设,在计算机三维空间中,我们的眼睛(eyeCamera)观察渲染整个看到的场景,此时观察到了一面镜子,镜子中的世界(镜中世界)呈现和现实世界对称的渲染场景,那么我们将自己的眼睛(eyeCamera)渲染的场景经过对称变换映射到镜面,不就是创造了一个“镜中世界”么?此时我们已经有了想法了,接下来就是实现过程了!
首先就让我们推导对称矩阵,因为计算机图形变换都是使用矩阵的,小伙伴如果从我博客最开始看起的,那么矩阵的推导无非就两种情况:
①.就是建立线性方程组对已知多个空间原始顶点和变换后顶点(大部分情况下图形学中矩阵就是空间变换的作用)坐标进行带入,最后求解矩阵中各个分量的值。
②.根据推导出来的原始顶点和变换后顶点的关系公式,进行矩阵的构建。
因为之前我们已经推导出了点与空间对称坐标公式,所以构建想要的矩阵即可,如下图:
既然相应的矩阵已经推导出来了,那么接下来就是程序实现环节了,如下:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class CameraReflect : MonoBehaviour
{
public MeshFilter mMeshFilter;
public MeshRenderer mMeshRender;
public Camera mEyeCamera;
private Mesh mMesh;
private float mA;
private float mB;
private float mC;
private float mD;
private Matrix4x4 mReflMat;
void Start()
{
genMirrorMesh();
calculateEquation();
mReflMat = genReflectMatrix();
//这一步比较关键,假设我们的主camera为整个场景的渲染camera
//那么我们模拟的eyeCamera就必须在MVP变换中ViewMatrix处理后一个阶段还得乘反射对称矩阵
mEyeCamera.worldToCameraMatrix = Camera.main.worldToCameraMatrix * mReflMat;
}
//创建一个镜子正方形网格
//ps这里提出unity的一个不足之处
//就是mesh类中vertices顶点坐标数组并不会随着transform的TRS变换而改变
//所以这里我使用自建mesh和顶点
//不然的话就要处理mesh的vertices的TRS变换
private void genMirrorMesh()
{
mMesh = new Mesh();
mMesh.vertices = new Vector3[]
{
new Vector3(-20,-2,20),
new Vector3(20,-2,20),
new Vector3(20,-2,-20),
new Vector3(-20,-2,-20)
};
mMesh.triangles = new int[]
{
0,1,2,
0,2,3
};
mMesh.uv = new Vector2[]
{
new Vector2(0,0),
new Vector2(1,0),
new Vector2(1,1),
new Vector2(0,1),
};
mMesh.normals = new Vector3[]
{
new Vector3(0,1,0),
new Vector3(0,1,0),
new Vector3(0,1,0),
new Vector3(0,1,0),
};
mMeshFilter.mesh = mMesh;
}
//计算出对应镜面平面方程系数
private void calculateEquation()
{
Vector3 p0 = mMesh.vertices[0];
Vector3 p1 = mMesh.vertices[1];
Vector3 p2 = mMesh.vertices[2];
Vector3 p0p1 = p1 - p0;
Vector3 p0p2 = p2 - p0;
Vector3 n = Vector3.Cross(p0p1, p0p2).normalized;
mA = n.x;
mB = n.y;
mC = n.z;
mD = -n.x * p0.x - n.y * p0.y - n.z * p0.z;
}
//构建反射对称矩阵
private Matrix4x4 genReflectMatrix()
{
float k = mA * mA + mB * mB + mC * mC;
Matrix4x4 mat = new Matrix4x4();
mat.m00 = (-mA * mA + mB * mB + mC * mC) / k;
mat.m01 = -2 * mA * mB / k;
mat.m02 = -2 * mA * mC / k;
mat.m03 = -2 * mA * mD;
mat.m10 = -2 * mA * mB / k;
mat.m11 = (mA * mA - mB * mB + mC * mC) / k;
mat.m12 = -2 * mB * mC / k;
mat.m13 = -2 * mB * mD;
mat.m20 = -2 * mA * mC / k;
mat.m21 = -2 * mB * mC / k;
mat.m22 = (mA * mA + mB * mB - mC * mC) / k;
mat.m23 = -2 * mC * mD;
mat.m30 = 0;
mat.m31 = 0;
mat.m32 = 0;
mat.m33 = 1;
return mat;
}
void Update()
{
//这个函数标识了反转剔除模式
//因为eyeCamera相机渲染被我用了对称矩阵修改了顶点
//但是法向量没有修改,所以在计算机图形处理中会导致错误
//只能反向剔除再render才能达到正确的现实
GL.invertCulling = true;
mEyeCamera.Render();
GL.invertCulling = false;
mMeshRender.sharedMaterial.SetTexture("_ReflTex", mEyeCamera.targetTexture);
}
}
CG shader着色代码如下:
Shader "Unlit/MirrorReflectUnlitShader"
{
Properties
{
_MainTex ("Texture", 2D) = "white" {}
_ReflTex("ReflTex",2D) = "white" {}
}
SubShader
{
Tags { "RenderType"="Opaque" }
LOD 100
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
// make fog work
#pragma multi_compile_fog
#include "UnityCG.cginc"
struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
};
struct v2f
{
float2 uv : TEXCOORD0;
float4 vertex : SV_POSITION;
float4 ScreenPos : TEXCOORD1;
};
sampler2D _MainTex;
float4 _MainTex_ST;
sampler2D _ReflTex; /*反射纹理*/
v2f vert (appdata v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv = TRANSFORM_TEX(v.uv, _MainTex);
//ComputeScreenPos这个函数就代表了从裁剪空间到屏幕空间
//MVP矩阵把顶点源坐标经过建模变换,视口变换,投影变换到裁剪立方空间
//这个ComputeScreenPos就是将裁剪空间坐标变换到屏幕坐标系坐标
//这里我们要将eyeCamera渲染的对称场景经过屏幕变换后
//将屏幕渲染对称场景纹理映射到镜面,然后进行颜色叠加渲染就得到了理想的反射效果
//这个函数我会在后面进行详细讲解和使用
o.ScreenPos = ComputeScreenPos(o.vertex);
return o;
}
fixed4 frag (v2f i) : SV_Target
{
fixed4 col = tex2D(_MainTex, i.uv);
//屏幕坐标值需要/w得到0-1范围内的采样uv
fixed4 reflcol = tex2D(_ReflTex, i.ScreenPos.xy / i.ScreenPos.w);
//最后进行颜色叠加
col += reflcol;
return col;
}
ENDCG
}
}
}
虽然我在代码中做了详细的注释,但是代码的一些细节及不足必须详细标识一下:
①.genMirrorMesh()创建镜面网格,我为什么选择自己创建网格呢?因为unity的mesh的veritces并不随着transform的TRS(平移旋转缩放)进行变换,也就是说vertices顶点数组一开始到运行结束就是固定值,我不知道这是unity的bug还是怎么样,但是这个情况确实存在,唯一的解释就是c#mesh类vertices储存的CG着色器中vertex顶点函数之前建模空间的源坐标,并不是在vertex函数进行MVP依次变换之后坐标,所以我们还是得自己计算矩阵变换到相应空间的坐标,或者采用其他推导矩阵进行计算。
②.calculateEquation()和genReflectMatrix()这两个就是计算平面方程和创建反射对称矩阵。
③.mEyeCamera.worldToCameraMatrix = Camera.main.worldToCameraMatrix * mReflMat;这个过程就是在ViewMatrix处理后进行对称矩阵变换处理。
④.GL.invertCulling = true;剔除模式反转,因为只处理了顶点坐标并未处理法向量,所以图形计算会出错,需要反转一次渲染。
⑤.CG shader中ComputeScreenPos流水线中屏幕变换,将裁剪空间坐标系变换到显示设备屏幕坐标系,这个函数我觉得我有必要额外开篇讲解功能及其应用,因为这个函数能做到的事情十分神奇!
⑥.此篇博客的技术实际上不算真实反射,而是一种基于物理上的模拟。
最后讲到这里,针对上述解释情况,后面继续扩展学习,给个效果图,如下:
so,我们接下来继续。