add android and desktop modules
This commit is contained in:
36
desktop/build.gradle.kts
Normal file
36
desktop/build.gradle.kts
Normal 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"))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
605
desktop/src/jvmDemo/kotlin/DemoDelaunay03.kt
Normal file
605
desktop/src/jvmDemo/kotlin/DemoDelaunay03.kt
Normal 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()
|
||||
}
|
||||
}
|
||||
271
desktop/src/jvmDemo/kotlin/DemoDelaunay3D.kt
Normal file
271
desktop/src/jvmDemo/kotlin/DemoDelaunay3D.kt
Normal 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
|
||||
}
|
||||
94
desktop/src/jvmDemo/kotlin/FindContours.kt
Normal file
94
desktop/src/jvmDemo/kotlin/FindContours.kt
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
373
desktop/src/jvmDemo/kotlin/HeightmapVolcanoGenerator.kt
Normal file
373
desktop/src/jvmDemo/kotlin/HeightmapVolcanoGenerator.kt
Normal 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()
|
||||
}
|
||||
Reference in New Issue
Block a user