diff options
Diffstat (limited to 'object_grease_scatter.py')
-rw-r--r-- | object_grease_scatter.py | 431 |
1 files changed, 431 insertions, 0 deletions
diff --git a/object_grease_scatter.py b/object_grease_scatter.py new file mode 100644 index 00000000..4d6aeddb --- /dev/null +++ b/object_grease_scatter.py @@ -0,0 +1,431 @@ +# ##### BEGIN GPL LICENSE BLOCK ##### +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# ##### END GPL LICENSE BLOCK ##### + +# <pep8-80 compliant> + +# Script copyright (C) Campbell Barton + +bl_info = { + "name": "Grease Scatter Objects", + "author": "Campbell Barton", + "version": (0, 1), + "blender": (2, 5, 8), + "api": 36079, + "location": "3D View, Toolbar", + "description": ("Scatter a group of objects onto the active mesh using " + "the grease pencil lines"), + "warning": "", + "wiki_url": ("http://wiki.blender.org/index.php/Extensions:2.5/Py/" + "Scripts/Object/Grease_Scatter"), + "tracker_url": ("https://projects.blender.org/tracker/index.php?" + "func=detail&aid=TODO"), + "support": 'OFFICIAL', + "category": "Object"} + +from mathutils import Vector, Matrix, Quaternion +from random import uniform, shuffle +import bpy + + +def _main(self, + obj, + group, + DENSITY=1.0, + SCALE=0.6, + RAND_LOC=0.8, + RAND_ALIGN=0.75, + ): + + from math import radians, pi + + # OFS = 0.2 + SEEK = 2.0 # distance for ray to seek + BAD_NORMAL = Vector((0.0, 0.0, -1.0)) + WALL_LIMIT = radians(45.0) + + mats = (Matrix.Rotation(radians(-45), 3, 'X'), + Matrix.Rotation(radians(+45), 3, 'X'), + Matrix.Rotation(radians(-45), 3, 'Y'), + Matrix.Rotation(radians(+45), 3, 'Y'), + Matrix.Rotation(radians(-45), 3, 'Z'), + Matrix.Rotation(radians(+45), 3, 'Z'), + ) + + Z_UP = Vector((0.0, 0.0, 1.0)) + Y_UP = Vector((0.0, 1.0, 0.0)) + + if not group: + self.report({'WARNING'}, "Group '%s' not found" % obj.name) + return + + def faces_from_hits(hit_list): + def from_pydata(self, verts, edges, faces): + """ + Make a mesh from a list of verts/edges/faces + Until we have a nicer way to make geometry, use this. + """ + self.add_geometry(len(verts), len(edges), len(faces)) + + verts_flat = [f for v in verts for f in v] + self.verts.foreach_set("co", verts_flat) + del verts_flat + + edges_flat = [i for e in edges for i in e] + self.edges.foreach_set("verts", edges_flat) + del edges_flat + + def treat_face(f): + if len(f) == 3: + return f[0], f[1], f[2], 0 + elif f[3] == 0: + return f[3], f[0], f[1], f[2] + return f + + faces_flat = [v for f in faces for v in treat_face(f)] + self.faces.foreach_set("verts_raw", faces_flat) + del faces_flat + + def debug_edge(v1, v2): + mesh = bpy.data.meshes.new("Retopo") + mesh.from_pydata([v1, v2], [(0.0, 1.0)], []) + + scene = bpy.context.scene + mesh.update() + obj_new = bpy.data.objects.new("Torus", mesh) + scene.objects.link(obj_new) + + ray = obj.ray_cast + closest_point_on_mesh = obj.closest_point_on_mesh + + obj_mat = obj.matrix_world.copy() + obj_mat_inv = obj_mat.inverted() + # obj_quat = obj_mat.to_quaternion() + # obj_quat_inv = obj_mat_inv.to_quaternion() + + DEBUG = False + + def fix_point(p): + hit, no, ind = closest_point_on_mesh(obj_mat_inv * p) + if ind != -1: + if DEBUG: + return [p, no, None] + else: + # print("good", hit, no) + return [hit, no, None] + + # worry! + print("bad!", p, BAD_NORMAL) + + return [p, BAD_NORMAL, None] + + def get_points(stroke): + return [fix_point(point.co) for point in stroke.points] + + def get_splines(gp): + if gp.layers.active: + frame = gp.layers.active.active_frame + return [get_points(stroke) for stroke in frame.strokes] + else: + return [] + + def main(): + scene = bpy.context.scene + obj = bpy.context.object + + gp = None + + if obj: + gp = obj.grease_pencil + + if not gp: + gp = scene.grease_pencil + + if not gp: + self.report({'WARNING'}, "No grease pencil layer found") + return + + splines = get_splines(gp) + + for s in splines: + for pt in s: + p = pt[0] + n = pt[1] + # print(p, n) + if n is BAD_NORMAL: + continue + + # # dont self intersect + best_nor = None + #best_hit = None + best_dist = 10000000.0 + pofs = p + n * 0.01 + + n_seek = n * SEEK + m_alt_1 = Matrix.Rotation(radians(22.5), 3, n) + m_alt_2 = Matrix.Rotation(radians(-22.5), 3, n) + for _m in mats: + for m in (_m, m_alt_1 * _m, m_alt_2 * _m): + hit, nor, ind = ray(pofs, pofs + (m * n_seek)) + if ind != -1: + dist = (pofs - hit).length + if dist < best_dist: + best_dist = dist + best_nor = nor + #best_hit = hit + + if best_nor: + pt[1].length = best_dist + best_nor.negate() + pt[2] = best_nor + + #scene.cursor_location[:] = best_hitnyway + # bpy.ops.wm.redraw_timer(type='DRAW_WIN_SWAP', + # iterations=1) + # debug_edge(p, best_hit) + # p[:] = best_hit + + # Now we need to do scattering. + # first corners + hits = [] + nors = [] + oris = [] + for s in splines: + # point, normal, n_other the closest hit normal + for p, n, n_other in s: + if n is BAD_NORMAL: + continue + if n_other: + # cast vectors twice as long as the distance + # needed just incase. + n_down = (n * -SEEK) + l = n_down.length + n_other.length = l + + vantage = p + n + if DEBUG: + p[:] = vantage + + # We should cast rays between n_down and n_other + #for f in (0.0, 0.2, 0.4, 0.6, 0.8, 1.0): + TOT = int(10 * DENSITY) + #for i in list(range(TOT)): + for i in list(range(TOT))[int(TOT / 1.5):]: # second half + f = i / (TOT - 1) + + # focus on the center + ''' + f -= 0.5 + f = f*f + f += 0.5 + ''' + + ntmp = f * n_down + (1.0 - f) * n_other + # randomize + ntmp.x += uniform(-l, l) * RAND_LOC + ntmp.y += uniform(-l, l) * RAND_LOC + ntmp.z += uniform(-l, l) * RAND_LOC + + hit, hit_no, ind = ray(vantage, vantage + ntmp) + # print(hit, hit_no) + if ind != -1: + if hit_no.angle(Z_UP) < WALL_LIMIT: + hits.append(hit) + nors.append(hit_no) + oris.append(n_other.cross(hit_no)) + #oris.append(n_other) + + if 0: + mesh = bpy.data.meshes.new("ScatterDupliFace") + mesh.from_pydata(hits, [], []) + + scene = bpy.context.scene + mesh.update() + obj_new = bpy.data.objects.new("ScatterPar", mesh) + scene.objects.link(obj_new) + obj_new.layers[:] = obj.layers + + # Now setup dupli-faces + obj_new.dupli_type = 'VERTS' + ob_child = bpy.data.objects["trash"] + ob_child.location = obj_new.location + ob_child.parent = obj_new + else: + + def apply_faces(triples): + # first randomize the faces + shuffle(triples) + + obs = group.objects[:] + tot = len(obs) + tot_div = int(len(triples) / tot) + + for inst_ob in obs: + triple_sub = triples[0:tot_div] + triples[0:tot_div] = [] + + vv = [tuple(v) for f in triple_sub for v in f] + + mesh = bpy.data.meshes.new("ScatterDupliFace") + mesh.from_pydata(vv, [], [(i * 3, i * 3 + 1, i * 3 + 2) + for i in range(len(triple_sub))]) + + scene = bpy.context.scene + mesh.update() + obj_new = bpy.data.objects.new("ScatterPar", mesh) + + scene.objects.link(obj_new) + obj_new.layers[:] = obj.layers + + # Now setup dupli-faces + obj_new.dupli_type = 'FACES' + obj_new.use_dupli_faces_scale = True + obj_new.dupli_faces_scale = 100.0 + + inst_ob.location = 0.0, 0.0, 0.0 + inst_ob.parent = obj_new + + # align the object with worldspace + obj_new.matrix_world = obj_mat + + # BGE settings for testiing + ''' + inst_ob.game.physics_type = 'RIGID_BODY' + inst_ob.game.use_collision_bounds = True + inst_ob.game.collision_bounds = 'TRIANGLE_MESH' + inst_ob.game.collision_margin = 0.1 + obj_new.select = True + ''' + + # build faces from vert/normals + tri = (Vector((0.0, 0.0, 0.01)), + Vector((0.0, 0.0, 0.0)), + Vector((0.0, 0.01, 0.01))) + + coords = [] + # face_ind = [] + for i in range(len(hits)): + co = hits[i] + no = nors[i] + ori = oris[i] + quat = no.to_track_quat('X', 'Z') + + # make 2 angles and blend + angle = uniform(-pi, pi) + angle_aligned = -(ori.angle(quat * Y_UP, pi)) + + quat = Quaternion(no, + (angle * (1.0 - RAND_ALIGN)) + + (angle_aligned * RAND_ALIGN) + ).cross(quat) + + f = uniform(0.1, 1.2) * SCALE + + coords.append([co + (quat * (tri[0] * f)), + co + (quat * (tri[1] * f)), + co + (quat * (tri[2] * f)), + ]) + + apply_faces(coords) + + main() + + +from bpy.props import FloatProperty, StringProperty + + +class Scatter(bpy.types.Operator): + '''''' + bl_idname = "object.scatter" + bl_label = "Grease Pencil Scatter" + + density = FloatProperty( + name="Density", + description="Multiplier for the density of items", + default=1.0, min=0.01, max=10.0, + ) + scale = FloatProperty( + name="Scale", + description="Size multiplier for duplifaces", + default=1.0, min=0.01, max=10.0, + ) + rand_align = FloatProperty( + name="Random Align", + description="Randomize alignmet with the walls", + default=0.75, min=0.0, max=1.0, + ) + rand_loc = FloatProperty( + name="Random Loc", + description="Randomize Placement", + default=0.75, min=0.0, max=1.0, + ) + # XXX, should not be a string - TODO, add a way for scritps to select ID's + group = StringProperty( + name="Group", + description=("Group name to use for object placement, " + "defaults to object name when that matches a group")) + + def execute(self, context): + obj = bpy.context.object + group = bpy.data.groups.get(self.group) + + if not group: + self.report({'ERROR'}, "Group %r not found", self.group) + return {'CANCELLED'} + + _main(self, + obj, + group, + DENSITY=self.density, + SCALE=self.scale, + RAND_LOC=self.rand_loc, + RAND_ALIGN=self.rand_align, + ) + return {'FINISHED'} + + def check(self, context): + if self.group not in bpy.data.groups: + self.group = "" + return True + return False + + def invoke(self, context, event): + + # useful to initialize, take a guess + if not self.group and context.object.name in bpy.data.groups: + self.group = context.object.name + + wm = context.window_manager + wm.invoke_props_dialog(self, width=180) + return {'RUNNING_MODAL'} + + +def menu_func(self, context): + self.layout.operator(Scatter.bl_idname, icon='AUTO') + + +def register(): + bpy.utils.register_class(Scatter) + bpy.types.INFO_MT_mesh_add.append(menu_func) + + +def unregister(): + bpy.utils.unregister_class(Scatter) + bpy.types.INFO_MT_mesh_add.remove(menu_func) + +#if __name__ == "__main__": +# _main() |