Álbum Fotográfico

¡La mejor forma de aprender sobre capturas de pantalla con Unity es haciendo un álbum de fotografías en tiempo real!

Lo que aprenderás:

  • Movimiento básico por el Escenario (Cámara)
  • Tomar un Screenshot a pantalla completa
  • Guardarlo en una Textura y visualizarlo en un objeto
  • Capturar solo una parte de la pantalla
  • Capturar la pantalla cuando ocurra un evento en particular
  • Guardar el Screenshot en Disco Duro
  • Acceder al Screenshot guardado en Disco Duro
  • Compilación Condicional para múltiples plataformas
  • Crear un álbum de fotos

Movimiento de la Cámara

Primero crearemos una nueva Escena y agregaremos algunos Objetos a los que les haremos algunas capturas de pantalla, por ejemplo algunos Cubos y Esferas (A tu preferencia).

En el instante en el que tomemos un Screenshot, se guardará en una Textura la vista que nuestra Cámara esté mostrando. Como no queremos capturar siempre el mismo sitio de la Escena, vamos hacer primero que nuestra Cámara se pueda desplazar y girar por el escenario.

Para ello vamos a crear un nuevo Script en C# llamado CameraMovement.cs y se lo agregamos a nuestra Cámara quedando de esta manera:

En mi caso estoy usando Universal Render Pipeline (URP) en la configuración de este proyecto. URP agrega automáticamente un Script a todas las Cámaras llamado UniversalAdditionalCameraData, un Componente que permite trabajar con diferentes Cámaras a la vez y así crear ciertos tipos de Efectos.

Sin embargo es algo que no usaremos para nuestro proyecto de capturas de pantalla, así que podemos seguir adelante sin importar la configuración de renderizado que estemos usando.

En nuestro nuevo Script CameraMovement.cs tendremos nuestro código para el desplazamiento de nuestra Cámara, así que empezaremos agregando un desplazamiento sencillo con nuestro teclado:

[SerializeField] float speed = 5f;

Empezamos agregando una variable que llamaremos speed y será la encargada de aumentar o disminuir la velocidad con la que nos desplazaremos por el entorno.

void Update()
{
    float horizontal = Input.GetAxisRaw("Horizontal");
    float vertical = Input.GetAxisRaw("Vertical");

    Vector3 movement = speed * Time.deltaTime * new Vector3(horizontal, 0, vertical).normalized;

    transform.Translate(movement);
}

Luego, en el método Update detectamos cuando son presionadas las teclas y cambiamos la posición de nuestra Cámara de forma local.

¡Excelente! Ahora podemos empezar a movernos con las Flechas de nuestro teclado o las letras WASD. Pero, también queremos que nuestra Cámara pueda girar así que ¡Vamos a hacerlo!

Para ello continuamos editando nuestro Script de movimiento de Cámara agregándole una variable más:

[SerializeField] float sensitivity = 100f;

La variable sensitivity nos ayudará a darle una velocidad de giro a la Cámara y así poder controlarla mucho mejor.

void Update()
{
    float horizontalRotation = Input.GetAxis("Mouse X");
    float verticalRotation = Input.GetAxis("Mouse Y");

    Vector3 rotation = sensitivity * Time.deltaTime * new Vector3(-verticalRotation, horizontalRotation, 0);

    transform.eulerAngles += rotation;
}

Similar al movimiento, en el método Update ahora detectamos cuando nuestro Ratón se mueve y cambiamos la rotación de nuestra Cámara para que gire.

Quedando de esta manera:

[SerializeField] float speed = 5f;
[SerializeField] float sensitivity = 100f;

void Update()
{
    float horizontal = Input.GetAxisRaw("Horizontal");
    float vertical = Input.GetAxisRaw("Vertical");

    Vector3 movement = speed * Time.deltaTime * new Vector3(horizontal, 0, vertical).normalized;

    transform.Translate(movement);

    float horizontalRotation = Input.GetAxis("Mouse X");
    float verticalRotation = Input.GetAxis("Mouse Y");

    Vector3 rotation = sensitivity * Time.deltaTime * new Vector3(-verticalRotation, horizontalRotation, 0);

    transform.eulerAngles += rotation;
}

¡Muy bien! Ya tenemos el desplazamiento y giro para nuestra Cámara.

Refactorizando código

Podemos notar que el método Update se ha hecho un poco largo. No es muy recomendable escribir todo nuestro código junto de esta manera, ya que dificulta su entendimiento y se vuelve más complicado a la hora de solucionar cualquier problema que pueda surgir en el futuro.

Así que vamos a refactorizar nuestro código de tal manera que el movimiento y la rotación queden separados teniendo así, un código más organizado y que sea más fácil su entendimiento.

void Movement()
{
    float horizontal = Input.GetAxisRaw("Horizontal");
    float vertical = Input.GetAxisRaw("Vertical");

    Vector3 movement = speed * Time.deltaTime * new Vector3(horizontal, 0, vertical).normalized;

    transform.Translate(movement);
}

Para ello, primero crearemos un nuevo método llamado Movement y copiaremos todo el código del movimiento dentro de el.

void Rotation()
{
    float horizontal = Input.GetAxis("Mouse X");
    float vertical = Input.GetAxis("Mouse Y");

    Vector3 rotation = sensitivity * Time.deltaTime * new Vector3(-vertical, horizontal, 0);

    transform.eulerAngles += rotation;
}

Hacemos exactamente lo mismo con la rotación, esta vez copiando el código dentro del método llamado Rotation. Observa que cambié las variables horizontalRotation y verticalRotation por solo horizontal y vertical respectivamente.

void Update()
{
    Movement();
    Rotation();
}

Esto hace que el método Update ahora solo quede llamando a métodos individuales que hacen una función en particular y quede todo mas limpio.

¡Perfecto! Ya tenemos una Cámara que puede moverse y mirar a todos lados junto a un código limpio y fácil de entender. ¡Pruébalo en el Editor!

Seguro te diste cuenta que al entrar en el modo Play, la Cámara gira de repente. Esto sucede porque al momento de iniciar, si la posición del Cursor es diferente a la posición que tenia justo antes de entrar en modo Play, se detecta como un movimiento del Cursor y hace que la Cámara gire.

void ConfigCursor()
{
    Cursor.lockState = CursorLockMode.Locked;
    Cursor.visible = false;
}

Adicionalmente podemos agregar un nuevo método llamado ConfigCursor y usarlo para bloquear y ocultar nuestro Cursor cuando el escenario sea cargado.

void Start()
{
    ConfigCursor();
}

Solo es necesario ejecutar esta función una sola vez, así que lo llamaremos dentro del método Start.

Quedando todo nuestro código de la siguiente manera:

[SerializeField] float speed = 5f;
[SerializeField] float sensitivity = 100f;

void Start()
{
    ConfigCursor();
}

void Update()
{
    Movement();
    Rotation();
}

void ConfigCursor()
{
    Cursor.lockState = CursorLockMode.Locked;
    Cursor.visible = false;
}

void Rotation()
{
    float horizontal = Input.GetAxis("Mouse X");
    float vertical = Input.GetAxis("Mouse Y");

    Vector3 rotation = sensitivity * Time.deltaTime * new Vector3(-vertical, horizontal, 0);

    transform.eulerAngles += rotation;
}

void Movement()
{
    float horizontal = Input.GetAxisRaw("Horizontal");
    float vertical = Input.GetAxisRaw("Vertical");

    Vector3 movement = speed * Time.deltaTime * new Vector3(horizontal, 0, vertical).normalized;

    transform.Translate(movement);
}

Ahora nuestro Cursor ya no es visible mientras estamos en modo Play. ¡Puff Magia! No te asustes, puedes volver a salir del modo Play presionando las teclas Control + P.

¡Buen trabajo! Tenemos una Cámara con vista a todos los lados de nuestro escenario y que podremos usar libremente para tomar todos los Screenshots que queramos.

Capturando la pantalla completa

Lo siguiente que haremos será tomar el Screenshot a pantalla completa presionando el botón izquierdo de nuestro Cursor. Luego lo guardaremos en una Textura, mostrando el resultado en un nuevo Objeto.

Vamos a crear un Quad para poder cambiarle su Textura y poder ver el resultado en pantalla. Este Quad tendrá un Material de color blanco, con el Shader Unlit seleccionado. Para que no tenga en cuenta la iluminación del Escenario y podamos ver la captura de pantalla tal cual como se guardará en Disco Duro.

Quedando de la siguiente forma:

Luego, vamos a crear un nuevo Script llamado CameraScreenshot.cs y se lo asignaremos a nuestra Cámara. Este nuevo Script contendrá el funcionamiento para hacer las capturas de pantalla.

¡Excelente! Ahora continuaremos y vamos a guardar en una Textura el Screenshot a pantalla completa y mostrarla en el Quad.

[SerializeField] Renderer quadRenderer;

Crearemos una variable llamada quadRenderer, será la referencia al Quad donde mostraremos la captura de pantalla. Arrastraremos el Quad desde la Jerarquía al campo quadRenderer quedando así:

Teniendo las referencias correctas vamos a crear el código necesario para realizar el Screenshot a pantalla completa.

void Update()
{
    if (Input.GetMouseButtonDown(0))
    {
        TakeScreenshotInQuad();
    }
}

Usamos la función Update para saber cuando presionamos el botón del Ratón y llamar al método TakeScreenshotInQuad para realizar la captura de pantalla y guardarla en la Textura que usa el Quad para poder visualizarla.

void TakeScreenshotInQuad()
{
    Texture2D screenshot = new Texture2D(Screen.width, Screen.height, TextureFormat.ARGB32, false);
    screenshot.ReadPixels(new Rect(0, 0, Screen.width, Screen.height), 0, 0);
    screenshot.Apply();

    quadRenderer.material.mainTexture = screenshot;
}

Vamos a desglosar linea por linea la función TakeScreenshotInQuad para comprender mejor su funcionamiento.

Texture2D screenshot = new Texture2D(Screen.width, Screen.height, TextureFormat.ARGB32, false);

Primero creamos una nueva Textura con el mismo tamaño de nuestra pantalla y el formato de Textura por defecto que tiene Unity el ARGB32.

screenshot.ReadPixels(new Rect(0, 0, Screen.width, Screen.height), 0, 0);

Seguidamente, llamamos a la función ReadPixels para guardar todos los píxeles que estamos viendo en pantalla en la variable screenshot que acabamos de crear.

screenshot.Apply();

Una vez guardada la información en nuestra Textura. La damos a conocer a nuestra GPU para que la empiece a renderizar con la función Apply.

quadRenderer.material.mainTexture = screenshot;

Por ultimo asignamos la textura que hemos creado al Material del Quad a través de su componente Renderer.

Ahora si presionamos el botón izquierdo del Ratón veremos como no sucede nada. Pero si vemos en la consola, nos marcará el siguiente error:

Esto sucede porque estamos intentando obtener los píxeles del renderizado antes de que este haya finalizado.

Para solucionarlo, solo debemos hacer que el Screenshot se realice al final del Frame actual, justo después de que se renderice completamente.

Para ello vamos a realizar un par de cambios y esperar hasta el final del Frame para poder realizar la captura de pantalla correctamente.

void Update()
{
    if (Input.GetMouseButtonDown(0))
    {
        StartCoroutine(TakeScreenshotInQuad());
    }
}

IEnumerator TakeScreenshotInQuad()
{
    yield return new WaitForEndOfFrame();

    Texture2D screenshot = new Texture2D(Screen.width, Screen.height, TextureFormat.ARGB32, false);
    screenshot.ReadPixels(new Rect(0, 0, Screen.width, Screen.height), 0, 0);
    screenshot.Apply();

    quadRenderer.material.mainTexture = screenshot;
}

Convertirmos el método normal TakeScreenshotInQuad en una Corrutina y esperamos hasta el final del Frame con la instrucción:

yield return new WaitForEndOfFrame();

¡Eso es todo! Ya podemos tomar capturas de pantalla completa y visualizarlas en nuestro Quad.

Capturando solo una parte de la pantalla

Para que podamos capturar solo una parte de la pantalla primero tenemos que entender como funcionan los Rects, la función ReadPixels y los UVs del Quad que estamos usando.

Los Rects son rectángulos representados en pantalla a través de coordenadas 2D. Recibe un total de 4 valores en sus parámetros. La esquina superior izquierda es su punto de inicio.

Rect screenRectangle = new Rect(0, 0, Screen.width, Screen.height);

El primer par de valores, son para definir la posición del punto inicial del rectángulo (x, y) y el segundo par de valores son el ancho y alto (w, h) respectivamente que tendrá nuestro rectángulo a partir de la posición indicada (x, y).

La función ReadPixels se crea a partir de un Rect y dos valores los cuales serán en coordenadas (x, y). Su punto inicial es la esquina inferior izquierda.

exampleTexture.ReadPixels(new Rect(0, 0, Screen.width, Screen.height), 0, 0);

Indicando la posición en píxeles a partir de la cual se guardará la información capturada por el Rect en la Textura desde la que se está llamando dicho método.

Las UVs en Unity tienen el mismo punto de partida y la misma dirección que la función ReadPixels. La diferencia está en que las UVs tienen como mínimo la coordenada (0, 0) y como máximo (1, 1).

¡Perfecto! Sabiendo como funcionan. Vamos a realizar un Screenshot más pequeño que el tamaño de nuestra pantalla en el Editor y ver que es lo que sucede.

IEnumerator TakeScreenshotInQuad()
{
    yield return new WaitForEndOfFrame();

    Texture2D screenshot = new Texture2D(Screen.width, Screen.height, TextureFormat.ARGB32, false);
    Rect screenRect = new Rect(0, 0, 512, 512);

    screenshot.ReadPixels(screenRect, 0, 0);
    screenshot.Apply();

    quadRenderer.material.mainTexture = screenshot;
}

Actualicemos el método TakeScreenshotInQuad de la captura de pantalla creando una variable llamada screenRect con las coordenadas (0, 0, 512, 512) será nuestro nuevo Rect a usar en la función ReadPixels.

Vamos asegurarnos de que en la ventana Game tenemos una resolución mayor a la indicada en el Rect que creamos anteriormente, para que podamos ver que es lo que está sucediendo.

Vemos que solo la esquina inferior izquierda ¡Tiene solo una porción de nuestra pantalla! El resto del Quad se mantiene en color blanco.

Esto se debe al comportamiento que vimos antes, sobre los Rects, ReadPixels y las UVs.

Texture2D screenshot = new Texture2D(Screen.width, Screen.height, TextureFormat.ARGB32, false);

El resto del Quad, de color blanco. Se debe a que creamos una Textura del tamaño de nuestra pantalla (En mi caso en el Editor seleccioné 1920×1080 píxeles) y solo capturamos con Rect una resolución cuadrada de 512×512 píxeles.

Relación de aspecto fija

Observamos que aunque la parte que capturamos fue una resolución cuadrada, esta se ve sin proporción en nuestro Quad. Esto se debe, a que la resolución con la que creamos la Textura no fue cuadrada, si no mas rectangular (1920 x 1080) píxeles. Y al querer colocar esta Textura en un Quad cuadrado, las UVs hacen que toda la Textura se adapte a la la proporción cuadrada de nuestro Quad, perdiendo así la relación de aspecto de nuestra Screenshot.

Vamos a cambiar la Escala de nuestro Quad para que concuerde con una resolución de 1920×1080 píxeles. Para ello le daremos las siguientes medidas:

Ahora si realizamos una nueva captura de pantalla, seleccionando en nuestro Editor una resolución de 1920×1080 píxeles. Veremos como la imagen se adapta perfectamente a nuestro Quad.

Relación de aspecto dinámica

Lo siguiente será, adaptar nuestro Quad para que mantenga la relación de aspecto de cualquier resolución que seleccionemos y podamos ver correctamente nuestro Screenshot en pantalla.

float aspectRatio = Screen.width / (float)Screen.height;

En la función TakeScreenshotInQuad calculamos la relación de aspecto, dividiendo el ancho entre el alto de la resolución. En este caso estamos usando la resolución directamente de la pantalla. Y la guardamos en una variable llamada aspectRatio.

quadRenderer.transform.localScale = new Vector3(5 * aspectRatio, 5, 1);

Luego, a nuestro Quad le cambiamos su Escala para que tenga en cuenta la relación de aspecto calculada anteriormente. Los números 5 en los componentes X, Y del Vector creado, nos indica las unidades de que tan grande será el tamaño de nuestro Quad. ¡Te animo a que pruebes diferentes valores! Y puedas observar lo que sucede.

Recuerda que la relación de aspecto, es la proporción entre su ancho y alto. Nos indica cuanto de ancho hay en la altura especificada.

Por lo tanto, solo debemos multiplicar el ancho de nuestro Quad. Para que mantenga la relación con la altura dada.

Quedando el código de la siguiente forma:

IEnumerator TakeScreenshotInQuad()
{
    yield return new WaitForEndOfFrame();

    Texture2D screenshot = new Texture2D(Screen.width, Screen.height, TextureFormat.ARGB32, false);
    Rect screenRect = new Rect(0, 0, Screen.width, Screen.height);

    screenshot.ReadPixels(screenRect, 0, 0);
    screenshot.Apply();

    float aspectRatio = Screen.width / (float)Screen.height;

    quadRenderer.transform.localScale = new Vector3(5 * aspectRatio, 5, 1);
    quadRenderer.material.mainTexture = screenshot;
}

¡Excelente! Ya podemos realizar capturas de pantalla y verlas en nuestro Quad manteniendo la relación de aspecto de la resolución elegida.