Combining 3D and 2D in macOS - swift

I need to draw 2D legends, 2D frame, etc. on top of a 3D scene. Here, I am trying to display a 2D label on top of a 3D sphere, so that the label will always face the user, even if they rotate the scene.
I am using a SCNView to draw the 3D scene (=> works fine)
I am using its NSView parent (subclassed to override its draw function) to display the 2D text (=> nothing is displayed)
(var twoDCoordinates:CGPoint is extern)
in my viewController:
#IBOutlet weak var sceneView: SCNView!
func sceneSetup() {
let scene = SCNScene()
// (I set the light and the camera...)
// Adding the sphere
let geometry = SCNSphere (radius: 4.0)
geometry.firstMaterial!.diffuse.contents = CGColor.red
geometry.firstMaterial!.specular.contents = CGColor.white
sphereNode = SCNNode (geometry: geometry)
sphereNode.position = SCNVector3Make(-6, 0, 0)
scene.rootNode.addChildNode (sphereNode)
sceneView.scene = scene
sceneView.allowsCameraControl = true
// compute the 2D coordinates of the text to be displayed
var q = sphereNode.position
q.x += 6.0; q.y += 6.0; q.z += 6.0 // outside of the sphere
let projected = sceneView.projectPoint (q)
twoDCoordinates = CGPoint (x: projected.x, y: projected.y)
sceneView.needsDisplay = true
}
=> The 3D sphere is correctly displayed; twoDCoordinates is correct
In TwoDView:NSView class (which is the sceneView container)
override func draw(_ dirtyRect: NSRect) {
super.draw(dirtyRect)
// Drawing code here.
let paragraphStyle = NSMutableParagraphStyle()
paragraphStyle.lineBreakMode = .byTruncatingTail
paragraphStyle.alignment = .left
let textAttributes = [
NSAttributedString.Key.font: NSFont.systemFont(ofSize: 72.0),
NSAttributedString.Key.paragraphStyle: paragraphStyle]
let label = "Sphere Label"
let size = label (withAttributes: textAttributes)
let rect = CGRect(x: twoDCoordinates.x, y: twoDCoordinates.y, width: size.width, height: size.height)
label.draw(in: rect, withAttributes: textAttributes)
}
=> the function draw is called, but no text is displayed.
I expect the text "Sphere label" to be displayed next to the 3D sphere, but only the 3D scene is displayed., even though function draw is called.

Related

How to show texture inside USDZ model's frame?

I'm using below code to show image in .usdz model image frame
var imageMaterial = UnlitMaterial()
imageMaterial.color = .init(tint: .white, texture: .init(texture))
modelEntity.model?.materials = [imageMaterial]
anchorEntity.addChild(modelEntity)
The issue is that image is showing on frame border as well but we need to show in image inside frame not on frame border.
Frame.usdz file:
Frame after adding image:
If the frame and its canvas is a single object (so called mono-model), then you cannot apply a texture to just the surface of the canvas. You need the canvas to be a separate part. The simplest solution in this case would be to create a Plane primitive and apply a texture to it. Then transform the plane as needed and attach it to the same anchor.
import UIKit
import RealityKit
class ViewController: UIViewController {
#IBOutlet var arView: ARView!
override func viewDidLoad() {
super.viewDidLoad()
let frameEntity = try! Entity.loadModel(named: "frame.usdz")
let canvas: MeshResource = .generatePlane(width: 0.3, height: 0.4)
var material = SimpleMaterial()
material.color = try! .init(texture: .init(.load(named: "canvas")))
let canvasEntity = ModelEntity(mesh: canvas, materials: [material])
canvasEntity.position = [0.3, 0.1, 0.0]
canvasEntity.scale *= 1.25
let anchorEntity = AnchorEntity()
anchorEntity.addChild(frameEntity)
anchorEntity.addChild(canvasEntity)
arView.scene.anchors.append(anchorEntity)
}
}

My SKTexture is size wrong for my SKSpriteNode

I have a 200 x 100 box-like menu button SKSpriteNode declared below.
menuBtn.size = CGSize(width: scrWidth * 1.5, height: 100)
menuBtn.texture = SKTexture(image: UIImage(named: "menuBtn")!)
The problem is, the hitbox for the button is still right, and the width of the SKTexture is fine, but the height of the SKTexture is around 1/2 the size of the actual menuBtn. It is a png, and I've checked and there's no clear textures around the sprite (just the transparent png ones), what am I doing wrong?
Pic of button: https://imgur.com/a/8BtXGLC
Pic of button in App: https://imgur.com/a/ZfW3xDL
Pic of how I want it to look: https://imgur.com/a/2zlSv8y
The texture's png image size is 1044x1044 if that has any impact.
First of all, make sure you are sending the correct size to your scene from GameViewController to your scene, in this example GameScene.
// without .sks
class GameViewController: UIViewController
{
override func viewDidLoad()
{
super.viewDidLoad()
// ...
if let view = self.view as! SKView?
{
let scene = GameScene()
// Set the scale mode to scale to fit the window
scene.scaleMode = .aspectFit
// Set anchorPoint and pass scene.size
scene.anchorPoint = CGPoint(x: 0.5, y: 0.5)
scene.size = view.bounds.size
// Present the scene
view.presentScene(scene)
}
// ...
}
// with .sks
class GameViewController: UIViewController
{
override func viewDidLoad()
{
super.viewDidLoad()
// ...
if let view = self.view as! SKView?
{
if let scene = SKScene(fileNamed: "GameScene.sks")
{
// Set the scale mode to scale to fit the window
scene.scaleMode = .aspectFit
// Pass scene.size
scene.size = view.bounds.size
// Present the scene
view.presentScene(scene)
}
}
// ...
}
In your GameScene create the object. I recommend working in screenWidth and screenHeight when you create objects so they scale to all iOS devices.
class GameScene: SKScene
{
override func didMove(to view: SKView)
{
// ...
// scene width / height
let sceneWidth = size.width
let sceneHeight = size.height
// object
let menu : SKSpriteNode = SKSpriteNode()
menu.texture = SKTexture(imageNamed: "menu")
menu.size = CGSize(width: sceneWidth * 0.75, height: 100)
let y_pos : CGFloat = -sceneHeight * 0.5 + menu.size.height * 0.5
menu.position = CGPoint(x: 0, y: y_pos)
menu.zPosition = 1
addChild(menu)
// ...
}
}
The problem I had was that there were extra transparent pixels in my base image, all I had to do was remove them with GIMP.

Orbit Scenekit camera around specific node

I'm having a difficult time trying to figure out how to get my SceneKit camera to orbit around a specific node in my game.
If I have a single node (a ship) and a camera in my scene everything works fine. If I add an additional node (a planet) the cameras pivot point appears to change from my ship to a space between my ship and planet.
Things I've tried:
Setting a lookat constraint on my camera (set to the ship)
Settingcamera position to my ship (it will move but the pivot point
still seems to be between the two objects)
Changing the cameras pivot point
example:
class TestSceneViewController: UIViewController, SCNSceneRendererDelegate {
var scnView: SCNView = SCNView()
var scnScene: SCNScene!
var cameraNode: SCNNode!
var ship: SCNNode!
override func viewDidLoad() {
super.viewDidLoad()
setupView()
setupScene()
setupCamera()
...
func setupView() {
// scnView = self.view as! SCNView
// retrieve the SCNView
scnView = SCNView(frame: view.frame)
scnView.showsStatistics = true
view.addSubview(scnView)
scnView.allowsCameraControl = true
scnView.defaultCameraController.interactionMode = .orbitTurntable
scnView.defaultCameraController.inertiaEnabled = true
scnView.delegate = self
scnView.isPlaying = true
scnView.loops = true
}
func setupScene () {
scnScene = SCNScene()
scnView.scene = scnScene
let ships = SCNScene(named: "art.scnassets/simpleshuttle3.scn")
ship = ships!.rootNode.childNode(withName: "ship", recursively: true)
ship?.position = SCNVector3(x: 0, y: 0, z: 0)
scnScene.rootNode.addChildNode(ship!)
let planets = SCNScene(named: "art.scnassets/sphere.scn")!
if let planet = planets.rootNode.childNode(withName: "Ball", recursively: true){
planet.position = SCNVector3(x: 0, y: 0, z: 40)
scnScene.rootNode.addChildNode(planet)
}
}
func setupCamera() {
cameraNode = SCNNode()
cameraNode.camera = SCNCamera()
cameraNode.position = SCNVector3(x: ship.position.x, y: ship.position.y, z: 80)
cameraNode.camera?.motionBlurIntensity = 1.0
cameraNode.camera?.automaticallyAdjustsZRange = true
scnScene.rootNode.addChildNode(cameraNode)
}
You are enabling the manual camera control.
scnView.allowsCameraControl = true
If you want to use e.g. SCNLookAtConstraint you have to disable that. Otherwise you have a conflict. The camera is not supposed to point at a certain position while simultaneously being rotated by the user.
If you want to stay with the default camera controller you can create an additional SCNNode as the parent for your planet and the ships. This parent node can now be moved so that the pivot point is at your desired position.

how to scale the window to fit in all devices?

I want to know how can I scale the window to fit with the same size in all diveces. The problem is that in some diveces the objects doesnt cover the same space of how I want.
I have my scaleMode = .ResizeFill but the problem is that if I make it .AspectFill it doesnt appear in the correct place. I think that the problem is that I added a new container on the scene, but I dont know how to solve it.
class GameViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
if let scene = GameScene(fileNamed:"GameScene") {
// Configure the view.
let skView = self.view as! SKView
skView.showsFPS = true
skView.showsNodeCount = true
/* Sprite Kit applies additional optimizations to improve rendering performance */
skView.ignoresSiblingOrder = true
/* Set the scale mode to scale to fit the window */
scene.scaleMode = .ResizeFill
scene.position = view.center
skView.presentScene(scene)
}
}
override func didMoveToView(view: SKView) {
/* Setup your scene here */
scaleMode = .ResizeFill
node.position = view.center
// 3) Add the container to the scene
//addChild(node)
// 4) Set the sprite's x position
sprite.position = CGPointMake(radius, 0)
// 5) Add the sprite to the container
node.addChild(sprite)
// 6) Rotate the container
rotate()
sprite.color = UIColor.whiteColor()
sprite.colorBlendFactor = 1.0
sprite.zPosition = 4.0
circulo = SKSpriteNode(imageNamed: "circuloFondo2")
circulo.size = CGSize(width: 294, height: 294)
circulo.color = UIColor(red: 0.15, green: 0.15, blue: 0.15, alpha: 1)
circulo.colorBlendFactor = 1
circulo.alpha = 0.35
circulo.position = view.center
self.addChild(circulo)
circulo.zPosition = 1
}
The issue might be in code where you draw circle,You might be drawing
the circle with same radius for all screen sizes.
Instead of drawing the circle with the same radius, you need to
provide dynamic radius of the circle according to the device width.
Replace this
circulo.size = CGSize(width: 294, height: 294)
Use following snippet
let padding:CGFloat = 40.0
circulo.size = CGSize(width:view.frame.size.width - padding , height: view.frame.size.width - padding)

Cut a hole on SKNode and update its physicsBody in Swift

I'm creating an app which will contains some holes on the image.
Since the code is very big I've created this simple code which has the same idea.
This code has an image with it's physicsBody set already.
What I would like is in the touchesBegan function to draw some transparent circles at the touched location and update the image physicsBody (making a hole on the image).
I've found several codes in objective C and UIImage, can someone help with Swift and SKSpriteNode?
import SpriteKit
class GameScene: SKScene {
override func didMoveToView(view: SKView) {
let texture = SKTexture(imageNamed: "Icon.png")
let node = SKSpriteNode(texture: texture)
node.position = CGPointMake(CGRectGetMidX(self.frame), CGRectGetMidY(self.frame))
addChild(node)
node.physicsBody = SKPhysicsBody(rectangleOfSize: texture.size())
node.physicsBody?.dynamic = false
}
override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
}
}
One option is to apply a mask and render that to an image and update your SKSpriteNode's texture. Then use that texture to determine the physics body. This process however will not be very performant.
For example in touchesBegan you can say something like:
override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
guard let touch = touches.first else { return }
if let node = self.nodeAtPoint(touch.locationInNode(self.scene!)) as? SKSpriteNode{
let layer = self.layerFor(touch, node: node) // We'll use a helper function to create the layer
let image = self.snapShotLayer(layer) // Helper function to snapshot the layer
let texture = SKTexture(image:image)
node.texture = texture
// This will map the physical bounds of the body to alpha channel of texture. Hit in performance
node.physicsBody = SKPhysicsBody(texture: node.texture!, size: node.size)
}
}
Here we just get the node and look at the first touch, you could generalize to allow multi touch, but we then create a layer with the new image with transparency, then create a texture out of it, then update the node's physics body.
For creating the layer you can say something like:
func layerFor(touch: UITouch, node: SKSpriteNode) -> CALayer
{
let touchDiameter:CGFloat = 20.0
let layer = CALayer()
layer.frame = CGRect(origin: CGPointZero, size: node.size)
layer.contents = node.texture?.CGImage()
let locationInNode = touch.locationInNode(node)
// Convert touch to layer coordinate system from node coordinates
let touchInLayerX = locationInNode.x + node.size.width * 0.5 - touchDiameter * 0.5
let touchInLayerY = node.size.height - (locationInNode.y + node.size.height * 0.5) - touchDiameter * 0.5
let circleRect = CGRect(x: touchInLayerX, y: touchInLayerY, width: touchDiameter, height: touchDiameter)
let circle = UIBezierPath(ovalInRect: circleRect)
let shapeLayer = CAShapeLayer()
shapeLayer.frame = CGRect(x: 0.0, y: 0.0, width: node.size.width, height: node.size.height)
let path = UIBezierPath(rect: shapeLayer.frame)
path.appendPath(circle)
shapeLayer.path = path.CGPath
shapeLayer.fillRule = kCAFillRuleEvenOdd
layer.mask = shapeLayer
return layer
}
Here we just set the current texture to the contents of a CALayer then we create a CAShapeLayer for the mask we want to create. We want the mask to be opaque for most of the layer but we want a transparent circle, so we create a path of a rectangle then add a circle to it. We set the fillRule to kCAFillRuleEvenOdd to fill everything but our circle.
Lastly, we render that layer to a UIImage that we can use to update our texture.
func snapShotLayer(layer: CALayer) -> UIImage
{
UIGraphicsBeginImageContextWithOptions(layer.frame.size, false, 0.0)
let context = UIGraphicsGetCurrentContext()
layer.renderInContext(context!)
let image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return image
}
Maybe someone will provide a more performant way of accomplishing this, but I think this will work for a lot of cases.