top of page

'''
@file   AtomAnimation.py
@author Aidan Connor
@date   4/22/20

Procedurally creates an animation of an atom.
'''

import maya.cmds as cmds
import random
import math
import functools

NUM_FRAMES = 240

# Creates the nucleus of the atom with the specified size
def create_nucleus(nucleus_size):
    
    NUCLEON_RADIUS = 0.5 # radius of nucleus spheres
    OVERLAP = 0.4 # nucleon overlap coefficient
    JITTER = 3.0 # helps randomly place nucleons
    nucleons = []
    nucleon_group = cmds.group(name = "nucleons#", empty = True)
    proton_group = cmds.group(name = "protons#", empty = True)
    neutron_group = cmds.group(name = "neutrons#", empty = True)
    cmds.parent([proton_group, neutron_group], nucleon_group)
    
    center_pos = nucleus_size / 2.0
    t1 = NUCLEON_RADIUS * 2.0 * (1 - OVERLAP) # spacing between nucleons
    t2 = NUCLEON_RADIUS * OVERLAP * JITTER * 0.5 # max random jitter
    
    # create and arrange the nucleons
    for x in range(nucleus_size):
        for y in range(nucleus_size):
            for z in range(nucleus_size):
                
                # determine if in bounds of nucleus sphere
                if (math.sqrt((x - center_pos) ** 2 + (y - center_pos) ** 2 + (z - center_pos) ** 2) > center_pos):
                    continue
                
                # determine position
                pos = [x * t1 + random.uniform(-t2, t2), y * t1 + random.uniform(-t2, t2), z * t1 + random.uniform(-t2, t2)] # [x, y, z]
                
                # create the nucleon
                nucleon = cmds.polySphere(r = NUCLEON_RADIUS, name = "nucleon#")[0]
                cmds.move(pos[0], pos[1], pos[2], nucleon)
                nucleons.append(nucleon)
                
                # randomly assign to protons or neutrons
                if (random.randint(0, 1) == 0):
                    cmds.parent(nucleon, proton_group)
                else:
                    cmds.parent(nucleon, neutron_group)
    
    # center the nucleon group's pivot and center the group
    cmds.xform(nucleon_group, centerPivots = True)
    pivot = cmds.getAttr(nucleon_group + ".scalePivot")[0]
    cmds.move(-pivot[0], -pivot[1], -pivot[2], nucleon_group)

# creates electrons surrounding the nucleus
def create_electrons(num_electrons):
    
    ELECTRON_RADIUS = 0.2
    MIN_DIST = 7.0
    MAX_DIST = 14.0
    MIN_SPEED = 3.0
    MAX_SPEED = 8.0
    cloud = cmds.group(name = "cloud#", empty = True)
    e_particle_group = cmds.group(name = "electron_particles#", empty = True)
    
    # create electrons
    for i in range(num_electrons):
        
        # create electron and orbit
        electron = cmds.polySphere(r = ELECTRON_RADIUS, subdivisionsAxis = 10, subdivisionsHeight = 10, name = "electron#")[0]
        e_orbit = cmds.circle(r = random.uniform(MIN_DIST, MAX_DIST), s = 50, name = "orbit#")[0]
        cmds.rotate(random.uniform(0, 360), random.uniform(0, 360), random.uniform(0, 360), e_orbit, os = True, fo = True)
        
        # attach electron to orbit
        cmds.select([electron, e_orbit])
        path = cmds.pathAnimation(startTimeU = 1, endTimeU = NUM_FRAMES / random.uniform(MIN_SPEED, MAX_SPEED))
        cmds.keyTangent(path + "_uValue", itt = "linear", ott = "linear")
        cmds.setAttr(path + "_uValue.postInfinity", 3)
        
        # put electron and orbit in cloud group
        cmds.parent([electron, e_orbit], cloud)
        
        # create particle emitter from electron
        e_emitter = cmds.emitter(electron, type = "omni", r = 30, spd = 0.5)[1]
        e_particle, e_particle_shape = cmds.nParticle()
        cmds.connectDynamic(e_particle, em = e_emitter)
        
        # set some properties of the particles
        nucleus_solver = cmds.ls("nucleus*")[0]
        cmds.setAttr(nucleus_solver + ".gravity", 0)
        cmds.setAttr(e_particle_shape + ".lifespanMode", 1)
        cmds.setAttr(e_particle_shape + ".lifespan", 0.5)
        cmds.setAttr(e_particle_shape + ".particleRenderType", 8)
        cmds.setAttr(e_particle_shape + ".colorInput", 1)
        cmds.setAttr(e_particle_shape + ".colorInputMax", 0.3)
        cmds.setAttr(e_particle_shape + ".opacityScaleInput", 1)
        cmds.setAttr(e_particle_shape + ".opacityScaleInputMax", 0.3)
        cmds.setAttr(e_particle_shape + ".collide", 0)
        
        # set opacity ramp
        cmds.setAttr(e_particle_shape + ".opacityScale[0].opacityScale_Interp", 3)
        cmds.setAttr(e_particle_shape + ".opacityScale[1].opacityScale_Position", 1.0)
        cmds.setAttr(e_particle_shape + ".opacityScale[1].opacityScale_FloatValue", 0.0)
        cmds.setAttr(e_particle_shape + ".opacityScale[1].opacityScale_Interp", 3)
        cmds.setAttr(e_particle_shape + ".opacityScale[2].opacityScale_Position", 0.32)
        cmds.setAttr(e_particle_shape + ".opacityScale[2].opacityScale_FloatValue", 0.25)
        cmds.setAttr(e_particle_shape + ".opacityScale[2].opacityScale_Interp", 3)
        
        # set color ramp
        cmds.setAttr(e_particle_shape + ".color[0].color_Color", 0.0, 1.0, 1.0, type = "double3")
        cmds.setAttr(e_particle_shape + ".color[0].color_Position", 0.23)
        cmds.setAttr(e_particle_shape + ".color[1].color_Color", 1.0, 0.5, 0.0, type = "double3")
        cmds.setAttr(e_particle_shape + ".color[1].color_Position", 1.0)
        
        # add to group
        cmds.parent(e_particle, e_particle_group)

# create the UI to input values affecting atom animation
def create_UI(window_title, on_create_callback):
    
    window_ID = "atom_animation"
    if (cmds.window(window_ID, exists = True)):
        cmds.deleteUI(window_ID)
    
    cmds.window(window_ID, title = window_title, sizeable = False, resizeToFitChildren = True)
    cmds.rowColumnLayout(numberOfColumns = 2, columnWidth = [(1, 100), (2, 50)], columnOffset = [(1, "right", 3)])
    
    # first row
    cmds.text(label = "Nucleus Size:")
    nucleus_size_field = cmds.intField(value = 5)
    
    # second row
    cmds.text(label = "Electron Count:")
    electron_count_field = cmds.intField(value = 28)
    
    # third row
    cmds.separator(h = 10, style = "none")
    cmds.separator(h = 10, style = "none")
    
    # fourth row
    cmds.button(label = "Create", command = functools.partial(on_create_callback, window_ID, nucleus_size_field, electron_count_field))
    
    def on_cancel_callback(*args):
        if (cmds.window(window_ID, exists = True)):
            cmds.deleteUI(window_ID)
    cmds.button(label = "Cancel", command = on_cancel_callback)
    
    cmds.showWindow()

# callback function to execute when 'create' button is pressed in UI
def create_callback(window_ID, nucleus_size_field, electron_count_field, *args):
    
    # get values from fields
    nucleus_size = cmds.intField(nucleus_size_field, query = True, value = True)
    electron_count = cmds.intField(electron_count_field, query = True, value = True)
    
    # close window
    cmds.deleteUI(window_ID)
    
    # create the thing
    random.seed(69420 * 3)
    create_nucleus(nucleus_size)
    create_electrons(electron_count)

##########################################################

# create 290 frames
cmds.playbackOptions(min = 1, max = NUM_FRAMES + 50, playbackSpeed = 0, maxPlaybackSpeed = 1)
cmds.currentTime(1)

# do the main part
create_UI("Create Atom", create_callback)

# cleanup
cmds.select(clear = True)

bottom of page