Как правильно рендерить 3D графику
Я пытался сделать кубик Рубикса в javafx
В итоге получилась очень плохая модель, как показано на этом рисунке. Я даю свой исходный код для этого, где я использовал RectangleBuilder
класс для создания прямоугольников и преобразования в 3d. Чтобы исправить графику, я также попытался построить используемые прямоугольники TriangleMesh
класс и после добавления материалов к ним, преобразовал их в 3D, чтобы снова оказаться в той же плохой графике. Почему это происходит и как от этого избавиться?
import javafx.scene.transform.Rotate;
import javafx.scene.PerspectiveCamera;
import javafx.scene.transform.Translate;
import javafx.application.Application;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.stage.Stage;
import javafx.animation.Animation;
import javafx.animation.KeyFrame;
import javafx.animation.KeyValue;
import javafx.animation.Timeline;
import javafx.event.EventHandler;
import javafx.scene.Group;
import javafx.scene.Node;
import javafx.scene.SceneAntialiasing;
import javafx.scene.input.MouseEvent;
import javafx.scene.paint.Color;
import javafx.scene.shape.RectangleBuilder;
import javafx.scene.transform.Rotate;
import javafx.util.Duration;
public class NewFXMain1 extends Application {
public class Cube extends Group {
final Rotate rx = new Rotate(0,Rotate.X_AXIS);
final Rotate ry = new Rotate(0,Rotate.Y_AXIS);
final Rotate rz = new Rotate(0,Rotate.Z_AXIS);
public Cube(double size, Color back,Color bottom,Color right,Color left,Color top,Color front, double shade) {
getTransforms().addAll(rz, ry, rx);
getChildren().addAll(
RectangleBuilder.create() // back face
.width(size).height(size)
.fill(back.deriveColor(0.0, 1.0, (1 - 0.5*shade), 1.0))
.translateX(-0.5*size)
.translateY(-0.5*size)
.translateZ(0.5*size)
.smooth(true)
.stroke(Color.BLACK)
.build(),
RectangleBuilder.create() // bottom face
.width(size).height(size)
.fill(bottom.deriveColor(0.0, 1.0, (1 - 0.4*shade), 1.0))
.translateX(-0.5*size)
.translateY(0)
.rotationAxis(Rotate.X_AXIS)
.rotate(90)
.smooth(true)
.stroke(Color.BLACK)
.build(),
RectangleBuilder.create() // right face
.width(size).height(size)
.fill(right.deriveColor(0.0, 1.0, (1 - 0.3*shade), 1.0))
.translateX(-1*size)
.translateY(-0.5*size)
.rotationAxis(Rotate.Y_AXIS)
.rotate(90)
.smooth(true)
.stroke(Color.BLACK)
.build(),
RectangleBuilder.create() // left face
.width(size).height(size)
.fill(left.deriveColor(0.0, 1.0, (1 - 0.2*shade), 1.0))
.translateX(0)
.translateY(-0.5*size)
.rotationAxis(Rotate.Y_AXIS)
.rotate(90)
.smooth(true)
.stroke(Color.BLACK)
.build(),
RectangleBuilder.create() // top face
.width(size).height(size)
.fill(top.deriveColor(0.0, 1.0, (1 - 0.1*shade), 1.0))
.translateX(-0.5*size)
.translateY(-1*size)
.rotationAxis(Rotate.X_AXIS)
.rotate(90)
.smooth(true)
.stroke(Color.BLACK)
.build(),
RectangleBuilder.create() // front face
.width(size).height(size)
.fill(front)
.translateX(-0.5*size)
.translateY(-0.5*size)
.translateZ(-0.5*size)
.smooth(true)
.stroke(Color.BLACK)
.build()
);
}
}
PerspectiveCamera camera = new PerspectiveCamera(true);
@Override public void start(Stage primaryStage) throws Exception {
Group root = new Group();
Scene scene=new Scene(root,600,600,true);
camera.setNearClip(0.00001);
camera.setFarClip(10000000.0);
camera.getTransforms().addAll (
new Rotate(0, Rotate.Y_AXIS),
new Rotate(0, Rotate.X_AXIS),
new Translate(0, 0, -1000));
scene.setCamera(camera);
Cube c1 = new Cube(50,Color.BLUE.darker(),Color.BLUE.darker(),Color.ORANGE.darker(),Color.BLUE.darker(),Color.BLUE.darker(),Color.RED.darker(),1);
c1.setTranslateX(100);
Cube c2 = new Cube(50,Color.GREEN.darker(),Color.GREEN.darker(),Color.GREEN.darker(),Color.YELLOW.darker(),Color.BLUE.darker(),Color.RED.darker(),1);
c2.setTranslateX(50);
Cube c3 = new Cube(50,Color.CYAN.brighter(),Color.GREEN.darker(),Color.GREEN.darker(),Color.YELLOW.darker(),Color.BLUE.darker(),Color.RED.darker(),1);
c3.setTranslateX(50);
c3.setTranslateZ(50);
Cube c4 = new Cube(50,Color.CYAN.brighter(),Color.GREEN.darker(),Color.ORANGE.darker(),Color.YELLOW.darker(),Color.BLUE.darker(),Color.RED.darker(),1);
c4.setTranslateX(100);
c4.setTranslateZ(50);
Cube c5 = new Cube(50,Color.BLUE.darker(),Color.GREEN.darker(),Color.ORANGE.darker(),Color.BLUE.darker(),Color.BLUE.darker(),Color.RED.darker(),1);
c5.setTranslateX(100);
c5.setTranslateY(50);
Cube c6 = new Cube(50,Color.GREEN.darker(),Color.GREEN.darker(),Color.GREEN.darker(),Color.YELLOW.darker(),Color.BLUE.darker(),Color.RED.darker(),1);
c6.setTranslateX(50);
c6.setTranslateY(50);
Cube c7 = new Cube(50,Color.CYAN.brighter(),Color.GREEN.darker(),Color.GREEN.darker(),Color.YELLOW.darker(),Color.BLUE.darker(),Color.RED.darker(),1);
c7.setTranslateX(50);
c7.setTranslateZ(50);
c7.setTranslateY(50);
Cube c8 = new Cube(50,Color.CYAN.brighter(),Color.GREEN.darker(),Color.ORANGE.darker(),Color.YELLOW.darker(),Color.BLUE.darker(),Color.RED.darker(),1);
c8.setTranslateX(100);
c8.setTranslateZ(50);
c8.setTranslateY(50);
handleMouse(scene,root);
Group k=new Group(c1,c2,c3,c4,c5,c6,c7,c8);
k.setTranslateZ(70);
root.getChildren().addAll(k);
primaryStage.setScene(scene);
primaryStage.show();
}
public static void main(String[] args) { launch(args); }
private static final double CONTROL_MULTIPLIER = 0.1;
private static final double SHIFT_MULTIPLIER = 10.0;
private static final double MOUSE_SPEED = 0.1;
private static final double ROTATION_SPEED = 2.0;
double mousePosX,mousePosY,mouseOldX,mouseOldY,mouseDeltaX,mouseDeltaY;
private void handleMouse(Scene scene, final Node root) {
scene.setOnMousePressed(new EventHandler<MouseEvent>() {
@Override public void handle(MouseEvent me) {
mousePosX = me.getSceneX();
mousePosY = me.getSceneY();
mouseOldX = me.getSceneX();
mouseOldY = me.getSceneY();
}
});
scene.setOnMouseDragged(new EventHandler<MouseEvent>() {
@Override public void handle(MouseEvent me) {
mouseOldX = mousePosX;
mouseOldY = mousePosY;
mousePosX = me.getSceneX();
mousePosY = me.getSceneY();
mouseDeltaX = (mousePosX - mouseOldX);
mouseDeltaY = (mousePosY - mouseOldY);
double modifier = 1.0;
if (me.isControlDown()) {
modifier = CONTROL_MULTIPLIER;
}
if (me.isShiftDown()) {
modifier = SHIFT_MULTIPLIER;
}
if (me.isPrimaryButtonDown()) {
camera.setRotationAxis(Rotate.Y_AXIS);camera.setRotate(camera.getRotate() -
mouseDeltaX*modifier*ROTATION_SPEED); //
camera.setRotationAxis(Rotate.X_AXIS);camera.setRotate(camera.getRotate() +
mouseDeltaY*modifier*ROTATION_SPEED); // -
}
else if (me.isSecondaryButtonDown()) {
double z = camera.getTranslateZ();
double newZ = z + mouseDeltaX*MOUSE_SPEED*modifier;
camera.setTranslateZ(newZ);
}
}
}); // setOnMouseDragged
} //handleMouse
}
2 ответа
РЕДАКТИРОВАТЬ:
Причина появления артефактов рендеринга, которая была изначально приведена здесь, была неправильной, и предлагаемое решение может не подходить *. Подробности можно найти в истории изменений. Фактическое решение намного проще. Извиняюсь за любые неудобства.
Причиной появления артефактов рендеринга является то, что плоскости отсечения камеры слишком далеко друг от друга. Вы устанавливаете
camera.setNearClip(0.00001);
camera.setFarClip(10000000.0);
что далеко за пределами того, что может быть разумно представлено в нормальном Z-буфере. Изменение этих строк на
camera.setNearClip(0.1);
camera.setFarClip(10000.0);
исправит ошибки рендеринга.
* Оригинальное решение предложило смоделировать коробки из нескольких Mesh
экземпляров. Это имеет то преимущество, что позволяет определять нормали и, таким образом, достигать "реалистичного" эффекта 3D, но требует немного больше усилий. Смотрите историю изменений для "настоящего 3D" решения.
Хотя @Marco13 - отличный и правильный ответ, если вы не хотите импортировать модель, как упоминает @jewelsea, есть также способ создать одну отдельную сетку для каждого куба и раскрасить грани, как это требуется для кубика Рубика.
Это может быть достигнуто путем использования чистого изображения для окраски граней сетки:
Если вы примените это изображение к Box
:
Box cube = new Box();
PhongMaterial material = new PhongMaterial();
material.setDiffuseMap(new Image(getClass().getResource("cubeNet.png").toExternalForm()));
cube.setMaterial(material);
Вы получите то же изображение, повторенное для шести лиц.
Таким образом, вы можете создать свой собственный блок для правильного отображения координат текстуры или использовать CuboidMesh
из библиотеки FXyz.
CuboidMesh cube = new CuboidMesh();
cube.setTextureModeImage(getClass().getResource("cubeNet.png").toExternalForm());
В этом посте показано, как можно окрасить разные грани одной сетки.
РЕДАКТИРОВАТЬ
Учитывая, что для любого из 27 кубов должно быть предоставлено другое изображение, лучшим подходом будет просто использование изображения, подобного этому:
и затем измените индексы текстуры соответственно, как объяснено в этом ответе.
В основном для каждого цвета:
public static final int RED = 0;
public static final int GREEN = 1;
public static final int BLUE = 2;
public static final int YELLOW = 3;
public static final int ORANGE = 4;
public static final int WHITE = 5;
public static final int GRAY = 6;
его нормализованная координата текстуры x будет:
public static final float X_RED = 0.5f / 7f;
public static final float X_GREEN = 1.5f / 7f;
public static final float X_BLUE = 2.5f / 7f;
public static final float X_YELLOW = 3.5f / 7f;
public static final float X_ORANGE = 4.5f / 7f;
public static final float X_WHITE = 5.5f / 7f;
public static final float X_GRAY = 6.5f / 7f;
Таким образом, используя TriangleMesh
создать коробку:
private TriangleMesh createCube(int[] face) {
TriangleMesh m = new TriangleMesh();
m.getPoints().addAll(
0.5f, 0.5f, 0.5f,
0.5f, -0.5f, 0.5f,
0.5f, 0.5f, -0.5f,
0.5f, -0.5f, -0.5f,
-0.5f, 0.5f, 0.5f,
-0.5f, -0.5f, 0.5f,
-0.5f, 0.5f, -0.5f,
-0.5f, -0.5f, -0.5f
);
m.getTexCoords().addAll(
X_RED, 0.5f,
X_GREEN, 0.5f,
X_BLUE, 0.5f,
X_YELLOW, 0.5f,
X_ORANGE, 0.5f,
X_WHITE, 0.5f,
X_GRAY, 0.5f
);
Наконец, нам просто нужно добавить грани: список вершин и индексы текстуры. Давайте создадим массив индексов, упорядоченных на основе общих обозначений граней на кубике Рубика: F - R - U - B - L - D:
m.getFaces().addAll(
2, face[0], 3, face[0], 6, face[0], // F
3, face[0], 7, face[0], 6, face[0],
0, face[1], 1, face[1], 2, face[1], // R
2, face[1], 1, face[1], 3, face[1],
1, face[2], 5, face[2], 3, face[2], // U
5, face[2], 7, face[2], 3, face[2],
0, face[3], 4, face[3], 1, face[3], // B
4, face[3], 5, face[3], 1, face[3],
4, face[4], 6, face[4], 5, face[4], // L
6, face[4], 7, face[4], 5, face[4],
0, face[5], 2, face[5], 4, face[5], // D
2, face[5], 6, face[5], 4, face[5]
);
return m;
}
Теперь очень просто создать куб и его 27 кубов, основываясь на одном кубе и шаблоне цветов.
Этот код
int[] p = new int[]{BLUE, GRAY, GRAY, GRAY, ORANGE, WHITE};
MeshView meshP = new MeshView();
meshP.setMesh(createCube(p));
PhongMaterial mat = new PhongMaterial();
mat.setDiffuseMap(new Image(getClass().getResourceAsStream("palette.png")));
meshP.setMaterial(mat);
создаст кубика для переднего правого положения.
Определив 27 позиций, это будет кубик Рубика:
Код, необходимый для его создания, можно найти здесь.