diff --git a/src/main/kotlin/graphics/scenery/Camera.kt b/src/main/kotlin/graphics/scenery/Camera.kt index 191316195..ccb13d929 100644 --- a/src/main/kotlin/graphics/scenery/Camera.kt +++ b/src/main/kotlin/graphics/scenery/Camera.kt @@ -6,6 +6,7 @@ import graphics.scenery.attribute.renderable.HasRenderable import graphics.scenery.attribute.spatial.DefaultSpatial import graphics.scenery.attribute.spatial.HasCustomSpatial import graphics.scenery.net.Networkable +import graphics.scenery.primitives.Cylinder import graphics.scenery.utils.extensions.minus import graphics.scenery.utils.extensions.plus import graphics.scenery.utils.extensions.times @@ -222,7 +223,7 @@ open class Camera : DefaultNode("Camera"), HasRenderable, HasMaterial, HasCustom } /** - * Returns the starting position and direction of a ray starting at the the screen space position + * Returns the starting position and direction of a ray starting at the screen space position * indicated by [x] and [y] targeting away from the camera. * * Returns (worldPos, worldDir) @@ -492,6 +493,129 @@ open class Camera : DefaultNode("Camera"), HasRenderable, HasMaterial, HasCustom } + /** + * This class provides possible position states for the orientation overlay. + */ + enum class OverlayAlignment { + CENTER, TOP_LEFT, TOP_RIGHT, BOTTOM_RIGHT, BOTTOM_LEFT + } + + private var orientationOverlayVisible = false + + /** + * Class to create an orientation overlay, consisting of three axes pointing towards positive X/Y/Z. + * Can be placed in the center or in any of the screen corners. + * @param camera the camera to attach the overlay to + * @param radius radius for the axes + * @param length length of the axes + * @param align four corners or the center of type [OverlayAlignment] + * @param margin how far from the corners the overlay should be + */ + private inner class OrientationOverlay( + camera: Camera = this, + radius: Float, + length: Float, + align: OverlayAlignment, + margin: Float + ) : DefaultNode("Orientation Overlay") { + + val axesRadius: Float = radius / 10 + val axesLength: Float = length / 10 + val axesParent: Node = Group() + + init { + axesParent.name = "Orientation Axes" + axesParent.ifSpatial { + position = getRelativePosition(margin, align) + } + var c = Cylinder(axesRadius / 2.0f, axesLength, 12) + + c.name = "X axis" + c.material { + diffuse = Vector3f(1f, 0f, 0f) + emissive = Vector4f(1f, 0f, 0f, 3f) + } + val halfPI = Math.PI.toFloat() / 2.0f + c.spatial().rotation = Quaternionf().rotateLocalZ(-halfPI) + axesParent.addChild(c) + + c = Cylinder(axesRadius / 2.0f, axesLength, 12) + c.name = "Y axis" + c.material { + diffuse = Vector3f(0f, 1f, 0f) + emissive = Vector4f(0f, 1f, 0f, 3f) + } + axesParent.addChild(c) + + c = Cylinder(axesRadius / 2.0f, axesLength, 12) + c.name = "Z axis" + c.material { + diffuse = Vector3f(0f, 0f, 1f) + emissive = Vector4f(0f, 0f, 1f, 3f) + } + c.spatial().rotation = Quaternionf().rotateLocalX(halfPI) + axesParent.addChild(c) + camera.addChild(axesParent) + + // workaround: continuously update the position. + // TODO Should be done with camera callback instead + thread { + while (true) { + axesParent.ifSpatial { + position = getRelativePosition(margin, align) + } + Thread.sleep(1000) + } + } + } + + fun getRelativePosition(margin: Float, align: OverlayAlignment): Vector3f { + // position normalized in range -1 to 1 + var pos = when (align) { + OverlayAlignment.CENTER -> Vector2f(0f, 0f) + OverlayAlignment.TOP_LEFT -> Vector2f(margin, margin) + OverlayAlignment.TOP_RIGHT -> Vector2f(1f - margin, 1f - margin) + OverlayAlignment.BOTTOM_RIGHT -> Vector2f(1f - margin, 1f - margin) + OverlayAlignment.BOTTOM_LEFT -> Vector2f(margin, 1f - margin) + } + return spatial().viewportToView(pos) + } + + fun updateRotation() { + axesParent.ifSpatial { + // NB: camera rotation is already inverse, no need to conjugate + rotation = this@Camera.spatial().rotation + } + } + } + + /** + * Call this method to attach an orientation overlay to the camera, + * consisting of three axes pointing towards positive X/Y/Z. + * @param align where to spawn the overlay, defaults to [OverlayAlignment.TOP_RIGHT]. + * @param margin how far from the screen corners to spawn the overlay. Defaults to a factor of 0.1. + * @param radius radius for the axes + * @param length length of the axes + */ + fun toggleOrientationOverlay( + align: OverlayAlignment = OverlayAlignment.TOP_RIGHT, + margin: Float = 0.1f, + radius: Float = 0.05f, + length: Float = 0.3f + ) { + orientationOverlayVisible = !orientationOverlayVisible + if (orientationOverlayVisible) { + val overlay = OrientationOverlay(this, radius, length, align, margin) + postUpdate.add { + overlay.updateRotation() + } + } else { + this.removeChild("Orientation Overlay") + } + } + + + @Deprecated(message = "", replaceWith = ReplaceWith("spatial().viewportToWorld(vector)")) fun viewportToWorld(vector: Vector2f): Vector3f { return spatial().viewportToWorld(vector) diff --git a/src/test/kotlin/graphics/scenery/tests/examples/basic/AtmosphereExample.kt b/src/test/kotlin/graphics/scenery/tests/examples/basic/AtmosphereExample.kt index b5d634dca..05e80ace4 100644 --- a/src/test/kotlin/graphics/scenery/tests/examples/basic/AtmosphereExample.kt +++ b/src/test/kotlin/graphics/scenery/tests/examples/basic/AtmosphereExample.kt @@ -42,7 +42,7 @@ class AtmosphereExample : SceneryBase("Atmosphere Example", renderer = hub.add( SceneryElement.Renderer, Renderer.createRenderer(hub, applicationName, scene, windowWidth, windowHeight)) - + renderer?.pushMode = true if (useVR) { hmd = OpenVRHMD(useCompositor = true) hub.add(SceneryElement.HMDInput, hmd) @@ -80,7 +80,8 @@ class AtmosphereExample : SceneryBase("Atmosphere Example", spatial { position = Vector3f(0.0f, 0.0f, 5.0f) } - perspectiveCamera(70.0f, 512, 768) + perspectiveCamera(70.0f, 768, 512) + if (!useVR) toggleOrientationOverlay() scene.addChild(this) }