master b24f207c9142 cached
5 files
47.2 KB
13.0k tokens
52 symbols
1 requests
Download .txt
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  
Download .txt
gitextract_6jzh4irb/

├── .github/
│   └── FUNDING.yml
├── FGA_VectorFields/
│   ├── __init__.py
│   ├── vf_editor.py
│   └── vf_io.py
└── README.md
Download .txt
SYMBOL INDEX (52 symbols across 3 files)

FILE: FGA_VectorFields/__init__.py
  class VFTOOLS_PT_menupanel (line 22) | class VFTOOLS_PT_menupanel(bpy.types.Panel):
    method __init__ (line 29) | def __init__(self):
    method poll (line 33) | def poll(self, context):
    method draw (line 36) | def draw(self, context):
  class vector_field (line 124) | class vector_field(bpy.types.PropertyGroup):
  class export_vectorfieldfile (line 130) | class export_vectorfieldfile(bpy.types.Operator, ExportHelper):
    method check_extension (line 170) | def check_extension(self):
    method check (line 173) | def check(self, context):
    method draw (line 177) | def draw(self,context):
    method execute (line 190) | def execute(self, context):
  class import_vectorfieldfile (line 216) | class import_vectorfieldfile(bpy.types.Operator, ImportHelper):
    method draw (line 237) | def draw(self,context):
    method execute (line 250) | def execute(self, context):
  class Preset_VFCreate (line 257) | class Preset_VFCreate(AddPresetBase, bpy.types.Operator):
  class Preset_VFCreate_Fluid (line 281) | class Preset_VFCreate_Fluid(AddPresetBase, bpy.types.Operator):
  class Presets_VFCreate (line 312) | class Presets_VFCreate(bpy.types.Menu):
  class Presets_VFCreate_Fluid (line 320) | class Presets_VFCreate_Fluid(bpy.types.Menu):
  function exportmenu_func (line 328) | def exportmenu_func(self, context):
  function importmenu_func (line 332) | def importmenu_func(self, context):
  function initdefaults (line 337) | def initdefaults():
  function clearvars (line 435) | def clearvars():
  function register (line 476) | def register():
  function unregister (line 486) | def unregister():

FILE: FGA_VectorFields/vf_editor.py
  function build_vectorfield (line 14) | def build_vectorfield(context):
  class VFTOOLS_OT_create_vectorfield (line 124) | class VFTOOLS_OT_create_vectorfield(bpy.types.Operator):
    method execute (line 130) | def execute(self, context):
  class calc_vectorfieldvelocities (line 138) | class calc_vectorfieldvelocities(bpy.types.Operator):
    method poll (line 145) | def poll(cls, context):
    method execute (line 148) | def execute(self, context):
  class vf_normalizevelocities (line 271) | class vf_normalizevelocities(bpy.types.Operator):
    method poll (line 278) | def poll(cls, context):
    method execute (line 281) | def execute(self, context):
  class vf_invertvelocities (line 290) | class vf_invertvelocities(bpy.types.Operator):
    method poll (line 297) | def poll(cls, context):
    method execute (line 300) | def execute(self, context):
  class calc_curvewindforce (line 315) | class calc_curvewindforce(bpy.types.Operator):
    method poll (line 322) | def poll(cls, context):
    method execute (line 325) | def execute(self, context):
  class edit_curvewindforce (line 403) | class edit_curvewindforce(bpy.types.Operator):
    method poll (line 410) | def poll(cls, context):
    method execute (line 414) | def execute(self, context):
  class toggle_vectorfieldinfo (line 436) | class toggle_vectorfieldinfo(bpy.types.Operator):
    method poll (line 442) | def poll(cls, context):
    method execute (line 445) | def execute(self, context):
  class toggle_vectorfieldvelocities (line 450) | class toggle_vectorfieldvelocities(bpy.types.Operator):
    method poll (line 458) | def poll(cls, context):
    method modal (line 461) | def modal(self, context, event):
    method invoke (line 471) | def invoke(self, context, event):
  function draw_vectorfield (line 512) | def draw_vectorfield(shader, batch, color):

FILE: FGA_VectorFields/vf_io.py
  function build_importedVectorField (line 10) | def build_importedVectorField(tempvelList, tempOffset):
  function parse_fgafile (line 25) | def parse_fgafile(self, context):
  function write_fgafile (line 96) | def write_fgafile(self, exportvol):
Condensed preview — 5 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (53K chars).
[
  {
    "path": ".github/FUNDING.yml",
    "chars": 17,
    "preview": "patreon: isathar\n"
  },
  {
    "path": "FGA_VectorFields/__init__.py",
    "chars": 18480,
    "preview": "bl_info = {\n\t\"name\": \"Vector Field Tools\",\n\t\"author\": \"Andreas Wiehn (isathar)\",\n\t\"version\": (1, 2, 1),\n\t\"blender\": (2, "
  },
  {
    "path": "FGA_VectorFields/vf_editor.py",
    "chars": 17076,
    "preview": "### Editor Functions\n\nimport bpy\nimport gpu\n\nfrom mathutils import Vector, Matrix\n\nfrom gpu_extras.batch import batch_fo"
  },
  {
    "path": "FGA_VectorFields/vf_io.py",
    "chars": 5771,
    "preview": "### Import/Export Functions\n\nimport bpy\nimport os.path\nfrom mathutils import Vector\n\n### Import\n\n# create new vector fie"
  },
  {
    "path": "README.md",
    "chars": 7003,
    "preview": "Blender - FGA Vector Field Editor\n=======================================\n\nAllows creation and manipulation of vector fi"
  }
]

About this extraction

This page contains the full source code of the isathar/Blender_UE4_VectorFieldEditor GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 5 files (47.2 KB), approximately 13.0k tokens, and a symbol index with 52 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!