Skip to content

Writing a custom FormatHandler plugin

Christian Schlinkmann edited this page Jan 7, 2016 · 4 revisions

Lets say we have a mesh that we'd like to draw in XML3D but we're not able to convert it to an XML3D format. In this case we can use the FormatHandler interface to write a small loader plugin that will adapt the raw mesh data into a form that XML3D can draw. Once we're done with this we'll be able to draw these meshes as we would any other:

<mesh src="Laurana50k.json" type="triangles"></mesh>

There are already several FormatHandler plugins out there. The XML3D examples include plugins for MeshLab JSON, the OpenCTM and the BLAST binary formats. In addition there are public repositories for an STL loader and a GLTF loader. This is a lot of example code to refer back to, but to get a better idea of what's involved in creating a FormatHandler lets take a closer look at the MeshLab JSON format and re-create the loader for it.

Here is the example mesh in MeshLab JSON Format that we'll be using throughout the tutorial and the complete source code of the finished FormatHandler plugin.

Every FormatHandler needs to inherit from XML3D.resource.FormatHandler and implement a set of interface functions. Lets start with the basic definition of our MeshLab handler:

var MeshLabFormatHandler = function () {
   XML3D.resource.FormatHandler.call(this);
};
XML3D.createClass(MeshLabFormatHandler, XML3D.resource.FormatHandler);

MeshLabFormatHandler.prototype.isFormatSupported = function (response) {
   // Is this response object something I can parse?
};

MeshLabFormatHandler.prototype.getFormatData = function (response) {
   // Parse the response body and return a usable document, or throw an exception if the 
   // body can't be understood by this FormatHandler
};

MeshLabFormatHandler.prototype.getFragmentData = function(data, fragment) {
   // If the format allows collections of objects return the object with the given uri fragment (id)
   // If the format always represents a single object it's ok to just give the data object back
};

MeshLabFormatHandler.prototype.getAdapter = function(data, aspect, canvasId) {
   // Given the data (which may be a part of or the whole document generated in getFormatData) return a 
   // usable Adapter object that puts the data into an Xflow graph that XML3D can render
};

XML3D.resource.registerFormatHandler(new MeshLabFormatHandler());

Note the 'response' parameter. As of 5.1 XML3D uses the Fetch API to handle all external resource requests. Our FormatHandler will be given the raw Response object associated with these requests and the standard Fetch API rules apply (for instance the response body can only be read once unless the response is cloned before hand).

These interface functions are in the same order as the chronological order of the internal request processing. When a response to a request comes in (eg. for our mesh that references "meshlab_object.json") XML3D first asks all available FormatHandlers if the Response is possibly something they can parse, usually based on the mimetype or file ending. This check should not touch the body of the Response object, doing so will trigger an exception, we're only trying to see if this Response is something that could possibly be handled by our FormatHandler. Lets go ahead and fill out the isFormatSupported function:

MeshLabFormatHandler.prototype.isFormatSupported = function (response) {
   if (response.headers.has("Content-Type")) {
      return response.headers.get("Content-Type") === "application/json";
   }
   return response.url.match(/\.json/);
};

Here we check if the response object has a "Content-Type" header, and if so we check if the mimetype of this response is application/json. If there is no content type header we check the url of the response for a .json file ending. This is not fool proof but it doesn't have to be. At this stage XML3D is only interested in whether our FormatHandler should be added to the list of candidates for this response.

Lets fill out the getFormatData function next, where we attempt to parse the body of the response object:

MeshLabFormatHandler.prototype.getFormatData = function (response) {
   return response.json().then(function(data) {
      if (data.comment != "Generated by MeshLab JSON Exporter")
         throw new Error("Unknown JSON format: " + data.comment);
      if (data.version != "0.1.0")
         throw new Error("Unknown MeshLab JSON version: " + data.version);

      return data;
    });
};

The FetchAPI is based on Javascript Promises. At this stage every candidate FormatHandler (including ours) is given a clone of the Response object to attempt to parse it. This means we now have permission to access the response body without causing an exception. The Fetch API provides several parsing functions as standard including .json(), which returns a Promise that can be chained to a .then() call which is given the parsed JSON data.

Inside our .then() handler we can inspect the JSON data itself and determine if the data is in MeshLab format, or possibly some other JSON based format that we're not interested in. If it is we simply return the parsed data, otherwise we throw an exception to tell XML3D that we aren't responsible for this particular format.

Note that this function must always return a Promise that should be either resolved or rejected. Here we implicitly resolve the promise when we return the data or implicitly reject it when we throw the exception.

Before we move on to the real work we should fill out the getFragmentData function. Since our MeshLab format is just encoding a single object and doesn't have the concept of sub-objects with their own ids, we can simply return the data that we're given:

MeshLabFormatHandler.prototype.getFragmentData = function(data, fragment) {
   return data;
};

This might seem redundant (and it is) but for compatibility every FormatHandler needs to return valid data here. This function will be called at least once during loading, possibly with a null fragment, so it's not an optional step.

The next step in our mesh's journey is to embed it into an Xflow graph so it can be rendered by XML3D. This is the most complicated step so lets try to break it down a bit.

The first thing we should do is create a simple wrapper for the Adapter interface that XML3D expects. This will be the same in any FormatHandler and the interface is very simple:

  var MeshLabJSONDataAdapter = function (data) {
     this.xflowDataNode = createXflowNode(data);
  };

  MeshLabJSONDataAdapter.prototype.getXflowNode = function () {
     return this.xflowDataNode;
  };

This is the object that will be returned by our getAdapter function, it only needs to implement a getXflowNode function that returns the root node of the Xflow data graph we'll be building.

Instead of examining the createXflowNode function line by line lets take a more abstract look at what we're trying to achieve with it. Conceptually the data graph that we're going to build is identical to the mesh data graphs that we've seen in the DOM in other examples, for instance the How to use Xflow article. It will contain a root DataNode with child InputNodes that will contain the actual mesh data in arrays. In the case of our Laurana50k.json file this tree will look like the following:

DataNode
  |- InputNode (position) [Float32Array]
  |- InputNode (normal) [Float32Array]
  |- InputNode (color) [Float32Array]
  |- InputNode (index) [UInt32Array]

The nodes for this graph are exposed through the global Xflow object. The root DataNode doesn't need any special handling, in our case it only acts as a data aggregator for the child InputNodes:

function createXflowNode(jsonData) {
   var node = new Xflow.DataNode();

   // ... add child InputNodes containing the mesh data ...

   return node;
}

Each InputNode consists of a name and an Xflow.BufferEntry, which is a small wrapper around the actual data. For simplicity lets pretend the vertex data for the mesh's "position" attribute is located in jsonData.position as a Float32Array. To create the appropriate InputNode we would do the following:

var positionInput = new Xflow.InputNode();
positionInput.name = "position";
positionInput.data = new Xflow.BufferEntry(Xflow.constants.DATA_TYPE.FLOAT3, jsonData.position);

node.appendChild(positionInput);

The BufferEntry constructor expects a DATA_TYPE enum specifying how the data in the Float32Array should be interpreted. In this case our mesh positions are 3 component tuples (x,y,z) so we give it the FLOAT3 data type.

This is all the code that's needed to build the Xflow graph, the rest of the code in createXflowNode is concerned with finding and interpreting the JSON data and will differ from format to format. No matter how your data is encoded you always want to bring it into the same form: a root DataNode with named child InputNodes each containing a BufferEntry.

The very last step is perhaps the most important, we need to register our new FormatHandler with XML3D:

// Create an instance of our MeshLabFormatHandler and register it with XML3D
XML3D.resource.registerFormatHandler(new MeshLabFormatHandler());

The finished loader (including the createXflowNode bits that we've glossed over here) can be found here. Be sure to check the other loaders at the top of this article for more example code, and remember that you can easily use third party libraries in your loader to do the heavy lifting of parsing the data. This is done in the stl loader, for example.