Now with the VR popularity at its highest, a 3D virtual environment website that could be explored with a VR headset sounded awesome to me. Trying to bring this idea to practice led me to the implementation of my own website where I experiment with different things. Here are the links to the latest full source code and site in production so you can more easily follow the text. I have also made a Gatsby.js starter project with boilerplate code. Feel free to experiment with it.
The site itself is pretty simple. It has a profile image, welcoming text and in the background, the whole viewport shows a virtual environment with animated 3D models. Those models, when clicked on, lead to different social media sites like LinkedIn, GitHub, Facebook, etc.
I will go through different steps I had to undertake to make this work and hopefully in the end you will have the required knowledge to build something similar.
A few words about Babylon.js and Gatsby.js
Creating virtual scenes in raw WebGL can be pretty cumbersome as you are required to write low-level shader code and take care of all the rendering optimizations. That’s where different rendering frameworks come to save the day. To create our scene, we will use Babylon.js game engine. I find this engine to be easy to learn, powerful, fast and full of features even compared to some big names like Unity. It also has tremendous support from creators and community. It is completely open-source and packed into a JavaScript framework.
Another piece of technology I used is Gatsby.js. It is a modern static site generator that helps you create fast and optimized sites. It takes care of a bunch of things while you focus on UI code written in React.
Now we have the technology context, so let’s make our first step towards a fully functioning 3D website.
Creating 3D models
Our website has animated 3D models floating inside the scene. So how do we create them?
Babylon.js has broad 3D model file type support. It has its own .babylon file format but it also includes loaders for many popular formats including .glTF, .obj, .stl etc. Depending on what models you would like to include in your scene, there are many sites where you can download models and textures in different formats made by other artists. This can make your life much easier because model creation can be quite hard if you never tried it before.
In my case, I needed 3D models of social network logos (LinkedIn, Facebook, .etc) and I decided to try out some modeling and create them on my own. For this task I used Blender. It is a very popular open-source 3D modeling tool. The web is full of tutorials that can get you started real quick if you haven’t tried it before. Blender and Babylon.js are a great combination because the Babylon.js team created a Blender extension which enables you to export models directly in native .babylon format.
Blender allows you to create a 3D model from an image. This can be done in a few simple steps explained here: https://www.youtube.com/watch?v=g6cmClwLhz8. I created my models by following the same tutorial.
We have our models. Onward to scene creation.
Scene
I won’t go into implementation-specific details here because this blog would turn out to be a full-blown Babylon.js tutorial but I will mention key stuff and give you links to where you can find all the relevant information.
Following principles of good software design and reusability we can create a React component BabylonScene. This component will render a canvas element where a Babylon scene will be initialized. The component receives a callback function onSceneMount(sceneArgs) as a prop. This callback will execute once the engine is initialized and the scene is created. Inside it, you can create all the elements a virtual environment scene needs. Those include a camera, lights, ground mesh, shadows, etc. Then add the models that you created, initialize animations and manipulate the scene however you want.
Pretty much the same thing that code in my example does can be found in this guide: https://doc.babylonjs.com/resources/babylonjs_and_reactjs. The only difference is I used old-school class components and the guide uses hooks.
The next thing I want to do is when I click on my 3D model inside a scene it works like a link and it opens a new URL in a new tab. The awesome news is, Babylon.js already has builtin support for this. We can use a feature called actions. Find more about it here: https://doc.babylonjs.com/how_to/how_to_use_actions. Actions allow you to react to specific triggers inside a scene. In our example, on every 3D logo model we will register an ExecuteCodeAction with an OnPickTrigger. This action will execute a callback function when we click on a model. Next, we need to include some <a> tags inside a component, for example like this:
<><BabylonScene onSceneMount={this.onSceneMount} adaptToDeviceRatio={true} />
<a ref={this.linkRefs.cv} target="_blank" rel="noopener noreferrer" href={...}>Resume</a>
<a ref={this.linkRefs.linkedin} target="_blank" rel="noopener noreferrer" href={...}>Linkedin</a>
<a ref={this.linkRefs.gmail} href={...}>Gmail</a>
<a ref={this.linkRefs.facebook} target="_blank" rel="noopener noreferrer" href={...}>Facebook</a>
<a ref={this.linkRefs.github} target="_blank" rel="noopener noreferrer" href={...}>Github</a></>
In the action callback we will programmatically click on the appropriate anchor tag. Voila! We have our virtual 3D links.
For the best user experience it is a better idea not to attach actions to 3D icons directly but to a transparent child box mesh. Your icons or any other models can have hollow parts which make them difficult to click on, especially when they are moving. For this reason for each custom model you create a child box mesh and make it receive actions. Child mesh will always follow parents coordinates whenever parent moves.
The original idea was to create an immersive 3D experience. Trying to achieve this I covered the entire viewport with the scene using trivial CSS. Over it, I positioned an image and a short greeting message. This is, of course, one of the infinite possible ways to do it. One of the possible variations of the top of my head could be to not have any HTML content and put the greeting message and an image inside a scene. Another one, to make your website visitor navigate through the scene just like in the game. You could also enable WebVR for which Babylon.js has built in support. This way, it would truly be an immersive experience.
Optimizations
There is one flaw that comes with using WebGL. Depending on how complex your scene is, it can quickly become a slow loading 10x megabytes bundle giant. If we were building a web game this could be acceptable. We show the user a loading screen just as in desktop games and everything’s fine. But we are building a personal website/portfolio. Our site visitors expect a snappy fast loading site and if they are greeted with several seconds long loading screen they will leave. So, what can we do?
Performance is one of the key features of Gatsby.js and it will take care of most of the standard optimizations with little work needed from your side. This includes things like build minification, SEO, caching, deterred JavaScript loading, etc. Everything you need to know can be found here: https://www.gatsbyjs.org/docs/performance/. (Quick advice: Pay great attention to SEO. With a little bit of research your page can quickly become the top result in search engines.)
Let’s focus on the virtual scene part. I will review a few things that really improved performance for me.
Es6 packages
One great feature of Babylon.js is it supports es6 packages. This improves production build tree shaking and can greatly reduce its size. Before, you would install and import the whole Babylon.js engine like this:
npm install babylonjs
and then, inside a JavaScript file you would use it like
import * as BABYLON from 'babylonjs'
But what if we don’t need some engine features? For example, advanced preprocessing effects or particles support. We don’t want to unnecessarily enlarge our bundle with code we don’t need. Es6 packages support allows us to do this:
npm install @babylonjs/core
and then, inside a JavaScript file:
import { Engine } from "@babylonjs/core/Engines/engine"
In this case, I only needed Engine class so I imported that class only and nothing else. This is an awesome feature and you should definitely use it.
Low-poly models
In the “Creating 3D models” chapter we went through some options to acquire models for your scene. Either third party models you found or models you created from 2D images can have unnecessary many mesh vertices and faces for your use case. The downside of this comes in two forms. First, vertices and faces data make your model file larger, regardless of the format you use, which in turn, makes it slower to download. Additionally, a large number of vertices and faces make a model harder to render. This can take more resources and slow down scene rendering.
You may say, but we are using WebGL, rendering shouldn’t be a problem! But remember, we are building a website, not a video game. Your site visitors may not even have the latest dedicated GPU but only an integrated CPU graphics unit.
I highly recommend that you reduce the model complexity. In many cases, depending on the rate of vertices and faces reduction, the reduction in the visual quality of a model is hardly noticeable. There are a few ways you can achieve this without too much hassle in Bender. You can use Decimate Modifier or Limited Dissolve features. They will maximize reduction while still retaining visual quality.
Asynchronous scene loading
The last thing I want to mention is how I loaded my scene. I followed the same principles applied to asynchronous requests. We need to immediately show as much page content as we can and then incrementally load different site parts as data is fetched.
The standard way you fetch scene assets in Babylon.js, including models, is with AssetManager. This class will load different assets inside a scene asynchronously but there is a problem with its default behavior. It will show a loading screen overlaying all content until all assets are loaded inside a scene. What I would recommend is to turn off this loading screen, start to render the scene as soon as possible and incrementally load models inside it. You can even add some entering animations for the models.
Implementing this enabled me to show HTML content (image and greeting message) and an empty scene immediately with models showing up later. AssetManager also has onProgress which I used to add a nice progress indicator in the top right corner of my page.
If you deep dive into Babylon.js I guarantee you will find many more ways to optimize your scene but this is quite enough for good user experience.
I hope I gave you a good idea of how a 3D personal website can be implemented. With this article and code provided you have the knowledge to create something of your own. Fork starter project and start experimenting!