I came up with a custom solution.
One interesting thing I have noticed is that it looks like only RGB_ETC_UNorm are affected by the issue. Also it seems that new AssetBundles created with Unity6 are using ETC2 format.
So, at least in my application, it is safe to only and always convert RGB_ETC_UNorm textures.
To do so, I am using the following shader to do the RGB to Linear conversion:
The shader is called with Graphics.Blit() and the result is stored in dynamically created RenderTextures. The original textures are replaced by the new RenderTextures in their respective materials.
Here is the code I execute after loading my Scene Bundle:
#if UNITY_ANDROID
// Compatibility steps for pre Unity 6 Android Asset Bundles
// For some reason RGB_ETC_UNorm textures require RGB to linear conversion
// This is easy todo with 2D textures but not cubemaps
// So for cubemaps we reduce the exposure/intensity by 0.5 of the Skyboxes and ReflectionProbes
// It seems that Unity 6 Android Asset Bundles are using ETC2 format by default so this code should only executes for older AssetBundles
// Iterate all materials currently loaded by the application (player and Asset Bundles materials)
var loadedMaterials = Resources.FindObjectsOfTypeAll<Material>();
foreach(var loadedMaterial in loadedMaterials) {
if(_playerMaterials.Contains(loadedMaterial)) { // discards materials that are not in the loaded AssetBundles
continue;
}
// Iterate the newly material textures
var texturePropertyIds = loadedMaterial.GetTexturePropertyNameIDs();
foreach(var texturePropertyId in texturePropertyIds) {
// Skip normal maps, it looks like they are not affected (_normalTextureId = Shader.PropertyToID("_BumpMap"), texturePropertyId = Shader.PropertyToID("_DetailNormalMap"))
if(texturePropertyId == _normalTextureId || texturePropertyId == _detailNormalTextureId) {
continue;
}
var texture = loadedMaterial.GetTexture(texturePropertyId);
// Only treat RGB_ETC_UNorm textures since it looks like these are the only one with such issue
if(texture && texture.graphicsFormat == GraphicsFormat.RGB_ETC_UNorm) {
// Cubemaps are more complicated to connvert, it's easier to reduce Skyboxe _Exposure coeefiient (but not perfect)
if(texture.dimension == TextureDimension.Cube) {
if(loadedMaterial.HasFloat("_Exposure")) {
loadedMaterial.SetFloat("_Exposure", 0.5f * loadedMaterial.GetFloat("_Exposure"));
}
}
else { // Copy the texture to a RenderTexture using RGB to Linear conversion shader
// create a RenderTexture with the same properties than the original one
RenderTexture convertedTexture = new(texture.width, texture.height, 0);
convertedTexture.useMipMap = texture.mipmapCount > 1;
convertedTexture.filterMode = texture.filterMode;
convertedTexture.wrapMode = texture.wrapMode;
convertedTexture.anisoLevel = texture.anisoLevel;
convertedTexture.mipMapBias = texture.mipMapBias;
convertedTexture.dimension = texture.dimension;
// Copy the texture into the RenderTexture using RGB to Linear conversion shader (must specify 0 since our shader is ShaderGraph)
Graphics.Blit(texture, convertedTexture, _rgbToLinearMaterial, 0);
// Replace the material texture by the RenderTexture
loadedMaterial.SetTexture(texturePropertyId, convertedTexture);
// Remember the created RenderTextures so we can destroy them later when we don't use the AssetBundle anymore (avoid memory leaks)
_convertedTextures.Add(convertedTexture);
}
}
}
}
// ReflectionProbe textures are not accessible via materials so we also iterate ReflectionProbes and set their intensity if necessary
var probes = Resources.FindObjectsOfTypeAll<ReflectionProbe>();
foreach(var probe in probes) {
if(probe.texture && probe.texture.graphicsFormat == GraphicsFormat.RGB_ETC_UNorm) {
probe.intensity *= 0.5f;
}
}
#endif
Note: this code is not fully optimized.
For instance, several materials could use the same texture, in that case my code create a new RenderTexture for each time the same original texture is referenced by materials.
Also, I am not unloading the original textures and the new RenderTextures are using at least as much additional memory.
Finally, there is the Cubemaps issue where I haven’t found an easy way to do the RGB to Linear conversion (doesn’t work with Graphics.Blit()). So I prefer to simply reduce the exposure/intensity of the Skyboxes/ReflectionProbes but it doesn’t produce the exact same result (ambient lighting and reflections are a bit less satured).
Edit: Normal maps don’t look good when converted. Not sure if they are affected by the issue but it’s preferable to skip them during the conversion step.