Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -62,3 +62,4 @@ godot-cpp/CMakeFiles/godot-cpp.dir/gen/src/classes

build-*/
/.codex
/.antigravitycli
3 changes: 2 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand All @@ -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:
Expand Down
23 changes: 19 additions & 4 deletions addons/libmaszyna/e3d/e3d_instancer.gd
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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(
Expand All @@ -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
Expand All @@ -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
Expand Down
28 changes: 28 additions & 0 deletions addons/libmaszyna/e3d/e3d_light.gd
Original file line number Diff line number Diff line change
@@ -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():
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the high level API for lights control should be on E3DModelInstance, which is a node proxy for instancer and a reference holder for a e3d model.

Optimised instancer will not use this node. It will control lights by calling RenderingServer directly.

var parent = get_parent()
if parent == null:
return
var light_root = parent.get_parent()
var is_end = parent.name.begins_with("end")
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure about this parent-child relation and hardcoded names of related nodes.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Me neither, but that's the best I could come up with. Also _end is a part of their standard naming of light-related nodes

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. I think that e3d_light contains logic which should be moved to e3d instancing or e3d parsing stage.
  2. MaSzyna quirks like matching something by part of a node name should be configurable. Please note that node name in Godot can be different than e3d (sub)model name.
  3. E3D Light in this implementation will be always a spotlight. You never be able to create omni lights or other lights. That's why it is better to move nodes creation to the factories like e3d nodes instancer.
  4. At the e3d parsing stage you can traverse e3d model tree to find parent-child relationships between submodels, if needed.
  5. Try to store references between e3d submodels in thei, i.e. add properties similar to nodepath (you can't use nodepath though, but a reference to other node should work)

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
1 change: 1 addition & 0 deletions addons/libmaszyna/e3d/e3d_light.gd.uid
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
uid://d0k5foqnxfdhu
31 changes: 27 additions & 4 deletions addons/libmaszyna/e3d/e3d_model_instance.gd
Original file line number Diff line number Diff line change
Expand Up @@ -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])
Expand Down Expand Up @@ -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:
Expand Down
128 changes: 121 additions & 7 deletions addons/libmaszyna/e3d/e3d_nodes_instancer.gd
Original file line number Diff line number Diff line change
@@ -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()
Expand All @@ -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)
Expand All @@ -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:
Expand All @@ -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
Expand Down
4 changes: 2 additions & 2 deletions addons/libmaszyna/materials/maszyna_material.gd
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Loading
Loading