More Articles
RSS

Mar 2023

Spinning N64 logo

Here I explore using WebGL to display a rotating Nintendo 64 logo using three.js

<html>
  <head>
    <meta charset="utf-8">
    <style>
      * { box-sizing: border-box; }
      html,body { margin: 0; background: #000; overflow: hidden; height: 100%; }
      #root { width: 100%; height: 100%; }
      canvas { width: 100% !important; height: 100% !important; object-fit: cover; image-rendering: pixelated; }
    </style>
    <script src="three.0.87.1.min.js"></script>
    <script src="three.0.87.1.gltfloader.min.js"></script>
    <script>
      const createNode = (options) => {
        const node = document.createElement(options.tag);
        if(options.className) node.setAttribute('class',options.className);
        if(options.innerHTML) node.innerHTML = options.innerHTML;
        if(options.attributes) Object.keys(options.attributes).forEach(key => node.setAttribute(key,options.attributes[key]) );
        if(options.style) Object.keys(options.style).forEach(key => node.style[key] = options.style[key]);
        if(options.root) options.root.appendChild(node);
        return node;
      }
      class App {
        constructor(){
          document.addEventListener('DOMContentLoaded',this.handleReady.bind(this));
          window.addEventListener('resize',this.handleResize.bind(this));
          this.resize_timeout = null;
        }
        handleResize(event){
          if(this.resize_timeout) clearTimeout(this.resize_timeout);
          this.resize_timeout = setTimeout(this.handleReady.bind(this),50);
        }
        handleReady(event){
          if(this.interval) cancelAnimationFrame(this.interval);
          this.els = {}
          this.els.root = document.getElementById('root');
          this.els.root.innerHTML = ``;
          const ratio = window.innerHeight / window.innerWidth;
          this.width = 320; // the n64 resolution width
          this.height = Math.floor(this.width * ratio);
          this.loader = new THREE.GLTFLoader();
          this.loader.load('n64.gltf', this.handleLoadModel.bind(this));
        }
        getCenterPosition(scene){
          const geometry = new THREE.BoxGeometry(1,1,1);
          const material = new THREE.MeshBasicMaterial({color: '#f00'});
          const placeholder = new THREE.Mesh(geometry, material);
          const box_from_scene = new THREE.Box3().setFromObject(scene);
          return box_from_scene.getCenter(new THREE.Vector3());
        }
        handleLoadModel(object){
          this.n64 = object.scene;
          this.center = this.getCenterPosition(this.n64);
          this.scene = new THREE.Scene();
          this.clock = new THREE.Clock();
          this.matrix = new THREE.Matrix4();
          this.camera = new THREE.PerspectiveCamera( 20, this.width / this.height, 1, 100 );
          this.camera.position.set(0,3,7);
          this.renderer = new THREE.WebGLRenderer( { antialias: true, powerPreference: 'high-performance' } );
          this.renderer.setSize( this.width, this.height );
          this.renderer.setClearColor( 0x000000 );
          // this.renderer.toneMapping = THREE.LinearToneMapping;
          // this.renderer.toneMappingExposure = Math.pow(0.94,5.0);
          // this.renderer.shadowMap.enabled = true;
          // this.renderer.shadowMap.type = THREE.PCFShadowMap;
          const point_light = new THREE.PointLight(0xffffcc,2,10);
          point_light.position.set(0,1,0);
          const ambient_light = new THREE.AmbientLight(0x20202A,9,1);
          ambient_light.position.set(0,0,0);
          this.scene.add(this.n64);
          this.scene.add(point_light);
          this.scene.add(ambient_light);
          this.els.root.appendChild(this.renderer.domElement);
          this.interval = window.requestAnimationFrame(this.render.bind(this));
        }
        async render(){
          this.matrix.makeRotationY(this.clock.getDelta() * -2 * Math.PI / 5); // 5 seconds per rotation
          this.camera.position.applyMatrix4(this.matrix);
          this.camera.lookAt(this.center);
          this.renderer.render(this.scene,this.camera)
          this.interval = window.requestAnimationFrame(this.render.bind(this));
        }
      }
      const app = new App();
    </script>
  </head>
  <body>
    <div id="root"></div>
  </body>
</html>

The first thing I did was look on Sketchfab for a model to use. There's quite a few n64 logos, and this one was the most accurate except for the hue of the colors which I tweaked a bit.

Once the 3d model is ready, it gets loaded using the three.js GLTFLoader module. This is another large function which I've minified. After loading, it is rendered to the canvas using requestAnimationFrame. At each interval, the camera rotates around a calculated center point of the object.

Project Assets

Asset File Description
n64.bin Download n64 3D model
n64.gltf Download GLTF metadata for n64 3D model
n64-blue.png Download Blue texture for 3D model
n64-green.png Download Green texture for 3D model
n64-red.png Download Red texture for 3D model
n64-yellow.png Download Yellow texture for 3D model
three.0.87.1.min.js Download Minified three.js v0.87.1
three.0.87.1.gltfloader.min.js Download Minified GLTF Loader for three.js v0.87.1

3d model credit: This work is based on N64DD - N64 Logo by julianbebout75 licensed under CC-BY-SA-4.0