DEV Community ๐Ÿ‘ฉโ€๐Ÿ’ป๐Ÿ‘จโ€๐Ÿ’ป

DEV Community ๐Ÿ‘ฉโ€๐Ÿ’ป๐Ÿ‘จโ€๐Ÿ’ป is a community of 967,911 amazing developers

We're a place where coders share, stay up-to-date and grow their careers.

Create account Log in
Cover image for How to make a wormhole like Stargate SG-1 in JavaScript?
Mehdi Zed
Mehdi Zed

Posted on • Originally published at jesuisundev.com

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.

https://across-multiverse.com/

Ha Iโ€™m proud of my baby.
You donโ€™t know my game yet ?
Here's a 3 minutes trailer to get started.

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.

And I am right.

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 )
Enter fullscreen mode Exit fullscreen mode

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 )
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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)
}
Enter fullscreen mode Exit fullscreen mode

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)
  }
Enter fullscreen mode Exit fullscreen mode

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!

Top comments (5)

Collapse
 
polaroidkidd profile image
Daniel Einars

Amazing write up

Collapse
 
fausrguez profile image
Faus Rodriguez

This is awesome ๐Ÿ‘Œ๐Ÿ‘Œ๐Ÿ‘Œ

Collapse
 
adam_cyclones profile image
Adam Crockett

A wonderful effort

Collapse
 
r0zar profile image
Ross Ragsdale

Came for the SG1 reference. Stayed for the THREE.js tips.

In defense of the modern web

I expect I'll annoy everyone with this post: the anti-JavaScript crusaders, justly aghast at how much of the stuff we slather onto modern websites; the people arguing the web is a broken platform for interactive applications anyway and we should start over;

React users; the old guard with their artisanal JS and hand authored HTML; and Tom MacWright, someone I've admired from afar since I first became aware of his work on Mapbox many years ago. But I guess that's the price of having opinions.