/*
* Copyright (c) 2016 Jesper Öqvist <jesper@llbit.se>
*
* This file is part of Chunky.
*
* Chunky is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Chunky is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
* You should have received a copy of the GNU General Public License
* along with Chunky. If not, see <http://www.gnu.org/licenses/>.
*/
package se.llbit.chunky.ui.render;
import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader;
import javafx.fxml.Initializable;
import javafx.scene.Parent;
import javafx.scene.canvas.Canvas;
import javafx.scene.canvas.GraphicsContext;
import javafx.scene.control.Button;
import javafx.scene.control.ChoiceBox;
import javafx.scene.control.TextField;
import javafx.scene.image.PixelFormat;
import javafx.scene.image.WritableImage;
import javafx.scene.image.WritablePixelFormat;
import javafx.stage.FileChooser;
import javafx.stage.Stage;
import org.apache.commons.math3.util.FastMath;
import se.llbit.chunky.renderer.scene.Camera;
import se.llbit.chunky.renderer.scene.PlayerModel;
import se.llbit.chunky.ui.DoubleAdjuster;
import se.llbit.math.BVH;
import se.llbit.math.ColorUtil;
import se.llbit.math.Matrix3;
import se.llbit.math.QuickMath;
import se.llbit.math.Ray;
import se.llbit.math.Vector3;
import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.nio.IntBuffer;
import java.util.Collections;
import java.util.LinkedList;
import java.util.ResourceBundle;
/**
* A tool for posing entities.
*/
public class Poser extends Stage implements Initializable {
private static final WritablePixelFormat<IntBuffer> PIXEL_FORMAT =
PixelFormat.getIntArgbInstance();
private final EntitiesTab.PlayerData player;
private BVH bvh = new BVH(Collections.emptyList());
private int[] pixels;
private int width = 300;
private int height = 300;
private Matrix3 transform = new Matrix3();
private Vector3 camPos = new Vector3();
private WritableImage image;
@FXML private Canvas preview;
@FXML private ChoiceBox<PlayerModel> playerModel;
@FXML private TextField skin;
@FXML private Button selectSkin;
@FXML private DoubleAdjuster direction;
@FXML private DoubleAdjuster headYaw;
@FXML private DoubleAdjuster headPitch;
@FXML private DoubleAdjuster leftArmPose;
@FXML private DoubleAdjuster rightArmPose;
@FXML private DoubleAdjuster leftLegPose;
@FXML private DoubleAdjuster rightLegPose;
private double lastX;
private double lastY;
public Poser(EntitiesTab.PlayerData data) throws IOException {
this.player = data;
FXMLLoader loader = new FXMLLoader(getClass().getResource("Poser.fxml"));
loader.setController(this);
Parent root = loader.load();
setScene(new javafx.scene.Scene(root));
setTitle("Pose Preview");
}
@Override public void initialize(URL location, ResourceBundle resources) {
selectSkin.setOnAction(e -> {
FileChooser fileChooser = new FileChooser();
fileChooser.setTitle("Load Skin");
fileChooser
.setSelectedExtensionFilter(new FileChooser.ExtensionFilter("Minecraft skin", "*.png"));
File skinFile = fileChooser.showOpenDialog(getScene().getWindow());
if (skinFile != null) {
player.entity.setTexture(skinFile.getAbsolutePath());
skin.setText(skinFile.getAbsolutePath());
redraw();
}
});
preview.setOnMousePressed(e -> {
lastX = e.getX();
lastY = e.getY();
});
preview.setOnMouseDragged(e -> {
double dx = e.getX() - lastX;
double dy = e.getY() - lastY;
lastX = e.getX();
lastY = e.getY();
direction.setAndUpdate(direction.get() + dx / 20);
headPitch.setAndUpdate(headPitch.get() - dy / 60);
});
skin.setText(player.entity.skin);
pixels = new int[width * height];
transform.setIdentity();
image = new WritableImage(width, height);
playerModel.getItems().addAll(PlayerModel.values());
playerModel.getSelectionModel().select(player.entity.model);
playerModel.getSelectionModel().selectedItemProperty()
.addListener((observable, oldValue, newValue) -> {
player.entity.model = newValue;
redraw();
});
direction.setName("Direction");
direction.setRange(-Math.PI, Math.PI);
direction.onValueChange(value -> {
player.entity.yaw = value;
redraw();
});
headYaw.setName("Head yaw");
headYaw.setRange(-QuickMath.HALF_PI, QuickMath.HALF_PI);
headYaw.onValueChange(value -> {
player.entity.headYaw = value;
redraw();
});
headPitch.setName("Head pitch");
headPitch.setRange(-QuickMath.HALF_PI, QuickMath.HALF_PI);
headPitch.onValueChange(value -> {
player.entity.pitch = value;
redraw();
});
leftArmPose.setName("Left arm pose");
leftArmPose.setRange(-Math.PI, Math.PI);
leftArmPose.onValueChange(value -> {
player.entity.leftArmPose = value;
redraw();
});
rightArmPose.setName("Right arm pose");
rightArmPose.setRange(-Math.PI, Math.PI);
rightArmPose.onValueChange(value -> {
player.entity.rightArmPose = value;
redraw();
});
leftLegPose.setName("Left leg pose");
leftLegPose.setRange(-QuickMath.HALF_PI, QuickMath.HALF_PI);
leftLegPose.onValueChange(value -> {
player.entity.leftLegPose = value;
redraw();
});
rightLegPose.setName("Right leg pose");
rightLegPose.setRange(-QuickMath.HALF_PI, QuickMath.HALF_PI);
rightLegPose.onValueChange(value -> {
player.entity.rightLegPose = value;
redraw();
});
redraw();
}
private void buildBvh() {
Vector3 offset = new Vector3(); // Offset to place player in focus.
offset.sub(player.entity.position);
bvh = new BVH(new LinkedList<>(player.entity.primitives(offset)));
}
private void redraw() {
buildBvh();
GraphicsContext gc = preview.getGraphicsContext2D();
Ray ray = new Ray();
double aspect = width / (double) height;
double fovTan = Camera.clampedFovTan(70);
camPos.set(0, 1, -2);
for (int y = 0; y < height; ++y) {
double rayy = fovTan * (.5 - ((double) y) / height);
for (int x = 0; x < width; ++x) {
double rayx = fovTan * aspect * (.5 - ((double) x) / width);
ray.setDefault();
ray.t = Double.MAX_VALUE;
ray.d.set(rayx, rayy, 1);
ray.d.normalize();
ray.o.set(camPos);
while (true) {
if (bvh.closestIntersection(ray)) {
if (ray.color.w > 0.9) {
break;
}
ray.o.scaleAdd(ray.t, ray.d);
} else {
if (x % 20 == 0 || y % 20 == 0) {
ray.color.set(0.7, 0.7, 0.7, 1);
} else {
ray.color.set(1, 1, 1, 1);
}
break;
}
}
ray.color.x = QuickMath.min(1, FastMath.sqrt(ray.color.x));
ray.color.y = QuickMath.min(1, FastMath.sqrt(ray.color.y));
ray.color.z = QuickMath.min(1, FastMath.sqrt(ray.color.z));
pixels[y * width + x] = ColorUtil.getRGB(ray.color);
}
}
image.getPixelWriter().setPixels(0, 0, width, height, PIXEL_FORMAT, pixels, 0, width);
gc.drawImage(image, 0, 0);
}
}