I'm drawing a set of triangles filled with plain color to off-screen texture.
My problem is that I get too big triangles in a result image, it looks like this:
My vertex coordinates are in pixels. (I simply generate them as a random float in (0, outTexture.width/height)). I do not multiply them by any projection in my vertex function (maybe that is my mistake?)
So my question is how does vertex coordinates correlate with pixel coordinate?
The solution was to make ortho projection and pass it as a uniform.
Here is the code that worked for me:
func makeOrthographicMatrix(left: Float, right: Float, bottom: Float, top: Float, near: Float, far: Float) -> [Float] {
let ral = right + left
let rsl = right - left
let tab = top + bottom
let tsb = top - bottom
let fan = far + near
let fsn = far - near
return [2.0 / rsl, 0.0, 0.0, 0.0,
0.0, 2.0 / tsb, 0.0, 0.0,
0.0, 0.0, -2.0 / fsn, 0.0,
-ral / rsl, -tab / tsb, -fan / fsn, 1.0]
}
And I called that like so :
var projection = makeOrthographicMatrix(0.0, right: Float(inTexture.width), bottom: 0.0, top: Float(inTexture.height), near: -1.0, far: 1.0)
Related
I have a scene of a cube with vertices defined like so
const positions = [
-10.0, 10.0,
10.0, 10.0,
-10.0, -10.0,
10.0, -10.0
];
I'm trying to go from a field of view perspective matrix (not shown) to a specified rectangle perspective projection matrix defined like so
const n = 0.1;
const f = 100.0;
const l = -50;
const r = 50;
const t = 50;
const b = -50;
Float32Array([
(2*n/r-l), 0.0, 0.0, 0.0,
0.0, (2*n/t-b), 0.0, 0.0,
(r+l/r-l), (t+b/t-b), -(f+n/f-n), -1.0,
0.0, 0.0, -(2*f*n/f-n), 0.0
]);
I also have a model matrix that moves the box -6 units in the z index so that it's within the bounds of the near and far clip planes.
Am I right to assume that before transforming anything the coordinates I use to specify the box and perspective matrix are in the same space/frame of reference? Therefore the box should be dead center of the view?
The box renders with the field of view matrix, but not the matrix defined above.
The formulas are not exact. Try this (I have added parentheses only):
Float32Array([
(2*n)/(r-l), 0.0, 0.0, 0.0,
0.0, (2*n)/(t-b), 0.0, 0.0,
(r+l)/(r-l), (t+b)/(t-b), -(f+n)/(f-n), -1.0,
0.0, 0.0, -(2*f*n)/(f-n), 0.0
]);
Goal: Define the normal vector for a plane represented by SCNPlane.
As mentioned in Apple's documentation Working with Vectors - Calculate the Normal of a Triangle, I can calculate the normal vector based on 3 points from the plane. The issue here is that I don't know how I can get 3 points that they make proper triangle. I've noticed that SCNPlane has a property boundingBox and it can represent 2 of the triangle vertices (min and max). How can I find the 3rd vertex that is placed on the SCNPlane? I cannot use the center of the SCNPlane, because it creates a line along with min and max points from boundingBox.
Is there any other way that can help me to retrieve a normal vector for the SCNPlane?
From the documentation we learn that
The surface is one-sided. Its surface normal vectors point in the positive z-axis direction of its local coordinate space, so it is only visible from that direction by default.
The normal of a SCNPlane is always (0, 0, 1) in local space and that cannot change.
When it is attached to a node, the orientation of that node determines the normal in an another coordinate system. You can use simdConvertVector:toNode: to convert between coordinate spaces:
// normal expressed in world space
let normal = simd_normalize(node.simdConvertVector(simd_float3(0, 0, 1), to: nil))
To add to the accepted answer, in order to retrieve the points that define the plane, you can query the plane's geometry sources.
let plane = SCNPlane(width: 100, height: 20)
print("Sources for normal: \(vertices(sources: plane.sources(for: .normal)))")
print("Sources for vertex: \(vertices(sources: plane.sources(for: .vertex)))")
extension UnsafeRawPointer {
func loadUnaligned<T>(as: T.Type, count: Int) -> [T] {
assert(_isPOD(T.self)) // relies on the type being POD (no refcounting or other management)
let buffer = UnsafeMutablePointer<T>.allocate(capacity: count)
defer { buffer.deallocate() }
memcpy(buffer, self, MemoryLayout<T>.size * count)
return (0..<count).map({ index in buffer.advanced(by: index).pointee })
}
}
func vertices(sources: [SCNGeometrySource]) -> [[SCNVector3]] {
var result = [[SCNVector3]]()
result.reserveCapacity(sources.count)
for source in sources {
precondition(source.usesFloatComponents == true, "SCNVector3 can handle only three-component vectors whose components are floating-point values, i.e., floats or doubles")
precondition(source.componentsPerVector == 3, "SCNVector3 can only be used for three components per vector")
let shouldUseFloatNotDouble: Bool
if source.bytesPerComponent == 4 {
shouldUseFloatNotDouble = true
}
else if source.bytesPerComponent == 8 {
shouldUseFloatNotDouble = false
}
else {
assert(false, "The SCNGeometrySource has reported an unexpected byte size for its vector components, not 4 bytes (float) or 8 bytes (double) but \(source.bytesPerComponent). I am not equipped for this so I am going to use floats and hope for the best. This will probably not work. Sorry.")
shouldUseFloatNotDouble = true
}
let vectors = source.data.withUnsafeBytes {
(p: UnsafeRawBufferPointer) -> [SCNVector3] in
if (shouldUseFloatNotDouble) {
let simdArray = (p.baseAddress! + source.dataOffset).loadUnaligned(as: SIMD3<Float>.self, count: source.vectorCount)
return simdArray.map { simd in SCNVector3(simd)}
} else {
let simdArray = (p.baseAddress! + source.dataOffset).loadUnaligned(as: SIMD3<Double>.self, count: source.vectorCount)
return simdArray.map { simd in SCNVector3(simd)}
}
}
result.append(vectors)
}
return result
}
Output:
Sources for normal: [[__C.SCNVector3(x: 0.0, y: 0.0, z: 1.0), __C.SCNVector3(x: 1.0, y: 0.5, z: -0.5), __C.SCNVector3(x: 0.0, y: 0.0, z: 1.0), __C.SCNVector3(x: 1.0, y: -0.5, z: 0.5)]]
Sources for vertex: [[__C.SCNVector3(x: -0.5, y: -0.5, z: 0.0), __C.SCNVector3(x: 0.0, y: 1.0, z: 0.0), __C.SCNVector3(x: 0.5, y: -0.5, z: 0.0), __C.SCNVector3(x: 0.0, y: 1.0, z: 1.0)]]
I'm writing a game in Swift 5 (but had the same problem with Swift 4 which i recently updated by game from), with all my SCNNode's centered around SCNVector3Zero. In other word's (0,0,0) is at the center of my play area.
The SCNCamera is attached to a SNCNode and positioned just outside the limits of the play area, looking at (0,0,0).
cameraNode.look(at: SCNVector3Zero)
If I place 5 nodes in the scene, all on the y=0 plane, (0,0,0), (-1,0,-1), (1,0,1), (-1,0,1) and (1,0,-1) like the face of a dice showing five, it all works great. I can rotate around the scene with the node at (0,0,0) staying centered with no movement.
If I add a 2nd row, so the first row moves to y=1 and the second row gets nodes with y=-1 the scene wobbles a little when rotating.
The further a node is moved from the center, the more exaggerated this wobble becomes.
Here's the code setting up the scene (this example has three rows and looks like a three dimensional "five" face of a dice, point in the center at (0,0,0) and the other dots at each corner of a cube) ...
addSphere(
x: 0.0,
y: 0.0,
z: 0.0,
radius: radius,
textureName: "earth")
addSphere(
x: -2.0 * spacing,
y: -2.0 * spacing,
z: 0.0,
radius: radius,
textureName: "earth")
addSphere(
x: 2.0 * spacing,
y: 2.0 * spacing,
z: 0.0,
radius: radius,
textureName: "earth")
addSphere(
x: -2.0 * spacing,
y: 2.0 * spacing,
z: 0.0,
radius: radius,
textureName: "granite")
addSphere(
x: 2.0 * spacing,
y: -2.0 * spacing,
z: 0.0,
radius: radius,
textureName: "granite")
addSphere(
x: 0.0,
y: -2.0 * spacing,
z: -2.0 * spacing,
radius: radius,
textureName: "slime")
addSphere(
x: 0.0,
y: 2.0 * spacing,
z: 2.0 * spacing,
radius: radius,
textureName: "slime")
addSphere(
x: 0.0,
y: 2.0 * spacing,
z: -2.0 * spacing,
radius: radius,
textureName: "wood")
addSphere(
x: 0.0,
y: -2.0 * spacing,
z: 2.0 * spacing,
radius: radius,
textureName: "wood")
This code behaves, everything is symmetrical around (0,0,0) with the node at (0,0,0) staying exactly where it should.
If I introduce this node into the scene, it all goes badly wrong ...
addSphere(
x: 6.0,
y: 6.0,
z: 6.0,
radius: radius,
textureName: "earth")
I've tried adding a fixed physics body to each node that has a mass of zero to no avail. It's like the camera is no longer looking directly at (0,0,0) but is influenced but the nodes in the scene.
I've tried all sorts of permutations of adding nodes, and on some tests it appears adding anything with value for the z-axis caused problems
This is the addSphere method ...
internal func addSphere(x: Float, y: Float, z: Float, radius: Float, color: UIColor, textureName: String) -> SCNNode {
let sphereGeometry = SCNSphere(radius: CGFloat(radius))
sphereGeometry.firstMaterial?.diffuse.contents = UIImage(imageLiteralResourceName: textureName)
let sphereNode = SCNNode(geometry: sphereGeometry)
sphereNode.name = "dot"
sphereNode.position = SCNVector3(x: x, y: y, z: z)
sphereNode.lines = [];
sphereNode.ignoreTaps = false
sphereNode.categoryBitMask = NodeBitMasks.dot
//sphereNode.physicsBody = SCNPhysicsBody(type: .static, shape: nil)
self.rootNode.addChildNode(sphereNode)
return sphereNode
}
There are no errors and as you might expect, any point positioned at (0,0,0) should remain static when rotating about while "looking at" this point.
Any pointers would be really appreciated as I can't make heads or tails of why it's behaving like this.
I assume you use the SCNView.allowsCameraControl = true to move the camera around. This built-in setting of SceneKit is actually only for debug purposes of your scene. It is not suited for anything when you want to have dedicated control over your camera movement.
You should instead try to implement a camera orbit, see https://stackoverflow.com/a/25674762/3358138.
I could reproduce your problem with the "wobbling" center of your scene, see Playground Gist https://gist.github.com/dirkolbrich/e2c247619b28a287c464abbc0595e23c.
A camera orbit solves this "wobbling" and let’s the camera stay on center, see Playground Gist https://gist.github.com/dirkolbrich/9e4dffb3026d0540d6edf6877f27d1e4.
I'm drawing bar graphs, and I have several stacked CGRects that are directly on top of each other (i.e. one rect's minY is the previous rect's maxY). However, there are still semi-transparent gaps between the rects. Is there any way to fix this? I've found that this also happens when drawing touching adjacent arcs.
Here's a screenshot of what I mean:
By zooming in, I've confirmed that this isn't just an optical illusion like one would find between adjacent red and blue rects. I would appreciate any input.
var upToNowSegmentTotal: CGFloat = 0
for k in 0..<data[i].bars[j].segments.count {
var segmentRect: CGRect = CGRect()
if barDirection == "vertical" {
let a: CGFloat = translateY(upToNowSegmentTotal)
let b: CGFloat = translateY(upToNowSegmentTotal + data[i].bars[j].segments[k].length)
upToNowSegmentTotal += data[i].bars[j].segments[k].length
var rectY: CGFloat
if a > b {
rectY = b
} else {
rectY = a
}
segmentRect = CGRect(
x: barWidthPosition,
y: rectY,
width: barWidthAbsolute,
height: abs(a - b)
)
}
}
Ignore the stuff about the width of the bars. Here's the translateY function. Basically, it translates coordinates from the graphing window into x/y stuff that's drawn. Remember that because the window/ graphing area does not change between drawn rects, the same y input will always produce the same result.
private func translateY(y: CGFloat) -> CGFloat {
if barsAreReversed {
return graphingArea.minY + graphingArea.height * (y - graphingWindow.startValue) / (graphingWindow.length)
} else {
return graphingArea.maxY - graphingArea.height * (y - graphingWindow.startValue) / (graphingWindow.length)
}
}
EDIT 2:
Here's a simplified version of my code that shows the problem:
override func drawRect(rect: CGRect) {
let rect1: CGRect = CGRect(
x: 0,
y: 0,
width: 40,
height: 33.7
)
let rect2: CGRect = CGRect(
x: 0,
y: rect1.height,
width: 40,
height: 33.5
)
let context: CGContextRef = UIGraphicsGetCurrentContext()
CGContextSetFillColorWithColor(context, UIColor(red: 1 / 255, green: 29 / 255, blue: 29 / 255, alpha: 1).CGColor)
CGContextAddRect(context, rect1)
CGContextFillRect(context, rect1)
CGContextSetFillColorWithColor(context, UIColor(red: 9 / 255, green: 47 / 255, blue: 46 / 255, alpha: 1).CGColor)
CGContextAddRect(context, rect2)
CGContextFillRect(context, rect2)
}
It produces this:
I suspect that in this particular case, the rects you are filling are not integral, i.e they might have origins/heights that are by default rendered with slightly transparent pixels (anti-aliasing). You could avoid this by properly rounding your Y-axis translation
private func translateY(y: CGFloat) -> CGFloat {
if barsAreReversed {
return round(graphingArea.minY + graphingArea.height * (y - graphingWindow.startValue) / (graphingWindow.length))
} else {
return round(graphingArea.maxY - graphingArea.height * (y - graphingWindow.startValue) / (graphingWindow.length))
}
}
With arcs and other shapes it is not as easy, however, you could try and get rid of it, by leaving a bit of overlap between shapes. Of course, as pointed out by matt, you could simply turn anti-aliasing off, in which case these transparent "half-pixels" will all be rendered as if they are actually fully-within the rect.
This is likely happening because the rectangle coordinates you are using to draw shapes are fractional values. As a result Core Graphics performs antialiasing at the edges of those rectangles when your coordinates land between pixel boundaries.
You could solve this by simply rounding the coordinates of the rectangles before drawing. You can use the CGRectIntegral function which performs this kind of rounding, for example:
CGContextFillRect(context, CGRectIntegral(rect1))
It's antialiasing. I can prevent this phenomenon by using your exact same code but drawing in a CGContext in which we have first called CGContextSetAllowsAntialiasing(context, false). Here it is without that call:
And here it is with that call:
But, as others have said, we can get the same result by changing your 33.7 and 33.5 to 40, so that we come down on pixel boundaries.
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
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!
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):
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))
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()
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.
I don't think your table has enough points, assuming the renderer is connecting them with straight line segments. On top of this, the thickness and dashing of the line make it difficult to see that. Try getting a smooth curve with a thin solid line first.
If you want to animate the progression of the curve, as if it were showing the flight of a projectile, it will probably be easiest to just write your function for the motion: y = k*x^2, and just render from x=0 to x=T for increasing values of T.