Total height of SCNNode childNodes - ios

I'm currently using the following to get the total height of all of the child nodes in a SCNNode. Is there a more efficient/better/shorter/more swift-like way to do this?
CGFloat(columnNode.childNodes.reduce(CGFloat()) {
let geometry = $1.geometry! as SCNBox
return $0 + geometry.height
})

Yes, and a way that'll get you a more correct answer, too. Summing the height of all the child nodes' geometries...
only works if the geometry is an SCNBox
doesn't account for the child nodes' transforms (what if they're moved, rotated or scaled?)
doesn't account for the parent node's transform (what if you want height in scene space?)
What you want is the SCNBoundingVolume protocol, which applies to all nodes and geometries, and describes the smallest rectangular or spherical space containing all of a node's (and its subnodes') content.
In Swift 3, this is super easy:
let (min, max) = columnNode.boundingBox
After this, min and max are the coordinates of the lower-rear-left and upper-front-right corners of the smallest possible box containing everything inside columnNode, no matter how that content is arranged (and what kind of geometry is involved). These coordinates are expressed in the same system as columnNode.position, so if the "height" you're looking for is in the y-axis direction of that space, just subtract the y-coordinates of those two vectors:
let height = max.y - min.y
In Swift 2, the syntax for it is a little weird, but it works well enough:
var min = SCNVector3Zero
var max = SCNVector3Zero
columnNode.getBoundingBoxMin(&min, max: &max)

Related

Align a SCNNode along an axis by rotating its parent

Using Apple's RoomPlan API, I managed to create a series of nodes representing the walls of a room. However, after applying their transform, the yaw (eulerAngles.y) ends up being a random number which makes aligning other nodes not generated as part of the room tricky.
My thought was to add all the walls as children to a parent node, then rotate the parent until the largest wall had a eulerAngle.y of 90. However, I am having trouble getting it to work right.
Code for generating the walls from a RoomPlan CapturedRoom
for scannedWall in roomScan.walls {
//Generate new wall geometry
let length = 0.01
let width = scannedWall.dimensions.x
let height = scannedWall.dimensions.y
//Generate new SCNNode
let newWallGeometry = SCNBox(
width: CGFloat(width),
height: CGFloat(height),
length: CGFloat(length),
chamferRadius: 0
)
newWallGeometry.firstMaterial?.transparency = 0.6
let newWall = SCNNode(geometry: newWallGeometry)
newWall.simdTransform = scannedWall.transform
parentNode.addChildNode(newWall)
}
My initial thought was just to rotate the parent node by the difference between the longest wall's rotation and 90 degrees, but that does not seem to be correct:
parentNode.eulerAngles.y = (.pi/2 - longestWall.eulerAngles.y)
Any help would be greatly appreciated, I'm fairly new to swift/scenekit and am banging my head against a wall

iOS - How to resize elements on a screen depending on the amount of the elements

So I am developing a game using Spritekit that uses a pyramid of Sprites (let's say circles for a simple instance). The user can choose the amount of rows of sprites they would like to have in the game. The sprites are to form a pyramid, so if you have 1 row, you have 1 sprite node. It increases by 2 the farther down you go (the more rows you choose) - creating the pyramid shape. So if a user picked 3 rows, the game board would look like this:
O
O O O
O O O O O
However, when it gets to 5 rows, it loses its pyramid shape because the screen is only so wide and it has to fit all the elements onto the screen (elements are more smushed together in rows further down).
My question is, to fix this issue, what would I have to do to make the pyramid resize and change its spacing between elements depending on how many rows are chosen? Would I have to multiply the spacing by a certain factor? I have also heard of people adding layers onto the screen - maybe drawing the sprites in some sort of container so that it always resizes the pyramid to fit the screen without skewing the pyramid shape?
Your idea is correct! Make a SKNode container, then update it's .size property, or do .setScale.
(not at xcode right now, pardon if not 100%)
// Say that our scene's size is 400x400:
let bkg = SKShapeNode(rectangleOfSize: self.size)
bkg.addChild(firstSprite)
bkg.addChild(secondSprite) // And so on...
// Find the farthest point in bkg:
var farthestX = CGFloat(0)
for node in bkg.children {
if node.position.x + node.frame.size.width / 2 > farthestX {
farthestX = node.position.x + node.frame.size.width / 2
}
}
// Do math to resize the bkg:
if self.size.width < farthestX {
let scaler = self.size.width / farthestX
bkg.setScale(scaler)
}
This should work, or at least the general idea should work... You would want to check for Y values and Height as well.
You can easily compute a symmetrical size proportional to the number of rows and resize your sprites accordingly. This is my idea in pseudocode:
let computedSize = deviceWidth/(2*(rows-1) + 1)
for sprite in sprites {
sprite.size.width = computedSize
sprite.size.height = computedSize
}

Swift: How to convert CGPoints from SpriteKit to CGRect?

In SpriteKit, I can use touch locatons to record "Hits" in a target, where center of the target, "bulls eye" have the coordinates (0,0). After plenty of shooting, I will fetch all hits as an array with CGPoints. Since the target is 500 x 500 points (SKScene, sks-file), all hits can have a x position from -250 to +250 and likewise for y position.
In the attatched photo, the hits are registered as points at around (150, 150).
The problem arises when I will use the famous LFHeatMap https://github.com/gpolak/LFHeatMap.
+ (UIImage *)heatMapWithRect:(CGRect)rect
boost:(float)boost
points:(NSArray *)points
weights:(NSArray *)weights;
The LFHeatMap generates a UIImage based on the array, which I add to a UIImageView. The problem is that the UIViews has the x and y values arranged differently from SKScenes
func setHeatMap() {
let points = getPointsFromCoreData()
let weigths = getWeightsFromCoreData()
var rect = CGRectMake(0, 0, 500, 500)
rect.origin = CGPointMake(-250, -250)
let image =
LFHeatMap.heatMapWithRect(rect, boost: 1, points: points, weights: weights)
heatMapView.contentMode = UIViewContentMode.ScaleAspectFit
heatMapView.image = image
}
Lowering the shots makes the heat move higher.
How can I solve this? Either All points have to be converted to fit another coordinate system, or the coordiate of the CGrect making the heatmap, must be changed. How can this be done?
This was embarrasingly easy when the solution first occured.
Run a loop trough the points array, and multiply the point.y with -1...
Then all the valus on the y-axis is correct.

SceneKit – Get Node's height [duplicate]

This question already has an answer here:
Total height of SCNNode childNodes
(1 answer)
Closed 7 years ago.
I have a SceneKit project in which I currently have 2 objects, a ground which is a simple plane and a pyramid. I want to set the height of the pyramid node to something like the following sudo code:
pyr.position = SCNVector3Make(0, ground.position.y+pyr.size.height, 0)
Is there a way to get the height of nodes in pixels or points?
NOTE
This is how I created the pyr & ground nodes:
let pyr = scene.rootNode.childNodeWithName("pyramid", recursively: true)!
let ground = scene.rootNode.childNodeWithName("ground", recursively: true)!
Have you tried this?
From the docs:
The SCNBoundingVolume protocol defines an interface for describing objects that occupy a volume in space, adopted by the SCNNode and SCNGeometry classes. Its methods measure the location and size of an object in the object’s local coordinate space, expressed as either a box or a sphere.
Gets the minimum and maximum corner points of the object’s bounding box.
func getBoundingBoxMin(_ min: UnsafeMutablePointer<SCNVector3>,
max max: UnsafeMutablePointer<SCNVector3>) -> Bool
min
On output, the minimum coordinates of the bounding box.
max
On output, the maximum coordinates of the bounding box.
(max.y - min.y) would be the height.

SceneKit applyTorque

I am trying to applyTorque to a node in my scene. The documentation states:
Each component of the torque vector relates to rotation about the
corresponding axis in the local coordinate system of the SCNNode
object containing the physics body. For example, applying a torque of
{0.0, 0.0, 1.0} causes a node to spin counterclockwise around its
z-axis.
However in my tests it seems that Physics animations do not affect actual position of the object. Therefore, the axis remain static (even though the actual node obviously moves). This results in the torque always being applied from the same direction (wherever the z axes was when the scene was initiated).
I would like to be able to apply torque so that it is always constant in relation to the object (e.g. to cause node to spin counterclockwise around z-axis of the node's presentationNode not the position node had(has?) when the scene was initiated)
SceneKit uses two versions of each node: the model node defines static behavior and the presentation node is what's actually involved in dynamic behavior and used on screen. This division mirrors that used in Core Animation, and enables features like implicit animation (where you can do things like set node.position and have it animate to the new value, without other parts of your code that query node.position having to working about intermediate values during the animation).
Physics operates on the presentation node, but in some cases--like this one--takes input in scene space.
However, the only difference between the presentation node and the scene is in terms of coordinate spaces, so all you need to do is convert your vector from presentation space to scene space. (The root node of the scene shouldn't be getting transformed by physics, actions, or inflight animations, so there's no practical difference between model-scene space and presentation-scene space.) To do that, use one of the coordinate conversion methods SceneKit provides, such as convertPosition:fromNode:.
Here's a Swift playground that illustrates your dilemma:
import Cocoa
import SceneKit
import XCPlayground
// Set up a scene for our tests
let scene = SCNScene()
let view = SCNView(frame: NSRect(x: 0, y: 0, width: 500, height: 500))
view.autoenablesDefaultLighting = true
view.scene = scene
let cameraNode = SCNNode()
cameraNode.camera = SCNCamera()
cameraNode.position = SCNVector3(x: 0, y: 0, z: 5)
scene.rootNode.addChildNode(cameraNode)
XCPShowView("view", view)
// Make a pyramid to test on
let node = SCNNode(geometry: SCNPyramid(width: 1, height: 1, length: 1))
scene.rootNode.addChildNode(node)
node.physicsBody = SCNPhysicsBody.dynamicBody()
scene.physicsWorld.gravity = SCNVector3Zero // Don't fall off screen
// Rotate around the axis that looks into the screen
node.physicsBody?.applyTorque(SCNVector4(x: 0, y: 0, z: 1, w: 0.1), impulse: true)
// Wait a bit, then try to rotate around the y-axis
node.runAction(SCNAction.waitForDuration(10), completionHandler: {
var axis = SCNVector3(x: 0, y: 1, z: 0)
node.physicsBody?.applyTorque(SCNVector4(x: axis.x, y: axis.y, z: axis.z, w: 1), impulse: true)
})
The second rotation effectively spins the pyramid around the screen's y-axis, not the pyramid's y-axis -- the one that goes through the apex of the pyramid. As you noted, it's spinning around what was the pyramid's y-axis as of before the first rotation; i.e. the y-axis of the scene (which is unaffected by physics), not that of the presentation node (that was rotated through physics).
To fix it, insert the following line (after the one that starts with var axis):
axis = scene.rootNode.convertPosition(axis, fromNode: node.presentationNode())
The call to convertPosition:fromNode: says "give me a vector in scene coordinate space that's equivalent to this one in presentation-node space". When you apply a torque around the converted axis, it effectively converts back to the presentation node's space to simulate physics, so you see it spin around the axis you want.
Update: Had some coordinate spaces wrong, but the end result is pretty much the same.
Unfortunately the solution provided by rickster does not work for me :(
Trying to solve this conundrum I have created (what i believe to be) a very sub-standard solution (more a proof of concept). It involves creating (null) objects on the axis i am trying to find, then I use their position to find the vector aligned to the axes.
As I have a fairly complex scene, I am loading it from a COLLADA file. Within that file i have modelled a simple coordinate tripod: three orthogonal cylinders with cones on top (makes it easer to visualise what is going on).
I then constrain this tripod object to the object I am trying to apply torque to. This way I have objects that allow me to retrieve two points on the axes of the presentationNode of the object I am trying to apply torque to. I can then use these two points to determine the vector to apply the torque from.
// calculate orientation vector in the most unimaginative way possible
// retrieve axis tripod objects. We will be using these as guide objects.
// The tripod is constructed as a cylinder called "Xaxis" with a cone at the top.
// All loaded from an external COLLADA file.
SCNNode *XaxisRoot = [scene.rootNode childNodeWithName:#"XAxis" recursively:YES];
SCNNode *XaxisTip = [XaxisRoot childNodeWithName:#"Cone" recursively:NO];
// To devise the vector we will need two points. One is the root of our tripod,
// the other is at the tip. First, we get their positions. As they are constrained
// to the _rotatingNode, presentationNode.position is always the same .position
// because presentationNode returns position in relation to the parent node.
SCNVector3 XaxisRootPos = XaxisRoot.position;
SCNVector3 XaxisTipPos = XaxisTip.position;
// We then convert these two points into _rotatingNode coordinate space. This is
// the coordinate space applyTorque seems to be using.
XaxisRootPos = [_rotatingNode convertPosition:XaxisRootPos fromNode:_rotatingNode.presentationNode];
XaxisTipPos = [_rotatingNode convertPosition:XaxisTipPos fromNode:_rotatingNode.presentationNode];
// Now, we have two *points* in _rotatingNode coordinate space. One is at the center
// of our _rotatingNode, the other is somewhere along it's Xaxis. Subtracting them
// will give us the *vector* aligned to the x axis of our _rotatingNode
GLKVector3 rawXRotationAxes = GLKVector3Subtract(SCNVector3ToGLKVector3(XaxisRootPos), SCNVector3ToGLKVector3(XaxisTipPos));
// we now normalise this vector
GLKVector3 normalisedXRotationAxes = GLKVector3Normalize(rawXRotationAxes);
//finally we are able to apply toque reliably
[_rotatingNode.physicsBody applyTorque:SCNVector4Make(normalisedXRotationAxis.x,normalisedXRotationAxis.y,normalisedXRotationAxis.z, 500) impulse:YES];
As you can probably see, I am quite inexperienced in SceneKit, but even I can see that much easier/optimised solution does exits, but I am unable to find it :(
I recently had this same problem, of how to convert a torque from the local space of the object to the world space required by the applyTorque method. The problem with using the node's convertPosition:toNode and fromNodes methods, is that they are also applying the node's translation to the torque, so this will only work when the node is at 0,0,0. What these methods do is treat the SCNVector3 as if it's a vec4 with a w component of 1.0. We just want to apply the rotation, in other words, we want the w component of the vec4 to be 0. Unlike SceneKit, GLKit gives us 2 options for how we want our vec3s to be multiplied:
GLKMatrix4MultiplyVector3 where
The input vector is treated as it were a 4-component vector with a w-component of 0.0.
and GLKMatrix4MultiplyVector3WithTranslation where
The input vector is treated as it were a 4-component vector with a w-component of 1.0.
What we want here is the former, just the rotation, not the translation.
So, we could roundtrip to GLKit. To convert for instance the local x axis (1,0,0), eg a pitch rotation, to the global axis needed for apply torque, would look like this:
let local = GLKMatrix4MultiplyVector3(SCNMatrix4ToGLKMatrix4(node.presentationNode.worldTransform), GLKVector3(v: (1,0,0)))
node.physicsBody?.applyTorque(SCNVector4(local.x, local.y, local.z, 10), impulse: false)
However, a more Swiftian approach would be to add a * operator for mat4 * vec3 which treats the vec3 like a vec4 with a 0.0 w component. Like this:
func * (left: SCNMatrix4, right: SCNVector3) -> SCNVector3 { //multiply mat4 by vec3 as if w is 0.0
return SCNVector3(
left.m11 * right.x + left.m21 * right.y + left.m31 * right.z,
left.m12 * right.x + left.m22 * right.y + left.m32 * right.z,
left.m13 * right.x + left.m23 * right.y + left.m33 * right.z
)
}
Although this operator makes an assumption about how we want our vec3s to be multiplied, my reasoning here is that as the convertPosition methods already treat w as 1, it would be redundant to have a * operator that also did this.
You could also add a mat4 * SCNVector4 operator that would let the user explicity choose whether or not they want w to be 0 or 1.
So, instead of having to roundtrip from SceneKit to GLKit, we can just write:
let local = node.presentationNode.worldTransform * SCNVector3(1,0,0)
node.physicsBody?.applyTorque(SCNVector4(local.x, local.y, local.z, 10), impulse: false)
You can use this method to apply rotation on multiple axes with one applyTorque call. So say if you have stick input where you want x on the stick to be yaw (local yUp-axis) and y on the stick to be pitch (local x-axis), but with flight-sim style "down to pull back/ up", then you could set it to SCNVector3(input.y, -input.x, 0)

Resources