From c7def7396da00b39eced666ad360397733712bfd Mon Sep 17 00:00:00 2001 From: Hop311 Date: Mon, 3 Apr 2023 18:38:27 +0100 Subject: Basic province id and shape loading and rendering. --- extension/src/MapSingleton.cpp | 150 +++++++++++++++++++++++++++++++ extension/src/MapSingleton.hpp | 30 +++++++ extension/src/openvic2/Map.cpp | 29 ++++++ extension/src/openvic2/Map.hpp | 28 ++++++ extension/src/register_types.cpp | 8 ++ game/common/map/provinces.json | 9 ++ game/common/map/provinces.png | Bin 0 -> 9464 bytes game/common/map/provinces.png.import | 3 + game/src/Autoload/Events.gd | 9 ++ game/src/GameSession/MapView.gd | 111 +++++++++++++++++++---- game/src/GameSession/MapView.tscn | 6 +- game/src/GameSession/TerrainMap.gdshader | 23 ++++- 12 files changed, 383 insertions(+), 23 deletions(-) create mode 100644 extension/src/MapSingleton.cpp create mode 100644 extension/src/MapSingleton.hpp create mode 100644 extension/src/openvic2/Map.cpp create mode 100644 extension/src/openvic2/Map.hpp create mode 100644 game/common/map/provinces.json create mode 100644 game/common/map/provinces.png create mode 100644 game/common/map/provinces.png.import diff --git a/extension/src/MapSingleton.cpp b/extension/src/MapSingleton.cpp new file mode 100644 index 0000000..10b750a --- /dev/null +++ b/extension/src/MapSingleton.cpp @@ -0,0 +1,150 @@ +#include "MapSingleton.hpp" + +#include +#include +#include + +using namespace godot; +using namespace OpenVic2; + +MapSingleton* MapSingleton::singleton = nullptr; + +void MapSingleton::_bind_methods() { + ClassDB::bind_method(D_METHOD("load_province_identifier_file", "file_path"), &MapSingleton::load_province_identifier_file); + ClassDB::bind_method(D_METHOD("load_province_shape_file", "file_path"), &MapSingleton::load_province_shape_file); + ClassDB::bind_method(D_METHOD("get_province_shape_image"), &MapSingleton::get_province_shape_image); +} + +MapSingleton* MapSingleton::get_singleton() { + return singleton; +} + +MapSingleton::MapSingleton() { + ERR_FAIL_COND(singleton != nullptr); + singleton = this; +} + +MapSingleton::~MapSingleton() { + ERR_FAIL_COND(singleton != this); + singleton = nullptr; +} + +Error MapSingleton::load_province_identifier_file(String const& file_path) { + UtilityFunctions::print("Loading identifier file: ", file_path); + Ref file = FileAccess::open(file_path, FileAccess::ModeFlags::READ); + Error err = FileAccess::get_open_error(); + if (err != OK || file.is_null()) { + UtilityFunctions::push_error("Failed to load province identifier file: ", file_path); + return err == OK ? FAILED : err; + } + String json_string = file->get_as_text(); + Ref json; + json.instantiate(); + err = json->parse(json_string); + if (err) { + UtilityFunctions::push_error("Failed to parse province identifier file as JSON: ", file_path, + "\nError at line ", json->get_error_line(), ": ", json->get_error_message()); + return err; + } + Variant json_var = json->get_data(); + Variant::Type type = json_var.get_type(); + if (type != Variant::DICTIONARY) { + UtilityFunctions::push_error("Invalid province identifier JSON: root has type ", + Variant::get_type_name(type), " (expected Dictionary)"); + return FAILED; + } + Dictionary prov_dict = json_var; + Array prov_identifiers = prov_dict.keys(); + for (int idx = 0; idx < prov_identifiers.size(); ++idx) { + String const& identifier = prov_identifiers[idx]; + Variant const& colour_var = prov_dict[identifier]; + if (identifier.is_empty()) { + UtilityFunctions::push_error("Empty province identifier with colour: ", colour_var); + err = FAILED; + continue; + } + static const String prov_prefix = "prov_"; + if (!identifier.begins_with(prov_prefix)) + UtilityFunctions::push_warning("Province identifier missing prefix: ", identifier); + type = colour_var.get_type(); + Province::colour_t colour = Province::NULL_COLOUR; + if (type == Variant::ARRAY) { + Array colour_array = colour_var; + if (colour_array.size() == 3) { + for (int jdx = 0; jdx < 3; ++jdx) { + Variant var = colour_array[jdx]; + if (var.get_type() != Variant::FLOAT) { + colour = Province::NULL_COLOUR; + break; + } + double colour_double = var; + if (std::trunc(colour_double) != colour_double) { + colour = Province::NULL_COLOUR; + break; + } + int64_t colour_int = static_cast(colour_double); + if (colour_int < 0 || colour_int > 255) { + colour = Province::NULL_COLOUR; + break; + } + colour = (colour << 8) | colour_int; + } + } + } else if (type == Variant::STRING) { + String colour_string = colour_var; + if (colour_string.is_valid_hex_number()) { + int64_t colour_int = colour_string.hex_to_int(); + if (0 <= colour_int && colour_int <= 0xFFFFFF) + colour = colour_int; + } + } + if (colour == Province::NULL_COLOUR) { + UtilityFunctions::push_error("Invalid province identifier colour for ", identifier, ": ", colour_var); + err = FAILED; + continue; + } + std::string error_message; + if (map.add_province(identifier.utf8().get_data(), colour, error_message)) { + UtilityFunctions::print(error_message.c_str()); + } else { + UtilityFunctions::push_error(error_message.c_str()); + err = FAILED; + } + } + return err; +} + +Error MapSingleton::load_province_shape_file(String const& file_path) { + if (province_shape_image.is_valid()) { + UtilityFunctions::push_error("Province shape file has already been loaded, cannot load: ", file_path); + return FAILED; + } + province_shape_image.instantiate(); + Error err = province_shape_image->load(file_path); + if (err != OK) { + UtilityFunctions::push_error("Failed to load province shape file: ", file_path); + province_shape_image.unref(); + return err; + } + int32_t width = province_shape_image->get_width(); + int32_t height = province_shape_image->get_height(); + if (width < 1 || height < 1) { + UtilityFunctions::push_error("Invalid dimensions (", width, "x", height, ") for province shape file: ", file_path); + err = FAILED; + } + static const Image::Format expected_format = Image::Format::FORMAT_RGB8; + Image::Format format = province_shape_image->get_format(); + if (format != expected_format) { + UtilityFunctions::push_error("Invalid format (", format, ", should be ", expected_format, ") for province shape file: ", file_path); + err = FAILED; + } + if (err) { + province_shape_image.unref(); + return err; + } + return err; +} + +Ref MapSingleton::get_province_shape_image() const { + return province_shape_image; +} diff --git a/extension/src/MapSingleton.hpp b/extension/src/MapSingleton.hpp new file mode 100644 index 0000000..767ae88 --- /dev/null +++ b/extension/src/MapSingleton.hpp @@ -0,0 +1,30 @@ +#pragma once + +#include +#include +#include "openvic2/Map.hpp" + +namespace OpenVic2 { + class MapSingleton : public godot::Object { + + GDCLASS(MapSingleton, godot::Object) + + static MapSingleton* singleton; + + godot::Ref province_shape_image; + Map map; + + protected: + static void _bind_methods(); + + public: + static MapSingleton* get_singleton(); + + MapSingleton(); + ~MapSingleton(); + + godot::Error load_province_identifier_file(godot::String const& file_path); + godot::Error load_province_shape_file(godot::String const& file_path); + godot::Ref get_province_shape_image() const; + }; +} diff --git a/extension/src/openvic2/Map.cpp b/extension/src/openvic2/Map.cpp new file mode 100644 index 0000000..c53b86d --- /dev/null +++ b/extension/src/openvic2/Map.cpp @@ -0,0 +1,29 @@ +#include "Map.hpp" + +#include +#include + +using namespace OpenVic2; + +std::string Province::to_string() const { + std::ostringstream stream; + stream << "(" << identifier << ", " << std::hex << std::setfill('0') << std::setw(6) << colour << ")"; + return stream.str(); +} + +bool Map::add_province(std::string const& identifier, Province::colour_t colour, std::string& error_message) { + Province new_province = { identifier, colour }; + for (Province const& province : provinces) { + if (province.identifier == identifier) { + error_message = "Duplicate province identifiers: " + province.to_string() + " and " + new_province.to_string(); + return false; + } + if (province.colour == colour) { + error_message = "Duplicate province colours: " + province.to_string() + " and " + new_province.to_string(); + return false; + } + } + provinces.push_back(new_province); + error_message = "Added province: " + new_province.to_string(); + return true; +} \ No newline at end of file diff --git a/extension/src/openvic2/Map.hpp b/extension/src/openvic2/Map.hpp new file mode 100644 index 0000000..3c9c6de --- /dev/null +++ b/extension/src/openvic2/Map.hpp @@ -0,0 +1,28 @@ +#pragma once + +#include +#include +#include + +namespace OpenVic2 { + + struct Province { + using colour_t = uint32_t; + + static const colour_t NULL_COLOUR = 0; + + std::string identifier; + colour_t colour; + + std::string to_string() const; + }; + + struct Map { + private: + std::vector provinces; + + public: + bool add_province(std::string const& identifier, Province::colour_t colour, std::string& error_message); + }; + +} diff --git a/extension/src/register_types.cpp b/extension/src/register_types.cpp index d1613a5..ef5428c 100644 --- a/extension/src/register_types.cpp +++ b/extension/src/register_types.cpp @@ -9,6 +9,7 @@ #include "Simulation.hpp" #include "Checksum.hpp" #include "LoadLocalisation.hpp" +#include "MapSingleton.hpp" using namespace godot; using namespace OpenVic2; @@ -17,6 +18,7 @@ static TestSingleton* _test_singleton; static Simulation* _simulation; static Checksum* _checksum; static LoadLocalisation* _load_localisation; +static MapSingleton* _map_singleton; void initialize_openvic2_types(ModuleInitializationLevel p_level) { @@ -40,6 +42,9 @@ void initialize_openvic2_types(ModuleInitializationLevel p_level) _load_localisation = memnew(LoadLocalisation); Engine::get_singleton()->register_singleton("LoadLocalisation", LoadLocalisation::get_singleton()); + ClassDB::register_class(); + _map_singleton = memnew(MapSingleton); + Engine::get_singleton()->register_singleton("MapSingleton", MapSingleton::get_singleton()); } void uninitialize_openvic2_types(ModuleInitializationLevel p_level) { @@ -58,6 +63,9 @@ void uninitialize_openvic2_types(ModuleInitializationLevel p_level) { Engine::get_singleton()->unregister_singleton("LoadLocalisation"); memdelete(_load_localisation); + + Engine::get_singleton()->unregister_singleton("MapSingleton"); + memdelete(_map_singleton); } extern "C" diff --git a/game/common/map/provinces.json b/game/common/map/provinces.json new file mode 100644 index 0000000..66de29b --- /dev/null +++ b/game/common/map/provinces.json @@ -0,0 +1,9 @@ +{ + "prov_britain": [150, 0, 0], + "prov_ireland": [23, 147, 31], + "prov_iceland": "343D91", + "prov_cuba": "1E29FF", + "prov_madagascar": "790091", + "prov_ceylon": "FF6A00", + "prov_formosa": "82B1FF" +} diff --git a/game/common/map/provinces.png b/game/common/map/provinces.png new file mode 100644 index 0000000..68bf528 Binary files /dev/null and b/game/common/map/provinces.png differ diff --git a/game/common/map/provinces.png.import b/game/common/map/provinces.png.import new file mode 100644 index 0000000..8dd0c09 --- /dev/null +++ b/game/common/map/provinces.png.import @@ -0,0 +1,3 @@ +[remap] + +importer="keep" diff --git a/game/src/Autoload/Events.gd b/game/src/Autoload/Events.gd index 25a185f..040cb06 100644 --- a/game/src/Autoload/Events.gd +++ b/game/src/Autoload/Events.gd @@ -2,3 +2,12 @@ extends Node var Options = preload("Events/Options.gd").new() var Localisation = preload("Events/Localisation.gd").new() + +const _province_identifier_file : String = "res://common/map/provinces.json" +const _province_shape_file : String = "res://common/map/provinces.png" + +func _ready(): + if MapSingleton.load_province_identifier_file(_province_identifier_file) != OK: + push_error("Failed to load province identifiers") + if MapSingleton.load_province_shape_file(_province_shape_file) != OK: + push_error("Failed to load province shapes") diff --git a/game/src/GameSession/MapView.gd b/game/src/GameSession/MapView.gd index 4d06066..982271d 100644 --- a/game/src/GameSession/MapView.gd +++ b/game/src/GameSession/MapView.gd @@ -7,6 +7,11 @@ const _action_west : StringName = &"map_west" const _action_zoomin : StringName = &"map_zoomin" const _action_zoomout : StringName = &"map_zoomout" +const _shader_param_provinces : StringName = &"province_tex" +const _shader_param_mouse_pos : StringName = &"mouse_pos" + +@export var _camera : Camera3D + @export var _move_speed : float = 1.0 @export var _zoom_target_min : float = 0.2 @@ -18,8 +23,53 @@ const _action_zoomout : StringName = &"map_zoomout" get: return _zoom_target set(v): _zoom_target = clamp(v, _zoom_target_min, _zoom_target_max) -@export var _camera : Camera3D @export var _map_mesh : MeshInstance3D +var _map_shader_material : ShaderMaterial +var _map_aspect_ratio : float = 1.0 +var _map_mesh_corner : Vector2 +var _map_mesh_dims : Vector2 + +var _mouse_pos : Vector2 = Vector2(0.5, 0.5) + +func _ready(): + if _camera == null: + push_error("MapView's _camera variable hasn't been set!") + return + if _map_mesh == null: + push_error("MapView's _map_mesh variable hasn't been set!") + return + var province_shape_image : Image = MapSingleton.get_province_shape_image() + if province_shape_image == null: + push_error("Failed to get province shape image!") + return + + # Set map mesh size and get bounds + _map_aspect_ratio = float(province_shape_image.get_width()) / float(province_shape_image.get_height()) + if _map_mesh.mesh.get_class() != "PlaneMesh": + push_error("Invalid map mesh class: ", _map_mesh.mesh.get_class()) + else: + # Width is doubled so that the map appears to loop horizontally + (_map_mesh.mesh as PlaneMesh).size = Vector2(_map_aspect_ratio * 2, 1) + var map_mesh_aabb := _map_mesh.get_aabb() * _map_mesh.transform + _map_mesh_corner = Vector2( + min(map_mesh_aabb.position.x, map_mesh_aabb.end.x), + min(map_mesh_aabb.position.z, map_mesh_aabb.end.z) + ) + _map_mesh_dims = abs(Vector2( + map_mesh_aabb.position.x - map_mesh_aabb.end.x, + map_mesh_aabb.position.z - map_mesh_aabb.end.z + )) + + var map_material = _map_mesh.get_active_material(0) + if map_material == null: + push_error("Map mesh is missing material!") + return + if map_material.get_class() != "ShaderMaterial": + push_error("Invalid map mesh material class: ", map_material.get_class()) + return + _map_shader_material = map_material + var texture := ImageTexture.create_from_image(province_shape_image) + _map_shader_material.set_shader_parameter(_shader_param_provinces, texture) func _input(event : InputEvent): if event.is_action_pressed(_action_zoomin, true): @@ -27,33 +77,60 @@ func _input(event : InputEvent): elif event.is_action_pressed(_action_zoomout, true): _zoom_target += _zoom_target_step -func _physics_process(delta): +func _physics_process(delta : float): # Process movement - var height : float = _camera.transform.origin.y + _move_process(delta) + # Keep within map bounds + _clamp_over_map() + # Process zooming + _zoom_process(delta) + # Orient based on height + _update_orientation() + # Calculate where the mouse lies on the map + _update_mouse_map_position() + +func _move_process(delta : float) -> void: var move := Vector3( float(Input.is_action_pressed(_action_east)) - float(Input.is_action_pressed(_action_west)), - 0.0, - float(Input.is_action_pressed(_action_south)) - float(Input.is_action_pressed(_action_north)), + 0, + float(Input.is_action_pressed(_action_south)) - float(Input.is_action_pressed(_action_north)) ) - move *= _move_speed * height * delta + # Scale movement speed with height + move *= _move_speed * _camera.transform.origin.y * delta _camera.global_translate(move) - # Keep within map bounds - var bounds := _map_mesh.get_aabb() * _map_mesh.transform - var width := bounds.end.x - bounds.position.x - var left := bounds.position.x + 0.25 * width - var longitude := fposmod(_camera.transform.origin.x - left, width * 0.5) +func _clamp_over_map() -> void: + var left := _map_mesh_corner.x + 0.25 * _map_mesh_dims.x + var longitude := fposmod(_camera.transform.origin.x - left, _map_mesh_dims.x * 0.5) _camera.transform.origin.x = left + longitude - _camera.transform.origin.z = clamp(_camera.transform.origin.z, bounds.position.z, bounds.end.z) + _camera.transform.origin.z = clamp(_camera.transform.origin.z, _map_mesh_corner.y, _map_mesh_corner.y + _map_mesh_dims.y) - # Process zooming - var zoom : float = _zoom_target - height +func _zoom_process(delta : float) -> void: + var height := _camera.transform.origin.y + var zoom := _zoom_target - height height += zoom * _zoom_speed * delta - var new_zoom : float = _zoom_target - height + var new_zoom := _zoom_target - height + # Set to target if height is within _zoom_epsilon of it or has overshot past it if abs(new_zoom) < _zoom_epsilon or sign(zoom) != sign(new_zoom): height = _zoom_target _camera.transform.origin.y = height - # Orient based on height - var dir := Vector3(0, -1, -exp(-height * 2.0 + 0.5)) +func _update_orientation() -> void: + var dir := Vector3(0, -1, -exp(-_camera.transform.origin.y * 2.0 + 0.5)) _camera.look_at(_camera.transform.origin + dir) + +func _update_mouse_map_position() -> void: + var mouse_pos_window := get_viewport().get_mouse_position() + var ray_origin := _camera.project_ray_origin(mouse_pos_window) + var ray_normal := _camera.project_ray_normal(mouse_pos_window) + # Plane with normal (0,1,0) facing upwards, at a distance 0 from the origin + var intersection = Plane(0, 1, 0, 0).intersects_ray(ray_origin, ray_normal) + if typeof(intersection) == TYPE_VECTOR3: + var intersection_vec := intersection as Vector3 + # This loops both horizontally (good) and vertically (bad) + _mouse_pos = (Vector2(intersection_vec.x, intersection_vec.z) - _map_mesh_corner) / _map_mesh_dims + _map_shader_material.set_shader_parameter(_shader_param_mouse_pos, _mouse_pos) + else: + # Normals parallel to the xz-plane could cause null intersections, + # but the camera's orientation should prevent such normals + push_error("Invalid intersection: ", intersection) diff --git a/game/src/GameSession/MapView.tscn b/game/src/GameSession/MapView.tscn index 4c34035..97a72a8 100644 --- a/game/src/GameSession/MapView.tscn +++ b/game/src/GameSession/MapView.tscn @@ -7,10 +7,10 @@ [sub_resource type="ShaderMaterial" id="ShaderMaterial_tayeg"] render_priority = 0 shader = ExtResource("1_upocn") -shader_parameter/tex = ExtResource("3_l8pnf") +shader_parameter/mouse_pos = Vector2(0.5, 0.5) +shader_parameter/terrain_tex = ExtResource("3_l8pnf") [sub_resource type="PlaneMesh" id="PlaneMesh_skc48"] -size = Vector2(10.8, 2) [node name="MapView" type="Node3D" node_paths=PackedStringArray("_camera", "_map_mesh")] script = ExtResource("1_exccw") @@ -21,6 +21,6 @@ _map_mesh = NodePath("MapMeshInstance") transform = Transform3D(1, 0, 0, 0, 0.707107, 0.707107, 0, -0.707107, 0.707107, 0, 1, 1) [node name="MapMeshInstance" type="MeshInstance3D" parent="."] -transform = Transform3D(5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0) +transform = Transform3D(10, 0, 0, 0, 10, 0, 0, 0, 10, 0, 0, 0) material_override = SubResource("ShaderMaterial_tayeg") mesh = SubResource("PlaneMesh_skc48") diff --git a/game/src/GameSession/TerrainMap.gdshader b/game/src/GameSession/TerrainMap.gdshader index 522e0f3..0c5f3e1 100644 --- a/game/src/GameSession/TerrainMap.gdshader +++ b/game/src/GameSession/TerrainMap.gdshader @@ -2,9 +2,26 @@ shader_type spatial; render_mode unshaded; -uniform sampler2D tex: source_color, repeat_enable; +// Cosmetic terrain texture +uniform sampler2D terrain_tex: source_color, repeat_enable; +// Province shape texture +uniform sampler2D province_tex: source_color, repeat_enable; + +// Mouse position in UV coords over the map mesh +uniform vec2 mouse_pos; + +// Transform map mesh UV coordinates to account for the extra +// half map on either side. This takes the x-coord from [0 -> 1] +// to [-0.5 -> 1.5], while leaving the y-coord unchanged. +vec2 fix_uv(vec2 uv) { + return vec2(uv.x * 2.0 - 0.5, uv.y); +} void fragment() { - vec2 new_uv = vec2(UV.x * 2.0 - 0.5, UV.y); - ALBEDO = texture(tex, new_uv).rgb; + vec2 fixed_uv = fix_uv(UV); + vec3 prov_colour = texture(province_tex, fixed_uv).rgb; + vec3 mouse_colour = texture(province_tex, fix_uv(mouse_pos)).rgb; + // Boost prov_colour's contribution if the mouse is over that colour and it isn't (0,0,0) + float mix_val = prov_colour == mouse_colour && mouse_colour != vec3(0.0) ? 0.8 : 0.4; + ALBEDO = mix(texture(terrain_tex, fixed_uv).rgb, prov_colour, mix_val); } -- cgit v1.2.3-56-ga3b1