Hello, i’m trying to render an obj file by loading it through a custom component.
The model on the left is straigth out exported from blender, using a scale of 100, while the model on the right is from the same .obj file, but parsed drectly from code and rendered by building a custom Vertex Factory, Index Buffer and Position Buffer, with FMeshBatch::bWireframe = true
:
As you can see, the model on the right renders with noticeable visual artifacts.
I started by creating a custom class that extends UPrimitiveComponent
and a custom proxy for it that extends FPrimitiveComponent
:
// UTestPrimitiveComponent.h
// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
#include "CoreMinimal.h"
#include "Components/PrimitiveComponent.h"
#include "UTestPrimitiveComponent.generated.h"
class FPrimitiveSceneProxy;
class FObjectInitializer;
class UStaticMesh;
class FStaticMeshDescription;
struct FRawMesh;
USTRUCT()
struct FOBJMesh {
GENERATED_BODY()
TArray<FVector> vertices;
TArray<FVector> normals;
TArray<FVector2D> uvs;
TArray<int32> tex_indices; // Unused
TArray<uint32> vertex_indices;
TArray<int32> normals_indices; // Unused
};
UCLASS(ClassGroup = (Custom), meta = (BlueprintSpawnableComponent))
class RENDERINGPIPELINE_API UTestPrimitiveComponent :
public UPrimitiveComponent
{
GENERATED_BODY()
private:
FOBJMesh* TheMesh;
void ParseLine(FString& line, FRawMesh& Desc);
void ReadNormalFromLine(FString& line, FVector& normal);
void ReadUVFromLine(FString& line, FVector2D& uv);
void ReadVertexFromLine(FString& line, FVector& vertex);
void ReadFaceInfoFromLine(FString& element, FRawMesh& Desc);
public:
UTestPrimitiveComponent(const FObjectInitializer&);
void BeginPlay() override;
__forceinline FOBJMesh* GetMesh() { return TheMesh; }
FPrimitiveSceneProxy* CreateSceneProxy() override;
};
WIthout getting into much detail, this is my Proxy’s code (i already ruled out the OBJ parsing step by loading a cube and checking with VS’s debugger if the vertices and indices were loaded correctly):
#include "UTestPrimitiveComponent.h"
#include "Components/MeshComponent.h"
#include "GenericPlatform/GenericPlatformFile.h"
#include "Engine/StaticMesh.h"
#include "PrimitiveSceneProxy.h"
#include "StaticMeshDescription.h"
#include "Rendering/StaticMeshVertexBuffer.h"
#include "RawMesh.h"
#include "RHICommandList.h"
#include "PrimitiveSceneProxy.h"
#include "Misc/FileHelper.h"
#include "Misc/Paths.h"
#include "Misc/DefaultValueHelper.h"
#include "Containers/Array.h"
#include "EngineMinimal.h"
class UTestPrimitiveComponent;
template <typename T>
static FVertexBufferRHIRef CreateVertexBufferWithData(TArray<T>& data) {
FRHIResourceCreateInfo info;
auto size = sizeof (T)* data.Num();
FVertexBufferRHIRef buf = RHICreateVertexBuffer(size, BUF_Static | BUF_ShaderResource, info);
void* ptr = static_cast<void*>(RHILockVertexBuffer(buf, 0, size, RLM_WriteOnly));
FMemory::Memcpy(ptr, static_cast<void*>(data.GetData()), size);
RHIUnlockVertexBuffer(buf);
return buf;
}
struct FOBJIndexBuffer : public FIndexBuffer {
TArray<uint32> Indices;
virtual void InitRHI() override {
FRHIResourceCreateInfo info;
auto ElementSizeInBytes = sizeof(uint32);
auto ArraySize = ElementSizeInBytes * Indices.Num();
IndexBufferRHI = RHICreateIndexBuffer(ElementSizeInBytes, ArraySize, BUF_Static, info);
void* ptr = static_cast<void*>(RHILockIndexBuffer(IndexBufferRHI, 0, ArraySize, RLM_WriteOnly));
FMemory::Memcpy(ptr, static_cast<void*>(Indices.GetData()), ArraySize);
RHIUnlockIndexBuffer(IndexBufferRHI);
}
};
struct FOBJPositionBuffer : public FVertexBuffer {
TArray<FVector> Vertices;
FShaderResourceViewRHIRef PositionComponentSRV;
virtual void InitRHI() override {
FRHIResourceCreateInfo info;
VertexBufferRHI = CreateVertexBufferWithData(Vertices);
if (VertexBufferRHI)
{
PositionComponentSRV =
RHICreateShaderResourceView(FShaderResourceViewInitializer(VertexBufferRHI, PF_R32_FLOAT));
}
}
};
struct FOBJMeshBuffers : public FRenderResource {
struct FTangentData {
FPackedNormal X;
FPackedNormal Z;
public:
FTangentData(FPackedNormal InZ, FPackedNormal InX)
: X(InX), Z(InZ) {}
};
FVertexBuffer TexCoordsBuffer;
FVertexBuffer TangentsBuffer;
TArray<FVector2D> uvs;
TArray<FTangentData> Tangents;
FShaderResourceViewRHIRef TexCoordsBufferSRV;
FShaderResourceViewRHIRef TangentsBufferSRV;
virtual void InitRHI() override {
TexCoordsBuffer.VertexBufferRHI = CreateVertexBufferWithData(uvs);
if (TexCoordsBuffer.VertexBufferRHI) {
// PF_G16R16F if using half precision, todo when implementing full thing
TexCoordsBufferSRV = RHICreateShaderResourceView(FShaderResourceViewInitializer(TexCoordsBuffer.VertexBufferRHI, PF_G32R32F));
}
TangentsBuffer.VertexBufferRHI = CreateVertexBufferWithData(Tangents);
if (TangentsBuffer.VertexBufferRHI) {
// PF_R8G8B8A8_SNORM if using half precision, todo when implementing full thing
TangentsBufferSRV =
RHICreateShaderResourceView(FShaderResourceViewInitializer(TangentsBuffer.VertexBufferRHI, PF_R8G8B8A8_SNORM));
}
BeginInitResource(&TexCoordsBuffer);
BeginInitResource(&TangentsBuffer);
}
};
struct FOBJRenderData {
FOBJPositionBuffer OBJPositionBuffer;
FOBJMeshBuffers OBJMeshBuffers;
};
class OBJSceneProxy : public FPrimitiveSceneProxy {
public:
UMaterialInterface* Material;
FLocalVertexFactory VertexFactory;
FOBJMeshBuffers MeshBuffers;
FOBJPositionBuffer PositionBuffer;
FOBJIndexBuffer IndexBuffer;
UTestPrimitiveComponent* TheComponent;
OBJSceneProxy(UTestPrimitiveComponent* Component)
: FPrimitiveSceneProxy(Component, TEXT("OBJ Component")),
VertexFactory(GetScene().GetFeatureLevel(), "FObjSceneProxy"),
TheComponent(Component) {
IndexBuffer.Indices = TheComponent->GetMesh()->vertex_indices;
PositionBuffer.Vertices = TheComponent->GetMesh()->vertices;
MeshBuffers.uvs = Component->GetMesh()->uvs;
for (int i = 1; i < IndexBuffer.Indices.Num(); i++) {
// This block of code here is mostly wrong; Still, it shouldn't impact the rendering as much
// Blender y u start counting from 1
auto CurIndex = IndexBuffer.Indices[i] - 1;
auto PrevIndex = IndexBuffer.Indices[i - 1] - 1;
auto prevVertex = PositionBuffer.Vertices[PrevIndex];
auto curVertex = PositionBuffer.Vertices[CurIndex];
auto TangentRight = (curVertex - prevVertex).GetSafeNormal();
auto TangentFront = FVector::CrossProduct(TangentRight, curVertex.GetSafeNormal());
MeshBuffers.Tangents.Add(FOBJMeshBuffers::FTangentData(TangentFront, TangentRight));
}
BeginInitResource(&IndexBuffer);
BeginInitResource(&PositionBuffer);
BeginInitResource(&MeshBuffers);
InitFactory();
BeginInitResource(&VertexFactory);
Material = UMaterial::GetDefaultMaterial(MD_Surface);
}
virtual ~OBJSceneProxy()
{
IndexBuffer.ReleaseResource();
PositionBuffer.ReleaseResource();
MeshBuffers.TexCoordsBuffer.ReleaseResource();
MeshBuffers.TangentsBuffer.ReleaseResource();
MeshBuffers.ReleaseResource();
VertexFactory.ReleaseResource();
}
void InitFactory() {
struct FactoryParams {
FLocalVertexFactory* Factory;
FOBJMeshBuffers* MeshBuffers;
FOBJPositionBuffer* PositionBuffer;
} Params;
Params.Factory = &VertexFactory;
Params.PositionBuffer = &PositionBuffer;
Params.MeshBuffers = &MeshBuffers;
ENQUEUE_RENDER_COMMAND(OBJInitVertexFactory)([Params] (FRHICommandListImmediate& RHICmdList) {
FLocalVertexFactory::FDataType Data;
// Position stuff
Data.PositionComponent = FVertexStreamComponent(
Params.PositionBuffer,
0,
sizeof(FVector),
VET_Float3
);
Data.PositionComponentSRV = Params.PositionBuffer->PositionComponentSRV;
// Tex Coords stuff
Data.TextureCoordinatesSRV = Params.MeshBuffers->TexCoordsBufferSRV;
Data.TextureCoordinates.Add(FVertexStreamComponent(
&Params.MeshBuffers->TexCoordsBuffer,
0,
sizeof(FVector2D),
VET_Float4, // Doppio della dimensione (VET_Float2): Why?
EVertexStreamUsage::ManualFetch
));
// Tangents stuff
Data.TangentsSRV = Params.MeshBuffers->TangentsBufferSRV;
typedef FPackedNormal TangentType;
auto TangentElemType = VET_Short2;
auto TangentXOffset = STRUCT_OFFSET(FOBJMeshBuffers::FTangentData, X);
auto TangentZOffset = STRUCT_OFFSET(FOBJMeshBuffers::FTangentData, Z);
auto TangentSizeInBytes = sizeof(TangentType);
Data.TangentBasisComponents[0] = FVertexStreamComponent(
&Params.MeshBuffers->TangentsBuffer,
TangentXOffset,
TangentSizeInBytes,
TangentElemType,
EVertexStreamUsage::ManualFetch
);
Data.TangentBasisComponents[1] = FVertexStreamComponent(
&Params.MeshBuffers->TangentsBuffer,
TangentZOffset,
TangentSizeInBytes,
TangentElemType,
EVertexStreamUsage::ManualFetch
);
Data.LightMapCoordinateComponent = FVertexStreamComponent(
&Params.MeshBuffers->TexCoordsBuffer,
0,
sizeof(FVector2D),
VET_Float2,
EVertexStreamUsage::ManualFetch
);
FColorVertexBuffer::BindDefaultColorVertexBuffer(Params.Factory, Data,
FColorVertexBuffer::NullBindStride::FColorSizeForComponentOverride);
Params.Factory->SetData(Data);
Params.Factory->InitResource();
});
}
// (StartIndex + IndexCount) * IndexBuffer->GetStride()
void DrawStaticElements(FStaticPrimitiveDrawInterface* DrawInterface)
{
check(IsInRenderingThread());
FMaterialRenderProxy* Proxy = Material->GetRenderProxy();
FMeshBatch batch;
batch.bWireframe = true;
batch.VertexFactory = &VertexFactory;
batch.Type = PT_TriangleList;
batch.MaterialRenderProxy = Proxy;
batch.DepthPriorityGroup = SDPG_World;
batch.CastShadow = true;
batch.LODIndex = 0;
auto& BatchElement = batch.Elements[0];
BatchElement.IndexBuffer = &IndexBuffer;
// for i = FirstIndex to NumPrimitives ?
BatchElement.FirstIndex = 0;
BatchElement.NumPrimitives = IndexBuffer.Indices.Num() / 3;
DrawInterface->DrawMesh(batch, MAX_FLT);
}
FPrimitiveViewRelevance GetViewRelevance(const FSceneView* View) const
{
// Partially copied from StaticMeshRenderer.cpp
FPrimitiveViewRelevance Result;
Result.bDrawRelevance = true;
Result.bRenderInMainPass = true;
Result.bVelocityRelevance = IsMovable() && Result.bOpaque && Result.bRenderInMainPass;
Result.bStaticRelevance = true;
Result.bDynamicRelevance = false;
return Result;
}
};