Skip to content
1 change: 1 addition & 0 deletions __init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
gui.SCENE_OT_dff_update,
gui.SCENE_OT_dff_import_map,
gui.SCENE_OT_ipl_select,
gui.SCENE_OT_adjust_viewport,
gui.OBJECT_OT_dff_generate_bone_props,
gui.OBJECT_OT_dff_set_parent_bone,
gui.OBJECT_OT_dff_clear_parent_bone,
Expand Down
16 changes: 15 additions & 1 deletion gtaLib/map.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,16 @@

from .data import map_data
from .img import img
from mathutils import Vector

#######################################################
@dataclass
class MapData:
object_instances: list
object_map_sections: dict
object_data: dict
cull_instances: list
map_objects_centroid: Vector

#######################################################
@dataclass
Expand Down Expand Up @@ -354,15 +357,24 @@ def load_map_data(game_id, game_root, ipl_section, is_custom_ipl):
object_instances = []
cull_instances = []
object_data = {}
object_map_sections = {}

# Get all insts into a flat list (array)
# Can't be an ID keyed dictionary, because there's many ipl
# entries with the same ID - multiple pieces of
# the same model (lamps, benches, trees etc.)
map_objects_centroid = Vector((0.0, 0.0, 0.0))
i = 1.0
if 'inst' in ipl:
for entry in ipl['inst']:
object_map_sections[entry] = ipl_section
object_instances.append(entry)

# Keep a running average of the instance positions to later focus the camera before load
map_objects_centroid += (Vector((float(entry.posX), float(entry.posY), float(entry.posZ)))
- map_objects_centroid) / i
i += 1.0

# Get all culls into a flat list (array)
if 'cull' in ipl:
for entry in ipl['cull']:
Expand All @@ -384,7 +396,9 @@ def load_map_data(game_id, game_root, ipl_section, is_custom_ipl):
return MapData(
object_instances = object_instances,
object_data = object_data,
cull_instances = cull_instances
cull_instances = cull_instances,
object_map_sections = object_map_sections,
map_objects_centroid = map_objects_centroid
)

########################################################################
Expand Down
22 changes: 8 additions & 14 deletions gui/col_ot.py
Original file line number Diff line number Diff line change
Expand Up @@ -263,13 +263,10 @@ class OBJECT_OT_dff_add_collision_box(bpy.types.Operator, AddCollisionHelper):

#######################################################
def execute(self, context):
ret = bpy.ops.object.empty_add(type='CUBE', radius=self.radius, location=self.location)
if ret != {'FINISHED'}:
return ret

obj = context.object

obj.name = "ColBox"
obj = bpy.data.objects.new("ColBox", None)
obj.empty_draw_type = 'CUBE'
obj.radius = self.radius
obj.location = self.location
obj.dff.type = 'COL'

obj.lock_rotation[0] = True
Expand All @@ -288,13 +285,10 @@ class OBJECT_OT_dff_add_collision_sphere(bpy.types.Operator, AddCollisionHelper)

#######################################################
def execute(self, context):
ret = bpy.ops.object.empty_add(type='SPHERE', radius=self.radius, location=self.location)
if ret != {'FINISHED'}:
return ret

obj = context.object

obj.name = "ColSphere"
obj = bpy.data.objects.new("ColSphere", None)
obj.empty_draw_type = 'SPHERE'
obj.radius = self.radius
obj.location = self.location
obj.dff.type = 'COL'

obj.lock_rotation[0] = True
Expand Down
2 changes: 1 addition & 1 deletion gui/dff_ot.py
Original file line number Diff line number Diff line change
Expand Up @@ -422,7 +422,7 @@ def execute(self, context):
# Import DFF and COL objects
for file in file_paths:
if file.lower().endswith(".col"):
col_list = col_importer.import_col_file(file, os.path.basename(file))
col_list = col_importer.import_col_file(file)
# Move all collisions to a top collection named for the file they came from
collection = bpy.data.collections.new(os.path.basename(file))
context.scene.collection.children.link(collection)
Expand Down
76 changes: 53 additions & 23 deletions gui/map_menus.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,19 +76,26 @@ def atomics_active_changed(self, context):
game_version_dropdown : bpy.props.EnumProperty(
name = 'Game',
items = (
(map_data.game_version.III, 'GTA III', 'GTA III map segments'),
(map_data.game_version.VC, 'GTA VC', 'GTA VC map segments'),
(map_data.game_version.SA, 'GTA SA', 'GTA SA map segments'),
(map_data.game_version.LCS, 'GTA LCS', 'GTA LCS map segments'),
(map_data.game_version.VCS, 'GTA VCS', 'GTA VCS map segments'),
(map_data.game_version.III, 'GTA III', 'GTA III map sections'),
(map_data.game_version.VC, 'GTA VC', 'GTA VC map sections'),
(map_data.game_version.SA, 'GTA SA', 'GTA SA map sections'),
(map_data.game_version.LCS, 'GTA LCS', 'GTA LCS map sections'),
(map_data.game_version.VCS, 'GTA VCS', 'GTA VCS map sections'),
)
)

map_sections : bpy.props.EnumProperty(
name = 'Map segment',
name = 'Map Section',
items = update_map_sections
)

map_section_load_prefix : bpy.props.StringProperty(
name = "Bulk Section Load Prefix",
default = '',
description = "Load all map sections that start with this case-insensitive prefix (Use * for entire map)",
options={'TEXTEDIT_UPDATE'}
)

custom_ipl_path : bpy.props.StringProperty(
name = "IPL path",
default = '',
Expand Down Expand Up @@ -251,20 +258,15 @@ def draw(self, context):
align=True)

col = flow.column()
col.prop(settings, "game_version_dropdown")
if settings.use_custom_map_section:
row = col.row(align=True)
row.prop(settings, "custom_ipl_path")
row.operator(SCENE_OT_ipl_select.bl_idname, text="", icon='FILEBROWSER')
else:
col.prop(settings, "map_sections")
col.prop(settings, "use_custom_map_section")

row = col.row()
row.operator("scene.dragonff_adjust_viewport")
col.separator()

box = col.box()
box.prop(settings, "load_txd")
if settings.load_txd:
box.prop(settings, "txd_pack")
col.prop(settings, 'game_root')
col.prop(settings, 'dff_folder')

col.separator()

col.prop(settings, "skip_lod")
col.prop(settings, "read_mat_split")
Expand All @@ -273,13 +275,41 @@ def draw(self, context):
col.prop(settings, "load_collisions")
col.prop(settings, "load_cull")

layout.separator()

layout.prop(settings, 'game_root')
layout.prop(settings, 'dff_folder')
box = col.box()
box.prop(settings, "load_txd")
if settings.load_txd:
box.prop(settings, "txd_pack")

row = layout.row()
col.separator()
row = col.row()
row.operator("scene.dragonff_map_import")
col.separator()

col = flow.column()
col.prop(settings, "game_version_dropdown")
if settings.use_custom_map_section:
row = col.row(align=True)
row.prop(settings, "custom_ipl_path")
row.operator(SCENE_OT_ipl_select.bl_idname, text="", icon='FILEBROWSER')
else:
if not settings.map_section_load_prefix:
col.prop(settings, "map_sections")
col.prop(settings, "map_section_load_prefix")
if settings.map_section_load_prefix:
items = settings.update_map_sections(bpy.context)
if settings.map_section_load_prefix == "*":
matches = [i[1] for i in items]
else:
prefix = settings.map_section_load_prefix.casefold()
matches = [i[1] for i in items if i[1].casefold().startswith(prefix)]
if matches:
col.label(text=f"Will load {len(matches)} sections:")
box = col.box()
for item in matches:
box.label(text=item)

if not settings.map_section_load_prefix:
col.prop(settings, "use_custom_map_section")

#######################################################@
class DFF_MT_AddMapObject(bpy.types.Menu):
Expand Down
77 changes: 56 additions & 21 deletions gui/map_ot.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,14 @@

#######################################################
class SCENE_OT_dff_import_map(bpy.types.Operator):
"""Tooltip"""
"""Begin map section import process

Warning: Importing multiple map sections at once can be very slow and may appear to freeze Blender.
Importing with collision enabled will be even slower as first a pass is done to load all collision files.
Loading performance suffers the more objects get loaded into the scene. This is just a constraint in Blender.
It is recommended to open the system console to better observe loading progress, and to be patient."""
bl_idname = "scene.dragonff_map_import"
bl_label = "Import map section"
bl_label = "Import map section(s)"

_timer = None
_updating = False
Expand All @@ -44,12 +49,15 @@ class SCENE_OT_dff_import_map(bpy.types.Operator):
_col_loaded = True

_cull_loaded = True
_time_start = 0

#######################################################
def modal(self, context, event):

if event.type in {'ESC'}:
self.cancel(context)
elapsed = time.time() - self._time_start
print(f"Map Load Time Elapsed: {elapsed:.2f} (Cancelled)")
return {'CANCELLED'}

if event.type == 'TIMER' and not self._updating:
Expand All @@ -68,22 +76,24 @@ def modal(self, context, event):

# Import collision files if there are any left to load
elif not self._col_loaded:
num_objects_at_once = 5

cols_num = len(importer.col_files)

for _ in range(num_objects_at_once):
if self._col_index >= cols_num:
self._col_loaded = True
break
# Fetch next collision
col_file = importer.col_files[self._col_index]

# Fetch next collision
col_file = importer.col_files[self._col_index]
self._col_index += 1
self._col_index += 1
if self._col_index >= cols_num:
self._col_loaded = True

importer.import_collision(context, col_file)
self._progress_current += 1
importer.import_collision(context, col_file)

# Update cursor progress indicator if something needs to be loaded
progress = (
float(self._col_index) / float(cols_num)
) if self._progress_total else 100

# Import objcets instances
# Import object instances
else:
# As the number of objects increases, loading performance starts to get crushed by scene updates, so
# we try to keep loading at least 5% of the total scene object count on each timer pulse.
Expand All @@ -106,21 +116,19 @@ def modal(self, context, event):

self._progress_current += 1

# Update cursor progress indicator if something needs to be loaded
progress = (
float(self._progress_current) / float(self._progress_total)
) if self._progress_total else 100
# Update cursor progress indicator if something needs to be loaded
progress = (
float(self._progress_current) / float(self._progress_total)
) if self._progress_total else 100

context.window_manager.progress_update(progress)

# Update dependency graph
dg = context.evaluated_depsgraph_get()
dg.update()

self._updating = False

if self._inst_loaded:
self.cancel(context)
elapsed = time.time() - self._time_start
print(f"Map Load Time Elapsed: {elapsed:.2f}")
return {'FINISHED'}

return {'PASS_THROUGH'}
Expand All @@ -130,6 +138,14 @@ def execute(self, context):

settings = context.scene.dff
self._importer = map_importer.load_map(settings)

# focus the viewport camera at the center of the map sections to be loaded
for area in bpy.context.screen.areas:
if area.type == 'VIEW_3D':
rv3d = area.spaces.active.region_3d
rv3d.view_location = self._importer.load_view_location
rv3d.view_distance = 1000.0
break

self._progress_current = 0
self._progress_total = 0
Expand Down Expand Up @@ -158,6 +174,8 @@ def execute(self, context):
self._timer = wm.event_timer_add(0.1, window=context.window)
wm.modal_handler_add(self)

self._time_start = time.time()

return {'RUNNING_MODAL'}

#######################################################
Expand Down Expand Up @@ -308,3 +326,20 @@ def execute(self, context):
obj.select_set(True)

return {'FINISHED'}


#######################################################
class SCENE_OT_adjust_viewport(bpy.types.Operator):
"""Adjust viewport for better map display by increasing far clipping plane and enabling texture shading"""
bl_idname = "scene.dragonff_adjust_viewport"
bl_label = "Adjust viewport"

def execute(self, context):
for area in bpy.context.screen.areas:
if area.type == 'VIEW_3D':
for space in area.spaces:
if space.type == 'VIEW_3D':
space.clip_end = 10000.0
space.shading.type = 'SOLID'
space.shading.color_type = 'TEXTURE'
return {'FINISHED'}
15 changes: 6 additions & 9 deletions ops/col_importer.py
Original file line number Diff line number Diff line change
Expand Up @@ -206,16 +206,13 @@ def __add_mesh(self, collection, name, verts, faces, face_groups, shadow=False):
self.__add_mesh_mats(obj, materials)

#######################################################
def add_to_scene(self, collection_prefix, link=True):
def add_to_scene(self, link=True):

collection_list = []

for model in self.col.models:

collection = create_collection("%s.%s" % (collection_prefix,
model.model_name),
link
)
collection = create_collection(model.model_name, link)

# Store the import bounds as a custom property of the collection
collection.dff.bounds_min = model.bounds.min
Expand Down Expand Up @@ -246,13 +243,13 @@ def add_to_scene(self, collection_prefix, link=True):
return collection_list

#######################################################
def import_col_file(filename, collection_prefix, link=True):
def import_col_file(filename, link=True):

col = col_importer.from_file(filename)
return col.add_to_scene(collection_prefix, link)
return col.add_to_scene(link)

#######################################################
def import_col_mem(mem, collection_prefix, link=True):
def import_col_mem(mem, link=True):

col = col_importer.from_mem(mem)
return col.add_to_scene(collection_prefix, link)
return col.add_to_scene(link)
Loading