UE4 学习笔记 (四) 编辑器Viewport窗口中的鼠标拾取原理
在UE4的LevelEditor中我们可以使用鼠标自由选中目标,并进行鼠标操作。这个过程我们一般称为Cursor Pick,也就是经常说的鼠标拾取或者光标拾取,今天我们简单分析下在UE4的Editor窗口中是如何实现这个功能。
一,背景介绍
在Directx10中,有一个例子就是实现的光标拾取:通过鼠标的点击,选中模型中最近的一个Triangle,并高亮。这里的实现是通过射线的方式,以摄像机为起点经过屏幕上点击的点的转换坐标(屏幕坐标线到世界坐标系)的一条直线,来计算经过直线最近的面片。
在Editor下Pick功能也是个必备的功能,选中场景内的物件并进行操作等。下面我们简单分析下UE4中是如何实现的,是否会给我们一点一样的启发。
二,UE4的实现方式
先说结论,我们再一步步分析。UE4下是通过在渲染每个网格对象的时候生成一个HitProxy的类,这个类里反向记录当前的Component对象等信息。然后将该HitProxy存入到数组中,根据数组索引生成唯一的索引ID。然后UE4根据条件来触发通过渲染Canvas,将所所有的网格对象的HitProxy的Id渲染到屏幕大小的贴图中。后续Cursor查询P(x,y)的时候在贴图上取出像素转换成HitProxy的Id,读取对应的信息。
a) HitProxy的生成
每个ActorComponent的基类中都有一个FPrimitiveSceneProxy类来记录一些渲染信息,以及HitProxy信息。在Coponent的创建过程中会为每个Component生成一个动态的HitProxy,并加入到全局的Array中。
if (GIsEditor)
{
// Create a dynamic hit proxy for the primitive.
DefaultDynamicHitProxy = Proxy->CreateHitProxies(InComponent,HitProxies);
if( DefaultDynamicHitProxy )
{
DefaultDynamicHitProxyId = DefaultDynamicHitProxy->Id;
}
}
这里的CreateHitProxies最终会在全局的Array里插入当前的HitProxy,并生成一个全局唯一的Id。
void HHitProxy::InitHitProxy()
{
// Allocate an entry in the global hit proxy array for this hit proxy, and use the index as the hit proxy’s ID.
Id = FHitProxyId(FHitProxyArray::Get().Add(this));
}
到这一步基本的准备做完了,一句话总结就是UE4把每个网格对象生成了一个辅助类对象,用于进行双向捆绑当前的Component对象与某个Id进行一一对应,那后续的查询应该也是对这个Id进行做文章。
b) HitProxy的采集
UE4中的编辑器窗口都是基于FEditorViewportClient作为父类来扩展的,比如我们常用的LevelEditorViewportClient。FEditorViewportClient中实现了一个函数GetCursor用于处理用户的光标的坐标的转换,这里的屏幕坐标系以屏幕的左上角为原点:
EMouseCursor::Type FEditorViewportClient::GetCursor(FViewport* InViewport,int32 X,int32 Y)
拿到光标的坐标之后,我们这里需要根据光标在屏幕上的位置获取UE4下世界坐标系下对应该点的Actor。我们在上一步中已经给每个Actor分配了一个int的Id标记,那现在是否有方法能根据(x,y)坐标来取出对应的Id呢 ? 我们来看看UE4是怎么做的:
GetHitProxy 展开代码
HHitProxy* FViewport::GetHitProxy(int32 X,int32 Y)
{
// 这里的HitProxySize = 5.
// 计算一个已(x,y)为中心2*HitProxySize为边长的矩形.
int32 MinX = X - HitProxySize,
MinY = Y - HitProxySize,
MaxX = X + HitProxySize,
MaxY = Y + HitProxySize;
FIntPoint VPSize = GetSizeXY();
// Clip the region to the viewport bounds.
MinX = FMath::Clamp(MinX, 0, VPSize.X - 1);
MinY = FMath::Clamp(MinY, 0, VPSize.Y - 1);
MaxX = FMath::Clamp(MaxX, 0, VPSize.X - 1);
MaxY = FMath::Clamp(MaxY, 0, VPSize.Y - 1);
int32 TestSizeX = MaxX - MinX + 1;
int32 TestSizeY = MaxY - MinY + 1;
HHitProxy* HitProxy = NULL;
if(TestSizeX > 0 && TestSizeY > 0)
{
// Read the hit proxy map from the device.
// 这一步非常重要, 读取proxy map信息.
TArray<HHitProxy*> ProxyMap;
GetHitProxyMap(FIntRect(MinX, MinY, MaxX + 1, MaxY + 1),ProxyMap);
check(ProxyMap.Num() == TestSizeX * TestSizeY);
// Find the hit proxy in the test region with the highest order.
// 查找这个区域中优先级最高的HitProxy.
int32 ProxyIndex = TestSizeY/2 * TestSizeX + TestSizeX/2;
check(ProxyIndex<ProxyMap.Num());
HitProxy = ProxyMap[ProxyIndex];
bool bIsOrtho = GetClient()->IsOrtho();
for(int32 TestY = 0;TestY < TestSizeY;TestY++)
{
for(int32 TestX = 0;TestX < TestSizeX;TestX++)
{
HHitProxy* TestProxy = ProxyMap[TestY * TestSizeX + TestX];
if(TestProxy && (!HitProxy || (bIsOrtho ? TestProxy->OrthoPriority : TestProxy->Priority) > (bIsOrtho ? HitProxy->OrthoPriority : HitProxy->Priority)))
{
HitProxy = TestProxy;
}
}
}
}
return HitProxy;
}
GetHitProxyMap中最关键的实现就是GetRawHitProxyData函数,看下这里的实现。
UWorld* World = ViewportClient->GetWorld();
FCanvas Canvas(&HitProxyMap, &HitProxyMap, World, World ? World->FeatureLevel.GetValue() : GMaxRHIFeatureLevel, FCanvas::CDM_DeferDrawing, ViewportClient->ShouldDPIScaleSceneCanvas() ? ViewportClient->GetDPIScale() : 1.0f);
{
ViewportClient->Draw(this, &Canvas);
}
Canvas.Flush_GameThread();
HitProxyMap中的信息是通过如上一段代码来填充的,HitProxyMap的其中一个基类是FRenderTarget,绑定之后推送到主渲染线程。这里通过Canvas来渲染整个屏幕中的HitProxy,主要实现在Draw函数中。通过这一步渲染出来一个屏幕大小的Texture,其中每个像素都是记录的HitProxy的Id值。
将HitProxy的Id信息写入贴图是通过shader的操作来执行的,在SceneHitProxyRendering.cpp中的GetShaderBindings函数中将HitProxyId传入shader中进行着色。
GetShaderBindings 展开代码
void GetShaderBindings(
const FScene* Scene,
ERHIFeatureLevel::Type FeatureLevel,
const FPrimitiveSceneProxy* PrimitiveSceneProxy,
const FMaterialRenderProxy& MaterialRenderProxy,
const FMaterial& Material,
const FMeshPassProcessorRenderState& DrawRenderState,
const FHitProxyShaderElementData& ShaderElementData,
FMeshDrawSingleShaderBindings& ShaderBindings) const
{
FMeshMaterialShader::GetShaderBindings(Scene, FeatureLevel, PrimitiveSceneProxy, MaterialRenderProxy, Material, DrawRenderState, ShaderElementData, ShaderBindings);
FHitProxyId hitProxyId = ShaderElementData.BatchHitProxyId;
if (PrimitiveSceneProxy && ShaderElementData.BatchHitProxyId == FHitProxyId())
{
hitProxyId = PrimitiveSceneProxy->GetPrimitiveSceneInfo()->DefaultDynamicHitProxyId;
}
// Per-instance hitproxies are supplied by the vertex factory.
if (PrimitiveSceneProxy && PrimitiveSceneProxy->HasPerInstanceHitProxies())
{
hitProxyId = FColor(0);
}
ShaderBindings.Add(HitProxyId, hitProxyId.GetColor().ReinterpretAsLinear());
}
FHitProxyId类专门用来转换像素和HitProxy的Id,这里丢弃高8位。
FHitProxyId::FHitProxyId(FColor Color)
{
Index = ((int32)Color.R << 16) | ((int32)Color.G << 8) | ((int32)Color.B << 0);
}
c) HitProxy查询
查询一步已经很简单了,只需要到FHitProxyArray中查询即可。
HHitProxy* GetHitProxyById(FHitProxyId Id)
{
return FHitProxyArray::Get().GetHitProxyById(Id.Index);
}
三,总结
这里我们简单分析了UE4下视图窗口个中实现Cursor Pick的过程,也是源于笔者在实现自定义Viewport时候新生成Actor对象发现无法选取,经过一番调试之后自己理解的一个过程。
因本人对渲染的了解有限,难免有纰漏,欢迎大家补充:-)