Added orx-camera and orx-noise
This commit is contained in:
@@ -4,12 +4,14 @@
|
|||||||
|
|
||||||
A growing library of assorted data structures, algorithms and utilities.
|
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-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-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-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-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-kdtree`, a kd-tree implementation for fast nearest point searches
|
||||||
- [`orx-mesh-generators`](orx-mesh-generators/README.md), triangular mesh generators
|
- [`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-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
|
- [`orx-obj-loader`](orx-obj-loader/README.md), simple Wavefront .obj mesh loader
|
||||||
|
|
||||||
|
|||||||
3
orx-camera/README.md
Normal file
3
orx-camera/README.md
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# orx-camera
|
||||||
|
|
||||||
|
3D camera and controls for OPENRNDR. This supersedes the to be deprecated functionality in OPENRNDR.
|
||||||
74
orx-camera/src/main/kotlin/Debug3D.kt
Normal file
74
orx-camera/src/main/kotlin/Debug3D.kt
Normal file
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
120
orx-camera/src/main/kotlin/OrbitalCamera.kt
Normal file
120
orx-camera/src/main/kotlin/OrbitalCamera.kt
Normal file
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
100
orx-camera/src/main/kotlin/OrbitalControls.kt
Normal file
100
orx-camera/src/main/kotlin/OrbitalControls.kt
Normal file
@@ -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) }
|
||||||
|
}
|
||||||
|
}
|
||||||
28
orx-noise/README.md
Normal file
28
orx-noise/README.md
Normal file
@@ -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() }
|
||||||
|
|
||||||
|
```
|
||||||
71
orx-noise/src/main/kotlin/UniformRandom.kt
Normal file
71
orx-noise/src/main/kotlin/UniformRandom.kt
Normal file
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -1,12 +1,14 @@
|
|||||||
rootProject.name = 'orx'
|
rootProject.name = 'orx'
|
||||||
|
|
||||||
include 'orx-compositor',
|
include 'orx-camera',
|
||||||
|
'orx-compositor',
|
||||||
'orx-filter-extension',
|
'orx-filter-extension',
|
||||||
'orx-integral-image',
|
'orx-integral-image',
|
||||||
'orx-jumpflood',
|
'orx-jumpflood',
|
||||||
'orx-kdtree',
|
'orx-kdtree',
|
||||||
'orx-mesh-generators',
|
'orx-mesh-generators',
|
||||||
'orx-no-clear',
|
'orx-no-clear',
|
||||||
|
'orx-noise',
|
||||||
'orx-obj-loader'
|
'orx-obj-loader'
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user