UE4 坐标轴绘制

最近在项目中需要应用到模型三轴移动,为达到最佳效果,在制作中有如下需求:
1、物体本身具有深度绘制
2、坐标轴具有深度绘制

  • 基于以上需求,出现了一个interesting的问题,坐标轴和物体同时进行深度绘制的时候,模型本身与坐标轴模型在深度绘制时无法区分坐标轴与选中模型。但是,UE4自带的编辑器又实实在在实现了这一效果,因此,说明引擎本身应该是支持的,只是方法不合适。
    之后,查阅了相关资料,最终都将UE4编辑器中的坐标轴绘制指向PDI绘制,经过查阅官方文档,以及翻看引擎源码,发现坐标轴本身并不是采用深度绘制实现效果,而是mesh的渲染线程处理方式不同。具体描述如下:
    UE4中Mesh具有两种渲染方式,也即是Mesh有两种: 1、UMeshComponent 2、DynamicMesh
    在渲染时,会先后处理这两种Mesh,也即会牵涉到SceneProxy,达到所谓“深度绘制”效果。
    对于UMeshComponent,在渲染前,会先将Mesh存入StaticMeshDrawList,然后进行渲染。而对于DynamicMesh,渲染前会在主线程把Mesh转换成FMeshBatch并保存在ViewMeshElementList中。之后再渲染线程再根据不同的pass执行不同的渲染策略。
    那么问题来了,我们究竟应该怎样达到想要的效果? 我们说过,UE4自带编辑器已经实现该效果,那么我们为何不直接借鉴一下呢?
    相信大家使用的都是源码版引擎(如果不是请自行到官网下载),经过查找,最终我在
    Source/Editor/UnrealED/Paivate下的UnrealWidget中找到了坐标轴绘制的代码,如下所示:
    /**
 * Draws an arrow head line for a specific axis.
 */
void FWidget::Render_Axis( const FSceneView* View, FPrimitiveDrawInterface* PDI, EAxisList::Type InAxis, FMatrix& InMatrix, UMaterialInterface* InMaterial, const FLinearColor& InColor, FVector2D& OutAxisDir, const FVector& InScale, bool bDrawWidget, bool bCubeHead )
{
    FMatrix AxisRotation = FMatrix::Identity;
    if( InAxis == EAxisList::Y )
    {
        AxisRotation = FRotationMatrix::MakeFromXZ(FVector(0, 1, 0), FVector(0, 0, 1));
    }
    else if( InAxis == EAxisList::Z )
    {
        AxisRotation = FRotationMatrix::MakeFromXY(FVector(0, 0, 1), FVector(0, 1, 0));
    }

    FMatrix ArrowToWorld = AxisRotation * InMatrix;

    // The scale that is passed in potentially leaves one component with a scale of 1, if that happens
    // we need to extract the inform scale and use it to construct the scale that transforms the primitives
    float UniformScale = InScale.GetMax() > 1.0f ? InScale.GetMax() : InScale.GetMin() < 1.0f ? InScale.GetMin() : 1.0f;
    // After the primitives have been scaled and transformed, we apply this inverse scale that flattens the dimension
    // that was scaled up to prevent it from intersecting with the near plane.  In perspective this won't have any effect,
    // but in the ortho viewports it will prevent scaling in the direction of the camera and thus intersecting the near plane.
    FVector FlattenScale = FVector(InScale.Component(0) == 1.0f ? 1.0f / UniformScale : 1.0f, InScale.Component(1) == 1.0f ? 1.0f / UniformScale : 1.0f, InScale.Component(2) == 1.0f ? 1.0f / UniformScale : 1.0f);

    FScaleMatrix Scale(UniformScale);
    ArrowToWorld = Scale * ArrowToWorld;

    if( bDrawWidget )
    {
        const bool bDisabled = EditorModeTools ? (EditorModeTools->IsDefaultModeActive() && GEditor->HasLockedActors() ) : false;
        PDI->SetHitProxy( new HWidgetAxis( InAxis, bDisabled) );

        const float AxisLength = AXIS_LENGTH + GetDefault<ULevelEditorViewportSettings>()->TransformWidgetSizeAdjustment;
        const float HalfHeight = AxisLength/2.0f;
        const float CylinderRadius = 1.2f;
        const FVector Offset( 0,0,HalfHeight );

        switch( InAxis )
        {
            case EAxisList::X:
            {
                DrawCylinder(PDI, ( Scale * FRotationMatrix(FRotator(-90, 0.f, 0)) * InMatrix ) * FScaleMatrix(FlattenScale), Offset, FVector(1, 0, 0), FVector(0, 1, 0), FVector(0, 0, 1), CylinderRadius, HalfHeight, 16, InMaterial->GetRenderProxy(false), SDPG_Foreground);
                break;
            }
            case EAxisList::Y:
            {
                DrawCylinder(PDI, (Scale * FRotationMatrix(FRotator(0, 0, 90)) * InMatrix)* FScaleMatrix(FlattenScale), Offset, FVector(1, 0, 0), FVector(0, 1, 0), FVector(0, 0, 1), CylinderRadius, HalfHeight, 16, InMaterial->GetRenderProxy(false), SDPG_Foreground );
                break;
            }
            case EAxisList::Z:
            {
                DrawCylinder(PDI, ( Scale * InMatrix ) * FScaleMatrix(FlattenScale), Offset, FVector(1, 0, 0), FVector(0, 1, 0), FVector(0, 0, 1), CylinderRadius, HalfHeight, 16, InMaterial->GetRenderProxy(false), SDPG_Foreground);
                break;
            }
        }

        if ( bCubeHead )
        {
            const float CubeHeadOffset = 3.0f;
            FVector RootPos(AxisLength + CubeHeadOffset, 0, 0);

            Render_Cube(PDI, (FTranslationMatrix(RootPos) * ArrowToWorld) * FScaleMatrix(FlattenScale), InMaterial, FVector(4.0f));
        }
        else
        {
            const float ConeHeadOffset = 12.0f;
            FVector RootPos(AxisLength + ConeHeadOffset, 0, 0);

            float Angle = FMath::DegreesToRadians( PI * 5 );
            DrawCone(PDI, ( FScaleMatrix(-13) * FTranslationMatrix(RootPos) * ArrowToWorld ) * FScaleMatrix(FlattenScale), Angle, Angle, 32, false, FColor::White, InMaterial->GetRenderProxy(false), SDPG_Foreground);
        }

        PDI->SetHitProxy( NULL );
    }

    FVector2D NewOrigin;
    FVector2D AxisEnd;
    const FVector AxisEndWorld = ArrowToWorld.TransformPosition(FVector(64, 0, 0));
    const FVector WidgetOrigin = InMatrix.GetOrigin();

    if (View->ScreenToPixel(View->WorldToScreen(WidgetOrigin), NewOrigin) &&
        View->ScreenToPixel(View->WorldToScreen(AxisEndWorld), AxisEnd))
    {
        // If both the origin and the axis endpoint are in front of the camera, trivially calculate the viewport space axis direction
        OutAxisDir = (AxisEnd - NewOrigin).GetSafeNormal();
    }
    else
    {
        // If either the origin or axis endpoint are behind the camera, translate the entire widget in front of the camera in the view direction before performing the
        // viewport space calculation
        const FMatrix InvViewMatrix = View->ViewMatrices.GetInvViewMatrix();
        const FVector ViewLocation = InvViewMatrix.GetOrigin();
        const FVector ViewDirection = InvViewMatrix.GetUnitAxis(EAxis::Z);
        const FVector Offset = ViewDirection * (FVector::DotProduct(ViewLocation - WidgetOrigin, ViewDirection) + 100.0f);
        const FVector AdjustedWidgetOrigin = WidgetOrigin + Offset;
        const FVector AdjustedWidgetAxisEnd = AxisEndWorld + Offset;

        if (View->ScreenToPixel(View->WorldToScreen(AdjustedWidgetOrigin), NewOrigin) &&
            View->ScreenToPixel(View->WorldToScreen(AdjustedWidgetAxisEnd), AxisEnd))
        {
            OutAxisDir = -(AxisEnd - NewOrigin).GetSafeNormal();
        }
    }
}

当然,只是整个绘制函数,其中有轴的判定已经一些相关计算(位置,放缩等),我们主要关注这几句:

   switch( InAxis )
        {
            case EAxisList::X:
            {
                DrawCylinder(PDI, ( Scale * FRotationMatrix(FRotator(-90, 0.f, 0)) * InMatrix ) * FScaleMatrix(FlattenScale), Offset, FVector(1, 0, 0), FVector(0, 1, 0), FVector(0, 0, 1), CylinderRadius, HalfHeight, 16, InMaterial->GetRenderProxy(false), SDPG_Foreground);
                break;
            }
            case EAxisList::Y:
            {
                DrawCylinder(PDI, (Scale * FRotationMatrix(FRotator(0, 0, 90)) * InMatrix)* FScaleMatrix(FlattenScale), Offset, FVector(1, 0, 0), FVector(0, 1, 0), FVector(0, 0, 1), CylinderRadius, HalfHeight, 16, InMaterial->GetRenderProxy(false), SDPG_Foreground );
                break;
            }
            case EAxisList::Z:
            {
                DrawCylinder(PDI, ( Scale * InMatrix ) * FScaleMatrix(FlattenScale), Offset, FVector(1, 0, 0), FVector(0, 1, 0), FVector(0, 0, 1), CylinderRadius, HalfHeight, 16, InMaterial->GetRenderProxy(false), SDPG_Foreground);
                break;
            }
        }

没错,这里就是绘制坐标轴的位置,从函数命名来看,绘制的是一个Cylinder。
该函数(位于Source\Runtime\Engine\Private\PrimitiveDrawingUtils.cpp)具体如下:

//还有其他重载,为节约篇幅,不过多赘述
void DrawCylinder(FPrimitiveDrawInterface* PDI, const FMatrix& CylToWorld, const FVector& Base, const FVector& XAxis, const FVector& YAxis, const FVector& ZAxis, float Radius, float HalfHeight, int32 Sides, const FMaterialRenderProxy* MaterialRenderProxy, uint8 DepthPriority)
{
    TArray<FDynamicMeshVertex> MeshVerts;
    TArray<int32> MeshIndices;
    BuildCylinderVerts(Base, XAxis, YAxis, ZAxis, Radius, HalfHeight, Sides, MeshVerts, MeshIndices);


    FDynamicMeshBuilder MeshBuilder;
    MeshBuilder.AddVertices(MeshVerts);
    MeshBuilder.AddTriangles(MeshIndices);

    MeshBuilder.Draw(PDI, CylToWorld, MaterialRenderProxy, DepthPriority,0.f);
}

从该函数不难看出,它是先使用传入参数构建出Cylinder顶点集,时候使用MeshBuilder创建并绘制mesh。函数参数多是用来创建顶点集,通过变量名就能看出,不再赘述。我们主要关注这个参数:DepthPriority。通过与之前DrawCylinder传入参数比对,不难发现,改传入值即为上文中的SDPG_Foreground(枚举类型(ESceneDepthPriorityGroup)),通过修改该值并测试,发现果然不出所料,该值是控制绘制物体的绘制深度优先权的。那么似乎问题到此就结束了,总结起来就是使用DynamicMeshBuilder这个类去绘制一个DynamicMesh,控制其DepthPriority为1(SDPG_Foreground)即可实现效果。但是,我们忽略了一个有意思的参数:PDI。是的,我们到现在都不知道这个参数是从哪来的,PrimitiveDrawInterface,从最开始的函数调用他就是参数,回到最初,我们发现从Render_Axis开始PDI即为参数,我也从这开始上溯过多个层级,最终也没能找到PDI的获得方式。
那么,从这又引出另一个问题,PrimitiveDrawInterface,到底怎么用?
(留作下次内容)

  • 1
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值