Skip to content

Commit 81ed5a6

Browse files
committed
Integrates the C++ geometry processing library libigl to handle collision detection efficiently in the ray tracing and image source model simulator.
1 parent c7365b8 commit 81ed5a6

File tree

9 files changed

+244
-15
lines changed

9 files changed

+244
-15
lines changed

CHANGELOG.rst

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,12 @@ adheres to `Semantic Versioning <http://semver.org/spec/v2.0.0.html>`_.
1111
`Unreleased`_
1212
-------------
1313

14+
Added
15+
~~~~~
16+
17+
- Integration of `libigl <https://github.com/libigl/libigl>`_ to efficiently
18+
handle ray-polygon intersection detection in the C++ engine.
19+
1420
Changed
1521
~~~~~~~
1622

CMakeLists.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ cmake_minimum_required(VERSION 3.15)
22
set(CMAKE_POLICY_VERSION_MINIMUM 3.5)
33

44
project(pyroomacoustics)
5+
set(CMAKE_CXX_STANDARD 17)
6+
set(CMAKE_CXX_STANDARD_REQUIRED ON)
57

68
# Find pybind11 (handled by external/CMakeLists.txt via FetchContent)
79
set(PYBIND11_FINDPYTHON ON)
@@ -30,6 +32,7 @@ target_include_directories(libroom PRIVATE
3032
target_link_libraries(libroom PRIVATE
3133
Eigen3::Eigen
3234
nanoflann::nanoflann
35+
igl::core
3336
)
3437

3538
# Compile options

external/CMakeLists.txt

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,13 @@ FetchContent_Declare(
2121
GIT_TAG v3.0.1
2222
)
2323

24+
# libigl
25+
FetchContent_Declare(
26+
libigl
27+
GIT_REPOSITORY https://github.com/libigl/libigl
28+
GIT_TAG v2.6.0
29+
)
30+
2431
# eigen
2532
set(EIGEN_BUILD_PKGCONFIG OFF CACHE BOOL "Disable Eigen pkg-config")
2633
set(EIGEN_BUILD_TESTING OFF CACHE BOOL "Disable Eigen testing")
@@ -34,4 +41,4 @@ set(BUILD_BENCHMARKS OFF CACHE BOOL "Disable nanoflann benchmarks" FORCE)
3441
# pybind11
3542
set(PYBIND11_TEST OFF CACHE BOOL "Disable pybind11 tests")
3643

37-
FetchContent_MakeAvailable(eigen nanoflann pybind11)
44+
FetchContent_MakeAvailable(eigen nanoflann pybind11 libigl)
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
// Included by geometry_utils.hpp
2+
#include <igl/polygons_to_triangles.h>
3+
#include <igl/remove_duplicate_vertices.h>
4+
#include <iostream>
5+
6+
std::tuple<Eigen::MatrixXf, Eigen::MatrixXi, std::vector<int>>
7+
triangulate_walls(const std::vector<Wall<3>> &walls) {
8+
std::vector<Eigen::Vector3f> all_vertices;
9+
std::vector<Eigen::Vector3i> all_faces;
10+
std::vector<int> face_to_wall;
11+
12+
for (size_t i = 0; i < walls.size(); ++i) {
13+
const auto &wall = walls[i];
14+
int n_corners = wall.flat_corners.cols();
15+
16+
// Local 2D coordinates for triangulation
17+
Eigen::MatrixXd V2(n_corners, 2);
18+
for (int j = 0; j < n_corners; ++j) {
19+
V2(j, 0) = (double)wall.flat_corners(0, j);
20+
V2(j, 1) = (double)wall.flat_corners(1, j);
21+
}
22+
23+
// Cumulative indices for polygons_to_triangles
24+
// For a single polygon, C = [0, n_corners]
25+
Eigen::VectorXi C(2);
26+
C << 0, n_corners;
27+
Eigen::VectorXi I(n_corners);
28+
for (int j = 0; j < n_corners; ++j)
29+
I(j) = j;
30+
31+
Eigen::MatrixXi F_wall;
32+
Eigen::VectorXi J_wall;
33+
igl::polygons_to_triangles(I, C, F_wall, J_wall);
34+
35+
// Map wall local vertex indices to global vertex indices
36+
int base_v_idx = all_vertices.size();
37+
for (int j = 0; j < n_corners; ++j) {
38+
all_vertices.push_back(wall.corners.col(j).cast<float>());
39+
}
40+
41+
for (int j = 0; j < F_wall.rows(); ++j) {
42+
all_faces.push_back(Eigen::Vector3i(base_v_idx + F_wall(j, 0),
43+
base_v_idx + F_wall(j, 1),
44+
base_v_idx + F_wall(j, 2)));
45+
face_to_wall.push_back(i);
46+
}
47+
}
48+
49+
// Convert to Eigen matrices
50+
Eigen::MatrixXf V(all_vertices.size(), 3);
51+
for (size_t i = 0; i < all_vertices.size(); ++i)
52+
V.row(i) = all_vertices[i];
53+
54+
Eigen::MatrixXi F(all_faces.size(), 3);
55+
for (size_t i = 0; i < all_faces.size(); ++i)
56+
F.row(i) = all_faces[i];
57+
58+
// Remove duplicate vertices to ensure a watertight mesh
59+
// This is important for AABB tree reliability
60+
Eigen::MatrixXf V_unique;
61+
Eigen::MatrixXi F_unique;
62+
Eigen::VectorXi S, _I;
63+
igl::remove_duplicate_vertices(V, F, 1e-5, V_unique, S, _I, F_unique);
64+
65+
return std::make_tuple(V_unique, F_unique, face_to_wall);
66+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
#ifndef __GEOMETRY_UTILS_H__
2+
#define __GEOMETRY_UTILS_H__
3+
4+
#include "common.hpp"
5+
#include "wall.hpp"
6+
#include <Eigen/Dense>
7+
#include <tuple>
8+
#include <vector>
9+
10+
/**
11+
* @brief Triangulates a set of walls into a single mesh.
12+
*
13+
* @param walls Vector of Wall objects (3D).
14+
* @return std::tuple<Eigen::MatrixXf, Eigen::MatrixXi, std::vector<int>>
15+
* - V: Matrix of vertices (n_vertices x 3)
16+
* - F: Matrix of faces (n_faces x 3)
17+
* - face_to_wall: Vector mapping each face to its original wall index.
18+
*/
19+
std::tuple<Eigen::MatrixXf, Eigen::MatrixXi, std::vector<int>>
20+
triangulate_walls(const std::vector<Wall<3>> &walls);
21+
22+
#include "geometry_utils.cpp"
23+
24+
#endif // __GEOMETRY_UTILS_H__

pyroomacoustics/libroom_src/libroom.cpp

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535

3636
#include "common.hpp"
3737
#include "geometry.hpp"
38+
#include "geometry_utils.hpp"
3839
#include "microphone.hpp"
3940
#include "random.hpp"
4041
#include "rir_builder.hpp"
@@ -56,6 +57,11 @@ PYBIND11_MODULE(libroom, m) {
5657
.def(py::init<const std::vector<Wall<3>> &, const std::vector<int> &,
5758
const std::vector<Microphone<3>> &, float, int, float,
5859
float, float, float, bool>())
60+
.def(
61+
py::init<const std::vector<Wall<3>> &, const std::vector<int> &,
62+
const Eigen::MatrixXf &, const Eigen::MatrixXi &,
63+
const std::vector<int> &, const std::vector<Microphone<3>> &,
64+
float, int, float, float, float, float, bool>())
5965
.def(py::init<const Vectorf<3> &,
6066
const Eigen::Array<float, Eigen::Dynamic, 6> &,
6167
const Eigen::Array<float, Eigen::Dynamic, 6> &,
@@ -330,4 +336,7 @@ PYBIND11_MODULE(libroom, m) {
330336
m.def("rir_builder", &rir_builder, "RIR builder");
331337
m.def("delay_sum", &delay_sum, "Delay and sum");
332338
m.def("fractional_delay", &fractional_delay, "Fractional delays");
339+
340+
m.def("triangulate_walls", &triangulate_walls,
341+
"Triangulates a set of walls into a mesh");
333342
}

pyroomacoustics/libroom_src/room.cpp

Lines changed: 49 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,10 @@
2424
* If not, see <https://opensource.org/licenses/MIT>.
2525
*/
2626

27-
28-
#include <cmath>
29-
#include <algorithm>
30-
#include "constants.hpp"
3127
#include "room.hpp"
28+
#include "constants.hpp"
29+
#include <algorithm>
30+
#include <cmath>
3231

3332
size_t number_image_sources_2(size_t max_order) {
3433
/*
@@ -104,6 +103,29 @@ Room<D>::Room(const Vectorf<D> &_room_size,
104103
init();
105104
}
106105

106+
template <size_t D>
107+
Room<D>::Room(const std::vector<Wall<D>> &_walls,
108+
const std::vector<int> &_obstructing_walls,
109+
const Eigen::MatrixXf &_V, const Eigen::MatrixXi &_F,
110+
const std::vector<int> &_face_to_wall,
111+
const std::vector<Microphone<D>> &_microphones,
112+
float _sound_speed, int _ism_order, float _energy_thres,
113+
float _time_thres, float _mic_radius, float _mic_hist_res,
114+
bool _is_hybrid_sim)
115+
: walls(_walls), obstructing_walls(_obstructing_walls),
116+
microphones(_microphones), V_mesh(_V), F_mesh(_F),
117+
face_to_wall(_face_to_wall), sound_speed(_sound_speed),
118+
ism_order(_ism_order), energy_thres(_energy_thres),
119+
time_thres(_time_thres), mic_radius(_mic_radius),
120+
mic_radius_sq(_mic_radius * _mic_radius), mic_hist_res(_mic_hist_res),
121+
is_hybrid_sim(_is_hybrid_sim), is_shoebox(false) {
122+
init();
123+
// Initialize AABB tree if mesh is provided
124+
if (F_mesh.rows() > 0) {
125+
mesh_aabb.init(V_mesh, F_mesh);
126+
}
127+
}
128+
107129
template <>
108130
void Room<2>::make_shoebox_walls(
109131
const Vectorf<2> &rs, // room_size
@@ -669,6 +691,28 @@ std::tuple<Vectorf<D>, int, float> Room<D>::next_wall_hit(
669691
(void)0; // no op
670692
}
671693
} else {
694+
if constexpr (D == 3) {
695+
if (F_mesh.rows() > 0 && !scattered_ray) {
696+
// Use libigl AABB tree for 3D ray tracing (non-scattered)
697+
igl::Hit<float> hit;
698+
Vectorf<3> _start = start.template cast<float>();
699+
Vectorf<3> _end = end.template cast<float>();
700+
Vectorf<3> _dir = (_end - _start).normalized();
701+
// Offset start to avoid hitting the same wall
702+
Vectorf<3> _start_offset = _start + 1e-4f * _dir;
703+
if (mesh_aabb.intersect_ray(V_mesh, F_mesh, _start_offset, _dir, hit)) {
704+
// hit.t is the distance to the hit point from _start_offset
705+
float actual_t = hit.t + 1e-4f;
706+
if (actual_t < hit_dist) {
707+
hit_dist = actual_t;
708+
result = start + actual_t * _dir.template cast<float>();
709+
next_wall_index = face_to_wall[hit.id];
710+
}
711+
}
712+
return std::make_tuple(result, next_wall_index, hit_dist);
713+
}
714+
}
715+
672716
// For case 1) in non-convex rooms, the segment might intersect several
673717
// walls. In this case, we are only interested on the closest wall to
674718
// 'start'. That's why we need a min_dist variable
@@ -696,7 +740,7 @@ std::tuple<Vectorf<D>, int, float> Room<D>::next_wall_hit(
696740
if (temp_dist > libroom_eps && temp_dist < hit_dist) {
697741
hit_dist = temp_dist;
698742
result = temp_hit;
699-
next_wall_index = i;
743+
next_wall_index = scattered_ray ? obstructing_walls[i] : i;
700744
}
701745
}
702746
}
@@ -1131,4 +1175,3 @@ bool Room<D>::contains(const Vectorf<D> point) {
11311175
// then the point is in the room => return true
11321176
return ((n_intersections % 2) == 1);
11331177
}
1134-

pyroomacoustics/libroom_src/room.hpp

Lines changed: 30 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
/*
1+
/*
22
* Definition of the Room class
33
* Copyright (C) 2019 Robin Scheibler, Cyril Cadoux
44
*
@@ -26,14 +26,17 @@
2626
#ifndef __ROOM_H__
2727
#define __ROOM_H__
2828

29-
#include <vector>
30-
#include <stack>
31-
#include <tuple>
29+
#include <Eigen/Core>
3230
#include <Eigen/Dense>
3331
#include <algorithm>
3432
#include <ctime>
33+
#include <igl/AABB.h>
34+
#include <stack>
35+
#include <tuple>
36+
#include <vector>
3537

3638
#include "common.hpp"
39+
#include "microphone.hpp"
3740
#include "wall.hpp"
3841

3942
template<size_t D>
@@ -93,6 +96,12 @@ class Room
9396
std::vector<Microphone<D>> microphones; // The microphones are in the room
9497
float sound_speed = 343.; // the speed of sound in the room
9598

99+
// libigl mesh for ray tracing
100+
Eigen::MatrixXf V_mesh;
101+
Eigen::MatrixXi F_mesh;
102+
std::vector<int> face_to_wall;
103+
igl::AABB<Eigen::MatrixXf, 3> mesh_aabb;
104+
96105
// Simulation parameters
97106
int ism_order = 0.;
98107

@@ -127,6 +136,23 @@ class Room
127136
// its size is n_microphones * n_sources
128137
MatrixXb visible_mics;
129138

139+
// Constructor for general rooms with mesh
140+
Room(
141+
const std::vector<Wall<D>> &_walls,
142+
const std::vector<int> &_obstructing_walls,
143+
const Eigen::MatrixXf &_V,
144+
const Eigen::MatrixXi &_F,
145+
const std::vector<int> &_face_to_wall,
146+
const std::vector<Microphone<D>> &_microphones,
147+
float _sound_speed,
148+
int _ism_order,
149+
float _energy_thres,
150+
float _time_thres,
151+
float _mic_radius,
152+
float _mic_hist_res,
153+
bool _is_hybrid_sim
154+
);
155+
130156
// Constructor for general rooms
131157
Room(
132158
const std::vector<Wall<D>> &_walls,
@@ -288,7 +314,6 @@ class Room
288314
bool is_visible_dfs(const Vectorf<D> &p, ImageSource<D> &is, Vectorf<D> &source_direction);
289315
bool is_obstructed_dfs(const Vectorf<D> &p, ImageSource<D> &is);
290316
int fill_sources();
291-
292317
};
293318

294319
#include "room.cpp"

pyroomacoustics/room.py

Lines changed: 49 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -882,11 +882,41 @@ def __init__(
882882
use_rand_ism=False,
883883
max_rand_disp=0.08,
884884
min_phase=False,
885+
V=None,
886+
F=None,
887+
face_to_wall=None,
885888
):
886-
self.walls = walls
889+
if walls is None and V is not None and F is not None:
890+
# derive walls from V and F
891+
# if face_to_wall is not provided, we assume each face is a wall
892+
# if provided, we group faces by wall index
893+
if face_to_wall is None:
894+
face_to_wall = np.arange(F.shape[0])
895+
896+
unique_walls = np.unique(face_to_wall)
897+
self.walls = []
898+
for w_idx in unique_walls:
899+
faces = F[face_to_wall == w_idx]
900+
# For simplicity, we just use the vertices of the first face
901+
# but pyroomacoustics Walls are polygons.
902+
# If we have multiple triangles for a single wall, we can't
903+
# easily represent them as a single polygon if it's not simple.
904+
# However, for simulation, the mesh is what matters.
905+
# Here we create a dummy wall for each triangle if they are not grouped.
906+
for face in faces:
907+
corners = V[face].T
908+
self.walls.append(
909+
wall_factory(corners, [0.0], [0.0], name=f"wall_{w_idx}")
910+
)
911+
else:
912+
self.walls = walls
913+
914+
self.V = V
915+
self.F = F
916+
self.face_to_wall = face_to_wall
887917

888918
# Get the room dimension from that of the walls
889-
self.dim = walls[0].dim
919+
self.dim = self.walls[0].dim
890920

891921
# Create a mapping with friendly names for walls
892922
self._wall_mapping()
@@ -999,7 +1029,23 @@ def _init_room_engine(self, *args):
9991029
# This is a polygonal room
10001030
# find the non convex walls
10011031
obstructing_walls = find_non_convex_walls(self.walls)
1002-
args += [self.walls, obstructing_walls]
1032+
1033+
if self.dim == 3:
1034+
# If mesh is not provided, we triangulate
1035+
if self.V is None or self.F is None:
1036+
self.V, self.F, self.face_to_wall = libroom.triangulate_walls(
1037+
self.walls
1038+
)
1039+
1040+
args += [
1041+
self.walls,
1042+
obstructing_walls,
1043+
self.V,
1044+
self.F,
1045+
self.face_to_wall,
1046+
]
1047+
else:
1048+
args += [self.walls, obstructing_walls]
10031049

10041050
# for shoebox rooms, the required arguments are passed to
10051051
# the function

0 commit comments

Comments
 (0)