How to display (add) plane text node between two points? - ios

How to display distance value plane surface between two points in measurement ARKit/SceneKit? Same as on image.

Here's a simple principle how to place a label with 3D text between two points.
let p1 = SCNNode() // POINT 1
p1.geometry = SCNSphere(radius: 0.3)
p1.position = SCNVector3(-1, -2, -3)
p1.geometry?.firstMaterial?.diffuse.contents = NSColor.red
scene.rootNode.addChildNode(p1)
let p2 = SCNNode() // POINT 2
p2.geometry = SCNSphere(radius: 0.2)
p2.position = SCNVector3(4, 5, 6)
p2.geometry?.firstMaterial?.diffuse.contents = NSColor.red
scene.rootNode.addChildNode(p2)
let x = (p1.position.x + p2.position.x) / 2
let y = (p1.position.y + p2.position.y) / 2
let z = (p1.position.z + p2.position.z) / 2
let label = SCNNode()
let text = SCNText(string: "label", extrusionDepth: 5)
label.geometry = text
// Shifting text's pivot to its center.
// Default pivot's position is lower left corner.
label.simdPivot.columns.3.x = Float((text.boundingBox.min.x +
text.boundingBox.max.x) / 2)
label.simdPivot.columns.3.y = Float((text.boundingBox.min.y +
text.boundingBox.max.y) / 2)
label.scale = SCNVector3(0.02, 0.02, 0.02)
label.position = SCNVector3(x, y, z)
label.geometry?.firstMaterial?.diffuse.contents = NSColor.red
scene.rootNode.addChildNode(label)
let constraint = SCNBillboardConstraint()
label.constraints = [constraint]
let plane = SCNNode()
plane.geometry = SCNPlane(width: 2, height: 1)
plane.position = SCNVector3(x, y, z)
plane.geometry?.firstMaterial?.diffuse.contents = NSColor.white
scene.rootNode.addChildNode(plane)
plane.constraints = [constraint]
The same way you may add ARAnchor and attach any label to it.

Related

2D Drawing On a Virtual 3D Plane in RealityKit

I'm trying to draw a 2D stroke using my finger on a 3D plane I generated and added to the scene:
let material = SimpleMaterial.init(color: .white,
roughness: 1,
isMetallic: false)
let plane = MeshResource.generatePlane(width: 1.5,
height: 1.0)
let planeEntity = ModelEntity(mesh: plane,
materials: [material])
planeEntity.collision = CollisionComponent(shapes: [.generateBox(size: [1.5, 1.0, 0.05])],
mode: .trigger,
filter: .sensor)
planeEntity.name = "TargetPlane"
planeEntity.position = SIMD3(x: 0.0,
y: 0.0,
z: 0.0)
self.scene.addChild(planeEntity)
I'm getting the points along the finger path using a pan recognizer on my ARView:
#objc func handlePan(_ sender: UIPanGestureRecognizer) {
let touchInView = sender.location(in: self.arView)
guard let hitEntity = self.arView.entity(at: touchInView) else { return } // No entity was hit.
if hitEntity.name == "TargetPlane" {
if sender.state == .began || sender.state == .changed {
let result = self.arView.hitTest(touchInView)
guard let collision = result.first else { return }
// Convert from world space to local plane space.
let position = planeEntity.convert(position: collision.position,
from: nil)
}
}
}
At this point I have points in the plane's local co-ordinates. I thought of using a UIBezierPath and drawing it onto a CAShapeLayer then rendering a UIImage out of it and using that as a texture on the plane.
The problem is I have no idea how to map the 3D points to 2D pixel co-ordinates on the plane.
I'm aware of the project(_:) method but that maps points to the whole viewport of the device. I need to map the points to the surface of the virtual plane.
I figured it out using the pinhole camera model.
if sender.state == .began || sender.state == .changed {
let result = self.arView.hitTest(touchInView)
guard let collision = result.first else { return }
// Convert the point from world space to the plane's local space.
let position = planeEntity.convert(position: collision.position,
from: nil)
// Get the focal length.
let intrinsics = (self.arView.session.currentFrame?.camera.intrinsics)!
let f_x = intrinsics.columns.0.x
let f_y = intrinsics.columns.1.y
// Co-ordinates of the principal point.
let c_x = intrinsics.columns.2.x
let c_y = intrinsics.columns.2.y
// (x, y, z) of the 3D point on the plane.
let x = position.x
let y = position.y
let z = collision.distance
// Map to 2D (u, v) co-ordinates.
let u = (x / z) * f_x + c_x
let v = (y / z) * f_y + c_y
// Shift the origin to the top left corner for Core Graphics.
let u_max = (planeEntity.model!.mesh.bounds.max.x / z) * f_x + c_x
let v_max = (planeEntity.model!.mesh.bounds.max.y / z) * f_y + c_y
let u_mapped = max(u, 0) // Clamp to avoid negative values.
let v_mapped = max(v_max - v, 0)
// Add this point to the UIBezierPath.
let mappedPoint = CGPoint(x: CGFloat(u_mapped),
y: CGFloat(v_mapped))
// Use this as the size of the image renderer.
let canvasSize = CGSize(width: u_max,
height: v_max)
}

How to calculate the distance between a SCNText's Center and its Left and Right Corners

How can I calculate distance between SCNText center and its left edge corner and its right edge corner.
For eg using the code below, if the word was "texas" the middle of the letter "x" would be the pivot point because it's in the middle of the word. How would I get the distance between the left side of the letter "t" and the letter "x". How would I get the distance between the right side of the letter "s" and the letter "x"
let material = SCNMaterial()
material.diffuse.contents = UIColor.red
let geometry = SCNText(string: "texas", extrusionDepth: 0.1)
geometry.flatness = 0
geometry.font = UIFont.systemFont(ofSize: 6.3)
geometry.materials = [material]
let textNode = SCNNode()
textNode.geometry = geometry
textNode.geometry?.firstMaterial?.isDoubleSided = true
// set pivot point
let min = textNode.boundingBox.min
let max = textNode.boundingBox.max
textNode.pivot = SCNMatrix4MakeTranslation(
min.x + (max.x - min.x)/2,
min.y + (max.y - min.y)/2,
min.z + (max.z - min.z)/2
)
let totalWidth: Float = textNode.boundingBox.max.x - textNode.boundingBox.min.x
let halfWidth: Float = totalWidth / 2
print("total width:", totalWidth)
print("half width:", halfWidth)

Spritekit Camera Node scale but pin the bottom of the scene

I have a camera node that is scaled at 1. When I run the game, I want it to scale it down (i.e. zoom out) but keep the "floor" at the bottom. How would I go about pinning the camera node to the bottom of the scene and effectively zooming "up" (difficult to explain). So the bottom of the scene stays at the bottom but the rest zooms out.
I have had a go with SKConstraints but not having any luck (I'm quite new at SpriteKit)
func setConstraints(with scene: SKScene, and frame: CGRect, to node: SKNode?) {
let scaledSize = CGSize(width: scene.size.width * xScale, height: scene.size.height * yScale)
let boardContentRect = frame
let xInset = min((scaledSize.width / 2), boardContentRect.width / 2)
let yInset = min((scaledSize.height / 2), boardContentRect.height / 2)
let insetContentRect = boardContentRect.insetBy(dx: xInset, dy: yInset)
let xRange = SKRange(lowerLimit: insetContentRect.minX, upperLimit: insetContentRect.maxX)
let yRange = SKRange(lowerLimit: insetContentRect.minY, upperLimit: insetContentRect.maxY)
let levelEdgeConstraint = SKConstraint.positionX(xRange, y: yRange)
if let node = node {
let zeroRange = SKRange(constantValue: 0.0)
let positionConstraint = SKConstraint.distance(zeroRange, to: node)
constraints = [positionConstraint, levelEdgeConstraint]
} else {
constraints = [levelEdgeConstraint]
}
}
then calling the function with:
gameCamera.setConstraints(with: self, and: scene!.frame, to: nil)
(This was code from a tutorial I was following) The "setConstraints" function is an extension of SKCameraNode
I'm not sure this will give me the correct output, but when I run the code to scale, it just zooms from the middle and shows the surrounding area of the scene .sks file.
gameCamera.run(SKAction.scale(to: 0.2, duration: 100))
This is the code to scale the gameCamera
EDIT: Answer below is nearly what I was looking for, this is my updated answer:
let scaleTo = 0.2
let duration = 100
let scaleTop = SKAction.customAction(withDuration:duration){
(node, elapsedTime) in
let newScale = 1 - ((elapsedTime/duration) * (1-scaleTo))
let currentScaleY = node.yScale
let currentHeight = node.scene!.size.height * currentScaleY
let newHeight = node.scene!.size.height * newScale
let heightDiff = newHeight - currentHeight
let yOffset = heightDiff / 2
node.setScale(newScale)
node.position.y += yOffset
}
You cannot use a constraint because your scale size is dynamic.
Instead you need to move your camera position to give the illusion it is only scaling in 3 directions.
To do this, I would recommend creating a custom action.
let scaleTo = 2.0
let duration = 1.0
let currentNodeScale = 0.0
let scaleTop = SKCustomAction(withDuration:duration){
(node, elapsedTime) in
if elapsedTime == 0 {currentNodeScale = node.scale}
let newScale = currentNodeScale - ((elapsedTime/duration) * (currentNodeScale-scaleTo))
let currentYScale = node.yScale
let currentHeight = node.scene.size.height * currentYScale
let newHeight = node.scene.size.height * newScale
let heightDiff = newHeight - currentHeight
let yOffset = heightDiff / 2
node.scale(to:newScale)
node.position.y += yOffset
}
What this is doing is comparing the new height of your camera with the old height, and moving it 1/2 the distance.
So if your current height is 1, this means your camera sees [-1/2 to 1/2] on the y axis. If you new scale height is 2, then your camera sees [-1 to 1] on the y axis. We need to move the camera up so that the camera sees [-1/2 to 3/2], meaning we need to add 1/2. So we do 2 - 1, which is 1, then go 1/2 that distance. This makes our yOffset 1/2, which you add to the camera.

Unable to create Cylinder for certain situations - Arkit

I have two vectors as inputs and I am trying to create a cylinder between them.
But this fails, when x and z coordinates of my points are same and y coordinate is different
I have the following functions:
func buildLineInTwoPointsWithRotation(from startPoint: SCNVector3,
to endPoint: SCNVector3,
radius: CGFloat,
color: UIColor) -> SCNNode {
let w = SCNVector3(x: endPoint.x-startPoint.x,
y: endPoint.y-startPoint.y,
z: endPoint.z-startPoint.z)
let l = CGFloat(sqrt(w.x * w.x + w.y * w.y + w.z * w.z))
if l == 0.0 {
// two points together.
let sphere = SCNSphere(radius: radius)
sphere.firstMaterial?.diffuse.contents = color
self.geometry = sphere
self.position = startPoint
return self
}
let cyl = SCNCylinder(radius: radius, height: l)
cyl.firstMaterial?.diffuse.contents = color
self.geometry = cyl
//original vector of cylinder above 0,0,0
let ov = SCNVector3(0, l/2.0,0)
//target vector, in new coordination
let nv = SCNVector3((endPoint.x - startPoint.x)/2.0, (endPoint.y - startPoint.y)/2.0,
(endPoint.z-startPoint.z)/2.0)
// axis between two vector
let av = SCNVector3( (ov.x + nv.x)/2.0, (ov.y+nv.y)/2.0, (ov.z+nv.z)/2.0)
//normalized axis vector
let av_normalized = normalizeVector(av)
let q0 = Float(0.0) //cos(angel/2), angle is always 180 or M_PI
let q1 = Float(av_normalized.x) // x' * sin(angle/2)
let q2 = Float(av_normalized.y) // y' * sin(angle/2)
let q3 = Float(av_normalized.z) // z' * sin(angle/2)
let r_m11 = q0 * q0 + q1 * q1 - q2 * q2 - q3 * q3
let r_m12 = 2 * q1 * q2 + 2 * q0 * q3
let r_m13 = 2 * q1 * q3 - 2 * q0 * q2
let r_m21 = 2 * q1 * q2 - 2 * q0 * q3
let r_m22 = q0 * q0 - q1 * q1 + q2 * q2 - q3 * q3
let r_m23 = 2 * q2 * q3 + 2 * q0 * q1
let r_m31 = 2 * q1 * q3 + 2 * q0 * q2
let r_m32 = 2 * q2 * q3 - 2 * q0 * q1
let r_m33 = q0 * q0 - q1 * q1 - q2 * q2 + q3 * q3
self.transform.m11 = r_m11
self.transform.m12 = r_m12
self.transform.m13 = r_m13
self.transform.m14 = 0.0
self.transform.m21 = r_m21
self.transform.m22 = r_m22
self.transform.m23 = r_m23
self.transform.m24 = 0.0
self.transform.m31 = r_m31
self.transform.m32 = r_m32
self.transform.m33 = r_m33
self.transform.m34 = 0.0
self.transform.m41 = (startPoint.x + endPoint.x) / 2.0
self.transform.m42 = (startPoint.y + endPoint.y) / 2.0
self.transform.m43 = (startPoint.z + endPoint.z) / 2.0
self.transform.m44 = 1.0
return self
}
func normalizeVector(_ iv: SCNVector3) -> SCNVector3 {
let length = sqrt(iv.x * iv.x + iv.y * iv.y + iv.z * iv.z)
if length == 0 {
return SCNVector3(0.0, 0.0, 0.0)
}
return SCNVector3( iv.x / length, iv.y / length, iv.z / length)
}
I am not still sure, as to why it fails, when the x,z coordinates of both points are same, but y coordinate is completely different.
I would be really glad, if someone can explain me.
Thanks in Advance.
Looking at your code, it looks very complicated for something which can be achieved in a lot less code.
This code can be used with an SCNBox or SCNCylinder Geometry.
class MeasuringLineNode: SCNNode{
init(startingVector vectorA: GLKVector3, endingVector vectorB: GLKVector3) {
super.init()
//1. Create The Height Our Box Which Is The Distance Between Our 2 Vectors
let height = CGFloat(GLKVector3Distance(vectorA, vectorB))
//2. Set The Nodes Position At The Same Position As The Starting Vector
self.position = SCNVector3(vectorA.x, vectorA.y, vectorA.z)
//3. Create A Second Node Which Is Placed At The Ending Vectors Posirions
let nodeVectorTwo = SCNNode()
nodeVectorTwo.position = SCNVector3(vectorB.x, vectorB.y, vectorB.z)
//4. Create An SCNNode For Alignment Purposes At 90 Degrees
let nodeZAlign = SCNNode()
nodeZAlign.eulerAngles.x = Float.pi/2
//5. Create An SCNCyclinder Geometry To Act As Our Line
let cylinder = SCNCylinder(radius: 0.001, height: height)
let material = SCNMaterial()
material.diffuse.contents = UIColor.white
cylinder.materials = [material]
/*
If you want to use an SCNBox then use the following:
let box = SCNBox(width: 0.001, height: height, length: 0.001, chamferRadius: 0)
let material = SCNMaterial()
material.diffuse.contents = UIColor.white
box.materials = [material]
*/
//6. Create The LineNode Centering On The Alignment Node
let nodeLine = SCNNode(geometry: cylinder)
nodeLine.position.y = Float(-height/2)
nodeZAlign.addChildNode(nodeLine)
self.addChildNode(nodeZAlign)
//7. Force The Node To Look At Our End Vector
self.constraints = [SCNLookAtConstraint(target: nodeVectorTwo)]
}
required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") }
}
Assuming you have 2 SCNNodes (which in my example, I will simply call nodeA and nodeB), you can create a joining line like so:
//1. Convert The Nodes SCNVector3 to GLVector3
let nodeAVector3 = GLKVector3Make(nodeA.position.x, nodeA.position.y, nodeA.position.z)
let nodeBVector3 = GLKVector3Make(nodeB.position.x, nodeB.position.y, nodeB.position.z)
//2. Draw A Line Between The Nodes
let line = MeasuringLineNode(startingVector: nodeAVector3 , endingVector: nodeBVector3)
self.augmentedRealityView.scene.rootNode.addChildNode(line)
Hope this helps...

SpriteNode is not visible

I'm trying to create my User SpriteNode with the following Methode. The texture is Kladde and the Image is not empty too.
func spawnPlayer() {
if didFirstTap == true && isGameOver == false {
let collisionMask = CollisionCategory.Surrounding.rawValue | CollisionCategory.Platform.rawValue
let x = self.frame.size.width / 4
let y = CGRectGetMidY(self.frame)
let node = SKNode()
node.position = CGPointMake(frame.size.width + playerTexture!.size().width, 0)
playerNode = SKSpriteNode(texture: playerTexture)
playerNode!.setScale(1.0)
playerNode!.position = CGPointMake(x, y)
playerNode!.speed = 1.0
playerNode!.zRotation = 0.0
playerNode!.physicsBody = SKPhysicsBody(circleOfRadius: playerNode!.size.height / 2)
playerNode!.physicsBody?.dynamic = true
playerNode!.physicsBody?.allowsRotation = false
playerNode!.physicsBody?.categoryBitMask = CollisionCategory.Player.rawValue
playerNode!.physicsBody?.contactTestBitMask = collisionMask
playerNode!.physicsBody?.collisionBitMask = collisionMask
playerNode!.physicsBody!.velocity = CGVectorMake( 0, 0 )
node.addChild(playerNode!)
addChild(node)
}
}
I Hope someone knows, why my Node is invisible?
Look at where the node is spawning:
node.position = CGPointMake(frame.size.width + playerTexture!.size().width, 0)
Then on top of that you move the player in the position of the node
let x = self.frame.size.width / 4
let y = CGRectGetMidY(self.frame)
playerNode!.position = CGPointMake(x, y)
This poor guy is way outside of the screen space.

Resources