diff --git a/README.md b/README.md index 6b4d71c7..36658a17 100644 --- a/README.md +++ b/README.md @@ -4,12 +4,14 @@ A growing library of assorted data structures, algorithms and utilities. +- [`orx-camera`](orx-camera/README.md), 3d camera and controls - [`orx-compositor`](orx-compositor/README.md), a simple toolkit to make composite (layered) images - [`orx-filter-extension`](orx-filter-extension/README.md), Program extension method that provides Filter based `extend()` - [`orx-integral-image`](orx-integral-image/README.md), a CPU-based implementation for integral images (summed area tables) - `orx-jumpflood`, a filter/shader based implementation of the jump flood algorithm for finding fast approximate (directional) distance fields - `orx-kdtree`, a kd-tree implementation for fast nearest point searches - [`orx-mesh-generators`](orx-mesh-generators/README.md), triangular mesh generators +- [`orx-noise`](orx-noise/README.md), library for random number generation and noise - [`orx-no-clear`](orx-no-clear/README.md), a simple extension that provides drawing without clearing the background - [`orx-obj-loader`](orx-obj-loader/README.md), simple Wavefront .obj mesh loader diff --git a/orx-camera/README.md b/orx-camera/README.md new file mode 100644 index 00000000..17b752cc --- /dev/null +++ b/orx-camera/README.md @@ -0,0 +1,3 @@ +# orx-camera + +3D camera and controls for OPENRNDR. This supersedes the to be deprecated functionality in OPENRNDR. \ No newline at end of file diff --git a/orx-camera/src/main/kotlin/Debug3D.kt b/orx-camera/src/main/kotlin/Debug3D.kt new file mode 100644 index 00000000..d2978695 --- /dev/null +++ b/orx-camera/src/main/kotlin/Debug3D.kt @@ -0,0 +1,74 @@ +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.Matrix44 +import org.openrndr.math.Vector3 + +@Suppress("unused") +class Debug3D(eye: Vector3 = Vector3(0.0, 0.0, 10.0), lookAt: Vector3 = Vector3.ZERO, private val fov: Double = 90.0) : Extension { + + override var enabled: Boolean = true + var showGrid = false + val orbitalCamera = OrbitalCamera(eye, lookAt) + private val orbitalControls = OrbitalControls(orbitalCamera) + private var lastSeconds: Double = -1.0 + + private val grid = vertexBuffer( + vertexFormat { + position(3) + } + , 4 * 21).apply { + put { + for (x in -10..10) { + write(Vector3(x.toDouble(), 0.0, -10.0)) + write(Vector3(x.toDouble(), 0.0, 10.0)) + write(Vector3(-10.0, 0.0, x.toDouble())) + write(Vector3(10.0, 0.0, x.toDouble())) + } + } + } + + override fun beforeDraw(drawer: Drawer, program: Program) { + if (lastSeconds == -1.0) lastSeconds = program.seconds + + val delta = program.seconds - lastSeconds + lastSeconds = program.seconds + orbitalCamera.update(delta) + + drawer.background(ColorRGBa.BLACK) + drawer.perspective(fov, program.window.size.x / program.window.size.y, 0.1, 1000.0) + drawer.view = orbitalCamera.viewMatrix() + + if (showGrid) { + drawer.isolated { + drawer.fill = ColorRGBa.WHITE + drawer.stroke = ColorRGBa.WHITE + drawer.vertexBuffer(grid, DrawPrimitive.LINES) + + // Axis cross + drawer.fill = ColorRGBa.RED + drawer.lineSegment(Vector3.ZERO, Vector3.UNIT_X) + + drawer.fill = ColorRGBa.GREEN + drawer.lineSegment(Vector3.ZERO, Vector3.UNIT_Y) + + drawer.fill = ColorRGBa.BLUE + drawer.lineSegment(Vector3.ZERO, Vector3.UNIT_Z) + } + } + } + + override fun afterDraw(drawer: Drawer, program: Program) { + drawer.isolated { + drawer.view = Matrix44.IDENTITY + drawer.ortho() + } + } + + override fun setup(program: Program) { + orbitalControls.setup(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 new file mode 100644 index 00000000..15e210ad --- /dev/null +++ b/orx-camera/src/main/kotlin/OrbitalCamera.kt @@ -0,0 +1,120 @@ +package org.openrndr.extras.camera + +import org.openrndr.* +import org.openrndr.color.ColorRGBa +import org.openrndr.draw.DrawPrimitive +import org.openrndr.draw.Drawer +import org.openrndr.draw.isolated +import org.openrndr.draw.vertexBuffer +import org.openrndr.draw.vertexFormat +import org.openrndr.math.Matrix44 +import org.openrndr.math.Spherical +import org.openrndr.math.Vector2 +import org.openrndr.math.Vector3 +import org.openrndr.math.transforms.lookAt as lookAt_ + +class OrbitalCamera(eye: Vector3, lookAt: Vector3) { + + // 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 + + var dampingFactor = 0.05 + var zoomSpeed = 1.0 + + 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) + } + + private 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 update(timeDelta: Double) { + if (!dirty) return + dirty = false + + val dampingFactor = dampingFactor * timeDelta / 0.0060 + val sphericalDelta = sphericalEnd - spherical + val lookAtDelta = lookAtEnd - lookAt + + if ( + Math.abs(sphericalEnd.radius) > EPSILON || + Math.abs(sphericalEnd.theta) > EPSILON || + Math.abs(sphericalEnd.phi) > EPSILON || + Math.abs(lookAtDelta.x) > EPSILON || + Math.abs(lookAtDelta.y) > EPSILON || + Math.abs(lookAtDelta.z) > EPSILON + ) { + + spherical += (sphericalDelta * dampingFactor) + 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 + } +} + + diff --git a/orx-camera/src/main/kotlin/OrbitalControls.kt b/orx-camera/src/main/kotlin/OrbitalControls.kt new file mode 100644 index 00000000..10ef3c79 --- /dev/null +++ b/orx-camera/src/main/kotlin/OrbitalControls.kt @@ -0,0 +1,100 @@ +package org.openrndr.extras.camera + +import org.openrndr.* +import org.openrndr.math.Vector2 +import org.openrndr.math.Vector3 + +class OrbitalControls(val orbitalCamera: OrbitalCamera) { + enum class STATE { + NONE, + ROTATE, + PAN, + } + + private var state = STATE.NONE + var fov = 90.0 + + private lateinit var program: Program + private lateinit var lastMousePosition: Vector2 + + private fun mouseScrolled(event: MouseEvent) { + + if (Math.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 * Math.tan((fov / 2) * Math.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 * Math.PI * delta.x / program.window.size.x + val rotY = 2 * Math.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(1.0, 0.0, 0.0) + } + if (keyEvent.key == KEY_ARROW_LEFT) { + orbitalCamera.pan(-1.0, 0.0, 0.0) + } + if (keyEvent.key == KEY_ARROW_UP) { + orbitalCamera.pan(0.0, 1.0, 0.0) + } + if (keyEvent.key == KEY_ARROW_DOWN) { + orbitalCamera.pan(0.0, -1.0, 0.0) + } + } + + fun setup(program: Program) { + this.program = program + 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) } + } +} diff --git a/orx-noise/README.md b/orx-noise/README.md new file mode 100644 index 00000000..70d2b8c8 --- /dev/null +++ b/orx-noise/README.md @@ -0,0 +1,28 @@ +# orx-noise + +A collection of noisy functions + +## Uniform random numbers + +```kotlin +val sua = Double.uniform() +val sub = Double.uniform(-1.0, 1.0) + +val v2ua = Vector2.uniform() +val v2ub = Vector2.uniform(-1.0, 1.0) +val v2uc = Vector2.uniform(Vector2(0.0, 0.0), Vector2(1.0, 1.0)) +val v2ur = Vector2.uniformRing(0.5, 1.0) + +val v3ua = Vector3.uniform() +val v3ub = Vector3.uniform(-1.0, 1.0) +val v3uc = Vector3.uniform(Vector3(0.0, 0.0, 0.0), Vector3(1.0, 1.0, 1.0)) +val v3ur = Vector3.uniformRing(0.5, 1.0) + +val v4ua = Vector4.uniform() +val v4ub = Vector4.uniform(-1.0, 1.0) +val v4uc = Vector4.uniform(Vector4(0.0, 0.0, 0.0, 0.0), Vector4(1.0, 1.0, 1.0, 1.0)) +val v4ur = Vector4.uniformRing(0.5, 1.0) + +val ringSamples = List(500) { Vector2.uniformRing() } + +``` \ No newline at end of file diff --git a/orx-noise/src/main/kotlin/UniformRandom.kt b/orx-noise/src/main/kotlin/UniformRandom.kt new file mode 100644 index 00000000..05e9f0b7 --- /dev/null +++ b/orx-noise/src/main/kotlin/UniformRandom.kt @@ -0,0 +1,71 @@ +package org.openrndr.extra.noise + +import org.openrndr.math.Vector2 +import org.openrndr.math.Vector3 +import org.openrndr.math.Vector4 + + +fun Double.Companion.uniform(min: Double = -1.0, max: Double = 1.0): Double { + return (Math.random() * (max - min)) + min +} + +fun Vector2.Companion.uniform(min: Vector2 = -ONE, max: Vector2 = ONE): Vector2 { + return Vector2(Double.uniform(min.x, max.x), Double.uniform(min.y, max.y)) +} + +fun Vector2.Companion.uniform(min: Double = -1.0, max: Double = 1.0) = + Vector2.uniform(Vector2(min, min), Vector2(max, max)) + +fun Vector2.Companion.uniformRing(innerRadius: Double = 0.0, outerRadius: Double = 1.0): Vector2 { + while (true) { + uniform(-outerRadius, outerRadius).let { + val squaredLength = it.squaredLength + if (squaredLength >= innerRadius * innerRadius && squaredLength < outerRadius * outerRadius) { + return it + } + } + } +} + +fun Vector3.Companion.uniform(min: Double = -1.0, max: Double = 1.0): Vector3 = + Vector3.uniform(Vector3(min, min, min), Vector3(max, max, max)) + +fun Vector3.Companion.uniform(min: Vector3 = -ONE, max: Vector3 = ONE): Vector3 { + return Vector3(Double.uniform(min.x, max.x), Double.uniform(min.y, max.y), Double.uniform(min.z, max.z)) +} + +// squared length 'polyfill' for OPENRNDR 0.3.30 +private val Vector3.squaredLength__: Double get() = x * x + y * y + z * z + +fun Vector3.Companion.uniformRing(innerRadius: Double = 0.0, outerRadius: Double = 1.0): Vector3 { + while (true) { + uniform(-outerRadius, outerRadius).let { + val squaredLength = it.squaredLength__ + if (squaredLength >= innerRadius * innerRadius && squaredLength < outerRadius * outerRadius) { + return it + } + } + } +} + +// squared length 'polyfill' for OPENRNDR 0.3.30 +private val Vector4.squaredLength__: Double get() = x * x + y * y + z * z + w * w + +fun Vector4.Companion.uniform(min: Double = -1.0, max: Double = 1.0): Vector4 = + Vector4.uniform(Vector4(min, min, min, min), Vector4(max, max,max, max)) + +fun Vector4.Companion.uniform(min: Vector4 = -ONE, max: Vector4 = ONE): Vector4 { + return Vector4(Double.uniform(min.x, max.x), Double.uniform(min.y, max.y), Double.uniform(min.z, max.z), Double.uniform(min.w, max.w)) +} + +fun Vector4.Companion.uniformRing(innerRadius: Double = 0.0, outerRadius: Double = 1.0): Vector4 { + while (true) { + uniform(-outerRadius, outerRadius).let { + val squaredLength = it.squaredLength__ + if (squaredLength >= innerRadius * innerRadius && squaredLength < outerRadius * outerRadius) { + return it + } + } + } +} + diff --git a/settings.gradle b/settings.gradle index d082eaf0..4762ec10 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,12 +1,14 @@ rootProject.name = 'orx' -include 'orx-compositor', +include 'orx-camera', + 'orx-compositor', 'orx-filter-extension', 'orx-integral-image', 'orx-jumpflood', 'orx-kdtree', 'orx-mesh-generators', 'orx-no-clear', + 'orx-noise', 'orx-obj-loader'