TouchDesignerとThree.jsでWebコンテンツ制作

# はじめに
この記事はTouchDesigner Advent Calendar 13日目の記事です。
こんにちは。普段TouchDesigner, Houdini等を使って創作活動をしているものです。
どんな記事を書こうかと思っていたのですが、最近TouchDesignerをWebコンテンツ制作に活かせないかなーと感じていたので、TouchDesignerで作ったアセットをThree.jsにもっていって描画するみたいなことをやろうと思います。

The Interactive & Immersive HQさんがYouTubeでチュートリアルを上げていたので、それを参考に進めました。
リンク: https://youtu.be/lCyILpkPQ4g

今回の記事ではTouchDesignerで生成したインスタンシング、カメラの情報を使ってThree.jsで描画するところまでをやります。
ソースコード: https://github.com/tai5863/td-threejs-sample

![threejs-with-code.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/459643/922b7a6a-f5bb-6493-5a0c-9820db365d52.jpeg)



# 開発環境
TouchDesigner: 2021.10330
Three.js: r135


# どうやってThree.jsにもっていくか
TouchDesignerではPythonのモジュールを使ってjsonの書き出しが出来るので、インスタンシングやカメラの情報をjsonに書き出し、それをThree.js側で読み込むという方法で行います。

# TouchDesignerで画を作る
まずはTouchDesignerで好きな画を作ります。このサンプルではText Sop -> Sprinkle Sopで表面上にランダムなポイントを生成し、Rotation, Scale, Colorなどの情報を適当に生成してインスタンシングし、一つのオペレーターにまとめています。
画を作った後は、executeHogeToJsonの箇所でchopの情報をjsonに書き出すプログラムを実行し、jsonファイルを書き出します。
![touchdesigner.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/459643/41c7fe14-4360-5a4a-2225-a7af8cbc7457.png)

```python:executeInstancesToJson.py
import json

# インスタンシングの情報をまとめたオペレーターを取得
instances = op('instances')

def onOffToOn(channel, sampleIndex, val, prev):
	
	samples = []
	
        # サンプルの数だけインスタンシングの情報を取得しjsonに保存する
	for i in range(0, instances.numSamples):
		
		instanceData = {}
		
		for c in instances.chans():
			
			instanceData[c.name] = instances[c.name][i]
		
		samples.append(instanceData)
	
	with open('data/instances.json', 'w') as outfile:
		
		json.dump(samples, outfile)
	
	return
```

# Three.js側でjsonを読み込んで画を作る

TDで作成したjsonファイルを使ってThree.jsで描画します。

```html:index.html
<script>
// レンダラー用の変数
let scene, camera, pointLight, renderer, controls;

init();

function init() {
    // jsonファイルの読み込み
    fetch('/data/cam_info.json')
        .then(response => response.json())
        .then(async cameraData => {
            scene = new THREE.Scene();
            camera = new THREE.PerspectiveCamera(cameraData['fov'], window.innerWidth / window.innerHeight, cameraData['near'], cameraData['far']);
            camera.position.z = cameraData['tz']; 

            // レンダラーの作成
            renderer = new THREE.WebGLRenderer({ antialias: true });
            renderer.setPixelRatio(window.devicePixelRatio);
            renderer.setSize(window.innerWidth, window.innerHeight);

            document.body.appendChild(renderer.domElement);

            // リサイズ処理
            function onWindowResize() {
                camera.aspect = window.innerWidth / window.innerHeight;
                camera.updateProjectionMatrix();
                renderer.setSize(window.innerWidth, window.innerHeight);
            }
            onWindowResize();
            window.addEventListener('resize', onWindowResize, false);

            // ライトのセッティング (環境光、方向光)
            pointLight = new THREE.PointLight(0xffffff, 1);
            pointLight.position.set(0, 0, 10);
            scene.add(pointLight);

            // カメラコントロールのセッティング
            controls = new THREE.OrbitControls(camera, renderer.domElement);

            // メッシュの作成
            addInstacedMesh();

            // 描画開始
            draw();
        })
}

function addInstacedMesh() {
    // jsonファイルの読み込み
    fetch('/data/instances.json')
        .then(response => response.json())
        .then(async instanceData => {
            // ジオメトリ (箱の大きさをTDに合わせる)
            let geometry = new THREE.BoxGeometry(1, 1, 1);

            // マテリアル
            let material = new THREE.MeshPhongMaterial();

            // メッシュ (インスタンシング)
            let mesh = new THREE.InstancedMesh(geometry, material, instanceData.length);

            let matrix = new THREE.Matrix4();
            for (let i = 0; i < instanceData.length; i++) {
                let instance = instanceData[i];

                let position = new THREE.Vector3(instance['tx'], instance['ty'], instance['tz']);
                let rotation = new THREE.Vector3(instance['rx'] * Math.PI / 180, instance['ry'] * Math.PI / 180, instance['rz'] * Math.PI / 180);
                let scale = new THREE.Vector3(instance['scale'], instance['scale'], instance['scale']);
                let color = new THREE.Color(instance['r'], instance['g'], instance['b']);

                // 行列の計算を行って、インスタンシングの位置、角度、スケールを設定
                matrix = new THREE.Matrix4().multiplyMatrices(new THREE.Matrix4().makeRotationY(rotation.y), new THREE.Matrix4().makeRotationX(rotation.x));
                matrix.premultiply(new THREE.Matrix4().makeRotationZ(rotation.z));
                matrix.scale(scale);
                matrix.setPosition(position);
                mesh.setMatrixAt(i, matrix);

                // 色の設定
                mesh.setColorAt(i, color);
            }
            scene.add(mesh);
        })
}

function draw() {
    // レンダラー、カメラコントロールのアップデート
    renderer.render(scene, camera);
    controls.update();

    // 描画更新
    requestAnimationFrame(draw);
}
</script>
```

細かい説明は省略しますが、ポイントは以下のように、インスタンシング用のメッシュを生成し、setMatrixAtとsetColorAtを使ってjsonから読みだされた情報から行列計算を行う&色を設定するところかなと思います。また、今回のサンプルでは動きのないメッシュを表示するだけで終わっていますが、Three.jsのShaderMaterialでカスタムシェーダー等を使って動きを加えるなどの発展のさせ方はあるかなと考えています。

```html:index.html
<script>
// メッシュ (インスタンシング)
let mesh = new THREE.InstancedMesh(geometry, material, instanceData.length);

let matrix = new THREE.Matrix4();
for (let i = 0; i < instanceData.length; i++) {
    let instance = instanceData[i];

    let position = new THREE.Vector3(instance['tx'], instance['ty'], instance['tz']);
    let rotation = new THREE.Vector3(instance['rx'] * Math.PI / 180, instance['ry'] * Math.PI / 180, instance['rz'] * Math.PI / 180);
    let scale = new THREE.Vector3(instance['scale'], instance['scale'], instance['scale']);
    let color = new THREE.Color(instance['r'], instance['g'], instance['b']);

    // 行列の計算を行って、インスタンシングの位置、角度、スケールを設定
    matrix = new THREE.Matrix4().multiplyMatrices(new THREE.Matrix4().makeRotationY(rotation.y), new THREE.Matrix4().makeRotationX(rotation.x));
    matrix.premultiply(new THREE.Matrix4().makeRotationZ(rotation.z));
    matrix.scale(scale);
    matrix.setPosition(position);
    mesh.setMatrixAt(i, matrix);

    // 色の設定
    mesh.setColorAt(i, color);
}
</script>
```

# さいごに
TouchDesignerがWebコンテンツ制作の現場で使われている事例はあまりみかけないですが、TDのさくっと作れるスピーディーさは素晴らしいので、うまく利用していければいいなあと思っています。