Почему вычисленная карта яркости префильтра выглядит иначе в opengl API по сравнению с dx11?

У меня есть шейдер, который вычисляет текстуру сияния в моем рендерере DX11, и я хочу перенести его в opengl.

//this ifdef is only for the showcase, it's not in the original shader
// because  version directive must be first statement and may not be repeated
//the code is the same for both shaders thus to reduce code in this question I've added these preprocessing defines

#ifdef OPENGL

#version 460 core

#define int3 ivec3
#define uint3 uvec3
#define float2 vec2
#define float3 vec3
#define lerp(x, y, a) mix(x, y, a)
#define static //there's no static keyword in glsl

#endif

static const float g_PI = 4 * atan(1.0f);
static const float g_PI2 = 2 * g_PI;
static const float g_epsilon = 0.001f;
static const uint g_numSamples = 1024;

float3 getCubeMapTexCoord(const float2 imageSize, const float3 DTid)
{
    const float2 st = DTid.xy / imageSize;
    const float2 uv = 2.0 * float2(st.x, 1.0 - st.y) - 1.0f;

    float3 ret;
    if (DTid.z == 0)
        ret = float3(1.0, uv.y, -uv.x);
    else if (DTid.z == 1)
        ret = float3(-1.0, uv.y, uv.x);
    else if (DTid.z == 2)
        ret = float3(uv.x, 1.0, -uv.y);
    else if (DTid.z == 3)
        ret = float3(uv.x, -1.0, uv.y);
    else if (DTid.z == 4)
        ret = float3(uv.x, uv.y, 1.0);
    else if (DTid.z == 5)
        ret = float3(-uv.x, uv.y, -1.0);
    return normalize(ret);
}

void ComputeBasisVectors(const float3 N, out float3 S, out float3 T)
{
    T = cross(N, float3(0.0, 1.0, 0.0));
    T = lerp(cross(N, float3(1.0, 0.0, 0.0)), T, step(g_epsilon, dot(T, T)));

    T = normalize(T);
    S = normalize(cross(N, T));
}

float3 TangentToWorld(const float3 v, const float3 N, const float3 S, const float3 T)
{
    return S * v.x + T * v.y + N * v.z;
}

float GGX(const float roughness, const float NoH)
{
    float a2 = roughness * roughness;
    
    float denom = NoH * NoH * (a2 - 1.0) + 1.0;
    denom = g_PI * denom * denom;
    
    return roughness / denom;
}

float RadicalInverse_VdC(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-10; // / 0x100000000
}

float2 SampleHammersley(const uint i, const uint samples)
{
    const float invSamples = 1.0 / float(samples);
    return float2(i * invSamples, RadicalInverse_VdC(i));
}

float3 RandomGGX(float2 random, float roughness)
{
    float a2 = roughness * roughness;
    
    float phi = g_PI2 * random.x;
    float cosTheta = sqrt((1.0 - random.y) / (1.0 + (a2 - 1.0) * random.y));
    float sinTheta = sqrt(1.0 - cosTheta * cosTheta);
    
    float3 dir;
    dir.x = cos(phi) * sinTheta;
    dir.y = sin(phi) * sinTheta;
    dir.z = cosTheta;
    return dir;
}

#ifdef OPENGL

layout(binding = 0, rgba32f) restrict writeonly uniform imageCube o_radianceMap;
layout(binding = 0) uniform samplerCube t_skybox;

layout(binding = 0) uniform Uniforms
{
    float g_perceptualRoughness;
};

#else

RWTexture2DArray<float4> o_radianceMap : register(u0);

SamplerState t_skyboxSampler : register(s0);
TextureCube t_skybox : register(t0);

cbuffer Uniforms : register(b0)
{
    float g_perceptualRoughness;
};

#endif

#ifdef OPENGL
layout(local_size_x = 32, local_size_y = 32, local_size_z = 1) in;
void main()
#else
[numthreads(32, 32, 1)]
void CSMain(uint3 gl_GlobalInvocationID : SV_DispatchThreadID)
#endif
{
    uint3 DTid = gl_GlobalInvocationID;

    float2 radianceMapSize;
    #ifdef OPENGL
    radianceMapSize = imageSize(o_radianceMap);
    #else
    float radianceElements;
    o_radianceMap.GetDimensions(radianceMapSize.x, radianceMapSize.y, radianceElements);
    #endif

    if (DTid.x >= uint(radianceMapSize.x) || DTid.y >= uint(radianceMapSize.y)) {
        return;
    }

    float2 skyboxSize;
    #ifdef OPENGL
    skyboxSize = textureSize(t_skybox, 0);
    #else
    float skyboxElements;
    t_skybox.GetDimensions(0, skyboxSize.x, skyboxSize.y, skyboxElements);
    #endif

        const float wt = 4.0 * g_PI / (6 * skyboxSize.x * skyboxSize.y);    
    const float3 N = getCubeMapTexCoord(radianceMapSize.xy, DTid);
    const float3 V = N;
    
    float3 S, T;
    ComputeBasisVectors(N, S, T);

    float3 radiance = float3(0.0f, 0.0f, 0.0f);
    float weight = 0;

    static const float MIN_PERCEPTUAL_ROUGHNESS = 0.045f;
    const float perceptualRoughness = clamp(g_perceptualRoughness, MIN_PERCEPTUAL_ROUGHNESS, 1.0);
    const float roughness = perceptualRoughness * perceptualRoughness;
    
    for (uint i = 0; i < g_numSamples; i++) {
        const float2 u = SampleHammersley(i, g_numSamples);
        const float3 H = TangentToWorld(RandomGGX(u, roughness), N, S, T);  
        const float3 L = reflect(-V, H);
        const float NoL = dot(N, L);
        
        if (NoL > g_epsilon) {
            const float NoH = max(dot(N, H), 0.0);    
            const float pdf = GGX(roughness, NoH) * 0.25;        
            const float ws = 1.0 / (g_numSamples * pdf);        
            const float mipLevel = max(0.5 * log2(ws / wt) + 1.0, 0.0);

            #ifdef OPENGL
            radiance += textureLod(t_skybox, L, mipLevel).rgb * NoL;
            #else
            radiance += t_skybox.SampleLevel(t_skyboxSampler, L, mipLevel).rgb * NoL;
            #endif
            weight += NoL;
        }
    }
    radiance /= weight;

    #ifdef OPENGL
    imageStore(o_radianceMap, ivec3(DTid), vec4(radiance, 1.0f));
    #else
    o_radianceMap[DTid] = float4(radiance, 1.0f);
    #endif
}

Приведенный выше шейдер один и тот же в обоих приложениях, но без этих ifdef. Я использую одну и ту же кубическую карту в обоих приложениях.

Проверил g_perceptualRoughness — в обоих приложениях значения совпадают.

Проблема в том, что текстура, сгенерированная с помощью OpenGL, отличается от текстуры dx11 (и я не говорю о том, что изображение перевернуто), а у некоторых MIP-карт есть проблемы с первым и последним столбцом (я считаю, что каждая MIP-карта имеет эту проблему, но это не так). хорошо видно).

Пожалуйста, взгляните на эти изображения:

На третьем скриншоте первый столбец слева красноватый, а последний голубоватый. Я не уверен, что это не ошибка RenderDoc, потому что, когда я изменяю размер окна текстур OpenGL, проблема иногда исчезает. Однако на dx этого не происходит. Это всегда хорошо.

Код C++ также выглядит практически идентично. Начнем с dx

auto& dx = D3D::get();

D3D11_TEXTURE2D_DESC desc = {};
desc.Width = 1024;
desc.Height = 1024;
desc.MipLevels = 11;
desc.ArraySize = 6;
desc.Format = DXGI_FORMAT_R32G32B32A32_FLOAT;
desc.SampleDesc.Count = 1;
desc.Usage = D3D11_USAGE_DEFAULT;
desc.BindFlags = D3D11_BIND_UNORDERED_ACCESS;
desc.MiscFlags = D3D11_RESOURCE_MISC_TEXTURECUBE;

ComPtr<ID3D11Texture2D> prefilteredTexture;
dx.getDevice5()->CreateTexture2D(&desc, nullptr, &prefilteredTexture);

D3D11_SAMPLER_DESC samplerDesc = {
    .Filter = D3D11_FILTER_MIN_MAG_MIP_LINEAR,
    .AddressU = D3D11_TEXTURE_ADDRESS_WRAP,
    .AddressV = D3D11_TEXTURE_ADDRESS_WRAP,
    .AddressW = D3D11_TEXTURE_ADDRESS_WRAP,
    .MinLOD = 0,
    .MaxLOD = D3D11_FLOAT32_MAX
};
ComPtr<ID3D11SamplerState> sampler;
D3D::get().getDevice5()->CreateSamplerState(&samplerDesc, &sampler);

//just any skybox, only 1 mip needed, for simplicity let's say there is a skybox texture created/loaded somewhere and we can access the view using getSkyboxView() func
ComPtr<ID3D11ShaderResourceView> skyboxView = getSkyboxView();
dx.getContext4()->CSSetShaderResources(0, 1, skyboxView.GetAddressOf());

dx.getContext4()->CSSetSamplers(0, 1, sampler.GetAddressOf());

ComPtr<ID3D11ComputeShader> prefilterShader = getPrefilterShader();
dx.getContext4()->CSSetShader(computeShader.Get(), nullptr, 0);

const std::uint32_t mipLevels = 11;
const float deltaRoughness = 1.0f / glm::max(float(mipLevels - 1), 1.0f);

for (std::uint32_t level = 0, size = 1024; level < mipLevels; ++level, size /= 2) {
    const std::uint32_t numGroups = glm::max<std::uint32_t>(1, size / 32);

    D3D11_UNORDERED_ACCESS_VIEW_DESC uavDesc = {};
    uavDesc.Format = DXGI_FORMAT_R32G32B32A32_FLOAT;
    uavDesc.ViewDimension = D3D11_UAV_DIMENSION_TEXTURE2DARRAY;
    uavDesc.Texture2DArray.MipSlice = level;
    uavDesc.Texture2DArray.FirstArraySlice = 0;
    uavDesc.Texture2DArray.ArraySize = 6;

    ComPtr<ID3D11UnorderedAccessView> uav;
    D3D::get().getDevice5()->CreateUnorderedAccessView(prefilteredTexture.Get(), &uavDesc, &uav);
    dx.getContext4()->CSSetUnorderedAccessViews(0, 1, uav.GetAddressOf(), nullptr);

    //creation of the cbuffer is not important in this context
    const float roughness = level * deltaRoughness;
    roughnessBuffer.AddData(roughness);
    roughnessBuffer.Bind();

    dx.getContext4()->Dispatch(numGroups, numGroups, 6);
}

Итак, еще раз, чтобы портировать это, я просто скопировал все целиком и изменил функции dx на эквиваленты opengl, например:

glEnable(GL_TEXTURE_CUBE_MAP_SEAMLESS);

GLuint prefilteredTexture;
glCreateTextures(GL_TEXTURE_CUBE_MAP, 1, &prefilteredTexture);
glTextureStorage2D(prefilteredTexture, 11, GL_RGBA32F, 1024, 1024);

GLuint sampler;
glCreateSamplers(1, &sampler);

glSamplerParameteri(sampler, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glSamplerParameteri(sampler, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
                              
glSamplerParameteri(sampler, GL_TEXTURE_WRAP_S, GL_REPEAT);
glSamplerParameteri(sampler, GL_TEXTURE_WRAP_T, GL_REPEAT);
glSamplerParameteri(sampler, GL_TEXTURE_WRAP_R, GL_REPEAT);

//just any skybox, only 1 mip needed, for simplicity let's say there is a skybox texture created/loaded somewhere and we can access it using getSkyboxView() func
GLuint skybox = getSkybox();
glBindSampler(0, sampler);
glBindTextureUnit(0, skybox);

GLuint prefilterShader = getPrefilterShader();
glUseProgram(prefilterShader);

const std::uint32_t mipLevels = 11;
const float deltaRoughness = 1.0f / glm::max(float(mipLevels - 1), 1.0f);

for (std::uint32_t level = 0, size = 1024; level < mipLevels; ++level, size /= 2) {
    const std::uint32_t numGroups = glm::max<std::uint32_t>(1, size / 32);

    glBindImageTexture(0, prefilteredTexture, level, true, 0, GL_WRITE_ONLY, GL_RGBA32F);

    //creation of the uniform buffer is not important in this context
    const float roughness = level * deltaRoughness;
    roughnessBuffer.AddData(roughness);
    roughnessBuffer.Bind();

    glDispatchCompute(numGroups, numGroups, 6);

    glMemoryBarrier(GL_SHADER_IMAGE_ACCESS_BARRIER_BIT);
}

как я могу это отладить? Я пропустил какую-то конфигурацию? Вы заметили что-то, что мне нужно изменить? В чем проблема?

Понятия не имею, что происходит, за исключением того, что OpenGl и DirectX имеют разное направление вертикальной оси.

Pepijn Kramer 05.08.2024 11:56

Да, я знаю. Но вертикальная ось не должна быть проблемой. Я просмотрел на github специальные средства визуализации opengl, чтобы украсть их код и сравнить результат: D. Всегда одно и то же :/

Gerrard 05.08.2024 11:59
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
1
2
66
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Хорошо, я взял этот шейдер и прогнал его несколько раз:

  1. равнопрямоугольный -> преобразовать в кубическую карту 1 miplevel -> вычислить яркость

    Результат при использовании Direct3D и OpenGL такой же, как в вашем примере OpenGL.

  2. равнопрямоугольный -> преобразовать в кубическую карту 11 уровней MIP -> вычислить яркость

    Результат приятнее и больше похож на ваш DX.

  3. равнопрямоугольный -> преобразовать в кубическую карту -> сохранить в DDS как BC1_UNORM -> загрузить из файла -> вычислить яркость

    Результат при использовании Direct3D и OpenGL такой же, как в вашем примере DX.

Пожалуйста, проверьте, как у вас загружается скайбокс в обоих приложениях.

Другие вопросы по теме