scimesh 0.3.2
Headless CPU-only 3D software renderer for scientific mesh visualization
Loading...
Searching...
No Matches
Getting Started with scimesh C++

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:

#include <scimesh/camera.h>
#include <scimesh/image.h>
using namespace scimesh;
int main() {
// 1. Create a mesh — a red cuboid
Mesh cube = generate_cuboid(
Vec3(0, 0, 0), // center
Vec3(1, 1, 1), // half-extents
Color(0.9f, 0.2f, 0.2f, 1) // RGBA color
);
// 2. Set up camera — auto-frame to fit the mesh
Vec3 view_dir = glm::normalize(Vec3(1.0f, 1.0f, 1.0f));
Camera cam = camera_fit_mesh(cube, view_dir,
Vec3(0, 1, 0), // up
45.0f, // FOV
1.1f // margin
);
// 3. Configure rendering
opts.width = 800;
opts.height = 600;
opts.background_color = Color(1, 1, 1, 1); // white background
opts.shading = ShadingMode::SMOOTH;
// 4. Render
Renderer renderer;
Image img = renderer.render_mesh(cube, cam, opts);
// 5. Save
img.write_ppm("cube.ppm");
img.write_bmp("cube.bmp");
img.write_tga("cube.tga"); // uncompressed true-color, no dependencies
return 0;
}
Camera definition and helper functions for view setup.
The main rendering engine.
Definition renderer.h:73
Image render_mesh(const Mesh &mesh, const Camera &camera, const RenderOptions &options)
Render a single mesh to an image.
Definition renderer.cpp:31
The Image — an RGBA pixel buffer with compositing and I/O operations.
glm::vec3 Vec3
3-component floating-point vector (xyz).
Definition types.h:46
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.
Definition camera.h:77
An RGBA color with floating-point components.
Definition types.h:88
A 2D RGBA image buffer.
Definition image.h:87
bool write_tga(const std::string &filename, bool use24bit=false) const
Write the image as a TGA file (Truevision Targa).
Definition image.cpp:471
bool write_ppm(const std::string &filename) const
Write the image as a PPM file (Portable Pixmap).
Definition image.cpp:392
bool write_bmp(const std::string &filename) const
Write the image as a BMP file (Windows Bitmap).
Definition image.cpp:405
A 3D triangle mesh using an indexed face set representation.
Definition mesh.h:76
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:

#include <scimesh/camera.h>
#include <scimesh/ply_io.h>
#include <scimesh/image.h>
using namespace scimesh;
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.width = 800;
opts.height = 600;
opts.background_color = Color(0.98f, 0.98f, 0.98f, 1);
opts.shading = ShadingMode::SMOOTH;
Renderer renderer;
Image img = renderer.render_mesh(mesh, cam, opts);
img.write_ppm("bunny.ppm");
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:

struct Mesh {
std::vector<Vec3> vertices; // vertex positions
std::vector<Triangle> triangles; // triangle index triplets
std::vector<Color> colors; // per-vertex RGBA (optional)
std::vector<Color> face_colors; // per-face RGBA (optional)
std::vector<Vec3> normals; // per-vertex normals (optional)
std::vector<Vec2> uvs; // texture coordinates (optional)
Image texture; // texture image (optional)
Color default_color;
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.
Definition types.h:127

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 scene;
scene.meshes.push_back(mesh_a);
scene.meshes.push_back(mesh_b);
A collection of Mesh objects to be rendered together.
Definition scene.h:57
std::vector< Mesh > meshes
The meshes in this scene, drawn in order.
Definition scene.h:59

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:

Camera cam;
cam.eye = Vec3(0, 0, 5); // where the camera is
cam.center = Vec3(0, 0, 0); // what it looks at
cam.up = Vec3(0, 1, 0); // up direction
cam.projection = ProjectionType::PERSPECTIVE;
cam.fov_degrees = 45.0f;
ProjectionType projection
Which projection type to use.
Definition camera.h:99
Vec3 center
The point the camera looks at.
Definition camera.h:88
Vec3 up
The camera's "up" direction.
Definition camera.h:94
Vec3 eye
Camera position in world space.
Definition camera.h:82
float fov_degrees
Vertical field of view in degrees (perspective only).
Definition camera.h:109

Use the auto-framing helpers to avoid manual positioning:

// Fit a single mesh
Camera cam = camera_fit_mesh(mesh, view_dir, up, 45.0f, 1.1f);
// Fit an entire scene
Camera cam = camera_fit_scene(scene, view_dir, up, 45.0f, 1.1f);

RenderOptions

Controls image size, shading, lighting, and post-processing:

opts.width = 1200;
opts.height = 900;
opts.shading = ShadingMode::SMOOTH; // or FLAT
opts.backface_culling = true;
opts.background_color = Color(0, 0, 0, 0); // transparent
// Lighting
opts.ambient = 0.2f;
opts.contrast = 1.1f;
opts.lights = {key_light, fill_light, rim_light};
opts.specular_color = Color(0.4f, 0.4f, 0.4f);
opts.shininess = 64.0f;
// Anti-aliasing
opts.aa_samples = 2; // 2x2 SSAA
// SSAO
opts.ssao_enabled = true;
opts.ssao_radius = 12.0f;
opts.ssao_intensity = 0.5f;
// Projection
opts.projection = ProjectionType::ORTHOGRAPHIC;
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:

Renderer renderer;
// Single mesh
Image img = renderer.render_mesh(mesh, cam, opts);
// Multi-mesh scene
Image img = renderer.render_scene(scene, cam, opts);
// Raw triangles (no index buffer)
Image img = renderer.render_triangles_raw(positions, colors, cam, opts);
// Points
Image img = renderer.render_points_raw(positions, colors, radius, cam, opts);
Image render_scene(const Scene &scene, const Camera &camera, const RenderOptions &options)
Render a scene (collection of meshes) to an image.
Definition renderer.cpp:43
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.
Definition renderer.cpp:77
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.
Definition renderer.cpp:54

Image

Image stores RGBA pixels and can write to several formats:

Image img(width, height);
img.write_ppm("output.ppm"); // ASCII PPM
img.write_bmp("output.bmp"); // Windows BMP
// Direct pixel access
img.set_pixel(x, y, r, g, b, a);
img.get_pixel(x, y, r, g, b, a);
img.clear(r, g, b, a);
// Bilinear texture sampling
Color c = img.sample_bilinear(u, v);
// Post-processing
img.apply_contrast(1.1f); // S-curve contrast stretch
// Downsample (e.g., for anti-aliasing)
Image small = img.downsample_box(2);
void apply_contrast(float contrast)
Apply a contrast adjustment to the image.
Definition image.cpp:379
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.
Definition image.cpp:39
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.
Definition image.cpp:49
Image downsample_box(int factor) const
Downsample the image by a factor using box filtering.
Definition image.cpp:77
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).
Definition image.cpp:61
Color sample_bilinear(float u, float v) const
Sample the image at texture coordinates (u, v) using bilinear interpolation.
Definition image.cpp:351

Mesh I/O

scimesh can read and write several mesh formats:

STL

#include <scimesh/stl_io.h>
Mesh mesh = stl_io::read("model.stl");
stl_io::write(mesh, "output.stl"); // binary
stl_io::write(mesh, "output.stl", true); // ASCII
Read and write STL files (.stl).

Wavefront OBJ

#include <scimesh/obj_io.h>
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

#include <scimesh/ply_io.h>
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(
Vec3(0, 0, 0), // center
1.0f, // radius
32, // segments
Color(0, 0.8f, 1, 1)
);
Mesh cylinder = generate_cylinder(
Vec3(0, 0, 0), // start
Vec3(0, 3, 0), // end
0.5f, // radius
16, // segments
Color(1, 0.5f, 0, 1)
);
Mesh cuboid = generate_cuboid(
Vec3(0, 0, 0), // center
Vec3(1, 2, 0.5f), // half-extents
Color(0.2f, 0.6f, 1, 1)
);
Mesh torus = generate_torus(
Vec3(0, 0, 0), // center
1.5f, // major radius
0.4f, // minor radius
32, 16, // major/minor segments
Color(1, 0.5f, 0, 1)
);
Mesh cone = generate_cone(
Vec3(0, 0, 0), // base
Vec3(0, 2, 0), // tip
0.8f, // radius
16, // segments
Color(0.5f, 0, 0.5f, 1)
);
Mesh plane = generate_plane(
Vec3(0, 0, 0), // center
Vec3(0, 1, 0), // normal
2.0f, 2.0f, // half-sizes
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 atoms = generate_multi_spheres(centers, radii, colors, 16);
// Cylinders between point pairs
Mesh bonds = generate_multi_cylinders(starts, ends, radii, colors, 12);
Mesh generate_multi_spheres(const std::vector< Vec3 > &centers, 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)); // 45 degrees around Z
// Arbitrary 4x4 matrix
glm::mat4 M = glm::translate(glm::mat4(1.0f), glm::vec3(0, 0, 0));
transform_mesh(mesh, M);
Mesh transformation functions: translate, scale, rotate, and general matrix transforms.

Lighting

Scimesh works in display-referred (sRGB) colour space for simplicity. Lights are defined as structs with position, color, and intensity:

Light key_light;
key_light.position = Vec3(0.5f, 1.0f, 1.0f);
key_light.color = Color(1.0f, 0.97f, 0.90f);
key_light.intensity = 1.5f;
Light fill_light;
fill_light.position = Vec3(-1.0f, 0.2f, 0.5f);
fill_light.color = Color(0.4f, 0.5f, 0.8f);
fill_light.intensity = 0.5f;
opts.lights = {key_light, fill_light};
opts.ambient = 0.2f;
opts.gamma = 2.2f;
opts.specular_color = Color(0.4f, 0.4f, 0.4f);
opts.shininess = 64.0f;
A light source for the Blinn-Phong shading model.
Definition types.h:179
Color color
Color of the light (default: white).
Definition types.h:189
Vec3 position
Position of the light.
Definition types.h:184
float intensity
Brightness multiplier (default: 1.0).
Definition types.h:194

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:

Image img = renderer.render_mesh(mesh, cam, opts);
img.apply_contrast(1.1f); // S-curve contrast stretch
img.write_png("output.png");
bool write_png(const std::string &filename) const
Write the image as a PNG file.
Definition image.cpp:521

Semi-Transparent Meshes

For transparency, set per-vertex colors with alpha < 1 and mark the mesh:

Mesh glass_mesh = /* ... */;
for (auto &c : glass_mesh.colors) c.a = 0.3f;
glass_mesh.has_transparency = true;
Scene scene;
scene.meshes.push_back(opaque_mesh);
scene.meshes.push_back(glass_mesh);
// The renderer sorts transparent meshes back-to-front
Image img = renderer.render_scene(scene, cam, opts);

Camera Helpers

Auto-framing

// Look from a direction, auto-distance to fit
Camera cam = camera_fit_mesh(mesh,
Vec3(-1, 0, 0.2f), // view direction
Vec3(0, 0, 1), // up
45.0f, // FOV
1.05f // 5% margin
);
// Fit an entire scene
Camera cam = camera_fit_scene(scene, view_dir, up, 45.0f, 1.1f);

Orthographic projection

cam.projection = ProjectionType::ORTHOGRAPHIC;
cam.fov_degrees = 40.0f; // orthographic extent

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);
Image img = renderer.render_mesh(mesh, orbit_cam, opts);
// write frame...
}

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:

opts.aa_samples = 2; // 2x2 SSAA — renders at 2x resolution, downsamples

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).