diff --git a/extension/doc_classes/ModelSingleton.xml b/extension/doc_classes/ModelSingleton.xml index 74c669af..a6889019 100644 --- a/extension/doc_classes/ModelSingleton.xml +++ b/extension/doc_classes/ModelSingleton.xml @@ -35,5 +35,19 @@ + + + + + Gets the animation from an internal cache or file system using the provided file path. + + + + + + + Gets the animation from an internal cache or file system using the provided file path. + + diff --git a/extension/src/openvic-extension/singletons/ModelSingleton.cpp b/extension/src/openvic-extension/singletons/ModelSingleton.cpp index 5b2fa8b8..2cae1804 100644 --- a/extension/src/openvic-extension/singletons/ModelSingleton.cpp +++ b/extension/src/openvic-extension/singletons/ModelSingleton.cpp @@ -1,5 +1,6 @@ #include "ModelSingleton.hpp" +#include #include #include @@ -9,6 +10,7 @@ #include "openvic-extension/singletons/GameSingleton.hpp" #include "openvic-extension/utility/ClassBindings.hpp" #include "openvic-extension/utility/Utilities.hpp" +#include "godot_cpp/classes/node3d.hpp" using namespace godot; using namespace OpenVic; @@ -19,6 +21,8 @@ void ModelSingleton::_bind_methods() { OV_BIND_METHOD(ModelSingleton::get_cultural_helmet_model, { "culture" }); OV_BIND_METHOD(ModelSingleton::get_flag_model, { "floating" }); OV_BIND_METHOD(ModelSingleton::get_buildings); + OV_BIND_METHOD(ModelSingleton::get_xsm_animation,{ "animation_name" }); + OV_BIND_METHOD(ModelSingleton::get_xac_model,{ "model_name" }); } ModelSingleton* ModelSingleton::get_singleton() { @@ -481,3 +485,39 @@ TypedArray ModelSingleton::get_buildings() { return ret; } + +Ref ModelSingleton::get_xsm_animation(String source_file) { + const xsm_map_t::const_iterator it = xsm_cache.find(source_file); + if(it != xsm_cache.end()) { + return it->second; + } + + GameSingleton const* game_singleton = GameSingleton::get_singleton(); + ERR_FAIL_NULL_V(game_singleton, {}); + + String path = game_singleton->lookup_file_path(source_file); + + //Logger::info("Load XSM Animation from file: ",Utilities::godot_to_std_string(source_file)); + + Ref anim = _load_xsm_animation(FileAccess::open(path, FileAccess::READ)); + xsm_cache.emplace(source_file,anim); + return anim; +} + +Node3D* ModelSingleton::get_xac_model(String source_file) { + const xac_map_t::const_iterator it = xac_cache.find(source_file); + if(it != xac_cache.end()) { + return (Node3D*)it->second->duplicate(); + } + + GameSingleton const* game_singleton = GameSingleton::get_singleton(); + ERR_FAIL_NULL_V(game_singleton, {}); + + String path = game_singleton->lookup_file_path(source_file); + + Logger::info("Load XAC Model from file: ",Utilities::godot_to_std_string(source_file)); + + Node3D* node = _load_xac_model(FileAccess::open(path, FileAccess::READ)); + xac_cache.emplace(source_file,node); + return node; +} \ No newline at end of file diff --git a/extension/src/openvic-extension/singletons/ModelSingleton.hpp b/extension/src/openvic-extension/singletons/ModelSingleton.hpp index ebec7504..f0598e10 100644 --- a/extension/src/openvic-extension/singletons/ModelSingleton.hpp +++ b/extension/src/openvic-extension/singletons/ModelSingleton.hpp @@ -1,10 +1,15 @@ #pragma once +#include +#include #include #include #include #include +#include "../utility/XSMLoader.hpp" +#include "../utility/XACLoader.hpp" +#include "godot_cpp/classes/node3d.hpp" namespace OpenVic { struct BuildingInstance; @@ -31,9 +36,13 @@ namespace OpenVic { using animation_map_t = deque_ordered_map; using model_map_t = deque_ordered_map; + using xsm_map_t = deque_ordered_map>; + using xac_map_t = deque_ordered_map; animation_map_t animation_cache; model_map_t model_cache; + xsm_map_t xsm_cache; + xac_map_t xac_cache; godot::Dictionary get_animation_dict(GFX::Actor::Animation const& animation); godot::Dictionary get_model_dict(GFX::Actor const& actor); @@ -56,5 +65,8 @@ namespace OpenVic { godot::Dictionary get_flag_model(bool floating); godot::TypedArray get_buildings(); + + godot::Ref get_xsm_animation(godot::String source_file); + godot::Node3D* get_xac_model(godot::String source_file); }; } diff --git a/extension/src/openvic-extension/utility/XACLoader.cpp b/extension/src/openvic-extension/utility/XACLoader.cpp new file mode 100644 index 00000000..e69de29b diff --git a/extension/src/openvic-extension/utility/XACLoader.hpp b/extension/src/openvic-extension/utility/XACLoader.hpp new file mode 100644 index 00000000..6dc824a8 --- /dev/null +++ b/extension/src/openvic-extension/utility/XACLoader.hpp @@ -0,0 +1,1026 @@ +#pragma once +#include +//#include +#include +#include +//#include +#include +#include "Utilities.hpp" +#include "XACUtilities.hpp" +//#include "godot_cpp/classes/animation.hpp" +#include "godot_cpp/classes/array_mesh.hpp" +#include "godot_cpp/classes/image.hpp" +#include "godot_cpp/classes/image_texture.hpp" +#include "godot_cpp/classes/material.hpp" +//#include "godot_cpp/classes/node.hpp" +#include +#include "godot_cpp/classes/mesh_instance3d.hpp" +#include "godot_cpp/classes/resource_loader.hpp" +#include "godot_cpp/classes/resource_preloader.hpp" +#include "godot_cpp/classes/shader_material.hpp" +#include "godot_cpp/classes/skeleton3d.hpp" +//#include "godot_cpp/variant/basis.hpp" +//#include "godot_cpp/variant/dictionary.hpp" +#include "godot_cpp/variant/dictionary.hpp" +#include "godot_cpp/variant/packed_byte_array.hpp" +#include "godot_cpp/variant/packed_float32_array.hpp" +#include "godot_cpp/variant/packed_int32_array.hpp" +#include "godot_cpp/variant/packed_int64_array.hpp" +#include "godot_cpp/variant/packed_string_array.hpp" +#include "godot_cpp/variant/packed_vector2_array.hpp" +#include "godot_cpp/variant/packed_vector3_array.hpp" +#include "godot_cpp/variant/packed_vector4_array.hpp" +#include "godot_cpp/variant/transform3d.hpp" +#include "godot_cpp/variant/typed_array.hpp" +#include "godot_cpp/variant/variant.hpp" +#include "godot_cpp/variant/vector3.hpp" +#include "openvic-simulation/utility/Logger.hpp" + +#include +#include +#include +#include + +//#include "openvic-extension/utility/Utilities.hpp" + +namespace OpenVic { + + //using OpenVic::Utilities::std_view_to_godot_string; + + static constexpr uint32_t XAC_FORMAT_SPECIFIER = ' CAX'; // Order reversed due to little endian + static constexpr uint8_t XAC_VERSION_MAJOR = 1, XAC_VERSION_MINOR = 0; + + #pragma pack(push) + #pragma pack(1) + + struct xac_header_t { + uint32_t format_identifier; + uint8_t version_major; + uint8_t version_minor; + uint8_t big_endian; + uint8_t multiply_order; + }; + + struct xac_metadata_v2_pack_t { + uint32_t reposition_mask; //1=position, 2=rotation, 4=scale + int32_t repositioning_node; + uint8_t exporter_major_version; + uint8_t exporter_minor_version; + uint16_t pad; + float retarget_root_offset; + }; + + struct node_hierarchy_pack_t { //v1 + int32_t node_count; + int32_t root_node_count; //nodes with parent_id == -1 + }; + + struct node_data_pack_t { //v1 + quat_v1_t rotation; + quat_v1_t scale_rotation; + vec3d_t position; + vec3d_t scale; + float unused[3]; + int32_t unknown[2]; + int32_t parent_node_id; + int32_t child_nodes_count; + int32_t include_in_bounds_calculation; //bool + matrix44_t transform; + float importance_factor; + }; + + struct material_totals_t { //v1 + int32_t total_materials_count; + int32_t standard_materials_count; + int32_t fx_materials_count; + }; + + struct material_definition_pack_t { + vec4d_t ambient_color; + vec4d_t diffuse_color; + vec4d_t specular_color; + vec4d_t emissive_color; + float shine; + float shine_strength; + float opacity; + float ior; //index of refraction + uint8_t double_sided; //bool + uint8_t wireframe; //bool + uint8_t unused; //in v1, unknown, but used + uint8_t layers_count; //in v1, unknown, but used + }; + + struct material_layer_pack_t { //also chunk 0x4, v2 + float amount; + vec2d_t uv_offset; + vec2d_t uv_tiling; + float rotation_in_radians; + int16_t material_id; + uint8_t map_type; // (1-5) enum MAP_TYPE + uint8_t unused; + }; + + struct mesh_pack_t { + int32_t node_id; + int32_t influence_ranges_count; + int32_t vertices_count; + int32_t indices_count; + int32_t submeshes_count; + int32_t attribute_layers_count; + uint8_t is_collision_mesh; //bool + uint8_t pad[3]; + }; + + struct vertices_attribute_pack_t { + int32_t type; //0-6 (enum ATTRIBUTE) + int32_t attribute_size; + uint8_t keep_originals; //bool + uint8_t is_scale_factor; //bool + uint16_t pad; + }; + + struct submesh_pack_t { + int32_t indices_count; + int32_t vertices_count; + int32_t material_id; + int32_t bones_count; + }; + + struct skinning_pack_t { + int32_t influences_count; + uint8_t is_for_collision; //bool + uint8_t pad[3]; + }; + + struct influence_data_t { + float weight; + int16_t bone_id; + int16_t pad; + }; + + struct influence_range_t { + int32_t first_influence_index; + int32_t influences_count; + }; + + // 0x4, 0x6, 0xA chunk types appear in vic2, but what they do + // is unknown + // 0x8 is junk data + // 0x0 is the older node/bone chunk + + //0x0, v3 + struct node_chunk_pack_t { + quat_v1_t rotation; + quat_v1_t scale_rotation; + vec3d_t position; + vec3d_t scale; + vec3d_t unused; //-1, -1, -1 + int32_t unknown; //-1 (0xFFFF) + int32_t parent_node_id; + float uncertain[17]; //likely a matrix44 + fimportancefactor + }; + + /* + 0x6, v? unknown + 12x int32? + 09x float? + */ + + #pragma pack(pop) + + struct MAP_TYPE { + enum Values : int32_t { + DIFFUSE = 2, + SPECULAR, + SHADOW, + NORMAL + }; + }; + + struct ATTRIBUTE { + enum Values : int32_t { + POSITION, + NORMAL, + TANGENT, + UV, + COL_32, + INFLUENCE_RANGE, + COL_128 + }; + }; + + struct xac_metadata_v2_t { + xac_metadata_v2_pack_t packed = {}; + String source_app; + String original_file_name; + String export_date; + String actor_name; + }; + + struct node_data_t { //v1 + node_data_pack_t packed = {}; + String name; + }; + + struct node_hierarchy_t { //v1 + node_hierarchy_pack_t packed = {}; + std::vector node_data; + }; + + struct material_layer_t { + material_layer_pack_t packed = {}; + String texture; + }; + + struct material_definition_t { + material_definition_pack_t packed = {}; + String name; + std::vector layers; + }; + + struct vertices_attribute_t { + vertices_attribute_pack_t packed = {}; + std::vector data; + }; + + struct submesh_t { + submesh_pack_t packed = {}; + std::vector relative_indices; + std::vector bone_ids; + }; + + struct mesh_t { + mesh_pack_t packed = {}; + std::vector vertices_attributes; + std::vector submeshes; + }; + + struct skinning_t { + int32_t node_id = 0; + int32_t local_bones_count = -1; //v3 only + skinning_pack_t packed = {}; + std::vector influence_data; + std::vector influence_ranges; + }; + + struct node_chunk_t { + node_chunk_pack_t packed = {}; + String name; + }; + + static bool read_xac_header(Ref const& file) { + xac_header_t header = {}; + ERR_FAIL_COND_V(!read_struct(file, header), false); + + ERR_FAIL_COND_V_MSG( + header.format_identifier != XAC_FORMAT_SPECIFIER, false, vformat( + "Invalid XAC format identifier: %x (should be %x)", header.format_identifier, XAC_FORMAT_SPECIFIER + ) + ); + + ERR_FAIL_COND_V_MSG( + header.version_major != XAC_VERSION_MAJOR || header.version_minor != XAC_VERSION_MINOR, false, vformat( + "Invalid XAC version: %d.%d (should be %d.%d)", + header.version_major, header.version_minor, XAC_VERSION_MAJOR, XAC_VERSION_MINOR + ) + ); + + ERR_FAIL_COND_V_MSG( + header.big_endian != 0, false, "Invalid XAC endianness: big endian (only little endian is supported)" + ); + + /*ERR_FAIL_COND_V_MSG( + header.multiply_order != 0, false, "Invalid XAC multiply order: ???" + );*/ + + return true; + } + + static bool read_xac_metadata(Ref const& file, xac_metadata_v2_t& metadata) { + bool ret = read_struct(file, metadata.packed); + ret &= read_string(file, metadata.source_app, false); + ret &= read_string(file, metadata.original_file_name, false); + ret &= read_string(file, metadata.export_date, false); + ret &= read_string(file, metadata.actor_name, false); + return ret; + } + + static bool read_node_data(Ref const& file, node_data_t& node_data) { + bool ret = read_struct(file, node_data.packed); + ret &= read_string(file, node_data.name); + return ret; + } + + static bool read_node_hierarchy(Ref const& file, node_hierarchy_t& hierarchy) { + bool ret = read_struct(file, hierarchy.packed); + for(int32_t i=0; i const& file, material_totals_t& totals) { + return read_struct(file, totals); + } + + static bool read_layer(Ref const& file, material_layer_t& layer) { + bool ret = true; + ret &= read_struct(file, layer.packed); + ret &= read_string(file, layer.texture, false); + //Logger::info(Utilities::godot_to_std_string(layer.texture)); + return ret; + } + + static bool read_material_definition(Ref const& file, material_definition_t& def, int32_t version) { + bool ret = read_struct(file, def.packed); + ret &= read_string(file, def.name, false); + if (version > 1) { //in version 1, the layers are defined in separate chunks of type 0x4 + for(int i=0; i const& file, vertices_attribute_t& attribute, int32_t vertices_count) { + bool ret = read_struct(file, attribute.packed); + ret &= read_struct_array(file, attribute.data, vertices_count*attribute.packed.attribute_size); + return ret; + } + + static bool read_submesh(Ref const& file, submesh_t& submesh) { + bool ret = read_struct(file, submesh.packed); + ret &= read_struct_array(file, submesh.relative_indices, submesh.packed.indices_count); + ret &= read_struct_array(file, submesh.bone_ids, submesh.packed.bones_count); + return ret; + } + + static bool read_mesh(Ref const& file, mesh_t& mesh) { + bool ret = read_struct(file, mesh.packed); + for(int i=0; i const& file, skinning_t& skin, std::vector const& meshes, int32_t version) { + bool ret = read_struct(file, skin.node_id); + if (version == 3) { + ret &= read_struct(file, skin.local_bones_count); + } + ret &= read_struct(file, skin.packed); + ret &= read_struct_array(file, skin.influence_data, skin.packed.influences_count); + bool found = false; + for(mesh_t const& mesh : meshes) { + if (mesh.packed.is_collision_mesh == skin.packed.is_for_collision && mesh.packed.node_id == skin.node_id) { + ret &= read_struct_array(file, skin.influence_ranges, mesh.packed.influence_ranges_count); + found = true; + break; + } + } + ret &= found; + return ret; + } + + static bool read_node_chunk(Ref const& file, node_chunk_t& node) { + bool ret = read_struct(file, node.packed); + ret &= read_string(file, node.name); + //Logger::info(Utilities::godot_to_std_string(node.name)); + return ret; + } + + /* + ==================================== + Skeleton helper functions + + ==================================== + */ + + //TODO: Verify + static Transform3D make_transform(vec3d_t const& position, quat_v1_t const& quaternion, vec3d_t const& scale) { + Transform3D transform = Transform3D(); + + Basis basis = Basis(); + basis.set_quaternion(quat_v1_to_godot(quaternion)); + basis.scale(vec3d_to_godot(scale)); + + transform.set_basis(basis); + transform.translate_local(vec3d_to_godot(position, true)); + + return transform; + } + + //TODO: do we want to use node's bone id instead of current_id? + static Skeleton3D* build_armature_hierarchy(node_hierarchy_t const& hierarchy) { + static const StringName skeleton_name = "skeleton"; + Skeleton3D* skeleton = memnew(Skeleton3D); + skeleton->set_name(skeleton_name); + + uint32_t current_id = 0; + + for(node_data_t const node : hierarchy.node_data) { + skeleton->add_bone(node.name); + skeleton->set_bone_parent(current_id, node.packed.parent_node_id); + + Transform3D transform = make_transform(node.packed.position, node.packed.rotation, node.packed.scale); + skeleton->set_bone_rest(current_id, transform); + skeleton->set_bone_pose(current_id, transform); + + current_id += 1; + } + + return skeleton; + } + + static Skeleton3D* build_armature_nodes(std::vector const& nodes) { + static const StringName skeleton_name = "skeleton"; + Skeleton3D* skeleton = memnew(Skeleton3D); + skeleton->set_name(skeleton_name); + + uint32_t current_id = 0; + + for(node_chunk_t const node : nodes) { + skeleton->add_bone(node.name); + skeleton->set_bone_parent(current_id, node.packed.parent_node_id); + + Transform3D transform = make_transform(node.packed.position, node.packed.rotation, node.packed.scale); + skeleton->set_bone_rest(current_id, transform); + skeleton->set_bone_pose(current_id, transform); + + current_id += 1; + } + + return skeleton; + } + + /* + ==================================== + Material helper functions + + TODO: wasn't there an alpha mode (alpha clip, ...) needed + to fix the textures + + ==================================== + */ + + struct material_mapping { + //-1 means unused + Ref godot_material; + int32_t diffuse_texture_index = -1; + int32_t specular_texture_index = -1; + int32_t scroll_index = -1; + }; + + static Error setup_flag_shader() { + Error result = OK; + GameSingleton const* game_singleton = GameSingleton::get_singleton(); + ERR_FAIL_NULL_V(game_singleton, {}); + + static const StringName Param_flag_dimensions = "flag_dims"; + static const StringName Param_flag_texture_sheet = "texture_flag_sheet_diffuse"; + static const Ref flag_shader = ResourcePreloader().get_resource("res://src/Game/Model/flag_mat.tres"); + + flag_shader->set_shader_parameter(Param_flag_dimensions, game_singleton->get_flag_dims()); + flag_shader->set_shader_parameter(Param_flag_texture_sheet, game_singleton->get_flag_sheet_texture()); + return result; + } + + static std::vector build_materials(std::vector const& materials) { + + static const StringName Textures_path = "gfx/anims/%s.dds"; + + // Parameters for the default model shader + static const StringName Param_texture_diffuse = "texture_diffuse"; + //red channel is specular, green and blue are nation colours + static const StringName Param_texture_nation_colors_mask = "texture_nation_colors_mask"; + + // Scrolling textures (smoke, tank tracks) + static const StringName Param_Scroll_texture_diffuse = "scroll_texture_diffuse"; + static const StringName Param_Scroll_factor = "scroll_factor"; + static Dictionary SCROLLING_MATERIAL_FACTORS; + SCROLLING_MATERIAL_FACTORS["TexAnim"] = 2.5; + SCROLLING_MATERIAL_FACTORS["Smoke"] = 0.3; + + static PackedStringArray Scrolling_textures_diffuse; + static PackedStringArray unit_textures_diffuse; + static PackedStringArray unit_textures_specular; + + // Flag textures + + static const StringName Param_texture_normal = "texture_normal"; + + //General + static const uint32_t MAX_UNIT_TEXTURES = 32; + + static const StringName Texture_skip_nospec = "nospec"; + static const StringName Texture_skip_flag = "unionjacksquare"; + static const StringName Texture_skip_diff = "test256texture"; + + ResourceLoader* loader = ResourceLoader::get_singleton(); + + static const Ref unit_shader = loader->load("res://src/Game/Model/unit_colours_mat.tres"); + static const Ref scrolling_shader = loader->load("res://src/Game/Model/scrolling_mat.tres"); + static const Ref flag_shader = loader->load("res://src/Game/Model/flag_mat.tres"); + + + std::vector mappings; + + AssetManager* asset_manager = AssetManager::get_singleton(); + ERR_FAIL_NULL_V(asset_manager, {}); + + // *** Collect the textures *** + + for(material_definition_t const& mat : materials) { + String diffuse_name; + String specular_name; + String normal_name; + + for(material_layer_t const& layer : mat.layers) { + if (layer.texture == Texture_skip_diff || layer.texture == Texture_skip_flag || layer.texture == Texture_skip_nospec) { + continue; + } + //Get the texture names + switch(layer.packed.map_type) { + case MAP_TYPE::DIFFUSE: + if (diffuse_name.is_empty()) { + diffuse_name = layer.texture; + } + else { + Logger::error("Multiple diffuse layers in material: ", Utilities::godot_to_std_string(diffuse_name), " and ", Utilities::godot_to_std_string(layer.texture)); + } + break; + case MAP_TYPE::SPECULAR: + if (specular_name.is_empty()) { + specular_name = layer.texture; + } + else { + Logger::error("Multiple specular layers in material: ", Utilities::godot_to_std_string(specular_name), " and ", Utilities::godot_to_std_string(layer.texture)); + } + break; + case MAP_TYPE::NORMAL: + if (normal_name.is_empty()) { + normal_name = layer.texture; + } + else { + Logger::error("Multiple normal layers in material: ", Utilities::godot_to_std_string(normal_name), " and ", Utilities::godot_to_std_string(layer.texture)); + } + break; + case MAP_TYPE::SHADOW: + //Layer type 4 (Fort_Early_Shadow.dds) is used by Fort_Early.xac + // it corresponds to the blob shadow? + // for now, skip + break; + default: + //Logger::warning("Unknown layer type: ", layer.packed.map_type); + UtilityFunctions::print( + vformat("Unknown layer type: %x", layer.packed.map_type) + ); + break; + } + } + + Ref diffuse_texture; + Ref specular_texture; + Ref normal_texture; + material_mapping mapping; + + if (!diffuse_name.is_empty()) { + diffuse_texture = asset_manager->get_texture(vformat(Textures_path, diffuse_name)); + } + if (!specular_name.is_empty()) { + specular_texture = asset_manager->get_texture(vformat(Textures_path, specular_name)); + } + if (!normal_name.is_empty()) { + normal_texture = asset_manager->get_texture(vformat(Textures_path, normal_name)); + } + + // *** Determine the correct material to use, and set it up *** + + //flag TODO: perhaps flag should be determined by hard-coding so that other models can have a normal texture? + if (!normal_texture.is_null() && diffuse_texture.is_null()) { + //There shouldn't be a specular texture + flag_shader->set_shader_parameter(Param_texture_normal, normal_texture); + mapping.godot_material = flag_shader; + } + //Scrolling texture + else if (!diffuse_texture.is_null() && SCROLLING_MATERIAL_FACTORS.has(mat.name)) { + int32_t scroll_textures_index_diffuse = Scrolling_textures_diffuse.find(diffuse_name); + if (scroll_textures_index_diffuse < 0) { + Scrolling_textures_diffuse.push_back(diffuse_name); + //err check + TypedArray scroll_diffuse_textures = scrolling_shader->get_shader_parameter(Param_Scroll_texture_diffuse); + scroll_diffuse_textures.push_back(diffuse_texture); + scrolling_shader->set_shader_parameter(Param_Scroll_texture_diffuse, scroll_diffuse_textures); + + PackedFloat32Array scroll_factors = scrolling_shader->get_shader_parameter(Param_Scroll_factor); + + scroll_factors.push_back(SCROLLING_MATERIAL_FACTORS[mat.name]); + scrolling_shader->set_shader_parameter(Param_Scroll_factor, scroll_factors); + } + mapping.godot_material = scrolling_shader; + mapping.scroll_index = scroll_textures_index_diffuse; + + } + //standard material (diffuse optionally with a specular/unit colours) + else { + int32_t textures_index_diffuse = unit_textures_diffuse.find(diffuse_name); + int32_t textures_index_specular = unit_textures_specular.find(specular_name); + + if (textures_index_diffuse < 0 && !diffuse_name.is_empty()) { + unit_textures_diffuse.push_back(diffuse_name); + if (unit_textures_diffuse.size() >= MAX_UNIT_TEXTURES) { + Logger::error("Number of diffuse textures exceeded max supported by a shader!"); + } + TypedArray diffuse_textures = unit_shader->get_shader_parameter(Param_texture_diffuse); + diffuse_textures.push_back(diffuse_texture); + textures_index_diffuse = diffuse_textures.size() - 1; + unit_shader->set_shader_parameter(Param_texture_diffuse, diffuse_textures); + } + //in a vic2 "specular" texture -> r=specular, g=secondary unit color, b=tertiary unit color + if (textures_index_specular < 0 && !specular_name.is_empty()) { + unit_textures_specular.push_back(specular_name); + if (unit_textures_specular.size() >= MAX_UNIT_TEXTURES) { + Logger::error("Number of specular textures exceeded max supported by a shader!"); + } + TypedArray specular_textures = unit_shader->get_shader_parameter(Param_texture_nation_colors_mask); + //Logger::info(specular_textures[0].) + specular_textures.push_back(specular_texture); + textures_index_specular = specular_textures.size() - 1; + unit_shader->set_shader_parameter(Param_texture_nation_colors_mask, specular_textures); + } + + mapping.godot_material = unit_shader; + mapping.diffuse_texture_index = textures_index_diffuse; + mapping.specular_texture_index = textures_index_specular; + } + + mappings.push_back(mapping); + } + + + return mappings; + } + + /* + ==================================== + Mesh helper functions + + ==================================== + */ + // + + //TODO: before, we used surface tool to generate the mesh + // this time around, it might be faster/easier to use the array tools + + //TODO: last time we loaded collision mesh chunks, since we can likely get + // away with a radius around a unit's position, why not skip them + //TODO: last time, we hardcoded lists of other chunk names not to load + // why not try this: + + // if skeleton but mesh chunk has no corresponding skinning chunk: + // skip + // if collision mesh chunk + // skip + + // alternatively, see if a mesh name corresponds to a name in the hierarchy + // if yes, keep the mesh + + //this should get rid of mesh chunks that aren't attached to anything + // Is this actually a case for reinterpret_cast? + template + static void convert_data(std::vector& target, std::vector const& source) { + target.resize(source.size()/sizeof(T)); + std::memcpy(target.data(),source.data(),source.size()); + } + + static MeshInstance3D* build_mesh(mesh_t const& mesh_chunk, skinning_t* skin, std::vector const& materials) { + static const uint32_t EXTRA_CULL_MARGIN = 2; + + MeshInstance3D* mesh_inst = memnew(MeshInstance3D); + mesh_inst->set_extra_cull_margin(EXTRA_CULL_MARGIN); + + Ref mesh = Ref(); + mesh.instantiate(); + + std::vector verts = {}; + std::vector normals = {}; + std::vector tangents = {}; + int uvs_read = 0; + std::vector uv1 = {}; + std::vector uv2 = {}; + std::vector influence_range_indices = {}; + + for(vertices_attribute_t const& attribute : mesh_chunk.vertices_attributes) { + switch(attribute.packed.type) { + case ATTRIBUTE::POSITION: + convert_data(verts, attribute.data); + break; + case ATTRIBUTE::NORMAL: + convert_data(normals, attribute.data); + break; + case ATTRIBUTE::TANGENT: + convert_data(tangents, attribute.data); + break; + case ATTRIBUTE::UV: + if (uvs_read == 0) { + convert_data(uv1, attribute.data); + } + else if (uvs_read == 1) { + convert_data(uv2, attribute.data); + } + uvs_read += 1; + break; + case ATTRIBUTE::INFLUENCE_RANGE: + if (skin == nullptr) { + Logger::warning("Mesh chunk has influence attribute but no corresponding skinning chunk"); + break; + } + convert_data(influence_range_indices, attribute.data); + break; + //for now, ignore color data + case ATTRIBUTE::COL_32: + case ATTRIBUTE::COL_128: + default: + break; + } + } + + //if (skin != nullptr) Logger::info("skin infdat size: ", skin->influence_data.size(), " ", skin->packed.influences_count); + //if (skin != nullptr) Logger::info("skin infran size: ", skin->influence_ranges.size(), " ", mesh_chunk.packed.influence_ranges_count); + //else Logger::info("no skinning chunk"); + + + uint32_t vert_total = 0; + static const StringName key_diffuse = "tex_index_diffuse"; + static const StringName key_specular = "tex_index_specular"; + static const StringName key_scroll = "scroll_tex_index_diffuse"; + + //for now we treat a submesh as a godot mesh surface + for(submesh_t submesh : mesh_chunk.submeshes) { + Array arrs; //surface vertex data arrays + ERR_FAIL_COND_V(arrs.resize(Mesh::ARRAY_MAX) != OK, {}); + + PackedVector3Array verts_submesh = {}; + PackedVector3Array normals_submesh = {}; + PackedFloat32Array tangents_submesh = {}; + PackedVector2Array uv1_submesh = {}; + PackedVector2Array uv2_submesh = {}; + PackedInt32Array bone_ids = {}; + PackedFloat32Array weights = {}; + //godot uses a fixed 4 bones influencing a vertex, so size the array accordingly + bone_ids.resize(submesh.relative_indices.size()*4); + bone_ids.fill(0); + + weights.resize(submesh.relative_indices.size()*4); + weights.fill(0); + + //1 vertex is in the surface per relative index + for(uint32_t rel_ind : submesh.relative_indices) { + uint32_t index = rel_ind + vert_total; + + verts_submesh.push_back(vec3d_to_godot(verts[index], true)); + if (normals.size() > index) { + normals_submesh.push_back( vec3d_to_godot(normals[index],true)); + } + + //TODO: verify if we need to invert the x for each tangent + if (tangents.size() > index) { + vec4d_t const& tan = tangents[index]; + tangents_submesh.push_back(-tan.x); + tangents_submesh.push_back(tan.y); + tangents_submesh.push_back(tan.z); + tangents_submesh.push_back(tan.w); + } + + if (uv1.size() > index) { + uv1_submesh.push_back(vec2d_to_godot(uv1[index])); + } + if (uv2.size() > index) { + uv2_submesh.push_back(vec2d_to_godot(uv2[index])); + } + + if (skin != nullptr && influence_range_indices.size() > index) { + uint32_t const& influence_range_ind = influence_range_indices[index]; + uint32_t const& influences_ind = skin->influence_ranges[influence_range_ind].first_influence_index; + uint32_t influences_count = Math::min(skin->influence_ranges[influence_range_ind].influences_count, 4); + + for(int i = 0; i < influences_count; i++) { + bone_ids[rel_ind + i] = skin->influence_data[influences_ind + i].bone_id; + weights[rel_ind + i] = skin->influence_data[influences_ind + i].weight; + } + } + } + + if (!verts_submesh.is_empty()) { + arrs[Mesh::ARRAY_VERTEX] = std::move(verts_submesh); + } + if (!normals_submesh.is_empty()) { + arrs[Mesh::ARRAY_NORMAL] = std::move(normals_submesh); + } + if (!tangents_submesh.is_empty()) { + arrs[Mesh::ARRAY_TANGENT] = std::move(tangents_submesh); + } + if (!uv1_submesh.is_empty()) { + arrs[Mesh::ARRAY_TEX_UV] = std::move(uv1_submesh); + } + if (!uv2_submesh.is_empty()) { + arrs[Mesh::ARRAY_TEX_UV2] = std::move(uv2_submesh); + } + if (skin != nullptr && influence_range_indices.size() > 0) { + //arrs[Mesh::ARRAY_BONES] = std::move(bone_ids); + //arrs[Mesh::ARRAY_WEIGHTS] = std::move(weights); + } + mesh->add_surface_from_arrays(Mesh::PRIMITIVE_TRIANGLES, arrs); + + //setup materials for the surface + material_mapping const& material_mapping = materials[submesh.packed.material_id]; + mesh->surface_set_material(mesh->get_surface_count()-1, material_mapping.godot_material); + if (material_mapping.diffuse_texture_index != -1) { + mesh_inst->set_instance_shader_parameter(key_diffuse, material_mapping.diffuse_texture_index); + } + if (material_mapping.specular_texture_index != -1) { + mesh_inst->set_instance_shader_parameter(key_specular, material_mapping.diffuse_texture_index); + } + if (material_mapping.scroll_index != -1) { + mesh_inst->set_instance_shader_parameter(key_scroll, material_mapping.diffuse_texture_index); + } + + vert_total += submesh.relative_indices.size(); + } + + mesh_inst->set_mesh(mesh); + + return mesh_inst; + } + + /* + ==================================== + Main loading function + + ==================================== + */ + static Node3D* _load_xac_model(Ref const& file) { + bool res = read_xac_header(file); + + Node3D* base = memnew(Node3D); + + if (!res) { + return base; + } + + xac_metadata_v2_t metadata; + + bool hierarchy_read = false; + node_hierarchy_t hierarchy; + std::vector nodes; //other way of making a bone hierarchy + + material_totals_t material_totals = {}; + std::vector materials; + + std::vector meshes; + std::vector skinnings; + + bool log_all = false; + + //0xA unknown (ex. Spanish_Helmet1.xac), only 2 bytes long... + //0x6 unknown (ex. ...) + //0x8 junk data (ie. old versions of the file duplicated in append mode) + // (ex. Port_Empty.xac) + //0xC morphtargets (not used in vic2) + + while (!file->eof_reached()) { + chunk_header_t header = {}; + if (!read_chunk_header(file, header)) { + break; + } + + if (log_all) { + UtilityFunctions::print( + vformat("XAC chunk: type = %x, length = %x, version = %d at %x", header.type, header.length, header.version, file->get_position()) + ); + } + + if (header.type == 0x7 && header.version == 2) { + read_xac_metadata(file, metadata); + } + else if (header.type == 0xB && header.version == 1) { + hierarchy_read = read_node_hierarchy(file, hierarchy); + } + else if (header.type == 0x0 && header.version == 3) { + node_chunk_t node; + read_node_chunk(file, node); + nodes.push_back(node); + } + else if (header.type == 0x1 && header.version == 1) { + mesh_t mesh; + read_mesh(file, mesh); + meshes.push_back(mesh); + } + else if (header.type == 0x2) { + skinning_t skin; + read_skinning(file, skin, meshes, header.version); + skinnings.push_back(skin); + } + else if (header.type == 0x3) { + material_definition_t mat; + read_material_definition(file, mat, header.version); + materials.push_back(mat); + } + else if (header.type == 0x4 && header.version == 2) { + material_layer_t layer; + read_layer(file, layer); + if (layer.packed.material_id < materials.size()) { + materials[layer.packed.material_id].layers.push_back(layer); + } + else{ + Logger::error("No material of id ", layer.packed.material_id, " to attach layer to"); + } + } + else if (header.type == 0xD && header.version == 1) { + read_material_totals(file, material_totals); + } + else if (header.type == 0x8 || header.type == 0xA) { //skip junk data and whatever 0xA is... + if (header.length + file->get_position() < file->get_length()) { + file->get_buffer(header.length); + } + else { + res = false; + break; + } + } + else { + UtilityFunctions::print( + vformat("Unsupported XAC chunk: type = %x, length = %x, version = %d at %x", header.type, header.length, header.version, file->get_position()) + ); + log_all = true; + // Skip unsupported chunks by using get_buffer, make sure this doesn't break anything, since chunk length can be wrong + if (header.length + file->get_position() < file->get_length()) { + file->get_buffer(header.length); + } + else { + res = false; + break; + } + } + } + + //Setup skeleton + Skeleton3D* skeleton = nullptr; + if (hierarchy_read) { + skeleton = build_armature_hierarchy(hierarchy); + base->add_child(skeleton); + } + else if (!nodes.empty()) { + skeleton = build_armature_nodes(nodes); + base->add_child(skeleton); + } + + //Setup materials + std::vector const& material_mappings = build_materials(materials); + + //setup mesh + for(mesh_t const& mesh : meshes) { + if (mesh.packed.is_collision_mesh) { continue; } //we'll use our own collision meshes where needed + + skinning_t* mesh_skin = nullptr; + //for(skinning skin : skinnings) { + for(int i=0; i < skinnings.size(); i++) { + skinning_t& skin = skinnings[i]; + if (skin.node_id == mesh.packed.node_id && skin.packed.is_for_collision == mesh.packed.is_collision_mesh) { + mesh_skin = &skin; + break; + } + } + + if (skeleton != nullptr && mesh_skin == nullptr) { continue; } + + MeshInstance3D* mesh_inst = build_mesh(mesh, mesh_skin, material_mappings); + base->add_child(mesh_inst); + mesh_inst->set_owner(base); + + if (skeleton != nullptr) { + mesh_inst->set_skeleton_path(mesh_inst->get_path_to(skeleton)); + int32_t node_id = mesh.packed.node_id; + if (hierarchy_read && node_id < hierarchy.node_data.size()) { + mesh_inst->set_name(hierarchy.node_data[node_id].name); + } + else if (!nodes.empty() && node_id < nodes.size()) { + mesh_inst->set_name(nodes[node_id].name); + } + } + } + + return base; + } +} \ No newline at end of file diff --git a/extension/src/openvic-extension/utility/XACUtilities.hpp b/extension/src/openvic-extension/utility/XACUtilities.hpp new file mode 100644 index 00000000..e9d3b787 --- /dev/null +++ b/extension/src/openvic-extension/utility/XACUtilities.hpp @@ -0,0 +1,221 @@ +#pragma once +#include +#include +#include +//#include "openvic-simulation/utility/Logger.hpp" +//#include "openvic-extension/utility/Utilities.hpp" +#include +#include + +namespace OpenVic { + #pragma pack(push) + #pragma pack(1) + + struct chunk_header_t { + int32_t type; + int32_t length; + int32_t version; + }; + + struct vec2d_t { //not a real datatype in the files. Just using it for convenience + float x; + float y; + }; + + struct vec3d_t { + float x; + float y; + float z; + }; + + struct vec4d_t { + float x; + float y; + float z; + float w; + }; + + struct quat_v1_t { + float x; + float y; + float z; + float w; + }; + + struct quat_v2_t { // divide by 32767 to get proper quat + int16_t x; + int16_t y; + int16_t z; + int16_t w; + }; + + struct matrix44_t { + vec4d_t col1; + vec4d_t col2; + vec4d_t col3; + vec4d_t col4; + }; + + struct color_32_t { + int8_t r; + int8_t g; + int8_t b; + int8_t a; + }; + + struct color_128_t { + int32_t r; + int32_t g; + int32_t b; + int32_t a; + }; + + #pragma pack(pop) + + using namespace godot; + using namespace OpenVic; + + static bool read_string(Ref const& file, String& str, bool replace_chars = true, bool log = false) { + //string = uint32 len, char[len] + uint32_t length = file->get_32(); + if (file->get_length() - file->get_position() < length) { + return false; + } + + str = file->get_buffer(length).get_string_from_ascii(); + if (log) { + UtilityFunctions::print(vformat("before %s", str)); + } + if (replace_chars) { + str = str.replace(":", "_"); + str = str.replace("\\", "_"); + str = str.replace("/", "_"); + } + if (log) { + UtilityFunctions::print(vformat("after %s", str)); + } + return true; + } + + template + static bool read_struct(Ref const& file, T& t) { + if (file->get_length() - file->get_position() < sizeof(T)) { + return false; + } + bool res = file->get_buffer(reinterpret_cast(&t), sizeof(t)) == sizeof(t); + return res; + } + + //Warning: works on the assumption of it being a packed struct being loaded into the array + template + static bool read_struct_array(Ref const& file, std::vector& t, uint32_t size) { + if (file->get_length() - file->get_position() < size*sizeof(T)) { + return false; + } + t.resize(size*sizeof(T)); + bool res = file->get_buffer(reinterpret_cast(t.data()), sizeof(T)*size) == sizeof(t); + return res; + } + + static bool read_chunk_header(Ref const& file, chunk_header_t& header) { + //ERR_FAIL_COND_V(!read_struct(file, header), false); + bool res = read_struct(file, header); + + //UtilityFunctions::print( + // vformat("XAC/XSM chunk: type = %x, length = %x, version = %d, successful? %s", header.type, header.length, header.version, res) + //); + + return res; + } + + static Vector2 vec2d_to_godot(vec2d_t const& vec2_in) { + Vector2 vec2_out = { + vec2_in.x, + vec2_in.y + }; + return vec2_out; + } + + static Vector3 vec3d_to_godot(vec3d_t const& vec3_in, bool is_position = false) { + Vector3 vec3_out = { + vec3_in.x, + vec3_in.y, + vec3_in.z + }; + if (is_position) { + vec3_out.x *= -1; + } + return vec3_out; + } + + static Vector4 vec4d_to_godot(vec4d_t const& vec4_in) { + Vector4 vec4_out = { + vec4_in.x, + vec4_in.y, + vec4_in.z, + vec4_in.w + }; + return vec4_out; + } + + static Quaternion quat_v1_to_godot(quat_v1_t const& quat_in) { + Quaternion quat_out = Quaternion( + quat_in.x, + -quat_in.y, + -quat_in.z, + quat_in.w + ); + if (!quat_out.is_normalized()) { + quat_out = Quaternion(); + } + return quat_out; + } + + static Quaternion quat_v2_to_godot(quat_v2_t const& quat_in) { + static const float scale = 32767; + Quaternion quat_out = Quaternion( + static_cast(quat_in.x) / scale, + static_cast(-quat_in.y) / scale, + static_cast(-quat_in.z) / scale, + static_cast(quat_in.w) / scale + ); + if (!quat_out.is_normalized()) { + quat_out = Quaternion(); + } + + return quat_out; + } + + static Color color_32_to_godot(color_32_t const& color_in) { + static const float scale = 256; + Color color_out = { + (float)color_in.r / scale, + (float)color_in.g / scale, + (float)color_in.b / scale, + (float)color_in.a / scale + }; + return color_out; + } + + //TODO: verify this conversion is correct >> don't think it is + static Color color_128_to_godot(color_128_t const& color_in) { + static const double scale = 2147483647; + Color color_out = { + static_cast(color_in.r / scale), + static_cast(color_in.g / scale), + static_cast(color_in.b / scale), + static_cast(color_in.a / scale) + }; + return color_out; + } + + static Color vec4d_to_godot_color(vec4d_t const& color_in) { + Color color_out = { + color_in.x, + color_in.y, + color_in.z, + color_in.w + }; + return color_out; + } +} \ No newline at end of file diff --git a/extension/src/openvic-extension/utility/XSMLoader.cpp b/extension/src/openvic-extension/utility/XSMLoader.cpp new file mode 100644 index 00000000..e69de29b diff --git a/extension/src/openvic-extension/utility/XSMLoader.hpp b/extension/src/openvic-extension/utility/XSMLoader.hpp new file mode 100644 index 00000000..8a719e23 --- /dev/null +++ b/extension/src/openvic-extension/utility/XSMLoader.hpp @@ -0,0 +1,428 @@ +#pragma once +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "XACUtilities.hpp" + +using namespace godot; +using namespace OpenVic; + +static constexpr uint32_t XSM_FORMAT_SPECIFIER = ' MSX'; /* Order reversed due to little endian */ +static constexpr uint8_t XSM_VERSION_MAJOR = 1, XSM_VERSION_MINOR = 0; + +// Pack structs of data that can be read directly + +#pragma pack(push) +#pragma pack(1) + +struct xsm_header_t { + uint32_t format_identifier; + uint8_t version_major; + uint8_t version_minor; + uint8_t big_endian; + uint8_t pad; +}; + +//v2 of the header adds these 2 properties at the start +struct xsm_metadata_v2_pack_extra_t { + float unused; + float max_acceptable_error; +}; + +struct xsm_metadata_pack_t { + int32_t fps; + uint8_t exporter_version_major; //these 3 properties are uncertain + uint8_t exporter_version_minor; + uint16_t pad; +}; + +struct position_key_t { + vec3d_t position; + float time; +}; + +struct rotation_key_v2_t { + quat_v2_t rotation; + float time; +}; + +struct rotation_key_v1_t { + quat_v1_t rotation; + float time; +}; + +struct scale_key_t { + vec3d_t scale; + float time; +}; + +//the quaternions come at the start of the skeletal submotion +struct submotion_rot_v2_pack_t { + quat_v2_t pose_rotation; + quat_v2_t bind_pose_rotation; + quat_v2_t pose_scale_rotation; + quat_v2_t bind_pose_scale_rotation; +}; + +struct submotion_rot_v1_pack_t { + quat_v1_t pose_rotation; + quat_v1_t bind_pose_rotation; + quat_v1_t pose_scale_rotation; + quat_v1_t bind_pose_scale_rotation; +}; + +struct skeletal_submotion_pack_t { + vec3d_t pose_position; + vec3d_t pose_scale; + vec3d_t bind_pose_position; + vec3d_t bind_pose_scale; + int32_t position_key_count; + int32_t rotation_key_count; + int32_t scale_key_count; + int32_t scale_rotation_key_count; + float max_error; +}; + +struct node_submotion_pack_t { + vec3d_t pose_position; + vec3d_t pose_scale; + vec3d_t bind_pose_position; + vec3d_t bind_pose_scale; + int32_t position_key_count; + int32_t rotation_key_count; + int32_t scale_key_count; + int32_t scale_rotation_key_count; +}; + +#pragma pack(pop) + +struct xsm_metadata_t { + bool has_v2_data = false; + xsm_metadata_v2_pack_extra_t v2_data = {}; + xsm_metadata_pack_t packed = {}; + String source_app; + String original_file_name; + String export_date; + String motion_name; +}; + +struct rotation_key_t { + Quaternion rotation; + float time; +}; + +struct skeletal_submotion_t { + Quaternion pose_rotation; + Quaternion bind_pose_rotation; + Quaternion pose_scale_rotation; + Quaternion bind_pose_scale_rotation; + skeletal_submotion_pack_t packed = {}; + String node_name; + std::vector position_keys; + std::vector rotation_keys; + std::vector scale_keys; + std::vector scale_rotation_keys; +}; + +struct bone_animation_t { + int32_t submotion_count = 0; + std::vector submotions; +}; + +/* +0xC8, v1 chunk documentation: (Let's call it node_submotion) + 4*8x 32 bit sections = 32 + 4x quat32 = 16 + 4x vec3 = 12 + 4x keycount 04 + 32 + string node_name + data... + + so this chunk is the same as a skeletal submotion v1 + but without the fMaxError +*/ + +struct node_submotion_t { + Quaternion pose_rotation; + Quaternion bind_pose_rotation; + Quaternion pose_scale_rotation; + Quaternion bind_pose_scale_rotation; + node_submotion_pack_t packed = {}; + String node_name; + std::vector position_keys; + std::vector rotation_keys; + std::vector scale_keys; + std::vector scale_rotation_keys; +}; + +static bool read_xsm_header(Ref const& file) { + xsm_header_t header = {}; + ERR_FAIL_COND_V(!read_struct(file, header), false); + + //Logger::info("XSM file version: ", header.version_major, ".", header.version_minor," big endian?: ",header.big_endian); + + ERR_FAIL_COND_V_MSG( + header.format_identifier != XSM_FORMAT_SPECIFIER, false, vformat( + "Invalid XSM format identifier: %x (should be %x)", header.format_identifier, XSM_FORMAT_SPECIFIER + ) + ); + + ERR_FAIL_COND_V_MSG( + header.version_major != XSM_VERSION_MAJOR || header.version_minor != XSM_VERSION_MINOR, false, vformat( + "Invalid XSM version: %d.%d (should be %d.%d)", + header.version_major, header.version_minor, XSM_VERSION_MAJOR, XSM_VERSION_MINOR + ) + ); + + ERR_FAIL_COND_V_MSG( + header.big_endian != 0, false, "Invalid XSM endianness: big endian (only little endian is supported)" + ); + + return true; +} + +static bool read_xsm_metadata(Ref const& file, xsm_metadata_t& metadata, int32_t version) { + bool res = true; + if (version != 1) { + res &= read_struct(file, metadata.v2_data); + metadata.has_v2_data = true; + } + res &= read_struct(file, metadata.packed); + res &= read_string(file, metadata.source_app); + res &= read_string(file, metadata.original_file_name); + res &= read_string(file, metadata.export_date); + res &= read_string(file, metadata.motion_name); + + return res; +} + +static bool read_rot_keys(Ref const& file, std::vector* keys_out, int32_t count, int32_t version) { + bool res = true; + + if (version == 1) { + std::vector rot_keys_v1; + res &= read_struct_array(file,rot_keys_v1,count); + for(rotation_key_v1_t key : rot_keys_v1) { + keys_out->push_back({quat_v1_to_godot(key.rotation), key.time}); + } + } + else { + std::vector rot_keys_v2; + res &= read_struct_array(file,rot_keys_v2,count); + for(rotation_key_v2_t key : rot_keys_v2) { + keys_out->push_back({quat_v2_to_godot(key.rotation), key.time}); + } + } + + return res; +} + +static bool read_skeletal_submotion(Ref const& file, skeletal_submotion_t& submotion, int32_t version) { + bool res = true; + if (version == 1) { //float component quats (v1) + submotion_rot_v1_pack_t rot_comps = {}; + res &= read_struct(file,rot_comps); + submotion.pose_rotation = quat_v1_to_godot(rot_comps.pose_rotation); + submotion.bind_pose_rotation = quat_v1_to_godot(rot_comps.bind_pose_rotation); + submotion.pose_scale_rotation = quat_v1_to_godot(rot_comps.pose_scale_rotation); + submotion.bind_pose_scale_rotation = quat_v1_to_godot(rot_comps.bind_pose_scale_rotation); + } + else { //int16 component quats (v2) + submotion_rot_v2_pack_t rot_comps = {}; + res &= read_struct(file,rot_comps); + submotion.pose_rotation = quat_v2_to_godot(rot_comps.pose_rotation); + submotion.bind_pose_rotation = quat_v2_to_godot(rot_comps.bind_pose_rotation); + submotion.pose_scale_rotation = quat_v2_to_godot(rot_comps.pose_scale_rotation); + submotion.bind_pose_scale_rotation = quat_v2_to_godot(rot_comps.bind_pose_scale_rotation); + } + + ERR_FAIL_COND_V(!read_struct(file, submotion.packed), false); + res &= read_string(file, submotion.node_name,true); + res &= read_struct_array(file,submotion.position_keys,submotion.packed.position_key_count); + res &= read_rot_keys(file,&submotion.rotation_keys,submotion.packed.rotation_key_count, version); + res &= read_struct_array(file,submotion.scale_keys,submotion.packed.scale_key_count); + res &= read_rot_keys(file,&submotion.scale_rotation_keys,submotion.packed.scale_rotation_key_count, version); + + return res; +} + +static bool read_xsm_bone_animation(Ref const& file, bone_animation_t& bone_animation, int32_t version) { + bone_animation.submotion_count = file->get_32(); + bool ret = true; + + for(int i=0; i const& file, std::vector* submotions) { + bool res = true; + node_submotion_t submotion; + + submotion_rot_v1_pack_t rot_comps = {}; + res &= read_struct(file,rot_comps); + submotion.pose_rotation = quat_v1_to_godot(rot_comps.pose_rotation); + submotion.bind_pose_rotation = quat_v1_to_godot(rot_comps.bind_pose_rotation); + submotion.pose_scale_rotation = quat_v1_to_godot(rot_comps.pose_scale_rotation); + submotion.bind_pose_scale_rotation = quat_v1_to_godot(rot_comps.bind_pose_scale_rotation); + + res &= read_struct(file, submotion.packed); + res &= read_string(file, submotion.node_name); + res &= read_struct_array(file,submotion.position_keys,submotion.packed.position_key_count); + res &= read_rot_keys(file,&submotion.rotation_keys,submotion.packed.rotation_key_count, 1); + res &= read_struct_array(file,submotion.scale_keys,submotion.packed.scale_key_count); + res &= read_rot_keys(file,&submotion.scale_rotation_keys,submotion.packed.scale_rotation_key_count, 1); + submotions->push_back(submotion); + + return res; +} + +//returns highest time in the submotion +template +float add_submotion(Ref anim, T submotion) { + static const StringName SKELETON_NODE_PATH = "./skeleton:%s"; + float max_time = 0; + + //NOTE: godot uses ':' to specify properties, so we replaced such characters with '_' when we read them in + String skeleton_path = vformat(SKELETON_NODE_PATH, submotion.node_name); + + int pos_id = anim->add_track(Animation::TYPE_POSITION_3D); + int rot_id = anim->add_track(Animation::TYPE_ROTATION_3D); + int scale_id = anim->add_track(Animation::TYPE_SCALE_3D); + + anim->track_set_path(pos_id, skeleton_path); + anim->track_set_path(rot_id, skeleton_path); + anim->track_set_path(scale_id, skeleton_path); + + for(position_key_t key : submotion.position_keys) { + anim->position_track_insert_key(pos_id, key.time, vec3d_to_godot(key.position, true)); + if (key.time > max_time) { + max_time = key.time; + } + } + + for(rotation_key_t key : submotion.rotation_keys) { + anim->rotation_track_insert_key(rot_id, key.time, key.rotation); + if (key.time > max_time) { + max_time = key.time; + } + } + + //not needed for vic2 animations, but we can still support it + for(scale_key_t key : submotion.scale_keys) { + anim->scale_track_insert_key(scale_id, key.time, vec3d_to_godot(key.scale)); + if (key.time > max_time) { + max_time = key.time; + } + } + + // TODO: SCALEROTATION + for(rotation_key_t key : submotion.scale_rotation_keys) { + if (key.time > max_time) { + max_time = key.time; + } + } + + //TODO: why is this hack needed to make the animations work? + if (anim->track_find_key(pos_id, 0) != -1) { + anim->track_remove_key_at_time(pos_id, 0); + } + if (anim->track_find_key(rot_id, 0) != -1) { + anim->track_remove_key_at_time(rot_id, 0); + } + if (anim->track_find_key(scale_id, 0) != -1) { + anim->track_remove_key_at_time(scale_id, 0); + } + //TODO: if you pay close attention, there's still a small jump along the loop point + // though this is visible in vic2 aswell + + //add the "default" positions/rotation/... as keys + anim->position_track_insert_key(pos_id, 0, vec3d_to_godot(submotion.packed.pose_position, true)); + anim->rotation_track_insert_key(rot_id, 0, submotion.pose_rotation); + anim->scale_track_insert_key(scale_id, 0, vec3d_to_godot(submotion.packed.pose_scale)); + + + return max_time; +} + +//a variable string or stringname with %s makes godot fail silently if placed outside a function + +static Ref _load_xsm_animation(Ref const& file) { + + read_xsm_header(file); + + xsm_metadata_t metadata; + bone_animation_t bone_anim; + std::vector node_submotions; + + bool res = true; + + while(!file->eof_reached()) { + chunk_header_t header = {}; + if (!read_chunk_header(file, header)) { break; } + if (header.type == 0xC9) { + read_xsm_metadata(file, metadata, header.version); //in zulu_moving.xsm, this is v1 + } + else if (header.type == 0xC8) { + read_node_submotion(file,&node_submotions); + } + else if (header.type == 0xCA) { + read_xsm_bone_animation(file, bone_anim, header.version); + } + else { + UtilityFunctions::print( + vformat("Unsupported XSM chunk: file: %s, type = %x, length = %x, version = %d at %x", file->get_path(), header.type, header.length, header.version, file->get_position()) + ); + + // Skip unsupported chunks + if (header.length + file->get_position() < file->get_length()) { + PackedByteArray buf = file->get_buffer(header.length); + UtilityFunctions::print(buf); + } + else { + res = false; + break; + } + } + } + + float animation_length = 0.0; + Ref anim = Ref(); + anim.instantiate(); + + anim->set_step(1.0/metadata.packed.fps); + anim->set_loop_mode(Animation::LOOP_LINEAR); + + if (res == false) { + return anim; //exit early if reading the chunks in failed + } + + for(skeletal_submotion_t submotion : bone_anim.submotions) { + float submotion_len = add_submotion(anim, submotion); + if (submotion_len > animation_length) { + animation_length = submotion_len; + } + } + + for(node_submotion_t submotion : node_submotions) { + float submotion_len = add_submotion(anim, submotion); + if (submotion_len > animation_length) { + animation_length = submotion_len; + } + } + + anim->set_length(animation_length); + + return anim; +} \ No newline at end of file diff --git a/game/src/Game/GameSession/ModelManager.gd b/game/src/Game/GameSession/ModelManager.gd index de9de547..1c841d04 100644 --- a/game/src/Game/GameSession/ModelManager.gd +++ b/game/src/Game/GameSession/ModelManager.gd @@ -6,7 +6,7 @@ extends Node3D const MODEL_SCALE : float = 1.0 / 256.0 func generate_units() -> void: - XACLoader.setup_flag_shader() + XACLoader.setup_flag_shader() #TODO: replace with C++ call for unit : Dictionary in ModelSingleton.get_units(): _generate_unit(unit) @@ -104,26 +104,35 @@ func _generate_model(model_dict : Dictionary, culture : String = "", is_unit : b or attachments_key in model_dict ) - var model : Node3D = XACLoader.get_xac_model(model_dict[file_key], is_unit) + var model : Node3D + if !is_unit: + model = ModelSingleton.get_xac_model(model_dict[file_key]) + #model = XACLoader.get_xac_model(model_dict[file_key], is_unit) + # print(file_key) + #ModelSingleton.get_xac_model(model_dict[file_key]) + else: + model = XACLoader.get_xac_model(model_dict[file_key], is_unit) + #var model : Node3D = XACLoader.get_xac_model(model_dict[file_key], is_unit) if not model: return null model.scale *= model_dict[scale_key] - + #print("GENERATE MODEL") if model is UnitModel: # Animations var idle_dict : Dictionary = model_dict.get(idle_key, {}) + var move_dict : Dictionary = model_dict.get(move_key, {}) + var attack_dict : Dictionary = model_dict.get(attack_key, {}) + if idle_dict: - model.idle_anim = XSMLoader.get_xsm_animation(idle_dict[animation_file_key]) + model.idle_anim = ModelSingleton.get_xsm_animation(idle_dict[animation_file_key]) model.scroll_speed_idle = idle_dict[animation_time_key] - var move_dict : Dictionary = model_dict.get(move_key, {}) if move_dict: - model.move_anim = XSMLoader.get_xsm_animation(move_dict[animation_file_key]) + model.move_anim = ModelSingleton.get_xsm_animation(move_dict[animation_file_key]) model.scroll_speed_move = move_dict[animation_time_key] - var attack_dict : Dictionary = model_dict.get(attack_key, {}) if attack_dict: - model.attack_anim = XSMLoader.get_xsm_animation(attack_dict[animation_file_key]) + model.attack_anim = ModelSingleton.get_xsm_animation(attack_dict[animation_file_key]) model.scroll_speed_attack = attack_dict[animation_time_key] # Attachments @@ -133,7 +142,7 @@ func _generate_model(model_dict : Dictionary, culture : String = "", is_unit : b attachment_model.free() if culture: - const gun_bone_name : String = "GunNode" + const gun_bone_name : StringName = &"GunNode" if model.has_bone(gun_bone_name): var gun_dict : Dictionary = ModelSingleton.get_cultural_gun_model(culture) if gun_dict: @@ -141,7 +150,7 @@ func _generate_model(model_dict : Dictionary, culture : String = "", is_unit : b if gun_model and model.attach_model(gun_bone_name, gun_model) != OK: gun_model.free() - const helmet_bone_name : String = "HelmetNode" + const helmet_bone_name : StringName = &"HelmetNode" if model.has_bone(helmet_bone_name): var helmet_dict : Dictionary = ModelSingleton.get_cultural_helmet_model(culture) if helmet_dict: diff --git a/game/src/Game/Model/unit_colours.gdshader b/game/src/Game/Model/unit_colours.gdshader index 2ffc7710..f291c544 100644 --- a/game/src/Game/Model/unit_colours.gdshader +++ b/game/src/Game/Model/unit_colours.gdshader @@ -1,7 +1,7 @@ shader_type spatial; -render_mode cull_disabled; +render_mode cull_disabled, depth_prepass_alpha; //hold all the textures for the units that need this shader to mix in their //nation colours (mostly generic infantry units) @@ -28,4 +28,5 @@ void fragment() { vec3 tertiary_col = mix(vec3(1.0, 1.0, 1.0), colour_tertiary, nation_colours_tex.r); ALBEDO = diffuse_tex.rgb * primary_col * secondary_col * tertiary_col; + ALPHA = diffuse_tex.a; }