There's something wrong with the ray_cast, which messes up on a few casts. But I think this could clear up if placed in a modal operator.
I believe the script from this answer achieves what you want.
The result looks very similiar to your gif.
- Convert the collision geometry to a bmesh, apply the transformation matrix and get a bvhtree from the transformed bmesh.
- Cast a ray from above the Cube's location straight downwards (0, 0, -1) and check if it intersects with the bvhtree.
- Create a keyframe.
import bpy
import bmesh
from mathutils.bvhtree import BVHTree
import mathutils
# the epsilon value for the BVHTree calculations
EPSILON = 0.0000001
# maximum ray distance XY plane
MAXIMUM_Z = 1000
# make sure you have a Camera and an Empty named like this
landscape = bpy.data.objects["Landscape"]
cube = bpy.data.objects["Cube"]
# create the BVHTrees from a bmesh of the "sticky" object
# the bmesh conversion makes it easy to apply the individual objects transformation matrices
bm = bmesh.new()
bm.from_object(landscape, bpy.context.scene)
bmesh.ops.transform(bm, matrix=landscape.matrix_world, verts=bm.verts)
tree = BVHTree.FromBMesh(bm, epsilon=EPSILON)
# the main method for setting the new z position
def reposition_z(scene = bpy.context.scene):
# create a direction vector, which points downwards and
# a origin for the raycast
dir = mathutils.Vector((0, 0, -1))
origin = mathutils.Vector((*cube.location[:2], MAXIMUM_Z - EPSILON))
# if there is a hit, move the cube to that location
loc, no, i, d = tree.ray_cast(origin, dir)
if d is not None:
# assign the cube's location
cube.location = loc
# remove all handlers, then create a new one with the reposition_z function
post_handlers= bpy.app.handlers.scene_update_post
[post_handlers.remove(h) for h in post_handlers if h.__name__ == "reposition_z"]
post_handlers.append(reposition_z)
Setting a keyframe could look like.
for i in range(start, end):
bpy.data.scenes["Scene"].frame_set(i)
reposition_z()
cube.keyframe_insert(data_path = 'location')