Last active
November 29, 2023 09:49
-
-
Save kooparse/d9a450bb907d7bb8a8ccfef750fca887 to your computer and use it in GitHub Desktop.
How to load a glTF model with gltf_parser.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
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