diff --git a/orx-mesh/src/commonMain/kotlin/CompoundMeshData.kt b/orx-mesh/src/commonMain/kotlin/CompoundMeshData.kt index b521c260a..c92d3563c 100644 --- a/orx-mesh/src/commonMain/kotlin/CompoundMeshData.kt +++ b/orx-mesh/src/commonMain/kotlin/CompoundMeshData.kt @@ -1,5 +1,8 @@ package org.openrndr.extra.objloader +/** + * Compound mesh data interface + */ interface ICompoundMeshData { val vertexData: IVertexData val compounds: Map @@ -12,6 +15,10 @@ class CompoundMeshData( override val compounds: Map ) : ICompoundMeshData { + init { + + } + override fun triangulate(): CompoundMeshData { return CompoundMeshData(vertexData, compounds.mapValues { it.value.triangulate() @@ -25,6 +32,10 @@ class MutableCompoundMeshData( ) : ICompoundMeshData { override fun triangulate(): MutableCompoundMeshData { - TODO("Not yet implemented") + return MutableCompoundMeshData( + vertexData, + compounds.mapValues { + it.value.triangulate() + }.toMutableMap()) } } \ No newline at end of file diff --git a/orx-mesh/src/commonMain/kotlin/CompoundMeshDataExtensions.kt b/orx-mesh/src/commonMain/kotlin/CompoundMeshDataExtensions.kt index 22087b9f1..6307a06c7 100644 --- a/orx-mesh/src/commonMain/kotlin/CompoundMeshDataExtensions.kt +++ b/orx-mesh/src/commonMain/kotlin/CompoundMeshDataExtensions.kt @@ -19,6 +19,6 @@ fun ICompoundMeshData.toVertexBuffer(): VertexBuffer { return vertexBuffer } -fun ICompoundMeshData.flattenPolygons(): Map> { - return compounds.mapValues { it.value.flattenPolygons() } +fun ICompoundMeshData.toPolygons(): Map> { + return compounds.mapValues { it.value.toPolygons() } } \ No newline at end of file diff --git a/orx-mesh/src/commonMain/kotlin/IndexedPolygon.kt b/orx-mesh/src/commonMain/kotlin/IndexedPolygon.kt index d49ce0a8e..aaa83f374 100644 --- a/orx-mesh/src/commonMain/kotlin/IndexedPolygon.kt +++ b/orx-mesh/src/commonMain/kotlin/IndexedPolygon.kt @@ -9,12 +9,38 @@ import kotlin.math.abs import kotlin.math.atan2 import kotlin.math.round +/** + * Indexed polygon interface + */ interface IIndexedPolygon { + /** + * Position indices + */ val positions: List + + /** + * Texture coordinate indices, optional + */ val textureCoords: List + + /** + * Normal indices, optional + */ val normals: List + + /** + * Color indices, optional + */ val colors: List + + /** + * Tangents, optional + */ val tangents: List + + /** + * Bitangents, optional + */ val bitangents: List fun base(vertexData: IVertexData): Matrix44 { @@ -30,6 +56,11 @@ interface IIndexedPolygon { ) } + /** + * Determine if polygon is planar + * @param vertexData the vertex data + * @param eps error tolerance + */ fun isPlanar(vertexData: IVertexData, eps: Double = 1E-2): Boolean { fun normal(i: Int): Vector3 { val p0 = vertexData.positions[positions[(i - 1).mod(positions.size)]] @@ -48,6 +79,9 @@ interface IIndexedPolygon { } } + /** + * Determine polygon convexity + */ fun isConvex(vertexData: IVertexData): Boolean { val planar = base(vertexData).inversed @@ -99,10 +133,16 @@ interface IIndexedPolygon { return abs(round(angleSum / (2 * PI))) == 1.0 } + /** + * Convert to [IPolygon] + * @param vertexData the vertex data required to build the [IPolygon] + */ fun toPolygon(vertexData: IVertexData): IPolygon } - +/** + * Immutable indexed polygon implementation + */ data class IndexedPolygon( override val positions: List, override val textureCoords: List, @@ -113,7 +153,7 @@ data class IndexedPolygon( ) : IIndexedPolygon { - fun tessellate(vertexData: IVertexData): List { + private fun tessellate(vertexData: IVertexData): List { val points = vertexData.positions.slice(positions.toList()) val triangles = org.openrndr.shape.triangulate(listOf(points)) @@ -129,6 +169,11 @@ data class IndexedPolygon( } } + /** + * Convert to a list of triangle [IndexedPolygon] + * + * Supports non-planar and non-convex polygons + */ fun triangulate(vertexData: IVertexData): List { return when { positions.size == 3 -> listOf(this) @@ -180,6 +225,9 @@ data class IndexedPolygon( } } +/** + * Mutable indexed polygon implementation + */ data class MutableIndexedPolygon( override val positions: MutableList, override val textureCoords: MutableList, diff --git a/orx-mesh/src/commonMain/kotlin/MeshData.kt b/orx-mesh/src/commonMain/kotlin/MeshData.kt index 6b46e7cee..ea164ec5c 100644 --- a/orx-mesh/src/commonMain/kotlin/MeshData.kt +++ b/orx-mesh/src/commonMain/kotlin/MeshData.kt @@ -2,13 +2,19 @@ package org.openrndr.extra.objloader import kotlin.jvm.JvmRecord +/** + * Mesh data interface + */ interface IMeshData { val vertexData: IVertexData val polygons: List fun triangulate(): IMeshData - fun flattenPolygons(): List + fun toPolygons(): List } +/** + * Immutable mesh data implementation + */ @JvmRecord data class MeshData( override val vertexData: VertexData, @@ -18,7 +24,7 @@ data class MeshData( return copy(polygons = polygons.flatMap { polygon -> polygon.triangulate(vertexData) }) } - override fun flattenPolygons(): List { + override fun toPolygons(): List { return polygons.map { ip -> ip.toPolygon(vertexData) } @@ -26,6 +32,9 @@ data class MeshData( } +/** + * Mutable mesh data implementation + */ data class MutableMeshData( override val vertexData: MutableVertexData, override val polygons: MutableList @@ -34,8 +43,7 @@ data class MutableMeshData( return copy(polygons = polygons.flatMap { it.triangulate(vertexData) }.toMutableList()) } - override fun flattenPolygons(): List { + override fun toPolygons(): List { return polygons.map { it.toPolygon(vertexData) } - } } \ No newline at end of file diff --git a/orx-mesh/src/commonMain/kotlin/MeshDataExtensions.kt b/orx-mesh/src/commonMain/kotlin/MeshDataExtensions.kt index 5397ef3b7..319852d55 100644 --- a/orx-mesh/src/commonMain/kotlin/MeshDataExtensions.kt +++ b/orx-mesh/src/commonMain/kotlin/MeshDataExtensions.kt @@ -1,5 +1,6 @@ package org.openrndr.extra.objloader +import org.openrndr.color.ColorRGBa import org.openrndr.draw.VertexBuffer import org.openrndr.draw.VertexFormat import org.openrndr.draw.vertexBuffer @@ -13,13 +14,14 @@ internal val objVertexFormat = vertexFormat { position(3) normal(3) textureCoordinate(2) + color(4) } /** * Converts a [MeshData] instance into a [VertexBuffer] */ fun IMeshData.toVertexBuffer(elementOffset: Int = 0, vertexBuffer: VertexBuffer? = null): VertexBuffer { - val objects = triangulate().flattenPolygons() + val objects = triangulate().toPolygons() val triangleCount = objects.size val vertexBuffer = vertexBuffer ?: vertexBuffer(objVertexFormat, triangleCount * 3) @@ -40,11 +42,14 @@ fun IMeshData.toVertexBuffer(elementOffset: Int = 0, vertexBuffer: VertexBuffer? } else { write(Vector2.ZERO) } + if (it.colors.isNotEmpty()) { + write(it.colors[i]) + } else { + write(ColorRGBa.WHITE) + } } } } - - vertexBuffer.shadow.destroy() return vertexBuffer } \ No newline at end of file diff --git a/orx-mesh/src/commonMain/kotlin/Polygon.kt b/orx-mesh/src/commonMain/kotlin/Polygon.kt index 8d2635f96..44fdb34c4 100644 --- a/orx-mesh/src/commonMain/kotlin/Polygon.kt +++ b/orx-mesh/src/commonMain/kotlin/Polygon.kt @@ -8,6 +8,10 @@ import org.openrndr.shape.Box import kotlin.math.max import kotlin.math.min + +/** + * 3D Polygon interface + */ interface IPolygon { val positions: List val normals: List @@ -20,7 +24,7 @@ interface IPolygon { } /** - * A 3D Polygon + * Immutable 3D Polygon implementation * * @property positions Vertex 3D positions * @property normals Vertex 3D normals @@ -54,6 +58,9 @@ class Polygon( } } +/** + * Mutable 3D Polygon implementation + */ class MutablePolygon( override val positions: MutableList = mutableListOf(), override val normals: MutableList = mutableListOf(), @@ -77,7 +84,7 @@ class MutablePolygon( /** - * Calculates the 3D bounding box of a list of [IPolygon]. + * Calculate the 3D bounding box of a list of [IPolygon]. */ fun bounds(polygons: List): Box { var minX = Double.POSITIVE_INFINITY @@ -101,3 +108,42 @@ fun bounds(polygons: List): Box { } return Box(Vector3(minX, minY, minZ), maxX - minX, maxY - minY, maxZ - minZ) } + + +/** + * Convert list of polygons to [MeshData] + */ +fun List.toMeshData(): MeshData { + val vertexData = MutableVertexData() + + for (p in this) { + vertexData.positions.addAll(p.positions) + vertexData.normals.addAll(p.normals) + vertexData.colors.addAll(p.colors) + vertexData.textureCoords.addAll(p.textureCoords) + vertexData.tangents.addAll(p.tangents) + vertexData.bitangents.addAll(p.bitangents) + } + + val indexedPolygons = mutableListOf() + + var vertexOffset = 0 + for (p in this) { + + val indices = (vertexOffset until vertexOffset + p.positions.size).toList() + + indexedPolygons.add( + IndexedPolygon( + positions = indices, + textureCoords = if (p.textureCoords.isNotEmpty()) indices else emptyList(), + normals = if (p.normals.isNotEmpty()) indices else emptyList(), + colors = if (p.colors.isNotEmpty()) indices else emptyList(), + tangents = if (p.tangents.isNotEmpty()) indices else emptyList(), + bitangents = if (p.bitangents.isNotEmpty()) indices else emptyList() + ) + ) + + vertexOffset += p.positions.size + } + return MeshData(vertexData.toVertexData(), indexedPolygons) +} \ No newline at end of file diff --git a/orx-mesh/src/commonMain/kotlin/VertexData.kt b/orx-mesh/src/commonMain/kotlin/VertexData.kt index 682b04402..c3754da46 100644 --- a/orx-mesh/src/commonMain/kotlin/VertexData.kt +++ b/orx-mesh/src/commonMain/kotlin/VertexData.kt @@ -4,7 +4,6 @@ import org.openrndr.color.ColorRGBa import org.openrndr.math.Vector2 import org.openrndr.math.Vector3 - /** * Vertex data interface */ @@ -40,6 +39,9 @@ interface IVertexData { val bitangents: List } +/** + * Immutable vertex data implementation + */ class VertexData( override val positions: List = emptyList(), override val normals: List = emptyList(), @@ -47,8 +49,27 @@ class VertexData( override val colors: List = emptyList(), override val tangents: List = emptyList(), override val bitangents: List = emptyList() -) : IVertexData +) : IVertexData { + + /** + * Convert to [MutableVertexData] + */ + fun toMutableVertexData(): MutableVertexData { + return MutableVertexData( + positions.toMutableList(), + normals.toMutableList(), + textureCoords.toMutableList(), + colors.toMutableList(), + tangents.toMutableList(), + bitangents.toMutableList() + ) + } +} + +/** + * Mutable vertex data implementation + */ class MutableVertexData( override val positions: MutableList = mutableListOf(), override val normals: MutableList = mutableListOf(), @@ -56,4 +77,19 @@ class MutableVertexData( override val colors: MutableList = mutableListOf(), override val tangents: MutableList = mutableListOf(), override val bitangents: MutableList = mutableListOf() -) : IVertexData \ No newline at end of file +) : IVertexData { + + /** + * Convert to [VertexData] + */ + fun toVertexData(): VertexData { + return VertexData( + positions.toList(), + normals.toList(), + textureCoords.toList(), + colors.toList(), + tangents.toList(), + bitangents.toList() + ) + } +} \ No newline at end of file diff --git a/orx-mesh/src/commonMain/kotlin/Wireframe.kt b/orx-mesh/src/commonMain/kotlin/Wireframe.kt index f9c776d7e..01f30910f 100644 --- a/orx-mesh/src/commonMain/kotlin/Wireframe.kt +++ b/orx-mesh/src/commonMain/kotlin/Wireframe.kt @@ -2,12 +2,17 @@ package org.openrndr.extra.objloader import org.openrndr.math.Vector3 +/** + * Extract wireframe from mesh data + */ fun IMeshData.wireframe(): List> { return polygons.map { ip -> ip.toPolygon(this.vertexData).positions.toList() } } + +/** + * Extract wireframe from compound mesh data + */ fun ICompoundMeshData.wireframe(): List> { - return compounds.values.flatMap { - it.wireframe() - } + return compounds.values.flatMap { it.wireframe() } } \ No newline at end of file diff --git a/orx-mesh/src/jvmMain/kotlin/VertexBufferExtensions.kt b/orx-mesh/src/jvmMain/kotlin/VertexBufferExtensions.kt new file mode 100644 index 000000000..249c0bba3 --- /dev/null +++ b/orx-mesh/src/jvmMain/kotlin/VertexBufferExtensions.kt @@ -0,0 +1,57 @@ +import org.openrndr.color.ColorRGBa +import org.openrndr.draw.VertexBuffer +import org.openrndr.extra.objloader.Polygon +import org.openrndr.extra.objloader.objVertexFormat +import org.openrndr.math.Vector2 +import org.openrndr.math.Vector3 +import java.nio.ByteBuffer +import java.nio.ByteOrder + + +private fun ByteBuffer.getVector3(): Vector3 { + val x = getFloat() + val y = getFloat() + val z = getFloat() + return Vector3(x.toDouble(), y.toDouble(), z.toDouble()) +} + +private fun ByteBuffer.getVector2(): Vector2 { + val x = getFloat() + val y = getFloat() + return Vector2(x.toDouble(), y.toDouble()) +} + +private fun ByteBuffer.getColorRGBa(): ColorRGBa { + val r = getFloat() + val g = getFloat() + val b = getFloat() + val a = getFloat() + return ColorRGBa(r.toDouble(), g.toDouble(), b.toDouble(), a.toDouble()) +} + +/** + Convert vertex buffer contents to a list of [Polygon] instances + */ +fun VertexBuffer.toPolygons(vertexCount: Int = this.vertexCount): List { + require(vertexFormat == objVertexFormat) + val triangleCount = vertexCount / 3 + val buffer = ByteBuffer.allocateDirect(this.vertexCount * vertexFormat.size) + buffer.order(ByteOrder.nativeOrder()) + + val polygons = mutableListOf() + for (t in 0 until triangleCount) { + val positions = mutableListOf() + val normals = mutableListOf() + val textureCoordinates = mutableListOf() + val colors = mutableListOf() + + for (v in 0 until 3) { + positions.add(buffer.getVector3()) + normals.add(buffer.getVector3()) + textureCoordinates.add(buffer.getVector2()) + colors.add(buffer.getColorRGBa()) + } + polygons.add(Polygon(positions, normals, textureCoordinates, colors)) + } + return polygons +} \ No newline at end of file diff --git a/orx-obj-loader/README.md b/orx-obj-loader/README.md index 6962c272b..8b7aac693 100644 --- a/orx-obj-loader/README.md +++ b/orx-obj-loader/README.md @@ -25,21 +25,21 @@ vertexBuffer.saveOBJ("my/path/exported.obj") ## Demos ### DemoObjLoader01 -[source code](src/demo/kotlin/DemoObjLoader01.kt) +[source code](src/jvmDemo/kotlin/DemoObjLoader01.kt) ![DemoObjLoader01Kt](https://raw.githubusercontent.com/openrndr/orx/media/orx-obj-loader/images/DemoObjLoader01Kt.png) ### DemoObjSaver01 -[source code](src/demo/kotlin/DemoObjSaver01.kt) +[source code](src/jvmDemo/kotlin/DemoObjSaver01.kt) ![DemoObjSaver01Kt](https://raw.githubusercontent.com/openrndr/orx/media/orx-obj-loader/images/DemoObjSaver01Kt.png) ### DemoObjSaver02 -[source code](src/demo/kotlin/DemoObjSaver02.kt) +[source code](src/jvmDemo/kotlin/DemoObjSaver02.kt) ![DemoObjSaver02Kt](https://raw.githubusercontent.com/openrndr/orx/media/orx-obj-loader/images/DemoObjSaver02Kt.png) ### DemoWireframe01 -[source code](src/demo/kotlin/DemoWireframe01.kt) +[source code](src/jvmDemo/kotlin/DemoWireframe01.kt) ![DemoWireframe01Kt](https://raw.githubusercontent.com/openrndr/orx/media/orx-obj-loader/images/DemoWireframe01Kt.png) diff --git a/orx-obj-loader/build.gradle.kts b/orx-obj-loader/build.gradle.kts index 8c340b2f0..f98c27fd6 100644 --- a/orx-obj-loader/build.gradle.kts +++ b/orx-obj-loader/build.gradle.kts @@ -1,12 +1,22 @@ plugins { - org.openrndr.extra.convention.`kotlin-jvm` + org.openrndr.extra.convention.`kotlin-multiplatform` } -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")) -} +kotlin { + sourceSets { + val commonMain by getting { + dependencies { + implementation(libs.openrndr.application) + implementation(libs.openrndr.math) + api(project(":orx-mesh")) + } + } + + val jvmDemo by getting { + dependencies { + implementation(project(":orx-camera")) + implementation(project(":orx-mesh-generators")) + } + } + } +} \ No newline at end of file diff --git a/orx-obj-loader/src/main/kotlin/OBJLoader.kt b/orx-obj-loader/src/commonMain/kotlin/ObjReader.kt similarity index 67% rename from orx-obj-loader/src/main/kotlin/OBJLoader.kt rename to orx-obj-loader/src/commonMain/kotlin/ObjReader.kt index 8d1af4ca8..fddb1cf66 100644 --- a/orx-obj-loader/src/main/kotlin/OBJLoader.kt +++ b/orx-obj-loader/src/commonMain/kotlin/ObjReader.kt @@ -2,56 +2,15 @@ package org.openrndr.extra.objloader import org.openrndr.color.ColorRGBa import org.openrndr.draw.VertexBuffer -import org.openrndr.math.* -import java.io.File -import java.net.MalformedURLException -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> { - return try { - val url = URL(fileOrUrl) - loadOBJ(url) - } catch (e: MalformedURLException) { - loadOBJ(File(fileOrUrl)) - } -} - -/** - * Loads an OBJ file as a [VertexBuffer]. - * Use this method to render / process the loaded OBJ data using the GPU. - */ -fun loadOBJasVertexBuffer(fileOrUrl: String): VertexBuffer { - return try { - val url = URL(fileOrUrl) - loadOBJasVertexBuffer(url) - } catch (e: MalformedURLException) { - loadOBJasVertexBuffer(File(fileOrUrl)) - } -} - -fun loadOBJasVertexBuffer(url: URL): VertexBuffer = loadOBJasVertexBuffer(url.readText().split("\n")) -fun loadOBJasVertexBuffer(file: File): VertexBuffer = loadOBJasVertexBuffer(file.readLines()) -fun loadOBJasVertexBuffer(lines: List): VertexBuffer { - return loadOBJMeshData(lines).toVertexBuffer() -} - -fun loadOBJ(file: File) = loadOBJ(file.readLines()) -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): Map> = loadOBJMeshData(lines).triangulate().flattenPolygons() - +import org.openrndr.math.Vector2 +import org.openrndr.math.Vector3 -fun loadOBJMeshData(file: File) = loadOBJMeshData(file.readLines()) -fun loadOBJMeshData(lines: List): CompoundMeshData { +fun readObjMeshData(lines: Iterable): CompoundMeshData { val meshes = mutableMapOf>() val positions = mutableListOf() val normals = mutableListOf() + val tangents = mutableListOf() + val bitangents = mutableListOf() val textureCoords = mutableListOf() val colors = mutableListOf() var activeMesh = mutableListOf() @@ -66,7 +25,6 @@ fun loadOBJMeshData(lines: List): CompoundMeshData { when (tokens.size) { 3, 4 -> { positions += Vector3(tokens[1].toDouble(), tokens[2].toDouble(), tokens[3].toDouble()) - colors += ColorRGBa.WHITE } 6 -> { @@ -88,7 +46,18 @@ fun loadOBJMeshData(lines: List): CompoundMeshData { } } - "vn" -> normals += Vector3(tokens[1].toDouble(), tokens[2].toDouble(), tokens[3].toDouble()) + "vn" -> { + when (tokens.size) { + 3, 4 -> normals += Vector3(tokens[1].toDouble(), tokens[2].toDouble(), tokens[3].toDouble()) + 9 -> { + normals += Vector3(tokens[1].toDouble(), tokens[2].toDouble(), tokens[3].toDouble()) + tangents += Vector3(tokens[4].toDouble(), tokens[5].toDouble(), tokens[6].toDouble()) + bitangents += Vector3(tokens[7].toDouble(), tokens[8].toDouble(), tokens[9].toDouble()) + } + + } + } + "vt" -> textureCoords += Vector2(tokens[1].toDouble(), tokens[2].toDouble()) "g" -> { activeMesh = mutableListOf() @@ -102,7 +71,9 @@ fun loadOBJMeshData(lines: List): CompoundMeshData { 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 + val hasColor = colors.isNotEmpty() + val hasTangents = tangents.isNotEmpty() + val hasBitangents = bitangents.isNotEmpty() activeMesh.add( IndexedPolygon( @@ -110,8 +81,8 @@ fun loadOBJMeshData(lines: List): CompoundMeshData { 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() + if (hasTangents) indices.map { it[2] - 1 } else listOf(), + if (hasBitangents) indices.map { it[2] - 1 } else listOf() ) ) @@ -132,3 +103,10 @@ fun loadOBJMeshData(lines: List): CompoundMeshData { } ) } + +fun loadOBJasVertexBuffer(lines: List): VertexBuffer { + return readObjMeshData(lines).toVertexBuffer() +} + +fun loadOBJ(lines: List): Map> = readObjMeshData(lines).triangulate().toPolygons() + diff --git a/orx-obj-loader/src/commonMain/kotlin/ObjWriter.kt b/orx-obj-loader/src/commonMain/kotlin/ObjWriter.kt new file mode 100644 index 000000000..2f9181fea --- /dev/null +++ b/orx-obj-loader/src/commonMain/kotlin/ObjWriter.kt @@ -0,0 +1,75 @@ +package org.openrndr.extra.objloader + +/** + * Convert mesh data to Wavefront OBJ representation + * @param allowNonStandardBehavior if true non-standard encoding of color and tangent data is allowed + */ +fun ICompoundMeshData.toObj(allowNonStandardBehavior: Boolean = true): String { + val sb = StringBuilder() + + require(compounds.values.all { it.vertexData == vertexData }) { + "compounds do not share vertex data" + } + + /* + Output positions + */ + if (vertexData.colors.isEmpty()) { + for (p in vertexData.positions) { + sb.appendLine("v ${p.x} ${p.y} ${p.z}") + } + } else { + require(vertexData.positions.size == vertexData.colors.size) { + "position and color data do not align" + } + for (pc in vertexData.positions.zip(vertexData.colors)) { + sb.appendLine("v ${pc.first.x} ${pc.first.y} ${pc.first.z} ${pc.second.r} ${pc.second.g} ${pc.second.b} ${pc.second.alpha}") + } + } + + /* + Output normals. Non-standard behavior where normal-tangent-bitangent is emitted as `vn` + */ + if (!allowNonStandardBehavior || vertexData.tangents.isEmpty() || vertexData.bitangents.isEmpty()) { + for (n in vertexData.normals) { + sb.appendLine("vn ${n.x} ${n.y} ${n.z}") + } + } else { + for (i in vertexData.normals.indices) { + val n = vertexData.normals[i] + val t = vertexData.tangents[i] + val b = vertexData.bitangents[i] + sb.appendLine("vn ${n.x} ${n.y} ${n.z} ${t.x} ${t.y} ${t.z} ${b.x} ${b.y} ${b.z}") + } + } + + /* + Output texture coordinates + */ + for (t in vertexData.textureCoords) { + sb.appendLine("vt ${t.x} ${t.y}") + } + + /* + Output compounds + */ + for (g in compounds) { + sb.appendLine("g ${g.key}") + + /* + Output polygons + */ + for (p in g.value.polygons) { + sb.appendLine("f ${ + (0 until p.positions.size).joinToString(" ") { i -> + listOf( + p.positions.getOrNull(i)?.plus(1)?.toString() ?: "", + p.textureCoords.getOrNull(i)?.plus(1)?.toString() ?: "", + p.normals.getOrNull(i)?.plus(1)?.toString() ?: "" + ).joinToString("/") + } + }") + } + } + return sb.toString() +} \ No newline at end of file diff --git a/orx-obj-loader/src/jvmDemo/kotlin/DemoObjCompoundRW01.kt b/orx-obj-loader/src/jvmDemo/kotlin/DemoObjCompoundRW01.kt new file mode 100644 index 000000000..79701f22b --- /dev/null +++ b/orx-obj-loader/src/jvmDemo/kotlin/DemoObjCompoundRW01.kt @@ -0,0 +1,15 @@ +import org.openrndr.application +import org.openrndr.extra.objloader.loadOBJMeshData +import org.openrndr.extra.objloader.toObj +import java.io.File + +fun main() { + application { + program { + val path = "demo-data/obj-models" + val cm = loadOBJMeshData(File("$path/suzanne/Suzanne.obj")) + + println(cm.toObj()) + } + } +} \ No newline at end of file diff --git a/orx-obj-loader/src/demo/kotlin/DemoObjLoader01.kt b/orx-obj-loader/src/jvmDemo/kotlin/DemoObjLoader01.kt similarity index 100% rename from orx-obj-loader/src/demo/kotlin/DemoObjLoader01.kt rename to orx-obj-loader/src/jvmDemo/kotlin/DemoObjLoader01.kt diff --git a/orx-obj-loader/src/demo/kotlin/DemoObjSaver01.kt b/orx-obj-loader/src/jvmDemo/kotlin/DemoObjSaver01.kt similarity index 100% rename from orx-obj-loader/src/demo/kotlin/DemoObjSaver01.kt rename to orx-obj-loader/src/jvmDemo/kotlin/DemoObjSaver01.kt diff --git a/orx-obj-loader/src/demo/kotlin/DemoObjSaver02.kt b/orx-obj-loader/src/jvmDemo/kotlin/DemoObjSaver02.kt similarity index 100% rename from orx-obj-loader/src/demo/kotlin/DemoObjSaver02.kt rename to orx-obj-loader/src/jvmDemo/kotlin/DemoObjSaver02.kt diff --git a/orx-obj-loader/src/demo/kotlin/DemoWireframe01.kt b/orx-obj-loader/src/jvmDemo/kotlin/DemoWireframe01.kt similarity index 94% rename from orx-obj-loader/src/demo/kotlin/DemoWireframe01.kt rename to orx-obj-loader/src/jvmDemo/kotlin/DemoWireframe01.kt index 9cf4bc7e9..5f0d5e8e7 100644 --- a/orx-obj-loader/src/demo/kotlin/DemoWireframe01.kt +++ b/orx-obj-loader/src/jvmDemo/kotlin/DemoWireframe01.kt @@ -8,7 +8,7 @@ import org.openrndr.draw.DrawPrimitive import org.openrndr.draw.TransformTarget import org.openrndr.draw.shadeStyle import org.openrndr.extra.camera.Orbital -import org.openrndr.extra.objloader.loadOBJMeshData +import org.openrndr.extra.objloader.readObjMeshData import org.openrndr.extra.objloader.loadOBJasVertexBuffer import org.openrndr.extra.objloader.wireframe import org.openrndr.math.Vector3 @@ -25,7 +25,7 @@ fun main() { } program { val vb = loadOBJasVertexBuffer("orx-obj-loader/test-data/non-planar.obj") - val md = loadOBJMeshData(File("orx-obj-loader/test-data/non-planar.obj").readLines()) + val md = readObjMeshData(File("orx-obj-loader/test-data/non-planar.obj").readLines()) val paths = md.wireframe().map { Path3D.fromPoints(it, true) diff --git a/orx-obj-loader/src/jvmMain/kotlin/OBJLoader.kt b/orx-obj-loader/src/jvmMain/kotlin/OBJLoader.kt new file mode 100644 index 000000000..f0acc8af0 --- /dev/null +++ b/orx-obj-loader/src/jvmMain/kotlin/OBJLoader.kt @@ -0,0 +1,44 @@ +package org.openrndr.extra.objloader + +import org.openrndr.draw.VertexBuffer +import java.io.File +import java.net.MalformedURLException +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> { + return try { + val url = URL(fileOrUrl) + loadOBJ(url) + } catch (e: MalformedURLException) { + loadOBJ(File(fileOrUrl)) + } +} + +/** + * Loads an OBJ file as a [VertexBuffer]. + * Use this method to render / process the loaded OBJ data using the GPU. + */ +fun loadOBJasVertexBuffer(fileOrUrl: String): VertexBuffer { + return try { + val url = URL(fileOrUrl) + loadOBJasVertexBuffer(url) + } catch (e: MalformedURLException) { + loadOBJasVertexBuffer(File(fileOrUrl)) + } +} + +fun loadOBJasVertexBuffer(url: URL): VertexBuffer = loadOBJasVertexBuffer(url.readText().split("\n")) +fun loadOBJasVertexBuffer(file: File): VertexBuffer = loadOBJasVertexBuffer(file.readLines()) + +fun loadOBJ(file: File) = loadOBJ(file.readLines()) +fun loadOBJEx(file: File) = readObjMeshData(file.readLines()) +fun loadOBJ(url: URL) = loadOBJ(url.readText().split("\n")) +fun loadOBJEx(url: URL) = readObjMeshData(url.readText().split("\n")) + + + +fun loadOBJMeshData(file: File) = readObjMeshData(file.readLines()) diff --git a/orx-obj-loader/src/main/kotlin/OBJSaver.kt b/orx-obj-loader/src/jvmMain/kotlin/OBJSaver.kt similarity index 99% rename from orx-obj-loader/src/main/kotlin/OBJSaver.kt rename to orx-obj-loader/src/jvmMain/kotlin/OBJSaver.kt index 252be3230..4597b6429 100644 --- a/orx-obj-loader/src/main/kotlin/OBJSaver.kt +++ b/orx-obj-loader/src/jvmMain/kotlin/OBJSaver.kt @@ -47,6 +47,7 @@ fun VertexBuffer.saveOBJ(filePath: String) { val bb = ByteBuffer.allocateDirect(vertexCount * vertexFormat.size) bb.order(ByteOrder.nativeOrder()) read(bb) + bb.rewind() val tokens = mapOf( "position" to "v",