Skip to content
Snippets Groups Projects
real_snow.py 14 KiB
Newer Older
  • Learn to ignore specific revisions
  • # SPDX-License-Identifier: GPL-2.0-or-later
    
    
    bl_info = {
        "name": "Real Snow",
        "description": "Generate snow mesh",
    
    Marco's avatar
    Marco committed
        "author": "Marco Pavanello, Drew Perttula",
    
        "version": (1, 3),
        "blender": (3, 1, 0),
    
        "location": "View 3D > Properties Panel",
    
        "doc_url": "{BLENDER_MANUAL_URL}/addons/object/real_snow.html",
    
        "tracker_url": "https://gitlab.com/marcopavanello/real-snow/-/issues",
    
        "support": "COMMUNITY",
        "category": "Object",
        }
    
    
    # Libraries
    import math
    import os
    import random
    import time
    
    import bpy
    import bmesh
    from bpy.props import BoolProperty, FloatProperty, IntProperty, PointerProperty
    from bpy.types import Operator, Panel, PropertyGroup
    from mathutils import Vector
    
    
    # Panel
    class REAL_PT_snow(Panel):
        bl_space_type = "VIEW_3D"
        bl_context = "objectmode"
        bl_region_type = "UI"
        bl_label = "Snow"
        bl_category = "Real Snow"
    
        def draw(self, context):
            scn = context.scene
            settings = scn.snow
            layout = self.layout
    
            col = layout.column(align=True)
            col.prop(settings, 'coverage', slider=True)
            col.prop(settings, 'height')
    
            layout.use_property_split = True
            layout.use_property_decorate = False
            flow = layout.grid_flow(row_major=True, columns=0, even_columns=False, even_rows=False, align=True)
            col = flow.column()
            col.prop(settings, 'vertices')
    
            row = layout.row(align=True)
            row.scale_y = 1.5
            row.operator("snow.create", text="Add Snow", icon="FREEZE")
    
    
    class SNOW_OT_Create(Operator):
        bl_idname = "snow.create"
        bl_label = "Create Snow"
        bl_description = "Create snow"
        bl_options = {'REGISTER', 'UNDO'}
    
        @classmethod
        def poll(cls, context) -> bool:
            return bool(context.selected_objects)
    
        def execute(self, context):
            coverage = context.scene.snow.coverage
            height = context.scene.snow.height
            vertices = context.scene.snow.vertices
    
    
    Marco's avatar
    Marco committed
            # Get a list of selected objects, except non-mesh objects
    
            input_objects = [obj for obj in context.selected_objects if obj.type == 'MESH']
            snow_list = []
    
    Marco's avatar
    Marco committed
            # Start UI progress bar
    
            length = len(input_objects)
            context.window_manager.progress_begin(0, 10)
    
    Marco's avatar
    Marco committed
            timer = 0
    
            for obj in input_objects:
    
    Marco's avatar
    Marco committed
                # Timer
    
                context.window_manager.progress_update(timer)
    
    Marco's avatar
    Marco committed
                # Duplicate mesh
    
                bpy.ops.object.select_all(action='DESELECT')
                obj.select_set(True)
                context.view_layer.objects.active = obj
                object_eval = obj.evaluated_get(context.view_layer.depsgraph)
                mesh_eval = bpy.data.meshes.new_from_object(object_eval)
                snow_object = bpy.data.objects.new("Snow", mesh_eval)
                snow_object.matrix_world = obj.matrix_world
                context.collection.objects.link(snow_object)
                bpy.ops.object.select_all(action='DESELECT')
                context.view_layer.objects.active = snow_object
                snow_object.select_set(True)
                bpy.ops.object.mode_set(mode = 'EDIT')
                bm_orig = bmesh.from_edit_mesh(snow_object.data)
                bm_copy = bm_orig.copy()
                bm_copy.transform(obj.matrix_world)
                bm_copy.normal_update()
    
    Marco's avatar
    Marco committed
                # Get faces data
    
                delete_faces(vertices, bm_copy, snow_object)
                ballobj = add_metaballs(context, height, snow_object)
                context.view_layer.objects.active = snow_object
                surface_area = area(snow_object)
                snow = add_particles(context, surface_area, height, coverage, snow_object, ballobj)
                add_modifiers(snow)
    
    Marco's avatar
    Marco committed
                # Place inside collection
    
                context.view_layer.active_layer_collection = context.view_layer.layer_collection
                if "Snow" not in context.scene.collection.children:
                    coll = bpy.data.collections.new("Snow")
                    context.scene.collection.children.link(coll)
                else:
                    coll = bpy.data.collections["Snow"]
                coll.objects.link(snow)
                context.view_layer.layer_collection.collection.objects.unlink(snow)
                add_material(snow)
    
    Marco's avatar
    Marco committed
                # Parent with object
    
                snow.parent = obj
                snow.matrix_parent_inverse = obj.matrix_world.inverted()
    
    Marco's avatar
    Marco committed
                # Add snow to list
    
                snow_list.append(snow)
    
    Marco's avatar
    Marco committed
                # Update progress bar
    
                timer += 0.1 / length
    
    Marco's avatar
    Marco committed
            # Select created snow meshes
    
            for s in snow_list:
                s.select_set(True)
    
    Marco's avatar
    Marco committed
            # End progress bar
    
            context.window_manager.progress_end()
    
            return {'FINISHED'}
    
    
    def add_modifiers(snow):
        bpy.ops.object.transform_apply(location=False, scale=True, rotation=False)
    
    Marco's avatar
    Marco committed
        # Decimate the mesh to get rid of some visual artifacts
    
        snow.modifiers.new("Decimate", 'DECIMATE')
        snow.modifiers["Decimate"].ratio = 0.5
        snow.modifiers.new("Subdiv", "SUBSURF")
        snow.modifiers["Subdiv"].render_levels = 1
        snow.modifiers["Subdiv"].quality = 1
        snow.cycles.use_adaptive_subdivision = True
    
    
    def add_particles(context, surface_area: float, height: float, coverage: float, snow_object: bpy.types.Object, ballobj: bpy.types.Object):
    
    Marco's avatar
    Marco committed
        # Approximate the number of particles to be emitted
        number = int(surface_area * 50 * (height ** -2) * ((coverage / 100) ** 2))
    
        bpy.ops.object.particle_system_add()
        particles = snow_object.particle_systems[0]
        psettings = particles.settings
        psettings.type = 'HAIR'
        psettings.render_type = 'OBJECT'
    
    Marco's avatar
    Marco committed
        # Generate random number for seed
    
        random_seed = random.randint(0, 1000)
        particles.seed = random_seed
    
    Marco's avatar
    Marco committed
        # Set particles object
    
        psettings.particle_size = height
        psettings.instance_object = ballobj
        psettings.count = number
    
    Marco's avatar
    Marco committed
        # Convert particles to mesh
    
        bpy.ops.object.select_all(action='DESELECT')
        context.view_layer.objects.active = ballobj
        ballobj.select_set(True)
        bpy.ops.object.convert(target='MESH')
        snow = bpy.context.active_object
        snow.scale = [0.09, 0.09, 0.09]
        bpy.ops.object.origin_set(type='ORIGIN_GEOMETRY')
        bpy.ops.object.select_all(action='DESELECT')
        snow_object.select_set(True)
        bpy.ops.object.delete()
        snow.select_set(True)
        return snow
    
    
    def add_metaballs(context, height: float, snow_object: bpy.types.Object) -> bpy.types.Object:
        ball_name = "SnowBall"
        ball = bpy.data.metaballs.new(ball_name)
        ballobj = bpy.data.objects.new(ball_name, ball)
        bpy.context.scene.collection.objects.link(ballobj)
    
    Marco's avatar
    Marco committed
        # These settings have proven to work on a large amount of scenarios
        ball.resolution = 0.7 * height + 0.3
    
        ball.threshold = 1.3
        element = ball.elements.new()
        element.radius = 1.5
        element.stiffness = 0.75
        ballobj.scale = [0.09, 0.09, 0.09]
        return ballobj
    
    
    def delete_faces(vertices, bm_copy, snow_object: bpy.types.Object):
    
    Marco's avatar
    Marco committed
        # Find upper faces
    
        if vertices:
    
    Marco's avatar
    Marco committed
            selected_faces = set(face.index for face in bm_copy.faces if face.select)
        # Based on a certain angle, find all faces not pointing up
        down_faces = set(face.index for face in bm_copy.faces if Vector((0, 0, -1.0)).angle(face.normal, 4.0) < (math.pi / 2.0 + 0.5))
    
        bm_copy.free()
        bpy.ops.mesh.select_all(action='DESELECT')
    
    Marco's avatar
    Marco committed
        # Select upper faces
    
        mesh = bmesh.from_edit_mesh(snow_object.data)
        for face in mesh.faces:
            if vertices:
    
    Marco's avatar
    Marco committed
                if face.index not in selected_faces:
    
                    face.select = True
            if face.index in down_faces:
                face.select = True
    
    Marco's avatar
    Marco committed
        # Delete unnecessary faces
    
        faces_select = [face for face in mesh.faces if face.select]
        bmesh.ops.delete(mesh, geom=faces_select, context='FACES_KEEP_BOUNDARY')
        mesh.free()
        bpy.ops.object.mode_set(mode = 'OBJECT')
    
    
    def area(obj: bpy.types.Object) -> float:
        bm_obj = bmesh.new()
        bm_obj.from_mesh(obj.data)
        bm_obj.transform(obj.matrix_world)
        area = sum(face.calc_area() for face in bm_obj.faces)
        bm_obj.free
        return area
    
    
    def add_material(obj: bpy.types.Object):
        mat_name = "Snow"
    
    Marco's avatar
    Marco committed
        # If material doesn't exist, create it
    
        if mat_name in bpy.data.materials:
    
            bpy.data.materials[mat_name].name = mat_name+".001"
    
        mat = bpy.data.materials.new(mat_name)
        mat.use_nodes = True
        nodes = mat.node_tree.nodes
    
    Marco's avatar
    Marco committed
        # Delete all nodes
    
        for node in nodes:
    
            nodes.remove(node)
    
    Marco's avatar
    Marco committed
        # Add nodes
    
        output = nodes.new('ShaderNodeOutputMaterial')
        principled = nodes.new('ShaderNodeBsdfPrincipled')
        vec_math = nodes.new('ShaderNodeVectorMath')
        com_xyz = nodes.new('ShaderNodeCombineXYZ')
        dis = nodes.new('ShaderNodeDisplacement')
        mul1 = nodes.new('ShaderNodeMath')
        add1 = nodes.new('ShaderNodeMath')
        add2 = nodes.new('ShaderNodeMath')
        mul2 = nodes.new('ShaderNodeMath')
        mul3 = nodes.new('ShaderNodeMath')
    
        range1 = nodes.new('ShaderNodeMapRange')
        range2 = nodes.new('ShaderNodeMapRange')
        range3 = nodes.new('ShaderNodeMapRange')
    
        vor = nodes.new('ShaderNodeTexVoronoi')
        noise1 = nodes.new('ShaderNodeTexNoise')
        noise2 = nodes.new('ShaderNodeTexNoise')
        noise3 = nodes.new('ShaderNodeTexNoise')
        mapping = nodes.new('ShaderNodeMapping')
        coord = nodes.new('ShaderNodeTexCoord')
    
    Marco's avatar
    Marco committed
        # Change location
    
        output.location = (100, 0)
    
        principled.location = (-200, 600)
    
        vec_math.location = (-400, 400)
        com_xyz.location = (-600, 400)
        dis.location = (-200, -100)
        mul1.location = (-400, -100)
        add1.location = (-600, -100)
        add2.location = (-800, -100)
        mul2.location = (-1000, -100)
        mul3.location = (-1000, -300)
    
        range1.location = (-400, 200)
        range2.location = (-1200, -300)
        range3.location = (-800, -300)
    
        vor.location = (-1500, 200)
        noise1.location = (-1500, 0)
    
        noise2.location = (-1500, -250)
        noise3.location = (-1500, -500)
    
        mapping.location = (-1700, 0)
        coord.location = (-1900, 0)
    
    Marco's avatar
    Marco committed
        # Change node parameters
    
        principled.distribution = "MULTI_GGX"
        principled.subsurface_method = "RANDOM_WALK"
        principled.inputs[0].default_value[0] = 0.904
        principled.inputs[0].default_value[1] = 0.904
        principled.inputs[0].default_value[2] = 0.904
        principled.inputs[1].default_value = 1
        principled.inputs[2].default_value[0] = 0.36
        principled.inputs[2].default_value[1] = 0.46
        principled.inputs[2].default_value[2] = 0.6
        principled.inputs[3].default_value[0] = 0.904
        principled.inputs[3].default_value[1] = 0.904
        principled.inputs[3].default_value[2] = 0.904
    
        principled.inputs[7].default_value = 0.224
        principled.inputs[9].default_value = 0.1
        principled.inputs[15].default_value = 0.1
    
        vec_math.operation = "MULTIPLY"
        vec_math.inputs[1].default_value[0] = 0.5
        vec_math.inputs[1].default_value[1] = 0.5
        vec_math.inputs[1].default_value[2] = 0.5
        com_xyz.inputs[0].default_value = 0.36
        com_xyz.inputs[1].default_value = 0.46
        com_xyz.inputs[2].default_value = 0.6
        dis.inputs[1].default_value = 0.1
        dis.inputs[2].default_value = 0.3
        mul1.operation = "MULTIPLY"
        mul1.inputs[1].default_value = 0.1
        mul2.operation = "MULTIPLY"
        mul2.inputs[1].default_value = 0.6
        mul3.operation = "MULTIPLY"
        mul3.inputs[1].default_value = 0.4
    
        range1.inputs[1].default_value = 0.525
        range1.inputs[2].default_value = 0.58
        range2.inputs[1].default_value = 0.069
        range2.inputs[2].default_value = 0.757
        range3.inputs[1].default_value = 0.069
        range3.inputs[2].default_value = 0.757
    
        vor.feature = "N_SPHERE_RADIUS"
        vor.inputs[2].default_value = 30
        noise1.inputs[2].default_value = 12
        noise2.inputs[2].default_value = 2
        noise2.inputs[3].default_value = 4
        noise3.inputs[2].default_value = 1
        noise3.inputs[3].default_value = 4
        mapping.inputs[3].default_value[0] = 12
        mapping.inputs[3].default_value[1] = 12
        mapping.inputs[3].default_value[2] = 12
    
    Marco's avatar
    Marco committed
        # Link nodes
    
        link = mat.node_tree.links
        link.new(principled.outputs[0], output.inputs[0])
        link.new(vec_math.outputs[0], principled.inputs[2])
        link.new(com_xyz.outputs[0], vec_math.inputs[0])
        link.new(dis.outputs[0], output.inputs[2])
        link.new(mul1.outputs[0], dis.inputs[0])
        link.new(add1.outputs[0], mul1.inputs[0])
        link.new(add2.outputs[0], add1.inputs[0])
        link.new(mul2.outputs[0], add2.inputs[0])
        link.new(mul3.outputs[0], add2.inputs[1])
    
        link.new(range1.outputs[0], principled.inputs[14])
        link.new(range2.outputs[0], mul3.inputs[0])
        link.new(range3.outputs[0], add1.inputs[1])
        link.new(vor.outputs[4], range1.inputs[0])
    
        link.new(noise1.outputs[0], mul2.inputs[0])
    
        link.new(noise2.outputs[0], range2.inputs[0])
        link.new(noise3.outputs[0], range3.inputs[0])
    
        link.new(mapping.outputs[0], vor.inputs[0])
        link.new(mapping.outputs[0], noise1.inputs[0])
        link.new(mapping.outputs[0], noise2.inputs[0])
        link.new(mapping.outputs[0], noise3.inputs[0])
        link.new(coord.outputs[3], mapping.inputs[0])
    
    Marco's avatar
    Marco committed
        # Set displacement and add material
    
        mat.cycles.displacement_method = "DISPLACEMENT"
        obj.data.materials.append(mat)
    
    
    # Properties
    class SnowSettings(PropertyGroup):
        coverage : IntProperty(
            name = "Coverage",
            description = "Percentage of the object to be covered with snow",
            default = 100,
            min = 0,
            max = 100,
            subtype = 'PERCENTAGE'
            )
    
        height : FloatProperty(
            name = "Height",
            description = "Height of the snow",
            default = 0.3,
            step = 1,
            precision = 2,
            min = 0.1,
            max = 1
            )
    
        vertices : BoolProperty(
            name = "Selected Faces",
            description = "Add snow only on selected faces",
            default = False
            )
    
    
    #############################################################################################
    classes = (
        REAL_PT_snow,
        SNOW_OT_Create,
        SnowSettings
        )
    
    register, unregister = bpy.utils.register_classes_factory(classes)
    
    # Register
    def register():
        for cls in classes:
            bpy.utils.register_class(cls)
        bpy.types.Scene.snow = PointerProperty(type=SnowSettings)
    
    
    # Unregister
    def unregister():
        for cls in classes:
            bpy.utils.unregister_class(cls)
        del bpy.types.Scene.snow
    
    
    if __name__ == "__main__":
        register()