Created
November 29, 2017 19:41
-
-
Save melix/c64c138b89b15363af4f6e593f1efef7 to your computer and use it in GitHub Desktop.
A Groovy+JavaFX port of https://github.com/s-macke/VoxelSpace
This file contains hidden or 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 static Groovy and JavaFX. | |
* | |
* Click on the panel to "fly". | |
* | |
* Run with : groovy voxel.groovy | |
* | |
* Twitter: @CedricChampeau | |
*/ | |
import groovy.transform.Canonical | |
import groovy.transform.CompileStatic | |
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.input.MouseEvent | |
import javafx.scene.layout.Pane | |
import javafx.scene.paint.Color | |
import javafx.stage.Stage | |
import javax.imageio.ImageIO | |
import java.awt.image.BufferedImage | |
import java.util.concurrent.atomic.AtomicBoolean | |
@CompileStatic | |
class Camera { | |
volatile double x = 512d | |
volatile double y = 300d | |
volatile double height = 78d | |
volatile double angle = 0d | |
volatile double horizon = 100d | |
volatile double distance = 800d | |
} | |
@CompileStatic | |
@Canonical | |
class VoxelMap { | |
final byte[] height | |
final int[] color | |
static VoxelMap load(String color, String height) { | |
new VoxelMap(heightMap(height), colorMap(color)) | |
} | |
private static int[] colorMap(String color) { | |
img(color).with { | |
def arr = new int[1024 * 1024] | |
for (int i = 0; i < arr.length; i++) { | |
arr[i] = getRGB(i % 1024, i.intdiv(1024)) | |
} | |
arr | |
} | |
} | |
private static byte[] heightMap(String height) { | |
int[] ints = img(height).data.getPixels(0, 0, 1024, 1024, new int[1024 * 1024]) | |
byte[] bytes = new byte[ints.length] | |
for (int i = 0; i < ints.length; i++) { | |
bytes[i] = (byte) ints[i] | |
} | |
bytes | |
} | |
private static BufferedImage img(String file) { | |
ImageIO.read("https://raw.githubusercontent.com/s-macke/VoxelSpace/master/maps/${file}.png".toURL()) | |
} | |
} | |
@CompileStatic | |
@Canonical | |
class Delta { | |
volatile double angleDelta | |
volatile double zDelta | |
} | |
@CompileStatic | |
class VoxelApp extends Application { | |
Camera camera = new Camera() | |
VoxelMap map = null | |
static void start(String colors, String height) { | |
launch(VoxelApp, colors, height) | |
} | |
@Override | |
void init() { | |
super.init() | |
def colors = parameters.unnamed[0] | |
def height = parameters.unnamed[1] | |
map = VoxelMap.load(colors, height) | |
} | |
@Override | |
void start(Stage primaryStage) { | |
primaryStage.title = "Voxel Engine demo" | |
def root = new Pane() | |
def canvas = new Canvas(800d, 600d) | |
def gc = canvas.graphicsContext2D | |
root.style = "-fx-background-color: #99ccff;" | |
render(gc) | |
root.children.add(canvas) | |
def scene = new Scene(root) | |
primaryStage.scene = scene | |
primaryStage.show() | |
setupResizeListener(scene, canvas, gc) | |
setupRenderLoop(primaryStage, gc, scene) | |
} | |
private static Color rgb(int rgb) { Color.rgb((rgb >> 16) & 255, (rgb >> 8) & 255, rgb & 255) } | |
private | |
void setupRenderLoop(Stage primaryStage, GraphicsContext gc, Scene scene) { | |
def dragging = new AtomicBoolean() | |
def delta = new Delta() | |
new Thread(new Task<Void>() { | |
Void call() { | |
while (true) { | |
if (dragging.get()) { | |
camera.with { | |
horizon += 2d * delta.zDelta | |
height += delta.zDelta | |
angle += delta.angleDelta | |
x -= Math.sin(angle) * 5d | |
y -= Math.cos(angle) * 5d | |
height = Math.max(0d, height) | |
height = Math.min(height, 255d) | |
} | |
} | |
sleep(50) | |
} | |
} | |
}).start() | |
setupMouseDragHandlers(primaryStage, scene, dragging, delta) | |
startAnimation(gc) | |
} | |
private void startAnimation(GraphicsContext gc) { | |
new AnimationTimer() { | |
void handle(long timestamp) { | |
render(gc) | |
} | |
}.start() | |
} | |
private void setupMouseDragHandlers(Stage primaryStage, Scene scene, AtomicBoolean dragging, Delta delta) { | |
scene.with { | |
onMousePressed = { mouseEvent -> | |
dragging.set(true) | |
scene.setCursor(Cursor.MOVE) | |
} | |
onMouseReleased = { | |
scene.setCursor(Cursor.HAND) | |
dragging.set(false) | |
delta.angleDelta = 0d | |
delta.zDelta = 0d | |
} | |
onMouseDragged = { MouseEvent mouseEvent -> | |
primaryStage.with { | |
double centerX = width / 2d | |
double centerY = height / 2d | |
delta.angleDelta = (centerX - mouseEvent.x) / (10d * width) | |
delta.zDelta = 10d * (centerY - mouseEvent.y) / height | |
} | |
} | |
} | |
} | |
private void setupResizeListener(Scene scene, Canvas canvas, GraphicsContext gc) { | |
scene.with { | |
widthProperty().addListener { _, oldValue, newValue -> | |
if (oldValue != newValue) { | |
canvas.width = (double) newValue | |
render(gc) | |
} | |
} | |
heightProperty().addListener { _, oldValue, newValue -> | |
if (oldValue != newValue) { | |
canvas.height = (double) newValue | |
render(gc) | |
} | |
} | |
} | |
} | |
private void drawLine(GraphicsContext g, double x, double ytop, double ybottom, int color) { | |
def top = ytop < 0d ? 0d : ytop | |
if (top > ybottom) return | |
g.stroke = rgb(color) | |
g.strokeLine(x, ybottom, x, top) | |
} | |
private void render(GraphicsContext g) { | |
def screenwidth = g.canvas.width | |
def screenheight = g.canvas.height | |
def sinang = Math.sin(camera.angle) | |
def cosang = Math.cos(camera.angle) | |
def screenWidthAsInt = (int) screenwidth | |
def hiddeny = new double[screenWidthAsInt] | |
for (int i = 0; i < screenWidthAsInt; i++) { | |
hiddeny[i] = screenheight | |
} | |
g.clearRect(0, 0, screenwidth, screenheight) | |
def dz = 1d | |
def z = 1d | |
while (z < camera.distance) { | |
// 90 degree field of view | |
def plx = -cosang * z - sinang * z | |
def ply = sinang * z - cosang * z | |
def prx = cosang * z - sinang * z | |
def pry = -sinang * z - cosang * z | |
def dx = (prx - plx) / screenwidth | |
def dy = (pry - ply) / screenwidth | |
plx += camera.x | |
ply += camera.y | |
def invz = 1d / z * 240d | |
for (int i=0; i<screenWidthAsInt; i++) { | |
def mapoffset = ((((int) Math.floor(ply)) & 1023) << 10) + (((int) Math.floor(plx)) & 1023) | |
def heightonscreen = (camera.height - map.height[mapoffset]) * invz + camera.horizon | |
drawLine(g, (double) i, 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