├── 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 | [![Discord](https://img.shields.io/discord/1286221154612281405.svg?label=&logo=discord&logoColor=ffffff&color=7389D8&labelColor=6A7EC2)](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 | ![image](https://github.com/user-attachments/assets/02868d1c-273b-47a2-927d-083aa5d45605) 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 | --------------------------------------------------------------------------------