mirror of
https://github.com/blender/blender-addons.git
synced 2025-08-01 16:06:15 +00:00
499 lines
16 KiB
Python
499 lines
16 KiB
Python
# SPDX-License-Identifier: GPL-2.0-or-later
|
|
|
|
import bpy
|
|
import gpu
|
|
import blf
|
|
import math
|
|
import enum
|
|
import random
|
|
|
|
from itertools import islice
|
|
from mathutils.bvhtree import BVHTree
|
|
from mathutils import Vector, Matrix, Euler
|
|
from gpu_extras.batch import batch_for_shader
|
|
|
|
from bpy_extras.view3d_utils import (
|
|
region_2d_to_vector_3d,
|
|
region_2d_to_origin_3d
|
|
)
|
|
|
|
|
|
# Modal Operator
|
|
################################################################
|
|
|
|
class ScatterObjects(bpy.types.Operator):
|
|
bl_idname = "object.scatter"
|
|
bl_label = "Scatter Objects"
|
|
bl_options = {'REGISTER', 'UNDO'}
|
|
|
|
@classmethod
|
|
def poll(cls, context):
|
|
return (
|
|
currently_in_3d_view(context)
|
|
and context.active_object is not None
|
|
and context.active_object.mode == 'OBJECT')
|
|
|
|
def invoke(self, context, event):
|
|
self.target_object = context.active_object
|
|
self.objects_to_scatter = get_selected_non_active_objects(context)
|
|
|
|
if self.target_object is None or len(self.objects_to_scatter) == 0:
|
|
self.report({'ERROR'}, "Select objects to scatter and a target object")
|
|
return {'CANCELLED'}
|
|
|
|
self.base_scale = get_max_object_side_length(self.objects_to_scatter)
|
|
|
|
self.targets = []
|
|
self.active_target = None
|
|
self.target_cache = {}
|
|
|
|
self.enable_draw_callback()
|
|
context.window_manager.modal_handler_add(self)
|
|
return {'RUNNING_MODAL'}
|
|
|
|
def modal(self, context, event):
|
|
context.area.tag_redraw()
|
|
|
|
if not event_is_in_region(event, context.region) and self.active_target is None:
|
|
return {'PASS_THROUGH'}
|
|
|
|
if event.type == 'ESC':
|
|
return self.finish('CANCELLED')
|
|
|
|
if event.type == 'RET' and event.value == 'PRESS':
|
|
self.create_scatter_object()
|
|
return self.finish('FINISHED')
|
|
|
|
event_used = self.handle_non_exit_event(event)
|
|
if event_used:
|
|
return {'RUNNING_MODAL'}
|
|
else:
|
|
return {'PASS_THROUGH'}
|
|
|
|
def handle_non_exit_event(self, event):
|
|
if self.active_target is None:
|
|
if event.type == 'LEFTMOUSE' and event.value == 'PRESS':
|
|
self.active_target = StrokeTarget()
|
|
self.active_target.start_build(self.target_object)
|
|
return True
|
|
else:
|
|
build_state = self.active_target.continue_build(event)
|
|
if build_state == BuildState.FINISHED:
|
|
self.targets.append(self.active_target)
|
|
self.active_target = None
|
|
self.remove_target_from_cache(self.active_target)
|
|
return True
|
|
|
|
return False
|
|
|
|
def enable_draw_callback(self):
|
|
self._draw_callback_view = bpy.types.SpaceView3D.draw_handler_add(self.draw_view, (), 'WINDOW', 'POST_VIEW')
|
|
self._draw_callback_px = bpy.types.SpaceView3D.draw_handler_add(self.draw_px, (), 'WINDOW', 'POST_PIXEL')
|
|
|
|
def disable_draw_callback(self):
|
|
bpy.types.SpaceView3D.draw_handler_remove(self._draw_callback_view, 'WINDOW')
|
|
bpy.types.SpaceView3D.draw_handler_remove(self._draw_callback_px, 'WINDOW')
|
|
|
|
def draw_view(self):
|
|
for target in self.iter_targets():
|
|
target.draw()
|
|
|
|
draw_matrices_batches(list(self.iter_matrix_batches()))
|
|
|
|
def draw_px(self):
|
|
draw_text((20, 20, 0), "Instances: " + str(len(self.get_all_matrices())))
|
|
|
|
def finish(self, return_value):
|
|
self.disable_draw_callback()
|
|
bpy.context.area.tag_redraw()
|
|
return {return_value}
|
|
|
|
def create_scatter_object(self):
|
|
matrix_chunks = make_random_chunks(
|
|
self.get_all_matrices(), len(self.objects_to_scatter))
|
|
|
|
collection = bpy.data.collections.new("Scatter")
|
|
bpy.context.collection.children.link(collection)
|
|
|
|
for obj, matrices in zip(self.objects_to_scatter, matrix_chunks):
|
|
make_duplicator(collection, obj, matrices)
|
|
|
|
def get_all_matrices(self):
|
|
settings = self.get_current_settings()
|
|
|
|
matrices = []
|
|
for target in self.iter_targets():
|
|
self.ensure_target_is_in_cache(target)
|
|
matrices.extend(self.target_cache[target].get_matrices(settings))
|
|
return matrices
|
|
|
|
def iter_matrix_batches(self):
|
|
settings = self.get_current_settings()
|
|
for target in self.iter_targets():
|
|
self.ensure_target_is_in_cache(target)
|
|
yield self.target_cache[target].get_batch(settings)
|
|
|
|
def iter_targets(self):
|
|
yield from self.targets
|
|
if self.active_target is not None:
|
|
yield self.active_target
|
|
|
|
def ensure_target_is_in_cache(self, target):
|
|
if target not in self.target_cache:
|
|
entry = TargetCacheEntry(target, self.base_scale)
|
|
self.target_cache[target] = entry
|
|
|
|
def remove_target_from_cache(self, target):
|
|
self.target_cache.pop(self.active_target, None)
|
|
|
|
def get_current_settings(self):
|
|
return bpy.context.scene.scatter_properties.to_settings()
|
|
|
|
class TargetCacheEntry:
|
|
def __init__(self, target, base_scale):
|
|
self.target = target
|
|
self.last_used_settings = None
|
|
self.base_scale = base_scale
|
|
self.settings_changed()
|
|
|
|
def get_matrices(self, settings):
|
|
self._handle_new_settings(settings)
|
|
if self.matrices is None:
|
|
self.matrices = self.target.get_matrices(settings)
|
|
return self.matrices
|
|
|
|
def get_batch(self, settings):
|
|
self._handle_new_settings(settings)
|
|
if self.gpu_batch is None:
|
|
self.gpu_batch = create_batch_for_matrices(self.get_matrices(settings), self.base_scale)
|
|
return self.gpu_batch
|
|
|
|
def _handle_new_settings(self, settings):
|
|
if settings != self.last_used_settings:
|
|
self.settings_changed()
|
|
self.last_used_settings = settings
|
|
|
|
def settings_changed(self):
|
|
self.matrices = None
|
|
self.gpu_batch = None
|
|
|
|
|
|
# Duplicator Creation
|
|
######################################################
|
|
|
|
def make_duplicator(target_collection, source_object, matrices):
|
|
triangle_scale = 0.1
|
|
|
|
duplicator = triangle_object_from_matrices(source_object.name + " Duplicator", matrices, triangle_scale)
|
|
duplicator.instance_type = 'FACES'
|
|
duplicator.use_instance_faces_scale = True
|
|
duplicator.show_instancer_for_viewport = True
|
|
duplicator.show_instancer_for_render = False
|
|
duplicator.instance_faces_scale = 1 / triangle_scale
|
|
|
|
copy_obj = source_object.copy()
|
|
copy_obj.name = source_object.name + " - copy"
|
|
copy_obj.location = (0, 0, 0)
|
|
copy_obj.parent = duplicator
|
|
|
|
target_collection.objects.link(duplicator)
|
|
target_collection.objects.link(copy_obj)
|
|
|
|
def triangle_object_from_matrices(name, matrices, triangle_scale):
|
|
mesh = triangle_mesh_from_matrices(name, matrices, triangle_scale)
|
|
return bpy.data.objects.new(name, mesh)
|
|
|
|
def triangle_mesh_from_matrices(name, matrices, triangle_scale):
|
|
mesh = bpy.data.meshes.new(name)
|
|
vertices, polygons = mesh_data_from_matrices(matrices, triangle_scale)
|
|
mesh.from_pydata(vertices, [], polygons)
|
|
mesh.update()
|
|
mesh.validate()
|
|
return mesh
|
|
|
|
unit_triangle_vertices = (
|
|
Vector((-3**-0.25, -3**-0.75, 0)),
|
|
Vector((3**-0.25, -3**-0.75, 0)),
|
|
Vector((0, 2/3**0.75, 0)))
|
|
|
|
def mesh_data_from_matrices(matrices, triangle_scale):
|
|
vertices = []
|
|
polygons = []
|
|
triangle_vertices = [triangle_scale * v for v in unit_triangle_vertices]
|
|
|
|
for i, matrix in enumerate(matrices):
|
|
vertices.extend((matrix @ v for v in triangle_vertices))
|
|
polygons.append((i * 3 + 0, i * 3 + 1, i * 3 + 2))
|
|
|
|
return vertices, polygons
|
|
|
|
|
|
# Target Provider
|
|
#################################################
|
|
|
|
class BuildState(enum.Enum):
|
|
FINISHED = enum.auto()
|
|
ONGOING = enum.auto()
|
|
|
|
class TargetProvider:
|
|
def start_build(self, target_object):
|
|
pass
|
|
|
|
def continue_build(self, event):
|
|
return BuildState.FINISHED
|
|
|
|
def get_matrices(self, scatter_settings):
|
|
return []
|
|
|
|
def draw(self):
|
|
pass
|
|
|
|
class StrokeTarget(TargetProvider):
|
|
def start_build(self, target_object):
|
|
self.points = []
|
|
self.bvhtree = bvhtree_from_object(target_object)
|
|
self.batch = None
|
|
|
|
def continue_build(self, event):
|
|
if event.type == 'LEFTMOUSE' and event.value == 'RELEASE':
|
|
return BuildState.FINISHED
|
|
|
|
mouse_pos = (event.mouse_region_x, event.mouse_region_y)
|
|
location, *_ = shoot_region_2d_ray(self.bvhtree, mouse_pos)
|
|
if location is not None:
|
|
self.points.append(location)
|
|
self.batch = None
|
|
return BuildState.ONGOING
|
|
|
|
def draw(self):
|
|
if self.batch is None:
|
|
self.batch = create_line_strip_batch(self.points)
|
|
draw_line_strip_batch(self.batch, color=(1.0, 0.4, 0.1, 1.0), thickness=5)
|
|
|
|
def get_matrices(self, scatter_settings):
|
|
return scatter_around_stroke(self.points, self.bvhtree, scatter_settings)
|
|
|
|
def scatter_around_stroke(stroke_points, bvhtree, settings):
|
|
scattered_matrices = []
|
|
for point, local_seed in iter_points_on_stroke_with_seed(stroke_points, settings.density, settings.seed):
|
|
matrix = scatter_from_source_point(bvhtree, point, local_seed, settings)
|
|
scattered_matrices.append(matrix)
|
|
return scattered_matrices
|
|
|
|
def iter_points_on_stroke_with_seed(stroke_points, density, seed):
|
|
for i, (start, end) in enumerate(iter_pairwise(stroke_points)):
|
|
segment_seed = sub_seed(seed, i)
|
|
segment_vector = end - start
|
|
|
|
segment_length = segment_vector.length
|
|
amount = round_random(segment_length * density, segment_seed)
|
|
|
|
for j in range(amount):
|
|
t = random_uniform(sub_seed(segment_seed, j, 0))
|
|
origin = start + t * segment_vector
|
|
yield origin, sub_seed(segment_seed, j, 1)
|
|
|
|
def scatter_from_source_point(bvhtree, point, seed, settings):
|
|
# Project displaced point on surface
|
|
radius = random_uniform(sub_seed(seed, 0)) * settings.radius
|
|
offset = random_vector(sub_seed(seed, 2)) * radius
|
|
location, normal, *_ = bvhtree.find_nearest(point + offset)
|
|
assert location is not None
|
|
normal.normalize()
|
|
|
|
up_direction = normal if settings.use_normal_rotation else Vector((0, 0, 1))
|
|
|
|
# Scale
|
|
min_scale = settings.scale * (1 - settings.random_scale)
|
|
max_scale = settings.scale
|
|
scale = random_uniform(sub_seed(seed, 1), min_scale, max_scale)
|
|
|
|
# Location
|
|
location += normal * settings.normal_offset * scale
|
|
|
|
# Rotation
|
|
z_rotation = Euler((0, 0, random_uniform(sub_seed(seed, 3), 0, 2 * math.pi))).to_matrix()
|
|
up_rotation = up_direction.to_track_quat('Z', 'X').to_matrix()
|
|
local_rotation = random_euler(sub_seed(seed, 3), settings.rotation).to_matrix()
|
|
rotation = local_rotation @ up_rotation @ z_rotation
|
|
|
|
return Matrix.Translation(location) @ rotation.to_4x4() @ scale_matrix(scale)
|
|
|
|
|
|
# Drawing
|
|
#################################################
|
|
|
|
box_vertices = (
|
|
(-1, -1, 1), ( 1, -1, 1), ( 1, 1, 1), (-1, 1, 1),
|
|
(-1, -1, -1), ( 1, -1, -1), ( 1, 1, -1), (-1, 1, -1))
|
|
|
|
box_indices = (
|
|
(0, 1, 2), (2, 3, 0), (1, 5, 6), (6, 2, 1),
|
|
(7, 6, 5), (5, 4, 7), (4, 0, 3), (3, 7, 4),
|
|
(4, 5, 1), (1, 0, 4), (3, 2, 6), (6, 7, 3))
|
|
|
|
box_vertices = tuple(Vector(vertex) * 0.5 for vertex in box_vertices)
|
|
|
|
def draw_matrices_batches(batches):
|
|
shader = get_uniform_color_shader()
|
|
shader.bind()
|
|
shader.uniform_float("color", (0.4, 0.4, 1.0, 0.3))
|
|
|
|
gpu.state.blend_set('ALPHA')
|
|
gpu.state.depth_mask_set(False)
|
|
|
|
for batch in batches:
|
|
batch.draw(shader)
|
|
|
|
gpu.state.blend_set('NONE')
|
|
gpu.state.depth_mask_set(True)
|
|
|
|
def create_batch_for_matrices(matrices, base_scale):
|
|
coords = []
|
|
indices = []
|
|
|
|
scaled_box_vertices = [base_scale * vertex for vertex in box_vertices]
|
|
|
|
for matrix in matrices:
|
|
offset = len(coords)
|
|
coords.extend((matrix @ vertex for vertex in scaled_box_vertices))
|
|
indices.extend(tuple(index + offset for index in element) for element in box_indices)
|
|
|
|
batch = batch_for_shader(get_uniform_color_shader(),
|
|
'TRIS', {"pos" : coords}, indices = indices)
|
|
return batch
|
|
|
|
|
|
def draw_line_strip_batch(batch, color, thickness=1):
|
|
shader = get_uniform_color_shader()
|
|
gpu.state.line_width_set(thickness)
|
|
shader.bind()
|
|
shader.uniform_float("color", color)
|
|
batch.draw(shader)
|
|
|
|
def create_line_strip_batch(coords):
|
|
return batch_for_shader(get_uniform_color_shader(), 'LINE_STRIP', {"pos" : coords})
|
|
|
|
|
|
def draw_text(location, text, size=15, color=(1, 1, 1, 1)):
|
|
font_id = 0
|
|
ui_scale = bpy.context.preferences.system.ui_scale
|
|
blf.position(font_id, *location)
|
|
blf.size(font_id, round(size * ui_scale), 72)
|
|
blf.draw(font_id, text)
|
|
|
|
|
|
# Utilities
|
|
########################################################
|
|
|
|
'''
|
|
Pythons random functions are designed to be used in cases
|
|
when a seed is set once and then many random numbers are
|
|
generated. To improve the user experience I want to have
|
|
full control over how random seeds propagate through the
|
|
functions. This is why I use custom random functions.
|
|
|
|
One benefit is that changing the object density does not
|
|
generate new random positions for all objects.
|
|
'''
|
|
|
|
def round_random(value, seed):
|
|
probability = value % 1
|
|
if probability < random_uniform(seed):
|
|
return math.floor(value)
|
|
else:
|
|
return math.ceil(value)
|
|
|
|
def random_vector(x, min=-1, max=1):
|
|
return Vector((
|
|
random_uniform(sub_seed(x, 0), min, max),
|
|
random_uniform(sub_seed(x, 1), min, max),
|
|
random_uniform(sub_seed(x, 2), min, max)))
|
|
|
|
def random_euler(x, factor):
|
|
return Euler(tuple(random_vector(x) * factor))
|
|
|
|
def random_uniform(x, min=0, max=1):
|
|
return random_int(x) / 2147483648 * (max - min) + min
|
|
|
|
def random_int(x):
|
|
x = (x<<13) ^ x
|
|
return (x * (x * x * 15731 + 789221) + 1376312589) & 0x7fffffff
|
|
|
|
def sub_seed(seed, index, index2=0):
|
|
return random_int(seed * 3243 + index * 5643 + index2 * 54243)
|
|
|
|
|
|
def currently_in_3d_view(context):
|
|
return context.space_data.type == 'VIEW_3D'
|
|
|
|
def get_selected_non_active_objects(context):
|
|
return set(context.selected_objects) - {context.active_object}
|
|
|
|
def make_random_chunks(sequence, chunk_amount):
|
|
sequence = list(sequence)
|
|
random.shuffle(sequence)
|
|
return make_chunks(sequence, chunk_amount)
|
|
|
|
def make_chunks(sequence, chunk_amount):
|
|
length = math.ceil(len(sequence) / chunk_amount)
|
|
return [sequence[i:i+length] for i in range(0, len(sequence), length)]
|
|
|
|
def iter_pairwise(sequence):
|
|
return zip(sequence, islice(sequence, 1, None))
|
|
|
|
def bvhtree_from_object(object):
|
|
import bmesh
|
|
bm = bmesh.new()
|
|
|
|
depsgraph = bpy.context.evaluated_depsgraph_get()
|
|
object_eval = object.evaluated_get(depsgraph)
|
|
mesh = object_eval.to_mesh()
|
|
bm.from_mesh(mesh)
|
|
bm.transform(object.matrix_world)
|
|
|
|
bvhtree = BVHTree.FromBMesh(bm)
|
|
object_eval.to_mesh_clear()
|
|
return bvhtree
|
|
|
|
def shoot_region_2d_ray(bvhtree, position_2d):
|
|
region = bpy.context.region
|
|
region_3d = bpy.context.space_data.region_3d
|
|
|
|
origin = region_2d_to_origin_3d(region, region_3d, position_2d)
|
|
direction = region_2d_to_vector_3d(region, region_3d, position_2d)
|
|
|
|
location, normal, index, distance = bvhtree.ray_cast(origin, direction)
|
|
return location, normal, index, distance
|
|
|
|
def scale_matrix(factor):
|
|
m = Matrix.Identity(4)
|
|
m[0][0] = factor
|
|
m[1][1] = factor
|
|
m[2][2] = factor
|
|
return m
|
|
|
|
def event_is_in_region(event, region):
|
|
return (region.x <= event.mouse_x <= region.x + region.width
|
|
and region.y <= event.mouse_y <= region.y + region.height)
|
|
|
|
def get_max_object_side_length(objects):
|
|
return max(
|
|
max(obj.dimensions[0] for obj in objects),
|
|
max(obj.dimensions[1] for obj in objects),
|
|
max(obj.dimensions[2] for obj in objects)
|
|
)
|
|
|
|
def get_uniform_color_shader():
|
|
return gpu.shader.from_builtin('3D_UNIFORM_COLOR')
|
|
|
|
|
|
# Registration
|
|
###############################################
|
|
|
|
def register():
|
|
bpy.utils.register_class(ScatterObjects)
|
|
|
|
def unregister():
|
|
bpy.utils.unregister_class(ScatterObjects)
|