プログラミング/JavaScript/three.js/再入門 のバックアップ(No.7)

更新


公開メモ

準備

https://www.jsdelivr.com/package/npm/three

へ行き、Copy HTML + SRI を押すと

LANG:html
<script src="https://cdn.jsdelivr.net/npm/three@0.138.3/build/three.min.js" 
integrity="sha256-TJ+e0PQs7GxkFRYdn1Cc0N0hL5xuh15qCSMqw8kBLDk=" crossorigin="anonymous"></script>

が手に入る。

そこでテスト用には、

LANG:html
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="utf-8">
<script src="https://cdn.jsdelivr.net/npm/three@0.138.3/build/three.min.js" 
integrity="sha256-TJ+e0PQs7GxkFRYdn1Cc0N0hL5xuh15qCSMqw8kBLDk=" crossorigin="anonymous"></script>
</head>
<body>
<!-- ここに html を書く -->
<script>
window.addEventListener("DOMContentLoaded", setup);
function setup() {
// ここにコードを書く
}
</script>
</body>
</html>

のようなひな形を使えば良さそう。

立方体のチュートリアル

レンダリングの基本

公式チュートリアル:
https://threejs.org/docs/index.html#manual/en/introduction/Creating-a-scene

一部こちらも参考にさせていただいて:
https://ics.media/entry/14771/

three.js で描画するには以下の手順を踏むことになる

  1. 描画先を指定してレンダラーを作る
  2. シーンを作る
    • シーンにオブジェクトや光源を追加する
  3. カメラを作る
  4. シーンとカメラを指定してレンダリングする

ここでは、

LANG:html
<canvas id="canvas1" width="800", height="600">

に書き込むことにする。

LANG:js
// ここにコードを書く

// 描画先の canvas エレメント
const canvas = document.querySelector("#canvas1");

// レンダラーを作成 (描画先を指定した)
const renderer = new THREE.WebGLRenderer({canvas: canvas});
renderer.setSize( canvas.width, canvas.height );
renderer.setPixelRatio(window.devicePixelRatio);

// カメラを作成 (画角、アスペクト比、描画開始距離、描画終了距離)
const camera = new THREE.PerspectiveCamera( 
   45, canvas.width / canvas.height, 0.1, 10000 );
camera.position.set(0, 0, +1000); // x, y, z

// シーンを作成
const scene = new THREE.Scene();

// ここでシーンにオブジェクトや光源を追加する
// setup_scene(scene);

// レンダリング
renderer.render(scene, camera);

これで正しくレンダリングされる。
ただし、オブジェクトも光源もないシーンなので真っ暗なまま。

オブジェクトと光源を追加してみる

LANG:js
// ここでシーンにオブジェクトや光源を追加する
setup_objects(scene);
setup_lights(scene);

function setup_lights(scene) {
  // 平行光源
  const light = new THREE.DirectionalLight(0xff80ff);
  light.intensity = 1;
  light.position.set(1, 1, 1);
  scene.add(light); // シーンに追加

  // 環境光
  const ambient = new THREE.AmbientLight( 0x80ffff );
  ambient.intensity = 0.2;
  scene.add(ambient); // シーンに追加
}

function setup_objects(scene) {
   // 箱を作成
  const geometry = new THREE.BoxGeometry(300, 300, 300);              // 形状
  const material = new THREE.MeshStandardMaterial({color: 0x8080ff}); // 単色の材質
  const box = new THREE.Mesh(geometry, material);  // 形状と材質を指定してメッシュを作成
  box.rotation.x = 3.14/4;
  box.rotation.y = 3.14/4;
  scene.add(box); // シーンに追加
}

これで立方体が表示された。

box1.png

アニメーションさせる

一定時間間隔でシーンを変更しつつ renderer.render を呼ぶための機構が備わっている。

LANG:js
// レンダリング
renderer.render(scene, camera);

としていた部分を、

LANG:js
 function renderFrame() {
   renderer.render( scene, camera );

   scene.update(); // シーンを更新する

   // 次フレームの描画を予約
   requestAnimationFrame( renderFrame );
 }
 
 // 次フレームの描画を予約
 requestAnimationFrame( renderFrame );

とするとともに、オブジェクトの追加時に

LANG:js
 function setup_objects(scene) {
   // 箱を作成
   const geometry = new THREE.BoxGeometry(300, 300, 300);              // 形状
   const material = new THREE.MeshStandardMaterial({color: 0x8080ff}); // 単色の材質
   const box = new THREE.Mesh(geometry, material);  // 形状と材質を指定してメッシュを作成
   box.rotation.x = 3.14/4;
   box.rotation.y = 3.14/4;
   scene.add(box); // シーンに追加
 
   // シーンの更新方法を指定(箱を回転させる)
   scene.update = function() {
    box.rotation.x += 0.01;
    box.rotation.y += 0.01;
   }
 }

のようにシーンの更新方法を登録しておく。

これでアニメーションが行われる。

box1.gif

ここまでのコード

LANG:html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<script src="https://cdn.jsdelivr.net/npm/three@0.138.3/build/three.min.js" 
integrity="sha256-TJ+e0PQs7GxkFRYdn1Cc0N0hL5xuh15qCSMqw8kBLDk=" crossorigin="anonymous"></script>
</head>
<body>

<canvas id="canvas1" width="800", height="600"></canvas>

<script>
window.addEventListener("DOMContentLoaded", init);

function init() {
  
   // 描画先の canvas エレメント
   const canvas = document.querySelector("#canvas1");
   
   // レンダラーを作成 (描画先を指定した)
   const renderer = new THREE.WebGLRenderer({canvas: canvas});
   renderer.setSize( canvas.width, canvas.height );
   renderer.setPixelRatio(window.devicePixelRatio);
   
   // カメラを作成 (画角、アスペクト比、描画開始距離、描画終了距離)
   const camera = new THREE.PerspectiveCamera( 
      45, canvas.width / canvas.height, 0.1, 10000 );
   camera.position.set(0, 0, +1000); // x, y, z
   
   // シーンを作成
   const scene = new THREE.Scene();
   
   // ここでシーンにオブジェクトや光源を追加する
   setup_objects(scene);
   setup_lights(scene);
   
   // レンダリング
   renderer.render(scene, camera);
  
  function renderFrame() {
    renderer.render( scene, camera );
 
    scene.update(); // シーンを更新する
 
    // 次フレームの描画を予約
    requestAnimationFrame( renderFrame );
  }
  
  // 次フレームの描画を予約
  requestAnimationFrame( renderFrame );
}
  
function setup_lights(scene) {
  // 平行光源
  const light = new THREE.DirectionalLight(0xff80ff);
  light.intensity = 1;
  light.position.set(1, 1, 1);
  scene.add(light); // シーンに追加
   
  // 環境光
  const ambientLight = new THREE.AmbientLight( 0x80ffff );
  ambientLight.intensity = 0.2;
  scene.add( ambientLight );
}

function setup_objects(scene) {
  // 箱を作成
  const geometry = new THREE.BoxGeometry(300, 300, 300);              // 形状
  const material = new THREE.MeshStandardMaterial({color: 0x8080ff}); // 単色の材質
  const box = new THREE.Mesh(geometry, material);  // 形状と材質を指定してメッシュを作成
  box.rotation.x = 3.14/4;
  box.rotation.y = 3.14/4;
  scene.add(box); // シーンに追加

  // シーンを更新する(箱を回転させる)
  scene.update = function() {
   box.rotation.x += 0.01;
   box.rotation.y += 0.01;
  }
}

</script>

</body>
</html>

任意形状のメッシュを生成

LANG:js
function setup_objects(scene) {

  // 正方形の頂点セット
  // 実際には2つの三角形を組み合わせて正方形にしているため
  // (0,0,0) と (100,100,0) が2回表れる
  const vertices = new Float32Array( [
          0,   0,   0,
        100,   0,   0,
        100, 100,   0,

        100, 100,   0,
          0, 100,   0,
          0,   0,   0,
  ] );

  const geometry = new THREE.BufferGeometry();
  // 頂点あたりの引数の数を itemSize = 3 として指定
  geometry.setAttribute( 'position', new THREE.BufferAttribute( vertices, 3 ) );
  geometry.computeVertexNormals();  // これを忘れると表示されない
  const material = new THREE.MeshStandardMaterial( { color: 0xff0000 } );
  material.side = THREE.DoubleSide; // 裏側も描画
  const mesh = new THREE.Mesh( geometry, material );
  scene.add(mesh);

  // シーンを更新する(箱を回転させる)
  scene.update = function() {
   mesh.rotation.x += 0.01;
   mesh.rotation.y += 0.02;
   mesh.rotation.z += 0.005;
  }
}

square1.gif

ExtrudeGeometry で厚みのある形状

https://threejs.org/docs/#api/en/geometries/ExtrudeGeometry

LANG:js
function setup_objects(scene) {
 
  const h = 120, w = 80;
  
  const shape = new THREE.Shape();
  shape.moveTo( 0,0 );
  shape.lineTo( 0, w );
  shape.lineTo( h, w );
  shape.lineTo( h, 0 );
  shape.lineTo( 0, 0 );
  
  const extrudeSettings = {
  	steps:           2, // 押し出し方向の分割数
  	depth:         160, // 押し出し長さ
  	bevelEnabled: true,
  	bevelThickness: 30, // 押し出し方向
  	bevelSize:      30, // 面内方向
  	bevelOffset:    20, // 外形からの距離(って何だ?)
  	bevelSegments:   1  // 大きくすると丸くなる
  };
  
  const geometry = new THREE.ExtrudeGeometry( shape, extrudeSettings );
 
  geometry.computeVertexNormals();  // これを忘れると表示されない
  const material = new THREE.MeshStandardMaterial( { color: 0xffffff } );
  material.side = THREE.DoubleSide; // 裏側も描画
  const mesh = new THREE.Mesh( geometry, material );
  scene.add(mesh);

  // シーンを更新する(箱を回転させる)
  scene.update = function() {
   mesh.rotation.x += 0.01;
   mesh.rotation.y += 0.02;
   mesh.rotation.z += 0.005;
  }
}

右は bevelSegments = 10 としたもの。

extruded1.gif extruded2.gif

円盤

https://observablehq.com/@rkaravia/2d-shapes-in-three-js を参考に、

LANG:js
  const shape = new THREE.Shape();
  shape.absarc(0, 0, 100);
  
  const extrudeSettings = {
  	steps:           2, // 押し出し方向の分割数
  	depth:           0, // 押し出し長さ
  	bevelEnabled: true,
  	bevelThickness: 20, // 押し出し方向
  	bevelSize:      20, // 面内方向
  	bevelOffset:     0, // 外形からの距離(って何だ?)
  	bevelSegments:  10, // 大きくすると丸くなる
        curveSegments:  60, // 大きくすると丸くなる
  };
  const geometry = new THREE.ExtrudeGeometry( shape, extrudeSettings );

として生成できた。

disk1.gif disk2.gif

curveSegments のデフォルトは 12 なので、これを大きくしないとガタガタになる(右図)。

スムーズシェーディング

https://discourse.threejs.org/t/smooth-shading-for-extruded-circle/25782

通常 ExtrudeGeometry で作った geometry は flatShading で描画されるのだけれど、 THREE.BufferGeometryUtils.mergeVertices で重なった頂点を除去しつつ、 頂点にインデックスをつけると computeVertexNormals() で頂点の法線ベクトルを計算できて スムーズシェーディングが効くようになるらしい(なんのこっちゃ?)

LANG:js
 const shape = new THREE.Shape();
 shape.absarc(0, 0, 100);

 const extrudeSettings = {
 	steps:           2, // 押し出し方向の分割数
 	depth:           0, // 押し出し長さ
 	bevelEnabled: true,
 	bevelThickness: 20, // 押し出し方向
 	bevelSize:      20, // 面内方向
 	bevelOffset:     0, // 外形からの距離(って何だ?)
 	bevelSegments:  5, // 大きくすると丸くなる
       curveSegments:  20, // 大きくすると丸くなる
 };
 const geometry1 = new THREE.ExtrudeGeometry( shape, extrudeSettings );
 geometry1.deleteAttribute( 'normal' );
 geometry1.deleteAttribute( 'uv' );
 const geometry = THREE.BufferGeometryUtils.mergeVertices( geometry1 );
 geometry.computeVertexNormals();

なぜだかはじめと終わりの継ぎ目と思われる部分にちょっとだけガタが残る(左)。

disk4.gif disk3.gif

LANG:js
 const geometry = THREE.BufferGeometryUtils.mergeVertices( geometry1, 1 );

のようにトレランスを大きくしたら良くなったかも(右)。

ガラス

参考:
https://tympanus.net/codrops/2021/10/27/creating-the-effect-of-transparent-glass-and-plastic-in-three-js/

LANG:js
 // 円
 const shape = new THREE.Shape();
 shape.absarc(0, 0, 100);

 // 押し出して丸みのある円柱にする
 const extrudeSettings = {
 	steps:           1, // 押し出し方向の分割数
 	depth:           0, // 押し出し長さ
 	bevelEnabled: true,
 	bevelThickness:  20, // 押し出し方向
 	bevelSize:       20, // 面内方向
 	bevelOffset:     0, // 外形からの距離(って何だ?)
 	bevelSegments:  5, // 大きくすると丸くなる
       curveSegments:  20, // 大きくすると丸くなる
 };
 const geometry1 = new THREE.ExtrudeGeometry( shape, extrudeSettings );

 // スムーズシェーディングするために頂点を整理する
 geometry1.deleteAttribute( 'normal' );
 geometry1.deleteAttribute( 'uv' );
 const geometry = THREE.BufferGeometryUtils.mergeVertices( geometry1, 1 );
 geometry.computeVertexNormals(true);

 // 透明感のある材質
 const material = new THREE.MeshPhysicalMaterial({
   roughness: 0.05,  
   transmission: 1,  
 })
 material.thickness = 40; // 屈折させるため厚みを持たせる

 const mesh = new THREE.Mesh( geometry, material );
 scene.add(mesh);

 // 背景画像を読み込む
 const loader = new THREE.TextureLoader();
 // Thanks, Lukas Blazek@goumbik https://unsplash.com/photos/EWDvHNNfUmQ
 loader.load('lukas-blazek-EWDvHNNfUmQ-unsplash.jpg' , function(texture) {
   const bgGeometry = new THREE.PlaneGeometry(1200, 800); // パネルを用意して
   const bgMaterial = new THREE.MeshBasicMaterial({ map: texture });
   const bgMesh = new THREE.Mesh(bgGeometry, bgMaterial); // 画像を貼る
   bgMesh.position.set(0, 0, -500);  // 場所を背後にする
   scene.add(bgMesh);
 });

disk5.gif

参考

メッシュの表示:
http://www.mwsoft.jp/programming/webgl/geometry.html

キャラクター
https://tympanus.net/codrops/2021/10/04/creating-3d-characters-in-three-js/


https://tympanus.net/codrops/2020/01/28/how-to-create-procedural-clouds-using-three-js-sprites/

BufferGeometry
https://threejs.org/docs/index.html#api/en/core/BufferGeometry

透明について
https://r105.threejsfundamentals.org/threejs/lessons/threejs-transparency.html
https://qiita.com/masato_makino/items/ce57452196d480aaa203


Counter: 4949 (from 2010/06/03), today: 1, yesterday: 0