Tutorial 51: Custom Vertex Types
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
| Format | C++ type | Size |
|---|---|---|
Single | float | 4 bytes |
Vector2 | float[2] | 8 bytes |
Vector3 | float[3] | 12 bytes |
Vector4 | float[4] | 16 bytes |
Color | uint8_t[4] RGBA | 4 bytes |
Byte4 | uint8_t[4] | 4 bytes |
Short2 | int16_t[2] | 4 bytes |
Short4 | int16_t[4] | 8 bytes |
VertexElementUsage enum
| Usage | GLSL attribute | Typical data |
|---|---|---|
Position | aPosition | Vertex position (XYZ or XY) |
Normal | aNormal | Surface normal (XYZ) |
TextureCoordinate | aTexCoord0 | UV coordinates |
Tangent | aTangent | Tangent for normal mapping |
Binormal | aBinormal | Binormal / bitangent |
Color | aColor | Per-vertex colour |
BlendIndices | aBlendIndices | Bone indices for skinning |
BlendWeight | aBlendWeights | Bone 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");