set property to native line ending
[blender-addons.git] / object_grease_scatter.py
blob8e9d6556571dc39c78f06379f3b58ab718ed60c8
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 # <pep8-80 compliant>
21 # Script copyright (C) Campbell Barton
23 bl_info = {
24 "name": "Grease Scatter Objects",
25 "author": "Campbell Barton",
26 "version": (0, 1),
27 "blender": (2, 58, 0),
28 "location": "3D View, Add Mesh",
29 "description": "Scatter a group of objects onto the active mesh using "
30 "the grease pencil lines",
31 "warning": "",
32 "wiki_url": "http://wiki.blender.org/index.php/Extensions:2.6/Py/"
33 "Scripts/Object/Grease_Scatter",
34 "tracker_url": "https://projects.blender.org/tracker/index.php?"
35 "func=detail&aid=TODO",
36 "support": 'OFFICIAL',
37 "category": "Object"}
39 from mathutils import Vector, Matrix, Quaternion
40 from random import uniform, shuffle
41 import bpy
44 def _main(self,
45 obj,
46 group,
47 DENSITY=1.0,
48 SCALE=0.6,
49 RAND_LOC=0.8,
50 RAND_ALIGN=0.75,
53 from math import radians, pi
55 # OFS = 0.2
56 SEEK = 2.0 # distance for ray to seek
57 BAD_NORMAL = Vector((0.0, 0.0, -1.0))
58 WALL_LIMIT = radians(45.0)
60 mats = (Matrix.Rotation(radians(-45), 3, 'X'),
61 Matrix.Rotation(radians(+45), 3, 'X'),
62 Matrix.Rotation(radians(-45), 3, 'Y'),
63 Matrix.Rotation(radians(+45), 3, 'Y'),
64 Matrix.Rotation(radians(-45), 3, 'Z'),
65 Matrix.Rotation(radians(+45), 3, 'Z'),
68 Z_UP = Vector((0.0, 0.0, 1.0))
69 Y_UP = Vector((0.0, 1.0, 0.0))
71 if not group:
72 self.report({'WARNING'}, "Group '%s' not found" % obj.name)
73 return
75 def debug_edge(v1, v2):
76 mesh = bpy.data.meshes.new("Retopo")
77 mesh.from_pydata([v1, v2], [(0.0, 1.0)], [])
79 scene = bpy.context.scene
80 mesh.update()
81 obj_new = bpy.data.objects.new("Torus", mesh)
82 scene.objects.link(obj_new)
84 ray = obj.ray_cast
85 closest_point_on_mesh = obj.closest_point_on_mesh
87 obj_mat = obj.matrix_world.copy()
88 obj_mat_inv = obj_mat.inverted()
89 # obj_quat = obj_mat.to_quaternion()
90 # obj_quat_inv = obj_mat_inv.to_quaternion()
92 DEBUG = False
94 def fix_point(p):
95 hit, no, ind = closest_point_on_mesh(obj_mat_inv * p)
96 if ind != -1:
97 if DEBUG:
98 return [p, no, None]
99 else:
100 # print("good", hit, no)
101 return [hit, no, None]
103 # worry!
104 print("bad!", p, BAD_NORMAL)
106 return [p, BAD_NORMAL, None]
108 def get_points(stroke):
109 return [fix_point(point.co) for point in stroke.points]
111 def get_splines(gp):
112 if gp.layers.active:
113 frame = gp.layers.active.active_frame
114 return [get_points(stroke) for stroke in frame.strokes]
115 else:
116 return []
118 def main():
119 scene = bpy.context.scene
120 obj = bpy.context.object
122 gp = None
124 if obj:
125 gp = obj.grease_pencil
127 if not gp:
128 gp = scene.grease_pencil
130 if not gp:
131 self.report({'WARNING'}, "No grease pencil layer found")
132 return
134 splines = get_splines(gp)
136 for s in splines:
137 for pt in s:
138 p = pt[0]
139 n = pt[1]
140 # print(p, n)
141 if n is BAD_NORMAL:
142 continue
144 # # dont self intersect
145 best_nor = None
146 #best_hit = None
147 best_dist = 10000000.0
148 pofs = p + n * 0.01
150 n_seek = n * SEEK
151 m_alt_1 = Matrix.Rotation(radians(22.5), 3, n)
152 m_alt_2 = Matrix.Rotation(radians(-22.5), 3, n)
153 for _m in mats:
154 for m in (_m, m_alt_1 * _m, m_alt_2 * _m):
155 hit, nor, ind = ray(pofs, pofs + (m * n_seek))
156 if ind != -1:
157 dist = (pofs - hit).length
158 if dist < best_dist:
159 best_dist = dist
160 best_nor = nor
161 #best_hit = hit
163 if best_nor:
164 pt[1].length = best_dist
165 best_nor.negate()
166 pt[2] = best_nor
168 #scene.cursor_location[:] = best_hitnyway
169 # bpy.ops.wm.redraw_timer(type='DRAW_WIN_SWAP',
170 # iterations=1)
171 # debug_edge(p, best_hit)
172 # p[:] = best_hit
174 # Now we need to do scattering.
175 # first corners
176 hits = []
177 nors = []
178 oris = []
179 for s in splines:
180 # point, normal, n_other the closest hit normal
181 for p, n, n_other in s:
182 if n is BAD_NORMAL:
183 continue
184 if n_other:
185 # cast vectors twice as long as the distance
186 # needed just in case.
187 n_down = (n * -SEEK)
188 l = n_down.length
189 n_other.length = l
191 vantage = p + n
192 if DEBUG:
193 p[:] = vantage
195 # We should cast rays between n_down and n_other
196 #for f in (0.0, 0.2, 0.4, 0.6, 0.8, 1.0):
197 TOT = int(10 * DENSITY)
198 #for i in list(range(TOT)):
199 for i in list(range(TOT))[int(TOT / 1.5):]: # second half
200 f = i / (TOT - 1)
202 # focus on the center
204 f -= 0.5
205 f = f*f
206 f += 0.5
209 ntmp = f * n_down + (1.0 - f) * n_other
210 # randomize
211 ntmp.x += uniform(-l, l) * RAND_LOC
212 ntmp.y += uniform(-l, l) * RAND_LOC
213 ntmp.z += uniform(-l, l) * RAND_LOC
215 hit, hit_no, ind = ray(vantage, vantage + ntmp)
216 # print(hit, hit_no)
217 if ind != -1:
218 if hit_no.angle(Z_UP) < WALL_LIMIT:
219 hits.append(hit)
220 nors.append(hit_no)
221 oris.append(n_other.cross(hit_no))
222 #oris.append(n_other)
224 if 0:
225 mesh = bpy.data.meshes.new("ScatterDupliFace")
226 mesh.from_pydata(hits, [], [])
228 scene = bpy.context.scene
229 mesh.update()
230 obj_new = bpy.data.objects.new("ScatterPar", mesh)
231 scene.objects.link(obj_new)
232 obj_new.layers[:] = obj.layers
234 # Now setup dupli-faces
235 obj_new.dupli_type = 'VERTS'
236 ob_child = bpy.data.objects["trash"]
237 ob_child.location = obj_new.location
238 ob_child.parent = obj_new
239 else:
241 def apply_faces(triples):
242 # first randomize the faces
243 shuffle(triples)
245 obs = group.objects[:]
246 tot = len(obs)
247 tot_div = int(len(triples) / tot)
249 for inst_ob in obs:
250 triple_sub = triples[0:tot_div]
251 triples[0:tot_div] = []
253 vv = [tuple(v) for f in triple_sub for v in f]
255 mesh = bpy.data.meshes.new("ScatterDupliFace")
256 mesh.from_pydata(vv, [], [(i * 3, i * 3 + 1, i * 3 + 2)
257 for i in range(len(triple_sub))])
259 scene = bpy.context.scene
260 mesh.update()
261 obj_new = bpy.data.objects.new("ScatterPar", mesh)
263 scene.objects.link(obj_new)
264 obj_new.layers[:] = obj.layers
266 # Now setup dupli-faces
267 obj_new.dupli_type = 'FACES'
268 obj_new.use_dupli_faces_scale = True
269 obj_new.dupli_faces_scale = 100.0
271 inst_ob.location = 0.0, 0.0, 0.0
272 inst_ob.parent = obj_new
274 # align the object with worldspace
275 obj_new.matrix_world = obj_mat
277 # BGE settings for testing
279 inst_ob.game.physics_type = 'RIGID_BODY'
280 inst_ob.game.use_collision_bounds = True
281 inst_ob.game.collision_bounds = 'TRIANGLE_MESH'
282 inst_ob.game.collision_margin = 0.1
283 obj_new.select = True
286 # build faces from vert/normals
287 tri = (Vector((0.0, 0.0, 0.01)),
288 Vector((0.0, 0.0, 0.0)),
289 Vector((0.0, 0.01, 0.01)))
291 coords = []
292 # face_ind = []
293 for i in range(len(hits)):
294 co = hits[i]
295 no = nors[i]
296 ori = oris[i]
297 quat = no.to_track_quat('X', 'Z')
299 # make 2 angles and blend
300 angle = uniform(-pi, pi)
301 angle_aligned = -(ori.angle(quat * Y_UP, pi))
303 quat = Quaternion(no,
304 (angle * (1.0 - RAND_ALIGN)) +
305 (angle_aligned * RAND_ALIGN)
306 ).cross(quat)
308 f = uniform(0.1, 1.2) * SCALE
310 coords.append([co + (quat * (tri[0] * f)),
311 co + (quat * (tri[1] * f)),
312 co + (quat * (tri[2] * f)),
315 apply_faces(coords)
317 main()
320 from bpy.props import FloatProperty, StringProperty
323 class Scatter(bpy.types.Operator):
324 """"""
325 bl_idname = "object.scatter"
326 bl_label = "Grease Pencil Scatter"
328 density = FloatProperty(
329 name="Density",
330 description="Multiplier for the density of items",
331 default=1.0, min=0.01, max=10.0,
333 scale = FloatProperty(
334 name="Scale",
335 description="Size multiplier for duplifaces",
336 default=1.0, min=0.01, max=10.0,
338 rand_align = FloatProperty(
339 name="Random Align",
340 description="Randomize alignment with the walls",
341 default=0.75, min=0.0, max=1.0,
343 rand_loc = FloatProperty(
344 name="Random Loc",
345 description="Randomize placement",
346 default=0.75, min=0.0, max=1.0,
348 # XXX, should not be a string - TODO, add a way for scritps to select ID's
349 group = StringProperty(
350 name="Group",
351 description=("Group name to use for object placement, "
352 "defaults to object name when that matches a group"))
354 def execute(self, context):
355 obj = bpy.context.object
356 group = bpy.data.groups.get(self.group)
358 if not group:
359 self.report({'ERROR'}, "Group %r not found", self.group)
360 return {'CANCELLED'}
362 _main(self,
363 obj,
364 group,
365 DENSITY=self.density,
366 SCALE=self.scale,
367 RAND_LOC=self.rand_loc,
368 RAND_ALIGN=self.rand_align,
370 return {'FINISHED'}
372 def check(self, context):
373 if self.group not in bpy.data.groups:
374 self.group = ""
375 return True
376 return False
378 def invoke(self, context, event):
380 # useful to initialize, take a guess
381 if not self.group and context.object.name in bpy.data.groups:
382 self.group = context.object.name
384 wm = context.window_manager
385 wm.invoke_props_dialog(self, width=180)
386 return {'RUNNING_MODAL'}
389 def menu_func(self, context):
390 self.layout.operator(Scatter.bl_idname, icon='AUTO')
393 def register():
394 bpy.utils.register_class(Scatter)
395 bpy.types.INFO_MT_mesh_add.append(menu_func)
398 def unregister():
399 bpy.utils.unregister_class(Scatter)
400 bpy.types.INFO_MT_mesh_add.remove(menu_func)
402 #if __name__ == "__main__":
403 # _main()