Di 09 Textures kita udah punya “kulit” buat objek. Tapi kulit doang gak cukup, kita juga harus nentuin kulit itu bahannya apa: kayu, besi, kaca, atau kain. Nah, itu urusan material.

Material itu tugasnya ngasih warna ke tiap piksel yang kelihatan dari geometry kita.

Algoritma yang mutusin warna tiap piksel itu ditulis di dalam program yang namanya shader. Nulis shader itu salah satu bagian paling susah di WebGL dan ThreeJS, tapi tenang, ThreeJS udah nyiapin banyak material bawaan yang shader-nya udah jadi. Kita tinggal pakai. Bikin shader sendiri ada babnya sendiri nanti.

Nyiapin Scene

Biar enak nyobain macam-macam material, kita siapin dulu scene yang manis.

1. Bikin 3 Objek dengan 1 Material

Kita bikin 3 mesh dari 3 geometry yang beda: bola, bidang datar, dan donat. Terus ketiganya pakai material yang sama persis. Iya, satu material boleh dipakai banyak mesh sekaligus, gak perlu bikin satu-satu.

Bola kita geser ke kiri dan donat ke kanan biar gak numpuk, pakai position.x kaya di 03 Transform Object > `position` Memindahkan Object. Method add(...) juga bisa nerima banyak objek sekaligus, jadi gak perlu dipanggil tiga kali:

/**
 * Objects
 */
// MeshBasicMaterial
const material = new THREE.MeshBasicMaterial()
 
const sphere = new THREE.Mesh(
    new THREE.SphereGeometry(0.5, 16, 16),
    material
)
sphere.position.x = - 1.5
 
const plane = new THREE.Mesh(
    new THREE.PlaneGeometry(1, 1),
    material
)
 
const torus = new THREE.Mesh(
    new THREE.TorusGeometry(0.3, 0.2, 16, 32),
    material
)
torus.position.x = 1.5
 
scene.add(sphere, plane, torus)

2. Muterin Objeknya

Biar kita bisa lihat materialnya dari segala sisi, kita puter pelan-pelan di fungsi tick pakai Clock:

/**
 * Animate
 */
const clock = new THREE.Clock()
 
const tick = () =>
{
    const elapsedTime = clock.getElapsedTime()
 
    // Update objects
    sphere.rotation.y = 0.1 * elapsedTime
    plane.rotation.y = 0.1 * elapsedTime
    torus.rotation.y = 0.1 * elapsedTime
 
    sphere.rotation.x = - 0.15 * elapsedTime
    plane.rotation.x = - 0.15 * elapsedTime
    torus.rotation.x = - 0.15 * elapsedTime
 
    // ...
}
 
tick()

rotation.x-nya sengaja dikasih minus biar muternya ke arah sebaliknya, jadi bidang datarnya kelihatan lebih lama sebelum menghilang (soalnya kalau dilihat dari samping persis, bidang datar itu tipis banget sampai hilang).

3. Load Semua Texture

Material-material yang mau kita coba itu pakai texture dengan cara yang macam-macam. Jadi kita load dulu semuanya pakai TextureLoader.

Semua gambarnya ada di folder static/textures/. Kita ambil semua texture pintu dari static/textures/door/, satu matcap dari static/textures/matcaps/, dan satu gradient dari static/textures/gradients/.

Ini harus ditulis sebelum bikin materialnya, kalau nggak texture-nya belum ada waktu mau dipakai:

/**
 * Textures
 */
const textureLoader = new THREE.TextureLoader()
 
const doorColorTexture = textureLoader.load('./textures/door/color.jpg')
const doorAlphaTexture = textureLoader.load('./textures/door/alpha.jpg')
const doorAmbientOcclusionTexture = textureLoader.load('./textures/door/ambientOcclusion.jpg')
const doorHeightTexture = textureLoader.load('./textures/door/height.jpg')
const doorNormalTexture = textureLoader.load('./textures/door/normal.jpg')
const doorMetalnessTexture = textureLoader.load('./textures/door/metalness.jpg')
const doorRoughnessTexture = textureLoader.load('./textures/door/roughness.jpg')
const matcapTexture = textureLoader.load('./textures/matcaps/1.png')
const gradientTexture = textureLoader.load('./textures/gradients/3.jpg')

Ingat aturan di bab Textures: cuma texture yang dipakai sebagai map dan matcap yang perlu di-set colorSpace-nya ke sRGB, karena cuma dua itu yang isinya beneran warna:

doorColorTexture.colorSpace = THREE.SRGBColorSpace
matcapTexture.colorSpace = THREE.SRGBColorSpace

Buat mastiin semuanya kebaca, coba tempel satu dulu ke materialnya:

const material = new THREE.MeshBasicMaterial({ map: doorColorTexture })

Selama ini kita cuma kenal MeshBasicMaterial. Padahal kalau kamu cari kata “material” di dokumentasi ThreeJS, class-nya ada banyak banget. Ayo kita cicipin satu-satu.

MeshBasicMaterial

MeshBasicMaterial itu ya material paling “dasar”… tapi ternyata propertinya masih ada beberapa yang belum kita sentuh.

Sebelum lanjut, ada dua gaya nulis yang hasilnya sama persis. Bisa dikirim sebagai objek waktu bikin materialnya, bisa juga di-set satu-satu setelah materialnya jadi:

const material = new THREE.MeshBasicMaterial({
    map: doorColorTexture
})
 
// Sama aja
const material = new THREE.MeshBasicMaterial()
material.map = doorColorTexture

Seterusnya kita pakai cara kedua biar gampang dikomen-komen waktu nyoba-nyoba, tapi terserah kamu sih.

1. Map

map nempelin texture ke permukaan geometry:

material.map = doorColorTexture

2. Color

color ngasih warna rata ke seluruh permukaan. Kalau kamu nge-set color secara langsung, isinya wajib instance dari class Color, gak bisa string doang. Formatnya sendiri bebas:

// material.map = doorColorTexture
material.color = new THREE.Color('#ff0000')
material.color = new THREE.Color('#f00')
material.color = new THREE.Color('red')
material.color = new THREE.Color('rgb(255, 0, 0)')
material.color = new THREE.Color(0xff0000)

❌ salah, color yang di-set langsung gak nerima string mentah

const material = new THREE.MeshBasicMaterial()
material.color = '#ff0000'

✅ benar, dibungkus Color dulu

const material = new THREE.MeshBasicMaterial()
material.color = new THREE.Color('#ff0000')

Kalau color dan map dipakai barengan, hasilnya texture-nya diwarnai sama warna itu, kaya dikasih filter:

material.map = doorColorTexture
material.color = new THREE.Color('#ff0000')

3. Wireframe

wireframe yang udah kita kenal di 07 Geometry > 2. Wireframe itu nampilin segitiga penyusun geometry pakai garis setebal 1 piksel, berapapun jarak kameranya:

material.wireframe = true

Perhatiin bidang datarnya: cuma disusun 2 segitiga. Ini bakal jadi masalah nanti di bagian displacementMap.

4. Opacity

opacity ngatur tingkat transparansi. Tapi dia gak bakal jalan kalau transparent belum di-set true, karena kita harus ngasih tau ThreeJS dulu kalau material ini emang mau main transparan:

❌ salah, gak ada efeknya sama sekali

material.opacity = 0.5

✅ benar, kasih tau dulu kalau materialnya transparan

material.transparent = true
material.opacity = 0.5

Hasilnya kelihatan abu-abu, tapi itu bukan karena warnanya berubah, itu latar hitamnya yang nembus dari balik material putih.

5. AlphaMap

Karena transparansinya udah nyala, sekarang kita bisa ngatur transparansi pakai gambar lewat alphaMap, persis kaya jenis texture alpha yang kita bahas di 09 Textures > Apa itu Texture?:

material.transparent = true
material.alphaMap = doorAlphaTexture

6. Side

side nentuin sisi mana dari sebuah face yang kelihatan. Bawaannya cuma sisi depan (THREE.FrontSide), tapi bisa diganti jadi sisi belakang (THREE.BackSide) atau dua-duanya (THREE.DoubleSide):

material.side = THREE.DoubleSide

Sekarang bidang datarnya kelihatan dari depan maupun belakang.

Tapi hindari THREE.DoubleSide kalau gak kepaksa, karena dia beneran makan lebih banyak sumber daya waktu render, bahkan buat sisi yang sebenarnya gak kelihatan.

Beberapa properti kaya wireframe dan opacity ini juga bisa dipakai di hampir semua material di bawah, jadi gak kita ulang-ulang lagi ya.

MeshNormalMaterial

MeshNormalMaterial nampilin warna ungu kebiruan yang cantik, mirip banget sama normal texture yang kita lihat di 09 Textures > Apa itu Texture?. Itu bukan kebetulan, dua-duanya emang berhubungan sama yang namanya normals.

Kita namain variabelnya material juga biar langsung ganti di semua objek, terus material sebelumnya kita komen aja siapa tau mau balik lagi:

// // MeshBasicMaterial
// const material = new THREE.MeshBasicMaterial()
// material.map = doorColorTexture
 
// MeshNormalMaterial
const material = new THREE.MeshNormalMaterial()

Normal itu informasi yang tersimpan di tiap vertex tentang arah mana yang “luar” dari sebuah face. Kalau normal itu digambar sebagai panah, kamu bakal lihat garis-garis lurus nyembul keluar dari tiap vertex. Ini informasi yang sama yang kita simpan waktu bikin BufferGeometry sendiri, cuma waktu itu kita cuma nulis position-nya aja.

Normal ini kepakai buat banyak hal: ngitung gimana cahaya harus jatuh ke sebuah face, atau gimana lingkungan sekitar harus mantul dan membias di permukaannya.

Di MeshNormalMaterial, warnanya cuma nunjukkin arah normal relatif terhadap kamera. Makanya kalau kamu muter-muterin bolanya, warnanya selalu sama aja di posisi layar yang sama.

Selain properti yang udah kita kenal (wireframe, transparent, opacity, side), ada properti baru namanya flatShading:

material.flatShading = true

flatShading bikin permukaannya jadi bidang-bidang datar yang tegas, karena normalnya gak dirata-ratakan lagi antar vertex. Jadi kelihatan kaya berlian yang dipotong-potong.

MeshNormalMaterial ini berguna banget buat ngedebug arah normal, tapi tampilannya juga bagus banget dipakai apa adanya, kaya yang dilakuin ilithya di portfolio-nya.

MeshMatcapMaterial

MeshMatcapMaterial ini material favorit banyak orang, karena hasilnya bagus banget tapi performanya enteng banget.

Cara kerjanya: dia butuh satu gambar referensi yang bentuknya kaya bola yang udah kena cahaya. Terus material ini bakal ngambil warna dari gambar itu sesuai arah normal relatif terhadap kamera.

Komen material sebelumnya, bikin MeshMatcapMaterial, terus isi properti matcap-nya:

// // MeshNormalMaterial
// const material = new THREE.MeshNormalMaterial()
// material.flatShading = true
 
// MeshMatcapMaterial
const material = new THREE.MeshMatcapMaterial()
material.matcap = matcapTexture

Objeknya kelihatan kena cahaya, padahal di scene kita gak ada lampu satupun. Itu semua ilusi dari gambarnya.

Kelemahannya juga di situ: hasilnya selalu sama dari sudut manapun kamera menghadap, dan kamu gak bisa ngutak-atik cahayanya karena cahayanya emang gak ada.

Coba-coba texture lain yang ada di folder /static/textures/matcaps/ (pilih salah satu):

const matcapTexture = textureLoader.load('/textures/matcaps/2.png')
const matcapTexture = textureLoader.load('/textures/matcaps/3.png')
const matcapTexture = textureLoader.load('/textures/matcaps/4.png')
const matcapTexture = textureLoader.load('/textures/matcaps/5.png')
const matcapTexture = textureLoader.load('/textures/matcaps/6.png')
const matcapTexture = textureLoader.load('/textures/matcaps/7.png')
const matcapTexture = textureLoader.load('/textures/matcaps/8.png')

Mau cari matcap lain? Ada kumpulan matcap yang gede banget di sini. Tapi lisensinya gak semua diverifikasi, jadi hati-hati, mungkin cuma boleh buat project pribadi.

Kamu juga bisa bikin sendiri di software 3D dengan cara me-render bola di depan kamera dalam gambar persegi, atau digambar manual di Photoshop, atau pakai Matcap Studio bikinan Kevin Chapelier.

MeshDepthMaterial

MeshDepthMaterial itu simpel: objek yang dekat sama nilai near kamera diwarnain putih, yang dekat sama nilai far diwarnain hitam. Ingat nilai near dan far yang kita bahas di 05 Camera > 3. Jarak Terdekat dan Terjauh? Nah, itu yang dipakai:

// // MeshMatcapMaterial
// const material = new THREE.MeshMatcapMaterial()
// material.matcap = matcapTexture
 
// MeshDepthMaterial
const material = new THREE.MeshDepthMaterial()

Aslinya sih lebih rumit dari itu. Material ini sebenarnya dipakai buat nyimpen informasi kedalaman ke dalam sebuah texture, yang nanti kepakai buat perhitungan rumit kaya bikin bayangan. Ada babnya sendiri nanti.

MeshLambertMaterial

Lanjut ke MeshLambertMaterial:

// // MeshDepthMaterial
// const material = new THREE.MeshDepthMaterial()
 
// MeshLambertMaterial
const material = new THREE.MeshLambertMaterial()

Lho, layarnya hitam total padahal di Console gak ada error sama sekali?

Tenang, gak ada yang salah. MeshLambertMaterial itu material pertama di daftar kita yang butuh cahaya biar bisa kelihatan. Semua material sebelumnya cuek sama cahaya, yang ini nggak. Gak ada lampu, ya gak ada yang kelihatan, sama kaya kamar gelap.

Nambah Cahaya

Kita tambahin dua lampu sederhana. Gak usah dalem-dalem dulu, cahaya ada babnya sendiri nanti.

Pertama AmbientLight, yaitu cahaya yang nyebar rata ke segala arah kaya cahaya siang yang mantul ke mana-mana:

/**
 * Lights
 */
const ambientLight = new THREE.AmbientLight(0xffffff, 1)
scene.add(ambientLight)

Kedua PointLight, yaitu cahaya dari satu titik ke segala arah kaya bohlam:

const pointLight = new THREE.PointLight(0xffffff, 30)
pointLight.position.x = 2
pointLight.position.y = 3
pointLight.position.z = 4
scene.add(pointLight)

Sekarang mulai kelihatan realistis. Belum meyakinkan sih, tapi lumayan buat permulaan.

MeshLambertMaterial dukung semua properti MeshBasicMaterial plus beberapa properti soal cahaya. Dia juga material paling irit di antara semua material yang pakai cahaya. Sayangnya parameternya kurang enak dipakai, dan kalau kamu perhatiin bola-nya dari dekat, bakal kelihatan pola-pola aneh di permukaannya.

MeshPhongMaterial

MeshPhongMaterial itu mirip banget sama MeshLambertMaterial, bedanya pola anehnya jauh berkurang dan pantulan cahayanya kelihatan di permukaan:

// // MeshLambertMaterial
// const material = new THREE.MeshLambertMaterial()
 
// MeshPhongMaterial
const material = new THREE.MeshPhongMaterial()

MeshPhongMaterial lebih berat dari MeshLambertMaterial, tapi di tahap belajar begini sih belum kerasa.

Pantulan cahayanya bisa diatur pakai shininess, makin gede makin mengkilap. Warna pantulannya juga bisa diganti pakai specular:

material.shininess = 100
material.specular = new THREE.Color(0x1188ff)

MeshToonMaterial

MeshToonMaterial propertinya mirip MeshLambertMaterial, tapi tampilannya gaya kartun:

// // MeshPhongMaterial
// const material = new THREE.MeshPhongMaterial()
// material.shininess = 100
// material.specular = new THREE.Color(0x1188ff)
 
// MeshToonMaterial
const material = new THREE.MeshToonMaterial()

Bawaannya cuma dua tingkat warna: satu buat bagian gelap, satu buat bagian terang. Buat nambah tingkatannya, kita pakai gradientTexture yang tadi udah kita load ke properti gradientMap:

material.gradientMap = gradientTexture

Lho, efek kartunnya malah ilang jadi gradasi mulus?

Itu karena gambar gradasinya itu ukurannya cuma 3 x 1 piksel, kecil banget. Waktu GPU ngambil piksel dari gambar sekecil itu buat permukaan segede bola, dia ngeblend piksel-pikselnya biar mulus, dan hilanglah batas tegas antar tingkatannya.

Untungnya kita udah tau cara ngendaliin ini dari bab Textures, yaitu pakai minFilter dan magFilter:

❌ salah, GPU ngeblend pikselnya dan efek kartunnya hilang

const material = new THREE.MeshToonMaterial()
material.gradientMap = gradientTexture

✅ benar, paksa GPU ngambil piksel apa adanya

const material = new THREE.MeshToonMaterial()
gradientTexture.minFilter = THREE.NearestFilter
gradientTexture.magFilter = THREE.NearestFilter
material.gradientMap = gradientTexture

Sekarang efek kartunnya balik lagi, plus satu tingkat perantara.

Dan karena THREE.NearestFilter emang gak pakai mipmap sama sekali, kita matiin aja pembuatannya biar hemat memori, persis kaya yang kita lakuin di 09 Textures > Filtering dan Mipmapping:

gradientTexture.minFilter = THREE.NearestFilter
gradientTexture.magFilter = THREE.NearestFilter
gradientTexture.generateMipmaps = false

Mau tingkatannya lebih banyak? Pakai gambar yang di static/textures/gradients/5.jpg:

const gradientTexture = textureLoader.load('/textures/gradients/5.jpg')

MeshStandardMaterial

MeshStandardMaterial ini pakai prinsip PBR yang udah kita bahas di bab Textures. Sama kaya Lambert dan Phong, dia butuh cahaya, tapi algoritmanya jauh lebih realistis dan parameternya lebih masuk akal kaya roughness dan metalness.

Namanya “standard” karena PBR emang udah jadi standar di banyak software, engine, dan library. Idenya: parameter yang realistis menghasilkan hasil yang realistis, dan hasilnya mirip di teknologi manapun kamu pakai:

// // MeshToonMaterial
// const material = new THREE.MeshToonMaterial()
// material.gradientMap = gradientTexture
 
// MeshStandardMaterial
const material = new THREE.MeshStandardMaterial()
material.metalness = 0.45
material.roughness = 0.65

1. Pasang Debug UI

Ini saat yang pas banget buat masang Debug UI, soalnya properti yang mau dicoba banyak banget dan males kalau harus ngedit kode terus.

lil-gui udah kita install dari bab pertama, jadi tinggal di-import dan dibikin instance-nya:

import GUI from 'lil-gui'
 
/**
 * Debug
 */
const gui = new GUI()

Terus tambahin tweak-nya setelah materialnya dibikin (ingat aturan di 08 Debug UI > 1. Range, objeknya harus udah ada duluan):

const material = new THREE.MeshStandardMaterial()
material.metalness = 0.45
material.roughness = 0.65
 
gui.add(material, 'metalness').min(0).max(1).step(0.0001)
gui.add(material, 'roughness').min(0).max(1).step(0.0001)

Sekarang tinggal geser-geser sampai ketemu yang sreg.

2. Nambah Environment Map

Sebelum lanjut ke properti lain, kita tambahin dulu satu fitur namanya environment map.

Environment map itu gambar dari apa yang ada di sekeliling scene kita. File-nya ada di /static/textures/environmentMap/2k.hdr. Kalau OS kamu gak bisa nampilin preview-nya ya gak apa-apa, ThreeJS tetap bisa baca.

Gunanya buat nambahin pantulan, pembiasan, sekaligus pencahayaan ke objek kita. Ada babnya sendiri nanti, termasuk cara bikin sendiri.

Pertama kita bikin materialnya lebih mengkilap dulu biar efeknya kerasa:

const material = new THREE.MeshStandardMaterial()
material.metalness = 0.7
material.roughness = 0.2

File .hdr gak bisa dibaca TextureLoader biasa, kita butuh RGBELoader:

import { RGBELoader } from 'three/examples/jsm/loaders/RGBELoader.js'

Bikin instance-nya, terus panggil method load()-nya:

/**
 * Environment map
 */
const rgbeLoader = new RGBELoader()
rgbeLoader.load('./textures/environmentMap/2k.hdr', (environmentMap) =>
{
    console.log(environmentMap)
})

Bedanya sama textureLoader: di sini fungsi callback-nya wajib, dan hasil muatannya kita terima lewat parameter fungsi itu.

Biar kepasang ke scene, kita ubah properti mapping-nya jadi THREE.EquirectangularReflectionMapping (istilah buat gambar bola yang dibentangin jadi persegi panjang, mirip peta dunia), terus tempel ke background dan environment milik scene:

rgbeLoader.load('./textures/environmentMap/2k.hdr', (environmentMap) =>
{
    environmentMap.mapping = THREE.EquirectangularReflectionMapping
 
    scene.background = environmentMap
    scene.environment = environmentMap
})

Sekarang lingkungannya mantul di permukaan objek. Coba geser-geser metalness dan roughness di Debug UI, hasilnya beda-beda semua.

Walaupun baru kita pasang sekarang, environment map ini juga jalan di MeshLambertMaterial dan MeshPhongMaterial.

Dan karena environment map aja udah cukup terang, dua lampu tadi kita komen aja:

// const ambientLight = new THREE.AmbientLight(0xffffff, 1)
// scene.add(ambientLight)
 
// const pointLight = new THREE.PointLight(0xffffff, 30)
// pointLight.position.x = 2
// pointLight.position.y = 3
// pointLight.position.z = 4
// scene.add(pointLight)

3. Properti Lainnya

Sekarang kita pasang satu-satu texture pintu yang tadi udah di-load. Ini bagian paling seru, karena semua jenis texture di 09 Textures > Apa itu Texture? akhirnya kepakai beneran.

map buat texture warnanya:

material.map = doorColorTexture

aoMap (singkatan dari ambient occlusion map) bikin bagian yang gelap di texture-nya jadi lebih gelap lagi, kaya bayangan palsu di celah-celah. Kekuatannya diatur pakai aoMapIntensity:

material.aoMap = doorAmbientOcclusionTexture
material.aoMapIntensity = 1

Celah-celahnya jadi lebih gelap, kontrasnya naik, objeknya kerasa lebih berdimensi. Catatan penting: aoMap cuma ngaruh ke cahaya dari AmbientLight, environment map, dan HemisphereLight, gak ngaruh ke cahaya lain.

displacementMap menggeser vertices beneran buat bikin permukaan yang timbul:

material.displacementMap = doorHeightTexture

Hasilnya jelek banget kan? Ada dua sebabnya: vertices-nya kurang banyak dan geserannya kekencengan.

Ingat penjelasan di 07 Geometry > 3. Kenapa Segments Itu Penting: kalau bidang datarnya cuma 2 segitiga, ya cuma 4 titik yang bisa digeser. Jadi kita naikin subdivisionnya:

❌ salah, geometry-nya kurang vertices, displacementMap gak ada gunanya

new THREE.SphereGeometry(0.5, 16, 16)
new THREE.PlaneGeometry(1, 1)
new THREE.TorusGeometry(0.3, 0.2, 16, 32)

✅ benar, vertices-nya cukup banyak buat digeser

new THREE.SphereGeometry(0.5, 64, 64)
new THREE.PlaneGeometry(1, 1, 100, 100)
new THREE.TorusGeometry(0.3, 0.2, 64, 128)

Sekarang timbulnya rapi, tapi masih kekencengan. Kita kecilin pakai displacementScale:

material.displacementScale = 0.1

Daripada nentuin metalness dan roughness rata buat seluruh permukaan, kita bisa nentuin per bagian pakai gambar:

material.metalnessMap = doorMetalnessTexture
material.roughnessMap = doorRoughnessTexture

Tapi pantulannya kok aneh? Itu karena nilai metalness dan roughness yang lama masih ikut ngaruh dan ngalikin nilai dari gambarnya. Biar gambarnya yang sepenuhnya nentuin, dua-duanya harus di-set 1:

❌ salah, nilai lama masih ngaduk-ngaduk hasil dari gambarnya

material.metalness = 0.45
material.roughness = 0.65
material.metalnessMap = doorMetalnessTexture
material.roughnessMap = doorRoughnessTexture

✅ benar, serahin sepenuhnya ke gambarnya

material.metalness = 1
material.roughness = 1
material.metalnessMap = doorMetalnessTexture
material.roughnessMap = doorRoughnessTexture

Sekarang bagian besi pintunya mantul beneran. Bagian kayunya juga masih agak mantul, itu emang disengaja sama pembuat texture-nya biar kelihatan kaya kayu berpernis.

normalMap nipu arah normal buat nambahin detail kecil tanpa peduli berapa subdivisionnya:

material.normalMap = doorNormalTexture

Kekuatannya diatur pakai normalScale. Hati-hati, ini Vector2, bukan angka biasa, jadi cara ngisinya beda:

❌ salah, normalScale bukan angka

material.normalScale = 0.5

✅ benar, pakai method set() karena isinya Vector2

material.normalScale.set(0.5, 0.5)

Terakhir alphaMap buat transparansinya, dan jangan lupa transparent = true kaya di 4. Opacity:

material.transparent = true
material.alphaMap = doorAlphaTexture

MeshPhysicalMaterial

MeshPhysicalMaterial itu sama persis kaya MeshStandardMaterial, cuma ditambahin efek-efek ekstra: clearcoat, sheen, iridescence, dan transmission.

Cara pakainya gampang, tinggal salin semua yang tadi, ganti class-nya, terus komen yang lama:

/**
 * MeshPhysicalMaterial
 */
// Base material
const material = new THREE.MeshPhysicalMaterial()
material.metalness = 1
material.roughness = 1
material.map = doorColorTexture
material.aoMap = doorAmbientOcclusionTexture
material.aoMapIntensity = 1
material.displacementMap = doorHeightTexture
material.displacementScale = 0.1
material.metalnessMap = doorMetalnessTexture
material.roughnessMap = doorRoughnessTexture
material.normalMap = doorNormalTexture
material.normalScale.set(0.5, 0.5)
 
gui.add(material, 'metalness').min(0).max(1).step(0.0001)
gui.add(material, 'roughness').min(0).max(1).step(0.0001)

Semua properti sebelumnya tetap jalan, karena MeshPhysicalMaterial itu turunan dari MeshStandardMaterial. Sekarang kita cobain fitur barunya satu-satu.

1. Clearcoat

clearcoat itu lapisan pernis tipis di atas material aslinya. Lapisan ini punya sifat pantulannya sendiri, sementara material di bawahnya tetap kelihatan:

// Clearcoat
material.clearcoat = 1
material.clearcoatRoughness = 0
 
gui.add(material, 'clearcoat').min(0).max(1).step(0.0001)
gui.add(material, 'clearcoatRoughness').min(0).max(1).step(0.0001)

Rasanya kaya ada kaca nempel di atas materialnya.

2. Sheen

sheen itu efek yang muncul kalau permukaannya dilihat dari sudut yang sempit, kaya kain beludru yang pinggirannya kelihatan berkilau:

// Sheen
material.sheen = 1
material.sheenRoughness = 0.25
material.sheenColor.set(1, 1, 1)
 
gui.add(material, 'sheen').min(0).max(1).step(0.0001)
gui.add(material, 'sheenRoughness').min(0).max(1).step(0.0001)
gui.addColor(material, 'sheenColor')

Efek ini paling cocok buat material kaya kain, tapi dipakai di material lain juga bikin permukaannya kerasa lebih lembut. Jangan lupa komen bagian clearcoat-nya dulu.

3. Iridescence

iridescence itu efek warna-warni kaya tumpahan bensin di aspal, gelembung sabun, atau permukaan CD:

// Iridescence
material.iridescence = 1
material.iridescenceIOR = 1
material.iridescenceThicknessRange = [ 100, 800 ]
 
gui.add(material, 'iridescence').min(0).max(1).step(0.0001)
gui.add(material, 'iridescenceIOR').min(1).max(2.333).step(0.0001)
gui.add(material.iridescenceThicknessRange, '0').min(1).max(1000).step(1)
gui.add(material.iridescenceThicknessRange, '1').min(1).max(1000).step(1)

Perhatiin dua baris terakhir: iridescenceThicknessRange itu array, jadi tweak-nya nunjuk ke '0' dan '1' sebagai nama propertinya. Ini trik yang sama kaya objek wadah di bab Debug UI, cuma wadahnya kebetulan berupa array.

4. Transmission

transmission bikin cahaya bisa menembus material. Ini beda sama sekadar transparansi pakai opacity, karena gambar di belakang objeknya ikut terdistorsi, persis kaya lihat lewat kaca tebal:

// Transmission
material.transmission = 1
material.ior = 1.5
material.thickness = 0.5
 
gui.add(material, 'transmission').min(0).max(1).step(0.0001)
gui.add(material, 'ior').min(1).max(10).step(0.0001)
gui.add(material, 'thickness').min(0).max(1).step(0.0001)

ior itu singkatan dari Index Of Refraction alias indeks bias, dan nilainya tergantung bahan apa yang mau kamu tiru. Berlian itu 2.417, air 1.333, dan udara 1.000293. Daftar lengkapnya ada di Wikipedia.

thickness itu nilai tetap, jadi ketebalan asli objeknya gak diitung sama sekali.

Sekarang texture-texture pintunya malah bikin berantakan, padahal transmission itu bagus banget kalau materialnya polos. Jadi kita komen semua map-nya dan set metalness sama roughness ke 0:

const material = new THREE.MeshPhysicalMaterial()
material.metalness = 0
material.roughness = 0
// material.map = doorColorTexture
// material.aoMap = doorAmbientOcclusionTexture
// material.aoMapIntensity = 1
// material.displacementMap = doorHeightTexture
// material.displacementScale = 0.1
// material.metalnessMap = doorMetalnessTexture
// material.roughnessMap = doorRoughnessTexture
// material.normalMap = doorNormalTexture
// material.normalScale.set(0.5, 0.5)
// material.transparent = true
// material.alphaMap = doorAlphaTexture

Jadi kaca beneran. Serunya, roughness-nya masih bisa digeser buat bikin efek kaca buram.

Satu peringatan penting: MeshPhysicalMaterial itu material paling berat dari semuanya. Jangan berharap frame rate-nya enak kalau kamu pasang ini ke banyak objek yang nutupin sebagian besar layar, apalagi di HP.

Material untuk Nanti

Ada beberapa material yang belum kita coba karena butuh ilmu tambahan dulu:

  1. PointsMaterial, dipakai buat ngurus partikel: ukurannya, warnanya, gambar di dalamnya, dan lain-lain.
  2. ShaderMaterial dan RawShaderMaterial, dipakai buat bikin material sendiri dari nol pakai bahasa khusus namanya GLSL. Ini topik yang gede banget sampai dikasih satu bab sendiri.

Code Lengkap

import * as THREE from 'three'
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'
import { RGBELoader } from 'three/examples/jsm/loaders/RGBELoader.js'
import GUI from 'lil-gui'
 
/**
 * Debug
 */
const gui = new GUI()
 
/**
 * Base
 */
// Canvas
const canvas = document.querySelector('canvas.webgl')
 
// Scene
const scene = new THREE.Scene()
 
// Sizes
const sizes = {
    width: window.innerWidth,
    height: window.innerHeight
}
 
/**
 * Textures
 */
const textureLoader = new THREE.TextureLoader()
 
const doorColorTexture = textureLoader.load('./textures/door/color.jpg')
const doorAlphaTexture = textureLoader.load('./textures/door/alpha.jpg')
const doorAmbientOcclusionTexture = textureLoader.load('./textures/door/ambientOcclusion.jpg')
const doorHeightTexture = textureLoader.load('./textures/door/height.jpg')
const doorNormalTexture = textureLoader.load('./textures/door/normal.jpg')
const doorMetalnessTexture = textureLoader.load('./textures/door/metalness.jpg')
const doorRoughnessTexture = textureLoader.load('./textures/door/roughness.jpg')
const matcapTexture = textureLoader.load('./textures/matcaps/1.png')
const gradientTexture = textureLoader.load('./textures/gradients/3.jpg')
 
doorColorTexture.colorSpace = THREE.SRGBColorSpace
matcapTexture.colorSpace = THREE.SRGBColorSpace
 
/**
 * Environment map
 */
const rgbeLoader = new RGBELoader()
rgbeLoader.load('./textures/environmentMap/2k.hdr', (environmentMap) =>
{
    environmentMap.mapping = THREE.EquirectangularReflectionMapping
 
    scene.background = environmentMap
    scene.environment = environmentMap
})
 
/**
 * Objects
 */
const material = new THREE.MeshPhysicalMaterial()
material.metalness = 0
material.roughness = 0
 
// Transmission
material.transmission = 1
material.ior = 1.5
material.thickness = 0.5
 
gui.add(material, 'metalness').min(0).max(1).step(0.0001)
gui.add(material, 'roughness').min(0).max(1).step(0.0001)
gui.add(material, 'transmission').min(0).max(1).step(0.0001)
gui.add(material, 'ior').min(1).max(10).step(0.0001)
gui.add(material, 'thickness').min(0).max(1).step(0.0001)
 
const sphere = new THREE.Mesh(
    new THREE.SphereGeometry(0.5, 64, 64),
    material
)
sphere.position.x = - 1.5
 
const plane = new THREE.Mesh(
    new THREE.PlaneGeometry(1, 1, 100, 100),
    material
)
 
const torus = new THREE.Mesh(
    new THREE.TorusGeometry(0.3, 0.2, 64, 128),
    material
)
torus.position.x = 1.5
 
scene.add(sphere, plane, torus)
 
/**
 * Camera
 */
const camera = new THREE.PerspectiveCamera(75, sizes.width / sizes.height, 0.1, 100)
camera.position.z = 3
scene.add(camera)
 
// Controls
const controls = new OrbitControls(camera, canvas)
controls.enableDamping = true
 
/**
 * Renderer
 */
const renderer = new THREE.WebGLRenderer({
    canvas: canvas
})
renderer.setSize(sizes.width, sizes.height)
 
/**
 * Animate
 */
const clock = new THREE.Clock()
 
const tick = () =>
{
    const elapsedTime = clock.getElapsedTime()
 
    // Update objects
    sphere.rotation.y = 0.1 * elapsedTime
    plane.rotation.y = 0.1 * elapsedTime
    torus.rotation.y = 0.1 * elapsedTime
 
    sphere.rotation.x = - 0.15 * elapsedTime
    plane.rotation.x = - 0.15 * elapsedTime
    torus.rotation.x = - 0.15 * elapsedTime
 
    // Update controls
    controls.update()
 
    // Render
    renderer.render(scene, camera)
 
    // Call tick again on the next frame
    window.requestAnimationFrame(tick)
}
 
tick()

Ringkasan

  1. Material itu yang nentuin warna tiap piksel geometry, dan algoritmanya ditulis di program bernama shader. ThreeJS udah nyiapin banyak material bawaan biar kita gak perlu nulis shader dulu.
  2. Satu material bisa dipakai banyak mesh sekaligus, dan scene.add(...) bisa nerima banyak objek dalam satu panggilan.
  3. Properti material bisa di-set lewat objek waktu bikin, atau satu-satu setelah jadi. Hasilnya sama.
  4. color yang di-set langsung wajib dibungkus new THREE.Color(...), gak bisa string mentah. Kalau digabung map, hasilnya texture yang diwarnai.
  5. opacity gak jalan sebelum transparent = true, begitu juga alphaMap.
  6. side nentuin sisi mana yang kelihatan. Hindari THREE.DoubleSide kalau gak kepaksa karena lebih berat.
  7. Normal itu informasi arah “luar” dari tiap vertex, dipakai buat ngitung cahaya dan pantulan. MeshNormalMaterial nampilin arah itu sebagai warna, dan flatShading bikin permukaannya jadi bidang-bidang tegas.
  8. MeshMatcapMaterial bikin objek kelihatan kena cahaya padahal gak ada lampu sama sekali, semuanya ilusi dari satu gambar bola. Enteng banget, tapi hasilnya gak berubah dari sudut manapun.
  9. MeshDepthMaterial mewarnai objek berdasarkan jarak ke nilai near dan far kamera, dan aslinya dipakai buat nyimpen data kedalaman buat bayangan.
  10. MeshLambertMaterial itu material pertama yang butuh cahaya. Layar hitam tanpa error itu tandanya lupa masang lampu, bukan tandanya kodenya salah.
  11. Urutan dari paling irit ke paling berat: MeshBasicMaterial dan MeshMatcapMaterial (gak butuh cahaya), MeshLambertMaterial, MeshPhongMaterial, MeshStandardMaterial, terus MeshPhysicalMaterial yang paling berat.
  12. MeshToonMaterial butuh minFilter dan magFilter di-set THREE.NearestFilter, kalau nggak gradasi 3x1 pikselnya bakal diblend GPU dan efek kartunnya ilang.
  13. MeshStandardMaterial pakai prinsip PBR dengan parameter realistis kaya metalness dan roughness, dan hasilnya mirip di teknologi manapun.
  14. File .hdr butuh RGBELoader, bukan TextureLoader, dan hasilnya dipasang ke scene.background dan scene.environment setelah mapping-nya diubah jadi THREE.EquirectangularReflectionMapping.
  15. Environment map itu sekaligus jadi sumber cahaya, jadi lampunya bisa dimatiin.
  16. displacementMap butuh subdivision yang banyak biar ada vertex yang bisa digeser, dan kekuatannya dikecilin pakai displacementScale.
  17. metalnessMap dan roughnessMap cuma bener kalau metalness dan roughness di-set 1, karena nilai itu masih ikut ngalikin hasil dari gambarnya.
  18. normalScale itu Vector2, jadi ngisinya pakai .set(x, y), bukan angka biasa.
  19. MeshPhysicalMaterial itu turunan MeshStandardMaterial plus 4 efek tambahan: clearcoat (lapisan pernis), sheen (kilau kain dari sudut sempit), iridescence (warna-warni gelembung sabun), dan transmission (tembus pandang yang mendistorsi latar).
  20. ior itu indeks bias yang nilainya tergantung bahan asli yang mau ditiru: berlian 2.417, air 1.333, udara 1.000293.
  21. Material sangat berpengaruh ke performa. Selalu pantau frame rate, apalagi di perangkat lemah kaya HP.

Bab selanjutnya kita akan bikin teks 3D, sekalian praktekin material-material yang barusan kita kenalan ini ke bentuk yang lebih menarik daripada bola dan donat.