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 "tracker_url": "https://developer.blender.org",
35 "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 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
):
154 hit
, nor
, ind
= ray(pofs
, pofs
+ (m
* n_seek
))
156 dist
= (pofs
- hit
).length
163 pt
[1].length
= best_dist
167 #scene.cursor_location[:] = best_hitnyway
168 # bpy.ops.wm.redraw_timer(type='DRAW_WIN_SWAP',
170 # debug_edge(p, best_hit)
173 # Now we need to do scattering.
179 # point, normal, n_other the closest hit normal
180 for p
, n
, n_other
in s
:
184 # cast vectors twice as long as the distance
185 # needed just in case.
194 # We should cast rays between n_down and n_other
195 #for f in (0.0, 0.2, 0.4, 0.6, 0.8, 1.0):
196 TOT
= int(10 * DENSITY
)
197 #for i in list(range(TOT)):
198 for i
in list(range(TOT
))[int(TOT
/ 1.5):]: # second half
201 # focus on the center
208 ntmp
= f
* n_down
+ (1.0 - f
) * n_other
210 ntmp
.x
+= uniform(-l
, l
) * RAND_LOC
211 ntmp
.y
+= uniform(-l
, l
) * RAND_LOC
212 ntmp
.z
+= uniform(-l
, l
) * RAND_LOC
214 hit
, hit_no
, ind
= ray(vantage
, vantage
+ ntmp
)
217 if hit_no
.angle(Z_UP
) < WALL_LIMIT
:
220 oris
.append(n_other
.cross(hit_no
))
221 #oris.append(n_other)
224 mesh
= bpy
.data
.meshes
.new("ScatterDupliFace")
225 mesh
.from_pydata(hits
, [], [])
227 scene
= bpy
.context
.scene
229 obj_new
= bpy
.data
.objects
.new("ScatterPar", mesh
)
230 scene
.objects
.link(obj_new
)
231 obj_new
.layers
[:] = obj
.layers
233 # Now setup dupli-faces
234 obj_new
.dupli_type
= 'VERTS'
235 ob_child
= bpy
.data
.objects
["trash"]
236 ob_child
.location
= obj_new
.location
237 ob_child
.parent
= obj_new
240 def apply_faces(triples
):
241 # first randomize the faces
244 obs
= group
.objects
[:]
246 tot_div
= int(len(triples
) / tot
)
249 triple_sub
= triples
[0:tot_div
]
250 triples
[0:tot_div
] = []
252 vv
= [tuple(v
) for f
in triple_sub
for v
in f
]
254 mesh
= bpy
.data
.meshes
.new("ScatterDupliFace")
255 mesh
.from_pydata(vv
, [], [(i
* 3, i
* 3 + 1, i
* 3 + 2)
256 for i
in range(len(triple_sub
))])
258 scene
= bpy
.context
.scene
260 obj_new
= bpy
.data
.objects
.new("ScatterPar", mesh
)
262 scene
.objects
.link(obj_new
)
263 obj_new
.layers
[:] = obj
.layers
265 # Now setup dupli-faces
266 obj_new
.dupli_type
= 'FACES'
267 obj_new
.use_dupli_faces_scale
= True
268 obj_new
.dupli_faces_scale
= 100.0
270 inst_ob
.location
= 0.0, 0.0, 0.0
271 inst_ob
.parent
= obj_new
273 # align the object with worldspace
274 obj_new
.matrix_world
= obj_mat
276 # BGE settings for testing
278 inst_ob.game.physics_type = 'RIGID_BODY'
279 inst_ob.game.use_collision_bounds = True
280 inst_ob.game.collision_bounds = 'TRIANGLE_MESH'
281 inst_ob.game.collision_margin = 0.1
282 obj_new.select = True
285 # build faces from vert/normals
286 tri
= (Vector((0.0, 0.0, 0.01)),
287 Vector((0.0, 0.0, 0.0)),
288 Vector((0.0, 0.01, 0.01)))
292 for i
in range(len(hits
)):
296 quat
= no
.to_track_quat('X', 'Z')
298 # make 2 angles and blend
299 angle
= uniform(-pi
, pi
)
300 angle_aligned
= -(ori
.angle(quat
* Y_UP
, pi
))
302 quat
= Quaternion(no
,
303 (angle
* (1.0 - RAND_ALIGN
)) +
304 (angle_aligned
* RAND_ALIGN
)
307 f
= uniform(0.1, 1.2) * SCALE
309 coords
.append([co
+ (quat
* (tri
[0] * f
)),
310 co
+ (quat
* (tri
[1] * f
)),
311 co
+ (quat
* (tri
[2] * f
)),
319 from bpy
.props
import FloatProperty
, StringProperty
322 class Scatter(bpy
.types
.Operator
):
324 bl_idname
= "object.scatter"
325 bl_label
= "Grease Pencil Scatter"
327 density
= FloatProperty(
329 description
="Multiplier for the density of items",
330 default
=1.0, min=0.01, max=10.0,
332 scale
= FloatProperty(
334 description
="Size multiplier for duplifaces",
335 default
=1.0, min=0.01, max=10.0,
337 rand_align
= FloatProperty(
339 description
="Randomize alignment with the walls",
340 default
=0.75, min=0.0, max=1.0,
342 rand_loc
= FloatProperty(
344 description
="Randomize placement",
345 default
=0.75, min=0.0, max=1.0,
347 # XXX, should not be a string - TODO, add a way for scritps to select ID's
348 group
= StringProperty(
350 description
=("Group name to use for object placement, "
351 "defaults to object name when that matches a group"))
353 def execute(self
, context
):
354 obj
= bpy
.context
.object
355 group
= bpy
.data
.groups
.get(self
.group
)
358 self
.report({'ERROR'}, "Group %r not found", self
.group
)
364 DENSITY
=self
.density
,
366 RAND_LOC
=self
.rand_loc
,
367 RAND_ALIGN
=self
.rand_align
,
371 def check(self
, context
):
372 if self
.group
not in bpy
.data
.groups
:
377 def invoke(self
, context
, event
):
379 # useful to initialize, take a guess
380 if not self
.group
and context
.object.name
in bpy
.data
.groups
:
381 self
.group
= context
.object.name
383 wm
= context
.window_manager
384 wm
.invoke_props_dialog(self
, width
=180)
385 return {'RUNNING_MODAL'}
388 def menu_func(self
, context
):
389 self
.layout
.operator(Scatter
.bl_idname
, icon
='AUTO')
393 bpy
.utils
.register_class(Scatter
)
394 bpy
.types
.INFO_MT_mesh_add
.append(menu_func
)
398 bpy
.utils
.unregister_class(Scatter
)
399 bpy
.types
.INFO_MT_mesh_add
.remove(menu_func
)
401 #if __name__ == "__main__":