'''
@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)