├── .gitignore ├── chart.png ├── render.jpg ├── cmake ├── bvh-config.cmake.in └── Install.cmake ├── test ├── load_obj.h ├── CMakeLists.txt ├── serialize.cpp ├── scenes │ └── cornell_box.obj ├── simple_example.cpp ├── load_obj.cpp ├── c_api_example.c └── benchmark.cpp ├── src └── bvh │ └── v2 │ ├── CMakeLists.txt │ ├── platform.h │ ├── c_api │ ├── CMakeLists.txt │ ├── bvh.cpp │ ├── bvh.h │ └── bvh_impl.h │ ├── stack.h │ ├── bbox.h │ ├── ray.h │ ├── split_heuristic.h │ ├── sphere.h │ ├── stream.h │ ├── tri.h │ ├── thread_pool.h │ ├── index.h │ ├── default_builder.h │ ├── executor.h │ ├── node.h │ ├── vec.h │ ├── utils.h │ ├── top_down_sah_builder.h │ ├── sweep_sah_builder.h │ ├── binned_sah_builder.h │ ├── bvh.h │ ├── reinsertion_optimizer.h │ └── mini_tree_builder.h ├── CMakeLists.txt ├── LICENSE.txt ├── .github └── workflows │ └── build-and-test.yml └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | build/ 3 | install/ 4 | -------------------------------------------------------------------------------- /chart.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/madmann91/bvh/HEAD/chart.png -------------------------------------------------------------------------------- /render.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/madmann91/bvh/HEAD/render.jpg -------------------------------------------------------------------------------- /cmake/bvh-config.cmake.in: -------------------------------------------------------------------------------- 1 | @PACKAGE_INIT@ 2 | 3 | include("${CMAKE_CURRENT_LIST_DIR}/bvh-targets.cmake") 4 | 5 | check_required_components(bvh_c) 6 | -------------------------------------------------------------------------------- /test/load_obj.h: -------------------------------------------------------------------------------- 1 | #ifndef LOAD_OBJ_H 2 | #define LOAD_OBJ_H 3 | 4 | #include 5 | 6 | #include 7 | 8 | #ifdef __cplusplus 9 | extern "C" { 10 | #endif 11 | 12 | struct tri { struct bvh_vec3f v[3]; }; 13 | struct tri* load_obj(const char*, size_t*); 14 | 15 | #ifdef __cplusplus 16 | } 17 | #endif 18 | 19 | #ifdef __cplusplus 20 | #include 21 | 22 | #include 23 | #include 24 | 25 | template 26 | std::vector> load_obj(const std::string& file); 27 | #endif 28 | 29 | #endif 30 | -------------------------------------------------------------------------------- /src/bvh/v2/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | add_library(bvh INTERFACE) 2 | find_package(Threads) 3 | if (Threads_FOUND) 4 | # Link with the threading library of the system, which may 5 | # be required by standard header on some systems 6 | target_link_libraries(bvh INTERFACE Threads::Threads) 7 | endif() 8 | 9 | target_include_directories(bvh INTERFACE 10 | $ 11 | $) 12 | 13 | set_target_properties(bvh PROPERTIES CXX_STANDARD 20) 14 | 15 | if (BVH_BUILD_C_API) 16 | add_subdirectory(c_api) 17 | endif() 18 | -------------------------------------------------------------------------------- /src/bvh/v2/platform.h: -------------------------------------------------------------------------------- 1 | #ifndef BVH_V2_PLATFORM_H 2 | #define BVH_V2_PLATFORM_H 3 | 4 | #if defined(__clang__) 5 | #define BVH_CLANG_ENABLE_FP_CONTRACT \ 6 | _Pragma("clang diagnostic push") \ 7 | _Pragma("clang diagnostic ignored \"-Wunknown-pragmas\"") \ 8 | _Pragma("STDC FP_CONTRACT ON") \ 9 | _Pragma("clang diagnostic pop") 10 | #else 11 | #define BVH_CLANG_ENABLE_FP_CONTRACT 12 | #endif 13 | 14 | #if defined(__GNUC__) || defined(__clang__) 15 | #define BVH_ALWAYS_INLINE __attribute__((always_inline)) inline 16 | #elif defined(_MSC_VER) 17 | #define BVH_ALWAYS_INLINE __forceinline 18 | #else 19 | #define BVH_ALWAYS_INLINE inline 20 | #endif 21 | 22 | #endif 23 | -------------------------------------------------------------------------------- /src/bvh/v2/c_api/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | add_library(bvh_c SHARED bvh.cpp) 2 | target_link_libraries(bvh_c PRIVATE bvh) 3 | target_compile_definitions(bvh_c PRIVATE -DBVH_BUILD_API) 4 | target_include_directories(bvh_c INTERFACE 5 | $ 6 | $) 7 | set_target_properties(bvh_c PROPERTIES 8 | CXX_STANDARD 20 9 | CXX_VISIBILITY_PRESET hidden 10 | INTERPROCEDURAL_OPTIMIZATION_RELEASE ON) 11 | 12 | if (BVH_C_API_STATIC_LINK_STDLIB) 13 | # Link statically against standard C++ library 14 | target_link_options(bvh_c PRIVATE $<$:-static-libstdc++>) 15 | endif() 16 | 17 | if (BVH_C_API_UNSAFE_CASTS) 18 | target_compile_definitions(bvh_c PRIVATE -DBVH_C_API_UNSAFE_CASTS) 19 | endif() 20 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.21) 2 | project(bvh VERSION 2.0) 3 | 4 | option(BVH_BUILD_C_API "Builds the C API library wrapper" OFF) 5 | option(BVH_C_API_STATIC_LINK_STDLIB "Link the C API library statically against the standard C++ library (only supported by clang/gcc)" OFF) 6 | option(BVH_C_API_UNSAFE_CASTS "Enable unsafe casts in C API" OFF) 7 | 8 | set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin) 9 | set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib) 10 | set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib) 11 | 12 | add_subdirectory(src/bvh/v2) 13 | 14 | if (PROJECT_IS_TOP_LEVEL) 15 | include(CTest) 16 | if (BUILD_TESTING) 17 | add_subdirectory(test) 18 | endif() 19 | 20 | include(cmake/Install.cmake) 21 | endif() 22 | -------------------------------------------------------------------------------- /src/bvh/v2/c_api/bvh.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | namespace bvh::v2::c_api { 6 | 7 | BVH_TYPES(float, 2, 2f) 8 | BVH_TYPES(float, 3, 3f) 9 | BVH_TYPES(double, 2, 2d) 10 | BVH_TYPES(double, 3, 3d) 11 | 12 | extern "C" { 13 | 14 | BVH_EXPORT struct bvh_thread_pool* bvh_thread_pool_create(size_t thread_count) { 15 | return reinterpret_cast(new bvh::v2::ThreadPool(thread_count)); 16 | } 17 | 18 | BVH_EXPORT void bvh_thread_pool_destroy(bvh_thread_pool* thread_pool) { 19 | return delete reinterpret_cast(thread_pool); 20 | } 21 | 22 | BVH_IMPL(float, 2, 2f) 23 | BVH_IMPL(float, 3, 3f) 24 | BVH_IMPL(double, 2, 2d) 25 | BVH_IMPL(double, 3, 3d) 26 | 27 | } // extern "C" 28 | } // namespace bvh::v2::c_api 29 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright 2022 Arsène Pérard-Gayot 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and 4 | associated documentation files (the "Software"), to deal in the Software without restriction, 5 | including without limitation the rights to use, copy, modify, merge, publish, distribute, 6 | sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is 7 | furnished to do so, subject to the following conditions: 8 | 9 | The above copyright notice and this permission notice shall be included in all copies or substantial 10 | portions of the Software. 11 | 12 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT 13 | NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 14 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES 15 | OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 16 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 17 | -------------------------------------------------------------------------------- /.github/workflows/build-and-test.yml: -------------------------------------------------------------------------------- 1 | name: build-and-test 2 | 3 | on: 4 | push: 5 | branches: [master] 6 | pull_request: 7 | 8 | jobs: 9 | build-and-test: 10 | name: Build and test on ${{matrix.os}} in ${{matrix.build-type}} mode 11 | runs-on: ${{matrix.os}} 12 | strategy: 13 | matrix: 14 | build-type: [Debug, Release] 15 | c-api: [ON, OFF] 16 | os: [ubuntu-latest, windows-latest, macos-latest] 17 | 18 | steps: 19 | - uses: actions/checkout@v2 20 | 21 | - name: Create Build Environment 22 | run: cmake -E make_directory ${{runner.workspace}}/build 23 | 24 | - name: Configure CMake 25 | working-directory: ${{runner.workspace}}/build 26 | shell: bash # Necessary because of the $GITHUB_WORKSPACE variable 27 | run: cmake -DCMAKE_BUILD_TYPE=${{matrix.build-type}} -DBVH_BUILD_C_API=${{matrix.c-api}} -DBUILD_TESTING=ON -S $GITHUB_WORKSPACE 28 | 29 | - name: Build 30 | working-directory: ${{runner.workspace}}/build 31 | run: cmake --build . --config ${{matrix.build-type}} 32 | 33 | - name: Test 34 | working-directory: ${{runner.workspace}}/build 35 | run: ctest -C ${{matrix.build-type}} --verbose --rerun-failed --output-on-failure 36 | -------------------------------------------------------------------------------- /src/bvh/v2/stack.h: -------------------------------------------------------------------------------- 1 | #ifndef BVH_V2_STACK_H 2 | #define BVH_V2_STACK_H 3 | 4 | #include 5 | #include 6 | 7 | namespace bvh::v2 { 8 | 9 | /// Fixed-size stack that can be used for a BVH traversal. 10 | template 11 | struct SmallStack { 12 | static constexpr unsigned capacity = Capacity; 13 | 14 | T elems[capacity]; 15 | unsigned size = 0; 16 | 17 | bool is_empty() const { return size == 0; } 18 | bool is_full() const { return size >= capacity; } 19 | 20 | void push(const T& t) { 21 | assert(!is_full()); 22 | elems[size++] = t; 23 | } 24 | 25 | T pop() { 26 | assert(!is_empty()); 27 | return elems[--size]; 28 | } 29 | }; 30 | 31 | /// Growing stack that can be used for BVH traversal. Its performance may be lower than a small, 32 | /// fixed-size stack, depending on the architecture. 33 | template 34 | struct GrowingStack { 35 | std::vector elems; 36 | 37 | bool is_empty() const { return elems.empty(); } 38 | void push(const T& t) { elems.push_back(t); } 39 | 40 | T pop() { 41 | assert(!is_empty()); 42 | auto top = std::move(elems.back()); 43 | elems.pop_back(); 44 | return top; 45 | } 46 | }; 47 | 48 | } // namespace bvh::v2 49 | 50 | #endif 51 | -------------------------------------------------------------------------------- /cmake/Install.cmake: -------------------------------------------------------------------------------- 1 | set(bvh_targets bvh) 2 | if (BVH_BUILD_C_API) 3 | list(APPEND bvh_targets bvh_c) 4 | endif() 5 | 6 | install( 7 | DIRECTORY ${PROJECT_SOURCE_DIR}/src/bvh 8 | DESTINATION include 9 | FILES_MATCHING PATTERN "*.h" 10 | PATTERN "c_api" EXCLUDE) 11 | install( 12 | FILES ${PROJECT_SOURCE_DIR}/src/bvh/v2/c_api/bvh.h 13 | DESTINATION include/bvh/v2/c_api/) 14 | install( 15 | TARGETS ${bvh_targets} 16 | EXPORT bvh_exports 17 | RUNTIME DESTINATION bin/ 18 | LIBRARY DESTINATION lib/ 19 | ARCHIVE DESTINATION lib/ 20 | INCLUDES DESTINATION include/) 21 | install( 22 | EXPORT bvh_exports 23 | FILE bvh-targets.cmake 24 | NAMESPACE bvh::v2:: 25 | DESTINATION lib/cmake/bvh/v2/) 26 | 27 | include(CMakePackageConfigHelpers) 28 | 29 | configure_package_config_file( 30 | "${PROJECT_SOURCE_DIR}/cmake/bvh-config.cmake.in" 31 | "${CMAKE_CURRENT_BINARY_DIR}/bvh-config.cmake" 32 | INSTALL_DESTINATION lib/cmake/bvh/v2/) 33 | 34 | write_basic_package_version_file( 35 | "${CMAKE_CURRENT_BINARY_DIR}/bvh-config-version.cmake" 36 | COMPATIBILITY AnyNewerVersion) 37 | 38 | install( 39 | FILES 40 | "${CMAKE_CURRENT_BINARY_DIR}/bvh-config.cmake" 41 | "${CMAKE_CURRENT_BINARY_DIR}/bvh-config-version.cmake" 42 | DESTINATION lib/cmake/bvh/v2/) 43 | -------------------------------------------------------------------------------- /test/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | if(CMAKE_CXX_COMPILER_ID STREQUAL "Clang" OR CMAKE_CXX_COMPILER_ID STREQUAL "GNU") 2 | add_compile_options(-Wall -Wextra -pedantic -march=native) 3 | endif() 4 | 5 | add_executable(simple_example simple_example.cpp) 6 | target_link_libraries(simple_example PUBLIC bvh) 7 | set_target_properties(simple_example PROPERTIES CXX_STANDARD 20) 8 | add_test(NAME simple_example COMMAND simple_example) 9 | 10 | add_executable(serialize serialize.cpp) 11 | target_link_libraries(serialize PUBLIC bvh) 12 | set_target_properties(serialize PROPERTIES CXX_STANDARD 20) 13 | add_test(NAME serialize COMMAND serialize) 14 | 15 | add_executable(benchmark benchmark.cpp load_obj.cpp) 16 | target_link_libraries(benchmark PUBLIC bvh) 17 | set_target_properties(benchmark PROPERTIES CXX_STANDARD 20) 18 | add_test( 19 | NAME benchmark 20 | COMMAND 21 | benchmark ${CMAKE_CURRENT_SOURCE_DIR}/scenes/cornell_box.obj 22 | --eye 0 1 2 23 | --dir 0 0 -1 24 | --up 0 1 0) 25 | 26 | if (BVH_BUILD_C_API) 27 | add_executable(c_api_example c_api_example.c load_obj.cpp) 28 | target_link_libraries(c_api_example PUBLIC bvh_c) 29 | set_target_properties(c_api_example PROPERTIES CXX_STANDARD 20 C_STANDARD 11) 30 | 31 | add_test( 32 | NAME c_api_example 33 | COMMAND 34 | c_api_example 35 | ${CMAKE_CURRENT_SOURCE_DIR}/scenes/cornell_box.obj 36 | --eye 0 1 2 37 | --dir 0 0 -1 38 | --up 0 1 0) 39 | endif() 40 | -------------------------------------------------------------------------------- /src/bvh/v2/bbox.h: -------------------------------------------------------------------------------- 1 | #ifndef BVH_V2_BBOX_H 2 | #define BVH_V2_BBOX_H 3 | 4 | #include "bvh/v2/vec.h" 5 | #include "bvh/v2/utils.h" 6 | 7 | #include 8 | 9 | namespace bvh::v2 { 10 | 11 | template 12 | struct BBox { 13 | Vec min, max; 14 | 15 | BBox() = default; 16 | BVH_ALWAYS_INLINE BBox(const Vec& min, const Vec& max) : min(min), max(max) {} 17 | BVH_ALWAYS_INLINE explicit BBox(const Vec& point) : BBox(point, point) {} 18 | 19 | BVH_ALWAYS_INLINE BBox& extend(const Vec& point) { 20 | return extend(BBox(point)); 21 | } 22 | 23 | BVH_ALWAYS_INLINE BBox& extend(const BBox& other) { 24 | min = robust_min(min, other.min); 25 | max = robust_max(max, other.max); 26 | return *this; 27 | } 28 | 29 | BVH_ALWAYS_INLINE Vec get_diagonal() const { return max - min; } 30 | BVH_ALWAYS_INLINE Vec get_center() const { return (max + min) * static_cast(0.5); } 31 | 32 | BVH_ALWAYS_INLINE T get_half_area() const { 33 | auto d = get_diagonal(); 34 | static_assert(N == 2 || N == 3); 35 | if constexpr (N == 3) return (d[0] + d[1]) * d[2] + d[0] * d[1]; 36 | if constexpr (N == 2) return d[0] + d[1]; 37 | return static_cast(0.); 38 | } 39 | 40 | BVH_ALWAYS_INLINE static constexpr BBox make_empty() { 41 | return BBox( 42 | Vec(+std::numeric_limits::max()), 43 | Vec(-std::numeric_limits::max())); 44 | } 45 | }; 46 | 47 | } // namespace bvh::v2 48 | 49 | #endif 50 | -------------------------------------------------------------------------------- /src/bvh/v2/ray.h: -------------------------------------------------------------------------------- 1 | #ifndef BVH_V2_RAY_H 2 | #define BVH_V2_RAY_H 3 | 4 | #include "bvh/v2/vec.h" 5 | 6 | namespace bvh::v2 { 7 | 8 | struct Octant { 9 | uint32_t value = 0; 10 | static constexpr size_t max_dim = sizeof(value) * CHAR_BIT; 11 | 12 | uint32_t operator [] (size_t i) const { return (value >> i) & uint32_t{1}; } 13 | }; 14 | 15 | template 16 | struct Ray { 17 | Vec org, dir; 18 | T tmin, tmax; 19 | 20 | Ray() = default; 21 | BVH_ALWAYS_INLINE Ray( 22 | const Vec& org, 23 | const Vec& dir, 24 | T tmin = 0, 25 | T tmax = std::numeric_limits::max()) 26 | : org(org), dir(dir), tmin(tmin), tmax(tmax) 27 | {} 28 | 29 | template 30 | BVH_ALWAYS_INLINE Vec get_inv_dir() const { 31 | return Vec::generate([&] (size_t i) { 32 | return SafeInverse ? safe_inverse(dir[i]) : static_cast(1) / dir[i]; 33 | }); 34 | } 35 | 36 | BVH_ALWAYS_INLINE Octant get_octant() const { 37 | static_assert(N <= Octant::max_dim); 38 | Octant octant; 39 | static_for<0, N>([&] (size_t i) { 40 | octant.value |= std::signbit(dir[i]) * (uint32_t{1} << i); 41 | }); 42 | return octant; 43 | } 44 | 45 | // Pads the inverse direction according to T. Ize's "Robust BVH ray traversal" 46 | BVH_ALWAYS_INLINE static Vec pad_inv_dir(const Vec& inv_dir) { 47 | return Vec::generate([&] (size_t i) { return add_ulp_magnitude(inv_dir[i], 2); }); 48 | } 49 | }; 50 | 51 | } // namespace bvh::v2 52 | 53 | #endif 54 | -------------------------------------------------------------------------------- /src/bvh/v2/split_heuristic.h: -------------------------------------------------------------------------------- 1 | #ifndef BVH_V2_SPLIT_HEURISTIC_H 2 | #define BVH_V2_SPLIT_HEURISTIC_H 3 | 4 | #include "bvh/v2/bbox.h" 5 | #include "bvh/v2/utils.h" 6 | 7 | #include 8 | 9 | namespace bvh::v2 { 10 | 11 | template 12 | class SplitHeuristic { 13 | public: 14 | /// Creates an SAH evaluator object, used by top-down builders to determine where to split. 15 | /// The two parameters are the log of the size of primitive clusters in base 2, and the ratio of 16 | /// the cost of intersecting a node (a ray-box intersection) over the cost of intersecting a 17 | /// primitive. 18 | BVH_ALWAYS_INLINE SplitHeuristic( 19 | size_t log_cluster_size = 0, 20 | T cost_ratio = static_cast(1.)) 21 | : log_cluster_size_(log_cluster_size) 22 | , prim_offset_(make_bitmask(log_cluster_size)) 23 | , cost_ratio_(cost_ratio) 24 | {} 25 | 26 | BVH_ALWAYS_INLINE size_t get_prim_count(size_t size) const { 27 | return (size + prim_offset_) >> log_cluster_size_; 28 | } 29 | 30 | template 31 | BVH_ALWAYS_INLINE T get_leaf_cost(size_t begin, size_t end, const BBox& bbox) const { 32 | return bbox.get_half_area() * static_cast(get_prim_count(end - begin)); 33 | } 34 | 35 | template 36 | BVH_ALWAYS_INLINE T get_non_split_cost(size_t begin, size_t end, const BBox& bbox) const { 37 | return bbox.get_half_area() * (static_cast(get_prim_count(end - begin)) - cost_ratio_); 38 | } 39 | 40 | private: 41 | size_t log_cluster_size_; 42 | size_t prim_offset_; 43 | T cost_ratio_; 44 | }; 45 | 46 | } // namespace bvh::v2 47 | 48 | #endif 49 | -------------------------------------------------------------------------------- /src/bvh/v2/sphere.h: -------------------------------------------------------------------------------- 1 | #ifndef BVH_V2_SPHERE_H 2 | #define BVH_V2_SPHERE_H 3 | 4 | #include "bvh/v2/vec.h" 5 | #include "bvh/v2/ray.h" 6 | 7 | #include 8 | #include 9 | #include 10 | 11 | namespace bvh::v2 { 12 | 13 | /// Sphere primitive defined by a center and a radius. 14 | template 15 | struct Sphere { 16 | Vec center; 17 | T radius; 18 | 19 | BVH_ALWAYS_INLINE Sphere() = default; 20 | BVH_ALWAYS_INLINE Sphere(const Vec& center, T radius) 21 | : center(center), radius(radius) 22 | {} 23 | 24 | BVH_ALWAYS_INLINE Vec get_center() const { return center; } 25 | BVH_ALWAYS_INLINE BBox get_bbox() const { 26 | return BBox(center - Vec(radius), center + Vec(radius)); 27 | } 28 | 29 | /// Intersects a ray with the sphere. If the ray is normalized, a dot product can be saved by 30 | /// setting `AssumeNormalized` to true. 31 | template 32 | [[nodiscard]] BVH_ALWAYS_INLINE std::optional> intersect(const Ray& ray) const { 33 | auto oc = ray.org - center; 34 | auto a = AssumeNormalized ? static_cast(1.) : dot(ray.dir, ray.dir); 35 | auto b = static_cast(2.) * dot(ray.dir, oc); 36 | auto c = dot(oc, oc) - radius * radius; 37 | 38 | auto delta = b * b - static_cast(4.) * a * c; 39 | if (delta >= 0) { 40 | auto inv = -static_cast(0.5) / a; 41 | auto sqrt_delta = std::sqrt(delta); 42 | auto t0 = robust_max((b + sqrt_delta) * inv, ray.tmin); 43 | auto t1 = robust_min((b - sqrt_delta) * inv, ray.tmax); 44 | if (t0 <= t1) 45 | return std::make_optional(std::make_pair(t0, t1)); 46 | } 47 | 48 | return std::nullopt; 49 | } 50 | }; 51 | 52 | } // namespace bvh::v2 53 | 54 | #endif 55 | -------------------------------------------------------------------------------- /src/bvh/v2/stream.h: -------------------------------------------------------------------------------- 1 | #ifndef BVH_V2_STREAM_H 2 | #define BVH_V2_STREAM_H 3 | 4 | #include 5 | #include 6 | 7 | namespace bvh::v2 { 8 | 9 | /// Stream of data that can be used to deserialize data structures. 10 | class InputStream { 11 | public: 12 | template 13 | T read(T&& default_val = {}) { 14 | T data; 15 | if (read_raw(&data, sizeof(T)) != sizeof(T)) 16 | data = std::move(default_val); 17 | return data; 18 | } 19 | 20 | protected: 21 | virtual size_t read_raw(void*, size_t) = 0; 22 | }; 23 | 24 | /// Stream of data that can be used to serialize data structures. 25 | class OutputStream { 26 | public: 27 | template 28 | bool write(const T& data) { return write_raw(&data, sizeof(T)); } 29 | 30 | protected: 31 | virtual bool write_raw(const void*, size_t) = 0; 32 | }; 33 | 34 | /// Stream adapter for standard library input streams. 35 | class StdInputStream : public InputStream { 36 | public: 37 | StdInputStream(std::istream& stream) 38 | : stream_(stream) 39 | {} 40 | 41 | using InputStream::read; 42 | 43 | protected: 44 | std::istream& stream_; 45 | 46 | size_t read_raw(void* data, size_t size) override { 47 | stream_.read(reinterpret_cast(data), static_cast(size)); 48 | return static_cast(stream_.gcount()); 49 | } 50 | }; 51 | 52 | /// Stream adapter for standard library output streams. 53 | class StdOutputStream : public OutputStream { 54 | public: 55 | StdOutputStream(std::ostream& stream) 56 | : stream_(stream) 57 | {} 58 | 59 | using OutputStream::write; 60 | 61 | protected: 62 | std::ostream& stream_; 63 | 64 | bool write_raw(const void* data, size_t size) override { 65 | stream_.write(reinterpret_cast(data), static_cast(size)); 66 | return stream_.good(); 67 | } 68 | }; 69 | 70 | } // namespace bvh::v2 71 | 72 | #endif 73 | -------------------------------------------------------------------------------- /test/serialize.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | #include 10 | #include 11 | 12 | using Scalar = float; 13 | using Vec3 = bvh::v2::Vec; 14 | using BBox = bvh::v2::BBox; 15 | using Tri = bvh::v2::Tri; 16 | using Node = bvh::v2::Node; 17 | using Bvh = bvh::v2::Bvh; 18 | using Ray = bvh::v2::Ray; 19 | 20 | using PrecomputedTri = bvh::v2::PrecomputedTri; 21 | using StdOutputStream = bvh::v2::StdOutputStream; 22 | using StdInputStream = bvh::v2::StdInputStream; 23 | 24 | static bool save_bvh(const Bvh& bvh, const std::string& file_name) { 25 | std::ofstream out(file_name, std::ofstream::binary); 26 | if (!out) 27 | return false; 28 | StdOutputStream stream(out); 29 | bvh.serialize(stream); 30 | return true; 31 | } 32 | 33 | static std::optional load_bvh(const std::string& file_name) { 34 | std::ifstream in(file_name, std::ofstream::binary); 35 | if (!in) 36 | return std::nullopt; 37 | StdInputStream stream(in); 38 | return std::make_optional(Bvh::deserialize(stream)); 39 | } 40 | 41 | int main() { 42 | std::vector tris; 43 | tris.emplace_back( 44 | Vec3( 1.0, -1.0, 1.0), 45 | Vec3( 1.0, 1.0, 1.0), 46 | Vec3(-1.0, 1.0, 1.0) 47 | ); 48 | tris.emplace_back( 49 | Vec3( 1.0, -1.0, 1.0), 50 | Vec3(-1.0, -1.0, 1.0), 51 | Vec3(-1.0, 1.0, 1.0) 52 | ); 53 | 54 | std::vector bboxes(tris.size()); 55 | std::vector centers(tris.size()); 56 | for (size_t i = 0; i < tris.size(); ++i) { 57 | bboxes[i] = tris[i].get_bbox(); 58 | centers[i] = tris[i].get_center(); 59 | } 60 | 61 | auto bvh = bvh::v2::DefaultBuilder::build(bboxes, centers); 62 | 63 | save_bvh(bvh, "bvh.bin"); 64 | auto other_bvh = load_bvh("bvh.bin"); 65 | if (!other_bvh) { 66 | std::cerr << "Cannot load bvh file" << std::endl; 67 | return 1; 68 | } 69 | 70 | if (bvh == other_bvh) { 71 | std::cout << "The deserialized BVH is the same as the original one" << std::endl; 72 | return 0; 73 | } else { 74 | std::cerr << "The deserialized BVH does not match the original one" << std::endl; 75 | return 1; 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/bvh/v2/tri.h: -------------------------------------------------------------------------------- 1 | #ifndef BVH_V2_TRI_H 2 | #define BVH_V2_TRI_H 3 | 4 | #include "bvh/v2/vec.h" 5 | #include "bvh/v2/ray.h" 6 | #include "bvh/v2/bbox.h" 7 | 8 | #include 9 | #include 10 | #include 11 | 12 | namespace bvh::v2 { 13 | 14 | template 15 | struct Tri { 16 | Vec p0, p1, p2; 17 | 18 | Tri() = default; 19 | 20 | BVH_ALWAYS_INLINE Tri(const Vec& p0, const Vec& p1, const Vec& p2) 21 | : p0(p0), p1(p1), p2(p2) 22 | {} 23 | 24 | BVH_ALWAYS_INLINE BBox get_bbox() const { return BBox(p0).extend(p1).extend(p2); } 25 | BVH_ALWAYS_INLINE Vec get_center() const { return (p0 + p1 + p2) * static_cast(1. / 3.); } 26 | }; 27 | 28 | /// A 3d triangle, represented as two edges and a point, with an (unnormalized, left-handed) normal. 29 | template 30 | struct PrecomputedTri { 31 | Vec p0, e1, e2, n; 32 | 33 | PrecomputedTri() = default; 34 | 35 | BVH_ALWAYS_INLINE PrecomputedTri(const Vec& p0, const Vec& p1, const Vec& p2) 36 | : p0(p0), e1(p0 - p1), e2(p2 - p0), n(cross(e1, e2)) 37 | {} 38 | 39 | BVH_ALWAYS_INLINE PrecomputedTri(const Tri& triangle) 40 | : PrecomputedTri(triangle.p0, triangle.p1, triangle.p2) 41 | {} 42 | 43 | BVH_ALWAYS_INLINE Tri convert_to_tri() const { return Tri(p0, p0 - e1, e2 + p0); } 44 | BVH_ALWAYS_INLINE BBox get_bbox() const { return convert_to_tri().get_bbox(); } 45 | BVH_ALWAYS_INLINE Vec get_center() const { return convert_to_tri().get_center(); } 46 | 47 | /// Returns a tuple containing the distance at which the ray intersects the triangle, and the 48 | /// barycentric coordinates of the hit point if the given ray intersects the triangle, otherwise 49 | /// returns nothing. The tolerance can be adjusted to account for numerical precision issues. 50 | [[nodiscard]] BVH_ALWAYS_INLINE std::optional> intersect( 51 | const Ray& ray, 52 | T tolerance = -std::numeric_limits::epsilon()) const; 53 | }; 54 | 55 | template 56 | std::optional> PrecomputedTri::intersect(const Ray& ray, T tolerance) const { 57 | auto c = p0 - ray.org; 58 | auto r = cross(ray.dir, c); 59 | auto inv_det = static_cast(1.) / dot(n, ray.dir); 60 | 61 | auto u = dot(r, e2) * inv_det; 62 | auto v = dot(r, e1) * inv_det; 63 | auto w = static_cast(1.) - u - v; 64 | 65 | // These comparisons are designed to return false 66 | // when one of t, u, or v is a NaN 67 | if (u >= tolerance && v >= tolerance && w >= tolerance) { 68 | auto t = dot(n, c) * inv_det; 69 | if (t >= ray.tmin && t <= ray.tmax) 70 | return std::make_optional(std::make_tuple(t, u, v)); 71 | } 72 | 73 | return std::nullopt; 74 | } 75 | 76 | } // namespace bvh::v2 77 | 78 | #endif 79 | -------------------------------------------------------------------------------- /src/bvh/v2/thread_pool.h: -------------------------------------------------------------------------------- 1 | #ifndef BVH_V2_THREAD_POOL_H 2 | #define BVH_V2_THREAD_POOL_H 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | namespace bvh::v2 { 12 | 13 | class ThreadPool { 14 | public: 15 | using Task = std::function; 16 | 17 | /// Creates a thread pool with the given number of threads (a value of 0 tries to autodetect 18 | /// the number of threads and uses that as a thread count). 19 | ThreadPool(size_t thread_count = 0) { start(thread_count); } 20 | 21 | ~ThreadPool() { 22 | wait(); 23 | stop(); 24 | join(); 25 | } 26 | 27 | inline void push(Task&& fun); 28 | inline void wait(); 29 | 30 | size_t get_thread_count() const { return threads_.size(); } 31 | 32 | private: 33 | static inline void worker(ThreadPool*, size_t); 34 | 35 | inline void start(size_t); 36 | inline void stop(); 37 | inline void join(); 38 | 39 | int busy_count_ = 0; 40 | bool should_stop_ = false; 41 | std::mutex mutex_; 42 | std::vector threads_; 43 | std::condition_variable avail_; 44 | std::condition_variable done_; 45 | std::queue tasks_; 46 | }; 47 | 48 | void ThreadPool::push(Task&& task) { 49 | { 50 | std::unique_lock lock(mutex_); 51 | tasks_.emplace(std::move(task)); 52 | } 53 | avail_.notify_one(); 54 | } 55 | 56 | void ThreadPool::wait() { 57 | std::unique_lock lock(mutex_); 58 | done_.wait(lock, [this] { return busy_count_ == 0 && tasks_.empty(); }); 59 | } 60 | 61 | void ThreadPool::worker(ThreadPool* pool, size_t thread_id) { 62 | while (true) { 63 | Task task; 64 | { 65 | std::unique_lock lock(pool->mutex_); 66 | pool->avail_.wait(lock, [pool] { return pool->should_stop_ || !pool->tasks_.empty(); }); 67 | if (pool->should_stop_ && pool->tasks_.empty()) 68 | break; 69 | task = std::move(pool->tasks_.front()); 70 | pool->tasks_.pop(); 71 | pool->busy_count_++; 72 | } 73 | task(thread_id); 74 | { 75 | std::unique_lock lock(pool->mutex_); 76 | pool->busy_count_--; 77 | } 78 | pool->done_.notify_one(); 79 | } 80 | } 81 | 82 | void ThreadPool::start(size_t thread_count) { 83 | if (thread_count == 0) 84 | thread_count = std::max(1u, std::thread::hardware_concurrency()); 85 | for (size_t i = 0; i < thread_count; ++i) 86 | threads_.emplace_back(worker, this, i); 87 | } 88 | 89 | void ThreadPool::stop() { 90 | { 91 | std::unique_lock lock(mutex_); 92 | should_stop_ = true; 93 | } 94 | avail_.notify_all(); 95 | } 96 | 97 | void ThreadPool::join() { 98 | for (auto& thread : threads_) 99 | thread.join(); 100 | } 101 | 102 | } // namespace bvh::v2 103 | 104 | #endif 105 | -------------------------------------------------------------------------------- /src/bvh/v2/index.h: -------------------------------------------------------------------------------- 1 | #ifndef BVH_V2_INDEX_H 2 | #define BVH_V2_INDEX_H 3 | 4 | #include "bvh/v2/utils.h" 5 | 6 | #include 7 | #include 8 | 9 | namespace bvh::v2 { 10 | 11 | /// Packed index data structure. This index can either refer to a range of primitives for a BVH 12 | /// leaf, or to the children of a BVH node. In either case, the index corresponds to a contiguous 13 | /// range, which means that: 14 | /// 15 | /// - For leaves, primitives in a BVH node should be accessed via: 16 | /// 17 | /// size_t begin = index.first_id(); 18 | /// size_t end = begin + index.prim_count(); 19 | /// for (size_t i = begin; i < end; ++i) { 20 | /// size_t prim_id = bvh.prim_ids[i]; 21 | /// // ... 22 | /// } 23 | /// 24 | /// Note that for efficiency, reordering the original data to avoid the indirection via 25 | /// `bvh.prim_ids` is preferable. 26 | /// 27 | /// - For inner nodes, children should be accessed via: 28 | /// 29 | /// auto& left_child = bvh.nodes[index.first_id()]; 30 | /// auto& right_child = bvh.nodes[index.first_id() + 1]; 31 | /// 32 | template 33 | struct Index { 34 | using Type = UnsignedIntType; 35 | 36 | static constexpr size_t bits = Bits; 37 | static constexpr size_t prim_count_bits = PrimCountBits; 38 | static constexpr Type max_prim_count = make_bitmask(prim_count_bits); 39 | static constexpr Type max_first_id = make_bitmask(bits - prim_count_bits); 40 | 41 | static_assert(PrimCountBits < Bits); 42 | 43 | Type value; 44 | 45 | Index() = default; 46 | explicit Index(Type value) : value(value) {} 47 | 48 | bool operator == (const Index&) const = default; 49 | bool operator != (const Index&) const = default; 50 | 51 | BVH_ALWAYS_INLINE Type first_id() const { return value >> prim_count_bits; } 52 | BVH_ALWAYS_INLINE Type prim_count() const { return value & max_prim_count; } 53 | BVH_ALWAYS_INLINE bool is_leaf() const { return prim_count() != 0; } 54 | BVH_ALWAYS_INLINE bool is_inner() const { return !is_leaf(); } 55 | 56 | BVH_ALWAYS_INLINE void set_first_id(size_t first_id) { 57 | *this = Index { first_id, static_cast(prim_count()) }; 58 | } 59 | 60 | BVH_ALWAYS_INLINE void set_prim_count(size_t prim_count) { 61 | *this = Index { static_cast(first_id()), prim_count }; 62 | } 63 | 64 | static BVH_ALWAYS_INLINE Index make_leaf(size_t first_prim, size_t prim_count) { 65 | assert(prim_count != 0); 66 | return Index { first_prim, prim_count }; 67 | } 68 | 69 | static BVH_ALWAYS_INLINE Index make_inner(size_t first_child) { 70 | return Index { first_child, 0 }; 71 | } 72 | 73 | private: 74 | explicit Index(size_t first_id, size_t prim_count) 75 | : Index( 76 | (static_cast(first_id) << prim_count_bits) | 77 | (static_cast(prim_count) & max_prim_count)) 78 | { 79 | assert(first_id <= static_cast(max_first_id)); 80 | assert(prim_count <= static_cast(max_prim_count)); 81 | } 82 | }; 83 | 84 | } // namespace bvh::v2 85 | 86 | #endif 87 | -------------------------------------------------------------------------------- /src/bvh/v2/default_builder.h: -------------------------------------------------------------------------------- 1 | #ifndef BVH_V2_DEFAULT_BUILDER_H 2 | #define BVH_V2_DEFAULT_BUILDER_H 3 | 4 | #include "bvh/v2/mini_tree_builder.h" 5 | #include "bvh/v2/sweep_sah_builder.h" 6 | #include "bvh/v2/binned_sah_builder.h" 7 | #include "bvh/v2/reinsertion_optimizer.h" 8 | #include "bvh/v2/thread_pool.h" 9 | 10 | namespace bvh::v2 { 11 | 12 | /// This builder is only a wrapper around all the other builders, which selects the best builder 13 | /// depending on the desired BVH quality and whether a multi-threaded build is desired. 14 | template 15 | class DefaultBuilder { 16 | using Scalar = typename Node::Scalar; 17 | using Vec = bvh::v2::Vec; 18 | using BBox = bvh::v2::BBox; 19 | 20 | public: 21 | enum class Quality { Low, Medium, High }; 22 | 23 | struct Config : TopDownSahBuilder::Config { 24 | /// The quality of the BVH produced by the builder. The higher the quality the faster the 25 | /// BVH is to traverse, but the slower it is to build. 26 | Quality quality = Quality::High; 27 | 28 | /// Threshold, in number of primitives, under which the builder operates in a single-thread. 29 | size_t parallel_threshold = 1024; 30 | }; 31 | 32 | /// Build a BVH in parallel using the given thread pool. 33 | [[nodiscard]] BVH_ALWAYS_INLINE static Bvh build( 34 | ThreadPool& thread_pool, 35 | std::span bboxes, 36 | std::span centers, 37 | const Config& config = {}) 38 | { 39 | if (bboxes.size() < config.parallel_threshold) 40 | return build(bboxes, centers, config); 41 | auto bvh = MiniTreeBuilder::build( 42 | thread_pool, bboxes, centers, make_mini_tree_config(config)); 43 | if (config.quality == Quality::High) 44 | ReinsertionOptimizer::optimize(thread_pool, bvh); 45 | return bvh; 46 | } 47 | 48 | /// Build a BVH in a single-thread. 49 | [[nodiscard]] BVH_ALWAYS_INLINE static Bvh build( 50 | std::span bboxes, 51 | std::span centers, 52 | const Config& config = {}) 53 | { 54 | if (config.quality == Quality::Low) 55 | return BinnedSahBuilder::build(bboxes, centers, config); 56 | else { 57 | auto bvh = SweepSahBuilder::build(bboxes, centers, config); 58 | if (config.quality == Quality::High) 59 | ReinsertionOptimizer::optimize(bvh); 60 | return bvh; 61 | } 62 | } 63 | 64 | private: 65 | BVH_ALWAYS_INLINE static auto make_mini_tree_config(const Config& config) { 66 | typename MiniTreeBuilder::Config mini_tree_config; 67 | static_cast::Config&>(mini_tree_config) = config; 68 | mini_tree_config.enable_pruning = config.quality == Quality::Low ? false : true; 69 | mini_tree_config.pruning_area_ratio = 70 | config.quality == Quality::High ? static_cast(0.01) : static_cast(0.1); 71 | mini_tree_config.parallel_threshold = config.parallel_threshold; 72 | return mini_tree_config; 73 | } 74 | }; 75 | 76 | } // namespace bvh::v2 77 | 78 | #endif 79 | -------------------------------------------------------------------------------- /src/bvh/v2/executor.h: -------------------------------------------------------------------------------- 1 | #ifndef BVH_V2_EXECUTOR_H 2 | #define BVH_V2_EXECUTOR_H 3 | 4 | #include "bvh/v2/thread_pool.h" 5 | 6 | #include 7 | #include 8 | #include 9 | 10 | namespace bvh::v2 { 11 | 12 | /// Helper object that provides iteration and reduction over one-dimensional ranges. 13 | template 14 | struct Executor { 15 | template 16 | BVH_ALWAYS_INLINE void for_each(size_t begin, size_t end, const Loop& loop) { 17 | return static_cast(this)->for_each(begin, end, loop); 18 | } 19 | 20 | template 21 | BVH_ALWAYS_INLINE T reduce(size_t begin, size_t end, const T& init, const Reduce& reduce, const Join& join) { 22 | return static_cast(this)->reduce(begin, end, init, reduce, join); 23 | } 24 | }; 25 | 26 | /// Executor that executes serially. 27 | struct SequentialExecutor : Executor { 28 | template 29 | BVH_ALWAYS_INLINE void for_each(size_t begin, size_t end, const Loop& loop) { 30 | loop(begin, end); 31 | } 32 | 33 | template 34 | BVH_ALWAYS_INLINE T reduce(size_t begin, size_t end, const T& init, const Reduce& reduce, const Join&) { 35 | T result(init); 36 | reduce(result, begin, end); 37 | return result; 38 | } 39 | }; 40 | 41 | /// Executor that executes in parallel using the given thread pool. 42 | struct ParallelExecutor : Executor { 43 | ThreadPool& thread_pool; 44 | size_t parallel_threshold; 45 | 46 | ParallelExecutor(ThreadPool& thread_pool, size_t parallel_threshold = 1024) 47 | : thread_pool(thread_pool), parallel_threshold(parallel_threshold) 48 | {} 49 | 50 | template 51 | BVH_ALWAYS_INLINE void for_each(size_t begin, size_t end, const Loop& loop) { 52 | if (end - begin < parallel_threshold) 53 | return loop(begin, end); 54 | 55 | auto chunk_size = std::max(size_t{1}, (end - begin) / thread_pool.get_thread_count()); 56 | for (size_t i = begin; i < end; i += chunk_size) { 57 | size_t next = std::min(end, i + chunk_size); 58 | thread_pool.push([=] (size_t) { loop(i, next); }); 59 | } 60 | thread_pool.wait(); 61 | } 62 | 63 | template 64 | BVH_ALWAYS_INLINE T reduce(size_t begin, size_t end, const T& init, const Reduce& reduce, const Join& join) { 65 | if (end - begin < parallel_threshold) { 66 | T result(init); 67 | reduce(result, begin, end); 68 | return result; 69 | } 70 | 71 | auto chunk_size = std::max(size_t{1}, (end - begin) / thread_pool.get_thread_count()); 72 | std::vector per_thread_result(thread_pool.get_thread_count(), init); 73 | for (size_t i = begin; i < end; i += chunk_size) { 74 | size_t next = std::min(end, i + chunk_size); 75 | thread_pool.push([&, i, next] (size_t thread_id) { 76 | auto& result = per_thread_result[thread_id]; 77 | reduce(result, i, next); 78 | }); 79 | } 80 | thread_pool.wait(); 81 | for (size_t i = 1; i < thread_pool.get_thread_count(); ++i) 82 | join(per_thread_result[0], std::move(per_thread_result[i])); 83 | return per_thread_result[0]; 84 | } 85 | }; 86 | 87 | } // namespace bvh::v2 88 | 89 | #endif 90 | -------------------------------------------------------------------------------- /test/scenes/cornell_box.obj: -------------------------------------------------------------------------------- 1 | # The original Cornell Box in OBJ format. 2 | # Note that the real box is not a perfect cube, so 3 | # the faces are imperfect in this data set. 4 | # 5 | # Created by Guedis Cardenas and Morgan McGuire at Williams College, 2011 6 | # Released into the Public Domain. 7 | # 8 | # http://graphics.cs.williams.edu/data 9 | # http://www.graphics.cornell.edu/online/box/data.html 10 | # 11 | 12 | mtllib cornell_box.mtl 13 | 14 | ## Object floor 15 | v -1.01 0.00 0.99 16 | v 1.00 0.00 0.99 17 | v 1.00 0.00 -1.04 18 | v -0.99 0.00 -1.04 19 | 20 | g floor 21 | usemtl floor 22 | f -4 -3 -2 -1 23 | 24 | ## Object ceiling 25 | v -1.02 1.99 0.99 26 | v -1.02 1.99 -1.04 27 | v 1.00 1.99 -1.04 28 | v 1.00 1.99 0.99 29 | 30 | g ceiling 31 | usemtl ceiling 32 | f -4 -3 -2 -1 33 | 34 | ## Object backwall 35 | v -0.99 0.00 -1.04 36 | v 1.00 0.00 -1.04 37 | v 1.00 1.99 -1.04 38 | v -1.02 1.99 -1.04 39 | 40 | g backWall 41 | usemtl backWall 42 | f -4 -3 -2 -1 43 | 44 | ## Object rightwall 45 | v 1.00 0.00 -1.04 46 | v 1.00 0.00 0.99 47 | v 1.00 1.99 0.99 48 | v 1.00 1.99 -1.04 49 | 50 | g rightWall 51 | usemtl rightWall 52 | f -4 -3 -2 -1 53 | 54 | ## Object leftWall 55 | v -1.01 0.00 0.99 56 | v -0.99 0.00 -1.04 57 | v -1.02 1.99 -1.04 58 | v -1.02 1.99 0.99 59 | 60 | g leftWall 61 | usemtl leftWall 62 | f -4 -3 -2 -1 63 | 64 | ## Object shortBox 65 | usemtl shortBox 66 | 67 | # Top Face 68 | v 0.53 0.60 0.75 69 | v 0.70 0.60 0.17 70 | v 0.13 0.60 0.00 71 | v -0.05 0.60 0.57 72 | f -4 -3 -2 -1 73 | 74 | # Left Face 75 | v -0.05 0.00 0.57 76 | v -0.05 0.60 0.57 77 | v 0.13 0.60 0.00 78 | v 0.13 0.00 0.00 79 | f -4 -3 -2 -1 80 | 81 | # Front Face 82 | v 0.53 0.00 0.75 83 | v 0.53 0.60 0.75 84 | v -0.05 0.60 0.57 85 | v -0.05 0.00 0.57 86 | f -4 -3 -2 -1 87 | 88 | # Right Face 89 | v 0.70 0.00 0.17 90 | v 0.70 0.60 0.17 91 | v 0.53 0.60 0.75 92 | v 0.53 0.00 0.75 93 | f -4 -3 -2 -1 94 | 95 | # Back Face 96 | v 0.13 0.00 0.00 97 | v 0.13 0.60 0.00 98 | v 0.70 0.60 0.17 99 | v 0.70 0.00 0.17 100 | f -4 -3 -2 -1 101 | 102 | # Bottom Face 103 | v 0.53 0.00 0.75 104 | v 0.70 0.00 0.17 105 | v 0.13 0.00 0.00 106 | v -0.05 0.00 0.57 107 | f -12 -11 -10 -9 108 | 109 | g shortBox 110 | usemtl shortBox 111 | 112 | ## Object tallBox 113 | usemtl tallBox 114 | 115 | # Top Face 116 | v -0.53 1.20 0.09 117 | v 0.04 1.20 -0.09 118 | v -0.14 1.20 -0.67 119 | v -0.71 1.20 -0.49 120 | f -4 -3 -2 -1 121 | 122 | # Left Face 123 | v -0.53 0.00 0.09 124 | v -0.53 1.20 0.09 125 | v -0.71 1.20 -0.49 126 | v -0.71 0.00 -0.49 127 | f -4 -3 -2 -1 128 | 129 | # Back Face 130 | v -0.71 0.00 -0.49 131 | v -0.71 1.20 -0.49 132 | v -0.14 1.20 -0.67 133 | v -0.14 0.00 -0.67 134 | f -4 -3 -2 -1 135 | 136 | # Right Face 137 | v -0.14 0.00 -0.67 138 | v -0.14 1.20 -0.67 139 | v 0.04 1.20 -0.09 140 | v 0.04 0.00 -0.09 141 | f -4 -3 -2 -1 142 | 143 | # Front Face 144 | v 0.04 0.00 -0.09 145 | v 0.04 1.20 -0.09 146 | v -0.53 1.20 0.09 147 | v -0.53 0.00 0.09 148 | f -4 -3 -2 -1 149 | 150 | # Bottom Face 151 | v -0.53 0.00 0.09 152 | v 0.04 0.00 -0.09 153 | v -0.14 0.00 -0.67 154 | v -0.71 0.00 -0.49 155 | f -8 -7 -6 -5 156 | 157 | g tallBox 158 | usemtl tallBox 159 | 160 | ## Object light 161 | v -0.24 1.98 0.16 162 | v -0.24 1.98 -0.22 163 | v 0.23 1.98 -0.22 164 | v 0.23 1.98 0.16 165 | 166 | g light 167 | usemtl light 168 | f -4 -3 -2 -1 169 | -------------------------------------------------------------------------------- /test/simple_example.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | #include 12 | 13 | using Scalar = float; 14 | using Vec3 = bvh::v2::Vec; 15 | using BBox = bvh::v2::BBox; 16 | using Tri = bvh::v2::Tri; 17 | using Node = bvh::v2::Node; 18 | using Bvh = bvh::v2::Bvh; 19 | using Ray = bvh::v2::Ray; 20 | 21 | using PrecomputedTri = bvh::v2::PrecomputedTri; 22 | 23 | int main() { 24 | // This is the original data, which may come in some other data type/structure. 25 | std::vector tris; 26 | tris.emplace_back( 27 | Vec3( 1.0, -1.0, 1.0), 28 | Vec3( 1.0, 1.0, 1.0), 29 | Vec3(-1.0, 1.0, 1.0) 30 | ); 31 | tris.emplace_back( 32 | Vec3( 1.0, -1.0, 1.0), 33 | Vec3(-1.0, -1.0, 1.0), 34 | Vec3(-1.0, 1.0, 1.0) 35 | ); 36 | 37 | bvh::v2::ThreadPool thread_pool; 38 | bvh::v2::ParallelExecutor executor(thread_pool); 39 | 40 | // Get triangle centers and bounding boxes (required for BVH builder) 41 | std::vector bboxes(tris.size()); 42 | std::vector centers(tris.size()); 43 | executor.for_each(0, tris.size(), [&] (size_t begin, size_t end) { 44 | for (size_t i = begin; i < end; ++i) { 45 | bboxes[i] = tris[i].get_bbox(); 46 | centers[i] = tris[i].get_center(); 47 | } 48 | }); 49 | 50 | typename bvh::v2::DefaultBuilder::Config config; 51 | config.quality = bvh::v2::DefaultBuilder::Quality::High; 52 | auto bvh = bvh::v2::DefaultBuilder::build(thread_pool, bboxes, centers, config); 53 | 54 | // Permuting the primitive data allows to remove indirections during traversal, which makes it faster. 55 | static constexpr bool should_permute = true; 56 | 57 | // This precomputes some data to speed up traversal further. 58 | std::vector precomputed_tris(tris.size()); 59 | executor.for_each(0, tris.size(), [&] (size_t begin, size_t end) { 60 | for (size_t i = begin; i < end; ++i) { 61 | auto j = should_permute ? bvh.prim_ids[i] : i; 62 | precomputed_tris[i] = tris[j]; 63 | } 64 | }); 65 | 66 | auto ray = Ray { 67 | Vec3(0., 0., 0.), // Ray origin 68 | Vec3(0., 0., 1.), // Ray direction 69 | 0., // Minimum intersection distance 70 | 100. // Maximum intersection distance 71 | }; 72 | 73 | static constexpr size_t invalid_id = std::numeric_limits::max(); 74 | static constexpr size_t stack_size = 64; 75 | static constexpr bool use_robust_traversal = false; 76 | 77 | auto prim_id = invalid_id; 78 | Scalar u, v; 79 | 80 | // Traverse the BVH and get the u, v coordinates of the closest intersection. 81 | bvh::v2::SmallStack stack; 82 | bvh.intersect(ray, bvh.get_root().index, stack, 83 | [&] (size_t begin, size_t end) { 84 | for (size_t i = begin; i < end; ++i) { 85 | size_t j = should_permute ? i : bvh.prim_ids[i]; 86 | if (auto hit = precomputed_tris[j].intersect(ray)) { 87 | prim_id = i; 88 | std::tie(ray.tmax, u, v) = *hit; 89 | } 90 | } 91 | return prim_id != invalid_id; 92 | }); 93 | 94 | if (prim_id != invalid_id) { 95 | std::cout 96 | << "Intersection found\n" 97 | << " primitive: " << prim_id << "\n" 98 | << " distance: " << ray.tmax << "\n" 99 | << " barycentric coords.: " << u << ", " << v << std::endl; 100 | return 0; 101 | } else { 102 | std::cout << "No intersection found" << std::endl; 103 | return 1; 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # BVH Construction and Traversal Library 2 | 3 | ![Build Status](https://github.com/madmann91/bvh/workflows/build-and-test/badge.svg) 4 | 5 | > Note: This is the 2nd version of this library. Check the `v1` branch for the older, first version 6 | > of this library. 7 | 8 | This library is a small, standalone library for BVH construction and traversal. It is licensed 9 | under the MIT license. 10 | 11 | ![Example rendering generated by a path tracer using this library](render.jpg) 12 | (Scene by Blend Swap user MaTTeSr, available [here](https://www.blendswap.com/blend/18762), 13 | distributed under CC-BY 3.0) 14 | 15 | ## Performance 16 | 17 | Here is a comparison of this library with other alternatives 18 | ([Embree](https://github.com/embree/embree), 19 | [Fast-BVH](https://github.com/brandonpelfrey/Fast-BVH), and 20 | [nanort](https://github.com/lighttransport/nanort)): 21 | 22 | ![Performance comparison with Embree, nanort, and Fast-BVH](chart.png) 23 | 24 | ## Features 25 | 26 | Here is a list of features supported by this library (changes from `v1` are indicated with [NEW]): 27 | 28 | - [NEW] C++20 interface using `std::span` instead of raw pointers, 29 | - Low-level API with direct access to various builders, 30 | - [NEW] High-level `DefaultBuilder` API which selects the best builder depending on the desired 31 | BVH quality level. 32 | - High-quality, single-threaded sweeping SAH builder, 33 | - Fast, medium-quality, single-threaded binned SAH builder inspired by 34 | "On Fast Construction of SAH-based Bounding Volume Hierarchies", by I. Wald, 35 | - Fast, high-quality, multithreaded mini-tree BVH builder inspired by 36 | "Rapid Bounding Volume Hierarchy Generation using Mini Trees", by P. Ganestam et al., 37 | - Reinsertion optimizer based on "Parallel Reinsertion for Bounding Volume Hierarchy 38 | Optimization", by D. Meister and J. Bittner, 39 | - Fast and robust traversal algorithm using "Robust BVH Ray Traversal", by T. Ize. 40 | - Fast ray-triangle intersection algorithm based on 41 | "Fast, Minimum Storage Ray/Triangle Intersection", by T. Möller and B. Trumbore, 42 | - [NEW] Surface area traversal order heuristic for shadow rays based on 43 | "SATO: Surface Area Traversal Order for Shadow Ray Tracing", by J. Nah and D. Manocha, 44 | - Fast ray-sphere intersection routine, 45 | - [NEW] Serialization/deserialization interface, 46 | - [NEW] Variable amount of dimensions (e.g. 2D, 3D, 4D BVHs are supported) and different scalar types 47 | (e.g. `float` or `double`), 48 | - [NEW] Only depends on the standard library (parallelization uses a custom thread pool based on 49 | `std::thread`), 50 | - [NEW] C API for the high-level parts of the library is available. 51 | 52 | ## Building 53 | 54 | This library is header-only, and can be added as a CMake subproject by cloning or adding as this 55 | repository as submodule, for instance in `/contrib/bvh`, and then adding this to 56 | `/CMakeLists.txt`: 57 | 58 | add_subdirectory(contrib/bvh) 59 | target_link_library(my_project PUBLIC bvh) 60 | 61 | If you want to build the examples, use: 62 | 63 | mkdir build 64 | cd build 65 | cmake .. -DCMAKE_BUILD_TYPE= -DENABLE_TESTING=ON 66 | cmake --build . 67 | 68 | ## C API 69 | 70 | The library can be used via a small set of high-level C bindings. These bindings are not enabled by 71 | default, but can be built by configuring CMake with `-DBVH_BUILD_C_API=ON`. Additionally, if the 72 | intent is to use the library in a pure C environment which does not have the C++ standard library as 73 | a dependency, it might be a good idea to statically the C++ standard library. That can be done by 74 | adding the flag `-DBVH_STATIC_LINK_STDLIB_C_API=ON` to the CMake command line. 75 | 76 | ## Usage 77 | 78 | The library contains several examples that are kept up-to-date with the API: 79 | 80 | - A [basic example](test/simple_example.cpp) that traces one ray on a scene made of a couple of triangles, 81 | - A [benchmarking utility](test/benchmark.cpp) that showcases what the library can do. 82 | - A [serialization test](test/serialize.cpp) that shows how to save and load a BVH from a file. 83 | - A [C API example](test/c_api_example.c) that shows how to use the C bindings to this library. 84 | -------------------------------------------------------------------------------- /test/load_obj.cpp: -------------------------------------------------------------------------------- 1 | #include "load_obj.h" 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | using namespace bvh::v2; 8 | 9 | namespace obj { 10 | 11 | inline void remove_eol(char* ptr) { 12 | int i = 0; 13 | while (ptr[i]) i++; 14 | i--; 15 | while (i > 0 && std::isspace(ptr[i])) { 16 | ptr[i] = '\0'; 17 | i--; 18 | } 19 | } 20 | 21 | inline char* strip_spaces(char* ptr) { 22 | while (std::isspace(*ptr)) ptr++; 23 | return ptr; 24 | } 25 | 26 | inline std::optional read_index(char** ptr) { 27 | char* base = *ptr; 28 | 29 | // Detect end of line (negative indices are supported) 30 | base = strip_spaces(base); 31 | if (!std::isdigit(*base) && *base != '-') 32 | return std::nullopt; 33 | 34 | int index = std::strtol(base, &base, 10); 35 | base = strip_spaces(base); 36 | 37 | if (*base == '/') { 38 | base++; 39 | 40 | // Handle the case when there is no texture coordinate 41 | if (*base != '/') 42 | std::strtol(base, &base, 10); 43 | 44 | base = strip_spaces(base); 45 | 46 | if (*base == '/') { 47 | base++; 48 | std::strtol(base, &base, 10); 49 | } 50 | } 51 | 52 | *ptr = base; 53 | return std::make_optional(index); 54 | } 55 | 56 | template 57 | inline std::vector> load_from_stream(std::istream& is) { 58 | static constexpr size_t max_line = 1024; 59 | char line[max_line]; 60 | 61 | std::vector> vertices; 62 | std::vector> triangles; 63 | 64 | while (is.getline(line, max_line)) { 65 | char* ptr = strip_spaces(line); 66 | if (*ptr == '\0' || *ptr == '#') 67 | continue; 68 | remove_eol(ptr); 69 | if (*ptr == 'v' && std::isspace(ptr[1])) { 70 | auto x = std::strtof(ptr + 1, &ptr); 71 | auto y = std::strtof(ptr, &ptr); 72 | auto z = std::strtof(ptr, &ptr); 73 | vertices.emplace_back(x, y, z); 74 | } else if (*ptr == 'f' && std::isspace(ptr[1])) { 75 | Vec points[2]; 76 | ptr += 2; 77 | for (size_t i = 0; ; ++i) { 78 | if (auto index = read_index(&ptr)) { 79 | size_t j = *index < 0 ? vertices.size() + *index : *index - 1; 80 | assert(j < vertices.size()); 81 | auto v = vertices[j]; 82 | if (i >= 2) { 83 | triangles.emplace_back(points[0], points[1], v); 84 | points[1] = v; 85 | } else { 86 | points[i] = v; 87 | } 88 | } else { 89 | break; 90 | } 91 | } 92 | } 93 | } 94 | 95 | return triangles; 96 | } 97 | 98 | template 99 | inline std::vector> load_from_file(const std::string& file) { 100 | std::ifstream is(file); 101 | if (is) 102 | return load_from_stream(is); 103 | return std::vector>(); 104 | } 105 | 106 | } // namespace obj 107 | 108 | template 109 | std::vector> load_obj(const std::string& file) { 110 | return obj::load_from_file(file); 111 | } 112 | 113 | template std::vector> load_obj(const std::string&); 114 | template std::vector> load_obj(const std::string&); 115 | 116 | extern "C" { 117 | 118 | tri* load_obj(const char* file_name, size_t* tri_count) { 119 | auto tri_vec = load_obj(file_name); 120 | *tri_count = tri_vec.size(); 121 | if (tri_vec.size() == 0) 122 | return NULL; 123 | tri* tris = reinterpret_cast(malloc(sizeof(tri) * tri_vec.size())); 124 | for (size_t i = 0; i < tri_vec.size(); ++i) { 125 | tris[i].v[0].x = tri_vec[i].p0[0]; 126 | tris[i].v[0].y = tri_vec[i].p0[1]; 127 | tris[i].v[0].z = tri_vec[i].p0[2]; 128 | tris[i].v[1].x = tri_vec[i].p1[0]; 129 | tris[i].v[1].y = tri_vec[i].p1[1]; 130 | tris[i].v[1].z = tri_vec[i].p1[2]; 131 | tris[i].v[2].x = tri_vec[i].p2[0]; 132 | tris[i].v[2].y = tri_vec[i].p2[1]; 133 | tris[i].v[2].z = tri_vec[i].p2[2]; 134 | } 135 | return tris; 136 | } 137 | 138 | } // extern "C" 139 | -------------------------------------------------------------------------------- /src/bvh/v2/node.h: -------------------------------------------------------------------------------- 1 | #ifndef BVH_V2_NODE_H 2 | #define BVH_V2_NODE_H 3 | 4 | #include "bvh/v2/index.h" 5 | #include "bvh/v2/vec.h" 6 | #include "bvh/v2/bbox.h" 7 | #include "bvh/v2/ray.h" 8 | #include "bvh/v2/stream.h" 9 | 10 | #include 11 | #include 12 | #include 13 | 14 | namespace bvh::v2 { 15 | 16 | /// Binary BVH node, containing its bounds and an index into its children or the primitives it 17 | /// contains. By definition, inner BVH nodes do not contain primitives; only leaves do. 18 | template < 19 | typename T, 20 | size_t Dim, 21 | size_t IndexBits = sizeof(T) * CHAR_BIT, 22 | size_t PrimCountBits = 4> 23 | struct Node { 24 | using Scalar = T; 25 | using Index = bvh::v2::Index; 26 | 27 | static constexpr size_t dimension = Dim; 28 | static constexpr size_t prim_count_bits = PrimCountBits; 29 | static constexpr size_t index_bits = IndexBits; 30 | 31 | /// Bounds of the node, laid out in memory as `[min_x, max_x, min_y, max_y, ...]`. Users should 32 | /// not really depend on a specific order and instead use `get_bbox()` and extract the `min` 33 | /// and/or `max` components accordingly. 34 | std::array bounds; 35 | 36 | /// Index to the children of an inner node, or to the primitives for a leaf node. 37 | Index index; 38 | 39 | Node() = default; 40 | 41 | bool operator == (const Node&) const = default; 42 | bool operator != (const Node&) const = default; 43 | 44 | BVH_ALWAYS_INLINE bool is_leaf() const { return index.is_leaf(); } 45 | 46 | BVH_ALWAYS_INLINE BBox get_bbox() const { 47 | return BBox( 48 | Vec::generate([&] (size_t i) { return bounds[i * 2]; }), 49 | Vec::generate([&] (size_t i) { return bounds[i * 2 + 1]; })); 50 | } 51 | 52 | BVH_ALWAYS_INLINE void set_bbox(const BBox& bbox) { 53 | static_for<0, Dim>([&] (size_t i) { 54 | bounds[i * 2 + 0] = bbox.min[i]; 55 | bounds[i * 2 + 1] = bbox.max[i]; 56 | }); 57 | } 58 | 59 | BVH_ALWAYS_INLINE Vec get_min_bounds(const Octant& octant) const { 60 | return Vec::generate([&] (size_t i) { return bounds[2 * static_cast(i) + octant[i]]; }); 61 | } 62 | 63 | BVH_ALWAYS_INLINE Vec get_max_bounds(const Octant& octant) const { 64 | return Vec::generate([&] (size_t i) { return bounds[2 * static_cast(i) + 1 - octant[i]]; }); 65 | } 66 | 67 | /// Robust ray-node intersection routine. See "Robust BVH Ray Traversal", by T. Ize. 68 | [[nodiscard]] BVH_ALWAYS_INLINE std::pair intersect_robust( 69 | const Ray& ray, 70 | const Vec& inv_dir, 71 | const Vec& inv_dir_pad, 72 | const Octant& octant) const 73 | { 74 | auto tmin = (get_min_bounds(octant) - ray.org) * inv_dir; 75 | auto tmax = (get_max_bounds(octant) - ray.org) * inv_dir_pad; 76 | return make_intersection_result(ray, tmin, tmax); 77 | } 78 | 79 | [[nodiscard]] BVH_ALWAYS_INLINE std::pair intersect_fast( 80 | const Ray& ray, 81 | const Vec& inv_dir, 82 | const Vec& inv_org, 83 | const Octant& octant) const 84 | { 85 | auto tmin = fast_mul_add(get_min_bounds(octant), inv_dir, inv_org); 86 | auto tmax = fast_mul_add(get_max_bounds(octant), inv_dir, inv_org); 87 | return make_intersection_result(ray, tmin, tmax); 88 | } 89 | 90 | BVH_ALWAYS_INLINE void serialize(OutputStream& stream) const { 91 | for (auto&& bound : bounds) 92 | stream.write(bound); 93 | stream.write(index.value); 94 | } 95 | 96 | [[nodiscard]] static BVH_ALWAYS_INLINE Node deserialize(InputStream& stream) { 97 | Node node; 98 | for (auto& bound : node.bounds) 99 | bound = stream.read(); 100 | node.index = Index(stream.read()); 101 | return node; 102 | } 103 | 104 | private: 105 | BVH_ALWAYS_INLINE static std::pair make_intersection_result( 106 | const Ray& ray, 107 | const Vec& tmin, 108 | const Vec& tmax) 109 | { 110 | auto t0 = ray.tmin; 111 | auto t1 = ray.tmax; 112 | static_for<0, Dim>([&] (size_t i) { 113 | t0 = robust_max(tmin[i], t0); 114 | t1 = robust_min(tmax[i], t1); 115 | }); 116 | return std::pair { t0, t1 }; 117 | } 118 | }; 119 | 120 | } // namespace bvh::v2 121 | 122 | #endif 123 | -------------------------------------------------------------------------------- /src/bvh/v2/vec.h: -------------------------------------------------------------------------------- 1 | #ifndef BVH_V2_VEC_H 2 | #define BVH_V2_VEC_H 3 | 4 | #include "bvh/v2/utils.h" 5 | 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | namespace bvh::v2 { 12 | 13 | template 14 | struct Vec { 15 | T values[N]; 16 | 17 | Vec() = default; 18 | template 19 | BVH_ALWAYS_INLINE Vec(T x, T y, Args&&... args) : values { x, y, static_cast(std::forward(args))... } {} 20 | BVH_ALWAYS_INLINE explicit Vec(T x) { std::fill(values, values + N, x); } 21 | 22 | template 23 | BVH_ALWAYS_INLINE size_t get_best_axis(Compare&& compare) const { 24 | size_t axis = 0; 25 | static_for<1, N>([&] (size_t i) { 26 | if (compare(values[i], values[axis])) 27 | axis = i; 28 | }); 29 | return axis; 30 | } 31 | 32 | // Note: These functions are designed to be robust to NaNs 33 | BVH_ALWAYS_INLINE size_t get_largest_axis() const { return get_best_axis(std::greater()); } 34 | BVH_ALWAYS_INLINE size_t get_smallest_axis() const { return get_best_axis(std::less()); } 35 | 36 | BVH_ALWAYS_INLINE T& operator [] (size_t i) { return values[i]; } 37 | BVH_ALWAYS_INLINE T operator [] (size_t i) const { return values[i]; } 38 | 39 | template 40 | BVH_ALWAYS_INLINE static Vec generate(F&& f) { 41 | Vec v; 42 | static_for<0, N>([&] (size_t i) { v[i] = f(i); }); 43 | return v; 44 | } 45 | }; 46 | 47 | template 48 | BVH_ALWAYS_INLINE Vec operator + (const Vec& a, const Vec& b) { 49 | return Vec::generate([&] (size_t i) { return a[i] + b[i]; }); 50 | } 51 | 52 | template 53 | BVH_ALWAYS_INLINE Vec operator - (const Vec& a, const Vec& b) { 54 | return Vec::generate([&] (size_t i) { return a[i] - b[i]; }); 55 | } 56 | 57 | template 58 | BVH_ALWAYS_INLINE Vec operator - (const Vec& a) { 59 | return Vec::generate([&] (size_t i) { return -a[i]; }); 60 | } 61 | 62 | template 63 | BVH_ALWAYS_INLINE Vec operator * (const Vec& a, const Vec& b) { 64 | return Vec::generate([&] (size_t i) { return a[i] * b[i]; }); 65 | } 66 | 67 | template 68 | BVH_ALWAYS_INLINE Vec operator / (const Vec& a, const Vec& b) { 69 | return Vec::generate([&] (size_t i) { return a[i] / b[i]; }); 70 | } 71 | 72 | template 73 | BVH_ALWAYS_INLINE Vec operator * (const Vec& a, T b) { 74 | return Vec::generate([&] (size_t i) { return a[i] * b; }); 75 | } 76 | 77 | template 78 | BVH_ALWAYS_INLINE Vec operator * (T a, const Vec& b) { 79 | return b * a; 80 | } 81 | 82 | template 83 | BVH_ALWAYS_INLINE Vec operator / (T a, const Vec& b) { 84 | return Vec::generate([&] (size_t i) { return a / b[i]; }); 85 | } 86 | 87 | template 88 | BVH_ALWAYS_INLINE Vec robust_min(const Vec& a, const Vec& b) { 89 | return Vec::generate([&] (size_t i) { return robust_min(a[i], b[i]); }); 90 | } 91 | 92 | template 93 | BVH_ALWAYS_INLINE Vec robust_max(const Vec& a, const Vec& b) { 94 | return Vec::generate([&] (size_t i) { return robust_max(a[i], b[i]); }); 95 | } 96 | 97 | template 98 | BVH_ALWAYS_INLINE T dot(const Vec& a, const Vec& b) { 99 | return std::transform_reduce(a.values, a.values + N, b.values, T(0)); 100 | } 101 | 102 | template 103 | BVH_ALWAYS_INLINE Vec cross(const Vec& a, const Vec& b) { 104 | return Vec( 105 | a[1] * b[2] - a[2] * b[1], 106 | a[2] * b[0] - a[0] * b[2], 107 | a[0] * b[1] - a[1] * b[0]); 108 | } 109 | 110 | template 111 | BVH_ALWAYS_INLINE Vec fast_mul_add(const Vec& a, const Vec& b, const Vec& c) { 112 | return Vec::generate([&] (size_t i) { return fast_mul_add(a[i], b[i], c[i]); }); 113 | } 114 | 115 | template 116 | BVH_ALWAYS_INLINE Vec safe_inverse(const Vec& v) { 117 | return Vec::generate([&] (size_t i) { return safe_inverse(v[i]); }); 118 | } 119 | 120 | template 121 | BVH_ALWAYS_INLINE T length(const Vec& v) { 122 | return std::sqrt(dot(v, v)); 123 | } 124 | 125 | template 126 | [[nodiscard]] BVH_ALWAYS_INLINE Vec normalize(const Vec& v) { 127 | return v * (static_cast(1.) / length(v)); 128 | } 129 | 130 | } // namespace bvh::v2 131 | 132 | #endif 133 | -------------------------------------------------------------------------------- /src/bvh/v2/utils.h: -------------------------------------------------------------------------------- 1 | #ifndef BVH_V2_UTILS_H 2 | #define BVH_V2_UTILS_H 3 | 4 | #include "bvh/v2/platform.h" 5 | 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | 13 | namespace bvh::v2 { 14 | 15 | /// Helper type that gives an unsigned integer type with the given number of bits. 16 | template 17 | struct UnsignedInt {}; 18 | 19 | template <> struct UnsignedInt< 8> { using Type = uint8_t ; }; 20 | template <> struct UnsignedInt<16> { using Type = uint16_t; }; 21 | template <> struct UnsignedInt<32> { using Type = uint32_t; }; 22 | template <> struct UnsignedInt<64> { using Type = uint64_t; }; 23 | 24 | template 25 | using UnsignedIntType = typename UnsignedInt::Type; 26 | 27 | /// Helper callable object that just ignores its arguments and returns nothing. 28 | struct IgnoreArgs { 29 | template 30 | void operator () (Args&&...) const {} 31 | }; 32 | 33 | /// Generates a bitmask with the given number of bits. 34 | template , bool> = true> 35 | BVH_ALWAYS_INLINE constexpr T make_bitmask(size_t bits) { 36 | return bits >= std::numeric_limits::digits ? static_cast(-1) : (static_cast(1) << bits) - 1; 37 | } 38 | 39 | // These two functions are designed to return the second argument if the first one is a NaN. 40 | template , bool> = true> 41 | BVH_ALWAYS_INLINE T robust_min(T a, T b) { return a < b ? a : b; } 42 | template , bool> = true> 43 | BVH_ALWAYS_INLINE T robust_max(T a, T b) { return a > b ? a : b; } 44 | 45 | /// Adds the given number of ULPs (Units in the Last Place) to the given floating-point number. 46 | template , bool> = true> 47 | BVH_ALWAYS_INLINE T add_ulp_magnitude(T t, unsigned ulp) { 48 | if (!std::isfinite(t)) 49 | return t; 50 | UnsignedIntType u; 51 | std::memcpy(&u, &t, sizeof(T)); 52 | u += ulp; 53 | std::memcpy(&t, &u, sizeof(T)); 54 | return t; 55 | } 56 | 57 | /// Computes the inverse of the given value, always returning a finite value. 58 | template , bool> = true> 59 | BVH_ALWAYS_INLINE T safe_inverse(T x) { 60 | return std::fabs(x) <= std::numeric_limits::epsilon() 61 | ? std::copysign(std::numeric_limits::max(), x) 62 | : static_cast(1.) / x; 63 | } 64 | 65 | /// Fast multiply-add operation. Should translate into an FMA for architectures that support it. On 66 | /// architectures which do not support FMA in hardware, or on which FMA is slow, this defaults to a 67 | /// regular multiplication followed by an addition. 68 | #if defined(_MSC_VER) && !defined(__clang__) 69 | #pragma float_control(push) 70 | #pragma float_control(precise, off) 71 | #pragma fp_contract(on) 72 | #endif 73 | template , bool> = true> 74 | BVH_ALWAYS_INLINE T fast_mul_add(T a, T b, T c) { 75 | #ifdef FP_FAST_FMAF 76 | return std::fma(a, b, c); 77 | #elif defined(__clang__) 78 | BVH_CLANG_ENABLE_FP_CONTRACT 79 | #endif 80 | return a * b + c; 81 | } 82 | #if defined(_MSC_VER) && !defined(__clang__) 83 | #pragma float_control(pop) 84 | #endif 85 | 86 | /// Executes the given function once for every integer in the range `[Begin, End)`. 87 | template 88 | BVH_ALWAYS_INLINE void static_for(F&& f) { 89 | if constexpr (Begin < End) { 90 | f(Begin); 91 | static_for(std::forward(f)); 92 | } 93 | } 94 | 95 | /// Computes the (rounded-up) compile-time log in base-2 of an unsigned integer. 96 | template , bool> = true> 97 | inline constexpr T round_up_log2(T i, T p = 0) { 98 | return (static_cast(1) << p) >= i ? p : round_up_log2(i, p + 1); 99 | } 100 | 101 | /// Split an unsigned integer such that its bits are spaced by 2 zeros. 102 | /// For instance, split_bits(0b00110010) = 0b000000001001000000001000. 103 | template , bool> = true> 104 | BVH_ALWAYS_INLINE T split_bits(T x) { 105 | constexpr size_t bit_count = sizeof(T) * CHAR_BIT; 106 | constexpr size_t log_bits = round_up_log2(bit_count); 107 | auto mask = static_cast(-1) >> (bit_count / 2); 108 | x &= mask; 109 | for (size_t i = log_bits - 1, n = size_t{1} << i; i > 0; --i, n >>= 1) { 110 | mask = (mask | (mask << n)) & ~(mask << (n / 2)); 111 | x = (x | (x << n)) & mask; 112 | } 113 | return x; 114 | } 115 | 116 | /// Morton-encode three unsigned integers into one. 117 | template , bool> = true> 118 | BVH_ALWAYS_INLINE T morton_encode(T x, T y, T z) { 119 | return split_bits(x) | (split_bits(y) << 1) | (split_bits(z) << 2); 120 | } 121 | 122 | /// Computes the maximum between an atomic variable and a value, and returns the value previously 123 | /// held by the atomic variable. 124 | template 125 | BVH_ALWAYS_INLINE T atomic_max(std::atomic& atomic, const T& value) { 126 | auto prev_value = atomic; 127 | while (prev_value < value && !atomic.compare_exchange_weak(prev_value, value)); 128 | return prev_value; 129 | } 130 | 131 | } // namespace bvh::v2 132 | 133 | #endif 134 | -------------------------------------------------------------------------------- /src/bvh/v2/top_down_sah_builder.h: -------------------------------------------------------------------------------- 1 | #ifndef BVH_V2_TOP_DOWN_SAH_BUILDER_H 2 | #define BVH_V2_TOP_DOWN_SAH_BUILDER_H 3 | 4 | #include "bvh/v2/bvh.h" 5 | #include "bvh/v2/vec.h" 6 | #include "bvh/v2/bbox.h" 7 | #include "bvh/v2/split_heuristic.h" 8 | 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | 16 | namespace bvh::v2 { 17 | 18 | /// Base class for all SAH-based, top-down builders. 19 | template 20 | class TopDownSahBuilder { 21 | protected: 22 | using Scalar = typename Node::Scalar; 23 | using Vec = bvh::v2::Vec; 24 | using BBox = bvh::v2::BBox; 25 | 26 | public: 27 | struct Config { 28 | /// SAH heuristic parameters that control how primitives are partitioned. 29 | SplitHeuristic sah; 30 | 31 | /// Nodes containing less than this amount of primitives will not be split. 32 | /// This is mostly to speed up BVH construction, and using large values may lead to lower 33 | /// quality BVHs. 34 | size_t min_leaf_size = 1; 35 | 36 | /// Nodes that cannot be split based on the SAH and have a number of primitives larger than 37 | /// this will be split using a fallback strategy. This should not happen often, but may 38 | /// happen in worst-case scenarios or poorly designed scenes. 39 | size_t max_leaf_size = 8; 40 | }; 41 | 42 | protected: 43 | struct WorkItem { 44 | size_t node_id; 45 | size_t begin; 46 | size_t end; 47 | 48 | BVH_ALWAYS_INLINE size_t size() const { return end - begin; } 49 | }; 50 | 51 | std::span bboxes_; 52 | std::span centers_; 53 | const Config& config_; 54 | 55 | BVH_ALWAYS_INLINE TopDownSahBuilder( 56 | std::span bboxes, 57 | std::span centers, 58 | const Config& config) 59 | : bboxes_(bboxes) 60 | , centers_(centers) 61 | , config_(config) 62 | { 63 | assert(bboxes.size() == centers.size()); 64 | assert(config.min_leaf_size <= config.max_leaf_size); 65 | } 66 | 67 | virtual std::vector& get_prim_ids() = 0; 68 | virtual std::optional try_split(const BBox& bbox, size_t begin, size_t end) = 0; 69 | 70 | BVH_ALWAYS_INLINE const std::vector& get_prim_ids() const { 71 | return const_cast(this)->get_prim_ids(); 72 | } 73 | 74 | Bvh build() { 75 | const auto prim_count = bboxes_.size(); 76 | 77 | Bvh bvh; 78 | bvh.nodes.reserve((2 * prim_count) / config_.min_leaf_size); 79 | bvh.nodes.emplace_back(); 80 | bvh.nodes.back().set_bbox(compute_bbox(0, prim_count)); 81 | 82 | std::stack stack; 83 | stack.push(WorkItem { 0, 0, prim_count }); 84 | while (!stack.empty()) { 85 | auto item = stack.top(); 86 | stack.pop(); 87 | 88 | auto& node = bvh.nodes[item.node_id]; 89 | if (item.size() > config_.min_leaf_size) { 90 | if (auto split_pos = try_split(node.get_bbox(), item.begin, item.end)) { 91 | auto first_child = bvh.nodes.size(); 92 | node.index = Node::Index::make_inner(first_child); 93 | 94 | bvh.nodes.resize(first_child + 2); 95 | 96 | auto first_bbox = compute_bbox(item.begin, *split_pos); 97 | auto second_bbox = compute_bbox(*split_pos, item.end); 98 | auto first_range = std::make_pair(item.begin, *split_pos); 99 | auto second_range = std::make_pair(*split_pos, item.end); 100 | 101 | // For "any-hit" queries, the left child is chosen first, so we make sure that 102 | // it is the child with the largest area, as it is more likely to contain an 103 | // an occluder. See "SATO: Surface Area Traversal Order for Shadow Ray Tracing", 104 | // by J. Nah and D. Manocha. 105 | if (first_bbox.get_half_area() < second_bbox.get_half_area()) { 106 | std::swap(first_bbox, second_bbox); 107 | std::swap(first_range, second_range); 108 | } 109 | 110 | auto first_item = WorkItem { first_child + 0, first_range.first, first_range.second }; 111 | auto second_item = WorkItem { first_child + 1, second_range.first, second_range.second }; 112 | bvh.nodes[first_child + 0].set_bbox(first_bbox); 113 | bvh.nodes[first_child + 1].set_bbox(second_bbox); 114 | 115 | // Process the largest child item first, in order to minimize the stack size. 116 | if (first_item.size() < second_item.size()) 117 | std::swap(first_item, second_item); 118 | 119 | stack.push(first_item); 120 | stack.push(second_item); 121 | continue; 122 | } 123 | } 124 | 125 | node.index = Node::Index::make_leaf(item.begin, item.size()); 126 | } 127 | 128 | bvh.prim_ids = std::move(get_prim_ids()); 129 | bvh.nodes.shrink_to_fit(); 130 | return bvh; 131 | } 132 | 133 | BVH_ALWAYS_INLINE BBox compute_bbox(size_t begin, size_t end) const { 134 | const auto& prim_ids = get_prim_ids(); 135 | auto bbox = BBox::make_empty(); 136 | for (size_t i = begin; i < end; ++i) 137 | bbox.extend(bboxes_[prim_ids[i]]); 138 | return bbox; 139 | } 140 | }; 141 | 142 | } // namespace bvh::v2 143 | 144 | #endif 145 | -------------------------------------------------------------------------------- /src/bvh/v2/sweep_sah_builder.h: -------------------------------------------------------------------------------- 1 | #ifndef BVH_V2_SWEEP_SAH_BUILDER_H 2 | #define BVH_V2_SWEEP_SAH_BUILDER_H 3 | 4 | #include "bvh/v2/top_down_sah_builder.h" 5 | 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | 13 | namespace bvh::v2 { 14 | 15 | /// Single-threaded top-down builder that partitions primitives based on the Surface 16 | /// Area Heuristic (SAH). Primitives are only sorted once along each axis. 17 | template 18 | class SweepSahBuilder : public TopDownSahBuilder { 19 | using typename TopDownSahBuilder::Scalar; 20 | using typename TopDownSahBuilder::Vec; 21 | using typename TopDownSahBuilder::BBox; 22 | 23 | using TopDownSahBuilder::build; 24 | using TopDownSahBuilder::config_; 25 | using TopDownSahBuilder::bboxes_; 26 | 27 | public: 28 | using typename TopDownSahBuilder::Config; 29 | 30 | [[nodiscard]] BVH_ALWAYS_INLINE static Bvh build( 31 | std::span bboxes, 32 | std::span centers, 33 | const Config& config = {}) 34 | { 35 | return SweepSahBuilder(bboxes, centers, config).build(); 36 | } 37 | 38 | protected: 39 | struct Split { 40 | size_t pos; 41 | Scalar cost; 42 | size_t axis; 43 | }; 44 | 45 | std::vector marks_; 46 | std::vector accum_; 47 | std::vector prim_ids_[Node::dimension]; 48 | 49 | BVH_ALWAYS_INLINE SweepSahBuilder( 50 | std::span bboxes, 51 | std::span centers, 52 | const Config& config) 53 | : TopDownSahBuilder(bboxes, centers, config) 54 | { 55 | marks_.resize(bboxes.size()); 56 | accum_.resize(bboxes.size()); 57 | for (size_t axis = 0; axis < Node::dimension; ++axis) { 58 | prim_ids_[axis].resize(bboxes.size()); 59 | std::iota(prim_ids_[axis].begin(), prim_ids_[axis].end(), 0); 60 | std::sort(prim_ids_[axis].begin(), prim_ids_[axis].end(), [&] (size_t i, size_t j) { 61 | return centers[i][axis] < centers[j][axis]; 62 | }); 63 | } 64 | } 65 | 66 | std::vector& get_prim_ids() override { return prim_ids_[0]; } 67 | 68 | void find_best_split(size_t axis, size_t begin, size_t end, Split& best_split) { 69 | size_t first_right = begin; 70 | 71 | // Sweep from the right to the left, computing the partial SAH cost 72 | auto right_bbox = BBox::make_empty(); 73 | for (size_t i = end - 1; i > begin;) { 74 | static constexpr size_t chunk_size = 32; 75 | size_t next = i - std::min(i - begin, chunk_size); 76 | auto right_cost = static_cast(0.); 77 | for (; i > next; --i) { 78 | right_bbox.extend(bboxes_[prim_ids_[axis][i]]); 79 | accum_[i] = right_cost = config_.sah.get_leaf_cost(i, end, right_bbox); 80 | } 81 | // Every `chunk_size` elements, check that we are not above the maximum cost 82 | if (right_cost > best_split.cost) { 83 | first_right = i; 84 | break; 85 | } 86 | } 87 | 88 | // Sweep from the left to the right, computing the full cost 89 | auto left_bbox = BBox::make_empty(); 90 | for (size_t i = begin; i < first_right; ++i) 91 | left_bbox.extend(bboxes_[prim_ids_[axis][i]]); 92 | for (size_t i = first_right; i < end - 1; ++i) { 93 | left_bbox.extend(bboxes_[prim_ids_[axis][i]]); 94 | auto left_cost = config_.sah.get_leaf_cost(begin, i + 1, left_bbox); 95 | auto cost = left_cost + accum_[i + 1]; 96 | if (cost < best_split.cost) 97 | best_split = Split { i + 1, cost, axis }; 98 | else if (left_cost > best_split.cost) 99 | break; 100 | } 101 | } 102 | 103 | BVH_ALWAYS_INLINE void mark_primitives(size_t axis, size_t begin, size_t split_pos, size_t end) { 104 | for (size_t i = begin; i < split_pos; ++i) marks_[prim_ids_[axis][i]] = true; 105 | for (size_t i = split_pos; i < end; ++i) marks_[prim_ids_[axis][i]] = false; 106 | } 107 | 108 | std::optional try_split(const BBox& bbox, size_t begin, size_t end) override { 109 | // Find the best split over all axes 110 | auto leaf_cost = config_.sah.get_non_split_cost(begin, end, bbox); 111 | auto best_split = Split { (begin + end + 1) / 2, leaf_cost, 0 }; 112 | for (size_t axis = 0; axis < Node::dimension; ++axis) 113 | find_best_split(axis, begin, end, best_split); 114 | 115 | // Make sure that the split is good before proceeding with it 116 | if (best_split.cost >= leaf_cost) { 117 | if (end - begin <= config_.max_leaf_size) 118 | return std::nullopt; 119 | 120 | // If the number of primitives is too high, fallback on a split at the 121 | // median on the largest axis. 122 | best_split.pos = (begin + end + 1) / 2; 123 | best_split.axis = bbox.get_diagonal().get_largest_axis(); 124 | } 125 | 126 | // Partition primitives (keeping the order intact so that the next recursive calls do not 127 | // need to sort primitives again). 128 | mark_primitives(best_split.axis, begin, best_split.pos, end); 129 | for (size_t axis = 0; axis < Node::dimension; ++axis) { 130 | if (axis == best_split.axis) 131 | continue; 132 | std::stable_partition( 133 | prim_ids_[axis].begin() + begin, 134 | prim_ids_[axis].begin() + end, 135 | [&] (size_t i) { return marks_[i]; }); 136 | } 137 | 138 | return std::make_optional(best_split.pos); 139 | } 140 | }; 141 | 142 | } // namespace bvh::v2 143 | 144 | #endif 145 | -------------------------------------------------------------------------------- /src/bvh/v2/binned_sah_builder.h: -------------------------------------------------------------------------------- 1 | #ifndef BVH_V2_BINNED_SAH_BUILDER_H 2 | #define BVH_V2_BINNED_SAH_BUILDER_H 3 | 4 | #include "bvh/v2/top_down_sah_builder.h" 5 | 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | 13 | namespace bvh::v2 { 14 | 15 | /// Single-threaded top-down builder that partitions primitives based on a binned approximation of 16 | /// the Surface Area Heuristic (SAH). This builder is inspired by 17 | /// "On Fast Construction of SAH-based Bounding Volume Hierarchies", by I. Wald. 18 | template 19 | class BinnedSahBuilder : public TopDownSahBuilder { 20 | using typename TopDownSahBuilder::Scalar; 21 | using typename TopDownSahBuilder::Vec; 22 | using typename TopDownSahBuilder::BBox; 23 | 24 | using TopDownSahBuilder::build; 25 | using TopDownSahBuilder::config_; 26 | using TopDownSahBuilder::bboxes_; 27 | using TopDownSahBuilder::centers_; 28 | 29 | public: 30 | using typename TopDownSahBuilder::Config; 31 | 32 | [[nodiscard]] BVH_ALWAYS_INLINE static Bvh build( 33 | std::span bboxes, 34 | std::span centers, 35 | const Config& config = {}) 36 | { 37 | return BinnedSahBuilder(bboxes, centers, config).build(); 38 | } 39 | 40 | protected: 41 | struct Split { 42 | size_t bin_id; 43 | Scalar cost; 44 | size_t axis; 45 | }; 46 | 47 | struct Bin { 48 | BBox bbox = BBox::make_empty(); 49 | size_t prim_count = 0; 50 | 51 | Bin() = default; 52 | 53 | BVH_ALWAYS_INLINE Scalar get_cost(const SplitHeuristic& sah) const { 54 | return sah.get_leaf_cost(0, prim_count, bbox); 55 | } 56 | 57 | BVH_ALWAYS_INLINE void add(const BBox& bbox, size_t prim_count = 1) { 58 | this->bbox.extend(bbox); 59 | this->prim_count += prim_count; 60 | } 61 | 62 | BVH_ALWAYS_INLINE void add(const Bin& bin) { add(bin.bbox, bin.prim_count); } 63 | }; 64 | 65 | using Bins = std::array; 66 | using PerAxisBins = std::array; 67 | 68 | std::vector prim_ids_; 69 | 70 | BVH_ALWAYS_INLINE BinnedSahBuilder( 71 | std::span bboxes, 72 | std::span centers, 73 | const Config& config) 74 | : TopDownSahBuilder(bboxes, centers, config) 75 | , prim_ids_(bboxes.size()) 76 | { 77 | std::iota(prim_ids_.begin(), prim_ids_.end(), 0); 78 | } 79 | 80 | std::vector& get_prim_ids() override { return prim_ids_; } 81 | 82 | BVH_ALWAYS_INLINE void fill_bins( 83 | PerAxisBins& per_axis_bins, 84 | const BBox& bbox, 85 | size_t begin, 86 | size_t end) 87 | { 88 | auto bin_scale = Vec(BinCount) / bbox.get_diagonal(); 89 | auto bin_offset = -bbox.min * bin_scale; 90 | 91 | for (size_t i = begin; i < end; ++i) { 92 | auto pos = fast_mul_add(centers_[prim_ids_[i]], bin_scale, bin_offset); 93 | static_for<0, Node::dimension>([&] (size_t axis) { 94 | size_t index = std::min(BinCount - 1, 95 | static_cast(robust_max(pos[axis], static_cast(0.)))); 96 | per_axis_bins[axis][index].add(bboxes_[prim_ids_[i]]); 97 | }); 98 | } 99 | } 100 | 101 | void find_best_split(size_t axis, const Bins& bins, Split& best_split) { 102 | Bin right_accum; 103 | std::array right_costs; 104 | for (size_t i = BinCount - 1; i > 0; --i) { 105 | right_accum.add(bins[i]); 106 | right_costs[i] = right_accum.get_cost(config_.sah); 107 | } 108 | 109 | Bin left_accum; 110 | for (size_t i = 0; i < BinCount - 1; ++i) { 111 | left_accum.add(bins[i]); 112 | auto cost = left_accum.get_cost(config_.sah) + right_costs[i + 1]; 113 | if (cost < best_split.cost) 114 | best_split = Split { i + 1, cost, axis }; 115 | } 116 | } 117 | 118 | size_t fallback_split(size_t axis, size_t begin, size_t end) { 119 | size_t mid = (begin + end + 1) / 2; 120 | std::partial_sort( 121 | prim_ids_.begin() + begin, 122 | prim_ids_.begin() + mid, 123 | prim_ids_.begin() + end, 124 | [&] (size_t i, size_t j) { return centers_[i][axis] < centers_[j][axis]; }); 125 | return mid; 126 | } 127 | 128 | std::optional try_split(const BBox& bbox, size_t begin, size_t end) override { 129 | PerAxisBins per_axis_bins; 130 | fill_bins(per_axis_bins, bbox, begin, end); 131 | 132 | auto largest_axis = bbox.get_diagonal().get_largest_axis(); 133 | auto best_split = Split { BinCount / 2, std::numeric_limits::max(), largest_axis }; 134 | for (size_t axis = 0; axis < Node::dimension; ++axis) 135 | find_best_split(axis, per_axis_bins[axis], best_split); 136 | 137 | // Make sure that the split is good before proceeding with it 138 | auto leaf_cost = config_.sah.get_non_split_cost(begin, end, bbox); 139 | if (best_split.cost >= leaf_cost) { 140 | if (end - begin <= config_.max_leaf_size) 141 | return std::nullopt; 142 | return fallback_split(largest_axis, begin, end); 143 | } 144 | 145 | auto split_pos = fast_mul_add( 146 | bbox.get_diagonal()[best_split.axis] / static_cast(BinCount), 147 | static_cast(best_split.bin_id), 148 | bbox.min[best_split.axis]); 149 | 150 | size_t index = std::partition(prim_ids_.begin() + begin, prim_ids_.begin() + end, 151 | [&] (size_t i) { return centers_[i][best_split.axis] < split_pos; }) - prim_ids_.begin(); 152 | if (index == begin || index == end) 153 | return fallback_split(largest_axis, begin, end); 154 | 155 | return std::make_optional(index); 156 | } 157 | }; 158 | 159 | } // namespace bvh::v2 160 | 161 | #endif 162 | -------------------------------------------------------------------------------- /src/bvh/v2/bvh.h: -------------------------------------------------------------------------------- 1 | #ifndef BVH_V2_BVH_H 2 | #define BVH_V2_BVH_H 3 | 4 | #include "bvh/v2/node.h" 5 | 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | 14 | namespace bvh::v2 { 15 | 16 | template 17 | struct Bvh { 18 | using Index = typename Node::Index; 19 | using Scalar = typename Node::Scalar; 20 | using Ray = bvh::v2::Ray; 21 | 22 | std::vector nodes; 23 | std::vector prim_ids; 24 | 25 | Bvh() = default; 26 | Bvh(Bvh&&) = default; 27 | 28 | Bvh& operator = (Bvh&&) = default; 29 | 30 | bool operator == (const Bvh& other) const = default; 31 | bool operator != (const Bvh& other) const = default; 32 | 33 | /// Returns whether the node located at the given index is the left child of its parent. 34 | static BVH_ALWAYS_INLINE bool is_left_sibling(size_t node_id) { return node_id % 2 == 1; } 35 | 36 | /// Returns the index of a sibling of a node. 37 | static BVH_ALWAYS_INLINE size_t get_sibling_id(size_t node_id) { 38 | return is_left_sibling(node_id) ? node_id + 1 : node_id - 1; 39 | } 40 | 41 | /// Returns the index of the left sibling of the node. This effectively returns the given index 42 | /// unchanged if the node is the left sibling, or the other sibling index otherwise. 43 | static BVH_ALWAYS_INLINE size_t get_left_sibling_id(size_t node_id) { 44 | return is_left_sibling(node_id) ? node_id : node_id - 1; 45 | } 46 | 47 | /// Returns the index of the right sibling of the node. This effectively returns the given index 48 | /// unchanged if the node is the right sibling, or the other sibling index otherwise. 49 | static BVH_ALWAYS_INLINE size_t get_right_sibling_id(size_t node_id) { 50 | return is_left_sibling(node_id) ? node_id + 1 : node_id; 51 | } 52 | 53 | /// Returns the root node of this BVH. 54 | BVH_ALWAYS_INLINE const Node& get_root() const { return nodes[0]; } 55 | 56 | /// Extracts the BVH rooted at the given node index. 57 | [[nodiscard]] inline Bvh extract_bvh(size_t root_id) const; 58 | 59 | /// Traverses the BVH from the given index in `start` using the provided stack. Every leaf 60 | /// encountered on the way is processed using the given `LeafFn` function, and every pair of 61 | /// nodes is processed with the function in `InnerFn`, which returns a triplet of booleans 62 | /// indicating whether the first child should be processed, whether the second child should be 63 | /// processed, and whether to traverse the second child first instead of the other way around. 64 | template 65 | inline void traverse_top_down(Index start, Stack&, LeafFn&&, InnerFn&&) const; 66 | 67 | /// Intersects the BVH with a single ray, using the given function to intersect the contents 68 | /// of a leaf. The algorithm starts at the node index `start` and uses the given stack object. 69 | /// When `IsAnyHit` is true, the function stops at the first intersection (useful for shadow 70 | /// rays), otherwise it finds the closest intersection. When `IsRobust` is true, a slower but 71 | /// numerically robust ray-box test is used, otherwise a fast, but less precise test is used. 72 | template 73 | inline void intersect(const Ray& ray, Index start, Stack&, LeafFn&&, InnerFn&& = {}) const; 74 | 75 | /// Traverses this BVH from the bottom to the top, using the given function objects to process 76 | /// leaves and inner nodes. 77 | template 78 | inline void traverse_bottom_up(LeafFn&& = {}, InnerFn&& = {}); 79 | 80 | /// Refits the BVH, using the given function object to recompute the bounding box of the leaves. 81 | template 82 | inline void refit(LeafFn&& = {}); 83 | 84 | template 85 | inline void serialize(OutputStream&) const; 86 | 87 | template 88 | [[nodiscard]] static inline Bvh deserialize(InputStream&); 89 | }; 90 | 91 | template 92 | Bvh Bvh::extract_bvh(size_t root_id) const { 93 | assert(root_id != 0); 94 | 95 | Bvh bvh; 96 | bvh.nodes.emplace_back(); 97 | 98 | std::stack> stack; 99 | stack.emplace(root_id, 0); 100 | while (!stack.empty()) { 101 | auto [src_id, dst_id] = stack.top(); 102 | stack.pop(); 103 | const auto& src_node = nodes[src_id]; 104 | auto& dst_node = bvh.nodes[dst_id]; 105 | dst_node = src_node; 106 | if (src_node.is_leaf()) { 107 | dst_node.index.set_first_id(bvh.prim_ids.size()); 108 | std::copy_n( 109 | prim_ids.begin() + src_node.index.first_id(), 110 | src_node.index.prim_count(), 111 | std::back_inserter(bvh.prim_ids)); 112 | } else { 113 | dst_node.index.set_first_id(bvh.nodes.size()); 114 | stack.emplace(src_node.index.first_id() + 0, bvh.nodes.size() + 0); 115 | stack.emplace(src_node.index.first_id() + 1, bvh.nodes.size() + 1); 116 | // Note: This may invalidate `dst_node` so has to happen after any access to it. 117 | bvh.nodes.emplace_back(); 118 | bvh.nodes.emplace_back(); 119 | } 120 | } 121 | return bvh; 122 | } 123 | 124 | template 125 | template 126 | void Bvh::traverse_top_down(Index start, Stack& stack, LeafFn&& leaf_fn, InnerFn&& inner_fn) const 127 | { 128 | stack.push(start); 129 | restart: 130 | while (!stack.is_empty()) { 131 | auto top = stack.pop(); 132 | while (top.prim_count() == 0) { 133 | auto& left = nodes[top.first_id()]; 134 | auto& right = nodes[top.first_id() + 1]; 135 | auto [hit_left, hit_right, should_swap] = inner_fn(left, right); 136 | 137 | if (hit_left) { 138 | auto near_index = left.index; 139 | if (hit_right) { 140 | auto far_index = right.index; 141 | if (should_swap) 142 | std::swap(near_index, far_index); 143 | stack.push(far_index); 144 | } 145 | top = near_index; 146 | } else if (hit_right) 147 | top = right.index; 148 | else [[unlikely]] 149 | goto restart; 150 | } 151 | 152 | [[maybe_unused]] auto was_hit = leaf_fn(top.first_id(), top.first_id() + top.prim_count()); 153 | if constexpr (IsAnyHit) { 154 | if (was_hit) return; 155 | } 156 | } 157 | } 158 | 159 | template 160 | template 161 | void Bvh::intersect(const Ray& ray, Index start, Stack& stack, LeafFn&& leaf_fn, InnerFn&& inner_fn) const { 162 | auto inv_dir = ray.template get_inv_dir(); 163 | auto inv_org = -inv_dir * ray.org; 164 | auto inv_dir_pad = ray.pad_inv_dir(inv_dir); 165 | auto octant = ray.get_octant(); 166 | 167 | traverse_top_down(start, stack, leaf_fn, [&] (const Node& left, const Node& right) { 168 | inner_fn(left, right); 169 | std::pair intr_left, intr_right; 170 | if constexpr (IsRobust) { 171 | intr_left = left .intersect_robust(ray, inv_dir, inv_dir_pad, octant); 172 | intr_right = right.intersect_robust(ray, inv_dir, inv_dir_pad, octant); 173 | } else { 174 | intr_left = left .intersect_fast(ray, inv_dir, inv_org, octant); 175 | intr_right = right.intersect_fast(ray, inv_dir, inv_org, octant); 176 | } 177 | return std::make_tuple( 178 | intr_left.first <= intr_left.second, 179 | intr_right.first <= intr_right.second, 180 | !IsAnyHit && intr_left.first > intr_right.first); 181 | }); 182 | } 183 | 184 | template 185 | template 186 | void Bvh::traverse_bottom_up(LeafFn&& leaf_fn, InnerFn&& inner_fn) { 187 | std::vector parents(nodes.size(), 0); 188 | for (size_t i = 0; i < nodes.size(); ++i) { 189 | if (nodes[i].is_leaf()) 190 | continue; 191 | parents[nodes[i].index.first_id()] = i; 192 | parents[nodes[i].index.first_id() + 1] = i; 193 | } 194 | std::vector seen(nodes.size(), false); 195 | for (size_t i = nodes.size(); i-- > 0;) { 196 | if (!nodes[i].is_leaf()) 197 | continue; 198 | leaf_fn(nodes[i]); 199 | seen[i] = true; 200 | for (size_t j = parents[i];; j = parents[j]) { 201 | auto& node = nodes[j]; 202 | if (seen[j] || !seen[node.index.first_id()] || !seen[node.index.first_id() + 1]) 203 | break; 204 | inner_fn(nodes[j]); 205 | seen[j] = true; 206 | } 207 | } 208 | } 209 | 210 | template 211 | template 212 | void Bvh::refit(LeafFn&& leaf_fn) { 213 | traverse_bottom_up(leaf_fn, [&] (Node& node) { 214 | const auto& left = nodes[node.index.first_id()]; 215 | const auto& right = nodes[node.index.first_id() + 1]; 216 | node.set_bbox(left.get_bbox().extend(right.get_bbox())); 217 | }); 218 | } 219 | 220 | template 221 | template 222 | void Bvh::serialize(OutputStream& stream) const { 223 | stream.write(static_cast(nodes.size())); 224 | stream.write(static_cast(prim_ids.size())); 225 | for (auto&& node : nodes) 226 | node.serialize(stream); 227 | for (auto&& prim_id : prim_ids) 228 | stream.write(static_cast(prim_id)); 229 | } 230 | 231 | template 232 | template 233 | Bvh Bvh::deserialize(InputStream& stream) { 234 | Bvh bvh; 235 | bvh.nodes.resize(stream.read()); 236 | bvh.prim_ids.resize(stream.read()); 237 | for (auto& node : bvh.nodes) 238 | node = Node::deserialize(stream); 239 | for (auto& prim_id : bvh.prim_ids) 240 | prim_id = stream.read(); 241 | return bvh; 242 | } 243 | 244 | } // namespace bvh::v2 245 | 246 | #endif 247 | -------------------------------------------------------------------------------- /src/bvh/v2/reinsertion_optimizer.h: -------------------------------------------------------------------------------- 1 | #ifndef BVH_V2_REINSERTION_OPTIMIZER_H 2 | #define BVH_V2_REINSERTION_OPTIMIZER_H 3 | 4 | #include "bvh/v2/bvh.h" 5 | #include "bvh/v2/thread_pool.h" 6 | #include "bvh/v2/executor.h" 7 | 8 | #include 9 | #include 10 | 11 | namespace bvh::v2 { 12 | 13 | template 14 | class ReinsertionOptimizer { 15 | using Scalar = typename Node::Scalar; 16 | using BBox = bvh::v2::BBox; 17 | 18 | public: 19 | struct Config { 20 | /// Fraction of the number of nodes to optimize per iteration. 21 | Scalar batch_size_ratio = static_cast(0.05); 22 | 23 | /// Maximum number of iterations. 24 | size_t max_iter_count = 3; 25 | }; 26 | 27 | static void optimize(ThreadPool& thread_pool, Bvh& bvh, const Config& config = {}) { 28 | ParallelExecutor executor(thread_pool); 29 | optimize(executor, bvh, config); 30 | } 31 | 32 | static void optimize(Bvh& bvh, const Config& config = {}) { 33 | SequentialExecutor executor; 34 | optimize(executor, bvh, config); 35 | } 36 | 37 | private: 38 | struct Candidate { 39 | size_t node_id = 0; 40 | Scalar cost = -std::numeric_limits::max(); 41 | 42 | BVH_ALWAYS_INLINE bool operator > (const Candidate& other) const { 43 | return cost > other.cost; 44 | } 45 | }; 46 | 47 | struct Reinsertion { 48 | size_t from = 0; 49 | size_t to = 0; 50 | Scalar area_diff = static_cast(0); 51 | 52 | BVH_ALWAYS_INLINE bool operator > (const Reinsertion& other) const { 53 | return area_diff > other.area_diff; 54 | } 55 | }; 56 | 57 | Bvh& bvh_; 58 | std::vector parents_; 59 | 60 | ReinsertionOptimizer(Bvh& bvh, std::vector&& parents) 61 | : bvh_(bvh) 62 | , parents_(std::move(parents)) 63 | {} 64 | 65 | template 66 | static void optimize(Executor& executor, Bvh& bvh, const Config& config) { 67 | auto parents = compute_parents(executor, bvh); 68 | ReinsertionOptimizer(bvh, std::move(parents)).optimize(executor, config); 69 | } 70 | 71 | template 72 | static std::vector compute_parents(Executor& executor, const Bvh& bvh) { 73 | std::vector parents(bvh.nodes.size()); 74 | parents[0] = 0; 75 | executor.for_each(0, bvh.nodes.size(), 76 | [&] (size_t begin, size_t end) { 77 | for (size_t i = begin; i < end; ++i) { 78 | auto& node = bvh.nodes[i]; 79 | if (!node.is_leaf()) { 80 | parents[node.index.first_id() + 0] = i; 81 | parents[node.index.first_id() + 1] = i; 82 | } 83 | } 84 | }); 85 | return parents; 86 | } 87 | 88 | BVH_ALWAYS_INLINE std::vector find_candidates(size_t target_count) { 89 | // Gather the `target_count` nodes that have the highest cost. 90 | // Note that this may produce fewer nodes if the BVH has fewer than `target_count` nodes. 91 | const auto node_count = std::min(bvh_.nodes.size(), target_count + 1); 92 | std::vector candidates; 93 | for (size_t i = 1; i < node_count; ++i) 94 | candidates.push_back(Candidate { i, bvh_.nodes[i].get_bbox().get_half_area() }); 95 | std::make_heap(candidates.begin(), candidates.end(), std::greater<>{}); 96 | for (size_t i = node_count; i < bvh_.nodes.size(); ++i) { 97 | auto cost = bvh_.nodes[i].get_bbox().get_half_area(); 98 | if (candidates.front().cost < cost) { 99 | std::pop_heap(candidates.begin(), candidates.end(), std::greater<>{}); 100 | candidates.back() = Candidate { i, cost }; 101 | std::push_heap(candidates.begin(), candidates.end(), std::greater<>{}); 102 | } 103 | } 104 | return candidates; 105 | } 106 | 107 | Reinsertion find_reinsertion(size_t node_id) { 108 | assert(node_id != 0); 109 | 110 | /* 111 | * Here is an example that explains how the cost of a reinsertion is computed. For the 112 | * reinsertion from A to C, in the figure below, we need to remove P1, replace it by B, 113 | * and create a node that holds A and C and place it where C was. 114 | * 115 | * R 116 | * / \ 117 | * Pn Q1 118 | * / \ 119 | * ... ... 120 | * / \ 121 | * P1 C 122 | * / \ 123 | * A B 124 | * 125 | * The resulting area *decrease* is (SA(x) means the surface area of x): 126 | * 127 | * SA(P1) + : P1 was removed 128 | * SA(P2) - SA(B) + : P2 now only contains B 129 | * SA(P3) - SA(B U sibling(P2)) + : Same but for P3 130 | * ... + 131 | * SA(Pn) - SA(B U sibling(P2) U ... U sibling(P(n - 1)) + : Same but for Pn 132 | * 0 + : R does not change 133 | * SA(Q1) - SA(Q1 U A) + : Q1 now contains A 134 | * SA(Q2) - SA(Q2 U A) + : Q2 now contains A 135 | * ... + 136 | * -SA(A U C) : For the parent of A and C 137 | */ 138 | 139 | Reinsertion best_reinsertion { .from = node_id }; 140 | auto node_area = bvh_.nodes[node_id].get_bbox().get_half_area(); 141 | auto parent_area = bvh_.nodes[parents_[node_id]].get_bbox().get_half_area(); 142 | auto area_diff = parent_area; 143 | auto sibling_id = Bvh::get_sibling_id(node_id); 144 | auto pivot_bbox = bvh_.nodes[sibling_id].get_bbox(); 145 | auto parent_id = parents_[node_id]; 146 | auto pivot_id = parent_id; 147 | 148 | std::vector> stack; 149 | do { 150 | // Try to find a reinsertion in the sibling at the current level of the tree 151 | stack.emplace_back(area_diff, sibling_id); 152 | while (!stack.empty()) { 153 | auto top = stack.back(); 154 | stack.pop_back(); 155 | if (top.first - node_area <= best_reinsertion.area_diff) 156 | continue; 157 | 158 | auto& dst_node = bvh_.nodes[top.second]; 159 | auto merged_area = dst_node.get_bbox().extend(bvh_.nodes[node_id].get_bbox()).get_half_area(); 160 | auto reinsert_area = top.first - merged_area; 161 | if (reinsert_area > best_reinsertion.area_diff) { 162 | best_reinsertion.to = top.second; 163 | best_reinsertion.area_diff = reinsert_area; 164 | } 165 | 166 | if (!dst_node.is_leaf()) { 167 | auto child_area = reinsert_area + dst_node.get_bbox().get_half_area(); 168 | stack.emplace_back(child_area, dst_node.index.first_id() + 0); 169 | stack.emplace_back(child_area, dst_node.index.first_id() + 1); 170 | } 171 | } 172 | 173 | // Compute the bounding box on the path from the node to the root, and record the 174 | // corresponding decrease in area. 175 | if (pivot_id != parent_id) { 176 | pivot_bbox.extend(bvh_.nodes[sibling_id].get_bbox()); 177 | area_diff += bvh_.nodes[pivot_id].get_bbox().get_half_area() - pivot_bbox.get_half_area(); 178 | } 179 | 180 | sibling_id = Bvh::get_sibling_id(pivot_id); 181 | pivot_id = parents_[pivot_id]; 182 | } while (pivot_id != 0); 183 | 184 | if (best_reinsertion.to == Bvh::get_sibling_id(best_reinsertion.from) || 185 | best_reinsertion.to == parents_[best_reinsertion.from]) 186 | best_reinsertion = {}; 187 | return best_reinsertion; 188 | } 189 | 190 | BVH_ALWAYS_INLINE void reinsert_node(size_t from, size_t to) { 191 | auto sibling_id = Bvh::get_sibling_id(from); 192 | auto parent_id = parents_[from]; 193 | auto sibling_node = bvh_.nodes[sibling_id]; 194 | auto dst_node = bvh_.nodes[to]; 195 | 196 | bvh_.nodes[to].index = Node::Index::make_inner(Bvh::get_left_sibling_id(from)); 197 | bvh_.nodes[sibling_id] = dst_node; 198 | bvh_.nodes[parent_id] = sibling_node; 199 | 200 | if (!dst_node.is_leaf()) { 201 | parents_[dst_node.index.first_id() + 0] = sibling_id; 202 | parents_[dst_node.index.first_id() + 1] = sibling_id; 203 | } 204 | if (!sibling_node.is_leaf()) { 205 | parents_[sibling_node.index.first_id() + 0] = parent_id; 206 | parents_[sibling_node.index.first_id() + 1] = parent_id; 207 | } 208 | 209 | parents_[sibling_id] = to; 210 | parents_[from] = to; 211 | refit_from(to); 212 | refit_from(parent_id); 213 | } 214 | 215 | BVH_ALWAYS_INLINE void refit_from(size_t index) { 216 | do { 217 | auto& node = bvh_.nodes[index]; 218 | if (!node.is_leaf()) { 219 | node.set_bbox( 220 | bvh_.nodes[node.index.first_id() + 0].get_bbox().extend( 221 | bvh_.nodes[node.index.first_id() + 1].get_bbox())); 222 | } 223 | index = parents_[index]; 224 | } while (index != 0); 225 | } 226 | 227 | BVH_ALWAYS_INLINE auto get_conflicts(size_t from, size_t to) { 228 | return std::array { 229 | to, from, 230 | Bvh::get_sibling_id(from), 231 | parents_[to], 232 | parents_[from] 233 | }; 234 | } 235 | 236 | template 237 | void optimize(Executor& executor, const Config& config) { 238 | auto batch_size = std::max(size_t{1}, 239 | static_cast(static_cast(bvh_.nodes.size()) * config.batch_size_ratio)); 240 | std::vector reinsertions; 241 | std::vector touched(bvh_.nodes.size()); 242 | 243 | for (size_t iter = 0; iter < config.max_iter_count; ++iter) { 244 | auto candidates = find_candidates(batch_size); 245 | 246 | std::fill(touched.begin(), touched.end(), false); 247 | reinsertions.resize(candidates.size()); 248 | executor.for_each(0, candidates.size(), 249 | [&] (size_t begin, size_t end) { 250 | for (size_t i = begin; i < end; ++i) 251 | reinsertions[i] = find_reinsertion(candidates[i].node_id); 252 | }); 253 | 254 | reinsertions.erase(std::remove_if(reinsertions.begin(), reinsertions.end(), 255 | [] (auto& r) { return r.area_diff <= 0; }), reinsertions.end()); 256 | std::sort(reinsertions.begin(), reinsertions.end(), std::greater<>{}); 257 | 258 | for (auto& reinsertion : reinsertions) { 259 | auto conflicts = get_conflicts(reinsertion.from, reinsertion.to); 260 | if (std::any_of(conflicts.begin(), conflicts.end(), [&] (size_t i) { return touched[i]; })) 261 | continue; 262 | for (auto conflict : conflicts) 263 | touched[conflict] = true; 264 | reinsert_node(reinsertion.from, reinsertion.to); 265 | } 266 | } 267 | } 268 | }; 269 | 270 | } // namespace bvh::v2 271 | 272 | #endif 273 | -------------------------------------------------------------------------------- /test/c_api_example.c: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include "load_obj.h" 4 | 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | 17 | #define INVALID_TRI_ID UINT32_MAX 18 | 19 | struct scene { 20 | struct bvh3f* bvh; 21 | struct tri* tris; 22 | size_t tri_count; 23 | }; 24 | 25 | struct options { 26 | const char* input_scene; 27 | const char* output_image; 28 | uint32_t width, height; 29 | struct bvh_vec3f eye, dir, up; 30 | }; 31 | 32 | struct image { 33 | uint8_t* pixels; 34 | uint32_t width, height; 35 | }; 36 | 37 | struct hit { 38 | float t, u, v; 39 | uint32_t tri_id; 40 | }; 41 | 42 | struct intersect_user_data { 43 | const struct bvh_ray3f* ray; 44 | const struct scene* scene; 45 | struct hit* hit; 46 | }; 47 | 48 | static inline float dot(struct bvh_vec3f v, struct bvh_vec3f w) { 49 | return v.x * w.x + v.y * w.y + v.z * w.z; 50 | } 51 | 52 | static inline float length(struct bvh_vec3f v) { 53 | return dot(v, v); 54 | } 55 | 56 | static inline struct bvh_vec3f sub(struct bvh_vec3f v, struct bvh_vec3f w) { 57 | return (struct bvh_vec3f) { 58 | .x = v.x - w.x, 59 | .y = v.y - w.y, 60 | .z = v.z - w.z, 61 | }; 62 | } 63 | 64 | static inline struct bvh_vec3f cross(struct bvh_vec3f v, struct bvh_vec3f w) { 65 | return (struct bvh_vec3f) { 66 | .x = v.y * w.z - v.z * w.y, 67 | .y = v.z * w.x - v.x * w.z, 68 | .z = v.x * w.y - v.y * w.x, 69 | }; 70 | } 71 | 72 | static inline struct bvh_vec3f normalize(struct bvh_vec3f v) { 73 | const float inv_len = 1.f / length(v); 74 | return (struct bvh_vec3f) { 75 | .x = v.x * inv_len, 76 | .y = v.y * inv_len, 77 | .z = v.z * inv_len, 78 | }; 79 | } 80 | 81 | static inline struct bvh_vec3f tri_center(const struct tri* tri) { 82 | return (struct bvh_vec3f) { 83 | .x = (tri->v[0].x + tri->v[1].x + tri->v[2].x) * (1.f / 3.f), 84 | .y = (tri->v[0].y + tri->v[1].y + tri->v[2].y) * (1.f / 3.f), 85 | .z = (tri->v[0].z + tri->v[1].z + tri->v[2].z) * (1.f / 3.f) 86 | }; 87 | } 88 | 89 | static inline struct bvh_bbox3f tri_bbox(const struct tri* tri) { 90 | struct bvh_vec3f min = tri->v[0], max = min; 91 | for (int i = 1; i < 3; ++i) { 92 | min.x = min.x < tri->v[i].x ? min.x : tri->v[i].x; 93 | min.y = min.y < tri->v[i].y ? min.y : tri->v[i].y; 94 | min.z = min.z < tri->v[i].z ? min.z : tri->v[i].z; 95 | max.x = max.x > tri->v[i].x ? max.x : tri->v[i].x; 96 | max.y = max.y > tri->v[i].y ? max.y : tri->v[i].y; 97 | max.z = max.z > tri->v[i].z ? max.z : tri->v[i].z; 98 | } 99 | return (struct bvh_bbox3f) { .min = min, .max = max }; 100 | } 101 | 102 | static inline void build_bvh(struct scene* scene) { 103 | assert(!scene->bvh); 104 | struct bvh_bbox3f* bboxes = malloc(sizeof(struct bvh_bbox3f) * scene->tri_count); 105 | struct bvh_vec3f* centers = malloc(sizeof(struct bvh_vec3f) * scene->tri_count); 106 | for (size_t i = 0; i < scene->tri_count; ++i) { 107 | bboxes[i] = tri_bbox(&scene->tris[i]); 108 | centers[i] = tri_center(&scene->tris[i]); 109 | } 110 | 111 | struct bvh_thread_pool* thread_pool = bvh_thread_pool_create(0); 112 | struct timespec ts0, ts1; 113 | timespec_get(&ts0, TIME_UTC); 114 | scene->bvh = bvh3f_build(thread_pool, bboxes, centers, scene->tri_count, NULL); 115 | timespec_get(&ts1, TIME_UTC); 116 | bvh_thread_pool_destroy(thread_pool); 117 | free(bboxes); 118 | free(centers); 119 | const uint64_t build_time = (ts1.tv_sec - ts0.tv_sec) * 1000 + (ts1.tv_nsec - ts0.tv_nsec) / 1000000; 120 | printf("Built BVH with %zu node(s) in %"PRIu64"\n", bvh3f_get_node_count(scene->bvh), build_time); 121 | } 122 | 123 | static inline void destroy_scene(struct scene* scene) { 124 | bvh3f_destroy(scene->bvh); 125 | free(scene->tris); 126 | memset(scene, 0, sizeof(struct scene)); 127 | } 128 | 129 | struct image alloc_image(size_t width, size_t height) { 130 | return (struct image) { 131 | .pixels = malloc(sizeof(uint8_t) * width * height * 3), 132 | .width = width, 133 | .height = height 134 | }; 135 | } 136 | 137 | static void free_image(struct image* image) { 138 | free(image->pixels); 139 | memset(image, 0, sizeof(struct image)); 140 | } 141 | 142 | static bool save_image(const struct image* image, const char* file_name) { 143 | FILE* file = fopen(file_name, "wb"); 144 | if (!file) 145 | return false; 146 | fprintf(file, "P6 %"PRIu32" %"PRIu32" 255\n", image->width, image->height); 147 | for (uint32_t y = image->height; y-- > 0;) 148 | fwrite(image->pixels + y * 3 * image->width, sizeof(uint8_t), 3 * image->width, file); 149 | fclose(file); 150 | return true; 151 | } 152 | 153 | static void set_pixel(struct image* image, uint32_t x, uint32_t y, uint8_t r, uint8_t g, uint8_t b) { 154 | uint8_t* p = &image->pixels[(y * image->width + x) * 3]; 155 | p[0] = r; 156 | p[1] = g; 157 | p[2] = b; 158 | } 159 | 160 | static void usage() { 161 | printf( 162 | "Usage: c_api_example [options] file.obj\n" 163 | "\nOptions:\n" 164 | " -h --help Shows this message.\n" 165 | " -e --eye Sets the position of the camera.\n" 166 | " -d --dir Sets the direction of the camera.\n" 167 | " -u --up Sets the up vector of the camera.\n" 168 | " -w --width Sets the image width.\n" 169 | " -h --height Sets the image height.\n" 170 | " -o Sets the output file name (defaults to 'render.ppm').\n"); 171 | } 172 | 173 | static inline bool check_arg(int i, int n, int argc, char** argv) { 174 | if (i + n > argc) { 175 | fprintf(stderr, "Missing argument(s) for '%s'\n", argv[i]); 176 | return false; 177 | } 178 | return true; 179 | } 180 | 181 | static inline bool parse_options(int argc, char** argv, struct options* options) { 182 | for (int i = 1; i < argc; ++i) { 183 | if (argv[i][0] == '-') { 184 | if (!strcmp(argv[i], "-e") || !strcmp(argv[i], "--eye")) { 185 | if (!check_arg(i, 3, argc, argv)) 186 | return false; 187 | options->eye.x = strtof(argv[++i], NULL); 188 | options->eye.y = strtof(argv[++i], NULL); 189 | options->eye.z = strtof(argv[++i], NULL); 190 | } else if (!strcmp(argv[i], "-d") || !strcmp(argv[i], "--dir")) { 191 | if (!check_arg(i, 3, argc, argv)) 192 | return false; 193 | options->dir.x = strtof(argv[++i], NULL); 194 | options->dir.y = strtof(argv[++i], NULL); 195 | options->dir.z = strtof(argv[++i], NULL); 196 | } else if (!strcmp(argv[i], "-u") || !strcmp(argv[i], "--up")) { 197 | if (!check_arg(i, 3, argc, argv)) 198 | return false; 199 | options->up.x = strtof(argv[++i], NULL); 200 | options->up.y = strtof(argv[++i], NULL); 201 | options->up.z = strtof(argv[++i], NULL); 202 | } else if (!strcmp(argv[i], "-w") || !strcmp(argv[i], "--width")) { 203 | if (!check_arg(i, 1, argc, argv)) 204 | return false; 205 | options->width = strtoul(argv[++i], NULL, 10); 206 | } else if (!strcmp(argv[i], "-h") || !strcmp(argv[i], "--height")) { 207 | if (!check_arg(i, 1, argc, argv)) 208 | return false; 209 | options->height = strtoul(argv[++i], NULL, 10); 210 | } else if (!strcmp(argv[i], "-o")) { 211 | if (!check_arg(i, 1, argc, argv)) 212 | return false; 213 | options->output_image = argv[++i]; 214 | } else if (!strcmp(argv[i], "-h") || !strcmp(argv[i], "--help")) { 215 | usage(); 216 | return false; 217 | } 218 | } else { 219 | if (options->input_scene) { 220 | fprintf(stderr, "Input scene file specified twice\n"); 221 | return false; 222 | } 223 | options->input_scene = argv[i]; 224 | } 225 | } 226 | if (!options->input_scene) { 227 | fprintf(stderr, "Missing input scene file\n"); 228 | return false; 229 | } 230 | return true; 231 | } 232 | 233 | static inline bool intersect_ray_tri(const struct bvh_ray3f* ray, const struct tri* tri, struct hit* hit) { 234 | const struct bvh_vec3f e1 = sub(tri->v[0], tri->v[1]); 235 | const struct bvh_vec3f e2 = sub(tri->v[2], tri->v[0]); 236 | const struct bvh_vec3f n = cross(e1, e2); 237 | const struct bvh_vec3f c = sub(tri->v[0], ray->org); 238 | const struct bvh_vec3f r = cross(ray->dir, c); 239 | 240 | const float inv_det = 1.f / dot(n, ray->dir); 241 | 242 | const float u = dot(r, e2) * inv_det; 243 | const float v = dot(r, e1) * inv_det; 244 | const float w = 1.f - u - v; 245 | 246 | static const float tolerance = -FLT_EPSILON; 247 | if (u >= tolerance && v >= tolerance && w >= tolerance) { 248 | const float t = dot(n, c) * inv_det; 249 | if (t >= ray->tmin && t <= hit->t) { 250 | hit->t = t; 251 | hit->u = u; 252 | hit->v = v; 253 | return true; 254 | } 255 | } 256 | return false; 257 | } 258 | 259 | static bool intersect_bvh_leaf(void* user_data, float* t, size_t begin, size_t end) { 260 | const struct bvh_ray3f* ray = ((struct intersect_user_data*)user_data)->ray; 261 | const struct scene* scene = ((struct intersect_user_data*)user_data)->scene; 262 | struct hit* hit = ((struct intersect_user_data*)user_data)->hit; 263 | bool was_hit = false; 264 | for (size_t i = begin; i < end; ++i) { 265 | const uint32_t tri_id = bvh3f_get_prim_id(scene->bvh, i); 266 | if (intersect_ray_tri(ray, &scene->tris[tri_id], hit)) { 267 | *t = hit->t; 268 | hit->tri_id = tri_id; 269 | was_hit = true; 270 | } 271 | } 272 | return was_hit; 273 | } 274 | 275 | static inline void render_image( 276 | const struct scene* scene, 277 | const struct options* options, 278 | struct image* image) 279 | { 280 | const struct bvh_vec3f dir = normalize(options->dir); 281 | const struct bvh_vec3f right = normalize(cross(dir, options->up)); 282 | const struct bvh_vec3f up = cross(right, dir); 283 | 284 | struct intersect_user_data user_data = { .scene = scene }; 285 | const struct bvh_intersect_callbackf callback = { 286 | .user_data = &user_data, 287 | .user_fn = intersect_bvh_leaf 288 | }; 289 | 290 | size_t intr_count = 0; 291 | struct timespec ts0, ts1; 292 | timespec_get(&ts0, TIME_UTC); 293 | for (uint32_t y = 0; y < options->height; ++y) { 294 | for (uint32_t x = 0; x < options->width; ++x) { 295 | const float u = 2.f * ((float)x) / ((float)options->width) - 1.f; 296 | const float v = 2.f * ((float)y) / ((float)options->height) - 1.f; 297 | 298 | const struct bvh_ray3f ray = { 299 | .org = options->eye, 300 | .dir = { 301 | .x = options->dir.x + u * right.x + v * up.x, 302 | .y = options->dir.y + u * right.y + v * up.y, 303 | .z = options->dir.z + u * right.z + v * up.z 304 | }, 305 | .tmin = 0.f, 306 | .tmax = FLT_MAX 307 | }; 308 | 309 | struct hit hit = { .tri_id = INVALID_TRI_ID, .t = FLT_MAX }; 310 | 311 | user_data.ray = &ray; 312 | user_data.hit = &hit; 313 | bvh3f_intersect_ray(scene->bvh, &ray, &callback); 314 | 315 | uint8_t r = 0; 316 | uint8_t g = 0; 317 | uint8_t b = 0; 318 | if (hit.tri_id != INVALID_TRI_ID) { 319 | r = (hit.tri_id * 79) % 255 + 1; 320 | g = (hit.tri_id * 43) % 255 + 1; 321 | b = (hit.tri_id * 57) % 255 + 1; 322 | intr_count++; 323 | } 324 | set_pixel(image, x, y, r, g, b); 325 | } 326 | } 327 | timespec_get(&ts1, TIME_UTC); 328 | const uint64_t render_time = (ts1.tv_sec - ts0.tv_sec) * 1000 + (ts1.tv_nsec - ts0.tv_nsec) / 1000000; 329 | printf("%zu intersection(s) found in %"PRIu64"\n", intr_count, render_time); 330 | } 331 | 332 | int main(int argc, char** argv) { 333 | struct options options = { 334 | .eye = (struct bvh_vec3f) { 0, 0, 0 }, 335 | .dir = (struct bvh_vec3f) { 0, 0, 1 }, 336 | .up = (struct bvh_vec3f) { 0, 1, 0 }, 337 | .output_image = "render.ppm", 338 | .width = 1024, 339 | .height = 1024 340 | }; 341 | if (!parse_options(argc, argv, &options)) 342 | return 1; 343 | 344 | struct scene scene = { 0 }; 345 | scene.tris = load_obj(argv[1], &scene.tri_count); 346 | if (!scene.tris) { 347 | fprintf(stderr, "Invalid or empty OBJ file\n"); 348 | return 1; 349 | } 350 | printf("Loaded file with %zu triangle(s)\n", scene.tri_count); 351 | 352 | build_bvh(&scene); 353 | 354 | struct image image = alloc_image(options.width, options.height); 355 | render_image(&scene, &options, &image); 356 | if (!save_image(&image, options.output_image)) { 357 | fprintf(stderr, "Could not save rendered image to '%s'\n", options.output_image); 358 | return 1; 359 | } else { 360 | printf("Image saved as '%s'\n", options.output_image); 361 | } 362 | 363 | free_image(&image); 364 | destroy_scene(&scene); 365 | return 0; 366 | } 367 | -------------------------------------------------------------------------------- /src/bvh/v2/mini_tree_builder.h: -------------------------------------------------------------------------------- 1 | #ifndef BVH_V2_MINI_TREE_BUILDER_H 2 | #define BVH_V2_MINI_TREE_BUILDER_H 3 | 4 | #include "bvh/v2/sweep_sah_builder.h" 5 | #include "bvh/v2/binned_sah_builder.h" 6 | #include "bvh/v2/thread_pool.h" 7 | #include "bvh/v2/executor.h" 8 | 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | 16 | namespace bvh::v2 { 17 | 18 | /// Multi-threaded top-down builder that partitions primitives using a grid. Multiple instances 19 | /// of a single-threaded builder are run in parallel on that partition, generating many small 20 | /// trees. Finally, a top-level tree is built on these smaller trees to form the final BVH. 21 | /// This builder is inspired by 22 | /// "Rapid Bounding Volume Hierarchy Generation using Mini Trees", by P. Ganestam et al. 23 | template 24 | class MiniTreeBuilder { 25 | using Scalar = typename Node::Scalar; 26 | using Vec = bvh::v2::Vec; 27 | using BBox = bvh::v2::BBox; 28 | 29 | public: 30 | struct Config : TopDownSahBuilder::Config { 31 | /// Flag that turns on/off mini-tree pruning. 32 | bool enable_pruning = true; 33 | 34 | /// Threshold on the area of a mini-tree node above which it is pruned, expressed in 35 | /// fraction of the area of bounding box around the entire set of primitives. 36 | Scalar pruning_area_ratio = static_cast(0.01); 37 | 38 | /// Minimum number of primitives per parallel task. 39 | size_t parallel_threshold = 1024; 40 | 41 | /// Log of the dimension of the grid used to split the workload horizontally. 42 | size_t log2_grid_dim = 4; 43 | }; 44 | 45 | /// Starts building a BVH with the given primitive data. The build algorithm is multi-threaded, 46 | /// and runs on the given thread pool. 47 | [[nodiscard]] BVH_ALWAYS_INLINE static Bvh build( 48 | ThreadPool& thread_pool, 49 | std::span bboxes, 50 | std::span centers, 51 | const Config& config = {}) 52 | { 53 | MiniTreeBuilder builder(thread_pool, bboxes, centers, config); 54 | auto mini_trees = builder.build_mini_trees(); 55 | if (config.enable_pruning) 56 | mini_trees = builder.prune_mini_trees(std::move(mini_trees)); 57 | return builder.build_top_bvh(mini_trees); 58 | } 59 | 60 | private: 61 | friend struct BuildTask; 62 | 63 | struct Bin { 64 | std::vector ids; 65 | 66 | BVH_ALWAYS_INLINE void add(size_t id) { ids.push_back(id); } 67 | 68 | BVH_ALWAYS_INLINE void merge(Bin&& other) { 69 | if (ids.empty()) 70 | ids = std::move(other.ids); 71 | else { 72 | ids.insert(ids.end(), other.ids.begin(), other.ids.end()); 73 | other.ids.clear(); 74 | } 75 | } 76 | }; 77 | 78 | struct LocalBins { 79 | std::vector bins; 80 | 81 | BVH_ALWAYS_INLINE Bin& operator [] (size_t i) { return bins[i]; } 82 | BVH_ALWAYS_INLINE const Bin& operator [] (size_t i) const { return bins[i]; } 83 | 84 | BVH_ALWAYS_INLINE void merge_small_bins(size_t threshold) { 85 | for (size_t i = 0; i < bins.size();) { 86 | size_t j = i + 1; 87 | for (; j < bins.size() && bins[j].ids.size() + bins[i].ids.size() <= threshold; ++j) 88 | bins[i].merge(std::move(bins[j])); 89 | i = j; 90 | } 91 | } 92 | 93 | BVH_ALWAYS_INLINE void remove_empty_bins() { 94 | bins.resize(std::remove_if(bins.begin(), bins.end(), 95 | [] (const Bin& bin) { return bin.ids.empty(); }) - bins.begin()); 96 | } 97 | 98 | BVH_ALWAYS_INLINE void merge(LocalBins&& other) { 99 | bins.resize(std::max(bins.size(), other.bins.size())); 100 | for (size_t i = 0, n = std::min(bins.size(), other.bins.size()); i < n; ++i) 101 | bins[i].merge(std::move(other[i])); 102 | } 103 | }; 104 | 105 | struct BuildTask { 106 | MiniTreeBuilder* builder; 107 | Bvh& bvh; 108 | std::vector prim_ids; 109 | 110 | std::vector bboxes; 111 | std::vector centers; 112 | 113 | BuildTask( 114 | MiniTreeBuilder* builder, 115 | Bvh& bvh, 116 | std::vector&& prim_ids) 117 | : builder(builder) 118 | , bvh(bvh) 119 | , prim_ids(std::move(prim_ids)) 120 | {} 121 | 122 | BVH_ALWAYS_INLINE void run() { 123 | // Make sure that rebuilds produce the same BVH 124 | std::sort(prim_ids.begin(), prim_ids.end()); 125 | 126 | // Extract bounding boxes and centers for this set of primitives 127 | bboxes.resize(prim_ids.size()); 128 | centers.resize(prim_ids.size()); 129 | for (size_t i = 0; i < prim_ids.size(); ++i) { 130 | bboxes[i] = builder->bboxes_[prim_ids[i]]; 131 | centers[i] = builder->centers_[prim_ids[i]]; 132 | } 133 | 134 | bvh = BinnedSahBuilder::build(bboxes, centers, builder->config_); 135 | 136 | // Permute primitive indices so that they index the proper set of primitives 137 | for (size_t i = 0; i < bvh.prim_ids.size(); ++i) 138 | bvh.prim_ids[i] = prim_ids[bvh.prim_ids[i]]; 139 | } 140 | }; 141 | 142 | ParallelExecutor executor_; 143 | std::span bboxes_; 144 | std::span centers_; 145 | const Config& config_; 146 | 147 | BVH_ALWAYS_INLINE MiniTreeBuilder( 148 | ThreadPool& thread_pool, 149 | std::span bboxes, 150 | std::span centers, 151 | const Config& config) 152 | : executor_(thread_pool) 153 | , bboxes_(bboxes) 154 | , centers_(centers) 155 | , config_(config) 156 | { 157 | assert(bboxes.size() == centers.size()); 158 | } 159 | 160 | std::vector> build_mini_trees() { 161 | // Compute the bounding box of all centers 162 | auto center_bbox = executor_.reduce(0, bboxes_.size(), BBox::make_empty(), 163 | [this] (BBox& bbox, size_t begin, size_t end) { 164 | for (size_t i = begin; i < end; ++i) 165 | bbox.extend(centers_[i]); 166 | }, 167 | [] (BBox& bbox, const BBox& other) { bbox.extend(other); }); 168 | 169 | assert(config_.log2_grid_dim <= std::numeric_limits::digits / Node::dimension); 170 | auto bin_count = size_t{1} << (config_.log2_grid_dim * Node::dimension); 171 | auto grid_dim = size_t{1} << config_.log2_grid_dim; 172 | auto grid_scale = Vec(static_cast(grid_dim)) * safe_inverse(center_bbox.get_diagonal()); 173 | auto grid_offset = -center_bbox.min * grid_scale; 174 | 175 | // Place primitives in bins 176 | auto final_bins = executor_.reduce(0, bboxes_.size(), LocalBins {}, 177 | [&] (LocalBins& local_bins, size_t begin, size_t end) { 178 | local_bins.bins.resize(bin_count); 179 | for (size_t i = begin; i < end; ++i) { 180 | auto p = robust_max(fast_mul_add(centers_[i], grid_scale, grid_offset), Vec(0)); 181 | auto x = std::min(grid_dim - 1, static_cast(p[0])); 182 | auto y = std::min(grid_dim - 1, static_cast(p[1])); 183 | auto z = std::min(grid_dim - 1, static_cast(p[2])); 184 | local_bins[morton_encode(x, y, z) & (bin_count - 1)].add(i); 185 | } 186 | }, 187 | [&] (LocalBins& result, LocalBins&& other) { result.merge(std::move(other)); }); 188 | 189 | // Note: Merging small bins will deteriorate the quality of the top BVH if there is no 190 | // pruning, since it will then produce larger mini-trees. For this reason, it is only enabled 191 | // when mini-tree pruning is enabled. 192 | if (config_.enable_pruning) 193 | final_bins.merge_small_bins(config_.parallel_threshold); 194 | final_bins.remove_empty_bins(); 195 | 196 | // Iterate over bins to collect groups of primitives and build BVHs over them in parallel 197 | std::vector> mini_trees(final_bins.bins.size()); 198 | for (size_t i = 0; i < final_bins.bins.size(); ++i) { 199 | auto task = new BuildTask(this, mini_trees[i], std::move(final_bins[i].ids)); 200 | executor_.thread_pool.push([task] (size_t) { task->run(); delete task; }); 201 | } 202 | executor_.thread_pool.wait(); 203 | 204 | return mini_trees; 205 | } 206 | 207 | std::vector> prune_mini_trees(std::vector>&& mini_trees) { 208 | // Compute the area threshold based on the area of the entire set of primitives 209 | auto avg_area = static_cast(0.); 210 | for (auto& mini_tree : mini_trees) 211 | avg_area += mini_tree.get_root().get_bbox().get_half_area(); 212 | avg_area /= static_cast(mini_trees.size()); 213 | auto threshold = avg_area * config_.pruning_area_ratio; 214 | 215 | // Cull nodes whose area is above the threshold 216 | std::stack stack; 217 | std::vector> pruned_roots; 218 | for (size_t i = 0; i < mini_trees.size(); ++i) { 219 | stack.push(0); 220 | auto& mini_tree = mini_trees[i]; 221 | while (!stack.empty()) { 222 | auto node_id = stack.top(); 223 | auto& node = mini_tree.nodes[node_id]; 224 | stack.pop(); 225 | if (node.get_bbox().get_half_area() < threshold || node.is_leaf()) { 226 | pruned_roots.emplace_back(i, node_id); 227 | } else { 228 | stack.push(node.index.first_id()); 229 | stack.push(node.index.first_id() + 1); 230 | } 231 | } 232 | } 233 | 234 | // Extract the BVHs rooted at the previously computed indices 235 | std::vector> pruned_trees(pruned_roots.size()); 236 | executor_.for_each(0, pruned_roots.size(), 237 | [&] (size_t begin, size_t end) { 238 | for (size_t i = begin; i < end; ++i) { 239 | if (pruned_roots[i].second == 0) 240 | pruned_trees[i] = std::move(mini_trees[pruned_roots[i].first]); 241 | else 242 | pruned_trees[i] = mini_trees[pruned_roots[i].first] 243 | .extract_bvh(pruned_roots[i].second); 244 | } 245 | }); 246 | return pruned_trees; 247 | } 248 | 249 | Bvh build_top_bvh(std::vector>& mini_trees) { 250 | // Build a BVH using the mini trees as leaves 251 | std::vector centers(mini_trees.size()); 252 | std::vector bboxes(mini_trees.size()); 253 | for (size_t i = 0; i < mini_trees.size(); ++i) { 254 | bboxes[i] = mini_trees[i].get_root().get_bbox(); 255 | centers[i] = bboxes[i].get_center(); 256 | } 257 | 258 | typename SweepSahBuilder::Config config = config_; 259 | config.max_leaf_size = config.min_leaf_size = 1; // Needs to have only one mini-tree in each leaf 260 | auto bvh = SweepSahBuilder::build(bboxes, centers, config); 261 | 262 | // Compute the offsets to apply to primitive and node indices 263 | std::vector node_offsets(mini_trees.size()); 264 | std::vector prim_offsets(mini_trees.size()); 265 | size_t node_count = bvh.nodes.size(); 266 | size_t prim_count = 0; 267 | for (size_t i = 0; i < mini_trees.size(); ++i) { 268 | node_offsets[i] = node_count - 1; // Skip root node 269 | prim_offsets[i] = prim_count; 270 | node_count += mini_trees[i].nodes.size() - 1; // idem 271 | prim_count += mini_trees[i].prim_ids.size(); 272 | } 273 | 274 | // Helper function to copy and fix the child/primitive index of a node 275 | auto copy_node = [&] (size_t i, Node& dst_node, const Node& src_node) { 276 | dst_node = src_node; 277 | dst_node.index.set_first_id(dst_node.index.first_id() + 278 | (src_node.is_leaf() ? prim_offsets[i] : node_offsets[i])); 279 | }; 280 | 281 | // Make the leaves of the top BVH point to the right internal nodes 282 | for (auto& node : bvh.nodes) { 283 | if (!node.is_leaf()) 284 | continue; 285 | assert(node.index.prim_count() == 1); 286 | size_t tree_id = bvh.prim_ids[node.index.first_id()]; 287 | copy_node(tree_id, node, mini_trees[tree_id].get_root()); 288 | } 289 | 290 | bvh.nodes.resize(node_count); 291 | bvh.prim_ids.resize(prim_count); 292 | executor_.for_each(0, mini_trees.size(), 293 | [&] (size_t begin, size_t end) { 294 | for (size_t i = begin; i < end; ++i) { 295 | auto& mini_tree = mini_trees[i]; 296 | 297 | // Copy the nodes of the mini tree with the offsets applied, without copying 298 | // the root node (since it is already copied to the top-level part of the BVH). 299 | for (size_t j = 1; j < mini_tree.nodes.size(); ++j) 300 | copy_node(i, bvh.nodes[node_offsets[i] + j], mini_tree.nodes[j]); 301 | 302 | std::copy( 303 | mini_tree.prim_ids.begin(), 304 | mini_tree.prim_ids.end(), 305 | bvh.prim_ids.begin() + prim_offsets[i]); 306 | } 307 | }); 308 | 309 | return bvh; 310 | } 311 | }; 312 | 313 | } // namespace bvh::v2 314 | 315 | #endif 316 | -------------------------------------------------------------------------------- /src/bvh/v2/c_api/bvh.h: -------------------------------------------------------------------------------- 1 | #ifndef BVH_V2_C_API_BVH_H 2 | #define BVH_V2_C_API_BVH_H 3 | 4 | // C API to the BVH library, providing high-level access to BVH construction and traversal. This API 5 | // may have an overhead compared to using the C++ API directly, as it may have to translate types 6 | // between the C interface and the C++ one, and because callbacks are not going to be inlined at the 7 | // API boundary. To mitigate that problem, specialized functions are provided which provide faster 8 | // operation for a small set of situations. 9 | 10 | #include 11 | #include 12 | #include 13 | 14 | #ifdef __cplusplus 15 | extern "C" { 16 | #endif 17 | 18 | #ifdef _MSC_VER 19 | #define BVH_EXPORT __declspec(dllexport) 20 | #define BVH_IMPORT __declspec(dllimport) 21 | #else 22 | #define BVH_EXPORT __attribute__((visibility("default"))) 23 | #define BVH_IMPORT BVH_EXPORT 24 | #endif 25 | 26 | #ifdef BVH_BUILD_API 27 | #define BVH_API BVH_EXPORT 28 | #else 29 | #define BVH_API BVH_IMPORT 30 | #endif 31 | 32 | #define BVH_ROOT_INDEX 0 33 | #define BVH_INVALID_PRIM_ID SIZE_MAX 34 | 35 | struct bvh2f; 36 | struct bvh3f; 37 | struct bvh_node2f; 38 | struct bvh_node3f; 39 | 40 | struct bvh2d; 41 | struct bvh3d; 42 | struct bvh_node2d; 43 | struct bvh_node3d; 44 | 45 | struct bvh_thread_pool; 46 | 47 | enum bvh_build_quality { 48 | BVH_BUILD_QUALITY_LOW, 49 | BVH_BUILD_QUALITY_MEDIUM, 50 | BVH_BUILD_QUALITY_HIGH 51 | }; 52 | 53 | struct bvh_build_config { 54 | enum bvh_build_quality quality; 55 | size_t min_leaf_size; 56 | size_t max_leaf_size; 57 | size_t parallel_threshold; 58 | }; 59 | 60 | struct bvh_vec2f { float x, y; }; 61 | struct bvh_vec3f { float x, y, z; }; 62 | struct bvh_vec2d { double x, y; }; 63 | struct bvh_vec3d { double x, y, z; }; 64 | 65 | struct bvh_bbox2f { struct bvh_vec2f min, max; }; 66 | struct bvh_bbox3f { struct bvh_vec3f min, max; }; 67 | struct bvh_bbox2d { struct bvh_vec2d min, max; }; 68 | struct bvh_bbox3d { struct bvh_vec3d min, max; }; 69 | 70 | struct bvh_ray2f { struct bvh_vec2f org, dir; float tmin, tmax; }; 71 | struct bvh_ray3f { struct bvh_vec3f org, dir; float tmin, tmax; }; 72 | struct bvh_ray2d { struct bvh_vec2d org, dir; double tmin, tmax; }; 73 | struct bvh_ray3d { struct bvh_vec3d org, dir; double tmin, tmax; }; 74 | 75 | struct bvh_intersect_callbackf { 76 | void* user_data; 77 | bool (*user_fn)(void*, float*, size_t begin, size_t end); 78 | }; 79 | 80 | struct bvh_intersect_callbackd { 81 | void* user_data; 82 | bool (*user_fn)(void*, double*, size_t begin, size_t end); 83 | }; 84 | 85 | // Thread Pool ------------------------------------------------------------------------------------ 86 | 87 | // A thread count of zero instructs the thread pool to detect the number of threads available on the 88 | // machine. 89 | 90 | BVH_API struct bvh_thread_pool* bvh_thread_pool_create(size_t thread_count); 91 | BVH_API void bvh_thread_pool_destroy(struct bvh_thread_pool*); 92 | 93 | // BVH Construction ------------------------------------------------------------------------------- 94 | 95 | // These construction functions can be called with a `NULL` thread pool, in which case the BVH is 96 | // constructed serially. The configuration can also be `NULL`, in which case the default 97 | // configuration is used. 98 | 99 | BVH_API struct bvh2f* bvh2f_build( 100 | struct bvh_thread_pool* thread_pool, 101 | const struct bvh_bbox2f* bboxes, 102 | const struct bvh_vec2f* centers, 103 | size_t prim_count, 104 | const struct bvh_build_config* config); 105 | 106 | BVH_API struct bvh3f* bvh3f_build( 107 | struct bvh_thread_pool* thread_pool, 108 | const struct bvh_bbox3f* bboxes, 109 | const struct bvh_vec3f* centers, 110 | size_t prim_count, 111 | const struct bvh_build_config* config); 112 | 113 | BVH_API struct bvh2d* bvh2d_build( 114 | struct bvh_thread_pool* thread_pool, 115 | const struct bvh_bbox2d* bboxes, 116 | const struct bvh_vec2d* centers, 117 | size_t prim_count, 118 | const struct bvh_build_config* config); 119 | 120 | BVH_API struct bvh3d* bvh3d_build( 121 | struct bvh_thread_pool* thread_pool, 122 | const struct bvh_bbox3d* bboxes, 123 | const struct bvh_vec3d* centers, 124 | size_t prim_count, 125 | const struct bvh_build_config* config); 126 | 127 | // BVH Destruction -------------------------------------------------------------------------------- 128 | 129 | BVH_API void bvh2f_destroy(struct bvh2f*); 130 | BVH_API void bvh3f_destroy(struct bvh3f*); 131 | BVH_API void bvh2d_destroy(struct bvh2d*); 132 | BVH_API void bvh3d_destroy(struct bvh3d*); 133 | 134 | // Serialization/Deserialization to Files --------------------------------------------------------- 135 | 136 | BVH_API void bvh2f_save(const struct bvh2f*, FILE*); 137 | BVH_API void bvh3f_save(const struct bvh3f*, FILE*); 138 | BVH_API void bvh2d_save(const struct bvh2d*, FILE*); 139 | BVH_API void bvh3d_save(const struct bvh3d*, FILE*); 140 | 141 | BVH_API struct bvh2f* bvh2f_load(FILE*); 142 | BVH_API struct bvh3f* bvh3f_load(FILE*); 143 | BVH_API struct bvh2d* bvh2d_load(FILE*); 144 | BVH_API struct bvh3d* bvh3d_load(FILE*); 145 | 146 | // Accessing Nodes and Primitive Indices ---------------------------------------------------------- 147 | 148 | BVH_API struct bvh_node2f* bvh2f_get_node(struct bvh2f*, size_t); 149 | BVH_API struct bvh_node3f* bvh3f_get_node(struct bvh3f*, size_t); 150 | BVH_API struct bvh_node2d* bvh2d_get_node(struct bvh2d*, size_t); 151 | BVH_API struct bvh_node3d* bvh3d_get_node(struct bvh3d*, size_t); 152 | 153 | BVH_API size_t bvh2f_get_prim_id(const struct bvh2f*, size_t); 154 | BVH_API size_t bvh3f_get_prim_id(const struct bvh3f*, size_t); 155 | BVH_API size_t bvh2d_get_prim_id(const struct bvh2d*, size_t); 156 | BVH_API size_t bvh3d_get_prim_id(const struct bvh3d*, size_t); 157 | 158 | BVH_API size_t bvh2f_get_prim_count(const struct bvh2f*); 159 | BVH_API size_t bvh3f_get_prim_count(const struct bvh3f*); 160 | BVH_API size_t bvh2d_get_prim_count(const struct bvh2d*); 161 | BVH_API size_t bvh3d_get_prim_count(const struct bvh3d*); 162 | 163 | BVH_API size_t bvh2f_get_node_count(const struct bvh2f*); 164 | BVH_API size_t bvh3f_get_node_count(const struct bvh3f*); 165 | BVH_API size_t bvh2d_get_node_count(const struct bvh2d*); 166 | BVH_API size_t bvh3d_get_node_count(const struct bvh3d*); 167 | 168 | // Accessing and Modifying Node properties -------------------------------------------------------- 169 | 170 | BVH_API bool bvh_node2f_is_leaf(const struct bvh_node2f*); 171 | BVH_API bool bvh_node3f_is_leaf(const struct bvh_node3f*); 172 | BVH_API bool bvh_node2d_is_leaf(const struct bvh_node2d*); 173 | BVH_API bool bvh_node3d_is_leaf(const struct bvh_node3d*); 174 | 175 | BVH_API size_t bvh_node2f_get_prim_count(const struct bvh_node2f*); 176 | BVH_API size_t bvh_node3f_get_prim_count(const struct bvh_node3f*); 177 | BVH_API size_t bvh_node2d_get_prim_count(const struct bvh_node2d*); 178 | BVH_API size_t bvh_node3d_get_prim_count(const struct bvh_node3d*); 179 | 180 | BVH_API void bvh_node2f_set_prim_count(struct bvh_node2f*, size_t); 181 | BVH_API void bvh_node3f_set_prim_count(struct bvh_node3f*, size_t); 182 | BVH_API void bvh_node2d_set_prim_count(struct bvh_node2d*, size_t); 183 | BVH_API void bvh_node3d_set_prim_count(struct bvh_node3d*, size_t); 184 | 185 | BVH_API size_t bvh_node2f_get_first_id(const struct bvh_node2f*); 186 | BVH_API size_t bvh_node3f_get_first_id(const struct bvh_node3f*); 187 | BVH_API size_t bvh_node2d_get_first_id(const struct bvh_node2d*); 188 | BVH_API size_t bvh_node3d_get_first_id(const struct bvh_node3d*); 189 | 190 | BVH_API void bvh_node2f_set_first_id(struct bvh_node2f*, size_t); 191 | BVH_API void bvh_node3f_set_first_id(struct bvh_node3f*, size_t); 192 | BVH_API void bvh_node2d_set_first_id(struct bvh_node2d*, size_t); 193 | BVH_API void bvh_node3d_set_first_id(struct bvh_node3d*, size_t); 194 | 195 | BVH_API struct bvh_bbox2f bvh_node2f_get_bbox(const struct bvh_node2f*); 196 | BVH_API struct bvh_bbox3f bvh_node3f_get_bbox(const struct bvh_node3f*); 197 | BVH_API struct bvh_bbox2d bvh_node2d_get_bbox(const struct bvh_node2d*); 198 | BVH_API struct bvh_bbox3d bvh_node3d_get_bbox(const struct bvh_node3d*); 199 | 200 | BVH_API void bvh_node2f_set_bbox(struct bvh_node2f*, const struct bvh_bbox2f*); 201 | BVH_API void bvh_node3f_set_bbox(struct bvh_node3f*, const struct bvh_bbox3f*); 202 | BVH_API void bvh_node2d_set_bbox(struct bvh_node2d*, const struct bvh_bbox2d*); 203 | BVH_API void bvh_node3d_set_bbox(struct bvh_node3d*, const struct bvh_bbox3d*); 204 | 205 | // Refitting and BVH Modification ----------------------------------------------------------------- 206 | 207 | // Refitting functions resize the bounding boxes of the BVH in a bottom-up fashion. This can be 208 | // combined with the optimization function below to allow updating the BVH in an incremental manner. 209 | // IMPORTANT: Appending a node to the BVH invalidates all the node pointers. 210 | 211 | BVH_API void bvh2f_append_node(struct bvh2f*); 212 | BVH_API void bvh3f_append_node(struct bvh3f*); 213 | BVH_API void bvh2d_append_node(struct bvh2d*); 214 | BVH_API void bvh3d_append_node(struct bvh3d*); 215 | 216 | BVH_API void bvh2f_remove_last_node(struct bvh2f*); 217 | BVH_API void bvh3f_remove_last_node(struct bvh3f*); 218 | BVH_API void bvh2d_remove_last_node(struct bvh2d*); 219 | BVH_API void bvh3d_remove_last_node(struct bvh3d*); 220 | 221 | BVH_API void bvh2f_refit(struct bvh2f*); 222 | BVH_API void bvh3f_refit(struct bvh3f*); 223 | BVH_API void bvh2d_refit(struct bvh2d*); 224 | BVH_API void bvh3d_refit(struct bvh3d*); 225 | 226 | BVH_API void bvh2f_optimize(struct bvh_thread_pool*, struct bvh2f*); 227 | BVH_API void bvh3f_optimize(struct bvh_thread_pool*, struct bvh3f*); 228 | BVH_API void bvh2d_optimize(struct bvh_thread_pool*, struct bvh2d*); 229 | BVH_API void bvh3d_optimize(struct bvh_thread_pool*, struct bvh3d*); 230 | 231 | // BVH Intersection ------------------------------------------------------------------------------- 232 | 233 | // Intersection routines: Intersects the BVH with a ray using a callback to intersect the primitives 234 | // contained in the leaves. 235 | // 236 | // The callback takes a pointer to user data, a pointer to the current distance along the ray, and a 237 | // range of primitives. It returns true when an intersection is found, in which case it writes the 238 | // intersection distance in the provided pointer, otherwise, if no intersection is found, it returns 239 | // false and leaves the intersection distance unchanged. The given range is expressed in terms of 240 | // *BVH primitives*, which means that it does not correspond to a range in the original set of 241 | // primitives. Instead, the user has two options: pre-permute the primitives according to the BVH 242 | // primitive indices, or perform an indirection everytime a primitive is accessed. 243 | // 244 | // Here is a basic example of an intersection callback: 245 | // 246 | // struct my_user_data { 247 | // struct bvh3f* bvh; 248 | // struct bvh_ray3f* ray; 249 | // struct my_prim* prims; 250 | // }; 251 | // 252 | // struct my_prim_hit { 253 | // float t; 254 | // }; 255 | // 256 | // // Declared & defined somewhere else. 257 | // bool intersect_prim(const struct my_prim*, const struct bvh_ray3f*, struct my_prim_hit*); 258 | // 259 | // bool my_user_fn(void* user_data, float* t, size_t begin, size_t end) { 260 | // struct my_user_data* my_user_data = (my_user_data)data; 261 | // bool was_hit = false; 262 | // for (size_t i = begin; i < end; ++i) { 263 | // const size_t prim_id = bvh3f_get_prim_id(my_user_data->bvh, i); 264 | // struct my_prim_hit hit; 265 | // if (intersect_prim(&my_user_data->prims[i], my_user_data->ray, &hit)) { 266 | // // Note: It is important to remember the maximum distance so that the BVH 267 | // // traversal routine can cull nodes that are too far away, and so that the primitive 268 | // // intersection routine can exit earlier when that is possible. 269 | // *t = my_user_data->ray->tmax = hit.t; 270 | // was_hit = true; 271 | // } 272 | // } 273 | // return was_hit; 274 | // } 275 | // 276 | 277 | BVH_API void bvh2f_intersect_ray_any(const struct bvh2f*, const struct bvh_ray2f*, const struct bvh_intersect_callbackf*); 278 | BVH_API void bvh3f_intersect_ray_any(const struct bvh3f*, const struct bvh_ray3f*, const struct bvh_intersect_callbackf*); 279 | BVH_API void bvh2d_intersect_ray_any(const struct bvh2d*, const struct bvh_ray2d*, const struct bvh_intersect_callbackd*); 280 | BVH_API void bvh3d_intersect_ray_any(const struct bvh3d*, const struct bvh_ray3d*, const struct bvh_intersect_callbackd*); 281 | 282 | BVH_API void bvh2f_intersect_ray_any_robust(const struct bvh2f*, const struct bvh_ray2f*, const struct bvh_intersect_callbackf*); 283 | BVH_API void bvh3f_intersect_ray_any_robust(const struct bvh3f*, const struct bvh_ray3f*, const struct bvh_intersect_callbackf*); 284 | BVH_API void bvh2d_intersect_ray_any_robust(const struct bvh2d*, const struct bvh_ray2d*, const struct bvh_intersect_callbackd*); 285 | BVH_API void bvh3d_intersect_ray_any_robust(const struct bvh3d*, const struct bvh_ray3d*, const struct bvh_intersect_callbackd*); 286 | 287 | BVH_API void bvh2f_intersect_ray(const struct bvh2f*, const struct bvh_ray2f*, const struct bvh_intersect_callbackf*); 288 | BVH_API void bvh3f_intersect_ray(const struct bvh3f*, const struct bvh_ray3f*, const struct bvh_intersect_callbackf*); 289 | BVH_API void bvh2d_intersect_ray(const struct bvh2d*, const struct bvh_ray2d*, const struct bvh_intersect_callbackd*); 290 | BVH_API void bvh3d_intersect_ray(const struct bvh3d*, const struct bvh_ray3d*, const struct bvh_intersect_callbackd*); 291 | 292 | BVH_API void bvh2f_intersect_ray_robust(const struct bvh2f*, const struct bvh_ray2f*, const struct bvh_intersect_callbackf*); 293 | BVH_API void bvh3f_intersect_ray_robust(const struct bvh3f*, const struct bvh_ray3f*, const struct bvh_intersect_callbackf*); 294 | BVH_API void bvh2d_intersect_ray_robust(const struct bvh2d*, const struct bvh_ray2d*, const struct bvh_intersect_callbackd*); 295 | BVH_API void bvh3d_intersect_ray_robust(const struct bvh3d*, const struct bvh_ray3d*, const struct bvh_intersect_callbackd*); 296 | 297 | #ifdef __cplusplus 298 | } 299 | #endif 300 | 301 | #endif 302 | -------------------------------------------------------------------------------- /src/bvh/v2/c_api/bvh_impl.h: -------------------------------------------------------------------------------- 1 | #ifndef BVH_V2_C_API_BVH_IMPL_H 2 | #define BVH_V2_C_API_BVH_IMPL_H 3 | 4 | #include 5 | #include 6 | #include 7 | 8 | namespace bvh::v2::c_api { 9 | 10 | template 11 | struct BvhTypes {}; 12 | 13 | template 14 | struct BvhCallback {}; 15 | 16 | template <> struct BvhCallback { using Type = bvh_intersect_callbackf; }; 17 | template <> struct BvhCallback { using Type = bvh_intersect_callbackd; }; 18 | 19 | template 20 | static auto translate(enum bvh_build_quality quality) { 21 | switch (quality) { 22 | #ifndef BVH_C_API_UNSAFE_CASTS 23 | case BVH_BUILD_QUALITY_LOW: 24 | return bvh::v2::DefaultBuilder>::Quality::Low; 25 | case BVH_BUILD_QUALITY_MEDIUM: 26 | return bvh::v2::DefaultBuilder>::Quality::Medium; 27 | case BVH_BUILD_QUALITY_HIGH: 28 | return bvh::v2::DefaultBuilder>::Quality::High; 29 | #endif 30 | default: 31 | return static_cast>::Quality>(quality); 32 | } 33 | } 34 | 35 | template 36 | static auto translate(const bvh_build_config* config) { 37 | auto translated_config = typename bvh::v2::DefaultBuilder>::Config {}; 38 | if (config) { 39 | translated_config.quality = translate(config->quality); 40 | translated_config.min_leaf_size = config->min_leaf_size; 41 | translated_config.max_leaf_size = config->max_leaf_size; 42 | translated_config.parallel_threshold = config->parallel_threshold; 43 | } 44 | return translated_config; 45 | } 46 | 47 | template 48 | static auto translate(const typename BvhTypes::Vec& vec) { 49 | static_assert(Dim == 2 || Dim == 3); 50 | if constexpr (Dim == 2) { 51 | return bvh::v2::Vec { vec.x, vec.y }; 52 | } else { 53 | return bvh::v2::Vec { vec.x, vec.y, vec.z }; 54 | } 55 | } 56 | 57 | template 58 | static auto translate(const bvh::v2::Vec& vec) { 59 | static_assert(Dim == 2 || Dim == 3); 60 | if constexpr (Dim == 2) { 61 | return typename BvhTypes::Vec { vec[0], vec[1] }; 62 | } else { 63 | return typename BvhTypes::Vec { vec[0], vec[1], vec[2] }; 64 | } 65 | } 66 | 67 | template 68 | static auto translate(const typename BvhTypes::BBox& bbox) { 69 | return bvh::v2::BBox { translate(bbox.min), translate(bbox.max) }; 70 | } 71 | 72 | template 73 | static auto translate(const bvh::v2::BBox& bbox) { 74 | return typename BvhTypes::BBox { translate(bbox.min), translate(bbox.max) }; 75 | } 76 | 77 | template 78 | static auto translate(const typename BvhTypes::Ray& ray) { 79 | return bvh::v2::Ray { translate(ray.org), translate(ray.dir), ray.tmin, ray.tmax }; 80 | } 81 | 82 | template 83 | static typename BvhTypes::Bvh* bvh_build( 84 | bvh_thread_pool* thread_pool, 85 | const typename BvhTypes::BBox* bboxes, 86 | const typename BvhTypes::Vec* centers, 87 | size_t prim_count, 88 | const bvh_build_config* config) 89 | { 90 | bvh::v2::BBox* translated_bboxes = nullptr; 91 | bvh::v2::Vec* translated_centers = nullptr; 92 | #ifdef BVH_C_API_UNSAFE_CASTS 93 | translated_bboxes = reinterpret_cast(bboxes); 94 | translated_centers = reinterpret_cast(centers); 95 | #else 96 | std::vector> bbox_buf(prim_count); 97 | std::vector> center_buf(prim_count); 98 | for (size_t i = 0; i < prim_count; ++i) { 99 | bbox_buf[i] = translate(bboxes[i]); 100 | center_buf[i] = translate(centers[i]); 101 | } 102 | translated_bboxes = bbox_buf.data(); 103 | translated_centers = center_buf.data(); 104 | #endif 105 | auto bvh = thread_pool 106 | ? bvh::v2::DefaultBuilder>::build( 107 | *reinterpret_cast(thread_pool), 108 | std::span { translated_bboxes, translated_bboxes + prim_count }, 109 | std::span { translated_centers, translated_centers + prim_count }, 110 | translate(config)) 111 | : bvh::v2::DefaultBuilder>::build( 112 | std::span { translated_bboxes, translated_bboxes + prim_count }, 113 | std::span { translated_centers, translated_centers + prim_count }, 114 | translate(config)); 115 | return reinterpret_cast::Bvh*>(new decltype(bvh) { std::move(bvh) }); 116 | } 117 | 118 | template 119 | static void bvh_save(const typename BvhTypes::Bvh* bvh, FILE* file) { 120 | struct FileOutputStream : bvh::v2::OutputStream { 121 | FILE* file; 122 | FileOutputStream(FILE* file) 123 | : file(file) 124 | {} 125 | bool write_raw(const void* data, size_t size) override { 126 | return fwrite(data, 1, size, file) == size; 127 | } 128 | }; 129 | FileOutputStream stream { file }; 130 | reinterpret_cast>*>(bvh)->serialize(stream); 131 | } 132 | 133 | template 134 | static typename BvhTypes::Bvh* bvh_load(FILE* file) { 135 | struct FileInputStream : bvh::v2::InputStream { 136 | FILE* file; 137 | FileInputStream(FILE* file) 138 | : file(file) 139 | {} 140 | size_t read_raw(void* data, size_t size) override { 141 | return fread(data, 1, size, file); 142 | } 143 | }; 144 | FileInputStream stream { file }; 145 | return reinterpret_cast::Bvh*>( 146 | new bvh::v2::Bvh>(bvh::v2::Bvh>::deserialize(stream))); 147 | } 148 | 149 | template 150 | static typename BvhTypes::Node* bvh_get_node(typename BvhTypes::Bvh* bvh, size_t node_id) { 151 | auto& nodes = reinterpret_cast>*>(bvh)->nodes; 152 | assert(node_id < nodes.size()); 153 | return reinterpret_cast::Node*>(&nodes[node_id]); 154 | } 155 | 156 | template 157 | static size_t bvh_get_prim_id(const typename BvhTypes::Bvh* bvh, size_t i) { 158 | auto& prim_ids = reinterpret_cast>*>(bvh)->prim_ids; 159 | assert(i < prim_ids.size()); 160 | return prim_ids[i]; 161 | } 162 | 163 | template 164 | static size_t bvh_get_prim_count(const typename BvhTypes::Bvh* bvh) { 165 | return reinterpret_cast>*>(bvh)->prim_ids.size(); 166 | } 167 | 168 | template 169 | static size_t bvh_get_node_count(const typename BvhTypes::Bvh* bvh) { 170 | return reinterpret_cast>*>(bvh)->nodes.size(); 171 | } 172 | 173 | template 174 | static bool bvh_node_is_leaf(const typename BvhTypes::Node* node) { 175 | return reinterpret_cast*>(node)->is_leaf(); 176 | } 177 | 178 | template 179 | static size_t bvh_node_get_prim_count(const typename BvhTypes::Node* node) { 180 | return reinterpret_cast*>(node)->index.prim_count(); 181 | } 182 | 183 | template 184 | static void bvh_node_set_prim_count(typename BvhTypes::Node* node, size_t prim_count) { 185 | return reinterpret_cast*>(node)->index.set_prim_count(prim_count); 186 | } 187 | 188 | template 189 | static size_t bvh_node_get_first_id(const typename BvhTypes::Node* node) { 190 | return reinterpret_cast*>(node)->index.first_id(); 191 | } 192 | 193 | template 194 | static void bvh_node_set_first_id(typename BvhTypes::Node* node, size_t first_id) { 195 | reinterpret_cast*>(node)->index.set_first_id(first_id); 196 | } 197 | 198 | template 199 | static typename BvhTypes::BBox bvh_node_get_bbox(const typename BvhTypes::Node* node) { 200 | return translate(reinterpret_cast*>(node)->get_bbox()); 201 | } 202 | 203 | template 204 | static void bvh_node_set_bbox(typename BvhTypes::Node* node, const typename BvhTypes::BBox* bbox) { 205 | reinterpret_cast*>(node)->set_bbox(translate(*bbox)); 206 | } 207 | 208 | template 209 | static void bvh_append_node(typename BvhTypes::Bvh* bvh) { 210 | reinterpret_cast>*>(bvh)->nodes.emplace_back(); 211 | } 212 | 213 | template 214 | static void bvh_remove_last_node(typename BvhTypes::Bvh* bvh) { 215 | reinterpret_cast>*>(bvh)->nodes.pop_back(); 216 | } 217 | 218 | template 219 | static void bvh_refit(typename BvhTypes::Bvh* bvh) { 220 | reinterpret_cast>*>(bvh)->refit(); 221 | } 222 | 223 | template 224 | static void bvh_optimize(bvh_thread_pool* thread_pool, typename BvhTypes::Bvh* bvh) { 225 | if (thread_pool) { 226 | bvh::v2::ReinsertionOptimizer>::optimize( 227 | *reinterpret_cast(thread_pool), 228 | *reinterpret_cast>*>(bvh)); 229 | } else { 230 | bvh::v2::ReinsertionOptimizer>::optimize( 231 | *reinterpret_cast>*>(bvh)); 232 | } 233 | } 234 | 235 | template 236 | static void bvh_intersect_ray( 237 | const typename BvhTypes::Bvh* bvh, 238 | const typename BvhTypes::Ray* ray, 239 | const typename BvhCallback::Type* callback) 240 | { 241 | static constexpr size_t stack_size = 64; 242 | auto translated_ray = translate(*ray); 243 | auto& translated_bvh = *reinterpret_cast>*>(bvh); 244 | bvh::v2::SmallStack::Index, stack_size> stack; 245 | translated_bvh.template intersect( 246 | translated_ray, translated_bvh.get_root().index, stack, 247 | [&] (size_t begin, size_t end) { 248 | return callback->user_fn(callback->user_data, &translated_ray.tmax, begin, end); 249 | }); 250 | } 251 | 252 | #define BVH_TYPES(T, Dim, suffix) \ 253 | template <> \ 254 | struct BvhTypes { \ 255 | using Bvh = bvh##suffix; \ 256 | using Vec = bvh_vec##suffix; \ 257 | using BBox = bvh_bbox##suffix; \ 258 | using Node = bvh_node##suffix; \ 259 | using Ray = bvh_ray##suffix; \ 260 | }; 261 | 262 | #define BVH_IMPL(T, Dim, suffix) \ 263 | BVH_EXPORT typename BvhTypes::Bvh* bvh##suffix##_build( \ 264 | bvh_thread_pool* thread_pool, \ 265 | const typename BvhTypes::BBox* bboxes, \ 266 | const typename BvhTypes::Vec* centers, \ 267 | size_t prim_count, \ 268 | const bvh_build_config* config) \ 269 | { \ 270 | return bvh_build(thread_pool, bboxes, centers, prim_count, config); \ 271 | } \ 272 | BVH_EXPORT void bvh##suffix##_destroy(typename BvhTypes::Bvh* bvh) { \ 273 | delete reinterpret_cast>*>(bvh); \ 274 | } \ 275 | BVH_EXPORT void bvh##suffix##_save(const typename BvhTypes::Bvh* bvh, FILE* file) { \ 276 | bvh_save(bvh, file); \ 277 | } \ 278 | BVH_EXPORT typename BvhTypes::Bvh* bvh##suffix##_load(FILE* file) { \ 279 | return bvh_load(file); \ 280 | } \ 281 | BVH_EXPORT typename BvhTypes::Node* bvh##suffix##_get_node(typename BvhTypes::Bvh* bvh, size_t node_id) { \ 282 | return bvh_get_node(bvh, node_id); \ 283 | } \ 284 | BVH_EXPORT size_t bvh##suffix##_get_prim_id(const typename BvhTypes::Bvh* bvh, size_t i) { \ 285 | return bvh_get_prim_id(bvh, i); \ 286 | } \ 287 | BVH_EXPORT size_t bvh##suffix##_get_prim_count(const typename BvhTypes::Bvh* bvh) { \ 288 | return bvh_get_prim_count(bvh); \ 289 | } \ 290 | BVH_EXPORT size_t bvh##suffix##_get_node_count(const typename BvhTypes::Bvh* bvh) { \ 291 | return bvh_get_node_count(bvh); \ 292 | } \ 293 | BVH_EXPORT bool bvh_node##suffix##_is_leaf(const typename BvhTypes::Node* node) { \ 294 | return bvh_node_is_leaf(node); \ 295 | } \ 296 | BVH_EXPORT size_t bvh_node##suffix##_get_prim_count(const typename BvhTypes::Node* node) { \ 297 | return bvh_node_get_prim_count(node); \ 298 | } \ 299 | BVH_EXPORT void bvh_node##suffix##_set_prim_count(typename BvhTypes::Node* node, size_t prim_count) { \ 300 | bvh_node_set_prim_count(node, prim_count); \ 301 | } \ 302 | BVH_EXPORT size_t bvh_node##suffix##_get_first_id(const typename BvhTypes::Node* node) { \ 303 | return bvh_node_get_first_id(node); \ 304 | } \ 305 | BVH_EXPORT void bvh_node##suffix##_set_first_id(typename BvhTypes::Node* node, size_t first_id) { \ 306 | bvh_node_set_first_id(node, first_id); \ 307 | } \ 308 | BVH_EXPORT typename BvhTypes::BBox bvh_node##suffix##_get_bbox(const typename BvhTypes::Node* node) { \ 309 | return bvh_node_get_bbox(node); \ 310 | } \ 311 | BVH_EXPORT void bvh_node##suffix##_set_bbox(typename BvhTypes::Node* node, const typename BvhTypes::BBox* bbox) { \ 312 | bvh_node_set_bbox(node, bbox); \ 313 | } \ 314 | BVH_EXPORT void bvh##suffix##_append_node(typename BvhTypes::Bvh* bvh) { \ 315 | bvh_append_node(bvh); \ 316 | } \ 317 | BVH_EXPORT void bvh##suffix##_remove_last_node(typename BvhTypes::Bvh* bvh) { \ 318 | bvh_remove_last_node(bvh); \ 319 | } \ 320 | BVH_EXPORT void bvh##suffix##_refit(typename BvhTypes::Bvh* bvh) { \ 321 | bvh_refit(bvh); \ 322 | } \ 323 | BVH_EXPORT void bvh##suffix##_optimize(bvh_thread_pool* thread_pool, typename BvhTypes::Bvh* bvh) { \ 324 | bvh_optimize(thread_pool, bvh); \ 325 | } \ 326 | BVH_EXPORT void bvh##suffix##_intersect_ray_any( \ 327 | const typename BvhTypes::Bvh* bvh, \ 328 | const typename BvhTypes::Ray* ray, \ 329 | const typename BvhCallback::Type* callback) \ 330 | { \ 331 | bvh_intersect_ray(bvh, ray, callback); \ 332 | } \ 333 | BVH_EXPORT void bvh##suffix##_intersect_ray_any_robust( \ 334 | const typename BvhTypes::Bvh* bvh, \ 335 | const typename BvhTypes::Ray* ray, \ 336 | const typename BvhCallback::Type* callback) \ 337 | { \ 338 | bvh_intersect_ray(bvh, ray, callback); \ 339 | } \ 340 | BVH_EXPORT void bvh##suffix##_intersect_ray( \ 341 | const typename BvhTypes::Bvh* bvh, \ 342 | const typename BvhTypes::Ray* ray, \ 343 | const typename BvhCallback::Type* callback) \ 344 | { \ 345 | bvh_intersect_ray(bvh, ray, callback); \ 346 | } \ 347 | BVH_EXPORT void bvh##suffix##_intersect_ray_robust( \ 348 | const typename BvhTypes::Bvh* bvh, \ 349 | const typename BvhTypes::Ray* ray, \ 350 | const typename BvhCallback::Type* callback) \ 351 | { \ 352 | bvh_intersect_ray(bvh, ray, callback); \ 353 | } 354 | 355 | } // namespace bvh::v2::c_api 356 | 357 | #endif 358 | -------------------------------------------------------------------------------- /test/benchmark.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | #include "load_obj.h" 12 | 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | #include 19 | #include 20 | #include 21 | 22 | using Scalar = float; 23 | using Vec3 = bvh::v2::Vec; 24 | using BBox = bvh::v2::BBox; 25 | using Tri = bvh::v2::Tri; 26 | using Node = bvh::v2::Node; 27 | using Bvh = bvh::v2::Bvh; 28 | using Ray = bvh::v2::Ray; 29 | 30 | template struct bvh::v2::Sphere; 31 | 32 | using PrecomputedTri = bvh::v2::PrecomputedTri; 33 | 34 | static constexpr size_t stack_size = 64; 35 | 36 | static void usage() { 37 | std::cout << 38 | "Usage: benchmark [options] file.obj\n" 39 | "\nOptions:\n" 40 | " --help Shows this message.\n" 41 | " -q --quality Sets the desired BVH quality (low, med, or high, defaults to high).\n" 42 | " -p --permute-primitives Activates the primitive permutation optimization (disabled by default).\n" 43 | " -i --build-iterations Sets the number of construction iterations (1 by default).\n" 44 | " --robust-traversal Use the robust traversal algorithm (disabled by default).\n" 45 | " -e --eye Sets the position of the camera.\n" 46 | " -d --dir Sets the direction of the camera.\n" 47 | " -u --up Sets the up vector of the camera.\n" 48 | " --fov Sets the field of view.\n" 49 | " -w --width Sets the image width.\n" 50 | " -h --height Sets the image height.\n" 51 | " -m --render-mode Sets the render mode (eyelight or debug, defaults to eyelight).\n" 52 | " -o Sets the output file name (defaults to 'render.ppm').\n" 53 | "\nRender-mode specific options:\n" 54 | " --debug-threshold \n\n" 55 | " Sets the maximum number of traversal and intersection steps used by\n" 56 | " the debug visualization (by default auto-detected).\n" 57 | << std::endl; 58 | } 59 | 60 | template 61 | auto profile(F&& f, size_t iter_count = 1) -> typename Clock::duration { 62 | std::vector durations; 63 | for (size_t i = 0; i < iter_count; ++i) { 64 | auto start = Clock::now(); 65 | f(); 66 | auto end = Clock::now(); 67 | durations.push_back(end - start); 68 | } 69 | std::sort(durations.begin(), durations.end()); 70 | return durations[durations.size() / 2]; 71 | } 72 | 73 | template 74 | static size_t to_ms(const Duration& duration) { 75 | return std::chrono::duration_cast(duration).count(); 76 | } 77 | 78 | enum class RenderMode { 79 | EyeLight, Debug 80 | }; 81 | 82 | struct Options { 83 | using Quality = bvh::v2::DefaultBuilder::Quality; 84 | 85 | bool show_usage = false; 86 | Vec3 eye = Vec3(0); 87 | Vec3 dir = Vec3(0, 0, 1); 88 | Vec3 up = Vec3(0, 1, 0); 89 | size_t width = 1024; 90 | size_t height = 1024; 91 | size_t build_iters = 1; 92 | bool permute_prims = false; 93 | bool robust_traversal = false; 94 | size_t debug_threshold = 0; 95 | RenderMode render_mode = RenderMode::EyeLight; 96 | Quality quality = Quality::High; 97 | std::string input_model; 98 | std::string output_image = "render.ppm"; 99 | 100 | bool parse(int argc, char** argv) { 101 | for (int i = 1; i < argc; ++i) { 102 | if (argv[i][0] != '-') { 103 | if (!input_model.empty()) { 104 | std::cerr << "Input model specified twice" << std::endl; 105 | return false; 106 | } 107 | input_model = argv[i]; 108 | continue; 109 | } 110 | 111 | using namespace std::literals; 112 | if (argv[i] == "--help"sv) { 113 | show_usage = true; 114 | return true; 115 | } else if (argv[i] == "--quality"sv || argv[i] == "-q"sv) { 116 | if (!check_arg(argc, argv, i)) 117 | return false; 118 | ++i; 119 | if (argv[i] == "low"sv) quality = Quality::Low; 120 | else if (argv[i] == "med"sv) quality = Quality::Medium; 121 | else if (argv[i] == "high"sv) quality = Quality::High; 122 | else { 123 | std::cerr << "Invalid BVH quality '" << argv[i] << "'" << std::endl; 124 | return false; 125 | } 126 | } else if (argv[i] == "--permute_prims"sv || argv[i] == "-p"sv) { 127 | permute_prims = true; 128 | } else if (argv[i] == "--build-iterations"sv || argv[i] == "-i"sv) { 129 | if (!check_arg(argc, argv, i)) 130 | return false; 131 | build_iters = std::strtoul(argv[++i], nullptr, 10); 132 | } else if (argv[i] == "--robust-traversal"sv) { 133 | robust_traversal = true; 134 | } else if (argv[i] == "--eye"sv || argv[i] == "-e"sv) { 135 | if (!check_arg(argc, argv, i, 3)) 136 | return false; 137 | eye[0] = static_cast(std::strtod(argv[++i], nullptr)); 138 | eye[1] = static_cast(std::strtod(argv[++i], nullptr)); 139 | eye[2] = static_cast(std::strtod(argv[++i], nullptr)); 140 | } else if (argv[i] == "--dir"sv || argv[i] == "-d"sv) { 141 | if (!check_arg(argc, argv, i, 3)) 142 | return false; 143 | dir[0] = static_cast(std::strtod(argv[++i], nullptr)); 144 | dir[1] = static_cast(std::strtod(argv[++i], nullptr)); 145 | dir[2] = static_cast(std::strtod(argv[++i], nullptr)); 146 | } else if (argv[i] == "--up"sv || argv[i] == "-u"sv) { 147 | if (!check_arg(argc, argv, i, 3)) 148 | return false; 149 | up[0] = static_cast(std::strtod(argv[++i], nullptr)); 150 | up[1] = static_cast(std::strtod(argv[++i], nullptr)); 151 | up[2] = static_cast(std::strtod(argv[++i], nullptr)); 152 | } else if (argv[i] == "--width"sv || argv[i] == "-w"sv) { 153 | if (!check_arg(argc, argv, i)) 154 | return false; 155 | width = std::strtoul(argv[++i], nullptr, 10); 156 | } else if (argv[i] == "--height"sv || argv[i] == "-h"sv) { 157 | if (!check_arg(argc, argv, i)) 158 | return false; 159 | height = std::strtoul(argv[++i], nullptr, 10); 160 | } else if (argv[i] == "--render-mode"sv || argv[i] == "-m"sv) { 161 | if (!check_arg(argc, argv, i)) 162 | return false; 163 | ++i; 164 | if (argv[i] == "eyelight"sv) render_mode = RenderMode::EyeLight; 165 | else if (argv[i] == "debug"sv) render_mode = RenderMode::Debug; 166 | else { 167 | std::cerr << "Invalid render mode '" << argv[i] << "'" << std::endl; 168 | return false; 169 | } 170 | } else if (argv[i] == "-o"sv) { 171 | if (!check_arg(argc, argv, i)) 172 | return false; 173 | output_image = argv[++i]; 174 | } else if (argv[i] == "--debug-threshold"sv) { 175 | if (!check_arg(argc, argv, i)) 176 | return false; 177 | debug_threshold = std::strtoull(argv[++i], nullptr, 10); 178 | } 179 | } 180 | if (input_model.empty()) { 181 | std::cerr << "Missing input model file name" << std::endl; 182 | return false; 183 | } 184 | return true; 185 | } 186 | 187 | private: 188 | static bool check_arg(int argc, char** argv, int i, int j = 1) { 189 | if (i + j >= argc) { 190 | std::cerr << "Missing argument for option '" << argv[i] << "'" << std::endl; 191 | return false; 192 | } 193 | return true; 194 | } 195 | }; 196 | 197 | struct Accel { 198 | Bvh bvh; 199 | std::vector tris; 200 | }; 201 | 202 | static Accel build_accel(bvh::v2::ThreadPool& thread_pool, const std::vector& tris, const Options& options) { 203 | bvh::v2::ParallelExecutor executor(thread_pool); 204 | 205 | std::vector bboxes(tris.size()); 206 | std::vector centers(tris.size()); 207 | executor.for_each(0, tris.size(), [&] (size_t begin, size_t end) { 208 | for (size_t i = begin; i < end; ++i) { 209 | bboxes[i] = tris[i].get_bbox(); 210 | centers[i] = tris[i].get_center(); 211 | } 212 | }); 213 | 214 | Accel accel; 215 | 216 | typename bvh::v2::DefaultBuilder::Config config; 217 | config.quality = options.quality; 218 | accel.bvh = bvh::v2::DefaultBuilder::build(thread_pool, bboxes, centers, config); 219 | 220 | accel.tris.resize(tris.size()); 221 | if (options.permute_prims) { 222 | executor.for_each(0, tris.size(), [&] (size_t begin, size_t end) { 223 | for (size_t i = begin; i < end; ++i) 224 | accel.tris[i] = tris[accel.bvh.prim_ids[i]]; 225 | }); 226 | } else { 227 | executor.for_each(0, tris.size(), [&] (size_t begin, size_t end) { 228 | for (size_t i = begin; i < end; ++i) 229 | accel.tris[i] = tris[i]; 230 | }); 231 | } 232 | 233 | return accel; 234 | } 235 | 236 | struct Image { 237 | size_t width = 0; 238 | size_t height = 0; 239 | std::unique_ptr data; 240 | 241 | Image() = default; 242 | Image(Image&&) = default; 243 | Image(const Image&) = delete; 244 | Image& operator = (Image&&) = default; 245 | 246 | Image(size_t width, size_t height) 247 | : width(width), height(height), data(std::make_unique(width * height * 3)) 248 | {} 249 | 250 | void save(const std::string& file_name) { 251 | std::ofstream out(file_name, std::ofstream::binary); 252 | out << "P6 " << width << " " << height << " " << 255 << "\n"; 253 | for (size_t j = height; j > 0; --j) 254 | out.write(reinterpret_cast(data.get() + (j - 1) * 3 * width), sizeof(uint8_t) * 3 * width); 255 | } 256 | }; 257 | 258 | struct TraversalStats { 259 | size_t visited_nodes = 0; 260 | size_t visited_leaves = 0; 261 | 262 | TraversalStats& operator += (const TraversalStats& other) { 263 | visited_nodes += other.visited_nodes; 264 | visited_leaves += other.visited_leaves; 265 | return *this; 266 | } 267 | }; 268 | 269 | struct RenderStats { 270 | TraversalStats traversal_stats; 271 | size_t intersections = 0; 272 | }; 273 | 274 | static constexpr size_t invalid_id = std::numeric_limits::max(); 275 | 276 | template 277 | static size_t intersect_accel(Ray& ray, const Accel& accel, TraversalStats& stats) { 278 | size_t prim_id = invalid_id; 279 | bvh::v2::SmallStack stack; 280 | accel.bvh.intersect(ray, accel.bvh.get_root().index, stack, 281 | [&] (size_t begin, size_t end) { 282 | if constexpr (CaptureStats) 283 | stats.visited_leaves++; 284 | for (size_t i = begin; i < end; ++i) { 285 | size_t j = PermutePrims ? i : accel.bvh.prim_ids[i]; 286 | if (auto hit = accel.tris[j].intersect(ray)) { 287 | ray.tmax = std::get<0>(*hit); 288 | prim_id = j; 289 | } 290 | } 291 | return prim_id != invalid_id; 292 | }, 293 | [&] (auto&&, auto&&) { 294 | if constexpr (CaptureStats) 295 | stats.visited_nodes++; 296 | }); 297 | return prim_id; 298 | } 299 | 300 | using IntersectAccelFn = size_t (*)(Ray&, const Accel&, TraversalStats&); 301 | static IntersectAccelFn get_intersect_accel_fn(const Options& options) { 302 | static const IntersectAccelFn intersect_accel_fns[] = { 303 | intersect_accel, 304 | intersect_accel, 305 | intersect_accel, 306 | intersect_accel, 307 | intersect_accel, 308 | intersect_accel, 309 | intersect_accel, 310 | intersect_accel 311 | }; 312 | return intersect_accel_fns[ 313 | (options.robust_traversal ? 4 : 0) | 314 | (options.permute_prims ? 2 : 0) | 315 | (options.render_mode == RenderMode::Debug ? 1 : 0)]; 316 | } 317 | 318 | std::tuple intensity_to_color(Scalar k) { 319 | static const Vec3 g[] = { 320 | Vec3(0, 0, 255), 321 | Vec3(0, 255, 255), 322 | Vec3(0, 128, 0), 323 | Vec3(255, 255, 0), 324 | Vec3(255, 0, 0) 325 | }; 326 | static constexpr size_t n = sizeof(g) / sizeof(g[0]); 327 | static constexpr auto s = Scalar{1} / static_cast(n); 328 | 329 | size_t i = std::min(n - 1, static_cast(k * n)); 330 | size_t j = std::min(n - 1, i + 1); 331 | 332 | auto t = (k - static_cast(i) * s) / s; 333 | auto c = (1.0f - t) * g[i] + t * g[j]; 334 | return std::make_tuple( 335 | static_cast(c[0]), 336 | static_cast(c[1]), 337 | static_cast(c[2])); 338 | } 339 | 340 | static Image render(const Accel& accel, RenderStats& render_stats, const Options& options) { 341 | auto intersect_accel = get_intersect_accel_fn(options); 342 | 343 | auto dir = normalize(options.dir); 344 | auto right = normalize(cross(dir, options.up)); 345 | auto up = cross(right, dir); 346 | 347 | auto debug_data = options.render_mode == RenderMode::Debug 348 | ? std::make_unique(options.width * options.height) : nullptr; 349 | 350 | Image image(options.width, options.height); 351 | for (size_t y = 0; y < options.height; ++y) { 352 | for (size_t x = 0; x < options.width; ++x) { 353 | auto u = Scalar{2} * static_cast(x) / static_cast(options.width) - Scalar{1}; 354 | auto v = Scalar{2} * static_cast(y) / static_cast(options.height) - Scalar{1}; 355 | auto pixel_id = y * options.width + x; 356 | 357 | TraversalStats traversal_stats; 358 | Ray ray(options.eye, dir + u * right + v * up); 359 | auto prim_id = intersect_accel(ray, accel, traversal_stats); 360 | if (prim_id != invalid_id) 361 | render_stats.intersections++; 362 | Scalar intensity = 0; 363 | if (options.render_mode == RenderMode::EyeLight) { 364 | if (prim_id != invalid_id) 365 | intensity = std::abs(dot(normalize(accel.tris[prim_id].n), ray.dir)); 366 | uint8_t pixel = static_cast( 367 | std::min(std::max(0, static_cast(intensity * Scalar{256})), 255)); 368 | image.data[3 * pixel_id + 0] = pixel; 369 | image.data[3 * pixel_id + 1] = pixel; 370 | image.data[3 * pixel_id + 2] = pixel; 371 | } else { 372 | debug_data[pixel_id] = 373 | traversal_stats.visited_nodes + traversal_stats.visited_leaves; 374 | render_stats.traversal_stats += traversal_stats; 375 | } 376 | } 377 | } 378 | 379 | if (options.render_mode == RenderMode::Debug) { 380 | Scalar debug_threshold = static_cast(options.debug_threshold); 381 | if (debug_threshold == static_cast(0)) 382 | debug_threshold = static_cast( 383 | *std::max_element(debug_data.get(), debug_data.get() + options.width * options.height)); 384 | for (size_t i = 0; i < options.width * options.height; ++i) { 385 | std::tie( 386 | image.data[3 * i + 0], 387 | image.data[3 * i + 1], 388 | image.data[3 * i + 2]) = intensity_to_color(static_cast(debug_data[i]) / debug_threshold); 389 | } 390 | } 391 | 392 | return image; 393 | } 394 | 395 | int main(int argc, char** argv) { 396 | Options options; 397 | if (!options.parse(argc, argv)) 398 | return 1; 399 | 400 | if (options.show_usage) { 401 | usage(); 402 | return 0; 403 | } 404 | 405 | auto tris = load_obj(options.input_model); 406 | if (tris.empty()) { 407 | std::cerr << "No triangle was found in input OBJ file" << std::endl; 408 | return 1; 409 | } 410 | std::cout << "Loaded file with " << tris.size() << " triangle(s)" << std::endl; 411 | 412 | Accel accel; 413 | bvh::v2::ThreadPool thread_pool; 414 | auto build_time = profile([&] { accel = build_accel(thread_pool, tris, options); }, options.build_iters); 415 | std::cout 416 | << "Built BVH with " << accel.bvh.nodes.size() << " node(s) in " 417 | << to_ms(build_time) << "ms" << std::endl; 418 | 419 | // Not needed, just for testing 420 | accel.bvh.refit(); 421 | 422 | RenderStats stats; 423 | Image image; 424 | auto intersection_time = profile([&] { image = render(accel, stats, options); }); 425 | 426 | std::cout << stats.intersections << " intersection(s) found in " << to_ms(intersection_time) << "ms\n"; 427 | if (options.render_mode == RenderMode::Debug) { 428 | std::cout 429 | << "Traversal visited " << stats.traversal_stats.visited_nodes 430 | << " nodes and " << stats.traversal_stats.visited_leaves << " leaves" << std::endl; 431 | } 432 | 433 | image.save(options.output_image); 434 | std::cout << "Image saved as '" << options.output_image << "'" << std::endl; 435 | return 0; 436 | } 437 | --------------------------------------------------------------------------------