Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

SceneKit - Draw 3D Parabola

I'm given three points and need to draw a smooth 3D parabola. The trouble is that curved line is choppy and has some weird divots in it

parabola

Here is my code...

func drawJump(jump: Jump){
    let halfDistance = jump.distance.floatValue/2 as Float
    let tup = CalcParabolaValues(0.0, y1: 0.0, x2: halfDistance, y2: jump.height.floatValue, x3: jump.distance.floatValue, y3: 0)
    println("tuple \tup")

    var currentX = 0 as Float
    var increment = jump.distance.floatValue / Float(50)
    while currentX < jump.distance.floatValue - increment {

        let x1 = Float(currentX)
        let x2 = Float((currentX+increment))
        let y1 = calcParabolaYVal(tup.a, b: tup.b, c: tup.c, x: x1)
        let y2 = calcParabolaYVal(tup.a, b: tup.b, c: tup.c, x: x2)

        drawLine(x1, y1: y1, x2: x2, y2: y2)

        currentX += increment
    }
}

func CalcParabolaValues(x1: Float, y1: Float, x2: Float, y2: Float, x3: Float, y3: Float) -> (a: Float, b: Float, c: Float) {
    println(x1, y1, x2, y2, x3, y3)
    let a = y1/((x1-x2)*(x1-x3)) + y2/((x2-x1)*(x2-x3)) + y3/((x3-x1)*(x3-x2))
    let b = (-y1*(x2+x3)/((x1-x2)*(x1-x3))-y2*(x1+x3)/((x2-x1)*(x2-x3))-y3*(x1+x2)/((x3-x1)*(x3-x2)))
    let c = (y1*x2*x3/((x1-x2)*(x1-x3))+y2*x1*x3/((x2-x1)*(x2-x3))+y3*x1*x2/((x3-x1)*(x3-x2)))

    return (a, b, c)
}

func calcParabolaYVal(a:Float, b:Float, c:Float, x:Float)->Float{
    return a * x * x + b * x + c
}

func drawLine(x1: Float, y1: Float,x2: Float, y2: Float) {
    println("drawLine \(x1) \(y1) \(x2) \(y2)")
    let positions: [Float32] = [
        x1, y1, 0,
        x2, y2, 0
    ]
    let positionData = NSData(bytes: positions, length: sizeof(Float32)*positions.count)
    let indices: [Int32] = [0, 1]
    let indexData = NSData(bytes: indices, length: sizeof(Int32) * indices.count)

    let source = SCNGeometrySource(data: positionData, semantic: SCNGeometrySourceSemanticVertex, vectorCount: indices.count, floatComponents: true, componentsPerVector: 3, bytesPerComponent: sizeof(Float32), dataOffset: 0, dataStride: sizeof(Float32) * 3)
    let element = SCNGeometryElement(data: indexData, primitiveType: SCNGeometryPrimitiveType.Line, primitiveCount: indices.count, bytesPerIndex: sizeof(Int32))

    let line = SCNGeometry(sources: [source], elements: [element])
    self.rootNode.addChildNode( SCNNode(geometry: line))
}

func renderer(aRenderer: SCNSceneRenderer, willRenderScene scene: SCNScene, atTime time: NSTimeInterval) {
    glLineWidth(20)
}

I also have to figure out how to animate the arc from left to right. Can someone help me out? Swift or Objective C is fine. Any help is appreciated. Thanks!

like image 458
MCR Avatar asked Jul 11 '15 04:07

MCR


1 Answers

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.

like image 131
rickster Avatar answered Sep 23 '22 08:09

rickster