自作のShaderEffectをサイズの大きいImageに適用すると処理落ちする

Category: fx wpf_ja

Question

neelabo on Fri, 20 May 2016 09:23:27


.Net 4.5, Windows10 で開発しております。

グレイスケールにする ShaderEffect を作成してImageに適用しているのですが、サイズ(Width,Height)を極端に大きくすると処理落ちが発生します。

標準のエフェクト(BlurEffect)ではこの問題が発生しません。標準エフェクトのように自作エフェクトでも処理落ちしないようにしたいのですが、どのようにしたら良いのでしょうか。

以下は使用しているシェーダーとShaderEffectクラス、症状が再現するサンプル(画像を16000x16000まで拡大するアニメ)になります。


// ビルド前イベンドで以下のコマンドでシェーダーを変換
// "C:\Program Files (x86)\Windows Kits\10\bin\x86\fxc.exe" /T ps_2_0 /E main /Fo "$(ProjectDir)GrayscaleEffect.ps" "$(ProjectDir)GrayscaleEffect.fx"
sampler2D input : register(S0);

float4 main(float2 uv : TEXCOORD) : COLOR
{
	float4 color = tex2D(input, uv);
	float4 gray = color.r * 0.299 + color.g * 0.587 + color.b * 0.114;
	gray.a = color.a;

	return gray;
}

Grayscale.fx

using System;
using System.Windows;
using System.Windows.Media;
using System.Windows.Media.Effects;

namespace ShaderTest2
{
    public class GrayscaleEffect : ShaderEffect
    {
        private static PixelShader _pixelShader = new PixelShader() { UriSource = new Uri(@"pack://application:,,,/ShaderTest2;component/GrayscaleEffect.ps") };

        public GrayscaleEffect()
        {
            PixelShader = _pixelShader;
            UpdateShaderValue(InputProperty);
        }

        public static readonly DependencyProperty InputProperty = ShaderEffect.RegisterPixelShaderSamplerProperty("Input", typeof(GrayscaleEffect), 0);
        public Brush Input
        {
            get { return (Brush)GetValue(InputProperty); }
            set { SetValue(InputProperty, value); }
        }
    }
}

GrayscaleEffect.cs

<Window x:Class="ShaderTest2.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:ShaderTest2"
        mc:Ignorable="d"
        Title="MainWindow" Height="350" Width="525">

    <Window.Resources>
        <Storyboard x:Key="animation" FillBehavior="HoldEnd" RepeatBehavior="Forever">
            <DoubleAnimation
                Storyboard.TargetName="image"
                Storyboard.TargetProperty="Width"
                From="0" To="16000" Duration="0:0:05" />
            <DoubleAnimation
                Storyboard.TargetName="image"
                Storyboard.TargetProperty="Height"
                From="0" To="16000" Duration="0:0:05" />
            <DoubleAnimation
                Storyboard.TargetName="rotate"
                Storyboard.TargetProperty="Angle"
                From="0" To="360" Duration="0:0:05" />
        </Storyboard>
    </Window.Resources>

    <Grid>
        <Image x:Name="image" Source="sample.jpg" HorizontalAlignment="Center" VerticalAlignment="Center">
            <Image.LayoutTransform>
                <RotateTransform x:Name="rotate"/>
            </Image.LayoutTransform>
            <Image.Triggers>
                <EventTrigger RoutedEvent="Loaded">
                    <BeginStoryboard Storyboard="{Binding Source={StaticResource animation}}" />
                </EventTrigger>
            </Image.Triggers>
            <Image.Effect>
                <!-- サイズが大きくなると重くなる問題 -->
                <local:GrayscaleEffect/>
                <!-- 標準エフェクトなら重くならない -->
                <!--<BlurEffect/>--> 
            </Image.Effect>
        </Image>
    </Grid>
</Window>

MainWindow.xaml

追記:

sample.jpg のサイズは 1024x1024(24bit)です。

動作環境:

  • Windows10 HOME 64bit
  • AMD Phenom X6 1065T
  • AMD Radeon HD 6670
  • Memory 8GB
  • VisualStudio 2015 Update2
  • .Net 4.5


Replies

sygh on Sat, 21 May 2016 14:57:37


質問するときは実行ハードウェア環境に関しても詳しく記載しましょう。特にパフォーマンス関連の問題は、環境によっては発生しないこともあります。また、コード中で"sample.jpg"という画像が使われていますが、画像のサイズは具体的にどの程度ですか?

とりあえず今回の問題に関しては、手元にあるWindows 8.1 x64, Core i7-4770K, GeForce GTX 760, Visual Studio 2015 Update 2の環境で、512x512x24bppのPNG画像を使った場合でも再現しました。

下記のようにエフェクトをImageではなくGrid(親パネル)側に適用してみてはどうでしょうか。

    <Grid>
        <Grid.Effect>
            <local:GrayscaleEffect/>
        </Grid.Effect>
        ……
    </Grid>

Image側にエフェクトを直接適用すると、Image要素を拡大したときに実際に画面表示されない部分にもピクセルシェーダーが走るので重くなるのだと思われます。

WPF組み込みのBlurEffectに関しては、もしかしたら表示領域のみにシェーダー適用範囲を制限するように最適化された仕組みが実装されているのかもしれません。

neelabo on Sat, 21 May 2016 20:57:09


ご指摘、ご回答ありがとうございます。画像サイズと動作環境を質問に追記しました

GridでEffectをかけたとこと、問題が発生しなくなることを確認しました。ありがとうござます。

実際に運用している環境ではCanvasを挟んでおり、この場合は親のGridでEffectをかけている場合でも問題が発生してしまいます。この場合はどのような解決方法がありますでしょうか。

以下再現サンプルになります。

<Window x:Class="ShaderTest2.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:ShaderTest2"
        mc:Ignorable="d"
        Title="MainWindow" Height="350" Width="525">

    <Window.Resources>
        <Storyboard x:Key="animation" FillBehavior="HoldEnd" RepeatBehavior="Forever">
            <DoubleAnimation
                Storyboard.TargetName="image"
                Storyboard.TargetProperty="Width"
                From="0" To="16000" Duration="0:0:05" />
            <DoubleAnimation
                Storyboard.TargetName="image"
                Storyboard.TargetProperty="Height"
                From="0" To="16000" Duration="0:0:05" />
            <DoubleAnimation
                Storyboard.TargetName="image"
                Storyboard.TargetProperty="(Canvas.Left)"
                From="0" To="-8000" Duration="0:0:05" />
            <DoubleAnimation
                Storyboard.TargetName="image"
                Storyboard.TargetProperty="(Canvas.Top)"
                From="0" To="-8000" Duration="0:0:05" />
            <DoubleAnimation
                Storyboard.TargetName="rotate"
                Storyboard.TargetProperty="Angle"
                From="0" To="360" Duration="0:0:05" />
        </Storyboard>
    </Window.Resources>

    <Grid>
        <Grid.Effect>
            <local:GrayscaleEffect/>
        </Grid.Effect>

        <Canvas HorizontalAlignment="Center" VerticalAlignment="Center">
            <Canvas.LayoutTransform>
                <RotateTransform x:Name="rotate"/>
            </Canvas.LayoutTransform>
            <Canvas.Triggers>
                <EventTrigger RoutedEvent="Loaded">
                    <BeginStoryboard Storyboard="{Binding Source={StaticResource animation}}" />
                </EventTrigger>
            </Canvas.Triggers>

            <Image x:Name="image" Source="sample.jpg" />

        </Canvas>
    </Grid>
</Window>

sygh on Sun, 22 May 2016 10:24:14


おそらく解決策は複数存在すると思うのですが、簡単なものとしてクリッピング用のパネルを1段挟んでやる方法があります。

    <Grid>
        <Grid.Effect>
            <local:GrayscaleEffect/>
        </Grid.Effect>
        <Grid ClipToBounds="True">
            <Canvas HorizontalAlignment="Center" VerticalAlignment="Center">
            ……
            </Canvas>
        </Grid>
    </Grid>
WPFの内部実装をイメージしながらレイアウトを組むと、パフォーマンス上の問題を解決しやすくなるのではないかと思います。

neelabo on Mon, 23 May 2016 12:07:16


ご提示いただいた方法で解決しました!ありがとうございます。