Import images: add file handler
[blender-addons.git] / real_snow.py
blob771fec34db4bcfc679c640069cfc2a411b5ea2f2
1 # SPDX-FileCopyrightText: 2020-2022 Blender Foundation
3 # SPDX-License-Identifier: GPL-2.0-or-later
5 bl_info = {
6 "name": "Real Snow",
7 "description": "Generate snow mesh",
8 "author": "Marco Pavanello, Drew Perttula",
9 "version": (1, 3, 2),
10 "blender": (4, 1, 0),
11 "location": "View 3D > Properties Panel",
12 "doc_url": "{BLENDER_MANUAL_URL}/addons/object/real_snow.html",
13 "tracker_url": "https://gitlab.com/marcopavanello/real-snow/-/issues",
14 "support": "COMMUNITY",
15 "category": "Object",
19 # Libraries
20 import math
21 import os
22 import random
23 import time
25 import bpy
26 import bmesh
27 from bpy.props import BoolProperty, FloatProperty, IntProperty, PointerProperty
28 from bpy.types import Operator, Panel, PropertyGroup
29 from mathutils import Vector
32 # Panel
33 class REAL_PT_snow(Panel):
34 bl_space_type = "VIEW_3D"
35 bl_context = "objectmode"
36 bl_region_type = "UI"
37 bl_label = "Snow"
38 bl_category = "Real Snow"
40 def draw(self, context):
41 scn = context.scene
42 settings = scn.snow
43 layout = self.layout
45 col = layout.column(align=True)
46 col.prop(settings, 'coverage', slider=True)
47 col.prop(settings, 'height')
49 layout.use_property_split = True
50 layout.use_property_decorate = False
51 flow = layout.grid_flow(row_major=True, columns=0, even_columns=False, even_rows=False, align=True)
52 col = flow.column()
53 col.prop(settings, 'vertices')
55 row = layout.row(align=True)
56 row.scale_y = 1.5
57 row.operator("snow.create", text="Add Snow", icon="FREEZE")
60 class SNOW_OT_Create(Operator):
61 bl_idname = "snow.create"
62 bl_label = "Create Snow"
63 bl_description = "Create snow"
64 bl_options = {'REGISTER', 'UNDO'}
66 @classmethod
67 def poll(cls, context) -> bool:
68 return bool(context.selected_objects)
70 def execute(self, context):
71 coverage = context.scene.snow.coverage
72 height = context.scene.snow.height
73 vertices = context.scene.snow.vertices
75 # Get a list of selected objects, except non-mesh objects
76 input_objects = [obj for obj in context.selected_objects if obj.type == 'MESH']
77 snow_list = []
78 # Start UI progress bar
79 length = len(input_objects)
80 context.window_manager.progress_begin(0, 10)
81 timer = 0
82 for obj in input_objects:
83 # Timer
84 context.window_manager.progress_update(timer)
85 # Duplicate mesh
86 bpy.ops.object.select_all(action='DESELECT')
87 obj.select_set(True)
88 context.view_layer.objects.active = obj
89 object_eval = obj.evaluated_get(context.view_layer.depsgraph)
90 mesh_eval = bpy.data.meshes.new_from_object(object_eval)
91 snow_object = bpy.data.objects.new("Snow", mesh_eval)
92 snow_object.matrix_world = obj.matrix_world
93 context.collection.objects.link(snow_object)
94 bpy.ops.object.select_all(action='DESELECT')
95 context.view_layer.objects.active = snow_object
96 snow_object.select_set(True)
97 bpy.ops.object.mode_set(mode = 'EDIT')
98 bm_orig = bmesh.from_edit_mesh(snow_object.data)
99 bm_copy = bm_orig.copy()
100 bm_copy.transform(obj.matrix_world)
101 bm_copy.normal_update()
102 # Get faces data
103 delete_faces(vertices, bm_copy, snow_object)
104 ballobj = add_metaballs(context, height, snow_object)
105 context.view_layer.objects.active = snow_object
106 surface_area = area(snow_object)
107 snow = add_particles(context, surface_area, height, coverage, snow_object, ballobj)
108 add_modifiers(snow)
109 # Place inside collection
110 context.view_layer.active_layer_collection = context.view_layer.layer_collection
111 if "Snow" not in context.scene.collection.children:
112 coll = bpy.data.collections.new("Snow")
113 context.scene.collection.children.link(coll)
114 else:
115 coll = bpy.data.collections["Snow"]
116 coll.objects.link(snow)
117 context.view_layer.layer_collection.collection.objects.unlink(snow)
118 add_material(snow)
119 # Parent with object
120 snow.parent = obj
121 snow.matrix_parent_inverse = obj.matrix_world.inverted()
122 # Add snow to list
123 snow_list.append(snow)
124 # Update progress bar
125 timer += 0.1 / length
126 # Select created snow meshes
127 for s in snow_list:
128 s.select_set(True)
129 # End progress bar
130 context.window_manager.progress_end()
132 return {'FINISHED'}
135 def add_modifiers(snow):
136 bpy.ops.object.transform_apply(location=False, scale=True, rotation=False)
137 # Decimate the mesh to get rid of some visual artifacts
138 snow.modifiers.new("Decimate", 'DECIMATE')
139 snow.modifiers["Decimate"].ratio = 0.5
140 snow.modifiers.new("Subdiv", "SUBSURF")
141 snow.modifiers["Subdiv"].render_levels = 1
142 snow.modifiers["Subdiv"].quality = 1
143 snow.cycles.use_adaptive_subdivision = True
146 def add_particles(context, surface_area: float, height: float, coverage: float, snow_object: bpy.types.Object, ballobj: bpy.types.Object):
147 # Approximate the number of particles to be emitted
148 number = int(surface_area * 50 * (height ** -2) * ((coverage / 100) ** 2))
149 bpy.ops.object.particle_system_add()
150 particles = snow_object.particle_systems[0]
151 psettings = particles.settings
152 psettings.type = 'HAIR'
153 psettings.render_type = 'OBJECT'
154 # Generate random number for seed
155 random_seed = random.randint(0, 1000)
156 particles.seed = random_seed
157 # Set particles object
158 psettings.particle_size = height
159 psettings.instance_object = ballobj
160 psettings.count = number
161 # Convert particles to mesh
162 bpy.ops.object.select_all(action='DESELECT')
163 context.view_layer.objects.active = ballobj
164 ballobj.select_set(True)
165 bpy.ops.object.convert(target='MESH')
166 snow = bpy.context.active_object
167 snow.scale = [0.09, 0.09, 0.09]
168 bpy.ops.object.origin_set(type='ORIGIN_GEOMETRY')
169 bpy.ops.object.select_all(action='DESELECT')
170 snow_object.select_set(True)
171 bpy.ops.object.delete()
172 snow.select_set(True)
173 return snow
176 def add_metaballs(context, height: float, snow_object: bpy.types.Object) -> bpy.types.Object:
177 ball_name = "SnowBall"
178 ball = bpy.data.metaballs.new(ball_name)
179 ballobj = bpy.data.objects.new(ball_name, ball)
180 bpy.context.scene.collection.objects.link(ballobj)
181 # These settings have proven to work on a large amount of scenarios
182 ball.resolution = 0.7 * height + 0.3
183 ball.threshold = 1.3
184 element = ball.elements.new()
185 element.radius = 1.5
186 element.stiffness = 0.75
187 ballobj.scale = [0.09, 0.09, 0.09]
188 return ballobj
191 def delete_faces(vertices, bm_copy, snow_object: bpy.types.Object):
192 # Find upper faces
193 if vertices:
194 selected_faces = set(face.index for face in bm_copy.faces if face.select)
195 # Based on a certain angle, find all faces not pointing up
196 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))
197 bm_copy.free()
198 bpy.ops.mesh.select_all(action='DESELECT')
199 # Select upper faces
200 mesh = bmesh.from_edit_mesh(snow_object.data)
201 for face in mesh.faces:
202 if vertices:
203 if face.index not in selected_faces:
204 face.select = True
205 if face.index in down_faces:
206 face.select = True
207 # Delete unnecessary faces
208 faces_select = [face for face in mesh.faces if face.select]
209 bmesh.ops.delete(mesh, geom=faces_select, context='FACES_KEEP_BOUNDARY')
210 mesh.free()
211 bpy.ops.object.mode_set(mode = 'OBJECT')
214 def area(obj: bpy.types.Object) -> float:
215 bm_obj = bmesh.new()
216 bm_obj.from_mesh(obj.data)
217 bm_obj.transform(obj.matrix_world)
218 area = sum(face.calc_area() for face in bm_obj.faces)
219 bm_obj.free
220 return area
223 def add_material(obj: bpy.types.Object):
224 mat_name = "Snow"
225 # If material doesn't exist, create it
226 if mat_name in bpy.data.materials:
227 bpy.data.materials[mat_name].name = mat_name+".001"
228 mat = bpy.data.materials.new(mat_name)
229 mat.use_nodes = True
230 nodes = mat.node_tree.nodes
231 # Delete all nodes
232 for node in nodes:
233 nodes.remove(node)
234 # Add nodes
235 output = nodes.new('ShaderNodeOutputMaterial')
236 principled = nodes.new('ShaderNodeBsdfPrincipled')
237 vec_math = nodes.new('ShaderNodeVectorMath')
238 com_xyz = nodes.new('ShaderNodeCombineXYZ')
239 dis = nodes.new('ShaderNodeDisplacement')
240 mul1 = nodes.new('ShaderNodeMath')
241 add1 = nodes.new('ShaderNodeMath')
242 add2 = nodes.new('ShaderNodeMath')
243 mul2 = nodes.new('ShaderNodeMath')
244 mul3 = nodes.new('ShaderNodeMath')
245 range1 = nodes.new('ShaderNodeMapRange')
246 range2 = nodes.new('ShaderNodeMapRange')
247 range3 = nodes.new('ShaderNodeMapRange')
248 vor = nodes.new('ShaderNodeTexVoronoi')
249 noise1 = nodes.new('ShaderNodeTexNoise')
250 noise2 = nodes.new('ShaderNodeTexNoise')
251 noise3 = nodes.new('ShaderNodeTexNoise')
252 mapping = nodes.new('ShaderNodeMapping')
253 coord = nodes.new('ShaderNodeTexCoord')
254 # Change location
255 output.location = (100, 0)
256 principled.location = (-200, 600)
257 vec_math.location = (-400, 400)
258 com_xyz.location = (-600, 400)
259 dis.location = (-200, -100)
260 mul1.location = (-400, -100)
261 add1.location = (-600, -100)
262 add2.location = (-800, -100)
263 mul2.location = (-1000, -100)
264 mul3.location = (-1000, -300)
265 range1.location = (-400, 200)
266 range2.location = (-1200, -300)
267 range3.location = (-800, -300)
268 vor.location = (-1500, 200)
269 noise1.location = (-1500, 0)
270 noise2.location = (-1500, -250)
271 noise3.location = (-1500, -500)
272 mapping.location = (-1700, 0)
273 coord.location = (-1900, 0)
274 # Change node parameters
275 principled.distribution = "MULTI_GGX"
276 principled.subsurface_method = "RANDOM_WALK_SKIN"
277 principled.inputs[0].default_value[0] = 0.904 # Base color
278 principled.inputs[0].default_value[1] = 0.904
279 principled.inputs[0].default_value[2] = 0.904
280 principled.inputs[7].default_value = 1 # Subsurface weight
281 principled.inputs[9].default_value = 1 # Subsurface scale
282 principled.inputs[8].default_value[0] = 0.36 # Subsurface radius
283 principled.inputs[8].default_value[1] = 0.46
284 principled.inputs[8].default_value[2] = 0.6
285 principled.inputs[12].default_value = 0.224 # Specular
286 principled.inputs[2].default_value = 0.1 # Roughness
287 principled.inputs[19].default_value = 0.1 # Coat roughness
288 principled.inputs[20].default_value = 1.2 # Coat IOR
289 vec_math.operation = "MULTIPLY"
290 vec_math.inputs[1].default_value[0] = 0.5
291 vec_math.inputs[1].default_value[1] = 0.5
292 vec_math.inputs[1].default_value[2] = 0.5
293 com_xyz.inputs[0].default_value = 0.36
294 com_xyz.inputs[1].default_value = 0.46
295 com_xyz.inputs[2].default_value = 0.6
296 dis.inputs[1].default_value = 0.1
297 dis.inputs[2].default_value = 0.3
298 mul1.operation = "MULTIPLY"
299 mul1.inputs[1].default_value = 0.1
300 mul2.operation = "MULTIPLY"
301 mul2.inputs[1].default_value = 0.6
302 mul3.operation = "MULTIPLY"
303 mul3.inputs[1].default_value = 0.4
304 range1.inputs[1].default_value = 0.525
305 range1.inputs[2].default_value = 0.58
306 range2.inputs[1].default_value = 0.069
307 range2.inputs[2].default_value = 0.757
308 range3.inputs[1].default_value = 0.069
309 range3.inputs[2].default_value = 0.757
310 vor.feature = "N_SPHERE_RADIUS"
311 vor.inputs[2].default_value = 30
312 noise1.inputs[2].default_value = 12
313 noise2.inputs[2].default_value = 2
314 noise2.inputs[3].default_value = 4
315 noise3.inputs[2].default_value = 1
316 noise3.inputs[3].default_value = 4
317 mapping.inputs[3].default_value[0] = 12
318 mapping.inputs[3].default_value[1] = 12
319 mapping.inputs[3].default_value[2] = 12
320 # Link nodes
321 link = mat.node_tree.links
322 link.new(principled.outputs[0], output.inputs[0])
323 link.new(vec_math.outputs[0], principled.inputs[8])
324 link.new(com_xyz.outputs[0], vec_math.inputs[0])
325 link.new(dis.outputs[0], output.inputs[2])
326 link.new(mul1.outputs[0], dis.inputs[0])
327 link.new(add1.outputs[0], mul1.inputs[0])
328 link.new(add2.outputs[0], add1.inputs[0])
329 link.new(mul2.outputs[0], add2.inputs[0])
330 link.new(mul3.outputs[0], add2.inputs[1])
331 link.new(range1.outputs[0], principled.inputs[18])
332 link.new(range2.outputs[0], mul3.inputs[0])
333 link.new(range3.outputs[0], add1.inputs[1])
334 link.new(vor.outputs[4], range1.inputs[0])
335 link.new(noise1.outputs[0], mul2.inputs[0])
336 link.new(noise2.outputs[0], range2.inputs[0])
337 link.new(noise3.outputs[0], range3.inputs[0])
338 link.new(mapping.outputs[0], vor.inputs[0])
339 link.new(mapping.outputs[0], noise1.inputs[0])
340 link.new(mapping.outputs[0], noise2.inputs[0])
341 link.new(mapping.outputs[0], noise3.inputs[0])
342 link.new(coord.outputs[3], mapping.inputs[0])
343 # Set displacement and add material
344 mat.displacement_method = "DISPLACEMENT"
345 obj.data.materials.append(mat)
348 # Properties
349 class SnowSettings(PropertyGroup):
350 coverage : IntProperty(
351 name = "Coverage",
352 description = "Percentage of the object to be covered with snow",
353 default = 100,
354 min = 0,
355 max = 100,
356 subtype = 'PERCENTAGE'
359 height : FloatProperty(
360 name = "Height",
361 description = "Height of the snow",
362 default = 0.3,
363 step = 1,
364 precision = 2,
365 min = 0.1,
366 max = 1
369 vertices : BoolProperty(
370 name = "Selected Faces",
371 description = "Add snow only on selected faces",
372 default = False
376 #############################################################################################
377 classes = (
378 REAL_PT_snow,
379 SNOW_OT_Create,
380 SnowSettings
383 register, unregister = bpy.utils.register_classes_factory(classes)
385 # Register
386 def register():
387 for cls in classes:
388 bpy.utils.register_class(cls)
389 bpy.types.Scene.snow = PointerProperty(type=SnowSettings)
392 # Unregister
393 def unregister():
394 for cls in classes:
395 bpy.utils.unregister_class(cls)
396 del bpy.types.Scene.snow
399 if __name__ == "__main__":
400 register()