using System; using UnityEngine.Assertions; using System.Collections.Generic; using UnityEditor; using UnityEngine; namespace Rokid.UXR.Interaction { public class CanvasCylinder : CanvasMesh, ICurvedPlane { [Serializable] public struct MeshGenerationSettings { [Delayed] public float VerticesPerDegree; [Delayed] public int MaxHorizontalResolution; [Delayed] public int MaxVerticalResolution; } public const int MIN_RESOLUTION = 2; [SerializeField] [Tooltip("The cylinder used to dictate the position and radius of the mesh.")] private Cylinder _cylinder; [SerializeField] [Tooltip("Determines how the mesh is projected on the cylinder wall. " + "Vertical results in a left-to-right curvature, Horizontal results in a top-to-bottom curvature.")] private CylinderOrientation _orientation = CylinderOrientation.Vertical; [SerializeField] private MeshGenerationSettings _meshGeneration = new MeshGenerationSettings() { VerticesPerDegree = 1.4f, MaxHorizontalResolution = 128, MaxVerticalResolution = 32 }; public float Radius => _cylinder.Radius; public Cylinder Cylinder => _cylinder; public float ArcDegrees { get; private set; } public float Rotation { get; private set; } public float Bottom { get; private set; } public float Top { get; private set; } private float CylinderRelativeScale => _cylinder.transform.lossyScale.x / transform.lossyScale.x; protected override void Start() { Assert.IsNotNull(_cylinder); } #if UNITY_EDITOR protected virtual void OnValidate() { _meshGeneration.MaxHorizontalResolution = Mathf.Max(MIN_RESOLUTION, _meshGeneration.MaxHorizontalResolution); _meshGeneration.MaxVerticalResolution = Mathf.Max(MIN_RESOLUTION, _meshGeneration.MaxVerticalResolution); _meshGeneration.VerticesPerDegree = Mathf.Max(0, _meshGeneration.VerticesPerDegree); if (Application.isPlaying) { EditorApplication.delayCall += () => { UpdateImposter(); }; } } #endif public override void UpdateImposter() { base.UpdateImposter(); UpdateMeshPosition(); UpdateCurvedPlane(); } protected override Vector3 MeshInverseTransform(Vector3 localPosition) { float angle = Mathf.Atan2(localPosition.x / _cylinder.CylinderCanvasScaleWidth, localPosition.z + Radius); float x = angle * Radius; float y = localPosition.y / _cylinder.CylinderCanvasScaleHeight; return new Vector3(x, y); } protected override void GenerateMesh(out List verts, out List tris, out List uvs) { verts = new List(); tris = new List(); uvs = new List(); Vector2 worldSize = GetWorldSize(); float scaledRadius = Radius * CylinderRelativeScale; float xPos = worldSize.x * 0.5f; float xNeg = -xPos; float yPos = worldSize.y * 0.5f; float yNeg = -yPos; Vector2Int GetClampedResolution(float arcMax, float axisMax) { int horizontalResolution = Mathf.Max(2, Mathf.RoundToInt(_meshGeneration.VerticesPerDegree * Mathf.Rad2Deg * arcMax / scaledRadius)); int verticalResolution = Mathf.Max(2, Mathf.RoundToInt(horizontalResolution * axisMax / arcMax)); horizontalResolution = Mathf.Clamp(horizontalResolution, 2, _meshGeneration.MaxHorizontalResolution); verticalResolution = Mathf.Clamp(verticalResolution, 2, _meshGeneration.MaxVerticalResolution); return new Vector2Int(horizontalResolution, verticalResolution); } Vector3 GetCurvedPoint(float u, float v) { float x = Mathf.Lerp(xNeg, xPos, u); float y = Mathf.Lerp(yNeg, yPos, v); float angle; Vector3 point; switch (_orientation) { default: case CylinderOrientation.Vertical: angle = x / scaledRadius; point.x = Mathf.Sin(angle) * scaledRadius; point.y = y; point.z = Mathf.Cos(angle) * scaledRadius - scaledRadius; break; case CylinderOrientation.Horizontal: angle = y / scaledRadius; point.x = x; point.y = Mathf.Sin(angle) * scaledRadius; point.z = Mathf.Cos(angle) * scaledRadius - scaledRadius; break; } return point; } Vector2Int resolution; switch (_orientation) { default: case CylinderOrientation.Vertical: resolution = GetClampedResolution(xPos, yPos); break; case CylinderOrientation.Horizontal: resolution = GetClampedResolution(yPos, xPos); break; } for (int y = 0; y < resolution.y; y++) { for (int x = 0; x < resolution.x; x++) { float u = x / (resolution.x - 1.0f); float v = y / (resolution.y - 1.0f); verts.Add(GetCurvedPoint(u, v)); uvs.Add(new Vector2(u, v)); } } for (int y = 0; y < resolution.y - 1; y++) { for (int x = 0; x < resolution.x - 1; x++) { int v00 = x + y * resolution.x; int v10 = v00 + 1; int v01 = v00 + resolution.x; int v11 = v00 + 1 + resolution.x; tris.Add(v00); tris.Add(v11); tris.Add(v10); tris.Add(v00); tris.Add(v01); tris.Add(v11); } } } private void UpdateMeshPosition() { Vector3 posInCylinder = _cylinder.transform.InverseTransformPoint(transform.position); Vector3 localYOffset = new Vector3(0, posInCylinder.y, 0); Vector3 localCancelY = posInCylinder - localYOffset; // If canvas position is on cylinder center axis, project forward. // Otherwise, project canvas onto cylinder wall from center axis. Vector3 projection = Mathf.Approximately(localCancelY.sqrMagnitude, 0f) ? Vector3.forward : localCancelY.normalized; Vector3 localUp; switch (_orientation) { default: case CylinderOrientation.Vertical: localUp = Vector3.up; break; case CylinderOrientation.Horizontal: localUp = Vector3.right; break; } transform.position = _cylinder.transform.TransformPoint((projection * _cylinder.Radius) + localYOffset); transform.rotation = _cylinder.transform.rotation * Quaternion.LookRotation(projection, localUp); if (_meshCollider != null && _meshCollider.transform != transform && !transform.IsChildOf(_meshCollider.transform)) { _meshCollider.transform.position = transform.position; _meshCollider.transform.rotation = transform.rotation; _meshCollider.transform.localScale *= transform.lossyScale.x / _meshCollider.transform.lossyScale.x; } } private Vector2 GetWorldSize() { Vector2Int resolution = _canvasRenderTexture.GetBaseResolutionToUse(); float width = _canvasRenderTexture.PixelsToUnits(Mathf.RoundToInt(resolution.x)) * _cylinder.CylinderCanvasScaleWidth; float height = _canvasRenderTexture.PixelsToUnits(Mathf.RoundToInt(resolution.y)) * _cylinder.CylinderCanvasScaleHeight; return new Vector2(width, height) / transform.lossyScale; } private void UpdateCurvedPlane() { // Get world size in cylinder space Vector2 cylinderSize = GetWorldSize() / CylinderRelativeScale; float arcSize, axisSize; switch (_orientation) { default: case CylinderOrientation.Vertical: arcSize = cylinderSize.x; axisSize = cylinderSize.y; break; case CylinderOrientation.Horizontal: arcSize = cylinderSize.y; axisSize = cylinderSize.x; break; } Vector3 posInCylinder = Cylinder.transform.InverseTransformPoint(transform.position); Rotation = Mathf.Atan2(posInCylinder.x, posInCylinder.z) * Mathf.Rad2Deg; ArcDegrees = (arcSize * 0.5f / Radius) * 2f * Mathf.Rad2Deg; Top = posInCylinder.y + (axisSize * 0.5f); Bottom = posInCylinder.y - (axisSize * 0.5f); } } }