Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
49 changes: 49 additions & 0 deletions src/ambient_occlusion.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
#include "default_types.h"
#include <igl/ambient_occlusion.h>
#include <nanobind/nanobind.h>
#include <nanobind/eigen/dense.h>

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<const Eigen::MatrixXN> &V,
const nb::DRef<const Eigen::MatrixXI> &F,
const nb::DRef<const Eigen::MatrixXN> &P,
const nb::DRef<const Eigen::MatrixXN> &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))");
}
18 changes: 18 additions & 0 deletions tests/test_all.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
18 changes: 2 additions & 16 deletions tutorial/tut-chapter1.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand All @@ -440,4 +426,4 @@
},
"nbformat": 4,
"nbformat_minor": 2
}
}
Loading