---
title: "Mesh geometry"
output: rmarkdown::html_vignette
vignette: >
  %\VignetteIndexEntry{Mesh geometry}
  %\VignetteEngine{knitr::rmarkdown}
  %\VignetteEncoding{UTF-8}
---

```{r setup, include = FALSE}
knitr::opts_chunk$set(
  collapse = TRUE,
  comment = "#>",
  fig.width = 7,
  fig.height = 3.2,
  dpi = 96,
  out.width = "100%"
)
library(ravetools)
oldpar <- graphics::par(no.readonly = TRUE)
```

The `vcg_*` and `mris_*` families are `ravetools`' low-level geometry layer.
They are the building blocks that the surface pipelines are assembled from, and
they are usable directly. This article walks the whole family, grouped the way
the `README` table groups it: construct, measure, repair, smooth, query,
subset, and draw.

Every mesh these functions return carries the `ravetools_mesh3d` class, which
is an `rgl`-style `mesh3d` -- a `3 x n` vertex matrix `vb` and a `3 x m`
one-based triangle matrix `it` -- so `plot()` renders it in base `R` graphics
with no `rgl` dependency. `ensure_mesh3d` coerces the other common surface
formats (`fs.surface` from `freesurferformats`, `ieegio_surface` from `ieegio`,
`surf.asc`) into the same layout, so a `FreeSurfer` surface drops into any
function below.

## 1. Two primitives

The cheapest way to get a mesh is to make one. `vcg_sphere` returns a
subdivided `icosphere` of unit radius centered at the origin, and
`plane_geometry` returns a flat triangulated grid in the `z = 0` plane.

```{r primitives}
sphere <- vcg_sphere(sub_division = 4)
sphere

plane <- plane_geometry(width = 2.6, height = 2.6, shape = c(40, 40))
plane
```

`shape` counts vertices per side, so a `40 x 40` grid is `39 x 39` cells and
twice that many triangles. Tilt the plane and lift it, and it slices through
the sphere:

```{r tilt}
tilt <- new_matrix4()$make_rotation_y(25 * pi / 180)$to_array()
plane$vb <- (tilt %*% rbind(plane$vb[1:3, ], 1))[1:3, ] + c(0, 0, 0.35)
```

`new_matrix4` is one of the in-place geometry classes (`new_vector3`,
`new_matrix4`, `new_quaternion`); `to_array` hands back the plain `4x4` matrix.

Faces alone do not carry orientation. `vcg_update_normals` computes per-vertex
normals, which the smoothing, projection, and ray-casting routines all read:

```{r normals}
plane <- vcg_update_normals(plane, weight = "area")
dim(plane$normals)
```

## 2. Asking questions about geometry

Three functions answer "where is this thing relative to that thing", and they
trade accuracy against cost differently.

### Exact collision

`vcg_detect_collision` reports, for every element of `y`, whether it comes
within `radius` of `x`, together with the exact minimum distance. Either side
can be a point cloud, a chain of connected line segments, or a triangular mesh,
so one call covers all nine pairings. Both sides here are meshes, so answers
come back one per triangle of `y`:

```{r collision}
cut <- vcg_detect_collision(sphere, plane, radius = 0.02)
cut$summary$y$unit_type
sum(cut$hit_unit)
head(cut$representation)
```

`representation` has one row per hit: `unit` is the plane triangle, `index` is
the vertex of that triangle nearest the sphere, `distance` is the true minimum
distance, and `x_index` is the sphere face that was hit. Keeping those triangles
gives the intersection band as a mesh in its own right:

```{r band}
band <- plane
band$it <- plane$it[, which(cut$hit_unit), drop = FALSE]
band
```

The `radius` argument makes this a proximity test rather than a contact test.
Raising it thickens the band, because more triangles fall inside the tolerance:

```{r radius}
vapply(c(0.02, 0.1, 0.3), function(r) {
  sum(vcg_detect_collision(sphere, plane, radius = r)$hit_unit)
}, 0L)
```

Line segments are the case worth knowing about, because that is how diffusion
streamlines and electrode shafts arrive. Several chains share one matrix and
are delimited by rows of `NA`:

```{r streamlines}
streamlines <- rbind(
  cbind(seq(-3, 3, by = 0.5), 0, 0),
  c(NA, NA, NA),
  cbind(seq(-3, 3, by = 0.5), 5, 0)
)
res <- vcg_detect_collision(sphere, streamlines,
                            mode_y = "segments", radius = 0.1)
res$hit_unit
res$representation
```

The first chain runs through the sphere and the second passes well clear of it.
`test_level` controls how hard the scan works: `"element"` (the default)
measures every segment and reports the closest, `"unit"` stops at the first hit
inside each chain, and `"whole"` stops at the first hit anywhere and answers
only the yes-or-no question:

```{r test-level}
vcg_detect_collision(sphere, streamlines, mode_y = "segments",
                     radius = 0.1, test_level = "whole")$collide
```

Finally, `include_interior` decides whether geometry buried inside a closed `x`
counts as a collision even when it never approaches the surface. The center of
the sphere is one unit away from every point of it:

```{r interior}
center <- rbind(c(0, 0, 0))
vcg_detect_collision(sphere, center, radius = 0.1)$collide
vcg_detect_collision(sphere, center, radius = 0.1,
                     include_interior = TRUE)$collide
```

That test uses ray casting, so it needs `x` to be `watertight` with coherently
oriented faces -- see `vcg_fix_defects` below for repairing one that is not.

### Nearest neighbors

`vcg_kdtree_nearest` builds a `K-D` tree over `target` and, for each point of
`query`, returns the `k` closest target points and their distances. Unlike
`vcg_detect_collision` it measures to the nearest *vertex*, not to the surface,
so it is an approximation -- a cheaper one that is usually good enough, and the
right tool when the identity of the nearest vertex is what is wanted:

```{r kdtree}
kd <- vcg_kdtree_nearest(target = sphere, query = plane, k = 1)
str(kd)
range(kd$distance)
```

Asking for more than one neighbor widens both matrices to `k` columns, which is
how you get a local neighborhood to average over:

```{r kdtree-k}
kd3 <- vcg_kdtree_nearest(target = sphere, query = plane, k = 3)
head(kd3$index)
```

Both arguments accept either a mesh or a plain `n x 3` matrix, and 2D points
work as well as 3D.

### Ray casting

`vcg_raycaster` shoots a ray from each origin along each direction and reports
the first face it meets. Lift the plane clear of the sphere and drop a ray
straight down from every vertex:

```{r raycast}
above <- plane$vb
above[3, ] <- above[3, ] + 1.8
rays <- vcg_raycaster(sphere, ray_origin = above, ray_direction = c(0, 0, -1))
str(rays[c("has_intersection", "distance", "face_index")])
sum(rays$has_intersection)
```

Rays whose origin lies over the sphere's silhouette hit it; the rest report
`has_intersection = FALSE`, `NA` for `distance` and `face_index`, and an
`intersection` column that should be ignored. `both_sides = TRUE` also searches
backwards along the ray, and `max_distance` caps how far it travels.

The hits form a point cloud, which is a mesh with vertices and no faces:

```{r pierce}
pierce <- structure(
  list(vb = rays$intersection[, rays$has_intersection, drop = FALSE]),
  class = "mesh3d"
)
```

Putting the three answers side by side:

```{r query-figure, fig.height = 3.6}
col <- color_ramp_continuous(kd$distance[, 1],
                             cmap = c("#f2f2f2", "#7fa8c9", "#2c5f8a"))

graphics::par(mfrow = c(1, 2), mar = c(0.1, 0.1, 2.1, 0.1), cex.main = 0.95)
plot_mesh_polygon(
  list(sphere, plane, band),
  col = list("gray55", col, "#a33a3a"), alpha = c(0.5, 0.92, 1),
  eye = c(3.5, -4, 2.2), up = c(0, 0, 1), zoom = 1.15,
  shadow_color = "white", ambient_intensity = 0.55,
  main = "Distance; cut faces in red"
)
plot_mesh_polygon(
  list(sphere, pierce),
  col = list("gray72", "#a33a3a"), cex = 0.03,
  eye = c(3.5, -4, 2.2), up = c(0, 0, 1), zoom = 0.68,
  shadow_color = "white", ambient_intensity = 0.55,
  main = "Ray entry points"
)
```

## 3. Building a mesh from a volume

Real surfaces come from volumes. `vcg_isosurface` runs marching cubes over a
3D array and returns the triangulation, mapping `voxel` indices into `RAS`
through `vox_to_ras`:

```{r isosurface}
data("left_hippocampus_mask", package = "ravetools")
dim(left_hippocampus_mask)

raw_mesh <- vcg_isosurface(left_hippocampus_mask)
raw_mesh
```

`mesh_from_volume` wraps the same step together with re-sampling and smoothing,
which is convenient when the goal is a display surface rather than an exact
level set:

```{r mesh-from-volume}
smoothed <- mesh_from_volume(
  left_hippocampus_mask, output_format = "rgl", threshold = 0.5,
  remesh = TRUE, remesh_voxel_size = 1, smooth = TRUE, verbose = FALSE
)
smoothed
```

`mris_make_surfaces` is the fourth constructor. It deforms an existing surface
along the intensity gradient of a volume to produce paired white and `pial`
surfaces, the way a cortical reconstruction does.

## 4. Measuring and diagnosing

Before doing anything to a mesh it is worth asking what shape it is in.
`vcg_count_edge_defects` counts the two things that break most algorithms:

```{r defects}
vcg_count_edge_defects(raw_mesh)
```

Forty boundary edges means the marching-cubes output has holes, so it is not
closed. `vcg_mesh_volume` says so itself rather than quietly returning a
number that means nothing:

```{r measure}
vcg_mesh_volume(raw_mesh)
vcg_average_edge_length(raw_mesh)
vcg_max_edge_length(raw_mesh)
```

The two edge-length measures are the ones to check before any operation with a
length parameter, since they say what scale the mesh is sampled at.

`mris_curvature` returns the four standard curvature fields per vertex --
mean, `Gaussian`, and the two principal curvatures:

```{r curvature-fields}
curv <- mris_curvature(raw_mesh)
str(curv)
```

## 5. Repairing and re-meshing

`vcg_fix_defects` merges duplicate vertices, fills holes, and reorients faces,
and it reports what it did in an `info` attribute:

```{r fix}
mesh <- vcg_fix_defects(raw_mesh, verbose = FALSE)
info <- attr(mesh, "info")
info[c("boundary_edges_before", "boundary_edges_after",
       "holes_filled", "is_closed_manifold")]

# the same call that warned above, now on a closed surface
vcg_mesh_volume(mesh)
```

The mesh is a closed `manifold` now, so its volume is trustworthy and
`include_interior` collision tests will work on it. Center it for the plots that
follow:

```{r center}
mesh$vb[1:3, ] <- mesh$vb[1:3, ] - rowMeans(mesh$vb[1:3, ])
```

Four routines change the sampling. They differ in what they preserve:

```{r remesh}
uniform  <- vcg_uniform_remesh(mesh, voxel_size = 1, verbose = FALSE)
split    <- vcg_subdivision(mesh, method = "edge")
capped   <- vcg_subdivide_max_edge_length(mesh, max_edge_len = 0.8)
isotropic <- mris_remesh(mesh, target_edge_length = 1.5, verbose = FALSE)

data.frame(
  method = c("input", "vcg_uniform_remesh", "vcg_subdivision",
             "vcg_subdivide_max_edge_length", "mris_remesh"),
  vertices = c(ncol(mesh$vb), ncol(uniform$vb), ncol(split$vb),
               ncol(capped$vb), ncol(isotropic$vb)),
  avg_edge = round(vapply(list(mesh, uniform, split, capped, isotropic),
                          vcg_average_edge_length, 0), 3),
  max_edge = round(vapply(list(mesh, uniform, split, capped, isotropic),
                          vcg_max_edge_length, 0), 3)
)
```

`vcg_uniform_remesh` re-samples through a distance field on a `voxel` grid, so
it also repairs topology at the cost of exact geometry. `vcg_subdivision`
splits every edge, doubling resolution everywhere. `vcg_subdivide_max_edge_length`
splits only edges above a threshold, leaving well-sampled regions alone.
`mris_remesh` implements the isotropic re-meshing of `Botsch` and `Kobbelt`
(2003), driving every edge toward one target length, and is the one to reach for
when downstream code cares about triangle quality.

## 6. Smoothing and deforming

Two smoothers come from `vcglib` and one from the cortical-surface literature:

```{r smooth}
taubin <- vcg_smooth_explicit(mesh, type = "taubin", iteration = 10)
implicit <- vcg_smooth_implicit(mesh, lambda = 0.2, degree = 2)
fs_style <- mris_smooth(mesh, niterations = 20L)

vapply(list(mesh, taubin, implicit, fs_style), vcg_mesh_volume, 0)
```

`vcg_smooth_explicit` applies a per-vertex `Laplacian` step repeatedly;
`type = "taubin"` alternates a positive and a negative step so the surface does
not shrink. `vcg_smooth_implicit` solves for the smoothed positions in one
sparse solve, which is stable at much larger `lambda`. Both preserve the vertex
count; only the positions move.

`mris_inflate` goes further, flattening the folds while preserving total
surface area. It also returns the `sulc` depth field -- how far each vertex
traveled -- which is what makes an inflated surface readable:

```{r inflate}
inflated <- mris_inflate(fs_style, n_averages = 4L, niterations = 8L,
                         scale_brain = FALSE, verbose = FALSE)
names(inflated)
range(inflated$sulc)
```

`mris_sphere` continues the deformation all the way onto a sphere of
`target_radius`, which is the mapping that surface-based registration is built
on. How close it gets is worth checking, because the residual spread in vertex
radius is the honest measure of convergence:

```{r sphere-map}
spherical <- mris_sphere(fs_style, target_radius = 100, verbose = FALSE)
radius <- sqrt(colSums(spherical$vb[1:3, ]^2))
c(min = min(radius), max = max(radius), cv = stats::sd(radius) / mean(radius))
```

Under two percent here. Both `mris_inflate` and `mris_sphere` were designed for
cortical surfaces, where the input is large, smooth, and genuinely
sphere-like; a small closed structure such as this hippocampus maps only
approximately, and raising `niterations` past the default makes it worse rather
than better on such an input.

Mapped side by side with mean curvature, the first two stages show what
inflation preserves:

```{r inflate-figure, fig.height = 3.2}
curv <- mris_curvature(fs_style)
lim <- stats::quantile(abs(curv$mean), 0.95)
curv_col <- color_ramp_continuous(
  curv$mean, clim = c(-lim, lim),
  cmap = c("#2c5f8a", "#f2f2f2", "#a33a3a")
)

graphics::par(mfrow = c(1, 2), mar = c(0.1, 0.1, 2.1, 0.1))
plot_mesh_polygon(fs_style, col = curv_col, eye = c(0, 100, 30),
                  up = c(0, 0, 1), zoom = 1.1, main = "Mean curvature")
plot_mesh_polygon(inflated$mesh, col = curv_col, eye = c(0, 100, 30),
                  up = c(0, 0, 1), zoom = 1.1, main = "The same, inflated")
```

## 7. Subsetting and extracting

`vcg_subset_vertex` keeps the vertices a logical selector marks and the faces
whose corners all survive:

```{r subset}
selector <- mesh$vb[1, ] > 0
half <- vcg_subset_vertex(mesh, selector)
c(input = ncol(mesh$vb), kept = ncol(half$vb))
```

`vcg_mesh_patch` cuts a mesh along a closed loop of `waypoints`. They are
snapped to the nearest vertices, the loop between them is walked, and the result
is a two-element list: the enclosed patch and everything else.

```{r patch}
target <- vcg_uniform_remesh(vcg_sphere(), verbose = FALSE)
patches <- vcg_mesh_patch(target, waypoints = diag(1, 3))
vapply(patches, function(p) ncol(p$it), 0L)
```

```{r patch-figure, fig.height = 3.4}
graphics::par(mar = c(0.1, 0.1, 0.1, 0.1))
plot_mesh_polygon(patches, col = list("#a33a3a", "gray70"),
                  alpha = c(1, 0.55), eye = c(10, 10, 10), zoom = 1.2,
                  shadow_color = "white", ambient_intensity = 0.55)
```

`dijkstras_surface_distance` walks the mesh graph from a start vertex and
returns the `geodesic` distance to every other vertex; `surface_path` then reads
the shortest path to any target back out of that result. Note that it takes the
transposed matrices -- one row per vertex and one row per face:

```{r dijkstra}
dist <- dijkstras_surface_distance(
  positions = t(mesh$vb[1:3, ]),
  faces = t(mesh$it),
  start_node = 1,
  face_index_start = 1
)
path <- surface_path(dist, target_node = ncol(mesh$vb))
c(vertices_on_path = length(path$path), length = max(path$distance))
```

Because the walk follows edges, the answer depends on the sampling -- which is
the practical reason to run `mris_remesh` before measuring distances across a
surface.

## 8. Drawing

`plot_mesh_polygon` projects every triangle with an orthographic camera, shades
it by how directly it faces that camera, depth-sorts everything, and draws it in
a single `polygon` call. `plot_mesh_dotcloud` draws vertices as rim-lit dots
instead. `plot()` dispatches to whichever suits the mesh.

Both take a list of meshes and render them into one shared depth space, so
several surfaces compose correctly. For one mesh, `col` is a single color, a
character vector of one color per vertex, or any other vector, which is read as
a depth gradient; for a list of meshes, pass a list of those, one element per
mesh. `alpha` is one value per mesh. A mesh with no face matrix is drawn as one
small `vcg_sphere` per vertex, scaled by `cex` -- which is how the ray hits in
section 2 were rendered.

Two controls are worth knowing. `mesh_clipping` discards triangles whose normal
points along the camera ray, peeling the front cap off a closed surface; pair it
with `side = "both"` so the exposed back wall is drawn rather than culled in
turn. `clipping_plane` culls faces against arbitrary world-space planes instead
-- each a length-5 vector of a normal, a signed offset, and which half-space to
keep -- and `clipping_plane_enabled` exempts individual meshes from it, which is
how electrodes stay whole while the surface around them is cut away.

Scattering some probes inside the surface -- found with the same interior test
from section 2 -- shows why the exemption matters: cut the surface open and the
probes stay whole.

```{r clipping, fig.height = 3.4}
bbox <- apply(fs_style$vb[1:3, ], 1L, range)
set.seed(1)
candidates <- cbind(
  stats::runif(600, bbox[1, 1], bbox[2, 1]),
  stats::runif(600, bbox[1, 2], bbox[2, 2]),
  stats::runif(600, bbox[1, 3], bbox[2, 3])
)
inside <- vcg_detect_collision(fs_style, candidates,
                               include_interior = TRUE)$hit_unit %in% TRUE
probes <- structure(
  list(vb = t(candidates[inside, , drop = FALSE])),
  class = "mesh3d"
)
sum(inside)

eye <- c(0, 100, 30)
graphics::par(mfrow = c(1, 3), mar = c(0.1, 0.1, 2.1, 0.1), cex.main = 0.95)

plot_mesh_polygon(fs_style, col = "steelblue", eye = eye, up = c(0, 0, 1),
                  zoom = 1.1, main = "Whole surface")

plot_mesh_polygon(
  list(fs_style, probes),
  col = list("steelblue", "#a33a3a"), cex = 1.1,
  eye = eye, up = c(0, 0, 1), zoom = 1.1,
  clipping_plane = c(0, 1, 0, 0, -1),
  clipping_plane_enabled = c(TRUE, FALSE),
  main = "Cut open, probes exempt"
)

# `plot_mesh_dotcloud` has no `main`; add the title afterwards
plot_mesh_dotcloud(fs_style, col = "steelblue", eye = eye, up = c(0, 0, 1),
                   zoom = 1.1, cex = 0.45)
graphics::title(main = "plot_mesh_dotcloud")
```

If `rgl` is installed, `rgl_view` and `rgl_call` drive an interactive window
using the same meshes, and `rgl_plot_normals` draws the normal field:

```{r rgl, eval = FALSE}
rgl_view({
  rgl_call("shade3d", mesh, col = "steelblue")
  rgl_call("wire3d", mesh, col = "black")
})
```

```{r cleanup, include = FALSE}
graphics::par(oldpar)
```

## References

`Botsch`, M, and `Kobbelt`, L (2003). A `remeshing` approach to
`multiresolution` modeling. *Proceedings of the 2004
`Eurographics`/`ACM` `SIGGRAPH` Symposium on Geometry Processing*, 185-192.

`Fischl`, B, `Sereno`, MI, and Dale, AM (1999). Cortical surface-based analysis
II: inflation, flattening, and a surface-based coordinate system.
*`NeuroImage`*, 9(2), 195-207.

The `vcg_*` functions are built on
[`vcglib`](https://github.com/cnr-isti-vclab/vcglib) from the Visual Computing
Lab, `ISTI`.
