From af39bfa9b948d9c688a14fce9c6eb0afe0669176 Mon Sep 17 00:00:00 2001 From: Razakhel Date: Tue, 13 Feb 2024 23:15:42 +0100 Subject: [PATCH] [Data/MeshDistanceField] Added an MDF structure - This computes signed distances to the nearest mesh geometry within a 3D grid using a BVH - This can be used for some algorithms, such as rendering ones that can use sphere marching through the 3D volume created from this field --- README.md | 20 +-- include/RaZ/Data/MeshDistanceField.hpp | 56 ++++++++ include/RaZ/RaZ.hpp | 1 + src/RaZ/Data/MeshDistanceField.cpp | 76 ++++++++++ src/RaZ/Script/LuaData.cpp | 13 ++ tests/src/RaZ/Data/MeshDistanceField.cpp | 175 +++++++++++++++++++++++ tests/src/RaZ/Script/LuaData.cpp | 11 ++ 7 files changed, 342 insertions(+), 10 deletions(-) create mode 100644 include/RaZ/Data/MeshDistanceField.hpp create mode 100644 src/RaZ/Data/MeshDistanceField.cpp create mode 100644 tests/src/RaZ/Data/MeshDistanceField.cpp diff --git a/README.md b/README.md index e3a8b5f7..3fe95f3a 100644 --- a/README.md +++ b/README.md @@ -88,13 +88,13 @@ If you also are working on some on your own, feel free to get in touch so that I ## Features -| Module | Features | -|:-------------:|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| **Animation** | - Skeleton data structure
- Animation support _(in progress)_ | -| **Audio** | - Using [OpenAL Soft](https://openal-soft.org/)
- Playing/pausing/stopping/repeating sounds
- Positional audio sources & listener
- Sound effects (reverberation, chorus, distortion, echo, ...)
- Audio input (microphone) mono/stereo support | -| **Data** | - [Bounding Volume Hierarchy (BVH)](https://en.wikipedia.org/wiki/Bounding_volume_hierarchy) acceleration structure
- [Directed graph](https://en.wikipedia.org/wiki/Directed_graph) structure
- Dynamic bitset
- File formats:
    - Meshes:
        - [glTF/GLB](https://en.wikipedia.org/wiki/GlTF) import (using [fastgltf](https://github.com/spnda/fastgltf))
        - [OBJ](https://en.wikipedia.org/wiki/Wavefront_.obj_file) import/export
        - [FBX](https://en.wikipedia.org/wiki/FBX) import (using the [FBX SDK](https://www.autodesk.com/developer-network/platform-technologies/fbx))
        - [OFF](https://en.wikipedia.org/wiki/OFF_(file_format)) import
    - Images:
        - [PNG](https://en.wikipedia.org/wiki/Portable_Network_Graphics), [JPEG](https://en.wikipedia.org/wiki/JPEG), [BMP](https://en.wikipedia.org/wiki/BMP_file_format), [HDR](https://en.wikipedia.org/wiki/RGBE_image_format), [TGA](https://en.wikipedia.org/wiki/Truevision_TGA), [GIF](https://en.wikipedia.org/wiki/GIF), [PPM/PGM](https://en.wikipedia.org/wiki/Netpbm#File_formats), [PSD](https://en.wikipedia.org/wiki/Adobe_Photoshop#File_format), PIC import (using [stb_image](https://github.com/nothings/stb))
        - [PNG](https://en.wikipedia.org/wiki/Portable_Network_Graphics) import/export (using [libpng](http://www.libpng.org/pub/png/libpng.html))
        - [TGA](https://en.wikipedia.org/wiki/Truevision_TGA) import
    - Audio: [WAV](https://en.wikipedia.org/wiki/WAV) import/export
    - Animation: [BVH](https://en.wikipedia.org/wiki/Biovision_Hierarchy) import _(in progress)_ | -| **Math** | - Vectors, matrices & quaternions
- Angles (degrees/radians)
- Transformations (translation, rotation, scale)
- Noise ([Perlin](https://en.wikipedia.org/wiki/Perlin_noise), [Worley](https://en.wikipedia.org/wiki/Worley_noise)) | -| **Physics** | - Shapes (line, plane, sphere, triangle, quad, AABB, OBB)
- Shape/shape collision checks _(in progress)_
- Ray/shape intersection checks _(in progress)_
- Rigid body simulation _(in progress)_ | -| **Rendering** | - OpenGL (4.6-3.3)
- Vulkan _(in progress)_
- [PBR](https://en.wikipedia.org/wiki/Physically_based_rendering) (Cook-Torrance) & legacy ([Blinn-Phong](https://en.wikipedia.org/wiki/Blinn–Phong_reflection_model)) material models
- [Deferred rendering](https://en.wikipedia.org/wiki/Deferred_shading), using a custom render graph
- Post effects: [bloom](https://en.wikipedia.org/wiki/Bloom_(shader_effect)), [tone mapping](https://en.wikipedia.org/wiki/Tone_mapping), SSR, [SSAO](https://en.wikipedia.org/wiki/Screen_space_ambient_occlusion), ... _(in progress)_
- Tessellation & compute shaders support
- Camera (perspective/orthographic)
- Light sources (point & directional)
- Windowing (window, keyboard/mouse inputs with custom callbacks), using [GLFW](https://www.glfw.org/)
- Overlay, using [ImGui](https://github.com/ocornut/imgui)
- [Cubemap](https://en.wikipedia.org/wiki/Cube_mapping)
- [Normal mapping](https://en.wikipedia.org/wiki/Normal_mapping) | -| **Scripting** | - [Lua](https://www.lua.org/about.html) scripting, using [Sol2](https://github.com/ThePhD/sol2) | -| **Misc** | - Custom [ECS (Entity Component System)](https://en.wikipedia.org/wiki/Entity_component_system) implementation
- Uniformized platform-dependent path strings
- Logging utilities
- Multithreading utilities, thread pool implementation & parallelization functions
- Plugin utilities, to load dynamic libraries
- Compiler, enum, string, file, floating-point & type utilities | +| Module | Features | +|:-------------:|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| **Animation** | - Skeleton data structure
- Animation support _(in progress)_ | +| **Audio** | - Using [OpenAL Soft](https://openal-soft.org/)
- Playing/pausing/stopping/repeating sounds
- Positional audio sources & listener
- Sound effects (reverberation, chorus, distortion, echo, ...)
- Audio input (microphone) mono/stereo support | +| **Data** | - [Bounding Volume Hierarchy (BVH)](https://en.wikipedia.org/wiki/Bounding_volume_hierarchy) acceleration structure
- [Directed graph](https://en.wikipedia.org/wiki/Directed_graph) structure
- Mesh signed distance field
- Dynamic bitset
- File formats:
    - Meshes:
        - [glTF/GLB](https://en.wikipedia.org/wiki/GlTF) import (using [fastgltf](https://github.com/spnda/fastgltf))
        - [OBJ](https://en.wikipedia.org/wiki/Wavefront_.obj_file) import/export
        - [FBX](https://en.wikipedia.org/wiki/FBX) import (using the [FBX SDK](https://www.autodesk.com/developer-network/platform-technologies/fbx))
        - [OFF](https://en.wikipedia.org/wiki/OFF_(file_format)) import
    - Images:
        - [PNG](https://en.wikipedia.org/wiki/Portable_Network_Graphics), [JPEG](https://en.wikipedia.org/wiki/JPEG), [BMP](https://en.wikipedia.org/wiki/BMP_file_format), [HDR](https://en.wikipedia.org/wiki/RGBE_image_format), [TGA](https://en.wikipedia.org/wiki/Truevision_TGA), [GIF](https://en.wikipedia.org/wiki/GIF), [PPM/PGM](https://en.wikipedia.org/wiki/Netpbm#File_formats), [PSD](https://en.wikipedia.org/wiki/Adobe_Photoshop#File_format), PIC import (using [stb_image](https://github.com/nothings/stb))
        - [PNG](https://en.wikipedia.org/wiki/Portable_Network_Graphics) import/export (using [libpng](http://www.libpng.org/pub/png/libpng.html))
        - [TGA](https://en.wikipedia.org/wiki/Truevision_TGA) import
    - Audio: [WAV](https://en.wikipedia.org/wiki/WAV) import/export
    - Animation: [BVH](https://en.wikipedia.org/wiki/Biovision_Hierarchy) import _(in progress)_ | +| **Math** | - Vectors, matrices & quaternions
- Angles (degrees/radians)
- Transformations (translation, rotation, scale)
- Noise ([Perlin](https://en.wikipedia.org/wiki/Perlin_noise), [Worley](https://en.wikipedia.org/wiki/Worley_noise)) | +| **Physics** | - Shapes (line, plane, sphere, triangle, quad, AABB, OBB)
- Shape/shape collision checks _(in progress)_
- Ray/shape intersection checks _(in progress)_
- Rigid body simulation _(in progress)_ | +| **Rendering** | - OpenGL (4.6-3.3)
- Vulkan _(in progress)_
- [PBR](https://en.wikipedia.org/wiki/Physically_based_rendering) (Cook-Torrance) & legacy ([Blinn-Phong](https://en.wikipedia.org/wiki/Blinn–Phong_reflection_model)) material models
- [Deferred rendering](https://en.wikipedia.org/wiki/Deferred_shading), using a custom render graph
- Post effects: [bloom](https://en.wikipedia.org/wiki/Bloom_(shader_effect)), [tone mapping](https://en.wikipedia.org/wiki/Tone_mapping), SSR, [SSAO](https://en.wikipedia.org/wiki/Screen_space_ambient_occlusion), ... _(in progress)_
- Tessellation & compute shaders support
- Camera (perspective/orthographic)
- Light sources (point & directional)
- Windowing (window, keyboard/mouse inputs with custom callbacks), using [GLFW](https://www.glfw.org/)
- Overlay, using [ImGui](https://github.com/ocornut/imgui)
- [Cubemap](https://en.wikipedia.org/wiki/Cube_mapping)
- [Normal mapping](https://en.wikipedia.org/wiki/Normal_mapping) | +| **Scripting** | - [Lua](https://www.lua.org/about.html) scripting, using [Sol2](https://github.com/ThePhD/sol2) | +| **Misc** | - Custom [ECS (Entity Component System)](https://en.wikipedia.org/wiki/Entity_component_system) implementation
- Uniformized platform-dependent path strings
- Logging utilities
- Multithreading utilities, thread pool implementation & parallelization functions
- Plugin utilities, to load dynamic libraries
- Compiler, enum, string, file, floating-point & type utilities | diff --git a/include/RaZ/Data/MeshDistanceField.hpp b/include/RaZ/Data/MeshDistanceField.hpp new file mode 100644 index 00000000..9bed1743 --- /dev/null +++ b/include/RaZ/Data/MeshDistanceField.hpp @@ -0,0 +1,56 @@ +#pragma once + +#ifndef RAZ_MESHDISTANCEFIELD_HPP +#define RAZ_MESHDISTANCEFIELD_HPP + +#include "RaZ/Utils/Shape.hpp" + +#include + +namespace Raz { + +class BoundingVolumeHierarchy; +class Image; + +/// 3-dimensional structure of signed distances to the closest mesh geometry in a specific area. Distances inside a mesh will be negative. +class MeshDistanceField { +public: + /// Creates a mesh distance field. + /// \param area Area inside which the distances will be computed. + /// \param width Number of divisions along the width; must be equal to or greater than 2. + /// \param height Number of divisions along the height; must be equal to or greater than 2. + /// \param depth Number of divisions along the depth; must be equal to or greater than 2. + MeshDistanceField(const AABB& area, unsigned int width, unsigned int height, unsigned int depth); + + float getDistance(std::size_t widthIndex, std::size_t heightIndex, std::size_t depthIndex) const; + + void setBvh(const BoundingVolumeHierarchy& bvh) { m_bvh = &bvh; } + + /// Computes the distance field's values for each point within the grid. + /// \param sampleCount Number of directions to sample around each point; a higher count will result in a better definition. + /// \note This requires a BVH to have been set. + /// \see setBvh() + void compute(std::size_t sampleCount); + /// Recovers the distance field's values in a list of 2D floating-point images. + /// \return Images of each slice of the field along the depth. + std::vector recoverSlices() const; + +private: + constexpr std::size_t computeIndex(std::size_t widthIndex, std::size_t heightIndex, std::size_t depthIndex) const noexcept { + assert("Error: The given width index is invalid." && widthIndex < m_width); + assert("Error: The given height index is invalid." && heightIndex < m_height); + assert("Error: The given channel depth is invalid." && depthIndex < m_depth); + return depthIndex * m_height * m_width + heightIndex * m_width + widthIndex; + } + + AABB m_area = AABB(Vec3f(0.f), Vec3f(0.f)); + unsigned int m_width {}; + unsigned int m_height {}; + unsigned int m_depth {}; + std::vector m_distanceField {}; + const BoundingVolumeHierarchy* m_bvh = nullptr; +}; + +} // namespace Raz + +#endif // RAZ_MESHDISTANCEFIELD_HPP diff --git a/include/RaZ/RaZ.hpp b/include/RaZ/RaZ.hpp index 2dee954a..1b347598 100644 --- a/include/RaZ/RaZ.hpp +++ b/include/RaZ/RaZ.hpp @@ -26,6 +26,7 @@ #include "Data/Image.hpp" #include "Data/ImageFormat.hpp" #include "Data/Mesh.hpp" +#include "Data/MeshDistanceField.hpp" #include "Data/MeshFormat.hpp" #include "Data/ObjFormat.hpp" #include "Data/OffFormat.hpp" diff --git a/src/RaZ/Data/MeshDistanceField.cpp b/src/RaZ/Data/MeshDistanceField.cpp new file mode 100644 index 00000000..230a6ac1 --- /dev/null +++ b/src/RaZ/Data/MeshDistanceField.cpp @@ -0,0 +1,76 @@ +#include "RaZ/Data/BoundingVolumeHierarchy.hpp" +#include "RaZ/Data/Image.hpp" +#include "RaZ/Data/MeshDistanceField.hpp" +#include "RaZ/Math/MathUtils.hpp" +#include "RaZ/Utils/Ray.hpp" +#include "RaZ/Utils/Threading.hpp" + +namespace Raz { + +MeshDistanceField::MeshDistanceField(const AABB& area, unsigned int width, unsigned int height, unsigned int depth) + : m_area{ area }, m_width{ width }, m_height{ height }, m_depth{ depth }, m_distanceField(width * height * depth, std::numeric_limits::max()) { + if (m_width < 2 || m_height < 2 || m_depth < 2) + throw std::invalid_argument("[MeshDistanceField] The width, height & depth must all be equal to or greater than 2."); +} + +float MeshDistanceField::getDistance(std::size_t widthIndex, std::size_t heightIndex, std::size_t depthIndex) const { + return m_distanceField[computeIndex(widthIndex, heightIndex, depthIndex)]; +} + +void MeshDistanceField::compute(std::size_t sampleCount) { + if (m_bvh == nullptr) + throw std::runtime_error("[MeshDistanceField] Computing a mesh distance field requires having given a BVH."); + + std::fill(m_distanceField.begin(), m_distanceField.end(), std::numeric_limits::max()); + + const Vec3f areaExtents = m_area.getMaxPosition() - m_area.getMinPosition(); + const float widthStep = areaExtents.x() / static_cast(m_width - 1); + const float heightStep = areaExtents.y() / static_cast(m_height - 1); + const float depthStep = areaExtents.z() / static_cast(m_depth - 1); + + Threading::parallelize(0, m_depth, [this, widthStep, heightStep, depthStep, sampleCount] (const Threading::IndexRange& range) { + for (std::size_t depthIndex = range.beginIndex; depthIndex < range.endIndex; ++depthIndex) { + for (std::size_t heightIndex = 0; heightIndex < m_height; ++heightIndex) { + for (std::size_t widthIndex = 0; widthIndex < m_width; ++widthIndex) { + const Vec3f rayPos = m_area.getMinPosition() + Vec3f(static_cast(widthIndex) * widthStep, + static_cast(heightIndex) * heightStep, + static_cast(depthIndex) * depthStep); + float& distance = m_distanceField[computeIndex(widthIndex, heightIndex, depthIndex)]; + + for (const Vec3f& rayDir : MathUtils::computeFibonacciSpherePoints(sampleCount)) { + RayHit hit {}; + + if (!m_bvh->query(Ray(rayPos, rayDir), &hit)) + continue; + + if (rayDir.dot(hit.normal) > 0.f) + hit.distance = -hit.distance; + + if (std::abs(hit.distance) < std::abs(distance)) + distance = hit.distance; + } + } + } + } + }); +} + +std::vector MeshDistanceField::recoverSlices() const { + std::vector slices; + slices.reserve(m_depth); + + for (std::size_t depthIndex = 0; depthIndex < m_depth; ++depthIndex) { + Image& slice = slices.emplace_back(m_width, m_height, ImageColorspace::GRAY, ImageDataType::FLOAT); + + for (std::size_t heightIndex = 0; heightIndex < m_height; ++heightIndex) { + for (std::size_t widthIndex = 0; widthIndex < m_width; ++widthIndex) { + const float distance = getDistance(widthIndex, heightIndex, depthIndex); + slice.setPixel(widthIndex, heightIndex, distance); + } + } + } + + return slices; +} + +} // namespace Raz diff --git a/src/RaZ/Script/LuaData.cpp b/src/RaZ/Script/LuaData.cpp index 32d66be2..7401d934 100644 --- a/src/RaZ/Script/LuaData.cpp +++ b/src/RaZ/Script/LuaData.cpp @@ -2,6 +2,8 @@ #include "RaZ/Data/BoundingVolumeHierarchy.hpp" #include "RaZ/Data/BoundingVolumeHierarchySystem.hpp" #include "RaZ/Data/Color.hpp" +#include "RaZ/Data/Image.hpp" +#include "RaZ/Data/MeshDistanceField.hpp" #include "RaZ/Script/LuaWrapper.hpp" #include "RaZ/Utils/TypeUtils.hpp" @@ -99,6 +101,17 @@ void LuaWrapper::registerDataTypes() { colorPreset["Yellow"] = ColorPreset::Yellow; colorPreset["White"] = ColorPreset::White; } + + { + sol::usertype mdf = state.new_usertype("MeshDistanceField", + sol::constructors< + MeshDistanceField(const Raz::AABB&, unsigned int, unsigned int, unsigned int) + >()); + mdf["getDistance"] = &MeshDistanceField::getDistance; + mdf["setBvh"] = &MeshDistanceField::setBvh; + mdf["compute"] = &MeshDistanceField::compute; + mdf["recoverSlices"] = &MeshDistanceField::recoverSlices; + } } } // namespace Raz diff --git a/tests/src/RaZ/Data/MeshDistanceField.cpp b/tests/src/RaZ/Data/MeshDistanceField.cpp new file mode 100644 index 00000000..84ecdd22 --- /dev/null +++ b/tests/src/RaZ/Data/MeshDistanceField.cpp @@ -0,0 +1,175 @@ +#include "RaZ/Entity.hpp" +#include "RaZ/Data/BoundingVolumeHierarchy.hpp" +#include "RaZ/Data/Image.hpp" +#include "RaZ/Data/Mesh.hpp" +#include "RaZ/Data/MeshDistanceField.hpp" + +#include "CatchCustomMatchers.hpp" + +#include + +TEST_CASE("MeshDistanceField computation", "[data]") { + // See: https://www.geogebra.org/m/nn5jtkrt + + // The MDF requires a definition of at least 2 on each axis + CHECK_THROWS(Raz::MeshDistanceField(Raz::AABB({}, {}), 0, 0, 0)); + CHECK_THROWS(Raz::MeshDistanceField(Raz::AABB({}, {}), 1, 1, 1)); + CHECK_NOTHROW(Raz::MeshDistanceField(Raz::AABB({}, {}), 2, 2, 2)); + + const Raz::AABB fieldBox(Raz::Vec3f(-1.f), Raz::Vec3f(1.f)); + + Raz::MeshDistanceField mdf(fieldBox, 9, 9, 9); // Choosing odd numbers to get the exact center position + CHECK(mdf.getDistance(0, 0, 0) == std::numeric_limits::max()); + CHECK(mdf.getDistance(8, 8, 8) == std::numeric_limits::max()); + + CHECK_THROWS(mdf.compute(1)); // No BVH set + + Raz::BoundingVolumeHierarchy bvh; + mdf.setBvh(bvh); + + mdf.compute(1); // The BVH is empty, nothing will be computed + CHECK(mdf.getDistance(0, 0, 0) == std::numeric_limits::max()); + CHECK(mdf.getDistance(8, 8, 8) == std::numeric_limits::max()); + + Raz::Entity mesh(0); + mesh.addComponent(Raz::AABB(fieldBox.getMinPosition() * 0.5f, fieldBox.getMaxPosition() * 0.5f)); + bvh.build({ &mesh }); + + mdf.compute(1); // A sample count too low isn't enough to find much intersection + // Corners + CHECK(mdf.getDistance(0, 0, 0) == std::numeric_limits::max()); + CHECK(mdf.getDistance(8, 8, 8) == std::numeric_limits::max()); + // Edges + CHECK(mdf.getDistance(4, 0, 0) == std::numeric_limits::max()); + CHECK(mdf.getDistance(0, 4, 0) == std::numeric_limits::max()); + CHECK(mdf.getDistance(0, 0, 4) == std::numeric_limits::max()); + // Faces + CHECK(mdf.getDistance(0, 4, 4) == 0.5f); // The first sample direction should be +X + CHECK(mdf.getDistance(4, 0, 4) == std::numeric_limits::max()); + CHECK(mdf.getDistance(4, 4, 0) == std::numeric_limits::max()); + CHECK(mdf.getDistance(4, 4, 8) == std::numeric_limits::max()); + CHECK(mdf.getDistance(4, 8, 4) == std::numeric_limits::max()); + CHECK(mdf.getDistance(8, 4, 4) == std::numeric_limits::max()); + // Center + CHECK(mdf.getDistance(4, 4, 4) == -0.5f); + + mdf.compute(2); + // Corners + CHECK(mdf.getDistance(0, 0, 0) == std::numeric_limits::max()); + CHECK(mdf.getDistance(8, 8, 8) == 1.f); + // Edges + CHECK(mdf.getDistance(4, 0, 0) == std::numeric_limits::max()); + CHECK(mdf.getDistance(0, 4, 0) == std::numeric_limits::max()); + CHECK(mdf.getDistance(0, 0, 4) == 1.f); + // Faces + CHECK_THAT(mdf.getDistance(0, 4, 4), IsNearlyEqualTo(0.577350259f)); + CHECK(mdf.getDistance(4, 0, 4) == std::numeric_limits::max()); + CHECK(mdf.getDistance(4, 4, 0) == std::numeric_limits::max()); + CHECK(mdf.getDistance(4, 4, 8) == std::numeric_limits::max()); + CHECK(mdf.getDistance(4, 8, 4) == std::numeric_limits::max()); + CHECK_THAT(mdf.getDistance(8, 4, 4), IsNearlyEqualTo(0.782987058f)); + // Center + CHECK_THAT(mdf.getDistance(4, 4, 4), IsNearlyEqualTo(-0.577350259f)); + + mdf.compute(100); + // Corner to corner, should tend toward 0.87 + CHECK_THAT(mdf.getDistance(0, 0, 0), IsNearlyEqualTo(0.912753105f)); + CHECK_THAT(mdf.getDistance(8, 0, 0), IsNearlyEqualTo(0.966534495f)); + CHECK_THAT(mdf.getDistance(8, 8, 0), IsNearlyEqualTo(0.980392158f)); + CHECK_THAT(mdf.getDistance(0, 8, 0), IsNearlyEqualTo(1.06382978f)); + CHECK_THAT(mdf.getDistance(0, 8, 8), IsNearlyEqualTo(1.162790895f)); + CHECK_THAT(mdf.getDistance(0, 0, 8), IsNearlyEqualTo(1.045123935f)); + CHECK_THAT(mdf.getDistance(8, 0, 8), IsNearlyEqualTo(1.048231006f)); + CHECK_THAT(mdf.getDistance(8, 8, 8), IsNearlyEqualTo(1.132885695f)); + // Edge to edge, should tend toward 0.71 + CHECK_THAT(mdf.getDistance(4, 0, 0), IsNearlyEqualTo(0.724637687f)); + CHECK_THAT(mdf.getDistance(0, 4, 0), IsNearlyEqualTo(0.772372246f)); + CHECK_THAT(mdf.getDistance(0, 0, 4), IsNearlyEqualTo(0.749056816f)); + CHECK_THAT(mdf.getDistance(4, 8, 8), IsNearlyEqualTo(0.761324167f)); + CHECK_THAT(mdf.getDistance(8, 4, 8), IsNearlyEqualTo(0.76935935f)); + CHECK_THAT(mdf.getDistance(8, 8, 4), IsNearlyEqualTo(0.725454986f)); + // Side to side, should tend toward 0.5 + CHECK_THAT(mdf.getDistance(4, 4, 0), IsNearlyEqualTo(0.510620058f)); + CHECK_THAT(mdf.getDistance(0, 4, 4), IsNearlyEqualTo(0.503709972f)); + CHECK_THAT(mdf.getDistance(4, 0, 4), IsNearlyEqualTo(0.50505048f)); + CHECK_THAT(mdf.getDistance(4, 4, 8), IsNearlyEqualTo(0.501562536f)); + CHECK_THAT(mdf.getDistance(8, 4, 4), IsNearlyEqualTo(0.504095972f)); + CHECK_THAT(mdf.getDistance(4, 8, 4), IsNearlyEqualTo(0.50505048f)); + // Inside at mid-distance between center & corners, should tend toward -0.25 + CHECK_THAT(mdf.getDistance(3, 3, 3), IsNearlyEqualTo(-0.250781268f)); + CHECK_THAT(mdf.getDistance(3, 3, 5), IsNearlyEqualTo(-0.252047986f)); + CHECK_THAT(mdf.getDistance(3, 5, 3), IsNearlyEqualTo(-0.250781268f)); + CHECK_THAT(mdf.getDistance(3, 5, 5), IsNearlyEqualTo(-0.252047986f)); + CHECK_THAT(mdf.getDistance(5, 3, 3), IsNearlyEqualTo(-0.250781268f)); + CHECK_THAT(mdf.getDistance(5, 3, 5), IsNearlyEqualTo(-0.251854986f)); + CHECK_THAT(mdf.getDistance(5, 5, 3), IsNearlyEqualTo(-0.250781268f)); + CHECK_THAT(mdf.getDistance(5, 5, 5), IsNearlyEqualTo(-0.251854986f)); + // Center to side, should tend toward -0.5 + CHECK_THAT(mdf.getDistance(4, 4, 4), IsNearlyEqualTo(-0.501562536f)); + + // Degenerate cases due to a grid definition too low, should tend toward 0 but fail to find an intersection other than the closest opposite inner face: + + // Right on each corner + CHECK_THAT(mdf.getDistance(2, 2, 2), IsNearlyEqualTo(-1.010100961f)); + CHECK_THAT(mdf.getDistance(2, 2, 6), IsNearlyEqualTo(-1.048736811f)); + CHECK_THAT(mdf.getDistance(2, 6, 2), IsNearlyEqualTo(-1.010100961f)); + CHECK_THAT(mdf.getDistance(2, 6, 6), IsNearlyEqualTo(-1.003125072f)); + CHECK_THAT(mdf.getDistance(6, 2, 2), IsNearlyEqualTo(-1.022850156f)); + CHECK_THAT(mdf.getDistance(6, 2, 6), IsNearlyEqualTo(-1.030927777f)); + CHECK_THAT(mdf.getDistance(6, 6, 2), IsNearlyEqualTo(-1.045168638f)); + CHECK_THAT(mdf.getDistance(6, 6, 6), IsNearlyEqualTo(-1.008191943f)); + // Right in the middle of each edge + CHECK_THAT(mdf.getDistance(2, 4, 2), IsNearlyEqualTo(-0.50505048f)); + CHECK_THAT(mdf.getDistance(2, 4, 6), IsNearlyEqualTo(-0.526315749f)); + CHECK_THAT(mdf.getDistance(4, 2, 2), IsNearlyEqualTo(-0.515975773f)); + CHECK_THAT(mdf.getDistance(4, 2, 6), IsNearlyEqualTo(-0.524368405f)); + CHECK_THAT(mdf.getDistance(4, 6, 2), IsNearlyEqualTo(-0.522584319f)); + CHECK_THAT(mdf.getDistance(4, 6, 6), IsNearlyEqualTo(-0.503709972f)); + CHECK_THAT(mdf.getDistance(6, 4, 2), IsNearlyEqualTo(-0.537634432f)); + CHECK_THAT(mdf.getDistance(6, 4, 6), IsNearlyEqualTo(-0.515463889f)); + // Right in the middle of each face + CHECK_THAT(mdf.getDistance(2, 4, 4), IsNearlyEqualTo(-0.501562536f)); + CHECK_THAT(mdf.getDistance(6, 4, 4), IsNearlyEqualTo(-0.511425078f)); + CHECK_THAT(mdf.getDistance(4, 2, 4), IsNearlyEqualTo(-0.511425078f)); + CHECK_THAT(mdf.getDistance(4, 6, 4), IsNearlyEqualTo(-0.501562536f)); + CHECK_THAT(mdf.getDistance(4, 4, 2), IsNearlyEqualTo(-0.50505048f)); + CHECK_THAT(mdf.getDistance(4, 4, 6), IsNearlyEqualTo(-0.503709972f)); +} + +TEST_CASE("MeshDistanceField slices", "[data]") { + // Creating a distance field with a single triangle inside + // + // -----^----- + // | / \ | + // |/_______\| + + Raz::Entity mesh(0); + mesh.addComponent(Raz::Triangle(Raz::Vec3f(-1.f, -0.5f, 0.f), Raz::Vec3f(1.f, -0.5f, 0.f), Raz::Vec3f(0.f, 0.5f, 0.f)), + Raz::Vec2f(), Raz::Vec2f(), Raz::Vec2f()); + + Raz::BoundingVolumeHierarchy bvh; + bvh.build({ &mesh }); + + Raz::MeshDistanceField mdf(Raz::AABB(Raz::Vec3f(-1.f, -0.5f, -0.25f), Raz::Vec3f(1.f, 0.5f, 0.25f)), 4, 3, 2); + mdf.setBvh(bvh); + mdf.compute(10); + + constexpr float backDistance = -0.289784729f; + constexpr float frontDistance = 0.271375328f; + + CHECK_THAT(mdf.getDistance(1, 1, 0), IsNearlyEqualTo(backDistance)); + CHECK_THAT(mdf.getDistance(1, 1, 1), IsNearlyEqualTo(frontDistance)); + + const std::vector imageSlices = mdf.recoverSlices(); + REQUIRE(imageSlices.size() == 2); // The number of slices is equal to the MDF's depth definition + + for (const Raz::Image& depthSlice : imageSlices) { + CHECK(depthSlice.getWidth() == 4); + CHECK(depthSlice.getHeight() == 3); + CHECK(depthSlice.getColorspace() == Raz::ImageColorspace::GRAY); + CHECK(depthSlice.getDataType() == Raz::ImageDataType::FLOAT); + } + + CHECK_THAT(imageSlices[0].recoverPixel(1, 1), IsNearlyEqualTo(backDistance)); + CHECK_THAT(imageSlices[1].recoverPixel(1, 1), IsNearlyEqualTo(frontDistance)); +} diff --git a/tests/src/RaZ/Script/LuaData.cpp b/tests/src/RaZ/Script/LuaData.cpp index 3c839c6a..5e5369b9 100644 --- a/tests/src/RaZ/Script/LuaData.cpp +++ b/tests/src/RaZ/Script/LuaData.cpp @@ -156,6 +156,17 @@ TEST_CASE("LuaData Mesh", "[script][lua][data]") { #endif } +TEST_CASE("LuaData MeshDistanceField", "[script][lua][data]") { + CHECK(Raz::LuaWrapper::execute(R"( + local mdf = MeshDistanceField.new(AABB.new(Vec3f.new(), Vec3f.new()), 2, 2, 2) + + assert(mdf:getDistance(0, 0, 0) ~= 0) + mdf:setBvh(BoundingVolumeHierarchy.new()) + mdf:compute(1) + assert(mdf:recoverSlices():size() == 2) + )")); +} + TEST_CASE("LuaData Submesh", "[script][lua][data]") { CHECK(Raz::LuaWrapper::execute(R"( local submesh = Submesh.new()