[three.js][typescript] - Material(5)
[three.js][typescript] - Material(4)[three.js][typescript] - Material(3)[three.js][typescript] - Material(2)[three.js][typescript] - Material(1)Three.js 의 Object3D의 파생클래스는 위와 같다. Point는 Geometry를 구성하는 좌표 하나 하
yoohwanihn.tistory.com
이전 글에서 재질의 Texture Mapping 속성 중에서 map만을 다뤘다.
Texture Mapping 속성

모든 Mesh Material은 공통으로 map속성을 제공한다.
map 외의 다양한 속성을 알아보기 위해 Texture 이미지가 필요하다.
Texture 이미지를 다운로드하자.
3D TEXTURES
Free seamless PBR textures with diffuse, normal, height, AO and roughness maps.
3dtextures.me
위 링크에 접속하면 다양한 카테고리의 3D Texture들을 제공한다.

우리는 이 중 Glass 카테고리의

GLASS WINDOW 002를 사용해 보자.

다양한 Texture와 map을 이용해 Glass Window 002와 같은 재질(material)을 만들 수가 있다.

Glass Window 002는 위와 같은 6개의 이미지를 사용하여 만들어진다.
Diffuse는 map 속성에 적용할 수 있는 이미지인데, 이 이미지를 다운로드하기 위해

해당 버튼을 통해 다운로드하면 된다. 혹은
three.js/data at main · yoohwanihn/three.js
[three.js][typescript]. Contribute to yoohwanihn/three.js development by creating an account on GitHub.
github.com
깃허브를 통해 다운로드하면 된다.

그리고 다운로드한 파일(매핑을 위한 7개의 이미지)을 프로젝트의 public 폴더에 업로드해 준다.
import * as THREE from 'three'
import { OrbitControls, RGBELoader } from 'three/examples/jsm/Addons.js'
import './style.css'
import GUI from 'three/examples/jsm/libs/lil-gui.module.min.js'
class App {
private renderer: THREE.WebGLRenderer //Renderer Field 추가
private domApp: Element
private scene: THREE.Scene
private camera?: THREE.PerspectiveCamera //?를 붙이면 PerspectiveCamera Type이나 Undefined Type을 가질 수 있음.(Optional Properties)
constructor() {
console.log("YooHwanIhn");
this.renderer = new THREE.WebGLRenderer({ antialias: true }) // 안티-알리아스 : 높은 렌더링 결과를 얻기 위해 픽셀 사이에 계단 현상을 방지하는 효과 추가
this.renderer.setPixelRatio(Math.min(2, window.devicePixelRatio)) // 고해상도에서 깨짐 방지를 위해 픽셀 비율 지정
this.domApp = document.querySelector('#app')!
this.domApp.appendChild(this.renderer.domElement) // renderer.domeElement : canvas Type의 DOM 객체
this.scene = new THREE.Scene()
this.setupCamera()
this.setupLight()
this.setupModels()
this.setupEvents()
}
private setupCamera() {
const width = this.domApp.clientWidth
const height = this.domApp.clientHeight
this.camera = new THREE.PerspectiveCamera(75, width / height, 0.1, 100)
this.camera.position.z = 4 // (0, 0, 4)
new OrbitControls(this.camera, this.domApp as HTMLElement)
}
private setupLight() {
// const light = new THREE.DirectionalLight(0xffffff, 1)
// light.position.set(1,2,1)
// this.scene.add(light)
/** HDRI */
const rgbeLoader = new RGBELoader() //HDRI를 사용하기 위한 로더 객체
rgbeLoader.load('./red_hill_cloudy_4k.hdr', (environmentMap) => {
environmentMap.mapping = THREE.EquirectangularRefractionMapping
this.scene.background = environmentMap
this.scene.environment = environmentMap
})
}
private setupModels() {
const textureLoader = new THREE.TextureLoader() // three.js는 이미지를 로드할 때 텍스쳐 타입으로 해야함
const map = textureLoader.load("./Glass_Window_002_basecolor.jpg")
const mapAO = textureLoader.load("./Glass_Window_002_ambientOcclusion.jpg")
const mapHeight = textureLoader.load("./Glass_Window_002_height.png")
const mapNormal = textureLoader.load("./Glass_Window_002_normal.jpg")
const mapRoughness = textureLoader.load("./Glass_Window_002_roughness.jpg")
const mapMetalic = textureLoader.load("./Glass_Window_002_metallic.jpg")
const mapAlpha = textureLoader.load("./Glass_Window_002_opacity.jpg")
const material = new THREE.MeshStandardMaterial({
map: map
})
const geomBox = new THREE.BoxGeometry(1, 1, 1)
const box = new THREE.Mesh(geomBox, material)
box.position.x = -1
this.scene.add(box)
const geomSphere = new THREE.SphereGeometry(0.6)
const sphere = new THREE.Mesh(geomSphere, material)
sphere.position.x = 1
this.scene.add(sphere)
}
//실제 이벤트와 렌더링 처리를 다룰 메서드
private setupEvents() {
window.onresize = this.resize.bind(this); // html의 size가 변경될 때마다 호출되는 함수(addEventListener 느낌??)
this.resize();
this.renderer.setAnimationLoop(this.render.bind(this)) // 연속적으로 render 메서드 호출(모니터 Frame에 맞게)
}
// html창 resize시 호출할 함수
private resize() {
const width = this.domApp.clientWidth
const height = this.domApp.clientHeight
//앞선 setUpCamera에서 설정한 camera 정보를 다시 수정해줌.
const camera = this.camera
if (camera) {
camera.aspect = width / height
camera.updateProjectionMatrix() // 카메라의 값이 변경되었다면 수정하도록 함.
}
//renderer도 마찬가지로 사이즈 수정함
this.renderer.setSize(width, height)
}
private update(time: number) {
time *= 0.001 // ms -> s 단위로 변경
}
private render(time: number) {
// time : setAnimationLoop의 값에 의해서 결정되는데 단위는 ms
this.update(time)
this.renderer.render(this.scene, this.camera!)
}
}
new App()
그 후 위의 코드로 변경시켜 보자.
변경된 부분은 setupModels()로 TextureLoader 객체를 통해 7개의 이미지를 로드하였다.
이제 로드한 7개의 텍스쳐를 바탕으로 옵션들을 살펴볼 것이다.
먼저 위 코드 그대로 map 속성을 이용해 baseColor의 이미지를 매핑시켜 주면

위와 같은 결과가 나온다. Sphere와 Box Geometry에 업로드한 basecolor 이미지가 재질로 붙여진 모습이다.

근데 위 이미지와 다르게 결과의 재질이 너무 밝게 나왔다.
아마 이전 글을 읽은 사람은 바로 이유를 알아챘을 것이다.
그 이유는 위 이미지의 색상 공간을 제대로 지정하지 않았기 때문이다.

basecolor를 로드한 객체에 색상 공간을 SRGB로 변경시켜 주자

이미지의 색상과 동일한 재질로 구성된 Geometry 모델들을 볼 수 있다.

이번엔 normalMap 속성을 추가하여 normal.jpg 이미지의 텍스쳐를 추가한다.

결과를 확대하여 보면 mesh에 금이 간듯한 모습이 연출되었다.
이유를 찾기 위해서 normalMap 속성과 추가된 이미지에 대해 알아보자

normal 이미지를 보면 우리가 봤던 것처럼 금이 간 모습을 확인할 수 있다.

그러면 다음과 같이 코드를 수정하여 한번 더 결과를 보자

기존보다 더 선명하게 금을 확인할 수 있는데, normalScale 옵션에서 기존 대비 10배로 크기를 키웠기 때문이다.
이제 감이 잡힐 것이다.

이 normalMap에 대한 이미지는 법선 벡터에 대한 정보를 이미지로 만든 것이다.
법선 벡터는 Mesh의 표면에 대한 수직 벡터로 광원에 대한 영향을 계산하는 데 사용된다.
그럼 모델에 법선 벡터를 표시해 보자

다음과 같이 boxHelper, SphereHelper를 만들어 모델에 법선에 대한 정보를 표시해 주자
그리고 이를 사용하기 위해

버텍스 노말 헬퍼 에드온을 import 해준다.

박스, 스피어에 대한 노말 벡터가 노란색 선으로 표시가 되었다.
박스, 스피어를 구성하는 좌표 하나에 따라 노말벡터가 한 개씩 지정되어 있다.
노란색의 노말벡터를 이용해 모델 표면에 수식 벡터를 얻을 수 있다.
모델 표면의 수식 벡터는 빛의 반사각도를 계산할 때 사용된다.
보다 정확히는 벡터의 내적 계산에 사용되는데,

normalMap이 지정되면 Geometry에 지정된 노란색의 법선 벡터에 normalMap 이미지가 갖고 있는 법선 벡터가 표시된다.
이렇게 되면 인위적으로 Mesh 표면에 각 픽셀에 대한 법선 벡터를 보정할 수 있게 되어서
각 픽셀 단위로 광원 효과가 달라져 입체감을 표현할 수 있게 된다.
하지만 이 Mesh의 Geometry 형상이 바뀌는 것은 아니기 때문에 normalMap에 의한 입체감은 착시이지만
매우 적은 Geometry 좌표 구성 만으로도 입체감을 표현할 수 있는 효과적인 방법이다.
displacementMap
이번엔 setupModels를 수정해서 노말 벡터를 표시하는 코드를 삭제하고 mapHeight를 사용해 보자
private setupModels() {
const textureLoader = new THREE.TextureLoader() // three.js는 이미지를 로드할 때 텍스쳐 타입으로 해야함
const map = textureLoader.load("./Glass_Window_002_basecolor.jpg")
map.colorSpace = THREE.SRGBColorSpace
const mapAO = textureLoader.load("./Glass_Window_002_ambientOcclusion.jpg")
const mapHeight = textureLoader.load("./Glass_Window_002_height.png")
const mapNormal = textureLoader.load("./Glass_Window_002_normal.jpg")
const mapRoughness = textureLoader.load("./Glass_Window_002_roughness.jpg")
const mapMetalic = textureLoader.load("./Glass_Window_002_metallic.jpg")
const mapAlpha = textureLoader.load("./Glass_Window_002_opacity.jpg")
const material = new THREE.MeshStandardMaterial({
map: map,
normalMap: mapNormal,
normalScale: new THREE.Vector2(1, 1),
displacementMap: mapHeight,
})
const geomBox = new THREE.BoxGeometry(1, 1, 1)
const box = new THREE.Mesh(geomBox, material)
box.position.x = -1
this.scene.add(box)
const geomSphere = new THREE.SphereGeometry(0.6)
const sphere = new THREE.Mesh(geomSphere, material)
sphere.position.x = 1
this.scene.add(sphere)
//const boxHelper = new VertexNormalsHelper(box, 0.1, 0xffff00)
//this.scene.add(boxHelper)
//const sphereHelper = new VertexNormalsHelper(sphere, 0.1, 0xffff00)
//this.scene.add(sphereHelper)
}
이 mapHeight 텍스쳐를 displacementMap에 지정해 보자.
결과는 다음과 같다.

기존 Geometry에서 많이 왜곡된 모습의 결과가 나왔다.

우리가 로드한 텍스쳐 이미지는 위와 같다.
위 텍스쳐 이미지가 왜곡된 결과를 만든 이유는 displacementMap 옵션 때문이다.
displacementMap은 실제로 Geometry의 좌표를 변형시켜서 입체감을 표현하기 위한 목적으로 사용된다.
이때 Map 이미지의 픽셀값이 더 밝을수록 좌표가 더 많이 움직인다.
움직이는 방향은 법선 벡터 방향인데, 위의 왜곡된 이미지는 좌표가 너무 많이 움직였다.
이를 수정하는 옵션을 추가해 보자

displacementScale 옵션을 추가하여 왜곡 스케일을 수정해 보자
기본값은 1인데 0.2로 지정하여 20%만 정점이 이동되도록 해주었다.

그러나 Box와 달리 Sphere는 정점이 제대로 이동되지 않았다.
이는, displacementMap이 실제 Geometry의 구성 좌표를 변경시키기 때문에
이 Box의 표면에 대한 구성 좌표가 더 많이 제공되어야 한다.
이를 위해서 Box의 Geometry 표면을 여러 개의 면으로 분할(segmentation)시켜야 한다.


각 Geometry 표면을 분할시킨 다음 결과를 다시 보면

더 잘 나타나는 것을 확인할 수 있다.
그러나 BoxGeometry를 보면 구성면이 좌표의 이동으로 인해 분리되는데
이를 조정하기 위해 또 속성을 지정해 보자

displacementBias 옵션을 추가해 보자.

박스의 면이 붙어있는 것을 확인할 수 있다.
aoMap
이번엔 aoMap에 대해 알아보자
기존 setupModels()를 다음과 같이 수정해 준다.
private setupModels() {
const textureLoader = new THREE.TextureLoader() // three.js는 이미지를 로드할 때 텍스쳐 타입으로 해야함
const map = textureLoader.load("./Glass_Window_002_basecolor.jpg")
map.colorSpace = THREE.SRGBColorSpace
const mapAO = textureLoader.load("./Glass_Window_002_ambientOcclusion.jpg")
const mapHeight = textureLoader.load("./Glass_Window_002_height.png")
const mapNormal = textureLoader.load("./Glass_Window_002_normal.jpg")
const mapRoughness = textureLoader.load("./Glass_Window_002_roughness.jpg")
const mapMetalic = textureLoader.load("./Glass_Window_002_metallic.jpg")
const mapAlpha = textureLoader.load("./Glass_Window_002_opacity.jpg")
const material = new THREE.MeshStandardMaterial({
// map: map,
// normalMap: mapNormal,
// normalScale: new THREE.Vector2(1, 1),
// displacementMap: mapHeight,
// displacementScale: 0.2, //default 1
// displacementBias: -0.15
aoMap: mapAO,
aoMapIntensity: 1, //aoMap 강도
})
const geomBox = new THREE.BoxGeometry(1, 1, 1, 256, 256, 256)
const box = new THREE.Mesh(geomBox, material)
box.position.x = -1
this.scene.add(box)
const geomSphere = new THREE.SphereGeometry(0.6, 512, 256)
const sphere = new THREE.Mesh(geomSphere, material)
sphere.position.x = 1
this.scene.add(sphere)
//const boxHelper = new VertexNormalsHelper(box, 0.1, 0xffff00)
//this.scene.add(boxHelper)
//const sphereHelper = new VertexNormalsHelper(sphere, 0.1, 0xffff00)
//this.scene.add(sphereHelper)
}
aoMap Texture를 재질에 추가해 주었으며, 확실한 확인을 위해 기존 옵션들을 주석 처리 하였다.

해당 이미지는 그림자와 음영효과를 그려서 표현한 이미지다.
이제 결과를 확인해 보자

aoMap을 통해 그림자와 음영을 처리할 수 있고, aoMapIntensity를 통해 그 강도를 조절할 수 있는데,
이를 기존의 코드 주석을 풀어서 비교해 보자

![]() |
![]() |
표의 좌측은 aoMap으로 그림자 효과를 주기 전, 우측은 aoMap으로 그림자, 음영 효과를 받은 모습이다.
직접 코드로 running을 하면 더 잘 확인할 수 있다.
이 aoMap을 이용해서 그림자를 미리 만들어두고 좀 더 세밀한 그림자를 이용하는 데 사용할 수 있다.
다음 글에서 이어서 roughness, metallic, alpha에 대해 알아보자
전체 코드(main.ts)
import * as THREE from 'three'
import { OrbitControls, RGBELoader, VertexNormalsHelper } from 'three/examples/jsm/Addons.js'
import './style.css'
import GUI from 'three/examples/jsm/libs/lil-gui.module.min.js'
class App {
private renderer: THREE.WebGLRenderer //Renderer Field 추가
private domApp: Element
private scene: THREE.Scene
private camera?: THREE.PerspectiveCamera //?를 붙이면 PerspectiveCamera Type이나 Undefined Type을 가질 수 있음.(Optional Properties)
constructor() {
console.log("YooHwanIhn");
this.renderer = new THREE.WebGLRenderer({ antialias: true }) // 안티-알리아스 : 높은 렌더링 결과를 얻기 위해 픽셀 사이에 계단 현상을 방지하는 효과 추가
this.renderer.setPixelRatio(Math.min(2, window.devicePixelRatio)) // 고해상도에서 깨짐 방지를 위해 픽셀 비율 지정
this.domApp = document.querySelector('#app')!
this.domApp.appendChild(this.renderer.domElement) // renderer.domeElement : canvas Type의 DOM 객체
this.scene = new THREE.Scene()
this.setupCamera()
this.setupLight()
this.setupModels()
this.setupEvents()
}
private setupCamera() {
const width = this.domApp.clientWidth
const height = this.domApp.clientHeight
this.camera = new THREE.PerspectiveCamera(75, width / height, 0.1, 100)
this.camera.position.z = 4 // (0, 0, 4)
new OrbitControls(this.camera, this.domApp as HTMLElement)
}
private setupLight() {
// const light = new THREE.DirectionalLight(0xffffff, 1)
// light.position.set(1,2,1)
// this.scene.add(light)
/** HDRI */
const rgbeLoader = new RGBELoader() //HDRI를 사용하기 위한 로더 객체
rgbeLoader.load('./red_hill_cloudy_4k.hdr', (environmentMap) => {
environmentMap.mapping = THREE.EquirectangularRefractionMapping
this.scene.background = environmentMap
this.scene.environment = environmentMap
})
}
private setupModels() {
const textureLoader = new THREE.TextureLoader() // three.js는 이미지를 로드할 때 텍스쳐 타입으로 해야함
const map = textureLoader.load("./Glass_Window_002_basecolor.jpg")
map.colorSpace = THREE.SRGBColorSpace
const mapAO = textureLoader.load("./Glass_Window_002_ambientOcclusion.jpg")
const mapHeight = textureLoader.load("./Glass_Window_002_height.png")
const mapNormal = textureLoader.load("./Glass_Window_002_normal.jpg")
const mapRoughness = textureLoader.load("./Glass_Window_002_roughness.jpg")
const mapMetalic = textureLoader.load("./Glass_Window_002_metallic.jpg")
const mapAlpha = textureLoader.load("./Glass_Window_002_opacity.jpg")
const material = new THREE.MeshStandardMaterial({
map: map,
normalMap: mapNormal,
normalScale: new THREE.Vector2(1, 1),
displacementMap: mapHeight,
displacementScale: 0.2, //default 1
displacementBias: -0.15,
aoMap: mapAO,
aoMapIntensity: 1, //aoMap 강도
})
const geomBox = new THREE.BoxGeometry(1, 1, 1, 256, 256, 256)
const box = new THREE.Mesh(geomBox, material)
box.position.x = -1
this.scene.add(box)
const geomSphere = new THREE.SphereGeometry(0.6, 512, 256)
const sphere = new THREE.Mesh(geomSphere, material)
sphere.position.x = 1
this.scene.add(sphere)
//const boxHelper = new VertexNormalsHelper(box, 0.1, 0xffff00)
//this.scene.add(boxHelper)
//const sphereHelper = new VertexNormalsHelper(sphere, 0.1, 0xffff00)
//this.scene.add(sphereHelper)
}
//실제 이벤트와 렌더링 처리를 다룰 메서드
private setupEvents() {
window.onresize = this.resize.bind(this); // html의 size가 변경될 때마다 호출되는 함수(addEventListener 느낌??)
this.resize();
this.renderer.setAnimationLoop(this.render.bind(this)) // 연속적으로 render 메서드 호출(모니터 Frame에 맞게)
}
// html창 resize시 호출할 함수
private resize() {
const width = this.domApp.clientWidth
const height = this.domApp.clientHeight
//앞선 setUpCamera에서 설정한 camera 정보를 다시 수정해줌.
const camera = this.camera
if (camera) {
camera.aspect = width / height
camera.updateProjectionMatrix() // 카메라의 값이 변경되었다면 수정하도록 함.
}
//renderer도 마찬가지로 사이즈 수정함
this.renderer.setSize(width, height)
}
private update(time: number) {
time *= 0.001 // ms -> s 단위로 변경
}
private render(time: number) {
// time : setAnimationLoop의 값에 의해서 결정되는데 단위는 ms
this.update(time)
this.renderer.render(this.scene, this.camera!)
}
}
new App()
'three.js' 카테고리의 다른 글
| [three.js][typescript] - Light(1) (0) | 2024.07.15 |
|---|---|
| [three.js][typescript] - Material(7) (0) | 2024.07.12 |
| [three.js][typescript] - Material(5) (0) | 2024.07.10 |
| [three.js][typescript] - Material(4) (0) | 2024.07.10 |
| [three.js][typescript] - Material(3) (0) | 2024.07.08 |

