ADD: Normal mapping

This commit is contained in:
2025-11-01 17:32:14 +09:00
parent d5ff6263ee
commit fbc937974d
28 changed files with 2802 additions and 264 deletions

View File

@@ -72,6 +72,8 @@ add_executable (vulkan_engine
scene/vk_scene.cpp
scene/vk_loader.h
scene/vk_loader.cpp
scene/tangent_space.h
scene/tangent_space.cpp
scene/camera.h
scene/camera.cpp
# compute
@@ -89,7 +91,13 @@ target_include_directories(vulkan_engine PUBLIC
"${CMAKE_CURRENT_SOURCE_DIR}/compute"
)
option(ENABLE_MIKKTS "Use MikkTSpace for tangent generation" ON)
target_link_libraries(vulkan_engine PUBLIC vma glm Vulkan::Vulkan fmt::fmt stb_image SDL2::SDL2 vkbootstrap imgui fastgltf::fastgltf)
if (ENABLE_MIKKTS)
target_link_libraries(vulkan_engine PUBLIC mikktspace)
target_compile_definitions(vulkan_engine PUBLIC MIKKTS_ENABLE=1)
endif()
add_library(vma_impl OBJECT vma_impl.cpp)
target_include_directories(vma_impl PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/../third_party/vma")

View File

@@ -7,6 +7,7 @@
#include <core/vk_resource.h>
#include <render/vk_materials.h>
#include <render/primitives.h>
#include <scene/tangent_space.h>
#include <stb_image.h>
#include "asset_locator.h"
@@ -142,6 +143,12 @@ std::shared_ptr<MeshAsset> AssetManager::createMesh(const MeshCreateInfo &info)
break;
}
// Ensure tangents exist for primitives (and provided geometry if needed)
if (!tmpVerts.empty() && !tmpInds.empty())
{
geom::generate_tangents(tmpVerts, tmpInds);
}
if (info.material.kind == MeshMaterialDesc::Kind::Default)
{
return createMesh(info.name, vertsSpan, indsSpan, {});
@@ -151,9 +158,11 @@ std::shared_ptr<MeshAsset> AssetManager::createMesh(const MeshCreateInfo &info)
auto [albedo, createdAlbedo] = loadImageFromAsset(opt.albedoPath, opt.albedoSRGB);
auto [mr, createdMR] = loadImageFromAsset(opt.metalRoughPath, opt.metalRoughSRGB);
auto [normal, createdNormal] = loadImageFromAsset(opt.normalPath, opt.normalSRGB);
const AllocatedImage &albedoRef = createdAlbedo ? albedo : _engine->_errorCheckerboardImage;
const AllocatedImage &mrRef = createdMR ? mr : _engine->_whiteImage;
const AllocatedImage &normRef = createdNormal ? normal : _engine->_flatNormalImage;
AllocatedBuffer matBuffer = createMaterialBufferWithConstants(opt.constants);
@@ -162,6 +171,8 @@ std::shared_ptr<MeshAsset> AssetManager::createMesh(const MeshCreateInfo &info)
res.colorSampler = _engine->_samplerManager->defaultLinear();
res.metalRoughImage = mrRef;
res.metalRoughSampler = _engine->_samplerManager->defaultLinear();
res.normalImage = normRef;
res.normalSampler = _engine->_samplerManager->defaultLinear();
res.dataBuffer = matBuffer.buffer;
res.dataBufferOffset = 0;
@@ -171,6 +182,7 @@ std::shared_ptr<MeshAsset> AssetManager::createMesh(const MeshCreateInfo &info)
_meshMaterialBuffers.emplace(info.name, matBuffer);
if (createdAlbedo) _meshOwnedImages[info.name].push_back(albedo);
if (createdMR) _meshOwnedImages[info.name].push_back(mr);
if (createdNormal) _meshOwnedImages[info.name].push_back(normal);
return mesh;
}
@@ -213,6 +225,10 @@ AllocatedBuffer AssetManager::createMaterialBufferWithConstants(
{
matConstants->colorFactors = glm::vec4(1.0f);
}
if (matConstants->extra[0].x == 0.0f)
{
matConstants->extra[0].x = 1.0f; // normal scale default
}
// Ensure writes are visible on non-coherent memory
vmaFlushAllocation(_engine->_deviceManager->allocator(), matBuffer.allocation, 0,
sizeof(GLTFMetallic_Roughness::MaterialConstants));
@@ -270,6 +286,8 @@ std::shared_ptr<MeshAsset> AssetManager::createMesh(const std::string &name,
matResources.colorSampler = _engine->_samplerManager->defaultLinear();
matResources.metalRoughImage = _engine->_whiteImage;
matResources.metalRoughSampler = _engine->_samplerManager->defaultLinear();
matResources.normalImage = _engine->_flatNormalImage;
matResources.normalSampler = _engine->_samplerManager->defaultLinear();
AllocatedBuffer matBuffer = createMaterialBufferWithConstants({});
matResources.dataBuffer = matBuffer.buffer;

View File

@@ -25,9 +25,13 @@ public:
{
std::string albedoPath;
std::string metalRoughPath;
// Optional tangent-space normal map for PBR (placeholder; not wired yet)
// When enabled later, this will be sampled in shaders and requires tangents.
std::string normalPath;
bool albedoSRGB = true;
bool metalRoughSRGB = false;
bool normalSRGB = false; // normal maps are typically non-sRGB
GLTFMetallic_Roughness::MaterialConstants constants{};

View File

@@ -51,6 +51,268 @@
#include "core/vk_pipeline_manager.h"
#include "core/config.h"
//
// ImGui helpers: keep UI code tidy and grouped in small functions.
// These render inside a single consolidated Debug window using tab items.
//
namespace {
// Background / compute playground
static void ui_background(VulkanEngine *eng)
{
if (!eng || !eng->_renderPassManager) return;
auto *background_pass = eng->_renderPassManager->getPass<BackgroundPass>();
if (!background_pass) { ImGui::TextUnformatted("Background pass not available"); return; }
ComputeEffect &selected = background_pass->_backgroundEffects[background_pass->_currentEffect];
ImGui::Text("Selected effect: %s", selected.name);
ImGui::SliderInt("Effect Index", &background_pass->_currentEffect, 0,
(int)background_pass->_backgroundEffects.size() - 1);
ImGui::InputFloat4("data1", reinterpret_cast<float *>(&selected.data.data1));
ImGui::InputFloat4("data2", reinterpret_cast<float *>(&selected.data.data2));
ImGui::InputFloat4("data3", reinterpret_cast<float *>(&selected.data.data3));
ImGui::InputFloat4("data4", reinterpret_cast<float *>(&selected.data.data4));
ImGui::Separator();
ImGui::SliderFloat("Render Scale", &eng->renderScale, 0.3f, 1.f);
}
// Quick stats & targets overview
static void ui_overview(VulkanEngine *eng)
{
if (!eng) return;
ImGui::Text("frametime %.2f ms", eng->stats.frametime);
ImGui::Text("draw time %.2f ms", eng->stats.mesh_draw_time);
ImGui::Text("update time %.2f ms", eng->_sceneManager->stats.scene_update_time);
ImGui::Text("triangles %i", eng->stats.triangle_count);
ImGui::Text("draws %i", eng->stats.drawcall_count);
ImGui::Separator();
ImGui::Text("Draw extent: %ux%u", eng->_drawExtent.width, eng->_drawExtent.height);
auto scExt = eng->_swapchainManager->swapchainExtent();
ImGui::Text("Swapchain: %ux%u", scExt.width, scExt.height);
ImGui::Text("Draw fmt: %s", string_VkFormat(eng->_swapchainManager->drawImage().imageFormat));
ImGui::Text("Swap fmt: %s", string_VkFormat(eng->_swapchainManager->swapchainImageFormat()));
}
// Shadows / Ray Query controls
static void ui_shadows(VulkanEngine *eng)
{
if (!eng) return;
const bool rq = eng->_deviceManager->supportsRayQuery();
const bool as = eng->_deviceManager->supportsAccelerationStructure();
ImGui::Text("RayQuery: %s", rq ? "supported" : "not available");
ImGui::Text("AccelStruct: %s", as ? "supported" : "not available");
ImGui::Separator();
auto &ss = eng->_context->shadowSettings;
int mode = static_cast<int>(ss.mode);
ImGui::TextUnformatted("Shadow Mode");
ImGui::RadioButton("Clipmap only", &mode, 0); ImGui::SameLine();
ImGui::RadioButton("Clipmap + RT", &mode, 1); ImGui::SameLine();
ImGui::RadioButton("RT only", &mode, 2);
if (!(rq && as) && mode != 0) mode = 0; // guard for unsupported HW
ss.mode = static_cast<uint32_t>(mode);
ss.hybridRayQueryEnabled = (ss.mode != 0);
ImGui::BeginDisabled(ss.mode != 1u);
ImGui::TextUnformatted("Cascades using ray assist:");
for (int i = 0; i < 4; ++i)
{
bool on = (ss.hybridRayCascadesMask >> i) & 1u;
std::string label = std::string("C") + std::to_string(i);
if (ImGui::Checkbox(label.c_str(), &on))
{
if (on) ss.hybridRayCascadesMask |= (1u << i);
else ss.hybridRayCascadesMask &= ~(1u << i);
}
if (i != 3) ImGui::SameLine();
}
ImGui::SliderFloat("N·L threshold", &ss.hybridRayNoLThreshold, 0.0f, 1.0f, "%.2f");
ImGui::EndDisabled();
ImGui::Separator();
ImGui::TextWrapped("Clipmap only: raster PCF+RPDB. Clipmap+RT: PCF assisted by ray query at low N·L. RT only: skip shadow maps and use ray tests only.");
}
// Render Graph inspection (passes, images, buffers)
static void ui_render_graph(VulkanEngine *eng)
{
if (!eng || !eng->_renderGraph) { ImGui::TextUnformatted("RenderGraph not available"); return; }
auto &graph = *eng->_renderGraph;
std::vector<RenderGraph::RGDebugPassInfo> passInfos;
graph.debug_get_passes(passInfos);
if (ImGui::Button("Reload Pipelines")) { eng->_pipelineManager->hotReloadChanged(); }
ImGui::SameLine();
ImGui::Text("%zu passes", passInfos.size());
if (ImGui::BeginTable("passes", 8, ImGuiTableFlags_RowBg | ImGuiTableFlags_SizingStretchProp))
{
ImGui::TableSetupColumn("Enable", ImGuiTableColumnFlags_WidthFixed, 70);
ImGui::TableSetupColumn("Name");
ImGui::TableSetupColumn("Type", ImGuiTableColumnFlags_WidthFixed, 80);
ImGui::TableSetupColumn("GPU ms", ImGuiTableColumnFlags_WidthFixed, 70);
ImGui::TableSetupColumn("CPU rec ms", ImGuiTableColumnFlags_WidthFixed, 90);
ImGui::TableSetupColumn("Imgs", ImGuiTableColumnFlags_WidthFixed, 55);
ImGui::TableSetupColumn("Bufs", ImGuiTableColumnFlags_WidthFixed, 55);
ImGui::TableSetupColumn("Attachments", ImGuiTableColumnFlags_WidthFixed, 100);
ImGui::TableHeadersRow();
auto typeName = [](RGPassType t){
switch (t) {
case RGPassType::Graphics: return "Graphics";
case RGPassType::Compute: return "Compute";
case RGPassType::Transfer: return "Transfer";
default: return "?";
}
};
for (size_t i = 0; i < passInfos.size(); ++i)
{
auto &pi = passInfos[i];
ImGui::TableNextRow();
ImGui::TableSetColumnIndex(0);
bool enabled = true;
if (auto it = eng->_rgPassToggles.find(pi.name); it != eng->_rgPassToggles.end()) enabled = it->second;
std::string chkId = std::string("##en") + std::to_string(i);
if (ImGui::Checkbox(chkId.c_str(), &enabled))
{
eng->_rgPassToggles[pi.name] = enabled;
}
ImGui::TableSetColumnIndex(1);
ImGui::TextUnformatted(pi.name.c_str());
ImGui::TableSetColumnIndex(2);
ImGui::TextUnformatted(typeName(pi.type));
ImGui::TableSetColumnIndex(3);
if (pi.gpuMillis >= 0.0f) ImGui::Text("%.2f", pi.gpuMillis); else ImGui::TextUnformatted("-");
ImGui::TableSetColumnIndex(4);
if (pi.cpuMillis >= 0.0f) ImGui::Text("%.2f", pi.cpuMillis); else ImGui::TextUnformatted("-");
ImGui::TableSetColumnIndex(5);
ImGui::Text("%u/%u", pi.imageReads, pi.imageWrites);
ImGui::TableSetColumnIndex(6);
ImGui::Text("%u/%u", pi.bufferReads, pi.bufferWrites);
ImGui::TableSetColumnIndex(7);
ImGui::Text("%u%s", pi.colorAttachmentCount, pi.hasDepth ? "+D" : "");
}
ImGui::EndTable();
}
if (ImGui::CollapsingHeader("Images", ImGuiTreeNodeFlags_DefaultOpen))
{
std::vector<RenderGraph::RGDebugImageInfo> imgs;
graph.debug_get_images(imgs);
if (ImGui::BeginTable("images", 7, ImGuiTableFlags_RowBg | ImGuiTableFlags_SizingStretchProp))
{
ImGui::TableSetupColumn("Id", ImGuiTableColumnFlags_WidthFixed, 40);
ImGui::TableSetupColumn("Name");
ImGui::TableSetupColumn("Fmt", ImGuiTableColumnFlags_WidthFixed, 120);
ImGui::TableSetupColumn("Extent", ImGuiTableColumnFlags_WidthFixed, 120);
ImGui::TableSetupColumn("Imported", ImGuiTableColumnFlags_WidthFixed, 70);
ImGui::TableSetupColumn("Usage", ImGuiTableColumnFlags_WidthFixed, 80);
ImGui::TableSetupColumn("Life", ImGuiTableColumnFlags_WidthFixed, 80);
ImGui::TableHeadersRow();
for (const auto &im : imgs)
{
ImGui::TableNextRow();
ImGui::TableSetColumnIndex(0); ImGui::Text("%u", im.id);
ImGui::TableSetColumnIndex(1); ImGui::TextUnformatted(im.name.c_str());
ImGui::TableSetColumnIndex(2); ImGui::TextUnformatted(string_VkFormat(im.format));
ImGui::TableSetColumnIndex(3); ImGui::Text("%ux%u", im.extent.width, im.extent.height);
ImGui::TableSetColumnIndex(4); ImGui::TextUnformatted(im.imported ? "yes" : "no");
ImGui::TableSetColumnIndex(5); ImGui::Text("0x%x", (unsigned)im.creationUsage);
ImGui::TableSetColumnIndex(6); ImGui::Text("%d..%d", im.firstUse, im.lastUse);
}
ImGui::EndTable();
}
}
if (ImGui::CollapsingHeader("Buffers"))
{
std::vector<RenderGraph::RGDebugBufferInfo> bufs;
graph.debug_get_buffers(bufs);
if (ImGui::BeginTable("buffers", 6, ImGuiTableFlags_RowBg | ImGuiTableFlags_SizingStretchProp))
{
ImGui::TableSetupColumn("Id", ImGuiTableColumnFlags_WidthFixed, 40);
ImGui::TableSetupColumn("Name");
ImGui::TableSetupColumn("Size", ImGuiTableColumnFlags_WidthFixed, 100);
ImGui::TableSetupColumn("Imported", ImGuiTableColumnFlags_WidthFixed, 70);
ImGui::TableSetupColumn("Usage", ImGuiTableColumnFlags_WidthFixed, 100);
ImGui::TableSetupColumn("Life", ImGuiTableColumnFlags_WidthFixed, 80);
ImGui::TableHeadersRow();
for (const auto &bf : bufs)
{
ImGui::TableNextRow();
ImGui::TableSetColumnIndex(0); ImGui::Text("%u", bf.id);
ImGui::TableSetColumnIndex(1); ImGui::TextUnformatted(bf.name.c_str());
ImGui::TableSetColumnIndex(2); ImGui::Text("%zu", (size_t)bf.size);
ImGui::TableSetColumnIndex(3); ImGui::TextUnformatted(bf.imported ? "yes" : "no");
ImGui::TableSetColumnIndex(4); ImGui::Text("0x%x", (unsigned)bf.usage);
ImGui::TableSetColumnIndex(5); ImGui::Text("%d..%d", bf.firstUse, bf.lastUse);
}
ImGui::EndTable();
}
}
}
// Pipeline manager (graphics)
static void ui_pipelines(VulkanEngine *eng)
{
if (!eng || !eng->_pipelineManager) { ImGui::TextUnformatted("PipelineManager not available"); return; }
std::vector<PipelineManager::GraphicsPipelineDebugInfo> pipes;
eng->_pipelineManager->debug_get_graphics(pipes);
if (ImGui::Button("Reload Changed")) { eng->_pipelineManager->hotReloadChanged(); }
ImGui::SameLine(); ImGui::Text("%zu graphics pipelines", pipes.size());
if (ImGui::BeginTable("gfxpipes", 5, ImGuiTableFlags_RowBg | ImGuiTableFlags_SizingStretchProp))
{
ImGui::TableSetupColumn("Name");
ImGui::TableSetupColumn("VS");
ImGui::TableSetupColumn("FS");
ImGui::TableSetupColumn("Valid", ImGuiTableColumnFlags_WidthFixed, 60);
ImGui::TableHeadersRow();
for (const auto &p : pipes)
{
ImGui::TableNextRow();
ImGui::TableSetColumnIndex(0); ImGui::TextUnformatted(p.name.c_str());
ImGui::TableSetColumnIndex(1); ImGui::TextUnformatted(p.vertexShaderPath.c_str());
ImGui::TableSetColumnIndex(2); ImGui::TextUnformatted(p.fragmentShaderPath.c_str());
ImGui::TableSetColumnIndex(3); ImGui::TextUnformatted(p.valid ? "yes" : "no");
}
ImGui::EndTable();
}
}
// Post-processing
static void ui_postfx(VulkanEngine *eng)
{
if (!eng) return;
if (auto *tm = eng->_renderPassManager ? eng->_renderPassManager->getPass<TonemapPass>() : nullptr)
{
float exp = tm->exposure();
int mode = tm->mode();
if (ImGui::SliderFloat("Exposure", &exp, 0.05f, 8.0f)) { tm->setExposure(exp); }
ImGui::TextUnformatted("Operator");
ImGui::SameLine();
if (ImGui::RadioButton("Reinhard", mode == 0)) { mode = 0; tm->setMode(mode); }
ImGui::SameLine();
if (ImGui::RadioButton("ACES", mode == 1)) { mode = 1; tm->setMode(mode); }
}
else
{
ImGui::TextUnformatted("Tonemap pass not available");
}
}
// Scene debug bits
static void ui_scene(VulkanEngine *eng)
{
if (!eng) return;
const DrawContext &dc = eng->_context->getMainDrawContext();
ImGui::Text("Opaque draws: %zu", dc.OpaqueSurfaces.size());
ImGui::Text("Transp draws: %zu", dc.TransparentSurfaces.size());
}
} // namespace
VulkanEngine *loadedEngine = nullptr;
static void print_vma_stats(DeviceManager* dev, const char* tag)
@@ -190,7 +452,7 @@ void VulkanEngine::init()
auto imguiPass = std::make_unique<ImGuiPass>();
_renderPassManager->setImGuiPass(std::move(imguiPass));
const std::string structurePath = _assetManager->modelPath("seoul_high.glb");
const std::string structurePath = _assetManager->modelPath("mirage.glb");
const auto structureFile = _assetManager->loadGLTF(structurePath);
assert(structureFile.has_value());
@@ -219,6 +481,11 @@ void VulkanEngine::init_default_data()
_blackImage = _resourceManager->create_image((void *) &black, VkExtent3D{1, 1, 1}, VK_FORMAT_R8G8B8A8_UNORM,
VK_IMAGE_USAGE_SAMPLED_BIT);
// Flat normal (0.5, 0.5, 1.0) for missing normal maps
uint32_t flatN = glm::packUnorm4x8(glm::vec4(0.5f, 0.5f, 1.0f, 1.0f));
_flatNormalImage = _resourceManager->create_image((void *) &flatN, VkExtent3D{1, 1, 1}, VK_FORMAT_R8G8B8A8_UNORM,
VK_IMAGE_USAGE_SAMPLED_BIT);
//checkerboard image
uint32_t magenta = glm::packUnorm4x8(glm::vec4(1, 0, 1, 1));
std::array<uint32_t, 16 * 16> pixels{}; //for 16x16 checkerboard texture
@@ -265,6 +532,7 @@ void VulkanEngine::init_default_data()
_resourceManager->destroy_image(_greyImage);
_resourceManager->destroy_image(_blackImage);
_resourceManager->destroy_image(_errorCheckerboardImage);
_resourceManager->destroy_image(_flatNormalImage);
});
//< default_img
}
@@ -581,272 +849,51 @@ void VulkanEngine::run()
ImGui::NewFrame();
if (ImGui::Begin("background"))
// Consolidated debug window with tabs
if (ImGui::Begin("Debug"))
{
auto background_pass = _renderPassManager->getPass<BackgroundPass>();
ComputeEffect &selected = background_pass->_backgroundEffects[background_pass->_currentEffect];
ImGui::Text("Selected effect: %s", selected.name);
ImGui::SliderInt("Effect Index", &background_pass->_currentEffect, 0,
background_pass->_backgroundEffects.size() - 1);
ImGui::InputFloat4("data1", reinterpret_cast<float *>(&selected.data.data1));
ImGui::InputFloat4("data2", reinterpret_cast<float *>(&selected.data.data2));
ImGui::InputFloat4("data3", reinterpret_cast<float *>(&selected.data.data3));
ImGui::InputFloat4("data4", reinterpret_cast<float *>(&selected.data.data4));
ImGui::SliderFloat("Render Scale", &renderScale, 0.3f, 1.f);
ImGui::End();
}
if (ImGui::Begin("Stats"))
{
ImGui::Text("frametime %f ms", stats.frametime);
ImGui::Text("draw time %f ms", stats.mesh_draw_time);
ImGui::Text("update time %f ms", _sceneManager->stats.scene_update_time);
ImGui::Text("triangles %i", stats.triangle_count);
ImGui::Text("draws %i", stats.drawcall_count);
ImGui::End();
}
// Shadows / Ray Query settings
if (ImGui::Begin("Shadows"))
{
const bool rq = _deviceManager->supportsRayQuery();
const bool as = _deviceManager->supportsAccelerationStructure();
ImGui::Text("RayQuery: %s", rq ? "supported" : "not available");
ImGui::Text("AccelStruct: %s", as ? "supported" : "not available");
ImGui::Separator();
auto &ss = _context->shadowSettings;
// Mode selection
int mode = static_cast<int>(ss.mode);
ImGui::TextUnformatted("Shadow Mode");
ImGui::RadioButton("Clipmap only", &mode, 0); ImGui::SameLine();
ImGui::RadioButton("Clipmap + RT", &mode, 1); ImGui::SameLine();
ImGui::RadioButton("RT only", &mode, 2);
// If device lacks RT support, force mode 0
if (!(rq && as) && mode != 0) mode = 0;
ss.mode = static_cast<uint32_t>(mode);
ss.hybridRayQueryEnabled = (ss.mode != 0);
// Hybrid controls (mode 1)
ImGui::BeginDisabled(ss.mode != 1u);
ImGui::TextUnformatted("Cascades using ray assist:");
for (int i = 0; i < 4; ++i)
const ImGuiTabBarFlags tf = ImGuiTabBarFlags_Reorderable | ImGuiTabBarFlags_AutoSelectNewTabs;
if (ImGui::BeginTabBar("DebugTabs", tf))
{
bool on = (ss.hybridRayCascadesMask >> i) & 1u;
std::string label = std::string("C") + std::to_string(i);
if (ImGui::Checkbox(label.c_str(), &on))
if (ImGui::BeginTabItem("Overview"))
{
if (on) ss.hybridRayCascadesMask |= (1u << i);
else ss.hybridRayCascadesMask &= ~(1u << i);
ui_overview(this);
ImGui::EndTabItem();
}
if (i != 3) ImGui::SameLine();
}
ImGui::SliderFloat("N·L threshold", &ss.hybridRayNoLThreshold, 0.0f, 1.0f, "%.2f");
ImGui::EndDisabled();
ImGui::Separator();
ImGui::TextWrapped("Clipmap only: raster PCF+RPDB. Clipmap+RT: PCF assisted by ray query at low N·L. RT only: skip shadow maps and use ray tests only.");
ImGui::End();
}
// Render Graph debug window
if (ImGui::Begin("Render Graph"))
{
if (_renderGraph)
{
auto &graph = *_renderGraph;
std::vector<RenderGraph::RGDebugPassInfo> passInfos;
graph.debug_get_passes(passInfos);
if (ImGui::Button("Reload Pipelines")) { _pipelineManager->hotReloadChanged(); }
ImGui::SameLine();
ImGui::Text("%zu passes", passInfos.size());
if (ImGui::BeginTable("passes", 8, ImGuiTableFlags_RowBg | ImGuiTableFlags_SizingStretchProp))
if (ImGui::BeginTabItem("Background"))
{
ImGui::TableSetupColumn("Enable", ImGuiTableColumnFlags_WidthFixed, 70);
ImGui::TableSetupColumn("Name");
ImGui::TableSetupColumn("Type", ImGuiTableColumnFlags_WidthFixed, 80);
ImGui::TableSetupColumn("GPU ms", ImGuiTableColumnFlags_WidthFixed, 70);
ImGui::TableSetupColumn("CPU rec ms", ImGuiTableColumnFlags_WidthFixed, 90);
ImGui::TableSetupColumn("Imgs", ImGuiTableColumnFlags_WidthFixed, 55);
ImGui::TableSetupColumn("Bufs", ImGuiTableColumnFlags_WidthFixed, 55);
ImGui::TableSetupColumn("Attachments", ImGuiTableColumnFlags_WidthFixed, 100);
ImGui::TableHeadersRow();
auto typeName = [](RGPassType t){
switch (t) {
case RGPassType::Graphics: return "Graphics";
case RGPassType::Compute: return "Compute";
case RGPassType::Transfer: return "Transfer";
default: return "?";
}
};
for (size_t i = 0; i < passInfos.size(); ++i)
{
auto &pi = passInfos[i];
ImGui::TableNextRow();
ImGui::TableSetColumnIndex(0);
bool enabled = true;
if (auto it = _rgPassToggles.find(pi.name); it != _rgPassToggles.end()) enabled = it->second;
std::string chkId = std::string("##en") + std::to_string(i);
if (ImGui::Checkbox(chkId.c_str(), &enabled))
{
_rgPassToggles[pi.name] = enabled;
}
ImGui::TableSetColumnIndex(1);
ImGui::TextUnformatted(pi.name.c_str());
ImGui::TableSetColumnIndex(2);
ImGui::TextUnformatted(typeName(pi.type));
ImGui::TableSetColumnIndex(3);
if (pi.gpuMillis >= 0.0f) ImGui::Text("%.2f", pi.gpuMillis); else ImGui::TextUnformatted("-");
ImGui::TableSetColumnIndex(4);
if (pi.cpuMillis >= 0.0f) ImGui::Text("%.2f", pi.cpuMillis); else ImGui::TextUnformatted("-");
ImGui::TableSetColumnIndex(5);
ImGui::Text("%u/%u", pi.imageReads, pi.imageWrites);
ImGui::TableSetColumnIndex(6);
ImGui::Text("%u/%u", pi.bufferReads, pi.bufferWrites);
ImGui::TableSetColumnIndex(7);
ImGui::Text("%u%s", pi.colorAttachmentCount, pi.hasDepth ? "+D" : "");
}
ImGui::EndTable();
ui_background(this);
ImGui::EndTabItem();
}
if (ImGui::CollapsingHeader("Images", ImGuiTreeNodeFlags_DefaultOpen))
if (ImGui::BeginTabItem("Shadows"))
{
std::vector<RenderGraph::RGDebugImageInfo> imgs;
graph.debug_get_images(imgs);
if (ImGui::BeginTable("images", 7, ImGuiTableFlags_RowBg | ImGuiTableFlags_SizingStretchProp))
{
ImGui::TableSetupColumn("Id", ImGuiTableColumnFlags_WidthFixed, 40);
ImGui::TableSetupColumn("Name");
ImGui::TableSetupColumn("Fmt", ImGuiTableColumnFlags_WidthFixed, 120);
ImGui::TableSetupColumn("Extent", ImGuiTableColumnFlags_WidthFixed, 120);
ImGui::TableSetupColumn("Imported", ImGuiTableColumnFlags_WidthFixed, 70);
ImGui::TableSetupColumn("Usage", ImGuiTableColumnFlags_WidthFixed, 80);
ImGui::TableSetupColumn("Life", ImGuiTableColumnFlags_WidthFixed, 80);
ImGui::TableHeadersRow();
for (const auto &im : imgs)
{
ImGui::TableNextRow();
ImGui::TableSetColumnIndex(0); ImGui::Text("%u", im.id);
ImGui::TableSetColumnIndex(1); ImGui::TextUnformatted(im.name.c_str());
ImGui::TableSetColumnIndex(2); ImGui::TextUnformatted(string_VkFormat(im.format));
ImGui::TableSetColumnIndex(3); ImGui::Text("%ux%u", im.extent.width, im.extent.height);
ImGui::TableSetColumnIndex(4); ImGui::TextUnformatted(im.imported ? "yes" : "no");
ImGui::TableSetColumnIndex(5); ImGui::Text("0x%x", (unsigned)im.creationUsage);
ImGui::TableSetColumnIndex(6); ImGui::Text("%d..%d", im.firstUse, im.lastUse);
}
ImGui::EndTable();
}
ui_shadows(this);
ImGui::EndTabItem();
}
if (ImGui::CollapsingHeader("Buffers"))
if (ImGui::BeginTabItem("Render Graph"))
{
std::vector<RenderGraph::RGDebugBufferInfo> bufs;
graph.debug_get_buffers(bufs);
if (ImGui::BeginTable("buffers", 6, ImGuiTableFlags_RowBg | ImGuiTableFlags_SizingStretchProp))
{
ImGui::TableSetupColumn("Id", ImGuiTableColumnFlags_WidthFixed, 40);
ImGui::TableSetupColumn("Name");
ImGui::TableSetupColumn("Size", ImGuiTableColumnFlags_WidthFixed, 100);
ImGui::TableSetupColumn("Imported", ImGuiTableColumnFlags_WidthFixed, 70);
ImGui::TableSetupColumn("Usage", ImGuiTableColumnFlags_WidthFixed, 100);
ImGui::TableSetupColumn("Life", ImGuiTableColumnFlags_WidthFixed, 80);
ImGui::TableHeadersRow();
for (const auto &bf : bufs)
{
ImGui::TableNextRow();
ImGui::TableSetColumnIndex(0); ImGui::Text("%u", bf.id);
ImGui::TableSetColumnIndex(1); ImGui::TextUnformatted(bf.name.c_str());
ImGui::TableSetColumnIndex(2); ImGui::Text("%zu", (size_t)bf.size);
ImGui::TableSetColumnIndex(3); ImGui::TextUnformatted(bf.imported ? "yes" : "no");
ImGui::TableSetColumnIndex(4); ImGui::Text("0x%x", (unsigned)bf.usage);
ImGui::TableSetColumnIndex(5); ImGui::Text("%d..%d", bf.firstUse, bf.lastUse);
}
ImGui::EndTable();
}
ui_render_graph(this);
ImGui::EndTabItem();
}
}
ImGui::End();
}
// Pipelines debug window (graphics)
if (ImGui::Begin("Pipelines"))
{
if (_pipelineManager)
{
std::vector<PipelineManager::GraphicsPipelineDebugInfo> pipes;
_pipelineManager->debug_get_graphics(pipes);
if (ImGui::Button("Reload Changed")) { _pipelineManager->hotReloadChanged(); }
ImGui::SameLine(); ImGui::Text("%zu graphics pipelines", pipes.size());
if (ImGui::BeginTable("gfxpipes", 5, ImGuiTableFlags_RowBg | ImGuiTableFlags_SizingStretchProp))
if (ImGui::BeginTabItem("Pipelines"))
{
ImGui::TableSetupColumn("Name");
ImGui::TableSetupColumn("VS");
ImGui::TableSetupColumn("FS");
ImGui::TableSetupColumn("Valid", ImGuiTableColumnFlags_WidthFixed, 60);
ImGui::TableHeadersRow();
for (const auto &p : pipes)
{
ImGui::TableNextRow();
ImGui::TableSetColumnIndex(0); ImGui::TextUnformatted(p.name.c_str());
ImGui::TableSetColumnIndex(1); ImGui::TextUnformatted(p.vertexShaderPath.c_str());
ImGui::TableSetColumnIndex(2); ImGui::TextUnformatted(p.fragmentShaderPath.c_str());
ImGui::TableSetColumnIndex(3); ImGui::TextUnformatted(p.valid ? "yes" : "no");
}
ImGui::EndTable();
ui_pipelines(this);
ImGui::EndTabItem();
}
if (ImGui::BeginTabItem("PostFX"))
{
ui_postfx(this);
ImGui::EndTabItem();
}
if (ImGui::BeginTabItem("Scene"))
{
ui_scene(this);
ImGui::EndTabItem();
}
ImGui::EndTabBar();
}
ImGui::End();
}
// Draw targets window
if (ImGui::Begin("Targets"))
{
ImGui::Text("Draw extent: %ux%u", _drawExtent.width, _drawExtent.height);
auto scExt = _swapchainManager->swapchainExtent();
ImGui::Text("Swapchain: %ux%u", scExt.width, scExt.height);
ImGui::Text("Draw fmt: %s", string_VkFormat(_swapchainManager->drawImage().imageFormat));
ImGui::Text("Swap fmt: %s", string_VkFormat(_swapchainManager->swapchainImageFormat()));
ImGui::End();
}
// PostFX window
if (ImGui::Begin("PostFX"))
{
if (auto *tm = _renderPassManager->getPass<TonemapPass>())
{
float exp = tm->exposure();
int mode = tm->mode();
if (ImGui::SliderFloat("Exposure", &exp, 0.05f, 8.0f)) { tm->setExposure(exp); }
ImGui::TextUnformatted("Operator");
ImGui::SameLine();
if (ImGui::RadioButton("Reinhard", mode == 0)) { mode = 0; tm->setMode(mode); }
ImGui::SameLine();
if (ImGui::RadioButton("ACES", mode == 1)) { mode = 1; tm->setMode(mode); }
}
else
{
ImGui::TextUnformatted("Tonemap pass not available");
}
ImGui::End();
}
// Scene window
if (ImGui::Begin("Scene"))
{
const DrawContext &dc = _context->getMainDrawContext();
ImGui::Text("Opaque draws: %zu", dc.OpaqueSurfaces.size());
ImGui::Text("Transp draws: %zu", dc.TransparentSurfaces.size());
ImGui::End();
}
ImGui::Render();
draw();

View File

@@ -97,6 +97,7 @@ public:
AllocatedImage _blackImage;
AllocatedImage _greyImage;
AllocatedImage _errorCheckerboardImage;
AllocatedImage _flatNormalImage; // 1x1 (0.5,0.5,1.0)
MaterialInstance defaultData;

View File

@@ -107,6 +107,8 @@ struct Vertex {
glm::vec3 normal;
float uv_y;
glm::vec4 color;
// Tangent.xyz = tangent direction; Tangent.w = handedness sign for B = sign * cross(N, T)
glm::vec4 tangent;
};
// holds the resources needed for a mesh

View File

@@ -24,10 +24,10 @@ inline void buildCube(std::vector<Vertex>& vertices, std::vector<uint32_t>& indi
for (auto& f : faces) {
uint32_t start = (uint32_t)vertices.size();
Vertex v0{f.v0, 0, f.normal, 0, glm::vec4(1.0f)};
Vertex v1{f.v1, 1, f.normal, 0, glm::vec4(1.0f)};
Vertex v2{f.v2, 0, f.normal, 1, glm::vec4(1.0f)};
Vertex v3{f.v3, 1, f.normal, 1, glm::vec4(1.0f)};
Vertex v0{f.v0, 0, f.normal, 0, glm::vec4(1.0f), glm::vec4(1,0,0,1)};
Vertex v1{f.v1, 1, f.normal, 0, glm::vec4(1.0f), glm::vec4(1,0,0,1)};
Vertex v2{f.v2, 0, f.normal, 1, glm::vec4(1.0f), glm::vec4(1,0,0,1)};
Vertex v3{f.v3, 1, f.normal, 1, glm::vec4(1.0f), glm::vec4(1,0,0,1)};
vertices.push_back(v0);
vertices.push_back(v1);
vertices.push_back(v2);
@@ -61,6 +61,7 @@ inline void buildSphere(std::vector<Vertex>& vertices, std::vector<uint32_t>& in
vert.uv_x = u;
vert.uv_y = 1.0f - v;
vert.color = glm::vec4(1.0f);
vert.tangent = glm::vec4(1,0,0,1);
vertices.push_back(vert);
}
}

View File

@@ -19,6 +19,7 @@ void GLTFMetallic_Roughness::build_pipelines(VulkanEngine *engine)
layoutBuilder.add_binding(0, VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER);
layoutBuilder.add_binding(1, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER);
layoutBuilder.add_binding(2, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER);
layoutBuilder.add_binding(3, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER);
materialLayout = layoutBuilder.build(engine->_deviceManager->device(),
VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT,
@@ -120,6 +121,8 @@ MaterialInstance GLTFMetallic_Roughness::write_material(VkDevice device, Materia
VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER);
writer.write_image(2, resources.metalRoughImage.imageView, resources.metalRoughSampler,
VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER);
writer.write_image(3, resources.normalImage.imageView, resources.normalSampler,
VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER);
writer.update_set(device, matData.materialSet);

View File

@@ -27,6 +27,8 @@ struct GLTFMetallic_Roughness
VkSampler colorSampler;
AllocatedImage metalRoughImage;
VkSampler metalRoughSampler;
AllocatedImage normalImage;
VkSampler normalSampler;
VkBuffer dataBuffer;
uint32_t dataBufferOffset;
};

219
src/scene/tangent_space.cpp Normal file
View File

@@ -0,0 +1,219 @@
#include "tangent_space.h"
#include <glm/glm.hpp>
#include <glm/gtc/epsilon.hpp>
#include <algorithm>
#include "glm/gtx/norm.hpp"
namespace
{
struct Range
{
size_t indexStart;
size_t indexCount;
size_t vertexStart;
size_t vertexCount;
};
static inline glm::vec3 orthonormal_tangent(const glm::vec3 &n)
{
// Build tangent orthogonal to n from an arbitrary axis
glm::vec3 a = (std::abs(n.z) < 0.999f) ? glm::vec3(0, 0, 1) : glm::vec3(0, 1, 0);
glm::vec3 t = glm::normalize(glm::cross(a, n));
return t;
}
static void generate_fallback(std::vector<Vertex> &vtx, const Range &r)
{
for (size_t i = 0; i < r.vertexCount; ++i)
{
Vertex &v = vtx[r.vertexStart + i];
glm::vec3 T = orthonormal_tangent(glm::normalize(v.normal));
v.tangent = glm::vec4(T, 1.0f);
}
}
} // namespace
namespace geom
{
#ifdef MIKKTS_ENABLE
#include <mikktspace.h>
struct MikkAdapter
{
std::vector<Vertex> *verts;
const std::vector<uint32_t> *inds;
Range range;
};
static int mikk_get_num_faces(const SMikkTSpaceContext *ctx)
{
const MikkAdapter *ad = reinterpret_cast<const MikkAdapter *>(ctx->m_pUserData);
return static_cast<int>(ad->range.indexCount / 3);
}
static int mikk_get_num_verts_of_face(const SMikkTSpaceContext *, const int /*face*/) { return 3; }
static void mikk_get_position(const SMikkTSpaceContext *ctx, float outpos[], const int face, const int vert)
{
const MikkAdapter *ad = reinterpret_cast<const MikkAdapter *>(ctx->m_pUserData);
uint32_t idx = ad->inds->at(ad->range.indexStart + face * 3 + vert);
const Vertex &v = ad->verts->at(idx);
outpos[0] = v.position.x;
outpos[1] = v.position.y;
outpos[2] = v.position.z;
}
static void mikk_get_normal(const SMikkTSpaceContext *ctx, float outnormal[], const int face, const int vert)
{
const MikkAdapter *ad = reinterpret_cast<const MikkAdapter *>(ctx->m_pUserData);
uint32_t idx = ad->inds->at(ad->range.indexStart + face * 3 + vert);
const Vertex &v = ad->verts->at(idx);
outnormal[0] = v.normal.x;
outnormal[1] = v.normal.y;
outnormal[2] = v.normal.z;
}
static void mikk_get_texcoord(const SMikkTSpaceContext *ctx, float outuv[], const int face, const int vert)
{
const MikkAdapter *ad = reinterpret_cast<const MikkAdapter *>(ctx->m_pUserData);
uint32_t idx = ad->inds->at(ad->range.indexStart + face * 3 + vert);
const Vertex &v = ad->verts->at(idx);
outuv[0] = v.uv_x;
outuv[1] = v.uv_y;
}
static void mikk_set_tspace_basic(const SMikkTSpaceContext *ctx, const float tangent[], const float sign,
const int face, const int vert)
{
const MikkAdapter *ad = reinterpret_cast<const MikkAdapter *>(ctx->m_pUserData);
uint32_t idx = ad->inds->at(ad->range.indexStart + face * 3 + vert);
Vertex &v = ad->verts->at(idx);
v.tangent = glm::vec4(tangent[0], tangent[1], tangent[2], sign);
}
static bool generate_mikk(std::vector<Vertex> &vertices, const std::vector<uint32_t> &indices, const Range &r)
{
SMikkTSpaceInterface iface{};
iface.m_getNumFaces = mikk_get_num_faces;
iface.m_getNumVerticesOfFace = mikk_get_num_verts_of_face;
iface.m_getPosition = mikk_get_position;
iface.m_getNormal = mikk_get_normal;
iface.m_getTexCoord = mikk_get_texcoord;
iface.m_setTSpaceBasic = mikk_set_tspace_basic;
MikkAdapter ad{&vertices, &indices, r};
SMikkTSpaceContext ctx{};
ctx.m_pInterface = &iface;
ctx.m_pUserData = &ad;
// angle weighting, respect vtx-deriv continuity by default
return genTangSpaceDefault(&ctx) != 0;
}
#endif // MIKKTS_ENABLE
void generate_tangents_range(std::vector<Vertex> &vertices, const std::vector<uint32_t> &indices,
size_t indexStart, size_t indexCount,
size_t vertexStart, size_t vertexCount)
{
Range r{indexStart, indexCount, vertexStart, vertexCount};
if (indexCount < 3 || vertexCount == 0)
{
generate_fallback(vertices, r);
return;
}
#ifdef MIKKTS_ENABLE
if (generate_mikk(vertices, indices, r))
{
return;
}
#endif
std::vector<glm::vec3> tan1(vertexCount, glm::vec3(0.0f));
std::vector<glm::vec3> bit1(vertexCount, glm::vec3(0.0f));
bool anyValid = false;
for (size_t it = 0; it + 2 < indexCount; it += 3)
{
uint32_t i0 = indices[r.indexStart + it];
uint32_t i1 = indices[r.indexStart + it + 1];
uint32_t i2 = indices[r.indexStart + it + 2];
// guard against out of range
if (i0 < r.vertexStart || i1 < r.vertexStart || i2 < r.vertexStart) continue;
if (i0 >= r.vertexStart + r.vertexCount || i1 >= r.vertexStart + r.vertexCount || i2 >= r.vertexStart + r.
vertexCount) continue;
const Vertex &v0 = vertices[i0];
const Vertex &v1 = vertices[i1];
const Vertex &v2 = vertices[i2];
glm::vec3 p0 = v0.position;
glm::vec3 p1 = v1.position;
glm::vec3 p2 = v2.position;
glm::vec2 w0{v0.uv_x, v0.uv_y};
glm::vec2 w1{v1.uv_x, v1.uv_y};
glm::vec2 w2{v2.uv_x, v2.uv_y};
glm::vec3 e1 = p1 - p0;
glm::vec3 e2 = p2 - p0;
glm::vec2 d1 = w1 - w0;
glm::vec2 d2 = w2 - w0;
float denom = d1.x * d2.y - d1.y * d2.x;
if (std::abs(denom) < 1e-8f)
{
continue; // degenerate UV mapping; skip this tri
}
anyValid = true;
float rcp = 1.0f / denom;
glm::vec3 t = (e1 * d2.y - e2 * d1.y) * rcp;
glm::vec3 b = (-e1 * d2.x + e2 * d1.x) * rcp;
size_t l0 = i0 - r.vertexStart;
size_t l1 = i1 - r.vertexStart;
size_t l2 = i2 - r.vertexStart;
tan1[l0] += t;
tan1[l1] += t;
tan1[l2] += t;
bit1[l0] += b;
bit1[l1] += b;
bit1[l2] += b;
}
if (!anyValid)
{
generate_fallback(vertices, r);
return;
}
for (size_t i = 0; i < r.vertexCount; ++i)
{
Vertex &v = vertices[r.vertexStart + i];
glm::vec3 N = glm::normalize(v.normal);
glm::vec3 T = tan1[i];
glm::vec3 B = bit1[i];
if (glm::length2(T) < 1e-16f)
{
T = orthonormal_tangent(N);
v.tangent = glm::vec4(T, 1.0f);
continue;
}
// Gram-Schmidt orthonormalize
T = glm::normalize(T - N * glm::dot(N, T));
// Compute handedness
float w = (glm::dot(glm::cross(N, T), B) < 0.0f) ? -1.0f : 1.0f;
v.tangent = glm::vec4(T, w);
}
}
void generate_tangents(std::vector<Vertex> &vertices, const std::vector<uint32_t> &indices)
{
if (vertices.empty() || indices.size() < 3) return;
generate_tangents_range(vertices, indices, 0, indices.size(), 0, vertices.size());
}
} // namespace geom

20
src/scene/tangent_space.h Normal file
View File

@@ -0,0 +1,20 @@
#pragma once
#include <vector>
#include <cstddef>
#include <core/vk_types.h>
namespace geom {
// Generate per-vertex tangents with a robust fallback when MikkTSpace is not available.
// - Fills Vertex.tangent (xyz = tangent, w = handedness sign for B = sign * cross(N, T))
// - Expects valid normals and UVs; if UVs are degenerate, builds an arbitrary orthonormal basis.
void generate_tangents(std::vector<Vertex>& vertices, const std::vector<uint32_t>& indices);
// Range variant for submeshes (indices [indexStart, indexStart+indexCount), vertices [vertexStart, vertexStart+vertexCount))
void generate_tangents_range(std::vector<Vertex>& vertices, const std::vector<uint32_t>& indices,
size_t indexStart, size_t indexCount,
size_t vertexStart, size_t vertexCount);
} // namespace geom

View File

@@ -14,6 +14,7 @@
#include <fastgltf/tools.hpp>
#include <fastgltf/util.hpp>
#include <optional>
#include "tangent_space.h"
//> loadimg
std::optional<AllocatedImage> load_image(VulkanEngine *engine, fastgltf::Asset &asset, fastgltf::Image &image, bool srgb)
{
@@ -312,6 +313,8 @@ std::optional<std::shared_ptr<LoadedGLTF> > loadGltf(VulkanEngine *engine, std::
file.materials[mat.name.c_str()] = newMat;
GLTFMetallic_Roughness::MaterialConstants constants;
// Defaults
constants.extra[0].x = 1.0f; // normalScale
constants.colorFactors.x = mat.pbrData.baseColorFactor[0];
constants.colorFactors.y = mat.pbrData.baseColorFactor[1];
constants.colorFactors.z = mat.pbrData.baseColorFactor[2];
@@ -334,6 +337,8 @@ std::optional<std::shared_ptr<LoadedGLTF> > loadGltf(VulkanEngine *engine, std::
materialResources.colorSampler = engine->_samplerManager->defaultLinear();
materialResources.metalRoughImage = engine->_whiteImage;
materialResources.metalRoughSampler = engine->_samplerManager->defaultLinear();
materialResources.normalImage = engine->_flatNormalImage;
materialResources.normalSampler = engine->_samplerManager->defaultLinear();
// set the uniform buffer for the material data
materialResources.dataBuffer = file.materialDataBuffer.buffer;
@@ -385,6 +390,39 @@ std::optional<std::shared_ptr<LoadedGLTF> > loadGltf(VulkanEngine *engine, std::
: engine->_samplerManager->defaultLinear();
}
}
// Normal map (tangent-space)
if (mat.normalTexture.has_value())
{
const auto &tex = gltf.textures[mat.normalTexture.value().textureIndex];
size_t imgIndex = tex.imageIndex.value();
bool hasSampler = tex.samplerIndex.has_value();
size_t sampler = hasSampler ? tex.samplerIndex.value() : SIZE_MAX;
if (imgIndex < gltf.images.size())
{
auto normalImg = load_image(engine, gltf, gltf.images[imgIndex], false);
if (normalImg.has_value())
{
materialResources.normalImage = *normalImg;
std::string key = std::string("normal_") + mat.name.c_str() + "_" + std::to_string(imgIndex);
file.images[key] = *normalImg;
}
else
{
materialResources.normalImage = images[imgIndex];
}
}
else
{
materialResources.normalImage = engine->_flatNormalImage;
}
materialResources.normalSampler = hasSampler ? file.samplers[sampler]
: engine->_samplerManager->defaultLinear();
// Store normal scale into material constants extra[0].x if available
sceneMaterialConstants[data_index].extra[0].x = mat.normalTexture->scale;
}
// build material
newMat->data = engine->metalRoughMaterial.write_material(engine->_deviceManager->device(), passType, materialResources,
file.descriptorPool);
@@ -442,12 +480,13 @@ std::optional<std::shared_ptr<LoadedGLTF> > loadGltf(VulkanEngine *engine, std::
fastgltf::iterateAccessorWithIndex<glm::vec3>(gltf, posAccessor,
[&](glm::vec3 v, size_t index) {
Vertex newvtx;
Vertex newvtx{};
newvtx.position = v;
newvtx.normal = {1, 0, 0};
newvtx.color = glm::vec4{1.f};
newvtx.uv_x = 0;
newvtx.uv_y = 0;
newvtx.tangent = glm::vec4(1,0,0,1);
vertices[initial_vtx + index] = newvtx;
});
}
@@ -483,6 +522,29 @@ std::optional<std::shared_ptr<LoadedGLTF> > loadGltf(VulkanEngine *engine, std::
});
}
// load tangents if present (vec4, w = sign)
auto tangents = p.findAttribute("TANGENT");
bool hasTangents = tangents != p.attributes.end();
if (hasTangents)
{
fastgltf::iterateAccessorWithIndex<glm::vec4>(gltf, gltf.accessors[(*tangents).second],
[&](glm::vec4 v, size_t index) {
vertices[initial_vtx + index].tangent = v;
});
}
// Generate tangents if missing and we have UVs
if (!hasTangents)
{
size_t primIndexStart = newSurface.startIndex;
size_t primIndexCount = newSurface.count;
size_t primVertexStart = initial_vtx;
size_t primVertexCount = vertices.size() - initial_vtx;
geom::generate_tangents_range(vertices, indices,
primIndexStart, primIndexCount,
primVertexStart, primVertexCount);
}
if (p.materialIndex.has_value())
{
newSurface.material = materials[p.materialIndex.value()];