From c1d2ea4eccc47003763bf28983ca7dcf8186006e Mon Sep 17 00:00:00 2001 From: Edwin Jakobs Date: Sat, 25 Jan 2025 13:57:39 +0100 Subject: [PATCH] [orx-mesh-noise] Refactor barycentric and mesh sampling logic into specialized files. --- .../src/commonMain/kotlin/Barycentric.kt | 65 +++++ .../kotlin/IIndexedPolygonExtensions.kt | 24 ++ .../commonMain/kotlin/IMeshDataExtensions.kt | 273 ++++++++++++++++++ .../src/commonMain/kotlin/MeshNoise.kt | 201 ------------- .../src/commonMain/kotlin/WeightFunction.kt | 26 ++ .../src/jvmDemo/kotlin/DemoMeshNoise01.kt | 44 ++- .../src/jvmDemo/kotlin/DemoMeshNoise02.kt | 49 ++-- .../src/jvmDemo/kotlin/DemoMeshNoise03.kt | 57 ++++ .../kotlin/DemoNonUniformMeshNoise01.kt | 75 +++++ 9 files changed, 562 insertions(+), 252 deletions(-) create mode 100644 orx-mesh-noise/src/commonMain/kotlin/Barycentric.kt create mode 100644 orx-mesh-noise/src/commonMain/kotlin/IIndexedPolygonExtensions.kt create mode 100644 orx-mesh-noise/src/commonMain/kotlin/IMeshDataExtensions.kt delete mode 100644 orx-mesh-noise/src/commonMain/kotlin/MeshNoise.kt create mode 100644 orx-mesh-noise/src/commonMain/kotlin/WeightFunction.kt create mode 100644 orx-mesh-noise/src/jvmDemo/kotlin/DemoMeshNoise03.kt create mode 100644 orx-mesh-noise/src/jvmDemo/kotlin/DemoNonUniformMeshNoise01.kt diff --git a/orx-mesh-noise/src/commonMain/kotlin/Barycentric.kt b/orx-mesh-noise/src/commonMain/kotlin/Barycentric.kt new file mode 100644 index 00000000..6a4c6e5a --- /dev/null +++ b/orx-mesh-noise/src/commonMain/kotlin/Barycentric.kt @@ -0,0 +1,65 @@ +package org.openrndr.extra.mesh.noise + +import org.openrndr.extra.noise.fhash1D +import org.openrndr.math.Vector3 +import kotlin.math.sqrt +import kotlin.random.Random + +typealias Barycentric = Vector3 + + +/** + * Generates a 3D vector with components representing uniform barycentric coordinates + * over a standard triangle. The barycentric coordinates are computed based on two + * input parameters, `u` and `v`, which are random values typically ranging between 0 and 1. + * + * @param u A random value used to compute the barycentric coordinate. + * @param v A random value used to compute the barycentric coordinate. + * @return A [Vector3] instance representing uniform barycentric coordinates, where each component + * corresponds to the weight of a vertex in the triangle. + */ +fun uniformBarycentric(u: Double, v: Double): Barycentric { + val su0 = sqrt(u) + val b0 = 1.0 - su0 + val b1 = v * su0 + return Barycentric(b0, b1, 1.0 - b0 - b1) +} + +/** + * Adjusts a barycentric coordinate based on weights for each component and normalizes the result. + * + * @param barycentric The original barycentric coordinate to be adjusted. + * @param weight0 The weight for the first component of the barycentric coordinate. + * @param weight1 The weight for the second component of the barycentric coordinate. + * @param weight2 The weight for the third component of the barycentric coordinate. + * @return A new barycentric coordinate with components adjusted by their corresponding weights and normalized. + */ +fun weightBarycentric(barycentric: Barycentric, weight0: Double, weight1: Double, weight2: Double): Barycentric { + val b0 = barycentric.x * weight0 + val b1 = barycentric.y * weight1 + val b2 = barycentric.z * weight2 + val totalWeight = b0 + b1 + b2 + return Barycentric(b0 / totalWeight, b1 / totalWeight, b2 / totalWeight) +} + +/** + * Computes a barycentric coordinate vector derived from hashing operations. + * + * This function generates a barycentric coordinate for a point within a triangle + * by utilizing hash-based techniques to ensure uniform randomization. The result + * is expressed as a 3D vector where the components represent the barycentric + * weights. + * + * @param seed An integer seed used for the hash function to ensure reproducibility. + * @param x An integer value that contributes to the hash-based computation. + * @return A `Vector3` instance containing the computed barycentric coordinates. + */ +fun hashBarycentric(seed: Int, x: Int): Barycentric { + val u = fhash1D(seed, x) + val v = fhash1D(seed, u.toRawBits().toInt() - x) + + val su0 = sqrt(u) + val b0 = 1.0 - su0 + val b1 = v * su0 + return Vector3(b0, b1, 1.0 - b0 - b1) +} diff --git a/orx-mesh-noise/src/commonMain/kotlin/IIndexedPolygonExtensions.kt b/orx-mesh-noise/src/commonMain/kotlin/IIndexedPolygonExtensions.kt new file mode 100644 index 00000000..9e141b01 --- /dev/null +++ b/orx-mesh-noise/src/commonMain/kotlin/IIndexedPolygonExtensions.kt @@ -0,0 +1,24 @@ +package org.openrndr.extra.mesh.noise + +import org.openrndr.extra.mesh.IIndexedPolygon +import org.openrndr.extra.mesh.IVertexData + + +/** + * Calculates the area of the triangular polygon. + * + * The method assumes that the polygon is a triangle and computes its area + * using the cross product formula. The computed area is a positive value as it + * represents the absolute area of the triangle. + * + * @param vertexData the vertex data containing positional information of the polygon vertices + * @return the area of the triangle as a Double + * @throws IllegalArgumentException if the polygon is not a triangle (i.e., does not have exactly 3 vertices) + */ +internal fun IIndexedPolygon.area(vertexData: IVertexData): Double { + require(positions.size == 3) { "polygon must be a triangle" } + val x = vertexData.positions.slice(positions) + val u = x[1] - x[0] + val v = x[2] - x[0] + return u.areaBetween(v) / 2.0 +} diff --git a/orx-mesh-noise/src/commonMain/kotlin/IMeshDataExtensions.kt b/orx-mesh-noise/src/commonMain/kotlin/IMeshDataExtensions.kt new file mode 100644 index 00000000..e1d30aac --- /dev/null +++ b/orx-mesh-noise/src/commonMain/kotlin/IMeshDataExtensions.kt @@ -0,0 +1,273 @@ +package org.openrndr.extra.mesh.noise + +import org.openrndr.extra.mesh.* +import org.openrndr.extra.noise.fhash1D +import org.openrndr.extra.noise.hammersley.hammersley2D +import org.openrndr.extra.noise.rsequence.rSeq2D +import org.openrndr.math.Vector2 +import org.openrndr.math.Vector3 +import kotlin.random.Random + +/** + * Represents a type alias for a function that processes data associated with a polygon and vertex data, + * producing a result of type [T]. + * + * This function takes the following parameters: + * - `polygon`: An instance of [IIndexedPolygon] representing the polygon in 3D space. + * - `vertexData`: An instance of [IVertexData] containing the associated vertex attributes, + * such as positions, texture coordinates, normals, etc. + * - `barycentric`: A [Barycentric] representation defining the barycentric coordinates for interpolation. + */ +typealias SampleFunction = (polygon: IIndexedPolygon, vertexData: IVertexData, barycentric: Barycentric) -> T + +/** + * A lambda function that computes the 3D position within an indexed polygon given vertex data + * and barycentric coordinates. + * + * This function utilizes the `position` method of an `IIndexedPolygon` instance to calculate + * a specific point in the 3D space based on the barycentric weights and vertex positions. + * + * @property positionSampleFunction A functional instance of type `SampleFunction` where: + * - `t` is the `IIndexedPolygon`, representing the polygon. + * - `v` is the `IVertexData`, representing the vertex attributes. + * - `b` is the barycentric coordinate used for interpolation. + * The lambda computes the resulting position by invoking `t.position(v, b)`. + */ +val positionSampleFunction: SampleFunction = + { t: IIndexedPolygon, v: IVertexData, b: Barycentric -> t.position(v, b) } + +/** + * A lambda function that extracts a [Point] from a given [IIndexedPolygon], [IVertexData], and [Barycentric] coordinate. + * + * This function utilizes the `point` method of [IIndexedPolygon] to compute the [Point] in 3D space. + * + * @property t An instance of [IIndexedPolygon] representing the polygon from which the point is derived. + * @property v An instance of [IVertexData] representing the vertex data associated with the polygon. + * @property b An instance of [Barycentric] representing the barycentric coordinates used to interpolate the point. + * @return A [Point] representing the interpolated 3D position and attributes at the specified barycentric coordinate. + */ +val pointSampleFunction: SampleFunction = { t: IIndexedPolygon, v: IVertexData, b: Barycentric -> t.point(v, b) } + +/** + * Generates a list of uniformly distributed points on the surface of the given mesh. + * + * The method uses triangulation and computes areas of triangular polygons to ensure + * uniform distribution of points across the surface. + * + * @param count the number of points to generate + * @param random a random number generator instance, defaulting to [Random.Default] + * @return a list of [Vector3] points uniformly distributed across the mesh surface + */ +fun IMeshData.uniform(count: Int, random: Random = Random.Default): List { + return nonuniformEx( + (0 until count).map { random.nextDouble() }, + (0 until count).map { Vector2(random.nextDouble(), random.nextDouble()) }, + identityWeightFunction, + positionSampleFunction + ) +} + +/** + * Generates a uniformly distributed set of points over the surface of the mesh. + * + * The method creates a specific number of points distributed across the mesh, + * based on the given count, using random values to determine their positions. + * + * @param count The number of points to generate and distribute across the mesh. + * @param random The random number generator used for creating the distribution of points (default: [Random.Default]). + * @return A list of [Point] instances representing the uniformly distributed points across the mesh. + */ +fun IMeshData.uniformPoints(count: Int, random: Random = Random.Default): List { + return nonuniformEx( + (0 until count).map { random.nextDouble() }, + (0 until count).map { Vector2(random.nextDouble(), random.nextDouble()) }, + identityWeightFunction, + pointSampleFunction + ) +} + +/** + * Generates a list of points within the mesh using a non-uniform distribution. + * The points are sampled according to a provided weight function, where each point + * is influenced by the weights of the vertices of the triangulated polygons. + * + * @param count the number of points to generate within the mesh + * @param random the random number generator used for sampling; defaults to [Random.Default] + * @param weightFunction a function that calculates the weight for a given vertex of a polygon + * @return a list of [Vector3] instances representing the sampled points within the mesh + */ +fun IMeshData.nonuniform(count: Int, random: Random = Random.Default, weightFunction: WeightFunction): List { + return nonuniformEx( + (0 until count).map { random.nextDouble() }, + (0 until count).map { Vector2(random.nextDouble(), random.nextDouble()) }, + weightFunction, + positionSampleFunction + ) +} + +/** + * Generates a list of non-uniformly distributed points on the mesh based on a specified weight function. + * + * @param count The number of points to generate. + * @param random A [Random] instance used to generate random values for the distribution. Defaults to [Random.Default]. + * @param weightFunction A function used to calculate weighting for vertices in a triangle, affecting the distribution of points. + * @return A list of [Point] instances representing non-uniformly distributed points on the mesh. + */ +fun IMeshData.nonuniformPoints( + count: Int, + random: Random = Random.Default, + weightFunction: WeightFunction +): List { + return nonuniformEx( + (0 until count).map { random.nextDouble() }, + (0 until count).map { Vector2(random.nextDouble(), random.nextDouble()) }, + weightFunction, + pointSampleFunction + ) +} + +/** + * Generates a list of 3D points distributed on a mesh surface using a nonuniform sampling strategy + * with Hammersley sequence and a custom weight function. + * + * The Hammersley sequence is used to generate an initial set of 2D points that are mapped to the + * surface of the mesh based on the provided weight function. Random numbers are also utilized + * to introduce variability in the sampling process. + * + * @param count The number of points to sample on the mesh surface. + * @param random An optional random number generator used for shuffling the Hammersley points + * and generating random values for the sampling. Defaults to `Random.Default`. + * @param weightFunction A function that determines the weight of individual components of the mesh, + * influencing the distribution of generated points. + * @return A list of 3D points (`Vector3`) distributed across the mesh surface based on the provided + * weight function and sampling method. + */ +fun IMeshData.nonuniformHammersley( + count: Int, + shuffle: Boolean = true, + random: Random = Random.Default, + weightFunction: WeightFunction +): List { + return nonuniformEx( + (0 until count).map { random.nextDouble() }, + (0 until count).map { hammersley2D(it, count) }.let { if (shuffle) it.shuffled(random) else it }, + weightFunction, + positionSampleFunction + ) +} + +/** + * Generates a non-uniform sequence of 3D points based on the provided weight function, using + * a combination of randomized values and the R2 low-discrepancy quasirandom sequence. + * + * @param count The number of points to generate. + * @param shuffle Determines whether the resulting R2 sequence should be shuffled. Default is true. + * @param random The source of randomness used for shuffling and random value generation. Default is [Random.Default]. + * @param weightFunction A function that computes the weight for a given vertex data and indexed polygon. This influences + * the distribution of points across the mesh. + * @return A list of [Vector3] objects representing the generated sequence of 3D points. + */ +fun IMeshData.nonuniformRSeq( + count: Int, + shuffle: Boolean = true, + random: Random = Random.Default, + weightFunction: WeightFunction +): List { + return nonuniformEx( + (0 until count).map { random.nextDouble() }, + (0 until count).map { rSeq2D(it) }.let { if (shuffle) it.shuffled(random) else it }, + weightFunction, + positionSampleFunction + ) +} + +/** + * Generates a list of samples by distributing random points across the mesh + * using weighted areas of triangles and a provided sample function. + * + * @param T The type of the sample produced by the sample function. + * @param randomsUnsorted A list of random values in the range [0, 1), which are used to + * distribute points across the mesh based on weighted triangle areas. + * @param randomPoints A list of random 2D points in barycentric coordinates, used to + * determine the position of generated samples within a triangle. + * @param weightFunction A function that calculates weights for vertices in a triangle, + * modifying the distribution of random points based on these weights. + * @param sampleFunction A function that generates a sample of type [T] given a triangle + * (as an [IIndexedPolygon]), associated vertex data, and barycentric coordinates. + * @return A list of samples of type [T], generated by applying the sample function + * to distributed random points across the mesh. + */ +fun IMeshData.nonuniformEx( + randomValuesUnsorted: List, + randomPoints: List, + weightFunction: WeightFunction, + sampleFunction: (IIndexedPolygon, IVertexData, Barycentric) -> T +): List { + val triangulated = triangulate() + val defaultWeights = doubleArrayOf(1.0, 1.0, 1.0) + val result = ArrayList(randomPoints.size) + + // Helper function to calculate triangle weights + fun calculateWeights(triangle: IIndexedPolygon): DoubleArray = + if (weightFunction !== identityWeightFunction) + doubleArrayOf( + weightFunction(vertexData, triangle, 0), + weightFunction(vertexData, triangle, 1), + weightFunction(vertexData, triangle, 2) + ) + else defaultWeights + + // Helper function to calculate total area + fun calculateTotalArea(): Double = + triangulated.polygons.sumOf { triangle -> + val weights = calculateWeights(triangle) + triangle.area(vertexData) * weights.sum() + } + + val totalArea = calculateTotalArea() + val randomValues = randomValuesUnsorted.sorted().map { it * totalArea } + + var sum = 0.0 + var idx = 0 + + // Iterate through each triangle in the triangulated mesh + for (triangle in triangulated.polygons) { + val weights = calculateWeights(triangle) + sum += triangle.area(vertexData) * weights.sum() + + // Distribute random points across the triangle + while (idx <= randomValues.lastIndex && sum > randomValues[idx]) { + val barycentricCoords = if (weightFunction !== identityWeightFunction) { + weightBarycentric( + uniformBarycentric(randomPoints[idx].x, randomPoints[idx].y), + weights[0], weights[1], weights[2] + ) + } else { + uniformBarycentric(randomPoints[idx].x, randomPoints[idx].y) + } + result.add(sampleFunction(triangle, vertexData, barycentricCoords)) + idx++ + } + } + + return result +} + +/** + * Generate points on the surface described by the mesh data + */ +fun IMeshData.hash(count: Int, seed: Int, x: Int): List { + val randoms = (0 until count).map { + fhash1D(seed, x + it) + } + val randomPoints = (0 until count).map { + val x = x + it + val u = fhash1D(seed xor 0x7f7f7f7f, x) + val v = fhash1D(seed xor 0x7f7f7f7f, u.toRawBits().toInt() - x) + Vector2(u, v) + } + + return nonuniformEx( + randoms, randomPoints, identityWeightFunction, positionSampleFunction + ) +} \ No newline at end of file diff --git a/orx-mesh-noise/src/commonMain/kotlin/MeshNoise.kt b/orx-mesh-noise/src/commonMain/kotlin/MeshNoise.kt deleted file mode 100644 index af9f08f0..00000000 --- a/orx-mesh-noise/src/commonMain/kotlin/MeshNoise.kt +++ /dev/null @@ -1,201 +0,0 @@ -package org.openrndr.extra.mesh.noise - -import org.openrndr.extra.mesh.IIndexedPolygon -import org.openrndr.extra.mesh.IMeshData -import org.openrndr.extra.mesh.IVertexData -import org.openrndr.extra.noise.fhash1D -import org.openrndr.extra.noise.uhash11 -import org.openrndr.extra.noise.uhash1D -import org.openrndr.math.Vector3 -import kotlin.math.sqrt -import kotlin.random.Random - -/** - * Generate a uniformly distributed barycentric coordinate - * @param random a random number generator - */ -fun uniformBarycentric(random: Random = Random.Default): Vector3 { - val u = random.nextDouble() - val v = random.nextDouble() - val su0 = sqrt(u) - val b0 = 1.0 - su0 - val b1 = v * su0 - return Vector3(b0, b1, 1.0 - b0 - b1) -} - -/** - * Generate a non-uniformly distributed barycentric coordinate - * @param random a random number generator - */ -fun nonUniformBarycentric(weight0: Double, weight1: Double, weight2: Double, random: Random = Random.Default): Vector3 { - val b = uniformBarycentric() - var b0 = b.x / weight0 - var b1 = b.y / weight1 - var b2 = b.z / weight2 - val b = uniformBarycentric(random) - var b0 = b.x * weight0 - var b1 = b.y * weight1 - var b2 = b.z * weight2 - val totalWeight = b0 + b1 + b2 - b0 /= totalWeight - b1 /= totalWeight - b2 /= totalWeight - return Vector3(b0, b1, b2) -} - -/** - * Generate a uniformly distributed barycentric coordinate - * @param random a random number generator - */ -fun hashBarycentric(seed: Int, x: Int): Vector3 { - val u = fhash1D(seed, x) - val v = fhash1D(seed, u.toRawBits().toInt() - x) - - - val su0 = sqrt(u) - val b0 = 1.0 - su0 - val b1 = v * su0 - return Vector3(b0, b1, 1.0 - b0 - b1) -} - - -/** - * Generate a uniformly distributed point that lies inside this [IIndexedPolygon] - * @param vertexData vertex data used to resolve positions - * @param random a random number generator - */ -fun IIndexedPolygon.uniform(vertexData: IVertexData, random: Random = Random.Default): Vector3 { - require(positions.size == 3) { "polygon must be a triangle" } - - val x = vertexData.positions.slice(positions) - val b = uniformBarycentric(random) - return x[0] * b.x + x[1] * b.y + x[2] * b.z -} - -/** - * Computes a point within a triangle defined by the current indexed polygon. The point is determined - * through non-uniform barycentric coordinates, which are influenced by the specified weights. - * - * @param vertexData the vertex data containing positions and other attributes - * @param weight0 the weight associated with the first vertex of the triangle - * @param weight1 the weight associated with the second vertex of the triangle - * @param weight2 the weight associated with the third vertex of the triangle - * @param random an optional random number generator used for generating the barycentric coordinates - * @return a 3D vector representing a point within the triangle specified by the barycentric coordinates - */ -fun IIndexedPolygon.nonUniform( - vertexData: IVertexData, - weight0: Double, - weight1: Double, - weight2: Double, - random: Random = Random.Default -): Vector3 { - require(positions.size == 3) { "polygon must be a triangle" } - - val x = vertexData.positions.slice(positions) - val b = nonUniformBarycentric(weight0, weight1, weight2, random) - return x[0] * b.x + x[1] * b.y + x[2] * b.z -} - -/** - * Generate a uniformly distributed point that lies inside this [IIndexedPolygon] - * @param vertexData vertex data used to resolve positions - * @param random a random number generator - */ -fun IIndexedPolygon.hash(vertexData: IVertexData, seed: Int, x: Int): Vector3 { - require(positions.size == 3) { "polygon must be a triangle" } - - val s = vertexData.positions.slice(positions) - val b = hashBarycentric(seed, x) - return s[0] * b.x + s[1] * b.y + s[2] * b.z -} - -/** - * Calculates the area of the triangular polygon. - * - * The method assumes that the polygon is a triangle and computes its area - * using the cross product formula. The computed area is a positive value as it - * represents the absolute area of the triangle. - * - * @param vertexData the vertex data containing positional information of the polygon vertices - * @return the area of the triangle as a Double - * @throws IllegalArgumentException if the polygon is not a triangle (i.e., does not have exactly 3 vertices) - */ -internal fun IIndexedPolygon.area(vertexData: IVertexData): Double { - require(positions.size == 3) { "polygon must be a triangle" } - val x = vertexData.positions.slice(positions) - val u = x[1] - x[0] - val v = x[2] - x[0] - return u.areaBetween(v) / 2.0 -} - -/** - * Computes the weighted area of a triangular polygon by scaling its area with the average of the given weights. - * - * @param vertexData the vertex data containing position information of the polygon vertices - * @param weight0 the weight associated with the first vertex of the polygon - * @param weight1 the weight associated with the second vertex of the polygon - * @param weight2 the weight associated with the third vertex of the polygon - * @return the weighted area of the triangular polygon - */ -internal fun IIndexedPolygon.weightedArea( - vertexData: IVertexData, - weight0: Double, - weight1: Double, - weight2: Double -): Double { - return area(vertexData) * (weight0 + weight1 + weight2) / 3.0 -} - -/** - * Generates a list of uniformly distributed points on the surface of the given mesh. - * - * The method uses triangulation and computes areas of triangular polygons to ensure - * uniform distribution of points across the surface. - * - * @param count the number of points to generate - * @param random a random number generator instance, defaulting to [Random.Default] - * @return a list of [Vector3] points uniformly distributed across the mesh surface - */ -fun IMeshData.uniform(count: Int, random: Random = Random.Default): List { - val triangulated = triangulate() - val result = mutableListOf() - val totalArea = triangulated.polygons.sumOf { it.area(vertexData) } - val randoms = (0 until count).map { - random.nextDouble(totalArea) - }.sorted() - - var idx = 0 - var sum = 0.0 - for (t in triangulated.polygons) { - sum += t.area(vertexData) - while (idx <= randoms.lastIndex && sum > randoms[idx]) { - result.add(t.uniform(vertexData, random)) - idx++ - } - } - return result -} - -/** - * Generate points on the surface described by the mesh data - */ -fun IMeshData.hash(count: Int, seed: Int, x: Int): List { - val triangulated = triangulate() - val result = mutableListOf() - val totalArea = triangulated.polygons.sumOf { it.area(vertexData) } - val randoms = (0 until count).map { - Pair(x + it, fhash1D(seed, x + it) * totalArea) - }.sortedBy { it.second } - - var idx = 0 - var sum = 0.0 - for (t in triangulated.polygons) { - sum += t.area(vertexData) - while (idx <= randoms.lastIndex && sum > randoms[idx].second) { - result.add(t.hash(vertexData, seed xor 0x7f7f7f, randoms[idx].first)) - idx++ - } - } - return result -} \ No newline at end of file diff --git a/orx-mesh-noise/src/commonMain/kotlin/WeightFunction.kt b/orx-mesh-noise/src/commonMain/kotlin/WeightFunction.kt new file mode 100644 index 00000000..1a86cde6 --- /dev/null +++ b/orx-mesh-noise/src/commonMain/kotlin/WeightFunction.kt @@ -0,0 +1,26 @@ +package org.openrndr.extra.mesh.noise + +import org.openrndr.extra.mesh.IIndexedPolygon +import org.openrndr.extra.mesh.IVertexData + +/** + * A type alias representing a weight function for vertices in a polygon. + * + * The function calculates the weight of a vertex within a given polygon based on its vertex data, + * the polygon's geometry, and the specific vertex index. + * + * @param vertexData The vertex data containing attributes such as positions, normals, and other properties. + * @param polygon The indexed polygon for which the weight is being calculated. + * @param vertexIndex The index of the vertex within the polygon for which the weight is being computed. + * @return The computed weight as a Double. + */ +typealias WeightFunction = (vertexData: IVertexData, polygon: IIndexedPolygon, vertexIndex: Int) -> Double + +/** + * A constant weight function for barycentric coordinates that always returns a weight of 1.0. + * + * This function can be used in scenarios where uniform weighting is required across + * all components of a barycentric coordinate, effectively resulting in no modification + * to the original weights of the components. + */ +val identityWeightFunction: WeightFunction = { _, _, _ -> 1.0 } \ No newline at end of file diff --git a/orx-mesh-noise/src/jvmDemo/kotlin/DemoMeshNoise01.kt b/orx-mesh-noise/src/jvmDemo/kotlin/DemoMeshNoise01.kt index 37bc0fa2..06343c7f 100644 --- a/orx-mesh-noise/src/jvmDemo/kotlin/DemoMeshNoise01.kt +++ b/orx-mesh-noise/src/jvmDemo/kotlin/DemoMeshNoise01.kt @@ -1,3 +1,4 @@ +import org.openrndr.WindowMultisample import org.openrndr.application import org.openrndr.draw.DrawPrimitive import org.openrndr.draw.isolated @@ -26,31 +27,28 @@ import kotlin.random.Random * The application runs with a window size of 720x720 pixels and positions the camera * in front of the scene using the "Orbital" extension. */ -fun main() { - application { - configure { - width = 720 - height = 720 - } - program { - val mesh = loadOBJMeshData(File("demo-data/obj-models/suzanne/Suzanne.obj")).toMeshData() - val points = mesh.uniform(1000, Random(0)) +fun main() = application { + configure { + width = 720 + height = 720 + multisample = WindowMultisample.SampleCount(8) + } + program { + val mesh = loadOBJMeshData(File("demo-data/obj-models/suzanne/Suzanne.obj")).toMeshData() + val points = mesh.uniform(1000, Random(0)) - val sphere = sphereMesh(radius = 0.1) - extend(Orbital()) { - eye = Vector3(0.0, 0.0, 2.0) + val sphere = sphereMesh(radius = 0.1) + extend(Orbital()) { + eye = Vector3(0.0, 0.0, 2.0) + } + extend { + drawer.shadeStyle = shadeStyle { + fragmentTransform = "x_fill = vec4(v_viewNormal*0.5+0.5, 1.0);" } - extend { - drawer.shadeStyle = shadeStyle { - fragmentTransform = "x_fill = vec4(v_viewNormal*0.5+0.5, 1.0);" - } - for (point in points) { - drawer.isolated { - drawer.translate(point) - drawer.vertexBuffer(sphere, DrawPrimitive.TRIANGLES) - } - } + for (point in points) drawer.isolated { + drawer.translate(point) + drawer.vertexBuffer(sphere, DrawPrimitive.TRIANGLES) } } } -} \ No newline at end of file +} diff --git a/orx-mesh-noise/src/jvmDemo/kotlin/DemoMeshNoise02.kt b/orx-mesh-noise/src/jvmDemo/kotlin/DemoMeshNoise02.kt index f4aa5c1b..625beb8a 100644 --- a/orx-mesh-noise/src/jvmDemo/kotlin/DemoMeshNoise02.kt +++ b/orx-mesh-noise/src/jvmDemo/kotlin/DemoMeshNoise02.kt @@ -1,3 +1,4 @@ +import org.openrndr.WindowMultisample import org.openrndr.application import org.openrndr.draw.DrawPrimitive import org.openrndr.draw.isolated @@ -5,44 +6,36 @@ import org.openrndr.draw.shadeStyle import org.openrndr.extra.camera.Orbital import org.openrndr.extra.mesh.noise.hash import org.openrndr.extra.objloader.loadOBJMeshData -import org.openrndr.extra.mesh.noise.uniform import org.openrndr.extra.meshgenerators.sphereMesh import org.openrndr.math.Vector3 import java.io.File import kotlin.math.cos -import kotlin.random.Random /** * Demonstrate uniform point on mesh generation using hash functions */ -fun main() { - application { - configure { - width = 720 - height = 720 +fun main() = application { + configure { + width = 720 + height = 720 + multisample = WindowMultisample.SampleCount(8) + } + program { + val mesh = loadOBJMeshData(File("demo-data/obj-models/suzanne/Suzanne.obj")).toMeshData() + + val sphere = sphereMesh(radius = 0.01) + extend(Orbital()) { + eye = Vector3(0.0, 0.0, 2.0) } - program { - val mesh = loadOBJMeshData(File("demo-data/obj-models/suzanne/Suzanne.obj")).toMeshData() - - val sphere = sphereMesh(radius = 0.01) - extend(Orbital()) { - eye = Vector3(0.0, 0.0, 2.0) + extend { + val points = mesh.hash((1000 + (cos(seconds) * 0.5 + 0.5) * 9000).toInt(), 808, (seconds * 1).toInt()) + drawer.shadeStyle = shadeStyle { + fragmentTransform = "x_fill = vec4(v_viewNormal*0.5+0.5, 1.0);" } - extend { - - val points = mesh.hash((1000 + (cos(seconds)*0.5+0.5)*9000).toInt(), 808, (seconds*10000).toInt()) - - - drawer.shadeStyle = shadeStyle { - fragmentTransform = "x_fill = vec4(v_viewNormal*0.5+0.5, 1.0);" - } - for (point in points) { - drawer.isolated { - drawer.translate(point) - drawer.vertexBuffer(sphere, DrawPrimitive.TRIANGLES) - } - } + for (point in points) drawer.isolated { + drawer.translate(point) + drawer.vertexBuffer(sphere, DrawPrimitive.TRIANGLES) } } } -} \ No newline at end of file +} diff --git a/orx-mesh-noise/src/jvmDemo/kotlin/DemoMeshNoise03.kt b/orx-mesh-noise/src/jvmDemo/kotlin/DemoMeshNoise03.kt new file mode 100644 index 00000000..951623d6 --- /dev/null +++ b/orx-mesh-noise/src/jvmDemo/kotlin/DemoMeshNoise03.kt @@ -0,0 +1,57 @@ +import org.openrndr.WindowMultisample +import org.openrndr.application +import org.openrndr.draw.DrawPrimitive +import org.openrndr.draw.isolated +import org.openrndr.draw.shadeStyle +import org.openrndr.extra.camera.Orbital +import org.openrndr.extra.objloader.loadOBJMeshData +import org.openrndr.extra.mesh.noise.uniformPoints +import org.openrndr.extra.meshgenerators.cylinderMesh +import org.openrndr.extra.meshgenerators.normals.estimateNormals +import org.openrndr.extra.meshgenerators.tangents.estimateTangents +import org.openrndr.math.Vector3 +import org.openrndr.math.transforms.buildTransform +import java.io.File +import kotlin.math.cos +import kotlin.random.Random + +/** + * This demo loads a 3D model from an OBJ file, processes the mesh data to estimate normals and tangents, and generates + * a set of uniformly distributed pose points. These pose points determine the transformations applied to individual + * objects rendered in the viewport. + * + * It extends the rendering with an orbital camera for navigation and shaders for custom visual + * effects. Cylinders represent transformed objects, with their scale animations based on time-dependent + * trigonometric functions. + */ +fun main() = application { + configure { + width = 720 + height = 720 + multisample = WindowMultisample.SampleCount(8) + } + program { + val mesh = loadOBJMeshData(File("demo-data/obj-models/suzanne/Suzanne.obj")).toMeshData().triangulate() + .estimateNormals().estimateTangents() + val poses = mesh.uniformPoints(10000, Random(0)).map { it.pose() } + + val cylinder = cylinderMesh(radius = 0.01, length = 0.2) + extend(Orbital()) { + eye = Vector3(0.0, 0.0, 2.0) + } + extend { + drawer.shadeStyle = shadeStyle { + fragmentTransform = "x_fill = vec4(v_viewNormal*0.5+0.5, 1.0);" + } + for (pose in poses) { + drawer.isolated { + drawer.model = buildTransform { + multiply(pose) + scale(1.0, 1.0, cos(pose.c3r0 * 10.0 + seconds) * 0.5 + 0.5) + } + drawer.vertexBuffer(cylinder, DrawPrimitive.TRIANGLES) + } + } + } + } +} \ No newline at end of file diff --git a/orx-mesh-noise/src/jvmDemo/kotlin/DemoNonUniformMeshNoise01.kt b/orx-mesh-noise/src/jvmDemo/kotlin/DemoNonUniformMeshNoise01.kt new file mode 100644 index 00000000..e6809524 --- /dev/null +++ b/orx-mesh-noise/src/jvmDemo/kotlin/DemoNonUniformMeshNoise01.kt @@ -0,0 +1,75 @@ +import org.openrndr.WindowMultisample +import org.openrndr.application +import org.openrndr.draw.DrawPrimitive +import org.openrndr.draw.isolated +import org.openrndr.draw.shadeStyle +import org.openrndr.extra.camera.Orbital +import org.openrndr.extra.mesh.IIndexedPolygon +import org.openrndr.extra.mesh.IVertexData +import org.openrndr.extra.mesh.noise.nonuniform +import org.openrndr.extra.mesh.noise.nonuniformHammersley +import org.openrndr.extra.mesh.noise.nonuniformRSeq +import org.openrndr.extra.objloader.loadOBJMeshData +import org.openrndr.extra.mesh.noise.uniform +import org.openrndr.extra.meshgenerators.normals.estimateNormals +import org.openrndr.extra.meshgenerators.sphereMesh +import org.openrndr.math.Spherical +import org.openrndr.math.Vector3 +import java.io.File +import kotlin.math.absoluteValue +import kotlin.math.cos +import kotlin.math.pow +import kotlin.math.sin +import kotlin.random.Random + + +/** + * The program demonstrates the loading of a 3D model, estimating its normals, + * sampling points based on non-uniform distribution, and rendering points as spheres. + * + * Key functionalities include: + * - Loading a 3D model from an OBJ file. + * - Estimating per-vertex normals for the mesh. + * - Generating and rendering a sphere mesh for sampled points. + * - Using a lighting direction vector to bias the point sampling distribution. + * - Extending the program with an orbital camera for interactive navigation. + * - Applying shading to simulate lighting effects based on vertex normals. + * + * The rendering of spheres is performed by iterating over the sampled points and isolating each in the transformation matrix. + * This setup allows customization for complex rendering pipelines. + */ +fun main() = application { + configure { + width = 720 + height = 720 + multisample = WindowMultisample.SampleCount(8) + } + program { + val mesh = loadOBJMeshData(File("demo-data/obj-models/suzanne/Suzanne.obj")).toMeshData().estimateNormals() + + val sphere = sphereMesh(radius = 0.0125) + extend(Orbital()) { + eye = Vector3(0.0, 0.0, 8.0) + fov = 25.0 + } + val v = Vector3(1.0, 1.0, 1.0).normalized + + val points = mesh.nonuniformRSeq( + 10000, + false, + Random((seconds * 0).toInt()) + ) { vertexData: IVertexData, polygon: IIndexedPolygon, vertexIndex: Int -> + vertexData.normals[polygon.normals[vertexIndex]].dot(v).coerceIn(0.1, 1.0).pow(2.0) + } + + extend { + drawer.shadeStyle = shadeStyle { + fragmentTransform = "x_fill = vec4( (v_viewNormal * 0.5 + 0.5), 1.0);" + } + for (point in points) drawer.isolated { + drawer.translate(point) + drawer.vertexBuffer(sphere, DrawPrimitive.TRIANGLES) + } + } + } +} \ No newline at end of file