############################################################
# Blender Python Script to setup an 'Off-Axis' Stereo Camera
#                     
# to run this script press:   Alt + P
# 
# Info: http://www.noeol.de/s3d
# (c) 2009 Sebastian Schneider
############################################################



import Blender
from Blender import *
from Blender.Draw import *
from Blender.BGL import *
from Blender.Scene import Render
import math

from Blender.Mesh import Primitives


#
# GUI
#
def gui():

	glClearColor(0.5, 0.5, 0.5, 0.0)
	glClear(GL_COLOR_BUFFER_BIT)

	# gui background colours	
	glColor3f(0.65, 0.65, 0.65) # medium grey 
	glRectf(5, 5, 405, 500) # background all

	glColor3f(0.75, 0.75, 0.75)  # light grey
	glRectf(5, 23, 405, 469) # content background

	glColor3f(0.85, 0.85, 0.85) # white (almost)
	glRectf(196, 310, 393, 290) # focal distance result bg
	glRectf(196, 280, 393, 260) # cam separation in blender units result bg
	glRectf(196, 250, 393, 230) # result delta in pixel bg
	glRectf(196, 220, 393, 200) # result shiftX left bg
	glRectf(196, 190, 393, 170) # result shiftX right bg

	# exit
	Button("Exit",1,338,475,60,19) 
	
	# script name
	glColor3f(1, 1, 1)	
	glRasterPos2d(15, 479)
	Text("BStereoOffAxisCamera v.0.4 (ShiftX Version)")
	
	# imput fields
	DATA_DICT['CAM_SEPARATION'] = Number("Camera Separation: ", 2, 15, 430, 380, 20, DATA_DICT['CAM_SEPARATION'].val, 1, 10000, "Distance between camera lenses'. To connect this value to the real world, it is set in mm (1/1000 meter)") #0-100m
	DATA_DICT['MAX_DEVIATION'] = Number("Deviation Rule: 1/", 3, 15, 400, 380, 20, DATA_DICT['MAX_DEVIATION'].val, 1, 100, "Simple stereoscopic rule (Camera Separation should be 1/30 of the distance to the projection plane with zero parallax)")
	DATA_DICT['USE_THIS_FO_DIST'] = Toggle("use this:", 9, 15, 370, 60, 20, DATA_DICT['USE_THIS_FO_DIST'].val, "Toggle this on, to set your own distance between camera and projection plane with zero parallax (Deviation rule will be disabled)")
	
	min_fo_dist = 0.01 # TODO
	DATA_DICT['OWN_FOCAL_DIST'] = Number("Focal Distance (in B.U.): ", 5, 75, 370, 320, 20, DATA_DICT['OWN_FOCAL_DIST'].val, min_fo_dist, 1000.00, "Distance from the camera to the projection plane with zero parallax")
	
	# calculate
	Button("Calculate",8,196,330,198,20,"Only calculate the stereo parameter")
		
	# result fo
	glColor3f(0, 0, 0)	
	glRasterPos2d(15, 297)	
	Text(DATA_DICT['FOCAL_DISTANCE'].val)	
		
	# show zero parallax plane
	DATA_DICT['ZERO_PLANE'] = Toggle("show it", 7, 332, 290, 60, 20, DATA_DICT['ZERO_PLANE'].val, "Shows the zero parallax as a plane")	
		
	# result cam separation
	glRasterPos2d(77, 267)	
	Text(DATA_DICT['CAM_SEPAR_BU'].val)
	
	# show the left and right camera to visualize the stereo base
	DATA_DICT['SHOW_BASE'] = Toggle("show it", 15, 332, 261, 60, 20, DATA_DICT['SHOW_BASE'].val, "Shows the left and right camera to visualize the stereo base (after press 'Set Stereo Camera')")
	
	# result delta (shiftX) in pixel
	glRasterPos2d(110, 237)	
	Text(DATA_DICT['DELTA_IN_PIXEL'].val)
	
	# result shiftX reft
	glRasterPos2d(90, 207)	
	Text(DATA_DICT['SHIFT_X_LEFT'].val)	
	
	# result shiftX right
	glRasterPos2d(82, 177)	
	Text(DATA_DICT['SHIFT_X_RIGHT'].val)		
	
	# set left,center or right camera as the active one in this scene (default center)
	#Button("ActiveCam",16,15,42,70,20, "Set the selected camera to the active camera in this scene")
		
	# link objects
	DATA_DICT['LINK_OBJECTS'] = Toggle("link objects", 11, 102, 126, 80, 20, DATA_DICT['LINK_OBJECTS'].val, "Links all objects to a left and right camera scene")
	
	# set stereo camera
	Button("Set Stereo Camera",10,196,124,196,24, "Calculate and set the stereo parameter for the active camera in this scene")
	
	# info
	glColor3f(0.791, 0.023, 0.023)
	glRasterPos2d(108, 108)	
	Text(DATA_DICT['WRONG_CAM_1'].val,'small')
	
	# about this script
	glColor3f(0.0, 0.0, 0.0)
	glRasterPos2d(108, 78)
	Text("This python script is experimental.")
	glRasterPos2d(80, 58)
	Text("Vers. 1.x is more accurate. Visit my website.")
		
	# weblink
	glColor3f(1, 1, 1)
	glRasterPos2d(15, 11)	
	Text("(c) 2009 www.noeol.de/s3d")		



#
# Off-Axis calculation methode
#
def stereo_calculation():
	
	global delta, fo, stereobase, distScale, camFov, renderWidth, shift_x_left, shift_x_right
	
	# get the current scene
	scn = Scene.GetCurrent()
	
	# get the fov of the current camera
	curCam = scn.objects.camera
	curCamData = curCam.getData()	
	camFov = curCamData.angle
	
	# get the current render width
	renderContext = scn.getRenderingContext()
	renderWidth = renderContext.imageSizeX() 
	
	# scale (this is a TODO)
	distScale = 1
	camScale = 1000
	
	# focal distance (zero parallax)	
	if(DATA_DICT['USE_THIS_FO_DIST'].val == 1):
		fo = DATA_DICT['OWN_FOCAL_DIST'].val/distScale
	else:
		fo = (float(DATA_DICT['CAM_SEPARATION'].val)/1000)*DATA_DICT['MAX_DEVIATION'].val
	
	DATA_DICT['FOCAL_DISTANCE'].val = "Focal Distance (zero parallax) = "+str(round(fo*distScale,2))+" B.U."
	
	# cam separation 'stereobase' in blender units
	stereobase = float(DATA_DICT['CAM_SEPARATION'].val)/camScale
	DATA_DICT['CAM_SEPAR_BU'].val = "Camera Separation = "+str(stereobase)+" B.U."
		
	#	
	# delta
	#
	delta = ( renderWidth * (float(DATA_DICT['CAM_SEPARATION'].val)/1000) ) / ( 2 * fo * math.tan(math.radians(camFov / 2)) )
	delta = int(round(delta)) 
	DATA_DICT['DELTA_IN_PIXEL'].val = "Delta in Pixel = "+str(delta)

	#
	# shift
	#
	shift_x_left = ((float(delta)/float(renderWidth))/2)
	shift_x_right = -(shift_x_left)
	DATA_DICT['SHIFT_X_LEFT'].val = "Left Image ShiftX = "+str(shift_x_left)
	DATA_DICT['SHIFT_X_RIGHT'].val = "Right Image ShiftX = "+str(shift_x_right)

	# draw it
	Redraw()


	
#
# Configure the stereo camera rig
#
def set_stereo_camera():
	
	scn = Scene.GetCurrent()
	
	# get the current camera
	curCam = scn.objects.camera
	curCamData = curCam.getData()
	
	# check if the current(active) cam is a center/normal cam and not a left or right cam (set by this script)
	if(curCam.name[:4]=="Left" or curCam.name[:5]=="Right"):
		
		# info for the user
		DATA_DICT['WRONG_CAM_1'].val = "Your active camera is already a Left- or Right Stereo Camera."
	
	else:
		
		#else do the settings
		DATA_DICT['WRONG_CAM_1'].val = " "
		
		# save the cameraData
		oldCamLocation = curCam.loc
		oldCamRotationX = curCam.RotX
		oldCamRotationY = curCam.RotY
		oldCamRotationZ = curCam.RotZ
		oldCamSize = curCam.size
		
		# delete a camera track-to constraint
		CamConstraints = curCam.constraints
		CamConstLen = CamConstraints.__len__()
		if(CamConstLen > 0):
			for	CamConst in CamConstraints:
				if(CamConst.type == Constraint.Type.TRACKTO):
					curCam.constraints.remove(CamConst)
						
		# check for a left or right camera and a zero parallax empty (set by this script) 
		leftCamExists = 0
		rightCamExists = 0
		zeroEmptyExists = 0
		zeroPlaneExists = 0
		camEmptyExists = 0
		for ob in scn.objects:
			if(ob.name == "Left"+curCam.name):
				leftCamExists = 1
			if(ob.name == "Right"+curCam.name):
				rightCamExists = 1
			if(ob.name == curCam.name+"ZeroParallax"):
				zeroEmptyExists = 1
			if(ob.name == curCam.name+"ZeroPlane"):
				zeroPlaneExists = 1
			if(ob.name == curCam.name+"StereoCenter"):
				camEmptyExists = 1
			
		# add a new left camera (or not)
		if(leftCamExists==0):
			c = Camera.New("persp")
			leftCam = Object.New("Camera", "Left"+curCam.name)
			leftCam.link(c)
			scn.objects.link(leftCam)
			leftCamData = leftCam.getData()
		else:
			leftCam = Blender.Object.Get("Left"+curCam.name)
			leftCamData = leftCam.getData()
		
		# add a new right camera (or not)
		if(rightCamExists==0):
			c = Camera.New("persp")
			rightCam = Object.New("Camera", "Right"+curCam.name)
			rightCam.link(c)
			scn.objects.link(rightCam)
			rightCamData = rightCam.getData()
		else:
			rightCam = Blender.Object.Get("Right"+curCam.name)
			rightCamData = rightCam.getData()
	
		# add/get a empty to show the zero parallax distance
		if(zeroEmptyExists==0):
			zeroEmpty = Object.New("Empty", curCam.name+"ZeroParallax")
			scn.objects.link(zeroEmpty)
		else:
			zeroEmpty = Blender.Object.Get(curCam.name+"ZeroParallax")
	
		# add/get a plane to show zero parallax distance
		if(zeroPlaneExists==0):
			zeroPlane_prim = Primitives.Plane(4.0)
			zeroPlane = Object.New("Mesh", curCam.name+"ZeroPlane")
			zeroPlane.link(zeroPlane_prim)
			scn.objects.link(zeroPlane)
		else:
			zeroPlane = Blender.Object.Get(curCam.name+"ZeroPlane")

		# add/get an empty for the current (activ) camera, this is an 'IPO' bugfix [18.Aug.09]
		if(camEmptyExists==0):
			camEmpty = Object.New("Empty", curCam.name+"StereoCenter")
			scn.objects.link(camEmpty)
		else:
			camEmpty = Blender.Object.Get(curCam.name+"StereoCenter")

		# before copy the scene, make the zero parallax plane invisible
		zeroPlane.layers = [] #invisible 
		
		# make this helper empty invisible too
		camEmpty.layers = []
			
		# translate/rotate/scale at scene orign
		camEmpty.loc = (0,0,0)
		camEmpty.rot = (0,0,0)
		camEmpty.size = (1,1,1)
		
		curCam.loc = (0,0,0)
		curCam.rot = (0,0,0)
		curCam.size = (1,1,1)
		
		leftCam.loc = (-(stereobase/2),0,0)
		leftCam.rot = (0,0,0)
		leftCam.size = (1,1,1)
		
		rightCam.loc = ((stereobase/2),0,0)
		rightCam.rot = (0,0,0)
		rightCam.size = (1,1,1)
		
		zeroEmpty.loc = (0,0,-(round(fo*distScale,2)))
		zeroEmpty.rot = (0,0,0)
		zeroEmpty.size = (1,1,1)
	
		zeroPlane.loc = (0,0,-(round(fo*distScale,2)))
		zeroPlane.rot = (0,0,0)
	
		# set the fov's
		curCamData.angle = camFov
		leftCamData.angle = camFov
		rightCamData.angle = camFov
		
		# shift the left and right image horizontal 
		leftCamData.shiftX = shift_x_left
		rightCamData.shiftX = shift_x_right		
	
		# check for a active camera with IPO [Bugfix]
		curCamIpo = curCam.getIpo()
		if(curCamIpo != None):
			#curCam has an IPO:		
			scn.update(1)				
			camEmpty.makeParent([leftCam, rightCam, zeroEmpty, zeroPlane], 0, 0)
			
			camEmpty.loc = oldCamLocation
			camEmpty.RotX = oldCamRotationX
			camEmpty.RotY = oldCamRotationY
			camEmpty.RotZ = oldCamRotationZ
			camEmpty.size = oldCamSize
				
			curCam.makeParent([camEmpty], 0, 0)
			
		else:
			#curCam has no IPO:
			scn.update(1)
			curCam.makeParent([leftCam, rightCam, zeroEmpty, zeroPlane, camEmpty], 0, 0)
				
			# reset original Loc/Rot/Size
			curCam.loc = oldCamLocation
			curCam.RotX = oldCamRotationX
			curCam.RotY = oldCamRotationY
			curCam.RotZ = oldCamRotationZ
			curCam.size = oldCamSize

		#
		# make (or get) the left/right Blender Scene and link all objects 
		#
		if(DATA_DICT['LINK_OBJECTS'].val == 1):
			# add or load left and right scene		
			try:
				scnRight = Scene.Get('Right_Stereo_Cam')
			except:
				scnRight = scn.copy(0)
				scnRight.setName('Right_Stereo_Cam')
		
			try:
				scnLeft = Scene.Get('Left_Stereo_Cam')
			except:
				scnLeft = scn.copy(0)
				scnLeft.setName('Left_Stereo_Cam')

			# link all objects
			for obj in scn.objects:
				try:
					scnRight.link(obj)
					scnLeft.link(obj)
				except:
					print 'object already in scene'	
		
			# set the active cam in the left/right scenes			
			for obj in scnRight.objects:
				if(obj.name=="Right"+curCam.name):
					#scnRight.setCurrentCamera(obj)
					scnRight.objects.camera = obj

			for obj in scnLeft.objects:		
				if(obj.name=="Left"+curCam.name):
					#scnLeft.setCurrentCamera(obj)
					scnLeft.objects.camera = obj
		
					
		# make the zero parallax plane visible in the curCam layers or not
		if(DATA_DICT['ZERO_PLANE'].val==1):
			zeroPlane.layers = curCam.layers
		else:
			zeroPlane.layers = [] #invisible

		# hide/unhide the left and right camera in the main scene, (only the center camera (curCam) should be visible)
		if(DATA_DICT['SHOW_BASE'].val==0):
			leftCam.layers = []
			rightCam.layers = []
			camEmpty.layers = []
		else:
			leftCam.layers = curCam.layers
			rightCam.layers = curCam.layers
			camEmpty.layers = curCam.layers


	# redraw the 3d window too
	Window.RedrawAll() 

	# draw it
	Redraw()



#
# exit event handler 
#
def event(evt, val):
	if(evt == QKEY and not val):
		Exit()



#
# button event handler
#		
def bevent(evt):
	if(evt == 1):
		Exit()
	elif(evt == 8):
		stereo_calculation()
	elif(evt == 10):
		stereo_calculation()
		set_stereo_camera()
		set_stereo_camera() # ToDo: Oohh this is bad. Fix it!
	elif(evt == 16):
		scn = Scene.GetCurrent()
		selectedObj = Object.GetSelected()
		scn.objects.camera = selectedObj[0]		

	# draw it			
	Redraw()		

		

#
# default values and text
#	
DATA_DICT = {
	'CAM_SEPARATION': Create(65),
	'MAX_DEVIATION': Create(30),
	'FOCAL_DISTANCE': Create("Focal Distance (zero parallax) = "),
	'CAM_SEPAR_BU': Create("Camera Separation = "),
	'DELTA_IN_PIXEL': Create("Delta in Pixel = "),
	'SHIFT_X_LEFT': Create("Left Image ShiftX = "),
	'SHIFT_X_RIGHT': Create("Right Image ShiftX = "),
	'WRONG_CAM_1': Create(" "),
	'USE_THIS_FO_DIST': Create(0),
	'OWN_FOCAL_DIST': Create(0.0),
	'LINK_OBJECTS': Create(1),
	'ZERO_PLANE': Create(0),
	'SHOW_BASE': Create(0)
       }



#
# register all
#		
Register(gui, event, bevent)