UE交换链默认使用的反转呈现模式,且并未提供更改交换链呈现模式的选项,因此窗口不支持SetLayeredWindowAttributes函数的键透明效果。
(这一点很多unity相关教程上都有提及过需要取消勾选DXGI filp model swapchain选项)
下面说一下代码实现具体思路:
UE创建的窗口不适合分层窗口的键透明效果,那么可以创建一个新窗口,将UE窗口的渲染结果呈现到新窗口上。
UE提供了相应的委托函数,可以通过该函数获取帧渲染结果,然后通过DC绘制到新创建的窗口上。
需要注意,新窗口需要在单独线程上运行,否则会争夺UE窗口的窗口消息,导致UE窗口不再更新。
C++代码:
(因为测试缘故,代码中可能包含了许多不必要的代码,且注释简略,具体函数用途请自行查阅)
(注:添加了托盘窗口相关代码,方便退出程序,自行更改托盘窗口的ico图标路径,或者注释掉)
(注:建议测试时,使用"独立进程的窗口"运行,且阅读完代码下方文章)
// Fill out your copyright notice in the Description page of Project Settings.
using UnrealBuildTool;
public class Game : ModuleRules
{
public Game(ReadOnlyTargetRules Target) : base(Target)
{
PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs;
PublicDependencyModuleNames.AddRange(new string[] { "Core", "CoreUObject", "Engine", "InputCore", "Slate", "SlateCore", "ApplicationCore", "Renderer", "RenderCore", "RHI", "ImageWrapper" });
PrivateDependencyModuleNames.AddRange(new string[] { });
// Uncomment if you are using Slate UI
// PrivateDependencyModuleNames.AddRange(new string[] { "Slate", "SlateCore" });
// Uncomment if you are using online features
// PrivateDependencyModuleNames.Add("OnlineSubsystem");
// To include OnlineSubsystemSteam, add it to the plugins section in your uproject file with the Enabled attribute set to true
}
}
// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
#include "Engine.h"
#include "Misc/FileHelper.h"
#include "Misc/Paths.h"
#include "HAL/PlatformFilemanager.h"
#include <windows/WindowsWindow.h>
#include "Windows/AllowWindowsPlatformTypes.h"
#include <Windows.h>
#include"winuser.h"
#include <shellapi.h>
#include <shellapi.h>
#include "Windows/HideWindowsPlatformTypes.h"
#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "MyActor.generated.h"
UCLASS()
class GAME_API AMyActor : public AActor
{
GENERATED_BODY()
public:
// Sets default values for this actor's properties
AMyActor();
protected:
// Called when the game starts or when spawned
virtual void BeginPlay() override;
virtual void Tick(float DeltaTime) override;
public:
HWND hwnd;
static HWND UEhwnd; //游戏窗口句柄
static NOTIFYICONDATA nid; //菜单结构体
TArray<FColor>outData; //渲染数据
//窗口消息函数
static LRESULT CALLBACK WndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam);
//渲染回调函数
void GetRenderBuffer(SWindow& SlateWindow, const FTexture2DRHIRef& BackBuffer);
//创建窗口
void CreateAndRunWindow();
};
// Fill out your copyright notice in the Description page of Project Settings.
#include "MyActor.h"
HWND AMyActor::UEhwnd = nullptr;
NOTIFYICONDATA AMyActor::nid;
// ets default values
AMyActor::AMyActor()
{
PrimaryActorTick.bCanEverTick = true;
}
void AMyActor::Tick(float DeltaTime)
{
Super::Tick(DeltaTime);
}
short left=0;
//窗口消息处理函数
LRESULT AMyActor::WndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
{
SendNotifyMessageW(UEhwnd, message, wParam, lParam);
switch (message)
{
case WM_PAINT:
break;
//托盘图标消息
case WM_USER + 1:
if (lParam == WM_RBUTTONUP)
{
HMENU hMenu = CreatePopupMenu();
AppendMenu(hMenu, MF_STRING, 1001, TEXT("return"));
POINT pt;
GetCursorPos(&pt);
SetForegroundWindow(hwnd);
TrackPopupMenu(hMenu, TPM_LEFTALIGN | TPM_BOTTOMALIGN, pt.x, pt.y, 0, hwnd, NULL);
PostMessageW(hwnd, WM_NULL, 0, 0);
DestroyMenu(hMenu);
}
break;
//菜单消息
case WM_COMMAND:
switch (LOWORD(wParam))
{
case 1001:
Shell_NotifyIcon(NIM_DELETE, &nid);
FPlatformMisc::RequestExit(false);
break;
}
case WM_DESTROY:
PostQuitMessage(0);
break;
//游标消息
case WM_SETCURSOR:
SetCursor(LoadCursor(NULL, IDC_ARROW));
break;
default:
return DefWindowProc(hwnd, message, wParam, lParam);
}
return 0;
}
#include <thread>
void AMyActor::BeginPlay()
{
Super::BeginPlay();
//创建线程
std::thread t(&AMyActor::CreateAndRunWindow,this);
t.detach();
//获取UE窗口句柄
TSharedPtr<FGenericWindow> NativeWindow = GEngine->GameViewport->GetWindow()->GetNativeWindow();
FWindowsWindow* Window = static_cast<FWindowsWindow*>(NativeWindow.Get());
UEhwnd = Window->GetHWnd();
//设置窗口
SetWindowPos(UEhwnd, HWND_TOP, 0, 0, GetSystemMetrics(SM_CXSCREEN), GetSystemMetrics(SM_CYSCREEN), SWP_HIDEWINDOW);
SetWindowLong(hwnd, GWL_EXSTYLE, GetWindowLongPtr(hwnd, GWL_EXSTYLE) | WS_EX_TOOLWINDOW|WS_EX_TRANSPARENT);
SetWindowLong(hwnd, GWL_STYLE, WS_VISIBLE | WS_POPUP);
//绑定委托
FSlateApplication::Get().GetRenderer()->OnBackBufferReadyToPresent().AddUObject(this, &AMyActor::GetRenderBuffer);
}
#include "Kismet/KismetSystemLibrary.h"
//回调函数,返回帧渲染结果,以及其对应的窗口句柄
void AMyActor::GetRenderBuffer(SWindow& SlateWindow, const FTexture2DRHIRef& BackBuffer)
{
//窗口有效,且为主窗口
if (!GEngine->GameViewport)return;
TSharedPtr<SWindow> W = GEngine->GameViewport->GetWindow();
if (&SlateWindow != W.Get())return;
//获取渲染结果表面宽高
int Swidth = BackBuffer->GetSizeX();
int Sheight = BackBuffer->GetSizeY();
//提取渲染结果
FIntRect Rect(0, 0, Swidth, Sheight);
FRHICommandListImmediate& RHICmdList = FRHICommandListExecutor::GetImmediateCommandList();
RHICmdList.ReadSurfaceData(BackBuffer, Rect, outData, FReadSurfaceDataFlags(RCM_UNorm));
for (FColor& c : outData)
{
c.A = 255;
}
//绘制像素
HDC hdc = GetDC(hwnd);
BITMAPINFOHEADER bi = { 0 };
bi.biSize = sizeof(BITMAPINFOHEADER);
bi.biWidth = Swidth;
bi.biHeight = -Sheight;
bi.biPlanes = 1;
bi.biBitCount = 32;
bi.biCompression = BI_RGB;
bi.biSizeImage = Swidth * Sheight * 4;
SetDIBitsToDevice(hdc, 0, 0, Swidth, Sheight, 0, 0, 0, Sheight, outData.GetData(), (BITMAPINFO*)&bi, DIB_RGB_COLORS);
ReleaseDC(hwnd, hdc);
}
void AMyActor::CreateAndRunWindow()
{
//创建窗口
WNDCLASSEX wc = { sizeof(WNDCLASSEX),0,WndProc,0L, 0L,GetModuleHandle(NULL),NULL,NULL,NULL,NULL,L"Window",NULL };
RegisterClassEx(&wc);
hwnd = CreateWindow(TEXT("Window"), TEXT("UE Windows"), WS_OVERLAPPEDWINDOW, 0, 0, 1024, 720, NULL, NULL, wc.hInstance, NULL);
ShowWindow(hwnd, SW_SHOW);
UpdateWindow(hwnd);
//添加托盘图标
HICON hIcon = (HICON)LoadImage(hInstance, TEXT("D:\\xx.ico"), IMAGE_ICON, 0, 0, LR_LOADFROMFILE);
nid.cbSize = sizeof(NOTIFYICONDATA);
nid.hWnd = hwnd;
nid.uID = 1;
nid.hIcon = hIcon;
nid.uFlags = NIF_ICON | NIF_MESSAGE | NIF_TIP;
nid.uCallbackMessage = WM_USER + 1;
lstrcpyn(nid.szTip, TEXT("桌面精灵"), sizeof(nid.szTip));
Shell_NotifyIcon(NIM_ADD, &nid);
//设置窗口
SetWindowPos(hwnd, HWND_TOPMOST, 0, 0, GetSystemMetrics(SM_CXSCREEN), GetSystemMetrics(SM_CYSCREEN), SWP_SHOWWINDOW);
SetWindowLong(hwnd, GWL_EXSTYLE, GetWindowLongPtr(hwnd, GWL_EXSTYLE) | WS_EX_LAYERED|WS_EX_TOOLWINDOW);
SetWindowLong(hwnd, GWL_STYLE, WS_VISIBLE | WS_POPUP);
SetLayeredWindowAttributes(hwnd, RGB(0, 0, 0), 0, LWA_COLORKEY);
MSG msg = { 0 };
while (GetMessageW(&msg, NULL, 0, 0))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
}
创建后期处理材质:
作用就是将距离屏幕过远的像素设置为键透明的颜色,允许Win进行窗口表面合并时进行相关处理
建议将透明键设置为一个非常用颜色,黑色的话会把物体阴影给透明掉,而且会有一些明显的噪点。
注意一下颜色的换算,UE材质内的(0-1)=RGB颜色的(0-255)
应用材质,并勾选无限范围
这个时候差不多就可以了,以下是运行效果
右键托盘图标可以关闭程序,若路径下无图标文件,则会显示透明图标。
文本内容使用英文,否则会乱码。
如果不使用"独立进程的窗口运行",这一步会使编辑器退出,相关问题可以去改源码。
关于性能问题:
ReadSurfaceData()这个函数占用了约25ms,而其它代码总共加起来也只有3-4ms,如果加上Game线程外的一些耗时,最终的测试帧率只有40fps上下。
ReadSurfaceData函数网上有优化教程,看介绍,大概能优化到10ms左右。
不过相较于这样,我更倾向于为新窗口分配表面,这样可以避免两设备之间的内存拷贝操作。
结尾:
剩下的就是自己去写移动,交互相关的逻辑了,下面给一个简单的案例:
操作:点击窗口非透明部分,且储蓄按住鼠标左键,可进行拖动,亦可以在按住过程中滚动鼠标滚轮
(上面的程序有一些鼠标交互的小问题,只有鼠标在窗口的非透明区域,才能接收到按键消息,否则只能接收到鼠标位移消息,这个可以自己去改,然后鼠标灵敏度适配相关功能也需要自己去做)
Pawn(代替character,控制相机矩阵的位置变幻)