How to improve the accuracy of SCNNode created from array of positions? - ios

I am creating an SCNNode using an array of x, y, and z positions. The following code generates the desired node shape; However, the shape is partially filled in at areas that aren't supposed to be filled in.
I think this could be improved with a better version generateIndices() method, but I'm not exactly sure the specifics of what I could do to improve this. I would really appreciate any suggestions.
Example:
Points generated from equation: "z = x^2 + y^2"
What I see:
What the shape is supposed to look like:
class CustomShapeNode: SCNNode {
var indices = [Int32]()
var vertices = [SCNVector3]()
init(positions: [(Double,Double,Double)]) {
super.init()
// Create an array of SCNVector3 from the positions array
for position in positions {
vertices.append(SCNVector3(Float(position.0), Float(position.1), Float(position.2)))
}
// Create the indices array
indices = generateIndices(positions)
let positionSource = SCNGeometrySource(vertices: vertices)
let element = SCNGeometryElement(indices: indices, primitiveType: .triangles)
let geometry = SCNGeometry(sources: [positionSource], elements: [element])
self.geometry = geometry
// Set the color of the node geometry
self.geometry?.firstMaterial?.diffuse.contents = UIColor(hexString: UIColor.systemOrange.toHexString(), alpha: 0.2)
self.geometry?.firstMaterial?.lightingModel = .constant
self.geometry?.firstMaterial?.isDoubleSided = true
// Set the physics body for the node (object will fall if alone since gravity exists)
//let shape = SCNPhysicsShape(geometry: geometry, options: [SCNPhysicsShape.Option.type: SCNPhysicsShape.ShapeType.convexHull])
//self.physicsBody = SCNPhysicsBody(type: .dynamic, shape: shape)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func generateIndices(_ positions: [(Double, Double, Double)]) -> [Int32] {
var indices: [Int32] = []
for i in 0..<positions.count {
let current = positions[i]
for j in i+1..<positions.count {
let next = positions[j]
indices.append(Int32(i))
indices.append(Int32(j))
indices.append(Int32((j+1) % positions.count))
}
}
return indices
}
}

Related

How to generating a concave or convex polygon plane in SceneKit from SCNVector3

I'm trying to create a custom SCNGeometry in the form of a plane with custom shape, which could be placed in an ARKit session. I'm using the option SCNGeometryPrimitiveTypePolygon in the following method which seems to work fine:
extension SCNGeometry {
class func polygonfrom(vectices: [SCNVector3]) -> SCNGeometry {
let indices: [Int32] = getIndices(count: vectices.count)
let indexData = Data(bytes: indices, count: indices.count * MemoryLayout<Int32>.size)
let source = SCNGeometrySource(vertices: vectices)
let element = SCNGeometryElement(data: indexData, primitiveType: .polygon, primitiveCount: 1, bytesPerIndex: MemoryLayout<Int32>.size)
return SCNGeometry(sources: [source], elements: [element])
}
class private func getIndices(count: Int) -> [Int32] {
var indices: [Int32] = []
indices.append(Int32(count))
for i in 0..<count{
indices.append(Int32(i))
}
return indices
}
}
Unfortunately, It doesn't fit for a concave polygon:
results
The SCNShape class creates a suited triangulation on your behalf.

How to calculate distance from point 2 to point 3 and 3 to 4 so on using ARKit?

I am making an app to calculate the distance and area now the problem is I made an array and I am appending my nodes in there.
func calculate () {
let start = dotNodes[0]
let end = dotNodes[1]
let a = end.position.x - start.position.x
let b = end.position.y - start.position.y
let c = end.position.z - start.position.z
let distance = sqrt(pow(a,2) + pow(b,2) + pow(c, 2))
updateText(text:"\(abs( distance))", atPosition: (end.position))
}
Now the start point is 0 index and end is index 1 but these are only two points. How can I make it to calculate distance from 2 to 3 and 3to 4 so on, and at the end when the last point is touching the point 1 it should give me area?
As #Maxim has said you can begin by simplifying your calculations ^______^.
I will attempt to answer your question however, using the GLK Math Helper Methods which if your'e interested you can read more about here: GLK Documentation.
In essence what you need to do is iterate through your array of positions, and calculate the distance between these in segments of 2. When your last iteration has only one element, then you would calculate the position between this and the first one.
Since I am not great a Maths, I did a quick search on StackOverflow to find a solution, and made use of the answer provided by #Gasim in the post Iterate Over Collections Two At A Time In Swift.
Since my attempt is quite lengthy, instead of going through each part step, by step, I have provided answer which is fully commented, and hope will point you in the right direction.
As always if someone else can help refactor and or improve the code, please feel free:
//
// ViewController.swift
// Measuring Example
//
// Created By Josh Robbins (∩`-´)⊃━☆゚.*・。゚* on 27/04/2019.
// Copyright © 2019 BlackMirrorz. All rights reserved.
//
import UIKit
import ARKit
class ViewController: UIViewController {
#IBOutlet weak var augmentedRealityView: ARSCNView!
var augmentedRealityConfiguration = ARWorldTrackingConfiguration()
var augmentedRealitySession = ARSession()
var markerNodes = [SCNNode]()
typealias NodeNameData = (name: String, node: SCNNode)
typealias DistanceData = (distance: Float, positionA: GLKVector3, positionB: GLKVector3)
//---------------------
//MARK:- Initialization
//---------------------
override func viewDidLoad() {
super.viewDidLoad()
setupARSession()
}
/// Sets Up Our ARSession
func setupARSession(){
augmentedRealityView.session = augmentedRealitySession
augmentedRealitySession.run(augmentedRealityConfiguration, options: [.removeExistingAnchors, .resetTracking])
}
/// Creates A Node To Mark The Touch Position In The Scene
///
/// - Returns: SCNNode
func markerNode() -> SCNNode{
let node = SCNNode(geometry: SCNSphere(radius: 0.01))
node.geometry?.firstMaterial?.diffuse.contents = UIColor.cyan
return node
}
//------------------------
//MARK:- Marker Placemenet
//------------------------
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
//1. Get The Users Current Touch Point & Check We Have A Valid HitTest Result
guard let touchPoint = touches.first?.location(in: self.augmentedRealityView),
let hitTest = self.augmentedRealityView.hitTest(touchPoint, types: .featurePoint).first
else { return }
//2. Get The World Transorm & Create An SCNNode At The Converted Touch Position
let transform = hitTest.worldTransform
let node = markerNode()
node.position = SCNVector3(transform.columns.3.x, transform.columns.3.y, transform.columns.3.z)
self.augmentedRealityView.scene.rootNode.addChildNode(node)
//3. Add The Node To Our Markers Array So We Can Calculate The Distance Later
markerNodes.append(node)
//4. If We Have 5 Marker Nodes Then Calculate The Distances Between Them & Join Them Together
if markerNodes.count == 5{
calculateMarkerNodeDistances()
markerNodes.removeAll()
}
}
//-------------------
//MARK:- Calculations
//-------------------
/// Enemurates Our Marker Nodes & Creates A Joining Node Between Them
func calculateMarkerNodeDistances(){
var index = 0;
while index < markerNodes.count {
let nodeA = markerNodes[index];
var nodeB : SCNNode? = nil;
if index + 1 < markerNodes.count {
nodeB = markerNodes[index+1];
}
//1. Create A Joining Node Between The Two Nodes And Calculate The Distance
if let lastNode = nodeB{
let nodeA = NodeNameData("Node \(index)", nodeA)
let nodeB = NodeNameData("Node \(index+1)", lastNode)
self.augmentedRealityView.scene.rootNode.addChildNode(joiningNode(between: [nodeA, nodeB]))
}else{
//2. Here We Can Assume The We Have Reached The Last Node So We Calculate The Distance Between The 1st & Last Nodes
guard let initialNode = markerNodes.first, let lastNode = markerNodes.last else { return }
let nodeA = NodeNameData("Node 0 ", initialNode)
let nodeB = NodeNameData("Node \(markerNodes.count)", lastNode)
self.augmentedRealityView.scene.rootNode.addChildNode(joiningNode(between: [nodeA, nodeB]))
}
//Increment By 1 So We Join The Nodes Together In The Correct Sequence e.g. (1, 2), (3, 4) And Not (1, 2), (3, 4)
index += 1;
}
}
/// Creates A Joining Node Between Two Names
///
/// - Parameter nodes: [NodeNameData]
/// - Returns: MeasuringLineNode
func joiningNode(between nodes: [NodeNameData]) -> MeasuringLineNode{
let distance = calculateDistanceBetweenNodes([nodes[0], nodes[1]])
let joiner = MeasuringLineNode(startingVector: distance.positionA, endingVector: distance.positionB)
return joiner
}
/// Calculates The Distance Between Two SCNNodes
///
/// - Parameter nodes: [NodeNameData]
/// - Returns: DistanceData
func calculateDistanceBetweenNodes(_ nodes: [NodeNameData]) -> DistanceData{
//1. Calculate The Distance
let positionA = GLKVectorThreeFrom(nodes[0].node.position)
let positionB = GLKVectorThreeFrom(nodes[1].node.position)
let distance = GLKVector3Distance(positionA, positionB)
let meters = Measurement(value: Double(distance), unit: UnitLength.meters)
print("Distance Between Markers [ \(nodes[0].name) & \(nodes[1].name) ] = \(String(format: "%.2f", meters.value))m")
//2. Return The Distance A Positions Of The Nodes
return (distance, positionA, positionB)
}
/// Creates A GLKVector3 From An SCNVectore3
///
/// - Parameter vector3: SCNVector3
/// - Returns: GLKVector3
func GLKVectorThreeFrom(_ vector3: SCNVector3) -> GLKVector3 { return GLKVector3Make(vector3.x, vector3.y, vector3.z) }
}
//-------------------------
//MARK:- Mesuring Line Node
//-------------------------
class MeasuringLineNode: SCNNode{
/// Creates A Line Between Two SCNNodes
///
/// - Parameters:
/// - vectorA: GLKVector3
/// - vectorB: GLKVector3
init(startingVector vectorA: GLKVector3, endingVector vectorB: GLKVector3) {
super.init()
let height = CGFloat(GLKVector3Distance(vectorA, vectorB))
self.position = SCNVector3(vectorA.x, vectorA.y, vectorA.z)
let nodeVectorTwo = SCNNode()
nodeVectorTwo.position = SCNVector3(vectorB.x, vectorB.y, vectorB.z)
let nodeZAlign = SCNNode()
nodeZAlign.eulerAngles.x = Float.pi/2
let box = SCNBox(width: 0.001, height: height, length: 0.001, chamferRadius: 0)
let material = SCNMaterial()
material.diffuse.contents = UIColor.white
box.materials = [material]
let nodeLine = SCNNode(geometry: box)
nodeLine.position.y = Float(-height/2)
nodeZAlign.addChildNode(nodeLine)
self.addChildNode(nodeZAlign)
self.constraints = [SCNLookAtConstraint(target: nodeVectorTwo)]
}
required init?(coder aDecoder: NSCoder) { super.init(coder: aDecoder) }
}
Based on this simple (and hopefully accurate answer) the result was something like so:
Distance Between Markers [ Node 0 & Node 1 ] = 0.14m
Distance Between Markers [ Node 1 & Node 2 ] = 0.09m
Distance Between Markers [ Node 2 & Node 3 ] = 0.09m
Distance Between Markers [ Node 3 & Node 4 ] = 0.05m
Distance Between Markers [ Node 0 & Node 5 ] = 0.36m
In my example I am calculating the distances of five nodes but you could call this a any point. And of course you will then need to use a formula for calculating the area itself. However this should be more than enough to point you in the right direction.
Hope it helps...
The best performing (and also the easiest) way is to use SIMD -
https://developer.apple.com/documentation/accelerate/simd/working_with_vectors
let dist = simd_distance(start, end)
where vectors should probably be redefined as simd_float3 (or SIMD3<Float>, if you are using Swift 5).
P.S. You need to import simd framework first.

Doing a Discrete Fourier Transformed followed by an Inverse Discrete Fourier Transform does not produce the original values on Accelerate vDSP

I have a series of discrete values. So, I have no imaginary values for the input part. I am doing a Discrete Fourier Transform to these values and performing an Inverse Discrete Fourier Transform, to get the same values back. The idea was to test if vDSP from Accelerate Framework was working as expected, cause the documentation is a piece of crap and you have to figure it out by yourself.
Consider the following code
lazy var DFTSetupForward: vDSP_DFT_Setup = {
guard let setup = vDSP_DFT_zop_CreateSetupD(
nil,
vDSP_Length(self.numSamples),
vDSP_DFT_Direction.FORWARD) else {
fatalError("can't create vDSP_DFT_Setup")
}
return setup
}()
lazy var DFTSetupInverse: vDSP_DFT_Setup = {
guard let setup = vDSP_DFT_zop_CreateSetupD(
nil,
vDSP_Length(self.numSamples),
vDSP_DFT_Direction.INVERSE) else {
fatalError("can't create vDSP_DFT_Setup")
}
return setup
}()
func discreteFourierTransform (_ valores:[Double] = []) -> ([Double], [Double]) {
let numeroDados = valores.count
let inputImag = Array<Double>(repeating:0.0, count:numeroDados)
var outputReal = Array<Double>(repeating:0.0, count:numeroDados)
var outputImag = Array<Double>(repeating:0.0, count:numeroDados)
vDSP_DFT_ExecuteD(DFTSetupForward, valores, inputImag, &outputReal, &outputImag)
return (outputReal, outputImag)
}
// faz a DISCRETE FOURIER TRANSFORM DOUBLE
// a saída corresponde aos vetores reais e imaginários
func discreteFourierTransformInverse (_ valoresReais:[Double] = [],
_ valoresImaginarios:[Double] = []) -> ([Double], [Double]) {
let numeroDados = valoresReais.count
var outputReal = Array<Double>(repeating:0.0, count:numeroDados)
var outputImag = Array<Double>(repeating:0.0, count:numeroDados)
vDSP_DFT_ExecuteD(DFTSetupInverse, valoresReais, valoresImaginarios, &outputReal, &outputImag)
return (outputReal, outputImag)
}
and later these calls
let (outputDFTreal, outputDFTImaginario ) = self.discreteFourierTransform(normalizedData)
let (sinalRealX, sinalImaginarioX ) = self.discreteFourierTransformInverse(outputDFTreal, outputDFTImaginario)
or in other words, I am taking the results of the Fourier Transform from the first let and injecting them into the Inverse Fourier Transform. I expect sinalRealX to be equal to the original data, that in this case is normalizedData and also expect signalImaginarioX to be all zeros.
But the values are completely different!
What is going on?
You shouldn't expect the IDFT to return the normalized values. You should expect it to return the orginal (pre-normalize) values, which I believe it does (within computational error):
let fft = FFT()
let data = Array(stride(from: 0.0, to: Double(fft.numSamples), by: 1))
let normalizedData = data.map{ $0/Double(fft.numSamples) }
let (outputDFTreal, outputDFTImaginario ) = fft.discreteFourierTransform(normalizedData)
let (sinalRealX, sinalImaginarioX ) = fft.discreteFourierTransformInverse(outputDFTreal, outputDFTImaginario)
print(data)
print(sinalRealX)
print(sinalImaginarioX)
let zeros = Array(repeating: 0.0, count: data.count)
func avgerr(_ a: [Double], _ b: [Double]) -> Double {
return zip(a, b).map { abs($0 - $1) }.reduce(0, +) / Double(a.count)
}
avgerr(data, sinalRealX) // 2.442490654175344e-15
avgerr(zeros, sinalImaginarioX) // 2.602442788260593e-15
Floating point numbers are an approximation. And 10^-19 is approximately zero, relative to cos(0), in double precision floating-point arithmetic.
So you did get all zeros in the result.

ARKit - getting distance from camera to anchor

I'm creating an anchor and adding it to my ARSKView at a certain distance in front of the camera like this:
func displayToken(distance: Float) {
print("token dropped at: \(distance)")
guard let sceneView = self.view as? ARSKView else {
return
}
// Create anchor using the camera's current position
if let currentFrame = sceneView.session.currentFrame {
// Create a transform with a translation of x meters in front of the camera
var translation = matrix_identity_float4x4
translation.columns.3.z = -distance
let transform = simd_mul(currentFrame.camera.transform, translation)
// Add a new anchor to the session
let anchor = ARAnchor(transform: transform)
sceneView.session.add(anchor: anchor)
}
}
then the node gets created for the anchor like this:
func view(_ view: ARSKView, nodeFor anchor: ARAnchor) -> SKNode? {
// Create and configure a node for the anchor added to the view's session.
if let image = tokenImage {
let texture = SKTexture(image: image)
let tokenImageNode = SKSpriteNode(texture: texture)
tokenImageNode.name = "token"
return tokenImageNode
} else {
return nil
}
}
This works fine and I see the image get added at the appropriate distance. However, what I'm trying to do is then calculate how far the anchor/node is in front of the camera as you move. The problem is the calculation seems to be off immediately using fabs(cameraZ - anchor.transform.columns.3.z). Please see my code below that is in the update() method to calculate distance between camera and object:
override func update(_ currentTime: TimeInterval) {
// Called before each frame is rendered
guard let sceneView = self.view as? ARSKView else {
return
}
if let currentFrame = sceneView.session.currentFrame {
let cameraZ = currentFrame.camera.transform.columns.3.z
for anchor in currentFrame.anchors {
if let spriteNode = sceneView.node(for: anchor), spriteNode.name == "token", intersects(spriteNode) {
// token is within the camera view
//print("token is within camera view from update method")
print("DISTANCE BETWEEN CAMERA AND TOKEN: \(fabs(cameraZ - anchor.transform.columns.3.z))")
print(cameraZ)
print(anchor.transform.columns.3.z)
}
}
}
}
Any help is appreciated in order to accurately get distance between camera and the anchor.
The last column of a 4x4 transform matrix is the translation vector (or position relative to a parent coordinate space), so you can get the distance in three dimensions between two transforms by simply subtracting those vectors.
let anchorPosition = anchor.transforms.columns.3
let cameraPosition = camera.transform.columns.3
// here’s a line connecting the two points, which might be useful for other things
let cameraToAnchor = cameraPosition - anchorPosition
// and here’s just the scalar distance
let distance = length(cameraToAnchor)
What you’re doing isn’t working right because you’re subtracting the z-coordinates of each vector. If the two points are different in x, y, and z, just subtracting z doesn’t get you distance.
This one is for scenekit, I'll leave it here though.
let end = node.presentation.worldPosition
let start = sceneView.pointOfView?.worldPosition
let dx = (end?.x)! - (start?.x)!
let dy = (end?.y)! - (start?.y)!
let dz = (end?.z)! - (start?.z)!
let distance = sqrt(pow(dx,2)+pow(dy,2)+pow(dz,2))
With RealityKit there is a slightly different way to do this. If you're using the world tracking configuration, your AnchorEntity object conforms to HasAnchoring which gives you a target. Target is an enum of AnchoringComponent.Target. It has a case .world(let transform). You can compare your world transform to the camera's world transform like this:
if case let AnchoringComponent.Target.world(transform) = yourAnchorEntity.anchoring.target {
let theDistance = distance(transform.columns.3, frame.camera.transform.columns.3)
}
This took me a bit to figure out but I figure others that might be using RealityKit might benefit from this.
As mentioned above by #codeman, this is the right solution:
let distance = simd_distance(YOUR_NODE.simdTransform.columns.3, (sceneView.session.currentFrame?.camera.transform.columns.3)!);
3D distance - You can check these utils,
class ARSceneUtils {
/// return the distance between anchor and camera.
class func distanceBetween(anchor : ARAnchor,AndCamera camera: ARCamera) -> CGFloat {
let anchorPostion = SCNVector3Make(
anchor.transform.columns.3.x,
anchor.transform.columns.3.y,
anchor.transform.columns.3.z
)
let cametaPosition = SCNVector3Make(
camera.transform.columns.3.x,
camera.transform.columns.3.y,
camera.transform.columns.3.z
)
return CGFloat(self.calculateDistance(from: cametaPosition , to: anchorPostion))
}
/// return the distance between 2 vectors.
class func calculateDistance(from: SCNVector3, to: SCNVector3) -> Float {
let x = from.x - to.x
let y = from.y - to.y
let z = from.z - to.z
return sqrtf( (x * x) + (y * y) + (z * z))
}
}
And now you can call:
guard let camera = session.currentFrame?.camera else { return }
let anchor = // you anchor
let distanceAchorAndCamera = ARSceneUtils.distanceBetween(anchor: anchor, AndCamera: camera)

How to choose from an array of sprites in swift

So basically I am looking to choose one of the 4 different coloured balls at random to come into the scene which each have an animation, physics properties and movement & spacing that I have already coded. I am not sure exactly how to make the array then choose at random from the array of the 4 coloured balls so that I have one ball chosen at random to come into the scene.
To make it more clear what I'm asking here's some code (I only use two balls in this code so you don't have to read as much):
var moveandremove = SKAction() < this is in my ballScene.swift
The spawn runBlock is inside didMovetoView
let spawn = SKAction.runBlock({
() in
self.allballs()
})
let delay = SKAction.waitForDuration(2.0)
let SpawnDelay = SKAction.sequence([spawn, delay])
let spawndelayforever = SKAction.repeatActionForever(SpawnDelay)
self.runAction(spawndelayforever)
let distance = CGFloat(brnball.frame.width * 20 + brnball.frame.width)
let moveball = SKAction.moveByX(-distance, y: 0, duration: NSTimeInterval(0.003 * distance))
let removeball = SKAction.removeFromParent()
moveandremove = SKAction.sequence([moveball])
}
func allballs() {
TextureAtlasblk = SKTextureAtlas(named: "blkball")
for i in 1...TextureAtlasblk.textureNames.count{
var Name = "blkball_\(i)"
blkarray.append(SKTexture(imageNamed: Name))
}
blkball = SKSpriteNode(imageNamed: "blkball_1")
blkball.position = CGPoint(x: CGRectGetMidX(self.frame) + 100, y: CGRectGetMidY(self.frame))
blkball.zPosition = 7
blkball.setScale(0.1)
self.addChild(blkball)
blkball.runAction(SKAction.repeatActionForever(SKAction.animateWithTextures(blkarray, timePerFrame: 0.2)))
//brownball
TextureAtlasbrn = SKTextureAtlas(named: "brnball")
for i in 1...TextureAtlasbrn.textureNames.count{
var Name = "brnball_\(i)"
brnarray.append(SKTexture(imageNamed: Name))
}
brnball = SKSpriteNode(imageNamed: "brnball_1")
brnball.position = CGPoint(x: CGRectGetMidX(self.frame) + 50, y: CGRectGetMidY(self.frame))
brnball.zPosition = 7
brnball.setScale(0.1)
self.addChild(brnball)
brnball.runAction(SKAction.repeatActionForever(SKAction.animateWithTextures(brnarray, timePerFrame: 0.2)))
Here is my terrible starting point at trying to make an array to choose from each ball (this is inside my allballs() function):
var ballarray: NSMutableArray = [blkball, brnball, yelball, bluball]
runAction(moveandremove)
I am new to swift and pretty hopeless, would be awesome if someone could help me out :)
Thanks
It's hard for me to find the array that you're talking about in your code. But nevertheless, I can still show you how.
Let's say we have an [Int]:
let ints = [10, 50, 95, 48, 77]
And we want to get a randomly chosen element of that array.
As you may already know, you use the subscript operator with the index of the element to access an element in the array, e.g. ints[2] returns 95. So if you give a random index to the subscript, a random item in the array will be returned!
Let's see how can we generate a random number.
The arc4random_uniform function returns a uniformly distributed random number between 0 and one less the parameter. Note that this function takes a UInt32 as a parameter and the return value is of the same type. So you need to do some casting:
let randomNumber = Int(arc4random_uniform(UInt32(ints.count)))
With randomNumber, we can access a random element in the array:
let randomItem = ints[randomNumber]
Try to apply this technique to your situation.
Here's a generic method to do this as well:
func randomItemInArray<T> (array: [T]) -> T? {
if array.isEmpty {
return nil
}
let randomNumber = Int(arc4random_uniform(UInt32(array.count)))
return array[randomNumber]
}
Note that if the array passed in is empty, it returns nil.
You could make and extension for Array that returns a random element.
extension Array {
func randomElement() -> Element {
let i = Int(arc4random_uniform(UInt32(count - 1)))
return self[i]
}
}
You could take that a step further and allow a function to be applied directly to a random element.
mutating func randomElement(perform: (Element) -> Element) {
let i = Int(arc4random_uniform(UInt32(count - 1)))
self[i] = perform(self[i])
}
You can use this function when using an array of reference types.
func randomElement(perform: (Element) -> ()) {
let i = Int(arc4random_uniform(UInt32(count - 1)))
perform(self[i])
}

Resources