YUV⇒RGB変換に関する情報

YUV⇒RGB変換

前回、AndroidでOpenGL ESを使用してYUVデータの表示を行いました。

今回はブラウザ上にYUV⇒RGB変換した画像を表示したいと思います。

現在、主要なブラウザでは、JavaScriptのAPIとしてOpenGL ESの派生規格であるWebGLをサポートしています。

今回、HTML5で追加された<canvas>要素に対し、ローカルファイルから読み込んだYUVデータをRGB変換して表示していきます。
最初は、YUV⇒RGB変換後の画像を2Dのレンダラーを使用して描画してみます。
その後、WebGLを使用した描画を行ってみます。
最後にWebGLとGLSL ESを使用し、GPUでYUV⇒RGB変換を行って画像を表示します。

なお、言語に関してはJavaScriptでも問題ないのですが、私としては型指定ができる方がしっくりと馴染むので、TypeScriptを用いたいと思います。

TypeScriptの開発環境

先ず、TypeScriptの開発環境を準備します。
詳細は他のホームページに任せますが、今回は以下の開発環境を使用します。

Visual Studio Code

IDE(開発環境)としてはVisual Studio Code(以下VSCode)を使用します。
こちらのページからダウンロードできます。

インストールについては

VSCode インストール

で検索すれば色々と出てきますのでそちらを参考にして下さい。

Live Server

現在のVSCodeでは、特に他のツール等を導入しなくても、単体でHTMLやJavaScriptの確認やデバッグを行うことができます。

一方、簡易なサーバーを立ち上げてHTMLやJavaScriptの確認やデバッグができれば、更に便利かと思います。
そんな場合に便利なのがVSCodeの拡張機能のLive Serverです。

使用方法等については、

Live Server

で検索すれば色々と出てきますのでそちらを参考にして下さい。

Node.js

TypeScriptのコンパイラー等の開発環境をVSCodeで使用する場合、Node.jsが必要となります。

WindowsではNode.jsのホームページから最新のインストーラーをダウンロードし、実行するだけでインストールできます。

その他のプラットホームでのインストールはNode.jsのダウンロードページにリンクがありますので、参照して下さい。

プロジェクト作成

開発環境が整いましたら、プロジェクトを作成して行きます。
今回はNode.jsを使用してプロジェクトを構築します。

先ず、プロジェクトを作成したいフォルダーを適当に作成して下さい。
例えば”ShowYUVwithWebGL”と云うフォルダーを作成し、プロジェクトのフォルダーとします。

VSCodeをオープンします。
“ファイル”メニューから”フォルダーを開く…“を選択し、作成したプロジェクトのフォルダーを選択します。
VSCodeの新しいウィンドウがオープンします。

TypeScriptのコンパイラーのインストール

TypeScriptのコンパイラーのインストールの方法は色々とあるようですが、今回はNode.jsの”npm”コマンドでインストールします。

VSCodeで”Ctrl-@“等でターミナルをオープンします。
ターミナルで以下のコマンドによりTypeScriptのコンパイラーをインストールします。

npm install -g typescript@latest

なお、“-g”オプションは、グローバルにインストールすることを示し、他のプロジェクトでもTypeScriptのコンパイラーを使用できるようにします。
“-g”オプションを使用しない場合は今回のプロジェクトでのみTypeScriptのコンパイラーを使用できるようになります。

TypeScriptのバージョン確認

以下のコマンドでTypeScriptのバージョンを確認できます。

tsc -v
    

もしバージョンが想定しているより低い、例えば”1.0.0”等のような場合には、デフォルトで古いバージョンのTypeScriptがインストールされている可能性があります。
環境変数の”Path”に”C:Files (x86)SDKs\1.0.x.x”等が記載されていないか確認して下さい。
もし記載がある場合には”Path”から削除してみて下さい。

tsconfig.jsonの作成と修正

以下のコマンドによりTypeScriptのコンパイルのデフォルトの設定ファイル”tsconfig.json”が作成されます。

tsc --init

デフォルトの設定ファイルに対しては、以下の修正、追加を行います。

targetを変更

“target”の項目を”ES6”に変更します。

"target": "ES6",
      
moduleを変更

“module”の項目を”commonjs”から”ES2015”に変更します。

"module": "ES2015",
ベースURL指定

インポート文で絶対パス指定をできるようにベースディレクトリを指定します。

"baseUrl": "./src",
        
入力及び出力ディレクトリの追加

ソース元のディレクトリとコンパイル後のファイルを出力するディレクトリを追加します。
以下の項目の行のコメントアウトを外し、値を修正します。
なお、ディレクトリ名は適宜変更してもOKです。

"rootDir": "./src",
"outDir": "./dst/js",
        

更に、此処で指定したディレクトリに対応したフォルダをプロジェクトフォルダ”ShowYUVwithWebGL”下に作成しておきます。

階層化したコンパイル対象の指定

階層化したファルダをコンパイルの対象とするため、“include”の項目を以下のように追加します。

"compilerOptions": {
    :
    :
  },
"include": ["src/**/*"]
        
JavaScriptを対象

念のため、JavaScriptファイルを対象とするように以下の項目のコメントアウトを外しておきます。

"allowJs": true,
sourceMapの追加

デバッグ用のソースマップを作成するため以下の項目のコメントアウトを外しておきます。
リリースする際には再度コメントアウトしてソースマップを作成しない様にします。

"sourceMap": true,
        

プロジェクトの初期化

“Ctrl-@”等でオープンしたターミナルで以下のコマンドを実行し、プロジェクトを初期化します。

npm init -y

作成された”package.json”については、以下のように修正を行います。

スクリプト修正

“package.json”の”scripts”の値を以下のように修正し、次に導入するwebpackのコマンドをNPMスクリプトとして登録します。

"scripts": {
  "build": "webpack",
  "build:watch": "webpack -w",
  "node_modules": "npm ci"
},
    

なお”package-lock.json”に従ってnode_modules内のパッケージを再インストールするためのコマンドも追加しています。

webpackの導入

今回、TypeScriptのコードを書くのですが、クラス毎にファイルを分割したいと思います。
ただ、コンパイル後に作成される、複数のJavaScriptのファイルをHTMLで読み込み、動作させるには、色々と面倒な手続きがあり、容易ではありません。

webpackは、TypeScriptのファイルから作成された、分割されているJavaScriptのファイルを1つに纏め、HTMLから読み込めるようにしてくれるツールです。
他にも機能はあるのですが、詳細は他のホームページに任せます。

インストールは”Ctrl-@“等でオープンしたターミナル上で以下のコマンドを使用します。

npm install --save-dev webpack webpack-cli ts-loader

webpack.config.jsの作成

以下の内容でwebpackの設定ファイル”webpack.config.js”を作成します。

const path = require('path');
module.exports = {
    mode: 'development',
    entry: {
        index: './src/index.ts',
    },
    output: {
      path: path.join(__dirname, 'dst/js'),
      filename: 'index.js',
    },
    resolve: {
      extensions: ['.ts', '.js'],
    },
    devServer: {
        static: {
            directory: path.join(__dirname, 'dst'),
        },
        open: true,
    },
    module: {
        rules: [
            {
            test: /\.ts$/,
            loader: 'ts-loader',
            },
        ],
    },
    target: 'electron-main',
};
    

YUV⇒RGB変換

前回、JavaにOpenGLのラッパーであるJOGLを使用してYUVデータを表示しました。

今回はAndroid端末にYUVデータを表示使用と思います。

従来のAndroid OSでは元々OpenGL ES 1.0をサポートしていました。
画像の描画については他の方法よりも高速な方法でした。

ただ現状ではOpenGL ESを使用しなくても、描画はハードウェアアクセラレーションの恩恵を受けていますので、OpenGL ESの複雑なAPIで下手なプログラムを作成するよりも、通常の描画方法を採用した方が高速になる場合もあります。

現在はAndroid OSもOpenGL ES 2.0や3.0等をサポートするようになり、デフォルトでGLSLを使用したシェーダープログラムを使用するようになりました。
その結果、従来、CPU側で行っていた画像計算をGPU側で行うことができるようになり、その分、高速化できる可能性が出てきました。

以下、Android端末を使用して単にYUVファイルを読み込んで画像を表示する事から、OpenGL ESとGLSLを使用してGPUでYUV⇒RGB変換を行って画像を表示するまでをステップバイステップで説明していきます。

Androidの開発環境

先ず、Androidの開発環境を準備します。
詳細は他のホームページに任せますが、今回は以下の開発環境を使用します。

Android Studioのダウンロード / インストール

Androidの開発を行うためのIDEは、EclipseやVisual Studio等、様々な物があります。
今回は、Googleが提供するAndroid Studioを使用します。

Android Studioはこちらのダウンロードページからダウンロードして下さい。

インストールについてはこちらのインストールガイドを参照して下さい。

基本的にはダウンロードしたファイル”android-studio-XXXX.Y.Z.WW-windows.exe”をダブルクリックでインストールが開始します。

Android Studioの設定

デフォルトの設定でOKです。
但し、途中android-sdk-licenseintel-android-extra-licenseに対して"Accept"をチェックする必要があります。

エラー対応

設定後にインストールが継続しますが、最後に

Intel HAXM installation failed!

と云うエラーが発生する場合があります。

この場合、以下のようにWHPXが有効であれば問題ないのでスルーします。

WHPX

Hyper-Vによる仮想化技術です。
有効化する場合は以下のようにします

  1. “Windowsの機能の有効化または無効化”ダイアログボックスを開く
  2. “Hyper-V”をチェック
  3. “Windows ハイパーバイザープラットフォーム”をチェック

詳細についてはAndroid Emulator のハードウェア アクセラレーションを設定するを参照して下さい。

使用言語

前回Javaを使用しましたので、今回もJavaを使用します。

Googleの推奨はKotlinですし、最近のAndroidプログラミングの解説ページはKotlinでの記述が増えてきています。
ただ、KotlinはAndroidプログラミング以外で利用される割合が低いですし、JavaからKotlinへの乗り換えは意外と簡単だと云われています。

なので、Javaを採用します。

とりあえずアプリケーションを作成

先ず最初は、Android Studioでプロジェクトを作成し、表示領域とボタン類を配置していきます。

プロジェクト作成

Android Studioででのプロジェクト作成は以下のようにします。

  1. Android StudioのiconをダブルクリックWelcome to Android Studio
  2. “New Project”ボタンをクリック
  3. エディター画面が既に表示されている場合にはメニューから”File”→“New”->“New Project”を選択
  4. “New Project”ウィンドウが表示される
    New Project / Empty Activity
  5. “Empty Activity”が選択されている事を確認後”Next”ボタンをクリック
  6. プロジェクト名等を設定するウィンドウが表示される
    New Project / Setting
  7. 今回は”Name”を”ShowYUVonAndroid”とする(他の名称でもOK)
  8. “Language”は”Java”を選択
  9. “Package name”や”Minimum SDK”はデフォルトでOK
  10. “Finish”ボタンをクリック

“MainActivity.java”ファイルには以下のように空のActivityが作成されています。

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    }
}
    

描画領域とボタンの作成

画像サイズの指定

今回もYUVファイルとしてCIFサイズの”akiyo_cif.yuv”を使用しますので、最初に画像サイズを指定しておきます。
以下のようにしてリソースに追加します。

  1. 左のProjectツリーペインの”app”を右クリック
  2. メニューから”New”→“XML”→“Values XML File”を選択
  3. リソースフォルダにXMLファイルを作成するウィンドウが表示される
    New Android Component / dimens.xml
  4. “Values File Name”に”dimens”と入力
  5. “Finish”ボタンをクリック
  6. “app”→“res”→“values”→“dimens.xml”ファイルが作成される

なお、画像サイズ等の寸法を指定するリソースは”dimens.xml”に記載する事が推奨されています。
詳細はアプリのリソースの概要を参照してください。

CIFサイズの幅と高さを指定するため、“dimens.xml”は以下のようにします。

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <dimen name="width">352dp</dimen>
    <dimen name="height">288dp</dimen>
</resources>
      

なお、“dimens.xml”内のリソースは必ず単位を付加します。
付加できる単位は、現状、以下の6つがサポートされています。

単位 意味
dp 密度非依存ピクセル
160dpiの画面を基準にしたピクセル
実際のピクセル数は画面密度により変化
sp スケール非依存ピクセル
dpに近いがフォントサイズ指定の際に使用
pt 72dpiの画面での1/72インチ
px ピクセル
実際の画面の画素数
mm ミリメートル
in インチ

詳細はその他のリソース→ディメンションを参照してください。

ボタン用のテキスト指定

AndroidではUI等に使用するテキストもリソースとして定義します。

左のProjectツリーペインの”app”→“res”→“values”→“strings.xml”ファイルを選択、オープンします。
以下のように”File…“ボタンと”Play”ボタンに表示するテキストを指定します。

<resources>
    <string name="app_name">ShowYUVonAndroid</string>
    <string name="play_button_text">Play</string>
    <string name="file_button_text">File...</string>
</resources>
      

レイアウトの修正

レイアウトを修正する場合には、先ず、左のProjectツリーペインから”app”→“res”→“layout”→“activity_main.xml”ファイルを選択、オープンします。

最初は"Layout Editor"が表示されるはずです。
“Layout Editor"の使い方についてはLayout Editor を使用して UI を作成する等を参照してください。

今回は右上の”View Mode”から”Code”モードを選択し、xmlコードを直接編集します。

最初はConstraintLayout内にTextViewとして”Hello World!“が表示されるようになっています。
今回は先ず、LinearLayout内にViewButtonを配置します。

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context=".MainActivity">

    <View
        android:id="@+id/view"
        android:layout_width="@dimen/width"
        android:layout_height="@dimen/height"
        android:layout_gravity="center_horizontal"
        android:layout_margin="16dp" />

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="horizontal">

        <Space
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1" />

        <Button
            android:id="@+id/play_button"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="center_horizontal"
            android:enabled="false"
            android:text="@string/play_button_text" />

        <Space
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_weight="1" />

        <Button
            android:id="@+id/file_button"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="center_horizontal"
            android:text="@string/file_button_text" />

        <Space
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_weight="1" />
    </LinearLayout>

</LinearLayout>
      

因みにViewは仮置きです。
後で色々と変更します。

View.OnClickListenerインターフェースの追加

ボタンを配置しましたので、ボタンクリックに対する処理を実装するため、MainActivityクラスにView.OnClickListenerインターフェースを追加します。
それに伴い、onClickメソッドを追加します。

public class MainActivity extends AppCompatActivity implements View.OnClickListener {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        findViewById(R.id.play_button).setOnClickListener(this);
        findViewById(R.id.file_button).setOnClickListener(this);
    }

    @Override
    public void onClick(View view) {

    }
}
      

なおonCreateメソッド内でボタンに対してsetOnClickListenerメソッドでリスナーを設定しておきます。

この状態で”Shift+F10”でアプリケーションを実行すると、デフォルトで作成されているデバイスエミュレーターが起動し以下のように表示されます。

最初の画面

AVD(Android Virtual Device)の作成

基本的にプログラムの実行/デバッグで使用するデバイスは、Android Studioと共にデフォルトで提供されているAVDで問題ないはずですが、以下のように新しく作成する事もできます。

  1. 右端上のタブ”DeviceManager”を選択
  2. “Device Manager”ペインの左上の”CreateDevice”ボタンをクリック
    Device Manager
  3. “Virtual Device Configuration”の”Select Hardware”ウィンドウが表示される
    Virtual Device Configulation / Select Hardweare
  4. 適当なデバイスを選択後、“Next”ボタンをクリック
    例として”Pixel 2”を選択
  5. “Virtual Device Configuration”の”System Image”ウィンドウが表示される Virtual Device Configulation / System Image
  6. 適当な”System Image”を選択して”Next”ボタンをクリック
    基本的にはデフォルトで選択されているImageでOK
  7. “Virtual Device Configuration”の”Android Virtual Device (AVD)“ウィンドウが表示される Virtual Device Configulation / Android Virtual Device
  8. 基本的にデフォルトで問題ないので”Finish”ボタンをクリック
    なお”AVD Name”は適当に変更可

以上で新しいAVDが作成されます。

なお実行/デバッグに使用するデバイスはエディター画面の右上部分で選択できます。AVDの選択

YUVファイル選択

“File…”ボタンをクリックした時にファイル選択のActivityを表示し、ファイルの選択を行えるようにします。

先ずActivityを呼び出す前にファイル選択の結果を受け取るコードを作成しておきます。
Activityから結果を受け取る為にはActivityResultLauncher<Intent>クラスのインスタンスをregisterForActivityResultで作成します。
ファイル選択の結果を受け取るコールバックメソッドはregisterForActivityResultの第2引数に渡すActivityResultCallback<ActivityResult>クラスインスタンスのonActivityResultメソッドに実装します。

public class MainActivity extends AppCompatActivity implements View.OnClickListener {
            :
            :
    ActivityResultLauncher<Intent> filePickerStartForResult = registerForActivityResult(new ActivityResultContracts.StartActivityForResult(),
            new ActivityResultCallback<ActivityResult>() {
                @Override
                public void onActivityResult(ActivityResult result) {
                    if (result.getResultCode() == Activity.RESULT_OK) {
                        Intent intent = result.getData();
                        Uri uri = intent.getData();
                        Toast.makeText(getApplicationContext(), uri.toString(), Toast.LENGTH_SHORT).show();
                    }
                }
            });
                :
                :
}
    

とりあえず現状はToastを使用してファイルのURIを表示するようにしています。

このインスタンスを使用して、onClickメソッド中でファイル選択のActivityを呼び出すには以下のようにします。

public class MainActivity extends AppCompatActivity implements View.OnClickListener {
            :
            :
   @Override
    public void onClick(View view) {
        if (view.getId() == R.id.file_button) {
            Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
            intent.addCategory(Intent.CATEGORY_OPENABLE);
            intent.setType("application/octet-stream");
            filePickerStartForResult.launch(intent);
        }
    }
}
    
  1. ACTION_OPEN_DOCUMENTを引数にしてIntentのインスタンスを作成
  2. addCategoryメソッドにCATEGORY_OPENABLEを指定して”ファイルオープン”とする
  3. setTypeメソッドにMIMEタイプ”application/octet-stream”を指定してバイナリファイル全般を対象とする
    MIMEタイプとして”video/*“等としたいが、”yuv”はビデオのタイプとして登録されていないのでバイナリファイルで代替
  4. ActivityResultLauncher<Intent>launchインスタンスメソッドでファイル選択のActivityを開始

なおMIMEタイプについてはMIME タイプ (IANA メディアタイプ)が詳しいので、そちらを参照してください。

実際のファイル選択

この時点で実行し”File…“ボタンをクリックすると画面が”ファイル選択のActivity”に切り替わります。

“akiyo_cif.yuv”ファイルはこの時点で未だAVDに送られていません。
ファイルをAVDに送る一番簡単な方法は、AVDの上にファイルをドラッグアンドドロップする事です。

ファイル自体は”ダウンロード”以下に配置されますので、左上”≡“をクリックして”ダウンロード”を選択します。
“akiyo_cif.yuv”ファイルがあるはずですのでクリックします。

Selecty download folder ⇒ Select akiyo_cif.yuv file

ファイル選択後は元のActivityに戻り、下部ToastにURIが表示されます。

ちなみに、もう少し柔軟にADV上のファイルを出し入れしたい場合には、メニューから”View”→“Tool Windows”→“Device File Explorer”を選択します。
“Device File Explorer”ペインが表示されますので、ADV上のファイルを操作する事ができます。

YUV⇒RGB変換用クラスの作成

上記でファイルの選択を行ったので、続けてYUVファイルをオープンし、データを読み込んだ後、YUV⇒RGB変換を行うクラスを作成しておきます。

  1. 左のprojectツリーペインの”app”→“java”→“com.example.showyuvonandroid”を右クリック
  2. メニューから”New”→“Java Class”を選択
  3. “New Java Class”ウィンドウが表示される
    New Java Class / ShowYUVImage
  4. クラス名として”ShowYUVImage”を入力、“Class”を選択しリターン

YUV⇒RGB変換用クラスのコンストラクタ作成

ShowYUVImageクラスが作成されますので、Contextクラスを引数とするコンストラクタを追加します。
インスタンス変数としては、画像サイズやファイルストリーム等を記載します。
なお、YUV⇒RGB変換も行いますので、“Y”, “U”, “V”, “ARGB”其々のデータを保持するインスタンス変数も追加しておきます。

public class ShowYUVImage {

    private int width;
    private int height;
    private int ySize;
    private int uvSize;

    private byte[] y;
    private byte[] u;
    private byte[] v;
    private int[] argb;

    private InputStream yuvFile;

    public ShowYUVImage(Context context) {
        int scale = context.getResources().getDisplayMetrics().densityDpi;
        int w = context.getResources().getDimensionPixelSize(R.dimen.width);
        int h = context.getResources().getDimensionPixelSize(R.dimen.height);
        width = w * 160 / scale;
        height = h * 160 / scale;
        ySize = width * height;
        uvSize = (width / 2) * (height / 2);
        y = new byte[ySize];
        u = new byte[uvSize];
        v = new byte[uvSize];
        argb = new int[ySize];
    }
}
      

なお、コンストラクタの最初の5行目、“height”の値を計算するまでは、“dp”で指定した画面サイズを”pixel”単位に変換する為の操作です。
これにより、“width”と”height”はCIFサイズ352 x 288にセットされます。

YUV⇒RGB変換用クラスでのファイルオープンとデータの読み込み

YUV⇒RGB変換用クラスShowYUVImageではファイルからYUVデータを読み込みます。
そのため、ファイルのURIを受け取ってオープンし、YUVデータを読み込むメソッドが必要となります。

まず、ファイルのURIを受け取ってオープンするメソッドとして以下を追加します。

private boolean available = false;

public boolean isAvailable() {
    return available;
}

public void setYUVFileURL(ContentResolver contentResolver, Uri uri) {
    try {
        yuvFile = contentResolver.openInputStream(uri);
        available = true;
        readYUV();    // YUVデータを読み込むメソッド
    }
    catch (Exception e) {
        Log.e("SetFile", e.toString());
        available = false;
    }
}
      

URIを指定してファイルをオープンするためには、ContentResolverクラスのopenInputStreamメソッドを使用します。
状況により例外が発生した場合にはLogCatに内容を出力してリターンします。

なお、インスタンス変数”available”はYUVデータが正常に読み込まれたことを示すフラグです。
“available”にはゲッターを作成しておきます。

YUVファイルが正常にオープンできましたら、YUVデータを読み込んでおきます。

protected void readYUV() {
    if (available) {
        try {
            yuvFile.read(y);
            yuvFile.read(u);
            int size = yuvFile.read(v);
            if (size < uvSize) {
                available = false;
            }
        } catch (IOException e) {
            Log.e("ReadFile", e.toString());
            available = false;
        }
    }
}
      

なお、“V”データを読み込んだ際にデータが足りない、もしくは読み込めなかった場合には読み込み失敗としてフラグavailableを"false"にセットします。

YUV⇒RGB変換メソッドの追加

YUVファイルからデータを読み込むことができましたら、次はYUV⇒RGB変換です。
基本的に使用する計算式は今までと同じですので、YUV⇒RGB変換(JOGL編)と概ね同じコードを使用できます。

protected int clip(int n) {
    return (n <= 0) ? 0 : ((n >= 255) ? 255 : n);
}

protected void transYUV2RGB() {
    for (int h = 0; h < height; h++) {
        int y_pos = h * width;
        int uv_pos = (h / 2) * (width / 2);

        for (int w = 0; w < width; w++) {
            int yi = y[y_pos + w] & 0x0FF;
            int ui = u[uv_pos + (w / 2)] & 0x0FF;
            int vi = v[uv_pos + (w / 2)] & 0x0FF;

            double y16 = yi - 16.0;
            double u128 = ui - 128.0;
            double v128 = vi - 128.0;

            int r = (int) ((1.164 * y16) + (0.0 * u128) + (1.596 * v128));
            int g = (int) ((1.164 * y16) + (-0.392 * u128) + (-0.813 * v128));
            int b = (int) ((1.164 * y16) + (2.017 * u128) + (0.0 * v128));
            r = clip(r);
            g = clip(g);
            b = clip(b);
            argb[y_pos + w] = (0xFF << 24) | (r << 16) | (g << 8) | b;
        }
    }
}
      

YUV⇒RGB変換クラスでのBitmapの作成

画像データをViewクラスに描画するためにはBitmap形式の画像としてViewクラスに渡す必要があります。
なので、作成したARGB形式のデータをBitmap形式に変換して返すメソッドを追加しておきます。

public Bitmap getNextBitmap() {
    if (available) {
        transYUV2RGB();
        Bitmap bitmap = Bitmap.createBitmap(argb, width, height, Bitmap.Config.ARGB_8888);
        readYUV();
        return bitmap;
    }
    return null;
}
      

ARGB形式のデータからBitmap形式の画像を作成するためにはcreateBitmapクラスメソッドを使用します。
Bitmap形式の画像作成後は次のYUVデータを読み込んでおきます。

Viewの修正

YUVファイルからデータを読み込んでBitmap形式の画像を作成できるようになりましたので、次はViewクラスを使って画像の表示を行う事にします。

Viewクラスは基本のクラスですし、描画のコードもonDrawメソッドに集約すれば良いので非常に手軽に使用できます。

Viewのサブクラスの作成

先ず、Viewのサブクラスを作成します。

  1. 左のprojectツリーペインの”app”→“java”→“com.example.showyuvonandroid”を右クリック
  2. メニューから”New”→“Java Class”を選択
  3. “New Java Class”ウィンドウが表示される
    New Java Class / ShowYUVView
  4. クラス名として”ShowYUVView”を入力、“Class”を選択しリターン

新たに”ShowYUVView.java”ファイルが作成されますので、ShowYUVViewクラスがViewクラスを継承するように、クラス名の後にextends Viewを追加しておきます。

ShowYUVViewクラスへのコンストラクタの追加

ShowYUVViewクラスは”activity_main.xml”ファイル内で”View”タグを置き換えます。
そのため、Viewクラスが実装しているコンストラクタは全て実装する事が推奨されます。

なお、ShowYUVViewクラスは描画のためにShowYUVImageクラスのインスタンスを必要としますので、インスタンス変数としてコンストラクタ内で作成しておきます。

public class ShowYUVView extends View {

    private ShowYUVImage showYUVImage;

    public ShowYUVView(Context context) {
        super(context);
        showYUVImage = new ShowYUVImage(context);
    }

    public ShowYUVView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        showYUVImage = new ShowYUVImage(context);
    }

    public ShowYUVView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        showYUVImage = new ShowYUVImage(context);
    }

    public ShowYUVView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
        showYUVImage = new ShowYUVImage(context);
    }
}
      

ShowYUVViewクラスのonDrawメソッドのオーバーライド

UIスレッドでShowYUVViewクラスのinvalidateメソッドがコールされると、onDrawメソッドがコールされます。
Viewクラスのサブクラスで描画を行うためには、ViewクラスのonDrawメソッドをオーバーライドします。

protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    Bitmap bmp = showYUVImage.getNextBitmap();
    if (bmp != null) {
        Rect dst = new Rect(0, 0, canvas.getWidth() - 1, canvas.getHeight() - 1);
        canvas.drawBitmap(bmp, null, dst, null);
    }
    else {
        canvas.drawARGB(0xFF, 0x00, 0x00, 0xFF);
    }
}
      

最初にViewクラスのonDrawメソッドをコールしておきます。
次にBitmap形式の画像を取得し、CanvasクラスのdrawBitmapメソッドで描画を行います。
なお、Bitmap形式の画像の取得に失敗した場合には、drawARGBメソッドで画面全体を赤色に塗り潰すようにしています。

因みに描画領域を示す”dst”を作成する際にRectクラスのコンストラクタの第3引数と第4引数は幅と高さではありません
右と下の画素の座標を指定する事になっていますので、描画領域全体を指定するには、幅および高さから1を引く必要があります。

ShowYUVViewクラスのその他のメソッド

ShowYUVImageクラスのインスタンスはShowYUVViewで保持するようにしましたので、ファイルのURIのセットやYUVデータが準備できているかの問い合わせはShowYUVViewクラスを通して行う必要があります。
そのためのメソッドを追加しておきます。

    public void setYUVFileURL(ContentResolver contentResolver, Uri uri) {
        showYUVImage.setYUVFileURL(contentResolver, uri);
    }

    public boolean isAvailable() {
        return showYUVImage.isAvailable();
    }
      

ViewのサブクラスへのYUVデータの表示

以上で準備ができましたので、以下、ViewのサブクラスをUIにセットし、描画を行うようにします。

actibity_main.xmlへのShowYUVViewの追加

“activity_main.xml”をオープンし、コードを表示します。
以下のように”View”タグ部分を”com.example.showyuvonandroid.ShowYUVView”に変更します。

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context=".MainActivity">

    <com.example.showyuvonandroid.ShowYUVView
        android:id="@+id/view"
        android:layout_width="@dimen/width"
        android:layout_height="@dimen/height"
        android:layout_gravity="center_horizontal"
        android:layout_margin="16dp" />

    <LinearLayout
        :
        :
    </LinearLayout>

</LinearLayout>
      

因みにレイアウト用のXML中で独自に作成したクラスをタグの要素名として指定する場合には、完全修飾クラス名を指定する必要があります。

ShowYUVViewでのファイル名設定と表示

ファイル名のセットとShowYUVViewへの描画を行うため、“MainActivity.java”を修正します。
MainActivityのインスタンス変数”filePickerStartForResult”中のコールバックメソッドonActivityで以前にToastでURIを表示している部分にコードを追加します。

ActivityResultLauncher<Intent> filePickerStartForResult = registerForActivityResult(new ActivityResultContracts.StartActivityForResult(),
        new ActivityResultCallback<ActivityResult>() {
            @Override
            public void onActivityResult(ActivityResult result) {
                if (result.getResultCode() == Activity.RESULT_OK) {
                    Intent intent = result.getData();
                    Uri uri = intent.getData();
                    Toast.makeText(getApplicationContext(), uri.toString(), Toast.LENGTH_SHORT).show();
                    ShowYUVView view = findViewById(R.id.view);    // 以下追加
                    view.setYUVFileURL(getContentResolver(), uri);
                    view.invalidate();
                    findViewById(R.id.play_button).setEnabled(true);
                }
            }
        });
      

この時点で実行し”File…“ボタンをクリックして”akiyo_cif.yuv”ファイルを選択すると、以下のように表示されるはずです。Viewクラスによる最初の画面

なお次で動画再生をするため、最後の部分で”Play”ボタンを有効化しておきます。

動画対応

定期的に画像を更新してYUVデータを動画として表示するため、タイマーを使用します。

タイマータスクの作成

タイマーにより定期的に実行されるタスクを記述するため、先ず、TimerTaskクラスを継承するクラスを作成します。
TimerTaskのサブクラスでは、定期的に実行されるコードはrunメソッド内に記述します。
とりあえずサブクラスの名前をShowYUVTimerTaskとし、MainActivityクラスの内部クラスとして実装しておきます。

public class MainActivity extends AppCompatActivity implements View.OnClickListener {
    Handler handler = new Handler();
    Timer timer;
    TimerTask timerTask;
        :
        :
    protected class ShowYUVTimerTask extends TimerTask {

        @Override
        public void run() {
            handler.post(new Runnable() {
                @Override
                public void run() {
                    ShowYUVView view = findViewById(R.id.view);
                    if (view.isAvailable()) {
                        findViewById(R.id.view).invalidate();
                    }
                    else {
                        timer.cancel();
                        findViewById(R.id.file_button).setEnabled(true);
                    }
                }
            });
        }
    }
}
      

TimerTaskrunメソッドは基本的にUIスレッドとは別のスレッドでコールされます。
ですので、そのままViewでの描画等を行ってしまいますと色々と不具合を生じます。
それを避けるため、Handlerでメッセージキューにコードをポストし、UIスレッドで実行させるようにします。

Playボタンでタイマー開始

“Play”ボタンをクリックした時にタイマーを開始するコードを追加します。
MainActivityクラスのonClickメソッドを以下のように修正します。

public void onClick(View view) {
    if (view.getId() == R.id.file_button) {
        Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
        intent.addCategory(Intent.CATEGORY_OPENABLE);
        intent.setType("application/octet-stream");
        filePickerStartForResult.launch(intent);
    }
    // 以下を追加
    else if (view.getId() == R.id.play_button) {
        timerTask = new ShowYUVTimerTask();
        timer = new Timer(true);
        long period = getResources().getInteger(R.integer.period);
        timer.schedule(timerTask, period, period);
        view.setEnabled(false);
        findViewById(R.id.file_button).setEnabled(false);
    }
}
      

因みに”R.integer.period”はリソースとして定義します。

タイマー間隔の指定

タイマーの間隔は66msecですが、リソースとして指定しておきます。

  1. 左のProjectツリーペインの”app”を右クリック
  2. メニューから”New”→“XML”→“Values XML File”を選択
  3. リソースフォルダにXMLファイルを作成するウィンドウが表示される
    New Android Component / constants.xml
  4. “Values File Name”に”constants”と入力
  5. “Finish”ボタンをクリック
  6. “app”→“res”→“values”→“constants.xml”ファイルが作成される

なお、整数の定数等を指定するリソースは”constants.xml”に記載する事が決まっている訳ではないようで、“integers.xml”等でもOKです。

タイマー間隔指定するため、“constants.xml”は以下のようにします。

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <integer name="period">66</integer>
</resources>
      

タグの要素名は”integer”です。

この時点で実行し”File…“ボタンをクリックして”akiyo_cif.yuv”を選択、“Play”ボタンをクリックすると動画が表示されます。

ここまでのコードはここからダウンロードできます。

YUV⇒RGB変換

前回、OpenGLを使用してYUVデータを表示しました。
その際、プログラム言語としてOpenGLと相性の良いC/C++C#を使用しました。

今回はOpenGLを使用してYUVデータを表示するためにプログラム言語としてJavaを使ってみようかと思います。

ただ、Javaから直接OpenGLのAPIをコールする事はできませんので、実際にはJNIを通してC/C++等のネイティブコードからOpenGLのAPIをコールする事になります。
ですのでJavaでOpenGLを使用するのは、かなりのオーバーヘッドを伴います。

また現在のJavaのGUIフレームワークではDirectXやOpenGLを使用して描画しているものが殆どですので、単純に描画を行うだけであれば態々OpenGLを使わなくても、GUIフレームワークをそのまま使用した方が良いでしょう。

JavaでOpenGLを使うメリットはGLSLを使用してGPU側でYUV⇒RGB変換等の重い計算を行わせる際に発揮されます。

以下、普通にJavaを使用してYUVファイルを読み込んで画像を表示する事からOpenGLとGLSLを使用してGPUでYUV⇒RGB変換を行って画像を表示するまでをステップバイステップで説明していきます。

Javaの開発環境

先ず、Javaの開発環境を準備します。
詳細は他の方の説明に任せますが、必要な物は以下の通りです。

JDK

Javaのプログラムを作成するためにはJDKが必要です。
最新版はOracleのページからダウンロードできます。
因みにJREとは異なりますので注意して下さい。

インストール方法についてはOracleのページに"Installation Instructions"へのリンクがありますので、そちらを参考にして下さい。

更に

JDK インストール

で検索すれば、色々と出てきますのでそちらを参考にして下さい。

Visual Studio Code

IDE開発環境としてはVisual Studio Code(以下VSCode)を使用します。
こちらのページからダウンロードできます。

インストールについては

VSCode インストール

で検索すれば、色々と出てきますのでそちらを参考にして下さい。

なお、JavaのIDEとしてはEclipseNetBeans等が有名ですが、今回はVSCodeを使用します。

Java向け拡張機能の追加

VSCodeでJavaの開発を行うためには、Java向けの拡張機能Java Extension Packを導入すると便利です。
と云うか必須です。

追加方法は

Java VScode 拡張機能

で検索すれば、色々と出てきますのでそちらを参考にして下さい。

Apache Maven

今回、OpenGLのJava向けラッパーのライブラリを使用しますが、その管理をJava用プロジェクト管理ツールであるApache Mavenを使用します。

ライブラリの管理等はJava Extension Packを導入した際に同時に導入される拡張機能Maven for Javaでできるようなのですが、Apache Maven本体をインストールすると全ての機能を使用できるようになります。

ダウンロードやインストールについてはApache Mavenのホームページが詳しいので、そちらを参考にして下さい。

とりあえずウィンドウを表示してみる

プロジェクト作成

VSCodeで”コマンドパレット”(Ctrl+Shift+P)をオープンします。
コマンドパレットでは以下のように選択、入力します。

  1. “Java: Create Java Project…”を選択Javaプロジェクトの作成
  2. “Maven create from archetype”を選択Mavenを選択
  3. “maven-archetype-quickstart”を選択Maven Archetype quickstartを選択
  4. “Select version of maven-archetype-quickstart”で1.4を選択Maven Versionを選択
  5. “Input group Id of your project.”で”com.example”を入力Group IDを入力
  6. “Input artifact of your project.で”showyuv-jogl”を入力Artifact IDを入力

なお”group Id”び”artifact Id”については適当でOKです。
ターミナル上でMavenの設定が始まり、以下の問い合わせがありますのでそのままリターンして下さい。Version1 Snapshot

  1. “Define value for property ‘version’ 1.0-SNAPSHOT:”でリターン
  2. “Y: :”でリターン

描画領域とボタンを作成

メインのクラス名変更とインターフェース追加

クラス名およびファイル名がAppとなっているのでShowYUVに変更します(別の名前でもOKです)。
なおウィンドウを表示するためjavax.swing.JFrameを継承します。
因みにJavaのGUIフレームワークにはAWTSwingJavaFX等がありますが使い易さと標準で組み込まれている事を考慮してGUIフレームワークはSwingを中心に使用して行きます。

更にボタンを使用するためjava.awt.event.ActionListenerインターフェースをインプリメントしておきます。
なおActionListenerインターフェースはvoid actionPerformed(ActionEvente)メソッドが必要なのでとりあえず空のメソッドを追加しておきます。

package com.example;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import javax.swing.JFrame;
/**
 * YUVファイルを表示
 *
 */
public class ShowYUV extends JFrame implements ActionListener {
    public static void main(String[] args) {
        System.out.println("Hello World!");
    }
    @Override
    public void actionPerformed(ActionEvent e) {
        // TODO Auto-generated method stub
    }
}

ShowYUVクラスのコンストラクタ作成

ShowYUVクラスのコンストラクタを作成しておきます。
コンストラクタ内ではファイルオープンボタンとプレイボタン、描画領域を追加します。
なおボタンと描画領域は他のメソッドからアクセスできるようにインスタンス変数としておきます。

public class ShowYUV extends JFrame implements ActionListener {
   static final int CIF_WIDTH = 352;
   static final int CIF_HEIGHT = 288;
   static final String TITLE = "CheckJOGL";
   static final String FILE_NAME = "File: ";
   static final String FILE_OPEN = "File...";
   static final String PLAY = "Play";
   private JPanel panel;
   private JLabel fileNameLabel;
   private JButton fileOpenButton;
   private JButton playButton;
   public ShowYUV(String title) throws HeadlessException {
       super(title);
       /* 描画領域 */
       JPanel borderPanel = new JPanel();
       borderPanel.setBorder(BorderFactory.createBevelBorder(BevelBorder.LOWERED));
       panel = new JPanel();
       panel.setPreferredSize(new Dimension(CIF_WIDTH, CIF_HEIGHT));
       borderPanel.add(panel);
       add(borderPanel);
       /* ボタン領域 */
       JPanel buttonPanel = new JPanel();
       buttonPanel.setLayout(new BoxLayout(buttonPanel, BoxLayout.X_AXIS));
       buttonPanel.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5));
       fileNameLabel = new JLabel(FILE_NAME);
       fileOpenButton = new JButton(FILE_OPEN);
       playButton = new JButton(PLAY);
       playButton.setEnabled(false);
       fileOpenButton.addActionListener(this);
       playButton.addActionListener(this);
       buttonPanel.add(fileNameLabel);
       buttonPanel.add(Box.createGlue());
       buttonPanel.add(fileOpenButton);
       buttonPanel.add(Box.createHorizontalStrut(5));
       buttonPanel.add(playButton);
       add(buttonPanel, BorderLayout.SOUTH);
       pack();
       setResizable(false);
       setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
       setLocationRelativeTo(null);
   }
       :
       :
}
とりあえずの描画領域の準備

描画領域はとりあえずJPanelクラスとしておきます(後で独自のクラスに置き換えます)。
なお描画領域”panel”をJPanelクラスの”borderPanel”の中に置いているのは単純に枠線を付けたいためなので枠線が必要ない場合にはJPanelを二重にする必要はありません。

描画領域のサイズは最終的にCIFサイズのYUVデータを描画するため(352, 288)にセットします。
因みにJPanelクラスのsetPreferredSizeメソッドで使用されているDimensionクラスはjava.awt.Dimensionなので注意が必要です。
他のフレームワークに同じ名前のクラスが存在するためIDEの補完機能で間違って選択されてしまって”???“となる場合があります。

ボタン類の配置

ファイルオープンボタン”fileOpenButton”とプレイボタン”playButton”の他にファイル名を表示するテキストラベル”fileNameLabel”を下部に配置します。
先ずこれらのボタン類を入れるコンテナJPanelクラスの”buttonPanel”をBoxレイアウトで準備します。
なおボタン類の周囲に余白を持たせるために”buttonPanel”に枠線を設定しておきます。
その中に左から”fileNameLabel”、“fileOpenButton”、“playButton”の順に配置して行きます。
各コンポーネントの間は適当にパディングを追加します。

コンポーネント配置後は”buttonPanel”をウィンドウの下部に配置しておきます。

メインウィンドウの諸設定

コンストラクタの最後ではウインドウの諸設定を行います。
先ずpack()メソッドでウィンドウのサイズを各コンポーネントの配置等に合わせて調整します。
次に描画するYUV画像のサイズが決まっているので”setResizable”メソッドでリサイズ出来ないようにしておきます。
更にウィンドウをクローズした時にアプリケーションを終了するように”setDefaultCloseOperation”メソッドで設定しておきます。

main関数でメインウィンドウの作成、表示設定

main関数ではShowYUVクラスのインスタンスを作成後setVisibleメソッドでウィンドウを表示します。

static final String TITLE = "ShowYUV";
public static void main(String[] args) {
    ShowYUV frame = new ShowYUV(TITLE);
    frame.setVisible(true);
}

この時点で実行すると、とりあえずのウィンドウを表示する事ができます。初期画面

ここまでのコードについてはGitHubからダウンロードできます。

YUV⇒RGB変換

前回、Direct3Dを使用してYUVデータを表示出来ましたので、次はDirectXと双璧をなすOpenGLを使用して表示してみようと思います。

OpenGLは、Khronos GroupがAPIを策定している3Dグラフィックスライブラリです。
Direct3Dが9から10、11、12と急速に進化したのと同様、OpenGLはVulkanと云う後継のライブラリが存在します。
ただ、四角形にテクスチャを貼り付ける程度の処理しか行いませんので、前回Direct3D9を使用したのと同様、今回はOpenGLを使用します。

なお、WPFやWindows Formsのコンポーネント上にOpenGLで描画するのは少々面倒なため、今回はGLFWを使用してウィンドウを表示し、そこに描画を行うようにします。

OpenGLのフレームワークとしては、他にGLUTが有名で、数多くの情報が得られますが、最近はアップデートが停止していて推奨されていません。
API互換のオープンソースであるFreeGLUTもありますので、そちらを使う手もありますが、今回はGLFWを使います。

ただ、Qt等の例外を除き、OpenGLのフレームワークは、WPFやWindows Formsのようにボタンやメニュー、テキストボックス等の便利なコンポーネントは準備されていません。
GLFWも同様で、マウスクリックやウィンドウのリサイズ等のイベントについてはイベントハンドラ等によって取り扱う事ができるようになっていますが、GUIを使用した便利なアプリをGLFWのみで作成するのは結構ハードルが高くなります。

ですので、今回はボタン等のGUIの部分についてはWPFを使い、YUVデータ表示については、GLFWで別ウィンドウを開いて、そちらに描画するようにします。

なお、OpenGL、GLFWおよびGLSL等につきましては、

等を参考にさせていただきました。

準備

Visual Studio 2022のインストールは済んでいるものとします。
なお、WPFやVC++を使用しますので、インストール時に「C++ によるデスクトップ開発」や「.NETデスクトップ開発」を選択しているものとします。

もし、選択されていない場合には、Visual Studio Installerの「変更」からインストール画面に入り、選択してください。

VisualStudio2022 Install VC++ & .NET

今回のサンプルプログラムでも、YUV420YV12データであるCIFサイズの画像、akiyo_cif.yuv"を使用します。
ダウンロードができていなければ、ダウンロードしておいてください。

サンプルプログラムの作成

早速、サンプルプログラムを作成して行きます。

前回までと違い、ベースとなるプログラムとして妥当なコードが見つかりませんでしたので、ホームページ等を参考にしつつ、コードを作成して行こうと思います。

ソリューションとプロジェクト開始

Visual Studio 2022を開始し、「新しいプロジェクトの作成」を選択します。

新しいプロジェクトの作成」画面で、「WPF アプリケーション」を選択します。
なお、上部でC#, Windows, デスクトップを選択すると、選択肢が減って選びやすくなります。

Select WPF App

プロジェクト名場所等を聞かれますが、適当に設定して構いません。
以下、サンプルコードでは、プロジェクト名を「OpenGLYUV」とし、ソリューションとプロジェクトを同じディレクトリに配置するのチェックは外します。
また、フレームワークは.NET 6.0 (長期的なサポート)を選択しておきます。

更に、C++のDLLのプロジェクトを追加します。

ソリューションエクスプローラ」ウィンドウでソリューションOpenGLYUVを右クリックし、「追加」から「新しいプロジェクト…」を選択します。

新しいプロジェクトの追加」画面で、「ダイナミックライブラリ (DLL)」を選択します。
なお、上部でC++, Windows, ライブラリを選択すると、選択肢が減って選びやすくなります。

Select C++ DLL

プロジェクト名を聞かれますが、適当に設定して構いません。
場所については、デフォルトでソリューションディレクトリが設定されているはずですので、そのままでOKです。

以下、サンプルコードでは、プロジェクト名を「ShowYUV」とします。

パッケージの追加

Windowsでは、OpenGLのDLLは標準でインストールされていますが、OpenGL 1.1で、非常に古いバージョンとなります。

また、GLFWはWindowsに標準でインストールされていませんので、追加でインストールする必要があります。

更に、GLEWGLMも使用しますので、それらも追加でインストールします。

何れも個別にサイトからダウンロードしてインストールする事もできますが、今回はVisual Studio 2022のNuGetパッケージからインストールします。

なお、GLFWとGLEWは、nupengl.coreパッケージをインストールすると、fleeglutと共に一緒にインストールされます。
ただ、現状、非推奨となっていますし、パスの設定を行う必要がある等、使い勝手が悪いので今回は使いません。

ですので、個別にglfwglew-2.2.0, glmをNuGetからインストールします。

なお、NuGetからのインストールは

  1. ソリューションエクスプローラー」のShowYUVプロジェクトを右クリック
  2. NuGetパッケージの管理…」を選択
  3. 参照」からパッケージを選択しインストール

する事で行います。

プロジェクトのプロパティ等

プロジェクトのコンパイルやデバッグのため、プロパティや依存関係を設定します。

OpenGLYUV

OpenGLYUVプロジェクトのプロパティについては、出力先やデバッグ設定を行います。

  1. Setting Output Dirソリューションエクスプローラー」でOpenGLYUVプロジェクトを右クリック
  2. プロパティ」を選択
  3. プロパティ画面で「ビルド」→「出力」を選択
  4. 基本出力パス」に$(SolutionDir)x64\を記入
  5. Setting Debug Nativeプロパティ画面で「デバッグ」を選択し、「デバッグ起動プロファイルUIを開く」をクリック
  6. ネイティブ コードのデバッグを有効にする」を☑チェック
  7. Projects Dipendences再度、「ソリューションエクスプローラー」でOpenGLYUVプロジェクトを右クリック
  8. ビルドの依存関係」→「プロジェクトの依存関係…」を選択
  9. プロジェクトの依存関係」ウィンドウで、「依存先」のShowYUVを☑チェックし、OKをクリック

ShowYUV

ShowYUVプロジェクトのプロパティでは、ライブラリの設定を行います。

  1. Add Opengl libソリューションエクスプローラー」でShowYUVプロジェクトを右クリック
  2. プロパティ」を選択
  3. ShowYUVプロパティ」ウィンドウの「構成」で全ての構成を、「プラットフォーム」でx64を選択
  4. 左ペインの「リンカー」→「入力」を選択
  5. 右ペインの「追加の依存ファイル」をクリックした後、右端の[∨]をクリックし「編集…」を選択
  6. 追加の依存ファイル」ウィンドウで、最上部の記入欄にopengl32.libを追加し、OKをクリック

GUIの作成

WPFを使用してGUIを作成します。

今回のGUIについては画像を表示する必要がありませんので、YUVファイルをオープンするためのボタンと再生ボタン、ファイル名を表示するための領域のみを配置したものとなります。

  1. OpenGLYUVプロジェクトのMainWindow.xamlのコードを開く
  2. <Grid>を2行2列に分割
  3. <Grid>の0行0列にFileOpen<Button>を配置
  4. <Grid>の0行1列にPlay<Button>を配置し、「IsEnable」をfalseに設定
  5. <Grid>の1行0列にFileName<TextBlock>を列幅2で配置し、「IsEnable」をfalseに設定
  6. OpenGLYUVプロジェクトのMainWindow.xamlのデザイナーを開く
  7. FileOpen<Button>を選択し、プロパティを表示
  8. 右上のイベントハンドラーボタンをクリックし、「Click」の項目をダブルクリック→FileOpen_Clickが挿入される
  9. Play<Button>を選択し、プロパティを表示
  10. 右上のイベントハンドラーボタンをクリックし、「Click」の項目をダブルクリック→Play_Clickが挿入される

なお、サンプルでは、ボタンのサイズを80(w)×40(h)に、テキストブロックのサイズを160(w)×20(h)にしておきます。
それに伴い、ウィンドウのサイズを200(w)×160(h)に変更しておきます。

コード

XAMLのコードは以下の通り

<Window x:Class="OpenGLYUV.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:OpenGLYUV"
        mc:Ignorable="d"
        Title="MainWindow" Height="160" Width="200" ResizeMode="NoResize">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition/>
            <RowDefinition/>
        </Grid.RowDefinitions>
        <Grid.ColumnDefinitions>
            <ColumnDefinition/>
            <ColumnDefinition/>
        </Grid.ColumnDefinitions>
        <Button x:Name="FileOpen" Content="File…" Width="80" Height="40" Click="FileOpen_Click" Margin="5"/>
        <Button x:Name="Play" Content="Play" IsEnabled="False" Grid.Column="1" Width="80" Height="40" Margin="5" Click="Play_Click"/>
        <TextBlock x:Name="FileName" Text="File Name" Grid.Row="1" Grid.ColumnSpan="2" Width="160" Height="20" IsEnabled="False" TextTrimming="CharacterEllipsis"/>

    </Grid>
</Window>
      

DLLの作成

ShowYUVプロジェクトでは、OpenGLを使用してYUVデータを表示するためのDLLを作成します。

APIは2つ、

  1. YUVファイル名を指定し、ウィンドウをオープンしてファイルの最初のフレームを表示
  2. 次のフレームを表示

のみです。

クラスは4つ

  1. GLFWを使用してウィンドウを表示すると共にOpenGLの設定を行うCWindowクラス
  2. レクタングルの頂点やテクスチャ座標を設定、描画を行うCRendererクラス
  3. 指定ファイルからYUVデータを読み込み、テクスチャを作成、設定するCTextureクラス
  4. シェーダープログラムを読み込み、コンパイル、設定するCShaderクラス

です。

更に、バーテックスシェーダプログラムを記述したファイルとフラグメントシェーダプログラムを記述したファイルを作成します。

ヘッダー追加: framework.h

framework.hにDLLで使用するヘッダーを追加します。

先ず、C++の標準ライブラリ関連で、<string>, <fstream>, <stdexcept>, <mutex>, <vector>を追加しておきます。

次に、OpenGL関連で、<GL/glew.h>, <GLFW/glfw3.h>, <glm/glm.hpp>, <glm/ext.hpp>を追加しておきます。

最終的に、framework.hは以下のようになります。

#pragma once

#define WIN32_LEAN_AND_MEAN             // Windows ヘッダーからほとんど使用されていない部分を除外する
// Windows ヘッダー ファイル
#include <windows.h>

#include <string>
#include <fstream>
#include <stdexcept>
#include <mutex>
#include <vector>

#include <GL/glew.h>
#include <GLFW/glfw3.h>
#include <glm/glm.hpp>
#include <glm/ext.hpp>
      

CWindowクラス

ファイル作成
  1. ソリューションエクスプローラー」でShowYUVプロジェクトを右クリック
  2. 追加」→「クラス…」を選択
  3. クラスの追加」ウィンドウで、「クラス名」にCWindowを「.hファイル」にWindow.hを「.cppファイル」にWindow.cppを記入し、OKをクリック
ヘッダーファイル: Window.h

ヘッダーファイルは以下のようにします。

#pragma once

#include "Renderer.h"

/**
* CWindow
* OpenGLのGLFWでウィンドウを表示
*/
class CWindow
{
private:
    static const std::string DEF_TITLE;
    static std::mutex init;
    static int init_count;

private:
    // ウィンドウのハンドル
    GLFWwindow* m_pWindow;
    CRenderer* m_pRenderer;

public:
    /**
    * コンストラクタ
    */
    CWindow(
        UINT width = CTexture::CIF_WIDTH,
        UINT height = CTexture::CIF_HEIGHT,
        const char* pFileName = nullptr,
        const char* pTitle = DEF_TITLE.c_str());

    /**
    * デストラクタ
    */
    virtual ~CWindow();

    /**
    * 描画
    */
    virtual HRESULT Render();

protected:
    /**
    * OpenGLを初期化
    */
    virtual void InitOpenGL();

    /**
    * OpenGLを初期化
    */
    virtual void TerminateOpenGL();

    /**
    * ウィンドウサイズをセット
    */
    void SetSize(GLuint width, GLuint height);

    /**
    * ウィンドウのサイズ変更時のコールバック関数
    */
    static void Resize(GLFWwindow* const window, int width, int height);

};
        
静的変数/定数

先ず、クラス全体で使用する定数や変数をWindows.hファイル内のCWindowクラス宣言の内部で定義します。

定数としては、ウィンドウのデフォルトのタイトルとしてDEF_TITLEを定義します。
静的変数としては、OpenGLの初期化と終了処理の回数を管理する、initとinit_countを定義しておきます。

なお、静的変数や定数の初期化は、Windows.cppファイルの先頭付近で以下のようにして行います。

const std::string CWindow::DEF_TITLE = "Show YUV";
std::mutex CWindow::init;
int CWindow::init_count = 0;
          
メンバ変数

インスタンスのメンバ変数については、GLFWのウィンドウのハンドルm_pWindowとCRendererのオブジェクトm_pRendererの2つを宣言しておきます。

なお、CRendererを使用しますので、Window.hファイルの先頭付近でRenderer.hをインクルードしておきます。

コンストラクタ

コンストラクタは、引数としてウィンドウのサイズ、ファイル名、ウィンドウのタイトルを取るようにします。
其々、以下のようにデフォルト値を設定しておきます。

CWindow(
    UINT width = CTexture::CIF_WIDTH,
    UINT height = CTexture::CIF_HEIGHT,
    const char* pFileName = nullptr,
    const char* pTitle = DEF_TITLE.c_str());
        

なお、ウィンドウのサイズに使用しているCTexture::CIF_WIDTHとCTexture::CIF_HEIGHTですが、CTextureクラス内でテクスチャのサイズとして定義している定数です。
定義しているヘッダーファイルTexture.hはRenderer.hでインクルードするので、そのまま使用できています。

Window.cppでの実装は以下の通りです。

CWindow::CWindow(UINT width, UINT height, const char* pFileName, const char* pTitle)
    : m_pWindow(NULL)
    , m_pRenderer(nullptr)
{
    InitOpenGL();

    m_pWindow = glfwCreateWindow(width, height, pTitle, NULL, NULL);
    if (NULL == m_pWindow)
    {
        throw std::runtime_error("Failed to create opengl window using GLFW.");
    }
    // 現在のウィンドウを処理対象にする
    glfwMakeContextCurrent(m_pWindow);

    glClearColor(1.0f, 0.0f, 0.0f, 1.0f);

    // GLEWを初期化する
    glewExperimental = GL_TRUE;
    if (GLEW_OK != glewInit())
    {
        TerminateOpenGL();
        throw std::runtime_error("Failed to initialize GLEW.");
    }

    // 垂直同期のタイミングを待つ
    glfwSwapInterval(1);

    // このインスタンスの this ポインタを記録しておく
    glfwSetWindowUserPointer(m_pWindow, this);

    // ウィンドウのサイズ変更時に呼び出す処理の登録
    glfwSetWindowSizeCallback(m_pWindow, Resize);

    m_pRenderer = new CRenderer(pFileName);

    // 開いたウィンドウの初期設定
    Resize(m_pWindow, width, height);
}
        

なお、InitOpenGL()メソッドとTerminateOpenGL()メソッド、Resize()メソッドは、後に追加します。

InitOpenGL()メソッドでOpenGLの初期化後、glfwCreateWindow()関数でウィンドウを作成します。

次に、glfwMakeContextCurrent()関数で作成したウィンドウを操作対象に指定します。

glClearColor()関数では、glClear()関数で描画領域をクリアする際の色を指定しています。
サンプルでは赤にしていますが、好きな色を指定してください。

その後、GLEWを初期化するため、glewInit()関数をコールします。
なお、

glewExperimental = GL_TRUE;

については、GLEWをフル活用するためのおまじないです。

glfwSwapInterval()関数は、描画を指示した後に実際の描画を行うタイミングの指定です。
何回目の垂直同期のタイミングで描画を行うかを指定するのですが、基本は1、つまり次の垂直同期のタイミングで描画を行うようにします。

glfwSetWindowUserPointer()関数については、後にクラスメソッドからオブジェクトにアクセスする際に使用するポインタを退避するためにコールしています。

glfwSetWindowSizeCallback()関数については、ウィンドウがリサイズされた際にコールされる関数を設定しています。

最後に、CRendererのオブジェクトを作成した後、Resize()メソッドで最初の描画を行っています。

デストラクタ

デストラクタでは、CRendererのオブジェクトを解放した後、glfwDestroyWindow()関数でウィンドウを破棄し、TerminateOpenGL()メソッドでOpenGLの終了処理を行っています。

CWindow::~CWindow()
{
    if (nullptr != m_pRenderer)
    {
        delete m_pRenderer;
    }
    glfwDestroyWindow(m_pWindow);
    TerminateOpenGL();
}
        
InitOpenGLメソッド

InitOpenGL()メソッドでは、glfwInit()関数でGLFWOpenGLを初期化後、glfwWindowHint()関数で使用するOpenGLのバージョンとプロファイルを設定しています。
今回は、Version 3.3, Core Profileとします。

なお、glfwInit()関数は

メインスレッドからのみコールされなければならない

ようなので、複数回のコールを適切に処理できるようにはなっていない感じです。

なので、今回はOpenGLの初期化の回数と終了処理の回数をカウントして、最初の初期化時のみglfwInit()関数をコールするようにします。
因みにmutexによるロックは、複数のスレッドから同時にアクセスされて初期化用のカウンタが壊されるのを防ぐためのものです。

void CWindow::InitOpenGL()
{
    init.lock();

    if (init_count == 0)
    {
        // GLFWを初期化する
        if (GL_FALSE == glfwInit())
        {
            init.unlock();
            throw std::runtime_error("Failed to initialize opengl using GLFW.");
        }

        // OpenGL Version 3.3 Core Profile を選択する
        glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
        glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
        glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE);
        glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
    }
    init_count++;

    init.unlock();
}
        
TerminateOpenGLメソッド

TerminateOpenGL()メソッドでは、初期化用のカウンタinit_countをデクリメントし、0になったらglfwTerminate()関数をコールしてOpenGLの終了処理を行います。
初期化用のカウンタを使用していますので、mutexで制御しています。

void CWindow::TerminateOpenGL()
{
    init.lock();
    if (--init_count <= 0)
    {
        glfwTerminate();
    }
    init.unlock();
}
        
Renderメソッド

Render()メソッドは、YUVファイルから次のフレームデータを読み込み、テクスチャにセットした後、描画します。

先ず、CRendererのオブジェクトが存在する事を確認後、CRenderer::SetNextTexture()メソッドでテクスチャに次のフレームのデータをセットします。

その後、glClear()関数でウィンドウの描画領域をクリアした後、CRenderer::Render()メソッドで描画を行います。

描画完了後、glfwSwapBuffers()関数でバッファの入れ替えを行います。

なお、今回は4角形の板の描画しかしませんので、glClear()関数では、GL_COLOR_BUFFER_BITのみを指定しています。

一般的に複数のオブジェクトを立体的に配置するような場合にはGL_DEPTH_BUFFER_BITを、画素のマスク等で特殊効果を使用する場合には、GL_STENCIL_BUFFER_BITも追加で指定してください。

HRESULT CWindow::Render()
{
    HRESULT hr = S_FALSE;
    if (nullptr != m_pRenderer)
    {
        hr = m_pRenderer->SetNextTexture();
        if (S_OK == hr)
        {
            glClear(GL_COLOR_BUFFER_BIT);
            m_pRenderer->Render();
            glfwSwapBuffers(m_pWindow);
        }
    }
    return hr;
}
        
SetSizeメソッド

SetSize()メソッドでは、ウィンドウのフレームバッファの横幅と高さを引数にして、CRendererのオブジェクトにサイズをセット後、再描画を行います。

先ず、CRendererのオブジェクトが存在する事を確認後、CRenderer::SetSize()メソッドでフレームバッファの新しい横幅と高さをセットします。

その後は、Render()メソッドと同様、描画を行います。
ただ、テクスチャについては、前に設定したデータをそのまま使用します。

void CWindow::SetSize(GLuint width, GLuint height)
{
    if (nullptr != m_pRenderer)
    {
        m_pRenderer->SetSize(width, height);
        glClear(GL_COLOR_BUFFER_BIT);
        m_pRenderer->Render();
        glfwSwapBuffers(m_pWindow);
    }
}
        
Resizeメソッド

Resize()メソッドは、ウィンドウがリサイズされた際にコールバックされる関数のため、クラスメソッドとして宣言しています。

引数としては、コールしたGLFWwindowクラスのオブジェクトと新しいウィンドウのサイズがセットされます。
ただ、ウィンドウのサイズと実際の描画対象となるフレームバッファのサイズは異なることがあるようなので、先ずは、glfwGetFramebufferSize()関数でフレームバッファのサイズを取得します。

その後、glViewport()関数でビューポートをフレームバッファに合わせて、全体が表示されるよう変更します。

次に、CRendererのオブジェクトのサイズをセットし、再描画を行うのですが、Resize()メソッドはクラスメソッドであるため、直接、CRendererのオブジェクトを使用する事ができません。
そこで、glfwGetWindowUserPointer()関数で、コンストラクタで登録しておいたCWindowのオブジェクトを取り出し、そのオブジェクトを使用してCWindow::SetSize()メソッドをコールします。

void CWindow::Resize(GLFWwindow* const window, int width, int height)
{
    // フレームバッファのサイズを調べてフレームバッファ全体をビューポートに設定する
    int fbWidth, fbHeight;
    glfwGetFramebufferSize(window, &fbWidth, &fbHeight);
    glViewport(0, 0, fbWidth, fbHeight);

    // windowに関連付けられているインスタンスを取得し、サイズをセット
    CWindow* const pinstance = static_cast<CWindow*>(glfwGetWindowUserPointer(window));
    if (NULL != pinstance)
    {
        pinstance->SetSize(fbWidth, fbHeight);
    }
}
        

CRendererクラス

ファイル作成
  1. ソリューションエクスプローラー」でShowYUVプロジェクトを右クリック
  2. 追加」→「クラス…」を選択
  3. クラスの追加」ウィンドウで、「クラス名」にCRendererを「.hファイル」にRenderer.hを「.cppファイル」にRenderer.cppを記入し、OKをクリック
ヘッダーファイル: Renderer.h

ヘッダーファイルは以下のようにします。

#pragma once

#include "Shader.h"
#include "Texture.h"

/**
* CRenderer
* 実際にレンダリングを行う
*/
class CRenderer
{
private:
    static const GLfloat VERTEX[];
    static const GLfloat TEXTURE[];

private:
    GLuint m_vertexArrayObjID;
    GLuint m_vbuffer;
    GLuint m_tbuffer;
    CShader* m_pShader;
    CTexture* m_pTexture;

public:
    /**
    * コンストラクタ
    */
    CRenderer(const char* pFileName = nullptr);

    /**
    * デストラクタ
    */
    virtual ~CRenderer();

    /**
    * 描画
    */
    virtual void Render();

    /**
    * サイズセット
    */
    virtual void SetSize(GLuint width, GLuint height);

    /**
    * 次のテクスチャセット
    */
    virtual HRESULT SetNextTexture();
};
        
定数

先ず、クラス全体で使用する定数をRenderer.hファイル内のCRendererクラス宣言の内部で定義します。

定数としては、頂点データのVERTEX[]とテクスチャ座標のTEXTURE[]を定義します。
テクスチャ座標については、テクスチャ全体を4角形全体に貼り付けるように設定しています。
また、頂点データは、中心を原点にして、CIFサイズの4角形になるように設定しています。

なお、定数の初期化は、Renderer.cppファイルの先頭付近で以下のようにして行います。

const GLfloat CRenderer::TEXTURE[] =
{
    0.0f, 1.0f,
    0.0f, 0.0f,
    1.0f, 0.0f,
    1.0f, 1.0f,
};

const GLfloat CRenderer::VERTEX[] =
{
    -(CTexture::CIF_WIDTH / 2.0f), -(CTexture::CIF_HEIGHT / 2.0f), 0.0f,
    -(CTexture::CIF_WIDTH / 2.0f),   CTexture::CIF_HEIGHT / 2.0f,  0.0f,
      CTexture::CIF_WIDTH / 2.0f,    CTexture::CIF_HEIGHT / 2.0f,  0.0f,
      CTexture::CIF_WIDTH / 2.0f,  -(CTexture::CIF_HEIGHT / 2.0f), 0.0f,
};
          
メンバ変数

インスタンスのメンバ変数については、Vertex Array Object名をセットするm_vertexArrayObjID、頂点バッファ名をセットするm_vbuffer、テクスチャバッファ名をセットするm_tbuffer、CShaderオブジェクトm_pShader、CTextureオブジェクトm_pTextureを宣言しておきます。

なお、CShaderおよびCTextureを使用しますので、Renderer.hファイルの先頭付近でShader.hとTexture.hをインクルードしておきます。

コンストラクタ

コンストラクタは、引数としてファイル名を取るようにします。
なお、引数のファイル名のデフォルト値にはnullptrを設定しておきます。

コンストラクタ内では、Vertex Array Objectを作成、バインドした後、頂点データ用のバッファとテクスチャ座標用のバッファを作成、バインドします。
今回は、単純な4角形の頂点とテクスチャだけですので、Vertex Array Objectの作成、バインドは、ただ面倒なだけなのですが、最近のOpenGLでは頂点データ等の受け渡しにはVertex Array Objectを使用する事がデフォルトになっているようですので、とりあえずglGenVertexArrays()関数で作成し、glBindVertexArray()関数でバインドしておきます。

次に、glGenBuffers()関数でバッファを作成し、glBindBuffer()関数でバインドしておきます。

最後に、glBufferData()関数で、実際の頂点データ(VERTEX[])やテクスチャ座標(TEXTURE[])をセットしておきます。なお、今回、頂点データやテクスチャ座標は、一旦セットした後は変更しませんので、第4引数の"usage"には、GL_STATIC_DRAWをセットしています。

バッファ作成後は、CShaderとCTextureオブジェクトを作成しています。

CRenderer::CRenderer(const char* pFileName)
    : m_vertexArrayObjID(0)
    , m_vbuffer(0)
    , m_tbuffer(0)
    , m_pShader(nullptr)
    , m_pTexture(nullptr)
{
    glGenVertexArrays(1, &m_vertexArrayObjID);
    glBindVertexArray(m_vertexArrayObjID);
    glGenBuffers(1, &m_vbuffer);
    glBindBuffer(GL_ARRAY_BUFFER, m_vbuffer);
    glBufferData(GL_ARRAY_BUFFER, sizeof(VERTEX), VERTEX, GL_STATIC_DRAW);
    glGenBuffers(1, &m_tbuffer);
    glBindBuffer(GL_ARRAY_BUFFER, m_tbuffer);
    glBufferData(GL_ARRAY_BUFFER, sizeof(TEXTURE), TEXTURE, GL_STATIC_DRAW);
    m_pShader = new CShader();
    m_pTexture = new CTexture(pFileName);
}
        
デストラクタ

デストラクタでは、CShaderオブジェクトとCTextureオブジェクトを解放した後、コンストラクタで作成した頂点データ用のバッファとテクスチャ座標用のバッファ、Vertex Array Objectを削除しておきます。

CRenderer::~CRenderer()
{
    if (nullptr != m_pTexture)
    {
        delete m_pTexture;
    }
    if (nullptr != m_pShader)
    {
        delete m_pShader;
    }
    glDeleteBuffers(1, &m_tbuffer);
    glDeleteBuffers(1, &m_vbuffer);
    glDeleteVertexArrays(1, &m_vertexArrayObjID);
}
        
Renderメソッド

Render()メソッドでは、実際の描画を行います。

先ずは、頂点データとテクスチャ座標データをバッファを通してバーテックスシェーダープログラムの入力にセットします。

glEnableVertexAttribArray()関数で、バーテックスシェーダープログラムの入力の番号を指定してデータの割り当てができるようにします。

その後、glBindBuffer()関数でセットするバッファを指定後、glVertexAttribPointer()関数でデータのサイズや型等の指定を行います。

頂点データとテクスチャ座標データのバッファ其々に設定を行った後、glDrawArrays()関数で4角形をレンダリングします。

最後に、glDisableVertexAttribArray()関数でデータ割り当ての終了を指示します。

void CRenderer::Render()
{
    glEnableVertexAttribArray(CShader::POSITION_ID);
    glEnableVertexAttribArray(CShader::TEXTURE_ID);

    glBindBuffer(GL_ARRAY_BUFFER, m_vbuffer);
    glVertexAttribPointer(
        CShader::POSITION_ID,   // 属性0:0に特に理由はありません。しかし、シェーダ内のlayoutとあわせないといけません。
        3,                      // サイズ
        GL_FLOAT,               // タイプ
        GL_FALSE,               // 正規化?
        0,                      // ストライド
        (void*)0                // 配列バッファオフセット
    );

    glBindBuffer(GL_ARRAY_BUFFER, m_tbuffer);
    glVertexAttribPointer(
        CShader::TEXTURE_ID,      // 属性0:0に特に理由はありません。しかし、シェーダ内のlayoutとあわせないといけません。
        2,                      // サイズ
        GL_FLOAT,               // タイプ
        GL_FALSE,               // 正規化?
        0,                      // ストライド
        (void*)0                // 配列バッファオフセット
    );

    glDrawArrays(GL_TRIANGLE_FAN, 0, 4); // 頂点0から始まります。合計4つの頂点です。

    glDisableVertexAttribArray(CShader::TEXTURE_ID);
    glDisableVertexAttribArray(CShader::POSITION_ID);
}
        
SetSizeメソッド

SetSize()メソッドでは、フレームバッファのサイズを受け取り、アスペクト比から視野空間(View Volume)の設定を行います。

CIFサイズの4角形をCIFのアスペクト比を維持しながら、フレーム一杯に表示するように、高さもしくは幅を拡縮し、CShader::SetTransformationMatrix()メソッドを使ってシェーダーの変換行列に設定します。

void CRenderer::SetSize(GLuint width, GLuint height)
{
    if (nullptr != m_pShader)
    {
        double tempH = static_cast<double>(CTexture::CIF_WIDTH) * height / width;
        double tempW;
        if (tempH > CTexture::CIF_HEIGHT)
        {
            tempW = static_cast<double>(CTexture::CIF_WIDTH);
        }
        else
        {
            tempH = static_cast<double>(CTexture::CIF_HEIGHT);
            tempW = static_cast<double>(CTexture::CIF_HEIGHT) * width / height;
        }
        m_pShader->SetTransformationMatrix(-tempW / 2, tempW / 2, -tempH / 2, tempH / 2);
    }
}
        
SetNextTextureメソッド

SetNexttexture()メソッドでは、CTextureオブジェクトのチェックを行った後、CTexture::SetNextFrame()メソッドをコールします。

HRESULT CRenderer::SetNextTexture()
{
    HRESULT hr = S_FALSE;
    if (nullptr != m_pTexture)
    {
        hr = m_pTexture->SetNextFrame();
    }
    return hr;
}
        

シェーダープログラム

CShaderクラスの説明の前に、バーテックス及びフラグメントシェーダープログラムについて見て行きます。

シェーダープログラムファイルは、Direct3DにおけるHLSLで記述するエフェクトファイルと同様、OpenGLにおけるGLSLで記述するファイルです。

前回のDirect3Dの場合には、テクスチャにセットされたY, U, VデータをRGB変換して、張り付けるプログラムをHSLSで記述しました。
今回は、OpenGLのGLSLで同様のプログラムをグラフィックスパイプライン中のバーテックスシェーダーとフラグメントシェーダーに対して行います。

なお、通常、この様な簡単な処理だけであれば、この二つのシェーダーのみをプログラムすれば事足ります。

ファイル作成
  1. ソリューションエクスプローラー」でShowYUVプロジェクトを右クリック
  2. 追加」→「新しい項目…」を選択
  3. 新しい項目の追加」ウィンドウで、「名前」にShader.vertと記入し、OKをクリック
  4. 同様にして、Shader.fragファイルも作成しておきます。

基本的にファイル名は何でもOKですが、今回はバーテックスシェーダープログラムをShader.vert、フラグメントシェーダプログラムをShader.fragとします。

出力先変更

なお、Shader.vertおよびShader.fragはC++のプログラム中で実行時にコンパイルするため、Visual Studioではコンパイルしないように設定しなければなりません。

また、実行時にはDLLと同じフォルダ、もしくはC++プログラム内で指定するフォルダにコピーしておく必要があるため、以下のように、コンパイルの代わりにファイルをコピーするようにします。

  1. Setting File Copyソリューションエクスプローラー」からShader.vertを右クリックし、「プロパティ」を選択
  2. Shader.vert プロパティページ」ウィンドウの「構成」で全ての構成を選択
  3. ビルドから除外」は空欄とする
  4. 項目の種類」でファイルをコピーするを選択し、「適用」ボタンをクリック
  5. Destination左ペインの「構成プロパティ」に「ファイルをコピーする」が追加されますので「全般」をクリック
  6. 移動先のディレクトリ」の右側をクリックし、<編集…>を選択
  7. 移動先のディレクトリ」ウィンドウで$(OutDir)net6.0-windows\を記入
  8. 親またはプロジェクトの規定値から継承」の☑チェックを外し、OKボタンをクリック
  9. 「Shader.vert プロパティページ」ウィンドウでOKボタンをクリック

同様にShader.fragファイルについてもファイルをコピーする設定を行います。

Shader.vert

バーテックスシェーダープログラムは、頂点データ等の処理を行います。

#version 330 core

in vec3 position;
in vec2 vertexUV;
out vec2 uv;
uniform mat4 modelViewProj;

void main()
{
    vec4 v = vec4(position, 1);
    gl_Position = modelViewProj * v;
    uv = vertexUV;
}
        
バージョン

先ず、先頭で

#version バージョン

の形式でバージョンを指定します。

バージョン指定は、必ず最初の行に記述する必要があります。

なお、使用するOpenGLのバージョンとGLSLのバージョンは合わせるようにします。
例えば、OpenGL3.3 Core Profileであれば、GLSLは"#version 330 core"とします。

外部変数

外部変数は、データの入出力を行うために使用します。

形式は以下の通りです。

修飾子 型 変数名

修飾子は、const, uniform, in, out, inoutで変数の属性を示します。
型については、float, vec4, mat4等の変数の型を示します。
変数名は、適当な名前を指定します。

今回は、OpenGLから頂点データを渡す為に3次元ベクトルとしてpositionを、テクスチャ座標データをを渡すために2次元ベクトルとしてvertexUVを指定します。
なお、バーテックスシェーダープログラムでOpenGLから渡される頂点データやテクスチャ座標データの変数はin修飾子を指定します。

次に、テクスチャ座標データを次のステージ、今回はフラグメントシェーダ―に渡すための変数uvを指定します。
次のステージに渡す変数には、out修飾子を指定します。

最後に、OpenGLから渡される変換行列としてmodelViewProj変数を指定します。
頂点データやテクスチャ座標データ以外でOpenGLから渡すデータの変数については、uniform修飾子を指定します。

メイン関数

メイン関数内では、頂点データにw要素として1を付加して4次元ベクトルとした後、変換行列を掛けます。

結果は、gl_Positionに代入します。
なお、gl_Positionは予め設定された変数で、バーテックスシェーダープログラムの頂点データの出力をセットする事になっています。

また、テクスチャ座標データについては、何もせずにそのまま出力に渡しています。

Shader.frag

フラグメントシェーダープログラムは、頂点で囲まれた領域の各画素の処理を行います。
基本的にテクスチャの貼り付け等もフラグメントシェーダで行います。

今回は、YUVデータをテクスチャとして渡して、RGBデータに変換後、色データとしてセットします。

#version 330 core

const mat4 TORGB = mat4(
    1.164f,  1.164f, 1.164f, 0.0f,
    0.0f,   -0.392f, 2.017f, 0.0f,
    1.596f, -0.813f, 0.0f,   0.0f,
    0.0f,    0.0f,   0.0f,   1.0f);
const vec4 DIFF = vec4(16.0f / 255, 128.0f / 255, 128.0f / 255, 0.0f);

in vec2 uv;
out vec4 color;

uniform sampler2D textureSamplerY;
uniform sampler2D textureSamplerU;
uniform sampler2D textureSamplerV;

void main()
{
    vec4 fy = texture(textureSamplerY, uv);
    vec4 fu = texture(textureSamplerU, uv);
    vec4 fv = texture(textureSamplerV, uv);
    vec4 yuv = vec4(fy.r, fu.r, fv.r, 1.0f);

    yuv -= DIFF;
    vec4 rgb = TORGB * yuv;
    color = clamp(rgb, 0.0f, 1.0f);
}
        
バージョン

バーテックスシェーダープログラムと同じです。

外部変数

基本はバーテックスシェーダープログラムと同じです。

先ず、YUV⇒RGB変換のための4×4の行列TORGBを定数としてセットします。
DirectXのHLSLでも同様ですが、行列の並びは、特別な設定を行わない限り、

{{1列目}, {2列目}, {3列目}, {4列目}}

と列方向の値を並べるのがデフォルトとなっています。
ですので、サンプルのプログラムのように4×4の形で記載すると、実際の行列を転置したような形となります。

次に、YUVデータを変換行列と掛ける前に、YUVデータをシフトする必要がありますので、そのシフト量DIFFを定数としてセットします。

TORGBDIFFも定数ですので、修飾子としてconstを指定します。

更に、バーテックスシェーダーから渡されるテクスチャ座標データuvを2次元のベクトルとして指定します。
バーテックスシェーダーから渡されるデータの変数については、in修飾子を指定します。

また、次のステージに渡す、画素の色データcolorを4次元ベクトルとして指定します。
次のステージに渡す変数なので、out修飾子を指定します。

最後に、テクスチャにアクセスするための変数、textureSamplerY, textureSamplerU, textureSamplerVをsampler2Dとして指定します。
sampler2Dについては、2次元のテクスチャにアクセスするための変数の型です。
今回は、テクスチャとしてY, U, Vの3つのテクスチャをセットしますので、3つ用意する必要があります。
sampler2Dの値もOpenGLから設定思案すので、uniform修飾子を指定しておきます。

メイン関数

メイン関数内では、texture()関数を使用して、textureSamplerY, textureSamplerU, textureSamplerVのsampler2Dにバインドされたテクスチャとテクスチャ座標から、Y, U, Vのデータを取得します。
取得したY, U, Vデータ、fy, fu, fvは、4次元ベクトルで、其々の要素は0.0~1.0に規格化されています。

次にY, U, Vデータのr要素とα値1.0から4次元ベクトルを作成し、シフトを行います。

得られた4次元ベクトルとYUV⇒RGB変換行列を掛け合わせて、RGBAの4次元ベクトルを取得します。
なお、4×4の行列と掛け合わせる4次元ベクトルが行ベクトルか列ベクトルかは、自動で切り替わるようなので、掛け合わせる順番は

TORGB * yuv

でも

yuv * TORGB

でも問題ないようです。

結果は、0.0~1.0にクランプし、colorに代入して出力とします。

以前のバージョンのGLSLでは、フラグメントシェーダの出力は予め設定された変数、gl_FragColorに代入する必要がありましたが、新しいバージョンのGLSLでは、gl_FragColorは使用できなくなり、out修飾子の変数に出力するようになっています。

CShaderクラス

シェーダーファイルを作成できましたので、それらを読み込み、コンパイルしてOpenGLにセットするためのCShaderクラスを作成して行きます。

ファイル作成
  1. ソリューションエクスプローラー」でShowYUVプロジェクトを右クリック
  2. 追加」→「クラス…」を選択
  3. クラスの追加」ウィンドウで、「クラス名」にCShaderを「.hファイル」にShader.hを「.cppファイル」にShader.cppを記入し、OKをクリック
ヘッダーファイル: Shader.h

ヘッダーファイルは以下のようにします。

#pragma once

/**
* CShader
* シェーダープログラムを読み込んで設定
*/
class CShader
{
public:
    static const GLuint POSITION_ID = 0;
    static const GLuint TEXTURE_ID  = 1;

private:
    static const std::string VERTEX_FILE;
    static const std::string FRAGMENT_FILE;

private:
    GLuint m_programID;
    GLuint m_vertexID;      // バーテックスシェーダの番号
    GLuint m_fragmentID;    // フラグメントシェーダの番号

public:
    CShader();
    virtual ~CShader();
    void SetTransformationMatrix(double left, double right, double bottom, double top);

protected:
    void DeleteAllObjects();
    void ReadProgram(GLuint shaderID, const std::string& file_name);
    void SetTextureSampler();
};
        
定数

先ず、クラス全体で使用する定数をShader.hファイル内のCShaderクラス宣言の内部で定義します。

定数としては、バーテックスシェーダープログラム内のグローバル変数、positionvertexUVに割り当てる番号として、POSITION_IDTEXTURE_IDを定義します。

更に、シェーダープログラムのファイル名をVERTEX_FILEFRAGMENT_FILEとして定義しておきます。
なお、定数の初期化は、Shader.cppファイルの先頭付近で以下のようにして行います。

const std::string CShader::VERTEX_FILE = "Shader.vert";
const std::string CShader::FRAGMENT_FILE = "Shader.frag";
          
メンバ変数

インスタンスのメンバ変数については、プログラム全体に割り当てるm_programID、バーテックスシェーダープログラムに割り当てるm_vertexID、フラグメントシェーダプログラムに割り当てるm_fragmentIDを宣言しておきます。

コンストラクタ

コンストラクタでは、glCreateProgram()関数とglCreateShader()関数でプログラム全体およびバーテックスシェーダープログラム、フラグメントシェーダープログラムの作成を行います。

次に、CShader::ReadProgram()メソッドでファイルからバーテックスシェーダープログラムとフラグメントシェーダープログラムを読み込み、コンパイル、アタッチを行います。

プログラムの読み込み等に失敗した場合には、CShader::DeleteAllObjects()メソッドで作成済みのオブジェクトを解放した後、例外をスローします。

プログラムの読み込みが成功すれば、glBindAttribLocation()関数によりバーテックスシェーダープログラム内のグローバル変数positionvertexUVにアクセス用のIDをバインドします。
同時に、CShader::SetTextureSampler()メソッドでフラグメントシェーダープログラム内のtextureSampler型のグローバル変数のアクセス用IDを取得しておきます。

最後にglLinkProgram()関数とglUseProgram()関数でプログラムのリンクおよび使用開始の宣言を行っておきます。

CShader::CShader()
    : m_programID(0)
    , m_vertexID(0)
    , m_fragmentID(0)
{
    m_programID = glCreateProgram();
    if (0 == m_programID)
    {
        throw std::runtime_error("Failed to create new program.");
    }

    m_vertexID = glCreateShader(GL_VERTEX_SHADER);
    if (0 == m_vertexID)
    {
        DeleteAllObjects();
        throw std::runtime_error("Failed to create new vertex shader.");
    }
    try
    {
        ReadProgram(m_vertexID, VERTEX_FILE);
    }
    catch (std::runtime_error e)
    {
        DeleteAllObjects();
        throw;
    }

    m_fragmentID = glCreateShader(GL_FRAGMENT_SHADER);
    if (0 == m_fragmentID)
    {
        DeleteAllObjects();
        throw std::runtime_error("Failed to create new fragment shader.");
    }
    try
    {
        ReadProgram(m_fragmentID, FRAGMENT_FILE);
    }
    catch (std::runtime_error e)
    {
        DeleteAllObjects();
        throw;
    }

    glBindAttribLocation(m_programID, POSITION_ID, "position");
    glBindAttribLocation(m_programID, TEXTURE_ID, "vertexUV");

    glLinkProgram(m_programID);

    glUseProgram(m_programID);

    SetTextureSampler();
}
        
デストラクタ

デストラクタでは、コンストラクタで作成したオブジェクトをCShader::DeleteAllObjects()メソッドで削除します。

CShader::~CShader()
{
    DeleteAllObjects();
}
        
SetTransformationMatrixメソッド

SetTransformationMatrix()メソッドでは、引数として渡された視野空間(View Volume)の左右上下の座標を元に、ModelViewProjection変換用の行列を作成して、バーテックスシェーダープログラム内の変換行列のグローバル変数に渡します。

void CShader::SetTransformationMatrix(double left, double right, double bottom, double top)
{
    glm::mat4 Projection = glm::ortho(left, right, bottom, top);
    glm::mat4 View = glm::lookAt(
        glm::vec3(0, 0, 1), // ワールド空間でカメラは(4,3,3)にあります。
        glm::vec3(0, 0, 0), // 原点を見ています。
        glm::vec3(0, 1, 0)  // 頭が上方向(0,-1,0にセットすると上下逆転します。)
    );
    glm::mat4 Model = glm::mat4(1.0f);
    glm::mat4 MVP = Projection * View * Model;

    GLuint MatrixID = glGetUniformLocation(m_programID, "modelViewProj");
    glUniformMatrix4fv(MatrixID, 1, GL_FALSE, &MVP[0][0]);
}
        

Direct3Dを使用したYUVデータの表示でも説明した通り、視野角があって遠くにある物ほど小さく見えるのでは都合が悪いため、投射行列には平行投影用の行列を使用します。

GLMでは、平行投影用の行列を作成する関数、glm::ortho()がありますので、それを使用します。

次に、視点であるViewですが、GLMにはカメラ位置や向き、方向を指定して行列を作成する関数、glm::lookAt()が用意されています。

最後に、物体の移動や変形等を指定するModelの行列ですが、今回は描画する四角形は移動も変形もせずにそのまま使用しますので、単位行列を使います。
GLMでは、4×4の単位行列はglm::mat4(1.0f)で作成できます。

最後に、Model, View, Projectionの行列を掛け合わせて、バーテックスシェーダープログラム内の変換行列とします。

バーテックスシェーダープログラム内の変換行列のグローバル変数modelViewProjは、uniformで宣言されていますので、glGetUniformLocation()関数により、アクセス用のIDを取得します。

バーテックスシェーダープログラム内の4×4の行列のuniform変数については、glUniformMatrix4fv()関数により、アクセス用のIDを使って値をセットします。

DeleteAllObjectsメソッド

DeleteAllObjects()メソッドについては、コンストラクタ内でプログラムにアタッチしたバーテックスシェーダープログラムとフラグメントシェーダープログラムをでタッチした後、削除します。

最後に、プログラム自体を削除します。

void CShader::DeleteAllObjects()
{
    glDetachShader(m_programID, m_fragmentID);
    glDetachShader(m_programID, m_vertexID);
    glDeleteShader(m_fragmentID);
    glDeleteShader(m_vertexID);
    glDeleteProgram(m_programID);
}
        
ReadProgramメソッド

ReadProgram()メソッドについては、ファイルからシェーダープログラムを読み込み、コンパイルした後、プログラムにアタッチします。

void CShader::ReadProgram(GLuint shaderID, const std::string& file_name)
{
    // ファイルからソースコードを読み込み
    std::ifstream sourceFile;
    sourceFile.open(file_name, std::ios::in | std::ios::binary | std::ios::ate);
    if (!sourceFile.is_open())
    {
        throw std::runtime_error("Failed to open shader file " + file_name);
    }

    size_t size = sourceFile.tellg();
    sourceFile.seekg(0, sourceFile.beg);
    if (size <= 0)
    {
        sourceFile.close();
        throw std::runtime_error("Failed to read shader file " + file_name);
    }

    std::vector<char> source(size + 1);
    char* psource = source.data();
    sourceFile.read(psource, size);
    sourceFile.close();
    *(psource + size) = '\0';

    // ソースコードをコンパイルしてプログラムにアタッチ
    glShaderSource(shaderID, 1, &psource, NULL);
    glCompileShader(shaderID);
    GLint result;
    glGetShaderiv(shaderID, GL_COMPILE_STATUS, &result);
    if (GL_FALSE == result)
    {
        // コンパイル失敗ならエラーログ出力
        std::vector<char> info(256);
        GLsizei len;
        glGetShaderInfoLog(shaderID, 256, &len, info.data());
        _RPT0(_CRT_ERROR, info.data());
        throw std::runtime_error("Failed to compile shader file" + file_name);
    }

    glAttachShader(m_programID, shaderID);
}
        

先ず、ファイル名はShader.vertShader.fragを指定されていますので、全体をバイナリデータとして読み込んでしまいます。
この辺りの読み込み方法については、幾つかのやり方がありますので、基本的には、どの方法を使用してもOKです。
なお、今回、ファイル全体をバイナリデータとして読み込みましたので、終端にNUL文字’\0'を付加して文字列の終わりを明示します。
これを忘れると意味不明なコンパイルエラーで悩む事になります。

シェーダープログラムを読み込んだら、glShaderSource()関数でプログラムの文字列を指定し、glCompileShader()関数でコンパイルします。

コンパイル結果については、glGetShaderiv()関数の第2引数にGL_COMPILE_STATUSを指定して問い合わせます。
コンパイルに失敗した場合には、glGetShaderInfoLog()関数でエラーログを得る事ができます。
もし、コンパイルエラーが出た場合には、エラーログを確認してシェーダープログラムの修正を行います。

最後に、コンパイルしたシェーダープログラムをglAttachShader()関数でアタッチします。

SetTextureSamplerメソッド

SetTextureSampler()メソッドでは、先ず、フラグメントシェーダープログラム内のtextureSamplerのアクセス用IDをglGetUniformLocation()を使って取得します。

次に、glUniform1i()関数を使用し、textureSamplerのアクセス用IDを用いて其々のtextureSamplerにテクスチャのユニット番号を割り当てます。

なお、テクスチャのユニット番号とは、GL_TEXTURE0, GL_TEXTURE1, …等として定義されたテクスチャの識別番号で、GL_TEXTUREに続く番号をglUniform1i()関数でtextureSamplerのグローバル変数にセットする事で、テクスチャをフラグメントシェーダープログラムに紐付ける事ができます。
今回は、Y, U, V其々のデータ毎にテクスチャを作成しますので、3つのテクスチャの割り当てが必要となります。

void CShader::SetTextureSampler()
{
    // GLSL内のTextureSamplerにIDを割り振り
    GLint samplerIDY = glGetUniformLocation(m_programID, "textureSamplerY");
    glUniform1i(samplerIDY, 0); // GL_TEXTURE0
    GLint samplerIDU = glGetUniformLocation(m_programID, "textureSamplerU");
    glUniform1i(samplerIDU, 1); // GL_TEXTURE1
    GLint samplerIDV = glGetUniformLocation(m_programID, "textureSamplerV");
    glUniform1i(samplerIDV, 2); // GL_TEXTURE2
}
        

CTextureクラス

指定のYUVファイルからYUVデータを1フレーム毎に読み込み、テクスチャを作成します。

ファイル作成
  1. ソリューションエクスプローラー」でShowYUVプロジェクトを右クリック
  2. 追加」→「クラス…」を選択
  3. クラスの追加」ウィンドウで、「クラス名」にCTextureを「.hファイル」にTexture.hを「.cppファイル」にTexture.cppを記入し、OKをクリック
ヘッダーファイル: Texture.h

ヘッダーファイルは以下のようにします。

#pragma once

class CTexture
{
public:
    static const UINT CIF_WIDTH = 352;
    static const UINT CIF_HEIGHT = 288;
    static const std::string YUV_FILE_NAME;

private:
    std::ifstream* m_pFile;
    GLuint m_textureIDY;
    GLuint m_textureIDU;
    GLuint m_textureIDV;
    GLubyte* m_pY;
    GLubyte* m_pU;
    GLubyte* m_pV;
    GLuint m_width;
    GLuint m_height;
    GLuint m_size;

public:
    CTexture(const char* pFileName = YUV_FILE_NAME.c_str(), GLuint width = CIF_WIDTH, GLuint height = CIF_HEIGHT);
    virtual ~CTexture();
    virtual HRESULT SetNextFrame();

protected:
    void SetTextureAttribute();
    HRESULT ReadYUV();
};
        
定数

先ず、クラス全体で使用する定数をTexture.hファイル内のCTextureクラス宣言の内部で定義します。

定数としては、サンプルのYUVファイルakiyo_cif.yuvがCIFサイズですので、デフォルトの幅と高さとしてCIF_WIDTH352, CIF_HEIGHT288を定義しておきます。

また、デフォルトのYUVファイルとしてYUV_FILE_NAMEを定義しておきます。
なお、定数の初期化は、Texture.cppファイルの先頭付近で以下のようにして行います。

const std::string CTexture::YUV_FILE_NAME = "akiyo_cif.yuv";
          
メンバ変数

インスタンスのメンバ変数については、テクスチャアクセス用のIDをY, U, V其々にm_textureIDY, m_textureIDU, m_textureIDVとして宣言しておきます。

更に、ファイルから読み込んだ1フレーム分のY, U, Vのデータをストアしておくための領域用にm_pY, m_pU, m_pVを宣言しておきます。

後は、テクスチャのサイズを保持するためのm_width, m_height, m_sizeを宣言します。

コンストラクタ

コンストラクタでは、YUVファイル名とテクスチャの幅と高さを引数に取ります。

先ず、指定された幅と高さから、m_width, m_height, m_sizeをセットします。

次に、指定されたファイルをオープンしておきます。

更に、Y, U, Vデータを1フレーム分ストアできる領域を確保します。
なお、YUVデータはYUV420を想定していますので、UおよびVのデータサイズは、Yと比べて幅および高さが其々半分なので、配列のサイズとしては1/4となります。

最後に、CTexture::SetTextureAttribute()メソッドでY, U, V其々のテクスチャの作成を行い、CTexture::SetNextFrame()メソッドでテクスチャに実際のデータをセットします。

CTexture::CTexture(const char* pFileName, GLuint width, GLuint height)
    : m_pFile(nullptr)
    , m_textureIDY(0)
    , m_textureIDU(0)
    , m_textureIDV(0)
    , m_pY(nullptr)
    , m_pU(nullptr)
    , m_pV(nullptr)
    , m_width(width)
    , m_height(height)
    , m_size(width * height)
{
    m_pFile = new std::ifstream(pFileName, std::ifstream::in | std::ifstream::binary);
    if ((nullptr == m_pFile) || !m_pFile->good())
    {
        delete m_pFile;
        throw std::runtime_error("Failed to open YUV file " + YUV_FILE_NAME);
    }

    m_pY = new GLubyte[m_size];
    m_pU = new GLubyte[m_size / 4];
    m_pV = new GLubyte[m_size / 4];

    SetTextureAttribute();

    SetNextFrame();
}
        
デストラクタ

デストラクタでは、オープンしたファイルを閉じ、コンストラクタで作成したY, U, Vデータを1フレーム分ストアできる領域を解放します。

更に、glDeleteTextures()関数でテクスチャを削除します。

CTexture::~CTexture()
{
    if (nullptr != m_pFile)
    {
        m_pFile->close();
        delete m_pFile;
    }
    if (nullptr != m_pY)
    {
        delete[] m_pY;
    }
    if (nullptr != m_pU)
    {
        delete[] m_pU;
    }
    if (nullptr != m_pV)
    {
        delete[] m_pV;
    }
    if (0 != m_textureIDY)
    {
        glDeleteTextures(1, &m_textureIDY);
    }
    if (0 != m_textureIDU)
    {
        glDeleteTextures(1, &m_textureIDU);
    }
    if (0 != m_textureIDV)
    {
        glDeleteTextures(1, &m_textureIDV);
    }
}
        
SetNextFrameメソッド

SetNextFrame()メソッドでは、先ず、CTexture::ReadYUV()メソッドで1フレーム分のYUVデータを読み込みます。

データの読み込みが成功したら、テクスチャのユニット番号を指定してglActiveTexture()関数を呼び出す事でフラグメントシェーダープログラム中のtextureSampler型のグローバル変数をアクティベートします。

glBindTexture()関数で指定のIDのテクスチャをバインドし、glTexImage2D()で実際のデータ、m_pY, m_pU, m_pVをセットします。

HRESULT CTexture::SetNextFrame()
{
    HRESULT hr = ReadYUV();
    if (S_OK == hr)
    {
        glActiveTexture(GL_TEXTURE0);
        glBindTexture(GL_TEXTURE_2D, m_textureIDY);
        glTexImage2D(GL_TEXTURE_2D, 0, GL_RED, m_width, m_height, 0, GL_RED, GL_UNSIGNED_BYTE, m_pY);
        glActiveTexture(GL_TEXTURE1);
        glBindTexture(GL_TEXTURE_2D, m_textureIDU);
        glTexImage2D(GL_TEXTURE_2D, 0, GL_RED, m_width / 2, m_height / 2, 0, GL_RED, GL_UNSIGNED_BYTE, m_pU);
        glActiveTexture(GL_TEXTURE2);
        glBindTexture(GL_TEXTURE_2D, m_textureIDV);
        glTexImage2D(GL_TEXTURE_2D, 0, GL_RED, m_width / 2, m_height / 2, 0, GL_RED, GL_UNSIGNED_BYTE, m_pV);
    }
    return hr;
}
        
SetTextureAttributeメソッド

SetTextureAttribute()メソッドでは、各テクスチャを作成し、属性を設定しています。

先ず、glGenTextures()関数でテクスチャを作成し、IDを取得します。

次に、glBindTexture()関数で作成したテクスチャをバインドし、それに続く関数が適用されるテクスチャを指定します。

glPixelStorei()関数では、m_pY, m_pU, m_pVがバイト単位のデータですので、メモリアライメントを1に指定しています。

また、glTexParameteri()関数では、テクスチャを拡大、縮小する際に用いるフィルターを線形フィルターに指定しています。

void CTexture::SetTextureAttribute()
{
    glGenTextures(1, &m_textureIDY);
    glBindTexture(GL_TEXTURE_2D, m_textureIDY);
    glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
    glGenTextures(1, &m_textureIDU);
    glBindTexture(GL_TEXTURE_2D, m_textureIDU);
    glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
    glGenTextures(1, &m_textureIDV);
    glBindTexture(GL_TEXTURE_2D, m_textureIDV);
    glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
}
        
ReadYUVメソッド

ReadYUV()メソッドでは、単純に、1フレーム分のYUVデータをファイルから読み込んでいます。

HRESULT CTexture::ReadYUV()
{
    HRESULT hr = S_FALSE;

    if ((nullptr != m_pFile) && (m_pFile->good()))
    {
        if (!m_pFile->eof())
        {
            m_pFile->read(reinterpret_cast<char*>(m_pY), m_size);
            m_pFile->read(reinterpret_cast<char*>(m_pU), m_size / 4);
            m_pFile->read(reinterpret_cast<char*>(m_pV), m_size / 4);
            hr = S_OK;
        }
    }

    return hr;
}
        

dllmain.cpp

DLLのAPIを作成していきます。
既にdllmain.cppは作成されていますので、DLLMain()関数以外のコードを追加していきます。

// dllmain.cpp : DLL アプリケーションのエントリ ポイントを定義します。
#include "pch.h"

#include "Window.h"

BOOL APIENTRY DllMain( HMODULE hModule,
                       DWORD  ul_reason_for_call,
                       LPVOID lpReserved
                     )
{
    switch (ul_reason_for_call)
    {
    case DLL_PROCESS_ATTACH:
    case DLL_THREAD_ATTACH:
    case DLL_THREAD_DETACH:
    case DLL_PROCESS_DETACH:
        break;
    }
    return TRUE;
}

static CWindow* pwin = NULL;

extern "C" HRESULT WINAPI CreateOpenGLWindow(const char* pFileName)
{
    HRESULT hr = S_OK;

    if (pwin != NULL)
    {
        delete pwin;
    }

    try
    {
        UINT sideLen = CTexture::CIF_WIDTH;
        pwin = new CWindow(sideLen, sideLen, pFileName);
    }
    catch (std::exception e)
    {
        _RPT0(_CRT_ERROR, e.what());
        hr = S_FALSE;
    }

    return hr;
}

extern "C" HRESULT WINAPI Render()
{
    HRESULT hr = S_FALSE;
    if (pwin != NULL)
    {
        hr = pwin->Render();
    }
    return hr;
}
      

基本的に、DLLのAPIからのアクセスは、CWindowクラス使用しますので、Window.hをインクルードしておきます。

次に、CWindowクラスのオブジェクトを保持しておくグローバル変数として、pwinを宣言しておきます。

APIとしての関数は2つです。
1つ目は、YUVファイル名を指定してYUVデータを表示するウィンドウを作成するための関数、CreateOpenGLWindow()です。
2つ目は、次のフレームを表示するための関数、Render()です。

CreateOpenGLWindow関数

引数は、YUVファイル名です。

既にウィンドウがオープンされている場合には、pwinはNULLではありませんので、解放してウィンドウを閉じておきます。

次に、ウィンドウサイズとYUVファイル名を指定して、CWindowのオブジェクトを作成します。
なお、ウィンドウサイズは、CIFサイズではなく、幅、高さ共にCIFの幅に合わせた、正方形としています。
こうする事で、画像が表示された際に上下に帯ができて、画像のクリアに使用した色の確認等ができます。
ウィンドウのサイズは、適当に設定しても問題ないはずです。

Render関数

CWindow::Render()メソッドをコールします。
YUVファイルが終わりに達した場合には、S_FAIL(1L)が返ります。

モジュール定義ファイル

DLLのAPIを定義します。

  1. ソリューションエクスプローラー」でShowYUVプロジェクトを右クリック
  2. 追加」→「新しい項目…」を選択
  3. 新しい項目の追加」ウィンドウの左ペインの「Visual C++」→「コード」を選択
  4. 中央ペインの、モジュール定義ファイル(.def)を選択
  5. 名前」にShowYUVと記入し、OKをクリック

作成されたファイルには、以下のように記入します。

LIBRARY ShowYUV.dll

EXPORTS
    CreateOpenGLWindow
    Render
      

GUIのコード作成

DLL作成後は、WPFで作成したGUIからAPIを呼び出します。

DLLの設定

MainWindow.xaml.csのMainWindowクラス内で、DLLのAPIの宣言をしておきます。

[System.Runtime.InteropServices.DllImport("../ShowYUV.dll")]
private static extern int CreateOpenGLWindow(string fileName);

[System.Runtime.InteropServices.DllImport("../ShowYUV.dll")]
private static extern int Render();
      

なお、ShowYUV.dllが作成される場所により、指定のフォルダが異なる場合がありますので、DLLの読み込みに失敗する場合には確認してください。

FileOpen<Button>への対応

FileOpen<Button>がクリックされたら、FileOpen_Click()メソッドがコールされます。

FileOpen_Click()メソッド内では、ファイルオープンダイアログを表示し、YUVファイル名を取得します。

YUVファイル名が取得できたら、DLLのAPIである、CreateOpenGLWindow()関数をコールして、ウィンドウを作成し、最初のフレームを表示します。

private void FileOpen_Click(object sender, RoutedEventArgs e)
{
    var dialog = new Microsoft.Win32.OpenFileDialog();
    dialog.Title = "YUVファイルの選択";
    dialog.Filter = "YUVァイル (*.yuv)|*.yuv";
    dialog.CheckFileExists = true;

    // ダイアログを表示する
    if (dialog.ShowDialog() == true)
    {
        FileName.Text = dialog.FileName;
        Play.IsEnabled = true;
        CreateOpenGLWindow(dialog.FileName);
    }

}
      

Play<Button>への対応

Play<Button>がクリックされたら、Play_Click()メソッドがコールされます。

Play_Click()メソッド内では、定期的にDLLのAPIであるRender()関数がコールされるように、タイマーをスタートさせます。

タイマーの設定は、予めMainWindowクラスのコンストラクタ内で行っておきます。
タイマーのイベントハンドラとしてCompositionTarget_Rendering()メソッドを設定し、間隔を33msecとしておきます。

public MainWindow()
{
    InitializeComponent();
    _playTimer = new DispatcherTimer();
    _playTimer.Tick += new EventHandler(CompositionTarget_Rendering);
    _playTimer.Interval = new TimeSpan(0, 0, 0, 0, 33);
}
      
DispatcherTimer _playTimer;
private void Play_Click(object sender, RoutedEventArgs e)
{
    Play.IsEnabled = false;
    _playTimer.Start();
}
      

イベントハンドラ

タイマーのイベントハンドラcompositionTarget_Rendering()メソッドでは、DLLのAPIであるRender()関数をコールします。
Render()関数の戻り値がS_OK(0)以外の場合には、YUVファイルが終わりに達した等のエラーが発生していますので、タイマーを止めます。

void CompositionTarget_Rendering(object? sender, EventArgs e)
{
    int rc = Render();
    if (rc != 0)
    {
        _playTimer.Stop();
    }
}
      

実行

Debug Runコンパイル、実行を行いますと、先ず、ボタンのみが配置されたウィンドウが表示されます。

Play WindowFile…ボタンをクリックすると、ファイルオープンダイアログが表示されますので、YUVデータakiyo_cif.yuvを選択します。
新たにウィンドウが表示され、画像が表示されます。

サンプルコードにつきましては、こちらからダウンロードできます。

まとめ

以上でOpenGLによってYUVファイルを表示する事ができるようになりました。

なお、Windows向けのアプリケーションを作成する場合には、OpenGLを使うよりも、DirectXを使用した方が、相性も良いですし、使い勝手も良いです。
ただ、AndroidやLinux向けのアプリケーションをターゲットにする場合には、DirectXよりもOpenGLの方が汎用性も高く、情報も得やすいと思います。
何れにしても、YUVデータを表示する際に、YUV⇒RGB変換をCPUで行うのは負担が非常に大きく、お勧めできません。

最近のCPUはSIMD命令も充実してきていますので、数枚程度の画像であれば問題ないのですが、動画のように短時間で数十フレーム以上も処理するのであれば、浮動小数点の行列/ベクトル計算に特化したGPUに任せた方が遥かに効率が良いです。

YUV⇒RGB変換

YUV⇒RGB変換

前回、YUV⇒RGB変換を行った後、Direct2Dを用いてYUVデータを動画として表示できました。
今回は、Direct3Dを使用してYUVデータを動画として表示しようかと思います。

本来、YUVデータを動画として表示する程度であれば、Direct2DとDirectXMathの組み合わせで十分と云う気がします。
とは云え、最近のグラフィックチップは3Dに特化しているものが多く、グラフィック関連の処理は3Dで行った方が自由度が高く、且つ高速に行えます。

と云う事で、Direct3Dを用いて、YUVデータを表示する事にします。

なお、今回は素のウィンドウアプリケーションではなく、WPFを使ってみようと思います。
WPFのようなフレームワークを使用できれば、ボタン等のGUIを手軽に利用できますので、アプリの開発が非常に楽になります。

ただ、WPFはC#等のマネージドコードを使用する事が前提となっているのに対し、Direct3DはC++を前提としているため、WPFで直接Direct3Dを利用する事ができません。

そのため、Managed DirectXやSlimDX、SharpDX等、マネージドコードから利用できるラッパーも提供されてはいますが、どれも開発が止まっているようで、使い勝手が良くありません。

Windows API Code Pack for Microsoft .NET Frameworkと云うマイクロソフトによる公式のマネージコード用ライブラリもあるみたいですが、扱いがNuGetに移っているようです。

ですので、今回は、基本的にはDirect3Dに係わる部分はC++で記述してDLLとして纏め、UI部分をWPFとC#で作成するようにします。

準備

Visual Studio 2022のインストールは済んでいるものとします。
なお、WPFやVC++を使用しますので、インストール時に「C++ によるデスクトップ開発」や「.NETデスクトップ開発」を選択しているものとします。

もし、選択されていない場合には、Visual Studio Installerの「変更」からインストール画面に入り、選択してください。

VisualStudio2022 Install VC++ & .NET

今回のサンプルプログラムでも、YUV420YV12データであるCIFサイズの画像、akiyo_cif.yuv"を使用します。
ダウンロードができていなければ、ダウンロードしておいてください。

プログラムの作成

プロジェクト開始

Visual Studio 2022を開始し、「新しいプロジェクトの作成」を選択します。

新しいプロジェクトの作成」画面で、「WPF アプリケーション」を選択します。
なお、上部でC#, Windows, デスクトップを選択すると、選択肢が減って選びやすくなります。

Select WPF App

プロジェクト名場所等を聞かれますが、適当に設定して構いません。
以下、サンプルコードでは、プロジェクト名を「WPFViewYUV」とし、ソリューションとプロジェクトを同じディレクトリに配置するのチェックは外します。
また、フレームワークは.NET 6.0 (長期的なサポート)を選択しておきます。

更に、C++のDLLのプロジェクトを追加します。

ソリューションエクスプローラ」ウィンドウでソリューションWPFViewYUVを右クリックし、「追加」から「新しいプロジェクト…」を選択します。

新しいプロジェクトの追加」画面で、「ダイナミックライブラリ (DLL)」を選択します。
なお、上部でC++, Windows, ライブラリを選択すると、選択肢が減って選びやすくなります。

Select C++ DLL

プロジェクト名を聞かれますが、適当に設定して構いません。
場所については、デフォルトでソリューションディレクトリが設定されているはずですので、そのままでOKです。

以下、サンプルコードでは、プロジェクト名を「Direct3DYUV」とします。

Direct3D9の実装

早速、Direct3Dを実装していきます。

現在、よく使われているDirect3Dは、Direct3D9~12がありますが、今回はDirect3D9を使用します。
Direct3D9については資料が豊富ですし、YUVデータを表示する程度でしたら能力的にも十分です。

DLLの作成

先ずは、MSDNで説明されている「チュートリアル: WPF でホストするための Direct3D9 コンテンツの作成」に沿って、C++のDLLを作成して行きます。

サンプルコード内では、プロジェクト名が「D3DContent」となっていますが、「Direct3DYUV」に読み替えて下さい。

前準備

チュートリアルの「Direct3D9 プロジェクトを作成するには」の1~4については、既にプロジェクトを作成していますのでスキップします。

6~9については、Windows SDK 10を使用するVisual Studio 2022では必要ありません。

11については、d3d9.libは必要ですが、d3dx9.libは必要ありません。

12については、後で説明しますので、D3DContent.defの追加は保留しておきます。

クラスの作成

とりあえずチュートリアルの「Direct3D9 コンテンツを作成するには」の1で指示されているクラス、CRenderer, CRendererManager, CTriangleRendererを作成します。

クラスの作成については、「ソリューションエクスプローラ」ウィンドウでプロジェクトDirect3DYUVを右クリックし、「追加」から「クラス…」を選択します。

作成するファイルは、Renderer.h, Renderer.cpp, RendererManager.h, RendererManager.cpp, TriangleRenderer.h, TriangleRenderer.cppとします。
なお、CTriangleRendererについては、「基底クラス」をCRendererとしておきます。

クラスを追加後、2~7のコードを其々のファイルにコピーして行きます。

なお、今回作成した「Direct3DYUV」プロジェクトではStdAfx.hを使用しません。
代わりにpch.hframework.hを使用します。

なので、.cppファイルにコードをコピーする場合、元のインクルードファイルの記述は残しつつ、

#include "StdAfx.h"

の行は削除してください。

stdafx.h

チュートリアルの「Direct3D9 コンテンツを作成するには」のコードの

8. コード エディターで stdafx.h を開き、自動生成されたコードを次のコードに置き換えます。

については、今回、stdafx.hファイルを使用しないので、代わりに内容をframework.hにコピーしてください。

dllmain.cpp

チュートリアルの「Direct3D9 コンテンツを作成するには」のコードの

9.コード エディターで dllmain.cpp を開き、自動生成されたコードを次のコードに置き換えます。

については、置き換えた後に

#include "StdAfx.h"

#include "pch.h"

に直しておいて下さい。

Direct3DYUV.def

チュートリアルの「Direct3D9 コンテンツを作成するには」のコードの

10.コード エディターで D3DContent.def を開きます。

については、現状、D3DContent.defの作成を保留している状態ですので存在しません。
ですので、新規に作成します。

ただ、今回はプロジェクト名を「Direct3DYUV」にしていますので、ファイル名をD3DContent.defの代わりにDirect3DYUV.defを作成します。

  1. ソリューションエクスプローラ」ウィンドウでプロジェクトDirect3DYUVを右クリックし、「追加」から「新しい項目…」を選択
  2. 新しい項目の追加」ウィンドウで、左ペインのVisualC++下のコードをクリックし、中ペインのモジュール定義ファイルを選択
  3. 名前」をDirect3DYUV.defとし、「追加」ボタンをクリック
  4. 作成されたDirect3DYUV.defにサンプルコードをコピー
  5. ライブラリ名「LIBRARY “D3DContent"」を「LIBRARY “Direct3DYUV"」に変更
ライブラリの追加

ここで、チュートリアルではコンパイルを指示されていますが、そのままコンパイルするとエラーが出ます。

Visual Studio 2022で採用されているWindows SDK 10では、DirectXのライブラリの多くが取り込まれていますが、D3DXMath等、以前のDirectX9と共に提供されていたライブラリが分離され、別のライブラリとして提供されるようになっています。

今回は、NuGetから必要なライブラリ等をダウンロードするようにします。

  1. プロジェクト」→「NuGetパッケージの管理…」を選択
  2. NuGetウィンドウで「参照」をクリック
  3. Microsoft.DXSDK.D3DXを選択、インストール(検索欄にDXSDKと記入すると選びやすい)
エラーの修正

チュートリアルのコードでは、ライブラリを追加後も幾つかのエラーが報告されますので修正します。

CTriangleRenderer::Initメソッド内、

IFC(CRenderer::Init(pD3D, pD3DEx, hwnd, uAdapter));

は、初期化後に記述する必要がありますので、

CUSTOMVERTEX vertices[] =
{
    { -1.0f, -1.0f, 0.0f, 0xffff0000, }, // x, y, z, color
    {  1.0f, -1.0f, 0.0f, 0xff00ff00, },
    {  0.0f,  1.0f, 0.0f, 0xff00ffff, },
};
          

の後に移動します。

また、CTriangleRenderer::Renderメソッド内、

IFC(m_pd3dDevice->BeginScene());
IFC(m_pd3dDevice->Clear(
    0,
    NULL,
    D3DCLEAR_TARGET,
    D3DCOLOR_ARGB(128, 0, 0, 128),  // NOTE: Premultiplied alpha!
    1.0f,
    0
));
          

も初期化後に記述する必要がありますので、

UINT  iTime = GetTickCount() % 1000;
FLOAT fAngle = iTime * (2.0f * D3DX_PI) / 1000.0f;
          

の後に移動します。

UIの作成

次に、MSDNで説明されている「チュートリアル: WPF での Direct3D9 コンテンツのホスト」に沿って、WPFでUIを作成して行きます。

コード作成

チュートリアルの「Direct3D9 コンテンツをインポートするには」の手順に従い、MainWindow.xaml.csにコードを追加します。

なお、今回のサンプルコードでは、DLLの名前をDirect3DYUVとしましたので、コード中、

[DllImport("D3DCode.dll")]

となっている部分は、

[DllImport("Direct3DYUV.dll")]

に変更します。

次に、チュートリアルの「Direct3D9 コンテンツをホストするには」に従い、MainWindow.xamlにUIの記述を追加します。

最後にコンパイルして間違いが無いか確認します。

DLLのコピー

チュートリアルの「Direct3D9 コンテンツをホストするには」では、

3.Direct3D9 コンテンツを含む DLL を bin/Debug フォルダーにコピーします。

となっていますが、今回の場合、DLLはbin\Debug\net6.0-windowsにコピーします。

なお、他にも必要となるDLLがありますので、以下のようにコピーは自動化しておきます。

  1. ソリューションエクスプローラ」ウィンドウでソリューションWPFViewYUVを右クリックし、「ビルドの依存関係」から「プロジェクトの依存関係…」を選択
  2. 依存先」でDirect3DYUVをチェックしOKボタンをクリック
  3. ソリューションエクスプローラ」ウィンドウでソリューションDirect3DYUVを右クリックし、「プロパティ」を選択
  4. 構成」で「全ての構成」を選択
  5. 構成プロパティ」の「全般」を選択
  6. 全般プロパティ」ページの「出力ディレクトリ」を$(SolutionDir)WPFViewYUV\bin\$(Configuration)\net6.0-windowsに変更し、OKボタンをクリック

設定が終われば、「ビルド」→「ソリューションのリビルド」メニューを選択し、ソリューション全体をリビルドします。

実行

WPFViewYUVを実行すると、三角形がくるくると回るデモが表示されます。

ここまでのサンプルコードは、こちらからダウンロードできます。

YUV⇒RGB変換

前回、YUV⇒RGB変換を行ってWindowsFormで表示できたので、次はDirect2Dで表示してみます。

昔はDirectDrawを使って、かなり簡単に表示できたのですが、現在のDirectXはDirectDrawが無くなり、代わりにDirect2Dが使えるようになりました。
ただ、Direct2DもDirectXの一部なので、C#等のマネージドコードからは使い難く、どうしてもVC++を使う必要があります。

今回は、Win32APIを使って実装してみます。
以前はフレームワークとしてMFCを使い、かなり簡単に実装できたのですが、現在は流行っていないので使いませんでした。
MFCは歴史が古く実績が多い上に、Visual Studio 2022では無償版でも使えるため、もう少し使われても良いと思うのですが…。

準備

Visual Studio 2022のインストールは済んでいるものとします。
なお、VC++を使用しますので、インストール時に「C++ によるデスクトップ開発」を選択しているものとします。
もし、選択されていない場合には、Visual Studio Installerの「変更」からインストール画面に入り、選択してください。

VisualStudio2022 Install VC++

今回のサンプルプログラムでも、YUV420YV12データであるCIFサイズの画像、akiyo_cif.yuv"を使用します。
ダウンロードができていなければ、ダウンロードしておいてください。

プログラムの作成

プロジェクト開始

Visual Studio 2022を開始し、「新しいプロジェクトの作成」を選択します。

新しいプロジェクトの作成」画面で、「Windows デスクトップアプリケーション」を選択します。
なお、上部でC++, Windows, デスクトップを選択すると、選択肢が減って選びやすくなります。

Select Windows Desktop App

プロジェクト名場所等を聞かれますが、適当に設定して構いません。
以下、サンプルコードでは、プロジェクト名を「WinDeskTopViewYUV」とし、ソリューションとプロジェクトを同じディレクトリに配置するのチェックは外します。

コンパイル

とりあえず、何もせずにコンパイル⇒実行してみましょう。
以下のような画面が表示されます。

Win32 Form1 Initial

Direct2Dの実装

早速、Direct2Dを実装して行きます。

Direct2Dについては、MSDNで詳しく説明されています。
先ずは、その中の「単純なDirect2Dアプリケーションの作成」に記載されたサンプルコードを略そのまま実装します。

ヘッダーファイル

パート 1: DemoApp ヘッダーを作成する」の1および2のヘッダーおよびインターフェース、マクロ類は、framework.hに追加します。

なお、追加するヘッダー類は元々framework.hに記載されているものもあるため、重複しない様にします。

また、<tchar.h>と<wchar.h>は機能が被りますので今回は<tchar.h>を使います。
結果としてframework.hは以下のようになります。

// header.h : 標準のシステム インクルード ファイルのインクルード ファイル、
// またはプロジェクト専用のインクルード ファイル
//

#pragma once

#include "targetver.h"
#define WIN32_LEAN_AND_MEAN             // Windows ヘッダーからほとんど使用されていない部分を除外する
// Windows ヘッダー ファイル
#include <windows.h>
// C ランタイム ヘッダー ファイル
#include <stdlib.h>
#include <malloc.h>
#include <memory.h>
#include <tchar.h>
#include <math.h>

#include <d2d1.h>
#include <d2d1helper.h>
#include <dwrite.h>
#include <wincodec.h>

template<class Interface>
inline void SafeRelease(
    Interface** ppInterfaceToRelease
)
{
    if (*ppInterfaceToRelease != NULL)
    {
        (*ppInterfaceToRelease)->Release();

        (*ppInterfaceToRelease) = NULL;
    }
}

#ifndef Assert
#if defined( DEBUG ) || defined( _DEBUG )
#define Assert(b) do {if (!(b)) {OutputDebugStringA("Assert: " #b "\n");}} while(0)
#else
#define Assert(b)
#endif //DEBUG || _DEBUG
#endif

#ifndef HINST_THISCOMPONENT
EXTERN_C IMAGE_DOS_HEADER __ImageBase;
#define HINST_THISCOMPONENT ((HINSTANCE)&__ImageBase)
#endif
      

クラス追加

次に、実際のDirect2Dの表示を行う為のクラス、DemoAppを追加します。

プロジェクト」メニューから、「クラスの追加」を選択します。
クラス名」にDemoAppと記入すれば、ファイル名が自動的に入力されるので、確認後にOKボタンをクリックします。

DemoAppクラスのヘッダーファイル

作成されたDemoApp.hに「パート 1: DemoApp ヘッダーを作成する」の3および4のコードを転記します。
なお、DemoApp.hの先頭に、先程変更したframework.hをインクルードする事を忘れないでください。

また、後程、「情報表示」ダイアログを表示するためのコールバックをWinDeskTopViewYUV.cppからコピーするので、About(…)関数をクラスメソッドとして宣言をしておきます。
結果としてDemoApp.hは以下のようになります。

#pragma once

#include "framework.h"   // 忘れないように!!

class DemoApp
{
private:
    HWND m_hwnd;
    ID2D1Factory* m_pDirect2dFactory;
    ID2D1HwndRenderTarget* m_pRenderTarget;
    ID2D1SolidColorBrush* m_pLightSlateGrayBrush;
    ID2D1SolidColorBrush* m_pCornflowerBlueBrush;

public:
    DemoApp();
    ~DemoApp();

    // Register the window class and call methods for instantiating drawing resources
    HRESULT Initialize();

    // Process and dispatch messages
    void RunMessageLoop();

private:
    // Initialize device-independent resources.
    HRESULT CreateDeviceIndependentResources();

    // Initialize device-dependent resources.
    HRESULT CreateDeviceResources();

    // Release device-dependent resource.
    void DiscardDeviceResources();

    // Draw content.
    HRESULT OnRender();

    // Resize the render target.
    void OnResize(
        UINT width,
        UINT height
    );

    // The windows procedure.
    static LRESULT CALLBACK WndProc(
        HWND hWnd,
        UINT message,
        WPARAM wParam,
        LPARAM lParam
    );

    // 情報表示ダイアログ用コールバック
    static INT_PTR CALLBACK About(
        HWND hDlg,
        UINT message,
        WPARAM wParam,
        LPARAM lParam
    );
};
        
DemoAppクラスのコード

作成されたDemoApp.cppに「パート 2: クラスインフラストラクチャを実装する」の1~3のコードを転記します。
4のコードについては、後程、使用します。

更に、「パート 3: Direct2D リソースの作成」の1~5のコードを転記します。

なお、2と3のコードはHRESULT DemoApp::CreateDeviceResources()メソッドの内容のみで、HRESULT DemoApp::CreateDeviceResources()メソッドの全体は4にまとまっていますので、2と3は飛ばして4のみを転記しておきます。

次に「パート 4: Direct2D コンテンツをレンダリングする」の1~13までのコードを転記します。
なお、2~12は1つのメソッドを分割していますので、HRESULT DemoApp::OnRender()メソッドに纏めます。

最後に、「情報表示」ダイアログのメッセージハンドラーをWinDeskTopViewYUV.cppのAbout(…)メッセージハンドラーからコピーしておきます。
追加として、Direct2Dのライブラリをコードの先頭でpragmaによって指定しておきます。

また、ID2D1Factory::GetDesktopDpiが非推奨とするエラーが出るので、pragmaで抑制しておきます。

更に、後程、Resource.hに定義された値を使用しますので、DemoAPP.cppの先頭でResource.hもインクルードしておきます。

結果としてDemoApp.cppは以下のようになります。

#pragma comment(lib, "d2d1.lib")
#pragma warning(disable:4996)

#include "DemoApp.h"
#include "Resource.h"

DemoApp::DemoApp() :
    m_hwnd(NULL),
    m_pDirect2dFactory(NULL),
    m_pRenderTarget(NULL),
    m_pLightSlateGrayBrush(NULL),
    m_pCornflowerBlueBrush(NULL)
{
}

DemoApp::~DemoApp()
{
    SafeRelease(&m_pDirect2dFactory);
    SafeRelease(&m_pRenderTarget);
    SafeRelease(&m_pLightSlateGrayBrush);
    SafeRelease(&m_pCornflowerBlueBrush);

}
        
void DemoApp::RunMessageLoop()
{
    MSG msg;

    while (GetMessage(&msg, NULL, 0, 0))
    {
        TranslateMessage(&msg);
        DispatchMessage(&msg);
    }
}
        
HRESULT DemoApp::Initialize()
{
    HRESULT hr;

    // Initialize device-indpendent resources, such
    // as the Direct2D factory.
    hr = CreateDeviceIndependentResources();

    if (SUCCEEDED(hr))
    {
        // Register the window class.
        WNDCLASSEX wcex = { sizeof(WNDCLASSEX) };
        wcex.style = CS_HREDRAW | CS_VREDRAW;
        wcex.lpfnWndProc = DemoApp::WndProc;
        wcex.cbClsExtra = 0;
        wcex.cbWndExtra = sizeof(LONG_PTR);
        wcex.hInstance = HINST_THISCOMPONENT;
        wcex.hbrBackground = NULL;
        wcex.lpszMenuName = NULL;
        wcex.hCursor = LoadCursor(NULL, IDI_APPLICATION);
        wcex.lpszClassName = L"D2DDemoApp";

        RegisterClassEx(&wcex);


        // Because the CreateWindow function takes its size in pixels,
        // obtain the system DPI and use it to scale the window size.
        FLOAT dpiX, dpiY;

        // The factory returns the current system DPI. This is also the value it will use
        // to create its own windows.
        m_pDirect2dFactory->GetDesktopDpi(&dpiX, &dpiY);


        // Create the window.
        m_hwnd = CreateWindow(
            L"D2DDemoApp",
            L"Direct2D Demo App",
            WS_OVERLAPPEDWINDOW,
            CW_USEDEFAULT,
            CW_USEDEFAULT,
            static_cast<UINT>(ceil(640.f * dpiX / 96.f)),
            static_cast<UINT>(ceil(480.f * dpiY / 96.f)),
            NULL,
            NULL,
            HINST_THISCOMPONENT,
            this
        );
        hr = m_hwnd ? S_OK : E_FAIL;
        if (SUCCEEDED(hr))
        {
            ShowWindow(m_hwnd, SW_SHOWNORMAL);
            UpdateWindow(m_hwnd);
        }
    }

    return hr;
}
        
HRESULT DemoApp::CreateDeviceIndependentResources()
{
    HRESULT hr = S_OK;

    // Create a Direct2D factory.
    hr = D2D1CreateFactory(D2D1_FACTORY_TYPE_SINGLE_THREADED, &m_pDirect2dFactory);

    return hr;
}
        
HRESULT DemoApp::CreateDeviceResources()
{
    HRESULT hr = S_OK;

    if (!m_pRenderTarget)
    {
        RECT rc;
        GetClientRect(m_hwnd, &rc);

        D2D1_SIZE_U size = D2D1::SizeU(
            rc.right - rc.left,
            rc.bottom - rc.top
        );

        // Create a Direct2D render target.
        hr = m_pDirect2dFactory->CreateHwndRenderTarget(
            D2D1::RenderTargetProperties(),
            D2D1::HwndRenderTargetProperties(m_hwnd, size),
            &m_pRenderTarget
        );


        if (SUCCEEDED(hr))
        {
            // Create a gray brush.
            hr = m_pRenderTarget->CreateSolidColorBrush(
                D2D1::ColorF(D2D1::ColorF::LightSlateGray),
                &m_pLightSlateGrayBrush
            );
        }
        if (SUCCEEDED(hr))
        {
            // Create a blue brush.
            hr = m_pRenderTarget->CreateSolidColorBrush(
                D2D1::ColorF(D2D1::ColorF::CornflowerBlue),
                &m_pCornflowerBlueBrush
            );
        }
    }

    return hr;
}
        
void DemoApp::DiscardDeviceResources()
{
    SafeRelease(&m_pRenderTarget);
    SafeRelease(&m_pLightSlateGrayBrush);
    SafeRelease(&m_pCornflowerBlueBrush);
}
        
LRESULT CALLBACK DemoApp::WndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
{
    LRESULT result = 0;

    if (message == WM_CREATE)
    {
        LPCREATESTRUCT pcs = (LPCREATESTRUCT)lParam;
        DemoApp* pDemoApp = (DemoApp*)pcs->lpCreateParams;

        ::SetWindowLongPtrW(
            hwnd,
            GWLP_USERDATA,
            reinterpret_cast<LONG_PTR>(pDemoApp)
        );

        result = 1;
    }
    else
    {
        DemoApp* pDemoApp = reinterpret_cast<DemoApp*>(static_cast<LONG_PTR>(
            ::GetWindowLongPtrW(
                hwnd,
                GWLP_USERDATA
            )));

        bool wasHandled = false;

        if (pDemoApp)
        {
            switch (message)
            {
            case WM_SIZE:
            {
                UINT width = LOWORD(lParam);
                UINT height = HIWORD(lParam);
                pDemoApp->OnResize(width, height);
            }
            result = 0;
            wasHandled = true;
            break;

            case WM_DISPLAYCHANGE:
            {
                InvalidateRect(hwnd, NULL, FALSE);
            }
            result = 0;
            wasHandled = true;
            break;

            case WM_PAINT:
            {
                pDemoApp->OnRender();
                ValidateRect(hwnd, NULL);
            }
            result = 0;
            wasHandled = true;
            break;

            case WM_DESTROY:
            {
                PostQuitMessage(0);
            }
            result = 1;
            wasHandled = true;
            break;
            }
        }

        if (!wasHandled)
        {
            result = DefWindowProc(hwnd, message, wParam, lParam);
        }
    }

    return result;
}
        
HRESULT DemoApp::OnRender()
{
    HRESULT hr = S_OK;

    hr = CreateDeviceResources();
    if (SUCCEEDED(hr))
    {
        m_pRenderTarget->BeginDraw();

        m_pRenderTarget->SetTransform(D2D1::Matrix3x2F::Identity());

        m_pRenderTarget->Clear(D2D1::ColorF(D2D1::ColorF::White));

        D2D1_SIZE_F rtSize = m_pRenderTarget->GetSize();

        // Draw a grid background.
        int width = static_cast<int>(rtSize.width);
        int height = static_cast<int>(rtSize.height);

        for (int x = 0; x < width; x += 10)
        {
            m_pRenderTarget->DrawLine(
                D2D1::Point2F(static_cast<FLOAT>(x), 0.0f),
                D2D1::Point2F(static_cast<FLOAT>(x), rtSize.height),
                m_pLightSlateGrayBrush,
                0.5f
            );
        }

        for (int y = 0; y < height; y += 10)
        {
            m_pRenderTarget->DrawLine(
                D2D1::Point2F(0.0f, static_cast<FLOAT>(y)),
                D2D1::Point2F(rtSize.width, static_cast<FLOAT>(y)),
                m_pLightSlateGrayBrush,
                0.5f
            );
        }

        // Draw two rectangles.
        D2D1_RECT_F rectangle1 = D2D1::RectF(
            rtSize.width / 2 - 50.0f,
            rtSize.height / 2 - 50.0f,
            rtSize.width / 2 + 50.0f,
            rtSize.height / 2 + 50.0f
        );

        D2D1_RECT_F rectangle2 = D2D1::RectF(
            rtSize.width / 2 - 100.0f,
            rtSize.height / 2 - 100.0f,
            rtSize.width / 2 + 100.0f,
            rtSize.height / 2 + 100.0f
        );

        // Draw a filled rectangle.
        m_pRenderTarget->FillRectangle(&rectangle1, m_pLightSlateGrayBrush);

        // Draw the outline of a rectangle.
        m_pRenderTarget->DrawRectangle(&rectangle2, m_pCornflowerBlueBrush);

        hr = m_pRenderTarget->EndDraw();
    }

    if (hr == D2DERR_RECREATE_TARGET)
    {
        hr = S_OK;
        DiscardDeviceResources();
    }

    return hr;
}
        
void DemoApp::OnResize(UINT width, UINT height)
{
    if (m_pRenderTarget)
    {
        // Note: This method can fail, but it's okay to ignore the
        // error here, because the error will be returned again
        // the next time EndDraw is called.
        m_pRenderTarget->Resize(D2D1::SizeU(width, height));
    }
}
        
// バージョン情報ボックスのメッセージ ハンドラーです。
INT_PTR CALLBACK DemoApp::About(HWND hDlg, UINT message, WPARAM wParam, LPARAM lParam)
{
    UNREFERENCED_PARAMETER(lParam);
    switch (message)
    {
    case WM_INITDIALOG:
        return (INT_PTR)TRUE;

    case WM_COMMAND:
        if (LOWORD(wParam) == IDOK || LOWORD(wParam) == IDCANCEL)
        {
            EndDialog(hDlg, LOWORD(wParam));
            return (INT_PTR)TRUE;
        }
        break;
    }
    return (INT_PTR)FALSE;
}
        
メニューの追加

MSDNのDirect2Dのサンプルコードではメニューが無いのですが、せっかく、Windowsデスクトップアプリケーションのテンプレートではメニューが付いていて、「バージョン情報」のダイアログも表示できるようになっているので、メニューを追加してみます。

ダイアログのメッセージハンドラーは既にDemoAppクラスに追加しているので、ウィンドウ作成時にメニューのリソースを追加すると共に、メインのメッセージハンドラーでメニュー選択時の動作を記述していきます。

先ず、DemoApp::Initialize()メソッド中のWindow Classを設定するための構造体、WNDCLASSEX wcexのlpszMenuNameメンバの値をNULLからMAKEINTRESOURCEW(IDC_WINDESKTOPVIEWYUV)に変更します。
結果、DemoApp::Initialize()メソッドは、以下の通りとなります。

HRESULT DemoApp::Initialize()
{
    HRESULT hr;

    // Initialize device-indpendent resources, such
    // as the Direct2D factory.
    hr = CreateDeviceIndependentResources();

    if (SUCCEEDED(hr))
    {
        // Register the window class.
        WNDCLASSEX wcex = { sizeof(WNDCLASSEX) };
        wcex.style = CS_HREDRAW | CS_VREDRAW;
        wcex.lpfnWndProc = DemoApp::WndProc;
        wcex.cbClsExtra = 0;
        wcex.cbWndExtra = sizeof(LONG_PTR);
        wcex.hInstance = HINST_THISCOMPONENT;
        wcex.hbrBackground = NULL;
        wcex.lpszMenuName = MAKEINTRESOURCEW(IDC_WINDESKTOPVIEWYUV);
        wcex.hCursor = LoadCursor(NULL, IDI_APPLICATION);
        wcex.lpszClassName = L"D2DDemoApp";

        RegisterClassEx(&wcex);


        // Because the CreateWindow function takes its size in pixels,
        // obtain the system DPI and use it to scale the window size.
        FLOAT dpiX, dpiY;

        // The factory returns the current system DPI. This is also the value it will use
        // to create its own windows.
        m_pDirect2dFactory->GetDesktopDpi(&dpiX, &dpiY);


        // Create the window.
        m_hwnd = CreateWindow(
            L"D2DDemoApp",
            L"Direct2D Demo App",
            WS_OVERLAPPEDWINDOW,
            CW_USEDEFAULT,
            CW_USEDEFAULT,
            static_cast<UINT>(ceil(640.f * dpiX / 96.f)),
            static_cast<UINT>(ceil(480.f * dpiY / 96.f)),
            NULL,
            NULL,
            HINST_THISCOMPONENT,
            this
        );
        hr = m_hwnd ? S_OK : E_FAIL;
        if (SUCCEEDED(hr))
        {
            ShowWindow(m_hwnd, SW_SHOWNORMAL);
            UpdateWindow(m_hwnd);
        }
    }

    return hr;
}
          

次に、メインのメッセージハンドラーであるDemoAPP::WndProc(…)にメニューのメッセージハンドラーを追加します。

元々、WinDeskTopViewYUV.cpp内のメッセージハンドラーとして登録されていたWndProc(…)内の、

case WM_COMMAND:
    {
        int wmId = LOWORD(wParam);
        // 選択されたメニューの解析:
        switch (wmId)
        {
        case IDM_ABOUT:
            DialogBox(hInst, MAKEINTRESOURCE(IDD_ABOUTBOX), hWnd, About);
            break;
        case IDM_EXIT:
            DestroyWindow(hWnd);
            break;
        default:
            return DefWindowProc(hWnd, message, wParam, lParam);
        }
    }
    break;
          

部分でメニューの処理をしていたので、これをそのままDemoAPP::WndProc(…)内にコピーします。
但し、hWndはhwndに、hInstはGetModuleHandle(NULL)に変える必要があります。

結果、DemoAPP::WndProc(…)は以下のようになります。

LRESULT CALLBACK DemoApp::WndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
{
    LRESULT result = 0;

    if (message == WM_CREATE)
    {
        LPCREATESTRUCT pcs = (LPCREATESTRUCT)lParam;
        DemoApp* pDemoApp = (DemoApp*)pcs->lpCreateParams;

        ::SetWindowLongPtrW(
            hwnd,
            GWLP_USERDATA,
            reinterpret_cast<LONG_PTR>(pDemoApp)
        );

        result = 1;
    }
    else
    {
        DemoApp* pDemoApp = reinterpret_cast<DemoApp*>(static_cast<LONG_PTR>(
            ::GetWindowLongPtrW(
                hwnd,
                GWLP_USERDATA
            )));

        bool wasHandled = false;

        if (pDemoApp)
        {
            switch (message)
            {
            case WM_COMMAND:
            {
                int wmId = LOWORD(wParam);
                // 選択されたメニューの解析:
                switch (wmId)
                {
                case IDM_ABOUT:
                    DialogBox(GetModuleHandle(NULL), MAKEINTRESOURCE(IDD_ABOUTBOX), hwnd, About);
                    break;
                case IDM_EXIT:
                    DestroyWindow(hwnd);
                    break;
                default:
                    return DefWindowProc(hwnd, message, wParam, lParam);
                }
            }
            result = 0;
            wasHandled = true;
            break;
            case WM_SIZE:
            {
                UINT width = LOWORD(lParam);
                UINT height = HIWORD(lParam);
                pDemoApp->OnResize(width, height);
            }
            result = 0;
            wasHandled = true;
            break;

            case WM_DISPLAYCHANGE:
            {
                InvalidateRect(hwnd, NULL, FALSE);
            }
            result = 0;
            wasHandled = true;
            break;

            case WM_PAINT:
            {
                pDemoApp->OnRender();
                ValidateRect(hwnd, NULL);
            }
            result = 0;
            wasHandled = true;
            break;

            case WM_DESTROY:
            {
                PostQuitMessage(0);
            }
            result = 1;
            wasHandled = true;
            break;
            }
        }

        if (!wasHandled)
        {
            result = DefWindowProc(hwnd, message, wParam, lParam);
        }
    }

    return result;
}
          
メイン関数の変更

元々のプロジェクトのコードであるWinDeskTopViewYUV.cppについては、とりあえずWin32APIでWindowを表示するコードが全て入っています。

ただ、DemoAPPクラスにDirect2Dを使用してWindowを表示するコードを記述しましたので、メイン関数以外は必要ありません。

ですので、wWinMain(…)関数とAbout(…)コールバック関数以外は、キッパリと削除します。

また、マクロ定義やグローバル変数、関数の宣言等も使用しないため削除します。

最後に、wWinMain(…)関数の内容は、「パート 2: クラスインフラストラクチャを実装する」の4のWinMain(…)関数の中身と入れ替えます。
wWinMain(…)関数内で、DemoAppを使っていますので、DemoApp.hをインクルードする事を忘れないでください。

なお、wWinmain(…)関数内の最初にCoInitialize(NULL)を、最後にCoUninitialize()をコールしているのは、DirectXを使用する際の、と云うか、COMを使う際の作法です。
通常、各スレッドの先頭でCoInitialize(NULL)を、最後にCoUninitialize()をコールしておきます。

結果として、WinDeskTopViewYUV.cppは以下のようになります。

// WinDeskTopViewYUV.cpp : アプリケーションのエントリ ポイントを定義します。
//

#include "framework.h"
#include "WinDeskTopViewYUV.h"
#include "DemoApp.h"

int APIENTRY wWinMain(_In_ HINSTANCE hInstance,
                     _In_opt_ HINSTANCE hPrevInstance,
                     _In_ LPWSTR    lpCmdLine,
                     _In_ int       nCmdShow)
{
    UNREFERENCED_PARAMETER(hInstance);
    UNREFERENCED_PARAMETER(hPrevInstance);
    UNREFERENCED_PARAMETER(lpCmdLine);
    UNREFERENCED_PARAMETER(nCmdShow);

    // TODO: ここにコードを挿入してください。

    // Use HeapSetInformation to specify that the process should
    // terminate if the heap manager detects an error in any heap used
    // by the process.
    // The return value is ignored, because we want to continue running in the
    // unlikely event that HeapSetInformation fails.
    HeapSetInformation(NULL, HeapEnableTerminationOnCorruption, NULL, 0);

    if (SUCCEEDED(CoInitialize(NULL)))
    {
        {
            DemoApp app;

            if (SUCCEEDED(app.Initialize()))
            {
                app.RunMessageLoop();
            }
        }
        CoUninitialize();
    }

    return 0;
}
        

動作確認

コンパイルして実行すれば、格子模様の真ん中に2重の四角が描画されたWindowが表示されます。
メニューもちゃんと動作するはずです。

Direct2D Demo App

なお、ここまでのコードについては、こちらからダウンロードできます。

YUV⇒RGB変換

前回、YUV⇒RGB変換を行ってBMPファイルに出力できたので、次はYUVデータを動画として表示してみます。

とりあえず一番簡単な方法として、Visual Studio 2022上でWindows Formと.NET 6.0を使い、PictureBox上に画像を動画として表示する事とします。
なお、Windows Formを使う際の言語はC#がデフォルトですので、今回はC#で実装します。

プログラムの作成

プロジェクト開始

Visual Studio 2022を開始し、「新しいプロジェクトの作成」を選択します。

新しいプロジェクトの作成」画面で、「Windows フォーム アプリ」を選択します。
なお、上部でC#, Windows, デスクトップを選択すると、選択肢が減って選びやすくなります。

Select WindowsForm App

プロジェクト名場所等を聞かれますが、適当に設定して構いません。

追加情報」画面でフレームワークを聞かれますが、今回は.NET 6.0を選択します。

以下、サンプルコードでは、プロジェクト名を「WinFormsViewYUV」とし、ソリューションとプロジェクトを同じディレクトリに配置するをチェックします。

コンパイル

とりあえず、何もせずにコンパイル⇒実行してみましょう。
以下のような画面が表示されます。

Form1 Initial

画面デザイン

次に、画面のデザインを行います。

Form1.csを表示し、画面上で右クリックをすると表示される「デザイナーの表示」を選択します。
画面デザインが表示されますので、以下の操作をします。

  1. PictureBoxを配置
  2. 配置したpictureBox1のSizeプロパティを352, 288とする
  3. 配置したpictureBox1のBorderStyleプロパティをFixed3Dとする
  4. TextBoxをPictureBoxの下に配置
  5. Buttonを2つ、TextBoxの横、PictureBoxの下に配置
  6. 全体のWindowをリサイズ

画面デザインは、以下のようになります。

Window design on Windows Forms

次に、button1のTextプロパティをButton1からFile…に、button2のTextプロパティをButton2からPlayに変更します。

更に、button1とtextBox1のEnabledプロパティをFalseとしておきます。

各パーツの役割は以下の通りです。

pictureBox1

画像を表示する領域です。
今回はCIFサイズの画像を表示しますので、サイズを352×288としています。

ボーダーはデフォルトの通り無くても構わないのですが、あった方が見栄えが良いので付けています。

textBox1

表示するYUVファイルの名前を表示します。
今回は、直接、ファイル名を入力できないように、EnabledをFalseにしました。

実用的なアプリでは、ファイル名を直接入力できるようにした方が使いやすいです。

button1

ファイル選択用のボタンです。
クリックした時に、ファイルオープンダイアログを表示して、YUVデータのファイルを選択できるようにします。

button2

画像表示を開始するためのボタンです。
ファイルが選択されるまではクリックできないよう、EnabledをFalseにしています。

初期化

では、プログラムを作成して行きます。
最初はFormが作成された時の初期化を確認していきます。

public const int CifWidth = 352;
public const int CifHeight = 288;

private Bitmap _bmp = new(CifWidth, CifHeight, System.Drawing.Imaging.PixelFormat.Format32bppRgb);

public Form1()
{
    InitializeComponent();
    pictureBox1.Image = _bmp;
}
    

コンストラクタで、コンポーネントの初期化が終わった後に、picturebox1に描画領域としてBitmapをセットしておきます。

BitmapについてはCIFサイズ352×28832ビットのXRGBを指定します。

ファイル選択

次に、表示するYUVファイルを選択するプログラムを作成します。

先ず、デザイナー画面でbutton1をダブルクリックします。
Form1.csファイルのForm1クラスにbutton1_Clickメソッドが追加されますので、以下のように変更します。

private void button1_Click(object sender, EventArgs e)
{
    var ofd = new OpenFileDialog
    {
        FileName = "akiyo_cif.yuv",
        Title = "Select CIF Size YUV File",
        Filter = "YUV file (*.yuv)|*.yuv",
        Multiselect = false,
        CheckFileExists = true,
        CheckPathExists = true
    };

    if (ofd.ShowDialog() == DialogResult.OK)
    {
        textBox1.Text = ofd.FileName;
        button2.Enabled = true;
    }
}
    

先ず、オープンファイルダイアログを作成、設定します。

OpenFileDialog::Filterには、yuvファイルのみを選択できるよう、設定します。

更に、複数選択はしませんので、OpenFileDialog::Multiselectをfalseにします。

また、ファイルやディレクトリが存在する事を担保するため、OpenFileDialog::CheckfileexistsとOpenFileDialog::CheckPathExistsをtrueとします。

最後に、textBox1にファイル名をセットし、buttun2のEnabledプロパティをtrueにして、クリックできるようにしておきます。

なお、ファイルのオープンについては、必要になった時点で、textBox1にセットしたファイル名を使用し、File.OpenReadメソッドを使用して行います。

ファイル名をtextBox1に直接書き込んでも良いのですが、ファイルが存在するかとか、".yuv"の拡張子か等、ファイルをオープンする前にチェックするコードを省くため、今回はtextBox1への直接書き込みは禁止しました。

YUVデータの読み込み

画像を作成するため、YUVデータをファイルから読み込みます。

先ず、YUVデータを保存するためのフィールドを設定します。

public const int CifWidth = 352;
public const int CifHeight = 288;
public const int YSize = CifWidth * CifHeight;
public const int UVSize = (CifWidth / 2) * (CifHeight / 2);

private byte[] _y = new byte[YSize];
private byte[] _u = new byte[UVSize];
private byte[] _v = new byte[UVSize];
    

次に、以下のメソッドでファイルから各フィールドにデータを読み込みます。

private int ReadYUV(FileStream fs)
{
    int size = fs.Read(_y, 0, YSize);
    if (size != YSize) return -1;
    size = fs.Read(_u, 0, UVSize);
    if (size != UVSize) return -1;
    size = fs.Read(_v, 0, UVSize);
    if (size != UVSize) return -1;

    return 0;
}
    

ファイルは既にオープンされていて、引数として渡されるものとします。

読み込みが成功すれば0を返すようにしています。

ファイルの最後に到達する等、読み込みが失敗すれば、-1を返します。

YUV⇒RGB変換

YUVデータをファイルから読み込んだ後は、RGBデータへの変換です。

public const int CifWidth = 352;
public const int CifHeight = 288;

unsafe private void ConvertYUV2RGB(IntPtr xrgb_data, int stride)
{
    Func<long, long> Clip = (n) => (n <= 0) ? 0 : ((n >= 256) ? 255 : n);

    uint* xrgb = (uint*)xrgb_data;

    for (var h = 0; h < CifHeight; h++)
    {
        int y_pos = h * CifWidth;
        int uv_pos = (h / 2) * (CifWidth / 2);
        int xrgb_pos = h * (stride / sizeof(uint));

        for (var w = 0; w < CifWidth; w++)
        {
            double y16 = _y[y_pos + w] - 16.0;
            double u128 = _u[uv_pos + (w / 2)] - 128.0;
            double v128 = _v[uv_pos + (w / 2)] - 128.0;

            double dr = (1.164 * y16) + (0.0 * u128) + (1.596 * v128);
            double dg = (1.164 * y16) + (-0.392 * u128) + (-0.813 * v128);
            double db = (1.164 * y16) + (2.017 * u128) + (0.0 * v128);
            dr = Math.Round(dr, MidpointRounding.AwayFromZero);
            dg = Math.Round(dg, MidpointRounding.AwayFromZero);
            db = Math.Round(db, MidpointRounding.AwayFromZero);

            long r = Clip((long)dr);
            long g = Clip((long)dg);
            long b = Clip((long)db);

            xrgb[xrgb_pos + w] = (uint)((r << 16) | (g << 8) | b);
        }
    }

    return;
}
    

変換後のRGBデータは、pictureBox1に割り当てた描画領域に書き込みます。
引数のxrgb_dataの型であるIntPtrは、アンマネージドのメモリ領域へのポインタです。

画像データを描画領域に展開する場合、描画領域はアンマネージドのメモリ領域に取られている事が普通です。
なので、動画の表示等を行う際は、アンマネージドのメモリ領域の操作を行う必要が出てきます。

メソッドの修飾子"unsafe"は、メソッド内でアンマネージドメモリ領域の操作を行う為の指定です。

なお、unsafeを使用した場合、コンパイル時に/unsafeオプションを指定する必要があります。
Visual Studio 2022では、プロジェクトのプロパティの「ビルド」⇒「全般」で「アンセーフコード」の'unsafe’キーワードを使用するコードをコンパイルできるようにします。にチェックします。

xrgb_dataは、メソッド内部でuintへのポインタとしてキャストして使用します。
pictureBox1の描画領域の設定時に、32ビットのXRGBのデータフォーマットを指定しているので、uintへのポインタとしてキャストしても問題ありません。

第2引数のstrideは、描画領域の実際の横幅をバイト単位でセットします。
通常、描画領域の実際の横幅は、描画領域指定時に設定した横幅と一致するのですが、稀に指定の横幅よりも大きなサイズで確保される場合があります。
なので、XRGBデータをセットする描画領域の位置を計算する際には、設定値の横幅ではなく、実際の横幅を取得して使用する必要があります。

後は、YUV⇒RGB変換でBMPファイルを作成した時と同様です。

実際の描画

実際の描画については以下の通りです。

public const int CifWidth = 352;
public const int CifHeight = 288;

private Bitmap _bmp = new(CifWidth, CifHeight, System.Drawing.Imaging.PixelFormat.Format32bppRgb);
private Rectangle _rect = new(0, 0, CifWidth, CifHeight);
private FileStream? _fs = null;

private void DrawImage()
{
    using (_fs = File.OpenRead(textBox1.Text))
    {
        while (ReadYUV(_fs) == 0)
        {
            var bmData = _bmp.LockBits(_rect, System.Drawing.Imaging.ImageLockMode.WriteOnly, _bmp.PixelFormat);
            ConvertYUV2RGB(bmData.Scan0, bmData.Stride);
            _bmp.UnlockBits(bmData);
            pictureBox1.Invalidate();
            pictureBox1.Update();
        }
    }
}
    

先ず、textBox1にセットしたファイル名を使用してファイルをオープンします。

次に、ファイルからYUVデータを順次読み込んで行きます。

描画領域としては、pictureBox1に割り当てたBitmapからLockBitsメソッドを使用して取得します。

LockBitsメソッドで取得したBitmapDataについては、Scan0に実際の描画領域へのポインタが、Strideに描画領域の実際の横幅がセットされます。

Scan0が指す領域はアンマネージドメモリの領域ですので、実際にデータを書き込んだりするには、unsafe指定を行う必要があります。

また、Strideについてはバイト単位・・・・・ですので注意が必要です。

YUV⇒RGB変換によって描画領域に1画面分のXRGBデータを書き込んだ後は、BitmapのUnlockBitsメソッドでBitmapに描画領域の操作が終了した事を通知します。

最後に、pictureBox1のInvalidateメソッドをコールして、pictureBox1に内容が更新された事を通知します。
続いて、pictureBox1のUpdateメソッドをコールして、更新された内容を反映します。

Playボタンのクリック

Playボタンをクリックした場合のイベントハンドラを実装します。

先ず、デザイナー画面でbutton2をダブルクリックします。
Form1.csファイルのForm1クラスにbutton2_Clickメソッドが追加されますので、以下のように変更します。

private void button2_Click(object sender, EventArgs e)
{
    button1.Enabled = false;
    button2.Enabled = false;
    DrawImage();
    button2.Enabled = true;
    button1.Enabled = true;
}
    

基本的には、ボタン類をクリックできないようにした後、描画を開始します。

ファイルの最後まで描画が終わった後、ボタン類をクリックできるようにして終了です。

YUV⇒RGB変換

動画や静止画の圧縮/伸張等をプログラミングしていると、意外にYUV⇔RGB変換を取り扱う機会が出てきます。
最近ではそれなりにしっかりと解説しているサイト等も増えていますが、以前は情報が少なく、規格書が主な情報源でした。
と云う訳で、今回はYUV⇒RGB変換に関する覚書です。

YUV形式とは

YUVとは色情報の表現形式の一種です。
同様の色情報の表現形式としてYCbCrYPbPr等があります。

輝度信号を表すYと、輝度信号と青色成分の差を表すU/Cb/Pb、輝度信号と赤色成分の差を表すV/Cr/Prの組み合わせで色を表現します。

厳密にはYUVとYCbCrやYPbPrは異なる形式で、アナログ信号とデジタル信号の違いやγ補正が掛かっているか、取り得る値の範囲等の違いがあります。
ただ、乱暴な云い方になりますが、Cb/Pb⇒U、Cr/Pr⇒Vと置き換えて読んでも実際上の問題は出ないと思います。

なお、コンピューター上でMPEG等の動画やJPEG等の静止画を扱う場合には、YUVの呼称が一般的かと思います。

YUVフォーマットについて

YUV形式の色情報を使って画像を表現する際のデータの並び方です。
MSDNにフォーマットの詳細が載っています。

色々な呼び方がありますが、メモリ上のデータ配置等も判るためFOURCCでの呼称が判り易いと思います。
なお、一般的に動画等でよく使用されるのは、YV12でしょうか?

YUVデータを直接扱えるハードウェアでは、NV12も結構使われているようです。

RGB形式とは

RGBとは色情報の表現形式の一種です。

赤を表すRと、緑を表すG、青を表すBの組み合わせで色を表現します。
BMP等の静止画やAVI等の動画で使われています。

RGB(モニター)
モニター画面の拡大

また、基本的にモニタ画面に表示する画像データはRGB形式です。
これは、目の前のモニタ画面を虫眼鏡ルーペで見て頂けると判りますが、実際にモニタ画面がRGBの組み合わせで色を表現しているために、非常に判り易い形式です。

RGBフォーマットについて

RGB形式の色情報を使って画像を表現する際のデータの並び方です。

RGB

1画素あたり3バイト(24ビット)を割り当てる形式です。

メモリ上で
RGBRGBRGB
のように並びます。

基本的にR→G→Bの順番ですが、ハードウェアの事情等、様々な条件により、順番が異なる場合があります。

XRGB

1画素あたり4バイト(32ビット)を割り当てる形式です。
RGBXとかXBGR等と記される事もあります。

メモリ上で
XRGBXRGB
のように並びます。
なお、Xの部分については使用しないため、値は不定としています。

基本的にX→R→G→Bの順番ですが、ハードウェアの事情やエンディアンの違い等、様々な条件により、順番が異なる場合があります。

この形式の利点は、RGBのフォーマットと異なり、1画素分のデータを4バイト(32ビット)の整数値(unsigned intやunsigned long)として一度に読み込める事です。

RGBのフォーマットは通常3バイトのデータですので、R, G, Bのデータを其々1バイト(8ビット)の整数として読み込む必要がり、データの読み込み速度として不利になる場合があります。

ARGB

XRGBと同様、1画素あたり4バイト(32ビット)を割り当てる形式です。
RGBAとかABGR等と記される事もあります。

メモリ上で
ARGBARGB
のように並びます。
なお、Aの部分についてはα(透明度)の値をセットします。

基本的にA→R→G→Bの順番ですが、ハードウェアの事情やエンディアンの違い等、様々な条件により、順番が異なる場合があります。

この形式の利点は、XRGBのフォーマットと同様、1画素分のデータを4バイト(32ビット)の整数値(unsigned intやunsigned long)として一度に読み込める事です。

α(透明度)

コンピューター上で取り扱う場合には、通常、整数であれば[0, 255]の範囲の値、浮動小数点の場合には、[0.0, 1.0]の範囲の値です。
通常、0(0.0)を完全に透明、255(1.0)を完全に不透明としますが、逆の場合もあるので注意が必要です。