FREENANCE Ad

【Three.js】Webカメラを使ったオーディオビジュアライザーの作り方

オーディオビジュアライザーとパーティクルアニメーション
FREENANCE Ad
ENGINEER

オーディオビジュアライザーとパーティクルアニメーション

Webカメラは通常、ビデオチャットに用いられることが多く、あまりクリエイティブな用途で使うことはありません。

しかしThree.jsと組み合わせれば、ゆがみ効果のついた魅力的なオーディオビジュアライザーが作れます。

今回はシンプルなコードだけで作れる、ユーザーのWebカメラを使ったオーディオビジュアライザーの作りかたを、チュートリアル形式でお伝えします。

ステップ1. Webカメラから画像データを取得し、キャンバスに描写し続ける

まず、Webカメラにアクセスして画像を取得します。

Webカメラへアクセスする

getUserMedia()を使うと、Webカメラにアクセスできます。

<video id="video" autoplay style="display: none;"></video>

 

video = document.getElementById("video");

const option = {
    video: true,
    audio: false
};

// Get image from camera
navigator.getUserMedia(option, (stream) => {
    video.srcObject = stream;  // Load as source of video tag
    video.addEventListener("loadeddata", () => {
        // ready
    });
}, (error) => {
    console.log(error);
});

画像をキャンバスに描画する

カメラにアクセスできたら、カメラから画像を取得してキャンバスに描画します。

const getImageDataFromVideo = () => {
    const w = video.videoWidth;
    const h = video.videoHeight;
    
    canvas.width = w;
    canvas.height = h;
    
    // Reverse image like a mirror
    ctx.translate(w, 0);
    ctx.scale(-1, 1);

    // Draw to canvas
    ctx.drawImage(image, 0, 0);

    // Get image as array
    return ctx.getImageData(0, 0, w, h);
};

取得したimageDataを使う

ctx.getImageData()は、「RGBA」が順に並んだ配列を返します。

[0]  // R
[1]  // G
[2]  // B
[3]  // A

[4]  // R
[5]  // G
[6]  // B
[7]  // A...

これを活用して、ピクセルの色情報にアクセスします

for (let i = 0, len = imageData.data.length; i < len; i+=4) {
    const index = i * 4;  // Get index of "R" so that we could access to index with 1 set of RGBA in every iteration.?0, 4, 8, 12...?
    const r = imageData.data[index];
    const g = imageData.data[index + 1];
    const b = imageData.data[index + 2];
    const a = imageData.data[index + 3];
}

画像のピクセルへアクセスする

画像を中央に配置できるように、X座標とY座標を計算しましょう。

const imageData = getImageDataFromVideo();
for (let y = 0, height = imageData.height; y < height; y += 1) {
    for (let x = 0, width = imageData.width; x < width; x += 1) {
        const vX = x - imageData.width / 2;  // Shift in X direction since origin is center of screen
        const vY = -y + imageData.height / 2;  // Shift in Y direction in the same way (you need -y)
    }
}

画像のピクセルからパーティクルを生成する

THREE.Geometry() と THREE.PointsMaterial()を使って、パーティクルを生成しましょう。

各ピクセルは、頂点としてジオメトリに追加されます。

const geometry = new THREE.Geometry();
geometry.morphAttributes = {};
const material = new THREE.PointsMaterial({
    size: 1,
    color: 0xff0000,
    sizeAttenuation: false
});

const imageData = getImageDataFromVideo();
for (let y = 0, height = imageData.height; y < height; y += 1) {
    for (let x = 0, width = imageData.width; x < width; x += 1) {
        const vertex = new THREE.Vector3(
            x - imageData.width / 2,
            -y + imageData.height / 2,
            0
        );
        geometry.vertices.push(vertex);
    }
}
particles = new THREE.Points(geometry, material);
scene.add(particles);

パーティクルをキャンバスに描画する

カメラから画像データを取得し、そこからグレースケール値を計算することによって、パーティクルを利用して画像を更新します。

すべてのフレームにおいてこのプロセスを呼び出し、画面をビデオのように更新し続けるという仕組みです。

const imageData = getImageDataFromVideo();
for (let i = 0, length = particles.geometry.vertices.length; i < length; i++) {
    const particle = particles.geometry.vertices[i];
    let index = i * 4;

    // Take an average of RGB and make it a gray value.
    let gray = (imageData.data[index] + imageData.data[index + 1] + imageData.data[index + 2]) / 3;

    let threshold = 200;
    if (gray < threshold) {
        // Apply the value to Z coordinate if the value of the target pixel is less than threshold.
        particle.z = gray * 50;
    } else {
        // If the value is greater than threshold, make it big value.
        particle.z = 10000;
    }
}
particles.geometry.verticesNeedUpdate = true;

ステップ2. オーディオファイルを読み込み、周波数を取得する

ここからは、音の処理方法を解説します。

オーディオファイルの読み込み/再生を行う

まずTHREE.AudioLoader()を使ってオーディオを読み込みます。

const audioListener = new THREE.AudioListener();
audio = new THREE.Audio(audioListener);

const audioLoader = new THREE.AudioLoader();
// Load audio file inside asset folder
audioLoader.load('asset/audio.mp3', (buffer) => {
    audio.setBuffer(buffer);
    audio.setLoop(true);
    audio.play();  // Start playback
});

analyser.getAverageFrequency() を使って、平均周波数を取得しましょう。

この値をパーティクルのZ座標に適用すれば、ビジュアライザーに奥行きがつきます。

オーディオの周波数を取得する

以下がオーディオの周波数を取得する方法です。

// About fftSize https://developer.mozilla.org/en-US/docs/Web/API/AnalyserNode/fftSize
analyser = new THREE.AudioAnalyser(audio, fftSize);

// analyser.getFrequencyData() returns array of half size of fftSize.
// ex. if fftSize = 2048, array size will be 1024.
// data includes magnitude of low ~ high frequency.
const data = analyser.getFrequencyData();

for (let i = 0, len = data.length; i < len; i++) {
    // access to magnitude of each frequency with data[i].
}

ステップ3. Webカメラとオーディオを合体する

最後に、カメラの画像とオーディオデータを組み合わせます。

取得した画像とオーディオを組み合わせる

ここまでのプロセスを組み合わせることで、パーティクルを使用してWebカメラの画像を描画し、オーディオデータによってビジュアルを操作できます。

const draw = () => {
    // Audio
    const data = analyser.getFrequencyData();
    let averageFreq = analyser.getAverageFrequency();

    // Video
    const imageData = getImageData();
    for (let i = 0, length = particles.geometry.vertices.length; i < length; i++) {
        const particle = particles.geometry.vertices[i];
    
        let index = i * 4;
        let gray = (imageData.data[index] + imageData.data[index + 1] + imageData.data[index + 2]) / 3;
        let threshold = 200;
        if (gray < threshold) {
            // Apply gray value of every pixels of web camera image and average value of frequency to Z coordinate of particle.
            particle.z = gray * (averageFreq / 255);
        } else {
            particle.z = 10000;
        }
    }
    particles.geometry.verticesNeedUpdate = true;  // Necessary to update

    renderer.render(scene, camera);

    requestAnimationFrame(draw);
};

これで完成です!

おわりに

以上が、ユーザーのWebカメラを使ったオーディオビジュアライザーを作るプロセスです。複雑そうに思えますが、コードと仕組みは意外にシンプルですよね。

今回はTHREE.Geometry と THREE.PointsMaterialを使いましたが、デモ2ではシェーダーを使っています。

オーディオビジュアライザーとパーティクルアニメーション2

▲デモ2

ぜひこのチュートリアルを参考に、オリジナルの作品を作ってみてください。

(執筆:Takemoto Ryota 翻訳:Nakajima Asuka 編集:内田一良 a.k.a. じきるう)

SHARE

  • 広告主募集
  • ライター・編集者募集
  • WorkshipSPACE
エンジニア副業案件
Workship