Tutorial 51: Custom Vertex Types

CNA — C++ XNA 4.0 reimplementation

CNA ships with built-in vertex types (VertexPositionColor, VertexPositionTexture, VertexPositionNormalTexture). For normal mapping, tangent-space lighting, or any custom shader attribute you need to define your own vertex layout using IVertexType, VertexDeclaration, and VertexElement.

IVertexType interface

#include "Microsoft/Xna/Framework/Graphics/IVertexType.hpp"
#include "Microsoft/Xna/Framework/Graphics/VertexDeclaration.hpp"
#include "Microsoft/Xna/Framework/Graphics/VertexElement.hpp"
#include "Microsoft/Xna/Framework/Graphics/VertexElementUsage.hpp"
#include "Microsoft/Xna/Framework/Graphics/VertexElementFormat.hpp"

namespace Microsoft::Xna::Framework::Graphics {

    // Implement this interface on your vertex struct
    struct IVertexType {
        // Returns a static VertexDeclaration describing the layout
        virtual const VertexDeclaration& getVertexDeclaration() const = 0;
    };

} // namespace

VertexElement struct

struct VertexElement {
    int                offset;   // byte offset from the start of the vertex
    VertexElementFormat format;  // data type / component count
    VertexElementUsage  usage;   // semantic (what this attribute means)
    int                usageIndex; // for multiple texcoords (0, 1, ...)
};

VertexElementFormat enum

FormatC++ typeSize
Singlefloat4 bytes
Vector2float[2]8 bytes
Vector3float[3]12 bytes
Vector4float[4]16 bytes
Coloruint8_t[4] RGBA4 bytes
Byte4uint8_t[4]4 bytes
Short2int16_t[2]4 bytes
Short4int16_t[4]8 bytes

VertexElementUsage enum

UsageGLSL attributeTypical data
PositionaPositionVertex position (XYZ or XY)
NormalaNormalSurface normal (XYZ)
TextureCoordinateaTexCoord0UV coordinates
TangentaTangentTangent for normal mapping
BinormalaBinormalBinormal / bitangent
ColoraColorPer-vertex colour
BlendIndicesaBlendIndicesBone indices for skinning
BlendWeightaBlendWeightsBone weights for skinning

sizeof alignment

Declare your vertex struct with #pragma pack(push, 1) (or __attribute__((packed))) to prevent the compiler from inserting padding between fields. The GPU expects the byte layout to match the VertexDeclaration exactly.

VertexPositionNormalTextureTangent struct + declaration

// CustomVertexTypes.hpp
#pragma once
#include "Microsoft/Xna/Framework/Vector2.hpp"
#include "Microsoft/Xna/Framework/Vector3.hpp"
#include "Microsoft/Xna/Framework/Graphics/IVertexType.hpp"
#include "Microsoft/Xna/Framework/Graphics/VertexDeclaration.hpp"
#include "Microsoft/Xna/Framework/Graphics/VertexElement.hpp"
#include "Microsoft/Xna/Framework/Graphics/VertexElementUsage.hpp"
#include "Microsoft/Xna/Framework/Graphics/VertexElementFormat.hpp"

using namespace Microsoft::Xna::Framework;
using namespace Microsoft::Xna::Framework::Graphics;

#pragma pack(push, 1)
struct VertexPositionNormalTextureTangent : public IVertexType {
    Vector3 Position;      //  0 bytes, 12 bytes
    Vector3 Normal;        // 12 bytes, 12 bytes
    Vector2 TexCoord;      // 24 bytes,  8 bytes
    Vector3 Tangent;       // 32 bytes, 12 bytes
    Vector3 Binormal;      // 44 bytes, 12 bytes
    //                        56 bytes total per vertex

    VertexPositionNormalTextureTangent() = default;

    VertexPositionNormalTextureTangent(
        const Vector3& pos,
        const Vector3& normal,
        const Vector2& tex,
        const Vector3& tangent,
        const Vector3& binormal)
        : Position(pos), Normal(normal), TexCoord(tex),
          Tangent(tangent), Binormal(binormal) {}

    // Static declaration — shared across all instances
    static const VertexDeclaration VertexDeclaration;

    const Graphics::VertexDeclaration& getVertexDeclaration() const override {
        return VertexDeclaration;
    }
};
#pragma pack(pop)

// CustomVertexTypes.cpp
const Graphics::VertexDeclaration
VertexPositionNormalTextureTangent::VertexDeclaration(
    56, // stride: total bytes per vertex
    {
        VertexElement( 0, VertexElementFormat::Vector3,
                       VertexElementUsage::Position,         0),
        VertexElement(12, VertexElementFormat::Vector3,
                       VertexElementUsage::Normal,           0),
        VertexElement(24, VertexElementFormat::Vector2,
                       VertexElementUsage::TextureCoordinate,0),
        VertexElement(32, VertexElementFormat::Vector3,
                       VertexElementUsage::Tangent,          0),
        VertexElement(44, VertexElementFormat::Vector3,
                       VertexElementUsage::Binormal,         0),
    }
);

Using the custom vertex with a shader

// Build your mesh
std::vector<VertexPositionNormalTextureTangent> verts;
// ... fill verts with geometry ...

// Create vertex buffer using the custom declaration
auto vb = std::make_unique<VertexBuffer>(
    getGraphicsDeviceProperty(),
    VertexPositionNormalTextureTangent::VertexDeclaration,
    static_cast<int>(verts.size()),
    BufferUsage::None);
vb->SetData(verts.data(), static_cast<int>(verts.size()));

// Bind and draw
gd.setVertexBuffer(*vb);
for (auto& pass : normalMapEffect_->getCurrentTechnique().Passes) {
    pass.Apply();
    gd.DrawPrimitives(PrimitiveType::TriangleList, 0,
                      static_cast<int>(verts.size()) / 3);
}

Verifying the layout at compile time

// These assertions catch mismatches between the struct and the declaration
static_assert(sizeof(VertexPositionNormalTextureTangent) == 56,
    "Vertex struct size mismatch — check packing");
static_assert(offsetof(VertexPositionNormalTextureTangent, Normal)   == 12, "Normal offset");
static_assert(offsetof(VertexPositionNormalTextureTangent, TexCoord) == 24, "TexCoord offset");
static_assert(offsetof(VertexPositionNormalTextureTangent, Tangent)  == 32, "Tangent offset");
static_assert(offsetof(VertexPositionNormalTextureTangent, Binormal) == 44, "Binormal offset");