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