├── .gitignore ├── README.md ├── __init__.py ├── export_ica.py ├── export_imd.py ├── export_ita.py ├── export_itp.py ├── export_nitro.py ├── local_logger.py ├── nns_material.py ├── nns_model.py ├── nns_object.py ├── nns_tga.py ├── primitive.py ├── util.py └── version.py /.gitignore: -------------------------------------------------------------------------------- 1 | /__pycache__ 2 | /.vscode 3 | io_scene_nns.zip 4 | io_scene_nns/ 5 | .idea/ 6 | test.blend 7 | test.blend1 8 | Text 9 | test-billboard.blend 10 | test-billboard.blend1 11 | node.txt 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Nintendo Nitro System Blender Plugin 2 | 3 | A plugin for blender 2.8 that is able to export intermediate model format (.imd) for use in homebrew projects or other NDS related activities. 4 | 5 | ## How to install 6 | 7 | Download the latest zip from releases and extract it in the addons folder. Alternatively you can also install the plugin from the addons window. 8 | 9 | ## Instructions 10 | 11 | After installing the plugin, you can export your model to imd by going to the export tab and clicking on "Nitro IMD". You can add a material to your object by clicking on the "Create NNS Material" button in the material tab. Be aware that vertex colored models need a vertex color layer called "Col" to be present. Alternatlivey you can also use a PrincipledBSDF node with limited options. 12 | 13 | Be sure to use nitro tga files for textures. All other formats will be ignored. You can generate nitro tga's by using Optpix or NitroPaint https://github.com/Garhoogin/NitroPaint/releases 14 | 15 | ## Material preview 16 | 17 | You can preview your material by switching to lookdev mode or rendered mode. This feature aids you in crafting your material but it is not a 100% accurate rendering of what it will look like on the ds. Be sure to have a vertex color layer named "Col" when using vertex colored materials, otherwise your material will be black. 18 | 19 | As for vertex lighting, the lights parameters can be changed in the section called "NNS scene" in the panel on the right in the 3D view window, by default the light properties are similar to the ones in mario kart ds tracks. 20 | 21 | There is also fog, you can enable it for the entire scene and per material, its parameters are just above the lights settings in the 3d view panel. 22 | 23 | ## Exporting bones and animation 24 | 25 | You can export your rigged model and the current active animation by enabling "export .ica" in the export window. You can influence the size-quality ratio by changing the tolerance and frame step mode. Be sure to set the node compression to none, otherwise no bones will be exported. 26 | 27 | Please keep these points in mind when making a rig and animation: 28 | * Make sure that each vertex is only part of one vertex group. 29 | * Do not use extreme transforms on your mesh or armature (Like using a scale of 0.00002 for example). Apply them if needed. 30 | * The length of the animation is set by the playback length of the scene. 31 | 32 | ## Exporting texture animation 33 | 34 | You can export texture animation by animating the SRT values in the material and then enabling .ita for export. 35 | 36 | ## Troubleshooting 37 | 38 | ### Material doesn't have transparency (or wrong transparency) in blender 39 | 40 | Under the settings tab of your material, you can choose the blend method. Please choose the appropriate blend method for your material. If your material doesn't use any kind of transparency or tranlucency, be sure to set it on opaque. 41 | 42 | ### Material doesn't work when updating blender or the plugin 43 | 44 | Change the type of the material, for example from vertex colored to solid+diffuse, then change it back to vertex colored 45 | 46 | ## Special thanks 47 | * Stomatol for suggesting features and helping with shader nodes 48 | * Gericom for technical knowledge on NNS and tristripping 49 | * SGC for suggesting features 50 | * PK dab for testing the plugin and suggesting features 51 | * Riidefi for giving examples on how to make a plugin 52 | -------------------------------------------------------------------------------- /__init__.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | from bpy.props import (BoolProperty, 3 | FloatProperty, 4 | StringProperty, 5 | EnumProperty) 6 | from bpy_extras.io_utils import ExportHelper 7 | from .nns_material import material_register, material_unregister 8 | from .nns_object import object_register, object_unregister 9 | from . import version 10 | 11 | 12 | bl_info = { 13 | "name": "Nitro Intermediate (.imd, .ita, .ica, .itp)", 14 | "author": "Jelle Streekstra, Gabriele Mercurio", 15 | "version": (0, 2, 1), 16 | "blender": (2, 80, 0), 17 | "location": "File > Export", 18 | "description": "Export intermediate files for Nitro system", 19 | "category": "Export" 20 | } 21 | 22 | 23 | class NTR_PT_export_imd(bpy.types.Panel): 24 | """Export to a Nitro Intermediate""" 25 | 26 | bl_space_type = 'FILE_BROWSER' 27 | bl_region_type = 'TOOL_PROPS' 28 | bl_label = "Intermediate Model Data (.imd)" 29 | bl_parent_id = "FILE_PT_operator" 30 | bl_options = {'DEFAULT_CLOSED'} 31 | 32 | @classmethod 33 | def poll(cls, context): 34 | sfile = context.space_data 35 | operator = sfile.active_operator 36 | 37 | return operator.bl_idname == "EXPORT_OT_nitro" 38 | 39 | def draw(self, context): 40 | layout = self.layout 41 | sfile = context.space_data 42 | operator = sfile.active_operator 43 | 44 | layout.prop(operator, 'imd_export') 45 | layout.prop(operator, 'imd_magnification') 46 | layout.prop(operator, 'imd_use_primitive_strip') 47 | layout.prop(operator, 'imd_compress_nodes') 48 | 49 | 50 | class NTR_PT_export_ita(bpy.types.Panel): 51 | bl_space_type = 'FILE_BROWSER' 52 | bl_region_type = 'TOOL_PROPS' 53 | bl_label = "Intermediate Texture Animation (.ita)" 54 | bl_parent_id = "FILE_PT_operator" 55 | bl_options = {'DEFAULT_CLOSED'} 56 | 57 | @classmethod 58 | def poll(cls, context): 59 | sfile = context.space_data 60 | operator = sfile.active_operator 61 | 62 | return operator.bl_idname == "EXPORT_OT_nitro" 63 | 64 | def draw(self, context): 65 | layout = self.layout 66 | sfile = context.space_data 67 | operator = sfile.active_operator 68 | 69 | layout.prop(operator, 'ita_export') 70 | # layout.prop(operator, 'ita_rotate_tolerance') 71 | # layout.prop(operator, 'ita_scale_tolerance') 72 | # layout.prop(operator, 'ita_translate_tolerance') 73 | 74 | 75 | class NTR_PT_export_ica(bpy.types.Panel): 76 | bl_space_type = 'FILE_BROWSER' 77 | bl_region_type = 'TOOL_PROPS' 78 | bl_label = "Intermediate Character Animation (.ica)" 79 | bl_parent_id = "FILE_PT_operator" 80 | bl_options = {'DEFAULT_CLOSED'} 81 | 82 | @classmethod 83 | def poll(cls, context): 84 | sfile = context.space_data 85 | operator = sfile.active_operator 86 | 87 | return operator.bl_idname == "EXPORT_OT_nitro" 88 | 89 | def draw(self, context): 90 | layout = self.layout 91 | sfile = context.space_data 92 | operator = sfile.active_operator 93 | 94 | layout.prop(operator, 'ica_export') 95 | layout.prop(operator, 'ica_frame_step') 96 | layout.prop(operator, 'ica_rotate_tolerance') 97 | layout.prop(operator, 'ica_scale_tolerance') 98 | layout.prop(operator, 'ica_translate_tolerance') 99 | 100 | 101 | class NTR_PT_export_itp(bpy.types.Panel): 102 | bl_space_type = 'FILE_BROWSER' 103 | bl_region_type = 'TOOL_PROPS' 104 | bl_label = "Intermediate Texture Pattern (.itp)" 105 | bl_parent_id = "FILE_PT_operator" 106 | bl_options = {'DEFAULT_CLOSED'} 107 | 108 | @classmethod 109 | def poll(cls, context): 110 | sfile = context.space_data 111 | operator = sfile.active_operator 112 | 113 | return operator.bl_idname == "EXPORT_OT_nitro" 114 | 115 | def draw(self, context): 116 | layout = self.layout 117 | sfile = context.space_data 118 | operator = sfile.active_operator 119 | 120 | layout.prop(operator, 'itp_export') 121 | 122 | 123 | class ExportNitro(bpy.types.Operator, ExportHelper): 124 | bl_idname = "export.nitro" 125 | bl_label = "Export Nitro" 126 | bl_options = {'PRESET'} 127 | 128 | filename_ext = "" 129 | filter_glob: StringProperty( 130 | default="*.imd;*.ita;*.ica,*.itp", 131 | options={'HIDDEN'}, 132 | ) 133 | 134 | pretty_print: BoolProperty(name="Pretty print", default=True) 135 | 136 | generate_log: BoolProperty(name="Generate log file", default=False) 137 | 138 | imd_export: BoolProperty(name="Export .imd", default=True) 139 | imd_magnification: FloatProperty(name="Magnification", 140 | default=0.0625, 141 | precision=4) 142 | imd_use_primitive_strip: BoolProperty(name="Use primitive strip", 143 | default=True) 144 | imd_compress_nodes: EnumProperty( 145 | name="Compress nodes", 146 | items=[ 147 | ("none", "None", '', 1), 148 | ("cull", "Cull", '', 2), 149 | ("merge", "Merge", '', 3), 150 | ("unite", "Unite", '', 4), 151 | ("unite_combine", "Unite and combine polygon", '', 5), 152 | ]) 153 | 154 | ita_export: BoolProperty(name="Export .ita") 155 | ita_rotate_tolerance: FloatProperty(name="Rotation tolerance", 156 | default=0.100000, 157 | precision=6) 158 | ita_scale_tolerance: FloatProperty(name="Scale tolerance", 159 | default=0.100000, 160 | precision=6) 161 | ita_translate_tolerance: FloatProperty(name="Translation tolerance", 162 | default=0.010000, 163 | precision=6) 164 | 165 | ica_export: BoolProperty(name="Export .ica") 166 | ica_frame_step: EnumProperty( 167 | name="Frame step mode", 168 | items=[ 169 | ("1", "1", '', 1), 170 | ("2", "2", '', 2), 171 | ("4", "4", '', 3), 172 | ]) 173 | ica_rotate_tolerance: FloatProperty(name="Rotation tolerance", 174 | default=0.100000, 175 | precision=6) 176 | ica_scale_tolerance: FloatProperty(name="Scale tolerance", 177 | default=0.100000, 178 | precision=6) 179 | ica_translate_tolerance: FloatProperty(name="Translation tolerance", 180 | default=0.010000, 181 | precision=6) 182 | 183 | itp_export: BoolProperty(name="Export .itp") 184 | 185 | def execute(self, context): 186 | from . import export_nitro 187 | 188 | settings = self.as_keywords() 189 | export_nitro.save(context, settings) 190 | self.report({'INFO'}, 'NNS: Exported scene.') 191 | return {'FINISHED'} 192 | 193 | def draw(self, context): 194 | layout = self.layout 195 | sfile = context.space_data 196 | operator = sfile.active_operator 197 | layout.prop(operator, 'pretty_print') 198 | layout.prop(operator, 'generate_log') 199 | 200 | 201 | def menu_func_export(self, context): 202 | self.layout.operator( 203 | ExportNitro.bl_idname, 204 | text="Nitro Intermediate (.imd, .ita, .ica, .itp)") 205 | 206 | 207 | def register(): 208 | version.addon_version = bl_info["version"] 209 | bpy.utils.register_class(ExportNitro) 210 | bpy.utils.register_class(NTR_PT_export_imd) 211 | bpy.utils.register_class(NTR_PT_export_ita) 212 | bpy.utils.register_class(NTR_PT_export_ica) 213 | bpy.utils.register_class(NTR_PT_export_itp) 214 | material_register() 215 | object_register() 216 | bpy.types.TOPBAR_MT_file_export.append(menu_func_export) 217 | 218 | 219 | def unregister(): 220 | bpy.utils.unregister_class(ExportNitro) 221 | bpy.utils.unregister_class(NTR_PT_export_imd) 222 | bpy.utils.unregister_class(NTR_PT_export_ita) 223 | bpy.utils.unregister_class(NTR_PT_export_ica) 224 | bpy.utils.unregister_class(NTR_PT_export_itp) 225 | material_unregister() 226 | object_unregister() 227 | bpy.types.TOPBAR_MT_file_export.remove(menu_func_export) 228 | 229 | 230 | if __name__ == "__main__": 231 | register() 232 | -------------------------------------------------------------------------------- /export_ica.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | from mathutils import Quaternion 3 | import xml.etree.ElementTree as ET 4 | from math import degrees 5 | from .nns_model import NitroModel 6 | 7 | 8 | settings = None 9 | 10 | 11 | class NitroBCAInfo(): 12 | def __init__(self): 13 | self.frame_size = 0 14 | 15 | def set_frame_size(self, size): 16 | if size > self.frame_size: 17 | self.frame_size = size 18 | 19 | 20 | class NitroBCAData(): 21 | def __init__(self, name): 22 | self.name = name 23 | if name == 'node_scale_data': 24 | self.data = [1.0] 25 | else: 26 | self.data = [0.0] 27 | 28 | def find_in_data(self, x): 29 | l1, l2 = len(self.data), len(x) 30 | for i in range(l1): 31 | if self.data[i:i+l2] == x: 32 | return i 33 | return -1 34 | 35 | def add_data(self, data): 36 | """ 37 | Adds and compresses data. Returns a tuple with head and size. 38 | """ 39 | head = len(self.data) 40 | if all(elem == data[0] for elem in data): 41 | # This animation consists of one element. 42 | # Try to find if the value already exist 43 | # otherwise add it. 44 | try: 45 | head = self.data.index(data[0]) 46 | except ValueError: 47 | self.data.append(data[0]) 48 | return (head, 1) 49 | else: 50 | # Try to find the pattern in the existing 51 | # data first. 52 | length = len(data) 53 | index = self.find_in_data(data) 54 | if index != -1: 55 | # Found the pattern, index is now the head. 56 | head = index 57 | else: 58 | # Didn't find anything, try checking if the 59 | # last inserted value is equal to the first 60 | # value in the data. 61 | if self.data[-1] == data[0]: 62 | data.pop(0) 63 | head = head - 1 64 | self.data.extend(data) 65 | return (head, length) 66 | 67 | 68 | class NitroBCAReference(): 69 | def __init__(self): 70 | self.data_head = -1 71 | self.data_size = -1 72 | self.frame_step = 1 73 | 74 | 75 | class NitroBCAAnimation(): 76 | def __init__(self, index): 77 | self.index = index 78 | self.references = { 79 | 'scale_x': NitroBCAReference(), 80 | 'scale_y': NitroBCAReference(), 81 | 'scale_z': NitroBCAReference(), 82 | 'rotate_x': NitroBCAReference(), 83 | 'rotate_y': NitroBCAReference(), 84 | 'rotate_z': NitroBCAReference(), 85 | 'translate_x': NitroBCAReference(), 86 | 'translate_y': NitroBCAReference(), 87 | 'translate_z': NitroBCAReference(), 88 | } 89 | 90 | def set_reference(self, name, head, size, step): 91 | reference = self.references[name] 92 | reference.data_head = head 93 | reference.data_size = size 94 | reference.frame_step = step 95 | 96 | 97 | class NitroBCA(): 98 | def __init__(self, model: NitroModel): 99 | self.info = NitroBCAInfo() 100 | self.scale_data = NitroBCAData('node_scale_data') 101 | self.rotate_data = NitroBCAData('node_rotate_data') 102 | self.translate_data = NitroBCAData('node_translate_data') 103 | self.animations = [] 104 | self.model = model 105 | 106 | def collect(self): 107 | # Make a reference for each node first. 108 | for node in self.model.nodes: 109 | self.find_animation(node.index) 110 | 111 | for obj in bpy.context.view_layer.objects: 112 | if obj.type != 'ARMATURE': 113 | continue 114 | scene = bpy.context.scene 115 | mtxs = [] 116 | frame_old = scene.frame_current 117 | for frame in range(scene.frame_start, scene.frame_end + 1): 118 | scene.frame_set(frame) 119 | 120 | transforms = {} 121 | for pose in obj.pose.bones: 122 | transform = pose.matrix.copy() 123 | if pose.parent: 124 | inv = pose.parent.matrix.inverted() 125 | transform = inv @ transform 126 | transforms[pose.bone.name] = transform 127 | mtxs.append(transforms) 128 | 129 | # Althought this was used in the sm64ds plugin, it doesn't 130 | # work. You need to inverse multiply it with the parent 131 | # logically. 132 | # mtxs.append([b.matrix.copy() for b in obj.pose.bones]) 133 | scene.frame_set(frame_old) 134 | 135 | self.info.set_frame_size(len(mtxs)) 136 | for i, bone in enumerate(obj.data.bones): 137 | self.process_bone(bone, [m[bone.name] for m in mtxs]) 138 | 139 | # Set the proper data for each non-animated node. 140 | for animation in self.animations: 141 | node = self.model.nodes[animation.index] 142 | scale = { 143 | 'scale_x': round(node.scale[0], 6), 144 | 'scale_y': round(node.scale[1], 6), 145 | 'scale_z': round(node.scale[2], 6) 146 | } 147 | for key in scale: 148 | if animation.references[key].data_head == -1: 149 | result = self.scale_data.add_data([scale[key]]) 150 | animation.set_reference(key, result[0], result[1], 1) 151 | 152 | rotate = { 153 | 'rotate_x': round(node.rotate[0], 6), 154 | 'rotate_y': round(node.rotate[1], 6), 155 | 'rotate_z': round(node.rotate[2], 6) 156 | } 157 | for key in rotate: 158 | if animation.references[key].data_head == -1: 159 | result = self.rotate_data.add_data([rotate[key]]) 160 | animation.set_reference(key, result[0], result[1], 1) 161 | 162 | translate = { 163 | 'translate_x': round(node.translate[0], 6), 164 | 'translate_y': round(node.translate[1], 6), 165 | 'translate_z': round(node.translate[2], 6) 166 | } 167 | for key in translate: 168 | if animation.references[key].data_head == -1: 169 | result = self.translate_data.add_data([translate[key]]) 170 | animation.set_reference(key, result[0], result[1], 1) 171 | 172 | def process_bone(self, bone, transforms): 173 | node = self.model.find_node(bone.name) 174 | animation = self.find_animation(node.index) 175 | 176 | mag = self.model.settings['imd_magnification'] 177 | 178 | scales = {'scale_x': [], 'scale_y': [], 'scale_z': []} 179 | rotations = {'rotate_x': [], 'rotate_y': [], 'rotate_z': []} 180 | trans = {'translate_x': [], 'translate_y': [], 'translate_z': []} 181 | 182 | # Get frames. 183 | for transform in transforms: 184 | scale = transform.to_scale() 185 | scales['scale_x'].append(round(scale[0], 6)) 186 | scales['scale_y'].append(round(scale[1], 6)) 187 | scales['scale_z'].append(round(scale[2], 6)) 188 | 189 | rotate = transform.to_euler('XYZ') 190 | rotations['rotate_x'].append(round(degrees(rotate[0]), 6)) 191 | rotations['rotate_y'].append(round(degrees(rotate[1]), 6)) 192 | rotations['rotate_z'].append(round(degrees(rotate[2]), 6)) 193 | 194 | translate = transform.to_translation() 195 | trans['translate_x'].append(round(translate[0] * mag, 6)) 196 | trans['translate_y'].append(round(translate[1] * mag, 6)) 197 | trans['translate_z'].append(round(translate[2] * mag, 6)) 198 | 199 | # Set scale frames. 200 | for key in scales: 201 | data, frame_step = self.process_curve( 202 | scales[key], 203 | settings['ica_scale_tolerance']) 204 | result = self.scale_data.add_data(data) 205 | animation.set_reference(key, result[0], result[1], frame_step) 206 | 207 | # Set rotation frames. 208 | for key in rotations: 209 | data, frame_step = self.process_curve( 210 | rotations[key], 211 | settings['ica_rotate_tolerance']) 212 | result = self.rotate_data.add_data(data) 213 | animation.set_reference(key, result[0], result[1], frame_step) 214 | 215 | # Set translation frames. 216 | for key in trans: 217 | data, frame_step = self.process_curve( 218 | trans[key], 219 | settings['ica_translate_tolerance']) 220 | result = self.translate_data.add_data(data) 221 | animation.set_reference(key, result[0], result[1], frame_step) 222 | 223 | def process_curve(self, data, tolerance): 224 | result = [] 225 | frame_step = 1 226 | 227 | if settings['ica_frame_step'] == "1": 228 | result = data 229 | elif settings['ica_frame_step'] == "2": 230 | frame_step = 2 231 | for i, v in enumerate(data): 232 | if i % frame_step == 0 or len(data) - i < frame_step: 233 | result.append(v) 234 | elif settings['ica_frame_step'] == "4": 235 | frame_step = 4 236 | for i, v in enumerate(data): 237 | if i % frame_step == 0 or len(data) - i < frame_step: 238 | result.append(v) 239 | 240 | # Calculate tolerance. 241 | min_value = result[0] 242 | max_value = result[0] 243 | for v in result: 244 | min_value = min(min_value, v) 245 | max_value = max(max_value, v) 246 | value_range = max_value - min_value 247 | if value_range < tolerance: 248 | frame_step = 1 249 | result = [result[0]] 250 | 251 | return (result, frame_step) 252 | 253 | def find_animation(self, index) -> NitroBCAAnimation: 254 | for animation in self.animations: 255 | if animation.index == index: 256 | return animation 257 | self.animations.append(NitroBCAAnimation(index)) 258 | return self.animations[-1] 259 | 260 | 261 | def generate_anm_info(ica, info, model): 262 | node_anm_info = ET.SubElement(ica, 'node_anm_info') 263 | node_anm_info.set('frame_size', str(info.frame_size)) 264 | node_anm_info.set('scaling_rule', 'standard') 265 | node_anm_info.set('magnify', str(settings['imd_magnification'])) 266 | node_anm_info.set('tool_start_frame', '0') 267 | node_anm_info.set('tool_end_frame', str(info.frame_size)) 268 | node_anm_info.set('interpolation', 'frame') 269 | node_anm_info.set('interp_end_to_start', 'off') 270 | node_anm_info.set('compress_node', settings['imd_compress_nodes']) 271 | node_anm_info.set('node_size', 272 | str(len(model.nodes)) + ' ' + str(len(model.nodes))) 273 | node_anm_info.set('frame_step_mode', '1') 274 | scale_tolerance = '{:.6f}'.format(settings['ica_scale_tolerance']) 275 | node_anm_info.set('tolerance_scale', scale_tolerance) 276 | rotate_tolerance = '{:.6f}'.format(settings['ica_rotate_tolerance']) 277 | node_anm_info.set('tolerance_rotate', rotate_tolerance) 278 | translate_tolerance = '{:.6f}'.format(settings['ica_translate_tolerance']) 279 | node_anm_info.set('tolerance_translate', translate_tolerance) 280 | 281 | 282 | def generate_data(ica, bca_data: NitroBCAData): 283 | node_data = ET.SubElement(ica, bca_data.name) 284 | node_data.set('size', str(len(bca_data.data))) 285 | data_string = ' '.join(['{:.6f}'.format(x) for x in bca_data.data]) 286 | node_data.text = data_string 287 | 288 | 289 | def generate_animations(ica, animations): 290 | node_anm_array = ET.SubElement(ica, 'node_anm_array') 291 | node_anm_array.set('size', str(len(animations))) 292 | for animation in animations: 293 | node_anm = ET.SubElement(node_anm_array, 'node_anm') 294 | node_anm.set('index', str(animation.index)) 295 | 296 | for key, reference in animation.references.items(): 297 | generate_reference(node_anm, key, reference) 298 | 299 | 300 | def generate_reference(ica, name, reference): 301 | ref = ET.SubElement(ica, name) 302 | ref.set('frame_step', str(reference.frame_step)) 303 | ref.set('data_size', str(reference.data_size)) 304 | ref.set('data_head', str(reference.data_head)) 305 | 306 | 307 | def generate_body(ica, model, export_settings): 308 | global settings 309 | settings = export_settings 310 | 311 | bca = NitroBCA(model) 312 | bca.collect() 313 | 314 | generate_anm_info(ica, bca.info, model) 315 | generate_data(ica, bca.scale_data) 316 | generate_data(ica, bca.rotate_data) 317 | generate_data(ica, bca.translate_data) 318 | generate_animations(ica, bca.animations) 319 | -------------------------------------------------------------------------------- /export_imd.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | import math 3 | from mathutils import Vector 4 | import xml.etree.ElementTree as ET 5 | import json 6 | from .nns_model import NitroModel 7 | from .util import VecFx32 8 | 9 | 10 | settings = None 11 | 12 | 13 | def generate_model_info(imd, model): 14 | model_info = ET.SubElement(imd, 'model_info') 15 | model_info.set('pos_scale', str(model.info.pos_scale)) 16 | model_info.set('scaling_rule', 'standard') 17 | model_info.set('vertex_style', 'direct') 18 | magnification = str(round(settings['imd_magnification'], 6)) 19 | model_info.set('magnify', magnification) 20 | model_info.set('tool_start_frame', '1') 21 | model_info.set('tex_matrix_mode', 'maya') 22 | model_info.set('compress_node', settings['imd_compress_nodes']) 23 | nodes_len = str(len(model.nodes)) 24 | model_info.set('node_size', nodes_len + ' ' + nodes_len) 25 | model_info.set('compress_material', 'off') 26 | material_len = str(len(model.materials)) 27 | model_info.set('material_size', material_len + ' ' + material_len) 28 | model_info.set('output_texture', 'used') 29 | model_info.set('force_full_weight', 'on') 30 | strip = 'on' if settings['imd_use_primitive_strip'] else 'off' 31 | model_info.set('use_primitive_strip', strip) 32 | 33 | 34 | def generate_box_test(imd, model): 35 | # Set Pos Scale 36 | pos_scale = model.box_test.pos_scale 37 | box_test = ET.SubElement(imd, 'box_test') 38 | box_test.set('pos_scale', str(pos_scale)) 39 | # Set Position 40 | xyz = model.box_test.xyz 41 | scaled_xyz = (VecFx32().from_vector(xyz) >> pos_scale).to_vector() 42 | floats = [str(v) for v in scaled_xyz] 43 | box_test.set('xyz', ' '.join(floats)) 44 | # Set Dimensions 45 | whd = model.box_test.whd 46 | scaled_whd = (VecFx32().from_vector(whd) >> pos_scale).to_vector() 47 | floats = [str(v) for v in scaled_whd] 48 | box_test.set('whd', ' '.join(floats)) 49 | 50 | 51 | def generate_textures(imd, model): 52 | if len(model.textures) == 0: 53 | return 54 | tex_image_array = ET.SubElement(imd, 'tex_image_array') 55 | tex_image_array.set('size', str(len(model.textures))) 56 | for tex in model.textures: 57 | tex_image = ET.SubElement(tex_image_array, 'tex_image') 58 | tex_image.set('index', str(tex.index)) 59 | tex_image.set('name', tex.name) 60 | tex_image.set('width', str(tex.width)) 61 | tex_image.set('height', str(tex.height)) 62 | tex_image.set('original_width', str(tex.original_width)) 63 | tex_image.set('original_height', str(tex.original_height)) 64 | tex_image.set('format', tex.format) 65 | if hasattr(tex, 'color0_mode'): 66 | tex_image.set('color0_mode', tex.color0_mode) 67 | if hasattr(tex, 'palette_name'): 68 | tex_image.set('palette_name', tex.palette_name) 69 | tex_image.set('path', tex.path) 70 | bitmap = ET.SubElement(tex_image, 'bitmap') 71 | bitmap.set('size', str(tex.bitmap_size)) 72 | bitmap.text = tex.bitmap_data 73 | if tex.format == 'tex4x4': 74 | tex4x4_palette_idx = ET.SubElement(tex_image, 'tex4x4_palette_idx') 75 | tex4x4_palette_idx.set('size', str(tex.tex4x4_palette_idx_size)) 76 | tex4x4_palette_idx.text = tex.tex4x4_palette_idx_data 77 | 78 | 79 | def generate_palettes(imd, model): 80 | if len(model.palettes) == 0: 81 | return 82 | tex_palette_array = ET.SubElement(imd, 'tex_palette_array') 83 | tex_palette_array.set('size', str(len(model.palettes))) 84 | for pal in model.palettes: 85 | tex_palette = ET.SubElement(tex_palette_array, 'tex_palette') 86 | tex_palette.set('index', str(pal.index)) 87 | tex_palette.set('name', pal.name) 88 | tex_palette.set('color_size', str(pal.size)) 89 | tex_palette.text = pal.data 90 | 91 | 92 | def generate_materials(imd, model): 93 | material_array = ET.SubElement(imd, 'material_array') 94 | material_array.set('size', str(len(model.materials))) 95 | for mat in model.materials: 96 | material = ET.SubElement(material_array, 'material') 97 | material.set('index', str(mat.index)) 98 | material.set('name', mat.name) 99 | material.set('light0', mat.light0) 100 | material.set('light1', mat.light1) 101 | material.set('light2', mat.light2) 102 | material.set('light3', mat.light3) 103 | material.set('face', mat.face) 104 | material.set('alpha', str(mat.alpha)) 105 | material.set('wire_mode', mat.wire_mode) 106 | material.set('polygon_mode', mat.polygon_mode) 107 | material.set('polygon_id', str(mat.polygon_id)) 108 | material.set('fog_flag', mat.fog_flag) 109 | material.set('depth_test_decal', mat.depth_test_decal) 110 | material.set('translucent_update_depth', mat.translucent_update_depth) 111 | material.set('render_1_pixel', mat.render_1_pixel) 112 | material.set('far_clipping', mat.far_clipping) 113 | material.set('diffuse', mat.diffuse) 114 | material.set('ambient', mat.ambient) 115 | material.set('specular', mat.specular) 116 | material.set('emission', mat.emission) 117 | material.set('shininess_table_flag', mat.shininess_table_flag) 118 | material.set('tex_image_idx', str(mat.image_idx)) 119 | material.set('tex_palette_idx', str(mat.palette_idx)) 120 | # Only output when there is a texture assigned 121 | if mat.image_idx != -1: 122 | material.set('tex_tiling', 123 | f'{mat.tex_tiling_u} {mat.tex_tiling_v}') 124 | material.set('tex_scale', mat.tex_scale) 125 | material.set('tex_rotate', mat.tex_rotate) 126 | material.set('tex_translate', mat.tex_translate) 127 | material.set('tex_gen_mode', mat.tex_gen_mode) 128 | if mat.tex_gen_mode == 'nrm' or mat.tex_gen_mode == 'pos': 129 | material.set('tex_gen_st_src', mat.tex_gen_st_src) 130 | material.set('tex_effect_mtx', mat.tex_effect_mtx) 131 | 132 | 133 | def generate_matrices(imd, model): 134 | matrix_array = ET.SubElement(imd, 'matrix_array') 135 | matrix_array.set('size', str(len(model.matrices))) 136 | for matrix in model.matrices: 137 | matrix_t = ET.SubElement(matrix_array, 'matrix') 138 | matrix_t.set('index', str(matrix.index)) 139 | matrix_t.set('mtx_weight', str(matrix.weight)) 140 | matrix_t.set('node_idx', str(matrix.node_idx)) 141 | 142 | 143 | def generate_polygons(imd, model): 144 | polygons = ET.SubElement(imd, 'polygon_array') 145 | polygons.set('size', str(len(model.polygons))) 146 | for polygon in model.polygons: 147 | polygon_t = ET.SubElement(polygons, 'polygon') 148 | polygon_t.set('index', str(polygon.index)) 149 | polygon_t.set('name', polygon.name) 150 | polygon_t.set('mtx_prim_size', str(len(polygon.mtx_prims))) 151 | polygon_t.set('nrm_flag', 'on' if polygon.use_nrm else 'off') 152 | polygon_t.set('clr_flag', 'on' if polygon.use_clr else 'off') 153 | polygon_t.set('tex_flag', 'on' if polygon.use_tex else 'off') 154 | polygon_t.set('vertex_size', str(polygon.vertex_size)) 155 | polygon_t.set('polygon_size', str(polygon.polygon_size)) 156 | polygon_t.set('triangle_size', str(polygon.triangle_size)) 157 | polygon_t.set('quad_size', str(polygon.quad_size)) 158 | 159 | for mtx_prim in polygon.mtx_prims: 160 | mtx_prim_t = ET.SubElement(polygon_t, 'mtx_prim') 161 | mtx_prim_t.set('index', str(mtx_prim.index)) 162 | 163 | mtx_list_t = ET.SubElement(mtx_prim_t, 'mtx_list') 164 | mtx_list_t.set('size', str(len(mtx_prim.mtx_list))) 165 | mtx_list_t.text = ' '.join([str(x) for x in mtx_prim.mtx_list]) 166 | 167 | primitive_array = ET.SubElement(mtx_prim_t, 'primitive_array') 168 | primitive_array.set('size', str(len(mtx_prim.primitives))) 169 | 170 | for index, primitive in enumerate(mtx_prim.primitives): 171 | primitive_t = ET.SubElement(primitive_array, 'primitive') 172 | primitive_t.set('index', str(index)) 173 | primitive_t.set('type', primitive.type) 174 | primitive_t.set('vertex_size', str(primitive.vertex_size)) 175 | for cmd in primitive.commands: 176 | command = ET.SubElement(primitive_t, cmd.type) 177 | command.set(cmd.tag, cmd.data) 178 | 179 | 180 | def generate_nodes(imd, model: NitroModel): 181 | node_array = ET.SubElement(imd, 'node_array') 182 | node_array.set('size', str(len(model.nodes))) 183 | 184 | for node in model.nodes: 185 | node_t = ET.SubElement(node_array, 'node') 186 | node_t.set('index', str(node.index)) 187 | node_t.set('name', node.name) 188 | node_t.set('kind', str(node.kind)) 189 | node_t.set('parent', str(node.parent)) 190 | node_t.set('child', str(node.child)) 191 | node_t.set('brother_next', str(node.brother_next)) 192 | node_t.set('brother_prev', str(node.brother_prev)) 193 | node_t.set('draw_mtx', 'on' if node.draw_mtx else 'off') 194 | node_t.set('billboard', node.billboard) 195 | scale = ' '.join([str(round(x, 6)) for x in node.scale]) 196 | node_t.set('scale', scale) 197 | rotate = ' '.join([str(round(x, 6)) for x in node.rotate]) 198 | node_t.set('rotate', rotate) 199 | translate = ' '.join([str(round(x, 6)) for x in node.translate]) 200 | node_t.set('translate', translate) 201 | node_t.set('visibility', 'on' if node.visibility else 'off') 202 | 203 | node_t.set('display_size', str(len(node.displays))) 204 | if node.vertex_size: 205 | node_t.set('vertex_size', str(node.vertex_size)) 206 | if node.polygon_size: 207 | node_t.set('polygon_size', str(node.polygon_size)) 208 | if node.triangle_size: 209 | node_t.set('triangle_size', str(node.triangle_size)) 210 | if node.quad_size: 211 | node_t.set('quad_size', str(node.quad_size)) 212 | 213 | for display in node.displays: 214 | display_t = ET.SubElement(node_t, 'display') 215 | display_t.set('index', str(display.index)) 216 | display_t.set('material', str(display.material)) 217 | display_t.set('polygon', str(display.polygon)) 218 | display_t.set('priority', '0') 219 | 220 | 221 | def generate_output_info(imd, model): 222 | output = model.output_info 223 | output_info = ET.SubElement(imd, 'output_info') 224 | output_info.set('vertex_size', str(output.vertex_size)) 225 | output_info.set('polygon_size', str(output.polygon_size)) 226 | output_info.set('triangle_size', str(output.triangle_size)) 227 | output_info.set('quad_size', str(output.quad_size)) 228 | 229 | 230 | def generate_body(imd, model, export_settings): 231 | global settings 232 | settings = export_settings 233 | 234 | generate_model_info(imd, model) 235 | generate_box_test(imd, model) 236 | generate_textures(imd, model) 237 | generate_palettes(imd, model) 238 | generate_materials(imd, model) 239 | generate_matrices(imd, model) 240 | generate_polygons(imd, model) 241 | generate_nodes(imd, model) 242 | generate_output_info(imd, model) 243 | -------------------------------------------------------------------------------- /export_ita.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | from mathutils import Vector 3 | import xml.etree.ElementTree as ET 4 | import math 5 | 6 | 7 | settings = None 8 | 9 | 10 | class NitroSRTInfo(): 11 | def __init__(self): 12 | self.frame_size = 0 13 | 14 | def set_frame_size(self, size): 15 | if size > self.frame_size: 16 | self.frame_size = size 17 | 18 | 19 | class NitroSRTData(): 20 | def __init__(self, name): 21 | self.name = name 22 | if name == 'tex_scale_data': 23 | self.data = [1.0] 24 | else: 25 | self.data = [0.0] 26 | 27 | def find_in_data(self, x): 28 | l1, l2 = len(self.data), len(x) 29 | for i in range(l1): 30 | if self.data[i:i+l2] == x: 31 | return i 32 | return -1 33 | 34 | def add_data(self, data): 35 | """ 36 | Adds and compresses data. Returns a tuple with head and size. 37 | """ 38 | head = len(self.data) 39 | if all(elem == data[0] for elem in data): 40 | # This animation consists of one element. 41 | # Try to find if the value already exist 42 | # otherwise add it. 43 | try: 44 | head = self.data.index(data[0]) 45 | except ValueError: 46 | self.data.append(data[0]) 47 | return (head, 1) 48 | else: 49 | # Try to find the pattern in the existing 50 | # data first. 51 | length = len(data) 52 | index = self.find_in_data(data) 53 | if index != -1: 54 | # Found the pattern, index is now the head. 55 | head = index 56 | else: 57 | # Didn't find anything, try checking if the 58 | # last inserted value is equal to the first 59 | # value in the data. 60 | if self.data[-1] == data[0]: 61 | data.pop(0) 62 | head = head - 1 63 | self.data.extend(data) 64 | return (head, length) 65 | 66 | 67 | class NitroSRTReference(): 68 | def __init__(self): 69 | self.data_head = 0 70 | self.data_size = 1 71 | self.frame_step = 1 72 | 73 | 74 | class NitroSRTAnimation(): 75 | def __init__(self, index, material_name): 76 | self.index = index 77 | self.material_name = material_name 78 | self.references = { 79 | 'tex_scale_s': NitroSRTReference(), 80 | 'tex_scale_t': NitroSRTReference(), 81 | 'tex_rotate': NitroSRTReference(), 82 | 'tex_translate_s': NitroSRTReference(), 83 | 'tex_translate_t': NitroSRTReference() 84 | } 85 | 86 | def set_reference(self, name, head, size, step): 87 | reference = self.references[name] 88 | reference.data_head = head 89 | reference.data_size = size 90 | reference.frame_step = step 91 | 92 | 93 | class NitroSRT(): 94 | def __init__(self): 95 | self.info = NitroSRTInfo() 96 | self.scale_data = NitroSRTData('tex_scale_data') 97 | self.rotate_data = NitroSRTData('tex_rotate_data') 98 | self.translate_data = NitroSRTData('tex_translate_data') 99 | self.animations = [] 100 | 101 | def collect(self): 102 | for mat in bpy.data.materials: 103 | anim_data = mat.nns_srt_translate.data.animation_data 104 | if anim_data is not None and anim_data.action is not None: 105 | action = anim_data.action 106 | self.process_action(mat.name, action) 107 | 108 | def process_action(self, material_name, action): 109 | for curve in action.fcurves: 110 | data = [] 111 | for i in range(int(action.frame_range[1]+1)): 112 | data.append(round(curve.evaluate(i), 6)) 113 | self.info.set_frame_size(len(data)) 114 | animation = self.find_animation(material_name) 115 | if curve.data_path == 'nns_srt_scale': 116 | result = self.scale_data.add_data(data) 117 | name = 'tex_scale_t' if curve.array_index else 'tex_scale_s' 118 | animation.set_reference(name, result[0], result[1], 1) 119 | elif curve.data_path == 'nns_srt_rotate': 120 | data = [math.degrees(x) for x in data] 121 | result = self.rotate_data.add_data(data) 122 | animation.set_reference('tex_rotate', result[0], result[1], 1) 123 | elif curve.data_path == 'nns_srt_translate': 124 | result = self.translate_data.add_data(data) 125 | name = 'tex_translate_t' if curve.array_index else 'tex_translate_s' 126 | animation.set_reference(name, result[0], result[1], 1) 127 | 128 | def find_animation(self, material_name): 129 | for animation in self.animations: 130 | if animation.material_name == material_name: 131 | return animation 132 | index = len(self.animations) 133 | self.animations.append(NitroSRTAnimation(index, material_name)) 134 | return self.animations[-1] 135 | 136 | 137 | def generate_srt_info(ita, info): 138 | tex_srt_info = ET.SubElement(ita, 'tex_srt_info') 139 | tex_srt_info.set('frame_size', str(info.frame_size)) 140 | tex_srt_info.set('tool_start_frame', '0') 141 | tex_srt_info.set('tool_end_frame', str(info.frame_size)) 142 | tex_srt_info.set('interpolation', 'frame') 143 | tex_srt_info.set('tex_matrix_mode', 'maya') 144 | tex_srt_info.set('compress_material', 'off') 145 | tex_srt_info.set('material_size', '1 1') 146 | tex_srt_info.set('frame_step_mode', '1') 147 | scale_tolerance = '{:.6f}'.format(settings['ita_scale_tolerance']) 148 | tex_srt_info.set('tolerance_tex_scale', scale_tolerance) 149 | rotate_tolerance = '{:.6f}'.format(settings['ita_rotate_tolerance']) 150 | tex_srt_info.set('tolerance_tex_rotate', rotate_tolerance) 151 | translate_tolerance = '{:.6f}'.format(settings['ita_translate_tolerance']) 152 | tex_srt_info.set('tolerance_tex_translate', translate_tolerance) 153 | 154 | 155 | def generate_data(ita, str_data: NitroSRTData): 156 | tex_data = ET.SubElement(ita, str_data.name) 157 | tex_data.set('size', str(len(str_data.data))) 158 | data_string = ' '.join(['{:.6f}'.format(x) for x in str_data.data]) 159 | tex_data.text = data_string 160 | 161 | 162 | def generate_animations(ita, animations): 163 | tex_srt_anm_array = ET.SubElement(ita, 'tex_srt_anm_array') 164 | tex_srt_anm_array.set('size', str(len(animations))) 165 | for animation in animations: 166 | tex_srt_anm = ET.SubElement(tex_srt_anm_array, 'tex_srt_anm') 167 | tex_srt_anm.set('index', str(animation.index)) 168 | tex_srt_anm.set('material_name', str(animation.material_name)) 169 | 170 | for key, reference in animation.references.items(): 171 | generate_reference(tex_srt_anm, key, reference) 172 | 173 | 174 | def generate_reference(ita, name, reference): 175 | ref = ET.SubElement(ita, name) 176 | ref.set('frame_step', str(reference.frame_step)) 177 | ref.set('data_size', str(reference.data_size)) 178 | ref.set('data_head', str(reference.data_head)) 179 | 180 | 181 | def generate_body(ita, export_settings): 182 | global settings 183 | settings = export_settings 184 | 185 | srt = NitroSRT() 186 | srt.collect() 187 | 188 | generate_srt_info(ita, srt.info) 189 | generate_data(ita, srt.scale_data) 190 | generate_data(ita, srt.rotate_data) 191 | generate_data(ita, srt.translate_data) 192 | generate_animations(ita, srt.animations) 193 | -------------------------------------------------------------------------------- /export_itp.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | import xml.etree.ElementTree as ET 3 | from .nns_model import NitroModel 4 | import os 5 | 6 | settings = None 7 | 8 | 9 | class NitroTXPInfo: 10 | def __init__(self): 11 | self.frame_size = 0 12 | 13 | def set_frame_size(self, size): 14 | if size > self.frame_size: 15 | self.frame_size = size 16 | 17 | 18 | class NitroTXPImagePalette: 19 | def __init__(self): 20 | self.images = [] 21 | self.palettes = [] 22 | 23 | def find_image(self, img): 24 | for i in range(len(self.images)): 25 | if self.images[i] == img: 26 | return i 27 | self.images.append(img) 28 | return len(self.images)-1 29 | 30 | def find_palette(self, plt): 31 | for i in range(len(self.palettes)): 32 | if self.palettes[i] == plt: 33 | return i 34 | self.palettes.append(plt) 35 | return len(self.palettes)-1 36 | 37 | 38 | class NitroTXPData: 39 | def __init__(self): 40 | self.frame_ids = [] 41 | self.image_ids = [] 42 | self.palette_ids = [] 43 | 44 | def find_plt_img_frm(self, palettes, images, frames): 45 | """ 46 | Entry other than the object :list of frames, list of images and 47 | list of palettes, all of the same length. 48 | role : finds an id in self.frame_ids, self.palette_ids and 49 | self.image_ids where the corresponding input list is equal 50 | to a portion of the former list starting at the id, if its 51 | not found it adds the input lists to their corresponding 52 | list and returns the next id after the last id before the addition. 53 | """ 54 | 55 | for i in range(len(self.frame_ids)): # same len for all elem tbh 56 | if (self.frame_ids[i:i+len(frames)] == frames 57 | and self.palette_ids[i:i+len(palettes)] == palettes 58 | and self.image_ids[i:i+len(images)] == images): 59 | return i 60 | retval = len(self.palette_ids) 61 | self.frame_ids += frames 62 | self.image_ids += images 63 | self.palette_ids += palettes 64 | return retval 65 | 66 | 67 | class NitroTXP: 68 | def __init__(self): 69 | self.info = NitroTXPInfo() 70 | self.imgPlt = NitroTXPImagePalette() 71 | self.data = NitroTXPData() 72 | self.pattern_anm = {} 73 | 74 | def collect(self, model: NitroModel): 75 | materials = model.materials 76 | 77 | for material in materials: 78 | bldMaterial = bpy.data.materials[material.blender_index] 79 | if bldMaterial.nns_texframe_reference: 80 | 81 | action = bldMaterial.animation_data.action 82 | self.info.set_frame_size(int(action.frame_range[1])) 83 | 84 | self.set_images(bldMaterial, model) 85 | 86 | self.set_data(action, bldMaterial, model) 87 | 88 | def set_data(self, action, material, model): 89 | material_imgPattern = [] 90 | material_pltPattern = [] 91 | material_frmPattern = [] 92 | 93 | for curve in action.fcurves: 94 | if curve.data_path.count("nns_texframe_reference_index"): 95 | prev = float("inf") 96 | for frame in range(int(action.frame_range[1]+1)): 97 | 98 | evaluation = curve.evaluate(frame) 99 | if evaluation != prev: 100 | 101 | prev = evaluation 102 | idTex = int(evaluation) 103 | tex = material.nns_texframe_reference[idTex] 104 | path = os.path.realpath( 105 | bpy.path.abspath(tex.image.filepath)) 106 | texName = model.find_texture(path) 107 | 108 | idTex = self.imgPlt.find_image(texName.name) 109 | material_imgPattern.append(idTex) 110 | 111 | idPlt = self.imgPlt.find_palette(texName.palette_name) 112 | material_pltPattern.append(idPlt) 113 | 114 | material_frmPattern.append(int(frame)) 115 | 116 | head = self.data.find_plt_img_frm( 117 | material_pltPattern, material_imgPattern, material_frmPattern) 118 | self.pattern_anm[material.name] = [len(material_frmPattern), head] 119 | 120 | def set_images(self, material, model): 121 | 122 | for ref in material.nns_texframe_reference: 123 | path = os.path.realpath(bpy.path.abspath(ref.image.filepath)) 124 | texName = model.find_texture(path) 125 | self.imgPlt.find_image(texName.name) 126 | self.imgPlt.find_palette(texName.palette_name) 127 | 128 | 129 | def generate_txp_info(itp, info): 130 | tex_pattern_info = ET.SubElement(itp, 'tex_pattern_info') 131 | tex_pattern_info.set('frame_size', str(info.frame_size)) 132 | tex_pattern_info.set('tool_start_frame', '0') 133 | tex_pattern_info.set('tool_end_frame', str(info.frame_size-1)) 134 | tex_pattern_info.set('compress_material', 'off') 135 | tex_pattern_info.set('material_size', '1 1') 136 | 137 | 138 | def generate_txp_pattern_list_data(itp, img_plt): 139 | 140 | tex_pattern_LD = ET.SubElement(itp, "tex_pattern_list_data") 141 | tex_pattern_LD.set('image_size', str(len(img_plt.images))) 142 | tex_pattern_LD.set('palette_size', str(len(img_plt.palettes))) 143 | 144 | for imgID in range(len(img_plt.images)): 145 | pattern_image = ET.SubElement(tex_pattern_LD, "image_name") 146 | pattern_image.set("index", str(imgID)) 147 | pattern_image.set("name", str(img_plt.images[imgID])) 148 | 149 | for pltID in range(len(img_plt.palettes)): 150 | pattern_image = ET.SubElement(tex_pattern_LD, "palette_name") 151 | pattern_image.set("index", str(pltID)) 152 | pattern_image.set("name", str(img_plt.palettes[pltID])) 153 | 154 | 155 | def generate_txp_pattern_data(itp, data): 156 | tex_pattern_data = ET.SubElement(itp, "tex_pattern_data") 157 | 158 | frame_idx = ET.SubElement(tex_pattern_data, "frame_idx") 159 | frame_idx.set("size", str(len(data.frame_ids))) 160 | frame_idx.text = ' '.join([str(x) for x in data.frame_ids]) 161 | 162 | image_idx = ET.SubElement(tex_pattern_data, "image_idx") 163 | image_idx.set("size", str(len(data.image_ids))) 164 | image_idx.text = ' '.join([str(x) for x in data.image_ids]) 165 | 166 | palette_idx = ET.SubElement(tex_pattern_data, "palette_idx") 167 | palette_idx.set("size", str(len(data.palette_ids))) 168 | palette_idx.text = ' '.join([str(x) for x in data.palette_ids]) 169 | 170 | 171 | def generate_txp_anm_array(itp, anm): 172 | tex_pattern_anm_array = ET.SubElement(itp, "tex_pattern_anm_array") 173 | tex_pattern_anm_array.set("size", str(len(anm))) 174 | 175 | for keyID in range(len(anm.keys())): 176 | name = list(anm.keys())[keyID] 177 | tex_pattern_anm = ET.SubElement( 178 | tex_pattern_anm_array, "tex_pattern_anm") 179 | tex_pattern_anm.set("index", str(keyID)) 180 | tex_pattern_anm.set("material_name", name) 181 | tex_pattern_anm.set("data_size", str(anm[name][0])) 182 | tex_pattern_anm.set("data_head", str(anm[name][1])) 183 | 184 | 185 | def generate_body(itp, model: NitroModel, export_settings): 186 | global settings 187 | settings = export_settings 188 | 189 | txp = NitroTXP() 190 | txp.collect(model) 191 | 192 | generate_txp_info(itp, txp.info) 193 | generate_txp_pattern_list_data(itp, txp.imgPlt) 194 | generate_txp_pattern_data(itp, txp.data) 195 | generate_txp_anm_array(itp, txp.pattern_anm) 196 | -------------------------------------------------------------------------------- /export_nitro.py: -------------------------------------------------------------------------------- 1 | import xml.etree.ElementTree as ET 2 | from xml.dom import minidom 3 | from . import local_logger as logger 4 | from .nns_model import NitroModel 5 | import os 6 | from .version import get_version_str 7 | 8 | 9 | def generate_header(imd, data_name): 10 | imd.set('version', '1.6.0') 11 | head = ET.SubElement(imd, 'head') 12 | 13 | title = ET.SubElement(head, 'title') 14 | title.text = data_name + ' for NINTENDO NITRO-System' 15 | 16 | generator = ET.SubElement(head, 'generator') 17 | generator.set('name', 'Nitro plugin for Blender 2.8') 18 | generator.set('version', get_version_str()) 19 | 20 | 21 | def generate_imd(settings, model): 22 | from . import export_imd 23 | 24 | imd = ET.Element('imd') 25 | generate_header(imd, 'Model Data') 26 | body = ET.SubElement(imd, 'body') 27 | export_imd.generate_body(body, model, settings) 28 | 29 | output = "" 30 | if settings['pretty_print']: 31 | output = minidom.parseString(ET.tostring(imd, encoding='unicode')) 32 | output = output.toprettyxml(indent=' ') 33 | else: 34 | output = ET.tostring(imd, encoding='unicode') 35 | 36 | with open(settings['filepath'] + '.imd', 'w') as f: 37 | f.write(output) 38 | 39 | 40 | def generate_ita(settings): 41 | from . import export_ita 42 | 43 | ita = ET.Element('ita') 44 | generate_header(ita, 'Texture SRT Animation Data') 45 | body = ET.SubElement(ita, 'body') 46 | export_ita.generate_body(body, settings) 47 | 48 | output = "" 49 | if settings['pretty_print']: 50 | output = minidom.parseString(ET.tostring(ita, encoding='unicode')) 51 | output = output.toprettyxml(indent=' ') 52 | else: 53 | output = ET.tostring(ita, encoding='unicode') 54 | 55 | with open(settings['filepath'] + '.ita', 'w') as f: 56 | f.write(output) 57 | 58 | 59 | def generate_ica(settings, model): 60 | from . import export_ica 61 | 62 | ica = ET.Element('ica') 63 | generate_header(ica, 'Character Animation Data') 64 | body = ET.SubElement(ica, 'body') 65 | export_ica.generate_body(body, model, settings) 66 | 67 | output = "" 68 | if settings['pretty_print']: 69 | output = minidom.parseString(ET.tostring(ica, encoding='unicode')) 70 | output = output.toprettyxml(indent=' ') 71 | else: 72 | output = ET.tostring(ica, encoding='unicode') 73 | 74 | with open(settings['filepath'] + '.ica', 'w') as f: 75 | f.write(output) 76 | 77 | 78 | def generate_itp(settings, model): 79 | from . import export_itp 80 | 81 | itp = ET.Element('itp') 82 | generate_header(itp, 'Texture Pattern Animation Data') 83 | body = ET.SubElement(itp, 'body') 84 | export_itp.generate_body(body, model, settings) 85 | 86 | output = "" 87 | if settings['pretty_print']: 88 | output = minidom.parseString(ET.tostring(itp, encoding='unicode')) 89 | output = output.toprettyxml(indent=' ') 90 | else: 91 | output = ET.tostring(itp, encoding='unicode') 92 | 93 | with open(settings['filepath'] + '.itp', 'w') as f: 94 | f.write(output) 95 | 96 | 97 | def save(context, settings): 98 | 99 | settings['filepath'] = os.path.splitext(settings['filepath'])[0] 100 | 101 | logger.create_log(settings['filepath'], settings['generate_log']) 102 | 103 | model = None 104 | 105 | if (settings['imd_export'] 106 | or settings['ica_export'] 107 | or settings['itp_export']): 108 | model = NitroModel(settings) 109 | model.collect() 110 | 111 | if settings['ita_export']: 112 | generate_ita(settings) 113 | if settings['ica_export']: 114 | generate_ica(settings, model) 115 | if settings['itp_export']: 116 | generate_itp(settings, model) 117 | # Generate the imd as last because the other files may have changed things. 118 | if settings['imd_export']: 119 | generate_imd(settings, model) 120 | -------------------------------------------------------------------------------- /local_logger.py: -------------------------------------------------------------------------------- 1 | _logger_filepath = '' 2 | 3 | 4 | _can_log = False 5 | 6 | 7 | def create_log(filepath, can_log): 8 | global _logger_filepath, _can_log 9 | _can_log = can_log 10 | if can_log: 11 | _logger_filepath = filepath + '.log' 12 | f = open(_logger_filepath, 'w+') 13 | f.close() 14 | 15 | 16 | def log(text): 17 | global _logger_filepath, _can_log 18 | print(text) 19 | if _can_log: 20 | f = open(_logger_filepath, 'a+') 21 | f.write(text + '\n') 22 | f.close() 23 | -------------------------------------------------------------------------------- /nns_material.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | from bpy.props import (BoolProperty, 3 | FloatProperty, 4 | EnumProperty, 5 | IntProperty, 6 | FloatVectorProperty, 7 | PointerProperty, 8 | CollectionProperty) 9 | from bpy.types import Image 10 | from bpy.app.handlers import persistent 11 | 12 | 13 | def generate_output_node(material, input): 14 | nodes = material.node_tree.nodes 15 | links = material.node_tree.links 16 | node_output = nodes.new(type='ShaderNodeOutputMaterial') 17 | if material.nns_hdr_add_self: 18 | add_shader1 = nodes.new(type='ShaderNodeAddShader') 19 | add_shader2 = nodes.new(type='ShaderNodeAddShader') 20 | links.new(input.outputs[0], add_shader1.inputs[0]) 21 | links.new(input.outputs[0], add_shader1.inputs[1]) 22 | if bpy.app.version < (3, 0, 0): 23 | links.new(add_shader1.outputs[0], add_shader2.inputs[0]) 24 | links.new(add_shader1.outputs[0], add_shader2.inputs[1]) 25 | links.new(add_shader2.outputs[0], node_output.inputs[0]) 26 | else: 27 | links.new(add_shader1.outputs[0], node_output.inputs[0]) 28 | else: 29 | links.new(input.outputs[0], node_output.inputs[0]) 30 | 31 | 32 | def generate_culling_nodes(material): 33 | nodes = material.node_tree.nodes 34 | links = material.node_tree.links 35 | 36 | node_geo = nodes.new(type='ShaderNodeNewGeometry') 37 | node_invert = nodes.new(type='ShaderNodeInvert') 38 | 39 | if material.nns_display_face == "front": 40 | node_invert.inputs[0].default_value = 1.0 41 | elif material.nns_display_face == "back": 42 | node_invert.inputs[0].default_value = 0.0 43 | 44 | node_mix_rgb = nodes.new(type='ShaderNodeMixRGB') 45 | node_mix_rgb.blend_type = 'MULTIPLY' 46 | node_mix_rgb.inputs[0].default_value = 1.0 47 | links.new(node_geo.outputs[6], node_invert.inputs[1]) 48 | links.new(node_invert.outputs[0], node_mix_rgb.inputs[1]) 49 | 50 | return node_mix_rgb 51 | 52 | 53 | def generate_srt_nodes(material, input): 54 | nodes = material.node_tree.nodes 55 | links = material.node_tree.links 56 | 57 | node_sub = nodes.new(type='ShaderNodeVectorMath') 58 | node_sub.operation = 'SUBTRACT' 59 | node_sub.location = (-100, 0) 60 | 61 | if material.nns_tex_gen_mode == "nrm": 62 | node_sub.inputs[1].default_value = (0, 0, 0) 63 | else: 64 | node_sub.inputs[1].default_value = (0.5, 0.5, 0.5) 65 | 66 | node_rt_mapping = nodes.new(type='ShaderNodeMapping') 67 | node_rt_mapping.name = 'nns_node_rt' 68 | node_rt_mapping.location = (0, 0) 69 | node_rt_mapping.inputs[3].default_value = (1, 1, 1) 70 | 71 | node_add = nodes.new(type='ShaderNodeVectorMath') 72 | node_add.operation = 'ADD' 73 | node_add.location = (100, 0) 74 | node_add.inputs[1].default_value = (0.5, 0.5, 0.5) 75 | 76 | node_s_mapping = nodes.new(type='ShaderNodeMapping') 77 | node_s_mapping.name = 'nns_node_s' 78 | node_s_mapping.location = (200, 0) 79 | node_s_mapping.inputs[3].default_value = (1, 1, 1) 80 | 81 | links.new(input.outputs[0], node_sub.inputs[0]) 82 | links.new(node_sub.outputs[0], node_rt_mapping.inputs[0]) 83 | links.new(node_rt_mapping.outputs[0], node_add.inputs[0]) 84 | links.new(node_add.outputs[0], node_s_mapping.inputs[0]) 85 | 86 | return node_s_mapping 87 | 88 | 89 | node_offset_x = 0 90 | node_offset_y = 0 91 | loca = (0, 0) 92 | 93 | 94 | def create_node(mat, name, node_type, location, offset_mode="x"): 95 | global node_offset_x 96 | global node_offset_y 97 | nodes = mat.node_tree.nodes 98 | newnode = nodes.new(type=node_type) 99 | newnode.name = name 100 | newnode.label = name 101 | if offset_mode == 0: 102 | newnode.location = location 103 | elif offset_mode == "x": 104 | newnode.location = (location[0] + node_offset_x, location[1] + node_offset_y) 105 | node_offset_x += 180 106 | elif offset_mode == "y": 107 | newnode.location = (location[0], location[1] + node_offset_y) 108 | node_offset_y -= 150 109 | elif offset_mode == "xy": 110 | newnode.location = (location[0] + node_offset_x, location[1] + node_offset_y) 111 | node_offset_x += 180 112 | newnode.location = (location[0], location[1] + node_offset_y) 113 | node_offset_y -= 150 114 | return newnode 115 | 116 | 117 | def create_light_nodes(mat, index, location): 118 | nodes = mat.node_tree.nodes 119 | links = mat.node_tree.links 120 | 121 | # get inputs 122 | 123 | # lights 124 | 125 | light_vec = nodes.get("Light" + str(index) + " Vector") 126 | light_spec = nodes.get("Light" + str(index) + " Specular") 127 | light_col = nodes.get("Light" + str(index) + " Color Filtered") 128 | 129 | # materials 130 | 131 | node_diffuse = nodes.get("df") 132 | node_ambient = nodes.get("amb") 133 | node_specular = nodes.get("spec") 134 | 135 | # transform inputs 136 | # normals to camera space 137 | 138 | normal_vec_node = create_node(mat, "normal vector(N)", 'ShaderNodeNewGeometry', location) 139 | 140 | vec_mult1 = create_node(mat, "Fix backface", "ShaderNodeVectorMath", location) 141 | vec_mult1.operation = "MULTIPLY" 142 | 143 | mult_add1 = create_node(mat, "remap backface factor", "ShaderNodeMath", location) 144 | mult_add1.operation = "MULTIPLY_ADD" 145 | mult_add1.inputs[1].default_value = -2.0 146 | mult_add1.inputs[2].default_value = 1.0 147 | 148 | vec_trans = create_node(mat, "transform to camera space", 'ShaderNodeVectorTransform', location) 149 | vec_trans.convert_from = "WORLD" 150 | vec_trans.convert_to = "CAMERA" 151 | 152 | links.new(normal_vec_node.outputs[6], mult_add1.inputs[0]) 153 | links.new(normal_vec_node.outputs[1], vec_mult1.inputs[0]) 154 | links.new(mult_add1.outputs[0], vec_mult1.inputs[1]) 155 | links.new(vec_mult1.outputs[0], vec_trans.inputs[0]) 156 | 157 | # LightVector in camera space 158 | vec_mult2 = create_node(mat, "inverse light angle", "ShaderNodeVectorMath", location) 159 | vec_mult2.inputs[1].default_value = (-1, -1, -1) 160 | vec_mult2.operation = "MULTIPLY" 161 | 162 | vec_norm1 = create_node(mat, "Normalize", "ShaderNodeVectorMath", location) 163 | vec_norm1.operation = "NORMALIZE" 164 | 165 | links.new(light_vec.outputs[0], vec_mult2.inputs[0]) 166 | links.new(vec_mult2.outputs[0], vec_norm1.inputs[0]) 167 | 168 | sep_xyz1 = create_node(mat, "SepXYZ", "ShaderNodeSeparateXYZ", location) 169 | comb_xyz1 = create_node(mat, "CombXYZ", "ShaderNodeCombineXYZ", location) 170 | 171 | links.new(vec_norm1.outputs[0], sep_xyz1.inputs[0]) 172 | links.new(sep_xyz1.outputs[0], comb_xyz1.inputs[0]) 173 | links.new(sep_xyz1.outputs[1], comb_xyz1.inputs[2]) 174 | links.new(sep_xyz1.outputs[2], comb_xyz1.inputs[1]) 175 | 176 | # ld : Diffuse reflection shininess 177 | # ls : Specular reflection shininess 178 | 179 | # calculation of ld 180 | 181 | dot_prod1 = create_node(mat, "Dot Prod1", "ShaderNodeVectorMath", location) 182 | dot_prod1.operation = "DOT_PRODUCT" 183 | 184 | links.new(vec_trans.outputs[0], dot_prod1.inputs[0]) 185 | links.new(comb_xyz1.outputs[0], dot_prod1.inputs[1]) 186 | 187 | clamp1 = create_node(mat, "Clamp1", "ShaderNodeClamp", location) 188 | clamp1.inputs[1].default_value = 0.0 189 | clamp1.inputs[2].default_value = 1.0 190 | 191 | ld = create_node(mat, "ld", "ShaderNodeMath", location) 192 | ld.operation = "POWER" 193 | ld.inputs[1].default_value = 1.50 194 | 195 | links.new(dot_prod1.outputs[1], clamp1.inputs[0]) 196 | links.new(clamp1.outputs[0], ld.inputs[0]) 197 | 198 | # half angle vector 199 | 200 | vec_add1 = create_node(mat, "VecAdd1", "ShaderNodeVectorMath", location) 201 | vec_add1.operation = "ADD" 202 | vec_add1.inputs[1].default_value = (0, 0.99, 0) 203 | 204 | vec_norm2 = create_node(mat, "VecNorm2", "ShaderNodeVectorMath", location) 205 | vec_norm2.operation = "NORMALIZE" 206 | 207 | links.new(comb_xyz1.outputs[0], vec_add1.inputs[0]) 208 | links.new(vec_add1.outputs[0], vec_norm2.inputs[0]) 209 | 210 | # calculation of ls (may not be 100% accurate due to me not knowing how to search for tables in the ida db 211 | # but it's accurate enough for preview purpose) 212 | # may be updated if i find a better approwimation or get the exact formula 213 | 214 | dot_prod2 = create_node(mat, "DotProd2", "ShaderNodeVectorMath", location) 215 | dot_prod2.operation = "DOT_PRODUCT" 216 | 217 | # specular corrective mask 218 | 219 | sign1 = create_node(mat, "Sign1", "ShaderNodeMath", location) 220 | sign1.operation = "SIGN" 221 | sign1.use_clamp = True 222 | 223 | # end of mask 224 | 225 | pow1 = create_node(mat, "Pow1", "ShaderNodeMath", location) 226 | pow1.operation = "POWER" 227 | pow1.inputs[1].default_value = 2.0 228 | 229 | links.new(vec_norm2.outputs[0], dot_prod2.inputs[0]) 230 | links.new(vec_trans.outputs[0], dot_prod2.inputs[1]) 231 | links.new(dot_prod2.outputs[1], pow1.inputs[0]) 232 | links.new(dot_prod2.outputs[1], sign1.inputs[0]) 233 | 234 | mult1 = create_node(mat, "Mult1", "ShaderNodeMath", location) 235 | mult1.operation = "MULTIPLY" 236 | mult1.inputs[1].default_value = 2.0 237 | 238 | sub1 = create_node(mat, "Sub1", "ShaderNodeMath", location) 239 | sub1.operation = "SUBTRACT" 240 | sub1.inputs[1].default_value = 1.0 241 | sub1.use_clamp = True 242 | 243 | links.new(pow1.outputs[0], mult1.inputs[0]) 244 | links.new(mult1.outputs[0], sub1.inputs[0]) 245 | 246 | # applying the spec corrective mask 247 | 248 | mult3 = create_node(mat, "Mult3", "ShaderNodeMath", location) 249 | mult3.operation = "MULTIPLY" 250 | links.new(sign1.outputs[0], mult3.inputs[0]) 251 | links.new(sub1.outputs[0], mult3.inputs[1]) 252 | 253 | ls = create_node(mat, "Specular brightness", "ShaderNodeMath", location) 254 | ls.operation = "POWER" 255 | ls.inputs[1].default_value = 2.0 256 | links.new(mult3.outputs[0], ls.inputs[0]) 257 | 258 | # Diffuse color 259 | 260 | di = create_node(mat, "Diffuse " + str(index), "ShaderNodeMixRGB", location) 261 | di.blend_type = "MULTIPLY" 262 | di.inputs[0].default_value = 1.0 263 | 264 | links.new(node_diffuse.outputs[0], di.inputs[1]) 265 | links.new(ld.outputs[0], di.inputs[2]) 266 | 267 | # Specular color 268 | 269 | pow2 = create_node(mat, "Pow2", "ShaderNodeMath", location) 270 | pow2.operation = "POWER" 271 | pow2.inputs[1].default_value = 1.5 272 | 273 | mult4 = create_node(mat, "Mult4", "ShaderNodeMath", location) 274 | mult4.operation = "MULTIPLY" 275 | 276 | si = create_node(mat, "Specular " + str(index), "ShaderNodeMixRGB", location) 277 | si.blend_type = "MULTIPLY" 278 | si.inputs[0].default_value = 1.0 279 | 280 | links.new(node_specular.outputs[0], si.inputs[1]) 281 | links.new(light_spec.outputs[0], pow2.inputs[0]) 282 | links.new(ls.outputs[0], mult4.inputs[0]) 283 | links.new(pow2.outputs[0], mult4.inputs[1]) 284 | links.new(mult4.outputs[0], si.inputs[2]) 285 | 286 | # addition of the three colors (all except emission) 287 | 288 | col_add1 = create_node(mat, "Ambient " + str(index), "ShaderNodeMixRGB", location) 289 | col_add1.blend_type = "ADD" 290 | col_add1.inputs[0].default_value = 1.0 291 | 292 | col_add2 = create_node(mat, "ColAdd2", "ShaderNodeMixRGB", location) 293 | col_add2.blend_type = "ADD" 294 | col_add2.inputs[0].default_value = 1.0 295 | 296 | links.new(node_ambient.outputs[0], col_add1.inputs[2]) 297 | links.new(di.outputs[0], col_add1.inputs[1]) 298 | links.new(col_add1.outputs[0], col_add2.inputs[1]) 299 | links.new(si.outputs[0], col_add2.inputs[2]) 300 | 301 | # multiply with light color 302 | 303 | col_mult1 = create_node(mat, "Result " + str(index), "ShaderNodeMixRGB", location) 304 | col_mult1.blend_type = "MULTIPLY" 305 | col_mult1.inputs[0].default_value = 1.0 306 | 307 | links.new(light_col.outputs[0], col_mult1.inputs[2]) 308 | links.new(col_add2.outputs[0], col_mult1.inputs[1]) 309 | light_node = col_mult1 310 | 311 | return light_node 312 | 313 | 314 | def generate_normal_lightning_color_nodes(material): 315 | global node_offset_y 316 | global node_offset_x 317 | global loca 318 | 319 | node_offset_x = 0 320 | node_offset_y = 0 321 | 322 | mat = material 323 | nodes = mat.node_tree.nodes 324 | links = mat.node_tree.links 325 | 326 | matcols = {"df": (material.nns_diffuse[0], 327 | material.nns_diffuse[1], 328 | material.nns_diffuse[2], 329 | 1.0), 330 | "amb": (material.nns_ambient[0], 331 | material.nns_ambient[1], 332 | material.nns_ambient[2], 333 | 1.0), 334 | "spec": (material.nns_specular[0], 335 | material.nns_specular[1], 336 | material.nns_specular[2], 337 | 1.0), 338 | "em": (material.nns_emission[0], 339 | material.nns_emission[1], 340 | material.nns_emission[2], 341 | 1.0)} 342 | 343 | light0 = {"LightVector": (0, 0, -1), "LightCol": (1, 1, 1, 1), "LightSpecular": 0.5, 344 | "isLightEnabled": mat.nns_light0, "LightIndex": 0} 345 | light1 = {"LightVector": (0, 0.5, -0.5), "LightCol": (1, 1, 1, 1), "LightSpecular": 1, 346 | "isLightEnabled": mat.nns_light1, "LightIndex": 1} 347 | light2 = {"LightVector": (0, 0, -1), "LightCol": (1, 0, 0, 1), "LightSpecular": 0.5, 348 | "isLightEnabled": mat.nns_light2, "LightIndex": 2} 349 | light3 = {"LightVector": (0, 0, 1), "LightCol": (1, 1, 0, 1), "LightSpecular": 0, "isLightEnabled": mat.nns_light3, 350 | "LightIndex": 3} 351 | 352 | lights = (light0, light1, light2, light3) 353 | 354 | # inputs 355 | node_offset_x = 0 356 | node_offset_y = -300 357 | loca = (-7500, -300) 358 | 359 | for i in range(4): 360 | light_vec = create_node(mat, "Light" + str(i) + " Vector", "ShaderNodeCombineXYZ", loca, offset_mode="y") 361 | for j in range(3): 362 | light_vec.inputs[j].default_value = lights[i]["LightVector"][j] 363 | 364 | light_col = create_node(mat, "Light" + str(i) + " Color", "ShaderNodeRGB", loca, offset_mode="y") 365 | light_col.outputs[0].default_value = lights[i]["LightCol"] 366 | 367 | light_spec = create_node(mat, "Light" + str(i) + " Specular", "ShaderNodeValue", loca, offset_mode="y") 368 | light_spec.outputs[0].default_value = lights[i]["LightSpecular"] 369 | 370 | light_enabled = create_node(mat, "Light" + str(i) + " Enabled", "ShaderNodeValue", loca, offset_mode="y") 371 | light_enabled.outputs[0].default_value = lights[i]["isLightEnabled"] 372 | 373 | mask_node = create_node(mat, "Light" + str(i) + " Color Filtered", "ShaderNodeMixRGB", (-7300, loca[1]), 374 | offset_mode="y") 375 | 376 | mask_node.blend_type = "MULTIPLY" 377 | mask_node.inputs[0].default_value = 1.0 378 | 379 | links.new(light_col.outputs[0], mask_node.inputs[1]) 380 | links.new(light_enabled.outputs[0], mask_node.inputs[2]) 381 | 382 | # material colors 383 | 384 | for name in matcols.keys(): 385 | col = create_node(mat, name, "ShaderNodeRGB", loca, offset_mode="y") 386 | col.outputs[0].default_value = matcols[name] 387 | 388 | # add all the results of the light0, 1, 2 and 3 calculations 389 | 390 | add_nodes_x = -600 391 | node_offset_y = -300 392 | 393 | l_col_add1 = create_node(mat, "LColAdd1", "ShaderNodeMixRGB", (add_nodes_x, -300), offset_mode="xy") 394 | l_col_add1.blend_type = "ADD" 395 | l_col_add1.inputs[0].default_value = 1.0 396 | 397 | l_col_add2 = create_node(mat, "LColAdd2", "ShaderNodeMixRGB", (add_nodes_x, -450), offset_mode="xy") 398 | l_col_add2.blend_type = "ADD" 399 | l_col_add2.inputs[0].default_value = 1.0 400 | 401 | l_col_add3 = create_node(mat, "LColAdd3", "ShaderNodeMixRGB", (add_nodes_x, -600), offset_mode="xy") 402 | l_col_add3.blend_type = "ADD" 403 | l_col_add3.inputs[0].default_value = 1.0 404 | 405 | l_col_add4 = create_node(mat, "Total result", "ShaderNodeMixRGB", (add_nodes_x, -750), offset_mode="xy") 406 | l_col_add4.blend_type = "ADD" 407 | l_col_add4.inputs[0].default_value = 1.0 408 | node_emission = nodes.get("em") 409 | 410 | use_diffuse_node = create_node(mat, "UseOnlyDiffuse?", "ShaderNodeMixRGB", (add_nodes_x, -750), offset_mode="xy") 411 | use_diffuse_node.blend_type = "MIX" 412 | 413 | links.new(l_col_add1.outputs[0], l_col_add2.inputs[1]) 414 | links.new(l_col_add2.outputs[0], l_col_add3.inputs[1]) 415 | links.new(l_col_add3.outputs[0], l_col_add4.inputs[1]) 416 | links.new(node_emission.outputs[0], l_col_add4.inputs[2]) 417 | 418 | node_offset_x = 0 419 | node_offset_y = -300 420 | 421 | for i in range(4): 422 | light_node = create_light_nodes(mat, i, (-6500 - i * 150, -300)) 423 | if i == 0 or i == 1: 424 | links.new(light_node.outputs[0], l_col_add1.inputs[i + 1]) 425 | elif i == 2: 426 | links.new(light_node.outputs[0], l_col_add2.inputs[2]) 427 | else: 428 | links.new(light_node.outputs[0], l_col_add3.inputs[2]) 429 | node_offset_y -= 350 430 | node_offset_x = 0 431 | 432 | links.new(l_col_add3.outputs[0], l_col_add4.inputs[1]) 433 | links.new(l_col_add4.outputs[0], use_diffuse_node.inputs[1]) 434 | light_total_result = use_diffuse_node 435 | 436 | # if no light is enbaled 437 | use_only_diffuse = True 438 | for light in lights: 439 | if light["isLightEnabled"]: 440 | use_only_diffuse = False 441 | 442 | light_total_result.inputs[0].default_value = use_only_diffuse 443 | light_total_result.inputs[2].default_value = matcols["df"] 444 | 445 | return light_total_result 446 | 447 | 448 | def generate_decal_vc_nodes(material): 449 | nodes = material.node_tree.nodes 450 | links = material.node_tree.links 451 | 452 | node_mix_1 = nodes.new(type='ShaderNodeMixRGB') 453 | node_mix_1.inputs[0].default_value = 0.0 454 | 455 | node_mix_2 = nodes.new(type='ShaderNodeMixRGB') 456 | 457 | links.new(node_mix_1.outputs[0], node_mix_2.inputs[2]) 458 | 459 | if "tx" in material.nns_mat_type: 460 | node_image = generate_image_nodes(material) 461 | links.new(node_image.outputs[0], node_mix_1.inputs[1]) 462 | links.new(node_image.outputs[1], node_mix_1.inputs[2]) 463 | links.new(node_image.outputs[1], node_mix_2.inputs[0]) 464 | 465 | node_mix_shader = nodes.new(type='ShaderNodeMixShader') 466 | node_trans_bsdf = nodes.new(type='ShaderNodeBsdfTransparent') 467 | 468 | links.new(node_mix_2.outputs[0], node_mix_shader.inputs[2]) 469 | links.new(node_trans_bsdf.outputs[0], node_mix_shader.inputs[1]) 470 | 471 | if "vc" in material.nns_mat_type: 472 | node_attr = nodes.new(type='ShaderNodeAttribute') 473 | node_attr.attribute_name = 'Col' 474 | links.new(node_attr.outputs[0], node_mix_2.inputs[1]) 475 | 476 | elif "nr" in material.nns_mat_type: 477 | node_vertex_lighting = generate_normal_lightning_color_nodes(material) 478 | links.new(node_vertex_lighting.outputs[0], node_mix_2.inputs[1]) 479 | 480 | else: 481 | node_diffuse = nodes.new(type='ShaderNodeMixRGB') 482 | node_diffuse.name = 'nns_node_diffuse' 483 | node_diffuse.blend_type = 'MULTIPLY' 484 | node_diffuse.inputs[0].default_value = 1.0 485 | node_diffuse.inputs[1].default_value = (1.0, 1.0, 1.0, 1.0) 486 | node_diffuse.inputs[2].default_value = ( 487 | material.nns_diffuse[0], 488 | material.nns_diffuse[1], 489 | material.nns_diffuse[2], 490 | 1.0) 491 | links.new(node_diffuse.outputs[0], node_mix_2.inputs[1]) 492 | 493 | node_mix_rgb = nodes.new(type='ShaderNodeMixRGB') 494 | node_mix_rgb.location = (0, 200) 495 | node_mix_rgb.blend_type = 'MIX' 496 | node_mix_rgb.name = 'nns_node_alpha' 497 | node_mix_rgb.inputs[0].default_value = 1.0 498 | node_mix_rgb.inputs[1].default_value = (0, 0, 0, 1) 499 | node_mix_rgb.inputs[2].default_value = ( 500 | material.nns_alpha / 31, 501 | material.nns_alpha / 31, 502 | material.nns_alpha / 31, 503 | 1.0) 504 | 505 | if material.nns_display_face == "both": 506 | links.new(node_mix_rgb.outputs[0], node_mix_shader.inputs[0]) 507 | else: 508 | node_face = generate_culling_nodes(material) 509 | links.new(node_mix_rgb.outputs[0], node_face.inputs[2]) 510 | links.new(node_face.outputs[0], node_mix_shader.inputs[0]) 511 | 512 | generate_output_node(material, node_mix_shader) 513 | 514 | 515 | def generate_image_nodes(material): 516 | nodes = material.node_tree.nodes 517 | links = material.node_tree.links 518 | 519 | 520 | node_image = nodes.new(type='ShaderNodeTexImage') 521 | node_image.name = 'nns_node_image' 522 | node_image.interpolation = 'Closest' 523 | if material.nns_image != '': 524 | try: 525 | #print(material.nns_image) 526 | node_image.image = material.nns_image 527 | except Exception: 528 | raise NameError("Cannot load image") 529 | 530 | # Make this ahead of time. Must always be filled. 531 | node_srt = None 532 | 533 | if material.nns_tex_gen_mode == "nrm": 534 | node_geo = nodes.new(type='ShaderNodeNewGeometry') 535 | node_vec_trans = nodes.new(type='ShaderNodeVectorTransform') 536 | node_vec_trans.convert_from = 'WORLD' 537 | node_vec_trans.convert_to = 'CAMERA' 538 | node_vec_trans.vector_type = 'NORMAL' 539 | node_mapping = nodes.new(type='ShaderNodeMapping') 540 | node_mapping.inputs[3].default_value = (0.5, 0.5, 0.5) 541 | links.new(node_geo.outputs[1], node_vec_trans.inputs[0]) 542 | links.new(node_vec_trans.outputs[0], node_mapping.inputs[0]) 543 | node_srt = generate_srt_nodes(material, node_mapping) 544 | else: 545 | node_uvmap = nodes.new(type='ShaderNodeUVMap') 546 | node_uvmap.uv_map = "UVMap" 547 | node_srt = generate_srt_nodes(material, node_uvmap) 548 | 549 | node_sp_xyz = nodes.new(type='ShaderNodeSeparateXYZ') 550 | links.new(node_srt.outputs[0], node_sp_xyz.inputs[0]) 551 | 552 | node_cb_xyz = nodes.new(type='ShaderNodeCombineXYZ') 553 | links.new(node_sp_xyz.outputs[2], node_cb_xyz.inputs[2]) 554 | 555 | if material.nns_tex_tiling_u == "flip": 556 | node_math_u = nodes.new(type='ShaderNodeMath') 557 | node_math_u.operation = 'PINGPONG' 558 | node_math_u.inputs[1].default_value = 1.0 559 | links.new(node_sp_xyz.outputs[0], node_math_u.inputs[0]) 560 | links.new(node_math_u.outputs[0], node_cb_xyz.inputs[0]) 561 | elif material.nns_tex_tiling_u == "clamp": 562 | node_math_u = nodes.new(type='ShaderNodeMath') 563 | node_math_u.operation = 'MINIMUM' 564 | node_math_u.inputs[1].default_value = 0.99 565 | node_math_u.use_clamp = True 566 | links.new(node_sp_xyz.outputs[0], node_math_u.inputs[0]) 567 | links.new(node_math_u.outputs[0], node_cb_xyz.inputs[0]) 568 | else: 569 | links.new(node_sp_xyz.outputs[0], node_cb_xyz.inputs[0]) 570 | 571 | if material.nns_tex_tiling_v == "flip": 572 | node_math_v = nodes.new(type='ShaderNodeMath') 573 | node_math_v.operation = 'PINGPONG' 574 | node_math_v.inputs[1].default_value = 1.0 575 | links.new(node_sp_xyz.outputs[1], node_math_v.inputs[0]) 576 | links.new(node_math_v.outputs[0], node_cb_xyz.inputs[1]) 577 | elif material.nns_tex_tiling_v == "clamp": 578 | node_math_v = nodes.new(type='ShaderNodeMath') 579 | node_math_v.operation = 'MINIMUM' 580 | node_math_v.inputs[1].default_value = 0.99 581 | node_math_v.use_clamp = True 582 | links.new(node_sp_xyz.outputs[1], node_math_v.inputs[0]) 583 | links.new(node_math_v.outputs[0], node_cb_xyz.inputs[1]) 584 | else: 585 | links.new(node_sp_xyz.outputs[1], node_cb_xyz.inputs[1]) 586 | 587 | links.new(node_cb_xyz.outputs[0], node_image.inputs[0]) 588 | 589 | return node_image 590 | 591 | 592 | def generate_mod_vc_nodes(material): 593 | nodes = material.node_tree.nodes 594 | links = material.node_tree.links 595 | 596 | node_mix = nodes.new(type='ShaderNodeMixRGB') 597 | node_mix.inputs[0].default_value = 0.0 598 | 599 | node_mix_rgb = nodes.new(type='ShaderNodeMixRGB') 600 | node_mix_rgb.blend_type = 'MULTIPLY' 601 | node_mix_rgb.name = 'nns_node_alpha' 602 | node_mix_rgb.inputs[0].default_value = 1.0 603 | node_mix_rgb.inputs[1].default_value = (1.0, 1.0, 1.0, 1.0) 604 | node_mix_rgb.inputs[2].default_value = ( 605 | material.nns_alpha / 31, 606 | material.nns_alpha / 31, 607 | material.nns_alpha / 31, 608 | 1.0 609 | ) 610 | 611 | if "tx" in material.nns_mat_type: 612 | node_image = generate_image_nodes(material) 613 | links.new(node_image.outputs[0], node_mix.inputs[1]) 614 | links.new(node_image.outputs[1], node_mix.inputs[2]) 615 | links.new(node_image.outputs[1], node_mix_rgb.inputs[1]) 616 | 617 | node_multiply = nodes.new(type='ShaderNodeMixRGB') 618 | node_multiply.name = 'nns_node_diffuse' 619 | node_multiply.blend_type = 'MULTIPLY' 620 | node_multiply.inputs[0].default_value = 1.0 621 | node_multiply.inputs[1].default_value = (1.0, 1.0, 1.0, 1.0) 622 | node_multiply.inputs[2].default_value = ( 623 | material.nns_diffuse[0], 624 | material.nns_diffuse[1], 625 | material.nns_diffuse[2], 626 | 1.0 627 | ) 628 | 629 | if "vc" in material.nns_mat_type: 630 | node_attr = nodes.new(type='ShaderNodeAttribute') 631 | node_attr.attribute_name = 'Col' 632 | links.new(node_attr.outputs[0], node_multiply.inputs[2]) 633 | 634 | elif "nr" in material.nns_mat_type: 635 | node_vertex_lighting = generate_normal_lightning_color_nodes(material) 636 | links.new(node_vertex_lighting.outputs[0], node_multiply.inputs[2]) 637 | 638 | node_bsdf = nodes.new(type='ShaderNodeBsdfTransparent') 639 | node_mix_shader = nodes.new(type='ShaderNodeMixShader') 640 | 641 | links.new(node_mix.outputs[0], node_multiply.inputs[1]) 642 | links.new(node_multiply.outputs[0], node_mix_shader.inputs[2]) 643 | 644 | if material.nns_display_face == "both": 645 | links.new(node_mix_rgb.outputs[0], node_mix_shader.inputs[0]) 646 | else: 647 | node_face = generate_culling_nodes(material) 648 | links.new(node_mix_rgb.outputs[0], node_face.inputs[2]) 649 | links.new(node_face.outputs[0], node_mix_shader.inputs[0]) 650 | 651 | links.new(node_bsdf.outputs[0], node_mix_shader.inputs[1]) 652 | generate_output_node(material, node_mix_shader) 653 | 654 | 655 | def generate_image_only_nodes(material): 656 | nodes = material.node_tree.nodes 657 | links = material.node_tree.links 658 | 659 | node_mix_rgb = nodes.new(type='ShaderNodeMixRGB') 660 | node_mix_rgb.blend_type = 'MULTIPLY' 661 | node_mix_rgb.name = 'nns_node_alpha' 662 | node_mix_rgb.inputs[0].default_value = 1.0 663 | node_mix_rgb.inputs[2].default_value = ( 664 | material.nns_alpha / 31, 665 | material.nns_alpha / 31, 666 | material.nns_alpha / 31, 667 | 1.0 668 | ) 669 | node_bsdf = nodes.new(type='ShaderNodeBsdfTransparent') 670 | node_mix_shader = nodes.new(type='ShaderNodeMixShader') 671 | node_image = generate_image_nodes(material) 672 | 673 | links.new(node_image.outputs[0], node_mix_shader.inputs[2]) 674 | links.new(node_image.outputs[1], node_mix_rgb.inputs[1]) 675 | 676 | if material.nns_display_face == "both": 677 | links.new(node_mix_rgb.outputs[0], node_mix_shader.inputs[0]) 678 | else: 679 | node_face = generate_culling_nodes(material) 680 | links.new(node_mix_rgb.outputs[0], node_face.inputs[2]) 681 | links.new(node_face.outputs[0], node_mix_shader.inputs[0]) 682 | 683 | links.new(node_bsdf.outputs[0], node_mix_shader.inputs[1]) 684 | generate_output_node(material, node_mix_shader) 685 | 686 | 687 | def generate_solid_color_nodes(material): 688 | nodes = material.node_tree.nodes 689 | links = material.node_tree.links 690 | 691 | node_mix_rgb = nodes.new(type='ShaderNodeMixRGB') 692 | node_mix_rgb.location = (0, 200) 693 | node_mix_rgb.blend_type = 'MULTIPLY' 694 | node_mix_rgb.name = 'nns_node_alpha' 695 | node_mix_rgb.inputs[0].default_value = 1.0 696 | node_mix_rgb.inputs[1].default_value = (1.0, 1.0, 1.0, 1.0) 697 | node_mix_rgb.inputs[2].default_value = ( 698 | material.nns_alpha / 31, 699 | material.nns_alpha / 31, 700 | material.nns_alpha / 31, 701 | 1.0 702 | ) 703 | node_bsdf = nodes.new(type='ShaderNodeBsdfTransparent') 704 | node_mix_df = nodes.new(type='ShaderNodeMixRGB') 705 | node_mix_df.location = (0, -100) 706 | node_mix_df.name = 'nns_node_diffuse' 707 | node_mix_df.inputs[0].default_value = 1.0 708 | node_mix_df.inputs[2].default_value = ( 709 | material.nns_diffuse[0], 710 | material.nns_diffuse[1], 711 | material.nns_diffuse[2], 712 | 1.0 713 | ) 714 | node_mix_shader = nodes.new(type='ShaderNodeMixShader') 715 | node_mix_shader.location = (200, 0) 716 | 717 | if material.nns_display_face == "both": 718 | links.new(node_mix_rgb.outputs[0], node_mix_shader.inputs[0]) 719 | else: 720 | node_face = generate_culling_nodes(material) 721 | links.new(node_mix_rgb.outputs[0], node_face.inputs[2]) 722 | links.new(node_face.outputs[0], node_mix_shader.inputs[0]) 723 | 724 | links.new(node_bsdf.outputs[0], node_mix_shader.inputs[1]) 725 | links.new(node_mix_df.outputs[0], node_mix_shader.inputs[2]) 726 | generate_output_node(material, node_mix_shader) 727 | 728 | 729 | def generate_only_normal_lighting(material): 730 | nodes = material.node_tree.nodes 731 | links = material.node_tree.links 732 | 733 | node_mix_rgb = nodes.new(type='ShaderNodeMixRGB') 734 | node_mix_rgb.blend_type = 'MULTIPLY' 735 | node_mix_rgb.name = 'nns_node_alpha' 736 | node_mix_rgb.inputs[0].default_value = 1.0 737 | node_mix_rgb.inputs[1].default_value = (1.0, 1.0, 1.0, 1.0) 738 | node_mix_rgb.inputs[2].default_value = ( 739 | material.nns_alpha / 31, 740 | material.nns_alpha / 31, 741 | material.nns_alpha / 31, 742 | 1.0 743 | ) 744 | node_bsdf = nodes.new(type='ShaderNodeBsdfTransparent') 745 | node_vc_light = generate_normal_lightning_color_nodes(material) 746 | node_mix_shader = nodes.new(type='ShaderNodeMixShader') 747 | 748 | if material.nns_display_face == "both": 749 | links.new(node_mix_rgb.outputs[0], node_mix_shader.inputs[0]) 750 | else: 751 | node_face = generate_culling_nodes(material) 752 | links.new(node_mix_rgb.outputs[0], node_face.inputs[2]) 753 | links.new(node_face.outputs[0], node_mix_shader.inputs[0]) 754 | 755 | links.new(node_bsdf.outputs[0], node_mix_shader.inputs[1]) 756 | links.new(node_vc_light.outputs[0], node_mix_shader.inputs[2]) 757 | generate_output_node(material, node_mix_shader) 758 | 759 | 760 | def generate_only_vc_nodes(material): 761 | nodes = material.node_tree.nodes 762 | links = material.node_tree.links 763 | 764 | node_mix_rgb = nodes.new(type='ShaderNodeMixRGB') 765 | node_mix_rgb.blend_type = 'MULTIPLY' 766 | node_mix_rgb.name = 'nns_node_alpha' 767 | node_mix_rgb.inputs[0].default_value = 1.0 768 | node_mix_rgb.inputs[1].default_value = (1.0, 1.0, 1.0, 1.0) 769 | node_mix_rgb.inputs[2].default_value = ( 770 | material.nns_alpha / 31, 771 | material.nns_alpha / 31, 772 | material.nns_alpha / 31, 773 | 1.0 774 | ) 775 | node_bsdf = nodes.new(type='ShaderNodeBsdfTransparent') 776 | node_vc = nodes.new(type='ShaderNodeVertexColor') 777 | node_vc.layer_name = 'Col' 778 | node_mix_shader = nodes.new(type='ShaderNodeMixShader') 779 | 780 | if material.nns_display_face == "both": 781 | links.new(node_mix_rgb.outputs[0], node_mix_shader.inputs[0]) 782 | else: 783 | node_face = generate_culling_nodes(material) 784 | links.new(node_mix_rgb.outputs[0], node_face.inputs[2]) 785 | links.new(node_face.outputs[0], node_mix_shader.inputs[0]) 786 | 787 | links.new(node_bsdf.outputs[0], node_mix_shader.inputs[1]) 788 | links.new(node_vc.outputs[0], node_mix_shader.inputs[2]) 789 | generate_output_node(material, node_mix_shader) 790 | 791 | # fog 792 | 793 | node_fog_offset_x = 0 794 | node_fog_offset_y = 0 795 | loca_fog = (0, 0) 796 | 797 | 798 | def create_node_fog(name, node_type, location=loca_fog, offset_mode="x"): 799 | global node_fog_offset_x 800 | global node_fog_offset_y 801 | nodes = bpy.data.node_groups.get("nns fog").nodes 802 | newnode = nodes.new(type=node_type) 803 | newnode.name = name 804 | newnode.label = name 805 | if offset_mode == 0: 806 | newnode.location = location 807 | elif offset_mode == "x": 808 | newnode.location = (location[0] + node_fog_offset_x, location[1] + node_fog_offset_y) 809 | node_fog_offset_x += 180 810 | elif offset_mode == "y": 811 | newnode.location = (location[0], location[1] + node_fog_offset_y) 812 | node_fog_offset_y -= 150 813 | elif offset_mode == "xy": 814 | newnode.location = (location[0] + node_fog_offset_x, location[1] + node_fog_offset_y) 815 | node_fog_offset_x += 180 816 | newnode.location = (location[0], location[1] + node_fog_offset_y) 817 | node_fog_offset_y -= 150 818 | return newnode 819 | 820 | 821 | def generate_fog_group(): 822 | if "nns fog" not in bpy.data.node_groups.keys(): 823 | fog_group = bpy.data.node_groups.new(name="nns fog", type="ShaderNodeTree") 824 | else: 825 | fog_group = bpy.data.node_groups.get("nns fog") 826 | 827 | nodes = fog_group.nodes 828 | nodes.clear() 829 | 830 | input_node = create_node_fog("input node", "NodeGroupInput", (-150, 0)) 831 | output_node = create_node_fog("output_node", "NodeGroupOutput", (1000, 0), 0) 832 | 833 | # reset group 834 | 835 | if "surface color" not in fog_group.inputs or "mat use fog" not in fog_group.inputs: 836 | for input in fog_group.inputs: 837 | fog_group.inputs.remove(input) 838 | fog_group.inputs.new("NodeSocketColor", "surface color") 839 | fog_group.inputs.new("NodeSocketFloat", "mat use fog") 840 | 841 | if "Color" not in fog_group.outputs: 842 | for output in fog_group.outputs: 843 | fog_group.outputs.remove(output) 844 | fog_group.outputs.new("NodeSocketColor", "Color") 845 | 846 | for node in nodes: 847 | nodes.remove(node) 848 | 849 | generate_fog_group_nodes(fog_group, input_node, output_node) 850 | 851 | 852 | def generate_fog_group_nodes(fog_group, input_node, output_node): 853 | global node_fog_offset_x 854 | global node_fog_offset_y 855 | 856 | node_fog_offset_x = 0 857 | node_fog_offset_y = 0 858 | # input data 859 | input_node = create_node_fog("inputs", "NodeGroupInput", (0, 150), 0) 860 | 861 | # invariable 862 | cam = create_node_fog("cam", "ShaderNodeCameraData", loca_fog, "y") 863 | 864 | # variable 865 | use_fog = create_node_fog("use_fog", "ShaderNodeValue", loca_fog, "y") 866 | scale = create_node_fog("scale", "ShaderNodeValue", loca_fog, "y") 867 | scale.outputs[0].default_value = 20 868 | fog_offset = create_node_fog("fog offset", "ShaderNodeValue", loca_fog, "y") 869 | fog_color = create_node_fog("fog color", "ShaderNodeRGB", loca_fog, "y") 870 | 871 | node_fog_offset_y = 0 872 | node_fog_offset_x = 150 873 | 874 | divide = create_node_fog("divide", "ShaderNodeMath") 875 | divide.operation = "DIVIDE" 876 | 877 | subtract = create_node_fog("subtract", "ShaderNodeMath") 878 | subtract.operation = "SUBTRACT" 879 | subtract.use_clamp = True 880 | 881 | sub_c1 = create_node_fog("subtract", "ShaderNodeMath") 882 | sub_c1.operation = "SUBTRACT" 883 | sub_c1.inputs[0].default_value = 1 884 | 885 | pow1 = create_node_fog("pow", "ShaderNodeMath") 886 | pow1.operation = "POWER" 887 | pow1.inputs[1].default_value = 7 888 | 889 | sub_c2 = create_node_fog("subtract", "ShaderNodeMath") 890 | sub_c2.operation = "SUBTRACT" 891 | sub_c2.inputs[0].default_value = 1 892 | 893 | pow2 = create_node_fog("pow", "ShaderNodeMath") 894 | pow2.operation = "POWER" 895 | pow2.inputs[1].default_value = 0.5 896 | 897 | mix_c = create_node_fog("mix 1", "ShaderNodeMixRGB") 898 | mix_c.blend_type = "MIX" 899 | 900 | curve = create_node_fog("curve", "ShaderNodeRGBCurve") 901 | curve.mapping.extend = 'HORIZONTAL' 902 | curve.width = 350 903 | node_fog_offset_x += 300 904 | 905 | mix_1 = create_node_fog("mix 1", "ShaderNodeMixRGB") 906 | mix_1.blend_type = "MIX" 907 | 908 | mult = create_node_fog("mult", "ShaderNodeMath") 909 | mult.operation = "MULTIPLY" 910 | 911 | mix_2 = create_node_fog("mix 2", "ShaderNodeMixRGB") 912 | mix_2.blend_type = "MIX" 913 | 914 | links = fog_group.links 915 | 916 | links.new(cam.outputs[1], divide.inputs[0]) 917 | links.new(scale.outputs[0], divide.inputs[1]) 918 | 919 | links.new(divide.outputs[0], subtract.inputs[0]) 920 | links.new(fog_offset.outputs[0], subtract.inputs[1]) 921 | 922 | # blending correction 923 | 924 | links.new(subtract.outputs[0], sub_c1.inputs[1]) 925 | links.new(sub_c1.outputs[0], pow1.inputs[0]) 926 | links.new(pow1.outputs[0], sub_c2.inputs[1]) 927 | 928 | links.new(fog_color.outputs[0], pow2.inputs[0]) 929 | links.new(pow2.outputs[0],mix_c.inputs[0]) 930 | links.new(sub_c2.outputs[0], mix_c.inputs[1]) 931 | links.new(subtract.outputs[0], mix_c.inputs[2]) 932 | 933 | # fog density curve 934 | 935 | links.new(mix_c.outputs[0], curve.inputs[1]) 936 | links.new(curve.outputs[0], mix_1.inputs[0]) 937 | links.new(input_node.outputs["surface color"], mix_1.inputs[1]) 938 | links.new(fog_color.outputs[0], mix_1.inputs[2]) 939 | 940 | links.new(input_node.outputs["mat use fog"], mult.inputs[0]) 941 | links.new(use_fog.outputs[0], mult.inputs[1]) 942 | 943 | links.new(mult.outputs[0], mix_2.inputs[0]) 944 | links.new(mix_1.outputs[0], mix_2.inputs[2]) 945 | links.new(input_node.outputs["surface color"], mix_2.inputs[1]) 946 | 947 | # output data 948 | 949 | output_node = create_node_fog("outputs", "NodeGroupOutput") 950 | links.new(mix_2.outputs[0], output_node.inputs["Color"]) 951 | 952 | 953 | def generate_fog_material_nodes(material): 954 | mat = material 955 | nodes = mat.node_tree.nodes 956 | links = mat.node_tree.links 957 | 958 | mix_shader = nodes.get("Mix Shader") 959 | nns_df = mix_shader.inputs[2].links[0].from_node 960 | 961 | nns_fog = create_node(mat, "nns fog", "ShaderNodeGroup", loca) 962 | update_fog_group_nodes(self=None, context=bpy.context) 963 | nns_fog.node_tree = bpy.data.node_groups.get("nns fog") 964 | nns_fog.inputs[1].default_value = int(mat.nns_fog) 965 | 966 | nns_fog.location = (mix_shader.location[0], mix_shader.location[1] - 150) 967 | links.new(nns_df.outputs[0], nns_fog.inputs[0]) 968 | links.new(nns_fog.outputs[0], mix_shader.inputs[2]) 969 | 970 | 971 | def generate_nodes(material): 972 | if material.is_nns: 973 | nodes = material.node_tree.nodes 974 | nodes.clear() 975 | links = material.node_tree.links 976 | links.clear() 977 | 978 | if material.nns_mat_type == "tx": 979 | print("oui") 980 | generate_image_only_nodes(material) 981 | elif material.nns_mat_type == "df": 982 | generate_solid_color_nodes(material) 983 | elif material.nns_mat_type == "vc": 984 | generate_only_vc_nodes(material) 985 | elif material.nns_mat_type == "df_nr": 986 | generate_only_normal_lighting(material) 987 | elif material.nns_polygon_mode == "modulate" \ 988 | or material.nns_polygon_mode == "toon_highlight" \ 989 | or material.nns_polygon_mode == "shadow": 990 | generate_mod_vc_nodes(material) 991 | elif material.nns_polygon_mode == "decal": 992 | generate_decal_vc_nodes(material) 993 | 994 | generate_fog_material_nodes(material) 995 | 996 | def update_fog_group_nodes(self, context): 997 | if "nns fog" not in bpy.data.node_groups.keys(): 998 | generate_fog_group() 999 | 1000 | fog_group = bpy.data.node_groups.get("nns fog") 1001 | if "fog offset" not in fog_group.nodes.keys(): 1002 | generate_fog_group() 1003 | 1004 | nodes = fog_group.nodes 1005 | 1006 | use_fog = nodes.get("use_fog") 1007 | use_fog.outputs[0].default_value = context.scene.Fog_enable # scene property 1008 | 1009 | scale = nodes.get("scale") 1010 | scale.outputs[0].default_value = context.scene.Fog_scale 1011 | 1012 | fog_offset = nodes.get("fog offset") 1013 | fog_offset.outputs[0].default_value = context.scene.Fog_offset 1014 | 1015 | fog_color = nodes.get("fog color") 1016 | color = context.scene.Fog_color 1017 | fog_color.outputs[0].default_value = (color[0], color[1], color[2], 1) 1018 | 1019 | 1020 | def update_material_fog(self, context): 1021 | material = context.material 1022 | nodes = material.node_tree.nodes 1023 | group = nodes.get("nns fog") 1024 | if group is not None: 1025 | group.inputs[1].default_value = int(material.nns_fog) 1026 | else: 1027 | generate_nodes(material) 1028 | 1029 | 1030 | def update_light0(self, context): 1031 | for mat in bpy.data.materials.values(): 1032 | if mat.is_nns: 1033 | if "nr" in mat.nns_mat_type: 1034 | nodes = mat.node_tree.nodes 1035 | col = nodes.get("Light0 Color") 1036 | if col is not None: 1037 | col.outputs[0].default_value = (bpy.context.scene.Light0_color[0], 1038 | bpy.context.scene.Light0_color[1], 1039 | bpy.context.scene.Light0_color[2], 1040 | 1.0) 1041 | vec = nodes.get("Light0 Vector") 1042 | for i in range(3): 1043 | vec.inputs[i].default_value = bpy.context.scene.Light0_vector[i] 1044 | spec = nodes.get("Light0 Specular") 1045 | spec.outputs[0].default_value = bpy.context.scene.Light0_specular 1046 | 1047 | 1048 | def update_light1(self, context): 1049 | for mat in bpy.data.materials.values(): 1050 | if mat.is_nns: 1051 | if "nr" in mat.nns_mat_type: 1052 | nodes = mat.node_tree.nodes 1053 | col = nodes.get("Light1 Color") 1054 | if col is not None: 1055 | col.outputs[0].default_value = (bpy.context.scene.Light1_color[0], 1056 | bpy.context.scene.Light1_color[1], 1057 | bpy.context.scene.Light1_color[2], 1058 | 1.0) 1059 | vec = nodes.get("Light1 Vector") 1060 | for i in range(3): 1061 | vec.inputs[i].default_value = bpy.context.scene.Light1_vector[i] 1062 | spec = nodes.get("Light1 Specular") 1063 | spec.outputs[0].default_value = bpy.context.scene.Light1_specular 1064 | 1065 | 1066 | def update_light2(self, context): 1067 | for mat in bpy.data.materials.values(): 1068 | if mat.is_nns: 1069 | if "nr" in mat.nns_mat_type: 1070 | nodes = mat.node_tree.nodes 1071 | col = nodes.get("Light2 Color") 1072 | if col is not None: 1073 | col.outputs[0].default_value = (bpy.context.scene.Light2_color[0], 1074 | bpy.context.scene.Light2_color[1], 1075 | bpy.context.scene.Light2_color[2], 1076 | 1.0) 1077 | vec = nodes.get("Light2 Vector") 1078 | for i in range(3): 1079 | vec.inputs[i].default_value = bpy.context.scene.Light2_vector[i] 1080 | spec = nodes.get("Light2 Specular") 1081 | spec.outputs[0].default_value = bpy.context.scene.Light2_specular 1082 | 1083 | 1084 | def update_light3(self, context): 1085 | for mat in bpy.data.materials.values(): 1086 | if mat.is_nns: 1087 | if "nr" in mat.nns_mat_type: 1088 | nodes = mat.node_tree.nodes 1089 | col = nodes.get("Light3 Color") 1090 | if col is not None: 1091 | col.outputs[0].default_value = (bpy.context.scene.Light3_color[0], 1092 | bpy.context.scene.Light3_color[1], 1093 | bpy.context.scene.Light3_color[2], 1094 | 1.0) 1095 | vec = nodes.get("Light3 Vector") 1096 | for i in range(3): 1097 | vec.inputs[i].default_value = bpy.context.scene.Light3_vector[i] 1098 | spec = nodes.get("Light3 Specular") 1099 | spec.outputs[0].default_value = bpy.context.scene.Light3_specular 1100 | 1101 | 1102 | def update_nodes_mode(self, context): 1103 | material = context.material 1104 | generate_nodes(material) 1105 | 1106 | 1107 | def update_nodes_mat_type(self, context): 1108 | material = context.material 1109 | generate_nodes(material) 1110 | 1111 | 1112 | def update_nodes_image(self, context): 1113 | material = context.material 1114 | if material.is_nns: 1115 | if material.nns_image != '': 1116 | try: 1117 | node_image = material.node_tree.nodes.get('nns_node_image') 1118 | node_image.image = material.nns_image 1119 | except Exception: 1120 | raise NameError("Cannot load image") 1121 | 1122 | 1123 | def update_nodes_alpha(self, context): 1124 | material = context.material 1125 | if material.is_nns: 1126 | if material.nns_polygon_mode == "modulate": 1127 | try: 1128 | node_alpha = material.node_tree.nodes.get('nns_node_alpha') 1129 | node_alpha.blend_type = "MULTIPLY" 1130 | node_alpha.inputs[2].default_value = ( 1131 | material.nns_alpha / 31, 1132 | material.nns_alpha / 31, 1133 | material.nns_alpha / 31, 1134 | 1.0 1135 | ) 1136 | except Exception: 1137 | raise NameError("Something alpha I think") 1138 | elif material.nns_polygon_mode == "decal": 1139 | try: 1140 | node_alpha = material.node_tree.nodes.get('nns_node_alpha') 1141 | node_alpha.blend_type = "MIX" 1142 | node_alpha.inputs[1].default_value = (0, 0, 0, 1) 1143 | node_alpha.inputs[0].default_value = material.nns_alpha / 31 1144 | except Exception: 1145 | raise NameError("Something alpha I think") 1146 | 1147 | 1148 | def update_nodes_diffuse(self, context): 1149 | material = context.material 1150 | if material.is_nns: 1151 | if material.nns_mat_type == "df" or material.nns_mat_type == "tx_df": 1152 | node_diffuse = material.node_tree.nodes.get('nns_node_diffuse') 1153 | node_diffuse.inputs[2].default_value = ( 1154 | material.nns_diffuse[0], 1155 | material.nns_diffuse[1], 1156 | material.nns_diffuse[2], 1157 | 1.0 1158 | ) 1159 | if "nr" in material.nns_mat_type: 1160 | node_diffuse1 = material.node_tree.nodes.get("df") 1161 | node_diffuse1.outputs[0].default_value = ( 1162 | material.nns_diffuse[0], 1163 | material.nns_diffuse[1], 1164 | material.nns_diffuse[2], 1165 | 1.0 1166 | ) 1167 | node_diffuse2 = material.node_tree.nodes.get("UseOnlyDiffuse?") 1168 | node_diffuse2.inputs[2].default_value = ( 1169 | material.nns_diffuse[0], 1170 | material.nns_diffuse[1], 1171 | material.nns_diffuse[2], 1172 | 1.0 1173 | ) 1174 | 1175 | 1176 | def update_nodes_emission(self, context): 1177 | material = context.material 1178 | if material.is_nns: 1179 | if "nr" in material.nns_mat_type: 1180 | node_emission = material.node_tree.nodes.get("em") 1181 | node_emission.outputs[0].default_value = ( 1182 | material.nns_emission[0], 1183 | material.nns_emission[1], 1184 | material.nns_emission[2], 1185 | 1.0 1186 | ) 1187 | 1188 | 1189 | def update_nodes_ambient(self, context): 1190 | material = context.material 1191 | if material.is_nns: 1192 | if "nr" in material.nns_mat_type: 1193 | node_emission = material.node_tree.nodes.get("amb") 1194 | node_emission.outputs[0].default_value = ( 1195 | material.nns_ambient[0], 1196 | material.nns_ambient[1], 1197 | material.nns_ambient[2], 1198 | 1.0 1199 | ) 1200 | 1201 | 1202 | def update_nodes_specular(self, context): 1203 | material = context.material 1204 | if material.is_nns: 1205 | if "nr" in material.nns_mat_type: 1206 | node_specular = material.node_tree.nodes.get("spec") 1207 | node_specular.outputs[0].default_value = ( 1208 | material.nns_specular[0], 1209 | material.nns_specular[1], 1210 | material.nns_specular[2], 1211 | 1.0 1212 | ) 1213 | 1214 | 1215 | def update_nodes_use_only_diffuse(material): 1216 | mask_node = material.node_tree.nodes.get("UseOnlyDiffuse?") 1217 | use_only_diffuse = True 1218 | lights = (material.nns_light0, material.nns_light1, material.nns_light2, material.nns_light3) 1219 | for light in lights: 1220 | if light: 1221 | use_only_diffuse = False 1222 | mask_node.inputs[0].default_value = use_only_diffuse 1223 | 1224 | 1225 | def update_nodes_light0(self, context): 1226 | material = context.material 1227 | update_nodes_use_only_diffuse(material) 1228 | if material.is_nns: 1229 | if "nr" in material.nns_mat_type: 1230 | node_light0 = material.node_tree.nodes.get("Light0 Enabled") 1231 | node_light0.outputs[0].default_value = material.nns_light0 1232 | 1233 | 1234 | def update_nodes_light1(self, context): 1235 | material = context.material 1236 | update_nodes_use_only_diffuse(material) 1237 | if material.is_nns: 1238 | if "nr" in material.nns_mat_type: 1239 | node_light1 = material.node_tree.nodes.get("Light1 Enabled") 1240 | node_light1.outputs[0].default_value = material.nns_light1 1241 | 1242 | 1243 | def update_nodes_light2(self, context): 1244 | material = context.material 1245 | update_nodes_use_only_diffuse(material) 1246 | if material.is_nns: 1247 | if "nr" in material.nns_mat_type: 1248 | node_light2 = material.node_tree.nodes.get("Light2 Enabled") 1249 | node_light2.outputs[0].default_value = material.nns_light2 1250 | 1251 | 1252 | def update_nodes_light3(self, context): 1253 | material = context.material 1254 | update_nodes_use_only_diffuse(material) 1255 | if material.is_nns: 1256 | if "nr" in material.nns_mat_type: 1257 | node_light3 = material.node_tree.nodes.get("Light3 Enabled") 1258 | node_light3.outputs[0].default_value = material.nns_light3 1259 | 1260 | 1261 | def update_nodes_face(self, context): 1262 | material = context.material 1263 | generate_nodes(material) 1264 | 1265 | 1266 | def update_nodes_tex_gen(self, context): 1267 | material = context.material 1268 | generate_nodes(material) 1269 | 1270 | 1271 | def update_nodes_srt(material): 1272 | if material.is_nns: 1273 | if "tx" in material.nns_mat_type: 1274 | try: 1275 | node_rt = material.node_tree.nodes.get('nns_node_rt') 1276 | node_rt.inputs[1].default_value = ( 1277 | -material.nns_srt_translate[0], 1278 | -material.nns_srt_translate[1], 1279 | 0 1280 | ) 1281 | node_rt.inputs[2].default_value[2] = material.nns_srt_rotate 1282 | node_s = material.node_tree.nodes.get('nns_node_s') 1283 | node_s.inputs[3].default_value = ( 1284 | material.nns_srt_scale[0], 1285 | material.nns_srt_scale[1], 1286 | 0 1287 | ) 1288 | 1289 | node_i = material.node_tree.nodes.get('nns_node_image') 1290 | if material.nns_texframe_reference: 1291 | node_i.image = material.nns_texframe_reference[material.nns_texframe_reference_index].image 1292 | else: 1293 | node_i.image = material.nns_image 1294 | except Exception: 1295 | raise NameError("Couldn't find node?") 1296 | 1297 | 1298 | def update_nodes_srt_hook(self, context): 1299 | for material in bpy.data.materials.values(): 1300 | if material.is_nns: 1301 | update_nodes_srt(material) 1302 | 1303 | 1304 | @persistent 1305 | def frame_change_handler(scene): 1306 | if bpy.context.active_object.active_material: 1307 | material = bpy.context.active_object.active_material 1308 | update_nodes_srt(material) 1309 | 1310 | 1311 | def create_nns_material(obj): 1312 | material = bpy.data.materials.new('Material') 1313 | obj.data.materials.append(material) 1314 | if bpy.context.object is not None: 1315 | bpy.context.object.active_material_index = len(obj.material_slots) - 1 1316 | 1317 | material.is_nns = True 1318 | material.use_nodes = True 1319 | material.blend_method = 'CLIP' 1320 | 1321 | generate_nodes(material) 1322 | 1323 | 1324 | # This class is taken and modified from kurethedead's fast64 plugin. 1325 | class CreateNNSMaterial(bpy.types.Operator): 1326 | bl_idname = 'object.create_nns_material' 1327 | bl_label = "Create NNS Material" 1328 | bl_options = {'REGISTER', 'UNDO', 'PRESET'} 1329 | 1330 | def execute(self, context): 1331 | obj = bpy.context.view_layer.objects.active 1332 | if obj is None: 1333 | self.report({'ERROR'}, 'No active object selected.') 1334 | else: 1335 | create_nns_material(obj) 1336 | self.report({'INFO'}, 'NNS: Created new material.') 1337 | return {'FINISHED'} 1338 | 1339 | 1340 | class NTRTexReference(bpy.types.PropertyGroup): 1341 | image: PointerProperty( 1342 | name='Texture', 1343 | type=Image) 1344 | 1345 | 1346 | class NewTexReference(bpy.types.Operator): 1347 | bl_idname = "nns_texframe_reference.new_texref" 1348 | bl_label = "Add a new texture reference" 1349 | 1350 | def execute(self, context): 1351 | context.material.nns_texframe_reference.add() 1352 | 1353 | return {'FINISHED'} 1354 | 1355 | 1356 | class DeleteTexReference(bpy.types.Operator): 1357 | bl_idname = "nns_texframe_reference.delete_texref" 1358 | bl_label = "Deletes a texture reference" 1359 | 1360 | @classmethod 1361 | def poll(cls, context): 1362 | return context.material.nns_texframe_reference 1363 | 1364 | def execute(self, context): 1365 | my_list = context.material.nns_texframe_reference 1366 | index = context.material.nns_texframe_reference_index 1367 | 1368 | my_list.remove(index) 1369 | context.material.nns_texframe_reference_index = min( 1370 | max(0, index - 1), len(my_list) - 1) 1371 | 1372 | return {'FINISHED'} 1373 | 1374 | 1375 | class NTR_UL_texframe(bpy.types.UIList): 1376 | def draw_item(self, context, layout, data, item, icon, active_data, 1377 | active_propname): 1378 | if self.layout_type in {'DEFAULT', 'COMPACT'}: 1379 | if item.image: 1380 | layout.label(text=item.image.name, translate=False, 1381 | icon_value=layout.icon(item.image)) 1382 | else: 1383 | layout.label(text="", translate=False, icon='FILE_IMAGE') 1384 | elif self.layout_type in {'GRID'}: 1385 | layout.alignment = 'CENTER' 1386 | layout.label(text="", icon_value=icon) 1387 | 1388 | 1389 | class NTR_PT_material_texframe(bpy.types.Panel): 1390 | bl_label = "NNS Material texframes" 1391 | bl_idname = "MATERIAL_TEXFRAME_PT_nns" 1392 | bl_space_type = 'PROPERTIES' 1393 | bl_region_type = 'WINDOW' 1394 | bl_context = "material" 1395 | bl_options = {'HIDE_HEADER'} 1396 | 1397 | def draw(self, context): 1398 | layout = self.layout 1399 | mat = context.material 1400 | 1401 | if mat is None: 1402 | pass 1403 | elif not (mat.use_nodes and mat.is_nns): 1404 | pass 1405 | elif "tx" in mat.nns_mat_type: 1406 | layout = layout.box() 1407 | title = layout.column() 1408 | title.box().label(text="NNS Material texture pattern") 1409 | layout.template_list("NTR_UL_texframe", "", 1410 | mat, "nns_texframe_reference", 1411 | mat, "nns_texframe_reference_index") 1412 | 1413 | row = layout.row() 1414 | row.operator('nns_texframe_reference.new_texref', text='New') 1415 | row.operator('nns_texframe_reference.delete_texref', text='Delete') 1416 | 1417 | if (mat.nns_texframe_reference_index >= 0 1418 | and mat.nns_texframe_reference): 1419 | idx = mat.nns_texframe_reference_index 1420 | item = mat.nns_texframe_reference[idx] 1421 | 1422 | row = layout.row() 1423 | row.template_ID(item, "image", open="image.open") 1424 | 1425 | 1426 | class NTR_PT_material_keyframe(bpy.types.Panel): 1427 | bl_label = "NNS Material Keyframes" 1428 | bl_idname = "MATERIAL_KEYFRAME_PT_nns" 1429 | bl_space_type = 'PROPERTIES' 1430 | bl_region_type = 'WINDOW' 1431 | bl_context = "material" 1432 | bl_options = {'HIDE_HEADER'} 1433 | 1434 | @classmethod 1435 | def poll(cls, context): 1436 | return context.material 1437 | 1438 | def draw(self, context): 1439 | layout = self.layout 1440 | mat = context.material 1441 | 1442 | if mat is None: 1443 | pass 1444 | elif not (mat.use_nodes and mat.is_nns): 1445 | pass 1446 | elif "tx" in mat.nns_mat_type: 1447 | layout = layout.box() 1448 | title = layout.column() 1449 | title.box().label(text="NNS Material SRT") 1450 | layout.row(align=True).prop(mat, "nns_srt_scale") 1451 | layout.prop(mat, "nns_srt_rotate") 1452 | layout.row(align=True).prop(mat, "nns_srt_translate") 1453 | 1454 | 1455 | class NTR_PT_material_visual(bpy.types.Panel): 1456 | bl_label = "NNS Material visual options" 1457 | bl_idname = "MATERIAL_VISUAL_PT_nns" 1458 | bl_space_type = 'PROPERTIES' 1459 | bl_region_type = 'WINDOW' 1460 | bl_context = "material" 1461 | bl_options = {'HIDE_HEADER'} 1462 | 1463 | @classmethod 1464 | def poll(cls, context): 1465 | return context.material 1466 | 1467 | def draw(self, context): 1468 | layout = self.layout 1469 | mat = context.material 1470 | 1471 | layout = layout.box() 1472 | title = layout.column() 1473 | title.box().label(text="NNS Material Visual Options") 1474 | layout.prop(mat, "nns_hdr_add_self") 1475 | 1476 | 1477 | class SCENE_PT_NNS_Panel(bpy.types.Panel): 1478 | bl_label = "nns scene settings" 1479 | bl_space_type = 'VIEW_3D' 1480 | bl_region_type = 'UI' 1481 | bl_category = "NNS Scene" 1482 | 1483 | def draw(self, context): 1484 | layout = self.layout 1485 | layout.scale_x = 5.0 1486 | 1487 | # disclaimer 1488 | 1489 | layout.label(text="/!\\ These settings are only for preview purpose,\n they won't be exported") 1490 | 1491 | # Fog 1492 | try: 1493 | fog_group = bpy.data.node_groups.get("nns fog") 1494 | if fog_group is not None: 1495 | if "curve" not in fog_group.nodes.keys(): 1496 | generate_fog_group() 1497 | else: 1498 | generate_fog_group() 1499 | 1500 | curve_node = fog_group.nodes.get("curve") 1501 | except Exception: 1502 | raise NameError("Curve node doesn't exist, try creating a NNS material") 1503 | 1504 | box = layout.box() 1505 | box.label(text="Fog properties:") 1506 | box.label(text="Fog density curve:") 1507 | 1508 | try: 1509 | box.template_curve_mapping(curve_node, "mapping") 1510 | except Exception: 1511 | raise NameError("Curve node doesn't exist, try creating a NNS material") 1512 | 1513 | col = box.split(factor=0.5, align=True) 1514 | col1 = col.column(align=True) 1515 | col1.prop(context.scene, "Fog_enable", text="enable fog") 1516 | col1.prop(context.scene, "Fog_color", text="fog color") 1517 | col2 = col.column(align=True) 1518 | col2.prop(context.scene, "Fog_scale", text="scale") 1519 | col2.prop(context.scene, "Fog_offset", text="fog offset") 1520 | 1521 | # Light 0 1522 | box = layout.box() 1523 | box.label(text="Light 0 properties:") 1524 | col = box.split(factor=0.5, align=True) 1525 | col1 = col.column(align=True) 1526 | col1.prop(context.scene, "Light0_color", text="color") 1527 | col1.prop(context.scene, "Light0_specular", text="specular") 1528 | col2 = col.column(align=True) 1529 | col2.prop(context.scene, "Light0_vector", text="vector") 1530 | 1531 | # Light 1 1532 | box = layout.box() 1533 | box.label(text="Light 1 properties:") 1534 | col = box.split(factor=0.5, align=True) 1535 | col1 = col.column(align=True) 1536 | col1.prop(context.scene, "Light1_color", text="color") 1537 | col1.prop(context.scene, "Light1_specular", text="specular") 1538 | col2 = col.column(align=True) 1539 | col2.prop(context.scene, "Light1_vector", text="vector") 1540 | 1541 | # Light 2 1542 | box = layout.box() 1543 | box.label(text="Light 2 properties:") 1544 | col = box.split(factor=0.5, align=True) 1545 | col1 = col.column(align=True) 1546 | col1.prop(context.scene, "Light2_color", text="color") 1547 | col1.prop(context.scene, "Light2_specular", text="specular") 1548 | col2 = col.column(align=True) 1549 | col2.prop(context.scene, "Light2_vector", text="vector") 1550 | 1551 | # Light 3 1552 | box = layout.box() 1553 | box.label(text="Light 3 properties:") 1554 | col = box.split(factor=0.5, align=True) 1555 | col1 = col.column(align=True) 1556 | col1.prop(context.scene, "Light3_color", text="color") 1557 | col1.prop(context.scene, "Light3_specular", text="specular") 1558 | col2 = col.column(align=True) 1559 | col2.prop(context.scene, "Light3_vector", text="vector") 1560 | 1561 | 1562 | class NTR_PT_material(bpy.types.Panel): 1563 | bl_label = "NNS Material Options" 1564 | bl_idname = "MATERIAL_PT_nns" 1565 | bl_space_type = 'PROPERTIES' 1566 | bl_region_type = 'WINDOW' 1567 | bl_context = "material" 1568 | bl_options = {'HIDE_HEADER'} 1569 | 1570 | @classmethod 1571 | def poll(cls, context): 1572 | return context.material 1573 | 1574 | def draw(self, context): 1575 | layout = self.layout 1576 | mat = context.material 1577 | 1578 | layout.operator(CreateNNSMaterial.bl_idname) 1579 | 1580 | if mat is None: 1581 | pass 1582 | elif not (mat.use_nodes and mat.is_nns): 1583 | pass 1584 | else: 1585 | layout = layout.box() 1586 | title = layout.column() 1587 | title.box().label(text="NNS Material Options") 1588 | 1589 | layout.prop(mat, "nns_mat_type") 1590 | 1591 | if "vc" in mat.nns_mat_type: 1592 | layout.label( 1593 | text='Note: There must be a vertex color layer named ' 1594 | '"Col"') 1595 | 1596 | if "tx" in mat.nns_mat_type: 1597 | layout.template_ID(mat, "nns_image", open="image.open") 1598 | 1599 | if "df" in mat.nns_mat_type: 1600 | layout.row().prop(mat, "nns_diffuse") 1601 | 1602 | if "nr" in mat.nns_mat_type: 1603 | layout.row().prop(mat, "nns_ambient") 1604 | layout.row().prop(mat, "nns_specular") 1605 | layout.row().prop(mat, "nns_emission") 1606 | 1607 | layout.row().prop(mat, "nns_alpha", slider=True) 1608 | 1609 | if "nr" in mat.nns_mat_type: 1610 | row = layout.row(align=True) 1611 | row.prop(mat, "nns_light0", toggle=True) 1612 | row.prop(mat, "nns_light1", toggle=True) 1613 | row.prop(mat, "nns_light2", toggle=True) 1614 | row.prop(mat, "nns_light3", toggle=True) 1615 | # Lights settings 1616 | 1617 | layout.prop(mat, "nns_use_srst") 1618 | layout.prop(mat, "nns_fog") 1619 | layout.prop(mat, "nns_wireframe") 1620 | layout.prop(mat, "nns_depth_test") 1621 | layout.prop(mat, "nns_update_depth_buffer") 1622 | layout.prop(mat, "nns_render_1_pixel") 1623 | layout.prop(mat, "nns_far_clipping") 1624 | layout.prop(mat, "nns_polygonid") 1625 | layout.prop(mat, "nns_display_face") 1626 | layout.prop(mat, "nns_polygon_mode") 1627 | 1628 | if "tx" in mat.nns_mat_type: 1629 | layout.prop(mat, "nns_tex_tiling_u") 1630 | layout.prop(mat, "nns_tex_tiling_v") 1631 | layout.row(align=True).prop(mat, "nns_tex_scale") 1632 | layout.prop(mat, "nns_tex_rotate") 1633 | layout.row(align=True).prop(mat, "nns_tex_translate") 1634 | layout.prop(mat, "nns_tex_gen_mode") 1635 | 1636 | if mat.nns_tex_gen_mode == 'nrm' or mat.nns_tex_gen_mode == 'pos': 1637 | layout.prop(mat, "nns_tex_gen_st_src") 1638 | box = layout.box() 1639 | box.label(text="Texture effect matrix") 1640 | row = box.row(align=True) 1641 | row.prop(mat, "nns_tex_effect_mtx_0") 1642 | row = box.row(align=True) 1643 | row.prop(mat, "nns_tex_effect_mtx_1") 1644 | row = box.row(align=True) 1645 | row.prop(mat, "nns_tex_effect_mtx_2") 1646 | row = box.row(align=True) 1647 | row.prop(mat, "nns_tex_effect_mtx_3") 1648 | 1649 | 1650 | def material_register(): 1651 | bpy.utils.register_class(NTRTexReference) 1652 | bpy.utils.register_class(NewTexReference) 1653 | bpy.utils.register_class(DeleteTexReference) 1654 | bpy.types.Material.nns_texframe_reference = CollectionProperty( 1655 | type=NTRTexReference) 1656 | bpy.types.Material.nns_texframe_reference_index = IntProperty( 1657 | name="Active texture reference index", default=0, update=update_nodes_srt_hook) 1658 | 1659 | bpy.types.Material.nns_hdr_add_self = BoolProperty( 1660 | default=False, name="HDR shaders", update=update_nodes_mode) 1661 | bpy.types.Material.is_nns = BoolProperty(default=False) 1662 | mat_type_items = [ 1663 | ("df", "Solid color", '', 1), 1664 | ("df_nr", "Solid color + normals", '', 2), 1665 | ("vc", "Vertex colored", '', 3), 1666 | ("tx_df", "Textured + solid color", '', 4), 1667 | ("tx_vc", "Textured + vertex colors", '', 5), 1668 | ("tx_nr_df", "Textured + normals", '', 6) 1669 | ] 1670 | bpy.types.Material.nns_mat_type = EnumProperty( 1671 | name="Material type", items=mat_type_items, 1672 | update=update_nodes_mat_type) 1673 | bpy.types.Material.nns_image = PointerProperty( 1674 | name='Texture', type=Image, update=update_nodes_image) 1675 | bpy.types.Material.nns_diffuse = FloatVectorProperty( 1676 | default=(1, 1, 1), subtype='COLOR', min=0.0, max=1.0, name='Diffuse', 1677 | update=update_nodes_diffuse) 1678 | bpy.types.Material.nns_ambient = FloatVectorProperty( 1679 | default=(1, 1, 1), subtype='COLOR', min=0.0, max=1.0, name='Ambient', 1680 | update=update_nodes_ambient) 1681 | bpy.types.Material.nns_specular = FloatVectorProperty( 1682 | default=(0, 0, 0), subtype='COLOR', min=0.0, max=1.0, name='Specular', 1683 | update=update_nodes_specular) 1684 | bpy.types.Material.nns_emission = FloatVectorProperty( 1685 | default=(0, 0, 0), subtype='COLOR', min=0.0, max=1.0, name='Emission', 1686 | update=update_nodes_emission) 1687 | bpy.types.Material.nns_light0 = BoolProperty(name="Light0", default=False, 1688 | update=update_nodes_light0) 1689 | bpy.types.Material.nns_light1 = BoolProperty(name="Light1", default=False, 1690 | update=update_nodes_light1) 1691 | bpy.types.Material.nns_light2 = BoolProperty(name="Light2", default=False, 1692 | update=update_nodes_light2) 1693 | bpy.types.Material.nns_light3 = BoolProperty(name="Light3", default=False, 1694 | update=update_nodes_light3) 1695 | 1696 | # scene fog properties 1697 | 1698 | bpy.types.Scene.Fog_enable = BoolProperty(name="Fog_enable", default=False, update=update_fog_group_nodes) 1699 | bpy.types.Scene.Fog_color = bpy.types.Scene.Light0_color = FloatVectorProperty( 1700 | name="Fog_color", 1701 | subtype='COLOR', 1702 | default=(1.0, 1.0, 1.0), 1703 | min=0.0, max=1.0, 1704 | description="color picker", 1705 | update=update_fog_group_nodes 1706 | ) 1707 | bpy.types.Scene.Fog_scale = FloatProperty(name="Fog_scale", default=1000, min=0.01, update=update_fog_group_nodes) 1708 | bpy.types.Scene.Fog_offset = FloatProperty(name="Fog_offset", default=0, min=0, max=20, update=update_fog_group_nodes) 1709 | 1710 | # scene lights properties 1711 | 1712 | bpy.types.Scene.Light0_color = FloatVectorProperty( 1713 | name="Light 0 color", 1714 | subtype='COLOR', 1715 | default=(1.0, 1.0, 1.0), 1716 | min=0.0, max=1.0, 1717 | description="color picker", 1718 | update=update_light0 1719 | ) 1720 | 1721 | bpy.types.Scene.Light1_color = FloatVectorProperty( 1722 | name="Light 1 color", 1723 | subtype='COLOR', 1724 | default=(1.0, 1.0, 1.0), 1725 | min=0.0, max=1.0, 1726 | description="color picker", 1727 | update=update_light1 1728 | ) 1729 | 1730 | bpy.types.Scene.Light2_color = FloatVectorProperty( 1731 | name="Light 2 color", 1732 | subtype='COLOR', 1733 | default=(1.0, 0, 0), 1734 | min=0.0, max=1.0, 1735 | description="color picker", 1736 | update=update_light2 1737 | ) 1738 | 1739 | bpy.types.Scene.Light3_color = FloatVectorProperty( 1740 | name="Light 3 color", 1741 | subtype='COLOR', 1742 | default=(1.0, 1.0, 0), 1743 | min=0.0, max=1.0, 1744 | description="color picker", 1745 | update=update_light3 1746 | ) 1747 | 1748 | bpy.types.Scene.Light0_specular = FloatProperty( 1749 | name="Light 0 specular", 1750 | default=0.5, 1751 | min=0, 1752 | max=1, 1753 | update=update_light0 1754 | ) 1755 | 1756 | bpy.types.Scene.Light1_specular = FloatProperty( 1757 | name="Light 1 specular", 1758 | default=1, 1759 | min=0, 1760 | max=1, 1761 | update=update_light1 1762 | ) 1763 | 1764 | bpy.types.Scene.Light2_specular = FloatProperty( 1765 | name="Light 2 specular", 1766 | default=0.5, 1767 | min=0, 1768 | max=1, 1769 | update=update_light2 1770 | ) 1771 | 1772 | bpy.types.Scene.Light3_specular = FloatProperty( 1773 | name="Light 3 specular", 1774 | default=0, 1775 | min=0, 1776 | max=1, 1777 | update=update_light3 1778 | ) 1779 | 1780 | bpy.types.Scene.Light0_vector = FloatVectorProperty( 1781 | name="Light 0 vector", 1782 | subtype='XYZ', 1783 | default=(0, 0, -1.0), 1784 | min=-1.0, max=1.0, 1785 | description="color picker", 1786 | update=update_light0 1787 | ) 1788 | 1789 | bpy.types.Scene.Light1_vector = FloatVectorProperty( 1790 | name="Light 1 vector", 1791 | subtype='XYZ', 1792 | default=(0, 0.5, -0.5), 1793 | min=-1.0, max=1.0, 1794 | description="color picker", 1795 | update=update_light1 1796 | ) 1797 | 1798 | bpy.types.Scene.Light2_vector = FloatVectorProperty( 1799 | name="Light 2 vector", 1800 | subtype='XYZ', 1801 | default=(0, 0, -1.0), 1802 | min=-1.0, max=1.0, 1803 | description="color picker", 1804 | update=update_light2 1805 | ) 1806 | 1807 | bpy.types.Scene.Light3_vector = FloatVectorProperty( 1808 | name="Light 3 vector", 1809 | subtype='XYZ', 1810 | default=(0, 0, 1.0), 1811 | min=-1.0, max=1.0, 1812 | description="color picker", 1813 | update=update_light3 1814 | ) 1815 | 1816 | bpy.types.Material.nns_use_srst = BoolProperty( 1817 | name="Use Specular Reflection Table", default=False) 1818 | bpy.types.Material.nns_fog = BoolProperty( 1819 | name="Fog", default=False, update=update_material_fog) 1820 | bpy.types.Material.nns_wireframe = BoolProperty( 1821 | name="Wireframe", default=False) 1822 | bpy.types.Material.nns_depth_test = BoolProperty( 1823 | name="Depth test for decal polygon", default=False) 1824 | bpy.types.Material.nns_update_depth_buffer = BoolProperty( 1825 | name="Translucent polygons update depth buffer", default=False) 1826 | bpy.types.Material.nns_render_1_pixel = BoolProperty( 1827 | name="Render 1-pixel polygon", default=False) 1828 | bpy.types.Material.nns_far_clipping = BoolProperty( 1829 | name="Far clipping", default=False) 1830 | bpy.types.Material.nns_polygonid = IntProperty( 1831 | name="Polygon ID", default=0) 1832 | display_face_items = [ 1833 | ("front", "Front face", '', 1), 1834 | ("back", "Back face", '', 2), 1835 | ("both", "Both faces", '', 3) 1836 | ] 1837 | bpy.types.Material.nns_display_face = EnumProperty( 1838 | name="Display face", items=display_face_items, 1839 | update=update_nodes_face) 1840 | polygon_mode_items = [ 1841 | ("modulate", "Modulate", '', 1), 1842 | ("decal", "Decal", '', 2), 1843 | ("toon_highlight", "Toon/highlight", '', 3), 1844 | ("shadow", "Shadow", '', 4) 1845 | ] 1846 | bpy.types.Material.nns_polygon_mode = EnumProperty( 1847 | name="Polygon mode", items=polygon_mode_items, 1848 | update=update_nodes_mode) 1849 | tex_gen_mode_items = [ 1850 | ("none", "None", '', 1), 1851 | ("tex", "Texcoord", '', 2), 1852 | ("nrm", "Normal", '', 3), 1853 | ("pos", "Vertex", '', 4) 1854 | ] 1855 | bpy.types.Material.nns_tex_gen_mode = EnumProperty( 1856 | name="Tex gen mode", items=tex_gen_mode_items, 1857 | update=update_nodes_tex_gen) 1858 | tex_gen_st_src_items = [ 1859 | ("polygon", "Polygon", '', 1), 1860 | ("material", "Material", '', 2), 1861 | ] 1862 | bpy.types.Material.nns_tex_gen_st_src = EnumProperty( 1863 | name="Tex gen ST source", items=tex_gen_st_src_items) 1864 | bpy.types.Material.nns_tex_effect_mtx_0 = FloatVectorProperty( 1865 | size=2, name='', default=(1, 0)) 1866 | bpy.types.Material.nns_tex_effect_mtx_1 = FloatVectorProperty( 1867 | size=2, name='', default=(0, 1)) 1868 | bpy.types.Material.nns_tex_effect_mtx_2 = FloatVectorProperty( 1869 | size=2, name='') 1870 | bpy.types.Material.nns_tex_effect_mtx_3 = FloatVectorProperty( 1871 | size=2, name='') 1872 | tex_tiling_items = [ 1873 | ("repeat", "Repeat", '', 1), 1874 | ("flip", "Flip", '', 2), 1875 | ("clamp", "Clamp", '', 3) 1876 | ] 1877 | bpy.types.Material.nns_tex_tiling_u = EnumProperty( 1878 | name="Tex tiling u", items=tex_tiling_items, update=update_nodes_mode) 1879 | bpy.types.Material.nns_tex_tiling_v = EnumProperty( 1880 | name="Tex tiling v", items=tex_tiling_items, update=update_nodes_mode) 1881 | bpy.types.Material.nns_alpha = IntProperty( 1882 | name="Alpha", min=0, max=31, default=31, update=update_nodes_alpha) 1883 | bpy.types.Material.nns_tex_scale = FloatVectorProperty( 1884 | size=2, name="Texture scale", default=(1, 1)) 1885 | bpy.types.Material.nns_tex_rotate = FloatProperty(name="Texture rotation") 1886 | bpy.types.Material.nns_tex_translate = FloatVectorProperty( 1887 | size=2, name="Texture translation") 1888 | 1889 | bpy.types.Material.nns_srt_translate = FloatVectorProperty( 1890 | size=2, name="Translate", update=update_nodes_srt_hook) 1891 | bpy.types.Material.nns_srt_scale = FloatVectorProperty( 1892 | size=2, name="Scale", update=update_nodes_srt_hook, default=(1, 1)) 1893 | bpy.types.Material.nns_srt_rotate = FloatProperty( 1894 | name="Rotate", update=update_nodes_srt_hook, subtype='ANGLE') 1895 | 1896 | print("Register frame handler") 1897 | bpy.app.handlers.frame_change_pre.append(frame_change_handler) 1898 | 1899 | bpy.utils.register_class(CreateNNSMaterial) 1900 | bpy.utils.register_class(NTR_PT_material_visual) 1901 | bpy.utils.register_class(NTR_PT_material) 1902 | bpy.utils.register_class(NTR_PT_material_keyframe) 1903 | bpy.utils.register_class(NTR_UL_texframe) 1904 | bpy.utils.register_class(NTR_PT_material_texframe) 1905 | bpy.utils.register_class(SCENE_PT_NNS_Panel) 1906 | 1907 | 1908 | def material_unregister(): 1909 | bpy.utils.unregister_class(NTRTexReference) 1910 | bpy.utils.unregister_class(NewTexReference) 1911 | bpy.utils.unregister_class(DeleteTexReference) 1912 | 1913 | bpy.utils.unregister_class(CreateNNSMaterial) 1914 | bpy.utils.unregister_class(NTR_PT_material_visual) 1915 | bpy.utils.unregister_class(NTR_PT_material) 1916 | bpy.utils.unregister_class(NTR_PT_material_keyframe) 1917 | bpy.utils.unregister_class(NTR_UL_texframe) 1918 | bpy.utils.unregister_class(NTR_PT_material_texframe) 1919 | bpy.utils.unregister_class(SCENE_PT_NNS_Panel) 1920 | -------------------------------------------------------------------------------- /nns_model.py: -------------------------------------------------------------------------------- 1 | import os 2 | import math 3 | import decimal 4 | from mathutils import Matrix 5 | import bpy 6 | from bpy_extras import node_shader_utils 7 | from bpy_extras.io_utils import axis_conversion 8 | from .util import * 9 | from .primitive import * 10 | from . import local_logger as logger 11 | from . import nns_tga 12 | 13 | 14 | class NitroModelInfo(): 15 | def __init__(self): 16 | self.pos_scale = 0 17 | self.max_coord = 0 18 | 19 | def add(self, vertex): 20 | max_coord = max(abs(vertex.x), abs(vertex.y), abs(vertex.z)) 21 | if max_coord > self.max_coord: 22 | self.max_coord = max_coord 23 | 24 | def calculate(self): 25 | self.pos_scale = calculate_pos_scale(self.max_coord) 26 | 27 | 28 | class NitroModelBoxTest(): 29 | def __init__(self): 30 | box = get_all_max_min() 31 | self.xyz = box['min'] 32 | self.whd = box['max'] - box['min'] 33 | 34 | max_whd = abs(max(self.whd.x, self.whd.y, self.whd.z)) 35 | min_xyz = abs(min(self.xyz.x, self.xyz.y, self.xyz.z)) 36 | max_coord = max(max_whd, min_xyz) 37 | self.pos_scale = calculate_pos_scale(max_coord) 38 | 39 | 40 | class NitroModelTexture(): 41 | def __init__(self, model, path, index): 42 | self.path = path 43 | self.index = index 44 | self.name = str(os.path.splitext(os.path.basename(path))[0])[0:15] 45 | 46 | # Load Nitro TGA Data from path 47 | tga = nns_tga.read_nitro_tga(path) 48 | 49 | # Set TexImage properties 50 | self.format = tga['nitro_data']['tex_format'] 51 | self.width = tga['header']['image_width'] 52 | self.height = tga['header']['image_heigth'] 53 | self.original_width = tga['header']['image_width'] 54 | self.original_height = tga['header']['image_heigth'] 55 | 56 | # Color 0 Mode 57 | transp = tga['nitro_data']['color_0_transp'] 58 | if self.format in ('palette4', 'palette16', 'palette256'): 59 | self.color0_mode = 'transparency' if transp else 'color' 60 | 61 | # Get Bitmap Data 62 | self.bitmap_data = nns_tga.get_bitmap_data(tga) 63 | self.bitmap_size = nns_tga.get_bitmap_size(tga) 64 | 65 | # Get Tex4x4 Palette Index Data 66 | if self.format == 'tex4x4': 67 | self.tex4x4_palette_idx_data = nns_tga.get_pltt_idx_data(tga) 68 | self.tex4x4_palette_idx_size = nns_tga.get_pltt_idx_size(tga) 69 | 70 | # Store the palette index that model.add_palette returns in here or 71 | # leave it -1. 72 | self.palette_idx = -1 73 | 74 | # Get Palette Data 75 | if self.format != 'direct': 76 | self.palette_name = tga['nitro_data']['palette_name'][0:15] 77 | plt_data = nns_tga.get_palette_data(tga) 78 | plt_size = nns_tga.get_palette_size(tga) 79 | palette = model.add_palette(self.palette_name, plt_data, plt_size) 80 | self.palette_idx = palette.index 81 | 82 | 83 | class NitroModelPalette(): 84 | def __init__(self, name, data, size, index): 85 | self.index = index 86 | self.name = name 87 | self.data = data 88 | self.size = size 89 | 90 | 91 | class NitroModelMaterial(): 92 | def __init__(self, model, blender_index, index): 93 | self.blender_index = blender_index 94 | self.index = index 95 | material = bpy.data.materials[blender_index] 96 | self.name = material.name 97 | 98 | self.type = material.nns_mat_type 99 | 100 | self.light0 = 'on' if material.nns_light0 else 'off' 101 | self.light1 = 'on' if material.nns_light1 else 'off' 102 | self.light2 = 'on' if material.nns_light2 else 'off' 103 | self.light3 = 'on' if material.nns_light3 else 'off' 104 | self.shininess_table_flag = 'on' if material.nns_use_srst else 'off' 105 | self.fog_flag = 'on' if material.nns_fog else 'off' 106 | self.wire_mode = 'on' if material.nns_wireframe else 'off' 107 | self.depth_test_decal = 'on' if material.nns_depth_test else 'off' 108 | self.translucent_update_depth = ('on' 109 | if material.nns_update_depth_buffer 110 | else 'off') 111 | self.render_1_pixel = 'on' if material.nns_render_1_pixel else 'off' 112 | self.far_clipping = 'on' if material.nns_far_clipping else 'off' 113 | self.polygon_id = material.nns_polygonid 114 | self.face = material.nns_display_face 115 | self.polygon_mode = material.nns_polygon_mode 116 | self.tex_gen_mode = material.nns_tex_gen_mode 117 | self.tex_gen_st_src = material.nns_tex_gen_st_src 118 | self.tex_tiling_u = material.nns_tex_tiling_u 119 | self.tex_tiling_v = material.nns_tex_tiling_v 120 | row0 = material.nns_tex_effect_mtx_0 121 | row1 = material.nns_tex_effect_mtx_1 122 | row2 = material.nns_tex_effect_mtx_2 123 | row3 = material.nns_tex_effect_mtx_3 124 | matrix = f'{row0[0]} {row0[1]} 0.0 0.0 ' \ 125 | f'{row1[0]} {row1[1]} 0.0 0.0 ' \ 126 | f'{row2[0]} {row2[1]} 1.0 0.0 ' \ 127 | f'{row3[0]} {row3[1]} 0.0 1.0' 128 | self.tex_effect_mtx = matrix 129 | self.tex_scale = f'{material.nns_tex_scale[0]} ' \ 130 | f'{material.nns_tex_scale[1]}' 131 | self.tex_rotate = str(material.nns_tex_rotate) 132 | self.tex_translate = f'{material.nns_tex_translate[0]} ' \ 133 | f'{material.nns_tex_translate[1]}' 134 | 135 | self.image_idx = -1 136 | self.palette_idx = -1 137 | 138 | if material.is_nns: 139 | self.alpha = material.nns_alpha 140 | self.diffuse = ' '.join( 141 | [str(int(round(lin2s(x) * 31))) 142 | for x in material.nns_diffuse]) 143 | self.specular = ' '.join( 144 | [str(int(round(lin2s(x) * 31))) 145 | for x in material.nns_specular]) 146 | self.ambient = ' '.join( 147 | [str(int(round(lin2s(x) * 31))) 148 | for x in material.nns_ambient]) 149 | self.emission = ' '.join( 150 | [str(int(round(lin2s(x) * 31))) 151 | for x in material.nns_emission]) 152 | if material.nns_image is not None \ 153 | and "tx" in material.nns_mat_type: 154 | filepath = material.nns_image.filepath 155 | path = os.path.realpath(bpy.path.abspath(filepath)) 156 | _, extension = os.path.splitext(path) 157 | if extension == '.tga': 158 | texture = model.find_texture(path) 159 | self.image_idx = texture.index 160 | self.palette_idx = texture.palette_idx 161 | 162 | for i in material.nns_texframe_reference: 163 | 164 | fPath=os.path.realpath(bpy.path.abspath(i.image.filepath)) 165 | model.find_texture(fPath) 166 | 167 | else: 168 | # For now let's use PrincipledBSDF to get the color and image. 169 | wrap = node_shader_utils.PrincipledBSDFWrapper(material) 170 | self.alpha = int(wrap.alpha * 31) 171 | self.diffuse = ' '.join([ 172 | str(int(round(lin2s(wrap.base_color[0]) * 31))), 173 | str(int(round(lin2s(wrap.base_color[1]) * 31))), 174 | str(int(round(lin2s(wrap.base_color[2]) * 31))) 175 | ]) 176 | self.specular = ' '.join( 177 | [str(int(round(wrap.specular * 31))) for _ in range(3)]) 178 | self.ambient = '31 31 31' 179 | self.emission = '0 0 0' 180 | 181 | tex_wrap = getattr(wrap, 'base_color_texture', None) 182 | if tex_wrap is not None and tex_wrap.image is not None: 183 | path = os.path.realpath(bpy.path.abspath( 184 | tex_wrap.image.filepath, library=tex_wrap.image.library)) 185 | _, extension = os.path.splitext(path) 186 | if extension == '.tga': 187 | texture = model.find_texture(path) 188 | self.image_idx = texture.index 189 | self.palette_idx = texture.palette_idx 190 | 191 | 192 | class NitroModelMatrix(): 193 | def __init__(self, index, node_idx, transform): 194 | self.index = index 195 | self.weight = 1 196 | self.node_idx = node_idx 197 | self.transform = transform 198 | 199 | 200 | class NitroModelCommand(): 201 | def __init__(self, type_, tag, data): 202 | self.type = type_ 203 | self.tag = tag 204 | self.data = data 205 | 206 | 207 | class NitroModelPrimitive(): 208 | def __init__(self, type_): 209 | self.type = type_ 210 | self.vertex_size = 0 211 | self.triangle_size = 0 212 | self.quad_size = 0 213 | self.commands = [] 214 | self._previous_vecfx32 = None 215 | self._previous_mtx = -1 216 | self._previous_nrm = None 217 | self._previous_tex = None 218 | self._previous_clr = None 219 | # for after sort 220 | # quad_strip=0 triangle_strip=1 quads=2 triangles=3 221 | self.sort_key = 0 222 | 223 | def is_empty(self): 224 | return self._previous_vecfx32 is None 225 | 226 | def add_command(self, type_: str, tag: str, data: str): 227 | self.commands.append(NitroModelCommand(type_, tag, data)) 228 | 229 | def insert_mtx(self, position, idx: int): 230 | self.commands.insert( 231 | position, NitroModelCommand('mtx', 'idx', str(idx))) 232 | 233 | def add_mtx(self, idx: int): 234 | self.add_command('mtx', 'idx', str(idx)) 235 | 236 | def add_pos_xyz(self, vec: Vector): 237 | floats = [str(round(v, 6)) for v in vec] 238 | self.add_command('pos_xyz', 'xyz', ' '.join(floats)) 239 | self.vertex_size += 1 240 | 241 | def add_pos_s(self, vec: Vector): 242 | floats = [str(round(v, 6)) for v in vec] 243 | self.add_command('pos_s', 'xyz', ' '.join(floats)) 244 | self.vertex_size += 1 245 | 246 | def add_pos_diff(self, vec: Vector): 247 | floats = [str(round(v, 6)) for v in vec] 248 | self.add_command('pos_diff', 'xyz', ' '.join(floats)) 249 | self.vertex_size += 1 250 | 251 | def add_pos_yz(self, vec: Vector): 252 | floats = [str(round(v, 6)) for v in [vec.y, vec.z]] 253 | self.add_command('pos_yz', 'yz', ' '.join(floats)) 254 | self.vertex_size += 1 255 | 256 | def add_pos_xz(self, vec: Vector): 257 | floats = [str(round(v, 6)) for v in [vec.x, vec.z]] 258 | self.add_command('pos_xz', 'xz', ' '.join(floats)) 259 | self.vertex_size += 1 260 | 261 | def add_pos_xy(self, vec: Vector): 262 | floats = [str(round(v, 6)) for v in [vec.x, vec.y]] 263 | self.add_command('pos_xy', 'xy', ' '.join(floats)) 264 | self.vertex_size += 1 265 | 266 | 267 | class NitroModelMtxPrim(): 268 | def __init__(self, index, parent_polygon): 269 | self.index = index 270 | self.mtx_list = [] 271 | self.primitives = [] 272 | self.parent_polygon = parent_polygon 273 | 274 | def add_matrix_reference(self, index): 275 | if index not in self.mtx_list: 276 | self.mtx_list.append(index) 277 | return self.mtx_list.index(index) 278 | 279 | def add_primitive(self, model, obj, prim: Primitive, material): 280 | if prim.type == 'triangles': 281 | primitive = self.get_primitive('triangles') 282 | primitive.sort_key = 3 283 | primitive.triangle_size += 1 284 | elif prim.type == 'quads': 285 | primitive = self.get_primitive('quads') 286 | primitive.sort_key = 2 287 | primitive.quad_size += 1 288 | elif prim.type == 'triangle_strip': 289 | primitive = self.get_primitive('triangle_strip') 290 | primitive.sort_key = 1 291 | primitive.triangle_size += prim.vertex_count - 1 292 | elif prim.type == 'quad_strip': 293 | primitive = self.get_primitive('quad_strip') 294 | primitive.sort_key = 0 295 | primitive.quad_size += int((prim.vertex_count - 2) / 2) 296 | 297 | if len(obj.data.vertex_colors) > 0 and "vc" in material.type: 298 | self.parent_polygon.use_clr = True 299 | 300 | if material.image_idx != -1 and "tx" in material.type \ 301 | and material.tex_gen_mode != "nrm" \ 302 | and material.tex_gen_st_src != "material": 303 | self.parent_polygon.use_tex = True 304 | 305 | if ((material.light0 == 'on' or 306 | material.light1 == 'on' or 307 | material.light2 == 'on' or 308 | material.light3 == 'on') and "nr" in material.type) or \ 309 | material.tex_gen_mode == "nrm": 310 | self.parent_polygon.use_nrm = True 311 | 312 | for idx in range(len(prim.positions)): 313 | # Find transform. 314 | group = prim.groups[idx] 315 | matrix = None 316 | if (model.settings['imd_compress_nodes'] 317 | in ['unite', 'unite_combine']): 318 | matrix = model.find_matrix_by_node_name('root_scene') 319 | group = -1 320 | elif group != -1: 321 | name = obj.vertex_groups[group].name 322 | matrix = model.find_matrix_by_node_name(name) 323 | else: 324 | matrix = model.find_matrix_by_node_name(obj.name) 325 | 326 | node = model.find_node_by_index(matrix.node_idx) 327 | node.draw_mtx = True 328 | 329 | # Add mtx command. 330 | if matrix is not None and primitive._previous_mtx != matrix.index: 331 | index = self.add_matrix_reference(matrix.index) 332 | primitive.add_mtx(index) 333 | primitive._previous_mtx = matrix.index 334 | primitive._previous_nrm = None 335 | 336 | # Texture coordinate. 337 | if (self.parent_polygon.use_tex 338 | and primitive._previous_tex != prim.texcoords[idx]): 339 | primitive._previous_tex = prim.texcoords[idx] 340 | tex = model.textures[material.image_idx] 341 | uv = prim.texcoords[idx].to_vector() 342 | s = uv.x * tex.width 343 | t = uv.y * -tex.height + tex.height 344 | primitive.add_command('tex', 'st', f'{s} {t}') 345 | 346 | # Color 347 | if (self.parent_polygon.use_clr 348 | and primitive._previous_clr != prim.colors[idx]): 349 | primitive._previous_clr = prim.colors[idx] 350 | r, g, b = prim.colors[idx] 351 | primitive.add_command('clr', 'rgb', f'{r} {g} {b}') 352 | 353 | # Normal 354 | if (self.parent_polygon.use_nrm 355 | and primitive._previous_nrm != prim.normals[idx]): 356 | primitive._previous_nrm = prim.normals[idx] 357 | normal = prim.normals[idx].to_vector() 358 | primitive.add_command('nrm', 'xyz', 359 | f'{normal.x} {normal.y} {normal.z}') 360 | 361 | # Recalculate vertex. 362 | scaled_vecfx32 = prim.positions[idx] >> model.info.pos_scale 363 | scaled_vec = scaled_vecfx32.to_vector() 364 | 365 | # Calculate difference from previous vertex. 366 | if not primitive.is_empty(): 367 | diff_vecfx32 = scaled_vecfx32 - primitive._previous_vecfx32 368 | diff_vec = diff_vecfx32.to_vector() 369 | 370 | # PosYZ 371 | if not primitive.is_empty() and diff_vecfx32.x == 0: 372 | primitive.add_pos_yz(scaled_vec) 373 | # PosXZ 374 | elif not primitive.is_empty() and diff_vecfx32.y == 0: 375 | primitive.add_pos_xz(scaled_vec) 376 | # PosXY 377 | elif not primitive.is_empty() and diff_vecfx32.z == 0: 378 | primitive.add_pos_xy(scaled_vec) 379 | # PosDiff 380 | elif not primitive.is_empty() and is_pos_diff(diff_vecfx32): 381 | primitive.add_pos_diff(diff_vec) 382 | # PosShort 383 | elif is_pos_s(scaled_vecfx32): 384 | primitive.add_pos_s(scaled_vec) 385 | # PosXYZ 386 | else: 387 | primitive.add_pos_xyz(scaled_vec) 388 | 389 | primitive._previous_vecfx32 = scaled_vecfx32 390 | 391 | def get_primitive(self, type_): 392 | if type_ != 'quad_strip' and type_ != 'triangle_strip': 393 | for primitive in self.primitives: 394 | if primitive.type == type_: 395 | return primitive 396 | self.primitives.append(NitroModelPrimitive(type_)) 397 | return self.primitives[-1] 398 | 399 | def set_initial_mtx(self): 400 | self.primitives[0].insert_mtx(0, 0) 401 | 402 | def optimize(self): 403 | previous_mtx = None 404 | for primitive in self.primitives: 405 | for command in primitive.commands: 406 | if command.type != 'mtx': 407 | continue 408 | if previous_mtx == command.data: 409 | primitive.commands.remove(command) 410 | else: 411 | previous_mtx = command.data 412 | 413 | 414 | class NitroModelPolygon(): 415 | def __init__(self, index, name): 416 | self.index = index 417 | self.name = name 418 | self.use_nrm = False 419 | self.use_clr = False 420 | self.use_tex = False 421 | self.mtx_prims = [] 422 | self.vertex_size = 0 423 | self.polygon_size = 0 424 | self.triangle_size = 0 425 | self.quad_size = 0 426 | 427 | def find_mtx_prim(self, index): 428 | for prim in self.mtx_prims: 429 | if prim.index == index: 430 | return prim 431 | index = len(self.mtx_prims) 432 | self.mtx_prims.append(NitroModelMtxPrim(index, self)) 433 | return self.mtx_prims[-1] 434 | 435 | def collect_statistics(self): 436 | for mtx_prim in self.mtx_prims: 437 | for primitive in mtx_prim.primitives: 438 | self.vertex_size += primitive.vertex_size 439 | size = primitive.quad_size + primitive.triangle_size 440 | self.polygon_size += size 441 | self.triangle_size += primitive.triangle_size 442 | self.quad_size += primitive.quad_size 443 | 444 | def optimize(self): 445 | for mtx_prim in self.mtx_prims: 446 | mtx_prim.optimize() 447 | 448 | 449 | class NitroModelDisplay(): 450 | def __init__(self, index, material, polygon): 451 | self.index = index 452 | self.material = material 453 | self.polygon = polygon 454 | self.priority = 0 455 | 456 | 457 | class NitroModelNode(): 458 | def __init__(self, index, name): 459 | self.index = index 460 | self.name = name 461 | self.kind = 'null' 462 | self.parent = -1 463 | self.child = -1 464 | self.brother_next = -1 465 | self.brother_prev = -1 466 | self.draw_mtx = False 467 | self.billboard = 'off' 468 | self.scale = (1, 1, 1) 469 | self.rotate = (0, 0, 0) 470 | self.translate = (0, 0, 0) 471 | self.mtx = None 472 | self.visibility = True 473 | self.displays = [] 474 | self.vertex_size = 0 475 | self.polygon_size = 0 476 | self.triangle_size = 0 477 | self.quad_size = 0 478 | 479 | def set_scale_rot_trans(self, mag): 480 | euler = self.mtx.to_euler('XYZ') 481 | self.rotate = [decimal.Decimal(math.degrees(e)) for e in euler] 482 | self.translate = self.mtx.to_translation() * mag 483 | self.scale = self.mtx.to_scale() 484 | 485 | def collect_statistics(self, model): 486 | for display in self.displays: 487 | polygon = model.polygons[display.polygon] 488 | self.vertex_size += polygon.vertex_size 489 | self.polygon_size += polygon.polygon_size 490 | self.triangle_size += polygon.triangle_size 491 | self.quad_size += polygon.quad_size 492 | 493 | def find_display(self, material_index, polygon_index): 494 | for display in self.displays: 495 | if (display.material == material_index 496 | and display.polygon == polygon_index): 497 | return display 498 | index = len(self.displays) 499 | self.displays.append(NitroModelDisplay( 500 | index, material_index, polygon_index)) 501 | return self.displays[-1] 502 | 503 | 504 | class NitroModelOutputInfo(): 505 | def __init__(self): 506 | self.vertex_size = 0 507 | self.polygon_size = 0 508 | self.triangle_size = 0 509 | self.quad_size = 0 510 | 511 | def collect(self, model): 512 | for polygon in model.polygons: 513 | self.vertex_size += polygon.vertex_size 514 | size = polygon.quad_size + polygon.triangle_size 515 | self.polygon_size += size 516 | self.triangle_size += polygon.triangle_size 517 | self.quad_size += polygon.quad_size 518 | 519 | 520 | class NitroModel(): 521 | def __init__(self, settings): 522 | self.info = NitroModelInfo() 523 | self.box_test = NitroModelBoxTest() 524 | self.textures = [] 525 | self.palettes = [] 526 | self.materials = [] 527 | self.matrices = [] 528 | self.polygons = [] 529 | self.nodes = [] 530 | self.output_info = NitroModelOutputInfo() 531 | self.settings = settings 532 | # Array with primitives and their objects. 533 | self.primitives = [] 534 | 535 | def collect(self): 536 | if self.settings['imd_compress_nodes'] in ['none', 'cull', 'merge']: 537 | self.collect_none() 538 | elif self.settings['imd_compress_nodes'] == 'unite': 539 | self.collect_unite() 540 | elif self.settings['imd_compress_nodes'] == 'unite_combine': 541 | self.collect_unite_combine() 542 | 543 | # Sort and collect statistics. 544 | for polygon in self.polygons: 545 | polygon.collect_statistics() 546 | for mtx_prim in polygon.mtx_prims: 547 | mtx_prim.primitives.sort(key=lambda x: x.sort_key) 548 | for node in self.nodes: 549 | node.collect_statistics(self) 550 | 551 | # Optimise polygons. 552 | for polygon in self.polygons: 553 | polygon.optimize() 554 | 555 | self.output_info.collect(self) 556 | 557 | def collect_none(self): 558 | root = self.find_node('root_scene') 559 | root.rotate = (-90, 0, 0) 560 | root_objects = [] 561 | for obj in bpy.context.view_layer.objects: 562 | if obj.parent: 563 | continue 564 | if obj.type in ['EMPTY', 'ARMATURE', 'MESH']: 565 | root_objects.append(obj) 566 | children = self.process_children(root, root_objects) 567 | root.child = children[0].index 568 | self.apply_transformations() 569 | self.info.calculate() 570 | for item in self.primitives: 571 | self.compile_primitives( 572 | item['primitives'], 573 | item['obj'], 574 | item['node'], 575 | ) 576 | 577 | if self.settings['imd_compress_nodes'] in ['cull', 'merge']: 578 | self.cull_nodes() 579 | 580 | def cull_nodes(self): 581 | root = self.find_node('root_scene') 582 | while True: 583 | node = self.get_childless_node() 584 | if node is not None: 585 | root.displays = node.displays 586 | self.remove_node(node) 587 | else: 588 | break 589 | child = self.find_node_by_index(root.child) 590 | if child.brother_next == -1: 591 | mtx = Matrix.Rotation(math.radians(-90), 4, 'X') 592 | child.mtx = mtx @ child.mtx 593 | child.set_scale_rot_trans(self.settings['imd_magnification']) 594 | child.displays = root.displays 595 | child.parent = -1 596 | self.nodes.remove(root) 597 | idx = 0 598 | for node in self.nodes: 599 | self.node_replace_index(node, idx) 600 | idx += 1 601 | 602 | def collect_unite(self): 603 | root = self.find_node('root_scene') 604 | for obj in bpy.context.view_layer.objects: 605 | if obj.type != 'MESH': 606 | continue 607 | self.process_mesh(root, obj) 608 | self.apply_transformations() 609 | self.info.calculate() 610 | for item in self.primitives: 611 | self.compile_primitives( 612 | item['primitives'], 613 | item['obj'], 614 | item['node'], 615 | ) 616 | 617 | def collect_unite_combine(self): 618 | root = self.find_node('root_scene') 619 | for obj in bpy.context.view_layer.objects: 620 | if obj.type != 'MESH': 621 | continue 622 | self.process_mesh(root, obj) 623 | self.apply_transformations() 624 | self.info.calculate() 625 | for item in self.primitives: 626 | self.compile_primitives_combined( 627 | item['primitives'], 628 | item['obj'], 629 | item['node'], 630 | ) 631 | 632 | def compile_primitives_combined(self, primitives, obj, node): 633 | poly_mats = [] 634 | for primitive in primitives: 635 | material = self.find_material(primitive.material_index) 636 | polygon = self.find_polygon('polygon' + str(material.index)) 637 | poly_mats.append((polygon, material)) 638 | mtx_prim = polygon.find_mtx_prim(0) 639 | mtx_prim.add_primitive(self, obj, primitive, material) 640 | for polygon, material in poly_mats: 641 | display = node.find_display(material.index, polygon.index) 642 | display.polygon = polygon.index 643 | 644 | def compile_primitives(self, primitives, obj, node): 645 | # A list of polygons and materials. 646 | poly_mats = [] 647 | # Make materials and polygons and add the primitives to their 648 | # respective mtx_prim elements. 649 | for primitive in primitives: 650 | material = self.find_material(primitive.material_index) 651 | polygon_name = obj.name + '_' + str(material.index) 652 | polygon = self.find_polygon(polygon_name) 653 | poly_mats.append((polygon, material)) 654 | mtx_prim = polygon.find_mtx_prim(0) 655 | logger.log(f"Add primitive. {primitive.type}") 656 | mtx_prim.add_primitive(self, obj, primitive, material) 657 | # Hook up each polygon to the proper display depending on 658 | # material index. 659 | for polygon, material in poly_mats: 660 | display = node.find_display(material.index, polygon.index) 661 | display.polygon = polygon.index 662 | 663 | def apply_transformations(self): 664 | for item in self.primitives: 665 | obj = item['obj'] 666 | for primitive in item['primitives']: 667 | for idx in range(len(primitive.positions)): 668 | vertex = primitive.positions[idx].to_vector() 669 | if self.settings['imd_compress_nodes'] in ['unite', 'unite_combine']: 670 | transform = axis_conversion( 671 | to_forward='-Z', to_up='Y').to_4x4() 672 | transform = transform @ obj.matrix_world 673 | vertex = transform @ vertex 674 | else: 675 | matrix = None 676 | group = primitive.groups[idx] 677 | if group != -1: 678 | name = obj.vertex_groups[group].name 679 | matrix = self.find_matrix_by_node_name(name) 680 | if matrix: 681 | vertex = matrix.transform.inverted() @ vertex 682 | vertex = vertex * self.settings['imd_magnification'] 683 | self.info.add(vertex) 684 | vecfx32_vertex = VecFx32().from_vector(vertex) 685 | primitive.positions[idx] = vecfx32_vertex 686 | for idx in range(len(primitive.normals)): 687 | if self.settings['imd_compress_nodes'] in ['unite', 'unite_combine']: 688 | normal = primitive.normals[idx].to_vector() 689 | transform = axis_conversion( 690 | to_forward='-Z', to_up='Y').to_4x4() 691 | transform = transform @ obj.matrix_world 692 | quat = transform.to_quaternion() 693 | normal = quat @ normal 694 | primitive.normals[idx] = vector_to_vecfx10(normal) 695 | else: 696 | group = primitive.groups[idx] 697 | if group != -1: 698 | name = obj.vertex_groups[group].name 699 | matrix = self.find_matrix_by_node_name(name) 700 | normal = primitive.normals[idx].to_vector() 701 | quat = matrix.transform.inverted().to_quaternion() 702 | normal = quat @ normal 703 | primitive.normals[idx] = vector_to_vecfx10(normal) 704 | 705 | def process_children(self, parent, objs): 706 | """ 707 | Recursively go through every child of every object. 708 | This will make a node for every object it will find. 709 | """ 710 | brothers = [] 711 | 712 | for obj in objs: 713 | if obj.type not in ['EMPTY', 'ARMATURE', 'MESH']: 714 | continue 715 | 716 | node = self.find_node(obj.name) 717 | 718 | # Transform, is equal for all objects. 719 | # Also store the matrix for culling and merging. 720 | node.mtx = obj.matrix_basis 721 | node.set_scale_rot_trans(self.settings['imd_magnification']) 722 | 723 | if obj.type == 'EMPTY': 724 | children = self.process_children(node, obj.children) 725 | if children: 726 | node.child = children[0].index 727 | 728 | elif obj.type == 'ARMATURE': 729 | # Process bones first. 730 | root_bones = [] 731 | 732 | if obj.data.bones: 733 | for bone in obj.data.bones: 734 | if bone.parent is None: 735 | root_bones.append(bone) 736 | bones = self.process_bones(node, root_bones) 737 | 738 | # Process children and add bones. 739 | children = self.process_children(node, obj.children) 740 | 741 | if bones: 742 | if children: 743 | bones[-1].brother_next = children[0].index 744 | children[0].brother_prev = bones[-1].index 745 | children = bones + children 746 | else: 747 | children.extend(bones) 748 | 749 | if children: 750 | node.child = children[0].index 751 | 752 | elif obj.type == 'MESH': 753 | node.kind = 'mesh' 754 | node.billboard = obj.nns_billboard 755 | if node.billboard in ['on', 'y_on']: 756 | # Not sure if this is a good fix. 757 | mtx = Matrix.Rotation(math.radians(-90), 4, 'X') 758 | node.mtx = node.mtx @ mtx 759 | node.set_scale_rot_trans(self.settings['imd_magnification']) 760 | self.process_mesh(node, obj) 761 | children = self.process_children(node, obj.children) 762 | if children: 763 | node.child = children[0].index 764 | 765 | node.parent = parent.index 766 | brothers.append(node) 767 | 768 | length = len(brothers) 769 | 770 | for index, brother in enumerate(brothers): 771 | if index > 0: 772 | brother.brother_prev = brothers[index - 1].index 773 | if index < (length - 1): 774 | brother.brother_next = brothers[index + 1].index 775 | 776 | return brothers 777 | 778 | def process_bones(self, parent, bones): 779 | brothers = [] 780 | 781 | for bone in bones: 782 | node = self.find_node(bone.name) 783 | node.kind = 'joint' 784 | 785 | # Make matrix for node. 786 | self.find_matrix(node.index, bone.matrix_local.copy()) 787 | 788 | # Calculate transform. 789 | transform = bone.matrix_local if bone else Matrix.Identity(4) 790 | if bone and bone.parent: 791 | transform = bone.parent.matrix_local.inverted() @ transform 792 | 793 | # Transform node. 794 | euler = transform.to_euler('XYZ') 795 | node.rotate = [decimal.Decimal(math.degrees(e)) for e in euler] 796 | mag = self.settings['imd_magnification'] 797 | node.translate = transform.to_translation() * mag 798 | 799 | # Get children. 800 | children = self.process_bones(node, bone.children) 801 | if children: 802 | node.child = children[0].index 803 | node.parent = parent.index 804 | 805 | brothers.append(node) 806 | 807 | length = len(brothers) 808 | 809 | for index, brother in enumerate(brothers): 810 | if index > 0: 811 | brother.brother_prev = brothers[index - 1].index 812 | if index < (length - 1): 813 | brother.brother_next = brothers[index + 1].index 814 | 815 | return brothers 816 | 817 | def process_mesh(self, node, obj): 818 | primitives = [] 819 | 820 | # fix copied from fast64 repo, in blender version 4.1 func was removed, in 4.1+ normals are always calculated 821 | if bpy.app.version < (4, 1, 0): 822 | obj.data.calc_normals_split() 823 | 824 | for polygon in obj.data.polygons: 825 | if len(polygon.loop_indices) > 4: 826 | logger.log("Polygon is ngon. Skipped.") 827 | continue 828 | if len(polygon.loop_indices) < 3: 829 | logger.log("Polygon is a line. Skipped.") 830 | continue 831 | index = get_global_mat_index(obj, polygon.material_index) 832 | if index == -1: 833 | logger.log("Polygon doesn't have material. Skipped.") 834 | continue 835 | 836 | # Add polygon to the list of primitives. 837 | primitives.append(Primitive(obj, polygon)) 838 | 839 | if self.settings['imd_use_primitive_strip']: 840 | quad_stripper = QuadStripper() 841 | primitives = quad_stripper.process(primitives) 842 | 843 | tri_stripper = TriStripper() 844 | primitives = tri_stripper.process(primitives) 845 | 846 | self.primitives.append({ 847 | 'obj': obj, 848 | 'node': node, 849 | 'primitives': primitives 850 | }) 851 | 852 | def add_palette(self, name, data, size): 853 | self.palettes.append( 854 | NitroModelPalette(name, data, size, len(self.palettes))) 855 | return self.palettes[-1] 856 | 857 | def find_texture(self, path): 858 | for texture in self.textures: 859 | if texture.path == path: 860 | return texture 861 | self.textures.append(NitroModelTexture(self, path, len(self.textures))) 862 | return self.textures[-1] 863 | 864 | def find_material(self, blender_index): 865 | for material in self.materials: 866 | if material.blender_index == blender_index: 867 | return material 868 | index = len(self.materials) 869 | self.materials.append(NitroModelMaterial(self, blender_index, index)) 870 | return self.materials[-1] 871 | 872 | def find_matrix(self, node_idx, matrix_): 873 | for matrix in self.matrices: 874 | if matrix.node_idx == node_idx: 875 | return matrix 876 | index = len(self.matrices) 877 | self.matrices.append(NitroModelMatrix(index, node_idx, matrix_)) 878 | return self.matrices[-1] 879 | 880 | def find_matrix_by_node_name(self, name): 881 | node = self.find_node(name) 882 | for matrix in self.matrices: 883 | if matrix.node_idx == node.index: 884 | return matrix 885 | return self.find_matrix(node.index, Matrix.Identity(4)) 886 | 887 | def get_childless_node(self): 888 | for node in self.nodes: 889 | if node.child == -1 and not self.node_has_matrix(node): 890 | return node 891 | return None 892 | 893 | def remove_node(self, node): 894 | for other in self.nodes: 895 | if other.child == node.index: 896 | if node.brother_next == -1: 897 | other.child = -1 898 | else: 899 | other.child = node.brother_next 900 | if other.brother_next == node.index: 901 | other.brother_next = node.brother_next 902 | if other.brother_prev == node.index: 903 | other.brother_prev = node.brother_prev 904 | if other.parent == node.index: 905 | raise Exception("Attempting to delete a parent node") 906 | self.nodes.remove(node) 907 | 908 | def node_replace_index(self, node, index): 909 | for other in self.nodes: 910 | if other.index == node.index: 911 | continue 912 | if other.child == node.index: 913 | other.child = index 914 | if other.brother_next == node.index: 915 | other.brother_next = index 916 | if other.brother_prev == node.index: 917 | other.brother_prev = index 918 | if other.parent == node.index: 919 | other.parent = index 920 | for matrix in self.matrices: 921 | if matrix.node_idx == node.index: 922 | matrix.node_idx = index 923 | node.index = index 924 | 925 | def node_has_matrix(self, node): 926 | for matrix in self.matrices: 927 | if matrix.node_idx == node.index: 928 | return True 929 | return False 930 | 931 | def find_polygon(self, name): 932 | for polygon in self.polygons: 933 | if polygon.name == name: 934 | return polygon 935 | index = len(self.polygons) 936 | self.polygons.append(NitroModelPolygon(index, name)) 937 | return self.polygons[-1] 938 | 939 | def find_node(self, name): 940 | for node in self.nodes: 941 | if node.name == name: 942 | return node 943 | index = len(self.nodes) 944 | self.nodes.append(NitroModelNode(index, name)) 945 | return self.nodes[-1] 946 | 947 | def find_node_by_index(self, index): 948 | for node in self.nodes: 949 | if node.index == index: 950 | return node 951 | return None 952 | -------------------------------------------------------------------------------- /nns_object.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | from bpy.props import EnumProperty 3 | 4 | 5 | class NTR_PT_object(bpy.types.Panel): 6 | bl_label = "NNS Object Options" 7 | bl_idname = "OBJECT_PT_nns" 8 | bl_space_type = 'PROPERTIES' 9 | bl_region_type = 'WINDOW' 10 | bl_context = "object" 11 | bl_options = {'HIDE_HEADER'} 12 | 13 | def draw(self, context): 14 | layout = self.layout 15 | obj = context.object 16 | layout = layout.box() 17 | title = layout.column() 18 | title.box().label(text="NNS Object Options") 19 | layout.prop(obj, "nns_billboard") 20 | 21 | 22 | def object_register(): 23 | billboard_items = [ 24 | ("off", "Off", '', 1), 25 | ("on", "Always face camera", '', 2), 26 | ("y_on", "Only face camera on y axis", '', 3) 27 | ] 28 | bpy.types.Object.nns_billboard = EnumProperty( 29 | name="Billboard settings", items=billboard_items) 30 | 31 | bpy.utils.register_class(NTR_PT_object) 32 | 33 | 34 | def object_unregister(): 35 | bpy.utils.unregister_class(NTR_PT_object) -------------------------------------------------------------------------------- /nns_tga.py: -------------------------------------------------------------------------------- 1 | def read_tga_header(f): 2 | return { 3 | 'id_field_length': int.from_bytes(f.read(1), byteorder='little'), 4 | 'color_map_type': int.from_bytes(f.read(1), byteorder='little'), 5 | 'image_type': int.from_bytes(f.read(1), byteorder='little'), 6 | 'color_map_origin': int.from_bytes(f.read(2), byteorder='little'), 7 | 'color_map_length': int.from_bytes(f.read(2), byteorder='little'), 8 | 'color_map_entry_size': int.from_bytes(f.read(1), byteorder='little'), 9 | 'image_x_origin': int.from_bytes(f.read(2), byteorder='little'), 10 | 'image_y_origin': int.from_bytes(f.read(2), byteorder='little'), 11 | 'image_width': int.from_bytes(f.read(2), byteorder='little'), 12 | 'image_heigth': int.from_bytes(f.read(2), byteorder='little'), 13 | 'image_pixel_size': int.from_bytes(f.read(1), byteorder='little'), 14 | 'image_descriptor': int.from_bytes(f.read(1), byteorder='little'), 15 | } 16 | 17 | 18 | def read_nitro_tga_id(f): 19 | return { 20 | 'version': f.read(16).decode('utf-8').replace('\x00', ''), 21 | 'nitro_data_offset': int.from_bytes(f.read(4), byteorder='little') 22 | } 23 | 24 | 25 | def read_nitro_tga_data(f, offset): 26 | color_0_transp = False 27 | pltt_idx_data = None 28 | optpix_data = None 29 | palette_name = '' 30 | palette = None 31 | 32 | # Get the end of the file 33 | f.seek(0, 2) 34 | end_of_file = f.tell() 35 | 36 | # Seek to Nitro TGA Data Offset 37 | f.seek(offset) 38 | 39 | while f.tell() + 12 <= end_of_file: 40 | sig = f.read(8).decode('ascii') 41 | length = int.from_bytes(f.read(4), byteorder='little') 42 | 43 | if sig == 'nns_frmt': 44 | tex_format = f.read(length - 12).decode('ascii') 45 | elif sig == 'nns_txel': 46 | texel_data = f.read(length - 12) 47 | elif sig == 'nns_pidx': 48 | pltt_idx_data = f.read(length - 12) 49 | elif sig == 'nns_pnam': 50 | palette_name = f.read(length - 12).decode('ascii') 51 | elif sig == 'nns_pcol': 52 | palette = f.read(length - 12) 53 | elif sig == 'nns_c0xp': 54 | color_0_transp = True 55 | elif sig == 'nns_gnam': 56 | generator_name = f.read(length - 12).decode('ascii') 57 | elif sig == 'nns_gver': 58 | generator_ver = f.read(length - 12).decode('ascii') 59 | elif sig == 'nns_imst': 60 | optpix_data = f.read(length - 12) 61 | elif sig == 'nns_endb': 62 | return { 63 | 'tex_format': tex_format, 64 | 'texel_data': texel_data, 65 | 'pltt_idx_data': pltt_idx_data, 66 | 'palette_name': palette_name, 67 | 'palette': palette, 68 | 'color_0_transp': color_0_transp, 69 | 'generator_name': generator_name, 70 | 'generator_ver': generator_ver, 71 | 'optpix_data': optpix_data 72 | } 73 | 74 | 75 | def read_nitro_tga(path): 76 | with open(path, "rb") as f: 77 | header = read_tga_header(f) 78 | nitro_tga_id = read_nitro_tga_id(f) 79 | nitro_data = read_nitro_tga_data(f, nitro_tga_id['nitro_data_offset']) 80 | 81 | f.close() 82 | 83 | return { 84 | 'header': header, 85 | 'nitro_tga_id': nitro_tga_id, 86 | 'nitro_data': nitro_data, 87 | } 88 | 89 | 90 | # These functions below purpose is inside the imd directly 91 | # and they might need to be moved somewhere else 92 | def format_hex_data(array, element_size): 93 | str_format = '0' + str(element_size * 2) + 'x' 94 | 95 | out_str = '' 96 | 97 | i = 0 98 | while i < len(array): 99 | temp = [array[j] for j in range(i, i + element_size)] 100 | element = int.from_bytes(temp, byteorder='little') 101 | 102 | out_str += format(element, str_format) + ' ' 103 | i += element_size 104 | 105 | return out_str 106 | 107 | 108 | def get_bitmap_data(tga): 109 | if tga['nitro_data']['tex_format'] == 'tex4x4': 110 | element_size = 4 111 | else: 112 | element_size = 2 113 | 114 | return format_hex_data(tga['nitro_data']['texel_data'], element_size) 115 | 116 | 117 | def get_bitmap_size(tga): 118 | if tga['nitro_data']['tex_format'] == 'tex4x4': 119 | return int(len(tga['nitro_data']['texel_data']) / 4) 120 | else: 121 | return int(len(tga['nitro_data']['texel_data']) / 2) 122 | 123 | 124 | def get_palette_data(tga): 125 | return format_hex_data(tga['nitro_data']['palette'], 2) 126 | 127 | 128 | def get_palette_size(tga): 129 | return int(len(tga['nitro_data']['palette']) / 2) 130 | 131 | 132 | # Used for tex4x4 only 133 | def get_pltt_idx_data(tga): 134 | return format_hex_data(tga['nitro_data']['pltt_idx_data'], 2) 135 | 136 | 137 | def get_pltt_idx_size(tga): 138 | return int(len(tga['nitro_data']['pltt_idx_data']) / 2) 139 | -------------------------------------------------------------------------------- /primitive.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import bpy 3 | from .util import * 4 | from . import local_logger as logger 5 | 6 | 7 | class TriStripper(): 8 | def get_previous_tri_edge(self, a, b): 9 | if b == 0: 10 | return (2 - (0 if a == 1 else 1), a) 11 | if b == 1: 12 | return (0 if a == 2 else 2, a) 13 | return (1 if a == 0 else 0, a) 14 | 15 | def try_strip_in_direction(self, tris, tri_idx, vtxa, vtxb): 16 | processed_tmp = [x.processed for x in tris] 17 | processed_tmp[tri_idx] = True 18 | tri = tris[tri_idx] 19 | vtxb, vtxa = self.get_previous_tri_edge(vtxb, vtxa) 20 | tri_count = 1 21 | while True: 22 | i = -1 23 | while i < 3: 24 | i += 1 25 | if i >= 3: 26 | break 27 | if tri.next_candidates[i] == -1: 28 | continue 29 | candidate = tris[tri.next_candidates[i]] 30 | if processed_tmp[tri.next_candidates[i]]: 31 | continue 32 | if not tri.is_suitable_tstrip_candidate_edge(candidate, 33 | vtxa, 34 | vtxb): 35 | continue 36 | pos_a = tri.positions[vtxa] 37 | vtxa = 0 38 | while vtxa < 3: 39 | if candidate.positions[vtxa] == pos_a: 40 | break 41 | vtxa += 1 42 | pos_b = tri.positions[vtxb] 43 | vtxb = 0 44 | while vtxb < 3: 45 | if candidate.positions[vtxb] == pos_b: 46 | break 47 | vtxb += 1 48 | if vtxa != 3 and vtxb != 3: 49 | vtxb, vtxa = self.get_previous_tri_edge(vtxb, vtxa) 50 | processed_tmp[tri.next_candidates[i]] = True 51 | tri_count += 1 52 | tri = candidate 53 | break 54 | if i == 3: 55 | break 56 | return tri_count 57 | 58 | def make_tstrip_primitive(self, tris, tri_idx, vtxa, vtxb): 59 | result = Primitive() 60 | result.type = 'triangle_strip' 61 | tri = tris[tri_idx] 62 | tri.processed = True 63 | result.material_index = tri.material_index 64 | result.add_vtx(tri, vtxa) 65 | result.add_vtx(tri, vtxb) 66 | vtxb, vtxa = self.get_previous_tri_edge(vtxb, vtxa) 67 | result.add_vtx(tri, vtxb) 68 | while True: 69 | i = -1 70 | while i < 3: 71 | i += 1 72 | if i >= 3: 73 | break 74 | if tri.next_candidates[i] == -1: 75 | continue 76 | candidate = tris[tri.next_candidates[i]] 77 | if candidate.processed: 78 | continue 79 | if not tri.is_suitable_tstrip_candidate_edge(candidate, 80 | vtxa, 81 | vtxb): 82 | continue 83 | pos_a = tri.positions[vtxa] 84 | vtxa = 0 85 | while vtxa < 3: 86 | if candidate.positions[vtxa] == pos_a: 87 | break 88 | vtxa += 1 89 | pos_b = tri.positions[vtxb] 90 | vtxb = 0 91 | while vtxb < 3: 92 | if candidate.positions[vtxb] == pos_b: 93 | break 94 | vtxb += 1 95 | if vtxa != 3 and vtxb != 3: 96 | vtxb, vtxa = self.get_previous_tri_edge(vtxb, vtxa) 97 | result.add_vtx(candidate, vtxb) 98 | candidate.processed = True 99 | tri = candidate 100 | if i == 3: 101 | break 102 | return result 103 | 104 | def process(self, primitives): 105 | result = [] 106 | tris = [x for x in primitives if x.type == 'triangles'] 107 | for tri in tris: 108 | tri.processed = False 109 | 110 | for tri in tris: 111 | tri.next_candidate_count = 0 112 | tri.next_candidates = [-1] * 4 113 | for i, candidate in enumerate(tris): 114 | if not tri.is_suitable_tstrip_candidate(candidate): 115 | continue 116 | tri.next_candidates[tri.next_candidate_count] = i 117 | tri.next_candidate_count += 1 118 | if tri.next_candidate_count >= 3: 119 | break 120 | while True: 121 | count = 0 122 | for tri in tris: 123 | if tri.processed: 124 | continue 125 | count += 1 126 | if tri.next_candidate_count > 0: 127 | cand_count = len([x for x in tri.next_candidates 128 | if x != -1 and not tris[x].processed]) 129 | tri.next_candidate_count = cand_count 130 | if count == 0: 131 | break 132 | min_cand_count_idx = -1 133 | min_cand_count = sys.maxsize 134 | for i, tri in enumerate(tris): 135 | if tri.processed: 136 | continue 137 | if tri.next_candidate_count < min_cand_count: 138 | min_cand_count = tri.next_candidate_count 139 | min_cand_count_idx = i 140 | if min_cand_count <= 1: 141 | break 142 | max_tris = 0 143 | max_tris_vtx0 = -1 144 | max_tris_vtx1 = -1 145 | for i in range(3): 146 | vtx0 = i 147 | vtx1 = 0 if i == 2 else i + 1 148 | tri_count = self.try_strip_in_direction( 149 | tris, min_cand_count_idx, vtx0, vtx1) 150 | if tri_count > max_tris: 151 | max_tris = tri_count 152 | max_tris_vtx0 = vtx0 153 | max_tris_vtx1 = vtx1 154 | if max_tris <= 1: 155 | tri = tris[min_cand_count_idx] 156 | tri.processed = True 157 | result.append(tri) 158 | else: 159 | result.append(self.make_tstrip_primitive(tris, 160 | min_cand_count_idx, 161 | max_tris_vtx0, 162 | max_tris_vtx1)) 163 | result.extend([x for x in primitives if x.type != 'triangles']) 164 | return result 165 | 166 | 167 | class QuadStripper(): 168 | def get_opposite_quad_edge(self, a, b): 169 | if a == 3 and b == 0: 170 | return (2, 1) 171 | if a == 0 and b == 3: 172 | return (1, 2) 173 | if a >= b: 174 | return ( 175 | 0 if a == 3 else a + 1, 176 | 3 if b == 0 else b - 1 177 | ) 178 | return ( 179 | 3 if a == 0 else a - 1, 180 | 0 if b == 3 else b + 1 181 | ) 182 | 183 | def try_strip_in_direction(self, quads, quad_idx, vtxa, vtxb): 184 | processed_tmp = [x.processed for x in quads] 185 | processed_tmp[quad_idx] = True 186 | quad = quads[quad_idx] 187 | vtxa, vtxb = self.get_opposite_quad_edge(vtxa, vtxb) 188 | quad_count = 1 189 | while True: 190 | i = -1 191 | while i < 4: 192 | i += 1 193 | if i >= 4: 194 | break 195 | if quad.next_candidates[i] == -1: 196 | continue 197 | candidate = quads[quad.next_candidates[i]] 198 | if processed_tmp[quad.next_candidates[i]]: 199 | continue 200 | if not quad.is_suitable_qstrip_candidate_edge(candidate, 201 | vtxa, 202 | vtxb): 203 | continue 204 | pos_a = quad.positions[vtxa] 205 | vtxa = 0 206 | while vtxa < 4: 207 | if candidate.positions[vtxa] == pos_a: 208 | break 209 | vtxa += 1 210 | pos_b = quad.positions[vtxb] 211 | vtxb = 0 212 | while vtxb < 4: 213 | if candidate.positions[vtxb] == pos_b: 214 | break 215 | vtxb += 1 216 | if vtxa != 4 and vtxb != 4: 217 | vtxa, vtxb = self.get_opposite_quad_edge(vtxa, vtxb) 218 | processed_tmp[quad.next_candidates[i]] = True 219 | quad_count += 1 220 | quad = candidate 221 | break 222 | if i == 4 or quad_count > 1706: 223 | break 224 | return quad_count 225 | 226 | def make_qstrip_primitive(self, quads, quad_idx, vtxa, vtxb): 227 | result = Primitive() 228 | result.type = 'quad_strip' 229 | quad = quads[quad_idx] 230 | quad.processed = True 231 | result.material_index = quad.material_index 232 | result.add_vtx(quad, vtxa) 233 | result.add_vtx(quad, vtxb) 234 | vtxa, vtxb = self.get_opposite_quad_edge(vtxa, vtxb) 235 | result.add_vtx(quad, vtxa) 236 | result.add_vtx(quad, vtxb) 237 | quad_count = 1 238 | while True: 239 | i = -1 240 | while i < 4: 241 | i += 1 242 | if i >= 4: 243 | break 244 | if quad.next_candidates[i] == -1: 245 | continue 246 | candidate = quads[quad.next_candidates[i]] 247 | if candidate.processed: 248 | continue 249 | if not quad.is_suitable_qstrip_candidate_edge(candidate, 250 | vtxa, 251 | vtxb): 252 | continue 253 | pos_a = quad.positions[vtxa] 254 | vtxa = 0 255 | while vtxa < 4: 256 | if candidate.positions[vtxa] == pos_a: 257 | break 258 | vtxa += 1 259 | pos_b = quad.positions[vtxb] 260 | vtxb = 0 261 | while vtxb < 4: 262 | if candidate.positions[vtxb] == pos_b: 263 | break 264 | vtxb += 1 265 | if vtxa != 4 and vtxb != 4: 266 | vtxa, vtxb = self.get_opposite_quad_edge(vtxa, vtxb) 267 | result.add_vtx(candidate, vtxa) 268 | result.add_vtx(candidate, vtxb) 269 | candidate.processed = True 270 | quad_count += 1 271 | quad = candidate 272 | break 273 | if i == 4 or quad_count >= 1706: 274 | break 275 | return result 276 | 277 | def process(self, primitives): 278 | result = [] 279 | quads = [x for x in primitives if x.type == 'quads'] 280 | 281 | for quad in quads: 282 | quad.next_candidate_count = 0 283 | quad.next_candidates = [-1] * 4 284 | for i, candidate in enumerate(quads): 285 | if not quad.is_suitable_qstrip_candidate(candidate): 286 | continue 287 | quad.next_candidates[quad.next_candidate_count] = i 288 | quad.next_candidate_count += 1 289 | if quad.next_candidate_count >= 4: 290 | break 291 | 292 | while True: 293 | count = 0 294 | for quad in quads: 295 | if quad.processed: 296 | continue 297 | count += 1 298 | if quad.next_candidate_count > 0: 299 | cand_count = len([x for x in quad.next_candidates 300 | if x != -1 and not quads[x].processed]) 301 | quad.next_candidate_count = cand_count 302 | if count == 0: 303 | break 304 | min_cand_count_idx = -1 305 | min_cand_count = sys.maxsize 306 | for i, quad in enumerate(quads): 307 | if quad.processed: 308 | continue 309 | if quad.next_candidate_count < min_cand_count: 310 | min_cand_count = quad.next_candidate_count 311 | min_cand_count_idx = i 312 | if min_cand_count <= 1: 313 | break 314 | max_quads = 0 315 | max_quads_vtx0 = -1 316 | max_quads_vtx1 = -1 317 | for i in range(4): 318 | vtx0 = i 319 | vtx1 = 0 if i == 3 else i + 1 320 | quad_count = self.try_strip_in_direction( 321 | quads, 322 | min_cand_count_idx, 323 | vtx0, 324 | vtx1) 325 | if quad_count > max_quads: 326 | max_quads = quad_count 327 | max_quads_vtx0 = vtx0 328 | max_quads_vtx1 = vtx1 329 | if max_quads <= 1: 330 | quad = quads[min_cand_count_idx] 331 | quad.processed = True 332 | result.append(quad) 333 | else: 334 | result.append(self.make_qstrip_primitive(quads, 335 | min_cand_count_idx, 336 | max_quads_vtx0, 337 | max_quads_vtx1)) 338 | result.extend([x for x in primitives if x.type != 'quads']) 339 | return result 340 | 341 | 342 | class Primitive(): 343 | """ 344 | Raw representation of blender data into a primitive used 345 | for stripping. 346 | """ 347 | def __init__(self, obj=None, polygon=None): 348 | if obj is None and polygon is None: 349 | self.type = 'illegal' 350 | self.positions = [] 351 | self.normals = [] 352 | self.colors = [] 353 | self.texcoords = [] 354 | self.groups = [] 355 | self.processed = False 356 | self.next_candidate_count = 0 357 | # An array of indexes. 358 | self.next_candidates = [] 359 | self.material_index = -1 360 | self.vertex_count = 0 361 | return 362 | 363 | self.type = 'illegal' 364 | self.positions = [] 365 | self.normals = [] 366 | self.colors = [] 367 | self.texcoords = [] 368 | # The group this vertex belongs to. 369 | # This is not important for stripping. 370 | self.groups = [] 371 | self.processed = False 372 | self.next_candidate_count = 0 373 | # An array of indexes. 374 | self.next_candidates = [] 375 | 376 | self.material_index = get_global_mat_index( 377 | obj, polygon.material_index) 378 | 379 | vertices = [v.co for v in obj.data.vertices.values()] 380 | 381 | if len(polygon.loop_indices) == 3: 382 | self.type = 'triangles' 383 | self.vertex_count = 3 384 | elif len(polygon.loop_indices) == 4: 385 | self.type = 'quads' 386 | self.vertex_count = 4 387 | 388 | use_colors = False 389 | 390 | if len(obj.data.vertex_colors) > 0: 391 | use_colors = True 392 | 393 | for idx in polygon.loop_indices: 394 | # Get vertex and convert it to VecFx32. 395 | vertex_index = obj.data.loops[idx].vertex_index 396 | vecfx32 = VecFx32().from_floats(vertices[vertex_index]) 397 | 398 | # Store position. 399 | self.positions.append(vecfx32) 400 | 401 | # Store group. 402 | groups = obj.data.vertices[vertex_index].groups 403 | if groups: 404 | self.groups.append(groups[0].group) 405 | else: 406 | self.groups.append(-1) 407 | 408 | # Color 409 | if use_colors: 410 | # Use special function to get color because the vertex colors 411 | # may not align with the vertex loops. 412 | color = get_color_from_obj(obj, idx) 413 | r = int(round(color[0] * 31)) 414 | g = int(round(color[1] * 31)) 415 | b = int(round(color[2] * 31)) 416 | self.colors.append((r, g, b)) 417 | else: 418 | self.colors.append((0, 0, 0)) 419 | 420 | # Normal 421 | normal = obj.data.loops[idx].normal 422 | self.normals.append(vector_to_vecfx10(normal.normalized())) 423 | 424 | # Texture coordinates 425 | if obj.data.uv_layers.active is not None: 426 | if len(obj.data.uv_layers.active.data) <= idx: 427 | logger.log('Object uv layer not aligned, add zero coord:') 428 | logger.log(f'UV layer: {obj.data.uv_layers.active.name}') 429 | self.texcoords.append(VecFx32([0, 0, 0])) 430 | else: 431 | uv = obj.data.uv_layers.active.data[idx].uv 432 | self.texcoords.append( 433 | VecFx32().from_vector(Vector([uv[0], uv[1], 0]))) 434 | else: 435 | self.texcoords.append(VecFx32([0, 0, 0])) 436 | 437 | def add_vtx(self, src, idx): 438 | self.vertex_count += 1 439 | self.positions.append(src.positions[idx]) 440 | self.colors.append(src.colors[idx]) 441 | self.normals.append(src.normals[idx]) 442 | self.texcoords.append(src.texcoords[idx]) 443 | self.groups.append(src.groups[idx]) 444 | 445 | def is_extra_data_equal(self, a, other, b): 446 | return ( 447 | self.colors[a] == other.colors[b] 448 | and self.material_index == other.material_index 449 | and self.normals[a] == other.normals[b] 450 | and self.texcoords[a] == other.texcoords[b] 451 | ) 452 | 453 | def is_suitable_tstrip_candidate(self, candidate): 454 | equal_count = 0 455 | first_i = 0 456 | first_j = 0 457 | for i in range(3): 458 | for j in range(3): 459 | if (self.positions[i] != candidate.positions[j] 460 | or not self.is_extra_data_equal(i, candidate, j)): 461 | continue 462 | if equal_count == 0: 463 | first_i = i 464 | first_j = j 465 | elif equal_count == 1: 466 | if first_i == 0 and i == 2: 467 | return first_j < j or (first_j == 2 and j == 0) 468 | return first_j > j or (first_j == 0 and j == 2) 469 | equal_count += 1 470 | return False 471 | 472 | def is_suitable_tstrip_candidate_edge(self, candidate, a, b): 473 | equal_count = 0 474 | for i in range(3): 475 | if (self.positions[a] == candidate.positions[i] 476 | and self.is_extra_data_equal(a, candidate, i)): 477 | equal_count += 1 478 | if (self.positions[b] == candidate.positions[i] 479 | and self.is_extra_data_equal(b, candidate, i)): 480 | equal_count += 1 481 | return equal_count == 2 482 | 483 | def is_suitable_qstrip_candidate(self, candidate): 484 | equal_count = 0 485 | first_i = 0 486 | first_j = 0 487 | for i in range(4): 488 | for j in range(4): 489 | if (self.positions[i] != candidate.positions[j] 490 | or not self.is_extra_data_equal(i, candidate, j)): 491 | continue 492 | if equal_count == 0: 493 | first_i = i 494 | first_j = j 495 | elif equal_count == 1: 496 | if first_i == 0 and i == 3: 497 | return first_j < j or (first_j == 3 and j == 0) 498 | return first_j > j or (first_j == 0 and j == 3) 499 | equal_count += 1 500 | return False 501 | 502 | def is_suitable_qstrip_candidate_edge(self, candidate, a, b): 503 | equal_count = 0 504 | for i in range(4): 505 | if (self.positions[a] == candidate.positions[i] 506 | and self.is_extra_data_equal(a, candidate, i)): 507 | equal_count += 1 508 | if (self.positions[b] == candidate.positions[i] 509 | and self.is_extra_data_equal(b, candidate, i)): 510 | equal_count += 1 511 | return equal_count == 2 512 | -------------------------------------------------------------------------------- /util.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | from mathutils import Vector 3 | 4 | 5 | def get_color_from_obj(obj, idx): 6 | """ 7 | This function exists because we cannot trust blender to have the vertex colors 8 | to be aligned with the vertex loops. Possibly this happens when you import a 9 | wrong model. 10 | """ 11 | if len(obj.data.vertex_colors[0].data) <= idx: 12 | return (0, 0, 0) 13 | else: 14 | return obj.data.vertex_colors[0].data[idx].color 15 | 16 | 17 | def is_pos_s(vecfx32): 18 | return ( 19 | (vecfx32.x & 0x3F) == 0 and 20 | (vecfx32.y & 0x3F) == 0 and 21 | (vecfx32.z & 0x3F) == 0 22 | ) 23 | 24 | 25 | def is_pos_diff(diff): 26 | # 512 is 0.125 in FX32 27 | return ( 28 | abs(diff.x) < 512 and 29 | abs(diff.y) < 512 and 30 | abs(diff.z) < 512 31 | ) 32 | 33 | 34 | def calculate_pos_scale(max_coord): 35 | m = float_to_fx32(max_coord) 36 | pos_scale = 0 37 | while m >= 0x8000: 38 | pos_scale += 1 39 | m >>= 1 40 | return pos_scale 41 | 42 | 43 | def get_object_max_min(obj): 44 | matrix = obj.matrix_world 45 | bounds = [matrix @ Vector(v) for v in obj.bound_box] 46 | return { 47 | 'min': bounds[0], 48 | 'max': bounds[6] 49 | } 50 | 51 | 52 | def get_all_max_min(): 53 | min_p = Vector([float('inf'), float('inf'), float('inf')]) 54 | max_p = Vector([-float('inf'), -float('inf'), -float('inf')]) 55 | for obj in bpy.context.view_layer.objects: 56 | if obj.type != 'MESH': 57 | continue 58 | max_min = get_object_max_min(obj) 59 | # Max 60 | max_p.x = max(max_p.x, max_min['max'].x) 61 | max_p.x = max(max_p.x, max_min['min'].x) 62 | max_p.y = max(max_p.y, max_min['max'].y) 63 | max_p.y = max(max_p.y, max_min['min'].y) 64 | max_p.z = max(max_p.z, max_min['max'].z) 65 | max_p.z = max(max_p.z, max_min['min'].z) 66 | # Min 67 | min_p.x = min(min_p.x, max_min['min'].x) 68 | min_p.x = min(min_p.x, max_min['max'].x) 69 | min_p.y = min(min_p.y, max_min['min'].y) 70 | min_p.y = min(min_p.y, max_min['max'].y) 71 | min_p.z = min(min_p.z, max_min['min'].z) 72 | min_p.z = min(min_p.z, max_min['max'].z) 73 | 74 | return { 75 | 'min': min_p, 76 | 'max': max_p 77 | } 78 | 79 | 80 | def get_global_mat_index(obj, index): 81 | if len(obj.material_slots) <= index: 82 | # If an object doesn't have (enough) material slots, the polygon 83 | # with the requested index shouldn't be converted. 84 | return -1 85 | if obj.material_slots[index].material is None: 86 | # Material doesn't have any material in the slot. 87 | return -1 88 | name = obj.material_slots[index].material.name 89 | return bpy.data.materials.find(name) 90 | 91 | 92 | def lin2s(x): 93 | """ 94 | Le color correction function. From some guy on blender stackexchange. 95 | http://entropymine.com/imageworsener/srgbformula/ 96 | """ 97 | if x <= 0.0031308: 98 | y = x * 12.92 99 | elif 0.0031308 < x <= 1: 100 | y = 1.055 * x ** (1 / 2.4) - 0.055 101 | return y 102 | 103 | 104 | def float_to_fx32(value): 105 | return int(round(value * 4096)) 106 | 107 | 108 | def fx32_to_float(value): 109 | return float(value) / 4096 110 | 111 | 112 | def float_to_fx10(value): 113 | return max(min(int(round(value * 512)), 511), -512) 114 | 115 | 116 | def fx10_to_float(value): 117 | return float(value) / 512 118 | 119 | 120 | def vector_to_vecfx10(vector): 121 | return Vecfx10([ 122 | float_to_fx10(vector.x), 123 | float_to_fx10(vector.y), 124 | float_to_fx10(vector.z), 125 | ]) 126 | 127 | 128 | class Vecfx10(): 129 | def __init__(self, vector=[0, 0, 0]): 130 | self.x = vector[0] 131 | self.y = vector[1] 132 | self.z = vector[2] 133 | 134 | def to_vector(self): 135 | return Vector([ 136 | fx10_to_float(self.x), 137 | fx10_to_float(self.y), 138 | fx10_to_float(self.z), 139 | ]) 140 | 141 | def __eq__(self, other): 142 | if other is None: 143 | return False 144 | return ( 145 | self.x == other.x 146 | and self.y == other.y 147 | and self.z == other.z 148 | ) 149 | 150 | 151 | class VecFx32(object): 152 | def __init__(self, vector=[0, 0, 0]): 153 | self.x = vector[0] 154 | self.y = vector[1] 155 | self.z = vector[2] 156 | 157 | def from_floats(self, floats): 158 | return VecFx32([ 159 | float_to_fx32(floats[0]), 160 | float_to_fx32(floats[1]), 161 | float_to_fx32(floats[2]) 162 | ]) 163 | 164 | def from_vector(self, vector): 165 | return VecFx32([ 166 | float_to_fx32(vector.x), 167 | float_to_fx32(vector.y), 168 | float_to_fx32(vector.z) 169 | ]) 170 | 171 | def to_vector(self): 172 | return Vector([ 173 | fx32_to_float(self.x), 174 | fx32_to_float(self.y), 175 | fx32_to_float(self.z), 176 | ]) 177 | 178 | def __str__(self): 179 | return str(self.x), str(self.y), str(self.z) 180 | 181 | def __sub__(self, other): 182 | if isinstance(other, self.__class__): 183 | return VecFx32([ 184 | self.x - other.x, 185 | self.y - other.y, 186 | self.z - other.z 187 | ]) 188 | elif isinstance(other, int): 189 | return VecFx32([ 190 | self.x - other, 191 | self.y - other, 192 | self.z - other 193 | ]) 194 | else: 195 | raise TypeError( 196 | "unsupported operand type(s) for -: '{}' and '{}'" 197 | ).format(self.__class__, type(other)) 198 | 199 | def __rshift__(self, other): 200 | if isinstance(other, self.__class__): 201 | return VecFx32([ 202 | self.x >> other.x, 203 | self.y >> other.y, 204 | self.z >> other.z 205 | ]) 206 | elif isinstance(other, int): 207 | return VecFx32([ 208 | self.x >> other, 209 | self.y >> other, 210 | self.z >> other 211 | ]) 212 | else: 213 | raise TypeError( 214 | "unsupported operand type(s) for >>: '{}' and '{}'" 215 | ).format(self.__class__, type(other)) 216 | 217 | def __lt__(self, other): 218 | if isinstance(other, self.__class__): 219 | return ( 220 | self.x < other.x and 221 | self.y < other.y and 222 | self.z < other.z 223 | ) 224 | elif isinstance(other, int): 225 | return ( 226 | self.x < other and 227 | self.y < other and 228 | self.z < other 229 | ) 230 | else: 231 | raise TypeError( 232 | "unsupported operand type(s) for <: '{}' and '{}'" 233 | ).format(self.__class__, type(other)) 234 | 235 | def __eq__(self, other): 236 | if other is None: 237 | return False 238 | return ( 239 | self.x == other.x 240 | and self.y == other.y 241 | and self.z == other.z 242 | ) 243 | -------------------------------------------------------------------------------- /version.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | addon_version = (0, 0, 0) 4 | 5 | 6 | def get_version_str(): 7 | return '.'.join(map(str, addon_version)) 8 | --------------------------------------------------------------------------------