Blender-Pipeline/studio/studio_renderLayer.py

970 lines
36 KiB
Python

# THIS FILE IS A PART OF VCStudio
# PYTHON 3
import os
import datetime
import json
import time
from subprocess import *
# GTK module ( Graphical interface
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk
from gi.repository import GLib
from gi.repository import Gdk
import cairo
# Own modules
from settings import settings
from settings import talk
from settings import fileformats
from settings import oscalls
from project_manager import pm_project
#UI modules
from UI import UI_elements
from UI import UI_color
from UI import UI_math
# Studio
from studio import studio_dialogs
from studio import analytics
from studio import story
# Network / Rendering
from network import network_renders
def getnumstr(num):
# This function turns numbers like 1 or 20 into numbers like 0001 or 0020
s = ""
for i in range(4-len(str(num))):
s = s + "0"
return s+str(num)
def getfileoutput(num, FORMAT):
# Function gives an output of a file. From the current frame that's rendering.
# instead of having frame 1 and format EXR it will give you 0001.exr
s = getnumstr(num)
if FORMAT == "JPEG":
s = s + ".jpg"
else:
s = s + "." + FORMAT.lower()
return s
def save_settings(win, filename):
############################################################################
# This function will save the render settings file.
############################################################################
folder = filename[:filename.rfind("/")]+"/extra"
savefile = folder+filename[filename.rfind("/"):]+".json"
# First let's make sure that the extra folder exists.
try:
os.makedirs(win.project+folder)
except:
pass
# Then let's write the file in there.
with open(win.project+savefile, 'w') as fp:
json.dump(win.renders[filename], fp, sort_keys=True, indent=4)
def layer(win, call):
##########################################################################
# This file will setup and manage rendering of shots. It's a bit complex
# in function. I have 2 network scripts at the moment. And it might grow
# beyond that.
# See:
# network/during_render.py
# network/network_renders.py
# This file is the UI part of the process. ( The main UI ) Some beats and
# peaces will exists in various windows through out the program.
##########################################################################
# Making the layer
surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, win.current['w'],
win.current['h'])
layer = cairo.Context(surface)
#text setting
layer.select_font_face("Monospace", cairo.FONT_SLANT_NORMAL, cairo.FONT_WEIGHT_NORMAL)
UI_color.set(layer, win, "dark_overdrop")
layer.rectangle(
0,
0,
win.current["w"],
win.current["h"],
)
layer.fill()
#########################################################################
#
# THE RENDER PREVIEW THINGY
#
#########################################################################
# Here I want to do a preview that's going to be rendered on the right side
the_render = win.renders[list(win.renders.keys())[0]]
amount_of_frames = the_render["end_frame"] - the_render["start_frame"]
passed = time.time() - win.render_runtime.get("started_rendering", 0)
passed = passed*1000000
if win.render_runtime.get("to_render"):
longest = passed
else:
longest = 0
save_folder = the_render["save_folder"]
for i in the_render["analytics"][save_folder].values():
if i > longest:
longest = i
rn_files = []
for n, frame in enumerate(range(the_render["start_frame"], the_render["end_frame"])):
frame_w = (win.current["w"]-630)/amount_of_frames
frame_h = (win.current["h"]-200)/3
try:
frac = the_render["analytics"][save_folder][str(frame)] / longest
frame_h = frame_h * frac
except Exception as e:
frame_h = 0
if win.render_runtime.get("current_frame", 0) == frame and win.render_runtime.get("to_render"):
frame_h = (win.current["h"]-200)/3 * (passed / longest)
frame_h = max(frame_h, frame_w)
minus_w = 2
round_r = int((frame_w-2)/2)
if frame_w < 5:
minus_w = -1
round_r = 0
UI_color.set(layer, win, "node_background")
if win.render_runtime.get("current_frame", 0) == frame and win.render_runtime.get("to_render"):
UI_color.set(layer, win, "progress_active")
UI_elements.roundrect(layer, win,
580+(n*frame_w),
100+(win.current["h"]-200-frame_h),
frame_w-minus_w,
frame_h,
int((frame_w-2)/2))
rn_files.append(getfileoutput(frame, the_render["image_format"]))
render = list(win.renders.keys())[0]
folder = render[:render.rfind("/")]
for f in reversed(rn_files):
rn_image = win.project+folder+"/"+save_folder+"/"+f
if os.path.exists(rn_image):
UI_elements.image(layer, win, rn_image,
580,
100,
win.current["w"] - 630,
int((win.current["h"]-200)/3*1.9),
cell="render_preview")
break
if win.render_runtime.get("to_render"):
UI_color.set(layer, win, "text_normal")
layer.set_font_size(20)
layer.move_to(50,
win.current["h"]-30)
layer.show_text(win.render_runtime.get("current_progress", ""))
#########################################################################
#
# THE RENDER MENU
#
#########################################################################
UI_color.set(layer, win, "node_background")
UI_elements.roundrect(layer, win,
310-250,
100,
500,
win.current["h"]-200,
10)
# Documentation entry
def do():
def after(win, var):
pass
studio_dialogs.help(win, "help", after, SEARCH=talk.text("documentation_render"))
UI_elements.roundrect(layer, win,
310-250,
win.current["h"]-140,
40,
40,
10,
do,
"question")
is_rendering = False # This will determen whether
for render in win.renders:
if win.renders[render]["rendering"]:
is_rendering = True
if not is_rendering:
# Render button
def do():
# This here will launch a script that will be on it's on from now
# on. See:
# network/during_render.py
try:
with open(win.project+"/render_runtime.json") as json_file:
runtime = json.load(json_file)
except:
runtime = {}
runtime["to_render"] = True
with open(win.project+"/render_runtime.json", 'w') as fp:
json.dump(runtime, fp, indent=4)
Popen(["python3", "network/during_render.py", win.project, oscalls.get_current_blender(win)])
UI_elements.roundrect(layer, win,
310-20,
win.current["h"]-140,
40,
40,
10,
button=do,
icon="right")
else:
# Stop Render button
def do():
network_renders.stop_render(win)
UI_elements.roundrect(layer, win,
310-20,
win.current["h"]-140,
40,
40,
10,
button=do,
icon="stop")
def do():
os.system("gnome-terminal -- python3 "+os.getcwd()+"/network/render_viewer.py "+win.project)
UI_elements.roundrect(layer, win,
310-20-40,
win.current["h"]-140,
40,
40,
10,
button=do,
icon="analytics")
# Exit button
def do():
win.current["calls"][call]["var"] = False
UI_elements.roundrect(layer, win,
100+420,
win.current["h"]-140,
40,
40,
10,
button=do,
icon="cancel",
tip=talk.text("cancel"),
url="render")
x = 70
y = 100 + 10
width = 500 - 20
height = win.current["h"]-200 - 20
UI_elements.roundrect(layer, win,
x,
y,
width,
height-60,
10,
fill=False)
layer.clip()
clip = [
x,
y,
width,
height-60]
# Let's get the filename of the current file that we want to setup.
filename = win.current["renders_window"]["filename"]
# So this window could be accessed from both main window and from the script.
# One is to see where each render is. And ther other is to configure and add
# renders to the list.
if filename:
# Basically if any file is inputted. It means that it's to configure.
# but sometimes. ( I'm tyring to see myself using it ) the user will
# click on the render button to just access the render. And to hell with
# it. Let's make the filename variable the selection of the file.
if filename not in win.renders:
# So on the initialization we want to do a couple of things.
# First of which will be to get render settings data from the
# blend file.
# I think a quick bpy script could do.
# See:
# studio/bpy_get_render_settings.py
blenderpath = oscalls.get_current_blender(win)
blend = win.project+filename
checkframes = Popen([blenderpath, "-b", blend , "-P",
os.getcwd()+"/studio/bpy_get_render_settings.py"],stdout=PIPE, universal_newlines=True)
checkframes.wait()
checkstring = checkframes.stdout.read()
# We are going to need the following options.
start_frame = 0
end_frame = 250
image_format = "PNG"
save_folder = "storyboard"
for line in checkstring.split("\n"):
if line.startswith("Start_frame"):
try:
start_frame = int(line[line.find(":")+1:])
except:
pass
if line.startswith("End_frame"):
try:
end_frame = int(line[line.find(":")+1:])
except:
pass
# Now since we've got the data. Let's write it to the dictionary first.
win.renders[filename] = {
"start_frame" :start_frame , # The frame we want to start on
"end_frame" :end_frame , # The frame we want to end on
"image_format" :image_format, # What format to save the images
"save_folder" :save_folder , # Into what folder to save images
"clean_folder" :False , # Whether to delete current frames before rendering
"current_frame":0 , # What is current frame rendering
"rendering" :False , # Whether it's currently rendering
"analytics" :{} # Times of each frame
}
# Now in order not to loose the data immediatly. We are going to need
# to add the filename into a special file.
s = open(win.project+"/set/active_renders.data", "a")
s.write(filename+"\n")
s.close()
# Also we want to make a little json file in the extra folder of
# the shot. This will contain our settings. And the file will be
# read by the renderer script while it's running. It has to be on
# it's own. So it's good to have extendable files.
save_settings(win, filename)
# Now let's get to the actuall UI of the stuff. Basically I want it to
# always give us a list of the currently set renders. And one of them
# might be selected and expendet to see they settings / current data.
# Setting up the scroll
if "render" not in win.scroll:
win.scroll["render"] = 0
current_Y = 0
# So let's do this.
is_rendering = False # This will determen whether
# Before we dive into settings and graphs. Let's make a deletion
if 65535 in win.current["keys"] and not win.renders[win.current["renders_window"]["filename"]]["rendering"]:
try:
del win.renders[win.current["renders_window"]["filename"]]
active_renders = open(win.project+"/set/active_renders.data")
active_renders = active_renders.read()
active_renders = active_renders.split("\n")
s = open(win.project+"/set/active_renders.data", "w")
for i in active_renders:
if i != win.current["renders_window"]["filename"] and i:
s.write(i+"\n")
s.close()
except:
pass
win.current["renders_window"]["filename"] = ""
win.current["keys"] = []
for render in win.renders:
# Let's get a shot name for each render.
shot = render[:render.rfind("/")].replace("/rnd", "")
blend = render[render.rfind("/"):]
tip = (shot+blend).replace("/", "", 1).replace("/", " | ")
if win.renders[render]["rendering"]:
tip = win.renders[render]["rendering"]
def do():
win.current["renders_window"]["filename"] = render
UI_elements.roundrect(layer, win,
x,
y+current_Y+win.scroll["render"],
width,
80,
10,
button=do,
tip=tip,
clip=clip)
# I think the render logo could be cool.
UI_elements.image(layer, win,
"settings/themes/"+win.settings["Theme"]+"/icons/render.png",
x+5, y+current_Y+win.scroll["render"]+5, 40, 40)
# And the name of the shot
UI_color.set(layer, win, "text_normal")
layer.set_font_size(20)
layer.move_to(x+60,
y+current_Y + win.scroll["render"]+30)
layer.show_text((shot+blend).replace("/", "", 1).replace("/", " | "))
# And let's draw the fraction
fraction = (win.renders[render]["current_frame"] - win.renders[render]["start_frame"])\
/ (win.renders[render]["end_frame"] - win.renders[render]["start_frame"])
fraction = max(0,min(1, fraction))
if fraction:
UI_color.set(layer, win, "progress_background")
UI_elements.roundrect(layer, win,
x+20,
y+current_Y + win.scroll["render"]+55,
width-40,
0,
5)
UI_color.set(layer, win, "progress_active")
UI_elements.roundrect(layer, win,
x+20,
y+current_Y + win.scroll["render"]+55,
(width-40)*fraction,
0,
5)
# Now selection. When you click on one you expend it. And you can see
# what settings are inside.
if win.current["renders_window"]["filename"] == render:
UI_color.set(layer, win, "progress_background")
UI_elements.roundrect(layer, win,
x,
y+current_Y+win.scroll["render"],
width,
80,
10,
fill=False)
layer.stroke()
current_Y = current_Y + 90
# We are going to have 2 differnt UI's for those who are rendering
# and those who are not rendering.
if not win.renders[render]["rendering"]:
# We need to choose the folder of where the given render will be done.
# For the user tho I don't think nessesary to understand that it's a
# folder nessesary. They will see 4 circles. 4 render qualities. And
# will decide stuff based on that.
fouricons = [ "storyboard", "opengl", "test_rnd", "rendered"]
for num, f in enumerate(fouricons):
if f == win.renders[render]["save_folder"]:
UI_color.set(layer, win, "progress_time")
UI_elements.roundrect(layer, win,
x+20+(40*num),
y+current_Y + win.scroll["render"],
40,
40,
10)
def do():
win.renders[render]["save_folder"] = f
save_settings(win, render)
UI_elements.roundrect(layer, win,
x+20+(40*num),
y+current_Y + win.scroll["render"],
40,
40,
10,
button=do,
icon=f,
tip=f)
# Here also I want to have a little clean icon. This will make sure
# to clean the current frames that are currently inside the folder.
# I know it's slightly counter intuitive compared to the rest of
# the program in that it's better not to give the user the ability
# to delete any actuall files. But in this case it makes a bit
# sense. Since for the user rendering again is like replacing the
# previous files with new ones. And the algorythm always renders
# from the last frame in the folder. So it never deletes anything
# by defaul. So I guess a button to clean frames could be a good
# thing.
if win.renders[render]["clean_folder"]:
UI_color.set(layer, win, "progress_time")
UI_elements.roundrect(layer, win,
x+width-40,
y+current_Y + win.scroll["render"],
40,
40,
10)
def do():
win.renders[render]["clean_folder"] = not win.renders[render]["clean_folder"]
save_settings(win, render)
UI_elements.roundrect(layer, win,
x+width-40,
y+current_Y + win.scroll["render"],
40,
40,
10,
button=do,
icon="clean",
tip=talk.text("clean_render_folder"))
current_Y = current_Y + 50
# Now let's put the settings them selves.
# First thing is we need start and end frames. And we also need it
# somehow readable for the user.
# I will probably need this.
def is_number(string):
try:
int(string)
return True
except:
return False
# START FRAME
UI_elements.text(layer, win, render+"start_frame",
x+10,
y+current_Y+win.scroll["render"],
100,
40,
set_text=str(win.renders[render]["start_frame"]),
tip=talk.text("rendering_start_frame"))
if win.text[render+"start_frame"]["text"] != str(win.renders[render]["start_frame"])\
and is_number(win.text[render+"start_frame"]["text"]):
def do():
win.renders[render]["start_frame"] = int(win.text[render+"start_frame"]["text"])
save_settings(win, render)
UI_elements.roundrect(layer, win,
x+70,
y+current_Y+win.scroll["render"],
40,
40,
10,
button=do,
icon="ok",
tip=talk.text("checked"))
# FILE FORMATS
# I will not add any video files since the algoryhm will require
# actuall frames to be stored as separate files. This will insure
# that ALL frames were rendered. And in case of crash the algorythm
# will pick up from a previous frame in a folder. With video is not
# only impossible. But in case of a crash with video all the previous
# frames will be lost.
# What I want to do is to ejucate the user on the Open Formats.
# You know using OGG video instead of MP4 and stuff like that.
# For images there are the same types of Open Formats. The point
# is that these formats could be made and played using entirelly
# free software.
# I will let selection of formats that are not in the list but I
# will mark them as non-recommended. I will do it like so.
# [ <start frame> ] [ PNG ] [ <png frame>
# V PNG ?
# V JPEG ?
# V EXR ?
# X HDR ?
# And so on and forth. You can see that HDR is marked with an X
# it will be a button linking to the :
linfo = "http://www.linfo.org/free_file_format.html"
# so the user could read a full document describing desisions
# about the formats.
formats = {
"PNG" : [True, "Image PNG", "https://en.wikipedia.org/wiki/Portable_Network_Graphics"],
"JPEG": [True, "Image JPEG", "https://en.wikipedia.org/wiki/JPEG"],
"EXR" : [True, "Open EXR", "https://en.wikipedia.org/wiki/OpenEXR"],
"HDR" : [False,"Radiance HDR", "https://en.wikipedia.org/wiki/RGBE_image_format"],
"BMP" : [False,"Microsoft BMP", "https://en.wikipedia.org/wiki/BMP_file_format"],
"TGA" : [False,"Truevision TGA", "https://en.wikipedia.org/wiki/Truevision_TGA"],
"TIFF": [False,"Tiff", "https://en.wikipedia.org/wiki/Tagged_Image_File_Format"]
}
if "selecting_render_file_format" not in win.current:
win.current["selecting_render_file_format"] = False
def do():
win.current["selecting_render_file_format"] = not win.current["selecting_render_file_format"]
if win.current["selecting_render_file_format"]:
win.scroll["render"] = win.scroll["render"]-50*len(formats)
win.current["LMB"] = False
win.previous["LMB"] = False
UI_elements.roundrect(layer, win,
x+120,
y+current_Y + win.scroll["render"],
235,
40,
10,
button=do,
tip=talk.text("rendering_file_format"))
currentformat = win.renders[render]["image_format"]
if not win.current["selecting_render_file_format"]:
UI_color.set(layer, win, "text_normal")
layer.set_font_size(20)
layer.move_to(310-len(formats[currentformat][1])*6,
y+current_Y + win.scroll["render"]+30)
layer.show_text(formats[currentformat][1])
# END FRAME
UI_elements.text(layer, win, render+"end_frame",
x+365,
y+current_Y+win.scroll["render"],
100,
40,
set_text=str(win.renders[render]["end_frame"]),
tip=talk.text("rendering_end_frame"))
if win.text[render+"end_frame"]["text"] != str(win.renders[render]["end_frame"])\
and is_number(win.text[render+"end_frame"]["text"]):
def do():
win.renders[render]["end_frame"] = int(win.text[render+"end_frame"]["text"])
save_settings(win, render)
UI_elements.roundrect(layer, win,
x+215+210,
y+current_Y+win.scroll["render"],
40,
40,
10,
button=do,
icon="ok",
tip=talk.text("checked"))
current_Y = current_Y + 50
if win.current["selecting_render_file_format"]:
for num, f in enumerate(formats):
if f == win.renders[render]["image_format"]:
UI_color.set(layer, win, "progress_time")
UI_elements.roundrect(layer, win,
x+120,
y+current_Y + win.scroll["render"],
235,
40,
10)
def do():
win.renders[render]["image_format"] = f
save_settings(win, render)
win.current["selecting_render_file_format"] = False
UI_elements.roundrect(layer, win,
x+120,
y+current_Y + win.scroll["render"],
235,
40,
10,
button=do)
UI_color.set(layer, win, "text_normal")
layer.set_font_size(20)
layer.move_to(310-len(formats[f][1])*6,
y+current_Y + win.scroll["render"]+30)
layer.show_text(formats[f][1])
# RECCOMENDATION
if formats[f][0]:
rec = talk.text("recommended_yes")
ic = "ok"
else:
rec = talk.text("recommended_not")
ic = "cancel"
def do():
os.system("xdg-open "+linfo)
UI_elements.roundrect(layer, win,
x+10,
y+current_Y + win.scroll["render"],
40,
40,
10,
button=do,
icon=ic,
tip=rec)
# WIKIPEDIA ABOUT THE FORMAT
def do():
os.system("xdg-open "+formats[f][-1])
UI_elements.roundrect(layer, win,
x+430,
y+current_Y + win.scroll["render"],
40,
40,
10,
button=do,
icon="question",
tip="Wikipedia")
current_Y = current_Y + 50
else:
# And here comes the UI of when it's during RENDERING
# First thing will be to draw a little graph. This will show current
# frame and analytics data about render times.
UI_color.set(layer, win, "dark_overdrop")
layer.rectangle(
x+5,
y+current_Y+win.scroll["render"],
width-10,
100)
layer.fill()
for frame in range(win.renders[render]["start_frame"], win.renders[render]["end_frame"]+1):
numofis = win.renders[render]["end_frame"] - win.renders[render]["start_frame"]
if frame == win.renders[render]["current_frame"]:
UI_color.set(layer, win, "progress_time")
layer.rectangle(
x+5+(width-10)/numofis*(frame-1),
y+current_Y+win.scroll["render"],
(width-10)/numofis,
100)
layer.fill()
# Now I want to be able to interact with the graph. For this I want to
# add a little mouse sensitive region. That will give me data about the
# current frame. In a tooltip?
if int(win.current["mx"]) in range(int(x+5+(width-10)/numofis*frame),int(x+5+(width-10)/numofis*frame+(width-10)/numofis))\
and int(win.current["my"]) in range(int(y+current_Y+win.scroll["render"]), int(y+current_Y+win.scroll["render"]+100)):
UI_color.set(layer, win, "progress_background")
layer.move_to(x+5+(width-10)/numofis*(frame-0.5),
y+current_Y+win.scroll["render"]
)
layer.line_to(
x+5+(width-10)/numofis*(frame-0.5),
y+current_Y+win.scroll["render"]+100
)
layer.stroke()
if win.renders[render]["save_folder"] in win.renders[render]["analytics"]:
if str(frame) in win.renders[render]["analytics"][win.renders[render]["save_folder"]]:
value = win.renders[render]["analytics"][win.renders[render]["save_folder"]][str(frame)]
UI_elements.tooltip(win, UI_math.timestring(value/1000000))
# Now let's draw a graph. I love graphs. They are cool AF. First of all tho
# we need to know the maximum value. Because who know how long was the render
mx = 0
allvalues = []
if win.renders[render]["save_folder"] in win.renders[render]["analytics"]:
for v in win.renders[render]["analytics"][win.renders[render]["save_folder"]]:
mx = max(win.renders[render]["analytics"][win.renders[render]["save_folder"]][v], mx)
allvalues.append(win.renders[render]["analytics"][win.renders[render]["save_folder"]][v])
UI_color.set(layer, win, "progress_background")
layer.move_to(x+5, y+current_Y+win.scroll["render"]+100)
for frame in range(win.renders[render]["start_frame"], win.renders[render]["end_frame"]+1):
numofis = win.renders[render]["end_frame"] - win.renders[render]["start_frame"]
if win.renders[render]["save_folder"] in win.renders[render]["analytics"]:
if str(frame) in win.renders[render]["analytics"][win.renders[render]["save_folder"]]:
value = win.renders[render]["analytics"][win.renders[render]["save_folder"]][str(frame)]
layer.line_to(
x+5+(width-10)/numofis*(frame-0.5),
(y+current_Y+win.scroll["render"]+100)-(100/mx*value)
)
layer.stroke()
current_Y = current_Y + 110
# Now let's put out some data in the text format. For the user to
# know stuff he or she might want to know.
# AVARAGE TIME
avarage = 0
try:
avarage = sum(allvalues) / len(allvalues)
except:
pass
UI_color.set(layer, win, "text_normal")
layer.set_font_size(20)
layer.move_to(x+10,
y+current_Y + win.scroll["render"]+30)
layer.show_text(talk.text("render_avarage_time")+" "+UI_math.timestring(avarage/1000000))
current_Y = current_Y + 40
# REMAINING TIME
remaining = avarage * (win.renders[render]["end_frame"] - win.renders[render]["current_frame"])
UI_color.set(layer, win, "text_normal")
layer.set_font_size(20)
layer.move_to(x+10,
y+current_Y + win.scroll["render"]+30)
layer.show_text(talk.text("render_remaining_time")+" "+UI_math.timestring(remaining/1000000))
current_Y = current_Y + 40
else:
current_Y = current_Y + 85
###########################
UI_elements.scroll_area(layer, win, "render",
x,
y,
width,
height-60,
current_Y,
bar=True,
mmb=True,
url="render"
)
return surface