├── 3dcam-engine-helper.py ├── README.md ├── blender-psx.jpg ├── io_export_psx_tmesh.py └── startfile.blend /3dcam-engine-helper.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | from bpy.props import IntProperty, StringProperty, PointerProperty, BoolProperty 3 | from bpy.types import PropertyGroup 4 | from bpy.app.handlers import persistent 5 | 6 | bl_info = { 7 | "name": "3Dcam engine custom properties helper", 8 | "author": "Schnappy", 9 | "blender": (2,79,0), 10 | "category": "Property helper", 11 | "version": (0,0,1), 12 | "location": "3D view > Object", 13 | "location": "Properties panel > Data", 14 | "description": "Set/Unset/Copy flags easily" 15 | } 16 | # Based on https://blog.hamaluik.ca/posts/dynamic-blender-properties by Kenton Hamaluik 17 | bpy.propertyGroupLayouts = { 18 | "Flags": [ 19 | { "name": "isAnim", "type": "boolean" }, 20 | { "name": "isProp", "type": "boolean" }, 21 | { "name": "isRigidBody", "type": "boolean" }, 22 | { "name": "isStaticBody","type": "boolean" }, 23 | { "name": "isRound", "type": "boolean" }, 24 | { "name": "isPrism", "type": "boolean" }, 25 | { "name": "isActor", "type": "boolean" }, 26 | { "name": "isLevel", "type": "boolean" }, 27 | { "name": "isWall", "type": "boolean" }, 28 | { "name": "isBG", "type": "boolean" }, 29 | { "name": "isSprite", "type": "boolean" }, 30 | { "name": "isLerp", "type": "boolean" }, 31 | { "name": "isXA", "type": "boolean" } 32 | ], 33 | "Others": [ 34 | { "name": "mass", "type": "int" } 35 | ] 36 | } 37 | # store keymaps here to access after registration 38 | addon_keymaps = [] 39 | bpy.samplePropertyGroups = {} 40 | last_selection = [] 41 | store_att_names = [] 42 | 43 | def getActiveObjProps(active_obj): 44 | object_custom_props = [prop for prop in store_att_names if prop in active_obj.data] 45 | return object_custom_props 46 | 47 | def menu_func(self, context): 48 | self.layout.operator(copyCustomPropToSelection.bl_idname) 49 | 50 | def copyCustomProps(context): 51 | # get active object 52 | active_obj = bpy.context.active_object 53 | # get active object's custom properties 54 | active_obj_custom_props = getActiveObjProps(active_obj) 55 | # get selected objects 56 | selection = bpy.context.selected_objects 57 | # discriminates against active_obj 58 | selection = [obj for obj in selection if obj != active_obj] 59 | # for each object that's not active object, add custom prop 60 | for obj in selection: 61 | for prop in active_obj_custom_props: 62 | obj.data[prop] = active_obj.data[prop] 63 | 64 | def updateCustomProps(self, context): 65 | global last_selection 66 | global store_att_names 67 | for att in store_att_names: 68 | if (att in last_selection.Flags): # and last_selection.Flags[att] : 69 | if ( att not in last_selection.data or 70 | last_selection.Flags[att] != last_selection.data[att] ): 71 | last_selection.data[att] = last_selection.Flags[att] 72 | if (att in last_selection.Others): 73 | if ( att not in last_selection.data or 74 | last_selection.Others[att] != last_selection.data[att] ): 75 | last_selection.data[att] = last_selection.Others[att] 76 | @persistent 77 | def selection_callback(scene): 78 | global last_selection 79 | global store_att_names 80 | if bpy.context.active_object != last_selection: 81 | last_selection = bpy.context.active_object 82 | for groupName, attributeDefinitions in bpy.propertyGroupLayouts.items(): 83 | # build the attribute dictionary for this group 84 | attributes = {} 85 | for attributeDefinition in attributeDefinitions: 86 | attType = attributeDefinition['type'] 87 | attName = attributeDefinition['name'] 88 | store_att_names.append(attName) 89 | value = 0 90 | if last_selection: 91 | if attName in last_selection.data: 92 | value = last_selection.data[attName] 93 | if attType == 'boolean': 94 | attributes[attName] = BoolProperty(name=attName, default=value, update=updateCustomProps ) 95 | elif attType == 'int': 96 | attributes[attName] = IntProperty(name=attName, default=value, update=updateCustomProps) 97 | elif attType == 'string': 98 | attributes[attName] = StringProperty(name=attName, default=value, update=updateCustomProps) 99 | else: 100 | raise TypeError('Unsupported type (%s) for %s on %s!' % (attType, attName, groupName)) 101 | # now build the property group class 102 | propertyGroupClass = type(groupName, (PropertyGroup,), attributes) 103 | # register it with Blender 104 | bpy.utils.register_class(propertyGroupClass) 105 | # apply it to all Objects 106 | setattr(bpy.types.Object, groupName, PointerProperty(type=propertyGroupClass)) 107 | # store it for later 108 | bpy.samplePropertyGroups[groupName] = propertyGroupClass 109 | 110 | class copyCustomPropToSelection(bpy.types.Operator): 111 | """Copy last selected object's custom properties to other selected objects""" 112 | bl_idname = "object.copy_custom_properties" 113 | bl_label = "Copy custom properties to selection" 114 | 115 | @classmethod 116 | def poll(cls, context): 117 | return context.active_object is not None 118 | 119 | def execute(self, context): 120 | copyCustomProps(context) 121 | return {'FINISHED'} 122 | 123 | class customPropsPanel(bpy.types.Panel): 124 | bl_label = "3D Cam engine custom properties" 125 | bl_space_type = 'PROPERTIES' 126 | bl_region_type = 'WINDOW' 127 | bl_context = "data" 128 | 129 | def draw(self, context): 130 | layout = self.layout 131 | obj = context.object 132 | # use our layout definition to dynamically create our panel items 133 | for groupName, attributeDefinitions in sorted(bpy.propertyGroupLayouts.items()): 134 | # get the instance of our group 135 | # dynamic equivalent of `obj.samplePropertyGroup` from before 136 | propertyGroup = getattr(obj, groupName) 137 | # start laying this group out 138 | col = layout.column_flow(columns=2) 139 | col.label(groupName) 140 | i = 0 141 | # loop through all the attributes and show them 142 | for attributeDefinition in attributeDefinitions: 143 | if i == len(attributeDefinitions)/2: 144 | col.label("") 145 | col.prop(propertyGroup, attributeDefinition["name"]) 146 | i += 1 147 | # draw a separation between groups 148 | layout.separator() 149 | 150 | def register(): 151 | # register the panel class 152 | bpy.utils.register_class(customPropsPanel) 153 | bpy.app.handlers.scene_update_post.clear() 154 | bpy.app.handlers.scene_update_post.append(selection_callback) 155 | # copy helper 156 | bpy.utils.register_class(copyCustomPropToSelection) 157 | bpy.types.VIEW3D_MT_object.append(menu_func) 158 | # shortcut 159 | wm = bpy.context.window_manager 160 | km = wm.keyconfigs.addon.keymaps.new(name='Object Mode', space_type='EMPTY') 161 | kmi = km.keymap_items.new(copyCustomPropToSelection.bl_idname, 'P', 'PRESS', ctrl=False, shift=True) 162 | # ~ kmi.properties.total = 4 163 | addon_keymaps.append((km, kmi)) 164 | 165 | def unregister(): 166 | # unregister the panel class 167 | bpy.utils.unregister_class(customPropsPanel) 168 | # unregister our components 169 | try: 170 | for key, value in bpy.samplePropertyGroups.items(): 171 | delattr(bpy.types.Object, key) 172 | bpy.utils.unregister_class(value) 173 | except UnboundLocalError: 174 | pass 175 | bpy.samplePropertyGroups = {} 176 | # copy helper 177 | bpy.utils.unregister_class(copyCustomPropToSelection) 178 | bpy.types.VIEW3D_MT_object.remove(menu_func) 179 | # handle the keymap 180 | for km, kmi in addon_keymaps: 181 | km.keymap_items.remove(kmi) 182 | addon_keymaps.clear() 183 | 184 | if __name__ == "__main__": 185 | register() 186 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Pic or it didn't happen](https://wiki.arthus.net/assets/blender-psx.jpg) 2 | 3 | # Blender 3dcam PSX engine Level exporter 4 | 5 | This Blender plugin is to be used in conjunction with the [3dcam PSX engine](https://github.com/ABelliqueux/3dcam-headers). 6 | It allows exporting a gouraud shaded, UV textured Blender scene to a format compatible with the aforementionned engine. 7 | 8 | ![3d scene](https://wiki.arthus.net/assets/demo.gif) 9 | 10 | ## Documentation 11 | 12 | [Check the Wiki](https://github.com/ABelliqueux/blender_io_export_psx_mesh/wiki) for in-depth informations. 13 | 14 | ## Features 15 | 16 | **Be warned this is WIP** ! 17 | 18 | ### Plugin 19 | 20 | * Export UV textured models 21 | * Export vertex painted models 22 | * Export camera positions for in game use 23 | * Export vertex animations 24 | * Export up to 3 light sources 25 | * Export pre-rendered backgrounds for in-game use (8bpp and 4bpp) 26 | * VRam auto layout for TIMs 27 | * Export sound/music as VAG/XA files 28 | 29 | ![comparison](https://wiki.arthus.net/assets/rt-8b-4b.gif) 30 | Real-time 3D / 8bpp background / 4bpp background 31 | 32 | ## Planned 33 | 34 | * Fix and improve all the things ! 35 | 36 | # Install the plugin 37 | 38 | **This plugin is not compatible with Blender > 2.79.** 39 | 40 | 1. Download and install Blender 2.79b. 41 | 42 | http://download.blender.org/release/Blender2.79/ 43 | 44 | 2. Clone this repository in the [addons folder](https://docs.blender.org/manual/en/latest/advanced/blender_directory_layout.html) of blender 2.79 : 45 | 46 | ```bash 47 | git clone https://github.com/ABelliqueux/blender_io_export_psx_mesh.git 48 | ``` 49 | 50 | 3. Dependencies 51 | 52 | These utilities should be in your [$PATH](https://stackoverflow.com/questions/44272416/how-to-add-a-folder-to-path-environment-variable-in-windows-10-with-screensho#44272417) : 53 | 54 | * [pngquant](https://pngquant.org/) : convert image to 4/8bpp palettized pngs 55 | * [ffmpeg](https://ffmpeg.org/) : convert audio to WAV 56 | * [img2tim](https://github.com/Lameguy64/img2tim) : convert image to psx TIM - Win32 pre-built bin : https://github.com/Lameguy64/img2tim#download 57 | * [wav2vag](https://github.com/ColdSauce/psxsdk/blob/master/tools/wav2vag.c) : convert WAV to psx VAG - Win32 pre-built bin : http://psx.arthus.net/tools/wav2vag-win32.zip 58 | * [psxavenc](https://github.com/ABelliqueux/candyk-psx/tree/master/toolsrc/psxavenc) : convert WAV to psx XA - Win32 pre-built bin : http://psx.arthus.net/sdk/candyk-psx-tools.zip 59 | * [xainterleave](https://github.com/ABelliqueux/candyk-psx/tree/master/toolsrc/xainterleave) : interleave psx XA files - Win32 pre-built bin : http://psx.arthus.net/sdk/candyk-psx-tools.zip 60 | 61 | Linux users, these utilities are trivial to build using `gcc -o output source.c`. 62 | Only `psxavenc` and `img2tim` are a bit more involved as you should install the ffmpeg and freeimage dev packages from your distro before compiling. 63 | 64 | On Debian, 65 | 66 | ```bash 67 | sudo apt install libavformat-dev libfreeimage-dev 68 | ``` 69 | 70 | should set you up. Arch users, dev files are already on your system as long as the package is installed. 71 | 72 | ```bash 73 | sudo pacman -S ffmpeg freeimage 74 | ``` 75 | 76 | Building `img2tim` : 77 | 78 | ```bash 79 | # In img2tim's sources directory : 80 | gcc -o img2tim main.cpp 81 | ``` 82 | 83 | Building `psxavenc` and `xainterleave` : 84 | 85 | ```bash 86 | # Use the Makefile that's in candyk-psx's sources directory : 87 | make tools 88 | # bins will appear in 'candyk-psx/bin' 89 | ``` 90 | 91 | For users with **Imagemagick** installed, there is an option when exporting to use that instead of pngquant. 92 | 93 | 4. Enable the add-on in Blender by going to user preferences, Add-ons tab, and enable `Import-Export: PSX TMesh exporter`. 94 | 95 | On Linux : `~/.config/blender/2.79/scripts/addons` 96 | On macOS : `./Blender.app/Contents/Resources/2.79/addons` 97 | On Windows : `%USERPROFILE%\AppData\Roaming\Blender Foundation\Blender\2.93\` 98 | 99 | # Install the 3D engine 100 | 101 | Head over to the [3dcam repo](https://github.com/ABelliqueux/3dcam-headers) and follow the setup instructions there. 102 | 103 | # Export your scene ! 104 | 105 | Open a working copy of your scene, add the needed [flags](https://github.com/ABelliqueux/blender_io_export_psx_mesh/wiki/Flags) and export your level in the `3dcam-headers` folder. 106 | Following [those steps](https://github.com/ABelliqueux/3dcam-headers#compiling), you should now see your scene running on PSX ! 107 | 108 | # Custom properties helper add-on 109 | 110 | ## 3dcam-helper 111 | 112 | A [small blender addon](https://github.com/ABelliqueux/blender_io_export_psx_mesh/blob/main/3dcam-engine-helper.py) is provided that facilitates setting and copying [flags](https://github.com/ABelliqueux/blender_io_export_psx_mesh/wiki/Flags) between several objects in your scene. 113 | 114 | ![Setting an object's flags](https://wiki.arthus.net/assets/3dcam-helper-flags.gif) 115 | 116 | See [the documentation](https://github.com/ABelliqueux/blender_io_export_psx_mesh/wiki/Flags#3dcam-helper) for usage instruction. 117 | 118 | **The script only does the job of creating/updating the object's custom properties, so it is not mandatory to use it.** 119 | 120 | # Credits 121 | 122 | Based on the [code](https://pastebin.com/suU9DigB) provided by TheDukeOfZill, 04-2014, on http://www.psxdev.net/forum/viewtopic.php?f=64&t=537#p4088 123 | pngquant : [https://github.com/kornelski/pngquant](https://github.com/kornelski/pngquant) 124 | img2tim : [https://github.com/Lameguy64/img2tim](https://github.com/Lameguy64/img2tim) 125 | Freeimage : [https://freeimage.sourceforge.io/](https://freeimage.sourceforge.io/) 126 | -------------------------------------------------------------------------------- /blender-psx.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ABelliqueux/blender_io_export_psx_mesh/18b7e607bc2acbbcbbcf53dd943eaf2f0f82f40d/blender-psx.jpg -------------------------------------------------------------------------------- /io_export_psx_tmesh.py: -------------------------------------------------------------------------------- 1 | # bpy. app. debug = True 2 | bl_info = { 3 | "name": "PSX TMesh exporter", 4 | "author": "Schnappy, TheDukeOfZill", 5 | "blender": (2,7,9), 6 | "version": (0,0,4), 7 | "location": "File > Import-Export", 8 | "description": "Export psx data format", 9 | "category": "Import-Export" 10 | } 11 | import os 12 | import bpy 13 | import bmesh 14 | import unicodedata 15 | import subprocess 16 | from math import radians, degrees, floor, cos, sin, sqrt, ceil 17 | from mathutils import Vector 18 | from collections import defaultdict 19 | from bpy.props import (CollectionProperty, 20 | StringProperty, 21 | BoolProperty, 22 | EnumProperty, 23 | FloatProperty, 24 | IntProperty 25 | ) 26 | from bpy_extras.io_utils import (ExportHelper, 27 | axis_conversion) 28 | from bpy_extras.object_utils import world_to_camera_view 29 | from re import sub 30 | class ExportMyFormat(bpy.types.Operator, ExportHelper): 31 | bl_idname = "export_psx.c"; 32 | bl_label = "PSX compatible scene exporter"; 33 | bl_options = {'PRESET'}; 34 | filename_ext = ".c"; 35 | exp_Triangulate = BoolProperty( 36 | name="Triangulate meshes ( Destructive ! )", 37 | description="Triangulate meshes (destructive ! Do not use your original file)", 38 | default=False, 39 | ) 40 | exp_Scale = FloatProperty( 41 | name="Scale", 42 | description="Scale of exported mesh.", 43 | min=1, max=1000, 44 | default=65.0, 45 | ) 46 | exp_Precalc = BoolProperty( 47 | name="Use precalculated BGs", 48 | description="Render backgrounds and converts them to TIMs", 49 | default=False, 50 | ) 51 | # ~ exp_ShowPortals = BoolProperty( 52 | # ~ name="Render Portals in precalculated BGs", 53 | # ~ description="Useful for debugging", 54 | # ~ default=False, 55 | # ~ ) 56 | exp_useIMforTIM = BoolProperty( 57 | name = "Use ImageMagick", 58 | description = "Use installed Image Magick's convert tool to convert PNGs to 8/4bpp", 59 | default = False 60 | ) 61 | exp_convTexToPNG = BoolProperty( 62 | name = "Convert images to PNG", 63 | description = "Use installed Image Magick's convert tool to convert images to PNG.", 64 | default = True 65 | ) 66 | exp_TIMbpp = BoolProperty( 67 | name = "Use 4bpp TIMs", 68 | description = "Converts rendered backgrounds to 4bpp TIMs instead of the default 8bpp", 69 | default = False 70 | ) 71 | exp_LvlNbr = IntProperty( 72 | name="Level number", 73 | description="That number is used in the symbols name.", 74 | min=1, max=10, 75 | default=0, 76 | ) 77 | exp_expMode = BoolProperty( 78 | name="Use blend file directory for export", 79 | description="Files will be exported in the same folder as the blend file.", 80 | default=False, 81 | ) 82 | exp_CustomTexFolder = StringProperty( 83 | name = "Textures Dir", 84 | description = "By default, the script looks for / saves textures in the ./TEX folder. You can tell it to use a different folder.", 85 | default="TEX" 86 | ) 87 | exp_XAmode = IntProperty( 88 | name="XA mode", 89 | description ="XA sector size : 0 = 2352, 1=2336", 90 | min=0, max=1, 91 | default=1 92 | ) 93 | exp_isoCfg = StringProperty( 94 | name="mkpsxiso config folder", 95 | description = "Where should we look for mkpsxiso's config file ?", 96 | default= "." + os.sep + "config" + os.sep + "3dcam.xml" 97 | ) 98 | exp_CompressAnims = BoolProperty( 99 | name="Compress animation data", 100 | description="Use Delta/RLE compression on animations 's data.", 101 | default=False, 102 | ) 103 | exp_mixOverlapingStrips = BoolProperty( 104 | name="Mix overlaping nla animation tracks", 105 | description="If set, the resulting animation will be an interpolation between the overlapping nla tracks.", 106 | default = False, 107 | ) 108 | def execute(self, context): 109 | ### Globals declaration 110 | global nextTpage, freeTpage 111 | global nextClutSlot, freeClutSlot 112 | global tpageY 113 | global TIMbpp 114 | global timFolder 115 | global objAnims 116 | XAmode = self.exp_XAmode 117 | # Set Scale 118 | scale = self.exp_Scale 119 | ### Functions 120 | def psxLoc(location, scale=scale): 121 | return round(location * scale) 122 | def triangulate_object(obj): 123 | # Triangulate an object's mesh 124 | # Source : https://blender.stackexchange.com/questions/45698/triangulate-mesh-in-python/45722#45722 125 | me = obj.data 126 | # Get a BMesh representation 127 | bm = bmesh.new() 128 | bm.from_mesh(me) 129 | bmesh.ops.triangulate(bm, faces=bm.faces[:], quad_method=0, ngon_method=0) 130 | # Finish up, write the bmesh back to the mesh 131 | bm.to_mesh(me) 132 | bm.free() 133 | def CleanName(strName): 134 | # Removes specials characters, dots ans space from string 135 | name = strName.replace(' ','_') 136 | name = name.replace('.','_') 137 | name = unicodedata.normalize('NFKD',name).encode('ASCII', 'ignore').decode() 138 | return name 139 | ### Space utilities 140 | def isInFrame(scene, cam, target): 141 | # Checks if an object is in view frame 142 | position = world_to_camera_view(scene, cam, target.location) 143 | if ( 144 | (position.x < 0 or position.x > 1 ) or 145 | (position.y < 0 or position.y > 1 ) or 146 | (position.z < 0 ) 147 | ) : 148 | return False 149 | else: 150 | return True 151 | def isInPlane(plane, obj): 152 | # Checks if 'obj' has its coordinates contained between the plane's coordinate. 153 | # Obj is a dict 154 | # If 'obj' is contained, returns 1. 155 | # If 'obj' is partly contained, returns which side (S == 2, W == 4, N == 8, E == 6) it's overlapping. 156 | # If 'obj' is not contained in 'plane', returns 0. 157 | if ( 158 | (plane.get('x1') <= obj.get('x1') and plane.get('x2') >= obj.get('x2') ) and 159 | (plane.get('y1') <= obj.get('y1') and plane.get('y2') >= obj.get('y2') ) 160 | ): 161 | return 1 162 | # Overlap on the West side of the plane 163 | if ( 164 | ( plane.get('x1') >= obj.get('x1') and plane.get('x1') <= obj.get('x2') ) and 165 | ( plane.get('y1') <= obj.get('y2') and plane.get('y2') >= obj.get('y1') ) 166 | ): 167 | return 4 168 | # Overlap on the East side of the plane 169 | if ( 170 | ( plane.get('x2') <= obj.get('x2') and plane.get('x2') >= obj.get('x1') ) and 171 | ( plane.get('y1') <= obj.get('y2') and plane.get('y2') >= obj.get('y1') ) 172 | ): 173 | return 6 174 | # Overlap on the North side of the plane 175 | if ( 176 | ( plane.get('y2') <= obj.get('y2') and plane.get('y2') >= obj.get('y1') ) and 177 | ( plane.get('x1') <= obj.get('x1') and plane.get('x2') >= obj.get('x2') ) 178 | ): 179 | return 8 180 | # Overlap on the South side of the plane 181 | if ( 182 | ( plane.get('y1') >= obj.get('y1') and plane.get('y1') <= obj.get('y2') ) and 183 | ( plane.get('x1') <= obj.get('x1') and plane.get('x2') >= obj.get('x2') ) 184 | ): 185 | return 2 186 | else: 187 | return 0 188 | def getSepLine(plane, side): 189 | # Construct the line used for BSP generation from 'plane' 's coordinates, on specified side (S, W, N, E) 190 | # Returns an array of 3 values 191 | if side == 'N': 192 | return [ LvlPlanes[plane]['x1'], LvlPlanes[plane]['y2'], LvlPlanes[plane]['x2'], LvlPlanes[plane]['y2'] ] 193 | if side == 'S': 194 | return [ LvlPlanes[plane]['x1'], LvlPlanes[plane]['y1'], LvlPlanes[plane]['x2'], LvlPlanes[plane]['y1'] ] 195 | if side == 'W': 196 | return [ LvlPlanes[plane]['x1'], LvlPlanes[plane]['y1'], LvlPlanes[plane]['x1'], LvlPlanes[plane]['y2'] ] 197 | if side == 'E': 198 | return [ LvlPlanes[plane]['x2'], LvlPlanes[plane]['y1'], LvlPlanes[plane]['x2'], LvlPlanes[plane]['y2'] ] 199 | def checkLine(lineX1, lineY1 ,lineX2 ,lineY2, objX1, objY1, objX2, objY2): 200 | # Returns wether object spanning from objXY1 to objXY2 is Back, Front, Same or Intersecting the line 201 | # defined by points (lineXY1, lineXY2) 202 | val1 = ( objX1 - lineX1 ) * ( lineY2-lineY1 ) - ( objY1 - lineY1 ) * ( lineX2 - lineX1 ) 203 | # Rounding to avoid false positives 204 | val1 = round(val1, 4) 205 | val2 = ( objX2 - lineX1 ) * ( lineY2-lineY1 ) - ( objY2 - lineY1 ) * ( lineX2 - lineX1 ) 206 | val2 = round(val2, 4) 207 | if ( (val1 > 0) and (val2 > 0) ): 208 | return "front" 209 | elif ( (val1 < 0) and (val2 < 0) ): 210 | return "back" 211 | elif ( (val1 == 0) and (val2 == 0) ): 212 | return "connected" 213 | elif ( 214 | ( (val1>0) and (val2==0) ) or 215 | ( (val1==0) and (val2>0) ) 216 | ): 217 | return "front" 218 | elif ( 219 | ( (val1<0) and (val2==0) ) or 220 | ( (val1==0) and (val2<0) ) 221 | ): 222 | return "back" 223 | elif ( 224 | ( (val1<0) and (val2>0) ) or 225 | ( (val1>0) and (val2<0) ) 226 | ): 227 | return "intersect" 228 | def objVertLtoW(target): 229 | # Converts an object's vertices coordinates from local to global 230 | worldPos = [] 231 | mw = target.matrix_world 232 | mesh = bpy.data.meshes[ target.name ] 233 | for vertex in mesh.vertices: 234 | worldPos.append( mw * vertex.co * scale ) 235 | return worldPos 236 | def objVertWtoS(scene, cam, target, toScale = 1): 237 | # Converts an object's vertices coordinates from local to screen coordinates 238 | screenPos = [] 239 | # Get objects world matrix 240 | mw = target.matrix_world 241 | # Get object's mesh 242 | mesh = bpy.data.meshes[ target.name ] 243 | # For each vertex in mesh, get screen coordinates 244 | for vertex in mesh.vertices: 245 | # Get meshes world coordinates 246 | screenPos.append( world_to_camera_view( scene, cam, ( mw * vertex.co ) ) ) 247 | if toScale: 248 | # Get current scene rsolution 249 | resX = scene.render.resolution_x 250 | resY = scene.render.resolution_y 251 | # Scale values 252 | for vector in screenPos: 253 | # ~ vector.x = int( resX * vector.x ) < 0 ? 0 : int( resX * vector.x ) > 320 ? 320 : int( resX * vector.x ) 254 | vector.x = max ( 0, min ( resX, int( resX * vector.x ) ) ) 255 | vector.y = resY - max ( 0, min ( resY, int( resY * vector.y ) ) ) 256 | vector.z = int( vector.z ) 257 | return screenPos 258 | ### Texture utilities 259 | def convertBGtoTIM( filePathWithExt, colors = 256, bpp = 8, timX = 640, timY = 0, clutX = 0, clutY = 480, transparency = 'alpha'): 260 | global timFolder 261 | # By default, converts a RGB to 8bpp, 256 colors indexed PNG, then to a 8bpp TIM image 262 | filePathWithoutExt = filePathWithExt[ : filePathWithExt.rfind('.') ] 263 | ext = filePathWithExt[ filePathWithExt.rfind('.') + 1 : ] 264 | fileBaseName = os.path.basename(filePathWithoutExt) 265 | # For windows users, add '.exe' to the command 266 | exe = "" 267 | if os.name == 'nt': 268 | exe = ".exe" 269 | # 8bpp TIM needs < 256 colors 270 | if bpp == 8: 271 | # Clamp number of colors to 256 272 | colors = min( 255, colors ) 273 | elif bpp == 4: 274 | # 4bpp TIM needs < 16 colors 275 | # Clamp number of colors to 16 276 | colors = min( 16, colors ) 277 | if transparency == "alpha": 278 | transpMethod = "-usealpha" 279 | elif transparency == "black": 280 | transpMethod = "-b" 281 | elif transparency == "nonblack": 282 | transpMethod = "-t" 283 | # Image magick's convert can be used alternatively ( https://imagemagick.org/ ) 284 | if self.exp_useIMforTIM : 285 | # ImageMagick alternative 286 | subprocess.call( [ "convert" + exe, filePathWithExt, "-colors", str( colors ), filePathWithoutExt + ".png" ] ) 287 | filePathWithExt = filePathWithoutExt + ".png" 288 | print("Using IM on " + filePathWithExt) 289 | else: 290 | if self.exp_convTexToPNG: 291 | if ext != "png" or ext != "PNG": 292 | # Convert images to PNG 293 | subprocess.call( [ "convert" + exe, filePathWithExt, "-colors", str( colors ), filePathWithoutExt + ".png" ] ) 294 | filePathWithExt = filePathWithoutExt + ".png" 295 | # Quantization of colors with pngquant ( https://pngquant.org/ ) 296 | subprocess.run( [ "pngquant" + exe, "-v", "--force", str( colors ), filePathWithExt, "--ext", ".pngq" ] ) 297 | # Convert to tim with img2tim ( https://github.com/Lameguy64/img2tim ) 298 | subprocess.call( [ "img2tim" + exe, transpMethod, "-bpp", str( bpp ), "-org", str( timX ), str( timY ), "-plt" , str( clutX ), str( clutY ),"-o", timFolder + os.sep + fileBaseName + ".tim", filePathWithExt + "q" ] ) 299 | ### VRAM utilities 300 | def VramIsFull( size ): 301 | # Returns True if not enough space in Vram for image 302 | # Transpose bpp to bitshift value 303 | global nextTpage, freeTpage 304 | global nextClutSlot, freeClutSlot 305 | global tpageY 306 | if TIMbpp == 8: 307 | shift = 1 308 | elif TIMbpp == 4: 309 | shift = 2 310 | else: 311 | shift = 0 312 | # Get image width in vram 313 | if not size: 314 | imageWidth = size[0] >> shift 315 | else: 316 | imageWidth = size >> shift 317 | # Divide by cell width ( 64 pixels ) 318 | imageWidthInTPage = ceil( imageWidth / 64 ) 319 | if ( tpageY == 0 and 320 | nextTpage + ( imageWidthInTPage * 64 ) < 1024 and 321 | freeTpage - imageWidthInTPage > 0 322 | ) : 323 | return False 324 | elif ( tpageY == 256 and 325 | nextTpage + ( imageWidthInTPage * 64 ) < 960 and 326 | freeTpage - imageWidthInTPage > 1 327 | ) : 328 | return False 329 | else: 330 | return True 331 | def setNextTimPos( image ): 332 | # Sets nextTpage, freeTpage, tpageY, nextClutSlot, freeClutSlot to next free space in Vram 333 | # Transpose bpp to bitshift value 334 | global nextTpage, freeTpage 335 | global nextClutSlot, freeClutSlot 336 | global tpageY 337 | if TIMbpp == 8: 338 | shift = 1 339 | elif TIMbpp == 4: 340 | shift = 2 341 | else: 342 | shift = 0 343 | # Get image width in vram 344 | imageWidth = image.size[0] >> shift 345 | # Divide by cell width ( 64 pixels ) 346 | imageWidthInTPage = ceil( imageWidth / 64 ) 347 | if ( tpageY == 0 and 348 | nextTpage + ( imageWidthInTPage * 64 ) < 1024 and 349 | freeTpage - imageWidthInTPage > 0 350 | ) : 351 | nextTpage += imageWidthInTPage * 64 352 | freeTpage -= imageWidthInTPage 353 | nextClutSlot += 1 354 | freeClutSlot -= 1 355 | elif ( tpageY == 256 and 356 | nextTpage + ( imageWidthInTPage * 64 ) < 960 and 357 | freeTpage - imageWidthInTPage > 1 358 | ) : 359 | nextTpage += imageWidthInTPage * 64 360 | freeTpage -= imageWidthInTPage 361 | nextClutSlot += 1 362 | freeClutSlot -= 1 363 | else: 364 | tpageY = 256 365 | nextTpage = 320 366 | nextClutSlot += 1 367 | freeClutSlot -= 1 368 | def linearToRGB(component): 369 | # Convert linear Color in range 0.0-1.0 to range 0-255 370 | # https://www.color.org/bgsrgb.pdf 371 | a = 0.055 372 | if component <= 0.0031308: 373 | linear = component * 12.92 374 | else: 375 | linear = ( 1 + a ) * pow( component, 1 / 2.4 ) - a 376 | return linear 377 | ### Animation utilities 378 | def rmEmptyNLA( obj ): 379 | # Remove lna_tracks with no strips 380 | if obj.animation_data.nla_tracks: 381 | for track in obj.animation_data.nla_tracks: 382 | if not track.strips: 383 | obj.animation_data.nla_tracks.remove(track) 384 | 385 | def bakeActionToNLA( obj ): 386 | # Bake action to nla_track 387 | # Converting an action to nla_track makes it timeline independant. 388 | hasAnim = 0 389 | if obj.animation_data: 390 | # Get action 391 | objectAction = obj.animation_data.action 392 | # If action exists 393 | if objectAction: 394 | # Create new nla_track 395 | nlaTrack = obj.animation_data.nla_tracks.new() 396 | # Create new strip from action 397 | nlaTrack.strips.new( objectAction.name, objectAction.frame_range[0], objectAction ) 398 | # Remove action 399 | obj.animation_data.action = None 400 | hasAnim = 1 401 | rmEmptyNLA(obj) 402 | return hasAnim 403 | 404 | def getTrackList(obj, parent): 405 | # Build a dictionary of object's nla tracks and strips 406 | # Dict data structure is like so: 407 | # objDict[ ][ ][ ] 408 | # objAnims is a defaultdict(dict) 409 | global objAnims 410 | if obj.animation_data: 411 | # Get nla tracks 412 | objTracks = obj.animation_data.nla_tracks 413 | for track in objTracks: 414 | for strip in track.strips: 415 | # If track struct exists in objAnims[parent], add strip to list 416 | if track in objAnims[parent]: 417 | if strip not in objAnims[parent][track]: 418 | objAnims[parent][track].append(strip) 419 | # If it doesn't, create dict item 'track' and initialize it to a list that contains the current strip 420 | else: 421 | objAnims[parent][track] = [strip] 422 | def getStripsTotal(objList): 423 | stripsTotal = [] 424 | for track in objList: 425 | for strip in objList[track]: 426 | stripsTotal.append(strip) 427 | return stripsTotal 428 | 429 | def findOverlappingTrack(obj): 430 | # Find overlapping strips through all the tracks 431 | # Get all strips 432 | tmpStrips = [] 433 | overlappingStrips = defaultdict(dict) 434 | for track in obj: 435 | for strip in obj[track]: 436 | tmpStrips.append(strip) 437 | # Check each strip for overlapping 438 | for tmpStrip in tmpStrips: 439 | # Find other strips 440 | otherStrips = [ otherStrip for otherStrip in tmpStrips if otherStrip is not tmpStrip ] 441 | for otherStrip in otherStrips: 442 | # If strips are overlapping 443 | if otherStrip.frame_start < tmpStrip.frame_end : 444 | if otherStrip.frame_end > tmpStrip.frame_start: 445 | # Add to list, unless already there 446 | if otherStrip in overlappingStrips: 447 | if tmpStrip not in overlappingStrips: 448 | overlappingStrips[otherStrip].append(tmpStrip) 449 | else: 450 | if tmpStrip not in overlappingStrips: 451 | overlappingStrips[otherStrip] = [tmpStrip] 452 | return overlappingStrips 453 | 454 | def writeMESH_ANIMS(f, obj, stripList, fileName): 455 | stripsTotal = len(stripList) 456 | symbolName = fileName + "_model" + CleanName(obj.data.name) + "_anims" 457 | f.write("MESH_ANIMS_TRACKS " + symbolName + " = {\n" + 458 | "\t" + str( stripsTotal ) + ",\n" + 459 | "\t{\n") 460 | i = 0 461 | for strip in stripList: 462 | f.write("\t\t&" + fileName + "_model" + CleanName(obj.data.name) + "_anim_" + CleanName(strip.name)) 463 | if i < stripsTotal - 1: 464 | f.write(",\n") 465 | else: 466 | f.write("\n") 467 | i += 1 468 | f.write("\t}\n};\n\n") 469 | return str( "MESH_ANIMS_TRACKS " + symbolName ) 470 | 471 | def writeVANIM(f, obj, strip, fileName, strip_start, strip_end, compress=False): 472 | # write the VANIM portion of a MESH_ANIMS struct declaration 473 | # Get strip total length 474 | # ~ print(strip.name) 475 | strip_len = strip_end - strip_start 476 | # Iteration counter 477 | i = 0; 478 | # Store temporary mesh in list for cleaning later 479 | tmp_mesh = [] 480 | frameList = [] 481 | for frame in range(int(strip_start), int(strip_end)): 482 | # Set current frame 483 | bpy.context.scene.frame_set(frame) 484 | # Update scene view 485 | bpy.context.scene.update() 486 | # Create a copy of the mesh with modifiers applied 487 | objMod = obj.to_mesh(bpy.context.scene, True, 'PREVIEW') 488 | # Get isLerp flag 489 | lerp = 0 490 | if 'isLerp' in obj.data: 491 | lerp = obj.data['isLerp'] 492 | # Write VANIM struct 493 | symbolName = fileName + "_model" + CleanName(obj.data.name) + "_anim_" + CleanName(strip.name) 494 | if frame == strip_start : 495 | f.write("VANIM " + symbolName + " = {\n" + 496 | "\t" + str(int(strip_len)) + ", // number of frames e.g 20\n" + 497 | "\t" + str(len(objMod.vertices)) + ", // number of vertices e.g 21\n" + 498 | "\t-1, // anim cursor : -1 means not playing back\n" + 499 | "\t0, // lerp cursor\n" + 500 | "\t0, // loop : if -1 , infinite loop, if n > 0, loop n times\n" + 501 | "\t1, // playback direction (1 or -1)\n" + 502 | "\t0, // ping pong animation (A>B>A)\n" + 503 | "\t" + str(lerp) + ", // use lerp to interpolate keyframes\n" + 504 | "\t{ // vertex pos as BVECTORs e.g 20 * 21 BVECTORS\n" 505 | ) 506 | # Add an empty list to the frame list 507 | frameList.append([]) 508 | currentFrameNbr = int(frame - strip_start) 509 | currentFrameItem = frameList[currentFrameNbr] 510 | if currentFrameNbr > 0: 511 | previousFrameItem = frameList[currentFrameNbr - 1] 512 | else: 513 | # If first iteration, use currentFrameItem 514 | previousFrameItem = currentFrameItem 515 | # Get vertices coordinates as a VECTORs 516 | for vertIndex in range(len(objMod.vertices)): 517 | # Store current vertex coords 518 | currentVertex = Vector( ( round( objMod.vertices[ vertIndex ].co.x * scale), round( -objMod.vertices[ vertIndex ].co.z * scale), round( objMod.vertices[ vertIndex ].co.y * scale) ) ) 519 | # Add current vertex to current frame item 520 | currentFrameItem.append(currentVertex) 521 | # If compressing anim 522 | if self.exp_CompressAnims: 523 | # Find delta between current frame and previous frame 524 | delta = currentFrameItem[vertIndex] - previousFrameItem[vertIndex] 525 | currentVertex = delta 526 | # Readability : if first vertex of the frame, write frame number as a comment 527 | if vertIndex == 0: 528 | f.write("\t\t//Frame " + str(currentFrameNbr) + "\n") 529 | # Write vertex coordinates x,z,y 530 | f.write( "\t\t{ " + str(int(currentVertex.x)) + 531 | "," + str(int(currentVertex.y)) + 532 | "," + str(int(currentVertex.z)) + 533 | " }" ) 534 | # If vertex is not the last in the list, write a comma 535 | if i != ( len(objMod.vertices) * (strip_len) * 3 ) - 3: 536 | f.write(",\n") 537 | # Readability : If vertex is the last in frame, insert a blank line 538 | if vertIndex == len(objMod.vertices) - 1: 539 | f.write("\n") 540 | # Increment counter 541 | i += 3; 542 | # Add temporary mesh to the cleaning list 543 | tmp_mesh.append( objMod ) 544 | # Close anim declaration 545 | f.write("\t}\n};\n\n") 546 | # ~ print(frameList) 547 | # Remove temporary meshes 548 | for o in tmp_mesh: 549 | bpy.data.meshes.remove( o ) 550 | return str( "VANIM " + symbolName ) 551 | 552 | ### Sound utilities 553 | class Sound: 554 | def __init__(self, objName, soundName, soundPath, convertedSoundPath, parent, location, volume, volume_min, volume_max, index, XAfile=-1, XAchannel=-1, XAsize=-1, XAend=-1): 555 | self.objName = objName 556 | self.soundName = soundName 557 | self.soundPath = soundPath 558 | self.convertedSoundPath = convertedSoundPath 559 | self.parent = parent 560 | self.location = location 561 | self.volume = volume 562 | self.volume_min = volume_min 563 | self.volume_max = volume_max 564 | self.index = index 565 | self.XAfile = XAfile 566 | self.XAchannel = XAchannel 567 | self.XAsize = XAsize 568 | self.XAend = XAend 569 | def __eq__(self, other): 570 | return self.convertedSoundPath == other.convertedSoundPath 571 | 572 | def sound2XA( soundPath, soundName, soundFolder="XA", bpp=4, XAfile=0, XAchannel=0 ): 573 | # Convert sound file to XA 574 | # exports in ./XA by default 575 | # ffmpeg -i input.mp3 -acodec pcm_s16le -ac 2 -ar 44100 output.wav 576 | # psxavenc -f 37800 -t xa -b 4 -c 2 -F 1 -C 0 "../hello_cdda/audio/beach.wav" "xa/beach.xa" 577 | exe = "" 578 | if os.name == 'nt': 579 | exe = ".exe" 580 | # find export folder 581 | filepath = self.filepath 582 | # ~ filepath = bpy.data.filepath 583 | expFolder = os.path.dirname(bpy.path.abspath(filepath)) + os.sep + soundFolder + os.sep 584 | # create if non-existent 585 | if not os.path.exists(expFolder): 586 | os.mkdir(expFolder) 587 | # find file base name 588 | basename = soundName.split('.')[0] 589 | exportPath = expFolder + basename + ".xa" 590 | # Convert to 16-B WAV 591 | subprocess.call( [ "ffmpeg" + exe, "-i", soundPath, "-acodec", "pcm_s16le", "-ac", "2", "-ar", "44100", "-y", "/tmp/tmp.wav"] ) 592 | # Convert WAV to XA 593 | subprocess.call( [ "psxavenc" + exe, "-f", "37800", "-t", "xa", "-b", str(bpp), "-c", "2", "-F", str(XAfile), "-C", str(XAchannel), "/tmp/tmp.wav", exportPath ] ) 594 | return exportPath 595 | 596 | def XAmanifest(XAlist, soundFolder="XA", XAchannels=8): 597 | # generate manifest file 598 | # find export folder 599 | filepath = self.filepath 600 | expFolder = os.path.dirname(bpy.path.abspath(filepath)) + os.sep + soundFolder + os.sep 601 | XAfiles = [] 602 | for file_index in range(len(XAlist)): 603 | manifestFile = open(os.path.normpath(expFolder + "inter_" + str(file_index) + ".txt" ), "w+") 604 | # ~ print("\nFile_" + str(file_index) + " :") 605 | lines = XAchannels 606 | for xa in XAlist[file_index]: 607 | manifestFile.write( str(XAmode) + " xa " + xa.convertedSoundPath + " " + str(xa.XAfile) + " " + str(xa.XAchannel) + "\n" ) 608 | lines -= 1 609 | while lines: 610 | manifestFile.write( str(XAmode) + " null\n") 611 | lines -= 1 612 | manifestFile.close() 613 | 614 | def writeIsoCfg(configFile, insertString): 615 | # Write insertString one line above searchString 616 | print(configFile) 617 | print(insertString) 618 | searchString = "\n' 640 | writeIsoCfg(configFile, insertString) 641 | 642 | def XAinterleave(XAlist, soundFolder="XA"): 643 | # Generate interleaved XA files from existing XA files referenced in soundFiles 644 | exe = "" 645 | if os.name == 'nt': 646 | exe = ".exe" 647 | # find export folder 648 | filepath = self.filepath 649 | for xa in range(len(XAlist)): 650 | manifestFile = expFolder + "inter_" + str(xa) + ".txt" 651 | outputFile = expFolder + "inter_" + str(xa) + ".xa" 652 | subprocess.call( [ "xainterleave" + exe, str(XAmode), manifestFile, outputFile ]) 653 | 654 | def sound2VAG( soundPath, soundName, soundFolder="VAG"): 655 | # Convert sound file to VAG 656 | # exports in ./VAG by default 657 | # For windows users, add '.exe' to the command 658 | exe = "" 659 | if os.name == 'nt': 660 | exe = ".exe" 661 | # find export folder 662 | filepath = self.filepath 663 | # ~ filepath = bpy.data.filepath 664 | expFolder = os.path.dirname(bpy.path.abspath(filepath)) + os.sep + soundFolder + os.sep 665 | # create if non-existent 666 | if not os.path.exists(expFolder): 667 | os.mkdir(expFolder) 668 | # find file base name 669 | basename = soundName.split('.')[0] 670 | exportPath = expFolder + basename + ".vag" 671 | # Convert to RAW WAV data 672 | subprocess.call( [ "ffmpeg" + exe, "-i", soundPath, "-f", "s16le", "-ac", "1", "-ar", "44100", "-y", "/tmp/tmp.dat"] ) 673 | # Convert WAV to VAG 674 | subprocess.call( [ "wav2vag" + exe, "/tmp/tmp.dat", exportPath, "-sraw16", "-freq=44100" ] ) 675 | return exportPath 676 | 677 | def writeExtList(f, soundName, level_symbols): 678 | soundName = soundName.split('.')[0] 679 | f.write("extern u_char _binary_VAG_" + soundName + "_vag_start;\n") 680 | 681 | def writeVAGbank(f, soundList, level_symbols): 682 | index = 0 683 | SPU = 0 684 | dups = [] 685 | for file_index in range(len(soundList)): 686 | if soundList[file_index].XAsize == -1 : 687 | if soundList[file_index] not in dups: 688 | writeExtList(f, soundList[file_index].soundName, level_symbols) 689 | dups.append(soundList[file_index]) 690 | index += 1 691 | f.write("\nVAGbank " + fileName + "_VAGBank = {\n" + 692 | "\t" + str(index) + ",\n" + 693 | "\t{\n") 694 | for sound in soundList: 695 | if sound.XAsize == -1: 696 | f.write("\t\t{ &_binary_VAG_" + sound.soundName.split('.')[0] + "_vag_start, SPU_0" + str(SPU) + "CH, 0 }") 697 | if SPU < index - 1: 698 | f.write(",\n") 699 | sound.index = SPU 700 | SPU += 1 701 | f.write("\n\t}\n};\n\n" ) 702 | level_symbols.append("VAGbank " + fileName + "_VAGBank") 703 | # If SPU, we're using VAGs 704 | return SPU 705 | 706 | def writeXAbank(f, XAfiles, level_symbols): 707 | index = 0 708 | XAinter = [] 709 | # ~ soundName = objName.split('.')[0] 710 | for file_index in range(len(XAfiles)): 711 | if XAfiles[file_index].XAsize != -1: 712 | index += 1 713 | if XAfiles[file_index].XAfile not in range( len( XAinter ) ) : 714 | XAinter.append( list() ) 715 | XAinter[ XAfiles[file_index].XAfile ].append(XAfiles[file_index]) 716 | for XAlistIndex in range(len(XAinter)): 717 | f.write("XAbank " + fileName + "_XABank_" + str(XAlistIndex) + " = {\n" + 718 | "\t\"\\\\INTER_" + str(XAlistIndex) + ".XA;1\",\n" + 719 | "\t" + str(len(XAinter[XAlistIndex])) + ",\n" + 720 | "\t0,\n" + 721 | "\t{\n") 722 | index = 0 723 | for sound in XAinter[XAlistIndex]: 724 | if sound.XAsize != -1: 725 | f.write( "\t\t{ " + str(index) + ", " + str(sound.XAsize) + ", " + str(sound.XAfile) + ", " + str(sound.XAchannel) + ", 0, " + str(sound.XAend) + " * XA_CHANNELS, -1 },\n" ) 726 | sound.index = index 727 | index += 1 728 | f.write( "\t}\n};\n" ) 729 | level_symbols.append("XAbank " + fileName + "_XABank_" + str(XAlistIndex)) 730 | return XAinter 731 | 732 | def writeXAfiles(f, XAlist, fileName): 733 | # Write XAFiles struct 734 | f.write("XAfiles " + fileName + "_XAFiles = {\n" + 735 | "\t" + str(len(XAlist)) + ",\n" + 736 | "\t{\n") 737 | if XAlist: 738 | for xa in range(len(XAlist)): 739 | f.write("\t\t&" + fileName + "_XABank_" + str(xa)) 740 | if xa < len(XAlist) - 1: 741 | f.write(",") 742 | else: 743 | f.write("\t\t0") 744 | f.write("\n\t}\n};\n") 745 | level_symbols.append("XAfiles " + fileName + "_XAFiles") 746 | 747 | def writeSoundObj(f, soundFiles, level_symbols): 748 | index = 0 749 | # Write SOUND_OBJECT structures 750 | for obj in soundFiles: 751 | f.write("SOUND_OBJECT " + fileName + "_" + obj.objName.replace(".", "_") + " = {\n" + 752 | "\t{" + str(psxLoc(obj.location.x)) + "," + str(psxLoc(obj.location.y)) + "," + str(psxLoc(obj.location.z)) + "},\n" + 753 | "\t" + str(obj.volume * 0x3fff) + ", " + str(obj.volume * 0x3fff) + ", " + str(obj.volume_min * 0x3fff) + ", " + str(obj.volume_max * 0x3fff) + ",\n" ) 754 | if obj.XAsize == -1 : 755 | f.write("\t&" + fileName + "_VAGBank.samples[" + str(obj.index) + "],\n" + 756 | "\t0,\n") 757 | else: 758 | f.write("\t0,\n" + 759 | "\t&" + fileName + "_XABank_" + str(obj.XAfile) + ".samples[" + str(obj.index) + "],\n") 760 | if obj.parent: 761 | f.write( "\t&" + fileName + "_mesh" + CleanName(obj.parent.name) + "\n") 762 | else: 763 | f.write("\t0\n") 764 | f.write("};\n\n") 765 | index += 1 766 | level_symbols.append("SOUND_OBJECT " + fileName + "_" + obj.objName.replace(".", "_")) 767 | f.write("LEVEL_SOUNDS " + fileName + "_sounds = {\n" + 768 | "\t" + str(index) + ",\n" + 769 | "\t{\n") 770 | for obj in range(len(soundFiles)): 771 | f.write( "\t\t&" + fileName + "_" + soundFiles[obj].objName.replace(".", "_")) 772 | if obj < len(soundFiles) - 1 : 773 | f.write(",\n") 774 | f.write("\n\t}\n};\n\n") 775 | level_symbols.append("LEVEL_SOUNDS " + fileName + "_sounds") 776 | return index 777 | # Set rendering resolution to 320x240 778 | bpy.context.scene.render.resolution_x = 320 779 | bpy.context.scene.render.resolution_y = 240 780 | ### VRam Layout 781 | nextTpage = 320 782 | nextClutSlot = 480 783 | freeTpage = 21 784 | freeClutSlot = 32 785 | tpageY = 0 786 | # Set TIMs default bpp value 787 | TIMbpp = 8 788 | TIMshift = 1 789 | if self.exp_TIMbpp: 790 | TIMbpp = 4 791 | TIMshift = 2 792 | # Set context area to 3d view 793 | previousAreaType = 0 794 | if bpy.context.mode != 'OBJECT' : 795 | previousAreaType = bpy.context.area.type 796 | bpy.context.area.type="VIEW_3D" 797 | if bpy.context.object is None: 798 | # select first object in scene 799 | bpy.context.scene.objects.active = bpy.context.scene.objects[0] 800 | # Leave edit mode to avoid errors 801 | bpy.ops.object.mode_set(mode='OBJECT') 802 | # restore previous area type 803 | bpy.context.area.type = previousAreaType 804 | # If set, triangulate objects of type mesh 805 | if self.exp_Triangulate: 806 | for o in range(len(bpy.data.objects)): 807 | if bpy.data.objects[o].type == 'MESH': 808 | triangulate_object(bpy.data.objects[o]) 809 | # Get export directory path 810 | filepath = self.filepath 811 | if self.exp_expMode: 812 | filepath = bpy.data.filepath 813 | expFolder = os.path.dirname(bpy.path.abspath(filepath)) 814 | # If the file wasn't saved before, expFolder will be empty. Default to current directory in that case 815 | if expFolder == "": 816 | expFolder = os.getcwd() 817 | # Get texture folder, default to ./TEX 818 | textureFolder = os.path.join( expFolder, "TEX") 819 | if self.exp_CustomTexFolder != "TEX": 820 | textureFolder = os.path.join( expFolder, self.exp_CustomTexFolder) 821 | timFolder = os.path.join( expFolder, "TIM") 822 | # If the TIM folder doesn't exist, create it 823 | if not os.path.exists(timFolder): 824 | os.mkdir(timFolder) 825 | ### Export pre-calculated backgrounds and construct a list of visible objects for each camera angle 826 | camAngles = [] 827 | defaultCam = 'NULL' 828 | # List of Rigid/Static bodies to ray a cast upon 829 | rayTargets = [] 830 | # If using precalculated BG, render and export them to ./TIM/ 831 | if self.exp_Precalc: 832 | # Get BGs TIM size depending on mode 833 | timSize = bpy.context.scene.render.resolution_x >> TIMshift 834 | timSizeInCell = ceil( timSize / 64 ) 835 | # Create folder if it doesn't exist 836 | # ~ os.makedirs(timFolder, exist_ok = 1) 837 | # Set file format config 838 | bpy.context.scene.render.image_settings.file_format = 'PNG' 839 | # ~ bpy.context.scene.render.image_settings.quality = 100 840 | # ~ bpy.context.scene.render.image_settings.compression = 0 841 | bpy.context.scene.render.image_settings.color_depth = '8' 842 | bpy.context.scene.render.image_settings.color_mode = 'RGB' 843 | # Get active cam 844 | scene = bpy.context.scene 845 | cam = scene.camera 846 | # Find default cam, and cameras in camPath 847 | for o in bpy.data.objects: 848 | # If orphan, ignore 849 | if o.users == 0: 850 | continue 851 | if o.type == 'CAMERA' and o.data.get('isDefault'): 852 | defaultCam = o.name 853 | if o.type == 'CAMERA' and o.name.startswith("camPath"): 854 | filepath = textureFolder + os.sep 855 | filename = "bg_" + CleanName(o.name) 856 | fileext = "." + str(bpy.context.scene.render.image_settings.file_format).lower() 857 | # Set camera as active 858 | bpy.context.scene.camera = o 859 | # Render and save image 860 | bpy.ops.render.render() 861 | bpy.data.images["Render Result"].save_render( filepath + filename + fileext ) 862 | # Convert PNG to TIM 863 | if not VramIsFull( bpy.context.scene.render.resolution_x ): 864 | convertBGtoTIM( filepath + filename + fileext , bpp = TIMbpp, timX = nextTpage, timY = tpageY, clutY = nextClutSlot, transparency = "nonblack" ) 865 | else: 866 | tpageY = 256 867 | nextTpage = 320 868 | if not VramIsFull( bpy.context.scene.render.resolution_x ): 869 | convertBGtoTIM( filepath + filename + fileext , bpp = TIMbpp, timX = nextTpage, timY = tpageY, clutY = nextClutSlot, transparency = "nonblack" ) 870 | # Add camera object to camAngles 871 | camAngles.append(o) 872 | # Notify layout change to vars 873 | nextTpage += timSizeInCell * 64 874 | freeTpage -= timSizeInCell 875 | nextClutSlot += 1 876 | freeClutSlot -= 1 877 | ### Start writing output files 878 | # Stolen from Lameguy64 : https://github.com/Lameguy64/Blender-RSD-Plugin/blob/b3b6fd4475aed4ca38587ca83d34000f60b68a47/io_export_rsd.py#L68 879 | filepath = self.filepath 880 | filepath = filepath.replace(self.filename_ext, "") # Quick fix to get around the aforementioned 'bugfix' 881 | # TODO : add option to export scenes as levels 882 | # ~ if self.exp_UseScenesAsLevels: 883 | # ~ fileName = cleanName(bpy.data.scenes[0].name) 884 | # ~ else: 885 | # 886 | # We're writing a few files: 887 | # - custom_types.h contains the 'engine' 's specific struct definitions 888 | # - level.h contains the forward declaration of the level's variables 889 | # - level.c contains the initialization and data of those variables 890 | # 'custom_types.h' goes in export folder 891 | custom_types_h = expFolder + os.sep + 'custom_types.h' 892 | # If export mode is set to Use blender file name 893 | # ~ if self.exp_expMode: 894 | # ~ fileName = bpy.path.basename(filepath) 895 | # ~ filepath = self.filepath 896 | # ~ folder = os.path.dirname(bpy.path.abspath(filepath)) 897 | # ~ levels_folder = folder + os.sep 898 | # ~ else: 899 | lvlNbr = self.exp_LvlNbr 900 | fileName = 'level' + str( lvlNbr ) 901 | # Levels files go in ./levels/ 902 | # If ./levels does not exist, create it 903 | if not os.path.exists( expFolder + os.sep + 'levels'): 904 | os.mkdir( expFolder + os.sep + 'levels') 905 | levels_folder = expFolder + os.sep + 'levels' + os.sep 906 | # TODO : dynamic filenaming 907 | level_h = levels_folder + fileName + '.h' 908 | level_c = levels_folder + fileName + '.c' 909 | ### Custom types Header (custom_types.h) 910 | # Open file 911 | h = open(os.path.normpath(custom_types_h),"w+") 912 | ## Add C structures definitions 913 | h.write( 914 | "#pragma once\n" + 915 | "#include \n" + 916 | "#include \n" + 917 | "#include \n" + 918 | "#include \n\n" 919 | ) 920 | # Partial declaration of structures to avoid inter-dependencies issues 921 | h.write("struct BODY;\n" + 922 | "struct BVECTOR;\n" + 923 | "struct VANIM;\n" + 924 | "struct MESH_ANIMS_TRACKS;\n" + 925 | "struct PRIM;\n" + 926 | "struct MESH;\n" + 927 | "struct CAMPOS;\n" + 928 | "struct CAMPATH;\n" + 929 | "struct CAMANGLE;\n" + 930 | "struct SIBLINGS;\n" + 931 | "struct CHILDREN;\n" + 932 | "struct NODE;\n" + 933 | "struct QUAD;\n" + 934 | "struct LEVEL;\n" + 935 | "struct VAGsound;\n" + 936 | "struct VAGbank;\n" + 937 | "struct XAsound;\n" + 938 | "struct XAbank;\n" + 939 | "struct XAfiles;\n" + 940 | "struct SOUND_OBJECT;\n" + 941 | "struct LEVEL_SOUNDS;\n" + 942 | "\n") 943 | # BODY 944 | h.write("typedef struct BODY {\n" + 945 | "\tVECTOR gForce;\n" + 946 | "\tVECTOR position;\n" + 947 | "\tSVECTOR velocity;\n" + 948 | "\tint mass;\n" + 949 | "\tint invMass;\n" + 950 | "\tVECTOR min; \n" + 951 | "\tVECTOR max; \n" + 952 | "\tint restitution; \n" + 953 | # ~ "\tstruct NODE * curNode; \n" + 954 | "\t} BODY;\n\n") 955 | # VANIM 956 | h.write("typedef struct BVECTOR {\n" + 957 | "\tint8_t vx, vy;\n" + 958 | "\tint8_t vz;\n" + 959 | "\t// int8_t factor; // could be useful for anims where delta is > 256 \n" + 960 | "} BVECTOR;\n\n") 961 | 962 | h.write("typedef struct VANIM { \n" + 963 | "\tint nframes; // number of frames e.g 20\n" + 964 | "\tint nvert; // number of vertices e.g 21\n" + 965 | "\tint cursor; // anim cursor : -1 == not playing, n>=0 == current frame number\n" + 966 | "\tint lerpCursor; // anim cursor\n" + 967 | "\tint loop; // loop anim : -1 == infinite, n>0 == play n times\n" + 968 | "\tint dir; // playback direction (1 or -1)\n" + 969 | "\tint pingpong; // ping pong animation (A>B>A)\n" + 970 | "\tint interpolate; // use lerp to interpolate keyframes\n" + 971 | "\tBVECTOR data[]; // vertex pos as SVECTORs e.g 20 * 21 SVECTORS\n" + 972 | "\t} VANIM;\n\n") 973 | 974 | h.write("typedef struct MESH_ANIMS_TRACKS {\n" + 975 | "\tu_short index;\n" + 976 | "\tVANIM * strips[];\n" + 977 | "} MESH_ANIMS_TRACKS;\n\n" ) 978 | # PRIM 979 | h.write("typedef struct PRIM {\n" + 980 | "\tVECTOR order;\n" + 981 | "\tint code; // Same as POL3/POL4 codes : Code (F3 = 1, FT3 = 2, G3 = 3,\n// GT3 = 4) Code (F4 = 5, FT4 = 6, G4 = 7, GT4 = 8)\n" + 982 | "\t} PRIM;\n\n") 983 | # MESH 984 | h.write("typedef struct MESH { \n"+ 985 | "\tint totalVerts;\n" + 986 | "\tTMESH * tmesh;\n" + 987 | "\tPRIM * index;\n" + 988 | "\tTIM_IMAGE * tim; \n" + 989 | "\tunsigned long * tim_data;\n"+ 990 | "\tMATRIX mat;\n" + 991 | "\tVECTOR pos;\n" + 992 | "\tSVECTOR rot;\n" + 993 | "\tshort isProp;\n" + 994 | "\tshort isRigidBody;\n" + 995 | "\tshort isStaticBody;\n" + 996 | "\tshort isRound;\n" + 997 | "\tshort isPrism;\n" + 998 | "\tshort isAnim;\n" + 999 | "\tshort isActor;\n" + 1000 | "\tshort isLevel;\n" + 1001 | "\tshort isWall;\n" + 1002 | "\tshort isBG;\n" + 1003 | "\tshort isSprite;\n" + 1004 | "\tlong p;\n" + 1005 | "\tlong OTz;\n" + 1006 | "\tBODY * body;\n" + 1007 | "\tMESH_ANIMS_TRACKS * anim_tracks;\n" + 1008 | "\tVANIM * currentAnim;\n" + 1009 | "\tstruct NODE * node;\n" + 1010 | "\tVECTOR pos2D;\n" + 1011 | "\t} MESH;\n\n") 1012 | #QUAD 1013 | h.write("typedef struct QUAD {\n" + 1014 | "\tVECTOR v0, v1;\n" + 1015 | "\tVECTOR v2, v3;\n" + 1016 | "\t} QUAD;\n\n") 1017 | # CAMPOS 1018 | h.write("typedef struct CAMPOS {\n" + 1019 | "\tSVECTOR pos;\n" + 1020 | "\tSVECTOR rot;\n" + 1021 | "\t} CAMPOS;\n\n" + 1022 | "\n// Blender cam ~= PSX cam with these settings : \n" + 1023 | "// NTSC - 320x240, PAL 320x256, pixel ratio 1:1,\n" + 1024 | "// cam focal length : perspective 90° ( 16 mm ))\n" + 1025 | "// With a FOV of 1/2, camera focal length is ~= 16 mm / 90°\n" + 1026 | "// Lower values mean wider angle\n\n") 1027 | # CAMANGLE 1028 | h.write("typedef struct CAMANGLE {\n" + 1029 | "\tCAMPOS * campos;\n" + 1030 | "\tTIM_IMAGE * BGtim;\n" + 1031 | "\tunsigned long * tim_data;\n" + 1032 | "\tQUAD bw, fw;\n" + 1033 | "\tint index;\n" + 1034 | "\tMESH * objects[];\n" + 1035 | "\t} CAMANGLE;\n\n") 1036 | # CAMPATH 1037 | h.write("typedef struct CAMPATH {\n" + 1038 | "\tshort len, cursor, pos;\n" + 1039 | "\tVECTOR points[];\n" + 1040 | "\t} CAMPATH;\n\n") 1041 | # SIBLINGS 1042 | h.write("typedef struct SIBLINGS {\n" + 1043 | "\tint index;\n" + 1044 | "\tstruct NODE * list[];\n" + 1045 | "\t} SIBLINGS ;\n\n") 1046 | # CHILDREN 1047 | h.write("typedef struct CHILDREN {\n" + 1048 | "\tint index;\n" + 1049 | "\tMESH * list[];\n" + 1050 | "\t} CHILDREN ;\n\n") 1051 | # NODE 1052 | h.write("typedef struct NODE {\n" + 1053 | "\tMESH * plane;\n" + 1054 | "\tSIBLINGS * siblings;\n" + 1055 | "\tCHILDREN * objects;\n" + 1056 | "\tCHILDREN * rigidbodies;\n" + 1057 | "\t} NODE;\n\n") 1058 | # SOUND 1059 | # VAG 1060 | h.write("//VAG\n" + 1061 | "typedef struct VAGsound {\n" + 1062 | "\tu_char * VAGfile; // Pointer to VAG data address\n" + 1063 | "\tu_long spu_channel; // SPU voice to playback to\n" + 1064 | "\tu_long spu_address; // SPU address for memory freeing spu mem\n" + 1065 | "\t} VAGsound;\n\n" ) 1066 | 1067 | h.write("typedef struct VAGbank {\n" + 1068 | "\tu_int index;\n" + 1069 | "\tVAGsound samples[];\n" + 1070 | "\t} VAGbank;\n\n") 1071 | 1072 | h.write("// XA\n" + 1073 | "typedef struct XAsound {\n" + 1074 | "\tu_int id;\n" + 1075 | "\tu_int size;\n" + 1076 | "\tu_char file, channel;\n" + 1077 | "\tu_int start, end;\n" + 1078 | "\tint cursor;\n" + 1079 | "\t} XAsound;\n\n") 1080 | 1081 | h.write("typedef struct XAbank {\n" + 1082 | "\tchar name[16];\n" + 1083 | "\tu_int index;\n" + 1084 | "\tint offset;\n" + 1085 | "\tXAsound samples[];\n" + 1086 | "\t} XAbank;\n\n") 1087 | 1088 | h.write("typedef struct XAfiles {\n" + 1089 | "\tu_int index;\n" + 1090 | "\tXAbank * banks[];\n" + 1091 | "\t} XAfiles;\n\n" ) 1092 | 1093 | h.write("typedef struct SOUND_OBJECT {\n" + 1094 | "\tVECTOR location;\n" + 1095 | "\tint volumeL, volumeR, volume_min, volume_max;\n" + 1096 | "\tVAGsound * VAGsample;\n" + 1097 | "\tXAsound * XAsample;\n" + 1098 | "\tMESH * parent;\n" + 1099 | "} SOUND_OBJECT;\n\n" ) 1100 | 1101 | h.write("typedef struct LEVEL_SOUNDS {\n" + 1102 | "\tint index;\n" + 1103 | "\tSOUND_OBJECT * sounds[];\n" + 1104 | "} LEVEL_SOUNDS;\n\n") 1105 | 1106 | # LEVEL 1107 | h.write("typedef struct LEVEL {\n" + 1108 | "\tCVECTOR * BGc;\n" + 1109 | "\tVECTOR * BKc;\n" + 1110 | "\tMATRIX * cmat;\n" + 1111 | "\tMATRIX * lgtmat;\n" + 1112 | "\tMESH ** meshes;\n" + 1113 | "\tint * meshes_length;\n" + 1114 | "\tMESH * actorPtr;\n" + 1115 | "\tMESH * levelPtr;\n" + 1116 | "\tMESH * propPtr;\n" + 1117 | "\tCAMANGLE * camPtr;\n" + 1118 | "\tCAMPATH * camPath;\n" + 1119 | "\tCAMANGLE ** camAngles;\n" + 1120 | "\tNODE * curNode;\n" + 1121 | "\tLEVEL_SOUNDS * levelSounds;\n" + 1122 | "\tVAGbank * VAG;\n" + 1123 | "\tXAfiles * XA;\n" + 1124 | "\t} LEVEL;\n") 1125 | h.close() 1126 | ## Level Data (level.c) 1127 | # Store every variable name in a list so that we can populate the level.h file later 1128 | level_symbols = [] 1129 | level_symbols.append("LEVEL " + fileName) 1130 | f = open(os.path.normpath(level_c),"w+") 1131 | f.write( 1132 | '#include "' + fileName + '.h"\n\n' + 1133 | "NODE_DECLARATION\n" 1134 | ) 1135 | ## Horizon & Ambient color 1136 | # Get world horizon colors 1137 | BGr = str( round( linearToRGB( bpy.data.worlds[0].horizon_color.r ) * 192 ) + 63 ) 1138 | BGg = str( round( linearToRGB( bpy.data.worlds[0].horizon_color.g ) * 192) + 63 ) 1139 | BGb = str( round( linearToRGB( bpy.data.worlds[0].horizon_color.b ) * 192 ) + 63 ) 1140 | f.write( 1141 | "CVECTOR " + fileName + "_BGc = { " + BGr + ", " + BGg + ", " + BGb + ", 0 };\n\n" 1142 | ) 1143 | level_symbols.append( "CVECTOR " + fileName + "_BGc" ) 1144 | # Get ambient color 1145 | BKr = str( round( linearToRGB( bpy.data.worlds[0].ambient_color.r ) * 192 ) + 63 ) 1146 | BKg = str( round( linearToRGB( bpy.data.worlds[0].ambient_color.g ) * 192 ) + 63 ) 1147 | BKb = str( round( linearToRGB( bpy.data.worlds[0].ambient_color.b ) * 192 ) + 63 ) 1148 | f.write( 1149 | "VECTOR " + fileName + "_BKc = { " + BKr + ", " + BKg + ", " + BKb + ", 0 };\n\n" 1150 | ) 1151 | level_symbols.append( "VECTOR " + fileName + "_BKc" ) 1152 | # Dictionaries 1153 | # Sound 1154 | # These speaker objects's positions will have to be updated 1155 | spkrParents = defaultdict(dict) 1156 | spkrOrphans = [] 1157 | # array of Sound objects 1158 | soundFiles = [] 1159 | # current XA files and channel 1160 | freeXAfile = 0 1161 | freeXAchannel = 0 1162 | # Lights 1163 | lmpObjects = {} 1164 | # Meshes 1165 | mshObjects = {} 1166 | # Vertex animation 1167 | # ~ mixOverlapingStrips = True 1168 | objAnims = defaultdict(dict) 1169 | # Use scene's Start/End frames as default 1170 | frame_start = int( bpy.context.scene.frame_start ) 1171 | frame_end = int( bpy.context.scene.frame_end ) 1172 | # Loop 1173 | for obj in bpy.data.objects: 1174 | # Build a dictionary of objects that have child SPEAKER objects 1175 | if obj.type == 'SPEAKER': 1176 | if obj.data.sound is not None: 1177 | # and child of a mesh 1178 | if obj.parent is not None: 1179 | if obj.parent.type == 'MESH': 1180 | parent = obj.parent 1181 | # has no parent 1182 | else: 1183 | parent = 0 1184 | # get sound informations 1185 | objName = obj.name 1186 | soundName = obj.data.sound.name 1187 | soundPath = bpy.path.abspath(obj.data.sound.filepath) 1188 | location = obj.location 1189 | volume = int(obj.data.volume) 1190 | volume_min = int(obj.data.volume_min) 1191 | volume_max = int(obj.data.volume_max) 1192 | # convert sound 1193 | if obj.data.get('isXA'): 1194 | XAsectorsize = 2336 if XAmode else 2352 1195 | if freeXAchannel > 7: 1196 | freeXAfile += 1 1197 | freeXAchannel = 0 1198 | convertedSoundPath = sound2XA(soundPath, soundName, bpp=4, XAfile=freeXAfile, XAchannel=freeXAchannel) 1199 | XAfile = freeXAfile 1200 | XAchannel = freeXAchannel 1201 | freeXAchannel += 1 1202 | if os.path.exists(convertedSoundPath): 1203 | XAsize = os.path.getsize(convertedSoundPath) 1204 | XAend = int((( XAsize / XAsectorsize ) - 1)) 1205 | else: 1206 | XAsize = -1 1207 | XAend = -1 1208 | soundFiles.append( Sound( objName, soundName, soundPath, convertedSoundPath, parent, location, volume, volume_min, volume_max, -1, XAfile, XAchannel, XAsize, XAend ) ) 1209 | else: 1210 | convertedSoundPath = sound2VAG(soundPath, soundName) 1211 | soundFiles.append( Sound( objName, soundName, soundPath, convertedSoundPath, parent, location, volume, volume_min, volume_max, -1 ) ) 1212 | # Build dict of objects <> data correspondance 1213 | # We want to be able to find an object based on it's data name. 1214 | if obj.type == 'LAMP': 1215 | lmpObjects[obj.data.name] = obj.name 1216 | if obj.type == 'MESH': 1217 | mshObjects[obj.data.name] = obj.name 1218 | ## Vertex Animation 1219 | # If isAnim flag is set, export object's vertex animations 1220 | # Vertex animation is possible using keyframes or shape keys 1221 | # Using nla tracks allows to export several animation for the same mesh 1222 | # If the mixAnim flag is set, the resulting animation will be an interpolation between the overlapping nla tracks. 1223 | #if len(bpy.data.actions): 1224 | # Find shape key based animations 1225 | if obj.active_shape_key: 1226 | # Get shape key name 1227 | shapeKeyName = obj.active_shape_key.id_data.name 1228 | # Get shape_key object 1229 | shapeKey = bpy.data.shape_keys[shapeKeyName] 1230 | # Bake action to LNA 1231 | if bakeActionToNLA(shapeKey): 1232 | getTrackList(shapeKey, obj) 1233 | # Find object based animation 1234 | if bakeActionToNLA(obj): 1235 | getTrackList(obj, obj) 1236 | ## Export anim tracks and strips 1237 | for obj in objAnims: 1238 | # If mixing nla tracks, only export one track 1239 | if self.exp_mixOverlapingStrips: 1240 | overlappingStrips = findOverlappingTrack(objAnims[obj]) 1241 | level_symbols.append( writeMESH_ANIMS( f, obj, overlappingStrips, fileName ) ) 1242 | for strip in overlappingStrips: 1243 | # Min frame start 1244 | strip_start = min( strip.frame_start , min([ action.frame_start for action in overlappingStrips[strip] ]) ) 1245 | # Max frame end 1246 | strip_end = max( strip.frame_start , max([ action.frame_end for action in overlappingStrips[strip] ]) ) 1247 | level_symbols.append( writeVANIM(f, obj, strip, fileName, strip_start, strip_end) ) 1248 | else: 1249 | allStrips = getStripsTotal(objAnims[obj]) 1250 | level_symbols.append( writeMESH_ANIMS( f, obj, allStrips, fileName ) ) 1251 | for track in objAnims[obj]: 1252 | # if flag is set, hide others nla_tracks 1253 | track.is_solo = True 1254 | for strip in objAnims[obj][track]: 1255 | # Use scene's Start/End frames as default 1256 | strip_start = strip.frame_start 1257 | strip_end = strip.frame_end 1258 | level_symbols.append( writeVANIM(f, obj, strip, fileName, strip_start, strip_end) ) 1259 | track.is_solo = False 1260 | # Close struct declaration 1261 | # ~ f.write("\t\t},\n") 1262 | # ~ f.write("\t}\n};\n") 1263 | # ~ level_symbols.append( "MESH_ANIMS_TRACKS " + fileName + "_model" + CleanName(obj.data.name) + "_anims" ) 1264 | 1265 | ## Camera setup 1266 | # List of points defining the camera path 1267 | camPathPoints = [] 1268 | # Define first mesh. Will be used as default if no properties are found in meshes 1269 | first_mesh = CleanName( bpy.data.meshes[ 0 ].name ) 1270 | # Set camera position and rotation in the scene 1271 | for o in range( len( bpy.data.objects ) ): 1272 | # Add objects of type MESH with a Rigidbody or StaticBody flag set to a list 1273 | if bpy.data.objects[ o ].type == 'MESH': 1274 | if ( 1275 | bpy.data.objects[ o ].data.get('isRigidBody') or 1276 | bpy.data.objects[ o ].data.get('isStaticBody') 1277 | #or bpy.data.objects[o].data.get('isPortal') 1278 | ): 1279 | rayTargets.append(bpy.data.objects[o]) 1280 | # Set object of type CAMERA with isDefault flag as default camera 1281 | if bpy.data.objects[o].type == 'CAMERA' and bpy.data.objects[o].data.get('isDefault'): 1282 | defaultCam = bpy.data.objects[o].name 1283 | # Declare each blender camera as a CAMPOS 1284 | if bpy.data.objects[o].type == 'CAMERA': 1285 | f.write("CAMPOS " + fileName + "_camPos_" + CleanName( bpy.data.objects[ o ].name ) + " = {\n" + 1286 | "\t{ " + str( round( -bpy.data.objects[o].location.x * scale ) ) + 1287 | "," + str( round( bpy.data.objects[o].location.z * scale ) ) + 1288 | "," + str( round( -bpy.data.objects[o].location.y * scale ) ) + " },\n" + 1289 | "\t{ " + str( round( -( degrees( bpy.data.objects[ o ].rotation_euler.x ) -90 ) / 360 * 4096 ) ) + 1290 | "," + str( round( degrees( bpy.data.objects[ o ].rotation_euler.z ) / 360 * 4096 ) ) + 1291 | "," + str( round( -( degrees( bpy.data.objects[ o ].rotation_euler.y ) ) / 360 * 4096 ) ) + 1292 | " }\n" + 1293 | "};\n\n") 1294 | level_symbols.append( "CAMPOS " + fileName + "_camPos_" + CleanName( bpy.data.objects[ o ].name ) ) 1295 | # Find camera path points and append them to camPathPoints[] 1296 | if bpy.data.objects[o].type == 'CAMERA' : 1297 | if ( bpy.data.objects[ o ].name.startswith( "camPath" ) 1298 | and not bpy.data.objects[ o ].data.get( 'exclude' ) 1299 | ) : 1300 | camPathPoints.append(bpy.data.objects[o].name) 1301 | # Write the CAMPATH structure 1302 | if camPathPoints: 1303 | # Populate with points found above 1304 | # ~ camPathPoints = list(reversed(camPathPoints)) 1305 | for point in range(len(camPathPoints)): 1306 | if point == 0: 1307 | f.write("CAMPATH " + fileName + "_camPath = {\n" + 1308 | "\t" + str( len( camPathPoints ) ) + ",\n" + 1309 | "\t0,\n" + 1310 | "\t0,\n" + 1311 | "\t{\n") 1312 | level_symbols.append( "CAMPATH " + fileName + "_camPath" ) 1313 | f.write( "\t\t{ " + str( round( -bpy.data.objects[ camPathPoints[ point ] ].location.x * scale ) ) + 1314 | "," + str( round( bpy.data.objects[ camPathPoints[ point ] ].location.z * scale ) ) + 1315 | "," + str( round( -bpy.data.objects[ camPathPoints[ point ] ].location.y * scale ) ) + 1316 | " }" ) 1317 | if point != len( camPathPoints ) - 1: 1318 | f.write(",\n") 1319 | f.write("\n\t}\n};\n\n") 1320 | else: 1321 | # If no camera path points are found, use default 1322 | f.write("CAMPATH " + fileName + "_camPath = {\n" + 1323 | "\t0,\n" + 1324 | "\t0,\n" + 1325 | "\t0,\n" + 1326 | "\t{0}\n" + 1327 | "};\n\n" ) 1328 | level_symbols.append( "CAMPATH " + fileName + "_camPath" ) 1329 | ## Lighting setup 1330 | # Light sources will be similar to Blender's sunlamp 1331 | # A maximum of 3 light sources will be used 1332 | # LLM : Local Light Matrix 1333 | if len( lmpObjects ) is not None: 1334 | cnt = 0 1335 | # ~ pad = 3 - len( lmpObjects ) if ( len( lmpObjects ) < 3 ) else 0 1336 | f.write( "MATRIX " + fileName + "_lgtmat = {\n") 1337 | for light in sorted(lmpObjects): 1338 | # Get rid of orphans 1339 | if bpy.data.lamps[light].users == 0: 1340 | continue 1341 | # PSX can only use 3 light sources 1342 | if cnt < 3 : 1343 | # Lightsource energy 1344 | energy = int( bpy.data.lamps[light].energy * 4096 ) 1345 | # ~ energy = int( light.energy * 4096 ) 1346 | # Get lightsource's world orientation 1347 | lightdir = bpy.data.objects[lmpObjects[light]].matrix_world * Vector( ( 0, 0, -1, 0 ) ) 1348 | f.write( 1349 | "\t" + str( int( lightdir.x * energy ) ) + ", " + 1350 | str( int( -lightdir.z * energy ) ) + ", " + 1351 | str( int( lightdir.y * energy ) ) 1352 | ) 1353 | if cnt < 2: 1354 | f.write(",") 1355 | f.write(" // L" + str(cnt+1) + "\n") 1356 | cnt += 1 1357 | # If less than 3 light sources exist in blender, fill the matrix with 0s. 1358 | # ~ if pad: 1359 | while cnt < 3: 1360 | f.write("\t0, 0, 0") 1361 | if cnt < 2: 1362 | f.write(",") 1363 | f.write("\n") 1364 | cnt += 1 1365 | f.write("\t};\n\n") 1366 | level_symbols.append( "MATRIX " + fileName + "_lgtmat" ) 1367 | # LCM : Local Color Matrix 1368 | f.write( "MATRIX " + fileName + "_cmat = {\n") 1369 | LCM = [] 1370 | cnt = 0 1371 | # If more than 3 light sources exists, use the 3 first in alphabetic order (same as in Blender's outliner) 1372 | for light in sorted(lmpObjects): 1373 | # If orphan, get on with it 1374 | if bpy.data.lamps[light].users == 0: 1375 | continue 1376 | if cnt < 3 : 1377 | LCM.append( str( int( bpy.data.lamps[light].color.r * 4096 ) if bpy.data.lamps[light].color.r else 0 ) ) 1378 | LCM.append( str( int( bpy.data.lamps[light].color.g * 4096 ) if bpy.data.lamps[light].color.g else 0 ) ) 1379 | LCM.append( str( int( bpy.data.lamps[light].color.b * 4096 ) if bpy.data.lamps[light].color.b else 0 ) ) 1380 | cnt += 1 1381 | if len(LCM) < 9: 1382 | while len(LCM) < 9: 1383 | LCM.append('0') 1384 | # Write LC matrix 1385 | f.write( 1386 | "// L1 L2 L3\n" 1387 | "\t" + LCM[ 0 ] + ", " + LCM[ 3 ] + ", " + LCM[ 6 ] + ", // R\n" + 1388 | "\t" + LCM[ 1 ] + ", " + LCM[ 4 ] + ", " + LCM[ 7 ] + ", // G\n" + 1389 | "\t" + LCM[ 2 ] + ", " + LCM[ 5 ] + ", " + LCM[ 8 ] + " // B\n" ) 1390 | f.write("\t};\n\n") 1391 | level_symbols.append( "MATRIX " + fileName + "_cmat" ) 1392 | ## Meshes 1393 | actorPtr = first_mesh 1394 | levelPtr = first_mesh 1395 | propPtr = first_mesh 1396 | nodePtr = first_mesh 1397 | timList = [] 1398 | for m in bpy.data.meshes: 1399 | # If orphan, ignore 1400 | if m.users == 0: 1401 | continue 1402 | if not m.get('isPortal') : 1403 | # Store vertices coordinates by axis to find max/min coordinates 1404 | Xvals = [] 1405 | Yvals = [] 1406 | Zvals = [] 1407 | cleanName = CleanName(m.name) 1408 | # Write vertices vectors 1409 | f.write( "SVECTOR " + fileName + "_model" + cleanName + "_mesh[] = {\n" ) 1410 | level_symbols.append( "SVECTOR " + "model" + cleanName + "_mesh[]" ) 1411 | for i in range( len( m.vertices ) ): 1412 | v = m.vertices[ i ].co 1413 | # Append vertex coords to lists 1414 | Xvals.append( v.x ) 1415 | Yvals.append( v.y ) 1416 | Zvals.append( -v.z ) 1417 | f.write("\t{ " + str( ceil( v.x * scale ) ) + 1418 | "," + str( ceil( -v.z * scale ) ) + 1419 | "," + str( ceil( v.y * scale ) ) + ",0 }" ) 1420 | if i != len(m.vertices) - 1: 1421 | f.write(",") 1422 | f.write("\n") 1423 | f.write("};\n\n") 1424 | # Write normals vectors 1425 | f.write("SVECTOR " + fileName + "_model"+cleanName+"_normal[] = {\n") 1426 | level_symbols.append( "SVECTOR " + fileName + "_model"+cleanName+"_normal[]" ) 1427 | for i in range(len(m.vertices)): 1428 | poly = m.vertices[i] 1429 | f.write( "\t"+ str( round( -poly.normal.x * 4096 ) ) + 1430 | "," + str( round( poly.normal.z * 4096 ) ) + 1431 | "," + str( round( -poly.normal.y * 4096 ) ) + ", 0" ) 1432 | if i != len(m.vertices) - 1: 1433 | f.write(",") 1434 | f.write("\n") 1435 | f.write("};\n\n") 1436 | # Write UV textures coordinates 1437 | if len(m.uv_textures) != None: 1438 | for t in range(len(m.uv_textures)): 1439 | if m.uv_textures[t].data[0].image != None: 1440 | f.write("SVECTOR " + fileName + "_model"+cleanName+"_uv[] = {\n") 1441 | level_symbols.append( "SVECTOR " + fileName + "_model" + cleanName + "_uv[]" ) 1442 | texture_image = m.uv_textures[t].data[0].image 1443 | tex_width = texture_image.size[0] 1444 | tex_height = texture_image.size[1] 1445 | uv_layer = m.uv_layers[0].data 1446 | for i in range(len(uv_layer)): 1447 | u = uv_layer[i].uv 1448 | ux = u.x * tex_width 1449 | uy = u.y * tex_height 1450 | # Clamp values to 0-255 to avoid tpage overflow 1451 | f.write("\t" + str( max( 0, min( round( ux ) , 255 ) ) ) + 1452 | "," + str( max( 0, min( round( tex_height - uy ) , 255 ) ) ) + 1453 | ", 0, 0" ) 1454 | if i != len(uv_layer) - 1: 1455 | f.write(",") 1456 | f.write("\n") 1457 | f.write("};\n\n") 1458 | # Save UV texture to a file in ./TEX 1459 | # It will have to be converted to a tim file 1460 | if texture_image.filepath == '': 1461 | # ~ os.makedirs(dirpath, exist_ok = 1) 1462 | texture_image.filepath_raw = textureFolder + os.sep + CleanName(texture_image.name) + "." + texture_image.file_format 1463 | texture_image.save() 1464 | # Write vertex colors vectors 1465 | f.write("CVECTOR " + fileName + "_model" + cleanName + "_color[] = {\n" ) 1466 | level_symbols.append( "CVECTOR " + fileName + "_model" + cleanName + "_color[]" ) 1467 | # If vertex colors exist, use them 1468 | if len(m.vertex_colors) != 0: 1469 | colors = m.vertex_colors[0].data 1470 | for i in range(len(colors)): 1471 | f.write("\t" + str( int( colors[ i ].color.r * 255 ) ) + "," + 1472 | str( int( colors[ i ].color.g * 255 ) ) + "," + 1473 | str( int( colors[ i ].color.b * 255 ) ) + ", 0" ) 1474 | if i != len(colors) - 1: 1475 | f.write(",") 1476 | f.write("\n") 1477 | # If no vertex colors, default to 2 whites, 1 grey 1478 | else: 1479 | for i in range(len(m.polygons) * 3): 1480 | if i % 3 == 0: 1481 | f.write("\t80, 80, 80, 0" ) 1482 | else: 1483 | f.write("\t128, 128, 128, 0" ) 1484 | if i != (len(m.polygons) * 3) - 1: 1485 | f.write(",") 1486 | f.write("\n") 1487 | f.write("};\n\n") 1488 | # Write polygons index + type 1489 | # Keep track of total number of vertices in the mesh 1490 | totalVerts = 0 1491 | f.write( "PRIM " + fileName + "_model" + cleanName + "_index[] = {\n" ) 1492 | level_symbols.append( "PRIM " + fileName + "_model" + cleanName + "_index[]" ) 1493 | for i in range(len(m.polygons)): 1494 | poly = m.polygons[i] 1495 | f.write( "\t" + str( poly.vertices[ 0 ] ) + "," + str( poly.vertices[ 1 ] ) + "," + str( poly.vertices[ 2 ] ) ) 1496 | totalVerts += 3 1497 | if len(poly.vertices) > 3: 1498 | f.write("," + str(poly.vertices[3]) + ",8") 1499 | totalVerts += 1 1500 | else: 1501 | f.write(",0,4") 1502 | if i != len(m.polygons) - 1: 1503 | f.write(",") 1504 | f.write("\n") 1505 | f.write("};\n\n") 1506 | # Get object's custom properties 1507 | # Set defaults values 1508 | chkProp = { 1509 | 'isAnim':0, 1510 | 'isProp':0, 1511 | 'isRigidBody':0, 1512 | 'isStaticBody':0, 1513 | 'isRound':0, 1514 | 'isPrism':0, 1515 | 'isActor':0, 1516 | 'isLevel':0, 1517 | 'isWall':0, 1518 | 'isBG':0, 1519 | 'isSprite':0, 1520 | 'mass': 10, 1521 | 'restitution': 0 1522 | } 1523 | # Get real values from object 1524 | for prop in chkProp: 1525 | if m.get(prop) is not None: 1526 | chkProp[prop] = m[prop] 1527 | # put isBG back to 0 if using precalculated BGs 1528 | if not self.exp_Precalc: 1529 | chkProp['isBG'] = 0; 1530 | if m.get('isActor'): 1531 | actorPtr = m.name 1532 | if m.get('isLevel'): 1533 | levelPtr = cleanName 1534 | if m.get('isProp'): 1535 | propPtr = cleanName 1536 | if chkProp['mass'] == 0: 1537 | chkProp['mass'] = 1 1538 | 1539 | ## Mesh world transform setup 1540 | # Write object matrix, rot and pos vectors 1541 | f.write( 1542 | "BODY " + fileName + "_model"+cleanName+"_body = {\n" + 1543 | "\t{0, 0, 0, 0},\n" + 1544 | "\t" + str(round(bpy.data.objects[mshObjects[m.name]].location.x * scale)) + "," + str(round(-bpy.data.objects[mshObjects[m.name]].location.z * scale)) + "," + str(round(bpy.data.objects[mshObjects[m.name]].location.y * scale)) + ", 0,\n" + 1545 | "\t"+ str(round(degrees(bpy.data.objects[mshObjects[m.name]].rotation_euler.x)/360 * 4096)) + "," + str(round(degrees(-bpy.data.objects[mshObjects[m.name]].rotation_euler.z)/360 * 4096)) + "," + str(round(degrees(bpy.data.objects[mshObjects[m.name]].rotation_euler.y)/360 * 4096)) + ", 0,\n" + 1546 | "\t" + str(int(chkProp['mass'])) + ",\n" + 1547 | "\tONE/" + str(int(chkProp['mass'])) + ",\n" + 1548 | # write min and max values of AABBs on each axis 1549 | "\t" + str(round(min(Xvals) * scale)) + "," + str(round(min(Zvals) * scale)) + "," + str(round(min(Yvals) * scale)) + ", 0,\n" + 1550 | "\t" + str(round(max(Xvals) * scale)) + "," + str(round(max(Zvals) * scale)) + "," + str(round(max(Yvals) * scale)) + ", 0,\n" + 1551 | "\t" + str(int(chkProp['restitution'])) + ",\n" + 1552 | # ~ "\tNULL\n" + 1553 | "\t};\n\n") 1554 | level_symbols.append( "BODY " + fileName + "_model"+cleanName+"_body" ) 1555 | # Write TMESH struct 1556 | f.write( "TMESH " + fileName + "_model" + cleanName + " = {\n" ) 1557 | f.write( "\t" + fileName + "_model" + cleanName + "_mesh,\n" ) 1558 | f.write( "\t" + fileName + "_model" + cleanName + "_normal,\n" ) 1559 | level_symbols.append( "TMESH " + fileName + "_model" + cleanName ) 1560 | # ~ level_symbols.append( "model" + cleanName + "_mesh" ) 1561 | # ~ level_symbols.append( "model" + cleanName + "_normal" ) 1562 | if len(m.uv_textures) != 0: 1563 | for t in range(len(m.uv_textures)): 1564 | if m.uv_textures[0].data[0].image != None: 1565 | f.write("\t" + fileName + "_model"+cleanName+"_uv,\n") 1566 | # ~ level_symbols.append( "model" + cleanName + "_uv" ) 1567 | else: 1568 | f.write("\t0,\n") 1569 | else: 1570 | f.write("\t0,\n") 1571 | f.write( "\t" + fileName + "_model" + cleanName + "_color, \n" ) 1572 | # According to libgte.h, TMESH.len should be # of vertices. Meh... 1573 | f.write( "\t" + str( len ( m.polygons ) ) + "\n" ) 1574 | f.write( "};\n\n" ) 1575 | # Write texture binary name and declare TIM_IMAGE 1576 | # By default, loads the file from the ./TIM folder 1577 | if len(m.uv_textures) != None: 1578 | for t in range(len(m.uv_textures)): 1579 | if m.uv_textures[0].data[0].image != None: 1580 | tex_name = texture_image.name 1581 | # extension defaults to the image file format 1582 | tex_ext = texture_image.file_format.lower() 1583 | prefix = str.partition(tex_name, ".")[0].replace('-','_') 1584 | prefix = CleanName(prefix) 1585 | # Add Tex name to list if it's not in there already 1586 | if prefix in timList: 1587 | break 1588 | else: 1589 | # Convert PNG to TIM 1590 | # If filename contains a dot, separate name and extension 1591 | if tex_name.find('.') != -1: 1592 | # store extension 1593 | tex_ext = tex_name[ tex_name.rfind( '.' ) + 1 : ] 1594 | # store name 1595 | tex_name = tex_name[ : tex_name.rfind( '.' ) ] 1596 | # ~ filePathWithExt = textureFolder + os.sep + CleanName( tex_name ) + "." + texture_image.file_format.lower() 1597 | filePathWithExt = textureFolder + os.sep + CleanName( tex_name ) + "." + tex_ext 1598 | if not VramIsFull( bpy.context.scene.render.resolution_x ): 1599 | convertBGtoTIM( filePathWithExt, bpp = TIMbpp, timX = nextTpage, timY = tpageY, clutY = nextClutSlot ) 1600 | setNextTimPos( texture_image ) 1601 | elif VramIsFull( bpy.context.scene.render.resolution_x ) and tpageY == 0: 1602 | tpageY = 256 1603 | nextTpage = 320 1604 | if not VramIsFull( bpy.context.scene.render.resolution_x ): 1605 | convertBGtoTIM( filePathWithExt, bpp = TIMbpp, timX = nextTpage, timY = tpageY, clutY = nextClutSlot ) 1606 | setNextTimPos( texture_image ) 1607 | else: 1608 | self.report({'ERROR'}, "Not enough space in VRam !") 1609 | else: 1610 | self.report({'ERROR'}, "Not enough space in VRam !") 1611 | # Write corresponding TIM declaration 1612 | f.write("extern unsigned long " + "_binary_TIM_" + prefix + "_tim_start[];\n") 1613 | f.write("extern unsigned long " + "_binary_TIM_" + prefix + "_tim_end[];\n") 1614 | f.write("extern unsigned long " + "_binary_TIM_" + prefix + "_tim_length;\n\n") 1615 | f.write("TIM_IMAGE " + fileName + "_tim_" + prefix + ";\n\n") 1616 | level_symbols.append( "unsigned long " + "_binary_TIM_" + prefix + "_tim_start[]" ) 1617 | level_symbols.append( "unsigned long " + "_binary_TIM_" + prefix + "_tim_end[]" ) 1618 | level_symbols.append( "unsigned long " + "_binary_TIM_" + prefix + "_tim_length" ) 1619 | level_symbols.append( "TIM_IMAGE " + fileName + "_tim_" + prefix ) 1620 | timList.append(prefix) 1621 | f.write( "MESH " + fileName + "_mesh" + cleanName + " = {\n" + 1622 | "\t" + str(totalVerts) + ",\n" + 1623 | "\t&" + fileName + "_model"+ cleanName +",\n" + 1624 | "\t" + fileName + "_model" + cleanName + "_index,\n" 1625 | ) 1626 | if len(m.uv_textures) != 0: 1627 | for t in range(len(m.uv_textures)): 1628 | if m.uv_textures[0].data[0].image != None: 1629 | tex_name = texture_image.name 1630 | prefix = str.partition(tex_name, ".")[0].replace('-','_') 1631 | prefix = CleanName(prefix) 1632 | f.write("\t&" + fileName + "_tim_"+ prefix + ",\n") 1633 | f.write("\t_binary_TIM_" + prefix + "_tim_start,\n") 1634 | else: 1635 | f.write("\t0,\n" + 1636 | "\t0,\n") 1637 | else: 1638 | f.write("\t0,\n" + 1639 | "\t0,\n") 1640 | # Find out if object as animations 1641 | symbol_name = "MESH_ANIMS_TRACKS " + fileName + "_model" + CleanName(obj.data.name) + "_anims" 1642 | if symbol_name in level_symbols: 1643 | symbol_name = "&" + fileName + "_model" + CleanName(obj.data.name) + "_anims" 1644 | else: 1645 | symbol_name = "0" 1646 | f.write( 1647 | "\t{0}, // Matrix\n" + 1648 | "\t{" + str(round(bpy.data.objects[mshObjects[m.name]].location.x * scale)) + "," 1649 | + str(round(-bpy.data.objects[mshObjects[m.name]].location.z * scale)) + "," 1650 | + str(round(bpy.data.objects[mshObjects[m.name]].location.y * scale)) + ", 0}, // position\n" + 1651 | "\t{"+ str(round(degrees(bpy.data.objects[mshObjects[m.name]].rotation_euler.x)/360 * 4096)) + "," 1652 | + str(round(degrees(-bpy.data.objects[mshObjects[m.name]].rotation_euler.z)/360 * 4096)) + "," 1653 | + str(round(degrees(bpy.data.objects[mshObjects[m.name]].rotation_euler.y)/360 * 4096)) + ", 0}, // rotation\n" + 1654 | "\t" + str( int( chkProp[ 'isProp' ] ) ) + ", // isProp\n" + 1655 | "\t" + str( int( chkProp[ 'isRigidBody' ] ) ) + ", // isRigidBody\n" + 1656 | "\t" + str(int(chkProp['isStaticBody'])) + ", // isStaticBody\n" + 1657 | "\t" + str(int(chkProp['isRound'])) + ", // isRound \n" + 1658 | "\t" + str(int(chkProp['isPrism'])) + ", // isPrism\n" + 1659 | "\t" + str(int(chkProp['isAnim'])) + ", // isAnim\n" + 1660 | "\t" + str(int(chkProp['isActor'])) + ", // isActor\n" + 1661 | "\t" + str(int(chkProp['isLevel'])) + ", // isLevel\n" + 1662 | "\t" + str(int(chkProp['isWall'])) + ", // isWall\n" + 1663 | "\t" + str(int(chkProp['isBG'])) + ", // isBG\n" + 1664 | "\t" + str(int(chkProp['isSprite'])) + ", // isSprite\n" + 1665 | "\t0, // p\n" + 1666 | "\t0, // otz\n" + 1667 | "\t&" + fileName + "_model" + cleanName + "_body,\n" + 1668 | "\t" + symbol_name + ", // Mesh anim tracks\n" + 1669 | "\t0, // Current VANIM\n" + 1670 | "\t" + "subs_" + CleanName(m.name) + ",\n" + 1671 | "\t0 // Screen space coordinates\n" + 1672 | "};\n\n" 1673 | ) 1674 | level_symbols.append( "MESH " + fileName + "_mesh" + cleanName ) 1675 | # Remove portals from mesh list as we don't want them to be exported 1676 | meshList = [] 1677 | # Build list without orphans 1678 | for mesh in bpy.data.meshes: 1679 | if mesh.users != 0: 1680 | meshList.append(mesh) 1681 | portalList = [] 1682 | for mesh in meshList: 1683 | if mesh.get('isPortal'): 1684 | meshList = [i for i in meshList if i != mesh] 1685 | # Nasty way of removing all occurrences of the mesh 1686 | # ~ try: 1687 | # ~ while True: 1688 | # ~ meshList.remove(mesh) 1689 | # ~ except ValueError: 1690 | # ~ pass 1691 | portalList.append( bpy.data.objects[mesh.name] ) 1692 | f.write("MESH * " + fileName + "_meshes[" + str( len(meshList ) ) + "] = {\n") 1693 | for k in range(len(meshList)): 1694 | cleanName = CleanName(meshList[k].name) 1695 | f.write("\t&" + fileName + "_mesh" + cleanName) 1696 | if k != len(meshList) - 1: 1697 | f.write(",\n") 1698 | f.write("\n}; \n\n") 1699 | f.write("int " + fileName + "_meshes_length = " + str( len( meshList ) ) + ";\n\n") 1700 | level_symbols.append( "MESH * " + fileName + "_meshes[" + str(len(meshList)) + "]") 1701 | level_symbols.append( "int " + fileName + "_meshes_length" ) 1702 | # If camAngles is empty, use default camera, and do not include pre-calculated backgrounds 1703 | if not camAngles: 1704 | f.write("CAMANGLE " + fileName + "_camAngle_" + CleanName(defaultCam) + " = {\n" + 1705 | "\t&" + fileName + "_camPos_" + CleanName(defaultCam) + ",\n" + 1706 | "\t0,\n\t 0,\n\t { 0 },\n\t { 0 },\n\t 0,\n\t 0\n" + 1707 | "};\n\n") 1708 | level_symbols.append( "CAMANGLE " + fileName + "_camAngle_" + CleanName(defaultCam) ) 1709 | # If camAngles is populated, use backgrounds and camera angles 1710 | for camera in camAngles: 1711 | # Get current scene 1712 | scene = bpy.context.scene 1713 | # List of portals 1714 | visiblePortal = [] 1715 | for portal in portalList: 1716 | if isInFrame(scene, camera, portal): 1717 | # Get normalized direction vector between camera and portal 1718 | dirToTarget = portal.location - camera.location 1719 | dirToTarget.normalize() 1720 | # Cast a ray from camera to body to determine visibility 1721 | result, location, normal, index, hitObject, matrix = scene.ray_cast( camera.location, dirToTarget ) 1722 | # If hitObject is portal, nothing is obstructing it's visibility 1723 | if hitObject is not None: 1724 | if hitObject in portalList: 1725 | if hitObject == portal: 1726 | visiblePortal.append(hitObject) 1727 | # If more than one portal is visible, only keep the two closest ones 1728 | if len( visiblePortal ) > 2: 1729 | # Store the tested portals distance to camera 1730 | testDict = {} 1731 | for tested in visiblePortal: 1732 | # Get distance between cam and tested portal 1733 | distToTested = sqrt( ( tested.location - camera.location ) * ( tested.location - camera.location ) ) 1734 | # Store distance 1735 | testDict[distToTested] = tested 1736 | # If dictionary has more than 2 portals, remove the farthest ones 1737 | while len( testDict ) > 2: 1738 | del testDict[max(testDict)] 1739 | # Reset visible portal 1740 | visiblePortal.clear() 1741 | # Get the portals stored in the dict and store them in the list 1742 | for Dportal in testDict: 1743 | visiblePortal.append(testDict[Dportal]) 1744 | # Revert to find original order back 1745 | visiblePortal.reverse() 1746 | # List of target found visible 1747 | visibleTarget = [] 1748 | for target in rayTargets: 1749 | # Chech object is in view frame 1750 | if isInFrame(scene, camera, target): 1751 | # Get normalized direction vector between camera and object 1752 | dirToTarget = target.location - camera.location 1753 | dirToTarget.normalize() 1754 | # Cast ray from camera to object 1755 | # Unpack results into several variables. 1756 | # We're only interested in 'hitObject' though 1757 | result, hitLocation, normal, index, hitObject, matrix = scene.ray_cast( camera.location, dirToTarget ) 1758 | # If hitObject is the same as target, nothing is obstructing it's visibility 1759 | if hitObject is not None: 1760 | # If hit object is a portal, cast a new ray from hit location to target 1761 | if hitObject.data.get('isPortal'): 1762 | # Find out if we're left or right of portal 1763 | # Get vertices world coordinates 1764 | v0 = hitObject.matrix_world * hitObject.data.vertices[0].co 1765 | v1 = hitObject.matrix_world * hitObject.data.vertices[1].co 1766 | # Check side : 1767 | # 'back' : portal in on the right of the cam, cam is on left of portal 1768 | # 'front' : portal in on the left of the cam, cam is on right of portal 1769 | side = checkLine(v0.x, v0.y, v1.x, v1.y , camera.location.x, camera.location.y, camera.location.x, camera.location.y ) 1770 | if side == 'front': 1771 | # we're on the right of the portal, origin.x must be > hitLocation.x 1772 | offset = [ 1.001, 0.999, 0.999 ] 1773 | else : 1774 | # we're on the left of the portal, origin.x must be < hitLocation.x 1775 | offset = [ 0.999, 1.001, 1.001 ] 1776 | # Add offset to hitLocation, so that the new ray won't hit the same portal 1777 | origin = Vector( ( hitLocation.x * offset[0], hitLocation.y * offset[1], hitLocation.z * offset[2] ) ) 1778 | result, hitLocationPort, normal, index, hitObjectPort, matrix = scene.ray_cast( origin , dirToTarget ) 1779 | if hitObjectPort is not None: 1780 | if hitObjectPort in rayTargets: 1781 | visibleTarget.append(target) 1782 | # If hitObject is not a portal, just add it 1783 | elif hitObject in rayTargets: 1784 | visibleTarget.append(target) 1785 | if bpy.data.objects[ actorPtr ] not in visibleTarget: 1786 | visibleTarget.append( bpy.data.objects[ actorPtr ] ) 1787 | # If visiblePortal length is under 2, this means there's only one portal 1788 | # Empty strings to be populated depending on portal position (left/right of screen) 1789 | before = '' 1790 | after = '' 1791 | if len( visiblePortal ) < 2 : 1792 | # Find wich side of screen the portal is on. left side : portal == bw, right side : portal == fw 1793 | screenCenterX = int( scene.render.resolution_x / 2 ) 1794 | screenY = int( scene.render.resolution_y ) 1795 | # Get vertices screen coordinates 1796 | s = objVertWtoS(scene, camera, visiblePortal[0]) 1797 | # Check line 1798 | side = checkLine( 1799 | screenCenterX, 0, screenCenterX, screenY, 1800 | s[1].x, 1801 | s[1].y, 1802 | s[3].x, 1803 | s[3].y 1804 | ) 1805 | # If front == right of screen : fw 1806 | if side == "front": 1807 | before = "\t{\n\t\t{ 0, 0, 0, 0 },\n\t\t{ 0, 0, 0, 0 },\n\t\t{ 0, 0, 0, 0 },\n\t\t{ 0, 0, 0, 0 }\n\t},\n" 1808 | # If back == left of screen : bw 1809 | else : 1810 | after = "\t{\n\t\t{ 0, 0, 0, 0 },\n\t\t{ 0, 0, 0, 0 },\n\t\t{ 0, 0, 0, 0 },\n\t\t{ 0, 0, 0, 0 }\n\t},\n" 1811 | prefix = CleanName(camera.name) 1812 | # Include Tim data 1813 | f.write("extern unsigned long "+"_binary_TIM_bg_" + prefix + "_tim_start[];\n") 1814 | f.write("extern unsigned long "+"_binary_TIM_bg_" + prefix + "_tim_end[];\n") 1815 | f.write("extern unsigned long "+"_binary_TIM_bg_" + prefix + "_tim_length;\n\n") 1816 | # Write corresponding TIM_IMAGE struct 1817 | f.write("TIM_IMAGE tim_bg_" + prefix + ";\n\n") 1818 | # Write corresponding CamAngle struct 1819 | f.write("CAMANGLE " + fileName + "_camAngle_" + prefix + " = {\n" + 1820 | "\t&" + fileName + "_camPos_" + prefix + ",\n" + 1821 | "\t&tim_bg_" + prefix + ",\n" + 1822 | "\t_binary_TIM_bg_" + prefix + "_tim_start,\n" + 1823 | "\t// Write quad NW, NE, SE, SW\n") 1824 | f.write( before ) 1825 | # Feed to level_symbols 1826 | level_symbols.append( "unsigned long "+"_binary_TIM_bg_" + prefix + "_tim_start[]") 1827 | level_symbols.append( "unsigned long "+"_binary_TIM_bg_" + prefix + "_tim_end[]") 1828 | level_symbols.append( "unsigned long "+"_binary_TIM_bg_" + prefix + "_tim_length") 1829 | level_symbols.append( "TIM_IMAGE tim_bg_" + prefix ) 1830 | level_symbols.append( "CAMANGLE " + fileName + "_camAngle_" + prefix ) 1831 | for portal in visiblePortal: 1832 | w = objVertLtoW(portal) 1833 | # ~ f.write("\t// " + str(portal) + "\n" ) 1834 | # Write portal'vertices world coordinates NW, NE, SE, SW 1835 | f.write("\t{\n\t\t" + 1836 | "{ " + str( int (w[3].x ) ) + ", " + str( int (w[3].y ) ) + ", " + str( int (w[3].z ) ) + ", 0 },\n\t\t" + 1837 | "{ " + str( int (w[2].x ) ) + ", " + str( int (w[2].y ) ) + ", " + str( int (w[2].z ) ) + ", 0 },\n\t\t" + 1838 | "{ " + str( int (w[0].x ) ) + ", " + str( int (w[0].y ) ) + ", " + str( int (w[0].z ) ) + ", 0 },\n\t\t" + 1839 | "{ " + str( int (w[1].x ) ) + ", " + str( int (w[1].y ) ) + ", " + str( int (w[1].z ) ) + ", 0 }\n" + 1840 | "\t},\n" ) 1841 | f.write( after ) 1842 | # UNUSED : Screen coords 1843 | # ~ s = objVertWtoS( scene, camera, portal ) 1844 | # ~ f.write("\t{\n\t\t" + 1845 | # ~ "{ " + str( int (s[3].x ) ) + ", " + str( int (s[3].y ) ) + ", " + str( int (s[3].z ) ) + ", 0 },\n\t\t" + 1846 | # ~ "{ " + str( int (s[2].x ) ) + ", " + str( int (s[2].y ) ) + ", " + str( int (s[2].z ) ) + ", 0 },\n\t\t" + 1847 | # ~ "{ " + str( int (s[0].x ) ) + ", " + str( int (s[0].y ) ) + ", " + str( int (s[0].z ) ) + ", 0 },\n\t\t" + 1848 | # ~ "{ " + str( int (s[1].x ) ) + ", " + str( int (s[1].y ) ) + ", " + str( int (s[1].z ) ) + ", 0 }\n" + 1849 | # ~ "\t},\n" ) 1850 | f.write("\t" + str( len( visibleTarget ) ) + ",\n" + 1851 | "\t{\n") 1852 | for target in range( len( visibleTarget ) ) : 1853 | f.write( "\t\t&" + fileName + "_mesh" + CleanName(visibleTarget[target].name) ) 1854 | if target < len(visibleTarget) - 1: 1855 | f.write(",\n") 1856 | f.write("\n\t}\n" + 1857 | "};\n\n") 1858 | # Write camera angles in an array for loops 1859 | f.write("CAMANGLE * " + fileName + "_camAngles[" + str(len(camAngles)) + "] = {\n") 1860 | for camera in camAngles: 1861 | prefix = CleanName(camera.name) 1862 | f.write("\t&" + fileName + "_camAngle_" + prefix + ",\n") 1863 | f.write("};\n\n") 1864 | # Feed to level_symbols 1865 | level_symbols.append( "CAMANGLE * " + fileName + "_camAngles[" + str(len(camAngles)) + "]" ) 1866 | ## Spatial Partitioning 1867 | # Planes in the level - dict of strings 1868 | LvlPlanes = {} 1869 | # Objects in the level - dict of strings 1870 | LvlObjects = {} 1871 | # Link objects to their respective plane 1872 | PlanesObjects = defaultdict(dict) 1873 | PlanesRigidBodies = defaultdict(dict) 1874 | # List of objects that can travel ( actor , moveable props...) 1875 | Moveables = [] 1876 | # Store starting plane for moveables 1877 | PropPlane = defaultdict(dict) 1878 | # Store XY1, XY2 values 1879 | Xvalues = [] 1880 | Yvalues = [] 1881 | # Find planes and objects bounding boxes 1882 | # Planes first 1883 | for o in bpy.data.objects: 1884 | # If orphan, ignore 1885 | if o.users == 0: 1886 | continue 1887 | # Only loop through meshes 1888 | if o.type == 'MESH' and not o.data.get('isPortal'): 1889 | # Get Level planes coordinates 1890 | if o.data.get('isLevel'): 1891 | # World matrix is used to convert local to global coordinates 1892 | mw = o.matrix_world 1893 | for v in bpy.data.objects[o.name].data.vertices: 1894 | # Convert local to global coords 1895 | Xvalues.append( (mw * v.co).x ) 1896 | Yvalues.append( (mw * v.co).y ) 1897 | LvlPlanes[o.name] = {'x1' : min(Xvalues), 1898 | 'y1' : min(Yvalues), 1899 | 'x2' : max(Xvalues), 1900 | 'y2' : max(Yvalues)} 1901 | # Clear X/Y lists for next iteration 1902 | Xvalues = [] 1903 | Yvalues = [] 1904 | # For each object not a plane, get its coordinates 1905 | if not o.data.get('isLevel'): 1906 | # World matrix is used to convert local to global coordinates 1907 | mw = o.matrix_world 1908 | for v in bpy.data.objects[o.name].data.vertices: 1909 | # Convert local to global coords 1910 | Xvalues.append( (mw * v.co).x ) 1911 | Yvalues.append( (mw * v.co).y ) 1912 | LvlObjects[o.name] = {'x1' : min(Xvalues), 1913 | 'y1' : min(Yvalues), 1914 | 'x2' : max(Xvalues), 1915 | 'y2' : max(Yvalues)} 1916 | # Clear X/Y lists for next iteration 1917 | Xvalues = [] 1918 | Yvalues = [] 1919 | # Add objects that can travel to the 1920 | if o.data.get("isRigidBody"): 1921 | Moveables.append(o) 1922 | # Declare LvlPlanes nodes to avoid declaration dependency issues 1923 | # ~ for k in LvlPlanes.keys(): 1924 | # ~ f.write("NODE node" + CleanName(k) + ";\n\n") 1925 | # Sides of the plane to check 1926 | checkSides = [ 1927 | ['N','S'], 1928 | ['S','N'], 1929 | ['W','E'], 1930 | ['E','W'] 1931 | ] 1932 | # Generate a dict : 1933 | # ~ { 1934 | # ~ 'S' : [] 1935 | # ~ 'N' : [] list of planes connected to this plane, and side they're on 1936 | # ~ 'W' : [] 1937 | # ~ 'E' : [] 1938 | # ~ 'objects' : [] list of objects on this plane 1939 | # ~ '' 1940 | # ~ } 1941 | overlappingObject = [] 1942 | for p in LvlPlanes: 1943 | # Find objects on plane 1944 | for o in LvlObjects: 1945 | # If object is overlapping between several planes 1946 | if isInPlane(LvlPlanes[p], LvlObjects[o]) > 1: 1947 | # Object not actor 1948 | if o != actorPtr: 1949 | # Object not in list 1950 | if o not in overlappingObject: 1951 | overlappingObject.append(o) 1952 | else: 1953 | overlappingObject.remove(o) 1954 | # Add this object to the plane's list 1955 | if 'objects' in PlanesObjects[p]: 1956 | PlanesObjects[p]['objects'].append(o) 1957 | else: 1958 | PlanesObjects[p] = { 'objects' : [o] } 1959 | # If object is above plane 1960 | if isInPlane(LvlPlanes[p], LvlObjects[o]) == 1: 1961 | # Add all objects but the actor 1962 | if o != actorPtr: 1963 | # Add this object to the plane's list 1964 | if 'objects' in PlanesObjects[p]: 1965 | PlanesObjects[p]['objects'].append(o) 1966 | else: 1967 | PlanesObjects[p] = { 'objects' : [o] } 1968 | else: 1969 | # If actor is on this plane, use it as starting node 1970 | levelPtr = p 1971 | nodePtr = p 1972 | # Add moveable objects in every plane 1973 | for moveable in Moveables: 1974 | # If moveable is not actor 1975 | if moveable.data.get( 'isProp' ): 1976 | # If is in current plane, add it to the list 1977 | if isInPlane( LvlPlanes[ p ], LvlObjects[ moveable.name ] ) : 1978 | PropPlane[moveable] = CleanName(p) 1979 | # ~ PropPlane[moveable] = CleanName(bpy.data.objects[p].data.name) 1980 | if 'rigidbodies' in PlanesRigidBodies[p]: 1981 | if moveable.name not in PlanesRigidBodies[p]['rigidbodies']: 1982 | # ~ PlanesRigidBodies[ p ][ 'rigidbodies' ].append(CleanName( moveable.name ) ) 1983 | PlanesRigidBodies[ p ][ 'rigidbodies' ].append(moveable.name ) 1984 | else: 1985 | PlanesRigidBodies[p] = { 'rigidbodies' : [ moveable.name ] } 1986 | # Find surrounding planes 1987 | for op in LvlPlanes: 1988 | # Loop on other planes 1989 | if op is not p: 1990 | # Check each side 1991 | for s in checkSides: 1992 | # If connected ('connected') plane exists... 1993 | if checkLine( 1994 | getSepLine(p, s[0])[0], 1995 | getSepLine(p, s[0])[1], 1996 | getSepLine(p, s[0])[2], 1997 | getSepLine(p, s[0])[3], 1998 | getSepLine(op, s[1])[0], 1999 | getSepLine(op, s[1])[1], 2000 | getSepLine(op, s[1])[2], 2001 | getSepLine(op, s[1])[3] 2002 | ) == 'connected' and ( 2003 | isInPlane( LvlPlanes[p], LvlPlanes[op] ) 2004 | ): 2005 | # ... add it to the list 2006 | if 'siblings' not in PlanesObjects[p]: 2007 | PlanesObjects[p]['siblings'] = {} 2008 | # If more than one plane is connected on the same side of the plane, 2009 | # add it to the corresponding list 2010 | if s[0] in PlanesObjects[p]['siblings']: 2011 | PlanesObjects[p]['siblings'][s[0]].append(op) 2012 | else: 2013 | PlanesObjects[p]['siblings'][s[0]] = [op] 2014 | pName = CleanName(p) 2015 | # Write SIBLINGS structure 2016 | nSiblings = 0 2017 | if 'siblings' in PlanesObjects[p]: 2018 | if 'S' in PlanesObjects[ p ][ 'siblings' ]: 2019 | nSiblings += len( PlanesObjects[ p ][ 'siblings' ][ 'S' ] ) 2020 | if 'N' in PlanesObjects[ p ][ 'siblings' ]: 2021 | nSiblings += len( PlanesObjects[ p ][ 'siblings' ][ 'N' ] ) 2022 | if 'E' in PlanesObjects[ p ][ 'siblings' ]: 2023 | nSiblings += len( PlanesObjects[ p ][ 'siblings' ][ 'E' ] ) 2024 | if 'W' in PlanesObjects[ p ][ 'siblings' ]: 2025 | nSiblings += len( PlanesObjects[ p ][ 'siblings' ][ 'W' ] ) 2026 | f.write("SIBLINGS " + fileName + "_node" + pName + "_siblings = {\n" + 2027 | "\t" + str(nSiblings) + ",\n" + 2028 | "\t{\n") 2029 | if 'siblings' in PlanesObjects[p]: 2030 | i = 0 2031 | for side in PlanesObjects[p]['siblings']: 2032 | for sibling in PlanesObjects[p]['siblings'][side]: 2033 | f.write("\t\t&" + fileName + "_node" + CleanName(sibling) ) 2034 | if i < ( nSiblings - 1 ) : 2035 | f.write(",") 2036 | i += 1 2037 | f.write("\n") 2038 | else: 2039 | f.write("\t\t0\n") 2040 | f.write("\t}\n" + 2041 | "};\n\n") 2042 | # Feed to level_symbols 2043 | level_symbols.append( "SIBLINGS " + fileName + "_node" + pName + "_siblings" ) 2044 | # Write CHILDREN static objects structure 2045 | f.write("CHILDREN " + fileName + "_node" + pName + "_objects = {\n") 2046 | if 'objects' in PlanesObjects[p]: 2047 | f.write("\t" + str(len(PlanesObjects[p]['objects'])) + ",\n" + 2048 | "\t{\n") 2049 | i = 0 2050 | for obj in PlanesObjects[p]['objects']: 2051 | f.write( "\t\t&" + fileName + "_mesh" + CleanName(bpy.data.objects[obj].data.name)) 2052 | if i < len(PlanesObjects[p]['objects']) - 1: 2053 | f.write(",") 2054 | i += 1 2055 | f.write("\n") 2056 | else: 2057 | f.write("\t0,\n" + 2058 | "\t{\n\t\t0\n") 2059 | f.write("\t}\n" + 2060 | "};\n\n") 2061 | # Feed to level_symbols 2062 | level_symbols.append( "CHILDREN " + fileName + "_node" + pName + "_objects" ) 2063 | # Write CHILDREN rigidbodies structure 2064 | f.write("CHILDREN " + fileName + "_node" + pName + "_rigidbodies = {\n") 2065 | if 'rigidbodies' in PlanesRigidBodies[p]: 2066 | f.write("\t" + str(len(PlanesRigidBodies[p]['rigidbodies'])) + ",\n" + 2067 | "\t{\n") 2068 | i = 0 2069 | for obj in PlanesRigidBodies[p]['rigidbodies']: 2070 | # ~ f.write( "\t\t&" + fileName + "_mesh" + CleanName(obj)) 2071 | f.write( "\t\t&" + fileName + "_mesh" + CleanName(bpy.data.objects[obj].data.name)) 2072 | if i < len(PlanesRigidBodies[p]['rigidbodies']) - 1: 2073 | f.write(",") 2074 | i += 1 2075 | f.write("\n") 2076 | else: 2077 | f.write("\t0,\n" + 2078 | "\t{\n\t\t0\n") 2079 | f.write("\t}\n" + 2080 | "};\n\n") 2081 | # Feed to level_symbols 2082 | level_symbols.append( "CHILDREN " + fileName + "_node" + pName + "_rigidbodies" ) 2083 | # Write NODE structure 2084 | f.write( "NODE " + fileName + "_node" + pName + " = {\n" + 2085 | "\t&" + fileName + "_mesh" + CleanName(bpy.data.objects[p].data.name) + ",\n" + 2086 | "\t&" + fileName + "_node" + pName + "_siblings,\n" + 2087 | "\t&" + fileName + "_node" + pName + "_objects,\n" + 2088 | "\t&" + fileName + "_node" + pName + "_rigidbodies\n" + 2089 | "};\n\n" ) 2090 | # Feed to level_symbols 2091 | level_symbols.append( "NODE " + fileName + "_node" + pName ) 2092 | f.write("MESH * " + fileName + "_actorPtr = &" + fileName + "_mesh" + CleanName(actorPtr) + ";\n") 2093 | # ~ f.write("MESH * " + fileName + "_levelPtr = &" + fileName + "_mesh" + CleanName(levelPtr) + ";\n") 2094 | f.write("MESH * " + fileName + "_levelPtr = &" + fileName + "_mesh" + CleanName(bpy.data.objects[levelPtr].data.name) + ";\n") 2095 | f.write("MESH * " + fileName + "_propPtr = &" + fileName + "_mesh" + propPtr + ";\n\n") 2096 | f.write("CAMANGLE * " + fileName + "_camPtr = &" + fileName + "_camAngle_" + CleanName(defaultCam) + ";\n\n") 2097 | f.write("NODE * " + fileName + "_curNode = &" + fileName + "_node" + CleanName(nodePtr) + ";\n\n") 2098 | # Feed to level_symbols 2099 | level_symbols.append( "MESH * " + fileName + "_actorPtr" ) 2100 | level_symbols.append( "MESH * " + fileName + "_levelPtr" ) 2101 | level_symbols.append( "MESH * " + fileName + "_propPtr" ) 2102 | level_symbols.append( "CAMANGLE * " + fileName + "_camPtr" ) 2103 | level_symbols.append( "NODE * " + fileName + "_curNode" ) 2104 | ## Sound 2105 | # Use dict generated earlier 2106 | # Default values 2107 | XAFiles = "0" 2108 | VAGBank = "0" 2109 | level_sounds = "0" 2110 | # If sound objects in scene 2111 | if soundFiles: 2112 | # Deal with VAGs 2113 | VAGBank = writeVAGbank(f, soundFiles, level_symbols) 2114 | if VAGBank and VAGBank != "0": 2115 | VAGBank = "&" + fileName + "_VAGBank" 2116 | # Deal with XA 2117 | XAlist = writeXAbank(f, soundFiles, level_symbols) 2118 | writeXAfiles(f, XAlist, fileName) 2119 | if XAlist: 2120 | XAmanifest(XAlist) 2121 | XAinterleave(XAlist) 2122 | # Update mkpsxiso config file if it exists 2123 | configFile = expFolder + os.sep + os.path.relpath(self.exp_isoCfg) 2124 | addXAtoISO(XAlist, configFile) 2125 | XAFiles = len(XAlist) 2126 | if XAFiles and XAFiles != "0": 2127 | XAFiles = "&" + fileName + "_XAFiles" 2128 | # Write Sound obj 2129 | level_sounds = writeSoundObj(f, soundFiles, level_symbols) 2130 | if level_sounds and level_sounds != "0": 2131 | level_sounds = "&" + fileName + "_sounds" 2132 | 2133 | # Write LEVEL struct 2134 | f.write( 2135 | "LEVEL " + fileName + " = {\n" + 2136 | "\t&" + fileName + "_BGc,\n" + 2137 | "\t&" + fileName + "_BKc,\n" + 2138 | "\t&" + fileName + "_cmat,\n" + 2139 | "\t&" + fileName + "_lgtmat,\n" + 2140 | "\t(MESH **)&" + fileName + "_meshes,\n" + 2141 | "\t&" + fileName + "_meshes_length,\n" + 2142 | "\t&" + fileName + "_mesh" + CleanName(actorPtr)+ ",\n" + 2143 | "\t&" + fileName + "_mesh" + CleanName(bpy.data.objects[levelPtr].data.name)+ ",\n" + 2144 | "\t&" + fileName + "_mesh" + propPtr + ",\n" + 2145 | "\t&" + fileName + "_camAngle_" + CleanName(defaultCam) + ",\n" + 2146 | "\t&" + fileName + "_camPath,\n" + 2147 | "\t(CAMANGLE **)&" + fileName + "_camAngles,\n" + 2148 | "\t&" + fileName + "_node" + CleanName(nodePtr) + ",\n" + 2149 | "\t" + level_sounds + ",\n" + 2150 | "\t" + VAGBank + ",\n" + 2151 | "\t" + XAFiles + "\n" + 2152 | "};\n\n") 2153 | # Set default camera back in Blender 2154 | if defaultCam != 'NULL': 2155 | bpy.context.scene.camera = bpy.data.objects[ defaultCam ] 2156 | f.close() 2157 | # Using a UGLY method here , sorry ! 2158 | # We're re-opening the file we just closed to substracts some values that were not available 2159 | # Fill in node in MESH structs 2160 | # Get the file content 2161 | f = open(os.path.normpath(level_c),"r") 2162 | filedata = f.read() 2163 | f.close() 2164 | # Declare LvlPlanes nodes to avoid declaration dependency issues 2165 | # Constuct and store the new string 2166 | Node_declaration = '' 2167 | for k in LvlPlanes.keys(): 2168 | Node_declaration += "NODE " + fileName + "_node" + CleanName(k) + ";\n\n" 2169 | level_symbols.append( "NODE " + fileName + "_node" + CleanName(k) ) 2170 | # Do the substitution only once 2171 | newdata = filedata.replace("NODE_DECLARATION\n", Node_declaration, 1) 2172 | newdata = filedata.replace("NODE_DECLARATION\n", "") 2173 | # Now substitute mesh name for corresponding plane's NODE 2174 | for moveable in PropPlane: 2175 | newdata = newdata.replace("subs_" + CleanName(moveable.name), "&" + fileName + "_node" + PropPlane[moveable]) 2176 | # Subsitute mesh name with 0 in the other MESH structs 2177 | newdata = sub("(?m)^\tsubs_.*$", "\t0,", newdata ) 2178 | # Open and write file 2179 | f = open(os.path.normpath(level_c),"w") 2180 | f.write( newdata ) 2181 | f.close() 2182 | ## Level forward declarations (level.h) 2183 | h = open(os.path.normpath(level_h),"w+") 2184 | h.write( 2185 | '#pragma once\n' + 2186 | '#include "../custom_types.h"\n' + 2187 | '#include "../include/defines.h"\n\n' 2188 | ) 2189 | for symbol in level_symbols: 2190 | h.write( "extern " + symbol + ";\n") 2191 | h.close() 2192 | return {'FINISHED'}; 2193 | def menu_func(self, context): 2194 | self.layout.operator(ExportMyFormat.bl_idname, text="PSX Format(.c)"); 2195 | def register(): 2196 | bpy.utils.register_module(__name__); 2197 | bpy.types.INFO_MT_file_export.append(menu_func); 2198 | def unregister(): 2199 | bpy.utils.unregister_module(__name__); 2200 | bpy.types.INFO_MT_file_export.remove(menu_func); 2201 | if __name__ == "__main__": 2202 | register() 2203 | -------------------------------------------------------------------------------- /startfile.blend: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ABelliqueux/blender_io_export_psx_mesh/18b7e607bc2acbbcbbcf53dd943eaf2f0f82f40d/startfile.blend --------------------------------------------------------------------------------