Drawing a tubular path with Julia

[This article was first published on Saturn Elephant, and kindly contributed to R-bloggers]. (You can report issue about the content on this page here)
Want to share your content on R-bloggers? click here if you have a blog, or here if you don't.

I implemented the framed closed curves exposed in this blog post, in Julia and R. In fact it is useless with R, because the rgl function cylinder3d is faster and better.

Here is the Julia implementation:

using LinearAlgebra
using Quaternions
using Meshes

# quaternion corresponding to "the" rotation mapping u to v
function quaternionFromTo(u, v)
  re = √((1.0 + u ⋅ v) / 2.0)
  w = (u × v) / 2.0 / re
  return QuaternionF64(re, w...)
end

# pts: points forming the path
# sides: number of sides of the tube
# radius: tube radius
# twists: number of twists
function closedTube(pts, sides, radius, twists)
  n, _ = size(pts)
  e = [pts[i+1, :] - pts[i, :] for i in 1:(n-1)]
  push!(e, pts[1, :] - pts[n, :])
  tangents = map(normalize, e)
  qtangents = map(tgt -> QuaternionF64(0.0, tgt...), tangents)
  dotproducts = [tangents[i+1, :] ⋅ tangents[i, :] for i in 1:(n-1)]
  a = sqrt.((1 .+ dotproducts) / 2.0)
  V = Vector{Vector{Float64}}(undef, n-1)
  for i in 1:(n-1)
    V[i] = - [imag_part(qtangents[i+1] * qtangents[i])...] / 
      sqrt(2.0 + 2.0*dotproducts[i])
  end
  Qs = [QuaternionF64(a[i], V[i]...) for i in 1:(n-1)]
  # the quaternions psi_k
  Qprodcuts = Vector{QuaternionF64}(undef, n-1)
  Qprodcuts[1] = Qs[1]
  for i in 2:(n-1)
    Qprodcuts[i] = Qs[i] * Qprodcuts[i-1]
  end
  Psi = Vector{QuaternionF64}(undef, n)
  psi0 = quaternionFromTo([1; 0; 0], tangents[1])
  Psi[1] = psi0
  for i in 1:(n-1)
    Psi[i+1] = Qs[i] * Psi[i]
  end
  # angle defect omega 
  init = zeros(Float64, 3)
  init[argmin(tangents[1])] = 1
  N0 = normalize(tangents[1] × init)
  qN0 = QuaternionF64(0.0, N0...)
  qlast = Qprodcuts[n-1]
  qNN = qlast * qN0 * conj(qlast)
  NN = [imag_part(qNN)...]
  x = NN ⋅ N0
  T0 = normalize(NN × N0)
  B0 = T0 × N0
  y = NN ⋅ B0
  omega = atan(y, x) + twists * 2.0 * pi
  # the quaternions psiTilde_k
  norms = map(v -> sqrt(v ⋅ v), e[1:(n-1)])
  s = cumsum([0.0, norms...])
  angles = -omega * s / (2*s[n])
  PsiTilde = Vector{QuaternionF64}(undef, n)
  PsiTilde[1] = Psi[1]
  for i in 2:n
    angle = angles[i]
    PsiTilde[i] = Psi[i] * 
      QuaternionF64(cos(angle), sin(angle), 0.0, 0.0)
  end
  # mesh 
  R = zeros(Float64, 3, sides, n-1)
  Hj = QuaternionF64(0.0, 0.0, 1.0, 0.0)
  for k in 1:sides
    α = (k - 1.0) / sides
    r0 = QuaternionF64(cospi(α), sinpi(α), 0.0, 0.0)
    r1 = r0 * Hj * conj(r0)
    for j in 1:(n-1)
      psi = PsiTilde[j]
      R[:, k, j] = pts[j, :] + 
        radius * [imag_part(psi * r1 * conj(psi))...]
    end
  end
  verts = hcat([R[:, :, i] for i in 1:(n-1)]...)
  points = [verts[:, i] for i in 1:((n-1)*sides)] 
  quads = GridTopology((sides, n-1), (true, true))
  return SimpleMesh([Meshes.Point(pt...) for pt in points], quads)
end

Here is an example, a knot:

using MeshViz
using GLMakie

theta = collect(LinRange(0, 2*pi, 151)[1:150])
knot = 
  [
      [sin.(theta) + 2*sin.(2*theta)]
    , [2*sin.(3*theta)]
    , [cos.(theta) - 2*cos.(2*theta)]
  ]

mesh = closedTube(knot, 60, 0.35, 0)
viz(mesh)

The current version of MeshViz always adds normals to the meshes, so we can’t correctly see what happens if we only set a couple of sides and if we use some twists. So let’s see what this gives with the R version, with four sides and two twists:

Below is the R code. But again, don’t use it, use rgl::cylinder3d instead.

library(onion)
library(rgl)

closedTubeMesh <- function(pts, nsides, epsilon, twist = 0) {
  n <- nrow(pts)
  # tangents
  e <- rbind(
    t(vapply(1L:(n-1L), function(i) pts[i+1L, ]-pts[i, ], numeric(3L))),
    pts[1L, ]-pts[n, ]
  )
  nrms <- sqrt(apply(e, 1L, crossprod))
  Tgs <- e / nrms
  Tgs_quat <- as.quaternion(rbind(0, t(Tgs)))
  # the quaternions q
  sprods <- vapply(
    1L:(n-1L), function(i) c(crossprod(Tgs[i+1L, ], Tgs[i, ])), numeric(1L)
  )
  a <- sqrt((1 + sprods) / 2)
  v <- quaternion(length.out = n-1L)
  for(i in 1L:(n-1L)) {
    v[i] <- -1 / sqrt(2 + 2*sprods[i]) * Im(Tgs_quat[i+1L] * Tgs_quat[i])
  }
  q <- a + v
  # the psi_k
  qpr <- Conj(onion_cumprod(Conj(q))) 
  Psi <- quaternion(length.out = n)
  psi0 <- cgalMeshes:::quaternionFromTo(c(1, 0, 0), Tgs[1L, ])
  Psi[1L] <- psi0
  for(i in 1L:(n-1L)) {
    Psi[i+1L] <- qpr[i] * psi0
  }
  # omega (angle defect)
  init <- c(0, 0, 0)
  init[which.min(abs(Tgs[1L, ]))] <- 1
  N0 <- cgalMeshes::crossProduct(Tgs[1L, ], init)
  N0 <- N0 / sqrt(c(crossprod(N0)))
  N0_quat <- as.quaternion(c(0, N0), single = TRUE)
  qN <- qpr[n-1L]
  NN_quat <- qN * N0_quat * Conj(qN)
  NN <- as.numeric(NN_quat)[-1L]
  x <- c(crossprod(NN, N0))
  T0 <- cgalMeshes::crossProduct(NN, N0)
  T0 <- T0 / sqrt(c(crossprod(T0)))
  B0 <- cgalMeshes::crossProduct(T0, N0) 
  y <- c(crossprod(NN, B0)) # x^2+y^2=1
  omega <- atan2(y, x) + twist*2*pi
  # the quaternions psiTilde_k
  s <- cumsum(c(0, apply(e[-n, ], 1L, crossprod)))
  L <- s[n]
  angles <- -omega * s / (2*L)
  PsiTilde <- quaternion(length.out = n)
  PsiTilde[1L] <- Psi[1L]
  for(i in 2L:n) {
    a <- angles[i]
    PsiTilde[i] <- 
      Psi[i] * as.quaternion(c(cos(a), sin(a), 0, 0), single = TRUE)
  }
  # the mesh
  nu <- n
  uperiodic <- TRUE
  u_ <- 1L:nu
  vperiodic <- TRUE
  nv <- as.integer(nsides)
  v_ <- 1L:nv
  R <- array(NA_real_, dim = c(3L, nv, nu))
  for(k in 1L:nv) {
    a <- (k - 1L) / nv
    r0 <- as.quaternion(c(cospi(a), sinpi(a), 0, 0), single = TRUE)
    r1 <- r0 * Hj * Conj(r0)
    for(j in 1L:nu) {
      psi <- PsiTilde[j]
      R[, k, j] <- 
        pts[j, ] + epsilon * as.numeric(psi * r1 * Conj(psi))[-1L]
    }
  }
  vs <- matrix(R, nrow = 3L, ncol = nu*nv)
  tris <- cgalMeshes:::meshTopology(nu, nv, uperiodic, vperiodic)
  tmesh3d(
    vertices    = vs,
    indices     = tris,
    homogeneous = FALSE
  )
}

################################################################################
theta <- seq(0, 2*pi, length.out = 751L)[-1L]
knot <- cbind(
  sin(theta) + 2*sin(2*theta), 
  2*sin(3*theta), 
  cos(theta) - 2*cos(2*theta)
)
mesh <- closedTubeMesh(knot, nsides = 4, epsilon = 0.55, twist = 2)
shade3d(mesh, color = "green")
To leave a comment for the author, please follow the link and comment on their blog: Saturn Elephant.

R-bloggers.com offers daily e-mail updates about R news and tutorials about learning R and many other topics. Click here if you're looking to post or find an R/data-science job.
Want to share your content on R-bloggers? click here if you have a blog, or here if you don't.

Never miss an update!
Subscribe to R-bloggers to receive
e-mails with the latest R posts.
(You will not see this message again.)

Click here to close (This popup will not appear again)