Introduction to THREE.JS

Useful links:

Example 1: Starter project

We will create a basic example showing how a THREE.JS app is initiated, fundamental structure and some useful UI add-one.

The result should resemble this file.

  1. Start by launching your Terminal (PowerShell on Windows.) Once it's running type in the command:

    node --version

    If you see a version number and it's higher than 14.0.0 - you'll be fine. If you get a response that amounts to "I don't know what you want from me!" you'll need to install NodeJS from their website: nodejs.org. Grab the version recommended for the most users and proceed with the installation. Once it's finished it might not be a bad idea to restart your machine - just to make sure all the paths are updated. After that try launching Terminal (PowerShell on Windows) and running the command again.

    node --version

    You should see the version number now. Also run this command:

    npm --version

    You should see a number higher than 6.0.0 If you don't - consult this guide for details "Downloading and installing Node.js and npm"

    If you think of a browser as a car NodeJS is the engine from that car, that has been extracted and packaged and can be installed in your garage (on your computer) to provide power to your tools. NPM is Node Package Manager, and it will help us manage and install new components for that engine. You ternimal (or PowerShell on Windows) should look something like this now:

    Terminal view with Node and NPM version commands
  2. We'll need to create a simple Node-based Web application and run it in a local server to make sure that the requests to OpenAI API come from an HTTP host and not just a file in your filesystem. There are many ways of doing this - one of the simpler ones is to create an app with Vite, a scaffolding engine.

    Before we create our app let's figure out where will we be building it. To find your current working directory run

    pwd

    command in your Terminal (or PowerShell on Windows.) This will print out the path. From there you can navigate to the directory where you'd like to build your app. For example, I prefer to keep my coding projects in the "Repositories" directory inside my user folder so to move there from my current personal user folder I will run a cd (change directory) command like this:

    cd Repositories

    Once there I will run a simple command to create a Vite-based web-app:

    npm create vite@latest

    and follow the prompts. I will name my project "openAIapiProject" and we'll be using vanilla JavaScript for this project. So my terminal will look like this:

    Terminal view after the creation of a Vite template
  3. Once you've created your web-app Vite will prompt you with 3 next steps: change to the newly created app directory (its name is the same as the name you gave your Vite project), install the packages necessary to run your app and then run it in a developer mode. After completing the first 2 steps your Terminal (or PowerShell) would look similar to this:

    Terminal view showing Vite instructions 1 and 2

    Now you should be ready to run your app. Run the third command (npm run dev) and your NodeJS server should start. If you press "h" for help you should be able to see this:

    Terminal view showing running Vite server

    You can press 'o' to open the default page in your browser. Vite server will track any chages you make to your web-app and will restart this browser page the moment you save your changes to your file, which is neat. So check back here as you progress with your work.

  4. Open the directory that Vite has created for your web-app in your code editor and you'll see that there are a lot of things there that we won't need. You can start deleting them

    File structure view showing what to delete in Vite: public folder, counter.js and javascript.svg

    You can also remove evrything from "style.css" and leave only one line in main.js, the first one:

    import './style.css'

    In your CSS file (style.css) select and delete all the content and add this line

    html, body { margin: 0; }

    They will simply make sure that the content of your page is flush with your browser window.

    Save all your changes and check back with your browser - you should see a blank page instead of the default page you saw before. You can check on the resulting file structure here. The last thing we need to do in your Terminal is to instal Three.JS library for NodeJS. In your Terminal press 'q' to quit your Vite server and run

    npm install three

    Once the installation has finished you can run

    npm run dev

    again to restart the Vite server.

  5. Next we'll need to import the Three.JS module into our main.js file, we can do that right after the CSS import

    import './style.css'
    import * as THREE from 'three'    
                    

    With that we'll have access to all the components of the standard ThreeJS library. There are some additinal component that we will need to import, but they are already installed in out app folder (in the node_modules section).

    With this we can implement the most basic scene in ThreeJS. We'll start by steeting up a data object for our window sizes - not strictly necessary, but it will come handy later. Then we'll create a scene and a camera and position the camera a bit up and a bit closer to us. Lastly we'll need to create a renderer that will render the graphics for us and append it to the HTML document.

    //scene size
    const sizes = {
        width: window.innerWidth,
        height: window.innerHeight
    }
    
    //scene
    const scene = new THREE.Scene()
    const camera = new THREE.PerspectiveCamera( 75, sizes.width / sizes.height, 0.1, 1000 )
    camera.position.z = 5
    camera.position.y = 2
    
    //renderer
    const renderer = new THREE.WebGLRenderer()
    renderer.setSize( sizes.width, sizes.height )
    document.body.appendChild( renderer.domElement )
                    

    After this we can add some contents to our scene and render it. We'll add a cube - this will require a box geometry and we'll start wil a Basic material, although we'll switch to Standard material very soon. The geometry and the material will allow us to create a mesh which we'll add to our scene. The last step here will be to render our scene as seen through our camera.

    //scene contents
    const cubeGeometry = new THREE.BoxGeometry( 1, 1, 1 )
    const cubeMaterial = new THREE.MeshBasicMaterial( { color: 0x48727f } )
    const cube = new THREE.Mesh( cubeGeometry, cubeMaterial )
    cube.position.y = 1
    
    scene.add( cube )
    
    renderer.render( scene, camera )                    
                    
  6. Let's add animation to our scene. We'll use a standard JavaScript animation technique which involves creating a function that will instruduce changes to our scene and then call itself again, thus repeating those changes. Just make sure to re-render the scene each time, so those increamental changes are visible. You can repace the renderer.render call with the following:

    //animation
    function draw() {
        requestAnimationFrame( draw )
    
        cube.rotation.x += 0.01
        cube.rotation.y += 0.01
    
        renderer.render( scene, camera )
    }
    draw()                    
                    

    Let's also add a bit of housekeeping. Right now if you resize your window the scene will remain whichever size it had when the window loaded. We can make it change the size to follow the size of the browser window. To do that we'll use the sizes data object we created earlier, and call updates on it every time the window is resized. This code can go at the end of your main.js file:

    window.addEventListener('resize', ()=>{
        console.debug("resize")
        // Update sizes
        sizes.width = window.innerWidth
        sizes.height = window.innerHeight
    
        // Update camera aspect ratio
        camera.aspect = sizes.width / sizes.height
        camera.updateProjectionMatrix()
    
        // Update the renderer
        renderer.setSize(sizes.width, sizes.height)
        renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2))
    })
                    
  7. You'll notice that the cube looks 2-dimensional and has no shadows. This is because we are using Basic Material and because there are no lights to give it dimensionality. Let's change that. We'll start by changing the material to MeshStandardMaterial:

    const cubeMaterial = new THREE.MeshStandardMaterial( { color: 0x48727f } )

    Then we can add some lights to the scene. We can put the code for lights anywhere, but I prefer to keep them near the general scene settings, near the camera and the renderer. You can see how the code is organized in this file. We'll add two lights - an ambient light to simulate daylight, and a directional light to serve as an intentional source of light. The directional light will be places above and to the right.

    const ambientLight = new THREE.AmbientLight( 0x404040 ) // soft white light
    scene.add( ambientLight )
    
    const directionalLight = new THREE.DirectionalLight( 0xFFFFFF )
    directionalLight.position.set( 4, 8, 0 )
    scene.add( directionalLight )                    
                    

Example 2: UI options

The previous code should give you a basic starter scene from which you can build further but it lacks certain UI options that will make a 3D environment comfortable. We'll add the ability to move around the scene and control parameters of the objects and the environment to make interactions easier. We'll also add helpers to make it easier to visualise the positions of the scene elements.

  1. We'll start by adding Orbit Controlls to allow us to move around the scene. To do that we'll need to import an add-on from the ThreeJS library, set up the controls and make sure the changes are displayed during the animation rendering.

    Start by adding the following line after the import declaration for ThreeJS itself.

    import { OrbitControls } from 'three/addons/controls/OrbitControls.js'

    Then we can instantiate the controlls. We can put this code anywhere, but I prefer to keep them near the general scene settings, near the camera and the renderer.

    const controls = new OrbitControls( camera, renderer.domElement )

    Then we will update the controlls to make sure the changes are reflected in the animation render. You can put this line after the cube rotation updates in the draw() function.

    controls.update()
  2. With the cube alone it is a little difficult to understand how the controls work, especially since the cube is positioned slightly above the scene center. We can help that by adding a 'floor' and an axisHelper - a set of three lines indication the orientation of the scene. For the floor we'll use plane and add it right after the cube. We'll need to rotate it to make it face up.

    const plane = new THREE.Mesh( new THREE.PlaneGeometry( 7, 5 ), new THREE.MeshStandardMaterial( { color: 0xf5ff9d, side: THREE.DoubleSide } ) )
    plane.rotation.set( -Math.PI * 0.5, 0, 0 )   
    
    scene.add( plane )                
                    

    The helper we can add near the Orbit Controls instantiation, but anywhere after the scene instantiation is fine, because we need to add it as an element to the scene:

    const axesHelper = new THREE.AxesHelper( 5 )
    scene.add( axesHelper )
                    

    We can also add a light helper to help us visualise how the light is positioned and directed. You can add the following code with the lights or with the helpers - but try to find a logic to the way you organize your code.

    const directionalLightHelper = new THREE.DirectionalLightHelper( directionalLight, 5 )
    scene.add( directionalLightHelper )
                    

    This should make it easy to understand how the scene is set-up and we can remove the elements once we're done with the build.

  3. During the build and experimentation it can be helpful to give yourself a quick way to tweak parameters, and we can do that with a simple pre-built GUI package. We'll need to install it with npm first, then import it to the file and then implement it. Start by switching to your terminal and quitting the Vite server by pressing 'q'. Then run 'npm install lil-gui' and restart your server when the installation is done with 'npm run dev'.

    lil-gui installation process

    To import it into your scene add the following after your Orbit Controls import:

    import GUI from 'lil-gui'

    We can then add instantiation for the gui object. I tend to keep with with other controls and helpers.

    const gui = new GUI()

    To add specific controls we need to add the following lines after the scene content - because the objects in the scene are references in the controls. We're adding the slider to change the horizontal position of the cube and two color pickers. There are plenty of other options in the lil-gui documentation.

    gui.add(cube.position, 'x').min(-5).max(5).step(0.1).name('Cube Position X')
    gui.addColor(cube.material, 'color').name('Cube Color')
    gui.addColor(plane.material, 'color').name('Plane Color')