[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.draw.DrawPrimitive
|
||||
import org.openrndr.draw.isolated
|
||||
@@ -26,11 +27,11 @@ 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 {
|
||||
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()
|
||||
@@ -44,13 +45,10 @@ fun main() {
|
||||
drawer.shadeStyle = shadeStyle {
|
||||
fragmentTransform = "x_fill = vec4(v_viewNormal*0.5+0.5, 1.0);"
|
||||
}
|
||||
for (point in points) {
|
||||
drawer.isolated {
|
||||
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.draw.DrawPrimitive
|
||||
import org.openrndr.draw.isolated
|
||||
@@ -5,21 +6,19 @@ 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 {
|
||||
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()
|
||||
@@ -29,20 +28,14 @@ fun main() {
|
||||
eye = Vector3(0.0, 0.0, 2.0)
|
||||
}
|
||||
extend {
|
||||
|
||||
val points = mesh.hash((1000 + (cos(seconds)*0.5+0.5)*9000).toInt(), 808, (seconds*10000).toInt())
|
||||
|
||||
|
||||
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);"
|
||||
}
|
||||
for (point in points) {
|
||||
drawer.isolated {
|
||||
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