Skip to content

Environment Maps

Video Lecture

Environment Maps Environment Maps

Description

In the case of PBR materials, such as MeshStandardMaterial and MeshPhysicalMaterial, we have the choice of setting the scene environment map, rather than placing multiple lights.

<>

An environment map will take slightly longer to initially render since it is usually created from an image, or multiple images, that you've downloaded. Where-as lights can be easily created and added to your scene when you set it up.

The problem with adding lights though, is that it uses more processing power needed during the render of each frame.

An environment map will be rendered much faster than using several lights each frame, and also produce a much more realistic effect.

In this example, we can toggle the environment map and lighting on and off.

Note that Environment maps don't cast or receive shadows. You can however implement a light that casts shadows along with using an environment map.

The best quality environment maps are created using HDR images. However, the file sizes of HDR images are quite large compared to JPG or PNG.

So, we can also compress HDR images, using an online tool in case it will benefit your application.

Note

Three r163 added new property scene.environmentIntensity. Affects PBR materials with material.envMap = null.

Changing a material.envMapIntensity will now only work if you have set a fully loaded texture to the material.envMap property first.

Note that after setting material.envMap = someFullyLoadedTexture, it will no longer be affected by changes to scene.environmentIntensity.

Lesson Scripts

./index.html

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Three.js TypeScript Tutorials by Sean Bradley : https://sbcode.net/threejs</title>
  </head>

  <body>
    <script type="module" src="/src/main.ts"></script>
  </body>
</html>

./src/style.css

1
2
3
4
body {
  overflow: hidden;
  margin: 0px;
}

./src/main.ts

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
import './style.css'
import * as THREE from 'three'
import { OrbitControls } from 'three/addons/controls/OrbitControls.js'
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js'
//import { RGBELoader } from 'three/addons/loaders/RGBELoader.js'
import Stats from 'three/addons/libs/stats.module.js'
import { GUI } from 'three/addons/libs/lil-gui.module.min.js'

const scene = new THREE.Scene()

const environmentTexture = new THREE.CubeTextureLoader().setPath('https://sbcode.net/img/').load(['px.png', 'nx.png', 'py.png', 'ny.png', 'pz.png', 'nz.png'])
scene.environment = environmentTexture
scene.background = environmentTexture

//const hdr = 'https://sbcode.net/img/rustig_koppie_puresky_1k.hdr'
// //const hdr = 'https://sbcode.net/img/venice_sunset_1k.hdr'
// //const hdr = 'https://sbcode.net/img/spruit_sunrise_1k.hdr'

// let environmentTexture: THREE.DataTexture

// new RGBELoader().load(hdr, (texture) => {
//   environmentTexture = texture
//   environmentTexture.mapping = THREE.EquirectangularReflectionMapping
//   scene.environment = environmentTexture
//   scene.background = environmentTexture
//   scene.environmentIntensity = 1 // added in Three r163
// })

const directionallight = new THREE.DirectionalLight(0xebfeff, Math.PI)
directionallight.position.set(1, 0.1, 1)
directionallight.visible = false
scene.add(directionallight)

const ambientLight = new THREE.AmbientLight(0xebfeff, Math.PI / 16)
ambientLight.visible = false
scene.add(ambientLight)

const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 100)
camera.position.set(-2, 0.5, 2)

const renderer = new THREE.WebGLRenderer({ antialias: true })
renderer.toneMapping = THREE.ACESFilmicToneMapping
renderer.setSize(window.innerWidth, window.innerHeight)
document.body.appendChild(renderer.domElement)

window.addEventListener('resize', () => {
  camera.aspect = window.innerWidth / window.innerHeight
  camera.updateProjectionMatrix()
  renderer.setSize(window.innerWidth, window.innerHeight)
})

const controls = new OrbitControls(camera, renderer.domElement)
controls.enableDamping = true

const texture = new THREE.TextureLoader().load('https://sbcode.net/img/grid.png')
texture.colorSpace = THREE.SRGBColorSpace

const material = new THREE.MeshPhysicalMaterial()
material.side = THREE.DoubleSide
// material.envMapIntensity = 0.7
// material.roughness = 0.17
// material.metalness = 0.07
// material.clearcoat = 0.43
// material.iridescence = 1
// material.transmission = 1
// material.thickness = 5.12
// material.ior = 1.78

const plane = new THREE.Mesh(new THREE.PlaneGeometry(10, 10), material)
plane.rotation.x = -Math.PI / 2
plane.position.y = -1
plane.visible = false
scene.add(plane)

new GLTFLoader().load('https://sbcode.net/models/suzanne_no_material.glb', (gltf) => {
  gltf.scene.traverse((child) => {
    ;(child as THREE.Mesh).material = material
  })
  scene.add(gltf.scene)
})

const data = { environment: true, background: true, mapEnabled: false, planeVisible: false }

const gui = new GUI()

gui.add(data, 'environment').onChange(() => {
  if (data.environment) {
    scene.environment = environmentTexture
    directionallight.visible = false
    ambientLight.visible = false
  } else {
    scene.environment = null
    directionallight.visible = true
    ambientLight.visible = true
  }
})

gui.add(scene, 'environmentIntensity', 0, 2, 0.01) // new in Three r163. Can be used instead of `renderer.toneMapping` with `renderer.toneMappingExposure`

gui.add(renderer, 'toneMappingExposure', 0, 2, 0.01)

gui.add(data, 'background').onChange(() => {
  if (data.background) {
    scene.background = environmentTexture
  } else {
    scene.background = null
  }
})

gui.add(scene, 'backgroundBlurriness', 0, 1, 0.01)

gui.add(data, 'mapEnabled').onChange(() => {
  if (data.mapEnabled) {
    material.map = texture
  } else {
    material.map = null
  }
  material.needsUpdate = true
})

gui.add(data, 'planeVisible').onChange((v) => {
  plane.visible = v
})

const materialFolder = gui.addFolder('meshPhysicalMaterial')
materialFolder.add(material, 'envMapIntensity', 0, 1.0, 0.01).onChange(() => {
  // Since r163, `envMap` is no longer copied from `scene.environment`. You will need to manually copy it, if you want to modify `envMapIntensity`
  if (!material.envMap) {
    material.envMap = scene.environment
  }
}) // from meshStandardMaterial
materialFolder.add(material, 'roughness', 0, 1.0, 0.01) // from meshStandardMaterial
materialFolder.add(material, 'metalness', 0, 1.0, 0.01) // from meshStandardMaterial
materialFolder.add(material, 'clearcoat', 0, 1.0, 0.01)
materialFolder.add(material, 'iridescence', 0, 1.0, 0.01)
materialFolder.add(material, 'transmission', 0, 1.0, 0.01)
materialFolder.add(material, 'thickness', 0, 10.0, 0.01)
materialFolder.add(material, 'ior', 1.0, 2.333, 0.01)
materialFolder.close()

const stats = new Stats()
document.body.appendChild(stats.dom)

function animate() {
  requestAnimationFrame(animate)

  controls.update()

  renderer.render(scene, camera)

  stats.update()
}

animate()

HDR Compression

HDR Images are usually very large to download over the internet. We can now use HDR images as a logarithmic gain map encoded with the JPEG algorithm. We can use a third party library to load them.

Example compression results for the venice_sunset.hdr.

Quality HDR HDRJPG Example
1k 1.4mb 123kb HDRJPG 1k Example
2k 5.6mb 451kb HDRJPG 2k Example
4k 22.3mb 1.7mb HDRJPG 4k Example

To install,

npm install @monogrid/gainmap-js --save-dev

Visit the Monogrid HDR Compression Tool, upload your HDR and then save it into your projects ./public/img/ folder.

Then in your code, import the HDRJPGLoader,

import { HDRJPGLoader } from '@monogrid/gainmap-js'

and then after you've instantiated your renderer, you can use it like this below.

new HDRJPGLoader(renderer).load(`/img/venice_sunset_1k.hdr.jpg`, (texture) => {
  texture.renderTarget.texture.mapping = THREE.EquirectangularReflectionMapping
  scene.environment = texture.renderTarget.texture
})

If you only need an environment map in your scene, then using the smallest HDRJPG may be good enough. But if you also want to show it as the background, and you want the background to look crisp, then the 4k HDRJPG will be better. But it will take longer to download across the internet.

Some working examples linked on this website will use the HDRJPGLoader in place of the RGBELoader.

Scene.environment (threejs.org)

MeshPhysicalMaterial (threejs.org)

WebGLRenderer.toneMapping (threejs.org)

HDRI to CubeMap (GitHub)

Monogrid HDR Compression Tool

Comments