-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy path11-texture.txt
424 lines (277 loc) · 17.4 KB
/
11-texture.txt
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
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
Introduction
00:00
Bored with your red cube yet? It's time to add some textures.
But first, what are textures and what can we really do with them?
What are textures?
00:16
Textures, as you probably know, are images that will cover the surface of your geometries. Many types of textures can have different effects on the appearance of your geometry. It's not just about the color.
Here are the most common types of textures using a famous door texture by João Paulo. Buy him a Ko-fi or become a Patreon if you like his work.
Color (or albedo)
The albedo texture is the most simple one. It'll only take the pixels of the texture and apply them to the geometry.
Alpha
The alpha texture is a grayscale image where white will be visible, and black won't.
Height
The height texture is a grayscale image that will move the vertices to create some relief. You'll need to add subdivision if you want to see it.
Normal
The normal texture will add small details. It won't move the vertices, but it will lure the light into thinking that the face is oriented differently. Normal textures are very useful to add details with good performance because you don't need to subdivide the geometry.
Ambient occlusion
The ambient occlusion texture is a grayscale image that will fake shadow in the surface's crevices. While it's not physically accurate, it certainly helps to create contrast.
Metalness
The metalness texture is a grayscale image that will specify which part is metallic (white) and non-metallic (black). This information will help to create reflection.
Roughness
The roughness is a grayscale image that comes with metalness, and that will specify which part is rough (white) and which part is smooth (black). This information will help to dissipate the light. A carpet is very rugged, and you won't see the light reflection on it, while the water's surface is very smooth, and you can see the light reflecting on it. Here, the wood is uniform because there is a clear coat on it.
PBR
Those textures (especially the metalness and the roughness) follow what we call PBR principles. PBR stands for Physically Based Rendering. It regroups many techniques that tend to follow real-life directions to get realistic results.
While there are many other techniques, PBR is becoming the standard for realistic renders, and many software, engines, and libraries are using it.
For now, we will simply focus on how to load textures, how to use them, what transformations we can apply, and how to optimize them. We will see more about PBR in later lessons, but if you're curious, you can learn more about it here:
https://marmoset.co/posts/basic-theory-of-physically-based-rendering/
https://marmoset.co/posts/physically-based-rendering-and-you-can-too/
How to load textures
10:45
Getting the URL of the image
To load the texture, we need the URL of the image file.
Because we are using Webpack, there are two ways of getting it.
You can put the image texture in the /src/ folder and import it like you would import a JavaScript dependency:
import imageSource from './image.png'
console.log(imageSource)
JavaScript
Or you can put that image in the /static/ folder and access it just by adding the path of the image (without /static) to the URL:
const imageSource = '/image.png'
console.log(imageSource)
JavaScript
Be careful, this /static/ folder only works because of the Webpack template's configuration. If you are using other types of bundler, you might need to adapt your project.
We will use the /static/ folder technique for the rest of the course.
Loading the image
You can find the door textures we just saw in the /static/ folder, and there are multiple ways of loading them.
Using native JavaScript
With native JavaScript, first, you must create an Image instance, listen to the load event, and then change its src property to start loading the image:
const image = new Image()
image.onload = () =>
{
console.log('image loaded')
}
image.src = '/textures/door/color.jpg'
JavaScript
You should see 'image loaded' appears in the console. As you can see, we set the source to '/textures/door/color.jpg' without the /static folder in the path.
We cannot use that image directly. We need to create a Texture from that image first.
This is because WebGL needs a very specific format that can be access by the GPU and also because some changes will be applied to the textures like the mipmapping but we will see more about that a little later.
Create the texture with the Texture class:
const image = new Image()
image.addEventListener('load', () =>
{
const texture = new THREE.Texture(image)
})
image.src = '/textures/door/color.jpg'
JavaScript
What we need to do now is use that texture in the material. Unfortunately, the texture variable has been declared in a function and we can not access it outside of this function. This is a JavaScript limitation called scope.
We could create the mesh inside the function, but there is a better solution consisting on creating the texture outside of the function and then updating it once the image is loaded by setting the texture needsUpdate property to true:
const image = new Image()
const texture = new THREE.Texture(image)
image.addEventListener('load', () =>
{
texture.needsUpdate = true
})
image.src = '/textures/door/color.jpg'
JavaScript
While doing this, you can immediately use the texture variable immediately, and the image will be transparent until it is loaded.
To see the texture on the cube, replace the color property by map and use the texture as value:
const material = new THREE.MeshBasicMaterial({ map: texture })
JavaScript
You should see the door texture on each side of your cube.
Using TextureLoader
The native JavaScript technique is not that complicated, but there is an even more straightforward way with TextureLoader.
Instantiate a variable using the TextureLoader class and use its .load(...) method to create a texture:
const textureLoader = new THREE.TextureLoader()
const texture = textureLoader.load('/textures/door/color.jpg')
JavaScript
Internally, Three.js will do what it did before to load the image and update the texture once it's ready.
You can load as many textures as you want with only one TextureLoader instance.
You can send 3 functions after the path. They will be called for the following events:
load when the image loaded successfully
progress when the loading is progressing
error if something went wrong
const textureLoader = new THREE.TextureLoader()
const texture = textureLoader.load(
'/textures/door/color.jpg',
() =>
{
console.log('loading finished')
},
() =>
{
console.log('loading progressing')
},
() =>
{
console.log('loading error')
}
)
JavaScript
If the texture doesn't work, it might be useful to add those callback functions to see what is happening and spot errors.
Using the LoadingManager
Finally, if you have multiple images to load and want to mutualize the events like being notified when all the images are loaded, you can use a LoadingManager.
Create an instance of the LoadingManager class and pass it to the TextureLoader:
const loadingManager = new THREE.LoadingManager()
const textureLoader = new THREE.TextureLoader(loadingManager)
JavaScript
You can listen to the various events by replacing the following properties by your own functions onStart, onLoad, onProgress, and onError:
const loadingManager = new THREE.LoadingManager()
loadingManager.onStart = () =>
{
console.log('loading started')
}
loadingManager.onLoad = () =>
{
console.log('loading finished')
}
loadingManager.onProgress = () =>
{
console.log('loading progressing')
}
loadingManager.onError = () =>
{
console.log('loading error')
}
const textureLoader = new THREE.TextureLoader(loadingManager)
JavaScript
You can now start loading all the images you need:
// ...
const colorTexture = textureLoader.load('/textures/door/color.jpg')
const alphaTexture = textureLoader.load('/textures/door/alpha.jpg')
const heightTexture = textureLoader.load('/textures/door/height.jpg')
const normalTexture = textureLoader.load('/textures/door/normal.jpg')
const ambientOcclusionTexture = textureLoader.load('/textures/door/ambientOcclusion.jpg')
const metalnessTexture = textureLoader.load('/textures/door/metalness.jpg')
const roughnessTexture = textureLoader.load('/textures/door/roughness.jpg')
JavaScript
As you can see here, we renamed the texture variable colorTexture so don't forget to change it in the material too:
const material = new THREE.MeshBasicMaterial({ map: colorTexture })
JavaScript
The LoadingManager is very useful if you want to show a loader and hide it only when all the assets are loaded. As we will see in a future lesson, you can also use it with other types of loaders.
UV unwrapping
34:30
While it is quite logical how to place a texture on a cube, things can be a little trickier for other geometries.
Try to replace your BoxGeometry with other geometries:
const geometry = new THREE.BoxGeometry(1, 1, 1)
// Or
const geometry = new THREE.SphereGeometry(1, 32, 32)
// Or
const geometry = new THREE.ConeGeometry(1, 1, 32)
// Or
const geometry = new THREE.TorusGeometry(1, 0.35, 32, 100)
JavaScript
As you can see, the texture is being stretched or squeezed in different ways to cover the geometry.
That is called UV unwrapping. You can imagine that like unwrapping an origami or a candy wrap to make it flat. Each vertex will have a 2D coordinate on a flat (usually square) plane.
You can actually see those UV 2D coordinates in the geometry.attributes.uv property:
console.log(geometry.attributes.uv)
JavaScript
Those UV coordinates are generated by Three.js when you use the primitives. If you create your own geometry and want to apply a texture to it, you'll have to specify the UV coordinates.
If you are making the geometry using a 3D software, you'll also have to do the UV unwrapping.
Don't worry; most 3D software also has auto unwrapping that should do the trick.
Transforming the texture
41:12
Let's get back to our cube with one texture and see what kind of transformations we can apply to that texture.
Repeat
You can repeat the texture using the repeat property, which is a Vector2, meaning that it has x and y properties.
Try to change these properties:
const colorTexture = textureLoader.load('/textures/door/color.jpg')
colorTexture.repeat.x = 2
colorTexture.repeat.y = 3
JavaScript
As you can see, the texture is not repeating, but it is smaller, and the last pixel seems stretched.
That is due to the texture not being set up to repeat itself by default. To change that, you have to update the wrapS and wrapT properties using the THREE.RepeatWrapping constant.
wrapS is for the x axis
wrapT is for the y axis
colorTexture.wrapS = THREE.RepeatWrapping
colorTexture.wrapT = THREE.RepeatWrapping
JavaScript
You can also alternate the direction with THREE.MirroredRepeatWrapping:
colorTexture.wrapS = THREE.MirroredRepeatWrapping
colorTexture.wrapT = THREE.MirroredRepeatWrapping
JavaScript
Offset
You can offset the texture using the offset property that is also a Vector2 with x and y properties. Changing these will simply offset the UV coordinates:
colorTexture.offset.x = 0.5
colorTexture.offset.y = 0.5
JavaScript
Rotation
You can rotate the texture using the rotation property, which is a simple number corresponding to the angle in radians:
colorTexture.rotation = Math.PI * 0.25
JavaScript
If you remove the offset and repeat properties, you'll see that the rotation occurs around the bottom left corner of the cube's faces:
That is, in fact, the 0, 0 UV coordinates. If you want to change the pivot of that rotation, you can do it using the center property which is also a Vector2:
colorTexture.rotation = Math.PI * 0.25
colorTexture.center.x = 0.5
colorTexture.center.y = 0.5
JavaScript
The texture will now rotate on its center.
Filtering and Mipmapping
49:50
If you look at the cube's top face while this face is almost hidden, you'll see a very blurry texture.
That is due to the filtering and the mipmapping.
Mipmapping (or "mip mapping" with a space) is a technique that consists of creating half a smaller version of a texture again and again until you get a 1x1 texture. All those texture variations are sent to the GPU, and the GPU will choose the most appropriate version of the texture.
Three.js and the GPU already handle all of this, and you can just set what filter algorithm to use. There are two types of filter algorithms: the minification filter and the magnification filter.
Minification filter
The minification filter happens when the pixels of texture are smaller than the pixels of the render. In other words, the texture is too big for the surface, it covers.
You can change the minification filter of the texture using the minFilter property.
There are 6 possible values:
THREE.NearestFilter
THREE.LinearFilter
THREE.NearestMipmapNearestFilter
THREE.NearestMipmapLinearFilter
THREE.LinearMipmapNearestFilter
THREE.LinearMipmapLinearFilter
The default is THREE.LinearMipmapLinearFilter. If you are not satisfied with how your texture looks, you should try the other filters.
We won't see each one, but we will test the THREE.NearestFilter, which has a very different result:
colorTexture.minFilter = THREE.NearestFilter
JavaScript
If you're using a device with a pixel ratio above one, you won't see much of a difference. If not, place the camera where this face is almost hidden, and you should get more details and strange artifacts.
If you test with the checkerboard-1024x1024.png texture located in the /static/textures/ folder, you will see those artefacts more clearly:
const colorTexture = textureLoader.load('/textures/checkerboard-1024x1024.png')
JavaScript
The artefacts you see are are called moiré patterns and you usually want to avoid them.
Magnification filter
The magnification filter works just like the minification filter, but when the pixels of the texture are bigger than the render's pixels. In other words, the texture too small for the surface it covers.
You can see the result using the checkerboard-8x8.png texture also located in the static/textures/ folder:
const colorTexture = textureLoader.load('/textures/checkerboard-8x8.png')
JavaScript
The texture gets all blurry because it's a very small texture on a very large surface.
While you might think this looks awful, it is probably for the best. If the effect isn't too exaggerated, the user will probably not even notice it.
You can change the magnification filter of the texture using the magFilter property.
There are only two possible values:
THREE.NearestFilter
THREE.LinearFilter
The default is THREE.LinearFilter.
If you test the THREE.NearestFilter, you'll see that the base image is preserved, and you get a pixelated texture:
colorTexture.magFilter = THREE.NearestFilter
JavaScript
It can be advantageous if you're going for a Minecraft style with pixelated textures.
You can see the result using the minecraft.png texture located in the static/textures/ folder:
const colorTexture = textureLoader.load('/textures/minecraft.png')
JavaScript
One final word about all those filters is that THREE.NearestFilter is cheaper than the other ones, and you should get better performances when using it.
Only use the mipmaps for the minFilter property. If you are using the THREE.NearestFilter, you don't need the mipmaps, and you can deactivate them with colorTexture.generateMipmaps = false:
colorTexture.generateMipmaps = false
colorTexture.minFilter = THREE.NearestFilter
JavaScript
That will slightly offload the GPU.
Texture format and optimisation
01:02:10
When you are preparing your textures, you must keep 3 crucial elements in mind:
The weight
The size (or the resolution)
The data
The weight
Don't forget that the users going to your website will have to download those textures. You can use most of the types of images we use on the web like .jpg (lossy compression but usually lighter) or .png (lossless compression but usually heavier).
Try to apply the usual methods to get an acceptable image but as light as possible. You can use compression websites like TinyPNG (also works with jpg) or any software.
The size
Each pixel of the textures you are using will have to be stored on the GPU regardless of the image's weight. And like your hard drive, the GPU has storage limitations. It's even worse because the automatically generated mipmapping increases the number of pixels that have to be stored.
Try to reduce the size of your images as much as possible.
If you remember what we said about the mipmapping, Three.js will produce a half smaller version of the texture repeatedly until it gets a 1x1 texture. Because of that, your texture width and height must be a power of 2. That is mandatory so that Three.js can divide the size of the texture by 2.
Some examples: 512x512, 1024x1024 or 512x2048
512, 1024 and 2048 can be divided by 2 until it reaches 1.
If you are using a texture with a width or height different than a power of 2 value, Three.js will try to stretch it to the closest power of 2 number, which can have visually poor results, and you'll also get a warning in the console.
The data
We haven't tested it yet, because we have other things to see first, but textures support transparency. As you may know, jpg files don't have an alpha channel, so you might prefer using a png.
Or you can use an alpha map, as we will see in a future lesson.
If you are using a normal texture (the purple one), you will probably want to have the exact values for each pixel's red, green, and blue channels, or you might end up with visual glitches. For that, you'll need to use a png because its lossless compression will preserve the values.