From 5fbbf9606cb2f6dbc0b4f166dd7cb4c92e44acdb Mon Sep 17 00:00:00 2001 From: Atirut Wattanamongkol Date: Tue, 4 Mar 2025 19:36:02 +0700 Subject: [PATCH 1/7] Rename Map Builder to Map Loader --- project.godot | 1 - scripts/{map_builder.gd => map_loader.gd} | 1 + scripts/{map_builder.gd.uid => map_loader.gd.uid} | 0 scripts/map_test.gd | 12 ++++++++---- scripts/world.gd | 12 ++++++++---- 5 files changed, 17 insertions(+), 9 deletions(-) rename scripts/{map_builder.gd => map_loader.gd} (99%) rename scripts/{map_builder.gd.uid => map_loader.gd.uid} (100%) diff --git a/project.godot b/project.godot index afb0f98..802725c 100644 --- a/project.godot +++ b/project.godot @@ -22,7 +22,6 @@ driver/enable_input=true [autoload] GameManager="*res://scripts/game_manager.gd" -MapBuilder="*res://scripts/map_builder.gd" AssetLoader="*res://scripts/asset_loader.gd" [filesystem] diff --git a/scripts/map_builder.gd b/scripts/map_loader.gd similarity index 99% rename from scripts/map_builder.gd rename to scripts/map_loader.gd index 89af055..3d38d83 100644 --- a/scripts/map_builder.gd +++ b/scripts/map_loader.gd @@ -1,3 +1,4 @@ +class_name MapLoader extends Node var items: Dictionary[int, ItemDef] diff --git a/scripts/map_builder.gd.uid b/scripts/map_loader.gd.uid similarity index 100% rename from scripts/map_builder.gd.uid rename to scripts/map_loader.gd.uid diff --git a/scripts/map_test.gd b/scripts/map_test.gd index 9f17294..09fc653 100644 --- a/scripts/map_test.gd +++ b/scripts/map_test.gd @@ -2,15 +2,19 @@ extends Node @onready var world := Node3D.new() var suzanne := preload("res://prefabs/suzanne.tscn") +var map_loader: MapLoader func _ready() -> void: + map_loader = MapLoader.new() + add_child(map_loader) + var start := Time.get_ticks_msec() - var target = MapBuilder.placements.size() + var target = map_loader.placements.size() var count := 0 var start_t := Time.get_ticks_msec() -# add_child(MapBuilder.map) - for ipl in MapBuilder.placements: - world.add_child(MapBuilder.spawn_placement(ipl)) +# add_child(map_loader.map) + for ipl in map_loader.placements: + world.add_child(map_loader.spawn_placement(ipl)) count += 1 if Time.get_ticks_msec() - start > (1.0 / 30.0) * 1000: start = Time.get_ticks_msec() diff --git a/scripts/world.gd b/scripts/world.gd index eea0795..e761945 100644 --- a/scripts/world.gd +++ b/scripts/world.gd @@ -5,15 +5,19 @@ extends Node @onready var moon = $moon @onready var sky = $WorldEnvironment var car := preload("res://scenes/car.tscn") +var map_loader: MapLoader func _ready() -> void: + map_loader = MapLoader.new() + add_child(map_loader) + var start := Time.get_ticks_msec() - var target = MapBuilder.placements.size() + var target = map_loader.placements.size() var count := 0 var start_t := Time.get_ticks_msec() -# add_child(MapBuilder.map) - for ipl in MapBuilder.placements: - world.add_child(MapBuilder.spawn_placement(ipl)) +# add_child(map_loader.map) + for ipl in map_loader.placements: + world.add_child(map_loader.spawn_placement(ipl)) count += 1 if Time.get_ticks_msec() - start > (1.0 / 30.0) * 1000: start = Time.get_ticks_msec() From bc3bec8dfe014e11d83796c76b38dd8fcb27effe Mon Sep 17 00:00:00 2001 From: Atirut Wattanamongkol Date: Tue, 4 Mar 2025 19:39:47 +0700 Subject: [PATCH 2/7] Move map node construction to MapLoader --- scripts/map_loader.gd | 23 ++++++++++++++++++++++- scripts/map_test.gd | 27 ++++++++++++--------------- scripts/world.gd | 27 ++++++++++++--------------- 3 files changed, 46 insertions(+), 31 deletions(-) diff --git a/scripts/map_loader.gd b/scripts/map_loader.gd index 3d38d83..99816b7 100644 --- a/scripts/map_loader.gd +++ b/scripts/map_loader.gd @@ -1,6 +1,9 @@ class_name MapLoader extends Node +signal loading_progress(progress_percent: float) +signal loading_completed(map_node: Node3D) + var items: Dictionary[int, ItemDef] var itemchilds: Array[TDFX] var placements: Array[ItemPlacement] @@ -118,8 +121,26 @@ func _read_map_data(path: String, line_handler: Callable) -> void: else: line_handler.call(section, tokens) -func clear_map() -> void: +func load_map() -> Node3D: map = Node3D.new() + + var start := Time.get_ticks_msec() + var target = placements.size() + var count := 0 + var start_t := Time.get_ticks_msec() + + for ipl in placements: + map.add_child(spawn_placement(ipl)) + count += 1 + if Time.get_ticks_msec() - start > (1.0 / 30.0) * 1000: + start = Time.get_ticks_msec() + var progress = float(count) / float(target) + emit_signal("loading_progress", progress) + await get_tree().physics_frame + + print("Map load completed in %f seconds" % ((Time.get_ticks_msec() - start_t) / 1000.0)) + emit_signal("loading_completed", map) + return map func spawn_placement(ipl: ItemPlacement) -> Node3D: return spawn(ipl.id, ipl.model_name, ipl.position, ipl.scale, ipl.rotation) diff --git a/scripts/map_test.gd b/scripts/map_test.gd index 09fc653..81636f9 100644 --- a/scripts/map_test.gd +++ b/scripts/map_test.gd @@ -1,6 +1,5 @@ extends Node -@onready var world := Node3D.new() var suzanne := preload("res://prefabs/suzanne.tscn") var map_loader: MapLoader @@ -8,20 +7,18 @@ func _ready() -> void: map_loader = MapLoader.new() add_child(map_loader) - var start := Time.get_ticks_msec() - var target = map_loader.placements.size() - var count := 0 - var start_t := Time.get_ticks_msec() -# add_child(map_loader.map) - for ipl in map_loader.placements: - world.add_child(map_loader.spawn_placement(ipl)) - count += 1 - if Time.get_ticks_msec() - start > (1.0 / 30.0) * 1000: - start = Time.get_ticks_msec() - print("%f" % (float(count) / float(target))) - await get_tree().physics_frame - print("Map load completed in %f seconds" % ((Time.get_ticks_msec() - start_t) / 1000)) - add_child(world) + # Connect signals for progress updates + map_loader.loading_progress.connect(_on_loading_progress) + map_loader.loading_completed.connect(_on_loading_completed) + + # Start loading the map + await map_loader.load_map() + +func _on_loading_progress(progress: float) -> void: + print("Loading: %d%%" % int(progress * 100)) + +func _on_loading_completed(world_node: Node3D) -> void: + add_child(world_node) func _unhandled_input(event: InputEvent) -> void: if event is InputEventKey: diff --git a/scripts/world.gd b/scripts/world.gd index e761945..371e1f1 100644 --- a/scripts/world.gd +++ b/scripts/world.gd @@ -1,6 +1,5 @@ extends Node -@onready var world := Node3D.new() @onready var sun = $sun @onready var moon = $moon @onready var sky = $WorldEnvironment @@ -11,20 +10,18 @@ func _ready() -> void: map_loader = MapLoader.new() add_child(map_loader) - var start := Time.get_ticks_msec() - var target = map_loader.placements.size() - var count := 0 - var start_t := Time.get_ticks_msec() -# add_child(map_loader.map) - for ipl in map_loader.placements: - world.add_child(map_loader.spawn_placement(ipl)) - count += 1 - if Time.get_ticks_msec() - start > (1.0 / 30.0) * 1000: - start = Time.get_ticks_msec() - print("%f" % (float(count) / float(target))) - await get_tree().physics_frame - print("Map load completed in %f seconds" % ((Time.get_ticks_msec() - start_t) / 1000)) - add_child(world) + # Connect signals for progress updates + map_loader.loading_progress.connect(_on_loading_progress) + map_loader.loading_completed.connect(_on_loading_completed) + + # Start loading the map + await map_loader.load_map() + +func _on_loading_progress(progress: float) -> void: + print("Loading: %d%%" % int(progress * 100)) + +func _on_loading_completed(world_node: Node3D) -> void: + add_child(world_node) sky.environment = load("res://scenes/world/day.tres") moon.visible = not moon.visible From 1e0c3c136b5fe357046108700415d1d19ab79404 Mon Sep 17 00:00:00 2001 From: Atirut Wattanamongkol Date: Wed, 5 Mar 2025 11:36:30 +0700 Subject: [PATCH 3/7] Map Loader: move map file parsing to load time --- scripts/map_loader.gd | 66 +++++++++++++++++++++++-------------------- 1 file changed, 35 insertions(+), 31 deletions(-) diff --git a/scripts/map_loader.gd b/scripts/map_loader.gd index 99816b7..58f7f06 100644 --- a/scripts/map_loader.gd +++ b/scripts/map_loader.gd @@ -12,37 +12,7 @@ var map: Node3D var _loaded := false func _ready() -> void: - var file := FileAccess.open(GameManager.gta_path + "data/gta3.dat", FileAccess.READ) - assert(file != null, "%d" % FileAccess.get_open_error()) - while not file.eof_reached(): - var line := file.get_line() - if not line.begins_with("#"): - var tokens := line.split(" ", false) - if tokens.size() > 0: - match tokens[0]: - "IDE": - _read_map_data(tokens[1], _read_ide_line) - "COLFILE": - var colfile := AssetLoader.open(GameManager.gta_path + tokens[2]) - - while colfile.get_position() < colfile.get_length(): - collisions.append(ColFile.new(colfile)) - "IPL": - _read_map_data(tokens[1], _read_ipl_line) - "CDIMAGE": - AssetLoader.load_cd_image(tokens[1]) - _: - push_warning("implement %s" % tokens[0]) - for child in itemchilds: - items[child.parent].childs.append(child) - for colfile in collisions: - if colfile.model_id in items: - items[colfile.model_id].colfile = colfile - else: - for k in items: - var item := items[k] as ItemDef - if item.model_name.matchn(colfile.model_name): - items[k].colfile = colfile + pass func _read_ide_line(section: String, tokens: Array[String]): var item := ItemDef.new() @@ -122,6 +92,40 @@ func _read_map_data(path: String, line_handler: Callable) -> void: line_handler.call(section, tokens) func load_map() -> Node3D: + if not _loaded: + var file := FileAccess.open(GameManager.gta_path + "data/gta3.dat", FileAccess.READ) + assert(file != null, "%d" % FileAccess.get_open_error()) + while not file.eof_reached(): + var line := file.get_line() + if not line.begins_with("#"): + var tokens := line.split(" ", false) + if tokens.size() > 0: + match tokens[0]: + "IDE": + _read_map_data(tokens[1], _read_ide_line) + "COLFILE": + var colfile := AssetLoader.open(GameManager.gta_path + tokens[2]) + + while colfile.get_position() < colfile.get_length(): + collisions.append(ColFile.new(colfile)) + "IPL": + _read_map_data(tokens[1], _read_ipl_line) + "CDIMAGE": + AssetLoader.load_cd_image(tokens[1]) + _: + push_warning("implement %s" % tokens[0]) + for child in itemchilds: + items[child.parent].childs.append(child) + for colfile in collisions: + if colfile.model_id in items: + items[colfile.model_id].colfile = colfile + else: + for k in items: + var item := items[k] as ItemDef + if item.model_name.matchn(colfile.model_name): + items[k].colfile = colfile + _loaded = true + map = Node3D.new() var start := Time.get_ticks_msec() From c7406cfeb6e5f2f63fde1a6814c6db470023829a Mon Sep 17 00:00:00 2001 From: Atirut Wattanamongkol Date: Wed, 5 Mar 2025 11:43:51 +0700 Subject: [PATCH 4/7] Map Loader: fix TODO --- scripts/map_loader.gd | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/scripts/map_loader.gd b/scripts/map_loader.gd index 58f7f06..af6611b 100644 --- a/scripts/map_loader.gd +++ b/scripts/map_loader.gd @@ -164,11 +164,10 @@ func spawn(id: int, model_name: String, position: Vector3, scale: Vector3, rotat light.position = child.position light.light_color = child.color light.distance_fade_enabled = true - # TODO: Remove half distance when https://github.com/godotengine/godot/issues/56657 is solved - light.distance_fade_begin = child.render_distance / 2.0 + light.distance_fade_begin = child.render_distance light.omni_range = child.range light.light_energy = float(child.shadow_intensity) / 20.0 -# light.shadow_enabled = true + light.shadow_enabled = true instance.add_child(light) var sb := StaticBody3D.new() if item.colfile != null: From d2eba5e18579096c54366799298f25adb6de9172 Mon Sep 17 00:00:00 2001 From: Atirut Wattanamongkol Date: Wed, 5 Mar 2025 11:54:33 +0700 Subject: [PATCH 5/7] Map Loader: clean up variables --- scripts/map_loader.gd | 52 ++++++++++++++++++++++--------------------- 1 file changed, 27 insertions(+), 25 deletions(-) diff --git a/scripts/map_loader.gd b/scripts/map_loader.gd index af6611b..b0e25e1 100644 --- a/scripts/map_loader.gd +++ b/scripts/map_loader.gd @@ -4,12 +4,11 @@ extends Node signal loading_progress(progress_percent: float) signal loading_completed(map_node: Node3D) -var items: Dictionary[int, ItemDef] -var itemchilds: Array[TDFX] -var placements: Array[ItemPlacement] -var collisions: Array[ColFile] -var map: Node3D -var _loaded := false +var _items: Dictionary[int, ItemDef] +var _itemchilds: Array[TDFX] +var _placements: Array[ItemPlacement] +var _collisions: Array[ColFile] +var _parsed := false func _ready() -> void: pass @@ -23,12 +22,12 @@ func _read_ide_line(section: String, tokens: Array[String]): item.txd_name = tokens[2] item.render_distance = tokens[4].to_float() item.flags = tokens[tokens.size() - 1].to_int() - items[id] = item + _items[id] = item "tobj": # TODO: Timed objects item.model_name = tokens[1] item.txd_name = tokens[2] - items[id] = item + _items[id] = item "2dfx": var parent := tokens[0].to_int() # Convert GTA to Godot coordinate system @@ -49,7 +48,7 @@ func _read_ide_line(section: String, tokens: Array[String]): lightdef.render_distance = tokens[11].to_float() lightdef.range = tokens[12].to_float() lightdef.shadow_intensity = tokens[15].to_int() - itemchilds.append(lightdef) + _itemchilds.append(lightdef) var type: push_warning("implement 2DFX type %d" % type) @@ -75,7 +74,7 @@ func _read_ipl_line(section: String, tokens: Array[String]): -tokens[10].to_float(), -tokens[9].to_float(), tokens[11].to_float(), ) - placements.append(placement) + _placements.append(placement) func _read_map_data(path: String, line_handler: Callable) -> void: var file := AssetLoader.open(path) @@ -92,9 +91,11 @@ func _read_map_data(path: String, line_handler: Callable) -> void: line_handler.call(section, tokens) func load_map() -> Node3D: - if not _loaded: + if not _parsed: var file := FileAccess.open(GameManager.gta_path + "data/gta3.dat", FileAccess.READ) assert(file != null, "%d" % FileAccess.get_open_error()) + + print("Loading map data...") while not file.eof_reached(): var line := file.get_line() if not line.begins_with("#"): @@ -107,33 +108,34 @@ func load_map() -> Node3D: var colfile := AssetLoader.open(GameManager.gta_path + tokens[2]) while colfile.get_position() < colfile.get_length(): - collisions.append(ColFile.new(colfile)) + _collisions.append(ColFile.new(colfile)) "IPL": _read_map_data(tokens[1], _read_ipl_line) "CDIMAGE": AssetLoader.load_cd_image(tokens[1]) _: push_warning("implement %s" % tokens[0]) - for child in itemchilds: - items[child.parent].childs.append(child) - for colfile in collisions: - if colfile.model_id in items: - items[colfile.model_id].colfile = colfile + for child in _itemchilds: + _items[child.parent].childs.append(child) + for colfile in _collisions: + if colfile.model_id in _items: + _items[colfile.model_id].colfile = colfile else: - for k in items: - var item := items[k] as ItemDef + for k in _items: + var item := _items[k] as ItemDef if item.model_name.matchn(colfile.model_name): - items[k].colfile = colfile - _loaded = true + _items[k].colfile = colfile + _parsed = true - map = Node3D.new() + var map := Node3D.new() var start := Time.get_ticks_msec() - var target = placements.size() + var target = _placements.size() var count := 0 var start_t := Time.get_ticks_msec() - for ipl in placements: + print("Loading map...") + for ipl in _placements: map.add_child(spawn_placement(ipl)) count += 1 if Time.get_ticks_msec() - start > (1.0 / 30.0) * 1000: @@ -150,7 +152,7 @@ func spawn_placement(ipl: ItemPlacement) -> Node3D: return spawn(ipl.id, ipl.model_name, ipl.position, ipl.scale, ipl.rotation) func spawn(id: int, model_name: String, position: Vector3, scale: Vector3, rotation: Quaternion) -> Node3D: - var item := items[id] as ItemDef + var item := _items[id] as ItemDef if item.flags & 0x40: return Node3D.new() var instance := StreamedMesh.new(item) From 748bc62a63b286c3bd806fc7076207cdf6c821d3 Mon Sep 17 00:00:00 2001 From: Atirut Wattanamongkol Date: Wed, 5 Mar 2025 13:10:38 +0700 Subject: [PATCH 6/7] Map Loader: make loading map blocking --- scripts/map_loader.gd | 90 ++++++++++++++++++++++--------------------- scripts/map_test.gd | 10 ++--- scripts/world.gd | 14 +++---- 3 files changed, 57 insertions(+), 57 deletions(-) diff --git a/scripts/map_loader.gd b/scripts/map_loader.gd index b0e25e1..89f9313 100644 --- a/scripts/map_loader.gd +++ b/scripts/map_loader.gd @@ -2,7 +2,6 @@ class_name MapLoader extends Node signal loading_progress(progress_percent: float) -signal loading_completed(map_node: Node3D) var _items: Dictionary[int, ItemDef] var _itemchilds: Array[TDFX] @@ -90,62 +89,65 @@ func _read_map_data(path: String, line_handler: Callable) -> void: else: line_handler.call(section, tokens) -func load_map() -> Node3D: - if not _parsed: - var file := FileAccess.open(GameManager.gta_path + "data/gta3.dat", FileAccess.READ) - assert(file != null, "%d" % FileAccess.get_open_error()) +func parse_map_data() -> void: + if _parsed: + return + + var file := FileAccess.open(GameManager.gta_path + "data/gta3.dat", FileAccess.READ) + assert(file != null, "%d" % FileAccess.get_open_error()) - print("Loading map data...") - while not file.eof_reached(): - var line := file.get_line() - if not line.begins_with("#"): - var tokens := line.split(" ", false) - if tokens.size() > 0: - match tokens[0]: - "IDE": - _read_map_data(tokens[1], _read_ide_line) - "COLFILE": - var colfile := AssetLoader.open(GameManager.gta_path + tokens[2]) - - while colfile.get_position() < colfile.get_length(): - _collisions.append(ColFile.new(colfile)) - "IPL": - _read_map_data(tokens[1], _read_ipl_line) - "CDIMAGE": - AssetLoader.load_cd_image(tokens[1]) - _: - push_warning("implement %s" % tokens[0]) - for child in _itemchilds: - _items[child.parent].childs.append(child) - for colfile in _collisions: - if colfile.model_id in _items: - _items[colfile.model_id].colfile = colfile - else: - for k in _items: - var item := _items[k] as ItemDef - if item.model_name.matchn(colfile.model_name): - _items[k].colfile = colfile - _parsed = true + print("Loading map data...") + while not file.eof_reached(): + var line := file.get_line() + if not line.begins_with("#"): + var tokens := line.split(" ", false) + if tokens.size() > 0: + match tokens[0]: + "IDE": + _read_map_data(tokens[1], _read_ide_line) + "COLFILE": + var colfile := AssetLoader.open(GameManager.gta_path + tokens[2]) + + while colfile.get_position() < colfile.get_length(): + _collisions.append(ColFile.new(colfile)) + "IPL": + _read_map_data(tokens[1], _read_ipl_line) + "CDIMAGE": + AssetLoader.load_cd_image(tokens[1]) + _: + push_warning("implement %s" % tokens[0]) + for child in _itemchilds: + _items[child.parent].childs.append(child) + for colfile in _collisions: + if colfile.model_id in _items: + _items[colfile.model_id].colfile = colfile + else: + for k in _items: + var item := _items[k] as ItemDef + if item.model_name.matchn(colfile.model_name): + _items[k].colfile = colfile + _parsed = true + +func load_map() -> Node3D: + # Make sure map data is parsed + parse_map_data() var map := Node3D.new() - var start := Time.get_ticks_msec() + var start_t := Time.get_ticks_msec() var target = _placements.size() var count := 0 - var start_t := Time.get_ticks_msec() print("Loading map...") for ipl in _placements: map.add_child(spawn_placement(ipl)) count += 1 - if Time.get_ticks_msec() - start > (1.0 / 30.0) * 1000: - start = Time.get_ticks_msec() - var progress = float(count) / float(target) - emit_signal("loading_progress", progress) - await get_tree().physics_frame + + # No await statement - calculate and emit progress, but don't yield + var progress = float(count) / float(target) + call_deferred("emit_signal", "loading_progress", progress) print("Map load completed in %f seconds" % ((Time.get_ticks_msec() - start_t) / 1000.0)) - emit_signal("loading_completed", map) return map func spawn_placement(ipl: ItemPlacement) -> Node3D: diff --git a/scripts/map_test.gd b/scripts/map_test.gd index 81636f9..db258cd 100644 --- a/scripts/map_test.gd +++ b/scripts/map_test.gd @@ -9,16 +9,14 @@ func _ready() -> void: # Connect signals for progress updates map_loader.loading_progress.connect(_on_loading_progress) - map_loader.loading_completed.connect(_on_loading_completed) - # Start loading the map - await map_loader.load_map() + # TODO: Implement a proper loading screen before enabling threaded loading + # For now, load the map directly + var map := map_loader.load_map() + add_child(map) func _on_loading_progress(progress: float) -> void: print("Loading: %d%%" % int(progress * 100)) - -func _on_loading_completed(world_node: Node3D) -> void: - add_child(world_node) func _unhandled_input(event: InputEvent) -> void: if event is InputEventKey: diff --git a/scripts/world.gd b/scripts/world.gd index 371e1f1..589d0bd 100644 --- a/scripts/world.gd +++ b/scripts/world.gd @@ -12,19 +12,19 @@ func _ready() -> void: # Connect signals for progress updates map_loader.loading_progress.connect(_on_loading_progress) - map_loader.loading_completed.connect(_on_loading_completed) - # Start loading the map - await map_loader.load_map() - -func _on_loading_progress(progress: float) -> void: - print("Loading: %d%%" % int(progress * 100)) + # TODO: Implement a proper loading screen before enabling threaded loading + # For now, load the map directly + var world_node := map_loader.load_map() -func _on_loading_completed(world_node: Node3D) -> void: + # Add world to scene and setup environment add_child(world_node) sky.environment = load("res://scenes/world/day.tres") moon.visible = not moon.visible +func _on_loading_progress(progress: float) -> void: + print("Loading: %d%%" % int(progress * 100)) + func _unhandled_input(event: InputEvent) -> void: if Input.is_action_pressed("spawn"): var car_node := car.instantiate() From 9dea21f638ea1e7126655f21eabf263f35e731a3 Mon Sep 17 00:00:00 2001 From: Atirut Wattanamongkol Date: Wed, 5 Mar 2025 22:07:14 +0700 Subject: [PATCH 7/7] Map Loader: correctly implement LoDs --- scripts/classes/item_def.gd | 6 + scripts/classes/streamed_mesh.gd | 138 ++++++++++++++++++--- scripts/map_loader.gd | 205 +++++++++++++++++++++++-------- 3 files changed, 277 insertions(+), 72 deletions(-) diff --git a/scripts/classes/item_def.gd b/scripts/classes/item_def.gd index 1ccc64c..f4e7fa7 100644 --- a/scripts/classes/item_def.gd +++ b/scripts/classes/item_def.gd @@ -7,3 +7,9 @@ var render_distance: float var flags: int var childs: Array[TDFX] var colfile: ColFile + +# LoD system additions +var lod_distances: Array[float] = [] +var num_lods: int = 0 +var is_big_building: bool = false +var related_model: ItemDef = null diff --git a/scripts/classes/streamed_mesh.gd b/scripts/classes/streamed_mesh.gd index e5ad5b2..37034ed 100644 --- a/scripts/classes/streamed_mesh.gd +++ b/scripts/classes/streamed_mesh.gd @@ -1,40 +1,140 @@ class_name StreamedMesh extends MeshInstance3D +# LoD System Constants +const DRAW_DISTANCE_FACTOR = 1.5 +const MAGIC_LOD_DISTANCE = 330.0 +const VEHICLE_LOD_DISTANCE = 70.0 +const VEHICLE_DRAW_DISTANCE = 280.0 + var _idef: ItemDef var _thread := Thread.new() -var _mesh_buf: Mesh +var _mesh_buf: Array[Mesh] = [] # Array to store different LoD meshes +var _current_lod_level: int = -1 # Current LoD level being displayed +var _lod_nodes: Array[MeshInstance3D] = [] # Child nodes for LoD models func _init(idef: ItemDef): _idef = idef + + # Create child nodes for each LoD level + if _idef.num_lods > 0: + for i in range(_idef.num_lods): + var lod_node := MeshInstance3D.new() + lod_node.name = "LOD_" + str(i) + lod_node.visible = false + add_child(lod_node) + _lod_nodes.append(lod_node) + _mesh_buf.append(null) + else: + # Default mesh buffer for base model + _mesh_buf.append(null) func _exit_tree(): if _thread.is_alive(): _thread.wait_to_finish() func _process(delta: float) -> void: - if _thread.is_started() == false: - if get_viewport().get_camera_3d() != null: - var dist := get_viewport().get_camera_3d().global_position.distance_to(global_position) - if dist < visibility_range_end and mesh == null: - _thread.start(_load_mesh) - while _thread.is_alive(): - await get_tree().process_frame - _thread.wait_to_finish() - mesh = _mesh_buf - elif dist > visibility_range_end and mesh != null: - mesh = null - -func _load_mesh() -> void: + if get_viewport().get_camera_3d() == null: + return + + # Calculate distance to camera + var camera_pos = get_viewport().get_camera_3d().global_position + var raw_distance = camera_pos.distance_to(global_position) + var distance = raw_distance / DRAW_DISTANCE_FACTOR + + # Select appropriate LoD level based on distance + var selected_lod = _select_lod_level(distance) + + # If LoD level changed or mesh not loaded, load the appropriate mesh + if selected_lod != _current_lod_level or (selected_lod >= 0 and _get_active_mesh() == null): + _current_lod_level = selected_lod + + # Hide all LoD nodes first + for node in _lod_nodes: + node.visible = false + mesh = null + + # If object is too far, don't show anything + if selected_lod < 0: + return + + # If we need to load a mesh but haven't yet + if _thread.is_started() == false and _mesh_buf[selected_lod] == null: + _thread.start(Callable(_load_mesh).bind(selected_lod)) + while _thread.is_alive(): + await get_tree().process_frame + _thread.wait_to_finish() + + # Show the appropriate mesh + if selected_lod == 0: + # Base model goes on the main instance + mesh = _mesh_buf[0] + elif selected_lod < _lod_nodes.size() + 1: + # LoD models go on child nodes + _lod_nodes[selected_lod - 1].mesh = _mesh_buf[selected_lod] + _lod_nodes[selected_lod - 1].visible = true + +func _select_lod_level(distance: float) -> int: + # Special handling for big buildings + if _idef.is_big_building: + if distance < MAGIC_LOD_DISTANCE and _idef.related_model != null: + return 0 # Show detailed model + return -1 # Too far, don't show + + # Normal LoD selection + if _idef.lod_distances.size() > 0: + # Check against each LoD distance threshold + for i in range(_idef.lod_distances.size()): + if distance < _idef.lod_distances[i]: + return i # Return the appropriate LoD level + + # Object is too far away, don't render + return -1 + else: + # No LoD information, use simple visibility range + return 0 if distance < _idef.render_distance else -1 + +func _get_active_mesh() -> Mesh: + if _current_lod_level == 0: + return mesh + elif _current_lod_level > 0 and _current_lod_level <= _lod_nodes.size(): + return _lod_nodes[_current_lod_level - 1].mesh + return null + +func _get_lod_model_name(lod_level: int) -> String: + var base_name = _idef.model_name + if lod_level == 0: + return base_name + else: + return base_name + "_l" + str(lod_level) + +func _load_mesh(lod_level: int) -> void: AssetLoader.mutex.lock() if _idef.flags & 0x40: + AssetLoader.mutex.unlock() return - var access := AssetLoader.open_asset(_idef.model_name + ".dff") + + # Get model name with appropriate LoD suffix + var model_name = _get_lod_model_name(lod_level) + + # Try to open the asset file + var access = AssetLoader.open_asset(model_name + ".dff") + if access == null: + # If the specific LoD model doesn't exist, fall back to the base model + if lod_level > 0: + access = AssetLoader.open_asset(_idef.model_name + ".dff") + + # If still no model, exit + if access == null: + AssetLoader.mutex.unlock() + return + + # Load the mesh geometry var glist := RWClump.new(access).geometry_list for geometry in glist.geometries: - _mesh_buf = geometry.mesh - for surf_id in _mesh_buf.get_surface_count(): - var material := _mesh_buf.surface_get_material(surf_id) as StandardMaterial3D + _mesh_buf[lod_level] = geometry.mesh + for surf_id in _mesh_buf[lod_level].get_surface_count(): + var material := _mesh_buf[lod_level].surface_get_material(surf_id) as StandardMaterial3D material.cull_mode = BaseMaterial3D.CULL_DISABLED if _idef.flags & 0x08: material.blend_mode = BaseMaterial3D.BLEND_MODE_ADD @@ -50,5 +150,5 @@ func _load_mesh() -> void: BaseMaterial3D.TRANSPARENCY_ALPHA_HASH if _idef.flags & 0x04 and not _idef.flags & 0x08 else BaseMaterial3D.TRANSPARENCY_ALPHA ) break - _mesh_buf.surface_set_material(surf_id, material) + _mesh_buf[lod_level].surface_set_material(surf_id, material) AssetLoader.mutex.unlock() diff --git a/scripts/map_loader.gd b/scripts/map_loader.gd index 89f9313..99b78ef 100644 --- a/scripts/map_loader.gd +++ b/scripts/map_loader.gd @@ -19,13 +19,46 @@ func _read_ide_line(section: String, tokens: Array[String]): "objs": item.model_name = tokens[1] item.txd_name = tokens[2] + + # Parse LoD information + var num_lods = tokens[3].to_int() + item.num_lods = num_lods + + # Set render distance for the base model item.render_distance = tokens[4].to_float() + + # Parse LoD distances if available + for i in range(num_lods): + if 4 + i < tokens.size() - 1: # Avoid reading flags as LoD distance + var lod_distance = tokens[4 + i].to_float() + item.lod_distances.append(lod_distance) + + # Check if this is a big building (based on research notes) + if item.lod_distances.size() > 0 and item.lod_distances[0] > 300.0 and num_lods < 3: + item.is_big_building = true + # Note: related model association will be done after all models are loaded + item.flags = tokens[tokens.size() - 1].to_int() _items[id] = item "tobj": # TODO: Timed objects item.model_name = tokens[1] item.txd_name = tokens[2] + + # Parse LoD information for timed objects too + if tokens.size() > 4: + var num_lods = tokens[3].to_int() + item.num_lods = num_lods + + # Set render distance for the base model + item.render_distance = tokens[4].to_float() + + # Parse LoD distances if available + for i in range(num_lods): + if 4 + i < tokens.size() - 1: # Avoid reading flags as LoD distance + var lod_distance = tokens[4 + i].to_float() + item.lod_distances.append(lod_distance) + _items[id] = item "2dfx": var parent := tokens[0].to_int() @@ -89,6 +122,37 @@ func _read_map_data(path: String, line_handler: Callable) -> void: else: line_handler.call(section, tokens) +func _find_related_models() -> void: + # Associate big buildings with their related low-detail models + # Based on the research notes, big buildings follow specific naming patterns + # For example, "LODxxx" is matched with "HDRxxx" + + var lod_models := {} + var hd_models := {} + + # First, collect all potential LOD and HD models by naming convention + for id in _items: + var item := _items[id] as ItemDef + var model_name := item.model_name.to_lower() + + if model_name.begins_with("lod"): + lod_models[model_name.substr(3)] = id + elif model_name.begins_with("hdr"): + hd_models[model_name.substr(3)] = id + + # Now associate the related models + for suffix in lod_models: + if suffix in hd_models: + var lod_id = lod_models[suffix] + var hd_id = hd_models[suffix] + + # Associate the HD model with its LOD model + if _items[hd_id].is_big_building: + _items[hd_id].related_model = _items[lod_id] + + # Also check for other naming patterns if needed + # (Add more patterns based on GTA3 specific conventions) + func parse_map_data() -> void: if _parsed: return @@ -126,6 +190,10 @@ func parse_map_data() -> void: var item := _items[k] as ItemDef if item.model_name.matchn(colfile.model_name): _items[k].colfile = colfile + + # Find and associate related models for big buildings + _find_related_models() + _parsed = true func load_map() -> Node3D: @@ -157,58 +225,89 @@ func spawn(id: int, model_name: String, position: Vector3, scale: Vector3, rotat var item := _items[id] as ItemDef if item.flags & 0x40: return Node3D.new() - var instance := StreamedMesh.new(item) - instance.position = position - instance.scale = scale - instance.quaternion = rotation - instance.visibility_range_end = item.render_distance - for child in item.childs: - if child is TDFXLight: - var light := OmniLight3D.new() - light.position = child.position - light.light_color = child.color - light.distance_fade_enabled = true - light.distance_fade_begin = child.render_distance - light.omni_range = child.range - light.light_energy = float(child.shadow_intensity) / 20.0 - light.shadow_enabled = true - instance.add_child(light) - var sb := StaticBody3D.new() - if item.colfile != null: - for collision in item.colfile.collisions: - var colshape := CollisionShape3D.new() - if collision is ColFile.TBox: - var aabb := AABB() - # Get min and max positions from collision box - var min_pos := collision.min as Vector3 - var max_pos := collision.max as Vector3 - - # Ensure AABB has positive size by sorting min/max for each axis - aabb.position = Vector3( - min(min_pos.x, max_pos.x), - min(min_pos.y, max_pos.y), - min(min_pos.z, max_pos.z) - ) - aabb.end = Vector3( - max(min_pos.x, max_pos.x), - max(min_pos.y, max_pos.y), - max(min_pos.z, max_pos.z) - ) - - # Only create the shape if size is valid - if aabb.size.x > 0 and aabb.size.y > 0 and aabb.size.z > 0: - var shape := BoxShape3D.new() - shape.size = aabb.size - colshape.shape = shape - colshape.position = aabb.get_center() + + # Create a Node3D container for big buildings with related models + var container: Node3D + + if item.is_big_building and item.related_model != null: + # For big buildings, create a container node + container = Node3D.new() + container.name = "BigBuilding_" + str(id) + container.position = position + container.scale = scale + container.quaternion = rotation + + # Create the high-detail model + var high_detail := StreamedMesh.new(item) + high_detail.name = "HighDetail" + container.add_child(high_detail) + + # Create the low-detail model using the related model definition + if item.related_model != null: + var low_detail := StreamedMesh.new(item.related_model) + low_detail.name = "LowDetail" + container.add_child(low_detail) + + # Add a script to handle switching between high and low detail + # (The StreamedMesh already handles this via _select_lod_level) + else: + # For regular models + container = StreamedMesh.new(item) + container.position = position + container.scale = scale + container.quaternion = rotation + + # Add effects and child objects + for child in item.childs: + if child is TDFXLight: + var light := OmniLight3D.new() + light.position = child.position + light.light_color = child.color + light.distance_fade_enabled = true + light.distance_fade_begin = child.render_distance + light.omni_range = child.range + light.light_energy = float(child.shadow_intensity) / 20.0 + light.shadow_enabled = true + container.add_child(light) + + # Add collision + var sb := StaticBody3D.new() + if item.colfile != null: + for collision in item.colfile.collisions: + var colshape := CollisionShape3D.new() + if collision is ColFile.TBox: + var aabb := AABB() + # Get min and max positions from collision box + var min_pos := collision.min as Vector3 + var max_pos := collision.max as Vector3 + + # Ensure AABB has positive size by sorting min/max for each axis + aabb.position = Vector3( + min(min_pos.x, max_pos.x), + min(min_pos.y, max_pos.y), + min(min_pos.z, max_pos.z) + ) + aabb.end = Vector3( + max(min_pos.x, max_pos.x), + max(min_pos.y, max_pos.y), + max(min_pos.z, max_pos.z) + ) + + # Only create the shape if size is valid + if aabb.size.x > 0 and aabb.size.y > 0 and aabb.size.z > 0: + var shape := BoxShape3D.new() + shape.size = aabb.size + colshape.shape = shape + colshape.position = aabb.get_center() + sb.add_child(colshape) + else: sb.add_child(colshape) - else: + if item.colfile.vertices.size() > 0: + var colshape := CollisionShape3D.new() + var shape := ConcavePolygonShape3D.new() + shape.set_faces(item.colfile.vertices) + colshape.shape = shape sb.add_child(colshape) - if item.colfile.vertices.size() > 0: - var colshape := CollisionShape3D.new() - var shape := ConcavePolygonShape3D.new() - shape.set_faces(item.colfile.vertices) - colshape.shape = shape - sb.add_child(colshape) - instance.add_child(sb) - return instance + container.add_child(sb) + + return container