Python Tools — Session 6B

Dynamic Rigid Body Simulation

2020.02.13 — Session 6B

Session Navigation

Overview

This session builds a complete dynamic scene: textured spheres are spawned on a grid, assigned random image materials, and dropped under gravity onto a randomly-deformed ground plane. It brings together material creation, rigid body physics, vertex queries, and boundary detection into one cohesive project.

Key Concepts

  • Rigid Body Dynamics — Setting up active and passive rigid bodies with gravity fields for physics simulation
  • Material Creation — Programmatically creating Blinn materials with file textures and connecting shader networks
  • Random Image Assignment — Using os.listdir and random.choice to pick textures from a directory
  • Vertex Boundary Detection — Using isOnBoundary() to check if vertices are on the edge of a mesh
  • Scene Setup — Managing playback options, viewport display modes, and scene cleanup from code

Code

faces_project_code_commented.py — Full Dynamic Scene

"""
#############################################################################
filename    faces_project_code_commented.py
author      Matt Osbond 
dP email    m.osbond@digipen.edu
course      CS115
Brief Description:
    Session 4B - Thursday Jan 30th
    Experimenting with creating a rigid body dynamics scene using Python
#############################################################################
"""

import pymel.core as pm
import random
import os

def get_random_image():
    """Grabs a random image from our directory and returns the path on disk
    """
    # Hard coded path to the face images directory
    dir = r"C:\path\to\your\file"

    all_files = os.listdir(dir)

    # Iterate through the list and update each element to include the root dir 
    # os.listdir only returns a list of names
    for f in xrange(0, len(all_files)):
        all_files[f] = os.path.join(dir, all_files[f])

    # use random.choice to randomly pick an element, and return it
    return random.choice(all_files)

    
def create_ball(position):
    """Creates a ball at the specified position
    Will also assign a new material using a randomly picked texture

    Args: world space position of the ball 

    Returns: the new ball object
    """
    # Create a new ball and move it to te corect position
    ball = pm.polySphere(sx=10, sy=10)[0]
    pm.move(ball, position)

    # Grab a random texture
    texture = get_random_image()

    # Create a new material using the new random texture
    new_material = create_material(texture)

    # Use the hyperShade to assign the new material (note, will work on the selected objects)
    pm.hyperShade(assign=new_material)

    # Return the ball object
    return ball


def create_material(image_path):
    """Create a new blinn material using the given texture path as a diffuse input

    Args: path on disk to the texture to us

    Returns: the new material object
    """
    # Create the new file node and assign the image path
    file_node = pm.createNode("file", skipSelect=True)
    file_node.fileTextureName.set(image_path)

    # Create a new material
    material = pm.createNode("blinn", skipSelect=True)

    # Create a new empty shading group
    sg = pm.sets(renderable=True, noSurfaceShader=True, empty=True)

    # Connect the file node (texture) color to the material color input
    pm.connectAttr( (file_node + ".outColor"), (material + ".color"), force=True)

    # Connect the output color of the material to the shading group surfaceShader
    pm.connectAttr( (material + ".outColor"), (sg + ".surfaceShader"), force=True)

    # Return the new material object
    return material

def create_dynamic_scene():
    """ Main trigger function for the project
    Runs all the operations

    CAUTION: will force a new empty scene, casuing loss of work. The newFile is commented out for this reason
    """
    # Comment out the newFile to be safe
    #pm.newFile(force=True)

    # Create the plane to put the ablls on and move it up
    sky_plane = pm.polyPlane(w=20, h=20)[0]
    pm.move(sky_plane, [0,20,0])

    # Create an empty list for the balls - we'll need this in a bit
    balls = list()
    for vert in sky_plane.verts:

        # Only create balls on the verts that are not on the edge
        if not vert.isOnBoundary():
            # Run the create_ball function, grab the resulting ball and add it to our list
            ball = create_ball(vert.getPosition(space='world'))
            balls.append(ball)

    # Create a gravit field
    grav = pm.gravity()
    # Add a rigid body solver to all our balls at once (so they use the same solver)
    pm.rigidBody(balls, active=True)
    # Connect the gravity to the bal objects
    pm.connectDynamic(balls, fields=grav)

    # Cleanup the scene by removing the sky plane - we only used it for position reference
    pm.delete(sky_plane)


    # Create a LOW RESOLUTION ground plane and randomly move each vert - we've done this a few times now
    ground_plane = pm.polyPlane(w=40, h=40, sx=6, sy=6)[0]
    for vert in ground_plane.verts:
        # Here, if the vert is on the edge, move it up higher
        # That way we can create a sort of "bowl" shape to stop the balls all bounding off
        if vert.isOnBoundary():
            pm.move(vert, [0,10,0], relative=True)
        else:
            rand = random.uniform(-6,6)
            pm.move(vert, [rand,rand,rand], relative=True)
    # Smooth our low res ground
    pm.polySmooth(ground_plane)

    # Apply some color
    pm.polyColorPerVertex(ground_plane, rgb=[0.38, 0.96, 0.37], cdo=True)

    # Apply a passive (static) rigid body to the ground
    pm.rigidBody(ground_plane, passive=True)

    # Set the scene up    
    pm.playbackOptions(maxTime=200) # Set the timeline
    pm.select(clear=True) # Select nothing
    pm.viewFit() # With nothing selected, this will frame the entire scene
    # This ONLY seems to work after you do it inside Maya (press "6") for the first time - will investigate!
    pm.mel.DisplayShadedAndTextured() 

        
# Trigger the whole process
create_dynamic_scene()
← Prev Next →