Skip to content

Commit

Permalink
added particle export
Browse files Browse the repository at this point in the history
  • Loading branch information
thomatoes50 committed Nov 28, 2022
1 parent 81f5ca4 commit 6cbe5dc
Show file tree
Hide file tree
Showing 2 changed files with 173 additions and 10 deletions.
152 changes: 150 additions & 2 deletions io_scene_forest/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
bl_info = {
"name": "BeamNG forest item (*.forest4.json)",
"author": "BeamNG / dmn",
"version": (0, 1, 1),
"version": (0, 2, 0),
"blender": (2, 80, 0),
"location": "File > Import-Export",
"description": "Import-Export forest files",
Expand All @@ -30,6 +30,7 @@
ImportHelper,
ExportHelper,
)
import os

#this is needed to force refresh of changed file
if "bpy" in locals() and "import_forest" in locals():
Expand Down Expand Up @@ -137,9 +138,151 @@ def execute(self, context):

return {'FINISHED'}

class PROPERTIES_PG_forest_particle(bpy.types.PropertyGroup):
export_path: bpy.props.StringProperty(
name="Export file path",
description="",
subtype='FILE_PATH')
fi_name: bpy.props.StringProperty(
name="Forest item name",
description="")

def particle_get_settings(context):
if context.particle_system:
return context.particle_system.settings
elif isinstance(context.space_data.pin_id, bpy.types.ParticleSettings):
return context.space_data.pin_id
return None

class ExportParticleForest(bpy.types.Operator):
"""Yes"""
bl_idname = "export_particle.forest"
bl_label = "Export particle forest"

@classmethod
def poll(cls, context):
psys = context.particle_system
pset = particle_get_settings(context)
return psys is not None and pset is not None

def execute(self, context):
degp = bpy.context.evaluated_depsgraph_get()
particle_systems = context.active_object.evaluated_get(degp).particle_systems
psys = particle_systems[context.particle_system.name] or None
pset = particle_get_settings(context)
if psys is not None and pset is not None:
with open(bpy.path.abspath(pset.forest.export_path), "w") as f:
export_forest.export_forest(f, pset.forest.fi_name, psys.particles)
return {'FINISHED'}
else:
print("psys:",psys)
print("pset:",pset)
self.report({'ERROR'}, 'ERROR : Could not get particle system or setting')
return {"CANCELLED"}

class PANEL_PT_forest_particle(bpy.types.Panel):
bl_label = "BNG Forest Properties"
bl_space_type = 'PROPERTIES'
bl_region_type = 'WINDOW'
bl_context = "particle"
bl_parent_id = "PARTICLE_PT_context_particles"

@classmethod
def poll(cls, context):
psys = context.particle_system
pset = particle_get_settings(context)
return psys is not None and pset is not None

def draw(self, context):
layout = self.layout
layout.use_property_split = True # Active single-column layout
layout.use_property_decorate = False

# particles are always empty because .....
#psys = context.particle_system

#https://devtalk.blender.org/t/manipulating-particles-in-python/7552/2
# Dependancy graph
degp = bpy.context.evaluated_depsgraph_get()
particle_systems = context.active_object.evaluated_get(degp).particle_systems
psys = particle_systems[context.particle_system.name] or None
# print("ctx name:",context.particle_system.name)
# print("psys is none : ", psys == None)

pset = particle_get_settings(context)

row = layout.row()
if not context.active_object.type == "MESH":
row.label(text='Non-mesh objects are not compatible with the exporter.',
icon='ERROR')
return
if not pset:
row.label(text='ParticleSetting not available',
icon='ERROR')
return
if psys and pset:
fo = None
try:
fo = pset.forest
except AttributeError:
# print("create")
bpy.types.ParticleSettings.forest = bpy.props.PointerProperty(type=PROPERTIES_PG_forest_particle)
fo = pset.forests
# print("fo :", fo)
row.prop(fo, "export_path")
row = layout.row()
row.prop(fo, "fi_name")

error = False

oprow = layout.row()
oprow.operator(ExportParticleForest.bl_idname,
text="Export",
icon='SCENE_DATA')

if len(pset.forest.fi_name) == 0:
row = layout.row()
row.label(text='Forest Item cannot be an empty string',
icon='ERROR')
error = error or True

if len(pset.forest.export_path) == 0 or os.path.isdir(bpy.path.abspath(pset.forest.export_path)):
row = layout.row()
row.label(text='Export path is invalid',
icon='ERROR')
error = error or True

if not pset.forest.export_path.endswith(".forest4.json"):
row = layout.row()
row.label(text='Wrong file extention (*.forest4.json)',
icon='ERROR')
#error = error or True ## not critical

if len(psys.particles)==0:
row = layout.row()
row.label(text='NO PARTICLES',
icon='ERROR')
error = error or True

oprow.enabled = not error

# DEBUG
# row = layout.row()
# row.label(text=f'child_particles = {len(psys.child_particles)}')
# row = layout.row()
# row.label(text=f'particles = {len(psys.particles)}')
# row = layout.row()
# row.label(text=f'targets = {len(psys.targets)}')




addon_classes = [ExportForest,
ImportForest,
SCENE_OT_instance
SCENE_OT_instance,
PROPERTIES_PG_forest_particle,
PANEL_PT_forest_particle,
ExportParticleForest
]

# Add to a menu
Expand All @@ -156,13 +299,18 @@ def register():
bpy.types.TOPBAR_MT_file_export.append(menu_func_export)
bpy.types.TOPBAR_MT_file_import.append(menu_func_import)

def make_pointer(prop_type):
return bpy.props.PointerProperty(type=prop_type)
bpy.types.ParticleSettings.forest = make_pointer(PROPERTIES_PG_forest_particle)


def unregister():
for c in addon_classes:
bpy.utils.unregister_class(c)

bpy.types.TOPBAR_MT_file_export.remove(menu_func_export)
bpy.types.TOPBAR_MT_file_import.remove(menu_func_import)
del bpy.types.ParticleSettings.forest

if __name__ == "__main__":
register()
31 changes: 23 additions & 8 deletions io_scene_forest/export_forest.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,20 +13,34 @@
######################################################
def export_forest(file, object_name, data_source):
items = []
print("exporting items :",len(data_source))
loc = [0.0,0.0,0.0]
for ob in data_source:
uniform_scale = (ob.scale[0] + ob.scale[1] + ob.scale[2]) / 3
if isinstance(ob , bpy.types.Particle):
uniform_scale = ob.size
object_quaternion = ob.rotation.copy()
e_rot = mathutils.Euler(( 0.0, 0.0, math.radians(90.0)), 'XYZ')
object_quaternion = object_quaternion @ e_rot.to_quaternion()

# get euler and rotate 180 deg
object_euler = ob.rotation_euler.copy()
object_euler.rotate_axis('Z', math.radians(180))
object_quaternion = object_euler.to_quaternion()
if len(ob.hair_keys) > 0:
loc = ob.hair_keys[0].co
else:
loc = ob.location

items.append('{"type":"' + object_name + '","pos":[' + str(ob.location[0]) + ',' + str(ob.location[1]) + ',' + str(ob.location[2]) + '],"quat":[' + str(object_quaternion[2]) + ',' + str(object_quaternion[1] * -1) + ',' + str(object_quaternion[0]) + "," + str(object_quaternion[3]) + '],"scale":' + str(uniform_scale) + "}")
else:
uniform_scale = (ob.scale[0] + ob.scale[1] + ob.scale[2]) / 3

# get euler and rotate 180 deg
object_euler = ob.rotation_euler.copy()
object_euler.rotate_axis('Z', math.radians(180))
object_quaternion = object_euler.to_quaternion()

loc = ob.location

items.append('{"type":"' + object_name + '","pos":[' + str(loc[0]) + ',' + str(loc[1]) + ',' + str(loc[2]) + '],"quat":[' + str(object_quaternion[2]) + ',' + str(object_quaternion[1] * -1) + ',' + str(object_quaternion[0]) + "," + str(object_quaternion[3]) + '],"scale":' + str(uniform_scale) + "}")

# write to file
file.write("\n".join(items))
file.close()
return


######################################################
Expand All @@ -49,6 +63,7 @@ def save_forest(filepath,
# write forest
file = open(filepath, 'w')
export_forest(file, forest_item, data_source)
file.close()

# forest export complete
print(" done in %.4f sec." % (time.clock() - time1))
Expand Down

0 comments on commit 6cbe5dc

Please sign in to comment.