//------------------------------------------------------------------------------ // imgui-glfw // Demonstrates basic integration with Dear Imgui (without custom // texture or custom font support). //------------------------------------------------------------------------------ #include "backends/imgui_impl_glfw.h" #include "backends/imgui_impl_opengl3.h" #include "imgui.h" #include "imnodes.h" #define SOKOL_IMPL #define SOKOL_GLCORE33 #include "sokol_gfx.h" #include "sokol_log.h" #include "sokol_time.h" #define SOKOL_GL_IMPL #include "util/sokol_gl.h" #define GLFW_INCLUDE_NONE #include #include "3rdparty/imgui-node-editor/imgui_node_editor.h" #include "3rdparty/json/json.hpp" #include "3rdparty/nativefiledialog-extended/src/include/nfd.h" #include "AnimGraph/AnimGraphBlendTree.h" #include "AnimGraph/AnimGraphData.h" #include "AnimGraphEditor/AnimGraphEditor.h" #include "Camera.h" #include "GLFW/glfw3.h" #include "SkinnedMesh.h" #include "SkinnedMeshRenderer.h" #include "SkinnedMeshResource.h" #include "embedded_fonts.h" const int Width = 1024; const int Height = 768; const int MaxVertices = (1 << 16); const int MaxIndices = MaxVertices * 3; uint64_t last_time = 0; const int cMSAASampleCount = 8; sg_pass_action pass_action; sg_pipeline pip; sg_bindings bind; typedef struct { ImVec2 disp_size; } vs_params_t; static void draw_imgui(ImDrawData*); #define HANDMADE_MATH_NO_SSE #include "HandmadeMath.h" // ozz-animation headers #include // fmodf #include #include "ozz/animation/runtime/animation.h" #include "ozz/animation/runtime/sampling_job.h" #include "ozz/animation/runtime/skeleton.h" #include "ozz/base/containers/vector.h" #include "ozz/base/io/archive.h" #include "ozz/base/io/stream.h" #include "ozz/base/log.h" #include "ozz/base/maths/soa_transform.h" static struct { struct { ozz::animation::Animation* animation = nullptr; ozz::animation::SamplingJob sampling_job; ozz::vector local_matrices; } ozz; sg_pass_action pass_action = {}; Camera camera; struct { bool skeleton; bool animation; bool failed; } loaded; struct { double frame; double anim_update_time; double absolute; uint64_t laptime; float factor; float anim_ratio; bool anim_ratio_ui_override; bool paused; bool use_graph = false; } time; } state = {}; typedef struct { int32_t mousedX; int32_t mousedY; int32_t mouseX; int32_t mouseY; uint8_t mouseButton; int32_t mouseScroll; char key; } GuiInputState; GuiInputState gGuiInputState = {0, 0, 0, 0, 0, 0, 0}; enum class ControlMode { ControlModeNone, ControlModeFPS }; ControlMode gControlMode = ControlMode::ControlModeNone; struct Viewport { sg_pass_action pass_action = {}; sg_pass pass = {}; sg_pipeline pip = {}; sgl_pipeline glpip = {}; sg_bindings bind = {}; sg_image color_image = {}; sg_image depth_image = {}; int size[2] = {400, 400}; Viewport() { Resize(size[0], size[1]); } void Resize(int width, int height) { width = width < 1 ? 1 : width; height = height < 1 ? 1 : height; this->pass_action.colors[0].action = SG_ACTION_CLEAR; this->pass_action.colors[0].value = {0.1f, 0.1f, 0.1f, 1.0f}; if (this->size[0] == width && this->size[1] == height && this->pass.id != 0) { return; } this->size[0] = width; this->size[1] = height; if (this->pass.id != 0) { sg_destroy_pass(this->pass); sg_destroy_image(this->depth_image); sg_destroy_image(this->color_image); } sg_image_desc img_desc = { .render_target = true, .width = this->size[0], .height = this->size[1], .pixel_format = SG_PIXELFORMAT_RGBA8, .sample_count = cMSAASampleCount, .min_filter = SG_FILTER_LINEAR, .mag_filter = SG_FILTER_LINEAR, .wrap_u = SG_WRAP_REPEAT, .wrap_v = SG_WRAP_REPEAT, .label = "color-image"}; this->color_image = sg_make_image(&img_desc); img_desc.pixel_format = SG_PIXELFORMAT_DEPTH_STENCIL; img_desc.label = "depth-image"; this->depth_image = sg_make_image(&img_desc); sg_pass_desc offscreen_pass_desc{}; offscreen_pass_desc.color_attachments[0].image = this->color_image; offscreen_pass_desc.depth_stencil_attachment.image = this->depth_image; offscreen_pass_desc.label = "offscreen-pass"; this->pass = sg_make_pass(&offscreen_pass_desc); } }; struct ApplicationConfig { int window_position[2] = {100, 30}; int window_size[2] = {1000, 600}; struct GraphEditor { bool visible = false; int position[2] = {20, 20}; int size[2] = {800, 500}; ax::NodeEditor::Config config = {}; ax::NodeEditor::EditorContext* context = nullptr; }; GraphEditor graph_editor; struct SkinnedMeshWidget { bool visible = false; int position[2] = {20, 20}; int size[2] = {800, 500}; }; SkinnedMeshWidget skinned_mesh_widget; struct AnimationPlayerWidget { bool visible = false; int position[2] = {20, 20}; int size[2] = {800, 500}; }; AnimationPlayerWidget animation_player_widget; struct ViewportWidget { bool visible = true; int position[2] = {20, 20}; int size[2] = {800, 500}; }; ViewportWidget viewport_widget; bool show_imgui_demo_window = false; bool show_another_window = false; bool show_camera_widget = false; }; void to_json(nlohmann::json& j, const ApplicationConfig& config) { j["type"] = "AnimTestbedConfig"; j["main_window"]["position"][0] = config.window_position[0]; j["main_window"]["position"][1] = config.window_position[1]; j["main_window"]["size"][0] = config.window_size[0]; j["main_window"]["size"][1] = config.window_size[1]; j["graph_editor"]["visible"] = config.graph_editor.visible; j["graph_editor"]["position"][0] = config.graph_editor.position[0]; j["graph_editor"]["position"][1] = config.graph_editor.position[1]; j["graph_editor"]["size"][0] = config.graph_editor.size[0]; j["graph_editor"]["size"][1] = config.graph_editor.size[1]; j["skinned_mesh_widget"]["visible"] = config.skinned_mesh_widget.visible; j["skinned_mesh_widget"]["position"][0] = config.skinned_mesh_widget.position[0]; j["skinned_mesh_widget"]["position"][1] = config.skinned_mesh_widget.position[1]; j["skinned_mesh_widget"]["size"][0] = config.skinned_mesh_widget.size[0]; j["skinned_mesh_widget"]["size"][1] = config.skinned_mesh_widget.size[1]; j["animation_player_widget"]["visible"] = config.animation_player_widget.visible; j["animation_player_widget"]["position"][0] = config.animation_player_widget.position[0]; j["animation_player_widget"]["position"][1] = config.animation_player_widget.position[1]; j["animation_player_widget"]["size"][0] = config.animation_player_widget.size[0]; j["animation_player_widget"]["size"][1] = config.animation_player_widget.size[1]; j["viewport_widget"]["visible"] = config.viewport_widget.visible; j["viewport_widget"]["position"][0] = config.viewport_widget.position[0]; j["viewport_widget"]["position"][1] = config.viewport_widget.position[1]; j["viewport_widget"]["size"][0] = config.viewport_widget.size[0]; j["viewport_widget"]["size"][1] = config.viewport_widget.size[1]; j["camera_widget"]["visible"] = config.show_camera_widget; } void from_json(const nlohmann::json& j, ApplicationConfig& config) { if (j.contains("main_window")) { if (j["main_window"].contains("position") and j["main_window"]["position"].size() == 2) { config.window_position[0] = j["main_window"]["position"].at(0); config.window_position[1] = j["main_window"]["position"].at(1); } if (j["main_window"].contains("size") and j["main_window"]["size"].size() == 2) { config.window_size[0] = j["main_window"]["size"].at(0); config.window_size[1] = j["main_window"]["size"].at(1); } } if (j.contains("graph_editor")) { if (j["graph_editor"].contains("visible")) { config.graph_editor.visible = j["graph_editor"]["visible"]; } if (j["graph_editor"].contains("position") and j["graph_editor"]["position"].size() == 2) { config.graph_editor.position[0] = j["graph_editor"]["position"].at(0); config.graph_editor.position[1] = j["graph_editor"]["position"].at(1); } if (j["graph_editor"].contains("size") and j["graph_editor"]["size"].size() == 2) { config.graph_editor.size[0] = j["graph_editor"]["size"].at(0); config.graph_editor.size[1] = j["graph_editor"]["size"].at(1); } } if (j.contains("skinned_mesh_widget")) { if (j["skinned_mesh_widget"].contains("visible")) { config.skinned_mesh_widget.visible = j["skinned_mesh_widget"]["visible"]; } if (j["skinned_mesh_widget"].contains("position") and j["skinned_mesh_widget"]["position"].size() == 2) { config.skinned_mesh_widget.position[0] = j["skinned_mesh_widget"]["position"].at(0); config.skinned_mesh_widget.position[1] = j["skinned_mesh_widget"]["position"].at(1); } if (j["skinned_mesh_widget"].contains("size") and j["skinned_mesh_widget"]["size"].size() == 2) { config.skinned_mesh_widget.size[0] = j["skinned_mesh_widget"]["size"].at(0); config.skinned_mesh_widget.size[1] = j["skinned_mesh_widget"]["size"].at(1); } } if (j.contains("animation_player_widget")) { if (j["animation_player_widget"].contains("visible")) { config.animation_player_widget.visible = j["animation_player_widget"]["visible"]; } if (j["animation_player_widget"].contains("position") and j["animation_player_widget"]["position"].size() == 2) { config.animation_player_widget.position[0] = j["animation_player_widget"]["position"].at(0); config.animation_player_widget.position[1] = j["animation_player_widget"]["position"].at(1); } if (j["animation_player_widget"].contains("size") and j["animation_player_widget"]["size"].size() == 2) { config.animation_player_widget.size[0] = j["animation_player_widget"]["size"].at(0); config.animation_player_widget.size[1] = j["animation_player_widget"]["size"].at(1); } } if (j.contains("viewport_widget")) { if (j["viewport_widget"].contains("visible")) { config.viewport_widget.visible = j["viewport_widget"]["visible"]; } if (j["viewport_widget"].contains("position") and j["viewport_widget"]["position"].size() == 2) { config.viewport_widget.position[0] = j["viewport_widget"]["position"].at(0); config.viewport_widget.position[1] = j["viewport_widget"]["position"].at(1); } if (j["viewport_widget"].contains("size") and j["viewport_widget"]["size"].size() == 2) { config.viewport_widget.size[0] = j["viewport_widget"]["size"].at(0); config.viewport_widget.size[1] = j["viewport_widget"]["size"].at(1); } } if (j.contains("camera_widget") and j["camera_widget"].contains("visible")) { config.show_camera_widget = j["camera_widget"]["visible"]; } } ApplicationConfig gApplicationConfig; // io buffers for skeleton and animation data files, we know the max file size upfront static uint8_t skel_data_buffer[4 * 1024]; static uint8_t anim_data_buffer[32 * 1024]; static void draw_grid(); static void frame(); void handle_mouse(GLFWwindow* w, GuiInputState* io_input_state) { if (!glfwGetWindowAttrib(w, GLFW_FOCUSED)) { return; } double mouse_x, mouse_y; glfwGetCursorPos(w, &mouse_x, &mouse_y); if (io_input_state->mouseButton) { io_input_state->mousedX = int32_t(mouse_x) - io_input_state->mouseX; io_input_state->mousedY = int32_t(mouse_y) - io_input_state->mouseY; } else { io_input_state->mousedX = 0; io_input_state->mousedY = 0; } io_input_state->mouseX = int32_t(mouse_x); io_input_state->mouseY = int32_t(mouse_y); io_input_state->mouseButton = glfwGetMouseButton(w, 0) + (glfwGetMouseButton(w, 1) << 1) + (glfwGetMouseButton(w, 2) << 2); } void load_application_config(const char* filename) { if (std::filesystem::exists(filename)) { std::ifstream input_file; input_file.open(filename); std::stringstream buffer; buffer << input_file.rdbuf(); nlohmann::json application_config = nlohmann::json::parse(buffer.str(), nullptr, false); if (application_config.is_discarded()) { std::cerr << "Error parsing json of file '" << filename << "'." << std::endl; } if (application_config.value("type", "undefined") != "AnimTestbedConfig") { std::cerr << "Invalid json object. Expected type 'AnimTestbedConfig' but got '" << application_config["type"] << "'." << std::endl; application_config["type"] = "AnimTestbedConfig"; } gApplicationConfig = application_config.get(); } } void save_application_config(const char* filename) { std::ofstream output_file; nlohmann::json application_config = gApplicationConfig; output_file.open(filename); output_file << to_string(application_config) << std::endl; output_file.close(); } void sokol_logger( const char* tag, uint32_t log_level, uint32_t log_item_id, const char* message_or_null, // a message string, may be nullptr in release mode uint32_t line_nr, // line number in sokol_gl.h const char* filename_or_null, // source filename, may be nullptr in release mode void* user_data) { fprintf(stderr, "%s\n", message_or_null); } int main() { // window and GL context via GLFW and flextGL glfwInit(); const char* glsl_version = "#version 130"; glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3); glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3); glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GLFW_TRUE); glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE); glfwWindowHint(GLFW_COCOA_RETINA_FRAMEBUFFER, GLFW_FALSE); glfwWindowHint(GLFW_SAMPLES, cMSAASampleCount); GLFWwindow* window = glfwCreateWindow(Width, Height, "ATP Editor", nullptr, nullptr); glfwMakeContextCurrent(window); glfwSwapInterval(1); load_application_config("animtestbed_config.json"); if (gApplicationConfig.window_position[0] != 0 || gApplicationConfig.window_position[1] != 0) { glfwSetWindowPos( window, gApplicationConfig.window_position[0], gApplicationConfig.window_position[1]); } if (gApplicationConfig.window_size[0] != 0 || gApplicationConfig.window_size[1] != 0) { glfwSetWindowSize( window, gApplicationConfig.window_size[0], gApplicationConfig.window_size[1]); } NFD_Init(); // setup sokol_gfx and sokol_time stm_setup(); sg_desc desc = { .logger = {.func = slog_func}, .context{.sample_count = cMSAASampleCount}}; sg_setup(&desc); assert(sg_isvalid()); // setup sokol-gl sgl_desc_t sgldesc = { .sample_count = cMSAASampleCount, .logger = sokol_logger}; sgl_setup(&sgldesc); sgl_defaults(); // sgl_context_desc_t sgl_context_desc = {}; // sgl_context ctx = sgl_make_context(&sgl_context_desc); SkinnedMeshResource skinned_mesh_resource; skinned_mesh_resource.loadFromFile("../media/SampleSkinnedMesh.json"); SkinnedMesh skinned_mesh; skinned_mesh_resource.createInstance(skinned_mesh); skinned_mesh.SetCurrentAnimation(0); AnimGraphBlendTree anim_graph; AnimGraphContext anim_graph_context; Pose anim_graph_output; anim_graph_output.m_local_matrices.resize( skinned_mesh.m_skeleton.num_soa_joints()); AnimGraphEditorClear(); state.time.factor = 1.0f; Camera_Init(&state.camera); // setup Dear Imgui ImGui::CreateContext(); ImGui::StyleColorsDark(); ImGuiIO& io = ImGui::GetIO(); io.ConfigFlags |= ImGuiConfigFlags_DockingEnable; io.IniFilename = "ATPImgui.ini"; //io.Fonts->AddFontDefault(); ImFontConfig font_config; font_config.OversampleH = 4; font_config.OversampleV = 4; font_config.GlyphExtraSpacing.x = 1.0f; io.Fonts->AddFontFromMemoryCompressedTTF( // roboto_medium_ttf_compressed_data, // roboto_medium_ttf_compressed_size, droid_sans_ttf_compressed_data, droid_sans_ttf_compressed_size, 14, &font_config); ImGui_ImplGlfw_InitForOpenGL(window, true); ImGui_ImplOpenGL3_Init(glsl_version); // ImNodes ImNodes::CreateContext(); // dynamic vertex- and index-buffers for imgui-generated geometry sg_buffer_desc vbuf_desc = {}; vbuf_desc.usage = SG_USAGE_STREAM; vbuf_desc.size = MaxVertices * sizeof(ImDrawVert); bind.vertex_buffers[0] = sg_make_buffer(&vbuf_desc); sg_buffer_desc ibuf_desc = {}; ibuf_desc.type = SG_BUFFERTYPE_INDEXBUFFER; ibuf_desc.usage = SG_USAGE_STREAM; ibuf_desc.size = MaxIndices * sizeof(ImDrawIdx); bind.index_buffer = sg_make_buffer(&ibuf_desc); // font texture for imgui's default font unsigned char* font_pixels; int font_width, font_height; io.Fonts->GetTexDataAsRGBA32(&font_pixels, &font_width, &font_height); sg_image_desc img_desc = {}; img_desc.width = font_width; img_desc.height = font_height; img_desc.pixel_format = SG_PIXELFORMAT_RGBA8; img_desc.wrap_u = SG_WRAP_CLAMP_TO_EDGE; img_desc.wrap_v = SG_WRAP_CLAMP_TO_EDGE; img_desc.data.subimage[0][0] = sg_range{font_pixels, size_t(font_width * font_height * 4)}; bind.fs_images[0] = sg_make_image(&img_desc); // shader object for imgui rendering sg_shader_desc shd_desc = {}; auto& ub = shd_desc.vs.uniform_blocks[0]; ub.size = sizeof(vs_params_t); ub.uniforms[0].name = "disp_size"; ub.uniforms[0].type = SG_UNIFORMTYPE_FLOAT2; shd_desc.vs.source = "#version 330\n" "uniform vec2 disp_size;\n" "layout(location=0) in vec2 position;\n" "layout(location=1) in vec2 texcoord0;\n" "layout(location=2) in vec4 color0;\n" "out vec2 uv;\n" "out vec4 color;\n" "void main() {\n" " gl_Position = vec4(((position/disp_size)-0.5)*vec2(2.0,-2.0), 0.5, " "1.0);\n" " uv = texcoord0;\n" " color = color0;\n" "}\n"; shd_desc.fs.images[0].name = "tex"; shd_desc.fs.images[0].image_type = SG_IMAGETYPE_2D; shd_desc.fs.source = "#version 330\n" "uniform sampler2D tex;\n" "in vec2 uv;\n" "in vec4 color;\n" "out vec4 frag_color;\n" "void main() {\n" " frag_color = texture(tex, uv) * color;\n" "}\n"; sg_shader shd = sg_make_shader(&shd_desc); // pipeline object for imgui rendering sg_pipeline_desc pip_desc = {}; pip_desc.layout.buffers[0].stride = sizeof(ImDrawVert); auto& attrs = pip_desc.layout.attrs; attrs[0].format = SG_VERTEXFORMAT_FLOAT2; attrs[1].format = SG_VERTEXFORMAT_FLOAT2; attrs[2].format = SG_VERTEXFORMAT_UBYTE4N; pip_desc.shader = shd; pip_desc.index_type = SG_INDEXTYPE_UINT16; pip_desc.colors[0].blend.enabled = true; pip_desc.colors[0].blend.src_factor_rgb = SG_BLENDFACTOR_SRC_ALPHA; pip_desc.colors[0].blend.dst_factor_rgb = SG_BLENDFACTOR_ONE_MINUS_SRC_ALPHA; pip_desc.colors[0].write_mask = SG_COLORMASK_RGB; pip_desc.sample_count = cMSAASampleCount; pip_desc.label = "imgui-rendering"; pip = sg_make_pipeline(&pip_desc); // initial clear color pass_action.colors[0].action = SG_ACTION_CLEAR; pass_action.colors[0].value = {0.1f, 0.1f, 0.1f, 1.0f}; Viewport offscreen_viewport; // Graph Editor gApplicationConfig.graph_editor.config.SettingsFile = "graph_editor.json"; gApplicationConfig.graph_editor.config.NavigateButtonIndex = 2; gApplicationConfig.graph_editor.context = ax::NodeEditor::CreateEditor(&gApplicationConfig.graph_editor.config); // draw loop while (!glfwWindowShouldClose(window)) { // Update time state.time.frame = stm_sec( stm_round_to_common_refresh_rate(stm_laptime(&state.time.laptime))); if (!state.time.paused) { state.time.anim_update_time = state.time.frame; state.time.absolute += state.time.frame * state.time.factor; } else { state.time.anim_update_time = 0.; } if (state.ozz.animation != nullptr) { state.time.absolute = fmod(state.time.absolute, state.ozz.animation->duration()); } // Update window state glfwGetWindowPos( window, &gApplicationConfig.window_position[0], &gApplicationConfig.window_position[1]); glfwGetWindowSize( window, &gApplicationConfig.window_size[0], &gApplicationConfig.window_size[1]); int cur_width, cur_height; glfwGetFramebufferSize(window, &cur_width, &cur_height); // this is standard ImGui demo code io.DisplaySize = ImVec2(float(cur_width), float(cur_height)); io.DeltaTime = (float)stm_sec(stm_laptime(&last_time)); ImGui::NewFrame(); // handle input handle_mouse(window, &gGuiInputState); if (!glfwGetMouseButton(window, GLFW_MOUSE_BUTTON_RIGHT)) { gControlMode = ControlMode::ControlModeNone; glfwSetInputMode(window, GLFW_CURSOR, GLFW_CURSOR_NORMAL); Camera_Update( &state.camera, offscreen_viewport.size[0], offscreen_viewport.size[1], static_cast(state.time.frame), 0, 0, nullptr); } if (gControlMode == ControlMode::ControlModeFPS) { float camera_accel[3] = {0.f, 0.f, 0.f}; float accel_scale = 100.0; if (glfwGetKey(window, GLFW_KEY_LEFT_SHIFT)) { accel_scale *= 3.; } else if (glfwGetKey(window, GLFW_KEY_LEFT_CONTROL)) { accel_scale /= 3.; } if (glfwGetKey(window, GLFW_KEY_W)) { camera_accel[0] -= accel_scale; } if (glfwGetKey(window, GLFW_KEY_S)) { camera_accel[0] += accel_scale; } if (glfwGetKey(window, GLFW_KEY_C)) { camera_accel[1] -= accel_scale; } if (glfwGetKey(window, GLFW_KEY_SPACE)) { camera_accel[1] += accel_scale; } if (glfwGetKey(window, GLFW_KEY_A)) { camera_accel[2] -= accel_scale; } if (glfwGetKey(window, GLFW_KEY_D)) { camera_accel[2] += accel_scale; } Camera_Update( &state.camera, offscreen_viewport.size[0], offscreen_viewport.size[1], static_cast(state.time.frame), static_cast(gGuiInputState.mousedX), static_cast(gGuiInputState.mousedY), camera_accel); } ImGui::DockSpaceOverViewport(0, ImGui::GetMainViewport()); if (ImGui::BeginMainMenuBar()) { if (ImGui::BeginMenu("File")) { if (ImGui::MenuItem("Quit")) { glfwSetWindowShouldClose(window, true); } ImGui::EndMenu(); } if (ImGui::BeginMenu("View")) { ImGui::Checkbox( "Viewport", &gApplicationConfig.viewport_widget.visible); ImGui::Checkbox( "Graph Editor", &gApplicationConfig.graph_editor.visible); ImGui::Checkbox( "Camera Settings", &gApplicationConfig.show_camera_widget); ImGui::Checkbox( "Skinned Mesh", &gApplicationConfig.skinned_mesh_widget.visible); ImGui::Checkbox( "Animation Player", &gApplicationConfig.animation_player_widget.visible); ImGui::Separator(); ImGui::Checkbox( "ImGui Demo", &gApplicationConfig.show_imgui_demo_window); ImGui::EndMenu(); } if (ImGui::Button("Update Runtime Graph")) { anim_graph.dealloc(); AnimGraphEditorGetRuntimeBlendTree(anim_graph); anim_graph_context.m_skeleton = &skinned_mesh.m_skeleton; anim_graph.Init(anim_graph_context); // For simplicity use first animation data output const std::vector& graph_output_sockets = anim_graph.GetGraphOutputs(); for (const auto& output : graph_output_sockets) { if (output.m_type == SocketType::SocketTypeAnimation) { anim_graph.SetOutput(output.m_name.c_str(), &anim_graph_output); } } } ImGui::EndMainMenuBar(); } // Animation Runtime { // Viewport if (gApplicationConfig.viewport_widget.visible) { ImGui::SetNextWindowPos( ImVec2( static_cast( gApplicationConfig.viewport_widget.position[0]), static_cast( gApplicationConfig.viewport_widget.position[1])), ImGuiCond_FirstUseEver); ImGui::SetNextWindowSize( ImVec2( static_cast(gApplicationConfig.viewport_widget.size[0]), static_cast(gApplicationConfig.viewport_widget.size[1])), ImGuiCond_FirstUseEver); ImGui::Begin("Viewport", &gApplicationConfig.viewport_widget.visible); if (ImGui::IsWindowHovered() && ImGui::IsMouseDown(ImGuiMouseButton_Right)) { if (gControlMode == ControlMode::ControlModeNone) { gControlMode = ControlMode::ControlModeFPS; Camera_CalcFromMatrix(&state.camera, &state.camera.mtxView[0]); glfwSetInputMode(window, GLFW_CURSOR, GLFW_CURSOR_DISABLED); } } ImVec2 viewport_widget_size = ImGui::GetWindowSize(); gApplicationConfig.viewport_widget.size[0] = static_cast(viewport_widget_size.x); gApplicationConfig.viewport_widget.size[1] = static_cast(viewport_widget_size.y); ImGui::Text( "Viewport size: %d, %d", offscreen_viewport.size[0], offscreen_viewport.size[1]); ImVec2 content_size = ImGui::GetContentRegionAvail(); int* current_size = offscreen_viewport.size; if (static_cast(current_size[0]) != content_size[0] || static_cast(current_size[1]) != content_size[1] || offscreen_viewport.pass.id == 0) { offscreen_viewport.Resize( static_cast(content_size[0]), static_cast(content_size[1])); } ImGui::Image( (ImTextureID)(uintptr_t)offscreen_viewport.color_image.id, ImVec2( static_cast(offscreen_viewport.size[0]), static_cast(offscreen_viewport.size[1])), ImVec2(0.0f, 1.0f), ImVec2(1.0f, 0.0f)); ImGui::End(); } if (gApplicationConfig.show_camera_widget) { ImGui::SetNextWindowPos(ImVec2(600, 30), ImGuiCond_FirstUseEver); ImGui::SetNextWindowSize(ImVec2(300, 170), ImGuiCond_FirstUseEver); ImGui::Begin("Camera", &gApplicationConfig.show_camera_widget); ImGui::SliderFloat3("pos", state.camera.pos, -100.f, 100.f); ImGui::SliderFloat("near", &state.camera.near, 0.001f, 10.f); ImGui::SliderFloat("far", &state.camera.far, 1.0f, 10000.f); ImGui::SliderFloat("heading", &state.camera.heading, -180.0f, 180.f); ImGui::SliderFloat("pitch", &state.camera.pitch, -179.0f, 179.f); ImGui::End(); } if (gApplicationConfig.skinned_mesh_widget.visible) { ImGui::SetNextWindowPos( ImVec2( static_cast( gApplicationConfig.skinned_mesh_widget.position[0]), static_cast( gApplicationConfig.skinned_mesh_widget.position[1])), ImGuiCond_FirstUseEver); ImGui::SetNextWindowSize( ImVec2( static_cast( gApplicationConfig.skinned_mesh_widget.size[0]), static_cast( gApplicationConfig.skinned_mesh_widget.size[1])), ImGuiCond_FirstUseEver); ImGui::Begin( "SkinnedMesh", &gApplicationConfig.skinned_mesh_widget.visible); ImVec2 skinned_mesh_widget_position = ImGui::GetWindowPos(); gApplicationConfig.skinned_mesh_widget.position[0] = static_cast(skinned_mesh_widget_position.x); gApplicationConfig.skinned_mesh_widget.position[1] = static_cast(skinned_mesh_widget_position.y); ImVec2 skinned_mesh_widget_size = ImGui::GetWindowSize(); gApplicationConfig.skinned_mesh_widget.size[0] = static_cast(skinned_mesh_widget_size.x); gApplicationConfig.skinned_mesh_widget.size[1] = static_cast(skinned_mesh_widget_size.y); SkinnedMeshWidget(&skinned_mesh); ImGui::End(); } if (gApplicationConfig.animation_player_widget.visible) { ImGui::SetNextWindowPos( ImVec2( static_cast( gApplicationConfig.animation_player_widget.position[0]), static_cast( gApplicationConfig.animation_player_widget.position[1])), ImGuiCond_FirstUseEver); ImGui::SetNextWindowSize( ImVec2( static_cast( gApplicationConfig.animation_player_widget.size[0]), static_cast( gApplicationConfig.animation_player_widget.size[1])), ImGuiCond_FirstUseEver); ImGui::Begin( "Animation Player", &gApplicationConfig.animation_player_widget.visible); ImVec2 animation_player_widget_position = ImGui::GetWindowPos(); gApplicationConfig.animation_player_widget.position[0] = static_cast(animation_player_widget_position.x); gApplicationConfig.animation_player_widget.position[1] = static_cast(animation_player_widget_position.y); ImVec2 animation_player_widget_size = ImGui::GetWindowSize(); gApplicationConfig.animation_player_widget.size[0] = static_cast(animation_player_widget_size.x); gApplicationConfig.animation_player_widget.size[1] = static_cast(animation_player_widget_size.y); if (anim_graph.m_nodes.size() > 0) { ImGui::Checkbox("Use Graph", &state.time.use_graph); } else { state.time.use_graph = false; } if (!state.time.use_graph) { ImGui::Text("Animation"); const char* items[255] = {0}; static int selected = -1; for (int i = 0; i < skinned_mesh.m_animations.size(); i++) { items[i] = skinned_mesh.m_animation_names[i].c_str(); } if (ImGui::Combo( "Animation", &selected, items, static_cast(skinned_mesh.m_animations.size()))) { state.ozz.animation = skinned_mesh.m_animations[selected]; } } if (state.time.paused) { if (ImGui::Button("Play")) { state.time.paused = false; } } else { if (ImGui::Button("Pause")) { state.time.paused = true; } } ImGui::SameLine(); if (ImGui::Button("Step")) { state.time.anim_update_time = 1. / 30.f; state.time.absolute += state.time.anim_update_time; } if (state.ozz.animation != nullptr) { ImGui::SameLine(); float time_absolute_float = static_cast(state.time.absolute); if (ImGui::SliderFloat( "Time", &time_absolute_float, 0, state.ozz.animation->duration(), "%.3f", 0)) { state.time.absolute = time_absolute_float; } } ImGui::End(); } if (state.ozz.animation != nullptr) { state.ozz.sampling_job.animation = state.ozz.animation; state.ozz.sampling_job.ratio = static_cast(state.time.absolute) / state.ozz.animation->duration(); state.ozz.sampling_job.context = &skinned_mesh.m_sampling_context; state.ozz.sampling_job.output = ozz::make_span(skinned_mesh.m_local_matrices); if (!state.ozz.sampling_job.Run()) { ozz::log::Err() << "Error sampling animation." << std::endl; } // TODO: add AnimGraph to calculate pose skinned_mesh.CalcModelMatrices(); } if (state.time.use_graph && !anim_graph.m_nodes.empty() && state.time.anim_update_time > 0.) { // TODO: update for new API after embedding refactor // anim_graph.MarkActiveNodes(); // anim_graph.UpdateTime()pdateTime(state.time.anim_update_time); // anim_graph.evaluate(anim_graph_context); skinned_mesh.m_local_matrices = anim_graph_output.m_local_matrices; skinned_mesh.CalcModelMatrices(); } sgl_defaults(); sgl_matrix_mode_projection(); sgl_load_matrix((const float*)&state.camera.mtxProj); sgl_matrix_mode_modelview(); sgl_load_matrix((const float*)&state.camera.mtxView); draw_grid(); RenderSkinnedMesh(skinned_mesh); } // Animation Graph Editor if (gApplicationConfig.graph_editor.visible) { ImGui::SetNextWindowPos( ImVec2( gApplicationConfig.graph_editor.position[0], gApplicationConfig.graph_editor.position[1]), ImGuiCond_FirstUseEver); ImGui::SetNextWindowSize( ImVec2( gApplicationConfig.graph_editor.size[0], gApplicationConfig.graph_editor.size[1]), ImGuiCond_FirstUseEver); ImGui::Begin( "Graph Editor", &gApplicationConfig.graph_editor.visible, ImGuiWindowFlags_MenuBar); ImVec2 graph_editor_position = ImGui::GetWindowPos(); gApplicationConfig.graph_editor.position[0] = graph_editor_position.x; gApplicationConfig.graph_editor.position[1] = graph_editor_position.y; ImVec2 graph_editor_size = ImGui::GetWindowSize(); gApplicationConfig.graph_editor.size[0] = graph_editor_size.x; gApplicationConfig.graph_editor.size[1] = graph_editor_size.y; AnimGraphEditorUpdate(gApplicationConfig.graph_editor.context); ImGui::End(); } // 3. Show the ImGui test window. Most of the sample code is in ImGui::ShowDemoWindow() if (gApplicationConfig.show_imgui_demo_window) { ImGui::SetNextWindowPos(ImVec2(460, 20), ImGuiCond_FirstUseEver); ImGui::ShowDemoWindow(); } sg_begin_pass(offscreen_viewport.pass, &offscreen_viewport.pass_action); sgl_load_pipeline(offscreen_viewport.glpip); sgl_draw(); sg_end_pass(); // Rendering of the main gui sg_begin_default_pass(&pass_action, cur_width, cur_height); ImGui::Render(); draw_imgui(ImGui::GetDrawData()); sg_end_pass(); sg_commit(); glfwSwapBuffers(window); glfwPollEvents(); } save_application_config("animtestbed_config.json"); /* cleanup */ ax::NodeEditor::DestroyEditor(gApplicationConfig.graph_editor.context); ImNodes::DestroyContext(); ImGui_ImplOpenGL3_Shutdown(); ImGui_ImplGlfw_Shutdown(); ImGui::DestroyContext(); sgl_shutdown(); sg_shutdown(); NFD_Quit(); glfwTerminate(); return 0; } bool LoadSkeleton(const char* _filename, ozz::animation::Skeleton* _skeleton) { assert(_filename && _skeleton); ozz::log::Out() << "Loading skeleton archive " << _filename << "." << std::endl; ozz::io::File file(_filename, "rb"); if (!file.opened()) { ozz::log::Err() << "Failed to open skeleton file " << _filename << "." << std::endl; return false; } ozz::io::IArchive archive(&file); if (!archive.TestTag()) { ozz::log::Err() << "Failed to load skeleton instance from file " << _filename << "." << std::endl; return false; } // Once the tag is validated, reading cannot fail. archive >> *_skeleton; return true; } bool LoadAnimation( const char* _filename, ozz::animation::Animation* _animation) { assert(_filename && _animation); ozz::log::Out() << "Loading animation archive: " << _filename << "." << std::endl; ozz::io::File file(_filename, "rb"); if (!file.opened()) { ozz::log::Err() << "Failed to open animation file " << _filename << "." << std::endl; return false; } ozz::io::IArchive archive(&file); if (!archive.TestTag()) { ozz::log::Err() << "Failed to load animation instance from file " << _filename << "." << std::endl; return false; } // Once the tag is validated, reading cannot fail. archive >> *_animation; return true; } static void draw_vec(const ozz::math::SimdFloat4& vec) { sgl_v3f(ozz::math::GetX(vec), ozz::math::GetY(vec), ozz::math::GetZ(vec)); } static void draw_line( const ozz::math::SimdFloat4& v0, const ozz::math::SimdFloat4& v1) { draw_vec(v0); draw_vec(v1); } static void draw_grid(void) { sgl_defaults(); sgl_matrix_mode_projection(); sgl_load_matrix((const float*)&state.camera.mtxProj); sgl_matrix_mode_modelview(); sgl_load_matrix((const float*)&state.camera.mtxView); const int grid_size = 10; sgl_begin_lines(); sgl_c3f(0.4f, 0.4f, 0.4f); for (int i = -grid_size; i <= grid_size; i++) { if (i == 0) { continue; } ozz::math::SimdFloat4 p0 = ozz::math::simd_float4::Load(i * 1.0f, 0.f, -grid_size * 1.0f, 1.f); ozz::math::SimdFloat4 p1 = ozz::math::simd_float4::Load(i * 1.0f, 0.f, grid_size * 1.0f, 1.f); draw_line(p0, p1); p0 = ozz::math::simd_float4::Load(-grid_size * 1.0f, 0.f, i * 1.0f, 1.f); p1 = ozz::math::simd_float4::Load(grid_size * 1.0f, 0.f, i * 1.0f, 1.f); draw_line(p0, p1); } sgl_c3f(0.7f, 0.4f, 0.2f); ozz::math::SimdFloat4 p0 = ozz::math::simd_float4::Load(0, 0.f, -grid_size * 1.0f, 1.f); ozz::math::SimdFloat4 p1 = ozz::math::simd_float4::Load(0, 0.f, grid_size * 1.0f, 1.f); draw_line(p0, p1); sgl_c3f(0.2f, 0.4f, 0.7f); p0 = ozz::math::simd_float4::Load(-grid_size * 1.0f, 0.f, 0.f, 1.f); p1 = ozz::math::simd_float4::Load(grid_size * 1.0f, 0.f, 0.f, 1.f); draw_line(p0, p1); sgl_end(); } // draw ImGui draw lists via sokol-gfx void draw_imgui(ImDrawData* draw_data) { assert(draw_data); if (draw_data->CmdListsCount == 0) { return; } sg_image default_image = bind.fs_images[0]; // render the command list sg_apply_pipeline(pip); vs_params_t vs_params; vs_params.disp_size.x = ImGui::GetIO().DisplaySize.x; vs_params.disp_size.y = ImGui::GetIO().DisplaySize.y; sg_apply_uniforms(SG_SHADERSTAGE_VS, 0, SG_RANGE(vs_params)); for (int cl_index = 0; cl_index < draw_data->CmdListsCount; cl_index++) { const ImDrawList* cl = draw_data->CmdLists[cl_index]; // append vertices and indices to buffers, record start offsets in resource binding struct const uint32_t vtx_size = cl->VtxBuffer.size() * sizeof(ImDrawVert); const uint32_t idx_size = cl->IdxBuffer.size() * sizeof(ImDrawIdx); const uint32_t vb_offset = sg_append_buffer( bind.vertex_buffers[0], {&cl->VtxBuffer.front(), vtx_size}); const uint32_t ib_offset = sg_append_buffer(bind.index_buffer, {&cl->IdxBuffer.front(), idx_size}); /* don't render anything if the buffer is in overflow state (this is also checked internally in sokol_gfx, draw calls that attempt from overflowed buffers will be silently dropped) */ if (sg_query_buffer_overflow(bind.vertex_buffers[0]) || sg_query_buffer_overflow(bind.index_buffer)) { continue; } bind.vertex_buffer_offsets[0] = vb_offset; bind.index_buffer_offset = ib_offset; sg_apply_bindings(&bind); int base_element = 0; for (const ImDrawCmd& pcmd : cl->CmdBuffer) { if (pcmd.UserCallback) { pcmd.UserCallback(cl, &pcmd); } else { uint32_t prev_fs_image_id = bind.fs_images[0].id; if (pcmd.TextureId != 0) { bind.fs_images[0].id = (uint32_t)(uintptr_t)pcmd.TextureId; sg_apply_bindings(&bind); } const int scissor_x = (int)(pcmd.ClipRect.x); const int scissor_y = (int)(pcmd.ClipRect.y); const int scissor_w = (int)(pcmd.ClipRect.z - pcmd.ClipRect.x); const int scissor_h = (int)(pcmd.ClipRect.w - pcmd.ClipRect.y); sg_apply_scissor_rect(scissor_x, scissor_y, scissor_w, scissor_h, true); sg_draw(base_element, pcmd.ElemCount, 1); if (pcmd.TextureId != 0) { bind.fs_images[0].id = prev_fs_image_id; sg_apply_bindings(&bind); } } base_element += pcmd.ElemCount; } } }