|
| 1 | +// VROInputPresenterOpenXR.h |
| 2 | +// ViroRenderer |
| 3 | +// |
| 4 | +// Visual presenter for the OpenXR (Meta Quest) input controller. Provides: |
| 5 | +// 1. A reticle (existing VROReticle) positioned at the hit point in world |
| 6 | +// space (not headlocked — Cardboard-style fixed pointer is wrong here |
| 7 | +// because we have actual 6DOF aim from controllers / FB hand-aim). |
| 8 | +// 2. A laser line from the aim origin to the reticle. |
| 9 | +// |
| 10 | +// Both are children of the presenter's _rootNode, which VROScene attaches to |
| 11 | +// the scene root via attachInputController(). The reticle position is updated |
| 12 | +// via the existing onGazeHit → onReticleGazeHit chain. The laser endpoints |
| 13 | +// are updated each frame by VROInputControllerOpenXR via updateAimRay(). |
| 14 | +// |
| 15 | +// Copyright © 2026 ReactVision. All rights reserved. |
| 16 | +// MIT License — see LICENSE file. |
| 17 | + |
| 18 | +#ifndef ANDROID_VROINPUTPRESENTEROPENXR_H |
| 19 | +#define ANDROID_VROINPUTPRESENTEROPENXR_H |
| 20 | + |
| 21 | +#include <memory> |
| 22 | +#include <vector> |
| 23 | +#include "VROInputPresenter.h" |
| 24 | +#include "VROReticle.h" |
| 25 | +#include "VRONode.h" |
| 26 | +#include "VROPolyline.h" |
| 27 | +#include "VROMaterial.h" |
| 28 | + |
| 29 | +class VROInputPresenterOpenXR : public VROInputPresenter { |
| 30 | +public: |
| 31 | + VROInputPresenterOpenXR() { |
| 32 | + // Reticle at world-space hit point (not headlocked). |
| 33 | + auto reticle = std::make_shared<VROReticle>(nullptr); |
| 34 | + reticle->setPointerFixed(false); |
| 35 | + setReticle(reticle); |
| 36 | + |
| 37 | + // Build the laser geometry once. Per-frame updates mutate the existing |
| 38 | + // polyline's points via setPaths() rather than swapping the node's |
| 39 | + // geometry — swapping caused a one-frame blink because the new |
| 40 | + // geometry isn't fully uploaded to the GPU until the next render. |
| 41 | + std::vector<VROVector3f> initialPath = { {0, 0, 0}, {0, 0, -1} }; |
| 42 | + _laserGeom = VROPolyline::createPolyline(initialPath, 0.003f /* thickness */); |
| 43 | + _laserGeom->setName("AimLaserGeom"); |
| 44 | + auto material = _laserGeom->getMaterials().front(); |
| 45 | + material->getDiffuse().setColor({ 0.33f, 0.976f, 0.968f, 1.0f }); // cyan |
| 46 | + material->setWritesToDepthBuffer(false); |
| 47 | + material->setReadsFromDepthBuffer(false); |
| 48 | + material->setReceivesShadows(false); |
| 49 | + |
| 50 | + _laserNode = std::make_shared<VRONode>(); |
| 51 | + _laserNode->setName("AimLaser"); |
| 52 | + _laserNode->setGeometry(_laserGeom); |
| 53 | + _laserNode->setHidden(true); // hidden until first updateAimRay() |
| 54 | + _rootNode->addChildNode(_laserNode); |
| 55 | + } |
| 56 | + |
| 57 | + ~VROInputPresenterOpenXR() override = default; |
| 58 | + |
| 59 | + /** |
| 60 | + * Inherit the existing reticle-positioning chain. processGazeEvent() in |
| 61 | + * VROInputControllerBase calls onGazeHit() on registered delegates; we |
| 62 | + * forward to onReticleGazeHit() which moves the reticle to hit world pos. |
| 63 | + */ |
| 64 | + void onGazeHit(int source, std::shared_ptr<VRONode> node, const VROHitTestResult &hit) override { |
| 65 | + VROInputPresenter::onReticleGazeHit(hit); |
| 66 | + } |
| 67 | + |
| 68 | + /** |
| 69 | + * Trigger reticle pulse animation on click for parity with OVR/Cardboard. |
| 70 | + */ |
| 71 | + void onClick(int source, std::shared_ptr<VRONode> node, ClickState clickState, |
| 72 | + std::vector<float> position) override { |
| 73 | + VROInputPresenter::onClick(source, node, clickState, position); |
| 74 | + if (clickState == ClickState::ClickUp && getReticle()) { |
| 75 | + getReticle()->trigger(); |
| 76 | + } |
| 77 | + } |
| 78 | + |
| 79 | + /** |
| 80 | + * Update the visible aim ray each frame. |
| 81 | + * |
| 82 | + * Mutates the persistent polyline's points in place via setPaths(). This |
| 83 | + * avoids the one-frame blink that occurs when VRONode::setGeometry() |
| 84 | + * swaps the geometry pointer and the new mesh hasn't been uploaded to |
| 85 | + * the GPU yet. |
| 86 | + * |
| 87 | + * @param origin World-space origin of the ray |
| 88 | + * @param hitPoint World-space endpoint |
| 89 | + * @param visible True to show, false to hide (no input active) |
| 90 | + */ |
| 91 | + void updateAimRay(const VROVector3f &origin, const VROVector3f &hitPoint, bool visible) { |
| 92 | + if (!visible) { |
| 93 | + _laserNode->setHidden(true); |
| 94 | + return; |
| 95 | + } |
| 96 | + _laserNode->setHidden(false); |
| 97 | + |
| 98 | + std::vector<std::vector<VROVector3f>> paths = { { origin, hitPoint } }; |
| 99 | + _laserGeom->setPaths(paths); |
| 100 | + } |
| 101 | + |
| 102 | +private: |
| 103 | + std::shared_ptr<VRONode> _laserNode; |
| 104 | + std::shared_ptr<VROPolyline> _laserGeom; |
| 105 | +}; |
| 106 | + |
| 107 | +#endif // ANDROID_VROINPUTPRESENTEROPENXR_H |
0 commit comments