平面反射
1.平面反射的原理
平面可以用来模拟光滑度很高的镜面效果,但是只能用在高度一致的平面,很多水体渲染方案中的反射部分,就是用平面反射来解决的,下面先讲一下实现平面反射的方法思路。
1.1基本思路
最简单的光线反射向量推导:
由反射现象可知,入射角等于反射角,对于反射材质,我们只能利用现有的法线和入射光线的方向来计算反射光线的方向,所以进行下面的计算:
求反射光线的代码:
float3 reflect(float3 I, float3 N)
{
return I - 2 * dotProduct(I, N) * N;
}
Unity直接提供reflect函数,传入入射光线和法线方向的单位矢量,即可以得出出射方向的单位矢量。
平面反射实现思路:
当前使用相机E,看向P、Q两点时,根据光线的反射原理,在P、Q两点应该看到的是A、B两点,所以我们的思路是:在这个平面采样一张贴图,贴图上的P点对应A点的颜色,Q点对应B点的颜色,并且保证光照正确,那这一张贴图该如何获取?如何采样?
想象出一个物体关于平面对称后产生一个倒立的一模一样的物体,原物体的 A、B 对应想象物体的A’、B’,根据光线的反射原理推出,相机 E要在P点通过反射看到A点,等同于相机的视线穿过平面看到A’点,我们只要采样到相机E此时看到的A’点时的颜色就可以了。
按上面的方法,我们有了反射平面的贴图了,该怎么采样它才能正确的显示呢?通过上面的推导和图可以知道, 当我们在模型变换后乘以反射矩阵,那么物体对应的点(仍然从原来摄像机的视角看过去,物体是先变换到对称位置再进行VP变换,此时我们看到的物体即是反射了的点)
1.2实现步骤
1.根据已知平面,计算出该平面的镜像矩阵R
2.对当前摄像机进行拷贝,得到一个新的相机,在相机的MVP矩阵的V之前乘以反射矩阵R,即得到MRVP(因为下面我们求得的反射矩阵是在世界空间下,将物体的点变换到关于世界空间中的平面的镜像点,所以在模型变换之后,得到点的世界坐标,需要先进行R变换后再进行VP的变换,R变换之后,其实我们是从和原来一样的视角看到物体在镜像的一个位置)
3.根据平面向量计算镜像相机的斜裁剪矩阵,以裁剪平面负方向的不可视区域
4.渲染镜像相机得到RenderTexture并投影到平面的材质shader,在shader中计算投影坐标最终得到镜面反射效果
2.反射矩阵
2.1反射矩阵的作用:
用来将物体的世界空间位置转移到对应的反射像的世界空间位置的矩阵。
2.2反射矩阵的推导过程:
由平面点法式可知:平面可以由平面上任意一点和垂直于平面的任意一个向量确定,这个垂直于平面的向量就是平面的法向量:
假设平面上有一个确定的点P0(x0,y0,z0),法向量为N(a,b,c),那么平面上任意一个点P(x,y,z)满足:P0P * N = 0,
即(x - x0, y - y0, z- z0) * (a, b, c) = 0,点法式展开,则为一般式:ax + by + cz + (-a * x0 - b * y0 - c * z0) = 0,其中(-a * x0 - b * y0 - c * z0)为常数,表示为d,则一般式可以表示为:
ax + by + cz + d = 0
所以点法式可以表示为:
P * N + d = 0,其中P为平面上任意一点,N为平面法线向量,而d则表示:d = -P * N = -dot(P, N),要求d,则知道N和平面上任意一点就可以,这个点可以为P0,所以d = -dot(P0, N);
下图是关于平面对称点的图:
由上图可知,我们只要求解出|AB|就可以通过已知的条件求解出平面的对称点,所以我们的问题可以转变为:
求世界空间中A点到平面P * N + d = 0的距离|AB|,再来一张图,推导一下AB的距离:
注意:图中P为平面上的任一点,A为空间中的任意一点,B为点A在平面上的投影点,N为平面法线
由图,我们已经得到|AB| = dot(A, N) - dot(P, N),而平面上任意一点可以表示为dot(P, N) + d = 0,所以:
dot(P, N) = -d,
所以:|AB| = dot(A, N) + d
而由上面推导的A’ = A - 2* N * |AB|可得:
A‘ = A - 2 * N * [dot(A, N) + d]
进行简化拆分:
到此,我们就得到了世界空间中某一个点变换到某一个平面的反射成像点的反射矩阵,在矩阵中,d是 -dot(P, N),其中N为该平面的法向量,P为平面上某一个点在世界空间中的坐标
3.平面反射代码
3.1摄像机脚本
挂在用于获取反射贴图的摄像机
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.Rendering.Universal;
using UnityEngine.SceneManagement;
using UnityEngine.UIElements;
using UnityEditor;
public class Planar_Reflection_camera_Script1 : MonoBehaviour
{
[ExecuteAlways]
public Camera Main_camera;
public Camera Reflection_camera;
public Camera Scene_Reflection_camera;
public GameObject Plane;
private readonly RenderTexture _Reflection_camera_RT;//定义反射RT
private int _Reflection_camera_RT_ID;//定义主纹理_MainTex属性名称的ID
public Material Reflection_material;//传入材质
private readonly RenderTexture _Scene_Reflection_camera_RT;
private int _Scene_Reflection_camera_RT_ID;
public Shader shader;
void Start()//Start函数在脚本运行开始的时候执行
{
Debug.Log("Planar Reflection succes!");
if (this.Reflection_camera == null)
{
var R_gameobject = new GameObject("Reflection camera");//申请新组件
this.Reflection_camera = R_gameobject.AddComponent<Camera>();//获取Camera类型的组件
}
RenderPipelineManager.beginCameraRendering += OnBeginCameraRendering;//订阅事件
}
void OnBeginCameraRendering(ScriptableRenderContext context, Camera camera)
{
if (camera == this.Reflection_camera)
{
Update_camera(this.Reflection_camera);
camera.clearFlags = CameraClearFlags.SolidColor;//清除刚初始化信息的Reflection_camera中的DepthBuffer和ColorBuffer,用Background属性颜色替代
camera.backgroundColor = Color.clear;//清除掉背景颜色
camera.cullingMask = LayerMask.GetMask("Reflection");//确定摄像机的渲染层
var Reflection_camera_M = CalculateReflectionCameraMatrix(this.Plane.transform.up, this.Plane.transform.position);//构建反射矩阵
Reflection_camera.worldToCameraMatrix = Reflection_camera.worldToCameraMatrix * Reflection_camera_M;//在进VP变换之前
GL.invertCulling = true;//进行裁剪顺序的翻转
UniversalRenderPipeline.RenderSingleCamera(context, camera);//摄像机开始渲染
RenderTexture Reflection_camera_temporary_RT = RenderTexture.GetTemporary(Screen.width, Screen.height, 0);//创建临时的RT(截图保存)
print(Screen.width);
print(Screen.height);
_Reflection_camera_RT_ID = Shader.PropertyToID("_Reflection_camera_RT");//先获取着色器属性名称_Reflection_camera_RT的唯一标识符_Reflection_camera_RT_ID
Shader.SetGlobalTexture(_Reflection_camera_RT_ID, Reflection_camera_temporary_RT);//再利用_Reflection_camera_RT_ID和Reflection_camera_temporary_RT为所有着色器设置一个全局纹理
camera.targetTexture = Reflection_camera_temporary_RT;//设置自定义的渲染纹理为摄像机的目标纹理,定义完成后,反射相机的输出就会输出到纹理上
Reflection_material.SetTexture(_Reflection_camera_RT_ID, Reflection_camera_temporary_RT);//将贴图传进材质
RenderTexture.ReleaseTemporary(Reflection_camera_temporary_RT);//释放掉临时纹理
}
else
{
GL.invertCulling = false;
}
}
private void OnDisable()
{
RenderPipelineManager.beginCameraRendering -= OnBeginCameraRendering;//取消事件订阅
}
private void Update_camera(Camera Reflection_camera)//同步两个摄像机的数据,相当于反射相机初始化
{
if (Reflection_camera == null||this.Main_camera == null)
return;
//先同步两个摄像机的数据,初始化完Reflection_camera后立刻背景颜色和深度的清除,然后设置在相机开始渲染前的各种设置
int target_display = Reflection_camera.targetDisplay;
Reflection_camera.CopyFrom(this.Main_camera);
Reflection_camera.targetDisplay = target_display;
}
private Matrix4x4 CalculateReflectionCameraMatrix(Vector3 N, Vector3 plane_position)//计算返回反射矩阵
{
//下面计算反射矩阵是在世界空间计算的
Matrix4x4 Reflection_camera_M = Matrix4x4.identity;//初始化反射矩阵
float d = -Vector3.Dot(plane_position, N);//d = -dot(P, N),P是平面上的任意一点,N是平面的法向量
Reflection_camera_M.m00 = 1 - 2 * N.x * N.x;
Reflection_camera_M.m01 = - 2 * N.x * N.y;
Reflection_camera_M.m02 = - 2 * N.x * N.z;
Reflection_camera_M.m03 = - 2 * N.x * d;
Reflection_camera_M.m10 = - 2 * N.x * N.y;
Reflection_camera_M.m11 = 1 - 2 * N.y * N.y;
Reflection_camera_M.m12 = - 2 * N.y * N.z;
Reflection_camera_M.m13 = - 2 * N.y * d;
Reflection_camera_M.m20 = - 2 * N.x * N.z;
Reflection_camera_M.m21 = - 2 * N.y * N.z;
Reflection_camera_M.m22 = 1 - 2 * N.z * N.z;
Reflection_camera_M.m23 = - 2 * N.z * d;
Reflection_camera_M.m30 = 0;
Reflection_camera_M.m31 = 0;
Reflection_camera_M.m32 = 0;
Reflection_camera_M.m33 = 1;
return Reflection_camera_M;
}
}
3.2进行RT贴图采样(shader)
这里同时进行了了天空盒的反射
Shader "Unlit/PlanerReflection_shader"
{
Properties
{
_Diffuse("Diffuse", Color) = (1, 1, 1, 1)
_ReflectColor("_ReflectColor", Color) = (1, 1, 1, 1)
_ReflectAmount("_ReflectAmount", Range(0,1)) = 1
_Reflection_CubeMap("_Reflection_CubeMap", Cube) = "_Skybox"{}
_Fresnel("Fresnel", Float) = 1.0
_Specular("Specular", Color) = (1, 1, 1, 1)
_Gloss("Gloss", Range(8,255)) = 8.0
}
SubShader
{
Tags {"Queue" = "Transparent" "RenderType" = "Transparent" "RenderPipeline" = "UniversalPipeline"}
Pass
{
Tags {"LightMode" = "UniversalForward" }
HLSLPROGRAM
#pragma vertex vert
#pragma fragment frag
#pragma multi_compile _ _MAIN_LIGHT_SHADOWS //接收阴影
#pragma multi_compile _ _MAIN_LIGHT_SHADOWS_CASCADE //得到正确的阴影坐标
#pragma multi_compile _ _SHADOWS_SOFT //软阴影
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Lighting.hlsl"
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Shadows.hlsl"
CBUFFER_START(UnityPerMaterial)
half4 _Diffuse;
float4 _ReflectColor;//用于控制反射的天空盒子的颜色量
float _ReflectAmount;//用于控制反射的天空盒子的颜色和漫反射diffuse在总体反射中的占比
samplerCUBE _Reflection_CubeMap;
float _Fresnel;
half4 _Specular;
half _Gloss;
TEXTURE2D(_Reflection_camera_RT);
SamplerState sampler_Reflection_camera_RT;//采样采样设置
TEXTURE2D(_Reflection_Scene_camera_RT);
SamplerState sample_Reflection_Scene_camera_RT;
CBUFFER_END
struct appdata
{
float4 vertex : POSITION;
float3 normal : NORMAL;
//float2 uv: TEXCOORD0;
};
struct v2f
{
float4 position_CS : SV_POSITION;//裁剪空间的坐标
float4 screenPos : TEXCOORD0;//屏幕坐标
float4 position_WS : TEXCOORD1;//顶点位置坐标
float3 view_direct_WS : TEXCOORD2;//人眼光路的入射方向
float3 reflec_direct_WS : TEXCOORD3;//人眼光路反射后的方向,用于采样天空盒
float3 normal_WS : TEXCOORD4;//顶点在世界空间的法线方向
};
v2f vert (appdata v)
{
v2f o;
o.position_CS = TransformObjectToHClip(v.vertex);//获得顶点在裁剪空间的坐标
o.position_WS = mul(unity_ObjectToWorld, v.vertex);
o.screenPos = ComputeScreenPos(o.position_CS);//计算用于采样反射贴图的顶点
o.normal_WS = TransformObjectToWorldNormal(v.normal);//计算世界空间下的法线方向
o.view_direct_WS = GetWorldSpaceViewDir(o.position_WS);//获取世界空间下的人眼光路的入射方向,这里获取的方向是从顶点指向摄像机
o.reflec_direct_WS = reflect(-normalize(o.view_direct_WS),normalize(o.normal_WS));//获取世界空间下人眼光路的反射光线
return o;
}
half4 frag (v2f i) : SV_Target
{
float4 SHADOW_COORDS = TransformWorldToShadowCoord(i.position_WS);//获取将世界空间的顶点坐标转换到光源空间获取的阴影坐标,这里在片元着色器里面进行,利用了插值之后的结果
Light main_light = GetMainLight(SHADOW_COORDS);
float3 light_direction_WS = normalize(TransformObjectToWorld(main_light.direction));//获取世界空间的光照单位矢量
float3 view_direct_WS = normalize(i.view_direct_WS);
float3 reflec_direct_WS = normalize(i.reflec_direct_WS);
float3 normal_WS = normalize(i.normal_WS);
half3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;//获取环境光强度
half3 diffuse = main_light.color.rgb * _Diffuse.rgb * saturate(dot(normal_WS, light_direction_WS));//获取漫反射强度,light.color.rgb是光照强度
half3 reflection = texCUBE(_Reflection_CubeMap,reflec_direct_WS).rgb * _ReflectColor.rgb;//获取反射的天空盒子的颜色
half3 half_dir = normalize(light_direction_WS + view_direct_WS);//获取半程向量
half3 specular = main_light.color.rgb * _Specular.rgb * pow(saturate(dot(half_dir,normal_WS)), _Gloss);//获取高光
reflection = lerp(diffuse, reflection, _ReflectAmount);//通过平滑函数获得最佳的反射
float fresnel = pow(1 - dot(normal_WS, view_direct_WS), _Fresnel);
half4 reflection_map_color = _Reflection_camera_RT.Sample(sampler_Reflection_camera_RT, i.screenPos.xy / i.position_CS.w);//采样反射纹理贴图
half4 color = half4(ambient + fresnel * reflection + specular, 1.0);
color = reflection_map_color + color;
return color;
}
ENDHLSL
}
}
Fallback off
}
3.3进行视锥体的裁剪
不对视锥体进行裁剪的话就会出现上面奇怪的现象,当实物移动到平面下面的时候,在平面上还是会出现其反射的像
具体方法过程如下:
思路:
视锥体要以反射平面为近平面,如下图,视锥体A部分要剔除,只保留B部分
分析:
要进行视锥体裁剪,需要用到斜截视锥体的技术:
通过改变MVP矩阵的中的P矩阵,实现用指定平面来当作近平面,同时影响远平面,而其他四个平面不受到影响。
推导参考以下文章:
https://vn.sru.baidu.com/r/TLGAzdGavS?f=qf&u=9c5e09c2692df66a
所以在订阅的事件内,在相机渲染之前,对反射相机(已获取的反射的像)进行视锥体裁剪,代码如下:
//下面进行视锥体裁剪
Vector4 viewPlane = new Vector4(this.Plane.transform.up.x,
this.Plane.transform.up.y,
this.Plane.transform.up.z,
-Vector3.Dot(this.Plane.transform.position, this.Plane.transform.up));//用四维向量表示平面
viewPlane = Reflection_camera.worldToCameraMatrix.inverse.transpose * viewPlane;//将世界空间中的平面表示转换成相机空间中的平面表示
var ClipMatrix = Reflection_camera.CalculateObliqueMatrix(viewPlane);//获取以反射平面为近平面的投影矩阵
Reflection_camera.projectionMatrix = ClipMatrix;//获取新的投影矩阵
最终的反射相机的脚本代码:
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.Rendering.Universal;
using UnityEngine.SceneManagement;
using UnityEngine.UIElements;
using UnityEditor;
public class Planar_Reflection_camera_Script1 : MonoBehaviour
{
[ExecuteAlways]
public Camera Main_camera;
public Camera Reflection_camera;
public Camera Scene_Reflection_camera;
public GameObject Plane;
private readonly RenderTexture _Reflection_camera_RT;//定义反射RT
private int _Reflection_camera_RT_ID;//定义主纹理_MainTex属性名称的ID
public Material Reflection_material;//传入材质
private readonly RenderTexture _Scene_Reflection_camera_RT;
private int _Scene_Reflection_camera_RT_ID;
public Shader shader;
void Start()//Start函数在脚本运行开始的时候执行
{
Debug.Log("Planar Reflection succes!");
if (this.Reflection_camera == null)
{
var R_gameobject = new GameObject("Reflection camera");//申请新组件
this.Reflection_camera = R_gameobject.AddComponent<Camera>();//获取Camera类型的组件
}
RenderPipelineManager.beginCameraRendering += OnBeginCameraRendering;//订阅事件
}
void OnBeginCameraRendering(ScriptableRenderContext context, Camera camera)
{
if (camera == this.Reflection_camera)
{
Update_camera(this.Reflection_camera);
camera.clearFlags = CameraClearFlags.SolidColor;//清除刚初始化信息的Reflection_camera中的DepthBuffer和ColorBuffer,用Background属性颜色替代
camera.backgroundColor = Color.clear;//清除掉背景颜色
camera.cullingMask = LayerMask.GetMask("Reflection");//确定摄像机的渲染层
var Reflection_camera_M = CalculateReflectionCameraMatrix(this.Plane.transform.up, this.Plane.transform.position);//构建反射矩阵
Reflection_camera.worldToCameraMatrix = Reflection_camera.worldToCameraMatrix * Reflection_camera_M;//在进VP变换之前
GL.invertCulling = true;//将裁剪顺序翻转回去,因为反射矩阵的变化会引起裁剪顺序的变化
//下面进行视锥体裁剪
Vector4 viewPlane = new Vector4(this.Plane.transform.up.x,
this.Plane.transform.up.y,
this.Plane.transform.up.z,
-Vector3.Dot(this.Plane.transform.position, this.Plane.transform.up));//用四维向量表示平面
viewPlane = Reflection_camera.worldToCameraMatrix.inverse.transpose * viewPlane;//将世界空间中的平面表示转换成相机空间中的平面表示
var ClipMatrix = Reflection_camera.CalculateObliqueMatrix(viewPlane);//获取以反射平面为近平面的投影矩阵
Reflection_camera.projectionMatrix = ClipMatrix;//获取新的投影矩阵
UniversalRenderPipeline.RenderSingleCamera(context, camera);//摄像机开始渲染
RenderTexture Reflection_camera_temporary_RT = RenderTexture.GetTemporary(Screen.width, Screen.height, 0);//创建临时的RT(截图保存)
_Reflection_camera_RT_ID = Shader.PropertyToID("_Reflection_camera_RT");//先获取着色器属性名称_Reflection_camera_RT的唯一标识符_Reflection_camera_RT_ID
Shader.SetGlobalTexture(_Reflection_camera_RT_ID, Reflection_camera_temporary_RT);//再利用_Reflection_camera_RT_ID和Reflection_camera_temporary_RT为所有着色器设置一个全局纹理
camera.targetTexture = Reflection_camera_temporary_RT;//设置自定义的渲染纹理为摄像机的目标纹理,定义完成后,反射相机的输出就会输出到纹理上
Reflection_material.SetTexture(_Reflection_camera_RT_ID, Reflection_camera_temporary_RT);//将贴图传进材质
RenderTexture.ReleaseTemporary(Reflection_camera_temporary_RT);//释放掉临时纹理
}
else
{
GL.invertCulling = false;
}
}
private void OnDisable()
{
RenderPipelineManager.beginCameraRendering -= OnBeginCameraRendering;//取消事件订阅
}
private void Update_camera(Camera Reflection_camera)//同步两个摄像机的数据,相当于反射相机初始化
{
if (Reflection_camera == null||this.Main_camera == null)
return;
//先同步两个摄像机的数据,初始化完Reflection_camera后立刻背景颜色和深度的清除,然后设置在相机开始渲染前的各种设置
int target_display = Reflection_camera.targetDisplay;
Reflection_camera.CopyFrom(this.Main_camera);
Reflection_camera.targetDisplay = target_display;
}
private Matrix4x4 CalculateReflectionCameraMatrix(Vector3 N, Vector3 plane_position)//计算返回反射矩阵
{
//下面计算反射矩阵是在世界空间计算的
Matrix4x4 Reflection_camera_M = Matrix4x4.identity;//初始化反射矩阵
float d = -Vector3.Dot(plane_position, N);//d = -dot(P, N),P是平面上的任意一点,N是平面的法向量
Reflection_camera_M.m00 = 1 - 2 * N.x * N.x;
Reflection_camera_M.m01 = - 2 * N.x * N.y;
Reflection_camera_M.m02 = - 2 * N.x * N.z;
Reflection_camera_M.m03 = - 2 * N.x * d;
Reflection_camera_M.m10 = - 2 * N.x * N.y;
Reflection_camera_M.m11 = 1 - 2 * N.y * N.y;
Reflection_camera_M.m12 = - 2 * N.y * N.z;
Reflection_camera_M.m13 = - 2 * N.y * d;
Reflection_camera_M.m20 = - 2 * N.x * N.z;
Reflection_camera_M.m21 = - 2 * N.y * N.z;
Reflection_camera_M.m22 = 1 - 2 * N.z * N.z;
Reflection_camera_M.m23 = - 2 * N.z * d;
Reflection_camera_M.m30 = 0;
Reflection_camera_M.m31 = 0;
Reflection_camera_M.m32 = 0;
Reflection_camera_M.m33 = 1;
return Reflection_camera_M;
}
}
补充资料:
Camera.clearFlags:
CommandBuffer.GetTemporaryRT
该函数不仅申请了一个临时rt,同时还将该rt与第一个参数"nameID"所代表的全局的ShaderProperty进行了绑定,也就是说之后的shader都可以用与nameID对应的名称使用该rt。
官方的urp渲染管线中,CopyColorPass就是用该函数将"_CameraColorTexture"这个纹理拷贝到临时rt中供之后的shader使用。
坑点:在调用CommandBuffer.ReleaseTemporaryRT进行释放并不是将该纹理清空了,该纹理依旧在内存中,如果在之后重新调用CommandBuffer.GetTemporaryRT申请一个大小格式相同的临时纹理,会拿到该纹理,也就是说CommandBuffer.GetTemporaryRT得到的不一定是一张干净的纹理,很有可能是已经被写过的,所以必要的时侯要进行clear。其次,如果发生了上述这种回收重用的情况,rt的名称可能会错乱,也就是说尽管shaderproperty的绑定逻辑是没有问题,但是rt的name没有相应的更新,在framedebugger中会让人十分困惑,例如两个相机,分别在afterRenderingTransparent之后进行了一次全屏copyblit,前者给了_Transparent的rt,后者给了_UITranspaernt的rt,但是之后发现在前一个相机渲染中,物体shader中_Transparent获取的纹理名称叫_UITranspaernt,在使用renderdoc会发现两次全屏copyblit的目标纹理使用的是同一个rt,自始至终只有一个叫_UITranspaernt的rt,该rt仅仅是在过程中更换一下和shaderproperty的绑定,因此shader中的_Transparent和_UITranspaernt都会得到该rt。虽然在逻辑上没有错,最终结果是正确的,但是会给debug过程中带来困扰,如果想要规避这种情况可以显示地使用new RenderTexture创建rt。
ComputeScreenPos
注意:unity之所以不在顶点着色器中除以w分量,是因为要留到片段着色器进行线性插值后再除以w,这样得到的结果是正确的。如果提前除以w再经过片段着色器线性差值后,得到的结果就不准确,因为投影空间不是线性空间
Unity Shader ProjectionParams
文章来源:https://www.toymoban.com/news/detail-411979.html
Camera.CalculateObliqueMatrix
文章来源地址https://www.toymoban.com/news/detail-411979.html
到了这里,关于[Unity/URP学习]平面反射(PlanarReflection)的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!