Basic Embedded 3D Models

A how-to guide that provides information about how to import and display 3D models in an application built on the JourneyApps platform using FBX files.

Prerequisites

  1. Make sure you have already created an app on OXIDE. If not, follow the guide on our official documentation.
  2. You have a .fbx file ready that you want to import. At the time of publishing this guide, the JourneyApps application runtime only supports .fbx files.
  3. To upload the 3D model to your application you’ll also need to zip the 3d model (How-to Windows - How-To Mac). If you don’t have a 3D model to test with, feel free to download our sample forklift 3D model using this link.

Data Model Setup

Let’s start by defining a new data model object in our app. Here is a sample:

<?xml version="1.0" encoding="UTF-8"?>
<data-model>
    <model name="user" label="User">
       <field name="name" label="Name" type="text:name"/>
       <display>{name}</display>
   </model>
 
   <model name="forklift" label="Forklift">
       <field name="name" label="Name" type="text" />
       <field name="model" label="Model" type="attachment" media="any" />
       <display>{name}</display>
   </model>
</data-model>

In this snippet, we have defined forklift in our data model (this is just an example, you can name it whatever you like).

We have also defined a new field on forklift called model. Note that the model field is of type ‘attachment’, which means we’ll be able to upload our zipped .fbx file to the application database and make it available to the application runtime on the user’s device.

Now deploy the application, by clicking the ‘Deploy to Testing’ button in OXIDE. Then, open the ‘Testing’ database in the Data Browser (click on the “Data browser” button in the top toolbar).

Once that’s loaded in a new browser window, select the “Forklift” type in the menu on the left and create a new instance of a “Forklift” object by clicking on the blue button in the top right corner.

Finally, give the Forklift a name and upload the zipped .fbx file to our newly created Forklift object. If you don’t have a 3D model to test with, feel free to download our sample forklift 3D model using this link.

Views

On the main.js or main.ts view of the application, we’re going to start wiring up the UI by adding the display-3d-model view component. Add the following to your view XML:

<?xml version="1.0" encoding="UTF-8"?>
<view title="3d Model">
   <var name="camera" type="text" />
   <var name="scene" type="attachment" media="any" />
   <var name="wireframe" type="boolean" />
   <var name="fog" type="boolean" />
   <var name="floor" type="boolean" />
   <var name="doubleSided" type="boolean" />
   <var name="debug" type="boolean" />
   <var name="scale" type="number" />
   <var name="offsetx" type="number" />
   <var name="offsety" type="number" />
   <var name="offsetz" type="number" />
   <var name="bounding" type="boolean" />
   <var name="normals" type="boolean" />
   <var name="vertexGroups" type="boolean" />
 
 
   <display-3d-model
       id="1"
       scale="scale"
       bind="scene"
       wireframe="wireframe"
       double-sided-materials="doubleSided"
       fog="fog"
       floor="floor"
       stats="debug"
       autoplay="false"
       loop="false"
       offset-x="offsetx"
       offset-y="offsety"
       offset-z="offsetz"
       bounding-box="bounding"
       material-vertex-groups="vertexGroups"
       debug-normals="normals"
       on-pick-mesh="$:pick($meshes)"
   />
 
   <button-group mode="row">
       <button label="Toggle wireframe" on-press="$:toggleWireframe()"/>
       <button label="Bounding" on-press="$:view.bounding = !view.bounding" />
       <button label="Stats" on-press="$:stats()" />
       <button label="Normals" on-press="$:normals()" />
       <button label="Vertex Material Groups" on-press="$:updateVertexGroups()" />
       <button label="Double Sided" on-press="$:doubleSided()" />
   </button-group>
 
   <button-group mode="row">
       <button label="-" on-press="$:scaleDown()"/>
       <button label="+" on-press="$:scaleUp()"/>
       <button label="Fit" on-press="$:computeScale()"/>
       <button label="Center" on-press="$:computeOffsets()"/>
       <button label="Toggle fog" on-press="$:toggleFog()"  />
       <button label="Toggle floor" on-press="$:toggleFloor()"  />
       <button label="Get camera" on-press="$:getPosition()" />
       <button label="Set camera" on-press="$:setPosition()" />   
   </button-group>
 
   <button-group mode="row">
       <button label="Start" on-press="$:start()" />
       <button label="Stop" on-press="$:stop()" />
       <button label="Get playhead" on-press="$:getPlayhead()" />
       <button label="Set playhead" on-press="$:setPlayhead()" />
   </button-group>
 
</view>

Notice we’ve added a few attributes here, each specifying behavior of the 3D model, such as:

  1. id: The unique if of the model in the view
  2. scale: Set the scale
  3. bind: The 3D attachment stored in the application database
  4. wireframe: Enable or disable
  5. double-sided-materials: Specify double-sided materials
  6. fog: Enable or disable fog
  7. floor: Enable or disable debug info such as the frames rendered per second
  8. stats: Enable or disable debug info such as the frames rendered per second
  9. autoplay: Enable or disable autoplay, this is useful for 3d models with animations
  10. loop: Enable or disable if the animations should play in a loop
  11. offset-x: Control x offset
  12. offset-y: Control y offset
  13. offset-z: Control z offset
  14. bounding-box: Enable or disable the bounding-box
  15. material-vertex-groups: Enable or disable the material-vertex-groups
  16. debug-normals: Enable or disable the to debug model normals
  17. on-pick-mesh: Specify a function that fetches the meshes from the model

Now let’s write a few lines of JavaScript or TypeScript code to retrieve the 3D model from the application database and bind it to the display-3d-model component. We’ll also add a few sample buttons to the view which will provide us with controls.

View Logic

In your main.js or main.ts, paste the following:

// This function is called when the app navigates to this view (using a link)
function init() {
   var forklift = DB.forklift.first("name = ?", "Forklift");
   view.scene = forklift.model;
 
   view.wireframe = false;
   view.fog = true;
   view.doubleSided = false;
   view.debug = true;
   view.floor = true;
   view.scale = 0.1;
   view.offsetx = 0;
   view.offsety = 0;
   view.offsetz = 0;
   view.bounding = true;
   view.vertexGroups = false;
   view.normals = false;
}
 
// This function is called when the user returns to this view from another view
function resume(from) {
   // from.back       (true/false) if true, the user pressed the "Back" button to return to this view
   // from.dismissed  (true/false) if true, the app dismissed to return to this view
   // from.path       contains the path of the view that the user returned from
   // if any data needs to be refreshed when the user returns to this view, you can do that here:
}
 
function normals(){
   view.normals = !view.normals;
}
 
function updateVertexGroups(){
   view.vertexGroups = !view.vertexGroups;
}
 
function save(){
   var model = DB.forklift.first();
   model.scene = view.scene;
   model.save();
}
 
function toggleWireframe(){
   view.wireframe = !view.wireframe;
}
 
function pick($meshes){
   for(var i = 0; i < $meshes.length; i++){
       console.log($meshes[i]); // {name : "scam_break_line_left_thing"}
   }
}
 
function scaleUp(){
   view.scale *= 10;
}
 
function computeScale(){
   view.scale = component.display3dModel({id: "1"}).computeScaleFactor(550);
}
 
function computeOffsets(){
   var offsets = component.display3dModel({id: "1"}).computeCenterOffset();
   view.offsetx = offsets[0];
   view.offsety = offsets[1];
   view.offsetz = offsets[2];
}
 
function scaleDown(){
   view.scale /= 10;
}
 
function toggleFog(){
   view.fog = !view.fog;
}
 
function toggleFloor(){
   view.floor = !view.floor;
}
 
function getPosition(){
   var c = component.display3dModel({id: "1"}).getCamera();
   view.camera = JSON.stringify(c);
   console.log(c);
}
 
function setPosition(){
   component.display3dModel({id: "1"}).setCamera(JSON.parse(view.camera));
}
 
function start(){
   component.display3dModel({id: "1"}).animationStart();
}
 
function stop(){
   component.display3dModel({id: "1"}).animationStop();
}
 
function doubleSided(){
   view.doubleSided = !view.doubleSided;
}
 
function stats(){
   view.debug = !view.debug;
}
 
function getPlayhead(){
  
   var pos = component.display3dModel({id: "1"}).getPlayhead();
   journey.dialog.simple({
       title: 'Playhead position',
       message: "The position is: "+ pos
   })
}
 
function setPlayhead(){
   var pos = journey.dialog.input({
       title: 'Set Playhead position',
       message: "enter the value in ms"
   });
   component.display3dModel({id: "1"}).setPlayhead(pos);
}

In the above example, first, we fetch the 3D model attachment from the application database and bind it to the display-3d-model component, and then initialize a few default attributes used by the component.

Furthermore, we have created a few functions that give us control over various aspects of the 3D model rendered in the app.

Using the component.display3dModel function in our view JavaScript or TypeScript file we can manipulate the model programmatically. The code sample above provides a few useful samples of this. Try it out.

Test App

Now, let’s launch the application and inspect our 3D model. To do so, click the “Test App” button at the bottom left in OXIDE. Select your preferred platform — the application will work on all of the supported platforms (iOS, Android, Windows, macOS, Linux, and RealWear).

1 Like