03-6845-0775平日10:00〜18:00受付
無料ガイド
お問い合わせ
資料請求

2025年03月07日

Agoraのライブビデオ配信にリアルタイムの3Dアバターを加える

Agoraのライブビデオ配信にリアルタイムの3Dアバターを加える

※この投稿は、Agoraの日本代理店であるブイキューブが、Agoraブログを翻訳した記事です。


急速に進化する現在のデジタル社会では、ライブストリーム動画がトレンドになっています。今までの配信形式よりユーザーがもっと没入的な機能でカスタマイズできるストリーミングオプションを期待しています。コンテンツの作成者は独創性をもつ形式でのライブ配信を求めており、配信者の動きや表情を反映するダイナミックな3Dアバターへのニーズが生まれました。

image02

今までリアルタイムのバーチャルアバターの生成には、複雑なモーションキャプチャー設備と高度なソフトウェアが必要となり、一般ユーザーや独立しているクリエイターにとっては敷居が高いものでした。しかし現在、人工知能の進歩によって開発の難易度は劇的に下がりました。コンピュータービジョンの技術進歩に伴って、人間の表情の動きを正確にキャプチャーし、リアルタイムでデジタルフォームに変換できる高度なAIアルゴリズムをデバイス上で実行できるようになりました。
この記事では、MediaPipeReadyPlayerMeの3Dアバターを使って、3DバーチャルアバターをAgoraライブ配信に統合する方法を紹介します。これによって視聴者のエンゲージメントを高めたり、アプリのビデオ通話/ライブ放送に面白いコンテンツを加えることが可能になります。これから3Dバーチャルペルソナ(3D virtual personas)を実現するための必要なステップを案内します。

前提条件

Agora + MediaPipeプロジェクト

本記事ではAgoraのライブビデオ配信にリアルタイムの3Dアバターを加える方法の紹介を主軸にするため、Agora Video SDKを用いたWebアプリの実装方法については、前提として理解されているものとして、この部分の詳細説明は省略します。Agora Video SDKを用いたWebアプリのビルド方法を復習するにはこちらを参照してください。

 

最初にまずは、デモプロジェクトをダウンロードしてください。ダウンロードした後に、ターミナルにてプロジェクトフォルダの階層まで移動して、npmを使用してnodeパッケージをインストールしてください。

git clone git@github.com:digitallysavvy/agora-mediapipe-readyplayerme.git
cd agora-mediapipe-readyplayerme
npm i

コアストラクチャー(HTML)

index.htmlのHTML構造から始めます。<body>の上部は「通話(call)」のUI要素(UI element)です。ここには、リモートビデオ用のコンテナ、オーディオ/ビデオのミュート/アンミュートのボタン付きのローカルユーザー用のコンテナ、およびビデオ通話の終了ボタンが含まれます。

<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <link rel="icon" type="image/svg+xml" href="/agora-box-logo.svg" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <link rel="stylesheet" type="text/css" href="style.css" />
    <title>Agora Live Video Demo</title>
  </head>
  <body>
    <div id="container"></div>
    <div id="local-user-container"></div>
    <div id="local-media-controls">
      <button id="mic-toggle" class="media-active">Mic</button>
      <button id="video-toggle" class="media-active">Video</button>
      <button id="leave-channel" class="media-active">Leave</button>
    </div>
    <div id="overlay" class="modal">
      <div id="form-container">
        <h1 id="form-header">Avatar Video Chat</h1>
        <form id="join-channel-form">
          <div id="form-body">
            <div class="form-group">
              <label for="form-rpm-url">Ready Player Me URL</label>
              <input type="text" id="form-rpm-url" placeholder="http://models.readyplayer.me/<MODEL-ID>.glb" class="form-control">
            </div>
            <div id="form-footer">
              <button type="submit" id="join-channel-btn">Join Channel</button>
            </div>
          </div>
        </form>
      </div>
    </div>
    <script type="module" src="/main.js"></script>
  </body>
</html>

通話UI以外に、ユーザーがアバターのURLを入れるためのオーバーレイ画面と、チャンネルに参加するためのボタンも必要です。

Agoraクライアントとデータ保存

main.jsでは、AgoraのSDKを利用するために新しいAgoraクライアントを作成し、localMediaを使用してオーディオ、ビデオ、キャンバス(canvas)のトラックとそのアクティブ状態の参照を保持します。また、MediPipeのコンピュータービジョンから取得したデータを保存するにはheadRotationとblendShapesが必要になります。

// Create the Agora Client
const client = AgoraRTC.createClient({ 
  codec: 'vp9',
  mode: 'live',
  role: 'host'
})

const localMedia = {
  audio: {
    track: null,
    isActive: false
  },
  video: {
    track: null,
    isActive: false
  },
  canvas: {
    track: null,
    isActive: false
  },
}

// Container for the remote streams
let remoteUsers = {}                

//  store data from facial landmarks
let headRotation
let blendShapes

DOMContentLoadedとイベントリスナー

Webページが読み込まれると、Agoraのイベント、メディアコントロールとフォーム送信のリスナーを追加します。リスナーの配置が完了すると、一旦オーバーレイフォームを表示するための準備ができました。

// Wait for DOM to load
document.addEventListener('DOMContentLoaded', async () => {
  // Add the Agora Event Listeners
  addAgoraEventListeners()
  // Add listeners to local media buttons
  addLocalMediaControlListeners()
  // Get the join channel form & handle form submission
  const joinform = document.getElementById('join-channel-form')
  joinform.addEventListener('submit', handleJoin)
  // Show the overlay form
  showOverlayForm(true) 
})

注意:全てのイベントが正常にトリガーされるようにチャンネルに参加(join)するより前のタイミングでクライアントのイベントリスナーを追加してください。

3Dとアバターのセットアップ

ReadyPlayerMeはAppleのARKit ARFaceAnchorの位置情報の命名規則に準拠した3Dファイルを提供するため、ReadyPlayerMeの3Dアバターを利用することは本記事の前提条件の一つとしています。この定義は業界標準であり、MediaPipeから出力された形式と一致しています。
これからコードの部分を説明します。ユーザーが 「Join 」ボタンをクリックするとThreeJSのシーンを初期化し<canvas>をlocalUserContainerに追加します。

// get the local-user container div
const localUserContainer = document.getElementById('local-user-container')

// create the scene and append canvas to localUserContainer
const { scene, camera, renderer } = await initScene(localUserContainer)

新しく作成したシーンを使用してglbURLでユーザーのReadyPlayerMeアバターを読み込みます。ここでURLパラメーターがglbURLに付加されていることが分かります。これは、ReadyPlayerMeが提供するデフォルトの.glbファイルにはブレンドシェイプが含まれていないからです。これらのパラメータはアバター用のReadyPlayerMe RESTful APIの一部です。
3Dアバターが読み込まれると、この3Dアバターのシーングラフ(scene graph)を処理してすべてのノード(node)を含むオブジェクトを作成します。これで、headMeshへのアクセスが速くなります。

// append url parameters to glb url - load ReadyPlayerMe avatar with morphtargets
const rpmMorphTargetsURL = glbURL + '?morphTargets=ARKit&textureAtlas=1024'
let nodes
// Load the GLB with morph targets
const loader = new GLTFLoader()
loader.load(rpmMorphTargetsURL, 
  async (gltf) => {
  const avatar = gltf.scene
  // build graph of avatar nodes
  nodes =  await getGraph(avatar)
  const headMesh = nodes['Wolf3D_Avatar']
  // adjust position 
  avatar.position.y = -1.65
  avatar.position.z = 1
  
  // add avatar to scene
  scene.add(avatar)
},
(event) => {
  // outout loading details
  console.log(event)
})

シーン(scene)の初期化から3Dアバターの読み込み完了までには一定の時間が発生します。この待機時間を考慮した対策として、モデルの読み込み中はローディングアニメーションを表示し、3Dアバターがシーンに追加されたタイミングでそのアニメーションを消すことで、ユーザーの混乱を防ぐことができます。

// show a loading animation
const loadingDiv = document.createElement('div')
loadingDiv.classList.add('lds-ripple')
loadingDiv.append(document.createElement('div'))
localUserContainer.append(loadingDiv)

/* loader.load - success callback */
loadingDiv.remove() // remove the loading spinner

Agoraでビデオ要素(video element)を初期化

Agoraでビデオ要素(video element)を初期化Agoraを使ってカメラにアクセスし、ビデオとオーディオトラックを作成します。作成されたビデオトラックをビデオ要素(video element)のソースとして設定することで、カメラ映像を表示することができます。詳細な実装手順や参考情報については、こちらをご覧ください。

// Init the local mic and camera
await initDevices('music_standard', '1080_3')
// Create video element
const video = document.createElement('video')
video.setAttribute('webkit-playsinline', 'webkit-playsinline');
video.setAttribute('playsinline', 'playsinline');
// Create a new MediaStream using camera track and set it the video's source object
video.srcObject = new MediaStream([localMedia.video.track.getMediaStreamTrack()])

MediaPipeのセットアップ

ユーザーの顔やジェスチャーを認識する前に、MediaPipeのコンピュータービジョン技術を活用するために必要な最新のWebAssembly(WASM)ファイルをダウンロードします。これらのファイルは、FaceLandmarkerタスクをセットアップする際に不可欠です。FaceLandmarkerは、ビデオストリームからユーザーの顔にある特定の「注目点(points of interest)」を識別する高精度なコンピュータービジョンアルゴリズムで、AIが顔の特徴を正確にトラッキングすることを可能にします。
コンピュータービジョンのタスクでは、AIにリクエストを送り、推定値(prediction)と呼ばれる信頼度の高い結果を受け取ります。このプロセスは、各ビデオフレームに対して実行され、predictionLoopと呼ばれるループで繰り返し処理されます。これにより、連続的かつリアルタイムで顔やジェスチャーの認識が可能となります。
顔のランドマーク(landmark)の構成では、FaceLandmarkerを設定して2つの重要なデータタイプを生成します。


1つ目は、outputFacialTransformationMatrixesで、顔の位置、回転、スケールといった情報の推定値を提供します。このデータは頭の動きをトラッキングするために不可欠であり、変換行列(transformation matrix)を利用して顔の向きや動きをリアルタイムで監視します。


2つ目は、outputFaceBlendshapes: trueです。これは、ブレンドシェイプ(blend shapes)またはシェイプキー(shape keys)として知られる3Dモデリング技術に活用されるデータを生成します。このデータにより、3Dメッシュ(3D mesh)が「動作前」と「動作後」の状態をスムーズにつなぐことが可能です。例えば、口が「閉じた状態」(0で表される)から「開いた状態」(1で表される)へと変化する際の中間状態を補完することで、動作が自然に見えるようになります。これにより、3Dアーティストがすべての顔の動きを個別にモデル化する必要がなく、レンダリングエンジンが中間の状態を自動的に補完する効率的な手法を実現します。
さらに、この設定はARKit標準のブレンドシェイプに対応しており、推定値は0〜1の範囲で提供されます。これにより、52種類の異なる顔の動きがカバーされ、表情や動作の幅広い表現が可能です。これらのデータタイプは、3DアバターやARアプリケーションでリアルな表情や動きを再現するために非常に重要な要素となります。

// initialize MediaPipe vision task
const faceLandmarker = await initVision()

// init MediaPipe vision
const initVision = async () => {
  // load latest Vision WASM
  const vision = await FilesetResolver.forVisionTasks('https://cdn.jsdelivr.net/npm/@mediapipe/tasks-vision@latest/wasm')
  // configure face landmark tracker
  const faceLandmarker = await FaceLandmarker.createFromOptions(
    vision, { 
      baseOptions: {
        modelAssetPath: `https://storage.googleapis.com/mediapipe-models/face_landmarker/face_landmarker/float16/1/face_landmarker.task`,
      },
      outputFaceBlendshapes: true,
      outputFacialTransformationMatrixes: true,
      runningMode: 'VIDEO'
    })
  return faceLandmarker
}

コンピュータービジョンの予測ループ

faceLandmarkerと<video/>のセットアップが完了すると、predictionLoopのループを起動して各ビデオフレームに対してMediaPipeのコンピュータービジョンのタスクを実行することができます。推定値(予測結果)が返されるとFacialTransformationMatrixesを取得してheadRotationを計算することができます。この予測結果は、顔メッシュ(face mesh)のブレンドシェイプ用の重み(weight)の推定値も返してくれます。

video.addEventListener("loadeddata", () => {
  video.play()                            // start video playback
  initPredictLoop(faceLandmarker, video)  // start face landmarks prediction loop
})

const initPredictLoop = (faceLandmarker, video) => {
  // flag to keep track of stream's playbacktime
  let lastVideoTime = -1
  // prediction loop
  const predict = () => {
    // create a timestamp
    const timeInMs = Date.now()
    // while video is still streaming
    if (lastVideoTime !== video.currentTime) {
      lastVideoTime = video.currentTime
      // run vison task to detect faces in video frame
      const result = faceLandmarker.detectForVideo(video, timeInMs)
      // get face matrix transformation for face 1
      const faceMatrix = result.facialTransformationMatrixes
      if (faceMatrix && faceMatrix.length > 0) {
        const matrix = new THREE.Matrix4().fromArray(faceMatrix[0].data)
        headRotation =  new THREE.Euler().setFromRotationMatrix(matrix)
      }
      // get blend shape predictions for face 1
      const blendShapePredictions = result.faceBlendshapes
      if (blendShapePredictions && blendShapePredictions.length > 0){
        blendShapes = blendShapePredictions[0].categories
      }
    }
    // predect om every frame update
    requestAnimationFrame(predict)
  }
  // start loop
  requestAnimationFrame(predict)
}

コミュータービジョン + 3Dアバター

Three.jsのシーンをレンダリングする際には、レンダリングループ(render loop)を使用します。このレンダリングループを初期化する際に、頭部の回転とブレンドシェイプの強度を更新する関数を渡しておきます。この関数は、予測ループ(prediction loop)で取得した結果を基にして値を更新します。


具体的には、予測ループで獲得した頭部の回転データ(変換行列)やブレンドシェイプの強度値を使用して、3Dモデルの動きをリアルタイムで反映します。レンダリングループ内でこれらの更新処理を行うことで、スムーズで自然な動作や表情のアニメーションを実現できます。


レンダリングループの役割は、シーン全体を一定間隔で再描画し、予測結果をモデルに反映させることで、インタラクティブなユーザー体験を提供することです。このプロセスにより、動的でリアルな表現を実現することが可能になります。

// create the render loop
const initRenderLoop = (scene, camera, renderer, sceneUpdates) => {
  const render = (time) => {
    // update the scene 
    sceneUpdates(time)
    // render scene using camera
    renderer.render(scene, camera)
    // add render back to the call stack
    requestAnimationFrame(render)
  }
  // start render loop
  requestAnimationFrame(render)
}

initRenderLoop(scene, camera, renderer, (time) => {
  // return early if nodes or head rotation are null
  if(!nodes || !headRotation) return
  // apply rotatation data to head, neck, and shoulders bones
  nodes.Head.rotation.set(headRotation.x, headRotation.y, headRotation.z)
  nodes.Neck.rotation.set(headRotation.x/2, headRotation.y/2, headRotation.z/2)
  nodes.Spine1.rotation.set(headRotation.x/3, headRotation.y/3, headRotation.z/3)
  // loop through the blend shapes
  blendShapes.forEach(blendShape => {
    const headMesh = nodes.Wolf3D_Avatar
    const blendShapeIndex = headMesh.morphTargetDictionary[blendShape.categoryName]
    if (blendShapeIndex >= 0) {
      headMesh.morphTargetInfluences[blendShapeIndex] = blendShape.score
    }
  })
})

デフォルトでは、アバターの口の動きはユーザーが表情を強調した時にしか見えないと思うかもしれません。通常、人が話す時の顔はここまで不自然な動きをしません。アバターの動き効果を上げるためにブレンドシェイプのscoreを多めに調整することで、アバターの口の動きがより見やすくなります。
調整するブレンドシェイプをリスト化して、基本スコア(base score)の倍率を設定します。アバターの口の動きをユーザーの口の動きに連動させ、かつ実際の口元の形状に近い形でコントロールするために、閾値の上限と下限を設定します。この閾値は、ブレンドシェイプ(blend shapes)や口の開閉を示す数値の範囲を制御するために使用されます。

// mouth blend shapes
const mouthBlendShapes = [
  'mouthSmile_L', 'mouthSmile_R', 'mouthFrown_L','mouthFrown_R',
  'mouthOpen', 'mouthPucker','mouthWide','mouthShrugUpper','mouthShrugLower',
]
// multipliyer to embelish mouth movement
const exagerationMultiplier = 1.5
const threshold ={ min: 0.25, max: 0.6}

倍率を適用するには、mouthBlendShapesリストで特定のキーを確認する必要があります。これはスコア(score)を適用するのと同じループ内で行うことができます。口のブレンドシェイプを認識すると同時にこの値が閾値内に収まるかどうかも確認します。

// loop through the blend shapes
blendShapes.forEach(blendShape => {
  const headMesh = nodes.Wolf3D_Avatar
  const blendShapeIndex = headMesh.morphTargetDictionary[blendShape.categoryName]
  if (blendShapeIndex >= 0) {
    // exaggerate the score for the mouth blendshapes
    if (mouthBlendShapes.includes[blendShape.categoryName] && blendShape.score > threshold.min && blendShape.score < threshold.max ) {
      blendShape.score *= exagerationMultiplier
    }
    headMesh.morphTargetInfluences[blendShapeIndex] = blendShape.score
  }
})

ThreeJSからAgoraビデオストリームへ

image04

レンダリングループは3Dシーンをキャンバス(canvas)にレンダリングします。レンダリングされた3Dシーンを<canvas>からAgoraに送るために、captureStreamを作成しビデオトラックでカスタムビデオトラックを初期化します。キャンバス要素(canvas element)でAgoraのビデオトラックを作成する方法について詳しくはこちらを参照してください。

// Get the canvas
const canvas = renderer.domElement
// Set the frame rate
const fps = 30
// Create the captureStream
const canvasStream = canvas.captureStream(fps)
// Get video track from canvas stream
const canvasVideoTrack = canvasStream.getVideoTracks()[0]
// use the canvasVideoTrack to create a custom Agora Video track
const customAgoraVideoTrack = AgoraRTC.createCustomVideoTrack({
  mediaStreamTrack: canvasVideoTrack,
  frameRate: fps
})
localMedia.canvas.track = customAgoraVideoTrack
localMedia.canvas.isActive = true
// publish the canvas track into the channel
await client.publish([localMedia.audio.track, localMedia.canvas.track])

ローカルクライアントがチャンネルに参加すると、事前に設定しておいたイベントリスナーがトリガーされます。このリスナーは、他のユーザーがチャンネルに入室した際のイベントを検知します。ユーザーが新たにチャンネルに入室すると、そのユーザーのビデオストリームが取得され、指定されたHTML要素(例:#container)に表示されます。

動作確認

Viteを利用することでローカルでのテストが容易になります。ターミナルにてプロジェクトフォルダの階層まで移動して、以下のコマンドを叩いてコードを実行します。

npm run dev

これでローカルサーバーが起動されるので、これからコードを実際にテストします。ReadyPlayer.Meから好きなアバターを選択してアバターのリンクをコピーします。そして、このリンクを入力フォームに貼り付けて「Join 」ボタンをクリックしてください(イメージは以下の添付画像を参照)。

image05

image06

チャンネルに複数のユーザーをシミュレートする際は、最初のタブで実行しているアプリケーションのURLをコピーし、別のブラウザウィンドウを開いてそのURLを貼り付けて実行します。同じURLを使用することで、他のユーザーも確実に同じチャンネルに参加できます。
なお、ブラウザの仕様上、ウェブサイトのタブがバックグラウンド状態になると、AnimationFrameの呼び出しが一時停止されるように最適化されています。このため、2つのウィンドウを個別に開かない場合、片方のキャンバス(canvas)が正常に描画されない可能性があります。これを回避するため、各ウィンドウを独立して開くことをお勧めします。

image07

複数のデバイスで動作確認する場合は、安全なhttps接続環境においてプロジェクトを実行する方法が必要になります。これについて実施方法は2つあります。1つは、ローカル端末用のカスタムSSL証明書を設定します。もう1つは、ngrokのようなサービスを利用します。後者はローカルマシンからトンネルを作成し、httpsのURLを生成してくれます。

終わりに

以上が、Web向けのAgora Video SDKをMediaPipeのコンピュータービジョンと組み合わせてカスタムの3Dアバターを実現する方法になります。魅力的なウェビナーやインタラクティブな教育プラットフォーム、さらにはライブビデオを主要な機能とするアプリケーションの開発において、今回の実装例は良い出発点となるでしょう。XR(クロスリアリティ)を実現するには、ぜひ本記事で紹介したコードを自由に微調整してAIの高度な機能を実装してみてください。

今回の記事では、Agoraの2つのビデオ機能:Raw VideoとCustom Videoを網羅しています。Web版のAgoraビデオ通話についての機能概要や実装方法はこちらを参照してください。

 


無料でAgoraをご体験いただけます。

こちらからサインアップし、リアルタイムの音声とビデオ通話を実装してみましょう!
利用分数が多くなければ料金は発生しません。

ブイキューブ

執筆者ブイキューブ

Agoraの日本総代理店として、配信/通話SDKの提供だけでなく、導入支援から行い幅広いコミュニケーションサービスに携わっている。

関連記事

先頭へ戻る