From d0acaada6579880f4004a1a095bd00466644c32c Mon Sep 17 00:00:00 2001 From: hydrogendeuteride Date: Tue, 18 Nov 2025 23:50:18 +0900 Subject: [PATCH] Merge: Bounding --- src/core/asset_manager.cpp | 30 ++++++++ src/core/asset_manager.h | 2 +- src/core/vk_engine_ui.cpp | 1 + src/render/primitives.h | 119 +++++++++++++++++++++++++++++- src/scene/vk_scene_picking.cpp | 129 ++++++++++++++++++++++++++++++++- 5 files changed, 278 insertions(+), 3 deletions(-) diff --git a/src/core/asset_manager.cpp b/src/core/asset_manager.cpp index 91a3415..eb4234b 100644 --- a/src/core/asset_manager.cpp +++ b/src/core/asset_manager.cpp @@ -111,6 +111,18 @@ std::shared_ptr AssetManager::getPrimitive(std::string_view name) con if (auto m = findBy("Sphere")) return m; return {}; } + if (name == std::string_view("plane") || name == std::string_view("Plane")) + { + if (auto m = findBy("plane")) return m; + if (auto m = findBy("Plane")) return m; + return {}; + } + if (name == std::string_view("capsule") || name == std::string_view("Capsule")) + { + if (auto m = findBy("capsule")) return m; + if (auto m = findBy("Capsule")) return m; + return {}; + } return {}; } @@ -145,6 +157,16 @@ std::shared_ptr AssetManager::createMesh(const MeshCreateInfo &info) vertsSpan = tmpVerts; indsSpan = tmpInds; break; + case MeshGeometryDesc::Type::Plane: + primitives::buildPlane(tmpVerts, tmpInds); + vertsSpan = tmpVerts; + indsSpan = tmpInds; + break; + case MeshGeometryDesc::Type::Capsule: + primitives::buildCapsule(tmpVerts, tmpInds); + vertsSpan = tmpVerts; + indsSpan = tmpInds; + break; } // Ensure tangents exist for primitives (and provided geometry if needed) @@ -246,7 +268,15 @@ std::shared_ptr AssetManager::createMesh(const MeshCreateInfo &info) case MeshGeometryDesc::Type::Sphere: surf.bounds.type = BoundsType::Sphere; break; + case MeshGeometryDesc::Type::Capsule: + surf.bounds.type = BoundsType::Capsule; + break; case MeshGeometryDesc::Type::Cube: + surf.bounds.type = BoundsType::Box; + break; + case MeshGeometryDesc::Type::Plane: + surf.bounds.type = BoundsType::Box; + break; case MeshGeometryDesc::Type::Provided: default: surf.bounds.type = BoundsType::Box; diff --git a/src/core/asset_manager.h b/src/core/asset_manager.h index 5fc398c..d6c2b73 100644 --- a/src/core/asset_manager.h +++ b/src/core/asset_manager.h @@ -40,7 +40,7 @@ public: struct MeshGeometryDesc { - enum class Type { Provided, Cube, Sphere }; + enum class Type { Provided, Cube, Sphere, Plane, Capsule }; Type type = Type::Provided; std::span vertices{}; diff --git a/src/core/vk_engine_ui.cpp b/src/core/vk_engine_ui.cpp index eceab94..288ea87 100644 --- a/src/core/vk_engine_ui.cpp +++ b/src/core/vk_engine_ui.cpp @@ -17,6 +17,7 @@ #include "core/texture_cache.h" #include "core/ibl_manager.h" #include "engine_context.h" +#include namespace { diff --git a/src/render/primitives.h b/src/render/primitives.h index ef60cac..14359f6 100644 --- a/src/render/primitives.h +++ b/src/render/primitives.h @@ -1,11 +1,16 @@ #pragma once #include +#include +#include #include #include #include "core/vk_types.h" +#include "glm/gtx/compatibility.hpp" +#include "glm/gtx/norm.hpp" namespace primitives { + // Axis-aligned unit cube centered at origin, size 1 on each side. inline void buildCube(std::vector &vertices, std::vector &indices) { vertices.clear(); @@ -50,6 +55,7 @@ namespace primitives } } + // Unit sphere centered at origin, radius 0.5. inline void buildSphere(std::vector &vertices, std::vector &indices, int sectors = 16, int stacks = 16) { @@ -93,5 +99,116 @@ namespace primitives } } } -} // namespace primitives + // Infinite grid plane trimmed to a unit quad on XZ, centered at origin (Y up). + inline void buildPlane(std::vector &vertices, std::vector &indices) + { + vertices.clear(); + indices.clear(); + + const glm::vec3 n(0.0f, 1.0f, 0.0f); + const float y = 0.0f; + + Vertex v0{{-0.5f, y, -0.5f}, 0.0f, n, 0.0f, glm::vec4(1.0f), glm::vec4(1, 0, 0, 1)}; + Vertex v1{{ 0.5f, y, -0.5f}, 1.0f, n, 0.0f, glm::vec4(1.0f), glm::vec4(1, 0, 0, 1)}; + Vertex v2{{ 0.5f, y, 0.5f}, 1.0f, n, 1.0f, glm::vec4(1.0f), glm::vec4(1, 0, 0, 1)}; + Vertex v3{{-0.5f, y, 0.5f}, 0.0f, n, 1.0f, glm::vec4(1.0f), glm::vec4(1, 0, 0, 1)}; + + vertices.push_back(v0); + vertices.push_back(v1); + vertices.push_back(v2); + vertices.push_back(v3); + + indices.push_back(0); + indices.push_back(1); + indices.push_back(2); + indices.push_back(2); + indices.push_back(3); + indices.push_back(0); + } + + // Capsule aligned with local Y axis. + // Radius ~0.5, total height ~2.0 (cylinder half-height 0.5). + inline void buildCapsule(std::vector &vertices, std::vector &indices, + int radialSegments = 16, int stackSegments = 16) + { + vertices.clear(); + indices.clear(); + + radialSegments = std::max(radialSegments, 3); + stackSegments = std::max(stackSegments, 2); + + const float radius = 0.5f; + const float halfHeight = 0.5f; // cylinder half-height + const float totalHalf = halfHeight + radius; + const float totalHeight = totalHalf * 2.0f; + + // Build a regular grid in (stackSegments+1) x (radialSegments+1) + // using an analytical capsule cross-section (capped cylinder). + for (int iy = 0; iy <= stackSegments; ++iy) + { + float v = (float) iy / (float) stackSegments; + float y = -totalHalf + totalHeight * v; + + float ay = std::abs(y); + float ringRadius; + if (ay <= halfHeight) + { + ringRadius = radius; + } + else + { + float dy = ay - halfHeight; + float inside = radius * radius - dy * dy; + ringRadius = inside > 0.0f ? std::sqrt(inside) : 0.0f; + } + + for (int ix = 0; ix <= radialSegments; ++ix) + { + float u = (float) ix / (float) radialSegments; + float theta = u * glm::two_pi(); + float x = ringRadius * std::cos(theta); + float z = ringRadius * std::sin(theta); + + glm::vec3 pos(x, y, z); + + float cy = glm::clamp(y, -halfHeight, halfHeight); + glm::vec3 center(0.0f, cy, 0.0f); + glm::vec3 n = glm::normalize(pos - center); + if (!glm::all(glm::isfinite(n)) || glm::length2(n) < 1e-8f) + { + n = glm::vec3(0.0f, y >= 0.0f ? 1.0f : -1.0f, 0.0f); + } + + Vertex vert{}; + vert.position = pos; + vert.normal = n; + vert.uv_x = u; + vert.uv_y = v; + vert.color = glm::vec4(1.0f); + vert.tangent = glm::vec4(1, 0, 0, 1); + vertices.push_back(vert); + } + } + + const uint32_t stride = static_cast(radialSegments + 1); + for (int iy = 0; iy < stackSegments; ++iy) + { + for (int ix = 0; ix < radialSegments; ++ix) + { + uint32_t i0 = (uint32_t) iy * stride + (uint32_t) ix; + uint32_t i1 = i0 + 1; + uint32_t i2 = i0 + stride; + uint32_t i3 = i2 + 1; + + indices.push_back(i0); + indices.push_back(i2); + indices.push_back(i1); + + indices.push_back(i1); + indices.push_back(i2); + indices.push_back(i3); + } + } + } +} // namespace primitives diff --git a/src/scene/vk_scene_picking.cpp b/src/scene/vk_scene_picking.cpp index 33458ea..fff977b 100644 --- a/src/scene/vk_scene_picking.cpp +++ b/src/scene/vk_scene_picking.cpp @@ -137,6 +137,130 @@ namespace return true; } + // Ray / capsule intersection in world space. Capsule is aligned with local Y axis + // and reconstructed from Bounds.origin/extents, assuming extents.x/z ~= radius + // and extents.y ~= halfHeight + radius (AABB center/half-size convention). + bool intersect_ray_capsule(const glm::vec3 &rayOrigin, + const glm::vec3 &rayDir, + const Bounds &bounds, + const glm::mat4 &worldTransform, + glm::vec3 &outWorldHit) + { + if (glm::length2(rayDir) < 1e-8f) + { + return false; + } + + // Transform ray into object-local space. + glm::mat4 invM = glm::inverse(worldTransform); + glm::vec3 localOrigin = glm::vec3(invM * glm::vec4(rayOrigin, 1.0f)); + glm::vec3 localDir = glm::vec3(invM * glm::vec4(rayDir, 0.0f)); + if (glm::length2(localDir) < 1e-8f) + { + return false; + } + localDir = glm::normalize(localDir); + + // Work in capsule-local space where Bounds.origin is at (0,0,0). + glm::vec3 ro = localOrigin - bounds.origin; + glm::vec3 rd = localDir; + + float radius = std::max(bounds.extents.x, bounds.extents.z); + if (radius <= 0.0f) + { + return false; + } + // extents.y is (halfCylinder + radius) for a symmetric capsule. + float halfSegment = std::max(bounds.extents.y - radius, 0.0f); + + float tHit = std::numeric_limits::max(); + bool hit = false; + + // 1) Cylinder part around Y axis: x^2 + z^2 = r^2, |y| <= halfSegment. + float a = rd.x * rd.x + rd.z * rd.z; + float b = 2.0f * (ro.x * rd.x + ro.z * rd.z); + float c = ro.x * ro.x + ro.z * ro.z - radius * radius; + + if (std::abs(a) > 1e-8f) + { + float disc = b * b - 4.0f * a * c; + if (disc >= 0.0f) + { + float s = std::sqrt(disc); + float invDen = 0.5f / a; + float t0 = (-b - s) * invDen; + float t1 = (-b + s) * invDen; + if (t0 > t1) std::swap(t0, t1); + + auto tryCylHit = [&](float t) + { + if (t < 0.0f || t >= tHit) + { + return; + } + glm::vec3 p = ro + rd * t; + if (std::abs(p.y) <= halfSegment + 1e-4f) + { + tHit = t; + hit = true; + } + }; + + tryCylHit(t0); + tryCylHit(t1); + } + } + + // 2) Spherical caps at y = +/- halfSegment. + auto intersectCap = [&](float capY) + { + glm::vec3 center(0.0f, capY, 0.0f); + glm::vec3 oc = ro - center; + float b2 = glm::dot(oc, rd); + float c2 = glm::dot(oc, oc) - radius * radius; + float disc = b2 * b2 - c2; + if (disc < 0.0f) + { + return; + } + float s = std::sqrt(disc); + float t0 = -b2 - s; + float t1 = -b2 + s; + + auto tryCapHit = [&](float t) + { + if (t < 0.0f || t >= tHit) + { + return; + } + tHit = t; + hit = true; + }; + + if (t0 >= 0.0f) tryCapHit(t0); + if (t1 >= 0.0f) tryCapHit(t1); + }; + + intersectCap(+halfSegment); + intersectCap(-halfSegment); + + if (!hit) + { + return false; + } + + glm::vec3 localHit = ro + rd * tHit; + glm::vec3 worldHit = glm::vec3(worldTransform * glm::vec4(localHit + bounds.origin, 1.0f)); + + if (glm::dot(worldHit - rayOrigin, rayDir) <= 0.0f) + { + return false; + } + + outWorldHit = worldHit; + return true; + } + // Ray / oriented-bounds intersection in world space using object-local shape. // Uses a quick sphere test first; on success refines based on BoundsType. // Returns true when hit; outWorldHit is the closest hit point in world space. @@ -173,9 +297,12 @@ namespace outWorldHit = rayOrigin + rayDir * sphereT; return true; } + case BoundsType::Capsule: + { + return intersect_ray_capsule(rayOrigin, rayDir, bounds, worldTransform, outWorldHit); + } case BoundsType::Box: case BoundsType::Mesh: // TODO: replace with BVH/mesh query; box is a safe fallback. - case BoundsType::Capsule: default: { // For Capsule and Mesh we currently fall back to the oriented box;