-
-
Save ThomasGorisse/f4f7614d4b6f672c25718bc70346f706 to your computer and use it in GitHub Desktop.
package io.github.sceneview.components | |
import android.animation.ObjectAnimator | |
import com.google.android.filament.* | |
import com.google.ar.sceneform.rendering.* | |
import dev.romainguy.kotlin.math.* | |
import io.github.sceneview.* | |
import io.github.sceneview.Entity | |
import io.github.sceneview.animation.TransformAnimator | |
import io.github.sceneview.gesture.* | |
import io.github.sceneview.math.* | |
import io.github.sceneview.scene.modelMatrix | |
import io.github.sceneview.scene.projectionMatrix | |
import io.github.sceneview.scene.viewMatrix | |
const val defaultSmoothSpeed = 1.0f | |
interface HasTransformComponent : HasComponent { | |
val transformManager get() = engine.transformManager | |
val transformInstance: EntityInstance get() = transformManager.getInstance(entity) | |
/** | |
* Returns whether a particular [Entity] is associated with a component of the | |
* [TransformManager] | |
* @return true if this [Entity] has a component associated with this manager | |
*/ | |
val hasComponent get() = transformManager.hasComponent(entity) | |
/** | |
* ### The node position to locate it within the coordinate system of its parent | |
* | |
* Default is `Position(x = 0.0f, y = 0.0f, z = 0.0f)`, indicating that the node is placed at | |
* the origin of the parent node's coordinate system. | |
* | |
* **Horizontal (X):** | |
* - left: x < 0.0f | |
* - center horizontal: x = 0.0f | |
* - right: x > 0.0f | |
* | |
* **Vertical (Y):** | |
* - top: y > 0.0f | |
* - center vertical : y = 0.0f | |
* - bottom: y < 0.0f | |
* | |
* **Depth (Z):** | |
* - forward: z < 0.0f | |
* - origin/camera position: z = 0.0f | |
* - backward: z > 0.0f | |
* | |
* ------- +y ----- -z | |
* | |
* ---------|----/---- | |
* | |
* ---------|--/------ | |
* | |
* -x - - - 0 - - - +x | |
* | |
* ------/--|--------- | |
* | |
* ----/----|--------- | |
* | |
* +z ---- -y -------- | |
*/ | |
var position: Position | |
get() = transform.position | |
set(value) { | |
transform = Transform(value, quaternion, scale) | |
} | |
/** | |
* ### The node world-space position | |
* | |
* The world position of this node (i.e. relative to the [SceneView]). | |
* This is the composition of this component's local position with its parent's world | |
* position. | |
* | |
* @see worldTransform | |
*/ | |
var worldPosition: Position | |
get() = worldTransform.position | |
set(value) { | |
position = worldToParent * value | |
} | |
/** | |
* TODO: Doc | |
*/ | |
var quaternion: Quaternion | |
get() = transform.quaternion | |
set(value) { | |
transform = Transform(position, value, scale) | |
} | |
/** | |
* ### The node world-space quaternion | |
* | |
* The world quaternion of this node (i.e. relative to the [SceneView]). | |
* This is the composition of this component's local quaternion with its parent's world | |
* quaternion. | |
* | |
* @see worldTransform | |
*/ | |
var worldQuaternion: Quaternion | |
get() = worldTransform.toQuaternion() | |
set(value) { | |
quaternion = worldToParent.toQuaternion() * value | |
} | |
/** | |
* ### The node orientation in Euler Angles Degrees per axis from `0.0f` to `360.0f` | |
* | |
* The three-component rotation vector specifies the direction of the rotation axis in degrees. | |
* Rotation is applied relative to the node's origin property. | |
* | |
* Default is `Rotation(x = 0.0f, y = 0.0f, z = 0.0f)`, specifying no rotation. | |
* | |
* Note that modifying the individual components of the returned rotation doesn't have any effect. | |
* | |
* ------- +y ----- -z | |
* | |
* ---------|----/---- | |
* | |
* ---------|--/------ | |
* | |
* -x - - - 0 - - - +x | |
* | |
* ------/--|--------- | |
* | |
* ----/----|--------- | |
* | |
* +z ---- -y -------- | |
*/ | |
var rotation: Rotation | |
get() = quaternion.toEulerAngles() | |
set(value) { | |
quaternion = Quaternion.fromEuler(value) | |
} | |
/** | |
* ### The node world-space rotation | |
* | |
* The world rotation of this node (i.e. relative to the [SceneView]). | |
* This is the composition of this component's local rotation with its parent's world | |
* rotation. | |
* | |
* @see worldTransform | |
*/ | |
var worldRotation: Rotation | |
get() = worldTransform.rotation | |
set(value) { | |
quaternion = worldToParent.toQuaternion() * Quaternion.fromEuler(value) | |
} | |
/** | |
* ### The node scale on each axis. | |
* | |
* Reduce (`scale < 1.0f`) / Increase (`scale > 1.0f`) | |
*/ | |
var scale: Scale | |
get() = transform.scale | |
set(value) { | |
transform = Transform(position, quaternion, value) | |
} | |
/** | |
* ### The node world-space scale | |
* | |
* The world scale of this node (i.e. relative to the [SceneView]). | |
* This is the composition of this component's local scale with its parent's world | |
* scale. | |
* | |
* @see worldTransform | |
*/ | |
var worldScale: Scale | |
get() = worldTransform.scale | |
set(value) { | |
scale = (worldToParent * scale(value)).scale | |
} | |
/** | |
* @see TransformManager.getTransform | |
* @see TransformManager.setTransform | |
*/ | |
var transform: Transform | |
get() = transformManager.getTransform(transformInstance) | |
set(value) { | |
transformManager.setTransform(transformInstance, value) | |
} | |
/** | |
* @see TransformManager.getWorldTransform | |
*/ | |
val worldTransform: Transform | |
get() = transformManager.getWorldTransform(transformInstance) | |
/** | |
* ### The transform from the world coordinate system to the coordinate system of the parent node | |
*/ | |
val worldToParent: Transform | |
get() = parentInstance?.let { | |
inverse(transformManager.getWorldTransform(it)) | |
} ?: Transform() | |
/** | |
* @see TransformManager.getParent | |
* @see TransformManager.setTransform | |
*/ | |
var parentEntity: Entity? | |
get() = transformManager.getParent(transformInstance).takeIf { it != 0 } | |
set(value) { | |
transformManager.setParent(transformInstance, value?.let { | |
transformManager.getInstance(value) | |
} ?: 0) | |
} | |
val parentInstance: EntityInstance? | |
get() = parentEntity?.let { transformManager.getInstance(it) } | |
/** | |
* Returns the number of children of an [EntityInstance]. | |
* | |
* @return The number of children of the queried component. | |
*/ | |
val childCount: Int | |
get() = transformManager.getChildCount(transformInstance) | |
/** | |
* Gets a list of children for a transform component. | |
* | |
* @param count The maximum number of children to retrieve. | |
* @return Array of retrieved children [Entity]. | |
*/ | |
val childEntities: IntArray | |
get() = transformManager.getChildren(transformInstance, childCount) | |
/** | |
* Creates a transform component and associates it with the given entity. The component is | |
* initialized with the identity transform. | |
* If this component already exists on the given entity, it is first | |
* destroyed as if [.destroy] was called. | |
* | |
* @see .destroy | |
*/ | |
fun createComponent(): EntityInstance = transformManager.create(entity) | |
/** | |
* Gets a list of children for a transform component. | |
* | |
* @param count The maximum number of children to retrieve. | |
* @return Array of retrieved children [Entity]. | |
*/ | |
fun getChildEntities(count: Int) = transformManager.getChildren(transformInstance, count) | |
/** | |
* ### The node scale | |
* | |
* - reduce size: scale < 1.0f | |
* - same size: scale = 1.0f | |
* - increase size: scale > 1.0f | |
*/ | |
fun setScale(scale: Float) { | |
this.scale.xyz = Scale(scale) | |
} | |
/** | |
* ## Change the node transform | |
* | |
* @see position | |
* @see quaternion | |
* @see scale | |
*/ | |
fun transform( | |
position: Position = this.position, | |
quaternion: Quaternion = this.quaternion, | |
scale: Scale = this.scale, | |
smooth: Boolean = false, | |
smoothSpeed: Float = defaultSmoothSpeed | |
) { | |
if (smooth) { | |
smooth(position, quaternion, scale, smoothSpeed) | |
} else { | |
this.position = position | |
this.quaternion = quaternion | |
this.scale = scale | |
} | |
} | |
/** | |
* ## Change the node transform | |
* | |
* @see position | |
* @see rotation | |
* @see scale | |
*/ | |
fun transform( | |
position: Position = this.position, | |
rotation: Rotation = this.rotation, | |
scale: Scale = this.scale, | |
smooth: Boolean = false, | |
smoothSpeed: Float = defaultSmoothSpeed | |
) = transform(position, rotation.toQuaternion(), scale, smooth, smoothSpeed) | |
/** | |
* ## Smooth move, rotate and scale at a specified speed | |
* | |
* @see position | |
* @see quaternion | |
* @see scale | |
* @see speed | |
*/ | |
fun smooth( | |
position: Position = this.position, | |
quaternion: Quaternion = this.quaternion, | |
scale: Scale = this.scale, | |
speed: Float = defaultSmoothSpeed | |
) = smooth(Transform(position, quaternion, scale), speed) | |
/** | |
* ## Smooth move, rotate and scale at a specified speed | |
* | |
* @see position | |
* @see quaternion | |
* @see scale | |
* @see speed | |
*/ | |
fun smooth( | |
position: Position = this.position, | |
rotation: Rotation = this.rotation, | |
scale: Scale = this.scale, | |
speed: Float = defaultSmoothSpeed | |
) = smooth(Transform(position, rotation, scale), speed) | |
/** | |
* ## Smooth move, rotate and scale at a specified speed | |
* | |
* @see transform | |
*/ | |
fun smooth(transform: Transform, speed: Float = defaultSmoothSpeed) { | |
val maxLength = floatArrayOf( | |
distance(this.position, transform.position), | |
length(this.quaternion - transform.quaternion), | |
distance(this.scale, transform.scale) | |
).maxOrNull()!! | |
TransformAnimator.ofTransform(this, transform).apply { | |
setAutoCancel(true) | |
duration = (maxLength * speed).toLong() | |
}.start() | |
} | |
fun animatePositions(vararg positions: Position): ObjectAnimator = | |
TransformAnimator.ofPosition(this, *positions) | |
fun animateQuaternions(vararg quaternions: Quaternion): ObjectAnimator = | |
TransformAnimator.ofQuaternion(this, *quaternions) | |
fun animateRotations(vararg rotations: Rotation): ObjectAnimator = | |
TransformAnimator.ofRotation(this, *rotations) | |
fun animateScales(vararg scales: Scale): ObjectAnimator = | |
TransformAnimator.ofScale(this, *scales) | |
fun animateTransforms(vararg transforms: Transform): ObjectAnimator = | |
TransformAnimator.ofTransform(this, *transforms) | |
// | |
// /** | |
// * ### Places the node at eye and rotates the node to face a point in world-space | |
// * | |
// * @param targetPosition The node position in local space | |
// * @param targetPosition The target position to look at in world space | |
// * @param upDirection The up direction will determine the orientation of the node around the direction | |
// * @param smooth Whether the rotation should happen smoothly | |
// */ | |
// fun lookAt( | |
// eyePosition: Position, | |
// targetPosition: Position, | |
// upDirection: Direction = Direction(y = 1.0f), | |
// smooth: Boolean = false | |
// ) { | |
// position = eyePosition | |
// val newQuaternion = lookAt( | |
// targetPosition, | |
// worldPosition, | |
// upDirection | |
// ).toQuaternion() | |
// if (smooth) { | |
// smooth(quaternion = newQuaternion) | |
// } else { | |
// transform(quaternion = newQuaternion) | |
// } | |
// } | |
/** | |
* ### Rotates the node to face a point in world-space | |
* | |
* @param targetPosition The target position to look at in world space | |
* @param upDirection The up direction will determine the orientation of the node around the direction | |
* @param smooth Whether the rotation should happen smoothly | |
*/ | |
fun lookAt( | |
targetPosition: Position, | |
upDirection: Direction = Direction(y = 1.0f), | |
smooth: Boolean = false | |
) { | |
val newQuaternion = lookAt( | |
worldPosition, | |
targetPosition, | |
upDirection | |
).toQuaternion() | |
if (smooth) { | |
smooth(quaternion = newQuaternion) | |
} else { | |
transform(quaternion = newQuaternion) | |
} | |
} | |
/** | |
* ### Rotates the node to face another node | |
* | |
* @param targetNode The target node to look at | |
* @param upDirection The up direction will determine the orientation of the node around the direction | |
* @param smooth Whether the rotation should happen smoothly | |
*/ | |
fun lookAt( | |
targetTransformable: HasTransformComponent, | |
upDirection: Direction = Direction(y = 1.0f), | |
smooth: Boolean = false | |
) = lookAt(targetTransformable.worldPosition, upDirection, smooth) | |
/** | |
* ### Rotates the node to face a direction in world-space | |
* | |
* The look direction and up direction cannot be coincident (parallel) or the orientation will | |
* be invalid. | |
* | |
* @param lookDirection The desired look direction in world-space | |
* @param upDirection The up direction will determine the orientation of the node around the look direction | |
* @param smooth Whether the rotation should happen smoothly | |
*/ | |
fun lookTowards( | |
lookDirection: Direction, | |
upDirection: Direction = Direction(y = 1.0f), | |
smooth: Boolean = false | |
) { | |
val newQuaternion = lookTowards(worldPosition, -lookDirection, upDirection).toQuaternion() | |
if (smooth) { | |
smooth(quaternion = newQuaternion) | |
} else { | |
transform(quaternion = newQuaternion) | |
} | |
} | |
} | |
/** | |
* @see clipSpaceToViewSpace | |
*/ | |
fun View.screenToClipSpace(screenPosition: Position) = | |
screenPosition / Float2(x = viewport.width.toFloat(), y = viewport.height.toFloat()) | |
/** | |
* @see viewSpaceToClipSpace | |
*/ | |
fun View.clipSpaceToScreen(clipSpacePosition: Position) = | |
clipSpacePosition * Float2(x = viewport.width.toFloat(), y = viewport.height.toFloat()) | |
/** | |
* @see View.screenToClipSpace | |
*/ | |
fun Camera.clipSpaceToViewSpace(clipSpacePosition: Position) = | |
inverse(projectionMatrix) * | |
(clipSpacePosition * 2.0f - 1.0f) | |
/** | |
* @see Camera.clipSpaceToViewSpace | |
*/ | |
fun Camera.viewSpaceToWorld(viewSpacePosition: Position) = | |
modelMatrix * (viewSpacePosition + 1.0f) / 2.0f | |
/** | |
* @see View.clipSpaceToScreen | |
*/ | |
fun Camera.viewSpaceToClipSpace(viewSpacePosition: Position) = | |
projectionMatrix * viewSpacePosition | |
/** | |
* @see viewSpaceToClipSpace | |
*/ | |
fun Camera.worldToViewSpace(worldPosition: Position) = | |
(projectionMatrix * viewMatrix) * worldPosition | |
/** | |
* Get a world space position from a screen space position | |
* | |
* @param x (0..View width) = (left..right) | |
* The X value is negative when the point is left of the viewport, between 0 and the width of | |
* the [SceneView] when the point is within the viewport, and greater than the width when | |
* the point is to the right of the viewport. | |
* | |
* @param y (0..View height) = (top..bottom) | |
* The Y value is negative when the point is below the viewport, between 0 and the height of | |
* the [SceneView] when the point is within the viewport, and greater than the height when | |
* the point is above the viewport. | |
* | |
* @param z (0..1) (far..near) | |
* The Z value is used for the depth between 1 and 0 (1=near, 0=infinity). | |
* | |
* @return a new Position that represents the point in screen-space. | |
* | |
* @see SceneView.worldToScreen | |
*/ | |
fun SceneView.screenToWorld(x: Float, y: Float, z: Float = 1.0f): Position { | |
// Invert Y because screen Y points down and Filament points up | |
val clipSpacePosition = view.screenToClipSpace(Position(x, -y, z)) | |
val viewSpacePosition = camera.clipSpaceToViewSpace(clipSpacePosition) | |
return camera.viewSpaceToWorld(viewSpacePosition) | |
} | |
/** | |
* Get a screen space position from a world space position | |
* | |
* @param worldPosition The world position to convert | |
* | |
* @return Screen space position in Android device screen coordinates within the SceneView: | |
* TopLeft = (0, 0), BottomRight = (SceneView Width, SceneView Height). | |
* - x (0..View width) = (left..right) | |
* - y (0..View height) = (top..bottom) | |
* - z (0..1) (far..near) | |
* | |
* The device coordinate space is unaffected by the orientation of the device | |
* | |
* @see SceneView.screenToWorld | |
*/ | |
fun SceneView.worldToScreen(worldPosition: Position): Position { | |
val viewSpacePosition = camera.worldToViewSpace(worldPosition) | |
val clipSpacePosition = camera.viewSpaceToClipSpace(viewSpacePosition) | |
return view.clipSpaceToScreen(clipSpacePosition).apply { | |
// Invert Y because Filament points up and screen Y points down. | |
y = -y | |
} | |
} |
Camera.clipSpaceToViewSpace
It seems that a correct equation should be inverse(projectionMatrix) * clipSpacePosition.xyz * clipSpacePosition.w.
I have to admit that I'm a little confused between those 2 answers:
So viewSpaceToClipSpace
should also be divided by w
?
fun Camera.viewSpaceToClipSpace(viewSpacePosition: Position) =
projectionMatrix * viewSpacePosition.xyz / viewSpacePosition.w
Here it is?
typealias ClipSpacePosition = Float4
fun Camera.clipSpaceToViewSpace(clipSpacePosition: ClipSpacePosition) : Position =
inverse(projectionMatrix) * clipSpacePosition.xyz * clipSpacePosition.w
fun Camera.viewSpaceToClipSpace(viewSpacePosition: Position): ClipSpacePosition {
val clipSpacePosition = projectionMatrix * ClipSpacePosition(viewSpacePosition.xyz, 1.0f)
return ClipSpacePosition(clipSpacePosition.xyz / clipSpacePosition.w, w = clipSpacePosition.w)
}
fun Camera.viewSpaceToWorld(viewSpacePosition: Position) : Position =
inverse(viewMatrix) * viewSpacePosition
fun Camera.worldToViewSpace(worldPosition: Position) : Position =
(projectionMatrix * viewMatrix) * worldPosition
fun FilamentView.clipSpaceToViewPort(clipSpacePosition: ClipSpacePosition): Position {
val viewPortSize = Float2(x = viewport.width.toFloat(), y = viewport.height.toFloat())
return ((clipSpacePosition + 1.0f) * 0.5f * viewPortSize).xyz
}
fun FilamentView.clipSpaceToViewSpace(clipSpacePosition: ClipSpacePosition): Position =
camera!!.clipSpaceToViewSpace(clipSpacePosition)
fun FilamentView.viewSpaceToWorld(viewSpacePosition: Position): Position =
camera!!.viewSpaceToWorld(viewSpacePosition)
fun FilamentView.worldToViewSpace(worldPosition: Position): Position =
camera!!.worldToViewSpace(worldPosition)
fun FilamentView.viewSpaceToClipSpace(viewSpacePosition: Position): ClipSpacePosition =
camera!!.viewSpaceToClipSpace(viewSpacePosition)
fun FilamentView.viewportToWorld(x: Float, y: Float, z: Float = 1.0f): Position {
val clipSpacePosition = viewPortToClipSpace(Position(x, y, z))
val viewSpacePosition = clipSpaceToViewSpace(clipSpacePosition)
return viewSpaceToWorld(viewSpacePosition)
}
fun FilamentView.worldToViewport(worldPosition: Position): Position {
val viewSpacePosition = worldToViewSpace(worldPosition)
val clipSpacePosition = viewSpaceToClipSpace(viewSpacePosition)
return clipSpaceToViewPort(clipSpacePosition)
}
fun viewToWorld(x: Float, y: Float, z: Float = 1.0f) = filamentView.viewportToWorld(
x = x,
// Invert Y because SceneView Y points down and Filament points up
y = height - 1 - y,
z = z
)
fun SceneView.worldToView(worldPosition: Position) = filamentView.worldToViewport(worldPosition).apply {
// Invert Y because Filament points up and screen Y points down.
y = height - 1 - y
}
So viewSpaceToClipSpace should also be divided by w?
Yes, we should manually do what OpenGL does internally.
Here it is?
In Camera.viewSpaceToClipSpace
we can divide only x
, y
and z
to be able to use w
later to convert the coordinates back.
If we don't expect the Filament camera
to be null
, can we use !!
to simplify the code?
In Camera.viewSpaceToClipSpace we can divide only x, y and z to be able to use w later to convert the coordinates back.
I edited the answer, can you check if everything is ok now? (I'd like to use it for the ViewNode MATCH_PARENT)
If we don't expect the Filament camera to be null, can we use !! to simplify the code?
You are totally right
I edited the answer, can you check if everything is ok now?
Yes, it is. I only think that it is not necessary to subtract 1 in the last two methods if we work with Float
numbers.
Camera.clipSpaceToViewSpace
It seems that a correct equation should be
inverse(projectionMatrix) * clipSpacePosition.xyz * clipSpacePosition.w
.Camera.viewSpaceToWorld
And here
inverse(viewMatrix) * viewSpacePosition
.