diff --git a/spine-c/docs-new.md b/spine-c/docs-new.md new file mode 100644 index 000000000..4176f74d8 --- /dev/null +++ b/spine-c/docs-new.md @@ -0,0 +1,819 @@ +# spine-c Runtime Documentation + +> **Licensing** +> +> Please see the [Spine Runtimes License](/spine-runtimes-license) before integrating the Spine Runtimes into your applications. + +# Introduction +spine-c is a generic runtime for integrating Spine animations in game engines and frameworks written in C, C++, Swift, Rust, Objective-C or any other language that can interface with C. + +spine-c provides functionality to: +* Load and manipulate [Spine skeletons](/spine-loading-skeleton-data) and [texture atlases](/spine-texture-packer) +* Apply and mix [animations](/spine-applying-animations) with crossfading +* Manage [skins](/spine-runtime-skins) for visual variations +* Manipulate and compute data required for rendering and physics based on the current [skeleton pose, slots & attachments states](/spine-runtime-skeletons) + +The runtime is engine-agnostic. You provide texture loading callbacks and feed the generated render commands into your engine's rendering system. + +spine-c is written in C99 for maximum compatibility. The API is generated from [spine-cpp](/spine-cpp), ensuring completeness and type safety. + +Example integrations: +* [spine-ios](/git/spine-runtimes/tree/spine-ios) - iOS integration +* [spine-flutter](/git/spine-runtimes/tree/spine-flutter) - Flutter integration +* [spine-sdl](/git/spine-runtimes/tree/spine-sdl) - SDL integration +* [spine-glfw](/git/spine-runtimes/tree/spine-glfw) - GLFW integration + +> **Note:** This guide assumes familiarity with [Spine runtime architecture](/spine-runtime-architecture) and terminology. See the [API reference](/spine-api-reference) for detailed function documentation. + +# Integrating spine-c + +## CMake Integration (Recommended) + +The easiest way to integrate spine-c into your project is via CMake FetchContent: + +```cmake +include(FetchContent) +FetchContent_Declare( + spine-c + GIT_REPOSITORY https://github.com/esotericsoftware/spine-runtimes.git + GIT_TAG 4.3 + SOURCE_SUBDIR spine-c +) +FetchContent_MakeAvailable(spine-c) + +# Link against spine-c +target_link_libraries(your_target spine-c) +``` + +This will automatically fetch and build spine-c along with its dependency (spine-cpp). + +Include `spine-c.h` in your code, which provides the complete API. All code samples in this guide assume `#include `. + +## Manual Integration + +If you prefer manual integration: + +1. Download the Spine Runtimes source using git (`git clone https://github.com/esotericsoftware/spine-runtimes`) or download it as a zip +2. Add the required source files to your project: + - Add sources from `spine-cpp/src`, `spine-c/src` +3. Add the include directories: `spine-cpp/include`, `spine-c/include` + +Include `spine-c.h` in your code, which provides the complete API. All code samples in this guide assume `#include `. + +See the [example integrations](#introduction) for complete implementations. + +# Exporting Spine assets for spine-c +![](/img/spine-runtimes-guide/spine-ue4/export.png) + +Follow the Spine User Guide to: +1. [Export skeleton & animation data](/spine-export) to JSON or binary format +2. [Export texture atlases](/spine-texture-packer) containing your skeleton's images + +An export yields these files: + +![](/img/spine-runtimes-guide/spine-ue4/exported-files.png) + +1. `skeleton-name.json` or `skeleton-name.skel`: skeleton and animation data +2. `skeleton-name.atlas`: texture atlas information +3. One or more `.png` files: atlas pages with packed images + +> **Note:** You can pack images from multiple skeletons into a single texture atlas for efficiency. See the [texture packing guide](/spine-texture-packer). + +# Loading Spine assets +spine-c provides APIs to load texture atlases, Spine skeleton data (bones, slots, attachments, skins, animations) and define mix times between animations through [animation state data](/spine-applying-animations#Mix-times). These three types of data, also known as setup pose data, are generally loaded once and then shared by every game object. The sharing mechanism is achieved by giving each game object its own skeleton and [animation state](/spine-applying-animations#AnimationState-API), also known as instance data. + +> **Note:** For a more detailed description of the overall loading architecture consult the generic [Spine Runtime Documentation](/spine-loading-skeleton-data). + +## Loading texture atlases +Texture atlas data is stored in a custom [atlas format](/spine-atlas-format) that describes the location of individual images within atlas pages. The atlas pages themselves are stored as plain `.png` files next to the atlas. + +spine-c provides two approaches for loading atlases: + +### Option 1: Load atlas without textures +Use `spine_atlas_load` to parse the atlas data without loading textures. You'll need to manually load textures for each atlas page: + +```c +// First, load the .atlas file contents into a string +char* atlasData = read_file_to_string("path/to/skeleton.atlas"); + +// Parse the atlas data (doesn't load textures) +spine_atlas_result result = spine_atlas_load(atlasData); + +// Check for errors +if (spine_atlas_result_get_error(result)) { + printf("Error loading atlas: %s\n", spine_atlas_result_get_error(result)); + spine_atlas_result_dispose(result); + exit(1); +} + +spine_atlas atlas = spine_atlas_result_get_atlas(result); +spine_atlas_result_dispose(result); + +// Now manually load textures for each page +spine_array_atlas_page pages = spine_atlas_get_pages(atlas); +int num_pages = spine_array_atlas_page_size(pages); + +for (int i = 0; i < num_pages; i++) { + spine_atlas_page page = spine_array_atlas_page_get(pages, i); + + // Get the texture filename from the atlas + const char* texture_name = spine_atlas_page_get_texture_path(page); + + // Construct full path (you need to know where your textures are) + char full_path[256]; + snprintf(full_path, sizeof(full_path), "%s/%s", atlas_dir, texture_name); + + // Load texture using your engine + void* texture = engine_load_texture(full_path); + + // Store the texture in the page + spine_atlas_page_set_texture(page, texture); + spine_atlas_page_set_width(page, texture_width); + spine_atlas_page_set_height(page, texture_height); +} +``` + +### Option 2: Provide texture loading callbacks +Use `spine_atlas_load_callback` to automatically load textures during atlas parsing: + +```c +// Define callbacks for your engine's texture system +void* my_load_texture(const char* path) { + // path is the full path: atlas_dir + "/" + texture_name + // e.g., "path/to/atlas/dir/skeleton.png" + return engine_load_texture(path); +} + +void my_unload_texture(void* texture) { + engine_unload_texture(texture); +} + +// First, load the .atlas file contents into a string +char* atlasData = read_file_to_string("path/to/skeleton.atlas"); + +// Load atlas with automatic texture loading +spine_atlas_result result = spine_atlas_load_callback( + atlasData, // Atlas file contents as string + "path/to/atlas/dir", // Directory where texture files are located + my_load_texture, // Your texture load function + my_unload_texture // Your texture unload function +); + +// Check for errors +if (spine_atlas_result_get_error(result)) { + printf("Error loading atlas: %s\n", spine_atlas_result_get_error(result)); + spine_atlas_result_dispose(result); + exit(1); +} + +spine_atlas atlas = spine_atlas_result_get_atlas(result); +spine_atlas_result_dispose(result); +``` + +## Loading skeleton data +Skeleton data (bones, slots, attachments, skins, animations) can be exported to human readable [JSON](/spine-json-format) or a custom [binary format](/spine-binary-format). spine-c stores skeleton data in `spine_skeleton_data` structs. + +For loading skeleton data: + +```c +// First, load the skeleton JSON file contents into a string +char* jsonString = read_file_to_string("path/to/skeleton.json"); + +// Load skeleton data from JSON +spine_skeleton_data_result json_result = spine_skeleton_data_load_json( + atlas, // Previously loaded atlas + jsonString, // JSON file contents as string + "skeleton.json" // Path for error reporting +); + +// Check for errors +if (spine_skeleton_data_result_get_error(json_result)) { + printf("Error loading skeleton: %s\n", spine_skeleton_data_result_get_error(json_result)); + spine_skeleton_data_result_dispose(json_result); + exit(1); +} + +// Get the skeleton data from the result +spine_skeleton_data skeleton_data = spine_skeleton_data_result_get_data(json_result); + +// Dispose the result (but keep the skeleton data) +spine_skeleton_data_result_dispose(json_result); +``` + +Loading skeleton data from a binary export: + +```c +// First, load the skeleton binary file into memory +uint8_t* binaryData = read_file_to_bytes("path/to/skeleton.skel", &dataLength); + +// Load skeleton data from binary +spine_skeleton_data_result binary_result = spine_skeleton_data_load_binary( + atlas, // Previously loaded atlas + binaryData, // Binary data as uint8_t array + dataLength, // Length of binary data + "skeleton.skel" // Path for error reporting +); + +// Check for errors and get skeleton data (same as JSON) +if (spine_skeleton_data_result_get_error(binary_result)) { + printf("Error loading skeleton: %s\n", spine_skeleton_data_result_get_error(binary_result)); + spine_skeleton_data_result_dispose(binary_result); + exit(1); +} + +spine_skeleton_data skeleton_data = spine_skeleton_data_result_get_data(binary_result); +spine_skeleton_data_result_dispose(binary_result); +``` + +> **Note:** Binary format is preferred for production as it's smaller and faster to load than JSON. + +## Preparing animation state data +Spine supports smooth transitions (crossfades) when switching from one animation to another. The crossfades are achieved by mixing one animation with another for a specific mix time. spine-c provides the `spine_animation_state_data` struct to define these mix times: + +```c +// Create the animation state data +spine_animation_state_data anim_state_data = spine_animation_state_data_create(skeleton_data); + +// Set the default mix time between any pair of animations in seconds +spine_animation_state_data_set_default_mix(anim_state_data, 0.1f); + +// Set the mix time between specific animations, overwriting the default +spine_animation_state_data_set_mix_1(anim_state_data, "jump", "walk", 0.2f); +``` + +The mix times defined in `spine_animation_state_data` can also be overwritten explicitly when applying animations (see below). + +# Skeletons +Setup pose data (skeleton data, texture atlases) is shared between game objects. Each game object gets its own `spine_skeleton` instance that references the shared `spine_skeleton_data` and `spine_atlas`. + +Skeletons can be freely modified (procedural bone manipulation, animations, attachments, skins) while the underlying data stays intact. This allows efficient sharing across any number of game objects. + +## Creating skeletons +```c +spine_skeleton skeleton = spine_skeleton_create(skeleton_data); +``` + +Each game object needs its own skeleton instance. The bulk data remains shared to reduce memory consumption and texture switches. + +> **Note:** Skeletons must be explicitly disposed with `spine_skeleton_dispose(skeleton)` when no longer needed. + +## Bones +A skeleton is a hierarchy of bones, with slots attached to bones, and attachments attached to slots. + +### Finding bones +All bones in a skeleton have a unique name: + +```c +// Returns NULL if no bone of that name exists +spine_bone bone = spine_skeleton_find_bone(skeleton, "mybone"); +``` + +### Local transform +A bone is affected by its parent bone, all the way back to the root bone. How a bone inherits from its parent is controlled by its [transform inheritance](/spine-bones#Transform-inheritance) setting. Each bone has a local transform relative to its parent consisting of: + +* `x` and `y` coordinates relative to the parent +* `rotation` in degrees +* `scaleX` and `scaleY` +* `shearX` and `shearY` in degrees + +The local transform is accessed through the bone's pose (`spine_bone_local`): + +```c +spine_bone bone = spine_skeleton_find_bone(skeleton, "mybone"); +spine_bone_local pose = spine_bone_get_pose(bone); + +// Get local transform properties +float x = spine_bone_local_get_x(pose); +float y = spine_bone_local_get_y(pose); +float rotation = spine_bone_local_get_rotation(pose); +float scaleX = spine_bone_local_get_scale_x(pose); +float scaleY = spine_bone_local_get_scale_y(pose); +float shearX = spine_bone_local_get_shear_x(pose); +float shearY = spine_bone_local_get_shear_y(pose); + +// Modify local transform +spine_bone_local_set_position(pose, 100, 50); +spine_bone_local_set_rotation(pose, 45); +spine_bone_local_set_scale_1(pose, 2, 2); +``` + +The local transform can be manipulated procedurally or via animations. Both can be done simultaneously, with the combined result stored in the pose. + +### World transform +After setting up local transforms (procedurally or via animations), you need the world transform of each bone for rendering and physics. + +The calculation starts at the root bone and recursively calculates all child bone world transforms. It also applies [IK](/spine-ik-constraints), [transform](/spine-transform-constraints), [path](/spine-path-constraints) and [slider](/spine-sliders) constraints. + +To calculate world transforms: + +```c +spine_skeleton_update(skeleton, deltaTime); +spine_skeleton_update_world_transform(skeleton, SPINE_PHYSICS_UPDATE); +``` + +`deltaTime` is the time between frames in seconds. The second parameter specifies physics behavior, with `SPINE_PHYSICS_UPDATE` being a good default. + +The world transform is accessed through the bone's applied pose (`spine_bone_pose`): + +```c +spine_bone_pose applied = spine_bone_get_applied_pose(bone); + +// Get world transform matrix components +float a = spine_bone_pose_get_a(applied); // 2x2 matrix encoding +float b = spine_bone_pose_get_b(applied); // rotation, scale +float c = spine_bone_pose_get_c(applied); // and shear +float d = spine_bone_pose_get_d(applied); + +// Get world position +float worldX = spine_bone_pose_get_world_x(applied); +float worldY = spine_bone_pose_get_world_y(applied); +``` + +Note that `worldX` and `worldY` are offset by the skeleton's x and y position. + +World transforms should never be modified directly. They're always derived from local transforms by calling `spine_skeleton_update_world_transform`. + +### Converting between coordinate systems +spine-c provides functions to convert between coordinate systems. These assume world transforms have been calculated: + +```c +spine_bone bone = spine_skeleton_find_bone(skeleton, "mybone"); +spine_bone_pose applied = spine_bone_get_applied_pose(bone); + +// Get world rotation and scale +float rotationX = spine_bone_pose_get_world_rotation_x(applied); +float rotationY = spine_bone_pose_get_world_rotation_y(applied); +float scaleX = spine_bone_pose_get_world_scale_x(applied); +float scaleY = spine_bone_pose_get_world_scale_y(applied); + +// Transform between world and local space +float localX, localY, worldX, worldY; +spine_bone_pose_world_to_local(applied, worldX, worldY, &localX, &localY); +spine_bone_pose_local_to_world(applied, localX, localY, &worldX, &worldY); + +// Transform rotations +float localRotation = spine_bone_pose_world_to_local_rotation(applied, worldRotation); +float worldRotation = spine_bone_pose_local_to_world_rotation(applied, localRotation); +``` + +> **Note:** Modifications to a bone's local transform (and its children) are reflected in world transforms after calling `spine_skeleton_update_world_transform`. + +## Positioning +By default, a skeleton is at the origin of the world coordinate system. To position a skeleton in your game world: + +```c +// Make a skeleton follow a game object +spine_skeleton_set_x(skeleton, myGameObject->worldX); +spine_skeleton_set_y(skeleton, myGameObject->worldY); + +// Or set both at once +spine_skeleton_set_position(skeleton, myGameObject->worldX, myGameObject->worldY); +``` + +> **Note:** Changes to the skeleton's position are reflected in bone world transforms after calling `spine_skeleton_update_world_transform`. + +## Flipping +A skeleton can be flipped to reuse animations for the opposite direction: + +```c +spine_skeleton_set_scale(skeleton, -1, 1); // Flip horizontally +spine_skeleton_set_scale(skeleton, 1, -1); // Flip vertically +// Or individually: spine_skeleton_set_scale_x(skeleton, -1); spine_skeleton_set_scale_y(skeleton, -1); +``` + +For coordinate systems with y-axis pointing down (Spine assumes y-up by default), set this globally: + +```c +spine_bone_set_y_down(true); // Affects all skeletons +``` + +> **Note:** Scale changes are reflected in bone world transforms after calling `spine_skeleton_update_world_transform`. + +## Setting skins +Artists can create multiple [skins](/spine-runtime-skins) to provide visual variations of the same skeleton (e.g., different characters or equipment). A [skin at runtime](/spine-runtime-skins) maps which [attachment](/spine-basic-concepts#Attachments) goes into which [slot](/spine-basic-concepts#Slots). + +Every skeleton has at least one skin defining the setup pose. Additional skins have names: + +```c +// Set a skin by name +spine_skeleton_set_skin_1(skeleton, "my_skin_name"); + +// Set the default setup pose skin +spine_skeleton_set_skin_2(skeleton, NULL); +``` + +### Creating custom skins +You can create custom skins at runtime by combining existing skins (mix and match): + +```c +// Create a new custom skin +spine_skin custom_skin = spine_skin_create("custom-character"); + +// Add multiple skins to create a mix-and-match combination +spine_skin_add_skin(custom_skin, spine_skeleton_data_find_skin(skeleton_data, "skin-base")); +spine_skin_add_skin(custom_skin, spine_skeleton_data_find_skin(skeleton_data, "armor/heavy")); +spine_skin_add_skin(custom_skin, spine_skeleton_data_find_skin(skeleton_data, "weapon/sword")); +spine_skin_add_skin(custom_skin, spine_skeleton_data_find_skin(skeleton_data, "hair/long")); + +// Apply the custom skin to the skeleton +spine_skeleton_set_skin_2(skeleton, custom_skin); +``` + +> **Note:** Custom skins must be manually disposed with `spine_skin_dispose(custom_skin)` when no longer needed. + +> **Note:** Setting a skin considers previously active attachments. See [skin changes](/spine-runtime-skins#Skin-changes) for details. + +## Setting attachments +You can set individual attachments on slots directly, useful for switching equipment: + +```c +// Set the "sword" attachment on the "hand" slot +spine_skeleton_set_attachment(skeleton, "hand", "sword"); + +// Clear the attachment on the "hand" slot +spine_skeleton_set_attachment(skeleton, "hand", NULL); +``` + +The attachment is searched first in the active skin, then in the default skin. + +## Tinting +You can tint all attachments in a skeleton: + +```c +// Tint all attachments red with half transparency +spine_skeleton_set_color_2(skeleton, 1.0f, 0.0f, 0.0f, 0.5f); + +// Or using a color struct +spine_color color = spine_skeleton_get_color(skeleton); +// Modify color... +spine_skeleton_set_color_1(skeleton, color); +``` + +> **Note:** Colors in spine-c are RGBA with values in the range [0-1]. + +Each slot also has its own color that can be manipulated: + +```c +spine_slot slot = spine_skeleton_find_slot(skeleton, "mySlot"); +spine_slot_pose pose = spine_slot_get_pose(slot); +spine_color slot_color = spine_slot_pose_get_color(pose); +// The slot color is multiplied with the skeleton color when rendering +``` + +Slot colors can be animated. Manual changes will be overwritten if an animation keys that slot's color. + +# Applying animations +The Spine editor lets artists create multiple, uniquely named [animations](/spine-animating). An animation is a set of [timelines](/spine-api-reference#Timeline). Each timeline specifies values over time for properties like bone transforms, attachment visibility, slot colors, etc. + +## Timeline API +spine-c provides a [timeline API](/spine-applying-animations#Timeline-API) for direct timeline manipulation. This low-level functionality allows full customization of how animations are applied. + +## Animation state API +For most use cases, use the [animation state API](/spine-applying-animations#AnimationState-API) instead of the timeline API. It handles: +- Applying animations over time +- Queueing animations +- Mixing between animations (crossfading) +- Applying multiple animations simultaneously (layering) + +The animation state API uses the timeline API internally. + +spine-c represents animation state via `spine_animation_state`. Each game object needs its own skeleton and animation state instance. These share the underlying `spine_skeleton_data` and `spine_animation_state_data` with all other instances to reduce memory consumption. + +### Creating animation states +```c +spine_animation_state animation_state = spine_animation_state_create(animation_state_data); +``` + +The function takes the `spine_animation_state_data` created during loading, which defines default mix times and mix times between specific animations for [crossfades](/spine-applying-animations#Mix-times). + +> **Note:** Animation states must be explicitly disposed with `spine_animation_state_dispose(animation_state)` when no longer needed. + +### Tracks & Queueing +An animation state manages one or more [tracks](/spine-applying-animations#Tracks). Each track is a list of animations played in sequence ([queuing](/spine-applying-animations#Queuing)). Tracks are indexed starting from 0. + +Queue an animation on a track: + +```c +// Add "walk" animation to track 0, looping, without delay +int track = 0; +bool loop = true; +float delay = 0; +spine_animation_state_add_animation_1(animation_state, track, "walk", loop, delay); +``` + +Queue multiple animations to create sequences: + +```c +// Start walking (looping) +spine_animation_state_add_animation_1(animation_state, 0, "walk", true, 0); + +// Jump after 3 seconds +spine_animation_state_add_animation_1(animation_state, 0, "jump", false, 3); + +// Idle indefinitely after jumping +spine_animation_state_add_animation_1(animation_state, 0, "idle", true, 0); +``` + +Clear animations: + +```c +// Clear track 0 +spine_animation_state_clear_track(animation_state, 0); + +// Clear all tracks +spine_animation_state_clear_tracks(animation_state); +``` + +To clear and set a new animation with crossfading from the previous animation: + +```c +// Clear track 0 and crossfade to "shot" (not looped) +spine_animation_state_set_animation_1(animation_state, 0, "shot", false); + +// Queue "idle" to play after "shot" +spine_animation_state_add_animation_1(animation_state, 0, "idle", true, 0); +``` + +To crossfade to the setup pose: + +```c +// Clear track 0 and crossfade to setup pose over 0.5 seconds with 1 second delay +spine_animation_state_set_empty_animation(animation_state, 0, 0.5f); + +// Or queue a crossfade to setup pose as part of a sequence +spine_animation_state_add_empty_animation(animation_state, 0, 0.5f, 0); +``` + +For complex games, use multiple tracks to layer animations: + +```c +// Walk on track 0 +spine_animation_state_set_animation_1(animation_state, 0, "walk", true); + +// Simultaneously shoot on track 1 +spine_animation_state_set_animation_1(animation_state, 1, "shoot", false); +``` + +> **Note:** Higher track animations overwrite lower track animations for any properties both animate. Ensure layered animations don't key the same properties. + +### Track entries +When setting or queueing an animation, a [track entry](/spine-api-reference#TrackEntry) is returned: + +```c +spine_track_entry entry = spine_animation_state_set_animation_1(animation_state, 0, "walk", true); +``` + +The track entry lets you further customize this specific playback instance of an animation: + +```c +// Override the mix duration when transitioning to this animation +spine_track_entry_set_mix_duration(entry, 0.5f); +``` + +The track entry is valid until the animation it represents is finished. It can be stored when setting the animation and reused as long as the animation is applied. Alternatively, call `spine_animation_state_get_current` to get the track entry for the animation currently playing on a track: + +```c +spine_track_entry current = spine_animation_state_get_current(animation_state, 0); +``` + +### Events +An animation state generates events while playing back queued animations: +* An animation **started** +* An animation was **interrupted**, e.g. by clearing a track +* An animation was **completed**, which may occur multiple times if looped +* An animation has **ended**, either due to interruption or completion +* An animation and its corresponding track entry have been **disposed** +* A [user defined **event**](/spine-events) was fired + +You can listen for these events by registering a function with the animation state or individual track entries: + +```c +// Define the function that will be called when an event happens +void my_listener(spine_animation_state state, spine_event_type type, + spine_track_entry entry, spine_event event, void* user_data) { + // Cast user_data to your context if needed + MyGameContext* context = (MyGameContext*)user_data; + + switch (type) { + case SPINE_EVENT_TYPE_START: + printf("Animation started\n"); + break; + case SPINE_EVENT_TYPE_INTERRUPT: + printf("Animation interrupted\n"); + break; + case SPINE_EVENT_TYPE_END: + printf("Animation ended\n"); + break; + case SPINE_EVENT_TYPE_COMPLETE: + printf("Animation completed (loops fire this each loop)\n"); + break; + case SPINE_EVENT_TYPE_DISPOSE: + printf("Track entry disposed\n"); + break; + case SPINE_EVENT_TYPE_EVENT: + // User-defined event from animation + if (event) { + spine_event_data data = spine_event_get_data(event); + const char* name = spine_event_data_get_name(data); + printf("Event: %s\n", name); + + // Access event data + int int_value = spine_event_data_get_int_value(data); + float float_value = spine_event_data_get_float_value(data); + const char* string_value = spine_event_data_get_string_value(data); + } + break; + } +} + +// Register listener for all tracks +MyGameContext* context = get_my_game_context(); +spine_animation_state_set_listener(animation_state, my_listener, context); + +// Or register listener for a specific track entry +spine_track_entry entry = spine_animation_state_set_animation_1(animation_state, 0, "walk", true); +spine_track_entry_set_listener(entry, my_listener, context); + +// Clear listeners by setting to NULL +spine_animation_state_set_listener(animation_state, NULL, NULL); +spine_track_entry_set_listener(entry, NULL, NULL); +``` + +The track entry is valid until the animation it represents is finished. Any registered listener will be called for events until the track entry is disposed. + +### Updating and applying +Each frame, advance the animation state by the frame delta time, then apply it to the skeleton: + +```c +// In your game loop +void update(float deltaTime) { + // Advance the animation state by deltaTime seconds + spine_animation_state_update(animation_state, deltaTime); + + // Apply the animation state to the skeleton's local transforms + spine_animation_state_apply(animation_state, skeleton); + + // Calculate world transforms for rendering + spine_skeleton_update(skeleton, deltaTime); + spine_skeleton_update_world_transform(skeleton, SPINE_PHYSICS_UPDATE); +} +``` + +`spine_animation_state_update` advances all tracks by the delta time, potentially triggering [events](/spine-events). + +`spine_animation_state_apply` poses the skeleton's local transforms based on the current state of all tracks. This includes: +- Applying individual animations +- Crossfading between animations +- Layering animations from multiple tracks + +After applying animations, call `spine_skeleton_update_world_transform` to calculate world transforms for rendering. + +## Skeleton drawable + +For simplified management, spine-c provides `spine_skeleton_drawable` which combines a skeleton, animation state, and animation state data into a single object: + +```c +// Create drawable from skeleton data +spine_skeleton_drawable drawable = spine_skeleton_drawable_create(skeleton_data); + +// Access the skeleton and animation state +spine_skeleton skeleton = spine_skeleton_drawable_get_skeleton(drawable); +spine_animation_state animation_state = spine_skeleton_drawable_get_animation_state(drawable); +spine_animation_state_data animation_state_data = spine_skeleton_drawable_get_animation_state_data(drawable); + +// Update and render in one call +spine_skeleton_drawable_update(drawable, deltaTime); +spine_render_command render_command = spine_skeleton_drawable_render(drawable); + +// Get animation state events +spine_animation_state_events events = spine_skeleton_drawable_get_animation_state_events(drawable); +int num_events = spine_animation_state_events_get_num_events(events); +for (int i = 0; i < num_events; i++) { + spine_event_type type = spine_animation_state_events_get_event_type(events, i); + spine_track_entry entry = spine_animation_state_events_get_track_entry(events, i); + if (type == SPINE_EVENT_TYPE_EVENT) { + spine_event event = spine_animation_state_events_get_event(events, i); + // Handle event + } +} +spine_animation_state_events_reset(events); + +// Dispose when done (disposes skeleton and animation state, but not skeleton data) +spine_skeleton_drawable_dispose(drawable); +``` + +The drawable simplifies the update cycle by combining update and apply operations. However, for full control over the animation pipeline, use the skeleton and animation state APIs directly. + +# Rendering + +spine-c provides a renderer-agnostic interface for drawing skeletons. The rendering process produces `spine_render_command` objects, each representing a batch of textured triangles with blend mode and texture information that can be submitted to any graphics API. + +## Render commands + +After updating a skeleton's world transforms, generate render commands: + +```c +// Using skeleton drawable +spine_render_command command = spine_skeleton_drawable_render(drawable); + +// Or using skeleton renderer directly (reusable for multiple skeletons, not thread-safe) +spine_skeleton_renderer renderer = spine_skeleton_renderer_create(); +spine_render_command command = spine_skeleton_renderer_render(renderer, skeleton); +``` + +The renderer handles everything automatically: +* Batches triangles from consecutive region and mesh attachments that share the same texture and blend mode +* Applies clipping for clipping attachments +* Generates optimized draw calls + +Each render command represents a batch with: +* Vertex data (positions, UVs, colors) +* Index data for triangles +* Texture to sample from +* Blend mode (normal, additive, multiply, screen) + +## Processing render commands + +Iterate through commands and submit them to your graphics API: + +```c +// Simplified graphics API for illustration +void render_skeleton(spine_render_command first_command) { + spine_render_command command = first_command; + + while (command) { + // Get command data + float* positions = spine_render_command_get_positions(command); + float* uvs = spine_render_command_get_uvs(command); + uint32_t* colors = spine_render_command_get_colors(command); + uint16_t* indices = spine_render_command_get_indices(command); + int num_vertices = spine_render_command_get_num_vertices(command); + int num_indices = spine_render_command_get_num_indices(command); + + // Get texture and blend mode + void* texture = spine_render_command_get_texture(command); + spine_blend_mode blend_mode = spine_render_command_get_blend_mode(command); + + // Set graphics state + graphics_bind_texture(texture); + graphics_set_blend_mode(blend_mode); + + // Submit vertices and indices to GPU + graphics_set_vertices(positions, uvs, colors, num_vertices); + graphics_draw_indexed(indices, num_indices); + + // Move to next command + command = spine_render_command_get_next(command); + } +} +``` + +## Blend modes + +Configure your graphics API blend function based on the blend mode: + +```c +void graphics_set_blend_mode(spine_blend_mode mode, bool premultiplied_alpha) { + switch (mode) { + case SPINE_BLEND_MODE_NORMAL: + // Premultiplied: src=GL_ONE, dst=GL_ONE_MINUS_SRC_ALPHA + // Straight: src=GL_SRC_ALPHA, dst=GL_ONE_MINUS_SRC_ALPHA + break; + case SPINE_BLEND_MODE_ADDITIVE: + // Premultiplied: src=GL_ONE, dst=GL_ONE + // Straight: src=GL_SRC_ALPHA, dst=GL_ONE + break; + case SPINE_BLEND_MODE_MULTIPLY: + // Both: src=GL_DST_COLOR, dst=GL_ONE_MINUS_SRC_ALPHA + break; + case SPINE_BLEND_MODE_SCREEN: + // Both: src=GL_ONE, dst=GL_ONE_MINUS_SRC_COLOR + break; + } +} +``` + +## Example implementations + +For complete rendering implementations, see: +* [spine-sfml](/spine-sfml): SFML-based renderer +* [spine-sdl](/spine-sdl): SDL-based renderer +* [spine-glfw](/spine-glfw): OpenGL renderer with GLFW + +These examples show how to integrate spine-c rendering with different graphics APIs and frameworks. + +# Memory management + +spine-c memory management is straightforward. Any object created with `spine_*_create` must be disposed with `spine_*_dispose`. Objects returned from loaders use result types that must be disposed with `spine_*_result_dispose`. + +Lifetime guidelines: +* Create setup pose data shared by instances (`spine_atlas`, `spine_skeleton_data`, `spine_animation_state_data`) at game or level startup, dispose at game or level end. +* Create instance data (`spine_skeleton`, `spine_animation_state`) when the game object is created, dispose when the game object is destroyed. +* Use `spine_skeleton_drawable` for simplified management: it combines skeleton, animation state, and animation state data. + +Track entries (`spine_track_entry`) are valid from when an animation is queued (`spine_animation_state_set_animation_*`, `spine_animation_state_add_animation_*`) until the `SPINE_EVENT_TYPE_DISPOSE` event is sent to your listener. Accessing a track entry after this event causes undefined behavior. + +When creating objects, you pass references to other objects. The referencing object never disposes the referenced object: +* Disposing `spine_skeleton` does not dispose `spine_skeleton_data` or `spine_atlas`. The skeleton data is likely shared by other skeleton instances. +* Disposing `spine_skeleton_data` does not dispose `spine_atlas`. The atlas may be shared by multiple skeleton data instances. +* Disposing `spine_skeleton_drawable` disposes its skeleton and animation state, but not the skeleton data. \ No newline at end of file diff --git a/spine-c/docs.md b/spine-c/docs.md new file mode 100644 index 000000000..8400cc5bc --- /dev/null +++ b/spine-c/docs.md @@ -0,0 +1,1011 @@ +# spine-c Runtime Documentation + +> **Licensing** +> +> Please see the [Spine Runtimes License](/spine-runtimes-license) before integrating the Spine Runtimes into your applications. + +# Introduction +spine-c is a generic runtime for integrating Spine animations in game engines and frameworks written in C, C++, Swift, Rust, Objective-C or any other language that can natively interface with C. + +spine-c provides functionality to + +* [Load Spine skeletons](/spine-loading-skeleton-data) exported to JSON or binary files. +* [Load texture atlases](/spine-loading-skeleton-data) storing the images used by the Spine skeletons. +* Manage and apply [skins](/spine-runtime-skins) to the skeleton. +* Manage and apply [animations](/spine-applying-animations) to the skeleton. +* Manipulate and compute data required for rendering and physics based on the current [skeleton pose, slots & attachments states](/spine-runtime-skeletons). + +As the spine-c runtime is a generic, engine independent runtime, users are required to implement a set of functions to provide engine specific file i/o and image loading to the spine-c runtime, render the data generated by the spine-c runtime via the engine's rendering system, and optionally integrate the data with the engine's physics system if more advanced use cases such as rag dolls are required. + +The spine-c runtime is written using C99 to guarantee compatibility with a wide range of platforms and compilers. + +Other official Spine runtimes are based on spine-c and can serve as examples for integration with your engine of choice: + +* [spine-sfml](/git/spine-runtimes/tree/spine-sfml/c), a runtime for [SFML](http://www.sfml-dev.org/). +* [spine-sdl](/git/spine-runtimes/tree/spine-sdl/), a runtime for [SDL](https://www.libsdl.org/). + +The following sections give a brief, engine independent overview of the spine-c runtime and how to use it. Most of the official Spine runtimes based on spine-c will encapsulate (parts of) the spine-c API in their own, easier to use API. It is still beneficial to understand the basics of the underlying spine-c runtime. + +> **Note:** This guide assumes you are familiar with the basic [runtime architecture](/spine-runtime-architecture) and terminology used by Spine. Please also consult the [API reference](/spine-api-reference) to explore more advanced functions of the runtime. + +# Exporting Spine assets for spine-c +![](/img/spine-runtimes-guide/spine-ue4/export.png) +Please follow the instructions in the Spine User Guide on how to + +1. [Export skeleton & animation data](/spine-export) to JSON or our binary format +2. [Export texture atlases containing the images of your skeleton](/spine-texture-packer) + +An export of the skeleton data and texture atlas of your skeleton will yield the following files: + +![](/img/spine-runtimes-guide/spine-ue4/exported-files.png) + +1. `skeleton-name.json` or `skeleton-name.skel`, containing your skeleton and animation data. +2. `skeleton-name.atlas`, containing information about the texture atlas. +3. One or more `.png` files, each representing one page of your texture atlas containing the packed images your skeleton uses. + +> **Note:** Instead of creating one texture atlas per skeleton, you may also pack the images of multiple skeletons into a single texture atlas. Please refer to the [texture packing guide](/spine-texture-packer). + +# Loading Spine assets +spine-c provides APIs to load texture atlases, Spine skeleton data (bones, slots, attachments, skins, animations) and define mix times between animations through [animation state data](/spine-applying-animations#Mix-times). These three types of data, also known as setup pose data, are generally loaded once and then shared by every game object. The sharing mechanism is achieved by giving each game object its own skeleton and [animation state](/spine-applying-animations#AnimationState-API), also known as instance data. + +> **Note:** For a more detailed description of the overall loading architecture consult the generic [Spine Runtime Documentation](/spine-loading-skeleton-data). + +## Loading texture atlases +Texture atlas data is stored in a custom [atlas format](/spine-atlas-format) that describes the location of individual images within atlas pages. The atlas pages themselves are stored as plain `.png` files next to the atlas. + +spine-c provides the functions [spAtlas_createFromFile](/git/spine-runtimes/blob/spine-c/spine-c/src/spine/Atlas.c#L301) and [spAtlas_create](/git/spine-runtimes/blob/spine-c/spine-c/src/spine/Atlas.c#L167) for this task. The former loads the atlas from a file, the latter reads the atlas from memory. If loading the atlas failed, the functions return 0. + +``` +// Load the atlas from a file. The last argument is a void* that will be +// stored in atlas->rendererObject. +spAtlas* atlas = spAtlas_createFromFile("myatlas.atlas", 0); + +// Load the atlas from memory, giving the memory location, the number +// of bytes and the directory relative to which atlas page textures +// should be loaded. The last argument is a void* that will be stored +// in atlas-rendererObject. +spAtlas* atlas = spAtlas_create(atlasInMemory, atlasDataLengthInBytes, dir, 0); +``` + +## Loading skeleton data +Skeleton data (bones, slots, attachments, skins, animations) can be exported to human readable [JSON](/spine-json-format) or a custom [binary format](/spine-binary-format). spine-c stores skeleton data in `spSkeletonData` structs. + +For loading the skeleton data from a JSON export, we create a `spSkeletonJson` which takes the previously loaded `spAtlas`, set the scale of the skeleton and finally read the skeleton data from the file: + +``` +// Create a spSkeletonJson used for loading and set the scale +// to make the loaded data two times as big as the original data +spSkeletonJson* json = spSkeletonJson_create(atlas); +json->scale = 2; + +// Load the skeleton .json file into a spSkeletonData +spSkeletonData* skeletonData = spSkeletonJson_readSkeletonDataFile(json, filename); + +// If loading failed, print the error and exit the app +if (!skeletonData) { + printf("%s\n", json->error); + spSkeletonJson_dispose(json); + exit(0); +} + +// Dispose the spSkeletonJson as we no longer need it after loading. +spSkeletonJson_dispose(json); +``` + +Loading skeleton data from a binary export works the same, except we use a `spSkeletonBinary` instead: + +``` +// Create a spSkeletonJson used for loading and set the scale +// to make the loaded data two times as big as the original data +spSkeletonBinary* binary = spSkeletonBinary_create(atlas); +binary->scale = 2; + +// Load the skeleton .skel file into a spSkeletonData +spSkeletonData* skeletonData = spSkeletonBinary_readSkeletonDataFile(binary, filename); + +// If loading failed, print the error and exit the app +if (!skeletonData) { + printf("%s\n", binary->error); + spSkeletonBinary_dispose(json); + exit(0); +} + +// Dispose the spSkeletonBinary as we no longer need it after loading. +SkeletonBinary_dispose(json); +``` + +## Preparing animation state data +Spine supports smooth transitions (crossfades) when switching from one animation to another. The crossfades are achieved by mixing one animation with another for a specific mix time. The spine-c runtime provides the `spAnimationStateData` struct to define these mix times: + +``` +// Create the spAnimationStateData +spAnimationStateData* animationStateData = spAnimationStateData_create(skeletonData); + +// Set the default mix time between any pair of animations in seconds. +spAnimationStateData_setDefaultMix(animationStateData, 0.1f); + +// Set the mix time between from the "jump" to the "walk" animation to 0.2 seconds, +// overwriting the default mix time for this from/to pair. +spAnimationStateData_setMixByName(animationStateData, "jump", "walk", 0.2f); +``` + +The mix times defined in `spAnimationStateData` can also be overwritten explicitly when applying animations (see below). + +# Skeletons +Setup pose data (skeleton data, texture atlases) are supposed to be shared between game objects. spine-c provides the `spSkeleton` struct to facilitate this sharing. Every game object receives its own instance of `spSkeleton` which in turn references an `spSkeletonData` and `spAtlas` instance as data sources. + +The `spSkeleton` can be freely modified, e.g. by procedurally modifying bones, applying animations or setting attachments and skins specific to a game object, while the underlying skeleton data and texture atlas stay in tact. This mechanism and setup allow sharing `spSkeletonData` and `spAtlas` instances by any amount of game objects. + +## Creating skeletons +To create a `spSkeleton` instance, call `spSkeleton_create`: + +``` +spSkeleton* skeleton = spSkeleton_create(skeletonData); +``` + +Every game object will need its own `spSkeleton`. The bulk of the data remains in `spSkeletonData` and `spAtlas` and will be shared by all `spSkeleton` instances to vastly reduce memory consumption and texture switches. The life-time of an `spSkeleton` is thus coupled with the life-time of its corresponding game object. + +## Bones +A skeleton is a hierarchy of bones, with slots attached to bones, and attachments attached to slots. + +### Finding bones +All bones in a skeleton have a unique name by which they can be fetched from the skeleton: + +``` +// returns 0 if no bone of that name could be found +spBone* bone = spSkeleton_findBone("mybone"); +``` + +### Local transform +A bone is affected by its parent bone, all the way back to the root bone. E.g. when rotating a bone, all its child bones and all their children are also rotated. To accomplish these hierarchical transformations, each bone has a local transformation relative to its parent bone consisting of: + +* `x` and `y` coordinates relative to the parent. +* `rotation` in degrees. +* `scaleX` and `scaleY`. +* `shearX` and `shearY` in degrees. + +The local transform of a bone can be manipulated procedurally or via applying an animation. The former allows to implement dynamic behavior like having a bone point at a mouse cursor, let feet bones follow the terrain etc. Both procedural modification of the local transform as well as applying animations can be done simultaneously. The end result will be a single combined local transform. + +### World transform +Once all the local transforms are setup, either through procedurally modifying the local transforms of bones or by applying animations, we need the world transform of each bone for rendering and physics. + +The calculation starts at the root bone, and then recursively calculates all child bone world transforms. The calculation also applies [IK](/spine-ik-constraints), [transform](/spine-transform-constraints) and [path](/spine-path-constraints) constraints defined by the artist in the Spine editor. + +To calculate the world transforms, we need to first update the skeleton's frame time for physics, followed by calculating the actual transforms: + +``` +spSkeleton_update(deltaTime); +spSkeleton_updateWorldTransform(skeleton, SP_PHYSICS_UPDATE); +``` + +`deltaTime` specifies the time passed between the current and last frame, given in seconds. The second parameter to `spSkeleton_updateWorldTransform` specifies if and how physics should be applied. `SP_PHYSICS_UPDATE` is a good default value. + +The result is stored on each bone, and consists of: + +* `a`, `b`, `c`, and `d`, a 2x2 column major matrix encoding rotation, scale and shear of the bone. +* `worldX`, `worldY`, storing the world position of the bone. + +Note that `worldX` and `worldY` are offset by `skeleton->x` and `skeleton->y`. These two fields can be used to position the skeleton in your game engine's world coordinate system. + +In general, the bones world transforms should never be modified directly. Instead, they should always be derived from the local transforms of the bones in the skeleton by calling `spSkeleton_updateWorldTransform`. The local transforms can be set either procedurally, e.g. setting the rotation of a bone so it points to the mouse cursor, or by applying animations (see below), or both. Once the (procedural) animation is applied, `spSkeleton_updateWorldTransform` is called and the resulting world transforms are recalculated based on the local transform as well as any constraints that are applied to bones. + +### Converting between coordinate systems +It is often easier to manipulate bones in the world coordinate system, as this is where coordinates from other entities or input events are usually given. However, since the world transform should not be directly manipulated, we need to apply any changes to a bone based on world coordinate system calculations to the local transform of that bone. + +The spine-c runtimes provides functions to extract rotation and scale information from the 2x2 world transform matrix of a bone, and transform locations and rotations from local space to world space and vice versa. All these functions assume that the bones' world transforms have been calculated before by calling `spSkeleton_updateWorldTransform`: + +``` +spBone* bone = spSkeleton_findBone("mybone"); + +// Get the rotation of a bone in world space relative to the world space x-axis in degrees +float rotationX = spBone_getWorldRotationX(bone); + +// Get the rotation of a bone in world space relative to the world space y-axis in degrees +float rotationY = spBone_getWorldRotationY(bone); + +// Get the scale of a bone in world space relative to the world space x-axis +float scaleX = spBone_getWorldScaleX(bone); + +// Get the scale of a bone in world space relative to the world space y-axis +float scaleY = spBone_getWorldScaleY(bone); + +// Transform a position given in world space to a bone's local space +float localX = 0, localY = 0; +spBone_worldToLocal(bone, worldX, worldY, &localX, &localY); + +// Transform a position given in a bone's local space to world space +float worldX = 0, worldY = 0; +spBone_localToWorld(bone, localX, localY, &worldX, &worldY); + +// Transform a rotation given in the bone's world transform relative to the world space x-axis to // a rotation given in local space relative to the local space x-axis in degrees. +float localRotationX = spBone_worldToLocalRotationX(bone) + +// Transform a rotation given in the bone's world transform relative to the world space y-axis to // a rotation given in local space relative to the local space y-axis in degrees. +float localRotationY = spBone_worldToLocalRotationY(bone) +``` + +> **Note:** Your modifications to the local transform of a bone (and thereby all its children) will be reflect in the bone's world transform after the next call to `spSkeleton_updateWorldTransform`. + +## Positioning +By default, a skeleton is assumed to be at the origin of the game's world coordinate system. To position a skeleton in a game's world coordinate system, you can use the `x` and `y` field: + +``` +// make a skeleton follow a game object in world space +skeleton->x = myGameObject->worldX; +skeleton->y = myGameObject->worldY; +``` + +> **Note:** Your modifications to the skeleton `x` and `y` fields will be reflect in the bone world transforms after the next call to `spSkeleton_updateWorldTransform`. + +## Flipping +A skeleton can be flipped vertically or horizontally. This allows reusing animations made for one direction for the opposing direction, or for working in coordinate systems with the y-axis pointing downwards (Spine assumes y-axis up by default): + +``` +// flip vertically around the x-axis +skeleton->scaleY = -1; + +// flip horizontally around the y-axis +skeleton->scaleX = -1; +``` + +> **Note:** Your modifications to the skeleton `scaleX` and `scaleY` fields will be reflected in the bone world transforms after the next call to `spSkeleton_updateWorldTransform`. + +## Setting skins +The artist creating the Spine skeleton may have added multiple [skins](/spine-runtime-skins) to the skeleton to add visual variations of the same skeleton, e.g. a female and male version. The spine-c runtime stores skins in instances of `spSkin`. + +A [skin at runtime](/spine-runtime-skins) is a map defining which [attachment](/spine-basic-concepts#Attachments) goes into which [slot](/spine-basic-concepts#Slots) of the skeleton. Every skeleton has at least one skin which defines which attachment is on what slot in the skeleton's setup pose. Additional skins have a name to identify them. + +Setting a skin on a skeleton via spine-c: + +``` +// set a skin by name +spSkeleton_setSkin(skeleton, "my_skin_name"); + +// set the default setup pose skin by passing 0 +spSkeleton_setSkin(skeleton, 0); +``` + +> **Note:** Setting a skin takes into account what skin and hence which attachments have previously been set. Please refer to the generic runtime guide for [more information on setting skins](/spine-runtime-skins#Skin-changes). + +## Setting attachments +spine-c allows setting a single attachment on a skeleton's slot directly, e.g. to switch out weapons. The attachment is first searched in the active skin, and if this fails, in the default setup pose skin: + +``` +// Set the attachment called "sword" on the "hand" slot +spSkeleton_setAttachment(skeleton, "hand", "sword"); + +// Clear the attachment on the slot "hand" so nothing is shown +spSkeleton_setAttachment(skeleton, "hand", 0); +``` + +### Tinting +You can tint all attachments in a skeleton by setting the skeleton's color: + +``` +spSkeleton* skeleton = ... + +// tint all attachments with red and make the skeleton half translucent. +skeleton->r = 1.0f; +skeleton->g = 0.0f; +skeleton->b = 0.0f; +skeleton->a = 0.5f; +``` + +> **Note:** Colors in spine-c are given as RGBA, with values for each channel in the range [0-1]. + +When rendering a skeleton, the renderer walks though the draw order of slots on the skeleton and renders the currently active attachment on each slot. In addition to the skeleton's color, every slot also has a color which can be manipulated at runtime: + +``` +spSlot* slot = skeleton->findSlotByName("mySlot"); +slot->r = 0.0f; +slot->g = 1.0f; +slot->b = 0.0f; +slot->a = 1.0f; +``` + +Note that slot colors can also be animated. If you manually change a slot's color and then apply an animation that keys that slot's color, your manual change will be overwritten. + +# Applying animations +The Spine editor lets artists create multiple, uniquely named [animations](/spine-animating). An animation is a set of [timelines](/spine-api-reference#Timeline). Each timeline specifies at what frame what property of a bone or the skeleton should change to what value. There are many different types of timelines, from timelines defining the transform of a bone over time, to timelines that change the drawing order. Timelines are part of the skeleton data and stored in `spSkeletonData` in spine-c. + +## Timeline API +spine-c provides a [timeline API](/spine-applying-animations#Timeline-API) should the need arise to directly work with timelines. This low-level functionality allows you to fully customize the way animations defined by your artist are applied to a skeleton. + +## Animation state API +In almost all cases, you should use the [animation state API](/spine-applying-animations#AnimationState-API) instead of the timeline API. The animation state API makes task such as applying animations over time, queueing animations, mixing between animations, and applying multiple animations at the same time considerably easier than the low-level timeline API. The animation state API uses the timeline API internally and can thus be seen as a wrapper. + +spine-c represents an animation state via the `spAnimationState` struct. Just like skeletons, an `spAnimationState` is instantiated per game object. In general, you will have one `spSkeleton` and one `spAnimationState` instance per game object in your game. And just like `spSkeleton`, the `spAnimationState` will share `spSkeletonData` (wherein animations and their timelines are stored) and `spAnimationStateData` (wherein mix times are stored) with all other `spAnimationState` instances sourcing the same skeleton data. + +### Creating animation states +To create an `spAnimationState`instance, call `spAnimationState_create`: + +``` +spAnimationState* animationState = spAnimationState_create(animationStateData); +``` + +The function takes an `spAnimationStateData` which is usually created when the skeleton data is loaded, and which defines the default mix time as well as mix times between specific animations for [crossfades](/spine-applying-animations#Mix-times). + +### Tracks & Queueing +An animation state manages one or more [tracks](/spine-applying-animations#Tracks). Each track is a list of animations that should be played back in the order they were added to the track. This is known as [queuing](/spine-applying-animations#Queuing). Tracks are indexed starting from 0. + +You can queue an animation on a track like this: + +``` +// Add the animation "walk" to track 0, without delay, and let it loop indefinitely +int track = 0; +int loop = 1; +float delay = 0; +spAnimationState_addAnimationByName(animationState, track, "walk", loop, delay); +``` + +You can queue multiple animations at once as a fire and forget way to create animation sequences: + +``` +// Start walking (note the looping) +spAnimationState_addAnimationByName(animationState, 0, "walk", 1, 0); + +// Jump after 3 seconds +spAnimationState_addAnimationByName(animationState, 0, "jump", 0, 3); + +// Once we are done jumping, idle indefinitely +spAnimationState_addAnimationByName(animationState, 0, "idle", 1, 0); +``` + +You can also clear all animations queued in a track: + +``` +// Clear all animations queued on track 0 +spAnimationState_clearTrack(animationState, 0); + +// Clear all animations queued on all tracks +spAnimationState_clearTracks(animationState); +``` + +Instead of clearing and adding a new animation to a track, you can call `spAnimationState_setAnimationByName`. This will clear all tracks, but remember what the last played back animation was before clearing and crossfade to the newly set animation. This way you can smoothly transition from one animation sequence to the next. You can add more animations to the track after calling `spAnimationState_setAnimationByName` by calling `spAnimationState_addAnimationByName`: + +``` +// Whatever is currently playing on track 0, clear the track and crossfade +// to the "shot" animation, which should not be looped (last parameter). +spAnimationState_setAnimationByName(animationState, 0, "shot", 0); + +// After shooting, we want to idle again +spAnimationState_addAnimationByName(animationState, 0, "idle", 1, 0); +``` + +To crossfade to the setup pose of the skeleton from an animation, you can use `spAnimationState_setEmptyAnimation`, `spAnimationState_addEmptyAnimation`, where the former clears the current track and crossfades to the skeleton, and the later enqueues a crossfade to the setup pose as part of the animation sequence on the track: + +``` +// Whatever is currently playing on track 0, clear the track and crossfade +// to the setup pose for 0.5 seconds (mix time) with a delay of 1 second. +spAnimationState_setEmptyAnimation(animationState, 0, 0.5f, 1); + +// Add a crossfade to the setup pose for 0.5 seconds as part of the animation +// sequence in track 0 +spAnimationState_addEmptyAnimation(animationState, 0, 0.5f) +``` + +For simple games, using a single track is usually enough to achieve your goals. More complex games may want to queue animations on separate tracks, e.g. to simultaneously play back a walk animation while shooting. This is where the real power of Spine comes into play: + +``` +// Apply the "walk" animation on track 0 indefinitely. +spAnimationState_setAnimationByName(animationState, 0, "walk", 1); + +// Simultaneously apply a "shot" animation on track 1 once. +spAnimationState_setAnimationByName(animationState, 1, "shot", 0); +``` + +Note that if you apply animations simultaneously like that, animations on the higher track will overwrite animations on the lower track for every value both animations have keyed. For authoring animations, this means to make sure two animations to be played back simultaneously don't key the same values in the skeleton, e.g. the same bone, attachment, color etc. + +You can control the mixing of animations on different track via [track entries](/spine-applying-animations#TrackEntry) + +### Track Entries +Every time you enqueue an animation on a track of an animation state, the corresponding functions will return an `spTrackEntry` instance. This track entry allows you to further customize both the queued animation, as well as its mixing behaviour with regards to animations on the same or other tracks. See the [TrackEntry documentation](/spine-api-reference#TrackEntry) for a full list of fields you can modify. + +As an example, lets assume the mix time between a "walk" and "run" animation defined in an `spAnimationStateData` is to high for this specific game object in its current situation. You can modify the mix time between "walk" and "run" ad-hoc, just for this specific queued animation: + +``` +// Walk indefinitely +spAnimationState_setAnimationByName(animationState, 0, "walk", 1); + +// At some point, queue the run animation. We want to speed up the mixing +// between "walk" and "run" defined in the `spAnimationStateData` (let's say 0.5 seconds) +// for this one specific call to be faster (0.1 seconds). +spTrackEntry* entry = spAnimationState_addAnimationByName(animationState, 0, "run", 1, 0); +entry->mixDuration = 0.1f; +``` + +You can hold on to the `spTrackEntry` to modify it over time. The `spTrackEntry` will be valid for as long as the animation is queued on that track. Once the animation is completed, the `spTrackEntry` will be deallocated. Any subsequent access will be invalid and likely result in a segfault. You can register a listener to get notified when the animation and hence the track entry are no longer valid. + +### Events +An animation state generates events while playing back queued animations to notify a listener about changes: + +* An animation **started**. +* An animation was **interrupted**, e.g. by clearing a track. +* An animation was **completed**, which may occur multiple times if looped. +* An animation has **ended**, either due to interruption or it has completed and is not looped. +* An animation and its corresponding `spTrackEntry` have been **disposed** and are no longer valid. +* A [user defined **event**](/spine-events) was fired. + +You can listen for these events by registering a function either with the animation state, or with individual `spTrackEntry` instances returned by the animation state. + +``` +// Define the function that will be called when an event happens +void myListener(spAnimationState* state, spEventType type, spTrackEntry* entry, spEvent* event) { + switch (type) { + // + case SP_ANIMATION_START: + printf("Animation %s started on track %i\n", entry->animation->data->name, entry->trackIndex); + break; + case SP_ANIMATION_INTERRUPT: + printf("Animation %s interrupted on track %i\n", entry->animation->data->name, entry->trackIndex); + break; + case SP_ANIMATION_END: + printf("Animation %s ended on track %i\n", entry->animation->data->name, entry->trackIndex); + break; + case SP_ANIMATION_COMPLETE: + printf("Animation %s completed on track %i\n", entry->animation->data->name, entry->trackIndex); + break; + case SP_ANIMATION_DISPOSE: + printf("Track entry for animation %s disposed on track %i\n", entry->animation->data->name, entry->trackIndex); + break; + case SP_ANIMATION_EVENT: + printf("User defined event for animation %s on track %i\n", entry->animation->data->name, entry->trackIndex); + printf("Event: %s: %d, %f, %s\n", event->data->name, event->intValue, event->floatValue, event->stringValue); + break; + default: + printf("Unknown event type: %i", type); + } +} + +// Register the function as a listener on the animation state. It will be called for all +// animations queued on the animation state. +animationState->listener = myListener; + +// Or you can register the function as a listener for events for a specific animation you enqueued +spTrackEntry* trackEntry = spAnimationState_setAnimationByName(animationState, 0, "walk", 1); +trackEntry->listener = myListener; +``` + +User defined events are perfect to markup times in an animation at which sounds should be played back, e.g. foot steps. + +Changes made to the animation state within a listener, such as setting a new animation, are not applied to skeletons until the next time `spAnimationState_apply` is called. You can immediately apply the changes within the listener: + +``` +void myListener(spAnimationState* state, spEventType type, spTrackEntry* entry, spEvent* event) { + if (somecondition) { + spAnimationState_setAnimation(state, 0, "run", 0); + spAnimationState_update(0); + spAnimationState_apply(skeleton); + } +} +``` + +### Applying animation states +Animation states are inherently time-based. To advance their state, you will need to update them every tick, supplying the amount of time that has passed since the last update in seconds: + +``` +spAnimationState_update(deltaTime); +``` + +This will advance the playback of animations on each track, coordinate crossfades and call any listeners you might have registered. + +After updating the animation state, you generally want to apply it to a skeleton to update its bones local transforms, attachments, slot colors, draw order and anything else that can be animated: + +``` +spAnimationState_apply(skeleton); +``` + +With the skeleton posed and animated, you finally update its frame time and bones' world transforms to prepare it for rendering or physics: + +``` +spSkeleton_update(deltaTime); +spSkeleton_updateWorldTransform(skeleton); +``` + +# Memory management +We have tried to make spine-c memory as straight forward as possible. Any struct that is allocated via `spStructName_create` needs to be deallocated with the corresponding `spStructName_dispose`. The life-times of structs depends on what type of struct it is. General rules of thumb: + +* Create setup pose data shared by instance data (`spAtlas`, `spSkeletonData`, `spAnimationStateData`) at game or level startup, dispose it at game or level end. +* Create instance data (`spSkeleton`, `spAnimationState`) when the corresponding game object is created, dispose it when the corresponding game object is destroyed. + +Track entries (`spTrackEntry`) are valid from a call to one of the enqueuing animation state functions (`spAnimationState_setAnimationByName`, `spAnimationState_addAnimationByName`, `spAnimationState_setEmptyAnimation`, `spAnimationState_addEmptyAnimation`) until the `SP_ANIMATION_DISPOSE` event is send to your listener. Accessing the track entry after this event will likely result in a segmentation fault. + +When creating structs, you often pass in other structs as references. The referencing struct will never dispose the referenced struct. E.g. an `spSkeleton` references an `spSkeletonData` which in turn references an `spAtlas`. + +* Disposing the `spSkeleton` will not dispose the `spSkeletonData` nor the `spAtlas`. This makes sense, as the `spSkeletonData` is likely shared by other `spSkeleton`instances. +* Disposing the `spSkeletonData` will not dispose the `spAtlas`. This also makes sense, as the `spAtlas` may be shared by other `spSkeletonData` instances, e.g. if the atlas contains the images of multiple skeletons. + +If you use a custom allocator, you can overwrite Spine's allocation strategy (using `malloc`, `realloc` and `free`, by changing the defines in [extension.h](/git/spine-runtimes/blob/spine-c/spine-c/include/spine/extension.h#L65-L67). + +# Putting it all together +Here's a simplified example of how to put all the above together, from loading and instantiating to applying animations (scroll to see all the code): + +``` +// Setup pose data, shared by all skeletons +spAtlas* atlas; +spSkeletonData* skeletonData; +spAnimationStateData* animationStateData; + +// 5 skeleton instances and their animation states +spSkeleton* skeleton[5]; +spAnimationState* animationState[5]; +char* animationNames[] = { "walk", "run", "shot" }; + +void setup() { + // setup your engine so textures can be loaded for atlases, create a window, etc. + engine_setup(); + + // Load the texture atlas + atlas = spAtlas_createFromFile("spineboy.atlas", 0); + if (!atlas) { + printf("Failed to load atlas"); + exit(0); + } + + // Load the skeleton data + spSkeletonJson* json = spSkeletonJson_create(atlas); + skeletonData = spSkeletonJson_readSkeletonDataFile(json, "spineboy.json"); + if (!skeletonData) { + printf("Failed to load skeleton data"); + spAtlas_dispose(atlas); + exit(0); + } + spSkeletonJson_dispose(json); + + // Setup mix times + animationStateData = spAnimationStateData_create(skeletonData); + animationStateData->defaultMix = 0.5f; + spAnimationStateData_setMixByName("walk", "run", 0.2f); + spAnimationStateData_setMixByName("walk", "shot", 0.1f); +} + +void mainLoop() { + // Create 5 skeleton instances and animation states + // representing 5 game objects + for (int i = 0; i < 5; i++) { + // Create the skeleton and put it at a random position + spSkeleton* skeleton = spSkeleton_create(skeletonData); + skeleton->x = random(0, 200); + skeleton->y = random(0, 200); + + // Create the animation state and enqueue a random animation, looping + spAnimationState animationState = spAnimationState_create(animationStateData); + spAnimationState_setAnimation(animationState, 0, animationNames[random(0, 3)], 1); + } + + while (engine_gameIsRunning()) { + engine_clearScreen(); + + // update the game objects + for (int i = 0; i < 5; i++) { + spSkeleton* skeleton = skeletons[i]; + spAnimationState* animationState = animationStates[i]; + + // First update the animation state by the delta time + spAnimationState_update(animationState, engine_getDeltaTime()); + + // Next, apply the state to the skeleton + spAnimationState_apply(animationState, skeleton); + + // Update the skeleton's frame time for physics + spSkeleton_update(engine_getDeltaTime()); + + // Calculate world transforms for rendering + spSkeleton_updateWorldTransform(skeleton, SP_PHYSICS_UPDATE); + + // Hand off rendering the skeleton to the engine + engine_drawSkeleton(skeleton); + } + } + + // Dispose of the instance data. Normally you'd do this when + // a game object is disposed. + for (int i = 0; i < 5) { + spSkeleton_dispose(skeleton); + spAnimationState_dispose(animationState); + } +} + +void dispose() { + // dispose all the shared resources + spAtlas_dispose(atlas); + spSkeletonData_dispose(skeletonData); + spAnimationStateData_dispose(animationStateData); +} + +int main(int argc, char* argv) { + setup(); + mainLoop(); + dispose(); +} +``` + +Note the distinction of setup pose data (`spAtlas`, `spSkeletonData`, `spAnimationStateData`) and instance data (`spSkeleton`, `spAnimationState`) and their different life-times. + +# Integrating spine-c in your engine +## Integrating the sources +spine-c is a set a of C header and implementation files found in the [spine-c/spine-c](/git/spine-runtimes/tree/spine-c/spine-c) folder of the runtime Git repository. You can either copy the sources into your project, or use CMake's `FetchContent` + +### Copy sources +1. Clone the [Spine runtime repository](https://github.com/EsotericSoftware/spine-runtimes). Use the [version branch](/spine-versioning) corresponding with your Spine Editor branch. +2. Copy the sources from the [spine-c/spine-c/src/spine](/git/spine-runtimes/tree/spine-c/spine-c/src/spine) folder to the source folder of your project and make sure they are part of the compilation step of your project. +3. copy the folder `spine` containing the headers from the [spine-c/spine-c/include](/git/spine-runtimes/tree/spine-c/spine-c/include/spine) folder to the header folder of your project and make sure they are part of the include path the compiler uses to look up headers. Make sure to keep the `spine` folder as the spine-c sources include headers via `#include "spine/xxx.h"`. + +### CMake `FetchContent` +Starting with Spine version 4.2, you can also use CMake's `FetchContent` feature to easily integrate the spine-c runtime in your project, as illustrated by this example `CMakeLists.txt` file. + +``` +cmake_minimum_required(VERSION 3.14) +project(MyProject C) + +set(CMAKE_C_STANDARD 99) +set(CMAKE_C_STANDARD_REQUIRED ON) +set(FETCHCONTENT_QUIET NO) + +# Fetch the spine-runtimes repository and make the spine-c library available +include(FetchContent) +FetchContent_Declare( + spine-runtimes + GIT_REPOSITORY https://github.com/esotericsoftware/spine-runtimes.git + GIT_TAG 4.2 + GIT_SHALLOW TRUE +) +FetchContent_MakeAvailable(spine-runtimes) +FetchContent_GetProperties(spine-runtimes) +if(NOT spine-runtimes_POPULATED) + FetchContent_Populate(spine-runtimes) +endif() +add_subdirectory(${spine-runtimes_SOURCE_DIR}/spine-c ${CMAKE_BINARY_DIR}/spine-runtimes) + + +# Create a simple C executable +file(GLOB SOURCES "src/*.c") +add_executable(MyExecutable ${SOURCES}) +target_include_directories(MyExecutable PRIVATE src/) + +# Link the spine-c library +target_link_libraries(MyExecutable spine-c) + +``` + +## Implementing extension functions +If you compile your project you will receive linker errors for the functions the spine-c runtime expects you to implement. E.g. compiling with Clang will yield something like: + +``` +Undefined symbols for architecture x86_64: + "__spAtlasPage_createTexture", referenced from: + _spAtlas_create in libspine-c.a(Atlas.c.o) + "__spAtlasPage_disposeTexture", referenced from: + _spAtlasPage_dispose in libspine-c.a(Atlas.c.o) + "__spUtil_readFile", referenced from: + _spAtlas_createFromFile in libspine-c.a(Atlas.c.o) + _spSkeletonBinary_readSkeletonDataFile in libspine-c.a(SkeletonBinary.c.o) + _spSkeletonJson_readSkeletonDataFile in libspine-c.a(SkeletonJson.c.o) +ld: symbol(s) not found for architecture x86_64 +``` + +The 3 functions the linker can not find are called extension functions. The spine-c runtime expects you to implement these via APIs provided by your engine. The extension functions are defined in [extension.h](/git/spine-runtimes/blob/spine-c/spine-c/include/spine/extension.h#L144-L150). + +> **Note:** if you are using any of the official Spine runtimes that are based on the spine-c runtime, these functions plus rendering are already implemented for you. You can ignore this section. + +### Implementing _spUtil_readFile +spine-c uses the `_spUtil_readFile` function to read the full content [JSON and binary skeleton files (.json/.skel)](/spine-export) exported from the Spine editor, as well as [texture atlas files (.atlas)](/spine-texture-packer#Texture-Packing) into memory. Use the "Find usages" or equivalent function of your C/C++ IDE to see where `_spUtil_readFile` is called. + +The signature of `_spUtil_readFile` is as follows: + +``` +char* _spUtil_readFile (const char* path, int* length); +``` + +The function takes a UTF-8 path to the file it should read, as well as a pointer to an `int` in which the function will store the number of bytes read from the file. The function returns `0` in case the file could not be read or a pointer to a memory block the function allocated and read all bytes of the file into. + +Code that calls `_spUtil_readFile` is supposed to deallocate the returned memory once it is done processing the data. All code in spine-c that uses this function will make sure the memory is deallocated so you do not have to worry about it. + +The memory is supposed to be allocated with the [MALLOC or CALLOC macros](/git/spine-runtimes/blob/spine-c/spine-c/include/spine/extension.h#L65-L66) defined in `extension.h`. The spine-c code will then deallocate the memory using the [FREE](/git/spine-runtimes/blob/spine-c/spine-c/include/spine/extension.h#L85) macro. You can redefine `MALLOC`, `CALLOC` and `FREE` to use a custom memory allocation scheme throughout the spine-c runtime. + +The simplest implementation of `_spUtil_readFile` looks like this: + +``` +char* _spUtil_readFile (const char* path, int* length){ + return _readFile(path, length); +} +``` + +It uses a function called `_readFile` which is provided by spine-c in [extension.c](/git/spine-runtimes/blob/spine-c/spine-c/src/spine/extension.c#L64). `_readFile` uses `fopen` to read the file relative to the current working directory. + +If your engine uses a more sophisticated way of managing files, e.g. by packing them into a single compressed file with an internal file structure, you can implement `_spUtil_readFile` using your engine's file i/o API. + +### Implementing _spAtlasPage_createTexture and _spAtlasPage_disposeTexture +spine-c uses the `_spAtlasPage_createTexture` function to load and create an engine specific texture representation for a single page of a [texture atlas](/spine-texture-packer) exported for a Spine skeleton by the Spine editor. The function is called as part of [spAtlas_create](/git/spine-runtimes/blob/spine-c/spine-c/src/spine/Atlas.c#L167) and [spAtlas_createFromFile](/git/spine-runtimes/blob/spine-c/spine-c/src/spine/Atlas.c#L301), the code responsible for reading a Spine texture atlas file (.atlas) and the corresponding image files (usually .png) making up the pages of the atlas. + +The signature of `_spAtlasPage_createTexture` is as follows: + +``` +void _spAtlasPage_createTexture (spAtlasPage* self, const char* path); +``` + +The function is supposed to load the atlas page image file to an engine specific texture and store it on the `spAtlasPage` passed to the function. The `spAtlasPage` structure has a special field called `rendererObject` of type `void*` in which the engine specific texture should be stored. This engine specific texture stored in `rendererObject` is then later used to render a Spine skeleton using the engine APIs. + +The function is also supposed to set the width and height of the `spAtlasPage` in pixels according to the texture file loaded by the engine. This data is required to compute texture coordinates by spine-c. + +The `path` parameter is the path to the page image file, relative to the `.atlas` file path passed to [spAtlas_createFromFile](/git/spine-runtimes/blob/spine-c/spine-c/src/spine/Atlas.c#L301) or relative to the `dir` parameter passed to [spAtlas_create](/git/spine-runtimes/blob/spine-c/spine-c/src/spine/Atlas.c#L167). These two functions are used to load a texture atlas from a file or a memory block respectively. + +For the purpose of illustration, assume your engine provides the following API to work with textures: + +``` +typedef struct Texture { + // ... OpenGL handle, image data, whatever ... + int width; + int height; +} Texture; + +Texture* engine_loadTexture(const char* file); +void engine_disposeTexture(Texture* texture); +``` + +Implementing `_spAtlasPage_createTexture` is then as simple as: + +``` +void _spAtlasPage_createTexture (AtlasPage* self, const char* path){ + Texture* texture = engine_loadTexture(path); + + // if texture loading failed, self->rendererObject, self->width and + // self->height remain 0 and we simply return. + if (!texture) return; + + // store the Texture on the rendererObject so we can + // retrieve it later for rendering. + self->rendererObject = texture; + + // store the texture width and height on the spAtlasPage + // so spine-c can calculate texture coordinates for + // rendering. + self->width = texture->width; + self->height = texture->height; +} +``` + +The second function that needs to be implemented is `_spAtlasPage_disposeTexture`. It is responsible for disposing the texture of an `spAtlasPage` when the corresponding `spAtlas` is disposed via a call to [spAtlas_dispose](/git/spine-runtimes/blob/spine-c/spine-c/src/spine/Atlas.c#L327). The signature of `_spAtlasPage_disposeTexture` is as follows: + +``` +void _spAtlasPage_disposeTexture (spAtlasPage* self); +``` + +Given our assumed engine API, an implementation would look like this: + +``` +void _spAtlasPage_disposeTexture (spAtlasPage* self) { + // if the rendererObject is not set, loading failed + // so we do not need to dispose of anything. + if (!self->rendererObject) return; + + // Dispose the texture + Texture* texture = (Texture*)self->rendererObject; + engine_disposeTexture(texture); +} +``` + +## Implementing Rendering +Rendering a Spine skeleton means rendering all currently active attachments in the current [draw order](/spine-basic-concepts#Slots). The draw order is defined as an [array of slots](/git/spine-runtimes/blob/spine-c/spine-c/include/spine/Skeleton.h#L54) on the skeleton. + +Drawable attachments ([regions](/spine-regions), [(deformable) meshes](/spine-meshes)) define UV mapped, vertex colored, triangle meshes. The [rendererObject](/git/spine-runtimes/blob/spine-c/spine-c/include/spine/RegionAttachment.h#L52) on a drawable attachment stores a reference to the texture atlas region to which the triangles of the attachment mesh are mapped to. + +Assuming you already have animated your skeleton, either procedurally or via an animation state, and that you have updated the skeleton bones' world transform via a call to `spSkeleton_updateWorldTransform`, you can render a skeleton as follows: + +* For each slot in the draw order array of the skeleton + * Fetch the currently active attachment from the slot (can be null if no attachment is active) + * Fetch the blend mode from the slot and translate it to your engine's API + * Calculate the tint color based on the skeleton's and slot's color + * Check the type of the attachment + * If it is a region attachment + * Compute its world vertices by calling `spRegionAttachment_computeWorldVertices` + * Fetch the atlas page texture from the attachment's render object + * Calculate the tint color of the attachment by multiplying the skeleton, slot, and attachment color, given as RGBA in the range [0-1] per channel + * Combine the world space vertices, UVs and color into a triangle mesh + * Bind the texture through your engine's API + * Submit the mesh for rendering + * If it is a mesh attachment + * Compute its world vertices by calling `spVertexAttachment_computeWorldVertices` + * Fetch the atlas page texture from the attachment's render object + * Calculate the tint color of the attachment by multiplying the skeleton, slot, and attachment color, given as RGBA in the range [0-1] per channel + * Combine the world space vertices, UVs and color into a triangle mesh + * Bind the texture through your engine's API + * Submit the mesh for rendering + +Translating this to your engine should be trivial, provided your engine allows rendering of UV mapped, vertex colored triangle meshes. For illustration purposes, let's assume the following engine API: + +``` +// A single vertex with UV +typedef struct Vertex { + // Position in x/y plane + float x, y; + + // UV coordinates + float u, v; + + // Color, each channel in the range from 0-1 + // (Should really be a 32-bit RGBA packed color) + float r, g, b, a; +} Vertex; + +enum BlendMode { + // See http://esotericsoftware.com/git/spine-runtimes/blob/spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/BlendMode.java#L37 + // for how these translate to OpenGL source/destination blend modes. + BLEND_NORMAL, + BLEND_ADDITIVE, + BLEND_MULTIPLY, + BLEND_SCREEN, +} + +// Draw the given mesh. +// - vertices is a pointer to an array of Vertex structures +// - start defines from which vertex in the vertices array to start +// - count defines how many vertices to use for rendering (should be divisible by 3, as we render triangles, each triangle requiring 3 vertices) +// - texture the texture to use +// - blendMode the blend mode to use +void engine_drawMesh(Vertex* vertices, int start, int count, Texture* texture, BlendMode blendmode); +``` + +The rendering process can then be implemented like this: + +``` +#define MAX_VERTICES_PER_ATTACHMENT 2048 +float worldVerticesPositions[MAX_VERTICES_PER_ATTACHMENT]; +Vertex vertices[MAX_VERTICES_PER_ATTACHMENT]; + +// Little helper function to add a vertex to the scratch buffer. Index will be increased +// by one after a call to this function. +void addVertex(float x, float y, float u, float v, float r, float g, float b, float a, int* index) { + Vertex* vertex = &vertices[*index]; + vertex->x = x; + vertex->y = y; + vertex->u = u; + vertex->v = v; + vertex->r = r; + vertex->g = g; + vertex->b = b; + vertex->a = a; + *index += 1; +} + +void drawSkeleton(spSkeleton* skeleton) { + // For each slot in the draw order array of the skeleton + for (int i = 0; i < skeleton->slotsCount; ++i) { + spSlot* slot = skeleton->drawOrder[i]; + + // Fetch the currently active attachment, continue + // with the next slot in the draw order if no + // attachment is active on the slot + spAttachment* attachment = slot->attachment; + if (!attachment) continue; + + // Fetch the blend mode from the slot and + // translate it to the engine blend mode + BlendMode engineBlendMode; + switch (slot->data->blendMode) { + case SP_BLEND_MODE_NORMAL: + engineBlendMode = BLEND_NORMAL; + break; + case SP_BLEND_MODE_ADDITIVE: + engineBlendMode = BLEND_ADDITIVE; + break; + case SP_BLEND_MODE_MULTIPLY: + engineBlendMode = BLEND_MULTIPLY; + break; + case SP_BLEND_MODE_SCREEN: + engineBlendMode = BLEND_SCREEN; + break; + default: + // unknown Spine blend mode, fall back to + // normal blend mode + engineBlendMode = BLEND_NORMAL; + } + + // Fill the vertices array depending on the type of attachment + Texture* texture = 0; + int vertexIndex = 0; + if (attachment->type == ATTACHMENT_REGION) { + // Cast to an spRegionAttachment so we can get the rendererObject + // and compute the world vertices + spRegionAttachment* regionAttachment = (spRegionAttachment*)attachment; + + // Calculate the tinting color based on the skeleton's color + // and the slot's color. Each color channel is given in the + // range [0-1], you may have to multiply by 255 and cast to + // and int if your engine uses integer ranges for color channels. + float tintR = skeleton->color.r * slot->color.r * regionAttachment.color.r; + float tintG = skeleton->color.g * slot->color.g * regionAttachment.color.g; + float tintB = skeleton->color.b * slot->color.b * regionAttachment.color.b; + float tintA = skeleton->color.a * slot->color.a * regionAttachment.color.a; + + // Computed the world vertices positions for the 4 vertices that make up + // the rectangular region attachment. This assumes the world transform of the + // bone to which the slot (and hence attachment) is attached has been calculated + // before rendering via spSkeleton_updateWorldTransform + spRegionAttachment_computeWorldVertices(regionAttachment, slot->bone, worldVerticesPositions, 0, 2); + + // Our engine specific Texture is stored in the spAtlasRegion which was + // assigned to the attachment on load. It represents the texture atlas + // page that contains the image the region attachment is mapped to + texture = (Texture*)((spAtlasRegion*)regionAttachment->rendererObject)->page->rendererObject; + + // Create 2 triangles, with 3 vertices each from the region's + // world vertex positions and its UV coordinates (in the range [0-1]). + addVertex(worldVerticesPositions[0], worldVerticesPositions[1], + regionAttachment->uvs[0], regionAttachment->uvs[1], + tintR, tintG, tintB, tintA, &vertexIndex); + + addVertex(worldVerticesPositions[2], worldVerticesPositions[3], + regionAttachment->uvs[2], regionAttachment->uvs[3], + tintR, tintG, tintB, tintA, &vertexIndex); + + addVertex(worldVerticesPositions[4], worldVerticesPositions[5], + regionAttachment->uvs[4], regionAttachment->uvs[5], + tintR, tintG, tintB, tintA, &vertexIndex); + + addVertex(worldVerticesPositions[4], worldVerticesPositions[5], + regionAttachment->uvs[4], regionAttachment->uvs[5], + tintR, tintG, tintB, tintA, &vertexIndex); + + addVertex(worldVerticesPositions[6], worldVerticesPositions[7], + regionAttachment->uvs[6], regionAttachment->uvs[7], + tintR, tintG, tintB, tintA, &vertexIndex); + + addVertex(worldVerticesPositions[0], worldVerticesPositions[1], + regionAttachment->uvs[0], regionAttachment->uvs[1], + tintR, tintG, tintB, tintA, &vertexIndex); + } else if (attachment->type == ATTACHMENT_MESH) { + // Cast to an spMeshAttachment so we can get the rendererObject + // and compute the world vertices + spMeshAttachment* mesh = (spMeshAttachment*)attachment; + + // Calculate the tinting color based on the skeleton's color + // and the slot's color. Each color channel is given in the + // range [0-1], you may have to multiply by 255 and cast to + // and int if your engine uses integer ranges for color channels. + float tintR = skeleton->color.r * slot->color.r * regionAttachment.color.r; + float tintG = skeleton->color.g * slot->color.g * regionAttachment.color.g; + float tintB = skeleton->color.b * slot->color.b * regionAttachment.color.b; + float tintA = skeleton->color.a * slot->color.a * regionAttachment.color.a; + + // Check the number of vertices in the mesh attachment. If it is bigger + // than our scratch buffer, we don't render the mesh. We do this here + // for simplicity, in production you want to reallocate the scratch buffer + // to fit the mesh. + if (mesh->super.worldVerticesLength > MAX_VERTICES_PER_ATTACHMENT) continue; + + // Computed the world vertices positions for the vertices that make up + // the mesh attachment. This assumes the world transform of the + // bone to which the slot (and hence attachment) is attached has been calculated + // before rendering via spSkeleton_updateWorldTransform + spVertexAttachment_computeWorldVertices(SUPER(mesh), slot, 0, mesh->super.worldVerticesLength, worldVerticesPositions, 0, 2); + + // Our engine specific Texture is stored in the spAtlasRegion which was + // assigned to the attachment on load. It represents the texture atlas + // page that contains the image the mesh attachment is mapped to + texture = (Texture*)((spAtlasRegion*)mesh->rendererObject)->page->rendererObject; + + // Mesh attachments use an array of vertices, and an array of indices to define which + // 3 vertices make up each triangle. We loop through all triangle indices + // and simply emit a vertex for each triangle's vertex. + for (int i = 0; i < mesh->trianglesCount; ++i) { + int index = mesh->triangles[i] << 1; + addVertex(worldVerticesPositions[index], worldVerticesPositions[index + 1], + mesh->uvs[index], mesh->uvs[index + 1], + tintR, tintG, tintB, tintA, &vertexIndex); + } + + } + + // Draw the mesh we created for the attachment + engine_drawMesh(vertices, 0, vertexIndex, texture, engineBlendMode); + } +} +``` + +This naive implementation will get you up and running quickly. However, there are a couple of low-hanging fruit in terms of optimization: + +* `engine_drawMesh` is assumed to submit the mesh for rendering immediately. This means we issue one draw call per attachment on the skeleton. A production grade implementation should batch all the meshes into a single mesh. Ideally, if all attachments use the same texture atlas page and blend mode, drawing a skeleton will only incur a single draw call. If all skeletons in a scene share the same texture atlas page and blend mode, you can even batch all skeletons into a single mesh, and hence a single draw call. +* The current setup does not use indexed meshes, but instead submits 3 vertices per triangle. +* The current setup does not support two color tinting. +* The current setup does not support clipping. See the [spine-sfml](https://github.com/EsotericSoftware/spine-runtimes/blob/4.0/spine-sfml/c/src/spine/spine-sfml.cpp#L293) runtime for an example on how to implement clipping. \ No newline at end of file diff --git a/spine-c/src/generated/atlas_region.cpp b/spine-c/src/generated/atlas_region.cpp index 4464819f8..d6104b983 100644 --- a/spine-c/src/generated/atlas_region.cpp +++ b/spine-c/src/generated/atlas_region.cpp @@ -236,6 +236,16 @@ void spine_atlas_region_set_region_height(spine_atlas_region self, int value) { _self->setRegionHeight(value); } +/*@null*/ void *spine_atlas_region_get_renderer_object(spine_atlas_region self) { + TextureRegion *_self = (TextureRegion *) (AtlasRegion *) self; + return _self->getRendererObject(); +} + +void spine_atlas_region_set_renderer_object(spine_atlas_region self, /*@null*/ void *value) { + TextureRegion *_self = (TextureRegion *) (AtlasRegion *) self; + _self->setRendererObject(value); +} + spine_rtti spine_atlas_region_rtti(void) { return (spine_rtti) &AtlasRegion::rtti; } diff --git a/spine-c/src/generated/atlas_region.h b/spine-c/src/generated/atlas_region.h index b3e934024..3bde88331 100644 --- a/spine-c/src/generated/atlas_region.h +++ b/spine-c/src/generated/atlas_region.h @@ -58,6 +58,8 @@ SPINE_C_API int spine_atlas_region_get_region_width(spine_atlas_region self); SPINE_C_API void spine_atlas_region_set_region_width(spine_atlas_region self, int value); SPINE_C_API int spine_atlas_region_get_region_height(spine_atlas_region self); SPINE_C_API void spine_atlas_region_set_region_height(spine_atlas_region self, int value); +SPINE_C_API /*@null*/ void *spine_atlas_region_get_renderer_object(spine_atlas_region self); +SPINE_C_API void spine_atlas_region_set_renderer_object(spine_atlas_region self, /*@null*/ void *value); SPINE_C_API spine_rtti spine_atlas_region_rtti(void); #ifdef __cplusplus diff --git a/spine-c/src/generated/texture_region.cpp b/spine-c/src/generated/texture_region.cpp index a0198482a..f3c68331d 100644 --- a/spine-c/src/generated/texture_region.cpp +++ b/spine-c/src/generated/texture_region.cpp @@ -76,6 +76,16 @@ void spine_texture_region_set_region_height(spine_texture_region self, int value _self->setRegionHeight(value); } +/*@null*/ void *spine_texture_region_get_renderer_object(spine_texture_region self) { + TextureRegion *_self = (TextureRegion *) self; + return _self->getRendererObject(); +} + +void spine_texture_region_set_renderer_object(spine_texture_region self, /*@null*/ void *value) { + TextureRegion *_self = (TextureRegion *) self; + _self->setRendererObject(value); +} + spine_rtti spine_texture_region_rtti(void) { return (spine_rtti) &TextureRegion::rtti; } diff --git a/spine-c/src/generated/texture_region.h b/spine-c/src/generated/texture_region.h index 11aed9cb3..d235a5c71 100644 --- a/spine-c/src/generated/texture_region.h +++ b/spine-c/src/generated/texture_region.h @@ -26,6 +26,8 @@ SPINE_C_API int spine_texture_region_get_region_width(spine_texture_region self) SPINE_C_API void spine_texture_region_set_region_width(spine_texture_region self, int value); SPINE_C_API int spine_texture_region_get_region_height(spine_texture_region self); SPINE_C_API void spine_texture_region_set_region_height(spine_texture_region self, int value); +SPINE_C_API /*@null*/ void *spine_texture_region_get_renderer_object(spine_texture_region self); +SPINE_C_API void spine_texture_region_set_renderer_object(spine_texture_region self, /*@null*/ void *value); SPINE_C_API spine_rtti spine_texture_region_rtti(void); #ifdef __cplusplus diff --git a/spine-cpp/include/spine/TextureRegion.h b/spine-cpp/include/spine/TextureRegion.h index dd5c4abb9..158133ec5 100644 --- a/spine-cpp/include/spine/TextureRegion.h +++ b/spine-cpp/include/spine/TextureRegion.h @@ -83,6 +83,12 @@ namespace spine { void setRegionHeight(int value) { _regionHeight = value; } + void *getRendererObject() const { + return _rendererObject; + } + void setRendererObject(void *value) { + _rendererObject = value; + } private: void *_rendererObject; diff --git a/spine-flutter/lib/generated/spine_dart_bindings_generated.dart b/spine-flutter/lib/generated/spine_dart_bindings_generated.dart index f32725d98..b6528d25f 100644 --- a/spine-flutter/lib/generated/spine_dart_bindings_generated.dart +++ b/spine-flutter/lib/generated/spine_dart_bindings_generated.dart @@ -10217,6 +10217,36 @@ class SpineDartBindings { late final _spine_atlas_region_set_region_height = _spine_atlas_region_set_region_heightPtr.asFunction(); + ffi.Pointer spine_atlas_region_get_renderer_object( + spine_atlas_region self, + ) { + return _spine_atlas_region_get_renderer_object( + self, + ); + } + + late final _spine_atlas_region_get_renderer_objectPtr = + _lookup Function(spine_atlas_region)>>( + 'spine_atlas_region_get_renderer_object'); + late final _spine_atlas_region_get_renderer_object = + _spine_atlas_region_get_renderer_objectPtr.asFunction Function(spine_atlas_region)>(); + + void spine_atlas_region_set_renderer_object( + spine_atlas_region self, + ffi.Pointer value, + ) { + return _spine_atlas_region_set_renderer_object( + self, + value, + ); + } + + late final _spine_atlas_region_set_renderer_objectPtr = + _lookup)>>( + 'spine_atlas_region_set_renderer_object'); + late final _spine_atlas_region_set_renderer_object = + _spine_atlas_region_set_renderer_objectPtr.asFunction)>(); + spine_rtti spine_atlas_region_rtti() { return _spine_atlas_region_rtti(); } @@ -35530,6 +35560,36 @@ class SpineDartBindings { late final _spine_texture_region_set_region_height = _spine_texture_region_set_region_heightPtr.asFunction(); + ffi.Pointer spine_texture_region_get_renderer_object( + spine_texture_region self, + ) { + return _spine_texture_region_get_renderer_object( + self, + ); + } + + late final _spine_texture_region_get_renderer_objectPtr = + _lookup Function(spine_texture_region)>>( + 'spine_texture_region_get_renderer_object'); + late final _spine_texture_region_get_renderer_object = + _spine_texture_region_get_renderer_objectPtr.asFunction Function(spine_texture_region)>(); + + void spine_texture_region_set_renderer_object( + spine_texture_region self, + ffi.Pointer value, + ) { + return _spine_texture_region_set_renderer_object( + self, + value, + ); + } + + late final _spine_texture_region_set_renderer_objectPtr = + _lookup)>>( + 'spine_texture_region_set_renderer_object'); + late final _spine_texture_region_set_renderer_object = _spine_texture_region_set_renderer_objectPtr + .asFunction)>(); + spine_rtti spine_texture_region_rtti() { return _spine_texture_region_rtti(); } diff --git a/spine-flutter/lib/generated/texture_region.dart b/spine-flutter/lib/generated/texture_region.dart index 69731611b..eddf79057 100644 --- a/spine-flutter/lib/generated/texture_region.dart +++ b/spine-flutter/lib/generated/texture_region.dart @@ -111,6 +111,11 @@ class TextureRegion { SpineBindings.bindings.spine_texture_region_set_region_height(_ptr, value); } + Pointer? get rendererObject { + final result = SpineBindings.bindings.spine_texture_region_get_renderer_object(_ptr); + return result; + } + static Rtti rttiStatic() { final result = SpineBindings.bindings.spine_texture_region_rtti(); return Rtti.fromPointer(result); diff --git a/spine-ios/Sources/SpineSwift/Generated/texture_region.swift b/spine-ios/Sources/SpineSwift/Generated/texture_region.swift index 10817b16b..f20ac163e 100644 --- a/spine-ios/Sources/SpineSwift/Generated/texture_region.swift +++ b/spine-ios/Sources/SpineSwift/Generated/texture_region.swift @@ -113,6 +113,11 @@ public class TextureRegion: NSObject { } } + public var rendererObject: UnsafeMutableRawPointer? { + let result = spine_texture_region_get_renderer_object(_ptr.assumingMemoryBound(to: spine_texture_region_wrapper.self)) + return result + } + public static func rttiStatic() -> Rtti { let result = spine_texture_region_rtti() return Rtti(fromPointer: result!)