Files
orx/orx-mesh-generators/src/commonMain/kotlin/TriangleMeshBuilder.kt
2023-05-22 12:51:43 +02:00

645 lines
18 KiB
Kotlin

package org.openrndr.extra.meshgenerators
import org.openrndr.collections.pop
import org.openrndr.collections.push
import org.openrndr.color.ColorRGBa
import org.openrndr.draw.VertexBuffer
import org.openrndr.math.Matrix44
import org.openrndr.math.Vector2
import org.openrndr.math.Vector3
import org.openrndr.math.transforms.buildTransform
import org.openrndr.math.transforms.normalMatrix
import org.openrndr.math.transforms.rotate
import org.openrndr.shape.Shape
import org.openrndr.utils.buffer.MPPBuffer
/**
* A class that provides a simple Domain Specific Language
* to construct and deform triangle-based 3D meshes.
*
*/
class TriangleMeshBuilder {
var color = ColorRGBa.WHITE
var transform = Matrix44.IDENTITY
set(value) {
field = value
normalTransform = normalMatrix(value)
}
var normalTransform: Matrix44 = Matrix44.IDENTITY
private set
private val transformStack = ArrayDeque<Matrix44>()
/**
* Applies a three-dimensional translation to the [transform] matrix.
* Affects meshes added afterward.
*/
fun translate(x: Double, y: Double, z: Double) {
transform *= buildTransform {
translate(x, y, z)
}
}
/**
* Applies a three-dimensional translation to the [transform] matrix.
* Affects meshes added afterward.
*/
fun translate(translation: Vector3) {
transform *= buildTransform {
translate(translation)
}
}
/**
* Applies a rotation over an arbitrary axis to the [transform] matrix.
* Affects meshes added afterward.
* @param axis the axis to rotate over, will be normalized
* @param degrees the rotation in degrees
*/
fun rotate(axis: Vector3, degrees: Double) {
transform *= buildTransform {
rotate(axis, degrees)
}
}
/**
* Push the active [transform] matrix on the transform state stack.
*/
fun pushTransform() {
transformStack.push(transform)
}
/**
* Pop the active [transform] matrix from the transform state stack.
*/
fun popTransform() {
transform = transformStack.pop()
}
/**
* Pushes the [transform] matrix, calls [function] and pops.
* @param function the function that is called in the isolation
*/
fun isolated(function: TriangleMeshBuilder.() -> Unit) {
pushTransform()
function()
popTransform()
}
/**
* A container class for vertex [position], [normal], [texCoord] and
* [color].
*/
class VertexData(
val position: Vector3,
val normal: Vector3,
val texCoord: Vector2,
val color: ColorRGBa
) {
/**
* Return a new vertex with the position transformed with [transform]
* and the normal transformed with [normalTransform]. Used to
* translate, rotate or scale vertices.
*/
fun transform(
transform: Matrix44,
normalTransform: Matrix44
) = VertexData(
(transform * position.xyz1).xyz,
(normalTransform * normal.xyz0).xyz,
texCoord,
color
)
}
/**
* Vertex storage
*/
var data = mutableListOf<VertexData>()
/**
* Write new vertex data into [data]. The current [color] is used for the
* vertex.
*/
fun write(position: Vector3, normal: Vector3, texCoord: Vector2) {
data.add(
VertexData(position, normal, texCoord, color).transform(
transform,
normalTransform
)
)
}
/**
* Append [other] data into [data], combining the two meshes.
*/
fun concat(other: TriangleMeshBuilder) {
data.addAll(other.data)
}
/**
* Returns a [MPPBuffer] representation of [data] used for rendering.
*/
fun toByteBuffer(): MPPBuffer {
//val bb = ByteBuffer.allocateDirect(data.size * (3 * 4 + 3 * 4 + 2 * 4 + 4 * 4))
val bb = MPPBuffer.allocate(data.size * (3 * 4 + 3 * 4 + 2 * 4 + 4 * 4))
//bb.order(ByteOrder.nativeOrder())
bb.rewind()
for (d in data) {
bb.putFloat(d.position.x.toFloat())
bb.putFloat(d.position.y.toFloat())
bb.putFloat(d.position.z.toFloat())
bb.putFloat(d.normal.x.toFloat())
bb.putFloat(d.normal.y.toFloat())
bb.putFloat(d.normal.z.toFloat())
bb.putFloat(d.texCoord.x.toFloat())
bb.putFloat(d.texCoord.y.toFloat())
bb.putFloat(d.color.r.toFloat())
bb.putFloat(d.color.g.toFloat())
bb.putFloat(d.color.b.toFloat())
bb.putFloat(d.color.alpha.toFloat())
}
bb.rewind()
return bb
}
}
/**
* Add a sphere mesh
*
* @param sides The number of steps around its axis.
* @param segments The number of steps from pole to pole.
* @param radius The radius of the sphere.
* @param flipNormals Create an inside-out shape if true.
*/
fun TriangleMeshBuilder.sphere(
sides: Int,
segments: Int,
radius: Double,
flipNormals: Boolean = false
) {
generateSphere(sides, segments, radius, flipNormals, this::write)
}
/**
* Add a hemisphere
*
* @param sides The number of steps around its axis.
* @param segments The number of steps from pole to pole.
* @param radius The radius of the sphere.
* @param flipNormals Create an inside-out shape if true.
*/
fun TriangleMeshBuilder.hemisphere(
sides: Int,
segments: Int,
radius: Double,
flipNormals: Boolean = false
) {
generateHemisphere(sides, segments, radius, flipNormals, this::write)
}
/**
* Used by the [grid] methods. Specifies how the UV or UVW
* coordinates the user function receives are scaled.
*/
enum class GridCoordinates {
/**
* The coordinates are the cell location index as Double.
*/
INDEX,
/**
* The coordinates with the cell's location are normalized
* to the 0.0 ~ 1.0 range.
*/
UNIPOLAR,
/**
* The coordinates with the cell's location are normalized
* to the -1.0 ~ 1.0 range.
*/
BIPOLAR,
}
/**
* Create a 2D grid of [width] x [height] 3D elements.
* The [builder] function will get called with the `u` and `v`
* coordinates of each grid cell, so you have an opportunity to add meshes
* to the scene using those coordinates. The coordinate values will be scaled
* according to [coordinates]. Use:
* - [GridCoordinates.INDEX] to get UV cell indices as [Double]s.
* - [GridCoordinates.BIPOLAR] to get values between -1.0 and 1.0
* - [GridCoordinates.UNIPOLAR] to get values between 0.0 and 1.0
*/
fun TriangleMeshBuilder.grid(
width: Int,
height: Int,
coordinates: GridCoordinates = GridCoordinates.BIPOLAR,
builder: TriangleMeshBuilder.(u: Double, v: Double) -> Unit
) {
for (v in 0 until height) {
for (u in 0 until width) {
group {
when (coordinates) {
GridCoordinates.INDEX -> this.builder(u * 1.0, v * 1.0)
GridCoordinates.BIPOLAR -> this.builder(
2 * u / (width - 1.0) - 1,
2 * v / (height - 1.0) - 1
)
GridCoordinates.UNIPOLAR -> this.builder(
u / (width - 1.0),
v / (height - 1.0)
)
}
}
}
}
}
/**
* Create a 3D grid of [width] x [height] x [depth] 3D elements.
* The [builder] function will get called with the `u`, `v` and `w`
* coordinates of each grid cell, so you have an opportunity to add meshes
* to the scene using those coordinates. The coordinate values will be scaled
* according to [coordinates]. Use:
* - [GridCoordinates.INDEX] to get the UVW cell indices as [Double]s.
* - [GridCoordinates.BIPOLAR] to get values between -1.0 and 1.0
* - [GridCoordinates.UNIPOLAR] to get values between 0.0 and 1.0
*/
fun TriangleMeshBuilder.grid(
width: Int,
height: Int,
depth: Int,
coordinates: GridCoordinates = GridCoordinates.BIPOLAR,
builder: TriangleMeshBuilder.(u: Double, v: Double, w: Double) -> Unit
) {
for (w in 0 until depth) {
for (v in 0 until height) {
for (u in 0 until width) {
group {
when (coordinates) {
GridCoordinates.INDEX -> this.builder(
u * 1.0,
v * 1.0,
w * 1.0
)
GridCoordinates.BIPOLAR -> this.builder(
2 * u / (width - 1.0) - 1,
2 * v / (height - 1.0) - 1,
2 * w / (depth - 1.0) - 1
)
GridCoordinates.UNIPOLAR -> this.builder(
u / (width - 1.0),
v / (height - 1.0),
w / (depth - 1.0)
)
}
}
}
}
}
}
/**
* Twists a 3D mesh around an axis that starts at [Vector3.ZERO] and ends
* at [axis]. [degreesPerUnit] controls the amount of twist. [start] is
* currently unused.
*/
fun TriangleMeshBuilder.twist(
degreesPerUnit: Double,
start: Double,
axis: Vector3 = Vector3.UNIT_Y
) {
data = data.map {
val p = it.position.projectedOn(axis)
val t = when {
axis.x != 0.0 -> p.x / axis.x
axis.y != 0.0 -> p.y / axis.y
axis.z != 0.0 -> p.z / axis.z
else -> throw IllegalArgumentException("0 axis")
}
val r = Matrix44.rotate(axis, t * degreesPerUnit)
TriangleMeshBuilder.VertexData(
(r * it.position.xyz1).xyz,
(r * it.normal.xyz0).xyz,
it.texCoord,
this@twist.color
)
}.toMutableList()
}
/**
* Generate a box of size [width], [height] and [depth].
* Specify the number of segments with [widthSegments], [heightSegments] and
* [depthSegments]. Use [flipNormals] for an inside-out shape.
*/
fun TriangleMeshBuilder.box(
width: Double,
height: Double,
depth: Double,
widthSegments: Int = 1,
heightSegments: Int = 1,
depthSegments: Int = 1,
flipNormals: Boolean = false
) {
generateBox(
width,
height,
depth,
widthSegments,
heightSegments,
depthSegments,
flipNormals,
this::write
)
}
/**
* Generate a cylinder
*
* @param sides the number of sides of the cylinder
* @param segments the number of segments along the z-axis
* @param radius the radius of the cylinder
* @param length the length of the cylinder
* @param flipNormals generates inside-out geometry if true
* @param center center the cylinder on the z-plane
*/
fun TriangleMeshBuilder.cylinder(
sides: Int,
segments: Int,
radius: Double,
length: Double,
flipNormals: Boolean = false,
center: Boolean = false
) {
generateCylinder(
sides,
segments,
radius,
length,
flipNormals,
center,
this::write
)
}
/**
* Generate dodecahedron mesh
*
* @param radius the radius of the dodecahedron
*/
fun TriangleMeshBuilder.dodecahedron(radius: Double) {
generateDodecahedron(radius, this::write)
}
/**
* Generate a tapered cylinder along the z-axis
*
* @param sides the number of sides of the tapered cylinder
* @param segments the number of segments along the z-axis
* @param startRadius the start radius of the tapered cylinder
* @param endRadius the end radius of the tapered cylinder
* @param length the length of the tapered cylinder
* @param flipNormals generates inside-out geometry if true
* @param center centers the cylinder on the z-plane if true
*/
fun TriangleMeshBuilder.taperedCylinder(
sides: Int,
segments: Int,
startRadius: Double,
endRadius: Double,
length: Double,
flipNormals: Boolean = false,
center: Boolean = false
) {
generateTaperedCylinder(
sides,
segments,
startRadius,
endRadius,
length,
flipNormals,
center,
this::write
)
}
/**
* Generate a shape by rotating an envelope around a vertical axis.
*
* @param sides the angular resolution of the cap
* @param radius the radius of the cap
* @param envelope a list of points defining the profile of the cap.
* The default envelope is a horizontal line which produces a flat round disk.
* By providing a more complex envelope one can create curved shapes like a bowl.
*/
fun TriangleMeshBuilder.cap(
sides: Int,
radius: Double,
envelope: List<Vector2>
) {
generateCap(sides, radius, envelope, this::write)
}
/**
* Generate a shape by rotating an envelope around a vertical axis.
*
* @param sides the angular resolution of the cap
* @param length the length of the shape. A multiplier for the y component of the envelope
* @param envelope a list of points defining the profile of the shape.
* The default envelope is a vertical line which produces a hollow cylinder.
*/
fun TriangleMeshBuilder.revolve(
sides: Int,
length: Double,
envelope: List<Vector2>
) {
generateRevolve(sides, length, envelope, this::write)
}
/**
* Generate plane centered at [center], using the [right], [forward] and [up]
* vectors for its orientation.
* [width] and [height] specify the dimensions of the plane.
* [widthSegments] and [heightSegments] control the plane's number of
* segments.
*/
fun TriangleMeshBuilder.plane(
center: Vector3,
right: Vector3,
forward: Vector3,
up: Vector3,
width: Double = 1.0,
height: Double = 1.0,
widthSegments: Int = 1,
heightSegments: Int = 1
) {
generatePlane(
center,
right,
forward,
up,
width,
height,
widthSegments,
heightSegments,
this::write
)
}
/**
* Extrudes a [Shape] from its triangulations
*
* @param baseTriangles triangle vertices for the caps
* @param contours contour vertices for the sides
* @param length the length of the extrusion
* @param scale scale factor for the caps
* @param frontCap add a front cap if true
* @param backCap add a back cap if true
* @param sides add the sides if true
*/
fun TriangleMeshBuilder.extrudeShape(
baseTriangles: List<Vector2>,
contours: List<List<Vector2>>,
length: Double,
scale: Double = 1.0,
frontCap: Boolean = true,
backCap: Boolean = true,
sides: Boolean = true
) {
extrudeShape(
baseTriangles = baseTriangles,
contours = contours,
front = -length / 2.0,
back = length / 2.0,
frontScale = scale,
backScale = scale,
frontCap = frontCap,
backCap = backCap,
sides = sides,
flipNormals = false,
writer = this::write
)
}
/**
* Extrudes a [Shape]
*
* @param shape the [Shape] to extrude
* @param length length of the extrusion
* @param scale scale factor of the caps
* @param frontCap add a front cap if true
* @param backCap add a back cap if true
* @param sides add the sides if true
* @param distanceTolerance controls how many segments will be created. Lower
* values result in higher vertex counts.
*/
fun TriangleMeshBuilder.extrudeShape(
shape: Shape,
length: Double,
scale: Double = 1.0,
frontCap: Boolean = true,
backCap: Boolean = true,
sides: Boolean = true,
distanceTolerance: Double = 0.5
) {
extrudeShape(
shape = shape,
front = -length / 2.0,
back = length / 2.0,
frontScale = scale,
backScale = scale,
frontCap = frontCap,
backCap = backCap,
sides = sides,
distanceTolerance = distanceTolerance,
flipNormals = false,
writer = this::write
)
}
/**
* Extrudes a list of [Shape]
*
* @param shapes The [Shape]s to extrude
* @param length length of the extrusion
* @param scale scale factor of the caps
* @param distanceTolerance controls how many segments will be created. Lower
* values result in higher vertex counts.
*/
fun TriangleMeshBuilder.extrudeShapes(
shapes: List<Shape>,
length: Double,
scale: Double = 1.0,
distanceTolerance: Double = 0.5
) {
extrudeShapes(
shapes = shapes,
front = -length / 2.0,
back = length / 2.0,
frontScale = scale,
backScale = scale,
frontCap = true,
backCap = true,
sides = true,
distanceTolerance = distanceTolerance,
flipNormals = false,
writer = this::write
)
}
/**
* Creates a triangle mesh builder
*
* @param vertexBuffer The optional [VertexBuffer] into which to write data.
* If not provided one is created.
* @param builder A user function that adds 3D meshes to the [vertexBuffer]
* @return The populated [VertexBuffer]
*/
fun buildTriangleMesh(
vertexBuffer: VertexBuffer? = null,
builder: TriangleMeshBuilder.() -> Unit
): VertexBuffer {
val gb = TriangleMeshBuilder()
gb.builder()
val vb = vertexBuffer ?: meshVertexBufferWithColor(gb.data.size)
val bb = gb.toByteBuffer()
bb.rewind()
vb.write(bb)
return vb
}
//fun generator(
// builder: TriangleMeshBuilder.() -> Unit
//): TriangleMeshBuilder {
// val gb = TriangleMeshBuilder()
// gb.builder()
// return gb
//}
/**
* Creates a group. Can be used to avoid leaking mesh properties like `color`
* and `transform` into following meshes or groups.
*
* @param builder A user function that adds 3D meshes to the [vertexBuffer]
* @see [TriangleMeshBuilder.isolated]
*/
fun TriangleMeshBuilder.group(
builder: TriangleMeshBuilder.() -> Unit
) {
val gb = TriangleMeshBuilder()
gb.builder()
this.concat(gb)
}