Camera.kt
src/jvmMain/kotlin/afield/graphics/vulkan/util/Camera.kt · 390 lines · cited by Keyframes: capturing, persisting and replaying live state
package afield.graphics.vulkan.util
The Vulkan renderer's look-at camera: a position, a target and an up vector, with a SuperProjection for its projection.
Camera supplies the model-view matrix as a uniform and moves in world or camera-local terms: dolly, pan, yaw, pitch, roll, and rotation around either the camera position or the target. OrbitControls drives it from the mouse, and CameraPose.kt converts it to and from a rigid pose.
import afield.graphics.vulkan.Renderer
import afield.graphics.vulkan.buffers.HasUniform
import afield.document.context.fromContext
import afield.math.linalg.d
import afield.math.linalg.f
import afield.math.linalg.set
import afield.math.linalg.vec
import afield.math.linalg.x
import afield.math.linalg.Matrix4f
import afield.math.linalg.Quaternionf
import afield.math.linalg.Vector3f
import kotlin.math.abs
class Camera(val renderer: Renderer = fromContext())
class Camera(val renderer: Renderer = fromContext()) {
var at = vec(0, 0, -10)
Where the camera is, in world space. Every movement helper writes through this vector rather than replacing it, so a reference taken once keeps following the camera.
var up = vec(0, 1, 0)
Which way is up, in world space. It need not be perpendicular to the view direction: forward and right cope with an oblique one, and reorthogonalizeUp squares it up again — which the rotation helpers do by default, and roll does not.
var target = vec(0, 0, 0)
The point the camera looks at. distanceToTarget measures to it, rotateAroundTarget orbits about it, and the default projection takes its focus distance from it.
val forward: Vector3f
Forward direction (normalized) from camera position toward target. Falls back to -Z if at == target.
val forward: Vector3f
get() {
val dir = Vector3f(target).sub(at)
if (dir.lengthSquared() < 1e-12f) {
return Vector3f(0f, 0f, -1f)
}
return dir.normalize()
}
val right: Vector3f
Right direction (normalized), derived from forward x up. If up is degenerate, this will be recomputed in a stable way.
val right: Vector3f
get() {
val f = forward
val u = Vector3f(up).normalize()
val r = f.cross(u, Vector3f())
if (r.lengthSquared() < 1e-12f) {
// Fallback to some orthogonal vector
return if (abs(f.y) > 0.9f) {
Vector3f(1f, 0f, 0f)
} else {
Vector3f(f.z, 0f, -f.x).normalize()
}
}
return r.normalize()
}
var p = SuperProjection(renderer = renderer, autoMatchDistance = { distanceToTarget().d})
The projection, built against the renderer's window and told to read its
SuperProjection.matchDepth from distanceToTarget. That is installed as
SuperProjection.auto, which also re-sizes the projection to the window on every evaluation,
so a one-off setAspect or p.resize(w, h) is overwritten on the next frame: to render into
an offscreen target whose aspect differs from the window's, replace SuperProjection.auto
rather than calling resize once.
The focus distance is why an orthographic blend holds still on whatever the camera is looking at while SuperProjection.orthoness is turned.
fun setAspect(aspect: Float)
Manually set projection aspect ratio (width/height).
fun setAspect(aspect: Float) {
p.setAspect(aspect)
}
var m
Model-view matrix uniform wrapper.
var m = object : HasUniform<Matrix4f> {
override fun evaluateUniform() = modelView()
}
fun modelView(): Matrix4f = Matrix4f().lookAtField(at, target, up)
Compute model-view matrix from at/target/up.
High-level programmatic control
fun distanceToTarget(): Float = at.distance(target)
Distance from camera to target.
fun lookAt
Set camera pose using a classical look-at description.
fun lookAt(
eye: Vector3f,
center: Vector3f,
up: Vector3f = this.up,
reorthogonalizeUp: Boolean = true
) {
this.at.set(eye)
this.target.set(center)
this.up.set(up)
if (reorthogonalizeUp) {
reorthogonalizeUp()
}
}
fun lookAlong
Look from position in the given direction.
direction does not need to be normalized.
fun lookAlong(
position: Vector3f,
direction: Vector3f,
upHint: Vector3f = this.up,
reorthogonalizeUp: Boolean = true
) {
val dir = Vector3f(direction)
if (dir.lengthSquared() < 1e-12f) return
dir.normalize()
at.set(position)
target.set(Vector3f(position).add(dir))
up.set(upHint)
if (reorthogonalizeUp) {
reorthogonalizeUp()
}
}
Translation
fun moveWorld(delta: Vector3f, moveTarget: Boolean = true)
Translate in world space.
If moveTarget is true, both at and target move by delta (pan/truck).
Otherwise only the camera position moves (dolly keeping the same target).
fun moveWorld(delta: Vector3f, moveTarget: Boolean = true) {
at.add(delta)
if (moveTarget) {
target.add(delta)
}
}
fun moveWorld(dx: Double, dy: Double, dz: Double, moveTarget: Boolean = true)
fun moveWorld(dx: Double, dy: Double, dz: Double, moveTarget: Boolean = true) {
moveWorld(Vector3f(dx.toFloat(), dy.toFloat(), dz.toFloat()), moveTarget)
}
fun moveLocal
Move in camera-local space.
If moveTarget is true, both at/target are translated (pan).
Otherwise only the camera position moves (dolly).
fun moveLocal(
forward: Double = 0.0,
right: Double = 0.0,
upAmount: Double = 0.0,
moveTarget: Boolean = true
) {
if (forward == 0.0 && right == 0.0 && upAmount == 0.0) return
val f = this.forward
val r = this.right
val u = Vector3f(up).normalize()
val delta = Vector3f()
if (forward != 0.0) {
delta.fma(forward.toFloat(), f)
}
if (right != 0.0) {
delta.fma(right.toFloat(), r)
}
if (upAmount != 0.0) {
delta.fma(upAmount.toFloat(), u)
}
moveWorld(delta, moveTarget)
}
fun dolly(amount: Double, moveTarget: Boolean = false)
Dolly along the view direction.
amount> 0 moves forward.- If
moveTargetis true, both at/target move (like a push in). Otherwise only the camera moves, changing distance to target.
fun dolly(amount: Double, moveTarget: Boolean = false) {
moveLocal(forward = amount, moveTarget = moveTarget)
}
fun pan(right: Double, upAmount: Double, moveTarget: Boolean = true)
Pan in screen space: right/left and up/down in camera-local space.
right > 0 moves camera to the right, upAmount > 0 moves upward.
fun pan(right: Double, upAmount: Double, moveTarget: Boolean = true) {
moveLocal(forward = 0.0, right = right, upAmount = upAmount, moveTarget = moveTarget)
}
Rotation helpers
fun rotateAroundAt
General rotation of view around the camera position (at).
Rotates the vector (target - at) around axis by angle radians.
If rotateUp is true, rotates up as well.
fun rotateAroundAt(
angle: Double,
axis: Vector3f,
rotateUp: Boolean = true,
reorthogonalizeUp: Boolean = true
) {
val ax = Vector3f(axis)
if (ax.lengthSquared() < 1e-12f || angle == 0.0) return
ax.normalize()
val q = Quaternionf().fromAxisAngleRad(ax, angle.toFloat())
val dir = Vector3f(target).sub(at)
q.transform(dir)
target.set(at).add(dir)
if (rotateUp) {
q.transform(up)
}
if (reorthogonalizeUp) {
reorthogonalizeUp()
}
}
fun rotateAroundTarget
General orbit of the camera position around the target.
Rotates the vector (at - target) around axis by angle radians.
If rotateUp is true, rotates up as well.
fun rotateAroundTarget(
angle: Double,
axis: Vector3f,
rotateUp: Boolean = true,
reorthogonalizeUp: Boolean = true
) {
val ax = Vector3f(axis)
if (ax.lengthSquared() < 1e-12f || angle == 0.0) return
ax.normalize()
val q = Quaternionf().fromAxisAngleRad(ax, angle.toFloat())
val dir = Vector3f(at).sub(target)
q.transform(dir)
at.set(target).add(dir)
if (rotateUp) {
q.transform(up)
}
if (reorthogonalizeUp) {
reorthogonalizeUp()
}
}
fun yaw
Yaw around camera up axis.
anglein radians, positive is left-handed around +up.- If
aroundTargetis true, orbit camera around target. Otherwise rotate view direction around camera position.
fun yaw(
angle: Double,
aroundTarget: Boolean = false,
rotateUp: Boolean = true,
reorthogonalizeUp: Boolean = true
) {
val axis = Vector3f(up)
if (axis.lengthSquared() < 1e-12f) {
axis.set(0f, 1f, 0f)
}
if (aroundTarget) {
rotateAroundTarget(angle, axis, rotateUp, reorthogonalizeUp)
} else {
rotateAroundAt(angle, axis, rotateUp, reorthogonalizeUp)
}
}
fun pitch
Pitch around camera right axis.
anglein radians; positive pitches up.- If
aroundTargetis true, orbit camera around target. Otherwise rotate view direction around camera position.
fun pitch(
angle: Double,
aroundTarget: Boolean = false,
rotateUp: Boolean = true,
reorthogonalizeUp: Boolean = true
) {
val f = Vector3f(target).sub(at)
if (f.lengthSquared() < 1e-12f) return
f.normalize()
val u = Vector3f(up).normalize()
val axis = f.cross(u, Vector3f()).normalize()
if (axis.lengthSquared() < 1e-12f) return
if (aroundTarget) {
rotateAroundTarget(angle, axis, rotateUp, reorthogonalizeUp)
} else {
rotateAroundAt(angle, axis, rotateUp, reorthogonalizeUp)
}
}
fun roll
Roll around the view direction, rotating up but leaving at and target fixed.
fun roll(
angle: Double,
reorthogonalizeUp: Boolean = false
) {
val f = Vector3f(target).sub(at)
if (f.lengthSquared() < 1e-12f) return
f.normalize()
val q = Quaternionf().fromAxisAngleRad(f, angle.toFloat())
q.transform(up)
if (reorthogonalizeUp) {
reorthogonalizeUp()
}
}
fun reorthogonalizeUp()
Make up orthogonal to the current forward direction, and ensure a stable, non-degenerate up vector.
fun reorthogonalizeUp() {
val f = Vector3f(target).sub(at)
if (f.lengthSquared() < 1e-12f) {
return
}
f.normalize()
val upVec = Vector3f(up)
val proj = upVec.dot(f)
upVec.fma(-proj, f) // up -= proj * f
if (upVec.lengthSquared() < 1e-6f) {
if (abs(f.y) > 0.9f) {
upVec.set(1f, 0f, 0f)
} else {
upVec.set(0f, 1f, 0f)
val p2 = upVec.dot(f)
upVec.fma(-p2, f)
}
}
upVec.normalize()
up.set(upVec)
}
}