Welcome to mirror list, hosted at ThFree Co, Russian Federation.

operator.py « object_scatter - git.blender.org/blender-addons.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
blob: 8be78672b3bb860661adefb0ac2c512083cd4dc7 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
# 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)