Skip to content

v1.0.0

Latest

Choose a tag to compare

@routersys routersys released this 14 May 09:50
· 1 commit to main since this release
89b0e5c

v1.0.0 - 影(立体投影)for YMM4

YukkuriMovieMaker4 向けの透視投影落ち影エフェクトプラグインの初回リリースです。
仮想点光源と接地面を定義し、透視変換に基づく立体的な落ち影を GPU 上でリアルタイム生成します。
HLSL ピクセルシェーダーによる逆投影サンプリング・距離連動動的ブラー・黄金角スパイラルサンプリング・
距離減衰・シルエットしきい値によるソフトシルエット生成・
全パラメータのキーフレームアニメーション対応・ビューポートコントローラーポイント・
8 言語対応 UI を備えた映像エフェクトプラグインです。


新機能

1. エフェクト定義(PerspectiveShadowEffect)

PerspectiveShadowEffectVideoEffectBase を継承します。

[VideoEffect] 属性は以下のパラメーターで宣言されます。

  • 表示名:Texts.PerspectiveShadowEffectName(ローカライズキー)
  • カテゴリー:VideoEffectCategories.Decoration
  • 検索タグ:"影(立体投影)""透視投影""3D影""落ち影""影"
  • IsAviUtlSupported = false により AviUtl 向け EXO 出力は非対応です。
  • ResourceType = typeof(Texts) でローカライズリソースを指定します。
    Label プロパティは Texts.PerspectiveShadowEffectName を返却します。

公開プロパティは以下のとおりです。

光源グループ(Texts.PerspectiveShadowGroupLight

  • LightXAnimation、デフォルト 0、内部範囲 VerySmallValueVeryLargeValue):
    [AnimationSlider("F1", "px", -1000d, 1000d)] でスライダー操作範囲 −1000〜1000 px として表示されます。
    光源の水平位置を指定します。

  • LightYAnimation、デフォルト -500、内部範囲 VerySmallValueVeryLargeValue):
    [AnimationSlider("F1", "px", -1000d, 1000d)] でスライダー操作範囲 −1000〜1000 px として表示されます。
    光源の垂直位置を指定します。

  • LightHeightAnimation、デフォルト 2000、内部範囲 1VeryLargeValue):
    [AnimationSlider("F1", "px", 50d, 2000d)] でスライダー操作範囲 50〜2000 px として表示されます。
    光源の仮想的な高さを指定します。小さいほど影が長く伸びます。

  • GroundYAnimation、デフォルト 0、内部範囲 VerySmallValueVeryLargeValue):
    [AnimationSlider("F1", "px", -1000d, 1000d)] でスライダー操作範囲 −1000〜1000 px として表示されます。
    影が落ちる接地面の Y 位置を素材下端からのオフセット (px) で指定します。
    影の見え方グループ(Texts.PerspectiveShadowGroupAppearance

  • OpacityAnimation、デフォルト 75、内部範囲 0100):
    [AnimationSlider("F1", "%", 0d, 100d)] でスライダー操作範囲 0〜100 % として表示されます。
    影全体の不透明度を指定します。

  • FalloffAnimation、デフォルト 40、内部範囲 0100):
    [AnimationSlider("F1", "%", 0d, 100d)] でスライダー操作範囲 0〜100 % として表示されます。
    影の先端に向かって薄くなる強さを指定します。

  • BlurRadiusAnimation、デフォルト 2、内部範囲 0256):
    [AnimationSlider("F1", "px", 0d, 32d)] でスライダー操作範囲 0〜32 px として表示されます。
    影のソフトエッジ基本半径を指定します。

  • SpreadAnimation、デフォルト 0.30、内部範囲 01):
    [AnimationSlider("F2", "", 0d, 1d)] でスライダー操作範囲 0〜1 として表示されます。
    距離に応じてブラーエッジが拡散する強さを指定します。

  • AlphaThresholdAnimation、デフォルト 0.05、内部範囲 01):
    [AnimationSlider("F2", "", 0d, 1d)] でスライダー操作範囲 0〜1 として表示されます。
    シルエットとして扱う不透明度のしきい値を指定します。

  • ShadowColorColor、デフォルト Color.FromArgb(255, 0, 0, 0)):
    [ColorPicker] でカラーピッカーとして表示されます。デフォルト値は不透明黒(A=255, R=0, G=0, B=0)です。
    プロパティ変更時は Set メソッドで変更通知が発行されます。
    CreateExoVideoFilters は空の IEnumerable<string> を返却し、EXO 向けフィルター出力は実装されていません。

CreateVideoEffect(IGraphicsDevicesAndContext devices)new PerspectiveShadowEffectProcessor(devices, this) を返却します。

GetAnimatablesLightXLightYLightHeightGroundYOpacityFalloffBlurRadiusSpreadAlphaThreshold の 9 アニメーションプロパティを返却します。
ShadowColorColor 型であり、GetAnimatables に含まれずキーフレームアニメーションの対象外です。


2. エフェクトプロセッサー(PerspectiveShadowEffectProcessor)

PerspectiveShadowEffectProcessorVideoEffectProcessorBase を継承し、
コンストラクターで IGraphicsDevicesAndContext devicesPerspectiveShadowEffect item を受け取ります。

内部フィールドとして effectPerspectiveShadowCustomEffect?)・isFirstbool)・
lightXlightYlightHeightgroundYopacityfalloffblurRadiusspreadalphaThreshold(各 double)・
shadowColorColor)を保持します。

CreateEffect メソッド

CreateEffect(IGraphicsDevicesAndContext devices) は以下の手順でエフェクトを構築します。

  1. new PerspectiveShadowCustomEffect(devices) でシェーダーエフェクトを生成します。
  2. IsEnabledfalse の場合は Dispose() して null を返却します。
  3. 有効と判断した場合は disposer.Collect(effect) で破棄管理に登録します。
  4. effect.Output を取得・disposer.Collect 後、これを戻り値として返却します。
    本エフェクトはシングルパス構成です。トライトーンのような複数エフェクトチェーン(Composite / Blend / CrossFade)は持たず、シェーダー単体で影の生成・合成をすべて完結させます。

setInput / ClearEffectChain メソッド

setInput(ID2D1Image? input)effect?.SetInput(0, input, true) でスロット 0 に入力を設定します。

ClearEffectChaineffect?.SetInput(0, null, true) でスロット 0 の入力を解除します。

Update メソッド

Update(EffectDescription effectDescription) は毎フレーム呼び出されます。

IsPassThroughEffecttrue、または effectnull の場合は effectDescription.DrawDescription をそのまま返却します。

effectDescription から frameItemPosition.Frame)・lengthItemDuration.Frame)・fpsFPS)を取得し、各アニメーション値を評価します。

  • lightXitem.LightX.GetValue(frame, length, fps)

  • lightYitem.LightY.GetValue(frame, length, fps)

  • lightHeightitem.LightHeight.GetValue(frame, length, fps)

  • groundYitem.GroundY.GetValue(frame, length, fps)

  • opacityitem.Opacity.GetValue(frame, length, fps)

  • falloffitem.Falloff.GetValue(frame, length, fps)

  • blurRadiusitem.BlurRadius.GetValue(frame, length, fps)

  • spreaditem.Spread.GetValue(frame, length, fps)

  • alphaThresholditem.AlphaThreshold.GetValue(frame, length, fps)

  • shadowColoritem.ShadowColor を直接参照します。
    isFirsttrue の場合、または前フレームからの差分がある場合に限り、以下の更新を行います。

  • effect.LightX = (float)lightX

  • effect.LightY = (float)lightY

  • effect.LightHeight = (float)lightHeight

  • effect.GroundY = (float)groundY

  • effect.Opacity = (float)opacity / 100f(0〜100 → 0〜1 正規化)

  • effect.Falloff = (float)falloff / 100f(0〜100 → 0〜1 正規化)

  • effect.BlurRadius = (float)blurRadius

  • effect.Spread = (float)spread

  • effect.AlphaThreshold = (float)alphaThreshold

  • effect.ShadowColor = new Vector4(shadowColor.R / 255f, shadowColor.G / 255f, shadowColor.B / 255f, shadowColor.A / 255f)
    更新後は isFirst = false をセットし、全キャッシュフィールドを現在値で上書きします。

ビューポートコントローラーポイント

Update の最後に VideoEffectController を生成し、DrawDescriptionControllers に追加して返却します。
コントローラーポイントは 1 点で、座標は (lightX, lightY, 0f) です。
ドラッグ操作時のデルタは item.LightX.AddToEachValues(arg.Delta.X) および item.LightY.AddToEachValues(arg.Delta.Y) として全キーフレームに一括適用されます。


3. カスタムシェーダーエフェクト(PerspectiveShadowCustomEffect)

PerspectiveShadowCustomEffectD2D1CustomShaderEffectBase を継承します。
コンストラクターは Create<EffectImpl>(devices) を呼び出します。

公開プロパティとして LightXLightYLightHeightGroundYOpacityFalloff
BlurRadiusSpreadShadowColorVector4)・AlphaThreshold の 10 個のプロパティを持ちます。
float 型プロパティは SetValue((int)EffectImpl.Properties.XXX, value) / GetFloatValue(...) で、
Vector4 型の ShadowColorSetValue((int)EffectImpl.Properties.ShadowColor, value) / GetVector4Value(...) で Direct2D プロパティシステムと通信します。

EffectImpl 内部クラス

EffectImplD2D1CustomShaderEffectImplBase<EffectImpl> を継承し、
[CustomEffect(1)] 属性により入力テクスチャ 1 枚のカスタムエフェクトとして宣言されます。

ConstantBuffer 構造体

[StructLayout(LayoutKind.Sequential)] で宣言された ConstantBuffer 構造体は
12 個の float フィールドおよび 1 個の Vector4 フィールドを以下の順序で保持します。

フィールド レジスター 役割
LightX c0.x 光源の水平位置
LightY c0.y 光源の垂直位置
LightHeight c0.z 光源の仮想高さ
GroundY c0.w 解決済み接地線 Y 座標(絶対座標)
Opacity c1.x 影の不透明度(0〜1)
Falloff c1.y 距離減衰強度(0〜1)
BlurRadius c1.z ぼかし基本半径
Spread c1.w 距離連動ブラー拡散率
ShadowColor c2.xyzw 影の色(RGBA、0〜1 正規化)
AlphaThreshold c3.x シルエットしきい値(0〜1)
Pad0 c3.y パディング
Pad1 c3.z パディング
Pad2 c3.w パディング

この配置は HLSL 定数バッファーの packoffset 宣言と厳密に対応します。

Properties 列挙型

Properties 列挙型はパディングを除く 10 フィールドを int 値 0〜9 でマッピングします。

列挙値
LightX 0
LightY 1
LightHeight 2
GroundY 3
Opacity 4
Falloff 5
BlurRadius 6
Spread 7
ShadowColor 8
AlphaThreshold 9

各プロパティは [CustomEffectProperty(PropertyType.Float, (int)Properties.XXX)] で修飾されます。
ShadowColor のみ PropertyType.Vector4 で宣言されます。

入力値のクランプ

C# 側のセッターは以下のクランプ処理を施します。

  • LightHeightMath.Max(value, 1f)(最小 1 px)
  • OpacityMath.Clamp(value, 0f, 1f)
  • FalloffMath.Clamp(value, 0f, 1f)
  • BlurRadiusMath.Max(value, 0f)
  • SpreadMath.Clamp(value, 0f, 1f)
  • AlphaThresholdMath.Clamp(value, 0f, 1f)
    LightXLightYGroundYShadowColor はクランプなしで直接設定されます。

コンストラクター

base(ShaderResourceUri.Get("PerspectiveShadow")) を呼び出します。
これによりコンパイル済みシェーダーオブジェクト(.cso)が WPF リソースとして読み込まれます。

UpdateConstants

drawInformation?.SetPixelShaderConstantBuffer(_cb) を呼び出して
ConstantBuffer 構造体をピクセルシェーダーの定数バッファーレジスター b0 へ転送します。

MapInputRectsToOutputRect(出力矩形計算)

本エフェクトは影の投影先を包含するよう出力矩形を動的に拡張します。処理は以下の手順で行われます。

  1. _resolvedGroundY = inputRect.Bottom + _groundYOffset として接地線の絶対座標を確定し、_cb.GroundY を更新します。
  2. 素材の四隅(左上・右上・左下・右下)それぞれについて ProjectToGround で影の投影先座標を計算し、
    その座標の最小・最大値を累積して出力矩形の候補範囲を求めます。
  3. 各コーナーで shadowDistspread から ComputeDynamicBlur を評価し、最大のブラー量 maxDynBlur を求めます。
  4. blurMargin = ceil(maxDynBlur) + 2 を余白として四辺に加算します。
  5. 出力矩形は上記の拡張範囲を素材矩形から最大 4096 px の範囲に制限して確定します。
    ProjectToGround は以下の射影計算を行います。
t = min(H / max(H - h, ε), 10.0)   ただし h = max(0, G - Q.Y)
Sx = lightX + (Q.X - lightX) × t
Sy = lightY + (G - lightY) × t

t は最大 10.0 にクランプされ、影が無限遠に伸びることを防ぎます。

ComputeDynamicBlur は以下の式でブラー量を算出します。

expansion = min(shadowDist / max(lightHeight, 1) × spread, 3.0)
dynamicBlur = blurRadius × (1 + expansion)

MapOutputRectToInputRects(入力矩形計算)

MapOutputRectToInputRects は出力矩形の境界上の点群を UnprojectFromShadow で逆変換し、
必要な入力矩形を計算します。処理は以下の手順で行われます。

  1. 出力矩形の境界を 4 × 4Subdivisions = 4)のグリッドに分割した境界上の点(内部点を除く辺上の点のみ)を列挙します。
  2. 各点を UnprojectFromShadow で逆変換し、有効な投影元座標 Q を得ます。
  3. Q の位置に ComputeDynamicBlur のブラー余白を加えた範囲で入力矩形を拡張します。
  4. 光源座標が出力矩形内に含まれる場合は、入力矩形全体を素材の境界まで拡張します(光源が領域内にある場合、任意の座標が素材全体を参照しうるため)。
  5. 最終的な入力矩形に ±2 px のマージンを付加して返却します。

4. HLSL ピクセルシェーダー(PerspectiveShadow.hlsl)

シェーダーモデル ps_5_0・エントリーポイント main でコンパイルされます。

リソース宣言

  • Texture2D InputTexture : register(t0):入力テクスチャ(スロット 0)
  • SamplerState InputSampler : register(s0):サンプラー(スロット 0)

定数バッファー宣言

cbuffer Constants : register(b0) に 10 個の有効値と 3 個のパディングを 4 float4 レジスターに格納します。

レジスター フィールド
c0.x / c0.y / c0.z / c0.w lightX / lightY / lightHeight / groundY
c1.x / c1.y / c1.z / c1.w opacity / falloff / blurRadius / spread
c2.xyzw shadowColor(float4)
c3.x / c3.y / c3.z / c3.w alphaThreshold / pad0 / pad1 / pad2

C# 側の ConstantBuffer 構造体の LayoutKind.Sequential 配置はこの packoffset 宣言と厳密に一致します。

定数宣言

定数名 用途
EPSILON 1e-4f ゼロ除算防止の微小値
BLUR_THRESHOLD 0.5f 単一サンプリングと多サンプリングの切り替え境界
GOLDEN_ANGLE 2.39996323f 黄金角(ラジアン、約 137.508°)
GAUSS_FALLOFF 2.0f ガウス重み係数(exp(-GAUSS_FALLOFF × r²)
MIN_WEIGHT_SUM 1e-3f 重み総和の最小値(正規化時のゼロ除算防止)
DISTANCE_ATTENUATION 0.005f 距離減衰の線形係数
MAX_SPREAD_FACTOR 3.0f 動的ブラー拡張率の上限
MIN_BLUR_SAMPLES 16 最小サンプル数
MAX_BLUR_SAMPLES 128 最大サンプル数

ヘルパー関数群

UnprojectFromShadow(float2 S, out bool valid)

出力ピクセル座標 S から、S の影を落としている投影元座標 Q を逆算します。

denom = groundY - lightY
m = (S.y - lightY) / denom
invM = 1 / m
Q.x = lightX + (S.x - lightX) × invM
Q.y = groundY - lightHeight × (1 - invM)

abs(denom) < EPSILON の場合、m < 1.0 の場合、または Q.y > groundY の場合は valid = false を返します。
これらの条件は「S が影として有効な位置に存在しない」ことを意味します。

SampleInput(float2 uv)

UV 座標が [0, 1] の範囲外の場合は float4(0, 0, 0, 0) を返し、
範囲内の場合は InputTexture.SampleLevel(InputSampler, uv, 0) でミップレベル 0 を明示サンプリングします。
これにより出力矩形外へのサンプリングが安全に処理されます。

SmoothAlphaThreshold(float alpha)

alphaThreshold < EPSILON の場合はそのまま alpha を返します。
それ以外の場合は smoothstep(threshold × 0.5, threshold × 1.5, alpha)alpha に乗算して返します。
これにより、しきい値付近の半透明ピクセルが滑らかに影の輪郭から除去されます。

main 関数

入力セマンティクスは SV_POSITIONpos)・SCENE_POSITIONposScene)・TEXCOORD0uv0)です。
posScene.xy が出力ピクセルの絶対シーン座標 P として使用されます。
uv0.xy が入力テクスチャのベース UV 座標、uv0.zw が 1 ピクセルあたりの UV 変換スケール pxToUV として使用されます。

処理フロー

  1. SampleInput(uv0.xy) で元映像ピクセル original を取得します。
  2. UnprojectFromShadow(P, valid) で投影元座標 Q を逆算します。valid = false の場合、shadow = float4(0,0,0,0) として最終合成へ進みます。
  3. valid = true の場合、以下の処理を行います。
    • shadowDist = length(P - Q) で影の投影距離を算出します。
    • Quv = uv0.xy + (Q - P) × pxToUV で Q の UV 座標を算出します。
    • dynamicBlur = blurRadius × (1 + min(shadowDist / max(lightHeight, 1) × spread, MAX_SPREAD_FACTOR)) で動的ブラーを算出します。
  4. ブランチによるサンプリング切り替え
    • dynamicBlur ≤ BLUR_THRESHOLD (0.5) の場合:SampleInput(Quv) で単一サンプリングし SmoothAlphaThreshold(s.a) を適用します。
    • dynamicBlur > BLUR_THRESHOLD の場合:黄金角スパイラルサンプリングを行います。
      サンプル数 numSamples = clamp(int(dynamicBlur × 2), 16, 128)
      インデックス i について、正規化半径 r = sqrt((i + 0.5) / numSamples)
      角度 θ = i × GOLDEN_ANGLE(ラジアン)で、
      オフセット off = (cos θ, sin θ) × r × dynamicBlur を計算します。
      各サンプル UV sUV = Quv + off × pxToUV についてガウス重み gauss = exp(-2r²) を乗算した加重平均を求め、
      acc / max(weightSum, MIN_WEIGHT_SUM) で正規化します。
  5. decay = lerp(1.0, 1.0 / (1.0 + shadowDist × 0.005), saturate(falloff)) で距離減衰係数を計算します。
  6. finalAlpha = alpha × opacity × decay × shadowColor.a で最終アルファ値を算出します。
  7. shadow = float4(shadowColor.rgb × finalAlpha, finalAlpha) でプリマルチプライ済み影ピクセルを生成します。
  8. src-over 合成:元映像の透明部分にのみ影を描画します。
    result.rgb = saturate(original.rgb + shadow.rgb × (1 - original.a))
    result.a   = saturate(original.a   + shadow.a   × (1 - original.a))
    

5. ローカライズ(Texts)

Texts クラスは [AutoGenLocalizer] 属性を持つ partial クラスとして宣言されます。
YukkuriMovieMaker.Generator のソースジェネレーターが Texts.csv を処理し、
各ロケールのリソースファイルを自動生成します。

対応言語:日本語 (ja-jp)・英語 (en-us)・中国語簡体字 (zh-cn)・中国語繁体字 (zh-tw)・
韓国語 (ko-kr)・スペイン語 (es-es)・アラビア語 (ar-sa)・インドネシア語 (id-id)

ローカライズキーの一覧は以下のとおりです(日本語訳)。

キー ja-jp
PerspectiveShadowEffectName 影(立体投影)
TagPerspectiveShadow 影(立体投影)
TagPerspective 透視投影
TagShadow3D 3D影
TagCastShadow 落ち影
TagShadow
PerspectiveShadowGroupLight 光源
PerspectiveShadowGroupAppearance 影の見え方
PerspectiveShadowLightXName 光源X
PerspectiveShadowLightXDesc 光源の水平位置 (px)
PerspectiveShadowLightYName 光源Y
PerspectiveShadowLightYDesc 光源の垂直位置 (px)
PerspectiveShadowLightHeightName 光源の高さ
PerspectiveShadowLightHeightDesc 光源の仮想的な高さ。小さいほど影が長く伸びます。
PerspectiveShadowGroundYName 接地線Y
PerspectiveShadowGroundYDesc 影が落ちる地面の位置。素材下端を基準としたオフセット (px)。
PerspectiveShadowOpacityName 不透明度
PerspectiveShadowOpacityDesc 影全体の不透明度。
PerspectiveShadowFalloffName 距離減衰
PerspectiveShadowFalloffDesc 影の先端に向かって薄くなる強さ。
PerspectiveShadowBlurName ぼかし半径
PerspectiveShadowBlurDesc 影のソフトエッジ半径 (px)。
PerspectiveShadowSpreadName 拡散
PerspectiveShadowSpreadDesc 距離が大きいほど影のエッジが拡散する強さ。
PerspectiveShadowAlphaThresholdName シルエットしきい値
PerspectiveShadowAlphaThresholdDesc シルエットとして扱う不透明度のしきい値。
PerspectiveShadowShadowColorName 影の色
PerspectiveShadowShadowColorDesc 影の色。

合計 8 言語 × 28 キーのローカライズエントリーが提供されます。