Unity standard shaders do not look nice in Subnautica. They are typically too dark, dull, do not react well to environmental lighting, and generally appear out of place.
That is why VF (unless you object loudly enough) replaces all shaders in your materials with the standard Subnautica shader MarmosetUBER, but it does nothing else.
Color, albedo and normal maps are transferred just fine, everything else is lost. Transparency is also lost but that is not the topic of this article.
Since the MarmosetUBER shader is largely (well, completely, really) undocumented, few have ever tried tinkering with it, even fewer successfully.
And all the achievements visible in the Echelon model are primarily due to copying the material properties of the Seamoth shader and then adapting some.
To simplify the process, I created four classes that should be integrable into every VF mod:
MaterialPrototype.cs, SurfaceShaderData.cs, MaterialAdaptation.cs, and MaterialFixer.cs. More on them later.
Material preparation in Unity
In order to get something meaningful out of these modifications, you should prepare your Unity standard materials in the following way:
The reflectivity of any translated material is derived from the smoothness property of the source Unity standard material. Depending on the smoothness source selection, either the main/albedo or the metallic map are forwarded as specular reflectivity source. That is, its alpha channel is used as an indicator for reflectivity where alpha=0 means no reflectivity and alpha=1 means basically a fuzzy mirror. This is akin to how smoothness works in Unity. The other channels (RGB) are not used as far my experiments went.
If the chosen source texture is not bound, the smoothness slider value is applied the same way for the entire material. If both are set, only the texture is used.
In addition to translating smoothness to reflectivity, the emission map (if set) is also applied as the Subnautica illumination map and will shine with the exact same brightness as on the Seamoth. It is added on top of the base texture and does not react to lighting.
It is unknown whether its alpha channel has any effect but the rest is pure addition. Meaning, if it's black, no emissions will be visible.
If no emission texture is set on the source material, the standard Unity black texture will be assigned instead, effectively turning emission off.
VF Configuration
To prevent VF from interfering with the following process, declare this in your main vehicle class:public override bool AutoApplyShaders => false;
That will tell VF not to mess with you. It also means you gotta do the material fixing yourself
Material fixing - Simple version
A new class, MaterialFixer.cs, was recently added that simplifies the process.
Simply instantiate a single invariant instance MaterialFixer on your vehicle implementation, then call MaterialFixer.OnUpdate() during your Update() routine and MaterialFixer.OnVehicleUndocked() in your OnVehicleUndocked() method.
It will automatically fix materials when it can. The default material selection processes all Standard shader Unity materials that don’t belong to renderers with ‘light’ in their name, that are not part of the canopy and that don’t contain skyboxes.
If you wish to disable the shader name limitation, you can pass the MaterialFixer.DefaultMaterialResolver method with altered arguments. Alternatively, you can specify your own resolver method to fetch the materials you wish fixed.
The class will do all the subsequent steps for you.
Material fixing - Manual steps
The material fixing involves the following steps:
- Load the Seamoth material prototype
- Fetch the MarmosetUBER shader
- Identify all materials you want to translate
- Extract surface shader data from these materials
- Combine the Seamoth material prototype, material surface shader data, and MarmosetUBER shader into one MaterialAdaptation per material
- Apply the MaterialAdaptation instance
- Possibly reapply some shader variables after undocking (extra chapter further down)
Prototype loading
First, you need to create an imprint of the seamoth hull material by using this class:
MaterialPrototype.cs
var prototype = MaterialPrototype.FromSeamoth();
Keep calling this method exactly once per frame in Update() until it returns non-null eventually. Then, using this prototype, continue on.
Fetch the MarmosetUBER shader
Fetch the correct shader this way:
Shader shader = Shader.Find("MarmosetUBER");
Renderer/material acquisition
Once you have a prototype and the shader, you then have to query every renderer and material in your vehicle game object, or retrieve them by some other means. VF typically does not target renderers containing ‘light’ in their name, but the logic is now entirely up to you. One option is:
var renderers = GetComponentsInChildren<Renderer>();
foreach (var renderer in renderers)
{
if (renderer.gameObject.name.ToLower().Contains("light"))
continue;
for (int i = 0; i < renderer.materials.Length; i++)
{
…
}
}
Surface shader data extraction
Using this class: SurfaceShaderData.cs, for every material you found by any means, you have to call the following method:
var data = SurfaceShaderData.From(renderer, i /*material index*/);
This will extract surface shader data (textures, color, etc) from a material. You should only call this once on every original, unmodified material. Once the transition is done, doing this again may yield weird results.
Note that by default that method returns null if the shader of the targeted material is not named “Standard”, which may or may not be what you want. If you want to get that data no matter what, pass a second parameter
ignoreShaderName:true
to the method.For the Echelon, material fixing is omitted if the default version of that method returns null. It may produce unexpected data if you force it to produce a result.
Alternatively, you can construct an instance of SurfaceShaderData yourself.
Apply the fix
To perform the final transition, instantiate this class:
MaterialAdaptation.cs
using this constructor:
var materialAdaptation = new MaterialAdaptation(prototype, data, shader);
and finally execute the transition:
materialAdaptation.ApplyToTarget();
All three classes (MaterialPrototype, SurfaceShaderData, and MaterialAdaptation) are read-only and can be stored in a list and re-applied any time. It only makes sense to store the final material adaptation, however.
For the following chapter you should really save these in a list on your vehicle.
Reverting undocking adjustments
Undocking (e.g. from a Moonpool) currently causes some shader variables to be reset. To counteract this, MaterialAdaptation instances have a second method
MaterialAdaptation.PostDockFixOnTarget()
which can be called whenever you detected an undocking event (via OnVehicleUndocked()). While ApplyToTarget() will achieve the same result, that method will check and correct every shader variable but experiments have shown that only three plus shader keywords actually need fixing. MaterialAdaptation.PostDockFixOnTarget() will reset only those values and not check the rest.The Echelon currently calls this method on all recorded material adaptations half a second after receiving an OnVehicleUndocked() event. When doing it immediately, you may see the fixes being unfixed again, so maybe wait a bit.
Examples
Manual version Update() loading:
if (!materialsFixed)
{
var prototype = MaterialPrototype.FromSeamoth();
if (prototype != null)
{
materialsFixed = true;
if (prototype.IsEmpty)
{
Debug.Log($"Material correction: No material found to reproduce");
}
else
{
Shader shader = Shader.Find("MarmosetUBER");
var renderers = GetComponentsInChildren<Renderer>();
foreach (var renderer in renderers)
{
if (renderer.gameObject.name.ToLower().Contains("light"))
continue;
for (int i = 0; i < renderer.materials.Length; i++)
{
try
{
var data = SurfaceShaderData.From(renderer, i);
if (data == null)
continue;
var materialAdaptation = new MaterialAdaptation(prototype, data, shader);
materialAdaptation.ApplyToTarget();
adaptations.Add(materialAdaptation);
}
catch (Exception ex)
{
Debug.Log($"Material correction: Deep copy failed of material #{i} of {renderer.name}: {ex}");
}
}
}
Debug.Log($"Material correction: All done. Applied {adaptations.Count} adaptations");
}
}
}
0 comments