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:
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);
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:
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
.
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