Environment Maps
Video Lecture
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
| 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
.
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
.
Useful Links
Scene.environment (threejs.org)
MeshPhysicalMaterial (threejs.org)
WebGLRenderer.toneMapping (threejs.org)
HDRI to CubeMap (GitHub)
Monogrid HDR Compression Tool