Skip to content

Commit ff185dd

Browse files
committed
- initial commit
1 parent 73a0eb2 commit ff185dd

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

69 files changed

+29696
-1
lines changed

LICENSE

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
MIT License
22

3-
Copyright (c) 2019 Interactive Computer Graphics
3+
Copyright (c) 2019 Jan Bender
44

55
Permission is hereby granted, free of charge, to any person obtaining a copy
66
of this software and associated documentation files (the "Software"), to deal

README.md

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
BlenderPartioTools is an open-source add-on to import particle data in Blender. The add-on can handle all file formats that are supported by the [Partio](https://www.disneyanimation.com/technology/partio.html) library.
2+
3+
The add-on requires Blender 2.80 or newer.
4+
5+
This add-on can be used to import and render the particle data generated with our fluid simulation library:
6+
- [https://github.com/InteractiveComputerGraphics/SPlisHSPlasH](https://github.com/InteractiveComputerGraphics/SPlisHSPlasH)
7+
8+
**Author**: [Jan Bender](http://www.interactive-graphics.de), **License**: MIT
9+
10+
![](screenshots/BlenderPartioTools.jpg)
11+
12+
## Installation
13+
14+
1. This add-on requires the partio python module. This module can be built by the calling
15+
16+
python setup.py build_ext
17+
18+
in the directory partio_extension.
19+
20+
2. Copy the generated file _partio.* (name depends on system) and the file partio.py to the Blender add-on folder.
21+
22+
3. Copy the file addon/BlenderPartioTools.py to the Blender add-on folder.
23+
24+
4. Start Blender and load add-on (Edit/Preferences/Add-ons)
25+
26+
## Usage
27+
28+
After loading the add-on a new importer appears. To import partio data do the following steps:
29+
30+
1. Click on "File/Import/Partio Import".
31+
2. Choose particle redius and maximum velocity (for coloring).
32+
3. Choose partio file (the add-on assumes that the last number in the file name is the frame number).
33+
34+
## Remarks
35+
36+
* Blender resets the particle system in frame 1. Therefore, the animation will start in frame 2 and all frame numbers are shifted by 1 (i.e. in frame 2 the file example_1.bgeo is loaded).
37+
* The add-on generates a hidden cube as emitter and renders the particles as spheres. If the radius should be adapted, edit the render settings of the cube's particle system.
38+
* By default the particle color is determined by the magnitude of the velocity of a particle. You can adapt this by modifying the shader.

addon/BlenderPartioTools.py

Lines changed: 269 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,269 @@
1+
bl_info = {
2+
"name": "BlenderPartioTools",
3+
"description": "Importer for partio files.",
4+
"author": "Jan Bender",
5+
"version": (1, 0),
6+
"blender": (2, 80, 0),
7+
"warning": "",
8+
"wiki_url": "https://github.com/InteractiveComputerGraphics/BlenderPartioTools",
9+
"support": "COMMUNITY",
10+
"category": "Import-Export"
11+
}
12+
13+
14+
import bpy
15+
import sys
16+
import os
17+
import re
18+
import mathutils
19+
import partio
20+
from bpy_extras.io_utils import ImportHelper
21+
from bpy.app.handlers import persistent
22+
from bpy.props import StringProperty, BoolProperty, EnumProperty, FloatProperty
23+
from bpy.types import Operator
24+
25+
26+
class PartioReader:
27+
def __init__( self, param ):
28+
self.param = param
29+
30+
def __call__(self, scene):
31+
partioFile = self.param[0]
32+
emitterObject = self.param[1]
33+
34+
try:
35+
dummy = emitterObject.name
36+
except:
37+
# emitter does not exist anymore
38+
#clear the post frame handler
39+
bpy.app.handlers.frame_change_post.remove(self)
40+
return
41+
42+
indexlist = re.findall(r'\d+', partioFile)
43+
self.isSequence = True
44+
if len(indexlist) == 0:
45+
self.isSequence = False
46+
fileName = partioFile
47+
else:
48+
frameNumber = int(indexlist[-1])
49+
idx = partioFile.rfind(str(frameNumber))
50+
l = len(str(frameNumber))
51+
fileName = str(partioFile[0:idx]) + str(scene.frame_current-1) + str(partioFile[idx+l:])
52+
53+
print("Read partio file: " + fileName)
54+
55+
p=partio.read(fileName)
56+
57+
if p != None:
58+
totalParticles = p.numParticles()
59+
print("# particles: " + str(totalParticles))
60+
61+
emitterObject.particle_systems[0].settings.count = totalParticles
62+
63+
if totalParticles > 10000:
64+
emitterObject.particle_systems[0].settings.display_method = 'DOT'
65+
66+
67+
degp = bpy.context.evaluated_depsgraph_get()
68+
particle_systems = emitterObject.evaluated_get(degp).particle_systems
69+
particles = particle_systems[0].particles
70+
71+
posAttr = None
72+
velAttr = None
73+
for i in range(p.numAttributes()):
74+
attr=p.attributeInfo(i)
75+
typeStr="NONE"
76+
if attr.name=="position": posAttr = attr
77+
if attr.name=="velocity": velAttr = attr
78+
79+
pos = []
80+
for i in range(p.numParticles()):
81+
position = p.get(posAttr, i)
82+
x = mathutils.Vector((position[0], -position[2], position[1]))
83+
84+
x = emitterObject.matrix_world @ x
85+
pos.append(x[0])
86+
pos.append(x[1])
87+
pos.append(x[2])
88+
# Set the location of all particle locations to flatList
89+
particles.foreach_set("location", pos)
90+
91+
92+
if velAttr is not None:
93+
vel = []
94+
for i in range(p.numParticles()):
95+
velocity = p.get(velAttr, i)
96+
v = mathutils.Vector((velocity[0], -velocity[2], velocity[1]))
97+
v = emitterObject.matrix_world @ v - emitterObject.location
98+
vel.append(v[0])
99+
vel.append(v[1])
100+
vel.append(v[2])
101+
particles.foreach_set("velocity", vel)
102+
103+
emitterObject.particle_systems[0].settings.frame_end = 0
104+
105+
106+
class PartioImporter(Operator, ImportHelper):
107+
bl_idname = "importer.partio"
108+
bl_label = "Import partio files"
109+
110+
filter_glob: StringProperty(
111+
default="*.bgeo",
112+
options={'HIDDEN'},
113+
maxlen=255,
114+
)
115+
116+
particleRadius: FloatProperty(
117+
name="Particle radius",
118+
description="Particle radius",
119+
default=0.025,
120+
)
121+
122+
maxVel: FloatProperty(
123+
name="Max. velocity",
124+
description="Max. velocity",
125+
default=5.0,
126+
)
127+
128+
def execute(self, context):
129+
self.emitterObject = None
130+
self.initParticleSystem()
131+
132+
#run the function on each frame
133+
param = [self.filepath, self.emitterObject]
134+
135+
self.emitterObject["partioFile"] = self.filepath
136+
137+
bpy.app.handlers.frame_change_post.append(PartioReader(param))
138+
139+
scn = bpy.context.scene
140+
scn.render.engine = 'CYCLES'
141+
142+
indexlist = re.findall(r'\d+', self.filepath)
143+
self.isSequence = True
144+
if len(indexlist) == 0:
145+
self.isSequence = False
146+
bpy.context.scene.frame_current = 2
147+
else:
148+
frameNumber = int(indexlist[-1])
149+
bpy.context.scene.frame_current = frameNumber+1
150+
151+
return {'FINISHED'}
152+
153+
def initParticleSystem(self):
154+
# create emitter object
155+
bpy.ops.mesh.primitive_cube_add(enter_editmode=False, location=(0, 0, 0))
156+
157+
self.emitterObject = bpy.context.active_object
158+
self.emitterObject.hide_viewport = False
159+
self.emitterObject.hide_render = False
160+
self.emitterObject.hide_select = False
161+
162+
# add particle system
163+
bpy.ops.object.modifier_add(type='PARTICLE_SYSTEM')
164+
bpy.context.object.show_instancer_for_render = False
165+
bpy.context.object.show_instancer_for_viewport = False
166+
167+
self.emitterObject
168+
self.emitterObject.particle_systems[0].settings.frame_start = 0
169+
self.emitterObject.particle_systems[0].settings.frame_end = 0
170+
self.emitterObject.particle_systems[0].settings.lifetime = 1000
171+
self.emitterObject.particle_systems[0].settings.particle_size = self.particleRadius
172+
self.emitterObject.particle_systems[0].settings.display_size = 2.0 * self.particleRadius
173+
174+
# add object for rendering particles
175+
bpy.ops.mesh.primitive_uv_sphere_add(radius=1, enter_editmode=False, location=(0, 0, 0))
176+
sphereObj = bpy.context.active_object
177+
sphereObj.hide_viewport = True
178+
sphereObj.hide_render = True
179+
sphereObj.hide_select = True
180+
181+
# add velocity-dependent color material
182+
found = True
183+
index = 1
184+
matNameBase = "ParticleMaterial"
185+
matName = matNameBase + str(index)
186+
materials = bpy.data.materials
187+
while (found):
188+
material = materials.get( matName )
189+
if material:
190+
index += 1
191+
matName = matNameBase + str(index)
192+
else:
193+
found = False
194+
195+
material = materials.new( matName )
196+
197+
198+
material.use_nodes = True
199+
nodes = material.node_tree.nodes
200+
links = material.node_tree.links
201+
nodes.clear()
202+
links.clear()
203+
output = nodes.new( type = 'ShaderNodeOutputMaterial' )
204+
diffuse = nodes.new( type = 'ShaderNodeBsdfDiffuse' )
205+
link = links.new( diffuse.outputs['BSDF'], output.inputs['Surface'] )
206+
207+
particleInfo = nodes.new( type = 'ShaderNodeParticleInfo' )
208+
209+
vecMath = nodes.new( type = 'ShaderNodeVectorMath' )
210+
vecMath.operation = 'DOT_PRODUCT'
211+
212+
math1 = nodes.new( type = 'ShaderNodeMath' )
213+
math1.operation = 'SQRT'
214+
math2 = nodes.new( type = 'ShaderNodeMath' )
215+
math2.operation = 'MULTIPLY'
216+
math2.inputs[1].default_value = 1.0/self.maxVel
217+
math2.use_clamp = True
218+
219+
220+
ramp = nodes.new( type = 'ShaderNodeValToRGB' )
221+
ramp.color_ramp.elements[0].color = (0, 0, 1, 1)
222+
223+
link = links.new( particleInfo.outputs['Velocity'], vecMath.inputs[0] )
224+
link = links.new( particleInfo.outputs['Velocity'], vecMath.inputs[1] )
225+
226+
link = links.new( vecMath.outputs['Value'], math1.inputs[0] )
227+
link = links.new( math1.outputs['Value'], math2.inputs[0] )
228+
link = links.new( math2.outputs['Value'], ramp.inputs['Fac'] )
229+
link = links.new( ramp.outputs['Color'], diffuse.inputs['Color'] )
230+
231+
232+
self.emitterObject.active_material = material
233+
sphereObj.active_material = material
234+
235+
self.emitterObject.particle_systems[0].settings.render_type = 'OBJECT'
236+
self.emitterObject.particle_systems[0].settings.instance_object = bpy.data.objects[sphereObj.name]
237+
238+
@persistent
239+
def loadPost(scene):
240+
for obj in bpy.data.objects:
241+
if "partioFile" in obj:
242+
param = [obj["partioFile"], obj]
243+
bpy.app.handlers.frame_change_post.append(PartioReader(param))
244+
245+
# Only needed if you want to add into a dynamic menu
246+
def menu_func_import(self, context):
247+
self.layout.operator(PartioImporter.bl_idname, text="Partio Import")
248+
249+
250+
def register():
251+
bpy.utils.register_class(PartioImporter)
252+
bpy.types.TOPBAR_MT_file_import.append(menu_func_import)
253+
bpy.app.handlers.load_post.append(loadPost)
254+
print(bpy.app.handlers.load_post)
255+
256+
257+
def unregister():
258+
bpy.utils.unregister_class(PartioImporter)
259+
bpy.types.TOPBAR_MT_file_import.remove(menu_func_import)
260+
bpy.app.handlers.load_post.remove(loadPost)
261+
262+
263+
if __name__ == "__main__":
264+
print ("main")
265+
register()
266+
267+
# test call
268+
bpy.ops.importer.partio('INVOKE_DEFAULT')
269+
unregister()

0 commit comments

Comments
 (0)