You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

基于C#实现像素艺术游戏实时去像素化窗口叠加方案咨询

Building a Real-Time Depixelization Overlay Tool for Pixel Art Games in C#

Hey there! Let's walk through how to create that real-time depixelization tool you want—one that works for both DOSBox classics and modern pixel art games, with an overlay effect just like Nvidia Freestyle (no extra window required) and hits that 30fps target.

Core Approach: Layered Window Overlay

Instead of capturing to a new window, we'll use a layered transparent window that sits directly on top of your target game window. This makes the filter feel like it's part of the original game, not a separate app. Here's how to pull it off:

1. Efficient Window Capture

The CaptureWindow method you mentioned works, but it's not the fastest option. For real-time performance, use PrintWindow (from user32.dll) to capture only the game window's client area, which cuts down on unnecessary processing.

First, import the required Win32 functions:

using System.Runtime.InteropServices;
using System.Drawing;

[DllImport("user32.dll")]
private static extern bool GetClientRect(IntPtr hWnd, out RECT lpRect);

[DllImport("user32.dll")]
private static extern bool PrintWindow(IntPtr hWnd, IntPtr hdcBlt, uint nFlags);

[StructLayout(LayoutKind.Sequential)]
private struct RECT
{
    public int Left;
    public int Top;
    public int Right;
    public int Bottom;
}

Then implement the capture function:

private Bitmap CaptureGameWindow(IntPtr gameWindowHandle)
{
    GetClientRect(gameWindowHandle, out RECT clientRect);
    int width = clientRect.Right - clientRect.Left;
    int height = clientRect.Bottom - clientRect.Top;

    // Create a bitmap to hold the captured frame
    Bitmap capturedFrame = new Bitmap(width, height);
    using (Graphics g = Graphics.FromImage(capturedFrame))
    {
        IntPtr hdc = g.GetHdc();
        // Capture the window's client area
        PrintWindow(gameWindowHandle, hdc, 0);
        g.ReleaseHdc(hdc);
    }
    return capturedFrame;
}

2. Create the Layered Overlay Window

A layered window lets us draw transparent content directly over the game. Use CreateWindowEx to set up the overlay, then UpdateLayeredWindow to refresh it with processed frames.

Add more Win32 imports:

[DllImport("user32.dll", SetLastError = true)]
private static extern IntPtr CreateWindowEx(
    uint dwExStyle, string lpClassName, string lpWindowName,
    uint dwStyle, int x, int y, int nWidth, int nHeight,
    IntPtr hWndParent, IntPtr hMenu, IntPtr hInstance, IntPtr lpParam);

[DllImport("user32.dll")]
private static extern bool UpdateLayeredWindow(
    IntPtr hwnd, IntPtr hdcDst, ref POINT pptDst, ref SIZE psize,
    IntPtr hdcSrc, ref POINT pprSrc, uint crKey, ref BLENDFUNCTION pblend, uint dwFlags);

[StructLayout(LayoutKind.Sequential)]
private struct POINT { public int X; public int Y; }

[StructLayout(LayoutKind.Sequential)]
private struct SIZE { public int CX; public int CY; }

[StructLayout(LayoutKind.Sequential)]
private struct BLENDFUNCTION
{
    public byte BlendOp;
    public byte BlendFlags;
    public byte SourceConstantAlpha;
    public byte AlphaFormat;
}

private const uint WS_EX_LAYERED = 0x80000;
private const uint WS_EX_TRANSPARENT = 0x20;
private const uint WS_POPUP = 0x80000000;
private const byte AC_SRC_OVER = 0;
private const byte AC_SRC_ALPHA = 1;
[DllImport("user32.dll")]
private static extern bool ShowWindow(IntPtr hWnd, int nCmdShow);
[DllImport("user32.dll")]
private static extern bool GetWindowRect(IntPtr hWnd, out RECT lpRect);

Create the overlay window:

private IntPtr CreateOverlayWindow(IntPtr gameWindowHandle, int width, int height)
{
    // Get the game window's position to place the overlay correctly
    GetWindowRect(gameWindowHandle, out RECT windowRect);
    
    // Create a layered, transparent popup window
    IntPtr overlayHandle = CreateWindowEx(
        WS_EX_LAYERED | WS_EX_TRANSPARENT,
        "Static", "", WS_POPUP,
        windowRect.Left, windowRect.Top, width, height,
        gameWindowHandle, IntPtr.Zero, IntPtr.Zero, IntPtr.Zero);
    
    // Show the overlay
    ShowWindow(overlayHandle, 5); // SW_SHOW
    return overlayHandle;
}

3. Real-Time Processing & Overlay Update

To hit 30fps, use a background task with frame timing control. Process each captured frame, then update the overlay:

using System.Threading.Tasks;
using System.Diagnostics;

public void StartRealTimeProcessing(IntPtr gameWindowHandle)
{
    GetClientRect(gameWindowHandle, out RECT clientRect);
    IntPtr overlayHandle = CreateOverlayWindow(gameWindowHandle, clientRect.Right - clientRect.Left, clientRect.Bottom - clientRect.Top);

    // Run processing in a background thread
    Task.Run(async () =>
    {
        const int targetFps = 30;
        int msPerFrame = 1000 / targetFps;

        while (true)
        {
            Stopwatch stopwatch = Stopwatch.StartNew();

            // Capture, process, and update
            using (Bitmap captured = CaptureGameWindow(gameWindowHandle))
            using (Bitmap depixelized = DepixelizeFrame(captured))
            {
                UpdateOverlay(overlayHandle, depixelized);
            }

            // Maintain target FPS by waiting for remaining time
            int elapsedMs = (int)stopwatch.ElapsedMilliseconds;
            if (elapsedMs < msPerFrame)
            {
                await Task.Delay(msPerFrame - elapsedMs);
            }
        }
    });
}

// Basic depixelization using bilinear interpolation (adjust for fancier algorithms)
private Bitmap DepixelizeFrame(Bitmap source)
{
    // Scale up the image to smooth pixel edges
    int scaledWidth = source.Width * 2;
    int scaledHeight = source.Height * 2;
    Bitmap result = new Bitmap(scaledWidth, scaledHeight);

    using (Graphics g = Graphics.FromImage(result))
    {
        g.InterpolationMode = System.Drawing.Drawing2D.InterpolationMode.Bilinear;
        g.DrawImage(source, new Rectangle(0, 0, scaledWidth, scaledHeight), new Rectangle(0, 0, source.Width, source.Height), GraphicsUnit.Pixel);
    }
    return result;
}

// Update the overlay with the processed frame
private void UpdateOverlay(IntPtr overlayHandle, Bitmap frame)
{
    using (Graphics g = Graphics.FromImage(frame))
    {
        IntPtr hdcSrc = g.GetHdc();
        POINT srcPoint = new POINT();
        SIZE frameSize = new SIZE { CX = frame.Width, CY = frame.Height };
        POINT dstPoint = new POINT();

        BLENDFUNCTION blend = new BLENDFUNCTION
        {
            BlendOp = AC_SRC_OVER,
            SourceConstantAlpha = 255, // Full opacity
            AlphaFormat = AC_SRC_ALPHA
        };

        UpdateLayeredWindow(overlayHandle, IntPtr.Zero, ref dstPoint, ref frameSize, hdcSrc, ref srcPoint, 0, ref blend, 2); // ULW_ALPHA
        g.ReleaseHdc(hdcSrc);
    }
}

4. Performance Optimizations for 30fps

  • Reuse Bitmaps: Instead of creating a new Bitmap every frame, pre-allocate buffers and reuse them to reduce GC overhead.
  • LockBits for Pixel Manipulation: For custom depixelization algorithms, use Bitmap.LockBits to access pixel data directly (faster than GetPixel/SetPixel).
  • SIMD Acceleration: Use System.Numerics.Vector to parallelize pixel processing operations.
  • Window Follow: Add a hook to detect when the game window moves/resizes, then adjust the overlay's position/size with SetWindowPos.

5. DOSBox Specific Considerations

DOSBox uses its own rendering pipeline, so PrintWindow might not capture the frame correctly in some cases. If this happens:

  • Try using the Desktop Duplication API (via DirectX) to capture the entire screen, then crop to the DOSBox window's area.
  • Check if your DOSBox version enables output=direct3d—this makes capturing more reliable.

Final Notes

This layered window approach gives you that seamless overlay feel just like Nvidia Freestyle. You can tweak the DepixelizeFrame method to use more advanced algorithms (like AI-based upscaling) if you have the processing power, but bilinear interpolation will get you real-time performance easily.

内容的提问来源于stack exchange,提问作者Ultra

火山引擎 最新活动