Microsoft MVP성태의 닷넷 이야기
글쓴 사람
정성태 (techsharer at outlook.com)
홈페이지
첨부 파일
(연관된 글이 1개 있습니다.)
(시리즈 글이 10개 있습니다.)
VC++: 121. DXGI를 이용한 윈도우 화면 캡처 소스 코드(Visual C++)
; https://www.sysnet.pe.kr/2/0/11385

.NET Framework: 705. C# - SharpDX + DXGI를 이용한 윈도우 화면 캡처 소스 코드
; https://www.sysnet.pe.kr/2/0/11400

.NET Framework: 706. C# - SharpDX + DXGI를 이용한 윈도우 화면 캡처 소스 코드 + Direct2D 출력
; https://www.sysnet.pe.kr/2/0/11401

.NET Framework: 712. C# - SharpDX + DXGI를 이용한 윈도우 화면 캡처 소스 코드 + Direct2D 출력 + OpenCV
; https://www.sysnet.pe.kr/2/0/11407

.NET Framework: 713. C# - SharpDX + DXGI를 이용한 윈도우 화면 캡처 소스 코드 + Direct2D 출력 + OpenCV (2)
; https://www.sysnet.pe.kr/2/0/11408

.NET Framework: 913. C# - SharpDX + DXGI를 이용한 윈도우 화면 캡처 라이브러리
; https://www.sysnet.pe.kr/2/0/12238

.NET Framework: 1123. C# - (SharpDX + DXGI) 화면 캡처한 이미지를 빠르게 JPG로 변환하는 방법
; https://www.sysnet.pe.kr/2/0/12889

.NET Framework: 1126. C# - snagit처럼 화면 캡처를 연속으로 수행해 동영상 제작
; https://www.sysnet.pe.kr/2/0/12895

.NET Framework: 1128. C# - 화면 캡처한 이미지를 ffmpeg(FFmpeg.AutoGen)로 동영상 처리
; https://www.sysnet.pe.kr/2/0/12897

.NET Framework: 1152. C# - 화면 캡처한 이미지를 ffmpeg(FFmpeg.AutoGen)로 동영상 처리 (저해상도 현상 해결)
; https://www.sysnet.pe.kr/2/0/12963




C# - SharpDX + DXGI를 이용한 윈도우 화면 캡처 소스 코드 + Direct2D 출력

지난번 소스 코드는,

C# - SharpDX + DXGI를 이용한 윈도우 화면 캡처 소스 코드
; https://www.sysnet.pe.kr/2/0/11400

DXGI로 캡처한 화면을 byte[] 배열로 받은 후 이것을 System.Drawing.Bitmap으로 다시 변환해 GDI 기반의 Graphics.DrawImage로 출력했습니다.

이번에는 DXGI로 캡처하는 것은 동일한데 이것을 DirectX 기반의 Direct2D를 이용해 직접 화면으로 출력하는 것을 해봤습니다. SharpDX의 경우 SharpDX.Direct2D1 모듈에서 Direct2D 기능을 제공하기 때문에 다음과 같이 Nuget으로부터 참조를 추가해야 합니다.

Install-Package SharpDX.Direct2D1 -Version 4.0.1 

이후 나머지 변경 코드는 출력 부분이 대다수를 차지합니다. 참고로, Direct2D의 사용법은 다음의 글들에서 자세하게 다루고 있습니다.

Your first DirectX 11 Metro application using SharpDX
; https://english.r2d2rigo.es/2012/05/28/your-first-directx-11-metro-application-using-sharpdx/

Basic Direct2D drawing with SharpDX
; https://english.r2d2rigo.es/2012/07/04/basic-direct2d-drawing-with-sharpdx/

Loading and drawing bitmaps with Direct2D using SharpDX
; https://english.r2d2rigo.es/2014/08/12/loading-and-drawing-bitmaps-with-direct2d-using-sharpdx/

또한 관련 코드는 SharpDX의 예제에서도 구할 수 있습니다.

SharpDX-Samples/Desktop/Direct2D1/BitmapApp/
; https://github.com/sharpdx/SharpDX-Samples/tree/master/Desktop/Direct2D1/BitmapApp




Direct2D로 그리기 위해서는 우선 Direct3D device 객체와 SwapChain을 구한 후 그것으로부터 2D 렌더링을 할 수 있는 RednerTarget을 구하는 순입니다. 또한 Direct3D device 객체는 Windows Forms 프로젝트의 경우 Window를 대상으로 생성하면 됩니다. 다음은 이에 관한 코드입니다.

public void Initialize(IntPtr windowHandle, int width, int height)
{
    _width = width;
    _height = height;

    var desc = new SwapChainDescription()
    {
        BufferCount = 1,
        ModeDescription = new ModeDescription(width, height,
                                    new Rational(60, 1), Format.B8G8R8A8_UNorm),
        IsWindowed = true,
        OutputHandle = windowHandle,
        SampleDescription = new SampleDescription(1, 0),
        SwapEffect = SwapEffect.Discard,
        Usage = Usage.RenderTargetOutput,
    };

    _viewPort = new SharpDX.Mathematics.Interop.RawViewportF
    {
        X = 0,
        Y = 0,
        Width = width,
        Height = height,
    };

    SharpDX.Direct3D11.Device.CreateWithSwapChain(SharpDX.Direct3D.DriverType.Hardware,
            SharpDX.Direct3D11.DeviceCreationFlags.BgraSupport,
            new[] { SharpDX.Direct3D.FeatureLevel.Level_10_0 }, desc, out _drawDevice, out _swapChain);

    _backBuffer = Texture2D.FromSwapChain<Texture2D>(_swapChain, 0);
    _backBufferView = new RenderTargetView(_drawDevice, _backBuffer);

    using (SharpDX.Direct2D1.Factory factory = new SharpDX.Direct2D1.Factory())
    {
        using (var surface = _backBuffer.QueryInterface<Surface>())
        {
            _renderTarget2D = new RenderTarget(factory, surface,
                new RenderTargetProperties(new SharpDX.Direct2D1.PixelFormat(Format.Unknown, SharpDX.Direct2D1.AlphaMode.Premultiplied)));
            _renderTarget2D.AntialiasMode = AntialiasMode.PerPrimitive;
        }
    }
}

RenderTarget을 생성했으면 이제 그리기 함수를 호출해야 하는데 이와 같은 작업은 다음과 같이 BeginDraw / EndDraw 사이에 넣게 됩니다.

public void Render(Action<RenderTarget> render)
{
    _drawDevice.ImmediateContext.Rasterizer.SetViewport(_viewPort);
    _drawDevice.ImmediateContext.OutputMerger.SetTargets(_backBufferView);

    _renderTarget2D.BeginDraw();

    render(_renderTarget2D);

    _renderTarget2D.EndDraw();

    _swapChain.Present(0, PresentFlags.None);
}

가령, Bitmap을 그리고 싶다면 SharpDX.Direct2D1.Bitmap을 생성해야 하는데,

var bitmapProperties = new BitmapProperties(new SharpDX.Direct2D1.PixelFormat(Format.B8G8R8A8_UNorm,
                    SharpDX.Direct2D1.AlphaMode.Premultiplied));

if (_renderTarget2D == null)
{
    return null;
}

SharpDX.Direct2D1.Bitmap bitmap = new SharpDX.Direct2D1.Bitmap(_renderTarget2D, new Size2(_width, _height), stream,
            _width * sizeof(int), bitmapProperties);

이에 앞서 Bitmap에 전달되는 DataStream 타입의 stream 객체에 이미지 데이터가 채워져 있어야 합니다. DXGI 화면 캡처의 경우, AcquireNextFrame/Map으로 구하게 되는 버퍼로부터 DataStream의 PositionPointer 주소로 내용을 복사하면 됩니다. 일단 Bitmap만 제대로 구성하게 되면 이후 화면 출력은 RenderTarget의 DrawBitmap 메서드를 이용해 다음과 같이 간단하게 출력할 수 있습니다.

_renderTarget2D.DrawBitmap(bitmap, 1.0f, BitmapInterpolationMode.Linear);




그런데, RenderTarget.DrawBitmap으로 그리면 대상이 되는 Window의 크기에 딱 맞게 Width/Height가 스케일링되어 출력이 됩니다. 즉, 1920 * 1080 화면을 캡처했는데 Direct3D device 객체와 연결된 Window의 크기가 300 * 200이면 다음과 같은 식으로 창 크기에 맞춰 나오기 때문에 화면이 알아볼 수 없을 정도로 축소가 됩니다.

dxgi_direct2d_1.png

축소를 원하지 않고 그냥 다음과 같은 식으로 출력하고 싶은데... 그 방법을 모르겠습니다. ^^; (혹시 아시는 분은 덧글 부탁드립니다.)

dxgi_direct2d_2.png

위의 문제를 해결하기 위해 할 수 없이 Window 안에 화면 캡처 크기 만큼의 자식 컨트롤을 넣는 것으로 우회했습니다.

또 한가지 문제는, Direct3D device가 Window와 연결되기 때문에 Control.Invalidate 메서드를 이용해 출력 지시를 하는 경우 깜빡임 문제가 발생한다는 것입니다. 왜냐하면, Window는 OnPaint로 화면을 그리기 전에 해당 화면을 전부 윈도우 기본 배경색으로 지우는 작업을 하기 때문입니다. 따라서, 매끄러운 화면 출력을 위해서는 지우는 작업을 없애야 하는데 이는 다음과 같이 OnPaintBackground를 재정의하는 것으로 가능합니다.

protected override void OnPaintBackground(PaintEventArgs e)
{
    // base.OnPaintBackground(e);
}

마지막으로, 알아야 할 것이 컬러 데이터의 저장 순서입니다. 캡처 데이터 원본은 B-G-R-A 순으로 픽셀 하나에 대해 4바이트가 연속하는 구조입니다. 재미있는 것은 이것이 Little-endian 구조의 CPU(인텔/AMD)에서는 해당 메모리를 int (4바이트로)로 읽어내면 역순이 되어 0xAARRGGBB 순으로 배열된다는 것입니다.

지난 글에서 DXGI 화면 캡처 데이터를 GDI로 출력하면서 System.Drawing.Bitmap을 이용했는데, System.Drawing.Bitmap의 내부 데이터 저장 순서가 바로 B-G-R-A순이기 때문에 그대로 복사하는 것으로 컬러 데이터를 유지할 수 있습니다.

그런데, Direct2D의 BitmapProperties는 이 순서를 정할 수 있습니다. 만약에 다음과 같이 R-G-B-A 순으로 저장하도록 SharpDX.Direct2D1.Bitmap을 생성한다면,

var bitmapProperties = new BitmapProperties(new SharpDX.Direct2D1.PixelFormat(Format.R8G8B8A8_UNorm,
                                SharpDX.Direct2D1.AlphaMode.Premultiplied));

화면 캡처 데이터가 B-G-R-A 순이기 때문에 데이터 순서를 R-G-B-A로 맞춰주기 위해 다음과 같이 전체 데이터를 순회하면서 직접 써야 합니다.

for (int y = 0; y < _renderTarget.Height; y++)
{
    for (int x = 0; x < _renderTarget.Width; x++)
    {
        IntPtr dstPixel = dstPtr + x * 4;
        IntPtr srcPixel = srcPtr + x * 4;

        // B-G-R-A 순의 바이트를 하나씩 읽어서,
        byte B = Marshal.ReadByte(srcPixel + 0);
        byte G = Marshal.ReadByte(srcPixel + 1);
        byte R = Marshal.ReadByte(srcPixel + 2);
        byte A = Marshal.ReadByte(srcPixel + 3);

        // SharpDX.Direct2D1.Bitmap의 R-G-B-A순으로 변환해서 저장
        Marshal.WriteByte(dstPixel + 0, R);
        Marshal.WriteByte(dstPixel + 1, G);
        Marshal.WriteByte(dstPixel + 2, B);
        Marshal.WriteByte(dstPixel + 3, A);

        /*
        // 1바이트씩 쓰기보다 다음과 같이 4바이트씩 쓰면 좀 더 효율적임
        int xabgr = R | (G << 8) | (B << 16) | (A << 24);
        Marshal.WriteInt32(dstPixel, xabgr); // little-endian 방식으로 저장되므로 aa-bb-gg-rr 4바이트가 rr-gg-bb-aa 순으로 저장됨
        */
    }

    srcPtr = IntPtr.Add(srcPtr, srcPitch);
    dstPtr = IntPtr.Add(dstPtr, _renderTarget.Width * 4);
}

실제로 아래의 소스 코드를 보면,

SharpDX-Samples/Desktop/Direct2D1/BitmapApp/Program.cs 
; https://raw.githubusercontent.com/sharpdx/SharpDX-Samples/master/Desktop/Direct2D1/BitmapApp/Program.cs

LoadFromFile 메서드에서 B-G-R-A로 저장된 데이터를 y*x for 루프를 돌면서 R-G-B-A로 변환하는 것을 볼 수 있습니다.

물론, 위와 같이 하면 블록 단위의 메모리 복사보다 속도가 더 느립니다. 따라서 화면 캡처 데이터 순서가 B-G-R-A이기 때문에 SharpDX.Direct2D1.Bitmap도 그 순서에 맞게 생성하면,

var bitmapProperties = new BitmapProperties(new SharpDX.Direct2D1.PixelFormat(Format.B8G8R8A8_UNorm,
                                SharpDX.Direct2D1.AlphaMode.Premultiplied));

좀 더 빠르게 블록 단위 복사가 가능합니다.

(첨부 파일은 이 글의 코드를 구현합니다.)




[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]

[연관 글]






[최초 등록일: ]
[최종 수정일: 6/27/2021]

Creative Commons License
이 저작물은 크리에이티브 커먼즈 코리아 저작자표시-비영리-변경금지 2.0 대한민국 라이센스에 따라 이용하실 수 있습니다.
by SeongTae Jeong, mailto:techsharer at outlook.com

비밀번호

댓글 작성자
 



2021-01-06 03시55분
성태님 !!!!

저거 화면 확장 기능 말고 하시려면

RenderTarget.Resize 기능 이용하셔서... 실시간 Client.Size 바인딩 해주시면 확장 안되고 잘 나온답니다 ^^
최선호
2021-01-06 04시11분
덧글 감사드립니다. ^^ 나중에 다시 이 코드를 다루게 되면 꼭 적용해 보겠습니다.
정성태

... 16  17  18  19  20  21  22  23  24  25  26  27  [28]  29  30  ...
NoWriterDateCnt.TitleFile(s)
12928정성태1/18/20226958개발 환경 구성: 629. AKS/Kubernetes에서 호스팅 중인 pod에 shell(/bin/bash)로 진입하는 방법
12927정성태1/18/20226731개발 환경 구성: 628. AKS 환경에 응용 프로그램 배포 방법
12926정성태1/17/20227207오류 유형: 787. AKS - pod 배포 시 ErrImagePull/ImagePullBackOff 오류
12925정성태1/17/20227299개발 환경 구성: 627. AKS의 준비 단계 - ACR(Azure Container Registry)에 docker 이미지 배포
12924정성태1/15/20228802.NET Framework: 1134. C# - ffmpeg(FFmpeg.AutoGen)를 이용한 비디오 디코딩 예제(decode_video.c) [2]파일 다운로드1
12923정성태1/15/20227720개발 환경 구성: 626. ffmpeg.exe를 사용해 비디오 파일을 MPEG1 포맷으로 변경하는 방법
12922정성태1/14/20226797개발 환경 구성: 625. AKS - Azure Kubernetes Service 생성 및 SLO/SLA 변경 방법
12921정성태1/14/20225770개발 환경 구성: 624. Docker Desktop에서 별도 서버에 설치한 docker registry에 이미지 올리는 방법
12920정성태1/14/20226522오류 유형: 786. Camtasia - An error occurred with the camera: Failed to Add Video Sampler.
12919정성태1/13/20226335Windows: 199. Host Network Service (HNS)에 의해서 점유되는 포트
12918정성태1/13/20226582Linux: 47. WSL - shell script에서 설정한 환경 변수가 스크립트 실행 후 반영되지 않는 문제
12917정성태1/12/20225789오류 유형: 785. C# - The type or namespace name '...' could not be found (are you missing a using directive or an assembly reference?)
12916정성태1/12/20225521오류 유형: 784. TFS - One or more source control bindings for this solution are not valid and are listed below.
12915정성태1/11/20225798오류 유형: 783. Visual Studio - We didn't find any interpreters
12914정성태1/11/20227751VS.NET IDE: 172. 비주얼 스튜디오 2022의 파이선 개발 환경 지원
12913정성태1/11/20228264.NET Framework: 1133. C# - byte * (바이트 포인터)를 FileStream으로 쓰는 방법 [1]
12912정성태1/11/20228897개발 환경 구성: 623. ffmpeg.exe를 사용해 비디오 파일의 이미지를 PGM(Portable Gray Map) 파일 포맷으로 출력하는 방법 [1]
12911정성태1/11/20226213VS.NET IDE: 171. 비주얼 스튜디오 - 더 이상 만들 수 없는 "ASP.NET Core 3.1 Web Application (.NET Framework)" 프로젝트
12910정성태1/10/20226691제니퍼 .NET: 30. 제니퍼 닷넷 적용 사례 (8) - CPU high와 DB 쿼리 성능에 문제가 함께 있는 사이트
12909정성태1/10/20228092오류 유형: 782. Visual Studio 2022 설치 시 "Couldn't install Microsoft.VisualCpp.Redist.14.Latest"
12908정성태1/10/20225942.NET Framework: 1132. C# - ref/out 매개변수의 IL 코드 처리
12907정성태1/9/20226402오류 유형: 781. (youtube-dl.exe) 실행 시 "This app can't run on your PC" / "Access is denied." 오류 발생
12906정성태1/9/20227018.NET Framework: 1131. C# - 네임스페이스까지 동일한 타입을 2개의 DLL에서 제공하는 경우 충돌을 우회하는 방법 [1]파일 다운로드1
12905정성태1/8/20226677오류 유형: 780. Could not load file or assembly 'Microsoft.VisualStudio.TextTemplating.VSHost.15.0, Version=16.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a' or one of its dependencies.
12904정성태1/8/20228694개발 환경 구성: 623. Visual Studio 2022 빌드 환경을 위한 github Actions 설정 [1]
12903정성태1/7/20227284.NET Framework: 1130. C# - ELEMENT_TYPE_INTERNAL 유형의 사용 예
... 16  17  18  19  20  21  22  23  24  25  26  27  [28]  29  30  ...