環境マップ
環境マップは3D空間において環境の光や影響を表現するために使用されるテクスチャです
*周囲の光や物体の反射、屈折、散乱などの影響を反映できる
環境マップにHDR(High Dynamic Range)画像を使用することは理想的です
Three.jsでは、これらのHDR画像を読み込むためには適切なローダーを使用し、テクスチャの.encodingプロパティを適切に設定する必要があります
環境マップとして使用する場合、テクスチャには適切なマッピング方法を設定する必要があります
HDR画像の読み込みとトーンマッピング
HDR(High Dynamic Range)画像は、従来のLDR(Low Dynamic Range)画像よりも広いダイナミックレンジを持っています
.hdrや.exrなどのファイル形式で保存されます
通常のディスプレイは、比較的狭いダイナミックレンジしか持っていないためHDR画像をそのまま使用すると明るさが失われたり色が歪んだりする可能性があり、通常はトーンマッピング(通常のディスプレイで表示可能な範囲に変換する処理)が必要です
ファイル形式とローダー、エンコーディング(色空間の取り扱い)について
Three.jsでは、テクスチャはデフォルトでsRGBEncodingに設定されており、sRGB色空間でエンコードされた画像のために最適化されています
*sRGB色空間はWeb上で一般的に使用される色空間です
HDR画像は、広いダイナミックレンジを持ち、色情報は線形色空間でエンコードされることが多いです
Three.jsでHDR画像を扱う場合、エンコーディングタイプは画像の形式によって異なります
HDR(High Dynamic Range)画像は、ファイルサイズが大きくなりがちなので、使用するテクスチャのサイズはなるべく小さくすることが推奨されます
- .hdr形式の画像はTHREE.RGBELoaderを使って読み込み
.encodingプロパティをTHREE.RGBEEncodingに設定 - .exr形式の画像はTHREE.EXRLoaderを使用し、
通常は.encodingプロパティをTHREE.LinearEncodingに設定
トーンマッピングは、HDRの明るさの範囲を、通常のディスプレイで扱えるLDR(Low Dynamic Range)に変換する処理ですrenderer.toneMapping
プロパティで設定できます
- THREE.NoToneMapping:デフォルト設定、トーンマッピングを行いません
- THREE.LinearToneMapping:リニアなトーンマッピングを行います
- THREE.ReinhardToneMapping:Reinhardトーンマッピングを行い、より現実的な結果を得ることができますが、色が洗いざらしに見えることがあります
- THREE.CineonToneMapping:Cineonログフィルムのようなトーンマッピング
- THREE.ACESFilmicToneMapping:映画で使用されるACESカラースペースを模倣したトーンマッピング
renderer.toneMappingExposure
は、Three.js でシーンの露出レベルを調整するためのプロパティで、シーンの全体的な明るさを制御でき特にトーンマッピングが適用されている場合に有効です
値を増加させると、シーンはより明るくなり、値を減少させると暗くなります
renderer.outputEncoding = THREE.sRGBEncoding; // レンダラーの出力をsRGB色空間に設定。
renderer.toneMapping = THREE.ACESFilmicToneMapping; // トーンマッピングをACESFilmicに設定。
renderer.toneMappingExposure = 2; // トーンマッピングの露光量を調整。
// HDRファイルのロードを開始。
const loader = new RGBELoader();
loader.load(
"image.hdr", // 実際のHDRイメージのパスに置き換える。
(texture) => {
texture.encoding = THREE.RGBEEncoding; // テクスチャのエンコーディングをRGBE形式に設定。
/*
テクスチャマッピングを矩形反射マッピングに設定
*テクスチャマッピングについては環境マップの使用例を参照
*/
texture.mapping = THREE.EquirectangularReflectionMapping;
scene.background = texture; // ロードしたテクスチャをシーンの背景として設定。
scene.environment = texture; // ロードしたテクスチャを環境マップとしても設定
},
(xhr) => { // ローディングの進行状況をハンドルできる
console.log((xhr.loaded / xhr.total * 100) + '% loaded');
},
(error) => { // エラーハンドリング用のコールバック
console.error('An error happened while loading the HDR texture:', error);
}
);
マッピング方法の種類
環境マップには一般に2つの主要なタイプがあります
- キューブマップ
これは6面の正方形のテクスチャからなるもので、各面が3D空間の異なる方向を表しています(上、下、左、右、前、後)
キューブマップは立体感を持たせるのに適しており、特に反射と屈折の表現によく使用されます
THREE.CubeTextureLoader を使用してキューブマップテクスチャを読み込みます
*スカイボックスを参照 - 球面マップ(Equirectangular Map)
これは360度の全天周の画像を平面に展開したもので、通常は正距円筒図テクスチャとして表されます
球面マップは環境全体を一つの2D画像にマッピングするときに使われ、HDR画像でよく見られます
処理コストが高くなりがちです
キューブマップは古くからある手法で、制御が簡単です
球面マップ(特にHDRでのEquirectangular Map)は、リアルなライティング効果を得ることができますが、計算コストは高く処理が複雑になることがあります
画像を平面に展開した正距円筒図形式の画像
*https://matheowis.github.io/HDRI-to-CubeMap/ では、HDRI画像をキューブマップに変換するためのウェブベースのツールが提供されています
余談:6枚の画像から球面マッピングも可能です
//6枚の画像から球面マッピング
const reflectionTexture = new THREE.CubeTextureLoader()
.setPath('path/to/textures/')
.load([
'1.jpg', '2.jpg',
'3.jpg', '4.jpg',
'5.jpg', '6.jpg'
]);
material.envMap = reflectionTexture;
material.envMap.mapping = THREE.SphericalReflectionMapping;
環境マップの使用例
- EquirectangularReflectionMapping
これは球面マッピングの一種で、360度のパノラマ画像を使用してオブジェクトの反射をシミュレートする方法です
オブジェクトの表面が環境を鏡のように反射するように見せる効果を作り出します - EquirectangularRefractionMapping
これも球面マッピングの一種ですが、こちらはオブジェクトの屈折をシミュレートするために使用されます
オブジェクトがガラスのように透明で、背後の景色を歪ませる効果を表現するのに使われます - CubeRefractionMapping:
キューブマップを使用して屈折効果をシミュレートする方法です
これにより、オブジェクトが透明な物質でできており、その内部を通過する光が屈折しているように見せることができます
シーンに環境マップを設定する場合
オブジェクトに環境マップを設定する場合
const loader = new RGBELoader();
loader.load(
"ファイルパス",
(texture) => {
texture.encoding = THREE.RGBEEncoding;
texture.mapping = THREE.EquirectangularReflectionMapping;
scene.background = texture;
/*シーンに環境マップを設定する場合
scene.environment = texture;*/
const sphere = new THREE.Mesh(
new THREE.SphereGeometry(0.3, 50, 50),
new THREE.MeshStandardMaterial({
roughness: 0,
metalness: 0.8,
color: 0xffea00,
//オブジェクトに環境マップを設定する場合
envMap: texture,
})
);
scene.add(sphere);
}
);
スカイボックス
スカイボックスは3D空間に背景を描画するための技術です
6つの面から成る正立方体の形をした画像データを用いて周囲の景色を表現します
スカイボックスの作成方法は、キューブマップを用いることが一般的です
cubeTextureLoader
はキューブマップテクスチャを読み込むことができます
6面のテクスチャ画像(上・下・左・右・前・後)で360度パノラマコンテンツができます
CubeTextureLoader を使用して6つの画像からなるキューブマップをロードし、scene.background
に設定します
const cubeTextureLoader = new THREE.CubeTextureLoader();
scene.background = cubeTextureLoader.load([
'ファイルパス',
'ファイルパス',
'ファイルパス',
'ファイルパス',
'ファイルパス',
'ファイルパス'
]);
- backgroundBlurrinessプロパティは、シーンの背景のぼかし(ブラー)の程度を制御します
背景の解像度が低い場合や、ユーザーに前景のオブジェクトに注目してもらいたい場合に有効 - backgroundIntensityプロパティは、背景の明るさを制御します
このプロパティは背景の明るさにのみ影響し、環境マップの明るさには影響を与えません
THREE.WebGLCubeRenderTarget
(size, options)はキューブマップのターゲットを作成するためのクラスですfromEquirectangularTexture
で正距円筒図テクスチャからキューブマップテクスチャを生成できます
スカイボックスとして背景に設定したり
環境マップに適用することで反射の見た目を再現できます
const loader = new THREE.TextureLoader();
loader.load(
'ファイルパス',
(texture) => {
const rt = new THREE.WebGLCubeRenderTarget(texture.image.height);
rt.fromEquirectangularTexture(renderer, texture);
scene.background = rt.texture;
//キューブ
const geometry = new THREE.BoxGeometry(1, 1, 1);
const material = new THREE.MeshStandardMaterial({
roughness: 0.5,
metalness: 0.2,
color: 0x00ffff,
envMapIntensity: 1, // 環境マップの反射強度
envMap: texture, //反射マッピング
});
const cube = new THREE.Mesh(geometry, material);
scene.add(cube);
}
});
アンチエイリアシングを有効にすると、エッジが滑らかに表示されるようになります
WebGLRenderer の antialias プロパティを true に設定することで、アンチエイリアシングを有効にできます
*アンチエイリアス機能を使うとある程度のリソースを消費します。ピクセル比率が1を超える画面ではアンチエイリアスが不要だと言われています
const renderer = new THREE.WebGLRenderer({ antialias: true });
//高ピクセル比のデバイスではパフォーマンスの問題が発生するので2に制限する
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2))
地面投影スカイボックス
環境マップを背景として使用するときにオブジェクトが空中に浮いているように見えますが、地面投影スカイボックスは、環境マップを地面に投影して、オブジェクトが実際にその環境に存在しているかのように見せることができます
*GroundProjectedSkybox クラスをインポート
import { GroundProjectedSkybox } from 'three/addons/objects/GroundProjectedSkybox.js'
const rgbeLoader = new RGBELoader();
rgbeLoader.load('/path/to/your/hdr/image.hdr', (texture) => {
texture.mapping = THREE.EquirectangularReflectionMapping;
scene.environment = texture; // 環境マップをシーンにセット
// GroundProjectedSkyboxをシーンに追加
const groundProjectedSkybox = new GroundProjectedSkybox(texture);
groundProjectedSkybox.scale.setScalar(20); // 必要に応じてスケールを調整
scene.add(groundProjectedSkybox);
groundProjectedSkybox.radius = 100 // 必要に応じて調整
groundProjectedSkybox.height = 10 // 必要に応じて調整
// 環境マップをマテリアルにセット
const geometry = new THREE.BoxGeometry(1, 1, 1);
const material = new THREE.MeshStandardMaterial({
roughness: 0.5,
metalness: 0.8,
color: 0x00ffff,
envMapIntensity: 1,
envMap: texture,
});
const cube = new THREE.Mesh(geometry, material);
scene.add(cube);
// 既存のcubeのマテリアルを更新
cube.material = material;
});
*うまく機能しない場合もある
余談:プログレスバーを表示する
*ローダーのハンドリングについてはThree.js備忘録(2)を参照
<progress id="progressbar" value="" max="100"></progress>
const progressBar = document.querySelector('#progressbar');
//読み込み中に呼び出されるコールバック関数内で
(xhr) => {
progressBar.value = (xhr.loaded / xhr.total) * 100;
},
//複数のローダーでハンドリング
const manager = new THREE.LoadingManager();
// テクスチャローダー
const textureLoader = new THREE.TextureLoader(manager);
// GLTFモデルローダー
const gltfLoader = new THREE.GLTFLoader(manager);
manager.onProgress = function (url, loaded, total) {
progressBar.value = (loaded / total) * 100;
};
//完了後
//プログレスバーを非表示にする
progressBar.style.display = "none";
3Dモデルの描画
3Dモデルのファイル形式には、多数の種類があり
three.jsで扱える代表的な3Dモデルのファイル形式
- GLTF / GLB
GLTF (GL Transmission Format) とそのバイナリ形式である GLB は、three.js で最も推奨されるフォーマットです
リアルタイムレンダリングに最適化されており、テクスチャ、マテリアル、アニメーションなどもサポートしています - OBJ
OBJ は古いが広くサポートされている形式で、テクスチャとマテリアルも .mtl ファイルと一緒に読み込むことができます
アニメーションはサポートしていないのが一般的です - FBX
FBX はAutodeskが開発した形式で、マテリアル、テクスチャ、アニメーションなど、複雑なデータも扱えます
とりあえず確認したいときは、ファイルをThree.js editorにアップロードすると便利です
*「add」→「DirectionalLight」を追加する
GLTFファイルの描画
GLTFにはテキストベース(.gltf)とバイナリベース(.glb)の2つの形式があります
- (.gltf) 基本的なフォーマット
メタデータはJSONファイル(.gltf)に保存され、ジオメトリやテクスチャは別のファイル(.binや画像ファイル)に保存されます - (.glb): すべての情報が1つのバイナリファイルに統合されています
これはロードが速いですが、テクスチャやジオメトリを後で編集できません
GLTFの拡張機能
- glTF-Draco: Draco圧縮アルゴリズムを使用してジオメトリデータを圧縮する拡張機能です
*DRACOLoaderで解凍する必要があります - glTF-Embedded: この形式では、JSON ファイル内にすべての資産(テクスチャ、ジオメトリ等)が埋め込まれています
3Dオブジェクトのシーングラフをコンソールで確認してみる
*テクスチャ用の画像やbinがあれば同じGLTFファイルと同じディレクトリに配置する
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js'
const loader = new GLTFLoader();
loader.load(
'ファイルパス',
(gltf) =>{
//gltf.sceneは、シーングラフでモデルのルートノードに相当
const root = gltf.scene
console.log(root)
scene.add(root);
});
gltf オブジェクトの構造
- scene: モデルの階層構造が含まれており、モデルのメッシュやスキン、アニメーションなどが含まれる
- scenes: GLTFファイルに複数のシーンが含まれている場合、ここにリストされる
- cameras: GLTFファイルに含まれるカメラを表す
- animations:エクスポートされたアニメーションデータを含んでいます。これらを THREE.AnimationMixer と組み合わせることで、アニメーションを再生できる
- assets: ロードされたアセットのメタデータ
- parser: GLTFの内容を解析した結果を含むオブジェクトで、ローダーの内部的な使用される
- userData: カスタムユーザーデータが含まれている場合、その情報を含むオブジェクト
name属性からノードを取得(getObjectByName)してマテリアルを変更してみる
const loader = new GLTFLoader();
loader.load(
'ファイルパス',
(gltf) =>{
scene.add(gltf.scene);
//getObjectByNameでノードを取得
root.getObjectByName('Cube_1_0').material.color.set(0xffff00)
}
);
3DオブジェクトをWeb上で使う前に3Dソフトウェアで編集した方がいい理由
3Dモデルは多くの子オブジェクトで構成されることがあります。これらの子オブジェクトを選択して必要なものだけを追加することはできますが、独自の位置やスケールを持っていることが多いので、個々に調整が必要です
また、不要な部分を省くことができます。表示速度が向上します
子オブジェクトを選択する場合
*注意:Three.jsの振る舞いとして、あるシーンまたはグループから子オブジェクトを別のシーンやグループに追加すると、その子オブジェクトは元のシーンやグループから自動的に削除されるのでforループやforEachは使わない
もしくは、別の配列を作成してその配列をループする
//個々に追加
const m1 = gltf.scene.children[0];
const m2 = gltf.scene.children[1];
scene.add(m1);
scene.add(m1);
//childrenをループする場合はforループやforEachは使わない
while(gltf.scene.children.length){
scene.add(gltf.scene.children[0])
}
//別の配列を作成してその配列をループ
const children = [...gltf.scene.children]
for(const child of children){
scene.add(child)
}
*GLTFファイルをインポートする場合、glTFがPBR(Physically-Based Rendering)をベースとして設計されているため、通常はMeshStandardMaterialが用いられるメッシュが作成されます
MeshStandardMaterialはライトがない場合オブジェクトは表示されません
他のマテリアルタイプに変更する
const loader = new THREE.GLTFLoader();
loader.load('ファイルパス', (gltf) => {
const model = gltf.scene;
model.traverse((object) => {
if (object.isMesh) {
// マテリアルを変更
object.material = new THREE.MeshBasicMaterial({ color: 0xff0000 });
}
});
scene.add(model);
});
traverse(…) メソッドは、Three.jsにおいてObject3Dオブジェクト全て(Mesh, Group, Sceneなど)に存在し、そのメソッドを使ってシーン内の全てのオブジェクトを再帰的に走査することができます。
scene.traverse((child) => {
console.log(child)
})
GLTFファイルを読み込んで描画したオブジェクトに影をつける
OBJファイルの描画
MTLファイルはOBJファイルと一緒に使用され、マテリアルの情報が含まれます
- OBJファイルにはジオメトリ情報
- MTLファイルにはマテリアルの情報
MTLファイルがある場合
MTLLoaderを使用してMTLファイルを読み込んだ後、そのマテリアルをOBJLoaderに設定setMaterials
はマテリアルをOBJLoaderに設定するためのメソッドです。
const mtlLoader = new MTLLoader();
const objLoader = new OBJLoader();
mtlLoader.load('hoge.mtl', (materials) =>{
materials.preload();
objLoader.setMaterials(materials);
objLoader.load('hoge.obj', (object) =>{
scene.add(object);
});
},);
OBJファイルにMTLファイルがない場合のマテリアル
OBJファイル単一のオブジェクトです
通常、複数のマテリアルを持つジオメトリを含んでいるため、traverse
メソッドを使用してOBJファイルから読み込まれたマテリアルごとのジオメトリを取得して処理します
const material = new THREE.MeshLambertMaterial({ color: 0x5c3a21 });
objLoader.load(
"hoge.obj",
(object) => {
object.traverse((child) => {
if (child instanceof THREE.Mesh) {
//法線の情報がない場合
child.geometry.computeFaceNormals();
child.geometry.computeVertexNormals();
//マテリアルの設定
child.material = material;
}
});
scene.add(object);
})
ジオメトリの法線の計算
computeFaceNormals
ジオメトリの面の法線は各面の3つの頂点の法線の平均値として計算されます
面が等しい重みを持つ場合に適していますcomputeVertexNormals
頂点の法線は頂点を共有する全ての面の法線の平均値として計算されます
ジオメトリがシームレスでない場合(継ぎ目が目立つ状態)に使用されます
計算量が多い- 外部ツールによる計算
ジオメトリに法線が含まれている場合は、OBJファイルから読み込むことができます
MTLファイルがない場合のOBJファイルを描画
無料でアセットが取得できるサイト
「CC0(パブリックドメイン)」を選択するとクレジット表記不要
HDRも無料でダウンロードできる