diff --git a/Readme.md b/Readme.md index b41a005..9b06b1c 100644 --- a/Readme.md +++ b/Readme.md @@ -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 diff --git a/docs/ASSETS.md b/docs/ASSETS.md index 2021352..e36406f 100644 --- a/docs/ASSETS.md +++ b/docs/ASSETS.md @@ -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) diff --git a/docs/BUILD.md b/docs/BUILD.md index 2e68115..e11ac9f 100644 --- a/docs/BUILD.md +++ b/docs/BUILD.md @@ -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. diff --git a/docs/SHADERS.md b/docs/SHADERS.md index 81629ca..1865847 100644 --- a/docs/SHADERS.md +++ b/docs/SHADERS.md @@ -18,6 +18,12 @@ - Name SPIR‑V files with full extension, e.g. `fullscreen.vert.spv`, `deferred_lighting.frag.spv`. - Use `EngineContext::getAssets()->shaderPath(".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. diff --git a/docs/TROUBLESHOOTING.md b/docs/TROUBLESHOOTING.md index 8ce7f89..a0b0f80 100644 --- a/docs/TROUBLESHOOTING.md +++ b/docs/TROUBLESHOOTING.md @@ -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. + diff --git a/docs/asset_manager.md b/docs/asset_manager.md index 52759e1..b55662e 100644 --- a/docs/asset_manager.md +++ b/docs/asset_manager.md @@ -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(v.data(), v.size()); ti.geometry.indices = std::span(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`. diff --git a/docs/materials.md b/docs/materials.md new file mode 100644 index 0000000..80ee22b --- /dev/null +++ b/docs/materials.md @@ -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`. diff --git a/shaders/gbuffer.frag b/shaders/gbuffer.frag index 02a51bc..9283dc6 100644 --- a/shaders/gbuffer.frag +++ b/shaders/gbuffer.frag @@ -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); } diff --git a/shaders/input_structures.glsl b/shaders/input_structures.glsl index f90ee87..bf38e79 100644 --- a/shaders/input_structures.glsl +++ b/shaders/input_structures.glsl @@ -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 diff --git a/shaders/mesh.frag b/shaders/mesh.frag index 5cd0c4e..f881195 100644 --- a/shaders/mesh.frag +++ b/shaders/mesh.frag @@ -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); diff --git a/shaders/mesh.vert b/shaders/mesh.vert index fc08be5..91bec7c 100644 --- a/shaders/mesh.vert +++ b/shaders/mesh.vert @@ -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; diff --git a/shaders/shadow.vert b/shaders/shadow.vert index f1dedb6..84d12a8 100644 --- a/shaders/shadow.vert +++ b/shaders/shadow.vert @@ -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{ diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 76caa4e..f381869 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -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") diff --git a/src/core/asset_manager.cpp b/src/core/asset_manager.cpp index 428a973..b613f30 100644 --- a/src/core/asset_manager.cpp +++ b/src/core/asset_manager.cpp @@ -7,6 +7,7 @@ #include #include #include +#include #include #include "asset_locator.h" @@ -142,6 +143,12 @@ std::shared_ptr 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 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 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 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 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; diff --git a/src/core/asset_manager.h b/src/core/asset_manager.h index d78dd12..6114ffa 100644 --- a/src/core/asset_manager.h +++ b/src/core/asset_manager.h @@ -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{}; diff --git a/src/core/vk_engine.cpp b/src/core/vk_engine.cpp index 958b8e4..2db1489 100644 --- a/src/core/vk_engine.cpp +++ b/src/core/vk_engine.cpp @@ -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(); + 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(&selected.data.data1)); + ImGui::InputFloat4("data2", reinterpret_cast(&selected.data.data2)); + ImGui::InputFloat4("data3", reinterpret_cast(&selected.data.data3)); + ImGui::InputFloat4("data4", reinterpret_cast(&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(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(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 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 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 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 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() : 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(); _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 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(); - 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(&selected.data.data1)); - ImGui::InputFloat4("data2", reinterpret_cast(&selected.data.data2)); - ImGui::InputFloat4("data3", reinterpret_cast(&selected.data.data3)); - ImGui::InputFloat4("data4", reinterpret_cast(&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(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(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 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 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 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 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()) - { - 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(); diff --git a/src/core/vk_engine.h b/src/core/vk_engine.h index b845c38..a514df9 100644 --- a/src/core/vk_engine.h +++ b/src/core/vk_engine.h @@ -97,6 +97,7 @@ public: AllocatedImage _blackImage; AllocatedImage _greyImage; AllocatedImage _errorCheckerboardImage; + AllocatedImage _flatNormalImage; // 1x1 (0.5,0.5,1.0) MaterialInstance defaultData; diff --git a/src/core/vk_types.h b/src/core/vk_types.h index 6b7c183..4e34792 100644 --- a/src/core/vk_types.h +++ b/src/core/vk_types.h @@ -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 diff --git a/src/render/primitives.h b/src/render/primitives.h index 5ac8510..a0888c0 100644 --- a/src/render/primitives.h +++ b/src/render/primitives.h @@ -24,10 +24,10 @@ inline void buildCube(std::vector& vertices, std::vector& 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& vertices, std::vector& 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); } } diff --git a/src/render/vk_materials.cpp b/src/render/vk_materials.cpp index 7113531..ec22ef2 100644 --- a/src/render/vk_materials.cpp +++ b/src/render/vk_materials.cpp @@ -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); diff --git a/src/render/vk_materials.h b/src/render/vk_materials.h index 106b3e8..e76d0ee 100644 --- a/src/render/vk_materials.h +++ b/src/render/vk_materials.h @@ -27,6 +27,8 @@ struct GLTFMetallic_Roughness VkSampler colorSampler; AllocatedImage metalRoughImage; VkSampler metalRoughSampler; + AllocatedImage normalImage; + VkSampler normalSampler; VkBuffer dataBuffer; uint32_t dataBufferOffset; }; diff --git a/src/scene/tangent_space.cpp b/src/scene/tangent_space.cpp new file mode 100644 index 0000000..e50bc8e --- /dev/null +++ b/src/scene/tangent_space.cpp @@ -0,0 +1,219 @@ +#include "tangent_space.h" + +#include +#include +#include + +#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 &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 + + struct MikkAdapter + { + std::vector *verts; + const std::vector *inds; + Range range; + }; + + static int mikk_get_num_faces(const SMikkTSpaceContext *ctx) + { + const MikkAdapter *ad = reinterpret_cast(ctx->m_pUserData); + return static_cast(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(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(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(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(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 &vertices, const std::vector &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 &vertices, const std::vector &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 tan1(vertexCount, glm::vec3(0.0f)); + std::vector 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 &vertices, const std::vector &indices) + { + if (vertices.empty() || indices.size() < 3) return; + generate_tangents_range(vertices, indices, 0, indices.size(), 0, vertices.size()); + } +} // namespace geom diff --git a/src/scene/tangent_space.h b/src/scene/tangent_space.h new file mode 100644 index 0000000..aebb9a5 --- /dev/null +++ b/src/scene/tangent_space.h @@ -0,0 +1,20 @@ +#pragma once + +#include +#include +#include + +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& vertices, const std::vector& indices); + +// Range variant for submeshes (indices [indexStart, indexStart+indexCount), vertices [vertexStart, vertexStart+vertexCount)) +void generate_tangents_range(std::vector& vertices, const std::vector& indices, + size_t indexStart, size_t indexCount, + size_t vertexStart, size_t vertexCount); + +} // namespace geom + diff --git a/src/scene/vk_loader.cpp b/src/scene/vk_loader.cpp index 10d9bab..b67b6b3 100644 --- a/src/scene/vk_loader.cpp +++ b/src/scene/vk_loader.cpp @@ -14,6 +14,7 @@ #include #include #include +#include "tangent_space.h" //> loadimg std::optional load_image(VulkanEngine *engine, fastgltf::Asset &asset, fastgltf::Image &image, bool srgb) { @@ -312,6 +313,8 @@ std::optional > 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 > 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 > 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 > loadGltf(VulkanEngine *engine, std:: fastgltf::iterateAccessorWithIndex(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 > 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(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()]; diff --git a/third_party/CMakeLists.txt b/third_party/CMakeLists.txt index a9aae04..2245812 100644 --- a/third_party/CMakeLists.txt +++ b/third_party/CMakeLists.txt @@ -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) diff --git a/third_party/MikkTSpace/README.md b/third_party/MikkTSpace/README.md new file mode 100644 index 0000000..9fda155 --- /dev/null +++ b/third_party/MikkTSpace/README.md @@ -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/. diff --git a/third_party/MikkTSpace/mikktspace.c b/third_party/MikkTSpace/mikktspace.c new file mode 100644 index 0000000..0342ae0 --- /dev/null +++ b/third_party/MikkTSpace/mikktspace.c @@ -0,0 +1,1899 @@ +/** \file mikktspace/mikktspace.c + * \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. + */ + +#include +#include +#include +#include +#include +#include + +#include "mikktspace.h" + +#define TFALSE 0 +#define TTRUE 1 + +#ifndef M_PI +#define M_PI 3.1415926535897932384626433832795 +#endif + +#define INTERNAL_RND_SORT_SEED 39871946 + +// internal structure +typedef struct { + float x, y, z; +} SVec3; + +static tbool veq( const SVec3 v1, const SVec3 v2 ) +{ + return (v1.x == v2.x) && (v1.y == v2.y) && (v1.z == v2.z); +} + +static SVec3 vadd( const SVec3 v1, const SVec3 v2 ) +{ + SVec3 vRes; + + vRes.x = v1.x + v2.x; + vRes.y = v1.y + v2.y; + vRes.z = v1.z + v2.z; + + return vRes; +} + + +static SVec3 vsub( const SVec3 v1, const SVec3 v2 ) +{ + SVec3 vRes; + + vRes.x = v1.x - v2.x; + vRes.y = v1.y - v2.y; + vRes.z = v1.z - v2.z; + + return vRes; +} + +static SVec3 vscale(const float fS, const SVec3 v) +{ + SVec3 vRes; + + vRes.x = fS * v.x; + vRes.y = fS * v.y; + vRes.z = fS * v.z; + + return vRes; +} + +static float LengthSquared( const SVec3 v ) +{ + return v.x*v.x + v.y*v.y + v.z*v.z; +} + +static float Length( const SVec3 v ) +{ + return sqrtf(LengthSquared(v)); +} + +static SVec3 Normalize( const SVec3 v ) +{ + return vscale(1 / Length(v), v); +} + +static float vdot( const SVec3 v1, const SVec3 v2) +{ + return v1.x*v2.x + v1.y*v2.y + v1.z*v2.z; +} + + +static tbool NotZero(const float fX) +{ + // could possibly use FLT_EPSILON instead + return fabsf(fX) > FLT_MIN; +} + +static tbool VNotZero(const SVec3 v) +{ + // might change this to an epsilon based test + return NotZero(v.x) || NotZero(v.y) || NotZero(v.z); +} + + + +typedef struct { + int iNrFaces; + int * pTriMembers; +} SSubGroup; + +typedef struct { + int iNrFaces; + int * pFaceIndices; + int iVertexRepresentitive; + tbool bOrientPreservering; +} SGroup; + +// +#define MARK_DEGENERATE 1 +#define QUAD_ONE_DEGEN_TRI 2 +#define GROUP_WITH_ANY 4 +#define ORIENT_PRESERVING 8 + + + +typedef struct { + int FaceNeighbors[3]; + SGroup * AssignedGroup[3]; + + // normalized first order face derivatives + SVec3 vOs, vOt; + float fMagS, fMagT; // original magnitudes + + // determines if the current and the next triangle are a quad. + int iOrgFaceNumber; + int iFlag, iTSpacesOffs; + unsigned char vert_num[4]; +} STriInfo; + +typedef struct { + SVec3 vOs; + float fMagS; + SVec3 vOt; + float fMagT; + int iCounter; // this is to average back into quads. + tbool bOrient; +} STSpace; + +static int GenerateInitialVerticesIndexList(STriInfo pTriInfos[], int piTriList_out[], const SMikkTSpaceContext * pContext, const int iNrTrianglesIn); +static void GenerateSharedVerticesIndexList(int piTriList_in_and_out[], const SMikkTSpaceContext * pContext, const int iNrTrianglesIn); +static void InitTriInfo(STriInfo pTriInfos[], const int piTriListIn[], const SMikkTSpaceContext * pContext, const int iNrTrianglesIn); +static int Build4RuleGroups(STriInfo pTriInfos[], SGroup pGroups[], int piGroupTrianglesBuffer[], const int piTriListIn[], const int iNrTrianglesIn); +static tbool GenerateTSpaces(STSpace psTspace[], const STriInfo pTriInfos[], const SGroup pGroups[], + const int iNrActiveGroups, const int piTriListIn[], const float fThresCos, + const SMikkTSpaceContext * pContext); + +static int MakeIndex(const int iFace, const int iVert) +{ + assert(iVert>=0 && iVert<4 && iFace>=0); + return (iFace<<2) | (iVert&0x3); +} + +static void IndexToData(int * piFace, int * piVert, const int iIndexIn) +{ + piVert[0] = iIndexIn&0x3; + piFace[0] = iIndexIn>>2; +} + +static STSpace AvgTSpace(const STSpace * pTS0, const STSpace * pTS1) +{ + STSpace ts_res; + + // this if is important. Due to floating point precision + // averaging when ts0==ts1 will cause a slight difference + // which results in tangent space splits later on + if (pTS0->fMagS==pTS1->fMagS && pTS0->fMagT==pTS1->fMagT && + veq(pTS0->vOs,pTS1->vOs) && veq(pTS0->vOt, pTS1->vOt)) + { + ts_res.fMagS = pTS0->fMagS; + ts_res.fMagT = pTS0->fMagT; + ts_res.vOs = pTS0->vOs; + ts_res.vOt = pTS0->vOt; + } + else + { + ts_res.fMagS = 0.5f*(pTS0->fMagS+pTS1->fMagS); + ts_res.fMagT = 0.5f*(pTS0->fMagT+pTS1->fMagT); + ts_res.vOs = vadd(pTS0->vOs,pTS1->vOs); + ts_res.vOt = vadd(pTS0->vOt,pTS1->vOt); + if ( VNotZero(ts_res.vOs) ) ts_res.vOs = Normalize(ts_res.vOs); + if ( VNotZero(ts_res.vOt) ) ts_res.vOt = Normalize(ts_res.vOt); + } + + return ts_res; +} + + + +static SVec3 GetPosition(const SMikkTSpaceContext * pContext, const int index); +static SVec3 GetNormal(const SMikkTSpaceContext * pContext, const int index); +static SVec3 GetTexCoord(const SMikkTSpaceContext * pContext, const int index); + + +// degen triangles +static void DegenPrologue(STriInfo pTriInfos[], int piTriList_out[], const int iNrTrianglesIn, const int iTotTris); +static void DegenEpilogue(STSpace psTspace[], STriInfo pTriInfos[], int piTriListIn[], const SMikkTSpaceContext * pContext, const int iNrTrianglesIn, const int iTotTris); + + +tbool genTangSpaceDefault(const SMikkTSpaceContext * pContext) +{ + return genTangSpace(pContext, 180.0f); +} + +tbool genTangSpace(const SMikkTSpaceContext * pContext, const float fAngularThreshold) +{ + // count nr_triangles + int * piTriListIn = NULL, * piGroupTrianglesBuffer = NULL; + STriInfo * pTriInfos = NULL; + SGroup * pGroups = NULL; + STSpace * psTspace = NULL; + int iNrTrianglesIn = 0, f=0, t=0, i=0; + int iNrTSPaces = 0, iTotTris = 0, iDegenTriangles = 0, iNrMaxGroups = 0; + int iNrActiveGroups = 0, index = 0; + const int iNrFaces = pContext->m_pInterface->m_getNumFaces(pContext); + tbool bRes = TFALSE; + const float fThresCos = (float) cos((fAngularThreshold*(float)M_PI)/180.0f); + + // verify all call-backs have been set + if ( pContext->m_pInterface->m_getNumFaces==NULL || + pContext->m_pInterface->m_getNumVerticesOfFace==NULL || + pContext->m_pInterface->m_getPosition==NULL || + pContext->m_pInterface->m_getNormal==NULL || + pContext->m_pInterface->m_getTexCoord==NULL ) + return TFALSE; + + // count triangles on supported faces + for (f=0; fm_pInterface->m_getNumVerticesOfFace(pContext, f); + if (verts==3) ++iNrTrianglesIn; + else if (verts==4) iNrTrianglesIn += 2; + } + if (iNrTrianglesIn<=0) return TFALSE; + + // allocate memory for an index list + piTriListIn = (int *) malloc(sizeof(int)*3*iNrTrianglesIn); + pTriInfos = (STriInfo *) malloc(sizeof(STriInfo)*iNrTrianglesIn); + if (piTriListIn==NULL || pTriInfos==NULL) + { + if (piTriListIn!=NULL) free(piTriListIn); + if (pTriInfos!=NULL) free(pTriInfos); + return TFALSE; + } + + // make an initial triangle --> face index list + iNrTSPaces = GenerateInitialVerticesIndexList(pTriInfos, piTriListIn, pContext, iNrTrianglesIn); + + // make a welded index list of identical positions and attributes (pos, norm, texc) + //printf("gen welded index list begin\n"); + GenerateSharedVerticesIndexList(piTriListIn, pContext, iNrTrianglesIn); + //printf("gen welded index list end\n"); + + // Mark all degenerate triangles + iTotTris = iNrTrianglesIn; + iDegenTriangles = 0; + for (t=0; tm_pInterface->m_getNumVerticesOfFace(pContext, f); + if (verts!=3 && verts!=4) continue; + + + // I've decided to let degenerate triangles and group-with-anythings + // vary between left/right hand coordinate systems at the vertices. + // All healthy triangles on the other hand are built to always be either or. + + /*// force the coordinate system orientation to be uniform for every face. + // (this is already the case for good triangles but not for + // degenerate ones and those with bGroupWithAnything==true) + bool bOrient = psTspace[index].bOrient; + if (psTspace[index].iCounter == 0) // tspace was not derived from a group + { + // look for a space created in GenerateTSpaces() by iCounter>0 + bool bNotFound = true; + int i=1; + while (i 0) bNotFound=false; + else ++i; + } + if (!bNotFound) bOrient = psTspace[index+i].bOrient; + }*/ + + // set data + for (i=0; ivOs.x, pTSpace->vOs.y, pTSpace->vOs.z}; + float bitang[] = {pTSpace->vOt.x, pTSpace->vOt.y, pTSpace->vOt.z}; + if (pContext->m_pInterface->m_setTSpace!=NULL) + pContext->m_pInterface->m_setTSpace(pContext, tang, bitang, pTSpace->fMagS, pTSpace->fMagT, pTSpace->bOrient, f, i); + if (pContext->m_pInterface->m_setTSpaceBasic!=NULL) + pContext->m_pInterface->m_setTSpaceBasic(pContext, tang, pTSpace->bOrient==TTRUE ? 1.0f : (-1.0f), f, i); + + ++index; + } + } + + free(psTspace); + + + return TTRUE; +} + +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +typedef struct { + float vert[3]; + int index; +} STmpVert; + +static const int g_iCells = 2048; + +#ifdef _MSC_VER +# define NOINLINE __declspec(noinline) +#else +# define NOINLINE __attribute__ ((noinline)) +#endif + +// it is IMPORTANT that this function is called to evaluate the hash since +// inlining could potentially reorder instructions and generate different +// results for the same effective input value fVal. +static NOINLINE int FindGridCell(const float fMin, const float fMax, const float fVal) +{ + const float fIndex = g_iCells * ((fVal-fMin)/(fMax-fMin)); + const int iIndex = (int)fIndex; + return iIndex < g_iCells ? (iIndex >= 0 ? iIndex : 0) : (g_iCells - 1); +} + +static void MergeVertsFast(int piTriList_in_and_out[], STmpVert pTmpVert[], const SMikkTSpaceContext * pContext, const int iL_in, const int iR_in); +static void MergeVertsSlow(int piTriList_in_and_out[], const SMikkTSpaceContext * pContext, const int pTable[], const int iEntries); +static void GenerateSharedVerticesIndexListSlow(int piTriList_in_and_out[], const SMikkTSpaceContext * pContext, const int iNrTrianglesIn); + +static void GenerateSharedVerticesIndexList(int piTriList_in_and_out[], const SMikkTSpaceContext * pContext, const int iNrTrianglesIn) +{ + + // Generate bounding box + int * piHashTable=NULL, * piHashCount=NULL, * piHashOffsets=NULL, * piHashCount2=NULL; + STmpVert * pTmpVert = NULL; + int i=0, iChannel=0, k=0, e=0; + int iMaxCount=0; + SVec3 vMin = GetPosition(pContext, 0), vMax = vMin, vDim; + float fMin, fMax; + for (i=1; i<(iNrTrianglesIn*3); i++) + { + const int index = piTriList_in_and_out[i]; + + const SVec3 vP = GetPosition(pContext, index); + if (vMin.x > vP.x) vMin.x = vP.x; + else if (vMax.x < vP.x) vMax.x = vP.x; + if (vMin.y > vP.y) vMin.y = vP.y; + else if (vMax.y < vP.y) vMax.y = vP.y; + if (vMin.z > vP.z) vMin.z = vP.z; + else if (vMax.z < vP.z) vMax.z = vP.z; + } + + vDim = vsub(vMax,vMin); + iChannel = 0; + fMin = vMin.x; fMax=vMax.x; + if (vDim.y>vDim.x && vDim.y>vDim.z) + { + iChannel=1; + fMin = vMin.y; + fMax = vMax.y; + } + else if (vDim.z>vDim.x) + { + iChannel=2; + fMin = vMin.z; + fMax = vMax.z; + } + + // make allocations + piHashTable = (int *) malloc(sizeof(int)*iNrTrianglesIn*3); + piHashCount = (int *) malloc(sizeof(int)*g_iCells); + piHashOffsets = (int *) malloc(sizeof(int)*g_iCells); + piHashCount2 = (int *) malloc(sizeof(int)*g_iCells); + + if (piHashTable==NULL || piHashCount==NULL || piHashOffsets==NULL || piHashCount2==NULL) + { + if (piHashTable!=NULL) free(piHashTable); + if (piHashCount!=NULL) free(piHashCount); + if (piHashOffsets!=NULL) free(piHashOffsets); + if (piHashCount2!=NULL) free(piHashCount2); + GenerateSharedVerticesIndexListSlow(piTriList_in_and_out, pContext, iNrTrianglesIn); + return; + } + memset(piHashCount, 0, sizeof(int)*g_iCells); + memset(piHashCount2, 0, sizeof(int)*g_iCells); + + // count amount of elements in each cell unit + for (i=0; i<(iNrTrianglesIn*3); i++) + { + const int index = piTriList_in_and_out[i]; + const SVec3 vP = GetPosition(pContext, index); + const float fVal = iChannel==0 ? vP.x : (iChannel==1 ? vP.y : vP.z); + const int iCell = FindGridCell(fMin, fMax, fVal); + ++piHashCount[iCell]; + } + + // evaluate start index of each cell. + piHashOffsets[0]=0; + for (k=1; kpTmpVert[l].vert[c]) fvMin[c]=pTmpVert[l].vert[c]; + if (fvMax[c]dx && dy>dz) channel=1; + else if (dz>dx) channel=2; + + fSep = 0.5f*(fvMax[channel]+fvMin[channel]); + + // stop if all vertices are NaNs + if (!isfinite(fSep)) + return; + + // terminate recursion when the separation/average value + // is no longer strictly between fMin and fMax values. + if (fSep>=fvMax[channel] || fSep<=fvMin[channel]) + { + // complete the weld + for (l=iL_in; l<=iR_in; l++) + { + int i = pTmpVert[l].index; + const int index = piTriList_in_and_out[i]; + const SVec3 vP = GetPosition(pContext, index); + const SVec3 vN = GetNormal(pContext, index); + const SVec3 vT = GetTexCoord(pContext, index); + + tbool bNotFound = TTRUE; + int l2=iL_in, i2rec=-1; + while (l20); // at least 2 entries + + // separate (by fSep) all points between iL_in and iR_in in pTmpVert[] + while (iL < iR) + { + tbool bReadyLeftSwap = TFALSE, bReadyRightSwap = TFALSE; + while ((!bReadyLeftSwap) && iL=iL_in && iL<=iR_in); + bReadyLeftSwap = !(pTmpVert[iL].vert[channel]=iL_in && iR<=iR_in); + bReadyRightSwap = pTmpVert[iR].vert[channel]m_pInterface->m_getNumFaces(pContext); f++) + { + const int verts = pContext->m_pInterface->m_getNumVerticesOfFace(pContext, f); + if (verts!=3 && verts!=4) continue; + + pTriInfos[iDstTriIndex].iOrgFaceNumber = f; + pTriInfos[iDstTriIndex].iTSpacesOffs = iTSpacesOffs; + + if (verts==3) + { + unsigned char * pVerts = pTriInfos[iDstTriIndex].vert_num; + pVerts[0]=0; pVerts[1]=1; pVerts[2]=2; + piTriList_out[iDstTriIndex*3+0] = MakeIndex(f, 0); + piTriList_out[iDstTriIndex*3+1] = MakeIndex(f, 1); + piTriList_out[iDstTriIndex*3+2] = MakeIndex(f, 2); + ++iDstTriIndex; // next + } + else + { + { + pTriInfos[iDstTriIndex+1].iOrgFaceNumber = f; + pTriInfos[iDstTriIndex+1].iTSpacesOffs = iTSpacesOffs; + } + + { + // need an order independent way to evaluate + // tspace on quads. This is done by splitting + // along the shortest diagonal. + const int i0 = MakeIndex(f, 0); + const int i1 = MakeIndex(f, 1); + const int i2 = MakeIndex(f, 2); + const int i3 = MakeIndex(f, 3); + const SVec3 T0 = GetTexCoord(pContext, i0); + const SVec3 T1 = GetTexCoord(pContext, i1); + const SVec3 T2 = GetTexCoord(pContext, i2); + const SVec3 T3 = GetTexCoord(pContext, i3); + const float distSQ_02 = LengthSquared(vsub(T2,T0)); + const float distSQ_13 = LengthSquared(vsub(T3,T1)); + tbool bQuadDiagIs_02; + if (distSQ_02m_pInterface->m_getPosition(pContext, pos, iF, iI); + res.x=pos[0]; res.y=pos[1]; res.z=pos[2]; + return res; +} + +static SVec3 GetNormal(const SMikkTSpaceContext * pContext, const int index) +{ + int iF, iI; + SVec3 res; float norm[3]; + IndexToData(&iF, &iI, index); + pContext->m_pInterface->m_getNormal(pContext, norm, iF, iI); + res.x=norm[0]; res.y=norm[1]; res.z=norm[2]; + return res; +} + +static SVec3 GetTexCoord(const SMikkTSpaceContext * pContext, const int index) +{ + int iF, iI; + SVec3 res; float texc[2]; + IndexToData(&iF, &iI, index); + pContext->m_pInterface->m_getTexCoord(pContext, texc, iF, iI); + res.x=texc[0]; res.y=texc[1]; res.z=1.0f; + return res; +} + +///////////////////////////////////////////////////////////////////////////////////////////////////// +///////////////////////////////////////////////////////////////////////////////////////////////////// + +typedef union { + struct + { + int i0, i1, f; + }; + int array[3]; +} SEdge; + +static void BuildNeighborsFast(STriInfo pTriInfos[], SEdge * pEdges, const int piTriListIn[], const int iNrTrianglesIn); +static void BuildNeighborsSlow(STriInfo pTriInfos[], const int piTriListIn[], const int iNrTrianglesIn); + +// returns the texture area times 2 +static float CalcTexArea(const SMikkTSpaceContext * pContext, const int indices[]) +{ + const SVec3 t1 = GetTexCoord(pContext, indices[0]); + const SVec3 t2 = GetTexCoord(pContext, indices[1]); + const SVec3 t3 = GetTexCoord(pContext, indices[2]); + + const float t21x = t2.x-t1.x; + const float t21y = t2.y-t1.y; + const float t31x = t3.x-t1.x; + const float t31y = t3.y-t1.y; + + const float fSignedAreaSTx2 = t21x*t31y - t21y*t31x; + + return fSignedAreaSTx2<0 ? (-fSignedAreaSTx2) : fSignedAreaSTx2; +} + +static void InitTriInfo(STriInfo pTriInfos[], const int piTriListIn[], const SMikkTSpaceContext * pContext, const int iNrTrianglesIn) +{ + int f=0, i=0, t=0; + // pTriInfos[f].iFlag is cleared in GenerateInitialVerticesIndexList() which is called before this function. + + // generate neighbor info list + for (f=0; f0 ? ORIENT_PRESERVING : 0); + + if ( NotZero(fSignedAreaSTx2) ) + { + const float fAbsArea = fabsf(fSignedAreaSTx2); + const float fLenOs = Length(vOs); + const float fLenOt = Length(vOt); + const float fS = (pTriInfos[f].iFlag&ORIENT_PRESERVING)==0 ? (-1.0f) : 1.0f; + if ( NotZero(fLenOs) ) pTriInfos[f].vOs = vscale(fS/fLenOs, vOs); + if ( NotZero(fLenOt) ) pTriInfos[f].vOt = vscale(fS/fLenOt, vOt); + + // evaluate magnitudes prior to normalization of vOs and vOt + pTriInfos[f].fMagS = fLenOs / fAbsArea; + pTriInfos[f].fMagT = fLenOt / fAbsArea; + + // if this is a good triangle + if ( NotZero(pTriInfos[f].fMagS) && NotZero(pTriInfos[f].fMagT)) + pTriInfos[f].iFlag &= (~GROUP_WITH_ANY); + } + } + + // force otherwise healthy quads to a fixed orientation + while (t<(iNrTrianglesIn-1)) + { + const int iFO_a = pTriInfos[t].iOrgFaceNumber; + const int iFO_b = pTriInfos[t+1].iOrgFaceNumber; + if (iFO_a==iFO_b) // this is a quad + { + const tbool bIsDeg_a = (pTriInfos[t].iFlag&MARK_DEGENERATE)!=0 ? TTRUE : TFALSE; + const tbool bIsDeg_b = (pTriInfos[t+1].iFlag&MARK_DEGENERATE)!=0 ? TTRUE : TFALSE; + + // bad triangles should already have been removed by + // DegenPrologue(), but just in case check bIsDeg_a and bIsDeg_a are false + if ((bIsDeg_a||bIsDeg_b)==TFALSE) + { + const tbool bOrientA = (pTriInfos[t].iFlag&ORIENT_PRESERVING)!=0 ? TTRUE : TFALSE; + const tbool bOrientB = (pTriInfos[t+1].iFlag&ORIENT_PRESERVING)!=0 ? TTRUE : TFALSE; + // if this happens the quad has extremely bad mapping!! + if (bOrientA!=bOrientB) + { + //printf("found quad with bad mapping\n"); + tbool bChooseOrientFirstTri = TFALSE; + if ((pTriInfos[t+1].iFlag&GROUP_WITH_ANY)!=0) bChooseOrientFirstTri = TTRUE; + else if ( CalcTexArea(pContext, &piTriListIn[t*3+0]) >= CalcTexArea(pContext, &piTriListIn[(t+1)*3+0]) ) + bChooseOrientFirstTri = TTRUE; + + // force match + { + const int t0 = bChooseOrientFirstTri ? t : (t+1); + const int t1 = bChooseOrientFirstTri ? (t+1) : t; + pTriInfos[t1].iFlag &= (~ORIENT_PRESERVING); // clear first + pTriInfos[t1].iFlag |= (pTriInfos[t0].iFlag&ORIENT_PRESERVING); // copy bit + } + } + } + t += 2; + } + else + ++t; + } + + // match up edge pairs + { + SEdge * pEdges = (SEdge *) malloc(sizeof(SEdge)*iNrTrianglesIn*3); + if (pEdges==NULL) + BuildNeighborsSlow(pTriInfos, piTriListIn, iNrTrianglesIn); + else + { + BuildNeighborsFast(pTriInfos, pEdges, piTriListIn, iNrTrianglesIn); + + free(pEdges); + } + } +} + +///////////////////////////////////////////////////////////////////////////////////////////////////// +///////////////////////////////////////////////////////////////////////////////////////////////////// + +static tbool AssignRecur(const int piTriListIn[], STriInfo psTriInfos[], const int iMyTriIndex, SGroup * pGroup); +static void AddTriToGroup(SGroup * pGroup, const int iTriIndex); + +static int Build4RuleGroups(STriInfo pTriInfos[], SGroup pGroups[], int piGroupTrianglesBuffer[], const int piTriListIn[], const int iNrTrianglesIn) +{ + const int iNrMaxGroups = iNrTrianglesIn*3; + int iNrActiveGroups = 0; + int iOffset = 0, f=0, i=0; + (void)iNrMaxGroups; /* quiet warnings in non debug mode */ + for (f=0; fiVertexRepresentitive = vert_index; + pTriInfos[f].AssignedGroup[i]->bOrientPreservering = (pTriInfos[f].iFlag&ORIENT_PRESERVING)!=0; + pTriInfos[f].AssignedGroup[i]->iNrFaces = 0; + pTriInfos[f].AssignedGroup[i]->pFaceIndices = &piGroupTrianglesBuffer[iOffset]; + ++iNrActiveGroups; + + AddTriToGroup(pTriInfos[f].AssignedGroup[i], f); + bOrPre = (pTriInfos[f].iFlag&ORIENT_PRESERVING)!=0 ? TTRUE : TFALSE; + neigh_indexL = pTriInfos[f].FaceNeighbors[i]; + neigh_indexR = pTriInfos[f].FaceNeighbors[i>0?(i-1):2]; + if (neigh_indexL>=0) // neighbor + { + const tbool bAnswer = + AssignRecur(piTriListIn, pTriInfos, neigh_indexL, + pTriInfos[f].AssignedGroup[i] ); + + const tbool bOrPre2 = (pTriInfos[neigh_indexL].iFlag&ORIENT_PRESERVING)!=0 ? TTRUE : TFALSE; + const tbool bDiff = bOrPre!=bOrPre2 ? TTRUE : TFALSE; + assert(bAnswer || bDiff); + (void)bAnswer, (void)bDiff; /* quiet warnings in non debug mode */ + } + if (neigh_indexR>=0) // neighbor + { + const tbool bAnswer = + AssignRecur(piTriListIn, pTriInfos, neigh_indexR, + pTriInfos[f].AssignedGroup[i] ); + + const tbool bOrPre2 = (pTriInfos[neigh_indexR].iFlag&ORIENT_PRESERVING)!=0 ? TTRUE : TFALSE; + const tbool bDiff = bOrPre!=bOrPre2 ? TTRUE : TFALSE; + assert(bAnswer || bDiff); + (void)bAnswer, (void)bDiff; /* quiet warnings in non debug mode */ + } + + // update offset + iOffset += pTriInfos[f].AssignedGroup[i]->iNrFaces; + // since the groups are disjoint a triangle can never + // belong to more than 3 groups. Subsequently something + // is completely screwed if this assertion ever hits. + assert(iOffset <= iNrMaxGroups); + } + } + } + + return iNrActiveGroups; +} + +static void AddTriToGroup(SGroup * pGroup, const int iTriIndex) +{ + pGroup->pFaceIndices[pGroup->iNrFaces] = iTriIndex; + ++pGroup->iNrFaces; +} + +static tbool AssignRecur(const int piTriListIn[], STriInfo psTriInfos[], + const int iMyTriIndex, SGroup * pGroup) +{ + STriInfo * pMyTriInfo = &psTriInfos[iMyTriIndex]; + + // track down vertex + const int iVertRep = pGroup->iVertexRepresentitive; + const int * pVerts = &piTriListIn[3*iMyTriIndex+0]; + int i=-1; + if (pVerts[0]==iVertRep) i=0; + else if (pVerts[1]==iVertRep) i=1; + else if (pVerts[2]==iVertRep) i=2; + assert(i>=0 && i<3); + + // early out + if (pMyTriInfo->AssignedGroup[i] == pGroup) return TTRUE; + else if (pMyTriInfo->AssignedGroup[i]!=NULL) return TFALSE; + if ((pMyTriInfo->iFlag&GROUP_WITH_ANY)!=0) + { + // first to group with a group-with-anything triangle + // determines it's orientation. + // This is the only existing order dependency in the code!! + if ( pMyTriInfo->AssignedGroup[0] == NULL && + pMyTriInfo->AssignedGroup[1] == NULL && + pMyTriInfo->AssignedGroup[2] == NULL ) + { + pMyTriInfo->iFlag &= (~ORIENT_PRESERVING); + pMyTriInfo->iFlag |= (pGroup->bOrientPreservering ? ORIENT_PRESERVING : 0); + } + } + { + const tbool bOrient = (pMyTriInfo->iFlag&ORIENT_PRESERVING)!=0 ? TTRUE : TFALSE; + if (bOrient != pGroup->bOrientPreservering) return TFALSE; + } + + AddTriToGroup(pGroup, iMyTriIndex); + pMyTriInfo->AssignedGroup[i] = pGroup; + + { + const int neigh_indexL = pMyTriInfo->FaceNeighbors[i]; + const int neigh_indexR = pMyTriInfo->FaceNeighbors[i>0?(i-1):2]; + if (neigh_indexL>=0) + AssignRecur(piTriListIn, psTriInfos, neigh_indexL, pGroup); + if (neigh_indexR>=0) + AssignRecur(piTriListIn, psTriInfos, neigh_indexR, pGroup); + } + + + + return TTRUE; +} + +///////////////////////////////////////////////////////////////////////////////////////////////////// +///////////////////////////////////////////////////////////////////////////////////////////////////// + +static tbool CompareSubGroups(const SSubGroup * pg1, const SSubGroup * pg2); +static void QuickSort(int* pSortBuffer, int iLeft, int iRight, unsigned int uSeed); +static STSpace EvalTspace(int face_indices[], const int iFaces, const int piTriListIn[], const STriInfo pTriInfos[], const SMikkTSpaceContext * pContext, const int iVertexRepresentitive); + +static tbool GenerateTSpaces(STSpace psTspace[], const STriInfo pTriInfos[], const SGroup pGroups[], + const int iNrActiveGroups, const int piTriListIn[], const float fThresCos, + const SMikkTSpaceContext * pContext) +{ + STSpace * pSubGroupTspace = NULL; + SSubGroup * pUniSubGroups = NULL; + int * pTmpMembers = NULL; + int iMaxNrFaces=0, iUniqueTspaces=0, g=0, i=0; + for (g=0; giNrFaces; i++) // triangles + { + const int f = pGroup->pFaceIndices[i]; // triangle number + int index=-1, iVertIndex=-1, iOF_1=-1, iMembers=0, j=0, l=0; + SSubGroup tmp_group; + tbool bFound; + SVec3 n, vOs, vOt; + if (pTriInfos[f].AssignedGroup[0]==pGroup) index=0; + else if (pTriInfos[f].AssignedGroup[1]==pGroup) index=1; + else if (pTriInfos[f].AssignedGroup[2]==pGroup) index=2; + assert(index>=0 && index<3); + + iVertIndex = piTriListIn[f*3+index]; + assert(iVertIndex==pGroup->iVertexRepresentitive); + + // is normalized already + n = GetNormal(pContext, iVertIndex); + + // project + vOs = vsub(pTriInfos[f].vOs, vscale(vdot(n,pTriInfos[f].vOs), n)); + vOt = vsub(pTriInfos[f].vOt, vscale(vdot(n,pTriInfos[f].vOt), n)); + if ( VNotZero(vOs) ) vOs = Normalize(vOs); + if ( VNotZero(vOt) ) vOt = Normalize(vOt); + + // original face number + iOF_1 = pTriInfos[f].iOrgFaceNumber; + + iMembers = 0; + for (j=0; jiNrFaces; j++) + { + const int t = pGroup->pFaceIndices[j]; // triangle number + const int iOF_2 = pTriInfos[t].iOrgFaceNumber; + + // project + SVec3 vOs2 = vsub(pTriInfos[t].vOs, vscale(vdot(n,pTriInfos[t].vOs), n)); + SVec3 vOt2 = vsub(pTriInfos[t].vOt, vscale(vdot(n,pTriInfos[t].vOt), n)); + if ( VNotZero(vOs2) ) vOs2 = Normalize(vOs2); + if ( VNotZero(vOt2) ) vOt2 = Normalize(vOt2); + + { + const tbool bAny = ( (pTriInfos[f].iFlag | pTriInfos[t].iFlag) & GROUP_WITH_ANY )!=0 ? TTRUE : TFALSE; + // make sure triangles which belong to the same quad are joined. + const tbool bSameOrgFace = iOF_1==iOF_2 ? TTRUE : TFALSE; + + const float fCosS = vdot(vOs,vOs2); + const float fCosT = vdot(vOt,vOt2); + + assert(f!=t || bSameOrgFace); // sanity check + if (bAny || bSameOrgFace || (fCosS>fThresCos && fCosT>fThresCos)) + pTmpMembers[iMembers++] = t; + } + } + + // sort pTmpMembers + tmp_group.iNrFaces = iMembers; + tmp_group.pTriMembers = pTmpMembers; + if (iMembers>1) + { + unsigned int uSeed = INTERNAL_RND_SORT_SEED; // could replace with a random seed? + QuickSort(pTmpMembers, 0, iMembers-1, uSeed); + } + + // look for an existing match + bFound = TFALSE; + l=0; + while (liVertexRepresentitive); + ++iUniqueSubGroups; + } + + // output tspace + { + const int iOffs = pTriInfos[f].iTSpacesOffs; + const int iVert = pTriInfos[f].vert_num[index]; + STSpace * pTS_out = &psTspace[iOffs+iVert]; + assert(pTS_out->iCounter<2); + assert(((pTriInfos[f].iFlag&ORIENT_PRESERVING)!=0) == pGroup->bOrientPreservering); + if (pTS_out->iCounter==1) + { + *pTS_out = AvgTSpace(pTS_out, &pSubGroupTspace[l]); + pTS_out->iCounter = 2; // update counter + pTS_out->bOrient = pGroup->bOrientPreservering; + } + else + { + assert(pTS_out->iCounter==0); + *pTS_out = pSubGroupTspace[l]; + pTS_out->iCounter = 1; // update counter + pTS_out->bOrient = pGroup->bOrientPreservering; + } + } + } + + // clean up and offset iUniqueTspaces + for (s=0; s=0 && i<3); + + // project + index = piTriListIn[3*f+i]; + n = GetNormal(pContext, index); + vOs = vsub(pTriInfos[f].vOs, vscale(vdot(n,pTriInfos[f].vOs), n)); + vOt = vsub(pTriInfos[f].vOt, vscale(vdot(n,pTriInfos[f].vOt), n)); + if ( VNotZero(vOs) ) vOs = Normalize(vOs); + if ( VNotZero(vOt) ) vOt = Normalize(vOt); + + i2 = piTriListIn[3*f + (i<2?(i+1):0)]; + i1 = piTriListIn[3*f + i]; + i0 = piTriListIn[3*f + (i>0?(i-1):2)]; + + p0 = GetPosition(pContext, i0); + p1 = GetPosition(pContext, i1); + p2 = GetPosition(pContext, i2); + v1 = vsub(p0,p1); + v2 = vsub(p2,p1); + + // project + v1 = vsub(v1, vscale(vdot(n,v1),n)); if ( VNotZero(v1) ) v1 = Normalize(v1); + v2 = vsub(v2, vscale(vdot(n,v2),n)); if ( VNotZero(v2) ) v2 = Normalize(v2); + + // weight contribution by the angle + // between the two edge vectors + fCos = vdot(v1,v2); fCos=fCos>1?1:(fCos<(-1) ? (-1) : fCos); + fAngle = (float) acos(fCos); + fMagS = pTriInfos[f].fMagS; + fMagT = pTriInfos[f].fMagT; + + res.vOs=vadd(res.vOs, vscale(fAngle,vOs)); + res.vOt=vadd(res.vOt,vscale(fAngle,vOt)); + res.fMagS+=(fAngle*fMagS); + res.fMagT+=(fAngle*fMagT); + fAngleSum += fAngle; + } + } + + // normalize + if ( VNotZero(res.vOs) ) res.vOs = Normalize(res.vOs); + if ( VNotZero(res.vOt) ) res.vOt = Normalize(res.vOt); + if (fAngleSum>0) + { + res.fMagS /= fAngleSum; + res.fMagT /= fAngleSum; + } + + return res; +} + +static tbool CompareSubGroups(const SSubGroup * pg1, const SSubGroup * pg2) +{ + tbool bStillSame=TTRUE; + int i=0; + if (pg1->iNrFaces!=pg2->iNrFaces) return TFALSE; + while (iiNrFaces && bStillSame) + { + bStillSame = pg1->pTriMembers[i]==pg2->pTriMembers[i] ? TTRUE : TFALSE; + if (bStillSame) ++i; + } + return bStillSame; +} + +static void QuickSort(int* pSortBuffer, int iLeft, int iRight, unsigned int uSeed) +{ + int iL, iR, n, index, iMid, iTmp; + + // Random + unsigned int t=uSeed&31; + t=(uSeed<>(32-t)); + uSeed=uSeed+t+3; + // Random end + + iL=iLeft; iR=iRight; + n = (iR-iL)+1; + assert(n>=0); + index = (int) (uSeed%n); + + iMid=pSortBuffer[index + iL]; + + + do + { + while (pSortBuffer[iL] < iMid) + ++iL; + while (pSortBuffer[iR] > iMid) + --iR; + + if (iL <= iR) + { + iTmp = pSortBuffer[iL]; + pSortBuffer[iL] = pSortBuffer[iR]; + pSortBuffer[iR] = iTmp; + ++iL; --iR; + } + } + while (iL <= iR); + + if (iLeft < iR) + QuickSort(pSortBuffer, iLeft, iR, uSeed); + if (iL < iRight) + QuickSort(pSortBuffer, iL, iRight, uSeed); +} + +///////////////////////////////////////////////////////////////////////////////////////////// +///////////////////////////////////////////////////////////////////////////////////////////// + +static void QuickSortEdges(SEdge * pSortBuffer, int iLeft, int iRight, const int channel, unsigned int uSeed); +static void GetEdge(int * i0_out, int * i1_out, int * edgenum_out, const int indices[], const int i0_in, const int i1_in); + +static void BuildNeighborsFast(STriInfo pTriInfos[], SEdge * pEdges, const int piTriListIn[], const int iNrTrianglesIn) +{ + // build array of edges + unsigned int uSeed = INTERNAL_RND_SORT_SEED; // could replace with a random seed? + int iEntries=0, iCurStartIndex=-1, f=0, i=0; + for (f=0; f pSortBuffer[iRight].array[channel]) + { + sTmp = pSortBuffer[iLeft]; + pSortBuffer[iLeft] = pSortBuffer[iRight]; + pSortBuffer[iRight] = sTmp; + } + return; + } + + // Random + t=uSeed&31; + t=(uSeed<>(32-t)); + uSeed=uSeed+t+3; + // Random end + + iL = iLeft; + iR = iRight; + n = (iR-iL)+1; + assert(n>=0); + index = (int) (uSeed%n); + + iMid=pSortBuffer[index + iL].array[channel]; + + do + { + while (pSortBuffer[iL].array[channel] < iMid) + ++iL; + while (pSortBuffer[iR].array[channel] > iMid) + --iR; + + if (iL <= iR) + { + sTmp = pSortBuffer[iL]; + pSortBuffer[iL] = pSortBuffer[iR]; + pSortBuffer[iR] = sTmp; + ++iL; --iR; + } + } + while (iL <= iR); + + if (iLeft < iR) + QuickSortEdges(pSortBuffer, iLeft, iR, channel, uSeed); + if (iL < iRight) + QuickSortEdges(pSortBuffer, iL, iRight, channel, uSeed); +} + +// resolve ordering and edge number +static void GetEdge(int * i0_out, int * i1_out, int * edgenum_out, const int indices[], const int i0_in, const int i1_in) +{ + *edgenum_out = -1; + + // test if first index is on the edge + if (indices[0]==i0_in || indices[0]==i1_in) + { + // test if second index is on the edge + if (indices[1]==i0_in || indices[1]==i1_in) + { + edgenum_out[0]=0; // first edge + i0_out[0]=indices[0]; + i1_out[0]=indices[1]; + } + else + { + edgenum_out[0]=2; // third edge + i0_out[0]=indices[2]; + i1_out[0]=indices[0]; + } + } + else + { + // only second and third index is on the edge + edgenum_out[0]=1; // second edge + i0_out[0]=indices[1]; + i1_out[0]=indices[2]; + } +} + + +///////////////////////////////////////////////////////////////////////////////////////////// +/////////////////////////////////// Degenerate triangles //////////////////////////////////// + +static void DegenPrologue(STriInfo pTriInfos[], int piTriList_out[], const int iNrTrianglesIn, const int iTotTris) +{ + int iNextGoodTriangleSearchIndex=-1; + tbool bStillFindingGoodOnes; + + // locate quads with only one good triangle + int t=0; + while (t<(iTotTris-1)) + { + const int iFO_a = pTriInfos[t].iOrgFaceNumber; + const int iFO_b = pTriInfos[t+1].iOrgFaceNumber; + if (iFO_a==iFO_b) // this is a quad + { + const tbool bIsDeg_a = (pTriInfos[t].iFlag&MARK_DEGENERATE)!=0 ? TTRUE : TFALSE; + const tbool bIsDeg_b = (pTriInfos[t+1].iFlag&MARK_DEGENERATE)!=0 ? TTRUE : TFALSE; + if ((bIsDeg_a^bIsDeg_b)!=0) + { + pTriInfos[t].iFlag |= QUAD_ONE_DEGEN_TRI; + pTriInfos[t+1].iFlag |= QUAD_ONE_DEGEN_TRI; + } + t += 2; + } + else + ++t; + } + + // reorder list so all degen triangles are moved to the back + // without reordering the good triangles + iNextGoodTriangleSearchIndex = 1; + t=0; + bStillFindingGoodOnes = TTRUE; + while (t (t+1)); + + // swap triangle t0 and t1 + if (!bJustADegenerate) + { + int i=0; + for (i=0; i<3; i++) + { + const int index = piTriList_out[t0*3+i]; + piTriList_out[t0*3+i] = piTriList_out[t1*3+i]; + piTriList_out[t1*3+i] = index; + } + { + const STriInfo tri_info = pTriInfos[t0]; + pTriInfos[t0] = pTriInfos[t1]; + pTriInfos[t1] = tri_info; + } + } + else + bStillFindingGoodOnes = TFALSE; // this is not supposed to happen + } + + if (bStillFindingGoodOnes) ++t; + } + + assert(bStillFindingGoodOnes); // code will still work. + assert(iNrTrianglesIn == t); +} + +static void DegenEpilogue(STSpace psTspace[], STriInfo pTriInfos[], int piTriListIn[], const SMikkTSpaceContext * pContext, const int iNrTrianglesIn, const int iTotTris) +{ + int t=0, i=0; + // deal with degenerate triangles + // punishment for degenerate triangles is O(N^2) + for (t=iNrTrianglesIn; t 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