У меня есть шейдер, который вычисляет текстуру сияния в моем рендерере 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);
}
как я могу это отладить? Я пропустил какую-то конфигурацию? Вы заметили что-то, что мне нужно изменить? В чем проблема?
Да, я знаю. Но вертикальная ось не должна быть проблемой. Я просмотрел на github специальные средства визуализации opengl, чтобы украсть их код и сравнить результат: D. Всегда одно и то же :/
Хорошо, я взял этот шейдер и прогнал его несколько раз:
равнопрямоугольный -> преобразовать в кубическую карту 1 miplevel -> вычислить яркость
Результат при использовании Direct3D и OpenGL такой же, как в вашем примере OpenGL.
равнопрямоугольный -> преобразовать в кубическую карту 11 уровней MIP -> вычислить яркость
Результат приятнее и больше похож на ваш DX.
равнопрямоугольный -> преобразовать в кубическую карту -> сохранить в DDS как BC1_UNORM -> загрузить из файла -> вычислить яркость
Результат при использовании Direct3D и OpenGL такой же, как в вашем примере DX.
Пожалуйста, проверьте, как у вас загружается скайбокс в обоих приложениях.
Понятия не имею, что происходит, за исключением того, что OpenGl и DirectX имеют разное направление вертикальной оси.