Commands/Shaders/Get-OBS3dPanelShader.ps1

function Get-OBS3dPanelShader {

[Alias('Set-OBS3dPanelShader','Add-OBS3dPanelShader')]
param(
# Set the credits of OBS3dPanelShader
[ComponentModel.DefaultBindingProperty('credits')]
[String]
$Credits,
# Set the scale of OBS3dPanelShader
[ComponentModel.DefaultBindingProperty('scale')]
[Single]
$Scale,
# Set the tilt_x_deg of OBS3dPanelShader
[Alias('tilt_x_deg')]
[ComponentModel.DefaultBindingProperty('tilt_x_deg')]
[Single]
$TiltXDeg,
# Set the tilt_y_deg of OBS3dPanelShader
[Alias('tilt_y_deg')]
[ComponentModel.DefaultBindingProperty('tilt_y_deg')]
[Single]
$TiltYDeg,
# Set the tilt_z_deg of OBS3dPanelShader
[Alias('tilt_z_deg')]
[ComponentModel.DefaultBindingProperty('tilt_z_deg')]
[Single]
$TiltZDeg,
# Set the pos_x of OBS3dPanelShader
[Alias('pos_x')]
[ComponentModel.DefaultBindingProperty('pos_x')]
[Single]
$PosX,
# Set the pos_y of OBS3dPanelShader
[Alias('pos_y')]
[ComponentModel.DefaultBindingProperty('pos_y')]
[Single]
$PosY,
# Set the thickness of OBS3dPanelShader
[ComponentModel.DefaultBindingProperty('thickness')]
[Single]
$Thickness,
# Set the radius_fb of OBS3dPanelShader
[Alias('radius_fb')]
[ComponentModel.DefaultBindingProperty('radius_fb')]
[Single]
$RadiusFb,
# Set the brightness of OBS3dPanelShader
[ComponentModel.DefaultBindingProperty('brightness')]
[Single]
$Brightness,
# Set the light_position of OBS3dPanelShader
[Alias('light_position')]
[ComponentModel.DefaultBindingProperty('light_position')]
[Int32]
$LightPosition,
# Set the wiggle of OBS3dPanelShader
[ComponentModel.DefaultBindingProperty('wiggle')]
[Single]
$Wiggle,
# Set the wiggle_rot of OBS3dPanelShader
[Alias('wiggle_rot')]
[ComponentModel.DefaultBindingProperty('wiggle_rot')]
[Management.Automation.SwitchParameter]
$WiggleRot,
# The name of the source. This must be provided when adding an item for the first time
[Parameter(ValueFromPipelineByPropertyName)]
[Alias('SceneItemName')]
[String]
$SourceName,
# The name of the filter. If this is not provided, this will default to the shader name.
[Parameter(ValueFromPipelineByPropertyName)]
[String]
$FilterName,
# The inline value of the shader. This will normally be provided as a default parameter, based off of the name.
[Alias('ShaderContent')]
[String]
$ShaderText,
# If set, will force the recreation of a shader that already exists
[Management.Automation.SwitchParameter]
$Force,
# If set, will pass thru the commands that would be sent to OBS (these can be sent at any time with Send-OBS)
[Management.Automation.SwitchParameter]
$PassThru,
# If set, will not wait for a response from OBS (this will be faster, but will not return anything)
[Management.Automation.SwitchParameter]
$NoResponse,
# If set, use the shader elapsed time, instead of the OBS system elapsed time
[ComponentModel.DefaultBindingProperty('use_shader_elapsed_time')]
[Management.Automation.SwitchParameter]
$UseShaderTime
)


process {
$shaderName = '3d-panel'
$ShaderNoun = 'OBS3dPanelShader'
if (-not $psBoundParameters['ShaderText']) {    
    $psBoundParameters['ShaderText'] = $ShaderText = '
//based on https://x.com/HoraiChan/status/1986268258883010766

uniform string credits<
    string widget_type = "info";
> = "Based on effect by <a href=''https://twitter.com/HoraiChan''>Horaiken</a>";

uniform float scale<
    string label = "大きさ / Scale";
    string widget_type = "slider";
    float minimum = 0.25;
    float maximum = 3.00;
    float step = 0.001;
> = 1.0;
uniform float tilt_x_deg<
    string label = "縦方向の傾き(X) / Tilt (X)";
    string widget_type = "slider";
    float minimum = -360.0;
    float maximum = 360.00;
    float step = 0.1;
> = 20.0;
uniform float tilt_y_deg<
    string label = "横方向の傾き(Y) / Tilt (Y)";
    string widget_type = "slider";
    float minimum = -360.0;
    float maximum = 360.00;
    float step = 0.1;
> = 35.0;
uniform float tilt_z_deg<
    string label = "回転(Z) / Roll (Z)";
    string widget_type = "slider";
    float minimum = -360.0;
    float maximum = 360.00;
    float step = 0.1;
> = 0.0;
uniform float pos_x<
    string label = "横位置 / Horizontal Position";
    string widget_type = "slider";
    float minimum = -1.00;
    float maximum = 1.00;
    float step = 0.0001;
> = 0.0;
uniform float pos_y<
    string label = "縦位置 / Vertical Position";
    string widget_type = "slider";
    float minimum = -1.00;
    float maximum = 1.00;
    float step = 0.0001;
> = 0.0;
uniform float thickness<
    string label = "厚み / Thickness";
    string widget_type = "slider";
    float minimum = 0.00;
    float maximum = 0.1;
    float step = 0.001;
> = 0.03;
uniform float radius_fb<
    string label = "角丸 / Corner Radius";
    string widget_type = "slider";
    float minimum = 0.00;
    float maximum = 1.00;
    float step = 0.01;
> = 0.2;
uniform float brightness<
    string label = "明るさ / Brightness";
    string widget_type = "slider";
    float minimum = 0.00;
    float maximum = 2.00;
    float step = 0.01;
> = 1.2;
uniform int light_position <
  string label = "照明の位置 / Light Direction";
  string widget_type = "select";
  int option_0_value = 0;
  string option_0_label = "左側に光 / Light From Left";
  int option_1_value = 1;
  string option_1_label = "右側に光 / Light From Right";
> = 0;
uniform float wiggle <
    string label = "ゆらゆら / Wiggle";
    string widget_type = "slider";
    float minimum = 0.00;
    float maximum = 2.50;
    float step = 0.01;
> = 0.0;
uniform bool wiggle_rot <
    string label = "角度もゆらゆら / Wiggle Rotation";
>;

float hash1(float n){ return frac(sin(n)*43758.5453123); }

float noise1D(float x) {
    float i = floor(x);
    float f = frac(x);
    float u = f*f*(3.0 - 2.0*f);
    return lerp(hash1(i), hash1(i+1.0), u); // 0..1
}

float fbm1D(float x) {
    float v = 0.0;
    float a = 0.5;
    float f = 1.0;
    for(int k=0;k<4;k++){
        v += a * noise1D(x * f);
        f *= 2.0;
        a *= 0.5;
    }
    return v;
}

float saturate(float x) { return clamp(x, 0.0, 1.0); }

float3 rotateX(float3 p, float a){ float c=cos(a), s=sin(a); return float3(p.x, c*p.y - s*p.z, s*p.y + c*p.z); }
float3 rotateY(float3 p, float a){ float c=cos(a), s=sin(a); return float3( c*p.x + s*p.z, p.y, -s*p.x + c*p.z); }
float3 rotateZ(float3 p, float a){ float c=cos(a), s=sin(a); return float3(c*p.x - s*p.y, s*p.x + c*p.y, p.z); }

// 2D 角丸長方形 SDF(中心、半径 bxy, 角丸 r)
float sdRoundRect2D(float2 p, float2 bxy, float r) {
    float2 q = abs(p) - bxy + r;
    return length(max(q, 0.0)) + min(max(q.x, q.y), 0.0) - r;
}

// 正面シルエット角丸 + Z方向に押し出し
float sdFrontViewRoundedPrism(float3 p, float3 b, float r_fb_norm) {
    float r_fb = saturate(r_fb_norm) * (0.999 * min(b.x, b.y));
    float a = sdRoundRect2D(p.xy, b.xy, r_fb);
    float dz = abs(p.z) - b.z;
    return max(a, dz);
}

// 法線
float3 calcNormal(float3 p, float3 b, float rfb) {
    const float e = 0.001;
    float3 ex=float3(e,0,0), ey=float3(0,e,0), ez=float3(0,0,e);
    float dx = sdFrontViewRoundedPrism(p+ex,b,rfb) - sdFrontViewRoundedPrism(p-ex,b,rfb);
    float dy = sdFrontViewRoundedPrism(p+ey,b,rfb) - sdFrontViewRoundedPrism(p-ey,b,rfb);
    float dz = sdFrontViewRoundedPrism(p+ez,b,rfb) - sdFrontViewRoundedPrism(p-ez,b,rfb);
    return normalize(float3(dx,dy,dz));
}

// 照明
float3 shade(float3 n, float3 v) {
    float3 l;
    if (light_position == 0) { // 左から光
        l = normalize(float3(-1.0, -0.1, 1.0));
    }
    else { // 右から光
        l = normalize(float3( 1.0, -0.1, 1.0));
    }
    float diff = saturate(dot(n,l));
    float rim = pow(1.0 - saturate(dot(n,v)), 2.0);
    float li = 0.25 + 0.75*diff + 0.08*rim;
    return float3(li, li, li);
}

float4 mainImage(VertData v_in) : TARGET {
    float2 uv = v_in.uv;

    // 画面座標(短辺基準)
    float aspect = uv_size.x / uv_size.y;
    float2 ndc = uv * 2.0 - 1.0;
    ndc += float2(pos_x, pos_y) * -1.0 * (scale + 1.0);
    float2 p2 = ndc;
    p2.x *= aspect;

    // カメラ設定
    float3 ro = float3(0.0, 0.0, 3.2);
    float3 rd = normalize(float3(p2, -4.0));

    // 回転(Z→Y→X の順に逆回転)
    float ax=radians(tilt_x_deg), ay=radians(tilt_y_deg), az=radians(tilt_z_deg);
    ro = rotateX(rotateY(rotateZ(ro, -az), -ay), -ax);
    rd = normalize(rotateX(rotateY(rotateZ(rd, -az), -ay), -ax));

    // 画面フィット(短辺基準)+ 厚み
    float2 baseXY;
    if (aspect > 1.0) {
        baseXY = float2(1.0, 1.0 / aspect);
    } else {
        const float portraitMargin = 0.6;
        baseXY = float2(aspect * portraitMargin, 1.0 * portraitMargin);
    }
    float3 b = float3(baseXY, thickness) * max(scale, 0.0001);

    // Wiggle
    float diag = length(2.0 * b);
    float amp = 0.05 * wiggle * diag;
    const float WSPD = 0.1;
    
    // 各軸に独立ノイズ
    float wx = (fbm1D(elapsed_time*WSPD + 13.37) * 2.0 - 1.0) * amp;
    float wy = (fbm1D(elapsed_time*WSPD + 47.11) * 2.0 - 1.0) * amp;
    float wz = (fbm1D(elapsed_time*WSPD + 91.73) * 2.0 - 1.0) * amp * 0.35;
    float3 woff = float3(wx, wy, wz);

    float rotAmp = radians(12.0) * wiggle;
    
    float wobX = (fbm1D(elapsed_time*WSPD + 128.31) * 2.0 - 1.0) * rotAmp;
    float wobY = (fbm1D(elapsed_time*WSPD + 299.91) * 2.0 - 1.0) * rotAmp;
    
    float3 ro2 = ro;
    float3 rd2 = rd;
    
    if (wiggle_rot) {
        ro2 = rotateX(ro2, wobX);
        ro2 = rotateY(ro2, wobY);
        rd2 = rotateX(rd2, wobX);
        rd2 = rotateY(rd2, wobY);
    }

    float t = 0.0;
    float d = 0.0;
    bool hit = false;
    for (int i=0; i<64; i++) {
        float3 pos = ro2 + rd2 * t;
        d = sdFrontViewRoundedPrism(pos - woff, b, radius_fb);
        if (d < 0.001) { hit = true; break; }
        t += d;
        if (t > 8.0) break;
    }

    // ヒットしなければ完全透明(元ソースは非表示)
    if (!hit) return float4(0.0, 0.0, 0.0, 0.0);

    float3 pos = ro2 + rd2 * t;
    float3 pObj = pos - woff;
    float3 n = calcNormal(pObj, b, radius_fb);
    float3 vdir = normalize(-rd2);

    // テクスチャ貼り付け
    float frontMask = smoothstep(0.5, 0.8, dot(n, float3(0.0, 0.0, 1.0)));
    float2 uvTex = (pObj.xy / b.xy) * 0.5 + 0.5;

    // サンプル(Address Clamp なので側面/背面は端ピクセルが“引き伸ばし”)
    float4 texFront = image.Sample(textureSampler, uvTex);
    float4 texEdge = image.Sample(textureSampler, uvTex);
    float4 tex = lerp(texEdge, texFront, frontMask);

    // フロント面エッジ・ハイライト(細い線)
    float r_fb = saturate(radius_fb) * (0.999 * min(b.x, b.y));
    float a_xy = sdRoundRect2D(pObj.xy, b.xy, r_fb); // XY角丸SDF
    float edgeWidth = 0.02 * min(b.x, b.y);
    float edgeIntensity = 0.6;
    float edgeProx = 1.0 - saturate(abs(a_xy) / edgeWidth);
    float edgeMask = frontMask * edgeProx;
    tex.rgb *= (1.0 + edgeMask * edgeIntensity);

    // 照明
    float3 lightTerm = shade(n, vdir);
    tex.rgb *= lightTerm;

    // 明るさスライダ
    tex.rgb *= brightness;

    // 出力
    return float4(tex.rgb, 1.0);
}

'

}
$MyVerb, $myNoun = $MyInvocation.InvocationName -split '-',2
if (-not $myNoun) {
    $myNoun = $myVerb
    $myVerb = 'Get'    
}
switch -regex ($myVerb) {
    Get {
        $FilterNamePattern = "(?>$(
            if ($FilterName) {
                [Regex]::Escape($FilterName)
            }
            else {
                [Regex]::Escape($ShaderNoun -replace '^OBS' -replace 'Shader$'),[Regex]::Escape($shaderName) -join '|'
            }
        ))"

        if ($SourceName) {
            Get-OBSInput | 
                Where-Object InputName -eq $SourceName |
                Get-OBSSourceFilterList |
                Where-Object FilterName -Match $FilterNamePattern
        } else {
            $obs.Inputs |
                Get-OBSSourceFilterList |
                Where-Object FilterName -Match $FilterNamePattern
        }        
    }
    'Remove' {
        if ($SourceName) {
            Get-OBSInput | 
                Where-Object InputName -eq $SourceName |
                Get-OBSSourceFilterList |
                Where-Object FilterName -Match $FilterNamePattern |
                Remove-OBSSourceFilter
        }
    }
    '(?>Add|Set)' {
        $ShaderSettings = [Ordered]@{}
        :nextParameter foreach ($parameterMetadata in $MyInvocation.MyCommand.Parameters[@($psBoundParameters.Keys)]) {
            foreach ($parameterAttribute in $parameterMetadata.Attributes) {
                if ($parameterAttribute -isnot [ComponentModel.DefaultBindingPropertyAttribute]) { continue }
                $ShaderSettings[$parameterAttribute.Name] = $PSBoundParameters[$parameterMetadata.Name]
                if ($ShaderSettings[$parameterAttribute.Name] -is [switch]) {
                    $ShaderSettings[$parameterAttribute.Name] = $ShaderSettings[$parameterAttribute.Name] -as [bool]
                }
                continue nextParameter
            }            
        }

        if (-not $PSBoundParameters['FilterName']) {
            $filterName = $PSBoundParameters['FilterName'] = $shaderName
        }

        $ShaderFilterSplat = [Ordered]@{
            ShaderSetting = $ShaderSettings
            FilterName = $FilterName
            SourceName = $SourceName
        }        

        foreach ($CarryOnParameter in "PassThru", "NoResponse","Force") {
            if ($PSBoundParameters.ContainsKey($CarryOnParameter)) {
                $ShaderFilterSplat[$CarryOnParameter] = $PSBoundParameters[$CarryOnParameter]
            }
        }

        if (-not $script:CachedShaderFilesFromCommand) {
            $script:CachedShaderFilesFromCommand = @{}
        }

        if ($Home -and -not $script:CachedShaderFilesFromCommand[$shaderName]) {
            $MyObsPowerShellPath = Join-Path $home ".obs-powershell"
            $ThisShaderPath = Join-Path $MyObsPowerShellPath "$shaderName.shader"
            $shaderText | Set-Content -LiteralPath $ThisShaderPath
            $script:CachedShaderFilesFromCommand[$shaderName] = Get-Item -LiteralPath $ThisShaderPath
        }
        if ($script:CachedShaderFilesFromCommand[$shaderName]) {
            $ShaderFilterSplat.ShaderFile = $script:CachedShaderFilesFromCommand[$shaderName].FullName
        } else {
            $ShaderFilterSplat.ShaderText = $shaderText
        }        

        if ($myVerb -eq 'Add') {                        
            Add-OBSShaderFilter @ShaderFilterSplat
        } else {
            Set-OBSShaderFilter @ShaderFilterSplat
        }
    }
}

}


}