diff --git a/extension/doc_classes/ModelSingleton.xml b/extension/doc_classes/ModelSingleton.xml index 74c669af..c340cdce 100644 --- a/extension/doc_classes/ModelSingleton.xml +++ b/extension/doc_classes/ModelSingleton.xml @@ -35,5 +35,37 @@ + + + + + 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..70ff0c28 100644 --- a/extension/src/openvic-extension/singletons/ModelSingleton.cpp +++ b/extension/src/openvic-extension/singletons/ModelSingleton.cpp @@ -1,5 +1,7 @@ #include "ModelSingleton.hpp" +#include +#include #include #include @@ -9,6 +11,11 @@ #include "openvic-extension/singletons/GameSingleton.hpp" #include "openvic-extension/utility/ClassBindings.hpp" #include "openvic-extension/utility/Utilities.hpp" +#include "godot_cpp/classes/global_constants.hpp" +#include "godot_cpp/classes/node3d.hpp" +#include "godot_cpp/classes/resource_loader.hpp" +#include "godot_cpp/classes/shader_material.hpp" +#include "godot_cpp/variant/string_name.hpp" using namespace godot; using namespace OpenVic; @@ -19,6 +26,11 @@ 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" }); + OV_BIND_METHOD(ModelSingleton::setup_flag_shader); + OV_BIND_METHOD(ModelSingleton::set_scroll_material_texture, {"texture_name"}); + OV_BIND_METHOD(ModelSingleton::set_unit_material_texture, {"type", "texture_name"}); } ModelSingleton* ModelSingleton::get_singleton() { @@ -481,3 +493,151 @@ 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()) { + Node3D* unit = (Node3D*)it->second->duplicate(); + unit->call("unit_init", true); + //if(unit->get_script()->) { + + //} + return unit; + //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 = XacLoader()._load_xac_model(FileAccess::open(path, FileAccess::READ)); + xac_cache.emplace(source_file,node); + + //if we return the "prototype" in the cache, then it will get scale by 1/256, and then all subsequent units + //duplicated from the cache will get effectively scaled by 1/256*1/256, among other problems. + Node3D* unit = (Node3D*)node->duplicate(); + unit->call("unit_init", true); + return unit; +} + +Error ModelSingleton::setup_flag_shader() { + godot::Error result = godot::OK; + GameSingleton const* game_singleton = GameSingleton::get_singleton(); + ERR_FAIL_NULL_V(game_singleton, {}); + + static const godot::StringName Param_flag_dimensions = "flag_dims"; + static const godot::StringName Param_flag_texture_sheet = "texture_flag_sheet_diffuse"; + godot::ResourceLoader* loader = godot::ResourceLoader::get_singleton(); + static const godot::Ref flag_shader = loader->load("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 void pushback_shader_array(godot::Ref shader, godot::String property, godot::Variant value) { + godot::Array arr = shader->get_shader_parameter(property); + arr.push_back(value); + shader->set_shader_parameter(property, arr); +} + +Ref ModelSingleton::get_unit_shader() { + if(unit_shader.is_null()) { + godot::ResourceLoader* loader = godot::ResourceLoader::get_singleton(); + unit_shader = loader->load("res://src/Game/Model/unit_colours_mat.tres"); + } + return unit_shader; +} + +Ref ModelSingleton::get_scroll_shader() { + if(scroll_shader.is_null()) { + godot::ResourceLoader* loader = godot::ResourceLoader::get_singleton(); + scroll_shader = loader->load("res://src/Game/Model/scrolling_mat.tres"); + } + return scroll_shader; +} + +//TODO: put this back, 64 is likely needed because of names being added twice +// (due to 2 loaders operating) +static constexpr uint32_t MAX_UNIT_TEXTURES = 32; + +int32_t ModelSingleton::set_unit_material_texture(int32_t type, godot::String texture_name) {//MAP_TYPE::Values + shader_array_index_map_t& map = type==2 ? diffuse_texture_index_map : specular_texture_index_map; //OpenVic::MAP_TYPE::DIFFUSE + const shader_array_index_map_t::const_iterator it = map.find(texture_name); + if(it != map.end()) { + //godot::UtilityFunctions::print( + // godot::vformat("cached texture: %s type: %d, index %d", texture_name, type, it->second) + //); + return it->second; //return the index + } + + int32_t index = map.size(); + map.emplace(texture_name,index); + if (map.size() >= MAX_UNIT_TEXTURES) { + Logger::error("Number of textures exceeded max supported by a shader!"); + return 0; + } + + // Parameters for the default model shader + static const godot::StringName Param_texture_diffuse = "texture_diffuse"; + //red channel is specular, green and blue are nation colours + static const godot::StringName Param_texture_nation_colors_mask = "texture_nation_colors_mask"; + //static const godot::StringName Param_texture_shadow = "texture_shadow"; + String shader_param = type==2 ? Param_texture_diffuse : Param_texture_nation_colors_mask; //OpenVic::MAP_TYPE::DIFFUSE + + //godot::UtilityFunctions::print( + // godot::vformat("texture: %s type: %d, index %d", texture_name, type, index) + //); + pushback_shader_array(get_unit_shader(), shader_param, XacLoader::get_model_texture(texture_name)); + + return index; +} + +int32_t ModelSingleton::set_scroll_material_texture(godot::String texture_name) { + const shader_array_index_map_t::const_iterator it = scroll_index_map.find(texture_name); + if(it != scroll_index_map.end()) { + return it->second; //return the index + } + + scroll_index_map.emplace(texture_name,scroll_index_map.size()); + if (scroll_index_map.size() >= MAX_UNIT_TEXTURES) { + Logger::error("Number of textures exceeded max supported by a shader!"); + return 0; + } + + static const godot::StringName Param_Scroll_texture_diffuse = "scroll_texture_diffuse"; + static const godot::StringName Param_Scroll_factor = "scroll_factor"; + + pushback_shader_array(get_scroll_shader(), Param_Scroll_texture_diffuse, XacLoader::get_model_texture(texture_name)); + + float scroll_factor = 0.0; + static const godot::StringName tracks = "TexAnim"; + static const godot::StringName smoke = "Smoke"; + if(texture_name == tracks) { scroll_factor = 2.5; } + else if(texture_name == smoke) { scroll_factor = 0.3; } + + pushback_shader_array(get_scroll_shader(), Param_Scroll_factor, scroll_factor); + + return scroll_index_map[texture_name]; +} \ 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..f4a470f0 100644 --- a/extension/src/openvic-extension/singletons/ModelSingleton.hpp +++ b/extension/src/openvic-extension/singletons/ModelSingleton.hpp @@ -1,12 +1,21 @@ #pragma once +#include +#include +#include #include #include #include #include +#include "../utility/XSMLoader.hpp" +#include "../utility/XACLoader.hpp" +#include "godot_cpp/classes/shader.hpp" +#include "godot_cpp/classes/shader_material.hpp" +#include "godot_cpp/classes/node3d.hpp" namespace OpenVic { + struct BuildingInstance; class ModelSingleton : public godot::Object { @@ -31,10 +40,23 @@ 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; + using shader_array_index_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::Ref unit_shader; + godot::Ref scroll_shader; + shader_array_index_map_t diffuse_texture_index_map; + shader_array_index_map_t specular_texture_index_map; + //shader_array_index_map_t shadow_texture_index_map; + shader_array_index_map_t scroll_index_map; + godot::Dictionary get_animation_dict(GFX::Actor::Animation const& animation); godot::Dictionary get_model_dict(GFX::Actor const& actor); @@ -56,5 +78,14 @@ 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); + godot::Error setup_flag_shader(); + + godot::Ref get_unit_shader(); + godot::Ref get_scroll_shader(); + int32_t set_unit_material_texture(int32_t type, godot::String name); //MAP_TYPE::Values + int32_t set_scroll_material_texture(godot::String texture_name); }; } diff --git a/extension/src/openvic-extension/utility/XACLoader.cpp b/extension/src/openvic-extension/utility/XACLoader.cpp new file mode 100644 index 00000000..bbff49a3 --- /dev/null +++ b/extension/src/openvic-extension/utility/XACLoader.cpp @@ -0,0 +1,712 @@ +#include "XACLoader.hpp" +//#include "Utilities.hpp" + +#include "godot_cpp/classes/array_mesh.hpp" +#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/script.hpp" +#include "godot_cpp/classes/shader.hpp" +#include "godot_cpp/classes/shader_material.hpp" +#include "godot_cpp/classes/skeleton3d.hpp" + +#include "godot_cpp/variant/array.hpp" +#include "godot_cpp/variant/dictionary.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_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/variant.hpp" + +#include "openvic-simulation/utility/Logger.hpp" + +using namespace::OpenVic; +using namespace::godot; + +bool XacLoader::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; +} + +bool XacLoader::read_xac_metadata(godot::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, true); + return ret; +} + +bool XacLoader::read_node_data(godot::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; +} + +bool XacLoader::read_node_hierarchy(godot::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); +} + +bool XacLoader::read_layer(godot::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; +} + +bool XacLoader::read_material_definition(godot::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(uint32_t 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; +} + +bool XacLoader::read_submesh(godot::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; +} + +bool XacLoader::read_mesh(godot::Ref const& file, mesh_t& mesh) { + bool ret = read_struct(file, mesh.packed); + for(uint32_t 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; +} + +bool XacLoader::read_node_chunk(godot::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 godot::Transform3D make_transform(vec3d_t const& position, quat_v1_t const& quaternion, vec3d_t const& scale) { + godot::Transform3D transform = godot::Transform3D(); + + godot::Basis basis = godot::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? +godot::Skeleton3D* XacLoader::build_armature_hierarchy(node_hierarchy_t const& hierarchy) { + static const godot::StringName skeleton_name = "skeleton"; + godot::Skeleton3D* skeleton = memnew(godot::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); + + godot::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; +} + +godot::Skeleton3D* XacLoader::build_armature_nodes(std::vector const& nodes) { + static const godot::StringName skeleton_name = "skeleton"; + godot::Skeleton3D* skeleton = memnew(godot::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); + + godot::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: remove if unnecessary +/*static void set_model_texture_set_texture(godot::String const& layer_name, godot::String* name) { + if (name->is_empty()) { + *name = layer_name; + } + else { + Logger::error("Multiple diffuse layers in material: ", + Utilities::godot_to_std_string(*name), + " and ", + Utilities::godot_to_std_string(layer_name) + ); + } + return; +}*/ + +XacLoader::model_texture_set XacLoader::get_model_textures(material_definition_t const& material) { + static const godot::StringName Texture_skip_nospec = "nospec"; + static const godot::StringName Texture_skip_flag = "unionjacksquare"; + static const godot::StringName Texture_skip_diff = "test256texture"; + + model_texture_set texture_set; + + for(material_layer_t const& layer : material.layers) { + if (layer.texture == Texture_skip_diff || layer.texture == Texture_skip_flag) { //|| layer.texture == Texture_skip_nospec + continue; + } + //Get the texture names + switch(static_cast(layer.packed.map_type)) { + case MAP_TYPE::DIFFUSE: + texture_set.diffuse_name = layer.texture; + break; + case MAP_TYPE::SPECULAR: + texture_set.specular_name = layer.texture; + break; + case MAP_TYPE::NORMAL: + texture_set.normal_name = layer.texture; + break; + case MAP_TYPE::SHADOW: + /*godot::UtilityFunctions::print( + godot::vformat("Shadow layer: %s", layer.texture) + );*/ + break; + default: + godot::UtilityFunctions::print( + godot::vformat("Unknown layer type: %x, texture: %s", layer.packed.map_type, layer.texture) + ); + break; + } + } + if(texture_set.specular_name.is_empty()) { texture_set.specular_name = Texture_skip_nospec; } + return texture_set; +} + +//unit colours not working, likely due to improper setup of the unit script +std::vector XacLoader::build_materials(std::vector const& materials) { + + // Scrolling textures (smoke, tank tracks) + static const godot::StringName tracks = "TexAnim"; + static const godot::StringName smoke = "Smoke"; + + //General + godot::ResourceLoader* loader = godot::ResourceLoader::get_singleton(); + std::vector mappings; + ModelSingleton* model_singleton = ModelSingleton::get_singleton(); + + + for(material_definition_t const& mat : materials) { + model_texture_set texture_names = get_model_textures(mat); + material_mapping mapping; + + // *** Determine the correct material to use, and set it up *** + // Note all 3 of these materials correspond to different compile targets of avatar.fx + // it has techniques for TexAnim (tracks), Smoke, Shadow, Flag, Standard (we combine TexAnim+smoke, shadow+standard), + // in the original shader, shadow = just sample the color, smoke and flag share speed properties + + //flag TODO: perhaps flag should be determined by hard-coding so that other models can have a normal texture? + //There shouldn't be a specular texture + if (!texture_names.normal_name.is_empty() && texture_names.diffuse_name.is_empty()) { + static const godot::StringName Param_texture_normal = "texture_normal"; + static const godot::Ref flag_shader = loader->load("res://src/Game/Model/flag_mat.tres"); + flag_shader->set_shader_parameter(Param_texture_normal, get_model_texture(texture_names.normal_name)); + mapping.godot_material = flag_shader; + } + //Scrolling texture + else if (!texture_names.diffuse_name.is_empty() && (mat.name == tracks || mat.name == smoke)) { + mapping.scroll_index = model_singleton->set_scroll_material_texture(texture_names.diffuse_name); + mapping.godot_material = model_singleton->get_scroll_shader(); + } + //standard material (diffuse optionally with a specular/unit colours) + else if(!texture_names.diffuse_name.is_empty()){ + mapping.diffuse_texture_index = model_singleton->set_unit_material_texture(2, texture_names.diffuse_name); //MAP_TYPE::DIFFUSE + mapping.specular_texture_index = model_singleton->set_unit_material_texture(3, texture_names.specular_name); //MAP_TYPE::SPECULAR + mapping.godot_material = model_singleton->get_unit_shader(); + } + + else { + godot::UtilityFunctions::push_warning( + godot::vformat("Material %s did not have a diffuse texture! Skipping", mat.name) + ); + } + + 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 + +template +struct byte_array_wrapper : std::span { + byte_array_wrapper() = default; + byte_array_wrapper(std::vector const& source) : + std::span(reinterpret_cast(source.data()), source.size() / sizeof(T)) {} +}; + +godot::MeshInstance3D* XacLoader::build_mesh(mesh_t const& mesh_chunk, skinning_t* skin, std::vector const& materials) { + static const uint32_t EXTRA_CULL_MARGIN = 2; + + godot::MeshInstance3D* mesh_inst = memnew(godot::MeshInstance3D); + mesh_inst->set_extra_cull_margin(EXTRA_CULL_MARGIN); + + godot::Ref mesh = godot::Ref(); + mesh.instantiate(); + + byte_array_wrapper verts; + byte_array_wrapper normals; + byte_array_wrapper tangents; + uint32_t uvs_read = 0; + byte_array_wrapper uv1; + byte_array_wrapper uv2; + byte_array_wrapper influence_range_indices; + + for(vertices_attribute_t const& attribute : mesh_chunk.vertices_attributes) { + switch(attribute.packed.type) { + case ATTRIBUTE::POSITION: + verts = attribute.data; + break; + case ATTRIBUTE::NORMAL: + normals = attribute.data; + break; + case ATTRIBUTE::TANGENT: + tangents = attribute.data; + break; + case ATTRIBUTE::UV: + if (uvs_read == 0) { + uv1 = attribute.data; + } + else if (uvs_read == 1) { + 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; + } + 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 godot::StringName key_diffuse = "tex_index_diffuse"; + static const godot::StringName key_specular = "tex_index_specular"; + static const godot::StringName key_scroll = "scroll_tex_index_diffuse"; + + //for now we treat a submesh as a godot mesh surface + for(submesh_t const& submesh : mesh_chunk.submeshes) { + godot::Array arrs; //surface vertex data arrays + ERR_FAIL_COND_V(arrs.resize(godot::Mesh::ARRAY_MAX) != godot::OK, {}); + + godot::PackedVector3Array verts_submesh = {}; + godot::PackedVector3Array normals_submesh = {}; + godot::PackedFloat32Array tangents_submesh = {}; + godot::PackedVector2Array uv1_submesh = {}; + godot::PackedVector2Array uv2_submesh = {}; + godot::PackedInt32Array bone_ids = {}; + godot::PackedFloat32Array weights = {}; + //godot uses a fixed 4 bones influencing a vertex, so size the array accordingly + //TODO: should this actually be verts.size()? + bone_ids.resize(submesh.relative_indices.size()*4); + weights.resize(submesh.relative_indices.size()*4); + + //1 vertex is in the surface per relative index + for(uint32_t const& 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]; + influence_range_t const& range = skin->influence_ranges[influence_range_ind]; + + for(uint32_t i = 0; i < godot::Math::min(range.influences_count,4); i++) { + influence_data_t const& inf_data = skin->influence_data[range.first_influence_index + i]; + bone_ids[rel_ind*4 + i] = inf_data.bone_id; + weights[rel_ind*4 + i] = inf_data.weight; + } + } + } + + if (!verts_submesh.is_empty()) { + arrs[godot::Mesh::ARRAY_VERTEX] = std::move(verts_submesh); + } + if (!normals_submesh.is_empty()) { + arrs[godot::Mesh::ARRAY_NORMAL] = std::move(normals_submesh); + } + if (!tangents_submesh.is_empty()) { + arrs[godot::Mesh::ARRAY_TANGENT] = std::move(tangents_submesh); + } + if (!uv1_submesh.is_empty()) { + arrs[godot::Mesh::ARRAY_TEX_UV] = std::move(uv1_submesh); + } + if (!uv2_submesh.is_empty()) { + arrs[godot::Mesh::ARRAY_TEX_UV2] = std::move(uv2_submesh); + } + if (skin != nullptr && influence_range_indices.size() > 0) { + arrs[godot::Mesh::ARRAY_BONES] = std::move(bone_ids); + arrs[godot::Mesh::ARRAY_WEIGHTS] = std::move(weights); + } + mesh->add_surface_from_arrays(godot::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.specular_texture_index); + } + if (material_mapping.scroll_index != -1) { + mesh_inst->set_instance_shader_parameter(key_scroll, material_mapping.scroll_index); + } + + vert_total += submesh.relative_indices.size(); + } + + mesh_inst->set_mesh(mesh); + + return mesh_inst; +} + +/* +==================================== +Main loading function + +==================================== +*/ + +godot::Node3D* XacLoader::_load_xac_model(godot::Ref const& file) { + bool res = read_xac_header(file); + + // TODO: gets the unit script if it has the specular/unit colours texture or + + /*is_unit = is_unit or ( + # Needed for animations + idle_key in model_dict or move_key in model_dict or attack_key in model_dict + # Currently needs UnitModel's attach_model helper function + or attachments_key in model_dict + )*/ + + godot::ResourceLoader* loader = godot::ResourceLoader::get_singleton(); + + //TODO: start with the assumption that all units will use this, then reduce later + static const godot::Ref unit_script = loader->load("res://src/Game/Model/UnitModel.gd"); + + godot::Node3D* base = memnew(godot::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) { + godot::UtilityFunctions::print( + godot::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 { + godot::UtilityFunctions::print( + godot::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; + } + } + } + + base->set_name(metadata.original_file_name.get_file().get_basename()); + //TODO: don't set the unit script if we don't have one of: + // -specular (unit colours) texture + // -skeleton with animation + base->set_script(unit_script); + + //Setup skeleton + //TODO: if no skinning chunk, skip + godot::Skeleton3D* skeleton = nullptr; + if(!skinnings.empty()) { + 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(uint32_t 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; } + + godot::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); + } + } + } + //base->call("unit_init", true); + + return base; +} \ No newline at end of file diff --git a/extension/src/openvic-extension/utility/XACLoader.hpp b/extension/src/openvic-extension/utility/XACLoader.hpp new file mode 100644 index 00000000..67eabe2e --- /dev/null +++ b/extension/src/openvic-extension/utility/XACLoader.hpp @@ -0,0 +1,326 @@ +#pragma once +#include +#include +#include + +#include "XACUtilities.hpp" + +#include +#include +#include + +#include +#include +#include +#include + +#include "godot_cpp/classes/mesh_instance3d.hpp" +#include "godot_cpp/classes/skeleton3d.hpp" +#include +#include +#include + + +namespace OpenVic { + + //needed for material loading functions, which use modelSingleton to track state between calls + struct ModelSingleton; + + /*enum struct MAP_TYPE { + DIFFUSE = 2, + SPECULAR, + SHADOW, + NORMAL + };*/ + /*struct MAP_TYPE { + enum Values : int32_t { + DIFFUSE = 2, + SPECULAR, + SHADOW, + NORMAL + }; + };*/ + + class XacLoader { + 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; + + //TODO: How do we get this enum to work both here and in modelSingleton? + //public: + enum class MAP_TYPE { + DIFFUSE = 2, + SPECULAR, + SHADOW, + NORMAL + }; + /*struct MAP_TYPE { + enum Values : int32_t { + DIFFUSE = 2, + SPECULAR, + SHADOW, + NORMAL + }; + }; + private:*/ + + #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 ATTRIBUTE { + enum Values : int32_t { + POSITION, + NORMAL, + TANGENT, + UV, + COL_32, + INFLUENCE_RANGE, + COL_128 + }; + }; + + 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 xac_metadata_v2_t { + xac_metadata_v2_pack_t packed = {}; + godot::String source_app; + godot::String original_file_name; + godot::String export_date; + godot::String actor_name; + }; + + struct node_data_t { //v1 + node_data_pack_t packed = {}; + godot::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 = {}; + godot::String texture; + }; + + struct material_definition_t { + material_definition_pack_t packed = {}; + godot::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 = {}; + godot::String name; + }; + + //Dataloading functions + bool read_xac_header(godot::Ref const& file); + bool read_xac_metadata(godot::Ref const& file, xac_metadata_v2_t& metadata); + bool read_node_data(godot::Ref const& file, node_data_t& node_data); + bool read_node_hierarchy(godot::Ref const& file, node_hierarchy_t& hierarchy); + bool read_material_totals(godot::Ref const& file, material_totals_t& totals); + bool read_layer(godot::Ref const& file, material_layer_t& layer); + bool read_material_definition(godot::Ref const& file, material_definition_t& def, int32_t version); + bool read_vertices_attribute(godot::Ref const& file, vertices_attribute_t& attribute, int32_t vertices_count); + bool read_submesh(godot::Ref const& file, submesh_t& submesh); + bool read_mesh(godot::Ref const& file, mesh_t& mesh); + bool read_skinning(godot::Ref const& file, skinning_t& skin, std::vector const& meshes, int32_t version); + bool read_node_chunk(godot::Ref const& file, node_chunk_t& node); + + //Xac -> godot conversion functions + struct material_mapping { + //-1 means unused + godot::Ref godot_material; + int32_t diffuse_texture_index = -1; + int32_t specular_texture_index = -1; + //int32_t shadow_texture_index = -1; + int32_t scroll_index = -1; + }; + + struct model_texture_set { + godot::String diffuse_name; + godot::String specular_name; + godot::String normal_name; + //godot::String shadow_name; + }; + + godot::Skeleton3D* build_armature_hierarchy(node_hierarchy_t const& hierarchy); + godot::Skeleton3D* build_armature_nodes(std::vector const& nodes); + + model_texture_set get_model_textures(material_definition_t const& material); + std::vector build_materials(std::vector const& materials); + godot::MeshInstance3D* build_mesh(mesh_t const& mesh_chunk, skinning_t* skin, std::vector const& materials); + + public: + //godot::Ref get_texture(godot::String name); + static godot::Ref get_model_texture(godot::String name) { + /*godot::UtilityFunctions::print( + godot::vformat("texture: %s", name) + );*/ + AssetManager* asset_manager = AssetManager::get_singleton(); + //ERR_FAIL_NULL_V(asset_manager, {}); + static const godot::StringName Textures_path = "gfx/anims/%s.dds"; + return asset_manager->get_texture(godot::vformat(Textures_path, name)); + } + godot::Node3D* _load_xac_model(godot::Ref const& file); + }; +} \ 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..488f372c --- /dev/null +++ b/extension/src/openvic-extension/utility/XACUtilities.hpp @@ -0,0 +1,218 @@ +#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) + + static bool read_string(godot::Ref const& file, godot::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) { + godot::UtilityFunctions::print(vformat("before %s", str)); + } + if (replace_chars) { + str = str.replace(":", "_"); + str = str.replace("\\", "_"); + str = str.replace("/", "_"); + } + if (log) { + godot::UtilityFunctions::print(vformat("after %s", str)); + } + return true; + } + + template + static bool read_struct(godot::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(godot::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(godot::Ref const& file, chunk_header_t& header) { + //ERR_FAIL_COND_V(!read_struct(file, header), false); + bool res = read_struct(file, header); + + //godot::UtilityFunctions::print( + // vformat("XAC/XSM chunk: type = %x, length = %x, version = %d, successful? %s", header.type, header.length, header.version, res) + //); + + return res; + } + + static godot::Vector2 vec2d_to_godot(vec2d_t const& vec2_in) { + godot::Vector2 vec2_out = { + vec2_in.x, + vec2_in.y + }; + return vec2_out; + } + + static godot::Vector3 vec3d_to_godot(vec3d_t const& vec3_in, bool is_position = false) { + godot::Vector3 vec3_out = { + vec3_in.x, + vec3_in.y, + vec3_in.z + }; + if (is_position) { + vec3_out.x *= -1; + } + return vec3_out; + } + + static godot::Vector4 vec4d_to_godot(vec4d_t const& vec4_in) { + godot::Vector4 vec4_out = { + vec4_in.x, + vec4_in.y, + vec4_in.z, + vec4_in.w + }; + return vec4_out; + } + + static godot::Quaternion quat_v1_to_godot(quat_v1_t const& quat_in) { + godot::Quaternion quat_out = godot::Quaternion( + quat_in.x, + -quat_in.y, + -quat_in.z, + quat_in.w + ); + if (!quat_out.is_normalized()) { + quat_out = godot::Quaternion(); + } + return quat_out; + } + + static godot::Quaternion quat_v2_to_godot(quat_v2_t const& quat_in) { + static const float scale = 32767; + godot::Quaternion quat_out = godot::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 = godot::Quaternion(); + } + + return quat_out; + } + + static godot::Color color_32_to_godot(color_32_t const& color_in) { + static const float scale = 256; + godot::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 godot::Color color_128_to_godot(color_128_t const& color_in) { + static const double scale = 2147483647; + godot::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 godot::Color vec4d_to_godot_color(vec4d_t const& color_in) { + godot::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..6715f529 --- /dev/null +++ b/extension/src/openvic-extension/utility/XSMLoader.hpp @@ -0,0 +1,427 @@ +#pragma once +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "XACUtilities.hpp" + +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 = {}; + godot::String source_app; + godot::String original_file_name; + godot::String export_date; + godot::String motion_name; + }; + + struct rotation_key_t { + godot::Quaternion rotation; + float time; + }; + + struct skeletal_submotion_t { + godot::Quaternion pose_rotation; + godot::Quaternion bind_pose_rotation; + godot::Quaternion pose_scale_rotation; + godot::Quaternion bind_pose_scale_rotation; + skeletal_submotion_pack_t packed = {}; + godot::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 { + godot::Quaternion pose_rotation; + godot::Quaternion bind_pose_rotation; + godot::Quaternion pose_scale_rotation; + godot::Quaternion bind_pose_scale_rotation; + node_submotion_pack_t packed = {}; + godot::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(godot::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, godot::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, godot::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(godot::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(godot::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 const& 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 const& key : rot_keys_v2) { + keys_out->push_back({quat_v2_to_godot(key.rotation), key.time}); + } + } + + return res; + } + + static bool read_skeletal_submotion(godot::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(godot::Ref const& file, bone_animation_t& bone_animation, int32_t version) { + bone_animation.submotion_count = file->get_32(); + bool ret = true; + + for(uint32_t 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(godot::Ref anim, T const& submotion) { + static const godot::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 + godot::String skeleton_path = godot::vformat(SKELETON_NODE_PATH, submotion.node_name); + + int32_t pos_id = anim->add_track(godot::Animation::TYPE_POSITION_3D); + int32_t rot_id = anim->add_track(godot::Animation::TYPE_ROTATION_3D); + int32_t scale_id = anim->add_track(godot::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 const& 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 const& 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 const& 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 const& 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 godot::Ref _load_xsm_animation(godot::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 { + godot::UtilityFunctions::print( + godot::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()) { + godot::PackedByteArray buf = file->get_buffer(header.length); + godot::UtilityFunctions::print(buf); + } + else { + res = false; + break; + } + } + } + + float animation_length = 0.0; + godot::Ref anim = godot::Ref(); + anim.instantiate(); + + anim->set_step(1.0/metadata.packed.fps); + anim->set_loop_mode(godot::Animation::LOOP_LINEAR); + + if (res == false) { + return anim; //exit early if reading the chunks in failed + } + + for(skeletal_submotion_t const& submotion : bone_anim.submotions) { + float submotion_len = add_submotion(anim, submotion); + if (submotion_len > animation_length) { + animation_length = submotion_len; + } + } + + for(node_submotion_t const& 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/GameSession.tscn b/game/src/Game/GameSession/GameSession.tscn index 8d882b99..0b7b68bf 100644 --- a/game/src/Game/GameSession/GameSession.tscn +++ b/game/src/Game/GameSession/GameSession.tscn @@ -48,9 +48,15 @@ _game_session_menu = NodePath("UICanvasLayer/UI/GameSessionMenu") [node name="MapView" parent="." instance=ExtResource("4_xkg5j")] -[node name="ModelManager" type="Node3D" parent="." node_paths=PackedStringArray("_map_view")] +[node name="ModelManager" type="Node3D" parent="." node_paths=PackedStringArray("_map_view", "_buildings", "_units")] script = ExtResource("3_qwk4j") _map_view = NodePath("../MapView") +_buildings = NodePath("Buildings") +_units = NodePath("Units") + +[node name="Buildings" type="Node3D" parent="ModelManager"] + +[node name="Units" type="Node3D" parent="ModelManager"] [node name="BillboardManager" type="MultiMeshInstance3D" parent="." node_paths=PackedStringArray("_map_view")] sorting_offset = 10.0 diff --git a/game/src/Game/GameSession/ModelManager.gd b/game/src/Game/GameSession/ModelManager.gd index de9de547..91ff0a5c 100644 --- a/game/src/Game/GameSession/ModelManager.gd +++ b/game/src/Game/GameSession/ModelManager.gd @@ -2,11 +2,13 @@ class_name ModelManager extends Node3D @export var _map_view : MapView +@export var _buildings : Node3D +@export var _units : Node3D const MODEL_SCALE : float = 1.0 / 256.0 func generate_units() -> void: - XACLoader.setup_flag_shader() + ModelSingleton.setup_flag_shader() for unit : Dictionary in ModelSingleton.get_units(): _generate_unit(unit) @@ -61,7 +63,7 @@ func _generate_unit(unit_dict : Dictionary) -> void: model.secondary_colour = unit_dict[secondary_colour_key] model.tertiary_colour = unit_dict[tertiary_colour_key] - add_child(model) + _units.add_child(model,true) func generate_buildings() -> void: for building : Dictionary in ModelSingleton.get_buildings(): @@ -75,12 +77,12 @@ func _generate_building(building_dict : Dictionary) -> void: var model : Node3D = _generate_model(building_dict[model_key]) if not model: return - model.scale *= MODEL_SCALE + model.rotate_y(PI + building_dict.get(rotation_key, 0.0)) model.set_position(_map_view._map_to_world_coords(building_dict[position_key]) + Vector3(0, 0.1 * MODEL_SCALE, 0)) - add_child(model) + _buildings.add_child(model,true) func _generate_model(model_dict : Dictionary, culture : String = "", is_unit : bool = false) -> Node3D: const file_key : StringName = &"file" @@ -104,26 +106,39 @@ 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) + model = ModelSingleton.get_xac_model(model_dict[file_key]) + #var model : UnitModel = ModelSingleton.get_xac_model(model_dict[file_key]) + #var model : Node3D = XACLoader.get_xac_model(model_dict[file_key], is_unit) + if not model: return null - model.scale *= model_dict[scale_key] - + model.scale *= model_dict[scale_key] #* MODEL_SCALE #TODO: sometimes 0? + + #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 +148,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 +156,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/UnitModel.gd b/game/src/Game/Model/UnitModel.gd index d482c12f..899cf572 100644 --- a/game/src/Game/Model/UnitModel.gd +++ b/game/src/Game/Model/UnitModel.gd @@ -107,7 +107,9 @@ const ANIMATION_ATTACK : String = ANIMATION_LIBRARY + "/attack" for unit : UnitModel in sub_units: unit.scroll_speed_attack = speed_in -func unit_init() -> void: +func unit_init(print:bool = false) -> void: + #if print: + # print("unit_init called!") for child : Node in get_children(): if child is MeshInstance3D: meshes.append(child) diff --git a/game/src/Game/Model/XACLoader.gd b/game/src/Game/Model/XACLoader.gd index ad2ea2d6..92a2eafc 100644 --- a/game/src/Game/Model/XACLoader.gd +++ b/game/src/Game/Model/XACLoader.gd @@ -354,8 +354,9 @@ static func make_materials(materialDefinitionChunks : Array[MaterialDefinitionCh if diffuse_name and specular_name: if normal_name: push_error("Normal texture present in unit colours material: ", normal_name) - - var textures_index_spec : int = added_unit_textures_spec.find(specular_name) + var textures_index_diffuse = ModelSingleton.set_unit_material_texture(2,diffuse_name) + var textures_index_spec = ModelSingleton.set_unit_material_texture(3,specular_name) + """var textures_index_spec : int = added_unit_textures_spec.find(specular_name) if textures_index_spec < 0: var unit_colours_mask_texture : ImageTexture = AssetManager.get_texture(TEXTURES_PATH % specular_name) if unit_colours_mask_texture: @@ -392,7 +393,8 @@ static func make_materials(materialDefinitionChunks : Array[MaterialDefinitionCh unit_shader.set_shader_parameter(param_texture_diffuse, diffuse_textures) else: push_error("Failed to load diffuse texture: ", diffuse_name) - + """ + materials.push_back(MaterialDefinition.new(unit_shader, textures_index_diffuse, textures_index_spec)) # Flag (diffuse is unionjacksquare which is ignored) @@ -414,7 +416,7 @@ static func make_materials(materialDefinitionChunks : Array[MaterialDefinitionCh push_error("Specular texture present in scrolling material: ", specular_name) if normal_name: push_error("Normal texture present in scrolling material: ", normal_name) - + #ModelSingleton. TODO var scroll_textures_index_diffuse : int = added_scrolling_textures_diffuse.find(diffuse_name) if scroll_textures_index_diffuse < 0: var diffuse_texture : ImageTexture = AssetManager.get_texture(TEXTURES_PATH % diffuse_name) diff --git a/game/src/Game/Model/unit_colours.gdshader b/game/src/Game/Model/unit_colours.gdshader index 2ffc7710..78f148bd 100644 --- a/game/src/Game/Model/unit_colours.gdshader +++ b/game/src/Game/Model/unit_colours.gdshader @@ -1,12 +1,14 @@ 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) -uniform sampler2D texture_diffuse[32] : source_color, filter_linear_mipmap, repeat_enable; -uniform sampler2D texture_nation_colors_mask[32] : source_color, filter_linear_mipmap, repeat_enable; +//TODO: decrease back to 32 +uniform sampler2D texture_diffuse[64] : source_color, filter_linear_mipmap, repeat_enable; +uniform sampler2D texture_nation_colors_mask[64] : source_color, filter_linear_mipmap, repeat_enable; +//uniform sampler2D texture_shadow[32] : source_color, filter_linear_mipmap, repeat_enable; instance uniform vec3 colour_primary : source_color; instance uniform vec3 colour_secondary : source_color; @@ -14,18 +16,24 @@ instance uniform vec3 colour_tertiary : source_color; //used to access the right textures since different units (with different textures) //will use this same shader -instance uniform uint tex_index_diffuse; -instance uniform uint tex_index_specular; +instance uniform int tex_index_diffuse; +instance uniform int tex_index_specular; +//instance uniform int tex_index_shadow = -1; //0 = none void fragment() { vec2 base_uv = UV; vec4 diffuse_tex = texture(texture_diffuse[tex_index_diffuse], base_uv); vec4 nation_colours_tex = texture(texture_nation_colors_mask[tex_index_specular], base_uv); - + //set colours to either be white (1,1,1) or the nation colour based on the mask vec3 primary_col = mix(vec3(1.0, 1.0, 1.0), colour_primary, nation_colours_tex.g); vec3 secondary_col = mix(vec3(1.0, 1.0, 1.0), colour_secondary, nation_colours_tex.b); 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; + /*if(tex_index_shadow != -1){ + vec4 shadow_tex = texture(texture_shadow[tex_index_shadow], base_uv); + ALBEDO = shadow_tex.rgb; + }*/ }