Next (Custom Commands) Previous (Plugins)

Custom Importer

Introduction

In this example, we will create an .obj importer using the API provided by Clara.io. Before we begin, there are some concepts that we must cover related to working with the API.

Firstly, there are three main methods of writing to the API. These are:

  • Command plugins
  • Operator plugins
  • Scripts

In this example, we will be using scripts, as they are the simplest. Later tutorials will cover plugin creation.

The next important piece of information is how to debug scripts. All errors and logs will appear within the browser console (Ctrl + Shift + J for Chrome, Ctrl + Shift + K for Firefox). Here you can use console.log or the debugger statement to inspect the state of the system and view any errors thrown by the script execution.

When importing into Clara.io, our goal is to convert the source information into our native mesh format, called a PolyMesh. For this to happen, we must compile our information into a JSON file, which can then be used to construct the PolyMesh. As well, to add our UVs and normals, we will create PolyMaps, which maps indices to information such as a UV or a normal.

Clara.io uses a number of external libraries and in this example, we will be working with ThreeJS for 3D vectors, Q promises for callbacks, and Underscore.js for utility functions. While their usage in this example is only cursory, many advanced techniques may require further knowledge of these libraries.

To avoid the automatic import of .obj files in Clara.io, we will be naming our files with the extension .myobj. The content of this file is identical to that of a regular .obj file. Please note that this is not meant to be a thorough .obj importer. Information such as grouping, smoothing groups, and materials are not considered.

All code below is written in the scripting window at the bottom of Clara.io. At any time, you can experiment with the results by pressing the Play button and observing the console window.

Fetching File Content

Our first function to write will fetch the contents of our .myobj file from the asset list. To do this, we must first create an .obj file, rename its extension to .myobj, and drag it into Clara.io. Once it appears in the asset list, we can access it by its file name by filtering the asset list files. If there is a name duplication, we will access the first element found. Since this function is asynchronous, we will also provide it with a callback.

var getContentFromMyOBJ = function(fileName, callback) {
  var myObjs = _.filter(ctx().scene.files.assets(), function(file) {
    return file.getName() === fileName && file.getFileType() === '.myobj';
  });
  myObjs[0].fetchContent(callback);
}

This function will be the main entry point into our function. Underscore's filter function will provide us with a relevant array, and at this point, we can use the fetchContent function to retrieve the contents of the file.

Parsing the Content

From here, we must deconstruct the .myobj string into its component parts and pushing the information to arrays. For more information on the .obj format, try Wikipedia or FileFormat.info.

This section of code will use Regular Expressions to parse lines of code and compose them into arrays. Our vertices and normals will be formated as a THREE.Vector3 object, and will be stored in a large array. UVs are stored as a THREE.Vector2 (a ThreeJS type). Our face, UV, and normal indices will be integer arrays within arrays, of the format [[1 2 3] [2 3 4] [3 4 5]], each number referencing a particular vector within the arrays we first created.

This function will be called as a callback to the last function, and will take in the content as a string. The err argument is a requirement of the fetchContent function used previously.

var continueImport = function(err, content) {
  // Split lines on newline or backslash
  var lines = content.split(/[\n\\]/);

  // Create empty containers
  var verts = [];
  var faces = [];
  var uvs = [];
  var uvIndices = [];
  var normals = [];
  var normalIndices = [];

  // Parse each line
  _.each(lines, function(line) {
    line = line.trim();
    // Split on whitespace
    var lineArr = line.split(/\s+/g);

    // Parse vertex positions
    if (lineArr[0] === 'v') {
      verts.push(new THREE.Vector3(parseFloat(lineArr[1]), parseFloat(lineArr[2]), parseFloat(lineArr[3])));
    }

    // Parse UVs
    else if (lineArr[0] === 'vt') {
      uvs.push(new THREE.Vector2(parseFloat(lineArr[1]), parseFloat(lineArr[2])));
    }

    // Parse normals
    else if (lineArr[0] === 'vn') {
      normals.push(new THREE.Vector3(parseFloat(lineArr[1]), parseFloat(lineArr[2]), parseFloat(lineArr[3])));
    }

    // Parse faces
    else if (lineArr[0] === 'f') {
      var face = [];
      var uvIndex = [];
      var normalIndex = [];
      // Parse each attribute set
      _.each(_.rest(lineArr), function(point) {
        var pointAttrs = point.split('/');

        // Attributes (off-by-one adjustment)
        // Position indices
        face.push(parseInt(pointAttrs[0]) - 1);

        // UV indices
        if (pointAttrs.length >= 2 && pointAttrs[1] !== "") {
          uvIndex.push(parseInt(pointAttrs[1]) - 1);
        }

        // Normal indices
        if (pointAttrs.length >= 3 && pointAttrs[2] !== "") {
          normalIndex.push(parseInt(pointAttrs[2]) - 1);
        }
      });
      // Add face to collection
      faces.push(face);

      // Add UVs and normals to collection, if they exist
      if (uvIndex.length > 0) {
        uvIndices.push(uvIndex);
      }
      if (normalIndex.length > 0) {
        normalIndices.push(normalIndex);
      }
    }
  });

Creating the PolyMesh

With this information, we can now create our PolyMesh object. It takes in our face index and vertex arrays. If we have UVs or normals, we will also create a PolyMap using our UV / normal indices and UV / normal arrays, while also assigning it to the PolyMesh.

  // Create polyMesh object
  var polyMesh = new exo.geometry.PolyMesh( faces, verts );

  // If there are UVs or normals, create a map to store them and apply to the polyMesh object
  if (uvs.length > 0 && uvIndices.length > 0) {
    polyMesh.setUVMap(new exo.geometry.PolyMap(uvIndices, uvs));
  }
  if (normals.length > 0 && normalIndices.length > 0) {
    polyMesh.setNormalMap(new exo.geometry.PolyMap(normalIndices, normals));
  }

Creating the Scene Object

Lastly, we must use the API function ctx() to add the JSON file and use that to create the scene graph object. The first call, ctx().addFile will add the file (invisibly) to the assets, formatting it in JSON so that our PolyMesh constructor can easily parse the information.

We then use promises to asynchronously continue to the next step with the then function. We use ctx to select the Objects node (root for all meshes without a parent) and assign it the name “MyMesh”, give it a primitive of type ‘PolyMesh’, and set its mesh geometry to be the contents of the JSON file passed by the promise.

  ctx().addFile({
    name: 'MyMesh.json',
    content: polyMesh,
    type: 'application/json',
    sourceType: 'import',
    internal: false
  }).then(function(myMesh) {
    ctx('%Objects').addNode('MyMesh', 'PolyMesh', { PolyMesh: { Mesh: { geometry: myMesh } } });
  });
}

Calling Our Function

The last step is to call the function we just created. The callback is provided as an argument, so this boils down to one line of code. Just be sure to set the name of the .myobj file you would like to pick up (without extension!).

getContentFromMyOBJ('Sphere', continueImport);

All Together

We hope this tutorial was sufficient to get you started on importing custom formats, and understanding the way Clara.io treats information. To import your own information, it is simply a matter of parsing your own data in a way that conforms to our vertex, face, UV, and normal information. Animation is a more complex subject and will be convered in a later tutorial.

Below is the script in its entirety.

var getContentFromMyOBJ = function(fileName, callback) {
  var myObjs = _.filter(ctx().scene.files.assets(), function(file) {
    return file.getName() === fileName && file.getFileType() === '.myobj';
  });
  myObjs[0].fetchContent(callback);
}

var continueImport = function(err, content) {
  // Split lines on newline or backslash
  var lines = content.split(/[\n\\]/);

  // Create empty containers
  var verts = [];
  var faces = [];
  var uvs = [];
  var uvIndices = [];
  var normals = [];
  var normalIndices = [];

  // Parse each line
  _.each(lines, function(line) {
    line = line.trim();
    // Split on whitespace
    var lineArr = line.split(/\s+/g);

    // Parse vertex positions
    if (lineArr[0] === 'v') {
      verts.push(new THREE.Vector3(parseFloat(lineArr[1]), parseFloat(lineArr[2]), parseFloat(lineArr[3])));
    }

    // Parse UVs
    else if (lineArr[0] === 'vt') {
      uvs.push(new THREE.Vector2(parseFloat(lineArr[1]), parseFloat(lineArr[2])));
    }

    // Parse normals
    else if (lineArr[0] === 'vn') {
      normals.push(new THREE.Vector3(parseFloat(lineArr[1]), parseFloat(lineArr[2]), parseFloat(lineArr[3])));
    }

    // Parse faces
    else if (lineArr[0] === 'f') {
      var face = [];
      var uvIndex = [];
      var normalIndex = [];
      // Parse each attribute set
      _.each(_.rest(lineArr), function(point) {
        var pointAttrs = point.split('/');

        // Attributes (off-by-one adjustment)
        // Position indices
        face.push(parseInt(pointAttrs[0]) - 1);

        // UV indices
        if (pointAttrs.length >= 2 && pointAttrs[1] !== "") {
          uvIndex.push(parseInt(pointAttrs[1]) - 1);
        }

        // Normal indices
        if (pointAttrs.length >= 3 && pointAttrs[2] !== "") {
          normalIndex.push(parseInt(pointAttrs[2]) - 1);
        }
      });
      // Add face to collection
      faces.push(face);

      // Add UVs and normals to collection, if they exist
      if (uvIndex.length > 0) {
        uvIndices.push(uvIndex);
      }
      if (normalIndex.length > 0) {
        normalIndices.push(normalIndex);
      }
    }
  });

  // Create polyMesh object
  var polyMesh = new exo.geometry.PolyMesh( faces, verts );

  // If there are UVs or normals, create a map to store them and apply to the polyMesh object
  if (uvs.length > 0 && uvIndices.length > 0) {
    polyMesh.setUVMap(new exo.geometry.PolyMap(uvIndices, uvs));
  }
  if (normals.length > 0 && normalIndices.length > 0) {
    polyMesh.setNormalMap(new exo.geometry.PolyMap(normalIndices, normals));
  }

  ctx().addFile({
    name: 'MyMesh.json',
    content: polyMesh,
    type: 'application/json',
    sourceType: 'import',
    internal: false
  }).then(function(myMesh) {
    ctx('%Objects').addNode('MyMesh', 'PolyMesh', { PolyMesh: { Mesh: { geometry: myMesh } } });
  });
}

getContentFromMyOBJ('Sphere', continueImport);

Next (Custom Commands) Previous (Plugins)