6b9442769af4a90a20fee847a2b45171c86fd566
[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=(('LIGHT', "Light", ""),
58                    ('MEDIUM', "Medium", ""),
59                    ('HEAVY', "Heavy", "")),
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
75     def execute(self, context):
76         fake_context = context.copy()
77         mesh_objects = [obj for obj in context.selected_objects
78                         if obj.type == 'MESH' and obj.mode == 'OBJECT']
79
80         if not mesh_objects:
81             self.report({'ERROR'}, "Select at least one mesh object")
82             return {'CANCELLED'}
83
84         mat = bpy.data.materials.new("Fur Material")
85         mat.strand.tip_size = 0.25
86         mat.strand.blend_distance = 0.5
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
108             obj.data.materials.append(mat)
109             psys.settings.material = len(obj.data.materials)
110
111         return {'FINISHED'}
112
113
114 class QuickExplode(Operator):
115     bl_idname = "object.quick_explode"
116     bl_label = "Quick Explode"
117     bl_options = {'REGISTER', 'UNDO'}
118
119     style = EnumProperty(
120             name="Explode Style",
121             items=(('EXPLODE', "Explode", ""),
122                    ('BLEND', "Blend", "")),
123             default='EXPLODE',
124             )
125     amount = IntProperty(
126             name="Amount of pieces",
127             min=2, max=10000,
128             soft_min=2, soft_max=10000,
129             default=100,
130             )
131     frame_duration = IntProperty(
132             name="Duration",
133             min=1, max=300000,
134             soft_min=1, soft_max=10000,
135             default=50,
136             )
137
138     frame_start = IntProperty(
139             name="Start Frame",
140             min=1, max=300000,
141             soft_min=1, soft_max=10000,
142             default=1,
143             )
144     frame_end = IntProperty(
145             name="End Frame",
146             min=1, max=300000,
147             soft_min=1, soft_max=10000,
148             default=10,
149             )
150
151     velocity = FloatProperty(
152             name="Outwards Velocity",
153             min=0, max=300000,
154             soft_min=0, soft_max=10,
155             default=1,
156             )
157
158     fade = BoolProperty(
159             name="Fade",
160             description="Fade the pieces over time",
161             default=True,
162             )
163
164     def execute(self, context):
165         fake_context = context.copy()
166         obj_act = context.active_object
167
168         if obj_act is None or obj_act.type != 'MESH':
169             self.report({'ERROR'}, "Active object is not a mesh")
170             return {'CANCELLED'}
171
172         mesh_objects = [obj for obj in context.selected_objects
173                         if obj.type == 'MESH' and obj != obj_act]
174         mesh_objects.insert(0, obj_act)
175
176         if self.style == 'BLEND' and len(mesh_objects) != 2:
177             self.report({'ERROR'}, "Select two mesh objects")
178             self.style = 'EXPLODE'
179             return {'CANCELLED'}
180         elif not mesh_objects:
181             self.report({'ERROR'}, "Select at least one mesh object")
182             return {'CANCELLED'}
183
184         for obj in mesh_objects:
185             if obj.particle_systems:
186                 self.report({'ERROR'},
187                             "Object %r already has a "
188                             "particle system" % obj.name)
189
190                 return {'CANCELLED'}
191
192         if self.fade:
193             tex = bpy.data.textures.new("Explode fade", 'BLEND')
194             tex.use_color_ramp = True
195
196             if self.style == 'BLEND':
197                 tex.color_ramp.elements[0].position = 0.333
198                 tex.color_ramp.elements[1].position = 0.666
199
200             tex.color_ramp.elements[0].color[3] = 1.0
201             tex.color_ramp.elements[1].color[3] = 0.0
202
203         if self.style == 'BLEND':
204             from_obj = mesh_objects[1]
205             to_obj = mesh_objects[0]
206
207         for obj in mesh_objects:
208             fake_context["object"] = obj
209             bpy.ops.object.particle_system_add(fake_context)
210
211             settings = obj.particle_systems[-1].settings
212             settings.count = self.amount
213             # first set frame end, to prevent frame start clamping
214             settings.frame_end = self.frame_end - self.frame_duration
215             settings.frame_start = self.frame_start
216             settings.lifetime = self.frame_duration
217             settings.normal_factor = self.velocity
218             settings.render_type = 'NONE'
219
220             explode = obj.modifiers.new(name='Explode', type='EXPLODE')
221             explode.use_edge_cut = True
222
223             if self.fade:
224                 explode.show_dead = False
225                 uv = obj.data.uv_textures.new("Explode fade")
226                 explode.particle_uv = uv.name
227
228                 mat = object_ensure_material(obj, "Explode Fade")
229
230                 mat.use_transparency = True
231                 mat.use_transparent_shadows = True
232                 mat.alpha = 0.0
233                 mat.specular_alpha = 0.0
234
235                 tex_slot = mat.texture_slots.add()
236
237                 tex_slot.texture = tex
238                 tex_slot.texture_coords = 'UV'
239                 tex_slot.uv_layer = uv.name
240
241                 tex_slot.use_map_alpha = True
242
243                 if self.style == 'BLEND':
244                     if obj == to_obj:
245                         tex_slot.alpha_factor = -1.0
246                         elem = tex.color_ramp.elements[1]
247                     else:
248                         elem = tex.color_ramp.elements[0]
249                     # Keep already defined alpha!
250                     elem.color[:3] = mat.diffuse_color
251                 else:
252                     tex_slot.use_map_color_diffuse = False
253
254             if self.style == 'BLEND':
255                 settings.physics_type = 'KEYED'
256                 settings.use_emit_random = False
257                 settings.rotation_mode = 'NOR'
258
259                 psys = obj.particle_systems[-1]
260
261                 fake_context["particle_system"] = obj.particle_systems[-1]
262                 bpy.ops.particle.new_target(fake_context)
263                 bpy.ops.particle.new_target(fake_context)
264
265                 if obj == from_obj:
266                     psys.targets[1].object = to_obj
267                 else:
268                     psys.targets[0].object = from_obj
269                     settings.normal_factor = -self.velocity
270                     explode.show_unborn = False
271                     explode.show_dead = True
272             else:
273                 settings.factor_random = self.velocity
274                 settings.angular_velocity_factor = self.velocity / 10.0
275
276         return {'FINISHED'}
277
278     def invoke(self, context, event):
279         self.frame_start = context.scene.frame_current
280         self.frame_end = self.frame_start + self.frame_duration
281         return self.execute(context)
282
283
284 def obj_bb_minmax(obj, min_co, max_co):
285     for i in range(0, 8):
286         bb_vec = obj.matrix_world * Vector(obj.bound_box[i])
287
288         min_co[0] = min(bb_vec[0], min_co[0])
289         min_co[1] = min(bb_vec[1], min_co[1])
290         min_co[2] = min(bb_vec[2], min_co[2])
291         max_co[0] = max(bb_vec[0], max_co[0])
292         max_co[1] = max(bb_vec[1], max_co[1])
293         max_co[2] = max(bb_vec[2], max_co[2])
294
295
296 def grid_location(x, y):
297     return (x * 200, y * 150)
298
299
300 class QuickSmoke(Operator):
301     bl_idname = "object.quick_smoke"
302     bl_label = "Quick Smoke"
303     bl_options = {'REGISTER', 'UNDO'}
304
305     style = EnumProperty(
306             name="Smoke Style",
307             items=(('SMOKE', "Smoke", ""),
308                    ('FIRE', "Fire", ""),
309                    ('BOTH', "Smoke + Fire", ""),
310                    ),
311             default='SMOKE',
312             )
313
314     show_flows = BoolProperty(
315             name="Render Smoke Objects",
316             description="Keep the smoke objects visible during rendering",
317             default=False,
318             )
319
320     def execute(self, context):
321         if not bpy.app.build_options.mod_smoke:
322             self.report({'ERROR'}, "Built without Smoke modifier support")
323             return {'CANCELLED'}
324
325         fake_context = context.copy()
326         mesh_objects = [obj for obj in context.selected_objects
327                         if obj.type == 'MESH']
328         min_co = Vector((100000.0, 100000.0, 100000.0))
329         max_co = -min_co
330
331         if not mesh_objects:
332             self.report({'ERROR'}, "Select at least one mesh object")
333             return {'CANCELLED'}
334
335         for obj in mesh_objects:
336             fake_context["object"] = obj
337             # make each selected object a smoke flow
338             bpy.ops.object.modifier_add(fake_context, type='SMOKE')
339             obj.modifiers[-1].smoke_type = 'FLOW'
340
341             # set type
342             obj.modifiers[-1].flow_settings.smoke_flow_type = self.style
343
344             if not self.show_flows:
345                 obj.draw_type = 'WIRE'
346
347             # store bounding box min/max for the domain object
348             obj_bb_minmax(obj, min_co, max_co)
349
350         # add the smoke domain object
351         bpy.ops.mesh.primitive_cube_add()
352         obj = context.active_object
353         obj.name = "Smoke Domain"
354
355         # give the smoke some room above the flows
356         obj.location = 0.5 * (max_co + min_co) + Vector((0.0, 0.0, 1.0))
357         obj.scale = 0.5 * (max_co - min_co) + Vector((1.0, 1.0, 2.0))
358
359         # setup smoke domain
360         bpy.ops.object.modifier_add(type='SMOKE')
361         obj.modifiers[-1].smoke_type = 'DOMAIN'
362         if self.style == 'FIRE' or self.style == 'BOTH':
363             obj.modifiers[-1].domain_settings.use_high_resolution = True
364
365         # Setup material
366
367         # Cycles
368         if context.scene.render.use_shading_nodes:
369             bpy.ops.object.material_slot_add()
370
371             mat = bpy.data.materials.new("Smoke Domain Material")
372             obj.material_slots[0].material = mat
373
374             # Make sure we use nodes
375             mat.use_nodes = True
376
377             # Set node variables and clear the default nodes
378             tree = mat.node_tree
379             nodes = tree.nodes
380             links = tree.links
381
382             nodes.clear()
383
384             # Create shader nodes
385
386             # Material output
387             node_out = nodes.new(type='ShaderNodeOutputMaterial')
388             node_out.location = grid_location(6, 1)
389
390             # Add shader 1
391             node_add_shader_1 = nodes.new(type='ShaderNodeAddShader')
392             node_add_shader_1.location = grid_location(5, 1)
393             links.new(node_add_shader_1.outputs["Shader"],
394                     node_out.inputs["Volume"])
395
396             if self.style in {'SMOKE', 'FIRE', 'BOTH'}:
397                 # Smoke
398
399                 # Add shader 2
400                 node_add_shader_2 = nodes.new(type='ShaderNodeAddShader')
401                 node_add_shader_2.location = grid_location(4, 2)
402                 links.new(node_add_shader_2.outputs["Shader"],
403                         node_add_shader_1.inputs[0])
404
405                 # Volume scatter
406                 node_scatter = nodes.new(type='ShaderNodeVolumeScatter')
407                 node_scatter.location = grid_location(3, 3)
408                 links.new(node_scatter.outputs["Volume"],
409                         node_add_shader_2.inputs[0])
410
411                 # Volume absorption
412                 node_absorption = nodes.new(type='ShaderNodeVolumeAbsorption')
413                 node_absorption.location = grid_location(3, 2)
414                 links.new(node_absorption.outputs["Volume"],
415                         node_add_shader_2.inputs[1])
416
417                 # Density Multiplier
418                 node_densmult = nodes.new(type='ShaderNodeMath')
419                 node_densmult.location = grid_location(2, 2)
420                 node_densmult.operation = 'MULTIPLY'
421                 node_densmult.inputs[1].default_value = 5.0
422                 links.new(node_densmult.outputs["Value"],
423                         node_scatter.inputs["Density"])
424                 links.new(node_densmult.outputs["Value"],
425                         node_absorption.inputs["Density"])
426
427                 # Attribute "density"
428                 node_attrib_density = nodes.new(type='ShaderNodeAttribute')
429                 node_attrib_density.attribute_name = "density"
430                 node_attrib_density.location = grid_location(1, 2)
431                 links.new(node_attrib_density.outputs["Fac"],
432                         node_densmult.inputs[0])
433
434                 # Attribute "color"
435                 node_attrib_color = nodes.new(type='ShaderNodeAttribute')
436                 node_attrib_color.attribute_name = "color"
437                 node_attrib_color.location = grid_location(2, 3)
438                 links.new(node_attrib_color.outputs["Color"],
439                         node_scatter.inputs["Color"])
440                 links.new(node_attrib_color.outputs["Color"],
441                         node_absorption.inputs["Color"])
442
443             if self.style in {'FIRE', 'BOTH'}:
444                 # Fire
445
446                 # Emission
447                 node_emission = nodes.new(type='ShaderNodeEmission')
448                 node_emission.inputs["Color"].default_value = (0.8, 0.1, 0.01, 1.0)
449                 node_emission.location = grid_location(4, 1)
450                 links.new(node_emission.outputs["Emission"],
451                         node_add_shader_1.inputs[1])
452
453                 # Flame strength multiplier
454                 node_flame_strength_mult = nodes.new(type='ShaderNodeMath')
455                 node_flame_strength_mult.location = grid_location(3, 1)
456                 node_flame_strength_mult.operation = 'MULTIPLY'
457                 node_flame_strength_mult.inputs[1].default_value = 2.5
458                 links.new(node_flame_strength_mult.outputs["Value"],
459                         node_emission.inputs["Strength"])
460
461                 # Color ramp Flame
462                 node_flame_ramp = nodes.new(type='ShaderNodeValToRGB')
463                 node_flame_ramp.location = grid_location(1, 1)
464                 ramp = node_flame_ramp.color_ramp
465                 ramp.interpolation = 'EASE'
466
467                 # orange
468                 elem = ramp.elements.new(0.5)
469                 elem.color = (1.0, 0.128, 0.0, 1.0)
470
471                 # yellow
472                 elem = ramp.elements.new(0.9)
473                 elem.color = (0.9, 0.6, 0.1, 1.0)
474
475                 links.new(node_flame_ramp.outputs["Color"],
476                         node_emission.inputs["Color"])
477
478                 # Attribute "flame"
479                 node_attrib_flame = nodes.new(type='ShaderNodeAttribute')
480                 node_attrib_flame.attribute_name = "flame"
481                 node_attrib_flame.location = grid_location(0, 1)
482                 links.new(node_attrib_flame.outputs["Fac"],
483                         node_flame_ramp.inputs["Fac"])
484                 links.new(node_attrib_flame.outputs["Fac"],
485                         node_flame_strength_mult.inputs[0])
486
487         # Blender Internal
488         else:
489             # create a volume material with a voxel data texture for the domain
490             bpy.ops.object.material_slot_add()
491
492             mat = bpy.data.materials.new("Smoke Domain Material")
493             obj.material_slots[0].material = mat
494             mat.type = 'VOLUME'
495             mat.volume.density = 0
496             mat.volume.density_scale = 5
497             mat.volume.step_size = 0.1
498
499             tex = bpy.data.textures.new("Smoke Density", 'VOXEL_DATA')
500             tex.voxel_data.domain_object = obj
501             tex.voxel_data.interpolation = 'TRICUBIC_BSPLINE'
502
503             tex_slot = mat.texture_slots.add()
504             tex_slot.texture = tex
505             tex_slot.texture_coords = 'ORCO'
506             tex_slot.use_map_color_emission = False
507             tex_slot.use_map_density = True
508             tex_slot.use_map_color_reflection = True
509
510             # for fire add a second texture for flame emission
511             mat.volume.emission_color = Vector((0.0, 0.0, 0.0))
512             tex = bpy.data.textures.new("Flame", 'VOXEL_DATA')
513             tex.voxel_data.domain_object = obj
514             tex.voxel_data.smoke_data_type = 'SMOKEFLAME'
515             tex.voxel_data.interpolation = 'TRICUBIC_BSPLINE'
516             tex.use_color_ramp = True
517
518             tex_slot = mat.texture_slots.add()
519             tex_slot.texture = tex
520             tex_slot.texture_coords = 'ORCO'
521
522             # add color ramp for flame color
523             ramp = tex.color_ramp
524             # dark orange
525             elem = ramp.elements.new(0.333)
526             elem.color = (0.2, 0.03, 0.0, 1.0)
527
528             # yellow glow
529             elem = ramp.elements.new(0.666)
530             elem.color = (1, 0.65, 0.25, 1.0)
531
532             mat.texture_slots[1].use_map_density = True
533             mat.texture_slots[1].use_map_emission = True
534             mat.texture_slots[1].emission_factor = 5
535
536         return {'FINISHED'}
537
538
539 class QuickFluid(Operator):
540     bl_idname = "object.quick_fluid"
541     bl_label = "Quick Fluid"
542     bl_options = {'REGISTER', 'UNDO'}
543
544     style = EnumProperty(
545             name="Fluid Style",
546             items=(('INFLOW', "Inflow", ""),
547                    ('BASIC', "Basic", "")),
548             default='BASIC',
549             )
550     initial_velocity = FloatVectorProperty(
551             name="Initial Velocity",
552             description="Initial velocity of the fluid",
553             min=-100.0, max=100.0,
554             default=(0.0, 0.0, 0.0),
555             subtype='VELOCITY',
556             )
557     show_flows = BoolProperty(
558             name="Render Fluid Objects",
559             description="Keep the fluid objects visible during rendering",
560             default=False,
561             )
562     start_baking = BoolProperty(
563             name="Start Fluid Bake",
564             description=("Start baking the fluid immediately "
565                          "after creating the domain object"),
566             default=False,
567             )
568
569     def execute(self, context):
570         if not bpy.app.build_options.mod_fluid:
571             self.report({'ERROR'}, "Built without Fluid modifier support")
572             return {'CANCELLED'}
573
574         fake_context = context.copy()
575         mesh_objects = [obj for obj in context.selected_objects
576                         if (obj.type == 'MESH' and 0.0 not in obj.dimensions)]
577         min_co = Vector((100000.0, 100000.0, 100000.0))
578         max_co = -min_co
579
580         if not mesh_objects:
581             self.report({'ERROR'}, "Select at least one mesh object")
582             return {'CANCELLED'}
583
584         for obj in mesh_objects:
585             fake_context["object"] = obj
586             # make each selected object a fluid
587             bpy.ops.object.modifier_add(fake_context, type='FLUID_SIMULATION')
588
589             # fluid has to be before constructive modifiers,
590             # so it might not be the last modifier
591             for mod in obj.modifiers:
592                 if mod.type == 'FLUID_SIMULATION':
593                     break
594
595             if self.style == 'INFLOW':
596                 mod.settings.type = 'INFLOW'
597                 mod.settings.inflow_velocity = self.initial_velocity
598             else:
599                 mod.settings.type = 'FLUID'
600                 mod.settings.initial_velocity = self.initial_velocity
601
602             obj.hide_render = not self.show_flows
603             if not self.show_flows:
604                 obj.draw_type = 'WIRE'
605
606             # store bounding box min/max for the domain object
607             obj_bb_minmax(obj, min_co, max_co)
608
609         # add the fluid domain object
610         bpy.ops.mesh.primitive_cube_add()
611         obj = context.active_object
612         obj.name = "Fluid Domain"
613
614         # give the fluid some room below the flows
615         # and scale with initial velocity
616         v = 0.5 * self.initial_velocity
617         obj.location = 0.5 * (max_co + min_co) + Vector((0.0, 0.0, -1.0)) + v
618         obj.scale = (0.5 * (max_co - min_co) +
619                      Vector((1.0, 1.0, 2.0)) +
620                      Vector((abs(v[0]), abs(v[1]), abs(v[2])))
621                      )
622
623         # setup smoke domain
624         bpy.ops.object.modifier_add(type='FLUID_SIMULATION')
625         obj.modifiers[-1].settings.type = 'DOMAIN'
626
627         # make the domain smooth so it renders nicely
628         bpy.ops.object.shade_smooth()
629
630         # create a ray-transparent material for the domain
631         bpy.ops.object.material_slot_add()
632
633         mat = bpy.data.materials.new("Fluid Domain Material")
634         obj.material_slots[0].material = mat
635
636         mat.specular_intensity = 1
637         mat.specular_hardness = 100
638         mat.use_transparency = True
639         mat.alpha = 0.0
640         mat.transparency_method = 'RAYTRACE'
641         mat.raytrace_transparency.ior = 1.33
642         mat.raytrace_transparency.depth = 4
643
644         if self.start_baking:
645             bpy.ops.fluid.bake('INVOKE_DEFAULT')
646
647         return {'FINISHED'}
648
649
650 classes = (
651     QuickExplode,
652     QuickFluid,
653     QuickFur,
654     QuickSmoke,
655 )