Cleanup: simplify file name incrementing logic
[blender-addons.git] / object_scatter / operator.py
blobc1cf0e13a235a81176dc368cecbfe510900b7fd5
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 up_direction = normal if settings.use_normal_rotation else Vector((0, 0, 1))
323 # Scale
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)
328 # Location
329 location += normal * settings.normal_offset * scale
331 # Rotation
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)
340 # Drawing
341 #################################################
343 box_vertices = (
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))
347 box_indices = (
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()
356 shader.bind()
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:
363 batch.draw(shader)
365 bgl.glDisable(bgl.GL_BLEND)
366 bgl.glDepthMask(bgl.GL_TRUE)
368 def create_batch_for_matrices(matrices, base_scale):
369 coords = []
370 indices = []
372 scaled_box_vertices = [base_scale * vertex for vertex in box_vertices]
374 for matrix in matrices:
375 offset = len(coords)
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)
381 return batch
384 def draw_line_strip_batch(batch, color, thickness=1):
385 shader = get_uniform_color_shader()
386 bgl.glLineWidth(thickness)
387 shader.bind()
388 shader.uniform_float("color", color)
389 batch.draw(shader)
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)):
396 font_id = 0
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)
403 # Utilities
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)
421 else:
422 return math.ceil(value)
424 def random_vector(x, min=-1, max=1):
425 return Vector((
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
436 def random_int(x):
437 x = (x<<13) ^ x
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):
463 import bmesh
464 bm = bmesh.new()
466 depsgraph = bpy.context.evaluated_depsgraph_get()
467 object_eval = object.evaluated_get(depsgraph)
468 mesh = object_eval.to_mesh()
469 bm.from_mesh(mesh)
470 bm.transform(object.matrix_world)
472 bvhtree = BVHTree.FromBMesh(bm)
473 object_eval.to_mesh_clear()
474 return bvhtree
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)
488 m[0][0] = factor
489 m[1][1] = factor
490 m[2][2] = factor
491 return m
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):
498 return max(
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')
508 # Registration
509 ###############################################
511 def register():
512 bpy.utils.register_class(ScatterObjects)
514 def unregister():
515 bpy.utils.unregister_class(ScatterObjects)