991 lines
31 KiB
Python
991 lines
31 KiB
Python
#A* -------------------------------------------------------------------
|
|
#B* This file contains source code for the PyMOL computer program
|
|
#C* Copyright (c) Schrodinger, LLC.
|
|
#D* -------------------------------------------------------------------
|
|
#E* It is unlawful to modify or remove this copyright notice.
|
|
#F* -------------------------------------------------------------------
|
|
#G* Please see the accompanying LICENSE file for further information.
|
|
#H* -------------------------------------------------------------------
|
|
#I* Additional authors of this source file include:
|
|
#-* Peter Haebel, Byron DeLaBarre
|
|
#-*
|
|
#-*
|
|
#Z* -------------------------------------------------------------------
|
|
|
|
import sys
|
|
cmd = sys.modules["pymol.cmd"]
|
|
import math
|
|
import os
|
|
import glob
|
|
import shutil
|
|
import threading
|
|
import time
|
|
from . import colorprinting
|
|
|
|
def get_movie_fps(_self):
|
|
r = _self.get_setting_float('movie_fps')
|
|
if r <= 0:
|
|
return 30
|
|
return r
|
|
|
|
def sweep(pause=0,cycles=1,_self=cmd):
|
|
pause = int(pause)
|
|
cycles = int(cycles)
|
|
n_state = _self.count_states("all")
|
|
if pause>0:
|
|
pass_string = "1 x%d 1 -%d %d x%d %d -1"%(pause, n_state, n_state, pause, n_state)
|
|
else:
|
|
pass_string = "1 -%d %d -1"%(n_state, n_state)
|
|
movie_list = [ pass_string ] * cycles
|
|
movie_string = " ".join(movie_list)
|
|
_self.mset(movie_string)
|
|
|
|
def pause(pause=15,cycles=1,_self=cmd):
|
|
pause = int(pause)
|
|
cycles = int(cycles)
|
|
n_state = _self.count_states("all")
|
|
if pause>0:
|
|
pass_string = "1 x%d 1 -%d %d x%d"%(pause, n_state, n_state, pause)
|
|
else:
|
|
pass_string = "1 -%d %d -1"%(n_state, n_state)
|
|
movie_list = [ pass_string ] * cycles
|
|
movie_string = " ".join(movie_list)
|
|
_self.mset(movie_string)
|
|
|
|
def load(pattern, nam = "mov", _self=cmd, **kw):
|
|
fils = glob.glob(pattern)
|
|
if not fils:
|
|
print("Error: no matching files")
|
|
return
|
|
for a in sorted(fils):
|
|
_self.load(a, nam, **kw)
|
|
|
|
def rock(first=1,last=-1,angle=30,phase=0,loop=1,axis='y',_self=cmd):
|
|
first=int(first)
|
|
last=int(last)
|
|
if last<0:
|
|
last = _self.count_frames()
|
|
angle=float(angle)
|
|
phase=float(phase)
|
|
loop=int(loop)
|
|
nstep = (last-first)+1
|
|
if nstep<0:
|
|
nstep = 1
|
|
if loop:
|
|
subdiv = nstep
|
|
else:
|
|
subdiv = nstep+1
|
|
ang_cur = math.pi*phase/180
|
|
ang_inc = 2*math.pi/subdiv
|
|
ang_cur = ang_cur - ang_inc
|
|
a = 0
|
|
while a<nstep:
|
|
last = angle*math.sin(ang_cur)/2
|
|
ang_cur = ang_cur + ang_inc
|
|
disp = angle*math.sin(ang_cur)/2
|
|
diff = disp-last
|
|
# com = "mdo %d:turn %s,%8.3f" % (first+a,axis,diff)
|
|
# _self.do(com)
|
|
_self.mdo("%d"%(first+a),"turn %s,%8.3f"% (axis,diff))
|
|
a = a + 1
|
|
|
|
def roll(first=1,last=-1,loop=1,axis='y',_self=cmd):
|
|
first=int(first)
|
|
last=int(last)
|
|
loop=int(loop)
|
|
axis=str(axis)
|
|
if last<0:
|
|
last = _self.count_frames()
|
|
n = last - first
|
|
if loop:
|
|
step = 2*math.pi/(n+1)
|
|
else:
|
|
step = 2*math.pi/n
|
|
a = 0
|
|
invert = 1
|
|
if axis[0:1]=='-':
|
|
axis=axis[1:]
|
|
invert = -1
|
|
deg = invert * (180*step/math.pi)
|
|
while a<=n:
|
|
# com = "mdo %d:turn %s,%8.3f" % (first+a,axis,deg)
|
|
# _self.do(com)
|
|
_self.mdo("%d" % (first+a), "turn %s,%8.3f" % (axis,deg))
|
|
a = a + 1
|
|
|
|
|
|
def tdroll(first,rangex,rangey,rangez,skip=1,_self=cmd):
|
|
'''
|
|
AUTHOR
|
|
|
|
Byron DeLaBarre
|
|
|
|
USAGE
|
|
|
|
movie.tdroll(rangx,rangey,rangez,skip=1,mset=0)
|
|
|
|
rangex/y/z = rotation range on respective axis
|
|
enter 0 for no rotation.
|
|
|
|
skip is angle increment in each frame
|
|
|
|
Use skip to reduce final movie size or to speed up rotation.
|
|
|
|
EXAMPLE
|
|
|
|
movie.tdroll 360,360,360,5
|
|
|
|
'''
|
|
first = int(first)
|
|
rangex = float(rangex)
|
|
rangey = float(rangey)
|
|
rangez = float(rangez)
|
|
skip = int(skip)
|
|
axis=['x','y','z']
|
|
rangel=[rangex,rangey,rangez]
|
|
axpos=0
|
|
frpos=1
|
|
for ax in axis:
|
|
range = int(rangel[axpos])
|
|
if range:
|
|
leftover = divmod(range,skip)
|
|
print(leftover[1])
|
|
if leftover[1]:
|
|
range = range + int(leftover[1])
|
|
a = 0
|
|
while a<range:
|
|
_self.mdo("%d" % (first+frpos-1), "turn %s,%8.3f" % (ax,skip))
|
|
a = a + skip
|
|
frpos = frpos + 1
|
|
axpos = axpos + 1
|
|
else:
|
|
axpos = axpos + 1
|
|
print((" tdroll: defined rotations for", frpos - 1,
|
|
"frames, starting at frame %d"%first))
|
|
|
|
def zoom(first,last,step=1,loop=1,axis='z',_self=cmd):
|
|
# Author: Peter Haebel
|
|
first=int(first)
|
|
last=int(last)
|
|
step=int(step)
|
|
loop=int(loop)
|
|
n = last - first
|
|
a = 0
|
|
while a<=n:
|
|
if (loop and a>n/2):
|
|
s = -step
|
|
else:
|
|
s = step
|
|
# com = "mdo %d:move %s,%8.3f" % (first+a,axis,s)
|
|
# _self.do(com)
|
|
_self.mdo("%d" % (first+a),"move %s,%8.3f" % (axis,s))
|
|
a = a + 1
|
|
|
|
def nutate(first,last,angle=30,phase=0,loop=1,shift=math.pi/2.0,_self=cmd):
|
|
first=int(first)
|
|
last=int(last)
|
|
angle=float(angle)
|
|
phase=float(phase)
|
|
loop=int(loop)
|
|
nstep = (last-first)+1
|
|
if nstep<0:
|
|
nstep = 1
|
|
if loop:
|
|
subdiv = nstep
|
|
else:
|
|
subdiv = nstep+1
|
|
ang_cur = math.pi*phase/180
|
|
ang_inc = 2*math.pi/subdiv
|
|
ang_cur = ang_cur - ang_inc
|
|
a = 0
|
|
while a<nstep:
|
|
lastx = angle*math.sin(ang_cur)/2
|
|
lasty = angle*math.sin(ang_cur+shift)/2
|
|
ang_cur = ang_cur + ang_inc
|
|
nextx = angle*math.sin(ang_cur)/2
|
|
nexty = angle*math.sin(ang_cur+shift)/2
|
|
# com = "mdo %d:turn %s,%8.3f" % (first+a,axis,diff)
|
|
# _self.do(com)
|
|
_self.mdo("%d"%(first+a),"turn x,%8.3f;turn y,%8.3f;turn y,%8.3f;turn x,%8.3f"%
|
|
(-lastx,-lasty,nexty,nextx))
|
|
a = a + 1
|
|
|
|
def screw(first,last,step=1,angle=30,phase=0,loop=1,axis='y',_self=cmd):
|
|
# Author: Peter Haebel
|
|
first=int(first)
|
|
last=int(last)
|
|
step=int(step)
|
|
angle=float(angle)
|
|
phase=float(phase)
|
|
loop=int(loop)
|
|
nstep = (last-first)+1
|
|
if nstep<0:
|
|
nstep = 1
|
|
if loop:
|
|
subdiv = nstep
|
|
else:
|
|
subdiv = nstep+1
|
|
ang_cur = math.pi*phase/180
|
|
ang_inc = 2*math.pi/subdiv
|
|
ang_cur = ang_cur - ang_inc
|
|
a = 0
|
|
while a<nstep:
|
|
if (loop and a>=nstep/2):
|
|
s = -step
|
|
else:
|
|
s = step
|
|
last = angle*math.sin(ang_cur)/2
|
|
ang_cur = ang_cur + ang_inc
|
|
disp = angle*math.sin(ang_cur)/2
|
|
diff = disp-last
|
|
# com = "mdo %d:turn %s,%8.3f; move z,%8.3f" % (first+a,axis,diff,s)
|
|
# _self.do(com)
|
|
_self.mdo("%d" % (first+a), "turn %s,%8.3f; move z,%8.3f" % (axis,diff,s))
|
|
a = a + 1
|
|
|
|
def timed_roll(period=12.0,cycles=1,axis='y',_self=cmd):
|
|
frames_per_sec = get_movie_fps(_self)
|
|
if frames_per_sec<1.0:
|
|
frames_per_sec=30.0
|
|
frames_per_cycle = int(period*frames_per_sec)
|
|
total = frames_per_cycle * cycles
|
|
|
|
_self.mset("1 x%d"%total)
|
|
step = 2*math.pi/(frames_per_cycle)
|
|
deg = (180*step/math.pi)
|
|
_self.mview('reset')
|
|
_self.rewind()
|
|
frame = 1
|
|
for cycle in range(cycles):
|
|
for cnt in range(frames_per_cycle):
|
|
_self.turn(axis,deg)
|
|
_self.mview('store',frame,freeze=1)
|
|
frame = frame + 1
|
|
|
|
# blank frames to be specified by the user
|
|
|
|
def add_blank(duration=12.0,start=0,_self=cmd):
|
|
'''
|
|
DESCRIPTION
|
|
|
|
Add "blank" time to the movie (without any key frames)
|
|
|
|
ARGUMENTS
|
|
|
|
duration = float: time to add to movie in seconds {default: 12}
|
|
|
|
start = int: start frame (1 = first frame; 0 = end of movie) {default: 0}
|
|
|
|
SEE ALSO
|
|
|
|
mset
|
|
'''
|
|
cmd = _self
|
|
if not start:
|
|
start = cmd.get_movie_length()+1
|
|
duration = float(duration)
|
|
fps = get_movie_fps(_self)
|
|
n_frame = int(round(fps * duration))
|
|
if n_frame > 0:
|
|
cmd.mset("1 x%d"%n_frame,start)
|
|
cmd.frame(start)
|
|
|
|
# new matrix-based camera interpolation routines
|
|
|
|
def add_roll(duration=12.0,loop=1,axis='y',start=0,_self=cmd):
|
|
'''
|
|
DESCRIPTION
|
|
|
|
Append a 360 degree camera rotation to the movie, using key frames.
|
|
|
|
ARGUMENTS
|
|
|
|
duration = float: time to add to movie in seconds {default: 12}
|
|
|
|
loop = 0/1: ???
|
|
|
|
axis = x, y or z: rotation axis in camera space {default: y}
|
|
|
|
start = int: start frame (1 = first frame; 0 = end of movie) {default: 0}
|
|
'''
|
|
cmd = _self
|
|
if not start:
|
|
start = cmd.get_movie_length()+1
|
|
duration = float(duration)
|
|
fps = get_movie_fps(_self)
|
|
n_frame = int(round(fps * duration))
|
|
if n_frame > 0:
|
|
cmd.mset("1 x%d"%n_frame,start,freeze=1)
|
|
cmd.mview("store",start,power=1,freeze=1)
|
|
cmd.turn(axis,120)
|
|
cmd.mview("store",start+n_frame/3,power=1,freeze=1)
|
|
cmd.turn(axis,120)
|
|
cmd.mview("store",start+(2*n_frame)/3,power=1,freeze=1)
|
|
if loop:
|
|
if (start == 1):
|
|
cmd.mview("interpolate",wrap=1)
|
|
cmd.turn(axis,120)
|
|
cmd.mview("store",start+n_frame-1,power=1,freeze=1)
|
|
cmd.turn(axis,120)
|
|
else:
|
|
adjustment = 360.0/n_frame
|
|
cmd.turn(axis,120 - adjustment)
|
|
cmd.mview("store",start+n_frame-1,power=1,freeze=1)
|
|
cmd.mview("interpolate")
|
|
cmd.turn(axis,adjustment)
|
|
else:
|
|
cmd.turn(axis,120)
|
|
cmd.mview("store",start+n_frame-1,power=1,freeze=1)
|
|
cmd.mview("interpolate")
|
|
cmd.frame(start)
|
|
# PYMOL-2881
|
|
if cmd.get_setting_int('movie_auto_interpolate'):
|
|
cmd.mview("reinterpolate")
|
|
|
|
def add_rock(duration=8.0,angle=30.0,loop=1,axis='y',start=0,_self=cmd):
|
|
'''
|
|
DESCRIPTION
|
|
|
|
Append a rocking camera motion to the movie, using key frames.
|
|
|
|
ARGUMENTS
|
|
|
|
duration = float: time to add to movie in seconds {default: 8}
|
|
|
|
angle = float: degrees {default: 30}
|
|
|
|
loop = 0/1: ???
|
|
|
|
axis = x, y or z: rotation axis in camera space {default: y}
|
|
|
|
start = int: start frame (1 = first frame; 0 = end of movie) {default: 0}
|
|
'''
|
|
cmd = _self
|
|
if not start:
|
|
start = cmd.get_movie_length()+1
|
|
duration = float(duration)
|
|
angle=float(angle)
|
|
loop=int(loop)
|
|
fps = get_movie_fps(_self)
|
|
n_frame = int(round(fps * duration))
|
|
if n_frame > 0:
|
|
cmd.mset("1 x%d"%n_frame,start,freeze=1)
|
|
cmd.turn(axis,angle/2.0)
|
|
cmd.mview("store",start+n_frame/4,power=-1,freeze=1)
|
|
cmd.turn(axis,-angle)
|
|
cmd.mview("store",start+(3*n_frame)/4,power=-1,freeze=1)
|
|
if loop and (start == 1):
|
|
cmd.mview("interpolate",wrap=1)
|
|
else:
|
|
cmd.mview("interpolate")
|
|
cmd.frame(start)
|
|
|
|
def add_state_sweep(factor=1,pause=2.0,first=-1,last=-1,loop=1,start=0,_self=cmd):
|
|
cmd = _self
|
|
if not start:
|
|
start = cmd.get_movie_length() + 1
|
|
loop = int(loop)
|
|
fps = get_movie_fps(_self)
|
|
n_state = cmd.count_states()
|
|
duration = (2 * pause) + (2 * factor * n_state) / fps
|
|
n_frame = int(round(fps * duration))
|
|
if n_frame > 0:
|
|
cmd.mset("1 x%d"%n_frame, start, freeze=1)
|
|
cmd.mview("store",start, state=1, freeze=1)
|
|
cmd.mview("store",start + (n_frame * pause) / duration, state=1, freeze=1)
|
|
cmd.mview("store",start + n_state * factor + (n_frame * pause) / duration - 1, state=n_state, freeze=1)
|
|
cmd.mview("store",start + n_state * factor + (2 * n_frame * pause) / duration, state=n_state, freeze=1)
|
|
cmd.mview("store",start + n_frame-1, state=1, freeze=1)
|
|
if loop and (start == 1):
|
|
cmd.mview("interpolate",wrap=1)
|
|
else:
|
|
cmd.mview("interpolate")
|
|
cmd.frame(start)
|
|
# PYMOL-2881
|
|
if cmd.get_setting_int('movie_auto_interpolate'):
|
|
cmd.mview("reinterpolate")
|
|
|
|
def add_state_loop(factor=1,pause=2.0,first=-1,last=-1,loop=1,start=0,_self=cmd):
|
|
cmd = _self
|
|
if not start:
|
|
start = cmd.get_movie_length() + 1
|
|
loop = int(loop)
|
|
fps = get_movie_fps(_self)
|
|
n_state = cmd.count_states()
|
|
duration = (pause) + (factor * n_state) / fps
|
|
n_frame = int(round(fps * duration))
|
|
if n_frame > 0:
|
|
cmd.mset("1 x%d"%n_frame, start, freeze=1)
|
|
cmd.mview("store",start, state=1, freeze=1)
|
|
cmd.mview("store",start + (n_frame * pause * 0.5) / duration, state=1, freeze=1)
|
|
cmd.mview("store",start + n_state * factor + (n_frame * pause * 0.5) / duration, state=n_state, freeze=1)
|
|
cmd.mview("store",start + n_frame-1, state=n_state, freeze=1)
|
|
if loop and (start == 1):
|
|
cmd.mview("interpolate",wrap=1)
|
|
else:
|
|
cmd.mview("interpolate")
|
|
cmd.frame(start)
|
|
# PYMOL-2881
|
|
if cmd.get_setting_int('movie_auto_interpolate'):
|
|
cmd.mview("reinterpolate")
|
|
|
|
def add_nutate(duration=8.0, angle=30.0, spiral=0, loop=1,
|
|
offset=0, phase=0, shift=math.pi/2.0, start=0,
|
|
_self=cmd):
|
|
'''
|
|
DESCRIPTION
|
|
|
|
Append a nutating camera motion to the movie, using key frames.
|
|
|
|
ARGUMENTS
|
|
|
|
duration = float: time to add to movie in seconds {default: 8}
|
|
|
|
angle = float: degrees {default: 30}
|
|
|
|
spiral = -1/0/1: If zero, do a circular motion, otherwise do a spiral
|
|
motion starting at the center (clockwise if 1, ccw if -1) {default: 0}
|
|
|
|
loop: (unused)
|
|
|
|
offset: (unused)
|
|
|
|
phase = float: phase offset in degrees {default: 0}
|
|
|
|
shift = float: x to y offset in radians {default: pi/2}
|
|
|
|
start = int: start frame (1 = first frame; 0 = end of movie) {default: 0}
|
|
'''
|
|
cmd = _self
|
|
if not start:
|
|
start = cmd.get_movie_length()+1
|
|
duration = float(duration)
|
|
angle = float(angle)
|
|
spiral = int(spiral)
|
|
loop = int(loop)
|
|
fps = get_movie_fps(_self)
|
|
n_frame = int(round(fps * duration))
|
|
if n_frame > 0:
|
|
cmd.mset("1 x%d"%n_frame,start,freeze=1)
|
|
for index in range(0,n_frame):
|
|
if spiral>0:
|
|
sp_angle = angle*float(index+1)/n_frame
|
|
elif spiral<0:
|
|
sp_angle = angle*(1.0 - float(index+1)/n_frame)
|
|
else:
|
|
sp_angle = angle
|
|
ang_cur = math.pi*phase/180.0 + (2*math.pi*index)/n_frame
|
|
x_rot = sp_angle * math.sin(ang_cur)/2
|
|
y_rot = sp_angle * math.sin(ang_cur+shift)/2
|
|
cmd.turn('x',x_rot)
|
|
cmd.turn('y',y_rot)
|
|
cmd.mview('store',start+index,freeze=1)
|
|
cmd.turn('y',-y_rot)
|
|
cmd.turn('x',-x_rot)
|
|
# PYMOL-2881
|
|
if cmd.get_setting_int('movie_auto_interpolate'):
|
|
cmd.mview("reinterpolate")
|
|
|
|
def _rock(mode,axis,first,last,period,pause,_self=cmd):
|
|
cmd = _self
|
|
n_frame = last - first + 1
|
|
angle = cmd.get_setting_float('sweep_angle')
|
|
if (period * 1.5) < pause:
|
|
n_cyc = int(round(pause / period))
|
|
frame_list = []
|
|
for cyc in range(n_cyc):
|
|
frame_list.extend( [(first + ((1+4*cyc)*n_frame)/(4*n_cyc)),
|
|
(first + ((3+4*cyc)*n_frame)/(4*n_cyc))] )
|
|
else:
|
|
frame_list = [ first+n_frame/4, first+(3*n_frame)/4 ]
|
|
if 1 or mode:
|
|
direction = 0
|
|
for frame in frame_list:
|
|
if not direction:
|
|
cmd.turn(axis,angle/2.0)
|
|
cmd.mview("store",frame,power=-1,freeze=1)
|
|
direction = -1
|
|
else:
|
|
cmd.turn(axis, direction * angle)
|
|
cmd.mview("store",frame,power=-1,freeze=1)
|
|
direction = -direction
|
|
cmd.turn(axis,direction * angle/2.0)
|
|
cmd.mview("store",last,power=-1,freeze=1)
|
|
cmd.mview("interpolate",first,last)
|
|
|
|
def _nutate_sub(start_frame, stop_frame, angle=15.0, spiral=0, loop=1,
|
|
offset=0, phase=0, shift=math.pi/2.0, _self=cmd):
|
|
cmd = _self
|
|
angle = float(angle)
|
|
spiral = int(spiral)
|
|
loop = int(loop)
|
|
fps = get_movie_fps(_self)
|
|
duration = (stop_frame - start_frame)/fps
|
|
n_frame = int(round(fps * duration))
|
|
if n_frame > 0:
|
|
for index in range(0,n_frame):
|
|
if spiral>0:
|
|
sp_angle = angle*float(index+1)/n_frame
|
|
elif spiral<0:
|
|
sp_angle = angle*(1.0 - float(index+1)/n_frame)
|
|
else:
|
|
sp_angle = angle
|
|
ang_cur = math.pi*phase/180.0 + (2*math.pi*index)/n_frame
|
|
x_rot = sp_angle * math.sin(ang_cur)/2
|
|
y_rot = sp_angle * math.sin(ang_cur+shift)/2
|
|
cmd.turn('x',x_rot)
|
|
cmd.turn('y',y_rot)
|
|
cmd.mview('store',start_frame+index,freeze=1)
|
|
cmd.turn('y',-y_rot)
|
|
cmd.turn('x',-x_rot)
|
|
|
|
def _nutate(mode,first,last,period,pause,_self=cmd):
|
|
cmd = _self
|
|
n_frame = last - first + 1
|
|
axis = 'y'
|
|
angle = cmd.get_setting_float('sweep_angle')
|
|
if (period * 1.5) < pause:
|
|
n_cyc = int(round(pause / period))
|
|
frame_list = []
|
|
for cyc in range(n_cyc):
|
|
frame_list.append( [(first + ((cyc)*n_frame)/(n_cyc)),
|
|
(first + ((cyc+1)*n_frame)/(n_cyc))] )
|
|
else:
|
|
frame_list = [ [first, first+n_frame] ]
|
|
direction = 0
|
|
spiral = 1
|
|
for frame in frame_list:
|
|
_nutate_sub(frame[0], frame[1], angle, spiral, _self=_self)
|
|
spiral = 0
|
|
|
|
def add_scenes(names=None, pause=8.0, cut=0.0, loop=1,
|
|
rock=-1, period=8.0, animate=-1, start=0,
|
|
_self=cmd):
|
|
'''
|
|
DESCRIPTION
|
|
|
|
Append a sequence of scenes to the movie, with camera animation
|
|
(rock/nutate) at each scene.
|
|
|
|
ARGUMENTS
|
|
|
|
names = list: list of scenes names {default: all scenes}
|
|
|
|
pause = float: display time per scene in seconds {default: 8}
|
|
|
|
cut = float 0.0-1.0: scene switch moment (0.0: beginning of transition,
|
|
1.0: end of transition) {default: 0.0}
|
|
|
|
loop = 0/1: end of movie interpolates back to first frame {default: 1}
|
|
|
|
rock = int: (sweep_mode + 1): 2=x-axis-rock, 3=y-axis-rock, 4=nutate
|
|
{default: -1, use sweep_mode setting}
|
|
|
|
period = float: rock/nutate time (e.g. "period=2, pause=6" will rock 3
|
|
times per scene) {default: 8}
|
|
|
|
animate = float: scene transition time in seconds
|
|
{default: -1, use scene_animation_duration setting}
|
|
|
|
start = int: start frame (1 = first frame; 0 = end of movie) {default: 0}
|
|
'''
|
|
cmd = _self
|
|
if not start:
|
|
start = cmd.get_movie_length()+1
|
|
animate = float(animate)
|
|
pause = float(pause)
|
|
period = float(period)
|
|
if period>pause:
|
|
period = pause
|
|
rock = int(rock)
|
|
if animate<0:
|
|
animate = float(cmd.get("scene_animation_duration"))
|
|
if names is None:
|
|
names = cmd.get_scene_list()
|
|
elif cmd.is_string(names):
|
|
names = cmd.safe_alpha_list_eval(names)
|
|
n_scene = len(names)
|
|
duration = n_scene*(pause+animate)
|
|
fps = get_movie_fps(_self)
|
|
n_frame = int(round(fps * duration))
|
|
|
|
if not loop:
|
|
act_n_frame = int(round(fps * (duration - animate)))
|
|
else:
|
|
act_n_frame = n_frame
|
|
if rock<0:
|
|
sweep_mode = int(cmd.get('sweep_mode'))
|
|
elif rock:
|
|
sweep_mode = rock - 1
|
|
else:
|
|
sweep_mode = 0
|
|
if n_frame > 0:
|
|
cmd.mset("1 x%d"%act_n_frame,start,freeze=1)
|
|
cnt = 0
|
|
for scene in names:
|
|
frame = start+int((cnt*n_frame)/n_scene)
|
|
cmd.mview("store",frame,scene=scene,freeze=1)
|
|
if rock:
|
|
cmd.mview("interpolate",cut=cut,wrap=0)
|
|
sweep_first = frame + 1
|
|
sweep_last = start + int(pause*fps+(cnt*n_frame)/n_scene) - 1
|
|
if sweep_last > act_n_frame:
|
|
sweep_last = act_n_frame
|
|
n_sweep_frame = sweep_last - sweep_first + 1
|
|
if n_sweep_frame > 0:
|
|
if sweep_mode==1: # x-axis rock
|
|
_rock(sweep_mode, 'x', sweep_first, sweep_last,
|
|
period, pause, _self=_self)
|
|
elif sweep_mode<3: # y-axis rock
|
|
_rock(sweep_mode, 'y', sweep_first, sweep_last,
|
|
period, pause, _self=_self)
|
|
elif sweep_mode == 3:
|
|
_nutate(sweep_mode, sweep_first, sweep_last,
|
|
period, pause, _self=_self)
|
|
frame = start+int(pause*fps+(cnt*n_frame)/n_scene)
|
|
if frame <= act_n_frame:
|
|
if sweep_mode!=3:
|
|
cmd.mview("store",frame,scene=scene,freeze=1)
|
|
cnt = cnt + 1
|
|
cmd.mview("interpolate",cut=cut,wrap=loop)
|
|
if rock:
|
|
cmd.mview("smooth")
|
|
cmd.frame(start)
|
|
|
|
_prefix = "mov"
|
|
|
|
def _watch(filename,done_event):
|
|
tries = 5
|
|
size = 0
|
|
import time,os
|
|
while not os.path.exists(filename):
|
|
if done_event.isSet():
|
|
break
|
|
tries = tries - 1
|
|
if tries < 0:
|
|
break
|
|
time.sleep(1)
|
|
if os.path.exists(filename):
|
|
tries = 5
|
|
while os.path.exists(filename):
|
|
stat = os.stat(filename)
|
|
if size != stat[6]:
|
|
tries = 5
|
|
size = stat[6]
|
|
if done_event.isSet():
|
|
break
|
|
print(" produce: %d bytes written..."%size)
|
|
else:
|
|
tries = tries - 1
|
|
if tries < 0:
|
|
break
|
|
time.sleep(2)
|
|
if done_event.isSet():
|
|
break
|
|
|
|
def _encode(filename,first,last,preserve,
|
|
encoder,tmp_path,prefix,img_ext,quality,quiet,_self=cmd):
|
|
import os
|
|
tries = 10
|
|
while 1: # loop until all of the files have been created...
|
|
done = 1
|
|
# check for the required output files
|
|
for index in range(first,last+1):
|
|
path = os.path.join(tmp_path, "%s%04d%s" % (prefix, index, img_ext))
|
|
if not os.path.exists(path):
|
|
done = 0
|
|
break;
|
|
if done:
|
|
break
|
|
elif _self.get_modal_draw(): # keep looping so long as we're rendering...
|
|
tries = 10
|
|
else:
|
|
tries = tries - 1
|
|
if tries < 0:
|
|
done = 0
|
|
break
|
|
time.sleep(0.25)
|
|
_self.sync()
|
|
ok = 1
|
|
result = None
|
|
|
|
# reduce chance of passing non-ascii file paths to sub processes
|
|
# by changing directory
|
|
fn_rel = os.path.relpath(filename, tmp_path)
|
|
old_cwd = os.getcwd()
|
|
fps = get_movie_fps(_self)
|
|
|
|
if done and ok and (encoder == 'mpeg_encode'):
|
|
try:
|
|
from pymol import mpeg_encode
|
|
except:
|
|
ok = 0
|
|
print("produce-error: Unable to import module pymol.mpeg_encode.")
|
|
if ok:
|
|
if not mpeg_encode.validate():
|
|
ok = 0
|
|
print("produce-error: Unable to validate pymol.mpeg_encode.")
|
|
if not ok:
|
|
print("produce-error: Unable to create mpeg file.")
|
|
else:
|
|
mpeg_quality = 1+int(((100-quality)*29)/100) # 1 to 30
|
|
input = mpeg_encode.input(fn_rel, '.',
|
|
prefix,first,last,mpeg_quality);
|
|
|
|
FPS_LEGAL_VALUES = [23.976, 24, 25, 29.97, 30, 50, 59.94, 60]
|
|
fps_legal = min(FPS_LEGAL_VALUES, key=lambda v: abs(v - fps))
|
|
if fps_legal != round(fps, 3):
|
|
colorprinting.warning(
|
|
" Warning: Adjusting frame rate to {} fps (legal values are: {})"
|
|
.format(fps_legal, FPS_LEGAL_VALUES))
|
|
input = input.replace('FRAME_RATE 30',
|
|
'FRAME_RATE {:.3f}'.format(fps_legal))
|
|
|
|
if not quiet:
|
|
print(" produce: creating '%s' (in background)..."%(filename))
|
|
|
|
os.chdir(tmp_path)
|
|
|
|
done_event = None
|
|
if not quiet:
|
|
done_event = threading.Event()
|
|
_self.async_(_watch, fn_rel, done_event, _self=_self)
|
|
|
|
try:
|
|
result = mpeg_encode.run(input)
|
|
finally:
|
|
os.chdir(old_cwd)
|
|
if done_event is not None:
|
|
done_event.set()
|
|
elif encoder == 'ffmpeg':
|
|
import subprocess
|
|
os.chdir(tmp_path)
|
|
try:
|
|
if fn_rel.endswith('.gif'):
|
|
palette = os.path.join(tmp_path, 'palette.png')
|
|
filters = f"fps={fps}"
|
|
process = subprocess.Popen(['ffmpeg', '-v', 'warning', '-thread_queue_size', '512', '-i', prefix + '%04d' + img_ext, '-vf', filters+',palettegen', '-y', palette], stderr=subprocess.PIPE)
|
|
stderr = process.communicate()[1]
|
|
colorprinting.warning(stderr.strip().decode(errors='replace'))
|
|
process = subprocess.Popen(['ffmpeg', '-v', 'warning', '-thread_queue_size', '512','-i', prefix + '%04d' + img_ext, '-i', palette, '-lavfi', filters+' [x]; [x][1:v] paletteuse', '-y', fn_rel], stderr=subprocess.PIPE)
|
|
else:
|
|
args = ['ffmpeg',
|
|
'-f', 'image2',
|
|
'-framerate', '{:.3f}'.format(fps),
|
|
'-i', prefix + '%04d' + img_ext,
|
|
]
|
|
if fn_rel.endswith('.webm'):
|
|
args_crf = ['-crf', '{:.0f}'.format(65 - (quality / 2))]
|
|
args += ['-c:v', 'libvpx-vp9', '-b:v', '0'] + args_crf
|
|
else:
|
|
args += [
|
|
'-crf', '10' if quality > 90 else '15' if quality > 80 else '20',
|
|
'-pix_fmt', 'yuv420p', # needed for Mac support
|
|
]
|
|
process = subprocess.Popen(args + [fn_rel], stderr=subprocess.PIPE)
|
|
stderr = process.communicate()[1]
|
|
colorprinting.warning(stderr.strip().decode(errors='replace'))
|
|
if process.returncode != 0:
|
|
colorprinting.error('ffmpeg failed with '
|
|
'exit status {}'.format(process.returncode))
|
|
finally:
|
|
os.chdir(old_cwd)
|
|
elif encoder == 'convert':
|
|
import subprocess
|
|
exe = find_exe(encoder)
|
|
try:
|
|
subprocess.check_call([exe,
|
|
'-delay', '{:.3f}'.format(100. / fps), # framerate
|
|
os.path.join(tmp_path, prefix) + '*' + img_ext,
|
|
filename])
|
|
finally:
|
|
pass
|
|
if not quiet:
|
|
if not os.path.exists(filename):
|
|
if result is not None:
|
|
print(input, result[0], result[1])
|
|
print(" produce: compression failed")
|
|
else:
|
|
print(" produce: finished.")
|
|
_self.unset("keep_alive")
|
|
if preserve<1:
|
|
shutil.rmtree(tmp_path)
|
|
|
|
produce_mode_dict = {
|
|
'normal' : 0,
|
|
'draw' : 1,
|
|
'ray' : 2,
|
|
}
|
|
|
|
produce_mode_sc = cmd.Shortcut(produce_mode_dict.keys())
|
|
|
|
|
|
def find_exe(exe):
|
|
r'''Return full path to executable or None.
|
|
Excludes C:\Windows\System32\convert.exe
|
|
Tests .exe extension on Unix (e.g. for legacy "mpeg_encode.exe" name).
|
|
'''
|
|
from shutil import which
|
|
|
|
if exe.startswith('convert') and sys.platform == 'win32':
|
|
# filter out C:\Windows\System32
|
|
path = os.pathsep.join(p
|
|
for p in os.getenv('PATH', '').split(os.pathsep)
|
|
if r'\windows\system32' not in p.lower())
|
|
return which(exe, path=path)
|
|
|
|
if exe == 'mpeg_encode':
|
|
legacy = which(exe + '.exe')
|
|
if legacy:
|
|
return legacy
|
|
|
|
return which(exe)
|
|
|
|
|
|
def produce(filename, mode='', first=0, last=0, preserve=0,
|
|
encoder='', quality=-1, quiet=1,
|
|
width=0, height=0, _self=cmd):
|
|
'''
|
|
DESCRIPTION
|
|
|
|
Export a movie to an MPEG, WEBM, or GIF file.
|
|
|
|
Which video formats and codecs are available depends on the availability
|
|
and feature set of your ffmpeg and ImageMagick installations.
|
|
|
|
ARGUMENTS
|
|
|
|
filename = str: filename of movie file to produce
|
|
|
|
mode = draw or ray: {default: check "ray_trace_frames" setting}
|
|
|
|
first = int: first frame to export {default: 1}
|
|
|
|
last = int: last frame to export {default: last frame of movie}
|
|
|
|
preserve = 0 or 1: don't delete temporary files {default: 0}
|
|
|
|
encoder = ffmpeg|convert|mpeg_encode: Tool used for video encoding
|
|
|
|
quality = 0-100: encoding quality {default: 90 (movie_quality setting)}
|
|
|
|
width = int: Width in pixels {default: from viewport}
|
|
|
|
height = int: Height in pixels {default: from viewport}
|
|
|
|
EXAMPLE
|
|
|
|
movie.produce video.mp4, height=1080
|
|
movie.produce video.webm, height=720
|
|
'''
|
|
from pymol import CmdException
|
|
|
|
def has_exe(exe):
|
|
return bool(find_exe(exe))
|
|
|
|
prefix = _prefix
|
|
|
|
if mode == '':
|
|
mode = -1
|
|
elif _self.is_string(mode):
|
|
mode = produce_mode_sc.auto_err(mode,"mode")
|
|
mode = produce_mode_dict[mode]
|
|
else:
|
|
mode = int(mode)
|
|
first = int(first)
|
|
last = int(last)
|
|
quiet = int(quiet)
|
|
preserve = int(preserve)
|
|
quality = int(quality)
|
|
if quality<0:
|
|
quality = _self.get_setting_int('movie_quality')
|
|
if quality>100:
|
|
quality = 100
|
|
ok = 1
|
|
splitext = os.path.splitext(filename)
|
|
tmp_path = splitext[0]+".tmp"
|
|
if splitext[1]=='':
|
|
splitext=(splitext[0],'.mpg')
|
|
filename = splitext[0]+splitext[1]
|
|
width, height = int(width), int(height)
|
|
img_ext = '.png'
|
|
|
|
# guess encoder
|
|
if not encoder:
|
|
if splitext[1] in ('.mpeg', '.mpg'):
|
|
encoder = 'mpeg_encode'
|
|
elif has_exe('ffmpeg'):
|
|
encoder = 'ffmpeg'
|
|
elif has_exe('convert'):
|
|
encoder = 'convert'
|
|
else:
|
|
raise CmdException('neither "ffmpeg" nor "convert" available for '
|
|
'video encoding')
|
|
print('using encoder "%s"' % encoder)
|
|
|
|
# check encoder
|
|
if encoder == 'mpeg_encode':
|
|
img_ext = '.ppm'
|
|
elif encoder not in ('ffmpeg', 'convert'):
|
|
raise CmdException('unknown encoder "%s"' % encoder)
|
|
elif not has_exe(encoder):
|
|
raise CmdException('encoder "%s" not available' % (encoder))
|
|
|
|
if img_ext == '.png':
|
|
_self.set('opaque_background', quiet=quiet)
|
|
|
|
# MP4 needs dimensions divisible by 2
|
|
if splitext[1] in ('.mp4', '.mov', '.webm'):
|
|
if width < 1 or height < 1:
|
|
w, h = _self.get_viewport()
|
|
if width > 0:
|
|
height = width * h / w
|
|
elif height > 0:
|
|
width = height * w / h
|
|
else:
|
|
width, height = w, h
|
|
if width % 2:
|
|
width -= 1
|
|
if height % 2:
|
|
height -= 1
|
|
|
|
# clean up old files if necessary
|
|
if os.path.exists(filename):
|
|
os.unlink(filename)
|
|
if not os.path.exists(tmp_path):
|
|
os.mkdir(tmp_path)
|
|
elif preserve==0:
|
|
# get rid of existing frames (if they exist)
|
|
for fil in glob.glob(os.path.join(tmp_path,prefix+"*")):
|
|
os.unlink(fil)
|
|
if preserve<0:
|
|
preserve = 0
|
|
if os.path.isdir(tmp_path):
|
|
if first <= 0:
|
|
first = 1
|
|
if last <= 0:
|
|
last = _self.count_frames()
|
|
if last <= 1:
|
|
last = 1
|
|
_self.set("keep_alive")
|
|
_self.mpng(os.path.join(tmp_path,prefix + img_ext),first,last,
|
|
preserve,mode=mode,modal=-1,quiet=quiet,
|
|
width=width, height=height)
|
|
# this may run asynchronously
|
|
else:
|
|
ok = 0
|
|
if ok:
|
|
args = ((filename, first, last, preserve,
|
|
encoder,tmp_path,prefix,img_ext,
|
|
quality,quiet,_self))
|
|
if _self.get_modal_draw():
|
|
t = threading.Thread(target=_encode, args=args)
|
|
t.setDaemon(1)
|
|
t.start()
|
|
else:
|
|
_encode(*args)
|
|
if ok:
|
|
return _self.DEFAULT_SUCCESS
|
|
else:
|
|
return _self.DEFAULT_ERROR
|