Unable to create circle using UIBezierPath and SCNShape - ios

Anyone familiar with using curved UIBezierPaths to create a SCNShape in ARKit? I'm creating a closed circle path, but I get a diamond shape in my ARKit scene.
Here is the code I use to create the bezier path and SCNShape:
let radius : CGFloat = 1.0
let outerPath = UIBezierPath(ovalIn: CGRect(x: -radius, y: -radius, width: 2* radius, height: 2* radius))
let material = SCNMaterial()
material.diffuse.contents = UIColor.blue
material.isDoubleSided = true
material.ambient.contents = UIColor.black
material.lightingModel = .constant
material.emission.contents = UIColor.blue
let shape = SCNShape(path: outerPath, extrusionDepth: 0.01)
shape.materials = [material]
let shapeNode = SCNNode(geometry: shape)
positioningNode.addChildNode(shapeNode)
I've successfully tested a rectangular bezier path, but even had issues with a rounded rect bezier path (using UIBezierPath(roundedRect:). For the rounded rect bezier path, ARKit shows the curved corners with 45 degree lines.
Thanks in advance!
UPDATE:
On the left is the initial SCNShape with UIBezierPath flatness set to 0.6. On the right is the same SCNShape with flatness set to 0.001.

I was able to find a solution. Basically the UIBezierPath's flatness variable is used to control curvature. The default value of 0.6 was too large in my case. I ended up using a flatness of 0.001
https://developer.apple.com/documentation/scenekit/scnshape/1523432-init

Related

Drawing filled polygon on ARKit Horizontal plane

How would I draw a filled polygon connecting points touched by user on a detected horizontal plane in ARKit world.
I went on to do this by keeping hit points on an array...
let position = SCNVector3.positionFrom(matrix: result.worldTransform)
let sphere = SphereNode(position: position)
nodes.append(position)
... and then trying to draw a bezier path
let bezierPath = UIBezierPath()
bezierPath.lineWidth = 0.1
for index in 0..<nodes.count {
let node = nodes[index] as SphereNode
if (index == 0) {
let point = CGPoint(x: CGFloat(node.position.x), y: CGFloat(node.position.z))
bezeierPath.move(to: point)
} else {
let point = CGPoint(x: CGFloat(node.position.x), y: CGFloat(node.position.z))
bezeierPath.addLine(to: point)
}
}
bezeierPath.close()
bezeierPath.fill()
bezeierPath.stroke()
let shape = SCNShape(path: bezeierPath, extrusionDepth: 0.03)
shape.firstMaterial?.diffuse.contents = UIColor.red
let node = SCNNode.init(geometry: shape)
sceneView.scene.rootNode.addChildNode(node)
But this doesn't draw the polygon on the correct place. it draw it behind the camera vertically angled.
2D geometries like SCNPlane, SCNText and SCNShape are vertical by nature because they work on the xy plane. In your example, you're using the z 3D coordinate as the y 2X coordinate.
Try setting node.eulerAngles.x to -.pi / 2 (if that doesn't work try without the -), and your node will be horizontal instead of vertical.

Drawing a wedge in ARKit using SceneKit

I am trying to draw a wedge of a circle using UIBezierPath, but when drawing it it shows a triangle and instead of drawing a curve through my points, it draws a straight line.
I am constructing the SCNNode Like so:
let path = UiBezierPath()
path.move(to: CGPoint.zero)
path.addArc(withCenter: CGPoint.zero, radius: 0.5, startAngle: 0, endAngle: .pi/2, clockwise: true)
path.close()
let shape = SCNShape(path: path, extrusionDepth: 0.1)
let mat = SCNMaterial()
mat.diffuse.contents = UIColor.orange
shape.materials = [mat]
let node = SCNNode(geometry: shape)
When I position this node in the world, and add it to the scene it draws a triangle. How can I make it so there is a curve instead of just a straight line?
Setting the flatness of the path to 0 fixed this issue:
path.flatness = 0

SceneKit CALayer unsharp drawing

I'm trying to draw UIBezierPaths and display them on a SCNPlane, though I'm getting a very unsharp result (See in the image) How could I make it sharper?
let displayLayer = CAShapeLayer()
displayLayer.frame = CGRect(x: 0, y: 0, width: 200, height: 200)
displayLayer.path = path.cgPath
displayLayer.fillColor = UIColor.clear.cgColor
displayLayer.strokeColor = stroke.cgColor
displayLayer.lineWidth = lineWidth
let plane = SCNPlane(width:size.width, height: size.height)
let material = SCNMaterial()
material.diffuse.contents = displayLayer
plane.materials = [material]
let node = SCNNode(geometry: plane)
I've tried to increase the displayLayer.frame's size but it only made things smaller.
Thanks for your answer!
Andras
This has been driving me crazy, so hopefully this answer helps someone else who stumbles across this. I'm mapping a CALayer to a SCNMaterial texture for use in an ARKit scene, and everything was blurry/pixelated. Setting the contentsScale property of the CALayer to UIScreen.main.scale didn't do anything, nor playing with shouldRasterize and rasterizationScale.
The solution is to set the layer's frame to the desired frame, multiplied by the screen's scale, otherwise the material is scale transforming the layer to fit the texture. In other words, the layer's size needs to be 3x its desired size.
let layer = CALayer()
layer.frame = CGRect(x: 0, y: 0, width: 500 * UIScreen.main.scale, height: 750 * UIScreen.main.scale)
Try to change UIBezierPath "flatness" property (0.1, 0.01, 0.001 etc).

Drawing A Circle With Gradient Stroke

I am working on a task in which I must create a circle with no fill, but a gradient stroke. For reference, here is the end result I am after;
Given other occurrences with the app, I am drawing my circle like so;
let c = UIGraphicsGetCurrentContext()!
c.saveGState()
let clipPath: CGPath = UIBezierPath(roundedRect: converted_rect, cornerRadius: converted_rect.width / 2).cgPath
c.addPath(clipPath)
c.setLineWidth(9.0)
c.setStrokeColor(UIColor.blue.cgColor)
c.closePath()
c.strokePath()
c.restoreGState()
let result = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
This results in a circle with a blue stroke. Despite many searches around SO, I'm struggling to figure out how I'd replace that setStrokeColor with a gradient, rather than a blue color. My most success came from creating a CAGradientLayer, then masking it with a CAShapeLayer created from the path, but I was only able to create a filled circle, not a hollow circle.
Thank you!
The basic idea is to use your path as a clipping path, then draw the gradient.
let c = UIGraphicsGetCurrentContext()!
let clipPath: CGPath = UIBezierPath(ovalIn: converted_rect).cgPath
c.saveGState()
c.setLineWidth(9.0)
c.addPath(clipPath)
c.replacePathWithStrokedPath()
c.clip()
// Draw gradient
let colors = [UIColor.blue.cgColor, UIColor.red.cgColor]
let offsets = [ CGFloat(0.0), CGFloat(1.0) ]
let grad = CGGradient(colorsSpace: CGColorSpaceCreateDeviceRGB(), colors: colors as CFArray, locations: offsets)
let start = converted_rect.origin
let end = CGPoint(x: converted_rect.maxX, y: converted_rect.maxY)
c.drawLinearGradient(grad!, start: start, end: end, options: [])
c.restoreGState()
let result = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
Setup a CGGradient first with the desired colors. Then for a linear gradient you use drawLinearGradient. For a radial gradient, use drawRadialGradient.

SCNShape with bezier path

I am tying to draw a 3d line in Scenekit iOS, i am trying below code to draw line,
func path() -> UIBezierPath
{
var bPath = UIBezierPath()
bPath.moveToPoint(CGPointMake(0, 0))
bPath.addLineToPoint(CGPointMake(10, 10))
bPath.addLineToPoint(CGPointMake(5, 5))
bPath.closePath()
return bPath
}
and the below code is to load the line in SCNNode,
let sap = SCNShape()
sap.path = path()
let carbonNode = SCNNode(geometry: sap)
carbonNode.position = SCNVector3Make(0, 0, 15)
scene.rootNode.addChildNode(carbonNode)
But, i am getting blank screen.
try changing your geometry's materials. You might have a white object on a white background.
edit:
it also turns out that the vertices of your triangle are colinear. You thus end up with a degenerate triangle.
Your path has no surface area since you start at (0,0), draw a line to (10, 10) and then a second line to (5,5) which is already on the line between (0,0) and (10, 10). Then you close the path by drawing another line to (0,0).
Changing either of the three points creates a path with a surface area.
Also, the z-axis points out of the screen. This means that depending on where your camera is positioned, you may be placing the carbonNode behind the camera. If instead you meant to position it further into the scene you should likely use a negative z value.
Here's some code I wrote to address the problem.
// Material colors
let cyanMaterial = SCNMaterial()
cyanMaterial.diffuse.contents = UIColor.cyanColor()
let anOrangeMaterial = SCNMaterial()
anOrangeMaterial.diffuse.contents = UIColor.orangeColor()
let aPurpleMaterial = SCNMaterial()
aPurpleMaterial.diffuse.contents = UIColor.purpleColor()
// A bezier path
let bezierPath = UIBezierPath()
bezierPath.moveToPoint(CGPointMake(-0.25, 0))
bezierPath.addLineToPoint(CGPointMake(0, -0.25))
bezierPath.addLineToPoint(CGPointMake(0.25, 0))
bezierPath.addLineToPoint(CGPointMake(0, 0.25))
bezierPath.closePath()
// Add shape
let shape = SCNShape(path: bezierPath, extrusionDepth: 0.75)
shape.materials = [cyanMaterial, anOrangeMaterial, aPurpleMaterial]
let shapeNode = SCNNode(geometry: shape)
shapeNode.position = SCNVector3(x: 0.2, y: 0.75, z: 0.1);
self.rootNode.addChildNode(shapeNode)
shapeNode.rotation = SCNVector4(x: -1.0, y: -1.0, z: 0.0, w: 0.0)
Keep in mind that the numbers, the Scene Kit scale, are in meters. So set the numbers similarly to those that work for the other shapes you create.

Resources