Creating the Effect of Transparent Glass and Plastic in Three.js

In a recent release of Three.js (r129 and beyond) some fabulous new features to MeshPhysicalMaterial were merged. The new features allow us to create convincing transparent, glass-like and plastic-like materials that refract and diffuse the content behind them, and are as easy-to-use as adding a couple of material properties!

About this article

This article explores some advanced properties of materials. While the results are very technically impressive, the new features that enable them are simple to use! Some experience with three and an intermediate understanding of the concept of “materials” in 3D graphics is ideal. Code examples are written for brevity, so it’s best to dive into the sandbox code (provided with each screenshot) if you’re interested in the gritty implementation details.

The physics of optics, light, reflection and refraction are not discussed in-depth here. This article approaches these effects through an aesthetic lens: aiming for convincing and visually pleasing results, even if they are not scientifically accurate.

Rather than introducing new concepts, this is primarily a walkthrough of features that exist within three and its MeshPhysicalMaterial class. I’d like to gush and shower praise upon the contributors and maintainers of three. It continues to be a core pillar of 3D in the browser. It has a vibrant community and extremely talented contributors who continue to push the boundaries of what’s possible on a humble web page.

Prior Art

Creating transparent materials, especially with texture and diffusion, has for a long time required deep technical expertise and creative problem solving. Some projects have achieved an impressive and convincing effect in WebGL through bespoke techniques:

A screenshot from Make Me Pulse’s 2018 Wishes, showcasing frosted glass materials in WebGL
The Jam3 FWA 100 project, showcasing glass-like orbs

Jesper Vos published an incredible tutorial here on Codrops: Real-time Multiside Refraction in Three Steps, which includes some great insights into the science and simulation of refraction.

In addition, these excellent technical examples provided the inspiration for writing this article, and further exploring what’s possible with these new features.

Three.js

three is an open-source javascript library for rendering 3D graphics in the browser. It provides a friendly API and abstractions that make working with WebGL more palatable and expressive. three has been around since 2010, is extremely well battle-tested, and is the de-facto standard for rendering 3D content on the internet. See the list of case studies on the home page, docs, examples, or source.

MeshPhysicalMaterial

MeshPhysicalMaterial is a relatively recent Physically-Based Rendering (PBR) built-in material for three. It’s an evolution and extension of the already impressive MeshStandardMaterial, providing additional features to pump the photo-realism.

This visual fidelity comes at a cost, from the docs:  As a result of these complex shading features, MeshPhysicalMaterial has a higher performance cost, per pixel, than other Three.js materials. Most effects are disabled by default, and add cost as they are enabled.

Beyond the properties offered in MeshStandardMaterial, it introduces some new ones:

Transmission

transmission is the key to transparent glass-like and plastic-like effects. Traditionally when we adjust the opacity of an element to make it transparent, its visual presence is diluted as a whole. The object appears ghostly, uniformly transparent, and not realistic as a see-through object. In the real-world, transparent objects reflect light and show glare. They have a physical presence even though they may be perfectly clear.

Reflectivity properties

MeshPhysicalMaterial includes some properties that estimate refraction through the transmissible object: thickness, ior (Index-of-refraction) and reflectivity. We’ll mostly ignore ior and reflectivity (which changes ior too, but is mapped to a 0-1 range) as the defaults work great!

thickness is the magic here, as we’ll see shortly.

Clearcoat

Like a layer of lacquer, clearcoat provides an additional thin reflective layer on the surface of objects. Previously this would require a second version of the object, with a separate material, and with different parameters.

Other

There are some other additional properties on MeshPhysicalMaterial like sheen and attenuationTint which I won’t be touching on in this article.

We can expect to see more and more features added to this material in future releases.


First steps

First things first, let’s create a scene and pop something in it! We’ll start with an Icosahedron because hey, they just look cool.

I’m skipping the basic scene setup stuff here, I recommend diving into the sandbox source or three docs if you’re unfamiliar with this.

const geometry = new THREE.IcosahedronGeometry(1, 0);
const material = new THREE.MeshNormalMaterial();
const mesh = new THREE.Mesh(geometry, material)
scene.add(mesh);

An Icosahedron with normal shading in a blank scene
View on CodeSandbox

Looks like an Icosahedron! Let’s apply our MeshPhysicalMaterial:

const material = new THREE.MeshPhysicalMaterial({
  metalness: 0,  
  roughness: 0
});

The options metalness and roughness are the two primary handles with PBR materials (they are on MeshStandardMaterial too). They can be used to set the stage for how our material responds to lighting and environment. Having both set at zero describes something like “A non-metallic object with a highly polished surface”.


View on CodeSandbox

Doesn’t look like much! Physically-based materials need light to reflect, so let’s add some light:

const light = new THREE.DirectionalLight(0xfff0dd, 1);
light.position.set(0, 5, 10);
scene.add(light);


View on CodeSandbox

Cool, there it is again… Now let’s make it transparent!

Call the Glazier

The transmission option is responsible for applying our transparency. It makes the “fill” or “body” of the object transparent, while leaving all lighting and reflections on the surface in-tact.

Note that we’re not using the opacity option, which applies a uniform transparency to the material as a whole. We also don’t need to include the transparent option on the material for it to appear transparent through transmission.

const material = new THREE.MeshPhysicalMaterial({  
  roughness: 0,  
  transmission: 1, // Add transparency
});


View on CodeSandbox

I think that’s transparent, we can see the background colour through it. Let’s pop something else behind it to be sure. We’ll add a textured plane as our “backdrop”:

const bgTexture = new THREE.TextureLoader().load("src/texture.jpg");
const bgGeometry = new THREE.PlaneGeometry(5, 5);
const bgMaterial = new THREE.MeshBasicMaterial({ map: bgTexture });
const bgMesh = new THREE.Mesh(bgGeometry, bgMaterial);
bgMesh.position.set(0, 0, -1);
scene.add(bgMesh);


View on CodeSandbox

It’s transparent! It’s lacking something though. There’s nothing but a tiny flicker of movement on the corners of our geometry; as if our material is made from the most delicate and fragile of super-thin, super-clear glass.

Now here’s the magic part!

const material = new THREE.MeshPhysicalMaterial({  
  roughness: 0,  
  transmission: 1,  
  thickness: 0.5, // Add refraction!
});


View on CodeSandbox

By adding a single option: thickness to our material, we’ve now been given the gift of refraction through our object! Our background plane, which is a completely separate object, simply sitting behind our Icosahedron in the scene, now gets refracted.

This is incredible! Previous methods of achieving this required much more work and intense technical understanding. This has immediately democratised refractive materials in WebGL.

The effect is especially impressive when viewed in motion, and from an angle:

Swooping around our glass object to see it refracting the rest of the scene

Have a play by dragging around in this sandbox:

Diverse objects

While the sharp facets of our Icosahedron show a nice “cut-gem” style of refraction, we rarely see such precisely cut glass objects at any size other than tiny. This effect is greatly enhanced when geometries with smoother edges are used.
Let’s increase the detail level of our Icosahedron to form a sphere:

const geometry = new THREE.IcosahedronGeometry(1, 15);


View on CodeSandbox

This shows some optical distortion in addition to the refraction based on the shape of the geometry!


Hot tip: with all of the PolyhedronGeometry types in three, any detail level above zero is rendered as a sphere, rather than a faceted polyhedron as far as transmission is concerned.


You may notice that the distorted content is a little pixelated, this is due to the material upscaling what’s transmitted through it to perform the distortion. We can mitigate this a bit with some other effects which we’ll cover later.

Let’s explore adding some texture to glass material:

const material = new THREE.MeshPhysicalMaterial({  
  roughness: 0.7,   
  transmission: 1,  
  thickness: 1
});

The roughness option on our transmissible material provides us with a “frosting” level, making light that passes through the material more diffuse.


View in CodeSandbox


This becomes immediately recognisable as a frosted glass object, with a fine powdery texture.

Notes on roughness:

  • The middle of the roughness range can display some quite noticeably pixelated transmitted content (at the time of writing). In my experience the best results are found in the low (0-0.15) and higher (0.65+) ends of the range. This can also be quite successfully mitigated with some of the things we’ll add shortly.
  • The distance of the transmissible object from the camera affects how roughness is rendered. It’s best to tweak the roughness parameter once you’ve established your scene

Hot tip: Using a small amount of roughness (0.05 – 0.15) can help soften aliasing on the transmitted content at the cost of a bit of sharpness.


For the rest of our examples we’ll include two additional geometries for reference: a RoundedBoxGeometry and a 3D model of a dragon (loaded as a GLTF, but only used for the geometry):


View on CodeSandbox

Through the lens

While the transmission effect is already appealing, there’s so much more we can do to make this appear truer-to-life.

The next thing we’ll do is add an environment map. It’s recommended that you always include an envMap when using MeshPhysicalMaterial, as per the docs: For best results, always specify an environment map when using this material.

Highly reflective objects show reflections, and glare, and glimpses of their surrounding environment reflected off their surface. It’s unusual for a shiny object to be perfectly unreflective; as they have been in our examples so far.

We’ll use a high quality High Dynamic Range Image (HDRI) environment map. I’ve chosen this one for its bright fluorescent overhead lighting:

const hdrEquirect = new THREE.RGBELoader().load(
  "src/empty_warehouse_01_2k.hdr",  
  () => { 
    hdrEquirect.mapping = THREE.EquirectangularReflectionMapping; 
  }
);
const material = new THREE.MeshPhysicalMaterial({  
  ...  
  envMap: hdrEquirect
});


View on CodeSandbox

NICE! Now that looks more realistic. The objects glint and shimmer in our bright environment; much more like the lighting challenges faced by a photographer of shiny things.

This is where our rounded geometries really shine too. Their smoother curves and edges catch light differently, really amplifying the effect of a highly polished surface.


Hot tip: Adding an envMap texture of some sort helps to resolve some rendering artifacts of this material. This is why it’s always recommended to include one (beyond the fact that it looks great!).


If you adjust the roughness level upward, you’ll notice that the reflections are diffused by the rougher frosted texture of the surface; however, we may want an object that’s semi-transparent while still having a shiny surface.

The clearcoat options allow us to include an additional reflective layer on the surface of our object (think lacquered wood, powder coatings, or plastic films). In the case of our transparent objects, we can make them from semi-transparent glass or plastic which still has a polished and reflective surface.


View on CodeSandbox

Adjusting the clearcoatRoughness option adjusts how highly polished the surface is; visually spanning the range from highly-polished frosted glass through to semi-gloss and matte frosted plastics. This effect is pretty convincing! You can almost feel the tack and texture of these objects.

So far we’ve been exploring objects with perfectly smooth surfaces. To really bring some texture to them, we can add a normal map:

const textureLoader = new THREE.TextureLoader();
const normalMapTexture = textureLoader.load("src/normal.jpg");
normalMapTexture.wrapS = THREE.RepeatWrapping;
normalMapTexture.wrapT = THREE.RepeatWrapping;
const material = new THREE.MeshPhysicalMaterial({  
  ...  
  normalMap: normalMapTexture,  
  clearcoatNormalMap: normalMapTexture,
});


View on CodeSandbox

The interplay between the normalMap and clearcoatNormalMap is interesting. By setting the normalMap we affect the transmission through the object, adding a textured frosting that refracts light differently. By setting the clearcoatNormalMap we affect the finish on the surface of the object.


Hot tip: The additional texture added by the normalMap greatly reduces the visible pixelation on the transmitted content, effectively solving this issue for us.


As a final touch, we’ll add a post-processing pass to apply bloom to our scene. Bloom adds that extra little bit of photographic appeal by simulating volumetric glare from the bright overhead lighting bathing our objects.

I’ll leave information around implementation post-processing within three to the docs and examples. In this sandbox I’ve included the UnrealBloomPass.

Bloom always looks good.

There we have it! Convincingly transparent, textured and reflective 3D objects, rendered in real-time, and without much effort. This deserves to be celebrated, what an empowering experience it is playing with MeshPhysicalMaterial.

Drippin ice

Just for fun, let’s crank the dial on this by rendering many of our transparent objects using three‘s InstancedMesh (link).


View on CodeSandbox

OOOUF! YES.
Instances can’t be seen through each other, which is a general limitation of transmission on MeshPhysicalMaterial (current as of r133); but in my opinion the effect is still very cool.

Explore for yourself

Finally, here’s our dragon model with a bunch of material options enabled in the GUI:

Have a play, check out metalness, play around with color to explore colourful tinted glass, tweak the ior to change our glass into crystal!

Sign-off

I’ve really only scratched the surface of what can be achieved with MeshPhysicalMaterial. There are even more options available within this material, sheen, roughnessMap, transmissionMap, attenuationTint and all sorts of other things provide inroads to many more effects. Dig deep into the docs and source if you’re interested!

This is an enabler, given the creative vision for a transparent object you can use these tools to work towards a convincing result. Transparent materials in three are here, you can start using them in your projects today.

Attributions

  • Environment map: Empty Warehouse 01 HDRI by Sergej Majboroda, via Poly Haven
  • 3D model: Dragon GLB by Stanford University and Morgan McGuire’s Computer Graphics Archive, via KhronosGroup
  • Normal map: Packed Dirt normal by Dim, via opengameart.org
  • Sandboxes: Hosted on CodeSandbox and running in canvas-sketch by @MattDesl.

© 2021 Kelly Milligan