Unity實時反射相關優化

這是侑虎科技第1072篇文章,感謝作者羅漢銘供稿。歡迎轉發分享,未經作者授權請勿轉載。如果您有任何獨到的見解或者發現也歡迎聯繫我們,一起探討。(QQ羣:793972859)

羅漢銘:朝夕光年無雙工作室 技術美術專家,目前就職於字節跳動101技術中心,08年加入上海昱泉國際,從事TA相關工作,參與了《流星蝴蝶劍》、《射鵰三部曲》項目的製作。之後加入完美世界引擎組,從事ARK1.0-ARK2.0的相關開發工作,並支持《最終兵器》、《射鵰英雄傳OL》項目的開發;後轉向Unity,參與了《射鵰英雄傳3D》手遊的開發工作。

8人同屏,最高畫質,耗時1.1ms(高通 驍龍710)。因爲項目未上線原因,只能用測試圖進行相關說明。

1. 源碼

using UnityEngine;using System.Collections;

[ExecuteInEditMode]public class MirrorReflection : MonoBehaviour{public Material m_matCopyDepth;public bool m_DisablePixelLights = true;public int m_TextureSize = 256;public float m_ClipPlaneOffset = 0.07f;

public LayerMask m_ReflectLayers = -1;

private Hashtable m_ReflectionCameras = new Hashtable(); // Camera -> Camera table

public RenderTexture m_ReflectionTexture = null;public RenderTexture m_ReflectionDepthTexture = null;private int m_OldReflectionTextureSize = 0;

private static bool s_InsideRendering = false;

public void OnWillRenderObject(){if (!enabled || !GetComponent() || !GetComponent().sharedMaterial || !GetComponent().enabled)return;

Camera cam = Camera.current;if (!cam)return;

// Safeguard from recursive reflections.if (s_InsideRendering)return;s_InsideRendering = true;

Camera reflectionCamera;CreateMirrorObjects(cam, out reflectionCamera);

// find out the reflection plane: position and normal in world spaceVector3 pos = transform.position;Vector3 normal = transform.up;

// Optionally disable pixel lights for reflectionint oldPixelLightCount = QualitySettings.pixelLightCount;if (m_DisablePixelLights)QualitySettings.pixelLightCount = 0;

UpdateCameraModes(cam, reflectionCamera);

// Render reflection// Reflect camera around reflection planefloat d = -Vector3.Dot(normal, pos) - m_ClipPlaneOffset;Vector4 reflectionPlane = new Vector4(normal.x, normal.y, normal.z, d);

Matrix4x4 reflection = Matrix4x4.zero;CalculateReflectionMatrix(ref reflection, reflectionPlane);Vector3 oldpos = cam.transform.position;Vector3 newpos = reflection.MultiplyPoint(oldpos);reflectionCamera.worldToCameraMatrix = cam.worldToCameraMatrix * reflection;

// Setup oblique projection matrix so that near plane is our reflection// plane. This way we clip everything below/above it for free.Vector4 clipPlane = CameraSpacePlane(reflectionCamera, pos, normal, 1.0f);Matrix4x4 projection = cam.projectionMatrix;CalculateObliqueMatrix(ref projection, clipPlane);reflectionCamera.projectionMatrix = projection;

reflectionCamera.cullingMask = ~(1 << 4) & m_ReflectLayers.value; // never render water layerreflectionCamera.targetTexture = m_ReflectionTexture;//GL.SetRevertBackfacing(true);GL.invertCulling = true;reflectionCamera.transform.position = newpos;Vector3 euler = cam.transform.eulerAngles;reflectionCamera.transform.eulerAngles = new Vector3(0, euler.y, euler.z);reflectionCamera.depthTextureMode = DepthTextureMode.Depth;reflectionCamera.Render();

// copy depthGraphics.SetRenderTarget(m_ReflectionDepthTexture);m_matCopyDepth.SetPass(0);DrawFullscreenQuad();Graphics.SetRenderTarget(null);// Graphics.Blit(m_ReflectionTexture, m_ReflectionDepthTexture, m_matCopyDepth);

reflectionCamera.transform.position = oldpos;//GL.SetRevertBackfacing(false);GL.invertCulling = false;Material[] materials = GetComponent().sharedMaterials;foreach (Material mat in materials){mat.SetTexture("_ReflectionTex", m_ReflectionTexture);mat.SetTexture("_ReflectionDepthTex", m_ReflectionDepthTexture);}

// // Set matrix on the shader that transforms UVs from object space into screen// // space. We want to just project reflection texture on screen.// Matrix4x4 scaleOffset = Matrix4x4.TRS(// new Vector3(0.5f, 0.5f, 0.5f), Quaternion.identity, new Vector3(0.5f, 0.5f, 0.5f));// Vector3 scale = transform.lossyScale;// Matrix4x4 mtx = transform.localToWorldMatrix * Matrix4x4.Scale(new Vector3(1.0f / scale.x, 1.0f / scale.y, 1.0f / scale.z));// mtx = scaleOffset * cam.projectionMatrix * cam.worldToCameraMatrix * mtx;// foreach (Material mat in materials)// {// mat.SetMatrix("_ProjMatrix", mtx);// }

// Restore pixel light countif (m_DisablePixelLights)QualitySettings.pixelLightCount = oldPixelLightCount;

s_InsideRendering = false;}

// Cleanup all the objects we possibly have createdvoid OnDisable(){if (m_ReflectionTexture){DestroyImmediate(m_ReflectionTexture);m_ReflectionTexture = null;}if (m_ReflectionDepthTexture){DestroyImmediate(m_ReflectionDepthTexture);m_ReflectionDepthTexture = null;}foreach (DictionaryEntry kvp in m_ReflectionCameras)DestroyImmediate(((Camera)kvp.Value).gameObject);m_ReflectionCameras.Clear();}

private void UpdateCameraModes(Camera src, Camera dest){if (dest == null)return;// set camera to clear the same way as current cameradest.clearFlags = src.clearFlags;dest.backgroundColor = src.backgroundColor;if (src.clearFlags == CameraClearFlags.Skybox){Skybox sky = src.GetComponent(typeof(Skybox)) as Skybox;Skybox mysky = dest.GetComponent(typeof(Skybox)) as Skybox;if (!sky || !sky.material){mysky.enabled = false;}else{mysky.enabled = true;mysky.material = sky.material;}}// update other values to match current camera.// even if we are supplying custom camera&projection matrices,// some of values are used elsewhere (e.g. skybox uses far plane)dest.farClipPlane = src.farClipPlane;dest.nearClipPlane = src.nearClipPlane;dest.orthographic = src.orthographic;dest.fieldOfView = src.fieldOfView;dest.aspect = src.aspect;dest.orthographicSize = src.orthographicSize;}

// On-demand create any objects we needprivate void CreateMirrorObjects(Camera currentCamera, out Camera reflectionCamera){reflectionCamera = null;

// Reflection render textureif (!m_ReflectionTexture || m_OldReflectionTextureSize != m_TextureSize){if (m_ReflectionTexture)DestroyImmediate(m_ReflectionTexture);m_ReflectionTexture = new RenderTexture(m_TextureSize, m_TextureSize, 16);m_ReflectionTexture.name = "__MirrorReflection" + GetInstanceID();m_ReflectionTexture.isPowerOfTwo = true;m_ReflectionTexture.hideFlags = HideFlags.DontSave;m_ReflectionTexture.filterMode = FilterMode.Bilinear;

if (m_ReflectionDepthTexture)DestroyImmediate(m_ReflectionDepthTexture);m_ReflectionDepthTexture = new RenderTexture(m_TextureSize, m_TextureSize, 0, RenderTextureFormat.RHalf);// m_ReflectionDepthTexture = new RenderTexture(m_TextureSize, m_TextureSize, 0, RenderTextureFormat.R8);m_ReflectionDepthTexture.name = "__MirrorReflectionDepth" + GetInstanceID();m_ReflectionDepthTexture.isPowerOfTwo = true;m_ReflectionDepthTexture.hideFlags = HideFlags.DontSave;m_ReflectionDepthTexture.filterMode = FilterMode.Bilinear;

m_OldReflectionTextureSize = m_TextureSize;}

// Camera for reflectionreflectionCamera = m_ReflectionCameras[currentCamera] as Camera;if (!reflectionCamera) // catch both not-in-dictionary and in-dictionary-but-deleted-GO{GameObject go = new GameObject("Mirror Refl Camera id" + GetInstanceID() + " for " + currentCamera.GetInstanceID(), typeof(Camera), typeof(Skybox));reflectionCamera = go.GetComponent();reflectionCamera.enabled = false;reflectionCamera.transform.position = transform.position;reflectionCamera.transform.rotation = transform.rotation;reflectionCamera.gameObject.AddComponent();go.hideFlags = HideFlags.HideAndDontSave;m_ReflectionCameras[currentCamera] = reflectionCamera;}}

// Extended sign: returns -1, 0 or 1 based on sign of aprivate static float sgn(float a){if (a > 0.0f) return 1.0f;if (a < 0.0f) return -1.0f;return 0.0f;}

// Given position/normal of the plane, calculates plane in camera space.private Vector4 CameraSpacePlane(Camera cam, Vector3 pos, Vector3 normal, float sideSign){Vector3 offsetPos = pos + normal * m_ClipPlaneOffset;Matrix4x4 m = cam.worldToCameraMatrix;Vector3 cpos = m.MultiplyPoint(offsetPos);Vector3 cnormal = m.MultiplyVector(normal).normalized * sideSign;return new Vector4(cnormal.x, cnormal.y, cnormal.z, -Vector3.Dot(cpos, cnormal));}

// Adjusts the given projection matrix so that near plane is the given clipPlane// clipPlane is given in camera space. See article in Game Programming Gems 5 and// http://aras-p.info/texts/obliqueortho.htmlprivate static void CalculateObliqueMatrix(ref Matrix4x4 projection, Vector4 clipPlane){Vector4 q = projection.inverse * new Vector4(sgn(clipPlane.x),sgn(clipPlane.y),1.0f,1.0f);Vector4 c = clipPlane * (2.0F / (Vector4.Dot(clipPlane, q)));// third row = clip plane - fourth rowprojection[2] = c.x - projection[3];projection[6] = c.y - projection[7];projection[10] = c.z - projection[11];projection[14] = c.w - projection[15];}

// Calculates reflection matrix around the given planeprivate static void CalculateReflectionMatrix(ref Matrix4x4 reflectionMat, Vector4 plane){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;}

static public void DrawFullscreenQuad(float z = 1.0f){GL.Begin(GL.QUADS);GL.Vertex3(-1.0f, -1.0f, z);GL.Vertex3(1.0f, -1.0f, z);GL.Vertex3(1.0f, 1.0f, z);GL.Vertex3(-1.0f, 1.0f, z);

GL.Vertex3(-1.0f, 1.0f, z);GL.Vertex3(1.0f, 1.0f, z);GL.Vertex3(1.0f, -1.0f, z);GL.Vertex3(-1.0f, -1.0f, z);GL.End();}}

https://github.com/unity3d-jp/unitychan-crs/blob/master/Assets/UnityChanStage/Visualizer/MirrorReflection.cs

2. 該腳本主要功能

在場景中獲取當前生效的相機,根據反射平面進行鏡像,並對設定的渲染層進行逐幀拍照,並將其傳給反射平面進行顯示。配合自帶的半透明反射材質效果,疊加到底層材質上使用。

3. 主要問題

優化之前,該反射效果大概耗時10ms以上。主要問題如下:

1. 反射效果均爲實時反射,使用Layer控制。要看到反射效果,只能將該層物體納入反射層。

2. 反射表面爲獨立於真實反射表面的半透明材質,既耗性能效果也無法保證。

3. 項目角色的面數極高(單角色萬面左右),極限同屏9個角色外加寵物。除了本身的角色渲染外,同時還存在描邊、壓片陰影等效果。如果反射直接採用相機拍照的方式生成,額外的性能消耗極大。

4. 項目角色本身渲染支持5盞像素光,帶法線貼圖,卡通光照圖等各種複雜渲染效果,如果反射直接採用相機拍照的方式生成,額外的性能消耗極大。

5. 該腳本本身也存在一些問題:

1)會根據場景中正在生效的相機,逐個生成反射相機。

2)逐幀獲取反射材質,進行參數傳遞。

3)反射層的設置需要美術每次使用時都設置一次,容易設置過多或漏設置。

針對以上問題,實時反射主要有兩大優化方向,美術效果優化及性能優化。

一、美術效果優化

1. 將反射效果與反射表面物理屬性關聯。

引入反射表面法線貼圖的支持,對反射效果增加真實的法線扭曲效果。

方法:棄置默認的半透明反射表面。將反射圖直接指定給反射表面的渲染材質,增加法線效果,使用法線貼圖的xy通道對反射圖的UV進行偏移(這裡還增加了法線扭曲強度及遮罩等的調節)。

2. 引入反射表面粗糙度設置。

可以調節反射效果的清晰度(模糊策略,後面性能優化詳述)。

方法:不考慮性能,這裡可以向腳本中傳參,使用高斯模糊來進行粗糙表面的反射模糊效果。

3. 引入反射深度,實現反射跟隨距離從有到無的柔和過渡。

方法:在渲染反射圖時,傳入衰減係數,根據頂點到地面高度的距離進行衰減,衰減強度寫入反射圖的A通道。

二、性能優化

概述:

性能優化方面主要集中在如下幾個方面:

1. 減少數據量,降低CPU負擔。

2. 降低DrawCall,增加C/GPU傳輸率。

3. 降低渲染複雜度,加速GPU渲染。

4. 優化腳本,減少GC及重複操作。

基於以上考慮,性能優化方向歸爲兩類:美術端優化和程序端優化。

美術端優化

1. 動靜分離

將反射效果拆分爲靜態物體的與烘焙反射和動態物體的實時反射兩部分組成。這樣可以大大降低實時運算量。

2. 預烘焙部分

根據場景需要的反射清晰度,預先烘焙尺寸儘可能小的反射圖。對於非常粗糙且並不清晰的反射場景,可以共用一張場景反射圖。

或者晝夜、雨晴等同場景不同狀態下的反射圖共用。通過調節材質參數(例如反射顏色、粗糙度及反射強度)來實現不同狀態效果。

通過BoxProjection方法,修正與烘焙CubeMap的場景對齊問題。

3. 實時部分

同靜態與烘焙相似,可以根據場景的反射清晰度,實時生成儘可能小的反射圖來使用。

(1)程序開放尺寸調節:

(2)代碼寫入支持尺寸配置:

降低實時反射的DrawCall,例如:儘可能地合併角色部件,忽略較小部件不進行反射等。

降低實時反射的數據量和計算量,例如:使用低面數的Lod來渲染反射,降低反射效果要求減少燈光數量,效果數量等。

4. 反射表面優化策略

同一個場景,只能存在一個反射表面。

將反射表面的材質作爲高配材質由美術特別對待。

將盡可能多的參數暴露給美術,儘可能地適配高性能參數。

程序端優化

同樣從概述中的幾個方面進行考慮來進行優化。

1. 去除不必要的渲染批次及效果

反射效果中因爲清晰度有限,描邊效果可以剔除,陰影效果也可以不進行渲染。

2. 降低反射渲染的計算複雜度

反射效果中,只需要表現出基本的明暗關係即可,故只保留1盞平行光效果。光照計算也從像素光照更改爲頂點光照。

一個角色渲染需要基本色紋理,法線紋理,粗糙金屬性紋理,身體材質區分紋理,光照紋理和buff效果紋理。在反射中,精簡效果到只需要基本色紋理和buff效果紋理即可。

要實現以上優化方案,就需要在進行反射渲染時,將動態物體的材質進行替換渲染。可用策略如下:

(1)更換材質

需要每幀用反射材質進行替換渲染。如果是後處理描邊、風格化單色等效果,這類方法比較適合。但角色材質多樣,參數各不相同,用這種方法需要自己維護動態物件的反射列表,比較繁瑣。

(2)Lod Int控制

就是使用SubShader的Lod參數進行控制。這樣,把各個材質的反射Shader集中到一個Lod下進行計算即可。最簡單的一種方式,但後來經過測試,實時切換Lod的消耗過大,不太實際。

(3)宏開關

比較簡單,集成性、通用性好,且切換消耗小。最初我們使用的優化方案。但因爲存在角色的多材質及多Pass渲染,後期對優化效率仍然不足夠(無法優化掉描邊和陰影)。

核心代碼:

(4)使用CommandBuff

可控性和自由度最高。但問題同(1),需要自己維護角色列表,對於頻繁發生角色更換的場合,維護成本會較高,但仍可實現。可以由邏輯層在發生角色更換時,更新角色列表到反射程序中進行處理即可。

部分核心代碼:

(5)使用CameraReplaceShader

我們項目最終優化採用的方法。優點是可以實現替換渲染,且不需要自己維護角色列表。缺點就是重寫反射渲染的Shader。

核心腳本代碼:

這裡簡單解釋一下SetReplacementShader命令。

他是將該相機中進行渲染的所有Shader,根據後面設定的replacementTag進行替換。

例如上面代碼中,我們使用了“MirrorReflect”的Tag,所以,在Shader中,凡是含有“MirrorReflect”Tag的SubShader都會被替換爲reflectShader中,相同“MirrorReflect”Tag的SubShader。

如上舉例,即可在相機進行反射渲染時,將含有相同“MirrorReflect”Tag的SubShader 進行對應替換渲染了。

程序策略性優化

1. 合併靜態與實時反射

只需要根據反射圖的A通道中,存儲的反射衰減值進行動靜反射混合即可。

2. 開放美術調節參數

如前所述的一些性能相關設置,可以開放給美術進行擇優。

3. 反射清晰度

引入粗糙度的概念,使用模糊來遮蓋反射圖分辨率不足的問題。可以進一步降低反射分辨率。

這裡的模糊策略採用了MipMap的方式來避免高斯模糊帶來的消耗。

程序邏輯層性能優化

1. 減反射相機數量

將反射相機個數從逐相機生成的多反射相機優化爲單反射相機,逐幀對位的方式。

2. 減GC

將申請RT,生成相機,獲取反射材質,傳遞材質參數等可一次性進行的操作,移出OnWillRenderObject()。

3. 優化刷新率

增加刷新率設置,將逐幀渲染改爲可配置刷新率渲染。目前默認爲1/2刷新率。

4. 反射層程序端寫死

避免美術出錯及加速配置。

5. 開關策略

當反射平面不可見時,關閉反射功能,將所有資源進行釋放(邏輯層控制)。

6. 平臺匹配

適配不同性能的平臺。目前我們僅高配平臺開啓實時反射效果。中配置平臺僅開啓靜態反射效果。

最後:優化總結

文末,再次感謝羅漢銘的分享,作者主頁:https://www.zhihu.com/people/lucifer-3-91,如果您有任何獨到的見解或者發現也歡迎聯繫我們,一起探討。(QQ羣:793972859)

作者在 UWA DAY 2021 大會中的《航母戰鬥羣中的巡洋艦 - 爲項目保駕護航的技術美術們》議題演講已經上線UWA學堂,課程限時優惠中,掃碼即可前往觀看。