What Is an Example of Drawing Custom Nodes with Vertices in Swift Scenekit

What is an example of drawing custom nodes with vertices in swift SceneKit?

A custom geometry is constructed from a set of vertices and normals.

Vertices

In this context, a vertex is a point where two or more lines intersect. For a cube, the vertices are the corners shown in the following figure

Sample Image

We construct the geometry by building the cube's faces with a set of triangles, two triangles per face. Our first triangle is defined by vertices 0, 2, and 3 as shown in the below figure, and the second triangle is defined by vertices 0, 1, and 2. It is important to note that each triangle has a front and back side. The side of the triangle is determined by the order of the vertices, where the front side is specified in counter-clockwise order. For our cube, the front side will always be the outside of the cube.

If the cube's center is the origin, the six vertices that define one of the cube's face can be defined by

let vertices:[SCNVector3] = [
SCNVector3(x:-1, y:-1, z:1), // 0
SCNVector3(x:1, y:1, z:1), // 2
SCNVector3(x:-1, y:1, z:1) // 3

SCNVector3(x:-1, y:-1, z:1), // 0
SCNVector3(x:1, y:-1, z:1), // 1
SCNVector3(x:1, y:1, z:1) // 2
]

and we create the vertex source by

let vertexSource = SCNGeometrySource(vertices: vertices)

At this point, we have a vertex source that can be use to construct a face of the cube; however, SceneKit doesn't know how the triangle should react to light sources in the scene. To properly reflect light, we need to provide our geometry with a least one normal vector for each vertex.

Normals

A normal is a vector that specifies the orientation of a vertex that affects how light reflects off the corresponding triangle. In this case, the normal vectors for the six vertices of the triangle are the same; they all point in the positive z direction (i.e., x = 0, y = 0, and z = 1); see the red arrows in the below figure.

Sample Image

The normals are defined by

let normals:[SCNVector3] = [
SCNVector3(x:0, y:0, z:1), // 0
SCNVector3(x:0, y:0, z:1), // 2
SCNVector3(x:0, y:0, z:1), // 3

SCNVector3(x:0, y:0, z:1), // 0
SCNVector3(x:0, y:0, z:1), // 1
SCNVector3(x:0, y:0, z:1) // 2
]

and the source is defined by

let normalSource = SCNGeometrySource(normals: normals)

We now have the sources (vertices and normals) needed to construct a limited geometry, i.e., one cube face (two triangles). The final piece is to create an array of indices into the vertex and normal arrays. In this case, the indices are sequential because the vertices are in the order they are used.

var indices:[Int32] = [0, 1, 2, 3, 4, 5]

From the indices, we create an geometry element. The setup is a bit more involved because SCNGeometryElement requires an NSData as a parameter.

let indexData = NSData(bytes: &indices, length: MemoryLayout<Int32>.size * indices.count)

let element = SCNGeometryElement(data: indexData as Data, primitiveType: .triangles, primitiveCount: indices.count, bytesPerIndex: MemoryLayout<Int32>.size)

We can now create the custom geometry with

let geometry = SCNGeometry(sources: [vertexSource, normalSource], elements: [element])


and lastly create a node and assign the custom geometry to its geometry property

let node = SCNNode()
node.geometry = geometry

scene.rootNode.addChildNode(node)

We now extend the vertices and normals to including all of the cube faces:

    // The vertices
let v0 = SCNVector3(x:-1, y:-1, z:1)
let v1 = SCNVector3(x:1, y:-1, z:1)
let v2 = SCNVector3(x:1, y:1, z:1)
let v3 = SCNVector3(x:-1, y:1, z:1)

let v4 = SCNVector3(x:-1, y:-1, z:-1)
let v5 = SCNVector3(x:1, y:-1, z:-1)
let v6 = SCNVector3(x:-1, y:1, z:-1)
let v7 = SCNVector3(x:1, y:1, z:-1)

// All the cube faces
let vertices:[SCNVector3] = [
// Front face
v0, v2, v3,
v0, v1, v2,

// Right face
v1, v7, v2,
v1, v5, v7,

// Back
v5, v6, v7,
v5, v4, v6,

// Left
v4, v3, v6,
v4, v0, v3,

// Top
v3, v7, v6,
v3, v2, v7,

// Bottom
v1, v4, v5,
v1, v0, v4
]

let normalsPerFace = 6
let plusX = SCNVector3(x:1, y:0, z:0)
let minusX = SCNVector3(x:-1, y:0, z:0)
let plusZ = SCNVector3(x:0, y:0, z:1)
let minusZ = SCNVector3(x:0, y:0, z:-1)
let plusY = SCNVector3(x:0, y:1, z:0)
let minusY = SCNVector3(x:0, y:-1, z:0)

// Create an array with the direction of each vertex. Each array element is
// repeated 6 times with the map function. The resulting array or arrays
// is then flatten to an array
let normals:[SCNVector3] = [
plusZ,
plusX,
minusZ,
minusX,
plusY,
minusY
].map{[SCNVector3](repeating:$0,count:normalsPerFace)}.flatMap{$0}

// Create an array of indices [0, 1, 2, ..., N-1]
let indices = vertices.enumerated().map{Int32($0.0)}

let vertexSource = SCNGeometrySource(vertices: vertices)

let normalSource = SCNGeometrySource(normals: normals)

let pointer = UnsafeRawPointer(indices)
let indexData = NSData(bytes: pointer, length: MemoryLayout<Int32>.size * indices.count)

let element = SCNGeometryElement(data: indexData as Data, primitiveType: .triangles, primitiveCount: indices.count/3, bytesPerIndex: MemoryLayout<Int32>.size)

let geometry = SCNGeometry(sources: [vertexSource, normalSource], elements: [element])

// Create a node and assign our custom geometry
let node = SCNNode()
node.geometry = geometry

scene.rootNode.addChildNode(node)

SceneKit – Drawing a line between two points

There are lots of ways to do this.

As noted, your custom geometry approach has some disadvantages. You should be able to correct the problem of it being invisible from one side by giving its material the doubleSided property. You still may have issues with it being two-dimensional, though.

You could also modify your custom geometry to include more triangles, so you get a tube shape with three or more sides instead of a flat rectangle. Or just have two points in your geometry source, and use the SCNGeometryPrimitiveTypeLine geometry element type to have Scene Kit draw a line segment between them. (Though you won't get as much flexibility in rendering styles with line drawing as with shaded polygons.)

You can also use the SCNCylinder approach you mentioned (or any of the other built-in primitive shapes). Remember that geometries are defined in their own local (aka Model) coordinate space, which Scene Kit interprets relative to the coordinate space defined by a node. In other words, you can define a cylinder (or box or capsule or plane or whatever) that's 1.0 units wide in all dimensions, then use the rotation/scale/position or transform of the SCNNode containing that geometry to make it long, thin, and stretching between the two points you want. (Also note that since your line is going to be pretty thin, you can reduce the segmentCounts of whichever built-in geometry you're using, because that much detail won't be visible.)

Yet another option is the SCNShape class that lets you create an extruded 3D object from a 2D Bézier path. Working out the right transform to get a plane connecting two arbitrary points sounds like some fun math, but once you do it you could easily connect your points with any shape of line you choose.

Draw SceneKit object between two points

Both solutions mentioned above work very well and I can contribute third solution to this question.

//extension code starts

func normalizeVector(_ iv: SCNVector3) -> SCNVector3 {
let length = sqrt(iv.x * iv.x + iv.y * iv.y + iv.z * iv.z)
if length == 0 {
return SCNVector3(0.0, 0.0, 0.0)
}

return SCNVector3( iv.x / length, iv.y / length, iv.z / length)

}

extension SCNNode {

func buildLineInTwoPointsWithRotation(from startPoint: SCNVector3,
to endPoint: SCNVector3,
radius: CGFloat,
color: UIColor) -> SCNNode {
let w = SCNVector3(x: endPoint.x-startPoint.x,
y: endPoint.y-startPoint.y,
z: endPoint.z-startPoint.z)
let l = CGFloat(sqrt(w.x * w.x + w.y * w.y + w.z * w.z))

if l == 0.0 {
// two points together.
let sphere = SCNSphere(radius: radius)
sphere.firstMaterial?.diffuse.contents = color
self.geometry = sphere
self.position = startPoint
return self

}

let cyl = SCNCylinder(radius: radius, height: l)
cyl.firstMaterial?.diffuse.contents = color

self.geometry = cyl

//original vector of cylinder above 0,0,0
let ov = SCNVector3(0, l/2.0,0)
//target vector, in new coordination
let nv = SCNVector3((endPoint.x - startPoint.x)/2.0, (endPoint.y - startPoint.y)/2.0,
(endPoint.z-startPoint.z)/2.0)

// axis between two vector
let av = SCNVector3( (ov.x + nv.x)/2.0, (ov.y+nv.y)/2.0, (ov.z+nv.z)/2.0)

//normalized axis vector
let av_normalized = normalizeVector(av)
let q0 = Float(0.0) //cos(angel/2), angle is always 180 or M_PI
let q1 = Float(av_normalized.x) // x' * sin(angle/2)
let q2 = Float(av_normalized.y) // y' * sin(angle/2)
let q3 = Float(av_normalized.z) // z' * sin(angle/2)

let r_m11 = q0 * q0 + q1 * q1 - q2 * q2 - q3 * q3
let r_m12 = 2 * q1 * q2 + 2 * q0 * q3
let r_m13 = 2 * q1 * q3 - 2 * q0 * q2
let r_m21 = 2 * q1 * q2 - 2 * q0 * q3
let r_m22 = q0 * q0 - q1 * q1 + q2 * q2 - q3 * q3
let r_m23 = 2 * q2 * q3 + 2 * q0 * q1
let r_m31 = 2 * q1 * q3 + 2 * q0 * q2
let r_m32 = 2 * q2 * q3 - 2 * q0 * q1
let r_m33 = q0 * q0 - q1 * q1 - q2 * q2 + q3 * q3

self.transform.m11 = r_m11
self.transform.m12 = r_m12
self.transform.m13 = r_m13
self.transform.m14 = 0.0

self.transform.m21 = r_m21
self.transform.m22 = r_m22
self.transform.m23 = r_m23
self.transform.m24 = 0.0

self.transform.m31 = r_m31
self.transform.m32 = r_m32
self.transform.m33 = r_m33
self.transform.m34 = 0.0

self.transform.m41 = (startPoint.x + endPoint.x) / 2.0
self.transform.m42 = (startPoint.y + endPoint.y) / 2.0
self.transform.m43 = (startPoint.z + endPoint.z) / 2.0
self.transform.m44 = 1.0
return self
}
}

//extension ended.

//in your code, you can like this.
let twoPointsNode1 = SCNNode()
scene.rootNode.addChildNode(twoPointsNode1.buildLineInTwoPointsWithRotation(
from: SCNVector3(1,-1,3), to: SCNVector3( 7,11,7), radius: 0.2, color: .cyan))
//end

you can reference http://danceswithcode.net/engineeringnotes/quaternions/quaternions.html

BTW, you will get same result when you use a cylinder to make a line between two points from above 3 methods. But indeed, they will have different normal lines. In another words, if you use box between two points, sides of box, except top and bottom, will face different direction from above 3 methods.

let me know pls if you need further explanation.

SceneKit – Drawing on 3D Object

I suspect you have depth precision issues; it looks like parts of your line are being clipped because they are intersecting parts of the sphere. Try drawing the line with depth testing disabled.

SceneKit. Place nodes in one surface

If I'm understanding correctly, you want some kind of tap/drag combination - get the points from the 2D world and translate to a 3D world. This is some game code for a missile command type game, maybe it will help you with unprojectPoint stuff. There are some timers that aren't included, but hopefully you will get the idea.

@objc func handleTap(recognizer: UITapGestureRecognizer)
{
if(data.gameState == .endGame)
{
endGameAnimates.stop()
let _ = Timer.scheduledTimer(withTimeInterval: 1, repeats: false, block: { _ in self.dismiss(animated: false, completion: nil) })

return
}

if(gameControl.isWaveComplete == true || gNodes.gameNodes.isPaused == true) { return }

currentLocation = recognizer.location(in: gameScene)
let projectedPoint = gameScene.projectPoint(SCNVector3(0, 0, 0))
let scenePoint = gameScene.unprojectPoint(SCNVector3(currentLocation.x, currentLocation.y, CGFloat(projectedPoint.z)))

if(data.gameState == .endGame) // Allow animations to finish, otherwise they will show up next round
{
DispatchQueue.main.async { self.endGameAnimates.stop() }
let _ = Timer.scheduledTimer(withTimeInterval: 1, repeats: false, block: { _ in self.dismiss(animated: false, completion: nil) })
return
}

if(data.missilesAvailable <= 0)
{
sound.playSoundType(vSoundType: .defenseFails)
hudControl.notify()
}
else
{
gameControl.defenseMissileShoot(vPosition: scenePoint, soundType: 0)
sound.playSoundType(vSoundType: .defenseFires)
}
}
//**************************************************************************
@objc func handlePan(recognizer: UIPanGestureRecognizer)
{
currentLocation = recognizer.location(in: gameScene)
let projectedPoint = gameScene.projectPoint(SCNVector3(0, 0, 0))
let scenePoint = gameScene.unprojectPoint(SCNVector3(currentLocation.x, currentLocation.y, CGFloat(projectedPoint.z)))

if(gameControl.isWaveComplete == true || gNodes.gameNodes.isPaused == true) { return }

switch recognizer.state
{
case UIGestureRecognizer.State.began:
gameControl.defenseMissileShoot(vPosition: scenePoint, soundType: 1)
SNDdefenseSoundCount = 0
if(data.missilesAvailable <= 0) { sound.playSoundType(vSoundType: .defenseFails); hudControl.notify() }
beginLocation.x = currentLocation.x
break
case UIGestureRecognizer.State.changed:
if(currentLocation.x > beginLocation.x + dragDistance)
{
beginLocation.x = currentLocation.x
if(data.missilesAvailable > 0) { gameControl.defenseMissileShoot(vPosition: scenePoint, soundType: 2) }
SNDdefenseSoundCount += 1
}
if(currentLocation.x < beginLocation.x - dragDistance)
{
beginLocation.x = currentLocation.x
if(data.missilesAvailable > 0) { gameControl.defenseMissileShoot(vPosition: scenePoint, soundType: 2) }
SNDdefenseSoundCount += 1
}
break
case UIGestureRecognizer.State.ended:
if(data.missilesAvailable > 0)
{
if(SNDdefenseSoundCount < 2) { sound.playSoundType(vSoundType: .defenseFires) }
else { sound.playSoundType(vSoundType: .defensePans) }
}
break
default:
break
}


Related Topics



Leave a reply



Submit