sjr*_*son 7 c# wpf caching rendering
下面是一个基于我遇到的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));
}
}
Run Code Online (Sandbox Code Playgroud)
ComponentManager.cs
此类提供用于管理一个便利对象List的Component这样的情况下该颗粒可被添加并施加到每个更新Component的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();
}
}
Run Code Online (Sandbox Code Playgroud)
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();
}
}
Run Code Online (Sandbox Code Playgroud)
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>
Run Code Online (Sandbox Code Playgroud)
问题
正如你可能已经从代码推测,问题在于Update方法ComponentManager和Component.为了使渲染成功,我必须Component每次将粒子添加到粒子系统时更新每一个 - 我试图通过使用is_done类中的标志来缓解任何性能问题,Component当设置为true时第一次计算粒子属性(positions,textures和triangles).然后,或者我认为,在每次后续调用Component::Update()组件时,将使用先前计算的这些集合的值.
但是,这不起作用is_done,因为如上所述设置为true将导致无法呈现任何内容.如果我注释掉is_done = true;然后所有内容都会呈现,但速度非常慢 - 很可能是由于positions每个元素的大量元素被添加到内容中Component(内存使用情况会爆炸,如调试器诊断所示).
题
为什么我必须继续将以前计算的元素添加到这些集合中才能进行渲染?
换句话说,为什么它不仅仅采用已经计算过的
Positions,TextureCoordinates而且TriangleIndices每个都Component在渲染时使用它们?
看起来这里可能有几个问题.
我发现的第一个是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(); });
}
Run Code Online (Sandbox Code Playgroud)
在那里复制n次Update()调用会导致n -1个点被渲染.