バックグラウンド
最近、VR 会議を続けていると、デスクトップの共有で難しい問題に遭遇しました。会議では、フレームごとに画像を送信することで共有デスクトップを実現します。Unity では、画像をレンダリングする必要があります。
3つの困難
Unity でリアルタイムに画像をレンダリングするには、次の 3 つの問題があります。
- テクスチャへのデータの埋め込みは非常に時間がかかる作業で、メイン スレッドで行うとフレーム レートに影響します。
- サーバーから送信される画像形式のデータはRABG形式という変則的な形式で、テクスチャに直接値を代入する方法はなく、RGBAやARGB形式に変換する変換が必要です。
- texture.Apply 関数は CPU から GPU にデータを転送します. この関数はメイン スレッドで実行する必要があります. もう一つの時間のかかる操作。
2 つの問題を解決するために、次の最適化が実行されます。
1.データをテクスチャに埋め込むスキーム
1. アンセーフ コードとメモリ コピー メソッドを使用します。このコピー方法は、画像の解像度が 1920x1280 の場合でも 4 ~ 5 ミリ秒かかります。
2. テクスチャへのデータの埋め込みは、メイン スレッドで実行する必要はありません。
private Texture2D imageTexture;
private NativeArray<byte> textureNaitve;
//先创建一个纹理,然后获取得到纹理的数据地址textureNaitve
void CreateTextureIfNeeded(int width, int height)
{
if (imageTexture != null && (imageTexture.width != width || imageTexture.height != height))
{
DestroyImmediate(imageTexture);
imageTexture = null;
}
if (imageTexture == null)
{
imageTexture = new Texture2D(width, height, TextureFormat.RGBA32, false, true);
imageTexture.wrapMode = TextureWrapMode.Clamp;
imageRenderer.material.SetTexture(mainTexPropertyName, imageTexture);
textureNaitve = imageTexture.GetRawTextureData<byte>();
}
}
public async Task LoadIntoTextureAsync(Texture2D texture, SByte[] data, long len)
{
//拷贝数据
Debug.Log("LoadIntoTextureAsyncTest 1");
int ret = await Utils.AwaitMethod<int>(false, () =>
{
unsafe
{
Debug.Log("Marshal.Copy 1");
Marshal.Copy((byte[])(Array)data, 0, (IntPtr)NativeArrayUnsafeUtility.GetUnsafeBufferPointerWithoutChecks(textureNaitve), (int)len);
Debug.Log("Marshal.Copy 2");
}
return 0;
});
}
public static Task<ReturnType> AwaitMethod<ReturnType>(bool isAttachCurrentThread, Func<ReturnType> cb)
{
var task = Task.Run(() =>
{
if (isAttachCurrentThread)
AndroidJNI.AttachCurrentThread();
try
{
return cb.Invoke();
}
finally
{
if (isAttachCurrentThread)
AndroidJNI.DetachCurrentThread();
}
});
return task;
}
注: テスト中、データをテクスチャ サーフェスにコピーするのに約 4 ~ 5 ミリ秒かかることがわかりました.そのとき、Task.Run が完了するのを待つのに約 20 ミリ秒かかりました. 自分の使用シナリオを見て、フレーム レートに影響を与えずにメイン スレッドで 4 ~ 5 ミリ秒を費やす場合は、それをメイン スレッドに入れて実行できます。
2. 画像フォーマット変換
今回のシナリオでは、サーバーから送信される画像のバイトデータはRABG形式ですが、UnityのTexture2Dにはこの形式が存在せず、形式変換が必要です。現在の写真はフレームごとに変換する必要があるため、多くの時間がかかります。
そのとき、次の 3 つの選択肢が思い浮かびました。
解決策 1: すべての操作は CPU で実行されます。Unityが提供するIJobParallelForを使用して、並列ジョブは NativeArray を使用してデータをそのデータ ソースとして格納します。複数のコア間でジョブの実行を並列化します。コアごとに 1 つのジョブがあり、各ジョブがワークロードの一部を処理します。IJobParallelForの動作はIJobと非常に似ていますが、1 つのExecuteメソッドのみを実行する代わりに、データ ソース内の各アイテムに対してExecuteメソッドを実行します。Executeメソッドには整数パラメーターがあります。このインデックスは、ジョブの特定の操作の実装で、データ ソースの個々の要素にアクセスして操作するためのものです。
[BurstCompile(CompileSynchronously = true)]
struct BGRToRGBJob : IJobParallelFor
{
public delegate void BGRToRGBDelegate(ref NativeSlice<byte> textureData, int index);
public static readonly FunctionPointer<BGRToRGBDelegate> RABG32ToRGBA32FP = BurstCompiler.CompileFunctionPointer<BGRToRGBDelegate>(RABG32ToRGBA32);
[BurstCompile(CompileSynchronously = true)]
static void RABG32ToRGBA32(ref NativeSlice<byte> textureData, int index)
{
var temp = textureData[mad(4, index, 1)];
textureData[mad(4, index, 1)] = textureData[mad(4, index, 3)];
textureData[mad(4, index, 3)] = temp;
}
[NativeDisableParallelForRestriction]
public NativeSlice<byte> textureData;
public FunctionPointer<BGRToRGBDelegate> processFunction;
public void Execute(int index) => processFunction.Invoke(ref textureData, index);
}
2 つのパッケージ、com.unity.burst、Mathematics をインストールする必要があります。呼び出して実行する方法は?
using static Unity.Mathematics.math;
using Unity.Burst;
var mipmapSlice = new NativeSlice<byte>(textureNaitve, 0, texture.width * texture.height * 4);
JobHandle jobHandle = new BGRToRGBJob
{
textureData = mipmapSlice,
processFunction = BGRToRGBJob.RABG32ToRGBA32FP
}.Schedule(texture.width * texture.height, 8192);
while (!jobHandle.IsCompleted) await Task.Yield();
jobHandle.Complete();
このようにして、同時実行のために複数のワーカーに配置できます。ただし、私たちのテストでは、1920x1280x4 バイトのデータの実行効率は高くなく、ここでも 20ms 近く費やしていることがわかりました。
解決策 2: レンダリング時に直接変換し、レンダリング シェーダーを自分で記述します。
Shader "Custom/ShareScreen"
{
Properties
{
_Color ("Color", Color) = (1,1,1,1)
_MainTex ("Albedo (RGB)", 2D) = "white" {}
_Glossiness ("Smoothness", Range(0,1)) = 0.5
_Metallic ("Metallic", Range(0,1)) = 0.0
}
SubShader
{
Tags { "RenderType"="Opaque" }
LOD 200
CGPROGRAM
// Physically based Standard lighting model, and enable shadows on all light types
#pragma surface surf Standard fullforwardshadows
// Use shader model 3.0 target, to get nicer looking lighting
#pragma target 3.0
sampler2D _MainTex;
struct Input
{
float2 uv_MainTex;
};
half _Glossiness;
half _Metallic;
fixed4 _Color;
// Add instancing support for this shader. You need to check 'Enable Instancing' on materials that use the shader.
// See https://docs.unity3d.com/Manual/GPUInstancing.html for more information about instancing.
// #pragma instancing_options assumeuniformscaling
UNITY_INSTANCING_BUFFER_START(Props)
// put more per-instance properties here
UNITY_INSTANCING_BUFFER_END(Props)
void surf (Input IN, inout SurfaceOutputStandard o)
{
// Albedo comes from a texture tinted by color
fixed4 c = tex2D (_MainTex, IN.uv_MainTex) * _Color;
float temp = c.g;
c.g = c.a;
c.a = temp;
o.Albedo = c.rgb;
// Metallic and smoothness come from slider variables
o.Metallic = _Metallic;
o.Smoothness = _Glossiness;
o.Alpha = c.a;
}
ENDCG
}
FallBack "Diffuse"
}
このように、データ変換には基本的に時間がかかりません。
要約する
関数 texture.Apply を最適化する良い方法はありません。これはメイン スレッドでのみ実行でき、1 フレームあたり 3 ~ 5 ミリ秒かかります。リアルタイムで画像を更新するためのより良い方法があるかどうかを確認してください。誰もがより多くのコメントをすることができます。
参照: