여기 이 Environment Map 이 앞서 언급했듯 Light 보다 StandardMaterial 에 동작에 있어 훨씬 부드럽고 사실성 있다고 언급하였다. 왜냐하면 PBR materials
Physically Based Rendering Material 이기 때문이다.
하지만 단점 역시 존재한다. 우선 Environment Maps은 여러 개의 이미지에서 생성되므로 최초 렌더링 시간이 오래 걸린다.
그러나 조명을 여러 개 사용하는 것 보다 더 빠르게 렌더링할 수 있다.
또한 Environment Maps 은 recieveShadow()
가 없는데 이를 따로 조명을 사용하여 구현할 수도 있다.
가장 좋은 품질의 Environment Maps 은 HDR 이미지를 사용하여 만드는데 이는 JPG, PNG 보다 훨씬 크다 따라서 application 의 원활한 동작을 위해 온라인 툴을 사용해 HDR 이미지를 압축하기도 한다.
그럼 우선 코드와 함께 결과물을 비교해보자.
우선 Environment Maps 을 킨 모습
const data = { environment: true, background: true, mapEnabled: false, planeVisible: false }
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
}
})
참고로 light 를 2개를 쓴 이유는 최대한 환경맵과 비슷하게 구현하기 위해서 사용했다.
Environment Maps 끈 모습
하지만 확실히 훨씬 부자연스러운 모습을 하고있는 것을 알 수 있다.
그리고 위 사진을 보면 알 수 있는 environment map 의 색상에 맞춰 Mesh의 색이 변하는데 이는 환경맵의 색상은 조명 모델에서 활용되기 때문이다.
언급한 MeshStandardMaterial
은 Lambertian Reflection 과 Specular Reflection 을 조합하여 반사 색상을 계산한다.
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)
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
})
Environment Maps 의 조도를 조절한다.
이를 사용하려면 아래 renderer 에 반드시 .ACESFilmicToneMapping
설정을 넣어줘야한다.
const renderer = new THREE.WebGLRenderer({ antialias: true })
renderer.toneMapping = THREE.ACESFilmicToneMapping
renderer.setSize(window.innerWidth, window.innerHeight)
document.body.appendChild(renderer.domElement)
Three.js r163 에서 새로 나온 속성값으로, renderer.toneMapping
, renderer.toneMappingExposure
의 대체재라고 불리지만 조금 다르다.
toneMappingExposure
는 환경맵의 전체적인 조도를 조절했다면 environmentIntensity 는 Mesh 에 반사되는 빛의 세기를 조절한다.
environment: Mesh 에 영향을 준다.
background: Mesh 와 상관없이 배경에만 영향을 준다.
MeshStandardMaterial 을 상속받아 만든 Material 이다.
Environment Maps 이 Mesh 에 영향을 주는 빛의 세기를 조절한다.
163 버전에 새로 scene.environmentIntensity 속성이 추가되었다. 이는 PBR Material 중 material.envMap = null 인 Mesh 에 영향을 준다.
이제 material.envMapIntensity를 변경하면 먼저 material.envMap 프로퍼티에 완전히 로드된 텍스처를 설정한 경우에만 작동한다.
material.envMap = someFullyLoadedTexture를 설정한 후에는 더 이상 scene.environmentIntensity의 변경에 영향을 받지 않는다.
메탈릭하게, 혹은 광나게(<=> 거칠게) 바꿔주는 값이다. 참고로 background 에 의존하지 않고 environment map 에 의존한다.
transmission(투명도)와 함께 동작하는 속성값들로 자주사용되진 않는다.
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 = '../public/rosendal_plains_2_4k.hdr'
const hdr = '../public/warm_restaurant_night_4k.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
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 다운로드 홈페이지: poly haven