object_quick_effects.py 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575
  1. # ##### BEGIN GPL LICENSE BLOCK #####
  2. #
  3. # This program is free software; you can redistribute it and/or
  4. # modify it under the terms of the GNU General Public License
  5. # as published by the Free Software Foundation; either version 2
  6. # of the License, or (at your option) any later version.
  7. #
  8. # This program is distributed in the hope that it will be useful,
  9. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  10. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  11. # GNU General Public License for more details.
  12. #
  13. # You should have received a copy of the GNU General Public License
  14. # along with this program; if not, write to the Free Software Foundation,
  15. # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
  16. #
  17. # ##### END GPL LICENSE BLOCK #####
  18. # <pep8-80 compliant>
  19. from mathutils import Vector
  20. import bpy
  21. from bpy.types import Operator
  22. from bpy.props import (
  23. BoolProperty,
  24. EnumProperty,
  25. FloatProperty,
  26. FloatVectorProperty,
  27. IntProperty,
  28. )
  29. def object_ensure_material(obj, mat_name):
  30. """ Use an existing material or add a new one.
  31. """
  32. mat = mat_slot = None
  33. for mat_slot in obj.material_slots:
  34. mat = mat_slot.material
  35. if mat:
  36. break
  37. if mat is None:
  38. mat = bpy.data.materials.new(mat_name)
  39. if mat_slot:
  40. mat_slot.material = mat
  41. else:
  42. obj.data.materials.append(mat)
  43. return mat
  44. class ObjectModeOperator:
  45. @classmethod
  46. def poll(cls, context):
  47. return context.mode == 'OBJECT'
  48. class QuickFur(ObjectModeOperator, Operator):
  49. """Add fur setup to the selected objects"""
  50. bl_idname = "object.quick_fur"
  51. bl_label = "Quick Fur"
  52. bl_options = {'REGISTER', 'UNDO'}
  53. density: EnumProperty(
  54. name="Fur Density",
  55. items=(
  56. ('LIGHT', "Light", ""),
  57. ('MEDIUM', "Medium", ""),
  58. ('HEAVY', "Heavy", "")
  59. ),
  60. default='MEDIUM',
  61. )
  62. view_percentage: IntProperty(
  63. name="View %",
  64. min=1, max=100,
  65. soft_min=1, soft_max=100,
  66. default=10,
  67. )
  68. length: FloatProperty(
  69. name="Length",
  70. min=0.001, max=100,
  71. soft_min=0.01, soft_max=10,
  72. default=0.1,
  73. )
  74. def execute(self, context):
  75. fake_context = context.copy()
  76. mesh_objects = [obj for obj in context.selected_objects
  77. if obj.type == 'MESH']
  78. if not mesh_objects:
  79. self.report({'ERROR'}, "Select at least one mesh object")
  80. return {'CANCELLED'}
  81. mat = bpy.data.materials.new("Fur Material")
  82. for obj in mesh_objects:
  83. fake_context["object"] = obj
  84. bpy.ops.object.particle_system_add(fake_context)
  85. psys = obj.particle_systems[-1]
  86. psys.settings.type = 'HAIR'
  87. if self.density == 'LIGHT':
  88. psys.settings.count = 100
  89. elif self.density == 'MEDIUM':
  90. psys.settings.count = 1000
  91. elif self.density == 'HEAVY':
  92. psys.settings.count = 10000
  93. psys.settings.child_nbr = self.view_percentage
  94. psys.settings.hair_length = self.length
  95. psys.settings.use_strand_primitive = True
  96. psys.settings.use_hair_bspline = True
  97. psys.settings.child_type = 'INTERPOLATED'
  98. psys.settings.tip_radius = 0.25
  99. obj.data.materials.append(mat)
  100. psys.settings.material = len(obj.data.materials)
  101. return {'FINISHED'}
  102. class QuickExplode(ObjectModeOperator, Operator):
  103. """Make selected objects explode"""
  104. bl_idname = "object.quick_explode"
  105. bl_label = "Quick Explode"
  106. bl_options = {'REGISTER', 'UNDO'}
  107. style: EnumProperty(
  108. name="Explode Style",
  109. items=(
  110. ('EXPLODE', "Explode", ""),
  111. ('BLEND', "Blend", ""),
  112. ),
  113. default='EXPLODE',
  114. )
  115. amount: IntProperty(
  116. name="Amount of pieces",
  117. min=2, max=10000,
  118. soft_min=2, soft_max=10000,
  119. default=100,
  120. )
  121. frame_duration: IntProperty(
  122. name="Duration",
  123. min=1, max=300000,
  124. soft_min=1, soft_max=10000,
  125. default=50,
  126. )
  127. frame_start: IntProperty(
  128. name="Start Frame",
  129. min=1, max=300000,
  130. soft_min=1, soft_max=10000,
  131. default=1,
  132. )
  133. frame_end: IntProperty(
  134. name="End Frame",
  135. min=1, max=300000,
  136. soft_min=1, soft_max=10000,
  137. default=10,
  138. )
  139. velocity: FloatProperty(
  140. name="Outwards Velocity",
  141. min=0, max=300000,
  142. soft_min=0, soft_max=10,
  143. default=1,
  144. )
  145. fade: BoolProperty(
  146. name="Fade",
  147. description="Fade the pieces over time",
  148. default=True,
  149. )
  150. def execute(self, context):
  151. fake_context = context.copy()
  152. obj_act = context.active_object
  153. if obj_act is None or obj_act.type != 'MESH':
  154. self.report({'ERROR'}, "Active object is not a mesh")
  155. return {'CANCELLED'}
  156. mesh_objects = [obj for obj in context.selected_objects
  157. if obj.type == 'MESH' and obj != obj_act]
  158. mesh_objects.insert(0, obj_act)
  159. if self.style == 'BLEND' and len(mesh_objects) != 2:
  160. self.report({'ERROR'}, "Select two mesh objects")
  161. self.style = 'EXPLODE'
  162. return {'CANCELLED'}
  163. elif not mesh_objects:
  164. self.report({'ERROR'}, "Select at least one mesh object")
  165. return {'CANCELLED'}
  166. for obj in mesh_objects:
  167. if obj.particle_systems:
  168. self.report({'ERROR'},
  169. "Object %r already has a "
  170. "particle system" % obj.name)
  171. return {'CANCELLED'}
  172. if self.style == 'BLEND':
  173. from_obj = mesh_objects[1]
  174. to_obj = mesh_objects[0]
  175. for obj in mesh_objects:
  176. fake_context["object"] = obj
  177. bpy.ops.object.particle_system_add(fake_context)
  178. settings = obj.particle_systems[-1].settings
  179. settings.count = self.amount
  180. # first set frame end, to prevent frame start clamping
  181. settings.frame_end = self.frame_end - self.frame_duration
  182. settings.frame_start = self.frame_start
  183. settings.lifetime = self.frame_duration
  184. settings.normal_factor = self.velocity
  185. settings.render_type = 'NONE'
  186. explode = obj.modifiers.new(name='Explode', type='EXPLODE')
  187. explode.use_edge_cut = True
  188. if self.fade:
  189. explode.show_dead = False
  190. uv = obj.data.uv_layers.new(name="Explode fade")
  191. explode.particle_uv = uv.name
  192. mat = object_ensure_material(obj, "Explode Fade")
  193. mat.blend_method = 'BLEND'
  194. mat.shadow_method = 'HASHED'
  195. if not mat.use_nodes:
  196. mat.use_nodes = True
  197. nodes = mat.node_tree.nodes
  198. for node in nodes:
  199. if node.type == 'OUTPUT_MATERIAL':
  200. node_out_mat = node
  201. break
  202. node_surface = node_out_mat.inputs['Surface'].links[0].from_node
  203. node_x = node_surface.location[0]
  204. node_y = node_surface.location[1] - 400
  205. offset_x = 200
  206. node_mix = nodes.new('ShaderNodeMixShader')
  207. node_mix.location = (node_x - offset_x, node_y)
  208. mat.node_tree.links.new(node_surface.outputs[0], node_mix.inputs[1])
  209. mat.node_tree.links.new(node_mix.outputs["Shader"], node_out_mat.inputs['Surface'])
  210. offset_x += 200
  211. node_trans = nodes.new('ShaderNodeBsdfTransparent')
  212. node_trans.location = (node_x - offset_x, node_y)
  213. mat.node_tree.links.new(node_trans.outputs["BSDF"], node_mix.inputs[2])
  214. offset_x += 200
  215. node_ramp = nodes.new('ShaderNodeValToRGB')
  216. node_ramp.location = (node_x - offset_x, node_y)
  217. offset_x += 200
  218. mat.node_tree.links.new(node_ramp.outputs["Alpha"], node_mix.inputs["Fac"])
  219. color_ramp = node_ramp.color_ramp
  220. color_ramp.elements[0].color[3] = 0.0
  221. color_ramp.elements[1].color[3] = 1.0
  222. if self.style == 'BLEND':
  223. color_ramp.elements[0].position = 0.333
  224. color_ramp.elements[1].position = 0.666
  225. if obj == to_obj:
  226. # reverse ramp alpha
  227. color_ramp.elements[0].color[3] = 1.0
  228. color_ramp.elements[1].color[3] = 0.0
  229. node_sep = nodes.new('ShaderNodeSeparateXYZ')
  230. node_sep.location = (node_x - offset_x, node_y)
  231. offset_x += 200
  232. mat.node_tree.links.new(node_sep.outputs["X"], node_ramp.inputs["Fac"])
  233. node_uv = nodes.new('ShaderNodeUVMap')
  234. node_uv.location = (node_x - offset_x, node_y)
  235. node_uv.uv_map = uv.name
  236. mat.node_tree.links.new(node_uv.outputs["UV"], node_sep.inputs["Vector"])
  237. if self.style == 'BLEND':
  238. settings.physics_type = 'KEYED'
  239. settings.use_emit_random = False
  240. settings.rotation_mode = 'NOR'
  241. psys = obj.particle_systems[-1]
  242. fake_context["particle_system"] = obj.particle_systems[-1]
  243. bpy.ops.particle.new_target(fake_context)
  244. bpy.ops.particle.new_target(fake_context)
  245. if obj == from_obj:
  246. psys.targets[1].object = to_obj
  247. else:
  248. psys.targets[0].object = from_obj
  249. settings.normal_factor = -self.velocity
  250. explode.show_unborn = False
  251. explode.show_dead = True
  252. else:
  253. settings.factor_random = self.velocity
  254. settings.angular_velocity_factor = self.velocity / 10.0
  255. return {'FINISHED'}
  256. def invoke(self, context, _event):
  257. self.frame_start = context.scene.frame_current
  258. self.frame_end = self.frame_start + self.frame_duration
  259. return self.execute(context)
  260. def obj_bb_minmax(obj, min_co, max_co):
  261. for i in range(0, 8):
  262. bb_vec = obj.matrix_world @ Vector(obj.bound_box[i])
  263. min_co[0] = min(bb_vec[0], min_co[0])
  264. min_co[1] = min(bb_vec[1], min_co[1])
  265. min_co[2] = min(bb_vec[2], min_co[2])
  266. max_co[0] = max(bb_vec[0], max_co[0])
  267. max_co[1] = max(bb_vec[1], max_co[1])
  268. max_co[2] = max(bb_vec[2], max_co[2])
  269. def grid_location(x, y):
  270. return (x * 200, y * 150)
  271. class QuickSmoke(ObjectModeOperator, Operator):
  272. """Use selected objects as smoke emitters"""
  273. bl_idname = "object.quick_smoke"
  274. bl_label = "Quick Smoke"
  275. bl_options = {'REGISTER', 'UNDO'}
  276. style: EnumProperty(
  277. name="Smoke Style",
  278. items=(
  279. ('SMOKE', "Smoke", ""),
  280. ('FIRE', "Fire", ""),
  281. ('BOTH', "Smoke + Fire", ""),
  282. ),
  283. default='SMOKE',
  284. )
  285. show_flows: BoolProperty(
  286. name="Render Smoke Objects",
  287. description="Keep the smoke objects visible during rendering",
  288. default=False,
  289. )
  290. def execute(self, context):
  291. if not bpy.app.build_options.mod_smoke:
  292. self.report({'ERROR'}, "Built without Smoke modifier support")
  293. return {'CANCELLED'}
  294. fake_context = context.copy()
  295. mesh_objects = [obj for obj in context.selected_objects
  296. if obj.type == 'MESH']
  297. min_co = Vector((100000.0, 100000.0, 100000.0))
  298. max_co = -min_co
  299. if not mesh_objects:
  300. self.report({'ERROR'}, "Select at least one mesh object")
  301. return {'CANCELLED'}
  302. for obj in mesh_objects:
  303. fake_context["object"] = obj
  304. # make each selected object a smoke flow
  305. bpy.ops.object.modifier_add(fake_context, type='SMOKE')
  306. obj.modifiers[-1].smoke_type = 'FLOW'
  307. # set type
  308. obj.modifiers[-1].flow_settings.smoke_flow_type = self.style
  309. if not self.show_flows:
  310. obj.display_type = 'WIRE'
  311. # store bounding box min/max for the domain object
  312. obj_bb_minmax(obj, min_co, max_co)
  313. # add the smoke domain object
  314. bpy.ops.mesh.primitive_cube_add()
  315. obj = context.active_object
  316. obj.name = "Smoke Domain"
  317. # give the smoke some room above the flows
  318. obj.location = 0.5 * (max_co + min_co) + Vector((0.0, 0.0, 1.0))
  319. obj.scale = 0.5 * (max_co - min_co) + Vector((1.0, 1.0, 2.0))
  320. # setup smoke domain
  321. bpy.ops.object.modifier_add(type='SMOKE')
  322. obj.modifiers[-1].smoke_type = 'DOMAIN'
  323. if self.style == 'FIRE' or self.style == 'BOTH':
  324. obj.modifiers[-1].domain_settings.use_high_resolution = True
  325. # Setup material
  326. # Cycles and Eevee
  327. bpy.ops.object.material_slot_add()
  328. mat = bpy.data.materials.new("Smoke Domain Material")
  329. obj.material_slots[0].material = mat
  330. # Make sure we use nodes
  331. mat.use_nodes = True
  332. # Set node variables and clear the default nodes
  333. tree = mat.node_tree
  334. nodes = tree.nodes
  335. links = tree.links
  336. nodes.clear()
  337. # Create shader nodes
  338. # Material output
  339. node_out = nodes.new(type='ShaderNodeOutputMaterial')
  340. node_out.location = grid_location(6, 1)
  341. # Add Principled Volume
  342. node_principled = nodes.new(type='ShaderNodeVolumePrincipled')
  343. node_principled.location = grid_location(4, 1)
  344. links.new(node_principled.outputs["Volume"],
  345. node_out.inputs["Volume"])
  346. node_principled.inputs["Density"].default_value = 5.0
  347. if self.style in {'FIRE', 'BOTH'}:
  348. node_principled.inputs["Blackbody Intensity"].default_value = 1.0
  349. return {'FINISHED'}
  350. class QuickFluid(ObjectModeOperator, Operator):
  351. """Use selected objects in a fluid simulation"""
  352. bl_idname = "object.quick_fluid"
  353. bl_label = "Quick Fluid"
  354. bl_options = {'REGISTER', 'UNDO'}
  355. style: EnumProperty(
  356. name="Fluid Style",
  357. items=(
  358. ('INFLOW', "Inflow", ""),
  359. ('BASIC', "Basic", ""),
  360. ),
  361. default='BASIC',
  362. )
  363. initial_velocity: FloatVectorProperty(
  364. name="Initial Velocity",
  365. description="Initial velocity of the fluid",
  366. min=-100.0, max=100.0,
  367. default=(0.0, 0.0, 0.0),
  368. subtype='VELOCITY',
  369. )
  370. show_flows: BoolProperty(
  371. name="Render Fluid Objects",
  372. description="Keep the fluid objects visible during rendering",
  373. default=False,
  374. )
  375. start_baking: BoolProperty(
  376. name="Start Fluid Bake",
  377. description=("Start baking the fluid immediately "
  378. "after creating the domain object"),
  379. default=False,
  380. )
  381. def execute(self, context):
  382. if not bpy.app.build_options.mod_fluid:
  383. self.report({'ERROR'}, "Built without Fluid modifier support")
  384. return {'CANCELLED'}
  385. fake_context = context.copy()
  386. mesh_objects = [obj for obj in context.selected_objects
  387. if (obj.type == 'MESH' and 0.0 not in obj.dimensions)]
  388. min_co = Vector((100000.0, 100000.0, 100000.0))
  389. max_co = -min_co
  390. if not mesh_objects:
  391. self.report({'ERROR'}, "Select at least one mesh object")
  392. return {'CANCELLED'}
  393. for obj in mesh_objects:
  394. fake_context["object"] = obj
  395. # make each selected object a fluid
  396. bpy.ops.object.modifier_add(fake_context, type='FLUID_SIMULATION')
  397. # fluid has to be before constructive modifiers,
  398. # so it might not be the last modifier
  399. for mod in obj.modifiers:
  400. if mod.type == 'FLUID_SIMULATION':
  401. break
  402. if self.style == 'INFLOW':
  403. mod.settings.type = 'INFLOW'
  404. mod.settings.inflow_velocity = self.initial_velocity
  405. else:
  406. mod.settings.type = 'FLUID'
  407. mod.settings.initial_velocity = self.initial_velocity
  408. obj.hide_render = not self.show_flows
  409. if not self.show_flows:
  410. obj.display_type = 'WIRE'
  411. # store bounding box min/max for the domain object
  412. obj_bb_minmax(obj, min_co, max_co)
  413. # add the fluid domain object
  414. bpy.ops.mesh.primitive_cube_add()
  415. obj = context.active_object
  416. obj.name = "Fluid Domain"
  417. # give the fluid some room below the flows
  418. # and scale with initial velocity
  419. v = 0.5 * self.initial_velocity
  420. obj.location = 0.5 * (max_co + min_co) + Vector((0.0, 0.0, -1.0)) + v
  421. obj.scale = (
  422. 0.5 * (max_co - min_co) +
  423. Vector((1.0, 1.0, 2.0)) +
  424. Vector((abs(v[0]), abs(v[1]), abs(v[2])))
  425. )
  426. # setup smoke domain
  427. bpy.ops.object.modifier_add(type='FLUID_SIMULATION')
  428. obj.modifiers[-1].settings.type = 'DOMAIN'
  429. # make the domain smooth so it renders nicely
  430. bpy.ops.object.shade_smooth()
  431. # create a ray-transparent material for the domain
  432. bpy.ops.object.material_slot_add()
  433. mat = bpy.data.materials.new("Fluid Domain Material")
  434. obj.material_slots[0].material = mat
  435. # Make sure we use nodes
  436. mat.use_nodes = True
  437. # Set node variables and clear the default nodes
  438. tree = mat.node_tree
  439. nodes = tree.nodes
  440. links = tree.links
  441. nodes.clear()
  442. # Create shader nodes
  443. # Material output
  444. node_out = nodes.new(type='ShaderNodeOutputMaterial')
  445. node_out.location = grid_location(6, 1)
  446. # Add Glass
  447. node_glass = nodes.new(type='ShaderNodeBsdfGlass')
  448. node_glass.location = grid_location(4, 1)
  449. links.new(node_glass.outputs["BSDF"], node_out.inputs["Surface"])
  450. node_glass.inputs["IOR"].default_value = 1.33
  451. # Add Absorption
  452. node_absorption = nodes.new(type='ShaderNodeVolumeAbsorption')
  453. node_absorption.location = grid_location(4, 2)
  454. links.new(node_absorption.outputs["Volume"], node_out.inputs["Volume"])
  455. node_absorption.inputs["Color"].default_value = (0.8, 0.9, 1.0, 1.0)
  456. if self.start_baking:
  457. bpy.ops.fluid.bake('INVOKE_DEFAULT')
  458. return {'FINISHED'}
  459. classes = (
  460. QuickExplode,
  461. QuickFluid,
  462. QuickFur,
  463. QuickSmoke,
  464. )