前回の続きです。
前回はOBSのプラグイン開発環境を整え、ビルドするまでの流れを短く解説しました。
画面効果を作る
今回からはいよいよ機能を作っていきます。
今回のゴールとして、エフェクトフィルタを追加するために必要な実装の解説、シェーダーを書いてプロパティからパラメータを一つ編集できるようにする所まで紹介します。
シェーダーの読み込み方法については、ほかのサンプルでよく使われている.effectの読み込みでは無く、あえてソース内に内包する方法を紹介します。
実装が必要な物
前回の物に追加して実装が必要なものは以下の通りです。
順に解説します。
- データ受け渡しの為の構造体
- 効果の名前を返す関数
- シェーダー
- 生成関数
- 破棄関数
- 描画関数
- プロパティの作成関数
- プロパティのデフォルト値を設定する関数
- プロパティの更新処理をする関数
受け渡し用データの構造体を定義
これから実装していくコールバック関数で受け取るための構造体を定義します。
struct SampleEffectData {
obs_source_t* Context = nullptr;
gs_effect_t* Effect = nullptr;
gs_eparam_t* AmountParam = nullptr;
float Amount = 1.0f;
// mallocされた物を使うので、明示的に呼んで初期化.
void Reset() {
Context = nullptr;
Effect = nullptr;
AmountParam = nullptr;
Amount = 1.0f;
}
};
コンテキスト、シェーダー、パラメーターなど必要なものをメンバーに加えます。
詳細は後述の初期化処理などで解説します。
効果名を渡す関数を作る
const char* SampleEffectName([[maybe_unused]] void* unused) {
return obs_module_text("SampleEffect");
}
OBS上のエフェクト一覧に出てくる名前を返す関数です。
シェーダーを埋め込む
初期化を始める前に、使用するシェーダーの準備をしましょう。
多くのサンプルは.effectをdataフォルダから読み込む例が紹介されていますが、時にシェーダーを外に出したくないシチュエーションもあります。
今回はコード内の埋め込んだシェーダーを読み込む形で進めたいと思います。
※c++11以降で使えるようになった生文字列リテラルを用いて、cppに埋め込みます。(もちろんですが、拡張子が.cの場合は使えません)
static const char* sampleShader_s = R"(
uniform float4x4 ViewProj;
uniform texture2d image;
uniform float amount;
sampler_state textureSampler {
Filter = Linear;
AddressU = Clamp;
AddressV = Clamp;
};
struct VertData {
float4 pos : POSITION;
float2 uv : TEXCOORD0;
};
VertData VSDefault(VertData v_in) {
VertData vert_out;
vert_out.pos = mul(float4(v_in.pos.xyz, 1.0), ViewProj);
vert_out.uv = v_in.uv;
return vert_out;
}
float4 PSSampleEffect(VertData v_in) : TARGET {
float4 color = image.Sample(textureSampler, v_in.uv);
return lerp(color, float4(color.r, 0.0, 0.0, 1.0),amount);
}
technique Draw {
pass {
vertex_shader = VSDefault(v_in);
pixel_shader = PSSampleEffect(v_in);
}
}
)";
これは、最低限の機能にできるだけ絞ったものです。
amountの値が1に近づくにつれ、画面全体が赤くなります。
以下にこのシェーダーの簡単な説明をします。
予約されたCB
uniform float4x4 ViewProj;
uniform texture2d image;
この2点はOBS側が自動的にコンスタントバッファを設定してくれる予約語のような物です。
imageにはこのフィルタに入る前の段階の絵が来ます。
頂点シェーダー
OBSでフィルタ系の効果を書く上ではViewProjection行列をかけて終わりです。
変形したい場合などはここで介入しましょう。
ピクセルシェーダー
画面効果をここで書きます。この例ではamountの値に基づいて、赤成分のみの画像と元画像をブレンドしています。
生成関数を準備する
ここで必要なデータの確保、シェーダーの生成、コンスタントバッファの確保などを行っています。
エフェクトの生成に失敗した際、いくつかのサンプルではobs_enter_graphicsとobs_leave_graphicsの間で破棄関数をすぐに呼び出し、obs_enter_graphicsを入れ子で呼んでしまい、不定な動作を誘発するものがあります。
明示的に気にした方がいいでしょう。
void* SampleEffectCreate(obs_data_t* data, obs_source_t* context) {
// この効果に必要な情報を受け渡すための構造体の確保.
SampleEffectData* sampleEffect = reinterpret_cast<SampleEffectData*>(bzalloc(sizeof(SampleEffectData)));
sampleEffect->Reset();
// コンテキストの保存.
sampleEffect->Context = context;
// 描画に関わるものはenter↔leave間で初期化.
obs_enter_graphics();
sampleEffect->Effect = gs_effect_create(sampleShader_s, nullptr, nullptr);
if (sampleEffect->Effect) {
sampleEffect->AmountParam = gs_effect_get_param_by_name(sampleEffect->Effect, "amount");
}
obs_leave_graphics();
if (nullptr == sampleEffect->Effect) {
SampleEffectDestroy(sampleEffect);
return nullptr;
}
SampleEffectUpdate(sampleEffect, data);
return sampleEffect;
}
破棄関数を準備する
同様に破棄関数を準備します。
前述の通り、呼び出し元と本関数内でenterとleaveが一致するように注意しましょう。
void SampleEffectDestroy(void* data) {
if (nullptr != data) {
SampleEffectData *sampleEffect = reinterpret_cast<SampleEffectData *>(data);
if (nullptr != sampleEffect->Effect) {
obs_enter_graphics();
gs_effect_destroy(sampleEffect->Effect);
obs_leave_graphics();
}
bfree(data);
}
}
描画関数
描画の本体です。
この中ではobs_enter_graphicsとobs_leave_graphicsは呼ぶ必要はありません。
obs_source_process_filter_begin_with_color_spaceでバッチを開始し、obs_source_process_filter_tech_endで使用するテクニックを明示して描画を行います。
コンスタントバッファへのパラメータの設定は、このbegin~endの間で行います。
一次的なレンダーターゲットに描く場合や、スプライト描画などに関してはまた別の機会に解説します。
void SampleEffectRender(void* data, [[maybe_unused]]gs_effect_t* effect) {
SampleEffectData* sampleEffect = reinterpret_cast<SampleEffectData*>(data);
const enum gs_color_space preferredSpaces[] = {
GS_CS_SRGB,
GS_CS_SRGB_16F,
GS_CS_709_EXTENDED,
GS_CS_709_SCRGB,
};
const enum gs_color_space sourceSpace = obs_source_get_color_space(obs_filter_get_target(sampleEffect->Context), OBS_COUNTOF(preferredSpaces), preferredSpaces);
const enum gs_color_format format = gs_get_format_from_space(sourceSpace);
if (obs_source_process_filter_begin_with_color_space(sampleEffect->Context, format, sourceSpace, OBS_ALLOW_DIRECT_RENDERING)) {
gs_effect_set_float(sampleEffect->AmountParam, sampleEffect->Amount);
obs_source_process_filter_tech_end(sampleEffect->Context, sampleEffect->Effect, 0, 0, "Draw");
}
}
プロパティの作成関数
プロパティを作成します。
obs_properties_t* SampleEffectProperties([[maybe_unused]] void* data) {
obs_properties_t* props = obs_properties_create();
obs_properties_add_float_slider(props, "amount_", "Amount", 0.0, 1.0, 0.001);
return props;
}
プロパティのデフォルト値を設定する関数
SampleEffectPropertiesで作成したプロパティのデフォルト値を指定する関数です。
"規定値"ボタンを押した際に適用されます。

obs_properties_add_float_sliderでnameに指定した方の文字列で指定します。
void SampleEffectDefaults(obs_data_t* data) {
obs_data_set_default_double(data, "amount_", 1.0);
}
更新関数
プロパティの更新などここで取得します。
void SampleEffectUpdate(void* data, obs_data_t* settings) {
SampleEffectData* sampleEffect = reinterpret_cast<SampleEffectData*>(data);
if (nullptr != sampleEffect) {
sampleEffect->Amount = static_cast<float>(obs_data_get_double(settings, "amount_"));
}
}
これでエフェクトを実装するために必要なものはそろいました。
実装したものを組み込む
さあ次はこれまでの実装物を前回作った物に組み込んでいきましょう。
obs_source_infoへの設定
obs_source_info型の変数を用意し、これまで作った関数を設定していきます。
この時の初期化順はobs_source_info内の変数の宣言順に準拠しましょう。
obs_source_info SampleEffectInfo_s = {
.id = "SampleEffect",
.type = OBS_SOURCE_TYPE_FILTER,
.output_flags = OBS_SOURCE_VIDEO,
.get_name = SampleEffectName,
.create = SampleEffectCreate,
.destroy = SampleEffectDestroy,
.get_defaults = SampleEffectDefaults,
.get_properties = SampleEffectProperties,
.update = SampleEffectUpdate,
.video_render = SampleEffectRender,
};
モジュールロード時に登録
準備したobs_source_info型の変数をモジュールロード時に登録します。
bool obs_module_load(void) {
obs_log(LOG_INFO, "plugin loaded successfully (version %s)", PLUGIN_VERSION);
obs_register_source(&SampleEffectInfo_s);
return true;
}
完成
エフェクトフィルタの一覧から作った効果を追加し、プロパティを編集できる事を確認できました。

応用
ここまで来たら、普段グラフィックスエンジニアをやっている方なら色々と実装できるイメージが湧くのではないでしょうか。
試しにぼけ具合の制御など入れてみました。

まとめ
今回はできるだけ最小限の解説に留めつつ、シェーダーの埋め込みなど、実践する場合、気になる所を紹介してみました。
前述した通り、別のレンダーターゲットへ描く方法などは改めて機会あれば紹介したいと思います。
