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

更新

[[公開メモ]]

#contents

* 準備 [#gf36364d]

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>

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

* 立方体のチュートリアル [#c97f1541]

** レンダリングの基本 [#u21e2247]

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

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

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

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

ここでは、

 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);

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

** オブジェクトと光源を追加してみる [#z005ed48]

 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); // シーンに追加
 }

これで立方体が表示された。
 
&ref(box1.png);

** アニメーションさせる [#s11c3846]

一定時間間隔でシーンを変更しつつ 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;
    }
  }

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

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

&attachref(box1.gif);

** ここまでのコード [#bea92dc5]

 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>

* 任意形状のメッシュを生成 [#k98c0e43]

 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;
   }
 }

&attachref(square1.gif);

* ExtrudeGeometry で厚みのある形状 [#u23e7cb1]

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 としたもの。

&attachref(extruded1.gif,,66%);
&attachref(extruded2.gif,,66%);

* 円盤 [#o83fd2b2]

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 );

として生成できた。

&attachref(disk1.gif);
&attachref(disk2.gif);

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

* スムースシェーディング [#jbc741e4]

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:  60, // 大きくすると丸くなる
  };
  const geometry1 = new THREE.ExtrudeGeometry( shape, extrudeSettings );
  geometry1.deleteAttribute( 'normal' );
  geometry1.deleteAttribute( 'uv' );
  const geometry = THREE.BufferGeometryUtils.mergeVertices( geometry1 );
  geometry.computeVertexNormals();

本来なら curveSegments = 30 くらいでも良さそうなのだけれど、
なぜだかはじめと終わりの継ぎ目と思われる部分にちょっとだけガタが残る。

&ref(disk3.gif);
&attachref(disk4.gif);

上のリンク先でも、

> You could try to use BufferGeometryUtils.mergeVertices() 12 to create an indexed geometry. However this function can only merge vertices if all existing vertex data match (vertices, normals, uvs, colors etc.). This makes it problematic to use it with ExtrudeGeometry see https://jsfiddle.net/r6zguh09/1/ 39.
>
>It’s probably best if you create a custom cylinder mesh in Blender and import it as a glTF asset.

と言われていて・・・これではうまく行かないのかもしれない?

Counter: 5027 (from 2010/06/03), today: 4, yesterday: 0