'Why SCNPhysicsBody resets position when set eulerAngles?

I'm trying to use SceneKit to develop a game for tvOS, and I'm having an issue. When I set the node's eulerAngle before apply an impulse to the physicsBody the node is reset to his original position.

I was expecting to see the nodes moving around on the floor's plane, but on each tap the nodes are moved to the origin position before the impulse is applied.

I'm new at the use of this framework, so I wonder where is the mistake. I'm using the new AppleTV with tvOS 9.0 and XCode 7.1.1

To reproduce it, you can create a new xcode project (Game for tvOS) and replace the GameViewController.m with this code:

#import "GameViewController.h"

SCNNode *ship;
SCNNode *node;
SCNNode *ground;

@implementation GameViewController

- (void)viewDidLoad
{
    [super viewDidLoad];

    // create a new scene
    SCNScene *scene = [[SCNScene alloc] init];
    scene.physicsWorld.gravity = SCNVector3Make(0, -800, 0);

    // create and add a camera to the scene
    SCNNode *cameraNode    = [SCNNode node];
    cameraNode.camera      = [SCNCamera camera];
    cameraNode.camera.zFar = 10000;
    [scene.rootNode addChildNode:cameraNode];

    // place the camera
    cameraNode.position = SCNVector3Make(0, 64, 64);

    // create and add a light to the scene
    SCNNode *lightNode   = [SCNNode node];
    lightNode.light      = [SCNLight light];
    lightNode.light.type = SCNLightTypeOmni;
    lightNode.position   = SCNVector3Make(0, 10, 10);
    [scene.rootNode addChildNode:lightNode];

    // create and add an ambient light to the scene
    SCNNode *ambientLightNode    = [SCNNode node];
    ambientLightNode.light       = [SCNLight light];
    ambientLightNode.light.type  = SCNLightTypeAmbient;
    ambientLightNode.light.color = [UIColor darkGrayColor];
    [scene.rootNode addChildNode:ambientLightNode];

    SCNGeometry *geometry;
    SCNMaterial *material;
    SCNNode *tempNode;
    SCNPhysicsShape* shape;
    SCNPhysicsBody* body;

    //--
    SCNScene *loaded = [SCNScene sceneNamed:@"art.scnassets/ship.scn"];
    tempNode = [loaded.rootNode childNodeWithName:@"ship" recursively:YES];

    geometry = [SCNCylinder cylinderWithRadius:16 height:8];
    shape    = [SCNPhysicsShape shapeWithGeometry:geometry options:nil];

    tempNode.physicsBody = [SCNPhysicsBody bodyWithType:SCNPhysicsBodyTypeDynamic shape:shape];
    tempNode.physicsBody.restitution      = 1;
    tempNode.physicsBody.friction         = 0.25;
    tempNode.physicsBody.categoryBitMask  = 2;
    tempNode.physicsBody.collisionBitMask = 1;
    tempNode.position = SCNVector3Make(32, 32, 0);
    [scene.rootNode addChildNode:tempNode];
    ship = tempNode;

    //--
    geometry = [SCNCylinder cylinderWithRadius:16 height:8];

    material = [[SCNMaterial alloc] init];
    material.diffuse.contents = UIColor.yellowColor;
    geometry.materials        = @[material];

    shape = [SCNPhysicsShape shapeWithGeometry:geometry options:nil];
    body  = [SCNPhysicsBody bodyWithType:SCNPhysicsBodyTypeDynamic shape:shape];

    tempNode = [SCNNode nodeWithGeometry: geometry];
    tempNode.physicsBody                  = body;
    tempNode.physicsBody.restitution      = 1;
    tempNode.physicsBody.friction         = 0.25;
    tempNode.physicsBody.categoryBitMask  = 2;
    tempNode.physicsBody.collisionBitMask = 1;
    tempNode.position = SCNVector3Make(0, 32, 0);
    [scene.rootNode addChildNode:tempNode];
    node = tempNode;

    //--
    geometry = [[SCNFloor alloc] init];

    material = [[SCNMaterial alloc] init];
    material.diffuse.contents = UIColor.blueColor;
    geometry.materials        = @[material];

    shape = [SCNPhysicsShape shapeWithGeometry:geometry options:nil];
    body  = [SCNPhysicsBody bodyWithType:SCNPhysicsBodyTypeKinematic shape:shape];

    tempNode = [SCNNode nodeWithGeometry: geometry];
    tempNode.physicsBody = body;
    tempNode.physicsBody.categoryBitMask = 1;
    [scene.rootNode addChildNode:tempNode];
    ground = tempNode;

    //--
    SCNLookAtConstraint * constraint = [SCNLookAtConstraint lookAtConstraintWithTarget: ground];
    constraint.gimbalLockEnabled = YES;
    cameraNode.constraints = @[constraint];

    // configure the SCNView
    SCNView *scnView = (SCNView *)self.view;
    scnView.scene = scene;
    scnView.allowsCameraControl = NO;
    //scnView.antialiasingMode = SCNAntialiasingModeMultisampling2X;
    scnView.debugOptions = SCNDebugOptionShowPhysicsShapes;
    scnView.showsStatistics = YES;
    scnView.backgroundColor = [UIColor blackColor];

    // add a tap gesture recognizer
    UITapGestureRecognizer *tapGesture = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleTap:)];
    NSMutableArray *gestureRecognizers = [NSMutableArray array];
    [gestureRecognizers addObject:tapGesture];
    [gestureRecognizers addObjectsFromArray:scnView.gestureRecognizers];
    scnView.gestureRecognizers = gestureRecognizers;
}

- (void) handleTap:(UIGestureRecognizer*)gestureRecognize
{
    float x = (rand() / (float)RAND_MAX) - 0.5f;
    float y = (rand() / (float)RAND_MAX) - 0.5f;
    float speed = (rand() / (float)RAND_MAX) * 300;

    CGPoint velocity = CGPointMake(x, y);
    float angle = [self AngleBetween:velocity And:CGPointMake(1, 0)] + M_PI_2;

    [node.physicsBody applyForce:SCNVector3Make(velocity.x*speed, 0, velocity.y*speed) impulse:YES];
    [ship.physicsBody applyForce:SCNVector3Make(velocity.x*speed, 0, velocity.y*speed) impulse:YES];

    // if comment these lines the problem doesn't appears
    node.eulerAngles = SCNVector3Make(0, angle, 0);
    ship.eulerAngles = SCNVector3Make(0, angle, 0);
}

- (void)didReceiveMemoryWarning
{
    [super didReceiveMemoryWarning];
}

- (float) AngleBetween:(CGPoint)_origin And:(CGPoint)_destination
{
    float dotProduct = (_origin.x * _destination.x) + (_origin.y * _destination.y);
    float perpDotProduct = (_origin.x * _destination.y) - (_origin.y * _destination.x);

    return -atan2f(-perpDotProduct, dotProduct);
}

@end

If you comment the lines where the euler angles are set (at handleTap method) the problem doesn't appears.



Solution 1:[1]

From Apple's SCNPhysicsBody documentation:

If you change the transform value—or any of the other properties that are components of the transform, such as position and rotation—of a node affected by physics, SceneKit resets the physics simulation for that node.

The physics simulated values are in the presentationNode property. So in your case the way to go is to copy the presentation node's position before applying the transformation, and then write it back:

SCNVector3 nodePosition = [[node presentationNode] position];
SCNVector3 shipPosition = [[ship presentationNode] position];

[node.physicsBody applyForce:SCNVector3Make(velocity.x*speed, 0, velocity.y*speed) impulse:YES];
[ship.physicsBody applyForce:SCNVector3Make(velocity.x*speed, 0, velocity.y*speed) impulse:YES];

// if comment these lines the problem doesn't appears
node.eulerAngles = SCNVector3Make(0, angle, 0);
ship.eulerAngles = SCNVector3Make(0, angle, 0);

[node setPosition: nodePosition];
[ship setPosition: shipPosition];

Solution 2:[2]

I found an answer for my problem.

based on this answer: https://stackoverflow.com/a/28921103/2845875

I realized that I must remove the nodes from the scene tree before apply the transformations, and then reinsert them.

To follow with my example, here is the code:

SCNNode *root = node.parentNode;
[node removeFromParentNode];
[ship removeFromParentNode];

[node.physicsBody applyForce:SCNVector3Make(velocity.x*speed, 0, velocity.y*speed) impulse:YES];
[ship.physicsBody applyForce:SCNVector3Make(velocity.x*speed, 0, velocity.y*speed) impulse:YES];

node.eulerAngles = SCNVector3Make(0, angle, 0);
ship.eulerAngles = SCNVector3Make(0, angle, 0);

[root addChildNode:node];
[root addChildNode:ship];

Solution 3:[3]

Bon, 6 years later... iOS 15, Swift 5.x doing animation and physics on the same node is still painful it seems.

This is the swift version of the previous answer. Note I am not trying to animate in every frame this renders because from a performance point of view it [a 2020 M1 MBP] it was killing my machine if I did. [Surely the loops looking for children the problem].

"node" is a child of "parentNode". The physics is changing the "parentNode". In the meantime I am rotating the child "node".

@MainActor class SceneDelegate: NSObject, SCNSceneRendererDelegate {
var share = Common.shared
func renderer(_ renderer: SCNSceneRenderer, updateAtTime time: TimeInterval) {
    
    if time > spawnTime2 && !share.landed {
        renderer.scene?.rootNode.childNodes.filter({ $0.name == "coreNode" }).forEach({ parentNode in
            share.updateVelocity(figure: (parentNode.physicsBody?.velocity.y)!)
        })
    }
    
    if time > spawnTime && !share.landed {
        print("spawn")
        renderer.scene?.rootNode.childNodes.filter({ $0.name == "coreNode" }).forEach({ parentNode in
            let savePos = parentNode.presentation.position

            parentNode.childNodes.filter({ $0.name == "shipNode" }).forEach({ node in
                let quaternion = simd_quatf(angle: GLKMathDegreesToRadians(1), axis: simd_float3(0,1,0))
                node.simdOrientation = quaternion * node.simdOrientation
                

// node.eulerAngles = SCNVector3(x: 0, y:GLKMathDegreesToRadians(rotater) , z:0) rotater += 1 })

            parentNode.position = savePos
           
        })
    }
}

func renderer(_ renderer: SCNSceneRenderer, didRenderScene scene: SCNScene, atTime time: TimeInterval) {
    spawnTime = time + TimeInterval(0.015)
    spawnTime2 = time + TimeInterval(0.018)
}
}

Both quaternion and Euler rotations work, which I why I left the code in place.

The spawnTime speeds are a fudged figures that worked for me. Too fast and it jumps, too slow and it doesn't work at all.

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 Ramy Al Zuhouri
Solution 2 Community
Solution 3