4

I have to draw a lot of shapes by using Direct2D. I use a factory to create a render target I use to draw. Above these shapes I need to add others shapes without changing the previous (freehand drawing), but if I use the same render target I have to refresh the entire canvas (that is, redraw all the shapes), and this is not feasible because it is too slow.

I need I solution that allows me to draw over the static shapes without continuously clear a draw all the canvas. I thought to create a new render target, by using the same factory, but this solution does not work for me (i.e. the new shapes are not displayed on the screen).

Is there a solution that can fix this problem? As for example to draw the static shapes on a bitmap?

Nick
  • 10,309
  • 21
  • 97
  • 201
  • 1
    The solution is to draw the static shapes to an offscreen bitmap and redraw it only when the shapes change. And in your rendering function you first draw the offscreen bitmap and then the free hand drawing on top of it. What render target do you use? hwnd render target? – Anton Angelov Mar 01 '15 at 11:59
  • @AntonAngelov Yes I use ID2D1HwndRenderTarget, can you provide a solution with this type of render target? – Nick Mar 01 '15 at 12:09

1 Answers1

3

Okay.. here it is. I have no MS VS installed now so I write a simple example in Delphi. The function names are all the same, so I hope you'll catch the idea..

Here are the variables I use:

FFactory: ID2D1Factory;             //ID2D1Factory* FFactory;
FHWNDRT: ID2D1HwndRenderTarget;     //ID2D1HwndRenderTarget* FHWNDRT;
FBitmapRT: ID2D1BitmapRenderTarget; //Etc..
FBrush: ID2D1SolidColorBrush;

Here is the example:

function TForm1.InitializeD2D: HRESULT;
var
  TargetRect: TRect;
  BrushProps: D2D1_BRUSH_PROPERTIES;
begin
  { Create factory }
  Result := D2D1CreateFactory( D2D1_FACTORY_TYPE_SINGLE_THREADED, ID2D1Factory, nil, FFactory );
  If Failed(Result) then Exit;

  { Get form's client rect }
  Winapi.Windows.GetClientRect( Self.Handle, targetRect );

  { Create hwnd render target }
  Result := FFactory.CreateHwndRenderTarget(
      D2D1RenderTargetProperties( D2D1_RENDER_TARGET_TYPE_HARDWARE, D2D1PixelFormat( DXGI_FORMAT_R8G8B8A8_UNORM, D2D1_ALPHA_MODE_PREMULTIPLIED ), 96, 96, D2D1_RENDER_TARGET_USAGE_NONE, D2D1_FEATURE_LEVEL_10 ),
      D2D1HwndRenderTargetProperties( Self.Handle, D2D1SizeU( TargetRect.Width, TargetRect.Height ) ),
      FHWNDRT
  );
  If Failed(Result) then Exit;

  { Create bitmap render target }
  Result := FHWNDRT.CreateCompatibleRenderTarget(
      nil,
      nil,
      nil,
      D2D1_COMPATIBLE_RENDER_TARGET_OPTIONS_NONE,
      FBitmapRT
  );
  If Failed(Result) then Exit;

  With BrushProps do Begin
    opacity := 1;
    transform := D2D1_MATRIX_3X2_F.Identity;
  End;

  { Create brush so we can draw }
  Result := FBitmapRT.CreateSolidColorBrush( D2D1ColorF( 0, 0.25, 0.75, 1 ), @BrushProps, FBrush );
end;

function TForm1.UpdateStaticShapes: HRESULT;
begin
  //Draw the static shapes on the offscreen bitmap
  FBitmapRT.BeginDraw;

  //Clear the offscreen bitmap
  FBitmapRT.Clear( D2D1ColorF(0, 0, 0, 1) );

  //Draw a line
  FBrush.SetColor( D2D1ColorF( 1, 0, 0, 1 ) );
  FBitmapRT.DrawLine( D2D1PointF( Random(10)+10, Random(10)+10 ), D2D1PointF( 50, 50 ), FBrush );

  //Draw a filled rect
  FBrush.SetColor( D2D1ColorF( 0, 0.25, 0.75, 1 ) );
  FBitmapRT.FillRectangle( D2D1RectF( Random(50), Random(50), Random(250)+50, Random(300) + 50 ), FBrush );

  //Etc.. (draw all your shapes)

  Result := FBitmapRT.EndDraw();
end;

function TForm1.Render: HRESULT;
var
  pBitmap: ID2D1Bitmap;
begin
  FHWNDRT.BeginDraw;

  FHWNDRT.Clear( D2D1ColorF( 0, 0, 0, 1 ) );

  { Draw the offscreen bitmap }
  FBitmapRT.GetBitmap( pBitmap );

  If pBitmap <> nil then Begin
    FHWNDRT.DrawBitmap( pBitmap );
    pBitmap := nil; //Equivalent to _Release()
  End;

  { Draw the additional free hand drawing here }
  FBrush.SetColor( D2D1ColorF( 1, 1, 1, 1 ) );
  FHWNDRT.DrawRectangle( D2D1RectF( 100, 100, 200, 200 ), FBrush );

  Result := FHWNDRT.EndDraw();
end;

The offscreen bitmap is redrawn in UpdateStaticShapes() method. And the final frame is rendered in Render() method.

Edit: I tried to do zooming and understood your problem. I guess the solution is to recreate the bitmap render target every time you change the zoom factor (also every time you resize the window). The size of the offscreen bitmap should be:

  OffscreenBitmap.Width/Height = HwndRT.Width/Height * ScaleFactor;

When you are drawing the shapes you have to use coordinates relative to the offscreen bitmap size. For example: instead of drawing line (50,50, 100,100), you should draw it (50*K, 50*K, 100*K, 100*K) where K = ScaleFactor;

Here is how I create the bitmap RT:

//In C++ this should look like:
//HRESULT TForm::CreateBitmapRT(float ScaleFactor) {}
function TForm1.CreateBitmapRT(ScaleFactor: Single): HRESULT;
var
  DesiredSize: D2D1_POINT_2F;
begin
  FBitmapRT := nil; //FBitmapRT->_Release(); FBitmapRT = NULL;

  { Decide offscreen bitmap's size }
  DesiredSize := D2D1PointF( FWindowRect.Width * ScaleFactor, FWindowRect.Height * ScaleFactor );

  { Create bitmap render target }
  Result := FHWNDRT.CreateCompatibleRenderTarget(
      @DesiredSize,
      nil,
      nil,
      D2D1_COMPATIBLE_RENDER_TARGET_OPTIONS_NONE,
      FBitmapRT
  );
end;

Here is the updated Render() method:

function TForm1.Render: HRESULT;
var
  pBitmap: ID2D1Bitmap;
  SrcRect, DestRect: D2D1_RECT_F;
begin
  FHWNDRT.BeginDraw;

  FHWNDRT.Clear( D2D1ColorF( 0, 0, 0, 1 ) );

  If FBitmapRT <> nil then Begin
    { Draw the offscreen bitmap }
    FBitmapRT.GetBitmap( pBitmap );

    If pBitmap <> nil then Begin
      SrcRect := D2D1RectF( FZoomOffset.x, FZoomOffset.y, FZoomOffset.x + FWindowRect.Width, FZoomOffset.y + FWindowRect.Height );
      DestRect := D2D1RectF( 0, 0, FWindowRect.Width, FWindowRect.Height );

      FHWNDRT.DrawBitmap( pBitmap, @DestRect, 1, D2D1_BITMAP_INTERPOLATION_MODE_LINEAR, @SrcRect );
      pBitmap := nil; //pBitmap->_Release(); pBitmap = NULL;
    End;
  End;

  { Draw the additional free hand drawing here }
  FBrush.SetColor( D2D1ColorF( 1, 1, 1, 1 ) );
  FHWNDRT.DrawRectangle( D2D1RectF( 100, 100, 200, 200 ), FBrush );

  Result := FHWNDRT.EndDraw();
end;

Here is what happen if I change the zoom factor (zoom in or zoom out):

function TForm1.ApplyZoom(fScaleFactor: Single): HRESULT;
begin
  If fScaleFactor = FZoomFactor then Exit(S_OK);    
  If fScaleFactor = 0 then fScaleFactor := 0.1;

  { Recreate the offscreen bitmap }
  Result := CreateBitmapRT( fScaleFactor );
  If Failed(Result) then Exit;

  { Here you have to redraw the shapes once again }  
  Result := UpdateStaticShapes;
  If Failed(Result) then Exit;

  { I save the new Zoom Factor in a class field }
  FZoomFactor := fScaleFactor;
end;
Anton Angelov
  • 1,223
  • 7
  • 19
  • Here the problem is that the displayed bitmap is different from the anti-aliased version of the shapes drawn without using the Bitmap render target (when I use a scale transformation, that is a zoom). Is there a way to create a bitmap that appears as a windows screenshot? That is, a bitmap that looks like the anti-aliased version. – Nick Mar 01 '15 at 20:43
  • This effect is caused because your shapes are being rasterized on the bitmap. The solution in to make the offscreen bitmap bigger (2, 4 or more times bigger) so it can hold more detailed raster. – Anton Angelov Mar 01 '15 at 21:11
  • Your solution is a valid solution, but I have the following problem: http://stackoverflow.com/questions/28806029/direct2d-bitmap-brush-elongated. Please feel free to answer. Thanks. – Nick Mar 02 '15 at 09:11
  • I have edited the answer. It is not perfect, but it should do the work. Also there are limits for the size of the offscreen bitmap, so if your bitmap grow very large you have to break it down to small ones. I tested it with size up to 16320x8644 and it seemed okay. Another note is that if you make really close zoom, you can drop the offscreen bitmap and draw the shapes directly. – Anton Angelov Mar 02 '15 at 09:35