Skip to content

Instantly share code, notes, and snippets.

@melix
Created November 29, 2017 19:42
Show Gist options
  • Star 5 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save melix/8fe65c46620488c445f0c869cfc69205 to your computer and use it in GitHub Desktop.
Save melix/8fe65c46620488c445f0c869cfc69205 to your computer and use it in GitHub Desktop.
A Kotlin+JavaFX port of https://github.com/s-macke/VoxelSpace
/**
* 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