0
\$\begingroup\$

I am creating an AR game in where I would like to virtualize an 8-ball pool table. Since semantic labeling is practically useless, I would like to have some manual placement of objects and some automatization. However I can't seem to fix the placement of the virtual pockets for my table. I am placing 3 pockets manually (from my initial perspective these are going to be bottom left, left middle and bottom - the order can be argued, but it is going to be a fixed one), the other 3 pockets are going to be automatically generated. Hovewer most of the time they are weirdly rotated (as group around a pivot) as they are all parented to a parent game object whose parent is the script I am using to place the pockets and autogenerate the missing ones. The first image shows how my ther markers are placed (aligning them is going to be another issue), the second how I imagine it is correct and third one how most of the time the code flips it incorrectly to the right. First image Second image Third

The current code that handles the logic is the following:

using Oculus.Interaction; using System.Collections.Generic; using TMPro; using UnityEngine; public class PocketSetupManager : MonoBehaviour { [Header("Marker Placement Settings")] [Tooltip("Prefab for the pocket marker (must have collider, rigidbody, Grabbable, HandGrabInteractable, etc.).")] public GameObject markerPrefab; public GameObject pocketMarkerRoot; //OVR Controls [Tooltip("Reference to left hand OVRHand component (for gesture detection).")] public OVRHand leftHand; [Tooltip("Reference to right hand OVRHand component (for gesture detection).")] public OVRHand rightHand; [Tooltip("Event sources for left and right source.")] public OVRMicrogestureEventSource LeftHandEventSource; public OVRMicrogestureEventSource RightHandEventSource; [Tooltip("Reference to the hand visual components.")] public HandVisual leftHandVisual; public HandVisual rightHandVisual; [Tooltip("Controller button to place a marker (fallback). e.g., Button.One = A (Right) or X (Left).")] public OVRInput.Button placeMarkerButton = OVRInput.Button.One; [Tooltip("Controller button to undo last marker (fallback). e.g., Button.Two = B (Right) or Y (Left).")] public OVRInput.Button undoMarkerButton = OVRInput.Button.Two; [Header("UI")] [Tooltip("Floating instruction text displayed to the user")] public TextMeshProUGUI instructionTextUI; [Header("Privates")] [Tooltip("Floating instruction text displayed to the user")] [SerializeField] private string _instructionText = string.Empty; [SerializeField] private sbyte _totalPockedNeeded = 3; private float _yCoordinateValueForMarkers = 0; private readonly string[] _pocketNames = { PocketName.BottomLeftCorner.ToString(), PocketName.MiddleLeftCorner.ToString(), PocketName.BottomRightCorner.ToString(), PocketName.TopLeftCorner.ToString(), PocketName.MiddleRightCorner.ToString(), PocketName.TopRightCorner.ToString(), }; private List<GameObject> _placedMarkers = new(6); private sbyte _markersPlacedCount = 0; private bool _groupModeActivate = false; private bool _allPocketsCalculated = false; // Start is called once before the first execution of Update after the MonoBehaviour is created private void Awake() { if (instructionTextUI != null) { instructionTextUI.text = _instructionText; UpdateInstructionUI(); } else { Debug.LogWarning("Instruction text UI refence not set."); } } void Start() { if (LeftHandEventSource == null || RightHandEventSource == null) { Debug.LogError($"No {nameof(OVRMicrogestureEventSource)} component attached to this gameobject."); } else { LeftHandEventSource.GestureRecognizedEvent.AddListener(gesture => OnMicrogestureRecognized(leftHand, gesture)); RightHandEventSource.GestureRecognizedEvent.AddListener(gesture => OnMicrogestureRecognized(rightHand, gesture)); } } // Update is called once per frame void Update() { if (_groupModeActivate) return; } void OnApplicationQuit() { LeftHandEventSource.GestureRecognizedEvent.RemoveAllListeners(); RightHandEventSource.GestureRecognizedEvent.RemoveAllListeners(); } private void OnDestroy() { LeftHandEventSource.GestureRecognizedEvent.RemoveAllListeners(); RightHandEventSource.GestureRecognizedEvent.RemoveAllListeners(); } public void OnMicrogestureRecognized(OVRHand hand, OVRHand.MicrogestureType gestureType) { Debug.Log($"Microgesture event: {gestureType} from hand {hand.name}."); //Right hand -> HandType is marked as internal. var isRightHand = hand.name.ToLower().Contains("right"); if (isRightHand) { switch (gestureType) { case OVRHand.MicrogestureType.ThumbTap: PlacePocketMarker(hand); break; case OVRHand.MicrogestureType.SwipeRight: FinalizePlacements(); break; case OVRHand.MicrogestureType.SwipeLeft: case OVRHand.MicrogestureType.SwipeForward: case OVRHand.MicrogestureType.SwipeBackward: default: Debug.Log("Gesture currently not supported."); break; } } UpdateInstructionUI(); } private void FinalizePlacements() { if (_allPocketsCalculated) { _instructionText = "All pockets have been placed at their respected positions. Nothing to do here."; Debug.Log(_instructionText); UpdateInstructionUI(); return; } if (_markersPlacedCount < _totalPockedNeeded) { Debug.LogWarning("Cannot finalize: not all required pocket markers are placed yet."); return; } Vector3 bottomLeft = _placedMarkers[(byte)PocketName.BottomLeftCorner].transform.position; Vector3 middleLeft = _placedMarkers[(byte)PocketName.MiddleLeftCorner].transform.position; Vector3 bottomRight = _placedMarkers[(byte)PocketName.BottomRightCorner].transform.position; //Top left Vector3 bottomLeftToLeftMiddle = middleLeft - bottomLeft; Vector3 topLeft = bottomLeft + 2 * bottomLeftToLeftMiddle; //Right middle Vector3 bottomLeftToBottomRight = bottomRight - bottomLeft; Vector3 middleRight = middleLeft + bottomLeftToBottomRight; //Top Right Vector3 topRight = topLeft + bottomLeftToBottomRight; topLeft.y = _yCoordinateValueForMarkers; middleRight.y = _yCoordinateValueForMarkers; topRight.y = _yCoordinateValueForMarkers; GameObject topLeftMarker = Instantiate(markerPrefab, topLeft, Quaternion.identity); topLeftMarker.name = _pocketNames[(byte)PocketName.TopLeftCorner]; GameObject middleRightMarker = Instantiate(markerPrefab, middleRight, Quaternion.identity); middleRightMarker.name = _pocketNames[(byte)PocketName.MiddleRightCorner]; GameObject topRightMarker = Instantiate(markerPrefab, topRight, Quaternion.identity); topRightMarker.name = _pocketNames[(byte)PocketName.TopRightCorner]; if (pocketMarkerRoot != null) { topLeftMarker.transform.SetParent(pocketMarkerRoot.transform, true); middleRightMarker.transform.SetParent(pocketMarkerRoot.transform, true); topRightMarker.transform.SetParent(pocketMarkerRoot.transform, true); } _placedMarkers[(byte)PocketName.TopLeftCorner] = topLeftMarker; _placedMarkers[(byte)PocketName.MiddleRightCorner] = middleRightMarker; _placedMarkers[(byte)PocketName.TopRightCorner] = topRightMarker; _markersPlacedCount = 6; _instructionText = "All 6 pockets positioned!"; Debug.Log("All pockets placed and finalized."); _allPocketsCalculated = true; UpdateInstructionUI(); } private void PlacePocketMarker(OVRHand hand) { if (_markersPlacedCount == _totalPockedNeeded) { _instructionText = "All of the required pockets have been instantiated. You can grab them and reposition them, before swiping right."; UpdateInstructionUI(); return; } //if (_groupModeActivate) return; if (pocketMarkerRoot == null) { Debug.LogError("No pocket marker root has been instantiated or has been deleted in runtime, so no (furher) pockets can be placed."); return; } var palmPosition = GetPalmWorldPosition(hand); // For the first pocket, store the Y-level if (_markersPlacedCount == 0) { _yCoordinateValueForMarkers = palmPosition.y; Debug.Log($"PalmPosition {palmPosition.y}"); Debug.DrawRay(palmPosition, Vector3.up * 0.1f, Color.green, 2f); } // Override Y to always match the first pocket’s Y palmPosition.y = _yCoordinateValueForMarkers; // Instantiate marker at (X,Z) of your hand + fixed Y GameObject marker = Instantiate(markerPrefab, palmPosition, Quaternion.identity); marker.name = _pocketNames[_markersPlacedCount]; marker.transform.SetParent(pocketMarkerRoot.transform, worldPositionStays: true); _placedMarkers.Add(marker); _markersPlacedCount++; // Feedback message sbyte remaining = (sbyte)(_totalPockedNeeded - _markersPlacedCount); _instructionText = remaining > 0 ? $"<b>{marker.name}</b> placed! {remaining} more to go…" : $"<b>{marker.name}</b> placed! Swipe right to confirm."; UpdateInstructionUI(); } private Vector3 GetPalmWorldPosition(OVRHand hand) { HandVisual handVisual = (hand == leftHand) ? leftHandVisual : rightHandVisual; if (handVisual == null) { Debug.LogWarning("Hand visual component not assigned."); return hand.transform.position; } return handVisual.GetTransformByHandJointId(Oculus.Interaction.Input.HandJointId.HandPalm).position; } private void UpdateInstructionUI() { if (instructionTextUI != null) { instructionTextUI.text = _instructionText; _instructionText = string.Empty; } else { Debug.LogWarning("Instruction text UI not assigned."); } } public void UndoLastMarker() { if (_markersPlacedCount == 0) return; _markersPlacedCount--; _markersPlacedCount = _markersPlacedCount < 0 ? (sbyte)0 : _markersPlacedCount; } } 

The prefab of the pocket also contains a scripts which limits the interaction to X and Z.

using UnityEngine; [RequireComponent(typeof(Transform))] public class XZOnlyConstraint : MonoBehaviour { [Tooltip("Whether this marker can currently be grabbed by the user.")] public bool GrabbableEnabled = true; private Vector3 _initialPosition; private Quaternion _initialRotation; private bool _isInitialized = false; public void Initialize(Quaternion worldRotation) { _initialPosition = transform.position; _initialRotation = worldRotation; _isInitialized = true; } public void Initialize() => Initialize(transform.rotation); private void LateUpdate() { if (!_isInitialized || !GrabbableEnabled || !this.isActiveAndEnabled) return; // Lock Y height Vector3 pos = transform.position; pos.y = _initialPosition.y; // Lock rotation to preserved transform.SetPositionAndRotation(pos, _initialRotation); } } 

How to aproach this issue of placing the pockets so that I can tackle other parts of defining the play area and what I am doing wrong? The essential features of the pocket placement part are:

  • Placing the pockets, having the manually placed pockets aligned.
  • Autogenerated the remaining pockets.
  • Undoing some (or all) misplaced pockets.
  • Confirming the placement and keep it somewhere so someone from the other Quest can acces it?
\$\endgroup\$

1 Answer 1

0
\$\begingroup\$

I would approach this differently, specifically I would start by creating the 3D model representing the table. Then all I have to do is place the model into the world, specifically: Position, Scale & Rotation

I would ask the user to give me 2 diagonally opposite corner pockets, say top-left and bottom-right.

Position

The center of the 3D model should be the center of the table therefore in order to position the table, all I need to do is average the position of the two pockets (that the user provided) - that will be the center of the table.

Scale

I know the (unscaled) length between 2 diagonally opposite pockets in the 3D model, therefore if I calculate the distance between the two user provided pockets, I know the amount to scale the model.

Rotation

Imagine two people are carrying a "stretchy" pool table by it's diagonally opposite corners. If they move closer and further apart the table grows and shrinks uniformly in all 3 axises.

If they move around keeping their separation fixed, they can position and orientate the table however they want, with one caveat there is a rotation along the line between them that they can't easily control.

Therefore we can actually render the table model as soon at the user gives us the second point. If the user drags either point we can update the position, scale and rotation such that it keeps both model corners aligned with the positions the user provided.

Initially we will assume the the positive Y axis is correct so that there is no rotation along the "uncontrolled" axis (the line between the two points).

Once the user is happy with the position of the two points we may provide an addition control to effective rock/tilt the table about the final axis (the line between the two points) - this may not be necessary if the room is mapped correctly.

Transformation Matrix

Given we have defined a position, scale and (unified) rotation what we have really defined is a 3D (3x4) transformation matrix.

Said matrix fully defines your table, hence that is all that needs to be shared with other clients / persisted for later use.

You could provide additional controls to directly manipulate the position/scale, however I think just defining two points and providing one additional rotation control is a fairly clean interface.

\$\endgroup\$
4
  • \$\begingroup\$ An interesting line of though, however I am poor at modeling, so calculating everything from 3 positions seems way quicker to me, since I don't need the whole pool table. Still the "rotation" and "location jump" are issue that bother me currently more. \$\endgroup\$ Commented Jul 13 at 9:31
  • 1
    \$\begingroup\$ Typically a pool table has a width to height ratio or approximately 1:2 hence the pockets are located at (-1, -2), (-1, 0), (-1, 2), (1, 2), (1, 0) and (1, -2) - adjust to exact ratio if required. Hence the strategy outlined above could be used without a 3D model, if you just wanted to transform those positions into real world 3D coordinates. \$\endgroup\$ Commented Jul 13 at 18:19
  • \$\begingroup\$ Hm, actually the ratio can be computed, but yeah it is around 1:2 and even if the final values are a bit off a treshold for autocorreting can be easily computed. Now I just need to fix the "jumping" issues, since I don not want my markers to "leave" the location. \$\endgroup\$ Commented Jul 14 at 10:20
  • \$\begingroup\$ It seems when I am using the swipe microgestures I am also "rotating my view, so that would explain the "jump" in the location of my marker \$\endgroup\$ Commented Jul 14 at 11:03

You must log in to answer this question.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.