commonMain.TriangleMeshBuilder.kt Maven / Gradle / Ivy
The newest version!
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()
/**
* 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()
/**
* 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,
[email protected]
)
}.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
) {
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
) {
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,
contours: List>,
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,
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)
}