diff --git a/orx-camera/src/main/kotlin/Debug3D.kt b/orx-camera/src/main/kotlin/Debug3D.kt index 0513a7ad..2d5eae04 100644 --- a/orx-camera/src/main/kotlin/Debug3D.kt +++ b/orx-camera/src/main/kotlin/Debug3D.kt @@ -1,51 +1,56 @@ -package org.openrndr.extras.camera - -import org.openrndr.Extension -import org.openrndr.color.ColorRGBa -import org.openrndr.draw.* -import org.openrndr.math.Vector3 - -@Suppress("unused") -class Debug3D(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 - } - } - } - - 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)) - } - } +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 Debug3D(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)) + } + } } \ No newline at end of file diff --git a/orx-camera/src/main/kotlin/Orbital.kt b/orx-camera/src/main/kotlin/Orbital.kt new file mode 100644 index 00000000..4faf0a48 --- /dev/null +++ b/orx-camera/src/main/kotlin/Orbital.kt @@ -0,0 +1,36 @@ +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 + + val camera by lazy { OrbitalCamera(eye, lookAt, fov, near, far) } + 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) + } +} \ No newline at end of file diff --git a/orx-camera/src/main/kotlin/OrbitalCamera.kt b/orx-camera/src/main/kotlin/OrbitalCamera.kt index 1ce77ff2..fbfff4e6 100644 --- a/orx-camera/src/main/kotlin/OrbitalCamera.kt +++ b/orx-camera/src/main/kotlin/OrbitalCamera.kt @@ -1,154 +1,168 @@ -package org.openrndr.extras.camera - -import org.openrndr.Extension -import org.openrndr.Program -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 org.openrndr.math.transforms.lookAt as lookAt_ - -class OrbitalCamera(eye: Vector3, lookAt: Vector3, var fov: Double, var near: Double = 0.1, var far: Double = 1000.0) : Extension { - // current position in spherical coordinates - var spherical = Spherical.fromVector(eye) - private set - var lookAt = lookAt - private set - - private var sphericalEnd = Spherical.fromVector(eye) - private var lookAtEnd = lookAt.copy() - private var dirty: Boolean = true - private var lastSeconds: Double = -1.0 - - var fovEnd = fov - - var dampingFactor = 0.05 - var zoomSpeed = 1.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(0.0, rotX, rotY) - 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 = Math.pow(0.95, zoomSpeed) - dolly(sphericalEnd.radius * zoomScale - sphericalEnd.radius) - } - - fun dollyOut() { - val zoomScale = Math.pow(0.95, zoomSpeed) - dolly(sphericalEnd.radius / zoomScale - sphericalEnd.radius) - } - - fun dolly(distance: Double) { - sphericalEnd += Spherical(distance, 0.0, 0.0) - 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 zoom(degrees: Double) { - fovEnd += degrees - } - - fun update(timeDelta: Double) { - if (!dirty) return - dirty = false - - val dampingFactor = dampingFactor * timeDelta / 0.0060 - val sphericalDelta = sphericalEnd - spherical - val lookAtDelta = lookAtEnd - lookAt - val fovDelta = fovEnd - fov - if ( - abs(sphericalEnd.radius) > EPSILON || - abs(sphericalEnd.theta) > EPSILON || - abs(sphericalEnd.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) - dirty = true - - } else { - spherical = sphericalEnd.copy() - lookAt = lookAtEnd.copy() - } - 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) - - drawer.perspective(fov, program.window.size.x / program.window.size.y, near, far) - drawer.view = viewMatrix() - } - - override fun afterDraw(drawer: Drawer, program: Program) { - drawer.view = Matrix44.IDENTITY - drawer.ortho() - } -} - - +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 org.openrndr.math.transforms.lookAt as lookAt_ + +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) : Extension { + // current position in spherical coordinates + var spherical = Spherical.fromVector(eye) + private set + var lookAt = lookAt + private set + + var depthTest = true + + 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 + + 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(0.0, rotX, rotY) + 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 = Math.pow(0.95, zoomSpeed) + dolly(sphericalEnd.radius * zoomScale - sphericalEnd.radius) + } + + fun dollyOut() { + val zoomScale = Math.pow(0.95, zoomSpeed) + dolly(sphericalEnd.radius / zoomScale - sphericalEnd.radius) + } + + fun dolly(distance: Double) { + sphericalEnd += Spherical(distance, 0.0, 0.0) + 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 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 = dampingFactor * timeDelta / 0.0060 + val sphericalDelta = sphericalEnd - spherical + val lookAtDelta = lookAtEnd - lookAt + val fovDelta = fovEnd - fov + if ( + abs(sphericalEnd.radius) > EPSILON || + abs(sphericalEnd.theta) > EPSILON || + abs(sphericalEnd.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) + dirty = true + + } else { + spherical = sphericalEnd.copy() + lookAt = lookAtEnd.copy() + } + 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) + + drawer.perspective(fov, program.window.size.x / program.window.size.y, near, far) + drawer.view = viewMatrix() + + if (depthTest) { + drawer.drawStyle.depthWrite = true + drawer.drawStyle.depthTestPass = DepthTestPass.LESS_OR_EQUAL + } + } + + override fun afterDraw(drawer: Drawer, program: Program) { + drawer.view = Matrix44.IDENTITY + drawer.ortho() + } +} + + diff --git a/orx-camera/src/main/kotlin/OrbitalControls.kt b/orx-camera/src/main/kotlin/OrbitalControls.kt index c581a281..f845daee 100644 --- a/orx-camera/src/main/kotlin/OrbitalControls.kt +++ b/orx-camera/src/main/kotlin/OrbitalControls.kt @@ -1,134 +1,134 @@ -package org.openrndr.extras.camera - -import org.openrndr.* -import org.openrndr.math.Vector2 -import org.openrndr.math.Vector3 -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 (abs(event.rotation.x) > 0.1) return - - when { - event.rotation.y > 0 -> orbitalCamera.dollyIn() - event.rotation.y < 0 -> orbitalCamera.dollyOut() - } - } - - private fun mouseMoved(event: MouseEvent) { - - 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) * PI / 180) - 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 = 2 * PI * delta.x / program.window.size.x - val rotY = 2 * PI * delta.y / program.window.size.y - orbitalCamera.rotate(rotX, rotY) - } - - } - - private fun mouseButtonDown(event: MouseEvent) { - 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.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) } - } - } -} +package org.openrndr.extras.camera + +import org.openrndr.* +import org.openrndr.math.Vector2 +import org.openrndr.math.Vector3 +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 (abs(event.rotation.x) > 0.1) return + + when { + event.rotation.y > 0 -> orbitalCamera.dollyIn() + event.rotation.y < 0 -> orbitalCamera.dollyOut() + } + } + + private fun mouseMoved(event: MouseEvent) { + + 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) * PI / 180) + 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 = 2 * PI * delta.x / program.window.size.x + val rotY = 2 * PI * delta.y / program.window.size.y + orbitalCamera.rotate(rotX, rotY) + } + + } + + private fun mouseButtonDown(event: MouseEvent) { + 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.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) } + } + } +}