Playing around with Three.JS — Part Two of a not quite a tutorial series

 

In Part One we looked at the basics of working with the Three.js graphics library.  We got as far as creating a camera and a textured 3D object.  Now is the true test of ease of use… getting a 3D model exported from Blender and displayed in our browser.  HTML libraries face an even bigger burden, as their access to the local file system isn’t as seamless as most other games.  Simply opening up an FBX or DAE file isn’t an option.  Let’s take a look at how ThreeJS works around this issues.

 

 

First’s thing first, I needed a Blender Blend file to work with.  This actually lead me down this road, resulting in a post about taking a Blend file from the web and making it game ready.  Anyways, I did.  I started with this file, merged the geometry, UV mapped it, and baked the Blender materials to a single texture map.  I am not entirely sure if I can share the resulting file or not, so you may have to provide your own Blender file or follow the linked tutorial to generate one of your own.

 

Anyways, this is what we are starting with…

image 

 

Let’s see how close we can get with Three.js.

 

The first obvious question is… how the hell do we get this model in to Three.JS from Blender?

Well, the answer is a plugin.  Follow the installation direction, however in my case the path was wrong.  For Blender 2.71, my actual plugin directory is C:Program FilesBlender FoundationBlender2.71scriptsaddonsio_mesh_threejs.

 

There is one very critical thing to be aware of here… when downloading the files from Github, be certain to download the RAW format:

image

 

This particular mistake caused me a bit of pain, don’t make the same mistake!

 

Once you’ve copied each of these three files, configure the plugin in Blender.  If Blender is running, restart it.

Now select File->User Preferences:

image

 

In the resulting dialog select Addons, then in the search box type “three”.  If it installed correctly it will show on the right.  Click the checkbox to enable the plugin.

image

 

Now if you check the File->Export menu, you should see Three.js as an option.

image

 

When exporting you can clearly see there are a ton of options:

image

 

The options I selected above is for just exporting the mesh and materials.  No animation data, lights, cameras, etc… Scaling and Flip YZ all depend on the orientation of your game engine.

 

This exporter creates a JSON js like this one:

 

{    	"metadata" :  	{  		"formatVersion" : 3.1,  		"generatedBy"   : "Blender 2.7 Exporter",  		"vertices"      : 8,  		"faces"         : 6,  		"normals"       : 2,  		"colors"        : 0,  		"uvs"           : [24],  		"materials"     : 1,  		"morphTargets"  : 0,  		"bones"         : 0  	},    	"scale" : 1.000000,    	"materials" : [	{  		"DbgColor" : 15658734,  		"DbgIndex" : 0,  		"DbgName" : "Material",  		"blending" : "NormalBlending",  		"colorAmbient" : [0.6400000190734865, 0.6400000190734865, 0.6400000190734865],  		"colorDiffuse" : [0.6400000190734865, 0.6400000190734865, 0.6400000190734865],  		"colorEmissive" : [0.0, 0.0, 0.0],  		"colorSpecular" : [0.5, 0.5, 0.5],  		"depthTest" : true,  		"depthWrite" : true,  		"mapDiffuse" : "crate.jpg",  		"mapDiffuseWrap" : ["repeat", "repeat"],  		"shading" : "Lambert",  		"specularCoef" : 50,  		"transparency" : 1.0,  		"transparent" : false,  		"vertexColors" : false  	}],    	"vertices" : [1,-1,0,1,0,1,-1,0,0,0,-1,0,1,0,0,0,1,1,-1,1,0,0,0,0],    	"morphTargets" : [],    	"normals" : [0.577349,0.577349,0.577349,0.577349,0.577349,-0.577349],    	"colors" : [],    	"uvs" : [[0.988679,0.99767,0.988677,0.016243,0.007251,0.016244,0.007252,0.  	997671,0.989755,0.017099,0.989755,0.998526,0.008328,0.998526,0.008328,0.017099,  	0.990714,0.989755,0.009287,0.989755,0.009286,0.008328,0.990713,0.008328,0.  	000516,0.993662,0.981943,0.993661,0.981942,0.012235,0.000516,0.012235,0.987766,  	0.997568,0.987766,0.016141,0.006339,0.016141,0.006339,0.997568,0.986807,0.  	986807,0.986807,0.005381,0.00538,0.00538,0.00538,0.986807]],    	"faces" : [43,0,3,2,1,0,0,1,2,3,0,0,1,1,43,4,7,6,5,0,4,5,6,7,0,0,1,1,43,0,4,5,1,  	0,8,9,10,11,0,0,1,1,43,1,2,6,5,0,12,13,14,15,1,1,1,1,43,2,3,7,6,0,16,17,18,19,1,  	0,0,1,43,3,0,4,7,0,20,21,22,23,0,0,0,0],    	"bones" : [],    	"skinIndices" : [],    	"skinWeights" : [],      "animations" : []      }

 

In theory things should just work, but since when did gamedev give a damn about theory?  Suffice to say I ran into a bit of a problem fully documented here.  The bug actually had nothing to do with Three.js, it was actually caused by my IDE WebStorm.

 

Anyways… once I figured out the problem, the code to load a model was extremely straightforward:

 

///<reference path="./three.d.ts"/>    class ThreeJSTest {      renderer:THREE.WebGLRenderer;      scene:THREE.Scene;      camera:THREE.Camera;        constructor() {          this.renderer = new THREE.WebGLRenderer({ alpha: true });            this.renderer.setSize(500, 500);          this.renderer.setClearColor(0xFFFFFF, 1);            document.getElementById('content').appendChild(this.renderer.domElement);            this.scene = new THREE.Scene();            this.camera = new THREE.PerspectiveCamera(75              , 1              , 0.1, 1000);            this.camera.position = new THREE.Vector3(10, 0, 10);          this.camera.lookAt(new THREE.Vector3(0, 0, 0));              // New code begins below          // Create a loader to load in the JSON file          var modelLoader = new THREE.JSONLoader();            // Call the load method, passing in the name of our generated JSON file          // and a callback for when loadign is complete.          // Not fat arrow typescript call for proper thisification.  AKA, we want           this to be this, not that          // or something else completely          modelLoader.load("robot.jsm", (geometry,materials) => {              // create a mesh using the passed in geometry and textures              var mesh = new THREE.SkinnedMesh(geometry,new THREE.MeshFaceMaterial(                         materials));              mesh.position.x = 0; mesh.position.y = mesh.position.z = 0;              // add it to the scene              this.scene.add(mesh);          });            this.scene.add(new THREE.AmbientLight(new THREE.Color(0.9,0.9,0.9).                         getHex()));          this.renderer.render(this.scene, this.camera);      }        render() {          requestAnimationFrame(() => this.render());          this.renderer.render(this.scene, this.camera);      }        start() {          this.render();      }  }    window.onload = () => {      var three = new ThreeJSTest();      three.start();  };

 

And when you run it:

image

 

Mission accomplished!  The astute reader may notice the file was renamed robot.jsm.  That was to work around the problem I mentioned earlier.

 

 

 

This isn’t actually the only option for loading 3D models into Three.js, there are actually a series of loaders available as a separate download from the Github site.  It is however certainly the easiest one!  The next few steps took me two full days to fight my way through!  Some of the blame is on TypeScript, some is on me and of course, CORS reared it ugly head as well.  Oh, and to add to the fun, a recent change in Three.js introduced an error in the version of ColladaLoader.js I downloaded. This was one of those trials that Google was no help with, so hopefully this guide will help others in the future.  Anyways… on with the adventure!

 

We are actually about to run smack into two different problems with using a Three.js plugin.  The first one is TypeScript related.  You see, the ColladaLoader plugin is not a core part of Three.js.  In JavaScript, this is no big deal.  In TypeScript however, big deal.  You see, until now we have been relying on the generated .d.ts file from StrictlyTyped for defining all of the types in Three.js from JavaScript in their corresponding TypeScript form.  However, since this is a plugin and not a core part of Three.js, this means the d.ts file has no idea how ColladaLoader works.

 

Ultimately this means you have to had roll your own Typescript definition file. I had to struggle a bit to find the exact TypeScript syntax to map ColladaLoader so it will run in TypeScript within the THREE namespace with proper callback syntax.  First off, create a file called ColladaLoader.d.ts   Now enter the following code:

 

///<reference path="./three.d.ts"/>    declare module THREE {      export class ColladaLoader{          options:any;          load(              name:string,              readyCallback🙁result:any)=> void,              progressCallback🙁 total:number,loaded:number)=> void);      }  }

 

 

I should probably point out, I only implemented the barest minimum of what I required.  In your case you may have to implement more of the interface.  Also note I also took the lazy approach to defining options.  By returning any, my code will compile in TypeScript, but I do lose some of the type checking.  The alternative would have been to define the Options type and I am way too lazy for that.  The above definition enable me to call the load() method and set options, which is all I actually needed.  I don’t even want to talk about how long it took me to puzzle out those 10 lines of code so that the generated code actually matched THREE.js!

 

OK, we now have ColladaLoader.d.ts defined let’s look at code to use ColladaLoader to load a DAE (COLLADA) file:

 

///<reference path="./three.d.ts"/>  ///<reference path="./ColladaLoader.d.ts"/>      class ThreeJSTest {      renderer:THREE.WebGLRenderer;      scene:THREE.Scene;      camera:THREE.Camera;      loader:THREE.ColladaLoader;      light:THREE.PointLight;        constructor() {          this.renderer = new THREE.WebGLRenderer({ alpha: true });            this.renderer.setSize(500, 500);          this.renderer.setClearColor(0xFFFFFF, 1);            document.getElementById('content').appendChild(this.renderer.domElement);            this.scene = new THREE.Scene();;            this.camera = new THREE.PerspectiveCamera(75              , 1              , 0.1, 1000);            this.camera.position = new THREE.Vector3(5, 0, 5);          this.camera.lookAt(new THREE.Vector3(0, 0, 0));            // Create a loader          this.loader = new THREE.ColladaLoader();            // Add a point light to the scene to light up our model          this.light = new THREE.PointLight();          this.light.position.set(100,100,100);          this.light.intensity = 0.8;          this.scene.add(this.light);            this.renderer.render(this.scene, this.camera);            // Blender COLLADA models have a different up vector than Three.js, set           this option to flip them          this.loader.options.convertUpAxis = true;            // Now load the model, passing the callback finishedLoading() when done.          this.loader.load("robot.dae",              (result) => this.finishedLoading(result),              (length,curLength)=>{                  // called as file is loading, if you want a progress bar              }          );      }        finishedLoading(result){          // Model file is loaded, add it to the scene          this.scene.add(result.scene);      }      render() {          requestAnimationFrame(() => this.render());          this.renderer.render(this.scene, this.camera);      }        start() {          this.render();      }  }    window.onload = () => {      var three = new ThreeJSTest();      three.start();  };

 

 

Finally of course we need to export our COLLADA model.  Using Blender, you can export using File->Export->Collada menu option.  These are the settings I used:

image

 

And when you run it:

image

 

That said, there is a really good chance this isn’t what is going to happen to you when you run your project.  Instead you are going to probably receive a 404 error that your dae file is not found.  This is because, unlike before with the JSON file being added directly to your project, this time you are loading the model using an XML HTTP Request.  This causes a number of problems.  The first and most likely problem you are going to encounter is if you are running your application locally from your file system instead of a server.  By default XHR requests do not work this way ( no idea why, seems kinda stupid to me ).  There is a switch that allows chrome to run XHR local requests ( link here ) using --allow-file-access-from-files.

 

In my case the problem was a little bit different.  I use WebStorm, which includes a built in web server to make this kind of stuff easier.  This however raises a completely different set of problems…  CORS  Cross Origin Resource Sharing.  In a very simple description, CORS is a security method for making XML HTTP Requests across different servers.  That said, how the heck do you set it when you are working with a built in stripped down development server?  Fortunately WebStorm have thought about that.

 

Assuming you are using Chrome and have the Webstorm plugin install, in the Chrome address bar, go to chrome://extensions.

image

 

Click the Options button.

image

Now simply add 127.0.0.1 to the allow list and press Apply. 

 

Now if you run from Webstorm, no more 404 errors.

 

A moment about TypeScript

 

I’ve been using TypeScript a fair bit lately and this is the first time I’ve run into major problems with it.  But the experience is enough that I can safely say…

 

TypeScript is not appropriate for new developers to use!

 

Simply put, unless you have a decent amount of experience with JavaScript, you really shouldn’t use TypeScript.  You will have to read the generated code at some point in time, and if you don’t full understand the code it generates, you are doomed.  The minute definition files arent available to you the experience becomes a hell of a lot less fun.  The process of mapping TypeScript types to existing JavaScript libraries is not trivial and requires you to have a pretty good understanding of both languages.  Especially when the library you are trying to define uses a number of clever JavaScript tricks, which basically… is all of them.  The fact there isnt a reliable tool out there for generating at least boilerplate .d.ts files from .js files is a bit of a puzzle to me.

 

Next, I also have to say TypeScript’s handling of this is just as mind bogglingly stupid as JavaScript’s.  That fat arrow ( => ) functions are used to capture local context, until used as an anonymous method, at which point they capture global (window) context, forcing you to resort to function() is downright perplexing.  I simply couldn’t get anonymous callback functions to have the proper this context no matter what syntax combination I tried.  Infuriatingly, the _this value Typescript automatically contains was set to the right value.

 

One other major downside I noticed about the language with my recent struggles is the newness of the language is very much an annoyance.  When researching bugs or workarounds you quite often find things that are reported as bugs, reported as changed,  or reported and never responded to.  This isn’t a bash on the language as it’s really only a problem that time can solve.  However, for a new developer, dealing with a language where a great deal of the material out there is potentially wrong because of language changes, that is certainly a challenge.  All languages change over time of course, but young languages change more and more dramatically.

 

Don’t get me wrong, I am not off Typescript, even though it spent a good part of the last two days pissing me off.  At least until ECMAScript 6 is the norm I can see a great deal of value in using TypeScript for large projects.

 

For beginners though, with little JavaScript experience… forget about it.  It’s going to cause more headaches than it’s worth.

Programming


Scroll to Top