animation_animall: remove workaround for T68666
[blender-addons.git] / object_scatter / operator.py
blob5294d173f131f3a8650a0472cdd6e08394820b1a
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 #####
19 import bpy
20 import gpu
21 import bgl
22 import blf
23 import math
24 import enum
25 import random
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
38 # Modal Operator
39 ################################################################
41 class ScatterObjects(bpy.types.Operator):
42 bl_idname = "object.scatter"
43 bl_label = "Scatter Objects"
44 bl_options = {'REGISTER', 'UNDO'}
46 @classmethod
47 def poll(cls, context):
48 return (
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.")
59 return {'CANCELLED'}
61 self.base_scale = get_max_object_side_length(self.objects_to_scatter)
63 self.targets = []
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)
85 if event_used:
86 return {'RUNNING_MODAL'}
87 else:
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)
95 return True
96 else:
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)
102 return True
104 return False
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')
114 def draw_view(self):
115 for target in self.iter_targets():
116 target.draw()
118 draw_matrices_batches(list(self.iter_matrix_batches()))
120 def draw_px(self):
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()
141 matrices = []
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))
145 return matrices
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):
171 self.target = target
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)
180 return self.matrices
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):
194 self.matrices = None
195 self.gpu_batch = None
198 # Duplicator Creation
199 ######################################################
201 def make_duplicator(target_collection, source_object, matrices):
202 triangle_scale = 0.1
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)
227 mesh.update()
228 mesh.validate()
229 return mesh
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):
237 vertices = []
238 polygons = []
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
248 # Target Provider
249 #################################################
251 class BuildState(enum.Enum):
252 FINISHED = enum.auto()
253 ONGOING = enum.auto()
255 class TargetProvider:
256 def start_build(self, target_object):
257 pass
259 def continue_build(self, event):
260 return BuildState.FINISHED
262 def get_matrices(self, scatter_settings):
263 return []
265 def draw(self):
266 pass
268 class StrokeTarget(TargetProvider):
269 def start_build(self, target_object):
270 self.points = []
271 self.bvhtree = bvhtree_from_object(target_object)
272 self.batch = None
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)
282 self.batch = None
283 return BuildState.ONGOING
285 def draw(self):
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
319 normal.normalize()
321 # Scale
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)
326 # Location
327 location += normal * settings.normal_offset * scale
329 # Rotation
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)
338 # Drawing
339 #################################################
341 box_vertices = (
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))
345 box_indices = (
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()
354 shader.bind()
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:
361 batch.draw(shader)
363 bgl.glDisable(bgl.GL_BLEND)
364 bgl.glDepthMask(bgl.GL_TRUE)
366 def create_batch_for_matrices(matrices, base_scale):
367 coords = []
368 indices = []
370 scaled_box_vertices = [base_scale * vertex for vertex in box_vertices]
372 for matrix in matrices:
373 offset = len(coords)
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)
379 return batch
382 def draw_line_strip_batch(batch, color, thickness=1):
383 shader = get_uniform_color_shader()
384 bgl.glLineWidth(thickness)
385 shader.bind()
386 shader.uniform_float("color", color)
387 batch.draw(shader)
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)):
394 font_id = 0
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)
401 # Utilities
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)
419 else:
420 return math.ceil(value)
422 def random_vector(x, min=-1, max=1):
423 return Vector((
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
434 def random_int(x):
435 x = (x<<13) ^ x
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):
461 import bmesh
462 bm = bmesh.new()
464 depsgraph = bpy.context.evaluated_depsgraph_get()
465 object_eval = object.evaluated_get(depsgraph)
466 mesh = object_eval.to_mesh()
467 bm.from_mesh(mesh)
468 bm.transform(object.matrix_world)
470 bvhtree = BVHTree.FromBMesh(bm)
471 object_eval.to_mesh_clear()
472 return bvhtree
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)
486 m[0][0] = factor
487 m[1][1] = factor
488 m[2][2] = factor
489 return m
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):
496 return max(
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')
506 # Registration
507 ###############################################
509 def register():
510 bpy.utils.register_class(ScatterObjects)
512 def unregister():
513 bpy.utils.unregister_class(ScatterObjects)