From ac4e43793448196de6830d5055e31fdf6e0d40c3 Mon Sep 17 00:00:00 2001 From: hydrogendeuteride Date: Thu, 13 Nov 2025 17:46:14 +0900 Subject: [PATCH] ADD: IBL added --- Readme.md | 4 +- shaders/background_env.frag | 31 +++ shaders/deferred_lighting.frag | 41 +++- shaders/deferred_lighting_nort.frag | 40 +++- shaders/mesh.frag | 34 +++- src/core/asset_manager.cpp | 22 +++ src/core/asset_manager.h | 5 + src/core/ibl_manager.cpp | 236 ++++++++++++++++++++++- src/core/ibl_manager.h | 26 ++- src/core/vk_descriptor_manager.cpp | 1 + src/core/vk_engine.cpp | 100 ++++++++++ src/core/vk_engine.h | 5 + src/render/vk_materials.cpp | 19 +- src/render/vk_materials.h | 1 + src/render/vk_renderpass_background.cpp | 185 +++++++++++++++--- src/render/vk_renderpass_background.h | 8 + src/render/vk_renderpass_lighting.cpp | 72 ++++++- src/render/vk_renderpass_lighting.h | 4 + src/render/vk_renderpass_transparent.cpp | 53 +++++ src/render/vk_renderpass_transparent.h | 2 + src/scene/vk_scene.cpp | 2 +- 21 files changed, 837 insertions(+), 54 deletions(-) create mode 100644 shaders/background_env.frag diff --git a/Readme.md b/Readme.md index 99b5ce7..6e476e8 100644 --- a/Readme.md +++ b/Readme.md @@ -9,13 +9,13 @@ Current structure: - PBR (IBL is WIP), cascaded shadows, normal mapping (MikkTSpace tangents optional) - GLTF loading and rendering, primitive creation and rendering. - Supports texture compression(BCn, non glTF standard), LRU reload +- IBL Work-In-Progress -- [ ] IBL - [ ] TAA - [ ] Multiple light - [ ] SSR -- [ ] SSAO, bloom +- [ ] bloom - [ ] Planet Rendering ## Build prequsites diff --git a/shaders/background_env.frag b/shaders/background_env.frag new file mode 100644 index 0000000..63df525 --- /dev/null +++ b/shaders/background_env.frag @@ -0,0 +1,31 @@ +#version 450 +#extension GL_GOOGLE_include_directive : require +#include "input_structures.glsl" + +layout(location=0) in vec2 inUV; +layout(location=0) out vec4 outColor; + +// IBL specular equirect 2D (LOD 0 for background) +layout(set=3, binding=0) uniform sampler2D iblSpec2D; + +vec2 dir_to_equirect(vec3 d) +{ + d = normalize(d); + float phi = atan(d.z, d.x); + float theta = acos(clamp(d.y, -1.0, 1.0)); + return vec2(phi * (0.15915494309) + 0.5, theta * (0.31830988618)); +} + +void main() +{ + // Reconstruct world-space direction from screen UV + vec2 ndc = inUV * 2.0 - 1.0; // [-1,1] + vec4 clip = vec4(ndc, 1.0, 1.0); + vec4 vpos = inverse(sceneData.proj) * clip; + vec3 viewDir = normalize(vpos.xyz / max(vpos.w, 1e-6)); + vec3 worldDir = normalize((inverse(sceneData.view) * vec4(viewDir, 0.0)).xyz); + + vec2 uv = dir_to_equirect(worldDir); + vec3 col = textureLod(iblSpec2D, uv, 0.0).rgb; + outColor = vec4(col, 1.0); +} diff --git a/shaders/deferred_lighting.frag b/shaders/deferred_lighting.frag index e16ab0e..07cf3df 100644 --- a/shaders/deferred_lighting.frag +++ b/shaders/deferred_lighting.frag @@ -10,6 +10,33 @@ layout(set=1, binding=0) uniform sampler2D posTex; layout(set=1, binding=1) uniform sampler2D normalTex; layout(set=1, binding=2) uniform sampler2D albedoTex; layout(set=2, binding=0) uniform sampler2D shadowTex[4]; +// IBL (set=3): specular prefiltered cube, diffuse irradiance cube, BRDF LUT +layout(set=3, binding=0) uniform sampler2D iblSpec2D; +layout(set=3, binding=1) uniform sampler2D iblBRDF; +layout(std140, set=3, binding=2) uniform IBL_SH { vec4 sh[9]; } iblSH; + +vec3 sh_eval_irradiance(vec3 n) +{ + float x=n.x, y=n.y, z=n.z; + const float c0=0.2820947918; + const float c1=0.4886025119; + const float c2=1.0925484306; + const float c3=0.3153915653; + const float c4=0.5462742153; + float Y[9]; + Y[0]=c0; Y[1]=c1*y; Y[2]=c1*z; Y[3]=c1*x; Y[4]=c2*x*y; Y[5]=c2*y*z; Y[6]=c3*(3.0*z*z-1.0); Y[7]=c2*x*z; Y[8]=c4*(x*x-y*y); + vec3 r=vec3(0.0); + for (int i=0;i<9;++i) r += iblSH.sh[i].rgb * Y[i]; + return r; +} + +vec2 dir_to_equirect(vec3 d) +{ + d = normalize(d); + float phi = atan(d.z, d.x); + float theta = acos(clamp(d.y, -1.0, 1.0)); + return vec2(phi * (0.15915494309) + 0.5, theta * (0.31830988618)); +} // TLAS for ray query (optional, guarded by sceneData.rtOptions.x) #ifdef GL_EXT_ray_query layout(set=0, binding=1) uniform accelerationStructureEXT topLevelAS; @@ -338,7 +365,17 @@ void main(){ vec3 irradiance = sceneData.sunlightColor.rgb * sceneData.sunlightColor.a * NdotL * visibility; vec3 color = (kD * albedo / PI + specular) * irradiance; - color += albedo * sceneData.ambientColor.rgb; + + // Image-Based Lighting: split-sum approximation + vec3 R = reflect(-V, N); + float levels = float(textureQueryLevels(iblSpec2D)); + float lod = clamp(roughness * max(levels - 1.0, 0.0), 0.0, max(levels - 1.0, 0.0)); + vec2 uv = dir_to_equirect(R); + vec3 prefiltered = textureLod(iblSpec2D, uv, lod).rgb; + vec2 brdf = texture(iblBRDF, vec2(max(dot(N, V), 0.0), roughness)).rg; + vec3 specIBL = prefiltered * (F0 * brdf.x + brdf.y); + vec3 diffIBL = (1.0 - metallic) * albedo * sh_eval_irradiance(N); + color += diffIBL + specIBL; outColor = vec4(color, 1.0); -} \ No newline at end of file +} diff --git a/shaders/deferred_lighting_nort.frag b/shaders/deferred_lighting_nort.frag index e452a4c..a709f38 100644 --- a/shaders/deferred_lighting_nort.frag +++ b/shaders/deferred_lighting_nort.frag @@ -10,6 +10,34 @@ layout(set=1, binding=1) uniform sampler2D normalTex; layout(set=1, binding=2) uniform sampler2D albedoTex; layout(set=2, binding=0) uniform sampler2D shadowTex[4]; +// IBL (set=3): specular prefiltered cube, diffuse irradiance cube, BRDF LUT +layout(set=3, binding=0) uniform sampler2D iblSpec2D; // equirect 2D with prefiltered mips +layout(set=3, binding=1) uniform sampler2D iblBRDF; // RG LUT +layout(std140, set=3, binding=2) uniform IBL_SH { vec4 sh[9]; } iblSH; + +vec3 sh_eval_irradiance(vec3 n) +{ + float x=n.x, y=n.y, z=n.z; + const float c0=0.2820947918; + const float c1=0.4886025119; + const float c2=1.0925484306; + const float c3=0.3153915653; + const float c4=0.5462742153; + float Y[9]; + Y[0]=c0; Y[1]=c1*y; Y[2]=c1*z; Y[3]=c1*x; Y[4]=c2*x*y; Y[5]=c2*y*z; Y[6]=c3*(3.0*z*z-1.0); Y[7]=c2*x*z; Y[8]=c4*(x*x-y*y); + vec3 r=vec3(0.0); + for (int i=0;i<9;++i) r += iblSH.sh[i].rgb * Y[i]; + return r; // already convolved with Lambert in CPU bake +} + +vec2 dir_to_equirect(vec3 d) +{ + d = normalize(d); + float phi = atan(d.z, d.x); + float theta = acos(clamp(d.y, -1.0, 1.0)); + return vec2(phi * (0.15915494309) + 0.5, theta * (0.31830988618)); +} + // Tunables for shadow quality and blending // Border smoothing width in light-space NDC (0..1). Larger = wider cross-fade. const float SHADOW_BORDER_SMOOTH_NDC = 0.08; @@ -267,7 +295,17 @@ void main(){ vec3 irradiance = sceneData.sunlightColor.rgb * sceneData.sunlightColor.a * NdotL * visibility; vec3 color = (kD * albedo / PI + specular) * irradiance; - color += albedo * sceneData.ambientColor.rgb; + + // Image-Based Lighting: split-sum approximation + vec3 R = reflect(-V, N); + float levels = float(textureQueryLevels(iblSpec2D)); + float lod = clamp(roughness * max(levels - 1.0, 0.0), 0.0, max(levels - 1.0, 0.0)); + vec2 uv = dir_to_equirect(R); + vec3 prefiltered = textureLod(iblSpec2D, uv, lod).rgb; + vec2 brdf = texture(iblBRDF, vec2(max(dot(N, V), 0.0), roughness)).rg; + vec3 specIBL = prefiltered * (F0 * brdf.x + brdf.y); + vec3 diffIBL = (1.0 - metallic) * albedo * sh_eval_irradiance(N); + color += diffIBL + specIBL; outColor = vec4(color, 1.0); } diff --git a/shaders/mesh.frag b/shaders/mesh.frag index af3d701..85ccdee 100644 --- a/shaders/mesh.frag +++ b/shaders/mesh.frag @@ -13,6 +13,28 @@ layout (location = 0) out vec4 outFragColor; const float PI = 3.14159265359; +// IBL bindings (set=3): specular equirect 2D + BRDF LUT + SH UBO +layout(set=3, binding=0) uniform sampler2D iblSpec2D; +layout(set=3, binding=1) uniform sampler2D iblBRDF; +layout(std140, set=3, binding=2) uniform IBL_SH { vec4 sh[9]; } iblSH; + +vec3 sh_eval_irradiance(vec3 n) +{ + float x=n.x, y=n.y, z=n.z; + const float c0=0.2820947918; const float c1=0.4886025119; const float c2=1.0925484306; const float c3=0.3153915653; const float c4=0.5462742153; + float Y[9]; + Y[0]=c0; Y[1]=c1*y; Y[2]=c1*z; Y[3]=c1*x; Y[4]=c2*x*y; Y[5]=c2*y*z; Y[6]=c3*(3.0*z*z-1.0); Y[7]=c2*x*z; Y[8]=c4*(x*x-y*y); + vec3 r=vec3(0.0); for (int i=0;i<9;++i) r += iblSH.sh[i].rgb * Y[i]; return r; +} + +vec2 dir_to_equirect(vec3 d) +{ + d = normalize(d); + float phi = atan(d.z, d.x); + float theta = acos(clamp(d.y, -1.0, 1.0)); + return vec2(phi * (0.15915494309) + 0.5, theta * (0.31830988618)); +} + vec3 fresnelSchlick(float cosTheta, vec3 F0) { return F0 + (1.0 - F0) * pow(1.0 - cosTheta, 5.0); @@ -92,7 +114,17 @@ void main() vec3 irradiance = sceneData.sunlightColor.rgb * sceneData.sunlightColor.a * NdotL; vec3 color = (kD * albedo / PI + specular) * irradiance; - color += albedo * sceneData.ambientColor.rgb; + + // IBL: specular from equirect 2D mips; diffuse from SH + vec3 R = reflect(-V, N); + float levels = float(textureQueryLevels(iblSpec2D)); + float lod = clamp(roughness * max(levels - 1.0, 0.0), 0.0, max(levels - 1.0, 0.0)); + vec2 uv = dir_to_equirect(R); + vec3 prefiltered = textureLod(iblSpec2D, uv, lod).rgb; + vec2 brdf = texture(iblBRDF, vec2(max(dot(N, V), 0.0), roughness)).rg; + vec3 specIBL = prefiltered * (F0 * brdf.x + brdf.y); + vec3 diffIBL = (1.0 - metallic) * albedo * sh_eval_irradiance(N); + color += diffIBL + specIBL; // Alpha from baseColor texture and factor (glTF spec) float alpha = clamp(baseTex.a * materialData.colorFactors.a, 0.0, 1.0); diff --git a/src/core/asset_manager.cpp b/src/core/asset_manager.cpp index 1575e99..8e83058 100644 --- a/src/core/asset_manager.cpp +++ b/src/core/asset_manager.cpp @@ -471,6 +471,28 @@ std::shared_ptr AssetManager::createMesh(const std::string &name, return mesh; } +std::shared_ptr AssetManager::createMaterialFromConstants( + const std::string &name, + const GLTFMetallic_Roughness::MaterialConstants &constants, + MaterialPass pass) +{ + if (!_engine) return {}; + GLTFMetallic_Roughness::MaterialResources res{}; + res.colorImage = _engine->_whiteImage; + res.colorSampler = _engine->_samplerManager->defaultLinear(); + res.metalRoughImage = _engine->_whiteImage; + res.metalRoughSampler = _engine->_samplerManager->defaultLinear(); + res.normalImage = _engine->_flatNormalImage; + res.normalSampler = _engine->_samplerManager->defaultLinear(); + + AllocatedBuffer buf = createMaterialBufferWithConstants(constants); + res.dataBuffer = buf.buffer; + res.dataBufferOffset = 0; + _meshMaterialBuffers[name] = buf; + + return createMaterial(pass, res); +} + std::shared_ptr AssetManager::getMesh(const std::string &name) const { auto it = _meshCache.find(name); diff --git a/src/core/asset_manager.h b/src/core/asset_manager.h index 6a2f439..d5f4c78 100644 --- a/src/core/asset_manager.h +++ b/src/core/asset_manager.h @@ -96,6 +96,11 @@ public: bool removeMesh(const std::string &name); + // Convenience: create a PBR material from constants using engine default textures + std::shared_ptr createMaterialFromConstants(const std::string &name, + const GLTFMetallic_Roughness::MaterialConstants &constants, + MaterialPass pass = MaterialPass::MainColor); + const AssetPaths &paths() const { return _locator.paths(); } void setPaths(const AssetPaths &p) { _locator.setPaths(p); } diff --git a/src/core/ibl_manager.cpp b/src/core/ibl_manager.cpp index 32ad047..dc0186c 100644 --- a/src/core/ibl_manager.cpp +++ b/src/core/ibl_manager.cpp @@ -3,15 +3,24 @@ #include #include #include +#include +#include +#include +#include +#include + +#include "vk_device.h" bool IBLManager::load(const IBLPaths &paths) { if (_ctx == nullptr || _ctx->getResources() == nullptr) return false; - ResourceManager* rm = _ctx->getResources(); + ensureLayout(); + ResourceManager *rm = _ctx->getResources(); - // Specular cubemap + // Load specular environment: prefer cubemap; fallback to 2D equirect with mips if (!paths.specularCube.empty()) { + // Try as cubemap first ktxutil::KtxCubemap kcm{}; if (ktxutil::load_ktx2_cubemap(paths.specularCube.c_str(), kcm)) { @@ -23,9 +32,177 @@ bool IBLManager::load(const IBLPaths &paths) kcm.imgFlags ); } + else + { + ktxutil::Ktx2D k2d{}; + if (ktxutil::load_ktx2_2d(paths.specularCube.c_str(), k2d)) + { + std::vector lv; + lv.reserve(k2d.mipLevels); + for (uint32_t mip = 0; mip < k2d.mipLevels; ++mip) + { + const auto &r = k2d.copies[mip]; + lv.push_back(ResourceManager::MipLevelCopy{ + .offset = r.bufferOffset, + .length = 0, + .width = r.imageExtent.width, + .height = r.imageExtent.height, + }); + } + _spec = rm->create_image_compressed(k2d.bytes.data(), k2d.bytes.size(), k2d.fmt, lv, + VK_IMAGE_USAGE_SAMPLED_BIT); + + ktxTexture2 *ktex = nullptr; + if (ktxTexture2_CreateFromNamedFile(paths.specularCube.c_str(), KTX_TEXTURE_CREATE_LOAD_IMAGE_DATA_BIT, + &ktex) == KTX_SUCCESS && ktex) + { + const VkFormat fmt = static_cast(ktex->vkFormat); + const bool isFloat16 = fmt == VK_FORMAT_R16G16B16A16_SFLOAT; + const bool isFloat32 = fmt == VK_FORMAT_R32G32B32A32_SFLOAT; + if (!ktxTexture2_NeedsTranscoding(ktex) && (isFloat16 || isFloat32) && ktex->baseWidth == 2 * ktex-> + baseHeight) + { + const uint32_t W = ktex->baseWidth; + const uint32_t H = ktex->baseHeight; + const uint8_t *dataPtr = reinterpret_cast( + ktxTexture_GetData(ktxTexture(ktex))); + + // Compute 9 SH coefficients (irradiance) from equirect HDR + struct Vec3 + { + float x, y, z; + }; + auto half_to_float = [](uint16_t h)-> float { + uint16_t h_exp = (h & 0x7C00u) >> 10; + uint16_t h_sig = h & 0x03FFu; + uint32_t sign = (h & 0x8000u) << 16; + uint32_t f_e, f_sig; + if (h_exp == 0) + { + if (h_sig == 0) + { + f_e = 0; + f_sig = 0; + } + else + { + // subnormals + int e = -1; + uint16_t sig = h_sig; + while ((sig & 0x0400u) == 0) + { + sig <<= 1; + --e; + } + sig &= 0x03FFu; + f_e = uint32_t(127 - 15 + e) << 23; + f_sig = uint32_t(sig) << 13; + } + } + else if (h_exp == 0x1Fu) + { + f_e = 0xFFu << 23; + f_sig = uint32_t(h_sig) << 13; + } + else + { + f_e = uint32_t(h_exp - 15 + 127) << 23; + f_sig = uint32_t(h_sig) << 13; + } + uint32_t f = sign | f_e | f_sig; + float out; + std::memcpy(&out, &f, 4); + return out; + }; + + auto sample_at = [&](uint32_t x, uint32_t y)-> Vec3 { + if (isFloat32) + { + const float *px = reinterpret_cast(dataPtr) + 4ull * (y * W + x); + return {px[0], px[1], px[2]}; + } + else + { + const uint16_t *px = reinterpret_cast(dataPtr) + 4ull * (y * W + x); + return {half_to_float(px[0]), half_to_float(px[1]), half_to_float(px[2])}; + } + }; + + constexpr int L = 2; // 2nd order (9 coeffs) + const float dtheta = float(M_PI) / float(H); + const float dphi = 2.f * float(M_PI) / float(W); + // Accumulate RGB SH coeffs + std::array c{}; + for (auto &v: c) v = glm::vec3(0); + + auto sh_basis = [](const glm::vec3 &d)-> std::array { + const float x = d.x, y = d.y, z = d.z; + // Real SH, unnormalized constants + const float c0 = 0.2820947918f; + const float c1 = 0.4886025119f; + const float c2 = 1.0925484306f; + const float c3 = 0.3153915653f; + const float c4 = 0.5462742153f; + return { + c0, + c1 * y, + c1 * z, + c1 * x, + c2 * x * y, + c2 * y * z, + c3 * (3.f * z * z - 1.f), + c2 * x * z, + c4 * (x * x - y * y) + }; + }; + + for (uint32_t y = 0; y < H; ++y) + { + float theta = (y + 0.5f) * dtheta; // [0,pi] + float sinT = std::sin(theta); + for (uint32_t x = 0; x < W; ++x) + { + float phi = (x + 0.5f) * dphi; // [0,2pi] + glm::vec3 dir = glm::vec3(std::cos(phi) * sinT, std::cos(theta), std::sin(phi) * sinT); + auto Lrgb = sample_at(x, y); + glm::vec3 Lvec(Lrgb.x, Lrgb.y, Lrgb.z); + auto Y = sh_basis(dir); + float dOmega = dphi * dtheta * sinT; // solid angle per pixel + for (int i = 0; i < 9; ++i) + { + c[i] += Lvec * (Y[i] * dOmega); + } + } + } + // Convolve with Lambert kernel via per-band scale + const float A0 = float(M_PI); + const float A1 = 2.f * float(M_PI) / 3.f; + const float A2 = float(M_PI) / 4.f; + const float Aband[3] = {A0, A1, A2}; + for (int i = 0; i < 9; ++i) + { + int band = (i == 0) ? 0 : (i < 4 ? 1 : 2); + c[i] *= Aband[band]; + } + + _shBuffer = rm->create_buffer(sizeof(glm::vec4) * 9, VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT, + VMA_MEMORY_USAGE_CPU_TO_GPU); + for (int i = 0; i < 9; ++i) + { + glm::vec4 v(c[i], 0.0f); + std::memcpy(reinterpret_cast(_shBuffer.info.pMappedData) + i * sizeof(glm::vec4), + &v, sizeof(glm::vec4)); + } + vmaFlushAllocation(_ctx->getDevice()->allocator(), _shBuffer.allocation, 0, + sizeof(glm::vec4) * 9); + } + ktxTexture_Destroy(ktxTexture(ktex)); + } + } + } } - // Diffuse cubemap + // Diffuse cubemap (optional; if missing, reuse specular) if (!paths.diffuseCube.empty()) { ktxutil::KtxCubemap kcm{}; @@ -40,14 +217,17 @@ bool IBLManager::load(const IBLPaths &paths) ); } } + if (_diff.image == VK_NULL_HANDLE && _spec.image != VK_NULL_HANDLE) + { + _diff = _spec; + } - // BRDF LUT (optional) + // BRDF LUT if (!paths.brdfLut2D.empty()) { ktxutil::Ktx2D lut{}; if (ktxutil::load_ktx2_2d(paths.brdfLut2D.c_str(), lut)) { - // Build regions into ResourceManager::MipLevelCopy to reuse compressed 2D helper std::vector lv; lv.reserve(lut.mipLevels); for (uint32_t mip = 0; mip < lut.mipLevels; ++mip) @@ -55,7 +235,7 @@ bool IBLManager::load(const IBLPaths &paths) const auto &r = lut.copies[mip]; lv.push_back(ResourceManager::MipLevelCopy{ .offset = r.bufferOffset, - .length = 0, // not needed for copy scheduling + .length = 0, .width = r.imageExtent.width, .height = r.imageExtent.height, }); @@ -71,9 +251,45 @@ bool IBLManager::load(const IBLPaths &paths) void IBLManager::unload() { if (_ctx == nullptr || _ctx->getResources() == nullptr) return; - auto* rm = _ctx->getResources(); - if (_spec.image) { rm->destroy_image(_spec); _spec = {}; } - if (_diff.image) { rm->destroy_image(_diff); _diff = {}; } - if (_brdf.image) { rm->destroy_image(_brdf); _brdf = {}; } + auto *rm = _ctx->getResources(); + if (_spec.image) + { + rm->destroy_image(_spec); + _spec = {}; + } + if (_diff.image && _diff.image != _spec.image) { rm->destroy_image(_diff); } + _diff = {}; + if (_brdf.image) + { + rm->destroy_image(_brdf); + _brdf = {}; + } + if (_iblSetLayout && _ctx && _ctx->getDevice()) + { + vkDestroyDescriptorSetLayout(_ctx->getDevice()->device(), _iblSetLayout, nullptr); + _iblSetLayout = VK_NULL_HANDLE; + } + if (_shBuffer.buffer) + { + rm->destroy_buffer(_shBuffer); + _shBuffer = {}; + } } +bool IBLManager::ensureLayout() +{ + if (_iblSetLayout != VK_NULL_HANDLE) return true; + if (!_ctx || !_ctx->getDevice()) return false; + + DescriptorLayoutBuilder builder; + // binding 0: environment/specular as 2D equirect with mips + builder.add_binding(0, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER); + // binding 1: BRDF LUT 2D + builder.add_binding(1, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER); + // binding 2: SH coefficients UBO (vec4[9]) + builder.add_binding(2, VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER); + _iblSetLayout = builder.build( + _ctx->getDevice()->device(), VK_SHADER_STAGE_FRAGMENT_BIT, + nullptr, VK_DESCRIPTOR_SET_LAYOUT_CREATE_UPDATE_AFTER_BIND_POOL_BIT); + return _iblSetLayout != VK_NULL_HANDLE; +} diff --git a/src/core/ibl_manager.h b/src/core/ibl_manager.h index 85411ec..8770027 100644 --- a/src/core/ibl_manager.h +++ b/src/core/ibl_manager.h @@ -8,18 +8,17 @@ class EngineContext; struct IBLPaths { std::string specularCube; // .ktx2 (GPU-ready BC6H or R16G16B16A16) - std::string diffuseCube; // .ktx2 - std::string brdfLut2D; // .ktx2 (BC5 RG UNORM or similar) + std::string diffuseCube; // .ktx2 + std::string brdfLut2D; // .ktx2 (BC5 RG UNORM or similar) }; -// Minimal IBL asset owner with optional residency control. class IBLManager { public: - void init(EngineContext* ctx) { _ctx = ctx; } + void init(EngineContext *ctx) { _ctx = ctx; } // Load all three textures. Returns true when specular+diffuse (and optional LUT) are resident. - bool load(const IBLPaths& paths); + bool load(const IBLPaths &paths); // Release GPU memory and patch to fallbacks handled by the caller. void unload(); @@ -27,13 +26,22 @@ public: bool resident() const { return _spec.image != VK_NULL_HANDLE || _diff.image != VK_NULL_HANDLE; } AllocatedImage specular() const { return _spec; } - AllocatedImage diffuse() const { return _diff; } - AllocatedImage brdf() const { return _brdf; } + AllocatedImage diffuse() const { return _diff; } + AllocatedImage brdf() const { return _brdf; } + AllocatedBuffer shBuffer() const { return _shBuffer; } + bool hasSH() const { return _shBuffer.buffer != VK_NULL_HANDLE; } + + // Descriptor set layout used by shaders (set=3) + VkDescriptorSetLayout descriptorLayout() const { return _iblSetLayout; } + + // Build descriptor set layout without loading images (for early pipeline creation) + bool ensureLayout(); private: - EngineContext* _ctx{nullptr}; + EngineContext *_ctx{nullptr}; AllocatedImage _spec{}; AllocatedImage _diff{}; AllocatedImage _brdf{}; + VkDescriptorSetLayout _iblSetLayout = VK_NULL_HANDLE; + AllocatedBuffer _shBuffer{}; // 9*vec4 coefficients (RGB in .xyz) }; - diff --git a/src/core/vk_descriptor_manager.cpp b/src/core/vk_descriptor_manager.cpp index d71c8fb..4a0a122 100644 --- a/src/core/vk_descriptor_manager.cpp +++ b/src/core/vk_descriptor_manager.cpp @@ -24,6 +24,7 @@ void DescriptorManager::init(DeviceManager *deviceManager) _deviceManager->device(), VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT, nullptr, VK_DESCRIPTOR_SET_LAYOUT_CREATE_UPDATE_AFTER_BIND_POOL_BIT); } + } void DescriptorManager::cleanup() diff --git a/src/core/vk_engine.cpp b/src/core/vk_engine.cpp index eb31cb5..25bd5a8 100644 --- a/src/core/vk_engine.cpp +++ b/src/core/vk_engine.cpp @@ -51,6 +51,7 @@ #include "core/vk_pipeline_manager.h" #include "core/config.h" #include "core/texture_cache.h" +#include "core/ibl_manager.h" // Query a conservative streaming texture budget based on VMA-reported // device-local heap budgets. Uses ~35% of total device-local budget. @@ -116,6 +117,85 @@ namespace { ImGui::SliderFloat("Render Scale", &eng->renderScale, 0.3f, 1.f); } + // IBL test grid spawner (spheres varying metallic/roughness) + static void spawn_ibl_test(VulkanEngine *eng) + { + if (!eng || !eng->_assetManager || !eng->_sceneManager) return; + using MC = GLTFMetallic_Roughness::MaterialConstants; + + std::vector verts; std::vector inds; + primitives::buildSphere(verts, inds, 24, 24); + + const float mVals[5] = {0.0f, 0.25f, 0.5f, 0.75f, 1.0f}; + const float rVals[5] = {0.04f, 0.25f, 0.5f, 0.75f, 1.0f}; + const float spacing = 1.6f; + const glm::vec3 origin(-spacing*2.0f, 0.0f, -spacing*2.0f); + + for (int iy=0; iy<5; ++iy) + { + for (int ix=0; ix<5; ++ix) + { + MC c{}; + c.colorFactors = glm::vec4(0.82f, 0.82f, 0.82f, 1.0f); + c.metal_rough_factors = glm::vec4(mVals[ix], rVals[iy], 0.0f, 0.0f); + const std::string base = fmt::format("ibltest.m{}_r{}", ix, iy); + auto mat = eng->_assetManager->createMaterialFromConstants(base+".mat", c, MaterialPass::MainColor); + + auto mesh = eng->_assetManager->createMesh(base+".mesh", std::span(verts.data(), verts.size()), + std::span(inds.data(), inds.size()), mat); + + const glm::vec3 pos = origin + glm::vec3(ix*spacing, 0.5f, iy*spacing); + glm::mat4 M = glm::translate(glm::mat4(1.0f), pos); + eng->_sceneManager->addMeshInstance(base+".inst", mesh, M); + eng->_iblTestNames.push_back(base+".inst"); + eng->_iblTestNames.push_back(base+".mesh"); + eng->_iblTestNames.push_back(base+".mat"); + } + } + + // Chrome and glass extras + { + MC chrome{}; chrome.colorFactors = glm::vec4(0.9f,0.9f,0.9f,1.0f); chrome.metal_rough_factors = glm::vec4(1.0f, 0.06f,0,0); + auto mat = eng->_assetManager->createMaterialFromConstants("ibltest.chrome.mat", chrome, MaterialPass::MainColor); + auto mesh = eng->_assetManager->createMesh("ibltest.chrome.mesh", std::span(verts.data(), verts.size()), + std::span(inds.data(), inds.size()), mat); + glm::mat4 M = glm::translate(glm::mat4(1.0f), origin + glm::vec3(5.5f, 0.5f, 0.0f)); + eng->_sceneManager->addMeshInstance("ibltest.chrome.inst", mesh, M); + eng->_iblTestNames.insert(eng->_iblTestNames.end(), {"ibltest.chrome.inst","ibltest.chrome.mesh","ibltest.chrome.mat"}); + } + { + MC glass{}; glass.colorFactors = glm::vec4(0.9f,0.95f,1.0f,0.25f); glass.metal_rough_factors = glm::vec4(0.0f, 0.02f,0,0); + auto mat = eng->_assetManager->createMaterialFromConstants("ibltest.glass.mat", glass, MaterialPass::Transparent); + auto mesh = eng->_assetManager->createMesh("ibltest.glass.mesh", std::span(verts.data(), verts.size()), + std::span(inds.data(), inds.size()), mat); + glm::mat4 M = glm::translate(glm::mat4(1.0f), origin + glm::vec3(5.5f, 0.5f, 2.0f)); + eng->_sceneManager->addMeshInstance("ibltest.glass.inst", mesh, M); + eng->_iblTestNames.insert(eng->_iblTestNames.end(), {"ibltest.glass.inst","ibltest.glass.mesh","ibltest.glass.mat"}); + } + } + + static void clear_ibl_test(VulkanEngine *eng) + { + if (!eng || !eng->_sceneManager || !eng->_assetManager) return; + for (size_t i=0;i_iblTestNames.size(); ++i) + { + const std::string &n = eng->_iblTestNames[i]; + // Remove instances and meshes by prefix + if (n.ends_with(".inst")) eng->_sceneManager->removeMeshInstance(n); + else if (n.ends_with(".mesh")) eng->_assetManager->removeMesh(n); + } + eng->_iblTestNames.clear(); + } + + static void ui_ibl(VulkanEngine *eng) + { + if (!eng) return; + if (ImGui::Button("Spawn IBL Test Grid")) { spawn_ibl_test(eng); } + ImGui::SameLine(); + if (ImGui::Button("Clear IBL Test")) { clear_ibl_test(eng); } + ImGui::TextUnformatted("5x5 spheres: metallic across columns, roughness across rows.\nExtra: chrome + glass."); + } + // Quick stats & targets overview static void ui_overview(VulkanEngine *eng) { @@ -600,6 +680,21 @@ void VulkanEngine::init() _renderGraph->init(_context.get()); _context->renderGraph = _renderGraph.get(); + // Create IBL manager early so set=3 layout exists before pipelines are built + _iblManager = std::make_unique(); + _iblManager->init(_context.get()); + // Publish to context for passes and pipeline layout assembly + _context->ibl = _iblManager.get(); + + // Try to load default IBL assets if present + { + IBLPaths ibl{}; + // ibl.specularCube = _assetManager->assetPath("ibl/docklands.ktx2"); + // ibl.diffuseCube = _assetManager->assetPath("ibl/docklands.ktx2"); // temporary: reuse if separate diffuse not provided + ibl.brdfLut2D = _assetManager->assetPath("ibl/brdf_lut.ktx2"); + _iblManager->load(ibl); + } + init_frame_resources(); // Build material pipelines early so materials can be created @@ -1062,6 +1157,11 @@ void VulkanEngine::run() ui_pipelines(this); ImGui::EndTabItem(); } + if (ImGui::BeginTabItem("IBL")) + { + ui_ibl(this); + ImGui::EndTabItem(); + } if (ImGui::BeginTabItem("PostFX")) { ui_postfx(this); diff --git a/src/core/vk_engine.h b/src/core/vk_engine.h index f9c850e..b8706fc 100644 --- a/src/core/vk_engine.h +++ b/src/core/vk_engine.h @@ -32,6 +32,7 @@ #include "render/rg_graph.h" #include "core/vk_raytracing.h" #include "core/texture_cache.h" +#include "core/ibl_manager.h" // Number of frames-in-flight. Affects per-frame command buffers, fences, // semaphores, and transient descriptor pools in FrameResources. @@ -69,6 +70,7 @@ public: std::unique_ptr _renderGraph; std::unique_ptr _rayManager; std::unique_ptr _textureCache; + std::unique_ptr _iblManager; struct SDL_Window *_window{nullptr}; @@ -109,6 +111,9 @@ public: std::vector renderPasses; + // Debug helpers: track spawned IBL test meshes to remove them easily + std::vector _iblTestNames; + // Debug: persistent pass enable overrides (by pass name) std::unordered_map _rgPassToggles; diff --git a/src/render/vk_materials.cpp b/src/render/vk_materials.cpp index ec22ef2..d642827 100644 --- a/src/render/vk_materials.cpp +++ b/src/render/vk_materials.cpp @@ -25,9 +25,21 @@ void GLTFMetallic_Roughness::build_pipelines(VulkanEngine *engine) VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT, nullptr, VK_DESCRIPTOR_SET_LAYOUT_CREATE_UPDATE_AFTER_BIND_POOL_BIT); + // Ensure IBL layout exists; add placeholder for set=2 + // Create a persistent empty set layout placeholder (lifetime = GLTFMetallic_Roughness) + { + VkDescriptorSetLayoutCreateInfo info{ VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO }; + VK_CHECK(vkCreateDescriptorSetLayout(engine->_deviceManager->device(), &info, nullptr, &emptySetLayout)); + } + VkDescriptorSetLayout iblLayout = emptySetLayout; + if (engine->_context->ibl && engine->_context->ibl->ensureLayout()) + iblLayout = engine->_context->ibl->descriptorLayout(); + VkDescriptorSetLayout layouts[] = { - engine->_descriptorManager->gpuSceneDataLayout(), - materialLayout + engine->_descriptorManager->gpuSceneDataLayout(), // set=0 + materialLayout, // set=1 + emptySetLayout, // set=2 (unused) + iblLayout // set=3 }; // Register pipelines with the central PipelineManager @@ -87,6 +99,8 @@ void GLTFMetallic_Roughness::build_pipelines(VulkanEngine *engine) }; engine->_pipelineManager->registerGraphics("mesh.gbuffer", gbufferInfo); + // Keep emptySetLayout until clear_resources() + engine->_pipelineManager->getMaterialPipeline("mesh.opaque", opaquePipeline); engine->_pipelineManager->getMaterialPipeline("mesh.transparent", transparentPipeline); engine->_pipelineManager->getMaterialPipeline("mesh.gbuffer", gBufferPipeline); @@ -95,6 +109,7 @@ void GLTFMetallic_Roughness::build_pipelines(VulkanEngine *engine) void GLTFMetallic_Roughness::clear_resources(VkDevice device) const { vkDestroyDescriptorSetLayout(device, materialLayout, nullptr); + if (emptySetLayout) vkDestroyDescriptorSetLayout(device, emptySetLayout, nullptr); } MaterialInstance GLTFMetallic_Roughness::write_material(VkDevice device, MaterialPass pass, diff --git a/src/render/vk_materials.h b/src/render/vk_materials.h index e76d0ee..c87bc1f 100644 --- a/src/render/vk_materials.h +++ b/src/render/vk_materials.h @@ -12,6 +12,7 @@ struct GLTFMetallic_Roughness MaterialPipeline gBufferPipeline; VkDescriptorSetLayout materialLayout; + VkDescriptorSetLayout emptySetLayout = VK_NULL_HANDLE; // placeholder for set=2 struct MaterialConstants { diff --git a/src/render/vk_renderpass_background.cpp b/src/render/vk_renderpass_background.cpp index 5302b7b..eab8abf 100644 --- a/src/render/vk_renderpass_background.cpp +++ b/src/render/vk_renderpass_background.cpp @@ -7,6 +7,13 @@ #include "core/vk_pipeline_manager.h" #include "core/asset_manager.h" #include "render/rg_graph.h" +#include + +#include "frame_resources.h" +#include "ibl_manager.h" +#include "vk_descriptor_manager.h" +#include "vk_device.h" +#include "vk_sampler_manager.h" void BackgroundPass::init(EngineContext *context) { @@ -43,6 +50,63 @@ void BackgroundPass::init_background_pipelines() _backgroundEffects.push_back(gradient); _backgroundEffects.push_back(sky); + // Graphics env (cubemap) background mode + ComputeEffect env{}; env.name = "env"; + _backgroundEffects.push_back(env); + + // Prepare graphics pipeline for environment background (cubemap) + // Create an empty descriptor set layout to occupy sets 1 and 2 (shader uses set=0 and set=3) + { + VkDescriptorSetLayoutCreateInfo info{ VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO }; + info.bindingCount = 0; + info.pBindings = nullptr; + vkCreateDescriptorSetLayout(_context->getDevice()->device(), &info, nullptr, &_emptySetLayout); + } + + GraphicsPipelineCreateInfo gp{}; + gp.vertexShaderPath = _context->getAssets()->shaderPath("fullscreen.vert.spv"); + gp.fragmentShaderPath = _context->getAssets()->shaderPath("background_env.frag.spv"); + VkDescriptorSetLayout sl0 = _context->getDescriptorLayouts()->gpuSceneDataLayout(); + VkDescriptorSetLayout sl1 = _emptySetLayout; // placeholder for set=1 + VkDescriptorSetLayout sl2 = _emptySetLayout; // placeholder for set=2 + // Ensure IBL layout exists (now owned by IBLManager) + VkDescriptorSetLayout sl3 = _emptySetLayout; + if (_context->ibl && _context->ibl->ensureLayout()) + sl3 = _context->ibl->descriptorLayout(); + gp.setLayouts = { sl0, sl1, sl2, sl3 }; + gp.configure = [this](PipelineBuilder &b) { + b.set_input_topology(VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST); + b.set_polygon_mode(VK_POLYGON_MODE_FILL); + b.set_cull_mode(VK_CULL_MODE_NONE, VK_FRONT_FACE_CLOCKWISE); + b.set_multisampling_none(); + b.disable_depthtest(); + b.disable_blending(); + b.set_color_attachment_format(_context->getSwapchain()->drawImage().imageFormat); + }; + _context->pipelines->createGraphicsPipeline("background.env", gp); + + // Create fallback 1x1x6 black cube + { + const uint32_t faceCount = 6; + const uint32_t pixel = 0x00000000u; // RGBA8 black + std::vector bytes(faceCount * 4); + for (uint32_t f = 0; f < faceCount; ++f) std::memcpy(bytes.data() + f * 4, &pixel, 4); + std::vector copies; + copies.reserve(faceCount); + for (uint32_t f = 0; f < faceCount; ++f) { + VkBufferImageCopy r{}; + r.bufferOffset = f * 4; + r.imageSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; + r.imageSubresource.mipLevel = 0; + r.imageSubresource.baseArrayLayer = f; + r.imageSubresource.layerCount = 1; + r.imageExtent = {1,1,1}; + copies.push_back(r); + } + _fallbackIblCube = _context->getResources()->create_image_compressed_layers( + bytes.data(), bytes.size(), VK_FORMAT_R8G8B8A8_UNORM, 1, faceCount, copies, + VK_IMAGE_USAGE_SAMPLED_BIT, VK_IMAGE_CREATE_CUBE_COMPATIBLE_BIT); + } } void BackgroundPass::execute(VkCommandBuffer) @@ -56,33 +120,92 @@ void BackgroundPass::register_graph(RenderGraph *graph, RGImageHandle drawHandle if (!graph || !drawHandle.valid() || !_context) return; if (_backgroundEffects.empty()) return; - graph->add_pass( - "Background", - RGPassType::Compute, - [drawHandle](RGPassBuilder &builder, EngineContext *) { - builder.write(drawHandle, RGImageUsage::ComputeWrite); - }, - [this, drawHandle](VkCommandBuffer cmd, const RGPassResources &res, EngineContext *ctx) { - VkImageView drawView = res.image_view(drawHandle); - if (drawView != VK_NULL_HANDLE) - { - _context->pipelines->setComputeInstanceStorageImage("background.gradient", 0, drawView); - _context->pipelines->setComputeInstanceStorageImage("background.sky", 0, drawView); + // Route to compute or graphics depending on selected mode + const ComputeEffect &effect = _backgroundEffects[_currentEffect]; + if (std::string_view(effect.name) == std::string_view("env")) + { + graph->add_pass( + "BackgroundEnv", + RGPassType::Graphics, + [drawHandle](RGPassBuilder &builder, EngineContext *) { + builder.write_color(drawHandle); + }, + [this, drawHandle](VkCommandBuffer cmd, const RGPassResources &res, EngineContext *ctx) { + VkImageView drawView = res.image_view(drawHandle); + (void) drawView; // handled by RG + + // pipeline + layout + if (!ctx->pipelines->getGraphics("background.env", _envPipeline, _envPipelineLayout)) return; + + // Per-frame scene UBO + AllocatedBuffer ubo = ctx->getResources()->create_buffer(sizeof(GPUSceneData), + VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT, + VMA_MEMORY_USAGE_CPU_TO_GPU); + ctx->currentFrame->_deletionQueue.push_function([rm = ctx->getResources(), ubo]() { rm->destroy_buffer(ubo); }); + VmaAllocationInfo ai{}; vmaGetAllocationInfo(ctx->getDevice()->allocator(), ubo.allocation, &ai); + *reinterpret_cast(ai.pMappedData) = ctx->getSceneData(); + vmaFlushAllocation(ctx->getDevice()->allocator(), ubo.allocation, 0, sizeof(GPUSceneData)); + + VkDescriptorSet global = ctx->currentFrame->_frameDescriptors.allocate( + ctx->getDevice()->device(), ctx->getDescriptorLayouts()->gpuSceneDataLayout()); + DescriptorWriter w0; w0.write_buffer(0, ubo.buffer, sizeof(GPUSceneData), 0, VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER); + w0.update_set(ctx->getDevice()->device(), global); + + // IBL set + VkImageView specView = _fallbackIblCube.imageView; + if (ctx->ibl && ctx->ibl->specular().imageView) specView = ctx->ibl->specular().imageView; + VkDescriptorSetLayout iblLayout = (ctx->ibl ? ctx->ibl->descriptorLayout() : _emptySetLayout); + VkDescriptorSet ibl = ctx->currentFrame->_frameDescriptors.allocate( + ctx->getDevice()->device(), iblLayout); + DescriptorWriter w3; + // Bind only specular at binding 0; other bindings are unused in this shader + w3.write_image(0, specView, ctx->getSamplers()->defaultLinear(), + VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER); + w3.update_set(ctx->getDevice()->device(), ibl); + + vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, _envPipeline); + vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, _envPipelineLayout, 0, 1, &global, 0, nullptr); + vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, _envPipelineLayout, 3, 1, &ibl, 0, nullptr); + + VkExtent2D extent = ctx->getDrawExtent(); + VkViewport vp{0.f, 0.f, float(extent.width), float(extent.height), 0.f, 1.f}; + VkRect2D sc{{0,0}, extent}; + vkCmdSetViewport(cmd, 0, 1, &vp); + vkCmdSetScissor(cmd, 0, 1, &sc); + vkCmdDraw(cmd, 3, 1, 0, 0); } + ); + } + else + { + graph->add_pass( + "Background", + RGPassType::Compute, + [drawHandle](RGPassBuilder &builder, EngineContext *) { + builder.write(drawHandle, RGImageUsage::ComputeWrite); + }, + [this, drawHandle](VkCommandBuffer cmd, const RGPassResources &res, EngineContext *ctx) { + VkImageView drawView = res.image_view(drawHandle); + if (drawView != VK_NULL_HANDLE) + { + _context->pipelines->setComputeInstanceStorageImage("background.gradient", 0, drawView); + _context->pipelines->setComputeInstanceStorageImage("background.sky", 0, drawView); + } - ComputeEffect &effect = _backgroundEffects[_currentEffect]; + ComputeEffect &eff = _backgroundEffects[_currentEffect]; - ComputeDispatchInfo dispatchInfo = ComputeManager::createDispatch2D( - ctx->getDrawExtent().width, ctx->getDrawExtent().height); - dispatchInfo.pushConstants = &effect.data; - dispatchInfo.pushConstantSize = sizeof(ComputePushConstants); + ComputeDispatchInfo dispatchInfo = ComputeManager::createDispatch2D( + ctx->getDrawExtent().width, ctx->getDrawExtent().height); + dispatchInfo.pushConstants = &eff.data; + dispatchInfo.pushConstantSize = sizeof(ComputePushConstants); - const char *instanceName = (std::string_view(effect.name) == std::string_view("gradient")) - ? "background.gradient" - : "background.sky"; - ctx->pipelines->dispatchComputeInstance(cmd, instanceName, dispatchInfo); - } - ); + const char *instanceName = (std::string_view(eff.name) == std::string_view("gradient")) + ? "background.gradient" + : "background.sky"; + ctx->pipelines->dispatchComputeInstance(cmd, instanceName, dispatchInfo); + } + ); + } } void BackgroundPass::cleanup() @@ -94,6 +217,22 @@ void BackgroundPass::cleanup() _context->pipelines->destroyComputePipeline("gradient"); _context->pipelines->destroyComputePipeline("sky"); } + if (_envPipeline != VK_NULL_HANDLE || _envPipelineLayout != VK_NULL_HANDLE) + { + // Pipelines are owned by PipelineManager and destroyed there on cleanup/hot-reload + _envPipeline = VK_NULL_HANDLE; + _envPipelineLayout = VK_NULL_HANDLE; + } + if (_emptySetLayout) + { + vkDestroyDescriptorSetLayout(_context->getDevice()->device(), _emptySetLayout, nullptr); + _emptySetLayout = VK_NULL_HANDLE; + } + if (_fallbackIblCube.image) + { + _context->getResources()->destroy_image(_fallbackIblCube); + _fallbackIblCube = {}; + } fmt::print("BackgroundPass::cleanup()\n"); _backgroundEffects.clear(); } diff --git a/src/render/vk_renderpass_background.h b/src/render/vk_renderpass_background.h index b92a14a..841eed4 100644 --- a/src/render/vk_renderpass_background.h +++ b/src/render/vk_renderpass_background.h @@ -25,4 +25,12 @@ private: EngineContext *_context = nullptr; void init_background_pipelines(); + + // Graphics env background pipeline + VkPipeline _envPipeline = VK_NULL_HANDLE; + VkPipelineLayout _envPipelineLayout = VK_NULL_HANDLE; + // Empty descriptor layout used as placeholder for sets 1 and 2 + VkDescriptorSetLayout _emptySetLayout = VK_NULL_HANDLE; + // Fallback 1x1x6 black cube if IBL not loaded + AllocatedImage _fallbackIblCube{}; }; diff --git a/src/render/vk_renderpass_lighting.cpp b/src/render/vk_renderpass_lighting.cpp index 45f6569..5200d81 100644 --- a/src/render/vk_renderpass_lighting.cpp +++ b/src/render/vk_renderpass_lighting.cpp @@ -17,13 +17,22 @@ #include "vk_swapchain.h" #include "render/rg_graph.h" #include +#include +#include "ibl_manager.h" #include "vk_raytracing.h" void LightingPass::init(EngineContext *context) { _context = context; + // Placeholder empty set layout to keep array sizes stable if needed + { + VkDescriptorSetLayoutCreateInfo info{ VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO }; + info.bindingCount = 0; info.pBindings = nullptr; + vkCreateDescriptorSetLayout(_context->getDevice()->device(), &info, nullptr, &_emptySetLayout); + } + // Build descriptor layout for GBuffer inputs { DescriptorLayoutBuilder builder; @@ -59,10 +68,16 @@ void LightingPass::init(EngineContext *context) } // Build lighting pipelines (RT and non-RT) through PipelineManager + // Ensure IBL layout exists (moved to IBLManager) + VkDescriptorSetLayout iblLayout = _emptySetLayout; + if (_context->ibl && _context->ibl->ensureLayout()) + iblLayout = _context->ibl->descriptorLayout(); + VkDescriptorSetLayout layouts[] = { - _context->getDescriptorLayouts()->gpuSceneDataLayout(), - _gBufferInputDescriptorLayout, - _shadowDescriptorLayout + _context->getDescriptorLayouts()->gpuSceneDataLayout(), // set=0 + _gBufferInputDescriptorLayout, // set=1 + _shadowDescriptorLayout, // set=2 + iblLayout // set=3 }; GraphicsPipelineCreateInfo baseInfo{}; @@ -92,7 +107,23 @@ void LightingPass::init(EngineContext *context) // Pipelines are owned by PipelineManager; only destroy our local descriptor set layout vkDestroyDescriptorSetLayout(_context->getDevice()->device(), _gBufferInputDescriptorLayout, nullptr); vkDestroyDescriptorSetLayout(_context->getDevice()->device(), _shadowDescriptorLayout, nullptr); + if (_emptySetLayout) vkDestroyDescriptorSetLayout(_context->getDevice()->device(), _emptySetLayout, nullptr); }); + + // Create tiny fallback textures for IBL (grey 2D and RG LUT) + // so shaders can safely sample even when IBL isn't loaded. + { + const uint32_t pixel = 0xFF333333u; // RGBA8 grey + _fallbackIbl2D = _context->getResources()->create_image(&pixel, VkExtent3D{1,1,1}, + VK_FORMAT_R8G8B8A8_UNORM, + VK_IMAGE_USAGE_SAMPLED_BIT); + } + { + // 1x1 RG UNORM for BRDF LUT fallback + const uint16_t rg = 0x0000u; // R=0,G=0 + _fallbackBrdfLut2D = _context->getResources()->create_image( + &rg, VkExtent3D{1,1,1}, VK_FORMAT_R8G8_UNORM, VK_IMAGE_USAGE_SAMPLED_BIT); + } } void LightingPass::execute(VkCommandBuffer) @@ -223,6 +254,41 @@ void LightingPass::draw_lighting(VkCommandBuffer cmd, } vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, _pipelineLayout, 2, 1, &shadowSet, 0, nullptr); + // IBL descriptor set (set = 3). Use loaded IBL if present, otherwise fall back to black. + VkImageView specView = _fallbackIbl2D.imageView; + VkImageView brdfView = _fallbackBrdfLut2D.imageView; + VkBuffer shBuf = VK_NULL_HANDLE; VkDeviceSize shSize = sizeof(glm::vec4)*9; + if (ctxLocal->ibl) + { + if (ctxLocal->ibl->specular().imageView) specView = ctxLocal->ibl->specular().imageView; + if (ctxLocal->ibl->brdf().imageView) brdfView = ctxLocal->ibl->brdf().imageView; + if (ctxLocal->ibl->hasSH()) shBuf = ctxLocal->ibl->shBuffer().buffer; + } + // If SH missing, create a zero buffer for this frame + AllocatedBuffer shZero{}; + if (shBuf == VK_NULL_HANDLE) + { + shZero = resourceManager->create_buffer(shSize, VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT, VMA_MEMORY_USAGE_CPU_TO_GPU); + std::memset(shZero.info.pMappedData, 0, shSize); + vmaFlushAllocation(deviceManager->allocator(), shZero.allocation, 0, shSize); + shBuf = shZero.buffer; + ctxLocal->currentFrame->_deletionQueue.push_function([resourceManager, shZero]() { resourceManager->destroy_buffer(shZero); }); + } + // Allocate from IBL layout (must exist because pipeline was created with it) + VkDescriptorSetLayout iblSetLayout = (ctxLocal->ibl ? ctxLocal->ibl->descriptorLayout() : _emptySetLayout); + VkDescriptorSet iblSet = ctxLocal->currentFrame->_frameDescriptors.allocate( + deviceManager->device(), iblSetLayout); + { + DescriptorWriter w; + w.write_image(0, specView, ctxLocal->getSamplers()->defaultLinear(), + VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER); + w.write_image(1, brdfView, ctxLocal->getSamplers()->defaultLinear(), + VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER); + w.write_buffer(2, shBuf, shSize, 0, VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER); + w.update_set(deviceManager->device(), iblSet); + } + vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, _pipelineLayout, 3, 1, &iblSet, 0, nullptr); + VkViewport viewport{}; viewport.x = 0; viewport.y = 0; diff --git a/src/render/vk_renderpass_lighting.h b/src/render/vk_renderpass_lighting.h index bd96a73..c944114 100644 --- a/src/render/vk_renderpass_lighting.h +++ b/src/render/vk_renderpass_lighting.h @@ -27,9 +27,13 @@ private: VkDescriptorSetLayout _gBufferInputDescriptorLayout = VK_NULL_HANDLE; VkDescriptorSet _gBufferInputDescriptorSet = VK_NULL_HANDLE; VkDescriptorSetLayout _shadowDescriptorLayout = VK_NULL_HANDLE; // set=2 (array) + // Fallbacks if IBL is not loaded + AllocatedImage _fallbackIbl2D{}; // 1x1 black + AllocatedImage _fallbackBrdfLut2D{}; // 1x1 RG, black VkPipelineLayout _pipelineLayout = VK_NULL_HANDLE; VkPipeline _pipeline = VK_NULL_HANDLE; + VkDescriptorSetLayout _emptySetLayout = VK_NULL_HANDLE; // placeholder if IBL layout missing void draw_lighting(VkCommandBuffer cmd, EngineContext *context, diff --git a/src/render/vk_renderpass_transparent.cpp b/src/render/vk_renderpass_transparent.cpp index 87705d2..f375d84 100644 --- a/src/render/vk_renderpass_transparent.cpp +++ b/src/render/vk_renderpass_transparent.cpp @@ -3,7 +3,9 @@ #include #include +#include "ibl_manager.h" #include "texture_cache.h" +#include "vk_sampler_manager.h" #include "vk_scene.h" #include "vk_swapchain.h" #include "core/engine_context.h" @@ -16,6 +18,12 @@ void TransparentPass::init(EngineContext *context) { _context = context; + // Create fallback images + const uint32_t pixel = 0x00000000u; + _fallbackIbl2D = _context->getResources()->create_image(&pixel, VkExtent3D{1,1,1}, + VK_FORMAT_R8G8B8A8_UNORM, VK_IMAGE_USAGE_SAMPLED_BIT); + _fallbackBrdf2D = _context->getResources()->create_image(&pixel, VkExtent3D{1,1,1}, + VK_FORMAT_R8G8B8A8_UNORM, VK_IMAGE_USAGE_SAMPLED_BIT); } void TransparentPass::execute(VkCommandBuffer) @@ -94,6 +102,41 @@ void TransparentPass::draw_transparent(VkCommandBuffer cmd, writer.write_buffer(0, gpuSceneDataBuffer.buffer, sizeof(GPUSceneData), 0, VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER); writer.update_set(deviceManager->device(), globalDescriptor); + // Build IBL descriptor set (set=3) once for this pass + VkDescriptorSet iblSet = VK_NULL_HANDLE; + VkDescriptorSetLayout iblLayout = ctxLocal->ibl ? ctxLocal->ibl->descriptorLayout() : VK_NULL_HANDLE; + VkImageView specView = VK_NULL_HANDLE, brdfView = VK_NULL_HANDLE; + VkBuffer shBuf = VK_NULL_HANDLE; VkDeviceSize shSize = sizeof(glm::vec4)*9; + if (iblLayout) + { + // Fallbacks: use black if any missing + specView = (ctxLocal->ibl && ctxLocal->ibl->specular().imageView) ? ctxLocal->ibl->specular().imageView + : _fallbackIbl2D.imageView; + brdfView = (ctxLocal->ibl && ctxLocal->ibl->brdf().imageView) ? ctxLocal->ibl->brdf().imageView + : _fallbackBrdf2D.imageView; + if (ctxLocal->ibl && ctxLocal->ibl->hasSH()) shBuf = ctxLocal->ibl->shBuffer().buffer; + + // If SH missing, allocate zero UBO for this frame + AllocatedBuffer shZero{}; + if (shBuf == VK_NULL_HANDLE) + { + shZero = resourceManager->create_buffer(shSize, VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT, VMA_MEMORY_USAGE_CPU_TO_GPU); + std::memset(shZero.info.pMappedData, 0, shSize); + vmaFlushAllocation(deviceManager->allocator(), shZero.allocation, 0, shSize); + shBuf = shZero.buffer; + ctxLocal->currentFrame->_deletionQueue.push_function([resourceManager, shZero]() { resourceManager->destroy_buffer(shZero); }); + } + + iblSet = ctxLocal->currentFrame->_frameDescriptors.allocate(deviceManager->device(), iblLayout); + DescriptorWriter iw; + iw.write_image(0, specView, ctxLocal->getSamplers()->defaultLinear(), VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, + VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER); + iw.write_image(1, brdfView, ctxLocal->getSamplers()->defaultLinear(), VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, + VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER); + iw.write_buffer(2, shBuf, shSize, 0, VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER); + iw.update_set(deviceManager->device(), iblSet); + } + // Sort transparent back-to-front using camera-space depth. // We approximate object depth by transforming the mesh bounds origin. // For better results consider using per-object center or per-draw depth range. @@ -132,6 +175,11 @@ void TransparentPass::draw_transparent(VkCommandBuffer cmd, vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, r.material->pipeline->pipeline); vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, r.material->pipeline->layout, 0, 1, &globalDescriptor, 0, nullptr); + if (iblSet) + { + vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, r.material->pipeline->layout, 3, 1, + &iblSet, 0, nullptr); + } } vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, r.material->pipeline->layout, 1, 1, &r.material->materialSet, 0, nullptr); @@ -163,5 +211,10 @@ void TransparentPass::draw_transparent(VkCommandBuffer cmd, void TransparentPass::cleanup() { + if (_context && _context->getResources()) + { + if (_fallbackIbl2D.image) _context->getResources()->destroy_image(_fallbackIbl2D); + if (_fallbackBrdf2D.image) _context->getResources()->destroy_image(_fallbackBrdf2D); + } fmt::print("TransparentPass::cleanup()\n"); } diff --git a/src/render/vk_renderpass_transparent.h b/src/render/vk_renderpass_transparent.h index b93d17e..986f36a 100644 --- a/src/render/vk_renderpass_transparent.h +++ b/src/render/vk_renderpass_transparent.h @@ -24,5 +24,7 @@ private: RGImageHandle depthHandle) const; EngineContext *_context{}; + mutable AllocatedImage _fallbackIbl2D{}; // 1x1 black (created in init) + mutable AllocatedImage _fallbackBrdf2D{}; // 1x1 black RG }; diff --git a/src/scene/vk_scene.cpp b/src/scene/vk_scene.cpp index 1677a4a..9934136 100644 --- a/src/scene/vk_scene.cpp +++ b/src/scene/vk_scene.cpp @@ -23,7 +23,7 @@ void SceneManager::init(EngineContext *context) mainCamera.yaw = 0; sceneData.ambientColor = glm::vec4(0.1f, 0.1f, 0.1f, 1.0f); - sceneData.sunlightDirection = glm::vec4(-1.0f, -1.0f, -0.1f, 1.0f); + sceneData.sunlightDirection = glm::vec4(-0.2f, -1.0f, -0.3f, 1.0f); sceneData.sunlightColor = glm::vec4(1.0f, 1.0f, 1.0f, 3.0f); }