/* * Copyright (c) Meta Platforms, Inc. and affiliates. * All rights reserved. * * Licensed under the Oculus SDK License Agreement (the "License"); * you may not use the Oculus SDK except in compliance with the License, * which is provided at the time of installation or download, or which * otherwise accompanies this software in either electronic or hard copy form. * * You may obtain a copy of the License at * * https://developer.oculus.com/licenses/oculussdk/ * * Unless required by applicable law or agreed to in writing, the Oculus SDK * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ using System.Runtime.InteropServices; using Unity.Collections; using UnityEngine; using UnityEngine.Rendering; namespace Oculus.Interaction { public struct TubePoint { public Vector3 position; public Quaternion rotation; public float relativeLength; } /// /// Creates and renders a tube mesh from a sequence of points. /// public class TubeRenderer : MonoBehaviour { [StructLayout(LayoutKind.Sequential)] private struct VertexLayout { public Vector3 pos; public Color32 color; public Vector2 uv; } /// /// The Mesh Filter that's included in the ReticleLine prefab. /// [Tooltip("The Mesh Filter that's included in the ReticleLine prefab.")] [SerializeField] private MeshFilter _filter; /// /// The Mesh Renderer that's included in the ReticleLine prefab. /// [Tooltip("The Mesh Renderer that's included in the ReticleLine prefab.")] [SerializeField] private MeshRenderer _renderer; /// /// The number of divisions to use when calculating the tube mesh's vertices. /// [Tooltip("The number of divisions to use when calculating the tube mesh's vertices.")] [SerializeField] private int _divisions = 6; /// /// The number of bevels to use when calculating the tube mesh's vertices. /// [Tooltip("The number of bevels to use when calculating the tube mesh's vertices.")] [SerializeField] private int _bevel = 4; /// /// Unity shader queue that determines when the tube is rendered. Defaults to -1, which uses the render queue of the shader. /// [Tooltip("Unity shader queue that determines when the tube is rendered. Defaults to -1, which uses the render queue of the shader.")] [SerializeField] private int _renderQueue = -1; public int RenderQueue { get { return _renderQueue; } set { _renderQueue = value; } } [SerializeField] private Vector2 _renderOffset = Vector2.zero; public Vector2 RenderOffset { get { return _renderOffset; } set { _renderOffset = value; } } /// /// The thickness of the tube. /// [Tooltip("The thickness of the tube.")] [SerializeField] private float _radius = 0.005f; public float Radius { get { return _radius; } set { _radius = value; } } /// /// The gradient of the tube. /// [Tooltip("The gradient of the tube.")] [SerializeField] private Gradient _gradient; public Gradient Gradient { get { return _gradient; } set { _gradient = value; } } /// /// The color of the tube. /// [Tooltip("The color of the tube.")] [SerializeField] private Color _tint = Color.white; public Color Tint { get { return _tint; } set { _tint = value; } } [SerializeField, Range(0f, 1f)] private float _progressFade = 0.2f; public float ProgressFade { get { return _progressFade; } set { _progressFade = value; } } /// /// Defines the length of the transparent portion at the beginning of the tube. The higher the value, the longer the transparent portion. /// [Tooltip("Defines the length of the transparent portion at the beginning of the tube. The higher the value, the longer the transparent portion.")] [SerializeField] private float _startFadeThresold = 0.2f; public float StartFadeThresold { get { return _startFadeThresold; } set { _startFadeThresold = value; } } /// /// Defines the length of the transparent portion at the end of the tube. The higher the value, the longer the transparent portion. /// [Tooltip("Defines the length of the transparent portion at the end of the tube. The higher the value, the longer the transparent portion.")] [SerializeField] private float _endFadeThresold = 0.2f; public float EndFadeThresold { get { return _endFadeThresold; } set { _endFadeThresold = value; } } /// /// Determines if the transparent portion of the tube should be in the middle instead of at the beginning and end. /// [Tooltip("Should the transparent portion of the tube be in the middle instead of at the beginning and end?")] [SerializeField] private bool _invertThreshold = false; public bool InvertThreshold { get { return _invertThreshold; } set { _invertThreshold = value; } } [SerializeField] private float _feather = 0.2f; public float Feather { get { return _feather; } set { _feather = value; } } [SerializeField] private bool _mirrorTexture; public bool MirrorTexture { get { return _mirrorTexture; } set { _mirrorTexture = value; } } public float Progress { get; set; } = 0f; public float TotalLength => _totalLength; private VertexAttributeDescriptor[] _dataLayout; private NativeArray _vertsData; private VertexLayout _layout = new VertexLayout(); private Mesh _mesh; private int[] _tris; private int _initializedSteps = -1; private int _vertsCount; private float _totalLength = 0f; private bool _hidden = false; private static readonly int _fadeLimitsShaderID = Shader.PropertyToID("_FadeLimit"); private static readonly int _fadeSignShaderID = Shader.PropertyToID("_FadeSign"); private static readonly int _offsetFactorShaderPropertyID = Shader.PropertyToID("_OffsetFactor"); private static readonly int _offsetUnitsShaderPropertyID = Shader.PropertyToID("_OffsetUnits"); #region Editor events protected virtual void Reset() { _filter = this.GetComponent(); _renderer = this.GetComponent(); } #endregion protected virtual void Awake() { _hidden = this.enabled; } protected virtual void OnEnable() { _renderer.enabled = !_hidden; } protected virtual void OnDisable() { _renderer.enabled = false; } /// /// Updates the mesh data for the tube with the specified points. /// If the component is enabled it will automatically show the renderer. /// /// The points that the tube must follow /// Indicates if the points are specified in local space or world space public void RenderTube(TubePoint[] points, Space space = Space.Self) { int steps = points.Length; if (steps != _initializedSteps) { InitializeMeshData(steps); _initializedSteps = steps; } _vertsData = new NativeArray(_vertsCount, Allocator.Temp); UpdateMeshData(points, space); _renderer.enabled = this.enabled; _hidden = false; } /// /// Hides the renderer of the tube /// public void Hide() { _renderer.enabled = false; _hidden = true; } /// /// Shows the renderer of the tube /// public void Show() { _renderer.enabled = true; _hidden = false; } private void InitializeMeshData(int steps) { _dataLayout = new VertexAttributeDescriptor[] { new VertexAttributeDescriptor(VertexAttribute.Position, VertexAttributeFormat.Float32, 3), new VertexAttributeDescriptor(VertexAttribute.Color, VertexAttributeFormat.UNorm8, 4), new VertexAttributeDescriptor(VertexAttribute.TexCoord0, VertexAttributeFormat.Float32, 2), }; _vertsCount = SetVertexCount(steps, _divisions, _bevel); SubMeshDescriptor submeshDesc = new SubMeshDescriptor(0, _tris.Length, MeshTopology.Triangles); _mesh = new Mesh(); _mesh.SetVertexBufferParams(_vertsCount, _dataLayout); _mesh.SetIndexBufferParams(_tris.Length, IndexFormat.UInt32); _mesh.SetIndexBufferData(_tris, 0, 0, _tris.Length); _mesh.subMeshCount = 1; _mesh.SetSubMesh(0, submeshDesc); _filter.mesh = _mesh; } private void UpdateMeshData(TubePoint[] points, Space space) { int steps = points.Length; float totalLength = 0f; Vector3 prevPoint = Vector3.zero; Pose pose = Pose.identity; Pose start = Pose.identity; Pose end = Pose.identity; Pose rootPose = this.transform.GetPose(Space.World); Quaternion inverseRootRotation = Quaternion.Inverse(rootPose.rotation); Vector3 rootPositionScaled = new Vector3( rootPose.position.x / this.transform.lossyScale.x, rootPose.position.y / this.transform.lossyScale.y, rootPose.position.z / this.transform.lossyScale.z); float uniformScale = space == Space.World ? this.transform.lossyScale.x : 1f; TransformPose(points[0], ref start); TransformPose(points[points.Length - 1], ref end); BevelCap(start, false, 0); for (int i = 0; i < steps; i++) { TransformPose(points[i], ref pose); Vector3 point = pose.position; Quaternion rotation = pose.rotation; float progress = points[i].relativeLength; Color color = Gradient.Evaluate(progress) * _tint; if (i > 0) { totalLength += Vector3.Distance(point, prevPoint); } prevPoint = point; if (i / (steps - 1f) < Progress) { color.a *= ProgressFade; } _layout.color = color; WriteCircle(point, rotation, _radius, i + _bevel, progress); } BevelCap(end, true, _bevel + steps); _mesh.bounds = new Bounds( (start.position + end.position) * 0.5f, end.position - start.position); _mesh.SetVertexBufferData(_vertsData, 0, 0, _vertsData.Length, 0, MeshUpdateFlags.DontRecalculateBounds); _totalLength = totalLength * uniformScale; RedrawFadeThresholds(); void TransformPose(in TubePoint tubePoint, ref Pose pose) { if (space == Space.Self) { pose.position = tubePoint.position; pose.rotation = tubePoint.rotation; return; } pose.position = inverseRootRotation * (tubePoint.position - rootPositionScaled); pose.rotation = inverseRootRotation * tubePoint.rotation; } } /// /// Resubmits the fading thresholds data to the material without re-generating the mesh /// public void RedrawFadeThresholds() { float originFadeIn = StartFadeThresold / _totalLength; float originFadeOut = (StartFadeThresold + Feather) / _totalLength; float endFadeIn = (_totalLength - EndFadeThresold) / _totalLength; float endFadeOut = (_totalLength - EndFadeThresold - Feather) / _totalLength; _renderer.material.SetVector(_fadeLimitsShaderID, new Vector4( _invertThreshold ? originFadeOut : originFadeIn, _invertThreshold ? originFadeIn : originFadeOut, endFadeOut, endFadeIn)); _renderer.material.SetFloat(_fadeSignShaderID, _invertThreshold ? -1 : 1); _renderer.material.renderQueue = _renderQueue; _renderer.material.SetFloat(_offsetFactorShaderPropertyID, _renderOffset.x); _renderer.material.SetFloat(_offsetUnitsShaderPropertyID, _renderOffset.y); } private void BevelCap(in Pose pose, bool end, int indexOffset) { Vector3 origin = pose.position; Quaternion rotation = pose.rotation; for (int i = 0; i < _bevel; i++) { float radiusFactor = Mathf.InverseLerp(-1, _bevel + 1, i); if (end) { radiusFactor = 1 - radiusFactor; } float positionFactor = Mathf.Sqrt(1 - radiusFactor * radiusFactor); Vector3 point = origin + (end ? 1 : -1) * (rotation * Vector3.forward) * _radius * positionFactor; WriteCircle(point, rotation, _radius * radiusFactor, i + indexOffset, end ? 1 : 0); } } private void WriteCircle(Vector3 point, Quaternion rotation, float width, int index, float progress) { Color color = Gradient.Evaluate(progress) * _tint; if (progress < Progress) { color.a *= ProgressFade; } _layout.color = color; for (int j = 0; j <= _divisions; j++) { float radius = 2 * Mathf.PI * j / _divisions; Vector3 circle = new Vector3(Mathf.Sin(radius), Mathf.Cos(radius), 0); Vector3 normal = rotation * circle; _layout.pos = point + normal * width; if (_mirrorTexture) { float x = (j / (float)_divisions) * 2f; if (j >= _divisions * 0.5f) { x = 2 - x; } _layout.uv = new Vector2(x, progress); } else { _layout.uv = new Vector2(j / (float)_divisions, progress); } int vertIndex = index * (_divisions + 1) + j; _vertsData[vertIndex] = _layout; } } private int SetVertexCount(int positionCount, int divisions, int bevelCap) { bevelCap = bevelCap * 2; int vertsPerPosition = divisions + 1; int vertCount = (positionCount + bevelCap) * vertsPerPosition; int tubeTriangles = (positionCount - 1 + bevelCap) * divisions * 6; int capTriangles = (divisions - 2) * 3; int triangleCount = tubeTriangles + capTriangles * 2; _tris = new int[triangleCount]; // handle triangulation for (int i = 0; i < positionCount - 1 + bevelCap; i++) { // add faces for (int j = 0; j < divisions; j++) { int vert0 = i * vertsPerPosition + j; int vert1 = (i + 1) * vertsPerPosition + j; int t = (i * divisions + j) * 6; _tris[t] = vert0; _tris[t + 1] = _tris[t + 4] = vert1; _tris[t + 2] = _tris[t + 3] = vert0 + 1; _tris[t + 5] = vert1 + 1; } } // triangulate the ends Cap(tubeTriangles, 0, divisions - 1, true); Cap(tubeTriangles + capTriangles, vertCount - divisions, vertCount - 1); void Cap(int t, int firstVert, int lastVert, bool clockwise = false) { for (int i = firstVert + 1; i < lastVert; i++) { _tris[t++] = firstVert; _tris[t++] = clockwise ? i : i + 1; _tris[t++] = clockwise ? i + 1 : i; } } return vertCount; } #region Inject public void InjectAllTubeRenderer(MeshFilter filter, MeshRenderer renderer, int divisions, int bevel) { InjectFilter(filter); InjectRenderer(renderer); InjectDivisions(divisions); InjectBevel(bevel); } public void InjectFilter(MeshFilter filter) { _filter = filter; } public void InjectRenderer(MeshRenderer renderer) { _renderer = renderer; } public void InjectDivisions(int divisions) { _divisions = divisions; } public void InjectBevel(int bevel) { _bevel = bevel; } #endregion } }