C and C++ API
Load, edit and evaluate grooms from your own program.
The SDK has one library and two headers:
groomlab/groomlab_c.h: the C API. Plain C99, opaque handles, error codes. This is the stable interface: see What is stable.groomlab/groomlab.hpp: a header-only C++17 wrapper over it. RAII handles, views into results, exceptions. It builds with any compiler, because only C crosses the library boundary.
The SDK
GroomLabSDK/
bin/ groomlab.dll
lib/ groomlab.lib (the import library)
include/ groomlab/groomlab_c.h, groomlab/groomlab.hpp
cmake/ GroomLabSDKConfig.cmake (find_package)
samples/ groom_to_obj.cpp, groom_counts.c, CMakeLists.txt, LICENSE.txtWith CMake, point CMAKE_PREFIX_PATH at the GroomLabSDK folder and link GroomLab::groomlab:
find_package(GroomLabSDK 1.0 CONFIG REQUIRED)
target_link_libraries(my_bridge PRIVATE GroomLab::groomlab)Without CMake: add include/ to the include path, link lib/groomlab.lib, and put
groomlab.dll where Windows finds it (see DLLs).
Build the samples to check your setup:
cmake -S GroomLabSDK/samples -B build
cmake --build build --config Release
build/Release/groom_to_obj --groom hair.groom --frame 1 hair.objEvaluate a groom in 20 lines
#include <cstdio>
#include "groomlab/groomlab.hpp"
int main() {
namespace gl = groomlab::api;
try {
gl::Session session = gl::Session::fromFile("hair.groom");
session.setFrame(12); // seconds = frame / the groom's frame rate
session.setFloat("noise", "amplitude", 0.1); // any node's param, by node name
gl::Strands strands = session.evaluate();
gl::View<float> p = strands.positions(); // x y z per point
gl::View<std::uint64_t> first = strands.offsets();
for (std::size_t s = 0; s < strands.strandCount(); ++s) {
const std::uint64_t root = first[s]; // the strand's points: first[s] to first[s + 1] - 1
std::printf("strand %zu: root %g %g %g\n", s, p[3 * root], p[3 * root + 1], p[3 * root + 2]);
}
} catch (const gl::Error& e) {
std::fprintf(stderr, "%s\n", e.what()); // e.code(): the gl_result
return 1;
}
}The same in C
#include <stdio.h>
#include "groomlab/groomlab_c.h"
int main(void) {
gl_session* session = NULL;
gl_strands* strands = NULL;
if (gl_session_create(&session) != GL_OK || gl_session_load_file(session, "hair.groom") != GL_OK ||
gl_session_set_frame(session, 12) != GL_OK ||
gl_session_set_param_float(session, "noise", "amplitude", 0.1) != GL_OK ||
gl_session_evaluate(session, NULL, &strands) != GL_OK) {
fprintf(stderr, "%s\n", gl_last_error()); /* before any other gl_ call */
gl_session_destroy(session);
return 1;
}
const float* p = gl_strands_positions(strands);
const uint64_t* first = gl_strands_offsets(strands);
for (size_t s = 0; s < gl_strands_strand_count(strands); ++s) {
const uint64_t root = first[s];
printf("strand %zu: root %g %g %g\n", s, p[3 * root], p[3 * root + 1], p[3 * root + 2]);
}
gl_strands_release(strands);
gl_session_destroy(session);
return 0;
}Rules
Sessions and results
A session (gl_session, C++ Session) holds one groom and keeps it evaluated. Hand it inputs
(a file, a scalp, a time, params), then evaluate. Only what changed since the last evaluation runs
again.
A result (gl_strands, C++ Strands) is one evaluation's strands. It does not depend on the
session: it stays valid and unchanged after more evaluations, edits, or the session's
destruction, until you release it.
Errors
- Every function that can fail returns a
gl_result:GL_OK(0) or an error code. gl_last_error()gives the message of the last failing call on this thread. It is valid until your nextgl_call, and""after a call that succeeded.- On failure every out-parameter is set to
NULLor 0. - A failing call leaves the session as it was. A bad file, graph or param keeps the previous one.
- The C++ wrapper throws
groomlab::api::Error:code()is thegl_result,message()the text.
| Code | When |
|---|---|
GL_ERROR_INVALID_ARGUMENT | A null handle or pointer, a size that does not add up. |
GL_ERROR_NOT_FOUND | No such node, param, attribute or operator type. |
GL_ERROR_TYPE_MISMATCH | A param read or written as another kind than it is. |
GL_ERROR_IO | A file that cannot be read or written. |
GL_ERROR_PARSE | Text that is not a groom file, or not JSON of the expected form. |
GL_ERROR_GRAPH | A graph that does not load (an unknown operator, a bad connection, a newer node). |
GL_ERROR_EVALUATION | An evaluation that gave no strands. |
GL_ERROR_STATE | A call the session cannot take now (scalp points with no scalp). |
GL_ERROR_OUT_OF_MEMORY | Out of memory. |
GL_ERROR_INTERNAL | Anything else: a bug, reported with its message. |
An evaluation can also half-succeed: a node fails, the rest still runs. Then it returns GL_OK
and gl_session_eval_errors_json lists what went wrong (a JSON array of strings, [] when
nothing did). Show those lines to the artist.
Ownership and lifetime
- Your buffers stay yours. The library copies what it keeps before the call returns.
- Text and bytes the library hands out through
char**out-parameters are yours to release withgl_free(). Neverfree(). Text is UTF-8 and NUL-terminated. The C++ wrapper copies them intostd::stringand frees them for you. - Results live until
gl_strands_release()(C++: theStrandsobject's destructor). Pointers their accessors return, and C++Views, are valid until then. NULLis fine forgl_session_destroy,gl_strands_releaseandgl_free.- Paths are UTF-8.
Threads
Use a session from one thread at a time; it evaluates in parallel inside, on the library's own threads (one per core, started at the first evaluation and kept until the process ends). Separate sessions can run on separate threads. A result is read-only and can be read from any thread.
Units and axes
Points are in the groom's scene units (the file's "units", cm by default) and its up axis
("upAxis", Y by default). A Z-up host turns the groom with gl_session_set_up_axis(session, "Z")
(C++: session.setUpAxis("Z")): the core turns the scalp, rest pose, guides, colliders and every
param that is a world direction (a spec's "worldDirection"), as Maya's import and the app do.
Y-up (x, y, z) is Z-up (x, -z, y). The library never rescales: a host in another unit scales
points on its own way in and out. Writing a host bridge
walks through Maya, Blender and Unreal.
Time
gl_session_set_frame and gl_frame_to_seconds turn frames into seconds by the rule every host
keeps: frame / fps, with an NTSC rate (23.976, 29.97, 59.94) exact. A frame is then the same time
here as in Maya, the CLI and the Arnold procedural, to the bit.
Attributes in two scopes
A name can be there once per scope: a banded HairColor writes colorR, colorG and colorB per
strand and per point. gl_strands_attribute gives the per strand one; gl_strands_attribute_info
says each listed attribute's scope, and gl_strands_attribute_scoped reads a name in the scope you
ask for (C++: attributes(), attribute(name, scope)).
The scalp fingerprint
A loaded groom remembers the scalp it was made on. While the scalp you set has other triangles or
another point order, gl_session_eval_errors_json lists "the scalp's vertex order or triangles
differ from the groom's: strands are rooted again by position". See
the format.
DLLs
groomlab.dll is the only DLL you ship. Besides Windows' own it needs the Visual C++ 2015-2022
runtime (msvcp140.dll, vcruntime140.dll, vcruntime140_1.dll), which DCC hosts already
load. Windows looks for groomlab.dll beside the program (the host's .exe), on PATH, or
in a folder you add. It does not look beside your plugin.
- A standalone tool: copy
groomlab.dllbeside it (the samples' CMake does this). - A plugin inside a host: link with
/DELAYLOAD:groomlab.dll(anddelayimp.lib) and load it yourself before the firstgl_call, withLoadLibraryExW(path, NULL, LOAD_WITH_ALTERED_SEARCH_PATH)on the full path of the SDK'sgroomlab.dll.
No TBB
The library runs its parallel loops on threads of its own, not on TBB. A host that loaded a
tbb.dll of its own (Houdini, Blender and Maya each ship one) keeps it, and GroomLab never
meets it. Once loaded, the library stays in the process until it ends, as its threads do:
FreeLibrary leaves it loaded.
Function reference
Generated from the comments in groomlab_c.h, which are the source of truth. The C++ wrapper
mirrors these one to one: gl_session_set_param_float(s, node, param, v) is
session.setFloat(node, param, v), gl_strands_positions(r) is strands.positions(), and so on.
Results
typedef int32_t gl_result;
enum {
GL_OK = 0,
GL_ERROR_INVALID_ARGUMENT = 1, /* a null handle or pointer, a size that does not add up */
GL_ERROR_NOT_FOUND = 2, /* no such node, param, attribute or operator type */
GL_ERROR_TYPE_MISMATCH = 3, /* a param read or written as another kind than it is */
GL_ERROR_IO = 4, /* a file that cannot be read or written */
GL_ERROR_PARSE = 5, /* text that is not a groom file / JSON of the expected form */
GL_ERROR_GRAPH = 6, /* a graph that does not load (unknown operator, bad connection) */
GL_ERROR_EVALUATION = 7, /* an evaluation that gave no strands (gl_session_eval_errors_json) */
GL_ERROR_STATE = 8, /* a call the session cannot take now (scalp points with no scalp) */
GL_ERROR_OUT_OF_MEMORY = 9,
GL_ERROR_INTERNAL = 10, /* anything else: a bug, reported with its message */
GL_ERROR_LICENSE = 11 /* an export of a groom saved in Learning mode (gl_session_check_export) */
};uint32_t gl_api_version(void);(major << 16) | minor of the library.
const char* gl_version_string(void);The GroomLab release the library was built from, e.g. "0.1.0" (static storage).
const char* gl_last_error(void);The message of the last failing call on this thread; "" after a call that succeeded. Valid until the next gl_* call on this thread. Never NULL.
const char* gl_result_name(gl_result result);A gl_result's name, e.g. "GL_ERROR_NOT_FOUND" (static storage).
void gl_free(void* buffer);Releases a buffer the library handed out (char** out-parameters). NULL is fine.
double gl_frame_to_seconds(double frame, double fps);Frame frame (fractional for subframes) at fps frames per second, in seconds, by the rule every host keeps: frame / fps, a rate within a hair of an NTSC one (n * 1000 / 1001: 23.976, 29.97, 59.94) taken as that rate exactly; a rate of 0 or less as 24. Since API 1.2.
Operators
gl_result gl_operator_types_json(char** out_json);Every registered operator type name, as a JSON array of strings.
gl_result gl_operator_specs_json(char** out_json);Every operator's spec (inputs, outputs, params with kind, default, limits, groups...) as a JSON array, the form the core's editors read.
gl_result gl_operator_spec_json(const char* type, char** out_json);One operator's spec (an object of the array above). GL_ERROR_NOT_FOUND for an unknown type.
gl_result gl_default_graph_json(char** out_json);The graph a new session evaluates (hair out of the box), as JSON.
Sessions
typedef struct gl_session gl_session;
typedef struct gl_strands gl_strands;gl_result gl_session_create(gl_session** out_session);A session with the default graph and no inputs (no scalp: evaluations give no strands).
void gl_session_destroy(gl_session* session);NULL is fine. Strands it gave stay valid.
gl_result gl_session_load_file(gl_session* session, const char* path);Replaces the session's groom with a .groom file's (graph, guides, scalp, parts, flow, correctives, controls, maps, frame rate); relative paths in it (texture maps) resolve against the file's folder, then its search paths. Animated caches the file names (scalp and guide Alembic caches, simulation bakes) are not opened: the host feeds the moving scalp itself (gl_session_update_scalp_points). GL_ERROR_IO, GL_ERROR_PARSE, GL_ERROR_GRAPH.
gl_result gl_session_load_memory(gl_session* session, const void* data, size_t size, const char* base_dir);The same from a .groom file's text in memory (size bytes, no NUL needed). base_dir (may be NULL) stands for the file's folder.
gl_result gl_session_save_file(gl_session* session, const char* path);Writes the session's groom as a .groom file (atomically: written beside, then moved over): the graph, guides, parts, flow, correctives, controls and the scalp as last set (after gl_session_update_scalp_points: those points), with what the loaded file carried besides.
gl_result gl_session_save_memory(gl_session* session, char** out_data, size_t* out_size);The same into memory: *out_data (NUL-terminated, release with gl_free), *out_size bytes without the NUL. out_size may be NULL.
gl_result gl_session_license(gl_session* session, char** out_license);The groom's license tag (a .groom file's "license"): "learning" for a groom saved in GroomLab's Learning mode (the free, non-commercial mode), "" otherwise. Saving keeps it. Release with gl_free. Since API 1.3.
gl_result gl_session_check_export(gl_session* session);Whether the session's groom may be exported (written out as an asset: curves, an engine's groom, cards): GL_OK, or GL_ERROR_LICENSE (the reason in gl_last_error()) for a groom saved in Learning mode, which no GroomLab host exports. A bridge calls it before writing the strands to a file; evaluating and rendering any groom is free. The library checks no license and never goes online. Since API 1.3.
gl_result gl_session_set_scalp(gl_session* session, const float* points, size_t point_count, const int32_t* face_counts, size_t face_count, const int32_t* face_indices, size_t index_count, const float* uvs);The scalp, in world space at the evaluation time: point_count points (3 floats each), face_count polygons of face_counts[i] corners (3 or more) whose point indices follow one another in face_indices (index_count = the sum of face_counts). uvs (may be NULL): 2 floats per polygon corner, in face_indices' order (a face-varying UV set). Polygons are triangulated as fans from their first corner (corners 0, i, i + 1); Catmull-Clark (the graph's Scalp node subdivision) refines the polygons, not the triangles. Replaces the scalp and its rest pose. A loaded groom keeps the fingerprint of the scalp it was made on (its file's scalp "topology": its point count and triangles); while the scalp set has another (another vertex order, point count or triangulation: a quad split across its other diagonal too), every evaluation lists "the scalp's vertex order or triangles differ from the groom's: strands are rooted again by position" in gl_session_eval_errors_json (the strands come all the same). To match a groom exactly, feed the file's own triangles (3-corner faces in its "triangles" order).
gl_result gl_session_update_scalp_points(gl_session* session, const float* points, size_t point_count, const float* rest_points);The scalp's points at another time (a deforming scalp, every frame), on the topology and UVs set before: point_count must be the scalp's. rest_points (may be NULL; point_count points): the rest pose (an undeformed scalp: strands keep their count and roots while it deforms; also what follow-the-scalp groom files evaluate on); NULL keeps the rest pose as it was. GL_ERROR_STATE without a scalp, GL_ERROR_INVALID_ARGUMENT for another point count.
gl_result gl_session_scalp_size(gl_session* session, size_t* out_point_count, size_t* out_triangle_count);The scalp as given (0, 0 without one). Either pointer may be NULL.
gl_result gl_session_get_up_axis(gl_session* session, char** out_axis);The groom's up axis, "Y" or "Z" (a loaded file's "upAxis"; Y for a new session). Release with gl_free. Since API 1.2.
gl_result gl_session_set_up_axis(gl_session* session, const char* axis);Turns the groom into another up axis, "Y" or "Z" (GL_ERROR_INVALID_ARGUMENT for another): the scalp and its rest pose, the guides, the colliders, and the graph's params that are world directions (a spec's "worldDirection": Gravity's, Wind's, Comb's, Simulate's directions, Mirror's axis) a quarter turn about X (Z-up (x, y, z) is Y-up (x, z, -y)), as Maya's import and the app turn a file. Call it after loading a file, before feeding scalp points of the host's own axis; saved files keep it. Nothing when the groom is in that axis already. Since API 1.2.
gl_result gl_session_set_time(gl_session* session, double seconds);
gl_result gl_session_get_time(gl_session* session, double* out_seconds);The evaluation time in seconds (0 by default).
gl_result gl_session_set_frame(gl_session* session, double frame);The time as a frame at the session's frame rate: seconds = gl_frame_to_seconds(frame, fps).
gl_result gl_session_set_frames_per_second(gl_session* session, double fps);
gl_result gl_session_get_frames_per_second(gl_session* session, double* out_fps);Frames per second (24 by default; a groom file's own when loaded). Must be above 0.
gl_result gl_session_set_control(gl_session* session, const char* name, float value);A Control node's value by name (a host's live attribute driving the graph).
gl_result gl_session_set_search_paths(gl_session* session, const char* const* directories, size_t count);Where relative file paths in the graph (texture maps) resolve, after a loaded file's own folder: count UTF-8 directories (environment variables allowed); 0 clears them. Saved with the file.
Evaluation
typedef struct gl_eval_options {
uint32_t struct_size; /* sizeof(gl_eval_options) as the caller knows it: gl_eval_options_init */
float density; /* the preview density, 0-1 (1: every strand) */
double point_budget; /* points the result may hold (strands thinned to fit); 0: no limit */
const char* view_node; /* a Strands node to show instead of the graph's output; NULL or "": the output */
} gl_eval_options;void gl_eval_options_init(gl_eval_options* options);Fills options with the defaults (density 1, no budget, the output) and its struct_size.
gl_result gl_session_evaluate(gl_session* session, const gl_eval_options* options, gl_strands** out_strands);Evaluates the groom at the session's time. options may be NULL (the defaults). On GL_OK *out_strands holds the result (release with gl_strands_release); it may hold no strands (no scalp, a graph that makes none). A graph that fails gives GL_ERROR_EVALUATION with the reasons in gl_last_error() (and gl_session_eval_errors_json). Only what changed since the last evaluation runs again (the graph's cache).
gl_result gl_session_eval_errors_json(gl_session* session, char** out_json);What the last evaluation said went wrong (a node that failed, input that did not parse), one string per line, as a JSON array; [] when nothing did. A partial result is GL_OK with lines here.
Results
void gl_strands_release(gl_strands* strands);Releases a result; NULL is fine.
size_t gl_strands_strand_count(const gl_strands* strands);
size_t gl_strands_point_count(const gl_strands* strands);Counts (0 for NULL).
const float* gl_strands_positions(const gl_strands* strands);Point positions: 3 floats (x, y, z) per point, strand after strand. NULL when there are none.
const float* gl_strands_widths(const gl_strands* strands);Width (a diameter, scene units) per point.
const uint64_t* gl_strands_ids(const gl_strands* strands);Each strand's stable ID (the same strand keeps it from frame to frame and at other densities).
const int32_t* gl_strands_point_counts(const gl_strands* strands);Points per strand (strand_count values).
const uint64_t* gl_strands_offsets(const gl_strands* strands);Each strand's first point (strand_count + 1 values: the last is point_count).
enum { GL_SCOPE_STRAND = 0, GL_SCOPE_POINT = 1 };Named attributes, float data. Scope: one value tuple per strand or per point. A name may be there twice, once per scope (HairColor's banded colorR/G/B: per strand and per point).
size_t gl_strands_attribute_count(const gl_strands* strands);
const char* gl_strands_attribute_name(const gl_strands* strands, size_t index);The attributes this result has: every float channel the graph wrote (per strand or per point: melanin, melaninRedness, colorR/G/B, roughness, clumpId, groupId, iridescence...), and these the core derives:
- "width" per point, 1 component (as
gl_strands_widths) - "rootUV" per strand, 2 components: the scalp's UV at the root (with scalp UVs)
- "color" per strand, 3 components: linear RGB (HairColor's RGB, else from melanin)
- "pointColor" per point, 3 components: linear RGB, when the color varies along strands
A channel of the same name and scope as a derived attribute wins. Names are in a fixed order (per strand ones first); the returned name is valid while the result lives. NULL for an index out of range.
gl_result gl_strands_attribute(const gl_strands* strands, const char* name, const float** out_data, int32_t* out_scope, int32_t* out_components);An attribute's data (*out_data: components values per strand or point, valid while the result lives; NULL when it has no elements), its scope and component count. Any out-pointer but out_data may be NULL. A name there in both scopes gives the per strand one (gl_strands_attribute_scoped picks). GL_ERROR_NOT_FOUND for a name it does not have.
gl_result gl_strands_attribute_info(const gl_strands* strands, size_t index, int32_t* out_scope, int32_t* out_components);The attribute at index (as gl_strands_attribute_name lists them): its scope and component count (either may be NULL). GL_ERROR_NOT_FOUND for an index out of range. Since API 1.2.
gl_result gl_strands_attribute_scoped(const gl_strands* strands, const char* name, int32_t scope, const float** out_data, int32_t* out_components);An attribute by name and scope (GL_SCOPE_STRAND or GL_SCOPE_POINT): its data and component count, as gl_strands_attribute gives them (out_components may be NULL). GL_ERROR_NOT_FOUND when it has no attribute of that name in that scope. Since API 1.2.
Graph
Nodes are named by their name in the graph (top level). Params hold JSON values: a float or int param a number, a bool true/false, a vec3 an array of 3 numbers, a string a string, a ramp an array of [position, value] pairs; a mappable number may hold a map instead (an object). A param a node does not set reads as its operator's default. Setting a param re-evaluates only the nodes it affects.
gl_result gl_session_get_graph_json(gl_session* session, char** out_json);The graph as JSON (the default graph's text for a session that has none of its own).
gl_result gl_session_set_graph_json(gl_session* session, const char* graph_json);Replaces the graph. GL_ERROR_GRAPH for one that does not load (the session keeps its graph).
gl_result gl_session_node_count(gl_session* session, size_t* out_count);How many nodes the graph has (top level).
gl_result gl_session_node_name(gl_session* session, size_t index, char** out_name);The node at index (in the graph's order). GL_ERROR_NOT_FOUND past the end.
gl_result gl_session_node_type(gl_session* session, const char* node, char** out_type);A node's operator type, e.g. "Scatter".
gl_result gl_session_node_spec_json(gl_session* session, const char* node, char** out_json);A node's ports and params as the editors see them (gl_operator_spec_json's form, a Compound's own inputs and exposed params included).
gl_result gl_session_upgrade_node(gl_session* session, const char* node, int32_t* out_upgraded);Raises a node to its operator's newest behaviour version, as the editors' Upgrade does: a node saved with an older one ("outdated" in gl_session_node_spec_json, its "nodeVersion" below the spec's "version") then evaluates as new nodes do, and params the newer version added take their defaults. The look may change. *out_upgraded (may be NULL): 1 when the node was raised, 0 when it had the newest version already (nothing changes). GL_ERROR_NOT_FOUND for no such node. Since API 1.1.
gl_result gl_session_get_param_json(gl_session* session, const char* node, const char* param, char** out_json);
gl_result gl_session_set_param_json(gl_session* session, const char* node, const char* param, const char* value_json);Any param as JSON text.
gl_result gl_session_get_param_float(gl_session* session, const char* node, const char* param, double* out_value);
gl_result gl_session_set_param_float(gl_session* session, const char* node, const char* param, double value);
gl_result gl_session_get_param_int(gl_session* session, const char* node, const char* param, int64_t* out_value);
gl_result gl_session_set_param_int(gl_session* session, const char* node, const char* param, int64_t value);Typed access. GL_ERROR_TYPE_MISMATCH when the param holds (or takes) another kind: a float param takes any number, an int param an integer; a float read of an int param converts.
gl_result gl_session_get_param_bool(gl_session* session, const char* node, const char* param, int32_t* out_value);
gl_result gl_session_set_param_bool(gl_session* session, const char* node, const char* param, int32_t value);Bools are int32_t: 0 false, anything else true.
gl_result gl_session_get_param_vec3(gl_session* session, const char* node, const char* param, double* out_value);
gl_result gl_session_set_param_vec3(gl_session* session, const char* node, const char* param, const double* value);value / out_value: 3 doubles.
gl_result gl_session_get_param_string(gl_session* session, const char* node, const char* param, char** out_value);
gl_result gl_session_set_param_string(gl_session* session, const char* node, const char* param, const char* value);UTF-8 text; *out_value is released with gl_free.