[orx-obj-loader] Add support for wireframes and non-planar and concave faces
This commit is contained in:
144
orx-obj-loader/src/main/kotlin/IndexedPolygon.kt
Normal file
144
orx-obj-loader/src/main/kotlin/IndexedPolygon.kt
Normal file
@@ -0,0 +1,144 @@
|
||||
package org.openrndr.extra.objloader
|
||||
|
||||
import org.openrndr.math.Matrix44
|
||||
import org.openrndr.math.Vector2
|
||||
import org.openrndr.math.Vector3
|
||||
import org.openrndr.math.Vector4
|
||||
import kotlin.math.PI
|
||||
import kotlin.math.abs
|
||||
import kotlin.math.atan2
|
||||
import kotlin.math.round
|
||||
|
||||
data class IndexedPolygon(
|
||||
val positions: IntArray, val textureCoords: IntArray, val normals: IntArray
|
||||
) {
|
||||
|
||||
fun base(vertexData: VertexData): Matrix44 {
|
||||
val u = (vertexData.positions[positions[1]] - vertexData.positions[positions[0]])
|
||||
val v = (vertexData.positions[positions[positions.size - 1]] - vertexData.positions[positions[0]])
|
||||
val normal = u.cross(v)
|
||||
val bitangent = normal.cross(u)
|
||||
return Matrix44.fromColumnVectors(
|
||||
u.xyz0.normalized,
|
||||
bitangent.xyz0.normalized,
|
||||
normal.xyz0.normalized,
|
||||
Vector4.UNIT_W
|
||||
)
|
||||
}
|
||||
|
||||
fun isPlanar(vertexData: VertexData, eps: Double = 1E-2): Boolean {
|
||||
fun normal(i: Int): Vector3 {
|
||||
val p0 = vertexData.positions[positions[(i - 1).mod(positions.size)]]
|
||||
val p1 = vertexData.positions[positions[(i).mod(positions.size)]]
|
||||
val p2 = vertexData.positions[positions[(i + 1).mod(positions.size)]]
|
||||
|
||||
val u = (p0 - p1).normalized
|
||||
val v = (p2 - p1).normalized
|
||||
return u.cross(v).normalized
|
||||
}
|
||||
return if (positions.size <= 3) {
|
||||
true
|
||||
} else {
|
||||
val n0 = normal(0)
|
||||
(1 until positions.size - 2).all { n0.dot(normal(it)) >= 1.0 - eps }
|
||||
}
|
||||
}
|
||||
|
||||
fun isConvex(vertexData: VertexData): Boolean {
|
||||
val planar = base(vertexData).inversed
|
||||
|
||||
fun p(v: Vector3): Vector2 {
|
||||
return (planar * v.xyz1).xy
|
||||
}
|
||||
|
||||
if (positions.size < 3) {
|
||||
return false
|
||||
}
|
||||
var old = p(vertexData.positions[positions[positions.size - 2]])
|
||||
var new = p(vertexData.positions[positions[positions.size - 1]])
|
||||
var newDirection = atan2(new.y - old.y, new.x - old.x)
|
||||
var angleSum = 0.0
|
||||
var oldDirection: Double
|
||||
var orientation = Double.POSITIVE_INFINITY
|
||||
for ((ndx, newPointIndex) in positions.withIndex()) {
|
||||
old = new
|
||||
oldDirection = newDirection
|
||||
val newPoint = p(vertexData.positions[newPointIndex])
|
||||
new = newPoint
|
||||
newDirection = atan2(new.y - old.y, new.x - old.x)
|
||||
|
||||
if (old == new) {
|
||||
return false
|
||||
}
|
||||
var angle = newDirection - oldDirection
|
||||
if (angle <= -Math.PI)
|
||||
angle += PI * 2.0
|
||||
|
||||
if (angle > PI) {
|
||||
angle -= PI * 2.0
|
||||
}
|
||||
|
||||
if (ndx == 0) {
|
||||
if (angle == 0.0) {
|
||||
return false
|
||||
}
|
||||
orientation = if (angle > 0.0) 1.0 else -1.0
|
||||
|
||||
} else {
|
||||
if (orientation * angle <= 0.0) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
angleSum += angle
|
||||
|
||||
}
|
||||
return abs(round(angleSum / (2 * PI))) == 1.0
|
||||
}
|
||||
|
||||
fun tessellate(vertexData: VertexData): List<IndexedPolygon> {
|
||||
val points = vertexData.positions.slice(positions.toList())
|
||||
val triangles = org.openrndr.shape.triangulate(listOf(points))
|
||||
|
||||
return triangles.windowed(3, 3).map {
|
||||
IndexedPolygon(
|
||||
positions.sliceArray(it),
|
||||
if (textureCoords.isNotEmpty()) textureCoords.sliceArray(it) else intArrayOf(),
|
||||
if (normals.isNotEmpty()) normals.sliceArray(it) else intArrayOf()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun triangulate(vertexData: VertexData): List<IndexedPolygon> {
|
||||
return when {
|
||||
positions.size == 3 -> listOf(this)
|
||||
isPlanar(vertexData) && isConvex(vertexData) -> {
|
||||
val triangleCount = positions.size - 2
|
||||
(0 until triangleCount).map {
|
||||
IndexedPolygon(
|
||||
intArrayOf(positions[0], positions[it + 1], positions[it + 2]),
|
||||
listOfNotNull(
|
||||
textureCoords.getOrNull(0),
|
||||
textureCoords.getOrNull(it),
|
||||
textureCoords.getOrNull(it + 1)
|
||||
).toIntArray(),
|
||||
listOfNotNull(
|
||||
normals.getOrNull(0),
|
||||
normals.getOrNull(it),
|
||||
normals.getOrNull(it + 1)
|
||||
).toIntArray(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
else -> tessellate(vertexData)
|
||||
}
|
||||
}
|
||||
|
||||
fun toPolygon(vertexData: VertexData): Polygon {
|
||||
return Polygon(
|
||||
vertexData.positions.slice(positions.toList()).toTypedArray(),
|
||||
vertexData.normals.slice(normals.toList()).toTypedArray(),
|
||||
vertexData.textureCoords.slice(textureCoords.toList()).toTypedArray()
|
||||
)
|
||||
}
|
||||
}
|
||||
18
orx-obj-loader/src/main/kotlin/MeshData.kt
Normal file
18
orx-obj-loader/src/main/kotlin/MeshData.kt
Normal file
@@ -0,0 +1,18 @@
|
||||
package org.openrndr.extra.objloader
|
||||
|
||||
@JvmRecord
|
||||
data class MeshData(val vertexData: VertexData, val polygonGroups: Map<String, List<IndexedPolygon>>) {
|
||||
fun triangulate(): MeshData {
|
||||
return copy(polygonGroups = polygonGroups.mapValues {
|
||||
it.value.flatMap { polygon -> polygon.triangulate(vertexData) }
|
||||
})
|
||||
}
|
||||
|
||||
fun flattenPolygons(): Map<String, List<Polygon>> {
|
||||
return polygonGroups.mapValues {
|
||||
it.value.map { ip ->
|
||||
ip.toPolygon(vertexData)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
43
orx-obj-loader/src/main/kotlin/MeshDataExtensions.kt
Normal file
43
orx-obj-loader/src/main/kotlin/MeshDataExtensions.kt
Normal file
@@ -0,0 +1,43 @@
|
||||
package org.openrndr.extra.objloader
|
||||
|
||||
import org.openrndr.draw.VertexBuffer
|
||||
import org.openrndr.draw.vertexBuffer
|
||||
import org.openrndr.draw.vertexFormat
|
||||
import org.openrndr.math.Vector2
|
||||
|
||||
private val objVertexFormat = vertexFormat {
|
||||
position(3)
|
||||
normal(3)
|
||||
textureCoordinate(2)
|
||||
}
|
||||
|
||||
fun MeshData.toVertexBuffer() : VertexBuffer {
|
||||
val objects = triangulate().flattenPolygons()
|
||||
val triangleCount = objects.values.sumOf { it.size }
|
||||
val vertexBuffer = vertexBuffer(objVertexFormat, triangleCount * 3)
|
||||
|
||||
vertexBuffer.put {
|
||||
objects.entries.forEach {
|
||||
it.value.forEach {
|
||||
for (i in it.positions.indices) {
|
||||
write(it.positions[i])
|
||||
if (it.normals.isNotEmpty()) {
|
||||
write(it.normals[i])
|
||||
} else {
|
||||
val d0 = it.positions[2] - it.positions[0]
|
||||
val d1 = it.positions[1] - it.positions[0]
|
||||
write(d0.normalized.cross(d1.normalized).normalized)
|
||||
}
|
||||
if (it.textureCoords.isNotEmpty()) {
|
||||
write(it.textureCoords[i])
|
||||
} else {
|
||||
write(Vector2.ZERO)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
vertexBuffer.shadow.destroy()
|
||||
return vertexBuffer
|
||||
}
|
||||
@@ -1,66 +1,12 @@
|
||||
package org.openrndr.extra.objloader
|
||||
|
||||
import org.openrndr.draw.VertexBuffer
|
||||
import org.openrndr.draw.vertexBuffer
|
||||
import org.openrndr.draw.vertexFormat
|
||||
import org.openrndr.math.Matrix44
|
||||
import org.openrndr.math.Vector2
|
||||
import org.openrndr.math.Vector3
|
||||
import org.openrndr.math.*
|
||||
import java.io.File
|
||||
import java.net.MalformedURLException
|
||||
import java.net.URL
|
||||
import kotlin.math.max
|
||||
import kotlin.math.min
|
||||
|
||||
class Triangle(val positions: Array<Vector3> = emptyArray(),
|
||||
val normals: Array<Vector3> = emptyArray(),
|
||||
val textureCoords: Array<Vector2> = emptyArray()) {
|
||||
fun transform(t: Matrix44): Triangle {
|
||||
return Triangle(positions.map { (t * it.xyz1).div }.toTypedArray(), normals, textureCoords)
|
||||
}
|
||||
}
|
||||
|
||||
class Box(val corner: Vector3, val width: Double, val height: Double, val depth: Double)
|
||||
|
||||
fun bounds(triangles: List<Triangle>): Box {
|
||||
var minX = Double.POSITIVE_INFINITY
|
||||
var minY = Double.POSITIVE_INFINITY
|
||||
var minZ = Double.POSITIVE_INFINITY
|
||||
|
||||
var maxX = Double.NEGATIVE_INFINITY
|
||||
var maxY = Double.NEGATIVE_INFINITY
|
||||
var maxZ = Double.NEGATIVE_INFINITY
|
||||
|
||||
triangles.forEach {
|
||||
it.positions.forEach {
|
||||
minX = min(minX, it.x)
|
||||
minY = min(minY, it.y)
|
||||
minZ = min(minZ, it.z)
|
||||
|
||||
maxX = max(maxX, it.x)
|
||||
maxY = max(maxY, it.y)
|
||||
maxZ = max(maxZ, it.z)
|
||||
}
|
||||
}
|
||||
return Box(Vector3(minX, minY, minZ), maxX - minX, maxY - minY, maxZ - minZ)
|
||||
}
|
||||
|
||||
fun List<Triangle>.vertexBuffer(): VertexBuffer {
|
||||
val vertexBuffer = vertexBuffer(objVertexFormat, size * 3)
|
||||
vertexBuffer.put {
|
||||
this@vertexBuffer.forEach {
|
||||
for (i in it.positions.indices) {
|
||||
write(it.positions[i])
|
||||
write(it.normals[i])
|
||||
write(Vector2.ZERO)
|
||||
}
|
||||
}
|
||||
}
|
||||
vertexBuffer.shadow.destroy()
|
||||
return vertexBuffer
|
||||
}
|
||||
|
||||
fun loadOBJ(fileOrUrl: String): Map<String, List<Triangle>> {
|
||||
fun loadOBJ(fileOrUrl: String): Map<String, List<Polygon>> {
|
||||
return try {
|
||||
val url = URL(fileOrUrl)
|
||||
loadOBJ(url)
|
||||
@@ -69,12 +15,6 @@ fun loadOBJ(fileOrUrl: String): Map<String, List<Triangle>> {
|
||||
}
|
||||
}
|
||||
|
||||
private val objVertexFormat = vertexFormat {
|
||||
position(3)
|
||||
normal(3)
|
||||
textureCoordinate(2)
|
||||
}
|
||||
|
||||
fun loadOBJasVertexBuffer(fileOrUrl: String): VertexBuffer {
|
||||
return try {
|
||||
val url = URL(fileOrUrl)
|
||||
@@ -87,51 +27,24 @@ fun loadOBJasVertexBuffer(fileOrUrl: String): VertexBuffer {
|
||||
fun loadOBJasVertexBuffer(url: URL): VertexBuffer = loadOBJasVertexBuffer(url.readText().split("\n"))
|
||||
fun loadOBJasVertexBuffer(file: File): VertexBuffer = loadOBJasVertexBuffer(file.readLines())
|
||||
fun loadOBJasVertexBuffer(lines: List<String>): VertexBuffer {
|
||||
val objects = loadOBJ(lines)
|
||||
val triangleCount = objects.values.sumBy { it.size }
|
||||
val vertexBuffer = vertexBuffer(objVertexFormat, triangleCount * 3)
|
||||
|
||||
vertexBuffer.put {
|
||||
objects.entries.forEach {
|
||||
it.value.forEach {
|
||||
for (i in it.positions.indices) {
|
||||
write(it.positions[i])
|
||||
if (it.normals.isNotEmpty()) {
|
||||
write(it.normals[i])
|
||||
} else {
|
||||
val d0 = it.positions[2] - it.positions[0]
|
||||
val d1 = it.positions[1] - it.positions[0]
|
||||
write(d0.normalized.cross(d1.normalized).normalized)
|
||||
}
|
||||
if (it.textureCoords.isNotEmpty()) {
|
||||
write(it.textureCoords[i])
|
||||
} else {
|
||||
write(Vector2.ZERO)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
vertexBuffer.shadow.destroy()
|
||||
return vertexBuffer
|
||||
return loadOBJMeshData(lines).toVertexBuffer()
|
||||
}
|
||||
|
||||
fun loadOBJ(file: File) = loadOBJ(file.readLines())
|
||||
fun loadOBJEx(file: File) = loadOBJEx(file.readLines())
|
||||
fun loadOBJEx(file: File) = loadOBJMeshData(file.readLines())
|
||||
fun loadOBJ(url: URL) = loadOBJ(url.readText().split("\n"))
|
||||
fun loadOBJEx(url: URL) = loadOBJEx(url.readText().split("\n"))
|
||||
fun loadOBJEx(url: URL) = loadOBJMeshData(url.readText().split("\n"))
|
||||
|
||||
class OBJData(val positions: List<Vector3>, val normals: List<Vector3>, val textureCoords: List<Vector2>)
|
||||
fun loadOBJ(lines: List<String>): Map<String, List<Polygon>> = loadOBJMeshData(lines).triangulate().flattenPolygons()
|
||||
|
||||
fun loadOBJ(lines: List<String>): Map<String, List<Triangle>> = loadOBJEx(lines).second
|
||||
|
||||
fun loadOBJEx(lines: List<String>): Pair<OBJData, Map<String, List<Triangle>>> {
|
||||
val meshes = mutableMapOf<String, List<Triangle>>()
|
||||
fun loadOBJMeshData(file: File) = loadOBJMeshData(file.readLines())
|
||||
fun loadOBJMeshData(lines: List<String>): MeshData {
|
||||
val meshes = mutableMapOf<String, List<IndexedPolygon>>()
|
||||
val positions = mutableListOf<Vector3>()
|
||||
val normals = mutableListOf<Vector3>()
|
||||
val textureCoords = mutableListOf<Vector2>()
|
||||
var activeMesh = mutableListOf<Triangle>()
|
||||
var activeMesh = mutableListOf<IndexedPolygon>()
|
||||
|
||||
lines.forEach { line ->
|
||||
if (line.isNotEmpty()) {
|
||||
@@ -142,54 +55,41 @@ fun loadOBJEx(lines: List<String>): Pair<OBJData, Map<String, List<Triangle>>> {
|
||||
"v" -> {
|
||||
positions += Vector3(tokens[1].toDouble(), tokens[2].toDouble(), tokens[3].toDouble())
|
||||
}
|
||||
|
||||
"vn" -> normals += Vector3(tokens[1].toDouble(), tokens[2].toDouble(), tokens[3].toDouble())
|
||||
"vt" -> textureCoords += Vector2(tokens[1].toDouble(), tokens[2].toDouble())
|
||||
"g" -> {
|
||||
activeMesh = mutableListOf()
|
||||
meshes[tokens.getOrNull(1) ?: "no-name-${meshes.size}"] = activeMesh
|
||||
}
|
||||
|
||||
"f" -> {
|
||||
val indices = tokens.subList(1, tokens.size).map { it.split("/") }.map {
|
||||
it.map { it.toIntOrNull() }
|
||||
it.map { it.toIntOrNull() ?: 0 }
|
||||
}
|
||||
val hasPosition = (indices[0].getOrNull(0) ?: 0) != 0
|
||||
val hasUV = (indices[0].getOrNull(1) ?: 0) != 0
|
||||
val hasNormal = (indices[0].getOrNull(2) ?: 0) != 0
|
||||
|
||||
for (i in 0 until indices.size - 2) {
|
||||
activeMesh.add(
|
||||
IndexedPolygon(
|
||||
if (hasPosition) indices.map { it[0] - 1 }.toIntArray() else intArrayOf(),
|
||||
if (hasUV) indices.map { it[1] - 1 }.toIntArray() else intArrayOf(),
|
||||
if (hasNormal) indices.map { it[2] - 1 }.toIntArray() else intArrayOf()
|
||||
)
|
||||
)
|
||||
|
||||
val attributes = indices[0].size
|
||||
val o = i * 2
|
||||
val s = indices.size
|
||||
|
||||
val ps = if (attributes >= 1) arrayOf(
|
||||
indices[(0 + o) % s][0]?.let { positions[it - 1] } ?: Vector3.ZERO,
|
||||
indices[(1 + o) % s][0]?.let { positions[it - 1] } ?: Vector3.ZERO,
|
||||
indices[(2 + o) % s][0]?.let { positions[it - 1] } ?: Vector3.ZERO)
|
||||
else
|
||||
emptyArray()
|
||||
|
||||
val tcs = if (attributes >= 2) arrayOf(
|
||||
indices[(0 + o) % s][1]?.let { textureCoords[it - 1] } ?: Vector2.ZERO,
|
||||
indices[(1 + o) % s][1]?.let { textureCoords[it - 1] } ?: Vector2.ZERO,
|
||||
indices[(2 + o) % s][1]?.let { textureCoords[it - 1] } ?: Vector2.ZERO)
|
||||
else
|
||||
emptyArray()
|
||||
|
||||
|
||||
val ns = if (attributes >= 3) arrayOf(
|
||||
indices[(0 + o) % s][2]?.let { normals[it - 1] } ?: Vector3.ZERO,
|
||||
indices[(1 + o) % s][2]?.let { normals[it - 1] } ?: Vector3.ZERO,
|
||||
indices[(2 + o) % s][2]?.let { normals[it - 1] } ?: Vector3.ZERO)
|
||||
else
|
||||
emptyArray()
|
||||
|
||||
activeMesh.add(Triangle(ps, ns, tcs))
|
||||
if (meshes.isEmpty()) {
|
||||
meshes["no-name"] = activeMesh
|
||||
}
|
||||
if (meshes.isEmpty()) {
|
||||
meshes["no-name"] = activeMesh
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return Pair(OBJData(positions, normals, textureCoords), meshes)
|
||||
|
||||
return MeshData(
|
||||
VertexData(positions.toTypedArray(), normals.toTypedArray(), textureCoords.toTypedArray()),
|
||||
meshes
|
||||
)
|
||||
}
|
||||
42
orx-obj-loader/src/main/kotlin/Polygon.kt
Normal file
42
orx-obj-loader/src/main/kotlin/Polygon.kt
Normal file
@@ -0,0 +1,42 @@
|
||||
package org.openrndr.extra.objloader
|
||||
|
||||
import org.openrndr.math.Matrix44
|
||||
import org.openrndr.math.Vector2
|
||||
import org.openrndr.math.Vector3
|
||||
import kotlin.math.max
|
||||
import kotlin.math.min
|
||||
|
||||
class Polygon(
|
||||
val positions: Array<Vector3> = emptyArray(),
|
||||
val normals: Array<Vector3> = emptyArray(),
|
||||
val textureCoords: Array<Vector2> = emptyArray()
|
||||
) {
|
||||
fun transform(t: Matrix44): Polygon {
|
||||
return Polygon(positions.map { (t * it.xyz1).div }.toTypedArray(), normals, textureCoords)
|
||||
}
|
||||
}
|
||||
|
||||
class Box(val corner: Vector3, val width: Double, val height: Double, val depth: Double)
|
||||
|
||||
fun bounds(polygons: List<Polygon>): Box {
|
||||
var minX = Double.POSITIVE_INFINITY
|
||||
var minY = Double.POSITIVE_INFINITY
|
||||
var minZ = Double.POSITIVE_INFINITY
|
||||
|
||||
var maxX = Double.NEGATIVE_INFINITY
|
||||
var maxY = Double.NEGATIVE_INFINITY
|
||||
var maxZ = Double.NEGATIVE_INFINITY
|
||||
|
||||
polygons.forEach {
|
||||
it.positions.forEach {
|
||||
minX = min(minX, it.x)
|
||||
minY = min(minY, it.y)
|
||||
minZ = min(minZ, it.z)
|
||||
|
||||
maxX = max(maxX, it.x)
|
||||
maxY = max(maxY, it.y)
|
||||
maxZ = max(maxZ, it.z)
|
||||
}
|
||||
}
|
||||
return Box(Vector3(minX, minY, minZ), maxX - minX, maxY - minY, maxZ - minZ)
|
||||
}
|
||||
10
orx-obj-loader/src/main/kotlin/VertexData.kt
Normal file
10
orx-obj-loader/src/main/kotlin/VertexData.kt
Normal file
@@ -0,0 +1,10 @@
|
||||
package org.openrndr.extra.objloader
|
||||
|
||||
import org.openrndr.math.Vector2
|
||||
import org.openrndr.math.Vector3
|
||||
|
||||
class VertexData(
|
||||
val positions: Array<Vector3> = emptyArray(),
|
||||
val normals: Array<Vector3> = emptyArray(),
|
||||
val textureCoords: Array<Vector2> = emptyArray()
|
||||
)
|
||||
9
orx-obj-loader/src/main/kotlin/Wireframe.kt
Normal file
9
orx-obj-loader/src/main/kotlin/Wireframe.kt
Normal file
@@ -0,0 +1,9 @@
|
||||
package org.openrndr.extra.objloader
|
||||
|
||||
import org.openrndr.math.Vector3
|
||||
|
||||
fun MeshData.wireframe() : List<List<Vector3>> {
|
||||
return polygonGroups.values.flatMap {
|
||||
it.map { ip -> ip.toPolygon(this.vertexData).positions.toList() }
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user