Unity Portal HDRP
Визуал
В камерах на порталах убрать пост процессинг. Убрать моушен блюр.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.Rendering.HighDefinition;
public class Portal : MonoBehaviour
{
[Header ("Main Settings")]
public Transform linkedPortal;
public MeshRenderer screen;
public int recursion = 3;
// Небольшой отступ чтобы избежать артефактов
public float nearClipOffset = 0.0f;
public float nearClipLimit = 0.0f;
RenderTexture viewTexture;
Camera portalCamera;
Camera otherPortalCam;
Camera mainCamera;
HDAdditionalCameraData hdCamData;
// Кэш для трансформаций порталов (оптимизация)
private Matrix4x4[] cachedTransforms;
private bool transformsCacheValid = false;
void Awake ()
{
//не забыть поставить тег на основную камеру
mainCamera = Camera.main;
portalCamera = GetComponentInChildren<Camera> (true);
if (linkedPortal != null)
otherPortalCam = linkedPortal.GetComponentInChildren<Camera> (true);
if (portalCamera != null)
{
portalCamera.enabled = false;
hdCamData = portalCamera.GetComponent<HDAdditionalCameraData>();
}
}
void Start()
{
// Создаем и назначаем текстуру ОДИН раз при старте
CreateViewTexture();
// Инициализируем кэш трансформаций
InitializeTransformCache();
}
void Update ()
{
// Render ();
// Проверяем, изменились ли трансформации порталов
if (transformsCacheValid && (transform.hasChanged || (linkedPortal != null && linkedPortal.hasChanged)))
{
InvalidateTransformCache();
transform.hasChanged = false;
if (linkedPortal != null) linkedPortal.hasChanged = false;
}
}
public void Render ()
{
if (linkedPortal == null || portalCamera == null || viewTexture == null) return;
// Рендерим рекурсивно от последней итерации к первой
for (int i = recursion - 1; i >= 0; i--)
{
RenderPortalIteration(i);
}
}
public void PrePortalRender() {
// Подготовка перед рендерингом (если нужно)
}
public void PostPortalRender() {
// Очистка после рендеринга (если нужно)
}
void CreateViewTexture()
{
if (screen == null || screen.material == null)
{
Debug.LogError($"[{name}] Portal screen or material is null!");
return;
}
int w = Mathf.Max(1, Screen.width);
int h = Mathf.Max(1, Screen.height);
bool needCreate = (viewTexture == null) || (viewTexture.width != w) || (viewTexture.height != h);
if (needCreate)
{
if (viewTexture != null)
{
viewTexture.Release();
DestroyImmediate(viewTexture);
viewTexture = null;
}
// --- HDR-compatible descriptor ---
var desc = new RenderTextureDescriptor(w, h)
{
msaaSamples = 1, // ОТКЛЮЧЕНО MSAA
volumeDepth = 1,
enableRandomWrite = false,
useMipMap = false,
autoGenerateMips = false,
dimension = UnityEngine.Rendering.TextureDimension.Tex2D,
depthBufferBits = 24,
// GraphicsFormat R16G16B16A16_SFloat — HDR-safe и совместим с HDRP
graphicsFormat = UnityEngine.Experimental.Rendering.GraphicsFormat.R16G16B16A16_SFloat,
// depthStencilFormat = UnityEngine.Experimental.Rendering.GraphicsFormat.D32,
sRGB = (QualitySettings.activeColorSpace == ColorSpace.Linear)
};
viewTexture = new RenderTexture(desc);
viewTexture.filterMode = FilterMode.Bilinear;
viewTexture.wrapMode = TextureWrapMode.Clamp;
viewTexture.useMipMap = false;
viewTexture.autoGenerateMips = false;
viewTexture.name = gameObject.name + "_PortalTexture";
if (!viewTexture.Create())
{
Debug.LogError($"[{name}] Failed to create RenderTexture!");
viewTexture = null;
return;
}
screen.material.SetTexture("_MainTex", viewTexture);
}
// Всегда устанавливаем target texture для камеры и отключаем MSAA/allowMSAA
if (portalCamera != null)
{
portalCamera.targetTexture = viewTexture;
portalCamera.allowMSAA = false; // отключаем на камере
portalCamera.allowHDR = true; // включаем HDR (по желанию)
}
}
void RenderPortalIteration(int iterationID)
{
if (linkedPortal == null || portalCamera == null || mainCamera == null) return;
if (iterationID == recursion -1)
{
screen.material.SetFloat("_displayMask", 0);
}
// Копируем позицию и поворот основной камеры
portalCamera.transform.position = mainCamera.transform.position;
portalCamera.transform.rotation = mainCamera.transform.rotation;
// Вычисляем финальную трансформацию камеры через порталы математически
TransformCameraThroughPortals(iterationID + 1);
// Устанавливаем параметры камеры и рендерим
UpdatePortalCameraSettings();
// Базовая проекция перед расчётом oblique
portalCamera.projectionMatrix = mainCamera.projectionMatrix;
// Отсечение за плоскостью портала (oblique near plane)
SetNearClipPlane();
if (iterationID != 0)
{
hdCamData.flipYMode = HDAdditionalCameraData.FlipYMode.ForceFlipY;
}
portalCamera.Render();
hdCamData.flipYMode = HDAdditionalCameraData.FlipYMode.Automatic;
screen.material.SetFloat("_displayMask", 1);
}
void TransformCameraThroughPortals(int portalCount)
{
if (portalCount <= 0 || linkedPortal == null) return;
// Вычисляем комбинированную трансформацию для прохода через N порталов
Matrix4x4 combinedTransform = CalculatePortalTransform(portalCount);
// Применяем трансформацию к позиции камеры
portalCamera.transform.position = combinedTransform.MultiplyPoint(portalCamera.transform.position);
// Применяем трансформацию к повороту камеры
portalCamera.transform.rotation = combinedTransform.rotation * portalCamera.transform.rotation;
}
void InitializeTransformCache()
{
if (cachedTransforms == null || cachedTransforms.Length != recursion)
{
cachedTransforms = new Matrix4x4[recursion];
transformsCacheValid = false;
}
}
void InvalidateTransformCache()
{
transformsCacheValid = false;
}
Matrix4x4 CalculatePortalTransform(int portalCount)
{
if (portalCount <= 0 || linkedPortal == null)
return Matrix4x4.identity;
// Проверяем кэш
if (transformsCacheValid && portalCount <= cachedTransforms.Length)
{
return cachedTransforms[portalCount - 1];
}
// Вычисляем и кэшируем трансформации
if (!transformsCacheValid)
{
Matrix4x4 singlePortalTransform = GetSinglePortalTransform();
for (int i = 0; i < recursion; i++)
{
if (i == 0)
{
cachedTransforms[i] = singlePortalTransform;
}
else
{
cachedTransforms[i] = singlePortalTransform * cachedTransforms[i - 1];
}
}
transformsCacheValid = true;
}
return cachedTransforms[portalCount - 1];
}
Matrix4x4 GetSinglePortalTransform()
{
// Создаем трансформацию для прохода через один портал
// 1. Переводим в локальные координаты входного портала
Matrix4x4 toLocal = transform.worldToLocalMatrix;
// 2. Поворот на 180 градусов вокруг Y оси
Matrix4x4 rotation180 = Matrix4x4.Rotate(Quaternion.Euler(0.0f, 180.0f, 0.0f));
// 3. Переводим в мировые координаты выходного портала
Matrix4x4 toWorld = linkedPortal.localToWorldMatrix;
// Комбинируем все трансформации
return toWorld * rotation180 * toLocal;
}
void UpdatePortalCameraSettings()
{
if (portalCamera == null || mainCamera == null) return;
// Копируем базовые настройки камеры
portalCamera.fieldOfView = mainCamera.fieldOfView;
portalCamera.nearClipPlane = mainCamera.nearClipPlane;
portalCamera.farClipPlane = mainCamera.farClipPlane;
portalCamera.aspect = mainCamera.aspect;
portalCamera.orthographic = mainCamera.orthographic;
portalCamera.orthographicSize = mainCamera.orthographicSize;
}
void OnTriggerEnter (Collider other) {
Debug.Log($"[Portal:{name}] OnTriggerEnter: {other.name}");
}
void OnTriggerExit (Collider other) {
Debug.Log($"[Portal:{name}] OnTriggerExit: {other.name}");
}
void SetNearClipPlane() {
if (linkedPortal == null || portalCamera == null || mainCamera == null) return;
// Определяем плоскость обрезки (плоскость портала)
Transform clipPlane = linkedPortal;
// Проверяем, находится ли камера портала за плоскостью портала
Vector3 portalToCamera = portalCamera.transform.position - clipPlane.position;
float distanceToPortal = Vector3.Dot(portalToCamera, clipPlane.forward);
// Если камера находится за порталом, не используем oblique projection
if (distanceToPortal < 0) {
portalCamera.projectionMatrix = mainCamera.projectionMatrix;
return;
}
// Простая и стабильная логика определения направления нормали
int dot = System.Math.Sign(Vector3.Dot(clipPlane.forward, clipPlane.position - portalCamera.transform.position));
Debug.Log($"dot {dot}");
// Конвертируем в пространство камеры портала
Vector3 camSpacePos = portalCamera.worldToCameraMatrix.MultiplyPoint(clipPlane.position);
Vector3 camSpaceNormal = portalCamera.worldToCameraMatrix.MultiplyVector(clipPlane.forward) * dot;
Debug.Log($"camSpaceNormal {camSpaceNormal}");
float camSpaceDst = -Vector3.Dot(camSpacePos, camSpaceNormal);
Debug.Log($"camSpaceDst {camSpaceDst}");
// Используем oblique projection только если мы не слишком близко к порталу
if (Mathf.Abs(camSpaceDst) > nearClipLimit) {
Vector4 clipPlaneCameraSpace = new Vector4(camSpaceNormal.x, camSpaceNormal.y, camSpaceNormal.z, camSpaceDst + nearClipOffset);
portalCamera.projectionMatrix = CalculateObliqueMatrix(portalCamera.projectionMatrix, clipPlaneCameraSpace);
// portalCamera.projectionMatrix = portalCamera.CalculateObliqueMatrix(clipPlaneCameraSpace);
} else {
// Если слишком близко, используем обычную проекцию
portalCamera.projectionMatrix = mainCamera.projectionMatrix;
}
}
// Метод для создания oblique projection matrix
Matrix4x4 CalculateObliqueMatrix(Matrix4x4 projection, Vector4 clipPlane) {
Vector4 q = projection.inverse * new Vector4(
System.Math.Sign(clipPlane.x),
System.Math.Sign(clipPlane.y),
1.0f,
-1.0f
);
Vector4 c = clipPlane * (2.0f / (Vector4.Dot(clipPlane, q)));
// Заменяем третью строку матрицы проекции
Matrix4x4 obliqueMatrix = projection;
obliqueMatrix[2] = c.x - obliqueMatrix[3];
obliqueMatrix[6] = c.y - obliqueMatrix[7];
obliqueMatrix[10] = c.z - obliqueMatrix[11];
obliqueMatrix[14] = c.w - obliqueMatrix[15];
return obliqueMatrix;
}
void OnDestroy()
{
CleanupTextures();
}
void OnDisable()
{
CleanupTextures();
}
void CleanupTextures()
{
if (viewTexture != null)
{
viewTexture.Release();
viewTexture = null;
}
}
}
Комментарии
Комментариев пока нет.