1 # SPDX-License-Identifier: GPL-2.0-or-later
10 from itertools
import islice
11 from mathutils
.bvhtree
import BVHTree
12 from mathutils
import Vector
, Matrix
, Euler
13 from gpu_extras
.batch
import batch_for_shader
15 from bpy_extras
.view3d_utils
import (
16 region_2d_to_vector_3d
,
17 region_2d_to_origin_3d
22 ################################################################
24 class ScatterObjects(bpy
.types
.Operator
):
25 bl_idname
= "object.scatter"
26 bl_label
= "Scatter Objects"
27 bl_options
= {'REGISTER', 'UNDO'}
30 def poll(cls
, context
):
32 currently_in_3d_view(context
)
33 and context
.active_object
is not None
34 and context
.active_object
.mode
== 'OBJECT')
36 def invoke(self
, context
, event
):
37 self
.target_object
= context
.active_object
38 self
.objects_to_scatter
= get_selected_non_active_objects(context
)
40 if self
.target_object
is None or len(self
.objects_to_scatter
) == 0:
41 self
.report({'ERROR'}, "Select objects to scatter and a target object")
44 self
.base_scale
= get_max_object_side_length(self
.objects_to_scatter
)
47 self
.active_target
= None
48 self
.target_cache
= {}
50 self
.enable_draw_callback()
51 context
.window_manager
.modal_handler_add(self
)
52 return {'RUNNING_MODAL'}
54 def modal(self
, context
, event
):
55 context
.area
.tag_redraw()
57 if not event_is_in_region(event
, context
.region
) and self
.active_target
is None:
58 return {'PASS_THROUGH'}
60 if event
.type == 'ESC':
61 return self
.finish('CANCELLED')
63 if event
.type == 'RET' and event
.value
== 'PRESS':
64 self
.create_scatter_object()
65 return self
.finish('FINISHED')
67 event_used
= self
.handle_non_exit_event(event
)
69 return {'RUNNING_MODAL'}
71 return {'PASS_THROUGH'}
73 def handle_non_exit_event(self
, event
):
74 if self
.active_target
is None:
75 if event
.type == 'LEFTMOUSE' and event
.value
== 'PRESS':
76 self
.active_target
= StrokeTarget()
77 self
.active_target
.start_build(self
.target_object
)
80 build_state
= self
.active_target
.continue_build(event
)
81 if build_state
== BuildState
.FINISHED
:
82 self
.targets
.append(self
.active_target
)
83 self
.active_target
= None
84 self
.remove_target_from_cache(self
.active_target
)
89 def enable_draw_callback(self
):
90 self
._draw
_callback
_view
= bpy
.types
.SpaceView3D
.draw_handler_add(self
.draw_view
, (), 'WINDOW', 'POST_VIEW')
91 self
._draw
_callback
_px
= bpy
.types
.SpaceView3D
.draw_handler_add(self
.draw_px
, (), 'WINDOW', 'POST_PIXEL')
93 def disable_draw_callback(self
):
94 bpy
.types
.SpaceView3D
.draw_handler_remove(self
._draw
_callback
_view
, 'WINDOW')
95 bpy
.types
.SpaceView3D
.draw_handler_remove(self
._draw
_callback
_px
, 'WINDOW')
98 for target
in self
.iter_targets():
101 draw_matrices_batches(list(self
.iter_matrix_batches()))
104 draw_text((20, 20, 0), "Instances: " + str(len(self
.get_all_matrices())))
106 def finish(self
, return_value
):
107 self
.disable_draw_callback()
108 bpy
.context
.area
.tag_redraw()
109 return {return_value}
111 def create_scatter_object(self
):
112 matrix_chunks
= make_random_chunks(
113 self
.get_all_matrices(), len(self
.objects_to_scatter
))
115 collection
= bpy
.data
.collections
.new("Scatter")
116 bpy
.context
.collection
.children
.link(collection
)
118 for obj
, matrices
in zip(self
.objects_to_scatter
, matrix_chunks
):
119 make_duplicator(collection
, obj
, matrices
)
121 def get_all_matrices(self
):
122 settings
= self
.get_current_settings()
125 for target
in self
.iter_targets():
126 self
.ensure_target_is_in_cache(target
)
127 matrices
.extend(self
.target_cache
[target
].get_matrices(settings
))
130 def iter_matrix_batches(self
):
131 settings
= self
.get_current_settings()
132 for target
in self
.iter_targets():
133 self
.ensure_target_is_in_cache(target
)
134 yield self
.target_cache
[target
].get_batch(settings
)
136 def iter_targets(self
):
137 yield from self
.targets
138 if self
.active_target
is not None:
139 yield self
.active_target
141 def ensure_target_is_in_cache(self
, target
):
142 if target
not in self
.target_cache
:
143 entry
= TargetCacheEntry(target
, self
.base_scale
)
144 self
.target_cache
[target
] = entry
146 def remove_target_from_cache(self
, target
):
147 self
.target_cache
.pop(self
.active_target
, None)
149 def get_current_settings(self
):
150 return bpy
.context
.scene
.scatter_properties
.to_settings()
152 class TargetCacheEntry
:
153 def __init__(self
, target
, base_scale
):
155 self
.last_used_settings
= None
156 self
.base_scale
= base_scale
157 self
.settings_changed()
159 def get_matrices(self
, settings
):
160 self
._handle
_new
_settings
(settings
)
161 if self
.matrices
is None:
162 self
.matrices
= self
.target
.get_matrices(settings
)
165 def get_batch(self
, settings
):
166 self
._handle
_new
_settings
(settings
)
167 if self
.gpu_batch
is None:
168 self
.gpu_batch
= create_batch_for_matrices(self
.get_matrices(settings
), self
.base_scale
)
169 return self
.gpu_batch
171 def _handle_new_settings(self
, settings
):
172 if settings
!= self
.last_used_settings
:
173 self
.settings_changed()
174 self
.last_used_settings
= settings
176 def settings_changed(self
):
178 self
.gpu_batch
= None
181 # Duplicator Creation
182 ######################################################
184 def make_duplicator(target_collection
, source_object
, matrices
):
187 duplicator
= triangle_object_from_matrices(source_object
.name
+ " Duplicator", matrices
, triangle_scale
)
188 duplicator
.instance_type
= 'FACES'
189 duplicator
.use_instance_faces_scale
= True
190 duplicator
.show_instancer_for_viewport
= True
191 duplicator
.show_instancer_for_render
= False
192 duplicator
.instance_faces_scale
= 1 / triangle_scale
194 copy_obj
= source_object
.copy()
195 copy_obj
.name
= source_object
.name
+ " - copy"
196 copy_obj
.location
= (0, 0, 0)
197 copy_obj
.parent
= duplicator
199 target_collection
.objects
.link(duplicator
)
200 target_collection
.objects
.link(copy_obj
)
202 def triangle_object_from_matrices(name
, matrices
, triangle_scale
):
203 mesh
= triangle_mesh_from_matrices(name
, matrices
, triangle_scale
)
204 return bpy
.data
.objects
.new(name
, mesh
)
206 def triangle_mesh_from_matrices(name
, matrices
, triangle_scale
):
207 mesh
= bpy
.data
.meshes
.new(name
)
208 vertices
, polygons
= mesh_data_from_matrices(matrices
, triangle_scale
)
209 mesh
.from_pydata(vertices
, [], polygons
)
214 unit_triangle_vertices
= (
215 Vector((-3**-0.25, -3**-0.75, 0)),
216 Vector((3**-0.25, -3**-0.75, 0)),
217 Vector((0, 2/3**0.75, 0)))
219 def mesh_data_from_matrices(matrices
, triangle_scale
):
222 triangle_vertices
= [triangle_scale
* v
for v
in unit_triangle_vertices
]
224 for i
, matrix
in enumerate(matrices
):
225 vertices
.extend((matrix
@ v
for v
in triangle_vertices
))
226 polygons
.append((i
* 3 + 0, i
* 3 + 1, i
* 3 + 2))
228 return vertices
, polygons
232 #################################################
234 class BuildState(enum
.Enum
):
235 FINISHED
= enum
.auto()
236 ONGOING
= enum
.auto()
238 class TargetProvider
:
239 def start_build(self
, target_object
):
242 def continue_build(self
, event
):
243 return BuildState
.FINISHED
245 def get_matrices(self
, scatter_settings
):
251 class StrokeTarget(TargetProvider
):
252 def start_build(self
, target_object
):
254 self
.bvhtree
= bvhtree_from_object(target_object
)
257 def continue_build(self
, event
):
258 if event
.type == 'LEFTMOUSE' and event
.value
== 'RELEASE':
259 return BuildState
.FINISHED
261 mouse_pos
= (event
.mouse_region_x
, event
.mouse_region_y
)
262 location
, *_
= shoot_region_2d_ray(self
.bvhtree
, mouse_pos
)
263 if location
is not None:
264 self
.points
.append(location
)
266 return BuildState
.ONGOING
269 if self
.batch
is None:
270 self
.batch
= create_line_strip_batch(self
.points
)
271 draw_line_strip_batch(self
.batch
, color
=(1.0, 0.4, 0.1, 1.0), thickness
=5)
273 def get_matrices(self
, scatter_settings
):
274 return scatter_around_stroke(self
.points
, self
.bvhtree
, scatter_settings
)
276 def scatter_around_stroke(stroke_points
, bvhtree
, settings
):
277 scattered_matrices
= []
278 for point
, local_seed
in iter_points_on_stroke_with_seed(stroke_points
, settings
.density
, settings
.seed
):
279 matrix
= scatter_from_source_point(bvhtree
, point
, local_seed
, settings
)
280 scattered_matrices
.append(matrix
)
281 return scattered_matrices
283 def iter_points_on_stroke_with_seed(stroke_points
, density
, seed
):
284 for i
, (start
, end
) in enumerate(iter_pairwise(stroke_points
)):
285 segment_seed
= sub_seed(seed
, i
)
286 segment_vector
= end
- start
288 segment_length
= segment_vector
.length
289 amount
= round_random(segment_length
* density
, segment_seed
)
291 for j
in range(amount
):
292 t
= random_uniform(sub_seed(segment_seed
, j
, 0))
293 origin
= start
+ t
* segment_vector
294 yield origin
, sub_seed(segment_seed
, j
, 1)
296 def scatter_from_source_point(bvhtree
, point
, seed
, settings
):
297 # Project displaced point on surface
298 radius
= random_uniform(sub_seed(seed
, 0)) * settings
.radius
299 offset
= random_vector(sub_seed(seed
, 2)) * radius
300 location
, normal
, *_
= bvhtree
.find_nearest(point
+ offset
)
301 assert location
is not None
304 up_direction
= normal
if settings
.use_normal_rotation
else Vector((0, 0, 1))
307 min_scale
= settings
.scale
* (1 - settings
.random_scale
)
308 max_scale
= settings
.scale
309 scale
= random_uniform(sub_seed(seed
, 1), min_scale
, max_scale
)
312 location
+= normal
* settings
.normal_offset
* scale
315 z_rotation
= Euler((0, 0, random_uniform(sub_seed(seed
, 3), 0, 2 * math
.pi
))).to_matrix()
316 up_rotation
= up_direction
.to_track_quat('Z', 'X').to_matrix()
317 local_rotation
= random_euler(sub_seed(seed
, 3), settings
.rotation
).to_matrix()
318 rotation
= local_rotation
@ up_rotation
@ z_rotation
320 return Matrix
.Translation(location
) @ rotation
.to_4x4() @ scale_matrix(scale
)
324 #################################################
327 (-1, -1, 1), ( 1, -1, 1), ( 1, 1, 1), (-1, 1, 1),
328 (-1, -1, -1), ( 1, -1, -1), ( 1, 1, -1), (-1, 1, -1))
331 (0, 1, 2), (2, 3, 0), (1, 5, 6), (6, 2, 1),
332 (7, 6, 5), (5, 4, 7), (4, 0, 3), (3, 7, 4),
333 (4, 5, 1), (1, 0, 4), (3, 2, 6), (6, 7, 3))
335 box_vertices
= tuple(Vector(vertex
) * 0.5 for vertex
in box_vertices
)
337 def draw_matrices_batches(batches
):
338 shader
= get_uniform_color_shader()
340 shader
.uniform_float("color", (0.4, 0.4, 1.0, 0.3))
342 gpu
.state
.blend_set('ALPHA')
343 gpu
.state
.depth_mask_set(False)
345 for batch
in batches
:
348 gpu
.state
.blend_set('NONE')
349 gpu
.state
.depth_mask_set(True)
351 def create_batch_for_matrices(matrices
, base_scale
):
355 scaled_box_vertices
= [base_scale
* vertex
for vertex
in box_vertices
]
357 for matrix
in matrices
:
359 coords
.extend((matrix
@ vertex
for vertex
in scaled_box_vertices
))
360 indices
.extend(tuple(index
+ offset
for index
in element
) for element
in box_indices
)
362 batch
= batch_for_shader(get_uniform_color_shader(),
363 'TRIS', {"pos" : coords
}, indices
= indices
)
367 def draw_line_strip_batch(batch
, color
, thickness
=1):
368 shader
= get_uniform_color_shader()
369 gpu
.state
.line_width_set(thickness
)
371 shader
.uniform_float("color", color
)
374 def create_line_strip_batch(coords
):
375 return batch_for_shader(get_uniform_color_shader(), 'LINE_STRIP', {"pos" : coords
})
378 def draw_text(location
, text
, size
=15, color
=(1, 1, 1, 1)):
380 ui_scale
= bpy
.context
.preferences
.system
.ui_scale
381 blf
.position(font_id
, *location
)
382 blf
.size(font_id
, round(size
* ui_scale
), 72)
383 blf
.draw(font_id
, text
)
387 ########################################################
390 Pythons random functions are designed to be used in cases
391 when a seed is set once and then many random numbers are
392 generated. To improve the user experience I want to have
393 full control over how random seeds propagate through the
394 functions. This is why I use custom random functions.
396 One benefit is that changing the object density does not
397 generate new random positions for all objects.
400 def round_random(value
, seed
):
401 probability
= value
% 1
402 if probability
< random_uniform(seed
):
403 return math
.floor(value
)
405 return math
.ceil(value
)
407 def random_vector(x
, min=-1, max=1):
409 random_uniform(sub_seed(x
, 0), min, max),
410 random_uniform(sub_seed(x
, 1), min, max),
411 random_uniform(sub_seed(x
, 2), min, max)))
413 def random_euler(x
, factor
):
414 return Euler(tuple(random_vector(x
) * factor
))
416 def random_uniform(x
, min=0, max=1):
417 return random_int(x
) / 2147483648 * (max - min) + min
421 return (x
* (x
* x
* 15731 + 789221) + 1376312589) & 0x7fffffff
423 def sub_seed(seed
, index
, index2
=0):
424 return random_int(seed
* 3243 + index
* 5643 + index2
* 54243)
427 def currently_in_3d_view(context
):
428 return context
.space_data
.type == 'VIEW_3D'
430 def get_selected_non_active_objects(context
):
431 return set(context
.selected_objects
) - {context
.active_object
}
433 def make_random_chunks(sequence
, chunk_amount
):
434 sequence
= list(sequence
)
435 random
.shuffle(sequence
)
436 return make_chunks(sequence
, chunk_amount
)
438 def make_chunks(sequence
, chunk_amount
):
439 length
= math
.ceil(len(sequence
) / chunk_amount
)
440 return [sequence
[i
:i
+length
] for i
in range(0, len(sequence
), length
)]
442 def iter_pairwise(sequence
):
443 return zip(sequence
, islice(sequence
, 1, None))
445 def bvhtree_from_object(object):
449 depsgraph
= bpy
.context
.evaluated_depsgraph_get()
450 object_eval
= object.evaluated_get(depsgraph
)
451 mesh
= object_eval
.to_mesh()
453 bm
.transform(object.matrix_world
)
455 bvhtree
= BVHTree
.FromBMesh(bm
)
456 object_eval
.to_mesh_clear()
459 def shoot_region_2d_ray(bvhtree
, position_2d
):
460 region
= bpy
.context
.region
461 region_3d
= bpy
.context
.space_data
.region_3d
463 origin
= region_2d_to_origin_3d(region
, region_3d
, position_2d
)
464 direction
= region_2d_to_vector_3d(region
, region_3d
, position_2d
)
466 location
, normal
, index
, distance
= bvhtree
.ray_cast(origin
, direction
)
467 return location
, normal
, index
, distance
469 def scale_matrix(factor
):
470 m
= Matrix
.Identity(4)
476 def event_is_in_region(event
, region
):
477 return (region
.x
<= event
.mouse_x
<= region
.x
+ region
.width
478 and region
.y
<= event
.mouse_y
<= region
.y
+ region
.height
)
480 def get_max_object_side_length(objects
):
482 max(obj
.dimensions
[0] for obj
in objects
),
483 max(obj
.dimensions
[1] for obj
in objects
),
484 max(obj
.dimensions
[2] for obj
in objects
)
487 def get_uniform_color_shader():
488 return gpu
.shader
.from_builtin('3D_UNIFORM_COLOR')
492 ###############################################
495 bpy
.utils
.register_class(ScatterObjects
)
498 bpy
.utils
.unregister_class(ScatterObjects
)