|
平面反射(Planar Reflection)原理
平面反射可以模拟光滑度很高的镜面效果,但也只能用于高度一致的平面,用在水面的效果是非常不错的。网上的文章讲反射矩阵和斜截视锥体的文章很多了,但细讲反射原理的很少,我觉得还是有些细节值得讲的。
基本思路
首先看一下平面反射的实现思路。(关于最基本的反射原理可以参考:王二:[数学] 光线的反射(reflect)与折射(refract))
平面反射原理
先介绍一种最简单直接的思路,不需要反射矩阵。
当前目标是,使用相机 ,在看向光滑平面上的 、 两点时,根据光线的反射原理,应该分别对应显示物体上的 、 两点。目前的思路是,平面上采样一张贴图,在对应的 点显示 点的颜色, 点显示 点的颜色,并且要保证光照效果正确,那么这张贴图应该长啥样?并且如何采样?
首先,想象出一个物体关于平面对称后产生一个倒立的一模一样的物体,原物体的 、 对应想象物体的 、 ,根据光线的反射原理推出,相机 要在 点通过反射看到 点,等同于相机的视线穿过平面看到 点,我们只要采样到相机 此时看到的 点时的颜色就可以了。
按上面的方法,我们有了反射平面的贴图了,该怎么采样它才能正确的显示呢?通过上面的推导和图可以知道, 点在 和 的裁剪空间中的位置是刚好颠倒的,所以直接用裁剪空间的坐标经过透视除法,转为屏幕空间坐标,再把 y 轴颠倒过来,用 xy 坐标当作 uv 去采样就可以了。
计算过程可以概括为以下几步:
- 将相机 的 World Space 坐标转换到平面的 Object Space(简称平面空间)
- 将相机 的平面空间坐标的 Y 轴取反,获得关于平面对称的反射相机 的平面空间坐标,并转换回 World Space
- 在平面空间获取相机 的 Z 轴朝向和 Y 轴朝向,将它们关于平面对称,获得反射相机 在平面空间对应的两个Z、Y轴朝向向量,然后转换到 World Space 然后赋予给反射相机
- 将反射相机渲染的图片缓存,在平面上使用将 y 轴颠倒的屏幕坐标采样
相关代码:
private void UpdateReflectionCamera(Camera curCamera) {
if (targetPlane == null) {
Debug.LogError("target plane is null!");
}
UpdateCamera(curCamera, _reflectionCamera); // 同步当前相机数据
// 将相机移转换到平面空间 plane space,再通过平面对称创建反射相机
Vector3 camPosPS = targetPlane.transform.worldToLocalMatrix.MultiplyPoint(curCamera.transform.position);
Vector3 reflectCamPosPS = Vector3.Scale(camPosPS, new Vector3(1, -1, 1)) + new Vector3(0, m_planeOffset, 0); // 反射相机平面空间
Vector3 reflectCamPosWS = targetPlane.transform.localToWorldMatrix.MultiplyPoint(reflectCamPosPS); // 将反射相机转换到世界空间
_reflectionCamera.transform.position = reflectCamPosWS;
// 设置反射相机方向
Vector3 camForwardPS = targetPlane.transform.worldToLocalMatrix.MultiplyVector(curCamera.transform.forward);
Vector3 reflectCamForwardPS = Vector3.Scale(camForwardPS, new Vector3(1, -1, 1));
Vector3 reflectCamForwardWS = targetPlane.transform.localToWorldMatrix.MultiplyVector(reflectCamForwardPS);
Vector3 camUpPS = targetPlane.transform.worldToLocalMatrix.MultiplyVector(curCamera.transform.up);
Vector3 reflectCamUpPS = Vector3.Scale(camUpPS, new Vector3(-1, 1, -1));
Vector3 reflectCamUpWS = targetPlane.transform.localToWorldMatrix.MultiplyVector(reflectCamUpPS);
_reflectionCamera.transform.rotation = Quaternion.LookRotation(reflectCamForwardWS, reflectCamUpWS);
// 斜截视锥体
Vector3 planeNormal = targetPlane.transform.up;
Vector3 planePos = targetPlane.transform.position + planeNormal * m_planeOffset;
var clipPlane = CameraSpacePlane(_reflectionCamera, planePos - Vector3.up * 0.1f, planeNormal, 1.0f);
_reflectionCamera.projectionMatrix = projection;
_reflectionCamera.cullingMask = m_settings.m_ReflectLayers; // never render water layer
}
采样 UV
优化方法思路
上面这种方法矩阵计算比较多,有一个同等效果的,计算量更少的方法,需要用到反射矩阵,我找到的网上现有的脚本也都是用的这种方法。
我们知道模型通过 MVP 矩阵从模型空间变到裁剪空间,我们并不需要让 移动到镜面对称的位置,而是让 同样处于 的位置,然后在 的 M 矩阵和 V 矩阵之间加入一个反射矩阵,把整个世界倒过来,这样相机 看到的就永远是一个关于平面镜像的世界,而且两个相机视锥体完全重合了,采样时也不用颠倒 y 轴坐标了。
关于这个方法我在找资料的过程中有一些发现:
Unity Shader-反射效果(CubeMap,Reflection Probe,Planar Reflection,Screen Space Reflection), 关于反射这篇文章讲的非常详细,但其中应该有个错误:
我们把R矩阵插在V之后,在一个物体在进行MV变换后,变到正常相机坐标系下,然后再进行一次反射变换,就相当于变换到了相对于平面对称的相机坐标系下,然后再进行正常的投影变换,就可以得到反射贴图了。 reflectionCamera.worldToCameraMatrix = curCamera.worldToCameraMatrix * reflection;实际上,因为矩阵是左乘,所以代码中反射矩阵并非在 V 矩阵之后,而应该是在 V 矩阵之前,M 矩阵之后使用的,而且反射矩阵中的计算平面和点用的都是世界空间坐标,所以这一步实际上是将整个世界关于平面对称倒过来之后,再变到视空间的。
另外, 的 V 矩阵是基于原相机 的 V 矩阵的,所以两个相机的位置相同,应该没有必要再设置 的 transform 。
我看到一些代码,包括官方 URP 的示例项目 《Boat Attack》,有这样的操作:
reflectionCamera.transform.position = newpos; // newpos 是关于目标平面对称或者世界空间 xz 平面对称的位置可按我的理解是没有必要的,因为反射相机的 V 矩阵都已经改了,再调整它的 transform 应该没有啥意义了,实际上我把这段代码注掉后的脚本都是正常运行,暂未发现有任何影响,如果确实是需要这一步的话,有知道原因的大佬希望能告知一下。
反射矩阵推导
上面的方法要在 的 V 矩阵前插入一个反射矩阵,让整个世界在它眼中倒过来。这个矩阵要做的就是 ,其中 是 关于平面对称的点,下面来推导一下这个反射矩阵。
反射矩阵推导
已知 为平面的单位法向量, 为平面上任意一点, 是 与平面的交点。 求反射矩阵 使平面外一点 ,和其关于平面对称点 满足
我们的目标是找到反射矩阵 ,从图中很容易看出 和 的关系:
因为其中 已知
为了简化下面带入
将上式带入(1)式
为方便观察,先列出每一项对应的计算
经过观察总结出反射矩阵
斜截视锥体(Oblique View Frustum)
虽然完成了镜面反射,但还有一个问题没解决,那就是反射相机当前的视锥体与主相机相同,所以镜像后能看到反射平面背后的东西,就会出现这种情况
反射相机视锥体问题
它的视锥体应该以反射平面为近平面,如下图所示,视锥体 A 部分应该被剔除,只保留 B 部分。
反射相机视锥体
这需要用到斜截视锥体的技术,简单概括,这种技术通过改变 MVP 中的 P 矩阵,实现用指定的平面来当作近平面,但同时会影响到远平面,而其他四个平面不受影响,这正是我们需要的。
以下内容参考论文:《Oblique View Frustum Depth Projection and Clipping》
平面的共变(covariant)
要想将视空间中的平面当作裁剪空间中的近平面,首先需要了解平面是如何变换的。
平面有两种表示形式,一种是四个系数的平面方程:
还可以用平面的法向量 ,和平面内已知的任意一点 来表示:
将(2)展开可以得到
结合(1)可以带入对应系数
平面可以用一个四维向量表示,不难发现,平面和法线类似,是一个共变向量,于是在透视变换中与法线变换一样,需要透视矩阵的逆的转置,具体原理可以参考:王二:[数学] 法线变换
另外,任意点与平面向量相乘可以表示为与平面的带符号的垂直距离,知道这一点后反射矩阵的计算更加容易了。
修改透视矩阵
从视空间变换到裁剪空间再经过透视除,就可以将视锥体转换成 xyz 三轴坐标都在 [-1,1] 之间的立方体(基于 OpenGL 风格,DirectX 的 z 轴是 [0,1])也就是 NDC,我们希望修改透视矩阵后,NDC 组成的仍然是这样一个立方体。于是可以将近平面在视空间中的坐标,用经过透视除法后的裁剪空间坐标表示
平面在视空间和裁剪空间的坐标关系
如上图所示,近平面对应了透视矩阵的第三行和第四行相加,而我们想修改透视矩阵来改变近平面的话,只能修改它的第三行,第四行不能改,因为第四行会把视空间中的 z 坐标转到裁剪空间的 w 坐标,这对于顶点属性的透视矫正插值是必要的,相关原因可以参考:王二:[数学] 重心坐标插值与透视校正插值
修改后的透视矩阵的第三行为 ,这会同时影响近平面和远平面,于是修改后的视空间中的近平面 和远平面 表示为:
如果我们指定的近平面的法线不垂直于 xy 平面的话,修改后的近远平面就不平行了,那它们在视空间中是什么样的呢?它们会在 xy 平面上相交,只要找 xy 平面上一点 就可以发现,因为 没有改动,所以 , 。所以它们现在可能长这样:
修改透视矩阵后的近远平面在 xy 平面相交
这只是一种可能,远平面也可能会截断原视锥体。这也挺反直觉的,都不能围成一个封闭体积了,还能通过透视矩阵变换成立方体吗?还真的可以,毕竟我们是根据透视矩阵反着推的。
不过这种形状很有问题,可能会裁剪掉原视锥体,也会严重影响深度缓冲的精度,这个在论文的最后做了分析,暂时可以理解为类似于原视锥体,深度值就是沿着z轴坐标值,为了让深度缓冲有更高的精度,在 [-1,1] 之间容纳更小的范围,我们尽可能让近远平面之间的距离更小。而现在深度值变得更复杂了,和近远平面的位置有关,沿着不同方向造成的精度损失也不同,为了让深度缓冲精度值更高,我们要让近远平面之间的夹角更小,同时不裁剪原视锥体,如下图所示
优化远平面
近平面隐藏了一个缩放系数 ,可以调整这个系数而不影响我们的最终目的,但可以调整远平面的角度:
下面通过求出视空间中的 从而要得到合适的 。 (其中 sgn 是符号函数,输出 ±1)
最终我们得到修改后的透视矩阵第三行
标准透视矩阵的应用
下面将我们得到的结果用于标准透视矩阵(OpenGL 风格)试试
当前的透视矩阵不会改变平面法线 xy 轴的正负, ,所以可免去将平面转到裁剪空间
系数 也可以化简为:
修改后的投影矩阵可以写为
相关代码:
private Matrix4x4 CalculateObliqueMatrix(Camera cam, Vector4 plane) {
Vector4 Q_clip = new Vector4(Mathf.Sign(plane.x), Mathf.Sign(plane.y), 1f, 1f);
Vector4 Q_view = cam.projectionMatrix.inverse.MultiplyPoint(Q_clip);
Vector4 scaled_plane = plane * 2.0f / Vector4.Dot(plane, Q_view);
Vector4 M3 = scaled_plane - cam.projectionMatrix.GetRow(3);
Matrix4x4 new_M = cam.projectionMatrix;
new_M.SetRow(2, M3);
// 使用 unity API
// var new_M = cam.CalculateObliqueMatrix(plane);
return new_M;
}
平面反射 C# 代码实现 (基于 Unity URP )
完整代码:
using System;
using UnityEngine.Experimental.Rendering;
using UnityEngine.Serialization;
using Unity.Mathematics;
namespace UnityEngine.Rendering.Universal {
[ExecuteAlways]
public class plannarReflectionTest : MonoBehaviour {
[Serializable]
public enum ResolutionMulltiplier { Full, Half, Third, Quarter }
[Serializable]
public class PlanarReflectionSettings {
public ResolutionMulltiplier m_ResolutionMultiplier = ResolutionMulltiplier.Third;
public float m_ClipPlaneOffset = 0.07f;
public LayerMask m_ReflectLayers = -1;
public bool m_Shadows;
}
[SerializeField]
public PlanarReflectionSettings m_settings = new PlanarReflectionSettings();
public GameObject targetPlane;
public float m_planeOffset;
private static Camera _reflectionCamera;
private RenderTexture _reflectionTexture;
private readonly int _planarReflectionTextureId = Shader.PropertyToID("_ReflectTex");
// public static event Action<ScriptableRenderContext, Camera> BeginPlanarReflections;
private void OnEnable() {
RenderPipelineManager.beginCameraRendering += runPlannarReflection; // 订阅 beginCameraRendering 事件,加入平面反射函数
}
private void OnDisable() {
Cleanup();
}
private void OnDestroy() {
Cleanup();
}
private void Cleanup() {
RenderPipelineManager.beginCameraRendering -= runPlannarReflection;
if(_reflectionCamera) { // 释放相机
_reflectionCamera.targetTexture = null;
SafeDestroy(_reflectionCamera.gameObject);
}
if (_reflectionTexture) { // 释放纹理
RenderTexture.ReleaseTemporary(_reflectionTexture);
}
}
private static void SafeDestroy(Object obj) {
if (Application.isEditor) {
DestroyImmediate(obj); //TODO
}
else {
Destroy(obj); //TODO
}
}
private void runPlannarReflection(ScriptableRenderContext context, Camera camera) {
// we dont want to render planar reflections in reflections or previews
if (camera.cameraType == CameraType.Reflection || camera.cameraType == CameraType.Preview)
return;
if (targetPlane == null) {
targetPlane = gameObject;
}
if (_reflectionCamera == null) {
_reflectionCamera = CreateReflectCamera();
}
var data = new PlanarReflectionSettingData(); // save quality settings and lower them for the planar reflections
data.Set(); // set quality settings
UpdateReflectionCamera(camera); // 设置相机位置和方向等参数
CreatePlanarReflectionTexture(camera); // create and assign RenderTexture
// BeginPlanarReflections?.Invoke(context, _reflectionCamera); // callback Action for PlanarReflection &#34;?.&#34;确保event被订阅
UniversalRenderPipeline.RenderSingleCamera(context, _reflectionCamera); // render planar reflections 开始渲染函数
data.Restore(); // restore the quality settings
Shader.SetGlobalTexture(_planarReflectionTextureId, _reflectionTexture); // Assign texture to water shader
}
private int2 ReflectionResolution(Camera cam, float scale) {
var x = (int)(cam.pixelWidth * scale * GetScaleValue());
var y = (int)(cam.pixelHeight * scale * GetScaleValue());
return new int2(x, y);
}
private float GetScaleValue() {
switch(m_settings.m_ResolutionMultiplier) {
case ResolutionMulltiplier.Full:
return 1f;
case ResolutionMulltiplier.Half:
return 0.5f;
case ResolutionMulltiplier.Third:
return 0.33f;
case ResolutionMulltiplier.Quarter:
return 0.25f;
default:
return 0.5f; // default to half res
}
}
private void CreatePlanarReflectionTexture(Camera cam) {
if (_reflectionTexture == null) {
var res = ReflectionResolution(cam, UniversalRenderPipeline.asset.renderScale); // 获取 RT 的大小
const bool useHdr10 = true;
const RenderTextureFormat hdrFormat = useHdr10 ? RenderTextureFormat.RGB111110Float : RenderTextureFormat.DefaultHDR;
_reflectionTexture = RenderTexture.GetTemporary(res.x, res.y, 16,
GraphicsFormatUtility.GetGraphicsFormat(hdrFormat, true));
}
_reflectionCamera.targetTexture = _reflectionTexture; // 将 RT 赋予相机
}
private void UpdateCamera(Camera src, Camera dest) {
if (dest == null) return;
// dest.CopyFrom(src);
dest.aspect = src.aspect;
dest.cameraType = src.cameraType; // 这个参数不同步就错
dest.clearFlags = src.clearFlags;
dest.fieldOfView = src.fieldOfView;
dest.depth = src.depth;
dest.farClipPlane = src.farClipPlane;
dest.focalLength = src.focalLength;
dest.useOcclusionCulling = false;
if (dest.gameObject.TryGetComponent(out UniversalAdditionalCameraData camData)) { // TODO
camData.renderShadows = m_settings.m_Shadows; // turn off shadows for the reflection camera
}
}
// Calculates reflection matrix around the given plane
private static Matrix4x4 CalculateReflectionMatrix(Vector4 plane)
{
Matrix4x4 reflectionMat = Matrix4x4.identity;
reflectionMat.m00 = (1F - 2F * plane[0] * plane[0]);
reflectionMat.m01 = (-2F * plane[0] * plane[1]);
reflectionMat.m02 = (-2F * plane[0] * plane[2]);
reflectionMat.m03 = (-2F * plane[3] * plane[0]);
reflectionMat.m10 = (-2F * plane[1] * plane[0]);
reflectionMat.m11 = (1F - 2F * plane[1] * plane[1]);
reflectionMat.m12 = (-2F * plane[1] * plane[2]);
reflectionMat.m13 = (-2F * plane[3] * plane[1]);
reflectionMat.m20 = (-2F * plane[2] * plane[0]);
reflectionMat.m21 = (-2F * plane[2] * plane[1]);
reflectionMat.m22 = (1F - 2F * plane[2] * plane[2]);
reflectionMat.m23 = (-2F * plane[3] * plane[2]);
reflectionMat.m30 = 0F;
reflectionMat.m31 = 0F;
reflectionMat.m32 = 0F;
reflectionMat.m33 = 1F;
return reflectionMat;
}
// Given position/normal of the plane, calculates plane in camera space.
private Vector4 CameraSpacePlane(Camera cam, Vector3 pos, Vector3 normal, float sideSign) {
var offsetPos = pos + normal * m_settings.m_ClipPlaneOffset;
var m = cam.worldToCameraMatrix;
var cameraPosition = m.MultiplyPoint(offsetPos);
var cameraNormal = m.MultiplyVector(normal).normalized * sideSign;
return new Vector4(cameraNormal.x, cameraNormal.y, cameraNormal.z, -Vector3.Dot(cameraPosition, cameraNormal));
}
private void UpdateReflectionCamera(Camera curCamera) {
if (targetPlane == null) {
Debug.LogError(&#34;target plane is null!&#34;);
}
Vector3 planeNormal = targetPlane.transform.up;
Vector3 planePos = targetPlane.transform.position + planeNormal * m_planeOffset;
UpdateCamera(curCamera, _reflectionCamera); // 同步当前相机数据
// 获取视空间平面,使用反射矩阵,将图像根据平面对称上下颠倒
var planVS = new Vector4(planeNormal.x, planeNormal.y, planeNormal.z, -Vector3.Dot(planeNormal, planePos));
Matrix4x4 reflectionMat = CalculateReflectionMatrix(planVS);
_reflectionCamera.worldToCameraMatrix = curCamera.worldToCameraMatrix * reflectionMat;
// 斜截视锥体
var clipPlane = CameraSpacePlane(_reflectionCamera, planePos, planeNormal, 1.0f);
var newProjectionMat = CalculateObliqueMatrix(curCamera, clipPlane);
_reflectionCamera.projectionMatrix = newProjectionMat;
_reflectionCamera.cullingMask = m_settings.m_ReflectLayers; // never render water layer
}
// private void UpdateReflectionCamera(Camera curCamera) {
// // 不使用反射矩阵的方法
// if (targetPlane == null) {
// Debug.LogError(&#34;target plane is null!&#34;);
// }
// UpdateCamera(curCamera, _reflectionCamera); // 同步当前相机数据
// // 将相机移转换到平面空间 plane space,再通过平面对称创建反射相机
// Vector3 camPosPS = targetPlane.transform.worldToLocalMatrix.MultiplyPoint(curCamera.transform.position);
// Vector3 reflectCamPosPS = Vector3.Scale(camPosPS, new Vector3(1, -1, 1)) + new Vector3(0, m_planeOffset, 0); // 反射相机平面空间
// Vector3 reflectCamPosWS = targetPlane.transform.localToWorldMatrix.MultiplyPoint(reflectCamPosPS); // 将反射相机转换到世界空间
// _reflectionCamera.transform.position = reflectCamPosWS;
// // 设置反射相机方向
// Vector3 camForwardPS = targetPlane.transform.worldToLocalMatrix.MultiplyVector(curCamera.transform.forward);
// Vector3 reflectCamForwardPS = Vector3.Scale(camForwardPS, new Vector3(1, -1, 1));
// Vector3 reflectCamForwardWS = targetPlane.transform.localToWorldMatrix.MultiplyVector(reflectCamForwardPS);
// Vector3 camUpPS = targetPlane.transform.worldToLocalMatrix.MultiplyVector(curCamera.transform.up);
// Vector3 reflectCamUpPS = Vector3.Scale(camUpPS, new Vector3(-1, 1, -1));
// Vector3 reflectCamUpWS = targetPlane.transform.localToWorldMatrix.MultiplyVector(reflectCamUpPS);
// _reflectionCamera.transform.rotation = Quaternion.LookRotation(reflectCamForwardWS, reflectCamUpWS);
// // 斜截视锥体
// Vector3 planeNormal = targetPlane.transform.up;
// Vector3 planePos = targetPlane.transform.position + planeNormal * m_planeOffset;
// var clipPlane = CameraSpacePlane(_reflectionCamera, planePos - Vector3.up * 0.1f, planeNormal, 1.0f);
// var newProjectionMat = CalculateObliqueMatrix(curCamera, clipPlane);
// _reflectionCamera.projectionMatrix = newProjectionMat;
// _reflectionCamera.cullingMask = m_settings.m_ReflectLayers; // never render water layer
// }
private Matrix4x4 CalculateObliqueMatrix(Camera cam, Vector4 plane) {
Vector4 Q_clip = new Vector4(Mathf.Sign(plane.x), Mathf.Sign(plane.y), 1f, 1f);
Vector4 Q_view = cam.projectionMatrix.inverse.MultiplyPoint(Q_clip);
Vector4 scaled_plane = plane * 2.0f / Vector4.Dot(plane, Q_view);
Vector4 M3 = scaled_plane - cam.projectionMatrix.GetRow(3);
Matrix4x4 new_M = cam.projectionMatrix;
new_M.SetRow(2, M3);
// 使用 unity API
// var new_M = cam.CalculateObliqueMatrix(plane);
return new_M;
}
private Camera CreateReflectCamera() {
var go = new GameObject(gameObject.name + &#34; Planar Reflection Camera&#34;,typeof(Camera));
var cameraData = go.AddComponent(typeof(UniversalAdditionalCameraData)) as UniversalAdditionalCameraData;
cameraData.requiresColorOption = CameraOverrideOption.Off;
cameraData.requiresDepthOption = CameraOverrideOption.Off;
cameraData.renderShadows = false;
cameraData.SetRenderer(1); // 根据 render list 的索引选择 render TODO
var t = transform;
var reflectionCamera = go.GetComponent<Camera>();
reflectionCamera.transform.SetPositionAndRotation(transform.position, t.rotation); // 相机初始位置设为当前 gameobject 位置
reflectionCamera.depth = -10; // 渲染优先级 [-100, 100]
reflectionCamera.enabled = false;
go.hideFlags = HideFlags.HideAndDontSave;
return reflectionCamera;
}
class PlanarReflectionSettingData {
private readonly bool _fog;
private readonly int _maxLod;
private readonly float _lodBias;
private bool _invertCulling;
public PlanarReflectionSettingData() {
_fog = RenderSettings.fog;
_maxLod = QualitySettings.maximumLODLevel;
_lodBias = QualitySettings.lodBias;
}
public void Set() {
_invertCulling = GL.invertCulling;
GL.invertCulling = !_invertCulling; // 因为镜像后绕序会反,将剔除反向
RenderSettings.fog = false; // disable fog for now as it&#39;s incorrect with projection
QualitySettings.maximumLODLevel = 1;
QualitySettings.lodBias = _lodBias * 0.5f;
}
public void Restore() {
GL.invertCulling = _invertCulling;
RenderSettings.fog = _fog;
QualitySettings.maximumLODLevel = _maxLod;
QualitySettings.lodBias = _lodBias;
}
}
}
}
URP 中没有 OnWillRenderObject 函数了,使用需要使用 C# 的 event 机制,订阅 beginCameraRendering 函数,脚本中的主函数是 runPlannarReflection ,保持与订阅事件相同函数签名。
UpdateReflectionCamera 是更新反射相机的函数,有两个实现,一种是不使用反射矩阵的,一种是使用的,两种方法切换时要切换 GL.invertCulling,因为使用反射矩阵会让相机的绕序反向。
参考
Unity Shader-反射效果(CubeMap,Reflection Probe,Planar Reflection,Screen Space Reflection)
破晓:Unity平面反射实现
http://www.terathon.com/lengyel/Lengyel-Oblique.pdf
Achieve beautiful, scalable, and performant graphics with the Universal Render Pipeline | Unity Blog
https://github.com/Unity-Technologies/BoatAttack |
本帖子中包含更多资源
您需要 登录 才可以下载或查看,没有账号?立即注册
×
|