So, I just dump my code for the function MyTube
. The function creates a tube around a polygonal line.
This does not really answer the OP's question, but the resulting GraphicsComplex
1.) gets rendered more fluently in Mathematica and 2.) can be exported by standard means to other mesh formats (e.g., 3ds, obj, stl...). So, it might become a piece in a more complex export pipeline.
The function MyTube
employs a (discrete) Bishop frame along the polygonal curve (slightly twisted in case the option "Closed"
is set to True
in order to produce a water-tight surface). The code doesn't look very elegant but it does the job and it has decent performance.
ClearAll[MyTube]
MyTube[pts_, OptionsPattern[{
"Radius" -> 0.025,
"Closed" -> False,
"InitialVector" -> Automatic,
Mesh -> 32
}]] :=
Module[{p, ν, nn, mm, dp, τ, b, e, ϕ, w, , u,
v, τ0, e0, A, a, q, u0, v0, normals, fflist, radius, Alist,
closedQ, angles, λ, ω, mostτ, restτ},
p = pts;
closedQ = OptionValue["Closed"];
radius = OptionValue["Radius"];
nn = OptionValue["Mesh"];
ν = Transpose[{Cos[#], Sin[#]} &[Most[Subdivide[0., 2. Pi, nn]]]];
If[closedQ, p = Join[{p[[-2]]}, p, {p[[2]]}], p = p;];
dp = Differences[p];
τ = cNormalize3[dp];
mostτ = Most[τ];
restτ = Rest[τ];
If[Length[τ] > 1,
b = cNormalize3[cCross3[mostτ, restτ]];
e = cCross3[b, Most[τ]];
ϕ = cTripleAngle3[mostτ, restτ, b]
,
b = {}; e = {}; ϕ = {};
];
w = NDSolve`FEM`MapThreadDot[
rotationMatrix3DAngleVector[0.5 ϕ, b], e/Cos[0.5 ϕ]];
τ0 = τ[[1]] ;
u0 = OptionValue["InitialVector"];
If[u0 === Automatic, u0 = N[IdentityMatrix[3]][[Ordering[Abs[τ[[1]]], 1][[1]]]]; ];
u0 = Normalize[u0 - τ0 τ0.u0]; v0 = Cross[τ0, u0];
Alist = If[Length[τ] > 1, rotationMatrix3DAngleVector[ϕ, b], {}];
If[Length[Alist] >= 1,
{u, v} = Compile[{{u0, _Real, 1}, {A, _Real, 3}},
Block[{U = u0}, Join[{u0}, Table[U = A[[i]].U, {i, 1, Length[A]}]]],
RuntimeAttributes -> {Listable},
Parallelization -> True
][{u0, v0}, Alist];
(* {u,v} is now a Bishop frame. If the curve is closed,
we have to twist it a bit in order to get a continuous frame. *)
If[closedQ,(*Then*)
λ = Sqrt[Dot[dp^2, ConstantArray[1., 3]]];
λ = 0.5 (Most[λ] + Rest[λ]);
ω = cTripleAngle3[u[[1]], u[[-2]], τ[[1]]];
angles = ω Join[ConstantArray[0., 1], Accumulate[λ]/Total[Most[λ]]];
{u, v} = {Cos[angles] u - Sin[angles] v, Sin[angles] u + Cos[angles] v};
];
A = With[{Part = Compile`GetElement},
Compile[{{u, _Real, 1}, {v, _Real, 1}, {w, _Real, 1}, {e, _Real, 1}, {b, _Real, 1}, {ϕ, _Real}},
If[ϕ < 10^(-12),
{u, v},
{u, v}.Table[e[[i]] w[[j]] + b[[i]] b[[j]], {i, 1, 3}, {j, 1, 3}] ],
RuntimeAttributes -> {Listable},
Parallelization -> True
][u[[1 ;; Length[ϕ]]], v[[1 ;; Length[ϕ]]],
w[[1 ;; Length[ϕ]]], e[[1 ;; Length[ϕ]]],
b[[1 ;; Length[ϕ]]], ϕ]
];
,
{u, v} = Developer`ToPackedArray[{{u0, u0}, {v0, v0}}];
A = {u, v};
];
normals = Flatten[Table[ν.A[[i]], {i, 1, Length[ϕ]}], 1];
a = -radius ν;
q = Flatten[Table[ConstantArray[p[[i + 1]], nn] + a.A[[i]], {i, 1, Length[ϕ]}], 1];
If[Length[p] == 2, q = {}; u = {u0}; v = {v0}];
If[closedQ,
mm = Length[ϕ] - 1;
q = q[[1 ;; -1 - nn]];
normals = normals[[1 ;; -2]];
fflist = Join[
getOpenTubeFaces[mm, nn],
ReplaceAll[
ReplaceAll[
getOpenTubeFaces[2, nn],
Dispatch[ Thread[Range[nn] -> Range[1 + (mm - 1) nn, nn + (mm - 1) nn]]]],
Dispatch[Thread[Range[nn + 1, 2 nn] -> Range[nn]]]
]
];
,
mm = Length[ϕ] + 2;
q = Join[ConstantArray[p[[1]], nn] + a.{u[[1]], v[[1]]}, q, ConstantArray[p[[-1]], nn] + a.{u[[-1]], v[[-1]]}];
normals = Join[ν.{u[[1]], v[[1]]}, normals, ν.{u[[-1]], v[[-1]]}];
fflist = getOpenTubeFaces[mm, nn];
];
GraphicsComplex[q, Polygon[fflist], VertexNormals -> normals]
]
Block[{u, uu, v, vv, w, ww, angle},
uu = Table[Compile`GetElement[u, i], {i, 1, 3}];
vv = Table[Compile`GetElement[v, i], {i, 1, 3}];
ww = Table[Compile`GetElement[w, i], {i, 1, 3}];
cNormalize3 = With[{code = Sqrt[Total[uu^2]], ϵ = 10^5 $MachineEpsilon},
Compile[{{u, _Real, 1}},
Block[{l = code},
If[l < ϵ, u 0., u/l]
],
CompilationTarget -> "C",
RuntimeAttributes -> Listable,
Parallelization -> True,
RuntimeOptions -> "Speed"
]
];
cTripleAngle3 = With[{code = ArcTan[uu.vv, Det[{uu, vv, ww}]]},
Compile[{{u, _Real, 1}, {v, _Real, 1}, {w, _Real, 1}},
code,
CompilationTarget -> "C",
RuntimeAttributes -> {Listable},
Parallelization -> True,
RuntimeOptions -> "Speed"
]
];
rotationMatrix3DAngleVector = With[{
ϵ = 1. 10^-14,
r2 = uu.uu,
id = N[IdentityMatrix[3]],
code = N[Simplify[ComplexExpand[RotationMatrix[angle, uu]], Compile`GetElement[u, 1] ∈ Reals]] /. Part -> Compile`GetElement
},
Compile[{{angle, _Real}, {u, _Real, 1}},
If[
Abs[angle] < ϵ || r2 < ϵ,
id,
code
],
CompilationTarget -> "C",
RuntimeAttributes -> {Listable},
Parallelization -> True,
RuntimeOptions -> "Speed"
]
];
];
cCross3 = Compile[{{X, _Real, 1}, {Y, _Real, 1}},
{
-Compile`GetElement[X, 3] Compile`GetElement[Y, 2] + Compile`GetElement[X, 2] Compile`GetElement[Y, 3], Compile`GetElement[X, 3] Compile`GetElement[Y, 1] - Compile`GetElement[X, 1] Compile`GetElement[Y, 3], -Compile`GetElement[X, 2] Compile`GetElement[Y, 1] + Compile`GetElement[X, 1] Compile`GetElement[Y, 2]
},
CompilationTarget -> "C",
RuntimeAttributes -> Listable,
Parallelization -> True,
RuntimeOptions -> "Speed"
];
getOpenTubeFaces = Compile[{{mm, _Integer}, {nn, _Integer}},
Join[
Flatten[Join[
Table[
{{i + 1 + nn (j - 1), i + nn (j - 1), i + nn j}, {i + 1 + nn (j - 1), i + nn j, i + 1 + nn j}},
{i, 1, nn - 1}, {j, 1, mm - 1}],
{Table[{{1 + nn (j - 1), nn + nn (j - 1), nn + nn j}, {1 + nn (j - 1), nn + nn j, 1 + nn j}}, {j, 1, mm - 1}]}
], 2]
],
CompilationTarget -> "C",
RuntimeOptions -> "Speed"
];
Here is the obligatory usage example:
pts = KnotData["FigureEight", "SpaceCurve"] /@ Subdivide[0., 2. Pi, 2000];
gc = MyTube[pts, "Closed" -> True, "Radius" -> 0.1]; // AbsoluteTiming // First
Length @@@ Cases[gc, _Polygon, All]
Graphics3D[{Orange, EdgeForm[], Specularity[White, 30], gc}, Lighting -> "Neutral"]
0.021071
{128000}
Line
byTube
andPoint
bySphere
(with appropriate radii). As far as I remember, three.js supports also OBJ format and you can export directly to it from Mathematica. $\endgroup$