Flying DNA. Three.js part 4
Last week at work I was trying to put together a stylised DNA icon in Abobe Illustrator. In order to fake the 3D look of DNA I was squishing shortening the paths, it looked ok but deep inside I knew that this was not the right thing to do.
I experimented with Illustrator’s 3D extrude and bevel effects to try and build a 3D shape but I didn’t get too far. I also wondered about building a 3D DNA shape in the copy of Cinema4D lite that now ships with After Effects CC. But with a deadline looming I didn’t really have time to pick up a new application from scratch. (That’s coming though).
It was only later that I had the idea to try building some DNA with a sweet procedural function in Three.js. So that’s just what I did.
See the Pen 3D dna with three.js by chris-creditdesign (@chris-creditdesign) on CodePen.
And luckily it turned out to be pretty easy! Here’s a rough guide to how I went about it.
Before I do anything else I’ll set the background colour of the canvas element to the same colour I had used in Illustrator.
canvas {
width: 100%;
height: 100%;
background-color: #DABDD8;
}
Next up I’ll make a note of the other colours used and store them in some variables for easy access. I can’t go on just using hotpink
and #BADA55
for the rest of my life.
var blue = 0x84D0F0;
var yellow = 0xFED162;
var purple = 0x651E59;
Because I know the DNA strand will end up being quite big I’ll move the camera back a bit further than in my previous experiments to make sure I get it all in shot.
camera.position.z = 20;
Now, whilst we could build up some shapes that resembled the DNA illustration by just squashing and stretching a bunch of CubeGeometry
s I don’t want the DNA to look like it was built in minecraft. It’s time to introduce some curves into our 3D world.
I’m going to introduce two new three.js shapes, a SphereGeometry
and a CylinderGeometry
. Check out the docs for a complete list of the other shapes available.
I’ll use two CylinederGoemetry
s next to each other to make up the connecting base pairs, one yellow and one blue. For now I’ll just create a variable to hold our geometry.
var tubeGeometry = new THREE.CylinderGeometry(0.3,0.3,6,32);
The arguments passed in correspond to radius top and radius bottom - you would make these different from each other if you wanted to make some sort of flat topped cone - height/length and radius segments. The radius segments refer to how many flat faces you want to combine to create the illusion of a curved surface - remember everything is flat in the 3D world if you look close enough. The default amount is 8 which will give you a chunky octagon shape. I’ve found that 32 gives quite a convincing result. I might be massively overloading the GPU but I’ll worry about that later.
By the way, the units used in three.js, such as a length of 6 and a radius of 0.3 don’t actually correspond to things in the real world such as pixels on screen or millimetres as you might expect. I’m not going to claim to fully understand the process here but they stem from the units used in OpenGL, the big daddy of WebGL. They exist only in relation to each other. Something clever takes these units and converts them into pixel coordinates on screen… that’s all I know right now but it’s enough to get going.
I’ll also create a SphereGeometry
for the balls sitting on the ends of the pairs.
var ballGeometry = new THREE.SphereGeometry(0.8,32,32);
To get a convincing ball we just need to pass in a value for the radius and the number of horizontal and vertical segments. I’ll choose 32 segments once again to get a nice smooth look.
For each of the colours used I’ll set up a material. Because I don’t want any shading on my model I’ll revert back to the MeshBasicMaterial
which just needs one argument, the colour to be passed in. This also means we can delete the lights from the previous example or not create any if you’re starting from scratch.
var blueMaterial = new THREE.MeshBasicMaterial( { color: blue } );
var yellowMaterial = new THREE.MeshBasicMaterial( { color: yellow } );
var purpleMaterial = new THREE.MeshBasicMaterial( { color: purple } );
Building the building blocks of life
Now we can start adding some shapes to our stage
var blueTube = new THREE.Mesh(tubeGeometry, blueMaterial);
scene.add(blueTube);
var yellowTube = new THREE.Mesh(tubeGeometry, yellowMaterial );
scene.add(yellowTube);
Here we’re creating two cylinders (I’m using tube in the variable names because it’s easier to spell), one blue and one yellow of exactly the same size because they’re both built from the same geometry.
However that’s not what you’ll see if you take look in your browser right now. Because both cylinders are added to the scene at exactly the same spot (perfectly centred in the middle of the scene at coordinate 0,0,0) only one will be visible.
To get the arrangement we want we’ll first rotate each cylinder by 90º around the z axis so they are lying down. The * Math.PI/180
part simply converts from radians to degrees to make the angles a bit easier to understand. Next we’ll move the blue cylinder to the left by half its length and the yellow cylinder to the right by half its length so they are both lying end to end, still centred on the scene.
blueTube.rotation.z = 90 * Math.PI/180;
blueTube.position.x = -3;
yellowTube.rotation.z = 90 * Math.PI/180;
yellowTube.position.x = 3;
Next we’ll add two purple balls to the scene and move one to the left and the other to the right so they sit right on top of the ends of our cylinders.
var ballRight = new THREE.Mesh( ballGeometry, purpleMaterial );
ballRight.position.x = 6;
scene.add(ballRight);
var ballLeft = new THREE.Mesh( ballGeometry, purpleMaterial );
ballLeft.position.x = -6;
scene.add(ballLeft);
What’s the procedure
All right! Now we have something on screen that looks like one of the DNA pairs from the illustration. But how do we go about drawing and positioning the other zillion we need?
Well we could use brute force and write a few lines of code for each pair, manually working out where they should be positioned and how much they should be rotated and typing those numbers in. But that’s not really what humans are good at and it’s exactly the kind of thing we should be letting the computer do for us.
It’s time to bust out our old friend the for loop.
var dna = new THREE.Object3D();
for (var i = 0; i <= 40; i++) {
var blueTube = new THREE.Mesh(tubeGeometry, blueMaterial);
blueTube.rotation.z = 90 * Math.PI/180;
blueTube.position.x = -3;
var yellowTube = new THREE.Mesh(tubeGeometry, yellowMaterial );
yellowTube.rotation.z = 90 * Math.PI/180;
yellowTube.position.x = 3;
var ballRight = new THREE.Mesh( ballGeometry, purpleMaterial );
ballRight.position.x = 6;
var ballLeft = new THREE.Mesh( ballGeometry, purpleMaterial );
ballLeft.position.x = -6;
var row = new THREE.Object3D();
row.add(blueTube);
row.add(yellowTube);
row.add(ballRight);
row.add(ballLeft);
row.position.y = i*2;
row.rotation.y = 30*i * Math.PI/180;
dna.add(row);
};
It might look like there’s a lot going on here but it’s basically the same as the code we were using to build one row except we’re looping from zero to 40 (I just picked that number out of the air) and each time adding a new pair that is a little higher and a little bit more rotated. In order to do that I’ve introduced a new concept the Object3D
.
We can use an Objetc3D
in exactly the same way we would use a group in Illustrator. Within the for loop we create a new Object3D
called row
and add the two cylinders and the two balls to it. Now instead of needing to calculate the translations for each object individually we can move them as a group which is a hell of a lot easier.
I also created another Object3D
called dna outside of the for loop to collect and group together all of the pairs as they are created.
Sweet. Lets add this dna to the screen to see what we’ve got.
… go on.
Well, that half looks like what we expected. It’s true that we are incrementally adding DNA pairs to the stage and each one has moved up a little and twisted a little bit more and in doing so we’ve saved ourselves lots of typing and clever thinking but… depending on the size of your screen you can probably only see ten pairs max.
To get around this I’ll just move the dna strand down so it’s centred on the screen. I know I created 40 pairs and each one is 2 units up from the last so the combined height of our strand from the centre of the bottom pair should be 80 units. So I can centre the strand on the screen simply by moving it down by half it’s height.
dna.position.y = -40;</code></pre>
Nice. Now we’re getting a pretty good view of the DNA on screen. You could stop here and be pretty proud of yourself. However, I wanted to finish this demo off my making the DNA spin around just like we did with the pink box. But if I add:
dna.rotation.x += 0.01;
dna.rotation.z += 0.01;
to the render function I get some pretty unpredictable results. That’s because even though we’ve added all of these balls and cylinders the dna Object3D
and it now reaches up into the sky, its centre is still considered to be at the centre of the scene at the coordinates 0,0,0. When we then move it down by setting its y position to -40 its centre is now at the coordinates 0, -40, 0.
When we apply the rotations to the dna directly it rotates around its centre 40 units down causing it to be out of shot for big chunks of time. Not very impressive.
Now there are number of ways to get around this. One perfectly acceptable way would be to move the camera down to match the centre of the dna object. Try it if you like. This is more entertaining but the DNA is rotating in a quite different fashion to our box and doesn’t look as interesting to my eyes.
My solution is to create yet another Object3D
:
var holder = new THREE.Object3D();</code></pre>
And add the dna object to this Object3D
and add it to the scene:
holder.add(dna)
scene.add(holder);
and preform those rotations on our new holder Object3D
:
holder.rotation.x += 0.01;
holder.rotation.y += 0.01;
Now holder is rotating around its centre which is also the centre of the scene and we get much more predictable and dare I say it impressive results.
One final thing I’ll add just for fun is a zoom control with our new found dat.GUI skills:
var CubeConfigData = function() {
this.zoom = 20;
};
var view = new CubeConfigData();
var gui = new dat.GUI();
gui.add( view, 'zoom', 0, 20 ).onChange( function(value) {
camera.position.z = value;
});
Just one control this time that lets us control the camera’s z coordinate and gives the effect of zooming in and out on the DNA. I find you can get some pretty nice views by hovering around the 3-5 mark.
And there we have it. I think you’ll agree that we have a pretty impressive demo here and we’ve only had to write about 80 lines of javascript thanks to the magic of three.js.
Thank you for reading and once again, if anything I’ve said anything here is complete rubbish (and that’s very likely) please give me a shout on twitter @creditdesign and I’ll try to put things right.