Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why do elements need to be continually added to GeometryModel3D property collections which have already been computed for rendering to occur?

Below follows a minimal, complete and verifiable example based on the issue I am encountering with WPF model rendering, here we are just rendering randomly distributed "particles" on an arbitrary 2D plane where each particle has a colour corresponding to its order of spawning.


MainWindow.cs

public partial class MainWindow : Window {
    // prng for position generation
    private static Random rng = new Random();
    private readonly ComponentManager comp_manager;
    private List<Color> color_list;
    // counter for particle no.
    private int current_particles;

    public MainWindow() {
        InitializeComponent();
        comp_manager = new ComponentManager();
        current_particles = 0;
        color_list = new List<Color>();
    }
    // computes the colours corresponding to each particle in 
    // order based on a rough temperature-gradient
    private void ComputeColorList(int total_particles) {
        for (int i = 0; i < total_particles; ++i) {
            Color color = new Color();
            color.ScA = 1;
            color.ScR = (float)i / total_particles;
            color.ScB = 1 - (float)i / total_particles;
            color.ScG = (i < total_particles / 2) ? (float)i / total_particles : (1 - (float)i / total_particles);
            // populate color_list
            color_list.Add(color);
        }
    }
    // clear the simulation view and all Children of WorldModels
    private void Clear() {
        comp_manager.Clear();
        color_list.Clear();
        current_particles = 0;
        // clear Model3DCollection and re-add the ambient light
        // NOTE: WorldModels is a Model3DGroup declared in MainWindow.xaml
        WorldModels.Children.Clear();
        WorldModels.Children.Add(new AmbientLight(Colors.White));
    }
    private void Generate(int total) {
        const int min = -75;
        const int max = 75;
        // generate particles
        while (current_particles < total) {
            int rand_x = rng.Next(min, max);
            int rand_y = rng.Next(min, max);
            comp_manager.AddParticleToComponent(new Point3D(rand_x, rand_y, .0), 1.0);
            Dispatcher.Invoke(() => { comp_manager.Update(); });
            ++current_particles;
        }
    }
    // generate_button click handler
    private void OnGenerateClick(object sender, RoutedEventArgs e) {
        if (current_particles > 0) Clear();
        int n_particles = (int)particles_slider.Value;
        // pre-compute colours of each particle
        ComputeColorList(n_particles);
        // add GeometryModel3D instances for each particle component to WorldModels (defined in the XAML code below)
        for (int i = 0; i < n_particles; ++i) {
            WorldModels.Children.Add(comp_manager.CreateComponent(color_list[i]));
        }
        // generate particles in separate thread purely to maintain
        // similarities between this minimal example and the actual code
        Task.Factory.StartNew(() => Generate(n_particles));
    }
}

ComponentManager.cs

This class provides a convenience object for managing a List of Component instances such that particles can be added and updates applied to each Component in the List.

public class ComponentManager {
    // also tried using an ObservableCollection<Component> but no difference
    private readonly List<Component> comp_list;
    private int id_counter = 0;
    private int current_counter = -1;
    
    public ComponentManager() {
        comp_list = new List<Component>();
    }
    public Model3D CreateComponent(Color color) {
        comp_list.Add(new Component(color, ++id_counter));
        // get the Model3D of most-recently-added Component and return it
        return comp_list[comp_list.Count - 1].ComponentModel;
    }
    public void AddParticleToComponent(Point3D pos, double size) {
        comp_list[++current_counter].SpawnParticle(pos, size);
    }
    public void Update() {
        // VERY SLOW, NEED WAY TO CACHE ALREADY RENDERED COMPONENTS
        foreach (var p in comp_list) { p.Update(); }
    }
    public void Clear() {
        id_counter = 0;
        current_counter = -1;
        foreach(var p in comp_list) { p.Clear(); }
        comp_list.Clear();
    }
}

Component.cs

This class represents the GUI model of a single particle instance with an associated GeometryModel3D giving the rendering properties of the particle (i.e. the material and thus colour as well as render target/visual).

// single particle of systems
public class Particle {
    public Point3D position;
    public double size;
}
public class Component {
    private GeometryModel3D component_model;
    private Point3DCollection positions;  // model Positions collection
    private Int32Collection triangles; // model TriangleIndices collection
    private PointCollection textures; // model TextureCoordinates collection
    private Particle p;
    private int id;
    // flag determining if this component has been rendered
    private bool is_done = false;

    public Component(Color _color, int _id) {
        p = null;
        id = _id;
        component_model = new GeometryModel3D { Geometry = new MeshGeometry3D() };
        Ellipse e = new Ellipse {
            Width = 32.0,
            Height = 32.0
        };
        RadialGradientBrush rb = new RadialGradientBrush();
        // set colours of the brush such that each particle has own colour
        rb.GradientStops.Add(new GradientStop(_color, 0.0));
        // fade boundary of particle
        rb.GradientStops.Add(new GradientStop(Colors.Black, 1.0));
        rb.Freeze();
        e.Fill = rb;
        e.Measure(new Size(32.0, 32.0));
        e.Arrange(new Rect(0.0, 0.0, 32.0, 32.0));
        // cached for increased performance
        e.CacheMode = new BitmapCache();
        BitmapCacheBrush bcb = new BitmapCacheBrush(e);
        DiffuseMaterial dm = new DiffuseMaterial(bcb);
        component_model.Material = dm;
        positions = new Point3DCollection();
        triangles = new Int32Collection();
        textures = new PointCollection();
        ((MeshGeometry3D)component_model.Geometry).Positions = positions;
        ((MeshGeometry3D)component_model.Geometry).TextureCoordinates = textures;
        ((MeshGeometry3D)component_model.Geometry).TriangleIndices = triangles;
    }
    public Model3D ComponentModel => component_model;
    public void Update() {
        if (p == null) return;
        if (!is_done) {
            int pos_index = id * 4;
            // compute positions 
            positions.Add(new Point3D(p.position.X, p.position.Y, p.position.Z));
            positions.Add(new Point3D(p.position.X, p.position.Y + p.size, p.position.Z));
            positions.Add(new Point3D(p.position.X + p.size, p.position.Y + p.size, p.position.Z));
            positions.Add(new Point3D(p.position.X + p.size, p.position.Y, p.position.Z));
            // compute texture co-ordinates
            textures.Add(new Point(0.0, 0.0));
            textures.Add(new Point(0.0, 1.0));
            textures.Add(new Point(1.0, 1.0));
            textures.Add(new Point(1.0, 0.0));
            // compute triangle indices
            triangles.Add(pos_index);
            triangles.Add(pos_index+2);
            triangles.Add(pos_index+1);
            triangles.Add(pos_index);
            triangles.Add(pos_index+3);
            triangles.Add(pos_index+2);
            // commenting out line below enables rendering of components but v. slow
            // due to continually filling up above collections
            is_done = true; 
        }
    }
    public void SpawnParticle(Point3D _pos, double _size) {
        p = new Particle {
            position = _pos,
            size = _size
        };
    }
    public void Clear() {
        ((MeshGeometry3D)component_model.Geometry).Positions.Clear();
        ((MeshGeometry3D)component_model.Geometry).TextureCoordinates.Clear();
        ((MeshGeometry3D)component_model.Geometry).TriangleIndices.Clear();
    }
}

MainWindow.xaml

The (crude) XAML code just for completeness in case anyone wants to verify this example.

<Window x:Class="GraphicsTestingWPF.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:GraphicsTestingWPF"
    mc:Ignorable="d"
    Title="MainWindow" Height="768" Width="1366">
<Grid>
    <Grid Background="Black" Visibility="Visible" Width ="Auto" Height="Auto" Margin="5,3,623,10" HorizontalAlignment="Stretch" VerticalAlignment="Stretch">
        <Viewport3D Name="World" Focusable="True">
            <Viewport3D.Camera>
                <OrthographicCamera x:Name="orthograghic_camera" Position="0,0,32" LookDirection="0,0,-32" UpDirection="0,1,0" Width="256"/>
            </Viewport3D.Camera>

            <Viewport3D.Children>
                <ModelVisual3D>
                    <ModelVisual3D.Content>
                        <Model3DGroup x:Name="WorldModels">
                            <AmbientLight Color="#FFFFFFFF" />
                        </Model3DGroup>
                    </ModelVisual3D.Content>
                </ModelVisual3D>
            </Viewport3D.Children>
        </Viewport3D>
    </Grid>
    <Slider Maximum="1000" TickPlacement="BottomRight" TickFrequency="50" IsSnapToTickEnabled="True" x:Name="particles_slider" Margin="0,33,130,0" VerticalAlignment="Top" Height="25" HorizontalAlignment="Right" Width="337"/>
    <Label x:Name="NParticles_Label" Content="Number of Particles" Margin="0,29,472,0" VerticalAlignment="Top" RenderTransformOrigin="1.019,-0.647" HorizontalAlignment="Right" Width="123"/>
    <TextBox Text="{Binding ElementName=particles_slider, Path=Value, UpdateSourceTrigger=PropertyChanged}" x:Name="particle_val" Height="23" Margin="0,32,85,0" TextWrapping="Wrap" VerticalAlignment="Top" TextAlignment="Right" HorizontalAlignment="Right" Width="40"/>
    <Button x:Name="generate_button" Content="Generate" Margin="0,86,520,0" VerticalAlignment="Top" Click="OnGenerateClick" HorizontalAlignment="Right" Width="75"/>
</Grid>
</Window>

Problem

As you may have surmised from the code, the issue lies in the Update methods of ComponentManager and Component. In order for the rendering to be successful I have to update each and every Component every time a particle is added to the system of particles - I tried to mitigate any performance issues from this by using the flag is_done in the class Component, to be set to true when the particle properties (positions, textures and triangles) were calculated the first time. Then, or so I thought, on each subsequent call to Component::Update() for a component the previously calculated values of these collections would be used.

However, this does not work here as setting is_done to true as explained above will simply cause nothing to be rendered. If I comment out is_done = true; then everything is rendered however it is incredibly slow - most likely due to the huge number of elements being added to the positions etc. collections of each Component (memory usage explodes as shown by the debugger diagnostics).


Question

Why do I have to keep adding previously calculated elements to these collections for rendering to occur?

In other words, why does it not just take the already calculated Positions, TextureCoordinates and TriangleIndices from each Component and use these when rendering?

like image 269
sjrowlinson Avatar asked Jun 21 '16 18:06

sjrowlinson


1 Answers

It looks like there may be several problems here.

The first that I found was that you're calling comp_mgr.Update() every time you add a particle. This, in turn, calls Update() on every particle. All of that results in an O(n^2) operation which means that for 200 particles (your min), you're running the component update logic 40,000 times. This is definitely what's causing it to be slow.

To eliminate this, I moved the comp_mgr.Update() call out of the while loop. But then I got no points, just like when you uncomment the is_done = true; line.

Interestingly, when I added a second call to comp_mgr.Update(), I got a single point. And with successive calls I got an additional point with each call. This implies that, even with the slower code, you're still only getting 199 points on the 200-point setting.

There seems to be a deeper issue somewhere, but I'm unable to find it. I'll update if I do. Maybe this will lead you or someone else to the answer.

For now, the MainWindow.Generate() method looks like this:

private void Generate(int _total)
{
    const int min = -75;
    const int max = 75;
    // generate particles
    while (current_particles < _total)
    {
        int rand_x = rng.Next(min, max);
        int rand_y = rng.Next(min, max);
        comp_manager.AddParticleToComponent(new Point3D(rand_x, rand_y, .0), 1.0);
        ++current_particles;
    }
    Dispatcher.Invoke(() => { comp_manager.Update(); });
}

where replicating the Update() call n times results in n-1 points being rendered.

like image 146
gregsdennis Avatar answered Oct 14 '22 04:10

gregsdennis