Three.js + F# + Fable = ❤
No wonder that by now, pretty much everyone who uses F# for fun & profit and does web development, has already tried Fable and played with SAFE Stack, talked/blogged about their experiences, used these technologies in production successfully, created related tools and libraries or contributed to them. I’ve tried and played as well, however, haven’t yet produced anything useful for others (neither Fable nor F# in general). F# Advent Calendar seemed like a good opportunity to fix that by putting some real pressure on me, so I decided to participate and do something Fable.
This post is a story about my journey: choosing what to do, showing the results, the process, and lessons learned. In the end, I share a step-by-step guide on how to do something similar, along with the links to helpful resources. So, if you don’t feel like reading the long story part, feel free to skip it and jump to ‘Results’ and then to ‘Want To Do Something Similar? Here is a Short Guide’.
Choosing the Topic and Getting Started
To back up my choice a bit. I have been interested in computer graphics for a while, though not in a nerdy, knowing-the-math-behind-it way, but rather in a geeky, playing-with-existing-tools way: had my fun with 3Ds MAX in high school, created a very simple Mesh object viewer in C# using Direct3D as a mid-term project in college, and my graduate project was an attempt to restore three-dimensional objects based on pictures of them taken from different angles. After that, there were 8 years of enterprise web development, and 3D fun has become just a faint but pleasant memory. So hey, why don’t I do something CG-related here? Even doing a small step in that direction would be sweet.
I looked more at Three.js, and my mind was blown by it. As I haven’t been closely following the evolution of CG programming for a while, I only had a vague idea of what is possible now, in 2018. Wow! Just look at this sample, for instance. Or this one. It all works in the browser without lagging, and not much code is needed to create such things! This seems even more amazing to me than WiFi on planes.
Having cloned the repository, I tried to build and run the code, but failed and then found out that the repo is pretty much dead now. Comments in the GitHub issue led me to the newer repos, one with the new bindings and one with Pixi Samples (it is a neat 2D graphics library). No new Three.js bindings were there though, so I needed to generate them on my own, which was fine, as I knew that a tool that could do the job existed.
As I opened the TS definitions for Three.js, I got scared for a bit. Tons of files. What if ts2fable cannot do the generation for multiple files with inter-dependencies? Luckily, it can since not too long ago. Hooray!
Having installed ts2fable and downloaded the type definitions into ‘three’ folder, I ran the following command:
ts2fable three/index.d.ts TestCompile/ThreeJs.fs -e three
You have probably guessed that index.d.ts file is the ‘entry point’ file. As a result, I got a huge bundle file with 6K+ lines of F# code. I included this file into a test project, and it compiled successfully after I made a minor fix of adding ‘interface end’ to empty interface definitions. Pretty cool! However, I ran into an issue while trying to run the application. The errors were of the following kind:
FABLE: Cannot have two module members with same name: Camera
And many more like that. It turned out to be a BABEL bug (or limitation ¯\_(ツ)_/¯). I looked at Three.js type definitions and found out that there is a Core module, and types from it are used in other modules with aliases, which apparently leads to a mess when Fable -> BABEL -> JS things are happening. I thought, ‘OK, what if I find the simplest sample possible and use Core part only?’ Commented everything out except for Core — nope, not going to work, as other modules depend on Core, and Core depends on other modules — lovely! And oh, that is why the namespace in my huge generated F# definitions file is declared as rec, which by the way leads VS Code to go crazy and hang every once in a while (I don’t blame it! 6K lines, rec…). I ended up throwing this file into a gist to have syntax highlighting, and using it to look things up, as I was not going to keep it in the project anyway.
Given the above, my goal in this adventure has become the following:
- Take not the coolest, but the simplest sample than only uses Three.js and nothing else, because time -> 0
The sample I ended up picking is the following one: camera_array. The idea is to display a scene where N instances of a 3D object are rendered in an animated fashion, turning around. There is also a basic light set up and a background texture that helps to demonstrate the shadow the object casts as it turns, being lit by the light.
Output, Visual Part
I have changed the sample a tiny bit for fun. Just tweaked the cylinders’ dimensions to transform them into cones (these are Hershey’s kisses!) and modified the colors used. This is as ‘Adventsy’ as I could get it without using textures or custom models. Chocolates and Christmas colors!
In case you haven’t seen these golden Hershey’s kisses in real life, here’s what they look like:
Output, Code Part
I have pushed the code for the sample here: w0lya/fable-three
The project is based on the minimal template for Fable 2, so if you follow the instructions in the template, everything should work for you.
And here is the ts2fable output in a gist: Fable.Import.Three.fs
After I got all the setup and preparation done, the implementation itself was pretty straightforward and not as full of adventures as the getting started part. Just had to figure a few things out.
- The first thing I faced was being able to create objects using constructors with parameters. If I had the type, not interface definitions, it would have been pretty straightforward. For interfaces, I ended up doing the following:
// Out initial type as-is.
type [<AllowNullLiteral>] Vector4 =abstract x : float with get,setabstract y : float with get, setabstract z : float with get, setabstract w : float with get, set// Add this as a constructor with optional parameters:
and [<AllowNullLiteral>] Vector4Type =[<Emit("new THREE.$0($1...)")>] abstract Create: ?p1:obj * ?p2:obj * ?p3:obj * ?p4:obj -> Vector4// Further in the type definition file, add a type with global exports for 'constructing' your objects.
type [<Erase>] Globals =
[<Global>] static member Vector4 with get(): Vector4Type = jsNative and set(v: Vector4Type): unit = jsNative
Given that, I can write the following code:
subcamera.bounds <- Globals.Vector4.Create( (x / amount, y / amount, size, size )
subcamera.bounds = new THREE.Vector4(x / amount, y / amount, size, size);
You have probably noticed [<Emit>], [<Erase>], and [<Global>] attributes in the code. They are pretty intuitive: the first one overrides what JS code is being generated based on your F# code, the second one marks things to ignore when generating JS, and the third one denotes the things that are to be globally accessible in your resulting JS. To learn more about these, check out the links I share at the end of the post.
2. The second thing that caused a bit of trouble was that in the original JS, there are a few global variables. Unfortunately, I had to overcome that issue using not the nicest kind of Emit hackery. My problem was that if I declare the global variables this way,
let globalCamera = Globals.ArrayCamera.Create()
const globalCamera = new THREE.ArrayCamera()
If I add ‘mutable’, the global declaration is then treated as a function, and if I try to set some properties on it, I get
globalCamera().propertyname = ...
which is not what I want.
To overcome this, I had to define global things 2 times, one to make JS happy and to actually have vars there, and one to make F# happy:
// 1) To actually have vars in JS
[<Emit("var globalScene, globalMesh, globalCamera, globalRenderer;")>]
let globalVarsDeclaration: unit = jsNative// 2) For F# not to be sad. [<Erase>] helps to exclude this stuff when generating JS.let [<Erase>] globalCamera = Globals.ArrayCamera.Create()let [<Erase>] globalScene = Globals.Scene.Create()let [<Erase>] globalRenderer = Globals.WebGLRenderer.Create()let [<Erase>] globalMesh = Globals.Mesh.Create()
And of course it doesn’t work :) This led me to more questionable hackery. I declared a local camera, scene, mesh and renderer inside the init() function, as unlike global, local ‘let mutable’ declaration results in reasonable JS and allows setting the properties as needed. Having the local objects set up properly, I then assigned them to the global variables 2 times each, one through Emit (for JS), one just plain assignment (for F#). I am not adding code snippets for this part here not to overload the post, but if you are curious, it’s all in the App.fs file. Though, I am pretty sure that a nicer way to overcome these issues exists.
Other than that, there were bits in the type definitions to be tweaked a bit, e.g. a method missing, or optional parameters needed, or a base type needed. But overall, I was able to get things working relatively painlessly.
Here I will just list the tools I was using. I was doing everything on Linux (Ubuntu 18.04), so it affected some of the choices.
- Visual Studio Code with Ionide
- Chromium developer tools
- gitzip to get small pieces of large repos (e.g. three Typescript bindings)
- ts2fable, both as a terminal tool and online
- new Fable 2.0 Online REPL
- legacy Fable REPL because the new REPL would output things that scared me. e.g. this:
let returnVal$$1 = Mesh$$$$002Ector();Mesh$$set_geometry$$Z47ACBE6C(returnVal$$1, geometry);
Want To Do Something Similar? Here is a Short Guide
If you are feeling adventurous and would like to port one of the more exciting Three.js samples (or do a similar thing with any other JS library that has TypeScript definitions available), here is a recap.
- Find your library’s TypeScript definitions here and download using a tool like gitzip
- Install ts2fable, in the directory with TS stuff run the command (see the readme for the right one for your case)
- Create an F# project, e.g. by copying and using the minimal template for Fable 2. Add the F# TS file there and reference in the .fsproj. You can either check that the file is good in the project by building and running it, or throw the file into online REPL and try running it.
- If you are not lucky, you will have to create your own type definitions manually, tweak them to support constructors, correct JS emission etc. :)
6. Write your F# code in App.fs and make sure it gets transformed into working JS :)
- This may be perfect for you if you want to work with a small simple library: Fable for busy moms and dads: use a JS lib in one minute!
- Awesome Fable
Possible Next Steps
Possibilities are endless. For me or anyone who got excited about this too, here are a few ideas to consider implementing:
- Create a full-blown SAFE stack-based app with the 3D awesomeness in it.
- Add interaction with the user, make the app configurable: change count, color and what not. This could even become a Dojo?
- Re-arrange the logic flow and make F# code look more like F# code we love than the code we don’t
- Think about how to overcome the Babel naming-related bug. how can I automagically re-generate the type definitions, so that they wouldn’t have this issue, and at the same time the JS output would be instantly usable, without having to do namespace hackery. This would be amazing, as it would ‘unlock’ the entire Three.js library for usage from F#!
Any feedback/questions/comments/criticism/trolling will be very much appreciated :) Feel free to bug me here or on Twitter.