By: Julia Developers
Over the last three months, I’ve been working on Compose3D,
which is an extension of the amazing Compose package to 3D. My work on
Compose3D began as a project for my Computer Graphics course along with Pranav T Bhat,
and by the end of the course, we had a working prototype for Compose3D with support for contexts and geometries and a
very basic WebGL backend.
It has been my pleasure to have been able to continue this work under the guidance of Shashi Gowda
and Simon Danisch as a part of the first ever Julia Summer of Code, generously
sponsored by the Gordon and Betty Moore Foundation. While I’ve been able to add quite a lot of
functionality to Compose3D, it isn’t totally ready for release yet. Hopefully, in some time it
will be ready. But as a happy side effect, I have been able to abstract out the WebGL rendering functionality provided
by the original prototype (and a lot more!) to a separate package called
which can now be used to render 3D graphics in browsers using Julia, opening up possibilities of displaying such
scenes in IJulia notebooks and Escher.
ThreeJS is now responsible for all the WebGL rendering done by Compose3D. It can also be used as a standalone package for
other graphics packages to use as a backend.
IJulia notebook, which would then run it! This worked pretty well in IJulia notebooks, but it was soon apparent that
there were several flaws with this approach.
- It was hard to extend.
- Did not play well with Escher.
- Nor did it work with Interact to provide interactivity.
So Shashi suggested implementing a Polymer wrapper around the excellent
three.js library, to create threejs web components. The Polymer team had done some work on
creating threejs components and had a basic implementation of the same ready, which I promptly forked
Julia during JSoC!
Switching over to using web components suddenly opened up 2 major avenues. Compose3D could now work with Escher and
also provided interactivity. ThreeJS outputs Patchwork
elements, which lets it use Patchwork’s clever diffing capabilities, thereby updating only the required DOM elements and
On the other hand, web components introduced issues with IJulia notebooks regarding serving the files required by
ThreeJS. I’m still working on finding a good solution for this problem, but for now, a hack gets ThreeJS working in
IJulia, albiet with some limitations.
Anyway, now we were all set to draw 3D scenes in browsers! The below code snippet, for example, would draw a red cube
illuminated from a corner. The camera in the scenes drawn by ThreeJS can be rotated, zoomed and panned using your mouse
or trackpad, allowing you to explore the scene.
import ThreeJS ThreeJS.outerdiv() << (ThreeJS.initscene() << [ ThreeJS.mesh(0.0, 0.0, 0.0) << [ ThreeJS.box(1.0,1.0,1.0), ThreeJS.material(Dict(:kind=>"lambert",:color=>"red")) ], ThreeJS.pointlight(3.0, 3.0, 3.0), ThreeJS.camera(0.0, 0.0, 10.0) ])
Making them interactive
Currently, interactivity is broken in IJulia (a side effect of the switch to Polymer 1.0, and the new sneaky DOM),
so Escher is the way to go if you want to interact with your 3D scene. So an example for this can be the same scene as before,
but after adding a slider and make it such that the size of the cube is controlled by the slider.
import ThreeJS function main(window) push!(window.assets, "widgets") push!(window.assets, ("ThreeJS", "threejs")) side = Input(1.0) vbox( slider(1.0:5.0) >>> side, lift(side) do val ThreeJS.outerdiv() << (ThreeJS.initscene() << [ ThreeJS.mesh(0.0, 0.0, 0.0) << [ ThreeJS.box(val, val, val), ThreeJS.material(Dict(:kind=>"lambert",:color=>"red")) ], ThreeJS.pointlight(3.0, 3.0, 3.0), ThreeJS.camera(0.0, 0.0, 10.0) ]) end ) end
You can also do animations!
Small scale animations can also be created using Escher. Instead of using sliders to update the elements,
we just update it at certain intervals using the
every function or the
fpswhen functions. A scene with a
rotating cube can be drawn using just a couple of modifications of the above code.
import ThreeJS function main(window) push!(window.assets, "widgets") push!(window.assets, ("ThreeJS", "threejs")) rx = 0.0 ry = 0.0 rz = 0.0 delta = fpswhen(window.alive, 60) #Update at 60 FPS lift(delta) do _ rx += 0.5 ry += 0.5 rz += 0.5 ThreeJS.outerdiv() << (ThreeJS.initscene() << [ ThreeJS.mesh(0.0, 0.0, 0.0) << [ ThreeJS.box(2.0, 2.0, 2.0, rx = rx, ry = ry, rz = rz), ThreeJS.material(Dict(:kind=>"lambert",:color=>"red")) ], ThreeJS.pointlight(3.0, 3.0, 3.0), ThreeJS.camera(0.0, 0.0, 10.0) ]) end end
Surf and mesh plots! (Sort of)
ThreeJS has support to render parametric surfaces, which are basically the kind of surfaces drawn by
surf plot. It also has support for drawing lines like a typical
mesh plot. Colormaps can
be applied to these surfaces by passing in an array of colors to be used. Colors to be applied are
calculated and chosen by ThreeJS. These come into effect when put together with materials using the
vertex. Screenshots of such surfaces drawn by ThreeJS are shown below.
Compose3D provides an abstraction over the rendering library and lets you compose together primitives to
build scenes just like the inspiration for it, the Compose library. This lets you create very interesting
structures, with very less code! Compose3D has similar features to Compose, with users being able to create 3D contexts, and then use relative and absolute measures inside them and compose other primitives together.
My favorite example to showcase Compose3D would be the Sierpinski pyramid example. Here, we split the parent context
into the sections that we want and then just draw the pyramid in them! So the bottom half of the 3D space is split into 4,
and then, a pyramid is arranged on top of them.
using Compose3D function sierpinski(n) if n == 0 compose(Context(0w,0h,0d,1w,1h,1d),pyramid(0w,0h,0d,1w,1h)) #The basic unit else t = sierpinski(n - 1) compose(Context(0w,0h,0d,1w,1h,1d), (Context(0w,0h,0d,(1/2)w,(1/2)h,(1/2)d), t), (Context(0w,0h,0.5d,(1/2)w,(1/2)h,(1/2)d), t), (Context(0.5w,0h,0.5d,(1/2)w,(1/2)h,(1/2)d), t), (Context(0.5w,0h,0d,(1/2)w,(1/2)h,(1/2)d), t), (Context(0.25w,0.5h,0.25d,(1/2)w,(1/2)h,(1/2)d), t)) #The top one end end compose(Context(-5mm,-5mm,-5mm,10mm,10mm,10mm),sierpinski(3))
And voila! You have a Sierpinski pyramid of level 3 like in the figure below.
The switch to ThreeJS allows Compose3D all the advantages that comes with ThreeJS. This includes interactivity
For example, the same Sierpinski example can be have some interactive elements, say a slider defining the
number of levels of recursion and maybe some controlling the colors of the pyramid. This can be done easily
in Escher just like it was done with ThreeJS. After defining the
sierpinski function given below, just creating a slider
and hooking it up to the
sierpinski function will set this up!
function main(window) push!(window.assets, ("ThreeJS", "threejs")) #Push the threejs static assets push!(window.assets, "widgets") n = Input(0.0) vbox( slider(0.0:3.0) >>> n, #Set up the slider lift(n) do i #Draw the composed figure! draw( Patchable3D(100,100), compose( Context(-5mm,-5mm,-5mm,10mm,10mm,10mm), sierpinski(i) ) ) end ) end
An an example for animations, I ported the Escher boids example by Ian Dunning from 2D to 3D and a screencast of the same can be found below.
- Several new primitives have been added in ThreeJS which don’t yet have corresponding primitives in Compose3D.
- Add support for text in ThreeJS allowing use of labels in plots.
- Being able to use
meshthat will automatically draw scaled surface plots in browsers and a WebGL based
plotting library around ThreeJS.
- Actually get Compose3D ready for public use!