The 3D scene in the new Open Studios website is fully responsive, changing the position of furniture and camera settings to better compose the scene for different viewports. This screenshot was taken with an extremely wide window for dramatic effect.

Building A Cool 3D website: Tips, Tricks and Lessons Learned

Paul Brzeski
11 min readOct 27, 2023

I first learned to make websites back in 1999 using Macromedia software like Dreamweaver, Fireworks and Flash. It was a golden age for vibrant interactive experiences that pushed the boundaries of what to expect and that type of web development is what influences me. As a kid of eleven, it was life changing to be able to make my own graphics, animations and interactive experience all with definitely not warez (hai fbi — did I mention I was a minor at the time). The film The Matrix also just came out and I was learning how to use Linux. What I’m saying is that I’ve always been a nerd.

Learning is a journey that never ends, certainly that’s even more true in the world of games programming where even the most sensible wisdoms might get thrown out the window to achieve a result. Some might find that never ending challenge off-putting, but I love it and think it’s fantastic. For Open Studios new website, we wanted to create a digital experience that represented our creative and technical capabilities as a team. Knowing that pushing up against limitations can sometimes shape and produce the best innovative outcomes in the delivery of any project, we decided to proceed without considering the limits and just design whatever we wanted.

Discussing what our new website would look like, we talked about building a virtual world and settled upon a 3D office. Inspired by experiences with video games like Cyberpunk 2077 and Grand Theft Auto, the idea was that you’ve entered an office and when you go to use a computer in that game world it pans and zooms the viewport to that computer for interaction.

Garrett put together a 3D design reference for me to follow so that I could focus my efforts on the programming side of things. In the end the limitations of what I could do with three.js and as a browser experience pushed me to elevate the final result to new heights — far beyond our original plans. This article is a retrospective of that build.

This design reference was made in Blender and rendered using Cycles by Garrett Kinder.

Architecture and tech stack

In the past I’ve followed a couple of personal taste choices in my work, most of these came about due to my own journey in the industry. For example I liked the Jade templating language because it was similar to what I liked about Haml back in the Ruby on Rails days. I’ve called this combination of items the “Kettlefish” stack.

Kettlefish is the combination of the following technologies:

  • A command line file generating build script
  • Rollup as the JS builder
  • Pug JS templates (Jade when the project started)
  • Stylus as the CSS preprocessing language

The name is a portmanteau of the phrase “A different kettle of fish” because static HTML generation was very novel at the time. It isn’t anymore and so in the 4 years since I’ve dropped my own builder script (and NPM package) in favour of the massive Eleventy ecosystem. So massive I was able to just re-create my Kettlefish stack again inside it.

HTML templating

Using Pug JS templates I split each page into variants — one that would live on the HTML fallback version of the site and another that would only live inside the virtual computer screen omitting things like header and footer menus. I originally tried to just hide the items in question using JavaScript but as they were loading inside iFrames that was problematic.

The About us page has a search engine facing HTML only page that is intended to load if the users device doesn’t support Javascript and an iFrame version for loading on the computer screen in the virtual world.

Here’s a quick rundown of the source files involved:

The /src/web/ folder contains Pug files that define the HTML pages that are going to be created, as you can see from the two About us source files Eleventy front matter data controls file output location, SEO fields. extends sets the template and include the common content source in /src/content/about_us.pug.
The /src/templates folder contains Pug files that contain common HTML structures such as the main HTML tag, head and body elements, etc. Pictured above are the two templates that power the iFrame and HTML fallback pages.

Javascript builder

I followed the (at the time) latest tutorial for developing with Three.js but that didn’t work with my Rollup configuration. The current recommended builder is Vite but I had issues getting that to work with my WIP code, unfortunately I hadn’t decided to add a builder until I got to prelaunch!

Along to the rescue came esBuild, somehow having just the right defaults that it required nothing but a default configuration for my setup. This was probably the first positive experience I’ve ever had adopting a new unknown JS builder… I hope other web developers reading this get a sense of just what that means to me cause I’ve definitely had a *bad* time before!

This is a side by side comparison of my build and watch configuration files for esBuild. In my experience a typical Rollup config is generally twice this size with maybe a couple of addon modules needed. Webpack will have a config magnitudes longer than Rollup. Esbuild just seems to ‘get it’ out of the box.

Loading screen

As I began to test for performance, it was immediately clear I had to implement some kind of loading screen. People with slow connections would be sitting there with nothing happening if assets were being loaded in and even on my relatively powerful computer I had a slight chug when constantly refreshing the scene from cache.

Turning back to my childhood love of The Matrix, I decided to implement a loading screen inspired by that. It was trivial to sneak in a bit of code into the HTML source that would create that effect before the main virtual office code and it’s dependencies would be loaded asynchronously while the user is distracted by the pretty effect.

This video demonstrates a longer loading time by throttling the network speed to a slow 3G connection using Chrome Web Developer Tools

Asset preparation

In real time 3D it’s best to use simple geometry where possible and so some of the objects in the office scene are nothing more than rectangles with well designed materials. In fact the room itself is just a box with the door programmatically cut out using three-bvh-csg.

The original models used in the room concept contained too much geometry and so I had to prepare them for the web in Blender first. Using the decimate modifier it was a bit of a trial and error process figuring out just how much detail I could lose while maintaining acceptable visuals.

This small monstera plant came free from BlenderKit, but still required a bit of work to prepare it for use in a real-time 3D setting like our Three.js website.

The glTF file format is recommended by Three.js and when I tried to use anything else I quickly learned why. When using the game industry standard FBX the rendered sizes of different assets was off and somehow their geometry got corrupted. The file sizes were also larger than their glTF counterparts. As the scene cannot begin playing until all the assets are loaded, it was important that their files were as small as possible.

Preparing 3D models for the web reminded me a lot of compressing images for the web back in the dialup internet days. Thankfully we have things like AJAX now so rather than rolling up my visual assets into one giant binary file, they are individually loaded in asynchronously while the loading screen with the Matrix effect plays for the user.

You may notice that the original concept and produced website look a bit different, part of how that came about is the options for materials and shaders that each 3D renderer and environment brings to the table. Within Blender the results between just the two renderers Eevee and Cycles vary greatly, and so it also goes when you export your assets from there for use in real time 3D applications like Three.JS or Unreal Engine. Even in the world of film where you have a bit more control, there can often be a big change between the original design and the final result.

On the left is the original concept render in Blender Cycles and on the right the Three.JS production result.

Performance optimisation

Perhaps one of the biggest risks and unknowns in a venture like this is how to handle the different devices and their hardware capability. Using the FPS (Frames per second) I was able to monitor the users game session for a few seconds and then scale the visual experience according to what their device was able to render smoothly enough. This worked OK in theory, until tested on actual older devices which just crashed before they could even render a single frame with effects like bloom switched on. Even trying to slowly switch them on one by one didn’t work because those larger effects just cannot work on some settings and crash. Problem!

Enter detect-gpu, a great little library containing a lookup table of many common device types and a simple scoring system. Using the score that detect-gpu provides, I was able to implement the logic that decides to switch on the very advanced effects if the device is deemed advanced enough — far preferable to slowing down/crashing peoples devices!

In the end I ended up with two modes, one that doesn’t use the effects compositing or anti aliasing and a high end GPU mode that has it all. It became more work than expected because of the way light was calculated in the room — without the bloom effects all the colour and brightness values shift so the non special effects version of the scene has a totally different set of settings in order to maintain visual acuity with the full version.

The website has a manual switch for forcing effects to be off, you can activate it by adding a query string key of “fast”.

On the left is the low end device variant of the scene without effects like bloom and filmic colour. On the right is the full version.

Responsiveness and viewports

Websites will reflow their content in order to present the best experience for users on different screen sizes. The portrait version of the virtual office was originally completely unusable and so a bit of work went into trying to re-compose the scene around the different possible viewports.

To make life easier with positioning, helpers were added to the scene. The website still has a debug mode if you want to have a look.

The result of the responsiveness work is a window resize event listener that changes room depth, moves furniture and changes the camera field of view, position and rotation so that the maximum amount of stuff is visible in a portrait frame — without completely changing the scene.

As you might notice when looking carefully at these screenshots, the furniture in the room and the field of view are shifted slightly in order to recompose the scene for portrait and landscape modes. We are also running without effects like bloom or anti aliasing here as my laptop couldn’t handle doing them.

Device testing

While I could load the website on my devices using my wi-fi, it was still important for debugging to have access to a console where I could see errors, run arbitrary commands and observe what happens when I run them without going back and forth between my phone and computer.

Chrome has an excellent built-in remote debugger for Android devices and I made extensive use of that when optimising the performance and viewport responsiveness of the site. Unfortunately I don’t have access to any iOS devices in my home so I had to rely on some online resources to render what that looked like and got feedback from a few people who had a variety of devices.

The Chrome Remote Debugger showing a live feed of my phone’s browser tab on the left and the corresponding console with Langenium engine output on the right.

One user with an iPhone 11 wasn’t able to load the website at all due to iOS13 not being able to do ECMAScript 2015. The asset loaders within my code and some of the vendor code relied on Promises so I made the call that anyone with iOS less 14 just has to see the “1999 experience”, which is what we call the HTML fallback in house.

Scope creep

Like any web development project, this one had some scope creep. Two notable ones that added about a month and a half to the project.

Delay one — Entering the virtual office

When the 3D scene loads on the homepage, that made sense to me because it’s the main door to the office. But say you enter via the “Case studies” page from Google, it’s easy enough for me to detect the page loaded and throw up the correct virtual screen but the delay to get back to reading content felt like an unreasonable disruption to the user journey they were on.

To address the issue of delays when requesting specific pages, I added an twinkly exit sign that would load in using a miniature Matrix effect in the top right corner. You can see this effect in action if you visit a page like About Us. This small effect took a couple of weeks to get right but the pay off was definitely worth it, creating a more cohesive user experience that demonstrates our absolute finesse and control with their journey.

Delay two — Visual acuity of the 1999 experience

The 1999 experience, or HTML fallback, went through a couple of design iterations. Originally looking more like the menu from an 1950s American diner, we opted for something a bit more modern.

V1 of the static website on the left compared to V4 on the right, as you can see a few changes happened between them!

It can be tricky making the call when to pause a project so that a blocker can be resolved. With our new branding, we started out with the logo and render of the office but didn’t have much in the way of additional graphics we could use on things like social media banners. I made the call the pause the project and worked on a 3D animation we dubbed “Riverside Outrun”.

Producing Riverside Outrun was a lot of fun. I used an old photo of the Perth skyline from before the mining buildings went up and gave it a bit of photomanipulation magic to turn it into a much longer skyline with the right colour profile for our vaporwave aesthetic. This image of the city was used as the skybox for the 3D animation.

Like the themes of The Matrix, the year 1999 and the Vaporwave Aesthetic itself, using this older photo of the Perth skyline on the left carries on our theme of celebrating the past but with a futuristic twist.

The vaporwave aesthetic of the video meshes well with the brand and the virtual office. This additional sprint of creative and visual production ended up giving rise to our new motto “To Another World” so I’m very happy with how it all turned out and am glad I took the time to do that.

“To Another World” is kind of like a re-launch trailer for our business, I’m currently working on an extended version that intersplices footage of some of our other work as the car is driving in the night.

The future

This new website is a platform for us to share the vision of our digital studio with the world. A few things from the original plan didn’t make it to launch — notably a wall of pet photos and a 3D talking cat. We will be adding those features over the months to come.

As time rolls on, we plan to add more rooms to the experience. The concept is that you are exploring a 3D building that represents our real world studio space but in the digital realm.

The first version is just our office with our computer workstations but we plan to add other rooms from our studio. The jewellery workshop and its show room are probably going to be the next steps. We also have a crafts room for sewing and studio photography.

Finally, if you still haven’t seen it, check out the Open Studios website now!

--

--

Paul Brzeski
Paul Brzeski

Written by Paul Brzeski

Sharing my opinion and passions about the many things in life.