Skip to content

Commit 3fcd5bc

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 3fcd5bc

File tree

7 files changed

+154
-15
lines changed

7 files changed

+154
-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)

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)