虚幻4 渲染组件【1】

[复制链接]
查看3280 | 回复2 | 2017-5-22 15:50:59 | 显示全部楼层 |阅读模式
欢迎大家加入虚幻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
sabo@ | 2017-5-23 15:47:40 | 显示全部楼层
跟着你的脚步学习一下。
回复 支持 反对

使用道具 举报

堕落的牧羊人 | 2017-5-31 00:07:35 | 显示全部楼层
谢谢分享
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

7

主题

21

回帖

23

积分

初始化成员

积分
23