为什么需要不断将元素添加到已计算的 GeometryModel3D 属性集合中才能进行渲染?



下面是一个基于我在 WPF 模型渲染中遇到的问题的最小、完整和可验证的示例,这里我们只是在任意 2D 平面上渲染随机分布的"粒子",其中每个粒子都有与其生成顺序相对应的颜色。

<小时 />

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

此类提供了一个方便的对象,用于管理Component实例的List,以便可以添加粒子并将更新应用于List中的每个Component

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

此类表示单个粒子实例的 GUI 模型,该实例具有关联的GeometryModel3D,提供粒子的渲染属性(即材质和颜色以及渲染目标/视觉)。

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

(粗略的)XAML 代码只是为了完整起见,以防有人想要验证此示例。

<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>
>问题

正如您可能从代码中推测的那样,问题在于ComponentManagerComponentUpdate方法。为了使渲染成功,每次将粒子添加到粒子系统时,我都必须更新每个Component- 我试图通过使用类Component中的标志is_done来缓解由此引起的任何性能问题,当粒子属性(positionstexturestriangles)是第一次计算的。然后,或者我想,在每次后续调用组件Component::Update()时,都将使用这些集合的先前计算值。

但是,这在这里不起作用,因为如上所述将is_done设置为 true 只会导致不呈现任何内容。如果我注释掉is_done = true;那么一切都会渲染,但它非常慢 - 很可能是由于每个Componentpositions等集合中添加了大量元素(内存使用量爆炸,如调试器诊断所示)。

<小时 />

问题

为什么我必须不断将以前计算的元素添加到这些集合中才能进行渲染?

换句话说,为什么它不只是从每个Component中获取已经计算好的PositionsTextureCoordinatesTriangleIndices,并在渲染时使用这些?

看起来这里可能有几个问题。

我发现的第一个是,每次添加粒子时,您都会调用comp_mgr.Update()。 反过来,这又要求对每个粒子进行Update()。 所有这些都会导致 O(n^2) 操作,这意味着对于 200 个粒子(您的最小值),您将运行组件更新逻辑 40,000 次。 这绝对是导致它变慢的原因。

为了消除这种情况,我将comp_mgr.Update()调用移出了 while 循环。 但后来我没有得到任何分数,就像你取消注释is_done = true;行一样。

有趣的是,当我向comp_mgr.Update()添加第二个调用时,我得到了一个点。 通过连续的通话,我每次通话都会得到额外的分数。 这意味着,即使使用较慢的代码,您在 200 点设置上仍然只能获得 199 分。

某处似乎有一个更深层次的问题,但我找不到它。 如果我这样做,我会更新。 也许这会引导您或其他人找到答案。

目前,MainWindow.Generate()方法如下所示:

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(); });
}

其中,复制nUpdate()调用会导致呈现n-1个点。

最新更新