ADD: IBL added
This commit is contained in:
@@ -9,13 +9,13 @@ Current structure:
|
|||||||
- PBR (IBL is WIP), cascaded shadows, normal mapping (MikkTSpace tangents optional)
|
- PBR (IBL is WIP), cascaded shadows, normal mapping (MikkTSpace tangents optional)
|
||||||
- GLTF loading and rendering, primitive creation and rendering.
|
- GLTF loading and rendering, primitive creation and rendering.
|
||||||
- Supports texture compression(BCn, non glTF standard), LRU reload
|
- Supports texture compression(BCn, non glTF standard), LRU reload
|
||||||
|
- IBL
|
||||||
|
|
||||||
Work-In-Progress
|
Work-In-Progress
|
||||||
- [ ] IBL
|
|
||||||
- [ ] TAA
|
- [ ] TAA
|
||||||
- [ ] Multiple light
|
- [ ] Multiple light
|
||||||
- [ ] SSR
|
- [ ] SSR
|
||||||
- [ ] SSAO, bloom
|
- [ ] bloom
|
||||||
- [ ] Planet Rendering
|
- [ ] Planet Rendering
|
||||||
|
|
||||||
## Build prequsites
|
## Build prequsites
|
||||||
|
|||||||
31
shaders/background_env.frag
Normal file
31
shaders/background_env.frag
Normal file
@@ -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);
|
||||||
|
}
|
||||||
@@ -10,6 +10,33 @@ layout(set=1, binding=0) uniform sampler2D posTex;
|
|||||||
layout(set=1, binding=1) uniform sampler2D normalTex;
|
layout(set=1, binding=1) uniform sampler2D normalTex;
|
||||||
layout(set=1, binding=2) uniform sampler2D albedoTex;
|
layout(set=1, binding=2) uniform sampler2D albedoTex;
|
||||||
layout(set=2, binding=0) uniform sampler2D shadowTex[4];
|
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)
|
// TLAS for ray query (optional, guarded by sceneData.rtOptions.x)
|
||||||
#ifdef GL_EXT_ray_query
|
#ifdef GL_EXT_ray_query
|
||||||
layout(set=0, binding=1) uniform accelerationStructureEXT topLevelAS;
|
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 irradiance = sceneData.sunlightColor.rgb * sceneData.sunlightColor.a * NdotL * visibility;
|
||||||
|
|
||||||
vec3 color = (kD * albedo / PI + specular) * irradiance;
|
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);
|
outColor = vec4(color, 1.0);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,34 @@ layout(set=1, binding=1) uniform sampler2D normalTex;
|
|||||||
layout(set=1, binding=2) uniform sampler2D albedoTex;
|
layout(set=1, binding=2) uniform sampler2D albedoTex;
|
||||||
layout(set=2, binding=0) uniform sampler2D shadowTex[4];
|
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
|
// Tunables for shadow quality and blending
|
||||||
// Border smoothing width in light-space NDC (0..1). Larger = wider cross-fade.
|
// Border smoothing width in light-space NDC (0..1). Larger = wider cross-fade.
|
||||||
const float SHADOW_BORDER_SMOOTH_NDC = 0.08;
|
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 irradiance = sceneData.sunlightColor.rgb * sceneData.sunlightColor.a * NdotL * visibility;
|
||||||
|
|
||||||
vec3 color = (kD * albedo / PI + specular) * irradiance;
|
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);
|
outColor = vec4(color, 1.0);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,28 @@ layout (location = 0) out vec4 outFragColor;
|
|||||||
|
|
||||||
const float PI = 3.14159265359;
|
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)
|
vec3 fresnelSchlick(float cosTheta, vec3 F0)
|
||||||
{
|
{
|
||||||
return F0 + (1.0 - F0) * pow(1.0 - cosTheta, 5.0);
|
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 irradiance = sceneData.sunlightColor.rgb * sceneData.sunlightColor.a * NdotL;
|
||||||
|
|
||||||
vec3 color = (kD * albedo / PI + specular) * irradiance;
|
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)
|
// Alpha from baseColor texture and factor (glTF spec)
|
||||||
float alpha = clamp(baseTex.a * materialData.colorFactors.a, 0.0, 1.0);
|
float alpha = clamp(baseTex.a * materialData.colorFactors.a, 0.0, 1.0);
|
||||||
|
|||||||
@@ -471,6 +471,28 @@ std::shared_ptr<MeshAsset> AssetManager::createMesh(const std::string &name,
|
|||||||
return mesh;
|
return mesh;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::shared_ptr<GLTFMaterial> 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<MeshAsset> AssetManager::getMesh(const std::string &name) const
|
std::shared_ptr<MeshAsset> AssetManager::getMesh(const std::string &name) const
|
||||||
{
|
{
|
||||||
auto it = _meshCache.find(name);
|
auto it = _meshCache.find(name);
|
||||||
|
|||||||
@@ -96,6 +96,11 @@ public:
|
|||||||
|
|
||||||
bool removeMesh(const std::string &name);
|
bool removeMesh(const std::string &name);
|
||||||
|
|
||||||
|
// Convenience: create a PBR material from constants using engine default textures
|
||||||
|
std::shared_ptr<GLTFMaterial> createMaterialFromConstants(const std::string &name,
|
||||||
|
const GLTFMetallic_Roughness::MaterialConstants &constants,
|
||||||
|
MaterialPass pass = MaterialPass::MainColor);
|
||||||
|
|
||||||
const AssetPaths &paths() const { return _locator.paths(); }
|
const AssetPaths &paths() const { return _locator.paths(); }
|
||||||
void setPaths(const AssetPaths &p) { _locator.setPaths(p); }
|
void setPaths(const AssetPaths &p) { _locator.setPaths(p); }
|
||||||
|
|
||||||
|
|||||||
@@ -3,15 +3,24 @@
|
|||||||
#include <core/vk_resource.h>
|
#include <core/vk_resource.h>
|
||||||
#include <core/ktx_loader.h>
|
#include <core/ktx_loader.h>
|
||||||
#include <core/vk_sampler_manager.h>
|
#include <core/vk_sampler_manager.h>
|
||||||
|
#include <core/vk_descriptors.h>
|
||||||
|
#include <cmath>
|
||||||
|
#include <algorithm>
|
||||||
|
#include <ktx.h>
|
||||||
|
#include <SDL_stdinc.h>
|
||||||
|
|
||||||
|
#include "vk_device.h"
|
||||||
|
|
||||||
bool IBLManager::load(const IBLPaths &paths)
|
bool IBLManager::load(const IBLPaths &paths)
|
||||||
{
|
{
|
||||||
if (_ctx == nullptr || _ctx->getResources() == nullptr) return false;
|
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())
|
if (!paths.specularCube.empty())
|
||||||
{
|
{
|
||||||
|
// Try as cubemap first
|
||||||
ktxutil::KtxCubemap kcm{};
|
ktxutil::KtxCubemap kcm{};
|
||||||
if (ktxutil::load_ktx2_cubemap(paths.specularCube.c_str(), kcm))
|
if (ktxutil::load_ktx2_cubemap(paths.specularCube.c_str(), kcm))
|
||||||
{
|
{
|
||||||
@@ -23,9 +32,177 @@ bool IBLManager::load(const IBLPaths &paths)
|
|||||||
kcm.imgFlags
|
kcm.imgFlags
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
ktxutil::Ktx2D k2d{};
|
||||||
|
if (ktxutil::load_ktx2_2d(paths.specularCube.c_str(), k2d))
|
||||||
|
{
|
||||||
|
std::vector<ResourceManager::MipLevelCopy> 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<VkFormat>(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<const uint8_t *>(
|
||||||
|
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<const float *>(dataPtr) + 4ull * (y * W + x);
|
||||||
|
return {px[0], px[1], px[2]};
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
const uint16_t *px = reinterpret_cast<const uint16_t *>(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<glm::vec3, 9> c{};
|
||||||
|
for (auto &v: c) v = glm::vec3(0);
|
||||||
|
|
||||||
|
auto sh_basis = [](const glm::vec3 &d)-> std::array<float, 9> {
|
||||||
|
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<char *>(_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())
|
if (!paths.diffuseCube.empty())
|
||||||
{
|
{
|
||||||
ktxutil::KtxCubemap kcm{};
|
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())
|
if (!paths.brdfLut2D.empty())
|
||||||
{
|
{
|
||||||
ktxutil::Ktx2D lut{};
|
ktxutil::Ktx2D lut{};
|
||||||
if (ktxutil::load_ktx2_2d(paths.brdfLut2D.c_str(), lut))
|
if (ktxutil::load_ktx2_2d(paths.brdfLut2D.c_str(), lut))
|
||||||
{
|
{
|
||||||
// Build regions into ResourceManager::MipLevelCopy to reuse compressed 2D helper
|
|
||||||
std::vector<ResourceManager::MipLevelCopy> lv;
|
std::vector<ResourceManager::MipLevelCopy> lv;
|
||||||
lv.reserve(lut.mipLevels);
|
lv.reserve(lut.mipLevels);
|
||||||
for (uint32_t mip = 0; mip < lut.mipLevels; ++mip)
|
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];
|
const auto &r = lut.copies[mip];
|
||||||
lv.push_back(ResourceManager::MipLevelCopy{
|
lv.push_back(ResourceManager::MipLevelCopy{
|
||||||
.offset = r.bufferOffset,
|
.offset = r.bufferOffset,
|
||||||
.length = 0, // not needed for copy scheduling
|
.length = 0,
|
||||||
.width = r.imageExtent.width,
|
.width = r.imageExtent.width,
|
||||||
.height = r.imageExtent.height,
|
.height = r.imageExtent.height,
|
||||||
});
|
});
|
||||||
@@ -71,9 +251,45 @@ bool IBLManager::load(const IBLPaths &paths)
|
|||||||
void IBLManager::unload()
|
void IBLManager::unload()
|
||||||
{
|
{
|
||||||
if (_ctx == nullptr || _ctx->getResources() == nullptr) return;
|
if (_ctx == nullptr || _ctx->getResources() == nullptr) return;
|
||||||
auto* rm = _ctx->getResources();
|
auto *rm = _ctx->getResources();
|
||||||
if (_spec.image) { rm->destroy_image(_spec); _spec = {}; }
|
if (_spec.image)
|
||||||
if (_diff.image) { rm->destroy_image(_diff); _diff = {}; }
|
{
|
||||||
if (_brdf.image) { rm->destroy_image(_brdf); _brdf = {}; }
|
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;
|
||||||
|
}
|
||||||
|
|||||||
@@ -8,18 +8,17 @@ class EngineContext;
|
|||||||
struct IBLPaths
|
struct IBLPaths
|
||||||
{
|
{
|
||||||
std::string specularCube; // .ktx2 (GPU-ready BC6H or R16G16B16A16)
|
std::string specularCube; // .ktx2 (GPU-ready BC6H or R16G16B16A16)
|
||||||
std::string diffuseCube; // .ktx2
|
std::string diffuseCube; // .ktx2
|
||||||
std::string brdfLut2D; // .ktx2 (BC5 RG UNORM or similar)
|
std::string brdfLut2D; // .ktx2 (BC5 RG UNORM or similar)
|
||||||
};
|
};
|
||||||
|
|
||||||
// Minimal IBL asset owner with optional residency control.
|
|
||||||
class IBLManager
|
class IBLManager
|
||||||
{
|
{
|
||||||
public:
|
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.
|
// 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.
|
// Release GPU memory and patch to fallbacks handled by the caller.
|
||||||
void unload();
|
void unload();
|
||||||
@@ -27,13 +26,22 @@ public:
|
|||||||
bool resident() const { return _spec.image != VK_NULL_HANDLE || _diff.image != VK_NULL_HANDLE; }
|
bool resident() const { return _spec.image != VK_NULL_HANDLE || _diff.image != VK_NULL_HANDLE; }
|
||||||
|
|
||||||
AllocatedImage specular() const { return _spec; }
|
AllocatedImage specular() const { return _spec; }
|
||||||
AllocatedImage diffuse() const { return _diff; }
|
AllocatedImage diffuse() const { return _diff; }
|
||||||
AllocatedImage brdf() const { return _brdf; }
|
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:
|
private:
|
||||||
EngineContext* _ctx{nullptr};
|
EngineContext *_ctx{nullptr};
|
||||||
AllocatedImage _spec{};
|
AllocatedImage _spec{};
|
||||||
AllocatedImage _diff{};
|
AllocatedImage _diff{};
|
||||||
AllocatedImage _brdf{};
|
AllocatedImage _brdf{};
|
||||||
|
VkDescriptorSetLayout _iblSetLayout = VK_NULL_HANDLE;
|
||||||
|
AllocatedBuffer _shBuffer{}; // 9*vec4 coefficients (RGB in .xyz)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ void DescriptorManager::init(DeviceManager *deviceManager)
|
|||||||
_deviceManager->device(), VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT,
|
_deviceManager->device(), VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT,
|
||||||
nullptr, VK_DESCRIPTOR_SET_LAYOUT_CREATE_UPDATE_AFTER_BIND_POOL_BIT);
|
nullptr, VK_DESCRIPTOR_SET_LAYOUT_CREATE_UPDATE_AFTER_BIND_POOL_BIT);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void DescriptorManager::cleanup()
|
void DescriptorManager::cleanup()
|
||||||
|
|||||||
@@ -51,6 +51,7 @@
|
|||||||
#include "core/vk_pipeline_manager.h"
|
#include "core/vk_pipeline_manager.h"
|
||||||
#include "core/config.h"
|
#include "core/config.h"
|
||||||
#include "core/texture_cache.h"
|
#include "core/texture_cache.h"
|
||||||
|
#include "core/ibl_manager.h"
|
||||||
|
|
||||||
// Query a conservative streaming texture budget based on VMA-reported
|
// Query a conservative streaming texture budget based on VMA-reported
|
||||||
// device-local heap budgets. Uses ~35% of total device-local budget.
|
// 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);
|
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<Vertex> verts; std::vector<uint32_t> 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<Vertex>(verts.data(), verts.size()),
|
||||||
|
std::span<uint32_t>(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<Vertex>(verts.data(), verts.size()),
|
||||||
|
std::span<uint32_t>(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<Vertex>(verts.data(), verts.size()),
|
||||||
|
std::span<uint32_t>(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<eng->_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
|
// Quick stats & targets overview
|
||||||
static void ui_overview(VulkanEngine *eng)
|
static void ui_overview(VulkanEngine *eng)
|
||||||
{
|
{
|
||||||
@@ -600,6 +680,21 @@ void VulkanEngine::init()
|
|||||||
_renderGraph->init(_context.get());
|
_renderGraph->init(_context.get());
|
||||||
_context->renderGraph = _renderGraph.get();
|
_context->renderGraph = _renderGraph.get();
|
||||||
|
|
||||||
|
// Create IBL manager early so set=3 layout exists before pipelines are built
|
||||||
|
_iblManager = std::make_unique<IBLManager>();
|
||||||
|
_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();
|
init_frame_resources();
|
||||||
|
|
||||||
// Build material pipelines early so materials can be created
|
// Build material pipelines early so materials can be created
|
||||||
@@ -1062,6 +1157,11 @@ void VulkanEngine::run()
|
|||||||
ui_pipelines(this);
|
ui_pipelines(this);
|
||||||
ImGui::EndTabItem();
|
ImGui::EndTabItem();
|
||||||
}
|
}
|
||||||
|
if (ImGui::BeginTabItem("IBL"))
|
||||||
|
{
|
||||||
|
ui_ibl(this);
|
||||||
|
ImGui::EndTabItem();
|
||||||
|
}
|
||||||
if (ImGui::BeginTabItem("PostFX"))
|
if (ImGui::BeginTabItem("PostFX"))
|
||||||
{
|
{
|
||||||
ui_postfx(this);
|
ui_postfx(this);
|
||||||
|
|||||||
@@ -32,6 +32,7 @@
|
|||||||
#include "render/rg_graph.h"
|
#include "render/rg_graph.h"
|
||||||
#include "core/vk_raytracing.h"
|
#include "core/vk_raytracing.h"
|
||||||
#include "core/texture_cache.h"
|
#include "core/texture_cache.h"
|
||||||
|
#include "core/ibl_manager.h"
|
||||||
|
|
||||||
// Number of frames-in-flight. Affects per-frame command buffers, fences,
|
// Number of frames-in-flight. Affects per-frame command buffers, fences,
|
||||||
// semaphores, and transient descriptor pools in FrameResources.
|
// semaphores, and transient descriptor pools in FrameResources.
|
||||||
@@ -69,6 +70,7 @@ public:
|
|||||||
std::unique_ptr<RenderGraph> _renderGraph;
|
std::unique_ptr<RenderGraph> _renderGraph;
|
||||||
std::unique_ptr<RayTracingManager> _rayManager;
|
std::unique_ptr<RayTracingManager> _rayManager;
|
||||||
std::unique_ptr<TextureCache> _textureCache;
|
std::unique_ptr<TextureCache> _textureCache;
|
||||||
|
std::unique_ptr<IBLManager> _iblManager;
|
||||||
|
|
||||||
struct SDL_Window *_window{nullptr};
|
struct SDL_Window *_window{nullptr};
|
||||||
|
|
||||||
@@ -109,6 +111,9 @@ public:
|
|||||||
|
|
||||||
std::vector<RenderPass> renderPasses;
|
std::vector<RenderPass> renderPasses;
|
||||||
|
|
||||||
|
// Debug helpers: track spawned IBL test meshes to remove them easily
|
||||||
|
std::vector<std::string> _iblTestNames;
|
||||||
|
|
||||||
// Debug: persistent pass enable overrides (by pass name)
|
// Debug: persistent pass enable overrides (by pass name)
|
||||||
std::unordered_map<std::string, bool> _rgPassToggles;
|
std::unordered_map<std::string, bool> _rgPassToggles;
|
||||||
|
|
||||||
|
|||||||
@@ -25,9 +25,21 @@ void GLTFMetallic_Roughness::build_pipelines(VulkanEngine *engine)
|
|||||||
VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT,
|
VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT,
|
||||||
nullptr, VK_DESCRIPTOR_SET_LAYOUT_CREATE_UPDATE_AFTER_BIND_POOL_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[] = {
|
VkDescriptorSetLayout layouts[] = {
|
||||||
engine->_descriptorManager->gpuSceneDataLayout(),
|
engine->_descriptorManager->gpuSceneDataLayout(), // set=0
|
||||||
materialLayout
|
materialLayout, // set=1
|
||||||
|
emptySetLayout, // set=2 (unused)
|
||||||
|
iblLayout // set=3
|
||||||
};
|
};
|
||||||
|
|
||||||
// Register pipelines with the central PipelineManager
|
// Register pipelines with the central PipelineManager
|
||||||
@@ -87,6 +99,8 @@ void GLTFMetallic_Roughness::build_pipelines(VulkanEngine *engine)
|
|||||||
};
|
};
|
||||||
engine->_pipelineManager->registerGraphics("mesh.gbuffer", gbufferInfo);
|
engine->_pipelineManager->registerGraphics("mesh.gbuffer", gbufferInfo);
|
||||||
|
|
||||||
|
// Keep emptySetLayout until clear_resources()
|
||||||
|
|
||||||
engine->_pipelineManager->getMaterialPipeline("mesh.opaque", opaquePipeline);
|
engine->_pipelineManager->getMaterialPipeline("mesh.opaque", opaquePipeline);
|
||||||
engine->_pipelineManager->getMaterialPipeline("mesh.transparent", transparentPipeline);
|
engine->_pipelineManager->getMaterialPipeline("mesh.transparent", transparentPipeline);
|
||||||
engine->_pipelineManager->getMaterialPipeline("mesh.gbuffer", gBufferPipeline);
|
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
|
void GLTFMetallic_Roughness::clear_resources(VkDevice device) const
|
||||||
{
|
{
|
||||||
vkDestroyDescriptorSetLayout(device, materialLayout, nullptr);
|
vkDestroyDescriptorSetLayout(device, materialLayout, nullptr);
|
||||||
|
if (emptySetLayout) vkDestroyDescriptorSetLayout(device, emptySetLayout, nullptr);
|
||||||
}
|
}
|
||||||
|
|
||||||
MaterialInstance GLTFMetallic_Roughness::write_material(VkDevice device, MaterialPass pass,
|
MaterialInstance GLTFMetallic_Roughness::write_material(VkDevice device, MaterialPass pass,
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ struct GLTFMetallic_Roughness
|
|||||||
MaterialPipeline gBufferPipeline;
|
MaterialPipeline gBufferPipeline;
|
||||||
|
|
||||||
VkDescriptorSetLayout materialLayout;
|
VkDescriptorSetLayout materialLayout;
|
||||||
|
VkDescriptorSetLayout emptySetLayout = VK_NULL_HANDLE; // placeholder for set=2
|
||||||
|
|
||||||
struct MaterialConstants
|
struct MaterialConstants
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -7,6 +7,13 @@
|
|||||||
#include "core/vk_pipeline_manager.h"
|
#include "core/vk_pipeline_manager.h"
|
||||||
#include "core/asset_manager.h"
|
#include "core/asset_manager.h"
|
||||||
#include "render/rg_graph.h"
|
#include "render/rg_graph.h"
|
||||||
|
#include <cstring>
|
||||||
|
|
||||||
|
#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)
|
void BackgroundPass::init(EngineContext *context)
|
||||||
{
|
{
|
||||||
@@ -43,6 +50,63 @@ void BackgroundPass::init_background_pipelines()
|
|||||||
|
|
||||||
_backgroundEffects.push_back(gradient);
|
_backgroundEffects.push_back(gradient);
|
||||||
_backgroundEffects.push_back(sky);
|
_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<uint8_t> bytes(faceCount * 4);
|
||||||
|
for (uint32_t f = 0; f < faceCount; ++f) std::memcpy(bytes.data() + f * 4, &pixel, 4);
|
||||||
|
std::vector<VkBufferImageCopy> 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)
|
void BackgroundPass::execute(VkCommandBuffer)
|
||||||
@@ -56,33 +120,92 @@ void BackgroundPass::register_graph(RenderGraph *graph, RGImageHandle drawHandle
|
|||||||
if (!graph || !drawHandle.valid() || !_context) return;
|
if (!graph || !drawHandle.valid() || !_context) return;
|
||||||
if (_backgroundEffects.empty()) return;
|
if (_backgroundEffects.empty()) return;
|
||||||
|
|
||||||
graph->add_pass(
|
// Route to compute or graphics depending on selected mode
|
||||||
"Background",
|
const ComputeEffect &effect = _backgroundEffects[_currentEffect];
|
||||||
RGPassType::Compute,
|
if (std::string_view(effect.name) == std::string_view("env"))
|
||||||
[drawHandle](RGPassBuilder &builder, EngineContext *) {
|
{
|
||||||
builder.write(drawHandle, RGImageUsage::ComputeWrite);
|
graph->add_pass(
|
||||||
},
|
"BackgroundEnv",
|
||||||
[this, drawHandle](VkCommandBuffer cmd, const RGPassResources &res, EngineContext *ctx) {
|
RGPassType::Graphics,
|
||||||
VkImageView drawView = res.image_view(drawHandle);
|
[drawHandle](RGPassBuilder &builder, EngineContext *) {
|
||||||
if (drawView != VK_NULL_HANDLE)
|
builder.write_color(drawHandle);
|
||||||
{
|
},
|
||||||
_context->pipelines->setComputeInstanceStorageImage("background.gradient", 0, drawView);
|
[this, drawHandle](VkCommandBuffer cmd, const RGPassResources &res, EngineContext *ctx) {
|
||||||
_context->pipelines->setComputeInstanceStorageImage("background.sky", 0, drawView);
|
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<GPUSceneData*>(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(
|
ComputeDispatchInfo dispatchInfo = ComputeManager::createDispatch2D(
|
||||||
ctx->getDrawExtent().width, ctx->getDrawExtent().height);
|
ctx->getDrawExtent().width, ctx->getDrawExtent().height);
|
||||||
dispatchInfo.pushConstants = &effect.data;
|
dispatchInfo.pushConstants = &eff.data;
|
||||||
dispatchInfo.pushConstantSize = sizeof(ComputePushConstants);
|
dispatchInfo.pushConstantSize = sizeof(ComputePushConstants);
|
||||||
|
|
||||||
const char *instanceName = (std::string_view(effect.name) == std::string_view("gradient"))
|
const char *instanceName = (std::string_view(eff.name) == std::string_view("gradient"))
|
||||||
? "background.gradient"
|
? "background.gradient"
|
||||||
: "background.sky";
|
: "background.sky";
|
||||||
ctx->pipelines->dispatchComputeInstance(cmd, instanceName, dispatchInfo);
|
ctx->pipelines->dispatchComputeInstance(cmd, instanceName, dispatchInfo);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void BackgroundPass::cleanup()
|
void BackgroundPass::cleanup()
|
||||||
@@ -94,6 +217,22 @@ void BackgroundPass::cleanup()
|
|||||||
_context->pipelines->destroyComputePipeline("gradient");
|
_context->pipelines->destroyComputePipeline("gradient");
|
||||||
_context->pipelines->destroyComputePipeline("sky");
|
_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");
|
fmt::print("BackgroundPass::cleanup()\n");
|
||||||
_backgroundEffects.clear();
|
_backgroundEffects.clear();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,4 +25,12 @@ private:
|
|||||||
EngineContext *_context = nullptr;
|
EngineContext *_context = nullptr;
|
||||||
|
|
||||||
void init_background_pipelines();
|
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{};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -17,13 +17,22 @@
|
|||||||
#include "vk_swapchain.h"
|
#include "vk_swapchain.h"
|
||||||
#include "render/rg_graph.h"
|
#include "render/rg_graph.h"
|
||||||
#include <array>
|
#include <array>
|
||||||
|
#include <cstring>
|
||||||
|
|
||||||
|
#include "ibl_manager.h"
|
||||||
#include "vk_raytracing.h"
|
#include "vk_raytracing.h"
|
||||||
|
|
||||||
void LightingPass::init(EngineContext *context)
|
void LightingPass::init(EngineContext *context)
|
||||||
{
|
{
|
||||||
_context = 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
|
// Build descriptor layout for GBuffer inputs
|
||||||
{
|
{
|
||||||
DescriptorLayoutBuilder builder;
|
DescriptorLayoutBuilder builder;
|
||||||
@@ -59,10 +68,16 @@ void LightingPass::init(EngineContext *context)
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Build lighting pipelines (RT and non-RT) through PipelineManager
|
// 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[] = {
|
VkDescriptorSetLayout layouts[] = {
|
||||||
_context->getDescriptorLayouts()->gpuSceneDataLayout(),
|
_context->getDescriptorLayouts()->gpuSceneDataLayout(), // set=0
|
||||||
_gBufferInputDescriptorLayout,
|
_gBufferInputDescriptorLayout, // set=1
|
||||||
_shadowDescriptorLayout
|
_shadowDescriptorLayout, // set=2
|
||||||
|
iblLayout // set=3
|
||||||
};
|
};
|
||||||
|
|
||||||
GraphicsPipelineCreateInfo baseInfo{};
|
GraphicsPipelineCreateInfo baseInfo{};
|
||||||
@@ -92,7 +107,23 @@ void LightingPass::init(EngineContext *context)
|
|||||||
// Pipelines are owned by PipelineManager; only destroy our local descriptor set layout
|
// Pipelines are owned by PipelineManager; only destroy our local descriptor set layout
|
||||||
vkDestroyDescriptorSetLayout(_context->getDevice()->device(), _gBufferInputDescriptorLayout, nullptr);
|
vkDestroyDescriptorSetLayout(_context->getDevice()->device(), _gBufferInputDescriptorLayout, nullptr);
|
||||||
vkDestroyDescriptorSetLayout(_context->getDevice()->device(), _shadowDescriptorLayout, 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)
|
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);
|
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{};
|
VkViewport viewport{};
|
||||||
viewport.x = 0;
|
viewport.x = 0;
|
||||||
viewport.y = 0;
|
viewport.y = 0;
|
||||||
|
|||||||
@@ -27,9 +27,13 @@ private:
|
|||||||
VkDescriptorSetLayout _gBufferInputDescriptorLayout = VK_NULL_HANDLE;
|
VkDescriptorSetLayout _gBufferInputDescriptorLayout = VK_NULL_HANDLE;
|
||||||
VkDescriptorSet _gBufferInputDescriptorSet = VK_NULL_HANDLE;
|
VkDescriptorSet _gBufferInputDescriptorSet = VK_NULL_HANDLE;
|
||||||
VkDescriptorSetLayout _shadowDescriptorLayout = VK_NULL_HANDLE; // set=2 (array)
|
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;
|
VkPipelineLayout _pipelineLayout = VK_NULL_HANDLE;
|
||||||
VkPipeline _pipeline = VK_NULL_HANDLE;
|
VkPipeline _pipeline = VK_NULL_HANDLE;
|
||||||
|
VkDescriptorSetLayout _emptySetLayout = VK_NULL_HANDLE; // placeholder if IBL layout missing
|
||||||
|
|
||||||
void draw_lighting(VkCommandBuffer cmd,
|
void draw_lighting(VkCommandBuffer cmd,
|
||||||
EngineContext *context,
|
EngineContext *context,
|
||||||
|
|||||||
@@ -3,7 +3,9 @@
|
|||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <unordered_set>
|
#include <unordered_set>
|
||||||
|
|
||||||
|
#include "ibl_manager.h"
|
||||||
#include "texture_cache.h"
|
#include "texture_cache.h"
|
||||||
|
#include "vk_sampler_manager.h"
|
||||||
#include "vk_scene.h"
|
#include "vk_scene.h"
|
||||||
#include "vk_swapchain.h"
|
#include "vk_swapchain.h"
|
||||||
#include "core/engine_context.h"
|
#include "core/engine_context.h"
|
||||||
@@ -16,6 +18,12 @@
|
|||||||
void TransparentPass::init(EngineContext *context)
|
void TransparentPass::init(EngineContext *context)
|
||||||
{
|
{
|
||||||
_context = 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)
|
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.write_buffer(0, gpuSceneDataBuffer.buffer, sizeof(GPUSceneData), 0, VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER);
|
||||||
writer.update_set(deviceManager->device(), globalDescriptor);
|
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.
|
// Sort transparent back-to-front using camera-space depth.
|
||||||
// We approximate object depth by transforming the mesh bounds origin.
|
// We approximate object depth by transforming the mesh bounds origin.
|
||||||
// For better results consider using per-object center or per-draw depth range.
|
// 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);
|
vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, r.material->pipeline->pipeline);
|
||||||
vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, r.material->pipeline->layout, 0, 1,
|
vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, r.material->pipeline->layout, 0, 1,
|
||||||
&globalDescriptor, 0, nullptr);
|
&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,
|
vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, r.material->pipeline->layout, 1, 1,
|
||||||
&r.material->materialSet, 0, nullptr);
|
&r.material->materialSet, 0, nullptr);
|
||||||
@@ -163,5 +211,10 @@ void TransparentPass::draw_transparent(VkCommandBuffer cmd,
|
|||||||
|
|
||||||
void TransparentPass::cleanup()
|
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");
|
fmt::print("TransparentPass::cleanup()\n");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,5 +24,7 @@ private:
|
|||||||
RGImageHandle depthHandle) const;
|
RGImageHandle depthHandle) const;
|
||||||
|
|
||||||
EngineContext *_context{};
|
EngineContext *_context{};
|
||||||
|
mutable AllocatedImage _fallbackIbl2D{}; // 1x1 black (created in init)
|
||||||
|
mutable AllocatedImage _fallbackBrdf2D{}; // 1x1 black RG
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ void SceneManager::init(EngineContext *context)
|
|||||||
mainCamera.yaw = 0;
|
mainCamera.yaw = 0;
|
||||||
|
|
||||||
sceneData.ambientColor = glm::vec4(0.1f, 0.1f, 0.1f, 1.0f);
|
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);
|
sceneData.sunlightColor = glm::vec4(1.0f, 1.0f, 1.0f, 3.0f);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user