Di 10 Materials > Nambah Cahaya kita udah sempat nyalain dua lampu asal-asalan biar objeknya kelihatan. Sekarang kita bedah semuanya satu-satu.

Nambahin cahaya itu segampang nambahin mesh: bikin instance dari class yang sesuai, terus masukin ke scene. Kita udah kenalan sama AmbientLight dan PointLight sekilas, sekarang kita bahas semua jenisnya secara lengkap.

Setup

Scene-nya udah disiapin di starter: ada bola, kubus, donat, dan satu bidang datar sebagai lantai.

Karena kita mau main cahaya, materialnya wajib yang bereaksi sama cahaya. Bisa aja pakai MeshLambertMaterial, MeshPhongMaterial, atau MeshToonMaterial, tapi kita pakai MeshStandardMaterial karena paling realistis. roughness-nya juga dikecilin ke 0.4 biar pantulan cahayanya kelihatan jelas.

❌ percuma, material ini cuek total sama cahaya

const material = new THREE.MeshBasicMaterial()

✅ benar, material ini bereaksi sama cahaya

const material = new THREE.MeshStandardMaterial()
material.roughness = 0.4

Kalau starter-nya udah jalan, hapus dulu AmbientLight dan PointLight-nya biar kita mulai dari nol. Hasilnya layar hitam total tanpa apa-apa. Tenang, itu bukan error, itu emang gelap gulita karena gak ada cahaya sama sekali.

AmbientLight

AmbientLight itu cahaya yang nyinarin semua geometry di scene dari segala arah sekaligus. Parameter pertama warnanya, parameter kedua kekuatannya:

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

Sama kaya material, propertinya juga bisa di-set setelah instance-nya jadi:

// Ambient light
const ambientLight = new THREE.AmbientLight()
ambientLight.color = new THREE.Color(0xffffff)
ambientLight.intensity = 1
scene.add(ambientLight)

Hasilnya sama persis. Tapi perhatiin, color itu wajib dibungkus new THREE.Color(...), aturannya sama kaya di 10 Materials > 2. Color:

❌ salah, color gak nerima angka mentah kalau di-set langsung

ambientLight.color = 0xffffff

✅ benar

ambientLight.color = new THREE.Color(0xffffff)

Dan sama kaya material juga, propertinya bisa dimasukin ke Debug UI biar gampang dicoba-coba:

gui.add(ambientLight, 'intensity').min(0).max(3).step(0.001)

Seterusnya kita gak nambahin tweak lagi ya biar catatannya gak penuh, tapi silakan tambahin sendiri kalau mau enak nyoba-nyobanya.

Nah ini yang penting dipahami: kalau di scene kamu cuma ada AmbientLight doang, hasilnya sama persis kaya pakai MeshBasicMaterial, karena semua sisi objek disinari sama rata. Gak ada gelap-terang, gak ada dimensi.

Terus buat apa dong? Gini logikanya: di dunia nyata, sisi objek yang membelakangi cahaya itu gak pernah hitam pekat, karena cahaya mantul-mantul dulu ke tembok dan benda lain sebelum nyampe ke situ. Pantulan cahaya kaya gitu gak didukung ThreeJS karena berat banget buat performa. Jadi AmbientLight yang redup itu dipakai buat mura-mura jadi pantulan cahaya tadi, biar bagian gelapnya gak hitam mati.

DirectionalLight

DirectionalLight itu cahaya kaya matahari, sinarnya datang sejajar semua. Parameter pertama warna, kedua kekuatannya:

// Directional light
const directionalLight = new THREE.DirectionalLight(0x00fffc, 0.9)
scene.add(directionalLight)

Bawaannya cahayanya datang dari atas. Buat ngubahnya, kita geser lampunya pakai position kaya objek ThreeJS biasa yang kita pelajari di 03 Transform Object > `position` Memindahkan Object:

directionalLight.position.set(1, 0.25, 0)

Yang unik: jaraknya gak ngaruh sama sekali di sini. Yang ngaruh cuma arahnya. Sinarnya dianggap datang dari ruang tak terhingga dan menuju ke arah tak terhingga sebaliknya, sejajar semua. Persis kaya matahari yang mau kamu deketin sedekat apapun ya sinarnya tetap segitu arahnya.

HemisphereLight

HemisphereLight itu mirip AmbientLight, bedanya warna dari langit dan warna dari tanah itu beda. Sisi objek yang menghadap ke atas disinari satu warna, sisi yang menghadap ke bawah disinari warna lain.

Bayangin lapangan rumput di sore hari: bagian atas objek kena warna oranye langit, bagian bawahnya kena pantulan hijau dari rumput.

Parameter pertama warna langit, kedua warna tanah (groundColor), ketiga kekuatannya:

// Hemisphere light
const hemisphereLight = new THREE.HemisphereLight(0xff0000, 0x0000ff, 0.9)
scene.add(hemisphereLight)

PointLight

PointLight itu kaya nyala korek api. Sumber cahayanya satu titik yang kecilnya tak terhingga, dan sinarnya nyebar rata ke segala arah. Parameter pertama warna, kedua kekuatannya:

// Point light
const pointLight = new THREE.PointLight(0xff9000, 1.5)
scene.add(pointLight)

Bisa digeser kaya objek biasa:

pointLight.position.set(1, - 0.5, 1)

Buat hasil yang realistis, biasanya kita cukup main di intensity aja. Tapi sebenarnya ada dua parameter lagi yang bisa kita atur.

1. Distance

Parameter ketiga namanya distance, yaitu jarak di mana kekuatan cahayanya jatuh sampai nol. Bawaannya 0, dan 0 di sini artinya tak terhingga, bukan artinya mati.

Coba isi 0.5:

const pointLight = new THREE.PointLight(0xff9000, 1.5, 0.5)

Kelihatannya mirip, tapi cahayanya anjlok drastis begitu mendekati batas 0.5 itu. Kita balikin dulu ke tak terhingga:

const pointLight = new THREE.PointLight(0xff9000, 1.5, 0)

2. Decay

Parameter keempat namanya decay, yaitu seberapa cepat cahayanya meredup. Makin kecil nilainya, makin cepat redupnya. Bawaannya 2.

Coba isi 0.5:

const pointLight = new THREE.PointLight(0xff9000, 1.5, 0, 0.5)

Cahayanya jadi meredup lebih cepat. Tapi buat hasil yang realistis dan sesuai fisika, mendingan biarin di nilai bawaannya:

const pointLight = new THREE.PointLight(0xff9000, 1.5, 0, 2)

RectAreaLight

RectAreaLight itu kaya lampu kotak gede yang dipakai di studio foto. Sifatnya campuran antara cahaya berarah dan cahaya menyebar.

Parameternya: warna, kekuatan, lebar kotaknya, dan tinggi kotaknya:

// Rect area light
const rectAreaLight = new THREE.RectAreaLight(0x4e00ff, 6, 1, 1)
scene.add(rectAreaLight)

Peringatan penting: RectAreaLight cuma jalan di MeshStandardMaterial dan MeshPhysicalMaterial. Kalau kamu pasang di material lain, dia bakal diam aja tanpa error dan kamu bakal bingung setengah mati nyari salahnya di mana.

Lampu ini bisa digeser dan diputar. Biar gampang muternya, kita pakai method lookAt(...) yang udah kita kenal di 03 Transform Object > Look at this!:

rectAreaLight.position.set(- 1.5, 0, 1.5)
rectAreaLight.lookAt(new THREE.Vector3())

Vector3 yang dibikin tanpa parameter apapun itu isinya 0, 0, 0, alias tepat di tengah scene. Jadi baris itu artinya “hadap ke tengah scene”.

SpotLight

SpotLight itu kaya senter. Bentuknya kerucut cahaya yang berangkat dari satu titik ke satu arah.

Parameternya paling banyak di antara semua:

  1. color, warnanya.
  2. intensity, kekuatannya.
  3. distance, jarak di mana kekuatannya jatuh ke nol.
  4. angle, seberapa lebar kerucut cahayanya.
  5. penumbra, seberapa kabur pinggiran kerucutnya.
  6. decay, seberapa cepat cahayanya meredup.
// Spot light
const spotLight = new THREE.SpotLight(0x78ff00, 4.5, 10, Math.PI * 0.1, 0.25, 1)
spotLight.position.set(0, 2, 3)
scene.add(spotLight)

Math.PI * 0.1 itu satuan radian lagi, sama kaya waktu kita muter objek.

Muter SpotLight Itu Aneh

Nah, muter SpotLight itu agak ribet dan ada jebakannya.

Instance-nya punya properti target yang isinya sebuah Object3D, dan SpotLight itu selalu ngeliat ke objek target itu. Tapi kalau kamu coba geser posisinya, lampunya gak bergeming sama sekali:

spotLight.target.position.x = - 0.75

Kok bisa? Ini perilaku yang aneh tapi ada alasannya.

Posisi, rotasi, dan skala sebuah objek itu dirangkum jadi satu hal yang namanya transform matrix, dan matrix itulah yang sebenarnya dipakai buat ngerender. Kedengeran rumit? Untungnya kita gak perlu ngurusin itu, ThreeJS yang ngurusin.

Tapi ThreeJS cuma ngitungin transform matrix buat objek yang ada di dalam scene. Ngapain juga repot-repot ngitungin objek yang gak ada di sana?

Nah, spotLight.target itu objek kosong yang belum pernah kita masukin ke scene. Jadi solusinya ya masukin aja:

❌ salah, target-nya gak pernah dihitung karena gak ada di scene

spotLight.target.position.x = - 0.75

✅ benar, target-nya dimasukin ke scene walaupun dia objek kosong

scene.add(spotLight.target)
spotLight.target.position.x = - 0.75

Sekarang lampunya beneran nengok ke kiri.

Performa

Cahaya itu bagus dan bisa bikin hasil realistis kalau dipakai dengan benar. Masalahnya, cahaya itu mahal banget buat performa. GPU harus ngitung banyak hal: jarak tiap face ke lampu, seberapa miring face itu terhadap lampu, apakah face-nya masuk ke dalam kerucut spotlight, dan seterusnya.

Pakai lampu sesedikit mungkin, dan pilih yang paling murah kalau bisa.

Biaya murah:

  1. AmbientLight
  2. HemisphereLight

Biaya sedang:

  1. DirectionalLight
  2. PointLight

Biaya mahal:

  1. SpotLight
  2. RectAreaLight

Baking

Ada satu teknik pencahayaan yang jempolan namanya baking. Idenya: cahayanya dipanggang langsung ke dalam texture. Jadi bayangan dan sorotan cahayanya udah jadi bagian dari gambar, bukan diitung ulang tiap frame.

Ini dikerjain di software 3D, dan hasilnya bisa luar biasa bagus dengan performa yang enteng banget, karena scene-nya sebenarnya gak punya lampu satupun.

Kelemahannya jelas: lampunya gak bisa digerakin lagi, wong emang gak ada lampunya. Dan kamu bakal butuh banyak texture.

Contoh bagusnya ada di halaman depan Three.js Journey itu sendiri. Cara bikinnya ada di bab Portal nanti.

Helpers

Naruh dan ngarahin lampu itu susah, soalnya lampunya sendiri gak kelihatan, yang kelihatan cuma efeknya. Buat bantuin kita, ada yang namanya helper, konsepnya sama kaya Axes Helper yang dulu kita pakai buat lihat arah sumbu.

Yang didukung cuma lima ini:

  1. HemisphereLightHelper
  2. DirectionalLightHelper
  3. PointLightHelper
  4. RectAreaLightHelper
  5. SpotLightHelper

Cara pakainya tinggal bikin instance-nya, kirim lampunya sebagai parameter pertama, terus masukin ke scene. Parameter kedua buat ngatur ukuran helper-nya:

const hemisphereLightHelper = new THREE.HemisphereLightHelper(hemisphereLight, 0.2)
scene.add(hemisphereLightHelper)
 
const directionalLightHelper = new THREE.DirectionalLightHelper(directionalLight, 0.2)
scene.add(directionalLightHelper)
 
const pointLightHelper = new THREE.PointLightHelper(pointLight, 0.2)
scene.add(pointLightHelper)
 
const spotLightHelper = new THREE.SpotLightHelper(spotLight)
scene.add(spotLightHelper)

Khusus RectAreaLightHelper agak beda, karena class-nya belum masuk ke variabel THREE. Jadi harus di-import sendiri dari folder examples, persis kaya waktu kita ngambil OrbitControls dan FontLoader:

import { RectAreaLightHelper } from 'three/examples/jsm/helpers/RectAreaLightHelper.js'

Baru setelah itu bisa dipakai:

const rectAreaLightHelper = new RectAreaLightHelper(rectAreaLight)
scene.add(rectAreaLightHelper)

Code Lengkap

import * as THREE from 'three'
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'
import { RectAreaLightHelper } from 'three/examples/jsm/helpers/RectAreaLightHelper.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
}
 
/**
 * Lights
 */
// Ambient light
const ambientLight = new THREE.AmbientLight(0xffffff, 1)
scene.add(ambientLight)
 
gui.add(ambientLight, 'intensity').min(0).max(3).step(0.001)
 
// Directional light
const directionalLight = new THREE.DirectionalLight(0x00fffc, 0.9)
directionalLight.position.set(1, 0.25, 0)
scene.add(directionalLight)
 
// Hemisphere light
const hemisphereLight = new THREE.HemisphereLight(0xff0000, 0x0000ff, 0.9)
scene.add(hemisphereLight)
 
// Point light
const pointLight = new THREE.PointLight(0xff9000, 1.5, 0, 2)
pointLight.position.set(1, - 0.5, 1)
scene.add(pointLight)
 
// Rect area light
const rectAreaLight = new THREE.RectAreaLight(0x4e00ff, 6, 1, 1)
rectAreaLight.position.set(- 1.5, 0, 1.5)
rectAreaLight.lookAt(new THREE.Vector3())
scene.add(rectAreaLight)
 
// Spot light
const spotLight = new THREE.SpotLight(0x78ff00, 4.5, 10, Math.PI * 0.1, 0.25, 1)
spotLight.position.set(0, 2, 3)
scene.add(spotLight)
 
scene.add(spotLight.target)
spotLight.target.position.x = - 0.75
 
/**
 * Helpers
 */
const hemisphereLightHelper = new THREE.HemisphereLightHelper(hemisphereLight, 0.2)
scene.add(hemisphereLightHelper)
 
const directionalLightHelper = new THREE.DirectionalLightHelper(directionalLight, 0.2)
scene.add(directionalLightHelper)
 
const pointLightHelper = new THREE.PointLightHelper(pointLight, 0.2)
scene.add(pointLightHelper)
 
const spotLightHelper = new THREE.SpotLightHelper(spotLight)
scene.add(spotLightHelper)
 
const rectAreaLightHelper = new RectAreaLightHelper(rectAreaLight)
scene.add(rectAreaLightHelper)
 
/**
 * Objects
 */
const material = new THREE.MeshStandardMaterial()
material.roughness = 0.4
 
const sphere = new THREE.Mesh(
    new THREE.SphereGeometry(0.5, 32, 32),
    material
)
sphere.position.x = - 1.5
 
const cube = new THREE.Mesh(
    new THREE.BoxGeometry(0.75, 0.75, 0.75),
    material
)
 
const torus = new THREE.Mesh(
    new THREE.TorusGeometry(0.3, 0.2, 32, 64),
    material
)
torus.position.x = 1.5
 
const plane = new THREE.Mesh(
    new THREE.PlaneGeometry(5, 5),
    material
)
plane.rotation.x = - Math.PI * 0.5
plane.position.y = - 0.65
 
scene.add(sphere, cube, torus, plane)
 
/**
 * Camera
 */
const camera = new THREE.PerspectiveCamera(75, sizes.width / sizes.height, 0.1, 100)
camera.position.x = 1
camera.position.y = 1
camera.position.z = 2
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
    cube.rotation.y = 0.1 * elapsedTime
    torus.rotation.y = 0.1 * elapsedTime
 
    sphere.rotation.x = 0.15 * elapsedTime
    cube.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. Nambah cahaya itu sama kaya nambah mesh: bikin instance class-nya, terus scene.add(...).
  2. Materialnya wajib yang bereaksi sama cahaya. MeshBasicMaterial dan MeshMatcapMaterial cuek total, pakai MeshStandardMaterial biar realistis.
  3. AmbientLight nyinarin semua sisi objek sama rata, jadi kalau cuma dia doang hasilnya sama kaya MeshBasicMaterial, gak ada dimensi.
  4. Pantulan cahaya gak didukung ThreeJS karena berat. AmbientLight yang redup dipakai buat mura-mura jadi pantulan itu, biar sisi gelap objek gak hitam mati.
  5. color yang di-set langsung wajib dibungkus new THREE.Color(...), sama kaya di material.
  6. DirectionalLight itu cahaya matahari, sinarnya sejajar semua. Jaraknya gak ngaruh, cuma arahnya yang ngaruh.
  7. HemisphereLight itu AmbientLight dua warna: satu dari langit, satu dari tanah.
  8. PointLight itu kaya korek api, nyebar rata ke segala arah dari satu titik. distance 0 artinya tak terhingga, dan decay makin kecil makin cepat redup (bawaannya 2, biarin aja).
  9. RectAreaLight itu lampu studio foto, dan cuma jalan di MeshStandardMaterial dan MeshPhysicalMaterial. Kalau dipasang di material lain, dia diam aja tanpa error.
  10. new THREE.Vector3() tanpa parameter itu isinya 0, 0, 0, alias titik tengah scene.
  11. SpotLight itu kaya senter dengan 6 parameter, dan arahnya diatur lewat properti target.
  12. spotLight.target wajib dimasukin ke scene, karena ThreeJS cuma ngitung transform matrix buat objek yang ada di dalam scene. Kalau nggak, digeser kemanapun lampunya gak bergeming.
  13. Cahaya itu mahal buat performa. Urutan dari murah ke mahal: AmbientLight dan HemisphereLight, terus DirectionalLight dan PointLight, terus SpotLight dan RectAreaLight.
  14. Baking itu manggang cahaya langsung ke dalam texture lewat software 3D. Hasilnya bagus dan enteng banget, tapi lampunya jadi gak bisa digerakin karena emang gak ada.
  15. Ada 5 helper buat bantuin naruh lampu, dan RectAreaLightHelper harus di-import sendiri karena belum masuk ke variabel THREE.

Bab selanjutnya kita akan belajar bikin bayangan, karena sekarang objek kita udah kena cahaya tapi belum ada satupun yang punya bayangan, jadi masih kerasa melayang gak natural.