Repository: isathar/Blender_UE4_VectorFieldEditor Branch: master Commit: b24f207c9142 Files: 5 Total size: 47.2 KB Directory structure: gitextract_6jzh4irb/ ├── .github/ │ └── FUNDING.yml ├── FGA_VectorFields/ │ ├── __init__.py │ ├── vf_editor.py │ └── vf_io.py └── README.md ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/FUNDING.yml ================================================ patreon: isathar ================================================ FILE: FGA_VectorFields/__init__.py ================================================ bl_info = { "name": "Vector Field Tools", "author": "Andreas Wiehn (isathar)", "version": (1, 2, 1), "blender": (2, 80, 0), "location": "View3D > Add > VectorField", "description": "Create and edit 3D Vector Fields using the .fga format.", "warning": "", "tracker_url": "https://github.com/isathar/Blender_UE4_VectorFieldEditor/issues/", "category": "Object"} import bpy from bpy_extras.io_utils import (ImportHelper,ExportHelper,path_reference_mode) from bl_operators.presets import AddPresetBase from . import vf_editor, vf_io # UI Panel class VFTOOLS_PT_menupanel(bpy.types.Panel): bl_idname = "VFTOOLS_PT_menupanel" bl_label = 'Vector Fields' bl_space_type = 'VIEW_3D' bl_region_type = 'UI' bl_category = "Particle Simulation" def __init__(self): pass @classmethod def poll(self, context): return True def draw(self, context): layout = self.layout # Create box = layout.box() show_createpanel = box.prop(context.window_manager, 'show_createpanel', toggle=True, text="Create") if context.window_manager.show_createpanel: box.row().column().prop(context.window_manager, 'vf_density', text='Resolution') box.row().column().prop(context.window_manager, 'vf_scale', text='Scale') box.row().prop(context.window_manager, 'vf_gravity', text='Gravity') box.row().prop(context.window_manager, 'vf_particleLifetime', text='Particle Lifetime') row = box.row() if context.active_object: if context.active_object.particle_systems: if context.active_object.particle_systems[0]: if context.active_object.particle_systems[0].settings.physics_type == 'FLUID': row.menu("Presets_VFCreate_Fluid",text=bpy.types.Presets_VFCreate_Fluid.bl_label) row.operator("object.preset_vfcreate_fluid", text="", icon='ADD') row.operator("object.preset_vfcreate_fluid", text="", icon='REMOVE').remove_active = True elif context.active_object.particle_systems[0].settings.physics_type == 'NEWTON': row.menu("Presets_VFCreate",text=bpy.types.Presets_VFCreate.bl_label) row.operator("object.preset_vfcreate", text="", icon='ADD') row.operator("object.preset_vfcreate", text="", icon='REMOVE').remove_active = True box.row().operator('vftools.create_vectorfield', text='Generate') numObjects = context.window_manager.vf_density[0] * context.window_manager.vf_density[1] * context.window_manager.vf_density[2] box.row().label(text="# of vectors: " + str(numObjects)) # Edit box = layout.box() show_editpanel = box.prop(context.window_manager, 'show_editpanel', toggle=True, text="Edit") if context.window_manager.show_editpanel: box2 = box.box() box2.row().label(text=" Velocity Type:") box2.row().prop(context.window_manager, 'pvelocity_veltype', text='') if context.window_manager.pvelocity_veltype == "VECT": box2.row(align=True).column().prop(context.window_manager, 'pvelocity_dirvector',text='Custom Direction') box2.row().label(text=" Blend Method:") box2.row().prop(context.window_manager, 'pvelocity_genmode', text='') if context.window_manager.pvelocity_genmode == 'AVG': box2.row().prop(context.window_manager, 'pvelocity_avgratio',text='Ratio') box2.row(align=True).prop(context.window_manager, 'pvelocity_selection',text='Selected Only') box2.row(align=True).prop(context.window_manager, 'pvelocity_invert',text='Invert Next') box2.row(align=True).operator('object.calc_vectorfieldvelocities', text='Calculate') box2 = box.box() box2.row().operator('object.vf_normalizevelocities', text='Normalize') box2.row().operator('object.vf_invertvelocities', text='Invert All') # Display box = layout.box() box.prop(context.window_manager, 'show_displaypanel', toggle=True, text="Display") if context.window_manager.show_displaypanel: box.prop(context.window_manager, 'vf_velocitylinescolor', toggle=True, text="Line Color") if context.window_manager.vf_showingvelocitylines < 1: box.row().operator('view3d.toggle_vectorfieldvelocities', text='Show') else: box.row().operator('view3d.toggle_vectorfieldvelocities', text='Hide') # Tools: box = layout.box() box.prop(context.window_manager, 'show_toolspanel', toggle=True, text="Tools") if context.window_manager.show_toolspanel: # # Wind Curve Force box = box.box() box.prop(context.window_manager, 'show_windcurvetool', toggle=True, text="Wind Curve Force") if context.active_object: if context.active_object.type == 'CURVE' or 'CurveForce' in context.active_object.name: if context.window_manager.show_windcurvetool: if context.active_object: if context.active_object.type == 'CURVE': box.row(align=True).prop(context.window_manager, 'curveForce_strength', text='Strength') box.row(align=True).prop(context.window_manager, 'curveForce_maxDist', text='Distance') box.row(align=True).prop(context.window_manager, 'curveForce_falloffPower', text='Power') box.row(align=True).prop(context.window_manager, 'curveForce_trailout', text='Trail') box.row(align=True).operator('object.calc_curvewindforce', text='Create New') elif 'CurveForce' in context.active_object.name: box.row(align=True).prop(context.window_manager, 'curveForce_strength', text='Strength') box.row(align=True).prop(context.window_manager, 'curveForce_maxDist', text='Distance') box.row(align=True).prop(context.window_manager, 'curveForce_falloffPower', text='Power') box.row(align=True).operator('object.edit_curvewindforce', text='Edit Selected') else: box.row().label(text='Select a curve or curve force') box.enabled = False # Saved Data class vector_field(bpy.types.PropertyGroup): vcoord : bpy.props.FloatVectorProperty(default=(0.0, 0.0, 0.0)) vvelocity : bpy.props.FloatVectorProperty(default=(0.0, 0.0, 0.0)) # Export class export_vectorfieldfile(bpy.types.Operator, ExportHelper): bl_idname = "object.export_vectorfieldfile" bl_label = "Export FGA" bl_description = 'Export selected volume as a FGA file' filename_ext = ".fga" filter_glob : bpy.props.StringProperty(default="*.fga", options={'HIDDEN'}) exportvf_allowmanualbounds : bpy.props.BoolProperty( name="Manual Bounds",default=False, description="Allow setting vector field bounds manually" ) exportvf_manualboundsneg : bpy.props.IntVectorProperty( name="Bounds Scale -",min=-10000,max=10000,default=(-100,-100,-100), subtype='TRANSLATION', description="Minimum values for bounds in cm (have to be less than maximum values)" ) exportvf_manualboundspos : bpy.props.IntVectorProperty( name="Bounds Scale +",min=-10000,max=10000,default=(100,100,100), subtype='TRANSLATION', description="Maximum values for bounds in cm (have to be greater than minimum values)" ) exportvf_manualvelocityscale : bpy.props.FloatProperty( name="Velocity Scale",min=1.0,max=10000.0,default=1.0, description="Multiplier for velocities when using manual bounds" ) exportvf_scale : bpy.props.FloatProperty( name="Bounds Scale",min=1.0,max=10000.0,default=100.0, description=("Scale the size of the volume's bounds by this on export" + " - actual size in UE4 = this * (the vector field's density) * 0.5 cm") ) exportvf_velscale : bpy.props.BoolProperty( name="Scale Velocity",default=True, description="Scale velocity with bounds scale" ) exportvf_locoffset : bpy.props.BoolProperty( name="Export Offset",default=True, description="Exports the location of the vector field's bounding volume as an offset to min/max bounds" ) def check_extension(self): return self.batch_mode == 'OFF' def check(self, context): is_def_change = super().check(context) return (is_def_change) def draw(self,context): layout = self.layout box = layout.box() box.row().prop(self, 'exportvf_allowmanualbounds', text='Manual Bounds') if self.exportvf_allowmanualbounds: box.row().column().prop(self, 'exportvf_manualboundsneg', text='Minimum Bounds') box.row().column().prop(self, 'exportvf_manualboundspos', text='Maximum Bounds') box.row().prop(self, 'exportvf_manualvelocityscale', text='Velocity Scale:') else: box.row().prop(self, 'exportvf_scale', text='Export Scale:') box.row().prop(self, 'exportvf_velscale', text='Scale Velocity') box.row().prop(self, 'exportvf_locoffset', text='Export Offset') def execute(self, context): if not self.filepath: raise Exception("filepath not set") else: if context.active_object != None: if 'custom_vectorfield' in context.active_object: vf_io.write_fgafile(self, context.active_object) else: activeobj = context.active_object found = False for obj in context.selectable_objects: if obj.parent == activeobj: if 'custom_vectorfield' in obj: vf_io.write_fgafile(self, obj) found = True break if not found: raise Exception("No velocities") else: raise Exception("Nothing selected") return {'FINISHED'} # Import class import_vectorfieldfile(bpy.types.Operator, ImportHelper): bl_idname = "object.import_vectorfieldfile" bl_label = "Import FGA" bl_description = 'Import FGA file as a vector field' filename_ext = ".fga" filter_glob : bpy.props.StringProperty(default="*.fga", options={'HIDDEN'}) importvf_scalemult : bpy.props.FloatProperty( name="Size Multiplier",min=0.0001,max=10000.0,step=0.0001,default=0.01, description="Multiplier to apply to the scale of the volume's bounds on import" ) importvf_velscale : bpy.props.BoolProperty( name="Scale Velocity",default=True, description="Scale velocity on import" ) importvf_getoffset : bpy.props.BoolProperty( name="Get Offset",default=True, description="Get location offset from file" ) def draw(self,context): layout = self.layout box = layout.box() row = box.row() row.prop(self, 'importvf_scalemult', text='Import Scale') row = box.row() row.prop(self, 'importvf_velscale', text='Scale Velocity') row = box.row() row.prop(self, 'importvf_getoffset', text='Import Offset') def execute(self, context): retmessage = vf_io.parse_fgafile(self, context) print ("FGA Import: " + retmessage + " (" + self.filepath + ")") return {'FINISHED'} class Preset_VFCreate(AddPresetBase, bpy.types.Operator): bl_idname = 'object.preset_vfcreate' bl_label = 'Physics Presets' bl_options = {'REGISTER', 'UNDO'} preset_menu = 'Presets_VFCreate' preset_subdir = 'VF_Default_Presets' preset_defines = [ "PSystem = bpy.context.active_object.particle_systems[0].settings" ] preset_values = [ # standard "PSystem.effector_weights.gravity", "PSystem.factor_random", "PSystem.particle_size", "PSystem.size_random", "PSystem.mass", "PSystem.brownian_factor", "PSystem.drag_factor", "PSystem.damping" ] class Preset_VFCreate_Fluid(AddPresetBase, bpy.types.Operator): bl_idname = 'object.preset_vfcreate_fluid' bl_label = 'Fluid Physics Presets' bl_options = {'REGISTER', 'UNDO'} preset_menu = 'Presets_VFCreate_Fluid' preset_subdir = 'VF_Fluid_Presets' preset_defines = [ "PSystem = bpy.context.active_object.particle_systems[0].settings" ] preset_values = [ # standard "PSystem.effector_weights.gravity", "PSystem.factor_random", "PSystem.particle_size", "PSystem.size_random", "PSystem.mass", "PSystem.brownian_factor", "PSystem.drag_factor", "PSystem.damping", # fluid "PSystem.fluid.stiffness", "PSystem.fluid.linear_viscosity", "PSystem.fluid.buoyancy", "PSystem.fluid.stiff_viscosity", "PSystem.fluid.fluid_radius", "PSystem.fluid.rest_density" ] class Presets_VFCreate(bpy.types.Menu): bl_label = "Physics Presets" bl_idname = "Presets_VFCreate" preset_subdir = "VF_Default_Presets" preset_operator = "script.execute_preset" draw = bpy.types.Menu.draw_preset class Presets_VFCreate_Fluid(bpy.types.Menu): bl_label = "Fluid Presets" bl_idname = "Presets_VFCreate_Fluid" preset_subdir = "VF_Fluid_Presets" preset_operator = "script.execute_preset" draw = bpy.types.Menu.draw_preset def exportmenu_func(self, context): self.layout.operator(export_vectorfieldfile.bl_idname, text="UE4 Vector Field (.fga)") def importmenu_func(self, context): self.layout.operator(import_vectorfieldfile.bl_idname, text="UE4 Vector Field (.fga)") def initdefaults(): bpy.types.Object.custom_vectorfield = bpy.props.CollectionProperty(type=vector_field) bpy.types.Object.custom_vf_startlocs = bpy.props.CollectionProperty(type=vector_field) bpy.types.Object.vf_object_density = bpy.props.FloatVectorProperty(default=(0.0,0.0,0.0)) bpy.types.Object.vf_object_scale = bpy.props.FloatVectorProperty(default=(1.0,1.0,1.0)) # generate bpy.types.WindowManager.vf_density = bpy.props.IntVectorProperty( default=(16,16,16),min=1,max=128, description="The number of points in the vector field" ) bpy.types.WindowManager.vf_scale = bpy.props.FloatVectorProperty( default=(1.0,1.0,1.0),min=0.25, description="Distance between points in the vector field" ) bpy.types.WindowManager.vf_gravity = bpy.props.FloatProperty( default=0.0,min=0.0,description="Amount of influence gravity has on the volume's particles" ) bpy.types.WindowManager.vf_particleLifetime = bpy.props.IntProperty(default=32) # calculate/edit bpy.types.WindowManager.pvelocity_veltype = bpy.props.EnumProperty( name="Velocity Type", items=(('VECT', "Custom Vector", "Use direction vector as velocities"), ('ANGVEL', "Angular Velocity", "Get particles' current angular velocities (spin)"), ('PNT', "Point", "Get a direction vector pointing away from 3D cursor"), ('DIST', "Distance", "Get particles' offsets from their initial locations"), ('PVEL', "Velocity", "Get particles' current velocities"), ), default='PVEL', description="Method of obtaining velocities from the particle system", ) bpy.types.WindowManager.pvelocity_genmode = bpy.props.EnumProperty( name="Calculation Method", items=(('REF', "Reflection", "Get the reflection vector between old and new velocities"), ('CRS', "Cross Product", "Get the cross product of old and current velocities"), ('AVG', "Average", "Get the average of old and new velocities"), ('MULT', "Multiply", "Multiply current velocities with old velocities"), ('ADD', "Add", "Add new velocities to existing ones"), ('MATH', "Formula", "Use a customizable function to calculate velocities"), ('REP', "Replace", "Default - Overwrite old velocities"), ), default='REP', description="Method of combining current and saved velocities", ) bpy.types.WindowManager.pvelocity_invert = bpy.props.BoolProperty( default=False,description="Invert current velocities before saving" ) bpy.types.WindowManager.pvelocity_selection = bpy.props.BoolProperty( default=False,description="Replace selected particles' velocities only" ) bpy.types.WindowManager.pvelocity_avgratio = bpy.props.FloatProperty( default=0.5,min=0.0,max=1.0, description="The ratio between the current and new velocities" ) bpy.types.WindowManager.pvelocity_dirvector = bpy.props.FloatVectorProperty( default=(0.0,0.0,1.0), subtype='TRANSLATION', unit='NONE', min=-100.0, max=100.0, description="Vector to set all velocities to" ) # curve force bpy.types.WindowManager.curveForce_strength = bpy.props.FloatProperty( default=8.0,description="The power of each wind force along the curve" ) bpy.types.WindowManager.curveForce_maxDist = bpy.props.FloatProperty( default=4.0,description="Maximum influence distance for wind forces" ) bpy.types.WindowManager.curveForce_falloffPower = bpy.props.FloatProperty( default=2.0,description="Distance falloff for wind forces" ) bpy.types.WindowManager.curveForce_trailout = bpy.props.BoolProperty( default=False,description="Fade the size and influence of the wind forces along the curve" ) # display bpy.types.WindowManager.vf_showingvelocitylines = bpy.props.IntProperty(default=-1) bpy.types.WindowManager.vf_velocitylinescolor = bpy.props.FloatVectorProperty( default=(1.0,1.0,1.0),min=0.25,subtype='COLOR', description="Line Color" ) # toggle vars for panel bpy.types.WindowManager.show_createpanel = bpy.props.BoolProperty( default=False,description="Toggle Section" ) bpy.types.WindowManager.show_editpanel = bpy.props.BoolProperty( default=False,description="Toggle Section" ) bpy.types.WindowManager.show_displaypanel = bpy.props.BoolProperty( default=False,description="Toggle Section" ) bpy.types.WindowManager.show_toolspanel = bpy.props.BoolProperty( default=False,description="Toggle Section" ) bpy.types.WindowManager.show_windcurvetool = bpy.props.BoolProperty( default=False,description="Toggle Section" ) def clearvars(): props = [ 'vf_density','vf_scale','vf_gravity','vf_particleLifetime','pvelocity_veltype','pvelocity_genmode', 'pvelocity_invert','pvelocity_selection','pvelocity_avgratio','pvelocity_dirvector', 'curveForce_strength','curveForce_maxDist','curveForce_falloffPower','curveForce_trailout','curveForce_dispSize' 'vf_showingvelocitylines','vf_velocitylinescolor', 'show_createpanel','show_editpanel','show_displaypanel','show_toolspanel','show_windcurvetool' ] for p in props: if bpy.context.window_manager.get(p) != None: del bpy.context.window_manager[p] try: x = getattr(bpy.types.WindowManager, p) del x except: pass classes = ( vector_field, Presets_VFCreate, Preset_VFCreate, Presets_VFCreate_Fluid, Preset_VFCreate_Fluid, vf_editor.calc_vectorfieldvelocities, vf_editor.VFTOOLS_OT_create_vectorfield, vf_editor.calc_curvewindforce, vf_editor.edit_curvewindforce, vf_editor.toggle_vectorfieldvelocities, vf_editor.vf_normalizevelocities, vf_editor.vf_invertvelocities, export_vectorfieldfile, import_vectorfieldfile, VFTOOLS_PT_menupanel, #exportmenu_func, #importmenu_func, ) def register(): from bpy.utils import register_class for cls in classes: register_class(cls) initdefaults() bpy.types.TOPBAR_MT_file_export.append(exportmenu_func) bpy.types.TOPBAR_MT_file_import.append(importmenu_func) def unregister(): bpy.types.TOPBAR_MT_file_export.remove(exportmenu_func) bpy.types.TOPBAR_MT_file_import.remove(importmenu_func) from bpy.utils import unregister_class for cls in reversed(classes): unregister_class(cls) clearvars() if __name__ == '__main__': register() ================================================ FILE: FGA_VectorFields/vf_editor.py ================================================ ### Editor Functions import bpy import gpu from mathutils import Vector, Matrix from gpu_extras.batch import batch_for_shader ### Create # Creates a new vector field from parameters def build_vectorfield(context): zeroVect = Vector((0.0,0.0,0.0)) densityVal = Vector(context.window_manager.vf_density) scaleVal = Vector(context.window_manager.vf_scale) baseLoc = -1.0 * (densityVal * 0.5) + Vector((0.5, 0.5, 0.5)) totalvertscount = densityVal[0] * densityVal[1] * densityVal[2] vf_startlocs = [zeroVect.copy() for v in range(int(totalvertscount))] vf_velocities = [zeroVect.copy() for v in range(int(totalvertscount))] volcount = 0 for v in range(len(context.scene.objects)): if ("VF_Volume" in str(context.scene.objects[v].name)): volcount += 1 # create the volume me = bpy.data.meshes.new("Vert") me.vertices.add(totalvertscount) from bpy_extras import object_utils object_utils.object_data_add(context, me, operator=None) volMesh = context.active_object volMesh.name = 'VF_Volume_' + str(volcount) volMesh.display.show_shadows = False volMesh.location = zeroVect # add the particle system bpy.ops.object.particle_system_add() degp = bpy.context.evaluated_depsgraph_get() particle_systems = volMesh.evaluated_get(degp).particle_systems psettings = particle_systems[0].settings #me = volMesh.data #me.update() meshverts = [v for v in me.vertices] volMesh.vf_object_density = densityVal volMesh.vf_object_scale = scaleVal # create vertices + initialize velocities list xval = int(densityVal[0]) yval = int(densityVal[1]) zval = int(densityVal[2]) tempV = zeroVect.copy() counter = 0 for i in range(zval): for j in range(yval): for k in range(xval): tempV[0] = (baseLoc[0] + (k)) * scaleVal[0] tempV[1] = (baseLoc[1] + (j)) * scaleVal[1] tempV[2] = (baseLoc[2] + (i)) * scaleVal[2] meshverts[counter].co = tempV vf_startlocs[counter][0] = tempV[0] vf_startlocs[counter][1] = tempV[1] vf_startlocs[counter][2] = tempV[2] counter += 1 me.update() del meshverts[:] # create the particle system psettings.count = totalvertscount psettings.emit_from = 'VERT' psettings.normal_factor = 0.0 psettings.use_emit_random = False psettings.frame_end = 1 psettings.lifetime = context.window_manager.vf_particleLifetime psettings.grid_resolution = 1 psettings.use_rotations = True psettings.use_dynamic_rotation = True psettings.effector_weights.gravity = context.window_manager.vf_gravity # create the bounding box bpy.ops.mesh.primitive_cube_add(location=(0.0,0.0,0.0)) boundsMesh = context.active_object boundsMesh.name = 'VF_Bounds_' + str(volcount) boundsMesh.display.show_shadows = False # match scale to the volume boundsMesh.scale[0] = (densityVal[0] * 0.5) * scaleVal[0] boundsMesh.scale[1] = (densityVal[1] * 0.5) * scaleVal[1] boundsMesh.scale[2] = (densityVal[2] * 0.5) * scaleVal[2] bpy.ops.object.transform_apply(scale=True) bpy.ops.object.mode_set(mode='EDIT') bpy.ops.mesh.delete(type='ONLY_FACE') bpy.ops.object.mode_set(mode='OBJECT') volMesh.parent = boundsMesh if len(vf_velocities) == len (vf_startlocs): for i in range(len(vf_velocities)): tempvertdata = volMesh.custom_vectorfield.add() tempvertdata.vcoord = Vector(vf_startlocs[i]) tempvertdata.vvelocity = Vector(vf_velocities[i]) else: print ("VectorField coords/velocities length mismatch!") del vf_velocities[:] del vf_startlocs[:] tempconstraint = volMesh.constraints.new(type='COPY_TRANSFORMS') tempconstraint.target = volMesh.parent return volMesh.name class VFTOOLS_OT_create_vectorfield(bpy.types.Operator): bl_idname = 'vftools.create_vectorfield' bl_label = 'Create VectorField' bl_description = 'Create a new vector field from resolution and scale values' bl_options = {'REGISTER', 'UNDO'} def execute(self, context): build_vectorfield(context) return {'FINISHED'} # Performs vector math + writes results to data class calc_vectorfieldvelocities(bpy.types.Operator): bl_idname = 'object.calc_vectorfieldvelocities' bl_label = 'Save VF EndLocations' bl_description = 'Calculate and save velocities' bl_options = {'REGISTER', 'UNDO'} @classmethod def poll(cls, context): return (context.mode == "OBJECT" and context.active_object != None) and 'VF_Volume_' in context.active_object.name def execute(self, context): invmult = -1.0 if context.window_manager.pvelocity_invert else 1.0 useselection = context.window_manager.pvelocity_selection particleslist = [] volmesh = context.active_object vf_velocities = [Vector(v.vvelocity) for v in volmesh.custom_vectorfield] degp = bpy.context.evaluated_depsgraph_get() particle_systems = volmesh.evaluated_get(degp).particle_systems ## Get velocities if context.window_manager.pvelocity_veltype == "VECT": tempvect = Vector(context.window_manager.pvelocity_dirvector) particleslist = [tempvect.copy() for v in vf_velocities] elif context.window_manager.pvelocity_veltype == "DIST": vf_startlocs = [Vector(v.vcoord) for v in volmesh.custom_vectorfield] vf_endLocs = [v.location for v in particle_systems[0].particles] particleslist = [(vf_endLocs[i] - vf_startlocs[i]) for i in range(len(vf_endLocs))] del vf_startlocs[:] del vf_endLocs[:] elif context.window_manager.pvelocity_veltype == "ANGVEL": particleslist = [p.angular_velocity for p in particle_systems[0].particles] elif context.window_manager.pvelocity_veltype == "PNT": cursorloc = context.scene.cursor.location particleslist = [(Vector(v.vcoord) - cursorloc).normalized() for v in volmesh.custom_vectorfield] else: particleslist = [p.velocity for p in particle_systems[0].particles] mvertslist = [] if useselection: me = volmesh.data mvertslist = tuple(v.select for v in me.vertices) ## Blend with List / calculate # multiply if context.window_manager.pvelocity_genmode == 'MULT': if useselection: for i in range(len(particleslist)): if mvertslist[i]: vf_velocities[i] = Vector( (vf_velocities[i][0] * (particleslist[i][0] * invmult), vf_velocities[i][1] * (particleslist[i][1] * invmult), vf_velocities[i][2] * (particleslist[i][2] * invmult)) ) else: for i in range(len(particleslist)): vf_velocities[i] = Vector( (vf_velocities[i][0] * (particleslist[i][0] * invmult), vf_velocities[i][1] * (particleslist[i][1] * invmult), vf_velocities[i][2] * (particleslist[i][2] * invmult)) ) # add elif context.window_manager.pvelocity_genmode == 'ADD': if useselection: for i in range(len(particleslist)): if mvertslist[i]: vf_velocities[i] = vf_velocities[i] + ((particleslist[i]) * invmult) else: for i in range(len(particleslist)): vf_velocities[i] = vf_velocities[i] + ((particleslist[i]) * invmult) # average elif context.window_manager.pvelocity_genmode == 'AVG': avgratio = context.window_manager.pvelocity_avgratio if useselection: for i in range(len(particleslist)): if mvertslist[i]: vf_velocities[i] = ((vf_velocities[i] * (1.0 - avgratio)) + ((particleslist[i] * invmult) * avgratio)) else: for i in range(len(particleslist)): vf_velocities[i] = ((vf_velocities[i] * (1.0 - avgratio)) + ((particleslist[i] * invmult) * avgratio)) # replace elif context.window_manager.pvelocity_genmode == 'REP': if useselection: for i in range(len(particleslist)): if mvertslist[i]: vf_velocities[i] = particleslist[i] * invmult else: for i in range(len(particleslist)): vf_velocities[i] = particleslist[i] * invmult # cross product elif context.window_manager.pvelocity_genmode == 'CRS': if useselection: for i in range(len(particleslist)): if mvertslist[i]: vf_velocities[i] = vf_velocities[i].cross(particleslist[i]) else: for i in range(len(particleslist)): vf_velocities[i] = vf_velocities[i].cross(particleslist[i]) # reflection elif context.window_manager.pvelocity_genmode == 'REF': if useselection: for i in range(len(particleslist)): if mvertslist[i]: vf_velocities[i] = vf_velocities[i].reflect(particleslist[i]) else: for i in range(len(particleslist)): vf_velocities[i] = vf_velocities[i].reflect(particleslist[i]) # write new velocities for i in range(len(vf_velocities)): volmesh.custom_vectorfield[i].vvelocity = vf_velocities[i].copy() del particleslist[:] del vf_velocities[:] context.window_manager.vf_showingvelocitylines = -1 return {'FINISHED'} # Normalizes the list class vf_normalizevelocities(bpy.types.Operator): bl_idname = 'object.vf_normalizevelocities' bl_label = 'Normalize' bl_description = 'Normalizes the currently saved velocity list' bl_options = {'REGISTER', 'UNDO'} @classmethod def poll(cls, context): return context.active_object != None and 'VF_Volume_' in context.active_object.name def execute(self, context): volmesh = context.active_object for i in range(len(volmesh.custom_vectorfield)): tempVect = Vector(volmesh.custom_vectorfield[i].vvelocity) volmesh.custom_vectorfield[i].vvelocity = tempVect.normalized() context.window_manager.vf_showingvelocitylines = -1 return {'FINISHED'} # Inverts the list class vf_invertvelocities(bpy.types.Operator): bl_idname = 'object.vf_invertvelocities' bl_label = 'Invert All' bl_description = 'Inverts the currently saved velocity list' bl_options = {'REGISTER', 'UNDO'} @classmethod def poll(cls, context): return context.active_object != None and 'VF_Volume_' in context.active_object.name def execute(self, context): volmesh = context.active_object for i in range(len(volmesh.custom_vectorfield)): volmesh.custom_vectorfield[i].vvelocity[0] *= -1.0 volmesh.custom_vectorfield[i].vvelocity[1] *= -1.0 volmesh.custom_vectorfield[i].vvelocity[2] *= -1.0 context.window_manager.vf_showingvelocitylines = -1 return {'FINISHED'} # Tools: # Curve Wind Force: # creates a wind tunnel from selected curve object class calc_curvewindforce(bpy.types.Operator): bl_idname = 'object.calc_curvewindforce' bl_label = 'Curve Wind force' bl_description = 'create wind forces along a spline to direct velocities along it' bl_options = {'REGISTER', 'UNDO'} @classmethod def poll(cls, context): return (context.mode == "OBJECT" and context.active_object != None) and context.active_object.type == 'CURVE' def execute(self, context): curvepoints = [] curveobj = context.active_object bpy.ops.object.empty_add(type='PLAIN_AXES') parentobj = context.active_object parentobj.name = 'CurveForce' if len(curveobj.data.splines[0].bezier_points) > 1: curvepoints = [v.co for v in curveobj.data.splines[0].bezier_points] else: curvepoints = [v.co for v in curveobj.data.splines[0].points] curveobj.parent = parentobj context.active_object.select_set(False) curveobj.select_set(True) previousnormal = Vector((0.0,0.0,0.0)) lastStrength = context.window_manager.curveForce_strength lastDistance = context.window_manager.curveForce_maxDist for i in range(len(curvepoints)): cpoint = Vector((curvepoints[i][0],curvepoints[i][1],curvepoints[i][2])) bpy.ops.object.empty_add(type='SINGLE_ARROW',location=(cpoint)) context.active_object.name = 'ForceObj' # turn into forcefield bpy.ops.object.forcefield_toggle() context.active_object.field.type = 'WIND' if context.window_manager.curveForce_trailout: if i > 0: lastStrength = lastStrength * 0.9 lastDistance = lastDistance * 0.9 context.active_object.field.strength = lastStrength context.active_object.field.use_max_distance = True context.active_object.field.distance_max = lastDistance context.active_object.field.falloff_power = context.window_manager.curveForce_falloffPower # get the curve's direction between points tempnorm = Vector((0,0,0)) if (i < len(curvepoints) - 1): cpoint2 = Vector((curvepoints[i + 1][0],curvepoints[i + 1][1],curvepoints[i + 1][2])) tempnorm = cpoint - cpoint2 if i > 0: if abs(previousnormal.length) > 0.0: tempnorm = (tempnorm + previousnormal) / 2.0 previousnormal = tempnorm else: if curveobj.data.splines[0].use_cyclic_u or curveobj.data.splines[0].use_cyclic_u: cpoint2 = Vector((curvepoints[0][0],curvepoints[0][1],curvepoints[0][2])) tempnorm = cpoint - cpoint2 if abs(previousnormal.length) > 0.0: tempnorm = (tempnorm + previousnormal) / 2.0 previousnormal = tempnorm else: cpoint2 = Vector((curvepoints[i - 1][0],curvepoints[i - 1][1],curvepoints[i - 1][2])) tempnorm = cpoint2 - cpoint if abs(previousnormal.length) > 0.0: tempnorm = (tempnorm + previousnormal) / 2.0 previousnormal = tempnorm if abs(tempnorm.length) > 0.0: z = Vector((0,0,1)) angle = tempnorm.angle(z) axis = z.cross(tempnorm) mat = Matrix.Rotation(angle, 4, axis) mat_world = context.active_object.matrix_world @ mat context.active_object.matrix_world = mat_world context.active_object.parent = parentobj return {'FINISHED'} # creates a wind tunnel from selected curve object class edit_curvewindforce(bpy.types.Operator): bl_idname = 'object.edit_curvewindforce' bl_label = 'Curve Wind force' bl_description = 'Edit settings on the selected curve wind force object' bl_options = {'REGISTER', 'UNDO'} @classmethod def poll(cls, context): if context.mode == "OBJECT" and context.active_object != None: return 'CurveForce' in context.active_object.name def execute(self, context): newStrength = context.window_manager.curveForce_strength newDistance = context.window_manager.curveForce_maxDist newFalloff = context.window_manager.curveForce_falloffPower curveforceobj = context.active_object objlist = [obj for obj in context.scene.objects if obj.parent == curveforceobj] for obj in objlist: if 'ForceObj' in obj.name: obj.field.strength = newStrength obj.field.distance_max = newDistance obj.field.falloff_power = newFalloff return {'FINISHED'} ### Display class toggle_vectorfieldinfo(bpy.types.Operator): bl_idname = "object.toggle_vectorfieldinfo" bl_label = 'Show Current VF Info' bl_description = 'Display information about the currently selected vector field' @classmethod def poll(cls, context): return context.active_object != None and 'VF_Volume_' in context.active_object.name def execute(self, context): return {'FINISHED'} # Toggle velocities display as lines class toggle_vectorfieldvelocities(bpy.types.Operator): bl_idname = "view3d.toggle_vectorfieldvelocities" bl_label = 'Show velocities' bl_description = 'Display velocities as 3D lines' _handle = None @classmethod def poll(cls, context): return context.mode == "OBJECT" and context.active_object != None and 'VF_Volume_' in context.active_object.name def modal(self, context, event): if context.area: context.area.tag_redraw() if context.window_manager.vf_showingvelocitylines == -1: bpy.types.SpaceView3D.draw_handler_remove(self._handle, 'WINDOW') context.window_manager.vf_showingvelocitylines = 0 return {"CANCELLED"} return {"PASS_THROUGH"} def invoke(self, context, event): #print ("test") if context.area.type == "VIEW_3D": if context.window_manager.vf_showingvelocitylines < 1: volmesh = context.active_object temploc = Vector((0.0,0.0,0.0)) if volmesh.parent: temploc = volmesh.parent.location vf_coords = [(Vector(v.vcoord) + temploc) for v in volmesh.custom_vectorfield] vf_velocities = [Vector(v.vvelocity) for v in volmesh.custom_vectorfield] vf_DrawVelocities = [Vector((0.0,0.0,0.0)) for i in range(len(volmesh.custom_vectorfield) * 2)] velcounter = 0 for i in range(len(vf_coords)): vf_DrawVelocities[velcounter] = vf_coords[i].copy() velcounter += 1 vf_DrawVelocities[velcounter] = vf_coords[i] + vf_velocities[i] velcounter += 1 shader = gpu.shader.from_builtin('3D_UNIFORM_COLOR') batch = batch_for_shader(shader, 'LINES', {"pos": vf_DrawVelocities}) context.window_manager.vf_showingvelocitylines = 1 color = Vector((context.window_manager.vf_velocitylinescolor[0], context.window_manager.vf_velocitylinescolor[1], context.window_manager.vf_velocitylinescolor[2], 1.0)) self._handle = bpy.types.SpaceView3D.draw_handler_add(draw_vectorfield, (shader, batch, color), 'WINDOW', 'POST_VIEW') context.window_manager.modal_handler_add(self) context.area.tag_redraw() return {"RUNNING_MODAL"} else: context.window_manager.vf_showingvelocitylines = -1 return {'RUNNING_MODAL'} else: self.report({"WARNING"}, "View3D not found, can't run operator") return {"CANCELLED"} # draw lines def draw_vectorfield(shader, batch, color): shader.bind() shader.uniform_float("color", color) batch.draw(shader) ================================================ FILE: FGA_VectorFields/vf_io.py ================================================ ### Import/Export Functions import bpy import os.path from mathutils import Vector ### Import # create new vector field from imported data def build_importedVectorField(tempvelList, tempOffset): # create blank vf from . import vf_editor volname = vf_editor.build_vectorfield(bpy.context) volmesh = bpy.context.scene.objects[volname] # copy imported velocities for i in range(len(tempvelList)): volmesh.custom_vectorfield[i].vvelocity = tempvelList[i] if volmesh.parent: volmesh.parent.location = volmesh.parent.location + tempOffset else: volmesh.location = volmesh.location + tempOffset # read data from file def parse_fgafile(self, context): returnmessage = "" fgafilepath = self.filepath if os.path.exists(fgafilepath): if os.path.isfile(fgafilepath): file = open(fgafilepath, 'r') importvf_scalemult = self.importvf_scalemult linecount = 0 tempvelList = [] tempMin = Vector((0.0,0.0,0.0)) tempOffset = Vector((0.0,0.0,0.0)) tempscalemult = Vector((0.0,0.0,0.0)) for line in file: slist = [] slist = line.split(',') if len(slist) > 3: slist.remove(slist[3]) flist = [float(s) for s in slist] if linecount <= 2: if linecount == 0: # Resolution context.window_manager.vf_density[0] = int(flist[0]) context.window_manager.vf_density[1] = int(flist[1]) context.window_manager.vf_density[2] = int(flist[2]) elif linecount == 1: # Min bounds tempMin[0] = flist[0] tempMin[1] = flist[1] tempMin[2] = flist[2] elif linecount == 2: # Max bounds, calc offset + scale tempscalemult = Vector((0.0,0.0,0.0)) tempscalemult[0] = abs(flist[0] - tempMin[0]) tempscalemult[1] = abs(flist[1] - tempMin[1]) tempscalemult[2] = abs(flist[2] - tempMin[2]) context.window_manager.vf_scale[0] = (tempscalemult[0] / context.window_manager.vf_density[0]) * importvf_scalemult context.window_manager.vf_scale[1] = (tempscalemult[1] / context.window_manager.vf_density[1]) * importvf_scalemult context.window_manager.vf_scale[2] = (tempscalemult[2] / context.window_manager.vf_density[2]) * importvf_scalemult if self.importvf_getoffset: tempOffset[0] = (((tempMin[0] + (tempscalemult[0] * 0.5)) * importvf_scalemult)) tempOffset[1] = (((tempMin[1] + (tempscalemult[1] * 0.5)) * importvf_scalemult)) tempOffset[2] = (((tempMin[2] + (tempscalemult[2] * 0.5)) * importvf_scalemult)) else: # Velocities if self.importvf_velscale: tempvelList.append(Vector((flist[0] * importvf_scalemult,flist[1] * importvf_scalemult,flist[2] * importvf_scalemult))) else: tempvelList.append(Vector(flist)) linecount += 1 if linecount < 3: returnmessage = "Import Failed: File is missing data" else: if len(tempvelList) > 0: returnmessage = "Import Successful" build_importedVectorField(tempvelList, tempOffset) file.close() else: returnmessage = "Import Failed: File not found" else: returnmessage = "Import Failed: Path not found" return returnmessage ### Export def write_fgafile(self, exportvol): usevelscale = self.exportvf_velscale useoffset = self.exportvf_locoffset tempDensity = Vector(exportvol.vf_object_density) fgascale = Vector(exportvol.vf_object_scale) file = open(self.filepath, "w", encoding="utf8", newline="\n") fw = file.write # Resolution: fw("%i,%i,%i," % (tempDensity[0],tempDensity[1],tempDensity[2])) # Minimum/Maximum Bounds: if self.exportvf_allowmanualbounds: fw("\n%f,%f,%f," % ( self.exportvf_manualboundsneg[0], self.exportvf_manualboundsneg[1], self.exportvf_manualboundsneg[2]) ) fw("\n%f,%f,%f," % ( self.exportvf_manualboundspos[0], self.exportvf_manualboundspos[1], self.exportvf_manualboundspos[2]) ) else: if useoffset: offsetvect = Vector((0.0,0.0,0.0)) if exportvol.parent: offsetvect = exportvol.parent.location fw("\n%f,%f,%f," % ( (((tempDensity[0] * -0.5) * fgascale[0]) + (offsetvect[0])) * self.exportvf_scale, (((tempDensity[1] * -0.5) * fgascale[1]) + (offsetvect[1])) * self.exportvf_scale, (((tempDensity[2] * -0.5) * fgascale[2]) + (offsetvect[2])) * self.exportvf_scale) ) fw("\n%f,%f,%f," % ( (((tempDensity[0] * 0.5) * fgascale[0]) + (offsetvect[0])) * self.exportvf_scale, (((tempDensity[1] * 0.5) * fgascale[1]) + (offsetvect[1])) * self.exportvf_scale, (((tempDensity[2] * 0.5) * fgascale[2]) + (offsetvect[2])) * self.exportvf_scale) ) else: # centered fw("\n%f,%f,%f," % ( ((tempDensity[0] * -0.5) * fgascale[0]) * self.exportvf_scale, ((tempDensity[1] * -0.5) * fgascale[1]) * self.exportvf_scale, ((tempDensity[2] * -0.5) * fgascale[2]) * self.exportvf_scale) ) fw("\n%f,%f,%f," % ( ((tempDensity[0] * 0.5) * fgascale[0]) * self.exportvf_scale, ((tempDensity[1] * 0.5) * fgascale[1]) * self.exportvf_scale, ((tempDensity[2] * 0.5) * fgascale[2]) * self.exportvf_scale) ) # Velocities if usevelscale and not self.exportvf_allowmanualbounds: for vec in exportvol.custom_vectorfield: fw("\n%f,%f,%f," % ( vec.vvelocity[0] * self.exportvf_scale, vec.vvelocity[1] * self.exportvf_scale, vec.vvelocity[2] * self.exportvf_scale) ) else: if self.exportvf_allowmanualbounds: for vec in exportvol.custom_vectorfield: fw("\n%f,%f,%f," % ( vec.vvelocity[0] * self.exportvf_manualvelocityscale, vec.vvelocity[1] * self.exportvf_manualvelocityscale, vec.vvelocity[2] * self.exportvf_manualvelocityscale) ) else: for vec in exportvol.custom_vectorfield: fw("\n%f,%f,%f," % ( vec.vvelocity[0], vec.vvelocity[1], vec.vvelocity[2]) ) file.close() ================================================ FILE: README.md ================================================ Blender - FGA Vector Field Editor ======================================= Allows creation and manipulation of vector fields using Blender particle simulations and vector math operations, as well as importing/exporting FGA files used by Unreal Engine 4. - Requires Blender 2.8 and up. - The menu panel can be found in the Panels section in the right part of the workspace. The documentation hasn't been updated to reflect this yet. ------------------------------------------------------------------------------------------------------- **Out-of-date Documentation is available here**: https://github.com/isathar/Blender_UE4_VectorFieldEditor/wiki (or the wiki link on the side) - Very out of date, will be updated as soon as time permits. Some example .fga files: http://www.mediafire.com/download/4x174fgf8lmec6g/VF_Examples.zip ------------------------------------------------------------------------------------------------------- ## Features **Vector Field Editor:** - Saves current particle system velocities and blends them with saved results using one of the following methods: - *Replace, Average, Add, Multiply, Cross Product, Vector Reflection* - Particle velocities used in these calculations can be obtained using the following methods: - *Velocity, Offset Distance, Angular Velocity, Custom Vector, Point* - Curve Force tool that uses wind forces to move particles along a line. **Importer + Exporter** for FGA files for use in Unreal Engine 4 ------------------------------------------------------------------------------------------------------- ## Installation - Extract to your addons directory - Enable it in the addon manager (named *FGA Vector Field Tools*) - A new tab named *Vector Fields* should be visible in the tools panel --------------------------------------------------------------------------------------------------------- ## Notes ###### Performance - This addon's performance is heavily dependent on the speed of your CPU and memory. - Blender may stop responding during the Create and Calculate operations, but shouldn't crash. - On vector fields with a density of less than 128^3, operations should take less than a minute, with lower density fields (<64^3) taking a few seconds at most. - At maximum density (128^3), creating a new vector field takes about 20 seconds on my mid-range Core i5 based PC, and calculating velocities can take up to 2 minutes (after recent tweaks). - Performance while editing reasonably sized (< 1 million vertices) vector fields is good, while a 128^3 volume can be painfully slow under the right (wrong) circumstances. ###### 128x128x128 and System Memory - Editing a 128x128x128 vector field requires a 64-bit system and Blender version, as well as a large amount (> 6-8 GB) of system memory. - This is due to the amount of particles that need to have their dynamics cached (and the number of vertices in the volume mesh) - To avoid running out of memory while editing very high resolution vector fields, you may want to lower your undo history steps. - Using a disk cache for your particles may help, too. ------------------------------------------------------------------------------------------------------- ## Changelog: ***1.2.1***: - probably fixed noneType error messages *(Thank you to kkaja123 for reporting and looking into fixing it!)* - moved the menu to the panels at the right side of the workspace ***1.2.0***: - initial update to support Blender 2.8. ***1.1.5***: - changed sorting of the calculation/velocity dropdown lists - new calculation method: Vector Reflection - added constraint to vector field volume, only moving the bounding box now moves the volume ***1.1.4***: - fix for display lines not updating when new velocities are calculated - new calculation method: Cross Product ***1.1.3***: - removed the need for the update data/offsets buttons - slight performance optimizations for display lines ***1.1.2***: - new presets can now be added/removed - export should now work with any part of the vector field selected (or both) ***1.1.0***: - added support for selecting a physics preset to edit particle physics settings easily for selected vector field ***1.0.1***: - renamed curve path tool to Wind Curve Force - added editor for created curve wind force strength, distance + falloff - the Ratio property should now work (apparently forgot to use the variable) ***1.0.0***: - *General:* - tools panel category renamed to *Vector Fields* - saved data only includes velocities now (removed position, index) - files made with old versions are still compatible - cleaned up ui panel to reduce clutter, added section toggles - some code cleanup - *Import/Export:* - moved import/export to standard menu - made import/export properties local to their functions - *Editor:* - performance tweaks for creating new vector fields + calculating velocities - removed slice selection tool (redundant, easily done in edit mode) - matched default scaling to (grid units x field density) - distances were at half scale before - density variable used for creation is now distance between particles - added undo functionality to *Generate* function - new velocity mode: Point - *Curve Force Tool:* - changed curve force tool to create an object group to remove scene outliner clutter - fixed curve force fields' parenting issue - all transformations to the curve force object should now work - curve forces now display an arrow pointing in the force's direction ***0.9.5***: - *Editor:* - added new calculation method: Multiply - added different methods for obtaining velocities - reorganized the main editor - added calculate for selection - added invert all button - seperated normalize function from calculation - it's a button again - *Import/Export:* - moved density variable to object space for export script - allows multiple vector fields in the scene during export (still exports one at a time) - added ability to use object locations as offsets + import/export them - scaling tweaks - manual bounds option - *General:* - added ability to undo slice selection, calculation, curve tool, and normalize - select x,y,z slice - created index by axis for velocities list for slice selection and upcoming features - switched bpy.context to passed context where possible - description text for all variables + operators (some may be vague) - added bug reporting url to addon manager (Github) - readme formatting ***0.9.1***: - added different generation modes: *Replace, Additive, Average* - added trail option for curve path (fade influence with curve position) - changed the way invert and normalize work - slight calculation performance tweak ***0.9***: - another performance tweak - added invert, normalize, disable gravity options ***0.8***: - added import functionality - massive speed improvement ***0.5***: - initial upload