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