Draw dashline cylinder in scenekit like measure app? - ios

I've followed this question to try to make the dash cylinder
final class LineNode: SCNNode {
convenience init(positionA: SCNVector3, positionB: SCNVector3) {
self.init()
let vector = SCNVector3(positionA.x - positionB.x, positionA.y - positionB.y, positionA.z - positionB.z)
let distance = vector.length
let midPosition = (positionA + positionB) / 2
let lineGeometry = SCNCylinder()
lineGeometry.radius = PileDrawer3D.lineWidth
lineGeometry.height = CGFloat(distance)
lineGeometry.radialSegmentCount = 5
lineGeometry.firstMaterial?.diffuse.contents = dashedImage
lineGeometry.firstMaterial?.diffuse.contentsTransform = SCNMatrix4MakeScale(distance * 10, Float(lineGeometry.radius * 10), 1)
lineGeometry.firstMaterial?.diffuse.wrapS = .repeat
lineGeometry.firstMaterial?.diffuse.wrapT = .repeat
lineGeometry.firstMaterial?.isDoubleSided = true
lineGeometry.firstMaterial?.multiply.contents = UIColor.green
lineGeometry.firstMaterial?.lightingModel = .constant
let rotation = SCNMatrix4MakeRotation(.pi / 2, 0, 0, 1)
lineGeometry.firstMaterial?.diffuse.contentsTransform = SCNMatrix4Mult(rotation, lineGeometry.firstMaterial!.diffuse.contentsTransform)
geometry = lineGeometry
position = midPosition
eulerAngles = SCNVector3.lineEulerAngles(vector: vector)
name = className
}
lazy var dashedImage: UIImage = {
let size = CGSize(width: 10, height: 3)
UIGraphicsBeginImageContextWithOptions(size, true, 0)
UIColor.white.setFill()
UIRectFill(CGRect(x: 0, y: 0, width: 7, height: size.height))
let img = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return img!
}()
}
However, the pipes is not dashed.
I'm not sure what I'm missing here please help.
UpdateT:
It turns out that the clear color (in the image) is rendered as black, not transparent in the SCNView. Still, no idea why the green color got darken like this.

Another approach for Line & DashLine
final class LineNode: SCNNode {
var color: UIColor? {
set { geometry?.firstMaterial?.diffuse.contents = newValue }
get { geometry?.firstMaterial?.diffuse.contents as? UIColor }
}
convenience init(positionA: SCNVector3, positionB: SCNVector3, dash: CGFloat = 0, in scene: SCNScene? = nil) {
self.init()
let indices: [Int32] = [0, 1]
let source = SCNGeometrySource(vertices: [positionA, positionB])
let element = SCNGeometryElement(indices: indices, primitiveType: .line)
geometry = SCNGeometry(sources: [source], elements: [element])
geometry?.firstMaterial?.diffuse.contents = UIColor.green
geometry?.firstMaterial?.lightingModel = .constant
return
}
}
final class DashLineNode: SCNNode {
convenience init(positionA: SCNVector3, positionB: SCNVector3) {
self.init()
let vector = (positionB - positionA)
let length = floor(vector.length / 1)
let segment = vector / length
let indices:[Int32] = Array(0..<Int32(length))
var vertices = [positionA]
for _ in indices {
vertices.append(vertices.last! + segment)
}
let source = SCNGeometrySource(vertices: vertices)
let element = SCNGeometryElement(indices: indices, primitiveType: .line)
geometry = SCNGeometry(sources: [source], elements: [element])
geometry?.firstMaterial?.lightingModel = .constant
}
}

Related

Metal Core Image Kernel Sampling

I wrote the following test kernel to understand sampling in Metal Core Image Shaders. What I want to achieve is the following. Any pixel outside the bounds(extent) of the inputImage should be black, other pixels should be pixels of inputImage as usual. But I don't see the desired output so something is wrong in my understanding of how samplers work in shaders. There is no easy way to grab the world coordinates of inputImage, only destination supports world coordinates. Here is my code.
extern "C" float4 testKernel(coreimage::sampler inputImage, coreimage::destination dest)
{
float2 inputCoordinate = inputImage.coord();
float4 color = inputImage.sample(inputCoordinate);
float2 inputOrigin = inputImage.origin();
float2 inputSize = inputImage.size();
float2 destCoord = dest.coord();
if (inputCoordinate.x * inputSize.x < destCoord.x || inputCoordinate.y * inputSize.y > destCoord.y) {
return float4(0.0, 0.0, 0.0, 1.0);
}
return color;
}
And here is Swift code for the filter:
class CIMetalTestRenderer: CIFilter {
var inputImage:CIImage?
static var kernel:CIKernel = { () -> CIKernel in
let bundle = Bundle.main
let url = bundle.url(forResource: "Kernels", withExtension: "ci.metallib")!
let data = try! Data(contentsOf: url)
return try! CIKernel(functionName: "testKernel", fromMetalLibraryData: data)
}()
override var outputImage: CIImage? {
guard let inputImage = inputImage else {
return nil
}
let dod = inputImage.extent.insetBy(dx: -10, dy: -10)
return CIMetalTestRenderer.kernel.apply(extent: dod, roiCallback: { index, rect in
return rect
}, arguments: [inputImage])
}
}
Update: Here is my full code of ViewController. I just have UIImageView in storyboard (which can be created in viewDidLoad as well):
class ViewController: UIViewController {
#IBOutlet weak var imageView:UIImageView!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
generateSolidImage()
}
private func generateSolidImage() {
let renderSize = imageView.bounds.size
let solidSize = CGSize(width: renderSize.width * 0.5, height: renderSize.height * 0.5)
var solidImage = CIImage(color: CIColor(red: 0.3, green: 0.6, blue: 0.754, alpha: 1))
var cropRect = CGRect.zero
cropRect.size = solidSize
solidImage = solidImage.cropped(to: cropRect)
solidImage = solidImage.transformed(by: CGAffineTransform.init(translationX: -10, y: -10))
let metalRenderer = CIMetalTestRenderer()
metalRenderer.inputImage = solidImage
var outputImage = metalRenderer.outputImage
outputImage = outputImage?.transformed(by: CGAffineTransform.init(translationX: 20, y: 20))
let cyanImage = CIImage(color: CIColor.cyan).cropped(to: CGRect(x: 0, y: 0, width: renderSize.width, height: renderSize.height))
outputImage = outputImage?.composited(over: cyanImage)
let ciContext = CIContext()
let cgImage = ciContext.createCGImage(outputImage!, from: outputImage!.extent)
imageView.image = UIImage(cgImage: cgImage!)
}
}
And here are the outputs (by commenting and uncommenting black pixel line respectively).
I think the problem is the comparison with dest.coord() because this also changes depending on the pixel that is currently being processed.
If you just want to check whether you are currently sampling outside the bounds of inputImage, you can simply do the following:
if (inputCoordinate.x < 0.0 || inputCoordinate.x > 1.0 ||
inputCoordinate.y < 0.0 || inputCoordinate.y > 1.0) {
return float4(0.0, 0.0, 0.0, 1.0);
}
However, there would be a simpler way to achieve a "clamp-to-black" effect:
let clapedToBlack = solidImage.composited(over: CIImage.black)

Add plane nodes to ARKit scene vertically and horizontally

I want my app to lay the nodes on the surface, which can be vertical or horizontal. However, the node is always vertical. Here's a pic, these nodes aren't placed correctly.
#objc func didTapAddButton() {
let screenCentre = CGPoint(x: self.sceneView.bounds.midX, y: self.sceneView.bounds.midY)
let arHitTestResults: [ARHitTestResult] = sceneView.hitTest(screenCentre, types: [.featurePoint]) // Alternatively, we could use '.existingPlaneUsingExtent' for more grounded hit-test-points.
if let closestResult = arHitTestResults.first {
let transform: matrix_float4x4 = closestResult.worldTransform
let worldCoord: SCNVector3 = SCNVector3Make(transform.columns.3.x, transform.columns.3.y, transform.columns.3.z)
if let node = createNode() {
sceneView.scene.rootNode.addChildNode(node)
node.position = worldCoord
}
}
}
func createNode() -> SCNNode? {
guard let theView = myView else {
print("Failed to load view")
return nil
}
let plane = SCNPlane(width: 0.06, height: 0.06)
let imageMaterial = SCNMaterial()
imageMaterial.isDoubleSided = true
imageMaterial.diffuse.contents = theView.asImage()
plane.materials = [imageMaterial]
let node = SCNNode(geometry: plane)
return node
}
The app is able to see the ground but the nodes are still parallel to us. How can I fix this?
Edit: I figured I can use node.eulerAngles.x = -.pi / 2, this makes sure that the plane is laid down horizontally but it's still horizontal on vertical surfaces as well.
Solved! Here's how to make the view "parallel" to the camera at all times:
let yourNode = SCNNode()
let billboardConstraint = SCNBillboardConstraint()
billboardConstraint.freeAxes = [.X, .Y, .Z]
yourNode.constraints = [billboardConstraint]
Or
guard let currentFrame = sceneView.session.currentFrame else {return nil}
let camera = currentFrame.camera
let transform = camera.transform
var translationMatrix = matrix_identity_float4x4
translationMatrix.columns.3.z = -0.1
let modifiedMatrix = simd_mul(transform, translationMatrix)
let node = SCNNode(geometry: plane)
node.simdTransform = modifiedMatrix

ARKit / SpriteKit - set pixelBufferAttributes to SKVideoNode or make transparent pixels in video (chroma-key effect) another way

My goal is to present 2D animated characters in the real environment using ARKit. The animated characters are part of a video at presented in the following snapshot from the video:
Displaying the video itself was achieved with no problem at all using the code:
func view(_ view: ARSKView, nodeFor anchor: ARAnchor) -> SKNode? {
guard let urlString = Bundle.main.path(forResource: "resourceName", ofType: "mp4") else { return nil }
let url = URL(fileURLWithPath: urlString)
let asset = AVAsset(url: url)
let item = AVPlayerItem(asset: asset)
let player = AVPlayer(playerItem: item)
let videoNode = SKVideoNode(avPlayer: player)
videoNode.size = CGSize(width: 200.0, height: 150.0)
videoNode.anchorPoint = CGPoint(x: 0.5, y: 0.0)
return videoNode
}
The result of this code is presented in the screen shot from the app below as expected:
But as you can see, the background of the characters isn't very nice, so I need to make it vanish, in order to create the illusion of the characters actually standing on the horizontal plane surface.
I'm trying to achieve this by making a chroma-key effect to the video.
For those who are not familiar with chroma-key, this is name of the "green screen effect" seen sometimes on TV to make a color transparent.
My approach to the chroma-key effect is to create a custom filter based on "CIColorCube" CIFilter, and then apply the filter to the video using AVVideoComposition.
First, is the code for creating the filter:
func RGBtoHSV(r : Float, g : Float, b : Float) -> (h : Float, s : Float, v : Float) {
var h : CGFloat = 0
var s : CGFloat = 0
var v : CGFloat = 0
let col = UIColor(red: CGFloat(r), green: CGFloat(g), blue: CGFloat(b), alpha: 1.0)
col.getHue(&h, saturation: &s, brightness: &v, alpha: nil)
return (Float(h), Float(s), Float(v))
}
func colorCubeFilterForChromaKey(hueAngle: Float) -> CIFilter {
let hueRange: Float = 20 // degrees size pie shape that we want to replace
let minHueAngle: Float = (hueAngle - hueRange/2.0) / 360
let maxHueAngle: Float = (hueAngle + hueRange/2.0) / 360
let size = 64
var cubeData = [Float](repeating: 0, count: size * size * size * 4)
var rgb: [Float] = [0, 0, 0]
var hsv: (h : Float, s : Float, v : Float)
var offset = 0
for z in 0 ..< size {
rgb[2] = Float(z) / Float(size) // blue value
for y in 0 ..< size {
rgb[1] = Float(y) / Float(size) // green value
for x in 0 ..< size {
rgb[0] = Float(x) / Float(size) // red value
hsv = RGBtoHSV(r: rgb[0], g: rgb[1], b: rgb[2])
// TODO: Check if hsv.s > 0.5 is really nesseccary
let alpha: Float = (hsv.h > minHueAngle && hsv.h < maxHueAngle && hsv.s > 0.5) ? 0 : 1.0
cubeData[offset] = rgb[0] * alpha
cubeData[offset + 1] = rgb[1] * alpha
cubeData[offset + 2] = rgb[2] * alpha
cubeData[offset + 3] = alpha
offset += 4
}
}
}
let b = cubeData.withUnsafeBufferPointer { Data(buffer: $0) }
let data = b as NSData
let colorCube = CIFilter(name: "CIColorCube", withInputParameters: [
"inputCubeDimension": size,
"inputCubeData": data
])
return colorCube!
}
And then the code for applying the filter to the video by modifying the function func view(_ view: ARSKView, nodeFor anchor: ARAnchor) -> SKNode? that I wrote earlier:
func view(_ view: ARSKView, nodeFor anchor: ARAnchor) -> SKNode? {
guard let urlString = Bundle.main.path(forResource: "resourceName", ofType: "mp4") else { return nil }
let url = URL(fileURLWithPath: urlString)
let asset = AVAsset(url: url)
let filter = colorCubeFilterForChromaKey(hueAngle: 38)
let composition = AVVideoComposition(asset: asset, applyingCIFiltersWithHandler: { request in
let source = request.sourceImage
filter.setValue(source, forKey: kCIInputImageKey)
let output = filter.outputImage
request.finish(with: output!, context: nil)
})
let item = AVPlayerItem(asset: asset)
item.videoComposition = composition
let player = AVPlayer(playerItem: item)
let videoNode = SKVideoNode(avPlayer: player)
videoNode.size = CGSize(width: 200.0, height: 150.0)
videoNode.anchorPoint = CGPoint(x: 0.5, y: 0.0)
return videoNode
}
The code is supposed to replace all pixels of each frame of the video to alpha = 0.0 if the pixel color match the hue range of the background.
But instead of getting transparent pixels I'm getting those pixels black as can be seen in the image below:
Now, even though this is not the wanted effect, it does not surprise me, as I knew that this is the way iOS displays videos with alpha channel.
But here is the real problem - When displaying a normal video in an AVPlayer, there is an option to add an AVPlayerLayer to the view, and to set pixelBufferAttributes to it, to let the player layer know we use a transparent pixel buffer, like so:
let playerLayer = AVPlayerLayer(player: player)
playerLayer.bounds = view.bounds
playerLayer.position = view.center
playerLayer.pixelBufferAttributes = [(kCVPixelBufferPixelFormatTypeKey as String): kCVPixelFormatType_32BGRA]
view.layer.addSublayer(playerLayer)
This code gives us a video with transparent background (GOOD!) but a fixed size and position (NOT GOOD...), as you can see in this screenshot:
I want to achieve the same effect, but on SKVideoNode, and not on AVPlayerLayer. However, I can't find any way to set pixelBufferAttributes to SKVideoNode, and setting a player layer does not achieve the desired effect of ARKit as it is fixed in position.
Is there any solution to my problem, or maybe is there another technique to achieve the same desired effect?
The solution is quite simple!
All that needs to be done is to add the video as a child of a SKEffectNode and apply the filter to the SKEffectNode instead of the video itself (the AVVideoComposition is not necessary).
Here is the code I used:
func view(_ view: ARSKView, nodeFor anchor: ARAnchor) -> SKNode? {
// Create and configure a node for the anchor added to the view's session.
let bialikVideoNode = videoNodeWith(resourceName: "Tsina_05", ofType: "mp4")
bialikVideoNode.size = CGSize(width: kDizengofVideoWidth, height: kDizengofVideoHeight)
bialikVideoNode.anchorPoint = CGPoint(x: 0.5, y: 0.0)
// Make the video background transparent using an SKEffectNode, since chroma-key doesn't work on video
let effectNode = SKEffectNode()
effectNode.addChild(bialikVideoNode)
effectNode.filter = colorCubeFilterForChromaKey(hueAngle: 120)
return effectNode
}
And here is the result as needed:
Thank you! Had the same problem + mixing [AR/Scene/Sprite]Kit. But I would recommend to use this algorithm instead. It gives a better result:
...
var r: [Float] = removeChromaKeyColor(r: rgb[0], g: rgb[1], b: rgb[2])
cubeData[offset] = r[0]
cubeData[offset + 1] = r[1]
cubeData[offset + 2] = r[2]
cubeData[offset + 3] = r[3]
offset += 4
...
func removeChromaKeyColor(r: Float, g: Float, b: Float) -> [Float] {
let threshold: Float = 0.1
let refColor: [Float] = [0, 1.0, 0, 1.0] // chroma key color
//http://www.shaderslab.com/demo-40---video-in-video-with-green-chromakey.html
let val = ceil(saturate(g - r - threshold)) * ceil(saturate(g - b - threshold))
var result = lerp(a: [r, g, b, 0.0], b: refColor, w: val)
result[3] = fabs(1.0 - result[3])
return result
}
func saturate(_ x: Float) -> Float {
return max(0, min(1, x));
}
func ceil(_ v: Float) -> Float {
return -floor(-v);
}
func lerp(a: [Float], b: [Float], w: Float) -> [Float] {
return [a[0]+w*(b[0]-a[0]), a[1]+w*(b[1]-a[1]), a[2]+w*(b[2]-a[2]), a[3]+w*(b[3]-a[3])];
}

SpriteKit: suggestions for rounding corners of unconventional grid?

The goal is to round the corners of an unconventional grid similar to the following:
https://s-media-cache-ak0.pinimg.com/564x/50/bc/e0/50bce0cb908913ebc2cf630d635331ef.jpg
https://s-media-cache-ak0.pinimg.com/564x/7e/29/ee/7e29ee80e957ec22bbba630ccefbfaa2.jpg
Instead of a grid with four corners like a conventional grid, these grids have multiple corners in need of rounding.
The brute force approach would be to identify tiles with corners exposed then round those corners either with a different background image or by clipping the corners in code.
Is there a cleaner approach?
The grid is rendered for an iOS app in a SpriteKit SKScene.
This is a really interesting question.You can build your matrix with different approaches but surely you must resolve everytime the changes about the 4 corners in background for each tiles.
Suppose you start with a GameViewController like this (without load SKS files and with anchorPoint equal to zero):
import UIKit
import SpriteKit
class GameViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
guard let view = self.view as! SKView? else { return }
view.ignoresSiblingOrder = true
view.showsFPS = true
view.showsNodeCount = true
let scene = GameScene(size:view.bounds.size)
scene.scaleMode = .resizeFill
scene.anchorPoint = CGPoint.zero
view.presentScene(scene)
}
}
My idea is to build a matrix like this:
import SpriteKit
class GameScene: SKScene {
private var sideTile:CGFloat = 40
private var gridWidthTiles:Int = 5
private var gridHeightTiles:Int = 6
override func didMove(to view: SKView) {
self.drawMatrix()
}
func drawMatrix(){
var index = 1
let matrixPos = CGPoint(x:50,y:150)
for i in 0..<gridHeightTiles {
for j in 0..<gridWidthTiles {
let tile = getTile()
tile.name = "tile\(index)"
addChild(tile)
tile.position = CGPoint(x:matrixPos.x+(sideTile*CGFloat(j)),y:matrixPos.y+(sideTile*CGFloat(i)))
let label = SKLabelNode.init(text: "\(index)")
label.fontSize = 12
label.fontColor = .white
tile.addChild(label)
label.position = CGPoint(x:tile.frame.size.width/2,y:tile.frame.size.height/2)
index += 1
}
}
}
func getTile()->SKShapeNode {
let tile = SKShapeNode(rect: CGRect(x: 0, y: 0, width: sideTile, height: sideTile), cornerRadius: 10)
tile.fillColor = .gray
tile.strokeColor = .gray
return tile
}
}
Output:
Now we can construct a background for each tile of our matrix.
We can made the same tile node but with a different color (maybe more clear than the tile color) and without corner radius. If we split this background in 4 parts we have:
left - bottom background tile
left - top background tile
right - bottom background tile
right - top background tile
Code for a typical background tile:
func getBgTileCorner()->SKShapeNode {
let bgTileCorner = SKShapeNode(rect: CGRect(x: 0, y: 0, width: sideTile/2, height: sideTile/2))
bgTileCorner.fillColor = .lightGray
bgTileCorner.strokeColor = .lightGray
bgTileCorner.lineJoin = .round
bgTileCorner.isAntialiased = false
return bgTileCorner
}
Now with the SKSCropNode we can obtain only the corner using the background tile and the tile:
func getCorner(at angle:String)->SKCropNode {
let cropNode = SKCropNode()
let tile = getTile()
let bgTile = getBgTileCorner()
cropNode.addChild(bgTile)
tile.position = CGPoint.zero
let tileFrame = CGRect(x: 0, y: 0, width: sideTile, height: sideTile)
switch angle {
case "leftBottom": bgTile.position = CGPoint(x:tile.position.x,y:tile.position.y)
case "rightBottom": bgTile.position = CGPoint(x:tile.position.x+tileFrame.size.width/2,y:tile.position.y)
case "leftTop": bgTile.position = CGPoint(x:tile.position.x,y:tile.position.y+tileFrame.size.height/2)
case "rightTop": bgTile.position = CGPoint(x:tile.position.x+tileFrame.size.width/2,y:tile.position.y+tileFrame.size.height/2)
default:break
}
tile.fillColor = self.backgroundColor
tile.strokeColor = self.backgroundColor
tile.lineWidth = 0.0
bgTile.lineWidth = 0.0
tile.blendMode = .replace
cropNode.position = CGPoint.zero
cropNode.addChild(tile)
cropNode.maskNode = bgTile
return cropNode
}
Output for a typical corner:
let corner = getCorner(at: "leftBottom")
addChild(corner)
corner.position = CGPoint(x:50,y:50)
Now we can rebuild the drawMatrix function with the corners for each tile:
func drawMatrix(){
var index = 1
let matrixPos = CGPoint(x:50,y:150)
for i in 0..<gridHeightTiles {
for j in 0..<gridWidthTiles {
let tile = getTile()
tile.name = "tile\(index)"
let bgTileLB = getCorner(at:"leftBottom")
let bgTileRB = getCorner(at:"rightBottom")
let bgTileLT = getCorner(at:"leftTop")
let bgTileRT = getCorner(at:"rightTop")
bgTileLB.name = "bgTileLB\(index)"
bgTileRB.name = "bgTileRB\(index)"
bgTileLT.name = "bgTileLT\(index)"
bgTileRT.name = "bgTileRT\(index)"
addChild(bgTileLB)
addChild(bgTileRB)
addChild(bgTileLT)
addChild(bgTileRT)
addChild(tile)
tile.position = CGPoint(x:matrixPos.x+(sideTile*CGFloat(j)),y:matrixPos.y+(sideTile*CGFloat(i)))
let label = SKLabelNode.init(text: "\(index)")
label.fontSize = 12
label.fontColor = .white
tile.addChild(label)
label.position = CGPoint(x:tile.frame.size.width/2,y:tile.frame.size.height/2)
bgTileLB.position = CGPoint(x:tile.position.x,y:tile.position.y)
bgTileRB.position = CGPoint(x:tile.position.x,y:tile.position.y)
bgTileLT.position = CGPoint(x:tile.position.x,y:tile.position.y)
bgTileRT.position = CGPoint(x:tile.position.x,y:tile.position.y)
index += 1
}
}
}
Output:
Very similar to your screenshots (these are two tile example:)
Now when you want to remove a tile, you can decide what corner you want to remove or leave because for each tile you have also the relative 4 corners :
Output:
Okay, the grid creation process isn't really relative to this. You just need some way of differentiating between a blank spot in the grid and a filled spot. In my example I have a Tile object with a type of .blank or .regular. You need to have all 15 images (you can change the style to whatever you like, although they have to be in the same order and they have to be prefixed with 1..15). It uses bit calculation to figure out which image to use as a background and offsets the background image by 1/2 tile size for x and y. Other than that it is pretty self explanitory. Those background images were my tester images I created when developing this, so feel free to use them.
struct GridPosition {
var col: Int = 0
var row: Int = 0
}
class GameScene: SKScene {
private var backgroundLayer = SKNode()
private var tileLayer = SKNode()
private var gridSize: CGSize = CGSize.zero
private var gridRows: Int = 0
private var gridCols: Int = 0
private var gridBlanks = [Int]()
private var tiles = [[Tile]]()
var tileSize: CGFloat = 150
override func didMove(to view: SKView) {
backgroundLayer.zPosition = 1
addChild(backgroundLayer)
tileLayer.zPosition = 2
addChild(tileLayer)
gridRows = 8
gridCols = 11
gridBlanks = [0,1,3,4,5,6,7,9,10,11,12,13,15,16,17,19,20,21,22,23,31,32,33,36,40,43,56,64,67,69,70,71,72,73,75,77,78,79,82,85,86,87]
createGrid()
createBackgroundTiles()
}
func createGrid() {
for row in 0 ..< gridRows {
var rowContent = [Tile]()
for col in 0 ..< gridCols {
let currentTileLocation: Int = row * gridCols + col
var tile: Tile
if gridBlanks.contains(currentTileLocation) {
tile = Tile(row: row, col: col, type: .blank, tileSize: tileSize)
}
else {
tile = Tile(row: row, col: col, type: .regular, tileSize: tileSize)
}
tile.position = positionInGrid(column: col, row: row)
tile.zPosition = CGFloat(100 + gridRows - row)
tileLayer.addChild(tile)
rowContent.append(tile)
}
tiles.append(rowContent)
}
}
func tileByGridPosition(_ gridPos: GridPosition) -> Tile {
return (tiles[Int(gridPos.row)][Int(gridPos.col)])
}
func positionInGrid(column: Int, row: Int) -> CGPoint {
let startX = 0 - CGFloat(gridCols / 2) * tileSize
let startY = 0 - CGFloat(gridRows / 2) * tileSize + tileSize / 2
return CGPoint(
x: startX + CGFloat(column) * tileSize,
y: startY + CGFloat(row) * tileSize)
}
func createBackgroundTiles() {
for row in 0...gridRows {
for col in 0...gridCols {
let topLeft = (col > 0) && (row < gridRows) && tileByGridPosition(GridPosition(col: col - 1, row: row)).type == .regular
let bottomLeft = (col > 0) && (row > 0) && tileByGridPosition(GridPosition(col: col - 1, row: row - 1)).type == .regular
let topRight = (col < gridCols) && (row < gridRows) && tileByGridPosition(GridPosition(col: col, row: row)).type == .regular
let bottomRight = (col < gridCols) && (row > 0) && tileByGridPosition(GridPosition(col: col, row: row - 1)).type == .regular
// The tiles are named from 0 to 15, according to the bitmask that is made by combining these four values.
let value = Int(NSNumber(value: topLeft)) | Int(NSNumber(value: topRight)) << 1 | Int(NSNumber(value: bottomLeft)) << 2 | Int(NSNumber(value: bottomRight)) << 3
// Values 0 (no tiles)
if value != 0 {
var gridPosition = positionInGrid(column: col, row: row)
gridPosition.x -= tileSize / 2
gridPosition.y -= tileSize / 2
let backgroundNode = SKSpriteNode(imageNamed: ("background_tile_\(value)"))
backgroundNode.size = CGSize(width: tileSize, height: tileSize)
backgroundNode.alpha = 0.8
backgroundNode.position = gridPosition
backgroundNode.zPosition = 1
backgroundLayer.addChild(backgroundNode)
}
}
}
}
}
class Tile: SKSpriteNode {
private var row = 0
private var col = 0
var type: TileType = .blank
init(row: Int, col: Int, type: TileType, tileSize: CGFloat) {
super.init(texture: nil ,color: .clear, size:CGSize(width: tileSize, height: tileSize))
self.type = type
size = self.size
let square = SKSpriteNode(color: type.color, size: size)
square.zPosition = 1
addChild(square)
}
}
Only thing that comes to mind is when one node touches another node, at that moment in time evaluate the display of said node, as well as change the neighbors that are affected by it.
What we did was lay out the tiles then call this function to round the nodes of exposed tiles.
// Rounds corners of exposed tiles. UIKit inverts coordinates so top is bottom and vice-versa.
fileprivate func roundTileCorners() {
// Get all tiles
var tiles = [TileClass]()
tileLayer.enumerateChildNodes(withName: ".//*") { node, stop in
if node is TileClass {
tiles.append(node as! TileClass)
}
}
// Round corners for each exposed tile
for t in tiles {
// Convert tile's position to root coordinates
let convertedPos = convert(t.position, from: t.parent!)
// Set neighbor positions
var leftNeighborPos = convertedPos
leftNeighborPos.x -= tileWidth
var rightNeighborPos = convertedPos
rightNeighborPos.x += tileWidth
var topNeighborPos = convertedPos
topNeighborPos.y += tileHeight
var bottomNeighborPos = convertedPos
bottomNeighborPos.y -= tileHeight
// Set default value for rounding
var cornersToRound : UIRectCorner?
// No neighbor below & to left? Round bottom left.
if !isTileAtPoint(point: bottomNeighborPos) && !isTileAtPoint(point: leftNeighborPos) {
cornersToRound = cornersToRound?.union(.topLeft) ?? .topLeft
}
// No neighbor below & to right? Round bottom right.
if !isTileAtPoint(point: bottomNeighborPos) && !isTileAtPoint(point: rightNeighborPos) {
cornersToRound = cornersToRound?.union(.topRight) ?? .topRight
}
// No neightbor above & to left? Round top left.
if !isTileAtPoint(point: topNeighborPos) && !isTileAtPoint(point: leftNeighborPos) {
cornersToRound = cornersToRound?.union(.bottomLeft) ?? .bottomLeft
}
// No neighbor above & to right? Round top right.
if !isTileAtPoint(point: topNeighborPos) && !isTileAtPoint(point: rightNeighborPos) {
cornersToRound = cornersToRound?.union(.bottomRight) ?? .bottomRight
}
// Any corners to round?
if cornersToRound != nil {
t.roundCorners(cornersToRound: cornersToRound!)
}
}
}
// Returns true if a tile exists at <point>. Assumes <point> is in root node's coordinates.
fileprivate func isTileAtPoint(point: CGPoint) -> Bool {
return nodes(at: point).contains(where: {$0 is BoardTileNode })
}

adding a border around SceneKit node

i am trying to highlight a selected node in SceneKit with a tap gesture. Unfortunately, I have not been able to accomplish it. The best thing I could do was to change the material when the node is tapped.
let material = key.geometry!.firstMaterial!
material.emission.contents = UIColor.blackColor()
Can someone suggest a way I can go about to just add a border or outline around the object instead of changing the color of the entire node?
Based on #Karl Sigiscar answer and another answer here in SO I came up with this:
func createLineNode(fromPos origin: SCNVector3, toPos destination: SCNVector3, color: UIColor) -> SCNNode {
let line = lineFrom(vector: origin, toVector: destination)
let lineNode = SCNNode(geometry: line)
let planeMaterial = SCNMaterial()
planeMaterial.diffuse.contents = color
line.materials = [planeMaterial]
return lineNode
}
func lineFrom(vector vector1: SCNVector3, toVector vector2: SCNVector3) -> SCNGeometry {
let indices: [Int32] = [0, 1]
let source = SCNGeometrySource(vertices: [vector1, vector2])
let element = SCNGeometryElement(indices: indices, primitiveType: .line)
return SCNGeometry(sources: [source], elements: [element])
}
func highlightNode(_ node: SCNNode) {
let (min, max) = node.boundingBox
let zCoord = node.position.z
let topLeft = SCNVector3Make(min.x, max.y, zCoord)
let bottomLeft = SCNVector3Make(min.x, min.y, zCoord)
let topRight = SCNVector3Make(max.x, max.y, zCoord)
let bottomRight = SCNVector3Make(max.x, min.y, zCoord)
let bottomSide = createLineNode(fromPos: bottomLeft, toPos: bottomRight, color: .yellow)
let leftSide = createLineNode(fromPos: bottomLeft, toPos: topLeft, color: .yellow)
let rightSide = createLineNode(fromPos: bottomRight, toPos: topRight, color: .yellow)
let topSide = createLineNode(fromPos: topLeft, toPos: topRight, color: .yellow)
[bottomSide, leftSide, rightSide, topSide].forEach {
$0.name = kHighlightingNode // Whatever name you want so you can unhighlight later if needed
node.addChildNode($0)
}
}
func unhighlightNode(_ node: SCNNode) {
let highlightningNodes = node.childNodes { (child, stop) -> Bool in
child.name == kHighlightingNode
}
highlightningNodes.forEach {
$0.removeFromParentNode()
}
}
SCNNode conforms to the SCNBoundingVolume Protocol.
This protocol defines the getBoundingBoxMin:max: method.
Use this to get the minimum and maximum coordinates of the bounding box of the geometry attached to the node.
Then use the SceneKit primitive type SCNGeometryPrimitiveTypeLine to draw the lines of the bounding box. Check SCNGeometryElement.
If your node is a primitive shape you can set a UIImage as the diffuse. The image will get stretched out to cover your node. If you use an image with a border on it, this will translate to creating a border on your node.
planeGeometry.firstMaterial?.diffuse.contents = UIImage(named: "blueSquareOutline.png")

Resources