Skip to content

Instantly share code, notes, and snippets.

@Libberator
Created February 3, 2023 20:23
Show Gist options
  • Save Libberator/26c9176e4e51d7a52481ab90175d265d to your computer and use it in GitHub Desktop.
Save Libberator/26c9176e4e51d7a52481ab90175d265d to your computer and use it in GitHub Desktop.
Roll-A-Tetrahedron
using UnityEngine;
// This is just a very basic example to get you started.
// To use, attach to an empty GameObject and use the ContextMenu options.
// Ideally this would be an EditorWindow script. But since this is just meant for a one-time use, it's not too important.
// Note: This does not generate UVs. Mapping a texture to just 4 potential UV coordinates is difficult; consider triplicating vertices or alternative approach.
public class TetrahedronMeshCreator : MonoBehaviour
{
private enum Orientation { PointyEndForward, PointyEndRight }
[SerializeField] private Material _defaultMaterial;
[SerializeField] private Orientation _orientation = Orientation.PointyEndForward;
#if UNITY_EDITOR
[Tooltip("This is for saving the mesh data to a file.")]
[SerializeField] private string _meshSaveName = "NewMeshData";
#endif
private const float ONE_OVER_ROOT_THREE = 0.577350269f; // floored center to vertex along the base
private const float ROOT_THREE_OVER_SIX = 0.288675135f; // floored center to mid-edge along the base
// private const float OVERALL_HEIGHT = 0.816496581f; // sqrt(2/3) = base to top vertex. for reference
private const float CENTER_TO_BOT = 0.204124145f; // sqrt(6)/12 = height diff from center to floor
private const float CENTER_TO_TOP = 0.612372436f; // OVERALL_HEIGHT - CENTER_TO_BOT
private void Reset()
{
var shader = Shader.Find("Universal Render Pipeline/Lit") ?? Shader.Find("Standard");
_defaultMaterial = new Material(shader);
}
// pointy end is forward
private readonly Vector3[] _verticesF = new Vector3[4] {
new Vector3(0.5f, -CENTER_TO_BOT, -ROOT_THREE_OVER_SIX), // back-right
new Vector3(0f, -CENTER_TO_BOT, ONE_OVER_ROOT_THREE), // forward
new Vector3(-0.5f, -CENTER_TO_BOT, -ROOT_THREE_OVER_SIX), // back-left
new Vector3(0f, CENTER_TO_TOP, 0f), // top center
};
// pointy end to the right
private readonly Vector3[] _verticesR = new Vector3[4] {
new Vector3(ONE_OVER_ROOT_THREE, -CENTER_TO_BOT, 0f), // right
new Vector3(-ROOT_THREE_OVER_SIX, -CENTER_TO_BOT, 0.5f), // front-left
new Vector3(-ROOT_THREE_OVER_SIX, -CENTER_TO_BOT, -0.5f), // back-left
new Vector3(0f, CENTER_TO_TOP, 0f), // top center
};
private readonly int[] _triangles = new int[12]
{
0, 1, 2, // bottom face
1, 0, 3, // front-right face
0, 2, 3, // back or back-right face
2, 1, 3 // front-left or left face
};
public Mesh GenerateMeshData()
{
var verts = _orientation == Orientation.PointyEndForward ? _verticesF : _verticesR;
var mesh = new Mesh() {
vertices = verts,
triangles = _triangles
};
mesh.RecalculateNormals();
return mesh;
}
[ContextMenu("Create Tetrahedron")]
public void CreateTetrahedron()
{
var mesh = GenerateMeshData();
var root = new GameObject("Tetrahedron").transform;
var child = new GameObject("Mesh Visuals");
child.transform.SetParent(root);
child.transform.localPosition = new Vector3(0f, CENTER_TO_BOT, 0f);
child.AddComponent<MeshFilter>().sharedMesh = mesh;
child.AddComponent<MeshRenderer>().material = _defaultMaterial;
child.AddComponent<MeshCollider>().sharedMesh = mesh;
#if UNITY_EDITOR
UnityEditor.EditorGUIUtility.PingObject(root);
#endif
Debug.Log("Tetrahedron created! Please add a [TetrahedronMover] component to the root GameObject.", root);
}
#if UNITY_EDITOR
[ContextMenu("Save Mesh Data to Assets")]
public void SaveAsset()
{
var savePath = $"Assets/{_meshSaveName}.mesh";
var mesh = GenerateMeshData();
UnityEditor.AssetDatabase.CreateAsset(mesh, savePath);
var asset = UnityEditor.AssetDatabase.LoadAssetAtPath(savePath, typeof(Mesh));
UnityEditor.EditorGUIUtility.PingObject(asset);
Debug.Log($"Mesh saved to: \"{savePath}\".", asset);
}
#endif
}
using UnityEngine;
using System.Collections;
[SelectionBase]
public class TetrahedronMover : MonoBehaviour
{
[Tooltip("This is where the raycasts will originate from and what gets rotated. Use child mesh object.")]
[SerializeField] private Transform _center;
[SerializeField, Min(0.05f)] private float _timeToMove = 0.5f;
[SerializeField] private AnimationCurve _ease = AnimationCurve.EaseInOut(0f, 0f, 1f, 1f); // select OutSine preset or customize instead
[Header("Misc")]
[SerializeField] private bool _randomlyWalk = false;
private const float ROOT_THREE_OVER_SIX = 0.288675135f; // floored center to edge, or half a step
private bool _isMoving = false;
private Transform _mainCam;
private void Start()
{
_mainCam = Camera.main.transform;
Physics.queriesHitBackfaces = true; // Note: this is a global setting
}
private void Update()
{
if (_isMoving) return;
var direction = GetInputDirection();
if (direction == Vector3.zero) return;
StartCoroutine(RollATetrahedron(direction));
}
private Vector3 GetInputDirection()
{
if (_randomlyWalk)
{
var rng = Random.insideUnitCircle;
return new Vector3(rng.x, 0f, rng.y);
}
var input = new Vector3(Input.GetAxisRaw("Horizontal"), 0f, Input.GetAxisRaw("Vertical"));
var camAngle = _mainCam.eulerAngles.y;
return Quaternion.Euler(0f, camAngle, 0f) * input;
}
private IEnumerator RollATetrahedron(Vector3 direction)
{
if (!Physics.Raycast(_center.position, direction, out RaycastHit hit, 1f)) yield break;
_isMoving = true;
// Set up the movement vectors for Slerping
var halfStep = ROOT_THREE_OVER_SIX * new Vector3(hit.normal.x, 0f, hit.normal.z).normalized;
var startOffset = _center.localPosition - halfStep;
var targetOffset = _center.localPosition + halfStep;
var anchor = transform.position - startOffset;
// Set up the rotation quaternions for Slerping
var deltaRot = Quaternion.FromToRotation(hit.normal, Vector3.down);
var startRot = _center.rotation;
var targetRot = deltaRot * startRot;
var elapsedTime = 0f;
while (elapsedTime < _timeToMove)
{
var t = _ease.Evaluate(elapsedTime / _timeToMove);
transform.position = anchor + Vector3.Slerp(startOffset, targetOffset, t);
_center.rotation = Quaternion.Slerp(startRot, targetRot, t);
yield return null;
elapsedTime += Time.deltaTime;
}
transform.position = anchor + targetOffset;
_center.rotation = targetRot;
_isMoving = false;
}
}
using UnityEngine;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
[SelectionBase]
public class TetrahedronMoverRotateAround : MonoBehaviour
{
[SerializeField] private List<Transform> _vertices;
[SerializeField, Min(0.05f)] private float _timeToMove = 0.75f;
private const float DEGREES = 109.4712206f;
private bool _isMoving = false;
private Transform _mainCam;
private void Start()
{
_mainCam = Camera.main.transform;
}
private void Update()
{
if (_isMoving) return;
var direction = GetInputDirection();
if (direction == Vector3.zero) return;
StartCoroutine(RollATetrahedron(direction));
}
private Vector3 GetInputDirection()
{
var input = new Vector3(Input.GetAxisRaw("Horizontal"), 0f, Input.GetAxisRaw("Vertical"));
var camAngle = _mainCam.eulerAngles.y;
return Quaternion.Euler(0f, camAngle, 0f) * input;
}
private IEnumerator RollATetrahedron(Vector3 dir)
{
_isMoving = true;
// acts as our "carrot on a stick", used for a distance check
var target = transform.position + dir;
// given world position vertices, ignore top-most vertex and take the closest two
var closestVertices = _vertices.Where(v => v.position.y < 0.5f)
.OrderBy(v => Vector3.Distance(v.position, target)).Take(2).ToArray();
// the direction of the axis vector is important for how RotateAround works
var axis = closestVertices[1].position - closestVertices[0].position;
// we can use the Cross Product to know if this will rotate correctly
if (Vector3.Cross(dir, axis).y < 0f) // the resulting vector should point upwards
axis = -axis;
// either one will work for the anchor point
var anchor = closestVertices[0].position;
var totalDegrees = 0f;
while (totalDegrees < DEGREES)
{
var deltaDeg = DEGREES * Time.deltaTime / _timeToMove;
transform.RotateAround(anchor, axis, deltaDeg);
totalDegrees += deltaDeg;
yield return null;
}
transform.RotateAround(anchor, axis, DEGREES - totalDegrees); // undo any slight overshooting
_isMoving = false;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment