diff --git a/orx-mesh-generators/src/commonMain/kotlin/Extrusion.kt b/orx-mesh-generators/src/commonMain/kotlin/Extrusion.kt index d7c245c6..45bace82 100644 --- a/orx-mesh-generators/src/commonMain/kotlin/Extrusion.kt +++ b/orx-mesh-generators/src/commonMain/kotlin/Extrusion.kt @@ -10,34 +10,6 @@ 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 @@ -49,7 +21,7 @@ fun quadToTris( * @param writer the vertex writer function */ fun contourSegment( - linearContour: List, + linearContour: List, frame0: Matrix44, frame1: Matrix44, writer: VertexWriter @@ -58,42 +30,16 @@ fun contourSegment( 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 v00 = (frame0 * v0.xy01).xyz + val v01 = (frame0 * v1.xy01).xyz - val v10 = (frame1 * v0.xyz1).xyz - val v11 = (frame1 * v1.xyz1).xyz + val v10 = (frame1 * v0.xy01).xyz + val v11 = (frame1 * v1.xy01).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. * @@ -129,7 +75,7 @@ fun extrudeContourSteps( writer: VertexWriter ) { val linearContour = contour.sampleLinear(contourDistanceTolerance) - val linearContourPoints = linearContour.adaptivePositions().map { it.xy0 } + val linearContourPoints = linearContour.adaptivePositions() val finalFrames = if (path.closed) frames + frames.first() else frames // First add caps @@ -141,36 +87,6 @@ fun extrudeContourSteps( } } -/** - * 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. @@ -202,7 +118,7 @@ fun extrudeContourAdaptive( writer: VertexWriter ) { val linearContour = contour.sampleLinear(contourDistanceTolerance) - val linearContourPoints = linearContour.adaptivePositions().map { it.xy0 } + val linearContourPoints = linearContour.adaptivePositions() val finalFrames = if (path.closed) frames + frames.first() else frames // First add caps @@ -442,4 +358,4 @@ fun TriangleMeshBuilder.extrudeContourAdaptive( contourDistanceTolerance, pathDistanceTolerance, writer = this::write -) \ No newline at end of file +) diff --git a/orx-mesh-generators/src/commonMain/kotlin/ExtrusionMorphed.kt b/orx-mesh-generators/src/commonMain/kotlin/ExtrusionMorphed.kt new file mode 100644 index 00000000..236881c1 --- /dev/null +++ b/orx-mesh-generators/src/commonMain/kotlin/ExtrusionMorphed.kt @@ -0,0 +1,111 @@ +package org.openrndr.extra.meshgenerators + +import org.openrndr.math.Matrix44 +import org.openrndr.math.Vector3 +import org.openrndr.shape.Path3D +import org.openrndr.shape.ShapeContour + +/** + * 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 extrudeContourStepsMorphed( + contour: (Double) -> 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 finalFrames = if (path.closed) frames + frames.first() else frames + + val crossSections = List(finalFrames.size) { + val t = it / (finalFrames.size - 1.0) + val linearContour = contour(t).sampleLinear(contourDistanceTolerance) + linearContour.adaptivePositions() + } + + // Add caps + if (!path.closed) { + if (startCap) { + triangulationWithFrame( + ShapeContour.fromPoints(crossSections.first(), true).shape.triangulation, + finalFrames.first(), false, writer + ) + } + if (endCap) { + triangulationWithFrame( + ShapeContour.fromPoints(crossSections.last(), true).shape.triangulation, + finalFrames.last(), false, writer + ) + } + } + + // Add sides + var i = 0 + (finalFrames zip crossSections).windowed(2, 1).forEach { + contourSegment( + it[0].second, it[1].second, + it[0].first, it[1].first, + i.toDouble() / crossSections.size, + (++i).toDouble() / crossSections.size, + writer) + } +} + + +/** + * 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.extrudeContourStepsMorphed( + contour: (Double) -> ShapeContour, + path: Path3D, + stepCount: Int, + up0: Vector3, + startCap: Boolean = true, + endCap: Boolean = true, + contourDistanceTolerance: Double = 0.5, + pathDistanceTolerance: Double = 0.5 +) = extrudeContourStepsMorphed( + contour, + path, + stepCount, + up0, + startCap = startCap, + endCap = endCap, + contourDistanceTolerance = contourDistanceTolerance, + pathDistanceTolerance = pathDistanceTolerance, + writer = this::write +) + diff --git a/orx-mesh-generators/src/commonMain/kotlin/ExtrusionScaled.kt b/orx-mesh-generators/src/commonMain/kotlin/ExtrusionScaled.kt new file mode 100644 index 00000000..9b6dffc3 --- /dev/null +++ b/orx-mesh-generators/src/commonMain/kotlin/ExtrusionScaled.kt @@ -0,0 +1,116 @@ +package org.openrndr.extra.meshgenerators + +import org.openrndr.math.Matrix44 +import org.openrndr.math.Vector3 +import org.openrndr.shape.Path3D +import org.openrndr.shape.ShapeContour + +/** + * Extrude a [contour] along a [path] specifying the number of steps. + * The [scale] argument can be used to make variable width shapes. + * For example `scale = { t: Double -> 0.5 - 0.5 * cos(t * 2 * PI) }` + * produces an extruded shape that begins and ends with hairline thickness. + * + * @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 scale A function that takes a curve `t` value and returns + * a scaling factor for [contour] at that point. + * @param writer the vertex writer function + */ +fun extrudeContourStepsScaled( + 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, + scale: (Double) -> Double = { _ -> 1.0 }, + writer: VertexWriter +) { + val finalFrames = if (path.closed) frames + frames.first() else frames + + val linearContour = contour.sampleLinear(contourDistanceTolerance) + val linearContourPoints2D = linearContour.adaptivePositions() + + val crossSections = List(finalFrames.size) { + val t = it / (finalFrames.size - 1.0) + linearContourPoints2D.map { p -> p * scale(t) } + } + if (!path.closed) { + if (startCap) { + triangulationWithFrame( + ShapeContour.fromPoints(crossSections.first(), true).shape.triangulation, + finalFrames.first(), false, writer + ) + } + if (endCap) { + triangulationWithFrame( + ShapeContour.fromPoints(crossSections.last(), true).shape.triangulation, + finalFrames.last(), false, writer + ) + } + } + + // Then add sides + (finalFrames zip crossSections).windowed(2, 1).forEach { + contourSegment(it[0].second, it[1].second, it[0].first, it[1].first, writer) + } +} + +/** + * Extrude a [contour] along a [path] specifying the number of steps. + * The [scale] argument can be used to make variable width shapes. + * For example `scale = { t: Double -> 0.5 - 0.5 * cos(t * 2 * PI) }` + * produces an extruded shape that begins and ends with hairline thickness. + * + * @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 + * @param scale A function that converts `t` into a radius + * [path]. Lower tolerance results in higher precision. + */ +fun TriangleMeshBuilder.extrudeContourStepsScaled( + contour: ShapeContour, + path: Path3D, + stepCount: Int, + up0: Vector3, + contourDistanceTolerance: Double = 0.5, + pathDistanceTolerance: Double = 0.5, + startCap: Boolean = true, + endCap: Boolean = true, + scale: (Double) -> Double = { _ -> 1.0 } +) = extrudeContourStepsScaled( + contour, + path, + stepCount, + up0, + contourDistanceTolerance, + pathDistanceTolerance, + startCap = startCap, + endCap = endCap, + scale = scale, + writer = this::write +) + diff --git a/orx-mesh-generators/src/commonMain/kotlin/Helpers.kt b/orx-mesh-generators/src/commonMain/kotlin/Helpers.kt new file mode 100644 index 00000000..a82cc4fb --- /dev/null +++ b/orx-mesh-generators/src/commonMain/kotlin/Helpers.kt @@ -0,0 +1,112 @@ +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.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 +) { + writeTri(v11, v01, v00, Vector2.ZERO, Vector2.ZERO, Vector2.ZERO, faceNormal, writer) + writeTri(v00, v10, v11, Vector2.ZERO, Vector2.ZERO, Vector2.ZERO, faceNormal, writer) +} + +/** + * Writes a triangle to [writer]. + * + * @param v0 vertex 0 + * @param v1 vertex 1 + * @param v2 vertex 2 + * @param tc0 texture coordinate 0 + * @param tc1 texture coordinate 1 + * @param tc2 texture coordinate 2 + * @param faceNormal the face normal + * @param writer the vertex writer function + */ +fun writeTri( + v0: Vector3, v1: Vector3, v2: Vector3, + tc0: Vector2, tc1: Vector2, tc2: Vector2, + faceNormal: Vector3, + writer: VertexWriter +) { + writer(v0, faceNormal, tc0) + writer(v1, faceNormal, tc1) + writer(v2, faceNormal, tc2) +} + +/** + * 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) + } +} + +/** + * 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 + */ +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) + } + } +} diff --git a/orx-mesh-generators/src/commonMain/kotlin/Segment.kt b/orx-mesh-generators/src/commonMain/kotlin/Segment.kt new file mode 100644 index 00000000..87149adb --- /dev/null +++ b/orx-mesh-generators/src/commonMain/kotlin/Segment.kt @@ -0,0 +1,108 @@ +package org.openrndr.extra.meshgenerators + +import org.openrndr.math.Matrix44 +import org.openrndr.math.Vector2 +import org.openrndr.math.Vector3 + +/** + * Writes quads to [writer] creating a surface that connects + * [linearContour0] with [linearContour1]. + * The positions and orientations of the two contours + * are defined by the [frame0] and [frame1] matrices. + * + * @param linearContour0 the first cross-section + * @param linearContour1 the second cross-section + * @param frame0 a transformation matrix with the pose of [linearContour0] + * @param frame1 a transformation matrix with the pose of [linearContour1] + * @param writer the vertex writer function + */ +fun contourSegment( + linearContour0: List, + linearContour1: List, + frame0: Matrix44, + frame1: Matrix44, + writer: VertexWriter +) { + for (i in linearContour0.indices) { + val v0a = linearContour0[i] + val v1a = linearContour0[(i + 1).mod(linearContour0.size)] + + val v0b = linearContour1[i] + val v1b = linearContour1[(i + 1).mod(linearContour1.size)] + + val v00a = (frame0 * v0a.xy01).xyz + val v01a = (frame0 * v1a.xy01).xyz + + val v10b = (frame1 * v0b.xy01).xyz + val v11b = (frame1 * v1b.xy01).xyz + val faceNormal = ((v10b - v00a).normalized cross (v01a - v00a).normalized).normalized + quadToTris(v00a, v01a, v10b, v11b, faceNormal, writer) + } +} + +/** + * Contour segment + * + * @param linearContour0 + * @param linearContour1 + * @param frame0 + * @param frame1 + * @param v0 + * @param v1 + * @param writer + */ +fun contourSegment( + linearContour0: List, + linearContour1: List, + frame0: Matrix44, + frame1: Matrix44, + v0: Double, + v1: Double, + writer: VertexWriter +) { + var i0 = 0 + var i1 = 0 + do { + val flipNormal = false + + val vCurr0 = (frame0 * linearContour0[i0 % linearContour0.size].xy01).xyz + val vCurr1 = (frame1 * linearContour1[i1 % linearContour1.size].xy01).xyz + val vNext0 = (frame0 * linearContour0[(i0 + 1) % linearContour0.size].xy01).xyz + val vNext1 = (frame1 * linearContour1[(i1 + 1) % linearContour1.size].xy01).xyz + + val tCurr0 = i0.toDouble() / linearContour0.size + val tCurr1 = i1.toDouble() / linearContour1.size + val tNext0 = (i0 + 1.0) / linearContour0.size + val tNext1 = (i1 + 1.0) / linearContour1.size + + val uvCurr0 = Vector2(tCurr0, v0) + val uvCurr1 = Vector2(tCurr1, v1) + + val vNext: Vector3 + val uvNext: Vector2 + + if (tNext0 < tNext1 || (tNext0 == tNext1 && tCurr0 < tCurr1)) { + ++i0 + vNext = vNext0 + uvNext = Vector2(i0.toDouble() / linearContour0.size, v0) + } else { + ++i1 + vNext = vNext1 + uvNext = Vector2(i1.toDouble() / linearContour1.size, v1) + } + + val faceNormal = if (flipNormal) + (vNext - vCurr1).cross(vCurr0 - vCurr1) + else + (vNext - vCurr0).cross(vCurr1 - vCurr0) + + writeTri( + vCurr0, vNext, vCurr1, + uvCurr0, uvNext, uvCurr1, + faceNormal.normalized, + writer + ) + + } while (i0 < linearContour0.size || i1 < linearContour1.size) +} + diff --git a/orx-mesh-generators/src/jvmDemo/kotlin/DemoExtrude05.kt b/orx-mesh-generators/src/jvmDemo/kotlin/DemoExtrude05.kt new file mode 100644 index 00000000..2d80da3c --- /dev/null +++ b/orx-mesh-generators/src/jvmDemo/kotlin/DemoExtrude05.kt @@ -0,0 +1,89 @@ +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.extrudeContourStepsScaled +import org.openrndr.extra.noise.Random +import org.openrndr.math.Vector3 +import org.openrndr.shape.Circle +import org.openrndr.shape.Path3D +import org.openrndr.shape.Segment3D +import kotlin.math.PI +import kotlin.math.cos + +/** + * Extruded Bézier tubes grown on a morphing Bézier surface. + * + */ +fun main() { + application { + configure { + width = 800 + height = 800 + multisample = WindowMultisample.SampleCount(8) + } + program { + val crossSection = Circle(0.0, 0.0, 0.2).contour + + extend(Orbital()) { + this.eye = Vector3(0.0, 3.0, 7.0) + this.lookAt = Vector3(0.0, 0.0, 0.0) + } + + extend { + drawer.shadeStyle = shadeStyle { + fragmentTransform = """ + x_fill = va_color; + x_fill.rgb *= v_viewNormal.z; + """.trimIndent() + } + + val m = buildTriangleMesh { + val beziers = List(4) { curveId -> + val n = List(12) { + Random.simplex(it * 7.387, curveId * 5.531 + seconds * 0.05) * 10.0 + } + Segment3D( + Vector3(n[0], n[1], n[2]), + Vector3(n[3], n[4], n[5]), + Vector3(n[6], n[7], n[8]), + Vector3(n[9], n[10], n[11]) + ) + } + + for (i in 0 until 20) { + val t = i / (20.0 - 1.0) + val path = Path3D( + listOf( + Segment3D( + beziers[0].position(t), + beziers[1].position(t), + beziers[2].position(t), + beziers[3].position(t) + ) + ), false + ) + color = if(i % 2 == 0) ColorRGBa.PINK else ColorRGBa.WHITE.shade(0.1) + extrudeContourStepsScaled( + crossSection, + path, + 120, + Vector3.UNIT_Y, + contourDistanceTolerance = 0.05, + pathDistanceTolerance = 0.05, + scale = { tt: Double -> 0.5 - 0.5 * cos(tt * 2 * PI) } + ) + } + } + + drawer.vertexBuffer(m, DrawPrimitive.TRIANGLES) + + // Remember to free the memory! Otherwise, the computer will quickly run out of RAM. + m.destroy() + } + } + } +} diff --git a/orx-mesh-generators/src/jvmDemo/kotlin/DemoExtrude06.kt b/orx-mesh-generators/src/jvmDemo/kotlin/DemoExtrude06.kt new file mode 100644 index 00000000..44cc90d7 --- /dev/null +++ b/orx-mesh-generators/src/jvmDemo/kotlin/DemoExtrude06.kt @@ -0,0 +1,130 @@ +import org.openrndr.WindowMultisample +import org.openrndr.application +import org.openrndr.color.ColorRGBa +import org.openrndr.draw.* +import org.openrndr.extra.camera.Orbital +import org.openrndr.extra.meshgenerators.buildTriangleMesh +import org.openrndr.extra.meshgenerators.extrudeContourStepsMorphed +import org.openrndr.extra.noise.Random +import org.openrndr.extra.noise.simplex +import org.openrndr.math.Polar +import org.openrndr.math.Vector2 +import org.openrndr.math.Vector3 +import org.openrndr.math.transforms.transform +import org.openrndr.shape.Circle +import org.openrndr.shape.Path3D +import org.openrndr.shape.Segment3D +import kotlin.math.PI +import kotlin.math.cos + +/** + * Demo extrudeContourStepsMorphed which allows to create a mesh with a morphing cross-section + * based on the t value along a Path3D. In other words, a tube in which the cross-section does not need + * to be constant, but can be scaled, rotated and displaced along its curvy axis. + */ +fun main() { + application { + configure { + width = 800 + height = 800 + multisample = WindowMultisample.SampleCount(8) + } + program { + Random.seed = System.currentTimeMillis().toString() + + val texture = loadImage("demo-data/images/peopleCity01.jpg").also { + it.wrapU = WrapMode.REPEAT + it.wrapV = WrapMode.REPEAT + it.filterMag = MagnifyingFilter.LINEAR + it.filterMin = MinifyingFilter.LINEAR + } + + val shader = shadeStyle { + fragmentTransform = """ + // A. Passed color + x_fill = va_color; + + // B. Sample texture + //x_fill = texture(p_img, va_texCoord0.yx * vec2(20.0, 1.0)); + + // Show (add) UV coords + x_fill.rb += va_texCoord0.yx; + + // Vertical lighting + x_fill.rgb *= dot(vec3(0.0, 1.0, 0.0), v_viewNormal.xyz) * 0.3 + 0.7; + + // Black fog (darken far away shapes) + x_fill.rgb += v_viewPosition.z * 0.05; + """.trimIndent() + parameter("img", texture) + } + + extend(Orbital()) { + eye = Vector3(0.0, 3.0, 7.0) + lookAt = Vector3(0.0, 0.0, 0.0) + } + extend { + drawer.stroke = null + + val path = get3DPath(10.0, seconds * 0.05, 400) + val tubes = makeTubes(path, seconds * 0.2) + + shader.parameter("seconds", seconds * 0.1) + drawer.fill = ColorRGBa.WHITE + drawer.shadeStyle = shader + tubes.forEachIndexed { i, vb -> + shader.parameter("offset", i * 0.3 + 0.2) + + // Mirror the mesh 5 times + repeat(5) { + drawer.isolated { + rotate(Vector3.UNIT_Z, it * 72.0) + vertexBuffer(vb, DrawPrimitive.TRIANGLES) + } + } + + // Remember to free the memory! Otherwise, the computer will quickly run out of RAM. + vb.destroy() + } + + } + } + } +} + +val crossSection = Circle(Vector2.ZERO, 0.1).contour.transform( + transform { scale(5.0, 1.0, 1.0) } +) + +// Create simplex-based 3D path +fun get3DPath(scale: Double, time: Double, steps: Int): Path3D { + val mult = 0.005 + val points = List(steps) { Vector3.simplex(337, time + it * mult) * scale } + return Path3D(points.windowed(2).map { Segment3D(it[0], it[1]) }, false) +} + +// Create 3 spinning tubes around path +fun makeTubes(path: Path3D, seconds: Double) = List(3) { i -> + buildTriangleMesh { + val degrees = seconds * 60 + val crossSection = crossSection + + color = listOf(ColorRGBa.RED, ColorRGBa.GREEN, ColorRGBa.BLUE)[i] + extrudeContourStepsMorphed( + { t: Double -> + val turns = t * 360 * 10 + val cosEnv = 0.5 - 0.49 * cos(t * 2 * PI) + crossSection.transform(transform { + val theta = i * 120.0 + translate(Polar(turns - degrees + theta, 0.6 * cosEnv).cartesian) + rotate(-turns - degrees) + scale(cosEnv) + }) + }, + path, + path.segments.size, + Vector3.UNIT_Y, + contourDistanceTolerance = 0.01 + ) + } +}