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

@@ -1,95 +1,121 @@
# orx-mesh-generators # orx-mesh-generators
Generates 3D meshes: sphere, box, cylinder, plane, dodecahedron. Generates various types of 3D meshes.
##### usage ## Simple meshes
```kotlin ```kotlin
// To create simple meshes
val sphere = sphereMesh(32, 32, 4.0) val sphere = sphereMesh(32, 32, 4.0)
val unitSphere = sphereMesh()
val cube = boxMesh()
val box = boxMesh(2.0, 4.0, 2.0) val box = boxMesh(2.0, 4.0, 2.0)
val cylinder = cylinderMesh(radius = 0.5, length = 1.0, center = true)
val dodecahedron = dodecahedronMesh(0.5)
val plane = planeMesh(Vector3.ZERO, Vector3.UNIT_X, Vector3.UNIT_Y)
val disk = capMesh(sides = 15, radius = 0.5)
val tube = revolveMesh(sides = 15, length = 1.0)
... // To draw the generated meshes
drawer.vertexBuffer(dodecahedron, DrawPrimitive.TRIANGLES)
drawer.vertexBuffer(sphere, DrawPrimitive.TRIANGLES)
drawer.vertexBuffer(unitSphere, DrawPrimitive.TRIANGLES)
drawer.vertexBuffer(cube, DrawPrimitive.TRIANGLES)
drawer.vertexBuffer(box, DrawPrimitive.TRIANGLES)
``` ```
## API ## Complex triangular mesh generation
`orx-mesh-generators` comes with `buildTriangleMesh`, which
implements a [DSL](https://en.wikipedia.org/wiki/Domain-specific_language)
to construct 3D shapes.
To create shapes we can call methods like `box()`, `sphere()`,
`cylinder()`, `dodecahedron()`, `plane()`, `revolve()`,
`taperedCylinder()`, `hemisphere()` and `cap()`.
```kotlin ```kotlin
fun sphereMesh( // Create a rotated box
sides: Int = 16, val mesh = buildTriangleMesh {
segments: Int = 16, rotate(Vector3.UNIT_Z, 45.0)
radius: Double = 1.0, box()
invert: Boolean = false): VertexBuffer }
fun groundPlaneMesh(
width: Double = 1.0,
height: Double = 1.0,
widthSegments: Int = 1,
heightSegments: Int): VertexBuffer
fun boxMesh(
width: Double = 1.0,
height: Double = 1.0,
depth: Double = 1.0,
widthSegments: Int = 1,
heightSegments: Int = 1,
depthSegments: Int = 1,
invert: Boolean = false): VertexBuffer
``` ```
<!-- __demos__ >
# Demos We can also use methods like `translate()` and `rotate()` to create
[DemoBoxKt](src/demo/kotlin/DemoBoxKt.kt more complex compositions. The `color` property sets the color of
![DemoBoxKt](https://github.com/openrndr/orx/blob/media/orx-mesh-generators/images/DemoBoxKt.png the next mesh.
[DemoComplex01Kt](src/demo/kotlin/DemoComplex01Kt.kt
![DemoComplex01Kt](https://github.com/openrndr/orx/blob/media/orx-mesh-generators/images/DemoComplex01Kt.png ```kotlin
[DemoComplex02Kt](src/demo/kotlin/DemoComplex02Kt.kt // Create a ring of boxes of various colors
![DemoComplex02Kt](https://github.com/openrndr/orx/blob/media/orx-mesh-generators/images/DemoComplex02Kt.png val mesh = buildTriangleMesh {
[DemoComplex03Kt](src/demo/kotlin/DemoComplex03Kt.kt repeat(12) {
![DemoComplex03Kt](https://github.com/openrndr/orx/blob/media/orx-mesh-generators/images/DemoComplex03Kt.png // Take a small step
[DemoComplex04Kt](src/demo/kotlin/DemoComplex04Kt.kt translate(2.0, 0.0, 0.0)
![DemoComplex04Kt](https://github.com/openrndr/orx/blob/media/orx-mesh-generators/images/DemoComplex04Kt.png // Turn 30 degrees
[DemoComplex05Kt](src/demo/kotlin/DemoComplex05Kt.kt rotate(Vector3.UNIT_Y, 30.0)
![DemoComplex05Kt](https://github.com/openrndr/orx/blob/media/orx-mesh-generators/images/DemoComplex05Kt.png // Set a color
color = rgb(it / 11.0, 1.0, 1.0 - it / 11.0)
// Add a colored box
box(1.0, 1.0, 1.0)
}
}
```
`isolated { ... }` can be used to encapsulate transformations and
avoid them accumulating to unpredictable values.
```kotlin
val mesh = buildTriangleMesh {
repeat(10) { x ->
repeat(10) { y ->
isolated {
translate(x * 1.0, y * 1.0, 0.0)
sphere(8, 8, 0.1)
}
}
}
}
```
Other available methods are:
- `grid()`: creates a tri-dimensional grid of meshes.
- `extrudeShape()`: gives depth to 2D `Shape`.
- `twist()`: post-processing effect to twist a mesh around an axis.
- `extrudeContourSteps()`: uses Parallel Transport Frames to extrude a contour along a 3D path.
The [demo folder](src/jvmDemo/kotlin) contains examples using these methods.
Check out the [source code](src/commonMain/kotlin) to learn about function arguments.
<!-- __demos__ --> <!-- __demos__ -->
## Demos ## Demos
### DemoAll ### DemoAll
[source code](src/demo/kotlin/DemoAll.kt) [source code](src/jvmDemo/kotlin/DemoAll.kt)
![DemoAllKt](https://raw.githubusercontent.com/openrndr/orx/media/orx-mesh-generators/images/DemoAllKt.png) ![DemoAllKt](https://raw.githubusercontent.com/openrndr/orx/media/orx-mesh-generators/images/DemoAllKt.png)
### DemoBox ### DemoBox
[source code](src/demo/kotlin/DemoBox.kt) [source code](src/jvmDemo/kotlin/DemoBox.kt)
![DemoBoxKt](https://raw.githubusercontent.com/openrndr/orx/media/orx-mesh-generators/images/DemoBoxKt.png) ![DemoBoxKt](https://raw.githubusercontent.com/openrndr/orx/media/orx-mesh-generators/images/DemoBoxKt.png)
### DemoComplex01 ### DemoComplex01
[source code](src/demo/kotlin/DemoComplex01.kt) [source code](src/jvmDemo/kotlin/DemoComplex01.kt)
![DemoComplex01Kt](https://raw.githubusercontent.com/openrndr/orx/media/orx-mesh-generators/images/DemoComplex01Kt.png) ![DemoComplex01Kt](https://raw.githubusercontent.com/openrndr/orx/media/orx-mesh-generators/images/DemoComplex01Kt.png)
### DemoComplex02 ### DemoComplex02
[source code](src/demo/kotlin/DemoComplex02.kt) [source code](src/jvmDemo/kotlin/DemoComplex02.kt)
![DemoComplex02Kt](https://raw.githubusercontent.com/openrndr/orx/media/orx-mesh-generators/images/DemoComplex02Kt.png) ![DemoComplex02Kt](https://raw.githubusercontent.com/openrndr/orx/media/orx-mesh-generators/images/DemoComplex02Kt.png)
### DemoComplex03 ### DemoComplex03
[source code](src/demo/kotlin/DemoComplex03.kt) [source code](src/jvmDemo/kotlin/DemoComplex03.kt)
![DemoComplex03Kt](https://raw.githubusercontent.com/openrndr/orx/media/orx-mesh-generators/images/DemoComplex03Kt.png) ![DemoComplex03Kt](https://raw.githubusercontent.com/openrndr/orx/media/orx-mesh-generators/images/DemoComplex03Kt.png)
### DemoComplex04 ### DemoComplex04
[source code](src/demo/kotlin/DemoComplex04.kt) [source code](src/jvmDemo/kotlin/DemoComplex04.kt)
![DemoComplex04Kt](https://raw.githubusercontent.com/openrndr/orx/media/orx-mesh-generators/images/DemoComplex04Kt.png) ![DemoComplex04Kt](https://raw.githubusercontent.com/openrndr/orx/media/orx-mesh-generators/images/DemoComplex04Kt.png)
### DemoComplex05 ### DemoComplex05
[source code](src/demo/kotlin/DemoComplex05.kt) [source code](src/jvmDemo/kotlin/DemoComplex05.kt)
![DemoComplex05Kt](https://raw.githubusercontent.com/openrndr/orx/media/orx-mesh-generators/images/DemoComplex05Kt.png) ![DemoComplex05Kt](https://raw.githubusercontent.com/openrndr/orx/media/orx-mesh-generators/images/DemoComplex05Kt.png)

View File

@@ -1,11 +1,24 @@
plugins { plugins {
org.openrndr.extra.convention.`kotlin-jvm` org.openrndr.extra.convention.`kotlin-multiplatform`
} }
dependencies { kotlin {
implementation(libs.openrndr.application) sourceSets {
implementation(libs.openrndr.math) @Suppress("UNUSED_VARIABLE")
demoImplementation(project(":orx-shapes")) val commonMain by getting {
demoImplementation(project(":orx-mesh-generators")) dependencies {
demoImplementation(project(":orx-camera")) api(libs.openrndr.application)
api(libs.openrndr.math)
}
}
@Suppress("UNUSED_VARIABLE")
val jvmDemo by getting {
dependencies {
implementation(project(":orx-shapes"))
implementation(project(":orx-mesh-generators"))
implementation(project(":orx-camera"))
}
}
}
} }

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

@@ -1,49 +1,63 @@
package org.openrndr.extra.meshgenerators package org.openrndr.extra.meshgenerators
import org.openrndr.draw.VertexBuffer import org.openrndr.draw.VertexBuffer
import org.openrndr.math.* 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 import org.openrndr.math.transforms.rotateY
/** /**
* A shape created by rotating an envelope around a vertical axis. * A shape created by rotating an envelope around a vertical axis.
* The default envelope is a horizontal line which produces a flat round disk. *
* By providing a more complex envelop one can create curved shapes like a bowl. * @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( fun capMesh(
sides: Int, sides: Int,
radius: Double, radius: Double,
enveloppe: List<Vector2> = listOf( envelope: List<Vector2> = listOf(
Vector2(0.0, 0.0), Vector2(0.0, 0.0),
Vector2(1.0, 0.0) Vector2(1.0, 0.0)
) )
): VertexBuffer { ): VertexBuffer {
val vb = meshVertexBuffer(6 * sides * (enveloppe.size - 1)) val vb = meshVertexBuffer(6 * sides * (envelope.size - 1))
vb.put { vb.put {
generateCap(sides, radius, enveloppe, bufferWriter(this)) generateCap(sides, radius, envelope, bufferWriter(this))
} }
return vb 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( fun generateCap(
sides: Int, sides: Int,
radius: Double, radius: Double,
enveloppe: List<Vector2> = listOf( envelope: List<Vector2> = listOf(
Vector2(0.0, 0.0), Vector2(0.0, 0.0),
Vector2(1.0, 0.0) Vector2(1.0, 0.0)
), ),
writer: VertexWriter writer: VertexWriter
) { ) {
val maxX = enveloppe.maxByOrNull { it.x } ?: Vector2(1.0, 0.0) val maxX = envelope.maxByOrNull { it.x } ?: Vector2(1.0, 0.0)
val a = maxX.x val a = maxX.x
val cleanEnveloppe = enveloppe.map { Vector2((it.x / a) * radius, it.y) } val cleanEnvelope = envelope.map { Vector2((it.x / a) * radius, it.y) }
val normals2D = enveloppe.zipWithNext().map { val normals2D = envelope.zipWithNext().map {
val d = it.second - it.first val d = it.second - it.first
d.normalized.perpendicular(YPolarity.CCW_POSITIVE_Y) d.normalized.perpendicular(YPolarity.CCW_POSITIVE_Y)
} }
val basePositions = cleanEnveloppe.map { Vector3(it.x, it.y, 0.0) } val basePositions = cleanEnvelope.map { Vector3(it.x, it.y, 0.0) }
val baseNormals = normals2D.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) { for (side in 0 until sides) {
@@ -58,9 +72,9 @@ fun generateCap(
for (segment in 0 until basePositions.size - 1) { for (segment in 0 until basePositions.size - 1) {
val p00 = v0[segment] val p00 = v0[segment]
val p01 = v0[segment+1] val p01 = v0[segment + 1]
val p10 = v1[segment] val p10 = v1[segment]
val p11 = v1[segment+1] val p11 = v1[segment + 1]
val nn0 = n0[segment] val nn0 = n0[segment]
val nn1 = n1[segment] val nn1 = n1[segment]
@@ -78,43 +92,55 @@ fun generateCap(
/** /**
* A shape created by rotating an envelope around a vertical axis. * A shape created by rotating an envelope around a vertical axis.
* The default envelope is a vertical line which produces a hollow cylinder. *
* @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( fun revolveMesh(
sides: Int, sides: Int,
length: Double, length: Double,
enveloppe: List<Vector2> = listOf( envelope: List<Vector2> = listOf(
Vector2(1.0, 0.0), Vector2(1.0, 0.0),
Vector2(1.0, 1.0) Vector2(1.0, 1.0)
) )
): VertexBuffer { ): VertexBuffer {
val vb = meshVertexBuffer(6 * sides * (enveloppe.size - 1)) val vb = meshVertexBuffer(6 * sides * (envelope.size - 1))
vb.put { vb.put {
generateRevolve(sides, length, enveloppe, bufferWriter(this)) generateRevolve(sides, length, envelope, bufferWriter(this))
} }
return vb 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( fun generateRevolve(
sides: Int, sides: Int,
length: Double, length: Double,
enveloppe: List<Vector2> = listOf( envelope: List<Vector2> = listOf(
Vector2(1.0, 0.0), Vector2(1.0, 0.0),
Vector2(1.0, 1.0) Vector2(1.0, 1.0)
), ),
writer: VertexWriter writer: VertexWriter
) { ) {
val maxY = enveloppe.maxByOrNull { it.y } ?: Vector2(0.0, 1.0) val maxY = envelope.maxByOrNull { it.y } ?: Vector2(0.0, 1.0)
val a = maxY.y val a = maxY.y
val cleanEnveloppe = enveloppe.map { Vector2((it.x), (it.y/a - 0.5) * length ) } val cleanEnvelope = envelope.map { Vector2((it.x), (it.y / a - 0.5) * length) }
val normals2D = enveloppe.zipWithNext().map { val normals2D = envelope.zipWithNext().map {
val d = it.second - it.first val d = it.second - it.first
d.normalized.perpendicular() * Vector2(1.0, -1.0) d.normalized.perpendicular() * Vector2(1.0, -1.0)
} }
val basePositions = cleanEnveloppe.map { Vector3(it.x, it.y, 0.0) } val basePositions = cleanEnvelope.map { Vector3(it.x, it.y, 0.0) }
val baseNormals = normals2D.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) { for (side in 0 until sides) {
@@ -129,9 +155,9 @@ fun generateRevolve(
for (segment in 0 until basePositions.size - 1) { for (segment in 0 until basePositions.size - 1) {
val p00 = v0[segment] val p00 = v0[segment]
val p01 = v0[segment+1] val p01 = v0[segment + 1]
val p10 = v1[segment] val p10 = v1[segment]
val p11 = v1[segment+1] val p11 = v1[segment + 1]
val nn0 = n0[segment] val nn0 = n0[segment]
val nn1 = n1[segment] val nn1 = n1[segment]

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

@@ -8,12 +8,15 @@ import kotlin.math.sqrt
// Based on // Based on
// https://github.com/mrdoob/three.js/blob/master/src/geometries/DodecahedronGeometry.js // https://github.com/mrdoob/three.js/blob/master/src/geometries/DodecahedronGeometry.js
// Create: /**
// val dode = dodecahedronMesh(400.0) * A dodecahedron mesh
// Draw: *
// drawer.vertexBuffer(dode, DrawPrimitive.TRIANGLES) * @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 { */
fun dodecahedronMesh(
radius: Double = 1.0
): VertexBuffer {
val vb = meshVertexBuffer(12 * 3 * 3) val vb = meshVertexBuffer(12 * 3 * 3)
vb.put { vb.put {
generateDodecahedron(radius, bufferWriter(this)) generateDodecahedron(radius, bufferWriter(this))
@@ -21,10 +24,19 @@ fun dodecahedronMesh(radius: Double = 1.0): VertexBuffer {
return vb return vb
} }
fun generateDodecahedron(radius: Double = 1.0, writer: VertexWriter) { /**
* 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 t = (1.0 + sqrt(5.0)) / 2
val r = 1 / t; val r = 1 / t
val vertices = listOf( val vertices = listOf(
// (±1, ±1, ±1) // (±1, ±1, ±1)
@@ -74,7 +86,7 @@ fun generateDodecahedron(radius: Double = 1.0, writer: VertexWriter) {
vertices[i * 3 + 1], vertices[i * 3 + 1],
vertices[i * 3 + 2]) * radius vertices[i * 3 + 2]) * radius
} }
val up = (tri[1] - tri[0]).cross(tri[2] - tri[0]).normalized; val up = (tri[1] - tri[0]).cross(tri[2] - tri[0]).normalized
writer(tri[0], up, uv) writer(tri[0], up, uv)
writer(tri[1], up, uv) writer(tri[1], up, uv)
writer(tri[2], 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)
}

View File

@@ -1,73 +0,0 @@
import org.openrndr.application
import org.openrndr.draw.DrawPrimitive
import org.openrndr.draw.shadeStyle
import org.openrndr.extensions.SingleScreenshot
import org.openrndr.extra.camera.Orbital
import org.openrndr.extra.meshgenerators.*
import org.openrndr.math.Vector3
import org.openrndr.math.transforms.transform
fun main() {
application {
program {
if (System.getProperty("takeScreenshot") == "true") {
extend(SingleScreenshot()) {
this.outputFile = System.getProperty("screenshotPath")
}
}
extend(Orbital()) {
this.eye = Vector3(0.0, 10.0, 20.0)
this.lookAt = Vector3(0.0, 5.0, 0.0)
}
val m = meshGenerator {
group {
hemisphere(32, 16, 5.0)
transform(transform {
translate(0.0, 12.0, 0.0)
})
}
group {
cylinder(32, 1, 5.0, 6.0)
transform(transform {
translate(0.0, 9.0, 0.0)
rotate(Vector3.UNIT_X, 90.0)
})
}
group {
hemisphere(32, 16, 5.0)
transform(transform {
translate(0.0, 6.0, 0.0)
rotate(Vector3.UNIT_X, 180.0)
})
}
group {
val legCount = 12
val baseRadius = 3.0
val legRadius = 0.05
val legLength = 4.0
for (i in 0 until legCount) {
group {
val dphi = 360.0 / legCount
cylinder(32, 1, legRadius, legLength)
transform(transform {
rotate(Vector3.UNIT_Y, dphi * i)
translate(baseRadius, 0.0, 0.0)
rotate(Vector3.UNIT_Z, -15.0)
translate(0.0, legLength/2.0, 0.0)
rotate(Vector3.UNIT_X, 90.0)
})
}
}
}
}
extend {
drawer.shadeStyle = shadeStyle {
fragmentTransform = """
x_fill.rgb *= v_viewNormal.z;
""".trimIndent()
}
drawer.vertexBuffer(m, DrawPrimitive.TRIANGLES)
}
}
}
}

View File

@@ -1,94 +0,0 @@
import org.openrndr.application
import org.openrndr.draw.DrawPrimitive
import org.openrndr.draw.shadeStyle
import org.openrndr.extensions.SingleScreenshot
import org.openrndr.extra.camera.Orbital
import org.openrndr.extra.meshgenerators.*
import org.openrndr.math.Vector3
import org.openrndr.math.transforms.transform
fun main() {
application {
program {
if (System.getProperty("takeScreenshot") == "true") {
extend(SingleScreenshot()) {
this.outputFile = System.getProperty("screenshotPath")
}
}
extend(Orbital()) {
this.eye = Vector3(0.0, 10.0, 20.0)
this.lookAt = Vector3(0.0, 5.0, 0.0)
}
val m = meshGenerator {
group {
hemisphere(32, 16, 5.0)
transform(transform {
translate(0.0, 12.0, 0.0)
})
}
val ridges = 5
val midLength = 6.0
val ridgeLength = midLength / ridges
val ridgeRadius = 5.5
for (r in 0 until ridges) {
group {
taperedCylinder(32, 1, 5.0, ridgeRadius, ridgeLength/ 2.0)
transform(transform {
translate(0.0,
ridgeLength/4.0 + r * ridgeLength + 6.0,
0.0)
rotate(Vector3.UNIT_X, 270.0)
})
}
group {
taperedCylinder(32, 1, ridgeRadius, 5.0, ridgeLength/2.0)
transform(transform {
translate(0.0,
ridgeLength/4.0 + ridgeLength/2.0 + r * ridgeLength + 6.0,
0.0)
rotate(Vector3.UNIT_X, 270.0)
})
}
}
group {
hemisphere(32, 16, 5.0)
transform(transform {
translate(0.0, 6.0, 0.0)
rotate(Vector3.UNIT_X, 180.0)
})
}
group {
val legCount = 12
val baseRadius = 3.0
val legRadius = 0.05
val legLength = 4.0
for (i in 0 until legCount) {
group {
val dphi = 360.0 / legCount
cylinder(32, 1, legRadius, legLength)
transform(transform {
rotate(Vector3.UNIT_Y, dphi * i)
translate(baseRadius, 0.0, 0.0)
rotate(Vector3.UNIT_Z, -15.0)
translate(0.0, legLength/2.0, 0.0)
rotate(Vector3.UNIT_X, 90.0)
})
}
}
}
}
extend {
drawer.shadeStyle = shadeStyle {
fragmentTransform = """
x_fill.rgb *= v_viewNormal.z;
""".trimIndent()
}
drawer.vertexBuffer(m, DrawPrimitive.TRIANGLES)
}
}
}
}

View File

@@ -1,119 +0,0 @@
import org.openrndr.application
import org.openrndr.draw.DrawPrimitive
import org.openrndr.draw.shadeStyle
import org.openrndr.extensions.SingleScreenshot
import org.openrndr.extra.camera.Orbital
import org.openrndr.extra.meshgenerators.*
import org.openrndr.math.Vector2
import org.openrndr.math.Vector3
import org.openrndr.math.transforms.transform
fun main() {
application {
program {
if (System.getProperty("takeScreenshot") == "true") {
extend(SingleScreenshot()) {
this.outputFile = System.getProperty("screenshotPath")
}
}
extend(Orbital()) {
this.eye = Vector3(0.0, 15.0, 15.0)
}
val m = meshGenerator {
val sides = 12
group {
cap(sides, 5.0, listOf(
Vector2(0.0, 1.0),
Vector2(0.5, 1.0),
Vector2(0.5, 0.5),
Vector2(0.9, 0.5),
Vector2(1.0, 0.0))
)
transform(transform {
translate(0.0, 12.0, 0.0)
})
}
val ridges = 5
val midLength = 6.0
val ridgeLength = midLength / ridges
val ridgeRadius = 5.5
for (r in 0 until ridges) {
group {
taperedCylinder(sides, 1, 5.0, ridgeRadius, ridgeLength / 3.0)
transform(transform {
translate(
0.0,
ridgeLength / 6.0 + r * ridgeLength + 6.0,
0.0
)
rotate(Vector3.UNIT_X, 270.0)
})
}
group {
taperedCylinder(sides, 1, ridgeRadius, ridgeRadius, ridgeLength / 3.0)
transform(transform {
translate(
0.0,
ridgeLength / 6.0 + ridgeLength / 3.0 + r * ridgeLength + 6.0,
0.0
)
rotate(Vector3.UNIT_X, 270.0)
})
}
group {
taperedCylinder(sides, 1, ridgeRadius, 5.0, ridgeLength / 3.0)
transform(transform {
translate(
0.0,
ridgeLength / 6.0 + 2 * ridgeLength / 3.0 + r * ridgeLength + 6.0,
0.0
)
rotate(Vector3.UNIT_X, 270.0)
})
}
}
group {
cap(sides, 5.0, listOf(Vector2(0.0, 0.0), Vector2(1.0, 0.0)))
transform(transform {
translate(0.0, 6.0, 0.0)
rotate(Vector3.UNIT_X, 180.0)
})
}
group {
val legCount = 12
val baseRadius = 4.5
val legRadius = 0.05
val legLength = 7.0
for (i in 0 until legCount) {
group {
val dphi = 360.0 / legCount
cylinder(sides, 1, legRadius, legLength)
transform(transform {
rotate(Vector3.UNIT_Y, dphi * i)
translate(baseRadius, 0.0, 0.0)
//rotate(Vector3.UNIT_Z, -15.0)
translate(0.0, legLength / 2.0, 0.0)
rotate(Vector3.UNIT_X, 90.0)
})
}
}
}
}
extend {
drawer.shadeStyle = shadeStyle {
fragmentTransform = """
x_fill.rgb *= v_viewNormal.z;
""".trimIndent()
}
drawer.vertexBuffer(m, DrawPrimitive.TRIANGLES)
}
}
}
}

View File

@@ -1,3 +1,4 @@
import org.openrndr.WindowMultisample
import org.openrndr.application import org.openrndr.application
import org.openrndr.color.ColorRGBa import org.openrndr.color.ColorRGBa
import org.openrndr.draw.* import org.openrndr.draw.*
@@ -9,12 +10,15 @@ import org.openrndr.shape.Rectangle
fun main() { fun main() {
application { application {
configure {
multisample = WindowMultisample.SampleCount(8)
}
program { program {
val meshes = listOf( val meshes = listOf(
boxMesh(1.0, 1.0, 1.0), boxMesh(1.0, 1.0, 1.0),
sphereMesh(radius = 0.5), sphereMesh(radius = 0.5),
dodecahedronMesh(0.5), dodecahedronMesh(0.5),
cylinderMesh(radius = 0.5, length = 1.0), cylinderMesh(radius = 0.5, length = 1.0, center = true),
planeMesh(Vector3.ZERO, Vector3.UNIT_X, Vector3.UNIT_Y), planeMesh(Vector3.ZERO, Vector3.UNIT_X, Vector3.UNIT_Y),
capMesh( capMesh(
15, 0.5, 15, 0.5,

View File

@@ -1,15 +1,19 @@
import org.openrndr.WindowMultisample
import org.openrndr.application import org.openrndr.application
import org.openrndr.color.ColorRGBa import org.openrndr.color.ColorRGBa
import org.openrndr.draw.CullTestPass
import org.openrndr.draw.DrawPrimitive import org.openrndr.draw.DrawPrimitive
import org.openrndr.draw.colorBuffer import org.openrndr.draw.colorBuffer
import org.openrndr.draw.shadeStyle import org.openrndr.draw.shadeStyle
import org.openrndr.extensions.SingleScreenshot
import org.openrndr.extra.camera.Orbital import org.openrndr.extra.camera.Orbital
import org.openrndr.extra.meshgenerators.boxMesh import org.openrndr.extra.meshgenerators.boxMesh
import org.openrndr.math.Vector3 import org.openrndr.math.Vector3
fun main() { fun main() {
application { application {
configure {
multisample = WindowMultisample.SampleCount(8)
}
program { program {
val box = boxMesh(1.0, 1.0, 1.0) val box = boxMesh(1.0, 1.0, 1.0)
@@ -22,11 +26,6 @@ fun main() {
} }
s.upload() s.upload()
if (System.getProperty("takeScreenshot") == "true") {
extend(SingleScreenshot()) {
this.outputFile = System.getProperty("screenshotPath")
}
}
extend(Orbital()) { extend(Orbital()) {
eye = Vector3(1.0, 1.0, 1.0) eye = Vector3(1.0, 1.0, 1.0)
} }
@@ -38,6 +37,7 @@ fun main() {
""".trimIndent() """.trimIndent()
parameter("texture", texture) parameter("texture", texture)
} }
drawer.drawStyle.cullTestPass = CullTestPass.FRONT
drawer.vertexBuffer(box, DrawPrimitive.TRIANGLES) drawer.vertexBuffer(box, DrawPrimitive.TRIANGLES)
} }
} }

View File

@@ -1,32 +1,32 @@
import org.openrndr.WindowMultisample
import org.openrndr.application import org.openrndr.application
import org.openrndr.color.ColorRGBa
import org.openrndr.draw.DrawPrimitive import org.openrndr.draw.DrawPrimitive
import org.openrndr.draw.shadeStyle import org.openrndr.draw.shadeStyle
import org.openrndr.extensions.SingleScreenshot
import org.openrndr.extra.camera.Orbital import org.openrndr.extra.camera.Orbital
import org.openrndr.extra.meshgenerators.box import org.openrndr.extra.meshgenerators.box
import org.openrndr.extra.meshgenerators.group import org.openrndr.extra.meshgenerators.buildTriangleMesh
import org.openrndr.extra.meshgenerators.meshGenerator
import org.openrndr.extra.meshgenerators.sphere import org.openrndr.extra.meshgenerators.sphere
import org.openrndr.math.Vector3 import org.openrndr.math.Vector3
import org.openrndr.math.transforms.transform
fun main() { fun main() {
application { application {
configure {
width = 800
height = 800
multisample = WindowMultisample.SampleCount(8)
}
program { program {
val m = meshGenerator { val m = buildTriangleMesh {
color = ColorRGBa.PINK
sphere(32, 32, 1.0) sphere(32, 32, 1.0)
group {
box(4.0, 4.0, 4.0) color = ColorRGBa.WHITE
transform(transform {
translate(0.0, -2.0, 0.0) translate(0.0, -2.0, 0.0)
}) box(4.0, 4.0, 4.0)
}
}
if (System.getProperty("takeScreenshot") == "true") {
extend(SingleScreenshot()) {
this.outputFile = System.getProperty("screenshotPath")
}
} }
extend(Orbital()) { extend(Orbital()) {
this.eye = Vector3(0.0, 3.0, 7.0) this.eye = Vector3(0.0, 3.0, 7.0)
this.lookAt = Vector3(0.0, 2.0, 0.0) this.lookAt = Vector3(0.0, 2.0, 0.0)
@@ -35,6 +35,7 @@ fun main() {
extend { extend {
drawer.shadeStyle = shadeStyle { drawer.shadeStyle = shadeStyle {
fragmentTransform = """ fragmentTransform = """
x_fill = va_color;
x_fill.rgb *= v_viewNormal.z; x_fill.rgb *= v_viewNormal.z;
""".trimIndent() """.trimIndent()
} }

View File

@@ -0,0 +1,67 @@
import org.openrndr.WindowMultisample
import org.openrndr.application
import org.openrndr.draw.DrawPrimitive
import org.openrndr.draw.shadeStyle
import org.openrndr.extra.camera.Orbital
import org.openrndr.extra.meshgenerators.buildTriangleMesh
import org.openrndr.extra.meshgenerators.cylinder
import org.openrndr.extra.meshgenerators.hemisphere
import org.openrndr.math.Vector3
fun main() {
application {
configure {
width = 800
height = 800
multisample = WindowMultisample.SampleCount(8)
}
program {
extend(Orbital()) {
this.eye = Vector3(0.0, 10.0, 20.0)
this.lookAt = Vector3(0.0, 5.0, 0.0)
}
val m = buildTriangleMesh {
isolated {
translate(0.0, 12.0, 0.0)
hemisphere(32, 16, 5.0)
}
isolated {
translate(0.0, 9.0, 0.0)
rotate(Vector3.UNIT_X, 90.0)
cylinder(32, 1, 5.0, 6.0, center = true)
}
isolated {
translate(0.0, 6.0, 0.0)
rotate(Vector3.UNIT_X, 180.0)
hemisphere(32, 16, 5.0)
}
isolated {
val legCount = 12
val baseRadius = 3.0
val legRadius = 0.05
val legLength = 4.0
for (i in 0 until legCount) {
isolated {
val dphi = 360.0 / legCount
rotate(Vector3.UNIT_Y, dphi * i)
translate(baseRadius, 0.0, 0.0)
rotate(Vector3.UNIT_Z, -15.0)
translate(0.0, legLength / 2.0, 0.0)
rotate(Vector3.UNIT_X, 90.0)
cylinder(32, 1, legRadius, legLength, center = true)
}
}
}
}
extend {
drawer.shadeStyle = shadeStyle {
fragmentTransform = """
x_fill.rgb *= v_viewNormal.z;
""".trimIndent()
}
drawer.vertexBuffer(m, DrawPrimitive.TRIANGLES)
}
}
}
}

View File

@@ -0,0 +1,83 @@
import org.openrndr.WindowMultisample
import org.openrndr.application
import org.openrndr.draw.DrawPrimitive
import org.openrndr.draw.shadeStyle
import org.openrndr.extra.camera.Orbital
import org.openrndr.extra.meshgenerators.*
import org.openrndr.math.Vector3
fun main() {
application {
configure {
width = 800
height = 800
multisample = WindowMultisample.SampleCount(8)
}
program {
extend(Orbital()) {
this.eye = Vector3(0.0, 10.0, 20.0)
this.lookAt = Vector3(0.0, 5.0, 0.0)
}
val m = buildTriangleMesh {
isolated {
translate(0.0, 12.0, 0.0)
hemisphere(32, 16, 5.0)
}
val ridges = 5
val midLength = 6.0
val ridgeLength = midLength / ridges
val ridgeRadius = 5.5
for (r in 0 until ridges) {
isolated {
translate(0.0,
ridgeLength/4.0 + r * ridgeLength + 6.0,
0.0)
rotate(Vector3.UNIT_X, 270.0)
taperedCylinder(32, 1, 5.0, ridgeRadius, ridgeLength/ 2.0, center = true)
}
isolated {
translate(0.0,
ridgeLength/4.0 + ridgeLength/2.0 + r * ridgeLength + 6.0,
0.0)
rotate(Vector3.UNIT_X, 270.0)
taperedCylinder(32, 1, ridgeRadius, 5.0, ridgeLength/2.0, center = true)
}
}
isolated {
translate(0.0, 6.0, 0.0)
rotate(Vector3.UNIT_X, 180.0)
hemisphere(32, 16, 5.0)
}
isolated {
val legCount = 12
val baseRadius = 3.0
val legRadius = 0.05
val legLength = 4.0
for (i in 0 until legCount) {
isolated {
val dphi = 360.0 / legCount
rotate(Vector3.UNIT_Y, dphi * i)
translate(baseRadius, 0.0, 0.0)
rotate(Vector3.UNIT_Z, -15.0)
translate(0.0, legLength/2.0, 0.0)
rotate(Vector3.UNIT_X, 90.0)
cylinder(32, 1, legRadius, legLength, center = true)
}
}
}
}
extend {
drawer.shadeStyle = shadeStyle {
fragmentTransform = """
x_fill.rgb *= v_viewNormal.z;
""".trimIndent()
}
drawer.vertexBuffer(m, DrawPrimitive.TRIANGLES)
}
}
}
}

View File

@@ -0,0 +1,104 @@
import org.openrndr.WindowMultisample
import org.openrndr.application
import org.openrndr.draw.DrawPrimitive
import org.openrndr.draw.shadeStyle
import org.openrndr.extra.camera.Orbital
import org.openrndr.extra.meshgenerators.*
import org.openrndr.math.Vector2
import org.openrndr.math.Vector3
fun main() {
application {
configure {
width = 800
height = 800
multisample = WindowMultisample.SampleCount(8)
}
program {
extend(Orbital()) {
this.eye = Vector3(0.0, 15.0, 15.0)
}
val m = buildTriangleMesh {
val sides = 12
isolated {
translate(0.0, 12.0, 0.0)
cap(sides, 5.0, listOf(
Vector2(0.0, 1.0),
Vector2(0.5, 1.0),
Vector2(0.5, 0.5),
Vector2(0.9, 0.5),
Vector2(1.0, 0.0))
)
}
val ridges = 5
val midLength = 6.0
val ridgeLength = midLength / ridges
val ridgeRadius = 5.5
for (r in 0 until ridges) {
isolated {
translate(
0.0,
ridgeLength / 6.0 + r * ridgeLength + 6.0,
0.0
)
rotate(Vector3.UNIT_X, 270.0)
taperedCylinder(sides, 1, 5.0, ridgeRadius, ridgeLength / 3.0, center = true)
}
isolated {
translate(
0.0,
ridgeLength / 6.0 + ridgeLength / 3.0 + r * ridgeLength + 6.0,
0.0
)
rotate(Vector3.UNIT_X, 270.0)
taperedCylinder(sides, 1, ridgeRadius, ridgeRadius, ridgeLength / 3.0, center = true)
}
isolated {
translate(
0.0,
ridgeLength / 6.0 + 2 * ridgeLength / 3.0 + r * ridgeLength + 6.0,
0.0
)
rotate(Vector3.UNIT_X, 270.0)
taperedCylinder(sides, 1, ridgeRadius, 5.0, ridgeLength / 3.0, center = true)
}
}
isolated {
translate(0.0, 6.0, 0.0)
rotate(Vector3.UNIT_X, 180.0)
cap(sides, 5.0, listOf(Vector2(0.0, 0.0), Vector2(1.0, 0.0)))
}
isolated {
val legCount = 12
val baseRadius = 4.5
val legRadius = 0.05
val legLength = 7.0
for (i in 0 until legCount) {
isolated {
val dphi = 360.0 / legCount
rotate(Vector3.UNIT_Y, dphi * i)
translate(baseRadius, 0.0, 0.0)
translate(0.0, legLength / 2.0, 0.0)
rotate(Vector3.UNIT_X, 90.0)
cylinder(sides, 1, legRadius, legLength, center = true)
}
}
}
}
extend {
drawer.shadeStyle = shadeStyle {
fragmentTransform = """
x_fill.rgb *= v_viewNormal.z;
""".trimIndent()
}
drawer.vertexBuffer(m, DrawPrimitive.TRIANGLES)
}
}
}
}

View File

@@ -1,30 +1,30 @@
import org.openrndr.WindowMultisample
import org.openrndr.application import org.openrndr.application
import org.openrndr.draw.CullTestPass
import org.openrndr.draw.DrawPrimitive import org.openrndr.draw.DrawPrimitive
import org.openrndr.draw.shadeStyle import org.openrndr.draw.shadeStyle
import org.openrndr.extensions.SingleScreenshot
import org.openrndr.extra.camera.Orbital import org.openrndr.extra.camera.Orbital
import org.openrndr.extra.meshgenerators.* import org.openrndr.extra.meshgenerators.*
import org.openrndr.math.Vector2
import org.openrndr.math.Vector3 import org.openrndr.math.Vector3
import org.openrndr.math.transforms.transform
import org.openrndr.shape.Circle import org.openrndr.shape.Circle
fun main() { fun main() {
application { application {
configure {
width = 800
height = 800
multisample = WindowMultisample.SampleCount(8)
}
program { program {
if (System.getProperty("takeScreenshot") == "true") {
extend(SingleScreenshot()) {
this.outputFile = System.getProperty("screenshotPath")
}
}
extend(Orbital()) { extend(Orbital()) {
this.eye = Vector3(0.0, 30.0, 50.0) this.eye = Vector3(0.0, 30.0, 50.0)
} }
val m = meshGenerator { val m = buildTriangleMesh {
grid(5,5, 5) { u, v, w -> grid(5,5, 5) { u, v, w ->
isolated {
translate(u * 20.0, v * 20.0, w * 20.0)
extrudeShape(Circle(0.0, 0.0, 50.0).shape, 4.0, scale = 0.1) extrudeShape(Circle(0.0, 0.0, 50.0).shape, 4.0, scale = 0.1)
transform(transform{ translate(u*20.0, v*20.0, w * 20.0)} ) }
} }
twist(360.0/200.0, 0.0) twist(360.0/200.0, 0.0)
twist(360.0/200.0, 0.0, Vector3.UNIT_X) twist(360.0/200.0, 0.0, Vector3.UNIT_X)
@@ -37,6 +37,7 @@ fun main() {
x_fill.rgb *= v_viewNormal.z; x_fill.rgb *= v_viewNormal.z;
""".trimIndent() """.trimIndent()
} }
drawer.drawStyle.cullTestPass = CullTestPass.FRONT
drawer.vertexBuffer(m, DrawPrimitive.TRIANGLES) drawer.vertexBuffer(m, DrawPrimitive.TRIANGLES)
} }
} }

View File

@@ -0,0 +1,67 @@
import org.openrndr.WindowMultisample
import org.openrndr.application
import org.openrndr.color.ColorRGBa
import org.openrndr.draw.DrawPrimitive
import org.openrndr.draw.shadeStyle
import org.openrndr.extra.camera.Orbital
import org.openrndr.extra.meshgenerators.buildTriangleMesh
import org.openrndr.extra.meshgenerators.extrudeContourSteps
import org.openrndr.math.Vector3
import org.openrndr.math.catmullRom
import org.openrndr.shape.Circle
import org.openrndr.shape.toPath3D
fun main() {
application {
configure {
width = 800
height = 800
multisample = WindowMultisample.SampleCount(8)
}
program {
val m = buildTriangleMesh {
color = ColorRGBa.PINK
val path = listOf(
Vector3(0.0, 0.0, 0.0),
Vector3(-2.0, 2.0, 2.0),
Vector3(2.0, -4.0, 4.0),
Vector3(0.0, 0.0, 8.0)
).catmullRom(0.5, closed = false).toPath3D()
translate(-1.0, 0.0, 0.0)
for (i in 0 until 3) {
extrudeContourSteps(
Circle(0.0, 0.0, 0.5).contour,
path,
160,
Vector3.UNIT_Y,
contourDistanceTolerance = 0.02,
pathDistanceTolerance = 0.001
)
translate(1.0, 0.0, 0.0)
}
}
extend(Orbital()) {
this.eye = Vector3(0.0, 3.0, 7.0)
this.lookAt = Vector3(0.0, 2.0, 0.0)
}
extend {
drawer.shadeStyle = shadeStyle {
fragmentTransform = """
x_fill = va_color;
x_fill.rgb *= v_viewNormal.z;
""".trimIndent()
}
drawer.vertexBuffer(m, DrawPrimitive.TRIANGLES)
}
}
}
}

View File

@@ -0,0 +1,71 @@
import org.openrndr.WindowMultisample
import org.openrndr.application
import org.openrndr.color.ColorRGBa
import org.openrndr.draw.DrawPrimitive
import org.openrndr.draw.shadeStyle
import org.openrndr.extra.camera.Orbital
import org.openrndr.extra.meshgenerators.buildTriangleMesh
import org.openrndr.extra.meshgenerators.extrudeShapeSteps
import org.openrndr.math.Vector3
import org.openrndr.math.catmullRom
import org.openrndr.shape.Circle
import org.openrndr.shape.Shape
import org.openrndr.shape.toPath3D
fun main() {
application {
configure {
width = 800
height = 800
multisample = WindowMultisample.SampleCount(8)
}
program {
val m = buildTriangleMesh {
color = ColorRGBa.PINK
val path = listOf(
Vector3(0.0, 0.0, 0.0),
Vector3(-2.0, 2.0, 2.0),
Vector3(2.0, -4.0, 4.0),
Vector3(0.0, 0.0, 8.0)
).catmullRom(0.5, closed = false).toPath3D()
translate(-5.0, 0.0, 0.0)
val ring = Shape(listOf(Circle(0.0, 0.0, 0.5).contour, Circle(0.0, 0.0, 0.25).contour.reversed))
for (i in 0 until 5) {
extrudeShapeSteps(
ring,
path,
160,
Vector3.UNIT_Y,
contourDistanceTolerance = 0.02,
pathDistanceTolerance = 0.001
)
translate(2.0, 0.0, 0.0)
}
}
extend(Orbital()) {
this.eye = Vector3(0.0, 3.0, 7.0)
this.lookAt = Vector3(0.0, 2.0, 0.0)
}
extend {
drawer.shadeStyle = shadeStyle {
fragmentTransform = """
x_fill = va_color;
x_fill.rgb *= v_viewNormal.z;
""".trimIndent()
}
drawer.vertexBuffer(m, DrawPrimitive.TRIANGLES)
}
}
}
}

View File

@@ -0,0 +1,96 @@
import org.openrndr.WindowMultisample
import org.openrndr.application
import org.openrndr.color.ColorRGBa
import org.openrndr.draw.DrawPrimitive
import org.openrndr.draw.shadeStyle
import org.openrndr.extra.camera.Orbital
import org.openrndr.extra.meshgenerators.buildTriangleMesh
import org.openrndr.extra.meshgenerators.extrudeContourAdaptive
import org.openrndr.math.Polar
import org.openrndr.math.Vector3
import org.openrndr.math.asDegrees
import org.openrndr.math.asRadians
import org.openrndr.shape.Circle
import org.openrndr.shape.Path3D
import kotlin.math.PI
import kotlin.math.exp
fun main() {
application {
configure {
width = 800
height = 800
multisample = WindowMultisample.SampleCount(8)
}
program {
fun spiralPath(a: Double, k: Double, cycles: Double, steps: Int, direction:Double = 1.0): Path3D {
val points = (0 until steps).map {
val theta = ((PI * 2.0 * cycles) / steps) * it
val radius = a * exp(k * theta)
val c = Polar(theta.asDegrees, radius).cartesian
c.xy0
}
return Path3D.fromPoints(points, false)
}
val spiral = buildTriangleMesh {
for (i in -1..1 step 2) {
val p = spiralPath(0.2 * i, 0.25, 4.0, 400)
extrudeContourAdaptive(
Circle(0.0, 0.0, 0.1).contour,
p,
Vector3.UNIT_Z,
contourDistanceTolerance = 0.02,
pathDistanceTolerance = 0.001
)
}
isolated {
color = ColorRGBa.YELLOW
rotate(Vector3.UNIT_X, 90.0)
//rotate(Vector3.UNIT_Y, 45.0)
for (j in 0 until 1) {
for (i in -1..1 step 2) {
val rotationDegrees = j * 180.0 / 1.0
val rotation = rotationDegrees.asRadians
val scale = exp(rotation * 0.25)
val p = spiralPath(0.2 * i * scale, 0.25, 4.0, 400)
extrudeContourAdaptive(
Circle(0.0, 0.0, 0.1).contour,
p,
Vector3.UNIT_Z,
contourDistanceTolerance = 0.02,
pathDistanceTolerance = 0.001
)
}
rotate(Vector3.UNIT_Y, 180.0 / 1.0)
}
}
}
extend(Orbital())
extend {
drawer.shadeStyle = shadeStyle {
fragmentTransform = """
x_fill = va_color;
x_fill.rgb *= v_viewNormal.z;
""".trimIndent()
}
drawer.rotate(Vector3.UNIT_X, seconds*20.0)
drawer.vertexBuffer(spiral, DrawPrimitive.TRIANGLES)
}
}
}
}

View File

@@ -1,61 +0,0 @@
package org.openrndr.extra.meshgenerators
import org.openrndr.draw.VertexBuffer
import org.openrndr.math.Vector3
fun boxMesh(width: Double = 1.0, height: Double = 1.0, depth: Double = 1.0,
widthSegments: Int = 1, heightSegments: Int = 1, depthSegments: Int = 1,
invert: 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,
invert, bufferWriter(this))
}
return vb
}
fun generateBox(width: Double = 1.0, height: Double = 1.0, depth: Double = 1.0,
widthSegments: Int = 1, heightSegments: Int = 1, depthSegments: Int = 1,
invert: Boolean = false,
writer: VertexWriter) {
val sign = if (invert) -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

@@ -1,83 +0,0 @@
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
fun cylinderMesh(sides: Int = 16, segments: Int = 16, radius: Double = 1.0, length: Double, invert: Boolean = false): VertexBuffer {
val vertexCount = 6 * sides * segments
val vb = meshVertexBuffer(vertexCount)
vb.put {
generateCylinder(sides, segments, radius, length, invert, bufferWriter(this))
}
return vb
}
fun generateCylinder(sides: Int, segments: Int, radius: Double, length: Double, invert: Boolean = false, vertexWriter: VertexWriter) {
return generateTaperedCylinder(sides, segments, radius, radius, length, invert, vertexWriter)
}
fun generateTaperedCylinder(sides: Int, segments: Int, radiusStart: Double, radiusEnd:Double, length: Double, invert: Boolean = false, vertexWriter: VertexWriter) {
val dphi = (Math.PI * 2) / sides
val ddeg = (360.0) / sides
val invertFactor = if (invert) 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 baseNormal = Vector3(1.0, 0.0, 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 - length/2.0
val z1 = (length / segments) * (segment + 1) - length/2.0
for (side in 0 until sides) {
val x00 = Math.cos(side * dphi) * radius0
val x10 = Math.cos(side * dphi + dphi) * radius0
val y00 = Math.sin(side * dphi) * radius0
val y10 = Math.sin(side * dphi + dphi) * radius0
val x01 = Math.cos(side * dphi) * radius1
val x11 = Math.cos(side * dphi + dphi) * radius1
val y01 = Math.sin(side * dphi) * radius1
val y11 = Math.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 (invert) {
vertexWriter(Vector3(x00, y00, z0), n0, Vector2(u0, v0))
vertexWriter(Vector3(x10, y10, z0), n1, Vector2(u0, v1))
vertexWriter(Vector3(x11, y11, z1), n1, Vector2(u1, v1))
vertexWriter(Vector3(x11, y11, z1), n1, Vector2(u1, v1))
vertexWriter(Vector3(x01, y01, z1), n0, Vector2(u1, v0))
vertexWriter(Vector3(x00, y00, z0), n0, Vector2(u0, v0))
} else {
vertexWriter(Vector3(x00, y00, z0), n0, Vector2(u0, v0))
vertexWriter(Vector3(x01, y01, z1), n0, Vector2(u1, v0))
vertexWriter(Vector3(x11, y11, z1), n1, Vector2(u1, v1))
vertexWriter(Vector3(x11, y11, z1), n1, Vector2(u1, v1))
vertexWriter(Vector3(x10, y10, z0), n1, Vector2(u0, v1))
vertexWriter(Vector3(x00, y00, z0), n0, Vector2(u0, v0))
}
}
}
}

View File

@@ -1,230 +0,0 @@
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.transforms.normalMatrix
import org.openrndr.math.transforms.rotate
import org.openrndr.shape.Shape
import java.nio.ByteBuffer
import java.nio.ByteOrder
class GeneratorBuffer {
class VertexData(val position: Vector3, val normal: Vector3, val texCoord: Vector2)
var data = mutableListOf<VertexData>()
fun write(position: Vector3, normal: Vector3, texCoord: Vector2) {
data.add(VertexData(position, normal, texCoord))
}
fun concat(other: GeneratorBuffer) {
data.addAll(other.data)
}
fun transform(m: Matrix44) {
val nm = normalMatrix(m)
data = data.map {
VertexData((m * (it.position.xyz1)).xyz, (nm * (it.normal.xyz0)).xyz, it.texCoord)
}.toMutableList()
}
fun transformUV(m: Matrix44) {
data = data.map {
VertexData(it.position, it.normal, (m * (it.texCoord.xy01)).xy)
}.toMutableList()
}
fun toByteBuffer(): ByteBuffer {
val bb = ByteBuffer.allocateDirect(data.size * (3 * 4 + 3 * 4 + 2 * 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())
}
return bb
}
}
fun GeneratorBuffer.sphere(sides: Int, segments: Int, radius: Double, invert: Boolean = false) {
generateSphere(sides, segments, radius, invert, this::write)
}
fun GeneratorBuffer.hemisphere(sides: Int, segments: Int, radius: Double, invert: Boolean = false) {
generateHemisphere(sides, segments, radius, invert, this::write)
}
enum class GridCoordinates {
INDEX,
UNIPOLAR,
BIPOLAR,
}
fun GeneratorBuffer.grid(width: Int, height: Int, coordinates: GridCoordinates = GridCoordinates.BIPOLAR, builder: GeneratorBuffer.(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))
}
}
}
}
}
fun GeneratorBuffer.twist(degreesPerUnit: Double, start: Double, axis: Vector3 = Vector3.UNIT_Y) {
data = data.map {
val p = it.position.projectedOn(axis)
val t = if (axis.x != 0.0) p.x / axis.x else if (axis.y != 0.0) p.y / axis.y else if (axis.z != 0.0) p.z / axis.z else
throw IllegalArgumentException("0 axis")
val r = Matrix44.rotate(axis, t * degreesPerUnit)
GeneratorBuffer.VertexData((r * it.position.xyz1).xyz, (r * it.normal.xyz0).xyz, it.texCoord)
}.toMutableList()
}
fun GeneratorBuffer.grid(width: Int, height: Int, depth: Int, coordinates: GridCoordinates = GridCoordinates.BIPOLAR, builder: GeneratorBuffer.(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))
}
}
}
}
}
}
fun GeneratorBuffer.box(width: Double, height: Double, depth: Double, widthSegments: Int = 1, heightSegments: Int = 1, depthSegments: Int = 1, invert: Boolean = false) {
generateBox(width, height, depth, widthSegments, heightSegments, depthSegments, invert, this::write)
}
fun GeneratorBuffer.cylinder(sides: Int, segments: Int, radius: Double, length: Double, invert: Boolean = false) {
generateCylinder(sides, segments, radius, length, invert, this::write)
}
fun GeneratorBuffer.dodecahedron(radius: Double) {
generateDodecahedron(radius, this::write)
}
fun GeneratorBuffer.taperedCylinder(sides: Int, segments: Int, startRadius: Double, endRadius: Double, length: Double, invert: Boolean = false) {
generateTaperedCylinder(sides, segments, startRadius, endRadius, length, invert, this::write)
}
fun GeneratorBuffer.cap(sides: Int, radius: Double, enveloppe: List<Vector2>) {
generateCap(sides, radius, enveloppe, this::write)
}
fun GeneratorBuffer.revolve(sides: Int, length: Double, enveloppe: List<Vector2>) {
generateRevolve(sides, length, enveloppe, this::write)
}
fun GeneratorBuffer.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)
fun GeneratorBuffer.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
)
}
fun GeneratorBuffer.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
)
}
fun GeneratorBuffer.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
)
}
fun meshGenerator(vertexBuffer: VertexBuffer? = null, builder: GeneratorBuffer.() -> Unit): VertexBuffer {
val gb = GeneratorBuffer()
gb.builder()
val vb = vertexBuffer ?: meshVertexBuffer(gb.data.size)
val bb = gb.toByteBuffer()
bb.rewind()
vb.write(bb)
return vb
}
fun generator(builder: GeneratorBuffer.() -> Unit): GeneratorBuffer {
val gb = GeneratorBuffer()
gb.builder()
return gb
}
fun GeneratorBuffer.group(builder: GeneratorBuffer.() -> Unit) {
val gb = GeneratorBuffer()
gb.builder()
this.concat(gb)
}

View File

@@ -1,193 +0,0 @@
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.Circle
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
*/
fun meshVertexBuffer(size: Int): VertexBuffer {
return vertexBuffer(vertexFormat {
position(3)
normal(3)
textureCoordinate(2)
}, 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
*/
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 ->
(points[mod(index + 1, points.size)] - points[mod(index - 1, points.size)]).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
*/
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
)
}
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
)
}
}
private val Vector2.safeNormalized: Vector2
get() {
return if (length > 0.0001) {
normalized
} else {
Vector2.ZERO
}
}

View File

@@ -1,97 +0,0 @@
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
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.
*/
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
*/
fun groundPlaneMesh(width: Double = 1.0,
height: Double = 1.0,
widthSegments: Int = 1,
heightSegments: Int = 1): VertexBuffer {
return 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
*/
fun wallPlaneMesh(width: Double = 1.0,
height: Double = 1.0,
widthSegments: Int = 1,
heightSegments: Int = 1): VertexBuffer {
return planeMesh(Vector3.ZERO, Vector3.UNIT_X, Vector3.UNIT_Y, Vector3.UNIT_Z,
width, height, widthSegments, heightSegments)
}
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 = 2,
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

@@ -1,84 +0,0 @@
package org.openrndr.extra.meshgenerators
import org.openrndr.draw.VertexBuffer
import org.openrndr.math.Spherical
import org.openrndr.math.Vector2
fun sphereMesh(sides: Int = 16, segments: Int = 16, radius: Double = 1.0, invert: Boolean = false): VertexBuffer {
val vertexCount = 2 * sides * 3 + Math.max(0, (segments - 2)) * sides * 6
val vb = meshVertexBuffer(vertexCount)
vb.put {
generateSphere(sides, segments, radius, invert, bufferWriter(this))
}
return vb
}
fun generateSphere(sides: Int, segments: Int, radius: Double = 1.0, invert: Boolean = false, writer: VertexWriter) {
val inverter = if (invert) -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 * inverter, Vector2(st00.theta / thetaMax + 0.5, 1.0 - st00.phi / phiMax))
writer(st01.cartesian, st01.cartesian.normalized * inverter, Vector2(st01.theta / thetaMax + 0.5, 1.0 - st01.phi / phiMax))
writer(st11.cartesian, st11.cartesian.normalized * inverter, Vector2(st11.theta / thetaMax + 0.5, 1.0 - st11.phi / phiMax))
}
segments - 1 -> {
writer(st11.cartesian, st11.cartesian.normalized * inverter, Vector2(st11.theta / thetaMax + 0.5, 1.0 - st11.phi / phiMax))
writer(st10.cartesian, st10.cartesian.normalized * inverter, Vector2(st10.theta / thetaMax + 0.5, 1.0 - st10.phi / phiMax))
writer(st00.cartesian, st00.cartesian.normalized * inverter, Vector2(st00.theta / thetaMax + 0.5, 1.0 - st00.phi / phiMax))
}
else -> {
writer(st00.cartesian, st00.cartesian.normalized * inverter, Vector2(st00.theta / thetaMax + 0.5, 1.0 - st00.phi / phiMax))
writer(st01.cartesian, st01.cartesian.normalized * inverter, Vector2(st01.theta / thetaMax + 0.5, 1.0 - st01.phi / phiMax))
writer(st11.cartesian, st11.cartesian.normalized * inverter, Vector2(st11.theta / thetaMax + 0.5, 1.0 - st11.phi / phiMax))
writer(st11.cartesian, st11.cartesian.normalized * inverter, Vector2(st11.theta / thetaMax + 0.5, 1.0 - st11.phi / phiMax))
writer(st10.cartesian, st10.cartesian.normalized * inverter, Vector2(st10.theta / thetaMax + 0.5, 1.0 - st10.phi / phiMax))
writer(st00.cartesian, st00.cartesian.normalized * inverter, Vector2(st00.theta / thetaMax + 0.5, 1.0 - st00.phi / phiMax))
}
}
}
}
}
fun generateHemisphere(sides: Int, segments: Int, radius: Double = 1.0, invert: Boolean = false, writer: VertexWriter) {
val inverter = if (invert) -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 * inverter, Vector2(st00.theta / thetaMax + 0.5, 1.0 - st00.phi / phiMax))
writer(st01.cartesian, st01.cartesian.normalized * inverter, Vector2(st01.theta / thetaMax + 0.5, 1.0 - st01.phi / phiMax))
writer(st11.cartesian, st11.cartesian.normalized * inverter, Vector2(st11.theta / thetaMax + 0.5, 1.0 - st11.phi / phiMax))
}
else -> {
writer(st00.cartesian, st00.cartesian.normalized * inverter, Vector2(st00.theta / thetaMax + 0.5, 1.0 - st00.phi / phiMax))
writer(st01.cartesian, st01.cartesian.normalized * inverter, Vector2(st01.theta / thetaMax + 0.5, 1.0 - st01.phi / phiMax))
writer(st11.cartesian, st11.cartesian.normalized * inverter, Vector2(st11.theta / thetaMax + 0.5, 1.0 - st11.phi / phiMax))
writer(st11.cartesian, st11.cartesian.normalized * inverter, Vector2(st11.theta / thetaMax + 0.5, 1.0 - st11.phi / phiMax))
writer(st10.cartesian, st10.cartesian.normalized * inverter, Vector2(st10.theta / thetaMax + 0.5, 1.0 - st10.phi / phiMax))
writer(st00.cartesian, st00.cartesian.normalized * inverter, Vector2(st00.theta / thetaMax + 0.5, 1.0 - st00.phi / phiMax))
}
}
}
}
}