How to make a 3D Website(Threejs) .

How to make a 3D  Website(Threejs) .

Welcome Coders! 💻️

In this guide, we'll explore the process of crafting an incredible interactive 3D website.

Before we proceed, take a look at my 3D Portfolio Website.

Let's get started! 👍️

Prerequisites:

  • Basic knowledge of Node.js

  • Installed Node.js and npm

Choosing the framework for creating a 3D website.

There are several libraries and frameworks for creating 3D websites.

Here are some examples:

  • Three js: A widely used JavaScript library for creating 3D graphics on the web. It provides a comprehensive set of tools for rendering 3D scenes and animations.

  • Babylon.js: Another powerful JavaScript framework for building 3D games and experiences on the web. It offers features for rendering, physics, collisions, and more.

  • WebGL (Web Graphics Library): Though not a framework, WebGL is a JavaScript API for rendering interactive 3D and 2D graphics within any compatible web browser.

For today's articles, our choice is to harness the power of Three.js as the primary tool to craft our 3D website.

🌟 Check out Threejs Examples on their website.

Inspiration

We're drawing inspiration from an amazing 3D artwork by Bob.Ho for our own website.

Our site will have a character standing on a pole, and we'll add a cool Sharingan Eye. The background will show an ocean and a night sky with a bright moon.

Let's setup our project

Every three.js project needs at least one HTML file to define the webpage, and a JavaScript file to run your three.js code.

  • index.html

      <!DOCTYPE html>
      <html lang="en">
          <head>
              <meta charset="utf-8">
              <meta name="viewport" content="width=device-width, initial-scale=1">
              <title>My first three.js app</title>
              <style>
                  body { margin: 0; }
              </style>
          </head>
          <body>
              <script type="module" src="/main.js"></script>
          </body>
      </html>
    
  • main.js

      import * as THREE from 'three';
    
      //...
    

/public

  • The public/ folder is sometimes also called a "static" folder because the files it contains are pushed to the website unchanged. Usually textures, audio, and 3D models will go here.

Installing Threejs library and a build tool (vite)

With a build tool, importing local JavaScript files and npm packages should work out of the box.

From the project folder terminal run the following code.

# three.js
npm install --save three 

# vite
npm install --save-dev vite

The installation will add node_modules/ and package.json to the project folder

Here is the folder structure (reference):

Setting the scene

Our scene comprises the following elements:

  • Ocean

  • Sky

  • Sasuke Model (Main Character)

  • Sharingan Eye Model

  • Pole

For Ocean and Sky, threejs includes example of Water Object and Sky Object they are pretty good.

Water Object and Sky Object can be found on Three.js github.

The first thing we’ll do is grab a copy of the Water.js and Sky.js sample in the Three.js repository. We’ll place this file within our project at objects/ folder

If we open the water.js file, about halfway down, we’ll start to see something that looks like this:

Vertexshader and Fragmentshader , these are shaders for ocean material written in OpenGraph Shader Language (GLSL).

Let's create shader directory in our objects/ folder

select the contents of vertexShader and fragmentShader, and move them into waterFragmentShader.glsl and waterVertexShader.glsl files, respectively**.**

To import the .glsl file into Water.js, we'll require the installation of a plugin named vite-plugin-glsl. Install this plugin using npm

npm install --save vite-plugin-glsl

Now create a vite.config.mjs file and add vite-plugin-glsl plugin.

  • vite.config.mjs

       import {defineConfig} from 'vite'
       import glsl from 'vite-plugin-glsl'
    
       export default defineConfig({
          plugins:[
              glsl({
    
              })
          ],
          base: '/Portfolio/'
       })
    

To actually be able to display anything with three.js, we need three things: scene, camera and renderer, so that we can render the scene with camera.

In main.js add scene, camera, renderer, water object, and sky object.

  • main.js

      import * as THREE from 'three';
    
      import  {Water} from './objects/Water'
      import {Sky} from './objects/Sky'
      import waternormals from './static/normals/waternormals.jpeg?url'
    
      import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
    
      //Scene and Camera 
      const scene = new THREE.Scene();
      const camera = new THREE.PerspectiveCamera( 75, window.innerWidth / window.innerHeight, 0.1, 2000 );
    
      let renderer,controls
    
      const waterGeometry = new THREE.PlaneGeometry(10000, 10000);
      //Water Object
      const water = new Water(
          waterGeometry,
          {
              textureWidth: 512,
              textureHeight: 512,
              waterNormals: new THREE.TextureLoader().load(waternormals, function (texture) {
                  texture.wrapS = texture.wrapT =THREE.MirroredRepeatWrapping;
              }),
              sunDirection: new THREE.Vector3(),
              sunColor:0xff0000,  //0xff0000,   //0xffffff
              waterColor:0x80080, //0x80080, //0x001e0f
              distortionScale: 3.7,
              fog: scene.fog !== undefined
          }
      );
    
      function init(){
    
          renderer = new THREE.WebGLRenderer({ antialias: true });
          renderer.setPixelRatio( window.devicePixelRatio );
          renderer.setSize( window.innerWidth, window.innerHeight );
          renderer.toneMapping = THREE.ReinhardToneMapping;
          document.body.appendChild( renderer.domElement );
    
          //Orbit Controls
          controls = new OrbitControls(camera, renderer.domElement);
    
          //Add Water object
          water.rotation.x = -Math.PI / 2;
          water.rotation.z = 0;
          scene.add(water)
    
          const sky = new Sky();
          sky.scale.setScalar(10000); // Specify the dimensions of the skybox
          scene.add(sky); // Add the sky to our scene
    
          const skyUniforms = sky.material.uniforms;
          skyUniforms['turbidity'].value = 1;  //1 //0.001 5   10
          skyUniforms['rayleigh'].value = 0.01; //0.01 //0.01 6   2
          skyUniforms['mieCoefficient'].value = 0.003; //0.003 0.005
          skyUniforms['mieDirectionalG'].value = 0.988;   //0.988  0.8
    
          const parameters = {
              elevation: 20 , //20,  //30
              azimuth:190    //190   //175  115
          };
          const pmremGenerator = new THREE.PMREMGenerator(renderer);
    
          const phi = THREE.MathUtils.degToRad(90 - parameters.elevation);
          const theta = THREE.MathUtils.degToRad(parameters.azimuth);
          const sun=new THREE.Vector3()
          sun.setFromSphericalCoords(1, phi, theta);
    
          sky.material.uniforms['sunPosition'].value.copy(sun);
          water.material.uniforms['sunDirection'].value.copy(sun).normalize();
          scene.environment = pmremGenerator.fromScene(sky).texture;
          water.material.uniforms['time'].value=10.0
    
           //Camera initial position
          camera.position.z = 30;
          camera.position.y=30
          camera.rotation.x=30
    
          controls.update()
    
      }
    
      window.addEventListener('resize',onWindowResize,false)
    
      //Handle Window resize
      function onWindowResize(){
          renderer.setSize( window.innerWidth, window.innerHeight );
          camera.aspect = window.innerWidth / window.innerHeight
          water.material.uniforms['time'].value+=1/60.0
          camera.updateProjectionMatrix()
      }
    
      //Render animation
      function animate() {
          requestAnimationFrame( animate );
          renderer.render( scene, camera );
          water.material.uniforms['time'].value+=1/60.0
      }
    
      init()
      animate();
    

Add scripts in package.json file to run project

  • package.json

      {
        "scripts": {
          "dev": "vite",
          "build": "vite build",
          "preview": "vite preview"
        },
        "dependencies": {
          "three": "^0.159.0"
        },
        "devDependencies": {
          "vite": "^5.0.8",
          "vite-plugin-glsl": "^1.2.1"
        }
      }
    

Run the following code in terminal to run the project:

npm run dev

If everything worked as expected, you'll be greeted with a stunning night sky and ocean displayed in your browser.

Next for the main character and sharingan eye we will use 3D models.

Here are some websites to download free 3D models:

Sharingan Eye

Animating character

Mixamo is an online platform that provides pre-made 3D character animations and rigging tools. Mixamo simplifies the process of animating 3D characters by allowing users to customize and apply.

The perfect tool for our project.

Your character most definitely should be in a T pose, because Mixamo is going to generate the skeleton for us, so it is expecting this.

The first thing you want to do is upload your character. Mixamo accepts FBX and OBT formats. Mixamo will automatically bring up the Auto-Rigger feature once your upload is complete.

Follow the instructions to place the markers on the key areas of your model. Once the auto-rigging is complete, you’ll see a panel with your character animating!

Let’s find a simple animation. The one we used in this guide is called “Neutral idle”. Click Download Character. Your format should be FBX and skin should be set to With Skin. Leave the rest as default. Download this file.

To load this model in our project GLTF loader dependency will be used.

For that, we need to convert the model format from FBX to GLB/GLTF.

For this purpose, blender is the tool we need. Blender is an open source 3D modeling tool software and much more.

Once Blender is open, start by removing unnecessary elements such as the camera, cube, and light.

Import the downloaded file(FBX fromat) from mixamo inside blender.

For conversion of file, export it in glb format

Create models directory in our public/ folder and move model files into the model folder.

We’re going to use our GLTFLoader dependency to load both models into our scene. And also create a Simple Cylinder Pole using Mesh.

  • main.js

      import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
      import sasukemodel from '/models/sasuke.glb?url'
      import sharinganeye from '/models/sharingan.glb?url'
    
      let sharingan
    
      //inside init function
      init (){
    
      //.....
      //Add Cylinder Pole to scene
      const geometry = new THREE.CylinderGeometry( 1.8, 1.8, 20, 25 ); 
      const material = new THREE.MeshStandardMaterial({
          color: 0x000000,
          roughness: 0.1, 
          metalness: 1, 
      });
      const cylinder = new THREE.Mesh( geometry, material ); 
      scene.add( cylinder );
    
      //Main character 
      let model
          const loader=new GLTFLoader()
          //Add Sasuke to scene
          loader.load(sasukemodel,function(glft){
          model=glft.scene
          model.scale.set(12, 12, 12);  
          model.position.y+=9.8
          scene.add(model)
      });
    
      //sharingan eye
      loader.load(sharinganeye,function(glft){
          sharingan=glft.scene
          sharingan.scale.set(300,300,300)
          sharingan.position.x+=0
          sharingan.position.z-=30
          sharingan.position.y+=30
          scene.add(sharingan)
      })
    
      //....
      }
    

All Elements are placed perfectly in place. Now we need to play the animation that we added using Maximo.

We’re going to create a new AnimationMixer, an AnimationMixer is a player for animations on a particular object in the scene.

An animation clip of the object can be accessed by its name.

Once the model gets loaded inside the GLTf loader method, console.log() to find the animation clip name.

So in our case, the name is "idle".

  • main.js

      //Clock for Model Animation
      let clock = new THREE.Clock()
      let  mixer,idle,
      //inside init function
    
      function init(){
      //......
    
          let model
          const loader=new GLTFLoader()
          //Add Sasuke to scene
          loader.load(sasukemodel,function(glft){
          model=glft.scene
          //console.log(model)
          model.scale.set(12, 12, 12);    //8
          model.position.y+=9.8
          let animation=glft.animations
          scene.add(model)
    
          mixer = new THREE.AnimationMixer(model)
          let idleAnim = THREE.AnimationClip.findByName(animation, 'idle');
          idle = mixer.clipAction(idleAnim);
          idle.play();
    
      });
      }
    
      //inside animate function
      function animate(){
      //.......
         if (mixer) {
              mixer.update(clock.getDelta());
          }
    
      //.......
    
      }
    

Let's make the Sharingan eye move along with how the orbit controls move. Using an event listener, we'll adjust how the Sharingan rotates by changing its x and y positions based on what the user does with the orbit controls.

We also need to customize the orbit controls by setting min/max PolarAngle(up/down) and min/max AzimuthAngle(right/left). This involves setting limits for how high or low and how far left or right the controls can move.

function init(){
//......

    loader.load(sharinganeye,function(glft){
    console.log(glft)
    sharingan=glft.scene
    sharingan.scale.set(300,300,300)
    sharingan.position.x+=0
    sharingan.position.z-=30
    sharingan.position.y+=30
    scene.add(sharingan)

    controls.addEventListener('change',()=>{
        let azimuthangle=controls.getAzimuthalAngle()
        //Sharigan Eye y rotation
        sharingan.rotation.y=azimuthangle/2    
        //Sharingan Eye x rotation
        if(azimuthangle<0){
             sharingan.rotation.x=-azimuthangle/2
        }else{
            sharingan.rotation.x=azimuthangle/2
        }

    })
})
     //Camera initial position
    camera.position.z = 30;
    camera.position.y=30
    camera.rotation.x=30
    controls.target.set(0,9.9,0)
    controls.distance=30
    controls.minDistance=30
    controls.maxDistance=30
    controls.minPolarAngle=Math.PI/2
    controls.maxPolarAngle=Math.PI/2
    controls.minAzimuthAngle=-0.9
    controls.maxAzimuthAngle=0.9
    controls.enablePan=false

    controls.update()
}

That's it!

For any questions, you can reach me on Linkedin