Map Loader: correctly implement LoDs

This commit is contained in:
Atirut Wattanamongkol 2025-03-05 22:07:14 +07:00
parent 748bc62a63
commit 9dea21f638
3 changed files with 276 additions and 71 deletions

View file

@ -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

View file

@ -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
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 _load_mesh() -> void:
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()

View file

@ -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