Created
November 29, 2017 19:42
-
-
Save melix/8fe65c46620488c445f0c869cfc69205 to your computer and use it in GitHub Desktop.
A Kotlin+JavaFX port of https://github.com/s-macke/VoxelSpace
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* An approximate port of https://github.com/s-macke/VoxelSpace | |
* using Kotlin and JavaFX. | |
* | |
* Run with : kotlinc -script voxel.kts | |
* | |
* Click on the panel to "fly". | |
* | |
* Twitter: @CedricChampeau | |
*/ | |
import javafx.animation.AnimationTimer | |
import javafx.application.Application | |
import javafx.concurrent.Task | |
import javafx.scene.Cursor | |
import javafx.scene.Scene | |
import javafx.scene.canvas.Canvas | |
import javafx.scene.canvas.GraphicsContext | |
import javafx.scene.layout.Pane | |
import javafx.scene.paint.Color | |
import javafx.stage.Stage | |
import java.lang.Thread.sleep | |
import java.net.URL | |
import java.util.concurrent.atomic.AtomicBoolean | |
import javax.imageio.ImageIO | |
data class Camera(@Volatile var x: Double = 512.0, | |
@Volatile var y: Double = 300.0, | |
@Volatile var height: Double = 78.0, | |
@Volatile var angle: Double = 0.0, | |
@Volatile var horizon: Double = 100.0, | |
@Volatile var distance: Double = 800.0) | |
data class VoxelMap(val height: ByteArray, | |
val color: IntArray) { | |
companion object { | |
fun load(color: String, height: String): VoxelMap = | |
VoxelMap(heightMap(height), colorMap(color)) | |
private fun colorMap(color: String): IntArray = img(color).run { | |
IntArray(1024 * 1024) { | |
getRGB(it % 1024, it / 1024) | |
} | |
} | |
private fun heightMap(height: String) = img(height).data.getPixels(0, 0, 1024, 1024, IntArray(1024 * 1024)).run { | |
ByteArray(1024 * 1024) { this[it].toByte() } | |
} | |
fun img(file: String) = ImageIO.read(URL("https://raw.githubusercontent.com/s-macke/VoxelSpace/master/maps/${file}.png")) | |
} | |
} | |
data class Delta(@Volatile var angleDelta: Double = 0.0, @Volatile var zDelta: Double = 0.0) | |
class VoxelApp : Application() { | |
val camera = Camera() | |
var map: VoxelMap? = null | |
companion object { | |
fun start(colors: String, height: String) = Application.launch(VoxelApp::class.java, colors, height) | |
} | |
override | |
fun init() { | |
super.init() | |
val (colors, height) = parameters.unnamed | |
map = VoxelMap.load(colors, height) | |
} | |
override | |
fun start(primaryStage: Stage?) { | |
primaryStage!!.setTitle("Voxel Engine demo") | |
val root = Pane() | |
val canvas = Canvas(800.0, 600.0) | |
val gc = canvas.graphicsContext2D!! | |
root.setStyle("-fx-background-color: #99ccff;") | |
render(gc) | |
root.children.add(canvas) | |
val scene = Scene(root) | |
primaryStage.setScene(scene) | |
primaryStage.show() | |
setupResizeListener(scene, canvas, gc) | |
setupRenderLoop(primaryStage, gc, scene) | |
} | |
private | |
fun rgb(rgb: Int) = Color.rgb(rgb shr 16 and 255, rgb shr 8 and 255, rgb and 255) | |
private | |
fun setupRenderLoop(primaryStage: Stage, gc: GraphicsContext, scene: Scene) { | |
val dragging = AtomicBoolean() | |
val delta = Delta() | |
Thread(object : Task<Unit>() { | |
override | |
fun call() { | |
while (true) { | |
if (dragging.get()) { | |
camera.run { | |
delta.run { | |
horizon += 2 * zDelta | |
height += zDelta | |
angle += angleDelta | |
x -= Math.sin(angle) * 5 | |
y -= Math.cos(angle) * 5 | |
height = Math.max(0.0, height) | |
height = Math.min(height, 255.0) | |
} | |
} | |
} | |
sleep(50) | |
} | |
} | |
}).start() | |
setupMouseDragHandlers(primaryStage, scene, dragging, delta) | |
startAnimation(gc) | |
} | |
private | |
fun startAnimation(gc: GraphicsContext) = object : AnimationTimer() { | |
override | |
fun handle(timestamp: Long) { | |
render(gc) | |
} | |
}.start() | |
private | |
fun setupMouseDragHandlers(primaryStage: Stage, scene: Scene, dragging: AtomicBoolean, delta: Delta) = scene.run { | |
setOnMousePressed { mouseEvent -> | |
dragging.set(true) | |
scene.setCursor(Cursor.MOVE) | |
} | |
setOnMouseReleased { | |
scene.setCursor(Cursor.HAND) | |
dragging.set(false) | |
delta.angleDelta = 0.0 | |
delta.zDelta = 0.0 | |
} | |
setOnMouseDragged { mouseEvent -> | |
primaryStage.run { | |
val centerX = width / 2 | |
val centerY = height / 2 | |
delta.angleDelta = (centerX - mouseEvent.x) / (10 * width) | |
delta.zDelta = 10 * (centerY - mouseEvent.y) / height | |
} | |
} | |
} | |
private | |
fun setupResizeListener(scene: Scene, canvas: Canvas, gc: GraphicsContext) = scene.run { | |
widthProperty().addListener { _, oldValue, newValue -> | |
if (oldValue != newValue) { | |
canvas.setWidth(newValue as Double) | |
render(gc) | |
} | |
} | |
heightProperty().addListener { _, oldValue, newValue -> | |
if (oldValue != newValue) { | |
canvas.setHeight(newValue as Double) | |
render(gc) | |
} | |
} | |
} | |
private | |
fun drawLine(g: GraphicsContext, x: Double, ytop: Double, ybottom: Double, color: Int) { | |
val top = if (ytop < 0) 0.0 else ytop | |
if (top > ybottom) return | |
g.setStroke(rgb(color)) | |
g.strokeLine(x, ybottom, x, top) | |
} | |
private | |
fun render(g: GraphicsContext) { | |
val screenwidth = g.canvas.width | |
val screenheight = g.canvas.height | |
val sinang = Math.sin(camera.angle) | |
val cosang = Math.cos(camera.angle) | |
val screenWidthAsInt = screenwidth.toInt() | |
val hiddeny = DoubleArray(screenWidthAsInt) { | |
screenheight | |
} | |
g.clearRect(0.0, 0.0, screenwidth, screenheight) | |
val dz: Double = 1.0 | |
var z: Double = 1.0 | |
while (z < camera.distance) { | |
// 90 degree field of view | |
var plx = -cosang * z - sinang * z; | |
var ply = sinang * z - cosang * z; | |
val prx = cosang * z - sinang * z; | |
val pry = -sinang * z - cosang * z; | |
val dx = (prx - plx) / screenwidth; | |
val dy = (pry - ply) / screenwidth; | |
plx += camera.x; | |
ply += camera.y; | |
val invz = 1.0 / z * 240.0 | |
for (i in 0 until screenWidthAsInt) { | |
val mapoffset = (Math.floor(ply).toInt() and 1023 shl 10) + (Math.floor(plx).toInt() and 1023) | |
val heightonscreen = (camera.height - map!!.height[mapoffset]) * invz + camera.horizon | |
drawLine(g, i.toDouble(), heightonscreen, hiddeny[i], map!!.color[mapoffset]) | |
if (heightonscreen < hiddeny[i]) { | |
hiddeny[i] = heightonscreen | |
} | |
plx += dx | |
ply += dy | |
} | |
z += dz | |
} | |
} | |
} | |
VoxelApp.start("C1W", "D1") |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment