wip: Path to Omnissiah
This commit is contained in:
@@ -51,10 +51,13 @@ Material:
|
|||||||
m_Offset: {x: 0, y: 0}
|
m_Offset: {x: 0, y: 0}
|
||||||
m_Ints: []
|
m_Ints: []
|
||||||
m_Floats:
|
m_Floats:
|
||||||
|
- _ClipDistance: 0
|
||||||
|
- _ClipInvert: 0
|
||||||
- _GrainSize: 0.4
|
- _GrainSize: 0.4
|
||||||
- _GrainStrength: 0.2
|
- _GrainStrength: 0.2
|
||||||
- _HighlightMax: 1
|
- _HighlightMax: 1
|
||||||
- _HighlightMin: 0.5
|
- _HighlightMin: 0.5
|
||||||
|
- _MaskID: 0
|
||||||
- _QueueControl: 0
|
- _QueueControl: 0
|
||||||
- _QueueOffset: 0
|
- _QueueOffset: 0
|
||||||
- _ShadowMax: 0.2
|
- _ShadowMax: 0.2
|
||||||
|
|||||||
+3
-2
@@ -20,8 +20,8 @@ Material:
|
|||||||
m_CorrespondingSourceObject: {fileID: 0}
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
m_PrefabInstance: {fileID: 0}
|
m_PrefabInstance: {fileID: 0}
|
||||||
m_PrefabAsset: {fileID: 0}
|
m_PrefabAsset: {fileID: 0}
|
||||||
m_Name: M_default_mask_0
|
m_Name: M_default_mask_A
|
||||||
m_Shader: {fileID: -6465566751694194690, guid: adf06fb1c82bec94d8daea4d30c135e9, type: 3}
|
m_Shader: {fileID: -6465566751694194690, guid: 10840deb9abeee9478cbbb5915f60f5c, type: 3}
|
||||||
m_Parent: {fileID: 0}
|
m_Parent: {fileID: 0}
|
||||||
m_ModifiedSerializedProperties: 0
|
m_ModifiedSerializedProperties: 0
|
||||||
m_ValidKeywords: []
|
m_ValidKeywords: []
|
||||||
@@ -57,6 +57,7 @@ Material:
|
|||||||
- _GrainStrength: 0.2
|
- _GrainStrength: 0.2
|
||||||
- _HighlightMax: 1
|
- _HighlightMax: 1
|
||||||
- _HighlightMin: 0.5
|
- _HighlightMin: 0.5
|
||||||
|
- _MaskID: 1
|
||||||
- _MaskValueOnDisabled: 0
|
- _MaskValueOnDisabled: 0
|
||||||
- _QueueControl: 0
|
- _QueueControl: 0
|
||||||
- _QueueOffset: 0
|
- _QueueOffset: 0
|
||||||
+3
-2
@@ -20,8 +20,8 @@ Material:
|
|||||||
m_CorrespondingSourceObject: {fileID: 0}
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
m_PrefabInstance: {fileID: 0}
|
m_PrefabInstance: {fileID: 0}
|
||||||
m_PrefabAsset: {fileID: 0}
|
m_PrefabAsset: {fileID: 0}
|
||||||
m_Name: M_default_mask_1
|
m_Name: M_default_mask_B
|
||||||
m_Shader: {fileID: -6465566751694194690, guid: adf06fb1c82bec94d8daea4d30c135e9, type: 3}
|
m_Shader: {fileID: -6465566751694194690, guid: 10840deb9abeee9478cbbb5915f60f5c, type: 3}
|
||||||
m_Parent: {fileID: 0}
|
m_Parent: {fileID: 0}
|
||||||
m_ModifiedSerializedProperties: 0
|
m_ModifiedSerializedProperties: 0
|
||||||
m_ValidKeywords: []
|
m_ValidKeywords: []
|
||||||
@@ -57,6 +57,7 @@ Material:
|
|||||||
- _GrainStrength: 0.2
|
- _GrainStrength: 0.2
|
||||||
- _HighlightMax: 1
|
- _HighlightMax: 1
|
||||||
- _HighlightMin: 0.5
|
- _HighlightMin: 0.5
|
||||||
|
- _MaskID: 2
|
||||||
- _MaskInvert: 1
|
- _MaskInvert: 1
|
||||||
- _MaskValueOnDisabled: 1
|
- _MaskValueOnDisabled: 1
|
||||||
- _QueueControl: 0
|
- _QueueControl: 0
|
||||||
@@ -21,7 +21,7 @@ Material:
|
|||||||
m_PrefabInstance: {fileID: 0}
|
m_PrefabInstance: {fileID: 0}
|
||||||
m_PrefabAsset: {fileID: 0}
|
m_PrefabAsset: {fileID: 0}
|
||||||
m_Name: M_Theatre_stainedGlass
|
m_Name: M_Theatre_stainedGlass
|
||||||
m_Shader: {fileID: -6465566751694194690, guid: adf06fb1c82bec94d8daea4d30c135e9, type: 3}
|
m_Shader: {fileID: -6465566751694194690, guid: 10840deb9abeee9478cbbb5915f60f5c, type: 3}
|
||||||
m_Parent: {fileID: 0}
|
m_Parent: {fileID: 0}
|
||||||
m_ModifiedSerializedProperties: 0
|
m_ModifiedSerializedProperties: 0
|
||||||
m_ValidKeywords: []
|
m_ValidKeywords: []
|
||||||
@@ -57,6 +57,7 @@ Material:
|
|||||||
- _GrainStrength: 0.2
|
- _GrainStrength: 0.2
|
||||||
- _HighlightMax: 1
|
- _HighlightMax: 1
|
||||||
- _HighlightMin: 0.5
|
- _HighlightMin: 0.5
|
||||||
|
- _MaskID: 1
|
||||||
- _MaskValueOnDisabled: 0
|
- _MaskValueOnDisabled: 0
|
||||||
- _QueueControl: 0
|
- _QueueControl: 0
|
||||||
- _QueueOffset: 0
|
- _QueueOffset: 0
|
||||||
|
|||||||
Binary file not shown.
+2913
-306
File diff suppressed because it is too large
Load Diff
@@ -1,54 +0,0 @@
|
|||||||
#if UNITY_EDITOR || DEVELOPMENT_BUILD
|
|
||||||
using UnityEngine;
|
|
||||||
using Unheimlich.Features.Portals;
|
|
||||||
|
|
||||||
namespace Unheimlich.Dev
|
|
||||||
{
|
|
||||||
public class PortalAudioDebugger : MonoBehaviour
|
|
||||||
{
|
|
||||||
private Portal _portal;
|
|
||||||
|
|
||||||
private void Awake()
|
|
||||||
{
|
|
||||||
_portal = GetComponent<Portal>();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnEnable()
|
|
||||||
{
|
|
||||||
if (_portal != null)
|
|
||||||
{
|
|
||||||
_portal.RoomLoaded += OnRoomLoadedDebug;
|
|
||||||
_portal.RoomUnloaded += OnRoomUnloadedDebug;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnDisable()
|
|
||||||
{
|
|
||||||
if (_portal != null)
|
|
||||||
{
|
|
||||||
_portal.RoomLoaded -= OnRoomLoadedDebug;
|
|
||||||
_portal.RoomUnloaded -= OnRoomUnloadedDebug;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnRoomLoadedDebug()
|
|
||||||
{
|
|
||||||
Debug.Log($"<color=green>[PortalDebug] RoomLoaded event fired on {gameObject.name}</color>");
|
|
||||||
|
|
||||||
if (_portal.LoadedRoomInstance != null)
|
|
||||||
{
|
|
||||||
AudioSource[] sources = _portal.LoadedRoomInstance.GetComponentsInChildren<AudioSource>();
|
|
||||||
Debug.Log($"<color=green>[PortalDebug] Found {sources.Length} AudioSources in loaded room:</color>");
|
|
||||||
|
|
||||||
foreach (var source in sources)
|
|
||||||
Debug.Log($" - {source.name} | Playing: {source.isPlaying} | Clip: {(source.clip != null ? source.clip.name : "NULL")}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnRoomUnloadedDebug()
|
|
||||||
{
|
|
||||||
Debug.Log($"<color=red>[PortalDebug] RoomUnloaded event fired on {gameObject.name}</color>");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
fileFormatVersion: 2
|
|
||||||
guid: 897784c22112f5f48af4313022397d8e
|
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
[ExecuteInEditMode, DefaultExecutionOrder(1)]
|
||||||
|
public class Portal : MonoBehaviour
|
||||||
|
{
|
||||||
|
[SerializeField, Range(0, 2)] private int _id;
|
||||||
|
[SerializeField, Range(0, 1)] private float _oval;
|
||||||
|
|
||||||
|
public int ID => _id;
|
||||||
|
public float Oval => _oval;
|
||||||
|
|
||||||
|
private void OnDrawGizmos()
|
||||||
|
{
|
||||||
|
Gizmos.matrix = transform.localToWorldMatrix;
|
||||||
|
Gizmos.color = Color.cyan;
|
||||||
|
Gizmos.DrawWireCube(Vector3.zero, new Vector3(1, 1, 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
void OnEnable()
|
||||||
|
{
|
||||||
|
if (PortalManager.Instance)
|
||||||
|
PortalManager.Instance.RegisterPortal(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
void OnDisable()
|
||||||
|
{
|
||||||
|
if (PortalManager.Instance)
|
||||||
|
PortalManager.Instance.UnregisterPortal(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 3b841c8d159a88e4a81dbf12ab26e346
|
||||||
@@ -0,0 +1,110 @@
|
|||||||
|
using OctoCore;
|
||||||
|
using UnityEngine;
|
||||||
|
using UnityEngine.SocialPlatforms;
|
||||||
|
|
||||||
|
[ExecuteInEditMode, DefaultExecutionOrder(0)]
|
||||||
|
public class PortalManager : MonoBehaviour
|
||||||
|
{
|
||||||
|
public static PortalManager Instance { get; private set; }
|
||||||
|
|
||||||
|
[System.Serializable]
|
||||||
|
public class State
|
||||||
|
{
|
||||||
|
public bool PlayerInside;
|
||||||
|
public Portal[] Portals;
|
||||||
|
}
|
||||||
|
|
||||||
|
[SerializeField, OctoReadOnly] private State _state;
|
||||||
|
|
||||||
|
void OnEnable()
|
||||||
|
{
|
||||||
|
_state = new State();
|
||||||
|
_state.Portals = new Portal[3];
|
||||||
|
|
||||||
|
if (!Instance)
|
||||||
|
Instance = this;
|
||||||
|
|
||||||
|
ResetMasks();
|
||||||
|
}
|
||||||
|
|
||||||
|
void OnDisable()
|
||||||
|
{
|
||||||
|
if (Instance == this)
|
||||||
|
Instance = null;
|
||||||
|
|
||||||
|
ResetMasks();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Update()
|
||||||
|
{
|
||||||
|
UpdateMasks();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetInside(bool inside)
|
||||||
|
{
|
||||||
|
_state.PlayerInside = inside;
|
||||||
|
|
||||||
|
UpdateMasks();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RegisterPortal(Portal portal)
|
||||||
|
{
|
||||||
|
_state.Portals[portal.ID] = portal;
|
||||||
|
|
||||||
|
ManageMask(portal, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void UnregisterPortal(Portal portal)
|
||||||
|
{
|
||||||
|
ManageMask(portal, false);
|
||||||
|
|
||||||
|
_state.Portals[portal.ID] = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ManageMask(Portal portal, bool active)
|
||||||
|
{
|
||||||
|
if (!portal)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (_state.Portals[portal.ID] != portal)
|
||||||
|
{
|
||||||
|
RegisterPortal(portal);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Vector3 camPos = Camera.main ? Camera.main.transform.position : Vector3.zero;
|
||||||
|
|
||||||
|
Matrix4x4 m = new Matrix4x4();
|
||||||
|
m.SetRow(0, new Vector4(camPos.x, camPos.y, camPos.z, !active || _state.PlayerInside ? 0 : 1));
|
||||||
|
m.SetRow(1, new Vector4(portal.transform.position.x, portal.transform.position.y, portal.transform.position.z, portal.Oval));
|
||||||
|
m.SetRow(2, new Vector4(portal.transform.up.x, portal.transform.up.y, portal.transform.up.z, portal.transform.localScale.x));
|
||||||
|
m.SetRow(3, new Vector4(portal.transform.forward.x, portal.transform.forward.y, portal.transform.forward.z, portal.transform.localScale.y));
|
||||||
|
|
||||||
|
SetMask(portal.ID, m);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateMasks()
|
||||||
|
{
|
||||||
|
for (int i = 0; i < _state.Portals.Length; i++)
|
||||||
|
ManageMask(_state.Portals[i], true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ResetMasks()
|
||||||
|
{
|
||||||
|
for (int i = 0; i < _state.Portals.Length; i++)
|
||||||
|
{
|
||||||
|
Matrix4x4 m = new Matrix4x4();
|
||||||
|
m.SetRow(0, Vector4.zero);
|
||||||
|
m.SetRow(1, Vector4.zero);
|
||||||
|
m.SetRow(2, Vector4.zero);
|
||||||
|
m.SetRow(3, Vector4.zero);
|
||||||
|
|
||||||
|
SetMask(i, m);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SetMask(int id, Matrix4x4 m)
|
||||||
|
{
|
||||||
|
Shader.SetGlobalMatrix($"_Mask{id}", m);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 746134082760dc24e8ffdf666230032d
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
fileFormatVersion: 2
|
|
||||||
guid: a7af0836297e37f40b61bdecbb261265
|
|
||||||
folderAsset: yes
|
|
||||||
DefaultImporter:
|
|
||||||
externalObjects: {}
|
|
||||||
userData:
|
|
||||||
assetBundleName:
|
|
||||||
assetBundleVariant:
|
|
||||||
@@ -1,733 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using UnityEngine;
|
|
||||||
using UnityEngine.Rendering.Universal;
|
|
||||||
using Unheimlich.Features.Portals;
|
|
||||||
using Unheimlich.Core.Utils;
|
|
||||||
|
|
||||||
namespace Unheimlich.Features.Portals
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Core component of the seamless portal system.
|
|
||||||
/// Creates a real-time view through to a linked portal and handles object teleportation.
|
|
||||||
///
|
|
||||||
/// How it works:
|
|
||||||
/// 1. Renders the view from the linked portal's perspective to a texture
|
|
||||||
/// 2. Displays that texture on this portal's screen mesh
|
|
||||||
/// 3. Tracks objects (PortalTravellers) that enter the portal
|
|
||||||
/// 4. Teleports them when they cross the portal plane
|
|
||||||
/// 5. Uses shader-based slicing to prevent visual artifacts during crossing
|
|
||||||
/// </summary>
|
|
||||||
[RequireComponent(typeof(BoxCollider))]
|
|
||||||
public class Portal : MonoBehaviour
|
|
||||||
{
|
|
||||||
/// <summary>Global list of all active portals in the scene</summary>
|
|
||||||
public static List<Portal> AllPortals = new List<Portal>();
|
|
||||||
|
|
||||||
[Header("Main Settings")]
|
|
||||||
[SerializeField] private string _portalId = "";
|
|
||||||
[SerializeField] private Portal _linkedPortal;
|
|
||||||
[SerializeField] private MeshRenderer _screen;
|
|
||||||
[SerializeField] private int _recursionLimit = 3;
|
|
||||||
|
|
||||||
[Header("Advanced Settings")]
|
|
||||||
[SerializeField] private float _nearClipOffset = 0.05f;
|
|
||||||
[SerializeField] private float _nearClipLimit = 0.2f;
|
|
||||||
[SerializeField] private float _transitionDistance = 0.5f;
|
|
||||||
[SerializeField] private float _loadRoomDistance = 30f;
|
|
||||||
|
|
||||||
[Header("Room Loading")]
|
|
||||||
[SerializeField] private GameObject _roomPrefabToLoad;
|
|
||||||
[SerializeField] private Transform _roomSpawnPoint;
|
|
||||||
[SerializeField] private string _linkedPortalPathInRoom = "Portal";
|
|
||||||
[SerializeField] private string _linkedPortalId = "";
|
|
||||||
|
|
||||||
private RenderTexture _viewTexture;
|
|
||||||
private Camera _portalCam;
|
|
||||||
private Camera _playerCam;
|
|
||||||
private List<PortalTraveller> _trackedTravellers;
|
|
||||||
private MeshFilter _screenMeshFilter;
|
|
||||||
private GameObject _loadedRoomInstance;
|
|
||||||
private string _roomId;
|
|
||||||
private PortalDoor _door;
|
|
||||||
private int _lastRenderedFrame = -1;
|
|
||||||
private bool _playerIsTeleporting = false;
|
|
||||||
|
|
||||||
public event Action RoomLoaded;
|
|
||||||
public event Action RoomUnloaded;
|
|
||||||
|
|
||||||
public Portal LinkedPortal => _linkedPortal;
|
|
||||||
public GameObject LoadedRoomInstance => _loadedRoomInstance;
|
|
||||||
public bool IsDoorOpen => _door == null || _door.IsOpen;
|
|
||||||
|
|
||||||
private void Awake()
|
|
||||||
{
|
|
||||||
_playerCam = Camera.main;
|
|
||||||
_portalCam = GetComponentInChildren<Camera>();
|
|
||||||
|
|
||||||
// Create portal camera if one doesn't exist
|
|
||||||
if (_portalCam == null)
|
|
||||||
{
|
|
||||||
GameObject camObj = new GameObject(PortalConstants.PORTAL_CAMERA_NAME);
|
|
||||||
camObj.transform.SetParent(transform);
|
|
||||||
camObj.transform.localPosition = Vector3.zero;
|
|
||||||
camObj.transform.localRotation = Quaternion.identity;
|
|
||||||
_portalCam = camObj.AddComponent<Camera>();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Configure portal camera to match player camera settings
|
|
||||||
_portalCam.enabled = false;
|
|
||||||
_portalCam.depth = _playerCam.depth - 1;
|
|
||||||
_portalCam.clearFlags = _playerCam.clearFlags;
|
|
||||||
_portalCam.backgroundColor = _playerCam.backgroundColor;
|
|
||||||
_portalCam.farClipPlane = _playerCam.farClipPlane;
|
|
||||||
_portalCam.nearClipPlane = _playerCam.nearClipPlane;
|
|
||||||
_portalCam.fieldOfView = _playerCam.fieldOfView;
|
|
||||||
_portalCam.cullingMask = _playerCam.cullingMask;
|
|
||||||
_portalCam.allowHDR = _playerCam.allowHDR;
|
|
||||||
_portalCam.allowMSAA = _playerCam.allowMSAA;
|
|
||||||
_portalCam.useOcclusionCulling = _playerCam.useOcclusionCulling;
|
|
||||||
|
|
||||||
// Configure URP camera data for shadows and better quality
|
|
||||||
var urpCamData = _portalCam.GetUniversalAdditionalCameraData();
|
|
||||||
if (urpCamData != null)
|
|
||||||
{
|
|
||||||
// Copy main camera URP settings
|
|
||||||
var mainCamData = _playerCam.GetUniversalAdditionalCameraData();
|
|
||||||
if (mainCamData != null)
|
|
||||||
{
|
|
||||||
urpCamData.renderType = CameraRenderType.Base;
|
|
||||||
urpCamData.renderShadows = mainCamData.renderShadows;
|
|
||||||
urpCamData.requiresDepthOption = mainCamData.requiresDepthOption;
|
|
||||||
urpCamData.requiresColorOption = mainCamData.requiresColorOption;
|
|
||||||
urpCamData.requiresDepthTexture = mainCamData.requiresDepthTexture;
|
|
||||||
urpCamData.requiresColorTexture = mainCamData.requiresColorTexture;
|
|
||||||
urpCamData.renderPostProcessing = false;
|
|
||||||
urpCamData.antialiasing = mainCamData.antialiasing;
|
|
||||||
urpCamData.antialiasingQuality = mainCamData.antialiasingQuality;
|
|
||||||
urpCamData.volumeLayerMask = 0;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// Fallback if main camera doesn't have URP data
|
|
||||||
urpCamData.renderType = CameraRenderType.Base;
|
|
||||||
urpCamData.renderShadows = true;
|
|
||||||
urpCamData.requiresDepthTexture = true;
|
|
||||||
urpCamData.renderPostProcessing = false;
|
|
||||||
urpCamData.volumeLayerMask = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_trackedTravellers = new List<PortalTraveller>();
|
|
||||||
_screenMeshFilter = _screen.GetComponent<MeshFilter>();
|
|
||||||
_screen.material.SetInt(PortalConstants.SHADER_PROPERTY_DISPLAY_MASK, 1);
|
|
||||||
|
|
||||||
_door = GetComponentInChildren<PortalDoor>();
|
|
||||||
|
|
||||||
if (_roomPrefabToLoad != null)
|
|
||||||
{
|
|
||||||
_roomId = $"{gameObject.name}_{_roomPrefabToLoad.name}";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnEnable()
|
|
||||||
{
|
|
||||||
if (!AllPortals.Contains(this))
|
|
||||||
AllPortals.Add(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnDisable()
|
|
||||||
{
|
|
||||||
if (AllPortals.Contains(this))
|
|
||||||
AllPortals.Remove(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private void Start()
|
|
||||||
{
|
|
||||||
if (PortalRoomManager.Instance != null && _roomPrefabToLoad != null)
|
|
||||||
{
|
|
||||||
PortalRoomManager.Instance.RegisterPortal(this);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnDestroy()
|
|
||||||
{
|
|
||||||
if (PortalRoomManager.Instance != null && _roomPrefabToLoad != null)
|
|
||||||
{
|
|
||||||
PortalRoomManager.Instance.UnregisterPortal(this);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void LateUpdate()
|
|
||||||
{
|
|
||||||
TryFindLinkedPortal();
|
|
||||||
HandleTravellers();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Attempts to find and link to a portal in a dynamically loaded room.
|
|
||||||
/// Called each frame until a link is established.
|
|
||||||
/// </summary>
|
|
||||||
private void TryFindLinkedPortal()
|
|
||||||
{
|
|
||||||
if (_linkedPortal != null || PortalRoomManager.Instance == null || _roomPrefabToLoad == null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
GameObject loadedRoom = PortalRoomManager.Instance.GetLoadedRoom(_roomId);
|
|
||||||
if (loadedRoom != null)
|
|
||||||
{
|
|
||||||
_loadedRoomInstance = loadedRoom;
|
|
||||||
FindLinkedPortalInRoom(loadedRoom);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Searches for the linked portal within a loaded room.
|
|
||||||
/// First tries to find by portal ID, then falls back to path-based search.
|
|
||||||
/// </summary>
|
|
||||||
private void FindLinkedPortalInRoom(GameObject room)
|
|
||||||
{
|
|
||||||
Portal foundPortal = null;
|
|
||||||
|
|
||||||
// Try to find portal by ID first
|
|
||||||
if (!string.IsNullOrEmpty(_linkedPortalId))
|
|
||||||
{
|
|
||||||
Portal[] portalsInRoom = room.GetComponentsInChildren<Portal>();
|
|
||||||
foreach (Portal portal in portalsInRoom)
|
|
||||||
{
|
|
||||||
if (portal._portalId == _linkedPortalId)
|
|
||||||
{
|
|
||||||
foundPortal = portal;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (foundPortal == null)
|
|
||||||
{
|
|
||||||
Debug.LogWarning($"[{name}] Could not find portal with ID '{_linkedPortalId}' in room '{room.name}'. Falling back to path search.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fallback to path-based search
|
|
||||||
if (foundPortal == null && !string.IsNullOrEmpty(_linkedPortalPathInRoom))
|
|
||||||
{
|
|
||||||
Transform portalTransform = room.transform.Find(_linkedPortalPathInRoom);
|
|
||||||
if (portalTransform != null)
|
|
||||||
{
|
|
||||||
foundPortal = portalTransform.GetComponent<Portal>();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Establish bidirectional link
|
|
||||||
if (foundPortal != null)
|
|
||||||
{
|
|
||||||
_linkedPortal = foundPortal;
|
|
||||||
_linkedPortal.SetLinkedPortal(this);
|
|
||||||
|
|
||||||
if (_viewTexture != null && _linkedPortal._screen != null)
|
|
||||||
{
|
|
||||||
_linkedPortal._screen.material.SetTexture(PortalConstants.SHADER_PROPERTY_MAIN_TEXTURE, _viewTexture);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
Debug.LogError($"[{name}] Could not find any portal to link in room '{room.name}'!");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Tracks all travellers in the portal and teleports them when they cross the portal plane.
|
|
||||||
///
|
|
||||||
/// How crossing detection works:
|
|
||||||
/// 1. Calculate which side of the portal the traveller is on using dot product
|
|
||||||
/// 2. Compare with previous frame's side
|
|
||||||
/// 3. If side changed = traveller crossed the portal plane
|
|
||||||
/// 4. Transform traveller's position/rotation through portal transformation matrix
|
|
||||||
/// 5. Teleport to the linked portal
|
|
||||||
/// </summary>
|
|
||||||
private void HandleTravellers()
|
|
||||||
{
|
|
||||||
if (_linkedPortal == null)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (int i = 0; i < _trackedTravellers.Count; i++)
|
|
||||||
{
|
|
||||||
PortalTraveller traveller = _trackedTravellers[i];
|
|
||||||
Transform travellerT = traveller.transform;
|
|
||||||
|
|
||||||
// Calculate transformation matrix: from this portal's space to linked portal's space
|
|
||||||
var m = _linkedPortal.transform.localToWorldMatrix * transform.worldToLocalMatrix * travellerT.localToWorldMatrix;
|
|
||||||
|
|
||||||
// Determine which side of portal the traveller is on
|
|
||||||
Vector3 offsetFromPortal = travellerT.position - transform.position;
|
|
||||||
int portalSide = Math.Sign(Vector3.Dot(offsetFromPortal, transform.forward));
|
|
||||||
int portalSideOld = Math.Sign(Vector3.Dot(traveller.PreviousOffsetFromPortal, transform.forward));
|
|
||||||
|
|
||||||
// If side changed, traveller has crossed the portal
|
|
||||||
if (portalSide != portalSideOld)
|
|
||||||
{
|
|
||||||
if (traveller.CompareTag("Player"))
|
|
||||||
{
|
|
||||||
_playerIsTeleporting = true;
|
|
||||||
_linkedPortal._playerIsTeleporting = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Hide both original and clone during teleport
|
|
||||||
traveller.SetGraphicsVisibility(false);
|
|
||||||
traveller.SetCloneVisibility(false);
|
|
||||||
|
|
||||||
// Teleport with transformed position and rotation
|
|
||||||
traveller.Teleport(transform, _linkedPortal.transform, m.GetColumn(3), m.rotation);
|
|
||||||
|
|
||||||
Physics.SyncTransforms();
|
|
||||||
|
|
||||||
// Show traveller at new location
|
|
||||||
traveller.SetGraphicsVisibility(true);
|
|
||||||
|
|
||||||
// Transfer tracking to the linked portal
|
|
||||||
_linkedPortal.OnTravellerEnterPortal(traveller);
|
|
||||||
_trackedTravellers.RemoveAt(i);
|
|
||||||
i--;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// Update clone position to match transformed position
|
|
||||||
traveller.GraphicsClone.transform.SetPositionAndRotation(m.GetColumn(3), m.rotation);
|
|
||||||
traveller.PreviousOffsetFromPortal = offsetFromPortal;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Called before rendering. Updates shader slicing parameters for all tracked travellers.
|
|
||||||
/// </summary>
|
|
||||||
public void PrePortalRender()
|
|
||||||
{
|
|
||||||
if (_linkedPortal == null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
foreach (var traveller in _trackedTravellers)
|
|
||||||
{
|
|
||||||
UpdateSliceParams(traveller);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Main portal rendering function.
|
|
||||||
/// Creates a virtual camera at the linked portal's position and renders the view to a texture.
|
|
||||||
/// Supports recursive rendering (portal looking at portal looking at portal...).
|
|
||||||
/// </summary>
|
|
||||||
public void Render()
|
|
||||||
{
|
|
||||||
// Prevent rendering same portal multiple times per frame
|
|
||||||
if (_lastRenderedFrame == Time.frameCount)
|
|
||||||
return;
|
|
||||||
|
|
||||||
_lastRenderedFrame = Time.frameCount;
|
|
||||||
|
|
||||||
// Special case: player is teleporting, render without screen
|
|
||||||
if (_playerIsTeleporting)
|
|
||||||
{
|
|
||||||
_screen.enabled = false;
|
|
||||||
_portalCam.Render();
|
|
||||||
_screen.enabled = true;
|
|
||||||
_playerIsTeleporting = false;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_linkedPortal == null)
|
|
||||||
{
|
|
||||||
if (_roomPrefabToLoad != null)
|
|
||||||
{
|
|
||||||
// Silently skip rendering while waiting for room to load
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_linkedPortal == null || _linkedPortal._screen == null)
|
|
||||||
{
|
|
||||||
if (_roomPrefabToLoad != null)
|
|
||||||
{
|
|
||||||
// Silently skip rendering while waiting for linked portal
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
Debug.LogError($"[{name}] Cannot render: linked portal is null or has NULL _screen!");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
float distanceToCamera = Vector3.Distance(_playerCam.transform.position, transform.position);
|
|
||||||
|
|
||||||
Vector3 offsetFromPortal = _playerCam.transform.position - transform.position;
|
|
||||||
float dotToPortal = Vector3.Dot(offsetFromPortal.normalized, transform.forward);
|
|
||||||
|
|
||||||
// Don't render screen when player is crossing through
|
|
||||||
bool playerCrossingPortal = distanceToCamera < 0.5f && dotToPortal > 0.5f;
|
|
||||||
|
|
||||||
if (playerCrossingPortal)
|
|
||||||
{
|
|
||||||
_screen.enabled = false;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Don't render during transition to avoid flickering
|
|
||||||
if (distanceToCamera < _transitionDistance && dotToPortal > 0.5f)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Frustum culling: don't render if portal screen not visible
|
|
||||||
if (!PortalCameraUtility.VisibleFromCamera(_linkedPortal._screen, _playerCam))
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
CreateViewTexture();
|
|
||||||
|
|
||||||
if (!_viewTexture.IsCreated())
|
|
||||||
{
|
|
||||||
_viewTexture.Create();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Calculate recursive portal positions (for portal-in-portal effect)
|
|
||||||
var localToWorldMatrix = _playerCam.transform.localToWorldMatrix;
|
|
||||||
var renderPositions = new Vector3[_recursionLimit];
|
|
||||||
var renderRotations = new Quaternion[_recursionLimit];
|
|
||||||
|
|
||||||
int startIndex = 0;
|
|
||||||
_portalCam.projectionMatrix = _playerCam.projectionMatrix;
|
|
||||||
|
|
||||||
// Calculate transform matrices for each recursion level
|
|
||||||
for (int i = 0; i < _recursionLimit; i++)
|
|
||||||
{
|
|
||||||
if (i > 0)
|
|
||||||
{
|
|
||||||
// Stop recursion if screens don't overlap
|
|
||||||
if (!PortalCameraUtility.BoundsOverlap(_screenMeshFilter, _linkedPortal._screenMeshFilter, _portalCam))
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Transform camera through portal pair
|
|
||||||
localToWorldMatrix = transform.localToWorldMatrix * _linkedPortal.transform.worldToLocalMatrix * localToWorldMatrix;
|
|
||||||
int renderOrderIndex = _recursionLimit - i - 1;
|
|
||||||
renderPositions[renderOrderIndex] = localToWorldMatrix.GetColumn(3);
|
|
||||||
renderRotations[renderOrderIndex] = localToWorldMatrix.rotation;
|
|
||||||
|
|
||||||
_portalCam.transform.SetPositionAndRotation(renderPositions[renderOrderIndex], renderRotations[renderOrderIndex]);
|
|
||||||
startIndex = renderOrderIndex;
|
|
||||||
}
|
|
||||||
|
|
||||||
_screen.enabled = false; // Hide current portal screen so camera can see through
|
|
||||||
|
|
||||||
// Render from each recursion depth
|
|
||||||
for (int i = startIndex; i < _recursionLimit; i++)
|
|
||||||
{
|
|
||||||
_portalCam.transform.SetPositionAndRotation(renderPositions[i], renderRotations[i]);
|
|
||||||
SetNearClipPlane();
|
|
||||||
HandleClipping();
|
|
||||||
|
|
||||||
_linkedPortal._screen.enabled = false;
|
|
||||||
_portalCam.Render();
|
|
||||||
_linkedPortal._screen.enabled = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
_screen.enabled = true;
|
|
||||||
|
|
||||||
_linkedPortal._screen.material.mainTexture = _viewTexture;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Handles shader-based clipping for travellers during rendering.
|
|
||||||
/// Determines which parts of original and clone should be visible based on portal positions.
|
|
||||||
/// Prevents visual artifacts when objects cross portal boundaries.
|
|
||||||
/// </summary>
|
|
||||||
private void HandleClipping()
|
|
||||||
{
|
|
||||||
const float hideDst = -1000;
|
|
||||||
const float showDst = 1000;
|
|
||||||
float screenThickness = _linkedPortal.ProtectScreenFromClipping(_portalCam.transform.position);
|
|
||||||
|
|
||||||
// Handle clipping for this portal's travellers
|
|
||||||
foreach (var traveller in _trackedTravellers)
|
|
||||||
{
|
|
||||||
// Original: hide if on same side as portal camera
|
|
||||||
if (SameSideOfPortal(traveller.transform.position, PortalCamPos))
|
|
||||||
traveller.SetSliceOffsetDst(hideDst, false);
|
|
||||||
else
|
|
||||||
traveller.SetSliceOffsetDst(showDst, false);
|
|
||||||
|
|
||||||
// Clone: adjust based on linked portal position
|
|
||||||
int cloneSideOfLinkedPortal = -SideOfPortal(traveller.transform.position);
|
|
||||||
bool camSameSideAsClone = _linkedPortal.SideOfPortal(PortalCamPos) == cloneSideOfLinkedPortal;
|
|
||||||
if (camSameSideAsClone)
|
|
||||||
traveller.SetSliceOffsetDst(screenThickness, true);
|
|
||||||
else
|
|
||||||
traveller.SetSliceOffsetDst(-screenThickness, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle clipping for linked portal's travellers
|
|
||||||
foreach (var linkedTraveller in _linkedPortal._trackedTravellers)
|
|
||||||
{
|
|
||||||
var travellerPos = linkedTraveller.GraphicsObject.transform.position;
|
|
||||||
bool cloneOnSameSideAsCam = _linkedPortal.SideOfPortal(travellerPos) != SideOfPortal(PortalCamPos);
|
|
||||||
|
|
||||||
if (cloneOnSameSideAsCam)
|
|
||||||
linkedTraveller.SetSliceOffsetDst(hideDst, true);
|
|
||||||
else
|
|
||||||
linkedTraveller.SetSliceOffsetDst(showDst, true);
|
|
||||||
|
|
||||||
bool camSameSideAsTraveller = _linkedPortal.SameSideOfPortal(linkedTraveller.transform.position, PortalCamPos);
|
|
||||||
if (camSameSideAsTraveller)
|
|
||||||
linkedTraveller.SetSliceOffsetDst(screenThickness, false);
|
|
||||||
else
|
|
||||||
linkedTraveller.SetSliceOffsetDst(-screenThickness, false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Called after all portals have rendered. Resets clipping parameters.
|
|
||||||
/// </summary>
|
|
||||||
public void PostPortalRender()
|
|
||||||
{
|
|
||||||
foreach (var traveller in _trackedTravellers)
|
|
||||||
UpdateSliceParams(traveller);
|
|
||||||
|
|
||||||
ProtectScreenFromClipping(_portalCam.transform.position);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Creates or recreates the render texture used to display the portal view.
|
|
||||||
/// Automatically adjusts to screen resolution changes.
|
|
||||||
/// </summary>
|
|
||||||
private void CreateViewTexture()
|
|
||||||
{
|
|
||||||
if (_viewTexture == null || _viewTexture.width != Screen.width || _viewTexture.height != Screen.height)
|
|
||||||
{
|
|
||||||
if (_viewTexture != null)
|
|
||||||
_viewTexture.Release();
|
|
||||||
|
|
||||||
_viewTexture = new RenderTexture(Screen.width, Screen.height, PortalConstants.RENDER_TEXTURE_DEPTH, PortalConstants.RENDER_TEXTURE_FORMAT);
|
|
||||||
_viewTexture.filterMode = FilterMode.Bilinear;
|
|
||||||
_viewTexture.antiAliasing = 1;
|
|
||||||
_viewTexture.Create();
|
|
||||||
|
|
||||||
_portalCam.targetTexture = _viewTexture;
|
|
||||||
_linkedPortal._screen.material.mainTexture = _viewTexture;
|
|
||||||
_linkedPortal._screen.material.SetInt(PortalConstants.SHADER_PROPERTY_DISPLAY_MASK, 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Adjusts the portal screen's thickness to prevent camera near-clip plane from cutting through it.
|
|
||||||
/// The thickness is calculated based on the camera's near clip distance and FOV.
|
|
||||||
/// Returns the calculated thickness for use in clipping calculations.
|
|
||||||
/// </summary>
|
|
||||||
private float ProtectScreenFromClipping(Vector3 viewPoint)
|
|
||||||
{
|
|
||||||
float halfHeight = _playerCam.nearClipPlane * Mathf.Tan(_playerCam.fieldOfView * 0.5f * Mathf.Deg2Rad);
|
|
||||||
float halfWidth = halfHeight * _playerCam.aspect;
|
|
||||||
float dstToNearClipPlaneCorner = new Vector3(halfWidth, halfHeight, _playerCam.nearClipPlane).magnitude;
|
|
||||||
float screenThickness = dstToNearClipPlaneCorner;
|
|
||||||
|
|
||||||
Transform screenT = _screen.transform;
|
|
||||||
bool camFacingSameDirAsPortal = Vector3.Dot(transform.forward, transform.position - viewPoint) > 0;
|
|
||||||
screenT.localScale = new Vector3(screenT.localScale.x, screenT.localScale.y, screenThickness);
|
|
||||||
return screenThickness;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Updates shader parameters for slicing a traveller's graphics at the portal plane.
|
|
||||||
/// Sets the slice position, normal, and offset for both original and clone materials.
|
|
||||||
/// This creates the effect of objects being "cut" by the portal surface.
|
|
||||||
/// </summary>
|
|
||||||
private void UpdateSliceParams(PortalTraveller traveller)
|
|
||||||
{
|
|
||||||
int side = SideOfPortal(traveller.transform.position);
|
|
||||||
Vector3 sliceNormal = transform.forward * -side;
|
|
||||||
Vector3 cloneSliceNormal = _linkedPortal.transform.forward * side;
|
|
||||||
|
|
||||||
Vector3 slicePos = transform.position;
|
|
||||||
Vector3 cloneSlicePos = _linkedPortal.transform.position;
|
|
||||||
|
|
||||||
float sliceOffsetDst = 0;
|
|
||||||
float cloneSliceOffsetDst = 0;
|
|
||||||
float screenThickness = _screen.transform.localScale.z;
|
|
||||||
|
|
||||||
// Adjust slicing based on player and traveller relative positions
|
|
||||||
bool playerSameSideAsTraveller = SameSideOfPortal(_playerCam.transform.position, traveller.transform.position);
|
|
||||||
if (!playerSameSideAsTraveller)
|
|
||||||
sliceOffsetDst = -screenThickness;
|
|
||||||
|
|
||||||
bool playerSameSideAsCloneAppearing = side != _linkedPortal.SideOfPortal(_playerCam.transform.position);
|
|
||||||
if (!playerSameSideAsCloneAppearing)
|
|
||||||
cloneSliceOffsetDst = -screenThickness;
|
|
||||||
|
|
||||||
// Apply slicing parameters to all materials
|
|
||||||
for (int i = 0; i < traveller.OriginalMaterials.Length; i++)
|
|
||||||
{
|
|
||||||
traveller.OriginalMaterials[i].SetVector(PortalConstants.SHADER_PROPERTY_SLICE_CENTER, slicePos);
|
|
||||||
traveller.OriginalMaterials[i].SetVector(PortalConstants.SHADER_PROPERTY_SLICE_NORMAL, sliceNormal);
|
|
||||||
traveller.OriginalMaterials[i].SetFloat(PortalConstants.SHADER_PROPERTY_SLICE_OFFSET, sliceOffsetDst);
|
|
||||||
|
|
||||||
traveller.CloneMaterials[i].SetVector(PortalConstants.SHADER_PROPERTY_SLICE_CENTER, cloneSlicePos);
|
|
||||||
traveller.CloneMaterials[i].SetVector(PortalConstants.SHADER_PROPERTY_SLICE_NORMAL, cloneSliceNormal);
|
|
||||||
traveller.CloneMaterials[i].SetFloat(PortalConstants.SHADER_PROPERTY_SLICE_OFFSET, cloneSliceOffsetDst);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Sets up an oblique projection matrix to use the portal surface as the near clip plane.
|
|
||||||
/// This prevents objects between the camera and portal from being rendered.
|
|
||||||
/// Uses camera-space calculations to create a custom projection matrix.
|
|
||||||
/// </summary>
|
|
||||||
private void SetNearClipPlane()
|
|
||||||
{
|
|
||||||
Transform clipPlane = transform;
|
|
||||||
int dot = Math.Sign(Vector3.Dot(clipPlane.forward, transform.position - _portalCam.transform.position));
|
|
||||||
|
|
||||||
// Transform portal plane to camera space
|
|
||||||
Vector3 camSpacePos = _portalCam.worldToCameraMatrix.MultiplyPoint(clipPlane.position);
|
|
||||||
Vector3 camSpaceNormal = _portalCam.worldToCameraMatrix.MultiplyVector(clipPlane.forward) * dot;
|
|
||||||
float camSpaceDst = -Vector3.Dot(camSpacePos, camSpaceNormal) + _nearClipOffset;
|
|
||||||
|
|
||||||
// Only use oblique projection if far enough from portal
|
|
||||||
if (Mathf.Abs(camSpaceDst) > _nearClipLimit)
|
|
||||||
{
|
|
||||||
Vector4 clipPlaneCameraSpace = new Vector4(camSpaceNormal.x, camSpaceNormal.y, camSpaceNormal.z, camSpaceDst);
|
|
||||||
_portalCam.projectionMatrix = _playerCam.CalculateObliqueMatrix(clipPlaneCameraSpace);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
_portalCam.projectionMatrix = _playerCam.projectionMatrix;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Called when a traveller enters this portal's trigger zone.
|
|
||||||
/// Initializes tracking and creates the visual clone.
|
|
||||||
/// </summary>
|
|
||||||
private void OnTravellerEnterPortal(PortalTraveller traveller)
|
|
||||||
{
|
|
||||||
if (!_trackedTravellers.Contains(traveller))
|
|
||||||
{
|
|
||||||
traveller.EnterPortalThreshold();
|
|
||||||
traveller.PreviousOffsetFromPortal = traveller.transform.position - transform.position;
|
|
||||||
_trackedTravellers.Add(traveller);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Unity physics callback when object enters portal's trigger collider.
|
|
||||||
/// </summary>
|
|
||||||
private void OnTriggerEnter(Collider other)
|
|
||||||
{
|
|
||||||
var traveller = other.GetComponent<PortalTraveller>();
|
|
||||||
if (traveller)
|
|
||||||
OnTravellerEnterPortal(traveller);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Unity physics callback when object exits portal's trigger collider.
|
|
||||||
/// Cleans up the clone if traveller exits without teleporting.
|
|
||||||
/// </summary>
|
|
||||||
private void OnTriggerExit(Collider other)
|
|
||||||
{
|
|
||||||
var traveller = other.GetComponent<PortalTraveller>();
|
|
||||||
if (traveller && _trackedTravellers.Contains(traveller))
|
|
||||||
{
|
|
||||||
traveller.ExitPortalThreshold();
|
|
||||||
_trackedTravellers.Remove(traveller);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void SetLinkedPortal(Portal portal)
|
|
||||||
{
|
|
||||||
_linkedPortal = portal;
|
|
||||||
}
|
|
||||||
|
|
||||||
public GameObject GetRoomPrefab() => _roomPrefabToLoad;
|
|
||||||
public string GetRoomId() => _roomId;
|
|
||||||
public string GetLinkedPortalPath() => _linkedPortalPathInRoom;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Returns the world position where the room should be spawned.
|
|
||||||
/// Uses room spawn point if set, otherwise calculates based on portal position.
|
|
||||||
/// </summary>
|
|
||||||
public Vector3 GetRoomSpawnPosition()
|
|
||||||
{
|
|
||||||
if (_roomSpawnPoint != null)
|
|
||||||
return _roomSpawnPoint.position;
|
|
||||||
return transform.position + (transform.up + transform.right).normalized * _loadRoomDistance;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Returns the rotation for spawning the room.
|
|
||||||
/// </summary>
|
|
||||||
public Quaternion GetRoomSpawnRotation()
|
|
||||||
{
|
|
||||||
if (_roomSpawnPoint != null)
|
|
||||||
return _roomSpawnPoint.rotation;
|
|
||||||
return transform.rotation;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Called by PortalRoomManager when the associated room is loaded.
|
|
||||||
/// </summary>
|
|
||||||
public void OnRoomLoaded(GameObject room)
|
|
||||||
{
|
|
||||||
_loadedRoomInstance = room;
|
|
||||||
RoomLoaded?.Invoke();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Called by PortalRoomManager when the associated room is unloaded.
|
|
||||||
/// Clears the linked portal reference.
|
|
||||||
/// </summary>
|
|
||||||
public void OnRoomUnloaded()
|
|
||||||
{
|
|
||||||
RoomUnloaded?.Invoke();
|
|
||||||
_loadedRoomInstance = null;
|
|
||||||
if (_roomPrefabToLoad != null)
|
|
||||||
_linkedPortal = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Determines which side of the portal a position is on.
|
|
||||||
/// Returns +1 if in front (same direction as portal forward), -1 if behind.
|
|
||||||
/// Uses dot product to compare position relative to portal plane.
|
|
||||||
/// </summary>
|
|
||||||
public int SideOfPortal(Vector3 pos)
|
|
||||||
{
|
|
||||||
return Math.Sign(Vector3.Dot(pos - transform.position, transform.forward));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Checks if two positions are on the same side of the portal.
|
|
||||||
/// </summary>
|
|
||||||
public bool SameSideOfPortal(Vector3 posA, Vector3 posB)
|
|
||||||
{
|
|
||||||
return SideOfPortal(posA) == SideOfPortal(posB);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Portal GetLinkedPortal() => _linkedPortal;
|
|
||||||
|
|
||||||
public string GetPortalId() => _portalId;
|
|
||||||
|
|
||||||
public string GetLinkedPortalId() => _linkedPortalId;
|
|
||||||
|
|
||||||
public PortalDoor GetDoor() => _door;
|
|
||||||
|
|
||||||
public float GetRoomLoadDistance() => _loadRoomDistance;
|
|
||||||
|
|
||||||
private Vector3 PortalCamPos => _portalCam.transform.position;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Unity editor callback to maintain bidirectional portal links in the inspector.
|
|
||||||
/// </summary>
|
|
||||||
private void OnValidate()
|
|
||||||
{
|
|
||||||
if (_linkedPortal != null)
|
|
||||||
_linkedPortal._linkedPortal = this;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
fileFormatVersion: 2
|
|
||||||
guid: 5bee016a7303e2c458585f5cc66817e2
|
|
||||||
MonoImporter:
|
|
||||||
externalObjects: {}
|
|
||||||
serializedVersion: 2
|
|
||||||
defaultReferences: []
|
|
||||||
executionOrder: 200
|
|
||||||
icon: {instanceID: 0}
|
|
||||||
userData:
|
|
||||||
assetBundleName:
|
|
||||||
assetBundleVariant:
|
|
||||||
@@ -1,307 +0,0 @@
|
|||||||
using UnityEngine;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using Unheimlich.Features.Portals;
|
|
||||||
using Unheimlich.Core.Utils;
|
|
||||||
using Unheimlich.Dev;
|
|
||||||
|
|
||||||
namespace Unheimlich.Features.Portals
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Creates virtual audio proxies for sounds coming from the other side of a portal.
|
|
||||||
/// When a portal links to a room, sounds from that room are projected through the portal
|
|
||||||
/// so the player hears them spatially from the portal's location. This creates seamless
|
|
||||||
/// audio transitions when walking through portals.
|
|
||||||
/// </summary>
|
|
||||||
public class PortalAudioProxy : MonoBehaviour
|
|
||||||
{
|
|
||||||
[Header("Settings")]
|
|
||||||
[SerializeField] private float _maxDistance = 20f;
|
|
||||||
[SerializeField] private float _fadeStartDistance = 15f;
|
|
||||||
[SerializeField] private float _transitionDistance = 2f;
|
|
||||||
[SerializeField] private bool _useLowPassFilter = true;
|
|
||||||
[SerializeField] private float _lowPassCutoff = 5000f;
|
|
||||||
[SerializeField] private float _timeSyncThreshold = 0.1f;
|
|
||||||
[SerializeField] private float _minSpread = 60f;
|
|
||||||
[SerializeField] private float _maxSpread = 180f;
|
|
||||||
// [SerializeField] private float _volumeRolloffPower = 2f;
|
|
||||||
|
|
||||||
[Header("Debug")]
|
|
||||||
[SerializeField] private List<AudioSource> _remoteSources = new List<AudioSource>();
|
|
||||||
|
|
||||||
private Portal _portal;
|
|
||||||
private Dictionary<AudioSource, AudioSource> _proxyMapping = new Dictionary<AudioSource, AudioSource>();
|
|
||||||
private Dictionary<AudioSource, AudioLowPassFilter> _filterMapping = new Dictionary<AudioSource, AudioLowPassFilter>();
|
|
||||||
private Camera _playerCamera;
|
|
||||||
|
|
||||||
private GameObject _lastLoadedRoomInstance;
|
|
||||||
private bool _needsRefresh;
|
|
||||||
|
|
||||||
private void Awake()
|
|
||||||
{
|
|
||||||
_portal = GetComponent<Portal>();
|
|
||||||
_playerCamera = Camera.main;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnEnable()
|
|
||||||
{
|
|
||||||
if (_portal != null)
|
|
||||||
{
|
|
||||||
_portal.RoomLoaded += OnRoomLoaded;
|
|
||||||
_portal.RoomUnloaded += OnRoomUnloaded;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnDisable()
|
|
||||||
{
|
|
||||||
if (_portal != null)
|
|
||||||
{
|
|
||||||
_portal.RoomLoaded -= OnRoomLoaded;
|
|
||||||
_portal.RoomUnloaded -= OnRoomUnloaded;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnRoomLoaded()
|
|
||||||
{
|
|
||||||
_needsRefresh = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnRoomUnloaded()
|
|
||||||
{
|
|
||||||
_needsRefresh = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Main update loop: refreshes audio sources when the room changes,
|
|
||||||
/// then updates all proxy positions and volumes each frame.
|
|
||||||
/// </summary>
|
|
||||||
private void Update()
|
|
||||||
{
|
|
||||||
if (_portal == null || _portal.LinkedPortal == null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
GameObject currentLoadedRoom = _portal.LoadedRoomInstance;
|
|
||||||
|
|
||||||
if (currentLoadedRoom != _lastLoadedRoomInstance)
|
|
||||||
{
|
|
||||||
_lastLoadedRoomInstance = currentLoadedRoom;
|
|
||||||
_needsRefresh = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_needsRefresh)
|
|
||||||
{
|
|
||||||
RefreshRemoteSources();
|
|
||||||
_needsRefresh = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
UpdateProxies();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Scans the loaded room for all AudioSource components and caches them.
|
|
||||||
/// Called whenever a new room is loaded through the portal.
|
|
||||||
/// </summary>
|
|
||||||
private void RefreshRemoteSources()
|
|
||||||
{
|
|
||||||
GameObject linkedRoom = _portal.LoadedRoomInstance;
|
|
||||||
if (linkedRoom == null)
|
|
||||||
{
|
|
||||||
_remoteSources.Clear();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
AudioSource[] sources = linkedRoom.GetComponentsInChildren<AudioSource>();
|
|
||||||
|
|
||||||
_remoteSources.Clear();
|
|
||||||
_remoteSources.AddRange(sources);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Updates all audio proxies each frame:
|
|
||||||
/// - Syncs playback state with remote sources
|
|
||||||
/// - Positions proxies at the portal location
|
|
||||||
/// - Adjusts volume based on distance and player orientation
|
|
||||||
/// - Applies low-pass filtering for sounds through the portal
|
|
||||||
/// This creates the illusion that sounds are coming from the portal opening.
|
|
||||||
/// </summary>
|
|
||||||
private void UpdateProxies()
|
|
||||||
{
|
|
||||||
Camera playerCam = _playerCamera;
|
|
||||||
if (playerCam == null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
Portal linkedPortal = _portal.LinkedPortal;
|
|
||||||
if (linkedPortal == null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
Vector3 playerPos = playerCam.transform.position;
|
|
||||||
Vector3 portalAPos = transform.position;
|
|
||||||
|
|
||||||
float distPlayerToPortalA = Vector3.Distance(playerPos, portalAPos);
|
|
||||||
|
|
||||||
Vector3 offsetFromPortalA = playerPos - portalAPos;
|
|
||||||
float dotToPortalA = Vector3.Dot(offsetFromPortalA.normalized, transform.forward);
|
|
||||||
bool playerFacingPortal = dotToPortalA > 0;
|
|
||||||
|
|
||||||
float transitionBlend = 0f;
|
|
||||||
if (playerFacingPortal && distPlayerToPortalA < _transitionDistance)
|
|
||||||
{
|
|
||||||
transitionBlend = 1f - (distPlayerToPortalA / _transitionDistance);
|
|
||||||
}
|
|
||||||
|
|
||||||
List<AudioSource> toRemove = new List<AudioSource>();
|
|
||||||
|
|
||||||
foreach (var remoteSource in _remoteSources)
|
|
||||||
{
|
|
||||||
if (remoteSource == null)
|
|
||||||
{
|
|
||||||
toRemove.Add(remoteSource);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!remoteSource.isPlaying)
|
|
||||||
{
|
|
||||||
if (_proxyMapping.ContainsKey(remoteSource))
|
|
||||||
{
|
|
||||||
AudioSource proxy = _proxyMapping[remoteSource];
|
|
||||||
if (proxy != null && proxy.isPlaying)
|
|
||||||
proxy.Stop();
|
|
||||||
}
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
AudioSource localProxy = GetOrCreateProxy(remoteSource);
|
|
||||||
|
|
||||||
if (localProxy.clip != remoteSource.clip)
|
|
||||||
{
|
|
||||||
localProxy.clip = remoteSource.clip;
|
|
||||||
localProxy.loop = remoteSource.loop;
|
|
||||||
localProxy.time = remoteSource.time;
|
|
||||||
localProxy.Play();
|
|
||||||
}
|
|
||||||
else if (!localProxy.isPlaying)
|
|
||||||
{
|
|
||||||
localProxy.time = remoteSource.time;
|
|
||||||
localProxy.Play();
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
float timeDifference = Mathf.Abs(localProxy.time - remoteSource.time);
|
|
||||||
if (timeDifference > _timeSyncThreshold)
|
|
||||||
localProxy.time = remoteSource.time;
|
|
||||||
}
|
|
||||||
|
|
||||||
localProxy.pitch = remoteSource.pitch;
|
|
||||||
|
|
||||||
if (_useLowPassFilter && _filterMapping.ContainsKey(remoteSource))
|
|
||||||
{
|
|
||||||
AudioLowPassFilter filter = _filterMapping[remoteSource];
|
|
||||||
if (filter != null)
|
|
||||||
filter.enabled = !_portal.IsDoorOpen;
|
|
||||||
}
|
|
||||||
|
|
||||||
Vector3 virtualPlayerPos = linkedPortal.transform.TransformPoint(transform.InverseTransformPoint(playerPos));
|
|
||||||
|
|
||||||
Vector3 proxyWorldPos = transform.TransformPoint(linkedPortal.transform.InverseTransformPoint(remoteSource.transform.position));
|
|
||||||
localProxy.transform.position = proxyWorldPos;
|
|
||||||
|
|
||||||
float baseVolume = remoteSource.volume;
|
|
||||||
AudioSourceDemo demo = remoteSource.GetComponent<AudioSourceDemo>();
|
|
||||||
if (demo != null)
|
|
||||||
baseVolume = demo.OriginalVolume;
|
|
||||||
|
|
||||||
localProxy.volume = baseVolume * (1f - transitionBlend);
|
|
||||||
|
|
||||||
if (demo != null)
|
|
||||||
{
|
|
||||||
float sourceFade = demo.GetDistanceFadeMultiplier(virtualPlayerPos);
|
|
||||||
localProxy.volume *= sourceFade;
|
|
||||||
}
|
|
||||||
|
|
||||||
float distanceToPortal = Vector3.Distance(playerPos, portalAPos);
|
|
||||||
if (distanceToPortal > _fadeStartDistance)
|
|
||||||
{
|
|
||||||
float fadeRange = _maxDistance - _fadeStartDistance;
|
|
||||||
float fadeAmount = (distanceToPortal - _fadeStartDistance) / fadeRange;
|
|
||||||
fadeAmount = Mathf.Clamp01(fadeAmount);
|
|
||||||
localProxy.volume *= (1f - fadeAmount);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (localProxy.volume <= PortalConstants.MIN_VOLUME_THRESHOLD && localProxy.isPlaying)
|
|
||||||
{
|
|
||||||
localProxy.Stop();
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
localProxy.spread = Mathf.Lerp(_minSpread, _maxSpread, transitionBlend);
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var source in toRemove)
|
|
||||||
{
|
|
||||||
if (_proxyMapping.ContainsKey(source))
|
|
||||||
{
|
|
||||||
AudioSource proxy = _proxyMapping[source];
|
|
||||||
if (proxy != null)
|
|
||||||
Destroy(proxy.gameObject);
|
|
||||||
|
|
||||||
_proxyMapping.Remove(source);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_filterMapping.ContainsKey(source))
|
|
||||||
_filterMapping.Remove(source);
|
|
||||||
|
|
||||||
_remoteSources.Remove(source);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private AudioSource GetOrCreateProxy(AudioSource remoteSource)
|
|
||||||
{
|
|
||||||
if (_proxyMapping.ContainsKey(remoteSource) && _proxyMapping[remoteSource] != null)
|
|
||||||
return _proxyMapping[remoteSource];
|
|
||||||
|
|
||||||
GameObject proxyObj = new GameObject($"AudioProxy_{remoteSource.name}");
|
|
||||||
proxyObj.transform.SetParent(transform);
|
|
||||||
proxyObj.transform.localPosition = Vector3.zero;
|
|
||||||
|
|
||||||
AudioSource proxy = proxyObj.AddComponent<AudioSource>();
|
|
||||||
proxy.spatialBlend = 1f;
|
|
||||||
proxy.spatialize = true;
|
|
||||||
proxy.spatializePostEffects = true;
|
|
||||||
proxy.reverbZoneMix = 1f;
|
|
||||||
proxy.spread = 60f;
|
|
||||||
proxy.rolloffMode = remoteSource.rolloffMode;
|
|
||||||
proxy.minDistance = remoteSource.minDistance;
|
|
||||||
proxy.maxDistance = remoteSource.maxDistance;
|
|
||||||
proxy.playOnAwake = false;
|
|
||||||
|
|
||||||
if (_useLowPassFilter)
|
|
||||||
{
|
|
||||||
AudioLowPassFilter filter = proxyObj.AddComponent<AudioLowPassFilter>();
|
|
||||||
filter.cutoffFrequency = _lowPassCutoff;
|
|
||||||
filter.enabled = false;
|
|
||||||
_filterMapping[remoteSource] = filter;
|
|
||||||
}
|
|
||||||
|
|
||||||
_proxyMapping[remoteSource] = proxy;
|
|
||||||
|
|
||||||
return proxy;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnDestroy()
|
|
||||||
{
|
|
||||||
foreach (var kvp in _proxyMapping)
|
|
||||||
{
|
|
||||||
if (kvp.Value != null)
|
|
||||||
Destroy(kvp.Value.gameObject);
|
|
||||||
}
|
|
||||||
_proxyMapping.Clear();
|
|
||||||
_filterMapping.Clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnDrawGizmosSelected()
|
|
||||||
{
|
|
||||||
Gizmos.color = Color.cyan;
|
|
||||||
Gizmos.DrawWireSphere(transform.position, _maxDistance);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
fileFormatVersion: 2
|
|
||||||
guid: 1ef7477556cb1fa45ab6cf4a9422c405
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
using UnityEngine;
|
|
||||||
using Unheimlich.World.Environment;
|
|
||||||
|
|
||||||
namespace Unheimlich.Features.Portals
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// A door that is synchronized between linked portals.
|
|
||||||
/// When opened/closed, it automatically syncs the state with the door on the other side.
|
|
||||||
/// </summary>
|
|
||||||
public class PortalDoor : DoorInteractable
|
|
||||||
{
|
|
||||||
private Portal _parentPortal;
|
|
||||||
|
|
||||||
protected override void Start()
|
|
||||||
{
|
|
||||||
base.Start();
|
|
||||||
_parentPortal = GetComponentInParent<Portal>();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Handles interaction with the door.
|
|
||||||
/// Toggles both this door and the linked portal's door simultaneously.
|
|
||||||
/// </summary>
|
|
||||||
public override void Interact()
|
|
||||||
{
|
|
||||||
SyncToggle();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Toggles this door and synchronizes the state with the linked portal's door.
|
|
||||||
/// Ensures both doors open/close together for a consistent experience.
|
|
||||||
/// </summary>
|
|
||||||
private void SyncToggle()
|
|
||||||
{
|
|
||||||
ToggleDoor();
|
|
||||||
|
|
||||||
if (_parentPortal != null && _parentPortal.GetLinkedPortal() != null)
|
|
||||||
{
|
|
||||||
PortalDoor linkedDoor = _parentPortal.GetLinkedPortal().GetDoor();
|
|
||||||
if (linkedDoor != null)
|
|
||||||
linkedDoor.ToggleDoor();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
fileFormatVersion: 2
|
|
||||||
guid: f96b3c121a38b8e4784628da571e6c5b
|
|
||||||
@@ -1,67 +0,0 @@
|
|||||||
using UnityEngine;
|
|
||||||
using UnityEngine.Rendering;
|
|
||||||
|
|
||||||
namespace Unheimlich.Features.Portals
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Main orchestrator for the portal rendering system.
|
|
||||||
/// Attach this to the main camera to enable seamless portal rendering.
|
|
||||||
/// Manages the render pipeline for all active portals in the scene.
|
|
||||||
///
|
|
||||||
/// URP-compatible version using RenderPipelineManager callbacks.
|
|
||||||
/// </summary>
|
|
||||||
public class PortalMainCamera : MonoBehaviour
|
|
||||||
{
|
|
||||||
private Camera _mainCamera;
|
|
||||||
|
|
||||||
private void Awake()
|
|
||||||
{
|
|
||||||
_mainCamera = GetComponent<Camera>();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnEnable()
|
|
||||||
{
|
|
||||||
RenderPipelineManager.beginCameraRendering += OnBeginCameraRendering;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnDisable()
|
|
||||||
{
|
|
||||||
RenderPipelineManager.beginCameraRendering -= OnBeginCameraRendering;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Called before each camera renders in URP pipeline.
|
|
||||||
/// This is where we inject the portal rendering logic before the main camera renders.
|
|
||||||
/// </summary>
|
|
||||||
private void OnBeginCameraRendering(ScriptableRenderContext context, Camera camera)
|
|
||||||
{
|
|
||||||
// Only render portals for the main camera, not for portal cameras themselves
|
|
||||||
if (camera != _mainCamera)
|
|
||||||
return;
|
|
||||||
|
|
||||||
// Phase 1: Prepare all portals for rendering (update shader clipping parameters)
|
|
||||||
for (int i = 0; i < Portal.AllPortals.Count; i++)
|
|
||||||
{
|
|
||||||
var p = Portal.AllPortals[i];
|
|
||||||
if (p != null && p.isActiveAndEnabled)
|
|
||||||
p.PrePortalRender();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Phase 2: Render all portals (create the view through each portal)
|
|
||||||
for (int i = 0; i < Portal.AllPortals.Count; i++)
|
|
||||||
{
|
|
||||||
var p = Portal.AllPortals[i];
|
|
||||||
if (p != null && p.isActiveAndEnabled)
|
|
||||||
p.Render();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Phase 3: Clean up after portal rendering
|
|
||||||
for (int i = 0; i < Portal.AllPortals.Count; i++)
|
|
||||||
{
|
|
||||||
var p = Portal.AllPortals[i];
|
|
||||||
if (p != null && p.isActiveAndEnabled)
|
|
||||||
p.PostPortalRender();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
fileFormatVersion: 2
|
|
||||||
guid: 98eb193612a097646bb0d333bb4aae51
|
|
||||||
MonoImporter:
|
|
||||||
externalObjects: {}
|
|
||||||
serializedVersion: 2
|
|
||||||
defaultReferences: []
|
|
||||||
executionOrder: 300
|
|
||||||
icon: {instanceID: 0}
|
|
||||||
userData:
|
|
||||||
assetBundleName:
|
|
||||||
assetBundleVariant:
|
|
||||||
@@ -1,288 +0,0 @@
|
|||||||
using System.Collections.Generic;
|
|
||||||
using UnityEngine;
|
|
||||||
using Unheimlich.Core;
|
|
||||||
using Unheimlich.Features.Portals;
|
|
||||||
|
|
||||||
namespace Unheimlich.Features.Portals
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Manages dynamic loading and unloading of rooms connected by portals.
|
|
||||||
/// Monitors player distance to portals and loads/unloads room instances accordingly.
|
|
||||||
/// Supports room pooling (keeping inactive rooms in memory) and distance-based activation.
|
|
||||||
/// </summary>
|
|
||||||
public class PortalRoomManager : Singleton<PortalRoomManager>
|
|
||||||
{
|
|
||||||
|
|
||||||
[Header("Settings")]
|
|
||||||
[SerializeField] private float _loadDistance = 15f;
|
|
||||||
[Tooltip("Distance at which a loaded room gets deactivated (must be > load distance to avoid flip-flop)")]
|
|
||||||
[SerializeField] private float _deactivateDistance = 50f;
|
|
||||||
[Tooltip("Distance at which a deactivated room gets reactivated (should be < deactivate distance)")]
|
|
||||||
[SerializeField] private float _reactivateDistance = 30f;
|
|
||||||
[SerializeField] private bool _keepRoomsInMemory = true;
|
|
||||||
|
|
||||||
[Header("Distance Calculation")]
|
|
||||||
[Tooltip("If true, calculates distance from player to actual room position (portal + loadRoomDistance)")]
|
|
||||||
[SerializeField] private bool _useRealRoomDistance = true;
|
|
||||||
|
|
||||||
private Transform _player;
|
|
||||||
private Dictionary<string, LoadedRoom> _loadedRooms = new Dictionary<string, LoadedRoom>();
|
|
||||||
private List<Portal> _registeredPortals = new List<Portal>();
|
|
||||||
private GameObject _currentPlayerRoom = null;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Represents a room instance that has been loaded through a portal.
|
|
||||||
/// Caches room bounds and colliders for efficient distance/containment calculations.
|
|
||||||
/// </summary>
|
|
||||||
private class LoadedRoom
|
|
||||||
{
|
|
||||||
public GameObject RoomInstance;
|
|
||||||
public string RoomId;
|
|
||||||
public Vector3 LoadPosition;
|
|
||||||
public Portal SourcePortal;
|
|
||||||
public Bounds CachedBounds;
|
|
||||||
public bool BoundsCalculated = false;
|
|
||||||
public Collider[] CachedColliders;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void Start()
|
|
||||||
{
|
|
||||||
_player = GameObject.FindGameObjectWithTag("Player")?.transform;
|
|
||||||
if (_player == null)
|
|
||||||
Debug.LogWarning("Player not found! Make sure your player has the 'Player' tag.");
|
|
||||||
}
|
|
||||||
|
|
||||||
private void Update()
|
|
||||||
{
|
|
||||||
if (_player == null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
CheckPortalsForLoading();
|
|
||||||
CheckRoomsForUnloading();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void RegisterPortal(Portal portal)
|
|
||||||
{
|
|
||||||
if (!_registeredPortals.Contains(portal))
|
|
||||||
_registeredPortals.Add(portal);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void UnregisterPortal(Portal portal)
|
|
||||||
{
|
|
||||||
if (_registeredPortals.Contains(portal))
|
|
||||||
_registeredPortals.Remove(portal);
|
|
||||||
}
|
|
||||||
|
|
||||||
public GameObject GetLoadedRoom(string roomId)
|
|
||||||
{
|
|
||||||
if (_loadedRooms.TryGetValue(roomId, out LoadedRoom loadedRoom))
|
|
||||||
return loadedRoom.RoomInstance;
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Checks all registered portals and loads rooms for portals within range.
|
|
||||||
/// Optionally accounts for the portal's configured room spawn distance.
|
|
||||||
/// </summary>
|
|
||||||
private void CheckPortalsForLoading()
|
|
||||||
{
|
|
||||||
foreach (Portal portal in _registeredPortals)
|
|
||||||
{
|
|
||||||
float distanceToPortal = Vector3.Distance(_player.position, portal.transform.position);
|
|
||||||
float effectiveLoadDistance = _loadDistance;
|
|
||||||
|
|
||||||
if (_useRealRoomDistance)
|
|
||||||
effectiveLoadDistance += portal.GetRoomLoadDistance();
|
|
||||||
|
|
||||||
if (distanceToPortal <= effectiveLoadDistance)
|
|
||||||
LoadRoom(portal);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Checks all loaded rooms and determines if they should be deactivated, reactivated, or unloaded
|
|
||||||
/// based on distance thresholds. Prevents unloading the room the player is currently inside.
|
|
||||||
/// </summary>
|
|
||||||
private void CheckRoomsForUnloading()
|
|
||||||
{
|
|
||||||
List<string> roomsToUnload = new List<string>();
|
|
||||||
|
|
||||||
UpdateCurrentPlayerRoom();
|
|
||||||
|
|
||||||
foreach (var kvp in _loadedRooms)
|
|
||||||
{
|
|
||||||
LoadedRoom loadedRoom = kvp.Value;
|
|
||||||
if (loadedRoom.RoomInstance == null)
|
|
||||||
{
|
|
||||||
roomsToUnload.Add(kvp.Key);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (loadedRoom.RoomInstance == _currentPlayerRoom)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
float distanceToPortal = GetDistanceToRoom(loadedRoom);
|
|
||||||
float effectiveDeactivateDistance = _deactivateDistance;
|
|
||||||
float effectiveReactivateDistance = _reactivateDistance;
|
|
||||||
|
|
||||||
if (_useRealRoomDistance)
|
|
||||||
{
|
|
||||||
float roomOffset = loadedRoom.SourcePortal.GetRoomLoadDistance();
|
|
||||||
effectiveDeactivateDistance += roomOffset;
|
|
||||||
effectiveReactivateDistance += roomOffset;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (distanceToPortal > effectiveDeactivateDistance)
|
|
||||||
{
|
|
||||||
if (loadedRoom.RoomInstance.activeSelf)
|
|
||||||
roomsToUnload.Add(kvp.Key);
|
|
||||||
}
|
|
||||||
else if (distanceToPortal <= effectiveReactivateDistance)
|
|
||||||
{
|
|
||||||
if (!loadedRoom.RoomInstance.activeSelf)
|
|
||||||
loadedRoom.RoomInstance.SetActive(true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (string roomId in roomsToUnload)
|
|
||||||
{
|
|
||||||
UnloadRoom(roomId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Calculates the distance from the player to a loaded room.
|
|
||||||
/// Lazily calculates and caches room bounds on first call for performance.
|
|
||||||
/// </summary>
|
|
||||||
private float GetDistanceToRoom(LoadedRoom loadedRoom)
|
|
||||||
{
|
|
||||||
if (!loadedRoom.BoundsCalculated)
|
|
||||||
{
|
|
||||||
loadedRoom.CachedBounds = CalculateRoomBounds(loadedRoom.RoomInstance);
|
|
||||||
loadedRoom.BoundsCalculated = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return Vector3.Distance(_player.position, loadedRoom.CachedBounds.center);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Calculates the combined bounding box of all renderers in a room.
|
|
||||||
/// Used to determine room center for distance calculations.
|
|
||||||
/// </summary>
|
|
||||||
private Bounds CalculateRoomBounds(GameObject room)
|
|
||||||
{
|
|
||||||
Renderer[] renderers = room.GetComponentsInChildren<Renderer>();
|
|
||||||
if (renderers.Length == 0)
|
|
||||||
return new Bounds(room.transform.position, Vector3.one);
|
|
||||||
|
|
||||||
Bounds bounds = renderers[0].bounds;
|
|
||||||
for (int i = 1; i < renderers.Length; i++)
|
|
||||||
bounds.Encapsulate(renderers[i].bounds);
|
|
||||||
|
|
||||||
return bounds;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Determines which room (if any) currently contains the player.
|
|
||||||
/// Uses cached collider bounds to detect room occupancy.
|
|
||||||
/// </summary>
|
|
||||||
private void UpdateCurrentPlayerRoom()
|
|
||||||
{
|
|
||||||
_currentPlayerRoom = null;
|
|
||||||
|
|
||||||
foreach (var kvp in _loadedRooms)
|
|
||||||
{
|
|
||||||
LoadedRoom loadedRoom = kvp.Value;
|
|
||||||
if (loadedRoom.RoomInstance == null)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
Collider[] roomColliders = loadedRoom.CachedColliders;
|
|
||||||
if (roomColliders == null) continue;
|
|
||||||
|
|
||||||
foreach (Collider col in roomColliders)
|
|
||||||
{
|
|
||||||
if (col != null && col.bounds.Contains(_player.position))
|
|
||||||
{
|
|
||||||
_currentPlayerRoom = loadedRoom.RoomInstance;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Instantiates a room from a portal's prefab at the configured spawn point.
|
|
||||||
/// If the room is already loaded, reactivates it instead of creating a new instance.
|
|
||||||
/// Notifies the portal when its room is successfully loaded.
|
|
||||||
/// </summary>
|
|
||||||
private void LoadRoom(Portal portal)
|
|
||||||
{
|
|
||||||
GameObject roomPrefab = portal.GetRoomPrefab();
|
|
||||||
if (roomPrefab == null)
|
|
||||||
{
|
|
||||||
Debug.LogWarning($"[LOAD] Portal {portal.name} has no prefab!");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
string roomId = portal.GetRoomId();
|
|
||||||
|
|
||||||
if (_loadedRooms.TryGetValue(roomId, out LoadedRoom existingRoom))
|
|
||||||
{
|
|
||||||
if (existingRoom.RoomInstance != null)
|
|
||||||
existingRoom.RoomInstance.SetActive(true);
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Vector3 spawnPosition = portal.GetRoomSpawnPosition();
|
|
||||||
Quaternion spawnRotation = portal.GetRoomSpawnRotation();
|
|
||||||
|
|
||||||
|
|
||||||
GameObject roomInstance = Instantiate(roomPrefab, spawnPosition, spawnRotation);
|
|
||||||
roomInstance.name = $"{roomPrefab.name}_Loaded";
|
|
||||||
|
|
||||||
foreach (var renderer in roomInstance.GetComponentsInChildren<MeshRenderer>())
|
|
||||||
renderer.gameObject.SendMessage("OnBecameVisible", SendMessageOptions.DontRequireReceiver);
|
|
||||||
|
|
||||||
LoadedRoom loadedRoom = new LoadedRoom
|
|
||||||
{
|
|
||||||
RoomInstance = roomInstance,
|
|
||||||
RoomId = roomId,
|
|
||||||
LoadPosition = portal.transform.position,
|
|
||||||
SourcePortal = portal,
|
|
||||||
CachedColliders = roomInstance.GetComponentsInChildren<Collider>()
|
|
||||||
};
|
|
||||||
|
|
||||||
_loadedRooms.Add(roomId, loadedRoom);
|
|
||||||
|
|
||||||
portal.OnRoomLoaded(roomInstance);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Deactivates or destroys a loaded room based on the _keepRoomsInMemory setting.
|
|
||||||
/// If keeping in memory, the room is deactivated but not destroyed (faster to reactivate).
|
|
||||||
/// If not keeping in memory, the room is fully destroyed and removed from tracking.
|
|
||||||
/// Notifies the source portal when its room is unloaded.
|
|
||||||
/// </summary>
|
|
||||||
private void UnloadRoom(string roomId)
|
|
||||||
{
|
|
||||||
if (_loadedRooms.TryGetValue(roomId, out LoadedRoom loadedRoom))
|
|
||||||
{
|
|
||||||
if (loadedRoom.RoomInstance != null)
|
|
||||||
{
|
|
||||||
if (loadedRoom.SourcePortal != null)
|
|
||||||
loadedRoom.SourcePortal.OnRoomUnloaded();
|
|
||||||
|
|
||||||
if (_keepRoomsInMemory)
|
|
||||||
loadedRoom.RoomInstance.SetActive(false);
|
|
||||||
else
|
|
||||||
{
|
|
||||||
Destroy(loadedRoom.RoomInstance);
|
|
||||||
_loadedRooms.Remove(roomId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
fileFormatVersion: 2
|
|
||||||
guid: 25abbf68f203db94381b5c22eb1573bc
|
|
||||||
MonoImporter:
|
|
||||||
externalObjects: {}
|
|
||||||
serializedVersion: 2
|
|
||||||
defaultReferences: []
|
|
||||||
executionOrder: 100
|
|
||||||
icon: {instanceID: 0}
|
|
||||||
userData:
|
|
||||||
assetBundleName:
|
|
||||||
assetBundleVariant:
|
|
||||||
@@ -1,148 +0,0 @@
|
|||||||
using System.Collections.Generic;
|
|
||||||
using UnityEngine;
|
|
||||||
using Unheimlich.Core.Utils;
|
|
||||||
|
|
||||||
namespace Unheimlich.Features.Portals
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Component that allows an object to travel through portals.
|
|
||||||
/// Handles teleportation, visual cloning when crossing, and shader-based slicing.
|
|
||||||
/// Attach this to any object that should be able to pass through portals (player, NPCs, etc.)
|
|
||||||
/// </summary>
|
|
||||||
[RequireComponent(typeof(CharacterController))]
|
|
||||||
public class PortalTraveller : MonoBehaviour
|
|
||||||
{
|
|
||||||
/// <summary>The visual representation of this traveller (mesh renderers)</summary>
|
|
||||||
public GameObject GraphicsObject;
|
|
||||||
|
|
||||||
/// <summary>Clone created when traveller enters portal threshold</summary>
|
|
||||||
public GameObject GraphicsClone { get; set; }
|
|
||||||
|
|
||||||
/// <summary>Tracks position relative to portal from previous frame to detect crossing</summary>
|
|
||||||
public Vector3 PreviousOffsetFromPortal { get; set; }
|
|
||||||
|
|
||||||
/// <summary>Materials of the original graphics (used for shader slicing)</summary>
|
|
||||||
public Material[] OriginalMaterials { get; set; }
|
|
||||||
|
|
||||||
/// <summary>Materials of the clone (used for shader slicing)</summary>
|
|
||||||
public Material[] CloneMaterials { get; set; }
|
|
||||||
|
|
||||||
private CharacterController _characterController;
|
|
||||||
|
|
||||||
private void Awake()
|
|
||||||
{
|
|
||||||
_characterController = GetComponent<CharacterController>();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Teleports this traveller through a portal.
|
|
||||||
/// Temporarily disables CharacterController to prevent physics conflicts during teleport.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="fromPortal">The portal being exited</param>
|
|
||||||
/// <param name="toPortal">The destination portal</param>
|
|
||||||
/// <param name="pos">New world position</param>
|
|
||||||
/// <param name="rot">New world rotation</param>
|
|
||||||
public virtual void Teleport(Transform fromPortal, Transform toPortal, Vector3 pos, Quaternion rot)
|
|
||||||
{
|
|
||||||
if (_characterController != null)
|
|
||||||
_characterController.enabled = false;
|
|
||||||
|
|
||||||
transform.position = pos;
|
|
||||||
transform.rotation = rot;
|
|
||||||
|
|
||||||
if (_characterController != null)
|
|
||||||
{
|
|
||||||
_characterController.enabled = true;
|
|
||||||
Physics.SyncTransforms();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Called when the traveller enters the portal's trigger zone.
|
|
||||||
/// Creates a visual clone that will appear on the other side of the portal.
|
|
||||||
/// </summary>
|
|
||||||
public virtual void EnterPortalThreshold()
|
|
||||||
{
|
|
||||||
if (GraphicsClone == null)
|
|
||||||
{
|
|
||||||
GraphicsClone = Instantiate(GraphicsObject);
|
|
||||||
GraphicsClone.transform.parent = GraphicsObject.transform.parent;
|
|
||||||
GraphicsClone.transform.localScale = GraphicsObject.transform.localScale;
|
|
||||||
OriginalMaterials = GetMaterials(GraphicsObject);
|
|
||||||
CloneMaterials = GetMaterials(GraphicsClone);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
GraphicsClone.SetActive(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Called when the traveller exits the portal's trigger zone without teleporting.
|
|
||||||
/// Hides the clone and resets shader slicing parameters.
|
|
||||||
/// </summary>
|
|
||||||
public virtual void ExitPortalThreshold()
|
|
||||||
{
|
|
||||||
GraphicsClone.SetActive(false);
|
|
||||||
|
|
||||||
for (int i = 0; i < OriginalMaterials.Length; i++)
|
|
||||||
OriginalMaterials[i].SetVector(PortalConstants.SHADER_PROPERTY_SLICE_NORMAL, Vector3.zero);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Sets the slice offset distance for shader-based clipping.
|
|
||||||
/// Used to hide/show parts of the traveller based on portal plane position.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="dst">Distance offset for slicing plane</param>
|
|
||||||
/// <param name="clone">If true, applies to clone materials; otherwise to original</param>
|
|
||||||
public void SetSliceOffsetDst(float dst, bool clone)
|
|
||||||
{
|
|
||||||
for (int i = 0; i < OriginalMaterials.Length; i++)
|
|
||||||
{
|
|
||||||
if (clone)
|
|
||||||
CloneMaterials[i].SetFloat(PortalConstants.SHADER_PROPERTY_SLICE_OFFSET, dst);
|
|
||||||
else
|
|
||||||
OriginalMaterials[i].SetFloat(PortalConstants.SHADER_PROPERTY_SLICE_OFFSET, dst);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Collects all materials from MeshRenderers in the graphics object hierarchy.
|
|
||||||
/// These materials will be modified with shader slicing parameters.
|
|
||||||
/// </summary>
|
|
||||||
private Material[] GetMaterials(GameObject g)
|
|
||||||
{
|
|
||||||
var renderers = g.GetComponentsInChildren<MeshRenderer>();
|
|
||||||
var matList = new List<Material>();
|
|
||||||
|
|
||||||
foreach (var renderer in renderers)
|
|
||||||
{
|
|
||||||
foreach (var mat in renderer.materials)
|
|
||||||
matList.Add(mat);
|
|
||||||
}
|
|
||||||
|
|
||||||
return matList.ToArray();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Shows or hides the original graphics by enabling/disabling renderers.
|
|
||||||
/// </summary>
|
|
||||||
public void SetGraphicsVisibility(bool visible)
|
|
||||||
{
|
|
||||||
var renderers = GraphicsObject.GetComponentsInChildren<Renderer>();
|
|
||||||
foreach (var renderer in renderers)
|
|
||||||
renderer.enabled = visible;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Shows or hides the clone graphics by enabling/disabling renderers.
|
|
||||||
/// </summary>
|
|
||||||
public void SetCloneVisibility(bool visible)
|
|
||||||
{
|
|
||||||
if (GraphicsClone != null)
|
|
||||||
{
|
|
||||||
var renderers = GraphicsClone.GetComponentsInChildren<Renderer>();
|
|
||||||
foreach (var renderer in renderers)
|
|
||||||
renderer.enabled = visible;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
fileFormatVersion: 2
|
|
||||||
guid: e825afa0efeb69949bf3ca8b3f99d7eb
|
|
||||||
MonoImporter:
|
|
||||||
externalObjects: {}
|
|
||||||
serializedVersion: 2
|
|
||||||
defaultReferences: []
|
|
||||||
executionOrder: 400
|
|
||||||
icon: {instanceID: 0}
|
|
||||||
userData:
|
|
||||||
assetBundleName:
|
|
||||||
assetBundleVariant:
|
|
||||||
@@ -1,14 +1,38 @@
|
|||||||
|
using OctoCore;
|
||||||
using OctoCore.Components;
|
using OctoCore.Components;
|
||||||
|
using System.Collections;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.ComponentModel;
|
||||||
|
using Unity.VisualScripting;
|
||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
|
using UnityEngine.Events;
|
||||||
|
using UnityEngine.InputSystem;
|
||||||
using UnityEngine.Rendering.Universal;
|
using UnityEngine.Rendering.Universal;
|
||||||
|
|
||||||
public class SpiralStaircase : MonoBehaviour
|
public class SpiralStaircase : MonoBehaviour
|
||||||
{
|
{
|
||||||
|
[System.Serializable]
|
||||||
|
public class DoorsPair
|
||||||
|
{
|
||||||
|
public Transform Left;
|
||||||
|
public Transform Right;
|
||||||
|
}
|
||||||
|
|
||||||
|
[System.Serializable]
|
||||||
|
public class State
|
||||||
|
{
|
||||||
|
public bool PlayerInside;
|
||||||
|
public int Turns;
|
||||||
|
public float TotalAngle;
|
||||||
|
[Range(0, 1)] public float Progression;
|
||||||
|
}
|
||||||
|
|
||||||
[System.Serializable]
|
[System.Serializable]
|
||||||
public class References
|
public class References
|
||||||
{
|
{
|
||||||
public UniversalRendererData RendererData;
|
public UniversalRendererData RendererData;
|
||||||
public GameObject Stairs;
|
public GameObject Stairs;
|
||||||
|
public DoorsPair[] Doors;
|
||||||
public GameObject[] Inside;
|
public GameObject[] Inside;
|
||||||
public GameObject[] Outside;
|
public GameObject[] Outside;
|
||||||
}
|
}
|
||||||
@@ -21,28 +45,43 @@ public class SpiralStaircase : MonoBehaviour
|
|||||||
public float EndAngle = 720;
|
public float EndAngle = 720;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[System.Serializable]
|
||||||
|
public class Events
|
||||||
|
{
|
||||||
|
public UnityEvent<bool> OnInside;
|
||||||
|
public UnityEvent OnTopReached;
|
||||||
|
public UnityEvent OnBottomReached;
|
||||||
|
}
|
||||||
|
|
||||||
|
[SerializeField, OctoReadOnly] private State _state;
|
||||||
[SerializeField] private References _references;
|
[SerializeField] private References _references;
|
||||||
[SerializeField] private Settings _settings;
|
[SerializeField] private Settings _settings;
|
||||||
|
[SerializeField] private Events _events;
|
||||||
|
|
||||||
private bool _playerInside;
|
private List<IEnumerator> _doorsCoroutine;
|
||||||
[SerializeField] private int _turns;
|
|
||||||
[SerializeField] private float _totalAngle;
|
void Awake()
|
||||||
[SerializeField] private float _progression;
|
{
|
||||||
|
_doorsCoroutine = new List<IEnumerator>();
|
||||||
|
for (int i = 0; i < _references.Doors.Length; i++)
|
||||||
|
_doorsCoroutine.Add(null);
|
||||||
|
}
|
||||||
|
|
||||||
void Start()
|
void Start()
|
||||||
{
|
{
|
||||||
ManageInside();
|
ManageInside();
|
||||||
|
_events.OnBottomReached?.Invoke();
|
||||||
}
|
}
|
||||||
|
|
||||||
void OnDisable()
|
void OnDisable()
|
||||||
{
|
{
|
||||||
_playerInside = false;
|
_state.PlayerInside = false;
|
||||||
ManageInside();
|
ManageInside();
|
||||||
}
|
}
|
||||||
|
|
||||||
void Update()
|
void Update()
|
||||||
{
|
{
|
||||||
if (_playerInside)
|
if (_state.PlayerInside)
|
||||||
Count();
|
Count();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -50,9 +89,9 @@ public class SpiralStaircase : MonoBehaviour
|
|||||||
{
|
{
|
||||||
if (OctoPlayer.Instance && OctoPlayer.Instance.gameObject == col.gameObject)
|
if (OctoPlayer.Instance && OctoPlayer.Instance.gameObject == col.gameObject)
|
||||||
{
|
{
|
||||||
if (!_playerInside)
|
if (!_state.PlayerInside)
|
||||||
{
|
{
|
||||||
_playerInside = true;
|
_state.PlayerInside = true;
|
||||||
ManageInside();
|
ManageInside();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -62,49 +101,104 @@ public class SpiralStaircase : MonoBehaviour
|
|||||||
{
|
{
|
||||||
if (OctoPlayer.Instance && OctoPlayer.Instance.gameObject == col.gameObject)
|
if (OctoPlayer.Instance && OctoPlayer.Instance.gameObject == col.gameObject)
|
||||||
{
|
{
|
||||||
if (_playerInside)
|
if (_state.PlayerInside)
|
||||||
{
|
{
|
||||||
_playerInside = false;
|
_state.PlayerInside = false;
|
||||||
ManageInside();
|
ManageInside();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void OpendDoors(bool open) { for (int i = 0; i < _references.Doors.Length; i++) OpendDoor(i, open); }
|
||||||
|
public void OpendDoor(int id) { OpendDoor(id, true); }
|
||||||
|
public void CloseDoor(int id) { OpendDoor(id, false); }
|
||||||
|
public void SetDoorOpened(int id) { OpendDoor(id, true, false); }
|
||||||
|
public void SetDoorClosed(int id) { OpendDoor(id, false, false); }
|
||||||
|
|
||||||
|
private void OpendDoor(int id, bool open, bool animate = true)
|
||||||
|
{
|
||||||
|
if (_doorsCoroutine[id] != null) StopCoroutine(_doorsCoroutine[id]);
|
||||||
|
StartCoroutine(_doorsCoroutine[id] = OpenDoorCoroutine(id, open, animate));
|
||||||
|
}
|
||||||
|
|
||||||
|
IEnumerator OpenDoorCoroutine(int id, bool open, bool animate)
|
||||||
|
{
|
||||||
|
const float duration = .6f;
|
||||||
|
|
||||||
|
DoorsPair door = _references.Doors[id];
|
||||||
|
|
||||||
|
Quaternion originRotL = door.Left.transform.localRotation;
|
||||||
|
Quaternion originRotR = door.Right.transform.localRotation;
|
||||||
|
Quaternion targetRotL = Quaternion.Euler(0, open ? 0 : 13.1f, 0);
|
||||||
|
Quaternion targetRotR = Quaternion.Euler(0, open ? 0 :-13.1f, 0);
|
||||||
|
|
||||||
|
if (animate)
|
||||||
|
{
|
||||||
|
for (float t = 0; t < duration; t += Time.deltaTime)
|
||||||
|
{
|
||||||
|
float lerp = .5f - Mathf.Cos(t / duration * Mathf.PI) * .5f;
|
||||||
|
|
||||||
|
door.Left.transform.localRotation = Quaternion.Lerp(originRotL, targetRotL, lerp);
|
||||||
|
door.Right.transform.localRotation = Quaternion.Lerp(originRotR, targetRotR, lerp);
|
||||||
|
|
||||||
|
yield return new WaitForEndOfFrame();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
door.Left.transform.localRotation = targetRotL;
|
||||||
|
door.Right.transform.localRotation = targetRotR;
|
||||||
|
|
||||||
|
_doorsCoroutine[id] = null;
|
||||||
|
}
|
||||||
|
|
||||||
private void Count()
|
private void Count()
|
||||||
{
|
{
|
||||||
if (!OctoPlayer.Instance) return;
|
if (!OctoPlayer.Instance) return;
|
||||||
|
|
||||||
|
|
||||||
Vector2[] steps = new Vector2[4] { Vector2.up, Vector2.left, Vector2.down, Vector2.right };
|
Vector2[] steps = new Vector2[4] { Vector2.up, Vector2.left, Vector2.down, Vector2.right };
|
||||||
Vector2 nextStep = steps[_turns % steps.Length];
|
Vector2 nextStep = steps[_state.Turns % steps.Length];
|
||||||
|
|
||||||
Vector3 relativePos3D = transform.InverseTransformPoint(OctoPlayer.Instance.transform.position);
|
Vector3 relativePos3D = transform.InverseTransformPoint(OctoPlayer.Instance.transform.position);
|
||||||
Vector2 relativePos2D = new Vector2(relativePos3D.x, relativePos3D.z);
|
Vector2 relativePos2D = new Vector2(relativePos3D.x, relativePos3D.z);
|
||||||
|
|
||||||
float quarterAngle = Vector2.Angle(nextStep, relativePos2D) * Mathf.Sign(Vector3.Cross(nextStep, relativePos2D).z);
|
float quarterAngle = Vector2.Angle(nextStep, relativePos2D) * Mathf.Sign(Vector3.Cross(nextStep, relativePos2D).z);
|
||||||
|
|
||||||
_totalAngle = Mathf.Max(0, _turns * 90 + quarterAngle);
|
_state.TotalAngle = Mathf.Max(0, _state.Turns * 90 + quarterAngle);
|
||||||
|
|
||||||
if (quarterAngle > 90 && quarterAngle < 100)
|
if (quarterAngle > 90 && quarterAngle < 100)
|
||||||
{
|
{
|
||||||
_turns++;
|
_state.Turns++;
|
||||||
}
|
}
|
||||||
else if (quarterAngle < 0 && _turns > 0)
|
else if (quarterAngle < 0 && _state.Turns > 0)
|
||||||
{
|
{
|
||||||
_turns--;
|
_state.Turns--;
|
||||||
}
|
}
|
||||||
|
|
||||||
_progression = Mathf.Clamp01(Mathf.InverseLerp(_settings.StartAngle, _settings.EndAngle, _totalAngle));
|
float progression = Mathf.Clamp01(Mathf.InverseLerp(_settings.StartAngle, _settings.EndAngle, _state.TotalAngle));
|
||||||
|
|
||||||
_references.Stairs.transform.localPosition = new Vector3(0, _progression * _settings.Offset, 0);
|
if (progression >= .5f && _state.Progression < .5f)
|
||||||
|
{
|
||||||
|
_events.OnTopReached?.Invoke();
|
||||||
|
}
|
||||||
|
else if (progression < .5f && _state.Progression >= .5f)
|
||||||
|
{
|
||||||
|
_events.OnBottomReached?.Invoke();
|
||||||
|
}
|
||||||
|
|
||||||
|
_state.Progression = progression;
|
||||||
|
|
||||||
|
_references.Stairs.transform.localPosition = new Vector3(0, _state.Progression * _settings.Offset, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ManageInside()
|
private void ManageInside()
|
||||||
{
|
{
|
||||||
foreach (GameObject obj in _references.Inside) obj.SetActive(_playerInside);
|
foreach (GameObject obj in _references.Inside) obj.SetActive(_state.PlayerInside);
|
||||||
foreach (GameObject obj in _references.Outside) obj.SetActive(!_playerInside);
|
foreach (GameObject obj in _references.Outside) obj.SetActive(!_state.PlayerInside);
|
||||||
|
|
||||||
Shader.SetGlobalFloat("_MaskEnabled", _playerInside ? 0 : 1);
|
_events.OnInside?.Invoke(_state.PlayerInside);
|
||||||
|
|
||||||
|
/*Shader.SetGlobalFloat("_MaskEnabled", _state.PlayerInside ? 0 : 1);
|
||||||
|
|
||||||
foreach (var feature in _references.RendererData.rendererFeatures)
|
foreach (var feature in _references.RendererData.rendererFeatures)
|
||||||
{
|
{
|
||||||
@@ -114,9 +208,9 @@ public class SpiralStaircase : MonoBehaviour
|
|||||||
|
|
||||||
if (feature.name == "HiddenThroughMask")
|
if (feature.name == "HiddenThroughMask")
|
||||||
{
|
{
|
||||||
feature.SetActive(!_playerInside);
|
feature.SetActive(!_state.PlayerInside);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}*/
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,106 +0,0 @@
|
|||||||
using UnityEngine;
|
|
||||||
using Unheimlich.Features.Portals;
|
|
||||||
|
|
||||||
namespace Unheimlich.World.CameraSystem
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Dynamically adjusts Unity's shadow rendering distance based on proximity to active portals.
|
|
||||||
/// When the player is near a portal with a loaded room, shadow distance is extended to ensure
|
|
||||||
/// shadows from the remote room are visible through the portal. This improves visual quality
|
|
||||||
/// while maintaining performance when far from portals.
|
|
||||||
/// </summary>
|
|
||||||
public class DynamicShadowDistance : MonoBehaviour
|
|
||||||
{
|
|
||||||
private const float PORTAL_CHECK_DISTANCE = 100f;
|
|
||||||
|
|
||||||
[Header("Shadow Distance Settings")]
|
|
||||||
[SerializeField] private float _baseShadowDistance = 50f;
|
|
||||||
[SerializeField] private float _extendedShadowDistance = 250f;
|
|
||||||
[SerializeField] private float _transitionSpeed = 2f;
|
|
||||||
|
|
||||||
[Header("Detection")]
|
|
||||||
[SerializeField] private float _checkInterval = 0.5f;
|
|
||||||
|
|
||||||
private float _targetShadowDistance;
|
|
||||||
private float _currentShadowDistance;
|
|
||||||
private float _nextCheckTime;
|
|
||||||
private Transform _playerTransform;
|
|
||||||
|
|
||||||
private void Awake()
|
|
||||||
{
|
|
||||||
_playerTransform = Camera.main != null ? Camera.main.transform : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void Start()
|
|
||||||
{
|
|
||||||
_currentShadowDistance = _baseShadowDistance;
|
|
||||||
_targetShadowDistance = _baseShadowDistance;
|
|
||||||
QualitySettings.shadowDistance = _baseShadowDistance;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void Update()
|
|
||||||
{
|
|
||||||
if (Time.time >= _nextCheckTime)
|
|
||||||
{
|
|
||||||
_nextCheckTime = Time.time + _checkInterval;
|
|
||||||
UpdateTargetShadowDistance();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!Mathf.Approximately(_currentShadowDistance, _targetShadowDistance))
|
|
||||||
{
|
|
||||||
_currentShadowDistance = Mathf.Lerp(_currentShadowDistance, _targetShadowDistance, Time.deltaTime * _transitionSpeed);
|
|
||||||
QualitySettings.shadowDistance = _currentShadowDistance;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Checks if any active portal with a loaded room is within range.
|
|
||||||
/// If found, extends shadow distance to cover the remote room.
|
|
||||||
/// </summary>
|
|
||||||
private void UpdateTargetShadowDistance()
|
|
||||||
{
|
|
||||||
bool needsExtendedDistance = false;
|
|
||||||
|
|
||||||
foreach (var portal in Portal.AllPortals)
|
|
||||||
{
|
|
||||||
if (portal == null || !portal.isActiveAndEnabled)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
if (portal.LoadedRoomInstance != null)
|
|
||||||
{
|
|
||||||
float distanceToPortal = Vector3.Distance(_playerTransform.position, portal.transform.position);
|
|
||||||
|
|
||||||
if (distanceToPortal < PORTAL_CHECK_DISTANCE)
|
|
||||||
{
|
|
||||||
needsExtendedDistance = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_targetShadowDistance = needsExtendedDistance ? _extendedShadowDistance : _baseShadowDistance;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnValidate()
|
|
||||||
{
|
|
||||||
if (_baseShadowDistance < 0)
|
|
||||||
_baseShadowDistance = 0;
|
|
||||||
if (_extendedShadowDistance < _baseShadowDistance)
|
|
||||||
_extendedShadowDistance = _baseShadowDistance;
|
|
||||||
}
|
|
||||||
|
|
||||||
#if UNITY_EDITOR
|
|
||||||
private void OnDrawGizmosSelected()
|
|
||||||
{
|
|
||||||
if (_playerTransform == null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
Gizmos.color = new Color(1f, 1f, 0f, 0.2f);
|
|
||||||
Gizmos.DrawWireSphere(_playerTransform.position, QualitySettings.shadowDistance);
|
|
||||||
|
|
||||||
UnityEditor.Handles.Label(_playerTransform.position + Vector3.up * 2,
|
|
||||||
$"Shadow Distance: {QualitySettings.shadowDistance:F0}");
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
fileFormatVersion: 2
|
|
||||||
guid: e8f216e6440b9d445ac547aa24daf2b7
|
|
||||||
@@ -12,7 +12,7 @@ MonoBehaviour:
|
|||||||
m_Script: {fileID: 11500000, guid: 6b3d386ba5cd94485973aee1479b272e, type: 3}
|
m_Script: {fileID: 11500000, guid: 6b3d386ba5cd94485973aee1479b272e, type: 3}
|
||||||
m_Name: HiddenThroughMask
|
m_Name: HiddenThroughMask
|
||||||
m_EditorClassIdentifier: Unity.RenderPipelines.Universal.Runtime::UnityEngine.Rendering.Universal.RenderObjects
|
m_EditorClassIdentifier: Unity.RenderPipelines.Universal.Runtime::UnityEngine.Rendering.Universal.RenderObjects
|
||||||
m_Active: 1
|
m_Active: 0
|
||||||
settings:
|
settings:
|
||||||
passTag: HiddenThroughMask
|
passTag: HiddenThroughMask
|
||||||
Event: 300
|
Event: 300
|
||||||
@@ -54,7 +54,7 @@ MonoBehaviour:
|
|||||||
m_Script: {fileID: 11500000, guid: 6b3d386ba5cd94485973aee1479b272e, type: 3}
|
m_Script: {fileID: 11500000, guid: 6b3d386ba5cd94485973aee1479b272e, type: 3}
|
||||||
m_Name: VisibleThroughMask
|
m_Name: VisibleThroughMask
|
||||||
m_EditorClassIdentifier: Unity.RenderPipelines.Universal.Runtime::UnityEngine.Rendering.Universal.RenderObjects
|
m_EditorClassIdentifier: Unity.RenderPipelines.Universal.Runtime::UnityEngine.Rendering.Universal.RenderObjects
|
||||||
m_Active: 1
|
m_Active: 0
|
||||||
settings:
|
settings:
|
||||||
passTag: VisibleThroughMask
|
passTag: VisibleThroughMask
|
||||||
Event: 600
|
Event: 600
|
||||||
@@ -116,7 +116,7 @@ MonoBehaviour:
|
|||||||
m_Script: {fileID: 11500000, guid: 6b3d386ba5cd94485973aee1479b272e, type: 3}
|
m_Script: {fileID: 11500000, guid: 6b3d386ba5cd94485973aee1479b272e, type: 3}
|
||||||
m_Name: Stencil Mask
|
m_Name: Stencil Mask
|
||||||
m_EditorClassIdentifier: Unity.RenderPipelines.Universal.Runtime::UnityEngine.Rendering.Universal.RenderObjects
|
m_EditorClassIdentifier: Unity.RenderPipelines.Universal.Runtime::UnityEngine.Rendering.Universal.RenderObjects
|
||||||
m_Active: 1
|
m_Active: 0
|
||||||
settings:
|
settings:
|
||||||
passTag: Stencil Mask
|
passTag: Stencil Mask
|
||||||
Event: 250
|
Event: 250
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,18 +0,0 @@
|
|||||||
fileFormatVersion: 2
|
|
||||||
guid: adf06fb1c82bec94d8daea4d30c135e9
|
|
||||||
ScriptedImporter:
|
|
||||||
internalIDToNameTable: []
|
|
||||||
externalObjects: {}
|
|
||||||
serializedVersion: 2
|
|
||||||
userData:
|
|
||||||
assetBundleName:
|
|
||||||
assetBundleVariant:
|
|
||||||
script: {fileID: 11500000, guid: 625f186215c104763be7675aa2d941aa, type: 3}
|
|
||||||
useAsTemplate: 0
|
|
||||||
exposeTemplateAsShader: 0
|
|
||||||
template:
|
|
||||||
name:
|
|
||||||
category:
|
|
||||||
description:
|
|
||||||
icon: {instanceID: 0}
|
|
||||||
thumbnail: {instanceID: 0}
|
|
||||||
@@ -67,61 +67,15 @@ float StableStripes(float x, float depth, float width, float smooth)
|
|||||||
return smoothstep(w + s, w - s, abs(uv.x - .5) * 2);
|
return smoothstep(w + s, w - s, abs(uv.x - .5) * 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
void EnkiBillal_float
|
float WorldStripes()
|
||||||
(
|
|
||||||
float4 Color,
|
|
||||||
float2 ScreenUV,
|
|
||||||
float3 PositionWS,
|
|
||||||
float3 PositionOS,
|
|
||||||
float3 NormalWS,
|
|
||||||
float3 NormalOS,
|
|
||||||
float3 ObjectScale,
|
|
||||||
float TriplanarBlend,
|
|
||||||
float StripesDensity,
|
|
||||||
float StripesWidth,
|
|
||||||
float StripesSmooth,
|
|
||||||
int Step,
|
|
||||||
out float3 Output
|
|
||||||
)
|
|
||||||
{
|
{
|
||||||
float3 scaledPositionOS = PositionOS * ObjectScale;
|
/*float StripesScale = StripesDensity;
|
||||||
float depth = length(PositionWS - _WorldSpaceCameraPos.xyz);
|
|
||||||
//Light mainLight = GetMainLight(0);
|
|
||||||
float3 mainLightDir = normalize(float3(1, -1, 1)); //normalize(float3(0, 13, 8) - PositionWS); //float3(0, -1, 0);//normalize(mainLight.direction);
|
|
||||||
float lightGradient = dot(normalize(NormalWS), normalize(mainLightDir));
|
|
||||||
lightGradient = smoothstep(-.5, 1, lightGradient);
|
|
||||||
float ao = GetScreenSpaceAmbientOcclusion(ScreenUV).directAmbientOcclusion;
|
|
||||||
|
|
||||||
|
|
||||||
// Triplanar
|
|
||||||
|
|
||||||
//float3 triplanarMask = TriplanarMask(NormalWS, TriplanarBlend);
|
|
||||||
|
|
||||||
// Shadow
|
|
||||||
|
|
||||||
float shadowMask = lightGradient;//MainLightShadowAtten(PositionWS, NormalWS, .8);
|
|
||||||
float shadowAtten = Remap(0, 1, .6, 1, shadowMask);
|
|
||||||
|
|
||||||
// Grain
|
|
||||||
|
|
||||||
float grain = ValueNoise3DLOD(PositionWS, scaledPositionOS, 600, 3.5);
|
|
||||||
float grainMin = lerp(.4, .8, shadowMask);
|
|
||||||
float grainAtten = Remap(0, 1, grainMin, 1, grain);
|
|
||||||
|
|
||||||
// Dirt
|
|
||||||
|
|
||||||
float dirt = FBMNoise3D(scaledPositionOS, 1, 3, 2, .5);
|
|
||||||
float dirtMin = lerp(.2, .7, shadowMask);
|
|
||||||
float dirtAtten = Remap(0, 1, dirtMin, 1, dirt);
|
|
||||||
|
|
||||||
/*// Stripes
|
|
||||||
|
|
||||||
float StripesScale = StripesDensity;
|
|
||||||
//float StripesWidth = .5;
|
//float StripesWidth = .5;
|
||||||
//float StripesSmooth = .05;
|
//float StripesSmooth = .05;
|
||||||
|
|
||||||
// Octahedral face normals (4 faces + 3 world axes)
|
// Octahedral face normals (4 faces + 3 world axes)
|
||||||
float3 octaFaceNormals[7] = {
|
float3 octaFaceNormals[7] =
|
||||||
|
{
|
||||||
normalize(float3(1, 1, 1)),
|
normalize(float3(1, 1, 1)),
|
||||||
normalize(float3(-1, 1, 1)),
|
normalize(float3(-1, 1, 1)),
|
||||||
normalize(float3(1, -1, 1)),
|
normalize(float3(1, -1, 1)),
|
||||||
@@ -140,11 +94,17 @@ void EnkiBillal_float
|
|||||||
{
|
{
|
||||||
float d = abs(dot(NormalWS, octaFaceNormals[i]));
|
float d = abs(dot(NormalWS, octaFaceNormals[i]));
|
||||||
octaMask[i] = pow(saturate(d), sharpness);
|
octaMask[i] = pow(saturate(d), sharpness);
|
||||||
if (d > hardMax) { hardMax = d; hardIdx = i; }
|
if (d > hardMax)
|
||||||
|
{
|
||||||
|
hardMax = d;
|
||||||
|
hardIdx = i;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
float maskSum = 0;
|
float maskSum = 0;
|
||||||
for (int i = 0; i < 7; i++) maskSum += octaMask[i];
|
for (int i = 0; i < 7; i++)
|
||||||
for (int i = 0; i < 7; i++) octaMask[i] /= max(maskSum, 0.0001);
|
maskSum += octaMask[i];
|
||||||
|
for (int i = 0; i < 7; i++)
|
||||||
|
octaMask[i] /= max(maskSum, 0.0001);
|
||||||
float hardFactor = smoothstep(.9, 1, TriplanarBlend);
|
float hardFactor = smoothstep(.9, 1, TriplanarBlend);
|
||||||
for (int i = 0; i < 7; i++)
|
for (int i = 0; i < 7; i++)
|
||||||
{
|
{
|
||||||
@@ -154,8 +114,8 @@ void EnkiBillal_float
|
|||||||
|
|
||||||
// Patern
|
// Patern
|
||||||
float3 stripesPos = PositionWS;
|
float3 stripesPos = PositionWS;
|
||||||
float3 stripesPos0 = stripesPos;// + PerlinColor3D(PositionWS, .63) * .10;
|
float3 stripesPos0 = stripesPos; // + PerlinColor3D(PositionWS, .63) * .10;
|
||||||
float3 stripesPos1 = stripesPos;// + PerlinColor3D(PositionWS, 5.7) * .02;
|
float3 stripesPos1 = stripesPos; // + PerlinColor3D(PositionWS, 5.7) * .02;
|
||||||
|
|
||||||
float width = StripesWidth; // * .5;
|
float width = StripesWidth; // * .5;
|
||||||
float smooth = max(StripesSmooth, 1e-5);
|
float smooth = max(StripesSmooth, 1e-5);
|
||||||
@@ -190,6 +150,169 @@ void EnkiBillal_float
|
|||||||
float stripes = max(stripes0, stripes1 * .5);
|
float stripes = max(stripes0, stripes1 * .5);
|
||||||
stripes *= smoothstep(.5, 0, lightGradient) * smoothstep(.97, .93, ao);*/
|
stripes *= smoothstep(.5, 0, lightGradient) * smoothstep(.97, .93, ao);*/
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
float ProjectionMask(
|
||||||
|
float3 PositionWS,
|
||||||
|
float3 CamPositionWS,
|
||||||
|
float3 RectPositionWS,
|
||||||
|
float3 RectNormalWS,
|
||||||
|
float3 RectUpWS,
|
||||||
|
float2 RectSize,
|
||||||
|
float Oval)
|
||||||
|
{
|
||||||
|
// Build the rectangle local frame
|
||||||
|
float3 N = normalize(RectNormalWS);
|
||||||
|
float3 U = normalize(RectUpWS);
|
||||||
|
float3 R = normalize(cross(N, U)); // Right axis
|
||||||
|
U = normalize(cross(R, N)); // Recompute Up orthogonally in case RectUpWS is not perfectly perpendicular to RectNormalWS
|
||||||
|
|
||||||
|
// Ray from camera toward the fragment
|
||||||
|
float3 rayDir = normalize(PositionWS - CamPositionWS);
|
||||||
|
|
||||||
|
// Ray-plane intersection
|
||||||
|
// Plane equation: dot(P - RectPositionWS, N) = 0
|
||||||
|
float denom = dot(rayDir, N);
|
||||||
|
|
||||||
|
// Ray is parallel to the plane or facing away — no hit
|
||||||
|
if (abs(denom) < 1e-6)
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
float t = dot(RectPositionWS - CamPositionWS, N) / denom;
|
||||||
|
|
||||||
|
// Intersection must be in front of the camera (t > 0)
|
||||||
|
// and not beyond the fragment (t <= distance from camera to fragment)
|
||||||
|
float tFrag = dot(PositionWS - CamPositionWS, rayDir);
|
||||||
|
if (t < 0.0 || t > tFrag + 1e-4)
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
// Compute the hit point on the plane
|
||||||
|
float3 hitPoint = CamPositionWS + rayDir * t;
|
||||||
|
|
||||||
|
// Project hit point into the rectangle local space
|
||||||
|
float3 localVec = hitPoint - RectPositionWS;
|
||||||
|
float localU = dot(localVec, R); // horizontal axis
|
||||||
|
float localV = dot(localVec, U); // vertical axis
|
||||||
|
|
||||||
|
float halfW = RectSize.x * 0.5;
|
||||||
|
float halfH = RectSize.y * 0.5;
|
||||||
|
|
||||||
|
// Corner radius: 0 = rectangle, min(halfW, halfH) = oval
|
||||||
|
float radius = saturate(Oval) * min(halfW, halfH);
|
||||||
|
|
||||||
|
// Rounded rectangle SDF
|
||||||
|
float2 uv = float2(localU, localV);
|
||||||
|
float2 ext = float2(halfW, halfH);
|
||||||
|
float2 q = abs(uv) - ext + radius;
|
||||||
|
float sdf = length(max(q, 0.0)) - radius;
|
||||||
|
|
||||||
|
return sdf <= 0.0 ? 1.0 : 0.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
float SampleProjectionMask(float3 PositionWS, float4x4 M)
|
||||||
|
{
|
||||||
|
if (M[0].w == 0)
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
return ProjectionMask(
|
||||||
|
PositionWS,
|
||||||
|
M[0].xyz, // CamPositionWS
|
||||||
|
M[1].xyz, // RectPositionWS
|
||||||
|
M[3].xyz, // RectNormalWS
|
||||||
|
M[2].xyz, // RectUpWS
|
||||||
|
float2(M[2].w, M[3].w), // RectSize
|
||||||
|
M[1].w // Shape
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void EnkiBillal_float
|
||||||
|
(
|
||||||
|
float4 Color,
|
||||||
|
float2 ScreenUV,
|
||||||
|
float3 PositionWS,
|
||||||
|
float3 PositionOS,
|
||||||
|
float3 NormalWS,
|
||||||
|
float3 NormalOS,
|
||||||
|
float3 ObjectScale,
|
||||||
|
float TriplanarBlend,
|
||||||
|
float StripesDensity,
|
||||||
|
float StripesWidth,
|
||||||
|
float StripesSmooth,
|
||||||
|
int Step,
|
||||||
|
float MaskID,
|
||||||
|
float4x4 Mask0,
|
||||||
|
float4x4 Mask1,
|
||||||
|
float4x4 Mask2,
|
||||||
|
out float3 Output,
|
||||||
|
out float Alpha
|
||||||
|
)
|
||||||
|
{
|
||||||
|
float3 scaledPositionOS = PositionOS * ObjectScale;
|
||||||
|
float depth = length(PositionWS - _WorldSpaceCameraPos.xyz);
|
||||||
|
//Light mainLight = GetMainLight(0);
|
||||||
|
float3 mainLightDir = normalize(float3(1, -1, 1)); //normalize(float3(0, 13, 8) - PositionWS); //float3(0, -1, 0);//normalize(mainLight.direction);
|
||||||
|
float lightGradient = dot(normalize(NormalWS), normalize(mainLightDir));
|
||||||
|
lightGradient = smoothstep(-.5, 1, lightGradient);
|
||||||
|
float ao = GetScreenSpaceAmbientOcclusion(ScreenUV).directAmbientOcclusion;
|
||||||
|
|
||||||
|
|
||||||
|
// Triplanar
|
||||||
|
|
||||||
|
//float3 triplanarMask = TriplanarMask(NormalWS, TriplanarBlend);
|
||||||
|
|
||||||
|
// Shadow
|
||||||
|
|
||||||
|
float shadowMask = lightGradient;//MainLightShadowAtten(PositionWS, NormalWS, .8);
|
||||||
|
float shadowAtten = Remap(0, 1, .6, 1, shadowMask);
|
||||||
|
|
||||||
|
// Grain
|
||||||
|
|
||||||
|
float grain = ValueNoise3DLOD(PositionWS, scaledPositionOS, 600, 3.5);
|
||||||
|
float grainMin = lerp(.4, .8, shadowMask);
|
||||||
|
float grainAtten = Remap(0, 1, grainMin, 1, grain);
|
||||||
|
|
||||||
|
// Dirt
|
||||||
|
|
||||||
|
float dirt = FBMNoise3D(scaledPositionOS, 1, 3, 2, .5);
|
||||||
|
float dirtMin = lerp(.2, .7, shadowMask);
|
||||||
|
float dirtAtten = Remap(0, 1, dirtMin, 1, dirt);
|
||||||
|
|
||||||
|
// Stripes
|
||||||
|
|
||||||
|
// float stripes = WorldStripes();
|
||||||
|
|
||||||
|
// Mask
|
||||||
|
|
||||||
|
if (Mask2[0].w == 1)
|
||||||
|
{
|
||||||
|
float mask = SampleProjectionMask(PositionWS, Mask2);
|
||||||
|
|
||||||
|
Alpha = (MaskID <= 1) ? 1 - mask : mask;
|
||||||
|
}
|
||||||
|
else if (MaskID == 0)
|
||||||
|
{
|
||||||
|
Alpha = 1;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
float Active = Mask0[0].w + Mask1[0].w;// + Mask2[0].w;
|
||||||
|
|
||||||
|
if (Active > 0)
|
||||||
|
{
|
||||||
|
float mask = saturate(
|
||||||
|
SampleProjectionMask(PositionWS, Mask0) +
|
||||||
|
SampleProjectionMask(PositionWS, Mask1)// +
|
||||||
|
//SampleProjectionMask(PositionWS, Mask2)
|
||||||
|
);
|
||||||
|
|
||||||
|
Alpha = (MaskID == 1) ? 1 - mask : mask;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Alpha = (MaskID == 1) ? 0 : 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Test
|
// Test
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user