A gentle introduction to using the scimesh C++ software renderer. No OpenGL, GPU, or display server required — just a C++17 compiler.
What is scimesh C++?
scimesh is a headless software renderer that takes 3D triangle meshes and produces RGBA images entirely on the CPU. It is designed for scientific visualization where GPU access is unavailable or impractical: HPC clusters, CI pipelines, headless servers, and containers.
The renderer supports multi-light Blinn-Phong shading, anti-aliasing, semi-transparent overlays, wireframe mode, SSAO, fog, clip planes, and procedural geometry.
scimesh is not a game engine, raytracer, or interactive viewer. It produces static images.
Prerequisites
- A C++17 compiler (GCC 7+, Clang 5+, MSVC 2017+)
- CMake 3.10 or newer
Project Layout
The C++ renderer core lives in src/core/:
| File | Purpose |
scimesh/renderer.h | Main Renderer class |
scimesh/mesh.h | Mesh struct (vertices, triangles, colors, UVs, normals) |
scimesh/scene.h | Scene struct (collection of meshes) |
scimesh/camera.h | Camera struct and auto-framing helpers |
scimesh/render_options.h | RenderOptions struct (resolution, shading, lights, AA, etc.) |
scimesh/image.h | Image struct (RGBA pixel buffer, PPM/BMP output) |
scimesh/primitives.h | Procedural geometry generators |
scimesh/transforms.h | Mesh translation, scaling, rotation |
scimesh/normals.h | Vertex normal computation |
scimesh/clipping.h | Triangle clipping against planes |
scimesh/rasterizer.h | Scanline rasterizer with z-buffer |
scimesh/math_utils.h | Inline math helpers |
third_party/glm/ | Vendored GLM math library (headers only) |
To use scimesh in your own project, compile the .cpp files from src/core/ alongside your code. Headers are under src/core/scimesh/ and included as #include <scimesh/header.h>.
Minimal Example
The simplest program builds a colored cube and renders it:
int main() {
Mesh cube = generate_cuboid(
Color(0.9f, 0.2f, 0.2f, 1)
);
Vec3 view_dir = glm::normalize(
Vec3(1.0f, 1.0f, 1.0f));
Camera cam = camera_fit_mesh(cube, view_dir,
45.0f,
1.1f
);
opts.
shading = ShadingMode::SMOOTH;
return 0;
}
Camera definition and helper functions for view setup.
The main rendering engine.
Image render_mesh(const Mesh &mesh, const Camera &camera, const RenderOptions &options)
Render a single mesh to an image.
The Image — an RGBA pixel buffer with compositing and I/O operations.
glm::vec3 Vec3
3-component floating-point vector (xyz).
Procedural geometry generators.
RenderOptions — all settings that control how meshes are drawn.
The Renderer — the main entry point for drawing meshes to images.
A virtual camera that defines the viewpoint for rendering.
An RGBA color with floating-point components.
bool write_tga(const std::string &filename, bool use24bit=false) const
Write the image as a TGA file (Truevision Targa).
bool write_ppm(const std::string &filename) const
Write the image as a PPM file (Portable Pixmap).
bool write_bmp(const std::string &filename) const
Write the image as a BMP file (Windows Bitmap).
A 3D triangle mesh using an indexed face set representation.
All settings that control rendering output.
Color background_color
Background color (default: opaque white).
int height
Image height in pixels (default: 600).
int width
Image width in pixels (default: 800).
ShadingMode shading
Shading mode (default: SMOOTH).
Loading a PLY Mesh
Instead of generating geometry, you can load a mesh from a PLY file:
int main() {
Mesh mesh = ply_io::read(
"bunny.ply");
Vec3 view_dir = glm::normalize(
Vec3(1.0f, 1.0f, 1.0f));
Camera cam = camera_fit_mesh(mesh, view_dir,
Vec3(0, 1, 0), 45.0f, 1.1f);
opts.
shading = ShadingMode::SMOOTH;
return 0;
}
Read Stanford PLY files (.ply).
Compile with ply_io.cpp added to the source list (see Building below).
Building
CMake with FetchContent (Recommended)
The easiest way to use scimesh in your own CMake project is via FetchContent. Add the following to your CMakeLists.txt:
include(FetchContent)
FetchContent_Declare(
scimesh
GIT_REPOSITORY https://github.com/dfsp-spirit/scimesh.git
GIT_TAG main # or a release tag, e.g. v0.2.8
)
FetchContent_MakeAvailable(scimesh)
add_executable(my_app main.cpp)
target_link_libraries(my_app PRIVATE scimesh)
That's it — CMake will clone scimesh, build it as a static library, and make all headers and compiled code available to your target. No manual source listing, include paths, or preprocessor defines needed.
For the in-repo examples, see any examples/cpp/*/CMakeLists.txt which use add_subdirectory() for the same effect.
Manually (without CMake)
Copy scimesh src/ so it sits next to your project. Assuming your code is in my_project/, the layout should look like this:
parent_directory/
├── my_project/ ← your application
│ ├── main.cpp
│ └── build/
└── src/ ← scimesh repo src/ copied here
├── core/
└── third_party/
From inside build/, scimesh sources are reachable at ../../src/. Compile the .cpp files from ../../src/core/ alongside ../main.cpp with -std=c++17.
Note: If your code calls write_png(), add -DSCIMESH_STB_WRITE_IMPL.
write_tga(), write_bmp() and write_ppm() need no extra defines — they use scimesh's own writers.
A runnable script that manually compiles one of our demos using g++: examples/cpp/all_primitives/build_manually.sh.
Core Concepts
Mesh
A Mesh holds the geometry and appearance data:
std::vector<Vec3> vertices;
std::vector<Triangle> triangles;
std::vector<Color> colors;
std::vector<Color> face_colors;
std::vector<Vec3> normals;
std::vector<Vec2> uvs;
bool has_transparency;
};
A Triangle is three indices into the vertex array:
struct Triangle { uint32_t v0, v1, v2; };
A triangle defined by three vertex indices.
Colors are RGBA floats in [0, 1]:
struct Color {
float r, g, b, a; };
Scene
A Scene is a collection of meshes rendered together:
scene.
meshes.push_back(mesh_a);
scene.
meshes.push_back(mesh_b);
A collection of Mesh objects to be rendered together.
std::vector< Mesh > meshes
The meshes in this scene, drawn in order.
When meshes have semi-transparent colors (has_transparency = true or alpha < 1), the renderer sorts them back-to-front and blends appropriately.
Camera
A Camera defines the viewpoint:
ProjectionType projection
Which projection type to use.
Vec3 center
The point the camera looks at.
Vec3 up
The camera's "up" direction.
Vec3 eye
Camera position in world space.
float fov_degrees
Vertical field of view in degrees (perspective only).
Use the auto-framing helpers to avoid manual positioning:
Camera cam = camera_fit_mesh(mesh, view_dir, up, 45.0f, 1.1f);
Camera cam = camera_fit_scene(scene, view_dir, up, 45.0f, 1.1f);
RenderOptions
Controls image size, shading, lighting, and post-processing:
opts.
shading = ShadingMode::SMOOTH;
opts.
lights = {key_light, fill_light, rim_light};
std::vector< Light > lights
List of light sources.
float ambient
Ambient light level (default: 0.3).
float shininess
Shininess exponent (default: 0.0 = no specular).
float contrast
Contrast adjustment (default: 1.0 = no change).
float ssao_intensity
SSAO darkening intensity (default: 0.8).
bool backface_culling
Enable backface culling (default: true).
Color specular_color
Specular highlight color (default: transparent = no specular).
bool ssao_enabled
Enable SSAO (default: false).
int aa_samples
Anti-aliasing sample count (default: 1 = no AA).
float ssao_radius
SSAO sample radius in pixels (default: 16).
ProjectionType projection
Projection type (default: PERSPECTIVE).
Renderer
The Renderer is the main entry point:
Image render_scene(const Scene &scene, const Camera &camera, const RenderOptions &options)
Render a scene (collection of meshes) to an image.
Image render_points_raw(const std::vector< Vec3 > &positions, const std::vector< Color > &colors, float radius, const Camera &camera, const RenderOptions &options)
Render a point cloud (spheres at each position) to an image.
Image render_triangles_raw(const std::vector< Vec3 > &positions, const std::vector< Color > &colors, const Camera &camera, const RenderOptions &options)
Render raw triangles (no Mesh wrapper) to an image.
Image
Image stores RGBA pixels and can write to several formats:
Image img(width, height);
void apply_contrast(float contrast)
Apply a contrast adjustment to the image.
void set_pixel(int x, int y, uint8_t r, uint8_t g, uint8_t b, uint8_t a)
Set a single pixel's RGBA value.
void get_pixel(int x, int y, uint8_t &r, uint8_t &g, uint8_t &b, uint8_t &a) const
Get a single pixel's RGBA value.
Image downsample_box(int factor) const
Downsample the image by a factor using box filtering.
void clear(uint8_t r, uint8_t g, uint8_t b, uint8_t a)
Fill the entire image with an RGBA color (byte values 0–255).
Color sample_bilinear(float u, float v) const
Sample the image at texture coordinates (u, v) using bilinear interpolation.
Mesh I/O
scimesh can read and write several mesh formats:
STL
Mesh mesh = stl_io::read(
"model.stl");
stl_io::write(mesh, "output.stl");
stl_io::write(mesh, "output.stl", true);
Read and write STL files (.stl).
Wavefront OBJ
Mesh mesh = obj_io::read(
"model.obj");
Read Wavefront OBJ files (.obj).
The OBJ reader supports vertices, faces, UV coordinates, and normals. For textured meshes, the texture image must be loaded separately.
Stanford PLY
Mesh mesh = ply_io::read(
"model.ply");
The PLY reader supports ASCII and binary formats, with optional per-vertex colors.
Procedural Geometry
Generate primitives without external files:
Mesh sphere = generate_sphere(
1.0f,
32,
);
Mesh cylinder = generate_cylinder(
0.5f,
16,
);
Mesh cuboid = generate_cuboid(
);
Mesh torus = generate_torus(
1.5f,
0.4f,
32, 16,
);
Mesh cone = generate_cone(
0.8f,
16,
);
Mesh plane = generate_plane(
2.0f, 2.0f,
Color(0.8f, 0.8f, 0.8f, 1)
);
Merged Primitives
For rendering many instances at once (e.g., molecular atoms), use the merged variants that combine multiple primitives into a single mesh:
std::vector<Vec3> centers = {
Vec3(0,0,0),
Vec3(2,0,0),
Vec3(1,2,0)};
std::vector<float> radii = {0.5f, 0.3f, 0.4f};
std::vector<Color> colors = {
Color(1,0,0,1),
Color(0,1,0,1),
Color(0,0,1,1)
};
Mesh generate_multi_spheres(const std::vector< Vec3 > ¢ers, const std::vector< float > &radii, const std::vector< Color > &colors, int segments)
Generate multiple spheres in a single mesh (efficient batching).
Mesh generate_multi_cylinders(const std::vector< Vec3 > &starts, const std::vector< Vec3 > &ends, const std::vector< float > &radii, const std::vector< Color > &colors, int segments)
Generate multiple cylinders in a single mesh (efficient batching).
Mesh Transforms
Transform vertices without modifying colors or normals:
translate_mesh(mesh,
Vec3(5, 0, 0));
scale_mesh(mesh, 2.0f);
rotate_mesh(mesh, 3.14159f / 4,
Vec3(0, 0, 1));
glm::mat4 M = glm::translate(glm::mat4(1.0f), glm::vec3(0, 0, 0));
transform_mesh(mesh, M);
Lighting
Scimesh works in display-referred (sRGB) colour space for simplicity. Lights are defined as structs with position, color, and intensity:
opts.
lights = {key_light, fill_light};
opts.gamma = 2.2f;
A light source for the Blinn-Phong shading model.
Color color
Color of the light (default: white).
Vec3 position
Position of the light.
float intensity
Brightness multiplier (default: 1.0).
When no lights are specified, a single headlight at (0, 0, 1) is used.
Contrast
opts.contrast (default 1.0, no change) applies an S-curve contrast stretch after shading via (value - 0.5) * contrast + 0.5. Values > 1.0 push darks toward black and lights toward white. Typical values: 1.1–1.3 for subtle contrast, up to 1.5 for dramatic.
Ambient
opts.ambient (default 0.3) controls how much light reaches surfaces facing away from the light source. Lower values produce deeper shadows. Typical values are 0.1–0.3.
Post-Processing
Image::apply_contrast() applies the same S-curve to an already rendered image:
bool write_png(const std::string &filename) const
Write the image as a PNG file.
Semi-Transparent Meshes
For transparency, set per-vertex colors with alpha < 1 and mark the mesh:
for (auto &c : glass_mesh.colors) c.a = 0.3f;
glass_mesh.has_transparency = true;
scene.
meshes.push_back(opaque_mesh);
scene.
meshes.push_back(glass_mesh);
Camera Helpers
Auto-framing
Camera cam = camera_fit_mesh(mesh,
45.0f,
1.05f
);
Camera cam = camera_fit_scene(scene, view_dir, up, 45.0f, 1.1f);
Orthographic projection
Orbit paths
camera_orbit() rotates a camera's eye and up around its center:
Camera cam = camera_fit_mesh(mesh, view_dir, up, 45.0f);
for (int i = 0; i < 48; i++) {
Camera orbit_cam = camera_orbit(cam,
Vec3(0, 0, 1), 360.0f / 48 * i);
}
This is useful for turntable video sequences. See examples/cpp/brain_video/ for a complete example.
Anti-Aliasing
Set aa_samples to 2 or 4 for supersampled anti-aliasing:
Higher values produce smoother edges but use more memory and time.
Examples
The examples/cpp/ directory contains complete, runnable programs:
| Example | What it demonstrates |
spot_cow/ | Textured OBJ mesh with multi-light setup and SSAO |
transparency/ | Semi-transparent overlays with FreeSurfer surfaces |
protein_data_bank_pdb_file/ | Protein visualization from PDB files |
whole_brain_sulc/ | Whole-brain sulcal depth rendering |
whole_brain_sulc_fsaverage/ | Same on fsaverage template |
whole_brain_annot/ | Cortical parcellation coloring |
brain_video/ | Turntable animation — 48 orbit frames via camera_orbit() |
Each example has its own CMakeLists.txt. To build and run:
cd examples/cpp/spot_cow
mkdir -p build && cd build
cmake ..
make
./spot_cow
Run all examples at once:
./examples/cpp/run_all_cpp.sh
Common Pitfalls
Black images / nothing visible:
- Check that near/far planes contain your geometry. The defaults work for unit-scale meshes; for very large or distant scenes, adjust
opts.near_plane and opts.far_plane.
- Make sure your camera is looking at the mesh. Use
camera_fit_mesh() to auto-frame.
Inverted faces / missing triangles:
- Triangle winding order matters for backface culling. If faces disappear when rotating, try
opts.backface_culling = false or opts.invert_normals = true.
Open surfaces:
- For surface that are open (not watertight), set
opts.backface_culling = false so both sides are visible.
Transparency not working:
- Make sure the mesh has
has_transparency = true and vertex colors with alpha < 1. Transparent meshes are rendered back-to-front.
- Note that you can also set the background color for the rendered image to transparent.
Performance:
- A 300k triangle mesh at 1200x900 with 2x AA renders in ~1-3 seconds. Higher AA samples and SSAO add cost. Use
opts.threads = 0 for auto-parallelization (requires OpenMP).