add android and desktop modules

This commit is contained in:
2025-11-24 00:30:31 +08:00
parent 72368deb85
commit f81eee8716
133 changed files with 9436 additions and 10 deletions

36
desktop/build.gradle.kts Normal file
View File

@@ -0,0 +1,36 @@
plugins {
id("org.openrndr.extra.convention.kotlin-multiplatform")
}
kotlin {
sourceSets {
val commonMain by getting {
dependencies {
api(openrndr.math)
api(openrndr.shape)
implementation(project(":orx-noise"))
}
}
val commonTest by getting {
dependencies {
implementation(project(":orx-shapes"))
implementation(openrndr.shape)
}
}
val jvmDemo by getting {
dependencies {
implementation(project(":orx-triangulation"))
implementation(project(":orx-shapes"))
implementation(project(":orx-noise"))
implementation(openrndr.shape)
implementation(project(":math"))
implementation(project(":orx-camera"))
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.2")
implementation(project(":orx-marching-squares"))
implementation(project(":orx-text-writer"))
implementation(project(":orx-obj-loader"))
}
}
}
}

View File

@@ -0,0 +1,605 @@
import com.icegps.math.geometry.Angle
import com.icegps.math.geometry.Vector3D
import com.icegps.math.geometry.degrees
import org.openrndr.KEY_ARROW_DOWN
import org.openrndr.KEY_ARROW_UP
import org.openrndr.application
import org.openrndr.color.ColorRGBa
import org.openrndr.draw.TextSettingMode
import org.openrndr.draw.loadFont
import org.openrndr.extra.camera.Camera2D
import org.openrndr.extra.marchingsquares.findContours
import org.openrndr.extra.noise.gradientPerturbFractal
import org.openrndr.extra.noise.simplex
import org.openrndr.extra.textwriter.writer
import org.openrndr.extra.triangulation.DelaunayTriangulation
import org.openrndr.math.Vector2
import org.openrndr.math.Vector3
import org.openrndr.shape.Segment2D
import org.openrndr.shape.Segment3D
import org.openrndr.shape.ShapeContour
import kotlin.math.absoluteValue
import kotlin.math.cos
import kotlin.math.sin
import kotlin.random.Random
/**
* @author tabidachinokaze
* @date 2025/11/22
*/
fun main() = application {
configure {
width = 720
height = 720
title = "Delaunator"
}
program {
val points3D = (0 until height).step(36).map { y ->
(0 until width).step(36).map { x ->
gradientPerturbFractal(
300,
frequency = 0.8,
position = Vector3(x.toDouble(), y.toDouble(), seconds)
)
}
}.flatten().map {
it.copy(z = it.z * 100)
}
/*val points3D = HeightmapVolcanoGenerator.generateVolcanoClusterHeightmap(
width = width,
height = height,
volcanoCount = 3
)*/
// val points3D = coordinateGenerate(width, height)
val zs = points3D.map { it.z }
println("zs = ${zs}")
val associate: MutableMap<Vector2, Double> = points3D.associate {
Vector2(it.x, it.y) to it.z
}.toMutableMap()
val delaunay = DelaunayTriangulation(associate.map { it.key })
//println(points3D.niceStr())
extend(Camera2D())
println("draw")
var targetHeight: Double = zs.average()
var logEnabled = true
var useInterpolation = false
var sampleLinear = false
keyboard.keyDown.listen {
logEnabled = true
println(it)
when (it.key) {
KEY_ARROW_UP -> targetHeight++
KEY_ARROW_DOWN -> targetHeight--
73 -> useInterpolation = !useInterpolation
83 -> sampleLinear = !sampleLinear
}
}
extend {
val triangles = delaunay.triangles()
val segments = mutableListOf<Segment2D>()
drawer.clear(ColorRGBa.BLACK)
val indexDiff = (frameCount / 1000) % triangles.size
for ((i, triangle) in triangles.withIndex()) {
val segment2DS = triangle.contour.segments.filter {
val startZ = associate[it.start]!!
val endZ = associate[it.end]!!
if (startZ < endZ) {
targetHeight in startZ..endZ
} else {
targetHeight in endZ..startZ
}
}
if (segment2DS.size == 2) {
val vector2s = segment2DS.map {
val startZ = associate[it.start]!!
val endZ = associate[it.end]!!
val start = Vector3(it.start.x, it.start.y, startZ)
val end = Vector3(it.end.x, it.end.y, endZ)
if (startZ < endZ) {
start to end
} else {
end to start
}
}.map { (start, end) ->
val segment3D = Segment3D(start, end)
val vector3 =
segment3D.position(calculatePositionRatio(targetHeight, start.z, end.z))
vector3
}.map {
associate[it.xy] = it.z
it.xy
}
val element = Segment2D(vector2s[0], vector2s[1])
segments.add(element)
}
drawer.fill = if (indexDiff == i) {
ColorRGBa.CYAN
} else {
ColorRGBa.PINK.shade(1.0 - i / (triangles.size * 1.2))
}
drawer.stroke = ColorRGBa.PINK.shade(i / (triangles.size * 1.0) + 0.1)
drawer.contour(triangle.contour)
}
val sorted = connectAllSegments(segments)
drawer.stroke = ColorRGBa.WHITE
drawer.strokeWeight = 2.0
if (logEnabled) {
segments.forEach {
println("${it.start} -> ${it.end}")
}
println("=====")
}
sorted.forEach {
it.forEach {
if (logEnabled) println("${it.start} -> ${it.end}")
drawer.lineSegment(it.start, it.end)
drawer.fill = ColorRGBa.WHITE
}
if (logEnabled) println("=")
drawer.fill = ColorRGBa.YELLOW
if (false) drawer.contour(ShapeContour.fromSegments(it, closed = true))
}
/*for (y in 0 until (area.height / cellSize).toInt()) {
for (x in 0 until (area.width / cellSize).toInt()) {
values[IntVector2(x, y)] = f(Vector2(x * cellSize + area.x, y * cellSize + area.y))
}
}*/
val contours = findContours(
f = {
val triangle = triangles.firstOrNull { triangle ->
isPointInTriangle(it, listOf(triangle.x1, triangle.x2, triangle.x3))
}
triangle ?: return@findContours 0.0
val interpolate = interpolateHeight(
point = it,
triangle = listOf(
triangle.x1,
triangle.x2,
triangle.x3,
).map {
Vector3(it.x, it.y, associate[it]!!)
}
)
interpolate.z - targetHeight
},
area = drawer.bounds,
cellSize = 4.0,
useInterpolation = useInterpolation
)
if (logEnabled) println("useInterpolation = $useInterpolation")
drawer.stroke = null
contours.forEach {
drawer.fill = ColorRGBa.GREEN.opacify(0.1)
drawer.contour(if (sampleLinear) it.sampleLinear() else it)
}
drawer.fontMap = loadFont("demo-data/fonts/IBMPlexMono-Regular.ttf", 24.0)
writer {
drawer.drawStyle.textSetting = TextSettingMode.SUBPIXEL
text(targetHeight.toString())
}
logEnabled = false
}
}
}
/**
* 射线法判断点是否在单个三角形内
*/
fun isPointInTriangle(point: Vector2, triangle: List<Vector2>): Boolean {
require(triangle.size == 3) { "三角形必须有3个顶点" }
val (v1, v2, v3) = triangle
// 计算重心坐标
val denominator = (v2.y - v3.y) * (v1.x - v3.x) + (v3.x - v2.x) * (v1.y - v3.y)
if (denominator == 0.0) return false // 退化三角形
val alpha = ((v2.y - v3.y) * (point.x - v3.x) + (v3.x - v2.x) * (point.y - v3.y)) / denominator
val beta = ((v3.y - v1.y) * (point.x - v3.x) + (v1.x - v3.x) * (point.y - v3.y)) / denominator
val gamma = 1.0 - alpha - beta
// 点在三角形内当且仅当所有重心坐标都在[0,1]范围内
return alpha >= 0 && beta >= 0 && gamma >= 0 &&
alpha <= 1 && beta <= 1 && gamma <= 1
}
/**
* 使用重心坐标计算点在三角形上的高度
* @param point 二维点 (x, y)
* @param triangle 三角形的三个顶点
* @return 三维点 (x, y, z)
*/
fun interpolateHeight(point: Vector2, triangle: List<Vector3>): Vector3 {
require(triangle.size == 3) { "三角形必须有3个顶点" }
val (v1, v2, v3) = triangle
// 计算重心坐标
val (alpha, beta, gamma) = calculateBarycentricCoordinates(point, v1, v2, v3)
// 使用重心坐标插值z值
val z = alpha * v1.z + beta * v2.z + gamma * v3.z
return Vector3(point.x, point.y, z)
}
/**
* 计算点在三角形中的重心坐标
*/
fun calculateBarycentricCoordinates(
point: Vector2,
v1: Vector3,
v2: Vector3,
v3: Vector3
): Triple<Double, Double, Double> {
val denom = (v2.y - v3.y) * (v1.x - v3.x) + (v3.x - v2.x) * (v1.y - v3.y)
val alpha = ((v2.y - v3.y) * (point.x - v3.x) + (v3.x - v2.x) * (point.y - v3.y)) / denom
val beta = ((v3.y - v1.y) * (point.x - v3.x) + (v1.x - v3.x) * (point.y - v3.y)) / denom
val gamma = 1.0 - alpha - beta
return Triple(alpha, beta, gamma)
}
fun connectAllSegments(segments: List<Segment2D>): List<List<Segment2D>> {
val remaining = segments.toMutableList()
val allPaths = mutableListOf<List<Segment2D>>()
while (remaining.isNotEmpty()) {
val path = mutableListOf<Segment2D>()
// 开始新路径
path.add(remaining.removeAt(0))
var changed: Boolean
do {
changed = false
// 向前扩展
val lastEnd = path.last().end
val forwardSegment = remaining.find { it.start == lastEnd || it.end == lastEnd }
if (forwardSegment != null) {
val connectedSegment = if (forwardSegment.start == lastEnd) {
forwardSegment // 正向
} else {
Segment2D(forwardSegment.end, forwardSegment.start) // 反向
}
path.add(connectedSegment)
remaining.remove(forwardSegment)
changed = true
}
// 向后扩展
val firstStart = path.first().start
val backwardSegment = remaining.find { it.end == firstStart || it.start == firstStart }
if (backwardSegment != null) {
val connectedSegment = if (backwardSegment.end == firstStart) {
backwardSegment // 正向
} else {
Segment2D(backwardSegment.end, backwardSegment.start) // 反向
}
path.add(0, connectedSegment)
remaining.remove(backwardSegment)
changed = true
}
} while (changed && remaining.isNotEmpty())
allPaths.add(path)
}
return allPaths
}
fun connectSegmentsEfficient(segments: List<Segment2D>): List<Segment2D> {
if (segments.isEmpty()) return emptyList()
val remaining = segments.toMutableList()
val connected = mutableListOf<Segment2D>()
// 构建端点查找表
val startMap = mutableMapOf<Vector2, MutableList<Segment2D>>()
val endMap = mutableMapOf<Vector2, MutableList<Segment2D>>()
segments.forEach { segment ->
startMap.getOrPut(segment.start) { mutableListOf() }.add(segment)
endMap.getOrPut(segment.end) { mutableListOf() }.add(segment)
}
// 从第一个线段开始
var currentSegment = remaining.removeAt(0)
connected.add(currentSegment)
// 更新查找表
startMap[currentSegment.start]?.remove(currentSegment)
endMap[currentSegment.end]?.remove(currentSegment)
// 向前连接
while (true) {
val nextFromStart = startMap[currentSegment.end]?.firstOrNull()
val nextFromEnd = endMap[currentSegment.end]?.firstOrNull()
when {
nextFromStart != null -> {
// 正向连接
connected.add(nextFromStart)
remaining.remove(nextFromStart)
startMap[nextFromStart.start]?.remove(nextFromStart)
endMap[nextFromStart.end]?.remove(nextFromStart)
currentSegment = nextFromStart
}
nextFromEnd != null -> {
// 反向连接
val reversed = Segment2D(nextFromEnd.end, nextFromEnd.start)
connected.add(reversed)
remaining.remove(nextFromEnd)
startMap[nextFromEnd.start]?.remove(nextFromEnd)
endMap[nextFromEnd.end]?.remove(nextFromEnd)
currentSegment = reversed
}
else -> break
}
}
// 向后连接
currentSegment = connected.first()
while (true) {
val prevFromEnd = endMap[currentSegment.start]?.firstOrNull()
val prevFromStart = startMap[currentSegment.start]?.firstOrNull()
when {
prevFromEnd != null -> {
// 正向连接到开头
connected.add(0, prevFromEnd)
remaining.remove(prevFromEnd)
startMap[prevFromEnd.start]?.remove(prevFromEnd)
endMap[prevFromEnd.end]?.remove(prevFromEnd)
currentSegment = prevFromEnd
}
prevFromStart != null -> {
// 反向连接到开头
val reversed = Segment2D(prevFromStart.end, prevFromStart.start)
connected.add(0, reversed)
remaining.remove(prevFromStart)
startMap[prevFromStart.start]?.remove(prevFromStart)
endMap[prevFromStart.end]?.remove(prevFromStart)
currentSegment = reversed
}
else -> break
}
}
return connected
}
fun connectSegments(segments: List<Segment2D>): List<Segment2D> {
if (segments.isEmpty()) return emptyList()
val remaining = segments.toMutableList()
val connected = mutableListOf<Segment2D>()
// 从第一个线段开始,保持原方向
connected.add(remaining.removeAt(0))
while (remaining.isNotEmpty()) {
val lastEnd = connected.last().end
var found = false
// 查找可以连接的线段
for (i in remaining.indices) {
val segment = remaining[i]
// 检查四种可能的连接方式
when {
// 正向连接:当前终点 == 线段起点
segment.start == lastEnd -> {
connected.add(segment)
remaining.removeAt(i)
found = true
break
}
// 反向连接:当前终点 == 线段终点,需要反转线段
segment.end == lastEnd -> {
connected.add(Segment2D(segment.end, segment.start)) // 反转
remaining.removeAt(i)
found = true
break
}
// 正向连接另一端:当前起点 == 线段终点,需要插入到前面
segment.end == connected.first().start -> {
connected.add(0, Segment2D(segment.end, segment.start)) // 反转后插入开头
remaining.removeAt(i)
found = true
break
}
// 反向连接另一端:当前起点 == 线段起点,需要反转并插入到前面
segment.start == connected.first().start -> {
connected.add(0, segment) // 直接插入开头(已经是正确方向)
remaining.removeAt(i)
found = true
break
}
}
}
if (!found) break // 无法找到连接线段
}
return connected
}
fun calculatePositionRatio(value: Double, rangeStart: Double, rangeEnd: Double): Double {
if (rangeStart == rangeEnd) return 0.0 // 避免除零
val ratio = (value - rangeStart) / (rangeEnd - rangeStart)
return ratio.coerceIn(0.0, 1.0)
}
fun sortLinesEfficient(lines: List<Segment2D>): List<Segment2D> {
if (lines.isEmpty()) return emptyList()
// 创建起点到线段的映射
val startMap = lines.associateBy { it.start }
val sorted = mutableListOf<Segment2D>()
// 找到起点(没有其他线段的终点指向它的起点)
var currentLine = lines.firstOrNull { line ->
lines.none { it.end == line.start }
} ?: lines.first()
sorted.add(currentLine)
while (true) {
val nextLine = startMap[currentLine.end]
if (nextLine == null || nextLine == lines.first()) break
sorted.add(nextLine)
currentLine = nextLine
}
return sorted
}
fun sortLines(lines: List<Segment2D>): List<Segment2D> {
if (lines.isEmpty()) return emptyList()
val remaining = lines.toMutableList()
val sorted = mutableListOf<Segment2D>()
// 从第一个线段开始
sorted.add(remaining.removeAt(0))
while (remaining.isNotEmpty()) {
val lastEnd = sorted.last().end
var found = false
// 查找下一个线段
for (i in remaining.indices) {
if (remaining[i].start == lastEnd) {
sorted.add(remaining.removeAt(i))
found = true
break
}
}
if (!found) break // 无法找到下一个线段
}
return sorted
}
fun findLineLoops(lines: List<Segment2D>): List<List<Segment2D>> {
val remaining = lines.toMutableList()
val loops = mutableListOf<List<Segment2D>>()
while (remaining.isNotEmpty()) {
val loop = findSingleLoop(remaining)
if (loop.isNotEmpty()) {
loops.add(loop)
// 移除已使用的线段
loop.forEach { line ->
remaining.remove(line)
}
} else {
// 无法形成环的线段
break
}
}
return loops
}
fun findSingleLoop(remaining: MutableList<Segment2D>): List<Segment2D> {
if (remaining.isEmpty()) return emptyList()
val loop = mutableListOf<Segment2D>()
loop.add(remaining.removeAt(0))
// 向前查找连接
while (remaining.isNotEmpty()) {
val lastEnd = loop.last().end
val nextIndex = remaining.indexOfFirst { it.start == lastEnd }
if (nextIndex == -1) {
// 尝试向后查找连接
val firstStart = loop.first().start
val prevIndex = remaining.indexOfFirst { it.end == firstStart }
if (prevIndex != -1) {
loop.add(0, remaining.removeAt(prevIndex))
} else {
break // 无法继续连接
}
} else {
loop.add(remaining.removeAt(nextIndex))
}
// 检查是否形成闭环
if (loop.last().end == loop.first().start) {
return loop
}
}
// 如果没有形成闭环,返回空列表(或者可以根据需求返回部分环)
remaining.addAll(loop) // 将线段放回剩余列表
return emptyList()
}
fun Vector3D.rotateAroundZ(angle: Angle): Vector3D {
val cosAngle = cos(angle.radians)
val sinAngle = sin(angle.radians)
return Vector3D(
x = x * cosAngle - y * sinAngle,
y = x * sinAngle + y * cosAngle,
z = z
)
}
fun coordinateGenerate(width: Int, height: Int): List<Vector3D> {
val minX = 0.0
val maxX = width.toDouble()
val minY = 0.0
val maxY = height.toDouble()
val minZ = -20.0
val maxZ = 20.0
val x: () -> Double = { Random.nextDouble(minX, maxX) }
val y: () -> Double = { Random.nextDouble(minY, maxY) }
val z: () -> Double = { Random.nextDouble(minZ, maxZ) }
val dPoints = (0..60).map {
Vector3D(x(), y(), z())
}
return dPoints
}
fun coordinateGenerate1(): List<Vector3D> {
val center = Vector3D(0.0, 0.0, 0.0)
val direction = Vector3D(0.0, 1.0, -1.0)
return (0..360).step(36).map<Int, List<Vector3D>> { degrees: Int ->
val newDirection = direction.rotateAroundZ(angle = degrees.degrees)
(0..5).map {
center + newDirection * it * 100
}
}.flatten()
}
fun Vector3D.niceStr(): String {
return "[$x, $y, $z]".format(this)
}
fun List<Vector3D>.niceStr(): String {
return joinToString(", ", "[", "]") {
it.niceStr()
}
}

View File

@@ -0,0 +1,271 @@
import org.openrndr.KEY_ARROW_DOWN
import org.openrndr.KEY_ARROW_UP
import org.openrndr.WindowMultisample
import org.openrndr.application
import org.openrndr.color.ColorRGBa
import org.openrndr.draw.DrawPrimitive
import org.openrndr.draw.TextSettingMode
import org.openrndr.draw.loadFont
import org.openrndr.draw.shadeStyle
import org.openrndr.extra.camera.Orbital
import org.openrndr.extra.marchingsquares.findContours
import org.openrndr.extra.noise.gradientPerturbFractal
import org.openrndr.extra.objloader.loadOBJasVertexBuffer
import org.openrndr.extra.textwriter.writer
import org.openrndr.extra.triangulation.DelaunayTriangulation
import org.openrndr.math.Vector2
import org.openrndr.math.Vector3
import org.openrndr.shape.Path3D
import org.openrndr.shape.Segment3D
import org.openrndr.shape.ShapeContour
/**
* @author tabidachinokaze
* @date 2025/11/22
*/
fun main() = application {
configure {
width = 720
height = 720
title = "Delaunator"
multisample = WindowMultisample.SampleCount(8)
}
program {
/*val points3D = (0 until height).step(36).map { y ->
(0 until width).step(36).map { x ->
gradientPerturbFractal(
300,
frequency = 0.8,
position = Vector3(x.toDouble(), y.toDouble(), seconds)
)
}
}.flatten().map {
it.copy(x = it.x - width / 2, y = it.y - height / 2, z = it.z * 100)
}*/
/*val points3D = HeightmapVolcanoGenerator.generateVolcanoClusterHeightmap(
width = width,
height = height,
volcanoCount = 3
)*/
val points3D = coordinateGenerate(width, height).map {
it.copy(x = it.x - width / 2, y = it.y - height / 2)
}
val zs = points3D.map { it.z }
println("zs = ${zs}")
val associate: MutableMap<Vector2, Double> = points3D.associate {
Vector2(it.x, it.y) to it.z
}.toMutableMap()
val delaunay = DelaunayTriangulation(associate.map { it.key })
//println(points3D.niceStr())
//extend(Camera2D())
val cam = Orbital()
extend(cam) {
eye = Vector3(x = 100.0, y = 100.0, z = 0.0)
lookAt = Vector3(x = 1.6, y = -1.9, z = 1.2)
}
println("draw")
var targetHeight: Double = zs.average()
var logEnabled = true
var useInterpolation = false
var sampleLinear = false
keyboard.keyDown.listen {
logEnabled = true
println(it)
when (it.key) {
KEY_ARROW_UP -> targetHeight++
KEY_ARROW_DOWN -> targetHeight--
73 -> useInterpolation = !useInterpolation
83 -> sampleLinear = !sampleLinear
}
}
val vb = loadOBJasVertexBuffer("orx-obj-loader/test-data/non-planar.obj")
extend {
val triangles = delaunay.triangles()
val segments = mutableListOf<Segment3D>()
drawer.clear(ColorRGBa.BLACK)
val indexDiff = (frameCount / 1000) % triangles.size
drawer.shadeStyle = shadeStyle {
fragmentTransform = """
x_fill.rgb = normalize(v_viewNormal) * 0.5 + vec3(0.5);
""".trimIndent()
}
drawer.vertexBuffer(vb, DrawPrimitive.TRIANGLES)
for ((i, triangle) in triangles.withIndex()) {
val segment2DS = triangle.contour.segments.filter {
val startZ = associate[it.start]!!
val endZ = associate[it.end]!!
if (startZ < endZ) {
targetHeight in startZ..endZ
} else {
targetHeight in endZ..startZ
}
}
if (segment2DS.size == 2) {
val vector2s = segment2DS.map {
val startZ = associate[it.start]!!
val endZ = associate[it.end]!!
val start = Vector3(it.start.x, it.start.y, startZ)
val end = Vector3(it.end.x, it.end.y, endZ)
if (startZ < endZ) {
start to end
} else {
end to start
}
}.map { (start, end) ->
val segment3D = Segment3D(start, end)
val vector3 =
segment3D.position(calculatePositionRatio(targetHeight, start.z, end.z))
vector3
}.onEach {
associate[it.xy] = it.z
}
val element = Segment3D(vector2s[0], vector2s[1])
segments.add(element)
}
drawer.strokeWeight = 20.0
drawer.stroke = ColorRGBa.PINK
val segment3DS = triangle.contour.segments.map {
val startZ = associate[it.start]!!
val endZ = associate[it.end]!!
Segment3D(it.start.vector3(z = startZ), it.end.vector3(z = endZ))
}
//drawer.contour(triangle.contour)
drawer.path(Path3D.fromSegments(segment3DS, closed = true))
}
val sorted = connectAllSegments(segments)
drawer.stroke = ColorRGBa.WHITE
drawer.strokeWeight = 2.0
if (logEnabled) {
segments.forEach {
println("${it.start} -> ${it.end}")
}
println("=====")
}
sorted.forEach {
it.forEach {
if (logEnabled) println("${it.start} -> ${it.end}")
drawer.lineSegment(it.start, it.end)
drawer.fill = ColorRGBa.WHITE
}
if (logEnabled) println("=")
drawer.fill = ColorRGBa.YELLOW
// if (false) drawer.contour(ShapeContour.fromSegments(it, closed = true))
}
/*for (y in 0 until (area.height / cellSize).toInt()) {
for (x in 0 until (area.width / cellSize).toInt()) {
values[IntVector2(x, y)] = f(Vector2(x * cellSize + area.x, y * cellSize + area.y))
}
}*/
val contours = findContours(
f = {
val triangle = triangles.firstOrNull { triangle ->
isPointInTriangle(it, listOf(triangle.x1, triangle.x2, triangle.x3))
}
triangle ?: return@findContours 0.0
val interpolate = interpolateHeight(
point = it,
triangle = listOf(
triangle.x1,
triangle.x2,
triangle.x3,
).map {
Vector3(it.x, it.y, associate[it]!!)
}
)
interpolate.z - targetHeight
},
area = drawer.bounds.movedTo(Vector2(-width / 2.0, -height / 2.0)),
cellSize = 4.0,
useInterpolation = useInterpolation
)
if (logEnabled) println("useInterpolation = $useInterpolation")
drawer.stroke = null
contours.map {
it.segments.map {
Segment3D(
it.start.vector3(),
it.end.vector3()
)
}
}.forEach {
drawer.fill = ColorRGBa.GREEN.opacify(0.1)
drawer.path(Path3D.fromSegments(it, closed = true))
}
if (false) writer {
drawer.fontMap = loadFont("demo-data/fonts/IBMPlexMono-Regular.ttf", 24.0)
drawer.drawStyle.textSetting = TextSettingMode.SUBPIXEL
text(targetHeight.toString())
}
logEnabled = false
}
}
}
data class Triangle3D(
val x1: Vector3,
val x2: Vector3,
val x3: Vector3,
) {
fun toList(): List<Vector3> = listOf(x1, x2, x3)
}
fun connectAllSegments(segments: List<Segment3D>): List<List<Segment3D>> {
val remaining = segments.toMutableList()
val allPaths = mutableListOf<List<Segment3D>>()
while (remaining.isNotEmpty()) {
val path = mutableListOf<Segment3D>()
// 开始新路径
path.add(remaining.removeAt(0))
var changed: Boolean
do {
changed = false
// 向前扩展
val lastEnd = path.last().end
val forwardSegment = remaining.find { it.start == lastEnd || it.end == lastEnd }
if (forwardSegment != null) {
val connectedSegment = if (forwardSegment.start == lastEnd) {
forwardSegment // 正向
} else {
Segment3D(forwardSegment.end, forwardSegment.start) // 反向
}
path.add(connectedSegment)
remaining.remove(forwardSegment)
changed = true
}
// 向后扩展
val firstStart = path.first().start
val backwardSegment = remaining.find { it.end == firstStart || it.start == firstStart }
if (backwardSegment != null) {
val connectedSegment = if (backwardSegment.end == firstStart) {
backwardSegment // 正向
} else {
Segment3D(backwardSegment.end, backwardSegment.start) // 反向
}
path.add(0, connectedSegment)
remaining.remove(backwardSegment)
changed = true
}
} while (changed && remaining.isNotEmpty())
allPaths.add(path)
}
return allPaths
}

View File

@@ -0,0 +1,94 @@
import org.openrndr.application
import org.openrndr.color.ColorRGBa
import org.openrndr.extra.camera.Camera2D
import org.openrndr.extra.marchingsquares.findContours
import org.openrndr.math.Vector2
import org.openrndr.math.Vector3
/**
* A simple demonstration of using the `findContours` method provided by `orx-marching-squares`.
*
* `findContours` lets one generate contours by providing a mathematical function to be
* sampled within the provided area and with the given cell size. Contours are generated
* between the areas in which the function returns positive and negative values.
*
* In this example, the `f` function returns the distance of a point to the center of the window minus 200.0.
* Therefore, sampled locations which are less than 200 pixels away from the center return
* negative values and all others return positive values, effectively generating a circle of radius 200.0.
*
* Try increasing the cell size to see how the precision of the circle reduces.
*
* The circular contour created in this program has over 90 segments. The number of segments depends on the cell
* size, and the resulting radius.
*/
fun main() = application {
configure {
width = 720
height = 720
}
program {
extend(Camera2D())
var showLog = true
val target = Vector2(0.0, 0.0)
val points3D = (0..10).map { x ->
(0..10).map { y ->
Vector3(x.toDouble(), y.toDouble(), x * y * 1.0)
}
}
extend {
drawer.clear(ColorRGBa.BLACK)
drawer.stroke = ColorRGBa.PINK
fun f3(v: Vector2): Double {
val distance = drawer.bounds.center.distanceTo(v)
return when (distance) {
in 0.0..<100.0 -> -3.0
in 100.0..<200.0 -> 1.0
in 200.0..300.0 -> -1.0
else -> distance
}
}
fun f(v: Vector2): Double {
val distanceTo = v.distanceTo(target)
return (distanceTo - 100.0).also {
if (showLog) println(
buildString {
appendLine("${v} distanceTo ${target} = ${distanceTo}")
appendLine("distanceTo - 100.0 = ${distanceTo - 100.0}")
}
)
}
}
val points = mutableListOf<Vector2>()
fun f1(v: Vector2): Double {
val result = if (v.x == v.y * 2 || v.x * 2 == v.y) {
points.add(v)
-1.0
} else 0.0
return result.also {
if (showLog) {
println("$v -> $result")
}
}
}
val contours = findContours(::f3, drawer.bounds, 4.0)
drawer.fill = null
drawer.contours(contours)
if (showLog) {
println(
buildString {
for ((index, contour) in contours.withIndex()) {
appendLine("index = ${index}, $contour")
}
}
)
}
showLog = false
}
}
}

View File

@@ -0,0 +1,373 @@
import com.icegps.math.geometry.Vector3D
import kotlin.math.PI
import kotlin.math.abs
import kotlin.math.cos
import kotlin.math.exp
import kotlin.math.floor
import kotlin.math.max
import kotlin.math.sin
import kotlin.math.sqrt
/**
* @author tabidachinokaze
* @date 2025/11/22
*/
object HeightmapVolcanoGenerator {
// 基础火山高度图
fun generateVolcanoHeightmap(
width: Int = 100,
height: Int = 100,
centerX: Double = 50.0,
centerY: Double = 50.0,
maxHeight: Double = 60.0,
craterRadius: Double = 8.0,
volcanoRadius: Double = 30.0
): List<Vector3D> {
val points = mutableListOf<Vector3D>()
for (x in 0 until width) {
for (y in 0 until height) {
// 计算到火山中心的距离
val dx = x - centerX
val dy = y - centerY
val distance = sqrt(dx * dx + dy * dy)
// 计算基础火山高度
var z = calculateVolcanoHeight(distance, craterRadius, volcanoRadius, maxHeight)
// 添加噪声细节
val noise = perlinNoise(x * 0.1, y * 0.1, 0.1) * 3.0
z = max(0.0, z + noise)
points.add(Vector3D(x.toDouble(), y.toDouble(), z))
}
}
return points
}
// 复合火山群高度图
fun generateVolcanoClusterHeightmap(
width: Int = 150,
height: Int = 150,
volcanoCount: Int = 3
): List<Vector3D> {
val points = mutableListOf<Vector3D>()
val volcanoes = generateRandomVolcanoPositions(volcanoCount, width, height)
for (x in (0 until width).step(25)) {
for (y in (0 until height).step(25)) {
var totalZ = 0.0
// 叠加所有火山的影响
for (volcano in volcanoes) {
val dx = x - volcano.x
val dy = y - volcano.y
val distance = sqrt(dx * dx + dy * dy)
if (distance <= volcano.radius) {
val volcanoHeight = calculateVolcanoHeight(
distance,
volcano.craterRadius,
volcano.radius,
volcano.maxHeight
)
totalZ += volcanoHeight
}
}
// 基础地形
val baseNoise = perlinNoise(x * 0.02, y * 0.02, 0.05) * 5.0
val detailNoise = perlinNoise(x * 0.1, y * 0.1, 0.2) * 2.0
points.add(Vector3D(x.toDouble(), y.toDouble(), totalZ + baseNoise + detailNoise))
}
}
return points
}
// 带熔岩流的火山高度图
fun generateVolcanoWithLavaHeightmap(
width: Int = 100,
height: Int = 100
): List<Vector3D> {
val points = mutableListOf<Vector3D>()
val centerX = width / 2.0
val centerY = height / 2.0
// 生成熔岩流路径
val lavaFlows = generateLavaFlowPaths(centerX, centerY, 3)
for (x in 0 until width) {
for (y in 0 until height) {
val dx = x - centerX
val dy = y - centerY
val distance = sqrt(dx * dx + dy * dy)
// 基础火山高度
var z = calculateVolcanoHeight(distance, 10.0, 35.0, 70.0)
// 添加熔岩流
z += calculateLavaFlowEffect(x.toDouble(), y.toDouble(), lavaFlows)
// 侵蚀效果
z += calculateErosionEffect(x.toDouble(), y.toDouble(), distance, z)
points.add(Vector3D(x.toDouble(), y.toDouble(), max(0.0, z)))
}
}
return points
}
// 破火山口高度图
fun generateCalderaHeightmap(
width: Int = 100,
height: Int = 100
): List<Vector3D> {
val points = mutableListOf<Vector3D>()
val centerX = width / 2.0
val centerY = height / 2.0
for (x in 0 until width) {
for (y in 0 until height) {
val dx = x - centerX
val dy = y - centerY
val distance = sqrt(dx * dx + dy * dy)
var z = calculateCalderaHeight(distance, 15.0, 45.0, 50.0)
// 内部平坦区域细节
if (distance < 20) {
z += perlinNoise(x * 0.2, y * 0.2, 0.3) * 1.5
}
points.add(Vector3D(x.toDouble(), y.toDouble(), max(0.0, z)))
}
}
return points
}
// 线性火山链高度图
fun generateVolcanoChainHeightmap(
width: Int = 200,
height: Int = 100
): List<Vector3D> {
val points = mutableListOf<Vector3D>()
// 在一条线上生成多个火山
val chainCenters = listOf(
Vector3D(30.0, 50.0, 0.0),
Vector3D(70.0, 50.0, 0.0),
Vector3D(110.0, 50.0, 0.0),
Vector3D(150.0, 50.0, 0.0),
Vector3D(170.0, 50.0, 0.0)
)
for (x in 0 until width) {
for (y in 0 until height) {
var totalZ = 0.0
for (center in chainCenters) {
val dx = x - center.x
val dy = y - center.y
val distance = sqrt(dx * dx + dy * dy)
if (distance <= 25.0) {
val volcanoZ = calculateVolcanoHeight(distance, 6.0, 25.0, 40.0)
totalZ += volcanoZ
}
}
// 添加基底地形,模拟山脉链
val baseRidge = calculateMountainRidge(x.toDouble(), y.toDouble(), width, height)
totalZ += baseRidge
points.add(Vector3D(x.toDouble(), y.toDouble(), totalZ))
}
}
return points
}
// 辅助函数
private data class VolcanoInfo(
val x: Double,
val y: Double,
val radius: Double,
val craterRadius: Double,
val maxHeight: Double
)
private data class LavaFlowInfo(
val startX: Double,
val startY: Double,
val angle: Double, // 弧度
val length: Double,
val width: Double,
val intensity: Double
)
private fun calculateVolcanoHeight(
distance: Double,
craterRadius: Double,
volcanoRadius: Double,
maxHeight: Double
): Double {
return when {
distance <= craterRadius -> {
// 火山口 - 中心凹陷
val craterDepth = maxHeight * 0.4
craterDepth * (1.0 - distance / craterRadius)
}
distance <= volcanoRadius -> {
// 火山锥
val slopeDistance = distance - craterRadius
val maxSlopeDistance = volcanoRadius - craterRadius
val normalized = slopeDistance / maxSlopeDistance
maxHeight * (1.0 - normalized * normalized)
}
else -> 0.0
}
}
private fun calculateCalderaHeight(
distance: Double,
innerRadius: Double,
outerRadius: Double,
rimHeight: Double
): Double {
return when {
distance <= innerRadius -> {
// 平坦的破火山口底部
rimHeight * 0.2
}
distance <= outerRadius -> {
// 陡峭的边缘
val rimDistance = distance - innerRadius
val rimWidth = outerRadius - innerRadius
val normalized = rimDistance / rimWidth
rimHeight * (1.0 - (1.0 - normalized) * (1.0 - normalized))
}
else -> {
// 外部平缓斜坡
val externalDistance = distance - outerRadius
rimHeight * exp(-externalDistance * 0.08)
}
}
}
private fun calculateLavaFlowEffect(x: Double, y: Double, lavaFlows: List<LavaFlowInfo>): Double {
var effect = 0.0
for (flow in lavaFlows) {
val dx = x - flow.startX
val dy = y - flow.startY
// 计算到熔岩流中心线的距离
val flowDirX = cos(flow.angle)
val flowDirY = sin(flow.angle)
val projection = dx * flowDirX + dy * flowDirY
if (projection in 0.0..flow.length) {
val perpendicularX = dx - projection * flowDirX
val perpendicularY = dy - projection * flowDirY
val perpendicularDist = sqrt(perpendicularX * perpendicularX + perpendicularY * perpendicularY)
if (perpendicularDist <= flow.width) {
val widthFactor = 1.0 - (perpendicularDist / flow.width)
val lengthFactor = 1.0 - (projection / flow.length)
effect += flow.intensity * widthFactor * lengthFactor
}
}
}
return effect
}
private fun calculateErosionEffect(x: Double, y: Double, distance: Double, height: Double): Double {
// 基于坡度的侵蚀
val slopeNoise = perlinNoise(x * 0.15, y * 0.15, 0.1) * 2.0
// 基于距离的侵蚀
val distanceErosion = if (distance > 25) perlinNoise(x * 0.08, y * 0.08, 0.05) * 1.5 else 0.0
return slopeNoise + distanceErosion
}
private fun calculateMountainRidge(x: Double, y: Double, width: Int, height: Int): Double {
// 创建山脉基底
val ridgeCenter = height / 2.0
val distanceToRidge = abs(y - ridgeCenter)
val ridgeWidth = height * 0.3
if (distanceToRidge <= ridgeWidth) {
val ridgeFactor = 1.0 - (distanceToRidge / ridgeWidth)
return ridgeFactor * 15.0 * perlinNoise(x * 0.01, y * 0.01, 0.02)
}
return 0.0
}
private fun generateRandomVolcanoPositions(count: Int, width: Int, height: Int): List<VolcanoInfo> {
return List(count) {
VolcanoInfo(
x = (width * 0.2 + random() * width * 0.6),
y = (height * 0.2 + random() * height * 0.6),
radius = 20.0 + random() * 20.0,
craterRadius = 5.0 + random() * 7.0,
maxHeight = 25.0 + random() * 35.0
)
}
}
private fun generateLavaFlowPaths(centerX: Double, centerY: Double, count: Int): List<LavaFlowInfo> {
return List(count) {
LavaFlowInfo(
startX = centerX,
startY = centerY,
angle = random() * 2 * PI,
length = 20.0 + random() * 15.0,
width = 2.0 + random() * 3.0,
intensity = 5.0 + random() * 8.0
)
}
}
private fun perlinNoise(x: Double, y: Double, frequency: Double): Double {
// 简化的柏林噪声实现
val x0 = floor(x * frequency)
val y0 = floor(y * frequency)
val x1 = x0 + 1
val y1 = y0 + 1
fun grad(ix: Int, iy: Int): Double {
val random = sin(ix * 12.9898 + iy * 78.233) * 43758.5453
return (random % 1.0) * 2 - 1
}
fun interpolate(a: Double, b: Double, w: Double): Double {
return a + (b - a) * (w * w * (3 - 2 * w))
}
val g00 = grad(x0.toInt(), y0.toInt())
val g10 = grad(x1.toInt(), y0.toInt())
val g01 = grad(x0.toInt(), y1.toInt())
val g11 = grad(x1.toInt(), y1.toInt())
val tx = x * frequency - x0
val ty = y * frequency - y0
val n0 = interpolate(g00, g10, tx)
val n1 = interpolate(g01, g11, tx)
return interpolate(n0, n1, ty)
}
private fun random(): Double = Math.random()
}