[orx-mesh] Split MeshData types from orx-obj-loader

This commit is contained in:
Edwin Jakobs
2024-09-13 22:41:31 +02:00
parent 3588aecd58
commit 8238207894
16 changed files with 466 additions and 172 deletions

View File

@@ -6,6 +6,7 @@ dependencies {
implementation(libs.openrndr.application)
implementation(libs.openrndr.math)
implementation(libs.openrndr.ffmpeg)
api(project(":orx-mesh"))
demoImplementation(project(":orx-camera"))
demoImplementation(project(":orx-mesh-generators"))
}

View File

@@ -1,144 +0,0 @@
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()
)
}
}

View File

@@ -1,18 +0,0 @@
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)
}
}
}
}

View File

@@ -1,50 +0,0 @@
package org.openrndr.extra.objloader
import org.openrndr.draw.VertexBuffer
import org.openrndr.draw.VertexFormat
import org.openrndr.draw.vertexBuffer
import org.openrndr.draw.vertexFormat
import org.openrndr.math.Vector2
/**
* The [VertexFormat] for a [VertexBuffer] with positions, normals and texture coordinates.
*/
private val objVertexFormat = vertexFormat {
position(3)
normal(3)
textureCoordinate(2)
}
/**
* Converts a [MeshData] instance into a [VertexBuffer]
*/
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
}

View File

@@ -1,5 +1,6 @@
package org.openrndr.extra.objloader
import org.openrndr.color.ColorRGBa
import org.openrndr.draw.VertexBuffer
import org.openrndr.math.*
import java.io.File
@@ -10,7 +11,7 @@ import java.net.URL
* Loads an OBJ file as a Map of names to lists of [Polygon].
* Use this method to access the loaded OBJ data from the CPU.
*/
fun loadOBJ(fileOrUrl: String): Map<String, List<Polygon>> {
fun loadOBJ(fileOrUrl: String): Map<String, List<IPolygon>> {
return try {
val url = URL(fileOrUrl)
loadOBJ(url)
@@ -43,15 +44,16 @@ fun loadOBJEx(file: File) = loadOBJMeshData(file.readLines())
fun loadOBJ(url: URL) = loadOBJ(url.readText().split("\n"))
fun loadOBJEx(url: URL) = loadOBJMeshData(url.readText().split("\n"))
fun loadOBJ(lines: List<String>): Map<String, List<Polygon>> = loadOBJMeshData(lines).triangulate().flattenPolygons()
fun loadOBJ(lines: List<String>): Map<String, List<IPolygon>> = loadOBJMeshData(lines).triangulate().flattenPolygons()
fun loadOBJMeshData(file: File) = loadOBJMeshData(file.readLines())
fun loadOBJMeshData(lines: List<String>): MeshData {
fun loadOBJMeshData(lines: List<String>): CompoundMeshData {
val meshes = mutableMapOf<String, List<IndexedPolygon>>()
val positions = mutableListOf<Vector3>()
val normals = mutableListOf<Vector3>()
val textureCoords = mutableListOf<Vector2>()
val colors = mutableListOf<ColorRGBa>()
var activeMesh = mutableListOf<IndexedPolygon>()
lines.forEach { line ->
@@ -60,7 +62,32 @@ fun loadOBJMeshData(lines: List<String>): MeshData {
if (tokens.isNotEmpty()) {
when (tokens[0]) {
"v" -> positions += Vector3(tokens[1].toDouble(), tokens[2].toDouble(), tokens[3].toDouble())
"v" -> {
when (tokens.size) {
3, 4 -> {
positions += Vector3(tokens[1].toDouble(), tokens[2].toDouble(), tokens[3].toDouble())
colors += ColorRGBa.WHITE
}
6 -> {
positions += Vector3(tokens[1].toDouble(), tokens[2].toDouble(), tokens[3].toDouble())
colors += ColorRGBa(tokens[4].toDouble(), tokens[5].toDouble(), tokens[6].toDouble())
}
7 -> {
positions += Vector3(tokens[1].toDouble(), tokens[2].toDouble(), tokens[3].toDouble())
colors += ColorRGBa(
tokens[4].toDouble(),
tokens[5].toDouble(),
tokens[6].toDouble(),
tokens[7].toDouble()
)
}
else -> error("vertex has ${tokens.size - 1} components, loader only supports 3/4/6/7 components")
}
}
"vn" -> normals += Vector3(tokens[1].toDouble(), tokens[2].toDouble(), tokens[3].toDouble())
"vt" -> textureCoords += Vector2(tokens[1].toDouble(), tokens[2].toDouble())
"g" -> {
@@ -75,12 +102,16 @@ fun loadOBJMeshData(lines: List<String>): MeshData {
val hasPosition = (indices[0].getOrNull(0) ?: 0) != 0
val hasUV = (indices[0].getOrNull(1) ?: 0) != 0
val hasNormal = (indices[0].getOrNull(2) ?: 0) != 0
val hasColor = hasPosition
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()
if (hasPosition) indices.map { it[0] - 1 } else listOf(),
if (hasUV) indices.map { it[1] - 1 } else listOf(),
if (hasNormal) indices.map { it[2] - 1 } else listOf(),
if (hasColor) indices.map { it[0] - 1 } else listOf(),
emptyList(),
emptyList()
)
)
@@ -93,8 +124,11 @@ fun loadOBJMeshData(lines: List<String>): MeshData {
}
}
return MeshData(
VertexData(positions.toTypedArray(), normals.toTypedArray(), textureCoords.toTypedArray()),
meshes
val vertexData = VertexData(positions, normals, textureCoords, colors)
return CompoundMeshData(
vertexData,
meshes.mapValues {
MeshData(vertexData, it.value)
}
)
}

View File

@@ -1,56 +0,0 @@
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
/**
* A 3D Polygon
*
* @property positions Vertex 3D positions
* @property normals Vertex 3D normals
* @property textureCoords Vertex 2D texture coordinates
* @constructor Create empty 3D Polygon
*/
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)
}
}
/**
* A 3D Box defined by an anchor point ([corner]), [width], [height] and [depth].
*/
class Box(val corner: Vector3, val width: Double, val height: Double, val depth: Double)
/**
* Calculates the 3D bounding box of a list of [Polygon].
*/
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 { pos ->
minX = min(minX, pos.x)
minY = min(minY, pos.y)
minZ = min(minZ, pos.z)
maxX = max(maxX, pos.x)
maxY = max(maxY, pos.y)
maxZ = max(maxZ, pos.z)
}
}
return Box(Vector3(minX, minY, minZ), maxX - minX, maxY - minY, maxZ - minZ)
}

View File

@@ -1,10 +0,0 @@
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()
)

View File

@@ -1,9 +0,0 @@
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() }
}
}