Skip to content

Guides Capsule

Devin edited this page Apr 27, 2026 · 1 revision

Guides · Capsule

What you'll build

A capsule mesh — two hemispheres joined by a cylinder — to use as a character proxy. Same data shape as Primitives::Sphere: positions, normals, uvs, indices. Drop into a SceneObject and draw with the same pipeline as everything else.

Prerequisites

The why

Primitives::Sphere ships with VCK; capsule doesn't, because most users want it sized to their character's collision profile. A capsule generator is ~40 lines of simple trig, written here with the same output shape so it pairs with VulkanMesh::Upload exactly like the built-in primitives.

radius controls the cylindrical waist + the hemisphere caps. halfHeight is the cylinder section's half-extent — the total capsule is 2*halfHeight + 2*radius tall.

Code

#include "VCK.h"
#include <cmath>
using namespace VCK;

Primitives::Mesh BuildCapsule(float radius, float halfHeight,
                              int rings = 8, int sectors = 16)
{
    Primitives::Mesh out;

    const float pi = 3.1415926535f;
    const int   ringsPerCap = rings;
    const int   ringCount   = 2 * ringsPerCap;            // top + bottom hemispheres

    out.positions.reserve((ringCount + 1) * (sectors + 1));
    out.normals  .reserve((ringCount + 1) * (sectors + 1));
    out.uvs      .reserve((ringCount + 1) * (sectors + 1));

    for (int r = 0; r <= ringCount; ++r)
    {
        // Latitude: top hemisphere [0..pi/2], bottom hemisphere [pi/2..pi].
        const float lat = (pi * 0.5f) * static_cast<float>(r)
                          / static_cast<float>(ringsPerCap);
        const float y    = std::cos(lat) * radius;
        const float ring = std::sin(lat) * radius;

        // Push the centre of the cylindrical section between the two caps.
        const float cylY = (r <= ringsPerCap) ?  halfHeight : -halfHeight;

        for (int s = 0; s <= sectors; ++s)
        {
            const float lon = (2.0f * pi) * static_cast<float>(s)
                              / static_cast<float>(sectors);
            const float x = std::cos(lon) * ring;
            const float z = std::sin(lon) * ring;

            const Vec3 p { x, y + cylY, z };
            const Vec3 n = Normalize({ x, y, z });   // hemisphere normal, ignore cyl offset
            const Vec2 uv{ static_cast<float>(s) / sectors,
                           static_cast<float>(r) / ringCount };

            out.positions.push_back(p);
            out.normals  .push_back(n);
            out.uvs      .push_back(uv);
        }
    }

    out.indices.reserve(ringCount * sectors * 6);
    for (int r = 0; r < ringCount; ++r)
    {
        for (int s = 0; s < sectors; ++s)
        {
            const uint32_t a = static_cast<uint32_t>( r      * (sectors + 1) + s    );
            const uint32_t b = static_cast<uint32_t>( r      * (sectors + 1) + s + 1);
            const uint32_t c = static_cast<uint32_t>((r + 1) * (sectors + 1) + s    );
            const uint32_t d = static_cast<uint32_t>((r + 1) * (sectors + 1) + s + 1);

            out.indices.push_back(a); out.indices.push_back(c); out.indices.push_back(b);
            out.indices.push_back(b); out.indices.push_back(c); out.indices.push_back(d);
        }
    }
    return out;
}

Same output struct as Primitives::Cube / Sphere. The capsule is just a sphere whose two hemispheres have been pulled apart by 2*halfHeight along Y; the hemisphere normals stay correct because we only translate the positions, not the normal data.

rings × sectors controls tessellation. 8 × 16 is plenty for a character proxy; bump them for a screen-filling capsule.

Upload and place

Primitives::Mesh capsule = BuildCapsule(/*radius*/0.4f, /*halfHeight*/0.8f);

struct V { Vec3 p; Vec3 n; };
static_assert(sizeof(V) == 24, "tightly packed");

std::vector<V> verts(capsule.positions.size());
for (std::size_t i = 0; i < verts.size(); ++i)
    verts[i] = { capsule.positions[i], capsule.normals[i] };

SceneObject character;
character.transform = Translate({0, 1.2f, 0});   // capsule centred 1.2m above ground
character.mesh.Upload(device, command,
                     verts.data(),
                     static_cast<VkDeviceSize>(sizeof(V) * verts.size()),
                     static_cast<uint32_t>(verts.size()),
                     capsule.indices.data(),
                     static_cast<uint32_t>(capsule.indices.size()));

scene.push_back(std::move(character));

A 0.4m-radius / 0.8m-half-height capsule is 2.4m tall. Adjust to whatever your character controller calls "tall".

What's next

  • Guides · Step & Jump — give the capsule velocity, gravity, and a jump
  • Cookbook — recipe 7 (instanced primitives), recipe 8 (skinned meshes)

VCK · Vulkan Core Kit

Getting Started

Guides

Reference

More


Single source of truth for the full API surface is the doc block at the top of VCK.h.

Clone this wiki locally