最近在项目中需要应用到模型三轴移动,为达到最佳效果,在制作中有如下需求:
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,到底怎么用?
(留作下次内容)