Live Rotation of SCNNode - ios

I want my node to rotate on the Y axis while the camera is rotating.
class ArtScene: SCNScene{
var image = UIImage()
var geometry = SCNBox()
var motion = MotionKit()
var node = SCNNode()
convenience init(create: Bool) {
self.init()
image = UIImage(named: "sf")!
let lenght: CGFloat = 50
let width: CGFloat = self.image.size.width
let height: CGFloat = self.image.size.height
self.geometry = SCNBox(width: width / 1000 , height: height / 1000, length: lenght / 1000, chamferRadius: 0.005)
print("Geometry WIDTH: \(self.geometry.width)")
print("Geometry LENGHT: \(self.geometry.length)")
self.geometry.firstMaterial?.diffuse.contents = UIColor.red
self.geometry.firstMaterial?.specular.contents = UIColor.white
self.geometry.firstMaterial?.emission.contents = UIColor.blue
node = SCNNode(geometry: self.geometry)
self.rootNode.addChildNode(node)
}
func rotate() {
motion.getDeviceMotionObject { (deviceMotion) in
let rotationY = deviceMotion.rotationRate.y
print(rotationY)
let rotationAction = SCNAction.rotate(by: CGFloat(1.0), around: SCNVector3(0,rotationY,0), duration: 2.0)
self.node.runAction(rotationAction)
}
}
}
I can print the Device motion, but can't get the node to rotate at the same time. How can I do it?

Related

SKShapeNode fillTexture with rotation

I am having trouble figuring out how the fillTexture property of an SKShapeNode works when rotating that node. In this example I create a fan of 4 rotating blades around the center. The texture applied however varies in intensity as the blades rotate. Why? I want the gradient texture application be invariable with rotation.
func buildBackground() {
let bg = SKShapeNode.init(rect: UIScreen.main.bounds)
bg.fillColor = .orange
self.addChild(bg) // self == SKScene
let size = UIScreen.main.bounds.size
let fan = SKNode.init()
let H = size.height
let W = size.width/2
let blades = 4
let triangle = CGMutablePath.init()
triangle.move(to: .zero)
triangle.addLine(to: CGPoint(-W,H))
triangle.addLine(to: CGPoint(+W,H))
triangle.addLine(to: .zero)
triangle.closeSubpath()
let da = 360.0/CGFloat(blades)
var r : CGFloat = 0
let texture = SKTexture.init(size: size, color0: .white, color1: .clear, radius0: 0.25, radius1: 1)
for i in 0..<blades {
let blade = SKShapeNode.init(path: triangle)
fan.addChild(blade)
blade.fillTexture = texture
blade.lineWidth = 0
blade.fillColor = .black
blade.zRotation = r.degreesToRadians
r += da
}
bg.addChild(fan)
fan.position = bg.frame.center
fan.run(.repeatForever(.rotate(byAngle: -0.1, duration: 0.2)))
fan.alpha = 0.5
}
public extension SKTexture {
convenience init(size : CGSize,
color0 : SKColor,
color1 : SKColor,
radius0 : CGFloat, // 0
radius1 : CGFloat) // 1
{
let coreImageContext = CIContext(options: nil)
let gradientFilter = CIFilter(name: "CIRadialGradient")!
let inputCenter = CIVector.init(x: size.width/2, y: size.height/2)
let inputRadius0 = radius0 * size.diagonal/2
let inputRadius1 = radius1 * size.diagonal/2
gradientFilter.setDefaults()
gradientFilter.setValue(inputCenter, forKey: "inputCenter")
gradientFilter.setValue(inputRadius0, forKey: "inputRadius0")
gradientFilter.setValue(inputRadius1, forKey: "inputRadius1")
gradientFilter.setValue(color0.asCIColor, forKey: "inputColor0")
gradientFilter.setValue(color1.asCIColor, forKey: "inputColor1")
let coreImage = coreImageContext.createCGImage(gradientFilter.outputImage!, from: .init(size)) //CGRect(x: 0, y: 0, width: size.width, height: size.height))
self.init(cgImage: coreImage!)
}
}

What is the difference between SKScene.size.width SKSpriteNode.size.width?

I'm new to swift 5.
I printed out the self.size.width in GameScene and the result is 677.0
I printed out the self.size.width from another class - lets say Ground and the result is 4002.0
I'm confused, please help.
Thanks a lot.
GameScene.swift:
import SpriteKit
class GameScene: SKScene {
let cam = SKCameraNode()
let bee = SKSpriteNode()
let ground = Ground()
override func didMove(to view: SKView) {
self.anchorPoint = .zero
//self.backgroundColor = UIColor(red: 0.4, green: 0.6, blue: 0.95, alpha: 1.0)
self.camera = cam
self.addingTheFlyingBee()
self.addBackground()
let bee2 = Bee()
bee2.position = CGPoint(x: 325, y: 325)
self.addChild(bee2)
let bee3 = Bee()
bee3.position = CGPoint(x: 200, y: 325)
self.addChild(bee3)
ground.position = CGPoint(x: -self.size.width * 2, y: 0)
ground.size = CGSize(width: self.size.width * 6, height: 0)
ground.createChildren()
self.addChild(ground)
}
override func didSimulatePhysics() {
self.camera!.position = bee.position
}
func addBackground() {
let bg = SKSpriteNode(imageNamed: "background-menu")
bg.position = CGPoint(x: 220, y: 220)
bg.zPosition = -1
self.addChild(bg)
}
func addingTheFlyingBee() {
bee.position = CGPoint(x: 250, y: 250)
bee.size = CGSize(width: 38, height: 34)
self.addChild(bee)
let beeAtlas = SKTextureAtlas(named: "Enemies")
let beeFrames : [SKTexture] = [
beeAtlas.textureNamed("bee"),
beeAtlas.textureNamed("bee-fly")
]
let flyAction = SKAction.animate(with: beeFrames, timePerFrame: 0.14)
let beeAction = SKAction.repeatForever(flyAction)
bee.run(beeAction)
let pathLeft = SKAction.moveBy(x: -200, y: -10, duration: 2)
let pathRight = SKAction.moveBy(x: 200, y: 10, duration: 2)
let flipTextureNegative = SKAction.scaleX(to: -1, duration: 0)
let flipTexturePositive = SKAction.scaleX(to: 1, duration: 0)
let flightOfTheBee = SKAction.sequence([ pathLeft, flipTextureNegative, pathRight, flipTexturePositive])
let neverEndingFlight = SKAction.repeatForever(flightOfTheBee)
bee.run(neverEndingFlight)
}
Ground.swift:
import Foundation
import SpriteKit
class Ground: SKSpriteNode, GameSprite {
var textureAtlas: SKTextureAtlas = SKTextureAtlas(named: "Environment")
var initialSize = CGSize.zero
func createChildren() {
self.anchorPoint = CGPoint(x: 0, y: 1)
let texture = textureAtlas.textureNamed("ground")
var tileCount: CGFloat = 0
let tileSize = CGSize(width: 35, height: 300)
while tileCount * tileSize.width < self.size.width {
let tileNode = SKSpriteNode(texture: texture)
tileNode.size = tileSize
tileNode.position.x = tileCount * tileSize.width
tileNode.anchorPoint = CGPoint(x: 0, y: 1)
self.addChild(tileNode)
tileCount += 1
}
}
func onTap() {}
}
I don't really understand the question. Since your scene width is 677.0px and your ground
width should be 6 * scene width so 4062px.
Is your question "Why is ground node width 4002px instead of 4062px?" or "What is the difference between those properties?"
If you question is the second one then this should be an answer:
SKScene.size.width will return width of the current scene while SKSpriteNode.size.width will return width of the target node in your case "ground" node.
self.size.width will return width of the current class you're editing.
So in GameScene.swift file if you call self.size.width inside a class GameScene: SKScene it will return GameScene width.
In Ground.swift file calling self.size.width inside a class Ground: SKSpriteNode, GameSprite will return Ground width.
I hope this answers your question.

How do I make a hexagon with 6 triangular SCNNodes?

I'm trying to make a hexagon grid with triangles without altering any pivot points, but I can't seem to position the triangles correctly to make single hexagon. I'm creating SCNNodes with UIBezierPaths to form triangles and then rotating the bezier paths. This seems to work fine UNTIL I try to use a parametric equation to position the triangles around a circle to form the hexagon, then they don't end up in the correct position. Can you help me spot where I'm doing wrong here?
class TrianglePlane: SCNNode {
var size: CGFloat = 0.1
var coords: SCNVector3 = SCNVector3Zero
var innerCoords: Int = 0
init(coords: SCNVector3, innerCoords: Int, identifier: Int) {
super.init()
self.coords = coords
self.innerCoords = innerCoords
setup()
}
init(identifier: Int) {
super.init()
// super.init(identifier: identifier)
setup()
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func setup() {
let myPath = path()
let geo = SCNShape(path: myPath, extrusionDepth: 0)
geo.firstMaterial?.diffuse.contents = UIColor.red
geo.firstMaterial?.blendMode = .multiply
self.geometry = geo
}
func path() -> UIBezierPath {
let max: CGFloat = self.size
let min: CGFloat = 0
let bPath = UIBezierPath()
bPath.move(to: .zero)
bPath.addLine(to: CGPoint(x: max / 2,
y: UIBezierPath.middlePeak(height: max)))
bPath.addLine(to: CGPoint(x: max, y: min))
bPath.close()
return bPath
}
}
extension TrianglePlane {
static func generateHexagon() -> [TrianglePlane] {
var myArr: [TrianglePlane] = []
let colors = [UIColor.red, UIColor.green,
UIColor.yellow, UIColor.systemTeal,
UIColor.cyan, UIColor.magenta]
for i in 0 ..< 6 {
let tri = TrianglePlane(identifier: 0)
tri.geometry?.firstMaterial?.diffuse.contents = colors[i]
tri.position = SCNVector3( -0.05, 0, -0.5)
// Rotate bezier path
let angleInDegrees = (Float(i) + 1) * 180.0
print(angleInDegrees)
let angle = CGFloat(deg2rad(angleInDegrees))
let geo = tri.geometry as! SCNShape
let path = geo.path!
path.rotateAroundCenter(angle: angle)
geo.path = path
// Position triangle in hexagon
let radius = Float(tri.size)/2
let deg: Float = Float(i) * 60
let radians = deg2rad(-deg)
let x1 = tri.position.x + radius * cos(radians)
let y1 = tri.position.y + radius * sin(radians)
tri.position.x = x1
tri.position.y = y1
myArr.append(tri)
}
return myArr
}
static func deg2rad(_ number: Float) -> Float {
return number * Float.pi / 180
}
}
extension UIBezierPath {
func rotateAroundCenter(angle: CGFloat) {
let center = self.bounds.center
var transform = CGAffineTransform.identity
transform = transform.translatedBy(x: center.x, y: center.y)
transform = transform.rotated(by: angle)
transform = transform.translatedBy(x: -center.x, y: -center.y)
self.apply(transform)
}
static func middlePeak(height: CGFloat) -> CGFloat {
return sqrt(3.0) / 2 * height
}
}
extension CGRect {
var center : CGPoint {
return CGPoint(x:self.midX, y:self.midY)
}
}
What it currently looks like:
What it SHOULD look like:
I created two versions – SceneKit and RealityKit.
SceneKit (macOS version)
The simplest way to compose a hexagon is to use six non-uniformly scaled SCNPyramids (flat) with their shifted pivot points. Each "triangle" must be rotated in 60 degree increments (.pi/3).
import SceneKit
class ViewController: NSViewController {
override func viewDidLoad() {
super.viewDidLoad()
let sceneView = self.view as! SCNView
let scene = SCNScene()
sceneView.scene = scene
sceneView.allowsCameraControl = true
sceneView.backgroundColor = NSColor.white
let cameraNode = SCNNode()
cameraNode.camera = SCNCamera()
scene.rootNode.addChildNode(cameraNode)
cameraNode.position = SCNVector3(x: 0, y: 0, z: 15)
for i in 1...6 {
let triangleNode = SCNNode(geometry: SCNPyramid(width: 1.15,
height: 1,
length: 1))
// the depth of the pyramid is almost zero
triangleNode.scale = SCNVector3(5, 5, 0.001)
// move a pivot point from pyramid its base to upper vertex
triangleNode.simdPivot.columns.3.y = 1
triangleNode.geometry?.firstMaterial?.diffuse.contents = NSColor(
calibratedHue: CGFloat(i)/6,
saturation: 1.0,
brightness: 1.0,
alpha: 1.0)
triangleNode.rotation = SCNVector4(0, 0, 1,
-CGFloat.pi/3 * CGFloat(i))
scene.rootNode.addChildNode(triangleNode)
}
}
}
RealityKit (iOS version)
In this project I generated a triangle with the help of MeshDescriptor and copied it 5 more times.
import UIKit
import RealityKit
class ViewController: UIViewController {
#IBOutlet var arView: ARView!
let anchor = AnchorEntity()
let camera = PointOfView()
let indices: [UInt32] = [0, 1, 2]
override func viewDidLoad() {
super.viewDidLoad()
self.arView.environment.background = .color(.black)
self.arView.cameraMode = .nonAR
self.camera.position.z = 9
let positions: [simd_float3] = [[ 0.00, 0.00, 0.00],
[ 0.52, 0.90, 0.00],
[-0.52, 0.90, 0.00]]
var descriptor = MeshDescriptor(name: "Hexagon's side")
descriptor.materials = .perFace(self.indices)
descriptor.primitives = .triangles(self.indices)
descriptor.positions = MeshBuffers.Positions(positions[0...2])
var material = UnlitMaterial()
let mesh: MeshResource = try! .generate(from: [descriptor])
let colors: [UIColor] = [.systemRed, .systemGreen, .yellow,
.systemTeal, .cyan, .magenta]
for i in 0...5 {
material.color = .init(tint: colors[i], texture: nil)
let triangleModel = ModelEntity(mesh: mesh,
materials: [material])
let trianglePivot = Entity() // made to control pivot point
trianglePivot.addChild(triangleModel)
trianglePivot.orientation = simd_quatf(angle: -.pi/3 * Float(i),
axis: [0,0,1])
self.anchor.addChild(trianglePivot)
}
self.anchor.addChild(self.camera)
self.arView.scene.anchors.append(self.anchor)
}
}
There are a few problems with the code as it stands. Firstly, as pointed out in the comments, the parametric equation for the translations needs to be rotated by 90 degrees:
let deg: Float = (Float(i) * 60) - 90.0
The next issue is that the centre of the bounding box of the triangle and the centroid of the triangle are not the same point. This is important because the parametric equation calculates where the centroids of the triangles must be located, not the centres of their bounding boxes. So we're going to need a way to calculate the centroid. This can be done by adding the following extension method to TrianglePlane:
extension TrianglePlane {
/// Calculates the centroid of the triangle
func centroid() -> CGPoint
{
let max: CGFloat = self.size
let min: CGFloat = 0
let peak = UIBezierPath.middlePeak(height: max)
let xAvg = (min + max / CGFloat(2.0) + max) / CGFloat(3.0)
let yAvg = (min + peak + min) / CGFloat(3.0)
return CGPoint(x: xAvg, y: yAvg)
}
}
This allows the correct radius for the parametric equation to be calculated:
let height = Float(UIBezierPath.middlePeak(height: tri.size))
let centroid = tri.centroid()
let radius = height - Float(centroid.y)
The final correction is to calculate the offset between the origin of the triangle and the centroid. This correction depends on whether the triangle has been flipped by the rotation or not:
let x1 = radius * cos(radians)
let y1 = radius * sin(radians)
let dx = Float(-centroid.x)
let dy = (i % 2 == 0) ? Float(centroid.y) - height : Float(-centroid.y)
tri.position.x = x1 + dx
tri.position.y = y1 + dy
Putting all this together gives the desired result.
Full working ViewController can be found int this gist
Note the code can be greatly simplified by making the origin of the triangle be the centroid.

How to rotate SKLabelNode in company with its parent SKShapeNode?

When press the screen, balls created with the following codes:
var n = 1
func addBall(_ x:CGFloat){
let numShape = SKShapeNode(circleOfRadius: 30)
numShape.name = "ball"
numShape.position = CGPoint(x: x, y: frame.maxY-40)
numShape.physicsBody = SKPhysicsBody(circleOfRadius: 30)
numShape.fillColor = SKColor.white
numShape.physicsBody?.density = 0.1
numShape.physicsBody?.affectedByGravity = true
numShape.physicsBody?.friction = 0;
numShape.physicsBody?.restitution = 0.6
numShape.physicsBody?.allowsRotation = true
numShape.physicsBody?.isDynamic = true
let numLabel = SKLabelNode(fontNamed: "Helvetica")
numLabel.text = "\(n)"
numLabel.name = "\(n)"
numLabel.fontColor = .red
numLabel.position = CGPoint(x: 0, y: 0)
numLabel.verticalAlignmentMode = .center
numShape.addChild(numLabel)
self.addChild(numShape)
n += 1
}
The balls can rotate, but their childNode numlabel don't rotate in company with them. I try to update their zRotation like below:
override func update(_ currentTime: TimeInterval) {
self.enumerateChildNodes(withName: "ball") {
node, stop in
if (node is SKShapeNode) {
let ball = node as! SKShapeNode
let num = ball.children[0]
num.zRotation = ball.zRotation
}
}
}
They still refuse to rotate. If I change zRotation directly like num.zRotation = 2, they work.
How can I make them rotate in company with SKShapeNode?
Thx.
You need to set your friction to a number other than 0.
Also, concerning the shape node performance:
look at the draw count at the bottom of 50 shapes:
for _ in 1...50 {
let x = arc4random_uniform(UInt32(frame.maxX));
let xx = CGFloat(x) - size.width/4
let y = arc4random_uniform(UInt32(frame.maxY))
let yy = CGFloat(y) - size.width/4
let shape = SKShapeNode(circleOfRadius: 50)
shape.strokeColor = .blue
shape.lineWidth = 1
shape.position = CGPoint(x: xx, y: yy)
addChild(shape)
}
But now compare that to this image of only 2 draws with only a few lines of refactoring:
func addFiftySprites() {
let shape = SKShapeNode(circleOfRadius: 50)
shape.strokeColor = .blue
shape.lineWidth = 1
let texture = SKView().texture(from: shape)
for _ in 1...50 {
let x = arc4random_uniform(UInt32(frame.maxX));
let xx = CGFloat(x) - size.width/4
let y = arc4random_uniform(UInt32(frame.maxY))
let yy = CGFloat(y) - size.width/4
let sprite = SKSpriteNode(texture: texture)
sprite.position = CGPoint(x: xx, y: yy)
addChild(sprite)
}
}
The magic here is using let texture = SKView().texture(from: <SKNode>) to convert the shape to a sprite :) let sprite = SKSpriteNode(texture: texture)

Fill screen with nodes

I'm struggling to find a way to fill the screen with rows and rows of spriteNodes. I can only currently get one solid line like so. Picture of simulator
Bellow is my code, any help with be greatly appreciated.
let world = SKNode()
let ground = Ground()
override func didMoveToView(view: SKView) {
/* Setup your scene here */
self.backgroundColor = UIColor(red: 0.4, green: 0.6, blue: 0.95, alpha: 1)
self.addChild(world)
var groundPosition = CGPoint(x: -self.size.width, y: 600)
let groundSize = CGSize(width: self.size.width * 3, height: 0)
ground.spawn(world, position: groundPosition, size: groundSize)
}
class Ground: SKSpriteNode, GameSprite{
var textureAtlas: SKTextureAtlas = SKTextureAtlas(named: "ground.atlas")
// Property named groundTexture to store current ground texture:
var groundTexture:SKTexture?
func spawn(parentNode: SKNode, position: CGPoint , size: CGSize) {
parentNode.addChild(self)
self.size = size
self.position = position
self.anchorPoint = CGPointMake(0, 1)
//default to the the mud texture
if groundTexture == nil {
groundTexture = textureAtlas.textureNamed("grass")
}
// Repeate the texture
createChildren()
}
//Builds nodes to repeate the ground
func createChildren() {
if var texture = groundTexture{
var tileCount:CGFloat = 0
let textureSize = texture.size()
let tileSize = CGSize(width: 25, height: 15)
// Build nodes until we cover the screen
while tileCount * tileSize.width < self.size.width{
// randomly chooses the texture that will be used
let random = arc4random_uniform(50)
if random < 25{
texture = textureAtlas.textureNamed("grass")
}else{
texture = textureAtlas.textureNamed("mud")
}
let tileNode = SKSpriteNode(texture: texture)
tileNode.size = tileSize
tileNode.position.x = tileCount * tileSize.width
tileNode.anchorPoint = CGPoint(x: 0, y: 1)
self.addChild(tileNode)
tileCount++
}
}
}

Resources