- Home /
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);
}
}
Answer by DiegoSLTS · Jun 23, 2015 at 05:00 PM
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"
}
Thanks for the help! It didn't work perfectly at first, but after I made a tweak it's flawless. When you're setting the camera's local position, the y value should be set to pos.y. Here's the script, for anyone arriving here from google.
using UnityEngine;
using System.Collections;
public class PortalBehavior : $$anonymous$$onoBehaviour
{
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<$$anonymous$$eshRenderer> ().material.SetTexture (0, texture);
partner.myCamera.targetTexture = texture;
}
void Update ()
{
RotateCamera ();
}
//$$anonymous$$isc methods
private void RotateCamera()
{
Transform playerCam = Camera.main.transform;
Transform camTrans = myCamera.transform;
Transform partnerTrans = partner.transform;
//Find the position of the camera
Vector3 pos = partnerTrans.InverseTransformPoint (playerCam.position);
camTrans.localPosition = new Vector3 (-pos.x, pos.y, -pos.z);
//Find the rotation
Vector3 euler = Vector3.zero;
euler.y = SignedAngle (-partnerTrans.forward, playerCam.forward, Vector3.up);
//TODO: Find the z-rotation
camTrans.localRotation = Quaternion.Euler (euler);
}
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 = $$anonymous$$athf.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;
}
}
Now I'm going to try and make it work without the mouse vertically locked. Got any ideas?
Also, I'm curious. What exactly does your shader do? I'm not familiar with whatever language is used to make shaders.
Glad it worked, to unlock the vertical movement you probably need to set euler.x to the x rotation of the main camera, or at least that's where I would start working.
If you look at the RenderTexture generated you'll see the camera sees more than the portal area, so this shader changes what portion of the RenderTexture is displayed in the portal quad (the whole texture by default). The shader gets the pixel information of a texture, that texture is the RenderTexture generated (you can set it in the inspector). It's a bit long to explain since you need some knowledge of how the rendering pipeline and shaders work, so I don't think here is the best place to explain all that, but IN.screenPos.xy is the position in viewport space of the vertex that's being processed. The "/ IN.screenPos.w"... I'm not really sure how it works, I've seen it in an example then tryed on my code and worked, but again it's related to how 3D rendering works, with 4x4 matrix where the "w" column has some complex meaning.
For some info about shaders in Unity read this: http://docs.unity3d.com/$$anonymous$$anual/SL-SurfaceShaderExamples.html
There is one problem that I can't seem to fix. The render textures are much darker than the rest of the game, so it makes it a little bit too obvious where the portal is. Is it possible to change the brightness of the texture inside that shader you wrote?
The texture is what the camera sees, but note that any light will also affect the quad. $$anonymous$$aybe you can disable the lights on the shader replacing:
#pragma surface surf Lambert
with:
#pragma surface surf NoLighting
Also you can try setting them to not receive nor cast shadows.
Answer by falconfetus8 · Jun 23, 2015 at 08:33 PM
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;
}
}
This works really excellently. However, I'm having issues using it with the Unity 5 FPS Controller. It's inverting the Y angle of the camera (and possibly the Z angle) but otherwise it's working really well for me. How would I get it so that this angle inversion isn't happening?
Your answer
Follow this Question
Related Questions
Making Portal-Like Objects 2 Answers
Creating portal camera view with render textur 0 Answers
True Portal Effect 1 Answer
Need help on the 3ds max style camera control 0 Answers
Slowly rotating camera to face same direction as player 1 Answer