ブラウザ上で3次元モデルを作成してみる 【JavaScript & Three.js】

Python

ブラウザ上で3次元のモデルを生成する方法について、サンプルコードをまとめ、公開しておくことにします。
3次元表示用のライブラリとして、Three.js を使うことにします。

以下の環境で動作確認をしています。
環境: Windows パソコン、Microsoft Edge ブラウザ

背景 ~ ブラウザで3D表示をやってみる!

これまで、このサイトでは、ブラウザなどで画像を描画する方法等について、サンプルコードの形でまとめ、公開してきています。

しかしながら、3次元のモデルについてはこれまで扱っていませんでした。
そこで、ブラウザ上で3次元モデルを生成して動かす方法について、ポイントをサンプルコードの形でまとめ、公開しておくことにします。

なお、プログラミング言語は JavaScript を使うことにします。また、3次元用のライブラリとして、現状、広く普及している Three.js を使ってみます。
ブラウザ上で3次元が自由自在に扱えるようになると、たとえば、Python 側から3次元のデータを出力してブラウザ上に表示をさせることも可能になります。
Web カメラの画像、Raspberry Pi、機械学習などを使って3次元の座標データを生成し、ブラウザに表示するなど、応用範囲が格段に広がります。

設定手順

① 以下を参考に、3D を扱うためのフォルダ(例: “3d_clock1″)をパソコン内に作成します。
例: C:/user/3d_clock1
② ①のフォルダの中に 3d_clock1.html という名前でテキストファイルを作成し、以下のサンプルスクリプトをコピー&ペーストして保存します。
例: C:/user/3d_clock1/3d_clock1.html
③ 以下の Three.js の公式サイトから、3D 表示に必要なライブラリ(three.min.js)をダウンロードします。
・ Three.js – JavaScript 3D Library (threejs.org)
※ 具体的には、上記のリンク先で、「download」となっている部分ををクリックします。
すると、ZIP ファイル(three.js-master.zip)がパソコンにダウンロードされます。この ZIP ファイルを、例えば、①のフォルダに移しておきます。
ダウンロードした ZIP ファイルについてマウスで右クリックして、「すべて展開…」→「展開」を選択し、ZIP ファイルを解凍します。
※ 解凍したフォルダ内に、LICENSE、README.md があります。テキストエディタ(メモ帳)などでこれらのファイルを開き、内容を確認しておきます。MIT ライセンスとなっており、(再配布時などには)著作権表示と permission notice の添付をすべき旨の記載となっています。
④ 解凍した ZIP ファイル内に “build” というフォルダがあり、この中に JavaScript のライブラリが入っています。”build” フォルダ内の three.min.js を、①のフォルダにコピーします。
⑤ ②で作成した HTML ファイル 3d_clock1.html をダブルクリックします。

→ 3D のモデル(3D の時計)が動いたら、成功です。

サンプルコードの説明

・ サンプルコードは、ローカルパソコンでも動くアナログ時計としてみました。Windows パソコンのタスクバーでは、時計が秒単位まで表示されず、やや不便だったからです。買うとちょっと高そうな感じのアナログ時計にしてみました。皆様に差し上げます。
・ サンプルコードは、HTML で記載しています。
・ 冒頭のヘッダー内で、Three.js の JavaScript のライブラリ “three.min.js” を読み込んでいます。もし、”three.js” を使いたい、ファイルのありかを変更したい、などの場合は、この部分を適宜、修正してください。
・ <script>…</script> としたところで、HTML の初期表示の設定と、3次元のオブジェクトを表示する関数を定義しています。
・ function draw1() { … } としたところで、3次元の空間を設定して、オブジェクトを配置し、アニメーションとして動くよう設定しています。
具体的には、シーン scene1 を定義して、3次元空間を設定しています。この3次元空間内に、3次元物体を追加していくことで、3次元空間内のシーンを作ります。
また、レンダラー renderer1 を定義して、2次元の画像を描画するための設定を行います。HTML 上に配置した2次元の領域の画素数の設定などをしています。
シーン scene1 内に3次元物体の配置が終わったら、レンダラー renderer1 に投げることで、2次元の画像を描画・生成する、という流れになっています。
・ 続いて、add_camera1( … ) を呼び出して上記の scene1 に設定することで、3次元空間内にカメラを設定しています。
・ add_light1() と add_light2() を呼び出している部分で、光源を設定しています。
光源を2つ設定しているのは、自然で立体感のある見栄えとするために、環境光の光源と指向性のある光源の2つを配置したかったためです。光源の片方を削除したり、また、新たな光源を追加することで、光源の設定を任意に調整することが可能です。
・ objects1 ~ objects4 を定義しているところで、空間内に配置する物体を作っています。
具体的には、objects1 では、時計の文字盤など、空間内に固定されている物体を定義しています。
objects2, 3, 4 は、時、分、秒に対応する時計の針を定義しています。動作時の回転量が異なるため、それぞれ別々に定義しています。
・ 関数 move1() で、アニメーションとして、上記の objects2, 3, 4 を動かしています。
具体的には、function move1() { … } の中で、setTimeout( move1, 1000 ) をさらに実行することで、1000ms ごとに、自分自身を繰り返し呼び出します。この関数の中で、時計の針 objects2, 3, 4 の回転角を設定し、時計の針を動かしています。
・ 関数 move1() の中で、renderer1.render( …) を実行しています。
render は英語で「描く」の意味があります。シーン scene1 は、単に3次元の物体を並べただけですので、どのような見栄えになるかは未定です。
そこで、render1.render( scene1, camera1 ) を実行することで、3次元の空間 scene1 を指定したカメラ camera1 から見るとどのようになるか、2次元画像を生成(描画)し、指定された HTML 上の画面 canvas1 に表示していると考えると理解しやすいと思います。
(3次元空間 scene1)→(レンダリング)→(2次元画像の生成、モニター canvas1 表示)

・ また、関数 add_cylinder1()、add_sphere1()、add_box1()、add_rectangle1()、add_plane1() で、円柱、球、直方体、平面(四角形)、平面(三角形:例)を定義しています。3次元形状を作るにあたり、通常、必要となる主な基本図形を定義しています。これらを組み合わせることで、任意の形状を生成できます。
・ 関数 add_object1, 2 で、上記の基本図形を組み合わせて3次元形状を作り、シーン scene1 に追加していくことで、空間内の自由な位置にオブジェクトを配置できるようにしています。
・ 末尾の <body … > … </body> としたところで、ブラウザでの画面表示を定義しています。
<canvas … > … </canvas> とした領域に、上記の3次元の描画をしています。
3次元の表示領域を、ブラウザの画面中央に配置したい、また、ブラウザ画面の大きさに表示領域の大きさを追従させたい、等のため、style や <div> </div> のタグなどを使っています。

Three.js の今後について
・ なお、今回、Three.js のライブラリ three.min.js を使いましたが、上記のリンク先を確認すると、今後は ES modules に統合されていくとのことです。今後、新しいバージョンを使う場合、動かすにあたりサーバーを準備する必要があるようです。
サーバーを使った Web アプリなどについても、このサイトで扱ってきていますが、設定がやや煩雑です。
そこで今回は、現状普及している three.min.js を使うことにし、サーバーなしでも、ローカルパソコンでも、簡単に動くサンプルについてまとめました。
Three.js については、しばらくは大きな仕様変更が続きそうですので、あまりリソースは投入しすぎず、投資対効果の観点でメリットが見込める範囲で、便利な機能をうまく活用していけばよいと思います。
MIT ライセンスなど、オープンソースのライセンスについて、末尾の関連リンクでまとめていますので、関心のある方は参照してみてください。

まとめ

ブラウザで3次元のオブジェクトを扱う方法について、ポイントをサンプルコードの形でまとめました。
これで3次元形状についても、ブラウザ上で自由に表示できるようになりました。
簡単な3次元モデルであれば、Three.js のライブラリで十分使えそうです。

これまでこのサイトでは、機械学習、サーバー技術、Raspberry Pi、Web カメラの扱い方などについて、スクリプトの形でまとめてきています。これらの要素技術と今回の3次元のプログラムを自由に組み合わせて活用することも可能です。
もし関心があるようでしたら、下記の関連リンクなども参照してみてください。

関連リンク
・ ローカル Web アプリでグラフ描画 【Python】
・ ブラウザで動くお絵描きアプリ 【JavaScript】
・ Python で Web サーバーを動かす 【Windows】
・ Raspberry Pi でローカルWebサーバー 【Python 活用】
・ レンタルサーバーの比較表
・ オープンソースに関するまとめ 【ライセンス戦略】

外部リンク
・ Three.js – JavaScript 3D Library (threejs.org)

サンプルコード: 3次元アナログ時計 3d_clock1.html

<!DOCTYPE html> 
<html lang="ja">
<head> 
<meta http-equiv="content-type" content="text/javascript; charset=utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>3 dimentional clock</title> 
<script src="three.min.js"></script>
<script>

let w1 = 800; 
let h1 = 600; 

window.onload = function() { 
  resize1(); 
  draw1(); 
} 

window.onresize = function() { 
  window.location.reload(); 
} 

function resize1() { 
  const w2 = document.documentElement.clientWidth-40; 
  const h2 = document.documentElement.clientHeight-40; 
  if ( w2/h2 > w1/h1 ) { 
    w1 = w1/h1 * h2  
    h1 = h2 
  } else { 
    h1 = h1/w1 * w2 
    w1 = w2; 
  } 
} 

function draw1() { 
  const scene1 = new THREE.Scene(); 
  const renderer1 = set_renderer1(); 

  const camera1 = add_camera1( 0, 0, 1300, 0, 0, 0, 0, 0, 0 ); 
  scene1.add( camera1 ); 

  const light1 = add_light1(); 
  scene1.add( light1 ); 
  const light2 = add_light2(); 
  scene1.add( light2 ); 

  const objects1 = add_object1(); 
  scene1.add( objects1 ); 
  const objects2 = add_object2(0, 140, -30, 10, 210); 
  scene1.add( objects2 ); 
  const objects3 = add_object2(0, 185, -15,  7, 300); 
  scene1.add( objects3 ); 
  const objects4 = add_object2(0, 205,   0,  4, 340); 
  scene1.add( objects4 ); 

  move1();

  function move1() { 
    let date1 = new Date(); 
    let H0 = date1.getHours(); 
    let M0 = date1.getMinutes(); 
    let S0 = date1.getSeconds(); 
    objects2.rotation.z = -(H0+M0/60.0)/12.0 * 2.0 * Math.PI; 
    objects3.rotation.z = -M0/60.0 * 2.0 * Math.PI; 
    objects4.rotation.z = -S0/60.0 * 2.0 * Math.PI; 
    renderer1.render( scene1, camera1 ); 
    setTimeout( move1, 1000 ); 
  } 
} 

function set_renderer1() { 
  const renderer1 = new THREE.WebGLRenderer({ canvas: document.querySelector('#canvas1') });
  renderer1.setClearColor( 0x222244 ); 
  renderer1.setPixelRatio( window.devicePixelRatio );
  renderer1.setSize( w1, h1 );
  renderer1.shadowMap.enabled = true; 
  return renderer1 
} 

function add_camera1(x1, y1, z1, x2, y2, z2, x3, y3, z3) { 
  const camera1 = new THREE.PerspectiveCamera(40, w1 / h1);
  camera1.position.set(x1, y1, z1); 
  camera1.rotation.z = x3*2*Math.PI; 
  camera1.rotation.y = y3*2*Math.PI; 
  camera1.rotation.x = z3*2*Math.PI; 
  return camera1 
} 

function add_light1() { 
  const light1 = new THREE.AmbientLight(0xffffff, 0.6);  // color, intensity 
  return light1 
} 

function add_light2() { 
  const light1 = new THREE.SpotLight(0xffffff, 0.9, 2500, -0.15*2.0*Math.PI, 0.9, 0.2); // color, intensity, distance, angle, spot light cone %, decay 
  light1.position.set( 400, 900, 700 ); 
  light1.castShadow = true; 
  light1.shadow.mapSize.width = 2048; 
  light1.shadow.mapSize.height = 2048; 
  return light1 
} 

function add_object1() { 
  const objects1 = new THREE.Group(); 
  const material1 = new THREE.MeshStandardMaterial({color: 0x403030, roughness:0.5, side: THREE.DoubleSide} ); 
  const material2 = new THREE.MeshStandardMaterial({color: 0xffc000, roughness:0.5, side: THREE.DoubleSide} ); 
  const obj1 = add_box1( material1, 0, 0, -80, 0, 0, 0, 2000, 2000, 5 ) 
  obj1.receiveShadow = true; 
  objects1.add( obj1 ); 
  const obj2 = add_cylinder1( material2, 0, 0, -75, 1/4, 0, 0, 430, 5 ); 
  obj2.receiveShadow = true; 
  objects1.add( obj2 ); 
  const obj3 = add_cylinder1( material1, 0, 0, -71, 1/4, 0, 0, 290, 5 ); 
  obj3.receiveShadow = true; 
  objects1.add( obj3 ); 
  const obj4 = add_sphere1( material2, 0, 0, 0, 12 ); 
  objects1.add( obj4 ); 
  const obj5 = add_cylinder1( material2, 0, 0, -20, 1/4, 0, 0, 12, 40 ); 
  objects1.add( obj5 ); 
  for ( let i1=0; i1<12; i1++ ) { 
    let obj6 = add_box1( material1, 350*Math.cos(i1/12*2*Math.PI), 350*Math.sin(i1/12*2*Math.PI), -40, 0, 0, i1/12, 100, 15, 15 ); 
    objects1.add( obj6 ); 
  } 
  return objects1 
} 

function add_object2( x1, y1, z1, r1, h1 ) { 
  const objects1 = new THREE.Group(); 
  const material1 = new THREE.MeshStandardMaterial({color: 0xffb800, roughness:0.5, side: THREE.DoubleSide} ); 
  const obj2 = add_cylinder1( material1, x1, y1, z1, 1/2, 0, 0, r1, h1 ); 
  objects1.add( obj2 ); 
  return objects1 
} 

function add_cylinder1( material1, x1, y1, z1, x2, y2, z2, r1, l1 ) { 
  const geo1 = new THREE.CylinderGeometry( r1, r1, l1, 128 ); 
  const obj1 = new THREE.Mesh( geo1, material1 ); 
  obj1.position.x = x1; 
  obj1.position.y = y1; 
  obj1.position.z = z1; 
  obj1.rotation.x = x2*2*Math.PI; 
  obj1.rotation.y = y2*2*Math.PI; 
  obj1.rotation.z = z2*2*Math.PI; 
  obj1.castShadow = true; 
  return obj1 
} 

function add_sphere1( material1, x1, y1, z1, r1 ) { 
  const geo1 = new THREE.SphereGeometry( r1, 16, 16 ); 
  const obj1 = new THREE.Mesh( geo1, material1 ); 
  obj1.position.x = x1; 
  obj1.position.y = y1; 
  obj1.position.z = z1; 
  obj1.castShadow = true; 
  return obj1 
} 

function add_box1( material1, x1, y1, z1, x2, y2, z2, x3, y3, z3 ) { 
  const geo1 = new THREE.BoxGeometry( x3, y3, z3 ); 
  const obj1 = new THREE.Mesh( geo1, material1 ); 
  obj1.position.x = x1; 
  obj1.position.y = y1; 
  obj1.position.z = z1; 
  obj1.rotation.x = x2*2*Math.PI; 
  obj1.rotation.y = y2*2*Math.PI; 
  obj1.rotation.z = z2*2*Math.PI; 
  obj1.castShadow = true; 
  return obj1 
} 

function add_rectangle1( material1, x1, y1, z1, x2, y2, z2, w1, h1 ) { 
  const geo1 = new THREE.PlaneGeometry( w1, h1, 32 );
  const obj1 = new THREE.Mesh( geo1, material1 ); 
  obj1.position.x = x1; 
  obj1.position.y = y1; 
  obj1.position.z = z1; 
  obj1.rotation.x = x2*2*Math.PI; 
  obj1.rotation.y = y2*2*Math.PI; 
  obj1.rotation.z = z2*2*Math.PI; 
  obj1.castShadow = true; 
  return obj1 
} 

function add_plane1( material1, x1, y1, z1, x2, y2, z2, x3, y3, x4, y4, x5, y5 ) { 
  const shape1 = new THREE.Shape(); 
  shape1.moveTo( x3, y3 ); shape1.lineTo( x4, y4 ); shape1.lineTo( x5, y5 ); shape1.lineTo( x3, y3 ); 
  const geo1 = new THREE.ShapeGeometry( shape1 ); 
  const obj1 = new THREE.Mesh( geo1, material1 ); 
  obj1.position.x = x1; 
  obj1.position.y = y1; 
  obj1.position.z = z1; 
  obj1.rotation.x = x2*2*Math.PI; 
  obj1.rotation.y = y2*2*Math.PI; 
  obj1.rotation.z = z2*2*Math.PI; 
  obj1.castShadow = true; 
  return obj1 
} 

</script>
</head> 
<body style="background-color:#312323" >
  <div style="width:fit-content; margin:0 auto;"><canvas id="canvas1"></canvas></div> 
</body>
</html>
タイトルとURLをコピーしました