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/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..31a03f53 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( @@ -35,6 +43,14 @@ 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 + 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: push_warning( @@ -49,7 +65,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 @@ -63,8 +79,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/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..ccba13f8 100644 --- a/addons/libmaszyna/e3d/e3d_model_instance.gd +++ b/addons/libmaszyna/e3d/e3d_model_instance.gd @@ -29,6 +29,13 @@ var _e3d_loaded: bool = false var _current_instancer: E3DInstancer var _current_editable: bool = false +@export var lights_state: Dictionary[String, bool] = {}: + set(x): + lights_state = x + if is_inside_tree() and e3d_loaded: + _current_instancer.sync_lights(self) + + var default_aabb_size: Vector3 = Vector3(1, 1, 1) ## E3DModel to instantiate (leave empty, if you want to lazy load with [member model_filename]) @@ -132,19 +139,35 @@ 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: _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 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 + 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..21f76fb1 100644 --- a/addons/libmaszyna/e3d/e3d_nodes_instancer.gd +++ b/addons/libmaszyna/e3d/e3d_nodes_instancer.gd @@ -1,12 +1,83 @@ @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("") + 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: - _do_add_submodels(target_node, target_node, model.submodels, editable) + var lights: Array = [] # first pass + _do_add_submodels(target_node, model, target_node, model.submodels, editable, lights) + + # 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 = target_node.get_path_to(node) + "off": + light_info.light_off_node = target_node.get_path_to(node) + 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(target_node.get_path_to(parent)) + if found_idx > -1: + _instance_lights.values()[found_idx].spotlight_node = target_node.get_path_to(spotlight) + break + + sync_lights(target_node) + #print(target_node.get_lights_state()) + + +func sync_lights(target_node: E3DModelInstance) -> void: + 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() @@ -18,13 +89,29 @@ 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, 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 parent.add_child(child, false, internal) @@ -33,15 +120,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 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() + 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, lights): var obj match submodel.submodel_type: @@ -56,6 +148,28 @@ 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_color.a = 1.0 + 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 + 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 + lights.append([null, "spotlight", obj]) + if obj: obj.visible = submodel.visible return obj 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/addons/libmaszyna/materials/material_factory.gd b/addons/libmaszyna/materials/material_factory.gd index d5c42884..4f8aba1a 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,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, 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 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) + 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( @@ -165,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) @@ -183,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)) @@ -197,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, @@ -204,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) @@ -228,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( @@ -267,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/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) 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/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 b69cbe5f..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") @@ -53,10 +54,10 @@ glow_enabled = true 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_density = 0.003 +volumetric_fog_anisotropy = 0.0 +volumetric_fog_length = 6.5 +volumetric_fog_detail_spread = 1.0 volumetric_fog_sky_affect = 0.405 adjustment_enabled = true @@ -102,7 +103,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 @@ -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")] @@ -248,7 +249,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) 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 @@ -263,6 +264,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) @@ -1149,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/sm42/sm_42.tscn b/demo/vehicles/sm42/sm_42.tscn index 8b852858..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") @@ -34,6 +35,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": true, +"headlamp12": true, +"headlamp13": true, +"headlamp21": false, +"headlamp22": false, +"headlamp23": false +}) data_path = "/dynamic/pkp/sm42_v1" model_filename = "6da" skins = ["6d-907"] @@ -85,6 +98,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 +124,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 +150,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 +176,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 +202,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/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/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..fbd77b39 100644 --- a/src/e3d/E3DModel.cpp +++ b/src/e3d/E3DModel.cpp @@ -6,13 +6,8 @@ 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(); } void E3DModel::_bind_methods() { @@ -20,6 +15,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..b910edd9 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 = 20260526; // 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..f9494b43 --- /dev/null +++ b/src/e3d/E3DModelLightDefinition.cpp @@ -0,0 +1,18 @@ +#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"); + } + +} // namespace godot diff --git a/src/e3d/E3DModelLightDefinition.hpp b/src/e3d/E3DModelLightDefinition.hpp new file mode 100644 index 00000000..0afddf8a --- /dev/null +++ b/src/e3d/E3DModelLightDefinition.hpp @@ -0,0 +1,17 @@ +#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) + protected: + static void _bind_methods(); + }; +} // namespace godot diff --git a/src/e3d/E3DSubModel.cpp b/src/e3d/E3DSubModel.cpp index 08ab028c..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; @@ -128,6 +122,38 @@ 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"); + 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 aff061e4..b82db709 100644 --- a/src/e3d/E3DSubModel.hpp +++ b/src/e3d/E3DSubModel.hpp @@ -80,6 +80,17 @@ 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) + MAKE_MEMBER_GS_NR(float, light_energy, 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..8868468f 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); } @@ -82,19 +89,24 @@ 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(); // 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); + 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_float(); // skip unused alpha + p_file->get_buffer(16); // skip unused RGBA ambient + 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,13 +118,36 @@ 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.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(); @@ -438,10 +473,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 +534,85 @@ 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; + + /* 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", + "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); + } + + 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..392fc3e7 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; @@ -78,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; @@ -89,5 +99,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);