Model I/O importing assets - swift3

I'm trying to use Model I/O as a quicker way to render imported 3d models for my iOS app. I have written a little bit of code importing and displaying a 3d model I have made with a .obj file format. For some reason, when I run my app only a black screen appears.Here is my code:
import UIKit
import ModelIO
import SceneKit
import SceneKit.ModelIO
class ViewController: UIViewController {
var sceneView: SCNView {
return self.view as! SCNView
}
override func viewDidLoad() {
// Do any additional setup after loading the view, typically from a nib.
guard let url = Bundle.main.url(forResource: "Digestive_System", withExtension: "obj") else {
fatalError("Failed to find model")
}
//Load Object
let asset = MDLAsset(url:url)
guard let object = asset.object(at: 0) as? MDLMesh else {
fatalError("failed to get mesh from asset")
}
// Wrap Model I/O object in SceneKit object
let scene = SCNScene()
let node = SCNNode(mdlObject: object)
scene.rootNode.addChildNode(node)
//Display Scene
sceneView.autoenablesDefaultLighting = true
sceneView.allowsCameraControl = true
sceneView.scene = scene
sceneView.backgroundColor = UIColor.black
super.viewDidLoad()
}
side note: my model is located in the main folder of the app.
Thanks for your help guys!
Noa

I have rendered .Obj model using Model I/O as:
Load model from bundle
NSURL *url = [[NSBundle mainBundle] URLForResource:#"modelName" withExtension:#"obj"];
MDLAsset *asset = [[MDLAsset alloc] initWithURL:url];
MDLMesh *meshObject = (MDLMesh *)[asset objectAtIndex:0];
Apply material to model
MDLScatteringFunction *scattering = [MDLScatteringFunction new];
MDLMaterial *mdMaterial = [[MDLMaterial alloc] initWithName:#"material"
scatteringFunction:scattering];
NSURL *baseMaterialURL = [[NSBundle mainBundle]
URLForResource:#"Image_Base_Color" withExtension:#"png"];
MDLMaterialProperty *baseColor = [MDLMaterialProperty new];
[baseColor setType:MDLMaterialPropertyTypeTexture];
[baseColor setSemantic:MDLMaterialSemanticBaseColor];
[baseColour setURLValue:baseMaterialURL];
[material setProperty:baseColor];
for(MDLSubmesh *subMesh in meshObject.submeshes)
{
sub.material = material;
}
Add model to scene
SCNNode *baseModelNode = [SCNNode nodeWithMDLObject:object];
[self.sceneView.scene rootNode] addChildNode:self.baseModelNode];
You can set scale and position as per your requirement.

Related

SwiftUI: HitTest on Scenekit

Goal: SceneKit hit test with SwiftUI (instead of UIKit)
Problem: When I embed the default ship scene on a SwiftUI "UIViewRepresentable", the example handleTap function doesn't work. and I get his error:
"Argument of '#selector' refers to instance method 'handleTap' that is not exposed to Objective-C"
How an I create a hit test, and pass data to another SwiftUI view?
import SwiftUI
import SceneKit
var handleTap: (() -> Void)
struct ScenekitView : UIViewRepresentable {
let scene = SCNScene(named: "ship.scn")!
func makeUIView(context: Context) -> SCNView {
// create and add a camera to the scene
let cameraNode = SCNNode()
cameraNode.camera = SCNCamera()
scene.rootNode.addChildNode(cameraNode)
// place the camera
cameraNode.position = SCNVector3(x: 0, y: 0, z: 15)
// retrieve the ship node
let ship = scene.rootNode.childNode(withName: "ship", recursively: true)!
// retrieve the SCNView
let scnView = SCNView()
return scnView
}
func updateUIView(_ scnView: SCNView, context: Context) {
scnView.scene = scene
// allows the user to manipulate the camera
scnView.allowsCameraControl = true
// show statistics such as fps and timing information
scnView.showsStatistics = true
// configure the view
scnView.backgroundColor = UIColor.black
// add a tap gesture recognizer
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(handleTap(_:)))
scnView.addGestureRecognizer(tapGesture)
}
func handleTap(_ gestureRecognize: UIGestureRecognizer) {
// retrieve the SCNView
let scnView = SCNView()
// check what nodes are tapped
let p = gestureRecognize.location(in: scnView)
let hitResults = scnView.hitTest(p, options: [:])
// check that we clicked on at least one object
if hitResults.count > 0 {
// retrieved the first clicked object
let result = hitResults[0]
// get material for selected geometry element
let material = result.node.geometry!.firstMaterial
// highlight it
SCNTransaction.begin()
SCNTransaction.animationDuration = 0.5
// on completion - unhighlight
SCNTransaction.completionBlock = {
SCNTransaction.begin()
SCNTransaction.animationDuration = 0.5
material?.emission.contents = UIColor.black
SCNTransaction.commit()
}
material?.emission.contents = UIColor.green
SCNTransaction.commit()
}
}
}
#if DEBUG
struct ScenekitView_Previews : PreviewProvider {
static var previews: some View {
ScenekitView()
}
}
#endif
Just hit this issue myself and finally found a solution: make a dummy struct that pulls from a class that actually holds your SCNView.
This works for me:
struct ScenekitView : UIViewRepresentable {
let scenekitClass = ScenekitClass()
func makeUIView(context: Context) -> SCNView {
return scenekitClass.view
}
func updateUIView(_ scnView: SCNView, context: Context) {
// your update UI view contents look like they can all be done in the initial creation
}
}
class ScenekitClass {
let view = SCNView()
let scene = SCNScene(named: "ship.scn")!
init() {
// create and add a camera to the scene
let cameraNode = SCNNode()
cameraNode.camera = SCNCamera()
scene.rootNode.addChildNode(cameraNode)
// place the camera
cameraNode.position = SCNVector3(x: 0, y: 0, z: 15)
// attach the scene
view.scene = scene
// allows the user to manipulate the camera
view.allowsCameraControl = true
// show statistics such as fps and timing information
view.showsStatistics = true
// configure the view
view.backgroundColor = UIColor.black
// add a tap gesture recognizer
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(handleTap(_:)))
view.addGestureRecognizer(tapGesture)
}
#objc func handleTap(_ gestureRecognize: UIGestureRecognizer) {
// check what nodes are tapped
let p = gestureRecognize.location(in: view)
let hitResults = view.hitTest(p, options: [:])
// check that we clicked on at least one object
if hitResults.count > 0 {
// retrieved the first clicked object
let result = hitResults[0]
// get material for selected geometry element
let material = result.node.geometry!.firstMaterial
// highlight it
SCNTransaction.begin()
SCNTransaction.animationDuration = 0.5
// on completion - unhighlight
SCNTransaction.completionBlock = {
SCNTransaction.begin()
SCNTransaction.animationDuration = 0.5
material?.emission.contents = UIColor.black
SCNTransaction.commit()
}
material?.emission.contents = UIColor.green
SCNTransaction.commit()
}
}
}
Based on this question.
For whatever reason, the SwiftUI SceneView does not conform to the SCNSceneRenderer protocol. If it did, then it would not be necessary to make use of a UIViewRepresentable (or NSViewRepresentable for macOS) view.
I have a complete example app, for macOS, here:
https://github.com/Thunor/HitTestApp
SceneView has a delegate argument. You can use a SCNSceneRenderDelegate to capture the SCNSceneRenderer and use it for hit testing. Here's an example:
import SwiftUI
import SceneKit
import Foundation
class RenderDelegate: NSObject, SCNSceneRendererDelegate {
// dummy render delegate to capture renderer
var lastRenderer: SCNSceneRenderer!
func renderer(_ renderer: SCNSceneRenderer, updateAtTime time: TimeInterval) {
// store the renderer for hit testing
lastRenderer = renderer
}
}
class Model: ObservableObject {
let scene = SCNScene(named: "scene.usdz")!
let renderDelegate = RenderDelegate()
}
struct ContentView: View {
#ObservedObject var model = Model()
var body: some View {
SceneView(scene: model.scene, options: [.allowsCameraControl, .autoenablesDefaultLighting], delegate: model.renderDelegate)
.gesture(
SpatialTapGesture(count: 1)
.onEnded(){ event in
// hit test
guard let renderer = model.renderDelegate.lastRenderer else { return }
let hits = renderer.hitTest(event.location, options: nil)
if let tappedNode = hits.first?.node {
// do something
}
}
)
}
}

How adapt code to work with a Tab Bar Controller

I'm redoing the navigation of my app to be based on a custom UITabBarController. The tab bar opens the various ViewController. This is working fine, however Im now getting errors with the code in the ViewController that was previously working.
The new customTabBarController
import UIKit
class CustomTabBarController: UITabBarController {
override func viewDidLoad() {
super.viewDidLoad()
let feedController = feedVC() //Name of the view controller
let firstNavigationController = UINavigationController(rootViewController: feedController)
firstNavigationController.title = "Feed"
firstNavigationController.tabBarItem.image = UIImage(named: "feed_icon")
let clubController = moreVC() //Name of the view controller
let secondNavigationController = UINavigationController(rootViewController: clubController)
secondNavigationController.title = "Club"
secondNavigationController.tabBarItem.image = UIImage(named: "club_icon")
let recordController = moreVC() //Name of the view controller
let thirdNavigationController = UINavigationController(rootViewController: recordController)
thirdNavigationController.title = "Record"
thirdNavigationController.tabBarItem.image = UIImage(named: "record_icon")
let profileController = moreVC() //Name of the view controller
let fourthNavigationController = UINavigationController(rootViewController: profileController)
fourthNavigationController.title = "Profile"
fourthNavigationController.tabBarItem.image = UIImage(named: "profile_icon")
let moreController = moreVC() //Name of the view controller
let fifthNavigationController = UINavigationController(rootViewController: moreController)
fifthNavigationController.title = "More"
fifthNavigationController.tabBarItem.image = UIImage(named: "more_icon")
viewControllers = [firstNavigationController, secondNavigationController, thirdNavigationController, fourthNavigationController, fifthNavigationController]
tabBar.isTranslucent = false
// Color of menu bar set in AppDelegate.swift
}
}
feedVC
import UIKit
import Firebase
class feedVC: UIViewController {
#IBOutlet weak var tableView: UITableView!
var activityArray = [Activity]()
var userdataArray = [Userdata]()
var cellSpacingHeight: CGFloat = 10 // Sets the spacing between the cells
override func viewDidLoad() {
super.viewDidLoad()
if Auth.auth().currentUser == nil {
let authVC = self.storyboard?.instantiateViewController(withIdentifier: "authVC") as? authVC
self.present(authVC!, animated: true, completion: nil)
}
tableView.delegate = self
tableView.dataSource = self
}
The error I get is:
Thread 1: EXC_BREAKPOINT (code=1, subcode=0x102eb7b50) linked to the below code in the feedVC.
tableView.delegate = self
tableView.dataSource = self
The second error I get is related to recordVC
import UIKit
import MapKit
class recordVC: UIViewController, MKMapViewDelegate {
#IBOutlet weak var mapView: MKMapView!
var tileRenderer: MKTileOverlayRenderer!
// set initial location in Aspøya
let initialLocation = CLLocation(latitude: 63.011018, longitude: 7.914721)
// Set the zoom level of the location
let regionRadius: CLLocationDistance = 1000
func centerMapOnLocation(location: CLLocation) {
let coordinateRegion = MKCoordinateRegionMakeWithDistance(location.coordinate,
regionRadius, regionRadius)
mapView.setRegion(coordinateRegion, animated: true)
}
func setupTileRenderer() {
// Fetching the map file from URL below. The {x}, {y}, and {z} are replaced at runtime by an individual tile’s coordinate. The z-coordinate, or zoom-level is specified by how much the user has zoomed in the map. The x and y are the index of the tile given the section of the Earth shown. A tile needs to be supplied for every x and y for each zoom level supported.
let template = "https://opencache.statkart.no/gatekeeper/gk/gk.open_gmaps?layers=norgeskart_bakgrunn&zoom={z}&x={x}&y={y}&format=image/png"
// Creates the overlay
let overlay = MKTileOverlay(urlTemplate: template)
// Indicates the tiles are opaque and replace the default map tiles
overlay.canReplaceMapContent = true
// Adds the overlay to the mapView
mapView.add(overlay, level: .aboveLabels)
// Creates a tile renderer which handles the drawing of the tiles.
tileRenderer = MKTileOverlayRenderer(tileOverlay: overlay)
}
This give a similar error of: hread 1: EXC_BREAKPOINT (code=1, subcode=0x10561bb50) for this code line
// Adds the overlay to the mapView
mapView.add(overlay, level: .aboveLabels)
For the first one you have to use UITableViewDelegate and UITableViewDataSource like below :
class feedVC: UIViewController, UITableViewDelegate, UITableViewDataSource {
....
}
For the second, You have to do UI operations on main thread.
DispatchQueue.main.async {
mapView.add(overlay, level: .aboveLabels)
}

In Swift Spritekit, whenever I transition from one Scene to another, my sks file is not transfering?

Basically I have a MenuScene and a GameScene for my game. The MenuScene is first as shown in the viewDidLoad function from ViewController
override func viewDidLoad() {
super.viewDidLoad()
if let scene = MenuScene(fileNamed:"MenuScene") {
// 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 = .aspectFit
skView.presentScene(scene)
}
}
There is a simple menu screen with a play button that transfers right into the game. Here is the if-block in my touchesBegan method:
if node == playButton {
playButton.size.height=playButton.size.height+30
playButton.size.width=playButton.size.height+30
if view != nil {
let transition:SKTransition = SKTransition.fade(withDuration: 1)
let scene:SKScene = GameScene(size: self.size)
self.view?.presentScene(scene, transition: transition)
}
}
But when it transitions into my GameScene, the sks files for my GameScene doesn't render? And all the variables in my GameScene.swift that connects to the sprites in the sks file are missing. How do I fix this?
Try: let scene = SKScene(fileNamed: "GameScene")
let scene:SKScene = GameScene(size: self.size) means that you are loading an instance of type GameScene. At no point will it load any file.
SKScene(fileNamed: "GameScene") on the other hand will load a file, just be sure to check that the custom class field on your sks file says GameScene

Animating individual characters in a SKLabelNode

Is there a more efficient way to animate text shivering with typewriting all in one sklabelnode? I'm trying to achieve the effect in some games like undertale where the words appear type writer style while they are shivering at the same time.
So far I've only been able to achieve it but with such luck:
class TextEffectScene: SKScene {
var typeWriterLabel : SKLabelNode?
var shiveringText_L : SKLabelNode?
var shiveringText_O : SKLabelNode?
var shiveringText_S : SKLabelNode?
var shiveringText_E : SKLabelNode?
var shiveringText_R : SKLabelNode?
var button : SKSpriteNode?
override func sceneDidLoad() {
button = self.childNode(withName: "//button") as? SKSpriteNode
self.scaleMode = .aspectFill //Very important for ensuring that the screen sizes do not change after transitioning to other scenes
typeWriterLabel = self.childNode(withName: "//typeWriterLabel") as? SKLabelNode
shiveringText_L = self.childNode(withName: "//L") as? SKLabelNode
shiveringText_O = self.childNode(withName: "//O") as? SKLabelNode
shiveringText_S = self.childNode(withName: "//S") as? SKLabelNode
shiveringText_E = self.childNode(withName: "//E") as? SKLabelNode
shiveringText_R = self.childNode(withName: "//R") as? SKLabelNode
}
// Type writer style animation
override func didMove(to view: SKView) {
fireTyping()
shiveringText_L?.run(SKAction.repeatForever(SKAction.init(named: "shivering")!))
shiveringText_O?.run(SKAction.repeatForever(SKAction.init(named: "shivering2")!))
shiveringText_S?.run(SKAction.repeatForever(SKAction.init(named: "shivering3")!))
shiveringText_E?.run(SKAction.repeatForever(SKAction.init(named: "shivering4")!))
shiveringText_R?.run(SKAction.repeatForever(SKAction.init(named: "shivering5")!))
}
let myText = Array("You just lost the game :)".characters)
var myCounter = 0
var timer:Timer?
func fireTyping(){
typeWriterLabel?.text = ""
timer = Timer.scheduledTimer(timeInterval: 0.5, target: self, selector: #selector(TextEffectScene.typeLetter), userInfo: nil, repeats: true)
}
func typeLetter(){
if myCounter < myText.count {
typeWriterLabel?.text = (typeWriterLabel?.text!)! + String(myText[myCounter])
//let randomInterval = Double((arc4random_uniform(8)+1))/20 Random typing speed
timer?.invalidate()
timer = Timer.scheduledTimer(timeInterval: 0.2, target: self, selector: #selector(TextEffectScene.typeLetter), userInfo: nil, repeats: false)
} else {
timer?.invalidate() // stop the timer
}
myCounter += 1
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
let touch = touches.first
if let location = touch?.location(in: self) {
if (button?.contains(location))! {
print("doggoSceneLoaded")
let transition = SKTransition.fade(withDuration: 0.5)
let newScene = SKScene(fileNamed: "GameScene") as! GameScene
self.view?.presentScene(newScene, transition: transition)
}
}
}
}
As you can see, I had to animate each individual label node in a word "loser".
To create this effect:
For those who may be interested to Swift 4 I've realized a gitHub project around this special request called SKAdvancedLabelNode.
You can find here all sources.
Usage:
// horizontal alignment : left
var advLabel = SKAdvancedLabelNode(fontNamed:"Optima-ExtraBlack")
advLabel.name = "advLabel"
advLabel.text = labelTxt
advLabel.fontSize = 20.0
advLabel.fontColor = .green
advLabel.horizontalAlignmentMode = .left
addChild(self.advLabel)
advLabel.position = CGPoint(x:frame.width / 2.5, y:frame.height*0.70)
advLabel.sequentiallyBouncingZoom(delay: 0.3,infinite: true)
Output:
something i have a lot of experience with... There is no way to do this properly outside of what you are already doing. My solution (for a text game) was to use NSAttributedString alongside CoreAnimation which allows you to have crazy good animations over UILabels... Then adding the UILabels in over top of SpriteKit.
I was working on a better SKLabel subclass, but ultimately gave up on it after I realized that there was no way to get the kerning right without a lot more work.
It is possible to use an SKSpriteNode and have a view as a texture, then you would just update the texture every frame, but this requires even more timing / resources.
The best way to do this is in the SK Editor how you have been doing it. If you need a lot of animated text, then you need to use UIKit and NSAttributedString alongside CoreAnimation for fancy things.
This is a huge, massive oversight IMO and is a considerable drawback to SpriteKit. SKLabelNode SUCKS.
As I said in a comment, you can subclass from an SKNode and use it to generate your labels for each characters. You then store the labels in an array for future reference.
I've thrown something together quickly and it works pretty well. I had to play a little bit with positionning so it looks decent, because spaces were a bit too small. Also horizontal alignement of each label has to be .left or else, it will be all crooked.
Anyway, it'S super easy to use! Go give it a try!
Here is a link to the gist I just created.
https://gist.github.com/sonoblaise/e3e1c04b57940a37bb9e6d9929ccce27

using UIImage as a condition in an if statement

I am trying to load variables into a detail view controller from a NSDictionary in the primary view controller.
I am trying to use the images in the primary view Controller to determine which variable to use in the detail view controller.
My code is like this in the primary View controller;
-(IBAction)pushView:(id) sender{
SomeDetailViewController *detailController = [[SomeDetailViewController alloc] init];
if ((self.imageView.image = [UIImage imageNamed:#"0002.jpg"])) {
NSMutableDictionary *myDictionary = [NSMutableDictionary dictionaryWithObjectsAndKeys:
#"titleA", #"headerkey2",,nil];
detailController.mapDictionary = mapDictionary;
NSLog(#"%#", [myDictionary objectForKey:#"headerkey2"]);
}
else if((self.imageView.image = [UIImage imageNamed:#"0003.jpg"])) {
NSMutableDictionary *myDictionary = [NSMutableDictionary dictionaryWithObjectsAndKeys:
#"TitleB", #"headerkey2",,nil];
detailController.myDictionary = myDictionary;
NSLog(#"%#", [myDictionary objectForKey:#"headerkey2"]);
}
[self.navigationController pushViewController:detailController animated:YES];
}
then in my receiving detail view controller my code is;
-(void) viewDidLoad{
[super viewDidLoad];
self.title = [(NSMutableDictionary *)myDictionary objectForKey:#"headerkey2"];
}
Unfortunately this is not working. The same thing shows up for the navigationBar title no matter what.
Can I use a UIImage as a condition?
Any help would be greatly appreciated.
I have used the backlash but doesn't seem to be working in all cases.
Do you use "=" in the if statement for some reason? if not, use "==" instead.
the doc says that [UIImage imageNamed] returns the object associated with specified image, which means it should always return the same object for a same image, so you can use UIImage in condition.