From 6ce37294be31d4a979e6ea7a5336bbed6e70525d Mon Sep 17 00:00:00 2001 From: Cameron Reikes Date: Sun, 2 Jul 2023 14:47:31 -0700 Subject: [PATCH] Skin mesh vertices from bone pose in vert shader, fix rotation of objects being weird --- armature.glsl | 59 +++++++++++++++++++++++ art/Exporter.py | 47 ++++++++++++++++-- art/art.blend | 4 +- main.c | 126 +++++++++++++++++++++++++++++++++++++++++------- run_codegen.bat | 1 + 5 files changed, 213 insertions(+), 24 deletions(-) create mode 100644 armature.glsl diff --git a/armature.glsl b/armature.glsl new file mode 100644 index 0000000..5e494e7 --- /dev/null +++ b/armature.glsl @@ -0,0 +1,59 @@ +@module armature + +@vs vs +in vec3 pos_in; +in vec2 uv_in; +in vec4 indices_in; // is a sokol SG_VERTEXFORMAT_USHORT4N, a 16 bit unsigned integer treated as a floating point number due to webgl compatibility +in vec4 weights_in; + +out vec3 pos; +out vec2 uv; + +uniform vs_params { + mat4 model; + mat4 view; + mat4 projection; + mat4 bones[4]; +}; + +void main() { + vec4 total_position = vec4(0.0f); + + for(int i = 0; i < 4; i++) + { + float index_float = indices_in[i]; + int index = int(index_float * 65535.0); + float weight = weights_in[i]; + + vec4 local_position = bones[index] * vec4(pos_in, 1.0f); + total_position += local_position * weight; + } + + gl_Position = projection * view * model * total_position; + //gl_Position = projection * view * model * vec4(pos_in, 1.0); + + pos = gl_Position.xyz; + uv = uv_in; +} +@end + +@fs fs +uniform sampler2D tex; +in vec3 pos; +in vec2 uv; +out vec4 frag_color; + +void main() { + vec4 col = texture(tex, uv); + if(col.a < 0.5) + { + discard; + } + else + { + frag_color = vec4(col.rgb, 1.0); + } +} +@end + +@program program vs fs diff --git a/art/Exporter.py b/art/Exporter.py index 3e255fe..ca691e6 100644 --- a/art/Exporter.py +++ b/art/Exporter.py @@ -30,6 +30,9 @@ def write_u64(f, number: int): def write_i32(f, number: int): f.write(bytes(struct.pack("i", number))) +def write_u16(f, number: int): # unsigned short, used in shaders to figure out which bone index is current + f.write(bytes(struct.pack("H", number))) + def write_v3(f, vector): write_f32(f, vector.x) write_f32(f, vector.y) @@ -52,6 +55,13 @@ def write_4x4matrix(f, m): for col in range(4): write_f32(f, m[row][col]) +def normalize_joint_weights(weights): + total_weights = sum(weights) + result = [0,0,0,0] + if total_weights != 0: + for i, weight in enumerate(weights): result[i] = weight/total_weights + return result + # for the level.bin level_object_data = [] collision_cubes = [] @@ -66,6 +76,10 @@ mapping = axis_conversion( ) mapping.resize_4x4() +with open(bpy.path.abspath(f"//{EXPORT_DIRECTORY}/shorttest.bin"), "wb") as f: + for i in range(4): + write_u16(f, i) + # meshes can either be Meshes, or Armatures. Armatures contain all mesh data to draw it, and any anims it has for o in D.objects: @@ -73,9 +87,9 @@ for o in D.objects: if o.parent and o.parent.type == "ARMATURE": mesh_object = o o = o.parent - object_transform_info = (mesh_name, mapping @ mesh_object.location, mesh_object.rotation_euler, mesh_object.scale) + object_transform_info = (mesh_name, mapping @ o.location, o.rotation_euler, o.scale) if o.users_collection[0].name == 'Level': - assert(False, "Cannot put armatures in the level. The level is for static placed meshes. For dynamic entities, you put them outside of the level collection") + assert(False, "Cannot put armatures in the level. The level is for static placed meshes. For dynamic entities, you put them outside of the level collection, their entity kind is encoded, and the game code decides how to draw them") else: placed_entities.append((mesh_object.name,) + object_transform_info) armature_name = o.data.name @@ -115,6 +129,7 @@ for o in D.objects: parent_space_pose = pose_bone.parent.matrix.inverted() @ pose_bone.matrix else: parent_space_pose = mapping @ pose_bone.matrix + #parent_space_pose = pose_bone.matrix print("parent_space_pose of the bone with no parent:") print(parent_space_pose) @@ -138,7 +153,7 @@ for o in D.objects: vertices = [] - + armature = o for polygon in mesh.polygons: if len(polygon.loop_indices) == 3: for loopIndex in polygon.loop_indices: @@ -147,16 +162,38 @@ for o in D.objects: uv = mesh.uv_layers.active.data[loop.index].uv normal = loop.normal - vertices.append((position, uv)) + jointIndices = [0,0,0,0] + jointWeights = [0,0,0,0] + for jointBindingIndex, group in enumerate(mesh.vertices[loop.vertex_index].groups): + if jointBindingIndex < 4: + groupIndex = group.group + boneName = mesh_object.vertex_groups[groupIndex].name + jointIndices[jointBindingIndex] = armature.data.bones.find(boneName) + if jointIndices[jointBindingIndex] == -1: + # it's fine that this references a real bone, the bone at index 0, + # because the weight of its influence is 0 + jointIndices[jointBindingIndex] = 0 + jointWeights[jointBindingIndex] = 0.0 + else: + jointWeights[jointBindingIndex] = group.weight + + + vertices.append((position, uv, jointIndices, normalize_joint_weights(jointWeights))) write_u64(f, len(vertices)) + vertex_i = 0 for v_and_uv in vertices: - v, uv = v_and_uv + v, uv, jointIndices, jointWeights = v_and_uv write_f32(f, v.x) write_f32(f, v.y) write_f32(f, v.z) write_f32(f, uv.x) write_f32(f, uv.y) + for i in range(4): + write_u16(f, jointIndices[i]) + for i in range(4): + write_f32(f, jointWeights[i]) + vertex_i += 1 print(f"Wrote {len(vertices)} vertices") else: # if the parent type isn't an armature, i.e just a bog standard mesh diff --git a/art/art.blend b/art/art.blend index a808e9b..64f6f62 100644 --- a/art/art.blend +++ b/art/art.blend @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8c67d82d883089b6ddc73ac444a7a215f7c53fab24d4ec99e7037ab99c76ce9a -size 7208696 +oid sha256:1a35534a95be924967140ae58b67752a24ec2b0f333c357149a5890dfed4c879 +size 13201896 diff --git a/main.c b/main.c index b586812..8f7e289 100644 --- a/main.c +++ b/main.c @@ -187,6 +187,7 @@ int min(int a, int b) // so can be grep'd and removed #define dbgprint(...) { printf("Debug | %s:%d | ", __FILE__, __LINE__); printf(__VA_ARGS__); } #define v3varg(v) v.x, v.y, v.z +#define v2varg(v) v.x, v.y #define qvarg(v) v.x, v.y, v.z, v.w Vec2 RotateV2(Vec2 v, float theta) { @@ -795,6 +796,7 @@ SER_MAKE_FOR_TYPE(Vec3); SER_MAKE_FOR_TYPE(AnimKind); SER_MAKE_FOR_TYPE(EntityRef); SER_MAKE_FOR_TYPE(NPCPlayerStanding); +SER_MAKE_FOR_TYPE(MD_u16); void ser_Quat(SerState *ser, Quat *q) { @@ -816,6 +818,8 @@ typedef struct { Vec3 position; Vec2 uv; + MD_u16 joint_indices[4]; + float joint_weights[4]; } ArmatureVertex; SER_MAKE_FOR_TYPE(Vertex); @@ -1037,6 +1041,17 @@ Armature load_armature(MD_Arena *arena, MD_String8 binary_file, MD_String8 armat { ser_Vec3(&ser, &to_return.vertices[i].position); ser_Vec2(&ser, &to_return.vertices[i].uv); + MD_u16 joint_indices[4]; + float joint_weights[4]; + for(int ii = 0; ii < 4; ii++) + ser_MD_u16(&ser, &joint_indices[ii]); + for(int ii = 0; ii < 4; ii++) + ser_float(&ser, &joint_weights[ii]); + + for(int ii = 0; ii < 4; ii++) + to_return.vertices[i].joint_indices[ii] = joint_indices[ii]; + for(int ii = 0; ii < 4; ii++) + to_return.vertices[i].joint_weights[ii] = joint_weights[ii]; } Log("Armature %.*s has %llu vertices\n", MD_S8VArg(armature_name), to_return.vertices_length); @@ -1163,7 +1178,7 @@ ThreeDeeLevel load_level(MD_Arena *arena, MD_String8 binary_file) if(!mesh_found) { MD_String8 to_load_filepath = MD_S8Fmt(scratch.arena, "assets/exported_3d/%.*s.bin", MD_S8VArg(placed_mesh_name)); - Log("Loading '%.*s'...\n", MD_S8VArg(to_load_filepath)); + Log("Loading mesh '%.*s'...\n", MD_S8VArg(to_load_filepath)); MD_String8 binary_mesh_file = MD_LoadEntireFile(scratch.arena, to_load_filepath); if(!binary_mesh_file.str) { @@ -1204,6 +1219,7 @@ ThreeDeeLevel load_level(MD_Arena *arena, MD_String8 binary_file) MD_String8 placed_entity_name = {0}; ser_MD_String8(&ser, &placed_entity_name, scratch.arena); + bool found = false; ARR_ITER_I(CharacterGen, characters, kind) { @@ -1224,6 +1240,8 @@ ThreeDeeLevel load_level(MD_Arena *arena, MD_String8 binary_file) { ser.cur_error = (SerError){.failed = true, .why = MD_S8Fmt(arena, "Couldn't find placed npc kind '%.*s'...\n", MD_S8VArg(placed_entity_name))}; } + + Log("Loaded placed entity '%.*s' at %f %f %f\n", MD_S8VArg(placed_entity_name), v3varg(new_placed->t.offset)); } } @@ -1237,6 +1255,7 @@ ThreeDeeLevel load_level(MD_Arena *arena, MD_String8 binary_file) #include "assets.gen.c" #include "quad-sapp.glsl.h" #include "threedee.glsl.h" +#include "armature.glsl.h" AABB level_aabb = { .upper_left = { 0.0f, 0.0f }, .lower_right = { TILE_SIZE * LEVEL_TILES, -(TILE_SIZE * LEVEL_TILES) } }; GameState gs = { 0 }; @@ -2599,6 +2618,7 @@ static struct sg_bindings bind; sg_pipeline threedee_pip; + sg_pipeline armature_pip; sg_bindings threedee_bind; } state; @@ -2610,6 +2630,7 @@ void draw_placed(Mat4 view, Mat4 projection, PlacedMesh *cur) { Mesh *drawing = cur->draw_with; state.threedee_bind.vertex_buffers[0] = drawing->loaded_buffer; + sg_apply_pipeline(state.threedee_pip); sg_apply_bindings(&state.threedee_bind); Mat4 model = transform_to_mat(cur->t); @@ -2628,16 +2649,32 @@ void draw_placed(Mat4 view, Mat4 projection, PlacedMesh *cur) void draw_armature(Mat4 view, Mat4 projection, Transform t, Armature *armature) { state.threedee_bind.vertex_buffers[0] = armature->loaded_buffer; + sg_apply_pipeline(state.armature_pip); sg_apply_bindings(&state.threedee_bind); Mat4 model = transform_to_mat(t); - threedee_vs_params_t params = {0}; + armature_vs_params_t params = {0}; memcpy(params.model, (float*)&model, sizeof(model)); memcpy(params.view, (float*)&view, sizeof(view)); memcpy(params.projection, (float*)&projection, sizeof(projection)); - sg_apply_uniforms(SG_SHADERSTAGE_VS, SLOT_threedee_vs_params, &SG_RANGE(params)); + for(MD_u64 i = 0; i < armature->bones_length; i++) + { + Bone *cur = &armature->bones[i]; + PoseBone *cur_pose_bone = &armature->poses[i]; + + Mat4 final = M4D(1.0f); + final = MulM4(cur->inverse_model_space_pos, final); + for(PoseBone *cur_posebone = cur_pose_bone; cur_posebone; cur_posebone = cur_posebone->parent) + { + final = MulM4(cur_posebone->parent_space_pose, final); + } + + memcpy(params.bones[i], (float*)&final, sizeof(final)); + } + + sg_apply_uniforms(SG_SHADERSTAGE_VS, SLOT_armature_vs_params, &SG_RANGE(params)); num_draw_calls += 1; num_vertices += (int)armature->vertices_length; sg_draw(0, (int)armature->vertices_length, 1); @@ -2871,8 +2908,13 @@ void init(void) DialogNode cur_node = { 0 }; load_assets(); - - MD_String8 binary_file = MD_LoadEntireFile(frame_arena, MD_S8Lit("assets/exported_3d/Player.bin")); + + MD_String8 binary_file; + + binary_file = MD_LoadEntireFile(frame_arena, MD_S8Lit("assets/exported_3d/level.bin")); + level_threedee = load_level(persistent_arena, binary_file); + + binary_file = MD_LoadEntireFile(frame_arena, MD_S8Lit("assets/exported_3d/Player.bin")); mesh_player = load_mesh(persistent_arena, binary_file, MD_S8Lit("Player.bin")); @@ -2883,8 +2925,6 @@ void init(void) MD_ArenaClear(frame_arena); - binary_file = MD_LoadEntireFile(frame_arena, MD_S8Lit("assets/exported_3d/level.bin")); - level_threedee = load_level(persistent_arena, binary_file); reset_level(); #ifdef DEVTOOLS @@ -3029,6 +3069,40 @@ void init(void) .label = "threedee", }); + desc = armature_program_shader_desc(sg_query_backend()); + assert(desc); + shd = sg_make_shader(desc); + + state.armature_pip = sg_make_pipeline(&(sg_pipeline_desc) + { + .shader = shd, + .depth = { + .compare = SG_COMPAREFUNC_LESS_EQUAL, + .write_enabled = true + }, + .layout = { + .attrs = + { + [ATTR_armature_vs_pos_in].format = SG_VERTEXFORMAT_FLOAT3, + [ATTR_armature_vs_uv_in].format = SG_VERTEXFORMAT_FLOAT2, + [ATTR_armature_vs_indices_in].format = SG_VERTEXFORMAT_USHORT4N, + [ATTR_armature_vs_weights_in].format = SG_VERTEXFORMAT_FLOAT4, + } + }, + .colors[0].blend = (sg_blend_state) { // allow transparency + .enabled = true, + .src_factor_rgb = SG_BLENDFACTOR_SRC_ALPHA, + .dst_factor_rgb = SG_BLENDFACTOR_ONE_MINUS_SRC_ALPHA, + .op_rgb = SG_BLENDOP_ADD, + .src_factor_alpha = SG_BLENDFACTOR_ONE, + .dst_factor_alpha = SG_BLENDFACTOR_ONE_MINUS_SRC_ALPHA, + .op_alpha = SG_BLENDOP_ADD, + }, + .label = "armature", + }); + + + state.clear_depth_buffer_pass_action = (sg_pass_action) { .colors[0] = { .action = SG_ACTION_LOAD }, @@ -3942,7 +4016,6 @@ Vec2 move_and_slide(MoveSlideParams p) BUFF_ITER(CollisionObj, &to_check) { - dbgplanerect(it->aabb); if (overlapping(at_new, it->aabb)) { BUFF_APPEND(&actually_overlapping, *it); @@ -4521,10 +4594,11 @@ void draw_item(ItemKind kind, AABB in_aabb, float alpha) Transform entity_transform(Entity *e) { - // the mods to e->rotation here are just chosen based on what looks right with model - // facing forward towards - Quat entity_rot = QFromAxisAngle_RH(V3(0,1,0), AngleRad(-e->rotation - PI32/2.0f)); - //dbgplaneline(e->pos, AddV2(e->pos, RotateV2(V2(5.0f, 0.0), e->rotation))); + // Models must face +X in blender. This is because, in the 2d game coordinate system, + // a zero degree 2d rotation means you're facing +x, and this is how it is in the game logic. + // The rotation is negative for some reason that I'm not quite sure about though, something about + // the handedness of the 3d coordinate system not matching the handedness of the 2d coordinate system + Quat entity_rot = QFromAxisAngle_RH(V3(0,1,0), AngleRad(-e->rotation)); return (Transform){.offset = AddV3(plane_point(e->pos), V3(0,0,0)), .rotation = entity_rot, .scale = V3(1, 1, 1)}; /* @@ -4574,7 +4648,6 @@ void frame(void) uint64_t time_start_frame = stm_now(); Vec3 player_pos = V3(gs.player->pos.x, 0.0, gs.player->pos.y); - dbg3dline(player_pos, V3(0,0,0)); //dbgline(V2(0,0), V2(500, 500)); const float vertical_to_horizontal_ratio = 0.8f; const float cam_distance = 20.0f; @@ -4638,6 +4711,7 @@ void frame(void) } projection = Perspective_RH_NO(PI32/4.0f, screen_size().x / screen_size().y, 0.01f, 1000.0f); + // debug draw armature assert(armature.bones_length == armature.poses_length); for(MD_u64 i = 0; i < armature.bones_length; i++) { @@ -4650,6 +4724,7 @@ void frame(void) Vec3 x = MulM4V3(cur->matrix_local, V3(cur->length,0,0)); Vec3 y = MulM4V3(cur->matrix_local, V3(0,cur->length,0)); Vec3 z = MulM4V3(cur->matrix_local, V3(0,0,cur->length)); + Vec3 dot = MulM4V3(cur->matrix_local, V3(cur->length,0,cur->length)); Vec3 should_be_zero = MulM4V3(cur->inverse_model_space_pos, from); assert(should_be_zero.x == 0.0); @@ -4660,9 +4735,16 @@ void frame(void) { // do some testing on the bone with no parent Vec3 should_be_zero = MulM4V3(cur_pose_bone->parent_space_pose, V3(0,0,0)); - assert(should_be_zero.x == 0.0); + + // there is another bone, that's not at (0,0,0) in model space on the model + // for debugging purposes right now + assert(should_be_zero.y == 0.0); + + /* + assert(should_be_zero.x == 0.0); assert(should_be_zero.z == 0.0); + */ } // from, x, y, and z are like vertex points. They are model-space @@ -4677,25 +4759,35 @@ void frame(void) final_mat = MulM4(cur->parent_space_pose, final_mat); } + // uncommenting this skips the pose transform, showing the debug skeleton + // as if it were in "edit mode" in blender + //final_mat = M4D(1.0f); + from = MulM4V3(final_mat, from); x = MulM4V3(final_mat, x); y = MulM4V3(final_mat, y); z = MulM4V3(final_mat, z); + dot = MulM4V3(final_mat, dot); from = AddV3(from, offset); x = AddV3(x, offset); y = AddV3(y, offset); z = AddV3(z, offset); + dot = AddV3(dot, offset); dbgcol(LIGHTBLUE) dbgsquare3d(y); dbgcol(RED) dbg3dline(from, x); - dbgcol(BLUE) + dbgcol(GREEN) dbg3dline(from, y); - dbgcol(YELLOW) + dbgcol(BLUE) dbg3dline(from, z); + dbgcol(YELLOW) + dbg3dline(from, dot); + dbgcol(PINK) + dbgsquare3d(dot); cur_pose_bone = cur_pose_bone->next; } @@ -6334,7 +6426,7 @@ void frame(void) Vec2 pos = V2(0.0, screen_size().Y); int num_entities = 0; ENTITIES_ITER(gs.entities) num_entities++; - MD_String8 stats = tprint("Frametime: %.1f ms\nProcessing: %.1f ms\nGameplay processing: %.1f ms\nEntities: %d\nDraw calls: %d\nDrawn Vertices: %d\nProfiling: %s\nNumber gameplay processing loops: %d\nFlyecam: %s\n", dt*1000.0, last_frame_processing_time*1000.0, last_frame_gameplay_processing_time*1000.0, num_entities, num_draw_calls, num_vertices, profiling ? "yes" : "no", num_timestep_loops, flycam ? "yes" : "no"); + MD_String8 stats = tprint("Frametime: %.1f ms\nProcessing: %.1f ms\nGameplay processing: %.1f ms\nEntities: %d\nDraw calls: %d\nDrawn Vertices: %d\nProfiling: %s\nNumber gameplay processing loops: %d\nFlyecam: %s\nPlayer position: %f %f\n", dt*1000.0, last_frame_processing_time*1000.0, last_frame_gameplay_processing_time*1000.0, num_entities, num_draw_calls, num_vertices, profiling ? "yes" : "no", num_timestep_loops, flycam ? "yes" : "no", v2varg(gs.player->pos)); AABB bounds = draw_text((TextParams) { true, stats, pos, BLACK, 1.0f }); pos.Y -= bounds.upper_left.Y - screen_size().Y; bounds = draw_text((TextParams) { true, stats, pos, BLACK, 1.0f }); diff --git a/run_codegen.bat b/run_codegen.bat index 248d4e9..94b3f52 100644 --- a/run_codegen.bat +++ b/run_codegen.bat @@ -16,6 +16,7 @@ mkdir gen @REM shaders thirdparty\sokol-shdc.exe --input quad.glsl --output gen\quad-sapp.glsl.h --slang glsl100:hlsl5:metal_macos:glsl330 || goto :error thirdparty\sokol-shdc.exe --input threedee.glsl --output gen\threedee.glsl.h --slang glsl100:hlsl5:metal_macos:glsl330 || goto :error +thirdparty\sokol-shdc.exe --input armature.glsl --output gen\armature.glsl.h --slang glsl100:hlsl5:metal_macos:glsl330 || goto :error @REM metadesk codegen cl /Ithirdparty /W3 /Zi /WX codegen.c || goto :error