├── gtaLib
├── __init__.py
├── data
│ ├── presets.py
│ └── col_materials.py
├── img.py
├── native_wdgl.py
├── pyffi
│ └── utils
│ │ └── trianglemesh.py
├── native_xbox.py
└── native_psp.py
├── .github
└── FUNDING.yml
├── .gitignore
├── blender_manifest.toml
├── gui
├── gui.py
├── cull_menus.py
├── map_menus.py
├── ext_2dfx_ot.py
└── map_ot.py
├── README.md
├── ops
├── ipl_exporter.py
├── state.py
├── cull_importer.py
├── cull_exporter.py
├── txd_importer.py
├── txd_exporter.py
├── col_importer.py
├── ext_2dfx_exporter.py
├── col_exporter.py
├── importer_common.py
├── ext_2dfx_importer.py
└── map_importer.py
└── __init__.py
/gtaLib/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | patreon: parik
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | __pycache__
2 | GPUCache
--------------------------------------------------------------------------------
/blender_manifest.toml:
--------------------------------------------------------------------------------
1 | schema_version = "1.0.0"
2 |
3 | id = "dragonff"
4 | version = "1.0.0"
5 | name = "DragonFF"
6 | tagline = "Blender Add-on to edit RenderWare Formats (.dff, .txd, .col)"
7 | maintainer = "Parik27"
8 | type = "add-on"
9 |
10 | website = "https://github.com/Parik27/DragonFF/"
11 |
12 | # # Optional: tag list defined by Blender and server, see:
13 | # # https://docs.blender.org/manual/en/dev/advanced/extensions/tags.html
14 | tags = ["Import-Export", "3D View", "Mesh", "Material"]
15 |
16 | blender_version_min = "4.2.0"
17 | # blender_version_max = "5.1.0"
18 |
19 | # License conforming to https://spdx.org/licenses/ (use "SPDX: prefix)
20 | # https://docs.blender.org/manual/en/dev/advanced/extensions/licenses.html
21 | license = [
22 | "SPDX:GPL-3.0-or-later"
23 | ]
24 |
25 | copyright = [
26 | "2019-2025 Parik",
27 | "2007-2012 Python File Format Interface"
28 | ]
29 |
30 | [permissions]
31 | files = "Import/export DFF/TXD/COL/IPL/IDE from/to disk"
--------------------------------------------------------------------------------
/gui/gui.py:
--------------------------------------------------------------------------------
1 | # GTA DragonFF - Blender scripts to edit basic GTA formats
2 | # Copyright (C) 2019 Parik
3 |
4 | # This program is free software: you can redistribute it and/or modify
5 | # it under the terms of the GNU General Public License as published by
6 | # the Free Software Foundation, either version 3 of the License, or
7 | # (at your option) any later version.
8 |
9 | # This program is distributed in the hope that it will be useful,
10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | # GNU General Public License for more details.
13 |
14 | # You should have received a copy of the GNU General Public License
15 | # along with this program. If not, see .
16 |
17 | from .col_ot import *
18 | from .dff_menus import *
19 | from .ext_2dfx_ot import *
20 | from .ext_2dfx_menus import *
21 | from .dff_ot import *
22 | from .map_ot import *
23 | from .map_menus import *
24 | from .gizmos import *
25 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # DragonFF 🐉
2 |
3 | [](https://discord.gg/QxpkwNqeTr)
4 |
5 | DragonFF is a Blender Addon for import and export of GTA files.
6 |
7 | At the moment, only Renderware files are supported. Support for formats other than .dff is planned.
8 |
9 | ## Supported Features
10 |
11 | The following is a list of supported features by the addon
12 |
13 | #### File Types
14 |
15 | - [X] Model files
16 | - [X] Texture Files
17 | - [X] Import
18 | - [X] Export *(partial, experimental)*
19 | - [X] Collision files (including the ones packed in dff)
20 | - [X] Import
21 | - [X] Export
22 | - [ ] Map files (.ipl, .ide)
23 | - [X] Import *(partial, experimental)*
24 | - [ ] Export
25 | - [ ] Animation files
26 |
27 | #### Model Features
28 |
29 | - [X] Skinned mesh support
30 | - [X] Multiple UV Maps
31 | - [X] Mass export
32 | - [X] Material Effects
33 | - [X] Environment/Normal Maps
34 | - [ ] Dual Textures
35 | - [X] UV Animation
36 | - [X] Rockstar Specular and Reflection Extensions
37 | - [X] 2D Effects
38 |
39 | ## Installation
40 |
41 | There are two ways to install DragonFF.
42 |
43 | ### From Blender Extentensions
44 |
45 | In Blender 4.2 and above, you can install DragonFF directly from within Blender.
46 |
47 | 
48 |
49 | 1. Go to Blender Preferences.
50 | 2. In the Get Extensions Tab, search for DragonFF
51 | 3. Click Install
52 |
53 | ## Manually
54 |
55 | In older versions of Blender or to get the newest changes (that may be unstable), use the following instructions to install Blender from GitHub.
56 |
57 | 1. [Download](https://github.com/Parik27/DragonFF/archive/refs/heads/master.zip) the addon zip file from the latest master branch
58 | 2. Import the downloaded .zip file by selecting it from *(User) Preferences/Addons/Install from File*
59 | 3. Set the addon "GTA DragonFF" to enabled
60 | 4. Import dff from Import tab or an IPL/IFP from the panel in *Scene Settings*
61 |
62 | ## Python Module
63 |
64 | The python scripts have been designed with reusability in mind. As of now, the dff module is standalone, and can be used with any other Python instance without the need for Blender API.
65 |
66 | #### Standalone Modules
67 |
68 | * [X] - DFF - `dff.py`
69 | * [X] - TXD - `txd.py`
70 | * [X] - COL - `col.py`
71 | * [X] - IPL/IDE - `map.py` *(partial, experimental)*
72 | * [ ] - IFP - `ifp.py`
73 |
74 | #### Contributors
75 |
76 | The following have contributed significantly to the project:
77 |
78 | * [swift502](https://github.com/swift502) - For the map importer.
79 | * [Psycrow101](https://github.com/Psycrow101) - For delta morphs importer.
80 |
81 |
--------------------------------------------------------------------------------
/gtaLib/data/presets.py:
--------------------------------------------------------------------------------
1 | material_colours = {
2 | (255, 60, 0, 255) : ("Right Tail Light", ""),
3 | (185, 255, 0, 255) : ("Left Tail Light", ""),
4 | (0, 255, 200, 255) : ("Right Headlight", ""),
5 | (255, 175, 0, 255) : ("Left Headlight", ""),
6 | (0, 255, 255, 255) : ("4 Colors Paintjob", ""),
7 | (255, 0, 255, 255) : ("Fourth Color", ""),
8 | (0, 255, 255, 255) : ("Third Color", ""),
9 | (255, 0, 175, 255) : ("Secondary Color", ""),
10 | (60, 255, 0, 255) : ("Primary Color", ""),
11 | (184, 255, 0, 255) : ("ImVehFT - Breaklight L", ""),
12 | (255, 59, 0, 255) : ("ImVehFT - Breaklight R", ""),
13 | (255, 173, 0, 255) : ("ImVehFT - Revlight L", ""),
14 | (0, 255, 198, 255) : ("ImVehFT - Revlight R", ""),
15 | (255, 174, 0, 255) : ("ImVehFT - Foglight L", ""),
16 | (0, 255, 199, 255) : ("ImVehFT - Foglight R", ""),
17 | (183, 255, 0, 255) : ("ImVehFT - Indicator LF", ""),
18 | (255, 58, 0, 255) : ("ImVehFT - Indicator RF", ""),
19 | (182, 255, 0, 255) : ("ImVehFT - Indicator LM", ""),
20 | (255, 57, 0, 255) : ("ImVehFT - Indicator RM", ""),
21 | (181, 255, 0, 255) : ("ImVehFT - Indicator LR", ""),
22 | (255, 56, 0, 255) : ("ImVehFT - Indicator RR", ""),
23 | (0, 16, 255, 255) : ("ImVehFT - Light Night", ""),
24 | (0, 17, 255, 255) : ("ImVehFT - Light All-day", ""),
25 | (0, 18, 255, 255) : ("ImVehFT - Default Day", ""),
26 | }
27 |
28 | material_specular_levels = {
29 | 0.00 : ("Off", "Disabled (0.0)"),
30 | 0.99 : ("60", "Specular (0.99)"),
31 | 0.87 : ("61", "Specular (0.87)"),
32 | 0.79 : ("62", "Specular (0.79)"),
33 | 0.69 : ("63", "Specular (0.69)"),
34 | 0.63 : ("64", "Specular (0.63)"),
35 | 0.56 : ("65", "Specular (0.56)"),
36 | 0.50 : ("66", "Specular (0.50)"),
37 | 0.44 : ("67", "Specular (0.44)"),
38 | 0.39 : ("68", "Specular (0.39)"),
39 | 0.34 : ("69", "Specular (0.34)"),
40 | 0.30 : ("70", "Specular (0.30)"),
41 | 0.26 : ("71", "Specular (0.26)"),
42 | 0.20 : ("72", "Specular (0.20)"),
43 | 0.19 : ("73", "Specular (0.19)"),
44 | 0.16 : ("74", "Specular (0.16)"),
45 | 0.14 : ("75", "Specular (0.14)"),
46 | 0.12 : ("76", "Specular (0.12)"),
47 | 0.09 : ("77", "Specular (0.09)"),
48 | 0.08 : ("78", "Specular (0.08)"),
49 | 0.06 : ("79", "Specular (0.06)"),
50 | 0.04 : ("80", "Specular (0.04)"),
51 | 0.03 : ("81", "Specular (0.03)"),
52 | 0.01 : ("82", "Specular (0.01)"),
53 | }
54 |
55 | material_reflection_intensities = {
56 | 0.0 : ("Off", "Reflection is disabled (0.0)"),
57 | **{
58 | round(i / 100, 2) : ("ENV %d" % i, "Reflection level %d" % i)
59 | for i in range(1, 51)
60 | },
61 | 1.0 : ("Mirror", "Mirror effect (1.0)"),
62 | 3.0 : ("Chrome", "Chrome surface (3.0)"),
63 | }
64 |
65 | material_reflection_scales = {
66 | 0.0 : ("OFF SPEC", "Glare is disabled"),
67 | 0.2 : ("SPEC 51", "51 Glare"),
68 | 0.4 : ("SPEC 102", "102 Glare"),
69 | 0.6 : ("SPEC 153", "153 Glare"),
70 | 0.8 : ("SPEC 204", "204 Glare"),
71 | 1.0 : ("MAX SPEC ", "255 Glare"),
72 | }
73 |
--------------------------------------------------------------------------------
/ops/ipl_exporter.py:
--------------------------------------------------------------------------------
1 | # GTA DragonFF - Blender scripts to edit basic GTA formats
2 | # Copyright (C) 2019 Parik
3 |
4 | # This program is free software: you can redistribute it and/or modify
5 | # it under the terms of the GNU General Public License as published by
6 | # the Free Software Foundation, either version 3 of the License, or
7 | # (at your option) any later version.
8 |
9 | # This program is distributed in the hope that it will be useful,
10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | # GNU General Public License for more details.
13 |
14 | # You should have received a copy of the GNU General Public License
15 | # along with this program. If not, see .
16 |
17 | import bpy
18 |
19 | from ..gtaLib.map import TextIPLData, MapDataUtility
20 | from ..ops.cull_exporter import cull_exporter
21 |
22 | #######################################################
23 | class ipl_exporter:
24 |
25 | only_selected = False
26 | game_id = None
27 | export_inst = False
28 | export_cull = False
29 |
30 | inst_objects = []
31 | cull_objects = []
32 | total_objects_num = 0
33 |
34 | #######################################################
35 | @staticmethod
36 | def collect_objects(context):
37 | """Collect objects that have IPL data"""
38 |
39 | self = ipl_exporter
40 |
41 | self.inst_objects = []
42 | self.cull_objects = []
43 |
44 | for obj in context.scene.objects:
45 | if self.only_selected and not obj.select_get():
46 | continue
47 |
48 | if self.export_inst and obj.dff.type == 'OBJ':
49 | self.inst_objects.append(obj)
50 |
51 | if self.export_cull and obj.dff.type == 'CULL':
52 | self.cull_objects.append(obj)
53 |
54 | self.total_objects_num = len(self.inst_objects) + len(self.cull_objects)
55 |
56 | #######################################################
57 | @staticmethod
58 | def format_inst_line(obj):
59 | """Format an object as an inst line based on game version"""
60 |
61 | self = ipl_exporter
62 |
63 | # TODO
64 | return ""
65 |
66 | #######################################################
67 | @staticmethod
68 | def export_ipl(filename):
69 | self = ipl_exporter
70 |
71 | self.collect_objects(bpy.context)
72 |
73 | if not self.total_objects_num:
74 | return
75 |
76 | object_instances = [self.format_inst_line(obj) for obj in self.inst_objects]
77 | cull_instacnes = cull_exporter.export_objects(self.cull_objects, self.game_id)
78 |
79 | ipl_Data = TextIPLData(
80 | object_instances,
81 | cull_instacnes,
82 | )
83 |
84 | MapDataUtility.write_ipl_data(filename, self.game_id, ipl_Data)
85 |
86 | #######################################################
87 | def export_ipl(options):
88 | """Main export function"""
89 |
90 | ipl_exporter.only_selected = options['only_selected']
91 | ipl_exporter.game_id = options['game_id']
92 | ipl_exporter.export_inst = options['export_inst']
93 | ipl_exporter.export_cull = options['export_cull']
94 |
95 | ipl_exporter.export_ipl(options['file_name'])
96 |
--------------------------------------------------------------------------------
/ops/state.py:
--------------------------------------------------------------------------------
1 | import bpy
2 | import time
3 | from bpy.app.handlers import depsgraph_update_post, load_post, persistent
4 |
5 | #######################################################
6 | class _StateMeta(type):
7 | def __init__(cls, *args, **kwargs):
8 | cls.last_export_refresh = 0
9 |
10 | #######################################################
11 | class State(metaclass=_StateMeta):
12 |
13 | @classmethod
14 | def update_scene(cls, scene=None):
15 |
16 | def append_frame_object(ob):
17 | if ob.parent and ob.parent not in frame_objects_set:
18 | append_frame_object(ob.parent)
19 | frame_objects_set.add(ob)
20 | ordered_frame_objects.append(ob)
21 |
22 | def update_frame_status(ob):
23 | is_frame, is_frame_locked = ob.dff.is_frame, False
24 | if ob.parent and not any(ch.dff.type == 'OBJ' for ch in ob.children):
25 | if ob.parent.type == 'ARMATURE' and not ob.parent_bone:
26 | is_frame, is_frame_locked = False, True
27 | else:
28 | is_frame, is_frame_locked = True, True
29 |
30 | if ob.dff.is_frame != is_frame:
31 | ob.dff.is_frame = is_frame
32 | if ob.dff.is_frame_locked != is_frame_locked:
33 | ob.dff.is_frame_locked = is_frame_locked
34 | return is_frame
35 |
36 | scene = scene or bpy.context.scene
37 | frame_objects, atomic_objects = [], []
38 |
39 | for ob in scene.objects:
40 | if ob.dff.type != 'OBJ':
41 | continue
42 |
43 | if ob.type == 'MESH':
44 | atomic_objects.append(ob)
45 | if update_frame_status(ob):
46 | frame_objects.append(ob)
47 |
48 | elif ob.type in ('EMPTY', 'ARMATURE'):
49 | frame_objects.append(ob)
50 |
51 | frame_objects.sort(key=lambda ob: ob.dff.frame_index)
52 | atomic_objects.sort(key=lambda ob: ob.dff.atomic_index)
53 |
54 | ordered_frame_objects, frame_objects_set = [], set()
55 | for ob in frame_objects:
56 | if ob not in frame_objects_set:
57 | append_frame_object(ob)
58 | frame_objects = ordered_frame_objects
59 |
60 | scene.dff.frames.clear()
61 | for i, ob in enumerate(frame_objects):
62 | frame_prop = scene.dff.frames.add()
63 | frame_prop.obj = ob
64 | frame_prop.icon = 'ARMATURE_DATA' if ob.type == 'ARMATURE' else 'EMPTY_DATA'
65 | ob.dff.frame_index = i
66 |
67 | scene.dff.atomics.clear()
68 | for i, ob in enumerate(atomic_objects):
69 | atomic_prop = scene.dff.atomics.add()
70 | atomic_prop.obj = ob
71 |
72 | frame_obj = None
73 | for modifier in ob.modifiers:
74 | if modifier.type == 'ARMATURE':
75 | frame_obj = modifier.object
76 | break
77 | if frame_obj is None:
78 | frame_obj = ob.parent
79 | atomic_prop.frame_obj = frame_obj
80 |
81 | ob.dff.atomic_index = i
82 |
83 | cls.last_export_refresh = time.time()
84 |
85 | @staticmethod
86 | @persistent
87 | def _onDepsgraphUpdate(scene):
88 | if scene.dff.real_time_update:
89 | if scene == bpy.context.scene and time.time() - State.last_export_refresh > 0.3:
90 | State.update_scene(scene)
91 |
92 | @staticmethod
93 | @persistent
94 | def _onLoad(_):
95 | State.update_scene()
96 |
97 | @classmethod
98 | def hook_events(cls):
99 | if not cls._onDepsgraphUpdate in depsgraph_update_post:
100 | depsgraph_update_post.append(cls._onDepsgraphUpdate)
101 | load_post.append(cls._onLoad)
102 |
103 | @classmethod
104 | def unhook_events(cls):
105 | if cls._onDepsgraphUpdate in depsgraph_update_post:
106 | depsgraph_update_post.remove(cls._onDepsgraphUpdate)
107 | load_post.remove(cls._onLoad)
108 |
--------------------------------------------------------------------------------
/gtaLib/img.py:
--------------------------------------------------------------------------------
1 | # GTA DragonFF - Blender scripts to edit basic GTA formats
2 | # Copyright (C) 2019 Parik
3 |
4 | # This program is free software: you can redistribute it and/or modify
5 | # it under the terms of the GNU General Public License as published by
6 | # the Free Software Foundation, either version 3 of the License, or
7 | # (at your option) any later version.
8 |
9 | # This program is distributed in the hope that it will be useful,
10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | # GNU General Public License for more details.
13 |
14 | # You should have received a copy of the GNU General Public License
15 | # along with this program. If not, see .
16 |
17 | import os
18 |
19 | from dataclasses import dataclass
20 | from struct import unpack, unpack_from
21 |
22 | #######################################################
23 | @dataclass
24 | class DirectoryEntry:
25 | offset: int
26 | size: int
27 | name: str
28 |
29 | #######################################################
30 | @classmethod
31 | def read_from_memory(cls, data, offset=0):
32 | offset, size, name = unpack_from("II24s", data, offset)
33 | name = name.split(b'\0', 1)[0].decode('utf-8')
34 | return cls(offset, size, name)
35 |
36 | #######################################################
37 | class img:
38 |
39 | #######################################################
40 | def load_dir_memory(self, data):
41 | data_len = len(data)
42 | for pos in range(0, data_len, 32):
43 | entry = DirectoryEntry.read_from_memory(data, pos)
44 | self.directory_entries.append(entry)
45 |
46 | #######################################################
47 | def clear(self):
48 | self.directory_entries:list[DirectoryEntry] = []
49 | self.entry_idx = 0
50 |
51 | #######################################################
52 | @classmethod
53 | def open(cls, filename):
54 | file = open(filename, mode='rb')
55 | self = cls(file)
56 |
57 | magic, entries_num = unpack("4sI", file.read(8))
58 |
59 | if magic == b"VER2":
60 | dir_data = file.read(entries_num * 32)
61 | self.load_dir_memory(dir_data)
62 |
63 | else:
64 | dir_filename = os.path.splitext(filename)[0] + '.dir'
65 | with open(dir_filename, mode='rb') as dir_file:
66 | dir_data = dir_file.read()
67 | self.load_dir_memory(dir_data)
68 |
69 | file.seek(0, os.SEEK_SET)
70 |
71 | return self
72 |
73 | #######################################################
74 | def close(self):
75 | if self._file and not self._file.closed:
76 | self._file.close()
77 |
78 | #######################################################
79 | def read_entry(self, entry_idx=None):
80 | if entry_idx is None:
81 | entry_idx = self.entry_idx
82 |
83 | if 0 <= entry_idx < len(self.directory_entries):
84 | entry = self.directory_entries[entry_idx]
85 | self._file.seek(entry.offset * 2048, os.SEEK_SET)
86 | return entry.name, self._file.read(entry.size * 2048)
87 |
88 | return "", b""
89 |
90 | #######################################################
91 | def find_entry_idx(self, name):
92 | return next(
93 | (idx for idx, entry in enumerate(self.directory_entries) if entry.name == name),
94 | -1
95 | )
96 |
97 | #######################################################
98 | def __init__(self, file):
99 | self._file = file
100 | self.clear()
101 |
102 | #######################################################
103 | def __enter__(self):
104 | return self
105 |
106 | #######################################################
107 | def __exit__(self, exc_type, exc_val, exc_tb):
108 | self.close()
109 |
110 | #######################################################
111 | def __del__(self):
112 | self.close()
113 |
--------------------------------------------------------------------------------
/ops/cull_importer.py:
--------------------------------------------------------------------------------
1 | # GTA DragonFF - Blender scripts to edit basic GTA formats
2 | # Copyright (C) 2019 Parik
3 |
4 | # This program is free software: you can redistribute it and/or modify
5 | # it under the terms of the GNU General Public License as published by
6 | # the Free Software Foundation, either version 3 of the License, or
7 | # (at your option) any later version.
8 |
9 | # This program is distributed in the hope that it will be useful,
10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | # GNU General Public License for more details.
13 |
14 | # You should have received a copy of the GNU General Public License
15 | # along with this program. If not, see .
16 |
17 | import bpy
18 |
19 | from math import atan2
20 | from mathutils import Vector
21 |
22 | #######################################################
23 | class cull_importer:
24 |
25 | """ Helper class for CULL importing """
26 |
27 | #######################################################
28 | @staticmethod
29 | def create_cull_object(location, scale, flags, angle=0):
30 | obj = bpy.data.objects.new("cull", None)
31 | obj.empty_display_type = 'CUBE'
32 | obj.location = location
33 | obj.rotation_euler = (0, 0, angle)
34 | obj.scale = scale
35 | obj.dff.type = 'CULL'
36 |
37 | obj.lock_rotation[0] = True
38 | obj.lock_rotation[1] = True
39 | obj.lock_rotation_w = True
40 |
41 | settings = obj.dff.cull
42 | settings.flags = {str(1 << i) for i in range(16) if flags & (1 << i)}
43 |
44 | return obj
45 |
46 | #######################################################
47 | @staticmethod
48 | def import_cull(cull):
49 | self = cull_importer
50 |
51 | location = Vector((float(cull.centerX), float(cull.centerY), float(cull.centerZ)))
52 | angle = 0
53 | scale = Vector()
54 | flags = int(cull.flags)
55 |
56 | wanted_level_drop = 0
57 | mirror_enabled = False
58 | mirror_axis = 'AXIS_X'
59 | mirror_coordinate = 0.0
60 |
61 | if hasattr(cull, 'widthX'):
62 | scale.x = float(cull.widthX)
63 | scale.y = float(cull.widthY)
64 |
65 | top_z = float(cull.topZ)
66 | bottom_z = float(cull.bottomZ)
67 |
68 | location.z = (top_z + bottom_z) * 0.5
69 | scale.z = (top_z - bottom_z) * 0.5
70 |
71 | angle = -atan2(2 * float(cull.skewX), 2 * scale.y)
72 |
73 | if hasattr(cull, 'Vx'):
74 | mirror_enabled = True
75 | vx, vy, vz = float(cull.Vx), float(cull.Vy), float(cull.Vz)
76 | if vx > 0.5:
77 | mirror_axis = 'AXIS_X'
78 | elif vx < -0.5:
79 | mirror_axis = 'AXIS_NEGATIVE_X'
80 | elif vy > 0.5:
81 | mirror_axis = 'AXIS_Y'
82 | elif vy < -0.5:
83 | mirror_axis = 'AXIS_NEGATIVE_Y'
84 | elif vz > 0.5:
85 | mirror_axis = 'AXIS_Z'
86 | elif vz < -0.5:
87 | mirror_axis = 'AXIS_NEGATIVE_Z'
88 | mirror_coordinate = float(cull.cm)
89 |
90 | elif hasattr(cull, "lowerLeftX"):
91 | lower_left = Vector((float(cull.lowerLeftX), float(cull.lowerLeftY), float(cull.lowerLeftZ)))
92 | upper_right = Vector((float(cull.upperRightX), float(cull.upperRightY), float(cull.upperRightZ)))
93 |
94 | for axis in range(3):
95 | location[axis] = (upper_right[axis] + lower_left[axis]) * 0.5
96 | scale[axis] = (upper_right[axis] - lower_left[axis]) * 0.5
97 |
98 | wanted_level_drop = int(cull.wantedLevelDrop)
99 |
100 | obj = self.create_cull_object(location, scale, flags, angle)
101 | settings = obj.dff.cull
102 | settings.wanted_level_drop = wanted_level_drop
103 | settings.mirror_enabled = mirror_enabled
104 | settings.mirror_axis = mirror_axis
105 | settings.mirror_coordinate = mirror_coordinate
106 |
107 | return obj
108 |
--------------------------------------------------------------------------------
/ops/cull_exporter.py:
--------------------------------------------------------------------------------
1 | # GTA DragonFF - Blender scripts to edit basic GTA formats
2 | # Copyright (C) 2019 Parik
3 |
4 | # This program is free software: you can redistribute it and/or modify
5 | # it under the terms of the GNU General Public License as published by
6 | # the Free Software Foundation, either version 3 of the License, or
7 | # (at your option) any later version.
8 |
9 | # This program is distributed in the hope that it will be useful,
10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | # GNU General Public License for more details.
13 |
14 | # You should have received a copy of the GNU General Public License
15 | # along with this program. If not, see .
16 |
17 | from math import tan
18 |
19 | from ..gtaLib.data.map_data import game_version
20 |
21 | #######################################################
22 | class cull_exporter:
23 |
24 | """ Helper class for CULL exporting """
25 |
26 | #######################################################
27 | @staticmethod
28 | def export_cull(obj, game_id):
29 |
30 | settings = obj.dff.cull
31 | cull_line = ""
32 |
33 | flags = 0
34 | for fl in settings.flags:
35 | flags |= int(fl)
36 |
37 | if game_id in (game_version.III, game_version.VC):
38 | center_x, center_y, center_z = obj.location.xyz
39 |
40 | lower_left_x = center_x - obj.scale.x
41 | lower_left_y = center_y - obj.scale.y
42 | lower_left_z = center_z - obj.scale.z
43 |
44 | upper_right_x = center_x + obj.scale.x
45 | upper_right_y = center_y + obj.scale.y
46 | upper_right_z = center_z + obj.scale.z
47 |
48 | wanted_level_drop = settings.wanted_level_drop
49 |
50 | cull_line += f"{center_x:.6f}, {center_y:.6f}, {center_z:.6f}"
51 | cull_line += f", {lower_left_x:.6f}, {lower_left_y:.6f}, {lower_left_z:.6f}"
52 | cull_line += f", {upper_right_x:.6f}, {upper_right_y:.6f}, {upper_right_z:.6f}"
53 | cull_line += f", {flags}, {wanted_level_drop}"
54 |
55 | else:
56 | center_x = obj.location.x
57 | center_y = obj.location.y
58 | center_z = obj.location.z - obj.scale.z
59 |
60 | width_x = obj.scale.x
61 | width_y = obj.scale.y
62 |
63 | top_z = obj.location.z + obj.scale.z
64 | bottom_z = center_z
65 |
66 | angle = obj.matrix_world.to_euler().z
67 |
68 | skew_x = -tan(angle) * width_y
69 | skew_y = tan(angle) * width_x
70 |
71 | cull_line += f"{center_x:.6f}, {center_y:.6f}, {center_z:.6f}"
72 | cull_line += f", {skew_x:.6f}, {width_y:.6f}, {bottom_z:.6f}, {width_x:.6f}, {skew_y:.6f}, {top_z:.6f}"
73 | cull_line += f", {flags}"
74 |
75 | if settings.mirror_enabled:
76 | if settings.mirror_axis == 'AXIS_X':
77 | vx, vy, vz = 1, 0, 0
78 | elif settings.mirror_axis == 'AXIS_Y':
79 | vx, vy, vz = 0, 1, 0
80 | elif settings.mirror_axis == 'AXIS_Z':
81 | vx, vy, vz = 0, 0, 1
82 | elif settings.mirror_axis == 'AXIS_NEGATIVE_X':
83 | vx, vy, vz = -1, 0, 0
84 | elif settings.mirror_axis == 'AXIS_NEGATIVE_Y':
85 | vx, vy, vz = 0, -1, 0
86 | elif settings.mirror_axis == 'AXIS_NEGATIVE_Z':
87 | vx, vy, vz = 0, 0, -1
88 | cm = settings.mirror_coordinate
89 |
90 | cull_line += f", {vx}, {vy}, {vz}, {cm:.6f}"
91 |
92 | else:
93 | cull_line += f", 0"
94 |
95 | return cull_line
96 |
97 | #######################################################
98 | @staticmethod
99 | def export_objects(objects, game_id):
100 |
101 | """ Export CULL objects to list of strings"""
102 |
103 | cull_objects = [obj for obj in objects if obj.dff.type == 'CULL']
104 | # cull_objects.sort(key=lambda obj: obj.name)
105 |
106 | culls = []
107 | for obj in cull_objects:
108 | cull = cull_exporter.export_cull(obj, game_id)
109 | culls.append(cull)
110 |
111 | return culls
112 |
--------------------------------------------------------------------------------
/ops/txd_importer.py:
--------------------------------------------------------------------------------
1 | # GTA DragonFF - Blender scripts to edit basic GTA formats
2 | # Copyright (C) 2019 Parik
3 |
4 | # This program is free software: you can redistribute it and/or modify
5 | # it under the terms of the GNU General Public License as published by
6 | # the Free Software Foundation, either version 3 of the License, or
7 | # (at your option) any later version.
8 |
9 | # This program is distributed in the hope that it will be useful,
10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | # GNU General Public License for more details.
13 |
14 | # You should have received a copy of the GNU General Public License
15 | # along with this program. If not, see .
16 |
17 | import bpy
18 | import os
19 |
20 | from ..gtaLib import txd
21 |
22 | #######################################################
23 | class txd_importer:
24 |
25 | skip_mipmaps = True
26 | pack = True
27 |
28 | __slots__ = [
29 | 'txd',
30 | 'images',
31 | 'file_name'
32 | ]
33 |
34 | #######################################################
35 | def _init():
36 | self = txd_importer
37 |
38 | # Variables
39 | self.txd = None
40 | self.images = {}
41 | self.file_name = ""
42 |
43 | #######################################################
44 | def _create_image(name, rgba, width, height, pack=False):
45 | pixels = []
46 | for h in range(height - 1, -1, -1):
47 | offset = h * width * 4
48 | pixels += list(map(lambda b: b / 0xff, rgba[offset:offset+width*4]))
49 |
50 | image = bpy.data.images.new(name, width, height, alpha=True)
51 | image.pixels = pixels
52 |
53 | if pack:
54 | image.pack()
55 |
56 | return image
57 |
58 | #######################################################
59 | def import_textures():
60 | self = txd_importer
61 |
62 | txd_name = os.path.basename(self.file_name).lower()
63 |
64 | # Import native textures
65 | for tex in self.txd.native_textures:
66 | images = []
67 | num_levels = tex.num_levels if not self.skip_mipmaps else 1
68 |
69 | for level in range(num_levels):
70 | image_name = "%s/%s/%d" % (txd_name, tex.name, level)
71 | image = bpy.data.images.get(image_name)
72 | if not image:
73 | image = txd_importer._create_image(image_name,
74 | tex.to_rgba(level),
75 | tex.get_width(level),
76 | tex.get_height(level),
77 | self.pack)
78 | images.append(image)
79 |
80 | self.images[tex.name] = images
81 |
82 | # Import textures
83 | for tex, imgs in zip(self.txd.textures, self.txd.images):
84 | images = []
85 | num_levels = len(imgs) if not self.skip_mipmaps else 1
86 |
87 | for level in range(num_levels):
88 | img = imgs[level]
89 | image_name = "%s/%s/%d" % (txd_name, tex.name, level)
90 | image = txd_importer._create_image(image_name,
91 | img.to_rgba(),
92 | img.width,
93 | img.height,
94 | self.pack)
95 | images.append(image)
96 |
97 | self.images[tex.name] = images
98 |
99 | #######################################################
100 | def import_txd(file_name):
101 | self = txd_importer
102 | self._init()
103 |
104 | self.txd = txd.txd()
105 | self.txd.load_file(file_name)
106 | self.file_name = file_name
107 |
108 | self.import_textures()
109 |
110 | #######################################################
111 | def import_txd(options):
112 |
113 | txd_importer.skip_mipmaps = options['skip_mipmaps']
114 | txd_importer.pack = options['pack']
115 |
116 | txd_importer.import_txd(options['file_name'])
117 |
118 | return txd_importer
119 |
--------------------------------------------------------------------------------
/gui/cull_menus.py:
--------------------------------------------------------------------------------
1 | # GTA DragonFF - Blender scripts to edit basic GTA formats
2 | # Copyright (C) 2019 Parik
3 |
4 | # This program is free software: you can redistribute it and/or modify
5 | # it under the terms of the GNU General Public License as published by
6 | # the Free Software Foundation, either version 3 of the License, or
7 | # (at your option) any later version.
8 |
9 | # This program is distributed in the hope that it will be useful,
10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | # GNU General Public License for more details.
13 |
14 | # You should have received a copy of the GNU General Public License
15 | # along with this program. If not, see .
16 |
17 | import bpy
18 |
19 | from ..gtaLib.data.map_data import game_version
20 |
21 | #######################################################
22 | class CULLObjectProps(bpy.types.PropertyGroup):
23 |
24 | flags : bpy.props.EnumProperty(
25 | name = "Flags",
26 | items = (
27 | ('1', 'Cam Close In For Player',
28 | 'Camera close in into player using closest third-person view camera mode,'
29 | 'does not close in if in first person or cinematic mode, camera mode cannot be changed while in the zone',
30 | 0, (1<<0)),
31 |
32 | ('2', 'Cam Stairs For Player',
33 | 'Camera remotely placed outside the zone, no control of camera, camera mode cannot be changed while in the zone',
34 | 0, (1<<1)),
35 |
36 | ('4', 'Cam 1st Person For Player',
37 | 'Lowers the camera angle on boats',
38 | 0, (1<<2)),
39 |
40 | ('8', 'No Rain',
41 | 'Rain-free, police helicopter-free zone',
42 | 0, (1<<3)),
43 |
44 | ('16', 'No Police',
45 | 'They will only exit if you do something to them (like shoot it).'
46 | 'Cops both on foot and in vehicles will not chase you but can shoot at you',
47 | 0, (1<<4)),
48 |
49 | ('32', 'Flag 32',
50 | '',
51 | 0, (1<<5)),
52 |
53 | ('64', 'Do Need To Load Collision',
54 | '',
55 | 0, (1<<6)),
56 |
57 | ('128', 'Flag 128',
58 | '',
59 | 0, (1<<7)),
60 |
61 | ('256', 'Police Abandon Cars',
62 | 'Police will always exit their vehicles once they are spawned ONLY IF you have a wanted level'
63 | "If you don't, they'll drive normally",
64 | 0, (1<<8)),
65 |
66 | ('512', 'In Room For Audio',
67 | '',
68 | 0, (1<<9)),
69 |
70 | ('1024', 'Water Fudge',
71 | 'Some visual ocean water effects are removed like the transparent waves and sparkles on the water',
72 | 0, (1<<10)),
73 |
74 | ('2048', 'Flag 2048',
75 | '',
76 | 0, (1<<11)),
77 |
78 | ('4096', 'Military Zone',
79 | '5-Star Military zone',
80 | 0, (1<<12)),
81 |
82 | ('8192', 'Flag 8192',
83 | '',
84 | 0, (1<<13)),
85 |
86 | ('16384', 'Extra Air Resistance',
87 | "Doesn't allow cars to reach top speed",
88 | 0, (1<<14)),
89 |
90 | ('32768', 'Fewer Cars',
91 | "Spawn fewer cars in this area",
92 | 0, (1<<15)),
93 | ),
94 | options = {'ENUM_FLAG'}
95 | )
96 |
97 | wanted_level_drop : bpy.props.IntProperty(
98 | name = "Wanted Level Drop"
99 | )
100 |
101 | mirror_enabled : bpy.props.BoolProperty(
102 | name = "Enable Mirror"
103 | )
104 |
105 | mirror_axis : bpy.props.EnumProperty(
106 | name = "Mirror Axis",
107 | description = "Mirror direction",
108 | items = (
109 | ('AXIS_X', 'X', ''),
110 | ('AXIS_Y', 'Y', ''),
111 | ('AXIS_Z', 'Z', ''),
112 | ('AXIS_NEGATIVE_X', '-X', ''),
113 | ('AXIS_NEGATIVE_Y', '-Y', ''),
114 | ('AXIS_NEGATIVE_Z', '-Z', ''),
115 | ),
116 | default = 'AXIS_Z'
117 | )
118 |
119 | mirror_coordinate : bpy.props.FloatProperty(
120 | name = "Mirror Coordinate",
121 | description = "Mirror plane coordinate in direction axis"
122 | )
123 |
124 | #######################################################
125 | class CULLMenus:
126 |
127 | #######################################################
128 | def draw_menu(layout, context):
129 | obj = context.object
130 |
131 | settings = obj.dff.cull
132 | game_id = context.scene.dff.game_version_dropdown
133 |
134 | layout.prop(context.scene.dff, "game_version_dropdown", text="Game")
135 |
136 | box = layout.box()
137 |
138 | if game_id in (game_version.III, game_version.VC):
139 | box.prop(settings, "wanted_level_drop")
140 | else:
141 | box.prop(settings, "mirror_enabled")
142 | if settings.mirror_enabled:
143 | box.prop(settings, "mirror_axis")
144 | box.prop(settings, "mirror_coordinate")
145 |
146 | box = layout.box()
147 | box.label(text="Flags")
148 | box.prop(settings, "flags")
149 |
--------------------------------------------------------------------------------
/__init__.py:
--------------------------------------------------------------------------------
1 | # GTA DragonFF - Blender scripts to edit basic GTA formats
2 | # Copyright (C) 2019 Parik
3 |
4 | # This program is free software: you can redistribute it and/or modify
5 | # it under the terms of the GNU General Public License as published by
6 | # the Free Software Foundation, either version 3 of the License, or
7 | # (at your option) any later version.
8 |
9 | # This program is distributed in the hope that it will be useful,
10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | # GNU General Public License for more details.
13 |
14 | # You should have received a copy of the GNU General Public License
15 | # along with this program. If not, see .
16 |
17 | import bpy
18 | from .gui import gui
19 |
20 | from bpy.utils import register_class, unregister_class
21 |
22 | bl_info = {
23 | "name": "GTA DragonFF",
24 | "author": "Parik",
25 | "version": (0, 0, 2),
26 | "blender": (2, 80, 0),
27 | "category": "Import-Export",
28 | "location": "File > Import/Export",
29 | "description": "Importer and Exporter for GTA Formats"
30 | }
31 |
32 |
33 | # Class list to register
34 | _classes = [
35 | gui.IMPORT_OT_dff,
36 | gui.EXPORT_OT_dff,
37 | gui.EXPORT_OT_txd,
38 | gui.EXPORT_OT_col,
39 | gui.EXPORT_OT_ipl_cull,
40 | gui.SCENE_OT_dff_frame_move,
41 | gui.SCENE_OT_dff_atomic_move,
42 | gui.SCENE_OT_dff_update,
43 | gui.SCENE_OT_dff_import_map,
44 | gui.SCENE_OT_ipl_select,
45 | gui.OBJECT_OT_dff_generate_bone_props,
46 | gui.OBJECT_OT_dff_set_parent_bone,
47 | gui.OBJECT_OT_dff_clear_parent_bone,
48 | gui.OBJECT_OT_facegoups_col,
49 | gui.OBJECT_OT_dff_add_collision_box,
50 | gui.OBJECT_OT_dff_add_collision_sphere,
51 | gui.OBJECT_OT_dff_add_2dfx_light,
52 | gui.OBJECT_OT_dff_add_2dfx_particle,
53 | gui.OBJECT_OT_dff_add_2dfx_ped_attractor,
54 | gui.OBJECT_OT_dff_add_2dfx_sun_glare,
55 | gui.OBJECT_OT_dff_add_2dfx_road_sign,
56 | gui.OBJECT_OT_dff_add_2dfx_trigger_point,
57 | gui.OBJECT_OT_dff_add_2dfx_cover_point,
58 | gui.OBJECT_OT_dff_add_2dfx_escalator,
59 | gui.OBJECT_OT_dff_add_cull,
60 | gui.MATERIAL_PT_dffMaterials,
61 | gui.OBJECT_PT_dffObjects,
62 | gui.OBJECT_PT_dffCollections,
63 | gui.COLLECTION_OT_dff_generate_bounds,
64 | gui.EXT2DFXObjectProps,
65 | gui.Light2DFXObjectProps,
66 | gui.RoadSign2DFXObjectProps,
67 | gui.CULLObjectProps,
68 | gui.IMPORT_OT_ParticleTXDNames,
69 | gui.DFFMaterialProps,
70 | gui.DFFObjectProps,
71 | gui.DFFCollectionProps,
72 | gui.MapImportPanel,
73 | gui.DFFFrameProps,
74 | gui.DFFAtomicProps,
75 | gui.DFFSceneProps,
76 | gui.DFF_MT_ExportChoice,
77 | gui.DFF_MT_EditArmature,
78 | gui.DFF_MT_Pose,
79 | gui.DFF_MT_AddCollisionObject,
80 | gui.DFF_MT_Add2DFXObject,
81 | gui.DFF_MT_AddMapObject,
82 | gui.DFF_MT_AddObject,
83 | gui.DFF_UL_FrameItems,
84 | gui.DFF_UL_AtomicItems,
85 | gui.SCENE_PT_dffFrames,
86 | gui.SCENE_PT_dffAtomics,
87 | gui.Bounds3DGizmo,
88 | gui.Bound2DWidthGizmo,
89 | gui.Bound2DHeightGizmo,
90 | gui.VectorPlaneGizmo,
91 | gui.CollisionCollectionGizmoGroup,
92 | gui.PedAttractor2DFXGizmoGroup,
93 | gui.RoadSign2DFXGizmoGroup,
94 | gui.Escalator2DFXGizmoGroup
95 | ]
96 |
97 | #######################################################
98 | def register():
99 |
100 | # Register all the classes
101 | for cls in _classes:
102 | register_class(cls)
103 |
104 | bpy.types.Scene.dff = bpy.props.PointerProperty(type=gui.DFFSceneProps)
105 | bpy.types.Light.ext_2dfx = bpy.props.PointerProperty(type=gui.Light2DFXObjectProps)
106 | bpy.types.TextCurve.ext_2dfx = bpy.props.PointerProperty(type=gui.RoadSign2DFXObjectProps)
107 | bpy.types.Material.dff = bpy.props.PointerProperty(type=gui.DFFMaterialProps)
108 | bpy.types.Object.dff = bpy.props.PointerProperty(type=gui.DFFObjectProps)
109 | bpy.types.Collection.dff = bpy.props.PointerProperty(type=gui.DFFCollectionProps)
110 |
111 | bpy.types.TOPBAR_MT_file_import.append(gui.import_dff_func)
112 | bpy.types.TOPBAR_MT_file_export.append(gui.export_dff_func)
113 | bpy.types.OUTLINER_MT_collection.append(gui.export_col_outliner)
114 | bpy.types.OUTLINER_MT_object.append(gui.export_dff_outliner)
115 | bpy.types.VIEW3D_MT_edit_armature.append(gui.edit_armature_dff_func)
116 | bpy.types.VIEW3D_MT_pose.append(gui.pose_dff_func)
117 | bpy.types.VIEW3D_MT_add.append(gui.add_object_dff_func)
118 |
119 | gui.State.hook_events()
120 |
121 | #######################################################
122 | def unregister():
123 |
124 | del bpy.types.Scene.dff
125 | del bpy.types.Light.ext_2dfx
126 | del bpy.types.TextCurve.ext_2dfx
127 | del bpy.types.Material.dff
128 | del bpy.types.Object.dff
129 | del bpy.types.Collection.dff
130 |
131 | bpy.types.TOPBAR_MT_file_import.remove(gui.import_dff_func)
132 | bpy.types.TOPBAR_MT_file_export.remove(gui.export_dff_func)
133 | bpy.types.OUTLINER_MT_collection.remove(gui.export_col_outliner)
134 | bpy.types.OUTLINER_MT_object.remove(gui.export_dff_outliner)
135 | bpy.types.VIEW3D_MT_edit_armature.remove(gui.edit_armature_dff_func)
136 | bpy.types.VIEW3D_MT_pose.remove(gui.pose_dff_func)
137 | bpy.types.VIEW3D_MT_add.remove(gui.add_object_dff_func)
138 |
139 | gui.FaceGroupsDrawer.disable_draw()
140 |
141 | gui.State.unhook_events()
142 |
143 | # Unregister all the classes
144 | for cls in _classes:
145 | unregister_class(cls)
146 |
--------------------------------------------------------------------------------
/gtaLib/data/col_materials.py:
--------------------------------------------------------------------------------
1 | # Converted from col.ini by Steve-M: http://ce2.steve-m.com/?cmd=materials
2 |
3 | default = {
4 | "group": 12,
5 | "material": 0
6 | }
7 |
8 | groups = {
9 | 0: ["Default", "505050"],
10 | 1: ["Concrete", "909090"],
11 | 2: ["Gravel", "645E53"],
12 | 3: ["Grass", "92C032"],
13 | 4: ["Dirt", "775F40"],
14 | 5: ["Sand", "E7E17E"],
15 | 6: ["Glass", "A7E9FC"],
16 | 7: ["Wood", "936944"],
17 | 8: ["Metal", "BFC8D5"],
18 | 9: ["Stone", "AFAAA0"],
19 | 10: ["Vegetation", "2EA563"],
20 | 11: ["Water", "6493E1"],
21 | 12: ["Misc", "F1AB07"]
22 | }
23 |
24 | sa_mats = {
25 | 0: [0, "Default"],
26 | 1: [0, "Tarmac"],
27 | 2: [0, "Tarmac (fucked)"],
28 | 3: [0, "Tarmac (really fucked)"],
29 | 4: [1, "Pavement"],
30 | 5: [1, "Pavement (fucked)"],
31 | 6: [2, "Gravel"],
32 | 7: [1, "Concrete (fucked)"],
33 | 8: [1, "Painted Ground"],
34 | 9: [3, "Grass (short, lush)"],
35 | 10: [3, "Grass (medium, lush)"],
36 | 11: [3, "Grass (long, lush)"],
37 | 12: [3, "Grass (short, dry)"],
38 | 13: [3, "Grass (medium, dry)"],
39 | 14: [3, "Grass (long, dry)"],
40 | 15: [3, "Golf Grass (rough)"],
41 | 16: [3, "Golf Grass (smooth)"],
42 | 17: [3, "Steep Slidy Grass"],
43 | 18: [9, "Steep Cliff"],
44 | 19: [4, "Flower Bed"],
45 | 20: [3, "Meadow"],
46 | 21: [4, "Waste Ground"],
47 | 22: [4, "Woodland Ground"],
48 | 23: [10, "Vegetation"],
49 | 24: [4, "Mud (wet)"],
50 | 25: [4, "Mud (dry)"],
51 | 26: [4, "Dirt"],
52 | 27: [4, "Dirt Track"],
53 | 28: [5, "Sand (deep)"],
54 | 29: [5, "Sand (medium)"],
55 | 30: [5, "Sand (compact)"],
56 | 31: [5, "Sand (arid)"],
57 | 32: [5, "Sand (more)"],
58 | 33: [5, "Sand (beach)"],
59 | 34: [1, "Concrete (beach)"],
60 | 35: [9, "Rock (dry)"],
61 | 36: [9, "Rock (wet)"],
62 | 37: [9, "Rock (cliff)"],
63 | 38: [11, "Water (riverbed)"],
64 | 39: [11, "Water (shallow)"],
65 | 40: [4, "Corn Field"],
66 | 41: [10, "Hedge"],
67 | 42: [7, "Wood (crates)"],
68 | 43: [7, "Wood (solid)"],
69 | 44: [7, "Wood (thin)"],
70 | 45: [6, "Glass"],
71 | 46: [6, "Glass Windows (large)"],
72 | 47: [6, "Glass Windows (small)"],
73 | 48: [12, "Empty1"],
74 | 49: [12, "Empty2"],
75 | 50: [8, "Garage Door"],
76 | 51: [8, "Thick Metal Plate"],
77 | 52: [8, "Scaffold Pole"],
78 | 53: [8, "Lamp Post"],
79 | 54: [8, "Metal Gate"],
80 | 55: [8, "Metal Chain fence"],
81 | 56: [8, "Girder"],
82 | 57: [8, "Fire Hydrant"],
83 | 58: [8, "Container"],
84 | 59: [8, "News Vendor"],
85 | 60: [12, "Wheelbase"],
86 | 61: [12, "Cardboard Box"],
87 | 62: [12, "Ped"],
88 | 63: [8, "Car"],
89 | 64: [8, "Car (panel)"],
90 | 65: [8, "Car (moving component)"],
91 | 66: [12, "Transparent Cloth"],
92 | 67: [12, "Rubber"],
93 | 68: [12, "Plastic"],
94 | 69: [9, "Transparent Stone"],
95 | 70: [7, "Wood (bench)"],
96 | 71: [12, "Carpet"],
97 | 72: [7, "Floorboard"],
98 | 73: [7, "Stairs (wood)"],
99 | 74: [5, "P Sand"],
100 | 75: [5, "P Sand (dense)"],
101 | 76: [5, "P Sand (arid)"],
102 | 77: [5, "P Sand (compact)"],
103 | 78: [5, "P Sand (rocky)"],
104 | 79: [5, "P Sand (beach)"],
105 | 80: [3, "P Grass (short)"],
106 | 81: [3, "P Grass (meadow)"],
107 | 82: [3, "P Grass (dry)"],
108 | 83: [4, "P Woodland"],
109 | 84: [4, "P Wood Dense"],
110 | 85: [2, "P Roadside"],
111 | 86: [5, "P Roadside Des"],
112 | 87: [4, "P Flowerbed"],
113 | 88: [4, "P Waste Ground"],
114 | 89: [1, "P Concrete"],
115 | 90: [12, "P Office Desk"],
116 | 91: [12, "P 711 Shelf 1"],
117 | 92: [12, "P 711 Shelf 2"],
118 | 93: [12, "P 711 Shelf 3"],
119 | 94: [12, "P Restuarant Table"],
120 | 95: [12, "P Bar Table"],
121 | 96: [5, "P Underwater (lush)"],
122 | 97: [5, "P Underwater (barren)"],
123 | 98: [5, "P Underwater (coral)"],
124 | 99: [5, "P Underwater (deep)"],
125 | 100: [4, "P Riverbed"],
126 | 101: [2, "P Rubble"],
127 | 102: [12, "P Bedroom Floor"],
128 | 103: [12, "P Kitchen Floor"],
129 | 104: [12, "P Livingroom Floor"],
130 | 105: [12, "P corridor Floor"],
131 | 106: [12, "P 711 Floor"],
132 | 107: [12, "P Fast Food Floor"],
133 | 108: [12, "P Skanky Floor"],
134 | 109: [9, "P Mountain"],
135 | 110: [4, "P Marsh"],
136 | 111: [10, "P Bushy"],
137 | 112: [10, "P Bushy (mix)"],
138 | 113: [10, "P Bushy (dry)"],
139 | 114: [10, "P Bushy (mid)"],
140 | 115: [3, "P Grass (wee flowers)"],
141 | 116: [3, "P Grass (dry, tall)"],
142 | 117: [3, "P Grass (lush, tall)"],
143 | 118: [3, "P Grass (green, mix)"],
144 | 119: [3, "P Grass (brown, mix)"],
145 | 120: [3, "P Grass (low)"],
146 | 121: [3, "P Grass (rocky)"],
147 | 122: [3, "P Grass (small trees)"],
148 | 123: [4, "P Dirt (rocky)"],
149 | 124: [4, "P Dirt (weeds)"],
150 | 125: [3, "P Grass (weeds)"],
151 | 126: [4, "P River Edge"],
152 | 127: [1, "P Poolside"],
153 | 128: [4, "P Forest (stumps)"],
154 | 129: [4, "P Forest (sticks)"],
155 | 130: [4, "P Forest (leaves)"],
156 | 131: [5, "P Desert Rocks"],
157 | 132: [4, "Forest (dry)"],
158 | 133: [4, "P Sparse Flowers"],
159 | 134: [2, "P Building Site"],
160 | 135: [1, "P Docklands"],
161 | 136: [1, "P Industrial"],
162 | 137: [1, "P Industrial Jetty"],
163 | 138: [1, "P Concrete (litter)"],
164 | 139: [1, "P Alley Rubbish"],
165 | 140: [2, "P Junkyard Piles"],
166 | 141: [4, "P Junkyard Ground"],
167 | 142: [4, "P Dump"],
168 | 143: [5, "P Cactus Dense"],
169 | 144: [1, "P Airport Ground"],
170 | 145: [4, "P Cornfield"],
171 | 146: [3, "P Grass (light)"],
172 | 147: [3, "P Grass (lighter)"],
173 | 148: [3, "P Grass (lighter 2)"],
174 | 149: [3, "P Grass (mid 1)"],
175 | 150: [3, "P Grass (mid 2)"],
176 | 151: [3, "P Grass (dark)"],
177 | 152: [3, "P Grass (dark 2)"],
178 | 153: [3, "P Grass (dirt mix)"],
179 | 154: [9, "P Riverbed (stone)"],
180 | 155: [4, "P Riverbed (shallow)"],
181 | 156: [4, "P Riverbed (weeds)"],
182 | 157: [5, "P Seaweed"],
183 | 158: [12, "Door"],
184 | 159: [12, "Plastic Barrier"],
185 | 160: [3, "Park Grass"],
186 | 161: [9, "Stairs (stone)"],
187 | 162: [8, "Stairs (metal)"],
188 | 163: [12, "Stairs (carpet)"],
189 | 164: [8, "Floor (metal)"],
190 | 165: [1, "Floor (concrete)"],
191 | 166: [12, "Bin Bag"],
192 | 167: [8, "Thin Metal Sheet"],
193 | 168: [8, "Metal Barrel"],
194 | 169: [12, "Plastic Cone"],
195 | 170: [12, "Plastic Dumpster"],
196 | 171: [8, "Metal Dumpster"],
197 | 172: [7, "Wood Picket Fence"],
198 | 173: [7, "Wood Slatted Fence"],
199 | 174: [7, "Wood Ranch Fence"],
200 | 175: [6, "Unbreakable Glass"],
201 | 176: [12, "Hay Bale"],
202 | 177: [12, "Gore"],
203 | 178: [12, "Rail Track"]
204 | }
205 |
206 | vc_mats = {
207 | 0: [0, "Default"],
208 | 1: [0, "Street"],
209 | 2: [3, "Grass"],
210 | 3: [4, "Mud"],
211 | 4: [4, "Dirt"],
212 | 5: [1, "Concrete"],
213 | 6: [8, "Aluminum"],
214 | 7: [6, "Glass"],
215 | 8: [8, "Metal Pole"],
216 | 9: [12, "Door"],
217 | 10: [8, "Metal Sheet"],
218 | 11: [8, "Metal"],
219 | 12: [8, "Small Metal Post"],
220 | 13: [8, "Large Metal Post"],
221 | 14: [8, "Medium Metal Post"],
222 | 15: [8, "Steel"],
223 | 16: [8, "Fence"],
224 | 17: [12, "???"],
225 | 18: [5, "Sand"],
226 | 19: [11, "Water"],
227 | 20: [7, "Wooden Box"],
228 | 21: [7, "Wooden Lathes"],
229 | 22: [7, "Wood"],
230 | 23: [8, "Metal Box"],
231 | 24: [8, "Metal Box?"],
232 | 25: [10, "Hedge"],
233 | 26: [9, "Rock"],
234 | 27: [8, "Metal Container"],
235 | 28: [8, "Metal Barrel"],
236 | 29: [12, "???"],
237 | 30: [8, "Metal Card Box,"],
238 | 31: [12, "???"],
239 | 32: [8, "Gate/Bars,"],
240 | 33: [5, "Sand 2 (VC)"],
241 | 34: [3, "Grass 2 (VC)"]
242 | }
243 |
244 | vc_sa_conv = {
245 | 0 : 0,
246 | 1 : 1,
247 | 2 : 9,
248 | 3 : 25,
249 | 4 : 26,
250 | 5 : 4,
251 | 6 : 0,
252 | 7 : 45,
253 | 8 : 52,
254 | 9 : 158,
255 | 10 : 167,
256 | 11 : 51,
257 | 12 : 53,
258 | 13 : 53,
259 | 14 : 53,
260 | 15 : 56,
261 | 16 : 55,
262 | 17 : 0,
263 | 18 : 33,
264 | 19 : 39,
265 | 20 : 42,
266 | 21 : 43,
267 | 22 : 44,
268 | 23 : 171,
269 | 24 : 171,
270 | 25 : 41,
271 | 26 : 35,
272 | 27 : 58,
273 | 28 : 168,
274 | 29 : 0,
275 | 30 : 59,
276 | 31 : 0,
277 | 32 : 54,
278 | 33 : 33,
279 | 34 : 9,
280 | }
281 |
--------------------------------------------------------------------------------
/ops/txd_exporter.py:
--------------------------------------------------------------------------------
1 | # GTA DragonFF - Blender scripts to edit basic GTA formats
2 | # Copyright (C) 2019 Parik
3 |
4 | # This program is free software: you can redistribute it and/or modify
5 | # it under the terms of the GNU General Public License as published by
6 | # the Free Software Foundation, either version 3 of the License, or
7 | # (at your option) any later version.
8 |
9 | # This program is distributed in the hope that it will be useful,
10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | # GNU General Public License for more details.
13 |
14 | # You should have received a copy of the GNU General Public License
15 | # along with this program. If not, see .
16 |
17 | import bpy
18 | import os
19 | import re
20 |
21 | from ..gtaLib import txd
22 | from ..gtaLib.txd import ImageEncoder
23 | from ..gtaLib.dff import NativePlatformType
24 |
25 | #######################################################
26 | def clear_extension(string):
27 | k = string.rfind('.')
28 | return string if k < 0 else string[:k]
29 |
30 | #######################################################
31 | class txd_exporter:
32 |
33 | mass_export = False
34 | only_used_textures = True
35 | version = None
36 | file_name = ""
37 | path = ""
38 | txd = None
39 |
40 | #######################################################
41 | @staticmethod
42 | def _create_texture_native_from_image(image, image_name):
43 | pixels = list(image.pixels)
44 | width, height = image.size
45 |
46 | rgba_data = bytearray()
47 | for h in range(height - 1, -1, -1):
48 | offset = h * width * 4
49 | row_pixels = pixels[offset:offset + width * 4]
50 | rgba_data.extend(int(round(p * 0xff)) for p in row_pixels)
51 |
52 | texture_native = txd.TextureNative()
53 | texture_native.platform_id = NativePlatformType.D3D9
54 | texture_native.filter_mode = 0x06 # Linear Mip Linear (Trilinear)
55 | texture_native.uv_addressing = 0b00010001 # Wrap for both U and V
56 |
57 | # Clean texture name - remove invalid characters and limit length
58 | clean_name = "".join(c for c in image_name if c.isalnum() or c in "_-.")
59 | clean_name = clean_name[:31] # Limit to 31 chars (32 with null terminator)
60 | if not clean_name:
61 | clean_name = "texture"
62 | texture_native.name = clean_name
63 | texture_native.mask = ""
64 |
65 | # Raster format flags for RGBA8888: format type (8888=5) at bit 8-11, no mipmaps, no palette
66 | texture_native.raster_format_flags = (txd.RasterFormat.RASTER_8888 << 8) | 0x05
67 | texture_native.d3d_format = txd.D3DFormat.D3D_8888
68 | texture_native.width = width
69 | texture_native.height = height
70 | texture_native.depth = 32
71 | texture_native.num_levels = 1
72 | texture_native.raster_type = 4 # Texture
73 |
74 | texture_native.platform_properties = type('PlatformProperties', (), {
75 | 'alpha': True,
76 | 'cube_texture': False,
77 | 'auto_mipmaps': False,
78 | 'compressed': False
79 | })()
80 |
81 | # No palette for RGBA8888 format
82 | texture_native.palette = b''
83 |
84 | # Convert RGBA to BGRA8888 format
85 | pixel_data = ImageEncoder.rgba_to_bgra8888(rgba_data)
86 | texture_native.pixels = [pixel_data]
87 |
88 | return texture_native
89 |
90 | #######################################################
91 | @staticmethod
92 | def extract_texture_info_from_name(name):
93 | """Extract texture info from TXD import naming pattern"""
94 | pattern = r'^[^/]+\.txd/([^/]+)/(\d+)$'
95 | match = re.match(pattern, name)
96 | if match:
97 | return match.group(1), int(match.group(2))
98 | else:
99 | return name, 0
100 |
101 | #######################################################
102 | @staticmethod
103 | def get_used_textures(objects_to_scan=None):
104 | """Collect textures that are used in scene materials"""
105 | used_textures = set()
106 |
107 | # Use provided objects or all scene objects
108 | objects = objects_to_scan if objects_to_scan is not None else bpy.context.scene.objects
109 |
110 | for obj in objects:
111 | for mat_slot in obj.material_slots:
112 | mat = mat_slot.material
113 | if not mat:
114 | continue
115 |
116 | node_tree = mat.node_tree
117 | if not node_tree:
118 | continue
119 |
120 | for node in node_tree.nodes:
121 | if node.type == 'TEX_IMAGE':
122 | texture_name = clear_extension(node.label or node.image.name)
123 | used_textures.add((texture_name, node.image))
124 |
125 | return used_textures
126 |
127 | #######################################################
128 | @staticmethod
129 | def populate_textures(objects_to_scan=None):
130 | self = txd_exporter
131 |
132 | self.txd.native_textures = []
133 |
134 | # Determine which textures to export based on context
135 | if objects_to_scan is not None:
136 | # Mass export mode: only export textures used by specific objects
137 | used_textures = self.get_used_textures(objects_to_scan)
138 | elif self.only_used_textures:
139 | # Single export with "only used textures" option
140 | used_textures = self.get_used_textures()
141 | else:
142 | # Single export, all textures
143 | used_textures = set()
144 | for image in bpy.data.images:
145 | # Skip invalid/system textures
146 | if (image.name.startswith("//") or
147 | image.name in ["Render Result", "Viewer Node"] or
148 | not image.name.strip() or
149 | image.size[0] == 0 or image.size[1] == 0):
150 | continue
151 |
152 | # Extract texture name from node.label (in case it follows TXD naming pattern)
153 | texture_name, mipmap_level = self.extract_texture_info_from_name(image.name)
154 |
155 | # Skip mipmaps
156 | if mipmap_level > 0:
157 | continue
158 |
159 | texture_name = clear_extension(texture_name)
160 | used_textures.add((texture_name, image))
161 |
162 | for texture_name, image in used_textures:
163 | # Skip images without pixel data
164 | if not hasattr(image, 'pixels') or len(image.pixels) == 0:
165 | continue
166 |
167 | texture_native = txd_exporter._create_texture_native_from_image(
168 | image, texture_name
169 | )
170 | self.txd.native_textures.append(texture_native)
171 |
172 | #######################################################
173 | @staticmethod
174 | def export_textures(objects_to_scan=None, file_name=None):
175 | self = txd_exporter
176 |
177 | self.txd = txd.txd()
178 | self.txd.device_id = txd.DeviceType.DEVICE_D3D9
179 |
180 | self.populate_textures(objects_to_scan)
181 | self.txd.write_file(file_name or self.file_name, self.version)
182 |
183 | #######################################################
184 | @staticmethod
185 | def export_txd(file_name):
186 | self = txd_exporter
187 |
188 | self.file_name = file_name
189 |
190 | if self.mass_export:
191 | # Export TXD files per selected object
192 | selected_objects = bpy.context.selected_objects
193 |
194 | if not selected_objects:
195 | print("No objects selected for mass export, exporting all textures to single file")
196 | self.export_textures()
197 | return
198 |
199 | selected_objects_num = 0
200 |
201 | for obj in bpy.context.selected_objects:
202 | # Only export for objects that have materials
203 | if not obj.material_slots:
204 | continue
205 |
206 | # Create filename based on object name
207 | safe_name = "".join(c for c in obj.name if c.isalnum() or c in "_-.")
208 | file_name = os.path.join(self.path, f"{safe_name}.txd")
209 | print(f"Exporting textures for object '{obj.name}' to {file_name}")
210 |
211 | # Export textures used by this specific object only
212 | self.export_txd([obj], file_name)
213 | selected_objects_num += 1
214 |
215 | print(f"Mass export completed for {selected_objects_num} objects")
216 |
217 | else:
218 | self.export_textures()
219 |
220 | #######################################################
221 | def export_txd(options):
222 |
223 | txd_exporter.mass_export = options.get('mass_export', False)
224 | txd_exporter.only_used_textures = options.get('only_used_textures', True)
225 | txd_exporter.version = options.get('version', 0x36003)
226 |
227 | txd_exporter.path = options['directory']
228 |
229 | txd_exporter.export_txd(options['file_name'])
230 |
--------------------------------------------------------------------------------
/ops/col_importer.py:
--------------------------------------------------------------------------------
1 | # GTA DragonFF - Blender scripts to edit basic GTA formats
2 | # Copyright (C) 2019 Parik
3 |
4 | # This program is free software: you can redistribute it and/or modify
5 | # it under the terms of the GNU General Public License as published by
6 | # the Free Software Foundation, either version 3 of the License, or
7 | # (at your option) any later version.
8 |
9 | # This program is distributed in the hope that it will be useful,
10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | # GNU General Public License for more details.
13 |
14 | # You should have received a copy of the GNU General Public License
15 | # along with this program. If not, see .
16 |
17 | import bpy
18 | import bmesh
19 | import mathutils
20 |
21 | from ..gtaLib import col
22 | from ..gtaLib.data import col_materials as mats
23 | from .importer_common import (
24 | link_object, create_collection, material_helper
25 | )
26 |
27 | #######################################################
28 | class col_importer:
29 |
30 | #######################################################
31 | def __init__(self, col):
32 | self.col = col
33 |
34 | #######################################################
35 | def from_file(filename):
36 |
37 | collision = col.coll()
38 | collision.load_file(filename)
39 |
40 | return col_importer(collision)
41 |
42 | #######################################################
43 | def from_mem(memory):
44 |
45 | collision = col.coll()
46 | collision.load_memory(memory)
47 |
48 | return col_importer(collision)
49 |
50 | #######################################################
51 | def __add_spheres(self, collection, array):
52 |
53 | for index, entity in enumerate(array):
54 | name = collection.name + ".ColSphere.%d" % index
55 |
56 | obj = bpy.data.objects.new(name, None)
57 |
58 | obj.location = entity.center
59 | obj.scale = [entity.radius] * 3
60 |
61 | obj.empty_display_type = 'SPHERE'
62 |
63 | obj.dff.type = 'COL'
64 | obj.dff.col_material = entity.surface.material
65 | obj.dff.col_flags = entity.surface.flags
66 | obj.dff.col_brightness = entity.surface.brightness
67 | obj.dff.col_day_light = entity.surface.light & 0xf
68 | obj.dff.col_night_light = (entity.surface.light >> 4) & 0xf
69 |
70 | link_object(obj, collection)
71 |
72 | #######################################################
73 | def __add_boxes(self, collection, array):
74 |
75 | for index, entity in enumerate(array):
76 | name = collection.name + ".ColBox.%d" % index
77 |
78 | obj = bpy.data.objects.new(name, None)
79 |
80 | mn = mathutils.Vector(entity.min)
81 | mx = mathutils.Vector(entity.max)
82 | half = 0.5 * (mx - mn)
83 | obj.location = mn + half
84 | obj.scale = half
85 |
86 | obj.empty_display_type = 'CUBE'
87 |
88 | obj.dff.type = 'COL'
89 | obj.dff.col_material = entity.surface.material
90 | obj.dff.col_flags = entity.surface.flags
91 | obj.dff.col_brightness = entity.surface.brightness
92 | obj.dff.col_day_light = entity.surface.light & 0xf
93 | obj.dff.col_night_light = (entity.surface.light >> 4) & 0xf
94 |
95 | link_object(obj, collection)
96 |
97 | #######################################################
98 | def __add_mesh_mats(self, object, materials):
99 |
100 | for surface in materials:
101 |
102 | colour = mats.groups[mats.default['group']][1]
103 | name = mats.groups[mats.default['group']][0]
104 |
105 | try:
106 | # SA
107 | if col.Sections.version == 3 or surface.material >= 34:
108 | mat = mats.sa_mats[surface.material]
109 |
110 | # VC/III
111 | else:
112 | mat = mats.vc_mats[surface.material]
113 |
114 | # Generate names
115 | colour = mats.groups[mat[0]][1]
116 | name = "%s - %s" % (mats.groups[mat[0]][0], mat[1])
117 |
118 | except KeyError:
119 | pass
120 |
121 | # Convert hex to a value Blender understands
122 | colour = [colour[0:2], colour[2: 4], colour[4: 6], "FF"]
123 | colour = [int(x, 16) for x in colour]
124 |
125 | mat = bpy.data.materials.new(name)
126 | mat.dff.col_mat_index = surface.material
127 | mat.dff.col_brightness = surface.brightness
128 | mat.dff.col_day_light = surface.light & 0xf
129 | mat.dff.col_night_light = (surface.light >> 4) & 0xf
130 |
131 | helper = material_helper(mat)
132 | helper.set_base_color(colour)
133 |
134 | object.data.materials.append(helper.material)
135 |
136 | #######################################################
137 | def __add_mesh(self, collection, name, verts, faces, face_groups, shadow=False):
138 |
139 | mesh = bpy.data.meshes.new(name)
140 | materials = {}
141 |
142 | bm = bmesh.new()
143 |
144 | for v in verts:
145 | bm.verts.new(v)
146 |
147 | bm.verts.ensure_lookup_table()
148 |
149 | for f in faces:
150 | try:
151 | face = bm.faces.new(
152 | [
153 | bm.verts[f.a],
154 | bm.verts[f.c],
155 | bm.verts[f.b]
156 | ]
157 | )
158 | if hasattr(f, "surface"):
159 | surface = f.surface
160 | else:
161 | surface = col.TSurface(f.material, 0, 1, f.light)
162 |
163 | if surface not in materials:
164 | materials[surface] = len(materials)
165 |
166 | face.material_index = materials[surface]
167 |
168 | except Exception as e:
169 | print(e)
170 |
171 | bm.to_mesh(mesh)
172 |
173 | # Face groups get stored in a face attribute on the mesh, each face storing the index of its group
174 | if face_groups:
175 | if (2, 93, 0) > bpy.app.version:
176 | attribute = mesh.attributes.new(name="face group", type="INT", domain="POLYGON")
177 | attribute = mesh.attributes[attribute.name]
178 | else:
179 | attribute = mesh.attributes.new(name="face group", type="INT", domain="FACE")
180 |
181 | for fg_idx, fg in enumerate(face_groups):
182 | for face_idx in range(fg.start, fg.end+1):
183 | if face_idx >= len(bm.faces):
184 | break
185 | attribute.data[face_idx].value = fg_idx
186 |
187 | obj = bpy.data.objects.new(name, mesh)
188 | obj.dff.type = 'SHA' if shadow else 'COL'
189 |
190 | link_object(obj, collection)
191 |
192 | self.__add_mesh_mats(obj, materials)
193 |
194 | #######################################################
195 | def add_to_scene(self, collection_prefix, link=True):
196 |
197 | collection_list = []
198 |
199 | for model in self.col.models:
200 |
201 | collection = create_collection("%s.%s" % (collection_prefix,
202 | model.model_name),
203 | link
204 | )
205 |
206 | # Store the import bounds as a custom property of the collection
207 | collection.dff.bounds_min = model.bounds.min
208 | collection.dff.bounds_max = model.bounds.max
209 |
210 | self.__add_spheres(collection, model.spheres)
211 | self.__add_boxes(collection, model.boxes)
212 |
213 | if len(model.mesh_verts) > 0:
214 | self.__add_mesh(collection,
215 | collection.name + ".ColMesh",
216 | model.mesh_verts,
217 | model.mesh_faces,
218 | model.face_groups if model.flags & 8 else None)
219 |
220 | if len(model.shadow_verts) > 0:
221 | self.__add_mesh(collection,
222 | collection.name + ".ShadowMesh",
223 | model.shadow_verts,
224 | model.shadow_faces,
225 | None,
226 | True)
227 |
228 | collection.dff.auto_bounds = len(collection.objects) > 0
229 |
230 | collection_list.append(collection)
231 |
232 | return collection_list
233 |
234 | #######################################################
235 | def import_col_file(filename, collection_prefix, link=True):
236 |
237 | col = col_importer.from_file(filename)
238 | return col.add_to_scene(collection_prefix, link)
239 |
240 | #######################################################
241 | def import_col_mem(mem, collection_prefix, link=True):
242 |
243 | col = col_importer.from_mem(mem)
244 | return col.add_to_scene(collection_prefix, link)
245 |
--------------------------------------------------------------------------------
/gtaLib/native_wdgl.py:
--------------------------------------------------------------------------------
1 | # GTA DragonFF - Blender scripts to edit basic GTA formats
2 | # Copyright (C) 2019 Parik
3 |
4 | # This program is free software: you can redistribute it and/or modify
5 | # it under the terms of the GNU General Public License as published by
6 | # the Free Software Foundation, either version 3 of the License, or
7 | # (at your option) any later version.
8 |
9 | # This program is distributed in the hope that it will be useful,
10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | # GNU General Public License for more details.
13 |
14 | # You should have received a copy of the GNU General Public License
15 | # along with this program. If not, see .
16 |
17 | from struct import unpack_from
18 |
19 | from .dff import SkinPLG, RGBA, TexCoords, Vector, ExtraVertColorExtension
20 |
21 | ATTRIB_ID_COORD = 0
22 | ATTRIB_ID_TEX_COORD = 1
23 | ATTRIB_ID_NORMAL = 2
24 | ATTRIB_ID_PRELIT = 3
25 | ATTRIB_ID_BONE_WEIGHT = 4
26 | ATTRIB_ID_BONE_INDEX = 5
27 | ATTRIB_ID_EXTRA_COLOR = 6
28 |
29 | ATTRIB_TYPE_FLOAT = 0
30 | ATTRIB_TYPE_BYTE = 1
31 | ATTRIB_TYPE_UBYTE = 2
32 | ATTRIB_TYPE_SHORT = 3
33 | ATTRIB_TYPE_USHORT = 4
34 |
35 | #######################################################
36 | class NativeOGLSkin:
37 |
38 | #######################################################
39 | @staticmethod
40 | def unpack(skin, data):
41 | skin.num_bones = unpack_from(".
16 |
17 | import bpy
18 |
19 | from .col_ot import FaceGroupsDrawer
20 | from .map_ot import SCENE_OT_ipl_select, OBJECT_OT_dff_add_cull
21 | from ..gtaLib.data import map_data
22 |
23 | #######################################################
24 | class DFFFrameProps(bpy.types.PropertyGroup):
25 | obj : bpy.props.PointerProperty(type=bpy.types.Object)
26 | icon : bpy.props.StringProperty()
27 |
28 | #######################################################
29 | class DFFAtomicProps(bpy.types.PropertyGroup):
30 | obj : bpy.props.PointerProperty(type=bpy.types.Object)
31 | frame_obj : bpy.props.PointerProperty(type=bpy.types.Object)
32 |
33 | #######################################################
34 | class DFFSceneProps(bpy.types.PropertyGroup):
35 |
36 | #######################################################
37 | def update_map_sections(self, context):
38 | return map_data.data[self.game_version_dropdown]['IPL_paths']
39 |
40 | #######################################################
41 | def frames_active_changed(self, context):
42 | scene_dff = context.scene.dff
43 |
44 | frames_num = len(scene_dff.frames)
45 | if not frames_num:
46 | return
47 |
48 | if scene_dff.frames_active >= frames_num:
49 | scene_dff.frames_active = frames_num - 1
50 | return
51 |
52 | frame_object = scene_dff.frames[scene_dff.frames_active].obj
53 |
54 | for a in scene_dff.frames: a.obj.select_set(False)
55 | frame_object.select_set(True)
56 | context.view_layer.objects.active = frame_object
57 |
58 | #######################################################
59 | def atomics_active_changed(self, context):
60 | scene_dff = context.scene.dff
61 |
62 | atomics_num = len(scene_dff.atomics)
63 | if not atomics_num:
64 | return
65 |
66 | if scene_dff.atomics_active >= atomics_num:
67 | scene_dff.atomics_active = atomics_num - 1
68 | return
69 |
70 | atomic_object = scene_dff.atomics[scene_dff.atomics_active].obj
71 |
72 | for a in scene_dff.atomics: a.obj.select_set(False)
73 | atomic_object.select_set(True)
74 | context.view_layer.objects.active = atomic_object
75 |
76 | game_version_dropdown : bpy.props.EnumProperty(
77 | name = 'Game',
78 | items = (
79 | (map_data.game_version.III, 'GTA III', 'GTA III map segments'),
80 | (map_data.game_version.VC, 'GTA VC', 'GTA VC map segments'),
81 | (map_data.game_version.SA, 'GTA SA', 'GTA SA map segments'),
82 | (map_data.game_version.LCS, 'GTA LCS', 'GTA LCS map segments'),
83 | (map_data.game_version.VCS, 'GTA VCS', 'GTA VCS map segments'),
84 | )
85 | )
86 |
87 | map_sections : bpy.props.EnumProperty(
88 | name = 'Map segment',
89 | items = update_map_sections
90 | )
91 |
92 | custom_ipl_path : bpy.props.StringProperty(
93 | name = "IPL path",
94 | default = '',
95 | description = "Custom IPL path (supports both relative paths from game root and absolute paths)"
96 | )
97 |
98 | use_custom_map_section : bpy.props.BoolProperty(
99 | name = "Use Custom Map Section",
100 | default = False
101 | )
102 |
103 | skip_lod: bpy.props.BoolProperty(
104 | name = "Skip LOD Objects",
105 | default = False
106 | )
107 |
108 | load_txd: bpy.props.BoolProperty(
109 | name = "Load TXD files",
110 | default = False
111 | )
112 |
113 | txd_pack : bpy.props.BoolProperty(
114 | name = "Pack Images",
115 | description = "Pack images as embedded data into the .blend file",
116 | default = False
117 | )
118 |
119 | read_mat_split : bpy.props.BoolProperty(
120 | name = "Read Material Split",
121 | description = "Whether to read material split for loading triangles",
122 | default = False
123 | )
124 |
125 | create_backfaces: bpy.props.BoolProperty(
126 | name = "Create Backfaces",
127 | description = "Create backfaces by duplicating existing faces. Incompatible with Use Edge Split",
128 | default = False
129 | )
130 |
131 | load_collisions: bpy.props.BoolProperty(
132 | name = "Load Map Collisions",
133 | default = False
134 | )
135 |
136 | load_cull: bpy.props.BoolProperty(
137 | name = "Load Map CULL",
138 | default = False
139 | )
140 |
141 | game_root : bpy.props.StringProperty(
142 | name = 'Game root',
143 | default = 'C:/Program Files (x86)/Steam/steamapps/common/',
144 | description = "Folder with the game's executable",
145 | subtype = 'DIR_PATH'
146 | )
147 |
148 | dff_folder : bpy.props.StringProperty(
149 | name = 'Dff folder',
150 | default = 'C:/Users/blaha/Documents/GitHub/DragonFF/tests/dff',
151 | description = "Define a folder where all of the dff models are stored.",
152 | subtype = 'DIR_PATH'
153 | )
154 |
155 | draw_facegroups : bpy.props.BoolProperty(
156 | name="Draw Face Groups",
157 | description="Display the Face Groups of the active object (if they exist) in the viewport",
158 | default=False,
159 | get=FaceGroupsDrawer.get_draw_enabled,
160 | set=FaceGroupsDrawer.set_draw_enabled
161 | )
162 |
163 | draw_bounds: bpy.props.BoolProperty(
164 | name="Draw Bounds",
165 | description = "Display the bounds of the active collection in the viewport",
166 | default = False
167 | )
168 |
169 | face_group_min : bpy.props.IntProperty(
170 | name = 'Face Group Minimum Size',
171 | description="Don't generate groups below this size",
172 | default = 20,
173 | min = 5,
174 | max = 200
175 | )
176 |
177 | face_group_max : bpy.props.IntProperty(
178 | name = 'Face Group Maximum Size',
179 | description="Don't generate groups above this size (minimum size overrides this if larger)",
180 | default = 50,
181 | min = 5,
182 | max = 200
183 | )
184 |
185 | face_group_avoid_smalls : bpy.props.BoolProperty(
186 | name = "Avoid overly small groups",
187 | description="Combine really small groups with their neighbor to avoid pointless isolated groups",
188 | default = True
189 | )
190 |
191 | frames : bpy.props.CollectionProperty(
192 | type = DFFFrameProps,
193 | options = {'SKIP_SAVE','HIDDEN'}
194 | )
195 |
196 | frames_active : bpy.props.IntProperty(
197 | name = "Active frame",
198 | default = 0,
199 | min = 0,
200 | update = frames_active_changed
201 | )
202 |
203 | atomics : bpy.props.CollectionProperty(
204 | type = DFFAtomicProps,
205 | options = {'SKIP_SAVE','HIDDEN'}
206 | )
207 |
208 | atomics_active : bpy.props.IntProperty(
209 | name = "Active atomic",
210 | default = 0,
211 | min = 0,
212 | update = atomics_active_changed
213 | )
214 |
215 | real_time_update : bpy.props.BoolProperty(
216 | name = "Real Time Update",
217 | description = "Update the list of objects in real time",
218 | default = True
219 | )
220 |
221 | filter_collection : bpy.props.BoolProperty(
222 | name = "Filter Collection",
223 | description = "Filter frames and atomics by active collection",
224 | default = True
225 | )
226 |
227 | #######################################################
228 | class MapImportPanel(bpy.types.Panel):
229 | """Creates a Panel in the scene context of the properties editor"""
230 | bl_label = "DragonFF - Map Import"
231 | bl_idname = "SCENE_PT_layout"
232 | bl_space_type = 'PROPERTIES'
233 | bl_region_type = 'WINDOW'
234 | bl_context = "scene"
235 |
236 | #######################################################
237 | def draw(self, context):
238 | layout = self.layout
239 | settings = context.scene.dff
240 |
241 | flow = layout.grid_flow(row_major=True,
242 | columns=0,
243 | even_columns=True,
244 | even_rows=False,
245 | align=True)
246 |
247 | col = flow.column()
248 | col.prop(settings, "game_version_dropdown")
249 | if settings.use_custom_map_section:
250 | row = col.row(align=True)
251 | row.prop(settings, "custom_ipl_path")
252 | row.operator(SCENE_OT_ipl_select.bl_idname, text="", icon='FILEBROWSER')
253 | else:
254 | col.prop(settings, "map_sections")
255 | col.prop(settings, "use_custom_map_section")
256 | col.separator()
257 |
258 | box = col.box()
259 | box.prop(settings, "load_txd")
260 | if settings.load_txd:
261 | box.prop(settings, "txd_pack")
262 |
263 | col.prop(settings, "skip_lod")
264 | col.prop(settings, "read_mat_split")
265 | col.prop(settings, "create_backfaces")
266 | col.prop(settings, "load_collisions")
267 | col.prop(settings, "load_cull")
268 |
269 | layout.separator()
270 |
271 | layout.prop(settings, 'game_root')
272 | layout.prop(settings, 'dff_folder')
273 |
274 | row = layout.row()
275 | row.operator("scene.dragonff_map_import")
276 |
277 | #######################################################@
278 | class DFF_MT_AddMapObject(bpy.types.Menu):
279 | bl_label = "Map"
280 |
281 | def draw(self, context):
282 | self.layout.operator(OBJECT_OT_dff_add_cull.bl_idname, text="CULL", icon="CUBE")
283 |
--------------------------------------------------------------------------------
/gui/ext_2dfx_ot.py:
--------------------------------------------------------------------------------
1 | import bpy
2 | import math
3 | from ..ops.ext_2dfx_importer import ext_2dfx_importer
4 | from ..ops.importer_common import link_object
5 |
6 | #######################################################
7 | class Add2DFXHelper:
8 | bl_options = {'REGISTER', 'UNDO'}
9 |
10 | location: bpy.props.FloatVectorProperty(
11 | name="Location",
12 | description="Location for the newly added object",
13 | subtype='XYZ',
14 | default=(0, 0, 0)
15 | )
16 |
17 | #######################################################
18 | def apply_2dfx_object(self, context, obj, effect):
19 | obj.location = self.location
20 | obj.dff.type = '2DFX'
21 | obj.dff.ext_2dfx.effect = effect
22 | link_object(obj, context.collection)
23 |
24 | context.view_layer.objects.active = obj
25 | for o in context.selected_objects:
26 | o.select_set(False)
27 | obj.select_set(True)
28 |
29 | #######################################################
30 | def invoke(self, context, event):
31 | self.location = context.scene.cursor.location
32 | return self.execute(context)
33 |
34 | #######################################################
35 | class OBJECT_OT_dff_add_2dfx_light(bpy.types.Operator, Add2DFXHelper):
36 |
37 | bl_idname = "object.dff_add_2dfx_light"
38 | bl_label = "Add 2DFX Light"
39 | bl_description = "Add 2DFX Light effect object to the scene"
40 |
41 | color: bpy.props.FloatVectorProperty(
42 | name="Color",
43 | subtype='COLOR',
44 | description = "Light color",
45 | min=0,
46 | max=1,
47 | default=(1, 1, 1)
48 | )
49 |
50 | #######################################################
51 | def execute(self, context):
52 | obj = ext_2dfx_importer.create_light_object(self.color)
53 | self.apply_2dfx_object(context, obj, '0')
54 |
55 | settings = obj.data.ext_2dfx
56 | settings.alpha = 0.784314
57 | settings.corona_far_clip = 200.0
58 | settings.point_light_range = 1.0
59 | settings.corona_size = 0.2
60 | settings.shadow_size = 0.0
61 | settings.corona_show_mode = '0'
62 | settings.corona_enable_reflection = False
63 | settings.corona_flare_type = 0
64 | settings.shadow_color_multiplier = 40
65 | settings.corona_tex_name = "coronastar"
66 | settings.shadow_tex_name = "shad_exp"
67 | settings.shadow_z_distance = 0
68 |
69 | settings.flag1_at_day = True
70 | settings.flag1_at_night = True
71 |
72 | settings.view_vector = (-110, 0, 0)
73 | settings.export_view_vector = True
74 |
75 | return {'FINISHED'}
76 |
77 | #######################################################
78 | class OBJECT_OT_dff_add_2dfx_particle(bpy.types.Operator, Add2DFXHelper):
79 |
80 | bl_idname = "object.dff_add_2dfx_particle"
81 | bl_label = "Add 2DFX Particle"
82 | bl_description = "Add 2DFX Particle effect object to the scene"
83 |
84 | effect: bpy.props.StringProperty(
85 | name="Effect Name",
86 | description="Effect name from effects.fxp",
87 | maxlen = 23,
88 | default="explosion_large"
89 | )
90 |
91 | #######################################################
92 | def execute(self, context):
93 | obj = ext_2dfx_importer.create_particle_object(self.effect)
94 | self.apply_2dfx_object(context, obj, '1')
95 |
96 | return {'FINISHED'}
97 |
98 | #######################################################
99 | class OBJECT_OT_dff_add_2dfx_ped_attractor(bpy.types.Operator, Add2DFXHelper):
100 |
101 | bl_idname = "object.dff_add_2dfx_ped_attractor"
102 | bl_label = "Add 2DFX Ped Attractor"
103 | bl_description = "Add 2DFX Ped Attractor effect object to the scene"
104 |
105 | ped_attractor_type : bpy.props.EnumProperty(
106 | name="Type",
107 | items=(
108 | ('0', 'ATM', 'Ped uses ATM (at day time only)'),
109 | ('1', 'Seat', 'Ped sits (at day time only)'),
110 | ('2', 'Stop', 'Ped stands (at day time only)'),
111 | ('3', 'Pizza', 'Ped stands for few seconds'),
112 | ('4', 'Shelter', 'Ped goes away after spawning, but stands if weather is rainy'),
113 | ('5', 'Trigger Script', 'Launches an external script'),
114 | ('6', 'Look At', 'Ped looks at object, then goes away'),
115 | ('7', 'Scripted', ''),
116 | ('8', 'Park', 'Ped lays (at day time only, ped goes away after 6 PM)'),
117 | ('9', 'Step', 'Ped sits on steps'),
118 | ),
119 | default='6'
120 | )
121 |
122 | ped_existing_probability : bpy.props.IntProperty(
123 | name="Ped Existing Probability",
124 | min=0,
125 | max=100,
126 | default=75
127 | )
128 |
129 | #######################################################
130 | def execute(self, context):
131 | obj = ext_2dfx_importer.create_ped_attractor_object(
132 | attractor_type=self.ped_attractor_type,
133 | queue_euler=(math.pi / 2, 0, math.pi),
134 | use_euler=(math.pi / 2, 0, math.pi),
135 | forward_euler=(math.pi / 2, 0, 0),
136 | external_script='none',
137 | ped_existing_probability=self.ped_existing_probability,
138 | unk=0
139 | )
140 | self.apply_2dfx_object(context, obj, '3')
141 |
142 | return {'FINISHED'}
143 |
144 | #######################################################
145 | class OBJECT_OT_dff_add_2dfx_sun_glare(bpy.types.Operator, Add2DFXHelper):
146 |
147 | bl_idname = "object.dff_add_2dfx_sun_glare"
148 | bl_label = "Add 2DFX Sun Glare"
149 | bl_description = "Add 2DFX Sun Glare effect object to the scene"
150 |
151 | #######################################################
152 | def execute(self, context):
153 | obj = ext_2dfx_importer.create_sun_glare_object()
154 | self.apply_2dfx_object(context, obj, '4')
155 |
156 | return {'FINISHED'}
157 |
158 | #######################################################
159 | class OBJECT_OT_dff_add_2dfx_road_sign(bpy.types.Operator, Add2DFXHelper):
160 |
161 | bl_idname = "object.dff_add_2dfx_road_sign"
162 | bl_label = "Add 2DFX Road Sign"
163 | bl_description = "Add 2DFX Road Sign effect object to the scene"
164 |
165 | color: bpy.props.EnumProperty(
166 | name="Color",
167 | items = (
168 | ('0', 'White', ''),
169 | ('1', 'Black', ''),
170 | ('2', 'Grey', ''),
171 | ('3', 'Red', ''),
172 | ),
173 | description = "Text color"
174 | )
175 |
176 | rotation_euler: bpy.props.FloatVectorProperty(
177 | name="Rotation",
178 | description="Rotation for the newly added object",
179 | subtype='EULER',
180 | min=-math.pi * 2,
181 | max=math.pi * 2,
182 | step=100,
183 | default=(math.pi / 2, -math.pi / 2, -math.pi / 2)
184 | )
185 |
186 | #######################################################
187 | def execute(self, context):
188 | obj = ext_2dfx_importer.create_road_sign_object(
189 | body="> Text\n^ Text\n# Text",
190 | size=(3.9, 2.3),
191 | color=self.color,
192 | rotation_euler=self.rotation_euler
193 | )
194 | self.apply_2dfx_object(context, obj, '7')
195 |
196 | return {'FINISHED'}
197 |
198 | #######################################################
199 | class OBJECT_OT_dff_add_2dfx_trigger_point(bpy.types.Operator, Add2DFXHelper):
200 |
201 | bl_idname = "object.dff_add_2dfx_trigger_point"
202 | bl_label = "Add 2DFX Trigger Point"
203 | bl_description = "Add 2DFX Trigger Point effect object to the scene"
204 |
205 | point_id: bpy.props.IntProperty(
206 | name="Point ID",
207 | description="Point ID",
208 | default=0
209 | )
210 |
211 | #######################################################
212 | def execute(self, context):
213 | obj = ext_2dfx_importer.create_trigger_point_object(self.point_id)
214 | self.apply_2dfx_object(context, obj, '8')
215 |
216 | return {'FINISHED'}
217 |
218 | #######################################################
219 | class OBJECT_OT_dff_add_2dfx_cover_point(bpy.types.Operator, Add2DFXHelper):
220 |
221 | bl_idname = "object.dff_add_2dfx_cover_point"
222 | bl_label = "Add 2DFX Cover Point"
223 | bl_description = "Add 2DFX Cover Point effect object to the scene"
224 |
225 | angle: bpy.props.FloatProperty(
226 | name="Angle",
227 | description="Angle along the Z axis",
228 | subtype='ANGLE',
229 | min=-math.pi * 2,
230 | max=math.pi * 2,
231 | step=100,
232 | default=0
233 | )
234 |
235 | cover_type: bpy.props.IntProperty(
236 | name="Cover Type",
237 | description="Cover Type",
238 | default=1
239 | )
240 |
241 | #######################################################
242 | def execute(self, context):
243 | obj = ext_2dfx_importer.create_cover_point_object(
244 | cover_type=self.cover_type,
245 | rotation_euler=(0, 0, self.angle)
246 | )
247 | self.apply_2dfx_object(context, obj, '9')
248 |
249 | return {'FINISHED'}
250 |
251 | #######################################################
252 | class OBJECT_OT_dff_add_2dfx_escalator(bpy.types.Operator, Add2DFXHelper):
253 |
254 | bl_idname = "object.dff_add_2dfx_escalator"
255 | bl_label = "Add 2DFX Escalator"
256 | bl_description = "Add 2DFX Escalator effect object to the scene"
257 |
258 | escalator_direction : bpy.props.EnumProperty(
259 | name="Direction",
260 | description="Escalator direction",
261 | items = (
262 | ('0', 'Down', 'Down Direction'),
263 | ('1', 'Up', 'Up Direction'),
264 | )
265 | )
266 |
267 | angle: bpy.props.FloatProperty(
268 | name="Angle",
269 | description="Angle along the Z axis",
270 | subtype='ANGLE',
271 | min=-math.pi * 2,
272 | max=math.pi * 2,
273 | step=100,
274 | default=0
275 | )
276 |
277 | #######################################################
278 | def execute(self, context):
279 | obj = ext_2dfx_importer.create_escalator_object(
280 | bottom=(0.0, -2.0, 0.0),
281 | top=(0.0, -11.0, 6.62),
282 | end=(0.0, -13.0, 6.62),
283 | direction=self.escalator_direction,
284 | angle=self.angle
285 | )
286 | self.apply_2dfx_object(context, obj, '10')
287 |
288 | return {'FINISHED'}
289 |
--------------------------------------------------------------------------------
/ops/ext_2dfx_exporter.py:
--------------------------------------------------------------------------------
1 | # GTA DragonFF - Blender scripts to edit basic GTA formats
2 | # Copyright (C) 2019 Parik
3 |
4 | # This program is free software: you can redistribute it and/or modify
5 | # it under the terms of the GNU General Public License as published by
6 | # the Free Software Foundation, either version 3 of the License, or
7 | # (at your option) any later version.
8 |
9 | # This program is distributed in the hope that it will be useful,
10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | # GNU General Public License for more details.
13 |
14 | # You should have received a copy of the GNU General Public License
15 | # along with this program. If not, see .
16 |
17 | import math
18 |
19 | from mathutils import Matrix, Vector
20 |
21 | from ..gtaLib import dff
22 |
23 | #######################################################
24 | class ext_2dfx_exporter:
25 |
26 | """ Helper class for 2dfx exporting """
27 |
28 | #######################################################
29 | def __init__(self, effects):
30 | self.effects = effects
31 |
32 | #######################################################
33 | def export_light(self, obj, use_local_position):
34 | if obj.type != 'LIGHT':
35 | return
36 |
37 | FL1, FL2 = dff.Light2dfx.Flags1, dff.Light2dfx.Flags2
38 | settings = obj.data.ext_2dfx
39 | loc = obj.location if use_local_position else obj.matrix_world.translation
40 |
41 | entry = dff.Light2dfx(loc)
42 | if settings.export_view_vector:
43 | entry.lookDirection = settings.view_vector
44 | entry.color = dff.RGBA._make(
45 | list(int(255 * x) for x in list(obj.data.color) + [settings.alpha])
46 | )
47 | entry.coronaFarClip = settings.corona_far_clip
48 | entry.pointlightRange = settings.point_light_range
49 | entry.coronaSize = settings.corona_size
50 | entry.shadowSize = settings.shadow_size
51 | entry.coronaShowMode = int(settings.corona_show_mode)
52 | entry.coronaEnableReflection = int(settings.corona_enable_reflection)
53 | entry.coronaFlareType = settings.corona_flare_type
54 | entry.shadowColorMultiplier = settings.shadow_color_multiplier
55 | entry.coronaTexName = settings.corona_tex_name
56 | entry.shadowTexName = settings.shadow_tex_name
57 | entry.shadowZDistance = settings.shadow_z_distance
58 |
59 | if settings.flag1_corona_check_obstacles:
60 | entry.set_flag(FL1.CORONA_CHECK_OBSTACLES.value)
61 |
62 | entry.set_flag(settings.flag1_fog_type << 1)
63 |
64 | if settings.flag1_without_corona:
65 | entry.set_flag(FL1.WITHOUT_CORONA.value)
66 |
67 | if settings.flag1_corona_only_at_long_distance:
68 | entry.set_flag(FL1.CORONA_ONLY_AT_LONG_DISTANCE.value)
69 |
70 | if settings.flag1_at_day:
71 | entry.set_flag(FL1.AT_DAY.value)
72 |
73 | if settings.flag1_at_night:
74 | entry.set_flag(FL1.AT_NIGHT.value)
75 |
76 | if settings.flag1_blinking1:
77 | entry.set_flag(FL1.BLINKING1.value)
78 |
79 | if settings.flag2_corona_only_from_below:
80 | entry.set_flag2(FL2.CORONA_ONLY_FROM_BELOW.value)
81 |
82 | if settings.flag2_blinking2:
83 | entry.set_flag2(FL2.BLINKING2.value)
84 |
85 | if settings.flag2_update_height_above_ground:
86 | entry.set_flag2(FL2.UPDATE_HEIGHT_ABOVE_GROUND.value)
87 |
88 | if settings.flag2_check_view_vector:
89 | entry.set_flag2(FL2.CHECK_DIRECTION.value)
90 |
91 | if settings.flag2_blinking3:
92 | entry.set_flag2(FL2.BLINKING3.value)
93 |
94 | return entry
95 |
96 | #######################################################
97 | def export_particle(self, obj, use_local_position):
98 | settings = obj.dff.ext_2dfx
99 | loc = obj.location if use_local_position else obj.matrix_world.translation
100 |
101 | entry = dff.Particle2dfx(loc)
102 | entry.effect = settings.val_str24_1
103 |
104 | return entry
105 |
106 | #######################################################
107 | def export_ped_attractor(self, obj, use_local_position):
108 | settings = obj.dff.ext_2dfx
109 | loc = obj.location if use_local_position else obj.matrix_world.translation
110 |
111 | entry = dff.PedAttractor2dfx(loc)
112 | entry.type = int(settings.ped_attractor_type)
113 | entry.queue_direction = settings.val_euler_1.to_matrix() @ Vector((0.0, 0.0, 1.0))
114 | entry.use_direction = obj.matrix_world.to_quaternion() @ Vector((0.0, 0.0, 1.0))
115 | entry.forward_direction = settings.val_euler_2.to_matrix() @ Vector((0.0, 0.0, 1.0))
116 | entry.external_script = settings.val_str8_1
117 | entry.ped_existing_probability = settings.val_chance_1
118 | entry.unk = settings.val_int_1
119 |
120 | return entry
121 |
122 | #######################################################
123 | def export_sun_glare(self, obj, use_local_position):
124 | loc = obj.location if use_local_position else obj.matrix_world.translation
125 |
126 | entry = dff.SunGlare2dfx(loc)
127 |
128 | return entry
129 |
130 | #######################################################
131 | def export_enter_exit(self, obj, use_local_position):
132 | settings = obj.dff.ext_2dfx
133 | loc = obj.location if use_local_position else obj.matrix_world.translation
134 |
135 | entry = dff.EnterExit2dfx(loc)
136 | entry.enter_angle = math.radians(settings.val_degree_1)
137 | entry.approximation_radius_x = settings.val_float_1
138 | entry.approximation_radius_y = settings.val_float_2
139 | entry.exit_location = settings.val_vector_1
140 | entry.exit_angle = settings.val_degree_2
141 | entry.interior = settings.val_short_1
142 | entry._flags1 = settings.val_byte_1
143 | entry.sky_color = settings.val_byte_2
144 | entry.interior_name = settings.val_str8_1
145 | entry.time_on = settings.val_hour_1
146 | entry.time_off = settings.val_hour_2
147 | entry._flags2 = settings.val_byte_3
148 | entry.unk = settings.val_byte_4
149 |
150 | return entry
151 |
152 | #######################################################
153 | def export_road_sign(self, obj, use_local_position):
154 | if obj.type != 'FONT':
155 | return
156 |
157 | lines = obj.data.body.split("\n")[:4]
158 | if not lines:
159 | return
160 |
161 | lines_num = len(lines)
162 | while len(lines) < 4:
163 | lines.append("_" * 16)
164 |
165 | max_chars_num = 2
166 | for i, line in enumerate(lines):
167 | if len(line) < 16:
168 | line += (16 - len(line)) * "_"
169 | line = line.replace(" ", "_")[:16]
170 | lines[i] = line
171 |
172 | line_chars_num = len(line.rstrip("_"))
173 | if max_chars_num < line_chars_num:
174 | max_chars_num = line_chars_num
175 |
176 | max_chars_num = next((i for i in (2, 4, 8, 16) if max_chars_num <= i), max_chars_num)
177 |
178 | settings = obj.data.ext_2dfx
179 |
180 | flags = {1:1, 2:2, 3:3, 4:0}[lines_num]
181 | flags |= {2:1, 4:2, 8:3, 16:0}[max_chars_num] << 2
182 | flags |= int(settings.color) << 4
183 |
184 | rotation = obj.matrix_world.to_euler('ZXY')
185 |
186 | entry = dff.RoadSign2dfx(obj.matrix_world.translation)
187 |
188 | entry.rotation = Vector((
189 | rotation.x * (180 / math.pi),
190 | rotation.y * (180 / math.pi),
191 | rotation.z * (180 / math.pi)
192 | ))
193 |
194 | entry.text1, entry.text2, \
195 | entry.text3, entry.text4 = lines
196 |
197 | entry.size = settings.size
198 | entry.flags = flags
199 |
200 | return entry
201 |
202 | #######################################################
203 | def export_trigger_point(self, obj, use_local_position):
204 | settings = obj.dff.ext_2dfx
205 | loc = obj.location if use_local_position else obj.matrix_world.translation
206 |
207 | entry = dff.TriggerPoint2dfx(loc)
208 | entry.point_id = settings.val_int_1
209 |
210 | return entry
211 |
212 | #######################################################
213 | def export_cover_point(self, obj, use_local_position):
214 | settings = obj.dff.ext_2dfx
215 | loc = obj.location if use_local_position else obj.matrix_world.translation
216 |
217 | entry = dff.CoverPoint2dfx(loc)
218 | entry.cover_type = settings.val_int_1
219 |
220 | direction = obj.matrix_world.to_quaternion() @ Vector((0.0, 1.0, 0.0))
221 | direction.z = 0
222 | direction.normalize()
223 |
224 | entry.direction_x = direction.x
225 | entry.direction_y = direction.y
226 |
227 | return entry
228 |
229 | #######################################################
230 | def export_escalator(self, obj, use_local_position):
231 | settings = obj.dff.ext_2dfx
232 | loc = obj.location if use_local_position else obj.matrix_world.translation
233 |
234 | matrix = obj.matrix_world.to_quaternion().to_matrix().to_4x4()
235 | for axis in range(3):
236 | matrix[axis][3] = loc[axis]
237 |
238 | bottom = (matrix @ Matrix.Translation(settings.val_vector_1)).to_translation()
239 | top = (matrix @ Matrix.Translation(settings.val_vector_2)).to_translation()
240 | end = (matrix @ Matrix.Translation(settings.val_vector_3)).to_translation()
241 |
242 | entry = dff.Escalator2dfx(loc)
243 | entry.bottom = tuple(bottom)
244 | entry.top = tuple(top)
245 | entry.end = tuple(end)
246 | entry.direction = int(settings.escalator_direction)
247 |
248 | return entry
249 |
250 | #######################################################
251 | def export_objects(self, objects, use_local_position=False):
252 |
253 | """ Export objects and fill 2dfx entries """
254 |
255 | functions = {
256 | 0: self.export_light,
257 | 1: self.export_particle,
258 | 3: self.export_ped_attractor,
259 | 4: self.export_sun_glare,
260 | 6: self.export_enter_exit,
261 | 7: self.export_road_sign,
262 | 8: self.export_trigger_point,
263 | 9: self.export_cover_point,
264 | 10: self.export_escalator,
265 | }
266 |
267 | ext_2dfx_objects = [obj for obj in objects if obj.dff.type == '2DFX']
268 | ext_2dfx_objects.sort(key=lambda obj: obj.name)
269 |
270 | for obj in ext_2dfx_objects:
271 | entry = functions[int(obj.dff.ext_2dfx.effect)](obj, use_local_position)
272 | if entry:
273 | self.effects.append_entry(entry)
274 |
--------------------------------------------------------------------------------
/gui/map_ot.py:
--------------------------------------------------------------------------------
1 | # GTA DragonFF - Blender scripts to edit basic GTA formats
2 | # Copyright (C) 2019 Parik
3 |
4 | # This program is free software: you can redistribute it and/or modify
5 | # it under the terms of the GNU General Public License as published by
6 | # the Free Software Foundation, either version 3 of the License, or
7 | # (at your option) any later version.
8 |
9 | # This program is distributed in the hope that it will be useful,
10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | # GNU General Public License for more details.
13 |
14 | # You should have received a copy of the GNU General Public License
15 | # along with this program. If not, see .
16 |
17 | import bpy
18 | import math
19 | import os
20 | import time
21 |
22 | from bpy_extras.io_utils import ImportHelper, ExportHelper
23 |
24 | from ..ops import map_importer, ipl_exporter
25 | from ..ops.cull_importer import cull_importer
26 | from ..ops.importer_common import link_object
27 |
28 | #######################################################
29 | class SCENE_OT_dff_import_map(bpy.types.Operator):
30 | """Tooltip"""
31 | bl_idname = "scene.dragonff_map_import"
32 | bl_label = "Import map section"
33 |
34 | _timer = None
35 | _updating = False
36 | _progress_current = 0
37 | _progress_total = 0
38 | _importer = None
39 |
40 | _inst_index = 0
41 | _inst_loaded = False
42 |
43 | _col_index = 0
44 | _col_loaded = True
45 |
46 | _cull_loaded = True
47 |
48 | #######################################################
49 | def modal(self, context, event):
50 |
51 | if event.type in {'ESC'}:
52 | self.cancel(context)
53 | return {'CANCELLED'}
54 |
55 | if event.type == 'TIMER' and not self._updating:
56 | self._updating = True
57 |
58 | importer = self._importer
59 |
60 | # Import CULL if there are any left to load
61 | if not self._cull_loaded:
62 |
63 | for cull in importer.cull_instances:
64 | importer.import_cull(context, cull)
65 |
66 | self._progress_current += 1
67 | self._cull_loaded = True
68 |
69 | # Import collision files if there are any left to load
70 | elif not self._col_loaded:
71 | num_objects_at_once = 5
72 | cols_num = len(importer.col_files)
73 |
74 | for _ in range(num_objects_at_once):
75 | if self._col_index >= cols_num:
76 | self._col_loaded = True
77 | break
78 |
79 | # Fetch next collision
80 | col_file = importer.col_files[self._col_index]
81 | self._col_index += 1
82 |
83 | importer.import_collision(context, col_file)
84 | self._progress_current += 1
85 |
86 | # Import objcets instances
87 | else:
88 | # As the number of objects increases, loading performance starts to get crushed by scene updates, so
89 | # we try to keep loading at least 5% of the total scene object count on each timer pulse.
90 | num_objects_at_once = max(10, int(0.05 * len(bpy.data.objects)))
91 | instances_num = len(importer.object_instances)
92 |
93 | for _ in range(num_objects_at_once):
94 | if self._inst_index >= instances_num:
95 | self._inst_loaded = True
96 | break
97 |
98 | # Fetch next instance
99 | inst = importer.object_instances[self._inst_index]
100 | self._inst_index += 1
101 |
102 | try:
103 | importer.import_object_instance(context, inst)
104 | except:
105 | print("Can`t import model... skipping")
106 |
107 | self._progress_current += 1
108 |
109 | # Update cursor progress indicator if something needs to be loaded
110 | progress = (
111 | float(self._progress_current) / float(self._progress_total)
112 | ) if self._progress_total else 100
113 |
114 | context.window_manager.progress_update(progress)
115 |
116 | # Update dependency graph
117 | dg = context.evaluated_depsgraph_get()
118 | dg.update()
119 |
120 | self._updating = False
121 |
122 | if self._inst_loaded:
123 | self.cancel(context)
124 | return {'FINISHED'}
125 |
126 | return {'PASS_THROUGH'}
127 |
128 | #######################################################
129 | def execute(self, context):
130 |
131 | settings = context.scene.dff
132 | self._importer = map_importer.load_map(settings)
133 |
134 | self._progress_current = 0
135 | self._progress_total = 0
136 |
137 | self._inst_index = 0
138 | self._inst_loaded = False
139 | self._progress_total += len(self._importer.object_instances)
140 |
141 | if self._importer.cull_instances:
142 | self._cull_loaded = False
143 | self._progress_total += 1
144 | else:
145 | self._cull_loaded = True
146 |
147 | if self._importer.col_files:
148 | self._col_index = 0
149 | self._col_loaded = False
150 | self._progress_total += len(self._importer.col_files)
151 | else:
152 | self._col_loaded = True
153 |
154 | wm = context.window_manager
155 | wm.progress_begin(0, 100.0)
156 |
157 | # Call the "modal" function every 0.1s
158 | self._timer = wm.event_timer_add(0.1, window=context.window)
159 | wm.modal_handler_add(self)
160 |
161 | return {'RUNNING_MODAL'}
162 |
163 | #######################################################
164 | def cancel(self, context):
165 | wm = context.window_manager
166 | wm.progress_end()
167 | wm.event_timer_remove(self._timer)
168 |
169 | #######################################################
170 | class SCENE_OT_ipl_select(bpy.types.Operator, ImportHelper):
171 |
172 | bl_idname = "scene.select_ipl"
173 | bl_label = "Select IPL File"
174 |
175 | filename_ext = ".ipl"
176 |
177 | filter_glob : bpy.props.StringProperty(
178 | default="*.ipl",
179 | options={'HIDDEN'})
180 |
181 | def invoke(self, context, event):
182 | self.filepath = context.scene.dff.game_root + "/data/maps/"
183 | context.window_manager.fileselect_add(self)
184 | return {'RUNNING_MODAL'}
185 |
186 | def execute(self, context):
187 | if os.path.splitext(self.filepath)[-1].lower() == self.filename_ext:
188 | filepath = os.path.normpath(self.filepath)
189 | # Try to find if the file is within the game directory structure
190 | sep_pos = filepath.upper().find(f"data{os.sep}maps")
191 |
192 | if sep_pos != -1:
193 | # File is within game directory, use relative path
194 | game_root = filepath[:sep_pos]
195 | context.scene.dff.game_root = game_root
196 | context.scene.dff.custom_ipl_path = os.path.relpath(filepath, game_root)
197 | else:
198 | # File is outside game directory, use absolute path
199 | # Don't change game_root, keep the existing one
200 | context.scene.dff.custom_ipl_path = filepath
201 | return {'FINISHED'}
202 |
203 | #######################################################
204 | class EXPORT_OT_ipl_cull(bpy.types.Operator, ExportHelper):
205 |
206 | bl_idname = "export_scene.dff_ipl_cull"
207 | bl_description = "Export a GTA CULL IPL File"
208 | bl_label = "DragonFF CULL (.ipl)"
209 | filename_ext = ".ipl"
210 |
211 | filepath : bpy.props.StringProperty(name="File path",
212 | maxlen=1024,
213 | default="",
214 | subtype='FILE_PATH')
215 |
216 | filter_glob : bpy.props.StringProperty(default="*.ipl",
217 | options={'HIDDEN'})
218 |
219 | only_selected : bpy.props.BoolProperty(
220 | name = "Only Selected",
221 | default = False
222 | )
223 |
224 | #######################################################
225 | def draw(self, context):
226 | layout = self.layout
227 |
228 | layout.prop(self, "only_selected")
229 | layout.prop(context.scene.dff, "game_version_dropdown", text="Game")
230 |
231 | #######################################################
232 | def execute(self, context):
233 |
234 | start = time.time()
235 | try:
236 | ipl_exporter.export_ipl(
237 | {
238 | "file_name" : self.filepath,
239 | "only_selected" : self.only_selected,
240 | "game_id" : context.scene.dff.game_version_dropdown,
241 | "export_inst" : False,
242 | "export_cull" : True,
243 | }
244 | )
245 |
246 | if not ipl_exporter.ipl_exporter.total_objects_num:
247 | report = "No objects with IPL data found"
248 | self.report({"ERROR"}, report)
249 | return {'CANCELLED'}, report
250 |
251 | self.report({"INFO"}, f"Finished export in {time.time() - start:.2f}s")
252 |
253 | except Exception as e:
254 | self.report({"ERROR"}, str(e))
255 |
256 | return {'FINISHED'}
257 |
258 | #######################################################
259 | class OBJECT_OT_dff_add_cull(bpy.types.Operator):
260 |
261 | bl_idname = "object.dff_add_cull"
262 | bl_label = "Add CULL Zone"
263 | bl_description = "Add CULL zone to the scene"
264 | bl_options = {'REGISTER', 'UNDO'}
265 |
266 | location: bpy.props.FloatVectorProperty(
267 | name="Location",
268 | description="Location for the newly added object",
269 | subtype='XYZ',
270 | default=(0, 0, 0)
271 | )
272 |
273 | scale: bpy.props.FloatVectorProperty(
274 | name="Scale",
275 | description="Scale for the newly added object",
276 | subtype='XYZ',
277 | default=(1, 1, 1)
278 | )
279 |
280 | angle: bpy.props.FloatProperty(
281 | name="Angle",
282 | description="Angle along the Z axis",
283 | subtype='ANGLE',
284 | min=-math.pi * 2,
285 | max=math.pi * 2,
286 | step=100,
287 | default=0
288 | )
289 |
290 | #######################################################
291 | def invoke(self, context, event):
292 | self.location = context.scene.cursor.location
293 | return self.execute(context)
294 |
295 | #######################################################
296 | def execute(self, context):
297 | obj = cull_importer.create_cull_object(
298 | location=self.location,
299 | scale=self.scale,
300 | flags=0,
301 | angle=self.angle
302 | )
303 | link_object(obj, context.collection)
304 |
305 | context.view_layer.objects.active = obj
306 | for o in context.selected_objects:
307 | o.select_set(False)
308 | obj.select_set(True)
309 |
310 | return {'FINISHED'}
311 |
--------------------------------------------------------------------------------
/gtaLib/pyffi/utils/trianglemesh.py:
--------------------------------------------------------------------------------
1 | # ***** BEGIN LICENSE BLOCK *****
2 | #
3 | # Copyright (c) 2007-2012, Python File Format Interface
4 | # All rights reserved.
5 | #
6 | # Redistribution and use in source and binary forms, with or without
7 | # modification, are permitted provided that the following conditions
8 | # are met:
9 | #
10 | # * Redistributions of source code must retain the above copyright
11 | # notice, this list of conditions and the following disclaimer.
12 | #
13 | # * Redistributions in binary form must reproduce the above
14 | # copyright notice, this list of conditions and the following
15 | # disclaimer in the documentation and/or other materials provided
16 | # with the distribution.
17 | #
18 | # * Neither the name of the Python File Format Interface
19 | # project nor the names of its contributors may be used to endorse
20 | # or promote products derived from this software without specific
21 | # prior written permission.
22 | #
23 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
24 | # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
25 | # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
26 | # FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
27 | # COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
28 | # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
29 | # BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
30 | # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
31 | # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
32 | # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
33 | # ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
34 | # POSSIBILITY OF SUCH DAMAGE.
35 | #
36 | # ***** END LICENSE BLOCK *****
37 |
38 | # modified from:
39 |
40 | # http://techgame.net/projects/Runeblade/browser/trunk/RBRapier/RBRapier/Tools/Geometry/Analysis/TriangleMesh.py?rev=760
41 |
42 | # original license:
43 |
44 | # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
45 | # ~ License
46 | # ~
47 | # - The RuneBlade Foundation library is intended to ease some
48 | # - aspects of writing intricate Jabber, XML, and User Interface (wxPython, etc.)
49 | # - applications, while providing the flexibility to modularly change the
50 | # - architecture. Enjoy.
51 | # ~
52 | # ~ Copyright (C) 2002 TechGame Networks, LLC.
53 | # ~
54 | # ~ This library is free software; you can redistribute it and/or
55 | # ~ modify it under the terms of the BSD style License as found in the
56 | # ~ LICENSE file included with this distribution.
57 | # ~
58 | # ~ TechGame Networks, LLC can be reached at:
59 | # ~ 3578 E. Hartsel Drive #211
60 | # ~ Colorado Springs, Colorado, USA, 80920
61 | # ~
62 | # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
63 |
64 | # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
65 | # ~ Imports
66 | # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
67 |
68 | import operator # itemgetter
69 | from weakref import WeakSet
70 |
71 |
72 | class Edge:
73 | """A directed edge which keeps track of its faces."""
74 |
75 | def __init__(self, ev0, ev1):
76 | """Edge constructor.
77 |
78 | >>> edge = Edge(6, 9)
79 | >>> edge.verts
80 | (6, 9)
81 | """
82 |
83 | if ev0 == ev1:
84 | raise ValueError("Degenerate edge.")
85 |
86 | self.verts = (ev0, ev1)
87 | """Vertices of the edge."""
88 |
89 | self.faces = WeakSet()
90 | """Weak set of faces that have this edge."""
91 |
92 | def __repr__(self):
93 | """String representation.
94 |
95 | >>> Edge(1, 2)
96 | Edge(1, 2)
97 | """
98 | return "Edge(%s, %s)" % self.verts
99 |
100 | class Face:
101 | """An oriented face which keeps track its adjacent faces."""
102 |
103 | def __init__(self, v0, v1, v2):
104 | """Construct face from vertices.
105 |
106 | >>> face = Face(3, 7, 5)
107 | >>> face.verts
108 | (3, 7, 5)
109 | >>> Face(30, 0, 30) # doctest: +ELLIPSIS
110 | Traceback (most recent call last):
111 | ...
112 | ValueError: ...
113 | """
114 | if v0 == v1 or v1 == v2 or v2 == v0:
115 | raise ValueError("Degenerate face.")
116 | if v0 < v1 and v0 < v2:
117 | self.verts = (v0, v1, v2)
118 | if v1 < v0 and v1 < v2:
119 | self.verts = (v1, v2, v0)
120 | if v2 < v0 and v2 < v1:
121 | self.verts = (v2, v0, v1)
122 | # no index yet
123 | self.index = None
124 |
125 | self.adjacent_faces = (WeakSet(), WeakSet(), WeakSet())
126 | """Weak sets of adjacent faces along edge opposite each vertex."""
127 |
128 | def __repr__(self):
129 | """String representation.
130 |
131 | >>> Face(3, 1, 2)
132 | Face(1, 2, 3)
133 | """
134 | return "Face(%s, %s, %s)" % self.verts
135 |
136 | def __eq__(self, other):
137 | """
138 | :param other:
139 | :return:
140 | """
141 | return (self.verts[0] == other.verts[0]) & (self.verts[1] == self.verts[1]) & (self.verts[2] == self.verts[2])
142 |
143 | def __hash__(self):
144 | return self.verts[0] + self.verts[1] + self.verts[2]
145 |
146 | def get_next_vertex(self, vi):
147 | """Get next vertex of face.
148 |
149 | >>> face = Face(8, 7, 5)
150 | >>> face.get_next_vertex(8)
151 | 7
152 | """
153 | # XXX using list(self.verts) instead of self.verts
154 | # XXX for Python 2.5 compatibility
155 | return self.verts[(1, 2, 0)[list(self.verts).index(vi)]]
156 |
157 | def get_adjacent_faces(self, vi):
158 | """Get adjacent faces associated with the edge opposite a vertex."""
159 | # XXX using list(self.verts) instead of self.verts
160 | # XXX for Python 2.5 compatibility
161 | return self.adjacent_faces[list(self.verts).index(vi)]
162 |
163 |
164 | class Mesh:
165 | """A mesh of interconnected faces.
166 |
167 | :ivar faces: List of faces of the mesh.
168 | :type faces: ``list`` of :class:`Face`"""
169 | def __init__(self, faces=None, lock=True):
170 | """Initialize a mesh, and optionally assign its faces and lock.
171 |
172 | :param faces: ``None``, or an iterator over faces to assign to
173 | the mesh.
174 | :type faces: ``Iterable`` or ``type(None)``
175 | :param lock: Whether to lock the mesh or not (ignored when
176 | `faces` are not specified).
177 | :type lock: ``bool``
178 | """
179 | self._faces = {}
180 | """Dictionary of all faces."""
181 |
182 | self._edges = {}
183 | """Dictionary of all edges."""
184 |
185 | if faces is not None:
186 | for v0, v1, v2 in faces:
187 | self.add_face(v0, v1, v2)
188 | if lock:
189 | self.lock()
190 |
191 | def __repr__(self):
192 | """String representation. Examples:
193 |
194 | >>> m = Mesh()
195 | >>> m
196 | Mesh()
197 | >>> tmp = m.add_face(1, 2, 3)
198 | >>> tmp = m.add_face(3, 2, 4)
199 | >>> m
200 | Mesh(faces=[(1, 2, 3), (2, 4, 3)], lock=False)
201 | >>> m.lock()
202 | >>> m
203 | Mesh(faces=[(1, 2, 3), (2, 4, 3)])
204 | >>> Mesh(faces=[(1, 2, 3),(3, 2, 4)])
205 | Mesh(faces=[(1, 2, 3), (2, 4, 3)])
206 | """
207 | try:
208 | self.faces
209 | except AttributeError:
210 | # unlocked
211 | if not self._faces:
212 | # special case
213 | return "Mesh()"
214 | return ("Mesh(faces=[%s], lock=False)"
215 | % ', '.join(repr(faceverts)
216 | for faceverts in sorted(self._faces)))
217 | else:
218 | # locked
219 | return ("Mesh(faces=[%s])"
220 | % ', '.join(repr(face.verts)
221 | for face in self.faces))
222 |
223 | def _add_edge(self, face, pv0, pv1):
224 | """Create new edge for mesh for given face, or return existing
225 | edge. Lists of faces of the new/existing edge is also updated,
226 | as well as lists of adjacent faces. For internal use only,
227 | called on each edge of the face in add_face.
228 | """
229 | # create edge if not found
230 | try:
231 | edge = self._edges[(pv0, pv1)]
232 | except KeyError:
233 | # create edge
234 | edge = Edge(pv0, pv1)
235 | self._edges[(pv0, pv1)] = edge
236 |
237 | # update edge's faces
238 | edge.faces.add(face)
239 |
240 | # find reverse edge in mesh
241 | try:
242 | otheredge = self._edges[(pv1, pv0)]
243 | except KeyError:
244 | pass
245 | else:
246 | # update adjacent faces
247 | pv2 = face.get_next_vertex(pv1)
248 | for otherface in otheredge.faces:
249 | otherpv2 = otherface.get_next_vertex(pv0)
250 | face.get_adjacent_faces(pv2).add(otherface)
251 | otherface.get_adjacent_faces(otherpv2).add(face)
252 |
253 | def add_face(self, v0, v1, v2):
254 | """Create new face for mesh, or return existing face. List of
255 | adjacent faces is also updated.
256 |
257 | >>> m = Mesh()
258 | >>> f0 = m.add_face(0, 1, 2)
259 | >>> [list(faces) for faces in f0.adjacent_faces]
260 | [[], [], []]
261 |
262 | >>> m = Mesh()
263 | >>> f0 = m.add_face(0, 1, 2)
264 | >>> f1 = m.add_face(2, 1, 3)
265 | >>> f2 = m.add_face(2, 3, 4)
266 | >>> len(m._faces)
267 | 3
268 | >>> len(m._edges)
269 | 9
270 |
271 |
272 | """
273 | face = Face(v0, v1, v2)
274 | try:
275 | face = self._faces[face.verts]
276 | except KeyError:
277 | # create edges and update links between faces
278 | self._add_edge(face, v0, v1)
279 | self._add_edge(face, v1, v2)
280 | self._add_edge(face, v2, v0)
281 | # register face in mesh
282 | self._faces[face.verts] = face
283 |
284 | return face
285 |
286 | def lock(self):
287 | """Lock the mesh. Frees memory by clearing the structures
288 | which are only used to update the face adjacency lists. Sets
289 | the faces attribute to the sorted list of all faces (sorting helps
290 | with ensuring that the strips in faces are close together).
291 |
292 | >>> m = Mesh()
293 | >>> f0 = m.add_face(3, 1, 2)
294 | >>> f1 = m.add_face(0, 1, 2)
295 | >>> m.faces # doctest: +ELLIPSIS
296 | Traceback (most recent call last):
297 | ...
298 | AttributeError: ...
299 | >>> m.lock()
300 | >>> m.faces # should be sorted
301 | [Face(0, 1, 2), Face(1, 2, 3)]
302 | >>> m.faces[0].index
303 | 0
304 | >>> m.faces[1].index
305 | 1
306 | """
307 | # store faces and set their index
308 | self.faces = []
309 | for i, (verts, face) in enumerate(sorted(iter(self._faces.items()),
310 | key=operator.itemgetter(0))):
311 | face.index = i
312 | self.faces.append(face)
313 | # remove helper structures
314 | del self._faces
315 | del self._edges
316 |
317 | def discard_face(self, face):
318 | """Remove the face from the mesh.
319 |
320 | >>> m = Mesh()
321 | >>> f0 = m.add_face(0, 1, 2)
322 | >>> f1 = m.add_face(1, 3, 2)
323 | >>> f2 = m.add_face(2, 3, 4)
324 | >>> m.lock()
325 | >>> list(f0.get_adjacent_faces(0))
326 | [Face(1, 3, 2)]
327 | >>> m.discard_face(f1)
328 | >>> list(f0.get_adjacent_faces(0))
329 | []
330 | """
331 | # note: don't delete, but set to None, to ensure that other
332 | # face indices remain valid
333 | self.faces[face.index] = None
334 | for adj_faces in face.adjacent_faces:
335 | for adj_face in adj_faces:
336 | for adj_adj_faces in adj_face.adjacent_faces:
337 | adj_adj_faces.discard(face)
338 | # faster (but breaks py3k!!):
339 | #if id(face) in adj_adj_faces.data:
340 | # del adj_adj_faces.data[id(face)]
341 |
342 | if __name__ == '__main__':
343 | import doctest
344 | doctest.testmod()
345 |
--------------------------------------------------------------------------------
/ops/col_exporter.py:
--------------------------------------------------------------------------------
1 | # GTA DragonFF - Blender scripts to edit basic GTA formats
2 | # Copyright (C) 2019 Parik
3 |
4 | # This program is free software: you can redistribute it and/or modify
5 | # it under the terms of the GNU General Public License as published by
6 | # the Free Software Foundation, either version 3 of the License, or
7 | # (at your option) any later version.
8 |
9 | # This program is distributed in the hope that it will be useful,
10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | # GNU General Public License for more details.
13 |
14 | # You should have received a copy of the GNU General Public License
15 | # along with this program. If not, see .
16 |
17 | import bpy
18 | import bmesh
19 | import os
20 | import math
21 | import mathutils
22 |
23 | from .importer_common import create_bmesh_for_mesh
24 | from ..gtaLib import col
25 |
26 | class col_exporter:
27 |
28 | coll = None
29 | filename = "" # Whether it will return a bytes file (not write to a file), if no file name is specified
30 | version = None
31 | apply_transformations = True
32 | only_selected = False
33 |
34 | #######################################################
35 | def _process_mesh(obj, verts, faces, face_groups=None):
36 | self = col_exporter
37 |
38 | mesh = obj.data
39 | bm = create_bmesh_for_mesh(mesh, obj.mode)
40 |
41 | if self.apply_transformations:
42 | matrix = obj.matrix_world
43 | else:
44 | matrix = mathutils.Matrix.Identity(4)
45 | matrix[0][0], matrix[1][1], matrix[2][2] = obj.scale
46 |
47 | bm = bm.copy()
48 | bm.transform(matrix)
49 |
50 | bmesh.ops.triangulate(bm, faces=bm.faces[:])
51 |
52 | vert_offset = len(verts)
53 |
54 | # Vertices
55 | for vert in bm.verts:
56 | verts.append((*vert.co,))
57 |
58 | # Setup for Face Groups
59 | layer = bm.faces.layers.int.get("face group")
60 | start_idx = fg_idx = 0
61 | fg_min = [256] * 3
62 | fg_max = [-256] * 3
63 |
64 | for i, face in enumerate(bm.faces):
65 |
66 | # Face Groups
67 | if layer and col.Sections.version > 1:
68 | lastface = i == len(bm.faces)-1
69 | idx = face[layer]
70 |
71 | # Evaluate bounds if still the same face group index or this is the last face in the list
72 | if idx == fg_idx or lastface:
73 | fg_min = [min(x, y) for x, y in zip(fg_min, face.verts[0].co)]
74 | fg_max = [max(x, y) for x, y in zip(fg_max, face.verts[0].co)]
75 | fg_min = [min(x, y) for x, y in zip(fg_min, face.verts[1].co)]
76 | fg_max = [max(x, y) for x, y in zip(fg_max, face.verts[1].co)]
77 | fg_min = [min(x, y) for x, y in zip(fg_min, face.verts[2].co)]
78 | fg_max = [max(x, y) for x, y in zip(fg_max, face.verts[2].co)]
79 |
80 | # Create the face group if the face group index changed or this is the last face in the list
81 | if idx != fg_idx or lastface:
82 | end_idx = i if lastface else i-1
83 | face_groups.append(col.TFaceGroup._make([fg_min, fg_max, start_idx, end_idx]))
84 | fg_min = [256] * 3
85 | fg_max = [-256] * 3
86 | start_idx = i
87 | fg_idx = idx
88 |
89 | bm.verts.index_update()
90 | surface = [0, 0, 0, 0]
91 | try:
92 | mat = obj.data.materials[face.material_index]
93 | surface[0] = mat.dff.col_mat_index
94 | surface[1] = mat.dff.col_flags
95 | surface[2] = mat.dff.col_brightness
96 | surface[3] = mat.dff.col_day_light | (mat.dff.col_night_light << 4)
97 |
98 | except (IndexError, AttributeError):
99 | pass
100 |
101 | if col.Sections.version == 1:
102 | faces.append(col.TFace._make(
103 | [vert.index + vert_offset for vert in (face.verts[0], face.verts[2], face.verts[1])] + [
104 | col.TSurface(*surface)
105 | ]
106 | ))
107 |
108 | else:
109 | faces.append(col.TFace._make(
110 | [vert.index + vert_offset for vert in (face.verts[0], face.verts[2], face.verts[1])] + [
111 | surface[0], surface[3]
112 | ]
113 | ))
114 |
115 | #######################################################
116 | def _convert_bounds():
117 | self = col_exporter
118 |
119 | radius = 0.0
120 | center = [0, 0, 0]
121 | rect_min = [0, 0, 0]
122 | rect_max = [0, 0, 0]
123 |
124 | if self.coll.bounds is not None:
125 | rect_max, rect_min = self.coll.bounds
126 | center = [(x + y) / 2 for x, y in zip(*self.coll.bounds)]
127 | radius = (
128 | mathutils.Vector(rect_max) - mathutils.Vector(rect_min)
129 | ).magnitude / 2
130 |
131 | self.coll.bounds = col.TBounds(max = col.TVector(*rect_max),
132 | min = col.TVector(*rect_min),
133 | center = col.TVector(*center),
134 | radius = radius
135 | )
136 |
137 | #######################################################
138 | def _process_spheres(obj):
139 | self = col_exporter
140 |
141 | radius = max(x * obj.empty_display_size for x in obj.scale)
142 | centre = col.TVector(*obj.location)
143 | surface = col.TSurface(
144 | obj.dff.col_material,
145 | obj.dff.col_flags,
146 | obj.dff.col_brightness,
147 | obj.dff.col_day_light | (obj.dff.col_night_light << 4)
148 | )
149 |
150 | self.coll.spheres.append(col.TSphere(radius=radius,
151 | surface=surface,
152 | center=centre
153 | ))
154 |
155 | pass
156 |
157 | #######################################################
158 | def _process_boxes(obj):
159 | self = col_exporter
160 |
161 | min = col.TVector(*(obj.location - obj.scale))
162 | max = col.TVector(*(obj.location + obj.scale))
163 |
164 | surface = col.TSurface(
165 | obj.dff.col_material,
166 | obj.dff.col_flags,
167 | obj.dff.col_brightness,
168 | obj.dff.col_day_light | (obj.dff.col_night_light << 4)
169 | )
170 |
171 | self.coll.boxes.append(col.TBox(min=min,
172 | max=max,
173 | surface=surface,
174 | ))
175 |
176 | pass
177 |
178 | #######################################################
179 | def _process_obj(obj):
180 | self = col_exporter
181 |
182 | if obj.type == 'MESH':
183 | # Meshes
184 | if obj.dff.type == 'SHA':
185 | self._process_mesh(obj,
186 | self.coll.shadow_verts,
187 | self.coll.shadow_faces
188 | )
189 |
190 | else:
191 | self._process_mesh(obj,
192 | self.coll.mesh_verts,
193 | self.coll.mesh_faces,
194 | self.coll.face_groups
195 | )
196 |
197 | elif obj.type == 'EMPTY':
198 | if obj.empty_display_type == 'SPHERE':
199 | self._process_spheres(obj)
200 | else:
201 | self._process_boxes(obj)
202 |
203 | #######################################################
204 | def export_col(collection, name):
205 | self = col_exporter
206 |
207 | col.Sections.init_sections(self.version)
208 |
209 | self.coll = col.ColModel()
210 | self.coll.version = self.version
211 | self.coll.model_name = os.path.basename(name)
212 |
213 | total_objects = 0
214 | bounds_objects = []
215 | for obj in collection.objects:
216 | if self.only_selected and not obj.select_get():
217 | continue
218 |
219 | if obj.dff.type == 'COL' or obj.dff.type == 'SHA':
220 | self._process_obj(obj)
221 | total_objects += 1
222 |
223 | if obj.dff.type == 'COL':
224 | bounds_objects.append(obj)
225 |
226 | if total_objects == 0 and (col_exporter.only_selected or collection.dff.auto_bounds):
227 | return b''
228 |
229 | # Get native bounds from collection (some collisions come in as just bounds with no other items)
230 | if collection.dff.auto_bounds:
231 | self.coll.bounds = calculate_bounds(bounds_objects, self.apply_transformations)
232 | else:
233 | self.coll.bounds = [collection.dff.bounds_max, collection.dff.bounds_min]
234 |
235 | self._convert_bounds()
236 |
237 | return col.coll(self.coll).write_memory()
238 |
239 | #######################################################
240 | def get_col_collection_name(collection, parent_collection=None):
241 | name = collection.name
242 |
243 | # Strip stuff like vehicles.col. from the name so that
244 | # for example vehicles.col.infernus changes to just infernus
245 | if parent_collection and parent_collection != collection:
246 | prefix = parent_collection.name + "."
247 | if name.startswith(prefix):
248 | name = name[len(prefix):]
249 |
250 | return name
251 |
252 | #######################################################
253 | def calculate_bounds(objects, apply_transformations=True):
254 | if not objects:
255 | return [[0, 0, 0], [0, 0, 0]]
256 |
257 | bounds = [
258 | [-math.inf] * 3,
259 | [math.inf] * 3
260 | ]
261 |
262 | for obj in objects:
263 | dimensions = obj.dimensions
264 | center = obj.location
265 |
266 | # Empties don't have a dimensions array
267 | if obj.type == 'EMPTY':
268 |
269 | if obj.empty_display_type == 'SPHERE':
270 | # Multiplied by 2 because empty_display_size is a radius
271 | dimensions = [
272 | max(x * obj.empty_display_size * 2 for x in obj.scale)] * 3
273 | elif obj.empty_display_type == 'CUBE':
274 | dimensions = obj.scale * 2
275 | else:
276 | dimensions = obj.scale
277 |
278 | # And Meshes require their proper center to be calculated because their transform is identity
279 | else:
280 | local_center = sum((mathutils.Vector(b) for b in obj.bound_box), mathutils.Vector()) / 8.0
281 | if apply_transformations:
282 | center = obj.matrix_world @ local_center
283 | else:
284 | center = local_center
285 |
286 | upper_bounds = [x + (y/2) for x, y in zip(center, dimensions)]
287 | lower_bounds = [x - (y/2) for x, y in zip(center, dimensions)]
288 |
289 | bounds = [
290 | [max(x, y) for x,y in zip(bounds[0], upper_bounds)],
291 | [min(x, y) for x,y in zip(bounds[1], lower_bounds)]
292 | ]
293 |
294 | return bounds
295 |
296 | #######################################################
297 | def export_col(options):
298 |
299 | col_exporter.version = options['version']
300 | col_exporter.collection = options['collection']
301 | col_exporter.apply_transformations = options['apply_transformations']
302 | col_exporter.only_selected = options['only_selected']
303 |
304 | file_name = options['file_name']
305 | output = b''
306 |
307 | if not col_exporter.collection:
308 | if col_exporter.only_selected:
309 | root_collections = {c for obj in bpy.context.selected_objects for c in obj.users_collection}
310 | else:
311 | scene_collection = bpy.context.scene.collection
312 | root_collections = scene_collection.children.values() + [scene_collection]
313 | else:
314 | root_collections = [col_exporter.collection]
315 |
316 | exported_collections = []
317 | for root_collection in root_collections:
318 | if root_collection.dff.type == 'NON':
319 | continue
320 |
321 | for c in root_collection.children.values() + [root_collection]:
322 | if c.dff.type == 'NON':
323 | continue
324 |
325 | if c not in exported_collections:
326 | name = get_col_collection_name(c, root_collection)
327 | output += col_exporter.export_col(c, name)
328 | exported_collections.append(c)
329 |
330 | if file_name:
331 | with open(file_name, mode='wb') as file:
332 | file.write(output)
333 | return
334 |
335 | return output
336 |
--------------------------------------------------------------------------------
/ops/importer_common.py:
--------------------------------------------------------------------------------
1 | # GTA DragonFF - Blender scripts to edit basic GTA formats
2 | # Copyright (C) 2019 Parik
3 |
4 | # This program is free software: you can redistribute it and/or modify
5 | # it under the terms of the GNU General Public License as published by
6 | # the Free Software Foundation, either version 3 of the License, or
7 | # (at your option) any later version.
8 |
9 | # This program is distributed in the hope that it will be useful,
10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | # GNU General Public License for more details.
13 |
14 | # You should have received a copy of the GNU General Public License
15 | # along with this program. If not, see .
16 |
17 | import bpy
18 | import bmesh
19 |
20 | from ..gtaLib.dff import strlen
21 | from ..gtaLib.data import presets
22 |
23 | #######################################################
24 | def set_object_mode(obj, mode):
25 | bpy.context.view_layer.objects.active = obj
26 | bpy.ops.object.mode_set(mode=mode, toggle=False)
27 |
28 | #######################################################
29 | def link_object(obj, collection):
30 | collection.objects.link(obj)
31 |
32 | #######################################################
33 | def create_collection(name, link=True):
34 | collection = bpy.data.collections.new(name)
35 | if link:
36 | bpy.context.scene.collection.children.link(collection)
37 |
38 | return collection
39 |
40 | #######################################################
41 | def hide_object(object, hide=True):
42 | object.hide_set(hide)
43 |
44 | #######################################################
45 | def create_bmesh_for_mesh(mesh, obj_mode):
46 | if obj_mode == "EDIT":
47 | bm = bmesh.from_edit_mesh(mesh)
48 | else:
49 | bm = bmesh.new()
50 | bm.from_mesh(mesh)
51 | return bm
52 |
53 | #######################################################
54 | def invert_matrix_safe(matrix):
55 | if abs(matrix.determinant()) > 1e-8:
56 | matrix.invert()
57 | else:
58 | matrix.identity()
59 |
60 | #######################################################
61 | def redraw_viewport():
62 | for area in bpy.context.window.screen.areas:
63 | if area.type == 'VIEW_3D':
64 | area.tag_redraw()
65 |
66 | #######################################################
67 | class material_helper:
68 |
69 | """ Material Helper for Blender 2.7x and Blender 2.8 compatibility"""
70 |
71 | #######################################################
72 | def set_base_color(self, color):
73 |
74 | if self.principled:
75 | self.principled.base_color = [i / 255 for i in color[:3]]
76 |
77 | # Set Alpha
78 | node = self.principled.node_principled_bsdf.inputs["Base Color"]
79 | node.default_value[3] = color[3] / 255
80 |
81 | self.material.diffuse_color = [i / 255 for i in color]
82 |
83 | else:
84 | self.material.diffuse_color = [i / 255 for i in color[:3]]
85 | self.material.alpha = color[3] / 255
86 |
87 | # Set preset material colours
88 | color_key = tuple(color)
89 | if color_key in presets.material_colours:
90 | colours = list(presets.material_colours)
91 | self.material.dff["preset_mat_cols"] = colours.index(color_key)
92 |
93 | #######################################################
94 | def set_texture(self, image, label="", filters=0, uv_addressing=0):
95 |
96 | if self.principled:
97 | self.principled.base_color_texture.node_image.label = label
98 | self.principled.base_color_texture.image = image
99 |
100 |
101 | # Connect Alpha output to Principled BSDF
102 | image_node = self.principled.base_color_texture.node_image
103 | principled_node = self.principled.node_principled_bsdf
104 | node_tree = self.principled.material.node_tree
105 |
106 | node_tree.links.new(image_node.outputs["Alpha"],
107 | principled_node.inputs["Alpha"])
108 |
109 | else:
110 | slot = self.material.texture_slots.add()
111 | slot.texture = bpy.data.textures.new(
112 | name = label,
113 | type = "IMAGE"
114 | )
115 | slot.texture.image = image
116 |
117 | self.material.dff.tex_filters = str(filters)
118 | self.material.dff.tex_u_addr = str((uv_addressing >> 4) & 0xF)
119 | self.material.dff.tex_v_addr = str(uv_addressing & 0xF)
120 |
121 | #######################################################
122 | def set_surface_properties(self, props):
123 |
124 | if self.principled:
125 | self.principled.specular = props.specular
126 | self.principled.roughness = props.diffuse
127 | self.material.dff.ambient = props.ambient
128 |
129 | else:
130 | self.material.diffuse_intensity = props.diffuse
131 | self.material.specular_intensity = props.specular
132 | self.material.ambient = props.ambient
133 |
134 | #######################################################
135 | def set_normal_map(self, image, label, intensity):
136 |
137 | if self.principled:
138 | self.principled.node_normalmap_get()
139 |
140 | self.principled.normalmap_texture.image = image
141 | self.principled.node_normalmap.label = label
142 | self.principled.normalmap_strength = intensity
143 |
144 | else:
145 | slot = self.material.texture_slots.add()
146 | slot.texture = bpy.data.textures.new(
147 | name = label,
148 | type = "IMAGE"
149 | )
150 |
151 | slot.texture.image = image
152 | slot.texture.use_normal_map = True
153 | slot.use_map_color_diffuse = False
154 | slot.use_map_normal = True
155 | slot.normal_factor = intensity
156 | pass
157 |
158 | #######################################################
159 | def set_environment_map(self, plugin):
160 |
161 | if plugin.env_map:
162 | self.material.dff.env_map_tex = plugin.env_map.name
163 |
164 | self.material.dff.export_env_map = True
165 | self.material.dff.env_map_coef = plugin.coefficient
166 | self.material.dff.env_map_fb_alpha = plugin.use_fb_alpha
167 |
168 | #######################################################
169 | def set_specular_material(self, plugin):
170 |
171 | self.material.dff.export_specular = True
172 | self.material.dff.specular_level = plugin.level
173 | self.material.dff.specular_texture = plugin.texture[:strlen(plugin.texture)].decode('ascii')
174 |
175 | # Set preset specular level
176 | level_key = round(plugin.level, 2)
177 | if level_key in presets.material_specular_levels:
178 | levels = list(presets.material_specular_levels)
179 | self.material.dff["preset_specular_levels"] = levels.index(level_key)
180 |
181 | #######################################################
182 | def set_reflection_material(self, plugin):
183 |
184 | self.material.dff.export_reflection = True
185 |
186 | self.material.dff.reflection_scale_x = plugin.s_x
187 | self.material.dff.reflection_scale_y = plugin.s_y
188 |
189 | self.material.dff.reflection_offset_y = plugin.o_y
190 | self.material.dff.reflection_offset_x = plugin.o_x
191 |
192 | self.material.dff.reflection_intensity = plugin.intensity
193 |
194 | # Set preset reflection intensities
195 | intensity_key = round(plugin.intensity, 2)
196 | if intensity_key in presets.material_reflection_intensities:
197 | intensities = list(presets.material_reflection_intensities)
198 | self.material.dff["preset_reflection_intensities"] = intensities.index(intensity_key)
199 |
200 | # Set preset reflection scales
201 | scale_key = round(plugin.s_x, 2)
202 | if scale_key in presets.material_reflection_scales:
203 | scales = list(presets.material_reflection_scales)
204 | self.material.dff["preset_reflection_scales"] = scales.index(scale_key)
205 |
206 | #######################################################
207 | def set_uv_animation(self, uv_anim):
208 |
209 | anim_data = self.material.node_tree.animation_data_create()
210 |
211 | if self.principled:
212 | mapping = self.principled.base_color_texture.node_mapping_get()
213 | mapping.vector_type = 'POINT'
214 |
215 | fps = bpy.context.scene.render.fps
216 |
217 | action = bpy.data.actions.new(uv_anim.name)
218 | anim_data.action = action
219 |
220 | if bpy.app.version < (4, 4, 0):
221 | action_fcurves = action.fcurves
222 |
223 | else:
224 | slot = action.slots.new(id_type='NODETREE', name='UV')
225 | anim_data.action_slot = slot
226 |
227 | layer = action.layers.new('UV')
228 | strip = layer.strips.new(type='KEYFRAME')
229 | channelbag = strip.channelbag(slot, ensure=True)
230 |
231 | action_fcurves = channelbag.fcurves
232 |
233 | fcurves = [
234 | None,
235 | action_fcurves.new(data_path=f'nodes["{mapping.name}"].inputs[3].default_value', index=0),
236 | action_fcurves.new(data_path=f'nodes["{mapping.name}"].inputs[3].default_value', index=1),
237 | None,
238 | action_fcurves.new(data_path=f'nodes["{mapping.name}"].inputs[1].default_value', index=0),
239 | action_fcurves.new(data_path=f'nodes["{mapping.name}"].inputs[1].default_value', index=1),
240 | ]
241 |
242 | for frame_idx, frame in enumerate(uv_anim.frames):
243 | for fc_idx, fc in enumerate(fcurves):
244 | if not fc:
245 | continue
246 |
247 | should_add_kp = True
248 |
249 | # Try to add constant interpolation
250 | if frame_idx > 0 and frame.time == uv_anim.frames[frame_idx-1].time:
251 |
252 | # We can overwrite the very first one keyframe
253 | if len(fc.keyframe_points) < 2:
254 | should_add_kp = False
255 |
256 | else:
257 | prev_kp, kp = fc.keyframe_points[-2:]
258 |
259 | # We can overwrite the previous keyframe with constant interpolation
260 | if prev_kp.interpolation == 'CONSTANT':
261 | should_add_kp = False
262 |
263 | # The values of the previous keyframes are equal,
264 | # so we can use constant interpolation and overwrite the last one
265 | elif prev_kp.co[1] == kp.co[1]:
266 | should_add_kp = False
267 | prev_kp.interpolation = 'CONSTANT'
268 |
269 | if should_add_kp:
270 | fc.keyframe_points.add(1)
271 |
272 | kp = fc.keyframe_points[-1]
273 | val = frame.uv[fc_idx]
274 |
275 | # Y coords are flipped in Blender
276 | if fc_idx == 5:
277 | val = 1 - val
278 |
279 | # Could also use round here perhaps. I don't know what's better
280 | kp.co = frame.time * fps, val
281 | kp.interpolation = 'LINEAR'
282 |
283 | self.material.dff.animation_name = uv_anim.name
284 | self.material.dff.export_animation = True
285 |
286 | #######################################################
287 | def set_user_data(self, user_data):
288 | self.material['dff_user_data'] = user_data.to_mem()[12:]
289 |
290 | #######################################################
291 | def __init__(self, material):
292 | self.material = material
293 | self.principled = None
294 |
295 | # Init Principled Wrapper for Blender 2.8
296 | from bpy_extras.node_shader_utils import PrincipledBSDFWrapper
297 |
298 | self.principled = PrincipledBSDFWrapper(self.material,
299 | is_readonly=False)
300 |
301 | #######################################################
302 | class object_helper:
303 |
304 | #######################################################
305 | def __init__(self, name):
306 |
307 | """
308 | An object helper for importing different types of objects
309 | """
310 |
311 | self.name = name
312 | self.mesh = None
313 | self.object = None
314 |
315 | #######################################################
316 | def get_object(self):
317 | pass
318 |
319 | pass
320 |
--------------------------------------------------------------------------------
/gtaLib/native_xbox.py:
--------------------------------------------------------------------------------
1 | # GTA DragonFF - Blender scripts to edit basic GTA formats
2 | # Copyright (C) 2019 Parik
3 |
4 | # This program is free software: you can redistribute it and/or modify
5 | # it under the terms of the GNU General Public License as published by
6 | # the Free Software Foundation, either version 3 of the License, or
7 | # (at your option) any later version.
8 |
9 | # This program is distributed in the hope that it will be useful,
10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | # GNU General Public License for more details.
13 |
14 | # You should have received a copy of the GNU General Public License
15 | # along with this program. If not, see .
16 |
17 | from struct import unpack_from, calcsize
18 |
19 | from .dff import RGBA, Sections, TexCoords, Triangle, Vector
20 | from .txd import ImageDecoder, TextureNative, PaletteType
21 |
22 | # geometry flags
23 | rpGEOMETRYTRISTRIP = 0x00000001
24 | rpGEOMETRYPOSITIONS = 0x00000002
25 | rpGEOMETRYTEXTURED = 0x00000004
26 | rpGEOMETRYPRELIT = 0x00000008
27 | rpGEOMETRYNORMALS = 0x00000010
28 | rpGEOMETRYLIGHT = 0x00000020
29 | rpGEOMETRYMODULATEMATERIALCOLOR = 0x00000040
30 | rpGEOMETRYTEXTURED2 = 0x00000080
31 | rpGEOMETRYNATIVE = 0x01000000
32 |
33 | ptTRIANGLELIST = 5
34 | ptTRIANGLESTRIP = 6
35 |
36 | # compressed formats
37 | D3DFMT_DXT1 = 0x0000000C
38 | D3DFMT_DXT2 = 0x0000000D
39 | D3DFMT_DXT3 = 0x0000000E
40 | D3DFMT_DXT5 = 0x0000000F
41 |
42 | #######################################################
43 | class NativeXboxSkin:
44 |
45 | #######################################################
46 | @staticmethod
47 | def unpack(skin, data, geometry):
48 | skin.num_bones = unpack_from("> 11
159 | z = (compressed_normal & 0xFFC00000) >> 22
160 | if x & 0x400 != 0:
161 | x -= 0x800
162 | if y & 0x400 != 0:
163 | y -= 0x800
164 | if z & 0x200 != 0:
165 | z -= 0x400
166 | normal = Vector(x / 0x3FF, y / 0x3FF, z / 0x1FF)
167 | geometry.normals.append(normal)
168 |
169 | if geometry.flags & rpGEOMETRYPRELIT != 0:
170 | b, g, r, a = unpack_from("<4B", data, self._read(4))
171 | prelit_color = RGBA(r, g, b ,a)
172 | geometry.prelit_colors.append(prelit_color)
173 |
174 | if geometry.flags & rpGEOMETRYTEXTURED != 0:
175 | tex_coord = Sections.read(TexCoords, data, self._read(8))
176 | geometry.uv_layers[0].append(tex_coord)
177 |
178 | if geometry.flags & rpGEOMETRYTEXTURED2 != 0:
179 | tex_coord = Sections.read(TexCoords, data, self._read(8))
180 | geometry.uv_layers[1].append(tex_coord)
181 |
182 | if not is_compressed_normal:
183 | normal = Sections.read(Vector, data, self._read(12))
184 | geometry.normals.append(normal)
185 |
186 | # Generate triangles
187 | for split_index, split_header in enumerate(geometry.split_headers):
188 | tri_vertices = indices[split_index]
189 |
190 | if geometry.flags & rpGEOMETRYTRISTRIP:
191 | for i in range(len(tri_vertices) - 2):
192 | if i % 2 == 0:
193 | idx1 = i + 1
194 | idx2 = i + 0
195 | else:
196 | idx1 = i + 0
197 | idx2 = i + 1
198 | idx3 = i + 2
199 |
200 | vertex1, vertex2, vertex3 = tri_vertices[idx1], tri_vertices[idx2], tri_vertices[idx3]
201 | if vertex1 != vertex2 and vertex1 != vertex3 and vertex2 != vertex3:
202 | geometry.triangles.append(Triangle(vertex1, vertex2, split_header.material, vertex3))
203 |
204 | else:
205 | for i in range(0, len(tri_vertices) - 2, 3):
206 | idx1 = i + 1
207 | idx2 = i + 0
208 | idx3 = i + 2
209 | vertex1, vertex2, vertex3 = tri_vertices[idx1], tri_vertices[idx2], tri_vertices[idx3]
210 | geometry.triangles.append(Triangle(vertex1, vertex2, split_header.material, vertex3))
211 |
212 | #######################################################
213 | def _read(self, size):
214 | current_pos = self._pos
215 | self._pos += size
216 |
217 | return current_pos
218 |
219 |
220 | #######################################################
221 | class NativeXboxSplitHeader:
222 | __slots__ = [
223 | "vertex_start",
224 | "vertex_end",
225 | "indices_num"
226 | ]
227 |
228 | #######################################################
229 | def __init__(self, vertex_start, vertex_end, indices_num):
230 | self.vertex_start = vertex_start
231 | self.vertex_end = vertex_end
232 | self.indices_num = indices_num
233 |
234 | #######################################################
235 | class NativeXboxTexture(TextureNative):
236 |
237 | #######################################################
238 | def __init__(self):
239 | super().__init__()
240 | self.compression = 0
241 |
242 | #######################################################
243 | def to_rgba(self, level=0):
244 | width = self.get_width(level)
245 | height = self.get_height(level)
246 | pixels = self.pixels[level]
247 | compression = self.compression
248 |
249 | if compression:
250 | if compression == D3DFMT_DXT1:
251 | return ImageDecoder.bc1(pixels, width, height, 0x00)
252 | elif compression == D3DFMT_DXT2:
253 | return ImageDecoder.bc2(pixels, width, height, True)
254 | elif compression == D3DFMT_DXT3:
255 | return ImageDecoder.bc2(pixels, width, height, False)
256 | elif compression == D3DFMT_DXT5:
257 | return ImageDecoder.bc3(pixels, width, height, False)
258 |
259 | return super().to_rgba(level)
260 |
261 | #######################################################
262 | @staticmethod
263 | def from_mem(data):
264 | self = NativeXboxTexture()
265 | self.pos = 0
266 | self.data = data
267 |
268 | (
269 | self.platform_id, self.filter_mode, self.uv_addressing
270 | ) = unpack_from("= width and i >= height:
342 | break
343 |
344 | res = bytearray(width * height * bpp)
345 |
346 | v = 0
347 | for y in range(height):
348 | u = 0
349 | for x in range(width):
350 | src_pos = (u | v) * bpp
351 | dst_pos = (y * width + x) * bpp
352 | res[dst_pos:dst_pos+bpp] = data[src_pos:src_pos+bpp]
353 | u = (u - maskU) & maskU
354 | v = (v - maskV) & maskV
355 |
356 | return res
357 |
358 | #######################################################
359 | def _read(self, size):
360 | current_pos = self.pos
361 | self.pos += size
362 |
363 | return current_pos
364 |
365 | #######################################################
366 | def _read_raw(self, size):
367 | offset = self._read(size)
368 | return self.data[offset:offset+size]
369 |
--------------------------------------------------------------------------------
/ops/ext_2dfx_importer.py:
--------------------------------------------------------------------------------
1 | # GTA DragonFF - Blender scripts to edit basic GTA formats
2 | # Copyright (C) 2019 Parik
3 |
4 | # This program is free software: you can redistribute it and/or modify
5 | # it under the terms of the GNU General Public License as published by
6 | # the Free Software Foundation, either version 3 of the License, or
7 | # (at your option) any later version.
8 |
9 | # This program is distributed in the hope that it will be useful,
10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | # GNU General Public License for more details.
13 |
14 | # You should have received a copy of the GNU General Public License
15 | # along with this program. If not, see .
16 |
17 | import bpy
18 | import math
19 | import os
20 |
21 | from mathutils import Vector
22 |
23 | from ..gtaLib import dff
24 |
25 | #######################################################
26 | def create_arrow_mesh(name):
27 | arrow_mesh = bpy.data.meshes.get(name)
28 | if arrow_mesh is None:
29 | arrow_mesh = bpy.data.meshes.new(name=name)
30 | verts = [
31 | (-0.0100, 0.0000, 0.0100),
32 | (-0.0100, 0.2500, 0.0100),
33 | (-0.0100, 0.0000, -0.0100),
34 | (-0.0100, 0.2500, -0.0100),
35 | (0.0100, 0.0000, 0.0100),
36 | (0.0100, 0.2500, 0.0100),
37 | (0.0100, 0.0000, -0.0100),
38 | (0.0100, 0.2500, -0.0100),
39 | (-0.0350, 0.2500, 0.0350),
40 | (-0.0350, 0.2500, -0.0350),
41 | (0.0350, 0.2500, 0.0350),
42 | (0.0350, 0.2500, -0.0350),
43 | (0.0000, 0.5000, 0.0000),
44 | ]
45 | edges = []
46 | faces = [
47 | (0, 1, 3, 2),
48 | (2, 3, 7, 6),
49 | (6, 7, 5, 4),
50 | (4, 5, 1, 0),
51 | (3, 1, 8, 9),
52 | (10, 11, 12),
53 | (1, 5, 10, 8),
54 | (7, 3, 9, 11),
55 | (5, 7, 11, 10),
56 | (9, 8, 12),
57 | (8, 10, 12),
58 | (11, 9, 12),
59 | ]
60 | arrow_mesh.from_pydata(verts, edges, faces)
61 |
62 | return arrow_mesh
63 |
64 | #######################################################
65 | class ext_2dfx_importer:
66 |
67 | """ Helper class for 2dfx importing """
68 |
69 | #######################################################
70 | def __init__(self, effects):
71 | self.effects = effects
72 |
73 | #######################################################
74 | @staticmethod
75 | def create_light_object(color):
76 | data = bpy.data.lights.new(name="2dfx_light", type='POINT')
77 | data.color = color
78 |
79 | obj = bpy.data.objects.new("2dfx_light", data)
80 |
81 | return obj
82 |
83 | #######################################################
84 | @staticmethod
85 | def create_particle_object(effect):
86 | obj = bpy.data.objects.new("2dfx_particle", None)
87 |
88 | settings = obj.dff.ext_2dfx
89 | settings.val_str24_1 = effect
90 |
91 | return obj
92 |
93 | #######################################################
94 | @staticmethod
95 | def create_ped_attractor_object(attractor_type, queue_euler, use_euler, forward_euler,
96 | external_script, ped_existing_probability, unk):
97 | obj = bpy.data.objects.new("2dfx_ped_attractor", None)
98 |
99 | settings = obj.dff.ext_2dfx
100 | settings.ped_attractor_type = attractor_type
101 | settings.val_euler_1 = queue_euler
102 | obj.rotation_euler = use_euler
103 | settings.val_euler_2 = forward_euler
104 | settings.val_str8_1 = external_script
105 | settings.val_chance_1 = ped_existing_probability
106 | settings.val_int_1 = unk
107 |
108 | return obj
109 |
110 | #######################################################
111 | @staticmethod
112 | def create_sun_glare_object():
113 | obj = bpy.data.objects.new("2dfx_sun_glare", None)
114 |
115 | return obj
116 |
117 | #######################################################
118 | @staticmethod
119 | def create_road_sign_object(body, size, color, rotation_euler):
120 | data = bpy.data.curves.new(name="2dfx_road_sign", type='FONT')
121 | data.body = body
122 | data.align_x = data.align_y = 'CENTER'
123 | data.size = 0.5
124 |
125 | font = bpy.data.fonts.get("DejaVu Sans Mono Book")
126 | if not font:
127 | font_path = os.path.join(bpy.utils.system_resource('DATAFILES'), "fonts", "DejaVuSansMono.woff2")
128 | if os.path.isfile(font_path):
129 | font = bpy.data.fonts.load(font_path)
130 |
131 | if font:
132 | data.font = font
133 |
134 | settings = data.ext_2dfx
135 | settings.size = size
136 | settings.color = color
137 |
138 | obj = bpy.data.objects.new("2dfx_road_sign", data)
139 | obj.rotation_mode = 'ZXY'
140 | obj.rotation_euler = rotation_euler
141 |
142 | return obj
143 |
144 | #######################################################
145 | @staticmethod
146 | def create_trigger_point_object(point_id):
147 | obj = bpy.data.objects.new("2dfx_trigger_point", None)
148 |
149 | settings = obj.dff.ext_2dfx
150 | settings.val_int_1 = point_id
151 |
152 | return obj
153 |
154 | #######################################################
155 | @staticmethod
156 | def create_cover_point_object(cover_type, rotation_euler):
157 | mesh = create_arrow_mesh("_2dfx_cover_point")
158 | obj = bpy.data.objects.new("2dfx_cover_point", mesh)
159 | obj.rotation_euler = rotation_euler
160 | obj.lock_rotation[0] = True
161 | obj.lock_rotation[1] = True
162 |
163 | settings = obj.dff.ext_2dfx
164 | settings.val_int_1 = cover_type
165 |
166 | return obj
167 |
168 | #######################################################
169 | @staticmethod
170 | def create_escalator_object(bottom, top, end, direction, angle=0):
171 | obj = bpy.data.objects.new("2dfx_escalator", None)
172 | obj.rotation_euler = (0, 0, angle)
173 | obj.lock_rotation[0] = True
174 | obj.lock_rotation[1] = True
175 | obj.lock_rotation_w = True
176 | obj.lock_scale[0] = True
177 | obj.lock_scale[1] = True
178 | obj.lock_scale[2] = True
179 |
180 | settings = obj.dff.ext_2dfx
181 | settings.val_vector_1 = bottom
182 | settings.val_vector_2 = top
183 | settings.val_vector_3 = end
184 | settings.escalator_direction = direction
185 |
186 | return obj
187 |
188 | #######################################################
189 | def import_light(self, entry):
190 | FL1, FL2 = dff.Light2dfx.Flags1, dff.Light2dfx.Flags2
191 |
192 | color = [i / 255 for i in entry.color[:3]]
193 | obj = ext_2dfx_importer.create_light_object(color)
194 |
195 | settings = obj.data.ext_2dfx
196 | settings.alpha = entry.color[3] / 255
197 | settings.corona_far_clip = entry.coronaFarClip
198 | settings.point_light_range = entry.pointlightRange
199 | settings.corona_size = entry.coronaSize
200 | settings.shadow_size = entry.shadowSize
201 | settings.corona_show_mode = str(entry.coronaShowMode)
202 | settings.corona_enable_reflection = entry.coronaEnableReflection != 0
203 | settings.corona_flare_type = entry.coronaFlareType
204 | settings.shadow_color_multiplier = entry.shadowColorMultiplier
205 | settings.corona_tex_name = entry.coronaTexName
206 | settings.shadow_tex_name = entry.shadowTexName
207 | settings.shadow_z_distance = entry.shadowZDistance
208 |
209 | settings.flag1_corona_check_obstacles = entry.check_flag(FL1.CORONA_CHECK_OBSTACLES)
210 | settings.flag1_fog_type |= entry.check_flag(FL1.FOG_TYPE)
211 | settings.flag1_fog_type |= entry.check_flag(FL1.FOG_TYPE2) << 1
212 | settings.flag1_without_corona = entry.check_flag(FL1.WITHOUT_CORONA)
213 | settings.flag1_corona_only_at_long_distance = entry.check_flag(FL1.CORONA_ONLY_AT_LONG_DISTANCE)
214 | settings.flag1_at_day = entry.check_flag(FL1.AT_DAY)
215 | settings.flag1_at_night = entry.check_flag(FL1.AT_NIGHT)
216 | settings.flag1_blinking1 = entry.check_flag(FL1.BLINKING1)
217 |
218 | settings.flag2_corona_only_from_below = entry.check_flag2(FL2.CORONA_ONLY_FROM_BELOW)
219 | settings.flag2_blinking2 = entry.check_flag2(FL2.BLINKING2)
220 | settings.flag2_update_height_above_ground = entry.check_flag2(FL2.UPDATE_HEIGHT_ABOVE_GROUND)
221 | settings.flag2_check_view_vector = entry.check_flag2(FL2.CHECK_DIRECTION)
222 | settings.flag2_blinking3 = entry.check_flag2(FL2.BLINKING3)
223 |
224 | if entry.lookDirection is not None:
225 | settings.view_vector = entry.lookDirection
226 | settings.export_view_vector = True
227 |
228 | return obj
229 |
230 | #######################################################
231 | def import_particle(self, entry):
232 | effect = entry.effect
233 | return ext_2dfx_importer.create_particle_object(effect)
234 |
235 | #######################################################
236 | def import_ped_attractor(self, entry):
237 | attractor_type = str(entry.type)
238 | queue_euler = Vector(entry.queue_direction).to_track_quat('Z', 'Y').to_euler()
239 | use_euler = Vector(entry.use_direction).to_track_quat('Z', 'Y').to_euler()
240 | forward_euler = Vector(entry.forward_direction).to_track_quat('Z', 'Y').to_euler()
241 | external_script = entry.external_script
242 | ped_existing_probability = entry.ped_existing_probability
243 | unk = entry.unk
244 | return ext_2dfx_importer.create_ped_attractor_object(attractor_type,
245 | queue_euler, use_euler, forward_euler,
246 | external_script, ped_existing_probability, unk)
247 |
248 | #######################################################
249 | def import_sun_glare(self, entry):
250 | return ext_2dfx_importer.create_sun_glare_object()
251 |
252 | #######################################################
253 | def import_enter_exit(self, entry):
254 | obj = bpy.data.objects.new("2dfx_enter_exit", None)
255 |
256 | settings = obj.dff.ext_2dfx
257 | settings.val_degree_1 = math.degrees(entry.enter_angle)
258 | settings.val_float_1 = entry.approximation_radius_x
259 | settings.val_float_2 = entry.approximation_radius_y
260 | settings.val_vector_1 = entry.exit_location
261 | settings.val_degree_2 = entry.exit_angle
262 | settings.val_short_1 = entry.interior
263 | settings.val_byte_1 = entry._flags1
264 | settings.val_byte_2 = entry.sky_color
265 | settings.val_str8_1 = entry.interior_name
266 | settings.val_hour_1 = entry.time_on
267 | settings.val_hour_2 = entry.time_off
268 | settings.val_byte_3 = entry._flags2
269 | settings.val_byte_4 = entry.unk
270 |
271 | return obj
272 |
273 | #######################################################
274 | def import_road_sign(self, entry):
275 | lines_num = {0:4, 1:1, 2:2, 3:3}[entry.flags & 0b11]
276 | max_chars_num = {0:16, 1:2, 2:4, 3:8}[(entry.flags >> 2) & 0b11]
277 |
278 | body = ""
279 | for line in (entry.text1, entry.text2, entry.text3, entry.text4)[:lines_num]:
280 | body = body + "\n" if body else body
281 | body += line.replace("_", " ")[:max_chars_num]
282 |
283 | size = entry.size
284 | color = str((entry.flags >> 4) & 0b11)
285 | rotation_euler = (
286 | entry.rotation.x * (math.pi / 180),
287 | entry.rotation.y * (math.pi / 180),
288 | entry.rotation.z * (math.pi / 180)
289 | )
290 | return ext_2dfx_importer.create_road_sign_object(body, size, color, rotation_euler)
291 |
292 | #######################################################
293 | def import_trigger_point(self, entry):
294 | point_id = entry.point_id
295 | return ext_2dfx_importer.create_trigger_point_object(point_id)
296 |
297 | #######################################################
298 | def import_cover_point(self, entry):
299 | cover_type = entry.cover_type
300 | direction = Vector((entry.direction_x, entry.direction_y, 0))
301 | rotation_euler = direction.to_track_quat('Y', 'Z').to_euler()
302 | return ext_2dfx_importer.create_cover_point_object(cover_type, rotation_euler)
303 |
304 | #######################################################
305 | def import_escalator(self, entry):
306 | bottom = tuple(entry.bottom[i] - entry.loc[i] for i in range(3))
307 | top = tuple(entry.top[i] - entry.loc[i] for i in range(3))
308 | end = tuple(entry.end[i] - entry.loc[i] for i in range(3))
309 | direction = str(entry.direction)
310 | return ext_2dfx_importer.create_escalator_object(bottom, top, end, direction)
311 |
312 | #######################################################
313 | def get_objects(self):
314 |
315 | """ Import and return the list of imported objects """
316 |
317 | functions = {
318 | 0: self.import_light,
319 | 1: self.import_particle,
320 | 3: self.import_ped_attractor,
321 | 4: self.import_sun_glare,
322 | 6: self.import_enter_exit,
323 | 7: self.import_road_sign,
324 | 8: self.import_trigger_point,
325 | 9: self.import_cover_point,
326 | 10: self.import_escalator,
327 | }
328 |
329 | objects = []
330 |
331 | for entry in self.effects.entries:
332 | if entry.effect_id in functions:
333 | obj = functions[entry.effect_id](entry)
334 | obj.dff.type = '2DFX'
335 | obj.dff.ext_2dfx.effect = str(entry.effect_id)
336 | obj.location = entry.loc
337 | objects.append(obj)
338 |
339 | return objects
--------------------------------------------------------------------------------
/ops/map_importer.py:
--------------------------------------------------------------------------------
1 | # GTA DragonFF - Blender scripts to edit basic GTA formats
2 | # Copyright (C) 2019 Parik
3 |
4 | # This program is free software: you can redistribute it and/or modify
5 | # it under the terms of the GNU General Public License as published by
6 | # the Free Software Foundation, either version 3 of the License, or
7 | # (at your option) any later version.
8 |
9 | # This program is distributed in the hope that it will be useful,
10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | # GNU General Public License for more details.
13 |
14 | # You should have received a copy of the GNU General Public License
15 | # along with this program. If not, see .
16 |
17 | import bpy
18 | import os
19 | from ..gtaLib import map as map_utilites
20 | from ..ops import dff_importer, col_importer, txd_importer
21 | from .cull_importer import cull_importer
22 | from .importer_common import hide_object
23 |
24 | #######################################################
25 | class map_importer:
26 |
27 | model_cache = {}
28 | object_data = []
29 | object_instances = []
30 | cull_instances = []
31 | col_files = []
32 | collision_collection = None
33 | object_instances_collection = None
34 | mesh_collection = None
35 | cull_collection = None
36 | map_section = ""
37 | settings = None
38 |
39 | #######################################################
40 | @staticmethod
41 | def import_object_instance(context, inst):
42 | self = map_importer
43 |
44 | # Skip LODs if user selects this
45 | if hasattr(inst, 'lod') and int(inst.lod) == -1 and self.settings.skip_lod:
46 | return
47 |
48 | # Deleted objects that Rockstar forgot to remove?
49 | if inst.id not in self.object_data:
50 | return
51 |
52 | model = self.object_data[inst.id].modelName
53 | txd = self.object_data[inst.id].txdName
54 |
55 | if inst.id in self.model_cache:
56 |
57 | # Get model from memory
58 | new_objects = {}
59 | model_cache = self.model_cache[inst.id]
60 |
61 | cached_objects = [obj for obj in model_cache if obj.dff.type == "OBJ"]
62 | for obj in cached_objects:
63 | new_obj = bpy.data.objects.new(model, obj.data)
64 | new_obj.location = obj.location
65 | new_obj.rotation_quaternion = obj.rotation_quaternion
66 | new_obj.scale = obj.scale
67 |
68 | if not self.settings.create_backfaces:
69 | modifier = new_obj.modifiers.new("EdgeSplit", 'EDGE_SPLIT')
70 | # When added to some objects (empties?), returned modifier is None
71 | if modifier is not None:
72 | modifier.use_edge_angle = False
73 |
74 | if '{}.dff'.format(model) in bpy.data.collections:
75 | bpy.data.collections['{}.dff'.format(model)].objects.link(
76 | new_obj
77 | )
78 | else:
79 | context.collection.objects.link(new_obj)
80 | new_objects[obj] = new_obj
81 |
82 | # Parenting
83 | for obj in cached_objects:
84 | if obj.parent in cached_objects:
85 | new_objects[obj].parent = new_objects[obj.parent]
86 |
87 | # Position root object
88 | for obj in new_objects.values():
89 | if not obj.parent:
90 | self.apply_transformation_to_object(
91 | obj, inst
92 | )
93 |
94 | cached_2dfx = [obj for obj in model_cache if obj.dff.type == "2DFX"]
95 | for obj in cached_2dfx:
96 | new_obj = bpy.data.objects.new(obj.name, obj.data)
97 | new_obj.location = obj.location
98 | new_obj.rotation_mode = obj.rotation_mode
99 | new_obj.lock_rotation = obj.lock_rotation
100 | new_obj.rotation_quaternion = obj.rotation_quaternion
101 | new_obj.rotation_euler = obj.rotation_euler
102 | new_obj.scale = obj.scale
103 |
104 | if obj.parent:
105 | new_obj.parent = new_objects[obj.parent]
106 |
107 | for prop in obj.dff.keys():
108 | new_obj.dff[prop] = obj.dff[prop]
109 |
110 | if '{}.dff'.format(model) in bpy.data.collections:
111 | bpy.data.collections['{}.dff'.format(model)].objects.link(
112 | new_obj
113 | )
114 | else:
115 | context.collection.objects.link(new_obj)
116 | new_objects[obj] = new_obj
117 |
118 | print(str(inst.id), 'loaded from cache')
119 | else:
120 |
121 | dff_filename = "%s.dff" % model
122 | txd_filename = "%s.txd" % txd
123 |
124 | dff_filepath = map_utilites.MapDataUtility.find_path_case_insensitive(self.settings.dff_folder, dff_filename)
125 | txd_filepath = map_utilites.MapDataUtility.find_path_case_insensitive(self.settings.dff_folder, txd_filename)
126 |
127 | # Import dff from a file if file exists
128 | if not dff_filepath:
129 | print("DFF not found:", os.path.join(self.settings.dff_folder, dff_filename))
130 | return
131 |
132 | txd_images = {}
133 | if self.settings.load_txd:
134 | if txd_filepath:
135 | txd_images = txd_importer.import_txd(
136 | {
137 | 'file_name' : txd_filepath,
138 | 'skip_mipmaps' : True,
139 | 'pack' : self.settings.txd_pack,
140 | }
141 | ).images
142 | else:
143 | print("TXD not found:", os.path.join(self.settings.dff_folder, txd_filename))
144 |
145 | importer = dff_importer.import_dff(
146 | {
147 | 'file_name' : "%s/%s.dff" % (
148 | self.settings.dff_folder, model
149 | ),
150 | 'txd_images' : txd_images,
151 | 'image_ext' : 'PNG',
152 | 'connect_bones' : False,
153 | 'use_mat_split' : self.settings.read_mat_split,
154 | 'remove_doubles' : not self.settings.create_backfaces,
155 | 'create_backfaces' : self.settings.create_backfaces,
156 | 'group_materials' : True,
157 | 'import_normals' : True,
158 | 'materials_naming' : "DEF",
159 | }
160 | )
161 |
162 | collection_objects = list(importer.current_collection.objects)
163 | root_objects = [obj for obj in collection_objects if obj.dff.type == "OBJ" and not obj.parent]
164 |
165 | for obj in root_objects:
166 | map_importer.apply_transformation_to_object(
167 | obj, inst
168 | )
169 |
170 | # Set root object as 2DFX parent
171 | if root_objects:
172 | for obj in collection_objects:
173 | # Skip Road Signs
174 | if obj.dff.type == "2DFX" and obj.dff.ext_2dfx.effect != '7':
175 | obj.parent = root_objects[0]
176 |
177 | # Move dff collection to a top collection named for the file it came from
178 | if not self.object_instances_collection:
179 | self.create_object_instances_collection(context)
180 |
181 | context.scene.collection.children.unlink(importer.current_collection)
182 | self.object_instances_collection.children.link(importer.current_collection)
183 |
184 | # Save into buffer
185 | self.model_cache[inst.id] = collection_objects
186 | print(str(inst.id), 'loaded new')
187 |
188 | # Look for collision mesh
189 | name = self.model_cache[inst.id][0].name
190 | for obj in bpy.data.objects:
191 | if obj.dff.type == 'COL' and obj.name.endswith("%s.ColMesh" % name):
192 | new_obj = bpy.data.objects.new(obj.name, obj.data)
193 | new_obj.dff.type = 'COL'
194 | new_obj.location = obj.location
195 | new_obj.rotation_quaternion = obj.rotation_quaternion
196 | new_obj.scale = obj.scale
197 | map_importer.apply_transformation_to_object(
198 | new_obj, inst
199 | )
200 | if '{}.dff'.format(name) in bpy.data.collections:
201 | bpy.data.collections['{}.dff'.format(name)].objects.link(
202 | new_obj
203 | )
204 | hide_object(new_obj)
205 |
206 | #######################################################
207 | @staticmethod
208 | def import_collision(context, filename):
209 | self = map_importer
210 |
211 | if not self.collision_collection:
212 | self.create_collisions_collection(context)
213 |
214 | collection = bpy.data.collections.new(filename)
215 | self.collision_collection.children.link(collection)
216 | col_list = col_importer.import_col_file(os.path.join(self.settings.dff_folder, filename), filename)
217 |
218 | # Move all collisions to a top collection named for the file they came from
219 | for c in col_list:
220 | context.scene.collection.children.unlink(c)
221 | collection.children.link(c)
222 |
223 | #######################################################
224 | @staticmethod
225 | def import_cull(context, cull):
226 | self = map_importer
227 |
228 | if not self.cull_collection:
229 | self.create_cull_collection(context)
230 |
231 | obj = cull_importer.import_cull(cull)
232 |
233 | self.cull_collection.objects.link(obj)
234 |
235 | #######################################################
236 | @staticmethod
237 | def create_object_instances_collection(context):
238 | self = map_importer
239 |
240 | coll_name = '%s Meshes' % self.settings.game_version_dropdown
241 | self.mesh_collection = bpy.data.collections.get(coll_name)
242 |
243 | if not self.mesh_collection:
244 | self.mesh_collection = bpy.data.collections.new(coll_name)
245 | context.scene.collection.children.link(self.mesh_collection)
246 |
247 | # Create a new collection in Mesh to hold all the subsequent dffs loaded from this map section
248 | coll_name = self.map_section
249 | if os.path.isabs(coll_name):
250 | coll_name = os.path.basename(coll_name)
251 | self.object_instances_collection = bpy.data.collections.new(coll_name)
252 | self.mesh_collection.children.link(self.object_instances_collection)
253 |
254 | #######################################################
255 | @staticmethod
256 | def create_collisions_collection(context):
257 | self = map_importer
258 |
259 | coll_name = '%s Collisions' % self.settings.game_version_dropdown
260 | self.collision_collection = bpy.data.collections.get(coll_name)
261 |
262 | if not self.collision_collection:
263 | self.collision_collection = bpy.data.collections.new(coll_name)
264 | context.scene.collection.children.link(self.collision_collection)
265 |
266 | # Hide collection
267 | context.view_layer.active_layer_collection = context.view_layer.layer_collection.children[coll_name]
268 | context.view_layer.active_layer_collection.hide_viewport = True
269 |
270 | #######################################################
271 | @staticmethod
272 | def create_cull_collection(context):
273 | self = map_importer
274 |
275 | coll_name = '%s CULL' % self.settings.game_version_dropdown
276 | self.cull_collection = bpy.data.collections.get(coll_name)
277 |
278 | if not self.cull_collection:
279 | self.cull_collection = bpy.data.collections.new(coll_name)
280 | context.scene.collection.children.link(self.cull_collection)
281 |
282 | # Hide collection
283 | context.view_layer.active_layer_collection = context.view_layer.layer_collection.children[coll_name]
284 | context.view_layer.active_layer_collection.hide_viewport = True
285 |
286 | #######################################################
287 | @staticmethod
288 | def load_map(settings):
289 | self = map_importer
290 |
291 | self.model_cache = {}
292 | self.col_files = []
293 | self.object_instances_collection = None
294 | self.mesh_collection = None
295 | self.collision_collection = None
296 | self.cull_collection = None
297 | self.settings = settings
298 |
299 | if self.settings.use_custom_map_section:
300 | self.map_section = self.settings.custom_ipl_path
301 | else:
302 | self.map_section = self.settings.map_sections
303 |
304 | # Get all the necessary IDE and IPL data
305 | map_data = map_utilites.MapDataUtility.load_map_data(
306 | self.settings.game_version_dropdown,
307 | self.settings.game_root,
308 | self.map_section,
309 | self.settings.use_custom_map_section)
310 |
311 | self.object_instances = map_data.object_instances
312 | self.object_data = map_data.object_data
313 |
314 | if self.settings.load_cull:
315 | self.cull_instances = map_data.cull_instances
316 | else:
317 | self.cull_instances = []
318 |
319 | if self.settings.load_collisions:
320 |
321 | # Get a list of the .col files available
322 | col_files_all = set()
323 | for filename in os.listdir(self.settings.dff_folder):
324 | if filename.lower().endswith(".col"):
325 | col_files_all.add(filename)
326 |
327 | # Run through all instances and determine which .col files to load
328 | for i in range(len(self.object_instances)):
329 | id = self.object_instances[i].id
330 | # Deleted objects that Rockstar forgot to remove?
331 | if id not in self.object_data:
332 | continue
333 |
334 | objdata = self.object_data[id]
335 | if not hasattr(objdata, 'filename'):
336 | continue
337 |
338 | prefix = objdata.filename.split('/')[-1][:-4].lower()
339 | for filename in col_files_all:
340 | if filename.startswith(prefix):
341 | if not bpy.data.collections.get(filename) and filename not in self.col_files:
342 | self.col_files.append(filename)
343 |
344 | #######################################################
345 | @staticmethod
346 | def apply_transformation_to_object(obj, inst):
347 | obj.location.x = float(inst.posX)
348 | obj.location.y = float(inst.posY)
349 | obj.location.z = float(inst.posZ)
350 |
351 | obj.rotation_mode = 'QUATERNION'
352 | obj.rotation_quaternion.w = -float(inst.rotW)
353 | obj.rotation_quaternion.x = float(inst.rotX)
354 | obj.rotation_quaternion.y = float(inst.rotY)
355 | obj.rotation_quaternion.z = float(inst.rotZ)
356 |
357 | if hasattr(inst, 'scaleX'):
358 | obj.scale.x = float(inst.scaleX)
359 | if hasattr(inst, 'scaleY'):
360 | obj.scale.y = float(inst.scaleY)
361 | if hasattr(inst, 'scaleZ'):
362 | obj.scale.z = float(inst.scaleZ)
363 |
364 | #######################################################
365 | def load_map(settings):
366 | map_importer.load_map(settings)
367 |
368 | return map_importer
369 |
--------------------------------------------------------------------------------
/gtaLib/native_psp.py:
--------------------------------------------------------------------------------
1 | # GTA DragonFF - Blender scripts to edit basic GTA formats
2 | # Copyright (C) 2019 Parik
3 |
4 | # This program is free software: you can redistribute it and/or modify
5 | # it under the terms of the GNU General Public License as published by
6 | # the Free Software Foundation, either version 3 of the License, or
7 | # (at your option) any later version.
8 |
9 | # This program is distributed in the hope that it will be useful,
10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 | # GNU General Public License for more details.
13 |
14 | # You should have received a copy of the GNU General Public License
15 | # along with this program. If not, see .
16 |
17 | from struct import unpack_from, calcsize
18 |
19 | from .dff import RGBA, TexCoords, Triangle, Vector
20 | from .txd import TextureNative, PaletteType
21 |
22 | # geometry flags
23 | rpGEOMETRYTRISTRIP = 0x00000001
24 | rpGEOMETRYPOSITIONS = 0x00000002
25 | rpGEOMETRYTEXTURED = 0x00000004
26 | rpGEOMETRYPRELIT = 0x00000008
27 | rpGEOMETRYNORMALS = 0x00000010
28 | rpGEOMETRYLIGHT = 0x00000020
29 | rpGEOMETRYMODULATEMATERIALCOLOR = 0x00000040
30 | rpGEOMETRYTEXTURED2 = 0x00000080
31 | rpGEOMETRYNATIVE = 0x01000000
32 |
33 | #######################################################
34 | class NativePSPSkin:
35 |
36 | #######################################################
37 | @staticmethod
38 | def unpack(skin, data, geometry):
39 |
40 | skin.num_bones, _num_used_bones, skin.max_weights_per_vertex = unpack_from("<3Bx", data)
41 |
42 | unpack_format = "<16f"
43 | pos = 4
44 |
45 | # Read bone matrices
46 | skin.bone_matrices = []
47 | for _ in range(skin.num_bones):
48 |
49 | _data = list(unpack_from(unpack_format, data, pos))
50 | _data[ 3] = 0.0
51 | _data[ 7] = 0.0
52 | _data[11] = 0.0
53 | _data[15] = 1.0
54 |
55 | skin.bone_matrices.append(
56 | [_data[0:4], _data[4:8], _data[8:12],
57 | _data[12:16]]
58 | )
59 |
60 | pos += calcsize(unpack_format)
61 |
62 | pos += 20
63 |
64 | bone_limit, splits_num, bone_pal_num = unpack_from("<3I", data, pos)
65 | pos += 12
66 |
67 | skin.bones_used = []
68 | if not splits_num:
69 | return
70 |
71 | for _ in range(skin.num_bones):
72 | skin.bones_used.append(unpack_from("> 16
130 | if tex_count == 0:
131 | tex_count = 2 if (geometry.flags & rpGEOMETRYTEXTURED2) else \
132 | 1 if (geometry.flags & rpGEOMETRYTEXTURED) else 0
133 | else:
134 | tex_count = 1
135 |
136 | for _ in range(tex_count):
137 | geometry.uv_layers.append([])
138 |
139 | chunk_size, strip, splits_num = unpack_from("> 11) & 3
159 |
160 | next_strip_offset = self._pos
161 |
162 | # Read scale matrix
163 | self._pos = matrix_offset
164 | scale_matrix = unpack_from("<16f", data, self._read(64))
165 |
166 | # Read indices map
167 | if index_format == 2:
168 | self._pos = indices_map_offset
169 | indices = unpack_from("<%dH" % indices_map_len, data, self._read(indices_map_len * 2))
170 | elif index_format == 0:
171 | o = len(geometry.vertices)
172 | indices = list(o + i for i in range(indices_num))
173 | self._indices_map.append(indices)
174 |
175 | # Read split geometry
176 | if prev_indices_offset != indices_offset:
177 | self._pos = indices_offset
178 | self._read_split_geometry(geometry, data, indices_num, fmt, scale_matrix)
179 |
180 | prev_indices_offset = indices_offset
181 |
182 | self._generate_triangles(geometry)
183 |
184 | if geometry.vertices:
185 | geometry.has_vertices = 1
186 |
187 | if geometry.normals:
188 | geometry.has_normals = 1
189 |
190 | if geometry.prelit_colors:
191 | geometry.flags |= rpGEOMETRYPRELIT
192 | else:
193 | geometry.flags &= ~rpGEOMETRYPRELIT
194 |
195 | if geometry.uv_layers and not geometry.uv_layers[0]:
196 | geometry.uv_layers = []
197 |
198 | #######################################################
199 | def _read(self, size):
200 | current_pos = self._pos
201 | self._pos += size
202 |
203 | return current_pos
204 |
205 | #######################################################
206 | def _read_split_geometry(self, geometry, data, indices_num, fmt, scale_matrix):
207 | uv_format = fmt & 3
208 | color_format = (fmt >> 2) & 7
209 | normal_format = (fmt >> 5) & 3
210 | vertex_format = (fmt >> 7) & 3
211 | weight_format = (fmt >> 9) & 3
212 | weights_num = ((fmt >> 14) & 7) + 1
213 | vertices_num = ((fmt >> 18) & 7) + 1
214 | coord_type = (fmt >> 23) & 1
215 |
216 | for _ in range(indices_num):
217 | if weight_format == 1:
218 | wn = (weights_num + 3) // 4 * 4
219 | weights = unpack_from("<%dB" % wn, data, self._read(wn))
220 | geometry._vertex_bone_weights.append(tuple(
221 | 1.0 if w == 128 else w / 127.0 for w in weights
222 | ))
223 |
224 | if uv_format == 1:
225 | tu, tv = unpack_from("<2b", data, self._read(2))
226 | geometry.uv_layers[0].append(TexCoords(tu / 127.0, tv / 127.0))
227 | for uv in geometry.uv_layers[1:]:
228 | uv.append(TexCoords(0, 0))
229 |
230 | elif uv_format == 2:
231 | tu, tv = unpack_from("<2h", data, self._read(4))
232 | geometry.uv_layers[0].append(TexCoords(tu / 32767.0, tv / 32767.0))
233 | for uv in geometry.uv_layers[1:]:
234 | uv.append(TexCoords(0, 0))
235 |
236 | elif uv_format == 3:
237 | tu, tv = unpack_from("<2f", data, self._read(8))
238 | geometry.uv_layers[0].append(TexCoords(tu, tv))
239 | for uv in geometry.uv_layers[1:]:
240 | uv.append(TexCoords(0, 0))
241 |
242 | if color_format > 3:
243 | if color_format == 6:
244 | color = unpack_from("> 4) & 0xF0,
249 | (color >> 8) & 0xF0
250 | ))
251 |
252 | elif color_format == 7:
253 | color = unpack_from("> 8) & 0xFF,
257 | (color >> 16) & 0xFF,
258 | (color >> 24) & 0xFF
259 | ))
260 |
261 | if normal_format == 1:
262 | nx, ny, nz = unpack_from("<3bx", data, self._read(4))
263 | nx /= 127.0
264 | ny /= 127.0
265 | nz /= 127.0
266 | geometry.normals.append(Vector(nx, ny, nz))
267 |
268 | elif normal_format == 2:
269 | nx, ny, nz = unpack_from("<3h", data, self._read(6))
270 | nx /= 32767.0
271 | ny /= 32767.0
272 | nz /= 32767.0
273 | geometry.normals.append(Vector(nx, ny, nz))
274 |
275 | elif normal_format == 3:
276 | nx, ny, nz = unpack_from("<3f", data, self._read(12))
277 | geometry.normals.append(Vector(nx, ny, nz))
278 |
279 | if vertex_format == 1:
280 | x, y, z = unpack_from("<3bx", data, self._read(4))
281 | vx = ( x / 127.0) * scale_matrix[0]
282 | vy = ( y / 127.0) * scale_matrix[5]
283 | vz = ( z / 127.0) * scale_matrix[10]
284 | geometry.vertices.append(Vector(vx, vy, vz))
285 |
286 | elif vertex_format == 2:
287 | x, y, z = unpack_from("<3h", data, self._read(6))
288 | vx = ( x / 32767.0) * scale_matrix[0]
289 | vy = ( y / 32767.0) * scale_matrix[5]
290 | vz = ( z / 32767.0) * scale_matrix[10]
291 | geometry.vertices.append(Vector(vx, vy, vz))
292 |
293 | elif vertex_format == 3:
294 | x, y, z = unpack_from("<3f", data, self._read(12))
295 | vx = x * scale_matrix[0]
296 | vy = y * scale_matrix[5]
297 | vz = z * scale_matrix[10]
298 | geometry.vertices.append(Vector(vx, vy, vz))
299 |
300 | #######################################################
301 | def _generate_triangles(self, geometry):
302 | for split, split_header in enumerate(geometry.split_headers):
303 | indices_map = self._indices_map[split]
304 |
305 | for i in range(split_header.indices_count - 2):
306 | idx1 = indices_map[i + 0]
307 | idx2 = indices_map[i + 1]
308 | idx3 = indices_map[i + 2]
309 |
310 | v1 = geometry.vertices[idx1]
311 | v2 = geometry.vertices[idx2]
312 | v3 = geometry.vertices[idx3]
313 | if v1 == v2 or v1 == v3 or v2 == v3:
314 | continue
315 |
316 | if i % 2 == 0:
317 | triangle = Triangle(
318 | idx2,
319 | idx1,
320 | split_header.material,
321 | idx3
322 | )
323 | else:
324 | triangle = Triangle(
325 | idx1,
326 | idx2,
327 | split_header.material,
328 | idx3
329 | )
330 |
331 | geometry.triangles.append(triangle)
332 |
333 | #######################################################
334 | class NativePSPTexture(TextureNative):
335 |
336 | #######################################################
337 | def to_rgba(self, level=0):
338 | width = self.get_width(level)
339 | height = self.get_height(level)
340 | pixels = self.pixels[level]
341 | palette = self.palette
342 |
343 | if palette and self.get_raster_palette_type() == PaletteType.PALETTE_4:
344 | return NativePSPTexture.decode_pal4(pixels, palette, width, height)
345 |
346 | return super().to_rgba(level)
347 |
348 | #######################################################
349 | @staticmethod
350 | def from_mem(data):
351 | self = NativePSPTexture()
352 | self.pos = 0
353 | self.data = data
354 |
355 | (
356 | self.raster_format_flags, self.width, self.height, self.depth,
357 | self.num_levels, texture_format, bit_flag, unk_flag
358 | ) = unpack_from("> 3
392 | res = bytearray(width * height)
393 |
394 | row_blocks = width // 16
395 | block_size = 16 * 8
396 |
397 | for y in range(height):
398 | block_y = y // 8
399 | y_in_block = y % 8
400 |
401 | for x in range(width):
402 | block_x = x // 16
403 | x_in_block = x % 16
404 |
405 | block_idx = block_x + (block_y * row_blocks)
406 | src_off = (block_idx * block_size) + x_in_block + y_in_block * 16
407 | dst_off = y * width + x
408 |
409 | res[dst_off] = data[src_off]
410 |
411 | return res
412 |
413 | #######################################################
414 | @staticmethod
415 | def decode_pal4(data, palette, width, height):
416 | pos = 0
417 | ret = bytearray(4 * width * height)
418 |
419 | for i in data:
420 | idx1, idx2 = i & 0xf, (i >> 4) & 0xf
421 | ret[pos+0:pos+4] = palette[idx1*4:idx1*4+4]
422 | ret[pos+4:pos+8] = palette[idx2*4:idx2*4+4]
423 | pos += 8
424 |
425 | return bytes(ret)
426 |
427 | #######################################################
428 | def _read(self, size):
429 | current_pos = self.pos
430 | self.pos += size
431 |
432 | return current_pos
433 |
434 | #######################################################
435 | def _read_raw(self, size):
436 | offset = self._read(size)
437 | return self.data[offset:offset+size]
438 |
--------------------------------------------------------------------------------