Skip to content

Instantly share code, notes, and snippets.

@kooparse
Last active November 29, 2023 09:49
Show Gist options
  • Save kooparse/d9a450bb907d7bb8a8ccfef750fca887 to your computer and use it in GitHub Desktop.
Save kooparse/d9a450bb907d7bb8a8ccfef750fca887 to your computer and use it in GitHub Desktop.
How to load a glTF model with gltf_parser.
/*
Some important definition here to understand what's going on.
1) Animation.
An animation have multiple "parts" (in glTF it's called channels).
Each part targets only one transformation, meanings one translation or rotation or scale on a specified node.
So when we want to play an animation, we go through each animation parts, and updates transformation "part"
for an associated node. After that, we compute the local transform matrix on those nodes,
then compute the updated world transform.
2) Skinning.
A model might have a skin (skeleton) associated with it. If so, a Skin structure
will be store on the model. This Skin contains an array of Joint_Info, which marks all node indices
that are used as joint in the skeleton, alongside a inverse bind matrix (used to go to the joint space).
3) On diffrent transform spaces.
Local transform : Local space related to the a node.
World transform : A space where points are related to the origin of the model.
This is computed by doing: parent_node.local_space * current_node.local_space.
Joint space : [Add description]
*/
// @todo: Add `is_changed` flag to trigger recompute of local_transform/world_transforms
// @todo: use bitmask for animation has_translation things...
// @todo: check all other todos
Skin :: struct {
joints : [..] Joint_Info;
}
Joint_Info :: struct {
ibm : Matrix4; // inverse bind matrix
node_index : int;
}
Animation_Transform :: struct {
translation : Vector3;
rotation : Quaternion;
scale := Vector3.{1, 1, 1};
// @todo: Use bitmask.
has_translation := false;
has_rotation := false;
has_scale := false;
animation_flags : enum_flags u16 { TRANSLATION; ROTATION; SCALE; NONE; } = .NONE;
}
Animation_Part :: struct {
keyframes : [..] float;
highest_keyframe: float;
target_data : [..] float;
target_type : GLTF_Target_Property;
interpolation : GLTF_Interpolation = .LINEAR;
node_ptr : *Model_Node;
}
Animation :: struct {
name : string;
parts : [..] Animation_Part;
animated_nodes : [..] int;
total_duration : float; // in secs. (keyframe max).
time : float; // current animation time in secs.
starter_time := FLOAT32_MAX;
has_finished : bool; // if time >= total_duration.
}
// @todo: should have a material.
Mesh :: struct {
gfx_handle : GFX_Handle;
using material : struct {
albedo_texture : GFX_Texture;
metallic_roughness_texture : GFX_Texture;
normal_texture : GFX_Texture;
emissive_texture : GFX_Texture;
occlusion_texture : GFX_Texture;
albedo_color : Vector4;
double_sided : bool;
alpha_mode : GLTF_Alpha_Mode;
alpha_cutoff : float;
};
}
Model_Node :: struct {
name : string;
parent := -1;
meshes : [..] Mesh;
children : [..] int; // child node indices
use_skin := false;
is_joint := false;
animation : Animation_Transform; // @todo: call it animation_transform?
translation := Vector3.{0, 0, 0};
rotation : Quaternion;
scale := Vector3.{1, 1, 1};
local_transform := Matrix4_Identity;
world_transform := Matrix4_Identity; // @todo: rename in global_space?
}
Model :: struct {
filepath : string;
root_nodes : [..] int;
flat_nodes : [..] Model_Node;
textures : [..] GFX_Texture;
has_skin := false;
skin : Skin;
animations : Table(string, Animation);
}
draw_model :: (model : *Model,
position := Vector3.{0, 0, 0},
rotation := Quaternion.{0, 0, 0, 1},
scaler := Vector3.{1, 1, 1}) {
draw_node :: (model: *Model, node_index: int, offset := Matrix4_Identity) {
joint_matrices: [100]Matrix4;
node := model.flat_nodes[node_index];
if node.use_skin {
for joint_info: model.skin.joints {
joint := model.flat_nodes[joint_info.node_index];
joint_mat := joint.world_transform * joint_info.ibm;
if node.parent != -1 {
parent := model.flat_nodes[node.parent];
joint_mat = inverse(parent.world_transform) * joint_mat;
}
joint_matrices[it_index] = transpose(joint_mat);
}
}
for node.meshes {
// We're doing this for now until we send the Material object.
handle := it.gfx_handle;
handle.albedo_texture_ptr = it.albedo_texture;
handle.metallic_roughness_texture_ptr = it.metallic_roughness_texture;
handle.normal_texture_ptr = it.normal_texture;
handle.emissive_texture_ptr = it.emissive_texture;
handle.occlusion_texture_ptr = it.occlusion_texture;
model := offset * node.world_transform;
model = transpose(model);
array_add(*meshes_to_draw, GFX_Mesh.{
handle = handle,
uniform = .{
joint_matrices = joint_matrices,
has_skin = node.use_skin,
model = model,
color = it.albedo_color,
alpha_mode = xx it.alpha_mode,
alpha_cutoff = it.alpha_cutoff,
}
});
}
for child_node_index: node.children {
draw_node(model, child_node_index, offset);
}
}
offset := Matrix4_Identity;
offset = translate(offset, position);
offset = rotate(offset, rotation);
offset = scale(offset, scaler);
for model.root_nodes draw_node(model, it, offset);
}
reset_animation :: (model: *Model, animation_name: string) {
using animation := table_find_pointer(*model.animations, animation_name);
if animation == null then crash("animation % not found.", animation_name);
time = 0;
has_finished = false;
}
find_animation:: (using model: *Model) -> *string {
for * animation: animations {
return *animation.name;
}
return null;
}
play_animation :: (using model: *Model, animation_name: string, loop := false) {
using animation := table_find_pointer(*animations, animation_name);
if animation == null then crash("animation % not found.", animation_name);
time += frame_time;
Clamp(*time, starter_time, total_duration);
if has_finished && loop {
time = starter_time;
has_finished = false;
}
for * anim_part: parts {
using anim_part;
// Keyframes are always sorted and ordered, also we can't have values
// under 0.
start_keyframe := keyframes[0];
end_keyframe := keyframes[keyframes.count - 1];
start_index := -1;
end_index := keyframes.count - 1;
// This part of the animation is finished or the
// first keyframe comes after the current time.
if time >= highest_keyframe {
if time >= total_duration {
has_finished = true;
}
continue;
}
for keyframe: keyframes {
if keyframe <= time {
start_keyframe = max(start_keyframe, keyframe);
start_index = max(start_index, it_index);
}
if keyframe >= time {
end_keyframe = min(end_keyframe, keyframe);
end_index = min(end_index, it_index);
}
}
assert(start_index != -1);
// Distance value between two keyframe, as a number between 0 and 1.
denominator := end_keyframe - start_keyframe;
numerator := time - start_keyframe;
distance := ifx denominator > 0 then (numerator / denominator) else 0;
if #complete target_type == {
case .TRANSLATION;
data := cast(*[3]float) target_data.data;
start := make_vector3(data[start_index]);
end := make_vector3(data[end_index]);
node_ptr.animation.translation = lerp(start, end, distance);
node_ptr.animation.has_translation = true;
case .ROTATION;
data := cast(*[4]float) target_data.data;
start := make_quaternion(data[start_index]);
end := make_quaternion(data[end_index]);
node_ptr.animation.rotation = nlerp_shortest(start, end, distance);
node_ptr.animation.has_rotation = true;
case .SCALE;
data := cast(*[3]float) target_data.data;
start := make_vector3(data[start_index]);
end := make_vector3(data[end_index]);
node_ptr.animation.scale = lerp(start, end, distance);
node_ptr.animation.has_scale = true;
case .WEIGHTS; crash("Weights animation not supported yet.");
}
}
for animated_nodes {
node := *model.flat_nodes[it];
animation := node.animation;
translation := ifx animation.has_translation
then make_translation_matrix4(animation.translation)
else make_translation_matrix4(node.translation);
rotation := ifx animation.has_rotation
then rotation_matrix(Matrix4, animation.rotation)
else rotation_matrix(Matrix4, node.rotation);
scale := ifx animation.has_scale
then make_scale_matrix4(animation.scale)
else make_scale_matrix4(node.scale);
node.local_transform = translation * rotation * scale;
}
set_world_transform :: (model: *Model, parent_transform: Matrix4, node: *Model_Node) {
node.world_transform = parent_transform * node.local_transform;
for node.children {
child_node := *model.flat_nodes[it];
set_world_transform(model, node.world_transform, child_node);
}
}
for model.root_nodes {
root_node := *model.flat_nodes[it];
set_world_transform(model, Matrix4_Identity, root_node);
}
}
free_model :: (model: *Model) {
free_node :: (node: Model_Node) {
for node.meshes free_mesh(it.gfx_handle);
array_free(node.meshes);
array_free(node.children);
}
defer for model.flat_nodes free_node(it);
defer array_free(model.flat_nodes);
}
load_model :: (filepath: string) -> Model {
model := Model.{};
folder_path := filepath;
folder_path.count = find_index_from_right(filepath, #char "/") + 1;
gltf_data := gltf_parse_file(filepath);
defer gltf_free(*gltf_data);
gltf_debug_print(gltf_data);
gltf_load_buffers(*gltf_data);
model.filepath = copy_string(filepath);
// Before parsing nodes, we first load all textures from the model because
// later, we're going to use texture indices to fill each node's textures.
for gltf_data.images {
img_path := join(folder_path, it.uri, allocator=temp);
gfx_texture := ifx it.uri add_image(img_path, false)
else add_image_from_memory(it.data, false);
array_add(*model.textures, gfx_texture);
}
root_scene := gltf_data.scenes[0];
// @todo: we should be able to load model with multiple scene.
if gltf_data.scenes.count >= 2 then log("Warning: Multiple scenes found!");
for root_node: root_scene.nodes {
array_add(*model.root_nodes, root_node);
}
for gltf_node: gltf_data.nodes {
node := parse_node(*model, *gltf_data, gltf_node);
array_add(*model.flat_nodes, node);
}
model.has_skin = gltf_data.skins.count >= 1;
if model.has_skin {
skin: Skin;
defer model.skin = skin;
// @todo: should be able to load multiple skeleton/armature.
if gltf_data.skins.count >= 2 {
crash("Multiple armature not supported.");
}
gltf_skin := gltf_data.skins[0];
accessor := gltf_data.accessors[gltf_skin.inverse_bind_matrices];
ibm_list : [..] float;
ibm_list.allocator = temp;
read_buffer_from_accessor(*gltf_data, accessor, *ibm_list);
ibm_as_mat4 := cast(*[16]float) ibm_list.data;
for node_index: gltf_skin.joints {
model.flat_nodes[node_index].is_joint = true;
ibm := Matrix4_Identity;
for ibm_as_mat4[it_index] {
ibm.floats[it_index] = it;
}
joint_info := Joint_Info.{
// Same as our world_transform, we want row-major matrices stored
// inside Matrix4 struct.
ibm = transpose(ibm),
node_index = node_index,
};
array_add(*skin.joints, joint_info);
}
}
for gltf_data.animations {
using animation: Animation;
name = ifx it.name != ""
then copy_string(it.name)
else sprint("animation_%", it_index);
defer table_add(*model.animations, animation.name, animation);
for channel: it.channels {
using animation_part : Animation_Part;
defer array_add(*parts, animation_part);
assert(channel.sampler != -1);
assert(channel.target.node != -1);
sampler := it.samplers[channel.sampler];
assert(sampler.input != -1);
assert(sampler.output != -1);
input := gltf_data.accessors[sampler.input];
output := gltf_data.accessors[sampler.output];
read_buffer_from_accessor(*gltf_data, input, *keyframes);
read_buffer_from_accessor(*gltf_data, output, *target_data);
for keyframes {
starter_time = min(it, starter_time);
highest_keyframe = max(it, highest_keyframe);
}
target_node := channel.target.node;
{
node_in_associated_nodes := false;
for animated_nodes {
if target_node == it then node_in_associated_nodes = true;
}
if !node_in_associated_nodes then array_add(*animated_nodes, target_node);
}
node_ptr = *model.flat_nodes[target_node];
target_type = channel.target.property;
interpolation = sampler.interpolation;
total_duration = max(total_duration, highest_keyframe);
if interpolation != GLTF_Interpolation.LINEAR {
crash("Only Linear interpolation is supported.");
}
if target_type == .WEIGHTS {
crash("Weights are not supported for animation.");
}
}
}
return model;
}
#scope_file
parse_node :: (model: *Model, gltf_data: *GLTF_Data, gltf_node: GLTF_Node) -> Model_Node {
node := Model_Node.{
// @todo: perhaps we dont want to copy?
name = copy_string(gltf_node.name),
parent = gltf_node.parent,
use_skin = gltf_node.skin != -1,
translation = make_vector3(gltf_node.translation),
rotation = make_quaternion(gltf_node.rotation),
scale = make_vector3(gltf_node.scale),
local_transform = gltf_node.local_transform,
world_transform = gltf_node.world_transform,
};
for child_node_index: gltf_node.children {
array_add(*node.children, child_node_index);
}
if (gltf_node.mesh == -1) return node;
node_mesh := gltf_data.meshes[gltf_node.mesh];
for primitive: node_mesh.primitives {
using primitive;
vertices: [..]float;
vertices.allocator = temp;
indices : [..]u16;
indices.allocator = temp;
uvs : [..]float;
uvs.allocator = temp;
colors : [..]float;
colors.allocator = temp;
normals : [..]float;
normals.allocator = temp;
tangents: [..]float;
tangents.allocator = temp;
joints : [..]u32;
joints.allocator = temp;
weights : [..]float;
weights.allocator = temp;
if indices_accessor != -1 {
accessor := gltf_data.accessors[indices_accessor];
read_buffer_from_accessor(gltf_data, accessor, *indices);
}
if position_accessor != -1 {
accessor := gltf_data.accessors[position_accessor];
read_buffer_from_accessor(gltf_data, accessor, *vertices);
}
if texcoord_0_accessor != -1 {
accessor := gltf_data.accessors[texcoord_0_accessor];
read_buffer_from_accessor(gltf_data, accessor, *uvs);
}
if color_accessor != -1 {
accessor := gltf_data.accessors[color_accessor];
read_buffer_from_accessor(gltf_data, accessor, *colors);
}
if normal_accessor != -1 {
accessor := gltf_data.accessors[normal_accessor];
read_buffer_from_accessor(gltf_data, accessor, *normals);
}
if tangent_accessor != -1 {
accessor := gltf_data.accessors[tangent_accessor];
read_buffer_from_accessor(gltf_data, accessor, *tangents);
}
if joints_accessor != -1 {
accessor := gltf_data.accessors[joints_accessor];
read_buffer_from_accessor(gltf_data, accessor, *joints);
}
if weights_accessor != -1 {
accessor := gltf_data.accessors[weights_accessor];
read_buffer_from_accessor(gltf_data, accessor, *weights);
}
vertex_count := vertices.count / 3;
if indices.count > 0 vertex_count = indices.count;
mesh := Mesh.{
gfx_handle = create_mesh(.{
vertices = vertices,
indices = indices,
uvs = uvs,
colors = colors,
normals = normals,
tangents = tangents,
joints = joints,
weights = weights,
vertex_count = vertex_count,
})
};
if primitive.material != -1 {
material := gltf_data.materials[primitive.material];
mesh.alpha_mode = material.alpha_mode;
mesh.alpha_cutoff = material.alpha_cutoff;
albedo_color := material.metallic_roughness.base_color_factor;
mesh.albedo_color = rgba(albedo_color[0], albedo_color[1],
albedo_color[2], albedo_color[3]);
if material.metallic_roughness.has_base_color {
index := material.metallic_roughness.base_color_texture.index;
assert(index != -1);
texture := gltf_data.textures[index];
texture_index := texture.source;
gfx_texture := model.textures[texture_index];
mesh.albedo_texture = gfx_texture;
}
if material.metallic_roughness.has_metallic_roughness {
index := material.metallic_roughness.metallic_roughness_texture.index;
assert(index != -1);
texture := gltf_data.textures[index];
texture_index := texture.source;
gfx_texture := model.textures[texture_index];
mesh.metallic_roughness_texture = gfx_texture;
}
if material.has_normal {
index := material.normal_texture.index;
assert(index != -1);
assert(material.normal_texture.scale == 1.0);
texture := gltf_data.textures[index];
texture_index := texture.source;
gfx_texture := model.textures[texture_index];
mesh.normal_texture = gfx_texture;
}
if material.has_emissive {
index := material.emissive_texture.index;
assert(index != -1);
texture := gltf_data.textures[index];
texture_index := texture.source;
gfx_texture := model.textures[texture_index];
mesh.emissive_texture = gfx_texture;
}
if material.has_occlusion {
index := material.occlusion_texture.index;
assert(index != -1);
texture := gltf_data.textures[index];
texture_index := texture.source;
gfx_texture := model.textures[texture_index];
mesh.occlusion_texture = gfx_texture;
}
}
array_add(*node.meshes, mesh);
}
return node;
}
make_quaternion :: (data: [4]float) -> Quaternion {
return make_quaternion(data[0], data[1], data[2], data[3]);
}
nlerp_shortest :: (a : Quaternion, b : Quaternion, t : float) -> Quaternion #must {
first := a;
second := b;
// When thinking about the axis-angle representation of the quaternions,
// if the dot product is negative, that means that the axes or rotation are
// at least 90 degrees apart. Negating the axis and angle (the whole quaternion)
// makes the quaternions' axes less than 90 degrees apart in that case, without
// changing the rotation the quaternion represents, and makes the interpolation
// take the shortest path.
if dot (first, second) < 0
second = -second;
return nlerp(first, second, t);
}
#import, dir "../../gltf_parser";
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment