VR portal rendering

This commit is contained in:
2022-10-09 16:53:57 +02:00
parent 29b604d769
commit 464a7e6767
7 changed files with 420 additions and 173 deletions

View File

@@ -0,0 +1,13 @@
using UnityEngine;
namespace Escape_Room_Engine.Portal
{
public class PlayerCamera : MonoBehaviour
{
public Camera camera;
[SerializeField] private Transform leftEye, rightEye;
public Transform getEyeTransform(Camera.StereoscopicEye eye) =>
eye == Camera.StereoscopicEye.Left ? leftEye : rightEye;
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: b2ceb62217b416743baa8beee7109cfb
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -1,22 +1,31 @@
using System;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.Rendering.Universal;
using UnityEngine.XR;
namespace Escape_Room_Engine.Portal
{
public class Portal : MonoBehaviour
{
private static readonly Matrix4x4 HalfRotation = Matrix4x4.Rotate(Quaternion.Euler(0, 180, 0));
private static readonly Camera.StereoscopicEye[] Eyes =
{ Camera.StereoscopicEye.Left, Camera.StereoscopicEye.Right };
private static readonly Dictionary<Camera.StereoscopicEye, int> EyeTextureNames = new()
{
{ Camera.StereoscopicEye.Left, Shader.PropertyToID("_LeftTex") },
{ Camera.StereoscopicEye.Right, Shader.PropertyToID("_RightTex") }
};
/// <summary>
/// The portal that is connected with this one.
/// </summary>
public Portal other;
/// <summary>
/// The main camera to take the position for the portal camera from.
/// The minimum near clip plane distance to be used when calculating the oblique clip plane.
/// </summary>
[SerializeField] private Camera playerCamera;
public float minNearClipPlane = 0.02f;
/// <summary>
/// The camera that will draw the view for this portal.
/// </summary>
@@ -26,24 +35,17 @@ namespace Escape_Room_Engine.Portal
/// </summary>
[SerializeField] private MeshRenderer portalQuad;
private RenderTexture _texture;
private PlayerCamera _playerCamera;
private Dictionary<Camera.StereoscopicEye, RenderTexture> _textures = new();
private void Awake()
{
// check whether the other portal is set up
if (!other || other.other != this) throw new Exception("Other Portal not set up correctly.");
// check whether the player camera is set up
if (!playerCamera) throw new Exception("No camera initialised.");
if (!other || other.other != this) throw new Exception("Other portal not set up correctly.");
// create render texture
if (!_texture) _texture = new RenderTexture(Screen.width, Screen.height, 24, RenderTextureFormat.ARGB32);
}
private void Start()
{
// assign render texture
portalCamera.targetTexture = _texture;
other.portalQuad.material.mainTexture = _texture;
// get player camera
if (Camera.main != null) _playerCamera = Camera.main.GetComponent<PlayerCamera>();
else throw new Exception("Main camera has no player camera script set up.");
}
private void OnEnable()
@@ -59,22 +61,42 @@ namespace Escape_Room_Engine.Portal
private void Render(ScriptableRenderContext scriptableRenderContext, Camera camera)
{
var t = transform;
// position portal camera
var portalCameraMatrix = t.localToWorldMatrix * HalfRotation * other.transform.worldToLocalMatrix *
playerCamera.transform.localToWorldMatrix;
portalCamera.transform.SetPositionAndRotation(portalCameraMatrix.GetPosition(), portalCameraMatrix.rotation);
// set camera clip plane to portal (otherwise the wall behind the portal would be rendered)
// calculating the clip plane: https://computergraphics.stackexchange.com/a/1506
var n = -t.forward; // clip plane normal
var portalPlane = new Plane(n, t.position); // clip plane in world space
var clipPlane = portalCamera.worldToCameraMatrix.inverse.transpose *
new Vector4(n.x, n.y, n.z, portalPlane.distance); // vector format clip plane in camera space
portalCamera.projectionMatrix = playerCamera.CalculateObliqueMatrix(clipPlane);
// render portal view
UniversalRenderPipeline.RenderSingleCamera(scriptableRenderContext, portalCamera);
foreach (var eye in Eyes)
{
// create portal texture if it doesn't exist yet
if (!_textures.ContainsKey(eye))
{
if (XRSettings.eyeTextureWidth > 0 && XRSettings.eyeTextureHeight > 0)
{
_textures.Add(eye,
new RenderTexture(XRSettings.eyeTextureWidth, XRSettings.eyeTextureHeight, 24));
other.portalQuad.material.SetTexture(EyeTextureNames[eye], _textures[eye]);
}
else // no texture was created so nothing should be rendered
continue;
}
// position portal camera
var m = t.localToWorldMatrix * HalfRotation * other.transform.worldToLocalMatrix *
_playerCamera.getEyeTransform(eye).localToWorldMatrix;
portalCamera.transform.SetPositionAndRotation(m.GetPosition(), m.rotation);
portalCamera.projectionMatrix = _playerCamera.camera.GetStereoProjectionMatrix(eye);
// set camera clip plane to portal (otherwise the wall behind the portal would be rendered)
// calculating the clip plane: https://computergraphics.stackexchange.com/a/1506
var n = -t.forward; // clip plane normal
var portalPlane = new Plane(n, t.position); // clip plane in world space
var clipPlane = portalCamera.worldToCameraMatrix.inverse.transpose *
new Vector4(n.x, n.y, n.z, portalPlane.distance); // vector format clip plane in camera space
if (Math.Abs(portalPlane.GetDistanceToPoint(portalCamera.transform.position)) >= minNearClipPlane)
// only adjust the near clip plane if it doesn't intersect with the camera
portalCamera.projectionMatrix = portalCamera.CalculateObliqueMatrix(clipPlane);
// render portal view
portalCamera.targetTexture = _textures[eye];
UniversalRenderPipeline.RenderSingleCamera(scriptableRenderContext, portalCamera);
}
}
}
}

View File

@@ -12,7 +12,7 @@ Material:
m_ValidKeywords: []
m_InvalidKeywords: []
m_LightmapFlags: 4
m_EnableInstancingVariants: 0
m_EnableInstancingVariants: 1
m_DoubleSidedGI: 0
m_CustomRenderQueue: -1
stringTagMap: {}

View File

@@ -48,7 +48,6 @@ MonoBehaviour:
m_Name:
m_EditorClassIdentifier:
other: {fileID: 0}
playerCamera: {fileID: 0}
portalCamera: {fileID: 7791795762741173939}
portalQuad: {fileID: 6216133567950691622}
--- !u!1 &6911937082098131204
@@ -239,7 +238,7 @@ MonoBehaviour:
m_StopNaN: 0
m_Dithering: 0
m_ClearDepth: 1
m_AllowXRRendering: 1
m_AllowXRRendering: 0
m_RequiresDepthTexture: 0
m_RequiresColorTexture: 0
m_Version: 2

View File

@@ -2,12 +2,13 @@ Shader "Escape Room Engine/Portal"
{
Properties
{
_MainTex("Portal Texture", 2D) = "" {}
_LeftTex("Left Eye Texture", 2D) = "" {}
_RightTex("Right Eye Texture", 2D) = "" {}
}
SubShader
{
Tags { "RenderType" = "Opaque" "RenderPipeline" = "UniversalPipeline" "Queue" = "Geometry" }
Tags { "RenderType" = "Opaque" "RenderPipeline" = "UniversalPipeline" }
Pass
{
@@ -21,33 +22,45 @@ Shader "Escape Room Engine/Portal"
HLSLPROGRAM
#pragma vertex vert
#pragma fragment frag
#pragma multi_compile_instancing
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
uniform sampler2D _MainTex;
struct Attributes
{
float4 vertex : POSITION;
float4 positionOS : POSITION;
UNITY_VERTEX_INPUT_INSTANCE_ID
};
struct Varyings
{
float4 vertex : SV_POSITION;
float4 screen : TEXCOORD0;
float4 positionHCS : SV_POSITION;
float4 positionScreen : TEXCOORD0;
UNITY_VERTEX_OUTPUT_STEREO
};
uniform sampler2D _LeftTex, _RightTex;
Varyings vert(Attributes IN)
{
Varyings OUT;
OUT.vertex = TransformObjectToHClip(IN.vertex.xyz);
OUT.screen = ComputeScreenPos(OUT.vertex);
UNITY_SETUP_INSTANCE_ID(IN);
UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(OUT);
OUT.positionHCS = TransformObjectToHClip(IN.positionOS.xyz);
// calculate the screen position which is used to map the portal texture onto the portal
OUT.positionScreen = ComputeScreenPos(OUT.positionHCS);
return OUT;
}
half4 frag(Varyings IN) : SV_Target
{
return tex2D(_MainTex, IN.screen.xy / IN.screen.w);
UNITY_SETUP_STEREO_EYE_INDEX_POST_VERTEX(IN);
const float2 uv = IN.positionScreen.xy / IN.positionScreen.w;
// sample from the correct texture depending on the eye rendered
return unity_StereoEyeIndex == 0 ? tex2D(_LeftTex, uv) : tex2D(_RightTex, uv);
}
ENDHLSL
}