Drawing a 3D Arc and Helix in Scenekit

Drawing a 3D arc and helix in SceneKit

An arc you can do with SCNShape. Start with the technique from my other answer to get an extruded, ribbon-like arc. You'll want to make sure that the part where your path traces back on itself is offset by a distance the same as your extrusion depth, so you end up with a shape that's square in cross section.

To make it circular in cross section, use the chamferProfile property — give it a path that's a quarter circle, and set the chamfer radius equal to half the extrusion depth, and the four quarter-circle chamfers will meet, forming a circular cross section.

A helix is another story. SCNShape takes a planar path — one that varies in only two dimensions — and extrudes it to make a three-dimensional solid. A helix is a path that varies in three dimensions to start with. SceneKit doesn't have anything that describes a shape in such terms, so there's no super simple answer here.

The shader modifier solution @HalMueller alludes to is interesting, but problematic. It's simple to use a modifier at the geometry entry point to make a simple bend — say, offset every y coordinate by some amount, even by an amount that's a function of why. But that's a one-dimensional transform, so you can't use it to wrap a wire around on itself. (It also changes the cross section.) And on top of that, shader modifiers happen on the GPU at render time, so their effects are an illusion: the "real" geometry in SceneKit's model is still a cylinder, so features like hit testing apply to that and not to the transformed geometry.

The best solution to making something like a helix is probably custom geometry — generating your own vertex data (SCNGeometrySource). The math for finding the set of points on a helix is pretty simple if you follow that shape's definition. To wrap a cross section around it, follow the Frenet formulas to create a local coordinate frame at each point on the helix. Then make an index buffer (SCNGeometryElement) to stitch all those points into a surface with triangles or tristrips. (Okay, that's a lot of hand-waving around a deep topic, but a full tutorial is too big for an SO answer. This should be enough of a breadcrumb to get started, though...)

SceneKit - Draw 3D Parabola

I'd recommend using SCNShape to create your parabola. To start, you'll need to represent your parabola as a Bézier curve. You can use UIBezierPath for that. For animation, I personally find shader modifiers are a nice fit for cases like this.

The Parabola

Watch out, though — you probably want a path that represents just the open stroke of the arc. If you do something like this:

let path = UIBezierPath()
path.moveToPoint(CGPointZero)
path.addQuadCurveToPoint(CGPoint(x: 100, y: 0), controlPoint: CGPoint(x: 50, y: 200))

You'll get a filled-in parabola, like this (seen in 2D in the debugger quick look, then extruded in 3D with SCNShape):

filled parabolafilled parabola in 3D

To create a closed shape that's just the arc, you'll need to trace back over the curve, a little bit away from the original:

let path = UIBezierPath()
path.moveToPoint(CGPointZero)
path.addQuadCurveToPoint(CGPoint(x: 100, y: 0), controlPoint: CGPoint(x: 50, y: 200))
path.addLineToPoint(CGPoint(x: 99, y: 0))
path.addQuadCurveToPoint(CGPoint(x: 1, y: 0), controlPoint: CGPoint(x: 50, y: 198))

open parabolaopen parabola in 3D

That's better.

... in Three-Dee!

How to actually make it 3D? Just make an SCNShape with the extrusion depth you like:

let shape = SCNShape(path: path, extrusionDepth: 10)

And set it in your scene:

shape.firstMaterial?.diffuse.contents = SKColor.blueColor()
let shapeNode = SCNNode(geometry: shape)
shapeNode.pivot = SCNMatrix4MakeTranslation(50, 0, 0)
shapeNode.eulerAngles.y = Float(-M_PI_4)
root.addChildNode(shapeNode)

Here I'm using a pivot to make the shape rotate around the major axis of the parabola, instead of the y = 0 axis of the planar Bézier curve. And making it blue. Also, root is just a shortcut I made for the view's scene's root node.

Animating

The shape of the parabola doesn't really need to change through your animation — you just need a visual effect that progressively reveals it along its x-axis. Shader modifiers are a great fit for that, because you can make the animated effect per-pixel instead of per-vertex and do all the expensive work on the GPU.

Here's a shader snippet that uses a progress parameter, varying from 0 to 1, to set opacity based on x-position:

// declare a variable we can set from SceneKit code
uniform float progress;
// tell SceneKit this shader uses transparency so we get correct draw order
#pragma transparent
// get the position in model space
vec4 mPos = u_inverseModelViewTransform * vec4(_surface.position, 1.0);
// a bit of math to ramp the alpha based on a progress-adjusted position
_surface.transparent.a = clamp(1.0 - ((mPos.x + 50.0) - progress * 200.0) / 50.0, 0.0, 1.0);

Set that as a shader modifier for the Surface entry point, and then you can animate the progress variable:

let modifier = "uniform float progress;\n #pragma transparent\n vec4 mPos = u_inverseModelViewTransform * vec4(_surface.position, 1.0);\n _surface.transparent.a = clamp(1.0 - ((mPos.x + 50.0) - progress * 200.0) / 50.0, 0.0, 1.0);"
shape.shaderModifiers = [ SCNShaderModifierEntryPointSurface: modifier ]
shape.setValue(0.0, forKey: "progress")

SCNTransaction.begin()
SCNTransaction.setAnimationDuration(10)
shape.setValue(1.0, forKey: "progress")
SCNTransaction.commit()

animating parabola

Further Considerations

Here's the whole thing in a form you can paste into a (iOS) playground. A few things left as exercises to the reader, plus other notes:

  • Factor out the magic numbers and make a function or class so you can alter the size/shape of your parabola. (Remember that you can scale SceneKit nodes relative to other scene elements, so they don't have to use the same units.)

  • Position the parabola relative to other scene elements. If you take away my line that sets the pivot, the shapeNode.position is the left end of the parabola. Change the parabola's length (or scale it), then rotate it around its y-axis, and you can make the other end line up with some other node. (For you to fire ze missiles at?)

  • I threw this together with Swift 2 beta, but I don't think there's any Swift-2-specific syntax in there — porting back to 1.2 if you need to deploy soon should be straightforward.

  • If you also want to do this on OS X, it's a bit trickier — there, SCNShape uses NSBezierPath, which unlike UIBezierPath doesn't support quadratic curves. Probably an easy way out would be to fake it with an elliptical arc.

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, how to bend an object

SCNBox has a heightSegmentCount property that you can use to produce more vertices.

Using a shader modifier you'll be able to bend the cube. That's how the grass blades are animated in Fox: Building a SceneKit Game with the Xcode Scene Editor. This can be done right in the SceneKit editor see (Enhancements to SceneKit from WWDC 2015) or programmatically.

edit:

Here is the modifier for the SCNShaderModifierEntryPointGeometry entry point:

float offset = _geometry.color.x * (sin(1.2 * u_time + (_geometry.position.x + _geometry.position.z) * 4.0) + 0.5) * 0.02;
_geometry.position.x += offset;
_geometry.color.xyz = vec3(1.0);

Bending weights are baked in the vertex color (SCNGeometrySourceSemanticColor, but you could also use texture coordinates) which is later reset to 1.0 so that they doesn't affect the final color of the geometry.

SCNShape using 3D coordinates

No! Is the simple answer to this.

The SCNShape is very strictly 2D. It only becomes 3D when bevelled/extruded, which is not a flexible, 3D deformation or modelling, just a straight extrude with rounded bevels.

3D splines as we know them in 3D modelling apps don't exist in SceneKit, and any attempt to create them is going to be a nightmare before you begin extruding them to get your filled shapes.

3D modelling apps probably don't give you dynamic abilities, but do make it 1000's of times easier to make 3D splines and then model from them.

There are ways to programmatically create your shapes in 3D apps like 3ds Max and Maya if you have data you're looking to represent, then automate export, etc. If that helps.

Connecting multiple SCNObjects in Swift Scenekit with cylinders/paths

This post: How to draw a line between two points in SceneKit? (Answer Cylinder line) should help you draw cylinder lines between two 3D points points. It handles the rotation for you, so there is less to worry about.

SceneKit transform/rotate object

Depending on what you mean by 'change shape' you have following options:

  1. If your object is simple (ex: SCNBox, SCNCone, SCNCylinder and alike) and you can change its shape the way you want using its animatable properties (ex: chamfer radius of SCNBox), then animations is simpliest way to go.
  2. If your object is SCNNode with contents loaded from external source and by change its shape you mean scale/rotate/translate, you can also use animations.
  3. If you can create complex object using graph of simple objects with acceptable performance and you can achieve desired shape change with animatable properties then you can also use animations.
  4. If your object is complex (for example, based on some mathematical model) and/or performance is main prioprity for you, then the only way to go with SceneKit is constructing custom SCNGeometries (by filling SCNGeometrySource/SCGeometryElement) from your model.

Since question have tag blender and title mentions rotate, then perhaps (2) is best for you.



Related Topics



Leave a reply



Submit