render stencil portals (opaque)

This commit is contained in:
2023-03-03 16:25:24 +01:00
parent 5e911f2df1
commit 6d2b5c52a1
16 changed files with 781 additions and 947 deletions

View File

@@ -4,7 +4,6 @@ using EscapeRoomEngine.Engine.Runtime.Modules;
using EscapeRoomEngine.Engine.Runtime.Utilities;
using NaughtyAttributes;
using UnityEngine;
using UnityEngine.Serialization;
namespace EscapeRoomEngine.Portal.Runtime
{
@@ -23,9 +22,13 @@ namespace EscapeRoomEngine.Portal.Runtime
/// </summary>
public Portal linkedPortal;
/// <summary>
/// The camera that will draw the view for this portal.
/// The minimum near clip plane distance to be used when calculating the oblique clip plane.
/// </summary>
[BoxGroup("Internal")] public PortalCamera portalCamera;
public float minNearClipPlane = 0.0001f;
/// <summary>
/// The mesh where the portal will be drawn.
/// </summary>
[BoxGroup("Internal")] public MeshRenderer screen;
/// <summary>
/// The transform marking the edge of the portal plane.
/// </summary>
@@ -51,12 +54,10 @@ namespace EscapeRoomEngine.Portal.Runtime
{
case DoorEventType.Connected:
linkedPortal = FromDoorState(Module.ConnectedDoorState);
portalCamera.screen.gameObject.SetActive(true);
portalCamera.enabled = true;
screen.gameObject.SetActive(true);
break;
case DoorEventType.Locked:
portalCamera.enabled = false;
portalCamera.screen.gameObject.SetActive(false);
screen.gameObject.SetActive(false);
linkedPortal = null;
break;
}
@@ -85,6 +86,31 @@ namespace EscapeRoomEngine.Portal.Runtime
}
}
public Camera SetUpCamera(Camera playerCamera)
{
// place camera
var m = portalTransform.localToWorldMatrix * HalfRotation *
linkedPortal.portalTransform.worldToLocalMatrix * playerCamera.transform.localToWorldMatrix;
playerCamera.transform.SetPositionAndRotation(m.GetPosition(), m.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
// clip plane normal
var n = -portalTransform.forward;
// clip plane in world space
var portalPlane = new Plane(n, portalTransform.position);
if (-portalPlane.GetDistanceToPoint(playerCamera.transform.position) >= minNearClipPlane)
{
// vector format clip plane in camera space
var clipPlane = playerCamera.worldToCameraMatrix.inverse.transpose *
new Vector4(n.x, n.y, n.z, portalPlane.distance);
// only adjust the near clip plane if it doesn't intersect with the camera
playerCamera.projectionMatrix = playerCamera.CalculateObliqueMatrix(clipPlane);
}
return playerCamera;
}
/// <summary>
/// Begin tracking a portal driver that came close to this portal and might need to be teleported.
/// </summary>

View File

@@ -1,105 +0,0 @@
using System.Collections.Generic;
using EscapeRoomEngine.VR.Runtime;
using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.Rendering.Universal;
using UnityEngine.XR;
namespace EscapeRoomEngine.Portal.Runtime
{
[RequireComponent(typeof(Camera))]
public class PortalCamera : MonoBehaviour
{
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 minimum near clip plane distance to be used when calculating the oblique clip plane.
/// </summary>
public float minNearClipPlane = 0.0001f;
/// <summary>
/// The portal this camera renders through.
/// </summary>
[SerializeField] private Portal portal;
/// <summary>
/// The mesh where the rendered texture will be drawn on.
/// </summary>
[SerializeField] public MeshRenderer screen;
private Camera _camera;
private readonly Dictionary<Camera.StereoscopicEye, RenderTexture> _textures = new();
private void Awake()
{
// get portal camera
_camera = GetComponent<Camera>();
}
private void OnEnable()
{
_camera.enabled = true;
RenderPipelineManager.beginCameraRendering += Render;
}
private void OnDisable()
{
RenderPipelineManager.beginCameraRendering -= Render;
_camera.enabled = false;
}
private void Render(ScriptableRenderContext scriptableRenderContext, Camera _)
{
// check whether the portal plane is visible from the player camera
var frustumPlanes = GeometryUtility.CalculateFrustumPlanes(Player.Instance.camera);
if (!GeometryUtility.TestPlanesAABB(frustumPlanes, portal.linkedPortal.portalCamera.screen.bounds))
// don't render this portal if it is not visible
return;
screen.enabled = false;
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));
portal.linkedPortal.portalCamera.screen.material.SetTexture(EyeTextureNames[eye], _textures[eye]);
}
else // no texture was created so nothing should be rendered
continue;
}
// position portal camera
var m = portal.portalTransform.localToWorldMatrix * Portal.HalfRotation *
portal.linkedPortal.portalTransform.worldToLocalMatrix *
Player.Instance.GetEye(eye).localToWorldMatrix;
transform.SetPositionAndRotation(m.GetPosition(), m.rotation);
_camera.projectionMatrix = Player.Instance.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 = -portal.portalTransform.forward; // clip plane normal
var portalPlane = new Plane(n, portal.portalTransform.position); // clip plane in world space
var clipPlane = _camera.worldToCameraMatrix.inverse.transpose *
new Vector4(n.x, n.y, n.z, portalPlane.distance); // vector format clip plane in camera space
if (-portalPlane.GetDistanceToPoint(transform.position) >= minNearClipPlane)
// only adjust the near clip plane if it doesn't intersect with the camera
_camera.projectionMatrix = _camera.CalculateObliqueMatrix(clipPlane);
// render portal view
_camera.targetTexture = _textures[eye];
UniversalRenderPipeline.RenderSingleCamera(scriptableRenderContext, _camera);
}
screen.enabled = true;
}
}
}

View File

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

View File

@@ -0,0 +1,113 @@
using System.Collections.Generic;
using EscapeRoomEngine.VR.Runtime;
using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.Rendering.Universal;
namespace EscapeRoomEngine.Portal.Runtime
{
public class RenderPortalCameras : ScriptableRendererFeature
{
private enum PassType
{
Opaque,
Transparent
}
private class PortalCameraRenderPass : ScriptableRenderPass
{
private static readonly List<ShaderTagId> ShaderTagIds = new List<ShaderTagId>
{
new("SRPDefaultUnlit"), new("UniversalForward"), new("UniversalForwardOnly")
};
private RenderStateBlock _renderStateBlock;
private CommandBuffer _commandBuffer;
private readonly LayerMask _layers;
private readonly PassType _passType;
public PortalCameraRenderPass(LayerMask layers, PassType passType)
{
_layers = layers;
_passType = passType;
renderPassEvent = RenderPassEvent.AfterRenderingOpaques;
_renderStateBlock = new RenderStateBlock(RenderStateMask.Nothing);
// set stencil
var stencilState = StencilState.defaultValue;
stencilState.enabled = true;
stencilState.SetCompareFunction(CompareFunction.Equal);
stencilState.SetPassOperation(StencilOp.Keep);
stencilState.SetFailOperation(StencilOp.Keep);
stencilState.SetZFailOperation(StencilOp.Keep);
_renderStateBlock.mask |= RenderStateMask.Stencil;
_renderStateBlock.stencilState = stencilState;
}
public override void OnCameraSetup(CommandBuffer cmd, ref RenderingData renderingData)
{
// we need to store a reference to the command buffer before executing, because the reference in RenderingData is internal
_commandBuffer = cmd;
}
public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData)
{
if (Player.Instance)
{
var drawingSettings = CreateDrawingSettings(ShaderTagIds, ref renderingData, _passType == PassType.Opaque ? renderingData.cameraData.defaultOpaqueSortFlags : SortingCriteria.CommonTransparent);
var filteringSettings = new FilteringSettings(_passType == PassType.Opaque ? RenderQueueRange.opaque : RenderQueueRange.transparent, _layers);
foreach (var portal in FindObjectsByType<Portal>(FindObjectsInactive.Exclude, FindObjectsSortMode.None))
{
var linkedPortal = portal.linkedPortal;
if (linkedPortal)
{
// set which portal to render
_renderStateBlock.stencilReference = portal.PortalNumber;
// prepare the camera
ref var cameraData = ref renderingData.cameraData;
var camera = cameraData.camera;
var cameraTransform = camera.transform;
var originalPosition = cameraTransform.position;
var originalRotation = cameraTransform.rotation;
var originalProjection = camera.projectionMatrix;
camera = linkedPortal.SetUpCamera(camera);
RenderingUtils.SetViewAndProjectionMatrices(_commandBuffer, camera.worldToCameraMatrix, GL.GetGPUProjectionMatrix(camera.projectionMatrix, true), false);
// execute command buffer
context.ExecuteCommandBuffer(_commandBuffer);
_commandBuffer.Clear();
// render the portal
context.DrawRenderers(renderingData.cullResults, ref drawingSettings, ref filteringSettings, ref _renderStateBlock);
// reset
RenderingUtils.SetViewAndProjectionMatrices(_commandBuffer, cameraData.GetViewMatrix(), cameraData.GetGPUProjectionMatrix(), false);
camera.transform.SetPositionAndRotation(originalPosition, originalRotation);
camera.projectionMatrix = originalProjection;
}
}
}
}
}
private PortalCameraRenderPass _opaquePass, _transparentPass;
public LayerMask layers;
public override void Create()
{
_opaquePass = new PortalCameraRenderPass(layers, PassType.Opaque);
_transparentPass = new PortalCameraRenderPass(layers, PassType.Transparent);
}
public override void AddRenderPasses(ScriptableRenderer renderer, ref RenderingData renderingData)
{
renderer.EnqueuePass(_opaquePass);
// renderer.EnqueuePass(_transparentPass); TODO: enable
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: eaf6ad8627b74652be2715a93378d05f
timeCreated: 1682588767