How do I use an NSImage as material of an SCNGeometry shape correctly? - swift

I got a task in the university to add a picture as texture to the SCNGeometry Octahedron. It's my first project in Swift.
There are lot's of advices for the UIKit with UIImage class, but I'm using AppKit for macos and NSImage class. And none of the options I found in the web haven't worked for me yet. Probably I misunderstand something fundamental.
Well, firstly I dragndroped a picture named "sims.jpg" to my project folder and to art.scnassets folder. And also added them with File → Add files to "art.scnassets" and general folder. And did nothing with Assets.xcassets.
Then here is how the shape is created:
func createOctahedron() {
let vertices: [SCNVector3] = [
SCNVector3(0, 1, 0),
SCNVector3(-0.5, 0, 0.5),
SCNVector3(0.5, 0, 0.5),
SCNVector3(0.5, 0, -0.5),
SCNVector3(-0.5, 0, -0.5),
SCNVector3(0, -1, 0)
]
let source = SCNGeometrySource(vertices: vertices)
let indices: [UInt16] = [
0, 1, 2,
2, 3, 0,
3, 4, 0,
4, 1, 0,
1, 5, 2,
2, 5, 3,
3, 5, 4,
4, 5, 1
]
let element = SCNGeometryElement(indices: indices, primitiveType: .triangles)
let geometry = SCNGeometry(sources: [source], elements: [element])
let node = SCNNode(geometry: geometry)
node.geometry?.firstMaterial?.diffuse.contents = NSColor.green // назначаем цвет октаэдру
let scnView = self.view as! SCNView
scnView.scene?.rootNode.addChildNode(node)
let rotateAction = SCNAction.repeatForever(SCNAction.rotateBy(x: 0, y: .pi, z: 0, duration: 5))
node.runAction(rotateAction)
}
Just in case let me left a full code
So, I would add the image like this
let imageMaterial = SCNMaterial()
let image = NSImage.Name("sims")
imageMaterial.diffuse.contents = image
geometry.materials = [imageMaterial, imageMaterial, imageMaterial, imageMaterial, imageMaterial, imageMaterial, imageMaterial, imageMaterial]
Or maybe like that?
node.geometry?.firstMaterial?.diffuse.contents = NSImage.Name("sims")
Or should I additionally map it somehow? Help me please because I really do not get it. Xcode outputs just a rotating octahedron with no additional texture, no errors either

This line here:
let image = NSImage.Name("sims")
Only declares the name. You need:
let image = NSImage(named: NSImage.Name("sims"))
The reason your code compiles is that the contents property has a type of Any? so you can put any old goo in the property. It fails silently.

I found out what the deal was about. There was no texture appearing because of no UV mapping. I should have declared an array of 2d coordinates on the segment [0, 1] with the help of CGPoint and then add it to the source array.
Here is how it roughly looks like:
let width:CGFloat = 1/8
let coordinates: [CGPoint] = [
CGPoint(x: 0, y: 0),// BAC
CGPoint(x: width, y: 0),
CGPoint(x: width, y: 1),
CGPoint(x: 0, y: 1),// BAE
CGPoint(x: width * 7, y: 0),
CGPoint(x: 1, y: 0),
CGPoint(x: 1, y: 1),// EDA
CGPoint(x: width * 7, y: 1),
CGPoint(x: width * 6, y: 0),
CGPoint(x: width * 7, y: 0),// DAC
CGPoint(x: width, y: 0),
CGPoint(x: width * 4, y: 0),
CGPoint(x: width * 7, y: 1),// DFC
CGPoint(x: width * 6, y: 1),
CGPoint(x: width * 4, y: 1),
CGPoint(x: width, y: 1),// BFC
CGPoint(x: width * 4, y: 0),
CGPoint(x: width * 5, y: 0),
CGPoint(x: width * 5, y: 1),// BFE
CGPoint(x: width * 6, y: 1),
CGPoint(x: width * 5, y: 0),
CGPoint(x: width * 6, y: 0),// EFD
CGPoint(x: width * 4, y: 1),
CGPoint(x: width * 5, y: 1),
]
let source = [
SCNGeometrySource(vertices: vertices),
SCNGeometrySource(textureCoordinates: coordinates)
]
let element = SCNGeometryElement(indices: indices, primitiveType: .triangles)
let octahedron = SCNGeometry(sources: source, elements: [element])
After we have specified the coordinates it's enough to wright the following:
let image = NSImage(named: NSImage.Name("sims"))
octahedron.firstMaterial?.diffuse.contents = image
And the image will be stretched on faces of our octahedron.
The full code attaching

Related

Applying a transformation to UIBezierPath makes drawing disappear

maybe this is a stupid question, but I'm trying to apply a transformation to a path as simple as this:
let path = UIBezierPath(rect: CGRect(x: 10, y: 10, width: 20, height: 20))
path.apply(CGAffineTransform(rotationAngle: 2.0))
"colorLiteral".setFill()
path.fill()
And seems like the fact of adding the "apply" line, completely erases the shape on the view, whereas if I don't include that line, the shape appears on its place correctly.
I'm quite new at Swift so I guess I'm missing an important step on this?
Thanks all!
Try this and remember rotationAngle takes Radians.
let bounds = CGRect(x: 10, y: 10, width: 20, height: 20)
let path = UIBezierPath(rect: bounds)
path.apply(CGAffineTransform(translationX: -bounds.midX, y: -bounds.midY))
path.apply(CGAffineTransform(rotationAngle: CGFloat.pi / 4))
path.apply(CGAffineTransform(translationX: bounds.midX, y: bounds.midY))
Using CATransform3D
shapeLayer.transform = CATransform3DMakeScale(1, -1, 1)
Transforming path,
let shapeBounds = shapeLayer.bounds
let mirror = CGAffineTransform(scaleX: 1,
y: -1)
let translate = CGAffineTransform(translationX: 0,
y: shapeBounds.size.height)
let concatenated = mirror.concatenating(translate)
bezierPath.apply(concatenated)
shapeLayer.path = bezierPath.cgPath
Transforming layer,
let shapeFrame = CGRect(x: -120, y: 120, width: 350, height: 350)
let mirrorUpsideDown = CGAffineTransform(scaleX: 1,
y: -1)
shapeLayer.setAffineTransform(mirrorUpsideDown)
shapeLayer.frame = shapeFrame

SCNVector4: Rotate on x-axis

I have a sphere and managed to rotate it. But unfortunately along the wrong axis.
My goal is a rotation like the earth along the x axis. Can you help me to apply this?
Here is my existing code:
let spin = CABasicAnimation(keyPath: "rotation")
// Use from-to to explicitly make a full rotation around z
spin.fromValue = NSValue(scnVector4: SCNVector4(x: 1, y: 0, z: 0, w: 0))
spin.toValue = NSValue(scnVector4: SCNVector4(x: 1, y: 0, z: 0, w: Float(CGFloat(-2 * Double.pi))))
spin.duration = 30
spin.repeatCount = .infinity
sphereNode.addAnimation(spin, forKey: "spin around")
It will be something like this
self.imageView.layer.transform = CATransform3DConcat(self.imageView.layer.transform, CATransform3DMakeRotation(M_PI,1.0,0.0,0.0));
You're already have working solution:
spin.fromValue = NSValue(scnVector4: SCNVector4(x: 1, y: 0, z: 0, w: 0))
spin.toValue = NSValue(scnVector4: SCNVector4(x: 1, y: 0, z: 0, w: CGFloat.pi * 2))
If you wanna change direction, just change vectors :)
spin.fromValue = NSValue(scnVector4: SCNVector4(x: 1, y: 0, z: 0, w: CGFloat.pi * 2))
spin.toValue = NSValue(scnVector4: SCNVector4(x: 1, y: 0, z: 0, w: 0))
You can rotate your Earth model about X-axis using SceneKit's Transaction:
let scene = SCNScene(named: "art.scnassets/model.scn")!
let modelEarth = scene.rootNode.childNode(withName: "model",
recursively: true)!
SCNTransaction.begin()
SCNTransaction.animationDuration = 500 // animation in seconds
SCNTransaction.animationTimingFunction = .init(name: .default)
modelEarth.rotation.x = 1
modelEarth.rotation.y = 0
modelEarth.rotation.z = 0
modelEarth.rotation.w = 100 * Float.pi // fourth component
SCNTransaction.commit()

Add a border/shadow effect to UIView

I've managed to draw a speech bubble with following code:
override func draw(_ rect: CGRect) {
let rounding:CGFloat = rect.width * 0.01
//Draw the main frame
let bubbleFrame = CGRect(x: 0, y: 0, width: rect.width, height: rect.height * 6 / 7)
let bubblePath = UIBezierPath(roundedRect: bubbleFrame,
byRoundingCorners: UIRectCorner.allCorners,
cornerRadii: CGSize(width: rounding, height: rounding))
//Color the bubbleFrame
color.setStroke()
color.setFill()
bubblePath.stroke()
bubblePath.fill()
//Add the point
let context = UIGraphicsGetCurrentContext()
//Start the line
context!.beginPath()
context?.move(to: CGPoint(x: bubbleFrame.minX + bubbleFrame.width * 1/2 - 8, y: bubbleFrame.maxY))
//Draw a rounded point
context?.addArc(tangent1End: CGPoint(x: rect.maxX * 1/2 + 4, y: rect.maxY), tangent2End: CGPoint(x:bubbleFrame.maxX , y: bubbleFrame.minY), radius: 0)
//Close the line
context?.addLine(to: CGPoint(x: bubbleFrame.minX + bubbleFrame.width * 1/2 + 16, y: bubbleFrame.maxY))
context!.closePath()
//fill the color
context?.setFillColor(UIColor.white.cgColor)
context?.fillPath()
context?.saveGState()
context?.setShadow(offset:CGSize(width: 1, height: 1), blur: 2, color: UIColor.gray.cgColor)
UIColor.gray.setStroke()
bubblePath.lineWidth = 0.5
bubblePath.stroke()
context?.restoreGState()
}
At the moment, it looks like this:
I seem to be having trouble adding a shadow effect to the speech bubble. I want the point at the bottom to have the shadow effect, no just the rectangle above. Also, the shadow/border at the top is too thick. This is the end result I'm looking to achieve:

How to set physics body for a sprite with rounded corners

I have created a SKShapeNode in the following way,
let sprite = SKShapeNode(rect: CGRect(x: -20, y: -10, width: 40, height: 20), cornerRadius: 10)
I also set a physics body like so,
sprite.physicsBody = SKPhysicsBody(rectangleOf: sprite.frame.size)
but I realized that the collisions and contacts weren't precise, so I changed showsPhysics to true and got the following. How can I make the physics body precise, even for the rounded corners?
Remember that too much precision is very bad for performance. Read this:
https://developer.apple.com/reference/spritekit/skphysicsbody
For general cases, it would be better to ignore the corners and stick with the normal rectangle physics, unless you REALLY need that precision. If you do, you could use a SKTexture on a SKSpriteNode instead of a ShapeNode. That would be much easier, because you could simply do this:
let size = CGSize(width: 100, height: 50)
let roundedRectangleTexture = SKTexture(image: UIImage(named: "textureImage")!)
let mySpriteNode = SKSpriteNode(texture: roundedRectangleTexture, size: size)
mySpriteNode.physicsBody = SKPhysicsBody(texture: roundedRectangleTexture, size: size)
With a texture, you can easily make a more detailed and precise physicsBody by simply using the SKPhysicsBody(texture: size:) constructor.
From the documentation:
Maybe the #4 is your case
let spaceShipTexture = SKTexture(imageNamed: "spaceShip.png")
Spaceship 1: circular physics body
let circularSpaceShip = SKSpriteNode(texture: spaceShipTexture)
circularSpaceShip.physicsBody = SKPhysicsBody(circleOfRadius: max(circularSpaceShip.size.width / 2,
circularSpaceShip.size.height / 2))
Spaceship 2: rectangular physics body
let rectangularSpaceShip = SKSpriteNode(texture: spaceShipTexture)
rectangularSpaceShip.physicsBody = SKPhysicsBody(rectangleOf: CGSize(width: circularSpaceShip.size.width,
height: circularSpaceShip.size.height))
Spaceship 3: polygonal physics body
let polygonalSpaceShip = SKSpriteNode(texture: spaceShipTexture)
let path = CGMutablePath()
path.addLines(between: [CGPoint(x: -5, y: 37), CGPoint(x: 5, y: 37), CGPoint(x: 10, y: 20),
CGPoint(x: 56, y: -5), CGPoint(x: 37, y: -35), CGPoint(x: 15, y: -30),
CGPoint(x: 12, y: -37), CGPoint(x: -12, y: -37), CGPoint(x: -15, y: -30),
CGPoint(x: -37, y: -35), CGPoint(x: -56, y: -5), CGPoint(x: -10, y: 20),
CGPoint(x: -5, y: 37)])
path.closeSubpath()
polygonalSpaceShip.physicsBody = SKPhysicsBody(polygonFrom: path)
Spaceship 4: physics body using texture’s alpha channel
let texturedSpaceShip = SKSpriteNode(texture: spaceShipTexture)
texturedSpaceShip.physicsBody = SKPhysicsBody(texture: spaceShipTexture,
size: CGSize(width: circularSpaceShip.size.width,
height: circularSpaceShip.size.height))
I would follow the documentation of SKPhysicsBody, and create a polygonal shape that approximates the rounded corners of your sprite.
An example on the documentation shows that you can define a polygonal physics body like so:
// Spaceship 3: polygonal physics body
let polygonalSpaceShip = SKSpriteNode(texture: spaceShipTexture)
let path = CGMutablePath()
path.addLines(between: [CGPoint(x: -5, y: 37), CGPoint(x: 5, y: 37), CGPoint(x: 10, y: 20),
CGPoint(x: 56, y: -5), CGPoint(x: 37, y: -35), CGPoint(x: 15, y: -30),
CGPoint(x: 12, y: -37), CGPoint(x: -12, y: -37), CGPoint(x: -15, y: -30),
CGPoint(x: -37, y: -35), CGPoint(x: -56, y: -5), CGPoint(x: -10, y: 20),
CGPoint(x: -5, y: 37)])
path.closeSubpath()
polygonalSpaceShip.physicsBody = SKPhysicsBody(polygonFrom: path)

SceneKit custom geometry produces “double not supported” / “invalid vertex format” runtime error

I don't understand what's wrong with the following code:
class Terrain {
private class func createGeometry () -> SCNGeometry {
let sources = [
SCNGeometrySource(vertices:[
SCNVector3(x: -1.0, y: -1.0, z: 0.0),
SCNVector3(x: -1.0, y: 1.0, z: 0.0),
SCNVector3(x: 1.0, y: 1.0, z: 0.0),
SCNVector3(x: 1.0, y: -1.0, z: 0.0)], count:4),
SCNGeometrySource(normals:[
SCNVector3(x: 0.0, y: 0.0, z: -1.0),
SCNVector3(x: 0.0, y: 0.0, z: -1.0),
SCNVector3(x: 0.0, y: 0.0, z: -1.0),
SCNVector3(x: 0.0, y: 0.0, z: -1.0)], count:4),
SCNGeometrySource(textureCoordinates:[
CGPoint(x: 0.0, y: 0.0),
CGPoint(x: 0.0, y: 1.0),
CGPoint(x: 1.0, y: 1.0),
CGPoint(x: 1.0, y: 0.0)], count:4)
]
let elements = [
SCNGeometryElement(indices: [0, 2, 3, 0, 1, 2], primitiveType: .Triangles)
]
let geo = SCNGeometry(sources:sources, elements:elements)
let mat = SCNMaterial()
mat.diffuse.contents = UIColor.redColor()
mat.doubleSided = true
geo.materials = [mat, mat]
return geo
}
class func createNode () -> SCNNode {
let node = SCNNode(geometry: createGeometry())
node.name = "Terrain"
node.position = SCNVector3()
return node
}
}
I use it as follows:
let terrain = Terrain.createNode()
sceneView.scene?.rootNode.addChildNode(terrain)
But get:
2016-01-19 22:21:17.600 SceneKit: error, C3DRendererContextSetupResidentMeshSourceAtLocation - double not supported
2016-01-19 22:21:17.601 SceneKit: error, C3DSourceAccessorToVertexFormat - invalid vertex format
/BuildRoot/Library/Caches/com.apple.xbs/Sources/Metal/Metal-55.2.6.1/Framework/MTLVertexDescriptor.mm:761: failed assertion `Unused buffer at index 18.'
The issue is that the geometry is expecting float components but you’re giving it doubles—CGPoint’s components are CGFloat values, which are typedef’d to double on 64-bit systems. Unfortunately, the SCNGeometrySource …textureCoordinates: initializer insists on CGPoints, so you can’t use that; the workaround I found was to create an NSData wrapping an array of SIMD float vectors, then use the much longer data:semantic:etc: initializer to consume the data. Something like this should do the trick:
let coordinates = [float2(0, 0), float2(0, 1), float2(1, 1), float2(1, 0)]
let coordinateData = NSData(bytes:coordinates, length:4 * sizeof(float2))
let coordinateSource = SCNGeometrySource(data: coordinateData,
semantic: SCNGeometrySourceSemanticTexcoord,
vectorCount: 4,
floatComponents: true,
componentsPerVector: 2,
bytesPerComponent: sizeof(Float),
dataOffset: 0,
dataStride: sizeof(float2))