Loading a CubeTexture with cubemaps produces black skybox/specular

I spent half a day on this problem and I must confess I’m out of idea right now. Hopefully, you can help me now.

I’m using cmgen/Filament to bake HDR environments, resulting in several files:

  • a cubemap (DDS file) without mipmap in full resolution, used to draw the skybox
  • a cubemap (DDS file) with 5 levels of mipmaps, smaller resolution (256x256 for level 0), containing the prefiltered specular contribution
  • a text file with spherical harmonics for the diffuse contribution

Loading the full resolution without mipmap seems to work fine, with the fix I have submitted yesterday.
However, with my prefiltered specular cubemap, it results in a black screen even if the cubemap appears correctly in Spector.js:

The PG can be found here: https://www.babylonjs-playground.com/#LVAB4K#3

The dds looks broken and actually you can see the result in firefox

By Broken I mean it might be a texture format not supported by babylon

Thanks, the message wasn’t displayed on Chrome, and actually, it’s quite helpful.
The problem is my DDS file contains 5 levels of mipmaps, but it’s 256x256, so incomplete because the 5th mipmap level is 8x8 (4x4, 2x2 and 1x1 aren’t uploaded).
The problem is TEXTURE_MAX_LEVEL is never set in Babylon (the default value 1000 is used)…
If I call gl.texParameteri(gl.TEXTURE_CUBE_MAP, gl.TEXTURE_MAX_LEVEL, 4); in _setCubeMapTextureParams, it works just fine!
I’ll try to submit a PR.

Yo @Michael did you ever get cmgen working correctly ?

I am currently using CMFT (Cubemap Filter Tools) and I am looking at switching to cmgen because its GGX and CMFT is Blinn/Phong.

Why not using the babylon tools as they generate the best results for babylon.

Yo @sebavan .. i am trying to work on my Babylon Toolkit Export from Unity to GLTF. So i generate my reflection from Unity During the export process. There is no manually taking a hdr, i generate directly from a Unity Reflection probe at export time.

But you right, the tools at Babylon.js Texture Tools are great… So i studied HOW the IBL make native .env file and create a Native Reflection Probe export diretly from unity.

Was easier than i thought is was going to be… The first MAJOR thing i had to do was PREFILTER iin Unity. I had been working on a shader to do the prefilter in Unity for use with DDS, Once i got my DDS Packer created, I thought i should be able to do this and pack ENV as well.

Check this out:

    public static void ExportBabylonIblEnvFromHdrCubemap(
        Cubemap src,
        string outEnvPath,
        int size = 128,
        int samples = 4096,
        bool rgba32f = false,
        string dumpFacesPngDir = null,
        string imageType = "image/webp",
        float lodGenerationScale = 0.8f,
        float lodGenerationOffset = 0.0f,
        bool forceBaseMipForAllMips = false)
    {
        if (src == null) throw new ArgumentNullException(nameof(src));
        if (size <= 0 || (size & (size - 1)) != 0) throw new Exception("size must be power-of-two (128, 256, 512, ...).");
        if (samples <= 0) samples = 1024;
        int mipCount = CalcMipCount(size);

        // Create cubemap RT (prefiltered result)
        var desc = new RenderTextureDescriptor(size, size, rgba32f ? RenderTextureFormat.ARGBFloat : RenderTextureFormat.ARGBHalf, 0);
        desc.dimension = UnityEngine.Rendering.TextureDimension.Cube;
        desc.volumeDepth = 6;
        desc.useMipMap = true;
        desc.autoGenerateMips = false;
        desc.msaaSamples = 1;
        desc.sRGB = false;

        var cubeRT = new RenderTexture(desc);
        cubeRT.name = "BabylonIBL_PrefilterCubeRT";
        cubeRT.Create();

        // Prefilter material/shader
        var shader = Shader.Find("Hidden/BabylonIBL/PrefilterGGX");
        if (shader == null) throw new Exception("Missing shader: Hidden/BabylonIBL/PrefilterGGX. Add BabylonIblPrefilter.shader to your project.");

        var mat = new Material(shader);
        mat.hideFlags = HideFlags.HideAndDontSave;

        mat.SetTexture("_EnvCube", src);
        mat.SetInt("_SampleCount", samples);
        mat.SetInt("_Convention", 0); // Note: Always Use Direct-X Convention

        // Render each face+mip
        for (int mip = 0; mip < mipCount; mip++)
        {
            // Compute roughness so that it matches Babylon's linear roughness -> mip mapping.
            // Babylon (TextureTools) uses a linear roughness (alpha) per LOD computed as:
            //   alpha = 2^((lod - lodOffset) / lodScale) / size
            // and expects the shader's "roughness" to be the perceptual roughness such that
            // alpha == roughness*roughness. To match that, we set roughness = sqrt(alpha).
            float linearRoughnessAlpha;
            if (mip == 0)
            {
                linearRoughnessAlpha = 0f; // perfectly smooth
            }
            else
            {
                // Use Babylon's formula: alpha = 2^((lod - offset) / scale) / size
                // We invert it for baking: for a given mip (lod) compute alpha using the same mapping
                linearRoughnessAlpha = Mathf.Pow(2.0f, (mip - lodGenerationOffset) / lodGenerationScale) / size;
            }

            float roughness = Mathf.Sqrt(Mathf.Max(0f, linearRoughnessAlpha));
            mat.SetFloat("_Roughness", roughness);

            for (int faceIndex = 0; faceIndex < 6; faceIndex++)
            {
                mat.SetInt("_FaceIndex", faceIndex);

                Graphics.SetRenderTarget(cubeRT, mip, Faces[faceIndex]);
                GL.PushMatrix();
                GL.LoadOrtho();
                mat.SetPass(0);
                DrawFullscreenQuad();
                GL.PopMatrix();
            }
        }

        // Pack to .env (and optional dump)
        Directory.CreateDirectory(Path.GetDirectoryName(outEnvPath) ?? ".");
        ENVPacker.BuildSingleCubemapEnvFromPrefilteredRenderTexture(
            cubeRT,
            outEnvPath,
            imageType,
            rgba32f,
            dumpFacesPngDir,
            flipYForDumpOnly: true,
            lodGenerationScale: lodGenerationScale,
            lodGenerationOffset: lodGenerationOffset,
            forceAllMipsToBase: forceBaseMipForAllMips);

        UnityEngine.Object.DestroyImmediate(mat);
        cubeRT.Release();
        UnityEngine.Object.DestroyImmediate(cubeRT);
    }

    /// <summary>
    /// Compute a lodGenerationOffset so that a given perceptual roughness (p) will map to desired targetMip when using the
    /// mapping: alpha = 2^((lod - offset) / lodScale) / size with alpha = p*p.
    /// offset = targetMip - lodScale * log2(alpha * size)
    /// </summary>
    public static float ComputeLodGenerationOffsetForTargetMip(int size, float perceptualRoughness, float lodGenerationScale, int targetMip)
    {
        float alpha = Mathf.Max(perceptualRoughness * perceptualRoughness, 1e-7f);
        float log2Val = Mathf.Log(alpha * size, 2.0f);
        return targetMip - lodGenerationScale * log2Val;
    }

    private static void DrawFullscreenQuad()
    {
        GL.Begin(GL.QUADS);
        GL.TexCoord2(0f, 0f); GL.Vertex3(0f, 0f, 0f);
        GL.TexCoord2(1f, 0f); GL.Vertex3(1f, 0f, 0f);
        GL.TexCoord2(1f, 1f); GL.Vertex3(1f, 1f, 0f);
        GL.TexCoord2(0f, 1f); GL.Vertex3(0f, 1f, 0f);
        GL.End();
    }

    private static int CalcMipCount(int size)
    {
        int m = 1;
        while (size > 1) { size >>= 1; m++; }
        return m;
    }

And my IBLPrefilterGGX shader:

Shader "Hidden/BabylonIBL/PrefilterGGX"
{
    SubShader
    {
        Tags { "RenderType"="Opaque" "Queue"="Overlay" }
        Cull Off ZWrite Off ZTest Always

        Pass
        {
            HLSLPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #include "UnityCG.cginc"

            samplerCUBE _EnvCube;
            int _FaceIndex;      // 0..5 in +X,-X,+Y,-Y,+Z,-Z
            int _SampleCount;    // 0 => handled in C#
            float _Roughness;    // 0..1
            int _Convention;     // 0 = DirectX-like (Unity DDS), 1 = alternate

            struct appdata { float4 vertex : POSITION; float2 uv : TEXCOORD0; };
            struct v2f { float4 pos : SV_POSITION; float2 uv : TEXCOORD0; };

            v2f vert(appdata v)
            {
                v2f o;
                o.pos = UnityObjectToClipPos(v.vertex);
                o.uv = v.uv;
                return o;
            }

            // Hammersley
            float RadicalInverse_VdC(uint bits)
            {
                bits = (bits << 16) | (bits >> 16);
                bits = ((bits & 0x55555555u) << 1) | ((bits & 0xAAAAAAAAu) >> 1);
                bits = ((bits & 0x33333333u) << 2) | ((bits & 0xCCCCCCCCu) >> 2);
                bits = ((bits & 0x0F0F0F0Fu) << 4) | ((bits & 0xF0F0F0F0u) >> 4);
                bits = ((bits & 0x00FF00FFu) << 8) | ((bits & 0xFF00FF00u) >> 8);
                return float(bits) * 2.3283064365386963e-10; // / 0x100000000
            }

            float2 Hammersley(uint i, uint N)
            {
                return float2((float)i / (float)N, RadicalInverse_VdC(i));
            }

            float3 ImportanceSampleGGX(float2 Xi, float roughness, float3 N)
            {
                float a = roughness * roughness;

                float phi = 2.0 * UNITY_PI * Xi.x;
                float cosTheta = sqrt((1.0 - Xi.y) / (1.0 + (a*a - 1.0) * Xi.y));
                float sinTheta = sqrt(1.0 - cosTheta * cosTheta);

                float3 H;
                H.x = cos(phi) * sinTheta;
                H.y = sin(phi) * sinTheta;
                H.z = cosTheta;

                // tangent space -> world
                float3 up = abs(N.z) < 0.999 ? float3(0,0,1) : float3(1,0,0);
                float3 tangent = normalize(cross(up, N));
                float3 bitangent = cross(N, tangent);

                float3 sampleVec = tangent * H.x + bitangent * H.y + N * H.z;
                return normalize(sampleVec);
            }

            // Face UV -> direction (DirectX-ish cubemap)
            float3 FaceUvToDir(float2 uv, int faceIndex, int convention)
            {
                float2 st = uv * 2.0 - 1.0; // [-1,1]
                float s = st.x;
                float t = st.y;

                // Most “scrambled skybox” issues are due to these signs.
                // convention=0 matches typical DDS/DirectX cubemap layout in Unity.
                // convention=1 is an alternate sign set you can try for Babylon differences.
                if (convention == 0)
                {
                    if (faceIndex == 0) return normalize(float3( 1.0, -t, -s)); // +X
                    if (faceIndex == 1) return normalize(float3(-1.0, -t,  s)); // -X
                    if (faceIndex == 2) return normalize(float3( s,  1.0,  t)); // +Y
                    if (faceIndex == 3) return normalize(float3( s, -1.0, -t)); // -Y
                    if (faceIndex == 4) return normalize(float3( s, -t,  1.0)); // +Z
                    return                 normalize(float3(-s, -t, -1.0));     // -Z
                }
                else
                {
                    // Alternate: flips some axes
                    if (faceIndex == 0) return normalize(float3( 1.0, -t,  s)); // +X
                    if (faceIndex == 1) return normalize(float3(-1.0, -t, -s)); // -X
                    if (faceIndex == 2) return normalize(float3( s,  1.0, -t)); // +Y
                    if (faceIndex == 3) return normalize(float3( s, -1.0,  t)); // -Y
                    if (faceIndex == 4) return normalize(float3(-s, -t,  1.0)); // +Z
                    return                 normalize(float3( s, -t, -1.0));     // -Z
                }
            }

            float4 frag(v2f i) : SV_Target
            {
                float3 R = FaceUvToDir(i.uv, _FaceIndex, _Convention);
                float roughness = saturate(_Roughness);

                // Roughness 0 => exact lookup
                if (roughness <= 1e-5)
                {
                    float3 c0 = texCUBE(_EnvCube, R).rgb;
                    return float4(c0, 1);
                }

                uint N = (uint)max(_SampleCount, 1);
                float3 Ndir = R;
                float3 V = R;

                float3 prefiltered = 0;
                float totalWeight = 0;

                [loop]
                for (uint s = 0; s < N; s++)
                {
                    float2 Xi = Hammersley(s, N);
                    float3 H = ImportanceSampleGGX(Xi, roughness, Ndir);
                    float3 L = normalize(2.0 * dot(V, H) * H - V);

                    float NoL = saturate(dot(Ndir, L));
                    if (NoL > 0.0)
                    {
                        float3 c = texCUBE(_EnvCube, L).rgb;
                        prefiltered += c * NoL;
                        totalWeight += NoL;
                    }
                }

                prefiltered /= max(totalWeight, 1e-4);
                return float4(prefiltered, 1);
            }
            ENDHLSL
        }
    }
}

And my ENV Packer:

#if UNITY_EDITOR
using System;
using System.IO;
using System.Text;
using System.Collections.Generic;
using UnityEngine;

public static class ENVPacker
{
    // Magic bytes used by BabylonJS env format
    private static readonly byte[] MagicBytes = new byte[] { 0x86, 0x16, 0x87, 0x96, 0xF6, 0xD6, 0x96, 0x36 };

    private static readonly string[] FaceNames = { "PositiveX","NegativeX","PositiveY","NegativeY","PositiveZ","NegativeZ" };

    public static void BuildSingleCubemapEnvFromPrefilteredRenderTexture(
        RenderTexture cube,
        string outEnvPath,
        string imageType = "image/webp",
        bool rgba32f = false,
        string dumpFacesPngDir = null,
        bool flipYForDumpOnly = true,
        float lodGenerationScale = 0.8f,
        float lodGenerationOffset = 0.0f,
        bool forceAllMipsToBase = false)
    {
        if (cube == null) throw new ArgumentNullException(nameof(cube));
        if (cube.dimension != UnityEngine.Rendering.TextureDimension.Cube || cube.volumeDepth != 6) throw new Exception("cube.volumeDepth MUST be 6. Allocate with RenderTextureDescriptor dimension=Cube, volumeDepth=6.");
        if (!cube.useMipMap) throw new Exception("cube.useMipMap must be true (need full mip chain down to 1x1).");

        int baseSize = cube.width;
        if (cube.height != baseSize) throw new Exception("Cubemap faces must be square.");

        int mipCount = CalcMipCount(baseSize);

        if (!string.IsNullOrEmpty(dumpFacesPngDir))
        {
            DumpCubemapFacesToPng(cube, dumpFacesPngDir, mipCount, rgba32f, flipYForDumpOnly);
        }

        Directory.CreateDirectory(Path.GetDirectoryName(outEnvPath) ?? ".");

        // Collect encoded images in the order expected by Babylon env (mip major, face minor)
        var encodedImages = new List<byte[]>();
        encodedImages.Capacity = mipCount * 6;

        // Buffer to optionally store base-face (mip 0) images for debug duplication
        byte[][] baseFaceImages = new byte[6][];

        for (int mip = 0; mip < mipCount; mip++)
        {
            int mipSize = Math.Max(1, baseSize >> mip);

            // Create a temporary 2D RT for copying each face+mip
            var rtDesc = new RenderTextureDescriptor(mipSize, mipSize, rgba32f ? RenderTextureFormat.ARGBFloat : RenderTextureFormat.ARGBHalf, 0);
            rtDesc.sRGB = false;
            rtDesc.useMipMap = false;
            rtDesc.autoGenerateMips = false;
            rtDesc.msaaSamples = 1;
            rtDesc.dimension = UnityEngine.Rendering.TextureDimension.Tex2D;

            for (int faceIndex = 0; faceIndex < 6; faceIndex++)
            {
                var tmp = RenderTexture.GetTemporary(rtDesc);
                tmp.Create();

                try
                {
                    Graphics.CopyTexture(cube, faceIndex, mip, tmp, 0, 0);

                    // If debug forcing is enabled and this is not mip 0, reuse the stored base image
                    if (forceAllMipsToBase && mip > 0 && baseFaceImages[faceIndex] != null)
                    {
                        encodedImages.Add(baseFaceImages[faceIndex]);
                        continue;
                    }

                    // Read into CPU texture
                    var texFormat = rgba32f ? TextureFormat.RGBAFloat : TextureFormat.RGBAHalf;
                    var cpu = new Texture2D(mipSize, mipSize, texFormat, false, true);

                    var prev = RenderTexture.active;
                    RenderTexture.active = tmp;
                    cpu.ReadPixels(new Rect(0, 0, mipSize, mipSize), 0, 0, false);
                    cpu.Apply(false, false);
                    RenderTexture.active = prev;

                    // Encode image using existing export helper which will RGBD-encode HDR when needed.
                    // Use flipY only for dumping purposes; Unity's ReadPixels maps correctly for our ordering.
                    var exported = UnityTools.RenderExportTexture(ref cpu, false, false, rgbaFloat: false, encodeRgbd: true);

                    try
                    {
                        byte[] png = exported.EncodeToPNG();

                        // If user requested webp, convert via existing toolchain (write temp PNG, run cwebp, read back .webp bytes)
                        if (!string.IsNullOrEmpty(imageType) && imageType.StartsWith("image/webp", StringComparison.OrdinalIgnoreCase))
                        {
                            // Enforce lossless WebP (forced).
                            string tmpPng = UnityTools.GetTempFileName("png");
                            string tmpWebp = UnityTools.GetTempFileName("webp");
                            try
                            {
                                File.WriteAllBytes(tmpPng, png);
                                // Create WEBP using existing helper (which uses lossless flags by default)
                                UnityTools.CreateCompressedTextures(tmpPng, tmpWebp, false, false, 1);

                                if (!File.Exists(tmpWebp)) throw new Exception($"Failed to generate webp: {tmpWebp}");
                                byte[] webpBytes = File.ReadAllBytes(tmpWebp);

                                // Store base face for debug duplication
                                if (mip == 0) baseFaceImages[faceIndex] = webpBytes;

                                encodedImages.Add(webpBytes);
                            }
                            finally
                            {
                                try { File.Delete(tmpPng); } catch { }
                                try { File.Delete(tmpWebp); } catch { }
                            }
                        }
                        else
                        {
                            // Default: PNG bytes

                            // Store base face for debug duplication
                            if (mip == 0) baseFaceImages[faceIndex] = png;

                            encodedImages.Add(png);
                        }
                    }
                    finally
                    {
                        UnityEngine.Object.DestroyImmediate(exported);
                        UnityEngine.Object.DestroyImmediate(cpu);
                    }
                }
                finally
                {
                    RenderTexture.ReleaseTemporary(tmp);
                }
            }
        }

        // Build JSON manifest
        var specularArray = new List<Dictionary<string, int>>(encodedImages.Count);
        int position = 0;
        foreach (var img in encodedImages)
        {
            specularArray.Add(new Dictionary<string, int>
            {
                { "length", img.Length },
                { "position", position }
            });
            position += img.Length;
        }

        // Build a simple manifest string matching EnvironmentTextureInfo v2 used by BabylonJS
        // We'll set irradiance to null and include specular.mipmaps array + lodGenerationScale
        var sb = new StringBuilder();
        sb.Append('{');
        sb.Append("\"version\":2,");
        sb.AppendFormat("\"width\":{0},", baseSize);
        sb.AppendFormat("\"imageType\":\"{0}\",", imageType);
        sb.Append("\"irradiance\":null,");
        sb.Append("\"specular\":{");
        // mipmaps
        sb.Append("\"mipmaps\":[");
        for (int i = 0; i < specularArray.Count; i++)
        {
            var d = specularArray[i];
            sb.Append('{');
            sb.AppendFormat("\"length\":{0},\"position\":{1}", d["length"], d["position"]);
            sb.Append('}');
            if (i + 1 < specularArray.Count) sb.Append(',');
        }
        sb.Append("],");
        sb.AppendFormat("\"lodGenerationScale\":{0},", lodGenerationScale.ToString(System.Globalization.CultureInfo.InvariantCulture));
        sb.AppendFormat("\"lodGenerationOffset\":{0}", lodGenerationOffset.ToString(System.Globalization.CultureInfo.InvariantCulture));
        sb.Append("}");
        sb.Append('}');

        byte[] infoBuffer = Encoding.UTF8.GetBytes(sb.ToString());

        // Now write final file: magic bytes + json + null terminator + binary payload
        using (var fs = File.Create(outEnvPath))
        using (var bw = new BinaryWriter(fs))
        {
            // Magic
            bw.Write(MagicBytes);
            // JSON header
            bw.Write(infoBuffer);
            // Null terminator
            bw.Write((byte)0x00);

            // Append image payload in the same order
            foreach (var img in encodedImages)
            {
                bw.Write(img);
            }

            bw.Flush();
        }
    }

    public static void DumpCubemapFacesToPng(RenderTexture cube, string outDir, int mipCount = -1, bool rgba32f = false, bool flipY = true)
    {
        if (cube == null) throw new ArgumentNullException(nameof(cube));
        Directory.CreateDirectory(outDir);
        int baseSize = cube.width;
        if (mipCount <= 0) mipCount = CalcMipCount(baseSize);

        for (int mip = 0; mip < mipCount; mip++)
        {
            int mipSize = Math.Max(1, baseSize >> mip);

            for (int faceIndex = 0; faceIndex < 6; faceIndex++)
            {
                var rtDesc = new RenderTextureDescriptor(mipSize, mipSize, rgba32f ? RenderTextureFormat.ARGBFloat : RenderTextureFormat.ARGBHalf, 0);
                rtDesc.sRGB = false;
                rtDesc.dimension = UnityEngine.Rendering.TextureDimension.Tex2D;
                rtDesc.useMipMap = false;
                rtDesc.autoGenerateMips = false;

                var tmp = RenderTexture.GetTemporary(rtDesc);
                tmp.Create();

                try
                {
                    Graphics.CopyTexture(cube, faceIndex, mip, tmp, 0, 0);

                    var cpu = new Texture2D(mipSize, mipSize, TextureFormat.RGBA32, false, true);
                    var prev = RenderTexture.active;
                    RenderTexture.active = tmp;
                    cpu.ReadPixels(new Rect(0, 0, mipSize, mipSize), 0, 0, false);
                    cpu.Apply(false, false);
                    RenderTexture.active = prev;

                    if (flipY)
                    {
                        cpu = FlipY(cpu);
                    }

                    byte[] png = cpu.EncodeToPNG();
                    string file = Path.Combine(outDir, $"m{mip}_{faceIndex}_{FaceNames[faceIndex]}.png");
                    File.WriteAllBytes(file, png);

                    UnityEngine.Object.DestroyImmediate(cpu);
                }
                finally
                {
                    RenderTexture.ReleaseTemporary(tmp);
                }
            }
        }
    }

    private static Texture2D FlipY(Texture2D src)
    {
        var dst = new Texture2D(src.width, src.height, src.format, false, true);
        var pixels = src.GetPixels32();
        var outPix = new Color32[pixels.Length];
        int w = src.width, h = src.height;

        for (int y = 0; y < h; y++)
        {
            int srcRow = y * w;
            int dstRow = (h - 1 - y) * w;
            Array.Copy(pixels, srcRow, outPix, dstRow, w);
        }

        dst.SetPixels32(outPix);
        dst.Apply(false, false);
        UnityEngine.Object.DestroyImmediate(src);
        return dst;
    }

    private static int CalcMipCount(int size)
    {
        int m = 1;
        while (size > 1) { size >>= 1; m++; }
        return m;
    }
}
#endif

So ExportBabylonIblEnvFromHdrCubemap is my main export function that uses my IBLPrefilterGGX shader to generate a native Unity CubeRenderTexture.

Then ENVPacker. BuildSingleCubemapEnvFromPrefilteredRenderTexture that I made from studting the IBL Texture Tools project (Thank you very much)

And … Bobs your uncle. Native BabylonJS .env filesDynamically generated from the unity scene at export. Including the global environment .env, the Unity gnerated Sperical Harmonics (not the ones generated from environment texture polynomials) and all the reflection probes thru out the scene…

NO MORE EXTERNAL TOOLS LIKE Lys, IBLBaker, CMFT or CMGEN

I Prefilter GGX style directly in unity with that Shader i showed you. Then pack the faces directly in the .env json using that ENVPacker i show you…

And i can control the lodGenerationScale and lodGenerationOffset at baking to set the Roughness used in the the Prefilter Stage…

Thanks again for the Babylon Texture Tools, I used that as a guide for my ENVPacker.

FYI… I had this working with a DDSPacker before… Just needed to finie tune the export process and replace my packer with the ENVPacker.

Work beautifully (EXCEPT THAT MIP MAP ISSUE I AM WORKING ON)

So here is aplayground using a Babylon Toolkit generated right handed Unity ball models with a daytime skybox

Sandbox:

And a playground loading the daytime .env environment

https://playground.babylonjs.com/#PF48GI#1

A smooth as butter export pipeline that generated native .env from unity reflection probes setup in the scene.

All with different lighting, but the reflections in the .exr loog good in each … at least to me. What do you think ?

EDIT: I think I still need to tweak lodGenerationScale and Offset. If i can talk to you for a bit about those properties in my prefilter process.. @sebavan

About the edit you should use the default as we do in the texture tools this would then work beautifully

I do use the defaults, this is how i prefilter, I was just wonder the ryme or reason for th 0.8 scale:

 var prefilterShader = Shader.Find("Hidden/BabylonIBL/PrefilterGGX");
        if (prefilterShader == null)
            throw new Exception("Missing shader: Hidden/BabylonIBL/PrefilterGGX. Add BabylonIblPrefilter.shader to your project.");

        var prefilterMat = new Material(prefilterShader) { hideFlags = HideFlags.HideAndDontSave };

        prefilterMat.SetTexture("_EnvCube", envForPrefilter);
        prefilterMat.SetInt("_SampleCount", samples);
        prefilterMat.SetInt("_Convention", 0); // Always use DX convention in your code

        for (int mip = 0; mip < mipCount; mip++)
        {
            float linearRoughnessAlpha;
            if (mip == 0)
            {
                linearRoughnessAlpha = 0f;
            }
            else
            {
                linearRoughnessAlpha = Mathf.Pow(2.0f, (mip - lodGenerationOffset) / lodGenerationScale) / size;
            }

            float roughness = Mathf.Sqrt(Mathf.Max(0f, linearRoughnessAlpha));
            prefilterMat.SetFloat("_Roughness", roughness);

            for (int faceIndex = 0; faceIndex < 6; faceIndex++)
            {
                prefilterMat.SetInt("_FaceIndex", faceIndex);

                Graphics.SetRenderTarget(cubeRT, mip, Faces[faceIndex]);
                GL.PushMatrix();
                GL.LoadOrtho();
                prefilterMat.SetPass(0);
                DrawFullscreenQuad();
                GL.PopMatrix();
            }
        }

The magic line for the prefilter shader:

linearRoughnessAlpha = Mathf.Pow(2.0f, (mip - lodGenerationOffset) / lodGenerationScale) / size;

I think I chose 0.8 as we thought on using 512 at first for the cube map size and it would have skipped 2 levels (1 and 2) to be sure we only use at min 4*4 pixels for more dynamicity in the rough results.

We then applied it by default everywhere :slight_smile: