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.
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.