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 #####
21 "description": "Generate snow mesh",
22 "author": "Wolf <wolf.art3d@gmail.com>",
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",
41 from bpy
.props
import BoolProperty
, FloatProperty
, IntProperty
, PointerProperty
42 from bpy
.types
import Operator
, Panel
, PropertyGroup
43 from mathutils
import Vector
47 class REAL_PT_snow(Panel
):
48 bl_space_type
= "VIEW_3D"
49 bl_context
= "objectmode"
52 bl_category
= "Real Snow"
54 def draw(self
, context
):
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)
67 col
.prop(settings
, 'vertices')
69 row
= layout
.row(align
=True)
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'}
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']
92 # start UI progress bar
93 length
= len(input_objects
)
94 context
.window_manager
.progress_begin(0, 10)
96 for obj
in input_objects
:
98 context
.window_manager
.progress_update(timer
)
100 bpy
.ops
.object.select_all(action
='DESELECT')
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()
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
)
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
)
129 coll
= bpy
.data
.collections
["Snow"]
130 coll
.objects
.link(snow
)
131 context
.view_layer
.layer_collection
.collection
.objects
.unlink(snow
)
135 snow
.matrix_parent_inverse
= obj
.matrix_world
.inverted()
137 snow_list
.append(snow
)
138 # update progress bar
139 timer
+= 0.1 / length
140 # select created snow meshes
144 context
.window_manager
.progress_end()
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)
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
198 element
= ball
.elements
.new()
200 element
.stiffness
= 0.75
201 ballobj
.scale
= [0.09, 0.09, 0.09]
205 def delete_faces(vertices
, bm_copy
, snow_object
: bpy
.types
.Object
):
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)]
212 bpy
.ops
.mesh
.select_all(action
='DESELECT')
214 mesh
= bmesh
.from_edit_mesh(snow_object
.data
)
215 for face
in mesh
.faces
:
217 if not face
.index
in selected_faces
:
219 if face
.index
in down_faces
:
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')
225 bpy
.ops
.object.mode_set(mode
= 'OBJECT')
228 def area(obj
: bpy
.types
.Object
) -> float:
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
)
237 def add_material(obj
: bpy
.types
.Object
):
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
)
244 nodes
= mat
.node_tree
.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')
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
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
)
364 class SnowSettings(PropertyGroup
):
365 coverage
: IntProperty(
367 description
= "Percentage of the object to be covered with snow",
371 subtype
= 'PERCENTAGE'
374 height
: FloatProperty(
376 description
= "Height of the snow",
384 vertices
: BoolProperty(
385 name
= "Selected Faces",
386 description
= "Add snow only on selected faces",
391 #############################################################################################
398 register
, unregister
= bpy
.utils
.register_classes_factory(classes
)
403 bpy
.utils
.register_class(cls
)
404 bpy
.types
.Scene
.snow
= PointerProperty(type=SnowSettings
)
410 bpy
.utils
.unregister_class(cls
)
411 del bpy
.types
.Scene
.snow
414 if __name__
== "__main__":