目次

はじめに

株式会社WinTicketでXRエンジニアをしています加田です。本記事では2024年4月にリリースした、競輪中継のオリジナルライブ映像「WINLIVE」の描画部分について紹介します。まず最初にシステム全体の構成を紹介し、そのあとにゲームエンジンのUnityを使ってどのようにリアルタイム映像を制作するかについて重点的に説明していきます。

オリジナルライブ映像「WINLIVE」とは

新機能オリジナルライブ映像WINLIVE 4月25日開始

「WINLIVE」は、競輪初心者に競輪の面白さを分かりやすく感じてもらえるよう開発された、WINTICKETのオリジナル競輪中継映像です。WINTICKETアプリ内やABEMA「WINTICKET ミッドナイト競輪」番組内で見ることができます。「WINLIVE」内では独自に算出した選手の体力(HP)表示を軸に、レース進捗状況、ライン状況、スリップストリームの状況などが分かるようになっています。2024年8月現在では小倉、前橋、平塚の3競輪場で実施されており、今後実施競輪場を拡大していく予定です。

「WINLIVE」の一例は下記からご覧になれます。

従来の映像と比較して様々な情報が付与されています。
従来映像とWINLIVEの比較。WINLIVEにはHPや温存OKなど情報が追加されている

レースの状況に応じてテロップとエフェクトの出し分けを行っています。選手に追従するエフェクトを表示することで、どの選手に注目すべきかという示唆も行っています。
レースの状況によってエフェクトの出し分けを行っている

また、レース前にはHP一覧を表示し、これから始まるレースがどのような力関係で行われるかの示唆を行っています。
レース前HP一覧表示

システム全体構成

「WINLIVE」システム全体の構成は下記の図のように、複数の映像と複数のWindows PCで構成されており、PC間はローカルネットワークで接続されています。

システム全体構成図

システムは大きく分けて4パートに分かれています。

  • 映像伝送
  • 物体認識
  • HP計算
  • CG描画

映像伝送パートはシステムの入口と出口になります。後段の処理で必要となる映像を競輪場から配信スタジオへIP網で送り、CG合成された中継映像をAWSのMediaLiveに配信する機能を持ちます。
物体認識パートでは、競輪場から送られてきた映像を元に2種類の推測を行っています。1つは競輪場のどこに選手がいるかという3D座標認識、もう1つは放送用映像内のどこに、どの選手が映っているかという2D座標認識を行っています。
HP計算パートでは、算出された3D座標を使って選手HP計算やレースの進捗率計算を行っています。
本記事のメインとなるCG描画パートでは、計算されたHPなどの情報と2D座標をもとに表示物のON/OFFや位置の制御を行っています。ここで出力されたCGを映像ミキサーで中継映像と合成し、アプリや番組へ配信しています。CG描画パートでは下記の画像のように、大きく分けて画面上下の位置に固定されたテロップと、選手に追従するエフェクトがあります。レースの進捗状況や選手HPはHP計算PCで算出された情報をもとに描画を行います。そして、エフェクトのON/OFFタイミングはレースの進捗状況や選手HP状況など、様々な要素を組み合わせて決定しています。例えば「選手HPの数値表示を始めるのはレースが3周半を過ぎたタイミング」といった具合です。また、選手に追従するエフェクトは2D座標認識PCで計算された選手の座標をもとに、画面上の表示位置を決定しています。

レース中のエフェクトの表示

選手HPの数値表示

なお、物体認識パートとHP計算パートについては、先日行われたCA DATA NIGHT #4内の「競輪選手の体力を視覚化するための物体認識とデータサイエンスの融合」講演で詳しく説明しています。興味のある方は是非ご覧ください。

CGと中継映像の合成方法

CGと中継映像の合成方法にはいくつかの方法があります。今回は3つの手法を紹介します。

1.キャプチャデバイスを使ってUnity内で合成する手法
2.クロマキーで出力し、映像ミキサーで合成する手法
3.Fill Keyで出力し、映像ミキサーで合成する手法

キャプチャデバイスを使ってUnity内で合成する手法

まず最初に考えたのがビデオキャプチャ機器などを用いて中継映像をUnity内に取り込み、CGをUnity内で中継映像と合成したのちに出力する手法です。

キャプチャデバイスを使ってUnity内で合成する手法

この手法の利点は実装が容易で、特殊な機器を使わずに実現できる点です。Unityではビデオキャプチャデバイスからの映像をWebCamTextureクラスを使うことで取得することができます。あとはRawImageなどにTextureとして設定すれば映像とCGの合成ができるようになります。WebCamTextureのコンストラクタにデバイス名を入れてPlay()を実行すればキャプチャが開始されます。なお、接続されているデバイス名一覧はWebCamTexture.devicesから取得することができます。下記に簡単なサンプルコードを示します。


using UnityEngine;
using UnityEngine.UI;

public class WebCamTextureSample : MonoBehaviour
{
    [SerializeField] RawImage rawImage;

    void Start()
    {
        var webcamTexture = new WebCamTexture("DeviceName");
        webcamTexture.Play();

        this.rawImage.texture = webcamTexture;
    }
}

この手法の弱点は、何らかの理由でUnityやPCが止まってしまった場合に、中継映像の送信まで止まってしまうことです。競輪中継のライブ配信を見ている人が、万が一でも中継を見られなくなる状況は避けたいので、この手法は採用を見送りました。

クロマキーで出力し、映像ミキサーで合成する手法

クロマキーとは、背景を特定の色で塗りつぶし、映像合成の際に特定の色を透明として扱うことで合成する技術です。グリーンバック合成やブルーバック合成ともよばれる技術です。クロマキーの場合、万が一CG描画用のPCが止まってしまっても、映像ミキサーでCG映像の合成を止めれば中継映像自体はそのまま配信することができます。また、背景に特定の色を指定するだけなので実装も簡単です。
しかし、クロマキーは特定の色を透明として扱うため、CG映像内で出現しない色を背景として使う必要があります。クロマキーではグリーン、ブルー、マゼンタなどが背景色によく使われますが、競輪ではこれらの色がの車番の色で使われてしまっています。車番を画面要素として使えなくなるのは表現が狭まってしまうので、クロマキーも採用を見送りました。

競輪選手のユニフォーム例
https://keirin.jp/pc/static/beginner/abcs/cyclists.html より引用

クロマキー合成の失敗例
上記はグリーンをキーにしてクロマキー合成した例ですが、6番車の色が消えてしまっています。また、アルファが0% or 100%であれば正しく描画されますが、半透明は表現に制約が出てしまいます。

Fill Keyで出力し、映像ミキサーで合成する手法

最後に紹介するのはFill Key(フィルキー)という合成手法です。キーフィルやエクスターナルキーとよばれることもあります。この手法は、色情報とアルファ情報を別映像として出力します。
色情報の映像をFill、アルファ情報の映像をKeyとよびます。下記のような2つの映像を利用して、合成映像を作成します。
フィル映像の例
キー映像の例
Keyは黒が透明(アルファ0%)、白が不透明(アルファ100%)となります。また、中間値は輝度値で表現します。

フィルキーと中継映像の合成イメージ

映像出力が2つになりますが、色や半透明の制限がなくなり自由な表現が可能になります。また、クロマキー合成と同様に万が一CG描画用のPCが止まってしまっても、中継映像自体はそのまま配信することが可能になります。

「WINLIVE」では合成手法としてこのFill Keyを使用しています。現在、出力デバイスとしてUltraStudio HD Miniを使用しています。また、UnityプラグインとしてAVPro DeckLinkを利用しています。有料のアセットになりますが、Fill Key以外にも様々な機能が内包されています。

描画システムの構成

ここからは描画システムの作りを詳細に見ていきます。競輪では1つの競輪場で1日に7レースから12レース開催されます。描画システムは最初のレース前に起動した後は、特別な理由がない限りは手動オペレーションを無しに1日の運用ができるように設計されています。起動からレース終了までの1サイクルを図にまとめました。このサイクルを1日の全レースが終わるまで繰り返します。

描画システムのサイクル

APIからは発走時間、レース名、規定周回数といったレース情報や、各選手の名前、最大HP、ライン情報()といった情報を取得しています。ここで取得した発走時間を用いて、レースが始まる前に待機中のテロップや発走30秒前に表示されるHP一覧テロップの表示タイミングを制御しています。

競輪ではラインとよばれるチームがあり、ラインが協力してレースを走ります。

UDP通信

レース中の通信の流れは下記の図の通りです。

UDP通信の経路図

各PCは後段のPCへUDPでデータを送っています。3D座標認識PCからHP計算PCへ各選手の3D座標、速度の情報を送ります。HP計算PCは3D座標、速度をもとに選手HPやレース進捗率の計算を行います。そして計算されたHPやレース進捗率をCG描画PCへUDP通信として送ります。また、2D座標認識PCは中継映像内で各選手がどの位置にいるかの情報をCG描画PCへUDP通信として送っています。UnityでのUDP受信はUdpClientを利用して下記のように実装しました。なおRxライブラリとしてR3を利用しています。


using System;
using System.Diagnostics;
using System.Net;
using System.Net.Sockets;
using R3;

/// 
/// ネットワーク経由でデータを受け取るクラス
/// 
public class NetworkDataReceiver : IDisposable
{
    UdpClient udpClient = null;

    Subject subject = new Subject();

    /// 
    /// データの受信イベント(メインスレッドではないので注意)
    /// 
    public Observable OnReceivedBytes => this.subject;

    public int Port { get; private set; }

    /// 
    /// コンストラクタ
    /// 
    /// 
    public NetworkDataReceiver(int port)
    {
        this.Port = port;
        this.udpClient = new UdpClient(port);
        this.udpClient.BeginReceive(OnReceived, this.udpClient);
    }

    private void OnReceived(System.IAsyncResult result)
    {
        UdpClient getUdp = (UdpClient)result.AsyncState;
        IPEndPoint ipEnd = null;

        var getByte = getUdp.EndReceive(result, ref ipEnd);

        this.subject.OnNext(getByte);

        getUdp.BeginReceive(OnReceived, getUdp);
    }


    public void Dispose()
    {
        this.udpClient.Close();
    }
}

上記のクラスを使う際はポートをコントラクタに入れてインスタンスを作成します。データが送られてくるとOnReceivedBytesが自動的に呼び出されます。
ここで1点注意事項があります。UDP通信自体は非メインスレッドで行われます。しかし、Unityの仕様上、UIの変更やUnity関数を使うときは、メインスレッドで行う必要があります。そのため、受信したデータをそのままUIに入れようとすると正しく動作しないことがあります。下記のコードでは.ObserveOnMainThread()を使って、必ずメインスレッドで処理するようにしています。


NetworkDataReceiver dataReceiver;

void Start()
{
    this.dataReceiver = new NetworkDataReceiver2(12345);
    this.dataReceiver
        .OnReceivedBytes
        .ObserveOnMainThread()  //UIへの適用などUnityの関数を使う場合にはメインスレッドで処理をする
        .Subscribe(bytes =>
    {
        //bytes[]をパースして使える形に変換する
    }).AddTo(this);
}

void OnDestroy()
{
    this.dataReceiver.Dispose();
}

オペレーター画面

描画システムの画面は、描画をする画面だけではなく運用するオペレーターが様々な情報を確認できるよう作られています。画面は大きく分けて4つに分割されており

  1. エフェクト表示部
  2. 3D座標認識表示部
  3. 選手HP表示部
  4. その他情報表示部

の構成になっています。
これらは画面全体を構成するCanvasがあり、その子要素として各表示部のCanvasがある作りになっています。

オペレーターが見ている画面

①エフェクト表示部が出力しているCGを表示するCanvasです。このCanvasの作りについては次節で詳しく説明します。エフェクト表示部を見ることで、どのエフェクトが出ているか画面上で確認することができます。
②3D座標認識表示部は3D座標認識の結果を競輪場の上から見た視点で描画しています。3D座標認識の精度はHPやスリップストリーム()の計算に影響を与え、結果としてCG出力にも影響があります。3D座標認識の精度を瞬時に見ることができるように同一画面上に表示を行っています。
③選手HP表示部はHP計算PCから送られてきた選手のHP、速度、スリップストリームなどの情報を表示しています。CGとして表示する前に、HPの状況を確認できます。
④その他情報表示部はAPIから取得したレース情報やデバッグのための機能などを表示しています。また、他PCから正しく通信が送られてきているかの通信状況表示も行っています。

スリップストリームとは、高速で走行している車体の直後にできる気圧の低さのことです。気圧低下による吸引効果や空気抵抗の低減によって、通常と同じ速度を少ない力で出すことができます。

エフェクト表示Canvas

エフェクト表示を行っているCanvasは、描画PCの最終出力となるCG映像の構築を行っています。Fill Key出力とオペレーター画面への出力を両立させるために、Camera,Canvas,RenderTextureの関係は下記の図のように構成されています。

エフェクトを描画するCanvas
カメラとRender Textureの設定

エフェクトを描画しているCanvasはRender ModeをScreen Space - Cameraに設定し、出力用のカメラ(Output Camera)をRender Cameraに指定しています。また、出力用のカメラはTarget Texturesに出力用のRender Texture(ここではOutputRTという名前のRender Texture)を指定しています。このRender Textureを出力デバイス経由でFill Keyとして出力します。同時に、オペレーター画面のImageとして設定することでオペレーターもエフェクトを確認できるようにしています。

オペレーター画面では、単体での見た目確認ができるようにCGの下敷きとして任意の動画やNDI()からの映像を入力できるようにしています。下記の図のように、Canvas内で下敷きにするRender Textureと、エフェクトのRender Textureを重ねて表示しています。

Fill Key合成のCanvasの構成

NDIのUnityプラグインにはKlakNDIを使用しています。このプラグインにはNDIを受信できるNDI Recieverコンポーネントがあります。NDI RecieverではNDI Nameに受信するNDIの名前を指定し、Target TextureにNDIで受信した映像を書き込むRender Textureを指定します。そして、NDI映像が書き込まれたRender TextureをRawImageのTextureに設定することで、UIとしてNDI映像を描画することができます。

NDI Recieverの設定

NDIとはネットワークを介して高品質な映像と音声をリアルタイムで送受信することが可能な標準規格です。専用のケーブルやハードウェアを使わずに素早く手軽に映像と音声のストリーミングや共有ができます。

エフェクトPrefab

画面に表示される要素はすべて2Dであるため、UGUIですべてのエフェクトやテロップは作成されています。各要素を単独のPrefabとして作成し、それを束ねるPrefabというNested Prefabの形で構成されています。各エフェクトのPrefabを独立させることで、単体テストが容易になるほか、多人数で分担してエフェクトを作成することが可能になります。

Nested Prefabで構成されたエフェクトPrefab

アーキテクチャ

各エフェクトのON/OFFや数値の変更はRxのイベントを契機に行っています。設計を簡単に示すと下記の図になります。

実装したアーキテクチャ

ViewModelがRepositoryの値を変更し、Repositoryの変更通知がViewModelを経由してViewに反映されるのが基本的な流れになります。ViewからViewModelへのUI操作の矢印が点線になっているのは、CG描画アプリではデバッグ機能くらいでしかUI操作起因で値の変更が発生しないためです。また、ViewのみがMonoBehaviourを継承しており、ViewModelやRepositoryはPure C#で記述しています。

簡単なサンプルコードを下記に示します。


using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using R3;

/// 
/// Viewのサンプル
/// 
public class ViewSample : MonoBehaviour
{
    [SerializeField] GameObject uiImage;

    ViewModelSample viewModel;
    void Start()
    {
        this.viewModel = new ViewModelSample();

        //ViewModelのIsVisibleの値が変更されたらUIの表示を変更
        this.viewModel.IsVisible.Subscribe(isVisible =>
        {
            this.uiImage.SetActive(isVisible);
        }).AddTo(this);
    }
}

/// 
/// ViewModelのサンプル
/// 
public class ViewModelSample
{
    RepositorySample repository;
    public ViewModelSample()
    {
        this.repository = new RepositorySample();

        //5秒後にUIの状態を変更
        Observable.Timer(System.TimeSpan.FromSeconds(5)).Subscribe(_ =>
        {
            this.repository.SetVisible(true);
        });
    }

    public Observable IsVisible => this.repository.IsVisible;
}

/// 
/// Repositoryのサンプル
/// 
public class RepositorySample
{
    //ReactivePropertyを使って値変更時にイベントを発火
    ReactiveProperty isVisible = new();

    //外部にはReadOnlyReactivePropertyとして公開
    public ReadOnlyReactiveProperty IsVisible => this.isVisible;

    public void SetVisible(bool isVisible)
    {
        this.isVisible.Value = isVisible;
    }
}

このコードは実行してから5秒後にuiImageのアクティブが変化するコードになっています。この処理だけ見るとMonoBehaviourで直接記述したほうが楽に見えます。しかし、書くコード量が増えても、細かく役割を分離した設計にすることで、Viewとロジックが分離される利点があります。その結果、コードが読みやすくなり、単体テストもやりやすくなります。最終的には不具合が少なく、保守しやすいプロダクトにつながっていきます。

シミュレーション機能

CG描画アプリは、描画された絵を見ながら調整する必要があり、細かいイテレーションが求められます。しかし、「システム全体構成」の章で説明したように、「WINLIVE」のシステムは多くのPCで構成されています。そのため、実際の機材を使ってテストをしようとすると準備にとても手間がかかってしまいます。そこで、CG描画アプリはなるべく手元で描画の確認ができるように、シミュレーション機能を持たせています。

実際の運用では、各PCからUDPでデータを受け取っていました。このとき各PCの計算結果はCSV形式のログファイルで保存されています。
UDP通信の経路

シミュレーション機能は各PCで保存している計算結果ログを使って、CG描画PCを稼働させることができる機能です。
シミュレーション機能を使った場合のデータの流れ

実環境では、各PCからUDP経由で計算結果を受け取り、Repositoryに書き込みを行っていました。シミュレーション機能では、ログファイルを読み込みデータを随時Repositoryに書き込むことで受信処理を再現しています。
実環境とシミュレーション環境のデータの流れの比較

シミュレーション機能は開発中に使うため、Editor拡張として実装しています。下記の画像はHPをシミュレートする機能のEditor Window画面です。
HPをシミュレートする機能のEditor Window

このような機能を作ることで、過去のレースを参考に新しいエフェクトを作ったり、何かトラブルがあった時にログをもとに描画状況を再現することができます。

2D座標ログ、HPログ、レース動画とUnity Recorderを組み合わせることで、高画質な動画を出力できるRendering機能も作成しました。Rendering機能は各種ログと動画をタイミングが合うように同時再生し、その結果をRecorderを使って動画を出力する機能になっています。従来はUnity Editorの画面をキャプチャしてデザイナー確認などを行っていました。このRendering機能を作ってからは確認用の動画が高画質になり、細かい部分まで確認がスムーズになりました。
Rendering機能のEditor Window

おわりに

本記事では「WINLIVE」のCG描画パートについて解説を行いました。現在はシステムのほぼすべてがオンプレミスで構築されているため、スケールしづらい構成となっています。今後はシステムのクラウド化を行い、実施競輪場を拡大していく予定です。なお、クラウド化については来月9月5日に行われる「AWS Media Seminar 2024 ~スポーツ業界の変革を加速するAWSの取組みと活用事例~」のイベントで講演を行います。ぜひご覧ください。

現在、新たなスポーツ映像の取り組みを促進させる仲間を募集中です。Unityエンジニアだけでなく、画像認識エンジニアやMLエンジニアなど幅広い職種を募集しています。興味がありましたら、ぜひお気軽にお申し込みください!

新時代のスポーツ映像を創るUnityエンジニア募集
スポーツ映像革新の先駆者!画像認識エンジニア募集
急成長中の競輪アプリ!WINTICKETの機械学習エンジニアを募集

関連記事