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