1 # SPDX-License-Identifier: GPL-2.0-or-later
5 "description": "Generate snow mesh",
6 "author": "Marco Pavanello, Drew Perttula",
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",
25 from bpy
.props
import BoolProperty
, FloatProperty
, IntProperty
, PointerProperty
26 from bpy
.types
import Operator
, Panel
, PropertyGroup
27 from mathutils
import Vector
31 class REAL_PT_snow(Panel
):
32 bl_space_type
= "VIEW_3D"
33 bl_context
= "objectmode"
36 bl_category
= "Real Snow"
38 def draw(self
, context
):
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)
51 col
.prop(settings
, 'vertices')
53 row
= layout
.row(align
=True)
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'}
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']
76 # Start UI progress bar
77 length
= len(input_objects
)
78 context
.window_manager
.progress_begin(0, 10)
80 for obj
in input_objects
:
82 context
.window_manager
.progress_update(timer
)
84 bpy
.ops
.object.select_all(action
='DESELECT')
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()
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
)
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
)
113 coll
= bpy
.data
.collections
["Snow"]
114 coll
.objects
.link(snow
)
115 context
.view_layer
.layer_collection
.collection
.objects
.unlink(snow
)
119 snow
.matrix_parent_inverse
= obj
.matrix_world
.inverted()
121 snow_list
.append(snow
)
122 # Update progress bar
123 timer
+= 0.1 / length
124 # Select created snow meshes
128 context
.window_manager
.progress_end()
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)
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
182 element
= ball
.elements
.new()
184 element
.stiffness
= 0.75
185 ballobj
.scale
= [0.09, 0.09, 0.09]
189 def delete_faces(vertices
, bm_copy
, snow_object
: bpy
.types
.Object
):
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))
196 bpy
.ops
.mesh
.select_all(action
='DESELECT')
198 mesh
= bmesh
.from_edit_mesh(snow_object
.data
)
199 for face
in mesh
.faces
:
201 if face
.index
not in selected_faces
:
203 if face
.index
in down_faces
:
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')
209 bpy
.ops
.object.mode_set(mode
= 'OBJECT')
212 def area(obj
: bpy
.types
.Object
) -> float:
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
)
221 def add_material(obj
: bpy
.types
.Object
):
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
)
228 nodes
= mat
.node_tree
.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')
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
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
)
348 class SnowSettings(PropertyGroup
):
349 coverage
: IntProperty(
351 description
= "Percentage of the object to be covered with snow",
355 subtype
= 'PERCENTAGE'
358 height
: FloatProperty(
360 description
= "Height of the snow",
368 vertices
: BoolProperty(
369 name
= "Selected Faces",
370 description
= "Add snow only on selected faces",
375 #############################################################################################
382 register
, unregister
= bpy
.utils
.register_classes_factory(classes
)
387 bpy
.utils
.register_class(cls
)
388 bpy
.types
.Scene
.snow
= PointerProperty(type=SnowSettings
)
394 bpy
.utils
.unregister_class(cls
)
395 del bpy
.types
.Scene
.snow
398 if __name__
== "__main__":