
three.js 入門 (10) - 環境マッピング
「three.js」で環境マッピングを行う方法をまとめました。
前回
1. 環境マッピング
環境マッピングとは、3DCGにおけるテクスチャマッピングの手法の1つで、3次元物体の表面に擬似的な周囲環境の映り込みを表現する手法です。
2. キューブ環境マッピングのテクスチャの準備
今回は、キューブ環境マッピングを行うので、立方体の展開図のような6枚のテクスチャ画像を用意します。

練習のため、以下のサイトの画像を利用させてもらいました。
1024x512の画像を以下のPythonスクリプトで分割しました。
from PIL import Image
# 画像サイズ
SIZE = 170
# 元画像の読み込み
image = Image.open('comfy_cafe_1k.jpg')
# 画像分割
im_posx = image.crop((SIZE*2, SIZE, SIZE*3, SIZE*2))
im_posx.save('posx.jpg', quality=80)
im_posy = image.crop((SIZE*1, SIZE*0, SIZE*2, SIZE*1))
im_posy.save('posy.jpg', quality=80)
im_posz = image.crop((SIZE*1, SIZE, SIZE*2, SIZE*2))
im_posz.save('posz.jpg', quality=80)
im_negx = image.crop((SIZE*0, SIZE, SIZE*1, SIZE*2))
im_negx.save('negx.jpg', quality=80)
im_negy = image.crop((SIZE*1, SIZE*2, SIZE*2, SIZE*3))
im_negy.save('negy.jpg', quality=80)
im_negz = image.crop((SIZE*3, SIZE, SIZE*4, SIZE*2))
im_negz.save('negz.jpg', quality=80)
3. キューブ環境マッピングの実装
「three.js 入門 (1) - 事始め」の立方体表示のサンプルをベースに環境マッピングを追加します。
(1) キューブ環境マッピングのテクスチャの読み込み
// キューブ環境マッピングのテクスチャの読み込み
const urls = [
'posx.jpg','negx.jpg',
'posy.jpg','negy.jpg',
'posz.jpg','negz.jpg',
]
const loader = new THREE.CubeTextureLoader()
const textureCube = loader.load(urls)
textureCube.mapping = THREE.CubeReflectionMapping //反射マッピングの設定
マッピングの種類は、次のとおりです。
・UVMapping : UVマッピング
・CubeRefractionMapping : キューブ屈折マッピング
・CubeReflectionMapping : キューブ反射マッピング
・EquirectangularReflectionMapping : 正距円筒図法屈折マッピング
・EquirectangularRefractionMapping : 正距円筒図法反射マッピング
・CubeUVReflectionMapping : キューブUV屈折マッピング
・CubeUVRefractionMapping : キューブUV反射マッピング
(2) 立方体を球体に変更し(反射が見やすいように)、MeshPhongMaterialのenvMapに環境マッピングのテクスチャ画像を指定。
// 球体の準備
const geometry = new THREE.SphereGeometry(1,30, 30) // ジオメトリ
const material = new THREE.MeshPhongMaterial({envMap: textureCube,}) // マテリアル
const cube = new THREE.Mesh(geometry, material) // メッシュ
scene.add(cube)
camera.position.z = 5

4. スカイボックスの設定
環境マッピングのテクスチャをスカイボックスに指定するには、scene.backgroundを使います。
// スカイボックスの設定
scene.background = textureCube

5. 全ソースコード
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<style>
body { margin: 0; }
</style>
</head>
<body>
<script src="three.js"></script>
<script>
// シーンの準備
const scene = new THREE.Scene()
// カメラの準備
const camera = new THREE.PerspectiveCamera(75, window.innerWidth/window.innerHeight, 0.1, 1000)
// レンダラーの準備
const renderer = new THREE.WebGLRenderer()
renderer.setSize(window.innerWidth, window.innerHeight)
document.body.appendChild(renderer.domElement)
// ライトの準備
const directionalLight = new THREE.DirectionalLight('#aaaaff', 1)
directionalLight.position.set(0, 10, 10)
scene.add(directionalLight)
// テクスチャ画像の読み込み
const urls = [
'posx.jpg','negx.jpg',
'posy.jpg','negy.jpg',
'posz.jpg','negz.jpg',
]
const loader = new THREE.CubeTextureLoader()
const textureCube = loader.load(urls)
textureCube.mapping = THREE.CubeReflectionMapping //反射マッピングの設定
// 球体の準備
const geometry = new THREE.SphereGeometry(1,30, 30) // ジオメトリ
const material = new THREE.MeshPhongMaterial({envMap: textureCube,}) // マテリアル
const cube = new THREE.Mesh(geometry, material) // メッシュ
scene.add(cube)
camera.position.z = 5
// スカイボックスの設定
scene.background = textureCube
// アニメーションループの開始
function animate() {
requestAnimationFrame(animate)
cube.rotation.x += 0.01
cube.rotation.y += 0.01
renderer.render(scene, camera)
}
animate()
</script>
</body>
</html>
5. 正距円筒図法反射マッピングの実装
元の1枚のテクスチャを以下のように読み込みます。
// 正距円筒図法反射マッピングのテクスチャの読み込み
const textureLoader = new THREE.TextureLoader()
textureEquirec = textureLoader.load('comfy_cafe_1k.jpg')
textureEquirec.mapping = THREE.EquirectangularReflectionMapping
textureEquirec.magFilter = THREE.LinearFilter
textureEquirec.minFilter = THREE.LinearMipMapLinearFilter
・magFilter : テクセルが複数ピクセルをカバーする場合のテクスチャのサンプリング方法。
・minFilter : テクセルが1ピクセル未満をカバーする場合のテクスチャのサンプリング方法。
「magFilter」の値は、次のとおりです。
・LinearFilter : テクスチャ座標に最も近い4つのテクスチャ要素値の加重平均。
・NearestFilter : テクスチャ座標に最も近いテクスチャ要素値。
「minFilter」の値は、次のとおりです。
・NearestMipmapNearestFilter : テクスチャリングされるピクセルのサイズに最も近いミップマップを選択し、NearestFilter基準を使用してテクスチャ値を生成。
・NearestMipmapLinearFilter : テクスチャリングされるピクセルのサイズに最も近い2つのミップマップを選択し、NearestFilter基準を使用して各ミップマップからテクスチャ値を生成。
・LinearMipmapNearestFilter : テクスチャリングされるピクセルのサイズに最も近いミップマップを選択し、LinearFilter基準を使用してテクスチャ値を生成。
・LinearMipmapLinearFilter : テクスチャリングされるピクセルのサイズに最も近い2つのミップマップを選択し、LinearFilter基準を使用して各ミップマップからテクスチャ値を生成。

【おまけ】 VRMの環境マッピング
「three.js で VRM を表示する (1) - 事始め」をベースに環境マッピングを追加します。
VRMモデルに環境マッピングを追加するには、VRMモデルのマテリアルをBlenderなどで「プリンシプルBSDF」にして(今回はメタリック:1、粗さ0で鏡のようにしてます)、VRM.from()でマテリアルインポーター経由でテクスチャ画像を指定します。
// テクスチャ画像
const envMapUrl = [
'posx.jpg',
'negx.jpg',
'posy.jpg',
'negy.jpg',
'posz.jpg',
'negz.jpg',
]
// マテリアルインポーターの準備
let ongoingRequestEnvMap = undefined
function requestEnvMap() {
// 環境マップのキャッシュを返す
if (ongoingRequestEnvMap) return ongoingRequestEnvMap
// 環境マップの読み込み
const loader = new THREE.CubeTextureLoader()
ongoingRequestEnvMap = new Promise((resolve, reject) => {
loader.load(
envMapUrl,
(texture) => {
scene.background = texture // スカイボックスにテクスチャ指定
resolve(texture)
},
undefined,
(error) => reject(error)
)
})
return ongoingRequestEnvMap
}
// VRMの読み込み
const loader = new GLTFLoader()
loader.load('./alicia.vrm',
(gltf) => {
VRM.from(gltf, {materialImporter: new VRMMaterialImporter({requestEnvMap})}).then( (vrm) => {
// シーンへの追加
scene.add(vrm.scene)
})
}
)
