Align a node with its neighbor in SceneKit - swift

Using Swift 5.5, iOS 14
Trying to create a simple 3D bar chart and I immediately find myself in trouble.
I wrote this code...
var virgin = true
for i in stride(from: 0, to: 6, by: 0.5) {
let rnd = CGFloat.random(in: 1.0...4.0)
let targetGeometry = SCNBox(width: 0.5,
height: rnd,
length: 0.5,
chamferRadius: 0.2)
targetGeometry.firstMaterial?.fillMode = .lines
targetGeometry.firstMaterial?.diffuse.contents = UIColor.blue
let box = SCNNode(geometry: targetGeometry)
box.simdPosition = SIMD3(x: 0, y: 0, z: 0)
coreNode.addChildNode(box)
}
This works well, but all the bars a centred around their centre. But how can I ask SceneKit to change the alignment?
Almost got this working with this code...
box.simdPosition = SIMD3(x: Float(i) - 3,
y: Float(rnd * 0.5),
z: 0)
But the result isn't right... I want/need the bar to grow from the base.
https://youtu.be/KJgvdBFBfyc
How can I make this grow from the base?
--Updated with working solution--
Tried Andy Jazz suggestion replacing simdposition with the following formulae.
box.simdPosition = SIMD3(x:box.simdPosition.x, y:0, z:0)
box.simdPivot.columns.3.y = box.boundingBox.max.y - Float(rnd * 0.5)
Worked well, to which I added some animation! Thanks Andy.
changer = changeling.sink(receiveValue: { [self] _ in
var rnds:[CGFloat] = []
for i in 0..<12 {
let rnd = CGFloat.random(in: 1.0...2.0)
let targetGeometry = SCNBox(width: 0.45, height: rnd, length: 0.45, chamferRadius: 0.01)
let newNode = SCNNode(geometry: targetGeometry)
sourceNodes[i].simdPivot.columns.3.y = newNode.boundingBox.min.y
sourceNodes[i].simdPosition = SIMD3(x: sourceNodes[i].simdPosition.x, y: 0, z: 0)
rnds.append(rnd)
}
for k in 0..<12 {
if virgin {
coreNode.addChildNode(sourceNodes[k])
}
let targetGeometry = SCNBox(width: 0.45, height: rnds[k], length: 0.45, chamferRadius: 0.01)
targetGeometry.firstMaterial?.fillMode = .lines
targetGeometry.firstMaterial?.diffuse.contents = UIColor.blue
let morpher = SCNMorpher()
morpher.targets = [targetGeometry]
sourceNodes[k].morpher = morpher
let animation = CABasicAnimation(keyPath: "morpher.weights[0]")
animation.toValue = 1.0
animation.repeatCount = 0.0
animation.duration = 1.0
animation.fillMode = .forwards
animation.isRemovedOnCompletion = false
sourceNodes[k].addAnimation(animation, forKey: nil)
}
virgin = false
})

You have to position a pivot point of each bar to its base:
boxNode.simdPivot.columns.3.y = someFloatNumber
To reposition a pivot to bar's base use bounding box property:
boxNode.simdPivot.columns.3.y += (boxNode.boundingBox.min.y as? simd_float1)!
After pivot's offset, reposition boxNode towards negative direction of Y-axis.
boxNode.position.y = 0

Related

Creating a scnplain that is the same size as half of the screen using SceneKit

I am trying to figure out how to create a plainNode in SceneKit that takes up exactly half of the screen.
So I found this routine to projectValues that seems correct.
extension CGPoint {
func scnVector3Value(view: SCNView, depth: Float) -> SCNVector3 {
let projectedOrigin = view.projectPoint(SCNVector3(0, 0, depth))
return view.unprojectPoint(SCNVector3(Float(x), Float(y), projectedOrigin.z))
}
}
And I fed these values into it...
let native = UIScreen.main.bounds
let maxMax = CGPoint(x: native.width, y: native.height * 0.5)
let newPosition1 = maxMax.scnVector3Value(view: view, depth: Float(0))
print("newPosition \(newPosition1)")
let minMin = CGPoint(x: 0, y: 0)
let newPosition2 = minMin.scnVector3Value(view: view, depth: Float(0))
print("newPosition \(newPosition2)")
let minMax = CGPoint(x: 0, y: native.height * 0.5)
let newPosition3 = minMax.scnVector3Value(view: view, depth: Float(0))
print("newPosition \(newPosition3)")
let maxMin = CGPoint(x: native.width, y: 0)
let newPosition4 = maxMin.scnVector3Value(view: view, depth: Float(0))
print("newPosition \(newPosition4)")
// approximations that look almost correct, but they are not...
let width = (maxMax.x - minMin.x) / 100 * 2
let height = (maxMax.y - minMin.y) / 100 * 2
let plainGeo = SCNPlane(width: width, height: height)
let planeNode = SCNNode(geometry: plainGeo)
planeNode.geometry?.firstMaterial?.diffuse.contents = UIColor.red
view.scene.rootNode?.addChildNode(planeNode)
But it isn't right? What am I doing wrong here?

SCNNode rotation around a generic axis with animation

I need to rotate the gray bar around the red dot (see picture below).
I managed to do that translating the center point of the bar, rotating the bar and then translating the bar back to the original position.
Down here my code:
struct TestView2: View {
var scene: SCNScene
var arm: SCNNode
var length = 16.0
var with = 1.0
init() {
scene = SCNScene()
scene.setAttribute(400000, forKey: SCNScene.Attribute.endTime.rawValue)
let rootNode = scene.rootNode
let armShape = SCNBox(width: with, height: 0.5, length: length, chamferRadius: 0)
armShape.firstMaterial?.diffuse.contents = NSColor.gray
arm = SCNNode()
arm.geometry = armShape
arm.position = SCNVector3(0, 0, 0)
rootNode.addChildNode(arm)
let axisShape = SCNCylinder(radius: with/2, height: 0.51)
axisShape.firstMaterial?.diffuse.contents = NSColor.red
let axis = SCNNode()
axis.geometry = axisShape
axis.position = SCNVector3(0, 0, length/2-with/2)
rootNode.addChildNode(axis)
// Camera
let camera = SCNNode()
camera.camera = SCNCamera()
camera.camera?.usesOrthographicProjection = true
camera.camera?.orthographicScale = 15
camera.camera?.zNear = 0
camera.camera?.zFar = 100
camera.position = SCNVector3(0, 0, 50)
let cameraOrbit = SCNNode()
cameraOrbit.addChildNode(camera)
rootNode.addChildNode(cameraOrbit)
var eulerAngles = cameraOrbit.eulerAngles
eulerAngles.x = CGFloat(Float.pi / 2.0)
eulerAngles.y = 0
eulerAngles.z = 0
cameraOrbit.eulerAngles = eulerAngles
}
var body: some View {
VStack {
SceneView(scene: scene, options: [.allowsCameraControl, .autoenablesDefaultLighting])
Button("Rotate") {
let currentTransform = arm.transform
let translationTransform = SCNMatrix4MakeTranslation(0, 0, length/2-with/2)
let rotationTrasform = SCNMatrix4MakeRotation(90 * .pi / 180.0, 0, 1, 0)
let backTranslationTransform = SCNMatrix4MakeTranslation(0, 0, -(length/2-with/2))
let newTransform = SCNMatrix4Mult(backTranslationTransform,
SCNMatrix4Mult(rotationTrasform,
SCNMatrix4Mult(translationTransform, currentTransform)))
arm.transform = newTransform
}
}
}
}
The bar rotate as expected. I then added an animation:
...
let animation = CABasicAnimation(keyPath: "transform")
let currentTransform = arm.transform
let translationTransform = SCNMatrix4MakeTranslation(0, 0, length/2-with/2)
let rotationTrasform = SCNMatrix4MakeRotation(90 * .pi / 180.0, 0, 1, 0)
let backTranslationTransform = SCNMatrix4MakeTranslation(0, 0, -(length/2-with/2))
let newTransform = SCNMatrix4Mult(backTranslationTransform,
SCNMatrix4Mult(rotationTrasform,
SCNMatrix4Mult(translationTransform, currentTransform)))
animation.fromValue = currentTransform
animation.toValue = newTransform
animation.duration = 1.0
arm.addAnimation(animation, forKey: nil)
arm.transform = newTransform
...
The result is very clumsy though because the translations before and after the rotation are visible:
Since I am new to SceneKit, I think my approach to rotate the bar is completely wrong.
How can I improve the code to have both the rotation and animation at the same time?

swift SceneKit calculate node angle

i trying to add a node (pin) to sphere node (when taping on sphere node), but can't correctly calculate an angle, can u help me pls ?
private func addPinToSphere(result: SCNHitTestResult) {
guard let scene = SCNScene(named: "art.scnassets/tree.scn") else { return }
guard let treeNode = scene.rootNode.childNode(withName: "Trunk", recursively: true) else { return }
let x = CGFloat(result.worldCoordinates.x)
let y = CGFloat(result.worldCoordinates.y)
treeNode.position = result.worldCoordinates
treeNode.eulerAngles = SCNVector3((x) * .pi / CGFloat(180), 0, (y - 90) * .pi / CGFloat(180)) // y axis is ignorable
result.node.addChildNode(treeNode)
}
when run code i have this
but want like this
You can do it similar to this code. This is a missile launcher, but it's basically the same thing. Your pin = my fireTube, so create a base node, then add a subnode with your materials to it and rotate it into place. You'll have to experiment with where to place the tube initially, but once you get it into the right place the lookat constraint will always point it in the right direction.
case .d1, .d2, .d3, .d4, .d5, .d6, .d7, .d8, .d9, .d10:
let BoxGeometry = SCNBox(width: 0.8, height: 0.8, length: 0.8, chamferRadius: 0.0)
let vNode = SCNNode(geometry: BoxGeometry)
BoxGeometry.materials = setDefenseTextures(vGameType: vGameType)
let tubeGeometry = SCNTube(innerRadius: 0.03, outerRadius: 0.05, height: 0.9)
let fireTube = SCNNode(geometry: tubeGeometry)
tubeGeometry.firstMaterial?.diffuse.contents = data.getTextureColor(vTheme: 0, vTextureType: .barrelColor)
fireTube.position = SCNVector3(0, 0.2, -0.3)
let vRotateX = SCNAction.rotateBy(x: CGFloat(Float(GLKMathDegreesToRadians(-90))), y: 0, z: 0, duration: 0)
fireTube.runAction(vRotateX)
vNode.addChildNode(fireTube)
return vNode
Then set target on your base node and your subnode will rotate with it:
func setTarget()
{
node.constraints = []
let vConstraint = SCNLookAtConstraint(target: targetNode)
vConstraint.isGimbalLockEnabled = true
node.constraints = [vConstraint]
}
In your case, target equals center mass of your sphere and the pin will always point to it, provided you built and aligned your box and pin correctly.

Getting SpriteKit texture from SKView returning nil after 104 calls

I am having some odd behavior in SpriteKit when creating a texture. The function below shows you what I am doing. In short, I'm in SceneKit and making a SCNNode out of an Array of colors (think pixel/voxels). It works like a charm. However, after exactly 104 calls the texture returned is nil. Afterwards, it is hit or miss whether the texture will be nil or not. I am also providing the exact color information. Thoughts?
func create2dModelSK(with colors: [String]) -> SCNNode? {
let geo = SCNBox(width: 1.0, height: 1.0, length: 0.1, chamferRadius: 0.0)
let base = SCNNode(geometry: geo)
let view = SKSpriteNode(color: .white, size: CGSize(width: 160, height: 160))
view.anchorPoint = CGPoint(x: 0, y: 1)
var xOffset = 0
var yOffset = 0
var count = 0
for _ in 0...15 {
for _ in 0...15 {
guard let newColor = UIColor(hexString: "#" + colors[count] + "ff") else { return base }
let n = SKSpriteNode(color: newColor, size: CGSize(width: 10, height: 10))
n.anchorPoint = CGPoint(x: 0, y: 1)
n.position = CGPoint(x: xOffset, y: yOffset)
view.addChild(n)
xOffset += 10
count += 1
}
xOffset = 0
yOffset -= 10
}
let skView = SKView(frame: CGRect(x: 0, y: 0, width: 160, height: 160))
let texture = skView.texture(from: view)
//AFTER being called 104 times, texture is nil.
let faceMaterial = SCNMaterial()
faceMaterial.diffuse.contents = texture
let sideMaterial = SCNMaterial()
sideMaterial.diffuse.contents = UIColor.white
let materialsForBox = [faceMaterial,sideMaterial,faceMaterial,sideMaterial,sideMaterial,sideMaterial]
base.geometry?.materials = materialsForBox
let scale = SCNVector3(x: 0.1, y: 0.1, z: 0.1)
base.scale = scale
return base
}
This is where autoreleasepool comes in handy, it allows you to release the memory when the autoreleasepool is finished so that you do not run out of space before using it again.
Of course this is not going to solve your main problem, where you are creating too many textures and running out of memory space, but it will allow you to at least make some more because it will release the temporary memory that view.texture(from:node) is holding on to.
func create2dModelSK(with colors: [String]) -> SCNNode? {
let geo = SCNBox(width: 1.0, height: 1.0, length: 0.1, chamferRadius: 0.0)
let base = SCNNode(geometry: geo)
let view = SKSpriteNode(color: .white, size: CGSize(width: 160, height: 160))
view.anchorPoint = CGPoint(x: 0, y: 1)
var xOffset = 0
var yOffset = 0
var count = 0
for _ in 0...15 {
for _ in 0...15 {
guard let newColor = UIColor(hexString: "#" + colors[count] + "ff") else { return base }
let n = SKSpriteNode(color: newColor, size: CGSize(width: 10, height: 10))
n.anchorPoint = CGPoint(x: 0, y: 1)
n.position = CGPoint(x: xOffset, y: yOffset)
view.addChild(n)
xOffset += 10
count += 1
}
xOffset = 0
yOffset -= 10
}
autoreleasepool{
let skView = SKView(frame: CGRect(x: 0, y: 0, width: 160, height: 160))
let texture = skView.texture(from: view)
//AFTER being called 104 times, texture is nil.
let faceMaterial = SCNMaterial()
faceMaterial.diffuse.contents = texture
let sideMaterial = SCNMaterial()
sideMaterial.diffuse.contents = UIColor.white
let materialsForBox = [faceMaterial,sideMaterial,faceMaterial,sideMaterial,sideMaterial,sideMaterial]
base.geometry?.materials = materialsForBox
}
let scale = SCNVector3(x: 0.1, y: 0.1, z: 0.1)
base.scale = scale
return base
}

simd_float4x4 Columns

I want to translate a plane without rotating the image. For any reason my image is being rotated.
var translation = matrix_identity_float4x4
translation.colum = -0.2
let transform = simd_mul(currentFrame.camera.transform, translation)
planeNode.simdWorldTransform = matrix_multiply(currentFrame.camera.transform, translation)
Also, I notice that matrix_identity_float4x4 contains 4 columns but the documentation is not available.
Why 4 columns? Are there the frame of the plane?
The simplest way to do it is to use the following code for positioning:
let planeNode = SCNNode()
planeNode.geometry = SCNPlane(width: 20, height: 20)
// At first we need to rotate a plane about its x axis in radians:
planeNode.rotation = SCNVector4(1, 0, 0, -Double.pi/2)
planeNode.geometry?.materials.first?.diffuse.contents = UIColor.red
planeNode.position.x = 10
planeNode.position.z = 10
// planeNode.position = SCNVector3(x: 10, y: 0, z: 10)
scene.rootNode.addChildNode(planeNode)
or this way:
let cameraNode = SCNNode()
cameraNode.camera = SCNCamera()
cameraNode.position = SCNVector3(x: 0, y: 0, z: 15)
scene.rootNode.addChildNode(cameraNode)
let planeNode = SCNNode()
planeNode.geometry = SCNPlane(width: 20, height: 20)
planeNode.rotation = SCNVector4(1, 0, 0, -Double.pi/2)
planeNode.geometry?.materials.first?.diffuse.contents = UIColor.red
let distance: Float = 50
planeNode.simdPosition = cameraNode.simdWorldFront * distance // -Z axis
planeNode.simdPosition = cameraNode.simdWorldRight * distance // +X axis
scene.rootNode.addChildNode(planeNode)
If you wanna know more about matrices used in ARKit and SceneKit frameworks just look at Figure 1-8 Matrix configurations for common transformations.
Hope this helps.