1 # SPDX-FileCopyrightText: 2018-2023 Blender Foundation
3 # SPDX-License-Identifier: GPL-2.0-or-later
12 from itertools
import islice
13 from mathutils
.bvhtree
import BVHTree
14 from mathutils
import Vector
, Matrix
, Euler
15 from gpu_extras
.batch
import batch_for_shader
17 from bpy_extras
.view3d_utils
import (
18 region_2d_to_vector_3d
,
19 region_2d_to_origin_3d
24 ################################################################
26 class ScatterObjects(bpy
.types
.Operator
):
27 bl_idname
= "object.scatter"
28 bl_label
= "Scatter Objects"
29 bl_options
= {'REGISTER', 'UNDO'}
32 def poll(cls
, context
):
34 currently_in_3d_view(context
)
35 and context
.active_object
is not None
36 and context
.active_object
.mode
== 'OBJECT')
38 def invoke(self
, context
, event
):
39 self
.target_object
= context
.active_object
40 self
.objects_to_scatter
= get_selected_non_active_objects(context
)
42 if self
.target_object
is None or len(self
.objects_to_scatter
) == 0:
43 self
.report({'ERROR'}, "Select objects to scatter and a target object")
46 self
.base_scale
= get_max_object_side_length(self
.objects_to_scatter
)
49 self
.active_target
= None
50 self
.target_cache
= {}
52 self
.enable_draw_callback()
53 context
.window_manager
.modal_handler_add(self
)
54 return {'RUNNING_MODAL'}
56 def modal(self
, context
, event
):
57 context
.area
.tag_redraw()
59 if not event_is_in_region(event
, context
.region
) and self
.active_target
is None:
60 return {'PASS_THROUGH'}
62 if event
.type == 'ESC':
63 return self
.finish('CANCELLED')
65 if event
.type == 'RET' and event
.value
== 'PRESS':
66 self
.create_scatter_object()
67 return self
.finish('FINISHED')
69 event_used
= self
.handle_non_exit_event(event
)
71 return {'RUNNING_MODAL'}
73 return {'PASS_THROUGH'}
75 def handle_non_exit_event(self
, event
):
76 if self
.active_target
is None:
77 if event
.type == 'LEFTMOUSE' and event
.value
== 'PRESS':
78 self
.active_target
= StrokeTarget()
79 self
.active_target
.start_build(self
.target_object
)
82 build_state
= self
.active_target
.continue_build(event
)
83 if build_state
== BuildState
.FINISHED
:
84 self
.targets
.append(self
.active_target
)
85 self
.active_target
= None
86 self
.remove_target_from_cache(self
.active_target
)
91 def enable_draw_callback(self
):
92 self
._draw
_callback
_view
= bpy
.types
.SpaceView3D
.draw_handler_add(self
.draw_view
, (), 'WINDOW', 'POST_VIEW')
93 self
._draw
_callback
_px
= bpy
.types
.SpaceView3D
.draw_handler_add(self
.draw_px
, (), 'WINDOW', 'POST_PIXEL')
95 def disable_draw_callback(self
):
96 bpy
.types
.SpaceView3D
.draw_handler_remove(self
._draw
_callback
_view
, 'WINDOW')
97 bpy
.types
.SpaceView3D
.draw_handler_remove(self
._draw
_callback
_px
, 'WINDOW')
100 for target
in self
.iter_targets():
103 draw_matrices_batches(list(self
.iter_matrix_batches()))
106 draw_text((20, 20, 0), "Instances: " + str(len(self
.get_all_matrices())))
108 def finish(self
, return_value
):
109 self
.disable_draw_callback()
110 bpy
.context
.area
.tag_redraw()
111 return {return_value}
113 def create_scatter_object(self
):
114 matrix_chunks
= make_random_chunks(
115 self
.get_all_matrices(), len(self
.objects_to_scatter
))
117 collection
= bpy
.data
.collections
.new("Scatter")
118 bpy
.context
.collection
.children
.link(collection
)
120 for obj
, matrices
in zip(self
.objects_to_scatter
, matrix_chunks
):
121 make_duplicator(collection
, obj
, matrices
)
123 def get_all_matrices(self
):
124 settings
= self
.get_current_settings()
127 for target
in self
.iter_targets():
128 self
.ensure_target_is_in_cache(target
)
129 matrices
.extend(self
.target_cache
[target
].get_matrices(settings
))
132 def iter_matrix_batches(self
):
133 settings
= self
.get_current_settings()
134 for target
in self
.iter_targets():
135 self
.ensure_target_is_in_cache(target
)
136 yield self
.target_cache
[target
].get_batch(settings
)
138 def iter_targets(self
):
139 yield from self
.targets
140 if self
.active_target
is not None:
141 yield self
.active_target
143 def ensure_target_is_in_cache(self
, target
):
144 if target
not in self
.target_cache
:
145 entry
= TargetCacheEntry(target
, self
.base_scale
)
146 self
.target_cache
[target
] = entry
148 def remove_target_from_cache(self
, target
):
149 self
.target_cache
.pop(self
.active_target
, None)
151 def get_current_settings(self
):
152 return bpy
.context
.scene
.scatter_properties
.to_settings()
154 class TargetCacheEntry
:
155 def __init__(self
, target
, base_scale
):
157 self
.last_used_settings
= None
158 self
.base_scale
= base_scale
159 self
.settings_changed()
161 def get_matrices(self
, settings
):
162 self
._handle
_new
_settings
(settings
)
163 if self
.matrices
is None:
164 self
.matrices
= self
.target
.get_matrices(settings
)
167 def get_batch(self
, settings
):
168 self
._handle
_new
_settings
(settings
)
169 if self
.gpu_batch
is None:
170 self
.gpu_batch
= create_batch_for_matrices(self
.get_matrices(settings
), self
.base_scale
)
171 return self
.gpu_batch
173 def _handle_new_settings(self
, settings
):
174 if settings
!= self
.last_used_settings
:
175 self
.settings_changed()
176 self
.last_used_settings
= settings
178 def settings_changed(self
):
180 self
.gpu_batch
= None
183 # Duplicator Creation
184 ######################################################
186 def make_duplicator(target_collection
, source_object
, matrices
):
189 duplicator
= triangle_object_from_matrices(source_object
.name
+ " Duplicator", matrices
, triangle_scale
)
190 duplicator
.instance_type
= 'FACES'
191 duplicator
.use_instance_faces_scale
= True
192 duplicator
.show_instancer_for_viewport
= True
193 duplicator
.show_instancer_for_render
= False
194 duplicator
.instance_faces_scale
= 1 / triangle_scale
196 copy_obj
= source_object
.copy()
197 copy_obj
.name
= source_object
.name
+ " - copy"
198 copy_obj
.location
= (0, 0, 0)
199 copy_obj
.parent
= duplicator
201 target_collection
.objects
.link(duplicator
)
202 target_collection
.objects
.link(copy_obj
)
204 def triangle_object_from_matrices(name
, matrices
, triangle_scale
):
205 mesh
= triangle_mesh_from_matrices(name
, matrices
, triangle_scale
)
206 return bpy
.data
.objects
.new(name
, mesh
)
208 def triangle_mesh_from_matrices(name
, matrices
, triangle_scale
):
209 mesh
= bpy
.data
.meshes
.new(name
)
210 vertices
, polygons
= mesh_data_from_matrices(matrices
, triangle_scale
)
211 mesh
.from_pydata(vertices
, [], polygons
)
216 unit_triangle_vertices
= (
217 Vector((-3**-0.25, -3**-0.75, 0)),
218 Vector((3**-0.25, -3**-0.75, 0)),
219 Vector((0, 2/3**0.75, 0)))
221 def mesh_data_from_matrices(matrices
, triangle_scale
):
224 triangle_vertices
= [triangle_scale
* v
for v
in unit_triangle_vertices
]
226 for i
, matrix
in enumerate(matrices
):
227 vertices
.extend((matrix
@ v
for v
in triangle_vertices
))
228 polygons
.append((i
* 3 + 0, i
* 3 + 1, i
* 3 + 2))
230 return vertices
, polygons
234 #################################################
236 class BuildState(enum
.Enum
):
237 FINISHED
= enum
.auto()
238 ONGOING
= enum
.auto()
240 class TargetProvider
:
241 def start_build(self
, target_object
):
244 def continue_build(self
, event
):
245 return BuildState
.FINISHED
247 def get_matrices(self
, scatter_settings
):
253 class StrokeTarget(TargetProvider
):
254 def start_build(self
, target_object
):
256 self
.bvhtree
= bvhtree_from_object(target_object
)
259 def continue_build(self
, event
):
260 if event
.type == 'LEFTMOUSE' and event
.value
== 'RELEASE':
261 return BuildState
.FINISHED
263 mouse_pos
= (event
.mouse_region_x
, event
.mouse_region_y
)
264 location
, *_
= shoot_region_2d_ray(self
.bvhtree
, mouse_pos
)
265 if location
is not None:
266 self
.points
.append(location
)
268 return BuildState
.ONGOING
271 if self
.batch
is None:
272 self
.batch
= create_line_strip_batch(self
.points
)
273 draw_line_strip_batch(self
.batch
, color
=(1.0, 0.4, 0.1, 1.0), thickness
=5)
275 def get_matrices(self
, scatter_settings
):
276 return scatter_around_stroke(self
.points
, self
.bvhtree
, scatter_settings
)
278 def scatter_around_stroke(stroke_points
, bvhtree
, settings
):
279 scattered_matrices
= []
280 for point
, local_seed
in iter_points_on_stroke_with_seed(stroke_points
, settings
.density
, settings
.seed
):
281 matrix
= scatter_from_source_point(bvhtree
, point
, local_seed
, settings
)
282 scattered_matrices
.append(matrix
)
283 return scattered_matrices
285 def iter_points_on_stroke_with_seed(stroke_points
, density
, seed
):
286 for i
, (start
, end
) in enumerate(iter_pairwise(stroke_points
)):
287 segment_seed
= sub_seed(seed
, i
)
288 segment_vector
= end
- start
290 segment_length
= segment_vector
.length
291 amount
= round_random(segment_length
* density
, segment_seed
)
293 for j
in range(amount
):
294 t
= random_uniform(sub_seed(segment_seed
, j
, 0))
295 origin
= start
+ t
* segment_vector
296 yield origin
, sub_seed(segment_seed
, j
, 1)
298 def scatter_from_source_point(bvhtree
, point
, seed
, settings
):
299 # Project displaced point on surface
300 radius
= random_uniform(sub_seed(seed
, 0)) * settings
.radius
301 offset
= random_vector(sub_seed(seed
, 2)) * radius
302 location
, normal
, *_
= bvhtree
.find_nearest(point
+ offset
)
303 assert location
is not None
306 up_direction
= normal
if settings
.use_normal_rotation
else Vector((0, 0, 1))
309 min_scale
= settings
.scale
* (1 - settings
.random_scale
)
310 max_scale
= settings
.scale
311 scale
= random_uniform(sub_seed(seed
, 1), min_scale
, max_scale
)
314 location
+= normal
* settings
.normal_offset
* scale
317 z_rotation
= Euler((0, 0, random_uniform(sub_seed(seed
, 3), 0, 2 * math
.pi
))).to_matrix()
318 up_rotation
= up_direction
.to_track_quat('Z', 'X').to_matrix()
319 local_rotation
= random_euler(sub_seed(seed
, 3), settings
.rotation
).to_matrix()
320 rotation
= local_rotation
@ up_rotation
@ z_rotation
322 return Matrix
.Translation(location
) @ rotation
.to_4x4() @ scale_matrix(scale
)
326 #################################################
329 (-1, -1, 1), ( 1, -1, 1), ( 1, 1, 1), (-1, 1, 1),
330 (-1, -1, -1), ( 1, -1, -1), ( 1, 1, -1), (-1, 1, -1))
333 (0, 1, 2), (2, 3, 0), (1, 5, 6), (6, 2, 1),
334 (7, 6, 5), (5, 4, 7), (4, 0, 3), (3, 7, 4),
335 (4, 5, 1), (1, 0, 4), (3, 2, 6), (6, 7, 3))
337 box_vertices
= tuple(Vector(vertex
) * 0.5 for vertex
in box_vertices
)
339 def draw_matrices_batches(batches
):
340 shader
= get_uniform_color_shader()
342 shader
.uniform_float("color", (0.4, 0.4, 1.0, 0.3))
344 gpu
.state
.blend_set('ALPHA')
345 gpu
.state
.depth_mask_set(False)
347 for batch
in batches
:
350 gpu
.state
.blend_set('NONE')
351 gpu
.state
.depth_mask_set(True)
353 def create_batch_for_matrices(matrices
, base_scale
):
357 scaled_box_vertices
= [base_scale
* vertex
for vertex
in box_vertices
]
359 for matrix
in matrices
:
361 coords
.extend((matrix
@ vertex
for vertex
in scaled_box_vertices
))
362 indices
.extend(tuple(index
+ offset
for index
in element
) for element
in box_indices
)
364 batch
= batch_for_shader(get_uniform_color_shader(),
365 'TRIS', {"pos" : coords
}, indices
= indices
)
369 def draw_line_strip_batch(batch
, color
, thickness
=1):
370 shader
= get_uniform_color_shader()
371 gpu
.state
.line_width_set(thickness
)
373 shader
.uniform_float("color", color
)
376 def create_line_strip_batch(coords
):
377 return batch_for_shader(get_uniform_color_shader(), 'LINE_STRIP', {"pos" : coords
})
380 def draw_text(location
, text
, size
=15, color
=(1, 1, 1, 1)):
382 ui_scale
= bpy
.context
.preferences
.system
.ui_scale
383 blf
.position(font_id
, *location
)
384 blf
.size(font_id
, round(size
* ui_scale
))
385 blf
.draw(font_id
, text
)
389 ########################################################
392 Pythons random functions are designed to be used in cases
393 when a seed is set once and then many random numbers are
394 generated. To improve the user experience I want to have
395 full control over how random seeds propagate through the
396 functions. This is why I use custom random functions.
398 One benefit is that changing the object density does not
399 generate new random positions for all objects.
402 def round_random(value
, seed
):
403 probability
= value
% 1
404 if probability
< random_uniform(seed
):
405 return math
.floor(value
)
407 return math
.ceil(value
)
409 def random_vector(x
, min=-1, max=1):
411 random_uniform(sub_seed(x
, 0), min, max),
412 random_uniform(sub_seed(x
, 1), min, max),
413 random_uniform(sub_seed(x
, 2), min, max)))
415 def random_euler(x
, factor
):
416 return Euler(tuple(random_vector(x
) * factor
))
418 def random_uniform(x
, min=0, max=1):
419 return random_int(x
) / 2147483648 * (max - min) + min
423 return (x
* (x
* x
* 15731 + 789221) + 1376312589) & 0x7fffffff
425 def sub_seed(seed
, index
, index2
=0):
426 return random_int(seed
* 3243 + index
* 5643 + index2
* 54243)
429 def currently_in_3d_view(context
):
430 return context
.space_data
.type == 'VIEW_3D'
432 def get_selected_non_active_objects(context
):
433 return set(context
.selected_objects
) - {context
.active_object
}
435 def make_random_chunks(sequence
, chunk_amount
):
436 sequence
= list(sequence
)
437 random
.shuffle(sequence
)
438 return make_chunks(sequence
, chunk_amount
)
440 def make_chunks(sequence
, chunk_amount
):
441 length
= math
.ceil(len(sequence
) / chunk_amount
)
442 return [sequence
[i
:i
+length
] for i
in range(0, len(sequence
), length
)]
444 def iter_pairwise(sequence
):
445 return zip(sequence
, islice(sequence
, 1, None))
447 def bvhtree_from_object(object):
451 depsgraph
= bpy
.context
.evaluated_depsgraph_get()
452 object_eval
= object.evaluated_get(depsgraph
)
453 mesh
= object_eval
.to_mesh()
455 bm
.transform(object.matrix_world
)
457 bvhtree
= BVHTree
.FromBMesh(bm
)
458 object_eval
.to_mesh_clear()
461 def shoot_region_2d_ray(bvhtree
, position_2d
):
462 region
= bpy
.context
.region
463 region_3d
= bpy
.context
.space_data
.region_3d
465 origin
= region_2d_to_origin_3d(region
, region_3d
, position_2d
)
466 direction
= region_2d_to_vector_3d(region
, region_3d
, position_2d
)
468 location
, normal
, index
, distance
= bvhtree
.ray_cast(origin
, direction
)
469 return location
, normal
, index
, distance
471 def scale_matrix(factor
):
472 m
= Matrix
.Identity(4)
478 def event_is_in_region(event
, region
):
479 return (region
.x
<= event
.mouse_x
<= region
.x
+ region
.width
480 and region
.y
<= event
.mouse_y
<= region
.y
+ region
.height
)
482 def get_max_object_side_length(objects
):
484 max(obj
.dimensions
[0] for obj
in objects
),
485 max(obj
.dimensions
[1] for obj
in objects
),
486 max(obj
.dimensions
[2] for obj
in objects
)
489 def get_uniform_color_shader():
490 return gpu
.shader
.from_builtin('UNIFORM_COLOR')
494 ###############################################
497 bpy
.utils
.register_class(ScatterObjects
)
500 bpy
.utils
.unregister_class(ScatterObjects
)