20
$\begingroup$

This question is a duplicate of a question on stack overflow But since the answer is 6 years old, things may have changed somewhat.

Question

I would like to render Mathematica Graphics3D in three.js.

Attempt

So far I have used

 Export["test.3ds", pl, "3DS"]

on this Mathematica plot.

Mathematica graphics

and I get this (click to animate) Mathematica graphics

Together with the 3ds loader from three.js this only works for mesh like objects, not lines or points for instance and it does not scale very well in terms of performance.

Another possibility seems to be the x3d format which may be loadable using this

We have written a specific three.js code which produces this:

Mathematica graphics

Any other efficient alternatives?

It would be great for instance to be able to export mathematica meshes.

Post Scriptum

Since I have been asked how these lines correspond to the cosmic web, i.e. where galaxies live in the universe cf this simulation.

Mathematica graphics

$\endgroup$
7
  • 1
    $\begingroup$ I am curious what this plot shows (but maybe chat is better for that). $\endgroup$
    – Szabolcs
    Commented Mar 21, 2018 at 12:32
  • $\begingroup$ You can try to use A3dsViewer. Export to the html5 (three.js format) $\endgroup$
    – Slider
    Commented Mar 22, 2018 at 12:20
  • $\begingroup$ Can you give an example of how to use it with a Mathematica object? $\endgroup$ Commented Mar 22, 2018 at 12:26
  • $\begingroup$ if you can export/get *.3ds file from the Mathematica object. Simply do load 3ds file in the A3dsViewer and export to html5(three.js format), it should work. $\endgroup$
    – Slider
    Commented Mar 23, 2018 at 12:40
  • $\begingroup$ Maybe it will work if you replace Line by Tube and Point by Sphere (with appropriate radii). As far as I remember, three.js supports also OBJ format and you can export directly to it from Mathematica. $\endgroup$ Commented Mar 28, 2018 at 8:20

5 Answers 5

7
+100
$\begingroup$

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}

enter image description here

$\endgroup$
13
  • $\begingroup$ it looks like this online: 3dviewer.horizon-simulation.org/3ds/fast-knot.html $\endgroup$
    – chris
    Commented Mar 31, 2018 at 16:33
  • $\begingroup$ I added, cCross3. Looks as if the three.js uses the opposite vertex normals: it renders only the inward side of the faces, not the outward ones... Or maybe something with the export went wrong. The built-in 3D exports are actually rather buggy. $\endgroup$ Commented Mar 31, 2018 at 16:42
  • $\begingroup$ Thanks, @chris. I guess this is not exactly what you were looking for... =/ $\endgroup$ Commented Apr 1, 2018 at 14:48
  • $\begingroup$ Well I was hoping to stimulate a generic approach for the sake of this site, but as far as my needs were concerned I was after an efficient tube like solution. $\endgroup$
    – chris
    Commented Apr 1, 2018 at 15:54
  • 1
    $\begingroup$ @ChipHurst Thank you for pointing that out! At least the two errors should be fixed now. $\endgroup$ Commented Mar 22, 2019 at 16:52
8
$\begingroup$

I faced with the same issue. Using Mathematica's function ExportString[…, "ExpressionJSON"] I attempted to export the whole tree of graphical functions and wrote a parser in JS.

It was uploaded to GitHub, here is the link.

There is a primitive construction with a lot of "switch-case" statements. Each of them implements self function like changing the color or applying a matrix to the group of primitives. After parsing it is rendering to the screen using Three.js library and some part of the code from Mathics project.

Example

  1. Plot some graphics (used a low-poly mode for smaller code)
Graphics3D[{
  SphericalPlot3D[
    2 SphericalHarmonicY[2, 0, t, p], {t, 0, Pi}, {p, 0, 2 Pi}, 
    PerformanceGoal -> "Speed"][[1]],
  Opacity[0.6], 
  Tetrahedron[{{1, 1, 1}, {-1, -1, 1}, {1, -1, -1}, {-1, 1, -1}}]
  }]

image

  1. Export as a JSON string
ExportString[%//N, "ExpressionJSON"]
[
    "Graphics3D",
    [
        "List",
        [
            "GraphicsComplex",
            [
                "List",
                ["List",
                    0.0,
                    0.0,
                    1.2615662610100797
                ]
                ,
                ["List",
                    0.0,
                    0.0,
                    1.2615662610100797
                ]
                ,...
  1. Copy and paste it to data.js
\data.js

var JSONThree = [...
  1. Run index.html

enter image description here

Shorter version

I wrote a figure exporter in Export2ThreeJS.nb file. It stores figure and supplementary libraries into a single .html autonomous page. Tell me, please, If someone knowns how to insert this function into the native Mathematica's menu.

Some figures...

1 2

PS: It has been helping me to communicate with my colleagues which do not have wolfram software a lot.

$\endgroup$
4
  • $\begingroup$ Thanks for this. This is the closest answer to my original question! $\endgroup$
    – chris
    Commented Feb 27, 2020 at 8:16
  • $\begingroup$ You are welcome, @chris! Hope it will help. $\endgroup$ Commented Feb 28, 2020 at 13:07
  • $\begingroup$ good job :), it helps me with learning threejs. $\endgroup$ Commented Jan 17, 2023 at 14:58
  • $\begingroup$ @AsukaMinato, :)) I am very glad to read this $\endgroup$ Commented Jan 17, 2023 at 19:02
4
$\begingroup$

As a teaser, and following up on the example given in the question I can follow the documentation

pointcloud = 
  ExampleData[{"Geometry3D", "StanfordBunny"}, "VertexData"];
Graphics3D[Point[RandomSample[pointcloud, 1000]]]

Mathematica graphics

make a 3D ListSurfacePlot3D out of it

pl = ListSurfacePlot3D[pointcloud, MaxPlotPoints -> 50, Axes -> None, 
  Boxed -> False, Mesh -> None, PlotStyle -> Gray]

Mathematica graphics

and export it a a 3ds file

Export["bunny.3ds", pl]

which I can then view in three.js as (click to animate)

Mathematica graphics

But this is just one example for meshes.

The corresponding three.js code read

!DOCTYPE html>
<html lang="en">
        <head>
                <title>3DS</title>
                <meta charset="utf-8">
                <meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0,\
 maximum-scale=1.0">
                <style>
                        body {
                                font-family: Monospace;
                                background-color: #000;
                                color: #000;
                                margin: 0px;
                                overflow: hidden;
                        }
                        #info {
                                color: #000;
                                position: absolute;
                                top: 10px;
                                width: 100%;
                                text-align: center;
                                z-index: 100;
                                display:block;
                        }
                        #info a, .button { color: #f00; font-weight: bold; text-decoration: underline; \
cursor: pointer }
                </style>
        </head>

        <body>
                <script src="https://threejs.org/build/three.js"></script>
                <script src="https://threejs.org/examples/js/controls/TrackballControls.js"></script>
                <script src="https://threejs.org/examples/js/loaders/TDSLoader.js"></script>
 <script>
                        var container, controls;
                        var camera, scene, renderer;
                        init();
                        animate();
                        function init() {
                                container = document.createElement( 'div' );
                                document.body.appendChild( container );
                                camera = new THREE.PerspectiveCamera( 60, window.innerWidth / window.in\
nerHeight, 0.1, 10 );
                                camera.position.z = 2;
                                controls = new THREE.TrackballControls( camera );
                                scene = new THREE.Scene();
                                scene.add( new THREE.HemisphereLight() );
                                var directionalLight = new THREE.DirectionalLight( 0xffeedd );
                                directionalLight.position.set( 0, 0, 2 );
                                scene.add( directionalLight );
                                var loader = new THREE.TDSLoader( );
                                loader.setPath( './' );
                                loader.load( './bunny.3ds', function ( object ) {
                                        object.traverse( function ( child ) {
                                        } );
                                        scene.add( object );
                                });
                                renderer = new THREE.WebGLRenderer();
                                renderer.setPixelRatio( window.devicePixelRatio );
                                renderer.setSize( window.innerWidth, window.innerHeight );
                                container.appendChild( renderer.domElement );
                                window.addEventListener( 'resize', resize, false );
                        }
                        function resize() {
                                camera.aspect = window.innerWidth / window.innerHeight;
                                camera.updateProjectionMatrix();
                                renderer.setSize( window.innerWidth, window.innerHeight );
                        }
                        function animate() {
                                controls.update();
                                renderer.render( scene, camera );
                                requestAnimationFrame( animate );
                        }
                </script>

        </body>
</html>

$\endgroup$
1
$\begingroup$

As an alternative, one can also use the stl format for other classes of objects (which seems to be the default for 3D Printing in Mathematica 11)

Mathematica graphics

Mathematica graphics

 Export["contour.3ds", pl]

Mathematica graphics

Also

model = ExampleData[{"Geometry3D", "Triceratops"}, "Region"]

Mathematica graphics

Export["contour.3ds", model]

Mathematica graphics

An example of implementation in three.js is shown here


A drawback of sts or 3ds is that they loose colours.

For instance

LeftRightTorus[i_, j_, k_] := 
 0.1 {i + Sin[t] (3.9 + Cos[r]), j + Cos[t] (3.9 + Cos[r]), k + Sin[r]}
FrontBackTorus[i_, j_, k_] := 
 0.1 {i + Sin[r], j + (3.9 + Cos[r]) Sin[t], k + (3.9 + Cos[r]) Cos[t]}
TopBottomTorus[i_, j_, k_] := 
 0.1 {i + (3.9 + Cos[r]) Cos[t], j + Sin[r], k + (3.9 + Cos[r]) Sin[t]}
pl = ParametricPlot3D[
  Evaluate[{FrontBackTorus[6, 0, 6], FrontBackTorus[12, 6, 12], 
    FrontBackTorus[12, 6, 0], FrontBackTorus[18, 0, 6], 
    FrontBackTorus[6, 12, 6], FrontBackTorus[18, 12, 6], 
    LeftRightTorus[12, 0, 0], LeftRightTorus[12, 12, 12], 
    LeftRightTorus[6, 6, 6], LeftRightTorus[18, 6, 6], 
    LeftRightTorus[12, 0, 12], LeftRightTorus[12, 12, 0], 
    TopBottomTorus[6, 0, 0], TopBottomTorus[18, 0, 0], 
    TopBottomTorus[6, 0, 12], TopBottomTorus[18, 0, 12], 
    TopBottomTorus[6, 12, 12], TopBottomTorus[18, 12, 12], 
    TopBottomTorus[6, 12, 0], TopBottomTorus[18, 12, 0], 
    TopBottomTorus[12, 6, 6]}], {t, 0, 2 \[Pi]}, {r, 0, 2 \[Pi]}, 
  Mesh -> None, Boxed -> False, Axes -> False
  , PlotStyle -> ColorData[10] /@ Range[10]]

Mathematica graphics

Then

 Export["knot.st", pl]

looks like this:

Mathematica graphics

i.e. it has lost the colour information.

$\endgroup$
1
  • $\begingroup$ I am afraid, exporting color information correctly in a universal way is rather nontrivial due to the very differering ways color is handled in various languages... $\endgroup$ Commented Apr 1, 2018 at 14:51
-1
$\begingroup$

The "WLJS Notebook" project is actively working on it:

$\endgroup$
1
  • $\begingroup$ Can you add an example that shows how to solve OP's problem? $\endgroup$
    – Kuba
    Commented Jan 13 at 9:02

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.