Simple Cube Rendering - Tutorial
This example demonstrates how to create a simple rotating wireframe cube using the helios game framework. It covers the fundamental concepts of 3D rendering, including shaders, materials, meshes, scene graphs, and the render loop.
What You'll Learn
- Setting up a Helios application with OpenGL backend
- Creating and configuring shaders
- Building materials and meshes
- Working with the scene graph
- Implementing a basic render loop
- Handling user input
Prerequisites
- helios framework properly built and installed
- Basic understanding of C++ and 3D graphics concepts
- Familiarity with OpenGL shader language (GLSL)
Project Structure
simple_cube_rendering/
├── main.cpp # Main application code
├── resources/
│ ├── cube.vert # Vertex shader
│ └── cube.frag # Fragment shader
└── README.md # This file
Step-by-Step Tutorial
1. Application and Window Setup
First, we create an OpenGL application using the GLFWFactory:
const auto app = GLFWFactory::makeOpenGLApp(
"helios - Simple Cube Renderer"
);
auto win = dynamic_cast<GLFWWindow*>(app->current());
helios::input::InputManager& inputManager = app->inputManager();
What's happening:
GLFWFactory::makeOpenGLApp()creates a complete application with a window and OpenGL context- We retrieve the current window and input manager for later use
2. Shader Creation
Shaders define how our geometry is transformed and colored. We load vertex and fragment shaders from files:
const auto basicStringFileReader = BasicStringFileReader();
auto shader_ptr = std::make_shared<OpenGLShader>(
"./resources/cube.vert",
"./resources/cube.frag",
basicStringFileReader
);
Vertex Shader (cube.vert):
#version 450 core
layout (location=0) in vec3 aPos;
layout (location=1) uniform mat4 worldMatrix;
void main() {
gl_Position = worldMatrix * vec4(aPos, 1.0f);
}
Fragment Shader (cube.frag):
#version 450 core
out vec4 FragColor;
void main() {
FragColor = vec4(1.0f, 1.0f, 0.0f, 1.0f); // Yellow color
}
Mapping Uniforms:
We tell the shader where to find the world transformation matrix:
auto uniformLocationMap = std::make_unique<OpenGLUniformLocationMap>();
uniformLocationMap->set(UniformSemantics::WorldMatrix, 1);
shader_ptr->setUniformLocationMap(std::move(uniformLocationMap));
3. Material Creation
Materials combine shaders with properties like color and roughness:
auto cubeMaterialProps = MaterialProperties(
helios::math::vec4f(1.0f, 0.0f, 1.0f, 1.0f), // Magenta RGBA
0.0f // Roughness (0 = smooth, 1 = rough)
);
auto cubeMaterialProps_shared = std::make_shared<MaterialProperties>(cubeMaterialProps);
auto material_ptr = std::make_shared<Material>(
shader_ptr, cubeMaterialProps_shared
);
Note: The material color is currently not used by the fragment shader (which hardcodes yellow). To use the material color, modify the fragment shader to accept a uniform.
4. Mesh (Geometry) Creation
A mesh defines the 3D shape. We use helios's built-in Cube shape:
auto cube = Cube{};
auto meshConfig = std::make_shared<const MeshConfig>(
PrimitiveType::LineLoop // Render as wireframe
);
auto mesh_ptr = std::make_shared<OpenGLMesh>(cube, meshConfig);
Primitive Types:
PrimitiveType::Triangles- Solid filled geometryPrimitiveType::LineLoop- Wireframe outlinePrimitiveType::Lines- Individual line segmentsPrimitiveType::Points- Vertex points
5. Creating a Renderable
A RenderPrototype combines a material with a mesh, and a Renderable makes it instantiable:
const auto renderPrototype = std::make_shared<RenderPrototype>(
material_ptr, mesh_ptr
);
auto cubeRenderable = std::make_shared<Renderable>(renderPrototype);
6. Scene Graph Setup
The scene graph organizes objects in 3D space. We create a scene and add our cube:
auto frustumCullingStrategy = std::make_unique<CullNoneStrategy>();
auto scene = std::make_unique<Scene>(std::move(frustumCullingStrategy));
auto cubeSceneNode = std::make_unique<SceneNode>(std::move(cubeRenderable));
auto* cubeNode = scene->addNode(std::move(cubeSceneNode));
// Scale the cube to half its original size
cubeNode->scale(vec3f(0.5f, 0.5f, 0.5f));
Scene Graph Concepts:
- Scene: The root container for all objects
- SceneNode: An object in the scene with a transform (position, rotation, scale)
- Culling Strategy: Determines which objects are visible (here: render everything)
7. Camera Setup
The camera defines the viewpoint from which we observe the scene:
auto cam = std::make_unique<Camera>();
auto* cameraNode = scene->addNode(std::move(cam));
const auto* camera = static_cast<const Camera*>(cameraNode);
8. Main Render Loop
The render loop updates the scene and draws each frame:
float degrees = 0.0f;
constexpr float rotationSpeed = 2.25f;
while (!win->shouldClose()) {
// 1. Process events
app->eventManager().dispatchAll();
inputManager.poll(0.0f);
// 2. Handle input (ESC to quit)
if (inputManager.isKeyPressed(Key::ESC)) {
win->setShouldClose(true);
}
// 3. Update rotation
degrees += rotationSpeed;
if (degrees >= 360.0f) {
degrees -= 360.0f;
}
// 4. Apply transformation
float rad = helios::math::radians(degrees);
cubeNode->rotate(helios::math::rotate(
helios::math::mat4f::identity(),
rad,
helios::math::vec3f(0.4f, 0.6f, 0.2f) // Rotation axis
));
// 5. Render the scene
const auto& snapshot = scene->createSnapshot(*camera);
auto renderPass = RenderPassFactory::getInstance().buildRenderPass(snapshot);
app->renderingDevice().render(renderPass);
// 6. Swap buffers (display the result)
win->swapBuffers();
}
Render Loop Breakdown:
- Event Processing: Handle window events (resize, close, etc.)
- Input Handling: Check for keyboard/mouse input
- Update Logic: Modify object transformations (rotation, position, etc.)
- Rendering: Capture scene state and render it
- Buffer Swap: Display the rendered frame
Building and Running
From the project root directory:
# Configure the build
cmake -S . -B build -DCMAKE_BUILD_TYPE=Release
# Build the project
cmake --build build --config Release
# Run the example
cd build/examples/simple_cube_rendering/Release
./simple_cube_rendering.exe
Expected Output
You should see a window displaying a rotating yellow wireframe cube. Press ESC to exit.
Customization Ideas
Change the Cube Color
Modify cube.frag:
void main() {
FragColor = vec4(0.0f, 1.0f, 0.0f, 1.0f); // Green cube
}
Render a Solid Cube
Change the primitive type in main.cpp:
auto meshConfig = std::make_shared<const MeshConfig>(
PrimitiveType::Triangles // Changed from LineLoop
);
Adjust Rotation Speed
Modify the rotation speed constant:
constexpr float rotationSpeed = 5.0f; // Faster rotation
Change Rotation Axis
Modify the rotation axis vector:
helios::math::vec3f(1.0f, 0.0f, 0.0f) // Rotate around X-axis
Key helios Concepts
Rendering Pipeline
- Scene Graph → Hierarchical organization of objects
- Snapshot → Frozen state of the scene at a moment in time
- RenderPass → Collection of render commands for the GPU
- RenderQueue → Ordered list of objects to render
- RenderCommand → Individual draw call with uniforms
Resource Management
helios uses std::shared_ptr for shared resources (shaders, materials, meshes) and std::unique_ptr for owned resources (scene nodes, render passes).
Module System
helios uses C++23 modules (import statements) instead of traditional #include headers for better compile times and cleaner dependencies.
Troubleshooting
Shader Compilation Errors
Check the console output for shader compilation errors. Common issues:
- Incorrect shader file paths
- GLSL syntax errors
- Unsupported GLSL version
Black Screen
Ensure:
- Shaders are compiling successfully
- The camera is positioned correctly
- The cube is within the view frustum
- OpenGL context is properly initialized
Crash on Startup
Verify:
- All shared resources are properly initialized
- Shader files exist in the
resources/directory - CMake build completed without errors
Further Reading
License
This example is part of the helios framework and is distributed under the MIT License.