Drawing a tubular path with Julia
Posted on August 4, 2023
by Stéphane Laurent
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)
= √((1.0 + u ⋅ v) / 2.0)
re = (u × v) / 2.0 / re
w 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)
, _ = size(pts)
ne = [pts[i+1, :] - pts[i, :] for i in 1:(n-1)]
!(e, pts[1, :] - pts[n, :])
push= map(normalize, e)
tangents = map(tgt -> QuaternionF64(0.0, tgt...), tangents)
qtangents = [tangents[i+1, :] ⋅ tangents[i, :] for i in 1:(n-1)]
dotproducts = sqrt.((1 .+ dotproducts) / 2.0)
a = Vector{Vector{Float64}}(undef, n-1)
V for i in 1:(n-1)
= - [imag_part(qtangents[i+1] * qtangents[i])...] /
V[i] 2.0 + 2.0*dotproducts[i])
sqrt(end
= [QuaternionF64(a[i], V[i]...) for i in 1:(n-1)]
Qs # the quaternions psi_k
= Vector{QuaternionF64}(undef, n-1)
Qprodcuts 1] = Qs[1]
Qprodcuts[for i in 2:(n-1)
= Qs[i] * Qprodcuts[i-1]
Qprodcuts[i] end
= Vector{QuaternionF64}(undef, n)
Psi = quaternionFromTo([1; 0; 0], tangents[1])
psi0 1] = psi0
Psi[for i in 1:(n-1)
+1] = Qs[i] * Psi[i]
Psi[iend
# angle defect omega
= zeros(Float64, 3)
init 1])] = 1
init[argmin(tangents[= normalize(tangents[1] × init)
N0 = QuaternionF64(0.0, N0...)
qN0 = Qprodcuts[n-1]
qlast = qlast * qN0 * conj(qlast)
qNN = [imag_part(qNN)...]
NN = NN ⋅ N0
x = normalize(NN × N0)
T0 = T0 × N0
B0 = NN ⋅ B0
y = atan(y, x) + twists * 2.0 * pi
omega # the quaternions psiTilde_k
= map(v -> sqrt(v ⋅ v), e[1:(n-1)])
norms = cumsum([0.0, norms...])
s = -omega * s / (2*s[n])
angles = Vector{QuaternionF64}(undef, n)
PsiTilde 1] = Psi[1]
PsiTilde[for i in 2:n
= angles[i]
angle = Psi[i] *
PsiTilde[i] , sin(angle), 0.0, 0.0)
QuaternionF64(cos(angle)end
# mesh
= zeros(Float64, 3, sides, n-1)
R = QuaternionF64(0.0, 0.0, 1.0, 0.0)
Hj for k in 1:sides
= (k - 1.0) / sides
α = QuaternionF64(cospi(α), sinpi(α), 0.0, 0.0)
r0 = r0 * Hj * conj(r0)
r1 for j in 1:(n-1)
= PsiTilde[j]
psi :, k, j] = pts[j, :] +
R[* [imag_part(psi * r1 * conj(psi))...]
radius end
end
= hcat([R[:, :, i] for i in 1:(n-1)]...)
verts = [verts[:, i] for i in 1:((n-1)*sides)]
points = GridTopology((sides, n-1), (true, true))
quads return SimpleMesh([Meshes.Point(pt...) for pt in points], quads)
end
Here is an example, a knot:
using MeshViz
using GLMakie
= collect(LinRange(0, 2*pi, 151)[1:150])
theta =
knot
[+ 2*sin.(2*theta)]
[sin.(theta) , [2*sin.(3*theta)]
, [cos.(theta) - 2*cos.(2*theta)]
]
= closedTube(knot, 60, 0.35, 0)
mesh 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)
function(pts, nsides, epsilon, twist = 0) {
closedTubeMesh <- nrow(pts)
n <-# tangents
rbind(
e <-t(vapply(1L:(n-1L), function(i) pts[i+1L, ]-pts[i, ], numeric(3L))),
-pts[n, ]
pts[1L, ]
) sqrt(apply(e, 1L, crossprod))
nrms <- e / nrms
Tgs <- as.quaternion(rbind(0, t(Tgs)))
Tgs_quat <-# the quaternions q
vapply(
sprods <-:(n-1L), function(i) c(crossprod(Tgs[i+1L, ], Tgs[i, ])), numeric(1L)
1L
) sqrt((1 + sprods) / 2)
a <- quaternion(length.out = n-1L)
v <-for(i in 1L:(n-1L)) {
-1 / sqrt(2 + 2*sprods[i]) * Im(Tgs_quat[i+1L] * Tgs_quat[i])
v[i] <-
} a + v
q <-# the psi_k
Conj(onion_cumprod(Conj(q)))
qpr <- quaternion(length.out = n)
Psi <- cgalMeshes:::quaternionFromTo(c(1, 0, 0), Tgs[1L, ])
psi0 <- psi0
Psi[1L] <-for(i in 1L:(n-1L)) {
+1L] <- qpr[i] * psi0
Psi[i
}# omega (angle defect)
c(0, 0, 0)
init <-which.min(abs(Tgs[1L, ]))] <- 1
init[ cgalMeshes::crossProduct(Tgs[1L, ], init)
N0 <- N0 / sqrt(c(crossprod(N0)))
N0 <- as.quaternion(c(0, N0), single = TRUE)
N0_quat <- qpr[n-1L]
qN <- qN * N0_quat * Conj(qN)
NN_quat <- as.numeric(NN_quat)[-1L]
NN <- c(crossprod(NN, N0))
x <- cgalMeshes::crossProduct(NN, N0)
T0 <- T0 / sqrt(c(crossprod(T0)))
T0 <- cgalMeshes::crossProduct(T0, N0)
B0 <- c(crossprod(NN, B0)) # x^2+y^2=1
y <- atan2(y, x) + twist*2*pi
omega <-# the quaternions psiTilde_k
cumsum(c(0, apply(e[-n, ], 1L, crossprod)))
s <- s[n]
L <- -omega * s / (2*L)
angles <- quaternion(length.out = n)
PsiTilde <- Psi[1L]
PsiTilde[1L] <-for(i in 2L:n) {
angles[i]
a <-
PsiTilde[i] <- Psi[i] * as.quaternion(c(cos(a), sin(a), 0, 0), single = TRUE)
}# the mesh
n
nu <- TRUE
uperiodic <- 1L:nu
u_ <- TRUE
vperiodic <- as.integer(nsides)
nv <- 1L:nv
v_ <- array(NA_real_, dim = c(3L, nv, nu))
R <-for(k in 1L:nv) {
(k - 1L) / nv
a <- as.quaternion(c(cospi(a), sinpi(a), 0, 0), single = TRUE)
r0 <- r0 * Hj * Conj(r0)
r1 <-for(j in 1L:nu) {
PsiTilde[j]
psi <-
R[, k, j] <- pts[j, ] + epsilon * as.numeric(psi * r1 * Conj(psi))[-1L]
}
} matrix(R, nrow = 3L, ncol = nu*nv)
vs <- cgalMeshes:::meshTopology(nu, nv, uperiodic, vperiodic)
tris <-tmesh3d(
vertices = vs,
indices = tris,
homogeneous = FALSE
)
}
################################################################################
seq(0, 2*pi, length.out = 751L)[-1L]
theta <- cbind(
knot <-sin(theta) + 2*sin(2*theta),
2*sin(3*theta),
cos(theta) - 2*cos(2*theta)
) closedTubeMesh(knot, nsides = 4, epsilon = 0.55, twist = 2)
mesh <-shade3d(mesh, color = "green")