A while ago we wrote about how we created a vision for what the world of Sword Reverie would look like using concept art. Today, we will talk about how we brought that vision to life in Unity 3d.
There were lots of challenges we needed to overcome to achieve the look and feel we want while simultaneously ensuring high performance. To maintain framerate we needed to support Single Pass Stereo Rendering and strictly monitor polycount and draw calls among other things.
The first step in our creation process is to create the actual terrain. Manipulating the terrain using Unity’s built in terrain tools is a little frustrating and there are quite a few assets on the asset store that can help improve Unity’s terrain creation workflow such as Gaia, World Creator, etc. We picked Gaia since it allows for procedural and manual creation using a stamp-based system and suits our needs well. We didn’t have to do anything specific to VR beyond from following Gaia’s performance best practices. We used a pixel error of 5 for our terrain. Try to keep the size of the terrain reasonable and use Sectr if you need large terrains.
Once the terrain is generated, its time to texture the terrain. Gaia does a good job of rule-based texturing, but to truly make the terrain shine, consider using Microsplat or CTS to add the finishing touches. These assets allows you to get more from your textures in terms of extra details and configurations. We used CTS 2019 since it provides great performance optimizations when you use the Lite version of the shader. Unity 2018.3+ is required. CTS is a fairly performance heavy system, but the Lite version allows you to use up to 16 PBR textures in one pass instead of one pass per 4 textures for the Unity default terrain shader.
Planting Trees (and Shrubs, and Grass as well)
A barren landscape is boring, so the next thing we wanted to do is add some vegetation (trees, shrubs, grass). However, vegetation is a huge a performance challenge in VR. Non low-poly trees have a heavy use of alpha cards and can quickly blow up the poly count. Lush grass can cause thousands of draw calls if unoptimized. Simply using dynamic and static batching isn’t enough.
We used Vegetation Studio Pro to help with the vegetation rendering performance. Vegetation Studio is a must own asset for creating scenes with lots of Vegetation. It has its own rendering system that uses GPU instancing and a cell-based structure to achieve huge performance boosts over trees rendered using Unity Terrain system. Vegetation Studio Pro has even better performance by using the job system and Burst complier for anyone using Unity 2018.2.11+. If CTS isn’t enough to get you to upgrade Unity, Vegetation Studio Pro surely is. We placed all our vegetation, buildings, props, grass, etc. with Vegetation Studio Pro and it only takes hundreds of draw calls and ~2ms of render time on a GTX 1070 running a Vive for hundreds of objects and lush grass. We used the Vegetation Studio billboarding system on the highest quality so we can be aggressive with billboarding and billboard early.
The Vegetation and object placement system is easy to use. Unfortunately, the GeNa 2 integration seems to be still broken as of the time of writing.
Now that we have a rendering system, we can go create trees we like. We didn’t like the look of low-poly trees that most VR games use, so we decided to use trees created using Unity Tree Creator. Trees created with Unity Tree Creator trees have pretty good performance in VR and can be easily edited in Unity. To achieve a stylized look, we picked simplified geometry and colors for the foliage and bark textures. Try to limit the amount of leaves since overdraw is a big problem in VR. If targeting a mobile VR platform (such as Quest), I would probably stick with low poly trees. SpeedTree is another option to consider, but it is a paid product and performance may be worse in certain cases.
To add the final touches, we wanted a foliage shader that lights up the leaves better. We picked Advanced Foliage Shader since it has good support for lighting and bending and performance is quite good .We loved the look of the lighting on the Tree Creator Shader. We edited the shader to support wind when using Vegetation Studio Pro.
Habitat for Humans
Next step is to add buildings and props to the environment, so the world feels like its populated. We also placed buildings and props using Vegetation Studio Pro, so performance remains high. Buildings and props are less of a bottleneck on performance than vegetation as long as you use assets with reasonable polycount, texture size, and one material.
We used Automatic LOD to automatically create LODs with lower poly count. It doesn’t look as good as manually created LODs but saved us lot of time. For billboards, we used Amplify Imposters to create billboards that have an illusion of a 3d object since it takes screenshots of an asset from multiple angles and displays the closest screenshot or a blend of the closest screenshots. The result still won’t look good when up close but will look much more 3 dimensional than a regular billboard and will allow you to use billboards earlier in a VR setting.
Smell the Sea and Feel the Sky
The last two components of a stunning environment are water and sky.
There are quite a few stylized water shader assets on the Unity Asset Store such as Definitize Stylized Water and Stylized Water Shader, but none that looked the way we wanted it to and has full VR support (Single Pass Stereo Rendering support). We ended rewriting part of the Definitive Stylized Water shader and real time reflection script to make it support Single Pass Stereo Rendering. The Definitive Stylized Water shader was created with Shader Forge so the easiest way to edit it is with Shader Forge. Unfortunately, the developer of Shader Forge stopped developing the project to focus on her games, so Shader Forge does not support Unity 2018+. We had to get an old version of Unity to get Shader Forge working (some forks of Shader Forge may support newer Unity versions).
Here is the Shader Forge Code Block we added as a Function_node:
float2 uvs = uv;
// If Single-Pass Stereo mode is active, transform the
// coordinates to get the correct output UV for the current eye.
float4 scaleOffset = unity_StereoScaleOffset[unity_StereoEyeIndex];
uvs = (uv - scaleOffset.zw) / scaleOffset.xy;
For the real time reflection script, we need the reflection camera to capture two inverted textures from the position of each eye instead of one and pass both textures to the shader. We thought real time water reflections would cost a lot of performance, but the performance impact was actually pretty low as long as the reflection resolution is kept low.
For our sky, we wanted to have beautiful volumetric clouds like those in JRPG games and a day night system. We checked out a few sky assets, including Enviro, Tenkoku, and Sky Master Ultimate.We picked Enviro because it has beautiful volumetric clouds based on Raymarching (based on a paper written by the devs for Horizon Zero Dawn), fog, day night cycle, realistic sun, moon, stars positions, and many more features. Enviro also has VR support out of the box, so it is a great starting point if you need more than a beautiful skybox. Since Enviro uses multiple full screen post-processing passes for certain features, which is especially hard on performance for VR due to high GPU pixel fill rate, you would need to pick which features to turn on and which to disable. I don’t recommend using Standard Enviro if your game depends on lots of post-processing effects, have high anti-aliasing/super-sampling requirements, or needs to target mobile VR (such as Quest). The mobile version may be more suited, but I didn’t test that with VR. You can do a little bit of performance optimization by removing Graphics.Blit calls for unused features (which hopefully makes it into the next update for the asset) and caching shader property ids. In the end, on a GTX 1070 running a Vive, volumetric clouds and fog each cost up to 1ms of render time.
General Settings and Optimization
Since we wanted to use MSAA for cheap anti-aliasing we had to use forward rendering. However, we also wanted to have dynamic day night cycle in an outdoor open world environment, so we needed real time precomputed GI instead of baked lighting. This limits the amount of lights we can have and make real time soft shadows very costly. We used an aggressive shadow distance to make things manageable. If you don’t need to change the position of your lights, using a mixed lighting strategy such as Shadowmask might give you better performance without impacting the visuals. If you are targeting a mobile VR platform, you might need to stick with baked lights only.
You can use IL2CPP scripting backend for faster script execution over Mono. The build time is longer, but you get some CPU performance boost if that is a bottleneck for your game.
Here is the result of browsing through tons of Unity Asset Store assets and weeks of performance optimization:
On a GTX 1070 running a Vive we get pretty good frame rate without reprojection or motion smoothing kicking in.