Skip to content

Instantly share code, notes, and snippets.

@ThomasGorisse
Created September 11, 2022 13:47
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ThomasGorisse/f4f7614d4b6f672c25718bc70346f706 to your computer and use it in GitHub Desktop.
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
}
}
@grassydragon
Copy link

Camera.clipSpaceToViewSpace
It seems that a correct equation should be inverse(projectionMatrix) * clipSpacePosition.xyz * clipSpacePosition.w.
Camera.viewSpaceToWorld
And here inverse(viewMatrix) * viewSpacePosition.

@ThomasGorisse
Copy link
Author

ThomasGorisse commented Sep 12, 2022

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:

@ThomasGorisse
Copy link
Author

So viewSpaceToClipSpace should also be divided by w?

fun Camera.viewSpaceToClipSpace(viewSpacePosition: Position) =
    projectionMatrix * viewSpacePosition.xyz / viewSpacePosition.w

@ThomasGorisse
Copy link
Author

ThomasGorisse commented Sep 12, 2022

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
}

@grassydragon
Copy link

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?

@ThomasGorisse
Copy link
Author

ThomasGorisse commented Sep 12, 2022

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

@grassydragon
Copy link

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment