[orx-obj-loader] Add obj saver, demos (#348)
This commit is contained in:
@@ -1,16 +1,23 @@
|
||||
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 }
|
||||
|
||||
@@ -6,6 +6,10 @@ 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<String, List<Polygon>> {
|
||||
return try {
|
||||
val url = URL(fileOrUrl)
|
||||
@@ -15,6 +19,10 @@ fun loadOBJ(fileOrUrl: String): Map<String, List<Polygon>> {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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)
|
||||
@@ -52,10 +60,7 @@ 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" -> 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" -> {
|
||||
@@ -92,4 +97,4 @@ fun loadOBJMeshData(lines: List<String>): MeshData {
|
||||
VertexData(positions.toTypedArray(), normals.toTypedArray(), textureCoords.toTypedArray()),
|
||||
meshes
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
94
orx-obj-loader/src/main/kotlin/OBJSaver.kt
Normal file
94
orx-obj-loader/src/main/kotlin/OBJSaver.kt
Normal file
@@ -0,0 +1,94 @@
|
||||
package org.openrndr.extra.objloader
|
||||
|
||||
import org.openrndr.draw.VertexBuffer
|
||||
import java.io.File
|
||||
import java.nio.ByteBuffer
|
||||
import java.nio.ByteOrder
|
||||
|
||||
/**
|
||||
* An Ordered Map
|
||||
* containing Vec2 / Vec3 String representations.
|
||||
* Ensures no strings are duplicates by using a Map
|
||||
* and maintains their order by using a List.
|
||||
*/
|
||||
private class UniqueCoords {
|
||||
// Map pointing unique strings to their index.
|
||||
// We could just use `coords.indexOf(floats)` but using a map is faster.
|
||||
private val indices = mutableMapOf<String, Int>()
|
||||
|
||||
// List containing unique Strings
|
||||
private val coords = mutableListOf<String>()
|
||||
|
||||
/**
|
||||
* Adds strings only if they are not found yet in `coords`.
|
||||
* Returns the index of the received argument inside `coords`.
|
||||
*/
|
||||
fun add(floats: String): Int {
|
||||
val index = indices[floats]
|
||||
return if (index == null) {
|
||||
coords.add(floats)
|
||||
val newIndex = coords.size
|
||||
indices[floats] = newIndex
|
||||
newIndex
|
||||
} else index
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a valid .obj block representing `coords`.
|
||||
*/
|
||||
fun toObjBlock(token: String) = coords.joinToString("\n$token ", "$token ", "\n")
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves a VertexBuffer to a Wavefront OBJ file.
|
||||
* Faces use indices. Vertices, normals and texture coordinates are deduplicated.
|
||||
*/
|
||||
fun VertexBuffer.saveOBJ(filePath: String) {
|
||||
val bb = ByteBuffer.allocateDirect(vertexCount * vertexFormat.size)
|
||||
bb.order(ByteOrder.nativeOrder())
|
||||
read(bb)
|
||||
|
||||
val tokens = mapOf(
|
||||
"position" to "v",
|
||||
"normal" to "vn",
|
||||
"texCoord0" to "vt"
|
||||
)
|
||||
|
||||
val indexMap = tokens.values.associateWith { UniqueCoords() }
|
||||
val lastIndices = tokens.values.associateWith { 0 }.toMutableMap()
|
||||
val vertexIndices = mutableListOf<String>()
|
||||
|
||||
// Process the ByteBuffer and populate data structures
|
||||
while (bb.position() < bb.capacity()) {
|
||||
vertexFormat.items.forEach { vertexElement ->
|
||||
val floats = List(vertexElement.type.componentCount) {
|
||||
bb.getFloat()
|
||||
}.joinToString(" ")
|
||||
val token = tokens[vertexElement.attribute]
|
||||
if (token != null) {
|
||||
lastIndices[token] = indexMap[token]!!.add(floats)
|
||||
}
|
||||
}
|
||||
vertexIndices.add("${lastIndices["v"]}/${lastIndices["vt"]}/${lastIndices["vn"]}")
|
||||
}
|
||||
|
||||
val f = File(filePath)
|
||||
f.bufferedWriter().use { writer ->
|
||||
writer.run {
|
||||
// Write header
|
||||
appendLine("# OPENRNDR")
|
||||
appendLine("# www.openrndr.org")
|
||||
appendLine("o " + f.name)
|
||||
|
||||
// Write v, vt, vn blocks
|
||||
indexMap.forEach { (token, verts) ->
|
||||
appendLine(verts.toObjBlock(token))
|
||||
|
||||
}
|
||||
// Write faces processing three vertices at a time
|
||||
vertexIndices.chunked(3) {
|
||||
appendLine("f ${it[0]} ${it[1]} ${it[2]}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,14 @@ 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(),
|
||||
@@ -16,8 +24,14 @@ class Polygon(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
@@ -28,14 +42,14 @@ fun bounds(polygons: List<Polygon>): Box {
|
||||
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)
|
||||
it.positions.forEach { pos ->
|
||||
minX = min(minX, pos.x)
|
||||
minY = min(minY, pos.y)
|
||||
minZ = min(minZ, pos.z)
|
||||
|
||||
maxX = max(maxX, it.x)
|
||||
maxY = max(maxY, it.y)
|
||||
maxZ = max(maxZ, it.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)
|
||||
|
||||
Reference in New Issue
Block a user