Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
7d128ab
Add Linear Bounding Volume Hierarchy (LBVH) implementation and profil…
zfergus Sep 1, 2025
ed41da8
Enhance profiler functionality and integrate into LBVH and BVH implem…
zfergus Sep 2, 2025
c0c67dc
Fix Linux and Window build
zfergus Sep 2, 2025
b986930
Fix 2D case
zfergus Sep 2, 2025
1bb9f8e
Refactor Morton code functions to support 2D points and improve bit e…
zfergus Sep 2, 2025
44203bc
Add concurrent queue for parallel candidate detection in LBVH
zfergus Oct 3, 2025
9f43de0
Merge branch 'main' into lbvh
zfergus Oct 26, 2025
aa1c1ce
Merge branch 'main' into lbvh
zfergus Jan 16, 2026
acffc2b
Update Python version matrix to include 3.14 for pull requests
zfergus Jan 16, 2026
1fa4de6
Fix clang-tidy errors
zfergus Jan 16, 2026
b2b4f39
Address copilot comments
zfergus Jan 16, 2026
ad5039f
Refactor LBVH Node Structure and Add SIMD Traversal
zfergus Jan 19, 2026
d9ae2da
Fix test LBVH::build
zfergus Jan 19, 2026
4b173cf
Fix LBVH in 2D
zfergus Jan 19, 2026
0cf9fea
Enhance profiler functionality with CSV output and improve code struc…
zfergus Jan 20, 2026
ad44e09
Use 1ULL for bit-shift constants in Morton functions
zfergus Jan 20, 2026
d150477
Resolve Copilot comments
zfergus Jan 20, 2026
9e75039
Resolve compiler warnings on linux
zfergus Jan 20, 2026
b0dec30
Put inf inside lambda in aabb.cpp
zfergus Jan 20, 2026
b72f570
Refactor Puffer-Ball test cases to use accessible mesh paths and upda…
zfergus Jan 21, 2026
b906cf4
Refactor AABB to use a fixed size Array3d
zfergus Jan 22, 2026
9a61fc4
Fix edges in examples/profiler.py
zfergus Jan 22, 2026
5f2cd23
Fix GH Action errors
zfergus Jan 22, 2026
ce61a18
Update LBVH documentation
zfergus Jan 22, 2026
213467a
Update profile.py
zfergus Jan 23, 2026
6872f95
Update benchmark test case descriptions
zfergus Jan 23, 2026
91ff457
Deprecate BVH and optimize LBVH and merges
zfergus Jan 26, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions .clang-format
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
---
Language: Cpp
BasedOnStyle: WebKit
AlignAfterOpenBracket: AlwaysBreak
AlignTrailingComments:
Expand Down Expand Up @@ -38,6 +37,5 @@ IncludeCategories:
SortPriority: 1
CaseSensitive: true
PackConstructorInitializers: CurrentLine
RemoveEmptyLinesInUnwrappedLines: true
SortIncludes: CaseInsensitive
...
2 changes: 1 addition & 1 deletion .github/workflows/python.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ jobs:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
python-version: ${{ github.event_name == 'pull_request' && fromJSON('["3.13"]') || fromJSON('["3.9", "3.10", "3.11", "3.12", "3.13"]') }}
python-version: ${{ github.event_name == 'pull_request' && fromJSON('["3.14"]') || fromJSON('["3.10", "3.11", "3.12", "3.13", "3.14"]') }}
include:
- os: ubuntu-latest
name: Linux
Expand Down
7 changes: 7 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ option(IPC_TOOLKIT_WITH_ROBIN_MAP "Use Tessil's robin-map rather tha
option(IPC_TOOLKIT_WITH_ABSEIL "Use Abseil's hash functions" ON)
option(IPC_TOOLKIT_WITH_FILIB "Use filib for interval arithmetic" ON)
option(IPC_TOOLKIT_WITH_INEXACT_CCD "Use the original inexact CCD method of IPC" OFF)
option(IPC_TOOLKIT_WITH_PROFILER "Enable performance profiler" OFF)

# Advanced options
option(IPC_TOOLKIT_WITH_SIMD "Enable SIMD" OFF)
Expand Down Expand Up @@ -230,6 +231,12 @@ if(IPC_TOOLKIT_WITH_FILIB)
target_link_libraries(ipc_toolkit PUBLIC filib::filib)
endif()

if(IPC_TOOLKIT_WITH_PROFILER)
# Add nlohmann/json for the profiler
include(json)
target_link_libraries(ipc_toolkit PUBLIC nlohmann_json::nlohmann_json)
endif()

# Extra warnings (link last for highest priority)
include(ipc_toolkit_warnings)
target_link_libraries(ipc_toolkit PRIVATE ipc::toolkit::warnings)
Expand Down
2 changes: 1 addition & 1 deletion cmake/ipc_toolkit/ipc_toolkit_tests_data.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ else()
SOURCE_DIR ${IPC_TOOLKIT_TESTS_DATA_DIR}

GIT_REPOSITORY https://github.com/ipc-sim/ipc-toolkit-tests-data.git
GIT_TAG 7ca6db695adcc00d3d6d978767dfc0d81722a515
GIT_TAG c7eba549d9a80d15569a013c473f0aff104ac44a

CONFIGURE_COMMAND ""
BUILD_COMMAND ""
Expand Down
3 changes: 3 additions & 0 deletions cmake/ipc_toolkit/ipc_toolkit_warnings.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,9 @@ else()

-Wno-sign-compare

-Wno-gnu-anonymous-struct
-Wno-nested-anon-types

###########
# GCC 6.1 #
###########
Expand Down
2 changes: 1 addition & 1 deletion cmake/recipes/evouga_ccd.cmake
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Etienne Vouga's CCD Library (https://github.com/evouga/collisiondetection.git)
# Etienne Vouga's CCD Library (https://github.com/evouga/collisiondetection)
# License: ???
if(TARGET evouga::ccd)
return()
Expand Down
2 changes: 1 addition & 1 deletion cmake/recipes/filib.cmake
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# filib (https://github.com/zfergus/filib.git)
# filib (https://github.com/zfergus/filib)
# License: LGPL-2.1
if(TARGET filib::filib)
return()
Expand Down
26 changes: 26 additions & 0 deletions cmake/recipes/pybind11_json.cmake
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# pybind11_json (https://github.com/pybind/pybind11_json)
# License: MIT
if(TARGET pybind11::json)
return()
endif()

message(STATUS "Third-party: creating target 'pybind11::json'")

include(CPM)
CPMAddPackage(
URI "gh:pybind/pybind11_json#0.2.15"
DOWNLOAD_ONLY YES
)

add_library(pybind11_json INTERFACE)
add_library(pybind11::json ALIAS pybind11_json)

target_include_directories(pybind11_json INTERFACE
"$<BUILD_INTERFACE:${pybind11_json_SOURCE_DIR}/include>"
)

include(pybind11)
target_link_libraries(pybind11_json INTERFACE pybind11::pybind11)

include(json)
target_link_libraries(pybind11_json INTERFACE nlohmann_json::nlohmann_json)
3 changes: 3 additions & 0 deletions docs/source/_static/graphviz/dependencies.dot
Original file line number Diff line number Diff line change
Expand Up @@ -83,4 +83,7 @@ digraph "IPC Toolkit Dependencies" {
"node5" -> "node13" [color = "#8FB976";];
"node13" -> "node0" [color = "#BE6562";];
"node13" -> "node9" [color = "#BE6562";];
// ipc_toolkit -> nlohmann_json
"node14" [label = "nlohmann_json\n(nlohmann_json::nlohmann_json)";shape = box;style = "rounded,filled";fillcolor = "#FFE6CC";color = "#DAA52D";];
"node5" -> "node14" [color = "#8FB976";];
}
91 changes: 52 additions & 39 deletions docs/source/_static/graphviz/dependencies.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
17 changes: 13 additions & 4 deletions docs/source/about/dependencies.rst
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,18 @@ Additionally, IPC Toolkit may optionally use the following libraries:
- `github.com/zfergus/filib <https://github.com/zfergus/filib>`_
- |:white_check_mark:|
- ``IPC_TOOLKIT_WITH_FILIB``
* - nlohmann/json
- JSON parsing for profiler and tests
- MIT
- `github.com/nlohmann/json <https://github.com/nlohmann/json>`_
- |:white_large_square:|
- ``IPC_TOOLKIT_WITH_PROFILER``
* - pybind11_json
- JSON support for pybind11 (used in profiler)
- MIT
- `github.com/pybind/pybind11_json <https://github.com/pybind/pybind11_json>`_
- |:white_large_square:|
- ``IPC_TOOLKIT_BUILD_PYTHON`` and ``IPC_TOOLKIT_WITH_PROFILER``
* - rational-cpp
- Rational arithmetic used for exact intersection checks (requires `GMP <https://gmplib.org>`_ to be installed at a system level)
- MIT
Expand Down Expand Up @@ -136,7 +148,4 @@ Unit Test Dependencies
- `github.com/catchorg/Catch2 <https://github.com/catchorg/Catch2.git>`_
* - finite-diff
- Finite-difference comparisons
- `github.com/zfergus/finite-diff <https://github.com/zfergus/finite-diff>`_
* - Nlohmann JSON
- Loading test data from JSON files
- `github.com/nlohmann/json <https://github.com/nlohmann/json>`_
- `github.com/zfergus/finite-diff <https://github.com/zfergus/finite-diff>`_
6 changes: 6 additions & 0 deletions docs/source/cpp-api/broad_phase.rst
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,12 @@ BVH
.. doxygenclass:: ipc::BVH
:allow-dot-graphs:

LBVH
---

.. doxygenclass:: ipc::LBVH
:allow-dot-graphs:

Sweep and Prune
-----------------------

Expand Down
7 changes: 7 additions & 0 deletions docs/source/python-api/broad_phase.rst
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,13 @@ BVH

.. autoclasstoc::

LBVH
----

.. autoclass:: ipctk.LBVH

.. autoclasstoc::

Sweep and Prune
---------------

Expand Down
30 changes: 30 additions & 0 deletions docs/source/references.bib
Original file line number Diff line number Diff line change
Expand Up @@ -128,3 +128,33 @@ @article{Huang2025GCP
number = 4,
note = {\url{https://doi.org/10.1145/3731142}}
}
@inproceedings{Karras2012HPG,
author = {Karras, Tero},
title = {Maximizing Parallelism in the Construction of BVHs, Octrees, and k-d Trees},
booktitle = {Proceedings of the Fourth Eurographics Conference on High-Performance Graphics},
series = {HPG '12},
year = {2012},
pages = {33--37},
publisher = {Eurographics Association},
address = {Goslar, Germany},
note = {\url{https://doi.org/10.2312/EGGH/HPG12/033-037}}
}
@phdthesis{Baraff1992PhD,
author = {Baraff, David},
title = {Dynamic Simulation of Non-Penetrating Rigid Bodies},
school = {Cornell University},
address = {Ithaca, NY, USA},
year = {1992},
note = {Technical Report TR 92-1275}
}
@inproceedings{Cohen1995ICOLLIDE,
author = {Cohen, Jonathan D. and Lin, Ming C. and Manocha, Dinesh and Ponamgi, Madhav},
title = {I-COLLIDE: An Interactive and Exact Collision Detection System for Large-Scale Environments},
booktitle = {Proceedings of the 1995 Symposium on Interactive 3D Graphics},
series = {I3D '95},
year = {1995},
pages = {189--196},
publisher = {ACM},
address = {New York, NY, USA},
note = {\url{https://doi.org/10.1145/199404.199437}}
}
3 changes: 1 addition & 2 deletions docs/source/tutorials/getting_started.rst
Original file line number Diff line number Diff line change
Expand Up @@ -531,8 +531,7 @@ The ``Candidates`` class represents the culled set of candidate pairs and is bui
candidates.build(
mesh, vertices_t0, vertices_t1, broad_phase=ipctk.HashGrid())

Possible values for ``broad_phase`` are: ``BruteForce`` (parallel brute force culling), ``HashGrid`` (default), ``SpatialHash`` (implementation from the original IPC codebase),
``BVH`` (`SimpleBVH <https://github.com/geometryprocessing/SimpleBVH>`_), ``SweepAndPrune`` (method of :cite:t:`Belgrod2023Time`), or ``SweepAndTiniestQueue`` (requires CUDA).
Possible values for ``broad_phase`` are: ``BruteForce`` (parallel brute force culling), ``HashGrid`` (default), ``SpatialHash`` (implementation from the original IPC codebase), ``BVH`` (`SimpleBVH <https://github.com/geometryprocessing/SimpleBVH>`_), ``LBVH`` (CPU implementation of :cite:t:`Karras2012HPG` using TBB), ``SweepAndPrune`` (a.k.a. Sort-and-Sweep from :cite:t:`Baraff1992PhD`), or ``SweepAndTiniestQueue`` (method of :cite:t:`Belgrod2023Time`; requires CUDA).

Narrow-Phase
^^^^^^^^^^^^
Expand Down
5 changes: 5 additions & 0 deletions python/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,11 @@ if (FILIB_BUILD_SHARED_LIB AND WIN32)
)
endif()

if (IPC_TOOLKIT_WITH_PROFILER)
include(pybind11_json)
target_link_libraries(ipctk PRIVATE pybind11::json)
endif()

# Extra warnings
# target_link_libraries(ipctk PRIVATE IPCToolkit::warnings)

Expand Down
85 changes: 85 additions & 0 deletions python/examples/lbvh.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
from find_ipctk import ipctk
import meshio
import polyscope as ps
from polyscope import imgui
import numpy as np

import pathlib

import argparse

parser = argparse.ArgumentParser()
parser.add_argument(
"--mesh",
type=pathlib.Path,
default=(pathlib.Path(__file__).parents[2] / "tests/data/puffer-ball/20.ply"))
args = parser.parse_args()

mesh = meshio.read(args.mesh)

lbvh = ipctk.LBVH()
lbvh.build(mesh.points, np.array([], dtype=int), mesh.cells_dict["triangle"])

ps.init()

ps.set_give_focus_on_show(True)

ps_mesh = ps.register_surface_mesh(
"mesh",
mesh.points,
mesh.cells_dict["triangle"]
)

nodes = lbvh.face_nodes


def traverse_lbvh(node, max_depth):
if node.is_inner and max_depth > 0:
V_left, E_left = traverse_lbvh(nodes[node.left], max_depth - 1)
V_right, E_right = traverse_lbvh(nodes[node.right], max_depth - 1)
return np.vstack([V_left, V_right]), np.vstack([E_left, E_right + V_left.shape[0]])

E = np.array([
[0, 1],
[0, 2],
[0, 3],
[1, 5],
[1, 4],
[2, 4],
[2, 6],
[3, 5],
[3, 6],
[7, 4],
[7, 5],
[7, 6],
])
V = np.array([
node.aabb_min,
[node.aabb_min[0], node.aabb_min[1], node.aabb_max[2]],
[node.aabb_min[0], node.aabb_max[1], node.aabb_min[2]],
[node.aabb_max[0], node.aabb_min[1], node.aabb_min[2]],
[node.aabb_min[0], node.aabb_max[1], node.aabb_max[2]],
[node.aabb_max[0], node.aabb_min[1], node.aabb_max[2]],
[node.aabb_max[0], node.aabb_max[1], node.aabb_min[2]],
node.aabb_max
])
return V, E


max_depth = 0
bvh_nodes, bvh_edges = traverse_lbvh(nodes[0], max_depth=max_depth)

ps.register_curve_network("bvh", bvh_nodes, bvh_edges)


def foo():
global max_depth
changed, max_depth = imgui.SliderInt("max depth", max_depth, 0, 20)
if changed:
bvh_nodes, bvh_edges = traverse_lbvh(nodes[0], max_depth=max_depth)
ps.register_curve_network("bvh", bvh_nodes, bvh_edges)


ps.set_user_callback(foo)

ps.show()
81 changes: 81 additions & 0 deletions python/examples/profiler.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import pandas as pd
import plotly.express as px
import pathlib
from io import StringIO

from find_ipctk import ipctk

def plot_profiler(title=None):
df = pd.read_csv(StringIO(ipctk.profiler().csv), na_filter=False)
# df["Time (s)"] = df["Time (ms)"] / 1000.0
# df["Parent"] = df["Parent"].astype(str)
# df["Id"] = df["Id"].astype(str)

fig = px.sunburst(
df,
ids="Id",
names="Name",
parents="Parent",
values="Time (ms)",
branchvalues="total",
color="Parent",
)
fig.update_traces(
hovertemplate="<b>%{label}</b><br>Time (ms): %{value}<br>Call Count: %{customdata[0]}<br>Percentage of Parent: %{percentParent:.2%}",
customdata=df[["Count"]].values,
# tiling=dict(
# orientation='v'
# )
)
fig.update_layout(
title=title or "Profiler Results",
title_x=0.5,
title_y=0.95,
margin=dict(t=0, l=0, r=0, b=0),
width=800,
height=800,
template="plotly_white",
)
# fig.write_image(f"icicle_{pathlib.Path(csv_path).stem}.png", scale=2)
# fig.write_image(f"sunburst_{pathlib.Path(csv_path).stem}.png", scale=2)
fig.show()
return fig

if __name__ == "__main__":
# plot(pathlib.Path(__file__).parent / "lbvh_profile.csv")

import meshio
import argparse
import numpy as np

parser = argparse.ArgumentParser()
parser.add_argument(
"--mesh",
type=pathlib.Path,
default=(pathlib.Path(__file__).parents[2] / "tests/data/puffer-ball/20.ply"))
parser.add_argument(
"--method",
type=str,
default="lvbh")
args = parser.parse_args()

mesh = meshio.read(args.mesh)
faces = mesh.cells_dict["triangle"]

edges = ipctk.edges(faces)
# indices = np.lexsort((edges[:, 1], edges[:, 0]))
# edges = edges[indices]

# indices = np.lexsort((faces[:, 2], faces[:, 1], faces[:, 0]))
# faces = faces[indices]

if args.method.lower() == "bvh":
bp = ipctk.BVH()
elif args.method.lower() == "lbvh":
bp = ipctk.LBVH()

bp.build(mesh.points, edges, faces)

candidates = bp.detect_collision_candidates()

plot_profiler(title=args.mesh.parent.name + "/" + args.mesh.name)
Loading