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
322 min_scale
= settings
.scale
* (1 - settings
.random_scale
)
323 max_scale
= settings
.scale
324 scale
= random_uniform(sub_seed(seed
, 1), min_scale
, max_scale
)
327 location
+= normal
* settings
.normal_offset
* scale
330 z_rotation
= Euler((0, 0, random_uniform(sub_seed(seed
, 3), 0, 2 * math
.pi
))).to_matrix()
331 normal_rotation
= normal
.to_track_quat('Z', 'X').to_matrix()
332 local_rotation
= random_euler(sub_seed(seed
, 3), settings
.rotation
).to_matrix()
333 rotation
= local_rotation
@ normal_rotation
@ z_rotation
335 return Matrix
.Translation(location
) @ rotation
.to_4x4() @ scale_matrix(scale
)
339 #################################################
342 (-1, -1, 1), ( 1, -1, 1), ( 1, 1, 1), (-1, 1, 1),
343 (-1, -1, -1), ( 1, -1, -1), ( 1, 1, -1), (-1, 1, -1))
346 (0, 1, 2), (2, 3, 0), (1, 5, 6), (6, 2, 1),
347 (7, 6, 5), (5, 4, 7), (4, 0, 3), (3, 7, 4),
348 (4, 5, 1), (1, 0, 4), (3, 2, 6), (6, 7, 3))
350 box_vertices
= tuple(Vector(vertex
) * 0.5 for vertex
in box_vertices
)
352 def draw_matrices_batches(batches
):
353 shader
= get_uniform_color_shader()
355 shader
.uniform_float("color", (0.4, 0.4, 1.0, 0.3))
357 bgl
.glEnable(bgl
.GL_BLEND
)
358 bgl
.glDepthMask(bgl
.GL_FALSE
)
360 for batch
in batches
:
363 bgl
.glDisable(bgl
.GL_BLEND
)
364 bgl
.glDepthMask(bgl
.GL_TRUE
)
366 def create_batch_for_matrices(matrices
, base_scale
):
370 scaled_box_vertices
= [base_scale
* vertex
for vertex
in box_vertices
]
372 for matrix
in matrices
:
374 coords
.extend((matrix
@ vertex
for vertex
in scaled_box_vertices
))
375 indices
.extend(tuple(index
+ offset
for index
in element
) for element
in box_indices
)
377 batch
= batch_for_shader(get_uniform_color_shader(),
378 'TRIS', {"pos" : coords
}, indices
= indices
)
382 def draw_line_strip_batch(batch
, color
, thickness
=1):
383 shader
= get_uniform_color_shader()
384 bgl
.glLineWidth(thickness
)
386 shader
.uniform_float("color", color
)
389 def create_line_strip_batch(coords
):
390 return batch_for_shader(get_uniform_color_shader(), 'LINE_STRIP', {"pos" : coords
})
393 def draw_text(location
, text
, size
=15, color
=(1, 1, 1, 1)):
395 ui_scale
= bpy
.context
.preferences
.system
.ui_scale
396 blf
.position(font_id
, *location
)
397 blf
.size(font_id
, round(size
* ui_scale
), bpy
.context
.preferences
.system
.dpi
)
398 blf
.draw(font_id
, text
)
402 ########################################################
405 Pythons random functions are designed to be used in cases
406 when a seed is set once and then many random numbers are
407 generated. To improve the user experience I want to have
408 full control over how random seeds propagate through the
409 functions. This is why I use custom random functions.
411 One benefit is that changing the object density does not
412 generate new random positions for all objects.
415 def round_random(value
, seed
):
416 probability
= value
% 1
417 if probability
< random_uniform(seed
):
418 return math
.floor(value
)
420 return math
.ceil(value
)
422 def random_vector(x
, min=-1, max=1):
424 random_uniform(sub_seed(x
, 0), min, max),
425 random_uniform(sub_seed(x
, 1), min, max),
426 random_uniform(sub_seed(x
, 2), min, max)))
428 def random_euler(x
, factor
):
429 return Euler(tuple(random_vector(x
) * factor
))
431 def random_uniform(x
, min=0, max=1):
432 return random_int(x
) / 2147483648 * (max - min) + min
436 return (x
* (x
* x
* 15731 + 789221) + 1376312589) & 0x7fffffff
438 def sub_seed(seed
, index
, index2
=0):
439 return random_int(seed
* 3243 + index
* 5643 + index2
* 54243)
442 def currently_in_3d_view(context
):
443 return context
.space_data
.type == 'VIEW_3D'
445 def get_selected_non_active_objects(context
):
446 return set(context
.selected_objects
) - {context
.active_object
}
448 def make_random_chunks(sequence
, chunk_amount
):
449 sequence
= list(sequence
)
450 random
.shuffle(sequence
)
451 return make_chunks(sequence
, chunk_amount
)
453 def make_chunks(sequence
, chunk_amount
):
454 length
= math
.ceil(len(sequence
) / chunk_amount
)
455 return [sequence
[i
:i
+length
] for i
in range(0, len(sequence
), length
)]
457 def iter_pairwise(sequence
):
458 return zip(sequence
, islice(sequence
, 1, None))
460 def bvhtree_from_object(object):
464 depsgraph
= bpy
.context
.evaluated_depsgraph_get()
465 object_eval
= object.evaluated_get(depsgraph
)
466 mesh
= object_eval
.to_mesh()
468 bm
.transform(object.matrix_world
)
470 bvhtree
= BVHTree
.FromBMesh(bm
)
471 object_eval
.to_mesh_clear()
474 def shoot_region_2d_ray(bvhtree
, position_2d
):
475 region
= bpy
.context
.region
476 region_3d
= bpy
.context
.space_data
.region_3d
478 origin
= region_2d_to_origin_3d(region
, region_3d
, position_2d
)
479 direction
= region_2d_to_vector_3d(region
, region_3d
, position_2d
)
481 location
, normal
, index
, distance
= bvhtree
.ray_cast(origin
, direction
)
482 return location
, normal
, index
, distance
484 def scale_matrix(factor
):
485 m
= Matrix
.Identity(4)
491 def event_is_in_region(event
, region
):
492 return (region
.x
<= event
.mouse_x
<= region
.x
+ region
.width
493 and region
.y
<= event
.mouse_y
<= region
.y
+ region
.height
)
495 def get_max_object_side_length(objects
):
497 max(obj
.dimensions
[0] for obj
in objects
),
498 max(obj
.dimensions
[1] for obj
in objects
),
499 max(obj
.dimensions
[2] for obj
in objects
)
502 def get_uniform_color_shader():
503 return gpu
.shader
.from_builtin('3D_UNIFORM_COLOR')
507 ###############################################
510 bpy
.utils
.register_class(ScatterObjects
)
513 bpy
.utils
.unregister_class(ScatterObjects
)