Improve mesh generator (#301)

Co-authored-by: Edwin Jakobs <edwin@rndr.studio>
This commit is contained in:
Abe Pazos
2023-04-19 10:34:55 +02:00
committed by GitHub
parent a8c2f7217c
commit 03bf971ff5
31 changed files with 2639 additions and 1164 deletions

View File

@@ -0,0 +1,114 @@
package org.openrndr.extra.meshgenerators
import org.openrndr.draw.VertexBuffer
import org.openrndr.math.Vector3
/**
* Returns a Box mesh
*
* @param width the width of the box
* @param height the height of the box
* @param depth the depth of the box
* @param widthSegments the number of segments along the x-axis
* @param heightSegments the number of segments along the z-axis
* @param depthSegments the number of segments along the y-axis
* @param flipNormals generates inside-out geometry if true
* @return A vertex buffer containing the triangles to render the 3D shape
*/
fun boxMesh(
width: Double = 1.0,
height: Double = 1.0,
depth: Double = 1.0,
widthSegments: Int = 1,
heightSegments: Int = 1,
depthSegments: Int = 1,
flipNormals: Boolean = false
): VertexBuffer {
val vb = meshVertexBuffer(
widthSegments * heightSegments * 6 * 2 +
widthSegments * depthSegments * 6 * 2 +
heightSegments * depthSegments * 6 * 2
)
vb.put {
generateBox(
width, height, depth,
widthSegments, heightSegments, depthSegments,
flipNormals, bufferWriter(this)
)
}
return vb
}
/**
* Generate a box
*
* @param width the width of the box
* @param height the height of the box
* @param depth the depth of the box
* @param widthSegments the number of segments along the x-axis
* @param heightSegments the number of segments along the z-axis
* @param depthSegments the number of segments along the y-axis
* @param flipNormals generates inside-out geometry if true
* @param writer the vertex writer function
*/
fun generateBox(
width: Double = 1.0,
height: Double = 1.0,
depth: Double = 1.0,
widthSegments: Int = 1,
heightSegments: Int = 1,
depthSegments: Int = 1,
flipNormals: Boolean = false,
writer: VertexWriter
) {
val sign = if (flipNormals) -1.0 else 1.0
// +x -- ZY
generatePlane(
Vector3(width / 2.0 * sign, 0.0, 0.0),
Vector3.UNIT_Z, Vector3.UNIT_Y, Vector3.UNIT_X,
-depth, -height,
depthSegments, heightSegments, writer
)
// -x -- ZY
generatePlane(
Vector3(-width / 2.0 * sign, 0.0, 0.0),
Vector3.UNIT_Z, Vector3.UNIT_Y, -Vector3.UNIT_X,
-depth, height,
depthSegments, heightSegments, writer
)
// +y -- XZ
generatePlane(
Vector3(0.0, height / 2.0 * sign, 0.0),
Vector3.UNIT_X, Vector3.UNIT_Z, Vector3.UNIT_Y,
width, depth,
widthSegments, depthSegments, writer
)
// -y -- XZ
generatePlane(
Vector3(0.0, -height / 2.0 * sign, 0.0),
Vector3.UNIT_X, Vector3.UNIT_Z, -Vector3.UNIT_Y,
width, -depth,
widthSegments, depthSegments, writer
)
// +z -- XY
generatePlane(
Vector3(0.0, 0.0, depth / 2.0 * sign),
Vector3.UNIT_X, Vector3.UNIT_Y, Vector3.UNIT_Z,
-width, height,
widthSegments, heightSegments, writer
)
// -z -- XY
generatePlane(
Vector3(0.0, 0.0, -depth / 2.0 * sign),
Vector3.UNIT_X, Vector3.UNIT_Y, -Vector3.UNIT_Z,
width, height,
widthSegments, heightSegments, writer
)
}

View File

@@ -0,0 +1,175 @@
package org.openrndr.extra.meshgenerators
import org.openrndr.draw.VertexBuffer
import org.openrndr.math.Matrix44
import org.openrndr.math.Vector2
import org.openrndr.math.Vector3
import org.openrndr.math.YPolarity
import org.openrndr.math.transforms.rotateY
/**
* A shape created 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.
* @return A vertex buffer containing the triangles to render the 3D shape
*/
fun capMesh(
sides: Int,
radius: Double,
envelope: List<Vector2> = listOf(
Vector2(0.0, 0.0),
Vector2(1.0, 0.0)
)
): VertexBuffer {
val vb = meshVertexBuffer(6 * sides * (envelope.size - 1))
vb.put {
generateCap(sides, radius, envelope, bufferWriter(this))
}
return vb
}
/**
* 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.
* @param writer the vertex writer function
*/
fun generateCap(
sides: Int,
radius: Double,
envelope: List<Vector2> = listOf(
Vector2(0.0, 0.0),
Vector2(1.0, 0.0)
),
writer: VertexWriter
) {
val maxX = envelope.maxByOrNull { it.x } ?: Vector2(1.0, 0.0)
val a = maxX.x
val cleanEnvelope = envelope.map { Vector2((it.x / a) * radius, it.y) }
val normals2D = envelope.zipWithNext().map {
val d = it.second - it.first
d.normalized.perpendicular(YPolarity.CCW_POSITIVE_Y)
}
val basePositions = cleanEnvelope.map { Vector3(it.x, it.y, 0.0) }
val baseNormals = normals2D.map { Vector3(it.x, it.y, 0.0) }
for (side in 0 until sides) {
val r0 = Matrix44.rotateY(360.0 / sides * side)
val r1 = Matrix44.rotateY(360.0 / sides * (side + 1))
val v0 = basePositions.map { (r0 * it.xyz0).xyz }
val v1 = basePositions.map { (r1 * it.xyz0).xyz }
val n0 = baseNormals.map { (r0 * it.xyz0).xyz }
val n1 = baseNormals.map { (r1 * it.xyz0).xyz }
for (segment in 0 until basePositions.size - 1) {
val p00 = v0[segment]
val p01 = v0[segment + 1]
val p10 = v1[segment]
val p11 = v1[segment + 1]
val nn0 = n0[segment]
val nn1 = n1[segment]
writer(p00, nn0, Vector2.ZERO)
writer(p01, nn0, Vector2.ZERO)
writer(p11, nn1, Vector2.ZERO)
writer(p11, nn1, Vector2.ZERO)
writer(p10, nn1, Vector2.ZERO)
writer(p00, nn0, Vector2.ZERO)
}
}
}
/**
* A shape created 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.
* @return A vertex buffer containing the triangles to render the 3D shape
*/
fun revolveMesh(
sides: Int,
length: Double,
envelope: List<Vector2> = listOf(
Vector2(1.0, 0.0),
Vector2(1.0, 1.0)
)
): VertexBuffer {
val vb = meshVertexBuffer(6 * sides * (envelope.size - 1))
vb.put {
generateRevolve(sides, length, envelope, bufferWriter(this))
}
return vb
}
/**
* 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.
* @param writer the vertex writer function
*/
fun generateRevolve(
sides: Int,
length: Double,
envelope: List<Vector2> = listOf(
Vector2(1.0, 0.0),
Vector2(1.0, 1.0)
),
writer: VertexWriter
) {
val maxY = envelope.maxByOrNull { it.y } ?: Vector2(0.0, 1.0)
val a = maxY.y
val cleanEnvelope = envelope.map { Vector2((it.x), (it.y / a - 0.5) * length) }
val normals2D = envelope.zipWithNext().map {
val d = it.second - it.first
d.normalized.perpendicular() * Vector2(1.0, -1.0)
}
val basePositions = cleanEnvelope.map { Vector3(it.x, it.y, 0.0) }
val baseNormals = normals2D.map { Vector3(it.x, it.y, 0.0) }
for (side in 0 until sides) {
val r0 = Matrix44.rotateY(360.0 / sides * side)
val r1 = Matrix44.rotateY(360.0 / sides * (side + 1))
val v0 = basePositions.map { (r0 * it.xyz0).xyz }
val v1 = basePositions.map { (r1 * it.xyz0).xyz }
val n0 = baseNormals.map { (r0 * it.xyz0).xyz }
val n1 = baseNormals.map { (r1 * it.xyz0).xyz }
for (segment in 0 until basePositions.size - 1) {
val p00 = v0[segment]
val p01 = v0[segment + 1]
val p10 = v1[segment]
val p11 = v1[segment + 1]
val nn0 = n0[segment]
val nn1 = n1[segment]
writer(p00, nn0, Vector2.ZERO)
writer(p10, nn1, Vector2.ZERO)
writer(p11, nn1, Vector2.ZERO)
writer(p11, nn1, Vector2.ZERO)
writer(p01, nn0, Vector2.ZERO)
writer(p00, nn0, Vector2.ZERO)
}
}
}

View File

@@ -0,0 +1,141 @@
package org.openrndr.extra.meshgenerators
import org.openrndr.draw.VertexBuffer
import org.openrndr.math.Matrix44
import org.openrndr.math.Vector2
import org.openrndr.math.Vector3
import org.openrndr.math.mix
import org.openrndr.math.transforms.rotateZ
import kotlin.math.PI
import kotlin.math.cos
import kotlin.math.sin
/**
* Creates a cylinder along the z-axis
*
* @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
* @return A vertex buffer containing the triangles to render the 3D shape
*/
fun cylinderMesh(
sides: Int = 16,
segments: Int = 16,
radius: Double = 1.0,
length: Double,
flipNormals: Boolean = false,
center: Boolean = false,
): VertexBuffer {
val vertexCount = 6 * sides * segments
val vb = meshVertexBuffer(vertexCount)
vb.put {
generateCylinder(sides, segments, radius, length, flipNormals, center, bufferWriter(this))
}
return vb
}
/**
* Generate a cylinder along the z-axis
* @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 centers the cylinder on the z-plane if true
* @param writer the vertex writer function
*/
fun generateCylinder(
sides: Int,
segments: Int,
radius: Double,
length: Double,
flipNormals: Boolean = false,
center: Boolean = false,
writer: VertexWriter
) = generateTaperedCylinder(sides, segments, radius, radius, length, flipNormals, center, writer)
/**
* 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 radiusStart the start radius of the tapered cylinder
* @param radiusEnd 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
* @param writer the vertex writer function
*/
fun generateTaperedCylinder(
sides: Int,
segments: Int,
radiusStart: Double,
radiusEnd: Double,
length: Double,
flipNormals: Boolean = false,
center: Boolean = false,
writer: VertexWriter
) {
val dphi = (PI * 2) / sides
val ddeg = (360.0) / sides
val invertFactor = if (flipNormals) 1.0 else -1.0
val dr = radiusEnd - radiusStart
val baseNormal = Vector2(length, dr).normalized.perpendicular().let { Vector3(x = it.y, y = 0.0, z = it.x) }
val zOffset = if (center) -length / 2.0 else 0.0
for (segment in 0 until segments) {
val radius0 = mix(radiusStart, radiusEnd, segment * 1.0 / segments)
val radius1 = mix(radiusStart, radiusEnd, (segment + 1) * 1.0 / segments)
val z0 = (length / segments) * segment + zOffset
val z1 = (length / segments) * (segment + 1) + zOffset
for (side in 0 until sides) {
val x00 = cos(side * dphi) * radius0
val x10 = cos(side * dphi + dphi) * radius0
val y00 = sin(side * dphi) * radius0
val y10 = sin(side * dphi + dphi) * radius0
val x01 = cos(side * dphi) * radius1
val x11 = cos(side * dphi + dphi) * radius1
val y01 = sin(side * dphi) * radius1
val y11 = sin(side * dphi + dphi) * radius1
val u0 = (segment + 0.0) / segments
val u1 = (segment + 1.0) / segments
val v0 = (side + 0.0) / sides
val v1 = (side + 1.0) / sides
val n0 = (Matrix44.rotateZ(side * ddeg) * baseNormal.xyz0).xyz.normalized * invertFactor
val n1 = (Matrix44.rotateZ((side + 1) * ddeg) * baseNormal.xyz0).xyz.normalized * invertFactor
if (flipNormals) {
writer(Vector3(x00, y00, z0), n0, Vector2(u0, v0))
writer(Vector3(x10, y10, z0), n1, Vector2(u0, v1))
writer(Vector3(x11, y11, z1), n1, Vector2(u1, v1))
writer(Vector3(x11, y11, z1), n1, Vector2(u1, v1))
writer(Vector3(x01, y01, z1), n0, Vector2(u1, v0))
writer(Vector3(x00, y00, z0), n0, Vector2(u0, v0))
} else {
writer(Vector3(x00, y00, z0), n0, Vector2(u0, v0))
writer(Vector3(x01, y01, z1), n0, Vector2(u1, v0))
writer(Vector3(x11, y11, z1), n1, Vector2(u1, v1))
writer(Vector3(x11, y11, z1), n1, Vector2(u1, v1))
writer(Vector3(x10, y10, z0), n1, Vector2(u0, v1))
writer(Vector3(x00, y00, z0), n0, Vector2(u0, v0))
}
}
}
}

View File

@@ -0,0 +1,94 @@
package org.openrndr.extra.meshgenerators
import org.openrndr.draw.VertexBuffer
import org.openrndr.math.Vector2
import org.openrndr.math.Vector3
import kotlin.math.sqrt
// Based on
// https://github.com/mrdoob/three.js/blob/master/src/geometries/DodecahedronGeometry.js
/**
* A dodecahedron mesh
*
* @param radius the radius of the dodecahedron
* @return A vertex buffer containing the triangles to render the 3D shape
*/
fun dodecahedronMesh(
radius: Double = 1.0
): VertexBuffer {
val vb = meshVertexBuffer(12 * 3 * 3)
vb.put {
generateDodecahedron(radius, bufferWriter(this))
}
return vb
}
/**
* Generate dodecahedron mesh
*
* @param radius the radius of the dodecahedron
* @param writer the vertex writer function
*/
fun generateDodecahedron(
radius: Double = 1.0,
writer: VertexWriter
) {
val t = (1.0 + sqrt(5.0)) / 2
val r = 1 / t
val vertices = listOf(
// (±1, ±1, ±1)
-1.0, -1.0, -1.0, -1.0, -1.0, 1.0,
-1.0, 1.0, -1.0, -1.0, 1.0, 1.0,
1.0, -1.0, -1.0, 1.0, -1.0, 1.0,
1.0, 1.0, -1.0, 1.0, 1.0, 1.0,
// (0, ±1/φ, ±φ)
0.0, -r, -t, 0.0, -r, t,
0.0, r, -t, 0.0, r, t,
// (±1/φ, ±φ, 0)
-r, -t, 0.0, -r, t, 0.0,
r, -t, 0.0, r, t, 0.0,
// (±φ, 0, ±1/φ)
-t, 0.0, -r, t, 0.0, -r,
-t, 0.0, r, t, 0.0, r
);
val indices = listOf(
3, 11, 7, 3, 7, 15, 3, 15, 13,
7, 19, 17, 7, 17, 6, 7, 6, 15,
17, 4, 8, 17, 8, 10, 17, 10, 6,
8, 0, 16, 8, 16, 2, 8, 2, 10,
0, 12, 1, 0, 1, 18, 0, 18, 16,
6, 10, 2, 6, 2, 13, 6, 13, 15,
2, 16, 18, 2, 18, 3, 2, 3, 13,
18, 1, 9, 18, 9, 11, 18, 11, 3,
4, 14, 12, 4, 12, 0, 4, 0, 8,
11, 9, 5, 11, 5, 19, 11, 19, 7,
19, 5, 14, 19, 14, 4, 19, 4, 17,
1, 12, 14, 1, 14, 5, 1, 5, 9
);
// TODO: assign texture uv coordinates
// cylindrical? spherical?
// billboarding pentagons?
// unwrap?
val uv = Vector2(0.0)
val ii = indices.iterator()
while (ii.hasNext()) {
val tri = List(3) {
val i = ii.next()
Vector3(vertices[i * 3],
vertices[i * 3 + 1],
vertices[i * 3 + 2]) * radius
}
val up = (tri[1] - tri[0]).cross(tri[2] - tri[0]).normalized
writer(tri[0], up, uv)
writer(tri[1], up, uv)
writer(tri[2], up, uv)
}
}

View File

@@ -0,0 +1,445 @@
package org.openrndr.extra.meshgenerators
import org.openrndr.math.Matrix44
import org.openrndr.math.Vector2
import org.openrndr.math.Vector3
import org.openrndr.math.Vector4
import org.openrndr.math.transforms.normalMatrix
import org.openrndr.shape.Path3D
import org.openrndr.shape.Shape
import org.openrndr.shape.ShapeContour
import org.openrndr.shape.Triangle
/**
* Writes two triangles to [writer] representing
* the quad formed by four vertices.
*
* @param v00 vertex (0, 0)
* @param v01 vertex (0, 1)
* @param v10 vertex (1, 0)
* @param v11 vertex (1, 1)
* @param faceNormal the face normal
* @param writer the vertex writer function
*/
fun quadToTris(
v00: Vector3,
v01: Vector3,
v10: Vector3,
v11: Vector3,
faceNormal: Vector3,
writer: VertexWriter
) {
writer(v11, faceNormal, Vector2.ZERO)
writer(v01, faceNormal, Vector2.ZERO)
writer(v00, faceNormal, Vector2.ZERO)
writer(v00, faceNormal, Vector2.ZERO)
writer(v10, faceNormal, Vector2.ZERO)
writer(v11, faceNormal, Vector2.ZERO)
}
/**
* Writes quads to [writer] creating a surface that connects two
* displaced instances of [linearContour]. The positions and orientations
* of the two contours are defined by the [frame0] and [frame1] matrices.
*
* @param linearContour the cross-section of the surface to create
* @param frame0 a transformation matrix that defines an initial position
* @param frame1 a transformation matrix that defines a final position
* @param writer the vertex writer function
*/
fun contourSegment(
linearContour: List<Vector3>,
frame0: Matrix44,
frame1: Matrix44,
writer: VertexWriter
) {
for (i in linearContour.indices) {
val v0 = linearContour[i]
val v1 = linearContour[(i + 1).mod(linearContour.size)]
val v00 = (frame0 * v0.xyz1).xyz
val v01 = (frame0 * v1.xyz1).xyz
val v10 = (frame1 * v0.xyz1).xyz
val v11 = (frame1 * v1.xyz1).xyz
val faceNormal = ((v10 - v00).normalized cross (v01 - v00).normalized).normalized
quadToTris(v00, v01, v10, v11, faceNormal, writer)
}
}
/**
* Writes a list of triangles transformed by the [frame]
* transformation matrix into [writer].
*
* @param triangulation the list of triangles to write
* @param frame a transformation matrix to apply to each triangle
* @param flipNormals generates inside-out geometry if true
* @param writer the vertex writer function
*/
fun triangulationWithFrame(
triangulation: List<Triangle>,
frame: Matrix44,
flipNormals: Boolean = true,
writer: VertexWriter
) {
val normalFrame = normalMatrix(frame)
val normalScale = if (!flipNormals) -1.0 else 1.0
val normal = ((normalFrame * Vector4(0.0, 0.0, normalScale, 0.0)).xyz)
for (triangle in triangulation) {
val t = if (!flipNormals) triangle else Triangle(triangle.x3, triangle.x2, triangle.x1)
writer((frame * t.x1.xy01).xyz, normal, Vector2.ZERO)
writer((frame * t.x2.xy01).xyz, normal, Vector2.ZERO)
writer((frame * t.x3.xy01).xyz, normal, Vector2.ZERO)
}
}
/**
* Extrude a [contour] along a [path] specifying the number of steps.
*
* @param contour the cross-section of the mesh
* @param path the 3D path
* @param stepCount the number of steps along the [path]
* @param up0 the initial up-vector
* @param contourDistanceTolerance precision for calculating steps along
* [contour]. Lower tolerance results in higher precision.
* @param pathDistanceTolerance precision for calculating steps along
* [path]. Lower tolerance results in higher precision.
* @param steps the resulting positions in the path
* @param frames a list of matrices holding the transformation matrices along
* the path
* @param startCap adds a start cap if set to true
* @param endCap adds an end cap if set to true
* @param writer the vertex writer function
*/
fun extrudeContourSteps(
contour: ShapeContour,
path: Path3D,
stepCount: Int,
up0: Vector3,
contourDistanceTolerance: Double = 0.5,
pathDistanceTolerance: Double = 0.5,
steps: List<Vector3> = path.equidistantPositions(
stepCount,
pathDistanceTolerance
),
frames: List<Matrix44> = steps.frames(up0),
startCap: Boolean = true,
endCap: Boolean = true,
writer: VertexWriter
) {
val linearContour = contour.sampleLinear(contourDistanceTolerance)
val linearContourPoints = linearContour.adaptivePositions().map { it.xy0 }
val finalFrames = if (path.closed) frames + frames.first() else frames
// First add caps
extrudeCaps(linearContour.shape, path, startCap, endCap, frames, writer)
// Then add sides
finalFrames.windowed(2, 1).forEach {
contourSegment(linearContourPoints, it[0], it[1], writer)
}
}
/**
* Adds caps to an extruded shape
*
* @param linearShape the cross-section of the mesh
* @param path the 3D path
* @param startCap adds a start cap if set to true
* @param endCap adds an end cap if set to true
* @param frames a list of matrices holding the transformation matrices along
* the path
* @param writer the vertex writer function
*/
private fun extrudeCaps(
linearShape: Shape,
path: Path3D,
startCap: Boolean,
endCap: Boolean,
frames: List<Matrix44>,
writer: VertexWriter
) {
if ((startCap || endCap) && !path.closed) {
val capTriangles = linearShape.triangulation
if (startCap) {
triangulationWithFrame(capTriangles, frames.first(), false, writer)
}
if (endCap) {
triangulationWithFrame(capTriangles, frames.last(), true, writer)
}
}
}
/**
* Extrude a [contour] along a [path]. The number of resulting steps
* along the path depends on the tolerance values.
*
* @param contour the cross-section of the mesh
* @param path the 3D path
* @param up0 the initial up-vector
* @param contourDistanceTolerance precision for calculating steps along
* [contour]. Lower tolerance results in higher precision and step count.
* @param pathDistanceTolerance precision for calculating steps along
* [path]. Lower tolerance results in higher precision and step count.
* @param steps the resulting positions in the path
* @param frames a list of matrices holding the transformation matrices along
* the path
* @param startCap adds a start cap if set to true
* @param endCap adds an end cap if set to true
* @param writer the vertex writer function
*/
fun extrudeContourAdaptive(
contour: ShapeContour,
path: Path3D,
up0: Vector3,
contourDistanceTolerance: Double = 0.5,
pathDistanceTolerance: Double = 0.5,
steps: List<Vector3> = path.adaptivePositions(pathDistanceTolerance),
frames: List<Matrix44> = steps.frames(up0),
startCap: Boolean = true,
endCap: Boolean = true,
writer: VertexWriter
) {
val linearContour = contour.sampleLinear(contourDistanceTolerance)
val linearContourPoints = linearContour.adaptivePositions().map { it.xy0 }
val finalFrames = if (path.closed) frames + frames.first() else frames
// First add caps
extrudeCaps(linearContour.shape, path, startCap, endCap, finalFrames, writer)
// Then add sides
finalFrames.windowed(2, 1).forEach {
contourSegment(linearContourPoints, it[0], it[1], writer)
}
}
/**
* Extrude a [shape] along a [path] specifying the number of steps.
*
* @param shape the cross-section of the mesh
* @param path the 3D path
* @param stepCount the number of steps along the [path]
* @param up0 the initial up-vector
* @param contourDistanceTolerance precision for calculating steps along
* [shape]. Lower tolerance results in higher precision.
* @param pathDistanceTolerance precision for calculating steps along
* [path]. Lower tolerance results in higher precision.
* @param steps the resulting positions in the path
* @param frames a list of matrices holding the transformation matrices along
* the path
* @param startCap adds a start cap if set to true
* @param endCap adds an end cap if set to true
* @param writer the vertex writer function
*/
fun extrudeShapeSteps(
shape: Shape,
path: Path3D,
stepCount: Int,
up0: Vector3,
contourDistanceTolerance: Double = 0.5,
pathDistanceTolerance: Double = 0.5,
steps: List<Vector3> = path.equidistantPositions(stepCount, pathDistanceTolerance),
frames: List<Matrix44> = steps.frames(up0),
startCap: Boolean,
endCap: Boolean,
writer: VertexWriter
) {
val linearShape = Shape(shape.contours.map { it.contour.sampleLinear(contourDistanceTolerance) })
// First add caps
extrudeCaps(linearShape, path, startCap, endCap, frames, writer)
// Then add sides
for (contour in linearShape.contours) {
extrudeContourSteps(
contour,
path,
stepCount,
up0,
contourDistanceTolerance,
pathDistanceTolerance,
steps,
frames,
startCap = false,
endCap = false,
writer
)
}
}
/**
* Extrude a [shape] along a [path]. The number of resulting steps
* along the path depends on the tolerance values.
*
* @param shape the cross-section of the mesh
* @param path the 3D path
* @param up0 the initial up-vector
* @param contourDistanceTolerance precision for calculating steps along
* [shape]. Lower tolerance results in higher precision and step count.
* @param pathDistanceTolerance precision for calculating steps along
* [path]. Lower tolerance results in higher precision and step count.
* @param steps the resulting positions in the path
* @param frames a list of matrices holding the transformation matrices along
* the path
* @param startCap adds a start cap if set to true
* @param endCap adds an end cap if set to true
* @param writer the vertex writer function
*/
fun extrudeShapeAdaptive(
shape: Shape,
path: Path3D,
up0: Vector3,
contourDistanceTolerance: Double = 0.5,
pathDistanceTolerance: Double = 0.5,
steps: List<Vector3> = path.adaptivePositions(pathDistanceTolerance),
frames: List<Matrix44> = steps.frames(up0),
startCap: Boolean,
endCap: Boolean,
writer: VertexWriter
) {
val linearShape = Shape(shape.contours.map { it.contour.sampleLinear(contourDistanceTolerance) })
// First add caps
extrudeCaps(linearShape, path, startCap, endCap, frames, writer)
// Then add sides
for (contour in linearShape.contours) {
extrudeContourAdaptive(
contour,
path,
up0,
contourDistanceTolerance,
pathDistanceTolerance,
steps,
frames,
startCap,
endCap,
writer
)
}
}
/**
* Extrude a [shape] along a [path] specifying the number of steps.
*
* @param shape the cross-section of the mesh
* @param path the 3D path
* @param stepCount the number of steps along the [path]
* @param up0 the up-vector
* @param contourDistanceTolerance precision for calculating steps along
* [shape]. Lower tolerance results in higher precision.
* @param pathDistanceTolerance precision for calculating steps along
* [path]. Lower tolerance results in higher precision.
* @param startCap adds a start cap if set to true
* @param endCap adds an end cap if set to true
*/
fun TriangleMeshBuilder.extrudeShapeSteps(
shape: Shape,
path: Path3D,
stepCount: Int,
up0: Vector3,
contourDistanceTolerance: Double = 0.5,
pathDistanceTolerance: Double = 0.5,
startCap: Boolean = true,
endCap: Boolean = true,
) = extrudeShapeSteps(
shape,
path,
stepCount,
up0,
contourDistanceTolerance,
pathDistanceTolerance,
startCap = startCap,
endCap = endCap,
writer = this::write
)
/**
* Extrude a [shape] along a [path]. The number of resulting steps
* along the path depends on the tolerance values.
*
* @param shape the cross-section of the mesh
* @param path the 3D path
* @param up0 the initial up-vector
* @param contourDistanceTolerance precision for calculating steps along
* [shape]. Lower tolerance results in higher precision and step count.
* @param pathDistanceTolerance precision for calculating steps along
* [path]. Lower tolerance results in higher precision and step count.
* @param startCap adds a start cap if set to true
* @param endCap adds an end cap if set to true
*/
fun TriangleMeshBuilder.extrudeShapeAdaptive(
shape: Shape,
path: Path3D,
up0: Vector3,
contourDistanceTolerance: Double = 0.5,
pathDistanceTolerance: Double = 0.5,
startCap: Boolean = true,
endCap: Boolean = true
) = extrudeShapeAdaptive(
shape,
path,
up0,
contourDistanceTolerance,
pathDistanceTolerance,
startCap = startCap,
endCap = endCap,
writer = this::write
)
/**
* Extrude a [contour] along a [path] specifying the number of steps.
*
* @param contour the cross-section of the mesh
* @param path the 3D path
* @param stepCount the number of steps along the [path]
* @param up0 the initial up-vector
* @param contourDistanceTolerance precision for calculating steps along
* [contour]. Lower tolerance results in higher precision.
* @param pathDistanceTolerance precision for calculating steps along
* [path]. Lower tolerance results in higher precision.
*/
fun TriangleMeshBuilder.extrudeContourSteps(
contour: ShapeContour,
path: Path3D,
stepCount: Int,
up0: Vector3,
contourDistanceTolerance: Double = 0.5,
pathDistanceTolerance: Double = 0.5,
) = extrudeContourSteps(
contour,
path,
stepCount,
up0,
contourDistanceTolerance,
pathDistanceTolerance,
writer = this::write
)
/**
* Extrude a [contour] along a [path]. The number of resulting steps
* along the path depends on the tolerance values.
*
* @param contour the cross-section of the shape
* @param path the 3D path
* @param up0 the up-vector
* @param contourDistanceTolerance precision for calculating steps along
* [contour]. Lower tolerance results in higher precision and step count.
* @param pathDistanceTolerance precision for calculating steps along
* [path]. Lower tolerance results in higher precision and step count.
*/
fun TriangleMeshBuilder.extrudeContourAdaptive(
contour: ShapeContour,
path: Path3D,
up0: Vector3,
contourDistanceTolerance: Double = 0.5,
pathDistanceTolerance: Double = 0.5
) = extrudeContourAdaptive(
contour,
path,
up0,
contourDistanceTolerance,
pathDistanceTolerance,
writer = this::write
)

View File

@@ -0,0 +1,60 @@
package org.openrndr.extra.meshgenerators
import org.openrndr.math.Matrix44
import org.openrndr.math.Vector3
import org.openrndr.math.Vector4
import org.openrndr.math.transforms.buildTransform
/**
* Calculate frames (pose matrices) using parallel transport
* @param up0 initial up vector, should not be collinear with `this[1] - this[0]`
*/
fun List<Vector3>.frames(up0: Vector3): List<Matrix44> {
val result = mutableListOf<Matrix44>()
if (this.isEmpty()) {
return emptyList()
}
if (this.size == 1) {
return listOf(Matrix44.IDENTITY)
}
var up = up0.normalized
run {
val current = this[0]
val next = this[1]
val forward = (next - current).normalized
val right = (forward cross up).normalized
up = ((right cross forward)).normalized
result.add(Matrix44.fromColumnVectors(right.xyz0, up.xyz0, forward.xyz0, current.xyz1))
}
require(up.length > 0.0) { "initial `up.length` is zero in .frames()" }
for (i in 1 until size - 1) {
val prev = this[i - 1]
val current = this[i]
val next = this[i + 1]
val f1 = (next - current).normalized
val f0 = (current - prev).normalized
val forward = (f0 + f1).normalized
require(forward.length > 0.0) { "`forward.length` is zero in .frames()" }
val right = (forward cross up).normalized
up = ((right cross forward)).normalized
require(up.length > 0.0) { "`up.length` is zero in .frames()" }
require(right.length > 0.0) { "`right.length` is zero in .frames()" }
//val m = Matrix44.fromColumnVectors(right.xyz0, up.xyz0, forward.xyz0, current.xyz1)
val m = buildTransform {
translate(current)
multiply(Matrix44.fromColumnVectors(right.xyz0, up.xyz0, forward.xyz0, Vector4.UNIT_W))
}
result.add(m)
}
return result
}

View File

@@ -0,0 +1,280 @@
package org.openrndr.extra.meshgenerators
import org.openrndr.draw.BufferWriter
import org.openrndr.draw.VertexBuffer
import org.openrndr.draw.vertexBuffer
import org.openrndr.draw.vertexFormat
import org.openrndr.math.Vector2
import org.openrndr.math.Vector3
import org.openrndr.math.mod
import org.openrndr.shape.Shape
import org.openrndr.shape.triangulate
/**
* Vertex writer function interface
*/
typealias VertexWriter = (position: Vector3, normal: Vector3, texCoord: Vector2) -> Unit
/**
* create a [VertexWriter] that writes into a [java.nio.ByteBuffer] through [BufferWriter]
*/
fun bufferWriter(bw: BufferWriter): VertexWriter {
return { p, n, t ->
bw.write(p)
bw.write(n)
bw.write(t)
}
}
/**
* Creates a [VertexBuffer] that is suited for holding meshes.
* Each vertex contains:
* - `position` (vec3)
* - `normal` (vec3)
* - `textureCoordinate` (vec2)
*/
fun meshVertexBuffer(size: Int): VertexBuffer {
return vertexBuffer(vertexFormat {
position(3)
normal(3)
textureCoordinate(2)
}, size)
}
/**
* Creates a [VertexBuffer] that is suited for holding meshes.
* Each vertex contains:
* - `position` (vec3)
* - `normal` (vec3)
* - `textureCoordinate` (vec2)
* - `color` (vec4)
*/
fun meshVertexBufferWithColor(size: Int): VertexBuffer {
return vertexBuffer(vertexFormat {
position(3)
normal(3)
textureCoordinate(2)
color(4)
}, size)
}
@Deprecated("binary compatibility only")
fun extrudeShape(
shape: Shape,
front: Double,
back: Double,
distanceTolerance: Double = 0.5,
writer: VertexWriter
) {
extrudeShape(
shape,
front,
back,
distanceTolerance = distanceTolerance,
flipNormals = false,
writer = writer
)
}
/**
* Extrudes a [Shape] from its triangulations
*
* @param baseTriangles triangle vertices for the caps
* @param contours contour vertices for the sides
* @param front the `z` position of the front
* @param back the `z` position of the back
* @param frontScale scale factor for the front cap
* @param backScale scale factor for the back cap
* @param frontCap add a front cap if true
* @param backCap add a back cap if true
* @param sides add the sides if true
* @param flipNormals generates inside-out geometry if true
* @param writer the vertex writer function
*/
fun extrudeShape(
baseTriangles: List<Vector2>,
contours: List<List<Vector2>>,
front: Double,
back: Double,
frontScale: Double = 1.0,
backScale: Double = 1.0,
frontCap: Boolean = true,
backCap: Boolean = true,
sides: Boolean = true,
flipNormals: Boolean = false,
writer: VertexWriter
) {
val depth = back - front
val flip = if (flipNormals) 1.0 else -1.0
run {
val normal = Vector3(0.0, 0.0, depth).normalized * flip
val negativeNormal = normal * -1.0
if (frontCap) {
baseTriangles.reversed().forEach {
writer(
(it * frontScale).vector3(z = front),
normal,
Vector2.ZERO
)
}
}
if (backCap) {
baseTriangles.forEach {
writer(
(it * backScale).vector3(z = back),
negativeNormal,
Vector2.ZERO
)
}
}
}
if (sides) {
contours.forEach {
val points = it
val normals = (points.indices).map { index ->
val a = mod(index + 1, points.size)
val b = mod(index - 1, points.size)
(points[a] - points[b]).safeNormalized * -flip
}
val forward = Vector3(0.0, 0.0, depth)
val base = Vector3(0.0, 0.0, front)
var offset = 0.0
(points zip normals).zipWithNext().forEach { (left, right) ->
val width = right.first.distanceTo(left.first)
val frontRight = (right.first * frontScale).xy0 + base
val frontLeft = (left.first * frontScale).xy0 + base
val backRight = (right.first * backScale).xy0 + base + forward
val backLeft = (left.first * backScale).xy0 + base + forward
val height = frontRight.distanceTo(backRight)
val backRightUV = Vector2(offset + width, 0.0)
val backLeftUV = Vector2(offset, 0.0)
val frontLeftUV = Vector2(offset, height)
val frontRightUV = Vector2(offset + width, height)
val lnormal =
(frontLeft - backLeft).normalized.cross(left.second.xy0)
val rnormal =
(frontRight - backRight).normalized.cross(right.second.xy0)
writer(frontLeft, lnormal, frontLeftUV)
writer(frontRight, rnormal, frontRightUV)
writer(backRight, rnormal, backRightUV)
writer(backRight, rnormal, backRightUV)
writer(backLeft, lnormal, backLeftUV)
writer(frontLeft, lnormal, frontLeftUV)
offset += width
}
}
}
}
/**
* Extrudes a [shape] by triangulating it and creating side- and cap geometry.
* @param front the `z` position of the front
* @param back the `z` position of the back
* @param frontScale scale factor for the front cap
* @param backScale scale factor for the back cap
* @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
* @param flipNormals generates inside-out geometry if true
* @param writer the vertex writer function
*/
fun extrudeShape(
shape: Shape,
front: Double,
back: Double,
frontScale: Double = 1.0,
backScale: Double = 1.0,
frontCap: Boolean = true,
backCap: Boolean = true,
sides: Boolean = true,
distanceTolerance: Double = 0.5,
flipNormals: Boolean = false,
writer: VertexWriter
) {
val baseTriangles = triangulate(shape, distanceTolerance)
val points = shape.contours.map { it.adaptivePositions(distanceTolerance) }
extrudeShape(
baseTriangles = baseTriangles,
contours = points,
front = front,
back = back,
frontScale = frontScale,
backScale = backScale,
frontCap = frontCap,
backCap = backCap,
sides = sides,
flipNormals = flipNormals,
writer = writer
)
}
/**
* Extrudes all [shapes]. Uses [writer] to write the resulting
* 3D meshes. The arguments are passed unmodified to [extrudeShape].
*/
fun extrudeShapes(
shapes: List<Shape>,
front: Double,
back: Double,
frontScale: Double = 1.0,
backScale: Double = 1.0,
frontCap: Boolean = true,
backCap: Boolean = true,
sides: Boolean = true,
distanceTolerance: Double = 0.5,
flipNormals: Boolean = false,
writer: VertexWriter
) {
shapes.forEach {
extrudeShape(
shape = it,
front = front,
back = back,
frontScale = frontScale,
backScale = backScale,
frontCap = frontCap,
backCap = backCap,
sides = sides,
distanceTolerance = distanceTolerance,
flipNormals = flipNormals,
writer = writer
)
}
}
/**
* Return a normalized [Vector2], or [Vector2.ZERO] if the vector is too small
*/
private val Vector2.safeNormalized: Vector2
get() {
return if (length > 0.0001) {
normalized
} else {
Vector2.ZERO
}
}

View File

@@ -0,0 +1,137 @@
package org.openrndr.extra.meshgenerators
import org.openrndr.draw.VertexBuffer
import org.openrndr.math.Vector2
import org.openrndr.math.Vector3
import org.openrndr.shape.Rectangle
/**
* Generate a finite 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.
* @return A vertex buffer containing the triangles to render the 3D shape.
*/
fun planeMesh(
center: Vector3,
right: Vector3,
forward: Vector3,
up: Vector3 = forward.cross(right).normalized,
width: Double = 1.0,
height: Double = 1.0,
widthSegments: Int = 1,
heightSegments: Int = 1
): VertexBuffer {
val vertexCount = (widthSegments * heightSegments) * 6
val vb = meshVertexBuffer(vertexCount)
vb.put {
generatePlane(
center, right, forward, up,
width, height, widthSegments, heightSegments, bufferWriter(this)
)
}
return vb
}
/**
* Converts a [Rectangle] to a [VertexBuffer] 2D mesh matching its location and
* dimensions. [resolution] specifies the size in pixels of the triangles in
* the mesh.
* @return A vertex buffer containing the triangles to render the 3D shape.
*/
fun Rectangle.toMesh(
resolution: Double = 2.0
) = planeMesh(
center.xy0, Vector3.UNIT_X, Vector3.UNIT_Y, Vector3.UNIT_Z,
width, height,
(width / resolution).toInt(),
(height / resolution).toInt()
)
/**
* Generates a finite plane with its center at (0,0,0) and spanning the
* xz-plane.
* @return A vertex buffer containing the triangles to render the 3D shape.
*/
fun groundPlaneMesh(
width: Double = 1.0,
height: Double = 1.0,
widthSegments: Int = 1,
heightSegments: Int = 1
) = planeMesh(
Vector3.ZERO, Vector3.UNIT_X, Vector3.UNIT_Z, Vector3.UNIT_Y,
width, height, widthSegments, heightSegments
)
/**
* Generates a finite plane with its center at (0,0,0) and spanning the xy-plane
* @return A vertex buffer containing the triangles to render the 3D shape.
*/
fun wallPlaneMesh(
width: Double = 1.0,
height: Double = 1.0,
widthSegments: Int = 1,
heightSegments: Int = 1
) = planeMesh(
Vector3.ZERO, Vector3.UNIT_X, Vector3.UNIT_Y, Vector3.UNIT_Z,
width, height, widthSegments, heightSegments
)
/**
* 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.
*
* @param writer the vertex writer function
*/
fun generatePlane(
center: Vector3,
right: Vector3,
forward: Vector3,
up: Vector3 = forward.cross(right).normalized,
width: Double = 1.0,
height: Double = 1.0,
widthSegments: Int = 1,
heightSegments: Int = 1,
writer: VertexWriter
) {
val forwardStep = forward.normalized * (height / heightSegments)
val rightStep = right.normalized * (width / widthSegments)
val corner = center -
forward.normalized * (height * 0.5) -
right.normalized * (width * 0.5)
val step = Vector2(1.0 / widthSegments, 1.0 / heightSegments)
for (v in 0 until heightSegments) {
for (u in 0 until widthSegments) {
val uv00 = Vector2(u + 0.0, v + 0.0) * step
val uv01 = Vector2(u + 0.0, v + 1.0) * step
val uv10 = Vector2(u + 1.0, v + 0.0) * step
val uv11 = Vector2(u + 1.0, v + 1.0) * step
val c00 = corner +
forwardStep * v.toDouble() + rightStep * u.toDouble()
val c01 = corner +
forwardStep * (v + 1).toDouble() + rightStep * u.toDouble()
val c10 = corner +
forwardStep * v.toDouble() + rightStep * (u + 1).toDouble()
val c11 = corner +
forwardStep * (v + 1).toDouble() + rightStep * (u + 1).toDouble()
writer(c11, up, uv00)
writer(c10, up, uv10)
writer(c00, up, uv11)
writer(c00, up, uv11)
writer(c01, up, uv01)
writer(c11, up, uv00)
}
}
}

View File

@@ -0,0 +1,127 @@
package org.openrndr.extra.meshgenerators
import org.openrndr.draw.VertexBuffer
import org.openrndr.math.Spherical
import org.openrndr.math.Vector2
import kotlin.math.max
/**
* Returns 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 sphereMesh(
sides: Int = 16,
segments: Int = 16,
radius: Double = 1.0,
flipNormals: Boolean = false
): VertexBuffer {
val vertexCount = 2 * sides * 3 + max(0, (segments - 2)) * sides * 6
val vb = meshVertexBuffer(vertexCount)
vb.put {
generateSphere(sides, segments, radius, flipNormals, bufferWriter(this))
}
return vb
}
/**
* Generate sphere centered at the origin.
*
* @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.
* @param writer The vertex writer function
*/
fun generateSphere(
sides: Int,
segments: Int,
radius: Double = 1.0,
flipNormals: Boolean = false,
writer: VertexWriter
) {
val invertFactor = if (flipNormals) -1.0 else 1.0
for (t in 0 until segments) {
for (s in 0 until sides) {
val st00 = Spherical(s * 180.0 * 2.0 / sides, t * 180.0 / segments, radius)
val st01 = Spherical(s * 180.0 * 2.0 / sides, (t + 1) * 180.0 / segments, radius)
val st10 = Spherical((s + 1) * 180.0 * 2.0 / sides, t * 180.0 / segments, radius)
val st11 = Spherical((s + 1) * 180.0 * 2.0 / sides, (t + 1) * 180.0 / segments, radius)
val thetaMax = 180.0 * 2.0
val phiMax = 180.0
when (t) {
0 -> {
writer(st00.cartesian, st00.cartesian.normalized * invertFactor, Vector2(st00.theta / thetaMax + 0.5, 1.0 - st00.phi / phiMax))
writer(st01.cartesian, st01.cartesian.normalized * invertFactor, Vector2(st01.theta / thetaMax + 0.5, 1.0 - st01.phi / phiMax))
writer(st11.cartesian, st11.cartesian.normalized * invertFactor, Vector2(st11.theta / thetaMax + 0.5, 1.0 - st11.phi / phiMax))
}
segments - 1 -> {
writer(st11.cartesian, st11.cartesian.normalized * invertFactor, Vector2(st11.theta / thetaMax + 0.5, 1.0 - st11.phi / phiMax))
writer(st10.cartesian, st10.cartesian.normalized * invertFactor, Vector2(st10.theta / thetaMax + 0.5, 1.0 - st10.phi / phiMax))
writer(st00.cartesian, st00.cartesian.normalized * invertFactor, Vector2(st00.theta / thetaMax + 0.5, 1.0 - st00.phi / phiMax))
}
else -> {
writer(st00.cartesian, st00.cartesian.normalized * invertFactor, Vector2(st00.theta / thetaMax + 0.5, 1.0 - st00.phi / phiMax))
writer(st01.cartesian, st01.cartesian.normalized * invertFactor, Vector2(st01.theta / thetaMax + 0.5, 1.0 - st01.phi / phiMax))
writer(st11.cartesian, st11.cartesian.normalized * invertFactor, Vector2(st11.theta / thetaMax + 0.5, 1.0 - st11.phi / phiMax))
writer(st11.cartesian, st11.cartesian.normalized * invertFactor, Vector2(st11.theta / thetaMax + 0.5, 1.0 - st11.phi / phiMax))
writer(st10.cartesian, st10.cartesian.normalized * invertFactor, Vector2(st10.theta / thetaMax + 0.5, 1.0 - st10.phi / phiMax))
writer(st00.cartesian, st00.cartesian.normalized * invertFactor, Vector2(st00.theta / thetaMax + 0.5, 1.0 - st00.phi / phiMax))
}
}
}
}
}
/**
* Generate hemisphere centered at the origin.
*
* @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.
* @param writer The vertex writer function
*/
fun generateHemisphere(
sides: Int,
segments: Int,
radius: Double = 1.0,
flipNormals: Boolean = false,
writer: VertexWriter
) {
val invertFactor = if (flipNormals) -1.0 else 1.0
for (t in 0 until segments) {
for (s in 0 until sides) {
val st00 = Spherical(s * 180.0 * 2.0 / sides, t * 180.0 / segments, radius)
val st01 = Spherical(s * 180.0 * 2.0 / sides, (t + 1) * 180.0 / segments, radius)
val st10 = Spherical((s + 1) * 180.0 * 2.0 / sides, t * 180.0 / segments, radius)
val st11 = Spherical((s + 1) * 180.0 * 2.0 / sides, (t + 1) * 180.0 / segments, radius)
val thetaMax = 180.0 * 2.0
val phiMax = 180.0 * 0.5
when (t) {
0 -> {
writer(st00.cartesian, st00.cartesian.normalized * invertFactor, Vector2(st00.theta / thetaMax + 0.5, 1.0 - st00.phi / phiMax))
writer(st01.cartesian, st01.cartesian.normalized * invertFactor, Vector2(st01.theta / thetaMax + 0.5, 1.0 - st01.phi / phiMax))
writer(st11.cartesian, st11.cartesian.normalized * invertFactor, Vector2(st11.theta / thetaMax + 0.5, 1.0 - st11.phi / phiMax))
}
else -> {
writer(st00.cartesian, st00.cartesian.normalized * invertFactor, Vector2(st00.theta / thetaMax + 0.5, 1.0 - st00.phi / phiMax))
writer(st01.cartesian, st01.cartesian.normalized * invertFactor, Vector2(st01.theta / thetaMax + 0.5, 1.0 - st01.phi / phiMax))
writer(st11.cartesian, st11.cartesian.normalized * invertFactor, Vector2(st11.theta / thetaMax + 0.5, 1.0 - st11.phi / phiMax))
writer(st11.cartesian, st11.cartesian.normalized * invertFactor, Vector2(st11.theta / thetaMax + 0.5, 1.0 - st11.phi / phiMax))
writer(st10.cartesian, st10.cartesian.normalized * invertFactor, Vector2(st10.theta / thetaMax + 0.5, 1.0 - st10.phi / phiMax))
writer(st00.cartesian, st00.cartesian.normalized * invertFactor, Vector2(st00.theta / thetaMax + 0.5, 1.0 - st00.phi / phiMax))
}
}
}
}
}

View File

@@ -0,0 +1,634 @@
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 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)
}