Share via


Doppler Three

The Doppler waves program has two bits of UI really: the "dynamic" wave drawing part, and the user interaction part, namely the slider. I could recreate a slider myself, or use some other interaction mechanism in my C++ DirectX variant of the program - the slider's not a hugely complex bit of UI. Or I could keep using standard Windows controls as far as possible, with a DirectX pane for the other...

The starting point for a Direct2D app is, not surprisingly, the "Direct 2D (XAML)" project template in Visual Studio. This creates a fairly simple project combining XAML and a DirectX surface, which displays a couple of "Hello" text lines, one created in XAML and one in DirectX code. The project creates the basic application framework I'm going to use almost as-is, and includes some useful base classes and helpers. Starting from the outside, the DirectXPage.xaml/.h/.cpp files define the window frame. Replace the XAML file body with the following to place the slider on the screen.

 <SwapChainBackgroundPanel x:Name="SwapChainPanel">
    <Slider x:Name="SliderSpeed" VerticalAlignment="Bottom" Margin="10" Orientation="Horizontal"
  ValueChanged="SliderSpeed_ValueChanged"
                Minimum="0" Maximum="1" LargeChange="0.1" SmallChange="0.05"
 IsThumbToolTipEnabled="False" />
</SwapChainBackgroundPanel>

Well, that's defined the whole UI - everything else happens in code. In the codebehind file, remove all the text related material, and add the slider value change handler (call m_renderer->SetSpeed(SliderSpeed->Value), which I'll define later. Since this app continuously updates the screen, there's no need for the render-needed optimisation, so that can be thrown away, leaving two interesting calls to m_renderer: Update (intended to set positions, etc. ready for drawing) and Render (actually does the drawing). The rest of DirectXPage.xaml.cpp looks after screen orientation changes and size changes (such as that resulting from snapping), and state saving and restoring (I'll ignore that for the time being).

As with the C# version, the core engine looks after positioning a moving spot, and maintains a list of growing "waves" - however, we don't have convenient animation classes to take care of the wave growth, so I need to do that myself. Switching focus to the innermost part of the source, I have a very simple Wave definition class, with its own Update and Render methods:

 class WaveDefinition
{
public:
  WaveDefinition(float x, float y, float t) : m_x(x), m_y(y), m_t(t), m_radius(0), m_thickness(0), m_alpha(0.7f)
 {}
 
 bool Update(float time);
    void Render(struct ID2D1RenderTarget* context);
 
private:
 float m_x, m_y, m_t;
    float m_radius, m_thickness, m_alpha;
};

Update, er, updates those last three member variables, doing something vaguely similar to the animations in the first C# program, and returns a boolean value indicating when the wave has finally faded away:

 bool WaveDefinition::Update(float time)
{
    float liveTime = time - m_t;
 m_radius = liveTime * 0.25f;
    m_thickness = liveTime * 0.005f + 0.01f;
  if(liveTime > 1.0f)
       m_alpha = 1.7f - liveTime;
  return m_alpha > 0.0f;
}

Render, not surprisingly, draws the wave - actually, it draws two (for the reason explained in the first article):

 void WaveDefinition::Render(ID2D1RenderTarget* context)
{
    D2D1_ELLIPSE ellipse = D2D1::Ellipse(D2D1::Point2F(m_x, m_y), m_radius, m_radius);
 
  Microsoft::WRL::ComPtr<ID2D1SolidColorBrush> brush;
    DX::ThrowIfFailed(context->CreateSolidColorBrush(D2D1::ColorF(~0, m_alpha), &brush));
 
   context->DrawEllipse(&ellipse, brush.Get(), m_thickness);
 ellipse.point.x -= 2.0f;
  context->DrawEllipse(&ellipse, brush.Get(), m_thickness);
}

Note that I've normalised the space on which the waves are drawn to be a rectangle 2 units by 1, hence the subtraction of 2 to position the phantom wave.

Having talked about the top and the bottom of the stack, it's time to take a look at the middle. WaveRenderer.h/.cpp started off as the project template's SimpleTextRenderer, then I threw away the text and color parts, and added the waves.

 The DirectXBase class, from which my WaveRenderer is derived, splits initialisation into three phases - items which are device independent, items that are device specific, and items that are sensitive to screen size and orientation. The idea is that, for example, items that are independent of screen size don't need to be reinitialised when the screen orientation is changed. In my initial version of this app, without any custom shader effect (even though that's the reason I'm delving into DirectX - but small steps to begin with...) I have only one device specific item, the red brush used to draw the moving spot.

 void WavesRenderer::CreateDeviceResources()
{
   DirectXBase::CreateDeviceResources();
 
 DX::ThrowIfFailed(
      m_d2dContext->CreateSolidColorBrush(ColorF(ColorF::Red), &m_ellipseBrush)
     );
}

There's a little bit more going on in the size dependent setup - as I said earlier, I've normalised the drawing space to a 2x1 rectangle, and the work here is to calculate the scale factor and positioning. Note that there is no need to do any processing for the elements defined in XAML as that layout is taken care of us by the framework.

 void WavesRenderer::CreateWindowSizeDependentResources()
{
  DirectXBase::CreateWindowSizeDependentResources();
 
    // Map 0 - 2, -0.5 - 0.5 to fit the screen
 float sw = m_window->Bounds.Width, sh = m_window->Bounds.Height;
   float xScale = sw / 2.0f;
 float yScale = sh / 1.0f;
 m_scale = xScale < yScale ? xScale : yScale;
 
    float w = m_scale * 2.0f;
 float h = m_scale * 1.0f;
 
    float xo = (sw - w) * 0.5f;
 float yo = (sh - h) * 0.5f;
 
    Matrix3x2F scaleTransform = Matrix3x2F::Scale(m_scale, m_scale);
  Matrix3x2F translateTransform = Matrix3x2F::Translation(xo, yo + h * 0.5f);
   m_outputTransform2D = scaleTransform * translateTransform * m_orientationTransform2D;
 m_d2dContext->SetTransform(m_outputTransform2D);
}

WaveRenderer has a few extra member variables, some of which have been mentioned earlier - for completeness, here they all are:

   Microsoft::WRL::ComPtr<ID2D1SolidColorBrush> m_ellipseBrush; // used to pain spot
  float m_x; // position of the spot (y is constant)
float m_scale; // scale factor between 2x1 canvas and screen
   D2D1::Matrix3x2F m_outputTransform2D; // full transformation between 2x1 and screen
   float m_speed; // speed value from slider
 float m_newWaveTime; // when to create a new wave

Moving on to the Update and Render pair, Update asks all the existing WaveDefinitions to update themselves - any which would become invisible are thrown away; the spot is moved by an amount dependent on m_speed, and if it's time to create another wave, a new WaveDefinition is created.

 void WavesRenderer::Update(float timeTotal, float timeDelta)
{
   std::vector<WaveDefinition> newWaves;
  for (auto i = m_waves.begin(), end = m_waves.end(); i != end; ++i)
      if (i->Update(timeTotal))
          newWaves.push_back(*i);
 m_waves=newWaves;
 
  m_x += timeDelta * m_speed;
    if(m_x > 2)
     m_x -= 2;
 
  if (timeTotal > m_newWaveTime)
   {
       m_waves.push_back(WaveDefinition(m_x, 0.0f, timeTotal));
        if (m_newWaveTime==0.0f)
           m_newWaveTime = timeTotal;
       m_newWaveTime += 0.4f;
    }
}

Incidentally, the time values here come from the project template helper classes, and have seconds as the unit.

Having got all our ducks lined up, Render is pretty simple:

 void WavesRenderer::Render()
{
  m_d2dContext->BeginDraw();
  
 m_d2dContext->Clear(ColorF(0));
 
    for (auto i = m_waves.begin(), end = m_waves.end(); i != end; ++i)
  i->Render(m_d2dContext.Get());
 
    D2D1_ELLIPSE ellipse = D2D1::Ellipse(D2D1::Point2F(m_x, 0.0f), 0.01f, 0.01f);
    m_d2dContext->FillEllipse(&ellipse, m_ellipseBrush.Get());
 
 // Ignore D2DERR_RECREATE_TARGET. This error indicates that the device
 // is lost. It will be handled during the next call to Present.
    HRESULT hr = m_d2dContext->EndDraw();
    if (hr != D2DERR_RECREATE_TARGET)
  {
       DX::ThrowIfFailed(hr);
  }
}

That's it for today - I'll add the shader effect next time.