Skip to content

Commit

Permalink
Merge pull request #15 from Reality-Dev/main
Browse files Browse the repository at this point in the history
Smooth orientation and position changes thanks to @Reality-Dev!
  • Loading branch information
maxxfrazer authored Jan 30, 2021
2 parents 3d567c3 + be28ae2 commit 1c8dbb7
Show file tree
Hide file tree
Showing 6 changed files with 96 additions and 49 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -327,14 +327,13 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_STYLE = Automatic;
DEVELOPMENT_ASSET_PATHS = "\"FocusEntity-Example/Preview Content\"";
DEVELOPMENT_TEAM = 278494H572;
DEVELOPMENT_TEAM = "";
ENABLE_PREVIEWS = YES;
INFOPLIST_FILE = "FocusEntity-Example/Info.plist";
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = "uk.rocketar.FocusEntity-Example";
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
Expand All @@ -347,14 +346,13 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_STYLE = Automatic;
DEVELOPMENT_ASSET_PATHS = "\"FocusEntity-Example/Preview Content\"";
DEVELOPMENT_TEAM = 278494H572;
DEVELOPMENT_TEAM = "";
ENABLE_PREVIEWS = YES;
INFOPLIST_FILE = "FocusEntity-Example/Info.plist";
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = "uk.rocketar.FocusEntity-Example";
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
Expand Down
2 changes: 2 additions & 0 deletions FocusEntity-Example/FocusEntity-Example/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key>
<string>FocusEnt-Clone</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
Expand Down
8 changes: 7 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@ If you're unfamiliar with using RealityKit, I would also recommend reading my ar

Add the URL of this repository to your Xcode 11+ Project.

`https://github.com/maxxfrazer/FocusEntity.git`
Go to File > Swift Packages > Add Package Dependency, and paste in this link:
`https://github.com/maxxfrazer/FocusEntity`

---
## Usage
Expand All @@ -42,6 +43,11 @@ See the [Example](./FocusEntity-Example) for a full working example as can be se
<br>```let focusSquare = FocusEntity(on: self.arView, style: .classic)```
<br><br>(Optional)<br>
- Set `focusSquare.delegate` to an object which conforms to `FocusEntityDelegate` if you wish to get callbacks for when the FocusEntity changes state.
- Optionally, you may select to use one of 3 visual styles: classic, color, and material.
- If you choose material, you may use the preset textures or provide your own customized textures.
- If you want to provide your own textures, add them to the Assets.xcassets catalog, then type the name of the asset in the appropriate place in this code:
<br>```let onColor: MaterialColorParameter = try .texture(.load(named: "<#customAsset1#>"))```
<br>```let offColor: MaterialColorParameter = try .texture(.load(named: "<#customAsset2#>"))```


If something's not making sense in the Example, [send me a tweet](https://twitter.com/maxxfrazer) or Fork & open a Pull Request on this repository to make something more clear.
Expand Down
79 changes: 47 additions & 32 deletions Sources/FocusEntity/FocusEntity+Alignment.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,20 +14,29 @@ import Combine
extension FocusEntity {

// MARK: Helper Methods

/// Update the position of the focus square.
internal func updatePosition(){
// Average using several most recent positions.
recentFocusEntityPositions = Array(recentFocusEntityPositions.suffix(10))

// Move to average of recent positions to avoid jitter.
let average = recentFocusEntityPositions.reduce(
SIMD3<Float>(repeating: 0), { $0 + $1 }
) / Float(recentFocusEntityPositions.count)
self.position = average
}

/// Update the transform of the focus square to be aligned with the camera.
internal func updateTransform(for position: SIMD3<Float>, raycastResult: ARRaycastResult, camera: ARCamera?) {
// Average using several most recent positions.
recentFocusEntityPositions = Array(recentFocusEntityPositions.suffix(10))

// Move to average of recent positions to avoid jitter.
let average = recentFocusEntityPositions.reduce(
SIMD3<Float>(repeating: 0), { $0 + $1 }
) / Float(recentFocusEntityPositions.count)
self.position = average
if self.scaleEntityBasedOnDistance {
self.scale = SIMD3<Float>(repeating: scaleBasedOnDistance(camera: camera))
}

self.updatePosition()

//Produces odd scaling when focus entity is moving towards the user along a horizontal plane;
//looks like the focus entity is sinking downwards.
// if self.scaleEntityBasedOnDistance {
// self.scale = SIMD3<Float>(repeating: scaleBasedOnDistance(camera: camera))
// }

// Correct y rotation of camera square.
guard let camera = camera else { return }
Expand Down Expand Up @@ -56,14 +65,6 @@ extension FocusEntity {
}

internal func updateAlignment(for raycastResult: ARRaycastResult, yRotationAngle angle: Float) {
// Abort if an animation is currently in progress.
if isChangingAlignment {
return
}

var shouldAnimateAlignmentChange = false
let tempNode = SCNNode()
tempNode.simdRotation = SIMD4<Float>(0, 1, 0, angle)

// Determine current alignment
var alignment: ARPlaneAnchor.Alignment?
Expand Down Expand Up @@ -92,28 +93,32 @@ extension FocusEntity {
alignment == .vertical && verticalHistory > alignCount / 2 ||
raycastResult.anchor is ARPlaneAnchor {
if alignment != self.currentAlignment {
shouldAnimateAlignmentChange = true
isChangingAlignment = true
self.currentAlignment = alignment
self.recentFocusEntityAlignments.removeAll()
}
} else {
// Alignment is different than most of the history - ignore it
alignment = self.currentAlignment
return
}

if alignment == .vertical {
tempNode.simdOrientation = raycastResult.worldTransform.orientation
shouldAnimateAlignmentChange = true

var targetAlignment : simd_quatf
if alignment == .horizontal {
targetAlignment = simd_quatf(angle: angle, axis: [0,1,0])
} else {
targetAlignment = raycastResult.worldTransform.orientation
}

// Change the focus square's alignment
if shouldAnimateAlignmentChange {
performAlignmentAnimation(to: tempNode.simdOrientation)
// Change the focus entity's alignment
if isChangingAlignment {
//Uses interpolation.
//Needs to be called on every frame that the animation is desired, Not just the first frame.
performAlignmentAnimation(to: targetAlignment)
} else {
orientation = tempNode.simdOrientation
orientation = targetAlignment
}
}


internal func normalize(_ angle: Float, forMinimalRotationTo ref: Float) -> Float {
// Normalize angle in steps of 90 degrees such that the rotation to the other angle is minimal
Expand Down Expand Up @@ -160,9 +165,19 @@ extension FocusEntity {
return results.first(where: { $0.target == .estimatedPlane })
}

internal func performAlignmentAnimation(to newOrientation: simd_quatf) {
orientation = newOrientation
}
///Uses interpolation between orientations to create a smooth `easeOut` orientation adjustment animation.
internal func performAlignmentAnimation(to newOrientation: simd_quatf) {
//Interpolate between current and target orientations.
orientation = simd_slerp(orientation, newOrientation, 0.15)
//This length creates a normalized vector (of length 1) with all 3 components being equal.
let axisLength = 1 / sqrtf(3)
let testVector: simd_float3 = [axisLength, axisLength, axisLength]
let point1 = orientation.act(testVector)
let point2 = newOrientation.act(testVector)
let vectorsDot = simd_dot(point1, point2)
//Stop interpolating when the rotations are close enough to each other.
self.isChangingAlignment = vectorsDot < 0.999
}

/**
Reduce visual size change with distance by scaling up when close and down when far away.
Expand Down
2 changes: 1 addition & 1 deletion Sources/FocusEntity/FocusEntity+Colored.swift
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ public extension FocusEntity {
var modelMaterial = UnlitMaterial(color: .clear)
modelMaterial.baseColor = endColor
//Necessary for transparency.
modelMaterial.tintColor = Material.Color.white.withAlphaComponent(0.99)
modelMaterial.tintColor = Material.Color.white.withAlphaComponent(0.995)
self.fillPlane?.model?.materials[0] = modelMaterial
}
}
Expand Down
48 changes: 37 additions & 11 deletions Sources/FocusEntity/FocusEntity.swift
Original file line number Diff line number Diff line change
Expand Up @@ -129,9 +129,8 @@ open class FocusEntity: Entity, HasAnchoring, HasFocusEntity {
}
case let .tracking(raycastResult, camera):
let stateChanged = oldValue == .initializing
if stateChanged {
if stateChanged && self.anchor != nil {
self.anchoring = AnchoringComponent(.world(transform: Transform.identity.matrix))
recentFocusEntityPositions.removeAll()
}
if let planeAnchor = raycastResult.anchor as? ARPlaneAnchor {
entityOnPlane(for: raycastResult, planeAnchor: planeAnchor, camera: camera)
Expand All @@ -153,8 +152,11 @@ open class FocusEntity: Entity, HasAnchoring, HasFocusEntity {
public internal(set) var isAnimating = false

/// Indicates if the square is currently changing its alignment.
internal var isChangingAlignment = false
public internal(set) var isChangingAlignment = false

///A camera anchor used for placing the focus entity in front of the camera.
internal var cameraAnchor: AnchorEntity!

/// The focus square's current alignment.
internal var currentAlignment: ARPlaneAnchor.Alignment?

Expand Down Expand Up @@ -197,6 +199,9 @@ open class FocusEntity: Entity, HasAnchoring, HasFocusEntity {

self.addChild(self.positioningEntity)

cameraAnchor = AnchorEntity(.camera)
arView.scene.addAnchor(cameraAnchor)

// Start the focus square as a billboard.
displayAsBillboard()
self.delegate?.toInitializingState?()
Expand Down Expand Up @@ -230,21 +235,35 @@ open class FocusEntity: Entity, HasAnchoring, HasFocusEntity {

/// Displays the focus square parallel to the camera plane.
private func displayAsBillboard() {
self.anchoring = AnchoringComponent(.camera)
self.onPlane = false

self.transform = .init(
scale: .one, rotation: simd_quatf(angle: .pi / 2, axis: [1, 0, 0]),
translation: [0, 0, -0.8]
)
self.currentAlignment = .none
stateChangedSetup()
}

///Places the focus entity in front of the camera instead of on a plane.
private func putInFrontOfCamera(){

//Works better than arView.ray()
let newPosition = cameraAnchor.convert(position: [0,0,-1.1], to: nil)
recentFocusEntityPositions.append(newPosition)
updatePosition()
//--//
//Make focus entity face the camera with a smooth animation.
var newRotation = arView?.cameraTransform.rotation ?? simd_quatf()
newRotation *= simd_quatf(angle: .pi / 2, axis: [1,0,0])
performAlignmentAnimation(to: newRotation)
}

/// Called when a surface has been detected.
private func displayOffPlane(for raycastResult: ARRaycastResult, camera: ARCamera?) {
self.stateChangedSetup()
let position = raycastResult.worldTransform.translation
recentFocusEntityPositions.append(position)
if self.currentAlignment != .none {
//It is ready to move over to a new surface.
recentFocusEntityPositions.append(position)
} else {
putInFrontOfCamera()
}
updateTransform(for: position, raycastResult: raycastResult, camera: camera)
}

Expand All @@ -254,7 +273,12 @@ open class FocusEntity: Entity, HasAnchoring, HasFocusEntity {
self.stateChangedSetup(newPlane: !anchorsOfVisitedPlanes.contains(planeAnchor))
anchorsOfVisitedPlanes.insert(planeAnchor)
let position = raycastResult.worldTransform.translation
recentFocusEntityPositions.append(position)
if self.currentAlignment != .none {
//It is ready to move over to a new surface.
recentFocusEntityPositions.append(position)
} else {
putInFrontOfCamera()
}
updateTransform(for: position, raycastResult: raycastResult, camera: camera)
}

Expand Down Expand Up @@ -285,6 +309,8 @@ open class FocusEntity: Entity, HasAnchoring, HasFocusEntity {
case .normal = camera.trackingState,
let result = self.smartRaycast()
else {
//We should place the focus entity in front of the camera instead of on a plane.
putInFrontOfCamera()
self.state = .initializing
return
}
Expand Down

0 comments on commit 1c8dbb7

Please sign in to comment.