Rotating around anchor point in SceneKit - ios

Objective: There is a SCNCylinder object in my scene. I want to be able to drag one end of the cylinder and rotate the cylinder in any direction, while keeping the other end in the same position.
Currently, I am calling localRotate(by: SCNQuaternion) to first rotate the node, then calculate the position offset needed to move the cylinder so that the other end can go back to its original position.
How can I achieve the objective in one step instead of what I am doing now?

The pivot property is what you're looking for. Or, since modern SceneKit often works better / makes nicer Swift / interoperates easier with ARKit when you use SIMD types, the simdPivot property.
Note this bit in the docs:
Changing the pivot transform alters these behaviors in many useful ways. You can:
Offset the node’s contents relative to its position. For example, by setting the pivot to a translation transform you can position a node containing a sphere geometry relative to where the sphere would rest on a floor instead of relative to its center.
Move the node’s axis of rotation. For example, with a translation transform you can cause a node to revolve around a faraway point instead of rotating around its center, and with a rotation transform you can tilt the axis of rotation.
Similarly, for a cylinder, you can make its pivot a transform matrix that translates the origin by half its height, giving it an "anchor point" (for position and rotation changes) at one end instead of in the center. Something like this (untested):
let cylinder = SCNCylinder(radius: /*...*/, height: /*...*/)
let cylinderNode = SCNNode(geometry: cylinder)
cylinderNode.simdPivot = float4x4(translation: cylinder.height / 2)
extension float4x4 {
init(translation vector: float3) {
self.init(float4(1, 0, 0, 0),
float4(0, 1, 0, 0),
float4(0, 0, 1, 0),
float4(vector.x, vector.y, vector.z, 1))
}
}
More generally, whenever you're using a scene-graph / transform-hierarchy based graphics framework, any time you find yourself doing math depending on one transform (rotation, translation, etc) to affect another, it's always good to check for API that can do that math for you — because doing that kind of math is what transform hierarchy is all about.
And if there's not an API fairly specific to what you need, remember that the hierarchy itself is good for making dependent transforms. For example, if you want one node to follow a circular orbit around another, you don't need to set its position using sines and cosines... just make it the child of another node, and rotate that other node.
In this case, pivot is a convenience equivalent to using the node hierarchy. You could just as well create an intermediate node and move the cylinder within it (something like this):
let cylinder = SCNCylinder(radius: /*...*/, height: /*...*/)
let cylinderNode = SCNNode(geometry: cylinder)
let offsetNode = SCNNode()
offsetNode.addChildNode(cylinderNode)
cylinderNode.simdPosition.y = cylinder.height / 2
offsetNode.position = /*...*/ // set world-space position of end of cylinder
offsetNode.eulerAngles.x = /*...*/ // rotate cylinder around its end

Related

Trouble implementing shadows in WebGL

I am trying to implement shadows into my WEBGL 2.0 Project using this tutorial
https://webgl2fundamentals.org/webgl/lessons/webgl-shadows.html
Currently I am getting really bad results like this:
Basically a ton of the terrain is being drawn in shadow that shouldn't be. The light projection is from your camera towards the direction you are looking so hypothetically you shouldn't be able to see any shdaows becuase the light projection is the same as your camera ( I am just doing this for testing until I can get this working properly)
I have everything the same as the tutorial I believe except I am using glMatrix instead of their matrix math library (shouldn't matter I would assume). Here's the thing though. I don't use a model view matrix for anything I am rendering so none of my points are on a -1,1 range. They can go out as far as -3200...ect Its just all one big terrain mesh chunked out.
I think the issue lies with how I am creating the texture matrix
textureMatrix = glMatrix.mat4.create();
glMatrix.mat4.translate(textureMatrix,textureMatrix,[0.5,0.5,0.5]);
glMatrix.mat4.scale(textureMatrix,textureMatrix,[0.5,0.5,0.5]);
glMatrix.mat4.multiply(textureMatrix,textureMatrix, projectionMatrix);
glMatrix.mat4.invert(lightMatrix,lightMatrix);
glMatrix.mat4.multiply(textureMatrix,textureMatrix, lightMatrix);
I am using the same matrix for the light projection as your normal projection, is that an issue? if anyone could help it would be greatly appreciated.
That's probably because the Y position of your light (in your example, it is much more the distance between the eye and the scene) is too big for the Z size of your shadow volume (the size of your shadow volume in the view direction.) Here if posY is inside the wireframe box :
But if you increase posY too much (i.e. your shapes get out of the shadow volume, they disappear
So you should increase the size of your shadow volume (or shrinken your scene, either way.) You cannot simulate that with the slider because they just give you the control to the two dimensions X and Y dimensions : projWidth and projHeight.
i.e. in the last code in your tutorial page, the latest parameter ("far") for example change it from 10 to 100
const lightProjectionMatrix = settings.perspective
? m4.perspective(
degToRad(settings.fieldOfView),
settings.projWidth / settings.projHeight,
0.5, // near
10) // far
: m4.orthographic(
-settings.projWidth / 2, // left
settings.projWidth / 2, // right
-settings.projHeight / 2, // bottom
settings.projHeight / 2, // top
0.5, // near
100); // far
Then you can increase posY far more :
without having your full code, it is hard to reproduce and help you. Could you not try to just inject your scene into the tutorial code ? You can bind the viewpoint with the source and orientation of the light by using the same inputs : (just adding 0.5 to X to see a bit of shadow and make sure it is properly computed.)
/*const cameraPosition = [settings.cameraX, settings.cameraY, 15];*/
const cameraPosition = [settings.posX+0.5, settings.posY, settings.posZ];
/*const target = [0, 0, 0]; */
const target = [settings.targetX, settings.targetY, settings.targetZ];

How to scale 3D object vertically (along y-axis) in Swift?

I created a door with ARKit and I want create a scale animation. My goal is to scale it only along the y-axis (stretch the door to be longer).
I want the door to grow within a duration of 1 second.
My approach was to simply scale it but I only have options that allow me to scale my entire object along all 3 axis.
Next I tried node.scale = SCNVector3(0, 2, 0) and that works ok, but it has no nice animation to it. When I create a SCNAction() and run the code as a block with duration time, it still just changes the size without any smooth animation.
You need to use SCNTransaction. The simplest way to animate your node scaling would be something like this:
SCNTransaction.begin()
SCNTransaction.animationDuration = 3
node.scale = SCNVector3(0, 2, 0)
SCNTransaction.commit()
You might also need to modify pivot property of the node to position the animation correctly.

Polar transformation in iOS

Can anyone help me to achieve this kind of animated transformation through Core Graphics. Look at the rough sketch:
This is a simple chart graphic, and I need to transform a histogramm-style bar (left shape) to a pie chart (right shape).
Literally the task is to roll a rectangle to a ring with a smooth animation. I almost found the way to do this with a tricky queue of drawings, mask clippings and affine transformations but this won't look exactly how I want it to look.
This is an interesting challenge, especially as you want to maintain the different segments. I won't give you a full answer (i.e full example code to achieving this), but I will explain what I think needs to be done to achieve the effect that you want.
Paths
First, I see both of these diagrams as a single line that is stroked (let's ignore the segments for a moment), so the challenge is going from a straight line to an enclosed circle.
I propose the following two paths, that you can animate between to achieve a nice wrapping effect.
// As we render a circle as a chain of straight line segments
// changing the count of said segments makes the circle more or less smooth
// Try this with other values, such as 8 or 32
let segments = 72
// With this segment count, the angle of each segment in the circle is
let angle = (2 * CGFloat(M_PI)) / CGFloat(segments)
// First path (straight)
let length = CGFloat(300.0)
let segmentLength = length / CGFloat(segments)
let straightPath = CGPathCreateMutable()
CGPathMoveToPoint(straightPath, nil, 0.0, 0.0)
for i in 0...segments {
CGPathAddLineToPoint(straightPath, nil, 0.0, CGFloat(i) * segmentLength)
}
// Second path (circle)
let radius = CGFloat(100.0)
let center = CGPoint(x: 104.0, y: 104.0)
let space = (x: 2.0, y: 2.0)
var circlePath = CGPathCreateMutable()
CGPathMoveToPoint(circlePath, nil, center.x + radius, center.y)
for i in 0...segments {
let x = cos(-CGFloat(i) * angle)
let y = sin(-CGFloat(i) * angle)
CGPathAddLineToPoint(circlePath, nil, center.x + radius * x, center.y + radius * y)
}
I have also uploaded a Swift plaground for you to experiment with, which you can find here
Segments
Now, handling the segments can be a bit tricky, however I propose a relatively naive implementation that might work. Mainly, CAShapeLayer has the following two properties - strokeStart and strokeEnd, which allow controlling the part of the path that is actually stroked.
With this in mind, you could create as many layers as there are segments, assign them all the same path(s) and tweak their respective strokeStart and strokeEnd properties to make it look the way you expect. Somewhat similar to what they do in this post.
Animation
Assuming you have conquered the previous two aspects, the animation aspect should be relatively straight forward, using the two types of paths you have, you can create a simple CABasicAnimation that goes from one to another. I will assume you are familiar with CoreAnimation and its usage (i.e how to properly change model values to match those that are presented etc.).
I will end my answer with a quick animation showing what the end result could look like (minus the segments), I have frozen the animation and am manipulating the timeOffset property of the layer to manually scrub through it.
I hope my answer helps you get closer to the solution you want. It is also important to emphasise that my code examples are just a beginning, you will likely need to tweak the values quite a bit to achieve a satisfying animation (for example, the length of the line should be similar to that of the circumference of the circle).
Skewing, twisting and bending are none trivial transformations on bodies.
These can't be done Core Graphics.
Better draw the chart yourself with CGContextAddArcToPoint in core graphic and mask out the inner circle.
The other (hardcore) way would be using a 3d engine - i.e. scene kit - and apply your chart as texture to it.

Convert Scene's (x, y) to screen's (x, y)

I have an application built with SceneKit that is currently displaying several nodes. I can figure out which node is pressed and want to use that to make a label appear below the Node that was touched. Now, when I set the label's center the following way...
nameLabel.center = CGPointMake(CGFloat(result.node.position.x), CGFloat(result.node.position.y+20)
…it appears in the upper left corner since the node is on (1, 0). What I figured is that the sceneView's (0, 0) is in the center of the screen while the actual physical display's (0, 0) is in the top left corner.
Is there a way to convert the two different numbers into each other? (I could hardcode since I know where the Node's are or create a separate label for each Node but that is not really a perfect solution.)
Thanks in advance :)
You can use the projectPoint: method:
var projected = view.projectPoint(result.node.position))
//projected is an SCNVector3
//projected.x and y are the node's position in screen coordinates
//projected.z is its depth relative to the near and far clipping planes
nameLabel.center = CGPointMake(CGFloat(projected.x), CGFloat(projected.y+20)

CATransform3D - understanding the transform values

The picture shows a simple UIView after applying the following transform:
- (CATransform3D) transformForOpenedMenu
{
CATransform3D transform = CATransform3DIdentity;
transform.m34 = -1.0 /450;
transform = CATransform3DRotate(transform, D2R(40), 0, 1, 0);
transform = CATransform3DTranslate(transform, 210, 150, -500);
return transform;
}
I'm trying to make the distances highlighted with black to have equal length. Could you please help me understand the logic behind the values and calculations?
Cheers
UPD Sept 13
Looks like removing 3DTranslate keeps distances equal. I see I can use layer's frame property to reposition rotated view to the bottom left of the screen. Not yet sure, but this might actually work.
The .m34 value you are setting is best set on the sublayerTransform of the containing view rather than the view you are transforming.
I don't fully understand the maths behind affine transforms so I made this project which allows me to play around with the transform values to achieve the effect I want. You can plug in the values from your code above and see what it looks like, though note that there is already a perspective value applied using the sublayerTransform property mentioned above.
For your specific case, I think you want to adjust the anchor point of the layer to (0.0,0.5) and apply the rotation transform only. This assumes you want the menu to swing back like a door, with the hinges on the left edge.
The problem you're seeing is caused by your CATransform3DTranslate call. You're essentially setting the Y Axis off center, and hence seeing a different perspective view of the frame.
Think of it this way;
You're standing in the center of a long narrow field stretching off into the horizon. The edge of the field appears as if it is converges to a center point somewhere off in the distance. The angle of each edge to the converging point will appear equal if you are at the center of the field. If, on the other hand, you move either to the left or the right, the angles change and one will seem greater than the other (inversely opposite of course).
This is essentially what is happening with your view; As your converging points are to the right, changing the Y axis away from 0 will have the same effect as moving to the left or right in my example above. You're no longer looking at the parallel lines from the center.
so in your code above Setting the ty in CATransform3DTranslate to 0 Should fix your problem I.E.
transform = CATransform3DTranslate(transform, 210, 0, -500);
You may also need to alter the tz and tx Value to make it fit.
OK, so what eventually solved my question is this:
3D transform on Y axis to swing the view like a door transform = CATransform3DRotate(transform, D2R(40), 0, 1, 0);
set Z anchor point on a layer, to move it back targetView.layer.anchorPointZ = 850;
adjust layer position so that the view is located slightly to the bottom left of the parent view:
newPosition.x += 135 * positionDirection;
newPosition.y += 70 * positionDirection;
This sequence adjusts position without CATransform3DTranslate and keeps the 'swinged' effect not distorted.
Thanks everybody!

Resources