Three.js備忘録(2)

目次
  1. setAnimationLoop
  2. ローダーについて
  3. テクスチャ
    1. TextureLoader
    2. フィルタリング
    3. 繰り返し・オフセット・回転
    4. マッピング
  4. MatCap
  5. 3Dテキスト
  6. シーングラフ
  7. lil-guiの使い方
  8. レンダーターゲット

setAnimationLoop

setAnimationLoop」について
アニメーションのループを簡単に設定するためのものです。このメソッドは、通常のJavaScriptのwindow.requestAnimationFrameと同じように機能しますが、Three.js独自の最適化と共に動作します

window.requestAnimationFrameの場合

function animate() {
  // 何らかのアニメーション処理

    renderer.render(scene, camera);
    window.requestAnimationFrame(tick);
}
animate();

setAnimationLoopの基本的な使い方
効率的な瞬間にanimate関数を呼び出します

//animateという名前の関数を作成
//この関数内でオブジェクトを動かしたり、何らかのアニメーション処理を行います
function animate() {
  // アニメーション処理 例
  mesh.rotation.x += 0.01;
  mesh.rotation.y += 0.01;

  // レンダリング
  renderer.render(scene, camera);
}
//animate関数をsetAnimationLoopメソッドに渡します
renderer.setAnimationLoop(animate);

//アニメーションループを停止する場合は、nullを渡します
renderer.setAnimationLoop(null);

アニメーションを固定の数字(例えば、毎フレームオブジェクトを10ピクセル動かすなど)で制御している場合、デバイスの性能やフレームレートの違いによって、実際に見るアニメーションの速さが異なります

THREE.ClockとgetElapsedTimeメソッドを使用することで、異なるデバイスや環境でもアニメーションの進行速度(タイミング)を一定に保つことが可能になります
THREE.Clockは内部で経過時間を計測し、getElapsedTimeメソッドを使ってその時間を取得できます

// THREE.Clockのインスタンスを作成
const clock = new THREE.Clock();

function animate() {
    // 経過時間を取得
    const elapsedTime = clock.getElapsedTime();
    
    // elapsedTimeを使用してアニメーション(例:オブジェクトの回転)
    // 経過時間に基づいてオブジェクトを回転させます。
    mesh.rotation.x = elapsedTime; //経過時間に直接設定

    renderer.render(scene, camera);
}
renderer.setAnimationLoop(animate);
const clock = new THREE.Clock()
let oldElapsedTime = 0
function animate(){
    const elapsedTime = clock.getElapsedTime()
//delteTime(前のフレームと現在のフレームとの間の時間差)を取得する
    const deltaTime = elapsedTime - oldElapsedTime
    oldElapsedTime = elapsedTime
    mesh.rotation.x += deltaTime; //フレーム毎にdelteTimeを加算する
    renderer.render(scene, camera);
}
renderer.setAnimationLoop(animate);

ローダーについて

ローダーは「テクスチャ、3Dモデル、音声ファイルなど」の外部リソースを読み込むために使用され、読み込むファイルの種類に応じて異なる種類があります

ローダー(THREE.TextureLoader、THREE.JSONLoader、THREE.GLTFLoaderなど)のloadメソッドについて

const loader = new THREE.TextureLoader();
loader.load(
  'ファイルのパス',
  //読み込みが完了したときに呼び出されるコールバック関数
  function (geometry, materials) {
    // ロード完了時の処理
  },
  // 読み込み中に呼び出されるコールバック関数
  function (xhr) {
    console.log((xhr.loaded / xhr.total * 100) + '%');
  },
  // 読み込みエラー時に呼び出されるコールバック関数
  function (error) {
    console.error(error);
  }
);

複数のファイルの読み込みを監視する場合

LoadingManagerは「テクスチャ、モデル、音声ファイル」などの読み込み中に進行状況を監視するためのオブジェクトです
リソースが完全に読み込まれたときに、その読み込みが失敗した場合に、または読み込み中にエラーが発生した場合に、適切な処理を実行できます

// LoadingManagerを作成する
const loadManager = new THREE.LoadingManager();
// すべてのリソースの読み込みが完了したときに呼び出される関数
loadManager.onLoad = function () {
    console.log('読み込みが完了しました。');
};
// 1つのリソースの読み込みが完了したときに呼び出される関数
loadManager.onProgress = function (url, itemsLoaded, itemsTotal) {
    console.log('読み込み中: ' + url + ' ' + itemsLoaded + '/' + itemsTotal);
};
// 読み込みが失敗したときに呼び出される関数
loadManager.onError = function (url) {
    console.log('読み込みエラー: ' + url);
};

テクスチャ

テクスチャとは3Dオブジェクトの表面に張り付ける2Dイメージのことです。テクスチャを用いることで、物体に色や模様、質感を与えることができます

テクスチャは最もメモリを使用する部分で最適化が重要です

サイズの重要性

  • GPUメモリ制限: Three.jsでテクスチャを扱う際には、各ピクセルがGPU(グラフィックス処理ユニット)に格納されます。GPUのストレージ容量は限られているため、テクスチャのサイズをできるだけ小さくすることが推奨されます。
  • 「ミップマッピング」 により、テクスチャの総ピクセル数が増加するため、更にGPUメモリが必要になります。

解像度は2のべき乗が望ましい

  • Three.jsでは、テクスチャの幅と高さが2のべき乗であることが推奨されます(例: 512×512、1024×1024、512×2048など)。
  • 2のべき乗でないサイズのテクスチャを使用すると、Three.jsはそのサイズを最も近い2のべき乗に自動的に調整しようとします。この処理により、テクスチャの品質が低下する可能性があり、コンソールに警告が表示される場合もあります。

備考
ミップマッピング」は、同じテクスチャを複数の異なる解像度で保存して、遠くにあるオブジェクトに低解像度のテクスチャを使用することで、パフォーマンスと見た目の両方を向上させるテクニックです
異なる解像度のテクスチャを予め用意しておき、視点や距離によって最適なものを選びます

テクスチャの種類と対応するプロパティ
*テクスチャは多くの場合、グレースケール(白、黒、その間の灰色)で表現され、色ではなく特定の物理的性質や挙動(例:反射、凹凸、透明度など)をコントロールできるのです

  • 通常のマップ(Color or Albedo Map)
    プロパティ: map
    3Dオブジェクトの表面に基本的な色や模様を貼り付けるために使用されるテクスチャです。
  • スペキュラマップ(Specular Map)
    プロパティ: specularMap
    3Dオブジェクトの表面の光沢感を制御するために使用されます。
  • バンプマップ(Bump Map)
    プロパティ: bumpMap
    3Dオブジェクトの表面に微妙な凹凸を加えるために使用されるテクスチャです。
  • ノーマルマップ(Normal Map)
    プロパティ: normalMap
    バンプマップよりも高度に表面の凹凸を制御するために使用されます。
  • アルファマップ(Alpha Map)
    プロパティ: alphaMap
    3Dオブジェクトの透明度を制御するために使用されるグレースケールテクスチャです。
    白は完全に不透明、黒は完全に透明
  • ライトマップ(Light Map)
    プロパティ: lightMap
    光源からの陰影や明るさを表現するために使用されます。
  • Ambient Occlusion (環境遮蔽 / AOマップ)
    プロパティ: aoMap
    3Dオブジェクトの細かい隙間や曲面に影を作成し、リアルな陰影を表現するために使用されます。
  • Metalness Map (金属マップ)
    プロパティ: metalnessMap
    3Dオブジェクトの各部分がどれくらい「金属的」であるかを制御します。金属部分は反射が多く、非金属部分は反射が少ないです。
  • Roughness Map (粗さマップ)
    プロパティ: roughnessMap
    3Dオブジェクトの表面がどれくらい粗い(または滑らか)であるかを制御します。粗い表面は光を多方向に散乱させ、滑らかな表面は光を反射させます。
  • Height Map (高さマップ)
    プロパティ: 通常は、displacementMap
    3Dオブジェクトの表面に高さ情報を追加します
    明るい部分が高く、暗い部分が低い
  • 環境マップ(Environment Map)
    プロパティ: envMap
    3Dオブジェクトが周囲の環境を反射するようにするために使用されるテクスチャです。

色空間について

  • sRGB:ウェブや一般的なディスプレイで使用される標準的な色空間
    ガンマ補正が適用されており、人間の目の感度に近い方法で色をエンコードします
  • リニアRGB:色の明るさ(輝度)が線形に表現される色空間
    シェーダーでの光や色の計算に適していますが、直接ディスプレイに出力すると自然に見えないため、表示前にsRGBなどに変換する必要があります

Three.jsでは、内部的に色の計算を行う際にリニアRGBを使用しています
テクスチャがsRGB色空間でエンコードされている場合、これをリニア色空間に変換してから内部的に使用します

テクスチャがsRGBデータであることをThree.js に伝え(シェーダーがテクスチャをどのように解釈すべきか、という情報を提供)、内部的にリニア色空間への変換を行います
*注意:デフォルトの renderer.outputEncoding (レンダリングプロセスの最終段階での出力)の設定は THREE.LinearEncodingです

const texture = new THREE.TextureLoader().load('path/to/image.jpg');
texture.encoding = THREE.sRGBEncoding;

TextureLoader

TextureLoader
シーンのbackgroundにテクスチャを使用したり
マテリアルでテクスチャを使用したりできます(mapプロパティに設定します)

const loader = new THREE.TextureLoader();
const texture = loader.load('ファイルパス');
//シーンのbackground
scene.background = texture;
//マテリアルでテクスチャを使用
const material = new THREE.MeshBasicMaterial({
  map: texture,
});

画像は非同期に読み込まれます
テクスチャの読み込み完了までまつ場合
loadメソッドにテクスチャの読み込みが完了したときに呼び出されるコールバックを渡します

loader.load('ファイルパス', (texture) => {
  const material = new THREE.MeshBasicMaterial({
    map: texture,
  });
  const cube = new THREE.Mesh(geometry, material);
  scene.add(cube);
});

複数のテクスチャの読み込み完了を待つ場合
LoadingManagerはローディング処理を管理するオブジェクトです
ローダーのコンストラクターにLoadingManagerオブジェクトを渡すことでローダーがLoadingManagerオブジェクトを使用できます
例:立方体の各面に異なる6つのテクスチャを貼り付ける場合はMeshを作るときに6つのマテリアルの配列を渡します

const loadManager = new THREE.LoadingManager();
const loader = new THREE.TextureLoader(loadManager); 
//6つのマテリアルの配列
const materials = [
  new THREE.MeshBasicMaterial({map: loader.load('1.jpg')}),
  new THREE.MeshBasicMaterial({map: loader.load('2.jpg')}),
  new THREE.MeshBasicMaterial({map: loader.load('3.jpg')}),
  new THREE.MeshBasicMaterial({map: loader.load('4.jpg')}),
  new THREE.MeshBasicMaterial({map: loader.load('5.jpg')}),
  new THREE.MeshBasicMaterial({map: loader.load('6.jpg')}),
]; 
loadManager.onLoad = () => {
  const cube = new THREE.Mesh(geometry, materials);
  scene.add(cube);
};

複数のマテリアルに対応しているジオメトリ

  • BoxGeometryは6つのマテリアル
  • ConeGeometryは2つのマテリアル(底面と円錐面)
  • CylinderGeometryは3つのマテリアル(底面と上面と側面)

テクスチャをダウンロードできるサイト

フィルタリング

フィルタリングとミップマッピングは、3Dグラフィックスにおいてテクスチャを効果的に表示するための重要な概念です

フィルタリングはテクスチャが3Dモデルにマッピングされる際に、どのようにサンプリングされるかを決定する処理
一般的な方法としては「ニアレストネイバー(Nearest Neighbor)」と「線形フィルタリング(Linear Filtering)」があり、通常、「線形フィルタリング(Linear Filtering)」がデフォルトで使用されます
ニアレストネイバーは単純だが荒く、線形フィルタリングは滑らかだが計算が多いです

テクスチャが元の大きさより大きく描かれたときのフィルタ設定(テクスチャが拡大される際)は
texture.magFilter属性に値を設定

  • THREE.NearestFilter
    テクスチャの最も近いピクセルを選ぶ
  • THREE.LinearFilter
    テクスチャから4ピクセルを選んで混ぜ合わせる
    *デフォルト

元の大きさよりもテクスチャが小さく描画された時のフィルタ設定(テクスチャが縮小される際)は
texture.minFilter属性に値を設定

  • THREE.NearestFilter
    テクスチャの最も近いピクセルを選ぶ
  • THREE.LinearFilter
    テクスチャから4ピクセルを選んで混ぜ合わせる
  • THREE.NearestMipmapNearestFilter
    適切なMIPを選びピクセルを一つ選ぶ
  • THREE.NearestMipmapLinearFilter
    2つMIPを選びそれぞれからピクセルを選んでその2つを混ぜる。
  • THREE.LinearMipmapNearestFilter
    適切なMIPを選び4ピクセルを選んで混ぜ合わせる
  • THREE.LinearMipmapLinearFilter
    2つMIPを選びそれぞれから4ピクセルを選んで8つ全部を混ぜ合わせて1ピクセルにする
    *デフォルト

THREE.NearestFilterは最も近いピクセルの値をそのまま使用するため、ミップマップの生成や使用は通常不要です。
ミップマップはテクスチャの異なる解像度を用意する技術であり、線形フィルタリングと組み合わせることでエイリアシングを防ぐ効果がありますが、THREE.NearestFilterではその効果が得られません
パフォーマンスを最優先するケースやピクセルアートや低解像度のテクスチャを使用している場合に有用です

texture.generateMipmaps = false //mipmapsを生成する処理をスキップしてGPUの負荷を軽減
texture.minFilter = THREE.NearestFilter

注意:最適なテクスチャの設定は目的によります。
ミップマッピングは遠近感をリアルに出すのに役立ちます
上記の設定は、高品質なレンダリングやリアルな遠近感が必要な場合には向いていません
高品質な描画を求める場合には他の設定を検討するべきです。

繰り返し・オフセット・回転

繰り返しす場合
*デフォルトのテクスチャは繰り返しなし

  • wrapS:水平方向のラッピング属性
  • wrapT:垂直方向のラッピング属性

属性の値

  • THREE.ClampToEdgeWrapping
    最後のピクセルが永遠に繰り返される
  • THREE.RepeatWrapping
    テクスチャが繰り返される
  • THREE.MirroredRepeatWrapping
    テクスチャの鏡像が取られて繰り返される
texture.wrapS = THREE.RepeatWrapping;

繰り返しはrepeat属性で設定

texture.repeat.set(水平方向に繰り返す回数, 垂直方向に繰り返す回数);

テクスチャのオフセットはoffset属性で設定
「0 」はオフセットなし「1」 はテクスチャ全体の大きさ

texture.offset.set(xOffset, yOffset);

テクスチャの回転はrotation属性(ラジアンで指定)
center属性で回転の中心を指定(デフォルトは0,0で左下の角で回転・.5, .5でテクスチャの中心での回転)

texture.center.set(.5, .5);
//45 * Math.PI / 180
texture.rotation = THREE.MathUtils.degToRad(45);

マッピング

3Dオブジェクトの表面に2Dのテクスチャや色情報を貼り付ける

「UV座標」を使用して、3Dモデルの各頂点を2Dテクスチャ上の特定の位置にマッピングします
「UVアンラッピング」を行うことで、3Dモデルの各頂点にUV座標を割り当てることができます

UVアンラッピング」は、3Dモデルの表面を2D平面に「展開」するプロセスです
3Dモデルの各頂点は2Dテクスチャ上の特定の位置にマッピングされます
例えば、キューブにテクスチャを適用することは比較的簡単ですが、球、円錐、トーラスなど複雑なジオメトリのような複雑な形状の場合、UVアンラッピングは複雑です
複雑なジオメトリのUVアンラッピングを効果的に行うためには、3Dモデリングソフトウェア(例えばBlendeなど)を使用するのが一般的です。

UV座標」は、3Dモデルの他の属性(頂点座標、法線ベクトルなど)と同様にジオメトリに関連付けられる2Dテクスチャ上の特定の点を指定するための座標です(属性)。
U座標はテクスチャの水平方向を表し、V座標はテクスチャの垂直方向を表します。UV座標は通常、0から1の範囲で指定され、この範囲内での座標値はテクスチャの対応する位置を指し示します。

参考まで
複雑な「3Dモデル」にリアルなテクスチャを適用する

  • UVアンラッピング:
    最初に、3Dモデリングソフトウェア(例: Blenderなど)を使用して3DモデルのUVアンラッピングを行います。このプロセスで、3Dモデルの各頂点にUV座標(テクスチャ上の位置)が割り当てられ、3Dモデルの表面が2D平面に展開されます。
  • テクスチャ画像の作成または編集:
    UVアンラッピングの結果を基にして、2Dテクスチャ画像を作成または編集します。このテクスチャ画像は、アンラップされた3Dモデルの2D表現に基づいています。テクスチャ画像は、色、模様、詳細など、3Dモデルの表面の外観を定義します。
  • テクスチャの適用:
    作成または編集された2Dテクスチャ画像を3Dモデルに適用します。この時点で、UV座標が重要な役割を果たし、テクスチャ画像のどの部分が3Dモデルのどの部分にマッピングされるかを指定します。

MatCap

3Dシーンでリアルまたはスタイリッシュなマテリアルと照明の設定を作成するには、多くの計算リソースと専門的な知識が必要ですが、「MatCap」はマテリアルの属性(色、質感など)と照明を一つのテクスチャに統合します。
これにより照明や反射を設定する必要がなく、計算リソースを大幅に削減できます

GitHubのmatcapsリポジトリ(多数のMatCapテクスチャを提供しています)を利用する
これらのテクスチャはPNGまたはZMT形式で提供され、個々にダウンロードすることができます
https://github.com/nidorx/matcaps

//MeshMatcapMaterialを使用する
const material = new THREE.MeshMatcapMaterial( { matcap: matcapTexture } );

3Dテキスト

TextGeometryクラスは、3Dテキストジオメトリを簡単に作成できるツールです。
ただし、このクラスを使用する際には、特定のJSON形式(typeface形式)で指定するフォントが必要です

TextGeometry(‘テキスト’, オプション)
独自でダウンロードしたフォントはFacetype.jsでjsonに変換
*ライブラリに含まれるフォント
node_modules/three/examples/fonts/フォルダ内にいくつかのサンプルフォントが含まれています
https://github.com/mrdoob/three.js/tree/master/examples/fonts
jsonファイルを THREE.FontLoader()で読み込む必要がある

const loader = new THREE.FontLoader();
loader.load('./helvetiker_regular.typeface.json', (font) => {
  const textGeometry = new THREE.TextGeometry('three.js', {
    font: font,
    size: 0.5, 
    height:0.1
  });
  const textMaterial = new THREE.MeshBasicMaterial({color:0xffffff});
  const text = new THREE.Mesh(textGeometry, textMaterial);
  scene.add(text);
});

オプション

  • font:使用されるフォント
  • size :テキストのサイズ(デフォルトは 100)
  • height: テキストを押し出す太さ(デフォルトは50)
  • curveSegments : : テキストの曲線部分の滑らかさを制御します。値が大きいほど滑らかになりますが、計算コストも増えます
  • bevelEnabled : テキストのエッジにベベル(斜めのエッジ)を付けるかどうかを決定します。trueに設定するとベベルが有効になります
  • bevelThickness :ベベルの厚さを指定します。この値を大きくするとベベルが太くなります(デフォルトは10)
  • bevelSize : ベベルのサイズを指定します。この値を大きくするとベベルの幅が広がります(デフォルトは8)
  • bevelOffset : ベベルのオフセットを指定します。通常は0のままで問題ありません(デフォルトは0)
  • bevelSegments: ベベルの滑らかさを制御します。この値を増やすとベベルが滑らかになります(デフォルトは3)

注意:Three.jsのバージョン133以降においてTextGeometryやFontLoaderのインポートとインスタンス化が変わっています
以前はThree.jsのコアライブラリに含まれていましたが、133以降は別のディレクトリです

import { FontLoader } from 'three/examples/jsm/loaders/FontLoader.js';
import { TextGeometry } from 'three/examples/jsm/geometries/TextGeometry.js';
const fontLoader = new FontLoader();
const textGeometry = new TextGeometry();

シーングラフ

Sceneクラスは3Dシーンのルートノードとして機能します
他のノードを子ノードとして持つことができ、オブジェクトの階層構造を柔軟に構築することができます

THREE.GroupもしくTHREE.Object3Dはシーングラフで使用されるオブジェクトクラスです
複数の3Dオブジェクトをまとめて位置、回転、スケーリング、親子関係などの変換情報を保持できます
THREE.GroupもしくTHREE.Object3Dで「空のノード(ローカルな空間)」を作り、そこにaddメソッドで子要素となる物体を追加したりできます
*シーンの座標とは別のローカルな座標ができるイメージです

子の位置は親の位置から相対的になる
*親オブジェクトが移動すると、その子オブジェクトも自動的に移動します

//空のノード
const parent = new THREE.Group();
scene.add(parent);

//parentに物体を追加
parent.add(物体);

lil-guiの使い方

 npmでインストール

npm install lil-gui --save-dev
import GUI from 'lil-gui'

CDNを使う

import GUI from 'https://cdn.jsdelivr.net/npm/lil-gui@0.18/+esm';
<script src="https://cdn.jsdelivr.net/npm/lil-gui@0.18"></script>
<script>
  const GUI = lil.GUI
</script>

プロパティのデータ型に基づいて適切なコントローラーが選択されます

const gui = new GUI();
obj = {
	myBoolean: true,
	myNumber: 1,
	myFunction: function() { alert( 'hi' ) },
	myColor: '#ffffff',
}
//コントロールパネルに表示を追加
gui.add( obj, 'myBoolean' ); 	// チェックボックス
gui.add( obj, 'myNumber' 0, 100, 10 ); //数字 (最小値,最大値,間隔)
gui.add( obj, 'myFunction' ); 	// ボタン
gui.addColor( obj, 'myColor' ); //カラー ピッカー

addFolder()
コントローラを折りたたみ可能なグループに編成

const folder = gui.addFolder( 'Position' );
folder.add( obj, 'x' );

onChangeメソッド
コントローラーが変更されるたびに関数を呼び出す
onFinishChangeメソッド
変更が確定した時点で関数を呼び出す

gui.add( params, 'foo' ).onChange( value => {
	console.log( value );
} );

地球とテキストを異なるスピードで回転させるために
親コンテナに「地球」と「テキストを子要素に持つオブジェクト」を子要素に追加してそれぞれを回転させる
*親は子と同じ位置でそれぞれを異なる速度で回転できる

レンダーターゲット

three.jsではデフォルトで画面(canvas要素)がレンダーターゲットに設定されていますが、これを変更することで、画面以外の場所にもレンダリングされた結果を描画することができます

メモリ上にレンダリングした結果をテクスチャとして使用します

WebGLRenderTarget
レンダーターゲットを作成
*横幅と高さが小さいと映像が粗くなります

const rtWidth = 1000;
const rtHeight = 1000;
const renderTarget = new THREE.WebGLRenderTarget(rtWidth, rtHeight);

オフスクリーンレンダリング用のシーンとカメラ
*カメラのアスペクトはレンダーターゲットのアスペクトに設定

const rtCamera = new THREE.PerspectiveCamera(75, rtWidth / rtHeight, 0.1, 5);
rtCamera.position.z = 2;
 
const rtScene = new THREE.Scene();
rtScene.background = new THREE.Color('red');

オフスクリーンのレンダリング

renderer.setRenderTarget(renderTarget);
renderer.render(rtScene,rtCamera);
renderer.setRenderTarget(null);

マテリアルにレンダーターゲットを適応する場合の例

const material = new THREE.MeshPhongMaterial({
  map: renderTarget.texture,
});