Fix T76383: Undo after cell fracture crashes
[blender-addons.git] / real_snow.py
blobf1091b2d065966f9547aa9b99d479af06332819f
1 # ##### BEGIN GPL LICENSE BLOCK #####
3 # This program is free software; you can redistribute it and/or
4 # modify it under the terms of the GNU General Public License
5 # as published by the Free Software Foundation; either version 2
6 # of the License, or (at your option) any later version.
8 # This program is distributed in the hope that it will be useful,
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # GNU General Public License for more details.
13 # You should have received a copy of the GNU General Public License
14 # along with this program; if not, write to the Free Software Foundation,
15 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 # ##### END GPL LICENSE BLOCK #####
19 bl_info = {
20 "name": "Real Snow",
21 "description": "Generate snow mesh",
22 "author": "Wolf <wolf.art3d@gmail.com>",
23 "version": (1, 1),
24 "blender": (2, 83, 0),
25 "location": "View 3D > Properties Panel",
26 "doc_url": "https://github.com/macio97/Real-Snow",
27 "tracker_url": "https://github.com/macio97/Real-Snow/issues",
28 "support": "COMMUNITY",
29 "category": "Object",
33 # Libraries
34 import math
35 import os
36 import random
37 import time
39 import bpy
40 import bmesh
41 from bpy.props import BoolProperty, FloatProperty, IntProperty, PointerProperty
42 from bpy.types import Operator, Panel, PropertyGroup
43 from mathutils import Vector
46 # Panel
47 class REAL_PT_snow(Panel):
48 bl_space_type = "VIEW_3D"
49 bl_context = "objectmode"
50 bl_region_type = "UI"
51 bl_label = "Snow"
52 bl_category = "Real Snow"
54 def draw(self, context):
55 scn = context.scene
56 settings = scn.snow
57 layout = self.layout
59 col = layout.column(align=True)
60 col.prop(settings, 'coverage', slider=True)
61 col.prop(settings, 'height')
63 layout.use_property_split = True
64 layout.use_property_decorate = False
65 flow = layout.grid_flow(row_major=True, columns=0, even_columns=False, even_rows=False, align=True)
66 col = flow.column()
67 col.prop(settings, 'vertices')
69 row = layout.row(align=True)
70 row.scale_y = 1.5
71 row.operator("snow.create", text="Add Snow", icon="FREEZE")
74 class SNOW_OT_Create(Operator):
75 bl_idname = "snow.create"
76 bl_label = "Create Snow"
77 bl_description = "Create snow"
78 bl_options = {'REGISTER', 'UNDO'}
80 @classmethod
81 def poll(cls, context) -> bool:
82 return bool(context.selected_objects)
84 def execute(self, context):
85 coverage = context.scene.snow.coverage
86 height = context.scene.snow.height
87 vertices = context.scene.snow.vertices
89 # get list of selected objects except non-mesh objects
90 input_objects = [obj for obj in context.selected_objects if obj.type == 'MESH']
91 snow_list = []
92 # start UI progress bar
93 length = len(input_objects)
94 context.window_manager.progress_begin(0, 10)
95 timer=0
96 for obj in input_objects:
97 # timer
98 context.window_manager.progress_update(timer)
99 # duplicate mesh
100 bpy.ops.object.select_all(action='DESELECT')
101 obj.select_set(True)
102 context.view_layer.objects.active = obj
103 object_eval = obj.evaluated_get(context.view_layer.depsgraph)
104 mesh_eval = bpy.data.meshes.new_from_object(object_eval)
105 snow_object = bpy.data.objects.new("Snow", mesh_eval)
106 snow_object.matrix_world = obj.matrix_world
107 context.collection.objects.link(snow_object)
108 bpy.ops.object.select_all(action='DESELECT')
109 context.view_layer.objects.active = snow_object
110 snow_object.select_set(True)
111 bpy.ops.object.mode_set(mode = 'EDIT')
112 bm_orig = bmesh.from_edit_mesh(snow_object.data)
113 bm_copy = bm_orig.copy()
114 bm_copy.transform(obj.matrix_world)
115 bm_copy.normal_update()
116 # get faces data
117 delete_faces(vertices, bm_copy, snow_object)
118 ballobj = add_metaballs(context, height, snow_object)
119 context.view_layer.objects.active = snow_object
120 surface_area = area(snow_object)
121 snow = add_particles(context, surface_area, height, coverage, snow_object, ballobj)
122 add_modifiers(snow)
123 # place inside collection
124 context.view_layer.active_layer_collection = context.view_layer.layer_collection
125 if "Snow" not in context.scene.collection.children:
126 coll = bpy.data.collections.new("Snow")
127 context.scene.collection.children.link(coll)
128 else:
129 coll = bpy.data.collections["Snow"]
130 coll.objects.link(snow)
131 context.view_layer.layer_collection.collection.objects.unlink(snow)
132 add_material(snow)
133 # parent with object
134 snow.parent = obj
135 snow.matrix_parent_inverse = obj.matrix_world.inverted()
136 # add snow to list
137 snow_list.append(snow)
138 # update progress bar
139 timer += 0.1 / length
140 # select created snow meshes
141 for s in snow_list:
142 s.select_set(True)
143 # end progress bar
144 context.window_manager.progress_end()
146 return {'FINISHED'}
149 def add_modifiers(snow):
150 bpy.ops.object.transform_apply(location=False, scale=True, rotation=False)
151 # decimate the mesh to get rid of some visual artifacts
152 snow.modifiers.new("Decimate", 'DECIMATE')
153 snow.modifiers["Decimate"].ratio = 0.5
154 snow.modifiers.new("Subdiv", "SUBSURF")
155 snow.modifiers["Subdiv"].render_levels = 1
156 snow.modifiers["Subdiv"].quality = 1
157 snow.cycles.use_adaptive_subdivision = True
160 def add_particles(context, surface_area: float, height: float, coverage: float, snow_object: bpy.types.Object, ballobj: bpy.types.Object):
161 # approximate the number of particles to be emitted
162 number = int(surface_area*50*(height**-2)*((coverage/100)**2))
163 bpy.ops.object.particle_system_add()
164 particles = snow_object.particle_systems[0]
165 psettings = particles.settings
166 psettings.type = 'HAIR'
167 psettings.render_type = 'OBJECT'
168 # generate random number for seed
169 random_seed = random.randint(0, 1000)
170 particles.seed = random_seed
171 # set particles object
172 psettings.particle_size = height
173 psettings.instance_object = ballobj
174 psettings.count = number
175 # convert particles to mesh
176 bpy.ops.object.select_all(action='DESELECT')
177 context.view_layer.objects.active = ballobj
178 ballobj.select_set(True)
179 bpy.ops.object.convert(target='MESH')
180 snow = bpy.context.active_object
181 snow.scale = [0.09, 0.09, 0.09]
182 bpy.ops.object.origin_set(type='ORIGIN_GEOMETRY')
183 bpy.ops.object.select_all(action='DESELECT')
184 snow_object.select_set(True)
185 bpy.ops.object.delete()
186 snow.select_set(True)
187 return snow
190 def add_metaballs(context, height: float, snow_object: bpy.types.Object) -> bpy.types.Object:
191 ball_name = "SnowBall"
192 ball = bpy.data.metaballs.new(ball_name)
193 ballobj = bpy.data.objects.new(ball_name, ball)
194 bpy.context.scene.collection.objects.link(ballobj)
195 # these settings have proven to work on a large amount of scenarios
196 ball.resolution = 0.7*height+0.3
197 ball.threshold = 1.3
198 element = ball.elements.new()
199 element.radius = 1.5
200 element.stiffness = 0.75
201 ballobj.scale = [0.09, 0.09, 0.09]
202 return ballobj
205 def delete_faces(vertices, bm_copy, snow_object: bpy.types.Object):
206 # find upper faces
207 if vertices:
208 selected_faces = [face.index for face in bm_copy.faces if face.select]
209 # based on a certain angle, find all faces not pointing up
210 down_faces = [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)]
211 bm_copy.free()
212 bpy.ops.mesh.select_all(action='DESELECT')
213 # select upper faces
214 mesh = bmesh.from_edit_mesh(snow_object.data)
215 for face in mesh.faces:
216 if vertices:
217 if not face.index in selected_faces:
218 face.select = True
219 if face.index in down_faces:
220 face.select = True
221 # delete unneccessary faces
222 faces_select = [face for face in mesh.faces if face.select]
223 bmesh.ops.delete(mesh, geom=faces_select, context='FACES_KEEP_BOUNDARY')
224 mesh.free()
225 bpy.ops.object.mode_set(mode = 'OBJECT')
228 def area(obj: bpy.types.Object) -> float:
229 bm_obj = bmesh.new()
230 bm_obj.from_mesh(obj.data)
231 bm_obj.transform(obj.matrix_world)
232 area = sum(face.calc_area() for face in bm_obj.faces)
233 bm_obj.free
234 return area
237 def add_material(obj: bpy.types.Object):
238 mat_name = "Snow"
239 # if material doesn't exist, create it
240 if mat_name in bpy.data.materials:
241 bpy.data.materials[mat_name].name = mat_name+".001"
242 mat = bpy.data.materials.new(mat_name)
243 mat.use_nodes = True
244 nodes = mat.node_tree.nodes
245 # delete all nodes
246 for node in nodes:
247 nodes.remove(node)
248 # add nodes
249 output = nodes.new('ShaderNodeOutputMaterial')
250 principled = nodes.new('ShaderNodeBsdfPrincipled')
251 vec_math = nodes.new('ShaderNodeVectorMath')
252 com_xyz = nodes.new('ShaderNodeCombineXYZ')
253 dis = nodes.new('ShaderNodeDisplacement')
254 mul1 = nodes.new('ShaderNodeMath')
255 add1 = nodes.new('ShaderNodeMath')
256 add2 = nodes.new('ShaderNodeMath')
257 mul2 = nodes.new('ShaderNodeMath')
258 mul3 = nodes.new('ShaderNodeMath')
259 ramp1 = nodes.new('ShaderNodeValToRGB')
260 ramp2 = nodes.new('ShaderNodeValToRGB')
261 ramp3 = nodes.new('ShaderNodeValToRGB')
262 vor = nodes.new('ShaderNodeTexVoronoi')
263 noise1 = nodes.new('ShaderNodeTexNoise')
264 noise2 = nodes.new('ShaderNodeTexNoise')
265 noise3 = nodes.new('ShaderNodeTexNoise')
266 mapping = nodes.new('ShaderNodeMapping')
267 coord = nodes.new('ShaderNodeTexCoord')
268 # change location
269 output.location = (100, 0)
270 principled.location = (-200, 500)
271 vec_math.location = (-400, 400)
272 com_xyz.location = (-600, 400)
273 dis.location = (-200, -100)
274 mul1.location = (-400, -100)
275 add1.location = (-600, -100)
276 add2.location = (-800, -100)
277 mul2.location = (-1000, -100)
278 mul3.location = (-1000, -300)
279 ramp1.location = (-500, 150)
280 ramp2.location = (-1300, -300)
281 ramp3.location = (-1000, -500)
282 vor.location = (-1500, 200)
283 noise1.location = (-1500, 0)
284 noise2.location = (-1500, -200)
285 noise3.location = (-1500, -400)
286 mapping.location = (-1700, 0)
287 coord.location = (-1900, 0)
288 # change node parameters
289 principled.distribution = "MULTI_GGX"
290 principled.subsurface_method = "RANDOM_WALK"
291 principled.inputs[0].default_value[0] = 0.904
292 principled.inputs[0].default_value[1] = 0.904
293 principled.inputs[0].default_value[2] = 0.904
294 principled.inputs[1].default_value = 1
295 principled.inputs[2].default_value[0] = 0.36
296 principled.inputs[2].default_value[1] = 0.46
297 principled.inputs[2].default_value[2] = 0.6
298 principled.inputs[3].default_value[0] = 0.904
299 principled.inputs[3].default_value[1] = 0.904
300 principled.inputs[3].default_value[2] = 0.904
301 principled.inputs[5].default_value = 0.224
302 principled.inputs[7].default_value = 0.1
303 principled.inputs[13].default_value = 0.1
304 vec_math.operation = "MULTIPLY"
305 vec_math.inputs[1].default_value[0] = 0.5
306 vec_math.inputs[1].default_value[1] = 0.5
307 vec_math.inputs[1].default_value[2] = 0.5
308 com_xyz.inputs[0].default_value = 0.36
309 com_xyz.inputs[1].default_value = 0.46
310 com_xyz.inputs[2].default_value = 0.6
311 dis.inputs[1].default_value = 0.1
312 dis.inputs[2].default_value = 0.3
313 mul1.operation = "MULTIPLY"
314 mul1.inputs[1].default_value = 0.1
315 mul2.operation = "MULTIPLY"
316 mul2.inputs[1].default_value = 0.6
317 mul3.operation = "MULTIPLY"
318 mul3.inputs[1].default_value = 0.4
319 ramp1.color_ramp.elements[0].position = 0.525
320 ramp1.color_ramp.elements[1].position = 0.58
321 ramp2.color_ramp.elements[0].position = 0.069
322 ramp2.color_ramp.elements[1].position = 0.757
323 ramp3.color_ramp.elements[0].position = 0.069
324 ramp3.color_ramp.elements[1].position = 0.757
325 vor.feature = "N_SPHERE_RADIUS"
326 vor.inputs[2].default_value = 30
327 noise1.inputs[2].default_value = 12
328 noise2.inputs[2].default_value = 2
329 noise2.inputs[3].default_value = 4
330 noise3.inputs[2].default_value = 1
331 noise3.inputs[3].default_value = 4
332 mapping.inputs[3].default_value[0] = 12
333 mapping.inputs[3].default_value[1] = 12
334 mapping.inputs[3].default_value[2] = 12
335 # link nodes
336 link = mat.node_tree.links
337 link.new(principled.outputs[0], output.inputs[0])
338 link.new(vec_math.outputs[0], principled.inputs[2])
339 link.new(com_xyz.outputs[0], vec_math.inputs[0])
340 link.new(dis.outputs[0], output.inputs[2])
341 link.new(mul1.outputs[0], dis.inputs[0])
342 link.new(add1.outputs[0], mul1.inputs[0])
343 link.new(add2.outputs[0], add1.inputs[0])
344 link.new(mul2.outputs[0], add2.inputs[0])
345 link.new(mul3.outputs[0], add2.inputs[1])
346 link.new(ramp1.outputs[0], principled.inputs[12])
347 link.new(ramp2.outputs[0], mul3.inputs[0])
348 link.new(ramp3.outputs[0], add1.inputs[1])
349 link.new(vor.outputs[4], ramp1.inputs[0])
350 link.new(noise1.outputs[0], mul2.inputs[0])
351 link.new(noise2.outputs[0], ramp2.inputs[0])
352 link.new(noise3.outputs[0], ramp3.inputs[0])
353 link.new(mapping.outputs[0], vor.inputs[0])
354 link.new(mapping.outputs[0], noise1.inputs[0])
355 link.new(mapping.outputs[0], noise2.inputs[0])
356 link.new(mapping.outputs[0], noise3.inputs[0])
357 link.new(coord.outputs[3], mapping.inputs[0])
358 # set displacement and add material
359 mat.cycles.displacement_method = "DISPLACEMENT"
360 obj.data.materials.append(mat)
363 # Properties
364 class SnowSettings(PropertyGroup):
365 coverage : IntProperty(
366 name = "Coverage",
367 description = "Percentage of the object to be covered with snow",
368 default = 100,
369 min = 0,
370 max = 100,
371 subtype = 'PERCENTAGE'
374 height : FloatProperty(
375 name = "Height",
376 description = "Height of the snow",
377 default = 0.3,
378 step = 1,
379 precision = 2,
380 min = 0.1,
381 max = 1
384 vertices : BoolProperty(
385 name = "Selected Faces",
386 description = "Add snow only on selected faces",
387 default = False
391 #############################################################################################
392 classes = (
393 REAL_PT_snow,
394 SNOW_OT_Create,
395 SnowSettings
398 register, unregister = bpy.utils.register_classes_factory(classes)
400 # Register
401 def register():
402 for cls in classes:
403 bpy.utils.register_class(cls)
404 bpy.types.Scene.snow = PointerProperty(type=SnowSettings)
407 # Unregister
408 def unregister():
409 for cls in classes:
410 bpy.utils.unregister_class(cls)
411 del bpy.types.Scene.snow
414 if __name__ == "__main__":
415 register()