Skip to content
Snippets Groups Projects
object_cloud_gen.py 22 KiB
Newer Older
# ##### BEGIN GPL LICENSE BLOCK #####
#
#  This program is free software; you can redistribute it and/or
#  modify it under the terms of the GNU General Public License
#  as published by the Free Software Foundation; either version 2
#  of the License, or (at your option) any later version.
#
#  This program is distributed in the hope that it will be useful,
#  but WITHOUT ANY WARRANTY; without even the implied warranty of
#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#  GNU General Public License for more details.
#
#  You should have received a copy of the GNU General Public License
#  along with this program; if not, write to the Free Software Foundation,
#  Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# ##### END GPL LICENSE BLOCK #####


bl_addon_info = {
    'name': 'Object: Cloud Generator',
    'author': 'Nick Keeline(nrk)',
    'blender': (2, 5, 3),
    'location': 'Tool Shelf ',
    'description': 'Creates Volumetric Clouds',
    'url': 'http://wiki.blender.org/index.php/Extensions:2.5/Py/' \
        'Scripts/Object/Cloud_Gen',
    'category': 'Object'}


"""
Place this file in the .blender/scripts/addons dir
You have to activated the script in the "Add-Ons" tab (user preferences).
The functionality can then be accessed via the Tool shelf when objects
are selected

Rev 0 initial release
Rev 0.1 added scene to create_mesh per python api change.
Rev 0.2 Added Point Density turbulence and fixed degenerate
Rev 0.3 Fixed bug in degenerate
"""

import bpy
import mathutils
from math import *
from bpy.props import *

# Deselect All
bpy.ops.object.select_all(action='DESELECT')


# This routine takes an object and deletes all of the geometry in it
# and adds a bounding box to it.
# It will add or subtract the bound box size by the variable sizeDifference.
def makeObjectIntoBoundBox(object, sizeDifference):
    # Deselect All
    bpy.ops.object.select_all(action='DESELECT')

    # Select the object
    object.selected = True

    # Go into Edit Mode
    bpy.ops.object.mode_set(mode='EDIT')

    mesh = object.data
    verts = mesh.verts

    #Set the max and min verts to the first vertex on the list
    maxVert = [verts[0].co[0], verts[0].co[1], verts[0].co[2]]
    minVert = [verts[0].co[0], verts[0].co[1], verts[0].co[2]]

    #Create Max and Min Vertex array for the outer corners of the box
    for vert in verts:
        #Max vertex
        if vert.co[0] > maxVert[0]:
            maxVert[0] = vert.co[0]
        if vert.co[1] > maxVert[1]:
            maxVert[1] = vert.co[1]
        if vert.co[2] > maxVert[2]:
            maxVert[2] = vert.co[2]

        #Min Vertex
        if vert.co[0] < minVert[0]:
            minVert[0] = vert.co[0]
        if vert.co[1] < minVert[1]:
            minVert[1] = vert.co[1]
        if vert.co[2] < minVert[2]:
            minVert[2] = vert.co[2]

    #Add the size difference to the max size of the box
    maxVert[0] = maxVert[0] + sizeDifference
    maxVert[1] = maxVert[1] + sizeDifference
    maxVert[2] = maxVert[2] + sizeDifference

    #subtract the size difference to the min size of the box
    minVert[0] = minVert[0] - sizeDifference
    minVert[1] = minVert[1] - sizeDifference
    minVert[2] = minVert[2] - sizeDifference

    #Create arrays of verts and faces to be added to the mesh
    addVerts = []

    #X high loop
    addVerts.append([maxVert[0], maxVert[1], maxVert[2]])
    addVerts.append([maxVert[0], maxVert[1], minVert[2]])
    addVerts.append([maxVert[0], minVert[1], minVert[2]])
    addVerts.append([maxVert[0], minVert[1], maxVert[2]])

    #x low loop
    addVerts.append([minVert[0], maxVert[1], maxVert[2]])
    addVerts.append([minVert[0], maxVert[1], minVert[2]])
    addVerts.append([minVert[0], minVert[1], minVert[2]])
    addVerts.append([minVert[0], minVert[1], maxVert[2]])

    # Make the faces of the bounding box.
    addFaces = []

    # Draw a box on paper and number the vertices.
    # Use right hand rule to come up with number orders for faces on
    # the box (with normals pointing out).
    addFaces.append([0, 3, 2, 1])
    addFaces.append([4, 5, 6, 7])
    addFaces.append([0, 1, 5, 4])
    addFaces.append([1, 2, 6, 5])
    addFaces.append([2, 3, 7, 6])
    addFaces.append([0, 4, 7, 3])

    # Delete all geometry from the object.
    bpy.ops.mesh.select_all(action='SELECT')
    bpy.ops.mesh.delete(type='ALL')

    # Must be in object mode for from_pydata to work
    bpy.ops.object.mode_set(mode='OBJECT')

    # Add the mesh data.
    mesh.from_pydata(addVerts, [], addFaces)

    # Update the mesh
    mesh.update()


def applyScaleRotLoc(scene, obj):
    # Deselect All
    bpy.ops.object.select_all(action='DESELECT')

    # Select the object
    obj.selected = True
    scene.objects.active = obj

    #bpy.ops.object.rotation_apply()
    bpy.ops.object.location_apply()
    bpy.ops.object.scale_apply()


def makeParent(parentobj, childobj, scene):

    applyScaleRotLoc(scene, parentobj)

    applyScaleRotLoc(scene, childobj)

    childobj.parent = parentobj

    #childobj.location = childobj.location - parentobj.location


def addNewObject(scene, name, copyobj):
    '''
    Add an object and do other silly stuff.
    '''
    # Create new mesh
    mesh = bpy.data.meshes.new(name)

    # Create a new object.
    ob_new = bpy.data.objects.new(name, mesh)
    tempme = copyobj.data
    ob_new.data = tempme.copy()
    ob_new.scale = copyobj.scale
    ob_new.location = copyobj.location

    # Link new object to the given scene and select it.
    scene.objects.link(ob_new)
    ob_new.selected = True

    return ob_new


def combineObjects(scene, combined, listobjs):
    # scene is the current scene
    # combined is the object we want to combine everything into
    # listobjs is the list of objects to stick into combined

    # Deselect All
    bpy.ops.object.select_all(action='DESELECT')

    # Select the new object.
    combined.selected = True
    scene.objects.active = combined

    # Add data
    if (len(listobjs) > 0):
            for i in listobjs:
                # Add a modifier
                bpy.ops.object.modifier_add(type='BOOLEAN')

                union = combined.modifiers
                union[0].name = "AddEmUp"
                union[0].object = i
                union[0].operation = 'UNION'

                # Apply modifier
                # Can't use bpy.ops.object.modifier_apply because poll fails.
                combined.data = combined.create_mesh(scene,
                    apply_modifiers=True,
                    settings='PREVIEW')
                combined.modifiers.remove(union[0])


# Returns True if we want to degenerate
# and False if we want to generate a new cloud.
def degenerateCloud(obj):
    if not obj:
        return False

    if "CloudMember" in obj:
        if obj["CloudMember"] != None:
            if obj.parent:
               if "CloudMember" not in obj.parent:
                return False

    return False


class View3DPanel(bpy.types.Panel):
    bl_space_type = 'VIEW_3D'
    bl_region_type = 'TOOLS'


class VIEW3D_PT_tools_cloud(View3DPanel):
    bl_label = "Cloud Generator"
    bl_context = "objectmode"

    def draw(self, context):
        active_obj = context.active_object

        degenerate = degenerateCloud(active_obj)

        if active_obj and degenerate:
            layout = self.layout

            col = layout.column(align=True)
            col.operator("cloud.generate_cloud", text="DeGenerate")

        elif active_obj is None:
            layout = self.layout

            col = layout.column(align=True)
            col.label(text="Select one or more")
            col.label(text="objects to generate")
            col.label(text="a cloud.")
        elif "CloudMember" in  active_obj:
            layout = self.layout

            col = layout.column(align=True)
            col.label(text="Must select")
            col.label(text="bound box")
           
        elif active_obj and active_obj.type == 'MESH':
            layout = self.layout

            col = layout.column(align=True)
            col.operator("cloud.generate_cloud", text="Generate Cloud")

        else:
            layout = self.layout

            col = layout.column(align=True)
            col.label(text="Select one or more")
            col.label(text="objects to generate")
            col.label(text="a cloud.")
        # col.label(active_obj["CloudMember"])

classes = [VIEW3D_PT_tools_cloud]


def register():
    register = bpy.types.register
    for cls in classes:
        register(cls)


def unregister():
    unregister = bpy.types.unregister
    for cls in classes:
        unregister(cls)

if __name__ == "__main__":
    register()


class GenerateCloud(bpy.types.Operator):
    bl_idname = "cloud.generate_cloud"
    bl_label = "Generate Cloud"
    bl_description = "Create a Cloud."
    bl_register = True
    bl_undo = True

    def execute(self, context):
        # Make variable that is the current .blend file main data blocks
        main = context.main

        # Make variable that is the active object selected by user
        active_object = context.active_object

        # Make variable scene that is current scene
        scene = context.scene

        if active_object and active_object.type == 'MESH':
            # Parameters the user may want to change:
            # Number of points this number is multiplied by the volume to get
            # the number of points the scripts will put in the volume.
            numOfPoints = 35
            maxNumOfPoints = 100000
            pointDensityRadius = 0.4

            # Should we degnerate?
            degenerate = degenerateCloud(active_object)

            if degenerate:
                if active_object is not None:
                   # Degenerate Cloud
                   mainObj = active_object

                   cloudMembers = active_object.children

                   createdObjects = []
                   definitionObjects = []
                   for member in cloudMembers:
                       applyScaleRotLoc(scene, member)
                       if (member["CloudMember"] == "CreatedObj"):
                          createdObjects.append(member)
                       else:
                          definitionObjects.append(member)

                   for defObj in definitionObjects:
                       # @todo check if it wouldn't be better to remove this
                       # in the first place (see del() in degenerateCloud)
                       #totally agree didn't know how before now...thanks! done.
                       if "CloudMember" in defObj:
                          del(defObj["CloudMember"])

                   for createdObj in createdObjects:
                       # Deselect All
                       bpy.ops.object.select_all(action='DESELECT')
   
                       # Select the object and delete it.
                       createdObj.selected = True
                       scene.objects.active = createdObj
                       bpy.ops.object.delete()

                   # Delete the main object
                   # Deselect All
                   bpy.ops.object.select_all(action='DESELECT')
   
                   # Select the object and delete it.
                   mainObj.selected = True
                   scene.objects.active = mainObj
   
                   # Delete all material slots in mainObj object
                   for i in range(len(mainObj.material_slots)):
                       mainObj.active_material_index = i - 1
                       bpy.ops.object.material_slot_remove()
  
                   # Delete the Main Object
                   bpy.ops.object.delete()
  
                   # Select all of the left over boxes so people can immediately
                   # press generate again if they want.
                   for eachMember in definitionObjects:
                       eachMember.max_draw_type = 'SOLID'
                       eachMember.selected = True
                       scene.objects.active = eachMember

            else:
                # Generate Cloud

                ###############Create Combined Object bounds##################
                # Make a list of all Selected objects.
                selectedObjects = bpy.context.selected_objects

                # Create a new object bounds
                    bounds = addNewObject(scene,
                        "CloudBounds",
                        [])

                else:
                    bounds = addNewObject(scene,
                        "CloudBounds",
                        selectedObjects[0])

                bounds.max_draw_type = 'BOUNDS'
                bounds.restrict_render = False

                # Just add a Definition Property designating this
                # as the main object.
                bounds["CloudMember"] = "MainObj"

                # Since we used iteration 0 to copy with object we
                # delete it off the list.
                firstObject = selectedObjects[0]
                del selectedObjects[0]

                # Apply location Rotation and Scale to all objects involved.
                applyScaleRotLoc(scene, bounds)
                for each in selectedObjects:
                    applyScaleRotLoc(scene, each)

                # Let's combine all of them together.
                combineObjects(scene, bounds, selectedObjects)

                # Let's add some property info to the objects.
                for selObj in selectedObjects:
                    selObj["CloudMember"] = "DefinitioinObj"
                    selObj.name = "DefinitioinObj"
                    selObj.max_draw_type = 'WIRE'
                    selObj.restrict_render = True
                    makeParent(bounds, selObj, scene)

                # Do the same to the 1. object since it is no longer in list.
                firstObject["CloudMember"] = "DefinitioinObj"
                firstObject.name = "DefinitioinObj"
                firstObject.max_draw_type = 'WIRE'
                firstObject.restrict_render = True
                makeParent(bounds, firstObject, scene)

                ###############Create Cloud for putting Cloud Mesh############
                # Create a new object cloud.
                cloud = addNewObject(scene, "CloudMesh", bounds)
                cloud["CloudMember"] = "CreatedObj"
                cloud.max_draw_type = 'WIRE'
                cloud.restrict_render = True

                makeParent(bounds, cloud, scene)

                bpy.ops.object.editmode_toggle()
                bpy.ops.mesh.select_all(action='SELECT')
                bpy.ops.mesh.subdivide(number_cuts=2, fractal=0, smoothness=1)
                bpy.ops.object.location_apply()
                bpy.ops.mesh.vertices_smooth(repeat=20)
                bpy.ops.mesh.tris_convert_to_quads()
                bpy.ops.mesh.faces_shade_smooth()
                bpy.ops.object.editmode_toggle()

                ###############Create Particles in cloud obj##################
                # Turn off gravity.
                scene.use_gravity = False

                # Set time to 0.
                scene.frame_current = 0

                # Add a new particle system.
                bpy.ops.object.particle_system_add()

                #Particle settings setting it up!
                cloudParticles = cloud.active_particle_system
                cloudParticles.name = "CloudParticles"
                cloudParticles.settings.frame_start = 0
                cloudParticles.settings.frame_end = 0
                cloudParticles.settings.emit_from = 'VOLUME'
                cloudParticles.settings.draw_as = 'DOT'
                cloudParticles.settings.ren_as = 'NONE'
                cloudParticles.settings.normal_factor = 0
                cloudParticles.settings.distribution = 'RAND'

                ####################Create Volume Material####################
                # Deselect All
                bpy.ops.object.select_all(action='DESELECT')

                # Select the object.
                bounds.selected = True
                scene.objects.active = bounds

                # Turn bounds object into a box.
                makeObjectIntoBoundBox(bounds, .2)

                # Delete all material slots in bounds object.
                for i in range(len(bounds.material_slots)):
                    bounds.active_material_index = i - 1
                    bpy.ops.object.material_slot_remove()

                # Add a new material.
                cloudMaterial = main.materials.new("CloudMaterial")
                bpy.ops.object.material_slot_add()
                bounds.material_slots[0].material = cloudMaterial

                # Set Up the Cloud Material
                cloudMaterial.name = "CloudMaterial"
                cloudMaterial.type = 'VOLUME'
                mVolume = cloudMaterial.volume
                mVolume.scattering = scattering
                mVolume.density = 0
                mVolume.density_scale = densityScale
                mVolume.transmission_color = [3, 3, 3]
                mVolume.step_size = 0.1
                mVolume.light_cache = True
                mVolume.cache_resolution = 75

                # Add a texture
                vMaterialTextureSlots = cloudMaterial.texture_slots
                cloudtex = main.textures.new("CloudTex")
                cloudMaterial.add_texture(cloudtex, 'ORCO')
                cloudtex.type = 'CLOUDS'
                cloudtex.noise_type = 'HARD_NOISE'
                cloudtex.noise_size = 2

                # Add a texture
                cloudPointDensity = main.textures.new("CloudPointDensity")
                cloudPointDensity.type = 'POINT_DENSITY'
                cloudMaterial.add_texture(cloudPointDensity, 'ORCO')
                pDensity = vMaterialTextureSlots[1].texture
                vMaterialTextureSlots[1].map_density = True
                vMaterialTextureSlots[1].rgb_to_intensity = True
                vMaterialTextureSlots[1].texture_coordinates = 'GLOBAL'
                pDensity.pointdensity.radius = pointDensityRadius
                pDensity.pointdensity.vertices_cache = 'WORLD_SPACE'
                pDensity.pointdensity.turbulence = True
                pDensity.pointdensity.noise_basis = 'VORONOI_F2'
                pDensity.pointdensity.turbulence_depth = 3

                pDensity.use_color_ramp = True
                pRamp = pDensity.color_ramp
                pRamp.interpolation = 'LINEAR'
                pRampElements = pRamp.elements
                #pRampElements[1].position = .9
                #pRampElements[1].color = [.18,.18,.18,.8]

                # Estimate the number of particles for the size of bounds.
                numParticles = int(bounds.dimensions[0] * bounds.dimensions[1]
                    * bounds.dimensions[2]) * numOfPoints
                if numParticles > maxNumOfPoints:
                    numParticles = maxNumOfPoints
                print(numParticles)

                # Set the number of particles according to the volume
                # of bounds.
                cloudParticles.settings.amount = numParticles

                # Set time to 1.
                scene.frame_current = 1

                ###############Create CloudPnts for putting points in#########
                # Create a new object cloudPnts
                cloudPnts = addNewObject(scene, "CloudPoints", bounds)
                cloudPnts["CloudMember"] = "CreatedObj"
                cloudPnts.max_draw_type = 'WIRE'
                cloudPnts.restrict_render = True

                makeParent(bounds, cloudPnts, scene)

                bpy.ops.object.editmode_toggle()
                bpy.ops.mesh.select_all(action='SELECT')

                bpy.ops.mesh.delete(type='ALL')

                meshPnts = cloudPnts.data

                listCloudParticles = cloudParticles.particles

                listMeshPnts = []
                for pTicle in listCloudParticles:
                    listMeshPnts.append(pTicle.location)

                # Must be in object mode fro from_pydata to work.
                bpy.ops.object.mode_set(mode='OBJECT')

                # Add in the mesh data.
                meshPnts.from_pydata(listMeshPnts, [], [])

                # Update the mesh.
                meshPnts.update()

                # Add a modifier.
                bpy.ops.object.modifier_add(type='DISPLACE')

                cldPntsModifiers = cloudPnts.modifiers
                cldPntsModifiers[0].name = "CloudPnts"
                cldPntsModifiers[0].texture = cloudtex
                cldPntsModifiers[0].texture_coordinates = 'OBJECT'
                cldPntsModifiers[0].texture_coordinate_object = cloud
                cldPntsModifiers[0].strength = -1.4

                # Apply modifier
                # Can't use bpy.ops.object.modifier_apply because poll fails.
                cloudPnts.data = cloudPnts.create_mesh(scene,
                    apply_modifiers=True,
                    settings='PREVIEW')
                cloudPnts.modifiers.remove(cldPntsModifiers[0])

                pDensity.pointdensity.point_source = 'OBJECT'
                pDensity.pointdensity.object = cloudPnts

                # Deselect All
                bpy.ops.object.select_all(action='DESELECT')

                # Select the object.
                cloud.selected = True
                scene.objects.active = cloud

                bpy.ops.object.particle_system_remove()

                # Deselect All
                bpy.ops.object.select_all(action='DESELECT')

                # Select the object.
                bounds.selected = True
                scene.objects.active = bounds

                # Add a force field to the points.
                #cloudField = bounds.field
                #cloudField.type = 'TEXTURE'
                #cloudField.strength = 2
                #cloudField.texture = cloudtex

                # Set time
                #for i in range(12):
                #    scene.current_frame = i
                #    scene.update()
                #bpy.ops.ptcache.bake_all(bake=False)

            #self.report({'WARNING'}, "Generating Cloud")

        return {'FINISHED'}

bpy.types.register(GenerateCloud)

if __name__ == "__main__":