[orx-obj-loader] Add obj saver, demos (#348)
This commit is contained in:
2
demo-data/obj-models/suzanne/Suzanne.mtl
Normal file
2
demo-data/obj-models/suzanne/Suzanne.mtl
Normal file
@@ -0,0 +1,2 @@
|
||||
# Blender 4.1.1 MTL File: 'None'
|
||||
# www.blender.org
|
||||
2066
demo-data/obj-models/suzanne/Suzanne.obj
Normal file
2066
demo-data/obj-models/suzanne/Suzanne.obj
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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)
|
||||
|
||||

|
||||
|
||||
### DemoObjSaver01
|
||||
[source code](src/demo/kotlin/DemoObjSaver01.kt)
|
||||
|
||||

|
||||
|
||||
### DemoObjSaver02
|
||||
[source code](src/demo/kotlin/DemoObjSaver02.kt)
|
||||
|
||||

|
||||
|
||||
### DemoWireframe01
|
||||
[source code](src/demo/kotlin/DemoWireframe01.kt)
|
||||
|
||||
|
||||
@@ -7,4 +7,5 @@ dependencies {
|
||||
implementation(libs.openrndr.math)
|
||||
implementation(libs.openrndr.ffmpeg)
|
||||
demoImplementation(project(":orx-camera"))
|
||||
demoImplementation(project(":orx-mesh-generators"))
|
||||
}
|
||||
31
orx-obj-loader/src/demo/kotlin/DemoObjLoader01.kt
Normal file
31
orx-obj-loader/src/demo/kotlin/DemoObjLoader01.kt
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
22
orx-obj-loader/src/demo/kotlin/DemoObjSaver01.kt
Normal file
22
orx-obj-loader/src/demo/kotlin/DemoObjSaver01.kt
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
35
orx-obj-loader/src/demo/kotlin/DemoObjSaver02.kt
Normal file
35
orx-obj-loader/src/demo/kotlin/DemoObjSaver02.kt
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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" -> {
|
||||
|
||||
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