3
$\begingroup$

I'd like to place objects on a mesh using snap with a python script in Blender 2.79, preferably on places where the gradient (steepness) of the mesh (terrain) is not too steep.

From the snap documentation and the API documentation for the transform I couldn't figure out how to do it in a script. The output of manually doing this in the info window (bpy.ops.transform.translate(value=(121.057, 0, 0), constraint_axis=(False, False, False), constraint_orientation='GLOBAL', mirror=False, proportional='DISABLED', proportional_edit_falloff='SMOOTH', proportional_size=1, release_confirm=True, use_accurate=False)) also didn't help.

One of the code snippets I tried:

cube = bpy.ops.mesh.primitive_cube_add()
bpy.ops.transform.resize(value=(4,1,1))
x,y,z = -100,-100,100
# Hoping that this line would place the object on a nearby surface of this point (It doesn't)
bpy.ops.transform.translate(snap=True, snap_target='ACTIVE', snap_align=True, snap_point=(x,y,z))
# Settings for the tool like doing it manually
bpy.context.scene.tool_settings.use_snap_align_rotation = True
bpy.context.scene.tool_settings.use_snap = True
bpy.context.scene.tool_settings.snap_target = 'ACTIVE'
bpy.ops.transform.translate(value=(121.057, 0, 0), constraint_axis=(False, False, False), constraint_orientation='GLOBAL', mirror=False, proportional='DISABLED', proportional_edit_falloff='SMOOTH', proportional_size=1, release_confirm=True, use_accurate=False)

Thanks in advance for any help, hints and other ways to place the objects on the mesh with proper orientation!

Image shows how I would like my objects to be placed:

What I am looking for

$\endgroup$
2
  • 1
    $\begingroup$ It looks correct to me. The objects center seems to be exactly aligned with the mesh, but you have to check for yourself. Does changing the object center to the bottom of your mesh lead to the result you want? $\endgroup$
    – TeeTrinker
    Commented Apr 19, 2020 at 20:08
  • $\begingroup$ @TeeTrinker The image is just to show what I would like. I created it using snap manually, my script does not snap the box to the closest object, just places it at the position I tell it to. $\endgroup$
    – a-doering
    Commented Apr 20, 2020 at 19:15

2 Answers 2

6
+50
$\begingroup$

Make a shrinkwrap footing.

Oops just saw the 2.79.. see edit below.

enter image description here Demonstrate the setup, the red dot foot is shrinkwrapped to grid. The wireframe cube is parented to it.

Similarly to how a tyre rig would be made, can make a 2d mesh footing and projection shrinkwrap to the landscape.

The "block" can then be placed in relation to the shrinkwrapped footing.

In the example below have used a simple circle as the footing and 3 vertices as a parent.

Setting the matrix world of "block" to a copy taken before removing footing places the "blocks". Set remove_block to False to keep the footing.

The script below sets random x, y locations in range -20, 20 to test. GIF became too large to demo,.. can arbitrarily rotate footing in z.

enter image description here Result of running 20 times with subsurfed upscaled 10x10 grid with texture displace

Object mode. Select grid, run script. Resets the grid to context so can repeat.

import bpy
from mathutils import Matrix
context = bpy.context

from random import uniform # random 20x20 to test
x, y = uniform(-20, 20), uniform(-20, 20)
remove_foot = True

target_surface = context.object


bpy.ops.mesh.primitive_cube_add(location=(0, 0, 0))
#make the origin the pottom of cube
cube = context.object
me = cube.data
me.transform(Matrix.Translation((0, 0, 1)))
cube.scale = (4, 1, 1)


bpy.ops.mesh.primitive_circle_add(
        location=(x, y, 0),
        fill_type='TRIFAN')
foot = context.object
sw = foot.modifiers.new(name="SW", type='SHRINKWRAP') 
sw.target = target_surface
sw.wrap_method = 'PROJECT'
sw.use_positive_direction = True
sw.use_negative_direction = True
sw.use_project_z = True

### set the relation to foot
cube.parent = foot
cube.parent_type = 'VERTEX_3'
n = len(foot.data.vertices)
cube.parent_vertices = range(1, n, n // 3)

if remove_foot:
    dg = context.evaluated_depsgraph_get()
    mw = cube.evaluated_get(dg).matrix_world.copy()
    bpy.data.objects.remove(foot)
    cube.matrix_world = mw
context.view_layer.objects.active = target_surface

Notes:

Could also look at the vertex coords and normals of the evaluated footing

Here the "block" is located at the middle circle (footing) vert and rotated such that vert normal is z up. For the default circle, vert 0 is the centre, vert 1 is at (0, 1, 0).

### set the relation to foot
dg = context.evaluated_depsgraph_get()
me_inst = foot.evaluated_get(dg).to_mesh(depsgraph=dg)
v = me_inst.vertices[0]

cube.location = foot.matrix_world @ v.co.copy()
q = v.normal.to_track_quat()
cube.rotation_euler = q.to_euler()
if remove_foot:
    bpy.data.objects.remove(foot)
context.view_layer.objects.active = target_surface

The angle between the vertex normal and z axis will give the grade, and could be tested so as to not add above a certain slope.

z_axis = Vector((0, 0, 1)
if v.normal.angle(z_axis) > radians(45):
    # don't put it there

Another way would be to add vertex groups to the footing and transform the block to it via constraints. eg copy location and a track to constraint targeting different vert groups.

Recommend making a more apt footing shape. Perhaps a poked plane matching bottom block face.

To do for many would use Object.copy()

EDIT 2.79 version of script above.

import bpy
from mathutils import Matrix
context = bpy.context

from random import uniform # random 20x20 to test
x, y = uniform(-20, 20), uniform(-20, 20)
remove_foot = True

scene = context.scene
target_surface = context.object


bpy.ops.mesh.primitive_cube_add(location=(0, 0, 0))
#make the origin the pottom of cube
cube = context.object
me = cube.data
me.transform(Matrix.Translation((0, 0, 1)))
cube.scale = (4, 1, 1)


bpy.ops.mesh.primitive_circle_add(
        location=(x, y, 0),
        fill_type='TRIFAN')
foot = context.object
sw = foot.modifiers.new(name="SW", type='SHRINKWRAP') 
sw.target = target_surface
sw.wrap_method = 'PROJECT'
sw.use_positive_direction = True
sw.use_negative_direction = True
sw.use_project_z = True

### set the relation to foot
cube.parent = foot
cube.parent_type = 'VERTEX_3'
n = len(foot.data.vertices)
cube.parent_vertices = range(1, n, n // 3)

if remove_foot:
    scene.update()
    cube.update_tag(refresh={'OBJECT'})
    mw = cube.matrix_world.copy()
    bpy.data.objects.remove(foot)
    cube.matrix_world = mw
scene.objects.active = target_surface
$\endgroup$
2
$\begingroup$

I faced a similar issue and wrote a script to place many armature bones, snapped to mesh edge orientations. I had a short play with it tonight and got it working in this new context.

enter image description here

I don't know how you want to use it, but for this example the script chooses random faces, creates a list of the selected face indexes, deselects and then re-selects each face in turn, snaps the cursor to the face, adds a cube at the cursor, creates a custom orientation gizmo from the normal of the selected face, aligns the cube to the custom orientation gizmo, cycles to the next face in the index and does it all over again. Yeah, I know. I haven't done much to sanity check this, but I distinctly remember how convoluted it was when I wrote the original script and that I couldn't find any way around it. Maybe others can cast an eye over it and suggest a few shortcuts. I am using 2.82 but I don't think there's anything here that conflicts with 2.79. It's as rough as, but I hope it helps.

import bpy
import bmesh

C = bpy.context
scene = C.scene
screen = C.screen

def deselect():
    for obj in bpy.context.visible_objects:
        obj.select_set(False)

def Frank():
    #Create custom orientation based on normal of selected face
    scene.transform_orientation_slots[0].type = 'NORMAL'
    bpy.ops.transform.create_orientation(name='Frank', overwrite=True)
    scene.transform_orientation_slots[0].type = 'Frank'

def Norm():
    # Delete custom orientation and return to 'NORMAL'
    bpy.ops.transform.delete_orientation()
    scene.transform_orientation_slots[0].type = 'NORMAL'

def get_co(name, scene=scene):
    # Return custom gizmo orientation matrix
    slots = scene.transform_orientation_slots
    for s in slots:
        if s.custom_orientation and s.custom_orientation.name == name:
            return s.custom_orientation            
    return None

def context_override():
    # Need to move context from Text Editor to 3d View when you hit "Run Script"
    for area in screen.areas:
        if area.type == 'VIEW_3D':
            context_override = {'window': bpy.context.window, 'screen': bpy.context.screen, 'area' : area}
            return context_override

######################## Script starts ####################
# Ensure transform orientation is set to 'NORMAL'
scene.transform_orientation_slots[0].type = 'NORMAL'

#Get object references
for obj in bpy.context.visible_objects:
    if obj.type == 'MESH':
        meshName = obj.name
        current_obj =  obj

#Force edit mode
bpy.ops.object.mode_set(mode='EDIT')

#Select random faces
bpy.ops.mesh.select_random(seed=2)

#Get index of selected faces
me = bpy.context.object.data
bm = bmesh.from_edit_mesh(me)
e = []
# Check all faces
for f in bm.faces:
    # If any are selected,
    if f.select:
        # add the face index number to the list of selected faces
        e.append(f.index)
        # Then deselect the face in readiness for cycling through and adding cubes
        f.select = False

for i in range(len(e)):
    # Cycle through list of selected faces
    bpy.ops.object.mode_set(mode = 'EDIT') 
    bpy.ops.mesh.select_mode(type="FACE")
    bpy.ops.mesh.select_all(action = 'DESELECT')
    # Select face while in object mode
    bpy.ops.object.mode_set(mode = 'OBJECT')
    obj.data.polygons[e[i]].select = True

    # Back to edit mode to snap cursor to selected face...
    bpy.ops.object.mode_set(mode = 'EDIT') 
    #... and create custom gizmo
    Frank()

    # Need context override to move focus from text window to view 3d...
    c_o = context_override()
    #... in order to snap cursor to selected face
    bpy.ops.view3d.snap_cursor_to_selected(c_o)

    #Back to Object mode to add cube
    bpy.ops.object.mode_set(mode='OBJECT')
    deselect()      # Need to deselect all first
    bpy.ops.mesh.primitive_cube_add(size=.1)

    # Get custom gizmo coordinate matrix...
    co = get_co("Frank")
    #... and align cube to gizmo
    bpy.context.active_object.matrix_local = co.matrix.to_4x4()

    # Snap newly minted cube to 3d cursor
    bpy.ops.view3d.snap_selected_to_cursor(c_o, use_offset=False)

    #... select the mesh, ...
    bpy.data.objects[meshName].select_set(True)
    #... make the Object selection active...
    bpy.context.view_layer.objects.active = current_obj
    #... and finally deselect the current face in readiness for the next face select
    obj.data.polygons[e[i]].select = False

# Set translation gizmo back to 'Normal for fine tuning
Norm()
$\endgroup$

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .