diff --git a/src/ambient_occlusion.cpp b/src/ambient_occlusion.cpp new file mode 100644 index 00000000..79e933c7 --- /dev/null +++ b/src/ambient_occlusion.cpp @@ -0,0 +1,49 @@ +#include "default_types.h" +#include +#include +#include + +namespace nb = nanobind; +using namespace nb::literals; + +namespace pyigl +{ + // Core (embree-free) ambient occlusion: builds an AABB over (V,F) and shoots + // rays with libigl's own intersector, so it is available in every wheel + // regardless of whether the embree module was built. + auto ambient_occlusion( + const nb::DRef &V, + const nb::DRef &F, + const nb::DRef &P, + const nb::DRef &N, + const int num_samples) + { + Eigen::VectorXN S; + igl::ambient_occlusion(V, F, P, N, num_samples, S); + return S; + } +} + +void bind_ambient_occlusion(nb::module_ &m) +{ + m.def( + "ambient_occlusion", + &pyigl::ambient_occlusion, + "V"_a, + "F"_a, + "P"_a, + "N"_a, + "num_samples"_a, + R"(Compute ambient occlusion per given point for a mesh (V,F). + +This is the embree-free implementation from igl::core; for the (typically +faster) embree-accelerated version see igl.embree.ambient_occlusion. + +@param[in] V #V by 3 list of mesh vertex positions +@param[in] F #F by 3 list of mesh triangle indices into rows of V +@param[in] P #P by 3 list of origin points +@param[in] N #P by 3 list of origin normals +@param[in] num_samples number of rays to shoot per point (e.g. 1000) +@return S #P list of ambient occlusion values between 1 (fully occluded) and + 0 (not occluded))"); +} diff --git a/tests/test_all.py b/tests/test_all.py index 370c7431..a6d7f120 100644 --- a/tests/test_all.py +++ b/tests/test_all.py @@ -1791,3 +1791,21 @@ def test_resolve_duplicated_faces(): F = np.array([[0, 1, 2], [0, 1, 2], [0, 1, 2], [3, 4, 5]], dtype=np.int64) F2, J = igl.resolve_duplicated_faces(F) assert set(map(tuple, F2.tolist())) == {(3, 4, 5)} + + +def test_ambient_occlusion(): + # Core (embree-free) ambient occlusion available directly on `igl`. + V, F = igl.icosahedron() + N = igl.per_vertex_normals(V, F) + P = V + 1e-4 * N + + S = igl.ambient_occlusion(V, F, P, N, 256) + assert S.shape == (V.shape[0],) + assert np.all(S >= 0.0) and np.all(S <= 1.0) + + # A convex mesh sampled along outward normals occludes nothing. + assert S.mean() < 0.05 + # Flipping the normals inward makes the same points substantially occluded. + S_in = igl.ambient_occlusion(V, F, P, -N, 256) + assert S_in.mean() > 0.1 + assert S_in.mean() > S.mean() diff --git a/tutorial/tut-chapter1.ipynb b/tutorial/tut-chapter1.ipynb index 65f3647b..26283d30 100644 --- a/tutorial/tut-chapter1.ipynb +++ b/tutorial/tut-chapter1.ipynb @@ -402,21 +402,7 @@ "execution_count": null, "metadata": {}, "outputs": [], - "source": [ - "v, f = igl.read_triangle_mesh(os.path.join(root_folder, \"data\", \"bunny_small.off\"))\n", - "\n", - "## Select a vertex from which the distances should be calculated\n", - "vs = np.array([0])\n", - "##All vertices are the targets\n", - "vt = np.arange(v.shape[0])\n", - "\n", - "d = igl.exact_geodesic(v, f, vs, vt)#, fs, ft)\n", - "\n", - "strip_size = 0.02\n", - "##The function should be 1 on each integer coordinate\n", - "c = np.abs(np.sin((d / strip_size * np.pi)))\n", - "plot(v, f, c, shading={\"wireframe\": False})" - ] + "source": "v, f = igl.read_triangle_mesh(os.path.join(root_folder, \"data\", \"bunny_small.off\"))\n\n## Select a vertex from which the distances should be calculated\nvs = np.array([0])\n##All vertices are the targets\nvt = np.arange(v.shape[0])\n\nd = igl.exact_geodesic(v, f, VS=vs, VT=vt)\n\nstrip_size = 0.02\n##The function should be 1 on each integer coordinate\nc = np.abs(np.sin((d / strip_size * np.pi)))\nplot(v, f, c, shading={\"wireframe\": False})" } ], "metadata": { @@ -440,4 +426,4 @@ }, "nbformat": 4, "nbformat_minor": 2 -} +} \ No newline at end of file