はじめに
Unityでランタイム中にCubemapを更新する方法はいくつかあります。
有名なのはCamera.RenderToCubemapですが、こちらはURPに対応しておらず、呼び出すとエラーを出します。
Recursive rendering is not supported in SRP (are you calling Camera.Render from within a render pipeline?).
UnityEngine.Camera:RenderToCubemap (UnityEngine.RenderTexture,int)
URPでのキューブマップレンダリングはサンプルもほとんど無くて、結構ハマったのでまとめておきます。
結論
RenderTextureDescriptorを使用することで、Cubemap形式のRenderTextureを作成することができます。
var cubemapRTDesc = new RenderTextureDescriptor(texSize, texSize, RenderTextureFormat.ARGB32) {
dimension = UnityEngine.Rendering.TextureDimension.Cube
};
var cubemapRT = new RenderTexture(cubemapRTDesc);
ただしこのRTにはURPでは直接レンダリングすることができません。
なので、一旦Texture2DタイプのRenderTextureにレンダリングして、それをBlitします。
// 描画先のRTを確保
var desc = new RenderTextureDescriptor(texSize, texSize, RenderTextureFormat.ARGB32, 16);
var rt = RenderTexture.GetTemporary(desc);
// RTにレンダリング
_camera.targetTexture = rt;
// ~~ ここらへんでカメラを各面の方向に向ける処理を行う ~~
UnityEngine.Rendering.Universal.UniversalRenderPipeline.RenderSingleCamera(context, _camera);
_camera.targetTexture = null;
// キューブマップへBlit
var faceIndex = CubemapFace.PositiveX;
Graphics.SetRenderTarget( cubemapRT, 0, faceIndex );
Graphics.Blit( rt, s_blitMtl );
RenderTexture.active = null;
RenderTexture.ReleaseTemporary( rt );
以上がURP上で最も簡単で軽快にCubemapをランタイム更新する方法です。
(他にもっと良い方法があればコメントいただけると助かります!)
以下、ここに至るまでに色々試した他の方法。
#1. Cubemap.SetPixelsを使用する方法
Cubemap.SetPixels/Cubemap.SetPixelDataを使用して、ピクセルデータの配列からキューブマップを生成する方法。
###手順
- キューブマップの各方向の面を向いたカメラからRTにレンダリングを行う
- レンダリング結果のRTからReadPixelsでTexture2Dを作成
- Texture2DからGetPixelsでピクセルデータの配列を取得
- Cubemap.SetPixelsでピクセルデータの配列からキューブマップを生成
明らかに重そうですが、SetPixels/SetPixelDataはCubemapクラスに定義されているほぼ唯一の更新関数なので、試してみます。
2~4の処理は以下のようなコードになります。
// まず各面のレンダリング結果をTexture2Dへ転写
var lastRTTgt = RenderTexture.active;
RenderTexture.active = rt;
tmpTex2D.ReadPixels(new Rect(0, 0, texSize, texSize), 0, 0);
tmpTex2D.Apply(false);
RenderTexture.active = lastRTTgt;
// Texture2Dからピクセル情報を読み込む
var srcPixels = tmpTex2D.GetPixels();
// キューブマップへ適応
var faceIndex = CubemapFace.PositiveX;
cubemap.SetPixels( srcPixels, faceIndex, 0 );
###結果
予想通りと言いますか、Texture2D.ReadPixelsがレンダリングスレッドとの同期待ちを起こすため、この部分だけで17msかかっています。
ReadPixelsが重いので、SetPixelsをSetPixelDataにしても大した変化はありませんでした。
これではさすがに実行中気軽には呼べませんね…。
#2. NativePluginでBlitする方法
Texture2D.ReadPixelsが重いのは、GPU側に持っているテクスチャデータをメインメモリ側に移そうとするからです。RenderTextureからキューブマップへのBlit処理を、GPU側で完結させれば早いはず。
ところがGraphics.Blit関数はCubemapを引数に設定できないため、ここの処理をNativePlugin側で実装します。
###手順
- キューブマップの各方向の面を向いたカメラからRTにレンダリングを行う
- レンダリング結果のRTとCubemapのテクスチャポインタをNativePluginへ渡す
- NativePlugin側でRTの内容をキューブマップへBlitする
2ではTexture.GetNativeTexturePtrを使用します。
blitTex2Cubemap(
rt[0].GetNativeTexturePtr(),
rt[1].GetNativeTexturePtr(),
rt[2].GetNativeTexturePtr(),
rt[3].GetNativeTexturePtr(),
rt[4].GetNativeTexturePtr(),
rt[5].GetNativeTexturePtr(),
cubemap.GetNativeTexturePtr(),
texSize
);
3のNativePluginはUnity公式のNativeRenderingPluginサンプルを参考に実装します。
例として、Dx11版のプラグイン側コードは以下のような感じになります
void blitCubemap(
void* srcTex0,
void* srcTex1,
void* srcTex2,
void* srcTex3,
void* srcTex4,
void* srcTex5,
void* cubemapTex,
int texWidth
) {
auto device = _d3d11->GetDevice();
ID3D11DeviceContext* ctx = nullptr;
device->GetImmediateContext(&ctx);
// 処理対象のテクスチャ
ID3D11Texture2D* srcTexs[] = {
static_cast<ID3D11Texture2D*>( srcTex0 ),
static_cast<ID3D11Texture2D*>( srcTex1 ),
static_cast<ID3D11Texture2D*>( srcTex2 ),
static_cast<ID3D11Texture2D*>( srcTex3 ),
static_cast<ID3D11Texture2D*>( srcTex4 ),
static_cast<ID3D11Texture2D*>( srcTex5 ),
};
auto dstTex = static_cast<ID3D11Texture2D*>( cubemapTex );
// コピー処理を行う
for (int i=0; i<6; ++i) {
ctx->CopySubresourceRegion(dstTex, i, 0, 0, 0, srcTexs[i], 0, nullptr);
}
ctx->Release();
}
###結果
プラグイン内の処理は非常に軽快なのですが、プラグインへ渡す際に呼び出すTexture.GetNativeTexturePtrがレンダリングスレッドとの同期待ちを引き起こすため、ここだけで10msかかっています。
CubemapをプールするなりしてGetNativeTexturePtrの結果をキャッシュするようにすれば緩和されますが、取り回し方に制限が出てくるのであまりベストな方法ではなさそうですね。
GetNativeTexturePtrはプラグイン側でテクスチャを処理するために必須なので、今回のCubemapにかかわらず、NativePluginでテクスチャを扱う以上この負荷は避けられない事になります。
NativePluginは書くのが大変なので、その手間がかかる分「とにかく早い!」みたいな記事が多い印象を受けますが、速度にこういう明確なデメリットがあるのは意外でしたね・・・。
#3. Unity側でCubemapに直接Blitする方法
こちらが初めに結論で書いた方法になります。
これが出来ないから苦労していたのですが・・・できました。
Cubemapクラスは使わずに、RenderTextureをキューブマップ形式で作るのがキモです。
###手順
- キューブマップの各方向の面を向いたカメラからRTにレンダリングを行う
- Cubemapクラスの代わりに、RenderTextureをキューブマップ形式で作成
- 1のRTを2のCubemapRTにBlitする
RenderTextureDescriptorのdimensionプロパティにTextureDimension.Cubeを指定することで、キューブマップ形式のRTを作成できます。
var cubemapRTDesc = new RenderTextureDescriptor(texSize, texSize, RenderTextureFormat.ARGB32) {
dimension = UnityEngine.Rendering.TextureDimension.Cube
};
var cubemapRT = new RenderTexture(cubemapRTDesc);
Graphics.BlitにはCubemap形式のRenderTextureを指定することができませんので、少しトリッキーな方法ですがSetRenderTargetを使用することでCubemap形式のRTを出力先に指定します。
var faceIndex = CubemapFace.PositiveX;
Graphics.SetRenderTarget( cubemapRT, 0, faceIndex );
Graphics.Blit( rt, s_blitMtl );
RenderTexture.active = null;
s_blitMtlには、ただ何もしないでTextureを表示するUnlitなシェーダを指定しておけばOKです。
(本当はここの引数がないものがあればよいのですが、Graphics.Blitに出力元だけのオーバーロードがないので)
###結果
非常に軽快!!
※ 本当はSetRenderTargetで指定した描画先に直接カメラからレンダリングが出来ればなお良かったのですが、
- RenderPipelineManager.beginCameraRenderingでセットする
- UniversalRenderPipeline.RenderSingleCameraの直前でセットする
- SetRenderTargetした後のGraphics.activeColorBufferの値をCamera.SetTargetBuffersに設定する
等を試しましたがダメでした。
ので、一回無駄なBlitが発生してしまいますが、それでも十分な速度を得られています。
#その他の方法
ReflectionProbeを使用する方法
ReflectionProbeをリアルタイム更新モードで使用して、レンダリングされたCubemapを使用する方法。
1面ずつレンダリングしたり出来なそうなので試していません。
Equirectangular形式を使用する方法
Cubemapではなく、2DTextureにEquirectangular形式で展開する方法。
確実に実行できますが、ハードウェアのサポートが得られるCubemapと違って手動で座標変換を行わなければならない点と、テクスチャ解像度が上がってしまう点から、使用時のテクスチャフェッチ負荷がCubemapより高いので試していません。
参考:(https://forum.unity.com/threads/camera-rendertocubemap-including-orientation.534469/)
ComputeShaderを使用する方法
ComputeShaderを使用して、2DTextureからCubemapへBlitする方法。
ComputeShaderが使用できない環境(WebGL)はシングルスレッドなので、GetNativeTexturePtrの待ち時間はほぼないはず、という仮説が正しければこの方法も有効かもしれませんが、早くとも最速にはならないはずなので試していません。
参考:(https://answers.unity.com/questions/1388753/how-to-write-to-a-rendertexture-as-a-cubemap.html)
RenderPipelineを拡張して対応
SRPはRenderPipelineを拡張できるので、Graphics.SetRenderTargetでCubemapを指定して直接レンダリングするようにRenderPipelineを書けば、より高速にレンダリングできるはず。
一回2DTexture形式のRTを経由する方法でとりあえずは満足しているので、試していません。
#おわり
このあたりはTexture2D.ReadPixelsを使用する手法しか情報がなくて大変でした。
結論の手法以外の実装も含めて、一応ソースコードをGithubで公開しています。→CubemapOnTheFly
(サンプル用に書いたコードじゃないので読みにくいですが・・・)