Silverlight 中的利萨茹动画

Silverlight 中的利萨茹动画

下载代码示例

我们通常会认为软件比硬件更加灵活,功能也更加多样。 在很多情况下确实是这样,因为硬件经常囿于一种配置,而软件可以在重新编程后执行完全不同的任务。

然而,某些十分普通的硬件实际上用途却相当多样。 我们来看一看常用(现在已不那么常用)的阴极射线管 (CRT)。 这种装置将电子束射到玻璃屏幕的内侧。 屏幕涂有一层荧光材料,这种材料通过短暂发光对电子产生反应。

在老式的电视机和计算机显示器上,电子枪在一种稳定模式下移动,横跨屏幕反复进行水平扫描,同时以更缓慢的速度从上到下移动。 任意时刻电子的强度决定了该点处光点的亮度。 彩色显示器中使用单独的电子枪分别产生红、绿和蓝三原色。

电子枪的方向由电磁铁控制,电子枪实际上可以瞄准玻璃屏幕二维平面上的任意位置。 这就是示波器中 CRT 的使用方式。 最常见的是,电子束以恒定速度在屏幕上水平进行扫描,通常与特定输入波形同步。 垂直偏转显示出该点处波形的幅度。 示波器中使用的荧光材料具有时间较长的余辉,从而可显示整个波形,相当于将波形“冻结”以便查看。

示波器还具有一个 X-Y 模式,通过这种模式,可由两个独立输入(通常为正弦曲线波形)来控制电子枪的水平和垂直偏转。 以两条正弦曲线作为输入,将在任意时间点照亮点 (x, y),其中,x 和 y 由以下参数方程确定:

A 值是幅度,ω 值是频率,而 k 值是相位偏移。

这两个正弦波相互作用得到的图形就是利萨茹曲线,它是以法国数学家朱尔•安托瓦内•利萨茹 (1822 - 1880) 的名字命名的,利萨茹通过将光束在一对与振动的音叉相连的镜子之间弹射,首先观察到这种曲线。

我的网站 (charlespetzold.com/silverlight/LissajousCurves/LissajousCurves.html) 提供了一个可产生利萨茹曲线的 Silverlight 程序,您可以用它进行实验。 图 1 是一个典型显示图。

图 1 Web 版 LissajousCurves 程序

尽管在静态屏幕快照中不太明显,一个绿点在深灰色屏幕上移动,后面留下一条轨迹,4 秒后逐渐淡出。 该点的水平位置由一条正弦曲线控制,垂直位置由另一条正弦曲线控制。 当两条曲线的频率为简单的整数比时,会产生重复图案。

现在人们普遍认为,Silverlight 程序必须植入 Windows Phone 7 中,才能显露在高性能台式计算机中无法发现的所有性能问题。 这个程序肯定也是这样,本文稍后将讨论这些性能问题。 图 2 是运行在 Windows Phone 7 仿真器上的程序。

图 2 用于 Windows Phone 7 的 LissajousCurves 程序

可下载代码包含一个名为 LissajousCurves 的 Visual Studio 解决方案。 该 Web 应用程序由项目 LissajousCurves 和 LissajousCurves.Web 组成。 Windows Phone 7 应用程序的项目名称为 LissajousCurves.Phone。 该解决方案还包含两个库项目:Petzold.Oscilloscope.Silverlight 和 Petzold.Oscilloscope.Phone,不过这两个项目共享所有相同的代码文件。

推还是拉?

除 TextBlock 和 Slider 控件之外,此程序中仅有的其他可视元素是一个从 UserControl 派生的名为 Oscilloscope 的类。 名为 SineCurve 的类的两个实例为 Oscilloscope 提供数据。

SineCurve 本身没有可视元素,但我是从 FrameworkElement 派生该类的,因此可以将这两个实例放在可视化树中,对它们定义绑定。 实际上,程序中的所有内容都与绑定有关:从 Slider 控件到 SineCurve 元素,从 SineCurve 到 Oscilloscope。 Web 版程序的 MainPage.xaml.cs 文件只有默认提供的代码,手机应用程序的等效文件仅实现删除逻辑。

SineCurve 定义两个属性(受依赖关系属性支持),分别名为 Frequency 和 Amplitude。 一个 SineCurve 实例提供 Oscilloscope 的水平值,另一个实例提供垂直值。

SineCurve 类还实现一个接口,我将它命名为 IProvideAxisValue:


   
   
  1.  
  2.           public interface IProvideAxisValue {
  3.   double GetAxisValue(DateTime dateTime);
  4. }
  5.         

SineCurve 通过一个十分简单的方法实现此接口,该方法引用两个字段以及这两个属性:


   
   
  1.  
  2.           public double GetAxisValue(DateTime dateTime) {
  3.   phaseAngle += 2 * Math.PI * this.Frequency * 
  4.     (dateTime - lastDateTime).TotalSeconds;
  5.   phaseAngle %= 2 * Math.PI;
  6.   lastDateTime = dateTime;
  7.  
  8.   return this.Amplitude * Math.Sin(phaseAngle);
  9. }
  10.         

Oscilloscope 类定义两个 IProvideAxisValue 类型的属性(也受依赖关系属性支持),分别名为 XProvider 和 YProvider。 为了实现移动,Oscilloscope 为 CompositionTarget.Rendering 事件安装一个处理程序。 此事件将与视频显示器的刷新率同步触发,可作为动画执行的便利工具。 每次调用 CompositionTarget.Rendering 处理程序时,Oscilloscope 都会对设置为自身的 XProvider 和 YProvider 属性的这两个 SineCurve 对象调用 GetAxisValue。

换句话说,该程序实现拉模型。 Oscilloscope 对象确定何时需要数据,然后从这两个数据提供程序拉出数据。 (我稍后会讨论它如何显示这些数据。)

随着我开始向这个程序添加更多功能(具体而言,是显示正弦曲线的附加控件的两个实例,但最终还是因并无作用反成干扰被我去掉了),我开始怀疑这种模型是否合理。 我有三个对象从两个提供程序拉出相同数据,我想可能采用推模型会更好。

我重新组织程序结构,以便 SineCurve 类为 CompositionTarget.Rendering 安装处理程序,并通过现在简单称为 X 和 Y、类型为 double 的属性将数据推到 Oscilloscope 控件。

我可能应该预料到这种特定推模型的基本缺陷:Oscilloscope 现在接收的 X 和 Y 分别都在变化,所构造的不是平滑的曲线,而是一系列梯级,如图 3 所示。

图 3 推模型实验的混乱结果

显然,很容易做出使用拉模型的决定!

通过 WriteableBitmap 呈现

从构思这个程序时起,我就坚定地认为,使用 WriteableBitmap 是实现实际 Oscilloscope 屏幕的最佳方法。

WriteableBitmap 是一种支持像素寻址的 Silverlight 位图。 位图的所有像素公开为 32 位整数数组。 程序可以任意获取和设置这些像素。 WriteableBitmap 还有一个 Render 方法,通过这种方法,可以将类型为 FrameworkElement 的任何对象的可视元素呈现到位图上。

如果 Oscilloscope 只是需要显示简单的静态曲线,我会使用 Polyline 或 Path,甚至不会考虑使用 WriteableBitmap。 即使该曲线需要改变形状,仍然会首选 Polyline 或 Path。 但是,由 Oscilloscope 显示的曲线需要增加大小,还需要着色(有点奇怪)。 线条需要逐渐淡出:最新显示的线条部分比旧的线条部分更加明亮。 如果我使用单条曲线,则它沿线需要各种颜色。 这在 Silverlight 中 是不受支持的!

如果不使用 WriteableBitmap,程序就需要创建几百个不同的 Polyline 元素,它们的颜色各不相同,位置各异,从而在每个 CompositionTarget.Rendering 事件之后都会触发布局过程。 根据我对 Silverlight 编程的了解,WriteableBitmap 的性能肯定会好得多。

Oscilloscope 类的一个早期版本对 CompositionTarget.Rendering 事件进行处理,方法是从两个 SineCurve 提供程序获取新值,将这些值调整到 WriteableBitmap 的大小,然后构造一个从上一个点到当前点的 Line 对象。 只需将该对象传递给 WriteableBitmap 的 Render 方法:


   
   
  1.  
  2.           writeableBitmap.Render(line, null);
  3.         

Oscilloscope 类定义了一个 Persistence 属性,指示任何颜色或像素的 Alpha 分量从 255 减少到 0 的秒数。让这些像素淡出涉及到直接像素寻址。 代码如图 4 所示。

图 4 像素值淡出代码


   
   
  1.  
  2.           accumulatedDecrease += 256 * 
  3.   (dateTime - lastDateTime).TotalSeconds / Persistence;
  4. int decrease = (int)accumulatedDecrease;
  5.  
  6. // If integral decrease, sweep through the pixels
  7. if (decrease > 0) {
  8.   accumulatedDecrease -= decrease;
  9.  
  10.   for (int index = 0; index < 
  11.     writeableBitmap.Pixels.Length; index++) {
  12.  
  13.     int pixel = writeableBitmap.Pixels[index];
  14.  
  15.     if (pixel != 0) {
  16.       int a = pixel >> 24 & 0xFF;
  17.       int r = pixel >> 16 & 0xFF;
  18.       int g = pixel >> 8 & 0xFF;
  19.       int b = pixel & 0xFF;
  20.  
  21.       a = Math.Max(0, a - decrease);
  22.       r = Math.Max(0, r - decrease);
  23.       g = Math.Max(0, g - decrease);
  24.       b = Math.Max(0, b - decrease);
  25.  
  26.       writeableBitmap.Pixels[index] = a << 24 | r << 16 | g << 8 | b;
  27.     }
  28.   }
  29. }
  30.         

在程序开发中的这个位置,我采取了一些必要步骤,以便该程序也能在手机上正常运行。 在 Web 和手机上,程序似乎运行得都很好,但我知道,问题并没有完全解决。 我没有在 Oscilloscope 屏幕上看到曲线:我看到的是一组连接起来的直线。 这样一组直线,瞬间就破坏了数字仿真模拟的效果!

插值

CompositionTarget.Rendering 处理程序的调用是与视频显示器刷新同步进行的。 对于大多数视频显示器(包括 Windows Phone 7 的显示器),刷新率通常为每秒 60 帧。 换句话说,大约每隔 16 或 17 毫秒就调用一次 CompositionTarget.Rendering 事件处理程序。 (实际上,您可以看到,这只是最好的情况。)即使正弦波的频率小到仅每秒一个周期,对于 480 个像素宽的示波器来说,两个相邻样点的像素坐标也可能相距约 35 个像素。

Oscilloscope 需要在一条曲线的连续样点之间插值。 但这是何种曲线?

我的第一个选择是规范样条(也称为基数样条)。 对于由控制点 p1、p2、p3 和 p4 组成的序列,规范样条可基于一个“张力”因子以某种弯曲度在 p2 和 p3 之间进行三次差值。 这是一种通用解决方案。

规范样条在 Windows 窗体中受支持,但从没有引入 Windows Presentation Foundation (WPF) 或 Silverlight 中。 幸运的是,我有一些 WPF 和 Silverlight 规范样条代码,这些代码是我为 2009 年的一篇博客文章开发的,这篇文章的标题正是“WPF 和 Silverlight 中的规范样条”(bit.ly/bDaWgt)。

通过插值生成 Polyline 之后,CompositionTarget.Rendering 以如下所示的调用结束处理:


   
   
  1.  
  2.           writeableBitmap.Render(polyline, null);
  3.         

规范样条起作用,但不太正确。 当两条正弦曲线的频率是简单的整数倍时,该曲线应稳定为一个固定模式。但这种情况并未发生,我意识到,经过插值的曲线会因实际采样点而略有不同。

这种问题在手机上会更严重,主要是因为手机处理器比较难于满足我施加给它的所有要求。 在较高频率下,手机上的利萨茹曲线看上去是光滑弯曲的,但似乎是以近乎随机的形式在移动!

我慢慢地意识到,我可以基于时间进行差值。 对 CompositionTarget.Rendering 事件处理程序的两次连续调用间隔大约 17 毫秒。 我只需遍历所有这些中间毫秒值,在两个 SineCurve 提供程序中调用 GetAxisValue 方法,就可以构造更光滑的折线。

这种方法的效果好得多。

提高性能

bit.ly/fdvh7Z 的文档页“Windows Phone 应用程序的性能注意事项”提供了适用于所有 Windows Phone 7 编程人员的重要信息。 除了关于提高手机应用程序性能的众多有用提示之外,它还介绍了在 Visual Studio 中运行程序时显示在屏幕侧边的数字的含义,如图 5 所示。

图 5 Windows Phone 7 中的性能指示器

通过将 Application.Current.Host.Settings.EnableFrameRateCounter 属性设置为 true,可以启用这行数字,如果程序在 Visual Studio 调试器中运行,则由标准 App.xaml.cs 文件进行这一设置。

前两个数字最为重要:有时,如果没有执行任何操作,则这两个数字显示为 0,但它们都可用于显示帧速率,即它们可显示每秒的帧数。 刚才提到过,大多数视频显示器都以每秒 60 次的速率进行刷新。 但是,应用程序可能会尝试执行动画,其中的每个新帧都需要 16 或 17 毫秒以上的处理时间。

例如,假设一个 CompositionTarget.Rendering 处理程序需要 50 毫秒执行当前操作。 在这种情况下,程序将以每秒 20 次的速率更新视频显示。 这就是程序的帧速率。

现在,每秒 20 帧并不是太差的帧速率。 请注意,电影的播放速率是每秒 24 帧,在美国,标准电视的有效帧速率(考虑隔行扫描)是每秒 30 帧,在欧洲是每秒 25 帧。 但是,一旦帧速率降到 15 或 10,影响就明显了。

Silverlight for Windows Phone 能够将某些动画卸载到图形处理单元 (GPU),这样,它就有一个辅助线程(有时称为复合线程或 GPU 线程)与 GPU 交互。 第一个数字是与该线程关联的帧速率。 第二个数字是 UI 帧速率,是指应用程序的主线程。 所有 CompositionTarget.Rendering 处理程序都在主线程中运行。

在我的手机上运行 LissajousCurves 程序,我看到数字 22 和 11,它们分别是 GPU 和 UI 线程的数据,如果我增加正弦曲线的频率,它们就会略微下降。 我能够做得更好吗?

我开始想知道我的 CompositionTarget.Rendering 方法中的这一重要语句需要多少时间:


   
   
  1.  
  2.           writeableBitmap.Render(polyline, null);
  3.         

对于 16 或 17 段的折线,每秒会调用 60 次该语句,但实际上,对于 90 段的折线,差不多每秒调用 11 次该语句。

在《Programming Windows Phone 7》(Microsoft Press,2010)中,我为 XNA 编写了一些线条呈现逻辑,我可以针对 Silverlight 在此 Oscilloscope 类对该逻辑进行调整。 那时,我根本不会调用 WriteableBitmap 的 Render 方法,而是直接改变位图中的像素来绘制折线。

遗憾的是,两个帧速率都急速降低到 0! 这使我想到,Silverlight 知道如何在位图上呈现线条,速度比我快得多。 (我还注意到,我的代码并没有针对折线得到优化。)

这时,我想知道是否应该使用 WriteableBitmap 以外的方法。 我用 Canvas 代替了 WriteableBitmap 和 Image 元素,构建每个 Polyline 时,我只是添加该 Canvas。

当然,您不能毫无限制这样做。 您不会希望 Canvas 拥有成千上万个子项。 此外,这些 Polyline 子项需要淡出。 我尝试过两种方法:第一种方法是将 ColorAnimation 连接到每个 Polyline 以降低颜色的 Alpha 通道,然后在动画完成时从 Canvas 中移除 Polyline。 第二种方法手动成分更多,它枚举 Polyline 子项,手动降低颜色的 Alpha 通道,然后在 Alpha 下降到 0 时移除子项。

这四种方法仍存在于 Oscilloscope 类中,在 C# 文件顶部使用四个 #define 语句可以启用这些方法。 图 6 显示了每种方法的帧速率。

图 6 四种 Oscilloscope 更新方法的帧速率

 复合线程UI 线程
WriteableBitmap(Polyline 呈现)2211
WriteableBitmap(手动轮廓填充)00
Canvas(Polyline,动画淡出)2020
Canvas(Polyline,手动淡出)3115

根据图 6,我对 WriteableBitmap 的最初直觉是错误的。 在这种情况下,将一组 Polyline 元素置于画布中真的会更好。 两种淡出方法很令人感兴趣:在由动画执行时,复合线程以每秒 20 帧的速率执行淡出。 手动执行时,UI 线程以每秒 15 帧的速率执行淡出。 但是,添加新的 Polyline 元素总是在 UI 线程中发生,当淡出逻辑卸载到 GPU 时,帧速率为 20。

总之,第三种方法的整体性能是最好的。

我们今天学到了什么? 显然,要达到最佳性能,有必要进行实验。 尝试不同的方法,不要相信自己的最初直觉。


原始链接:http://msdn.microsoft.com/zh-cn/magazine/gg983480.aspx

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值