[orx-mesh] Split MeshData types from orx-obj-loader

This commit is contained in:
Edwin Jakobs
2024-09-13 22:41:31 +02:00
parent 3588aecd58
commit 8238207894
16 changed files with 466 additions and 172 deletions

26
orx-mesh/build.gradle.kts Normal file
View File

@@ -0,0 +1,26 @@
plugins {
org.openrndr.extra.convention.`kotlin-multiplatform`
}
kotlin {
sourceSets {
val commonMain by getting {
dependencies {
api(libs.openrndr.application)
api(libs.openrndr.math)
api(libs.openrndr.shape)
implementation(project(":orx-shapes"))
}
}
val jvmDemo by getting {
dependencies {
api(libs.openrndr.shape)
implementation(project(":orx-shapes"))
implementation(project(":orx-mesh-generators"))
implementation(project(":orx-camera"))
implementation(project(":orx-noise"))
}
}
}
}

View File

@@ -0,0 +1,30 @@
package org.openrndr.extra.objloader
interface ICompoundMeshData {
val vertexData: IVertexData
val compounds: Map<String, IMeshData>
fun triangulate(): ICompoundMeshData
}
class CompoundMeshData(
override val vertexData: VertexData,
override val compounds: Map<String, MeshData>
) : ICompoundMeshData {
override fun triangulate(): CompoundMeshData {
return CompoundMeshData(vertexData, compounds.mapValues {
it.value.triangulate()
})
}
}
class MutableCompoundMeshData(
override val vertexData: MutableVertexData,
override val compounds: MutableMap<String, MutableMeshData>
) : ICompoundMeshData {
override fun triangulate(): MutableCompoundMeshData {
TODO("Not yet implemented")
}
}

View File

@@ -0,0 +1,24 @@
package org.openrndr.extra.objloader
import org.openrndr.draw.VertexBuffer
import org.openrndr.draw.vertexBuffer
fun ICompoundMeshData.toVertexBuffer(): VertexBuffer {
val triangulated = this.triangulate()
val triangleCount = triangulated.compounds.values.sumOf { it.polygons.size }
val vertexBuffer = vertexBuffer(objVertexFormat, triangleCount * 3)
var elementOffset = 0
for (compound in compounds) {
compound.value.toVertexBuffer(elementOffset, vertexBuffer)
elementOffset += compound.value.polygons.size * 3
}
return vertexBuffer
}
fun ICompoundMeshData.flattenPolygons(): Map<String, List<IPolygon>> {
return compounds.mapValues { it.value.flattenPolygons() }
}

View File

@@ -9,11 +9,15 @@ import kotlin.math.abs
import kotlin.math.atan2
import kotlin.math.round
data class IndexedPolygon(
val positions: IntArray, val textureCoords: IntArray, val normals: IntArray
) {
interface IIndexedPolygon {
val positions: List<Int>
val textureCoords: List<Int>
val normals: List<Int>
val colors: List<Int>
val tangents: List<Int>
val bitangents: List<Int>
fun base(vertexData: VertexData): Matrix44 {
fun base(vertexData: IVertexData): Matrix44 {
val u = (vertexData.positions[positions[1]] - vertexData.positions[positions[0]])
val v = (vertexData.positions[positions[positions.size - 1]] - vertexData.positions[positions[0]])
val normal = u.cross(v)
@@ -26,7 +30,7 @@ data class IndexedPolygon(
)
}
fun isPlanar(vertexData: VertexData, eps: Double = 1E-2): Boolean {
fun isPlanar(vertexData: IVertexData, eps: Double = 1E-2): Boolean {
fun normal(i: Int): Vector3 {
val p0 = vertexData.positions[positions[(i - 1).mod(positions.size)]]
val p1 = vertexData.positions[positions[(i).mod(positions.size)]]
@@ -44,7 +48,7 @@ data class IndexedPolygon(
}
}
fun isConvex(vertexData: VertexData): Boolean {
fun isConvex(vertexData: IVertexData): Boolean {
val planar = base(vertexData).inversed
fun p(v: Vector3): Vector2 {
@@ -71,7 +75,7 @@ data class IndexedPolygon(
return false
}
var angle = newDirection - oldDirection
if (angle <= -Math.PI)
if (angle <= -PI)
angle += PI * 2.0
if (angle > PI) {
@@ -95,37 +99,69 @@ data class IndexedPolygon(
return abs(round(angleSum / (2 * PI))) == 1.0
}
fun tessellate(vertexData: VertexData): List<IndexedPolygon> {
fun toPolygon(vertexData: IVertexData): IPolygon
}
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 tangents: List<Int>,
override val bitangents: List<Int>
) : IIndexedPolygon {
fun tessellate(vertexData: IVertexData): List<IndexedPolygon> {
val points = vertexData.positions.slice(positions.toList())
val triangles = org.openrndr.shape.triangulate(listOf(points))
return triangles.windowed(3, 3).map {
IndexedPolygon(
positions.sliceArray(it),
if (textureCoords.isNotEmpty()) textureCoords.sliceArray(it) else intArrayOf(),
if (normals.isNotEmpty()) normals.sliceArray(it) else intArrayOf()
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 (tangents.isNotEmpty()) tangents.slice(it) else listOf(),
if (bitangents.isNotEmpty()) bitangents.slice(it) else listOf()
)
}
}
fun triangulate(vertexData: VertexData): List<IndexedPolygon> {
fun triangulate(vertexData: IVertexData): List<IndexedPolygon> {
return when {
positions.size == 3 -> listOf(this)
isPlanar(vertexData) && isConvex(vertexData) -> {
val triangleCount = positions.size - 2
(0 until triangleCount).map {
IndexedPolygon(
intArrayOf(positions[0], positions[it + 1], positions[it + 2]),
listOf(positions[0], positions[it + 1], positions[it + 2]),
listOfNotNull(
textureCoords.getOrNull(0),
textureCoords.getOrNull(it),
textureCoords.getOrNull(it + 1)
).toIntArray(),
),
listOfNotNull(
normals.getOrNull(0),
normals.getOrNull(it),
normals.getOrNull(it + 1)
).toIntArray(),
),
listOfNotNull(
colors.getOrNull(0),
colors.getOrNull(it + 1),
colors.getOrNull(it + 2)
),
listOfNotNull(
tangents.getOrNull(0),
tangents.getOrNull(it + 1),
tangents.getOrNull(it + 2)
),
listOfNotNull(
bitangents.getOrNull(0),
bitangents.getOrNull(it + 1),
bitangents.getOrNull(it + 2)
),
)
}
}
@@ -134,11 +170,31 @@ data class IndexedPolygon(
}
}
fun toPolygon(vertexData: VertexData): Polygon {
override fun toPolygon(vertexData: IVertexData): Polygon {
return Polygon(
vertexData.positions.slice(positions.toList()).toTypedArray(),
vertexData.normals.slice(normals.toList()).toTypedArray(),
vertexData.textureCoords.slice(textureCoords.toList()).toTypedArray()
vertexData.positions.slice(positions),
vertexData.normals.slice(normals),
vertexData.textureCoords.slice(textureCoords),
vertexData.colors.slice(colors)
)
}
}
data class MutableIndexedPolygon(
override val positions: MutableList<Int>,
override val textureCoords: MutableList<Int>,
override val normals: MutableList<Int>,
override val colors: MutableList<Int>,
override val tangents: MutableList<Int>,
override val bitangents: MutableList<Int>
) : IIndexedPolygon {
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()
)
}
}

View File

@@ -0,0 +1,41 @@
package org.openrndr.extra.objloader
import kotlin.jvm.JvmRecord
interface IMeshData {
val vertexData: IVertexData
val polygons: List<IIndexedPolygon>
fun triangulate(): IMeshData
fun flattenPolygons(): List<IPolygon>
}
@JvmRecord
data class MeshData(
override val vertexData: VertexData,
override val polygons: List<IndexedPolygon>,
) : IMeshData {
override fun triangulate(): MeshData {
return copy(polygons = polygons.flatMap { polygon -> polygon.triangulate(vertexData) })
}
override fun flattenPolygons(): List<Polygon> {
return polygons.map { ip ->
ip.toPolygon(vertexData)
}
}
}
data class MutableMeshData(
override val vertexData: MutableVertexData,
override val polygons: MutableList<IndexedPolygon>
) : IMeshData {
override fun triangulate(): MutableMeshData {
return copy(polygons = polygons.flatMap { it.triangulate(vertexData) }.toMutableList())
}
override fun flattenPolygons(): List<Polygon> {
return polygons.map { it.toPolygon(vertexData) }
}
}

View File

@@ -0,0 +1,50 @@
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.
*/
internal val objVertexFormat = vertexFormat {
position(3)
normal(3)
textureCoordinate(2)
}
/**
* Converts a [MeshData] instance into a [VertexBuffer]
*/
fun IMeshData.toVertexBuffer(elementOffset: Int = 0, vertexBuffer: VertexBuffer? = null): VertexBuffer {
val objects = triangulate().flattenPolygons()
val triangleCount = objects.size
val vertexBuffer = vertexBuffer ?: vertexBuffer(objVertexFormat, triangleCount * 3)
vertexBuffer.put(elementOffset) {
objects.forEach {
for (i in it.positions.indices) {
write(it.positions[i])
if (it.normals.isNotEmpty()) {
write(it.normals[i])
} else {
val d0 = it.positions[2] - it.positions[0]
val d1 = it.positions[1] - it.positions[0]
write(d0.normalized.cross(d1.normalized).normalized)
}
if (it.textureCoords.isNotEmpty()) {
write(it.textureCoords[i])
} else {
write(Vector2.ZERO)
}
}
}
}
vertexBuffer.shadow.destroy()
return vertexBuffer
}

View File

@@ -0,0 +1,103 @@
package org.openrndr.extra.objloader
import org.openrndr.color.ColorRGBa
import org.openrndr.math.Matrix44
import org.openrndr.math.Vector2
import org.openrndr.math.Vector3
import org.openrndr.shape.Box
import kotlin.math.max
import kotlin.math.min
interface IPolygon {
val positions: List<Vector3>
val normals: List<Vector3>
val textureCoords: List<Vector2>
val colors: List<ColorRGBa>
val tangents: List<Vector3>
val bitangents: List<Vector3>
fun transform(t: Matrix44): IPolygon
}
/**
* 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(
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 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)
}
/**
* Create a [MutablePolygon] by copying
*/
fun toMutablePolygon(): MutablePolygon {
return MutablePolygon(
positions.toMutableList(),
normals.toMutableList(),
textureCoords.toMutableList(),
colors.toMutableList(),
tangents.toMutableList(),
bitangents.toMutableList()
)
}
}
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 tangents: MutableList<Vector3> = mutableListOf(),
override val bitangents: MutableList<Vector3> = mutableListOf()
) : IPolygon {
override fun transform(t: Matrix44): MutablePolygon {
return MutablePolygon(
positions.map { (t * it.xyz1).div }.toMutableList(),
ArrayList(normals),
ArrayList(textureCoords),
ArrayList(colors),
ArrayList(tangents),
ArrayList(bitangents)
)
}
}
/**
* Calculates the 3D bounding box of a list of [IPolygon].
*/
fun bounds(polygons: List<IPolygon>): Box {
var minX = Double.POSITIVE_INFINITY
var minY = Double.POSITIVE_INFINITY
var minZ = Double.POSITIVE_INFINITY
var maxX = Double.NEGATIVE_INFINITY
var maxY = Double.NEGATIVE_INFINITY
var maxZ = Double.NEGATIVE_INFINITY
polygons.forEach {
it.positions.forEach { pos ->
minX = min(minX, pos.x)
minY = min(minY, pos.y)
minZ = min(minZ, pos.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)
}

View File

@@ -0,0 +1,59 @@
package org.openrndr.extra.objloader
import org.openrndr.color.ColorRGBa
import org.openrndr.math.Vector2
import org.openrndr.math.Vector3
/**
* Vertex data interface
*/
interface IVertexData {
/**
* Vertex positions
*/
val positions: List<Vector3>
/**
* Vertex normals
*/
val normals: List<Vector3>
/**
* Vertex texture coordinates
*/
val textureCoords: List<Vector2>
/**
* Vertex colors
*/
val colors: List<ColorRGBa>
/**
* Vertex tangents
*/
val tangents: List<Vector3>
/**
* Vertex bitangents
*/
val bitangents: List<Vector3>
}
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 tangents: List<Vector3> = emptyList(),
override val bitangents: List<Vector3> = emptyList()
) : IVertexData
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 tangents: MutableList<Vector3> = mutableListOf(),
override val bitangents: MutableList<Vector3> = mutableListOf()
) : IVertexData

View File

@@ -0,0 +1,13 @@
package org.openrndr.extra.objloader
import org.openrndr.math.Vector3
fun IMeshData.wireframe(): List<List<Vector3>> {
return polygons.map { ip -> ip.toPolygon(this.vertexData).positions.toList() }
}
fun ICompoundMeshData.wireframe(): List<List<Vector3>> {
return compounds.values.flatMap {
it.wireframe()
}
}

View File

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

View File

@@ -1,18 +0,0 @@
package org.openrndr.extra.objloader
@JvmRecord
data class MeshData(val vertexData: VertexData, val polygonGroups: Map<String, List<IndexedPolygon>>) {
fun triangulate(): MeshData {
return copy(polygonGroups = polygonGroups.mapValues {
it.value.flatMap { polygon -> polygon.triangulate(vertexData) }
})
}
fun flattenPolygons(): Map<String, List<Polygon>> {
return polygonGroups.mapValues {
it.value.map { ip ->
ip.toPolygon(vertexData)
}
}
}
}

View File

@@ -1,50 +0,0 @@
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 }
val vertexBuffer = vertexBuffer(objVertexFormat, triangleCount * 3)
vertexBuffer.put {
objects.entries.forEach {
it.value.forEach {
for (i in it.positions.indices) {
write(it.positions[i])
if (it.normals.isNotEmpty()) {
write(it.normals[i])
} else {
val d0 = it.positions[2] - it.positions[0]
val d1 = it.positions[1] - it.positions[0]
write(d0.normalized.cross(d1.normalized).normalized)
}
if (it.textureCoords.isNotEmpty()) {
write(it.textureCoords[i])
} else {
write(Vector2.ZERO)
}
}
}
}
}
vertexBuffer.shadow.destroy()
return vertexBuffer
}

View File

@@ -1,5 +1,6 @@
package org.openrndr.extra.objloader
import org.openrndr.color.ColorRGBa
import org.openrndr.draw.VertexBuffer
import org.openrndr.math.*
import java.io.File
@@ -10,7 +11,7 @@ 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<IPolygon>> {
return try {
val url = URL(fileOrUrl)
loadOBJ(url)
@@ -43,15 +44,16 @@ fun loadOBJEx(file: File) = loadOBJMeshData(file.readLines())
fun loadOBJ(url: URL) = loadOBJ(url.readText().split("\n"))
fun loadOBJEx(url: URL) = loadOBJMeshData(url.readText().split("\n"))
fun loadOBJ(lines: List<String>): Map<String, List<Polygon>> = loadOBJMeshData(lines).triangulate().flattenPolygons()
fun loadOBJ(lines: List<String>): Map<String, List<IPolygon>> = loadOBJMeshData(lines).triangulate().flattenPolygons()
fun loadOBJMeshData(file: File) = loadOBJMeshData(file.readLines())
fun loadOBJMeshData(lines: List<String>): MeshData {
fun loadOBJMeshData(lines: List<String>): CompoundMeshData {
val meshes = mutableMapOf<String, List<IndexedPolygon>>()
val positions = mutableListOf<Vector3>()
val normals = mutableListOf<Vector3>()
val textureCoords = mutableListOf<Vector2>()
val colors = mutableListOf<ColorRGBa>()
var activeMesh = mutableListOf<IndexedPolygon>()
lines.forEach { line ->
@@ -60,7 +62,32 @@ 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" -> {
when (tokens.size) {
3, 4 -> {
positions += Vector3(tokens[1].toDouble(), tokens[2].toDouble(), tokens[3].toDouble())
colors += ColorRGBa.WHITE
}
6 -> {
positions += Vector3(tokens[1].toDouble(), tokens[2].toDouble(), tokens[3].toDouble())
colors += ColorRGBa(tokens[4].toDouble(), tokens[5].toDouble(), tokens[6].toDouble())
}
7 -> {
positions += Vector3(tokens[1].toDouble(), tokens[2].toDouble(), tokens[3].toDouble())
colors += ColorRGBa(
tokens[4].toDouble(),
tokens[5].toDouble(),
tokens[6].toDouble(),
tokens[7].toDouble()
)
}
else -> error("vertex has ${tokens.size - 1} components, loader only supports 3/4/6/7 components")
}
}
"vn" -> normals += Vector3(tokens[1].toDouble(), tokens[2].toDouble(), tokens[3].toDouble())
"vt" -> textureCoords += Vector2(tokens[1].toDouble(), tokens[2].toDouble())
"g" -> {
@@ -75,12 +102,16 @@ fun loadOBJMeshData(lines: List<String>): MeshData {
val hasPosition = (indices[0].getOrNull(0) ?: 0) != 0
val hasUV = (indices[0].getOrNull(1) ?: 0) != 0
val hasNormal = (indices[0].getOrNull(2) ?: 0) != 0
val hasColor = hasPosition
activeMesh.add(
IndexedPolygon(
if (hasPosition) indices.map { it[0] - 1 }.toIntArray() else intArrayOf(),
if (hasUV) indices.map { it[1] - 1 }.toIntArray() else intArrayOf(),
if (hasNormal) indices.map { it[2] - 1 }.toIntArray() else intArrayOf()
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(),
emptyList(),
emptyList()
)
)
@@ -93,8 +124,11 @@ fun loadOBJMeshData(lines: List<String>): MeshData {
}
}
return MeshData(
VertexData(positions.toTypedArray(), normals.toTypedArray(), textureCoords.toTypedArray()),
meshes
val vertexData = VertexData(positions, normals, textureCoords, colors)
return CompoundMeshData(
vertexData,
meshes.mapValues {
MeshData(vertexData, it.value)
}
)
}

View File

@@ -1,56 +0,0 @@
package org.openrndr.extra.objloader
import org.openrndr.math.Matrix44
import org.openrndr.math.Vector2
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(),
val textureCoords: Array<Vector2> = emptyArray()
) {
fun transform(t: Matrix44): Polygon {
return Polygon(positions.map { (t * it.xyz1).div }.toTypedArray(), normals, textureCoords)
}
}
/**
* 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
var minZ = Double.POSITIVE_INFINITY
var maxX = Double.NEGATIVE_INFINITY
var maxY = Double.NEGATIVE_INFINITY
var maxZ = Double.NEGATIVE_INFINITY
polygons.forEach {
it.positions.forEach { pos ->
minX = min(minX, pos.x)
minY = min(minY, pos.y)
minZ = min(minZ, pos.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)
}

View File

@@ -1,10 +0,0 @@
package org.openrndr.extra.objloader
import org.openrndr.math.Vector2
import org.openrndr.math.Vector3
class VertexData(
val positions: Array<Vector3> = emptyArray(),
val normals: Array<Vector3> = emptyArray(),
val textureCoords: Array<Vector2> = emptyArray()
)

View File

@@ -1,9 +0,0 @@
package org.openrndr.extra.objloader
import org.openrndr.math.Vector3
fun MeshData.wireframe() : List<List<Vector3>> {
return polygonGroups.values.flatMap {
it.map { ip -> ip.toPolygon(this.vertexData).positions.toList() }
}
}