[orx-mesh-noise] Refactor barycentric and mesh sampling logic into specialized files.
This commit is contained in:
65
orx-mesh-noise/src/commonMain/kotlin/Barycentric.kt
Normal file
65
orx-mesh-noise/src/commonMain/kotlin/Barycentric.kt
Normal file
@@ -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)
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
273
orx-mesh-noise/src/commonMain/kotlin/IMeshDataExtensions.kt
Normal file
273
orx-mesh-noise/src/commonMain/kotlin/IMeshDataExtensions.kt
Normal file
@@ -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<T> = (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<Vector3> =
|
||||||
|
{ 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<Point> = { 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<Vector3> {
|
||||||
|
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<Point> {
|
||||||
|
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<Vector3> {
|
||||||
|
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<Point> {
|
||||||
|
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<Vector3> {
|
||||||
|
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<Vector3> {
|
||||||
|
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 <T> IMeshData.nonuniformEx(
|
||||||
|
randomValuesUnsorted: List<Double>,
|
||||||
|
randomPoints: List<Vector2>,
|
||||||
|
weightFunction: WeightFunction,
|
||||||
|
sampleFunction: (IIndexedPolygon, IVertexData, Barycentric) -> T
|
||||||
|
): List<T> {
|
||||||
|
val triangulated = triangulate()
|
||||||
|
val defaultWeights = doubleArrayOf(1.0, 1.0, 1.0)
|
||||||
|
val result = ArrayList<T>(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<Vector3> {
|
||||||
|
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
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -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<Vector3> {
|
|
||||||
val triangulated = triangulate()
|
|
||||||
val result = mutableListOf<Vector3>()
|
|
||||||
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<Vector3> {
|
|
||||||
val triangulated = triangulate()
|
|
||||||
val result = mutableListOf<Vector3>()
|
|
||||||
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
|
|
||||||
}
|
|
||||||
26
orx-mesh-noise/src/commonMain/kotlin/WeightFunction.kt
Normal file
26
orx-mesh-noise/src/commonMain/kotlin/WeightFunction.kt
Normal file
@@ -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 }
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import org.openrndr.WindowMultisample
|
||||||
import org.openrndr.application
|
import org.openrndr.application
|
||||||
import org.openrndr.draw.DrawPrimitive
|
import org.openrndr.draw.DrawPrimitive
|
||||||
import org.openrndr.draw.isolated
|
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
|
* The application runs with a window size of 720x720 pixels and positions the camera
|
||||||
* in front of the scene using the "Orbital" extension.
|
* in front of the scene using the "Orbital" extension.
|
||||||
*/
|
*/
|
||||||
fun main() {
|
fun main() = application {
|
||||||
application {
|
configure {
|
||||||
configure {
|
width = 720
|
||||||
width = 720
|
height = 720
|
||||||
height = 720
|
multisample = WindowMultisample.SampleCount(8)
|
||||||
}
|
}
|
||||||
program {
|
program {
|
||||||
val mesh = loadOBJMeshData(File("demo-data/obj-models/suzanne/Suzanne.obj")).toMeshData()
|
val mesh = loadOBJMeshData(File("demo-data/obj-models/suzanne/Suzanne.obj")).toMeshData()
|
||||||
val points = mesh.uniform(1000, Random(0))
|
val points = mesh.uniform(1000, Random(0))
|
||||||
|
|
||||||
val sphere = sphereMesh(radius = 0.1)
|
val sphere = sphereMesh(radius = 0.1)
|
||||||
extend(Orbital()) {
|
extend(Orbital()) {
|
||||||
eye = Vector3(0.0, 0.0, 2.0)
|
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 {
|
for (point in points) drawer.isolated {
|
||||||
drawer.shadeStyle = shadeStyle {
|
drawer.translate(point)
|
||||||
fragmentTransform = "x_fill = vec4(v_viewNormal*0.5+0.5, 1.0);"
|
drawer.vertexBuffer(sphere, DrawPrimitive.TRIANGLES)
|
||||||
}
|
|
||||||
for (point in points) {
|
|
||||||
drawer.isolated {
|
|
||||||
drawer.translate(point)
|
|
||||||
drawer.vertexBuffer(sphere, DrawPrimitive.TRIANGLES)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import org.openrndr.WindowMultisample
|
||||||
import org.openrndr.application
|
import org.openrndr.application
|
||||||
import org.openrndr.draw.DrawPrimitive
|
import org.openrndr.draw.DrawPrimitive
|
||||||
import org.openrndr.draw.isolated
|
import org.openrndr.draw.isolated
|
||||||
@@ -5,44 +6,36 @@ import org.openrndr.draw.shadeStyle
|
|||||||
import org.openrndr.extra.camera.Orbital
|
import org.openrndr.extra.camera.Orbital
|
||||||
import org.openrndr.extra.mesh.noise.hash
|
import org.openrndr.extra.mesh.noise.hash
|
||||||
import org.openrndr.extra.objloader.loadOBJMeshData
|
import org.openrndr.extra.objloader.loadOBJMeshData
|
||||||
import org.openrndr.extra.mesh.noise.uniform
|
|
||||||
import org.openrndr.extra.meshgenerators.sphereMesh
|
import org.openrndr.extra.meshgenerators.sphereMesh
|
||||||
import org.openrndr.math.Vector3
|
import org.openrndr.math.Vector3
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import kotlin.math.cos
|
import kotlin.math.cos
|
||||||
import kotlin.random.Random
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Demonstrate uniform point on mesh generation using hash functions
|
* Demonstrate uniform point on mesh generation using hash functions
|
||||||
*/
|
*/
|
||||||
fun main() {
|
fun main() = application {
|
||||||
application {
|
configure {
|
||||||
configure {
|
width = 720
|
||||||
width = 720
|
height = 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 {
|
extend {
|
||||||
val mesh = loadOBJMeshData(File("demo-data/obj-models/suzanne/Suzanne.obj")).toMeshData()
|
val points = mesh.hash((1000 + (cos(seconds) * 0.5 + 0.5) * 9000).toInt(), 808, (seconds * 1).toInt())
|
||||||
|
drawer.shadeStyle = shadeStyle {
|
||||||
val sphere = sphereMesh(radius = 0.01)
|
fragmentTransform = "x_fill = vec4(v_viewNormal*0.5+0.5, 1.0);"
|
||||||
extend(Orbital()) {
|
|
||||||
eye = Vector3(0.0, 0.0, 2.0)
|
|
||||||
}
|
}
|
||||||
extend {
|
for (point in points) drawer.isolated {
|
||||||
|
drawer.translate(point)
|
||||||
val points = mesh.hash((1000 + (cos(seconds)*0.5+0.5)*9000).toInt(), 808, (seconds*10000).toInt())
|
drawer.vertexBuffer(sphere, DrawPrimitive.TRIANGLES)
|
||||||
|
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
57
orx-mesh-noise/src/jvmDemo/kotlin/DemoMeshNoise03.kt
Normal file
57
orx-mesh-noise/src/jvmDemo/kotlin/DemoMeshNoise03.kt
Normal file
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user