WinCNT

AppSW(ASW)와 Motion Vector로 인해, Windows용 빌드에서 렌더링이 이상해지는 이슈(VR기기에서는 정상) 본문

Unity/Unity 개발 중 발생한 이슈 정리

AppSW(ASW)와 Motion Vector로 인해, Windows용 빌드에서 렌더링이 이상해지는 이슈(VR기기에서는 정상)

WinCNT_SSS 2023. 12. 20. 17:41

발생한 이슈

CI 빌드 자동화 테스트를 겸해서 Windows용 빌드를 만들어 실행해봤는데, 렌더링이 이상해지는 이슈가 발생했다

좌) 이슈 발생! / 우) 정상일 경우

에디터나 VR기기(안드로이드)에서는 아무런 이상이 없었는데 Windows용 빌드에서만 이슈가 발생하고 있었다


이슈 상세

여러 조사로 알게 된 것은 카메라의 Post Processing을 끄면 이슈가 발생하지 않는다는 것뿐이었다

이제 이걸 어떡한담...


발생 원인 조사 경위

원인 조사를 위해 수많은 삽질과 시간이 필요했다

씬 복사하기, 프로젝트 버전 올리기, 동일 버전의 새로운 프로젝트 생성해서 같은 환경 구축하기, WinMerge해서 차이나는 파일을 되돌려고 빌드해서 확인하기

 

최종적으로는 WinMerge로 문제 부분을 특정할 수 있어서 조사해보니 발생 원인은 꽤나 복잡했다

물론 조사를 위해서 또 삽질함

그래도 차례대로 정리해보고자 한다

AppSW을 위한 모션 벡터 패스 도입

우선 해당 이슈가 발생하게 된 것은 오큘러스의 AppSW란 기능을 도입하기 위해 추가한 모션 벡터의 Render Pass라고 보면 될 것 같다

AppSW는 매우 간단히 말하자면 모션 벡터를 바탕으로 VR기기에서의 프레임을 보정해주는 오큘러스의 기능이라고 할 수 있다

공식 문서를 살펴보면 URP에서 AppSW를 적용하기 위해 추가해야할 소스 코드 샘플들을 제공하고 있으며, URP fork도 있기에 일반적으로는 그대로 사용해도 되는 듯 하다

Application SpaceWarp Developer Guide: Unity | Oculus Developers

 

Application SpaceWarp Developer Guide: Unity | Oculus Developers

 

developer.oculus.com

[SpaceWarp] URP V1 Commit · Oculus-VR/Unity-Graphics@56cb0ec

 

[SpaceWarp] URP V1 Commit · Oculus-VR/Unity-Graphics@56cb0ec

First commit which adds support for Application SpaceWarp motion vector generation to the URP. Consists of using the motion vector texture handle from the XR pass to create a render pass, then to e...

github.com

 

 

이 이슈가 발생한 프로젝트에서도 URP fork를 도입했으며, 이번에 문제가 된 부분은 아래와 같다

// UniversalRenderer.cs에서 발췌

public override void Setup(ScriptableRenderContext context, ref RenderingData renderingData)
{

    // ...생략

#if !UNITY_EDITOR
            if (cameraData.xr.motionVectorRenderTargetValid)
            {
                RenderTargetHandle motionVecHandle = new RenderTargetHandle(cameraData.xr.motionVectorRenderTarget);
                var rtMotionId = motionVecHandle.Identifier();
                rtMotionId = new RenderTargetIdentifier(rtMotionId, 0, CubemapFace.Unknown, -1);

                // ID is the same since a RenderTexture encapsulates all the attachments, including both color+depth.
                m_OculusMotionVecPass.Setup(rtMotionId, rtMotionId);
                EnqueuePass(m_OculusMotionVecPass);
            }
#endif

    // ...생략

}

시험 삼아 #if !UNITY_EDITOR 부분을 없애보니, 역시나 유니티 에디터에서도 드디어 같은 이슈를 재현할 수 있었기에, 이후 조사는 Frame Debug나 브레이크 포인트를 사용해서 진행했다

모션 벡터 패스와 Color Grading

해당 소스 코드 자체는 문제가 없지만, 이 추가 부분이 앞 부분의 Color Grading에 영향을 미쳐서 대환장 콜라보가 발생한 것으로 추측된다

// UniversalRenderer.cs에서 발췌

if (generateColorGradingLUT)
{
    colorGradingLutPass.Setup(colorGradingLut);
    EnqueuePass(colorGradingLutPass);
}

#if ENABLE_VR && ENABLE_XR_MODULE
if (cameraData.xr.hasValidOcclusionMesh)
    EnqueuePass(m_XROcclusionMeshPass);
#endif

#if !UNITY_EDITOR
if (cameraData.xr.motionVectorRenderTargetValid)
{
    RenderTargetHandle motionVecHandle = new RenderTargetHandle(cameraData.xr.motionVectorRenderTarget);
    var rtMotionId = motionVecHandle.Identifier();
    rtMotionId = new RenderTargetIdentifier(rtMotionId, 0, CubemapFace.Unknown, -1);

    // ID is the same since a RenderTexture encapsulates all the attachments, including both color+depth.
    m_OculusMotionVecPass.Setup(rtMotionId, rtMotionId);
    EnqueuePass(m_OculusMotionVecPass);
}
#endif

 

#if !UNITY_EDITOR를 코멘트하고, 프레임 디버그를 통해 정상적일 때와 문제가 생겼을 때의 Color Grading의 텍스처를 확인해보니 다음과 같았다

 

정상적인 경우(왼쪽)하고 이슈가 발생했을 경우(오른쪽)를 비교해보니, 이슈가 발생했을 경우의 렌더 타겟에는 카메라에 비치고 있는 구체의 무엇인가가 출력되고 있었다

이 무엇인가는 모션 벡터로 추정되었는데, 카메라에 비치는 오브젝트가 바뀜에 따라 Color Grading의 텍스처도 바뀌었기 때문이다

 

추가로 프레임 디버그에서 렌더 타겟을 확인해보니 Color Grading의 렌더 패스와 Motion Vector의 렌더 패스가 _InternalGradingLut로 같다는 걸 알 수 있었다

(참고로 _InternalGradingLut는 Color Grading용 렌더 타겟)

 

반대로 VR기기 빌드에서는 XR Texture [8]라는 렌더 타겟이 설정된 것을 확인할 수 있었다

 

SetRenderTarget를 보자!!!

여기까지 오니 Post Processing의 체크 자체가 문제는 아니었다는 것은 알게 되었다

Post Processing의 체크를 끄면 이슈가 Color Grading을 스킵하기 때문에 이슈가 안 보였을 뿐이었다

 

이제는 왜 VR기기에서는 렌더 타겟이 제대로 변경되는데 Windows에서는 이슈가 발생하지 조사해야할 필요가 생겼다

한동안 또 삽질을 하다가 #if !UNITY_EDITOR를 주석 처리 했을 때, 콘솔창에 다음과 같은 경고가 발생하는 것을 발견했다

CommandBuffer: temporary render texture not found while executing (SetRenderTarget color buffer)
UnityEngine.GUIUtility:ProcessEvent (int,intptr,bool&)
CommandBuffer: temporary render texture not found while executing (SetRenderTarget depth buffer)
UnityEngine.GUIUtility:ProcessEvent (int,intptr,bool&)

 

딱 보니 모션 벡터용 렌더 타겟(의 텍스처)를 발견하지 못 해서 그 전의 렌더 타겟(즉 _InternalGradingLut)이 그대로 사용된 것이란 느낌이 왔다

자체 엔진 만들 때 비슷한 삽질이 많았기 때문

 

그렇다고 브레이크 포인트를 걸고 확인하지는 못 했다…

왜냐하면 해당 경고는 ScriptableRenderContext.Submit()에서, 즉 Unity의 네이티브 코드(C++)에서 발생하고 있었기 때문이다

 

그래서 SetRenderTarget이 호출되는 부분을 확인해봤는데……

아쉽게도 아무런 문제가 없었다

 

참고로 렌더 패스는 10개가 확인되었다(물론 순서는 도중에 Sorting됨)

렌더 패스명 비고
MainLightShadowCasterPass  
AdditionalLightsShadowCasterPass  
ColorGradingLutPass  
OculusMotionVectorPass AppSW를 위한 모션 벡터 패스
DrawObjectsPass  
DrawObjectsPass  
InvokeOnRenderObjectCallbackPass  
PostProcessPass  
ScreenShotPass 커스텀 렌더 패스
FinalBlitPass  

XRSystem과 XRPass와 XRDisplaySubsystem.XRRenderPass

어쩔 수 없이 AppSW을 위해 추가된 다른 부분을 살펴보니 그럴듯한 부분을 발견했다

// XRPass.cs의 Create 메소드 내부에 AppSW 대응을 위해 추가된 부분

if (xrRenderPass.hasMotionVectorPass)
{
    passInfo.motionVectorRenderTarget = new RenderTargetIdentifier(xrRenderPass.motionVectorRenderTarget, 0, CubemapFace.Unknown, -1);

    RenderTextureDescriptor xrMotionVectorDesc = xrRenderPass.renderTargetDesc;
    RenderTextureDescriptor rtMotionVectorDesc = new RenderTextureDescriptor(xrMotionVectorDesc.width, xrMotionVectorDesc.height, xrMotionVectorDesc.colorFormat, xrMotionVectorDesc.depthBufferBits, xrMotionVectorDesc.mipCount);
    rtMotionVectorDesc.dimension = xrRenderPass.renderTargetDesc.dimension;
    rtMotionVectorDesc.volumeDepth = xrRenderPass.renderTargetDesc.volumeDepth;
    rtMotionVectorDesc.vrUsage = xrRenderPass.renderTargetDesc.vrUsage;
    rtMotionVectorDesc.sRGB = xrRenderPass.renderTargetDesc.sRGB;
    passInfo.motionVectorRenderTargetDesc = rtMotionVectorDesc;

    Debug.Assert(passInfo.motionVectorRenderTargetValid, "Invalid motion vector render target from XRDisplaySubsystem!");
}

 

주변 코드를 살펴보니 AppSW를 위한 모션 벡터용 렌더 타겟은 XRPass에 만들어주는 것으로 파악되었다(당연히 이해가 틀렸을 수도 있다…)

일단 Create 메소드가 호출되면 motionVectorRenderTarget = XRPass.invalidRT를 설정하고, 모션 백터가 있는 경우에는 위의 코드가 실행되서 렌더 타겟을 만드는 것으로 보인다

 

그래서 중단점으로 확인해 보니 위의 부분은 에디터와 Windows용 빌드에서는 호출되지 않았다

좀 더 조사해보니 에디터와 Windows용 빌드에서는 아래의 코드 bool xrEnabled = RefreshXrSdk()에서 false가 리턴되어서 XRPass.cs의 Create 메소드 자체가 호출되지 않는 상태였다

// XRSystem.cs

internal List<XRPass> SetupFrame(Camera camera, bool enableXRRendering)
{
    bool xrEnabled = RefreshXrSdk();

    if (display != null)
    {
        // XRTODO: Handle stereo mode selection in URP pipeline asset UI
        display.textureLayout = XRDisplaySubsystem.TextureLayout.Texture2DArray;
        display.zNear = camera.nearClipPlane;
        display.zFar  = camera.farClipPlane;
        display.sRGB  = QualitySettings.activeColorSpace == ColorSpace.Linear;
    }

    if (framePasses.Count > 0)
    {
        Debug.LogWarning("XRSystem.ReleaseFrame() was not called!");
        ReleaseFrame();
    }

    if (camera == null)
        return framePasses;

    // Enable XR layout only for game camera
    bool isGameCamera = (camera.cameraType == CameraType.Game || camera.cameraType == CameraType.VR);
    bool xrSupported = isGameCamera && camera.targetTexture == null && enableXRRendering;

    if (xrEnabled && xrSupported)
    {
        // Disable vsync on the main display when rendering to a XR device.
        QualitySettings.vSyncCount = 0;
        // On Android and iOS, vSyncCount is ignored and all frame rate control is done using Application.targetFrameRate.
        float frameRate = 300.0f;
        Application.targetFrameRate = Mathf.CeilToInt(frameRate);

        // 내부에서 XRPass.cs의 Create가 호출됨
        CreateLayoutFromXrSdk(camera, singlePassAllowed: true);

        OverrideForAutomatedTests(camera);
    }
    else
    {
        AddPassToFrame(emptyPass);
    }

    return framePasses;
}

 

그 대신 AddPassToFrame(emptyPass)에서 보이듯이 emptyPass라는 게 패스로 추가된다

여기서는 약간 추측이지만, emptyPass의 모션 벡터용 렌더 타겟은 선언만 되어 있을 뿐, 초기화나 리소스 획득 등이 이루어진 것은 아니므로(적어도 필자는 확인하지 못 했다…), 실제로 렌더링 처리 시에 모션 벡터용 렌더 타겟을 찾지 못 해 이번의 이슈가 발생하는 것이 아닐까 싶다


발생 원인 정리

  • AssSW를 위해서는 모션 벡터를 구할 필요가 있으며, URP에서의 샘플은 다음의 참고 사이트에 나와있다
  • AssSW용 모션 벡터 렌더 타겟은 XRPass에 추가하며, 이는 XRDisplaySubsystem.XRRenderPassmotionVectorRenderTarget으로 생성하는 것으로 보인다
  • 실제로 생성하는 것으로 보이는 부분은 XRPass.cs의 Create 메소드이며, 항상 생성하는 것이 아니라 xrRenderPass.hasMotionVectorPass로 분기한다
    • 이 부분도 AppSW를 위해 추가한 부분
  • 해당 메소드는 XRSystem.cs의 SetupFrame 메소드에서 매 프레임마다 호출되는데, 문제는 XR인지 아닌지를 SDK를 통해 체크한 결과가 false이면 호출이 스킵된다
    • bool xrEnabled = RefreshXrSdk()
  • 스킵될 시에는 비어있는 XRPass로 처리를 진행하는 것으로 보인다
    • AddPassToFrame(emptyPass);
  • emptyPass가 설정된 경우는 모션 벡터용 렌더 타겟은 선언만 되어 있을 뿐, 초기화나 리소스 획득 등이 이루어진 것은 아닌 것으로 추정된다
    • 즉, motionVectorRenderTarget(타입: RenderTargetIdentifier)의 값이 전부 0
    • 따라서 UniversalRenderer.cs의 if (cameraData.xr.motionVectorRenderTargetValid)에서 True가 되어서, 렌더 패스로써 추가된다
      • motionVectorRenderTargetValid는 motionVectorRenderTarget != invalidRT로 판정
  • 하지만 실제로 생성된 렌더 타겟은 없으므로, 렌더링 처리에서는 렌더 타겟을 찾지 못 해서, 우연히 앞에 설정된 Color Grading의 렌더 타겟(_InternalGradingLut)에 그대로 모션 벡터의 결과를 렌더링한 것으로 추정된다
 

[SpaceWarp] URP V1 Commit · Oculus-VR/Unity-Graphics@56cb0ec

First commit which adds support for Application SpaceWarp motion vector generation to the URP. Consists of using the motion vector texture handle from the XR pass to create a render pass, then to e...

github.com


해결법

결론부터 말하자면 없었다

정확히는 ‘모처럼 AppSW용으로 모션 벡터를 만들고 있으니 그걸 Windows에서는 일반적인 모션 벡터처럼 쓰면 재활용도 되고 좋지 않을까? 캬 이거 생각한 나 천재 아님?’은 실현 불가능했다

적어도 필자의 실력으로는 현재 구조를 간단히 수정하는 건 불가능하다는 결론에 이르렀다

 

XR인지 아닌지를 SKD에서 판단하고 있고, 그 내부를 볼 수 없는데다 Low Level에 관련되어 있는 것 같아서 이 이상의 조사는 불가능했다

사실 수정이 정말로 불가능하다냐면 그런 것은 아니고, 다만 대대적인 구조 변경과 다양한 케이스 처리가 필요하고 추가로 영향 범위로 상당해서, 안 하느니만 못 한 결과가 나올 확률이 매우 높다는 말이 정확하긴 할 것이다

우린 그걸 불가능하다고 부르기로 했어요

즉 모션 벡터가 필요하다면 그냥 일반적인 모션 벡터 패스를 만드는 게 낫다는 것…


2024.02.02 추가) ASW 모션 벡터 따위 필요 없을 경우라면!

AppSW용으로 모션 벡터 같은 건 필요 없고 그냥 단순히 Windows용 Build를 Oculus Link 등으로 실행시키고 싶다! 같은 상황이면 해결책은 존재한다

 

라고 할까 매우 단순한 얘기이다

그냥 다음과 같은 느낌으로 Windows용 Build시에는 모션 벡터 패스가 렌더링 파이프라인에 Enqueue되는 것을 막으면 된다

// **UniversalRenderer.cs에서 발췌**

public override void Setup(ScriptableRenderContext context, ref RenderingData renderingData)
{

    // ...생략

#if !UNITY_EDITOR
#if !UNITY_STANDALONE_WIN // 추가
            if (cameraData.xr.motionVectorRenderTargetValid)
            {
                RenderTargetHandle motionVecHandle = new RenderTargetHandle(cameraData.xr.motionVectorRenderTarget);
                var rtMotionId = motionVecHandle.Identifier();
                rtMotionId = new RenderTargetIdentifier(rtMotionId, 0, CubemapFace.Unknown, -1);

                // ID is the same since a RenderTexture encapsulates all the attachments, including both color+depth.
                m_OculusMotionVecPass.Setup(rtMotionId, rtMotionId);
                EnqueuePass(m_OculusMotionVecPass);
            }
#endif // 추가
#endif

    // ...생략

}

 

애초에 Oculus Link 등으로 실행하면 VR기기에서는 아무런 문제가 발생하지 않는다

(아마도 VR기기니 XRPass에서 모션 벡터용 렌더 타겟을 잘 만들어주는 것으로 추정된다)

하지만 모션 벡터를 렌더링하는 처리는 발생하고 AppSW는 작동하지 않으니 그냥 제외하는 게 베스트인 것으로 보인다


추가 정보) Unity Mock HMD

Project Settings에 XR Plug-in Management에는 Unity Mock HMD라는 설정이 있다

About the Mock HMD XR Plugin | MockHMD XR Plugin | 1.0.1-preview.3

 

About the Mock HMD XR Plugin | MockHMD XR Plugin | 1.0.1-preview.3

About the Mock HMD XR Plugin The Mock HMD XR Plugin enables you to build VR applications without a device. It provides stereo rendering support by mimicking the display properties of an HTC Vive HMD. XR plugin systems Display Display supports all graphics

docs.unity3d.com

이 설정을 On으로 하면 유니티 상에서도 VR기기의 양쪽 카메라 따로따로 보여주는데, 이 때 #if !UNITY_EDITOR를 코멘트한 상태해도 이슈가 발생하지 않았다

 

중단점으로 조사해보니 bool xrEnabled = RefreshXrSdk()에서 True가 반환되는 것이 확인되었다

하지만 프레임 디버거로 조사해보니 화면을 분할해서 보여주기 위해 다른 렌더 패스를 구축하는 것으로 확인되었으므로, 해결책이 맞는지는 의문이 든다

// XRSystem.cs

internal List<XRPass> SetupFrame(Camera camera, bool enableXRRendering)
{
    bool xrEnabled = RefreshXrSdk();
    // 생략...
    return framePasses;
}

마무리

“왜 안 되는 거지?!“와 “왜 되는 거지?!”가 번갈아 일어났던 이슈…

초반에는 재현이 너무 안 되서 미제 사건으로 남을 줄 알았는데, 해결은 못 했어도 원인 특정은 결국 해냈으니 일단 그걸로 만족해야겠다


참고 자료

Application SpaceWarp Developer Guide: Unity | Oculus Developers

 

Application SpaceWarp Developer Guide: Unity | Oculus Developers

 

developer.oculus.com

[SpaceWarp] URP V1 Commit · Oculus-VR/Unity-Graphics@56cb0ec

 

[SpaceWarp] URP V1 Commit · Oculus-VR/Unity-Graphics@56cb0ec

First commit which adds support for Application SpaceWarp motion vector generation to the URP. Consists of using the motion vector texture handle from the XR pass to create a render pass, then to e...

github.com

URPのRenderTarget関係のクラス解説 - Qiita

 

URPのRenderTarget関係のクラス解説 - Qiita

RenderTargetを管理するクラスは色々あります。最終的にエンジンのコアAPIが受け取るのは、RenderTexture 及び、RenderTargetIdentifer、NameId(int…

qiita.com