Home > Back-end >  2D color mesh plot in WPF using Viewport3D - tricks to improve performance?
2D color mesh plot in WPF using Viewport3D - tricks to improve performance?

Time:06-02

I am working on creating a color mesh plot in a WPF App, see a simplified example project below.

enter image description here

As I wanted the possibility to extend it to 3D I am currently using Viewport3D as a base. Now I've noticed however that it becomes quite slow and uses a lot of memory when increasing the number of color points.

Works fine with 500x500 points and ok with 1000x1000, but need to be able to handle a lot more.

Suggestions on how to improve performance or alternative tools (including 2D) to do this in WPF in order to improve performance are greatly appreciated!

The three files for my simplified example are included below.

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 ----------------
}

Simplified custom Viewport-object to allow panning with left mouse button:

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 ----------------
}

CodePudding user response:

Ok, I realized what is most likely the obvious solution to this problem.

Instead of creating a huge geometry with rectangles representing each color, I now create a bitmap image with one color per pixel and put it in an VisaulBrush that is applied to a simple geometry of only one rectangle.

Edit: I first used ImageBrush, but couldn't get RenderOptions.SetBitmapScalingMode = NearestNeighbor to work for it so changed to VisualBrush where it can be set on the Image object. This is used to show each pixel separately instead of smoothing out the colors between pixels.

Updated 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 ----------------
}
  • Related