Roll Dice in High-Fidelity 3D
Gameification PWA Tutorial

Roll Dice in High-Fidelity 3D

Scott C. Krause | Monday, Nov 2, 2020

Rolling dice virtually is not the same experience as rolling dice physically. It just doesn't feel real. The dice do not appear to tumble or make sounds. This disconnected experience makes one suspect that maybe these on-line dice are not really random.

It would be great to recreate the experience of actually rolling dice in the browser. It would be reminiscent of marathon Monopoly with your friends. It would be like Vegas baby!

In this article I introduce a gamified micro-interaction in which dice are rolled virtually by shaking the phone. The sensation is complete with haptic and audio feedback making it seem like the bones are rattling in your sweaty palm. The mathematical integrity of the roll result is achieved through the high entropy of the Web Crypto API.

"Why?", you might and should ask. I prefer to answer the "Why" in terms of business value.

  1. The PWA proposition: It is not uncommon for companies to create a web app, an iPhone app, and an Android app. When management is convinced that a web app can be just as engaging as native they will decide to focus solely on the web app and get to market much quicker. Making a web app installable results in increased traffic, visitor retention, sales per customer, and conversions. I believe that this project proves beyond doubt the immersive potential of browser APIs.

  2. The argument for Gamification: Framing a customer touch-point as a playful game has the potential to differentiate, engage, and persuade like nothing else.

  3. Professional Branding: I am doing this because it allows me to integrate emerging browser APIs in such a way that I have a slick deliverable to demonstrate when I am done.

You will learn these skills while building this project:

  • Accelerometer API
  • Audio API
  • Blender 3D Modeling
  • Git
  • glTF
  • HTML canvas
  • JavaScript
  • NPM
  • Three.js
  • Vibration API
  • UV Mapping
  • Web Crypto API
  • WebGL
  • Webpack

This is totally doable for you because you are a smart developer, designer, and magical unicorn.

Project Overview

We will write custom JavaScript that will animate the dice by changing 3D properties exposed by the Three.js framework. This script will also implement the following browser based APIs: Accelerometer, Audio, Vibration, Web Crypto, and WebGL (via Three.js). Hopefully the end result will behave more like a native app than a web page.

3D Model Preview

Creating the 3D model

I will use the open source Blender v2.90 to create the 3D dice (or die singular). This is by no means a Blender tutorial, there are plenty of those on youtube however we will cover the basic steps to produce this particular 3D model.

If you don't want to learn how to create the model you can skip this section then download the pre-made assets from the git repo.

Download Blender and fire it up. Conveniently Blender starts up with a sample cube in the project, that's pretty close to a die so let's go with that.

  1. Select the cube and change its size to 1.6 CM (all dimensions). Thats going to make it small so use your mouse wheel to zoom in.

  2. Smooth the edges with a "Bevel Modifier". In the lower-right panel click the wrench icon to view modifiers. Select the cube and click the Add Modifier drop-down > Bevel. Amount 0.2 and Segments 8. The cube should now look smooooooth. Save your work!

  3. UV Mapping is the process by which one wraps a 2D image around a 3D model. For us that means the little black dots (called pips) that determine the dice's value are actually in a flat JPG file derived from a layered Adobe Illustrator AI file. UV Mapping can be a difficult endeavor with complex geometries but a square die is about as simple as it gets. How convenient!

  4. To add the image map; Click into the Shading workspace (top horz menu). Click Add > Texture > Image Texture from the dropdown menu. Click the folder icon then navigate to the dice_uv_map.jpg file. Drag the Color (little dot on the right) onto the Base Color node attaching the new image to the objects texture. The cube should be an orange color and have numbers.

  5. glTF is a relatively new open format for transporting 3D assets. We need to export from Blender in this format so that we can load it into Three.js to display it in the browser. Click the Layout workspace (up top). Select the die then choose File > Export > glTF 2.0 from the application menu. You will be presented with a dialog for saving your model. On the right is a configuration section titled Geometry. Open that and tick Apply Modifiers. Now save your model as dice.glb into your project's root folder.

    Self-check: You can preview your model in a browser by uploading the GLB file to the glTF Viewer website.

Blender 3D Modeling

Create 3D Dice with Strong Random Entropy

Install Three.js

NPM Install Command
npm install --save three
WebPack Config
const path = require('path')

module.exports = {
  entry: './a55_3d_template.js',
  output: {
    path: path.resolve(__dirname, '../static/js'),
    filename: 'a55_3d.js',
  },
  devServer: {
    publicPath: '/public/',
    compress: true,
    port: 9000,
  },
}
a55_3d_template.js
import * as THREE from 'three'

import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader';

window.o3 = THREE;
window.o3loadr = GLTFLoader;
window.o3orbit = OrbitControls;

Rendering 3D object in the browser - three.js

Three.js is a JavaScript framework for implementing WebGL in the browser. It is a popular way to make high-touch experiences on the web.

You will need a recent version of Node.js and NPM installed. This is kind of a must for any modern knowledge worker. Install Three.js via the NPM command then make the required changes to the webpack and template config files.

The objective is to have Webpack rollup only the JavaScript we need into one JS file that we can load like this:

 <script type="module" src ="/js/a55_3d.js"></script>


var o3 = null;
var o3Config = (function(){
    var camera, scene, renderer, controls;
    return {
    "autoSpin": function(){
        controls.update();
        requestAnimationFrame( o3Config.autoSpin );
        renderer.render( scene, camera );
    },
    "spin": function(){
        renderer.render( scene, camera );
    },
    "init": function( e3ds ){
            var sPath = e3ds.dataset.thr3Load, sW = e3ds.dataset.thr3W, sH = e3ds.dataset.thr3H;
                scene = new o3.Scene();/* Scene */

                var pointLightOrange = new o3.PointLight( 0xC9F4ED, 2, 800 );
                pointLightOrange.position.set( -4000, -400, -66 ).normalize();
                scene.add( pointLightOrange );

                camera = new o3.PerspectiveCamera( 4, 1, 1, 1000 );/* Camera */
                camera.position.x = 56; camera.position.y = 56; camera.position.z = 56;
                scene.add( camera );
                camera.add( pointLightOrange ); // Add light to camera

                renderer = new o3.WebGLRenderer( { antialias: true, alpha: true } );/* Renderer */
                renderer.setPixelRatio( window.devicePixelRatio );
                renderer.setSize( sW, sH );
                e3ds.appendChild( renderer.domElement );

                var loader = new o3loadr();/* Loader */
                loader.load( sPath , function ( gltf ) {
                scene.add( gltf.scene );
                o3Config.spin();
                } );

                controls = new o3orbit( camera, renderer.domElement );/* Controls */
                controls.autoRotate = true; controls.autoRotateSpeed = 9.4;
                controls.addEventListener( 'change', o3Config.spin );
                controls.update();
                setTimeout( o3Config.autoSpin, 256);

                window.addEventListener( 'resize', onWindowResize, false );
                function onWindowResize() {
                camera.aspect = 1;
                camera.updateProjectionMatrix();
                renderer.setSize( sW, sH );
                o3Config.spin();
                }
            }
    }
})();
var e3 = document.querySelector("[data-thr3-load]");
if( e3 ) { // Delay 3d call if still null
    setTimeout(function(){
    o3Config.init( e3 );
    }, (o3===null) ? 3800 : 800 )
}
            
Accelerometer API Audio API Blender Git glTF HTML canvas JavaScript NPM Three.js UV Mapping Web Crypto API WebGL Webpack