How to make a wormhole like Stargate SG-1 in JavaScript?
Today we’re going to talk about the effect that impressed the most people in my latest project. A central effect that moved the game’s plot forward. An effect that I forced in front of everyone’s eyes several times because I was so proud of it: the wormhole of Across The Multiverse!
In the previous episode
This article is the direct continuation of last Monday’s article where I explained how I created the whole universe. Yes, yes. A free 3D game in JavaScript in your browser.
A game that just won a prestigious fucking internet award.
Ha I’m proud of my baby. You don’t know my game yet? Come on, a 3 minutes trailer to make you want to play it.
Anyway, where were we? Oh yes!
How to build a wormhole?
We are at the point in the project where I generate infinite star fields with excellent performance. At this moment, the beautiful game you see in the YouTube video, it looks like this.
Clearly at that point, the possibilities are endless. I have 10,000 ideas per second. So obviously I’m thinking of adding nebulae, galaxies, red giants and everything else that makes up the universe.
And we’ll get to that in the next article.
But I have one particular idea that comes to mind. If I can generate one universe, it means that I can generate several! There is a lot of theory around the multiverse.
Some theories claim that a black hole is in fact a wormhole that leads into another universe.
I am obsessed with this idea!
I have absolutely no idea how to do that. But I know it’s possible to create a wormhole in JavaScript. So I’m going to get started on that right away.
Step 1: I need a visual reference.
As I said in the previous article, I always used a reference image or video. A real representation of what I wanted to recreate. Something to look at to get as close as possible to a “realistic” rendering.
Very quickly, I decide to do the tunnel effect of the stargate SG-1 series again!
It looks pretty complicated, though! Almost too complicated for the knowledge I have at this time. And what do you do when the problem is too complicated?
Reducing complexity
Building a wormhole? Too complicated. I don’t even know where to start. OK, so let’s narrow it down.
Moving the camera in a tube? That means almost nothing to me at this point. You have to build a tube first!
Building an infinite curved tube? Still too complicated, let’s reduce it.
Build a simple tube?
Ha !
That sounds doable to me!
It’s time to get started.
How to build a simple tube?
I immediately think that making a simple tube, in a big library like Three.js., it already exists.
A ready-made geometry object exists and is ready to use. Even better, the code already exists. This means that the first step is what?
A big copy and paste of the code from the doc as we like.
class CustomSinCurve extends THREE.Curve { constructor( scale = 1 ) { super() this.scale = scale } getPoint( t, optionalTarget = new THREE.Vector3() ) { const tx = t * 3 - 1.5 const ty = Math.sin( 2 * Math.PI * t ) const tz = 0 return optionalTarget.set( tx, ty, tz ).multiplyScalar( this.scale ) } } const path = new CustomSinCurve( 10 ) const geometry = new THREE.TubeGeometry( path, 20, 2, 8, false ) const material = new THREE.MeshBasicMaterial( { color: 0x00ff00 } ) const mesh = new THREE.Mesh( geometry, material ) scene.add( mesh )
Now, if we want to continue our wormhole, we have to understand what we just copied and pasted. First, we see a CustomSinCurve class that extends another THREE.curve class.
I understand very quickly that this is the Three.js way to create 3D curves!
It’s very good that I have that in mind at this point in the development of the game. I’m going to use curves a lot in the future. I’m happy and I don’t know yet how important it will be.
In this Custom class, there is a simple constructor and a getPoint function.
I will understand later in detail what this last function corresponds to. I deduce for the moment that it is linked to the curvature of the tube! Indeed, the complicated mathematical model on the three axes (x, y, z) is what allows this shape of the tube.
The rest is just basic Three.js.
I still pay attention to these two lines.
const path = new CustomSinCurve( 10 ); const geometry = new THREE.TubeGeometry( path, 20, 2, 8, false );
Here, I can see that the tube is waiting for a curve to be displayed on the screen. So what? So, it means that we can give our tube the shape we want!
I have my next goal!
How to build an infinite curved tube?
I then cast an eye back to my reference model, the wormhole in Stargate.
It’s a permanent near-curve that goes a bit in every direction. However, it doesn’t do any tight turn madness. Looks like several tortured ellipses.
I decide to keep looking at the doc in search of a geometry that fits what I want.
And bingo.
The TorusKnotGeometry is a perfect candidate for my wormhole shape.
All we have to do is instantiate a TorusKnot, take its shape and add it to our previously made tube!
Easy.
Let’s write this down.
const wormhole = {} wormhole.shape = new THREE.Curves.TorusKnot(500) const wormholeMaterial = new THREE.MeshBasicMaterial({ map: null, wireframe: true }) const wormholeGeometry = new THREE.TubeGeometry(wormhole.shape, 800, 5, 12, true) const wormholeTubeMesh = new THREE.Mesh( wormholeGeometry, wormholeMaterial ) scene.add(wormholeTubeMesh)
I am very happy with the curve of this tube. You will notice that I managed the infinite side of the tube in the simplest way in the world.
By closing the tube!
const wormholeGeometry = new THREE.TubeGeometry(wormhole.shape, 800, 5, 12, true)
Indeed, the last parameter of TubeGeometry is set to true, which instructs the script behind to join the two ends of the tube.
That’s a quick win!
As a developer, we’re not here to do complicated things, we’re here to respond to needs.
You don’t need to write three kilometers of hieroglyphics to get what you want!
It perfectly meets my needs without taking the headache.
Exactly what we want.
Now, how do we move in there?
How to move the camera in a tube?
The idea is to move the camera forward, inside the tube, with each image. Really, put the user’s eyes inside the tube! Thinking about it, I say to myself that it is enough to update the position of the camera at each image.
Just do it according to the curves of the tube.
And then I remembered the getPoint() function which I didn’t really understand at first.
This function returns the coordinates of the curve according to a given value.
Wait… But isn’t that EXACTLY what we want?
It seems to me that it is.
This said, we will have the right position in time for the camera, but we will not have the right angle. So we will have to force the camera to look forward, permanently. The lookAt function of the camera object will do this for us.
OK, let’s write this down.
let wormhole = { CameraPositionIndex: 0, speed: 1500 } function updatePositionInWormhole () { wormhole.CameraPositionIndex++ if (wormhole.CameraPositionIndex > wormhole.speed) { wormhole.CameraPositionIndex = 0 } const wormholeCameraPosition = wormhole.shape.getPoint(wormhole.CameraPositionIndex / wormhole.speed) camera.position.x = wormholeCameraPosition.x camera.position.y = wormholeCameraPosition.y camera.position.z = wormholeCameraPosition.z camera.lookAt(wormhole.shape.getPoint((wormhole.CameraPositionIndex + 1) / wormhole.speed)) renderer.render(scene, camera) } function animate() { updatePositionInWormhole() requestAnimationFrame(animate) }
And it works!
We have a beautiful trip in roller coaster mode in a tube.
The hardest part is behind us, now it’s just dressing up.
And the little trick to make something pretty is to put several materials with different textures. Then you play with GSAP for the opacity and the order of appearance of the materials. You can get sublime things by playing with all this.
this.wormholeTubeMesh = SceneUtils.createMultiMaterialObject(this.wormholeGeometry, [ this.wireframedStarsSpeederMaterial, this.auraSpeederMaterial, this.nebulaSpeederMaterial, this.starsSpeederMaterial, this.clusterSpeederMaterial ]) async animate () { this.wormholeTimeline = gsap.timeline() // initial massive boost at wormhole enter this.wormholeTimeline .to(this.starsSpeederMaterial, { duration: 7, opacity: 1 }, 0) .to(this.wireframedStarsSpeederMaterial, { duration: 7, ease: 'expo.out', opacity: 1 }, 0) .to(this.auraSpeederMaterial, { duration: 7, ease: 'expo.out', opacity: 1 }, 0) .to(window.wormhole, { duration: 7, ease: 'expo.out', speed: 2500 }, 0) // adding speed and noises this.wormholeTimeline .to(this.clusterSpeederMaterial, { duration: 6, opacity: 1 }, 7) .to(this.auraSpeederMaterial, { duration: 2, opacity: 0 }, 7) .to(window.wormhole, { duration: 6, speed: 2000 }, 7) // adding speed and nebula distorded this.wormholeTimeline .to(this.nebulaSpeederMaterial, { duration: 6, opacity: 1 }, 13) .to(this.clusterSpeederMaterial, { duration: 6, opacity: 0 }, 13) .to(this.auraSpeederMaterial, { duration: 6, opacity: 0.7 }, 13) .to(window.wormhole, { duration: 6, speed: 1800 }, 13) if (!window.isMobileOrTabletFlag) { window.controls.velocity.x = 0 window.controls.velocity.z = 0 } return this.wormholeTimeline.then(() => true) }
As said in the previous article, for the sake of length and simplification, sometimes I don’t explain everything. And especially, I don’t show everything. This is the case for this part where it’s just a presentation and not pure logic.
You have the full source code if you want to see everything.
And more precisely the wormhole class is here.
Epilogue
And here we have a perfect wormhole. Very good performance and very pretty! We are ready to go from universe to universe with this. In the next article, we will see how to create nebulae and more particularly supernova remnants!