---
title: Rebuilding my WebXR portfolio, one headset test at a time
date: 2026-09-26
summary: Bringing my old portfolio gallery up to date with Three.js, readable lighting, natural hand interaction, and an AR view you can hold in your hands.
status: published
tags: webxr, three.js, interaction design, software engineering, ai collaboration
heroImage: /img/blog/webxr-refresh/entrance-after.webp
cardImage: /img/blog/webxr-refresh/podium-after.webp
socialImage: /img/blog/webxr-refresh/entrance-after.webp
imageAlt: The refreshed portfolio gallery, with illuminated lettering, framed photographs, and a timber ceiling.
imageCaption: The same room, rebuilt around the people who will visit it.
---

# Rebuilding my WebXR portfolio, one headset test at a time

My WebXR portfolio had become a time capsule. The client work was still worth showing. So were the experiments with games, broadcasts, 360-degree media, and spatial interfaces. But the room holding all of it had fallen behind the rest of my site. Some of the writing was out of date. The materials looked like placeholders. Even the little “About this site” sign was an image of text that had been edited to change the copyright years.

I wanted to keep the room and the history in it. I also wanted someone considering me for a job to be able to walk through it and find a current, cared-for piece of software.

I worked through the refresh with GPT-6 in Codex. The useful part of that collaboration was the iteration: inspect the existing implementation, make a bounded change, test it, then put on the headset and see what the tests missed. My first Quest 3 pass looked much better and felt smooth. It also gave me a very specific list of things to fix.

## Keeping the gallery, replacing the machinery

The original scene used A-Frame. That was a useful way to build the first version: describe the room in HTML, attach components, and get into a headset quickly. Over time, the page accumulated dependencies and behaviors that were harder to inspect together.

The refreshed gallery uses Three.js directly, with WebXR for the headset session and cannon-es for device physics. The exhibits, positions, text, and material choices live in a scene manifest. Loading, rendering, navigation, grabbing, and session management have separate modules.

That separation matters when something goes wrong. If a tablet moves strangely, I can inspect its body and the hand holding it. If a sign is hard to read, I can change its lettering without rewriting the movement code. The project images and arrangement still tell the same story.

The comparisons in this post use the same camera position, direction, field of view, and image dimensions. I captured the old version from the live site before replacing it. The refreshed views come from the local build. These are browser captures, not headset recordings; navigation overlays are hidden in both versions so the room is easier to compare. Tap an image to inspect it at full size.

| Original live gallery | Refreshed gallery |
| --- | --- |
| [![Original entrance with flat colors and the large portfolio sign.](/img/blog/webxr-refresh/entrance-before.webp)](/img/blog/webxr-refresh/entrance-before.webp) | [![The same entrance with finished materials, framed signage and colored heading backlights.](/img/blog/webxr-refresh/entrance-after.webp)](/img/blog/webxr-refresh/entrance-after.webp) |

## Lighting that helps me read

The first lighting pass made the room feel more coherent, but it also made parts of it drab. Brightening everything would have brought back the washed-out look I was trying to leave behind.

The more useful change was to decide what should produce light and what should receive it. The description panels now read as light boxes. Photographs have illuminated frames. Large section headings keep a crisp white face with colored light inset behind the letters. The room has warmer rugs, richer teal timber, mineral walls, and a few restrained decorative details. At the entrance, the big WebXR script is now a rich violet against a softly graded teal enamel panel. The saturated lettering and softer backdrop make that first view feel more welcoming, while a dark edge keeps the script readable where it crosses the large portfolio letters.

One last headset pass caught flickering where the script letters overlap. The transparent padding around those letters was writing competing depths at the same surface. Keeping it out of the depth buffer, while still checking the lettering against the solid room, fixed that without adding another rendering pass.

Most of that effect does not require another light in the scene. The panels use luminous materials, the local halos are small pieces of geometry, and the lettering gets its edge treatment in the text shader. Two actual feature lights give the room some direction. There is no real-time shadow pass for every frame on the wall.

The surface detail is shared, too. Plaster, ash, woven fabric, terrazzo, powder-coated metal, and aluminium use small textures generated once when the gallery loads. Their scale is based on metres in the room, so stretching a wall does not turn a fine texture into an enormous pattern. Repeated static pieces are combined where that saves drawing work without making the whole building one indivisible object.

| Original live gallery | Refreshed gallery |
| --- | --- |
| [![Original view of the right-hand section dividers.](/img/blog/webxr-refresh/gallery-before.webp)](/img/blog/webxr-refresh/gallery-before.webp) | [![Matching divider view with colored heading backlights, framed exhibits and richer materials.](/img/blog/webxr-refresh/gallery-after.webp)](/img/blog/webxr-refresh/gallery-after.webp) |

## A hand should behave like a hand

The headset exposed a problem that looked fine with controllers. The hand models lined up when I held the controllers, then twisted at the wrist when I put them down. The controller's pointing direction was being used for a job that needed the hand's actual anatomy.

The new gloves follow the wrist and finger joints supplied by WebXR. A controller ray still does the pointing; it no longer decides how an articulated wrist should be rotated. The gloves use teal fabric, mint trim, and rounded finger segments, with shared geometry to keep them inexpensive to draw.

My next observation was about grabbing. A precise thumb-and-forefinger pinch worked, but that is not how I naturally pick up a phone. I wanted to curl my fingers around it or support it on an upward-facing palm.

The interaction now checks the palm and fingers against the device's oriented collider. A curled grip can acquire it without a pinch. Opening that grip releases it. An open palm can support it, and tilting the palm lets it go. Once a grip is established, the device keeps its position relative to the hand instead of snapping to an arbitrary pose. Release returns it to physics with the recent motion of the hand.

Passing a device between hands exposed another small but uncomfortable problem. Both hands cannot own the same phone. A successful second grab now transfers it to that hand, keeping its position and rotation at the moment of transfer. The first hand can open or move away without dropping it, and fingers that are still curled do not immediately grab it back.

This is still an interaction model, not a simulation of skin and pressure. It needs to tolerate tracking noise and small occlusions without making a tablet jump. The important distinction is that the gesture follows the thing I am trying to do.

Pinching away from an object still activates the teleport arc. While holding that pinch, twisting the wrist changes the arrow at the destination, giving hands a counterpart to the controller thumbstick's landing-direction control. A small dead zone and smoothing keep tiny wrist movements from making the arrow twitch.

## Current content, without erasing the old projects

A portfolio can be technically current and still give someone the wrong impression if its content is stale.

The résumé posters and pickup tablets now use captures of my actual [About page](/about/). A small capture script arranges complete sections onto two sheets, checks that they fit, and records what was captured. The same images are used on the wall and on the tablets, with matching proportions. They also contain fewer pixels than the old square textures, despite presenting more current information.

The tablet backs say “Ben McNulty.” The contact panel points to LinkedIn for professional conversations and `@benlivenow` on Instagram and Threads for personal updates. The personal panel says my partner and I have been together since 2012, which will remain true without an annual edit. Descriptions of completed projects stay in the past tense.

| Original live gallery | Refreshed gallery |
| --- | --- |
| [![Old résumé posters on the gallery wall.](/img/blog/webxr-refresh/resume-before.webp)](/img/blog/webxr-refresh/resume-before.webp) | [![New résumé posters captured from the current About page, with clear sightlines.](/img/blog/webxr-refresh/resume-after.webp)](/img/blog/webxr-refresh/resume-after.webp) |

The social podium now carries extruded Instagram and Threads marks and a readable, luminous handle. Those shapes are built from the official brand vectors. They are geometry in the scene, with a little depth and a material finish, rather than another screenshot pretending to be a physical object.

| Original live gallery | Refreshed gallery |
| --- | --- |
| [![Original social podium with platform names written as text.](/img/blog/webxr-refresh/podium-before.webp)](/img/blog/webxr-refresh/podium-before.webp) | [![The refreshed podium with extruded logos, illuminated handle and updated résumé tablets.](/img/blog/webxr-refresh/podium-after.webp)](/img/blog/webxr-refresh/podium-after.webp) |

The credits sign is maintained as text now. It names the current technology, credits GPT-6 for the enhancement work, and keeps the “made with love” sentiment with a small red heart, rounded out from the wall with a flat back. I asked for another pass on that shape: a plump little sculpture felt more fitting than a beveled cutout. Updating a sentence no longer means editing an image of a sentence.

| Original live gallery | Refreshed gallery |
| --- | --- |
| [![The old credits image listing the original tools and copyright years.](/img/blog/webxr-refresh/credits-before.webp)](/img/blog/webxr-refresh/credits-before.webp) | [![Editable credits with the current stack, GPT-6 attribution and a three-dimensional heart.](/img/blog/webxr-refresh/credits-after.webp)](/img/blog/webxr-refresh/credits-after.webp) |

## AR needs its own way of looking at the room

Entering AR should give me a useful view of the gallery in my physical space. A life-size room dropped around me is not particularly helpful for that.

The new AR mode presents the gallery as a miniature. The roof and overhead structure are removed from that view, so I can look down into it. One hand can move and rotate the model; holding it with two hands lets me change its size and facing. It is designed for the kind of close inspection I would give an architectural model.

The miniature reuses the gallery's exhibit geometry and materials, with a compact beveled base beneath the room. The full-size physics world pauses while it is open. Returning from AR restores the regular gallery and my previous desktop position. That avoids scaling a physics simulation down, then hoping it will return to normal afterward.

## Smooth is something I have to check

The Quest 3 feedback on the earlier refreshed build was encouraging: the scene looked good and movement felt smooth. That was useful evidence, but it did not prove that every later change would be free.

Angled edges were still sharper than I wanted. Enabling native antialiasing looked noticeably better when I tried it in the Quest 3, so that smoother view is the baseline now. The headset quality choices remain available, along with an off setting for comparison. I want to keep improving the appearance without giving up comfortable movement. A stack of full-screen effects is not worth it if the room stops feeling smooth.

Desktop rendering has its own resolution budget. On the development Mac, the actual browser canvas kept a roughly 60 Hz cadence across five views with smoothing enabled, including a Retina-sized buffer capped at 4.2 million pixels. Those measurements include the running gallery; an offscreen rendering benchmark had made the desktop cost look much worse. Testing the path people actually use matters.

The automated checks cover loading, text layout, texture use, collisions, controller behavior, hand gestures, teleporting, and AR entry and exit. Desktop GPU measurements help me spot expensive changes. The headset passes confirmed the improved locomotion and edge quality and helped refine the grabbing. Automated checks now cover passing a device between hands, including the first hand letting go afterward. I then tried the hand-to-hand transfer in the Quest 3, where it worked smoothly. The miniature AR interactions have automated coverage, with broader device testing still ahead. A desktop timing result or an emulated headset cannot settle how every interaction will feel on every device.

That is also why I wanted the gallery back in the main paths through the site. It gives visitors a working project to inspect alongside the résumé and project descriptions. The [Hire Me page](/hire-me/) now gives it a direct entrance, alongside the other ways to learn about my experience.

[Explore the gallery](/webxr/) on a regular screen, or open the same page in a compatible headset. The résumé and conventional portfolio remain available if a walk through a 3D room is not how you want to browse.

### Technical references

- [Three.js WebXRManager](https://threejs.org/docs/pages/WebXRManager.html): controller, grip and hand spaces; headset rendering controls.
- [WebXR Hand Input](https://www.w3.org/TR/webxr-hand-input-1/): articulated joint poses and anatomical axes.
- [Instagram brand resources](https://www.meta.com/brand/resources/instagram/instagram-brand/) and [Threads brand resources](https://www.meta.com/brand/resources/threads/): source geometry for the podium marks.
