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

@@ -6,7 +6,7 @@ Work-In-Progress Vulkan render engine
Current structure: Current structure:
- Flexible render graph system with multiple render passes - Flexible render graph system with multiple render passes
- Deferred rendering - Deferred rendering
- PBR(IBL is WIP), shadow, - 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.
Work-In-Progress Work-In-Progress

View File

@@ -20,4 +20,8 @@
- Materials & sRGB - Materials & sRGB
- See `docs/asset_manager.md` for mesh/material creation and sRGB/UNORM handling. - See `docs/asset_manager.md` for mesh/material creation and sRGB/UNORM handling.
- Conventions:
- Albedo/base color → sRGB
- Metallic-Roughness → UNORM (G=roughness, B=metallic)
- Normal map → UNORM (+Y green up)

View File

@@ -32,6 +32,11 @@
- Thirdparty deps - Thirdparty deps
- Vendored under `third_party/` and brought in via CMake. Do not edit headers directly; update through targets. - Vendored under `third_party/` and brought in via CMake. Do not edit headers directly; update through targets.
- Optional: MikkTSpace tangents
- Enable at configure time: `-DENABLE_MIKKTS=ON` (default ON if found).
- Requires `third_party/MikkTSpace/mikktspace.c` and `mikktspace.h` (provided).
- Disable to use the builtin GramSchmidt generator: `-DENABLE_MIKKTS=OFF`.
- Validation Layers - Validation Layers
- Enabled in Debug (`kUseValidationLayers = true` in `src/core/config.h`). - Enabled in Debug (`kUseValidationLayers = true` in `src/core/config.h`).
- Disable by building Release or toggling the flag during local experimentation. - Disable by building Release or toggling the flag during local experimentation.

View File

@@ -18,6 +18,12 @@
- Name SPIRV files with full extension, e.g. `fullscreen.vert.spv`, `deferred_lighting.frag.spv`. - Name SPIRV files with full extension, e.g. `fullscreen.vert.spv`, `deferred_lighting.frag.spv`.
- Use `EngineContext::getAssets()->shaderPath("<name>.spv")` when registering pipelines. - Use `EngineContext::getAssets()->shaderPath("<name>.spv")` when registering pipelines.
- Use sRGB formats for albedo textures and UNORM for PBR control textures (see `docs/asset_manager.md`). - Use sRGB formats for albedo textures and UNORM for PBR control textures (see `docs/asset_manager.md`).
- Material UBO layout (`GLTFMaterialData`):
- `vec4 colorFactors;`
- `vec4 metal_rough_factors; // x = metallic, y = roughness`
- `vec4 extra[14]; // extra[0].x = normalScale`
- Material texture bindings (set=1):
- binding=1 `colorTex`, binding=2 `metalRoughTex`, binding=3 `normalMap`.
- Adding a pipeline (graphics) - Adding a pipeline (graphics)
- Fill `GraphicsPipelineCreateInfo` with shader paths, descriptor set layouts, optional push constants, and a `configure(PipelineBuilder&)` callback to set topology, raster, depth/blend, and attachment formats. - Fill `GraphicsPipelineCreateInfo` with shader paths, descriptor set layouts, optional push constants, and a `configure(PipelineBuilder&)` callback to set topology, raster, depth/blend, and attachment formats.

View File

@@ -24,3 +24,13 @@
- Confirm the `.spv` file changed (timestamp) and click “Reload Changed” in the Pipelines window. - Confirm the `.spv` file changed (timestamp) and click “Reload Changed” in the Pipelines window.
- Ensure you are editing the correct files referenced by `shaderPath()`. - Ensure you are editing the correct files referenced by `shaderPath()`.
- GLSL error: `no such field in structure 'materialData': extra`
- Ensure `shaders/input_structures.glsl` defines `vec4 extra[14];` inside `GLTFMaterialData` to match C++ `MaterialConstants`.
- Normals look inverted when using normal maps
- The engine expects +Y (green up) tangent-space normals. Flip the green channel in your texture if needed.
- Tangent seams or artifacts
- Build with MikkTSpace enabled: `-DENABLE_MIKKTS=ON`.
- Check that your mesh has non-degenerate UVs.

View File

@@ -40,9 +40,11 @@ Use either the convenience descriptor (`MeshCreateInfo`) or the direct overload
struct AssetManager::MaterialOptions { struct AssetManager::MaterialOptions {
std::string albedoPath; // resolved through AssetManager std::string albedoPath; // resolved through AssetManager
std::string metalRoughPath; // resolved through AssetManager std::string metalRoughPath; // resolved through AssetManager
std::string normalPath; // resolved through AssetManager (tangent-space normal)
bool albedoSRGB = true; // VK_FORMAT_R8G8B8A8_SRGB when true bool albedoSRGB = true; // VK_FORMAT_R8G8B8A8_SRGB when true
bool metalRoughSRGB = false; // VK_FORMAT_R8G8B8A8_UNORM when false bool metalRoughSRGB = false; // VK_FORMAT_R8G8B8A8_UNORM when false
GLTFMetallic_Roughness::MaterialConstants constants{}; bool normalSRGB = false; // normal maps should be UNORM
GLTFMetallic_Roughness::MaterialConstants constants{}; // extra[0].x as normalScale
MaterialPass pass = MaterialPass::MainColor; // or Transparent MaterialPass pass = MaterialPass::MainColor; // or Transparent
}; };
@@ -113,7 +115,7 @@ ctx->scene->addMeshInstance("sphere.instance", sphere,
glm::translate(glm::mat4(1.f), glm::vec3(2.f, 0.f, -2.f))); glm::translate(glm::mat4(1.f), glm::vec3(2.f, 0.f, -2.f)));
``` ```
Textured primitive (albedo + metal-rough): Textured primitive (albedo + metal-rough + normal):
```c++ ```c++
AssetManager::MeshCreateInfo ti{}; AssetManager::MeshCreateInfo ti{};
@@ -124,7 +126,9 @@ ti.geometry.vertices = std::span<Vertex>(v.data(), v.size());
ti.geometry.indices = std::span<uint32_t>(i.data(), i.size()); ti.geometry.indices = std::span<uint32_t>(i.data(), i.size());
ti.material.kind = AssetManager::MeshMaterialDesc::Kind::Textured; ti.material.kind = AssetManager::MeshMaterialDesc::Kind::Textured;
ti.material.options.albedoPath = "textures/ground_albedo.png"; // sRGB ti.material.options.albedoPath = "textures/ground_albedo.png"; // sRGB
ti.material.options.metalRoughPath = "textures/ground_mr.png"; // UNORM ti.material.options.metalRoughPath = "textures/ground_mr.png"; // UNORM, G=roughness, B=metallic
ti.material.options.normalPath = "textures/ground_n.png"; // UNORM
ti.material.options.constants.extra[0].x = 1.0f; // normalScale
// ti.material.options.pass = MaterialPass::Transparent; // optional // ti.material.options.pass = MaterialPass::Transparent; // optional
auto texturedPlane = ctx->getAssets()->createMesh(ti); auto texturedPlane = ctx->getAssets()->createMesh(ti);
@@ -156,4 +160,5 @@ ctx->scene->removeGLTFInstance("chair01");
- Reuse by name: `createMesh("name", ...)` returns the cached mesh if it already exists. Use a unique name or call `removeMesh(name)` to replace. - Reuse by name: `createMesh("name", ...)` returns the cached mesh if it already exists. Use a unique name or call `removeMesh(name)` to replace.
- sRGB/UNORM: Albedo is sRGB by default, metal-rough is UNORM by default. Adjust via `MaterialOptions`. - sRGB/UNORM: Albedo is sRGB by default, metal-rough is UNORM by default. Adjust via `MaterialOptions`.
- Hot reload: Shaders are resolved via `shaderPath()`; pipeline hot reload is handled by the pipeline manager, not the AssetManager. - Hot reload: Shaders are resolved via `shaderPath()`; pipeline hot reload is handled by the pipeline manager, not the AssetManager.
- Normal maps: Not wired into the default GLTF PBR material in this branch. Adding them would require descriptor and shader updates. - Normal maps: Supported. If `normalPath` is empty, a flat normal is used.
- Tangents: Loaded from glTF when present; otherwise generated. Enable MikkTSpace at configure time with `-DENABLE_MIKKTS=ON`.

43
docs/materials.md Normal file
View File

@@ -0,0 +1,43 @@
Materials and Textures Overview (PBR)
Current state (as of Nov 1, 2025)
- PBR textures bound per material (set=1):
- binding=0: GLTFMaterialData (UBO)
- binding=1: `colorTex` (albedo/base color) — sRGB
- binding=2: `metalRoughTex` (G=roughness, B=metallic) — UNORM
- binding=3: `normalMap` (tangent-space normal, UNORM)
- GBuffer writes worldspace normals. Tangentspace normal maps are decoded with TBN using a signcorrect bitangent (B = sign * cross(N, T)).
- Numeric fallbacks via `MaterialConstants` (CPU) / `GLTFMaterialData` (GPU):
- `colorFactors` (RGBA). Defaults to 1 if zero.
- `metal_rough_factors` (X=metallic, Y=roughness). Roughness is clamped to ≥ 0.04 in shaders.
- `extra[0].x` = `normalScale` (scalar, default 1.0). Multiplies the XY of decoded normal.
- Defaults when a texture is missing:
- Albedo → checkerboard error texture
- MR → white (no effect)
- Normal → 1×1 flat normal (0.5, 0.5, 1.0)
Implications for primitive meshes
- Primitives can use:
- Albedo + MR + Normal textures, or
- Numeric factors only, or
- Any mix (missing textures fall back to defaults).
Texture conventions
- Albedo/base color: sRGB.
- Metallic-Roughness: UNORM; channels: G=roughness, B=metallic.
- Normal map: UNORM; expected +Y (green up). If your maps look inverted, flip the green channel offline.
Notes on tangent space
- Tangents are loaded from glTF when present (`TANGENT` attribute, vec4 where w is handedness).
- If missing, the engine generates tangents:
- Default: robust GramSchmidt with handedness.
- Preferred: MikkTSpace (enabled by CMake option `ENABLE_MIKKTS=ON`).
Notes on MikkTSpace
- Recommended for parity with content tools. Enable at configure time: `-DENABLE_MIKKTS=ON`.
- Falls back automatically to the internal generator if disabled or if MikkTS fails.
Usage Examples
- Adjust normal strength per material: set `material.constants.extra[0].x` (CPU) or `normalTexture.scale` in glTF.
- Primitive with PBR textures:
- Set `MeshMaterialDesc::Kind::Textured` and fill `albedoPath`, `metalRoughPath`, and `normalPath`.

View File

@@ -6,6 +6,7 @@ layout(location = 0) in vec3 inNormal;
layout(location = 1) in vec3 inColor; layout(location = 1) in vec3 inColor;
layout(location = 2) in vec2 inUV; layout(location = 2) in vec2 inUV;
layout(location = 3) in vec3 inWorldPos; layout(location = 3) in vec3 inWorldPos;
layout(location = 4) in vec4 inTangent;
layout(location = 0) out vec4 outPos; layout(location = 0) out vec4 outPos;
layout(location = 1) out vec4 outNorm; layout(location = 1) out vec4 outNorm;
@@ -20,7 +21,17 @@ void main() {
float roughness = clamp(mrTex.x * materialData.metal_rough_factors.y, 0.04, 1.0); float roughness = clamp(mrTex.x * materialData.metal_rough_factors.y, 0.04, 1.0);
float metallic = clamp(mrTex.y * materialData.metal_rough_factors.x, 0.0, 1.0); float metallic = clamp(mrTex.y * materialData.metal_rough_factors.x, 0.0, 1.0);
// Normal mapping: decode tangent-space normal and transform to world space
// Expect UNORM normal map (not sRGB). Flat fallback is (0.5, 0.5, 1.0).
vec3 Nm = texture(normalMap, inUV).xyz * 2.0 - 1.0;
float normalScale = max(materialData.extra[0].x, 0.0);
Nm.xy *= normalScale;
vec3 N = normalize(inNormal);
vec3 T = normalize(inTangent.xyz);
vec3 B = normalize(cross(N, T)) * inTangent.w;
vec3 Nw = normalize(T * Nm.x + B * Nm.y + N * Nm.z);
outPos = vec4(inWorldPos, 1.0); outPos = vec4(inWorldPos, 1.0);
outNorm = vec4(normalize(inNormal), roughness); outNorm = vec4(Nw, roughness);
outAlbedo = vec4(albedo, metallic); outAlbedo = vec4(albedo, metallic);
} }

View File

@@ -28,8 +28,10 @@ layout(set = 1, binding = 0) uniform GLTFMaterialData{
vec4 colorFactors; vec4 colorFactors;
vec4 metal_rough_factors; vec4 metal_rough_factors;
vec4 extra[14];
} materialData; } materialData;
layout(set = 1, binding = 1) uniform sampler2D colorTex; layout(set = 1, binding = 1) uniform sampler2D colorTex;
layout(set = 1, binding = 2) uniform sampler2D metalRoughTex; layout(set = 1, binding = 2) uniform sampler2D metalRoughTex;
layout(set = 1, binding = 3) uniform sampler2D normalMap; // tangent-space normal, UNORM

View File

@@ -7,6 +7,7 @@ layout (location = 0) in vec3 inNormal;
layout (location = 1) in vec3 inColor; layout (location = 1) in vec3 inColor;
layout (location = 2) in vec2 inUV; layout (location = 2) in vec2 inUV;
layout (location = 3) in vec3 inWorldPos; layout (location = 3) in vec3 inWorldPos;
layout (location = 4) in vec4 inTangent;
layout (location = 0) out vec4 outFragColor; layout (location = 0) out vec4 outFragColor;
@@ -57,7 +58,14 @@ void main()
float roughness = clamp(mrTex.x * materialData.metal_rough_factors.y, 0.04, 1.0); float roughness = clamp(mrTex.x * materialData.metal_rough_factors.y, 0.04, 1.0);
float metallic = clamp(mrTex.y * materialData.metal_rough_factors.x, 0.0, 1.0); float metallic = clamp(mrTex.y * materialData.metal_rough_factors.x, 0.0, 1.0);
vec3 N = normalize(inNormal); // Normal mapping path for forward/transparent pipeline
vec3 Nm = texture(normalMap, inUV).xyz * 2.0 - 1.0;
float normalScale = max(materialData.extra[0].x, 0.0);
Nm.xy *= normalScale;
vec3 Nn = normalize(inNormal);
vec3 T = normalize(inTangent.xyz);
vec3 B = normalize(cross(Nn, T)) * inTangent.w;
vec3 N = normalize(T * Nm.x + B * Nm.y + Nn * Nm.z);
vec3 camPos = vec3(inverse(sceneData.view)[3]); vec3 camPos = vec3(inverse(sceneData.view)[3]);
vec3 V = normalize(camPos - inWorldPos); vec3 V = normalize(camPos - inWorldPos);
vec3 L = normalize(-sceneData.sunlightDirection.xyz); vec3 L = normalize(-sceneData.sunlightDirection.xyz);

View File

@@ -9,6 +9,7 @@ layout (location = 0) out vec3 outNormal;
layout (location = 1) out vec3 outColor; layout (location = 1) out vec3 outColor;
layout (location = 2) out vec2 outUV; layout (location = 2) out vec2 outUV;
layout (location = 3) out vec3 outWorldPos; layout (location = 3) out vec3 outWorldPos;
layout (location = 4) out vec4 outTangent; // xyz: world tangent, w: sign
struct Vertex { struct Vertex {
@@ -17,6 +18,7 @@ struct Vertex {
vec3 normal; vec3 normal;
float uv_y; float uv_y;
vec4 color; vec4 color;
vec4 tangent;
}; };
layout(buffer_reference, std430) readonly buffer VertexBuffer{ layout(buffer_reference, std430) readonly buffer VertexBuffer{
@@ -38,6 +40,8 @@ void main()
gl_Position = sceneData.viewproj * worldPos; gl_Position = sceneData.viewproj * worldPos;
outNormal = (PushConstants.render_matrix * vec4(v.normal, 0.f)).xyz; outNormal = (PushConstants.render_matrix * vec4(v.normal, 0.f)).xyz;
vec3 worldTangent = (PushConstants.render_matrix * vec4(v.tangent.xyz, 0.f)).xyz;
outTangent = vec4(normalize(worldTangent), v.tangent.w);
// Pass pure vertex color; apply baseColorFactor only in fragment // Pass pure vertex color; apply baseColorFactor only in fragment
outColor = v.color.xyz; outColor = v.color.xyz;
outUV.x = v.uv_x; outUV.x = v.uv_x;

View File

@@ -9,6 +9,7 @@ struct Vertex {
vec3 position; float uv_x; vec3 position; float uv_x;
vec3 normal; float uv_y; vec3 normal; float uv_y;
vec4 color; vec4 color;
vec4 tangent;
}; };
layout(buffer_reference, std430) readonly buffer VertexBuffer{ layout(buffer_reference, std430) readonly buffer VertexBuffer{

View File

@@ -72,6 +72,8 @@ add_executable (vulkan_engine
scene/vk_scene.cpp scene/vk_scene.cpp
scene/vk_loader.h scene/vk_loader.h
scene/vk_loader.cpp scene/vk_loader.cpp
scene/tangent_space.h
scene/tangent_space.cpp
scene/camera.h scene/camera.h
scene/camera.cpp scene/camera.cpp
# compute # compute
@@ -89,7 +91,13 @@ target_include_directories(vulkan_engine PUBLIC
"${CMAKE_CURRENT_SOURCE_DIR}/compute" "${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) 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) add_library(vma_impl OBJECT vma_impl.cpp)
target_include_directories(vma_impl PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/../third_party/vma") 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 <core/vk_resource.h>
#include <render/vk_materials.h> #include <render/vk_materials.h>
#include <render/primitives.h> #include <render/primitives.h>
#include <scene/tangent_space.h>
#include <stb_image.h> #include <stb_image.h>
#include "asset_locator.h" #include "asset_locator.h"
@@ -142,6 +143,12 @@ std::shared_ptr<MeshAsset> AssetManager::createMesh(const MeshCreateInfo &info)
break; 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) if (info.material.kind == MeshMaterialDesc::Kind::Default)
{ {
return createMesh(info.name, vertsSpan, indsSpan, {}); 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 [albedo, createdAlbedo] = loadImageFromAsset(opt.albedoPath, opt.albedoSRGB);
auto [mr, createdMR] = loadImageFromAsset(opt.metalRoughPath, opt.metalRoughSRGB); 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 &albedoRef = createdAlbedo ? albedo : _engine->_errorCheckerboardImage;
const AllocatedImage &mrRef = createdMR ? mr : _engine->_whiteImage; const AllocatedImage &mrRef = createdMR ? mr : _engine->_whiteImage;
const AllocatedImage &normRef = createdNormal ? normal : _engine->_flatNormalImage;
AllocatedBuffer matBuffer = createMaterialBufferWithConstants(opt.constants); AllocatedBuffer matBuffer = createMaterialBufferWithConstants(opt.constants);
@@ -162,6 +171,8 @@ std::shared_ptr<MeshAsset> AssetManager::createMesh(const MeshCreateInfo &info)
res.colorSampler = _engine->_samplerManager->defaultLinear(); res.colorSampler = _engine->_samplerManager->defaultLinear();
res.metalRoughImage = mrRef; res.metalRoughImage = mrRef;
res.metalRoughSampler = _engine->_samplerManager->defaultLinear(); res.metalRoughSampler = _engine->_samplerManager->defaultLinear();
res.normalImage = normRef;
res.normalSampler = _engine->_samplerManager->defaultLinear();
res.dataBuffer = matBuffer.buffer; res.dataBuffer = matBuffer.buffer;
res.dataBufferOffset = 0; res.dataBufferOffset = 0;
@@ -171,6 +182,7 @@ std::shared_ptr<MeshAsset> AssetManager::createMesh(const MeshCreateInfo &info)
_meshMaterialBuffers.emplace(info.name, matBuffer); _meshMaterialBuffers.emplace(info.name, matBuffer);
if (createdAlbedo) _meshOwnedImages[info.name].push_back(albedo); if (createdAlbedo) _meshOwnedImages[info.name].push_back(albedo);
if (createdMR) _meshOwnedImages[info.name].push_back(mr); if (createdMR) _meshOwnedImages[info.name].push_back(mr);
if (createdNormal) _meshOwnedImages[info.name].push_back(normal);
return mesh; return mesh;
} }
@@ -213,6 +225,10 @@ AllocatedBuffer AssetManager::createMaterialBufferWithConstants(
{ {
matConstants->colorFactors = glm::vec4(1.0f); 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 // Ensure writes are visible on non-coherent memory
vmaFlushAllocation(_engine->_deviceManager->allocator(), matBuffer.allocation, 0, vmaFlushAllocation(_engine->_deviceManager->allocator(), matBuffer.allocation, 0,
sizeof(GLTFMetallic_Roughness::MaterialConstants)); sizeof(GLTFMetallic_Roughness::MaterialConstants));
@@ -270,6 +286,8 @@ std::shared_ptr<MeshAsset> AssetManager::createMesh(const std::string &name,
matResources.colorSampler = _engine->_samplerManager->defaultLinear(); matResources.colorSampler = _engine->_samplerManager->defaultLinear();
matResources.metalRoughImage = _engine->_whiteImage; matResources.metalRoughImage = _engine->_whiteImage;
matResources.metalRoughSampler = _engine->_samplerManager->defaultLinear(); matResources.metalRoughSampler = _engine->_samplerManager->defaultLinear();
matResources.normalImage = _engine->_flatNormalImage;
matResources.normalSampler = _engine->_samplerManager->defaultLinear();
AllocatedBuffer matBuffer = createMaterialBufferWithConstants({}); AllocatedBuffer matBuffer = createMaterialBufferWithConstants({});
matResources.dataBuffer = matBuffer.buffer; matResources.dataBuffer = matBuffer.buffer;

View File

@@ -25,9 +25,13 @@ public:
{ {
std::string albedoPath; std::string albedoPath;
std::string metalRoughPath; 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 albedoSRGB = true;
bool metalRoughSRGB = false; bool metalRoughSRGB = false;
bool normalSRGB = false; // normal maps are typically non-sRGB
GLTFMetallic_Roughness::MaterialConstants constants{}; GLTFMetallic_Roughness::MaterialConstants constants{};

View File

@@ -51,6 +51,268 @@
#include "core/vk_pipeline_manager.h" #include "core/vk_pipeline_manager.h"
#include "core/config.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; VulkanEngine *loadedEngine = nullptr;
static void print_vma_stats(DeviceManager* dev, const char* tag) static void print_vma_stats(DeviceManager* dev, const char* tag)
@@ -190,7 +452,7 @@ void VulkanEngine::init()
auto imguiPass = std::make_unique<ImGuiPass>(); auto imguiPass = std::make_unique<ImGuiPass>();
_renderPassManager->setImGuiPass(std::move(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); const auto structureFile = _assetManager->loadGLTF(structurePath);
assert(structureFile.has_value()); 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, _blackImage = _resourceManager->create_image((void *) &black, VkExtent3D{1, 1, 1}, VK_FORMAT_R8G8B8A8_UNORM,
VK_IMAGE_USAGE_SAMPLED_BIT); 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 //checkerboard image
uint32_t magenta = glm::packUnorm4x8(glm::vec4(1, 0, 1, 1)); uint32_t magenta = glm::packUnorm4x8(glm::vec4(1, 0, 1, 1));
std::array<uint32_t, 16 * 16> pixels{}; //for 16x16 checkerboard texture 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(_greyImage);
_resourceManager->destroy_image(_blackImage); _resourceManager->destroy_image(_blackImage);
_resourceManager->destroy_image(_errorCheckerboardImage); _resourceManager->destroy_image(_errorCheckerboardImage);
_resourceManager->destroy_image(_flatNormalImage);
}); });
//< default_img //< default_img
} }
@@ -581,272 +849,51 @@ void VulkanEngine::run()
ImGui::NewFrame(); ImGui::NewFrame();
if (ImGui::Begin("background")) // Consolidated debug window with tabs
if (ImGui::Begin("Debug"))
{ {
auto background_pass = _renderPassManager->getPass<BackgroundPass>(); const ImGuiTabBarFlags tf = ImGuiTabBarFlags_Reorderable | ImGuiTabBarFlags_AutoSelectNewTabs;
ComputeEffect &selected = background_pass->_backgroundEffects[background_pass->_currentEffect]; if (ImGui::BeginTabBar("DebugTabs", tf))
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)
{ {
bool on = (ss.hybridRayCascadesMask >> i) & 1u; if (ImGui::BeginTabItem("Overview"))
std::string label = std::string("C") + std::to_string(i);
if (ImGui::Checkbox(label.c_str(), &on))
{ {
if (on) ss.hybridRayCascadesMask |= (1u << i); ui_overview(this);
else ss.hybridRayCascadesMask &= ~(1u << i); ImGui::EndTabItem();
} }
if (i != 3) ImGui::SameLine(); if (ImGui::BeginTabItem("Background"))
}
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))
{ {
ImGui::TableSetupColumn("Enable", ImGuiTableColumnFlags_WidthFixed, 70); ui_background(this);
ImGui::TableSetupColumn("Name"); ImGui::EndTabItem();
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();
} }
if (ImGui::BeginTabItem("Shadows"))
if (ImGui::CollapsingHeader("Images", ImGuiTreeNodeFlags_DefaultOpen))
{ {
std::vector<RenderGraph::RGDebugImageInfo> imgs; ui_shadows(this);
graph.debug_get_images(imgs); ImGui::EndTabItem();
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::BeginTabItem("Render Graph"))
if (ImGui::CollapsingHeader("Buffers"))
{ {
std::vector<RenderGraph::RGDebugBufferInfo> bufs; ui_render_graph(this);
graph.debug_get_buffers(bufs); ImGui::EndTabItem();
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();
}
} }
} if (ImGui::BeginTabItem("Pipelines"))
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))
{ {
ImGui::TableSetupColumn("Name"); ui_pipelines(this);
ImGui::TableSetupColumn("VS"); ImGui::EndTabItem();
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();
} }
if (ImGui::BeginTabItem("PostFX"))
{
ui_postfx(this);
ImGui::EndTabItem();
}
if (ImGui::BeginTabItem("Scene"))
{
ui_scene(this);
ImGui::EndTabItem();
}
ImGui::EndTabBar();
} }
ImGui::End(); 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(); ImGui::Render();
draw(); draw();

View File

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

View File

@@ -107,6 +107,8 @@ struct Vertex {
glm::vec3 normal; glm::vec3 normal;
float uv_y; float uv_y;
glm::vec4 color; 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 // 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) { for (auto& f : faces) {
uint32_t start = (uint32_t)vertices.size(); uint32_t start = (uint32_t)vertices.size();
Vertex v0{f.v0, 0, f.normal, 0, 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)}; 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)}; 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)}; 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(v0);
vertices.push_back(v1); vertices.push_back(v1);
vertices.push_back(v2); 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_x = u;
vert.uv_y = 1.0f - v; vert.uv_y = 1.0f - v;
vert.color = glm::vec4(1.0f); vert.color = glm::vec4(1.0f);
vert.tangent = glm::vec4(1,0,0,1);
vertices.push_back(vert); 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(0, VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER);
layoutBuilder.add_binding(1, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER); layoutBuilder.add_binding(1, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER);
layoutBuilder.add_binding(2, 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(), materialLayout = layoutBuilder.build(engine->_deviceManager->device(),
VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT, 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); VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER);
writer.write_image(2, resources.metalRoughImage.imageView, resources.metalRoughSampler, writer.write_image(2, resources.metalRoughImage.imageView, resources.metalRoughSampler,
VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER); 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); writer.update_set(device, matData.materialSet);

View File

@@ -27,6 +27,8 @@ struct GLTFMetallic_Roughness
VkSampler colorSampler; VkSampler colorSampler;
AllocatedImage metalRoughImage; AllocatedImage metalRoughImage;
VkSampler metalRoughSampler; VkSampler metalRoughSampler;
AllocatedImage normalImage;
VkSampler normalSampler;
VkBuffer dataBuffer; VkBuffer dataBuffer;
uint32_t dataBufferOffset; 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/tools.hpp>
#include <fastgltf/util.hpp> #include <fastgltf/util.hpp>
#include <optional> #include <optional>
#include "tangent_space.h"
//> loadimg //> loadimg
std::optional<AllocatedImage> load_image(VulkanEngine *engine, fastgltf::Asset &asset, fastgltf::Image &image, bool srgb) 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; file.materials[mat.name.c_str()] = newMat;
GLTFMetallic_Roughness::MaterialConstants constants; GLTFMetallic_Roughness::MaterialConstants constants;
// Defaults
constants.extra[0].x = 1.0f; // normalScale
constants.colorFactors.x = mat.pbrData.baseColorFactor[0]; constants.colorFactors.x = mat.pbrData.baseColorFactor[0];
constants.colorFactors.y = mat.pbrData.baseColorFactor[1]; constants.colorFactors.y = mat.pbrData.baseColorFactor[1];
constants.colorFactors.z = mat.pbrData.baseColorFactor[2]; 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.colorSampler = engine->_samplerManager->defaultLinear();
materialResources.metalRoughImage = engine->_whiteImage; materialResources.metalRoughImage = engine->_whiteImage;
materialResources.metalRoughSampler = engine->_samplerManager->defaultLinear(); materialResources.metalRoughSampler = engine->_samplerManager->defaultLinear();
materialResources.normalImage = engine->_flatNormalImage;
materialResources.normalSampler = engine->_samplerManager->defaultLinear();
// set the uniform buffer for the material data // set the uniform buffer for the material data
materialResources.dataBuffer = file.materialDataBuffer.buffer; materialResources.dataBuffer = file.materialDataBuffer.buffer;
@@ -385,6 +390,39 @@ std::optional<std::shared_ptr<LoadedGLTF> > loadGltf(VulkanEngine *engine, std::
: engine->_samplerManager->defaultLinear(); : 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 // build material
newMat->data = engine->metalRoughMaterial.write_material(engine->_deviceManager->device(), passType, materialResources, newMat->data = engine->metalRoughMaterial.write_material(engine->_deviceManager->device(), passType, materialResources,
file.descriptorPool); file.descriptorPool);
@@ -442,12 +480,13 @@ std::optional<std::shared_ptr<LoadedGLTF> > loadGltf(VulkanEngine *engine, std::
fastgltf::iterateAccessorWithIndex<glm::vec3>(gltf, posAccessor, fastgltf::iterateAccessorWithIndex<glm::vec3>(gltf, posAccessor,
[&](glm::vec3 v, size_t index) { [&](glm::vec3 v, size_t index) {
Vertex newvtx; Vertex newvtx{};
newvtx.position = v; newvtx.position = v;
newvtx.normal = {1, 0, 0}; newvtx.normal = {1, 0, 0};
newvtx.color = glm::vec4{1.f}; newvtx.color = glm::vec4{1.f};
newvtx.uv_x = 0; newvtx.uv_x = 0;
newvtx.uv_y = 0; newvtx.uv_y = 0;
newvtx.tangent = glm::vec4(1,0,0,1);
vertices[initial_vtx + index] = newvtx; 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()) if (p.materialIndex.has_value())
{ {
newSurface.material = materials[p.materialIndex.value()]; newSurface.material = materials[p.materialIndex.value()];

View File

@@ -50,3 +50,7 @@ target_sources(imgui PRIVATE
target_link_libraries(imgui PUBLIC Vulkan::Vulkan SDL2::SDL2) target_link_libraries(imgui PUBLIC Vulkan::Vulkan SDL2::SDL2)
target_include_directories(stb_image INTERFACE stb_image) target_include_directories(stb_image INTERFACE stb_image)
# MikkTSpace (optional)
add_library(mikktspace STATIC MikkTSpace/mikktspace.c)
target_include_directories(mikktspace PUBLIC MikkTSpace)

4
third_party/MikkTSpace/README.md vendored Normal file
View File

@@ -0,0 +1,4 @@
# MikkTSpace
A common standard for tangent space used in baking tools to produce normal maps.
More information can be found at http://www.mikktspace.com/.

1899
third_party/MikkTSpace/mikktspace.c vendored Normal file

File diff suppressed because it is too large Load Diff

145
third_party/MikkTSpace/mikktspace.h vendored Normal file
View File

@@ -0,0 +1,145 @@
/** \file mikktspace/mikktspace.h
* \ingroup mikktspace
*/
/**
* Copyright (C) 2011 by Morten S. Mikkelsen
*
* This software is provided 'as-is', without any express or implied
* warranty. In no event will the authors be held liable for any damages
* arising from the use of this software.
*
* Permission is granted to anyone to use this software for any purpose,
* including commercial applications, and to alter it and redistribute it
* freely, subject to the following restrictions:
*
* 1. The origin of this software must not be misrepresented; you must not
* claim that you wrote the original software. If you use this software
* in a product, an acknowledgment in the product documentation would be
* appreciated but is not required.
* 2. Altered source versions must be plainly marked as such, and must not be
* misrepresented as being the original software.
* 3. This notice may not be removed or altered from any source distribution.
*/
#ifndef __MIKKTSPACE_H__
#define __MIKKTSPACE_H__
#ifdef __cplusplus
extern "C" {
#endif
/* Author: Morten S. Mikkelsen
* Version: 1.0
*
* The files mikktspace.h and mikktspace.c are designed to be
* stand-alone files and it is important that they are kept this way.
* Not having dependencies on structures/classes/libraries specific
* to the program, in which they are used, allows them to be copied
* and used as is into any tool, program or plugin.
* The code is designed to consistently generate the same
* tangent spaces, for a given mesh, in any tool in which it is used.
* This is done by performing an internal welding step and subsequently an order-independent evaluation
* of tangent space for meshes consisting of triangles and quads.
* This means faces can be received in any order and the same is true for
* the order of vertices of each face. The generated result will not be affected
* by such reordering. Additionally, whether degenerate (vertices or texture coordinates)
* primitives are present or not will not affect the generated results either.
* Once tangent space calculation is done the vertices of degenerate primitives will simply
* inherit tangent space from neighboring non degenerate primitives.
* The analysis behind this implementation can be found in my master's thesis
* which is available for download --> http://image.diku.dk/projects/media/morten.mikkelsen.08.pdf
* Note that though the tangent spaces at the vertices are generated in an order-independent way,
* by this implementation, the interpolated tangent space is still affected by which diagonal is
* chosen to split each quad. A sensible solution is to have your tools pipeline always
* split quads by the shortest diagonal. This choice is order-independent and works with mirroring.
* If these have the same length then compare the diagonals defined by the texture coordinates.
* XNormal which is a tool for baking normal maps allows you to write your own tangent space plugin
* and also quad triangulator plugin.
*/
typedef int tbool;
typedef struct SMikkTSpaceContext SMikkTSpaceContext;
typedef struct {
// Returns the number of faces (triangles/quads) on the mesh to be processed.
int (*m_getNumFaces)(const SMikkTSpaceContext * pContext);
// Returns the number of vertices on face number iFace
// iFace is a number in the range {0, 1, ..., getNumFaces()-1}
int (*m_getNumVerticesOfFace)(const SMikkTSpaceContext * pContext, const int iFace);
// returns the position/normal/texcoord of the referenced face of vertex number iVert.
// iVert is in the range {0,1,2} for triangles and {0,1,2,3} for quads.
void (*m_getPosition)(const SMikkTSpaceContext * pContext, float fvPosOut[], const int iFace, const int iVert);
void (*m_getNormal)(const SMikkTSpaceContext * pContext, float fvNormOut[], const int iFace, const int iVert);
void (*m_getTexCoord)(const SMikkTSpaceContext * pContext, float fvTexcOut[], const int iFace, const int iVert);
// either (or both) of the two setTSpace callbacks can be set.
// The call-back m_setTSpaceBasic() is sufficient for basic normal mapping.
// This function is used to return the tangent and fSign to the application.
// fvTangent is a unit length vector.
// For normal maps it is sufficient to use the following simplified version of the bitangent which is generated at pixel/vertex level.
// bitangent = fSign * cross(vN, tangent);
// Note that the results are returned unindexed. It is possible to generate a new index list
// But averaging/overwriting tangent spaces by using an already existing index list WILL produce INCRORRECT results.
// DO NOT! use an already existing index list.
void (*m_setTSpaceBasic)(const SMikkTSpaceContext * pContext, const float fvTangent[], const float fSign, const int iFace, const int iVert);
// This function is used to return tangent space results to the application.
// fvTangent and fvBiTangent are unit length vectors and fMagS and fMagT are their
// true magnitudes which can be used for relief mapping effects.
// fvBiTangent is the "real" bitangent and thus may not be perpendicular to fvTangent.
// However, both are perpendicular to the vertex normal.
// For normal maps it is sufficient to use the following simplified version of the bitangent which is generated at pixel/vertex level.
// fSign = bIsOrientationPreserving ? 1.0f : (-1.0f);
// bitangent = fSign * cross(vN, tangent);
// Note that the results are returned unindexed. It is possible to generate a new index list
// But averaging/overwriting tangent spaces by using an already existing index list WILL produce INCRORRECT results.
// DO NOT! use an already existing index list.
void (*m_setTSpace)(const SMikkTSpaceContext * pContext, const float fvTangent[], const float fvBiTangent[], const float fMagS, const float fMagT,
const tbool bIsOrientationPreserving, const int iFace, const int iVert);
} SMikkTSpaceInterface;
struct SMikkTSpaceContext
{
SMikkTSpaceInterface * m_pInterface; // initialized with callback functions
void * m_pUserData; // pointer to client side mesh data etc. (passed as the first parameter with every interface call)
};
// these are both thread safe!
tbool genTangSpaceDefault(const SMikkTSpaceContext * pContext); // Default (recommended) fAngularThreshold is 180 degrees (which means threshold disabled)
tbool genTangSpace(const SMikkTSpaceContext * pContext, const float fAngularThreshold);
// To avoid visual errors (distortions/unwanted hard edges in lighting), when using sampled normal maps, the
// normal map sampler must use the exact inverse of the pixel shader transformation.
// The most efficient transformation we can possibly do in the pixel shader is
// achieved by using, directly, the "unnormalized" interpolated tangent, bitangent and vertex normal: vT, vB and vN.
// pixel shader (fast transform out)
// vNout = normalize( vNt.x * vT + vNt.y * vB + vNt.z * vN );
// where vNt is the tangent space normal. The normal map sampler must likewise use the
// interpolated and "unnormalized" tangent, bitangent and vertex normal to be compliant with the pixel shader.
// sampler does (exact inverse of pixel shader):
// float3 row0 = cross(vB, vN);
// float3 row1 = cross(vN, vT);
// float3 row2 = cross(vT, vB);
// float fSign = dot(vT, row0)<0 ? -1 : 1;
// vNt = normalize( fSign * float3(dot(vNout,row0), dot(vNout,row1), dot(vNout,row2)) );
// where vNout is the sampled normal in some chosen 3D space.
//
// Should you choose to reconstruct the bitangent in the pixel shader instead
// of the vertex shader, as explained earlier, then be sure to do this in the normal map sampler also.
// Finally, beware of quad triangulations. If the normal map sampler doesn't use the same triangulation of
// quads as your renderer then problems will occur since the interpolated tangent spaces will differ
// eventhough the vertex level tangent spaces match. This can be solved either by triangulating before
// sampling/exporting or by using the order-independent choice of diagonal for splitting quads suggested earlier.
// However, this must be used both by the sampler and your tools/rendering pipeline.
#ifdef __cplusplus
}
#endif
#endif