Comment faire un trou de ver à la Stargate  SG-1 en JavaScript ?

Comment faire un trou de ver à la Stargate SG-1 en JavaScript ?

Aujourd’hui on va parler de l’effet qui a impressionné le plus de monde dans mon dernier projet en date. Un effet central qui faisait avancer l’intrigue du jeu. Un effet que j’ai forcé devant les yeux de tout le monde plusieurs fois tellement j’en étais fier : le trou de ver d’Across The Multiverse !



Dans l’épisode précédent

Cet article est la suite directe de l’article de lundi dernier où je t’expliquais comment j’ai créé l’univers tout entier. Oui oui. Un jeu 3D gratuit en JavaScript dans ton navigateur.

Un jeu qui vient de gagner un putain d’award prestigieux des internets.



J’ai codé l’univers entier dans le navigateur


Ha je suis fier de mon bébé. Tu connais pas encore mon jeu ? Allez petit trailer de 3 minutes seulement pour te donner envie d’y jouer.





Bref, on en était où ? Ha oui !



Comment construire un trou de ver ?

On est au moment du projet où je génère des champs d’étoiles à l’infini avec d’excellentes performances. À ce moment, le jeu magnifique tu vois dans la vidéo YouTube, il ressemble à ça.





Clairement à ce moment-là, les possibilités sont infinies. J’ai 10 000 idées à la seconde. Alors évidemment, je pense à ajouter des nébuleuses, des galaxies, des géantes rouges et tout ce qui fait l’univers.

Et on va y venir dans le prochain article.

Mais j’ai une idée en particulier qui me vient à l’esprit. Je me dis que si je peux générer un univers, ça veut dire que je peux en générer plusieurs ! Il y a plein de théorie autour du multivers.

Certaines théories affirment qu’un trou noir et en fait un trou de ver qui amène dans un autre univers.





Cette idée m’obsède !

J’ai absolument aucune idée de comment faire ça. Mais je sais que c’est possible de créer un trou de ver en JavaScript. Donc je me lance immédiatement là-dessus.

Étape 1 : Il me faut un visuel de référence.

Comme je le disais dans l’article précédent, j’ai toujours utilisé une image ou une vidéo de référence. Une représentation réelle de ce que je voulais recréer. Quelque chose à regarder pour me rapprocher le plus possible d’un rendu « réaliste ».

Très vite, je me décide à refaire l’effet de tunnel de la série stargate SG-1 !



Ça a l’air pas mal compliqué quand même cette affaire ! Presque trop compliqué pour les connaissances que j’ai à ce moment-là. Et que fait-on quand le problème est trop compliqué ?



Réduire la complexité

Construire un trou de ver ? Trop compliqué. Je ne sais même pas par où commencer. OK, alors réduisons ça.

Déplacer la caméra dans un tube ? Ça veut presque rien dire pour moi à ce moment-là. Il faut construire un tube d’abord !

Construire un tube courbé infini ? Toujours trop compliqué, réduisons ça.

Construire un tube tout simple ?

Ha !

Ça me semble faisable ça !

La complexité est assez réduite à mon goût.

Il est temps de s’y mettre.



Comment construire un tube tout simple ?

Je me dis tout de suite que faire un tube tout simple, dans une grande libraire comme Three.js., c’est sûr que ca existe déjà.

Et j’ai raison.

Un objet de géométrie tout prêt existe et est prêt à l’utilisation. Encore mieux, le code existe déjà. Ça veut dire que la première étape c’est quoi ?

Un gros copier-coller du code de la doc comme on aime.

Quel plaisir !

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 )


Maintenant, si on veut continuer notre trou de ver, il faut tout de même que l’on comprenne ce qu’on vient de copier-coller. Premièrement, on voit une classe CustomSinCurve qui en étend une autre THREE.curve.

Je comprends très vite que c’est la façon Three.js de créer des courbes en 3D !

C’est très bon que j’aie ça en tête à ce moment du développement du jeu. Je vais beaucoup utiliser les courbes dans le futur. Je suis content et je ne sais pas encore à quel point ça va être important.





Dans cette classe Custom, on trouve un simple constructeur et une fonction getPoint.

Je comprendrai plus tard en détail à quoi correspond cette dernière fonction. Je déduis pour le moment qu’elle est liée à la courbure du tube ! En effet, le modèle mathématique compliqué sur les trois axes (x, y, z) est ce qui permet cette forme du tube.

Le reste c’est du Three.js tout ce qui a de plus basique.

Je porte quand même mon attention sur ces deux lignes.

const path = new CustomSinCurve( 10 );
const geometry = new THREE.TubeGeometry( path, 20, 2, 8, false );

Là, je vois bien que le tube attend une courbe pour être affiché à l’écran. Et alors ? Et alors, ça veut dire qu’on peut donner la forme qu’on veut à notre tube !

J’ai mon prochain objectif !



Comment construire un tube courbé infini ?

Je rejette alors un oeil à mon modèle de référence, le trou de ver dans Stargate.

C’est une courbe en quasi virage permanent qui part un peu dans tous les sens. Cependant, ça ne fait pas non plus des folies de virage serré. En diraient plusieurs ellipses torturées.

Je décide de continuer à regarder la doc en quête d’une géométrie qui correspond à ce que je veux.

Et bingo.

La TorusKnotGeometry est une parfaite candidate pour la forme de mon trou de ver.

On a plus qu’à instancier une TorusKnot, prendre sa forme et l’ajouter à notre tube fait précédemment !

Facile.

Écrivons ça.

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)


Je suis très satisfait de la courbe de ce tube. On remarquera que j’ai géré le côté infini du tube de la façon la simple au monde.

En fermant le tube !

const wormholeGeometry = new THREE.TubeGeometry(wormhole.shape, 800, 5, 12, true)

En effet, le dernier paramètre de TubeGeometry est à true, ce qui ordonne au script derrière de rejoindre les deux extrémités du tube.

Ca c’est du quick win !

En tant que développeur, on est pas là pour faire des choses compliquées, on est là pour répondre aux besoins.

Pas besoin d’écrire trois kilomètres de hiéroglyphes pour arriver à ce qu’on veut !





Ça répond parfaitement à mon besoin sans se prendre la tête.

Exactement ce qu’on veut.

Maintenant, comment on bouge là-dedans ?



Comment déplacer la caméra dans un tube ?

L’idée c’est de faire avancer la caméra, à l’intérieur du tube, à chaque image. Vraiment, mettre les yeux de l’utilisateur dans le tube ! En réfléchissant un peu, je me dis qu’il suffit de mettre à jour la position de la caméra à chaque image.

Il suffit de le faire selon les courbures du tube.

Et là, je me suis rappelé de la fonction getPoint() dont je n’avais pas bien saisi l’utilité au début.

Cette fonction permet de retourner les coordonnées de la courbe selon une valeur donnée.

Attends… Mais, c’est pas EXACTEMENT ce qu’on veut ?

Il me semble que si.

Ceci dit, on aura la bonne position dans le temps pour la caméra, mais on n’aura pas le bon angle. Il faudra donc forcer la caméra à regarder devant soi, en permanence. La fonction lookAt de l’objet caméra va faire ça pour nous.

OK, écrivons ça.



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)
}


Et ça marche !

On a un beau voyage en mode montagne russe dans un tube.

Le plus dur est derrière nous, maintenant c’est juste de l’habillage.

Et la petite astuce pour faire un truc jolie c’est de mettre plusieurs matériels avec des textures différentes. Ensuite tu joues avec GSAP pour l’opacité et l’ordre d’apparition des matériels. Tu arrives très rapidement à des choses sublimes en jouant avec tout ça.

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)
  }

Comme dit dans l’article précédent, par souci de longueur et de simplification, parfois je n’explique pas tout. Et surtout, je ne montre pas tout. C’est le cas pour cette partie ou c’est juste de l’habillage et pas de la logique pure.

Tu as le source code entier si tu veux tout voir.

Et plus précisément la classe que gère le trou de ver est ici.



Épilogue

Et voilà, on a un parfait trou de ver. Très bon niveau performances et très jolie ! On est prêt pour passer d’univers en univers avec ça. Dans le prochain article, on va voir comment créer des nébuleuses et plus particulièrement des restants de supernova ! Les articles moins techniques seront de retour peu de temps après cette série.

Qui me parle ?

jesuisundev
Je suis un dev. En ce moment, je suis développeur backend senior / DevOps à Montréal pour un géant du jeux vidéo. Le dev est l'une de mes passions et j'écris comme je parle. Je continue à te parler quotidiennement sur mon Twitter. Tu peux m'insulter à cet e-mail ou le faire directement dans les commentaires juste en dessous. Y'a même une newsletter !

Pour me soutenir, la boutique officielle est disponible ! Sinon désactiver le bloqueur de pub et/ou utiliser les liens affiliés dans les articles, ça m'aide aussi.

T'en penses quoi ?

Your email address will not be published. Required fields are marked *