From 0783382d7e11391b1f8cd5bb9fb386881379e822 Mon Sep 17 00:00:00 2001 From: DoS Date: Sat, 9 May 2026 11:20:00 +0200 Subject: [PATCH 1/7] Add basic SpotLight instancing --- AGENTS.md | 3 +- addons/libmaszyna/e3d/e3d_instancer.gd | 10 +- addons/libmaszyna/e3d/e3d_light.gd | 28 ++++ addons/libmaszyna/e3d/e3d_light.gd.uid | 1 + addons/libmaszyna/e3d/e3d_model_instance.gd | 18 ++- addons/libmaszyna/e3d/e3d_nodes_instancer.gd | 70 +++++++- demo/demo_3d.tscn | 7 +- doc_classes/E3DSubModel.xml | 27 ++++ src/e3d/E3DModel.cpp | 8 + src/e3d/E3DModel.hpp | 6 +- src/e3d/E3DModelLightDefinition.cpp | 25 +++ src/e3d/E3DModelLightDefinition.hpp | 18 +++ src/e3d/E3DSubModel.cpp | 29 ++++ src/e3d/E3DSubModel.hpp | 10 ++ src/lighting/TrainLighting.cpp | 2 +- src/parsers/e3d_parser.cpp | 158 +++++++++++++++++-- src/parsers/e3d_parser.hpp | 12 ++ src/register_types.cpp | 2 + 18 files changed, 408 insertions(+), 26 deletions(-) create mode 100644 addons/libmaszyna/e3d/e3d_light.gd create mode 100644 addons/libmaszyna/e3d/e3d_light.gd.uid create mode 100644 src/e3d/E3DModelLightDefinition.cpp create mode 100644 src/e3d/E3DModelLightDefinition.hpp diff --git a/AGENTS.md b/AGENTS.md index 61103b4e..ed993018 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -16,6 +16,7 @@ Code generation: * GDSCRIPT: do not replace normal singleton/global access with `/root/...` lookups as a workaround * GDSCRIPT: do not add `is_connected()` guard clutter for signal lifecycle issues; keep one direct `connect` and one matching direct `disconnect` * keep guards minimal; do not generate guard bloat or defensive condition chains when one necessary condition is enough +* do not useset/get/has_meta for accessing/saving/loading node state General guidelines: @@ -26,7 +27,7 @@ General guidelines: Custom nodes and Godot Editor: * place editor related code in addons/libmaszyna/editor -* use libmaszyna.gd just for bootstraping and proxying to editor plugins +* use libmaszyna.gd just for bootstrapping and proxying to editor plugins * make sure C++ singletons never inherit from RefCounted Documentation: diff --git a/addons/libmaszyna/e3d/e3d_instancer.gd b/addons/libmaszyna/e3d/e3d_instancer.gd index 678f0891..e3a91f8f 100644 --- a/addons/libmaszyna/e3d/e3d_instancer.gd +++ b/addons/libmaszyna/e3d/e3d_instancer.gd @@ -17,12 +17,20 @@ func sync(target_node: E3DModelInstance) -> void: push_error("%s.sync() must be implemented" % get_script().resource_path) +## Synchronizes the visibility of light nodes based on the target node's light state. +## Implementations should evaluate [method E3DModelInstance.get_lights_state] and enable/disable +## appropriate visual nodes. +func sync_lights(target_node: E3DModelInstance) -> void: + push_error("%s.sync_lights() must be implemented" % get_script().resource_path) + + func _is_submodel_valid(target_node: E3DModelInstance, submodel: E3DSubModel) -> bool: if submodel.skip_rendering: return false match submodel.submodel_type: - E3DSubModel.SubModelType.SUBMODEL_TRANSFORM: + E3DSubModel.SubModelType.SUBMODEL_TRANSFORM, \ + E3DSubModel.SubModelType.SUBMODEL_FREE_SPOTLIGHT: return true E3DSubModel.SubModelType.SUBMODEL_GL_TRIANGLES: return not target_node.exclude_node_names.any( diff --git a/addons/libmaszyna/e3d/e3d_light.gd b/addons/libmaszyna/e3d/e3d_light.gd new file mode 100644 index 00000000..e9afe19e --- /dev/null +++ b/addons/libmaszyna/e3d/e3d_light.gd @@ -0,0 +1,28 @@ +@tool +extends SpotLight3D + +var meshes_on: Array[Node3D] = [] +var meshes_off: Array[Node3D] = [] + +@export var enabled: bool = true: + set(v): + enabled = v + _update_state() + +func _ready(): + _update_state() + +func _update_state(): + var parent = get_parent() + if parent == null: + return + var light_root = parent.get_parent() + var is_end = parent.name.begins_with("end") + if (!is_end): + var base_name = parent.name.trim_suffix("_on") + var lamp_off_node = light_root.get_node_or_null(NodePath(base_name + "_off")) + parent.visible = enabled + if lamp_off_node != null: + lamp_off_node.visible = !enabled + else: + parent.visible = enabled diff --git a/addons/libmaszyna/e3d/e3d_light.gd.uid b/addons/libmaszyna/e3d/e3d_light.gd.uid new file mode 100644 index 00000000..a6f05578 --- /dev/null +++ b/addons/libmaszyna/e3d/e3d_light.gd.uid @@ -0,0 +1 @@ +uid://d0k5foqnxfdhu diff --git a/addons/libmaszyna/e3d/e3d_model_instance.gd b/addons/libmaszyna/e3d/e3d_model_instance.gd index 49c6c02a..d00d7886 100644 --- a/addons/libmaszyna/e3d/e3d_model_instance.gd +++ b/addons/libmaszyna/e3d/e3d_model_instance.gd @@ -29,6 +29,8 @@ var _e3d_loaded: bool = false var _current_instancer: E3DInstancer var _current_editable: bool = false +var _lights_state: Dictionary[String, bool] = {} + var default_aabb_size: Vector3 = Vector3(1, 1, 1) ## E3DModel to instantiate (leave empty, if you want to lazy load with [member model_filename]) @@ -141,10 +143,24 @@ func _exit_tree() -> void: _current_instancer.clear(self) -func is_e3d_loaded(): +func is_e3d_loaded() -> bool: return _e3d_loaded +func get_lights_state() -> Dictionary[String, bool]: + return _lights_state + + +func set_light_enabled(light_name: String, state: bool) -> void: + if _lights_state.has(light_name): + if _lights_state[light_name] == state: + return + + _lights_state[light_name] = state + if _current_instancer: + _current_instancer.sync_lights(self) + + func _resolve_instancer(): match instancer: Instancer.NODES, Instancer.EDITABLE_NODES: diff --git a/addons/libmaszyna/e3d/e3d_nodes_instancer.gd b/addons/libmaszyna/e3d/e3d_nodes_instancer.gd index 32ad606b..9b8167d7 100644 --- a/addons/libmaszyna/e3d/e3d_nodes_instancer.gd +++ b/addons/libmaszyna/e3d/e3d_nodes_instancer.gd @@ -1,9 +1,41 @@ @tool extends E3DInstancer - func instantiate(target_node: E3DModelInstance, model: E3DModel, editable: bool = false) -> void: - _do_add_submodels(target_node, target_node, model.submodels, editable) + var lights: Array = [] + var light_node_list: Dictionary[String, NodePath] = {} #NodePath -> custom class + _do_add_submodels(target_node, model, target_node, model.submodels, editable, lights) + + var lights_state: Dictionary[String, bool] = target_node.get_lights_state() + var parsed_lights: Dictionary = model.get_lights() + for light_name: String in parsed_lights: + if not lights_state.has(light_name): + lights_state[light_name] = false + + sync_lights(target_node) + print(target_node.get_lights_state()) + + +func sync_lights(target_node: E3DModelInstance) -> void: + var all_nodes: Array[Node] = target_node.find_children("*", "Node3D", true, true) + var node_map: Dictionary = {} + for node: Node in all_nodes: + node_map[node.name.to_lower()] = node + + var lights_state: Dictionary[String, bool] = target_node.get_lights_state() + for light_name: String in lights_state: + var state: bool = lights_state[light_name] + var base_name: String = light_name.to_lower() + + var on_suffixes: Array[String] = ["_on", "_xon"] + for suffix: String in on_suffixes: + var on_node: Node = node_map.get(base_name + suffix) + if on_node is Node3D: + (on_node as Node3D).visible = state + + var off_node: Node = node_map.get(base_name + "_off") + if off_node is Node3D: + (off_node as Node3D).visible = not state func clear(target_node: E3DModelInstance) -> void: @@ -18,13 +50,18 @@ func sync(target_node: E3DModelInstance) -> void: func _do_add_submodels( target_node:E3DModelInstance, + model: E3DModel, parent, - submodels, - editable:bool + submodels: Array[E3DSubModel], + editable: bool, + lights: Array ) -> void: for submodel in submodels: if _is_submodel_valid(target_node, submodel): - var child:Node = _create_submodel_instance(target_node, submodel) + var child:Node = _create_submodel_instance(target_node, submodel, model) + if not child: + continue + _update_submodel_material(target_node, child, submodel) var internal = InternalMode.INTERNAL_MODE_DISABLED if editable else InternalMode.INTERNAL_MODE_BACK parent.add_child(child, false, internal) @@ -33,15 +70,20 @@ func _do_add_submodels( # Applying transform before adding may cause issues (especially on windows) if child is Node3D and submodel.transform: var child_node:Node3D = child as Node3D - child_node.transform = submodel.transform + if child is SpotLight3D: + # Do not scale SpotLight3D to avoid configuration warnings + child_node.position = submodel.transform.origin + child_node.basis = submodel.transform.basis.orthonormalized() + else: + child_node.transform = submodel.transform if Engine.is_editor_hint(): child.owner = target_node.owner if editable else target_node if submodel.submodels: - _do_add_submodels(target_node, child, submodel.submodels, editable) + _do_add_submodels(target_node, model, child, submodel.submodels, editable, lights) -func _create_submodel_instance(target_node: E3DModelInstance, submodel: E3DSubModel): +func _create_submodel_instance(target_node: E3DModelInstance, submodel: E3DSubModel, model: E3DModel): var obj match submodel.submodel_type: @@ -56,6 +98,18 @@ func _create_submodel_instance(target_node: E3DModelInstance, submodel: E3DSubMo obj.visibility_range_begin = submodel.visibility_range_begin obj.visibility_range_end = submodel.visibility_range_end + E3DSubModel.SubModelType.SUBMODEL_FREE_SPOTLIGHT: + obj = SpotLight3D.new() + obj.name = submodel.resource_name + obj.light_color = submodel.diffuse_color + obj.light_energy = submodel.visibility_light + obj.shadow_enabled = true + obj.distance_fade_enabled = true + obj.spot_range = submodel.light_range + obj.spot_angle = submodel.light_angle + obj.spot_attenuation = submodel.light_attenuation + obj.distance_fade_begin = submodel.near_attenuation_start + if obj: obj.visible = submodel.visible return obj diff --git a/demo/demo_3d.tscn b/demo/demo_3d.tscn index b69cbe5f..b950577a 100644 --- a/demo/demo_3d.tscn +++ b/demo/demo_3d.tscn @@ -54,9 +54,9 @@ fog_enabled = true fog_sky_affect = 0.4 volumetric_fog_enabled = true volumetric_fog_density = 0.029 -volumetric_fog_anisotropy = 0.9 -volumetric_fog_length = 0.5 -volumetric_fog_detail_spread = 5.598199 +volumetric_fog_anisotropy = 0.0 +volumetric_fog_length = 0.4 +volumetric_fog_detail_spread = 1.0 volumetric_fog_sky_affect = 0.405 adjustment_enabled = true @@ -226,6 +226,7 @@ clip_contents = true layout_mode = 2 [node name="DeveloperConsole" parent="." unique_id=495569075 instance=ExtResource("2_ig825")] +visible = null [node name="UserSettingsPanel" parent="." unique_id=1798306963 instance=ExtResource("12_eihw1")] diff --git a/doc_classes/E3DSubModel.xml b/doc_classes/E3DSubModel.xml index c37ab2e5..a0961027 100644 --- a/doc_classes/E3DSubModel.xml +++ b/doc_classes/E3DSubModel.xml @@ -21,6 +21,33 @@ The index mapping for the dynamic material state. + + The cone angle of the spotlight in degrees. + + + The attenuation factor of the light source. + + + The maximum range of the light source. + + + Distance where the near attenuation starts (used for halo/aureola effects). + + + Distance where the near attenuation ends. + + + If [code]true[/code], near attenuation (halo effects) is enabled for this light source. + + + The type of distance attenuation (0: none, 1 or 2: inverse distance powers). + + + Cosine of the inner hotspot cone angle. + + + Cosine of the current viewing angle (used for visibility calculation). + The ambient light level threshold at which the submodel's self-illumination becomes active. diff --git a/src/e3d/E3DModel.cpp b/src/e3d/E3DModel.cpp index 57ba4385..ff9e91e3 100644 --- a/src/e3d/E3DModel.cpp +++ b/src/e3d/E3DModel.cpp @@ -13,6 +13,7 @@ namespace godot { } } submodels.clear(); + lights.clear(); } void E3DModel::_bind_methods() { @@ -20,6 +21,13 @@ namespace godot { BIND_PROPERTY_W_HINT_RES_ARRAY( Variant::ARRAY, "submodels", "submodels", &E3DModel::set_submodels, &E3DModel::get_submodels, "p_submodels", PROPERTY_HINT_ARRAY_TYPE, "E3DSubModel"); + BIND_PROPERTY( + Variant::DICTIONARY, "lights", "lights", &E3DModel::set_lights, &E3DModel::get_lights, "p_lights"); + ClassDB::bind_method(D_METHOD("register_light", "p_name", "p_entry"), &E3DModel::register_light); + } + + void E3DModel::register_light(const String &p_name, const Ref &p_entry) { + lights[p_name] = p_entry; } void E3DModel::add_child(const Ref &p_sub_model) { diff --git a/src/e3d/E3DModel.hpp b/src/e3d/E3DModel.hpp index e6919e7d..4322d51e 100644 --- a/src/e3d/E3DModel.hpp +++ b/src/e3d/E3DModel.hpp @@ -1,21 +1,25 @@ #pragma once +#include "E3DModelLightDefinition.hpp" #include "E3DSubModel.hpp" #include +#include #include namespace godot { class E3DModel : public Resource { GDCLASS(E3DModel, Resource) public: - static constexpr int FORMAT_VERSION = 20260524; // must be incremented when public API of E3DModel or + static constexpr int FORMAT_VERSION = 20260525; // must be incremented when public API of E3DModel or // E3DSubModel is changing ~E3DModel() override; protected: static void _bind_methods(); MAKE_MEMBER_GS_NR_NO_DEF(TypedArray, submodels); + MAKE_MEMBER_GS_NR_NO_DEF(Dictionary, lights); void add_child(const Ref &p_sub_model); + void register_light(const String &p_name, const Ref &p_entry); void clear(); }; } // namespace godot diff --git a/src/e3d/E3DModelLightDefinition.cpp b/src/e3d/E3DModelLightDefinition.cpp new file mode 100644 index 00000000..6ab6ab7b --- /dev/null +++ b/src/e3d/E3DModelLightDefinition.cpp @@ -0,0 +1,25 @@ +#include "E3DModelLightDefinition.hpp" + +namespace godot { + void E3DModelLightDefinition::_bind_methods() { + ClassDB::bind_method(D_METHOD("set_on_submodel", "p_model"), &E3DModelLightDefinition::set_on_submodel); + ClassDB::bind_method(D_METHOD("get_on_submodel"), &E3DModelLightDefinition::get_on_submodel); + ADD_PROPERTY( + PropertyInfo(Variant::OBJECT, "on_submodel", PROPERTY_HINT_RESOURCE_TYPE, "E3DSubModel"), + "set_on_submodel", "get_on_submodel"); + + ClassDB::bind_method(D_METHOD("set_off_submodel", "p_model"), &E3DModelLightDefinition::set_off_submodel); + ClassDB::bind_method(D_METHOD("get_off_submodel"), &E3DModelLightDefinition::get_off_submodel); + ADD_PROPERTY( + PropertyInfo(Variant::OBJECT, "off_submodel", PROPERTY_HINT_RESOURCE_TYPE, "E3DSubModel"), + "set_off_submodel", "get_off_submodel"); + + ClassDB::bind_method( + D_METHOD("set_spotlight_submodel", "p_model"), &E3DModelLightDefinition::set_spotlight_submodel); + ClassDB::bind_method(D_METHOD("get_spotlight_submodel"), &E3DModelLightDefinition::get_spotlight_submodel); + ADD_PROPERTY( + PropertyInfo(Variant::OBJECT, "spotlight_submodel", PROPERTY_HINT_RESOURCE_TYPE, "E3DSubModel"), + "set_spotlight_submodel", "get_spotlight_submodel"); + } + +} // namespace godot diff --git a/src/e3d/E3DModelLightDefinition.hpp b/src/e3d/E3DModelLightDefinition.hpp new file mode 100644 index 00000000..44da87ff --- /dev/null +++ b/src/e3d/E3DModelLightDefinition.hpp @@ -0,0 +1,18 @@ +#pragma once +#include +#include + +#include "E3DSubModel.hpp" +#include "macros.hpp" + +namespace godot { + class E3DModelLightDefinition : public Resource { + GDCLASS(E3DModelLightDefinition, Resource) + + MAKE_MEMBER_GS_NR_NO_DEF(Ref, on_submodel) + MAKE_MEMBER_GS_NR_NO_DEF(Ref, off_submodel) + MAKE_MEMBER_GS_NR_NO_DEF(Ref, spotlight_submodel) + protected: + static void _bind_methods(); + }; +} // namespace godot diff --git a/src/e3d/E3DSubModel.cpp b/src/e3d/E3DSubModel.cpp index 08ab028c..ce8ac581 100644 --- a/src/e3d/E3DSubModel.cpp +++ b/src/e3d/E3DSubModel.cpp @@ -128,6 +128,35 @@ namespace godot { BIND_PROPERTY( Variant::BOOL, "skip_rendering", "skip_rendering", &E3DSubModel::set_skip_rendering, &E3DSubModel::get_skip_rendering, "p_skip_rendering"); + + BIND_PROPERTY( + Variant::FLOAT, "light_range", "light_range", &E3DSubModel::set_light_range, + &E3DSubModel::get_light_range, "p_light_range"); + BIND_PROPERTY( + Variant::FLOAT, "light_attenuation", "light_attenuation", &E3DSubModel::set_light_attenuation, + &E3DSubModel::get_light_attenuation, "p_light_attenuation"); + BIND_PROPERTY( + Variant::FLOAT, "light_angle", "light_angle", &E3DSubModel::set_light_angle, + &E3DSubModel::get_light_angle, "p_light_angle"); + BIND_PROPERTY( + Variant::FLOAT, "near_attenuation_start", "near_attenuation_start", + &E3DSubModel::set_near_attenuation_start, &E3DSubModel::get_near_attenuation_start, + "p_near_attenuation_start"); + BIND_PROPERTY( + Variant::FLOAT, "near_attenuation_end", "near_attenuation_end", &E3DSubModel::set_near_attenuation_end, + &E3DSubModel::get_near_attenuation_end, "p_near_attenuation_end"); + BIND_PROPERTY( + Variant::BOOL, "use_near_attenuation", "use_near_attenuation", &E3DSubModel::set_use_near_attenuation, + &E3DSubModel::get_use_near_attenuation, "p_use_near_attenuation"); + BIND_PROPERTY( + Variant::INT, "far_attenuation_decay", "far_attenuation_decay", &E3DSubModel::set_far_attenuation_decay, + &E3DSubModel::get_far_attenuation_decay, "p_far_attenuation_decay"); + BIND_PROPERTY( + Variant::FLOAT, "cos_hotspot_angle", "cos_hotspot_angle", &E3DSubModel::set_cos_hotspot_angle, + &E3DSubModel::get_cos_hotspot_angle, "p_cos_hotspot_angle"); + BIND_PROPERTY( + Variant::FLOAT, "cos_view_angle", "cos_view_angle", &E3DSubModel::set_cos_view_angle, + &E3DSubModel::get_cos_view_angle, "p_cos_view_angle"); } void E3DSubModel::add_child(const Ref &p_sub_model) { diff --git a/src/e3d/E3DSubModel.hpp b/src/e3d/E3DSubModel.hpp index aff061e4..b3dcf54d 100644 --- a/src/e3d/E3DSubModel.hpp +++ b/src/e3d/E3DSubModel.hpp @@ -80,6 +80,16 @@ namespace godot { MAKE_MEMBER_GS_NR(bool, visible, true) MAKE_MEMBER_GS_NR(bool, skip_rendering, false) + MAKE_MEMBER_GS_NR(float, light_range, 0.0) + MAKE_MEMBER_GS_NR(float, light_attenuation, 1.0) + MAKE_MEMBER_GS_NR(float, light_angle, 45.0) + MAKE_MEMBER_GS_NR(float, near_attenuation_start, 0.0) + MAKE_MEMBER_GS_NR(float, near_attenuation_end, 0.0) + MAKE_MEMBER_GS_NR(bool, use_near_attenuation, false) + MAKE_MEMBER_GS_NR(int, far_attenuation_decay, 0) + MAKE_MEMBER_GS_NR(float, cos_hotspot_angle, 0.0) + MAKE_MEMBER_GS_NR(float, cos_view_angle, 0.0) + void add_child(const Ref &p_sub_model); void set_parent(E3DSubModel *p_sub_model); void clear(); diff --git a/src/lighting/TrainLighting.cpp b/src/lighting/TrainLighting.cpp index d215d61f..1e941a2d 100644 --- a/src/lighting/TrainLighting.cpp +++ b/src/lighting/TrainLighting.cpp @@ -32,7 +32,7 @@ namespace godot { BIND_PROPERTY_W_HINT_RES_ARRAY( Variant::ARRAY, "light_position_list", "lights/list", &TrainLighting::set_light_position_list, &TrainLighting::get_light_position_list, "light_position_list", PROPERTY_HINT_TYPE_STRING, - "LighListItem"); + "LightListItem"); BIND_PROPERTY_W_HINT( Variant::INT, "light_source", "light/source", &TrainLighting::set_light_source, &TrainLighting::get_light_source, "source", PROPERTY_HINT_ENUM, diff --git a/src/parsers/e3d_parser.cpp b/src/parsers/e3d_parser.cpp index 964b8912..63049aad 100644 --- a/src/parsers/e3d_parser.cpp +++ b/src/parsers/e3d_parser.cpp @@ -1,13 +1,20 @@ +#include "e3d/E3DModelLightDefinition.hpp" #include "parsers/e3d_parser.hpp" #include #include #include #include +#include #include +#include #include namespace godot { + static constexpr const char *LIGHT_ON_SUFFIX = "_on"; + static constexpr const char *LIGHT_ON_ALT_SUFFIX = "_xon"; + static constexpr const char *LIGHT_OFF_SUFFIX = "_off"; + void E3DParser::_bind_methods() { ClassDB::bind_method(D_METHOD("parse", "file"), &E3DParser::parse); } @@ -86,15 +93,18 @@ namespace godot { result.visibility_light_threshold = p_file->get_float(); // ReSharper disable once CppExpressionWithoutSideEffects p_file->get_buffer(16); // skip unused RGBA ambient - Color diffuse_color = Color(p_file->get_float(), p_file->get_float(), p_file->get_float(), 1.0); - // ReSharper disable once CppExpressionWithoutSideEffects - p_file->get_float(); // skip unused alpha + const float diffuse_r = p_file->get_float(); + const float diffuse_g = p_file->get_float(); + const float diffuse_b = p_file->get_float(); + const float diffuse_a = p_file->get_float(); + Color diffuse_color = Color(diffuse_r, diffuse_g, diffuse_b, diffuse_a); // ReSharper disable once CppExpressionWithoutSideEffects p_file->get_buffer(16); // skip unused RGBA specular - Color selfillum_color = Color(p_file->get_float(), p_file->get_float(), p_file->get_float(), 1.0); - - // ReSharper disable once CppExpressionWithoutSideEffects - p_file->get_float(); // skip unused alpha + const float selfillum_r = p_file->get_float(); + const float selfillum_g = p_file->get_float(); + const float selfillum_b = p_file->get_float(); + const float selfillum_a = p_file->get_float(); + Color selfillum_color = Color(selfillum_r, selfillum_g, selfillum_b, selfillum_a); if (const auto transparent = result.flags & 32; transparent == 0u) { selfillum_color.a = 1.0; diffuse_color.a = 1.0; @@ -106,8 +116,22 @@ namespace godot { result.lod_max_distance = p_file->get_float(); result.lod_min_distance = p_file->get_float(); - // ReSharper disable once CppExpressionWithoutSideEffects - p_file->get_buffer(32); // skip attrs for lighting + result.near_attenuation_start = p_file->get_float(); + result.near_attenuation_end = p_file->get_float(); + result.use_near_attenuation = p_file->get_32() != 0; + result.far_attenuation_decay = static_cast(p_file->get_32()); + result.light_range = p_file->get_float(); // fFarDecayRadius + const float cos_falloff = p_file->get_float(); // fCosFalloffAngle + result.cos_hotspot_angle = p_file->get_float(); + result.cos_view_angle = p_file->get_float(); + + result.light_angle = Math::rad_to_deg(Math::acos(Math::clamp(cos_falloff, -1.0f, 1.0f))); + // spot_attenuation in Godot controls BOTH distance and angular attenuation (softness). + // Since E3D has iFarAttenDecay for distance and a hotspot/falloff for angle, + // we'll leave it at 1.0 here and let the instancer decide based on all params. + result.light_attenuation = 1.0f; + result.diffuse_color = diffuse_color; + result.index_count = p_file->get_32(); result.first_index_idx = p_file->get_32(); result.transparent = result.flags & 0b000001; @@ -438,10 +462,23 @@ namespace godot { submodel->set_transform(p_submodel.matrix); return submodel; } + case E3DSubModel::SubModelType::SUBMODEL_FREE_SPOTLIGHT: + submodel->set_light_range(p_submodel.light_range); + submodel->set_light_attenuation(p_submodel.light_attenuation); + submodel->set_light_angle(p_submodel.light_angle); + submodel->set_near_attenuation_start(p_submodel.near_attenuation_start); + submodel->set_near_attenuation_end(p_submodel.near_attenuation_end); + submodel->set_use_near_attenuation(p_submodel.use_near_attenuation); + submodel->set_far_attenuation_decay(p_submodel.far_attenuation_decay); + submodel->set_cos_hotspot_angle(p_submodel.cos_hotspot_angle); + submodel->set_cos_view_angle(p_submodel.cos_view_angle); + submodel->set_transform(p_submodel.matrix); + submodel->set_diffuse_color(p_submodel.diffuse_color); + return submodel; default: UtilityFunctions::push_error( "Unsupported submodel (name: " + p_submodel.name + ", type: " + String::num(p_submodel.type) + - ")"); //@TODO Display type name based on p_sumbodel.type + ")"); //@TODO Display type name based on p_submodel.type return submodel; } } @@ -486,6 +523,107 @@ namespace godot { } } + _register_lights(model, submodels, submodels_meta); + return model; } + + void E3DParser::_register_lights( + const Ref &p_model, const std::vector> &p_submodels, + const std::vector &p_meta) const { + + std::unordered_set used_off; + + static const std::unordered_set allowed_light_names = { + "endsignal12", "endsignal13", "endsignal22", "endsignal23", "endsignals1", "endsignals2", + "endtab1", "endtab2", "headlamp11", "headlamp12", "headlamp13", "highbeam12", + "highbeam13", "headlamp21", "headlamp22", "headlamp23", "highbeam22", "highbeam23", + "headsignal12", "headsignal13", "headsignal22", "headsignal23"}; + + for (size_t i = 0; i < p_submodels.size(); i++) { + const Ref &sm = p_submodels[i]; + String sm_name = sm->get_name(); + if (sm_name.is_empty()) { + continue; + } + + String sm_name_lower = sm_name.to_lower(); + String base_name; + + if (sm_name_lower.ends_with(LIGHT_ON_SUFFIX)) { + base_name = sm_name.substr(0, sm_name.length() - String(LIGHT_ON_SUFFIX).length()); + } else if (sm_name_lower.ends_with(LIGHT_ON_ALT_SUFFIX)) { + base_name = sm_name.substr(0, sm_name.length() - String(LIGHT_ON_ALT_SUFFIX).length()); + } else { + continue; + } + + if (base_name.is_empty()) { + continue; + } + + String base_name_lower = base_name.to_lower(); + if (allowed_light_names.find(base_name_lower.utf8().get_data()) == allowed_light_names.end()) { + continue; + } + + String light_name = base_name; + + if (p_model->get_lights().has(light_name)) { + UtilityFunctions::push_error("[E3DParser]: Duplicate light name: " + light_name); + continue; + } + + Ref entry; + entry.instantiate(); + entry->set_on_submodel(sm); + + // look for matching _off sibling + String off_name = base_name + LIGHT_OFF_SUFFIX; + String off_name_lower = off_name.to_lower(); + Ref found_off; + + for (size_t j = 0; j < p_submodels.size(); j++) { + if (used_off.find(j) == used_off.end() && p_submodels[j]->get_name().to_lower() == off_name_lower) { + found_off = p_submodels[j]; + used_off.insert(j); + break; + } + } + if (found_off.is_valid()) { + entry->set_off_submodel(found_off); + } + + // look for FREE_SPOTLIGHT child within this submodel's children + Ref found_spotlight; + const TypedArray &children = sm->get_submodels(); + for (const auto &c: children) { + Ref child = c; + if (child.is_valid() && + child->get_submodel_type() == E3DSubModel::SubModelType::SUBMODEL_FREE_SPOTLIGHT) { + found_spotlight = child; + break; + } + } + + // also check if the _on submodel itself is a parent with spotlight among all submodels + if (!found_spotlight.is_valid() && p_meta[i].first_child_idx > -1) { + int child_idx = p_meta[i].first_child_idx; + while (child_idx > -1 && static_cast(child_idx) < p_submodels.size()) { + if (p_submodels[child_idx]->get_submodel_type() == + E3DSubModel::SubModelType::SUBMODEL_FREE_SPOTLIGHT) { + found_spotlight = p_submodels[child_idx]; + break; + } + child_idx = p_meta[child_idx].next_idx; + } + } + + if (found_spotlight.is_valid()) { + entry->set_spotlight_submodel(found_spotlight); + } + + p_model->register_light(light_name, entry); + } + } } // namespace godot diff --git a/src/parsers/e3d_parser.hpp b/src/parsers/e3d_parser.hpp index 5265d333..a5fac9f4 100644 --- a/src/parsers/e3d_parser.hpp +++ b/src/parsers/e3d_parser.hpp @@ -69,6 +69,15 @@ namespace godot { float gl_lines_size; float lod_max_distance; float lod_min_distance; + float light_range; + float light_attenuation; + float light_angle; + float near_attenuation_start; + float near_attenuation_end; + bool use_near_attenuation; + int far_attenuation_decay; + float cos_hotspot_angle; + float cos_view_angle; uint32_t index_count; uint32_t first_index_idx; uint32_t transparent; @@ -89,5 +98,8 @@ namespace godot { std::vector _buffer_to_strings(const PackedByteArray &p_buffer) const; Transform3D _read_matrix(const Ref &p_file) const; Ref _create_submodel(SubModelData &p_submodel) const; + void _register_lights( + const Ref &p_model, const std::vector> &p_submodels, + const std::vector &p_meta) const; }; } // namespace godot diff --git a/src/register_types.cpp b/src/register_types.cpp index 39521d05..97a730a7 100644 --- a/src/register_types.cpp +++ b/src/register_types.cpp @@ -11,6 +11,7 @@ #include "core/UserSettings.hpp" #include "doors/TrainDoors.hpp" #include "e3d/E3DModel.hpp" +#include "e3d/E3DModelLightDefinition.hpp" #include "e3d/E3DSubModel.hpp" #include "engines/TrainDieselElectricEngine.hpp" #include "engines/TrainDieselEngine.hpp" @@ -59,6 +60,7 @@ void initialize_libmaszyna_module(const ModuleInitializationLevel p_level) { GDREGISTER_CLASS(E3DSubModel); GDREGISTER_CLASS(E3DModel); GDREGISTER_CLASS(E3DParser); + GDREGISTER_CLASS(E3DModelLightDefinition); GDREGISTER_CLASS(E3DResourceFormatLoader); GDREGISTER_CLASS(MaszynaParser); GDREGISTER_CLASS(OggVorbisFormatLoader); From 30d30ce1b7b1d57b923318e510cc8677a3942c22 Mon Sep 17 00:00:00 2001 From: Marcin Nowak Date: Mon, 25 May 2026 01:34:32 +0200 Subject: [PATCH 2/7] instantiate lights and toggle via E3DModelInstance.lights_state --- addons/libmaszyna/e3d/e3d_model_instance.gd | 21 +++- addons/libmaszyna/e3d/e3d_nodes_instancer.gd | 110 +++++++++++++------ demo/vehicles/sm42/sm_42.tscn | 32 ++++++ src/e3d/E3DModelLightDefinition.cpp | 7 -- src/e3d/E3DModelLightDefinition.hpp | 1 - src/parsers/e3d_parser.cpp | 36 ++---- 6 files changed, 133 insertions(+), 74 deletions(-) diff --git a/addons/libmaszyna/e3d/e3d_model_instance.gd b/addons/libmaszyna/e3d/e3d_model_instance.gd index d00d7886..3fa325db 100644 --- a/addons/libmaszyna/e3d/e3d_model_instance.gd +++ b/addons/libmaszyna/e3d/e3d_model_instance.gd @@ -29,7 +29,12 @@ var _e3d_loaded: bool = false var _current_instancer: E3DInstancer var _current_editable: bool = false -var _lights_state: Dictionary[String, bool] = {} +@export var lights_state: Dictionary[String, bool] = {}: + set(x): + lights_state = x + if is_inside_tree(): + _current_instancer.sync_lights(self) + var default_aabb_size: Vector3 = Vector3(1, 1, 1) @@ -148,15 +153,19 @@ func is_e3d_loaded() -> bool: func get_lights_state() -> Dictionary[String, bool]: - return _lights_state + return lights_state func set_light_enabled(light_name: String, state: bool) -> void: - if _lights_state.has(light_name): - if _lights_state[light_name] == state: + if not _model.has_light(light_name): + push_error("Light not found: "+light_name) + return + + if lights_state.has(light_name): + if lights_state[light_name] == state: return - - _lights_state[light_name] = state + + lights_state[light_name] = state if _current_instancer: _current_instancer.sync_lights(self) diff --git a/addons/libmaszyna/e3d/e3d_nodes_instancer.gd b/addons/libmaszyna/e3d/e3d_nodes_instancer.gd index 9b8167d7..da114d18 100644 --- a/addons/libmaszyna/e3d/e3d_nodes_instancer.gd +++ b/addons/libmaszyna/e3d/e3d_nodes_instancer.gd @@ -1,44 +1,80 @@ @tool extends E3DInstancer +class LightNodeInfo: + var light_name: String = "" + var light_on_node:NodePath = NodePath("") + var light_off_node:NodePath = NodePath("") + var spotlight_node:NodePath = NodePath("") + +var instances_lights : Dictionary = {} + func instantiate(target_node: E3DModelInstance, model: E3DModel, editable: bool = false) -> void: - var lights: Array = [] - var light_node_list: Dictionary[String, NodePath] = {} #NodePath -> custom class + var lights: Array = [] # first pass _do_add_submodels(target_node, model, target_node, model.submodels, editable, lights) - - var lights_state: Dictionary[String, bool] = target_node.get_lights_state() - var parsed_lights: Dictionary = model.get_lights() - for light_name: String in parsed_lights: - if not lights_state.has(light_name): - lights_state[light_name] = false - + + # prebuild LightNodeInfo dictionary + var _instance_lights:Dictionary = {} + var _spotlights = [] + + for entry in lights: + var light_name = entry[0] + var entry_type = entry[1] + var node = entry[2] + if light_name: + var light_info:LightNodeInfo = _instance_lights.get(light_name) + if not light_info: + light_info = LightNodeInfo.new() + light_info.light_name = light_name + _instance_lights[light_name] = light_info + match entry_type: + "on": + light_info.light_on_node = node.get_path() + "off": + light_info.light_off_node = node.get_path() + else: + if entry_type == "spotlight": + _spotlights.append(node) + + var _light_on_nodes = _instance_lights.values().map(func(x): return x.light_on_node) + + # assign instance lights to the global registry + instances_lights[target_node] = _instance_lights + + for spotlight:SpotLight3D in _spotlights: + while true: + var parent = spotlight.get_parent() + if not parent: + break + var found_idx = _light_on_nodes.find(parent.get_path()) + if found_idx > -1: + _instance_lights.values()[found_idx].spotlight_node = spotlight.get_path() + break + sync_lights(target_node) - print(target_node.get_lights_state()) + #print(target_node.get_lights_state()) func sync_lights(target_node: E3DModelInstance) -> void: - var all_nodes: Array[Node] = target_node.find_children("*", "Node3D", true, true) - var node_map: Dictionary = {} - for node: Node in all_nodes: - node_map[node.name.to_lower()] = node - - var lights_state: Dictionary[String, bool] = target_node.get_lights_state() - for light_name: String in lights_state: - var state: bool = lights_state[light_name] - var base_name: String = light_name.to_lower() - - var on_suffixes: Array[String] = ["_on", "_xon"] - for suffix: String in on_suffixes: - var on_node: Node = node_map.get(base_name + suffix) - if on_node is Node3D: - (on_node as Node3D).visible = state - - var off_node: Node = node_map.get(base_name + "_off") - if off_node is Node3D: - (off_node as Node3D).visible = not state + var state = target_node.get_lights_state() + for light_name in state.keys(): + var light_info:LightNodeInfo = instances_lights[target_node].get(light_name) + if light_info: + var enabled = state[light_name] + var on_node = target_node.get_node_or_null(light_info.light_on_node) + var off_node = target_node.get_node_or_null(light_info.light_off_node) + var spotlight:SpotLight3D = target_node.get_node_or_null(light_info.spotlight_node) + + if on_node: + on_node.visible = enabled + if off_node: + off_node.visible = not enabled + if spotlight: + spotlight.visible = enabled func clear(target_node: E3DModelInstance) -> void: + instances_lights.erase(target_node) for child: Node in target_node.get_children(true): target_node.remove_child(child) child.queue_free() @@ -58,9 +94,20 @@ func _do_add_submodels( ) -> void: for submodel in submodels: if _is_submodel_valid(target_node, submodel): - var child:Node = _create_submodel_instance(target_node, submodel, model) + var child:Node = _create_submodel_instance(target_node, submodel, model, lights) if not child: continue + if ( + submodel.resource_name.ends_with("_on") + or submodel.resource_name.ends_with("_off") + or submodel.resource_name.ends_with("_xon") + ): + for light_name in model.lights.keys(): + var light_info:E3DModelLightDefinition = model.lights[light_name] + if submodel == light_info.on_submodel: + lights.append([light_name, "on", child]) + elif submodel == light_info.off_submodel: + lights.append([light_name, "off", child]) _update_submodel_material(target_node, child, submodel) var internal = InternalMode.INTERNAL_MODE_DISABLED if editable else InternalMode.INTERNAL_MODE_BACK @@ -83,7 +130,7 @@ func _do_add_submodels( _do_add_submodels(target_node, model, child, submodel.submodels, editable, lights) -func _create_submodel_instance(target_node: E3DModelInstance, submodel: E3DSubModel, model: E3DModel): +func _create_submodel_instance(target_node: E3DModelInstance, submodel: E3DSubModel, model: E3DModel, lights): var obj match submodel.submodel_type: @@ -109,6 +156,7 @@ func _create_submodel_instance(target_node: E3DModelInstance, submodel: E3DSubMo obj.spot_angle = submodel.light_angle obj.spot_attenuation = submodel.light_attenuation obj.distance_fade_begin = submodel.near_attenuation_start + lights.append([null, "spotlight", obj]) if obj: obj.visible = submodel.visible diff --git a/demo/vehicles/sm42/sm_42.tscn b/demo/vehicles/sm42/sm_42.tscn index 8b852858..1fb86538 100644 --- a/demo/vehicles/sm42/sm_42.tscn +++ b/demo/vehicles/sm42/sm_42.tscn @@ -34,6 +34,18 @@ visible = true visibility_parent = NodePath("") layers = 1 script = ExtResource("4_c88oy") +lights_state = Dictionary[String, bool]({ +"endsignal12": false, +"endsignal13": false, +"endsignal22": false, +"endsignal23": false, +"headlamp11": false, +"headlamp12": true, +"headlamp13": true, +"headlamp21": false, +"headlamp22": false, +"headlamp23": false +}) data_path = "/dynamic/pkp/sm42_v1" model_filename = "6da" skins = ["6d-907"] @@ -85,6 +97,10 @@ visible = true visibility_parent = NodePath("") layers = 1 script = ExtResource("4_c88oy") +lights_state = Dictionary[String, bool]({ +"EndTab1": false, +"EndTab2": false +}) data_path = "/dynamic/pkp/401w_v2" model_filename = "main/401w" skins = ["5217,1", "5217,2"] @@ -107,6 +123,10 @@ visible = true visibility_parent = NodePath("") layers = 1 script = ExtResource("4_c88oy") +lights_state = Dictionary[String, bool]({ +"EndTab1": false, +"EndTab2": false +}) data_path = "/dynamic/pkp/401w_v2" model_filename = "main/401w" skins = ["5217,1", "5217,2"] @@ -129,6 +149,10 @@ visible = true visibility_parent = NodePath("") layers = 1 script = ExtResource("4_c88oy") +lights_state = Dictionary[String, bool]({ +"EndTab1": false, +"EndTab2": false +}) data_path = "/dynamic/pkp/401w_v2" model_filename = "main/401w" skins = ["5217,1", "5217,2"] @@ -151,6 +175,10 @@ visible = true visibility_parent = NodePath("") layers = 1 script = ExtResource("4_c88oy") +lights_state = Dictionary[String, bool]({ +"EndTab1": false, +"EndTab2": false +}) data_path = "/dynamic/pkp/401w_v2" model_filename = "main/401w" skins = ["5217,1", "5217,2"] @@ -173,6 +201,10 @@ visible = true visibility_parent = NodePath("") layers = 1 script = ExtResource("4_c88oy") +lights_state = Dictionary[String, bool]({ +"EndTab1": false, +"EndTab2": false +}) data_path = "/dynamic/pkp/401w_v2" model_filename = "main/401w" skins = ["5217,1", "5217,2"] diff --git a/src/e3d/E3DModelLightDefinition.cpp b/src/e3d/E3DModelLightDefinition.cpp index 6ab6ab7b..f9494b43 100644 --- a/src/e3d/E3DModelLightDefinition.cpp +++ b/src/e3d/E3DModelLightDefinition.cpp @@ -13,13 +13,6 @@ namespace godot { ADD_PROPERTY( PropertyInfo(Variant::OBJECT, "off_submodel", PROPERTY_HINT_RESOURCE_TYPE, "E3DSubModel"), "set_off_submodel", "get_off_submodel"); - - ClassDB::bind_method( - D_METHOD("set_spotlight_submodel", "p_model"), &E3DModelLightDefinition::set_spotlight_submodel); - ClassDB::bind_method(D_METHOD("get_spotlight_submodel"), &E3DModelLightDefinition::get_spotlight_submodel); - ADD_PROPERTY( - PropertyInfo(Variant::OBJECT, "spotlight_submodel", PROPERTY_HINT_RESOURCE_TYPE, "E3DSubModel"), - "set_spotlight_submodel", "get_spotlight_submodel"); } } // namespace godot diff --git a/src/e3d/E3DModelLightDefinition.hpp b/src/e3d/E3DModelLightDefinition.hpp index 44da87ff..0afddf8a 100644 --- a/src/e3d/E3DModelLightDefinition.hpp +++ b/src/e3d/E3DModelLightDefinition.hpp @@ -11,7 +11,6 @@ namespace godot { MAKE_MEMBER_GS_NR_NO_DEF(Ref, on_submodel) MAKE_MEMBER_GS_NR_NO_DEF(Ref, off_submodel) - MAKE_MEMBER_GS_NR_NO_DEF(Ref, spotlight_submodel) protected: static void _bind_methods(); }; diff --git a/src/parsers/e3d_parser.cpp b/src/parsers/e3d_parser.cpp index 63049aad..ec672e9f 100644 --- a/src/parsers/e3d_parser.cpp +++ b/src/parsers/e3d_parser.cpp @@ -534,6 +534,13 @@ namespace godot { std::unordered_set used_off; + /* FIXME: the logic should be opoosite. Here should be a list of non-lights names with _on/_off suffixes + * which should be EXCLUDED; everything else with _on/_off/_xon should be treated as light. + * + * The names listed below are only well-known lights used in vehicles. Any other lights will be unsupported, + * which is wrong. + * */ + static const std::unordered_set allowed_light_names = { "endsignal12", "endsignal13", "endsignal22", "endsignal23", "endsignals1", "endsignals2", "endtab1", "endtab2", "headlamp11", "headlamp12", "headlamp13", "highbeam12", @@ -594,35 +601,6 @@ namespace godot { entry->set_off_submodel(found_off); } - // look for FREE_SPOTLIGHT child within this submodel's children - Ref found_spotlight; - const TypedArray &children = sm->get_submodels(); - for (const auto &c: children) { - Ref child = c; - if (child.is_valid() && - child->get_submodel_type() == E3DSubModel::SubModelType::SUBMODEL_FREE_SPOTLIGHT) { - found_spotlight = child; - break; - } - } - - // also check if the _on submodel itself is a parent with spotlight among all submodels - if (!found_spotlight.is_valid() && p_meta[i].first_child_idx > -1) { - int child_idx = p_meta[i].first_child_idx; - while (child_idx > -1 && static_cast(child_idx) < p_submodels.size()) { - if (p_submodels[child_idx]->get_submodel_type() == - E3DSubModel::SubModelType::SUBMODEL_FREE_SPOTLIGHT) { - found_spotlight = p_submodels[child_idx]; - break; - } - child_idx = p_meta[child_idx].next_idx; - } - } - - if (found_spotlight.is_valid()) { - entry->set_spotlight_submodel(found_spotlight); - } - p_model->register_light(light_name, entry); } } From 2d87b5d72a18697ac2f58ef956420a5e3aeb84f7 Mon Sep 17 00:00:00 2001 From: Marcin Nowak Date: Mon, 25 May 2026 01:58:34 +0200 Subject: [PATCH 3/7] little fixes/tuning by guess --- addons/libmaszyna/e3d/e3d_nodes_instancer.gd | 7 +++++-- demo/demo_3d.tscn | 7 +++++-- demo/vehicles/sm42/sm_42.tscn | 2 +- 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/addons/libmaszyna/e3d/e3d_nodes_instancer.gd b/addons/libmaszyna/e3d/e3d_nodes_instancer.gd index da114d18..334f4287 100644 --- a/addons/libmaszyna/e3d/e3d_nodes_instancer.gd +++ b/addons/libmaszyna/e3d/e3d_nodes_instancer.gd @@ -117,7 +117,7 @@ func _do_add_submodels( # Applying transform before adding may cause issues (especially on windows) if child is Node3D and submodel.transform: var child_node:Node3D = child as Node3D - if child is SpotLight3D: + if submodel.submodel_type == E3DSubModel.SUBMODEL_FREE_SPOTLIGHT: # Do not scale SpotLight3D to avoid configuration warnings child_node.position = submodel.transform.origin child_node.basis = submodel.transform.basis.orthonormalized() @@ -149,7 +149,10 @@ func _create_submodel_instance(target_node: E3DModelInstance, submodel: E3DSubMo obj = SpotLight3D.new() obj.name = submodel.resource_name obj.light_color = submodel.diffuse_color - obj.light_energy = submodel.visibility_light + obj.light_color.a = 1.0 + #obj.light_energy = submodel.visibility_light # ???? + obj.light_energy = 10.0 # FIXME: guessing + obj.light_volumetric_fog_energy = 4.0 # FIXME: guessing obj.shadow_enabled = true obj.distance_fade_enabled = true obj.spot_range = submodel.light_range diff --git a/demo/demo_3d.tscn b/demo/demo_3d.tscn index b950577a..9095fc24 100644 --- a/demo/demo_3d.tscn +++ b/demo/demo_3d.tscn @@ -53,9 +53,9 @@ glow_enabled = true fog_enabled = true fog_sky_affect = 0.4 volumetric_fog_enabled = true -volumetric_fog_density = 0.029 +volumetric_fog_density = 0.01 volumetric_fog_anisotropy = 0.0 -volumetric_fog_length = 0.4 +volumetric_fog_length = 55.89 volumetric_fog_detail_spread = 1.0 volumetric_fog_sky_affect = 0.405 adjustment_enabled = true @@ -233,6 +233,8 @@ visible = null [node name="MaszynaEnvironmentNode" type="Node" parent="." unique_id=1464463242] script = ExtResource("2_py6pt") world_environment = NodePath("../WorldEnvironment") +season = 2 +weather = 0 sky_texture_offset = Vector2(0.49, 0.005) sky_texture_scale = Vector2(0.547, 0.865) sky_energy = 0.85 @@ -248,6 +250,7 @@ size = Vector3(5000, 0.1, 5000) [node name="DirectionalLight3D" type="DirectionalLight3D" parent="." unique_id=786641912] transform = Transform3D(-0.846557, 0.01871, 0.531969, 0.393589, 0.694835, 0.601906, -0.358369, 0.718925, -0.595582, 5.83027, -6.14961, 0) +visible = false light_color = Color(0.929688, 0.848771, 0.817108, 1) light_energy = 1.12 light_indirect_energy = 2.0 diff --git a/demo/vehicles/sm42/sm_42.tscn b/demo/vehicles/sm42/sm_42.tscn index 1fb86538..ad09793d 100644 --- a/demo/vehicles/sm42/sm_42.tscn +++ b/demo/vehicles/sm42/sm_42.tscn @@ -39,7 +39,7 @@ lights_state = Dictionary[String, bool]({ "endsignal13": false, "endsignal22": false, "endsignal23": false, -"headlamp11": false, +"headlamp11": true, "headlamp12": true, "headlamp13": true, "headlamp21": false, From cf2a1eab2abaefea049616e23ff39c13651e78c9 Mon Sep 17 00:00:00 2001 From: Marcin Nowak Date: Mon, 25 May 2026 09:39:14 +0200 Subject: [PATCH 4/7] Refactor MaterialManager to support more options (like e3d submodel selfillum) --- addons/libmaszyna/e3d/e3d_instancer.gd | 10 ++-- .../libmaszyna/materials/material_factory.gd | 19 +++---- .../libmaszyna/materials/material_manager.gd | 49 +++++++++++-------- 3 files changed, 42 insertions(+), 36 deletions(-) diff --git a/addons/libmaszyna/e3d/e3d_instancer.gd b/addons/libmaszyna/e3d/e3d_instancer.gd index e3a91f8f..9a95ddbc 100644 --- a/addons/libmaszyna/e3d/e3d_instancer.gd +++ b/addons/libmaszyna/e3d/e3d_instancer.gd @@ -43,6 +43,11 @@ func _is_submodel_valid(target_node: E3DModelInstance, submodel: E3DSubModel) -> func _get_material_override(target_node: E3DModelInstance, submodel: E3DSubModel) -> Material: var unprefixed_model_path: String = "/".join(target_node.data_path.split("/").slice(1)) + var options = MaterialManager.MaterialOptions.new() + + # TODO: handle more material options here (selfillum, diffuse_color, etc) + options.force_transparent = submodel.material_transparent + if submodel.dynamic_material: if target_node.skins.size() < submodel.dynamic_material_index + 1: push_warning( @@ -57,7 +62,7 @@ func _get_material_override(target_node: E3DModelInstance, submodel: E3DSubModel else MaterialManager.Transparency.Disabled ) var skin: Variant = target_node.skins[submodel.dynamic_material_index] - return MaterialManager.get_material(unprefixed_model_path, skin, dynamic_transparency) + return MaterialManager.get_material(unprefixed_model_path, skin, options) if submodel.material_colored: return _colored_material @@ -71,8 +76,7 @@ func _get_material_override(target_node: E3DModelInstance, submodel: E3DSubModel return MaterialManager.get_material( unprefixed_model_path, submodel.material_name, - named_transparency, - submodel.diffuse_color, + options, ) return null diff --git a/addons/libmaszyna/materials/material_factory.gd b/addons/libmaszyna/materials/material_factory.gd index d5c42884..95e72039 100644 --- a/addons/libmaszyna/materials/material_factory.gd +++ b/addons/libmaszyna/materials/material_factory.gd @@ -81,8 +81,7 @@ func create( model_path: String = "", season: MaterialManager.Season = MaterialManager.Season.SEASON_SUMMER, weather: MaterialManager.Weather = MaterialManager.Weather.WEATHER_CLEAR, - force_transparent: bool = false, - diffuse_color: Color = Color.WHITE + options: MaterialManager.MaterialOptions = MaterialManager.MaterialOptions.new(), ) -> Material: var variant: MaszynaMaterial.MaszynaMaterialVariant = mmat.get_variant(season, weather) var shader_meta:MaszynaShaderMeta = _get_shader_meta(variant.shader) @@ -94,8 +93,7 @@ func create( shader_meta, shader_meta.texture_map, model_path, - force_transparent, - diffuse_color, + options, ) return material @@ -106,8 +104,7 @@ func apply( model_path: String, season: MaterialManager.Season, weather: MaterialManager.Weather, - force_transparent: bool, - diffuse_color: Color = Color.WHITE + options: MaterialManager.MaterialOptions = MaterialManager.MaterialOptions.new(), ) -> void: var variant: MaszynaMaterial.MaszynaMaterialVariant = mmat.get_variant(season, weather) var shader_meta:MaszynaShaderMeta = _get_shader_meta(variant.shader) @@ -118,8 +115,7 @@ func apply( shader_meta, shader_meta.texture_map, model_path, - force_transparent, - diffuse_color + options, ) @@ -138,8 +134,7 @@ func _apply( shader_meta: MaszynaShaderMeta, texture_map: TextureMap, model_path: String, - force_transparent: bool, - diffuse_color: Color, + options: MaterialManager.MaterialOptions, ) -> void: if not material is ShaderMaterial or not shader_meta.base_material is ShaderMaterial: @@ -151,9 +146,9 @@ func _apply( if property_name == "shader" or property_name == "render_priority" or property_name.begins_with("shader_parameter/"): target_shader_material.set(property_name, source_shader_material.get(property_name)) - shader_meta.factory.call(mmat, variant, material, texture_map, model_path, diffuse_color) + shader_meta.factory.call(mmat, variant, material, texture_map, model_path, options.diffuse_color) var transparency: MaterialManager.Transparency = MaterialManager.Transparency.Disabled - if mmat.transparent or force_transparent: + if mmat.transparent or options.force_transparent: transparency = MaterialManager.Transparency.AlphaScissor target_shader_material.set_shader_parameter("transparency", transparency) target_shader_material.set_shader_parameter("alpha_scissor_threshold", 0.5) diff --git a/addons/libmaszyna/materials/material_manager.gd b/addons/libmaszyna/materials/material_manager.gd index f0284dce..6fe6a195 100644 --- a/addons/libmaszyna/materials/material_manager.gd +++ b/addons/libmaszyna/materials/material_manager.gd @@ -12,11 +12,15 @@ var _managed_materials: Dictionary = {} enum Transparency { Disabled, Alpha, AlphaScissor } -const _transparency_codes = { - Transparency.Disabled: "0", - Transparency.Alpha: "a", - Transparency.AlphaScissor: "s", -} + +class MaterialOptions: + var diffuse_color: Color = Color.WHITE + var selfillum_color: Color = Color.WHITE + var selfillum_energy: float = 1.0 + var selfillum_enabled: bool = false + var force_transparent: bool = false # TODO: AphaCut/Alpha modes support + var alpha_scissor_threshold: float = 0.5 + @export var season: Season = Season.SEASON_SUMMER: set(x): @@ -41,10 +45,9 @@ func load_material(model_path:String, material_name:String) -> MaszynaMaterial: func get_material( model_path:String, material_path:String, - transparent:Transparency = Transparency.Disabled, - diffuse_color: Color = Color(1.0, 1.0, 1.0) + options: MaterialOptions = MaterialOptions.new(), ) -> Material: - var cache_hash: String = _compute_cache_hash(model_path, material_path, transparent, diffuse_color) + var cache_hash: String = _compute_cache_hash(model_path, material_path, options) var managed_material: Dictionary = _managed_materials.get(cache_hash, {}) if managed_material: var material_ref: WeakRef = managed_material.get("material_ref") as WeakRef @@ -52,20 +55,19 @@ func get_material( if material: return material _managed_materials.erase(cache_hash) - var force_transparent := not transparent == Transparency.Disabled # TODO: ALPHA + var force_transparent = options.force_transparent # TODO: ALPHA var output: ShaderMaterial = _materials_cache.get(cache_hash) as ShaderMaterial if not output: var mmat: MaszynaMaterial = load_material(model_path, material_path) - output = MaterialFactory.create(mmat, model_path, season, weather, force_transparent, diffuse_color) + output = MaterialFactory.create(mmat, model_path, season, weather, options) else: var mmat: MaszynaMaterial = load_material(model_path, material_path) - MaterialFactory.apply(output, mmat, model_path, season, weather, force_transparent, diffuse_color) + MaterialFactory.apply(output, mmat, model_path, season, weather, options) _managed_materials[cache_hash] = { "material_ref": weakref(output), "model_path": model_path, "material_path": material_path, - "force_transparent": force_transparent, - "diffuse_color": diffuse_color, + "options": options, } _materials_cache.set(cache_hash, output) return output @@ -107,14 +109,18 @@ func load_texture(model_path:String, material_name:String, normal:bool = false) func _compute_cache_hash( model_path: String, material_path: String, - transparent: Transparency, - diffuse_color: Color, + options: MaterialOptions, ) -> String: - return model_path.path_join(("%s_t%s_%s.res" % [ - material_path, - _transparency_codes[transparent], - "%x%x%x" % [diffuse_color.r8, diffuse_color.g8, diffuse_color.b8], - ])) + var options_hash = ":".join([ + options.force_transparent, + options.diffuse_color.to_html(true), + options.alpha_scissor_threshold, + options.selfillum_enabled, + options.selfillum_color.to_html(true), + options.selfillum_energy, + ].map(str)).md5_text() + return model_path.path_join("%s_%s.res" % [material_path, options_hash]) + func _refresh_managed_materials() -> void: var cache_hashes: Array = _managed_materials.keys() @@ -134,7 +140,8 @@ func _refresh_managed_material(cache_hash: String) -> void: var material_path: String = managed_material.get("material_path", "") var force_transparent: Transparency = managed_material.get("force_transparent", false) var diffuse_color: Color = managed_material.get("diffuse_color", Color.WHITE) + var options:MaterialOptions = managed_material.get("options") var mmat: MaszynaMaterial = load_material(model_path, material_path) mmat.transparent = mmat.transparent or force_transparent - MaterialFactory.apply(material, mmat, model_path, season, weather, force_transparent, diffuse_color) + MaterialFactory.apply(material, mmat, model_path, season, weather, options) _materials_cache.set(cache_hash, material) From 9201da5f829b5a5c7228152ac0872050f95347dc Mon Sep 17 00:00:00 2001 From: Marcin Nowak Date: Mon, 25 May 2026 17:12:51 +0200 Subject: [PATCH 5/7] add emission --- addons/libmaszyna/e3d/e3d_instancer.gd | 3 +++ .../libmaszyna/materials/material_factory.gd | 18 ++++++++++++------ .../materials/types/default.gdshader | 9 +++++++++ .../materials/types/normalmap.gdshader | 10 ++++++++++ .../types/normalmap_specgloss.gdshader | 10 ++++++++++ .../materials/types/parallax.gdshader | 1 + .../types/shadowlessnormalmap.gdshader | 11 +++++++++++ .../materials/types/sunlessnormalmap.gdshader | 11 +++++++++++ demo/demo_3d.tscn | 9 +++------ src/parsers/e3d_parser.cpp | 5 +++-- 10 files changed, 73 insertions(+), 14 deletions(-) diff --git a/addons/libmaszyna/e3d/e3d_instancer.gd b/addons/libmaszyna/e3d/e3d_instancer.gd index 9a95ddbc..31a03f53 100644 --- a/addons/libmaszyna/e3d/e3d_instancer.gd +++ b/addons/libmaszyna/e3d/e3d_instancer.gd @@ -47,6 +47,9 @@ func _get_material_override(target_node: E3DModelInstance, submodel: E3DSubModel # TODO: handle more material options here (selfillum, diffuse_color, etc) options.force_transparent = submodel.material_transparent + options.selfillum_energy = options.selfillum_color.a # legacy renderer + options.selfillum_color = submodel.self_illumination + options.selfillum_enabled = options.selfillum_energy > 0.0 and submodel.lights_on_threshold >= 1.0 # legacy renderer logic if submodel.dynamic_material: if target_node.skins.size() < submodel.dynamic_material_index + 1: diff --git a/addons/libmaszyna/materials/material_factory.gd b/addons/libmaszyna/materials/material_factory.gd index 95e72039..4f8aba1a 100644 --- a/addons/libmaszyna/materials/material_factory.gd +++ b/addons/libmaszyna/materials/material_factory.gd @@ -146,12 +146,15 @@ func _apply( if property_name == "shader" or property_name == "render_priority" or property_name.begins_with("shader_parameter/"): target_shader_material.set(property_name, source_shader_material.get(property_name)) - shader_meta.factory.call(mmat, variant, material, texture_map, model_path, options.diffuse_color) + shader_meta.factory.call(mmat, variant, material, texture_map, model_path, options) var transparency: MaterialManager.Transparency = MaterialManager.Transparency.Disabled if mmat.transparent or options.force_transparent: transparency = MaterialManager.Transparency.AlphaScissor target_shader_material.set_shader_parameter("transparency", transparency) target_shader_material.set_shader_parameter("alpha_scissor_threshold", 0.5) + target_shader_material.set_shader_parameter("emission_enabled", options.selfillum_enabled) + target_shader_material.set_shader_parameter("emission_color", options.selfillum_color if options.selfillum_color else Color(1.0, 1.0, 1.0, 1.0)) + target_shader_material.set_shader_parameter("emission_energy", options.selfillum_energy) func _apply_default_material( @@ -160,7 +163,7 @@ func _apply_default_material( material: ShaderMaterial, texture_map: TextureMap, model_path: String, - diffuse_color: Color, + options: MaterialManager.MaterialOptions, ) -> void: var diffuse_texture: String = variant.get_texture_path(texture_map.albedo) var normalmap_texture: String = variant.get_texture_path(texture_map.normalmap) @@ -178,7 +181,7 @@ func _apply_default_material( 1.0 )) else: - material.set_shader_parameter("albedo", diffuse_color) + material.set_shader_parameter("albedo", options.diffuse_color) if normalmap_texture: material.set_shader_parameter("texture_normal", MaterialManager.load_texture(model_path, normalmap_texture, true)) @@ -192,6 +195,9 @@ func _apply_default_material( if variant.has_parameter("reflection"): material.set_shader_parameter("metallic", variant.get_parameter("reflection")) + material.set_shader_parameter("emission_enabled", options.selfillum_enabled) + material.set_shader_parameter("emission_color", options.selfillum_color) + material.set_shader_parameter("emission_energy", options.selfillum_energy) func _apply_parallax( mmat: MaszynaMaterial, @@ -199,7 +205,7 @@ func _apply_parallax( material: ShaderMaterial, texture_map: TextureMap, model_path: String, - diffuse_color: Color, + options: MaterialManager.MaterialOptions, ) -> void: var diffuse_texture_path: String = variant.get_texture_path(texture_map.albedo) var normalmap_texture_path: String = variant.get_texture_path(texture_map.normalmap) @@ -223,7 +229,7 @@ func _apply_parallax( var albedo_multiplier:Color = Color(1.0, 1.0, 1.0, 1.0) if not diffuse_texture_path: - albedo_multiplier = diffuse_color + albedo_multiplier = options.diffuse_color if variant.has_parameter("diffuse"): albedo_multiplier = Color( @@ -262,7 +268,7 @@ func _apply_water( material: ShaderMaterial, texture_map: TextureMap, model_path: String, - diffuse_color: Color, + options: MaterialManager.MaterialOptions, ) -> void: var diffuse_texture_path: String = variant.get_texture_path(texture_map.albedo) var normalmap_texture_path: String = variant.get_texture_path(texture_map.normalmap) diff --git a/addons/libmaszyna/materials/types/default.gdshader b/addons/libmaszyna/materials/types/default.gdshader index 3654b7e0..b7ab8337 100644 --- a/addons/libmaszyna/materials/types/default.gdshader +++ b/addons/libmaszyna/materials/types/default.gdshader @@ -21,6 +21,10 @@ uniform float metallic : hint_range(0.0, 1.0, 0.01); uniform sampler2D texture_normal : hint_roughness_normal, filter_linear_mipmap, repeat_enable; uniform float normal_scale : hint_range(-16.0, 16.0); +uniform bool emission_enabled = false; +uniform vec4 emission_color; +uniform float emission_energy : hint_range(0.0, 16.0); + uniform vec3 uv1_scale; uniform vec3 uv1_offset; uniform vec3 uv2_scale; @@ -50,4 +54,9 @@ void fragment() { // Normal Map: Enabled NORMAL_MAP = texture(texture_normal, base_uv).rgb; NORMAL_MAP_DEPTH = normal_scale; + + if(emission_enabled) { + EMISSION = emission_color.rgb * albedo_tex.rgb * emission_energy; + } + } diff --git a/addons/libmaszyna/materials/types/normalmap.gdshader b/addons/libmaszyna/materials/types/normalmap.gdshader index fe558b7a..99d1943c 100644 --- a/addons/libmaszyna/materials/types/normalmap.gdshader +++ b/addons/libmaszyna/materials/types/normalmap.gdshader @@ -21,6 +21,10 @@ uniform float metallic : hint_range(0.0, 1.0, 0.01); uniform sampler2D texture_normal : hint_roughness_normal, filter_linear_mipmap, repeat_enable; uniform float normal_scale : hint_range(-16.0, 16.0); +uniform bool emission_enabled = false; +uniform vec4 emission_color; +uniform float emission_energy : hint_range(0.0, 16.0); + uniform vec3 uv1_scale; uniform vec3 uv1_offset; uniform vec3 uv2_scale; @@ -50,4 +54,10 @@ void fragment() { // Normal Map: Enabled NORMAL_MAP = texture(texture_normal, base_uv).rgb; NORMAL_MAP_DEPTH = normal_scale; + + if(emission_enabled) { + EMISSION = emission_color.rgb * albedo_tex.rgb * emission_energy; + } + + } diff --git a/addons/libmaszyna/materials/types/normalmap_specgloss.gdshader b/addons/libmaszyna/materials/types/normalmap_specgloss.gdshader index 8ca3ca1c..76a73728 100644 --- a/addons/libmaszyna/materials/types/normalmap_specgloss.gdshader +++ b/addons/libmaszyna/materials/types/normalmap_specgloss.gdshader @@ -21,6 +21,10 @@ uniform float metallic : hint_range(0.0, 1.0, 0.01); uniform sampler2D texture_normal : hint_roughness_normal, filter_linear_mipmap, repeat_enable; uniform float normal_scale : hint_range(-16.0, 16.0); +uniform bool emission_enabled = false; +uniform vec4 emission_color; +uniform float emission_energy : hint_range(0.0, 16.0); + uniform vec3 uv1_scale; uniform vec3 uv1_offset; uniform vec3 uv2_scale; @@ -50,4 +54,10 @@ void fragment() { // Normal Map: Enabled NORMAL_MAP = texture(texture_normal, base_uv).rgb; NORMAL_MAP_DEPTH = normal_scale; + + if(emission_enabled) { + EMISSION = emission_color.rgb * albedo_tex.rgb * emission_energy; + } + + } diff --git a/addons/libmaszyna/materials/types/parallax.gdshader b/addons/libmaszyna/materials/types/parallax.gdshader index f1c1602d..f392d3d6 100644 --- a/addons/libmaszyna/materials/types/parallax.gdshader +++ b/addons/libmaszyna/materials/types/parallax.gdshader @@ -149,5 +149,6 @@ void fragment() { NORMAL_MAP = decode_normal_from_rg(normal_sample); NORMAL_MAP_DEPTH = 1.0; + } } diff --git a/addons/libmaszyna/materials/types/shadowlessnormalmap.gdshader b/addons/libmaszyna/materials/types/shadowlessnormalmap.gdshader index ccc14383..24c339fe 100644 --- a/addons/libmaszyna/materials/types/shadowlessnormalmap.gdshader +++ b/addons/libmaszyna/materials/types/shadowlessnormalmap.gdshader @@ -18,6 +18,11 @@ uniform sampler2D texture_roughness : hint_roughness_r, filter_linear_mipmap, re uniform float specular : hint_range(0.0, 1.0, 0.01); uniform float metallic : hint_range(0.0, 1.0, 0.01); +uniform bool emission_enabled = false; +uniform vec4 emission_color; +uniform float emission_energy : hint_range(0.0, 16.0); + + uniform vec3 uv1_scale; uniform vec3 uv1_offset; uniform vec3 uv2_scale; @@ -43,4 +48,10 @@ void fragment() { vec4 roughness_texture_channel = vec4(1.0, 0.0, 0.0, 0.0); float roughness_tex = dot(texture(texture_roughness, base_uv), roughness_texture_channel); ROUGHNESS = roughness_tex * roughness; + + if(emission_enabled) { + EMISSION = emission_color.rgb * albedo_tex.rgb * emission_energy; + } + + } diff --git a/addons/libmaszyna/materials/types/sunlessnormalmap.gdshader b/addons/libmaszyna/materials/types/sunlessnormalmap.gdshader index 8ca3ca1c..4f0b2171 100644 --- a/addons/libmaszyna/materials/types/sunlessnormalmap.gdshader +++ b/addons/libmaszyna/materials/types/sunlessnormalmap.gdshader @@ -21,6 +21,11 @@ uniform float metallic : hint_range(0.0, 1.0, 0.01); uniform sampler2D texture_normal : hint_roughness_normal, filter_linear_mipmap, repeat_enable; uniform float normal_scale : hint_range(-16.0, 16.0); +uniform bool emission_enabled = false; +uniform vec4 emission_color; +uniform float emission_energy : hint_range(0.0, 16.0); + + uniform vec3 uv1_scale; uniform vec3 uv1_offset; uniform vec3 uv2_scale; @@ -50,4 +55,10 @@ void fragment() { // Normal Map: Enabled NORMAL_MAP = texture(texture_normal, base_uv).rgb; NORMAL_MAP_DEPTH = normal_scale; + + if(emission_enabled) { + EMISSION = emission_color.rgb * albedo_tex.rgb * emission_energy; + } + + } diff --git a/demo/demo_3d.tscn b/demo/demo_3d.tscn index 9095fc24..1ffa517c 100644 --- a/demo/demo_3d.tscn +++ b/demo/demo_3d.tscn @@ -53,9 +53,9 @@ glow_enabled = true fog_enabled = true fog_sky_affect = 0.4 volumetric_fog_enabled = true -volumetric_fog_density = 0.01 +volumetric_fog_density = 0.003 volumetric_fog_anisotropy = 0.0 -volumetric_fog_length = 55.89 +volumetric_fog_length = 6.5 volumetric_fog_detail_spread = 1.0 volumetric_fog_sky_affect = 0.405 adjustment_enabled = true @@ -233,8 +233,6 @@ visible = null [node name="MaszynaEnvironmentNode" type="Node" parent="." unique_id=1464463242] script = ExtResource("2_py6pt") world_environment = NodePath("../WorldEnvironment") -season = 2 -weather = 0 sky_texture_offset = Vector2(0.49, 0.005) sky_texture_scale = Vector2(0.547, 0.865) sky_energy = 0.85 @@ -250,9 +248,8 @@ size = Vector3(5000, 0.1, 5000) [node name="DirectionalLight3D" type="DirectionalLight3D" parent="." unique_id=786641912] transform = Transform3D(-0.846557, 0.01871, 0.531969, 0.393589, 0.694835, 0.601906, -0.358369, 0.718925, -0.595582, 5.83027, -6.14961, 0) -visible = false light_color = Color(0.929688, 0.848771, 0.817108, 1) -light_energy = 1.12 +light_energy = 1.2 light_indirect_energy = 2.0 light_volumetric_fog_energy = 11.811 shadow_enabled = true diff --git a/src/parsers/e3d_parser.cpp b/src/parsers/e3d_parser.cpp index ec672e9f..9818861f 100644 --- a/src/parsers/e3d_parser.cpp +++ b/src/parsers/e3d_parser.cpp @@ -89,8 +89,9 @@ namespace godot { result.first_vertex_idx = u32s(p_file->get_32()); result.material_idx = u32s(p_file->get_32()); result.is_material_colored = (result.material_idx == 0); - result.lights_on_threshold = p_file->get_float(); - result.visibility_light_threshold = p_file->get_float(); + p_file->get_float(); // offset 40 UNUSED + result.lights_on_threshold = p_file->get_float(); // offset 44 + // result.visibility_light_threshold = p_file->get_float(); // ReSharper disable once CppExpressionWithoutSideEffects p_file->get_buffer(16); // skip unused RGBA ambient const float diffuse_r = p_file->get_float(); From 175af9e6fd10e854d5beffa7e77f5745ef73bd3e Mon Sep 17 00:00:00 2001 From: Marcin Nowak Date: Mon, 25 May 2026 19:28:34 +0200 Subject: [PATCH 6/7] initial high level lights api on railvehicle3d --- addons/libmaszyna/e3d/e3d_model_instance.gd | 6 ++-- addons/libmaszyna/e3d/e3d_nodes_instancer.gd | 2 +- addons/libmaszyna/rail_vehicle_3d.gd | 30 ++++++++++++++++++-- demo/demo_3d.tscn | 13 ++++++++- demo/vehicles/sm42/sm_42.tscn | 1 + 5 files changed, 44 insertions(+), 8 deletions(-) diff --git a/addons/libmaszyna/e3d/e3d_model_instance.gd b/addons/libmaszyna/e3d/e3d_model_instance.gd index 3fa325db..ccba13f8 100644 --- a/addons/libmaszyna/e3d/e3d_model_instance.gd +++ b/addons/libmaszyna/e3d/e3d_model_instance.gd @@ -32,7 +32,7 @@ var _current_editable: bool = false @export var lights_state: Dictionary[String, bool] = {}: set(x): lights_state = x - if is_inside_tree(): + if is_inside_tree() and e3d_loaded: _current_instancer.sync_lights(self) @@ -139,9 +139,7 @@ func _ready() -> void: func _enter_tree() -> void: - if _model and _current_instancer: - _current_instancer.instantiate(self, _model, _current_editable) - + pass func _exit_tree() -> void: if _current_instancer: diff --git a/addons/libmaszyna/e3d/e3d_nodes_instancer.gd b/addons/libmaszyna/e3d/e3d_nodes_instancer.gd index 334f4287..fd41211a 100644 --- a/addons/libmaszyna/e3d/e3d_nodes_instancer.gd +++ b/addons/libmaszyna/e3d/e3d_nodes_instancer.gd @@ -151,7 +151,7 @@ func _create_submodel_instance(target_node: E3DModelInstance, submodel: E3DSubMo obj.light_color = submodel.diffuse_color obj.light_color.a = 1.0 #obj.light_energy = submodel.visibility_light # ???? - obj.light_energy = 10.0 # FIXME: guessing + obj.light_energy = 1.0 # FIXME: guessing obj.light_volumetric_fog_energy = 4.0 # FIXME: guessing obj.shadow_enabled = true obj.distance_fade_enabled = true diff --git a/addons/libmaszyna/rail_vehicle_3d.gd b/addons/libmaszyna/rail_vehicle_3d.gd index b2bb6de4..28aecda3 100644 --- a/addons/libmaszyna/rail_vehicle_3d.gd +++ b/addons/libmaszyna/rail_vehicle_3d.gd @@ -2,7 +2,25 @@ extends Node3D class_name RailVehicle3D +const EMPTY_LIGHTS:Dictionary[String, bool] = {} + # FIXME: Head Display implementation is experimental and only for demo purposes +@export_node_path("E3DModelInstance") var model_instance_path:NodePath = NodePath(""): + set(x): + if not x == model_instance_path: + model_instance_path = x + lights = {} + if is_inside_tree() and model_instance_path: + _model_node = get_node_or_null(model_instance_path) + lights = _model_node.lights_state if _model_node else EMPTY_LIGHTS + _dirty = true + +@export var lights:Dictionary[String, bool] = EMPTY_LIGHTS: + set(x): + if not x == lights: + lights = x + if is_inside_tree() and _model_node: + _model_node.lights_state = lights @export_node_path("TrainController") var controller_path:NodePath = NodePath(""): set(x): @@ -40,6 +58,7 @@ var _head_display_e3d:E3DModelInstance var _cabin:Cabin3D var _camera:FreeCamera3D var _controller:TrainController +var _model_node:E3DModelInstance var _t:float = 0.0 @@ -163,8 +182,11 @@ func _process(delta): if _head_display_e3d: _head_display_e3d.e3d_loaded.connect(func(): _needs_head_display_update = true) - if controller_path and is_inside_tree(): - _controller = get_node(controller_path) + if is_inside_tree(): + if controller_path: + _controller = get_node_or_null(controller_path) + if model_instance_path: + _model_node = get_node_or_null(model_instance_path) _t += delta if _t > 0.25 and _needs_head_display_update: @@ -184,3 +206,7 @@ func _ready() -> void: for instance:E3DModelInstance in find_children("", "E3DModelInstance", true, false): instance.e3d_loaded.connect(_schedule_head_display_update) + + var model_node = get_node_or_null(model_instance_path) + if model_node: + model_node.lights_state = lights diff --git a/demo/demo_3d.tscn b/demo/demo_3d.tscn index 1ffa517c..c89b76d2 100644 --- a/demo/demo_3d.tscn +++ b/demo/demo_3d.tscn @@ -102,7 +102,6 @@ script = ExtResource("1_ut343") [node name="DebugMenuFPS" parent="." unique_id=28357571 instance=ExtResource("2_qg2pq")] [node name="TopBar" type="Container" parent="." unique_id=1102380660] -top_level = true clip_contents = true anchors_preset = 10 anchor_right = 1.0 @@ -264,6 +263,18 @@ environment = SubResource("Environment_jrns0") [node name="SM42-099" parent="." unique_id=1125812614 instance=ExtResource("11_8gidn")] transform = Transform3D(-0.9996828, 0, 0.02518662, 0, 1, 0, -0.02518662, 0, -0.9996828, -25.735577, 0.16, 180.73814) +lights = Dictionary[String, bool]({ +"endsignal12": false, +"endsignal13": false, +"endsignal22": true, +"endsignal23": true, +"headlamp11": true, +"headlamp12": true, +"headlamp13": true, +"headlamp21": false, +"headlamp22": false, +"headlamp23": false +}) [node name="Impuls" parent="." unique_id=2111644575 instance=ExtResource("12_hsov8")] transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -0.021647027, -0.019251347, -4.3020163) diff --git a/demo/vehicles/sm42/sm_42.tscn b/demo/vehicles/sm42/sm_42.tscn index ad09793d..8b28d7c2 100644 --- a/demo/vehicles/sm42/sm_42.tscn +++ b/demo/vehicles/sm42/sm_42.tscn @@ -11,6 +11,7 @@ size = Vector3(4.5332, 4.72223, 17.0549) [node name="SM42-099" type="Node3D" unique_id=1967816975] transform = Transform3D(-1, 0, -8.74228e-08, 0, 1, 0, 8.74228e-08, 0, -1, 0, 0, 0) script = ExtResource("1_iiqbf") +model_instance_path = NodePath("SM42-099") controller_path = NodePath("SM42v1") cabin_scene = ExtResource("2_htu7b") low_poly_cabin_path = NodePath("SM42-099-LowPolyCab") From 0491f0884b49cc72c83164b26ea1e1ac68a72b21 Mon Sep 17 00:00:00 2001 From: DoS Date: Tue, 26 May 2026 18:32:52 +0200 Subject: [PATCH 7/7] Improve detecting light_energy. Add ep09 for tests --- .gitignore | 1 + addons/libmaszyna/e3d/e3d_nodes_instancer.gd | 21 +++++-- .../libmaszyna/materials/maszyna_material.gd | 4 +- demo/demo_3d.tscn | 4 ++ demo/project.godot | 8 +-- demo/vehicles/ep09/ep09.tscn | 63 +++++++++++++++++++ demo/vehicles/impuls/impuls.tscn | 24 +++++++ demo/vehicles/tem2/tem2.tscn | 12 ++++ src/e3d/E3DModel.cpp | 6 -- src/e3d/E3DModel.hpp | 2 +- src/e3d/E3DSubModel.cpp | 9 +-- src/e3d/E3DSubModel.hpp | 1 + src/parsers/e3d_parser.cpp | 16 ++++- src/parsers/e3d_parser.hpp | 3 +- 14 files changed, 143 insertions(+), 31 deletions(-) create mode 100644 demo/vehicles/ep09/ep09.tscn diff --git a/.gitignore b/.gitignore index 3e9301f6..4a68f4d3 100644 --- a/.gitignore +++ b/.gitignore @@ -62,3 +62,4 @@ godot-cpp/CMakeFiles/godot-cpp.dir/gen/src/classes build-*/ /.codex +/.antigravitycli \ No newline at end of file diff --git a/addons/libmaszyna/e3d/e3d_nodes_instancer.gd b/addons/libmaszyna/e3d/e3d_nodes_instancer.gd index fd41211a..21f76fb1 100644 --- a/addons/libmaszyna/e3d/e3d_nodes_instancer.gd +++ b/addons/libmaszyna/e3d/e3d_nodes_instancer.gd @@ -1,6 +1,9 @@ @tool extends E3DInstancer +const DEFAULT_LIGHT_ENERGY := 10 +const DEFAULT_END_LIGHT_ENERGY := 5 + class LightNodeInfo: var light_name: String = "" var light_on_node:NodePath = NodePath("") @@ -29,9 +32,9 @@ func instantiate(target_node: E3DModelInstance, model: E3DModel, editable: bool _instance_lights[light_name] = light_info match entry_type: "on": - light_info.light_on_node = node.get_path() + light_info.light_on_node = target_node.get_path_to(node) "off": - light_info.light_off_node = node.get_path() + light_info.light_off_node = target_node.get_path_to(node) else: if entry_type == "spotlight": _spotlights.append(node) @@ -46,9 +49,9 @@ func instantiate(target_node: E3DModelInstance, model: E3DModel, editable: bool var parent = spotlight.get_parent() if not parent: break - var found_idx = _light_on_nodes.find(parent.get_path()) + var found_idx = _light_on_nodes.find(target_node.get_path_to(parent)) if found_idx > -1: - _instance_lights.values()[found_idx].spotlight_node = spotlight.get_path() + _instance_lights.values()[found_idx].spotlight_node = target_node.get_path_to(spotlight) break sync_lights(target_node) @@ -150,8 +153,14 @@ func _create_submodel_instance(target_node: E3DModelInstance, submodel: E3DSubMo obj.name = submodel.resource_name obj.light_color = submodel.diffuse_color obj.light_color.a = 1.0 - #obj.light_energy = submodel.visibility_light # ???? - obj.light_energy = 1.0 # FIXME: guessing + if submodel.light_energy == 0: + if submodel.resource_name.begins_with("end"): + obj.light_energy = DEFAULT_END_LIGHT_ENERGY + else: + obj.light_energy = DEFAULT_LIGHT_ENERGY + else: + obj.light_energy = submodel.light_energy + obj.light_volumetric_fog_energy = 4.0 # FIXME: guessing obj.shadow_enabled = true obj.distance_fade_enabled = true diff --git a/addons/libmaszyna/materials/maszyna_material.gd b/addons/libmaszyna/materials/maszyna_material.gd index d7fec71f..54c2df61 100644 --- a/addons/libmaszyna/materials/maszyna_material.gd +++ b/addons/libmaszyna/materials/maszyna_material.gd @@ -13,14 +13,14 @@ const WEATHER_RAIN_KEY = "rain" const WEATHER_SNOW_KEY = "snow" -const SEASONS_MAP: Dictionary[MaterialManager.Season, String] = { +var SEASONS_MAP: Dictionary[MaterialManager.Season, String] = { MaterialManager.Season.SEASON_WINTER: SEASON_WINTER_KEY, MaterialManager.Season.SEASON_SPRING: SEASON_SPRING_KEY, MaterialManager.Season.SEASON_SUMMER: SEASON_SUMMER_KEY, MaterialManager.Season.SEASON_AUTUMN: SEASON_AUTUMN_KEY, } -const WEATHER_MAP: Dictionary[MaterialManager.Weather, String] = { +var WEATHER_MAP: Dictionary[MaterialManager.Weather, String] = { MaterialManager.Weather.WEATHER_CLEAR: WEATHER_CLEAR_KEY, MaterialManager.Weather.WEATHER_CLOUDY: WEATHER_CLOUDY_KEY, MaterialManager.Weather.WEATHER_RAIN: WEATHER_RAIN_KEY, diff --git a/demo/demo_3d.tscn b/demo/demo_3d.tscn index c89b76d2..78861541 100644 --- a/demo/demo_3d.tscn +++ b/demo/demo_3d.tscn @@ -25,6 +25,7 @@ [ext_resource type="E3DModel" uid="uid://buxg17oldnjq6" path="res://e3d_models/dzik.e3d" id="21_sa6xs"] [ext_resource type="Script" uid="uid://do35jm13yiccm" path="res://addons/libmaszyna/traction/maszyna_traction_3d.gd" id="24_6m450"] [ext_resource type="Script" uid="uid://dotmfqnin31lo" path="res://addons/libmaszyna/traction/maszyna_traction_line.gd" id="25_jchga"] +[ext_resource type="PackedScene" uid="uid://fv4an7lb63lu" path="res://vehicles/ep09/ep09.tscn" id="26_jchga"] [sub_resource type="ShaderMaterial" id="ShaderMaterial_s18ef"] shader = ExtResource("2_ydn4r") @@ -1161,5 +1162,8 @@ model_filename = "tra/-3d" skins = ["tra/betonrelief1"] metadata/_custom_type_script = "uid://dgifxvb2e4hww" +[node name="Node3D" parent="." unique_id=1451239513 instance=ExtResource("26_jchga")] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -29.282572, -4.7683716e-07, 169.09251) + [connection signal="index_pressed" from="TopBar/HBoxContainer/MenuBar/PopupMenu" to="." method="_on_popup_menu_index_pressed"] [connection signal="toggled" from="TopBar/HBoxContainer/ToggleAllControls" to="." method="_on_show_all_controls_button_toggled"] diff --git a/demo/project.godot b/demo/project.godot index 0daf87cb..9ee2d3fd 100644 --- a/demo/project.godot +++ b/demo/project.godot @@ -35,13 +35,13 @@ TractionRenderingServer="*res://addons/libmaszyna/servers/traction_rendering_ser NodebankLibraryBuilder="*uid://dv25nc25tcanw" MaszynaEnvironment="*uid://3nkx0u4dgdws" Console="*uid://d046w0soh2iou" +MaterialManager="*uid://wsbd7kr83ncg" MaterialParser="*uid://7b48fl428yqa" +MaterialFactory="*uid://v144f3irjg7m" E3DModelManager="*uid://b2v733o52i40x" E3DNodesInstancer="*uid://dl44jll5gimwi" E3DModelTool="*uid://df7a5f7f35wd1" AudioStreamManager="*uid://cp8sgfk334adm" -MaterialManager="*uid://wsbd7kr83ncg" -MaterialFactory="*uid://v144f3irjg7m" [debug] @@ -52,10 +52,6 @@ file_logging/log_path="user://logs/app.log" window/size/mode=3 window/size/initial_position_type=3 -[editor_overrides] - -text_editor/behavior/files/trim_trailing_whitespace_on_save=true - [editor_plugins] enabled=PackedStringArray("res://addons/gut/plugin.cfg", "res://addons/libmaszyna/editor/e3d_toolbar/plugin.cfg", "res://addons/libmaszyna/editor/nodebank/plugin.cfg", "res://addons/libmaszyna/editor/user_settings_dock/plugin.cfg", "res://addons/libmaszyna/plugin.cfg") diff --git a/demo/vehicles/ep09/ep09.tscn b/demo/vehicles/ep09/ep09.tscn new file mode 100644 index 00000000..f19ba43e --- /dev/null +++ b/demo/vehicles/ep09/ep09.tscn @@ -0,0 +1,63 @@ +[gd_scene format=3 uid="uid://fv4an7lb63lu"] + +[ext_resource type="Script" uid="uid://dgifxvb2e4hww" path="res://addons/libmaszyna/e3d/e3d_model_instance.gd" id="1_1nw4k"] + +[node name="Node3D" type="Node3D" unique_id=1451239513] + +[node name="ep09_lowpoly_interior" type="VisualInstance3D" parent="." unique_id=742765134] +unique_name_in_owner = false +process_mode = 0 +process_priority = 0 +process_physics_priority = 0 +process_thread_group = 0 +physics_interpolation_mode = 0 +auto_translate_mode = 0 +editor_description = "" +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0) +rotation_edit_mode = 0 +rotation_order = 2 +top_level = false +visible = true +visibility_parent = NodePath("") +layers = 1 +script = ExtResource("1_1nw4k") +data_path = "/dynamic/pkp/ep09_v1/" +model_filename = "low_poly_int/lowpoly]elektro" +skins = ["104e-035", ""] +exclude_node_names = ["cien"] +metadata/_custom_type_script = "uid://dgifxvb2e4hww" + +[node name="ep09" type="VisualInstance3D" parent="." unique_id=1498765359] +unique_name_in_owner = false +process_mode = 0 +process_priority = 0 +process_physics_priority = 0 +process_thread_group = 0 +physics_interpolation_mode = 0 +auto_translate_mode = 0 +editor_description = "" +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0) +rotation_edit_mode = 0 +rotation_order = 2 +top_level = false +visible = true +visibility_parent = NodePath("") +layers = 1 +script = ExtResource("1_1nw4k") +lights_state = Dictionary[String, bool]({ +"endsignal12": true, +"endsignal13": true, +"endsignal22": false, +"endsignal23": false, +"headlamp11": false, +"headlamp12": false, +"headlamp13": false, +"headlamp21": true, +"headlamp22": true, +"headlamp23": true +}) +data_path = "/dynamic/pkp/ep09_v1/" +model_filename = "104e_6" +skins = ["104e-035", ""] +exclude_node_names = ["cien"] +metadata/_custom_type_script = "uid://dgifxvb2e4hww" diff --git a/demo/vehicles/impuls/impuls.tscn b/demo/vehicles/impuls/impuls.tscn index 7419e2a3..a83c6086 100644 --- a/demo/vehicles/impuls/impuls.tscn +++ b/demo/vehicles/impuls/impuls.tscn @@ -39,6 +39,18 @@ visible = true visibility_parent = NodePath("") layers = 1 script = ExtResource("1_pes3o") +lights_state = Dictionary[String, bool]({ +"endsignal12": false, +"endsignal13": false, +"endsignal22": false, +"endsignal23": false, +"headlamp11": true, +"headlamp12": true, +"headlamp13": true, +"headlamp21": false, +"headlamp22": false, +"headlamp23": false +}) data_path = "/dynamic/pkp/impuls_v1" model_filename = "main/36wea-a_kd" skins = ["36wea-019a,1", "36wea-019a,2", "36wea-019a,3", "36wea-019a,4"] @@ -242,6 +254,18 @@ visible = true visibility_parent = NodePath("") layers = 1 script = ExtResource("1_pes3o") +lights_state = Dictionary[String, bool]({ +"endsignal12": false, +"endsignal13": false, +"endsignal22": true, +"endsignal23": true, +"headlamp11": false, +"headlamp12": false, +"headlamp13": false, +"headlamp21": false, +"headlamp22": false, +"headlamp23": false +}) data_path = "/dynamic/pkp/impuls_v1" model_filename = "main/31wea-d_kd" skins = ["36wea-019a,1", "36wea-019a,2", "36wea-019a,3", "36wea-019a,4"] diff --git a/demo/vehicles/tem2/tem2.tscn b/demo/vehicles/tem2/tem2.tscn index 2f48d8f5..6dc4dc65 100644 --- a/demo/vehicles/tem2/tem2.tscn +++ b/demo/vehicles/tem2/tem2.tscn @@ -153,6 +153,18 @@ visible = true visibility_parent = NodePath("") layers = 1 script = ExtResource("2_q4wpc") +lights_state = Dictionary[String, bool]({ +"endsignal12": true, +"endsignal13": true, +"endsignal22": false, +"endsignal23": false, +"headlamp11": false, +"headlamp12": false, +"headlamp13": false, +"headlamp21": true, +"headlamp22": true, +"headlamp23": true +}) data_path = "/dynamic/pkp/tem2_v2" model_filename = "tem2-122a" skins = ["tem2-122"] diff --git a/src/e3d/E3DModel.cpp b/src/e3d/E3DModel.cpp index ff9e91e3..fbd77b39 100644 --- a/src/e3d/E3DModel.cpp +++ b/src/e3d/E3DModel.cpp @@ -6,12 +6,6 @@ namespace godot { } void E3DModel::clear() { - for (int i = 0; i < submodels.size(); i++) { - Ref sm = submodels.get(i); - if (sm.is_valid()) { - sm->clear(); - } - } submodels.clear(); lights.clear(); } diff --git a/src/e3d/E3DModel.hpp b/src/e3d/E3DModel.hpp index 4322d51e..b910edd9 100644 --- a/src/e3d/E3DModel.hpp +++ b/src/e3d/E3DModel.hpp @@ -9,7 +9,7 @@ namespace godot { class E3DModel : public Resource { GDCLASS(E3DModel, Resource) public: - static constexpr int FORMAT_VERSION = 20260525; // must be incremented when public API of E3DModel or + static constexpr int FORMAT_VERSION = 20260526; // must be incremented when public API of E3DModel or // E3DSubModel is changing ~E3DModel() override; diff --git a/src/e3d/E3DSubModel.cpp b/src/e3d/E3DSubModel.cpp index ce8ac581..ddbcd582 100644 --- a/src/e3d/E3DSubModel.cpp +++ b/src/e3d/E3DSubModel.cpp @@ -6,12 +6,6 @@ namespace godot { } void E3DSubModel::clear() { - for (int i = 0; i < submodels.size(); i++) { - Ref sm = submodels.get(i); - if (sm.is_valid()) { - sm->clear(); - } - } submodels.clear(); mesh.unref(); parent = nullptr; @@ -157,6 +151,9 @@ namespace godot { BIND_PROPERTY( Variant::FLOAT, "cos_view_angle", "cos_view_angle", &E3DSubModel::set_cos_view_angle, &E3DSubModel::get_cos_view_angle, "p_cos_view_angle"); + BIND_PROPERTY( + Variant::FLOAT, "light_energy", "light_energy", &E3DSubModel::set_light_energy, + &E3DSubModel::get_light_energy, "p_light_energy"); } void E3DSubModel::add_child(const Ref &p_sub_model) { diff --git a/src/e3d/E3DSubModel.hpp b/src/e3d/E3DSubModel.hpp index b3dcf54d..b82db709 100644 --- a/src/e3d/E3DSubModel.hpp +++ b/src/e3d/E3DSubModel.hpp @@ -89,6 +89,7 @@ namespace godot { MAKE_MEMBER_GS_NR(int, far_attenuation_decay, 0) MAKE_MEMBER_GS_NR(float, cos_hotspot_angle, 0.0) MAKE_MEMBER_GS_NR(float, cos_view_angle, 0.0) + MAKE_MEMBER_GS_NR(float, light_energy, 0.0) void add_child(const Ref &p_sub_model); void set_parent(E3DSubModel *p_sub_model); diff --git a/src/parsers/e3d_parser.cpp b/src/parsers/e3d_parser.cpp index 9818861f..8868468f 100644 --- a/src/parsers/e3d_parser.cpp +++ b/src/parsers/e3d_parser.cpp @@ -89,6 +89,7 @@ namespace godot { result.first_vertex_idx = u32s(p_file->get_32()); result.material_idx = u32s(p_file->get_32()); result.is_material_colored = (result.material_idx == 0); + // ReSharper disable once CppExpressionWithoutSideEffects p_file->get_float(); // offset 40 UNUSED result.lights_on_threshold = p_file->get_float(); // offset 44 // result.visibility_light_threshold = p_file->get_float(); @@ -134,10 +135,19 @@ namespace godot { result.diffuse_color = diffuse_color; result.index_count = p_file->get_32(); - result.first_index_idx = p_file->get_32(); + result.first_index_idx = p_file->get_32(); // Offset 160 -> 164 + result.light_energy = p_file->get_float(); // Offset 164 -> 168 + result.transparent = result.flags & 0b000001; - // ReSharper disable once CppExpressionWithoutSideEffects - p_file->get_buffer(p_chunk_size - 164); // read to the end of the chunk + + // Cleanup trash + if (p_chunk_size == 256) { + // ReSharper disable once CppExpressionWithoutSideEffects + p_file->get_buffer(88); // 88 bytes of dev variables which should be 0s + } else { + // ReSharper disable once CppExpressionWithoutSideEffects + p_file->get_buffer(p_chunk_size - 168); // for SUB1 + } result.vertices = PackedVector3Array(); result.normals = PackedVector3Array(); result.uvs = PackedVector2Array(); diff --git a/src/parsers/e3d_parser.hpp b/src/parsers/e3d_parser.hpp index a5fac9f4..392fc3e7 100644 --- a/src/parsers/e3d_parser.hpp +++ b/src/parsers/e3d_parser.hpp @@ -87,7 +87,8 @@ namespace godot { PackedInt32Array indices; PackedFloat64Array tangents; Transform3D matrix; - }; + float light_energy; + }; ChunkHeader _read_chunk_header(const Ref &p_file) const; int u32s(uint32_t p_value) const;