欢迎大家加入虚幻4技术美术 ,图形渲染和各种游戏效果实现学习群。大家一起学习一起讨论。192946459 
最近在研究虚幻4的渲染组件,我会一一介绍以下几个组件: 
SceneComponent 
PrimitiveComponent 
MeshComponent 
CustomMeshComponent 
ProcedureMeshComponent 
CableComponent 
我们一层一层研究,就会对虚幻的渲染管线的application阶段有一个很好的认识,比如如何自己画一个模型,如何动态更新顶点缓冲区如何动态更新索引缓冲区。如何上传顶点缓冲区等。希望有兴趣研究虚幻4图形效果的朋友能一起来探讨。 
欢迎大家加入虚幻4技术美术 ,图形渲染和各种游戏效果实现学习群。大家一起学习一起讨论。192946459 
 
下面先开始讨论SceneComponent和PrimitiveComponent 
SceneComponent是虚幻引擎里面一个十分基础的组件。主要负责Transform的相关逻辑,并且提供了大量接口,如移动缩放位移,可见性,附加等基础功能。当然在它的下层还有ActorComponent,这里就不讨论了,有兴趣的可以去翻翻源码。 
 
PrimitiveComponent是虚幻4渲染组件,包含了大量渲染相关的逻辑,也是游戏线程和逻辑线程沟通的媒介。虚幻中的Staticmeshcomponent   SkeletalMeshcomponent  CustomMeshComponent  ProcedurMeshComponent  CableMeshComponent都从它派生而来。它不仅和渲染引擎有千丝万缕的关系,它还和物理引擎打交道,其内部有大量和物理交互接口。我们这里主要研究渲染部分,那么我们应该重点关心primitiveComponent的场景代理(SceneProxy)。 
 
/** 
     * Creates a proxy to represent the primitive to the scene manager in the rendering thread. 
     * @return The proxy object. 
     */ 
    virtual FPrimitiveSceneProxy* CreateSceneProxy() 
    { 
        return NULL; 
    } 
这个函数在primitiveComponent中没有任何逻辑,以后我们如果想要画个模型什么的需要我们自己重写这个函数,重写这个类。 
那么到底如何画一个模型呢,如何上传顶点缓冲和索引缓冲呢,下面我们打开CustomMeshComponent的代码,这个组件相当简单,但是很清晰地告诉了我们如何完成模型绘制。 
 
下面是CustomMeshComponent.h的代码,非常简单,一共就60多行。但是比较完整展示了虚幻引擎渲染管线的图元汇编阶段的实现。 
 
#pragma once 
 
#include "CoreMinimal.h" 
#include "UObject/ObjectMacros.h" 
#include "Components/MeshComponent.h" 
#include "CustomMeshComponent.generated.h" 
 
class FPrimitiveSceneProxy; 
 
USTRUCT(BlueprintType) 
struct FCustomMeshTriangle 
{ 
    GENERATED_USTRUCT_BODY() 
 
    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category=Triangle) 
    FVector Vertex0; 
 
    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category=Triangle) 
    FVector Vertex1; 
 
    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category=Triangle) 
    FVector Vertex2; 
}; 
 
/** Component that allows you to specify custom triangle mesh geometry */ 
UCLASS(hidecategories=(Object,LOD, Physics, Collision), editinlinenew, meta=(BlueprintSpawnableComponent), ClassGroup=Rendering) 
class CUSTOMMESHCOMPONENT_API UCustomMeshComponent : public UMeshComponent 
{ 
    GENERATED_UCLASS_BODY() 
 
    /** Set the geometry to use on this triangle mesh */ 
    UFUNCTION(BlueprintCallable, Category="Components|CustomMesh") 
    bool SetCustomMeshTriangles(const TArray<FCustomMeshTriangle>& Triangles); 
 
    /** Add to the geometry to use on this triangle mesh.  This may cause an allocation.  Use SetCustomMeshTriangles() instead when possible to reduce allocations. */ 
    UFUNCTION(BlueprintCallable, Category = "Components|CustomMesh") 
    void AddCustomMeshTriangles(const TArray<FCustomMeshTriangle>& Triangles); 
 
    /** Removes all geometry from this triangle mesh.  Does not deallocate memory, allowing new geometry to reuse the existing allocation. */ 
    UFUNCTION(BlueprintCallable, Category = "Components|CustomMesh") 
    void ClearCustomMeshTriangles(); 
 
private: 
 
    //~ Begin UPrimitiveComponent Interface. 
    virtual FPrimitiveSceneProxy* CreateSceneProxy() override; 
    //~ End UPrimitiveComponent Interface. 
 
    //~ Begin UMeshComponent Interface. 
    virtual int32 GetNumMaterials() const override; 
    //~ End UMeshComponent Interface. 
 
    //~ Begin USceneComponent Interface. 
    virtual FBoxSphereBounds CalcBounds(const FTransform& LocalToWorld) const override; 
    //~ Begin USceneComponent Interface. 
 
    /** */ 
    TArray<FCustomMeshTriangle> CustomMeshTris; 
 
    friend class FCustomMeshSceneProxy; 
}; 
 
首先它前置声明了对渲染至关重要的class FPrimitiveSceneProxy这个类。然后重写了PrimitiveComponent中的 virtual FPrimitiveSceneProxy* CreateSceneProxy() override;这个接口。这个接口用于创建场景代理,然后重写了 virtual FBoxSphereBounds CalcBounds(const FTransform& LocalToWorld) const override;这个接口用于定义渲染包围盒。然后还定义了一些用于更新上传顶点缓冲区的函数接口。最后的一句话是将FCustomMeshSceneProxy;这个类声明为friend。这个类是哪里来的呢,是我们自己定义的。他的定义位置在CustommeshComponent.cpp中,由SceneProxy派生而来。下面我们就看看customMeshComponent.cpp中的逻辑。 
 
要渲染模型,肯定我们需要提供渲染资源,那么下一步我们将重写各种资源类,为渲染自己的模型提供渲染资源,由于这是渲染的开始,所以我们需要创建顶点缓冲  索引缓冲  输入布局等资源。 
首先创建顶点缓冲区。这个只是作为图元汇编阶段的渲染资源 
/** Vertex Buffer */ 
class FCustomMeshVertexBuffer : public FVertexBuffer 
{ 
public: 
    TArray<FDynamicMeshVertex> Vertices; 
 
    virtual void InitRHI() override 
    { 
        FRHIResourceCreateInfo CreateInfo; 
        void* VertexBufferData = nullptr; 
        VertexBufferRHI = RHICreateAndLockVertexBuffer(Vertices.Num() * sizeof(FDynamicMeshVertex), BUF_Static, CreateInfo, VertexBufferData); 
 
        // Copy the vertex data into the vertex buffer.        
        FMemory::Memcpy(VertexBufferData,Vertices.GetData(),Vertices.Num() * sizeof(FDynamicMeshVertex)); 
        RHIUnlockVertexBuffer(VertexBufferRHI); 
    } 
 
}; 
 
然后是索引缓冲区资源 
/** Index Buffer */ 
class FCustomMeshIndexBuffer : public FIndexBuffer 
{ 
public: 
    TArray<int32> Indices; 
 
    virtual void InitRHI() override 
    { 
        FRHIResourceCreateInfo CreateInfo; 
        void* Buffer = nullptr; 
        IndexBufferRHI = RHICreateAndLockIndexBuffer(sizeof(int32),Indices.Num() * sizeof(int32),BUF_Static, CreateInfo, Buffer); 
 
        // Write the indices to the index buffer.        
        FMemory::Memcpy(Buffer,Indices.GetData(),Indices.Num() * sizeof(int32)); 
        RHIUnlockIndexBuffer(IndexBufferRHI); 
    } 
}; 
 
虚幻里面还有一个控制InputLayer的类,它就是FLocalVertexFactory 
 
/** Vertex Factory */ 
class FCustomMeshVertexFactory : public FLocalVertexFactory 
{ 
public: 
 
    FCustomMeshVertexFactory() 
    {} 
 
    /** Init function that should only be called on render thread. */ 
    void Init_RenderThread(const FCustomMeshVertexBuffer* VertexBuffer) 
    { 
        check(IsInRenderingThread()); 
 
        FDataType NewData; 
        NewData.PositionComponent = STRUCTMEMBER_VERTEXSTREAMCOMPONENT(VertexBuffer, FDynamicMeshVertex, Position, VET_Float3); 
        NewData.TextureCoordinates.Add( 
            FVertexStreamComponent(VertexBuffer, STRUCT_OFFSET(FDynamicMeshVertex, TextureCoordinate), sizeof(FDynamicMeshVertex), VET_Float2) 
            ); 
        NewData.TangentBasisComponents[0] = STRUCTMEMBER_VERTEXSTREAMCOMPONENT(VertexBuffer, FDynamicMeshVertex, TangentX, VET_PackedNormal); 
        NewData.TangentBasisComponents[1] = STRUCTMEMBER_VERTEXSTREAMCOMPONENT(VertexBuffer, FDynamicMeshVertex, TangentZ, VET_PackedNormal); 
        NewData.ColorComponent = STRUCTMEMBER_VERTEXSTREAMCOMPONENT(VertexBuffer, FDynamicMeshVertex, Color, VET_Color); 
 
        SetData(NewData); 
    } 
 
    /** Initialization */ 
    void Init(const FCustomMeshVertexBuffer* VertexBuffer) 
    { 
        if (IsInRenderingThread()) 
        { 
            Init_RenderThread(VertexBuffer); 
        } 
        else 
        { 
            ENQUEUE_UNIQUE_RENDER_COMMAND_TWOPARAMETER( 
                InitCustomMeshVertexFactory, 
                FCustomMeshVertexFactory*, VertexFactory, this, 
                const FCustomMeshVertexBuffer*, VertexBuffer, VertexBuffer, 
                { 
                VertexFactory->Init_RenderThread(VertexBuffer); 
            }); 
        }    
    } 
}; 
 
我这里不详细讲每行代码的意思,你可以去翻阅最近一位大神写的书(大象无形)里面有一个章节专门讲了上面那些代码的意思,我这里旨在理清楚组件的实现方式。 
 
有了以上的渲染资源之后,下一步我们需要把这些资源提交给渲染引擎。那么就用到了我们前面一直在关注的SceneProxy 
下面虚幻的CustomMeshComponent重写了这个类 
class FCustomMeshSceneProxy : public FPrimitiveSceneProxy 
 
这个类各种函数和成员如下  
@构造函数用于初始化各种资源和成员  FCustomMeshSceneProxy(UCustomMeshComponent* Component) 
        : FPrimitiveSceneProxy(Component) 
@析构函数,用于回收各种资源virtual ~FCustomMeshSceneProxy() 
@重写了绘制函数virtual void GetDynamicMeshElements。可以理解为虚幻的drawcall 
@virtual FPrimitiveViewRelevance GetViewRelevance(const FSceneView* View) const override 
@virtual bool CanBeOccluded() const override 
@virtual uint32 GetMemoryFootprint( void ) const override { return( sizeof( *this ) + GetAllocatedSize() ); } 
 
下面是FCustomMeshSceneProxy的私有变量 
private: 
 
    UMaterialInterface* Material;            材质 
    FCustomMeshVertexBuffer VertexBuffer;    顶点缓冲区 
    FCustomMeshIndexBuffer IndexBuffer;      索引缓冲 
    FCustomMeshVertexFactory VertexFactory;  顶点输入布局工厂 
 
    FMaterialRelevance MaterialRelevance; 
 
然后让CustomMeshComponent创建FCustomMeshSceneProxy场景代理实例 
FPrimitiveSceneProxy* UCustomMeshComponent::CreateSceneProxy() 
{ 
    FPrimitiveSceneProxy* Proxy = NULL; 
    if(CustomMeshTris.Num() > 0) 
    { 
        Proxy = new FCustomMeshSceneProxy(this); 
    } 
    return Proxy; 
} 
 
渲染包围盒的创建,如果不创建包围盒,组件将无法被渲染。 
FBoxSphereBounds UCustomMeshComponent::CalcBounds(const FTransform& LocalToWorld) const 
{ 
    FBoxSphereBounds NewBounds; 
    NewBounds.Origin = FVector::ZeroVector; 
    NewBounds.BoxExtent = FVector(HALF_WORLD_MAX,HALF_WORLD_MAX,HALF_WORLD_MAX); 
    NewBounds.SphereRadius = FMath::Sqrt(3.0f * FMath::Square(HALF_WORLD_MAX)); 
    return NewBounds; 
} 
 
不知道有没有人发现,虚幻4官方的CustomMeshComponent没有uv,无法投射贴图,那是因为CustommeshComponent根本就没定义顶点的uv和tangent。下一章介绍ProcedurMeshComponent的时候我会和大家一起改造CustomMeshComponent组件。本人才疏学浅,来这里发帖也是希望大家能加入我们一起讨论。欢迎大家加入虚幻4技术美术 ,图形渲染和各种游戏效果实现学习群。大家一起学习一起讨论。192946459 
 |