一、前言

        上一篇文章对PBR中直接光的高光反射项和漫反射项进行了整理,但显然那样的光照模型仍然不够完整,那这篇文章就要来讲一讲PBR中环境光的处理。对于环境光,计算上没有直接光这么简单,但使用的BRDF模型是一致的,所以下面会以计算处理中的不同为主,不会再去关注具体使用了什么算法模型。

二、IBL高光反射BRDF

        在Unity中,我们通常会使用IBL(基于图像的光照)来模拟环境光照,在进行IBL的BRDF计算时,我们会采用LUT(查找表)的形式,来获取BRDF的计算结果。在学习中,我曾有过疑问,为什么直接光能够计算的BRDF,换成IBL以后就需要去查表了,两者的本质区别在哪。

        首先给出结论,IBL的BRDF实时计算成本远高于直接光。我们需要知道,光线可以认为是在表面某点的球形任意方向上照射到该点,而只有该点表面法线同侧的上半球光线能够对该点产生影响,因此BRDF实际上是在计算一个半球积分,表达式如下:

L_o(p, w_o) =\int_{\Omega}f_r(p,w_i,w_o)\cdot L_i(p,w_i)(n\cdot w_i)dw_i

        其中L_o为出射光,p为光源位置,w_o为出射方向,\int_{\Omega}为半球面积分,f_r为表面反射规律的函数,w_i为入射方向,n为法向。

        直接光和IBL两者的区别在于直接光的BRDF计算是单方向点积分,由于直接光只在单个方向上有非零辐射度,因此BRDF的积分操作会直接塌缩为单点求值,塌缩后的表达式如下:

L_o =f_r(w_i,w_o)\cdot L_i(n\cdot w_i)

        而对于IBL的BRDF积分,由于IBL的本质是物体表面接收到来自整个半球所有方向的间接光线,此时在所有方向上都有非零辐射度,积分无法塌缩,必须对整个半球进行连续积分,同时考虑到表面对于IBL的间接光线采样质量,计算次数会成倍增长,同时BRDF积分是同时与入射和出射方向相关的,也就是相机移动和物体旋转都会影响积分结果,此时需要在保证性能的情况下实时计算IBL积分几乎是不现实的。

        因此通过LUT来缓存预计算的BRDF积分结果是目前最优的解决方案,但这个方案有一定局限,它要求使用微表面BRDF且菲涅尔项需要能够线性分离。这个方案来源于Epic在2013年提出的Split Sum近似,以Cook-Torrance BRDF为例,它对积分运算做了如下拆分:

        Cook-Torrance镜面BRDF完整表示为:

        f_r = \frac{D(h)G(w_i,w_o,h)F(w_o,h)}{4(n\cdot w_i)(n\cdot w_o)}

        其中 h 为半角向量。

        将菲涅尔项 F 用Schlick近似展开:

F(w_o, h) = F_0+(1-F_0)(1-(w_o\cdot h))^5

        代入原积分,我们可以将整个半球积分拆成两项:

L_o(p, w_o) =F_0\int_{\Omega}\frac{DG}{4(n\cdot w_o)(n\cdot w_i)}\cdot L_i(p,w_i)(n\cdot w_i)dw_i+(1-F_0)\int_{\Omega}\frac{DG}{4(n\cdot w_o)(n\cdot w_i)}(1-(w_o\cdot h)^5)L_i(p,w_i)(n\cdot w_i)dw_i

        通过上式,可以发现积分内部实际只与法线、出射方向和粗糙度相关,与物体本身金属度颜色无关。于是我们可以将整个计算拆成两张独立的预计算纹理,预过滤环境贴图和BRDF积分LUT图。

1. 预过滤环境贴图

        预过滤环境贴图是对环境光L及BRDF中的D、G项预卷积,生成带有mipmap的立方体环境贴图,其中每个mip级别对应一个粗糙度值,粗糙度越高,对应的mip级别越模糊,且这张图只与环境光相关,和材质视角无关。

2. BRDF积分LUT图

        积分中剩下的两个系数,我们可以将其预计算至一张2D纹理中作为积分运算的LUT,通过将(n\cdot w_o, roughness)作为UV坐标对LUT图进行采样,我们可以获得积分的两项系数输出。值得一提的是,这张积分LUT图只与你采用的BRDF相关,与其他因素完全无关,因此只要采用的是同一套BRDF,所有材质均可以使用这张LUT图。

3. IBL高光反射计算

        有了环境光贴图和积分LUT图,我们能够对IBL高光反射进行最终计算。首先我们根据粗糙度,采样预过滤环境贴图对应的mip级别,得到光照影响L_{env};通过(n\cdot w_o, roughness)对BRDF LUT图进行采样,得到积分系数(A,B);最后对高光反射颜色进行计算,算式如下:

specularIBL = L_{env}\cdot (F0\cdot A+(1-F0)\cdot B)

        由此,仅需通过纹理采样和简单运算,就可以获得整个半球的间接光照。

三、IBL漫反射BRDF

        IBL漫反射相对于高光反射计算要简单,以Lambert漫反射BRDF为例,此时f_r = \frac{albedo}{\pi},积分可以简化为如下形式:

L_{diffuse} =\frac{albedo}{\pi}\int_{\Omega}L_i(p,w_i)(n\cdot w_i)dw_i

        其中积分项就是简单根据法线对环境光辐照度进行采样,而这个采样在引擎中是由对应调用的,非常简便。

        如果需要使用上一篇文章中提到的Disney改进型BRDF进行漫反射计算,将对应的BRDF带入积分中,可以发现并没有Lambert这么简单,积分区域较为复杂,甚至可能还需要一张LUT图来帮助计算。

        鉴于环境光的漫反射对最终表现没有太大的影响,在我的实现中就偷了个小懒,在Lambert BRDF的基础上引入菲涅尔项及金属度对漫反射能量的影响来近似,我的实现表达式如下:

L_{diffuse} =\frac{albedo(1-F)(1-metallic)}{\pi}\int_{\Omega}L_i(p,w_i)(n\cdot w_i)dw_i

        至此,结合上一篇文章,我的PBR实现结构就基本完整了。

四、代码实现

1. IBL高光反射

float mipLevel = linearRoughness * (1.7 - 0.7 * linearRoughness) * UNITY_SPECCUBE_LOD_STEPS;
float4 encodeSpecularRadiance = unity_SpecCube0.SampleLevel(samplerunity_SpecCube0, reflectDir, mipLevel);
float3 specularRadiance = DecodeHDREnvironment(encodeSpecularRadiance, unity_SpecCube0_HDR);
float NdotV = max(0, dot(normalWS, V));

float2 brdfSample = float2(max(NdotV, 0), linearRoughness);
float2 brdfValue = tex2D(_BRDF_LUT, brdfSample).rg;

float3 specularIBL = specularRadiance * (F0 * brdfValue.x + (1 - F0) * brdfValue.y);

2. IBL漫反射

float3 diffuseRadiance = SampleSH(normalWS);
float3 F = Fresnel_Schlick(F0, NdotV);
float3 kD = (1 - F) * (1 - metallic);
float3 diffuseIBL = albedo * kD * diffuseRadiance / PI;

3. PBR Shader

注:在我的Shader中,BRDF相关函数全放在了另一个hlsl里,上一篇文章都贴过相关实现,这里就不重复放了,可以结合进行补全。

Shader "MyPBR/BasicPBR"
{
    Properties
    {
        _AlbedoMap("Albedo Map", 2D) = "white" {}
        _AlbedoColor("Albedo Color", Color) = (1,1,1,1)

        _MetallicMap("Metallic Map", 2D) = "white" {}
        _Metallic("Metallic", Range(0,1)) = 0

        [Toggle(_IS_SMOOTHNESS)] _IsSmoothness("Is Smoothness", float) = 0
        _RoughnessSmoothnessMap("Roughtness/Smoothness Map", 2D) = "white" {}
        _RoughnessSmoothness("Roughness/Smoothness", Range(0,1)) = 0.5

        _NormalMap("Normal Map", 2D) = "bump" {}
        _NormalIntensity("Normal Intensity", float) = 1

        _OcclusionMap("Occlusion Map", 2D) = "white" {}
        _OcclusionIntensity("Occlusion Intensity", float) = 1

        _EmissionMap("Emission Map", 2D) = "black" {}
        [HDR] _EmissionColor("Emission Color", Color) = (1,1,1,1)

        _BRDF_LUT("BRDF LUT", 2D) = "white" {}
    }
    SubShader
    {
        HLSLINCLUDE
            #pragma multi_compile _ _MAIN_LIGHT_SHADOWS _MAIN_LIGHT_SHADOWS_CASCADE _MAIN_LIGHT_SHADOWS_SCREEN
            #pragma multi_compile _ _ADDITIONAL_LIGHTS_VERTEX _ADDITIONAL_LIGHTS
            #pragma multi_compile _ _LIGHTMAP_ON
            #pragma multi_compile _ EVALUATE_SH_MIXED EVALUATE_SH_VERTEX
            #pragma multi_compile_fragment _ _ADDITIONAL_LIGHT_SHADOWS
            #pragma multi_compile_fragment _ _REFLECTION_PROBE_BLENDING
            #pragma multi_compile_fragment _ _REFLECTION_PROBE_PROJECTION
            #pragma multi_compile_fragment _ _REFLECTION_BOX_PROJECTION
            #pragma multi_compile_fragment _ _SHADOWS_SOFT _SHADOWS_SOFT_LOW _SHADOWS_SOFT_MEDIUM _SHADOWS_SOFT_HIGH

            #pragma shader_feature _ _IS_SMOOTHNESS
            #pragma shader_feature _ _IS_ANISTROPY

            #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
            #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Lighting.hlsl"
            #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/GlobalIllumination.hlsl"
            #include "../BRDF/BRDFUtils.hlsl"

            struct a2v
            { 
                float3 positionOS : POSITION;
                float2 uv : TEXCOORD0;
                float3 normalOS : NORMAL;
                float4 tangentOS : TANGENT;
            };

            struct v2f
            { 
                float4 positionCS_SV : SV_POSITION;
                float2 uv : TEXCOORD0;
                float3 positionWS : TEXCOORD1;
                float3 normalWS : NORMAL;
                float3 tangentWS : TANGENT;
                float3 bitangentWS : TEXCOORD2;
            };

            sampler2D _AlbedoMap;
            float4 _AlbedoColor;
            sampler2D _MetallicMap;
            float _Metallic;
            sampler2D _RoughnessSmoothnessMap;
            float _RoughnessSmoothness;
            sampler2D _NormalMap;
            float _NormalIntensity;
            sampler2D _OcclusionMap;
            float _OcclusionIntensity;
            sampler2D _EmissionMap;
            float4 _EmissionColor;

            sampler2D _BRDF_LUT;
        ENDHLSL

        Pass
        {
            Tags
            {
                "LightMode" = "UniversalForward"
            }

            HLSLPROGRAM
                #pragma vertex vert
                #pragma fragment frag

                v2f vert(a2v v)
                { 
                    v2f o;

                    o.positionCS_SV = TransformObjectToHClip(v.positionOS);
                    o.positionWS = TransformObjectToWorld(v.positionOS);
                    o.normalWS = TransformObjectToWorldNormal(v.normalOS);
                    o.tangentWS = normalize(TransformObjectToWorldDir(v.tangentOS.xyz));
                    o.bitangentWS = normalize(cross(o.normalWS, v.tangentOS.xyz) * v.tangentOS.w);
                    o.uv = v.uv;

                    return o;
                }

                float4 frag(v2f i) : SV_Target
                { 
                    float3x3 TBN = float3x3(normalize(i.tangentWS), normalize(i.bitangentWS), normalize(i.normalWS));
                    float3x3 tangentToWorld = transpose(TBN);

                    float3 albedo = tex2D(_AlbedoMap, i.uv).rgb * _AlbedoColor.rgb;
                    float metallic = tex2D(_MetallicMap, i.uv).r * _Metallic;
                #ifdef _IS_SMOOTHNESS
                    float smoothness = tex2D(_RoughnessSmoothnessMap, i.uv).b * _RoughnessSmoothness;
                    float perceptualRoughness = 1 - smoothness;
                #else
                    float perceptualRoughness = tex2D(_RoughnessSmoothnessMap, i.uv).b * _RoughnessSmoothness;
                #endif
                    perceptualRoughness = clamp(perceptualRoughness, 0.04, 0.98);

                    float3 normalTS = UnpackNormal(tex2D(_NormalMap, i.uv));
                    normalTS.xy *= _NormalIntensity;
                    normalTS.z = sqrt(1 - saturate(dot(normalTS.xy, normalTS.xy)));
                    float3 normalWS = normalize(mul(tangentToWorld, normalTS));

                    float3 occlusion = tex2D(_OcclusionMap, i.uv).g * _OcclusionIntensity;

                    float3 emission = tex2D(_EmissionMap, i.uv).rgb * _EmissionColor.rgb;

                    float roughness = max(HALF_MIN_SQRT, perceptualRoughness * perceptualRoughness);
                    float roughness2 = roughness * roughness;

                    float dielectricSpecular = 0.04;
                    float3 F0 = lerp(dielectricSpecular, albedo, metallic);

                    Light lights[1+8];
                    lights[0] = GetMainLight(TransformWorldToShadowCoord(i.positionWS), i.positionWS, 1);

                    int lightCount = 1 + GetAdditionalLightsCount();
                    for(int j = 1; j < lightCount; j++)
                    {
                        lights[j] = GetAdditionalLight(j - 1, i.positionWS, 1);
                    }

                    float3 diffuse = 0;
                    float3 specular = 0;
                    float3 ambient = 0;
                    float3 finalColor = 0;

                    float3 V = normalize(GetCameraPositionWS() - i.positionWS);

                    float3 reflectDir = reflect(-V, normalWS);

                    for(int k = 0; k < lightCount; k++)
                    {
                        Light light = lights[k];
                        float3 L = normalize(light.direction);
                        float3 H = normalize(V + L);

                        float NdotL = max(0,dot(normalWS, L));
                        float NdotV = max(0,dot(normalWS, V));
                        float NdotH = max(0,dot(normalWS, H));
                        float LdotH = dot(L, H);
                        float VdotH = dot(V, H);

                        float3 F = Fresnel_Schlick(F0, VdotH);

                        float NDF = NDF_GGX(NdotH, roughness);

                        float k = (1 + roughness) * (1 + roughness) / 8;
                        float G = Geometry_Smith(NdotV, NdotL, k);

                        //Cook-Torrance
                        float3 nominator = F * NDF * G;
                        float denominator = 4 * NdotV * NdotL + 0.001;
                        float3 specularBRDF = nominator / denominator;

                        float diffuseFactor = DisneyDiffuseFrostbite(NdotV, NdotL, LdotH, roughness);

                        float3 kD = (1 - F) * (1 - metallic) * diffuseFactor;
                        float3 diffuseBRDF = kD * albedo / PI;

                        float3 radiance = light.color * NdotL;

                        diffuse += diffuseBRDF * radiance;
                        specular += specularBRDF * radiance;
                    }

                    float mipLevel = perceptualRoughness * (1.7 - 0.7 * perceptualRoughness) * UNITY_SPECCUBE_LOD_STEPS;
			        float4 encodeSpecularRadiance = unity_SpecCube0.SampleLevel(samplerunity_SpecCube0, reflectDir, mipLevel);
				    float3 specularRadiance = DecodeHDREnvironment(encodeSpecularRadiance, unity_SpecCube0_HDR);

                    float NdotV = max(0, dot(normalWS, V));
                    float2 brdfSample = float2(max(NdotV, 0), perceptualRoughness);
                    float2 brdfValue = tex2D(_BRDF_LUT, brdfSample).rg;

                    float3 specularIBL = specularRadiance * (F0 * brdfValue.x + (1 - F0) * brdfValue.y);

                    float3 diffuseRadiance = SampleSH(normalWS);
                    float3 F = Fresnel_Schlick(F0, NdotV);
                    float3 kD = (1 - F) * (1 - metallic);

                    float3 diffuseIBL = albedo * kD * diffuseRadiance / PI;

                    ambient = (diffuseIBL + specularIBL) * occlusion;

                    finalColor = diffuse + specular + ambient + emission;
                    
                    return float4(finalColor, 1);
                }
            ENDHLSL
        }

        UsePass "Universal Render Pipeline/Lit/DepthOnly"
        UsePass "Universal Render Pipeline/Lit/DepthNormals"
    }

    Fallback "Universal Render Pipeline/Lit"
}

4. LUT图生成

注:我使用的BRDF后续有过修改和优化,但没有同步更新LUT生成,所以两边BRDF可能会有部分偏差,但采用的模型大差不差,最后效果上也不会有特别大的问题。

using UnityEngine;
using UnityEditor;
using System.Collections.Generic;
using System.IO;

public class BRDFLUTGenerator : EditorWindow
{
    private int lutSize = 512;
    private string savePath = "Assets/BRDF_LUT.png";
    private Texture2D previewTexture;
    private bool isGenerating = false;

    [MenuItem("Tools/PBR/Generate BRDF LUT")]
    public static void ShowWindow()
    {
        GetWindow<BRDFLUTGenerator>("BRDF LUT Generator");
    }

    void OnGUI()
    {
        GUILayout.Label("BRDF积分查找贴图生成工具", EditorStyles.boldLabel);
        lutSize = EditorGUILayout.IntSlider("LUT尺寸", lutSize, 32, 2048);
        savePath = EditorGUILayout.TextField("保存路径", savePath);

        EditorGUILayout.Space();

        if (GUILayout.Button("生成BRDF LUT") && !isGenerating)
        {
            GenerateBRDFLUT();
        }

        EditorGUILayout.Space();

        if (previewTexture != null)
        {
            GUILayout.Label("预览:");
            Rect rect = GUILayoutUtility.GetRect(256, 256);
            EditorGUI.DrawPreviewTexture(rect, previewTexture);
        }

        EditorGUILayout.HelpBox(
            "此工具将生成BRDF积分查找贴图,用于基于图像的照明(Irradiance)计算。\n" +
            "贴图的横坐标对应N·V(法线·视线),纵坐标对应粗糙度。\n" +
            "R通道存储缩放因子,G通道存储偏移因子。",
            MessageType.Info);
    }

    private void GenerateBRDFLUT()
    {
        isGenerating = true;

        try
        {
            // 创建纹理
            Texture2D lut = new Texture2D(lutSize, lutSize, TextureFormat.RGBAFloat, false);
            lut.wrapMode = TextureWrapMode.Clamp;
            lut.filterMode = FilterMode.Bilinear;

            // 计算进度
            float totalSteps = lutSize * lutSize;
            float currentStep = 0;

            // 蒙特卡洛采样数
            const int sampleCount = 1024;

            // 为每个像素计算BRDF积分
            for (int y = 0; y < lutSize; y++)
            {
                for (int x = 0; x < lutSize; x++)
                {
                    currentStep++;

                    // 报告进度
                    if (currentStep % 1000 == 0)
                    {
                        float progress = currentStep / totalSteps;
                        EditorUtility.DisplayProgressBar("生成BRDF LUT",
                            $"计算中... {(int)(progress * 100)}%", progress);
                    }

                    // 将像素坐标映射到参数空间
                    // x: N·V [0, 1]
                    // y: 粗糙度 [0, 1]
                    float NdotV = (x + 0.5f) / lutSize;
                    float roughness = (y + 0.5f) / lutSize;

                    // 计算BRDF积分
                    Vector2 brdf = IntegrateBRDF(NdotV, roughness, sampleCount);

                    // 存储到纹理
                    Color color = new Color(brdf.x, brdf.y, 0, 1);
                    lut.SetPixel(x, y, color);
                }
            }

            EditorUtility.ClearProgressBar();

            // 应用纹理更改
            lut.Apply();

            // 保存为PNG
            byte[] bytes = EncodeToPNG(lut);
            File.WriteAllBytes(savePath, bytes);

            // 导入设置
            AssetDatabase.ImportAsset(savePath);
            TextureImporter importer = AssetImporter.GetAtPath(savePath) as TextureImporter;
            if (importer != null)
            {
                importer.textureType = TextureImporterType.Default;
                importer.sRGBTexture = false; // 线性空间
                importer.wrapMode = TextureWrapMode.Clamp;
                importer.filterMode = FilterMode.Bilinear;
                importer.mipmapEnabled = false;
                importer.maxTextureSize = 512;

                TextureImporterPlatformSettings settings = importer.GetDefaultPlatformTextureSettings();
                settings.format = TextureImporterFormat.RGBA32;
                settings.maxTextureSize = 512;
                importer.SetPlatformTextureSettings(settings);

                importer.SaveAndReimport();
            }

            // 预览
            previewTexture = AssetDatabase.LoadAssetAtPath<Texture2D>(savePath);

            Debug.Log($"BRDF LUT生成完成,保存至: {savePath}");

            // 选中生成的文件
            Selection.activeObject = AssetDatabase.LoadAssetAtPath<Texture2D>(savePath);
        }
        catch (System.Exception e)
        {
            EditorUtility.ClearProgressBar();
            Debug.LogError($"生成BRDF LUT时出错: {e.Message}");
        }
        finally
        {
            isGenerating = false;
        }
    }

    // 积分BRDF函数 - 基于你的BRDF模型
    private Vector2 IntegrateBRDF(float NdotV, float roughness, int sampleCount)
    {
        Vector3 V = new Vector3(
            Mathf.Sqrt(1.0f - NdotV * NdotV), // sin(theta)
            0.0f,
            NdotV); // cos(theta)

        float A = 0.0f;
        float B = 0.0f;

        Vector3 N = new Vector3(0.0f, 0.0f, 1.0f);
        float roughnessSq = roughness * roughness;

        for (int i = 0; i < sampleCount; i++)
        {
            // 重要性采样 - 基于GGX分布
            Vector2 xi = Hammersley(i, sampleCount);
            Vector3 H = SchlickGGX_Sample(xi, N, roughnessSq);
            Vector3 L = (2 * Vector3.Dot(V, H) * H - V).normalized;

            float NdotL = Mathf.Max(L.z, 0.0f);
            float NdotH = Mathf.Max(H.z, 0.0f);
            float VdotH = Mathf.Max(Vector3.Dot(V, H), 0.0f);

            if (NdotL > 0.0f)
            {
                // 基于你的BRDF模型计算几何项
                //float k = (roughnessSq + 1.0f) * (roughnessSq + 1.0f) / 8.0f;
                float k = roughnessSq * roughnessSq / 2f;
                float G = G_SmithCustom(NdotV, NdotL, k);
                float G_Vis = (G * VdotH) / (NdotH * NdotV);

                // 菲涅尔项
                float Fc = Mathf.Pow(1.0f - VdotH, 5.0f);

                // 计算权重
                A += (1.0f - Fc) * G_Vis;
                B += Fc * G_Vis;
            }
        }

        return new Vector2(A / sampleCount, B / sampleCount);
    }

    // GGX法线分布函数
    private float D_GGX(float NdotH, float roughnessSq)
    {
        float a = NdotH * roughnessSq;
        float k = roughnessSq / (1.0f - NdotH * NdotH + a * a);
        return k * k * (1.0f / Mathf.PI);
    }

    // 重要性采样 - GGX分布
    private Vector3 SchlickGGX_Sample(Vector2 xi, Vector3 norm, float roughness)
    {
        float a = roughness * roughness;

        float phi = 2.0f * Mathf.PI * xi.x;
        float cosTheta = Mathf.Sqrt((1.0f - xi.y) / (1.0f + (a * a - 1.0f) * xi.y));
        float sinTheta = Mathf.Sqrt(1.0f - cosTheta * cosTheta);

        Vector3 H = new Vector3(
            Mathf.Cos(phi) * sinTheta,
            Mathf.Sin(phi) * sinTheta,
            cosTheta);

        Vector4 rot = Quat_ZTo(norm);

        return Quat_Rotate(rot, H);
    }

    private float G_SchlickGGXCustom(float NdotV, float k)
    {
        return NdotV / (NdotV * (1 - k) + k);
    }

    private float G_SmithCustom(float NdotV, float NdotL, float k)
    {
        float ggx1 = G_SchlickGGXCustom(NdotV, k);
        float ggx2 = G_SchlickGGXCustom(NdotL, k);
        return ggx1 * ggx2;
    }

    private Vector4 Quat_ZTo(Vector3 to)
    {
        float cosHalfTheta = Mathf.Sqrt(Mathf.Max(0, (to.z + 1) * 0.5f));
        float twoCosHalfTheta = 2.0f * cosHalfTheta;
        return new Vector4(-to.y / twoCosHalfTheta, to.x / twoCosHalfTheta, 0, cosHalfTheta);
    }

    private Vector3 Quat_Rotate(Vector4 q, Vector3 p)
    {
        Vector3 q_xyz = new Vector3(q.x, q.y, q.z);
        Vector3 temp = q.w * p + Vector3.Cross(q_xyz, p);
        Vector4 qp = new Vector4(temp.x, temp.y, temp.z, -Vector3.Dot(q_xyz, p));
        Vector4 invQ = Quat_Inverse(q);

        Vector3 invQ_xyz = new Vector3(invQ.x, invQ.y, invQ.z);
        Vector3 qp_xyz = new Vector3(qp.x, qp.y, qp.z);
        Vector3 qpInQ = qp.w * invQ_xyz + invQ.w * qp_xyz + Vector3.Cross(qp_xyz, invQ_xyz);

        return qpInQ;
    }

    private Vector4 Quat_Inverse(Vector4 q)
    {
        return new Vector4(-q.x, -q.y, -q.z, q.w);
    }

    // Hammersley序列,用于低差异采样
    private Vector2 Hammersley(int i, int N)
    {
        uint bits = (uint)i;
        bits = (bits << 16) | (bits >> 16);
        bits = ((bits & 0x55555555) << 1) | ((bits & 0xAAAAAAAA) >> 1);
        bits = ((bits & 0x33333333) << 2) | ((bits & 0xCCCCCCCC) >> 2);
        bits = ((bits & 0x0F0F0F0F) << 4) | ((bits & 0xF0F0F0F0) >> 4);
        bits = ((bits & 0x00FF00FF) << 8) | ((bits & 0xFF00FF00) >> 8);

        return new Vector2((float)i / N, (float)bits / 0xFFFFFFFF);
    }

    // 将浮点纹理编码为PNG(需要特殊处理浮点数据)
    private byte[] EncodeToPNG(Texture2D texture)
    {
        int width = texture.width;
        int height = texture.height;

        // 将浮点数据转换为字节数据
        Texture2D byteTexture = new Texture2D(width, height, TextureFormat.RGBA32, false);

        for (int y = 0; y < height; y++)
        {
            for (int x = 0; x < width; x++)
            {
                Color floatColor = texture.GetPixel(x, y);
                Color32 byteColor = new Color32(
                    (byte)(Mathf.Clamp01(floatColor.r) * 255),
                    (byte)(Mathf.Clamp01(floatColor.g) * 255),
                    (byte)(Mathf.Clamp01(floatColor.b) * 255),
                    (byte)(Mathf.Clamp01(floatColor.a) * 255)
                );
                byteTexture.SetPixel(x, y, byteColor);
            }
        }

        byteTexture.Apply();
        return byteTexture.EncodeToPNG();
    }

    // 另一种方法:使用RenderTexture和Shader生成BRDF LUT
    [MenuItem("Tools/PBR/Generate BRDF LUT via Shader")]
    public static void GenerateBRDFLUTViaShader()
    {
        int lutSize = 512;

        // 创建渲染纹理
        RenderTexture rt = new RenderTexture(lutSize, lutSize, 0,
            RenderTextureFormat.ARGBFloat, RenderTextureReadWrite.Linear);
        rt.enableRandomWrite = true;
        rt.Create();

        // 创建计算着色器或材质
        Shader shader = Shader.Find("Hidden/BRDFLUTGenerator");
        if (shader == null)
        {
            Debug.LogError("未找到Hidden/BRDFLUTGenerator着色器");
            return;
        }

        Material material = new Material(shader);

        // 渲染到纹理
        RenderTexture previous = RenderTexture.active;
        RenderTexture.active = rt;
        GL.Clear(true, true, Color.black);
        GL.PushMatrix();
        GL.LoadOrtho();

        material.SetPass(0);

        GL.Begin(GL.QUADS);
        GL.TexCoord2(0, 0);
        GL.Vertex3(0, 0, 0);
        GL.TexCoord2(1, 0);
        GL.Vertex3(1, 0, 0);
        GL.TexCoord2(1, 1);
        GL.Vertex3(1, 1, 0);
        GL.TexCoord2(0, 1);
        GL.Vertex3(0, 1, 0);
        GL.End();

        GL.PopMatrix();
        RenderTexture.active = previous;

        // 保存纹理
        Texture2D tex = new Texture2D(lutSize, lutSize, TextureFormat.RGBAFloat, false);
        RenderTexture.active = rt;
        tex.ReadPixels(new Rect(0, 0, lutSize, lutSize), 0, 0);
        tex.Apply();
        RenderTexture.active = null;

        byte[] bytes = tex.EncodeToPNG();
        string path = "Assets/BRDF_LUT_Shader.png";
        File.WriteAllBytes(path, bytes);

        // 清理
        rt.Release();
        DestroyImmediate(material);
        DestroyImmediate(tex);

        AssetDatabase.ImportAsset(path);
        Debug.Log($"BRDF LUT已生成: {path}");
    }
}

五、最终效果

        *不知道为啥图片还是放不上来,后面再补充更新吧

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐