mirror of
https://github.com/FOSS-Supremacy/OpenLiberty.git
synced 2025-04-28 11:57:58 +03:00
Map Loader: correctly implement LoDs
This commit is contained in:
parent
748bc62a63
commit
9dea21f638
3 changed files with 276 additions and 71 deletions
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue