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 #####
21 # Script copyright (C) Campbell Barton
24 "name": "Grease Scatter Objects",
25 "author": "Campbell Barton",
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",
32 "wiki_url": "http://wiki.blender.org/index.php/Extensions:2.6/Py/"
33 "Scripts/Object/Grease_Scatter",
34 "support": 'OFFICIAL',
38 from mathutils
import Vector
, Matrix
, Quaternion
39 from random
import uniform
, shuffle
52 from math
import radians
, pi
55 SEEK
= 2.0 # distance for ray to seek
56 BAD_NORMAL
= Vector((0.0, 0.0, -1.0))
57 WALL_LIMIT
= radians(45.0)
59 mats
= (Matrix
.Rotation(radians(-45), 3, 'X'),
60 Matrix
.Rotation(radians(+45), 3, 'X'),
61 Matrix
.Rotation(radians(-45), 3, 'Y'),
62 Matrix
.Rotation(radians(+45), 3, 'Y'),
63 Matrix
.Rotation(radians(-45), 3, 'Z'),
64 Matrix
.Rotation(radians(+45), 3, 'Z'),
67 Z_UP
= Vector((0.0, 0.0, 1.0))
68 Y_UP
= Vector((0.0, 1.0, 0.0))
71 self
.report({'WARNING'}, "Group '%s' not found" % obj
.name
)
74 def debug_edge(v1
, v2
):
75 mesh
= bpy
.data
.meshes
.new("Retopo")
76 mesh
.from_pydata([v1
, v2
], [(0.0, 1.0)], [])
78 scene
= bpy
.context
.scene
80 obj_new
= bpy
.data
.objects
.new("Torus", mesh
)
81 scene
.objects
.link(obj_new
)
84 closest_point_on_mesh
= obj
.closest_point_on_mesh
86 obj_mat
= obj
.matrix_world
.copy()
87 obj_mat_inv
= obj_mat
.inverted()
88 # obj_quat = obj_mat.to_quaternion()
89 # obj_quat_inv = obj_mat_inv.to_quaternion()
94 ok
, hit
, no
, ind
= closest_point_on_mesh(obj_mat_inv
* p
)
99 # print("good", hit, no)
100 return [hit
, no
, None]
103 print("bad!", p
, BAD_NORMAL
)
105 return [p
, BAD_NORMAL
, None]
107 def get_points(stroke
):
108 return [fix_point(point
.co
) for point
in stroke
.points
]
112 frame
= gp
.layers
.active
.active_frame
113 return [get_points(stroke
) for stroke
in frame
.strokes
]
118 scene
= bpy
.context
.scene
119 obj
= bpy
.context
.object
124 gp
= obj
.grease_pencil
127 gp
= scene
.grease_pencil
130 self
.report({'WARNING'}, "No grease pencil layer found")
133 splines
= get_splines(gp
)
143 # # dont self intersect
146 best_dist
= 10000000.0
150 m_alt_1
= Matrix
.Rotation(radians(22.5), 3, n
)
151 m_alt_2
= Matrix
.Rotation(radians(-22.5), 3, n
)
153 for m
in (_m
, m_alt_1
* _m
, m_alt_2
* _m
):
155 ok
, hit
, nor
, ind
= ray(pofs
, pdir
, best_dist
)
157 best_dist
= (pofs
- hit
).length
162 pt
[1].length
= best_dist
166 #scene.cursor_location[:] = best_hitnyway
167 # bpy.ops.wm.redraw_timer(type='DRAW_WIN_SWAP',
169 # debug_edge(p, best_hit)
172 # Now we need to do scattering.
178 # point, normal, n_other the closest hit normal
179 for p
, n
, n_other
in s
:
183 # cast vectors twice as long as the distance
184 # needed just in case.
193 # We should cast rays between n_down and n_other
194 #for f in (0.0, 0.2, 0.4, 0.6, 0.8, 1.0):
195 TOT
= int(10 * DENSITY
)
196 #for i in list(range(TOT)):
197 for i
in list(range(TOT
))[int(TOT
/ 1.5):]: # second half
200 # focus on the center
207 ntmp
= f
* n_down
+ (1.0 - f
) * n_other
209 ntmp
.x
+= uniform(-l
, l
) * RAND_LOC
210 ntmp
.y
+= uniform(-l
, l
) * RAND_LOC
211 ntmp
.z
+= uniform(-l
, l
) * RAND_LOC
213 ok
, hit
, hit_no
, ind
= ray(vantage
, ntmp
, ntmp
.length
)
216 if hit_no
.angle(Z_UP
) < WALL_LIMIT
:
219 oris
.append(n_other
.cross(hit_no
))
220 #oris.append(n_other)
223 mesh
= bpy
.data
.meshes
.new("ScatterDupliFace")
224 mesh
.from_pydata(hits
, [], [])
226 scene
= bpy
.context
.scene
228 obj_new
= bpy
.data
.objects
.new("ScatterPar", mesh
)
229 scene
.objects
.link(obj_new
)
230 obj_new
.layers
[:] = obj
.layers
232 # Now setup dupli-faces
233 obj_new
.dupli_type
= 'VERTS'
234 ob_child
= bpy
.data
.objects
["trash"]
235 ob_child
.location
= obj_new
.location
236 ob_child
.parent
= obj_new
239 def apply_faces(triples
):
240 # first randomize the faces
243 obs
= group
.objects
[:]
245 tot_div
= int(len(triples
) / tot
)
248 triple_sub
= triples
[0:tot_div
]
249 triples
[0:tot_div
] = []
251 vv
= [tuple(v
) for f
in triple_sub
for v
in f
]
253 mesh
= bpy
.data
.meshes
.new("ScatterDupliFace")
254 mesh
.from_pydata(vv
, [], [(i
* 3, i
* 3 + 1, i
* 3 + 2)
255 for i
in range(len(triple_sub
))])
257 scene
= bpy
.context
.scene
259 obj_new
= bpy
.data
.objects
.new("ScatterPar", mesh
)
261 scene
.objects
.link(obj_new
)
262 obj_new
.layers
[:] = obj
.layers
264 # Now setup dupli-faces
265 obj_new
.dupli_type
= 'FACES'
266 obj_new
.use_dupli_faces_scale
= True
267 obj_new
.dupli_faces_scale
= 100.0
269 inst_ob
.location
= 0.0, 0.0, 0.0
270 inst_ob
.parent
= obj_new
272 # align the object with worldspace
273 obj_new
.matrix_world
= obj_mat
275 # BGE settings for testing
277 inst_ob.game.physics_type = 'RIGID_BODY'
278 inst_ob.game.use_collision_bounds = True
279 inst_ob.game.collision_bounds = 'TRIANGLE_MESH'
280 inst_ob.game.collision_margin = 0.1
281 obj_new.select = True
284 # build faces from vert/normals
285 tri
= (Vector((0.0, 0.0, 0.01)),
286 Vector((0.0, 0.0, 0.0)),
287 Vector((0.0, 0.01, 0.01)))
291 for i
in range(len(hits
)):
295 quat
= no
.to_track_quat('X', 'Z')
297 # make 2 angles and blend
298 angle
= uniform(-pi
, pi
)
299 angle_aligned
= -(ori
.angle(quat
* Y_UP
, pi
))
301 quat
= Quaternion(no
,
302 (angle
* (1.0 - RAND_ALIGN
)) +
303 (angle_aligned
* RAND_ALIGN
)
306 f
= uniform(0.1, 1.2) * SCALE
308 coords
.append([co
+ (quat
* (tri
[0] * f
)),
309 co
+ (quat
* (tri
[1] * f
)),
310 co
+ (quat
* (tri
[2] * f
)),
318 from bpy
.props
import FloatProperty
, StringProperty
321 class Scatter(bpy
.types
.Operator
):
323 bl_idname
= "object.scatter"
324 bl_label
= "Grease Pencil Scatter"
326 density
= FloatProperty(
328 description
="Multiplier for the density of items",
329 default
=1.0, min=0.01, max=10.0,
331 scale
= FloatProperty(
333 description
="Size multiplier for duplifaces",
334 default
=1.0, min=0.01, max=10.0,
336 rand_align
= FloatProperty(
338 description
="Randomize alignment with the walls",
339 default
=0.75, min=0.0, max=1.0,
341 rand_loc
= FloatProperty(
343 description
="Randomize placement",
344 default
=0.75, min=0.0, max=1.0,
346 # XXX, should not be a string - TODO, add a way for scritps to select ID's
347 group
= StringProperty(
349 description
=("Group name to use for object placement, "
350 "defaults to object name when that matches a group"))
352 def execute(self
, context
):
353 obj
= bpy
.context
.object
354 group
= bpy
.data
.groups
.get(self
.group
)
357 self
.report({'ERROR'}, "Group %r not found" % self
.group
)
363 DENSITY
=self
.density
,
365 RAND_LOC
=self
.rand_loc
,
366 RAND_ALIGN
=self
.rand_align
,
370 def check(self
, context
):
371 if self
.group
not in bpy
.data
.groups
:
376 def invoke(self
, context
, event
):
378 # useful to initialize, take a guess
379 if not self
.group
and context
.object.name
in bpy
.data
.groups
:
380 self
.group
= context
.object.name
382 wm
= context
.window_manager
383 wm
.invoke_props_dialog(self
, width
=180)
384 return {'RUNNING_MODAL'}
387 def menu_func(self
, context
):
388 self
.layout
.operator(Scatter
.bl_idname
, icon
='AUTO')
392 bpy
.utils
.register_class(Scatter
)
393 bpy
.types
.INFO_MT_mesh_add
.append(menu_func
)
397 bpy
.utils
.unregister_class(Scatter
)
398 bpy
.types
.INFO_MT_mesh_add
.remove(menu_func
)
400 #if __name__ == "__main__":