Merge branch 'blender2.7'
[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.style == 'BLEND':
196             from_obj = mesh_objects[1]
197             to_obj = mesh_objects[0]
198
199         for obj in mesh_objects:
200             fake_context["object"] = obj
201             bpy.ops.object.particle_system_add(fake_context)
202
203             settings = obj.particle_systems[-1].settings
204             settings.count = self.amount
205             # first set frame end, to prevent frame start clamping
206             settings.frame_end = self.frame_end - self.frame_duration
207             settings.frame_start = self.frame_start
208             settings.lifetime = self.frame_duration
209             settings.normal_factor = self.velocity
210             settings.render_type = 'NONE'
211
212             explode = obj.modifiers.new(name='Explode', type='EXPLODE')
213             explode.use_edge_cut = True
214
215             if self.fade:
216                 explode.show_dead = False
217                 uv = obj.data.uv_layers.new(name="Explode fade")
218                 explode.particle_uv = uv.name
219
220                 mat = object_ensure_material(obj, "Explode Fade")
221                 mat.blend_method = 'BLEND'
222                 mat.transparent_shadow_method = 'HASHED'
223                 if not mat.use_nodes:
224                     mat.use_nodes = True
225
226                 nodes = mat.node_tree.nodes
227                 for node in nodes:
228                     if (node.type == 'OUTPUT_MATERIAL'):
229                         node_out_mat = node
230                         break
231
232                 node_surface = node_out_mat.inputs['Surface'].links[0].from_node
233
234                 node_x = node_surface.location[0]
235                 node_y = node_surface.location[1] - 400
236                 offset_x = 200
237
238                 node_mix = nodes.new('ShaderNodeMixShader')
239                 node_mix.location = (node_x - offset_x, node_y)
240                 mat.node_tree.links.new(node_surface.outputs["BSDF"], node_mix.inputs[1])
241                 mat.node_tree.links.new(node_mix.outputs["Shader"], node_out_mat.inputs['Surface'])
242                 offset_x += 200
243
244                 node_trans = nodes.new('ShaderNodeBsdfTransparent')
245                 node_trans.location = (node_x - offset_x, node_y)
246                 mat.node_tree.links.new(node_trans.outputs["BSDF"], node_mix.inputs[2])
247                 offset_x += 200
248
249                 node_ramp = nodes.new('ShaderNodeValToRGB')
250                 node_ramp.location = (node_x - offset_x, node_y)
251                 offset_x += 200
252                 mat.node_tree.links.new(node_ramp.outputs["Alpha"], node_mix.inputs["Fac"])
253                 color_ramp = node_ramp.color_ramp
254                 color_ramp.elements[0].color[3] = 0.0
255                 color_ramp.elements[1].color[3] = 1.0
256
257                 if self.style == 'BLEND':
258                     color_ramp.elements[0].position = 0.333
259                     color_ramp.elements[1].position = 0.666
260                     if obj == to_obj:
261                         # reverse ramp alpha
262                         color_ramp.elements[0].color[3] = 1.0
263                         color_ramp.elements[1].color[3] = 0.0
264
265                 node_sep = nodes.new('ShaderNodeSeparateXYZ')
266                 node_sep.location = (node_x - offset_x, node_y)
267                 offset_x += 200
268                 mat.node_tree.links.new(node_sep.outputs["X"], node_ramp.inputs["Fac"])
269
270                 node_uv = nodes.new('ShaderNodeUVMap')
271                 node_uv.location = (node_x - offset_x, node_y)
272                 node_uv.uv_map = uv.name
273                 mat.node_tree.links.new(node_uv.outputs["UV"], node_sep.inputs["Vector"])
274
275             if self.style == 'BLEND':
276                 settings.physics_type = 'KEYED'
277                 settings.use_emit_random = False
278                 settings.rotation_mode = 'NOR'
279
280                 psys = obj.particle_systems[-1]
281
282                 fake_context["particle_system"] = obj.particle_systems[-1]
283                 bpy.ops.particle.new_target(fake_context)
284                 bpy.ops.particle.new_target(fake_context)
285
286                 if obj == from_obj:
287                     psys.targets[1].object = to_obj
288                 else:
289                     psys.targets[0].object = from_obj
290                     settings.normal_factor = -self.velocity
291                     explode.show_unborn = False
292                     explode.show_dead = True
293             else:
294                 settings.factor_random = self.velocity
295                 settings.angular_velocity_factor = self.velocity / 10.0
296
297         return {'FINISHED'}
298
299     def invoke(self, context, event):
300         self.frame_start = context.scene.frame_current
301         self.frame_end = self.frame_start + self.frame_duration
302         return self.execute(context)
303
304
305 def obj_bb_minmax(obj, min_co, max_co):
306     for i in range(0, 8):
307         bb_vec = obj.matrix_world @ Vector(obj.bound_box[i])
308
309         min_co[0] = min(bb_vec[0], min_co[0])
310         min_co[1] = min(bb_vec[1], min_co[1])
311         min_co[2] = min(bb_vec[2], min_co[2])
312         max_co[0] = max(bb_vec[0], max_co[0])
313         max_co[1] = max(bb_vec[1], max_co[1])
314         max_co[2] = max(bb_vec[2], max_co[2])
315
316
317 def grid_location(x, y):
318     return (x * 200, y * 150)
319
320
321 class QuickSmoke(Operator):
322     bl_idname = "object.quick_smoke"
323     bl_label = "Quick Smoke"
324     bl_options = {'REGISTER', 'UNDO'}
325
326     style: EnumProperty(
327         name="Smoke Style",
328         items=(
329             ('SMOKE', "Smoke", ""),
330             ('FIRE', "Fire", ""),
331             ('BOTH', "Smoke + Fire", ""),
332         ),
333         default='SMOKE',
334     )
335
336     show_flows: BoolProperty(
337         name="Render Smoke Objects",
338         description="Keep the smoke objects visible during rendering",
339         default=False,
340     )
341
342     def execute(self, context):
343         if not bpy.app.build_options.mod_smoke:
344             self.report({'ERROR'}, "Built without Smoke modifier support")
345             return {'CANCELLED'}
346
347         fake_context = context.copy()
348         mesh_objects = [obj for obj in context.selected_objects
349                         if obj.type == 'MESH']
350         min_co = Vector((100000.0, 100000.0, 100000.0))
351         max_co = -min_co
352
353         if not mesh_objects:
354             self.report({'ERROR'}, "Select at least one mesh object")
355             return {'CANCELLED'}
356
357         for obj in mesh_objects:
358             fake_context["object"] = obj
359             # make each selected object a smoke flow
360             bpy.ops.object.modifier_add(fake_context, type='SMOKE')
361             obj.modifiers[-1].smoke_type = 'FLOW'
362
363             # set type
364             obj.modifiers[-1].flow_settings.smoke_flow_type = self.style
365
366             if not self.show_flows:
367                 obj.display_type = 'WIRE'
368
369             # store bounding box min/max for the domain object
370             obj_bb_minmax(obj, min_co, max_co)
371
372         # add the smoke domain object
373         bpy.ops.mesh.primitive_cube_add()
374         obj = context.active_object
375         obj.name = "Smoke Domain"
376
377         # give the smoke some room above the flows
378         obj.location = 0.5 * (max_co + min_co) + Vector((0.0, 0.0, 1.0))
379         obj.scale = 0.5 * (max_co - min_co) + Vector((1.0, 1.0, 2.0))
380
381         # setup smoke domain
382         bpy.ops.object.modifier_add(type='SMOKE')
383         obj.modifiers[-1].smoke_type = 'DOMAIN'
384         if self.style == 'FIRE' or self.style == 'BOTH':
385             obj.modifiers[-1].domain_settings.use_high_resolution = True
386
387         # Setup material
388
389         # Cycles and Eevee
390         bpy.ops.object.material_slot_add()
391
392         mat = bpy.data.materials.new("Smoke Domain Material")
393         obj.material_slots[0].material = mat
394
395         # Make sure we use nodes
396         mat.use_nodes = True
397
398         # Set node variables and clear the default nodes
399         tree = mat.node_tree
400         nodes = tree.nodes
401         links = tree.links
402
403         nodes.clear()
404
405         # Create shader nodes
406
407         # Material output
408         node_out = nodes.new(type='ShaderNodeOutputMaterial')
409         node_out.location = grid_location(6, 1)
410
411         # Add Principled Volume
412         node_principled = nodes.new(type='ShaderNodeVolumePrincipled')
413         node_principled.location = grid_location(4, 1)
414         links.new(node_principled.outputs["Volume"],
415                   node_out.inputs["Volume"])
416
417         node_principled.inputs["Density"].default_value = 5.0
418
419         if self.style in {'FIRE', 'BOTH'}:
420             node_principled.inputs["Blackbody Intensity"].default_value = 1.0
421
422         return {'FINISHED'}
423
424
425 class QuickFluid(Operator):
426     bl_idname = "object.quick_fluid"
427     bl_label = "Quick Fluid"
428     bl_options = {'REGISTER', 'UNDO'}
429
430     style: EnumProperty(
431         name="Fluid Style",
432         items=(
433             ('INFLOW', "Inflow", ""),
434             ('BASIC', "Basic", ""),
435         ),
436         default='BASIC',
437     )
438     initial_velocity: FloatVectorProperty(
439         name="Initial Velocity",
440         description="Initial velocity of the fluid",
441         min=-100.0, max=100.0,
442         default=(0.0, 0.0, 0.0),
443         subtype='VELOCITY',
444     )
445     show_flows: BoolProperty(
446         name="Render Fluid Objects",
447         description="Keep the fluid objects visible during rendering",
448         default=False,
449     )
450     start_baking: BoolProperty(
451         name="Start Fluid Bake",
452         description=("Start baking the fluid immediately "
453                      "after creating the domain object"),
454         default=False,
455     )
456
457     def execute(self, context):
458         if not bpy.app.build_options.mod_fluid:
459             self.report({'ERROR'}, "Built without Fluid modifier support")
460             return {'CANCELLED'}
461
462         fake_context = context.copy()
463         mesh_objects = [obj for obj in context.selected_objects
464                         if (obj.type == 'MESH' and 0.0 not in obj.dimensions)]
465         min_co = Vector((100000.0, 100000.0, 100000.0))
466         max_co = -min_co
467
468         if not mesh_objects:
469             self.report({'ERROR'}, "Select at least one mesh object")
470             return {'CANCELLED'}
471
472         for obj in mesh_objects:
473             fake_context["object"] = obj
474             # make each selected object a fluid
475             bpy.ops.object.modifier_add(fake_context, type='FLUID_SIMULATION')
476
477             # fluid has to be before constructive modifiers,
478             # so it might not be the last modifier
479             for mod in obj.modifiers:
480                 if mod.type == 'FLUID_SIMULATION':
481                     break
482
483             if self.style == 'INFLOW':
484                 mod.settings.type = 'INFLOW'
485                 mod.settings.inflow_velocity = self.initial_velocity
486             else:
487                 mod.settings.type = 'FLUID'
488                 mod.settings.initial_velocity = self.initial_velocity
489
490             obj.hide_render = not self.show_flows
491             if not self.show_flows:
492                 obj.display_type = 'WIRE'
493
494             # store bounding box min/max for the domain object
495             obj_bb_minmax(obj, min_co, max_co)
496
497         # add the fluid domain object
498         bpy.ops.mesh.primitive_cube_add()
499         obj = context.active_object
500         obj.name = "Fluid Domain"
501
502         # give the fluid some room below the flows
503         # and scale with initial velocity
504         v = 0.5 * self.initial_velocity
505         obj.location = 0.5 * (max_co + min_co) + Vector((0.0, 0.0, -1.0)) + v
506         obj.scale = (0.5 * (max_co - min_co) +
507                      Vector((1.0, 1.0, 2.0)) +
508                      Vector((abs(v[0]), abs(v[1]), abs(v[2])))
509                      )
510
511         # setup smoke domain
512         bpy.ops.object.modifier_add(type='FLUID_SIMULATION')
513         obj.modifiers[-1].settings.type = 'DOMAIN'
514
515         # make the domain smooth so it renders nicely
516         bpy.ops.object.shade_smooth()
517
518         # create a ray-transparent material for the domain
519         bpy.ops.object.material_slot_add()
520
521         mat = bpy.data.materials.new("Fluid Domain Material")
522         obj.material_slots[0].material = mat
523
524         # Make sure we use nodes
525         mat.use_nodes = True
526
527         # Set node variables and clear the default nodes
528         tree = mat.node_tree
529         nodes = tree.nodes
530         links = tree.links
531
532         nodes.clear()
533
534         # Create shader nodes
535
536         # Material output
537         node_out = nodes.new(type='ShaderNodeOutputMaterial')
538         node_out.location = grid_location(6, 1)
539
540         # Add Glass
541         node_glass = nodes.new(type='ShaderNodeBsdfGlass')
542         node_glass.location = grid_location(4, 1)
543         links.new(node_glass.outputs["BSDF"], node_out.inputs["Surface"])
544         node_glass.inputs["IOR"].default_value = 1.33
545
546         # Add Absorption
547         node_absorption = nodes.new(type='ShaderNodeVolumeAbsorption')
548         node_absorption.location = grid_location(4, 2)
549         links.new(node_absorption.outputs["Volume"], node_out.inputs["Volume"])
550         node_absorption.inputs["Color"].default_value = (0.8, 0.9, 1.0, 1.0)
551
552         if self.start_baking:
553             bpy.ops.fluid.bake('INVOKE_DEFAULT')
554
555         return {'FINISHED'}
556
557
558 classes = (
559     QuickExplode,
560     QuickFluid,
561     QuickFur,
562     QuickSmoke,
563 )