[orx-mesh, orx-mesh-generator, orx-obj-loader] Add decal and tangent tools

This commit is contained in:
Edwin Jakobs
2024-09-25 09:51:46 +02:00
parent fb3bb6f7a6
commit e016891b1d
28 changed files with 1072 additions and 82 deletions

View File

@@ -10,6 +10,7 @@ kotlin {
api(libs.openrndr.application)
api(libs.openrndr.math)
implementation(project(":orx-shapes"))
api(project(":orx-mesh"))
}
}
@@ -20,6 +21,7 @@ kotlin {
implementation(project(":orx-mesh-generators"))
implementation(project(":orx-camera"))
implementation(project(":orx-noise"))
implementation(project(":orx-obj-loader"))
}
}
}

View File

@@ -0,0 +1,201 @@
package org.openrndr.extra.meshgenerators.decal
import org.openrndr.extra.mesh.*
import org.openrndr.math.Matrix44
import org.openrndr.math.Vector2
import org.openrndr.math.Vector3
import kotlin.math.abs
/**
* Create a decal mesh
* @param projectorMatrix
* @param size
*/
fun IMeshData.decal(
projectorMatrix: Matrix44,
size: Vector3
): IVertexData {
require(isTriangular())
val projectorMatrixInverse = projectorMatrix.inversed
val positions = vertexData.positions.slice(polygons.flatMap { it.positions }).map {
(projectorMatrixInverse * (it.xyz1)).div
}
val normals = vertexData.normals.slice(polygons.flatMap { it.normals })
val textureCoords = vertexData.textureCoords.slice(polygons.flatMap { it.textureCoords })
val colors = vertexData.colors.slice(polygons.flatMap { it.colors })
val tangents = vertexData.tangents.slice(polygons.flatMap { it.tangents })
val bitangents = vertexData.bitangents.slice(polygons.flatMap { it.bitangents })
var decalVertices: IVertexData = VertexData(positions, textureCoords, colors, normals, tangents, bitangents)
decalVertices = decalVertices.clipToPlane(size, Vector3(1.0, 0.0, 0.0))
decalVertices = decalVertices.clipToPlane(size, Vector3(-1.0, 0.0, 0.0))
decalVertices = decalVertices.clipToPlane(size, Vector3(0.0, 1.0, 0.0))
decalVertices = decalVertices.clipToPlane(size, Vector3(0.0, -1.0, 0.0))
decalVertices = decalVertices.clipToPlane(size, Vector3(0.0, 0.0, 1.0))
decalVertices = decalVertices.clipToPlane(size, Vector3(0.0, 0.0, -1.0))
val decalMesh = MutableVertexData()
for (i in decalVertices.positions.indices) {
val v = decalVertices[i]
val w = v.copy(
position = (projectorMatrix * v.position.xyz1).div,
textureCoord = v.position.xy / size.xy + Vector2(0.5)
)
decalMesh.add(w)
}
return decalMesh
}
fun IVertexData.clipToPlane(
size: Vector3,
plane: Vector3
): IVertexData {
val outVertices = MutableVertexData()
val s = 0.5 * abs(size.dot(plane))
fun clip(
v0: Point,
v1: Point, p: Vector3, s: Double
): Point {
val d0 = v0.position.dot(p) - s;
val d1 = v1.position.dot(p) - s;
val s0 = d0 / (d0 - d1)
val v = Point(
v0.position + (v1.position - v0.position) * s0,
if (v0.textureCoord != null) {
v0.textureCoord!! + (v1.textureCoord!! - v0.textureCoord!!) * s0
} else {
null
},
if (v0.color != null) {
v0.color!! + (v1.color!! - v0.color!!) * s0
} else {
null
},
if (v0.normal != null) {
v0.normal!! + (v1.normal!! - v0.normal!!) * s0
} else {
null
},
if (v0.tangent != null) {
v0.tangent!! + (v1.tangent!! - v0.tangent!!) * s0
} else {
null
},
if (v0.bitangent != null) {
v0.bitangent!! + (v1.bitangent!! - v0.bitangent!!) * s0
} else {
null
}
)
return v
}
for (i in positions.indices step 3) {
val d1 = positions[i + 0].dot(plane) - s
val d2 = positions[i + 1].dot(plane) - s
val d3 = positions[i + 2].dot(plane) - s
val v1Out = d1 > 0
val v2Out = d2 > 0
val v3Out = d3 > 0
val total = (if (v1Out) 1 else 0) + (if (v2Out) 1 else 0) + (if (v3Out) 1 else 0)
when (total) {
0 -> {
outVertices.add(this[i])
outVertices.add(this[i + 1])
outVertices.add(this[i + 2])
}
1 -> {
if (v1Out) {
val nV1 = this[i + 1]
val nV2 = this[i + 2]
val nV3 = clip(this[i], nV1, plane, s)
val nV4 = clip(this[i], nV2, plane, s)
outVertices.add(nV1)
outVertices.add(nV2)
outVertices.add(nV3)
outVertices.add(nV4)
outVertices.add(nV3)
outVertices.add(nV2)
}
if (v2Out) {
val nV1 = this[i];
val nV2 = this[i + 2];
val nV3 = clip(this[i + 1], nV1, plane, s)
val nV4 = clip(this[i + 1], nV2, plane, s)
outVertices.add(nV3)
outVertices.add(nV2)
outVertices.add(nV1)
outVertices.add(nV2)
outVertices.add(nV3)
outVertices.add(nV4)
}
if (v3Out) {
val nV1 = this[i]
val nV2 = this[i + 1]
val nV3 = clip(this[i + 2], nV1, plane, s)
val nV4 = clip(this[i + 2], nV2, plane, s)
outVertices.add(nV1)
outVertices.add(nV2)
outVertices.add(nV3)
outVertices.add(nV4)
outVertices.add(nV3)
outVertices.add(nV2)
}
}
2 -> {
if (!v1Out) {
val nV1 = this[i]
val nV2 = clip(nV1, this[i + 1], plane, s)
val nV3 = clip(nV1, this[i + 2], plane, s)
outVertices.add(nV1)
outVertices.add(nV2)
outVertices.add(nV3)
}
if (!v2Out) {
val nV1 = this[i + 1]
val nV2 = clip(nV1, this[i + 2], plane, s)
val nV3 = clip(nV1, this[i], plane, s)
outVertices.add(nV1)
outVertices.add(nV2)
outVertices.add(nV3)
}
if (!v3Out) {
val nV1 = this[i + 2]
val nV2 = clip(nV1, this[i], plane, s)
val nV3 = clip(nV1, this[i + 1], plane, s)
outVertices.add(nV1)
outVertices.add(nV2)
outVertices.add(nV3)
}
}
else -> {
}
}
}
return outVertices
}

View File

@@ -0,0 +1,63 @@
package org.openrndr.extra.meshgenerators.normals
import org.openrndr.extra.mesh.IMeshData
import org.openrndr.extra.mesh.IndexedPolygon
import org.openrndr.extra.mesh.MeshData
import org.openrndr.extra.mesh.VertexData
import org.openrndr.math.Vector3
/**
* Estimate per-vertex normals
*/
fun IMeshData.estimateNormals(): MeshData {
val normals = MutableList(vertexData.positions.size) { Vector3.ZERO }
for (polygon in polygons) {
for (p in polygon.positions) {
normals[p] += polygon.normal(vertexData)
}
}
for (i in normals.indices) {
normals[i] = normals[i].normalized
}
return MeshData(
VertexData(
vertexData.positions,
vertexData.textureCoords,
vertexData.colors,
normals,
vertexData.tangents,
vertexData.bitangents
),
polygons.map {
IndexedPolygon(it.positions, it.textureCoords, it.colors, it.positions, it.tangents, it.bitangents)
})
}
/**
* Assign vertex normals based on face normals
*/
fun IMeshData.assignFaceNormals(): MeshData {
val normals = MutableList(polygons.size) { Vector3.ZERO }
for (i in polygons.indices) {
normals[i] = polygons[i].normal(vertexData)
}
return MeshData(
VertexData(
vertexData.positions,
vertexData.textureCoords,
vertexData.colors,
normals,
vertexData.tangents,
vertexData.bitangents
),
polygons.mapIndexed { index, it ->
IndexedPolygon(it.positions, it.textureCoords, it.colors, it.positions.map {
index
}, it.tangents, it.bitangents)
})
}

View File

@@ -0,0 +1,94 @@
package org.openrndr.extra.meshgenerators.tangents
import org.openrndr.extra.mesh.*
import org.openrndr.math.Vector3
/**
* Estimate tangents from normals and texture coordinates
* https://terathon.com/blog/tangent-space.html
*/
fun IMeshData.estimateTangents(): MeshData {
require(vertexData.textureCoords.isNotEmpty()) {
"need texture coordinates to estimate tangents"
}
require(isTriangular()) {
}
val normals = MutableList(vertexData.positions.size) { Vector3.ZERO }
val tan1 = MutableList(vertexData.positions.size) { Vector3.ZERO }
val tan2 = MutableList(vertexData.positions.size) { Vector3.ZERO }
for (polygon in polygons) {
val v1 = vertexData.positions[polygon.positions[0]]
val v2 = vertexData.positions[polygon.positions[1]]
val v3 = vertexData.positions[polygon.positions[2]]
val w1 = vertexData.textureCoords[polygon.textureCoords[0]]
val w2 = vertexData.textureCoords[polygon.textureCoords[1]]
val w3 = vertexData.textureCoords[polygon.textureCoords[2]]
val x1 = (v2.x - v1.x)
val x2 = (v3.x - v1.x)
val y1 = (v2.y - v1.y)
val y2 = (v3.y - v1.y)
val z1 = (v2.z - v1.z)
val z2 = (v3.z - v1.z)
val s1 = (w2.x - w1.x)
val s2 = (w3.x - w1.x)
val t1 = (w2.y - w1.y)
val t2 = (w3.y - w1.y)
var det = s1 * t2 - s2 * t1
if (det == 0.0) det = 1.0
val r = 1.0/ (det)
val sdir = Vector3(
(t2 * x1 - t1 * x2) * r, (t2 * y1 - t1 * y2) * r,
(t2 * z1 - t1 * z2) * r
).normalized
val tdir = Vector3(
(s1 * x2 - s2 * x1) * r, (s1 * y2 - s2 * y1) * r,
(s1 * z2 - s2 * z1) * r
).normalized
tan1[polygon.positions[0]] += sdir
tan1[polygon.positions[1]] += sdir
tan1[polygon.positions[2]] += sdir
tan2[polygon.positions[0]] += tdir
tan2[polygon.positions[1]] += tdir
tan2[polygon.positions[2]] += tdir
normals[polygon.positions[0]] += vertexData.normals[polygon.normals[0]]
normals[polygon.positions[1]] += vertexData.normals[polygon.normals[1]]
normals[polygon.positions[2]] += vertexData.normals[polygon.normals[2]]
}
for (a in 0 until vertexData.positions.size) {
normals[a] = normals[a].normalized
tan1[a] = tan1[a].normalized
tan2[a] = tan2[a].normalized
val t = tan1[a]
val n = normals[a]
tan1[a] = (t - n * n.dot(t)).normalized
val w = if ((n.cross(t)).dot(tan2[a]) < 0.0f) -1.0 else 1.0
tan2[a] = n.cross((t)).normalized * w
}
return MeshData(VertexData(vertexData.positions, vertexData.textureCoords, vertexData.colors, normals, tan1, tan2),
polygons = polygons.map {
IndexedPolygon(
it.positions,
it.textureCoords,
it.colors,
normals = it.positions,
tangents = it.positions,
bitangents = it.positions
)
}
)
}

View File

@@ -0,0 +1,55 @@
package decal
import org.openrndr.application
import org.openrndr.draw.DrawPrimitive
import org.openrndr.draw.shadeStyle
import org.openrndr.extra.camera.Orbital
import org.openrndr.extra.mesh.*
import org.openrndr.extra.objloader.loadOBJMeshData
import org.openrndr.extra.meshgenerators.decal.decal
import org.openrndr.math.Vector3
import org.openrndr.math.transforms.buildTransform
import java.io.File
/**
* Demonstrate decal generator as an object slicer
*/
fun main() {
application {
configure {
width = 720
height = 720
}
program {
val obj = loadOBJMeshData(File("demo-data/obj-models/suzanne/Suzanne.obj")).toMeshData().triangulate()
val slices = 25
val sliceStep = 0.1
val sliceWidth = 0.14
val sliceVBs = (0 until slices).map {
val projector = buildTransform {
translate(0.0, 0.0, -1.0 + it * sliceStep)
}
val decal = obj.decal(projector, Vector3(4.0, 4.0, sliceWidth))
val vb = decal.toVertexBuffer()
vb
}
extend(Orbital()) {
eye = Vector3(0.0, 0.0, 2.0)
}
extend {
drawer.shadeStyle = shadeStyle {
fragmentTransform = """x_fill.rgb = v_viewNormal.rgb * 0.5 + 0.5; """
}
drawer.translate(0.0, 0.0, slices * 0.5 * 0.5)
for (i in 0 until sliceVBs.size) {
drawer.vertexBuffer(sliceVBs[i], DrawPrimitive.TRIANGLES)
drawer.translate(0.0, 0.0, -0.5)
}
}
}
}
}

View File

@@ -0,0 +1,86 @@
package decal
import org.openrndr.application
import org.openrndr.draw.DrawPrimitive
import org.openrndr.draw.isolated
import org.openrndr.draw.shadeStyle
import org.openrndr.extra.camera.Orbital
import org.openrndr.extra.objloader.loadOBJMeshData
import org.openrndr.extra.mesh.toVertexBuffer
import org.openrndr.extra.meshgenerators.decal.decal
import org.openrndr.math.Vector3
import org.openrndr.math.transforms.buildTransform
import java.io.File
import kotlin.math.PI
/**
* Demonstrate decal generation and rendering
*/
fun main() {
application {
configure {
width = 720
height = 720
}
program {
/** base object */
val obj = loadOBJMeshData(File("demo-data/obj-models/suzanne/Suzanne.obj"))
.toMeshData() // convert from CompoundMeshData to MeshData
.triangulate() // convert to triangles, we need this for the decal generation steps
/** object [VertexBuffer] */
val objVB = obj.toVertexBuffer()
/** positions for the decal projectors */
val decalPositions = listOf(
Vector3(0.35, 0.245, 0.8),
Vector3(-0.35, 0.245, 0.8)
)
/** decal vertex buffers */
val decalVBs = decalPositions.map {
val projector = buildTransform {
translate(it)
}
val decal = obj.decal(projector, Vector3(2.0, 2.0, 0.5))
val vb = decal.toVertexBuffer()
vb
}
extend(Orbital()) {
eye = Vector3(0.0, 0.0, 2.0)
}
extend {
/* draw the base mesh */
drawer.isolated {
drawer.shadeStyle = shadeStyle {
fragmentTransform = """x_fill.rgb = vec3(v_viewNormal * 0.5 + 0.5); """
}
drawer.vertexBuffer(objVB, DrawPrimitive.TRIANGLES)
}
/* draw the decals */
drawer.isolated {
for ((index, decal) in decalVBs.withIndex()) {
/* offset the projection transform to avoid z-fighting */
drawer.projection = buildTransform {
translate(0.0, 0.0, -1e-4)
} * drawer.projection
/* draw effects on the decal geometry */
drawer.shadeStyle = shadeStyle {
fragmentTransform = """
float d = length(va_texCoord0.xy - vec2(0.5));
float sd = smoothstep(-0.01, 0.01, cos(p_time + d * 3.1415 * 2.0 * 10.0));
float l = max(0.0, va_normal.z);
x_fill = vec4(0.0, 0.0, 0.0, l * sd * 0.5); """
parameter("time", seconds * PI * 2 + index * PI)
}
drawer.vertexBuffer(decal, DrawPrimitive.TRIANGLES)
}
}
}
}
}
}

View File

@@ -3,7 +3,7 @@ import org.openrndr.draw.DrawPrimitive
import org.openrndr.draw.isolated
import org.openrndr.draw.shadeStyle
import org.openrndr.extra.camera.Orbital
import org.openrndr.extra.mesh.loadOBJMeshData
import org.openrndr.extra.objloader.loadOBJMeshData
import org.openrndr.extra.mesh.noise.uniform
import org.openrndr.extra.meshgenerators.sphereMesh
import org.openrndr.math.Vector3

View File

@@ -18,6 +18,7 @@ kotlin {
api(libs.openrndr.shape)
implementation(project(":orx-shapes"))
implementation(project(":orx-mesh-generators"))
implementation(project(":orx-obj-loader"))
implementation(project(":orx-camera"))
implementation(project(":orx-noise"))
}

View File

@@ -3,6 +3,9 @@ package org.openrndr.extra.mesh
import org.openrndr.draw.VertexBuffer
import org.openrndr.draw.vertexBuffer
/**
* Write compound mesh data to [VertexBuffer]
*/
fun ICompoundMeshData.toVertexBuffer(): VertexBuffer {
val triangulated = this.triangulate()
@@ -19,6 +22,9 @@ fun ICompoundMeshData.toVertexBuffer(): VertexBuffer {
return vertexBuffer
}
/**
* Convert compound mesh data to [IPolygon] compounds
*/
fun ICompoundMeshData.toPolygons(): Map<String, List<IPolygon>> {
return compounds.mapValues { it.value.toPolygons() }
}

View File

@@ -4,10 +4,7 @@ import org.openrndr.math.Matrix44
import org.openrndr.math.Vector2
import org.openrndr.math.Vector3
import org.openrndr.math.Vector4
import kotlin.math.PI
import kotlin.math.abs
import kotlin.math.atan2
import kotlin.math.round
import kotlin.math.*
/**
* Indexed polygon interface
@@ -133,6 +130,15 @@ interface IIndexedPolygon {
return abs(round(angleSum / (2 * PI))) == 1.0
}
/**
* Evaluate polygon normal
*/
fun normal(vertexData: IVertexData) : Vector3 {
val u = vertexData.positions[positions[1]] - vertexData.positions[positions[0]]
val v = vertexData.positions[positions[2]] - vertexData.positions[positions[0]]
return u.cross(v).normalized
}
/**
* Convert to [IPolygon]
* @param vertexData the vertex data required to build the [IPolygon]
@@ -146,8 +152,8 @@ interface IIndexedPolygon {
data class IndexedPolygon(
override val positions: List<Int>,
override val textureCoords: List<Int>,
override val normals: List<Int>,
override val colors: List<Int>,
override val normals: List<Int>,
override val tangents: List<Int>,
override val bitangents: List<Int>
@@ -161,8 +167,8 @@ data class IndexedPolygon(
IndexedPolygon(
positions.slice(it),
if (textureCoords.isNotEmpty()) textureCoords.slice(it) else listOf(),
if (normals.isNotEmpty()) normals.slice(it) else listOf(),
if (colors.isNotEmpty()) colors.slice(it) else listOf(),
if (normals.isNotEmpty()) normals.slice(it) else listOf(),
if (tangents.isNotEmpty()) tangents.slice(it) else listOf(),
if (bitangents.isNotEmpty()) bitangents.slice(it) else listOf()
)
@@ -187,16 +193,16 @@ data class IndexedPolygon(
textureCoords.getOrNull(it),
textureCoords.getOrNull(it + 1)
),
listOfNotNull(
normals.getOrNull(0),
normals.getOrNull(it),
normals.getOrNull(it + 1)
),
listOfNotNull(
colors.getOrNull(0),
colors.getOrNull(it + 1),
colors.getOrNull(it + 2)
),
listOfNotNull(
normals.getOrNull(0),
normals.getOrNull(it),
normals.getOrNull(it + 1)
),
listOfNotNull(
tangents.getOrNull(0),
tangents.getOrNull(it + 1),
@@ -218,9 +224,39 @@ data class IndexedPolygon(
override fun toPolygon(vertexData: IVertexData): Polygon {
return Polygon(
vertexData.positions.slice(positions),
vertexData.normals.slice(normals),
vertexData.textureCoords.slice(textureCoords),
vertexData.colors.slice(colors)
vertexData.colors.slice(colors),
vertexData.normals.slice(normals),
vertexData.tangents.slice(tangents),
vertexData.bitangents.slice(bitangents)
)
}
/**
* Shift indices
* @param positions position index shift
* @param textureCoords texture coordinate index shift
* @param colors color index shift
* @param normals normal index shift
* @param tangents tangent index shift
* @param bitangents bitangent index shift
*
*/
fun shiftIndices(
positions: Int = 0,
textureCoords: Int = 0,
colors: Int = 0,
normals: Int = 0,
tangents: Int = 0,
bitangents: Int = 0
): IndexedPolygon {
return IndexedPolygon(
positions = this.positions.map { it + positions },
textureCoords = this.textureCoords.map { it + textureCoords },
colors = this.colors.map { it + colors },
normals = this.normals.map { it + normals },
tangents = this.tangents.map { it + tangents },
bitangents = this.bitangents.map { it + bitangents }
)
}
}
@@ -240,9 +276,9 @@ data class MutableIndexedPolygon(
override fun toPolygon(vertexData: IVertexData): MutablePolygon {
return MutablePolygon(
vertexData.positions.slice(positions).toMutableList(),
vertexData.normals.slice(normals).toMutableList(),
vertexData.textureCoords.slice(textureCoords).toMutableList(),
vertexData.colors.slice(colors).toMutableList()
vertexData.colors.slice(colors).toMutableList(),
vertexData.normals.slice(normals).toMutableList()
)
}
}

View File

@@ -0,0 +1,33 @@
package org.openrndr.extra.mesh
import org.openrndr.math.LinearType
import org.openrndr.math.Vector3
internal fun <T : LinearType<T>> bc(barycentric: Vector3, items: List<T>): T {
return (items[0] * barycentric.x) + (items[1] * barycentric.y) + (items[2] * barycentric.z)
}
/**
* Evaluate a point in triangle
* @param vertexData the vertex data to use
* @param barycentric the barycentric coordinates of the point to evaluate
*/
fun IIndexedPolygon.point(vertexData: VertexData, barycentric: Vector3): Point {
require(positions.size == 3)
val positions = vertexData.positions.slice(positions)
val colors = vertexData.colors.slice(colors)
val normals = vertexData.normals.slice(normals)
val tangents = vertexData.tangents.slice(tangents)
val bitangents = vertexData.bitangents.slice(bitangents)
val textureCoords = vertexData.textureCoords.slice(textureCoords)
return Point(
(if (positions.isNotEmpty()) bc(barycentric, positions) else null)!!,
if (textureCoords.isNotEmpty()) bc(barycentric, textureCoords) else null,
if (colors.isNotEmpty()) bc(barycentric, colors) else null,
if (normals.isNotEmpty()) bc(barycentric, normals) else null,
if (tangents.isNotEmpty()) bc(barycentric, tangents) else null,
if (bitangents.isNotEmpty()) bc(barycentric, bitangents) else null
)
}

View File

@@ -8,8 +8,26 @@ import kotlin.jvm.JvmRecord
interface IMeshData {
val vertexData: IVertexData
val polygons: List<IIndexedPolygon>
/**
* Convert mesh data to triangular mesh data
*/
fun triangulate(): IMeshData
/**
* Convert mesh data to a list of [IPolygon]
*/
fun toPolygons(): List<IPolygon>
/**
* Join mesh data with [other] mesh data
*/
fun join(other: IMeshData): IMeshData
fun toMeshData(): MeshData
fun toMutableMeshData() : MutableMeshData
}
/**
@@ -32,6 +50,94 @@ data class MeshData(
ip.toPolygon(vertexData)
}
}
override fun join(other: IMeshData): IMeshData {
if (vertexData === other.vertexData) {
@Suppress("UNCHECKED_CAST")
return MeshData(vertexData, polygons + (other.polygons as List<IndexedPolygon>))
} else {
val positionsShift: Int
val positions = if (vertexData.positions === other.vertexData.positions) {
positionsShift = 0
vertexData.positions
} else {
positionsShift = vertexData.positions.size
vertexData.positions + other.vertexData.positions
}
val textureCoordsShift: Int
val textureCoords = if (vertexData.textureCoords === other.vertexData.textureCoords) {
textureCoordsShift = 0
vertexData.textureCoords
} else {
textureCoordsShift = vertexData.textureCoords.size
vertexData.textureCoords + other.vertexData.textureCoords
}
val colorsShift: Int
val colors = if (vertexData.colors === other.vertexData.colors) {
colorsShift = 0
vertexData.colors
} else {
colorsShift = vertexData.colors.size
vertexData.colors + other.vertexData.colors
}
val normalsShift: Int
val normals = if (vertexData.normals === other.vertexData.normals) {
normalsShift = 0
vertexData.normals
} else {
normalsShift = vertexData.normals.size
vertexData.normals + other.vertexData.normals
}
val tangentsShift: Int
val tangents = if (vertexData.tangents === other.vertexData.tangents) {
tangentsShift = 0
vertexData.tangents
} else {
tangentsShift = vertexData.tangents.size
vertexData.tangents + other.vertexData.tangents
}
val bitangentsShift: Int
val bitangents = if (vertexData.bitangents === other.vertexData.bitangents) {
bitangentsShift = 0
vertexData.bitangents
} else {
bitangentsShift = vertexData.bitangents.size
vertexData.bitangents + other.vertexData.bitangents
}
return MeshData(
VertexData(
positions = positions,
textureCoords = textureCoords,
colors = colors,
normals = normals,
tangents = tangents,
bitangents = bitangents
),
polygons + other.polygons.map {
(it as IndexedPolygon).shiftIndices(
positionsShift,
textureCoordsShift,
colorsShift,
normalsShift,
tangentsShift,
bitangentsShift
)
}
)
}
}
override fun toMeshData(): MeshData = this
override fun toMutableMeshData(): MutableMeshData {
TODO("Not yet implemented")
}
}
@@ -53,4 +159,16 @@ data class MutableMeshData(
override fun toPolygons(): List<Polygon> {
return polygons.map { it.toPolygon(vertexData) }
}
override fun join(other: IMeshData): IMeshData {
TODO("Not yet implemented")
}
override fun toMutableMeshData(): MutableMeshData {
return this
}
override fun toMeshData(): MeshData {
TODO("Not yet implemented")
}
}

View File

@@ -1,11 +1,8 @@
package org.openrndr.extra.mesh
import org.openrndr.color.ColorRGBa
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
import org.openrndr.draw.*
import org.openrndr.math.*
/**
* The [VertexFormat] for a [VertexBuffer] with positions, normals and texture coordinates.
@@ -17,6 +14,16 @@ internal val objVertexFormat = vertexFormat {
color(4)
}
internal val objVertexFormatTangents = vertexFormat {
position(3)
normal(3)
textureCoordinate(2)
color(4)
attribute("tangent", VertexElementType.VECTOR3_FLOAT32)
attribute("bitangent", VertexElementType.VECTOR3_FLOAT32)
}
/**
* Determine if [IMeshData] is triangular by checking if each polygon has exactly 3 vertices
*/
@@ -30,11 +37,15 @@ fun IMeshData.isTriangular(): Boolean {
fun IMeshData.toVertexBuffer(elementOffset: Int = 0, vertexBuffer: VertexBuffer? = null): VertexBuffer {
val objects = triangulate().toPolygons()
val triangleCount = objects.size
val vertexBuffer = vertexBuffer ?: vertexBuffer(objVertexFormat, triangleCount * 3)
val format = if (vertexData.tangents.isNotEmpty() && vertexData.bitangents.isNotEmpty()) {
objVertexFormatTangents
} else objVertexFormat
val vertexBuffer = vertexBuffer ?: vertexBuffer(format, triangleCount * 3)
vertexBuffer.put(elementOffset) {
objects.forEach {
for (i in it.positions.indices) {
write(it.positions[i])
if (it.normals.isNotEmpty()) {
@@ -54,9 +65,164 @@ fun IMeshData.toVertexBuffer(elementOffset: Int = 0, vertexBuffer: VertexBuffer?
} else {
write(ColorRGBa.WHITE)
}
if (format == objVertexFormatTangents) {
write(it.tangents[i])
write(it.bitangents[i])
}
}
}
}
vertexBuffer.shadow.destroy()
return vertexBuffer
}
/**
* Weld vertices
* @param positionFractBits number of bits to use for fractional representation, negative amount to skip welding
*/
fun IMeshData.weld(
positionFractBits: Int,
textureCoordFractBits: Int = -1,
colorFractBits: Int = -1,
normalFractBits: Int = -1,
tangentFractBits: Int = -1,
bitangentFractBits: Int = -1
): MeshData {
fun MutableMap<IntVector3, Int>.quantize(v: Vector3, bits: Int): Int =
getOrPut((v * (1 shl bits).toDouble()).toInt()) { this.size }
fun MutableMap<IntVector2, Int>.quantize(v: Vector2, bits: Int): Int =
getOrPut((v * (1 shl bits).toDouble()).toInt()) { this.size }
fun MutableMap<IntVector4, Int>.quantize(v: Vector4, bits: Int): Int =
getOrPut((v * (1 shl bits).toDouble()).toInt()) { this.size }
val positionMap = mutableMapOf<IntVector3, Int>()
val textureCoordMap = mutableMapOf<IntVector2, Int>()
val colorMap = mutableMapOf<IntVector4, Int>()
val normalMap = mutableMapOf<IntVector3, Int>()
val tangentMap = mutableMapOf<IntVector3, Int>()
val bitangentMap = mutableMapOf<IntVector3, Int>()
if (positionFractBits >= 0) {
for (p in vertexData.positions) {
positionMap.quantize(p, positionFractBits)
}
}
if (textureCoordFractBits >= 0) {
for (p in vertexData.textureCoords) {
textureCoordMap.quantize(p, textureCoordFractBits)
}
}
if (colorFractBits >= 0) {
for (p in vertexData.colors) {
colorMap.quantize(p.toVector4(), colorFractBits)
}
}
if (normalFractBits >= 0) {
for (p in vertexData.normals) {
normalMap.quantize(p, normalFractBits)
}
}
if (tangentFractBits >= 0) {
for (p in vertexData.tangents) {
tangentMap.quantize(p, tangentFractBits)
}
}
if (bitangentFractBits >= 0) {
for (p in vertexData.bitangents) {
bitangentMap.quantize(p, bitangentFractBits)
}
}
val reindexedPolygons = mutableListOf<IndexedPolygon>()
for (polygon in polygons) {
val positions = if (positionFractBits >= 0) {
vertexData.positions.slice(polygon.positions).map { positionMap.quantize(it, positionFractBits) }
} else {
polygon.positions
}
val textureCoords = if (textureCoordFractBits >= 0) {
vertexData.textureCoords.slice(polygon.textureCoords)
.map { textureCoordMap.quantize(it, textureCoordFractBits) }
} else {
polygon.textureCoords
}
val colors = if (colorFractBits >= 0) {
vertexData.colors.slice(polygon.colors).map { colorMap.quantize(it.toVector4(), colorFractBits) }
} else {
polygon.colors
}
val normals = if (normalFractBits >= 0) {
vertexData.normals.slice(polygon.normals).map { normalMap.quantize(it, normalFractBits) }
} else {
polygon.normals
}
val tangents = if (tangentFractBits >= 0) {
vertexData.tangents.slice(polygon.tangents).map { tangentMap.quantize(it, tangentFractBits) }
} else {
polygon.tangents
}
val bitangents = if (bitangentFractBits >= 0) {
vertexData.bitangents.slice(polygon.bitangents).map { bitangentMap.quantize(it, bitangentFractBits) }
} else {
polygon.bitangents
}
reindexedPolygons.add(IndexedPolygon(positions, textureCoords, colors, normals, tangents, bitangents))
}
val positionByIndex = vertexData.positions.associateBy { positionMap.quantize(it, positionFractBits) }
val textureCoordByIndex =
vertexData.textureCoords.associateBy { textureCoordMap.quantize(it, textureCoordFractBits) }
val colorByIndex = vertexData.colors.associateBy { colorMap.quantize(it.toVector4(), colorFractBits) }
val normalByIndex = vertexData.normals.associateBy { normalMap.quantize(it, normalFractBits) }
val tangentByIndex = vertexData.tangents.associateBy { tangentMap.quantize(it, tangentFractBits) }
val bitangentByIndex = vertexData.bitangents.associateBy { bitangentMap.quantize(it, bitangentFractBits) }
val reindexedVertexData = VertexData(
if (positionFractBits >= 0) {
(0 until positionByIndex.size).map { positionByIndex.getValue(it) }
} else {
vertexData.positions
},
if (textureCoordFractBits >= 0) {
(0 until textureCoordByIndex.size).map { textureCoordByIndex.getValue(it) }
} else {
vertexData.textureCoords
},
if (colorFractBits >= 0) {
(0 until colorByIndex.size).map { colorByIndex.getValue(it) }
} else {
vertexData.colors
},
if (normalFractBits >= 0) {
(0 until normalByIndex.size).map { normalByIndex.getValue(it) }
} else {
vertexData.normals
},
if (tangentFractBits >= 0) {
(0 until tangentByIndex.size).map { tangentByIndex.getValue(it) }
} else {
vertexData.tangents
},
if (bitangentFractBits >= 0) {
(0 until bitangentByIndex.size).map { bitangentByIndex.getValue(it) }
} else {
vertexData.bitangents
}
)
return MeshData(reindexedVertexData, reindexedPolygons)
}

View File

@@ -0,0 +1,23 @@
package org.openrndr.extra.mesh
import org.openrndr.color.ColorRGBa
import org.openrndr.math.Vector2
import org.openrndr.math.Vector3
/**
* Point with optional attributes
* @param position position attribute
* @param textureCoord optional texture coordinate attribute
* @param color optional color attribute
* @param normal optional normal attribute
* @param tangent optional tangent attribute
* @param bitangent optional bitangent attribute
*/
data class Point(
val position: Vector3,
val textureCoord: Vector2? = null,
val color: ColorRGBa? = null,
val normal: Vector3? = null,
val tangent: Vector3? = null,
val bitangent: Vector3? =null
)

View File

@@ -14,9 +14,9 @@ import kotlin.math.min
*/
interface IPolygon {
val positions: List<Vector3>
val normals: List<Vector3>
val textureCoords: List<Vector2>
val colors: List<ColorRGBa>
val normals: List<Vector3>
val tangents: List<Vector3>
val bitangents: List<Vector3>
@@ -33,14 +33,14 @@ interface IPolygon {
*/
class Polygon(
override val positions: List<Vector3> = emptyList(),
override val normals: List<Vector3> = emptyList(),
override val textureCoords: List<Vector2> = emptyList(),
override val colors: List<ColorRGBa> = emptyList(),
override val normals: List<Vector3> = emptyList(),
override val tangents: List<Vector3> = emptyList(),
override val bitangents: List<Vector3> = emptyList(),
) : IPolygon {
override fun transform(t: Matrix44): Polygon {
return Polygon(positions.map { (t * it.xyz1).div }, normals, textureCoords, colors, tangents, bitangents)
return Polygon(positions.map { (t * it.xyz1).div }, textureCoords, colors, normals, tangents, bitangents)
}
/**
@@ -49,9 +49,9 @@ class Polygon(
fun toMutablePolygon(): MutablePolygon {
return MutablePolygon(
positions.toMutableList(),
normals.toMutableList(),
textureCoords.toMutableList(),
colors.toMutableList(),
normals.toMutableList(),
tangents.toMutableList(),
bitangents.toMutableList()
)
@@ -63,9 +63,9 @@ class Polygon(
*/
class MutablePolygon(
override val positions: MutableList<Vector3> = mutableListOf(),
override val normals: MutableList<Vector3> = mutableListOf(),
override val textureCoords: MutableList<Vector2> = mutableListOf(),
override val colors: MutableList<ColorRGBa> = mutableListOf(),
override val normals: MutableList<Vector3> = mutableListOf(),
override val tangents: MutableList<Vector3> = mutableListOf(),
override val bitangents: MutableList<Vector3> = mutableListOf()
@@ -73,9 +73,9 @@ class MutablePolygon(
override fun transform(t: Matrix44): MutablePolygon {
return MutablePolygon(
positions.map { (t * it.xyz1).div }.toMutableList(),
ArrayList(normals),
ArrayList(textureCoords),
ArrayList(colors),
ArrayList(normals),
ArrayList(tangents),
ArrayList(bitangents)
)
@@ -136,8 +136,8 @@ fun List<IPolygon>.toMeshData(): MeshData {
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(),
normals = if (p.normals.isNotEmpty()) indices else emptyList(),
tangents = if (p.tangents.isNotEmpty()) indices else emptyList(),
bitangents = if (p.bitangents.isNotEmpty()) indices else emptyList()
)

View File

@@ -13,11 +13,6 @@ interface IVertexData {
*/
val positions: List<Vector3>
/**
* Vertex normals
*/
val normals: List<Vector3>
/**
* Vertex texture coordinates
*/
@@ -28,6 +23,11 @@ interface IVertexData {
*/
val colors: List<ColorRGBa>
/**
* Vertex normals
*/
val normals: List<Vector3>
/**
* Vertex tangents
*/
@@ -37,6 +37,16 @@ interface IVertexData {
* Vertex bitangents
*/
val bitangents: List<Vector3>
/**
* Convert to [VertexData]
*/
fun toVertexData() : VertexData
/**
* Convert to [MutableVertexData]
*/
fun toMutableVertexData() : MutableVertexData
}
/**
@@ -44,27 +54,24 @@ interface IVertexData {
*/
class VertexData(
override val positions: List<Vector3> = emptyList(),
override val normals: List<Vector3> = emptyList(),
override val textureCoords: List<Vector2> = emptyList(),
override val colors: List<ColorRGBa> = emptyList(),
override val normals: List<Vector3> = emptyList(),
override val tangents: List<Vector3> = emptyList(),
override val bitangents: List<Vector3> = emptyList()
) : IVertexData {
/**
* Convert to [MutableVertexData]
*/
fun toMutableVertexData(): MutableVertexData {
return MutableVertexData(
override fun toVertexData(): VertexData = this
override fun toMutableVertexData(): MutableVertexData = MutableVertexData(
positions.toMutableList(),
normals.toMutableList(),
textureCoords.toMutableList(),
colors.toMutableList(),
normals.toMutableList(),
tangents.toMutableList(),
bitangents.toMutableList()
)
}
}
/**
@@ -72,24 +79,54 @@ class VertexData(
*/
class MutableVertexData(
override val positions: MutableList<Vector3> = mutableListOf(),
override val normals: MutableList<Vector3> = mutableListOf(),
override val textureCoords: MutableList<Vector2> = mutableListOf(),
override val colors: MutableList<ColorRGBa> = mutableListOf(),
override val normals: MutableList<Vector3> = mutableListOf(),
override val tangents: MutableList<Vector3> = mutableListOf(),
override val bitangents: MutableList<Vector3> = mutableListOf()
) : IVertexData {
/**
* Convert to [VertexData]
*/
fun toVertexData(): VertexData {
return VertexData(
override fun toVertexData(): VertexData = VertexData(
positions.toList(),
normals.toList(),
textureCoords.toList(),
colors.toList(),
normals.toList(),
tangents.toList(),
bitangents.toList()
)
override fun toMutableVertexData(): MutableVertexData = this
}
/**
* Add [point] to vertex data
*/
fun MutableVertexData.add(point: Point) {
positions.add(point.position)
point.color?.let { colors.add(it) }
point.textureCoord?.let { textureCoords.add(it) }
point.normal?.let { normals.add(it) }
point.tangent?.let { tangents.add(it) }
point.bitangent?.let { bitangents.add(it) }
}
/**
* Retrieve [Point] from vertex data
*/
operator fun IVertexData.get(
index: Int,
textureCoordsIndex: Int = index,
colorsIndex: Int = index,
normalsIndex: Int = index,
tangentsIndex: Int = index,
bitangentsIndex: Int = index
): Point {
return Point(
positions[index],
textureCoords.getOrNull(textureCoordsIndex),
colors.getOrNull(colorsIndex),
normals.getOrNull(normalsIndex),
tangents.getOrNull(tangentsIndex),
bitangents.getOrNull(bitangentsIndex)
)
}

View File

@@ -0,0 +1,68 @@
package org.openrndr.extra.mesh
import org.openrndr.color.ColorRGBa
import org.openrndr.draw.VertexBuffer
import org.openrndr.draw.vertexBuffer
import org.openrndr.math.Vector2
import org.openrndr.math.Vector3
/**
* Convert vertex data to [VertexBuffer]. Assumes every 3 consecutive vertices encode a triangle.
*/
fun IVertexData.toVertexBuffer(elementOffset: Int = 0, vertexBuffer: VertexBuffer? = null): VertexBuffer {
val triangleCount = positions.size / 3
val vertexBuffer = vertexBuffer ?: vertexBuffer(objVertexFormat, triangleCount * 3)
vertexBuffer.put(elementOffset) {
var offset = 0
for (triangle in 0 until triangleCount) {
for (i in 0 until 3) {
write(positions[offset])
if (normals.isNotEmpty()) {
write(normals[offset])
} else {
write(Vector3.ZERO)
}
if (textureCoords.isNotEmpty()) {
write(textureCoords[offset])
} else {
write(Vector2.ZERO)
}
if (colors.isNotEmpty()) {
write(colors[offset])
} else {
write(ColorRGBa.WHITE)
}
offset++
}
}
}
vertexBuffer.shadow.destroy()
return vertexBuffer
}
/**
* Convert vertex data to [MeshData]. Assumes every 3 consecutive vertices encode a triangle.
*/
fun VertexData.toMeshData(): MeshData {
val polygons = mutableListOf<IndexedPolygon>()
val triangleCount = positions.size / 3
for (t in 0 until triangleCount) {
val indices = listOf(t * 3, t * 3 + 1, t * 3 + 2)
polygons.add(
IndexedPolygon(
indices,
if (textureCoords.isNotEmpty()) indices else emptyList(),
if (colors.isNotEmpty()) indices else emptyList(),
if (normals.isNotEmpty()) indices else emptyList(),
if (tangents.isNotEmpty()) indices else emptyList(),
if (bitangents.isNotEmpty()) indices else emptyList()
)
)
}
return MeshData(this, polygons)
}

View File

@@ -9,7 +9,6 @@ fun IMeshData.wireframe(): List<List<Vector3>> {
return polygons.map { ip -> ip.toPolygon(this.vertexData).positions.toList() }
}
/**
* Extract wireframe from compound mesh data
*/

View File

@@ -2,8 +2,6 @@ package org.openrndr.extra.mesh
import org.openrndr.color.ColorRGBa
import org.openrndr.draw.VertexBuffer
import org.openrndr.extra.mesh.Polygon
import org.openrndr.extra.mesh.objVertexFormat
import org.openrndr.math.Vector2
import org.openrndr.math.Vector3
import java.nio.ByteBuffer
@@ -53,7 +51,7 @@ fun VertexBuffer.toPolygons(vertexCount: Int = this.vertexCount): List<Polygon>
textureCoordinates.add(buffer.getVector2())
colors.add(buffer.getColorRGBa())
}
polygons.add(Polygon(positions, normals, textureCoordinates, colors))
polygons.add(Polygon(positions, textureCoordinates, colors, normals))
}
return polygons
}

View File

@@ -1,7 +1,8 @@
package org.openrndr.extra.mesh
package org.openrndr.extra.objloader
import org.openrndr.color.ColorRGBa
import org.openrndr.draw.VertexBuffer
import org.openrndr.extra.mesh.*
import org.openrndr.math.Vector2
import org.openrndr.math.Vector3
@@ -79,8 +80,8 @@ fun readObjMeshData(lines: Iterable<String>): CompoundMeshData {
IndexedPolygon(
if (hasPosition) indices.map { it[0] - 1 } else listOf(),
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(),
if (hasNormal) indices.map { it[2] - 1 } else listOf(),
if (hasTangents) indices.map { it[2] - 1 } else listOf(),
if (hasBitangents) indices.map { it[2] - 1 } else listOf()
)
@@ -95,7 +96,7 @@ fun readObjMeshData(lines: Iterable<String>): CompoundMeshData {
}
}
val vertexData = VertexData(positions, normals, textureCoords, colors)
val vertexData = VertexData(positions, textureCoords, colors, normals)
return CompoundMeshData(
vertexData,
meshes.mapValues {

View File

@@ -1,4 +1,6 @@
package org.openrndr.extra.mesh
package org.openrndr.extra.objloader
import org.openrndr.extra.mesh.ICompoundMeshData
/**
* Convert mesh data to Wavefront OBJ representation

View File

@@ -1,6 +1,6 @@
import org.openrndr.application
import org.openrndr.extra.mesh.loadOBJMeshData
import org.openrndr.extra.mesh.toObj
import org.openrndr.extra.objloader.loadOBJMeshData
import org.openrndr.extra.objloader.toObj
import java.io.File
fun main() {

View File

@@ -3,7 +3,7 @@ import org.openrndr.color.ColorRGBa
import org.openrndr.draw.DepthTestPass
import org.openrndr.draw.DrawPrimitive
import org.openrndr.draw.shadeStyle
import org.openrndr.extra.mesh.loadOBJasVertexBuffer
import org.openrndr.extra.objloader.loadOBJasVertexBuffer
import org.openrndr.math.Vector3
fun main() = application {

View File

@@ -1,7 +1,7 @@
import org.openrndr.application
import org.openrndr.draw.loadFont
import org.openrndr.extra.mesh.loadOBJasVertexBuffer
import org.openrndr.extra.mesh.saveOBJ
import org.openrndr.extra.objloader.loadOBJasVertexBuffer
import org.openrndr.extra.objloader.saveOBJ
fun main() = application {
configure {

View File

@@ -2,7 +2,7 @@ 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.mesh.saveOBJ
import org.openrndr.extra.objloader.saveOBJ
fun main() = application {
configure {

View File

@@ -8,8 +8,8 @@ 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.mesh.readObjMeshData
import org.openrndr.extra.mesh.loadOBJasVertexBuffer
import org.openrndr.extra.objloader.readObjMeshData
import org.openrndr.extra.objloader.loadOBJasVertexBuffer
import org.openrndr.extra.mesh.wireframe
import org.openrndr.math.Vector3
import org.openrndr.shape.Path3D

View File

@@ -1,6 +1,7 @@
package org.openrndr.extra.mesh
package org.openrndr.extra.objloader
import org.openrndr.draw.VertexBuffer
import org.openrndr.extra.mesh.IPolygon
import java.io.File
import java.net.MalformedURLException
import java.net.URL

View File

@@ -1,4 +1,4 @@
package org.openrndr.extra.mesh
package org.openrndr.extra.objloader
import org.openrndr.draw.VertexBuffer
import java.io.File