0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【人生ログ開発 #2】3D可視化への挑戦。Three.jsで点と線による軌跡描画をマスターする

0
Posted at

はじめに

この記事では、Three.jsを初めて使う筆者が、マウス操作などのインタラクティブな要素を実装した記録を記載していきます。

ただ、学ぶだけではつまらないので、題材として大学でお世話になったローレンツ方程式の軌跡をアニメーションで表示することを目標としています。今回はグラフィックボード等を積んでいない普通のノートパソコンで、どれだけの描画が可能なのかも見てみたいと思います。

学習は、Three.jsの公式サイトにあるマニュアルを見ながら進めていきます。

1. 環境構築

Three.jsを扱うには、セキュリティ上の制約として、Web Serverを必要とするそうです。

先に進む前に開発環境のセットアップの話をする必要があります。特にセキュリティ上の理由から、WebGLはハードディスクから直接画像を扱う事ができません。開発をするためにはWebサーバーを利用する必要があります。幸運な事に開発用のWebサーバーをセットアップし利用する事は非常に簡単です。(Three.jsの公式サイトにあるマニュアル セットアップより)

ということで、以下の作業を実施しました。

  1. フォルダD:\ThreeJS_Onrampを作成
  2. npm install --save threeを実行
    image.png
  3. npm install --save-dev viteを実行
    image.png
  4. index.htmlファイルを新規に作成して、Three.jsの公式サイトにあるマニュアルのhtmlファイルを作成する。
index.html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <title>My first three.js app</title>
    <style>
      body { margin: 0; }
    </style>
  </head>
  <body>
    <script type="module" src="/main.js"></script>
  </body>
</html>
  1. main.jsファイルを新規に作成して、Three.jsの公式サイトにあるマニュアルに従って以下のスクリプトを記載。
main.js
import * as THREE from 'three';

image.png
6. npx viteを実行。
image.png
7. ブラウザから、Local欄に記載されたアドレス(上記画像の場合、http://localhost:5173/)にアクセス
image.png
何も映らないけど、タイトルは"My first three.js app"だし、F12を押したら作成したhtmlが見えたから、多分大丈夫...

ということで、無事に環境構築完成です。

今回使用した環境は以下です。

>node -v
v24.15.0
>npm -v
11.12.1

image.png

Node.jsのダウンロードとインストールはNode.js ダウンロードページから可能のようです。

2. はじめての描画

To actually be able to display anything with three.js, we need three things: scene, camera and renderer, so that we can render the scene with camera. (Three.jsの公式サイトにあるマニュアル Create a sceneより)

ということで、これから、

  • シーン
  • カメラ
  • レンダラー
  • アニメーション ※のちほどアニメーションも必要になるという記載がある。

を作成することによって、物体を描画できるらしい。ということで、それぞれを作成していきます。

2.1. シーンの作成

THREE.Scene()でシーンのインスタンスを作成

const scene = new THREE.Scene();

2.2. カメラの作成

Three.jsにはいろいろなカメラの種類があるそうですが、今回はPerspectiveCameraを使うそうです。

const camera = new THREE.PerspectiveCamera( 75, window.innerWidth / window.innerHeight, 0.1, 1000 );

PerspectiveCameraの引数については、以下のページに詳しく書いてありました。

  • 第一引数 : 視野角 (Field of View)
  • 第二引数 : アスペクト比 (Aspect)
  • 第三引数 : 描画を行うカメラから最も近い距離 (Near)
  • 第四引数 : 描画を行うカメラから最も遠い距離 (Far)

image.png
Three.jsの公式サイトにあるマニュアル 基礎知識より

2.3. レンダラーの作成

THREE.WebGLRenderer()でレンダラーのインスタンスを作成した後に、renderer.setSize()により、カメラから出力された画像を指定したサイズに変更します。

const renderer = new THREE.WebGLRenderer();
renderer.setSize( window.innerWidth, window.innerHeight );

2.4. レンダリング結果のHTMLへの追加

レンダリングした結果をHTMLにdomElementとして追加します。

document.body.appendChild( renderer.domElement );

2.5. シーンへのオブジェクトの追加

ここまで、シーン、カメラ、レンダラーを作成してきましたが、肝心のカメラに映るオブジェクトを作成していませんでした。そこで、今回は、THREE.BoxGeometry()で3Dキューブを作成していきます。またTHREE.MeshBasicMaterial( { color: 0x00ff00 } )で表面の色を緑色に設定します。

const geometry = new THREE.BoxGeometry( 1, 1, 1 );
const material = new THREE.MeshBasicMaterial( { color: 0x00ff00 } );
const cube = new THREE.Mesh( geometry, material );
scene.add( cube );

このままだと、カメラとキューブが同じ位置にあり、カメラの映像がキューブの内部を映してしまうため、camera.positionでカメラの位置をずらします。

camera.position.z = 5;

2.6. アニメーションによるレンダリングの実行

If you copied the code from above into the main.js file we created earlier, you wouldn't be able to see anything. This is because we're not actually rendering anything yet. For that, we need what's called a render or animation loop. (Three.jsの公式サイトにあるマニュアル Create a sceneより)

ということで、画像を描画するにはアニメーションが必要だということがわかりました。そこで、以下のようにJavascript関数でレンダリングを行うループを作成します。

function animate( time ) {
  renderer.render( scene, camera );
}
renderer.setAnimationLoop( animate );

2.7. 静止画の描画

ここまでのスクリプトを合わせると以下のよになります。

main.js
import * as THREE from 'three';

const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera( 75, window.innerWidth / window.innerHeight, 0.1, 1000 );

const renderer = new THREE.WebGLRenderer();
renderer.setSize( window.innerWidth, window.innerHeight );
document.body.appendChild( renderer.domElement );

const geometry = new THREE.BoxGeometry( 1, 1, 1 );
const material = new THREE.MeshBasicMaterial( { color: 0x00ff00 } );
const cube = new THREE.Mesh( geometry, material );
scene.add( cube );

camera.position.z = 5;

function animate( time ) {
  renderer.render( scene, camera );
}
renderer.setAnimationLoop( animate );

これを、npx viteで動かしてみました。
image.png
緑色の箱が描画されました。次にこの箱を動かしていきます。

2.8. オブジェクトが動く様子の描画

オブジェクトを動かすには、先ほどのレンダリングを行うために作成したJavascript関数(animate)の中で、箱の位置を、時間の関数として、指定します。

function animate( time ) {
  cube.rotation.x = time / 2000;
  cube.rotation.y = time / 1000;
  
  renderer.render( scene, camera );
}

その結果以下のようなスクリプトとなります。

main.js
import * as THREE from 'three';

const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera( 75, window.innerWidth / window.innerHeight, 0.1, 1000 );

const renderer = new THREE.WebGLRenderer();
renderer.setSize( window.innerWidth, window.innerHeight );
document.body.appendChild( renderer.domElement );

const geometry = new THREE.BoxGeometry( 1, 1, 1 );
const material = new THREE.MeshBasicMaterial( { color: 0x00ff00 } );
const cube = new THREE.Mesh( geometry, material );
scene.add( cube );

camera.position.z = 5;

function animate( time ) {
  cube.rotation.x = time / 2000;
  cube.rotation.y = time / 1000;
  
  renderer.render( scene, camera );
}
renderer.setAnimationLoop( animate );

このスクリプトを保存して、npx viteで実行すると、緑色の箱が動いている様子が描画されました。
image.png

3. 線と点の描画

WireFrameによる3Dモデルではなく線を描画していきます。まずは、描画に必要な、

線の描画については、Three.jsの公式サイトにあるマニュアル Drawing Lines を参考にしています。

  • シーン
  • カメラ
  • レンダラー
  • アニメーション

を作成します。

line.js
const renderer = new THREE.WebGLRenderer();
renderer.setSize( window.innerWidth, window.innerHeight );
document.body.appendChild( renderer.domElement );

const camera = new THREE.PerspectiveCamera( 45, window.innerWidth / window.innerHeight, 1, 500 );
camera.position.set( 0, 0, 100 );
camera.lookAt( 0, 0, 0 );

const scene = new THREE.Scene();

3.1. 線のデザイン

LineBasicMaterialで線のマテリアルを決定します。

//create a blue LineBasicMaterial
const material = new THREE.LineBasicMaterial( { color: 0x0000ff } );

3.2. 線でつながれる点の作成

次に、JavascriptのArrayで線でつながれる点(points)を定義し、THREE.BufferGeometry()に変換します。

const points = [];
points.push( new THREE.Vector3( - 10, 0, 0 ) );
points.push( new THREE.Vector3( 0, 10, 0 ) );
points.push( new THREE.Vector3( 10, 0, 0 ) );

const geometry = new THREE.BufferGeometry().setFromPoints( points );

3.3. 線の定義

定義された点のArrayに基づいて、隣り合う点が線で結ばれます。

const line = new THREE.Line( geometry, material );

点を定義した配列の最初と最後の点は結ばれません。

Note that lines are drawn between each consecutive pair of vertices, but not between the first and last (the line is not closed.)
( Three.jsの公式サイトにあるマニュアル Drawing Lines より)

最後に線をシーンに追加します。

scene.add( line );
renderer.render( scene, camera );

3.4. 線の描画

以上のスクリプトを合わせると以下のようになります。

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <title>My first three.js app</title>
    <style>
      body { margin: 0; }
    </style>
  </head>
  <body>
-   <script type="module" src="/main.js"></script>
+   <script type="module" src="/line.js"></script>
  </body>
</html>
line.js
import * as THREE from 'three';

const renderer = new THREE.WebGLRenderer();
renderer.setSize( window.innerWidth, window.innerHeight );
document.body.appendChild( renderer.domElement );

const camera = new THREE.PerspectiveCamera( 45, window.innerWidth / window.innerHeight, 1, 500 );
camera.position.set( 0, 0, 100 );
camera.lookAt( 0, 0, 0 );

const scene = new THREE.Scene();

//create a blue LineBasicMaterial
const material = new THREE.LineBasicMaterial( { color: 0x0000ff } );

const points = [];
points.push( new THREE.Vector3( -10,  0, 0 ) );
points.push( new THREE.Vector3(   0, 10, 0 ) );
points.push( new THREE.Vector3(  10,  0, 0 ) );

const geometry = new THREE.BufferGeometry().setFromPoints( points );

const line = new THREE.Line( geometry, material );

scene.add( line );
function animate( time ) {
  renderer.render( scene, camera );
}
renderer.setAnimationLoop( animate );

image.png

上記スクリプトをnpx viteで実行すると、青色の線が描画されました。
image.png

4. マウスによる視点移動

マウスによる視点移動については、Three.jsの公式サイトにあるマニュアル Cameras を参考にしています。

ここは。まだ理解が甘いのですが、以下のように { OrbitControls }を追加することで、マウスでのカメラの操作が可能になります。

本来、カメラを動かすには『マウスの移動量を取得してカメラの座標を計算し直す』という複雑な処理が必要ですが、OrbitControls はその面倒な計算をすべて肩代わりしてくれる魔法のようなツールのようです。

import * as THREE from 'three';
+ import {OrbitControls} from 'three/addons/controls/OrbitControls.js';
+ const controls = new OrbitControls(camera, renderer.domElement);
+ controls.enableDamping = true;
+ 
function animate( time ) {
+ controls.update();
  renderer.render( scene, camera );
}

main.js
main.js
import * as THREE from 'three';
+ import {OrbitControls} from 'three/addons/controls/OrbitControls.js';

const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera( 75, window.innerWidth / window.innerHeight, 0.1, 1000 );

const renderer = new THREE.WebGLRenderer();
renderer.setSize( window.innerWidth, window.innerHeight );
document.body.appendChild( renderer.domElement );

const geometry = new THREE.BoxGeometry( 1, 1, 1 );
const material = new THREE.MeshBasicMaterial( { color: 0x00ff00 } );
const cube = new THREE.Mesh( geometry, material );
scene.add( cube );

camera.position.z = 5;

+ const controls = new OrbitControls(camera, renderer.domElement);
+ controls.enableDamping = true;

function animate( time ) {
  cube.rotation.x = time / 2000;
  cube.rotation.y = time / 1000;
  
+ controls.update();
  renderer.render( scene, camera );
}
renderer.setAnimationLoop( animate );
line.js
line.js
import * as THREE from 'three';
+ import {OrbitControls} from 'three/addons/controls/OrbitControls.js';

const renderer = new THREE.WebGLRenderer();
renderer.setSize( window.innerWidth, window.innerHeight );
document.body.appendChild( renderer.domElement );

const camera = new THREE.PerspectiveCamera( 45, window.innerWidth / window.innerHeight, 1, 500 );
camera.position.set( 0, 0, 100 );
camera.lookAt( 0, 0, 0 );

const scene = new THREE.Scene();

//create a blue LineBasicMaterial
const material = new THREE.LineBasicMaterial( { color: 0x0000ff } );

const points = [];
points.push( new THREE.Vector3( -10,  0, 0 ) );
points.push( new THREE.Vector3(   0, 10, 0 ) );
points.push( new THREE.Vector3(  10,  0, 0 ) );

const geometry = new THREE.BufferGeometry().setFromPoints( points );

const line = new THREE.Line( geometry, material );

scene.add( line );

+ const controls = new OrbitControls(camera, renderer.domElement);
+ controls.enableDamping = true;
+ 
function animate( time ) {
  controls.update();
  renderer.render( scene, camera );
}
renderer.setAnimationLoop( animate );

main.jsline.js の描画を変更するために index.html がLoadするJavascriptファイルは適宜変更してください。

index.html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <title>My first three.js app</title>
    <style>
      body { margin: 0; }
    </style>
  </head>
  <body>
-   <script type="module" src="/line.js"></script>
+   <script type="module" src="/main.js"></script>
  </body>
</html>

5. ローレンツ方程式の描画とアニメーション

最後に、ローレンツ方程式を計算して、アニメーションで表示させます。

5.1. ローレンツ方程式とは

以下の微分方程式で定義されるカオス的な振る舞いをするモデルです。

$$\frac{dx}{dt} = \sigma(y - x)$$
$$\frac{dy}{dt} = x(\rho - z) - y$$
$$\frac{dz}{dt} = xy - \beta z$$

今回は、以下の定数と初期値を用いて、2本の線を描画するスクリプトを作成してみました。

  • 定数
    $$ \sigma = 10, \rho = 28, \beta = \frac{8}{3} $$
  • 1本目の初期値
    $$ x_1 = 0.1, y_1 = 0.0, z_1 = 0.0 $$
  • 2本目の初期値
    $$ x_2 = 0.0, y_2 = 0.1, z_2 = 0.0 $$

実装コード

lorenz.js
lorenz.js
import * as THREE from 'three';
import {OrbitControls} from 'three/addons/controls/OrbitControls.js';

// Initial Values
const X_1 = 0.1, Y_1 = 0.0, Z_1 = 0.0;
const X_2 = 0.0, Y_2 = 0.1, Z_2 = 0.0;

// Update Coordinate Value
let x_1 = X_1, y_1 = Y_1, z_1 = Z_1;
let x_2 = X_2, y_2 = Y_2, z_2 = Z_2;

// Cionstants
const sigma = 10, rho = 28, beta = 8/3;
const dt = 0.01;
const MAX_POINTS = 30;

// Create a renderer
const renderer = new THREE.WebGLRenderer();
renderer.setSize( window.innerWidth, window.innerHeight );
document.body.appendChild( renderer.domElement );

// Create a camera
const camera = new THREE.PerspectiveCamera( 45, window.innerWidth / window.innerHeight, 1, 500 );
camera.position.set(0, 50, 100);
camera.lookAt(0, 0, 0);

// Create scene
const scene = new THREE.Scene();

// Create Geometry
const lorenz_1_Geometry = new THREE.BufferGeometry();
const lorenz_2_Geometry = new THREE.BufferGeometry();

// Allocate Point Data Buffer
const lorenz_1_Points = new Float32Array(MAX_POINTS * 3);
const lorenz_2_Points = new Float32Array(MAX_POINTS * 3);
lorenz_1_Geometry.setAttribute('position', new THREE.BufferAttribute(lorenz_1_Points, 3));
lorenz_2_Geometry.setAttribute('position', new THREE.BufferAttribute(lorenz_2_Points, 3));

// Set Draw range to 0
lorenz_1_Geometry.setDrawRange(0, 0);
lorenz_2_Geometry.setDrawRange(0, 0);

// Create Line Material
const lorenz_1_Material = new THREE.LineBasicMaterial({ color: 0x00ffff });
const lorenz_2_Material = new THREE.LineBasicMaterial({ color: 0xffff00 });

// Create Line
const lorenz_1_Line = {
    line: new THREE.Line(lorenz_1_Geometry, lorenz_1_Material),
    positions: lorenz_1_Points,
    count: 0
};
const lorenz_2_Line = {
    line: new THREE.Line(lorenz_2_Geometry, lorenz_2_Material),
    position: lorenz_2_Points,
    count: 0
};
scene.add(lorenz_1_Line.line);
scene.add(lorenz_2_Line.line);

function updateLorenz() {
    // Calculate equation
    const dx_1 = sigma * (y_1 - x_1) * dt;
    const dy_1 = (x_1 * (rho - z_1) - y_1) * dt;
    const dz_1 = (x_1 * y_1 - beta * z_1) * dt;
    x_1 += dx_1; y_1 += dy_1; z_1 += dz_1;

    const dx_2 = sigma * (y_2 - x_2) * dt;
    const dy_2 = (x_2 * (rho - z_2) - y_2) * dt;
    const dz_2 = (x_2 * y_2 - beta * z_2) * dt;
    x_2 += dx_2; y_2 += dy_2; z_2 += dz_2;

    // Update exist point data array
    const lorenz_1_posAttr = lorenz_1_Line.line.geometry.attributes.position;
    const lorenz_2_posAttr = lorenz_2_Line.line.geometry.attributes.position;

    const lorenz_1_index = Math.min(lorenz_1_Line.count, MAX_POINTS);
    const lorenz_2_index = Math.min(lorenz_2_Line.count, MAX_POINTS);

    if (lorenz_1_index < MAX_POINTS) {
        lorenz_1_posAttr.array[lorenz_1_Line.count * 3 + 0] = x_1;
        lorenz_1_posAttr.array[lorenz_1_Line.count * 3 + 1] = y_1;
        lorenz_1_posAttr.array[lorenz_1_Line.count * 3 + 2] = z_1;
    } else {
        lorenz_1_posAttr.array.set(lorenz_1_posAttr.array.subarray(3))
        lorenz_1_posAttr.array[(MAX_POINTS - 1) * 3 + 0] = x_1;
        lorenz_1_posAttr.array[(MAX_POINTS - 1) * 3 + 1] = y_1;
        lorenz_1_posAttr.array[(MAX_POINTS - 1) * 3 + 2] = z_1;
    }

    lorenz_1_Line.line.geometry.setDrawRange(0, lorenz_1_index);
    lorenz_1_posAttr.needsUpdate = true;
    lorenz_1_Line.count++;

    if (lorenz_2_index < MAX_POINTS) {
        lorenz_2_posAttr.array[lorenz_2_Line.count * 3 + 0] = x_2;
        lorenz_2_posAttr.array[lorenz_2_Line.count * 3 + 1] = y_2;
        lorenz_2_posAttr.array[lorenz_2_Line.count * 3 + 2] = z_2;
    } else {
        lorenz_2_posAttr.array.set(lorenz_2_posAttr.array.subarray(3))
        lorenz_2_posAttr.array[(MAX_POINTS - 1) * 3 + 0] = x_2;
        lorenz_2_posAttr.array[(MAX_POINTS - 1) * 3 + 1] = y_2;
        lorenz_2_posAttr.array[(MAX_POINTS - 1) * 3 + 2] = z_2;
    }

        lorenz_2_Line.line.geometry.setDrawRange(0, lorenz_2_index);
        lorenz_2_posAttr.needsUpdate = true;
        lorenz_2_Line.count++;

    controls.update();
    renderer.render(scene, camera);    
}

const controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
controls.target.set(0, 0, 25);
controls.update();

function animate() {
    updateLorenz();
    controls.update();
    renderer.render(scene, camera);
}
renderer.setAnimationLoop( animate );

window.addEventListener('resize', () => {
    camera.aspect = window.innerWidth / window.innerHeight;
    camera.updateProjectionMatrix();
    renderer.setSize(window.innerWidth, window.innerHeight);
});
index.html
index.html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <title>My first three.js app</title>
    <style>
      body { margin: 0; }
    </style>
  </head>
  <body>
    <script type="module" src="/lorenz.js"></script>
  </body>
</html>

結果

image.png

Adobe Express - 画面録画 2026-04-29 222415 (1).gif

5.2. つまずいたポイント

5.2.1. つまずいたポイントその1 : Lineをアニメーション内で新しく作成しない

最初に私の頭の中にあった流れは以下でした。

  • ローレンツ方程式を計算
const dx_1 = sigma * (y_1 - x_1) * dt;
const dy_1 = (x_1 * (rho - z_1) - y_1) * dt;
const dz_1 = (x_1 * y_1 - beta * z_1) * dt;
  • 新しい点を配列に追加
lorenz_1_Points.push(new THREE.Vector3(x_1, y_1, z_1));
  • Geometryを更新
lorenz_1_Geometry.setFromPoints(lorenz_1_Points);
  • 線を描く
let lorenz_1_Line = new THREE.Line(lorenz_1_Geometry, lorenz_1_Material);
実際に書いていたスクリプト
lorenz.js
import * as THREE from 'three';
import {OrbitControls} from 'three/addons/controls/OrbitControls.js';

// Initial Values
const X_1 = 0.1, Y_1 = 0.0, Z_1 = 0.0;
const X_2 = 0.0, Y_2 = 0.1, Z_2 = 0.0;

// Cionstants
const sigma = 28, rho = 10, beta = 8/3;
const dt = 0.01;

// Create a renderer
const renderer = new THREE.WebGLRenderer();
renderer.setSize( window.innerWidth, window.innerHeight );
document.body.appendChild( renderer.domElement );

// Create a camera
const camera = new THREE.PerspectiveCamera( 45, window.innerWidth / window.innerHeight, 1, 500 );
camera.position.set( 0, 0, 10 );

// Create scene
const scene = new THREE.Scene();

const lorenz_1_Material = new THREE.LineBasicMaterial({ color: 0x00ffff });
const lorenz_2_Material = new THREE.LineBasicMaterial({ color: 0xffff00 });
let lorenz_1_Geometry = new THREE.BufferGeometry();
let lorenz_2_Geometry = new THREE.BufferGeometry();

let x_1 = X_1, y_1 = Y_1, z_1 = Z_1;
let x_2 = X_2, y_2 = Y_2, z_2 = Z_2;
let lorenz_1_Points = [], lorenz_2_Points = [];

function updateLorenz() {
    // Calculate equation
    const dx_1 = sigma * (y_1 - x_1) * dt;
    const dy_1 = (x_1 * (rho - z_1) - y_1) * dt;
    const dz_1 = (x_1 * y_1 - beta * z_1) * dt;

    const dx_2 = sigma * (y_2 - x_2) * dt;
    const dy_2 = (x_2 * (rho - z_2) - y_2) * dt;
    const dz_2 = (x_2 * y_2 - beta * z_2) * dt;
    
    x_1 += dx_1; y_1 += dy_1; z_1 += dz_1;
    lorenz_1_Points.push(new THREE.Vector3(x_1, y_1, z_1));
    console.log(lorenz_1_Points)

    x_2 += dx_2; y_2 += dy_2; z_2 += dz_2;
    lorenz_2_Points.push(new THREE.Vector3(x_2, y_2, z_2));
    
    lorenz_1_Geometry.setFromPoints(lorenz_1_Points);
    lorenz_2_Geometry.setFromPoints(lorenz_2_Points);

    let lorenz_1_Line = new THREE.Line(lorenz_1_Geometry, lorenz_1_Material);
    let lorenz_2_Line = new THREE.Line(lorenz_2_Geometry, lorenz_2_Material);

    scene.add(lorenz_1_Line);
    scene.add(lorenz_2_Line);
}

const controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;

function animate() {
    updateLorenz();
    
    controls.update();
    renderer.render(scene, camera);
}
renderer.setAnimationLoop( animate );

しかしこれでは、シーンに複数の線を追記することになり、ブラウザが重くなります。正しい流れは以下になります。

  • Geometryのデータを削除
lorenz_1_Geometry.dispose();

Geometryの削除をしない場合、Buffer size too small for points data. Use .dispose() and create a new geometry.というエラーが出ました。
調査したところ、Geometryの最初の定義で含まれる点の数とローレンツ方程式の計算後に含まれる点の数が異なることが原因のようです。そのため、一度Geometryを削除する必要があります。

  • ローレンツ方程式を計算
const dx_1 = sigma * (y_1 - x_1) * dt;
const dy_1 = (x_1 * (rho - z_1) - y_1) * dt;
const dz_1 = (x_1 * y_1 - beta * z_1) * dt;
  • 新しい点を配列に追加
lorenz_1_Points.push(new THREE.Vector3(x_1, y_1, z_1));
  • 配列が無限に大きくなることを避けるために、Geometryに含む点の数に上限を設ける。
if (lorenz_1_Points.length > 30) lorenz_1_Points.shift();
  • Geometryを新たに作成
lorenz_1_Geometry = new THREE.BufferGeometry().setFromPoints(lorenz_1_Points);
  • 新たなGeometryをLineに登録
lorenz_1_Line.geometry = lorenz_1_Geometry;

上記の変更で、グラフィックボードのないノートパソコンでも滑らかに線が描画されるようになりました。

実際に書いていたスクリプト
lorenz.js
import * as THREE from 'three';
import {OrbitControls} from 'three/addons/controls/OrbitControls.js';

// Initial Values
const X_1 = 0.1, Y_1 = 0.0, Z_1 = 0.0;
const X_2 = 0.0, Y_2 = 0.1, Z_2 = 0.0;

// Cionstants
const sigma = 10, rho = 28, beta = 8/3;
const dt = 0.01;

// Create a renderer
const renderer = new THREE.WebGLRenderer();
renderer.setSize( window.innerWidth, window.innerHeight );
document.body.appendChild( renderer.domElement );

// Create a camera
const camera = new THREE.PerspectiveCamera( 45, window.innerWidth / window.innerHeight, 1, 500 );
camera.position.set(0, 50, 100);
camera.lookAt(0, 0, 0);

// Create scene
const scene = new THREE.Scene();

const lorenz_1_Material = new THREE.LineBasicMaterial({ color: 0x00ffff });
const lorenz_2_Material = new THREE.LineBasicMaterial({ color: 0xffff00 });
let lorenz_1_Geometry = new THREE.BufferGeometry();
let lorenz_2_Geometry = new THREE.BufferGeometry();

const lorenz_1_Line = new THREE.Line(lorenz_1_Geometry, lorenz_1_Material);
const lorenz_2_Line = new THREE.Line(lorenz_2_Geometry, lorenz_2_Material);

scene.add(lorenz_1_Line);
scene.add(lorenz_2_Line);

let x_1 = X_1, y_1 = Y_1, z_1 = Z_1;
let x_2 = X_2, y_2 = Y_2, z_2 = Z_2;
let lorenz_1_Points = [], lorenz_2_Points = [];

function updateLorenz() {
    lorenz_1_Geometry.dispose();
    lorenz_2_Geometry.dispose();

    // Calculate equation
    const dx_1 = sigma * (y_1 - x_1) * dt;
    const dy_1 = (x_1 * (rho - z_1) - y_1) * dt;
    const dz_1 = (x_1 * y_1 - beta * z_1) * dt;

    const dx_2 = sigma * (y_2 - x_2) * dt;
    const dy_2 = (x_2 * (rho - z_2) - y_2) * dt;
    const dz_2 = (x_2 * y_2 - beta * z_2) * dt;
    
    x_1 += dx_1; y_1 += dy_1; z_1 += dz_1;
    lorenz_1_Points.push(new THREE.Vector3(x_1, y_1, z_1));
    if (lorenz_1_Points.length > 30) lorenz_1_Points.shift();

    x_2 += dx_2; y_2 += dy_2; z_2 += dz_2;
    lorenz_2_Points.push(new THREE.Vector3(x_2, y_2, z_2));
    if (lorenz_2_Points.length > 30) lorenz_2_Points.shift();

    lorenz_1_Geometry = new THREE.BufferGeometry().setFromPoints(lorenz_1_Points);
    lorenz_2_Geometry = new THREE.BufferGeometry().setFromPoints(lorenz_2_Points);

    lorenz_1_Line.geometry = lorenz_1_Geometry;
    lorenz_2_Line.geometry = lorenz_2_Geometry;
}

const controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
controls.target.set(0, 0, 25);
controls.update();

function animate() {
    updateLorenz();
    controls.update();
    renderer.render(scene, camera);
}
renderer.setAnimationLoop( animate );

window.addEventListener('resize', () => {
    camera.aspect = window.innerWidth / window.innerHeight;
    camera.updateProjectionMatrix();
    renderer.setSize(window.innerWidth, window.innerHeight);
});

つまずいたポイントその2 : アニメーションの中のFor文

つまずいたポイントその1で書いたスクリプトでも動いていたのですが、スクリーンレコーディングを行うと画面が白飛びする現象が見られました。そこで、dispose()でGeometryを初期化するのではなく、最初からGeometryに必要な要素数を持つ配列を渡して置く戦略をとろうとしました。

実際に書いていたスクリプト
import * as THREE from 'three';
import {OrbitControls} from 'three/addons/controls/OrbitControls.js';

// Initial Values
const X_1 = 0.1, Y_1 = 0.0, Z_1 = 0.0;
const X_2 = 0.0, Y_2 = 0.1, Z_2 = 0.0;

// Update Coordinate Value
let x_1 = X_1, y_1 = Y_1, z_1 = Z_1;
let x_2 = X_2, y_2 = Y_2, z_2 = Z_2;

// Cionstants
const sigma = 10, rho = 28, beta = 8/3;
const dt = 0.01;
const MAX_POINTS = 30;

// Create a renderer
const renderer = new THREE.WebGLRenderer();
renderer.setSize( window.innerWidth, window.innerHeight );
document.body.appendChild( renderer.domElement );

// Create a camera
const camera = new THREE.PerspectiveCamera( 45, window.innerWidth / window.innerHeight, 1, 500 );
camera.position.set(0, 50, 100);
camera.lookAt(0, 0, 0);

// Create scene
const scene = new THREE.Scene();

// Create Geometry
const lorenz_1_Geometry = new THREE.BufferGeometry();
const lorenz_2_Geometry = new THREE.BufferGeometry();

// Allocate Point Data Buffer
const lorenz_1_Points = new Float32Array(MAX_POINTS * 3);
const lorenz_2_Points = new Float32Array(MAX_POINTS * 3);
lorenz_1_Geometry.setAttribute('position', new THREE.BufferAttribute(lorenz_1_Points, 3));
lorenz_2_Geometry.setAttribute('position', new THREE.BufferAttribute(lorenz_2_Points, 3));

// Set Draw range to 0
lorenz_1_Geometry.setDrawRange(0, 0);
lorenz_2_Geometry.setDrawRange(0, 0);

// Create Line Material
const lorenz_1_Material = new THREE.LineBasicMaterial({ color: 0x00ffff });
const lorenz_2_Material = new THREE.LineBasicMaterial({ color: 0xffff00 });

// Create Line
const lorenz_1_Line = {
    line: new THREE.Line(lorenz_1_Geometry, lorenz_1_Material),
    positions: lorenz_1_Points,
    count: 0
};
const lorenz_2_Line = {
    line: new THREE.Line(lorenz_2_Geometry, lorenz_2_Material),
    position: lorenz_2_Points,
    count: 0
};
scene.add(lorenz_1_Line.line);
scene.add(lorenz_2_Line.line);

function updateLorenz() {
    // Calculate equation
    const dx_1 = sigma * (y_1 - x_1) * dt;
    const dy_1 = (x_1 * (rho - z_1) - y_1) * dt;
    const dz_1 = (x_1 * y_1 - beta * z_1) * dt;
    x_1 += dx_1; y_1 += dy_1; z_1 += dz_1;

    const dx_2 = sigma * (y_2 - x_2) * dt;
    const dy_2 = (x_2 * (rho - z_2) - y_2) * dt;
    const dz_2 = (x_2 * y_2 - beta * z_2) * dt;
    x_2 += dx_2; y_2 += dy_2; z_2 += dz_2;

    // Update exist point data array
    const lorenz_1_posAttr = lorenz_1_Line.line.geometry.attributes.position;
    const lorenz_2_posAttr = lorenz_2_Line.line.geometry.attributes.position;

    const lorenz_1_index = Math.min(lorenz_1_Line.count, MAX_POINTS);
    const lorenz_2_index = Math.min(lorenz_2_Line.count, MAX_POINTS);

    if (lorenz_1_index > 0) {
        for (let i = lorenz_1_index - 1; i > 0; i--) {
            lorenz_1_posAttr.array[i * 3 + 0] = lorenz_1_posAttr.array[(i - 1) * 3 + 0];
            lorenz_1_posAttr.array[i * 3 + 1] = lorenz_1_posAttr.array[(i - 1) * 3 + 1];
            lorenz_1_posAttr.array[i * 3 + 2] = lorenz_1_posAttr.array[(i - 1) * 3 + 2];
        }
    }

    lorenz_1_posAttr.array[0] = x_1;
    lorenz_1_posAttr.array[1] = y_1;
    lorenz_1_posAttr.array[2] = z_1;

    lorenz_1_Line.line.geometry.setDrawRange(0, lorenz_1_index);

    lorenz_1_posAttr.needsUpdate = true;

    lorenz_1_Line.count++;

    if (lorenz_2_index > 0) {
        for (let i = lorenz_2_index - 1; i > 0; i--) {
            lorenz_2_posAttr.array[i * 3 + 0] = lorenz_2_posAttr.array[(i - 1) * 3 + 0];
            lorenz_2_posAttr.array[i * 3 + 1] = lorenz_2_posAttr.array[(i - 1) * 3 + 1];
            lorenz_2_posAttr.array[i * 3 + 2] = lorenz_2_posAttr.array[(i - 1) * 3 + 2];
        }
    }

        lorenz_2_posAttr.array[0] = x_2;
        lorenz_2_posAttr.array[1] = y_2;
        lorenz_2_posAttr.array[2] = z_2;

        lorenz_2_Line.line.geometry.setDrawRange(0, lorenz_2_index);

        lorenz_2_posAttr.needsUpdate = true;

        lorenz_2_Line.count++;

    controls.update();
    renderer.render(scene, camera);    
}

const controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
controls.target.set(0, 0, 25);
controls.update();

function animate() {
    updateLorenz();
    controls.update();
    renderer.render(scene, camera);
}
renderer.setAnimationLoop( animate );

window.addEventListener('resize', () => {
    camera.aspect = window.innerWidth / window.innerHeight;
    camera.updateProjectionMatrix();
    renderer.setSize(window.innerWidth, window.innerHeight);
});

しかし、改善するために作成したスクリプトは逆に非常に重くなってしまいました。原因は、 アニメーションループの中にCPUに負荷のかかるfor文を入れてしまったことのようでした。

アニメーション内にFor文を入れると処理が重くなり、動画も重くなる。

そこで、set()を用いた高速化を図り、現在のスクリプトに至りました。このスクリプトでブラウザのアニメーションを録画したところ、全く白飛びが発生しませんでした。

つまずいたポイントその3 : ブラウザのサイズ変更に自動対応させる

これはつまずいたポイントというより、新たに得た知識ですが、以下のスクリプトを加えることで、アニメーション描画中にブラウザのウインドウサイズが変更されても、自動的に対応されます。

window.addEventListener('resize', () => {
    camera.aspect = window.innerWidth / window.innerHeight;
    camera.updateProjectionMatrix();
    renderer.setSize(window.innerWidth, window.innerHeight);
});

まとめ

今回は、将来的な「人生ログの3D可視化」を見据え、Three.jsを用いた動的な描画に挑戦しました。

本記事での収穫

  • Three.jsの基本サイクル:Scene, Camera, Rendererの構築から、アニメーションループによる描画更新の流れを理解できた。
  • OrbitControlsによる操作性向上:わずか数行の追加で、3D空間を自由自在に操れる体験を得られた。
  • リソース管理の重要性:アニメーションループ内でオブジェクトを生成し続けると、あっという間にブラウザのメモリを圧迫し、パフォーマンスが低下することを身をもって学んだ。

次なる課題

ローレンツ方程式の描画を通じて、座標の連続性を表現することはできました。しかし、目標とする「人生ログの可視化」には、まだ以下の壁があります。

  • スケーラビリティ:数千、数万の「事実(Fact)」を描画した際のパフォーマンス維持。
  • インタラクションの深掘り:人生ログでは、点や線ではなく、そのグラフが示す内容をテキストでインタラクティブに表示する必要があります。
  • 実データの結合:前作で作ったTauri + Reactの環境に、今回のThree.jsをどう統合していくかは要検討です。

一歩ずつではありますが、これからも人生ログアプリ作成のための技術スタックの勉強に励んでいきたいと思います。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?