在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对象发现无法选取,经过一番调试之后自己理解的一个过程。
因本人对渲染的了解有限,难免有纰漏,欢迎大家补充:-)