Fix FBX char type being interpreted as bool
[blender-addons.git] / real_snow.py
blob8cc0e68555a2207c047637cb4fbb7d470f54506c
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),
10 "blender": (3, 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"
277 principled.inputs[0].default_value[0] = 0.904
278 principled.inputs[0].default_value[1] = 0.904
279 principled.inputs[0].default_value[2] = 0.904
280 principled.inputs[1].default_value = 1
281 principled.inputs[2].default_value[0] = 0.36
282 principled.inputs[2].default_value[1] = 0.46
283 principled.inputs[2].default_value[2] = 0.6
284 principled.inputs[3].default_value[0] = 0.904
285 principled.inputs[3].default_value[1] = 0.904
286 principled.inputs[3].default_value[2] = 0.904
287 principled.inputs[7].default_value = 0.224
288 principled.inputs[9].default_value = 0.1
289 principled.inputs[15].default_value = 0.1
290 vec_math.operation = "MULTIPLY"
291 vec_math.inputs[1].default_value[0] = 0.5
292 vec_math.inputs[1].default_value[1] = 0.5
293 vec_math.inputs[1].default_value[2] = 0.5
294 com_xyz.inputs[0].default_value = 0.36
295 com_xyz.inputs[1].default_value = 0.46
296 com_xyz.inputs[2].default_value = 0.6
297 dis.inputs[1].default_value = 0.1
298 dis.inputs[2].default_value = 0.3
299 mul1.operation = "MULTIPLY"
300 mul1.inputs[1].default_value = 0.1
301 mul2.operation = "MULTIPLY"
302 mul2.inputs[1].default_value = 0.6
303 mul3.operation = "MULTIPLY"
304 mul3.inputs[1].default_value = 0.4
305 range1.inputs[1].default_value = 0.525
306 range1.inputs[2].default_value = 0.58
307 range2.inputs[1].default_value = 0.069
308 range2.inputs[2].default_value = 0.757
309 range3.inputs[1].default_value = 0.069
310 range3.inputs[2].default_value = 0.757
311 vor.feature = "N_SPHERE_RADIUS"
312 vor.inputs[2].default_value = 30
313 noise1.inputs[2].default_value = 12
314 noise2.inputs[2].default_value = 2
315 noise2.inputs[3].default_value = 4
316 noise3.inputs[2].default_value = 1
317 noise3.inputs[3].default_value = 4
318 mapping.inputs[3].default_value[0] = 12
319 mapping.inputs[3].default_value[1] = 12
320 mapping.inputs[3].default_value[2] = 12
321 # Link nodes
322 link = mat.node_tree.links
323 link.new(principled.outputs[0], output.inputs[0])
324 link.new(vec_math.outputs[0], principled.inputs[2])
325 link.new(com_xyz.outputs[0], vec_math.inputs[0])
326 link.new(dis.outputs[0], output.inputs[2])
327 link.new(mul1.outputs[0], dis.inputs[0])
328 link.new(add1.outputs[0], mul1.inputs[0])
329 link.new(add2.outputs[0], add1.inputs[0])
330 link.new(mul2.outputs[0], add2.inputs[0])
331 link.new(mul3.outputs[0], add2.inputs[1])
332 link.new(range1.outputs[0], principled.inputs[14])
333 link.new(range2.outputs[0], mul3.inputs[0])
334 link.new(range3.outputs[0], add1.inputs[1])
335 link.new(vor.outputs[4], range1.inputs[0])
336 link.new(noise1.outputs[0], mul2.inputs[0])
337 link.new(noise2.outputs[0], range2.inputs[0])
338 link.new(noise3.outputs[0], range3.inputs[0])
339 link.new(mapping.outputs[0], vor.inputs[0])
340 link.new(mapping.outputs[0], noise1.inputs[0])
341 link.new(mapping.outputs[0], noise2.inputs[0])
342 link.new(mapping.outputs[0], noise3.inputs[0])
343 link.new(coord.outputs[3], mapping.inputs[0])
344 # Set displacement and add material
345 mat.cycles.displacement_method = "DISPLACEMENT"
346 obj.data.materials.append(mat)
349 # Properties
350 class SnowSettings(PropertyGroup):
351 coverage : IntProperty(
352 name = "Coverage",
353 description = "Percentage of the object to be covered with snow",
354 default = 100,
355 min = 0,
356 max = 100,
357 subtype = 'PERCENTAGE'
360 height : FloatProperty(
361 name = "Height",
362 description = "Height of the snow",
363 default = 0.3,
364 step = 1,
365 precision = 2,
366 min = 0.1,
367 max = 1
370 vertices : BoolProperty(
371 name = "Selected Faces",
372 description = "Add snow only on selected faces",
373 default = False
377 #############################################################################################
378 classes = (
379 REAL_PT_snow,
380 SNOW_OT_Create,
381 SnowSettings
384 register, unregister = bpy.utils.register_classes_factory(classes)
386 # Register
387 def register():
388 for cls in classes:
389 bpy.utils.register_class(cls)
390 bpy.types.Scene.snow = PointerProperty(type=SnowSettings)
393 # Unregister
394 def unregister():
395 for cls in classes:
396 bpy.utils.unregister_class(cls)
397 del bpy.types.Scene.snow
400 if __name__ == "__main__":
401 register()