Hello, afroWorld

afroscriptのafroblog

【ハンズオン形式】WebAudio×Three.jsで、Beatboxにノリノリで反応する3Dモデル(なぜかブタ)を作る

※この記事は、Leverages Advent Calendar 2018の3日目です!

やりたいこと。

どーも。teratailのDevRelをやってる@afroscriptです。非エンジニアです。 なので、コードの書き方が変とか説明が変とかはお手柔らかにご指摘していただけるとうれしいです...!mm

さて、本題。88年生まれの僕からすると、中学校時代の懐かしい思い出のひとつと言えば、そう、ハモネプです。RAG FAIRおっくんのボイパに憧れ、学芸会的なやつで5人くらいでアカペラやったなー。ま、そのときはベースだったけど。(てか、Wikipediaみると、おっくん、もう40歳なんだ!衝撃!)

さてさて、その頃からひっそりと憧れ、その後ひっそりと隠れてたまに練習して、いつのまにかひっそりとできるようになってたbeatbox(僕ら世代は「ボイパ」って言った方がいいかも)を、今回はWebAudioとThree.jsを使って3D空間で可視化してみようと思います。

Demo

完成動画はこんな感じ。

ソースコード

完成版のソースコードこちら

3D空間の準備&線を表示してみる

最初にThree.jsで3D空間を用意し、白い線を表示してみましょう。

f:id:afroscript:20181201145633g:plain

なお、今回のThree.jsのバージョンは、r98です。マウスコントロールできるようにOrbitControls.jsも入れてます。これで、マウスでグリグリ動かせるようになります。

また、白い線以外に、いろんな3Dオブジェクトを表示してみたい方は下記を参考にしてみてください。

qiita.com

index.html

<!DOCTYPE html>
<html>
    <head>
       <title>3D Beatbox</title>
       <meta charset="utf-8">
       <style>
            body {
                background-color: #000;
                margin: 0px;
                overflow: hidden;
            }
        </style>
   </head>

    <body>
        <script src="js/three.min.js"></script>
        <script src="js/libs/OrbitControls.js"></script>

        <script src="js/main.js"></script>

        <script>

       </script>

    </body>
</html>

main.js

ざっくり説明すると、initの中で、camera、sceneを用意する、そこにライトや3Dオブジェクト(ここでは createLine()で線を生成している)をシーンに追加、そして、animate()で繰り返しレンダリングする、ということが行われています。

var container;

var camera, scene, renderer;

var mouseX = 0, mouseY = 0;

var initCameraPositinZ = 250;

var windowHalfX = window.innerWidth / 2;
var windowHalfY = window.innerHeight / 2;

var orbitContlols;

var line;

init();
animate();



function init() {

    container = document.createElement( 'div' );
    document.body.appendChild( container );

    camera = new THREE.PerspectiveCamera( 45, window.innerWidth / window.innerHeight, 1, 2000 );
    camera.position.z = initCameraPositinZ;

    // scene

    scene = new THREE.Scene();

    var ambientLight = new THREE.AmbientLight( 0xcccccc, 0.4 );
    scene.add( ambientLight );

    var pointLight = new THREE.PointLight( 0xffffff, 0.8 );
    camera.add( pointLight );
    scene.add( camera );

    // renderer
 
    renderer = new THREE.WebGLRenderer();
    renderer.setPixelRatio( window.devicePixelRatio );
    renderer.setSize( window.innerWidth, window.innerHeight );
    container.appendChild( renderer.domElement );

    document.addEventListener( 'mousemove', onDocumentMouseMove, false );

    // Controls

    orbitControls = new THREE.OrbitControls(camera, renderer.domElement);

    window.addEventListener( 'resize', onWindowResize, false );

    // createLine
    createLine();

}

function onWindowResize() {

    windowHalfX = window.innerWidth / 2;
    windowHalfY = window.innerHeight / 2;

    camera.aspect = window.innerWidth / window.innerHeight;
    camera.updateProjectionMatrix();

    renderer.setSize( window.innerWidth, window.innerHeight );

}

function onDocumentMouseMove( event ) {

    mouseX = ( event.clientX - windowHalfX ) / 2;
    mouseY = ( event.clientY - windowHalfY ) / 2;

}

function createLine() {

    var lineGeometry = new THREE.Geometry();
    for(var i = 0; i < 32; ++i) {
        lineGeometry.vertices.push(
            new THREE.Vector3( i * 10, Math.random() * 10, 0 )
        ); 
    }

    //線オブジェクトの生成   
    line = new THREE.Line( lineGeometry, new THREE.LineBasicMaterial( { color: 0xffffff} ) );

    //sceneにlineを追加
    scene.add( line );

}



//

function animate() {

    requestAnimationFrame( animate );
    render();

}

function render() {

    camera.lookAt( scene.position );

    renderer.render( scene, camera );

}

コードの補足

次のステップのために、Lineオブジェクトの生成のところだけ少し解説しておきます。function createLine()のところです。

下記の部分で、3次元の座標(ベクトル)を32個用意してます。

    for(var i = 0; i < 32; ++i) {
        lineGeometry.vertices.push(
            new THREE.Vector3( i * 10, Math.random() * 10, 0 )
        ); 
    }

具体的には、 (0, 1~10のランダムの数字, 0),(10, 1~10のランダムの数字, 0),(20, 1~10のランダムの数字, 0), ..., (310, 1~10のランダムの数字, 0) といった(x, y, z)座標が32個ある感じです。x座標が10ずつ増えて、y座標は1〜10のランダムな数字になって、z座標は0のままです。

で、LINEオブジェクトは、1つ目の座標と2つ目の座標を結んで、2つ目の座標と3つ目の座標を結んで、、、、といった形で生成されるので、xy平面(画面を読み込んだ状態では、画面の右側がx軸方向、上側がy軸方向、奥行きがz軸方向です)上に、先に表示した折れ線ができるのです。

音を3Dのラインで可視化してみよう

次に、この3DLineを使って、音を表現してみましょう。音を扱うにはWebAudioを使います。音源をどうするかはいろいろ方法あるのですが、今回はPCのマイクから入力された音を取り出して、3DLineに反映することにします。

完成すると、下記のようになります。

f:id:afroscript:20181201165124g:plain

マイクから音をひろってくる

init()の前に、下記のコードを追加します。

var data;

// PCのマイクから音をひろってくる

var audioctx = new AudioContext();

var analyser = audioctx.createAnalyser();
analyser.fftSize = 128;
analyser.minDecibels = -100;
analyser.maxDecibels = -30;

var getUserMedia = navigator.getUserMedia ? 'getUserMedia' :
    navigator.webkitGetUserMedia ? 'webkitGetUserMedia' :
    navigator.mozGetUserMedia ? 'mozGetUserMedia' :
    navigator.msGetUserMedia ? 'msGetUserMedia' :
    undefined;
var astream, micsrc;
var conditions={audio:true, video:false};

const Mic = () => {
    navigator[getUserMedia](
        conditions,
        (stream) => {
            astream=stream;
            micsrc=audioctx.createMediaStreamSource(stream);
            micsrc.connect(audioctx.destination);
            micsrc.connect(analyser);
        },
        (e) => { console.error(e); }
    );
}

また、getUserMedia()でPCのマイクにアクセスして、Mic()関数の中で、PCマイクからの入力がanalyzerノードに入っていくようになっています。そして、このAnalyzerから、スペクトラムデータ、や波形データを取得することができるので、次はこのanalyzer取得したデータを使って、3DLineを表示してみましょう。ちなみに、var data;は、analyzerから取得したデータを格納する変数として使います。

なお、WebAudioをいろいろ触ってみたい方、理解したい方は、こちらの資料(特に「8. マイク入力を可視化する」の部分)を参考にしてください。

マイクからの音のスペクトラムデータを3Dの線に適用する

function createLine()を少し改造します。render()内で、analyzerから波形データを取得し、変数dataに格納、それをcreateLine()内で、Lineオブジェクトの座標を指定するのに使っています。

また、マイクからの音をリアルタイムで取得して反映したい(つまり、レンダリングのタイミングで毎回音声データを取得→そのデータでLineを作りたい)ので、createLine()は、render()内にで実行するように書きましょう。(そして、init()内からは削除します。)

function createLine(data) {

    scene.remove(line)

    var lineGeometry = new THREE.Geometry();
    for(var i = 0; i < 32; ++i) {
        lineGeometry.vertices.push(
            new THREE.Vector3( i * 10, data[i] * 0.5, 0 )
        ); 
    }

    //線オブジェクトの生成   
    line = new THREE.Line( lineGeometry, new THREE.LineBasicMaterial( { color: 0xffffff} ) );

    //sceneにlineを追加
    scene.add( line );

}

// 省略

function render() {

    data = new Uint8Array(512);

    analyser.getByteFrequencyData(data); //Spectrum Dataの取得
    // analyser.getByteTimeDomainData(data); //Waveform Dataの取得

    // createLine
    createLine(data);

    camera.lookAt( scene.position );

    renderer.render( scene, camera );

}

main.jsのinit()内からcreateLine()を削除↓

function init() {
    /*
     *...省略...
    */

    //createLine() ← ★これを削除!
}

3Dモデルを表示しよう

次は、3Dモデルを表示してみましょう。こんな感じです。少しづつシュールになってきました。

f:id:afroscript:20181204002051g:plain
pig_line

下記ブログを参考にさせていただきました。 creatorsblog.nijibox.jp

3Dモデルのダウンロード

下記サイトから、好きな3Dモデルをダウンロードします。3Dモデルの形式にはいくつか種類があるみたいですが、今回は参考ブログにしたがってobjファイルがあるものを探してみました。

free3d.com

無料にも関わらず、かなりリアルなものやかわいいものもあり迷いますが、今回は豚にしてみました。特に意味はないですが、このリアルな豚がノリノリだとなかなか愛おしくなりそうな気がしたので。

f:id:afroscript:20181201170857p:plain

下記からdownloadし、解凍したら、./modelsディレクトリ下に置きます。

https://free3d.com/3d-model/pig-13314.html

3Dモデルの表示

こちらの記事を参考(sourceはこちら)に実装してみます。

まずは、sammpleコードの./js/loaders内にある下記3つのjsファイルをコピペし、自身の作成中フォルダにも同様に./js/loadersファルをつくり、格納します。

  • DDSLoader.js
  • MTLLoader.js
  • OBJLoader.js

そして、index.htmlの<body></body>の中に、下記を追加しましょう。

     <script src="js/loaders/DDSLoader.js"></script>
        <script src="js/loaders/MTLLoader.js"></script>
        <script src="js/loaders/OBJLoader.js"></script>

また、main.jsですが、冒頭の変数宣言部分に、var obj;を追加。さらに、init()内のscene.add( camera );の下に3Dモデルを読み込み、表示するためのコードを追加します。

var obj; // ここを追加

// 省略

function init() {

    //省略

    var pointLight = new THREE.PointLight( 0xffffff, 0.8 );
    camera.add( pointLight );
    scene.add( camera );

    // model ← ★ここ以下を追加!

    var onProgress = function ( xhr ) {
  
        if ( xhr.lengthComputable ) {

            var percentComplete = xhr.loaded / xhr.total * 100; 
            console.log( Math.round( percentComplete, 2 ) + '% downloaded' );

        }

    };

    var onError = function () { };

    THREE.Loader.Handlers.add( /\.dds$/i, new THREE.DDSLoader() );

    new THREE.MTLLoader()
        .setPath( 'models/' )
        .load( '', function ( materials ) {
            
            materials.preload();

            new THREE.OBJLoader()
                .setMaterials( materials )
                .setPath( 'models/' )
                .load( 'pig.obj', function ( object ) {

                    objmodel = object.clone();
                    objmodel.scale.set(100, 100, 100);      // 縮尺の初期化
                    objmodel.rotation.set(0, 0, 0);         // 角度の初期化
                    objmodel.position.set(0, 0, 0);         // 位置の初期化

                    obj = new THREE.Object3D();
                    obj.add(objmodel);

                    scene.add(obj);                
                
                }, onProgress, onError );

        } );

    // renderer

    // 省略

}

// 以下、省略

3Dモデルの色をつけましょう

次は、先ほどの豚ちゃんに色をつけます...とやりたかったんですが、なぜかうまくいかず。。。

下記の★ここ★の部分に、pig.mtlと入れれば大丈夫なはずなんですが、、、THREE.DDSLoader.parse: Unsupported FourCC codeというエラーが出て対処できず。。。

new THREE.MTLLoader()
        .setPath( 'models/' )
        .load( '★ここ★', function ( materials ) 

先ほどの参考ブログだと、PhotoshopでMTLファイルを出力してうまくいってるみたいなんですが、Photoshopが手元にないと、アドベントカレンダーの期日が迫ってきたので、一旦あきらめます。。。 (そのうち、修正します。。。)

ま、無色の豚もかわいいということで。

3Dモデルを音でノリノリにしよう

さて、次はこの3Dのブタちゃん(いつのまにか愛着湧いて「ちゃん」付けに...w)が、音に合わせてノリノリになるようにしましょう。ノリノリにするには、音声データをもとに大きくなったり小さくなったりすればいいですね。つまり、拡大/縮小です。ということで、下記の関数objTransform()を作成し、createLine()の下にでも追加しましょう。

function objTransform (data) {

    obj.scale.x = data[0] * 0.008;
    obj.scale.y = data[0] * 0.008;
    obj.scale.z = data[0] * 0.008;

}

そして、render()に、objTransform()を追加します。これで、レンダリングごとにobjのサイズを拡大してくれます。

function render() {

    data = new Uint8Array(512);

    analyser.getByteFrequencyData(data); //Spectrum Dataの取得
    // analyser.getByteTimeDomainData(data); //Waveform Dataの取得

    // createLine
    createLine(data);

    // 3Dモデルの拡大・縮小 ← ★ここを追加!
    objTransform(data);

    camera.lookAt( scene.position );

    renderer.render( scene, camera );

}

こんな感じになります。自分の声や周囲の音に合わせて、ノリノリなってくれます。もはやすごく愛おしい。

f:id:afroscript:20181204003528g:plain

ブタでいっぱにしよう

1匹だとかわいそうなので、増殖しましょう。まず、冒頭のvar obj;var obj = [];に変更し、今回は9匹表示するため、var num_pig = 9;を用意します。

そして、init()内のnew THREE.MTLLoader()の中を変更し、それに合わせてobjTransform()の中も変更します。

var obj = [];          // var obj; → var obj = []; に変更
var num_pig = 9; // ★これを追加!

// 省略

function init() {

    // 省略

    new THREE.MTLLoader()
        .setPath( 'models/' )
        .load( '', function ( materials ) {
            
            materials.preload();

            new THREE.OBJLoader()
                .setMaterials( materials )
                .setPath( 'models/' )
                .load( 'pig.obj', function ( object ) { // ★ここから変更!

                    var objmodel = [];

                    for (i = 0; i < num_pig; i++) {

                        objmodel[i] = object.clone();
                        objmodel[i].scale.set(10, 10, 10);      // 縮尺の初期化
                        objmodel[i].rotation.set(0, 0, 0);         // 角度の初期化
                        var y = (i < 3) ? 50 : (i < 6) ? 0 : -50;
                        objmodel[i].position.set(-50 + i % 3 * 50, y, 0);  // 位置の初期化

                        obj[i] = new THREE.Object3D();
                        obj[i].add(objmodel[i]);

                        scene.add(obj[i]);  

                    }              
                
                }, onProgress, onError );

        } );

// 省略

}

// 省略

function objTransform (data) {

    for (i = 0; i < num_pig; i++) {

        var num_scale = Math.random() * 0.01;
        obj[i].scale.x = data[0] * num_scale;
        obj[i].scale.y = data[0] * num_scale;
        obj[i].scale.z = data[0] * num_scale;

    }

}

こんな感じです。

f:id:afroscript:20181204013147g:plain
9pigs_line

beatboxしてみよう

beatboxを習得しましょう。

さて、仕上げです。ここからはbeatboxの練習を初めていきますが、ここは気合いが大事ということと、ちょっとしたコツだけお伝えしておきます。

コツは、まずは人気(ひとけ)のないところに移動し、いろんな口の形を変えながらそれっぽい音を出してみることです。するとたまに、「おっ、今のそれっぽくね?」みたいな音が出るので、それを繰り返しつつ、口の形や息の吐き出し方(普通に吐いたり、歯の隙間から出してみたり、唇の隙間から出したり、とか)を変えてみて、いい感じの音にしていきます。

よく分かんないなって人は、「ブンッ」「ツーッ」「カッ」「ツッ」を何回も言いながら、口で言ってる部分を徐々になくして、無声音に変えていってみると、それっぽいことになります。

ちなみに、ちょっとだけかっこつけた励ましを書いておくと、「ボイパ」とはボイスパーカッションの略なので、ボイス(声)でパーカッション(打楽器の演奏)をしなきゃいけないですが、「beatbox」とは、あくまでbeat(≒リズム)が出てくるbox(箱)です。つまり、別にドラムの音に似せる必要はないのです。リアルな音に近いかどうかは重要でなく、各々の思うままの音を、自分なりにかっこいいと思うリズムで出していけばいいんです。

↑はい、ただのウケウリです。

マイクを購入

さて、ここは重要な項目です。マイクを買いましょう。絶対かって?はい、絶対です。

PC内臓のマイクを使えばいいじゃないかって?それは違います!マイクを握った方がテンションが上がるじゃん!すると、いい音が出る気がするんですよ!

あと補足すると、意外とBeatboxをやっているときの口の動きって、基本的にキモいんですよw(個人的な感想)

なのでマイクで隠した方がそれっぽく、かっこよく見えるし、マイクを買いましょう。ということで、もろもろ調べて下記のマイクを購入。

www.amazon.co.jp

カメラの回転させよう

おまけですが、beatbox中はあまりマウスいじったりできないので、カメラをぐるぐる回して、3D感を感じられるようにしましょう。render()のところで、camera.positionを変更していくと、回転は表現できますので、下記のように変更してください。また、一旦ブタちゃんたちに集中するため、createLine(data);も削除しておきましょう。

// 省略

var counter = 0;

// 省略

function render() {

    data = new Uint8Array(512);

    analyser.getByteFrequencyData(data); //Spectrum Dataの取得
    // analyser.getByteTimeDomainData(data); //Waveform Dataの取得

    // createLine
    //createLine(data); ← ★ここを削除!

    // 3Dモデルの拡大・縮小
    objTransform(data);

    // カメラの回転 ← ★ここを追加!
    counter += 0.8;
    camera.position.x = initCameraPositinZ * Math.sin(counter * Math.PI / 180);
    camera.position.z = initCameraPositinZ * Math.cos(counter * Math.PI / 180);
    camera.lookAt(new THREE.Vector3(0, 0, 0)); // 原点方向を見つめる

    camera.lookAt( scene.position );

    renderer.render( scene, camera );

}

ということで、ついにこれで完成です!あとは、こころおきなくbeatboxをしていきましょう:)

Happy Coding & Happy Beatboxing!!

まとめ

  • ひとまず3Dオブジェクトがマイクからの音声データで動くようになった。
  • 残念ながらテクスチャを貼るのがうまくいかなかった→近いうち再挑戦します。
  • でもbeatboxの特性はまだ生かしきれてない気がする。
  • 例えば、ベース音とかの低い音をキャッチすると、なにか特定の動きが加わるとか特定のオブジェクトだけが動くとかできたらかっこよさそう。
  • 波長の表現を、いろんな方向から稲妻みたいな感じで出てくるようにしたい(中2病w)
  • キーボード入力から、好きにノリノリ3Dオブジェクトを出現/消滅させられるとかもしたい(p押したらブタ増えて、d押したら犬出てきてみたいな)
  • キーボード入力から、いや、できればLeap Motionの手の動きを察知して、音を出したり、3Dの効果(映像にノイズ入ったりとか)つけれたらだいぶかっこよくなりそう。

だいぶ夢が広がりますね:)

3Dオブジェクトを変えるとこんなこともできる

これは、トーラスノットという3Dオブジェクトです。今回は3Dモデルをノリノリにしましたが、デフォルトで用意されている3Dオブジェクトを、色も変えたりするとなんかいい感じになりますね:)ってことで、みなさんもいろいろ試してみてください:)

参考記事

過去にWebAudioハンズオンやったときの資料。

WebAudioに入門したいという方はこちらをどうぞ。 webmusicdevelopers.appspot.com

three.jsを触ってみた(その2) 〜 3Dモデルを描画してみるの巻 〜

Three.jsで3Dモデルを表示する際に参考にさせていただきました。 creatorsblog.nijibox.jp