ADD: Normal mapping
This commit is contained in:
@@ -6,7 +6,7 @@ Work-In-Progress Vulkan render engine
|
||||
Current structure:
|
||||
- Flexible render graph system with multiple render passes
|
||||
- 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.
|
||||
|
||||
Work-In-Progress
|
||||
|
||||
@@ -20,4 +20,8 @@
|
||||
|
||||
- Materials & sRGB
|
||||
- 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)
|
||||
|
||||
|
||||
@@ -32,6 +32,11 @@
|
||||
- Third‑party deps
|
||||
- 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 built‑in Gram–Schmidt generator: `-DENABLE_MIKKTS=OFF`.
|
||||
|
||||
- Validation Layers
|
||||
- Enabled in Debug (`kUseValidationLayers = true` in `src/core/config.h`).
|
||||
- Disable by building Release or toggling the flag during local experimentation.
|
||||
|
||||
@@ -18,6 +18,12 @@
|
||||
- Name SPIR‑V files with full extension, e.g. `fullscreen.vert.spv`, `deferred_lighting.frag.spv`.
|
||||
- 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`).
|
||||
- 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)
|
||||
- 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.
|
||||
|
||||
@@ -24,3 +24,13 @@
|
||||
- Confirm the `.spv` file changed (timestamp) and click “Reload Changed” in the Pipelines window.
|
||||
- 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.
|
||||
|
||||
|
||||
@@ -40,9 +40,11 @@ Use either the convenience descriptor (`MeshCreateInfo`) or the direct overload
|
||||
struct AssetManager::MaterialOptions {
|
||||
std::string albedoPath; // 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 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
|
||||
};
|
||||
|
||||
@@ -113,7 +115,7 @@ ctx->scene->addMeshInstance("sphere.instance", sphere,
|
||||
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++
|
||||
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.material.kind = AssetManager::MeshMaterialDesc::Kind::Textured;
|
||||
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
|
||||
|
||||
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.
|
||||
- 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.
|
||||
- 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
43
docs/materials.md
Normal 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)
|
||||
- G‑Buffer writes world‑space normals. Tangent‑space normal maps are decoded with TBN using a sign‑correct 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 Gram–Schmidt 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`.
|
||||
@@ -6,6 +6,7 @@ layout(location = 0) in vec3 inNormal;
|
||||
layout(location = 1) in vec3 inColor;
|
||||
layout(location = 2) in vec2 inUV;
|
||||
layout(location = 3) in vec3 inWorldPos;
|
||||
layout(location = 4) in vec4 inTangent;
|
||||
|
||||
layout(location = 0) out vec4 outPos;
|
||||
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 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);
|
||||
outNorm = vec4(normalize(inNormal), roughness);
|
||||
outNorm = vec4(Nw, roughness);
|
||||
outAlbedo = vec4(albedo, metallic);
|
||||
}
|
||||
|
||||
@@ -28,8 +28,10 @@ layout(set = 1, binding = 0) uniform GLTFMaterialData{
|
||||
|
||||
vec4 colorFactors;
|
||||
vec4 metal_rough_factors;
|
||||
vec4 extra[14];
|
||||
|
||||
} materialData;
|
||||
|
||||
layout(set = 1, binding = 1) uniform sampler2D colorTex;
|
||||
layout(set = 1, binding = 2) uniform sampler2D metalRoughTex;
|
||||
layout(set = 1, binding = 3) uniform sampler2D normalMap; // tangent-space normal, UNORM
|
||||
|
||||
@@ -7,6 +7,7 @@ layout (location = 0) in vec3 inNormal;
|
||||
layout (location = 1) in vec3 inColor;
|
||||
layout (location = 2) in vec2 inUV;
|
||||
layout (location = 3) in vec3 inWorldPos;
|
||||
layout (location = 4) in vec4 inTangent;
|
||||
|
||||
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 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 V = normalize(camPos - inWorldPos);
|
||||
vec3 L = normalize(-sceneData.sunlightDirection.xyz);
|
||||
|
||||
@@ -9,6 +9,7 @@ layout (location = 0) out vec3 outNormal;
|
||||
layout (location = 1) out vec3 outColor;
|
||||
layout (location = 2) out vec2 outUV;
|
||||
layout (location = 3) out vec3 outWorldPos;
|
||||
layout (location = 4) out vec4 outTangent; // xyz: world tangent, w: sign
|
||||
|
||||
struct Vertex {
|
||||
|
||||
@@ -17,6 +18,7 @@ struct Vertex {
|
||||
vec3 normal;
|
||||
float uv_y;
|
||||
vec4 color;
|
||||
vec4 tangent;
|
||||
};
|
||||
|
||||
layout(buffer_reference, std430) readonly buffer VertexBuffer{
|
||||
@@ -38,6 +40,8 @@ void main()
|
||||
gl_Position = sceneData.viewproj * worldPos;
|
||||
|
||||
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
|
||||
outColor = v.color.xyz;
|
||||
outUV.x = v.uv_x;
|
||||
|
||||
@@ -9,6 +9,7 @@ struct Vertex {
|
||||
vec3 position; float uv_x;
|
||||
vec3 normal; float uv_y;
|
||||
vec4 color;
|
||||
vec4 tangent;
|
||||
};
|
||||
|
||||
layout(buffer_reference, std430) readonly buffer VertexBuffer{
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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{};
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -97,6 +97,7 @@ public:
|
||||
AllocatedImage _blackImage;
|
||||
AllocatedImage _greyImage;
|
||||
AllocatedImage _errorCheckerboardImage;
|
||||
AllocatedImage _flatNormalImage; // 1x1 (0.5,0.5,1.0)
|
||||
|
||||
MaterialInstance defaultData;
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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
219
src/scene/tangent_space.cpp
Normal 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
20
src/scene/tangent_space.h
Normal 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
|
||||
|
||||
@@ -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()];
|
||||
|
||||
4
third_party/CMakeLists.txt
vendored
4
third_party/CMakeLists.txt
vendored
@@ -50,3 +50,7 @@ target_sources(imgui PRIVATE
|
||||
target_link_libraries(imgui PUBLIC Vulkan::Vulkan SDL2::SDL2)
|
||||
|
||||
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
4
third_party/MikkTSpace/README.md
vendored
Normal 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
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
145
third_party/MikkTSpace/mikktspace.h
vendored
Normal 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
|
||||
Reference in New Issue
Block a user