Portal effect using render textures: how should I move the camera?

I’m trying to make a “portal” effect using render textures. I have two quads which are linked to each other and have their own cameras/render textures. My plan was to use some math to make the cameras rotate and shift with the player, but I’m at a loss as to how to do that. The rotation is never quite right no matter what I try.

Here’s my code, showing the closest I’ve come.

using UnityEngine;
using System.Collections;

public class PortalBehavior : MonoBehaviour
{
	private int width = 1000;
	private int height = 1000;
	private int depth = 1;

	public PortalBehavior partner;
	public Camera camera;

	public RenderTexture texture;

	//Events

	void Awake()
	{
		texture = new RenderTexture (width, height, depth);
		GetComponent<MeshRenderer> ().material.SetTexture (0, texture);

		partner.camera.targetTexture = texture;
	}

	void Update ()
	{
		RotateCamera ();

		if (Input.GetButtonDown ("Fire1")) {
			GetComponent<MeshRenderer>().enabled = !GetComponent<MeshRenderer>().enabled;
		}
	}

	//Misc methods

	private void RotateCamera()
	{
		Transform partnerCamera = partner.camera.transform;
		Transform playerCamera = Camera.main.transform;

		//Create a reference point
		Vector3 referencePoint = playerCamera.position + playerCamera.forward * 100;
		Vector3 diff = referencePoint - transform.position;
		Vector3 projectedPoint = partner.transform.position + diff;

		//Move the partner's camera
		diff = playerCamera.position - transform.position;
		partnerCamera.position = (diff * 0.1f) + partner.transform.position;

		//Rotate the partner's camera toward the projected point
		partnerCamera.LookAt (projectedPoint);
	}
}

I did this some weeks ago but the mouse is vertically locked because it was easier to test:

Like you did, each portal has it’s own camera, a reference to the other portal and a reference to the player (with the main camera attached).

To set the position of the camera for one portal I first get the player’s position but relative to the opposite portal:

Vector3 pos = oppositePortal.transform.InverseTransformPoint(player.position);

then I set the local position of the camera at the reflected point on x and z:

cam.transform.localPosition = new Vector3(-pos.x,cam.transform.localPosition.y,-pos.z);

For the rotation I get the angle between the back of the portal and the forward of the player, and set that angle as the “y” local rotation of the camera:

float angle = SignedAngle(-oppositePortal.transform.forward,player.transform.forward,Vector3.up);
cam.transform.localRotation = Quaternion.Euler(0f,angle,0f);

My SignedAngle function looks like this:

float SignedAngle(Vector3 a, Vector3 b, Vector3 n) {
	// angle in [0,180]
	float angle = Vector3.Angle(a,b);
	float sign = Mathf.Sign(Vector3.Dot(n,Vector3.Cross(a, b)));
		
	// angle in [-179,180]
	float signed_angle = angle * sign;
		
	while(signed_angle < 0) signed_angle += 360;
	
	return signed_angle;
}

This should work to get the renderTexture right, but just setting the render texture wasn’t enough to make it look OK, I had to write a custom shader:

Shader "Custom/TextureCoordinates/PortalView" {
    Properties {
      _MainTex ("Texture", 2D) = "white" {}
    }
    SubShader {
      Tags { "RenderType" = "Opaque" }
      CGPROGRAM
      #pragma surface surf Lambert
      
      struct Input {
          float4 screenPos;
      };
      
      sampler2D _MainTex;
      
      void surf (Input IN, inout SurfaceOutput o) {
          o.Albedo = tex2D (_MainTex, IN.screenPos.xy / IN.screenPos.w).rgb;
      }
      ENDCG
    } 
    Fallback "Diffuse"
}

Managed to solve it with the help of DiegoSLTS. Here is the full working code. It even works with the mouse vertically unlocked.

using UnityEngine;
using System.Collections;

public class PortalBehavior : MonoBehaviour
{
	public PortalBehavior partner;
	public Camera myCamera;

	public RenderTexture texture;

	//Events

	void Awake()
	{
		//Create the render texture
		texture = new RenderTexture (Screen.width, Screen.height, 1);
		GetComponent<MeshRenderer> ().material.SetTexture (0, texture);

		partner.myCamera.targetTexture = texture;
	}

	void Update ()
	{
		RotateCamera ();
	}

	//Misc methods

	private void RotateCamera()
	{
		Transform playerCam = Camera.main.transform;
		Transform camTrans = myCamera.transform;
		Transform partnerTrans = partner.transform;

		Vector3 cameraEuler = Vector3.zero;


		//Find the position of the camera
		Vector3 pos = partnerTrans.InverseTransformPoint (playerCam.position);
		camTrans.localPosition = new Vector3 (-pos.x, pos.y, -pos.z);


		//Find the x-rotation
		Transform prevParent = playerCam.parent;
		playerCam.SetParent (transform);

		cameraEuler.x = playerCam.localEulerAngles.x;

		playerCam.SetParent (prevParent);


		/*Find the y-rotation*/

		//Temporarily rotate the player cam so it's flat
		Vector3 oldPlayerRot = playerCam.localEulerAngles;
		playerCam.localRotation = Quaternion.Euler (0, oldPlayerRot.y, oldPlayerRot.z);

		//Use DiegoSLTS's method for finding the y-rot.
		cameraEuler.y = SignedAngle (-partnerTrans.forward, playerCam.forward, Vector3.up);

		//Restore the player cam
		playerCam.localRotation = Quaternion.Euler (oldPlayerRot);

		//Apply the rotation
		camTrans.localRotation = Quaternion.Euler (cameraEuler);

	}

	private float SignedAngle(Vector3 a, Vector3 b, Vector3 n) {
		//Code stolen from DiegoSLTS
		//http://answers.unity3d.com/questions/992289/portal-effect-using-render-textures-how-should-i-m.html

		// angle in [0,180]
		float angle = Vector3.Angle(a,b);
		float sign = Mathf.Sign(Vector3.Dot(n,Vector3.Cross(a, b)));
		
		// angle in [-179,180]
		float signed_angle = angle * sign;
		
		while(signed_angle < 0) signed_angle += 360;
		
		return signed_angle;
	}
}