4
$\begingroup$

I'm exporting some plots from Mathematica into x3d for display on the web, and I'm having some trouble getting the plot mesh to display consistently. I believe this behaviour is ultimately a bug on the part of the Mathematica x3d exporter, and I'd like to document it here.

Start off by building a 3D plot of a surface in Mathematica:

plot = Plot3D[x^2+y^2, {x, -1, 1}, {y, -1, 1}, PlotRange → {0, 1}, ClippingStyle → None]

Mathematica graphics

and then export this into x3d:

Export[FileNameJoin[{NotebookDirectory[], "plot1.x3d"}], plot]

To see this in a browser, make a barebones html wrapper,

<html> 
<head>  
<!-- X3DOM inclusions -->
<script type='text/javascript' src='x3dom.js'></script> 
<link rel='stylesheet' type='text/css' href='x3dom.css'></link> 
</head> 
<body>
<x3d width='800px' height='500px'>
  <scene>
   <Viewpoint id="viewpoint" position="0 3 2" 
     orientation="0 0.5 0.9 3" centerOfRotation="0 0 0"  description="camera"></Viewpoint>
   <Inline url="plot1.x3d" />
  </scene>
</x3d>
</body>
</html>

and serve it locally however you want. (I run it over jekyll, but any local web server will do.)

The problem is that the plot, and specifically the mesh, gets displayed differently by different browsers. As an example, on my machine (over Windows 10), this is what Chrome and Firefox show:

(The image links to this document, so you can see how it renders in your browser.)

To say it out loud, the mesh is completely missing from the Firefox render. (On other machines, however, the behaviour is different. With mobile Chrome the mesh is missing, and on my linux box the behaviour seems to be reversed, with Firefox displaying the mesh and Chrome skipping it completely.)

What's going on, and how can this be fixed?

$\endgroup$
1
3
$\begingroup$

This behaviour seems to come down to a bug in how the x3d exporter writes down the colours for the mesh.

The x3d file is simply a specialized xml document, and it can be opened in a text file. This particular instance contains two <Shape> nodes in the scene $-$ an <IndexedFaceSet> node which carries the surface, and an <IndexedLineSet> which encodes the mesh. The <IndexedLineSet> has two nodes inside it: a <Coordinate> nod, and a <Color> node. The problem seems to be in that second node, and here is what happens if it is removed by hand:

Presto! problem solved.


So, the next question is: how can this be fixed within Mathematica, so one can edit the plot in a consistent manner, without having to go back and forth between Mathematica and a text editor to remove those nodes by hand. (This is especially irksome because x3d files can be quite large for text editors to handle, particularly because they have very long lines, so cutting by hand is not necessarily even feasible and it can cause your machine to crash if the plot is complex enough.)

So, let's build a toolkit to handle this in a more robust fashion:

  • This will entail editing the x3d file's xml internals, so first we make a U-turn reimporter to get the x3d export into Mathematica as an xml object:

    plotToXML[plot_] := ImportString[
      ExportString[
        plot
       , "X3D"]
      , "XML", "ReadDTD" → False]
    

    The setting of "ReadDTD" → False is required because the XML importer also seems to be broken, since some point around late v10/early v11.

  • While we're here, let's make a function that will clean up some other buggy elements in the export, namely the black background and extraneous lights documented here:

    cleanX3Dscene[x3dInputInXML_] := DeleteCases[
      x3dInputInXML,
      XMLElement["PointLight", {"color" → _, "location" → _, "radius" → "10000"}, {}] | 
       XMLElement["Background", {"skyColor" → "0. 0. 0."}, {}]
      ,∞]
    
  • The overall approach is to extract the surfaces and the lines separately and then reassemble them into a full plot when we're done. To this end, the first thing we need is a blank canvas,

    x3dCanvas = ImportString[
       "<?xml version='1.0' encoding='UTF-8'?>
        <!DOCTYPE X3D PUBLIC 'ISO//Web3D//DTD X3D 3.0//EN' 'http://www.web3d.org/specifications/x3d-3.0.dtd'>
        <X3D profile='Immersive'
            xmlns:xsd='http://www.w3.org/2001/XMLSchema-instance'
            xsd:noNamespaceSchemaLocation='http://www.web3d.org/specifications/x3d-3.0.xsd'>
         <head></head>
         <Scene />
        </X3D>"
       , "XML", "ReadDTD" -> False];
    

    and a way to get stuff into that blank canvas:

    x3dScene[ListOfXMLElements_] = ReplaceAll[
       x3dCanvas,
       XMLElement["Scene", {}, {}] → XMLElement["Scene", {}, ListOfXMLElements]
       ];
    
  • After this, we need a way to extract the surfaces encoded in the <IndexedFaceSet> nodes

    getSurfacesFromX3D[x3dInputInXML_] := Cases[
      x3dInputInXML,
      XMLElement["Shape", {}, {XMLElement["IndexedFaceSet", __], ___}]
      ,∞]
    

    and the lines in the <IndexedLineSet> nodes:

    getLinesFromX3D[x3dInputInXML_] := Cases[
      x3dInputInXML,
      XMLElement["Shape", {}, {XMLElement["IndexedLineSet", __], ___}]
      ,∞]
    
  • Finally, we need a way to destroy the colour tags in the lines object:

    removeX3DcolorSpec[xmlElement_] := DeleteCases[xmlElement, XMLElement["Color", _, _], ∞]
    
  • ... and, while we're here, let's also build a way to shiny up the surface itself, by replacing the default-produced color node

    replaceX3DcolorSpec[xmlElement_, replacementColor_] := Block[{xmlElementTemp = xmlElement},
    
      (*re-set the colorPerVertex value*)
      xmlElementTemp = ReplaceAll[xmlElementTemp, ("colorPerVertex" → "true") → ("colorPerVertex" → "false")];
    
      (*delete existing color tags*)
      xmlElementTemp = DeleteCases[xmlElementTemp, XMLElement["Color", _, _], ∞];
    
      (*insert new appearance tag*)
      xmlElementTemp = Insert[xmlElementTemp, 
                         xmlAppearanceFromColor[replacementColor], 
                         Position[xmlElementTemp, XMLElement["IndexedFaceSet", _, _]]
                         ];
    
      xmlElementTemp
      ]
    

    with a custom-defined <Appearance> node

    xmlAppearanceFromColor[color_] := XMLElement["Appearance", {},
      {XMLElement["Material", {
         "diffuseColor" → StringJoin@@Riffle[ToString /@ List @@ ColorConvert[color, "RGB"], " "]
         , "specularColor" → "1. 1. 1.", "shininess" → "0.9"
         }, {}]}]
    

OK, that was a lot of functions. Let's put them to use:

Export[
 FileNameJoin[{NotebookDirectory[], "plot2.x3d"}],
 x3dScene[Flatten[{
    Map[
     replaceX3DcolorSpec[#, Orange] &,
     getSurfacesFromX3D[cleanX3Dscene[plotToXML[plot]]]
     ],
    Map[
     removeX3DcolorSpec,
     getLinesFromX3D[cleanX3Dscene[plotToXML[plot]]]
     ]
    }]]
 , "XML"]

which produces

and we're good to go!

$\endgroup$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

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