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 #####
27 from itertools
import islice
28 from mathutils
.bvhtree
import BVHTree
29 from mathutils
import Vector
, Matrix
, Euler
30 from gpu_extras
.batch
import batch_for_shader
32 from bpy_extras
.view3d_utils
import (
33 region_2d_to_vector_3d
,
34 region_2d_to_origin_3d
39 ################################################################
41 class ScatterObjects(bpy
.types
.Operator
):
42 bl_idname
= "object.scatter"
43 bl_label
= "Scatter Objects"
44 bl_options
= {'REGISTER', 'UNDO'}
47 def poll(cls
, context
):
49 currently_in_3d_view(context
)
50 and context
.active_object
is not None
51 and context
.active_object
.mode
== 'OBJECT')
53 def invoke(self
, context
, event
):
54 self
.target_object
= context
.active_object
55 self
.objects_to_scatter
= get_selected_non_active_objects(context
)
57 if self
.target_object
is None or len(self
.objects_to_scatter
) == 0:
58 self
.report({'ERROR'}, "Select objects to scatter and a target object")
61 self
.base_scale
= get_max_object_side_length(self
.objects_to_scatter
)
64 self
.active_target
= None
65 self
.target_cache
= {}
67 self
.enable_draw_callback()
68 context
.window_manager
.modal_handler_add(self
)
69 return {'RUNNING_MODAL'}
71 def modal(self
, context
, event
):
72 context
.area
.tag_redraw()
74 if not event_is_in_region(event
, context
.region
) and self
.active_target
is None:
75 return {'PASS_THROUGH'}
77 if event
.type == 'ESC':
78 return self
.finish('CANCELLED')
80 if event
.type == 'RET' and event
.value
== 'PRESS':
81 self
.create_scatter_object()
82 return self
.finish('FINISHED')
84 event_used
= self
.handle_non_exit_event(event
)
86 return {'RUNNING_MODAL'}
88 return {'PASS_THROUGH'}
90 def handle_non_exit_event(self
, event
):
91 if self
.active_target
is None:
92 if event
.type == 'LEFTMOUSE' and event
.value
== 'PRESS':
93 self
.active_target
= StrokeTarget()
94 self
.active_target
.start_build(self
.target_object
)
97 build_state
= self
.active_target
.continue_build(event
)
98 if build_state
== BuildState
.FINISHED
:
99 self
.targets
.append(self
.active_target
)
100 self
.active_target
= None
101 self
.remove_target_from_cache(self
.active_target
)
106 def enable_draw_callback(self
):
107 self
._draw
_callback
_view
= bpy
.types
.SpaceView3D
.draw_handler_add(self
.draw_view
, (), 'WINDOW', 'POST_VIEW')
108 self
._draw
_callback
_px
= bpy
.types
.SpaceView3D
.draw_handler_add(self
.draw_px
, (), 'WINDOW', 'POST_PIXEL')
110 def disable_draw_callback(self
):
111 bpy
.types
.SpaceView3D
.draw_handler_remove(self
._draw
_callback
_view
, 'WINDOW')
112 bpy
.types
.SpaceView3D
.draw_handler_remove(self
._draw
_callback
_px
, 'WINDOW')
115 for target
in self
.iter_targets():
118 draw_matrices_batches(list(self
.iter_matrix_batches()))
121 draw_text((20, 20, 0), "Instances: " + str(len(self
.get_all_matrices())))
123 def finish(self
, return_value
):
124 self
.disable_draw_callback()
125 bpy
.context
.area
.tag_redraw()
126 return {return_value}
128 def create_scatter_object(self
):
129 matrix_chunks
= make_random_chunks(
130 self
.get_all_matrices(), len(self
.objects_to_scatter
))
132 collection
= bpy
.data
.collections
.new("Scatter")
133 bpy
.context
.collection
.children
.link(collection
)
135 for obj
, matrices
in zip(self
.objects_to_scatter
, matrix_chunks
):
136 make_duplicator(collection
, obj
, matrices
)
138 def get_all_matrices(self
):
139 settings
= self
.get_current_settings()
142 for target
in self
.iter_targets():
143 self
.ensure_target_is_in_cache(target
)
144 matrices
.extend(self
.target_cache
[target
].get_matrices(settings
))
147 def iter_matrix_batches(self
):
148 settings
= self
.get_current_settings()
149 for target
in self
.iter_targets():
150 self
.ensure_target_is_in_cache(target
)
151 yield self
.target_cache
[target
].get_batch(settings
)
153 def iter_targets(self
):
154 yield from self
.targets
155 if self
.active_target
is not None:
156 yield self
.active_target
158 def ensure_target_is_in_cache(self
, target
):
159 if target
not in self
.target_cache
:
160 entry
= TargetCacheEntry(target
, self
.base_scale
)
161 self
.target_cache
[target
] = entry
163 def remove_target_from_cache(self
, target
):
164 self
.target_cache
.pop(self
.active_target
, None)
166 def get_current_settings(self
):
167 return bpy
.context
.scene
.scatter_properties
.to_settings()
169 class TargetCacheEntry
:
170 def __init__(self
, target
, base_scale
):
172 self
.last_used_settings
= None
173 self
.base_scale
= base_scale
174 self
.settings_changed()
176 def get_matrices(self
, settings
):
177 self
._handle
_new
_settings
(settings
)
178 if self
.matrices
is None:
179 self
.matrices
= self
.target
.get_matrices(settings
)
182 def get_batch(self
, settings
):
183 self
._handle
_new
_settings
(settings
)
184 if self
.gpu_batch
is None:
185 self
.gpu_batch
= create_batch_for_matrices(self
.get_matrices(settings
), self
.base_scale
)
186 return self
.gpu_batch
188 def _handle_new_settings(self
, settings
):
189 if settings
!= self
.last_used_settings
:
190 self
.settings_changed()
191 self
.last_used_settings
= settings
193 def settings_changed(self
):
195 self
.gpu_batch
= None
198 # Duplicator Creation
199 ######################################################
201 def make_duplicator(target_collection
, source_object
, matrices
):
204 duplicator
= triangle_object_from_matrices(source_object
.name
+ " Duplicator", matrices
, triangle_scale
)
205 duplicator
.instance_type
= 'FACES'
206 duplicator
.use_instance_faces_scale
= True
207 duplicator
.show_instancer_for_viewport
= True
208 duplicator
.show_instancer_for_render
= False
209 duplicator
.instance_faces_scale
= 1 / triangle_scale
211 copy_obj
= source_object
.copy()
212 copy_obj
.name
= source_object
.name
+ " - copy"
213 copy_obj
.location
= (0, 0, 0)
214 copy_obj
.parent
= duplicator
216 target_collection
.objects
.link(duplicator
)
217 target_collection
.objects
.link(copy_obj
)
219 def triangle_object_from_matrices(name
, matrices
, triangle_scale
):
220 mesh
= triangle_mesh_from_matrices(name
, matrices
, triangle_scale
)
221 return bpy
.data
.objects
.new(name
, mesh
)
223 def triangle_mesh_from_matrices(name
, matrices
, triangle_scale
):
224 mesh
= bpy
.data
.meshes
.new(name
)
225 vertices
, polygons
= mesh_data_from_matrices(matrices
, triangle_scale
)
226 mesh
.from_pydata(vertices
, [], polygons
)
231 unit_triangle_vertices
= (
232 Vector((-3**-0.25, -3**-0.75, 0)),
233 Vector((3**-0.25, -3**-0.75, 0)),
234 Vector((0, 2/3**0.75, 0)))
236 def mesh_data_from_matrices(matrices
, triangle_scale
):
239 triangle_vertices
= [triangle_scale
* v
for v
in unit_triangle_vertices
]
241 for i
, matrix
in enumerate(matrices
):
242 vertices
.extend((matrix
@ v
for v
in triangle_vertices
))
243 polygons
.append((i
* 3 + 0, i
* 3 + 1, i
* 3 + 2))
245 return vertices
, polygons
249 #################################################
251 class BuildState(enum
.Enum
):
252 FINISHED
= enum
.auto()
253 ONGOING
= enum
.auto()
255 class TargetProvider
:
256 def start_build(self
, target_object
):
259 def continue_build(self
, event
):
260 return BuildState
.FINISHED
262 def get_matrices(self
, scatter_settings
):
268 class StrokeTarget(TargetProvider
):
269 def start_build(self
, target_object
):
271 self
.bvhtree
= bvhtree_from_object(target_object
)
274 def continue_build(self
, event
):
275 if event
.type == 'LEFTMOUSE' and event
.value
== 'RELEASE':
276 return BuildState
.FINISHED
278 mouse_pos
= (event
.mouse_region_x
, event
.mouse_region_y
)
279 location
, *_
= shoot_region_2d_ray(self
.bvhtree
, mouse_pos
)
280 if location
is not None:
281 self
.points
.append(location
)
283 return BuildState
.ONGOING
286 if self
.batch
is None:
287 self
.batch
= create_line_strip_batch(self
.points
)
288 draw_line_strip_batch(self
.batch
, color
=(1.0, 0.4, 0.1, 1.0), thickness
=5)
290 def get_matrices(self
, scatter_settings
):
291 return scatter_around_stroke(self
.points
, self
.bvhtree
, scatter_settings
)
293 def scatter_around_stroke(stroke_points
, bvhtree
, settings
):
294 scattered_matrices
= []
295 for point
, local_seed
in iter_points_on_stroke_with_seed(stroke_points
, settings
.density
, settings
.seed
):
296 matrix
= scatter_from_source_point(bvhtree
, point
, local_seed
, settings
)
297 scattered_matrices
.append(matrix
)
298 return scattered_matrices
300 def iter_points_on_stroke_with_seed(stroke_points
, density
, seed
):
301 for i
, (start
, end
) in enumerate(iter_pairwise(stroke_points
)):
302 segment_seed
= sub_seed(seed
, i
)
303 segment_vector
= end
- start
305 segment_length
= segment_vector
.length
306 amount
= round_random(segment_length
* density
, segment_seed
)
308 for j
in range(amount
):
309 t
= random_uniform(sub_seed(segment_seed
, j
, 0))
310 origin
= start
+ t
* segment_vector
311 yield origin
, sub_seed(segment_seed
, j
, 1)
313 def scatter_from_source_point(bvhtree
, point
, seed
, settings
):
314 # Project displaced point on surface
315 radius
= random_uniform(sub_seed(seed
, 0)) * settings
.radius
316 offset
= random_vector(sub_seed(seed
, 2)) * radius
317 location
, normal
, *_
= bvhtree
.find_nearest(point
+ offset
)
318 assert location
is not None
321 up_direction
= normal
if settings
.use_normal_rotation
else Vector((0, 0, 1))
324 min_scale
= settings
.scale
* (1 - settings
.random_scale
)
325 max_scale
= settings
.scale
326 scale
= random_uniform(sub_seed(seed
, 1), min_scale
, max_scale
)
329 location
+= normal
* settings
.normal_offset
* scale
332 z_rotation
= Euler((0, 0, random_uniform(sub_seed(seed
, 3), 0, 2 * math
.pi
))).to_matrix()
333 up_rotation
= up_direction
.to_track_quat('Z', 'X').to_matrix()
334 local_rotation
= random_euler(sub_seed(seed
, 3), settings
.rotation
).to_matrix()
335 rotation
= local_rotation
@ up_rotation
@ z_rotation
337 return Matrix
.Translation(location
) @ rotation
.to_4x4() @ scale_matrix(scale
)
341 #################################################
344 (-1, -1, 1), ( 1, -1, 1), ( 1, 1, 1), (-1, 1, 1),
345 (-1, -1, -1), ( 1, -1, -1), ( 1, 1, -1), (-1, 1, -1))
348 (0, 1, 2), (2, 3, 0), (1, 5, 6), (6, 2, 1),
349 (7, 6, 5), (5, 4, 7), (4, 0, 3), (3, 7, 4),
350 (4, 5, 1), (1, 0, 4), (3, 2, 6), (6, 7, 3))
352 box_vertices
= tuple(Vector(vertex
) * 0.5 for vertex
in box_vertices
)
354 def draw_matrices_batches(batches
):
355 shader
= get_uniform_color_shader()
357 shader
.uniform_float("color", (0.4, 0.4, 1.0, 0.3))
359 bgl
.glEnable(bgl
.GL_BLEND
)
360 bgl
.glDepthMask(bgl
.GL_FALSE
)
362 for batch
in batches
:
365 bgl
.glDisable(bgl
.GL_BLEND
)
366 bgl
.glDepthMask(bgl
.GL_TRUE
)
368 def create_batch_for_matrices(matrices
, base_scale
):
372 scaled_box_vertices
= [base_scale
* vertex
for vertex
in box_vertices
]
374 for matrix
in matrices
:
376 coords
.extend((matrix
@ vertex
for vertex
in scaled_box_vertices
))
377 indices
.extend(tuple(index
+ offset
for index
in element
) for element
in box_indices
)
379 batch
= batch_for_shader(get_uniform_color_shader(),
380 'TRIS', {"pos" : coords
}, indices
= indices
)
384 def draw_line_strip_batch(batch
, color
, thickness
=1):
385 shader
= get_uniform_color_shader()
386 bgl
.glLineWidth(thickness
)
388 shader
.uniform_float("color", color
)
391 def create_line_strip_batch(coords
):
392 return batch_for_shader(get_uniform_color_shader(), 'LINE_STRIP', {"pos" : coords
})
395 def draw_text(location
, text
, size
=15, color
=(1, 1, 1, 1)):
397 ui_scale
= bpy
.context
.preferences
.system
.ui_scale
398 blf
.position(font_id
, *location
)
399 blf
.size(font_id
, round(size
* ui_scale
), 72)
400 blf
.draw(font_id
, text
)
404 ########################################################
407 Pythons random functions are designed to be used in cases
408 when a seed is set once and then many random numbers are
409 generated. To improve the user experience I want to have
410 full control over how random seeds propagate through the
411 functions. This is why I use custom random functions.
413 One benefit is that changing the object density does not
414 generate new random positions for all objects.
417 def round_random(value
, seed
):
418 probability
= value
% 1
419 if probability
< random_uniform(seed
):
420 return math
.floor(value
)
422 return math
.ceil(value
)
424 def random_vector(x
, min=-1, max=1):
426 random_uniform(sub_seed(x
, 0), min, max),
427 random_uniform(sub_seed(x
, 1), min, max),
428 random_uniform(sub_seed(x
, 2), min, max)))
430 def random_euler(x
, factor
):
431 return Euler(tuple(random_vector(x
) * factor
))
433 def random_uniform(x
, min=0, max=1):
434 return random_int(x
) / 2147483648 * (max - min) + min
438 return (x
* (x
* x
* 15731 + 789221) + 1376312589) & 0x7fffffff
440 def sub_seed(seed
, index
, index2
=0):
441 return random_int(seed
* 3243 + index
* 5643 + index2
* 54243)
444 def currently_in_3d_view(context
):
445 return context
.space_data
.type == 'VIEW_3D'
447 def get_selected_non_active_objects(context
):
448 return set(context
.selected_objects
) - {context
.active_object
}
450 def make_random_chunks(sequence
, chunk_amount
):
451 sequence
= list(sequence
)
452 random
.shuffle(sequence
)
453 return make_chunks(sequence
, chunk_amount
)
455 def make_chunks(sequence
, chunk_amount
):
456 length
= math
.ceil(len(sequence
) / chunk_amount
)
457 return [sequence
[i
:i
+length
] for i
in range(0, len(sequence
), length
)]
459 def iter_pairwise(sequence
):
460 return zip(sequence
, islice(sequence
, 1, None))
462 def bvhtree_from_object(object):
466 depsgraph
= bpy
.context
.evaluated_depsgraph_get()
467 object_eval
= object.evaluated_get(depsgraph
)
468 mesh
= object_eval
.to_mesh()
470 bm
.transform(object.matrix_world
)
472 bvhtree
= BVHTree
.FromBMesh(bm
)
473 object_eval
.to_mesh_clear()
476 def shoot_region_2d_ray(bvhtree
, position_2d
):
477 region
= bpy
.context
.region
478 region_3d
= bpy
.context
.space_data
.region_3d
480 origin
= region_2d_to_origin_3d(region
, region_3d
, position_2d
)
481 direction
= region_2d_to_vector_3d(region
, region_3d
, position_2d
)
483 location
, normal
, index
, distance
= bvhtree
.ray_cast(origin
, direction
)
484 return location
, normal
, index
, distance
486 def scale_matrix(factor
):
487 m
= Matrix
.Identity(4)
493 def event_is_in_region(event
, region
):
494 return (region
.x
<= event
.mouse_x
<= region
.x
+ region
.width
495 and region
.y
<= event
.mouse_y
<= region
.y
+ region
.height
)
497 def get_max_object_side_length(objects
):
499 max(obj
.dimensions
[0] for obj
in objects
),
500 max(obj
.dimensions
[1] for obj
in objects
),
501 max(obj
.dimensions
[2] for obj
in objects
)
504 def get_uniform_color_shader():
505 return gpu
.shader
.from_builtin('3D_UNIFORM_COLOR')
509 ###############################################
512 bpy
.utils
.register_class(ScatterObjects
)
515 bpy
.utils
.unregister_class(ScatterObjects
)