[orx-obj-loader] Add obj saver, demos (#348)

This commit is contained in:
Abe Pazos
2024-07-18 10:23:32 +02:00
committed by GitHub
parent 411f7ffc54
commit 556794b634
11 changed files with 2312 additions and 14 deletions

View File

@@ -0,0 +1,2 @@
# Blender 4.1.1 MTL File: 'None'
# www.blender.org

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
# orx-obj-loader
Simple loader for Wavefront .obj 3D mesh files.
Simple loader and saver for Wavefront .obj 3D mesh files.
##### Usage
@@ -11,13 +11,34 @@ val vertexBuffer = loadOBJasVertexBuffer("data/someObject.obj")
```
The loaded vertex buffer can be drawn like this:
```kotlin
drawer.vertexBuffer(vertexBuffer, DrawPrimitive.TRIANGLES)
```
To save a vertex buffer as an .obj file:
```kotlin
vertexBuffer.saveOBJ("my/path/exported.obj")
```
<!-- __demos__ -->
## Demos
### DemoObjLoader01
[source code](src/demo/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)
![DemoObjSaver01Kt](https://raw.githubusercontent.com/openrndr/orx/media/orx-obj-loader/images/DemoObjSaver01Kt.png)
### DemoObjSaver02
[source code](src/demo/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)

View File

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

View File

@@ -0,0 +1,31 @@
import org.openrndr.application
import org.openrndr.color.ColorRGBa
import org.openrndr.draw.DepthTestPass
import org.openrndr.draw.DrawPrimitive
import org.openrndr.draw.shadeStyle
import org.openrndr.extra.objloader.loadOBJasVertexBuffer
import org.openrndr.math.Vector3
fun main() = application {
program {
val mesh = loadOBJasVertexBuffer("demo-data/obj-models/suzanne/Suzanne.obj")
extend {
drawer.perspective(60.0, width * 1.0 / height, 0.01, 1000.0)
drawer.depthWrite = true
drawer.depthTestPass = DepthTestPass.LESS_OR_EQUAL
drawer.fill = ColorRGBa.PINK
drawer.shadeStyle = shadeStyle {
fragmentTransform = """
vec3 lightDir = normalize(vec3(0.3, 1.0, 0.5));
float l = dot(va_normal, lightDir) * 0.4 + 0.5;
x_fill.rgb *= l;
""".trimIndent()
}
drawer.translate(0.0, 0.0, -2.0)
drawer.rotate(Vector3.UNIT_X, -seconds * 2 + 30)
drawer.rotate(Vector3.UNIT_Y, -seconds * 15 + 20)
drawer.vertexBuffer(mesh, DrawPrimitive.TRIANGLES)
}
}
}

View File

@@ -0,0 +1,22 @@
import org.openrndr.application
import org.openrndr.draw.loadFont
import org.openrndr.extra.objloader.loadOBJasVertexBuffer
import org.openrndr.extra.objloader.saveOBJ
fun main() = application {
configure {
height = 100
}
program {
val path = "demo-data/obj-models"
val mesh = loadOBJasVertexBuffer("$path/suzanne/Suzanne.obj")
mesh.saveOBJ("$path/Suzanne-exported.obj")
val font = loadFont("demo-data/fonts/IBMPlexMono-Regular.ttf", 24.0)
extend {
drawer.fontMap = font
drawer.text(".obj file loaded and saved", 10.0, 80.0)
}
}
}

View File

@@ -0,0 +1,35 @@
import org.openrndr.application
import org.openrndr.draw.loadFont
import org.openrndr.extra.meshgenerators.buildTriangleMesh
import org.openrndr.extra.meshgenerators.sphere
import org.openrndr.extra.objloader.saveOBJ
fun main() = application {
configure {
height = 100
}
program {
val path = "demo-data/obj-models/"
val mesh = buildTriangleMesh {
repeat(4) { x ->
repeat(4) { y ->
repeat(4) { z ->
isolated {
translate(x * 1.0, y * 1.0, z * 1.0)
sphere(8, 8,
(x * 91 + y * 79 + z * 17).mod(5) * 0.2 + 0.1)
}
}
}
}
}
mesh.saveOBJ("$path/sphere-composition-exported.obj")
val font = loadFont("demo-data/fonts/IBMPlexMono-Regular.ttf", 24.0)
extend {
drawer.fontMap = font
drawer.text("Mesh generated and .obj file saved", 10.0, 80.0)
}
}
}

View File

@@ -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 }

View File

@@ -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
)
}
}

View 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]}")
}
}
}
}

View File

@@ -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)