Immediate mode geometry tool

Info

  • Languages: C++
  • Time frame: 4 weeks
  • Team Size: Solo

Overview

Inspired by the simplicity and beauty of ImGui, I wanted to create a tool with an immediate mode API. I had recently played a lot of Townscaper, a casual city builder where players place building blocks on a grid. I decided to mash the two together and develop an immediate mode grid tool.

As the project took shape, its scope grew. Instead of just handling grid interactions, it turned into a tool for creating, styling, and interacting with various geometries (including grids!).

Key achievements

  • ● Built a backend-agnostic geometry tool for easy engine integration.
  • ● Developed standalone input handling for Win32 and geometry rendering using DirectX11.
  • ● Implemented an ID system with push/pop functionality and retained geometry state.
  • ● Added geometry interaction through picking (hover, click, etc.).
  • ● Enabled arbitrary data storage and querying directly on geometry.
  • ● Built an immediate mode interaction API with ID-based state querying.

Immediate and retained data

Immediate mode is an architectural pattern in which elements are defined within every frame, rather than being stored and updated as persistent objects. However, interactions such as drag start/stop, hover enter/stay/exit, and arbitrary data storing and retrieval require retained state. Solving this was one of the main challenges of the project.

Inspired by ImGui, I implemented an ID system that lets developers push and pop IDs to uniquely identify geometry. Geometry draw calls are rebuilt every frame, while interaction and custom data are stored in an unordered map keyed by ID. This made it possible to query geometry state through a simple immediate mode API while keeping the rendering stateless. The geometry state is updated once per frame using 3D picking.

Layout and grids

A core goal of the tool was to make interactive grid creation simple. To support this, I built a layout system that automatically positioned and generated geometry based on developer-defined grid settings.

This made it easy to build interactive grid-based systems while still allowing developers to query individual geometry interactions.

Backend agnostic setup

Like ImGui, I wanted the tool to be easy to integrate into different engines and rendering pipelines.

To support this, the tool maintains an abstract IO layer and a draw commands, which backend implementations translate between platform-specific input/output systems and rendering APIs. For example, the Win32 backend handles input via WndProc and updates the IO state, while the DirectX 11 backend converts draw commands into GPU draw calls.

Example code

View tool on GitHub >

InGeo::NewFrameWin32();
InGeo::NewFrame(myCamera.GetTransform(), myCamera.GetProjectionMatrix());

// Set up style
constexpr InGeo::Vector4f baseColor{ 1.f, 0.f, 0.f, 1.f };
constexpr InGeo::Vector4f hoverColor{ 1.f, 1.f, 0.f, 1.f };
constexpr InGeo::Vector4f clickedColor{ 0.f, 1.f, 0.f, 1.f };
constexpr InGeo::Style baseStyle{ baseColor, hoverColor, clickedColor };

// Create quad geometry
InGeo::PushStyle(baseStyle);
InGeo::PushId("Quads");

constexpr InGeo::Vector3f quadPosition{ 0.f, 0.f, 0.f };
constexpr InGeo::Vector3f quadPitchYawRoll{ 0.f, 0.f, 0.f };
constexpr float quadSideSize = 1.f;

InGeo::CreateQuad("QuadOne", quadPosition, quadPitchYawRoll, quadSideSize, quadSideSize);

if (InGeo::IsGeometryClicked()) // Handle interaction
{
	std::cout << "QuadOne is Clicked!" << '\n';
}

constexpr InGeo::QuadData quadTwoData{ quadSideSize, quadSideSize};
InGeo::CreateGeometry("QuadTwo", quadTwoData, quadPosition);

if (InGeo::IsGeometryHovered() && InGeo::IsInputStarted(InGeo::Input::Delete)) // Handle interaction
{
	std::cout << "Are you sure you want to delete QuadTwo?" << '\n';;
}

InGeo::PopId();

// Create Cube geometry
InGeo::PushId("Cube");

constexpr InGeo::CuboidData cuboid{ 2.f, 3.f, 6.f };
InGeo::CreateGeometry("Cuboid", cuboid, { 10.f, 0.f, 2.f });

if (InGeo::IsCuboidFaceClicked(InGeo::CuboidFace::Top))
{
	std::cout << "Clicked top of cuboid!" << '\n';
}

if (InGeo::IsCuboidFaceClicked(InGeo::CuboidFace::Front))
{
	std::cout << "Clicked front of cuboid!" << '\n';
}

InGeo::PopId();
InGeo::PopStyle();

// Create grid style
constexpr InGeo::Vector4f gridBaseColor{ 0.f, 1.f, 0.f, 0.f };
constexpr InGeo::Vector4f gridHoverColor{ 0.f, 1.f, 0.f, 1.f };
constexpr InGeo::Vector4f gridClickedColor{ 0.f, 0.f, 1.f, 1.f };
constexpr InGeo::Style gridStyle{ gridBaseColor, gridHoverColor, gridClickedColor };

InGeo::PushStyle(gridStyle);

// Create grid
constexpr InGeo::Vector3f geometryDimensions{ 1.f, 1.f, 5.f };
constexpr InGeo::Vector3f gridSpacing{ 0.5f, 0.25f, 1.f };

constexpr InGeo::GridSettings gridSettings // Geometry in grid adheres to grid settings
{
	geometryDimensions.x, // Width
	geometryDimensions.y, // Depth
	geometryDimensions.z, // Height

	gridSpacing.x, // Column space
	gridSpacing.y, // Row Space
	gridSpacing.z, // Layer Space
};

constexpr int gridLayers = 10;
constexpr int gridRows = 10;
constexpr int gridColumns = 10;

constexpr InGeo::Vector3f gridPosition{ 0.f, 0.f, 0.f };

InGeo::BeginGrid("Grid", gridPosition, {}, gridSettings);
for (int layerIndex = 0; layerIndex < gridLayers; layerIndex++)
{
	for (int rowIndex = 0; rowIndex < gridLayers; rowIndex++)
	{
		for (int columnIndex = 0; columnIndex < gridLayers; columnIndex++)
		{
			if (columnIndex % 3 == 0)
			{
				InGeo::NextGridCell(InGeo::GeometryType::Quad); 
			}
			else if (columnIndex % 2 == 0) 
			{
				InGeo::NextGridCell(InGeo::GeometryType::Cuboid);

				if (InGeo::IsGeometryHovered()) // Handle interaction on grid
				{
					std::cout << "Hovering cube! on grid" << '\n';
				}
			}
			else 
			{
				InGeo::NextGridCell(InGeo::GeometryType::None); // Leave grid tile empty
			}
		}

		InGeo::NextGridRow();
	}
	
	InGeo::NextGridLayer();
}
InGeo::EndGrid();

InGeo::PopStyle();

// Render drawdata
const InGeo::Matrix4x4f cameraToProjection = myCamera.GetProjectionMatrix();
const InGeo::Matrix4x4f worldToCamera = myCamera.GetTransform().GetInverse();

InGeo::Render();
InGeo::RenderDrawDataDx11(myContext.Get(), InGeo::GetDrawData(), cameraToProjection, worldToCamera);

Take aways

This project pushed me into a type of programming I had never worked with before, which made it both challenging and very rewarding.

One area I would improve is the geometry creation itself. Right now, the system only supports hardcoded quads and cuboids, which limits its flexibility. Ideally, I would extend it to support arbitrary meshes, making any type of geometry interactive.

You can see the tool applied in my other project, tower builder & validator