Created
March 2, 2018 12:03
-
-
Save eugenebokhan/13c1ebcbf88eea0a583ddee92057e083 to your computer and use it in GitHub Desktop.
ARSCNView extensions
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import ARKit | |
extension ARSCNView { | |
func setup() { | |
antialiasingMode = .multisampling4X | |
automaticallyUpdatesLighting = false | |
preferredFramesPerSecond = 60 | |
contentScaleFactor = 1.3 | |
if let camera = pointOfView?.camera { | |
camera.wantsHDR = true | |
camera.wantsExposureAdaptation = true | |
camera.exposureOffset = -1 | |
camera.minimumExposure = -1 | |
camera.maximumExposure = 3 | |
} | |
} | |
struct HitTestRay { | |
let origin: SCNVector3 | |
let direction: SCNVector3 | |
} | |
func hitTestRayFromScreenPos(_ point: CGPoint) -> HitTestRay? { | |
guard let frame = self.session.currentFrame else { | |
return nil | |
} | |
let cameraPos = SCNVector3.positionFromTransform(frame.camera.transform) | |
// Note: z: 1.0 will unproject() the screen position to the far clipping plane. | |
let positionVec = SCNVector3(x: Float(point.x), y: Float(point.y), z: 1.0) | |
let screenPosOnFarClippingPlane = self.unprojectPoint(positionVec) | |
var rayDirection = screenPosOnFarClippingPlane - cameraPos | |
rayDirection.normalize() | |
return HitTestRay(origin: cameraPos, direction: rayDirection) | |
} | |
func hitTestWithInfiniteHorizontalPlane(_ point: CGPoint, _ pointOnPlane: SCNVector3) -> SCNVector3? { | |
guard let ray = hitTestRayFromScreenPos(point) else { | |
return nil | |
} | |
// Do not intersect with planes above the camera or if the ray is almost parallel to the plane. | |
if ray.direction.y > -0.03 { | |
return nil | |
} | |
// Return the intersection of a ray from the camera through the screen position with a horizontal plane | |
// at height (Y axis). | |
return rayIntersectionWithHorizontalPlane(rayOrigin: ray.origin, direction: ray.direction, planeY: pointOnPlane.y) | |
} | |
func rayIntersectionWithHorizontalPlane(rayOrigin: SCNVector3, direction: SCNVector3, planeY: Float) -> SCNVector3? { | |
let direction = direction.normalized() | |
// Special case handling: Check if the ray is horizontal as well. | |
if direction.y == 0 { | |
if rayOrigin.y == planeY { | |
// The ray is horizontal and on the plane, thus all points on the ray intersect with the plane. | |
// Therefore we simply return the ray origin. | |
return rayOrigin | |
} else { | |
// The ray is parallel to the plane and never intersects. | |
return nil | |
} | |
} | |
// The distance from the ray's origin to the intersection point on the plane is: | |
// (pointOnPlane - rayOrigin) dot planeNormal | |
// -------------------------------------------- | |
// direction dot planeNormal | |
// Since we know that horizontal planes have normal (0, 1, 0), we can simplify this to: | |
let dist = (planeY - rayOrigin.y) / direction.y | |
// Do not return intersections behind the ray's origin. | |
if dist < 0 { | |
return nil | |
} | |
// Return the intersection point. | |
return rayOrigin + (direction * dist) | |
} | |
struct FeatureHitTestResult { | |
let position: SCNVector3 | |
let distanceToRayOrigin: Float | |
let featureHit: SCNVector3 | |
let featureDistanceToHitResult: Float | |
} | |
func hitTestWithFeatures(_ point: CGPoint, coneOpeningAngleInDegrees: Float, | |
minDistance: Float = 0, | |
maxDistance: Float = Float.greatestFiniteMagnitude, | |
maxResults: Int = 1) -> [FeatureHitTestResult] { | |
var results = [FeatureHitTestResult]() | |
guard let features = self.session.currentFrame?.rawFeaturePoints else { | |
return results | |
} | |
guard let ray = hitTestRayFromScreenPos(point) else { | |
return results | |
} | |
let maxAngleInDeg = min(coneOpeningAngleInDegrees, 360) / 2 | |
let maxAngle = ((maxAngleInDeg / 180) * Float.pi) | |
let points = features.__points | |
for i in 0...features.__count { | |
let feature = points.advanced(by: Int(i)) | |
let featurePos = SCNVector3(feature.pointee) | |
let originToFeature = featurePos - ray.origin | |
let crossProduct = originToFeature.cross(ray.direction) | |
let featureDistanceFromResult = crossProduct.length() | |
let hitTestResult = ray.origin + (ray.direction * ray.direction.dot(originToFeature)) | |
let hitTestResultDistance = (hitTestResult - ray.origin).length() | |
if hitTestResultDistance < minDistance || hitTestResultDistance > maxDistance { | |
// Skip this feature - it is too close or too far away. | |
continue | |
} | |
let originToFeatureNormalized = originToFeature.normalized() | |
let angleBetweenRayAndFeature = acos(ray.direction.dot(originToFeatureNormalized)) | |
if angleBetweenRayAndFeature > maxAngle { | |
// Skip this feature - is is outside of the hit test cone. | |
continue | |
} | |
// All tests passed: Add the hit against this feature to the results. | |
results.append(FeatureHitTestResult(position: hitTestResult, | |
distanceToRayOrigin: hitTestResultDistance, | |
featureHit: featurePos, | |
featureDistanceToHitResult: featureDistanceFromResult)) | |
} | |
// Sort the results by feature distance to the ray. | |
results = results.sorted(by: { (first, second) -> Bool in | |
return first.distanceToRayOrigin < second.distanceToRayOrigin | |
}) | |
// Cap the list to maxResults. | |
var cappedResults = [FeatureHitTestResult]() | |
var i = 0 | |
while i < maxResults && i < results.count { | |
cappedResults.append(results[i]) | |
i += 1 | |
} | |
return cappedResults | |
} | |
func hitTestWithFeatures(_ point: CGPoint) -> [FeatureHitTestResult] { | |
var results = [FeatureHitTestResult]() | |
guard let ray = hitTestRayFromScreenPos(point) else { | |
return results | |
} | |
if let result = self.hitTestFromOrigin(origin: ray.origin, direction: ray.direction) { | |
results.append(result) | |
} | |
return results | |
} | |
func hitTestFromOrigin(origin: SCNVector3, direction: SCNVector3) -> FeatureHitTestResult? { | |
guard let features = self.session.currentFrame?.rawFeaturePoints else { | |
return nil | |
} | |
let points = features.__points | |
// Determine the point from the whole point cloud which is closest to the hit test ray. | |
var closestFeaturePoint = origin | |
var minDistance = Float.greatestFiniteMagnitude | |
for i in 0...features.__count { | |
let feature = points.advanced(by: Int(i)) | |
let featurePos = SCNVector3(feature.pointee) | |
let originVector = origin - featurePos | |
let crossProduct = originVector.cross(direction) | |
let featureDistanceFromResult = crossProduct.length() | |
if featureDistanceFromResult < minDistance { | |
closestFeaturePoint = featurePos | |
minDistance = featureDistanceFromResult | |
} | |
} | |
// Compute the point along the ray that is closest to the selected feature. | |
let originToFeature = closestFeaturePoint - origin | |
let hitTestResult = origin + (direction * direction.dot(originToFeature)) | |
let hitTestResultDistance = (hitTestResult - origin).length() | |
return FeatureHitTestResult(position: hitTestResult, | |
distanceToRayOrigin: hitTestResultDistance, | |
featureHit: closestFeaturePoint, | |
featureDistanceToHitResult: minDistance) | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment