1 # SPDX-FileCopyrightText: 2020-2022 Blender Foundation
3 # SPDX-License-Identifier: GPL-2.0-or-later
7 "description": "Generate snow mesh",
8 "author": "Marco Pavanello, Drew Perttula",
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",
27 from bpy
.props
import BoolProperty
, FloatProperty
, IntProperty
, PointerProperty
28 from bpy
.types
import Operator
, Panel
, PropertyGroup
29 from mathutils
import Vector
33 class REAL_PT_snow(Panel
):
34 bl_space_type
= "VIEW_3D"
35 bl_context
= "objectmode"
38 bl_category
= "Real Snow"
40 def draw(self
, context
):
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)
53 col
.prop(settings
, 'vertices')
55 row
= layout
.row(align
=True)
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'}
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']
78 # Start UI progress bar
79 length
= len(input_objects
)
80 context
.window_manager
.progress_begin(0, 10)
82 for obj
in input_objects
:
84 context
.window_manager
.progress_update(timer
)
86 bpy
.ops
.object.select_all(action
='DESELECT')
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()
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
)
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
)
115 coll
= bpy
.data
.collections
["Snow"]
116 coll
.objects
.link(snow
)
117 context
.view_layer
.layer_collection
.collection
.objects
.unlink(snow
)
121 snow
.matrix_parent_inverse
= obj
.matrix_world
.inverted()
123 snow_list
.append(snow
)
124 # Update progress bar
125 timer
+= 0.1 / length
126 # Select created snow meshes
130 context
.window_manager
.progress_end()
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)
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
184 element
= ball
.elements
.new()
186 element
.stiffness
= 0.75
187 ballobj
.scale
= [0.09, 0.09, 0.09]
191 def delete_faces(vertices
, bm_copy
, snow_object
: bpy
.types
.Object
):
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))
198 bpy
.ops
.mesh
.select_all(action
='DESELECT')
200 mesh
= bmesh
.from_edit_mesh(snow_object
.data
)
201 for face
in mesh
.faces
:
203 if face
.index
not in selected_faces
:
205 if face
.index
in down_faces
:
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')
211 bpy
.ops
.object.mode_set(mode
= 'OBJECT')
214 def area(obj
: bpy
.types
.Object
) -> float:
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
)
223 def add_material(obj
: bpy
.types
.Object
):
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
)
230 nodes
= mat
.node_tree
.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')
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
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
)
350 class SnowSettings(PropertyGroup
):
351 coverage
: IntProperty(
353 description
= "Percentage of the object to be covered with snow",
357 subtype
= 'PERCENTAGE'
360 height
: FloatProperty(
362 description
= "Height of the snow",
370 vertices
: BoolProperty(
371 name
= "Selected Faces",
372 description
= "Add snow only on selected faces",
377 #############################################################################################
384 register
, unregister
= bpy
.utils
.register_classes_factory(classes
)
389 bpy
.utils
.register_class(cls
)
390 bpy
.types
.Scene
.snow
= PointerProperty(type=SnowSettings
)
396 bpy
.utils
.unregister_class(cls
)
397 del bpy
.types
.Scene
.snow
400 if __name__
== "__main__":