Search code examples
androidquaternionssceneformsceneviewdraggesture

How to rotate an 3D model using DragGesture in Android SceneForm


I followed the solution here for rotating a TransformableNode on the X axis based on the user's DragGesture, using the Sceneform Android SDK. However, I would also like to rotate on the Y and Z axis as well, similar to how ARCore SceneViewer does it.

How can I achieve that?

What I have currently is on the left (rotates only on X axis), and what is desired is on the right (rotates on all axes, as in ARCore Scene Viewer).

class DragRotationController(transformableNode: BaseTransformableNode, gestureRecognizer: DragGestureRecognizer) :
    BaseTransformationController<DragGesture>(transformableNode, gestureRecognizer) {

    // Rate that the node rotates in degrees per degree of twisting.
    var rotationRateDegrees = 0.5f

    public override fun canStartTransformation(gesture: DragGesture): Boolean {
        return transformableNode.isSelected
    }

    public override fun onContinueTransformation(gesture: DragGesture) {

        var localRotation = transformableNode.localRotation

        val rotationAmountX = gesture.delta.x * rotationRateDegrees
        val rotationDeltaX = Quaternion(Vector3.up(), rotationAmountX)
        localRotation = Quaternion.multiply(localRotation, rotationDeltaX)

        // *** this only rotates on X axis. How do I rotate on all axes? ***

        transformableNode.localRotation = localRotation
    }

    public override fun onEndTransformation(gesture: DragGesture) {}
}

Solution

  • I was able to find a working solution here: https://github.com/chnouman/SceneView

    Here are the relevant snippets of code, with some of my adaptations to make it work for .glb files.

    I have forked this repo and am working on keyboard input support if anyone's interested in that.

    Rendering the object:

    private fun renderLocalObject() {
    
            skuProgressBar.setVisibility(View.VISIBLE)
            ModelRenderable.builder()
                .setSource(this,
                    RenderableSource.builder().setSource(
                        this,
                        Uri.parse(localModel),
                        RenderableSource.SourceType.GLB)/*RenderableSource.SourceType.GLTF2)*/
                        .setScale(0.25f)
                        .setRecenterMode(RenderableSource.RecenterMode.ROOT)
                        .build())
                .setRegistryId(localModel)
                .build()
                .thenAccept { modelRenderable: ModelRenderable ->
                    skuProgressBar.setVisibility(View.GONE)
                    addNodeToScene(modelRenderable)
                }
    
    

    Adding the object to the SceneView:

    private fun addNodeToScene(model: ModelRenderable) {
            if (sceneView != null) {
                val transformationSystem = makeTransformationSystem()
                var dragTransformableNode = DragTransformableNode(1f, transformationSystem)
                dragTransformableNode?.renderable = model
                sceneView.getScene().addChild(dragTransformableNode)
                dragTransformableNode?.select()
                sceneView.getScene()
                    .addOnPeekTouchListener { hitTestResult: HitTestResult?, motionEvent: MotionEvent? ->
                        transformationSystem.onTouch(
                            hitTestResult,
                            motionEvent
                        )
                    }
            }
        }
    

    Custom TransformableNode:

    class DragTransformableNode(val radius: Float, transformationSystem: TransformationSystem) :
        TransformableNode(transformationSystem) {
        val dragRotationController = DragRotationController(
            this,
            transformationSystem.dragRecognizer
        )
    }
    

    Custom TransformationController:

    class DragRotationController(
            private val transformableNode: DragTransformableNode,
            gestureRecognizer: DragGestureRecognizer
    ) :
        BaseTransformationController<DragGesture>(transformableNode, gestureRecognizer) {
    
        companion object {
    
            private const val initialLat = 26.15444376319647
            private const val initialLong = 18.995950736105442
    
            var lat: Double = initialLat
            var long: Double = initialLong
        }
    
        // Rate that the node rotates in degrees per degree of twisting.
        private var rotationRateDegrees = 0.5f
    
        public override fun canStartTransformation(gesture: DragGesture): Boolean {
            return transformableNode.isSelected
        }
    
        private fun getX(lat: Double, long: Double): Float {
            return (transformableNode.radius * Math.cos(Math.toRadians(lat)) * Math.sin(Math.toRadians(long))).toFloat()
        }
    
        private fun getY(lat: Double, long: Double): Float {
            return transformableNode.radius * Math.sin(Math.toRadians(lat)).toFloat()
        }
    
        private fun getZ(lat: Double, long: Double): Float {
            return (transformableNode.radius * Math.cos(Math.toRadians(lat)) * Math.cos(Math.toRadians(long))).toFloat()
        }
    
        override fun onActivated(node: Node?) {
            super.onActivated(node)
            Handler().postDelayed({
                transformCamera(lat, long)
            }, 0)
        }
    
        public override fun onContinueTransformation(gesture: DragGesture) {
    
            val rotationAmountY = gesture.delta.y * rotationRateDegrees
            val rotationAmountX = gesture.delta.x * rotationRateDegrees
            val deltaAngleY = rotationAmountY.toDouble()
            val deltaAngleX = rotationAmountX.toDouble()
    
            long -= deltaAngleX
            lat += deltaAngleY
    
            //lat = Math.max(Math.min(lat, 90.0), 0.0)
    
            transformCamera(lat, long)
        }
    
        private fun transformCamera(lat: Double, long: Double) {
            val camera = transformableNode.scene?.camera
    
            var rot = Quaternion.eulerAngles(Vector3(0F, 0F, 0F))
            val pos = Vector3(getX(lat, long), getY(lat, long), getZ(lat, long))
            rot = Quaternion.multiply(rot, Quaternion(Vector3.up(), (long).toFloat()))
            rot = Quaternion.multiply(rot, Quaternion(Vector3.right(), (-lat).toFloat()))
            camera?.localRotation = rot
            camera?.localPosition = pos
        }
    
        fun resetInitialState() {
            transformCamera(initialLat, initialLong)
        }
    
    
    
        public override fun onEndTransformation(gesture: DragGesture) {}
    
    
    }