自言自语
又是好久没有更新笔记了。最近项目真的很忙。一直想更新的笔记现在才有空梳理。今天要记载的就是平面反射。
一、C# builltin
由于平面反射一般只需要传RT(RenderTexture)给shader使用。所以这次笔记就不用放上相应shader了。具体可以用平面反射实现水面效果,镜面反射,倒影等等一些列效果。
并且,由于转换矩阵都是复杂的推理和运算。超出了我这个美术的现有知识和理解范畴,因此我都是借鉴前辈的函数直接使用。自己只是根据需求做了一些变更。因此就不放相应的平面反射矩阵了。网上前辈们实现好的也很多。Unity也有封装好已经实现的函数可以直接使用。比如/利用Unity提供的 API CalculateObliqueMatrix 来做斜视锥体投影矩阵。
这里我只记录部分自己做了修改的代码。主要是提醒自己关于RT的管理以及防止RT内存泄漏的问题。
首先声明一个枚举来设置RT尺寸
//声明一个枚举类型来控制RT尺寸
public enum RTSize
{
_128 = 128, _256 = 256, _512 = 512, _1024 = 1024, _2048 = 2048, _4096 = 4096, _8192 = 8192,
}
public RTSize ReflectTexSize = RTSize._1024;
然后进行场景初始化清理
private void Start()
{
//不管场景中有没有 运行前先清掉一波反射相机
//这里使用DestroyImmediate方法主要是要再 editmode下使其生效
DestroyImmediate(GameObject.Find("Reflection Camera"));
DestroyImmediate(GameObject.Find("Character Reflection Camera"));
noTexture = new RenderTexture(1, 1, 0);
}
在OnwillRender方法中传参并调用平面反射的具体运算
private void OnWillRenderObject()
{
Shader.SetGlobalTexture("_ReflectTex", noTexture);
Shader.SetGlobalTexture("_SourceTex", noTexture);
Shader.SetGlobalTexture("_CHReflectTex", noTexture);
Shader.SetGlobalTexture("_CHSourceTex", noTexture);
PlanarReflectionBlur();
CharacterPlanarReflectionBlur();
}
进行场景物件的平面反射计算 (这里我把场景角色做了区分,分别用两个反射相机来做。可以实现更多自定义。原理都一样 就只放一个场景的吧)
另外Camera.main 和 Camera.current 在不同Unity版本和管线产生的观察效果不同.至于为什么不懂…可能是Unity玄学.这点在相应代码处已注释明白
private void PlanarReflectionBlur()
{
//如果没有渲染 也就是脚本勾掉 则返回空 否则 设置渲染状态为true 并进行后边操作
if (isReflectionCameraRendering == true)
return;
isReflectionCameraRendering = true;
//如果没有反射相机 创建一个反射相机
if (!reflectionCamera)
{
//不管场景中有没有 运行前先清掉一波反射相机
DestroyImmediate(GameObject.Find("Reflection Camera"));
//先声明个物体 名字叫 Reflection Camera tag设置好
GameObject go = new GameObject("Reflection Camera")
{
tag = "ReflectCamera",
};
//然后给这个物体添加相机组件
reflectionCamera = go.AddComponent<Camera>();
//并把当前相机中的相关参数 copy给反射相机
reflectionCamera.CopyFrom(Camera.main);
reflectionCamera.cullingMask &= ~(1 << 8);
reflectionCamera.cullingMask &= ~(1 << 5);
reflectionCamera.cullingMask &= ~(1 << 9);
}
//如果没有RT 就设置个RT 从当前缓存拿到一个RT 大小格式设置好
if(!sourceRT)
{
sourceRT = RenderTexture.GetTemporary((int)ReflectTexSize / downSample, (int)ReflectTexSize / downSample, 24);
}
//需要实时同步相机的参数,比如编辑器下滚动滚轮,Editor相机的远近裁剪面就会变化
UpdateCamearaParams(Camera.main, reflectionCamera);
//设置反射相机的目标RT 为刚才声明的RT
reflectionCamera.targetTexture = sourceRT;
//将反射相机先设置为false 因为并不需要他真的传入屏幕图像 只是把拍到的传入RT中 所以关闭
reflectionRT = RenderTexture.GetTemporary((int)ReflectTexSize / downSample, (int)ReflectTexSize / downSample, 24);
Graphics.Blit(sourceRT, reflectionRT);
reflectionCamera.enabled = false;
//即需要保证平面模型的原点在平面上,否则可以尝试增加offset偏移 这里我就加了offsetplanar
Transform planarPos = GetComponent<Transform>();
planarPos.localPosition = new Vector3(0, offsetPlanar, 0);
//根据上文平面定义,需要平面法向量和平面上任意点,此处使用transform.up为法向量,transform.position为平面上的点
//在不懂的情况下 直接抄一个平面反射矩阵
var reflectM = CaculateReflectMatrix(transform.up, transform.position);
//将反射相机的转换矩阵设置为当前相机拍到的平面的反射矩阵. 注意都是在相机空间下进行的转换
//builltin在我使用的Unity版本中有个BUG 如果 采用Camera.current相机则会造成反射相机的FOV不实时更新,需要销毁RT重新创建才生效.无法实现效果.因此只能调用Camera.main 但带来的问题就是Scene窗口和Game窗口观察效果不一致.
//后经其他版本,包括URP管线测试,则没有此问题,可以使用Camera.current相机来使Scene窗口观察效果与Game窗口一致
reflectionCamera.worldToCameraMatrix = Camera.main.worldToCameraMatrix * reflectM;
//确定平面的法线朝向
var normal = transform.up;
//求得目标点经过平面的倒影的距离 相关原理能看懂 但矩阵着实弄不明白
var d = -Vector3.Dot(normal, transform.position);
//将平面到点的距离
Vector4 plane = new Vector4(normal.x, normal.y, normal.z, d);
//将平面利用相机空间的逆转置矩阵 把平面转换到反射相机空间下
Vector4 viewSpacePlane = reflectionCamera.worldToCameraMatrix.inverse.transpose * plane;
//利用Unity提供的 API CalculateObliqueMatrix 来做斜视锥体投影矩阵
var clipMatrix = Camera.main.CalculateObliqueMatrix(viewSpacePlane);
//再把转换好的平面 传递给 反射相机的投影矩阵 使相机成像
reflectionCamera.projectionMatrix = clipMatrix;
reflectionCamera.depthTextureMode |= DepthTextureMode.Depth;
//需要将背面裁剪反过来,因为仅改变了顶点,没有改变法向量,绕序反向,裁剪会不对
//这段也是完全没懂
GL.invertCulling = true;
reflectionCamera.Render();
GL.invertCulling = false;
//高斯模糊
if (IsBlur == false)
{
如果没有反射材质 则去获取拿到当前平面的渲染组件 获得其材质
//if (reflectionMaterial == null)
//{
// Renderer renderer = GetComponent<Renderer>();
// //因为要可读写 所以需要用到共享材质 而不是Clone材质
// reflectionMaterial = renderer.sharedMaterial;
// //然后把相机拍到的反射RT 传到材质中
//}
Shader.SetGlobalTexture("_ReflectTex", sourceRT);
Shader.SetGlobalTexture("_SourceTex", sourceRT);
}
else
{
if (blurShader == null)
{
//if (reflectionMaterial == null)
//{
// Renderer renderer = GetComponent<Renderer>();
// //因为要可读写 所以需要用到共享材质 而不是Clone材质
// reflectionMaterial = renderer.sharedMaterial;
// //然后把相机拍到的反射RT 传到材质中
//}
Shader.SetGlobalTexture("_ReflectTex", sourceRT);
Shader.SetGlobalTexture("_SourceTex", sourceRT);
}
else
{
if (blurMaterial == null)
{
blurMaterial = new Material(blurShader);
}
//在这里用FOR循环引入Gaussian模糊迭代次数
for (int i = 0; i < iteration; i++)
{
//循环迭代前设置好模糊距离即shader中进行卷积操作的采样范围
blurMaterial.SetFloat("_BlurSize", 1.0f + i * blurSpread);
RenderTexture buffer = RenderTexture.GetTemporary((int)ReflectTexSize / downSample, (int)ReflectTexSize / downSample,24);
//然后经过shader中第一个PASS的verticalblur后,把buffer0存入到buffer中
Graphics.Blit(reflectionRT, buffer, blurMaterial, 0);
//图像再用shader中的第二个pass 进行一次horizontalblur 存入到buffer1中
Graphics.Blit(buffer, reflectionRT, blurMaterial, 1);
//此时就是已经经历过一次完整迭代的 纵横卷积的图像 存入到buffer1中了 释放buffer0的缓存 以备后用
RenderTexture.ReleaseTemporary(buffer);
}
//if (reflectionMaterial == null)
//{
// Renderer renderer = GetComponent<Renderer>();
// //因为要可读写 所以需要用到共享材质 而不是Clone材质
// reflectionMaterial = renderer.sharedMaterial;
// //然后把相机拍到的反射RT 传到材质中
//}
Shader.SetGlobalTexture("_ReflectTex", reflectionRT);
Shader.SetGlobalTexture("_SourceTex", sourceRT);
}
}
RenderTexture.ReleaseTemporary(reflectionRT);
isReflectionCameraRendering = false;
}
最后在Disable下清除反射相机和RT
private void OnDisable()
{
//清楚RT 并销毁相机
RenderTexture.ReleaseTemporary(sourceRT);
RenderTexture.ReleaseTemporary(reflectionRT);
RenderTexture.ReleaseTemporary(CHsourceRT);
RenderTexture.ReleaseTemporary(CHreflectionRT);
noTexture.Release();
if (reflectionCamera)
DestroyImmediate(reflectionCamera.gameObject);
if (characterReflectionCamera)
DestroyImmediate(characterReflectionCamera.gameObject);
reflectionCamera = null;
characterReflectionCamera = null;
}
实时计算相机参数的方法
//这个就是实时保持反射相机的参数和主相机参数一致
private void UpdateCamearaParams(Camera srcCamera, Camera destCamera)
{
//如果目标相机和原相机有一个不存在则返回 不做操作 否则当他们都存在的时候进行如下步骤统一参数
if (destCamera == null || srcCamera == null)
return;
destCamera.clearFlags = CameraClearFlags.SolidColor;
destCamera.backgroundColor = Color.clear;
destCamera.farClipPlane = srcCamera.farClipPlane;
destCamera.nearClipPlane = srcCamera.nearClipPlane;
destCamera.orthographic = srcCamera.orthographic;
destCamera.fieldOfView = srcCamera.fieldOfView;
destCamera.aspect = srcCamera.aspect;
destCamera.orthographicSize = srcCamera.orthographicSize;
destCamera.allowMSAA = srcCamera.allowMSAA;
destCamera.allowHDR = true;
}
二、URP
URP下调用相机的方法有所区别,其他都与builltin大同小异 这里只记录调用相机渲染的不同之处
GL.invertCulling = true;
//reflectionCamera.Render();
//RenderSingleCamera(context,camra) 这个方法看了元数据也没懂啥意思 暂且先抄之。。后续再啃
UniversalRenderPipeline.RenderSingleCamera(context,reflectionCamera);
GL.invertCulling = false;
总结
C#代码学过一段时间,但是关于Unity API及各个版本和管线的变化实在有点繁杂。只能在实践中慢慢掌握了。路真的漫漫啊。