diff --git a/orx-shapes/src/commonMain/kotlin/BezierPatch3D.kt b/orx-shapes/src/commonMain/kotlin/BezierPatch3D.kt new file mode 100644 index 00000000..d60d406c --- /dev/null +++ b/orx-shapes/src/commonMain/kotlin/BezierPatch3D.kt @@ -0,0 +1,274 @@ +package org.openrndr.extra.shapes + +import org.openrndr.color.AlgebraicColor +import org.openrndr.color.ColorRGBa +import org.openrndr.color.ConvertibleToColorRGBa +import org.openrndr.math.Matrix44 +import org.openrndr.math.Vector3 +import org.openrndr.shape.Path3D +import org.openrndr.shape.Segment3D +import kotlin.random.Random + +open class BezierPatch3DBase( + val points: List>, + val colors: List> = emptyList() +) + where C : AlgebraicColor, C : ConvertibleToColorRGBa { + init { + require(points.size == 4 && points.all { it.size == 4 }) + require(colors.isEmpty() || colors.size == 4 && colors.all { it.size == 4 }) + } + + /** + * Return a transposed version of the bezier path by transposing the [points] matrix + */ + val transposed + get() = BezierPatch3DBase( + listOf( + listOf(points[0][0], points[1][0], points[2][0], points[3][0]), + listOf(points[0][1], points[1][1], points[2][1], points[3][1]), + listOf(points[0][2], points[1][2], points[2][2], points[3][2]), + listOf(points[0][3], points[1][3], points[2][3], points[3][3]), + ), + if (colors.isEmpty()) emptyList() else { + listOf( + listOf(colors[0][0], colors[1][0], colors[2][0], colors[3][0]), + listOf(colors[0][1], colors[1][1], colors[2][1], colors[3][1]), + listOf(colors[0][2], colors[1][2], colors[2][2], colors[3][2]), + listOf(colors[0][3], colors[1][3], colors[2][3], colors[3][3]), + ) + } + ) + + fun transform(transform: Matrix44) = BezierPatch3DBase(points.map { r -> + r.map { (transform * it.xyz1).div } + }, colors) + + private fun coeffs2(t: Double): DoubleArray { + val it = 1.0 - t + val it2 = it * it + val t2 = t * t + return doubleArrayOf(it2, 2 * it * t, t2) + } + + private fun coeffs3(t: Double): DoubleArray { + val it = 1.0 - t + val it2 = it * it + val it3 = it2 * it + val t2 = t * t + val t3 = t2 * t + return doubleArrayOf(it3, 3 * it2 * t, 3 * it * t2, t3) + } + + /** + * Return a point on the patch by using its u,v parameterization + * @param u a value between 0 and 1 + * @param v a value between 0 and 1 + */ + fun position(u: Double, v: Double): Vector3 { + val csu = coeffs3(u) + val csv = coeffs3(v) + var result = Vector3.ZERO + for (j in 0 until 4) { + for (i in 0 until 4) { + result += points[j][i] * csu[i] * csv[j] + } + } + return result + } + + /** + * Return a gradient vector on the patch by using its u,v parameterization + * @param u a value between 0 and 1 + * @param v a value between 0 and 1 + */ + fun gradient(u: Double, v: Double): Vector3 { + val f0 = List(4) { MutableList(3) { Vector3.ZERO } } + for (j in 0 until 4) { + for (i in 0 until 3) { + f0[j][i] = points[j][i + 1] - points[j][i] + } + } + + val f1 = List(3) { MutableList(3) { Vector3.ZERO } } + for (j in 0 until 3) { + for (i in 0 until 3) { + f1[j][i] = f0[j + 1][i] - f0[j][i] + } + } + + val csu = coeffs2(u) + val csv = coeffs2(v) + var result = Vector3.ZERO + for (j in 0 until 3) { + for (i in 0 until 3) { + result += f1[j][i] * csu[i] * csv[j] + } + } + return result + } + + /** + * Generate a random point on the path + * @return a point that is uniformly distributed in uv space + */ + fun randomPoint(random: Random = Random.Default) = position(random.nextDouble(), random.nextDouble()) + + fun horizontal(v: Double): Path3D { + val cs = coeffs3(v) + val cps = Array(4) { Vector3.ZERO } + for (j in 0 until 4) { + for (i in 0 until 4) { + cps[j] += points[i][j] * cs[i] + } + } + return Path3D(listOf(Segment3D(cps[0], cps[1], cps[2], cps[3])), false) + } + + fun vertical(u: Double): Path3D { + val cs = coeffs3(u) + val cps = Array(4) { Vector3.ZERO } + for (j in 0 until 4) { + for (i in 0 until 4) { + cps[j] += points[j][i] * cs[i] + } + } + return Path3D(listOf(Segment3D(cps[0], cps[1], cps[2], cps[3])), false) + } + + /** + * Extract a sub-patch based on uv parameterization + */ + fun sub(u0: Double, v0: Double, u1: Double, v1: Double): BezierPatch3DBase { + val c0 = Segment3D(points[0][0], points[0][1], points[0][2], points[0][3]).sub(u0, u1) + val c1 = Segment3D(points[1][0], points[1][1], points[1][2], points[1][3]).sub(u0, u1) + val c2 = Segment3D(points[2][0], points[2][1], points[2][2], points[2][3]).sub(u0, u1) + val c3 = Segment3D(points[3][0], points[3][1], points[3][2], points[3][3]).sub(u0, u1) + + val sub0 = bezierPatch(c0, c1, c2, c3) + val d0 = Segment3D(sub0.points[0][0], sub0.points[1][0], sub0.points[2][0], sub0.points[3][0]).sub(v0, v1) + val d1 = Segment3D(sub0.points[0][1], sub0.points[1][1], sub0.points[2][1], sub0.points[3][1]).sub(v0, v1) + val d2 = Segment3D(sub0.points[0][2], sub0.points[1][2], sub0.points[2][2], sub0.points[3][2]).sub(v0, v1) + val d3 = Segment3D(sub0.points[0][3], sub0.points[1][3], sub0.points[2][3], sub0.points[3][3]).sub(v0, v1) + + return fromSegments(d0, d1, d2, d3).transposed + } + + val path: Path3D = Path3D( + listOf( + Segment3D(points[0][0], points[0][1], points[0][2], points[0][3]), + Segment3D(points[0][3], points[1][3], points[2][3], points[3][3]), + Segment3D(points[3][3], points[3][2], points[3][1], points[3][0]), + Segment3D(points[3][0], points[2][0], points[1][0], points[0][0]), + ), true + ) + + operator fun times(scale: Double) = + BezierPatch3DBase( + points.map { j -> j.map { i -> i * scale } }, + if (colors.isEmpty()) colors else colors.map { j -> j.map { i -> i * scale } } + ) + + operator fun div(scale: Double) = + BezierPatch3DBase(points.map { j -> j.map { i -> i / scale } }, + if (colors.isEmpty()) colors else colors.map { j -> j.map { i -> i / scale } } + ) + operator fun plus(right: BezierPatch3DBase) = + BezierPatch3DBase(List(4) { j -> List(4) { i -> points[j][i] + right.points[j][i] } }, + if (colors.isEmpty() && right.colors.isEmpty()) { colors } + else if (colors.isEmpty() && right.colors.isNotEmpty()) { right.colors } + else if (colors.isNotEmpty() && right.colors.isEmpty()) { colors } + else { List(4) { j -> List(4) { i -> colors[j][i] + right.colors[j][i] } } } + ) + + operator fun minus(right: BezierPatch3DBase) = + BezierPatch3DBase(List(4) { j -> List(4) { i -> points[j][i] - right.points[j][i] } }, + if (colors.isEmpty() && right.colors.isEmpty()) { colors } + else if (colors.isEmpty() && right.colors.isNotEmpty()) { right.colors } + else if (colors.isNotEmpty() && right.colors.isEmpty()) { colors } + else { List(4) { j -> List(4) { i -> colors[j][i] - right.colors[j][i] } } } + ) + + fun withColors(colors: List>): BezierPatch3DBase + where K : AlgebraicColor, K : ConvertibleToColorRGBa { + return BezierPatch3DBase(points, colors) + } + + companion object { + fun fromSegments(c0: Segment3D, c1: Segment3D, c2: Segment3D, c3: Segment3D): BezierPatch3DBase + where C : AlgebraicColor, C : ConvertibleToColorRGBa { + val c0c = c0.cubic + val c1c = c1.cubic + val c2c = c2.cubic + val c3c = c3.cubic + + val c0l = listOf(c0c.start, c0c.control[0], c0c.control[1], c0c.end) + val c1l = listOf(c1c.start, c1c.control[0], c1c.control[1], c1c.end) + val c2l = listOf(c2c.start, c2c.control[0], c2c.control[1], c2c.end) + val c3l = listOf(c3c.start, c3c.control[0], c3c.control[1], c3c.end) + + return BezierPatch3DBase(listOf(c0l, c1l, c2l, c3l)) + } + } +} + +class BezierPatch3D(points: List>, colors: List> = emptyList()) : + BezierPatch3DBase(points, colors) + +/** + * Create a cubic bezier patch from 4 segments. The control points of the segments are used in row-wise fashion + */ +fun bezierPatch(c0: Segment3D, c1: Segment3D, c2: Segment3D, c3: Segment3D): BezierPatch3D { + val c0c = c0.cubic + val c1c = c1.cubic + val c2c = c2.cubic + val c3c = c3.cubic + + val c0l = listOf(c0c.start, c0c.control[0], c0c.control[1], c0c.end) + val c1l = listOf(c1c.start, c1c.control[0], c1c.control[1], c1c.end) + val c2l = listOf(c2c.start, c2c.control[0], c2c.control[1], c2c.end) + val c3l = listOf(c3c.start, c3c.control[0], c3c.control[1], c3c.end) + + return BezierPatch3D(listOf(c0l, c1l, c2l, c3l)) +} + +/** + * Create a bezier patch from a closed shape contour (with 4 segments). + * @param alpha control for linearity, default is `1.0/3.0` + */ +fun bezierPatch(path: Path3D, alpha: Double = 1.0 / 3.0): BezierPatch3D { + require(path.segments.size == 4) { + """contour needs exactly 4 segments (has ${path.segments.size})""" + } + val c0 = path.segments[0].cubic + val c1 = path.segments[1].cubic + val c2 = path.segments[2].cubic + val c3 = path.segments[3].cubic + + val fa = 1.0 - alpha + val fb = alpha + + val x00 = (c0.control[0] * fa + c2.control[1] * fb + c3.control[1] * fa + c1.control[0] * fb) / 2.0 + val x01 = (c0.control[1] * fa + c2.control[0] * fb + c3.control[1] * fb + c1.control[0] * fa) / 2.0 + val x10 = (c0.control[0] * fb + c2.control[1] * fa + c3.control[0] * fa + c1.control[1] * fb) / 2.0 + val x11 = (c0.control[1] * fb + c2.control[0] * fa + c3.control[0] * fb + c1.control[1] * fa) / 2.0 + val cps = listOf( + listOf(c0.start, c0.control[0], c0.control[1], c0.end), + listOf(c3.control[1], x00, x01, c1.control[0]), + listOf(c3.control[0], x10, x11, c1.control[1]), + listOf(c2.end, c2.control[1], c2.control[0], c2.start), + ) + return BezierPatch3D(cps) +} + +/** + * Create a bezier patch from 4 corners + * @param corners a list of corners from which to create the patch + * @param alpha control for linearity, default is `1.0/3.0` + */ +fun bezierPatch(corners: List, alpha: Double = 1.0 / 3.0): BezierPatch3D { + require(corners.size == 4) { + """need exactly 4 corners (got ${corners.size}""" + } + return bezierPatch(Path3D.fromPoints(corners, true), alpha) +} diff --git a/orx-shapes/src/commonMain/kotlin/drawers/BezierPatchDrawer.kt b/orx-shapes/src/commonMain/kotlin/drawers/BezierPatchDrawer.kt index 1594e5d6..4c95a1c8 100644 --- a/orx-shapes/src/commonMain/kotlin/drawers/BezierPatchDrawer.kt +++ b/orx-shapes/src/commonMain/kotlin/drawers/BezierPatchDrawer.kt @@ -3,6 +3,7 @@ package org.openrndr.extra.shapes.drawers import org.openrndr.color.ColorRGBa import org.openrndr.draw.* import org.openrndr.extra.shapes.BezierPatchBase +import org.openrndr.extra.shapes.BezierPatch3DBase import org.openrndr.internal.Driver import org.openrndr.math.Vector2 @@ -258,6 +259,97 @@ class BezierPatchDrawer { ) shader.end() } + + @JvmName("drawBezierPatch3D") + fun drawBezierPatches( + context: DrawContext, + drawStyle: DrawStyle, + bezierPatches: List>, + subdivisions: Int = 32 + ) { + ensureVertexCount(bezierPatches.size * 16) + val shader = shadeStyleManager.shader( + drawStyle.shadeStyle, + listOf(vertices.vertexFormat), + emptyList() + ) + vertices.put { + for (bezierPatch in bezierPatches) { + for (j in 0 until 4) { + for (i in 0 until 4) { + write(bezierPatch.points[j][i]) + if (bezierPatch.colors.isEmpty()) { + write(ColorRGBa.WHITE) + } else { + write(bezierPatch.colors[j][i]) + } + write(Vector2(i / 3.0, j / 3.0)) + } + } + } + } + shader.begin() + shader.uniform("u_subdivisions", subdivisions) + context.applyToShader(shader) + drawStyle.applyToShader(shader) + Driver.instance.setState(drawStyle) + Driver.instance.drawVertexBuffer( + shader, + listOf(vertices), + DrawPrimitive.PATCHES, + 0, + 16 * bezierPatches.size, + 16 + ) + shader.end() + } + + @JvmName("drawBezierPatch3DOKLab") + fun drawBezierPatches( + context: DrawContext, + drawStyle: DrawStyle, + bezierPatches: List>, + subdivisions: Int = 32 + ) { + ensureVertexCount(bezierPatches.size * 16) + val shader = shadeStyleManagerOKLab.shader( + drawStyle.shadeStyle, + listOf(vertices.vertexFormat), + emptyList() + ) + + vertices.put { + for(bezierPatch in bezierPatches) { + for (j in 0 until 4) { + for (i in 0 until 4) { + write(bezierPatch.points[j][i]) + if (bezierPatch.colors.isEmpty()) { + write(ColorRGBa.WHITE) + } else { + write(bezierPatch.colors[j][i].let { + Vector4(it.l, it.a, it.b, it.alpha) + }) + } + write(Vector2(i / 3.0, j / 3.0)) + } + } + } + } + shader.begin() + shader.uniform("u_subdivisions", subdivisions) + context.applyToShader(shader) + drawStyle.applyToShader(shader) + Driver.instance.setState(drawStyle) + Driver.instance.drawVertexBuffer( + shader, + listOf(vertices), + DrawPrimitive.PATCHES, + 0, + 16 * bezierPatches.size, + 16 + ) + shader.end() + } } private val Drawer.bezierPatchDrawer: BezierPatchDrawer by lazy { BezierPatchDrawer() } @@ -280,4 +372,24 @@ fun Drawer.bezierPatch(bezierPatch: BezierPatchBase, subdivisions: @JvmName("bezierPatchesOKLAB") fun Drawer.bezierPatches(bezierPatch: List>, subdivisions: Int = 32) { bezierPatchDrawer.drawBezierPatches(context, drawStyle, bezierPatch, subdivisions) -} \ No newline at end of file +} + +@JvmName("bezierPatch3DRGBa") +fun Drawer.bezierPatch(bezierPatch: BezierPatch3DBase, subdivisions: Int = 32) { + bezierPatchDrawer.drawBezierPatches(context, drawStyle, listOf(bezierPatch), subdivisions) +} + +@JvmName("bezierPatches3DRGBa") +fun Drawer.bezierPatches(bezierPatch: List>, subdivisions: Int = 32) { + bezierPatchDrawer.drawBezierPatches(context, drawStyle, bezierPatch, subdivisions) +} + +@JvmName("bezierPatch3DOKLAB") +fun Drawer.bezierPatch(bezierPatch: BezierPatch3DBase, subdivisions: Int = 32) { + bezierPatchDrawer.drawBezierPatches(context, drawStyle, listOf(bezierPatch), subdivisions) +} + +@JvmName("bezierPatches3DOKLAB") +fun Drawer.bezierPatches(bezierPatch: List>, subdivisions: Int = 32) { + bezierPatchDrawer.drawBezierPatches(context, drawStyle, bezierPatch, subdivisions) +} diff --git a/orx-shapes/src/demo/kotlin/DemoBezierPatch05.kt b/orx-shapes/src/demo/kotlin/DemoBezierPatch05.kt new file mode 100644 index 00000000..0c65198e --- /dev/null +++ b/orx-shapes/src/demo/kotlin/DemoBezierPatch05.kt @@ -0,0 +1,82 @@ +import org.openrndr.WindowMultisample +import org.openrndr.application +import org.openrndr.color.ColorRGBa +import org.openrndr.draw.BufferMultisample +import org.openrndr.extensions.SingleScreenshot +import org.openrndr.extra.shapes.bezierPatch +import org.openrndr.extra.shapes.drawers.bezierPatch +import org.openrndr.extras.camera.Orbital +import org.openrndr.math.Vector3 +import org.openrndr.shape.Segment3D + +/** + * Shows how to + * - create a [bezierPatch] out of 4 [Segment3D] + * - create a sub-patch out of a [bezierPatch] + * - create horizontal and vertical [Path3D]s out of [bezierPatch]es + * - add colors to a [bezierPatch] + * - draw a [bezierPatch] surface + * + * The created contours are horizontal and vertical in "bezier-patch space" but + * are rendered deformed following the shape of the bezier patch. + */ +suspend 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") + multisample = BufferMultisample.SampleCount(8) + } + } + + val c0 = Segment3D(Vector3(-5.0, 0.0, -9.0), Vector3(5.0, 0.0, -9.0)) + val c1 = Segment3D(Vector3(-5.0, -5.0, -3.0), Vector3(5.0, -5.0, -3.0)) + val c2 = Segment3D(Vector3(-5.0, 5.0, 3.0), Vector3(5.0, 5.0, 3.0)) + val c3 = Segment3D(Vector3(-5.0, 0.0, 9.0), Vector3(5.0, 0.0, 9.0)) + + val col = listOf(ColorRGBa.PINK, ColorRGBa.RED, ColorRGBa.BLUE, ColorRGBa.PINK) + val cols = listOf(col, col, col, col) + val bp = bezierPatch(c0, c1, c2, c3).withColors(cols) + val bpSub = bp.sub(0.1, 0.1, 0.6, 0.6) + + val cam = Orbital() + extend(cam){ + eye = Vector3(x=9.9, y=12.8, z=6.9) + lookAt = Vector3(x=1.6, y=-1.9, z=1.2) + } + + extend { + drawer.clear(ColorRGBa.PINK) + + drawer.translate(-5.0, 0.0, 0.0) + // Show the segments that form the bezier patch + drawer.stroke = ColorRGBa.YELLOW + drawer.strokeWeight = 50.0 + drawer.segments(listOf(c0, c1, c2, c3)) + + // Show the grid + drawer.strokeWeight = 1.0 + val n = 10 + for (i in 0..n) { + drawer.stroke = ColorRGBa.BLACK + drawer.lineStrip(bp.horizontal(i / n.toDouble()).adaptivePositions(0.01)) + drawer.lineStrip(bp.vertical(i / n.toDouble()).adaptivePositions(0.01)) + + drawer.stroke = ColorRGBa.RED + drawer.lineStrip(bpSub.horizontal(i / n.toDouble()).adaptivePositions(0.01)) + drawer.lineStrip(bpSub.vertical(i / n.toDouble()).adaptivePositions(0.01)) + } + + // Draw the colored Bezier surface + drawer.translate(10.0, 0.0, 0.0) + drawer.bezierPatch(bp) + } + } + } +} \ No newline at end of file