If you’re currently working on a project which involves porting an ArchViz scene into Unreal Engine and deploying on Oculus Quest, then this guide is for you. With virtual reality devices becoming cheaper and more accessible, we’re seeing more architecture, engineering, and construction (AEC) firms adopt VR in their design pipeline. Architectural Visualization (ArchViz) in particular is a lucrative industry for VR. Displaying realistic 3D models using VR and game engines are one of the best and cost effective way for designers to convey their ideas to clients before actual construction. However, importing 3D scenes into VR requires a meticulous process of graphics optimization due to hardware performance limitations. This prompted me to make this guide.

NOTE: This blog post is work-in-progress. It is incomplete due to ongoing projects I have at the moment. I will try to finish this guide when I have the time.

This post is a summary of the steps I do to optimize graphics for VR architectural visualization using Unreal Engine 4.26.2 and Oculus Quest 2. For this study, I’ll be using the ArchViz Interior sample project by Epic Games. This scene was created with ray tracing on desktop PC in mind. It has all the graphics bells and whistles we usually avoid in VR such as dynamic lighting, high triangle count, high texture resolution, etc. You can download the project yourself and follow along.

ArchViz Interior

I listed the primary references I used at the end of this post. Hope you find them useful for further reading.

Fundamentals of Real-time Rendering

Before attempting to optimize, it’s important to understand the fundamentals of real-time rendering and familiarize yourself with all the terminology. This course by Sjoerd de Jong is a very good refresher on the subject. Additionally, you can take a look at my graphics optimization resources list here. Some of the key takeaways are:

  • It’s more important to know what to optimize than how to optimize.
  • Optimization should be integrated into the pipeline, not a last minute job at the end of production.
  • Profile your app on a standalone build, not in the editor. Running the profiler in the editor can skew your numbers. You’d end up scratching your head figuring out why draw calls are 2-3 times higher than expected.
  • Perform your profiling on the target hardware, not on your high-end workstation.
  • Triangle count isn’t as big of a deal as it used to be in the past.

Performance Targets for Oculus Quest 2

Oculus provided the following numbers as a general guideline for Oculus Quest 1 apps:

  • 72 FPS for Oculus Quest 1
  • 150-175 draw calls per frame
  • 300,000-500,000 triangles per frame.

From my experience, ArchViz scenes are mostly static which allows you to squeeze more performance and go a little beyond these numbers. Furthermore, we’ll be targeting Quest 2 which provides more leeway. Below are the specs performance comparison between Quest 1 and Quest 2 taken from Tom Langan’s Facebook Connect 2020 presentation.

Ideally, ArchViz scenes to be ported to VR should be designed and created with VR in mind from the very beginning. In practice, however, this is seldom the case. Clients often create amazing looking renders first, and then decide to port to VR at the end. It’s important to manage the clients’ expectations in terms of the quality of the VR experience. Some rendering features need to be disabled to deliver a smooth performance.

Optimizing Project Settings for VR Development

Download and open the ArchViz Interior project. We need to change a bunch of settings and disable some rendering features for VR to work smoothly at 72 fps. We’ll be covering a lot of topics which I listed below:

  • Target Hardware Settings
  • Ray Tracing
  • Virtual Texturing
  • Forward Rendering
  • Mobile HDR
  • Volumetric Fog
  • Screen-space Post-processing
  • Dynamic Lights

Target Hardware Settings

Under Project Settings > Project > Target Hardware, change the settings to Mobile / Tablet and Scalable 3D or 2D.

Target Hardware

Changing these settings automatically disables a lot of rendering features which you can find in the DefaultEngine.ini file found in the Config folder of your project directory.

[/Script/EngineSettings.GameMapsSettings]
EditorStartupMap=/Engine/Maps/Templates/Template_Default.Template_Default
LocalMapOptions=
TransitionMap=None
bUseSplitscreen=False
TwoPlayerSplitscreenLayout=Horizontal
ThreePlayerSplitscreenLayout=FavorTop
FourPlayerSplitscreenLayout=Grid
bOffsetPlayerGamepadIds=False
GameInstanceClass=/Script/Engine.GameInstance
GameDefaultMap=/Engine/Maps/Templates/Template_Default.Template_Default
ServerDefaultMap=/Engine/Maps/Entry.Entry
GlobalDefaultGameMode=/Script/Engine.GameModeBase
GlobalDefaultServerGameMode=None

[/Script/HardwareTargeting.HardwareTargetingSettings]
TargetedHardwareClass=EHardwareClass::Mobile
DefaultGraphicsPerformance=EGraphicsPreset::Scalable

[/Script/Engine.RendererSettings]
r.Mobile.DisableVertexFog=True
r.Shadow.CSM.MaxMobileCascades=2
r.MobileMSAA=1
r.Mobile.AllowDitheredLODTransition=False
r.Mobile.AllowSoftwareOcclusion=False
r.Mobile.VirtualTextures=False
r.DiscardUnusedQuality=False
r.AllowOcclusionQueries=True
r.MinScreenRadiusForLights=0.030000
r.MinScreenRadiusForDepthPrepass=0.030000
r.MinScreenRadiusForCSMDepth=0.010000
r.PrecomputedVisibilityWarning=False
r.TextureStreaming=True
Compat.UseDXT5NormalMaps=False
r.VirtualTextures=False
r.VirtualTexturedLightmaps=False
r.VT.TileSize=128
r.VT.TileBorderSize=4
r.vt.FeedbackFactor=16
r.VT.EnableCompressZlib=True
r.VT.EnableCompressCrunch=False
r.ClearCoatNormal=False
r.ReflectionCaptureResolution=128
r.Mobile.ReflectionCaptureCompression=False
r.ReflectionEnvironmentLightmapMixBasedOnRoughness=True
r.ForwardShading=False
r.VertexFoggingForOpaque=True
r.AllowStaticLighting=True
r.NormalMapsForStaticLighting=False
r.GenerateMeshDistanceFields=False
r.DistanceFieldBuild.EightBit=False
r.GenerateLandscapeGIData=False
r.DistanceFieldBuild.Compress=False
r.TessellationAdaptivePixelsPerTriangle=48.000000
r.SeparateTranslucency=False
r.TranslucentSortPolicy=0
TranslucentSortAxis=(X=0.000000,Y=-1.000000,Z=0.000000)
r.CustomDepth=1
r.CustomDepthTemporalAAJitter=True
r.PostProcessing.PropagateAlpha=0
r.DefaultFeature.Bloom=False
r.DefaultFeature.AmbientOcclusion=False
r.DefaultFeature.AmbientOcclusionStaticFraction=True
r.DefaultFeature.AutoExposure=False
r.DefaultFeature.AutoExposure.Method=0
r.DefaultFeature.AutoExposure.Bias=1.000000
r.DefaultFeature.AutoExposure.ExtendDefaultLuminanceRange=False
r.UsePreExposure=True
r.EyeAdaptation.EditorOnly=False
r.DefaultFeature.MotionBlur=False
r.DefaultFeature.LensFlare=False
r.TemporalAA.Upsampling=False
r.SSGI.Enable=False
r.DefaultFeature.AntiAliasing=0
r.DefaultFeature.LightUnits=1
r.DefaultBackBufferPixelFormat=4
r.Shadow.UnbuiltPreviewInGame=True
r.StencilForLODDither=False
r.EarlyZPass=3
r.EarlyZPassOnlyMaterialMasking=False
r.DBuffer=True
r.ClearSceneMethod=1
r.BasePassOutputsVelocity=False
r.VertexDeformationOutputsVelocity=False
r.SelectiveBasePassOutputs=False
bDefaultParticleCutouts=False
fx.GPUSimulationTextureSizeX=1024
fx.GPUSimulationTextureSizeY=1024
r.AllowGlobalClipPlane=False
r.GBufferFormat=1
r.MorphTarget.Mode=True
r.GPUCrashDebugging=False
vr.InstancedStereo=False
r.MobileHDR=True
vr.MobileMultiView=False
r.Mobile.UseHWsRGBEncoding=False
vr.RoundRobinOcclusion=False
vr.ODSCapture=False
r.MeshStreaming=False
r.WireframeCullThreshold=5.000000
r.RayTracing=False
r.RayTracing.UseTextureLod=False
r.SupportStationarySkylight=True
r.SupportLowQualityLightmaps=True
r.SupportPointLightWholeSceneShadows=True
r.SupportAtmosphericFog=True
r.SupportSkyAtmosphere=True
r.SupportSkyAtmosphereAffectsHeightFog=False
r.SkinCache.CompileShaders=False
r.SkinCache.DefaultBehavior=1
r.SkinCache.SceneMemoryLimitInMB=128.000000
r.Mobile.EnableStaticAndCSMShadowReceivers=True
r.Mobile.EnableMovableLightCSMShaderCulling=True
r.Mobile.AllowDistanceFieldShadows=True
r.Mobile.AllowMovableDirectionalLights=True
r.MobileNumDynamicPointLights=4
r.MobileDynamicPointLightsUseStaticBranch=True
r.Mobile.EnableMovableSpotlights=False
r.Mobile.EnableMovableSpotlightsShadow=False
r.GPUSkin.Support16BitBoneIndex=False
r.GPUSkin.Limit2BoneInfluences=False
r.SupportDepthOnlyIndexBuffers=True
r.SupportReversedIndexBuffers=True
r.LightPropagationVolume=False
r.Mobile.AmbientOcclusion=False
r.GPUSkin.UnlimitedBoneInfluences=False
r.GPUSkin.UnlimitedBoneInfluencesThreshold=8
r.Mobile.PlanarReflectionMode=0
bStreamSkeletalMeshLODs=(Default=False,PerPlatform=())
bDiscardSkeletalMeshOptionalLODs=(Default=False,PerPlatform=())
VisualizeCalibrationColorMaterialPath=None
VisualizeCalibrationCustomMaterialPath=None
VisualizeCalibrationGrayscaleMaterialPath=None

[/Script/Slate.SlateSettings]
bExplicitCanvasChildZOrder=True

NOTE: Decals not affected by Exponential Height Fog on ES3.1. Link.

Ray Tracing

Obviously Ray Tracing is a big no-no for VR. Let’s make sure the setting is disabled under Project Settings > Engine > Rendering > Ray Tracing. Moreover, let’s set Project Settings > Platforms > Windows > Default RHI to Default.

Virtual Texturing

Virtual Texturing allows you to use large textures while consuming lower memory footprint. However, this feature causes lightmap errors which crashes the editor when packaging for Android:

Error: Assertion failed: bAllowHighQualityLightMaps == true [File:D:/Build/++UE4/Sync/Engine/Source/Runtime/Engine/Private/SceneManagement.cpp] [Line: 392]

Let’s disable this feature by unchecking Enable virtual texture support on Mobile found in Project Settings > Engine > Rendering > Mobile. Additionally, uncheck Enable virtual texture support in Project Settings > Engine > Rendering > Virtual Textures.

Volumetric Effects

Outside the window is an Exponential Height Fog. By default, fog does not render on mobile platforms for performance reasons. There are, however, situations when we need it such as this one. To enable fog rendering, uncheck the setting in Project Settings > Engine > Rendering > Mobile > Disable vertex fogging in mobile shaders.

Sometimes, clients accidentally leave the volumetric fog setting turned on. This is a massive performance cost. You don’t even see the effect if the fog is not thick enough. Turn this off.

To maximize performance, the best solution is to remove all volumetric effects such as clouds and fog and replace them with a static skybox.

Forward Rendering

UE4 uses deferred rendering by default because it provides access to more rendering features. However, you want to prioritize performance over visual quality in terms of VR apps. This is where forward rendering comes in. There have been claims that switching to forward rendering improves performance by 15-20%. Other than performance, the other feature we want with forward rendering is Multisample Anti-Aliasing (MSAA).

There are limitations, however, as mentioned in the documentation. Here are some notable ones:

  • Screen Space Techniques (SSR, SSAO, Contact Shadows)
  • Dynamically Shadowed Translucency
  • Translucency receiving environment shadows from a Stationary Light
  • MSAA on D-Buffer Decals and Motion Blur

Now let’s do the following:

  • Enable forward shading in Project Settings > Engine > Rendering > Forwarding Shading.
  • Set Anti-Aliasing Method to MSAA in Project Settings > Engine > Rendering > Default Settings > Anti-Aliasing.
  • Set 4x MSAA in Project Settings > Engine > Rendering > Mobile > Mobile MSAA.

Mobile HDR

Mobile HDR is required for some post-processing features to work. However, it causes severe performance issues on Oculus Quest. It’s best to disable this in Project Settings > Engine > Rendering > VR > Mobile HDR.

Post-Processing

Next, let’s disable features that are turned on by default on mobile platforms such as bloom, lens flares, vignette, depth of field, screen-space ambient occlusion (SSAO), motion blur, and screen-space reflections (SSR). Make sure the Intensity is set to zero. Simply unchecking the setting will not turn it off. Since we’re using forward rendering, SSAO and SSR won’t work. I still set them to zero just to be sure.

Furthermore, I disabled Auto Exposure because we need to relight the scene. It’s found in Project Settings > Engine > Rendering > Default Settings > Auto Exposure.

Dynamic Lights

ArchViz heavily relies on lighting to achieve the best visual quality. Right now, the scene doesn’t look good due to all the changes we made eariler.

We need to strike the balance between quality and performance. The way forward is to convert all dynamic lights to static lights and make a build on the Quest 2 to check for performance. Once the frame rate is okay, slowly adjust the lights until an acceptable visual quality is achieved. Take note that forward rendering can only support 4 overlapping shadow casting movable lights. If you absolutely need multiple shadow casting lights movable, then consider this limitation.

Overlapping Lights

Also make sure that lightmass baking is actually enabled by unchecking Force No Precomputed Lighting.

Force no precomputed lighting

Now it’s time to build the lighting. Adjust the lighting parameters and rebuild lighting until you get a decent result. Don’t aim for quality yet, we only need to test the performance.

After Lightmass

Optimizing Triangle Count Using Automatic LODs

Now that we’ve disabled most of the performance-heavy rendering features, we can begin the actual optimization process. Use stat RHI to get a quick glance at the draw calls and triangle count. These numbers aren’t accurate because we’re using it while the editor is open, but we can clearly see that triangle count is the one we should address right now with 11 million tris. Another tip, disable the editor widgets by pressing “G” while on the viewport to prevent the widgets from skewing the numbers.

Ideally, we reduce the triangle count by opening the 3D model files in a 3D modeling app and create LODs from there. However, most of the time we don’t have access to these files and also have a tight deadline. Exporting these models out of UE4 results in triangulated meshes which are cumbersome to clean. Thus, auto-generating level-of-detail is the only viable option. Check out how to generate LODs in the documentation.

The Statistics table, found in the Window drop-down menu, is a handy tool we can use to identify which objects in the scene are contributing to the bottlenecks. Sorting based on triangle count, the following have high triangle density: blanket, curtains, fan blades, couch, hanging light, vases, and so on. With this information, we now know which specific assets to optimize.

Statistics

Using this information, go ahead and generate LODs for each mesh. In my case, I was able to reduce the triangle count from 11,000,000 to 300,000.

Stat RHI after optimization

Take note that auto LODs can bust some shapes such as the top of this vase. This is a case which requires manual editing in your 3d modeling app.

Optimizing Draw Calls

The draw calls are at 370 right now. The documentation provides methods on how to reduce this number. In our case, the best way is to minimize the number of materials, create texture atlases, and combine different objects. In other ArchViz levels withmultiple rooms, proper visibility and occlusion culling is the way to go.

Unfortunately, I don’t have access to the original 3D asset files, I can combine these materials to reduce draw calls. I’ll leave them for now.

Optimizing Shader Complexity

The shader complexity viewport mode allows you to quickly spot the performance impact of certain shaders. Below, you can see that the glass materials are the most expensive. It’s alright to leave these alone as long as they occupy a small area the user’s view. Difference in forward shading.

Shader Complexity

A quick look at the master material reveals room for optimization. We can remove all the nodes related to ray tracing. Additionally, we can do channel packing and combine the Rougness, Metallic, and Ambient Occlusion maps into a single RMA texture.

Deploying on Oculus Quest 2

We’ve done the first pass of optimization. Let’s now configure the project for Oculus development and make a test build on Oculus Quest 2.

A word of advice. A lot of developers have experienced issues when setting up Android SDK and NDK because the environment variables are not set properly. Because of this, you’ll encounter an error when running SetupAndroid.bat. Check out this tutorial by Code Prof which works for UE 4.26.2.

OVR Metrics Tool

OVR Metrics Tool is very useful for obtaining an app’s performance data on the Oculus Quest. Once our ArchViz app is running, we can use this tool to answer basic questions:

  • Do we have a consistent 72 fps?
  • Are we GPU or CPU bound?
  • What are the GPU/CPU utilization percentage?
OVR Metrics Tool
Image from Oculus Developer Documentation

In ArchViz, we are GPU bound most of the time. The documentation provides an easy way to know if we’re GPU/CPU bound by looking at APP T:

  • If App T > 13800 & FPS < 72, then 100% GPU bottleneck (CPU could also be going over budget but is masked by GPU)
  • If App T < 13800 & FPS < 72, then 100% CPU bottleneck.

Other useful indicators are CPU U, GPU U, CPU L, and GPU L (“U” stands for utilization while “L” stands for level). These numbers indicate how much load the CPU and GPU are doing. High numbers signal room for optimization.

Unfortunately, the OVR Metrics Tool overlay does not show up on screenshots. I can’t show you the graph I see inside the Quest. So, I’ll summarize the the numbers I see below:

  • There is 100% GPU usage but only 50% CPU usage
  • There are no dropped frames
  • Opening the Oculus menu causes frame drops.

To Be Continued…

This post is incomplete due to ongoing projects I have right now. I will try to finish this guide when I have the time. I plan to talk more about the following topics:

  • Foliage
  • Mirror
  • Water
  • Transparent glass layers
  • Complex shaders for hair, fur, fabric

References


Send me a message.


Recent Posts: