使用 Viewport3D 在 WPF 中绘制 2D 彩色网格图 - 提高性能的技巧?

2D color mesh plot in WPF using Viewport3D - tricks to improve performance?

我正在 WPF 应用程序中创建彩色网格图,请参阅下面的简化示例项目。

因为我希望有可能将它扩展到 3D,所以我目前使用 Viewport3D 作为基础。但是现在我注意到它变得很慢并且在增加色点数量时会占用大量内存。

适用于 500x500 点,适用于 1000x1000,但需要能够处理更多。

非常感谢有关如何在 WPF 中提高性能或使用替代工具(包括 2D)执行此操作以提高性能的建议!

我的简化示例的三个文件包含在下面。

MainWindow.xaml:

<Window.DataContext>
    <local:MainViewModel />
</Window.DataContext>

<Grid>
    <Grid.RowDefinitions>
        <RowDefinition Height="Auto" />
        <RowDefinition Height="*" />
    </Grid.RowDefinitions>

    <!--#region Settings-->
    <GroupBox Header="Settings" >
        <WrapPanel Orientation="Horizontal">
            <WrapPanel Margin="10,0">
                <TextBlock Text="Number of Horizontal Positions:" />
                <TextBox Text="{Binding NoOfHorizontalPoints, UpdateSourceTrigger=LostFocus}" Width="60" Margin="5,0,10,0"/>
            </WrapPanel>

            <WrapPanel Margin="10,0" >
                <TextBlock Text="Number of Vertical Positions:" />
                <TextBox Text="{Binding NoOfVerticalPoints, UpdateSourceTrigger=LostFocus}" Width="60" Margin="5,0,10,0"/>
            </WrapPanel>

        </WrapPanel>
    </GroupBox>
    <!--#endregion Settings-->

    <!--#region Graphics-->
    <Grid Grid.Row="1">

        <local:Viewport2DCamera x:Name="viewport" Grid.Column="1" Grid.Row="1">
            
            <local:Viewport2DCamera.Camera>
                <PerspectiveCamera 
                        LookDirection="0,0,1" 
                        UpDirection="0,1,0" 
                        Position="0,0,-3" 
                        FieldOfView="45" />
            </local:Viewport2DCamera.Camera>

            <local:Viewport2DCamera.Children>

                <ModelVisual3D>
                    <ModelVisual3D.Content>

                        <Model3DGroup >
                            <Model3DGroup.Children>

                                <AmbientLight Color="White" />

                                <GeometryModel3D Geometry="{Binding Geometry}" Material="{Binding Material}"/>

                            </Model3DGroup.Children>
                        </Model3DGroup>

                    </ModelVisual3D.Content>

                </ModelVisual3D>

            </local:Viewport2DCamera.Children>

        </local:Viewport2DCamera >

    </Grid>
    <!--#endregion Graphics-->
    
</Grid>

MainViewModel.cs:

public class MainViewModel : INotifyPropertyChanged
{
    #region ---------------- Fields ----------------

    private MeshGeometry3D _geometry;
    private readonly Material _material;

    private int _noOfHorizontalPoints;
    private int _noOfVerticalPoints;

    #endregion ------------- Fields ----------------

    #region -------------- Properties --------------

    public MeshGeometry3D Geometry
    {
        get { return _geometry; }
        set
        {
            if (_geometry != value)
            {
                _geometry = value;
                this.OnPropertyChanged();
            }
        }
    }

    public Material Material
    {
        get { return _material; }
    }

    public int NoOfHorizontalPoints
    {
        get { return _noOfHorizontalPoints; }
        set
        {
            if (_noOfHorizontalPoints != value)
            {
                _noOfHorizontalPoints = value;
                this.OnPropertyChanged();
            }
        }
    }

    public int NoOfVerticalPoints
    {
        get { return _noOfVerticalPoints; }
        set
        {
            if (_noOfVerticalPoints != value)
            {
                _noOfVerticalPoints = value;
                this.OnPropertyChanged();
            }
        }
    }
    
    #endregion ----------- Properties --------------

    #region ------------- Constructors -------------

    public MainViewModel()
    {
        _geometry = new MeshGeometry3D();
        _material = new DiffuseMaterial();

        _noOfHorizontalPoints = 500;
        _noOfVerticalPoints = 500;

        defineMaterial();
        defineGeometry();

        PropertyChanged += onPropertyChanged;
    }

    #endregion ---------- Constructors -------------

    #region --------------- Methods ----------------

    private void onPropertyChanged(object sender, PropertyChangedEventArgs e)
    {
        if (e.PropertyName == nameof(NoOfHorizontalPoints) || e.PropertyName == nameof(NoOfVerticalPoints))
        {
            defineGeometry();
        }
    }

    private void defineMaterial()
    {
        #region Create color scheme

        var gradient = new GradientStopCollection();
        gradient.Add(new GradientStop(Colors.Red, 0));
        gradient.Add(new GradientStop(Colors.Orange, 0.25));
        gradient.Add(new GradientStop(Colors.Yellow, 0.5));
        gradient.Add(new GradientStop(Colors.YellowGreen, 0.75));
        gradient.Add(new GradientStop(Colors.Green, 1));

        var linearGradient = new LinearGradientBrush(gradient, new Point(0, 1), new Point(1, 1));

        #endregion Create color scheme

        ((DiffuseMaterial)_material).Brush = linearGradient;
    }

    private void defineGeometry()
    {   
        double totalHeight = 1.0;
        double totalWidth = 1.0;

        int noOfRows = NoOfVerticalPoints;
        int noOfColumns = NoOfHorizontalPoints;

        double heightSizeStep = totalHeight / noOfRows;
        double widthSizeStep = totalWidth / noOfColumns;

        double startHeightPos = -totalHeight / 2.0;
        double startWidthPos = -totalWidth / 2.0;

        var geometry = new MeshGeometry3D();

        var colorRandomizer = new Random();

        for (int row = 0; row < noOfRows; row++)
        {
            for (int col = 0; col < noOfColumns; col++)
            {
                var x1 = startWidthPos + col * widthSizeStep;
                var y1 = startHeightPos + row * heightSizeStep;
                var z1 = 0.0;

                var x2 = startWidthPos + (1 + col) * widthSizeStep;
                var y2 = startHeightPos + (1 + row) * heightSizeStep;
                var z2 = 0.0;

                geometry.Positions.Add(new Point3D(x1, y1, z1));
                geometry.Positions.Add(new Point3D(x2, y1, z2));
                geometry.Positions.Add(new Point3D(x1, y2, z1));
                geometry.Positions.Add(new Point3D(x2, y2, z2));

                var lastPoint = geometry.Positions.Count - 1;

                geometry.TriangleIndices.Add(lastPoint - 3);
                geometry.TriangleIndices.Add(lastPoint - 1);
                geometry.TriangleIndices.Add(lastPoint - 2);
                geometry.TriangleIndices.Add(lastPoint - 2);
                geometry.TriangleIndices.Add(lastPoint - 1);
                geometry.TriangleIndices.Add(lastPoint);

                #region Set color for the current points
                // Colors are randomized for example

                var colorPoint = new Point(colorRandomizer.NextDouble(), 1);

                for (int j = 0; j < 4; j++)
                {
                    geometry.TextureCoordinates.Add(colorPoint);
                }

                #endregion Set color for the current points
            }
        }

        Geometry = geometry;
    }

    #region INotifyPropertyChanged

    /// <summary>
    /// Raises the PropertyChange event for the property specified
    /// </summary>
    /// <param name="propertyName">Property name to update. Is case-sensitive.</param>
    public virtual void RaisePropertyChanged(string propertyName)
    {
        OnPropertyChanged(propertyName);
    }

    /// <summary>
    /// Raised when a property on this object has a new value.
    /// </summary>
    public event PropertyChangedEventHandler PropertyChanged;

    /// <summary>
    /// Raises this object's PropertyChanged event.
    /// </summary>
    /// <param name="propertyName">The property that has a new value.</param>

    protected void OnPropertyChanged([CallerMemberName] string propertyName = "")
    {
        OnPropertyChangedExplicit(propertyName);
    }

    protected void OnPropertyChanged<TProperty>(Expression<Func<TProperty>> projection)
    {
        var memberExpression = (MemberExpression)projection.Body;
        OnPropertyChangedExplicit(memberExpression.Member.Name);
    }

    void OnPropertyChangedExplicit(string propertyName)
    {
        PropertyChangedEventHandler handler = this.PropertyChanged;
        if (handler != null)
        {
            var e = new PropertyChangedEventArgs(propertyName);
            handler(this, e);
        }
    }

    #endregion INotifyPropertyChanged

    #endregion ------------ Methods ----------------
}

简化了自定义视口对象以允许使用鼠标左键进行平移:

public class Viewport2DCamera : Viewport3D
{
    #region ---------------- Fields ----------------

    private Point _previousMousePosition;

    #endregion ------------- Fields ----------------

    #region --------------- Methods ----------------

    protected override void OnPreviewMouseMove(MouseEventArgs e)
    {
        var newPos = e.GetPosition(this as IInputElement);
        double dx = newPos.X - _previousMousePosition.X;
        double dy = newPos.Y - _previousMousePosition.Y;
        
        _previousMousePosition = newPos;

        if (e.MouseDevice.LeftButton is MouseButtonState.Pressed)
        {
            double scale = ActualWidth * ((ProjectionCamera)Camera).Position.Z *0.2;

            ((ProjectionCamera)Camera).Position -= new Vector3D(dx / scale, dy / scale, 0.0);
        }
    }

    #endregion ------------ Methods ----------------
}

好的,我意识到最有可能解决这个问题的明显方法。

我现在创建一个每个像素一种颜色的位图图像,然后将其放入 VisaulBrush 中,然后应用到只有一个矩形的简单几何图形,而不是创建一个巨大的几何图形,其中矩形代表每种颜色。

编辑:我首先使用 ImageBrush,但无法使用 RenderOptions.SetBitmapScalingMode = NearestNeighbor,因此更改为 VisualBrush,可以在 Image 对象上设置它。这用于单独显示每个像素,而不是平滑像素之间的颜色。

已更新 MainViewModel.cs:

public class MainViewModel : INotifyPropertyChanged
{
    #region ---------------- Fields ----------------

    private MeshGeometry3D _geometry;
    private Material _material;

    private int _noOfHorizontalPoints;
    private int _noOfVerticalPoints;

    #endregion ------------- Fields ----------------

    #region -------------- Properties --------------

    public MeshGeometry3D Geometry
    {
        get { return _geometry; }
        set
        {
            if (_geometry != value)
            {
                _geometry = value;
                this.OnPropertyChanged();
            }
        }
    }

    public Material Material
    {
        get { return _material; }
        set
        {
            if (_material != value)
            {
                _material = value;
                this.OnPropertyChanged();
            }
        }
    }

    public int NoOfHorizontalPoints
    {
        get { return _noOfHorizontalPoints; }
        set
        {
            if (_noOfHorizontalPoints != value)
            {
                _noOfHorizontalPoints = value;
                this.OnPropertyChanged();
            }
        }
    }

    public int NoOfVerticalPoints
    {
        get { return _noOfVerticalPoints; }
        set
        {
            if (_noOfVerticalPoints != value)
            {
                _noOfVerticalPoints = value;
                this.OnPropertyChanged();
            }
        }
    }

    #endregion ----------- Properties --------------

    #region ------------- Constructors -------------

    public MainViewModel()
    {
        _geometry = new MeshGeometry3D();
        _material = new DiffuseMaterial();

        _noOfHorizontalPoints = 500;
        _noOfVerticalPoints = 500;

        defineMaterial();
        defineGeometry();

        PropertyChanged += onPropertyChanged;
    }

    #endregion ---------- Constructors -------------

    #region --------------- Methods ----------------

    private void onPropertyChanged(object sender, PropertyChangedEventArgs e)
    {
        if (e.PropertyName == nameof(NoOfHorizontalPoints) || e.PropertyName == nameof(NoOfVerticalPoints))
        {
            defineMaterial();
            defineGeometry();
        }
    }

    private void defineMaterial()
    {
        var colorRandomizer = new Random();

        var pic = new System.Drawing.Bitmap(NoOfHorizontalPoints, NoOfVerticalPoints);
        
        for (int row = 0; row < NoOfVerticalPoints; row++)
        {
            for (int col = 0; col < NoOfHorizontalPoints; col++)
            {
                var tmp = colorRandomizer.NextDouble();

                System.Drawing.Color color;

                if (tmp < 0.2)
                    color = System.Drawing.Color.Green;
                else if (tmp < 0.4)
                    color = System.Drawing.Color.YellowGreen;
                else if (tmp < 0.6)
                    color = System.Drawing.Color.Yellow;
                else if (tmp < 0.8)
                    color = System.Drawing.Color.Orange;
                else
                    color = System.Drawing.Color.Red;

                pic.SetPixel(col, row, color);
            }
        }

        BitmapSource imageSource = Imaging.CreateBitmapSourceFromHBitmap(pic.GetHbitmap(), IntPtr.Zero, Int32Rect.Empty, BitmapSizeOptions.FromEmptyOptions());

        var image = new Image();

        image.Source = imageSource;

        var visualBrush = new VisualBrush(image);

        RenderOptions.SetBitmapScalingMode(image, BitmapScalingMode.NearestNeighbor);   // Used to avoid smooth transition between pixels

        ((DiffuseMaterial)Material).Brush = visualBrush;
    }

    private void defineGeometry()
    {
        double totalHeight = 1.0;
        double totalWidth = 1.0;

        double startHeightPos = -totalHeight / 2.0;
        double startWidthPos = -totalWidth / 2.0;

        var geometry = new MeshGeometry3D();

        var x1 = startWidthPos;
        var y1 = startHeightPos;
        var z1 = 0.0;

        var x2 = startWidthPos + totalWidth;
        var y2 = startHeightPos + totalHeight;
        var z2 = 0.0;

        geometry.Positions.Add(new Point3D(x1, y1, z1));
        geometry.Positions.Add(new Point3D(x2, y1, z2));
        geometry.Positions.Add(new Point3D(x1, y2, z1));
        geometry.Positions.Add(new Point3D(x2, y2, z2));

        geometry.TriangleIndices.Add(0);
        geometry.TriangleIndices.Add(2);
        geometry.TriangleIndices.Add(1);
        geometry.TriangleIndices.Add(1);
        geometry.TriangleIndices.Add(2);
        geometry.TriangleIndices.Add(3);

        geometry.TextureCoordinates.Add(new Point(NoOfHorizontalPoints, NoOfVerticalPoints));
        geometry.TextureCoordinates.Add(new Point(0, NoOfVerticalPoints));
        geometry.TextureCoordinates.Add(new Point(NoOfHorizontalPoints, 0));
        geometry.TextureCoordinates.Add(new Point(0, 0));

        Geometry = geometry;
    }

    #region INotifyPropertyChanged

    /// <summary>
    /// Raises the PropertyChange event for the property specified
    /// </summary>
    /// <param name="propertyName">Property name to update. Is case-sensitive.</param>
    public virtual void RaisePropertyChanged(string propertyName)
    {
        OnPropertyChanged(propertyName);
    }

    /// <summary>
    /// Raised when a property on this object has a new value.
    /// </summary>
    public event PropertyChangedEventHandler PropertyChanged;

    /// <summary>
    /// Raises this object's PropertyChanged event.
    /// </summary>
    /// <param name="propertyName">The property that has a new value.</param>

    protected void OnPropertyChanged([CallerMemberName] string propertyName = "")
    {
        OnPropertyChangedExplicit(propertyName);
    }

    protected void OnPropertyChanged<TProperty>(Expression<Func<TProperty>> projection)
    {
        var memberExpression = (MemberExpression)projection.Body;
        OnPropertyChangedExplicit(memberExpression.Member.Name);
    }

    void OnPropertyChangedExplicit(string propertyName)
    {
        PropertyChangedEventHandler handler = this.PropertyChanged;
        if (handler != null)
        {
            var e = new PropertyChangedEventArgs(propertyName);
            handler(this, e);
        }
    }

    #endregion INotifyPropertyChanged

    #endregion ------------ Methods ----------------
}