[orx-camera] convert to MPP

This commit is contained in:
Edwin Jakobs
2021-06-29 14:10:53 +02:00
parent bfd7414594
commit 2677b6cb77
7 changed files with 105 additions and 4 deletions

View File

@@ -0,0 +1,78 @@
package org.openrndr.extras.camera
import org.openrndr.Extension
import org.openrndr.Program
import org.openrndr.color.ColorRGBa
import org.openrndr.draw.*
import org.openrndr.math.Matrix33
import org.openrndr.math.Matrix44
import org.openrndr.math.Vector3
@Suppress("unused")
class AxisHelper(var size: Int = 80) : Extension {
override var enabled: Boolean = true
val fbo = renderTarget(size, size) {
colorBuffer()
depthBuffer()
}
val side = size.toDouble()
val axisLength = size / 2.0
val planeLength = axisLength + 10.0
override fun afterDraw(drawer: Drawer, program: Program) {
draw(drawer)
}
fun draw(drawer: Drawer) {
val viewMatrix = drawer.view
val x = drawer.width - (size + 5.0)
val y = 5.0
drawer.isolatedWithTarget(fbo) {
drawer.defaults()
drawer.ortho(fbo)
drawer.clear(ColorRGBa.TRANSPARENT)
drawer.stroke = null
drawer.fill = ColorRGBa.PINK.opacify(0.7)
drawer.circle(axisLength, axisLength, axisLength)
drawer.ortho(-planeLength, planeLength, -planeLength, planeLength, -planeLength, planeLength)
drawer.view = getRotation(viewMatrix)
drawer.strokeWeight = 0.6
drawer.fill = ColorRGBa.RED
drawer.stroke = ColorRGBa.RED
drawer.lineSegment(Vector3.ZERO, Vector3.UNIT_X * axisLength)
drawer.fill = ColorRGBa.GREEN
drawer.stroke = ColorRGBa.GREEN
drawer.lineSegment(Vector3.ZERO, Vector3.UNIT_Y * axisLength)
drawer.fill = ColorRGBa.BLUE
drawer.stroke = ColorRGBa.BLUE
drawer.lineSegment(Vector3.ZERO, Vector3.UNIT_Z * axisLength)
}
drawer.isolated {
drawer.defaults()
drawer.image(fbo.colorBuffer(0), x, y, side, side)
}
}
}
internal fun getRotation(mat: Matrix44): Matrix44 {
val mat3 = mat.matrix33 // without translation
val c0 = mat3[0].length
val c1 = mat3[1].length
val c2 = mat3[2].length
return Matrix33(
mat3.c0r0 / c0, mat3.c1r0 / c1, mat3.c2r0 / c2,
mat3.c0r1 / c0, mat3.c1r1 / c1, mat3.c2r1 / c2,
mat3.c0r2 / c0, mat3.c1r2 / c1, mat3.c2r2 / c2
).matrix44
}

View File

@@ -0,0 +1,56 @@
package org.openrndr.extras.camera
import org.openrndr.Extension
import org.openrndr.Program
import org.openrndr.color.ColorRGBa
import org.openrndr.draw.*
import org.openrndr.math.Vector3
@Suppress("unused")
class GridHelper(val size: Int = 10, val divisions: Int = 10) : Extension {
override var enabled: Boolean = true
private val step = size / divisions.toDouble()
private val grid = vertexBuffer(
vertexFormat {
position(3)
}
, 4 * (size * divisions + 1)).apply {
put {
val halfSize = size / 2.0
var k = -halfSize
for(i in 0 until divisions + 1) {
write(Vector3(-halfSize, 0.0, k))
write(Vector3(halfSize, 0.0, k))
write(Vector3(k, 0.0, -halfSize))
write(Vector3(k, 0.0, halfSize))
k += step
}
}
}
override fun beforeDraw(drawer: Drawer, program: Program) {
draw(drawer)
}
fun draw(drawer: Drawer) {
drawer.isolated {
drawer.fill = ColorRGBa.WHITE
drawer.stroke = ColorRGBa.WHITE
drawer.vertexBuffer(grid, DrawPrimitive.LINES)
// Axis cross
drawer.stroke = ColorRGBa.RED
drawer.lineSegment(Vector3.ZERO, Vector3(step, 0.0, 0.0))
drawer.stroke = ColorRGBa.GREEN
drawer.lineSegment(Vector3.ZERO, Vector3(0.0, step, 0.0))
drawer.stroke = ColorRGBa.BLUE
drawer.lineSegment(Vector3.ZERO, Vector3(0.0, 0.0, step))
}
}
}

View File

@@ -0,0 +1,46 @@
package org.openrndr.extras.camera
import org.openrndr.Extension
import org.openrndr.Program
import org.openrndr.draw.Drawer
import org.openrndr.math.Vector3
/**
* Extension that provides orbital camera view and controls.
*/
class Orbital : Extension {
override var enabled: Boolean = true
var eye = Vector3.UNIT_Z * 10.0
var lookAt = Vector3.ZERO
var near = 0.1
var far = 1000.0
var fov = 90.0
var userInteraction = true
var keySpeed = 1.0
var projectionType = ProjectionType.PERSPECTIVE
/**
* Damping factor for camera motion, set to 0 for no damping
*/
var dampingFactor = 0.05
val camera by lazy {
OrbitalCamera(eye, lookAt, fov, near, far, projectionType).apply {
dampingFactor = this@Orbital.dampingFactor
}
}
val controls by lazy { OrbitalControls(camera, userInteraction, keySpeed) }
override fun setup(program: Program) {
controls.setup(program)
}
override fun beforeDraw(drawer: Drawer, program: Program) {
camera.beforeDraw(drawer, program)
}
override fun afterDraw(drawer: Drawer, program: Program) {
camera.afterDraw(drawer, program)
}
}

View File

@@ -0,0 +1,224 @@
package org.openrndr.extras.camera
import org.openrndr.Extension
import org.openrndr.Program
import org.openrndr.draw.DepthTestPass
import org.openrndr.draw.Drawer
import org.openrndr.math.Matrix44
import org.openrndr.math.Spherical
import org.openrndr.math.Vector3
import kotlin.math.abs
import kotlin.math.pow
import org.openrndr.math.transforms.lookAt as lookAt_
enum class ProjectionType {
PERSPECTIVE,
ORTHOGONAL
}
class OrbitalCamera(eye: Vector3 = Vector3.ZERO, lookAt: Vector3 = Vector3.UNIT_Z, var fov: Double = 90.0, var near: Double = 0.1, var far: Double = 1000.0, var projectionType: ProjectionType = ProjectionType.PERSPECTIVE) : Extension {
// current position in spherical coordinates
var spherical = Spherical.fromVector(eye)
private set
var lookAt = lookAt
private set
var depthTest = true
var magnitude = 100.0
var magnitudeEnd = magnitude
private var sphericalEnd = Spherical.fromVector(eye)
private var lookAtEnd = lookAt
private var dirty: Boolean = true
private var lastSeconds: Double = -1.0
var fovEnd = fov
var dampingFactor = 0.05
var zoomSpeed = 1.0
var orthoNear = -1000.0
var orthoFar = 1000.0
fun setView(lookAt: Vector3, spherical: Spherical, fov: Double) {
this.lookAt = lookAt
this.lookAtEnd = lookAt
this.spherical = spherical
this.sphericalEnd = spherical
this.fov = fov
this.fovEnd = fov
}
fun rotate(rotX: Double, rotY: Double) {
sphericalEnd += Spherical(rotX, rotY, 0.0)
sphericalEnd = sphericalEnd.makeSafe()
dirty = true
}
fun rotateTo(rotX: Double, rotY: Double) {
sphericalEnd = sphericalEnd.copy(theta = rotX, phi = rotY)
sphericalEnd = sphericalEnd.makeSafe()
dirty = true
}
fun rotateTo(eye: Vector3) {
sphericalEnd = Spherical.fromVector(eye)
sphericalEnd = sphericalEnd.makeSafe()
dirty = true
}
fun dollyIn() {
val zoomScale = pow(0.95, zoomSpeed)
dolly(sphericalEnd.radius * zoomScale - sphericalEnd.radius)
}
fun dollyOut() {
val zoomScale = pow(0.95, zoomSpeed)
dolly(sphericalEnd.radius / zoomScale - sphericalEnd.radius)
}
fun dolly(distance: Double) {
sphericalEnd += Spherical(0.0, 0.0, distance)
dirty = true
}
fun pan(x: Double, y: Double, z: Double) {
val view = viewMatrix()
val xColumn = Vector3(view.c0r0, view.c1r0, view.c2r0) * x
val yColumn = Vector3(view.c0r1, view.c1r1, view.c2r1) * y
val zColumn = Vector3(view.c0r2, view.c1r2, view.c2r2) * z
lookAtEnd += xColumn + yColumn + zColumn
dirty = true
}
fun panTo(target: Vector3) {
lookAtEnd = target
dirty = true
}
fun dollyTo(distance: Double) {
sphericalEnd = sphericalEnd.copy(radius = distance)
dirty = true
}
fun scale(s: Double) {
magnitudeEnd += s
dirty = true
}
fun scaleTo(s: Double) {
magnitudeEnd = s
dirty = true
}
fun zoom(degrees: Double) {
fovEnd += degrees
dirty = true
}
fun zoomTo(degrees: Double) {
fovEnd = degrees
dirty = true
}
fun update(timeDelta: Double) {
if (!dirty) return
dirty = false
val dampingFactor = if (dampingFactor > 0.0) { dampingFactor * timeDelta / 0.0060 } else 1.0
val sphericalDelta = sphericalEnd - spherical
val lookAtDelta = lookAtEnd - lookAt
val fovDelta = fovEnd - fov
val magnitudeDelta = magnitudeEnd - magnitude
if (
abs(sphericalDelta.radius) > EPSILON ||
abs(sphericalDelta.theta) > EPSILON ||
abs(sphericalDelta.phi) > EPSILON ||
abs(lookAtDelta.x) > EPSILON ||
abs(lookAtDelta.y) > EPSILON ||
abs(lookAtDelta.z) > EPSILON ||
abs(fovDelta) > EPSILON
) {
fov += (fovDelta * dampingFactor)
spherical += (sphericalDelta * dampingFactor)
spherical = spherical.makeSafe()
lookAt += (lookAtDelta * dampingFactor)
magnitude += (magnitudeDelta * dampingFactor)
dirty = true
} else {
magnitude = magnitudeEnd
spherical = sphericalEnd.copy()
lookAt = lookAtEnd.copy()
fov = fovEnd
}
spherical = spherical.makeSafe()
}
fun viewMatrix(): Matrix44 {
return lookAt_(Vector3.fromSpherical(spherical) + lookAt, lookAt, Vector3.UNIT_Y)
}
companion object {
private const val EPSILON = 0.000001
}
// EXTENSION
override var enabled: Boolean = true
override fun beforeDraw(drawer: Drawer, program: Program) {
if (lastSeconds == -1.0) lastSeconds = program.seconds
val delta = program.seconds - lastSeconds
lastSeconds = program.seconds
update(delta)
applyTo(drawer)
}
override fun afterDraw(drawer: Drawer, program: Program) {
drawer.view = Matrix44.IDENTITY
drawer.ortho()
}
}
/**
* Temporarily enables this camera, calls function to draw using
* that camera, then disables it by popping the last matrix changes.
* It makes it easy to combine perspective and orthographic projections
* in the same program.
* @param function the function that is called in the isolation
*/
fun OrbitalCamera.isolated(drawer: Drawer, function: Drawer.() -> Unit) {
drawer.pushTransforms()
drawer.pushStyle()
applyTo(drawer)
function(drawer)
drawer.popStyle()
drawer.popTransforms()
}
/**
* Enables the perspective camera. Use this faster method instead of .isolated()
* if you don't need to revert back to the orthographic projection.
*/
fun OrbitalCamera.applyTo(drawer: Drawer) {
if (projectionType == ProjectionType.PERSPECTIVE) {
drawer.perspective(fov, drawer.width.toDouble() / drawer.height, near, far)
} else {
val ar = drawer.width * 1.0 / drawer.height
drawer.ortho(-ar * magnitude, ar * magnitude, -1.0 * magnitude, 1.0 * magnitude, orthoNear, orthoFar)
}
drawer.view = viewMatrix()
if (depthTest) {
drawer.drawStyle.depthWrite = true
drawer.drawStyle.depthTestPass = DepthTestPass.LESS_OR_EQUAL
}
}
private fun pow(a:Double, x:Double): Double = a.pow(x)

View File

@@ -0,0 +1,150 @@
package org.openrndr.extras.camera
import org.openrndr.*
import org.openrndr.math.Vector2
import org.openrndr.math.Vector3
import org.openrndr.math.asRadians
import kotlin.math.PI
import kotlin.math.abs
import kotlin.math.tan
class OrbitalControls(val orbitalCamera: OrbitalCamera, val userInteraction: Boolean = true, val keySpeed: Double = 1.0) : Extension {
enum class STATE {
NONE,
ROTATE,
PAN,
}
private var state = STATE.NONE
var fov = orbitalCamera.fov
private lateinit var program: Program
private lateinit var lastMousePosition: Vector2
private fun mouseScrolled(event: MouseEvent) {
if (!event.propagationCancelled) {
if (orbitalCamera.projectionType == ProjectionType.PERSPECTIVE) {
if (abs(event.rotation.x) > 0.1) return
when {
event.rotation.y > 0 -> orbitalCamera.dollyIn()
event.rotation.y < 0 -> orbitalCamera.dollyOut()
}
} else {
if (abs(event.rotation.x) > 0.1) return
when {
event.rotation.y > 0 -> orbitalCamera.scale(1.0)
event.rotation.y < 0 -> orbitalCamera.scale(-1.0)
}
}
}
}
private fun mouseMoved(event: MouseEvent) {
if (!event.propagationCancelled) {
if (state == STATE.NONE) return
val delta = lastMousePosition - event.position
lastMousePosition = event.position
if (state == STATE.PAN) {
val offset = Vector3.fromSpherical(orbitalCamera.spherical) - orbitalCamera.lookAt
// half of the fov is center to top of screen
val targetDistance = offset.length * tan(fov / 2).asRadians
val panX = (2 * delta.x * targetDistance / program.window.size.x)
val panY = (2 * delta.y * targetDistance / program.window.size.y)
orbitalCamera.pan(panX, -panY, 0.0)
} else {
val rotX = 360.0 * delta.x / program.window.size.x
val rotY = 360.0 * delta.y / program.window.size.y
orbitalCamera.rotate(rotX, rotY)
}
}
}
private fun mouseButtonDown(event: MouseEvent) {
if (!event.propagationCancelled) {
val previousState = state
when (event.button) {
MouseButton.LEFT -> {
state = STATE.ROTATE
}
MouseButton.RIGHT -> {
state = STATE.PAN
}
MouseButton.CENTER -> {
}
MouseButton.NONE -> {
}
}
if (previousState == STATE.NONE) {
lastMousePosition = event.position
}
}
}
fun keyPressed(keyEvent: KeyEvent) {
if (!keyEvent.propagationCancelled) {
if (keyEvent.key == KEY_ARROW_RIGHT) {
orbitalCamera.pan(keySpeed, 0.0, 0.0)
}
if (keyEvent.key == KEY_ARROW_LEFT) {
orbitalCamera.pan(-keySpeed, 0.0, 0.0)
}
if (keyEvent.key == KEY_ARROW_UP) {
orbitalCamera.pan(0.0, keySpeed, 0.0)
}
if (keyEvent.key == KEY_ARROW_DOWN) {
orbitalCamera.pan(0.0, -keySpeed, 0.0)
}
if (keyEvent.name == "q") {
orbitalCamera.pan(0.0, -keySpeed, 0.0)
}
if (keyEvent.name == "e") {
orbitalCamera.pan(0.0, keySpeed, 0.0)
}
if (keyEvent.name == "w") {
orbitalCamera.pan(0.0, 0.0, -keySpeed)
}
if (keyEvent.name == "s") {
orbitalCamera.pan(0.0, 0.0, keySpeed)
}
if (keyEvent.name == "a") {
orbitalCamera.pan(-keySpeed, 0.0, 0.0)
}
if (keyEvent.name == "d") {
orbitalCamera.pan(keySpeed, 0.0, 0.0)
}
if (keyEvent.key == KEY_PAGE_UP) {
orbitalCamera.zoom(keySpeed)
}
if (keyEvent.key == KEY_PAGE_DOWN) {
orbitalCamera.zoom(-keySpeed)
}
}
}
// EXTENSION
override var enabled: Boolean = true
override fun setup(program: Program) {
this.program = program
if (userInteraction) {
program.mouse.moved.listen { mouseMoved(it) }
program.mouse.buttonDown.listen { mouseButtonDown(it) }
program.mouse.buttonUp.listen { state = STATE.NONE }
program.mouse.scrolled.listen { mouseScrolled(it) }
program.keyboard.keyDown.listen { keyPressed(it) }
program.keyboard.keyRepeat.listen { keyPressed(it) }
}
}
}