add : unabomber scene

This commit is contained in:
2026-09-28 14:58:25 +02:00
parent e95980d88c
commit 769a75f9bb
137 changed files with 12749 additions and 9 deletions
@@ -0,0 +1,90 @@
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Rendering;
/// <summary>Reachable landmarks at the gaps between four repeated server clusters.</summary>
[ExecuteAlways, DisallowMultipleComponent, RequireComponent(typeof(PortalMeadow)), DefaultExecutionOrder(205)]
public sealed class PortalExitMarker : MonoBehaviour
{
[SerializeField] private Material exitMaterial;
[SerializeField] private float rotationY = 45;
[SerializeField] private Vector3 dimensions = new Vector3(1.5f, 3.2f, 0.2f);
private PortalMeadow meadow;
private readonly List<Transform> markers = new List<Transform>();
private PortalServerGrid grid;
private float period;
private Material runtimeMaterial;
private bool rebuild;
private Vector3 initialCentre;
private Material lastMaterial;
private void OnEnable()
{
meadow = GetComponent<PortalMeadow>(); grid = GetComponent<PortalServerGrid>(); rebuild = true;
RenderPipelineManager.beginCameraRendering += Sync;
}
private void OnValidate() { rebuild = true; }
private void Update()
{
if (!meadow || !exitMaterial || !grid || !grid.enabled || grid.RepetitionPeriod <= 0) { Clear(); return; }
if (!rebuild && markers.Count > 0 && initialCentre == meadow.Center && lastMaterial == exitMaterial && period == grid.RepetitionPeriod) return;
Clear(); rebuild = false; initialCentre = meadow.Center; lastMaterial = exitMaterial;
period = grid.RepetitionPeriod;
runtimeMaterial = new Material(exitMaterial) { hideFlags = HideFlags.DontSave, name = "Distant Exit (runtime)" };
for (int z = -1; z <= 1; z++)
for (int x = -1; x <= 1; x++)
{
var marker = GameObject.CreatePrimitive(PrimitiveType.Cube);
marker.name = "Luminous Gap Exit (pooled)";
marker.hideFlags = HideFlags.DontSave;
marker.transform.SetParent(transform, false);
marker.transform.localPosition = initialCentre + new Vector3((x + 0.5f) * period, 0, (z + 0.5f) * period)
+ Vector3.up * Mathf.Max(0.1f, dimensions.y) * 0.5f;
marker.transform.localRotation = Quaternion.Euler(0, rotationY, 0);
marker.transform.localScale = new Vector3(Mathf.Max(0.1f, dimensions.x), Mathf.Max(0.1f, dimensions.y), Mathf.Max(0.01f, dimensions.z));
marker.GetComponent<BoxCollider>().isTrigger = true;
var renderer = marker.GetComponent<MeshRenderer>();
renderer.sharedMaterial = runtimeMaterial;
renderer.renderingLayerMask = meadow.RenderingLayers;
renderer.shadowCastingMode = ShadowCastingMode.Off;
renderer.allowOcclusionWhenDynamic = false;
Bounds bounds = renderer.localBounds;
float smallest = Mathf.Min(marker.transform.localScale.x, marker.transform.localScale.y, marker.transform.localScale.z);
bounds.Expand(meadow.BoundsPadding * 2 / smallest);
renderer.localBounds = bounds;
markers.Add(marker.transform);
}
}
private void LateUpdate()
{
if (!meadow || !meadow.Following || period <= 0) return;
float poolPeriod = period * 3;
foreach (Transform marker in markers)
{
Vector3 p = marker.localPosition;
p.x += Mathf.Floor((meadow.SurfaceCenter.x - p.x + poolPeriod * 0.5f) / poolPeriod) * poolPeriod;
p.z += Mathf.Floor((meadow.SurfaceCenter.z - p.z + poolPeriod * 0.5f) / poolPeriod) * poolPeriod;
marker.localPosition = p;
}
}
private void Sync(ScriptableRenderContext context, Camera camera)
{
if (!runtimeMaterial || !meadow || !meadow.MeadowMaterial) return;
runtimeMaterial.CopyPropertiesFromMaterial(exitMaterial);
meadow.ApplyBend(runtimeMaterial);
meadow.ConfigureRegion(runtimeMaterial);
}
private void OnDisable() { RenderPipelineManager.beginCameraRendering -= Sync; Clear(); }
private void Clear()
{
foreach (Transform marker in markers)
if (marker) { marker.gameObject.SetActive(false); Release(marker.gameObject); }
markers.Clear();
if (runtimeMaterial) Release(runtimeMaterial);
runtimeMaterial = null;
}
private static void Release(Object value)
{
if (Application.isPlaying) Destroy(value); else DestroyImmediate(value);
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: bd9a18fc47d64b90b9c4131e4dfdcb62
+106
View File
@@ -0,0 +1,106 @@
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Rendering;
/// <summary>Concentric curtains using Omnissiah's procedural fog and the meadow's bend.</summary>
[ExecuteAlways, DisallowMultipleComponent, RequireComponent(typeof(PortalMeadow)), DefaultExecutionOrder(215)]
public sealed class PortalFogRings : MonoBehaviour
{
[SerializeField] private Material fogMaterial;
[SerializeField, Range(1, 8)] private int layers = 4;
[SerializeField, Range(0.1f, 0.9f)] private float innerRadius = 0.35f;
[SerializeField, Range(0.2f, 1.2f)] private float outerRadius = 0.98f;
[SerializeField, Min(1)] private float height = 8;
[SerializeField] private float baseOffset = -1;
private PortalMeadow meadow;
private GameObject generated;
private Mesh mesh;
private readonly List<Transform> rings = new List<Transform>();
private readonly List<Material> materials = new List<Material>();
private Material lastMaterial;
private bool rebuild;
private void OnEnable()
{
meadow = GetComponent<PortalMeadow>(); rebuild = true;
RenderPipelineManager.beginCameraRendering += Sync;
}
private void OnValidate() { rebuild = true; }
private void LateUpdate()
{
if (!meadow || !fogMaterial) { Clear(); return; }
if (rebuild || !generated || lastMaterial != fogMaterial) Build();
generated.transform.localPosition = meadow.SurfaceCenter + Vector3.up * baseOffset;
for (int i = 0; i < rings.Count; i++)
{
float t = rings.Count == 1 ? 1 : (float)i / (rings.Count - 1);
float radius = meadow.Radius * Mathf.Lerp(innerRadius, Mathf.Max(innerRadius, outerRadius), t);
rings[i].localScale = new Vector3(radius, Mathf.Max(1, height) * Mathf.Lerp(0.65f, 1, t), radius);
var renderer = rings[i].GetComponent<MeshRenderer>();
Bounds bounds = mesh.bounds;
bounds.Expand(meadow.BoundsPadding * 2 / Mathf.Max(0.01f, Mathf.Min(radius, rings[i].localScale.y)));
renderer.localBounds = bounds;
}
}
private void Build()
{
Clear(); rebuild = false; lastMaterial = fogMaterial;
generated = new GameObject("Atmospheric Fog Rings (generated)") { hideFlags = HideFlags.DontSave };
generated.transform.SetParent(transform, false);
const int segments = 128, rows = 8;
var vertices = new Vector3[(segments + 1) * (rows + 1)];
var triangles = new int[segments * rows * 6];
int n = 0;
for (int y = 0; y <= rows; y++)
for (int x = 0; x <= segments; x++)
{
float angle = x * Mathf.PI * 2 / segments;
vertices[y * (segments + 1) + x] = new Vector3(Mathf.Sin(angle), (float)y / rows, Mathf.Cos(angle));
if (y == rows || x == segments) continue;
int a = y * (segments + 1) + x, b = a + segments + 1;
triangles[n++] = a; triangles[n++] = b; triangles[n++] = a + 1;
triangles[n++] = a + 1; triangles[n++] = b; triangles[n++] = b + 1;
}
mesh = new Mesh { name = "Fog cylinder curtain", hideFlags = HideFlags.DontSave };
mesh.vertices = vertices; mesh.triangles = triangles; mesh.RecalculateBounds();
for (int i = 0; i < Mathf.Clamp(layers, 1, 8); i++)
{
var ring = new GameObject("Fog layer " + (i + 1)) { hideFlags = HideFlags.DontSave };
ring.transform.SetParent(generated.transform, false);
ring.AddComponent<MeshFilter>().sharedMesh = mesh;
var mat = new Material(fogMaterial) { name = "PortalB Fog (runtime)", hideFlags = HideFlags.DontSave };
materials.Add(mat);
var renderer = ring.AddComponent<MeshRenderer>();
renderer.sharedMaterial = mat;
renderer.shadowCastingMode = ShadowCastingMode.Off;
renderer.receiveShadows = false;
renderer.allowOcclusionWhenDynamic = false;
rings.Add(ring.transform);
}
}
private void Sync(ScriptableRenderContext context, Camera camera)
{
if (!meadow || !meadow.MeadowMaterial || !fogMaterial) return;
for (int i = 0; i < materials.Count; i++)
{
var mat = materials[i];
mat.CopyPropertiesFromMaterial(fogMaterial);
// From outside to inside, after the sky dome. The player is inside all curtains.
mat.renderQueue = 3000 + materials.Count - 1 - i;
meadow.ApplyBend(mat);
meadow.ApplyPortalMask(mat);
}
}
private void OnDisable() { RenderPipelineManager.beginCameraRendering -= Sync; Clear(); }
private void Clear()
{
if (generated) { generated.SetActive(false); Release(generated); }
foreach (Material mat in materials) if (mat) Release(mat);
if (mesh) Release(mesh);
generated = null; mesh = null; materials.Clear(); rings.Clear();
}
private static void Release(Object obj)
{
if (Application.isPlaying) Destroy(obj); else DestroyImmediate(obj);
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: f6d937b536e247789348f0994c873cb1
+315
View File
@@ -0,0 +1,315 @@
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Rendering;
/// <summary>Deterministic meadow, generated in edit mode and at runtime. Wind and bending run on the GPU.</summary>
[ExecuteAlways, DisallowMultipleComponent, DefaultExecutionOrder(100)]
public sealed class PortalMeadow : MonoBehaviour
{
[SerializeField] private Material material;
[SerializeField] private Vector2 size = new Vector2(32, 48);
[SerializeField] private Vector3 center = new Vector3(0, -0.03f, 16);
[SerializeField] private bool centerOnPlayerAtStart = true;
[Tooltip("Optional player reference. Otherwise uses the Player tag, then the main camera.")]
[SerializeField] private Transform initialPlayer;
[SerializeField] private bool circular = true;
[Tooltip("Keep the visible disk around the moving player and recycle existing grass tiles. No player teleportation.")]
[SerializeField] private bool endless = true;
[Tooltip("Disable while PortalB is only seen through a portal: the disk stays fixed until SetFollowPlayer(true).")]
[SerializeField] private bool followPlayer = true;
[SerializeField, Min(0.1f)] private float edgeFade = 4;
[SerializeField] private Color edgeColor = new Color(0.36f, 0.43f, 0.46f, 1);
[Tooltip("Thickness below the flat ground surface. Shader bending is visual only.")]
[SerializeField, Min(0.01f)] private float colliderThickness = 0.2f;
[SerializeField, Range(1, 60)] private int bladesPerSquareMetre = 30;
[SerializeField] private Vector2 bladeHeight = new Vector2(0.25f, 0.65f);
[SerializeField, Range(0.01f, 0.15f)] private float bladeWidth = 0.055f;
[SerializeField] private int seed = 1978;
[Tooltip("Extra bounds in every direction to accommodate camera-relative shader bending.")]
[SerializeField, Min(1)] private float boundsPadding = 128;
[Tooltip("Rendering layers of every generated PortalB renderer, so lights can be restricted to this world.")]
[SerializeField] private RenderingLayerMask renderingLayers = 1u;
[Tooltip("While not following the player, the bend is computed from this frame (e.g. the portal, looking along its forward axis) " +
"instead of the camera, so it stays aligned and still behind the portal.")]
[SerializeField] private Transform bendAnchor;
[Tooltip("Blend from the anchor frame to the camera once the player follows.")]
[SerializeField, Min(0)] private float bendFadeDuration = 1.5f;
private static readonly string[] BendProperties = { "_Curvature", "_StartDistance", "_HorizontalInfluence", "_BendAngle" };
private static readonly int FrameId = Shader.PropertyToID("_CurvedWorldFrame");
private static readonly int FrameInvId = Shader.PropertyToID("_CurvedWorldFrameInv");
private static readonly int FrameFixedId = Shader.PropertyToID("_CurvedWorldFrameFixed");
private float frameBlend;
private readonly List<Mesh> meshes = new List<Mesh>();
private GameObject generated;
private bool rebuild;
private Vector3 initialCenterOffset;
private bool centerCaptured;
private bool wasPlaying;
private Transform capturedPlayer;
private Vector3 surfaceCenter;
private Material renderedMaterial;
private BoxCollider groundCollider;
private readonly List<Transform> tiles = new List<Transform>();
private readonly List<MeshRenderer> tileRenderers = new List<MeshRenderer>();
private Vector2 patchSize;
private Vector2 tileSize;
public Vector2 Size => size;
public Vector3 Center => center + (centerOnPlayerAtStart ? initialCenterOffset : Vector3.zero);
public Material MeadowMaterial => material;
public float BoundsPadding => boundsPadding;
public uint RenderingLayers => renderingLayers;
public float Radius => Mathf.Min(size.x, size.y) * 0.5f;
public bool Endless => endless && Application.isPlaying;
/// <summary>Endless and currently recentering on the player (see SetFollowPlayer).</summary>
public bool Following => Endless && followPlayer;
public Vector3 SurfaceCenter => Following && capturedPlayer ? surfaceCenter : Center;
public void ConfigureRegion(Material target)
{
Vector3 p = SurfaceCenter;
target.SetVector("_MeadowRegion", new Vector4(p.x, p.y, p.z, circular ? Radius : 0));
target.SetMatrix("_MeadowWorldToLocal", transform.worldToLocalMatrix);
target.SetFloat("_MeadowEdgeFade", Mathf.Min(edgeFade, Radius * 0.5f));
target.SetColor("_MeadowEdgeColor", edgeColor);
ApplyPortalMask(target);
}
/// <summary>The meadow material's portal mask is the reference for every PortalB material.</summary>
public void ApplyPortalMask(Material target)
{
if (material && material.HasProperty("_MaskID") && target.HasProperty("_MaskID"))
target.SetFloat("_MaskID", material.GetFloat("_MaskID"));
}
/// <summary>The meadow material's bend is the reference for every PortalB material.</summary>
public void ApplyBend(Material target)
{
if (!material) return;
foreach (string property in BendProperties)
if (material.HasProperty(property) && target.HasProperty(property))
target.SetFloat(property, material.GetFloat(property));
}
public void SetFollowPlayer(bool follow) { followPlayer = follow; }
private void BeforeCameraRendering(ScriptableRenderContext context, Camera camera)
{
if (!bendAnchor || frameBlend >= 1) { Shader.SetGlobalFloat(FrameFixedId, 0); return; }
Transform view = camera.transform;
Vector3 position = Vector3.Lerp(bendAnchor.position, view.position, frameBlend);
Quaternion rotation = Quaternion.Slerp(bendAnchor.rotation, view.rotation, frameBlend);
// Same convention as a camera view matrix: looks along -Z.
Matrix4x4 frameToWorld = Matrix4x4.TRS(position, rotation, new Vector3(1, 1, -1));
Shader.SetGlobalMatrix(FrameId, frameToWorld.inverse);
Shader.SetGlobalMatrix(FrameInvId, frameToWorld);
Shader.SetGlobalFloat(FrameFixedId, 1);
}
private void LateUpdate()
{
frameBlend = !Following ? 0 : bendFadeDuration > 0 ? Mathf.MoveTowards(frameBlend, 1, Time.deltaTime / bendFadeDuration) : 1;
surfaceCenter = Center;
if (Following && capturedPlayer)
{
Vector3 p = transform.InverseTransformPoint(capturedPlayer.position);
surfaceCenter.x = p.x; surfaceCenter.z = p.z;
}
if (groundCollider) groundCollider.center = SurfaceCenter + Vector3.down * (colliderThickness * 0.5f);
if (renderedMaterial && material)
{
renderedMaterial.CopyPropertiesFromMaterial(material);
ConfigureRegion(renderedMaterial);
ApplyBend(renderedMaterial);
}
if (!generated) return;
for (int i = 0; i < tiles.Count; i++)
{
Transform tile = tiles[i];
Vector3 p = tile.localPosition;
if (Following)
{
p.x += Mathf.Floor((SurfaceCenter.x - p.x - tileSize.x * 0.5f + patchSize.x * 0.5f) / patchSize.x) * patchSize.x;
p.z += Mathf.Floor((SurfaceCenter.z - p.z - tileSize.y * 0.5f + patchSize.y * 0.5f) / patchSize.y) * patchSize.y;
tile.localPosition = p;
}
float dx = Mathf.Max(0, Mathf.Abs(p.x + tileSize.x * 0.5f - SurfaceCenter.x) - tileSize.x * 0.5f);
float dz = Mathf.Max(0, Mathf.Abs(p.z + tileSize.y * 0.5f - SurfaceCenter.z) - tileSize.y * 0.5f);
tileRenderers[i].enabled = !circular || dx * dx + dz * dz <= (Radius + 0.3f) * (Radius + 0.3f);
}
}
private void OnEnable() { rebuild = true; centerCaptured = false; initialCenterOffset = Vector3.zero; wasPlaying = Application.isPlaying; frameBlend = 0; RenderPipelineManager.beginCameraRendering += BeforeCameraRendering; }
private void OnValidate()
{
size.x = Mathf.Clamp(size.x, 1, 128);
size.y = Mathf.Clamp(size.y, 1, 128);
colliderThickness = Mathf.Max(0.01f, colliderThickness);
bladeHeight.x = Mathf.Clamp(bladeHeight.x, 0.05f, 2);
bladeHeight.y = Mathf.Clamp(bladeHeight.y, bladeHeight.x, 3);
// Defer Unity object creation until Update, outside the serialization callback.
rebuild = true;
}
private void Update()
{
// Capture again when entering Play even when domain/scene reload is disabled.
if (wasPlaying != Application.isPlaying)
{
wasPlaying = Application.isPlaying;
centerCaptured = false;
initialCenterOffset = Vector3.zero;
rebuild = true;
}
if ((centerOnPlayerAtStart || endless) && !centerCaptured) CaptureInitialCenter();
if (!rebuild) return;
rebuild = false;
Build();
}
private void OnDisable()
{
RenderPipelineManager.beginCameraRendering -= BeforeCameraRendering;
Shader.SetGlobalFloat(FrameFixedId, 0);
Clear();
}
[ContextMenu("Regenerate meadow")]
private void Regenerate() { rebuild = true; }
[ContextMenu("Recenter on player once")]
private void Recenter() { centerCaptured = false; rebuild = true; }
private void CaptureInitialCenter()
{
Transform player = initialPlayer;
if (!player)
{
foreach (GameObject candidate in GameObject.FindGameObjectsWithTag("Player"))
if (candidate.scene == gameObject.scene) { player = candidate.transform; break; }
}
if (!player && Camera.main) player = Camera.main.transform;
if (!player) return;
capturedPlayer = player;
Vector3 local = transform.InverseTransformPoint(player.position);
initialCenterOffset = new Vector3(local.x - center.x, 0, local.z - center.z);
surfaceCenter = Center;
centerCaptured = true;
rebuild = true;
}
private void Clear()
{
if (generated) { generated.SetActive(false); Release(generated); }
generated = null;
foreach (Mesh mesh in meshes) if (mesh) Release(mesh);
meshes.Clear();
tiles.Clear();
tileRenderers.Clear();
if (renderedMaterial) Release(renderedMaterial);
renderedMaterial = null;
}
private static void Release(Object value)
{
if (Application.isPlaying) Destroy(value);
else DestroyImmediate(value);
}
private void Build()
{
Clear();
generated = new GameObject("Grass Ground (generated)") { hideFlags = HideFlags.DontSave, layer = gameObject.layer };
generated.transform.SetParent(transform, false);
// The top face matches the undeformed ground; X/Z match the full meadow footprint.
groundCollider = generated.AddComponent<BoxCollider>();
groundCollider.size = new Vector3(size.x, colliderThickness, size.y);
groundCollider.center = Center + Vector3.down * (colliderThickness * 0.5f);
if (!material) return;
renderedMaterial = new Material(material) { name = "Meadow (circular)", hideFlags = HideFlags.DontSave };
ConfigureRegion(renderedMaterial);
ApplyBend(renderedMaterial);
var random = new System.Random(seed);
int nx = Mathf.CeilToInt(size.x / 8f), nz = Mathf.CeilToInt(size.y / 8f);
float sx = size.x / nx, sz = size.y / nz;
// One extra tile on each side keeps the disk covered while tiles wrap around it.
nx += 2; nz += 2;
patchSize = new Vector2(nx * sx, nz * sz);
tileSize = new Vector2(sx, sz);
for (int z = 0; z < nz; z++)
for (int x = 0; x < nx; x++)
BuildTile(new Vector3(Center.x - patchSize.x / 2 + x * sx, Center.y,
Center.z - patchSize.y / 2 + z * sz), sx, sz, random, x, z);
}
private void BuildTile(Vector3 origin, float sx, float sz, System.Random random, int tx, int tz)
{
var vertices = new List<Vector3>();
var normals = new List<Vector3>();
var uvs = new List<Vector2>();
var colors = new List<Color>();
var triangles = new List<int>();
void Vertex(Vector3 position, Vector3 normal, Vector2 uv, Color color)
{ vertices.Add(position); normals.Add(normal); uvs.Add(uv); colors.Add(color); }
float Rand(float a, float b) { return Mathf.Lerp(a, b, (float)random.NextDouble()); }
// A subdivided ground is essential: a single quad cannot follow quadratic bending.
int gx = Mathf.CeilToInt(sx * 2), gz = Mathf.CeilToInt(sz * 2);
for (int z = 0; z <= gz; z++)
for (int x = 0; x <= gx; x++)
Vertex(new Vector3(sx * x / gx, 0, sz * z / gz), Vector3.up,
Vector2.zero, new Color(0.5f, 0, 0, 0));
for (int z = 0; z < gz; z++)
for (int x = 0; x < gx; x++)
{
int a = z * (gx + 1) + x, b = a + gx + 1;
triangles.AddRange(new[] { a, b, a + 1, a + 1, b, b + 1 });
}
int count = Mathf.RoundToInt(sx * sz * bladesPerSquareMetre);
for (int blade = 0; blade < count; blade++)
{
Vector3 root = new Vector3(Rand(0, sx), 0.005f, Rand(0, sz));
float angle = Rand(0, Mathf.PI * 2), height = Rand(bladeHeight.x, bladeHeight.y);
Vector3 side = new Vector3(Mathf.Cos(angle), 0, Mathf.Sin(angle));
Vector3 normal = Vector3.Cross(side, Vector3.up);
Vector3 lean = normal * Rand(-0.18f, 0.18f);
Color color = new Color(Rand(0, 1), Rand(0, 1), 0, 1);
float width = bladeWidth * Rand(0.65f, 1.35f);
int first = vertices.Count;
for (int level = 0; level < 3; level++)
{
float t = level / 3f;
Vector3 p = root + Vector3.up * height * t + lean * t * t;
Vector3 half = side * width * (1 - t) * 0.5f;
Vertex(p - half, normal, new Vector2(0, t), color);
Vertex(p + half, normal, new Vector2(1, t), color);
}
Vertex(root + Vector3.up * height + lean, normal, new Vector2(0.5f, 1), color);
triangles.AddRange(new[] { first, first + 2, first + 1, first + 1, first + 2, first + 3,
first + 2, first + 4, first + 3, first + 3, first + 4, first + 5, first + 4, first + 6, first + 5 });
}
var mesh = new Mesh { name = $"Meadow {tx},{tz}", hideFlags = HideFlags.DontSave };
if (vertices.Count > 65535) mesh.indexFormat = IndexFormat.UInt32;
mesh.SetVertices(vertices); mesh.SetNormals(normals); mesh.SetUVs(0, uvs);
mesh.SetColors(colors); mesh.SetTriangles(triangles, 0);
mesh.RecalculateBounds();
Bounds bounds = mesh.bounds;
bounds.Expand(boundsPadding * 2);
mesh.bounds = bounds;
meshes.Add(mesh);
var tile = new GameObject($"Meadow {tx},{tz}") { hideFlags = HideFlags.DontSave, layer = gameObject.layer };
tile.transform.SetParent(generated.transform, false);
tile.transform.localPosition = origin;
tile.AddComponent<MeshFilter>().sharedMesh = mesh;
var renderer = tile.AddComponent<MeshRenderer>();
renderer.sharedMaterial = renderedMaterial;
renderer.renderingLayerMask = renderingLayers;
tiles.Add(tile.transform);
tileRenderers.Add(renderer);
// Camera-relative bending must not be evaluated from a shadow light's view matrix.
renderer.shadowCastingMode = ShadowCastingMode.Off;
renderer.receiveShadows = false;
renderer.allowOcclusionWhenDynamic = false;
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: db39d731f9ed4af998db7d9054ad7421
+240
View File
@@ -0,0 +1,240 @@
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Rendering;
/// <summary>Fixed concentric server rings, facing the initial meadow centre. Only the shader bends them.</summary>
[ExecuteAlways, DisallowMultipleComponent, RequireComponent(typeof(PortalMeadow)), DefaultExecutionOrder(200)]
public sealed class PortalServerGrid : MonoBehaviour
{
[SerializeField] private GameObject serverPrefab;
[SerializeField] private Shader curvedServerShader;
[Tooltip("X: minimum chord distance between neighbours. Y: distance between rings.")]
[SerializeField] private Vector2 spacing = new Vector2(5, 6);
[Tooltip("Empty border inside the terrain on X and Z.")]
[SerializeField] private Vector2 edgeMargin = new Vector2(1, 1);
[SerializeField, Min(1)] private float firstRingRadius = 8;
[SerializeField, Range(0, 360)] private float angleOffset;
[Tooltip("Pivot adjustment along each server's facing direction (positive = towards the centre).")]
[SerializeField] private float offsetZ;
[Tooltip("Additional height above the ground, after automatically aligning the model base.")]
[SerializeField] private float heightOffset;
[SerializeField] private Vector3 rotation = new Vector3(0, 180, 0);
[Tooltip("Multiplier applied to the prefab's original scale.")]
[SerializeField, Min(0.001f)] private float scaleMultiplier = 1;
private PortalMeadow meadow;
private GameObject generated;
private Vector2 lastSize;
private Vector3 lastCenter;
private bool rebuild;
private readonly Dictionary<Material, Material> curvedMaterials = new Dictionary<Material, Material>();
private readonly List<Transform> servers = new List<Transform>();
private float wrapExtent;
// Distance between the centres of two repeated server clusters.
public float RepetitionPeriod => wrapExtent * 2;
private bool lastEndless;
private void OnEnable()
{
meadow = GetComponent<PortalMeadow>();
rebuild = true;
RenderPipelineManager.beginCameraRendering += BeforeCameraRendering;
}
private void BeforeCameraRendering(ScriptableRenderContext context, Camera camera)
{
// Match the terrain's current material, including changes made after Update.
if (meadow) SyncBend();
}
private void OnValidate()
{
spacing.x = Mathf.Max(0.1f, spacing.x);
spacing.y = Mathf.Max(0.1f, spacing.y);
edgeMargin.x = Mathf.Max(0, edgeMargin.x);
edgeMargin.y = Mathf.Max(0, edgeMargin.y);
scaleMultiplier = Mathf.Max(0.001f, scaleMultiplier);
firstRingRadius = Mathf.Max(1, firstRingRadius);
rebuild = true;
}
private void Update()
{
if (!meadow) meadow = GetComponent<PortalMeadow>();
if (!meadow) return;
SyncBend();
if (!rebuild && lastSize == meadow.Size && lastCenter == meadow.Center && lastEndless == meadow.Endless) return;
rebuild = false;
lastSize = meadow.Size;
lastCenter = meadow.Center;
lastEndless = meadow.Endless;
Build();
}
private void OnDisable()
{
RenderPipelineManager.beginCameraRendering -= BeforeCameraRendering;
Clear();
}
private void LateUpdate()
{
if (!meadow || !meadow.Following || wrapExtent <= 0) return;
Vector3 centre = meadow.SurfaceCenter;
float period = wrapExtent * 2;
foreach (Transform server in servers)
{
Vector3 p = server.localPosition;
// Recycle only outside the visible disk. Nearby transforms/rotations stay fixed.
p.x += Mathf.Floor((centre.x - p.x + wrapExtent) / period) * period;
p.z += Mathf.Floor((centre.z - p.z + wrapExtent) / period) * period;
server.localPosition = p;
}
}
[ContextMenu("Regenerate server rings")]
private void Regenerate() { rebuild = true; }
private static void Release(Object value)
{
if (Application.isPlaying) Destroy(value);
else DestroyImmediate(value);
}
private void Clear()
{
if (generated) { generated.SetActive(false); Release(generated); }
generated = null;
foreach (Material material in curvedMaterials.Values) if (material) Release(material);
curvedMaterials.Clear();
servers.Clear();
}
private GameObject CreateServer()
{
var instance = Instantiate(serverPrefab, generated.transform, false);
instance.transform.localPosition = Vector3.zero;
instance.transform.localRotation = Quaternion.Euler(rotation) * serverPrefab.transform.localRotation;
instance.transform.localScale = serverPrefab.transform.localScale * scaleMultiplier;
foreach (Transform child in instance.GetComponentsInChildren<Transform>(true))
child.gameObject.hideFlags = HideFlags.DontSave;
return instance;
}
private void Build()
{
Clear();
if (!serverPrefab) return;
// Inactive during setup: avoid briefly enabling all instances at the origin.
generated = new GameObject("Server Rings (generated)") { hideFlags = HideFlags.DontSave };
generated.SetActive(false);
generated.transform.SetParent(transform, false);
var prototype = CreateServer();
if (!TryGetLocalBounds(prototype, out Bounds bounds))
{
Debug.LogWarning("Server grid: the prefab needs a Renderer to calculate its footprint.", this);
Clear();
return;
}
float footprint = new Vector2(bounds.size.x, bounds.size.z).magnitude;
wrapExtent = meadow.Radius + footprint + 1;
float pitch = Mathf.Max(spacing.x, footprint, 0.1f);
float ringStep = Mathf.Max(spacing.y, footprint, 0.1f);
float maxRadius = Mathf.Min(lastSize.x * 0.5f - edgeMargin.x,
lastSize.y * 0.5f - edgeMargin.y) - footprint * 0.5f - Mathf.Abs(offsetZ);
float startRadius = Mathf.Max(firstRingRadius, footprint * 0.5f + Mathf.Abs(offsetZ));
var poses = new List<Pose>();
int ring = 0;
for (float radius = startRadius; radius <= maxRadius; radius += ringStep, ring++)
{
// Use chord length, not arc length, so neighbours cannot overlap on small rings.
float placementRadius = radius - offsetZ;
int around = pitch <= 2 * placementRadius
? Mathf.Max(1, Mathf.FloorToInt(Mathf.PI / Mathf.Asin(pitch / (2 * placementRadius)))) : 1;
if (poses.Count + around > 4096)
{
Debug.LogWarning("Server rings: more than 4096 instances requested. Increase Spacing.", this);
Clear();
return;
}
for (int i = 0; i < around; i++)
{
float angle = angleOffset * Mathf.Deg2Rad + (i + (ring % 2) * 0.5f) * Mathf.PI * 2 / around;
Vector3 radial = new Vector3(Mathf.Sin(angle), 0, Mathf.Cos(angle));
Quaternion facing = Quaternion.LookRotation(-radial, Vector3.up);
Vector3 position = lastCenter + radial * radius + facing * Vector3.forward * offsetZ;
// Bounds already include the prefab rotation, scale and user rotation correction.
position -= facing * new Vector3(bounds.center.x, bounds.min.y, bounds.center.z);
position.y += heightOffset;
poses.Add(new Pose(position, facing));
}
}
int count = 0;
foreach (Pose pose in poses)
{
GameObject server = count == 0 ? prototype : CreateServer();
server.transform.localPosition = pose.position;
server.transform.localRotation = pose.rotation * Quaternion.Euler(rotation) * serverPrefab.transform.localRotation;
server.name = $"Server {++count:000}";
ApplyCurvedMaterials(server);
servers.Add(server.transform);
}
if (count == 0) { Clear(); return; }
SyncBend();
generated.SetActive(true);
}
private void ApplyCurvedMaterials(GameObject server)
{
if (!curvedServerShader) return;
foreach (Renderer renderer in server.GetComponentsInChildren<Renderer>(true))
{
Material[] materials = renderer.sharedMaterials;
for (int i = 0; i < materials.Length; i++)
{
Material source = materials[i];
if (!source) continue;
if (!curvedMaterials.TryGetValue(source, out Material curved))
{
curved = new Material(source) { shader = curvedServerShader,
name = source.name + " (Curved)", hideFlags = HideFlags.DontSave };
curved.shaderKeywords = source.shaderKeywords;
curvedMaterials.Add(source, curved);
}
materials[i] = curved;
}
renderer.sharedMaterials = materials;
renderer.renderingLayerMask = meadow.RenderingLayers;
renderer.shadowCastingMode = ShadowCastingMode.Off;
renderer.allowOcclusionWhenDynamic = false;
Bounds expanded = renderer.localBounds;
Vector3 scale = renderer.transform.lossyScale;
float minScale = Mathf.Max(0.001f, Mathf.Min(Mathf.Abs(scale.x), Mathf.Abs(scale.y), Mathf.Abs(scale.z)));
expanded.Expand(meadow.BoundsPadding * 2 / minScale);
renderer.localBounds = expanded;
}
}
private void SyncBend()
{
if (!meadow.MeadowMaterial) return;
foreach (Material material in curvedMaterials.Values)
{
meadow.ApplyBend(material);
meadow.ConfigureRegion(material);
}
}
private bool TryGetLocalBounds(GameObject instance, out Bounds bounds)
{
bounds = default;
bool found = false;
foreach (Renderer renderer in instance.GetComponentsInChildren<Renderer>(true))
{
Bounds local = renderer.localBounds;
Matrix4x4 matrix = generated.transform.worldToLocalMatrix * renderer.transform.localToWorldMatrix;
for (int i = 0; i < 8; i++)
{
Vector3 corner = local.center + Vector3.Scale(local.extents,
new Vector3((i & 1) == 0 ? -1 : 1, (i & 2) == 0 ? -1 : 1, (i & 4) == 0 ? -1 : 1));
Vector3 point = matrix.MultiplyPoint3x4(corner);
if (!found) { bounds = new Bounds(point, Vector3.zero); found = true; }
else bounds.Encapsulate(point);
}
}
return found;
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 8aa0d5b728d34ab4b9fc5e4b23bedad0
+98
View File
@@ -0,0 +1,98 @@
using UnityEngine;
using UnityEngine.Rendering;
/// <summary>A local sky, independent of RenderSettings.skybox, ready for the existing projection masks.</summary>
[ExecuteAlways, DisallowMultipleComponent, RequireComponent(typeof(PortalMeadow)), DefaultExecutionOrder(210)]
public sealed class PortalSkyDome : MonoBehaviour
{
public enum SkyVisibility { Preview, PortalOnly, Hidden }
[SerializeField] private Material skyMaterial;
[SerializeField] private SkyVisibility visibility = SkyVisibility.Preview;
[SerializeField] private bool insidePortalB;
[Tooltip("The sphere also expands automatically to contain the terrain and its bend bounds.")]
[SerializeField, Min(10)] private float minimumRadius = 100;
private PortalMeadow meadow;
private GameObject dome;
private Mesh mesh;
private Material runtimeMaterial;
private Material sourceMaterial;
private void OnEnable() { meadow = GetComponent<PortalMeadow>(); }
private void LateUpdate()
{
if (!meadow) meadow = GetComponent<PortalMeadow>();
if (!meadow || !skyMaterial) { Clear(); return; }
if (!dome || sourceMaterial != skyMaterial) Build();
// The centre is the same initial snapshot used by the terrain and the server rings.
dome.transform.localPosition = meadow.SurfaceCenter;
float radius = Mathf.Max(minimumRadius, meadow.Size.magnitude + meadow.BoundsPadding + 20);
dome.transform.localScale = Vector3.one * radius;
runtimeMaterial.CopyPropertiesFromMaterial(skyMaterial);
runtimeMaterial.SetFloat("_Visibility", (float)visibility);
runtimeMaterial.SetFloat("_InsidePortalB", insidePortalB ? 1 : 0);
}
// Ready to connect to the future portal's enter/exit UnityEvents. No distance trigger is inferred.
public void EnterPortalB() { SetInsidePortalB(true); }
public void ExitPortalB() { SetInsidePortalB(false); }
public void SetInsidePortalB(bool inside)
{
insidePortalB = inside;
visibility = SkyVisibility.PortalOnly;
if (runtimeMaterial)
{
runtimeMaterial.SetFloat("_Visibility", (float)visibility);
runtimeMaterial.SetFloat("_InsidePortalB", inside ? 1 : 0);
}
}
private void Build()
{
Clear();
sourceMaterial = skyMaterial;
runtimeMaterial = new Material(skyMaterial) { name = "PortalB Sky (runtime)", hideFlags = HideFlags.DontSave };
dome = new GameObject("PortalB Sky Sphere (generated)") { hideFlags = HideFlags.DontSave, layer = gameObject.layer };
dome.transform.SetParent(transform, false);
const int segments = 64, rings = 32;
var positions = new Vector3[(segments + 1) * (rings + 1)];
var triangles = new int[segments * rings * 6];
for (int y = 0; y <= rings; y++)
for (int x = 0; x <= segments; x++)
{
float latitude = Mathf.PI * y / rings, longitude = 2 * Mathf.PI * x / segments;
positions[y * (segments + 1) + x] = new Vector3(Mathf.Sin(latitude) * Mathf.Cos(longitude),
Mathf.Cos(latitude), Mathf.Sin(latitude) * Mathf.Sin(longitude));
}
int index = 0;
for (int y = 0; y < rings; y++)
for (int x = 0; x < segments; x++)
{
int a = y * (segments + 1) + x, b = a + segments + 1;
triangles[index++] = a; triangles[index++] = a + 1; triangles[index++] = b;
triangles[index++] = b; triangles[index++] = a + 1; triangles[index++] = b + 1;
}
mesh = new Mesh { name = "Portal sky sphere", hideFlags = HideFlags.DontSave };
mesh.vertices = positions;
mesh.triangles = triangles;
mesh.RecalculateBounds();
dome.AddComponent<MeshFilter>().sharedMesh = mesh;
var renderer = dome.AddComponent<MeshRenderer>();
renderer.sharedMaterial = runtimeMaterial;
renderer.shadowCastingMode = ShadowCastingMode.Off;
renderer.receiveShadows = false;
renderer.allowOcclusionWhenDynamic = false;
}
private void OnDisable() { Clear(); }
private void Clear()
{
if (dome) { dome.SetActive(false); Release(dome); }
if (mesh) Release(mesh);
if (runtimeMaterial) Release(runtimeMaterial);
dome = null; mesh = null; runtimeMaterial = null; sourceMaterial = null;
}
private static void Release(Object value)
{
if (Application.isPlaying) Destroy(value);
else DestroyImmediate(value);
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: bfc7df93461546eea0d803683d7f3c78