[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

26
orx-mesh/build.gradle.kts Normal file
View File

@@ -0,0 +1,26 @@
plugins {
org.openrndr.extra.convention.`kotlin-multiplatform`
}
kotlin {
sourceSets {
val commonMain by getting {
dependencies {
api(libs.openrndr.application)
api(libs.openrndr.math)
api(libs.openrndr.shape)
implementation(project(":orx-shapes"))
}
}
val jvmDemo by getting {
dependencies {
api(libs.openrndr.shape)
implementation(project(":orx-shapes"))
implementation(project(":orx-mesh-generators"))
implementation(project(":orx-camera"))
implementation(project(":orx-noise"))
}
}
}
}

View File

@@ -0,0 +1,30 @@
package org.openrndr.extra.objloader
interface ICompoundMeshData {
val vertexData: IVertexData
val compounds: Map<String, IMeshData>
fun triangulate(): ICompoundMeshData
}
class CompoundMeshData(
override val vertexData: VertexData,
override val compounds: Map<String, MeshData>
) : ICompoundMeshData {
override fun triangulate(): CompoundMeshData {
return CompoundMeshData(vertexData, compounds.mapValues {
it.value.triangulate()
})
}
}
class MutableCompoundMeshData(
override val vertexData: MutableVertexData,
override val compounds: MutableMap<String, MutableMeshData>
) : ICompoundMeshData {
override fun triangulate(): MutableCompoundMeshData {
TODO("Not yet implemented")
}
}

View File

@@ -0,0 +1,24 @@
package org.openrndr.extra.objloader
import org.openrndr.draw.VertexBuffer
import org.openrndr.draw.vertexBuffer
fun ICompoundMeshData.toVertexBuffer(): VertexBuffer {
val triangulated = this.triangulate()
val triangleCount = triangulated.compounds.values.sumOf { it.polygons.size }
val vertexBuffer = vertexBuffer(objVertexFormat, triangleCount * 3)
var elementOffset = 0
for (compound in compounds) {
compound.value.toVertexBuffer(elementOffset, vertexBuffer)
elementOffset += compound.value.polygons.size * 3
}
return vertexBuffer
}
fun ICompoundMeshData.flattenPolygons(): Map<String, List<IPolygon>> {
return compounds.mapValues { it.value.flattenPolygons() }
}

View File

@@ -0,0 +1,200 @@
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
interface IIndexedPolygon {
val positions: List<Int>
val textureCoords: List<Int>
val normals: List<Int>
val colors: List<Int>
val tangents: List<Int>
val bitangents: List<Int>
fun base(vertexData: IVertexData): 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: IVertexData, 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: IVertexData): 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 <= -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 toPolygon(vertexData: IVertexData): IPolygon
}
data class IndexedPolygon(
override val positions: List<Int>,
override val textureCoords: List<Int>,
override val normals: List<Int>,
override val colors: List<Int>,
override val tangents: List<Int>,
override val bitangents: List<Int>
) : IIndexedPolygon {
fun tessellate(vertexData: IVertexData): 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.slice(it),
if (textureCoords.isNotEmpty()) textureCoords.slice(it) else listOf(),
if (normals.isNotEmpty()) normals.slice(it) else listOf(),
if (colors.isNotEmpty()) colors.slice(it) else listOf(),
if (tangents.isNotEmpty()) tangents.slice(it) else listOf(),
if (bitangents.isNotEmpty()) bitangents.slice(it) else listOf()
)
}
}
fun triangulate(vertexData: IVertexData): List<IndexedPolygon> {
return when {
positions.size == 3 -> listOf(this)
isPlanar(vertexData) && isConvex(vertexData) -> {
val triangleCount = positions.size - 2
(0 until triangleCount).map {
IndexedPolygon(
listOf(positions[0], positions[it + 1], positions[it + 2]),
listOfNotNull(
textureCoords.getOrNull(0),
textureCoords.getOrNull(it),
textureCoords.getOrNull(it + 1)
),
listOfNotNull(
normals.getOrNull(0),
normals.getOrNull(it),
normals.getOrNull(it + 1)
),
listOfNotNull(
colors.getOrNull(0),
colors.getOrNull(it + 1),
colors.getOrNull(it + 2)
),
listOfNotNull(
tangents.getOrNull(0),
tangents.getOrNull(it + 1),
tangents.getOrNull(it + 2)
),
listOfNotNull(
bitangents.getOrNull(0),
bitangents.getOrNull(it + 1),
bitangents.getOrNull(it + 2)
),
)
}
}
else -> tessellate(vertexData)
}
}
override fun toPolygon(vertexData: IVertexData): Polygon {
return Polygon(
vertexData.positions.slice(positions),
vertexData.normals.slice(normals),
vertexData.textureCoords.slice(textureCoords),
vertexData.colors.slice(colors)
)
}
}
data class MutableIndexedPolygon(
override val positions: MutableList<Int>,
override val textureCoords: MutableList<Int>,
override val normals: MutableList<Int>,
override val colors: MutableList<Int>,
override val tangents: MutableList<Int>,
override val bitangents: MutableList<Int>
) : IIndexedPolygon {
override fun toPolygon(vertexData: IVertexData): MutablePolygon {
return MutablePolygon(
vertexData.positions.slice(positions).toMutableList(),
vertexData.normals.slice(normals).toMutableList(),
vertexData.textureCoords.slice(textureCoords).toMutableList(),
vertexData.colors.slice(colors).toMutableList()
)
}
}

View File

@@ -0,0 +1,41 @@
package org.openrndr.extra.objloader
import kotlin.jvm.JvmRecord
interface IMeshData {
val vertexData: IVertexData
val polygons: List<IIndexedPolygon>
fun triangulate(): IMeshData
fun flattenPolygons(): List<IPolygon>
}
@JvmRecord
data class MeshData(
override val vertexData: VertexData,
override val polygons: List<IndexedPolygon>,
) : IMeshData {
override fun triangulate(): MeshData {
return copy(polygons = polygons.flatMap { polygon -> polygon.triangulate(vertexData) })
}
override fun flattenPolygons(): List<Polygon> {
return polygons.map { ip ->
ip.toPolygon(vertexData)
}
}
}
data class MutableMeshData(
override val vertexData: MutableVertexData,
override val polygons: MutableList<IndexedPolygon>
) : IMeshData {
override fun triangulate(): MutableMeshData {
return copy(polygons = polygons.flatMap { it.triangulate(vertexData) }.toMutableList())
}
override fun flattenPolygons(): List<Polygon> {
return polygons.map { it.toPolygon(vertexData) }
}
}

View File

@@ -0,0 +1,50 @@
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.
*/
internal val objVertexFormat = vertexFormat {
position(3)
normal(3)
textureCoordinate(2)
}
/**
* Converts a [MeshData] instance into a [VertexBuffer]
*/
fun IMeshData.toVertexBuffer(elementOffset: Int = 0, vertexBuffer: VertexBuffer? = null): VertexBuffer {
val objects = triangulate().flattenPolygons()
val triangleCount = objects.size
val vertexBuffer = vertexBuffer ?: vertexBuffer(objVertexFormat, triangleCount * 3)
vertexBuffer.put(elementOffset) {
objects.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

@@ -0,0 +1,103 @@
package org.openrndr.extra.objloader
import org.openrndr.color.ColorRGBa
import org.openrndr.math.Matrix44
import org.openrndr.math.Vector2
import org.openrndr.math.Vector3
import org.openrndr.shape.Box
import kotlin.math.max
import kotlin.math.min
interface IPolygon {
val positions: List<Vector3>
val normals: List<Vector3>
val textureCoords: List<Vector2>
val colors: List<ColorRGBa>
val tangents: List<Vector3>
val bitangents: List<Vector3>
fun transform(t: Matrix44): IPolygon
}
/**
* 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(
override val positions: List<Vector3> = emptyList(),
override val normals: List<Vector3> = emptyList(),
override val textureCoords: List<Vector2> = emptyList(),
override val colors: List<ColorRGBa> = emptyList(),
override val tangents: List<Vector3> = emptyList(),
override val bitangents: List<Vector3> = emptyList(),
) : IPolygon {
override fun transform(t: Matrix44): Polygon {
return Polygon(positions.map { (t * it.xyz1).div }, normals, textureCoords, colors, tangents, bitangents)
}
/**
* Create a [MutablePolygon] by copying
*/
fun toMutablePolygon(): MutablePolygon {
return MutablePolygon(
positions.toMutableList(),
normals.toMutableList(),
textureCoords.toMutableList(),
colors.toMutableList(),
tangents.toMutableList(),
bitangents.toMutableList()
)
}
}
class MutablePolygon(
override val positions: MutableList<Vector3> = mutableListOf(),
override val normals: MutableList<Vector3> = mutableListOf(),
override val textureCoords: MutableList<Vector2> = mutableListOf(),
override val colors: MutableList<ColorRGBa> = mutableListOf(),
override val tangents: MutableList<Vector3> = mutableListOf(),
override val bitangents: MutableList<Vector3> = mutableListOf()
) : IPolygon {
override fun transform(t: Matrix44): MutablePolygon {
return MutablePolygon(
positions.map { (t * it.xyz1).div }.toMutableList(),
ArrayList(normals),
ArrayList(textureCoords),
ArrayList(colors),
ArrayList(tangents),
ArrayList(bitangents)
)
}
}
/**
* Calculates the 3D bounding box of a list of [IPolygon].
*/
fun bounds(polygons: List<IPolygon>): 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

@@ -0,0 +1,59 @@
package org.openrndr.extra.objloader
import org.openrndr.color.ColorRGBa
import org.openrndr.math.Vector2
import org.openrndr.math.Vector3
/**
* Vertex data interface
*/
interface IVertexData {
/**
* Vertex positions
*/
val positions: List<Vector3>
/**
* Vertex normals
*/
val normals: List<Vector3>
/**
* Vertex texture coordinates
*/
val textureCoords: List<Vector2>
/**
* Vertex colors
*/
val colors: List<ColorRGBa>
/**
* Vertex tangents
*/
val tangents: List<Vector3>
/**
* Vertex bitangents
*/
val bitangents: List<Vector3>
}
class VertexData(
override val positions: List<Vector3> = emptyList(),
override val normals: List<Vector3> = emptyList(),
override val textureCoords: List<Vector2> = emptyList(),
override val colors: List<ColorRGBa> = emptyList(),
override val tangents: List<Vector3> = emptyList(),
override val bitangents: List<Vector3> = emptyList()
) : IVertexData
class MutableVertexData(
override val positions: MutableList<Vector3> = mutableListOf(),
override val normals: MutableList<Vector3> = mutableListOf(),
override val textureCoords: MutableList<Vector2> = mutableListOf(),
override val colors: MutableList<ColorRGBa> = mutableListOf(),
override val tangents: MutableList<Vector3> = mutableListOf(),
override val bitangents: MutableList<Vector3> = mutableListOf()
) : IVertexData

View File

@@ -0,0 +1,13 @@
package org.openrndr.extra.objloader
import org.openrndr.math.Vector3
fun IMeshData.wireframe(): List<List<Vector3>> {
return polygons.map { ip -> ip.toPolygon(this.vertexData).positions.toList() }
}
fun ICompoundMeshData.wireframe(): List<List<Vector3>> {
return compounds.values.flatMap {
it.wireframe()
}
}