[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
|
# orx-obj-loader
|
||||||
|
|
||||||
Simple loader for Wavefront .obj 3D mesh files.
|
Simple loader and saver for Wavefront .obj 3D mesh files.
|
||||||
|
|
||||||
##### Usage
|
##### Usage
|
||||||
|
|
||||||
@@ -11,13 +11,34 @@ val vertexBuffer = loadOBJasVertexBuffer("data/someObject.obj")
|
|||||||
```
|
```
|
||||||
|
|
||||||
The loaded vertex buffer can be drawn like this:
|
The loaded vertex buffer can be drawn like this:
|
||||||
|
|
||||||
```kotlin
|
```kotlin
|
||||||
drawer.vertexBuffer(vertexBuffer, DrawPrimitive.TRIANGLES)
|
drawer.vertexBuffer(vertexBuffer, DrawPrimitive.TRIANGLES)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
To save a vertex buffer as an .obj file:
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
vertexBuffer.saveOBJ("my/path/exported.obj")
|
||||||
|
```
|
||||||
|
|
||||||
<!-- __demos__ -->
|
<!-- __demos__ -->
|
||||||
## 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
|
### DemoWireframe01
|
||||||
[source code](src/demo/kotlin/DemoWireframe01.kt)
|
[source code](src/demo/kotlin/DemoWireframe01.kt)
|
||||||
|
|
||||||
|
|||||||
@@ -7,4 +7,5 @@ dependencies {
|
|||||||
implementation(libs.openrndr.math)
|
implementation(libs.openrndr.math)
|
||||||
implementation(libs.openrndr.ffmpeg)
|
implementation(libs.openrndr.ffmpeg)
|
||||||
demoImplementation(project(":orx-camera"))
|
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
|
package org.openrndr.extra.objloader
|
||||||
|
|
||||||
import org.openrndr.draw.VertexBuffer
|
import org.openrndr.draw.VertexBuffer
|
||||||
|
import org.openrndr.draw.VertexFormat
|
||||||
import org.openrndr.draw.vertexBuffer
|
import org.openrndr.draw.vertexBuffer
|
||||||
import org.openrndr.draw.vertexFormat
|
import org.openrndr.draw.vertexFormat
|
||||||
import org.openrndr.math.Vector2
|
import org.openrndr.math.Vector2
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The [VertexFormat] for a [VertexBuffer] with positions, normals and texture coordinates.
|
||||||
|
*/
|
||||||
private val objVertexFormat = vertexFormat {
|
private val objVertexFormat = vertexFormat {
|
||||||
position(3)
|
position(3)
|
||||||
normal(3)
|
normal(3)
|
||||||
textureCoordinate(2)
|
textureCoordinate(2)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a [MeshData] instance into a [VertexBuffer]
|
||||||
|
*/
|
||||||
fun MeshData.toVertexBuffer() : VertexBuffer {
|
fun MeshData.toVertexBuffer() : VertexBuffer {
|
||||||
val objects = triangulate().flattenPolygons()
|
val objects = triangulate().flattenPolygons()
|
||||||
val triangleCount = objects.values.sumOf { it.size }
|
val triangleCount = objects.values.sumOf { it.size }
|
||||||
|
|||||||
@@ -6,6 +6,10 @@ import java.io.File
|
|||||||
import java.net.MalformedURLException
|
import java.net.MalformedURLException
|
||||||
import java.net.URL
|
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>> {
|
fun loadOBJ(fileOrUrl: String): Map<String, List<Polygon>> {
|
||||||
return try {
|
return try {
|
||||||
val url = URL(fileOrUrl)
|
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 {
|
fun loadOBJasVertexBuffer(fileOrUrl: String): VertexBuffer {
|
||||||
return try {
|
return try {
|
||||||
val url = URL(fileOrUrl)
|
val url = URL(fileOrUrl)
|
||||||
@@ -52,10 +60,7 @@ fun loadOBJMeshData(lines: List<String>): MeshData {
|
|||||||
|
|
||||||
if (tokens.isNotEmpty()) {
|
if (tokens.isNotEmpty()) {
|
||||||
when (tokens[0]) {
|
when (tokens[0]) {
|
||||||
"v" -> {
|
"v" -> positions += Vector3(tokens[1].toDouble(), tokens[2].toDouble(), tokens[3].toDouble())
|
||||||
positions += Vector3(tokens[1].toDouble(), tokens[2].toDouble(), tokens[3].toDouble())
|
|
||||||
}
|
|
||||||
|
|
||||||
"vn" -> normals += 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())
|
"vt" -> textureCoords += Vector2(tokens[1].toDouble(), tokens[2].toDouble())
|
||||||
"g" -> {
|
"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.max
|
||||||
import kotlin.math.min
|
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(
|
class Polygon(
|
||||||
val positions: Array<Vector3> = emptyArray(),
|
val positions: Array<Vector3> = emptyArray(),
|
||||||
val normals: 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)
|
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 {
|
fun bounds(polygons: List<Polygon>): Box {
|
||||||
var minX = Double.POSITIVE_INFINITY
|
var minX = Double.POSITIVE_INFINITY
|
||||||
var minY = Double.POSITIVE_INFINITY
|
var minY = Double.POSITIVE_INFINITY
|
||||||
@@ -28,14 +42,14 @@ fun bounds(polygons: List<Polygon>): Box {
|
|||||||
var maxZ = Double.NEGATIVE_INFINITY
|
var maxZ = Double.NEGATIVE_INFINITY
|
||||||
|
|
||||||
polygons.forEach {
|
polygons.forEach {
|
||||||
it.positions.forEach {
|
it.positions.forEach { pos ->
|
||||||
minX = min(minX, it.x)
|
minX = min(minX, pos.x)
|
||||||
minY = min(minY, it.y)
|
minY = min(minY, pos.y)
|
||||||
minZ = min(minZ, it.z)
|
minZ = min(minZ, pos.z)
|
||||||
|
|
||||||
maxX = max(maxX, it.x)
|
maxX = max(maxX, pos.x)
|
||||||
maxY = max(maxY, it.y)
|
maxY = max(maxY, pos.y)
|
||||||
maxZ = max(maxZ, it.z)
|
maxZ = max(maxZ, pos.z)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return Box(Vector3(minX, minY, minZ), maxX - minX, maxY - minY, maxZ - minZ)
|
return Box(Vector3(minX, minY, minZ), maxX - minX, maxY - minY, maxZ - minZ)
|
||||||
|
|||||||
Reference in New Issue
Block a user