// [新增] 运行时 IBL 烘焙 Shader。
// 使用全屏三角形逐面写入 Cube RTV，不需要额外顶点缓冲。

#pragma pack_matrix(row_major)

static const float IBL_PI = 3.14159265359f;
static const float IBL_EPSILON = 1.0e-5f;

cbuffer CBBake : register(b0) {
    // x=roughness, y=源 Cube 尺寸, z=源 Cube 最大 mip, w=保留
    float4 gBakeFloatParams;
    // x=目标 face, y=采样数, z/w=保留
    uint4 gBakeUIntParams;
};

TextureCube<float4> gSourceEnvironment : register(t0);
SamplerState gSourceSampler : register(s0);

struct VSOutput {
    float4 positionCS : SV_POSITION;
    float2 uv : TEXCOORD0;
};

VSOutput VSFullscreen(uint vertexId : SV_VertexID) {
    VSOutput output;

    // 一个三角形覆盖整个目标视口。
    const float2 positions[3] = {
        float2(-1.0f, -1.0f),
        float2(-1.0f,  3.0f),
        float2( 3.0f, -1.0f)
    };

    const float2 position = positions[vertexId];
    output.positionCS = float4(position, 0.0f, 1.0f);
    output.uv = float2(position.x * 0.5f + 0.5f,
                       0.5f - position.y * 0.5f);
    return output;
}

// [新增] 把目标 Cube 某个面的 UV 映射为世界方向。
// face 顺序必须与 D3D11：+X、-X、+Y、-Y、+Z、-Z 完全一致。
float3 CubeDirection(uint faceIndex, float2 uv) {
    const float2 ndc = float2(uv.x * 2.0f - 1.0f,
                              1.0f - uv.y * 2.0f);

    float3 direction = float3(ndc.x, ndc.y, 1.0f);
    switch (faceIndex) {
        case 0u: direction = float3( 1.0f, ndc.y, -ndc.x); break; // +X
        case 1u: direction = float3(-1.0f, ndc.y,  ndc.x); break; // -X
        case 2u: direction = float3( ndc.x, 1.0f, -ndc.y); break; // +Y
        case 3u: direction = float3( ndc.x,-1.0f,  ndc.y); break; // -Y
        case 4u: direction = float3( ndc.x, ndc.y,  1.0f); break; // +Z
        case 5u: direction = float3(-ndc.x, ndc.y, -1.0f); break; // -Z
        default: break;
    }
    return normalize(direction);
}

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

float2 Hammersley(uint index, uint sampleCount) {
    return float2(float(index) / float(sampleCount), RadicalInverseVdC(index));
}

void BuildBasis(float3 normal, out float3 tangent, out float3 bitangent) {
    const float3 up = abs(normal.z) < 0.999f
        ? float3(0.0f, 0.0f, 1.0f)
        : float3(1.0f, 0.0f, 0.0f);
    tangent = normalize(cross(up, normal));
    bitangent = cross(normal, tangent);
}

float3 CosineSampleHemisphere(float2 xi, float3 normal) {
    const float radius = sqrt(xi.y);
    const float phi = 2.0f * IBL_PI * xi.x;

    float sinePhi;
    float cosinePhi;
    sincos(phi, sinePhi, cosinePhi);

    const float3 localDirection = float3(
        radius * cosinePhi,
        radius * sinePhi,
        sqrt(saturate(1.0f - xi.y)));

    float3 tangent;
    float3 bitangent;
    BuildBasis(normal, tangent, bitangent);
    return normalize(
        tangent * localDirection.x +
        bitangent * localDirection.y +
        normal * localDirection.z);
}

float3 ImportanceSampleGGX(float2 xi, float3 normal, float roughness) {
    const float alpha = max(roughness * roughness, 0.001f);
    const float alpha2 = alpha * alpha;

    const float phi = 2.0f * IBL_PI * xi.x;
    const float cosTheta = sqrt(
        saturate((1.0f - xi.y) /
                 max(1.0f + (alpha2 - 1.0f) * xi.y, IBL_EPSILON)));
    const float sinTheta = sqrt(saturate(1.0f - cosTheta * cosTheta));

    float sinePhi;
    float cosinePhi;
    sincos(phi, sinePhi, cosinePhi);

    const float3 halfVectorTangent = float3(
        cosinePhi * sinTheta,
        sinePhi * sinTheta,
        cosTheta);

    float3 tangent;
    float3 bitangent;
    BuildBasis(normal, tangent, bitangent);
    return normalize(
        tangent * halfVectorTangent.x +
        bitangent * halfVectorTangent.y +
        normal * halfVectorTangent.z);
}

float DistributionGGXForPdf(float nDotH, float roughness) {
    const float alpha = max(roughness * roughness, 0.001f);
    const float alpha2 = alpha * alpha;
    const float denominator = nDotH * nDotH * (alpha2 - 1.0f) + 1.0f;
    return alpha2 / max(IBL_PI * denominator * denominator, IBL_EPSILON);
}

float4 PSIrradiance(VSOutput input) : SV_TARGET {
    const float3 normal = CubeDirection(gBakeUIntParams.x, input.uv);
    const uint sampleCount = max(gBakeUIntParams.y, 1u);

    float3 irradiance = float3(0.0f, 0.0f, 0.0f);
    [loop]
    for (uint sampleIndex = 0u; sampleIndex < sampleCount; ++sampleIndex) {
        const float2 xi = Hammersley(sampleIndex, sampleCount);
        const float3 lightDirection = CosineSampleHemisphere(xi, normal);
        irradiance += gSourceEnvironment.SampleLevel(
            gSourceSampler, lightDirection, 0.0f).rgb;
    }

    // 这里直接保存 Lambert 卷积后的漫反射环境项 E/PI。
    // 因此运行时只需乘 baseColor，不再额外除以 PI。
    irradiance /= float(sampleCount);
    return float4(irradiance, 1.0f);
}

float4 PSPrefilter(VSOutput input) : SV_TARGET {
    const float3 normal = CubeDirection(gBakeUIntParams.x, input.uv);
    const float roughness = saturate(gBakeFloatParams.x);

    // roughness=0 的 mip0 直接保留原环境，避免 GGX 极窄分布的数值误差。
    if (roughness <= 0.001f) {
        return float4(gSourceEnvironment.SampleLevel(
            gSourceSampler, normal, 0.0f).rgb, 1.0f);
    }

    const uint sampleCount = max(gBakeUIntParams.y, 1u);
    const float sourceSize = max(gBakeFloatParams.y, 1.0f);
    const float sourceMaxMip = max(gBakeFloatParams.z, 0.0f);

    const float3 viewDirection = normal;
    float3 prefilteredColor = float3(0.0f, 0.0f, 0.0f);
    float totalWeight = 0.0f;

    const float texelSolidAngle =
        4.0f * IBL_PI / (6.0f * sourceSize * sourceSize);

    [loop]
    for (uint sampleIndex = 0u; sampleIndex < sampleCount; ++sampleIndex) {
        const float2 xi = Hammersley(sampleIndex, sampleCount);
        const float3 halfVector = ImportanceSampleGGX(xi, normal, roughness);
        const float3 lightDirection = normalize(
            2.0f * dot(viewDirection, halfVector) * halfVector - viewDirection);

        const float nDotL = saturate(dot(normal, lightDirection));
        if (nDotL > 0.0f) {
            const float nDotH = saturate(dot(normal, halfVector));
            const float hDotV = saturate(dot(halfVector, viewDirection));
            const float distribution = DistributionGGXForPdf(nDotH, roughness);
            const float pdf = max(
                distribution * nDotH / max(4.0f * hDotV, IBL_EPSILON),
                IBL_EPSILON);

            const float sampleSolidAngle =
                1.0f / (float(sampleCount) * pdf + IBL_EPSILON);
            const float mipLevel = clamp(
                0.5f * log2(max(sampleSolidAngle / texelSolidAngle, IBL_EPSILON)),
                0.0f, sourceMaxMip);

            prefilteredColor += gSourceEnvironment.SampleLevel(
                gSourceSampler, lightDirection, mipLevel).rgb * nDotL;
            totalWeight += nDotL;
        }
    }

    prefilteredColor /= max(totalWeight, IBL_EPSILON);
    return float4(prefilteredColor, 1.0f);
}

float GeometrySchlickGGXIBL(float nDotX, float roughness) {
    const float k = (roughness * roughness) * 0.5f;
    return nDotX / max(nDotX * (1.0f - k) + k, IBL_EPSILON);
}

float GeometrySmithIBL(float nDotV, float nDotL, float roughness) {
    return GeometrySchlickGGXIBL(nDotV, roughness) *
           GeometrySchlickGGXIBL(nDotL, roughness);
}

float2 IntegrateBrdf(float nDotV, float roughness, uint sampleCount) {
    const float3 normal = float3(0.0f, 0.0f, 1.0f);
    const float3 viewDirection = float3(
        sqrt(saturate(1.0f - nDotV * nDotV)), 0.0f, nDotV);

    float scale = 0.0f;
    float bias = 0.0f;

    [loop]
    for (uint sampleIndex = 0u; sampleIndex < sampleCount; ++sampleIndex) {
        const float2 xi = Hammersley(sampleIndex, sampleCount);
        const float3 halfVector = ImportanceSampleGGX(xi, normal, roughness);
        const float3 lightDirection = normalize(
            2.0f * dot(viewDirection, halfVector) * halfVector - viewDirection);

        const float nDotL = saturate(lightDirection.z);
        const float nDotH = saturate(halfVector.z);
        const float vDotH = saturate(dot(viewDirection, halfVector));

        if (nDotL > 0.0f) {
            const float geometry = GeometrySmithIBL(nDotV, nDotL, roughness);
            const float geometryVisibility =
                geometry * vDotH /
                max(nDotH * nDotV, IBL_EPSILON);
            const float fresnel = pow(1.0f - vDotH, 5.0f);

            scale += (1.0f - fresnel) * geometryVisibility;
            bias += fresnel * geometryVisibility;
        }
    }

    return float2(scale, bias) / float(sampleCount);
}

float2 PSBrdfLut(VSOutput input) : SV_TARGET {
    const float nDotV = clamp(input.uv.x, 0.001f, 1.0f);
    const float roughness = clamp(input.uv.y, 0.001f, 1.0f);
    const uint sampleCount = max(gBakeUIntParams.y, 1u);
    return IntegrateBrdf(nDotV, roughness, sampleCount);
}
