diff --git a/orx-mesh-generators/README.md b/orx-mesh-generators/README.md index 3a225dde..52653893 100644 --- a/orx-mesh-generators/README.md +++ b/orx-mesh-generators/README.md @@ -1,95 +1,121 @@ # orx-mesh-generators -Generates 3D meshes: sphere, box, cylinder, plane, dodecahedron. +Generates various types of 3D meshes. -##### usage +## Simple meshes ```kotlin +// To create simple meshes val sphere = sphereMesh(32, 32, 4.0) -val unitSphere = sphereMesh() -val cube = boxMesh() 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) -... - -drawer.vertexBuffer(sphere, DrawPrimitive.TRIANGLES) -drawer.vertexBuffer(unitSphere, DrawPrimitive.TRIANGLES) -drawer.vertexBuffer(cube, DrawPrimitive.TRIANGLES) -drawer.vertexBuffer(box, DrawPrimitive.TRIANGLES) - +// To draw the generated meshes +drawer.vertexBuffer(dodecahedron, 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 -fun sphereMesh( - sides: Int = 16, - segments: Int = 16, - radius: Double = 1.0, - 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 +// Create a rotated box +val mesh = buildTriangleMesh { + rotate(Vector3.UNIT_Z, 45.0) + box() +} ``` - ## Demos ### 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) ### 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) ### 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) ### 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) ### 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) ### 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) ### 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) diff --git a/orx-mesh-generators/build.gradle.kts b/orx-mesh-generators/build.gradle.kts index 91e9555b..41d26602 100644 --- a/orx-mesh-generators/build.gradle.kts +++ b/orx-mesh-generators/build.gradle.kts @@ -1,11 +1,24 @@ plugins { - org.openrndr.extra.convention.`kotlin-jvm` + org.openrndr.extra.convention.`kotlin-multiplatform` } -dependencies { - implementation(libs.openrndr.application) - implementation(libs.openrndr.math) - demoImplementation(project(":orx-shapes")) - demoImplementation(project(":orx-mesh-generators")) - demoImplementation(project(":orx-camera")) -} \ No newline at end of file +kotlin { + sourceSets { + @Suppress("UNUSED_VARIABLE") + val commonMain by getting { + dependencies { + 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")) + } + } + } +} diff --git a/orx-mesh-generators/src/commonMain/kotlin/Box.kt b/orx-mesh-generators/src/commonMain/kotlin/Box.kt new file mode 100644 index 00000000..ebc49d89 --- /dev/null +++ b/orx-mesh-generators/src/commonMain/kotlin/Box.kt @@ -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 + ) +} \ No newline at end of file diff --git a/orx-mesh-generators/src/main/kotlin/Cap.kt b/orx-mesh-generators/src/commonMain/kotlin/Cap.kt similarity index 51% rename from orx-mesh-generators/src/main/kotlin/Cap.kt rename to orx-mesh-generators/src/commonMain/kotlin/Cap.kt index 18de22f7..f586b316 100644 --- a/orx-mesh-generators/src/main/kotlin/Cap.kt +++ b/orx-mesh-generators/src/commonMain/kotlin/Cap.kt @@ -1,49 +1,63 @@ package org.openrndr.extra.meshgenerators 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 /** * 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( sides: Int, radius: Double, - enveloppe: List = listOf( + envelope: List = listOf( Vector2(0.0, 0.0), Vector2(1.0, 0.0) ) ): VertexBuffer { - val vb = meshVertexBuffer(6 * sides * (enveloppe.size - 1)) + val vb = meshVertexBuffer(6 * sides * (envelope.size - 1)) vb.put { - generateCap(sides, radius, enveloppe, bufferWriter(this)) + generateCap(sides, radius, envelope, bufferWriter(this)) } return vb } +/** + * Generate a shape by rotating an envelope around a vertical axis. + * + * @param sides the angular resolution of the cap + * @param radius the radius of the cap + * @param envelope a list of points defining the profile of the cap. The default envelope is a horizontal line which produces a flat round disk. By providing a more complex envelope one can create curved shapes like a bowl. + * @param writer the vertex writer function + */ fun generateCap( sides: Int, radius: Double, - enveloppe: List = listOf( + envelope: List = listOf( Vector2(0.0, 0.0), Vector2(1.0, 0.0) ), 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 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 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) } for (side in 0 until sides) { @@ -58,9 +72,9 @@ fun generateCap( for (segment in 0 until basePositions.size - 1) { val p00 = v0[segment] - val p01 = v0[segment+1] + val p01 = v0[segment + 1] val p10 = v1[segment] - val p11 = v1[segment+1] + val p11 = v1[segment + 1] val nn0 = n0[segment] val nn1 = n1[segment] @@ -78,43 +92,55 @@ fun generateCap( /** * 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( sides: Int, length: Double, - enveloppe: List = listOf( + envelope: List = listOf( Vector2(1.0, 0.0), Vector2(1.0, 1.0) ) ): VertexBuffer { - val vb = meshVertexBuffer(6 * sides * (enveloppe.size - 1)) + val vb = meshVertexBuffer(6 * sides * (envelope.size - 1)) vb.put { - generateRevolve(sides, length, enveloppe, bufferWriter(this)) + generateRevolve(sides, length, envelope, bufferWriter(this)) } return vb } +/** + * Generate a shape by rotating an envelope around a vertical axis. + * + * @param sides the angular resolution of the cap + * @param length the length of the shape. A multiplier for the y component of the envelope + * @param envelope a list of points defining the profile of the shape. The default envelope is a vertical line which produces a hollow cylinder. + * @param writer the vertex writer function + */ fun generateRevolve( sides: Int, length: Double, - enveloppe: List = listOf( + envelope: List = listOf( Vector2(1.0, 0.0), Vector2(1.0, 1.0) ), 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 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 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) } for (side in 0 until sides) { @@ -129,9 +155,9 @@ fun generateRevolve( for (segment in 0 until basePositions.size - 1) { val p00 = v0[segment] - val p01 = v0[segment+1] + val p01 = v0[segment + 1] val p10 = v1[segment] - val p11 = v1[segment+1] + val p11 = v1[segment + 1] val nn0 = n0[segment] val nn1 = n1[segment] diff --git a/orx-mesh-generators/src/commonMain/kotlin/Cylinder.kt b/orx-mesh-generators/src/commonMain/kotlin/Cylinder.kt new file mode 100644 index 00000000..1ce5e22e --- /dev/null +++ b/orx-mesh-generators/src/commonMain/kotlin/Cylinder.kt @@ -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)) + } + } + } +} \ No newline at end of file diff --git a/orx-mesh-generators/src/main/kotlin/Dodecahedron.kt b/orx-mesh-generators/src/commonMain/kotlin/Dodecahedron.kt similarity index 80% rename from orx-mesh-generators/src/main/kotlin/Dodecahedron.kt rename to orx-mesh-generators/src/commonMain/kotlin/Dodecahedron.kt index f7b64201..9e5ce484 100644 --- a/orx-mesh-generators/src/main/kotlin/Dodecahedron.kt +++ b/orx-mesh-generators/src/commonMain/kotlin/Dodecahedron.kt @@ -8,12 +8,15 @@ import kotlin.math.sqrt // Based on // https://github.com/mrdoob/three.js/blob/master/src/geometries/DodecahedronGeometry.js -// Create: -// val dode = dodecahedronMesh(400.0) -// Draw: -// drawer.vertexBuffer(dode, DrawPrimitive.TRIANGLES) - -fun dodecahedronMesh(radius: Double = 1.0): VertexBuffer { +/** + * A dodecahedron mesh + * + * @param radius the radius of the dodecahedron + * @return A vertex buffer containing the triangles to render the 3D shape + */ +fun dodecahedronMesh( + radius: Double = 1.0 +): VertexBuffer { val vb = meshVertexBuffer(12 * 3 * 3) vb.put { generateDodecahedron(radius, bufferWriter(this)) @@ -21,10 +24,19 @@ fun dodecahedronMesh(radius: Double = 1.0): VertexBuffer { 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 r = 1 / t; + val t = (1.0 + sqrt(5.0)) / 2 + val r = 1 / t val vertices = listOf( // (±1, ±1, ±1) @@ -74,7 +86,7 @@ fun generateDodecahedron(radius: Double = 1.0, writer: VertexWriter) { vertices[i * 3 + 1], 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[1], up, uv) writer(tri[2], up, uv) diff --git a/orx-mesh-generators/src/commonMain/kotlin/Extrusion.kt b/orx-mesh-generators/src/commonMain/kotlin/Extrusion.kt new file mode 100644 index 00000000..d7c245c6 --- /dev/null +++ b/orx-mesh-generators/src/commonMain/kotlin/Extrusion.kt @@ -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, + 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, + 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 = path.equidistantPositions( + stepCount, + pathDistanceTolerance + ), + frames: List = 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, + 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 = path.adaptivePositions(pathDistanceTolerance), + frames: List = 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 = path.equidistantPositions(stepCount, pathDistanceTolerance), + frames: List = 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 = path.adaptivePositions(pathDistanceTolerance), + frames: List = 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 +) \ No newline at end of file diff --git a/orx-mesh-generators/src/commonMain/kotlin/Frames.kt b/orx-mesh-generators/src/commonMain/kotlin/Frames.kt new file mode 100644 index 00000000..7e8bf821 --- /dev/null +++ b/orx-mesh-generators/src/commonMain/kotlin/Frames.kt @@ -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.frames(up0: Vector3): List { + val result = mutableListOf() + + 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 +} \ No newline at end of file diff --git a/orx-mesh-generators/src/commonMain/kotlin/MeshGenerators.kt b/orx-mesh-generators/src/commonMain/kotlin/MeshGenerators.kt new file mode 100644 index 00000000..4873dbb5 --- /dev/null +++ b/orx-mesh-generators/src/commonMain/kotlin/MeshGenerators.kt @@ -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, + contours: List>, + 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, + 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 + } + } diff --git a/orx-mesh-generators/src/commonMain/kotlin/Plane.kt b/orx-mesh-generators/src/commonMain/kotlin/Plane.kt new file mode 100644 index 00000000..a520f99f --- /dev/null +++ b/orx-mesh-generators/src/commonMain/kotlin/Plane.kt @@ -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) + } + } +} diff --git a/orx-mesh-generators/src/commonMain/kotlin/Sphere.kt b/orx-mesh-generators/src/commonMain/kotlin/Sphere.kt new file mode 100644 index 00000000..13fe474d --- /dev/null +++ b/orx-mesh-generators/src/commonMain/kotlin/Sphere.kt @@ -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)) + } + } + } + } +} \ No newline at end of file diff --git a/orx-mesh-generators/src/commonMain/kotlin/TriangleMeshBuilder.kt b/orx-mesh-generators/src/commonMain/kotlin/TriangleMeshBuilder.kt new file mode 100644 index 00000000..f6ec9eb6 --- /dev/null +++ b/orx-mesh-generators/src/commonMain/kotlin/TriangleMeshBuilder.kt @@ -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() + + /** + * 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() + + /** + * 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 +) { + generateCap(sides, radius, envelope, this::write) +} + +/** + * Generate a shape by rotating an envelope around a vertical axis. + * + * @param sides the angular resolution of the cap + * @param length the length of the shape. A multiplier for the y component of the envelope + * @param envelope a list of points defining the profile of the shape. + * The default envelope is a vertical line which produces a hollow cylinder. + */ +fun TriangleMeshBuilder.revolve( + sides: Int, + length: Double, + envelope: List +) { + generateRevolve(sides, length, envelope, this::write) +} + +/** + * Generate plane centered at [center], using the [right], [forward] and [up] + * vectors for its orientation. + * [width] and [height] specify the dimensions of the plane. + * [widthSegments] and [heightSegments] control the plane's number of + * segments. + */ +fun TriangleMeshBuilder.plane( + center: Vector3, + right: Vector3, + forward: Vector3, + up: Vector3, + width: Double = 1.0, + height: Double = 1.0, + widthSegments: Int = 1, + heightSegments: Int = 1 +) { + generatePlane( + center, + right, + forward, + up, + width, + height, + widthSegments, + heightSegments, + this::write + ) +} + +/** + * Extrudes a [Shape] from its triangulations + * + * @param baseTriangles triangle vertices for the caps + * @param contours contour vertices for the sides + * @param length the length of the extrusion + * @param scale scale factor for the caps + * @param frontCap add a front cap if true + * @param backCap add a back cap if true + * @param sides add the sides if true + */ +fun TriangleMeshBuilder.extrudeShape( + baseTriangles: List, + contours: List>, + length: Double, + scale: Double = 1.0, + frontCap: Boolean = true, + backCap: Boolean = true, + sides: Boolean = true +) { + extrudeShape( + baseTriangles = baseTriangles, + contours = contours, + front = -length / 2.0, + back = length / 2.0, + frontScale = scale, + backScale = scale, + frontCap = frontCap, + backCap = backCap, + sides = sides, + flipNormals = false, + writer = this::write + ) +} + +/** + * Extrudes a [Shape] + * + * @param shape the [Shape] to extrude + * @param length length of the extrusion + * @param scale scale factor of the caps + * @param frontCap add a front cap if true + * @param backCap add a back cap if true + * @param sides add the sides if true + * @param distanceTolerance controls how many segments will be created. Lower + * values result in higher vertex counts. + */ +fun TriangleMeshBuilder.extrudeShape( + shape: Shape, + length: Double, + scale: Double = 1.0, + frontCap: Boolean = true, + backCap: Boolean = true, + sides: Boolean = true, + distanceTolerance: Double = 0.5 +) { + extrudeShape( + shape = shape, + front = -length / 2.0, + back = length / 2.0, + frontScale = scale, + backScale = scale, + frontCap = frontCap, + backCap = backCap, + sides = sides, + distanceTolerance = distanceTolerance, + flipNormals = false, + writer = this::write + ) +} + +/** + * Extrudes a list of [Shape] + * + * @param shapes The [Shape]s to extrude + * @param length length of the extrusion + * @param scale scale factor of the caps + * @param distanceTolerance controls how many segments will be created. Lower + * values result in higher vertex counts. + */ +fun TriangleMeshBuilder.extrudeShapes( + shapes: List, + length: Double, + scale: Double = 1.0, + distanceTolerance: Double = 0.5 +) { + extrudeShapes( + shapes = shapes, + front = -length / 2.0, + back = length / 2.0, + frontScale = scale, + backScale = scale, + frontCap = true, + backCap = true, + sides = true, + distanceTolerance = distanceTolerance, + flipNormals = false, + writer = this::write + ) +} + +/** + * Creates a triangle mesh builder + * + * @param vertexBuffer The optional [VertexBuffer] into which to write data. + * If not provided one is created. + * @param builder A user function that adds 3D meshes to the [vertexBuffer] + * @return The populated [VertexBuffer] + */ +fun buildTriangleMesh( + vertexBuffer: VertexBuffer? = null, + builder: TriangleMeshBuilder.() -> Unit +): VertexBuffer { + val gb = TriangleMeshBuilder() + gb.builder() + + val vb = vertexBuffer ?: meshVertexBufferWithColor(gb.data.size) + + val bb = gb.toByteBuffer() + bb.rewind() + vb.write(bb) + return vb +} + +//fun generator( +// builder: TriangleMeshBuilder.() -> Unit +//): TriangleMeshBuilder { +// val gb = TriangleMeshBuilder() +// gb.builder() +// return gb +//} + +/** + * Creates a group. Can be used to avoid leaking mesh properties like `color` + * and `transform` into following meshes or groups. + * + * @param builder A user function that adds 3D meshes to the [vertexBuffer] + * @see [TriangleMeshBuilder.isolated] + */ +fun TriangleMeshBuilder.group( + builder: TriangleMeshBuilder.() -> Unit +) { + val gb = TriangleMeshBuilder() + gb.builder() + this.concat(gb) +} diff --git a/orx-mesh-generators/src/demo/kotlin/DemoComplex02.kt b/orx-mesh-generators/src/demo/kotlin/DemoComplex02.kt deleted file mode 100644 index c0045300..00000000 --- a/orx-mesh-generators/src/demo/kotlin/DemoComplex02.kt +++ /dev/null @@ -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) - } - } - } -} \ No newline at end of file diff --git a/orx-mesh-generators/src/demo/kotlin/DemoComplex03.kt b/orx-mesh-generators/src/demo/kotlin/DemoComplex03.kt deleted file mode 100644 index 8a880206..00000000 --- a/orx-mesh-generators/src/demo/kotlin/DemoComplex03.kt +++ /dev/null @@ -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) - } - } - } -} \ No newline at end of file diff --git a/orx-mesh-generators/src/demo/kotlin/DemoComplex04.kt b/orx-mesh-generators/src/demo/kotlin/DemoComplex04.kt deleted file mode 100644 index 7f7ff205..00000000 --- a/orx-mesh-generators/src/demo/kotlin/DemoComplex04.kt +++ /dev/null @@ -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) - } - } - } -} \ No newline at end of file diff --git a/orx-mesh-generators/src/demo/kotlin/DemoAll.kt b/orx-mesh-generators/src/jvmDemo/kotlin/DemoAll.kt similarity index 92% rename from orx-mesh-generators/src/demo/kotlin/DemoAll.kt rename to orx-mesh-generators/src/jvmDemo/kotlin/DemoAll.kt index bfe7f13e..2602ceba 100644 --- a/orx-mesh-generators/src/demo/kotlin/DemoAll.kt +++ b/orx-mesh-generators/src/jvmDemo/kotlin/DemoAll.kt @@ -1,3 +1,4 @@ +import org.openrndr.WindowMultisample import org.openrndr.application import org.openrndr.color.ColorRGBa import org.openrndr.draw.* @@ -9,12 +10,15 @@ import org.openrndr.shape.Rectangle fun main() { application { + configure { + multisample = WindowMultisample.SampleCount(8) + } program { val meshes = listOf( boxMesh(1.0, 1.0, 1.0), sphereMesh(radius = 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), capMesh( 15, 0.5, diff --git a/orx-mesh-generators/src/demo/kotlin/DemoBox.kt b/orx-mesh-generators/src/jvmDemo/kotlin/DemoBox.kt similarity index 81% rename from orx-mesh-generators/src/demo/kotlin/DemoBox.kt rename to orx-mesh-generators/src/jvmDemo/kotlin/DemoBox.kt index 857e3f94..9d9d7adc 100644 --- a/orx-mesh-generators/src/demo/kotlin/DemoBox.kt +++ b/orx-mesh-generators/src/jvmDemo/kotlin/DemoBox.kt @@ -1,15 +1,19 @@ +import org.openrndr.WindowMultisample import org.openrndr.application import org.openrndr.color.ColorRGBa +import org.openrndr.draw.CullTestPass import org.openrndr.draw.DrawPrimitive import org.openrndr.draw.colorBuffer import org.openrndr.draw.shadeStyle -import org.openrndr.extensions.SingleScreenshot import org.openrndr.extra.camera.Orbital import org.openrndr.extra.meshgenerators.boxMesh import org.openrndr.math.Vector3 fun main() { application { + configure { + multisample = WindowMultisample.SampleCount(8) + } program { val box = boxMesh(1.0, 1.0, 1.0) @@ -22,11 +26,6 @@ fun main() { } s.upload() - if (System.getProperty("takeScreenshot") == "true") { - extend(SingleScreenshot()) { - this.outputFile = System.getProperty("screenshotPath") - } - } extend(Orbital()) { eye = Vector3(1.0, 1.0, 1.0) } @@ -38,6 +37,7 @@ fun main() { """.trimIndent() parameter("texture", texture) } + drawer.drawStyle.cullTestPass = CullTestPass.FRONT drawer.vertexBuffer(box, DrawPrimitive.TRIANGLES) } } diff --git a/orx-mesh-generators/src/demo/kotlin/DemoComplex01.kt b/orx-mesh-generators/src/jvmDemo/kotlin/DemoComplex01.kt similarity index 56% rename from orx-mesh-generators/src/demo/kotlin/DemoComplex01.kt rename to orx-mesh-generators/src/jvmDemo/kotlin/DemoComplex01.kt index c07dc52b..f52fbcee 100644 --- a/orx-mesh-generators/src/demo/kotlin/DemoComplex01.kt +++ b/orx-mesh-generators/src/jvmDemo/kotlin/DemoComplex01.kt @@ -1,32 +1,32 @@ +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.extensions.SingleScreenshot import org.openrndr.extra.camera.Orbital import org.openrndr.extra.meshgenerators.box -import org.openrndr.extra.meshgenerators.group -import org.openrndr.extra.meshgenerators.meshGenerator +import org.openrndr.extra.meshgenerators.buildTriangleMesh import org.openrndr.extra.meshgenerators.sphere import org.openrndr.math.Vector3 -import org.openrndr.math.transforms.transform fun main() { application { + configure { + width = 800 + height = 800 + multisample = WindowMultisample.SampleCount(8) + } program { - val m = meshGenerator { + val m = buildTriangleMesh { + color = ColorRGBa.PINK sphere(32, 32, 1.0) - group { - box(4.0, 4.0, 4.0) - transform(transform { - translate(0.0, -2.0, 0.0) - }) - } - } - if (System.getProperty("takeScreenshot") == "true") { - extend(SingleScreenshot()) { - this.outputFile = System.getProperty("screenshotPath") - } + + color = ColorRGBa.WHITE + translate(0.0, -2.0, 0.0) + box(4.0, 4.0, 4.0) + } + extend(Orbital()) { this.eye = Vector3(0.0, 3.0, 7.0) this.lookAt = Vector3(0.0, 2.0, 0.0) @@ -35,6 +35,7 @@ fun main() { extend { drawer.shadeStyle = shadeStyle { fragmentTransform = """ + x_fill = va_color; x_fill.rgb *= v_viewNormal.z; """.trimIndent() } diff --git a/orx-mesh-generators/src/jvmDemo/kotlin/DemoComplex02.kt b/orx-mesh-generators/src/jvmDemo/kotlin/DemoComplex02.kt new file mode 100644 index 00000000..fec94728 --- /dev/null +++ b/orx-mesh-generators/src/jvmDemo/kotlin/DemoComplex02.kt @@ -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) + } + } + } +} \ No newline at end of file diff --git a/orx-mesh-generators/src/jvmDemo/kotlin/DemoComplex03.kt b/orx-mesh-generators/src/jvmDemo/kotlin/DemoComplex03.kt new file mode 100644 index 00000000..f53d53f8 --- /dev/null +++ b/orx-mesh-generators/src/jvmDemo/kotlin/DemoComplex03.kt @@ -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) + } + } + } +} \ No newline at end of file diff --git a/orx-mesh-generators/src/jvmDemo/kotlin/DemoComplex04.kt b/orx-mesh-generators/src/jvmDemo/kotlin/DemoComplex04.kt new file mode 100644 index 00000000..9f364ef2 --- /dev/null +++ b/orx-mesh-generators/src/jvmDemo/kotlin/DemoComplex04.kt @@ -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) + } + } + } +} \ No newline at end of file diff --git a/orx-mesh-generators/src/demo/kotlin/DemoComplex05.kt b/orx-mesh-generators/src/jvmDemo/kotlin/DemoComplex05.kt similarity index 63% rename from orx-mesh-generators/src/demo/kotlin/DemoComplex05.kt rename to orx-mesh-generators/src/jvmDemo/kotlin/DemoComplex05.kt index c5cf386e..8169749b 100644 --- a/orx-mesh-generators/src/demo/kotlin/DemoComplex05.kt +++ b/orx-mesh-generators/src/jvmDemo/kotlin/DemoComplex05.kt @@ -1,30 +1,30 @@ +import org.openrndr.WindowMultisample import org.openrndr.application +import org.openrndr.draw.CullTestPass 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 import org.openrndr.shape.Circle fun main() { application { + configure { + width = 800 + height = 800 + multisample = WindowMultisample.SampleCount(8) + } program { - if (System.getProperty("takeScreenshot") == "true") { - extend(SingleScreenshot()) { - this.outputFile = System.getProperty("screenshotPath") - } - } extend(Orbital()) { this.eye = Vector3(0.0, 30.0, 50.0) } - val m = meshGenerator { - + val m = buildTriangleMesh { grid(5,5, 5) { u, v, w -> - 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)} ) + 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) + } } twist(360.0/200.0, 0.0) twist(360.0/200.0, 0.0, Vector3.UNIT_X) @@ -37,6 +37,7 @@ fun main() { x_fill.rgb *= v_viewNormal.z; """.trimIndent() } + drawer.drawStyle.cullTestPass = CullTestPass.FRONT drawer.vertexBuffer(m, DrawPrimitive.TRIANGLES) } } diff --git a/orx-mesh-generators/src/jvmDemo/kotlin/DemoExtrude01.kt b/orx-mesh-generators/src/jvmDemo/kotlin/DemoExtrude01.kt new file mode 100644 index 00000000..c0ed6323 --- /dev/null +++ b/orx-mesh-generators/src/jvmDemo/kotlin/DemoExtrude01.kt @@ -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) + } + } + } +} \ No newline at end of file diff --git a/orx-mesh-generators/src/jvmDemo/kotlin/DemoExtrude02.kt b/orx-mesh-generators/src/jvmDemo/kotlin/DemoExtrude02.kt new file mode 100644 index 00000000..dda3024d --- /dev/null +++ b/orx-mesh-generators/src/jvmDemo/kotlin/DemoExtrude02.kt @@ -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) + } + } + } +} \ No newline at end of file diff --git a/orx-mesh-generators/src/jvmDemo/kotlin/DemoExtrude03.kt b/orx-mesh-generators/src/jvmDemo/kotlin/DemoExtrude03.kt new file mode 100644 index 00000000..421d249a --- /dev/null +++ b/orx-mesh-generators/src/jvmDemo/kotlin/DemoExtrude03.kt @@ -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) + + } + } + } +} \ No newline at end of file diff --git a/orx-mesh-generators/src/main/kotlin/Box.kt b/orx-mesh-generators/src/main/kotlin/Box.kt deleted file mode 100644 index a159f455..00000000 --- a/orx-mesh-generators/src/main/kotlin/Box.kt +++ /dev/null @@ -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) -} \ No newline at end of file diff --git a/orx-mesh-generators/src/main/kotlin/Cylinder.kt b/orx-mesh-generators/src/main/kotlin/Cylinder.kt deleted file mode 100644 index b6078f97..00000000 --- a/orx-mesh-generators/src/main/kotlin/Cylinder.kt +++ /dev/null @@ -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)) - } - } - } -} \ No newline at end of file diff --git a/orx-mesh-generators/src/main/kotlin/GeneratorBuffer.kt b/orx-mesh-generators/src/main/kotlin/GeneratorBuffer.kt deleted file mode 100644 index 71ee89cd..00000000 --- a/orx-mesh-generators/src/main/kotlin/GeneratorBuffer.kt +++ /dev/null @@ -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() - - 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) { - generateCap(sides, radius, enveloppe, this::write) -} - -fun GeneratorBuffer.revolve(sides: Int, length: Double, enveloppe: List) { - 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, - contours: List>, - length: Double, - scale: Double = 1.0, - frontCap: Boolean = true, - backCap: Boolean = true, - sides: Boolean = true -) { - extrudeShape( - baseTriangles = baseTriangles, - contours = contours, - front = -length / 2.0, - back = length / 2.0, - frontScale = scale, - backScale = scale, - frontCap = frontCap, - backCap = backCap, - sides = sides, - flipNormals = false, - writer = this::write - ) -} - -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, 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) -} diff --git a/orx-mesh-generators/src/main/kotlin/MeshGenerators.kt b/orx-mesh-generators/src/main/kotlin/MeshGenerators.kt deleted file mode 100644 index fb7478a9..00000000 --- a/orx-mesh-generators/src/main/kotlin/MeshGenerators.kt +++ /dev/null @@ -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, contours: List>, 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, - 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 - } - } diff --git a/orx-mesh-generators/src/main/kotlin/Plane.kt b/orx-mesh-generators/src/main/kotlin/Plane.kt deleted file mode 100644 index 36992009..00000000 --- a/orx-mesh-generators/src/main/kotlin/Plane.kt +++ /dev/null @@ -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) - } - } -} diff --git a/orx-mesh-generators/src/main/kotlin/Sphere.kt b/orx-mesh-generators/src/main/kotlin/Sphere.kt deleted file mode 100644 index b8799450..00000000 --- a/orx-mesh-generators/src/main/kotlin/Sphere.kt +++ /dev/null @@ -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)) - } - } - } - } -} \ No newline at end of file