Sean’s Stuff

Various musings on software development and technology.

Session – WPF: Extensible BitmapEffects, Pixel Shaders, and WPF Graphics Futures

Posted by Sean on 6 November, 2008

PDC 2008, Day #4, Session #4, 1 hr 15 mins

David Teitlebaum
Program Manager
WPF Team

My final session at PDC 2008 was a talk about the improvements in WPF graphics that are available in .NET Framework 3.5 SP1.  David also touched briefly some possible future features (i.e. that would appear in .NET Framework 4.0).

David’s main topic was to walk through the details of the new Shader Effects model, which replaces the old Bitmap Effects feature.

What are Bitmap Effects?

These are effects that are applied to an individual UI element, like a button, to create some desired visual effect.  This includes things like drop shadow, bevels and blur effects.

BitmapEffect

The BitmapEffect object was introduced in Framework 3.0 (the first WPF release).  But there were some problems with it, that led to now replacing it with Shader Effects in 3.5SP1.

Problems with BitmapEffect:

  • They were rendered in software
  • Blur operations were very slow
  • There were various limitations, including no ClearType support, no anisotripic filtering, etc.

New Shader Effects

Basic characteristics in the new Shader Effects include:

  • GPU accelerated
  • Have implemented hardware acceleration of the most popular bitmap effects
    • But did not implement outer glow
  • Can author custom hardware-accelerated bitmap effects using HLSL
  • There is a software-only fallback pipeline that is actually faster than the old Bitmap Effects
  • New Shader Effects run on most video cards
    • Require PixelShader 2.0, which is about 5 years old

How Do You Do Shader Effects?

Here’s an outline of how you use the new Shader Effect model:

  • Derive a custom class from the new ShaderEffect class (which derives from Effect)
  • You write your actual pixel shader code in HLSL, which is used for doing custom hardware-accelerated stuff using Direct3D
    • Language is C-like
    • Compiled to byte-code, consumed by video driver, runs on GPU
  • Some more details about HLSL, as used in WPF
    • DirectX 10 supports HLSL 4.0
    • WPF currently only supports Pixelshader 2.0

So what do pixel shaders really do?  They basically take in a texture (bitmap) as input, do some processing on each point, and return a revised texture as an output.

Basically, you have a main function that accepts the coordinates of the current single pixel to be mapped.  Your code then accesses the original input texture through a register, so it just uses the input parameter (X/Y coordinate) to index into the source texture.  It then does some processing on the pixel in question and returns a color value.  This resultant color value just represents—the resulting RGB color at the specified coordinate.

The final step is to create, in managed code, a class that derives from ShaderEffect and hook it up to the pixel shader code (e.g. xyz.ps file) that you wrote.  You can then apply your shader to any WPF UIElement using XAML.  (By setting the Effect property).

Direct3D Interop

David’s next topic was to talk a bit about interop’ing with Direct3D.  This just means that your WPF application can easily host Direct3D content by using a new class called D3DImage.

This was pretty cool.  David demoed displaying a Direct3D wireframe in the background (WPF 3D subsystem can’t do wireframes), with WPF GUI elements in the foreground, overlaying the background image.

The basic idea is that you create a Direct3D device in unmanaged code and then hook it to a new instance of a WPF D3DImage element, which you include in your visual hierarchy.

WPF Futures

Finally, David touched very briefly on some possible future features.  These are things that may show up in WPF 4.0 (.NET Framework 4.0), or possibly beyond that.

Some of the features likely included in WPF 4.0 include:

  • Increased graphical richness  (e.g. Pixelshader 3.0)
  • Offloading more work to the GPU
  • Better rendering quality
    • Integrate DirectWrite for text clarity
    • Layout rounding

And some of the possible post-4.0 features include:

  • Better exploitation of hardware
  • Vertex shaders
  • Shader groups
  • Shaders in WPF 3D
  • 3D improvements
  • Better media extensibility

References

You can get at David’s PDC08 slide deck for this talk here: http://mschnlnine.vo.llnwd.net/d1/pdc08/PPTX/PC07.pptx

And you can find full video from the session at:  http://mschnlnine.vo.llnwd.net/d1/pdc08/WMV-HQ/PC07.wmv

Posted in PDC 2008, WPF | Tagged: , , , , , , , | No Comments »

Session – Microsoft .NET Framework: Overview and Applications for Babies

Posted by Sean on 28 October, 2008

PDC 2008, Day #1, Session #5, 1 hr 15 min.

Scott Hanselman

There’s no way that I was going to miss Hanselman’s talk.  I’m a big fan of his podcasts—Scott is one of the most knowledgeable tech podcasters out there and he can also be pretty entertaining.  I’m always amazed listening to Scott’s podcasts.  Some guest will be talking about the esoterics of some new platform or service and Scott will just “get it”, quickly grokking what the guy is talking about and end up summarizing it in a nice way.

Scott’s talk didn’t disappoint.  I got to the room early and got a front row seat.  Scott also wins the prize for speaking in the most comfy room at the convention center—the talk was in a cozy little theatre with cushy theatre chairs.

The goal of Scott’s talk was to take a spin around some of the newer (3.5 and 4.0) areas of the .NET Framework.  The vehicle was by extending his WPF BabySmash application to use as many features and services as possible.

BabySmash was the little application that Scott used to teach himself WPF.  He’s blogged about his adventures with BabySmash and learning WPF.

I didn’t take notes during Scott’s session, but let’s see if I can remember the different .NET technologies that Scott dabbled in with BabySmash:

  • Silverlight 2
  • Windows Mobile
  • Surface
  • ADO.NET Data Services
  • Entity Framework
  • New ASP.NET charts/graphs

Basically, Scott ported BabySmash to each of these platforms, or made use of the platform to add some new feature to BabySmash.  It was a great way, using a little app like BabySmash, to illustrate how these technologies fit together.

The grand finale was having all of Scott’s Twitter “minions” go to an online BabySmash web page, which collected metrics about which keys were being pressed.  Scott then displayed a live histogram on an ASP.NET page, showing the distribution of the keys pressed.  It’s a testament to Scott’s popularity that the graph grew quickly, with the frequency for some letters quickly moving into the thousands.

What’s even more amazing is that Scott said he hadn’t tested the Twitter app, at least in a broad/distributed way, until the talk.  Very cool.

I can’t really add much to what Scott writes himself.  So do yourself a favor and check him out at hanselman.com.

Posted in PDC 2008, Silverlight, WPF | Tagged: , , , , , , , | 1 Comment »

Session – Microsoft Silverlight, WPF and the Microsoft .NET Framework: Sharing Skills and Code

Posted by Sean on 28 October, 2008

PDC 2008, Day #1, Session #4, 1 hr 15 min.

Ian Ellison-Taylor

This session focused on sharing code between WPF and Silverlight applications.  How easy is it to take an existing WPF application and run it in the cloud by converting it to Silverlight 2?  Conversely, how easy is it to take a Silverlight 2 application and run it locally as a WPF application?

The bottom line is that it’s really quite easy to run the same application as either a local WPF application or a cloud-based Silverlight 2 app, with just a few modifications.

Ian started with a quick summary of when you’d want to use WPF vs. Silverlight 2:

  • WPF: best for desktop apps needing maximum performance and leveraging 3D graphics on the desktop
  • Silverlight 2: best for RIAs, smaller and lighter, able to run on various platforms and in various browsers

One of the more interesting parts of the talk was Ian’s description of the history of Silverlight 2.  We know that Silverlight 2 uses a smaller version (much smaller) of the .NET Framework, which it deploys via the browser, if a client needs it.

But Ian described how, in the first attempt at a Silverlight 2 framework (WPF/e at the time), they started with the full framework and started pulling stuff out.  They quickly found, however, that it made more sense to start with a clean slate and then only pull in the bits that they needed for Silverlight 2.

Applications written in WPF or Silverlight 2 can be moved to the other platform fairly easily, but Ian said that it was a bit easier to convert Silverlight 2 apps to run as WPF than the other way around.  This makes sense—WPF apps might be using parts of the full .NET framework that aren’t supported in the Silverlight 2 subset.

Also interesting, Ian suggested that developers start by learning Silverlight 2 and then moving to WPF, rather than the other way around.  Things are done in Silverlight 2 in a much simpler way, so the learning curve will likely be shorter.  As an example, he talked about the property system, which is far more complex in WPF.

This was an excellent talk, with some nice demos.  Ian worked simultaneously on a WPF and a Silverlight 2 application, adding features to one and then moving them over to the other platform.  It was an excellent way to highlight some of the differences and the gotchas that developers will run into.  But it also showed off how similar the platforms are and how easy it is to migrate an app from one to the other.

Posted in PDC 2008, Silverlight, WPF | Tagged: , , , , | 1 Comment »

I WPF, Therefore I Blend

Posted by Sean on 26 September, 2008

If you’re a developer doing WPF development, you really need to be using Expression Blend.

Yes, I know the party line on WPF development runs something like this:

  • Every dev team should have at least 1 developer and 1 designer
  • Developers can’t design decent-looking GUIs to save their soul
  • Designers can’t be trusted with code, or anything close to code (excepting XAML)
  • Devs will open a project in Visual Studio and do all of their work there
  • Designers will open the same project in Blend and do all of their work there
  • Devs wear button-up shirts that don’t match their Dockers
  • Designers wear brand-name labels and artsy little berets

I don’t quite buy into the idea of a simple developer/designer separation, with one tool for each of them.  (I also don’t wear Dockers).

It’s absolutely true that Blend makes it easier for a designer to be part of the team and work directly on the product.  The old model was to have the designers do static mockups in Photoshop and then have your devs painstakingly reproduce the images, working in Visual Studio.  The old model sucks.

The new model, having Blend work directly with XAML and even open the same solution file as Visual Studio, is a huge advancement.  Designers get access to all of the flashy photoshoppy features in Blend, which means that they can do their magic and create something that actually looks great.  And devs will instantly get the new GUI layout when they use Visual Studio to open/run the project.

The problem that I have with the designer/developer divide is as follows.  To achieve an excellent user experience takes more than just independently creating form and function and then marrying the two together.  A designer might create GUI screens that are the most beautiful thing on the planet.  And the dev working with him might write the most efficient and elegant code-behind imaginable.  But this isn’t nearly enough to guarantee a great user experience.

User experience is all about user interaction.  Poorly done user interaction will lead to a failed or unused application far more quickly than either an ugly GUI or poorly performing code.

So what exactly is “user interaction”?  In my opinion, it’s everything in the application except for the code and the GUI.  User interaction is all about how the user uses your application to get her work done (or to create what she wants to create).  Does the application make sense to her?  Does using it feel natural?  Allow her to be efficient?  Are features discoverable?  Does the flow of the application match her existing workflow?

The only way to get user interaction correct is to know your user.  This means truly understanding the problem that your users are trying to solve, as well as what knowledge they have about the problem space.

There is an easy four step process to get at this information: 1) talk to the users; 2) prototype; 3) observe them using the prototype; 4) repeat.

There are a whole host of specific strategies to help you in this process, including things like: use cases, user stories, storyboarding, etc.  The literature is full of good processes and techniques for working early and often with users to get both the right set of functionality and a great user experience.

But let’s get back to designers and developers.  The reason that I don’t buy into the clean GUI/code split (or code + markup, if you’re a Petzold fan) is that good user interaction requires both code and markup.  Somebody needs to be responsible for the user interaction model and it should come first, requiring some code and some markup.

If you do buy into the devs-Studio/designers-Blend party line for WPF development, there are two simplistic approaches that you might be tempted to take, both equally bad:

  • Developer codes up all required functionality, puts API on it and designer creates screens that call into the API
  • Designer mocks up screens and then developers create code behind those screens to get desired functionality

The problem behind both approaches is, of course, that no one is focused on how the user is using the application.  The designer is thinking about the user in aesthetic terms and that’s a huge improvement over a battleship grey GUI.  But it’s not nearly enough–not if your goal is to achieve a great user experience.

If someone needs to be responsible for the user experience, it should be the developer.  If you are lucky enough to be working with a designer, the developer is still the team member that drives the entire process.  The designer is likely working in support of the developer, not the other way around.  (Note: I’m talking here about developing rich WPF client software, rather than web-based sites or applications.  With web-based projects, it’s likely the designer that is driving the project).

My vote is for a process that looks something like the following:

  • Developer initiates requirements gathering through user stories and use cases
  • Developer starts sketching up storyboards, with input from designer
  • Developer builds prototype, using both Visual Studio and Blend
  • Team presents prototype to user, walks through use cases, gets feedback, iterates
    • Important to focus here on how the user works w/application, rather than how it looks
  • As pieces of user interaction solidify
    • Designer begins refining those pieces of GUI for aesthetics, branding, etc.
    • Developer begins fleshing out code behind and full functionality
  • Continue iterating/reviewing with user

You might agree with this process, but say that the developer should work exclusively in Visual Studio to generate the prototypes.  Why is it important for them to use Blend for prototyping and iterating with the user?

The simple truth is that Blend is far superior to Visual Studio for doing basic GUI layout.  Using Visual Studio, you can definitely set property values using the property grid or by entering XAML directly.  But the property editors in Blend make it much easier to quickly set properties and tweak controls.

Given that the developer should be doing the GUI prototyping, I think it makes sense for them to use both Blend and Visual Studio, rather than just Visual Studio alone.

The bottom line is this: the choice of using Blend vs. Visual Studio should be based on the task that you are doing, rather than who is doing that task.  Instead of Blend just being a tool for designers and Visual Studio a tool for developers, it’s more true that Blend is a tool for doing GUI design and Visual Studio a tool for writing/debugging code.  Given that I think the developer should be the person responsible for early prototyping of the GUI, they should be using both Blend and Visual Studio during the early phases of a project.

So if you’re a developer just getting into WPF, don’t write off Blend as an artsy-fartsy tool for designers.  Instead, just think of it as a GUI design tool.  Though you may not be great at putting together beautiful user interfaces, it’s definitely your job to create the early GUI prototypes.  You may not be responsible for the design of the GUI, but you should be responsible for designing the GUI.  So if you WPF, you really ought to Blend.  Who knows?  You might like it so much that you start wearing a beret.

Posted in Blend, WPF | Tagged: , , , , , , | 2 Comments »

Writing a Screen Saver in WPF

Posted by Sean on 1 September, 2008

I take my Raindrop Animation from last time and converted it into a screen saver, complete with a Settings dialog, to allow tweaking the various parameters.

Last time, I created a WPF application that displayed an animated simulation of raindrops falling on water.  It was a little work, but not a huge effort, to convert that application into a Windows screen saver.

A screen saver is mainly just a regular .exe file with a .scr extension that has been copied into your C:\Windows\system32 directory.  In the simplest implementation, your application will just run when the screen saver kicks in.  But a fully functional screen saver in Windows will also support two additional features—running in the little preview window in the Screen Saver dialog and providing a customization GUI that is launched from the Settings button in the Screen Saver dialog.  You’ll also want to tweak the normal runtime behavior so that your application runs maximized, without window borders, and responds to mouse and/or keyboard events to shut down gracefully.

Our existing Raindrops WPF application runs in a WPF window.  We can easily tweak its behavior to run maximized and without a window border.  But we also need to interpret command line parameters so that we can decide which of the three following modes to run in:

  • Normal  (run screen saver maximized)
  • Preview  (run screen saver that is hosted in the little preview window)
  • Settings  (show dialog allowing user to tweak settings)

The first thing that we need to do is to change the main Application object in our WPF application and tell it not to start up a window, but to execute some code.  We remove the StartupUri property (was set to “Window1.xaml”) and replace it with a Startup property that points to an Application_Startup method.

Here is the modified App.xaml code:


<Application x:Class="WaveSim.App"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    Startup="Application_Startup">
    <Application.Resources>

    </Application.Resources>
</Application>

The bulk of our changes will be in the new Application_Startup method.  It’s here that we parse the command line and figure out what mode we should run under.  The Screen Saver mechanism and dialog uses the following API to tell a screen saver how to run:

  • /p handle    Run in preview mode, hosting inside preview window whose handle is passed in
  • /s        Run in normal screen saver mode (full screen)
  • /c        Run in settings (configuration) mode, showing GUI to change settings

Here are the entire contents of App.xaml.cs, with the command line parsing logic:


using System;
using System.Collections.Generic;
using System.Configuration;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Windows;
using System.Windows.Interop;
using System.Runtime.InteropServices;
using System.Windows.Media;

namespace WaveSim
{
    /// <summary>
    /// Interaction logic for App.xaml
    /// </summary>
    public partial class App : Application
    {
        // Used to host WPF content in preview mode, attach HwndSource to parent Win32 window.
        private HwndSource winWPFContent;
        private Window1 winSaver;

        private void Application_Startup(object sender, StartupEventArgs e)
        {
            // Preview mode--display in little window in Screen Saver dialog
            // (Not invoked with Preview button, which runs Screen Saver in
            // normal /s mode).
            if (e.Args[0].ToLower().StartsWith("/p"))        
            {
                winSaver = new Window1();

                Int32 previewHandle = Convert.ToInt32(e.Args[1]);
                //WindowInteropHelper interopWin1 = new WindowInteropHelper(win);
                //interopWin1.Owner = new IntPtr(previewHandle);

                IntPtr pPreviewHnd = new IntPtr(previewHandle);

                RECT lpRect = new RECT();
                bool bGetRect = Win32API.GetClientRect(pPreviewHnd, ref lpRect);

                HwndSourceParameters sourceParams = new HwndSourceParameters("sourceParams");

                sourceParams.PositionX = 0;
                sourceParams.PositionY = 0;
                sourceParams.Height = lpRect.Bottom - lpRect.Top;
                sourceParams.Width = lpRect.Right - lpRect.Left;
                sourceParams.ParentWindow = pPreviewHnd;
                sourceParams.WindowStyle = (int)(WindowStyles.WS_VISIBLE | WindowStyles.WS_CHILD | WindowStyles.WS_CLIPCHILDREN);

                winWPFContent = new HwndSource(sourceParams);
                winWPFContent.Disposed += new EventHandler(winWPFContent_Disposed);
                winWPFContent.RootVisual = winSaver.grid1;
            }

            // Normal screensaver mode.  Either screen saver kicked in normally,
            // or was launched from Preview button
            else if (e.Args[0].ToLower().StartsWith("/s"))     
            {
                Window1 win = new Window1();
                win.WindowState = WindowState.Maximized;
                win.Show();
            }

            // Config mode, launched from Settings button in screen saver dialog
            else if (e.Args[0].ToLower().StartsWith("/c"))     
            {
                SettingsWindow win = new SettingsWindow();
                win.Show();
            }

            // If not running in one of the sanctioned modes, shut down the app
            // immediately (because we don't have a GUI).
            else
            {
                Application.Current.Shutdown();
            }
        }

        /// <summary>
        /// Event that triggers when parent window is disposed--used when doing
        /// screen saver preview, so that we know when to exit.  If we didn't
        /// do this, Task Manager would get a new .scr instance every time
        /// we opened Screen Saver dialog or switched dropdown to this saver.
        /// </summary>
        ///
<param name="sender"></param>
        ///
<param name="e"></param>
        void winWPFContent_Disposed(object sender, EventArgs e)
        {
            winSaver.Close();
//            Application.Current.Shutdown();
        }
    }
}

The most complicated thing about this code is what we do in preview mode.  We need to basically take our WPF window and host it inside an existing Win32 window—the little preview window on the Screen Saver dialog.  To start with, all we have is the handle of this window.  The trick is to create a new HwndSource object, specifying the desired size and who we want for a parent window.  Then we attach our WPF window by changing the HwndSource.RootVisual property.  We also hook up an event handler so that we know when the window gets disposed.  When the parent window goes away, we need to make sure to shut our application down (or it will continue to run).

Running in normal screen saver mode is the most straightforward of the three options.  We simply instantiate our Window1 window and show it.

For settings/configuration mode, we show a new SettingsWindow window that we’ve created.  This window will display some sliders to let the user change various settings and it will also persist the new settings to an .xml file.

The Raindrop settings are encapsulated in the new RaindropSettings class.  This class just contains public (serializable) properties for the various things we want to tweak, and it includes Save and Load methods that serialize the properties to an .xml file and read them back in.

It’s important that we serialize these properties in an .xml file because the screen saver architecture doesn’t expect to display a settings dialog while the screen saver is running.  Instead, it expects to run the application once to allow the user to change settings and then run again to show the screen saver.

Here is the full code for the RaindropSettings class.  Note that we use auto-implemented properties so that we don’t have to write prop getter/setter code:


using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Xml.Serialization;

namespace WaveSim
{
    /// <summary>
    /// Persist raindrop screen saver settings in memory and provide support
    /// for loading from file and persisting to file.
    /// </summary>
    public class RaindropSettings
    {
        public const string SettingsFile = "Raindrops.xml";

        public double RaindropPeriodInMS { get; set; }  
        public double SplashAmplitude { get; set; }
        public int DropSize { get; set; }
        public double Damping { get; set; }

        /// <summary>
        /// Instantiate the class, loading settings from a specified file.
        /// If the file doesn't exist, use default values.
        /// </summary>
        ///
<param name="sSettingsFilename"></param>
        public RaindropSettings()
        {
            SetDefaults();      // Clean object, start w/defaults
        }

        /// <summary>
        /// Set all values to their defaults
        /// </summary>
        public void SetDefaults()
        {
            RaindropPeriodInMS = 35.0;
            SplashAmplitude = -3.0;
            DropSize = 1;
            Damping = 0.96;
        }

        /// <summary>
        /// Save current settings to external file
        /// </summary>
        ///
<param name="sSettingsFilename"></param>
        public void Save(string sSettingsFilename)
        {
            try
            {
                XmlSerializer serial = new XmlSerializer(typeof(RaindropSettings));

                FileStream fs = new FileStream(sSettingsFilename, FileMode.Create);
                TextWriter writer = new StreamWriter(fs, new UTF8Encoding());
                serial.Serialize(writer, this);
                writer.Close();
            }
            catch { }
        }

        /// <summary>
        /// Attempt to load settings from external file.  If the file doesn't
        /// exist, or if there is a problem, no settings are changed.
        /// </summary>
        ///
<param name="sSettingsFilename"></param>
        public static RaindropSettings Load(string sSettingsFilename)
        {
            RaindropSettings settings = null;

            try
            {
                XmlSerializer serial = new XmlSerializer(typeof(RaindropSettings));
                FileStream fs = new FileStream(sSettingsFilename, FileMode.OpenOrCreate);
                TextReader reader = new StreamReader(fs);
                settings = (RaindropSettings)serial.Deserialize(reader);
            }
            catch {
                // If we can't load, just create a new object, which gets default values
                settings = new RaindropSettings();     
            }

            return settings;
        }
    }
}

Here is the .xaml for our SettingsWindow class.  The window will contain four sliders, one for each setting.  It also includes a button that resets everything back to the default values.  When the user clicks the OK button, all settings are persisted to the RaindropSettings.xml file.  (There is no cancel function).


<Window x:Class="WaveSim.SettingsWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    Title="Raindrop Screensaver Settings" Height="300" Width="300">
    <Grid>
        <Button Height="23" Margin="0,0,48,17" Name="btnClose" VerticalAlignment="Bottom" Click="btnClose_Click" HorizontalAlignment="Right" Width="76">OK</Button>
        <Slider Height="21" Margin="0,27,10,0" Name="slidNumDrops" VerticalAlignment="Top" Minimum="1" Maximum="1000" AutoToolTipPlacement="BottomRight" HorizontalAlignment="Right" Width="164" ValueChanged="slidNumDrops_ValueChanged" />
        <Label Height="28" Margin="24,25,0,0" Name="label1" VerticalAlignment="Top" HorizontalAlignment="Left" Width="70">Num Drops</Label>
        <Button Height="23" HorizontalAlignment="Left" Margin="43,0,0,17" Name="btnDefaults" VerticalAlignment="Bottom" Width="76" Click="btnDefaults_Click">Defaults</Button>
        <Label Height="28" HorizontalAlignment="Left" Margin="6,66,0,0" Name="label2" VerticalAlignment="Top" Width="88">Drop Strength</Label>
        <Slider AutoToolTipPlacement="BottomRight" Height="21" Margin="104,70,10,0" Maximum="15" Minimum="0" Name="slidDropStrength" VerticalAlignment="Top" ValueChanged="slidDropStrength_ValueChanged" />
        <Label HorizontalAlignment="Left" Margin="29,111,0,123" Name="label3" Width="61">Drop Size</Label>
        <Slider AutoToolTipPlacement="BottomRight" Margin="104,114,10,127" Maximum="6" Minimum="1" Name="slidDropSize" ValueChanged="slidDropSize_ValueChanged" />
        <Label Height="28" HorizontalAlignment="Left" Margin="30,0,0,79" Name="label4" VerticalAlignment="Bottom" Width="61">Damping</Label>
        <Slider AutoToolTipPlacement="BottomRight" Height="21" Margin="104,0,10,83" Maximum="100" Minimum="50" Name="slidDamping" VerticalAlignment="Bottom" ValueChanged="slidDamping_ValueChanged" SmallChange="0.01" LargeChange="0.1" />
    </Grid>
</Window>

And here is the full code for SettingsWindow.xaml.cs.  When we load the window, we read in settings from the .xml file and change the value of the sliders.  When the user clicks OK, we just save out the current settings to RaindropSettings.xml.


using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Shapes;

namespace WaveSim
{
    /// <summary>
    /// Interaction logic for SettingsWindow.xaml
    /// </summary>
    public partial class SettingsWindow : Window
    {
        private RaindropSettings settings;

        public SettingsWindow()
        {
            InitializeComponent();

            // Load settings from file (or just set to default values
            // if file not found)
            settings = RaindropSettings.Load(RaindropSettings.SettingsFile);

            SetSliders();
        }

        private void btnClose_Click(object sender, RoutedEventArgs e)
        {
            settings.Save(RaindropSettings.SettingsFile);
            this.Close();
        }

        /// <summary>
        /// Set all sliders to their default values
        /// </summary>
        ///
<param name="sender"></param>
        ///
<param name="e"></param>
        private void btnDefaults_Click(object sender, RoutedEventArgs e)
        {
            settings.SetDefaults();
            SetSliders();
        }

        private void SetSliders()
        {
            slidNumDrops.Value = 1.0 / (settings.RaindropPeriodInMS / 1000.0);
            slidDropStrength.Value = -1.0 * settings.SplashAmplitude;
            slidDropSize.Value = settings.DropSize;
            slidDamping.Value = settings.Damping * 100;
        }

        private void slidDropStrength_ValueChanged(object sender, RoutedPropertyChangedEventArgs<double> e)
        {
            if (settings != null)
            {
                // Slider runs [0,30], so our amplitude runs [-30,0]. 
                // Negative amplitude is desirable because we see little towers of 
                // water as each drop bloops in. 
                settings.SplashAmplitude = -1.0 * slidDropStrength.Value;
            }
        }

        private void slidNumDrops_ValueChanged(object sender, RoutedPropertyChangedEventArgs<double> e)
        {
            if (settings != null)
            {
                // Slider runs from [1,1000], with 1000 representing more drops (1 every ms) and 
                // 1 representing fewer (1 ever 1000 ms).  This is to make slider seem natural 
                // to user.  But we need to invert it, to get actual period (ms) 
                settings.RaindropPeriodInMS = (1.0 / slidNumDrops.Value) * 1000.0;
            }
        }

        private void slidDropSize_ValueChanged(object sender, RoutedPropertyChangedEventArgs<double> e)
        {
            if (settings != null)
            {
                settings.DropSize = (int)slidDropSize.Value;
            }
        }

        private void slidDamping_ValueChanged(object sender, RoutedPropertyChangedEventArgs<double> e)
        {
            if (settings != null)
            {
                settings.Damping = slidDamping.Value / 100;
            }
        }
    }
}

The only remaining thing to be done is to change Window1 to get rid of our earlier sliders and to read in the settings from the .xml file.

Here is the modified Window1.xaml code:


<Window x:Class="WaveSim.Window1"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    Title="Window1"
    MouseWheel="Window_MouseWheel" ShowInTaskbar="False" ResizeMode="NoResize" WindowStyle="None"
        MouseDown="Window_MouseDown" KeyDown="Window_KeyDown" Background="Black">
    <Grid Name="grid1">
        <Viewport3D Name="viewport3D1">
            <Viewport3D.Camera>
                <PerspectiveCamera x:Name="camMain" Position="255 38.5 255" LookDirection="-130 -40 -130" FarPlaneDistance="450" UpDirection="0,1,0" NearPlaneDistance="1" FieldOfView="70">

                </PerspectiveCamera>
            </Viewport3D.Camera>
            <ModelVisual3D x:Name="vis3DLighting">
                <ModelVisual3D.Content>
                    <DirectionalLight x:Name="dirLightMain" Direction="2, -2, 0"/>
                </ModelVisual3D.Content>
            </ModelVisual3D>
            <ModelVisual3D>
                <ModelVisual3D.Content>
                    <DirectionalLight Direction="0, -2, 2"/>
                </ModelVisual3D.Content>
            </ModelVisual3D>
            <ModelVisual3D>
                <ModelVisual3D.Content>
                    <GeometryModel3D x:Name="gmodMain">
                        <GeometryModel3D.Geometry>
                            <MeshGeometry3D x:Name="meshMain" >
                            </MeshGeometry3D>
                        </GeometryModel3D.Geometry>
                        <GeometryModel3D.Material>
                            <MaterialGroup>
                                <DiffuseMaterial x:Name="matDiffuseMain">
                                    <DiffuseMaterial.Brush>
                                        <SolidColorBrush Color="DarkBlue"/>
                                    </DiffuseMaterial.Brush>
                                </DiffuseMaterial>
                                <SpecularMaterial SpecularPower="24">
                                    <SpecularMaterial.Brush>
                                        <SolidColorBrush Color="LightBlue"/>
                                    </SpecularMaterial.Brush>
                                </SpecularMaterial>
                            </MaterialGroup>
                        </GeometryModel3D.Material>
                    </GeometryModel3D>
                </ModelVisual3D.Content>
            </ModelVisual3D>
        </Viewport3D>
    </Grid>
</Window>

And here is the updated Window1.xaml.cs.  Note that we also add event handlers to shut down the application when a mouse or keyboard button is pressed.


using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Media3D;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
using System.Windows.Threading;

namespace WaveSim
{
    /// <summary>
    /// Interaction logic for Window1.xaml
    /// </summary>
    public partial class Window1 : Window
    {
        private Vector3D zoomDelta;

        private WaveGrid _grid;
        private bool _rendering;
        private double _lastTimeRendered;
        private Random _rnd = new Random(1234);

        // Raindrop parameters, from .xml settings file
        private RaindropSettings _settings;

        private double _splashDelta = 1.0;      // Actual splash height is Ampl +/- Delta (random)
        private double _waveHeight = 15.0;

        // Values to try:
        //   GridSize=20, RenderPeriod=125
        //   GridSize=50, RenderPeriod=50
        private const int GridSize = 250; //50;   
        private const double RenderPeriodInMS = 60; //50; 

        public Window1()
        {
            InitializeComponent();

            // Read in settings from .xml file
            _settings = RaindropSettings.Load(RaindropSettings.SettingsFile);

            // Set up the grid
            _grid = new WaveGrid(GridSize);
            _grid.Damping = _settings.Damping;
            meshMain.Positions = _grid.Points;
            meshMain.TriangleIndices = _grid.TriangleIndices;

            // On each WheelMouse change, we zoom in/out a particular % of the original distance
            const double ZoomPctEachWheelChange = 0.02;
            zoomDelta = Vector3D.Multiply(ZoomPctEachWheelChange, camMain.LookDirection);

            StartStopRendering();
        }

        private void Window_MouseWheel(object sender, MouseWheelEventArgs e)
        {
            if (e.Delta > 0)
                // Zoom in
                camMain.Position = Point3D.Add(camMain.Position, zoomDelta);
            else
                // Zoom out
                camMain.Position = Point3D.Subtract(camMain.Position, zoomDelta);
        }

        // Start/stop animation
        private void StartStopRendering()
        {
            if (!_rendering)
            {
                //_grid = new WaveGrid(GridSize);        // New grid allows buffer reset
                _grid.FlattenGrid();
                meshMain.Positions = _grid.Points;

                _lastTimeRendered = 0.0;
                CompositionTarget.Rendering += new EventHandler(CompositionTarget_Rendering);
                _rendering = true;
            }
            else
            {
                CompositionTarget.Rendering -= new EventHandler(CompositionTarget_Rendering);
                _rendering = false;
            }
        }

        void CompositionTarget_Rendering(object sender, EventArgs e)
        {
            RenderingEventArgs rargs = (RenderingEventArgs)e;
            if ((rargs.RenderingTime.TotalMilliseconds - _lastTimeRendered) > RenderPeriodInMS)
            {
                // Unhook Positions collection from our mesh, for performance
                // (see http://blogs.msdn.com/timothyc/archive/2006/08/31/734308.aspx)
                meshMain.Positions = null;

                // Do the next iteration on the water grid, propagating waves
                double NumDropsThisTime = RenderPeriodInMS / _settings.RaindropPeriodInMS;

                // Result at this point for number of drops is something like
                // 2.25.  We'll induce integer portion (e.g. 2 drops), then
                // 25% chance for 3rd drop.
                int NumDrops = (int)NumDropsThisTime;   // trunc
                for (int i = 0; i < NumDrops; i++)
                    _grid.SetRandomPeak(_settings.SplashAmplitude, _splashDelta, _settings.DropSize);

                if ((NumDropsThisTime - NumDrops) > 0)
                {
                    double DropChance = NumDropsThisTime - NumDrops;
                    if (_rnd.NextDouble() <= DropChance)
                        _grid.SetRandomPeak(_settings.SplashAmplitude, _splashDelta, _settings.DropSize);
                }

                _grid.ProcessWater();

                // Then update our mesh to use new Z values
                meshMain.Positions = _grid.Points;

                _lastTimeRendered = rargs.RenderingTime.TotalMilliseconds;
            }
        }

        private void Window_MouseDown(object sender, MouseButtonEventArgs e)
        {
            Application.Current.Shutdown();
        }

        private void Window_KeyDown(object sender, KeyEventArgs e)
        {
            Application.Current.Shutdown();
        }
    }
}

Here is a .zip file containing the entire Raindrops Screen Saver project.  After you build it, you’ll need to:

  • Rename WaveSimScrSaver.exe to WaveSimScrSaver.scr
  • Copy WaveSimScrSaver.scr to C:\Windows\system32

Here’s a screen shot of the screen saver running in Preview mode.  This is very satisfying, since getting this to work properly was the hardest part of the project.

Next Steps

There are a few obvious “next steps” to take in this project, including:

  • Stop screen saver on mouse move (stop on large movement, but not small movement)
  • Run screen saver on multiple monitors/screens
  • Allow user to set the background image
  • Allow user to set an image to get mapped onto the surface of the water

Sources

Here are some of the sources that I used in learning how to create and run a screen saver in WPF:

Posted in WPF | Tagged: , , , | No Comments »

Raindrop Animation in WPF

Posted by Sean on 24 August, 2008

I’ve expanded a bit on my earlier example of simulating ripples on water in WPF.  Last time, I started a ripple by inducing a single peak value into a grid of points and then watching the ripples propagate.

This time, we go much further, inducing peaks at random intervals to simulate raindrops falling on a liquid surface.  The underlying algorithm for propagating the ripples is identical to last time—calculating new height values for every point in a 2D mesh, using a basic filtering/smoothing algorithm.

To see the final result right away, you can download/run the WPF application from here.  As before, you can use the mouse wheel to zoom in/out, while the simulation is running.

I’ve updated the GUI to include a few knobs that you can play with.  The three sliders that control the raindrops are:

  • Num Drops – Controls how fast the drops are falling.  For starters, the average time between raindrops is 35ms.  The slider allows changing the frequency, such that the time between drops ranges from 1ms to 1000ms.  (On average)
  • Drop Strength – Controls how deep the drop falls, which impacts the amplitude of the resulting ripples.  Defaults to creating a drop that goes 3.0 units deep, with a range of [0,15].  (Grid is 250×250 units).
  • Drop Size – The diameter of the drop that comes down.  (Actually, drops are square, so this value is the length of one side of the square).  Defaults to 1, range is [1,6].

To start the animation, with the default values, click on the Start Rain button.  You’ll get a nice/natural animated scene, with raindrops falling on the water.  (On my graphics card, at least, this results in an animation that feels close to real-time—this may not be true on slower/faster cards).

The next thing to try playing with is the Num Drops setting, leaving everything else the same.  The raindrop frequency will increase as you move the slider, and you’ll a much more agitated surface, since the ripples don’t have enough time to damp.

Now try turning the Num Drops setting back down low and turn up the Drop Size setting.  Now you’ll get nice fat drops that create pretty good-size ripples.

Finally, set Drop Size back down again and try playing with the Drop Strength setting.  You’ll simulate stronger drops, as we create much deeper craters for each drop initially.  Also notice the little tower of water the jumps up as the first visual indication of a drop.

You can obviously play with all three of the settings at the same time.  Doing so, you can easily get a pretty crazy bathtub effect, as the waves just get larger and larger.

Use of the Wave button is left as an exercise to the reader.  It basically introduces a deep channel across the entire wave mesh, which results in a fairly large wave that propagates out in both directions.

One interesting thing to note about the wave is that you’ll see the existing ripples bend around the wave and continue propagating outward.  Also note that, because we add all amplitudes to existing point heights, new drops that fall on the wave will be at the proper height, relative to the current wave height.

Ok, I can’t resist.  Here’s a screencap of the Wave in action.

Below is the WPF code that I used for the simulation.  As before, the three parts are: a) the static XAML that sets up the window; b) the code-behind for Window1, which runs the Rendering loop and c) the WaveGrid class, which does the actual simulation and contains the two point buffers.

Here is the XAML code for the main window, nothing too spectacular:


<Window x:Class="WaveSim.Window1"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    Title="Window1" Height="679.023" Width="812.646"
    MouseWheel="Window_MouseWheel">
    <Grid Name="grid1" Height="618.12" Width="759.015">
        <Grid.RowDefinitions>
            <RowDefinition Height="76*" />
            <RowDefinition Height="542.12*" />
        </Grid.RowDefinitions>
        <Button HorizontalAlignment="Right" Margin="0,11.778,115,0" Name="btnStart" Width="75" Click="btnStart_Click" Height="22.649" VerticalAlignment="Top">Start Rain</Button>
        <Viewport3D Name="viewport3D1" Grid.Row="1">
            <Viewport3D.Camera>
                <PerspectiveCamera x:Name="camMain" Position="255 38.5 255" LookDirection="-130 -40 -130" FarPlaneDistance="450" UpDirection="0,1,0" NearPlaneDistance="1" FieldOfView="70">

                </PerspectiveCamera>
            </Viewport3D.Camera>
            <ModelVisual3D x:Name="vis3DLighting">
                <ModelVisual3D.Content>
                    <DirectionalLight x:Name="dirLightMain" Direction="2, -2, 0"/>
                </ModelVisual3D.Content>
            </ModelVisual3D>
            <ModelVisual3D>
                <ModelVisual3D.Content>
                    <DirectionalLight Direction="0, -2, 2"/>
                </ModelVisual3D.Content>
            </ModelVisual3D>
            <ModelVisual3D>
                <ModelVisual3D.Content>
                    <GeometryModel3D x:Name="gmodMain">
                        <GeometryModel3D.Geometry>
                            <MeshGeometry3D x:Name="meshMain" >
                            </MeshGeometry3D>
                        </GeometryModel3D.Geometry>
                        <GeometryModel3D.Material>
                            <MaterialGroup>
                                <DiffuseMaterial x:Name="matDiffuseMain">
                                    <DiffuseMaterial.Brush>
                                        <SolidColorBrush Color="DarkBlue"/>
                                    </DiffuseMaterial.Brush>
                                </DiffuseMaterial>
                                <SpecularMaterial SpecularPower="24">
                                    <SpecularMaterial.Brush>
                                        <SolidColorBrush Color="LightBlue"/>
                                    </SpecularMaterial.Brush>
                                </SpecularMaterial>
                            </MaterialGroup>
                        </GeometryModel3D.Material>
                    </GeometryModel3D>
                </ModelVisual3D.Content>
            </ModelVisual3D>
        </Viewport3D>
        <Slider Margin="0,13.596,198,0" Name="slidPeakHeight" ValueChanged="slidPeakHeight_ValueChanged" Minimum="0" Maximum="15" HorizontalAlignment="Right" Width="167.256" Height="20.831" VerticalAlignment="Top" />
        <Label Margin="286,11.964,0,36.083" Name="lblDropDepth" HorizontalAlignment="Left" Width="89.015">Drop Strength</Label>
        <Slider Name="slidNumDrops" HorizontalAlignment="Left" Margin="111,15.452,0,0" Maximum="1000" Minimum="1" Width="167.256" ValueChanged="slidNumDrops_ValueChanged" Height="20.831" VerticalAlignment="Top" />
        <Label HorizontalAlignment="Left" Margin="12,13.596,0,34.451" Name="label1" Width="89">Num Drops</Label>
        <Button HorizontalAlignment="Right" Margin="0,11.963,19,0" Name="btnWave" Width="75" Click="btnWave_Click" Height="22.649" VerticalAlignment="Top">Wave !</Button>
        <Slider Height="20.831" HorizontalAlignment="Left" Margin="111,0,0,5.266" Maximum="6" Minimum="1" Name="slidDropSize" VerticalAlignment="Bottom" Width="167.256" ValueChanged="slidDropSize_ValueChanged"/>
        <Label Height="27.953" HorizontalAlignment="Left" Margin="12,0,0,0" Name="label2" VerticalAlignment="Bottom" Width="89">Drop Size</Label>
    </Grid>
</Window>

Here is the Window1.xaml.cs code.  Some things to take note of:

  • We’re no longer setting peaks in the center of the grid, but calling SetRandomPeak to induce each raindrop
  • As before, we’re using the CompositionTarget_Rendering event handler as our main rendering loop.  During the loop, we induce new raindrops, tell the grid to process the point mesh (propagating waves) and we then reattach the new point grid to our MeshGeometry3D
  • Note that we calculate the number of drops to induce by first calculating how many drops we should drop each time we visit this loop (should be moved outside the loop).  We induce points for the integer portion of this number and then use the fractional part as a % chance of dropping one more point.

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Media3D;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
using System.Windows.Threading;

namespace WaveSim
{
    /// <summary>
    /// Interaction logic for Window1.xaml
    /// </summary>
    public partial class Window1 : Window
    {
        private Vector3D zoomDelta;

        private WaveGrid _grid;
        private bool _rendering;
        private double _lastTimeRendered;
        private Random _rnd = new Random(1234);

        // Raindrop parameters.  Negative amplitude causes little tower of
        // water to jump up vertically in the instant after the drop hits.
        private double _splashAmplitude; // Average height (depth, since negative) of raindrop splashes.
        private double _splashDelta = 1.0;      // Actual splash height is Ampl +/- Delta (random)
        private double _raindropPeriodInMS;
        private double _waveHeight = 15.0;
        private int _dropSize;

        // Values to try:
        //   GridSize=20, RenderPeriod=125
        //   GridSize=50, RenderPeriod=50
        private const int GridSize = 250; //50;
        private const double RenderPeriodInMS = 60; //50;    

        public Window1()
        {
            InitializeComponent();

            _splashAmplitude = -3.0;
            slidPeakHeight.Value = -1.0 * _splashAmplitude;

            _raindropPeriodInMS = 35.0;
            slidNumDrops.Value = 1.0 / (_raindropPeriodInMS / 1000.0);

            _dropSize = 1;
            slidDropSize.Value = _dropSize;

            // Set up the grid
            _grid = new WaveGrid(GridSize);
            meshMain.Positions = _grid.Points;
            meshMain.TriangleIndices = _grid.TriangleIndices;

            // On each WheelMouse change, we zoom in/out a particular % of the original distance
            const double ZoomPctEachWheelChange = 0.02;
            zoomDelta = Vector3D.Multiply(ZoomPctEachWheelChange, camMain.LookDirection);
        }

        private void Window_MouseWheel(object sender, MouseWheelEventArgs e)
        {
            if (e.Delta > 0)
                // Zoom in
                camMain.Position = Point3D.Add(camMain.Position, zoomDelta);
            else
                // Zoom out
                camMain.Position = Point3D.Subtract(camMain.Position, zoomDelta);
        }

        // Start/stop animation
        private void btnStart_Click(object sender, RoutedEventArgs e)
        {
            if (!_rendering)
            {
                //_grid = new WaveGrid(GridSize);        // New grid allows buffer reset
                _grid.FlattenGrid();
                meshMain.Positions = _grid.Points;

                _lastTimeRendered = 0.0;
                CompositionTarget.Rendering += new EventHandler(CompositionTarget_Rendering);
                btnStart.Content = "Stop";
                _rendering = true;
            }
            else
            {
                CompositionTarget.Rendering -= new EventHandler(CompositionTarget_Rendering);
                btnStart.Content = "Start";
                _rendering = false;
            }
        }

        void CompositionTarget_Rendering(object sender, EventArgs e)
        {
            RenderingEventArgs rargs = (RenderingEventArgs)e;
            if ((rargs.RenderingTime.TotalMilliseconds - _lastTimeRendered) > RenderPeriodInMS)
            {
                // Unhook Positions collection from our mesh, for performance
                // (see http://blogs.msdn.com/timothyc/archive/2006/08/31/734308.aspx)
                meshMain.Positions = null;

                // Do the next iteration on the water grid, propagating waves
                double NumDropsThisTime = RenderPeriodInMS / _raindropPeriodInMS;

                // Result at this point for number of drops is something like
                // 2.25.  We'll induce integer portion (e.g. 2 drops), then
                // 25% chance for 3rd drop.
                int NumDrops = (int)NumDropsThisTime;   // trunc
                for (int i = 0; i < NumDrops; i++)
                    _grid.SetRandomPeak(_splashAmplitude, _splashDelta, _dropSize);

                if ((NumDropsThisTime - NumDrops) > 0)
                {
                    double DropChance = NumDropsThisTime - NumDrops;
                    if (_rnd.NextDouble() <= DropChance)
                        _grid.SetRandomPeak(_splashAmplitude, _splashDelta, _dropSize);
                }

                _grid.ProcessWater();

                // Then update our mesh to use new Z values
                meshMain.Positions = _grid.Points;

                _lastTimeRendered = rargs.RenderingTime.TotalMilliseconds;
            }
        }

        private void slidPeakHeight_ValueChanged(object sender, RoutedPropertyChangedEventArgs<double> e)
        {
            // Slider runs [0,30], so our amplitude runs [-30,0].
            // Negative amplitude is desirable because we see little towers of
            // water as each drop bloops in.
            _splashAmplitude = -1.0 * slidPeakHeight.Value;
        }

        private void slidNumDrops_ValueChanged(object sender, RoutedPropertyChangedEventArgs<double> e)
        {
            // Slider runs from [1,1000], with 1000 representing more drops (1 every ms) and
            // 1 representing fewer (1 ever 1000 ms).  This is to make slider seem natural
            // to user.  But we need to invert it, to get actual period (ms)
            _raindropPeriodInMS = (1.0 / slidNumDrops.Value) * 1000.0;
        }

        private void btnWave_Click(object sender, RoutedEventArgs e)
        {
            _grid.InduceWave(_waveHeight);
        }

        private void slidDropSize_ValueChanged(object sender, RoutedPropertyChangedEventArgs<double> e)
        {
            _dropSize = (int)slidDropSize.Value;
        }
    }
}

Finally, here is the updated code for the WaveGrid class.  Things to note:

  • We’ve replaced SetCenterPeak with SetRandomPeak, which does the “dropping”
  • The crazy wave is induced in InduceWave
  • I’ve added a FlattenGrid function, to calm things down

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Windows.Media;
using System.Windows.Media.Media3D;

namespace WaveSim
{
    class WaveGrid
    {
        // Constants
        const int MinDimension = 5;
        const double Damping = 0.96;    // SAVE: 0.96
        const double SmoothingFactor = 2.0;     // Gives more weight to smoothing than to velocity

        // Private member data
        private Point3DCollection _ptBuffer1;
        private Point3DCollection _ptBuffer2;
        private Int32Collection _triangleIndices;
        private Random _rnd = new Random(48339);

        private int _dimension;

        // Pointers to which buffers contain:
        //    - Current: Most recent data
        //    - Old: Earlier data
        // These two pointers will swap, pointing to ptBuffer1/ptBuffer2 as we cycle the buffers
        private Point3DCollection _currBuffer;
        private Point3DCollection _oldBuffer;

        /// <summary>
        /// Construct new grid of a given dimension
        /// </summary>
        ///
<param name="Dimension"></param>
        public WaveGrid(int Dimension)
        {
            if (Dimension < MinDimension)
                throw new ApplicationException(string.Format("Dimension must be at least {0}", MinDimension.ToString()));

            _ptBuffer1 = new Point3DCollection(Dimension * Dimension);
            _ptBuffer2 = new Point3DCollection(Dimension * Dimension);
            _triangleIndices = new Int32Collection((Dimension - 1) * (Dimension - 1) * 2);

            _dimension = Dimension;

            InitializePointsAndTriangles();

            _currBuffer = _ptBuffer2;
            _oldBuffer = _ptBuffer1;
        }

        /// <summary>
        /// Access to underlying grid data
        /// </summary>
        public Point3DCollection Points
        {
            get { return _currBuffer; }
        }

        /// <summary>
        /// Access to underlying triangle index collection
        /// </summary>
        public Int32Collection TriangleIndices
        {
            get { return _triangleIndices; }
        }

        /// <summary>
        /// Dimension of grid--same dimension for both X & Y
        /// </summary>
        public int Dimension
        {
            get { return _dimension; }
        }

        /// <summary>
        /// Induce new disturbance in grid at random location.  Height is
        /// PeakValue +/- Delta.  (Random value in this range)
        /// </summary>
        ///
<param name="BasePeakValue">Base height of new peak in grid</param>
        ///
<param name="PlusOrMinus">Max amount to add/sub from BasePeakValue to get actual value</param>
        ///
<param name="PeakWidth"># pixels wide, [1,4]</param>
        public void SetRandomPeak(double BasePeakValue, double Delta, int PeakWidth)
        {
            if ((PeakWidth < 1) || (PeakWidth > (_dimension / 2)))
                throw new ApplicationException("WaveGrid.SetRandomPeak: PeakWidth param must be <= half the dimension");

            int row = (int)(_rnd.NextDouble() * ((double)_dimension - 1.0));
            int col = (int)(_rnd.NextDouble() * ((double)_dimension - 1.0));

            // When caller specifies 0.0 peak, we assume always 0.0, so don't add delta
            if (BasePeakValue == 0.0)
                Delta = 0.0;

            double PeakValue = BasePeakValue + (_rnd.NextDouble() * 2 * Delta) - Delta;

            // row/col will be used for top-left corner.  But adjust, if that
            // puts us out of the grid.
            if ((row + (PeakWidth - 1)) > (_dimension - 1))
                row = _dimension - PeakWidth;
            if ((col + (PeakWidth - 1)) > (_dimension - 1))
                col = _dimension - PeakWidth;

            // Change data
            for (int ir = row; ir < (row + PeakWidth); ir++)
                for (int ic = col; ic < (col + PeakWidth); ic++)
                {
                    Point3D pt = _oldBuffer[(ir * _dimension) + ic];
                    pt.Y = pt.Y + (int)PeakValue;
                    _oldBuffer[(ir * _dimension) + ic] = pt;
                }
        }

        /// <summary>
        /// Induce wave along back edge of grid by creating large
        /// wall.
        /// </summary>
        ///
<param name="WaveHeight"></param>
        public void InduceWave(double WaveHeight)
        {
            if (_dimension >= 15)
            {
                // Just set height of a few rows of points (in middle of grid)
                int NumRows = 20;
                //double[] SineCoeffs = new double[10] { 0.156, 0.309, 0.454, 0.588, 0.707, 0.809, 0.891, 0.951, 0.988, 1.0 };

                Point3D pt;
                int StartRow = _dimension / 2;
                for (int i = (StartRow - 1) * _dimension; i < (_dimension * (StartRow + NumRows)); i++)
                {
                    int RowNum = (i / _dimension) + StartRow;
                    pt = _oldBuffer[i];
                    //pt.Y = pt.Y + (WaveHeight * SineCoeffs[RowNum]);
                    pt.Y = pt.Y + WaveHeight ;
                    _oldBuffer[i] = pt;
                }
            }
        }

        /// <summary>
        /// Leave buffers in place, but change notation of which one is most recent
        /// </summary>
        private void SwapBuffers()
        {
            Point3DCollection temp = _currBuffer;
            _currBuffer = _oldBuffer;
            _oldBuffer = temp;
        }

        /// <summary>
        /// Clear out points/triangles and regenerates
        /// </summary>
        ///
<param name="grid"></param>
        private void InitializePointsAndTriangles()
        {
            _ptBuffer1.Clear();
            _ptBuffer2.Clear();
            _triangleIndices.Clear();

            int nCurrIndex = 0;     // March through 1-D arrays

            for (int row = 0; row < _dimension; row++)
            {
                for (int col = 0; col < _dimension; col++)
                {
                    // In grid, X/Y values are just row/col numbers
                    _ptBuffer1.Add(new Point3D(col, 0.0, row));

                    // Completing new square, add 2 triangles
                    if ((row > 0) && (col > 0))
                    {
                        // Triangle 1
                        _triangleIndices.Add(nCurrIndex - _dimension - 1);
                        _triangleIndices.Add(nCurrIndex);
                        _triangleIndices.Add(nCurrIndex - _dimension);

                        // Triangle 2
                        _triangleIndices.Add(nCurrIndex - _dimension - 1);
                        _triangleIndices.Add(nCurrIndex - 1);
                        _triangleIndices.Add(nCurrIndex);
                    }

                    nCurrIndex++;
                }
            }

            // 2nd buffer exists only to have 2nd set of Z values
            _ptBuffer2 = _ptBuffer1.Clone();
        }

        /// <summary>
        /// Set height of all points in mesh to 0.0.  Also resets buffers to
        /// original state.
        /// </summary>
        public void FlattenGrid()
        {
            Point3D pt;

            for (int i = 0; i < (_dimension * _dimension); i++)
            {
                pt = _ptBuffer1[i];
                pt.Y = 0.0;
                _ptBuffer1[i] = pt;
            }

            _ptBuffer2 = _ptBuffer1.Clone();
            _currBuffer = _ptBuffer2;
            _oldBuffer = _ptBuffer1;
        }

        /// <summary>
        /// Determine next state of entire grid, based on previous two states.
        /// This will have the effect of propagating ripples outward.
        /// </summary>
        public void ProcessWater()
        {
            // Note that we write into old buffer, which will then become our
            //    "current" buffer, and current will become old.
            // I.e. What starts out in _currBuffer shifts into _oldBuffer and we
            // write new data into _currBuffer.  But because we just swap pointers,
            // we don't have to actually move data around.

            // When calculating data, we don't generate data for the cells around
            // the edge of the grid, because data smoothing looks at all adjacent
            // cells.  So instead of running [0,n-1], we run [1,n-2].

            double velocity;    // Rate of change from old to current
            double smoothed;    // Smoothed by adjacent cells
            double newHeight;
            int neighbors;

            int nPtIndex = 0;   // Index that marches through 1-D point array

            // Remember that Y value is the height (the value that we're animating)
            for (int row = 0; row < _dimension; row++)
            {
                for (int col = 0; col < _dimension; col++)
                {
                    velocity = -1.0 * _oldBuffer[nPtIndex].Y;     // row, col
                    smoothed = 0.0;

                    neighbors = 0;
                    if (row > 0)    // row-1, col
                    {
                        smoothed += _currBuffer[nPtIndex - _dimension].Y;
                        neighbors++;
                    }

                    if (row < (_dimension - 1))   // row+1, col
                    {
                        smoothed += _currBuffer[nPtIndex + _dimension].Y;
                        neighbors++;
                    }

                    if (col > 0)          // row, col-1
                    {
                        smoothed += _currBuffer[nPtIndex - 1].Y;
                        neighbors++;
                    }

                    if (col < (_dimension - 1))   // row, col+1
                    {
                        smoothed += _currBuffer[nPtIndex + 1].Y;
                        neighbors++;
                    }

                    // Will always have at least 2 neighbors
                    smoothed /= (double)neighbors;

                    // New height is combination of smoothing and velocity
                    newHeight = smoothed * SmoothingFactor + velocity;

                    // Damping
                    newHeight = newHeight * Damping;

                    // We write new data to old buffer
                    Point3D pt = _oldBuffer[nPtIndex];
                    pt.Y = newHeight;   // row, col
                    _oldBuffer[nPtIndex] = pt;

                    nPtIndex++;
                }
            }

            SwapBuffers();
        }
    }
}

That’s basically it.  If anyone is interested in getting the source code, leave a comment and I’ll take the trouble to post it somewhere.

Posted in WPF | Tagged: , , , | 4 Comments »

Simple Water Animation in WPF

Posted by Sean on 21 August, 2008

Many years ago (mid-80s), I was working at a company that had a Silicon Graphics workstation.  Among a handful of demos designed to show off the SGI machine’s high-end graphics was a simulation of wave propagation in a little wireframe mesh.  It was great fun to play with by changing the height of points in the mesh and then letting the simulation run.  And the SGI machine was fast enough that the resulting animation was just mesmerizing.

Recreating this water simulation in WPF seemed like a nice way to learn a little more about 3D graphics in WPF.  (The end result is here).

The first step was to find an algorithm that simulates wave propagation through water.  It turns out that there is a very simple algorithm that achieves the desired effect simply by taking the average height of neighboring points.  The basic algorithm is described in detail in this article on 2D Water.  The same algorithm is also described in The Water Effect Explained.

The next step is to set up the 3D viewport and its constituent elements.  I used two different directional lights, to create more contrast on the surface of the water, as well as defining both diffuse and specular material properties for the surface of the water.

Here is the relevant XAML.  Note that meshMain is the mesh that will contain the surface of the water.


        <Viewport3D Name="viewport3D1" Margin="0,8.181,0,0" Grid.Row="1">
            <Viewport3D.Camera>
                <PerspectiveCamera x:Name="camMain" Position="48 7.8 41" LookDirection="-48 -7.8 -41" FarPlaneDistance="100" UpDirection="0,1,0" NearPlaneDistance="1" FieldOfView="70">

                </PerspectiveCamera>
            </Viewport3D.Camera>
            <ModelVisual3D x:Name="vis3DLighting">
                <ModelVisual3D.Content>
                    <DirectionalLight x:Name="dirLightMain" Direction="2, -2, 0"/>
                </ModelVisual3D.Content>
            </ModelVisual3D>
            <ModelVisual3D>
                <ModelVisual3D.Content>
                    <DirectionalLight Direction="0, -2, 2"/>
                </ModelVisual3D.Content>
            </ModelVisual3D>
            <ModelVisual3D>
                <ModelVisual3D.Content>
                    <GeometryModel3D x:Name="gmodMain">
                        <GeometryModel3D.Geometry>
                            <MeshGeometry3D x:Name="meshMain" >
                            </MeshGeometry3D>
                        </GeometryModel3D.Geometry>
                        <GeometryModel3D.Material>
                            <MaterialGroup>
                                <DiffuseMaterial x:Name="matDiffuseMain">
                                    <DiffuseMaterial.Brush>
                                        <SolidColorBrush Color="DarkBlue"/>
                                    </DiffuseMaterial.Brush>
                                </DiffuseMaterial>
                                <SpecularMaterial SpecularPower="24">
                                    <SpecularMaterial.Brush>
                                        <SolidColorBrush Color="LightBlue"/>
                                    </SpecularMaterial.Brush>
                                </SpecularMaterial>
                            </MaterialGroup>
                        </GeometryModel3D.Material>
                    </GeometryModel3D>
                </ModelVisual3D.Content>
            </ModelVisual3D>
        </Viewport3D>

Next, we create a WaveGrid class that implements the basic algorithm described above.  The basic idea is that we maintain two separate buffers of mesh data—one representing the current state of the water and one the prior state.  WaveGrid stores this data in two Point3DCollection objects.  As we run the simulation, we alternate which buffer we’re writing into and attach our MeshGeometry3D.Positions property to the most recent buffer.  Note that we’re only changing the vertical height of the points—which is the Y value.

WaveGrid also builds up the triangle indices for the mesh, in an Int32Collection which will also get connected to our MeshGeometry3D.

All of the interesting stuff happens in ProcessWater.  This is where we implement the smoothing algorithm described in the articles.  Since I wanted to fully animate every point in the mesh, I processed not just the internal points that have four neighboring points, but the points along the edge of the mesh, as well.  As we add in height values of neighboring points, we keep track of how many neighbors we found, so that we can do the averaging properly.

The final value for each point is a function of both the smoothing (average height of your neighbors) and the “velocity”, which is basically—how far from equilibrium was the point during the last iteration?  We also then apply a damping factor, since waves will gradually lose their amplitude.

Here’s the complete code for the WaveGrid class:


using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows.Media;
using System.Windows.Media.Media3D;

namespace WaveSim
{
    class WaveGrid
    {
        // Constants
        const int MinDimension = 5;    
        const double Damping = 0.96;
        const double SmoothingFactor = 2.0;     // Gives more weight to smoothing than to velocity

        // Private member data
        private Point3DCollection _ptBuffer1;
        private Point3DCollection _ptBuffer2;
        private Int32Collection _triangleIndices;

        private int _dimension;

        // Pointers to which buffers contain:
        //    - Current: Most recent data
        //    - Old: Earlier data
        // These two pointers will swap, pointing to ptBuffer1/ptBuffer2 as we cycle the buffers
        private Point3DCollection _currBuffer;
        private Point3DCollection _oldBuffer;

        /// <summary>
        /// Construct new grid of a given dimension
        /// </summary>
        ///
<param name="Dimension"></param>
        public WaveGrid(int Dimension)
        {
            if (Dimension < MinDimension)
                throw new ApplicationException(string.Format("Dimension must be at least {0}", MinDimension.ToString()));

            _ptBuffer1 = new Point3DCollection(Dimension * Dimension);
            _ptBuffer2 = new Point3DCollection(Dimension * Dimension);
            _triangleIndices = new Int32Collection((Dimension - 1) * (Dimension - 1) * 2);

            _dimension = Dimension;

            InitializePointsAndTriangles();

            _currBuffer = _ptBuffer2;
            _oldBuffer = _ptBuffer1;
        }

        /// <summary>
        /// Access to underlying grid data
        /// </summary>
        public Point3DCollection Points
        {
            get { return _currBuffer; }
        }

        /// <summary>
        /// Access to underlying triangle index collection
        /// </summary>
        public Int32Collection TriangleIndices
        {
            get { return _triangleIndices; }
        }

        /// <summary>
        /// Dimension of grid--same dimension for both X & Y
        /// </summary>
        public int Dimension
        {
            get { return _dimension; }
        }

        /// <summary>
        /// Set center of grid to some peak value (high point).  Leave
        /// rest of grid alone.  Note: If dimension is even, we're not
        /// exactly at the center of the grid--no biggie.
        /// </summary>
        ///
<param name="PeakValue"></param>
        public void SetCenterPeak(double PeakValue)
        {
            int nCenter = (int)_dimension / 2;

            // Change data in oldest buffer, then make newest buffer
            // become oldest by swapping
            Point3D pt = _oldBuffer[(nCenter * _dimension) + nCenter];
            pt.Y = (int)PeakValue;
            _oldBuffer[(nCenter * _dimension) + nCenter] = pt;

            SwapBuffers();
        }

        /// <summary>
        /// Leave buffers in place, but change notation of which one is most recent
        /// </summary>
        private void SwapBuffers()
        {
            Point3DCollection temp = _currBuffer;
            _currBuffer = _oldBuffer;
            _oldBuffer = temp;
        }

        /// <summary>
        /// Clear out points/triangles and regenerates
        /// </summary>
        ///
<param name="grid"></param>
        private void InitializePointsAndTriangles()
        {
            _ptBuffer1.Clear();
            _ptBuffer2.Clear();
            _triangleIndices.Clear();

            int nCurrIndex = 0;     // March through 1-D arrays

            for (int row = 0; row < _dimension; row++)
            {
                for (int col = 0; col < _dimension; col++)
                {
                    // In grid, X/Y values are just row/col numbers
                    _ptBuffer1.Add(new Point3D(col, 0.0, row));

                    // Completing new square, add 2 triangles
                    if ((row > 0) && (col > 0))
                    {
                        // Triangle 1
                        _triangleIndices.Add(nCurrIndex - _dimension - 1);
                        _triangleIndices.Add(nCurrIndex);
                        _triangleIndices.Add(nCurrIndex - _dimension);

                        // Triangle 2
                        _triangleIndices.Add(nCurrIndex - _dimension - 1);
                        _triangleIndices.Add(nCurrIndex - 1);
                        _triangleIndices.Add(nCurrIndex);
                    }

                    nCurrIndex++;
                }
            }

            // 2nd buffer exists only to have 2nd set of Z values
            _ptBuffer2 = _ptBuffer1.Clone();
        }

        /// <summary>
        /// Determine next state of entire grid, based on previous two states.
        /// This will have the effect of propagating ripples outward.
        /// </summary>
        public void ProcessWater()
        {
            // Note that we write into old buffer, which will then become our
            //    "current" buffer, and current will become old. 
            // I.e. What starts out in _currBuffer shifts into _oldBuffer and we
            // write new data into _currBuffer.  But because we just swap pointers,
            // we don't have to actually move data around.

            // When calculating data, we don't generate data for the cells around
            // the edge of the grid, because data smoothing looks at all adjacent
            // cells.  So instead of running [0,n-1], we run [1,n-2].

            double velocity;    // Rate of change from old to current
            double smoothed;    // Smoothed by adjacent cells
            double newHeight;
            int neighbors;

            int nPtIndex = 0;   // Index that marches through 1-D point array

            // Remember that Y value is the height (the value that we're animating)
            for (int row = 0; row < _dimension ; row++)
            {
                for (int col = 0; col < _dimension; col++)
                {
                    velocity = -1.0 * _oldBuffer[nPtIndex].Y;     // row, col
                    smoothed = 0.0;

                    neighbors = 0;
                    if (row > 0)    // row-1, col
                    {
                        smoothed += _currBuffer[nPtIndex - _dimension].Y;
                        neighbors++;
                    }

                    if (row < (_dimension - 1))   // row+1, col
                    {
                        smoothed += _currBuffer[nPtIndex + _dimension].Y;
                        neighbors++;
                    }

                    if (col > 0)          // row, col-1
                    {
                        smoothed += _currBuffer[nPtIndex - 1].Y;
                        neighbors++;
                    }

                    if (col < (_dimension - 1))   // row, col+1
                    {
                        smoothed += _currBuffer[nPtIndex + 1].Y;
                        neighbors++;
                    }

                    // Will always have at least 2 neighbors
                    smoothed /= (double)neighbors;

                    // New height is combination of smoothing and velocity
                    newHeight = smoothed * SmoothingFactor + velocity;

                    // Damping
                    newHeight = newHeight * Damping;

                    // We write new data to old buffer
                    Point3D pt = _oldBuffer[nPtIndex];
                    pt.Y = newHeight;   // row, col
                    _oldBuffer[nPtIndex] = pt;

                    nPtIndex++;
                }
            }

            SwapBuffers();
        }
    }
}

Finally, we need to hook everything up.  When our main window fires up, we create an instance of WaveGrid and set the center point in the grid to some peak value.  When we start the animation, this higher point will fall and trigger the waves.

We do all of the animation in the CompositionTarget.Rendering event handler.  This is the recommended spot to do custom animations in WPF, as opposed to doing the animation in some timer Tick event.  (Windows Presentation Foundation Unleashed, Nathan, pg 470).

When you attach a handler to the Rendering event, WPF just continues rendering frames indefinitely.  One problem is that the handler will get called for every frame rendered, which turns out to be too fast for our water animation.  To get the water to look right, we keep track of the time that we last rendered a frame and then wait a specified number of milliseconds before rendering another.

Here is the full source code for Window1.xaml.cs:


using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Media3D;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
using System.Windows.Threading;

namespace WaveSim
{
    /// <summary>
    /// Interaction logic for Window1.xaml
    /// </summary>
    public partial class Window1 : Window
    {
        private Vector3D zoomDelta;

        private WaveGrid _grid;
        private bool _rendering;
        private double _lastTimeRendered;
        private double _firstPeak = 6.5;

        // Values to try:
        //   GridSize=20, RenderPeriod=125
        //   GridSize=50, RenderPeriod=50
        private const int GridSize = 50;   
        private const double RenderPeriodInMS = 50;    

        public Window1()
        {
            InitializeComponent();

            _grid = new WaveGrid(GridSize);        // 10x10 grid
            slidPeakHeight.Value = _firstPeak;
            _grid.SetCenterPeak(_firstPeak);
            meshMain.Positions = _grid.Points;
            meshMain.TriangleIndices = _grid.TriangleIndices;

            // On each WheelMouse change, we zoom in/out a particular % of the original distance
            const double ZoomPctEachWheelChange = 0.02;
            zoomDelta = Vector3D.Multiply(ZoomPctEachWheelChange, camMain.LookDirection);
        }

        private void Window_MouseWheel(object sender, MouseWheelEventArgs e)
        {
            if (e.Delta > 0)
                // Zoom in
                camMain.Position = Point3D.Add(camMain.Position, zoomDelta);
            else
                // Zoom out
                camMain.Position = Point3D.Subtract(camMain.Position, zoomDelta);
            Trace.WriteLine(camMain.Position.ToString());
        }

        // Start/stop animation
        private void btnStart_Click(object sender, RoutedEventArgs e)
        {
            if (!_rendering)
            {
                _grid = new WaveGrid(GridSize);        // 10x10 grid
                _grid.SetCenterPeak(_firstPeak);
                meshMain.Positions = _grid.Points;

                _lastTimeRendered = 0.0;
                CompositionTarget.Rendering += new EventHandler(CompositionTarget_Rendering);
                btnStart.Content = "Stop";
                slidPeakHeight.IsEnabled = false;
                _rendering = true;
            }
            else
            {
                CompositionTarget.Rendering -= new EventHandler(CompositionTarget_Rendering);
                btnStart.Content = "Start";
                slidPeakHeight.IsEnabled = true;
                _rendering = false;
            }
        }

        void CompositionTarget_Rendering(object sender, EventArgs e)
        {
            RenderingEventArgs rargs = (RenderingEventArgs)e;
            if ((rargs.RenderingTime.TotalMilliseconds - _lastTimeRendered) > RenderPeriodInMS)
            {
                // Unhook Positions collection from our mesh, for performance
                // (see http://blogs.msdn.com/timothyc/archive/2006/08/31/734308.aspx)
                meshMain.Positions = null;

                // Do the next iteration on the water grid, propagating waves
                _grid.ProcessWater();

                // Then update our mesh to use new Z values
                meshMain.Positions = _grid.Points;

                _lastTimeRendered = rargs.RenderingTime.TotalMilliseconds;
            }
        }

        private void slidPeakHeight_ValueChanged(object sender, RoutedPropertyChangedEventArgs<double> e)
        {
            _firstPeak = slidPeakHeight.Value;
            _grid.SetCenterPeak(_firstPeak);
        }
    }
}

The end result is pretty satisfying—a nice smooth animation of a series of ripples propagating out from the initial disturbance.  You can install and run the simulation by clicking here.  Note that you can zoom in/out using the mouse wheel.