I have a pretty specific problem with javaFx's ColorAdjust effect, I'm trying to apply a grayscale filter on an image, I'm using a ColorAdjust effect and setting the saturation
Here is a reproducible example of what I'm trying to do
public class App extends Application {
#Override
public void start(Stage ps) {
Pane root = new Pane();
root.setMinSize(300, 300);
root.setStyle("-fx-background-color: #40444b;");
ImageView view = new ImageView(new Image("https://res.cloudinary.com/mesa-clone/image/upload/v1642936429/1f914_tydc44.png"));
view.setTranslateX(5);
view.setTranslateY(5);
view.setEffect(new ColorAdjust(0, -1, 0, 0));
root.getChildren().add(view);
ps.setScene(new Scene(root));
ps.show();
}
}
now this piece of code does exactly what it's supposed to do, but I'm not satisfied with the result, I want a grayscale filter that behaves similarly to the web css grayscale filter, which produces much better results for my use case :
<html>
<body style="background-color: #40444b;">
<img src="https://res.cloudinary.com/mesa-clone/image/upload/v1642936429/1f914_tydc44.png" style="filter: grayscale(100);">
</body>
</html>
[ Left is javafx, Right is Web (firefox) ]
I know the difference isn't a lot but it's crucial for my use case and I would appreciate if anyone has better ideas to get similar results to the web version of the grayscale filter
IMO, the update you provided with the PixelWriter solution is probably the best you can do, and you should edit the question to remove the update and place the update as an answer.
I realize that the process in the update is slightly more involved than using a ColorAdjust effect.
But I don't think you can do what you want using ColorAdjust.
The grayscale function adjusts colors via RGB multiplication:
double gray = 0.21 * red + 0.71 * green + 0.07 * blue;
return Color.color(gray, gray, gray, opacity);
ColorAdjust adjusts via HSB.
I could be wrong, but I don't think you can provide the HSB params to ColorAdjust to perform the equivalent RGB multiplication.
Potential higher performance solutions
Only spend time on this if you have a performance issue and must have better performance.
You could create your own GrayscaleEffect using the effect pipeline that uses the hardware acceleration and that might be something to look into if you need high performance. But that process is really complicated, it is not easy to create a hardware accelerated effect, there is no documentation for it and to do so you would need to study the existing effect code in the openjfx repository and adapt that for your purposes.
If the hardware accelerated implementation is too tricky, you could use the fork join pool to do the adjustments in parallel for speed up, operating on the byte buffer directly rather than using color objects. The algorithm you need to apply for each pixel is given by the grayscale function implementation. There is a partial example in the answers to:
javafx argb to grayscale conversion
int pixel = pixelReader.getArgb(x, y);
int alpha = ((pixel >> 24) & 0xff);
int red = ((pixel >> 16) & 0xff);
int green = ((pixel >> 8) & 0xff);
int blue = (pixel & 0xff);
int grayLevel = (int) (0.2162 * red + 0.7152 * green + 0.0722 * blue);
int gray = (alpha << 24) + (grayLevel << 16) + (grayLevel << 8) + grayLevel;
grayImage.getPixelWriter().setArgb(x, y, gray);
But that doesn't use the byte buffer and it may be more efficient to work directly on the bytebuffer from the pixel reader/writer, though that is a little trickier.
I might have suggested ColorConvertOp, shown here, but it's marginally slower than PixelWriter and not especially more versatile.
As an illustration of #jewelsea's key insight, note the low contrast between the hand and face in the unadjusted image. Manipulation via ColorAdjust can alter the properties of the image as a whole. Unfortunately, it can't alter the relative contrast of image areas, as shown in your preferred result.
Nevertheless, the example will let you adjust the properties empirically in case you find an acceptable result.
import javafx.application.Application;
import javafx.beans.property.DoubleProperty;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Slider;
import javafx.scene.control.Tooltip;
import javafx.scene.effect.ColorAdjust;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
/** #see https://stackoverflow.com/q/70821638/230513 */
public class GrayApp extends Application {
private final Insets insets = new Insets(10);
private Slider createSlider(DoubleProperty dp) {
Slider s = new Slider(-1, 1, dp.get());
s.setBlockIncrement(0.1);
s.setTooltip(new Tooltip(dp.getName()));
dp.bind(s.valueProperty());
VBox.setMargin(s, insets);
return s;
}
#Override
public void start(Stage stage) {
ImageView view = new ImageView(new Image(
"https://res.cloudinary.com/mesa-clone/image/upload/v1642936429/1f914_tydc44.png"));
ColorAdjust adjust = new ColorAdjust(0, -1, 0, 0);
view.setEffect(adjust);
Slider hSlider = createSlider(adjust.hueProperty());
Slider sSlider = createSlider(adjust.saturationProperty());
Slider bSlider = createSlider(adjust.brightnessProperty());
Slider cSlider = createSlider(adjust.contrastProperty());
VBox root = new VBox();
root.setPadding(insets);
VBox.setMargin(view, insets);
root.setAlignment(Pos.CENTER);
root.setStyle("-fx-background-color: #808080;");
root.getChildren().addAll(view, hSlider, sSlider, bSlider, cSlider);
stage.setScene(new Scene(root));
stage.show();
}
}
manually converting the image to grayscale using a WritableImage and Color.grayscale() gives better results but it would complicate the process of switching between color and grayscale :
public class App extends Application {
#Override
public void start(Stage ps) {
Pane root = new Pane();
root.setMinSize(300, 300);
root.setStyle("-fx-background-color: #40444b;");
Image image = new Image("https://res.cloudinary.com/mesa-clone/image/upload/v1642936429/1f914_tydc44.png");
ImageView view = new ImageView(grayScale(image));
view.setTranslateX(5);
view.setTranslateY(5);
root.getChildren().add(view);
ps.setScene(new Scene(root));
ps.setTitle("javafx grayscale test");
ps.show();
}
private static Image grayScale(Image img) {
WritableImage res = new WritableImage((int) img.getWidth(), (int) img.getHeight());
PixelReader pr = img.getPixelReader();
PixelWriter pw = res.getPixelWriter();
for (int y = 0; y < img.getHeight(); y++) {
for (int x = 0; x < img.getWidth(); x++) {
pw.setColor(x, y, pr.getColor(x, y).grayscale());
}
}
return res;
}
}
you have the choice of saving the filtered image or generating it every time, depending on whether that trade-off (increased memory usage for increased performance) is worthwhile.
Related
Creating new components in JavaFX is still a but muddy to me compared to "Everything is a JPanel" in Swing.
I'm trying to make a fixed size component. I hesitate to call it a control, it's a pane of activity, not a button.
But here's my problem.
The fixed size I want is smaller than the contents of the element.
The grid is, in truth, 200x200. I'm shifting it up and left 25x25, and I'm trying to make the fixed size of 150x150. You can see in my example I've tried assorted ways of forcing it to 150, but in my tests, the size never sticks. Also, to be clear, I would expect the lines to clip at the boundary of the component.
This is, roughly, what I'm shooting for in my contrived case (note this looks bigger than 150x150 because of the retina display on my Mac, which doubles everything):
I've put some in to a FlowPane, and they stack right up, but ignore the 150x150 dimensions.
FlowPane fp = new FlowPane(new TestPane(), new TestPane(), new TestPane());
var scene = new Scene(fp, 640, 480);
stage.setScene(scene);
I tried sticking one in a ScrollPane, and the scroll bars never appear, even after resizing the window.
TestPane pane = new TestPane();
ScrollPane sp = new ScrollPane(pane);
var scene = new Scene(sp, 640, 480);
stage.setScene(scene);
And I struggle to discern whether I should be extending Region or Control in these cases.
I am missing something fundamental.
package pkg;
import javafx.collections.ObservableList;
import javafx.scene.Node;
import javafx.scene.control.Control;
import javafx.scene.shape.Line;
import javafx.scene.transform.Translate;
public class TestPane extends Control {
public TestPane() {
setMinHeight(150);
setMaxHeight(150);
setMinWidth(150);
setMaxWidth(150);
setPrefHeight(150);
setPrefWidth(150);
populate();
}
#Override
protected double computePrefHeight(double width) {
return 150;
}
#Override
protected double computePrefWidth(double height) {
return 150;
}
#Override
protected double computeMaxHeight(double width) {
return 150;
}
#Override
protected double computeMaxWidth(double height) {
return 150;
}
#Override
protected double computeMinHeight(double width) {
return 150;
}
#Override
protected double computeMinWidth(double height) {
return 150;
}
#Override
public boolean isResizable() {
return false;
}
private void populate() {
Translate translate = new Translate();
translate.setX(-25);
translate.setY(-25);
getTransforms().clear();
getTransforms().addAll(translate);
ObservableList<Node> children = getChildren();
for (int i = 0; i < 4; i++) {
Line line = new Line(0, i * 50, 200, i * 50);
children.add(line);
line = new Line(i * 50, 0, i * 50, 200);
children.add(line);
}
}
}
Addenda, to clarify.
I want a fixed sized component. It's a rectangle. I want it X x Y big.
I want to draw things in my box. Lines, circles, text.
I want the things I draw to clip to the boundaries of the component.
I don't want to use Canvas.
More addenda.
What I'm looking for is not much different from what a ScrollPane does, save I don't want any scroll bars, and I don't want the size of the outlying pane to grow or shrink.
TLDR:
Subclass Region,
make isResizable() return true to respect pref, min, and max sizes,
explicitly set a clip to avoid painting outside the local bounds.
Most of the documentation for this is in the package documentation for javafx.scene.layout
First, note the distinction between resizable and non-resizable nodes. Resizable nodes (for which isResizable() returns true) are resized by their parent during layout, and the parent will make a best-effort to respect their preferred, minimum, and maximum sizes.
Non-resizable nodes are not resized by their parent. If isResizable() returns false, then resize() is a no-op and the preferred, minimum, and maximum sizes are effectively ignored. Their sizes are computed internally and reported to the parent via its visual bounds. Ultimately, all JavaFX nodes have a peer node in the underlying graphical system, and AFAIK the only way a non-resizable node can determine its size is by directly setting the size of the peer. (I'm happy to be corrected on this.)
So unless you want to get your hands really dirty with custom peer nodes (and I don't even know if the API has mechanisms for this), I think the preferred way to create a "fixed size node" is by creating a resizable node with preferred, minimum, and maximum sizes all set to the same value. This is likely by design: as noted in a comment to your question, fixed-size nodes in layout-driven UI toolkits are generally discouraged, other than very low-level components (Text, Shape, etc).
Transformations applied to resizable nodes are generally applied after layout (i.e. they don't affect the layout bounds). Therefore using a translation to manage the internal positioning of the child nodes is not a good approach; it will have effects on the layout of the custom node in the parent which you probably don't intend.
As you note, you are not really defining a control here; it has no behavior or skin. Thus subclassing Control is not really the rigth approach. The most appropriate hook in the API is to subclass Region. Override the layoutChildren() method to position the child nodes (for Shapes and Text nodes, set their coordinates, for resizable children call resizeRelocate(...)).
Finally, to prevent the node spilling out of its intended bounds (150x150 in your example), either ensure no child nodes are positioned outside those bounds, or explicitly set the clip.
Here's a refactoring of your example:
import javafx.scene.layout.Region;
import javafx.scene.shape.Line;
import javafx.scene.shape.Rectangle;
public class TestPane extends Region {
private Line[] verticalLines ;
private Line[] horizontalLines ;
private static final int WIDTH = 150 ;
private static final int HEIGHT = 150 ;
private static final int LINE_GAP = 50 ;
public TestPane() {
populate();
}
#Override
protected double computePrefHeight(double width) {
return HEIGHT;
}
#Override
protected double computePrefWidth(double height) {
return HEIGHT;
}
#Override
protected double computeMaxHeight(double width) {
return HEIGHT;
}
#Override
protected double computeMaxWidth(double height) {
return WIDTH;
}
#Override
protected double computeMinHeight(double width) {
return WIDTH;
}
#Override
protected double computeMinWidth(double height) {
return WIDTH;
}
#Override
public boolean isResizable() {
return true;
}
#Override
public void layoutChildren() {
double w = getWidth();
double h = getHeight() ;
double actualWidth = verticalLines.length * LINE_GAP ;
double actualHeight = horizontalLines.length * LINE_GAP ;
double hOffset = (actualWidth - w) / 2 ;
double vOffset = (actualHeight - h) / 2 ;
for (int i = 0 ; i < verticalLines.length ; i++) {
double x = i * LINE_GAP - hOffset;
verticalLines[i].setStartX(x);
verticalLines[i].setEndX(x);
verticalLines[i].setStartY(0);
verticalLines[i].setEndY(h);
}
for (int i = 0 ; i < horizontalLines.length ; i++) {
double y = i * LINE_GAP - vOffset;
horizontalLines[i].setStartY(y);
horizontalLines[i].setEndY(y);
horizontalLines[i].setStartX(0);
horizontalLines[i].setEndX(w);
}
setClip(new Rectangle(0, 0, w, h));
}
private void populate() {
verticalLines = new Line[4] ;
horizontalLines = new Line[4] ;
for (int i = 0 ; i <verticalLines.length ; i++) {
verticalLines[i] = new Line();
getChildren().add(verticalLines[i]);
}
for (int i = 0 ; i <horizontalLines.length ; i++) {
horizontalLines[i] = new Line();
getChildren().add(horizontalLines[i]);
}
}
}
A more sophisticated example might have, for example, LINE_GAP as a property. When that property changes you would call requestLayout() to mark the component as "dirty", so its layoutChildren() method would be called again on the next frame rendered.
Here's a quick test case:
import javafx.application.Application;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.layout.FlowPane;
import javafx.stage.Stage;
public class App extends Application {
#Override
public void start(Stage stage) {
FlowPane root = new FlowPane();
root.setAlignment(Pos.TOP_LEFT);
root.setPadding(new Insets(10));
root.setHgap(5);
root.setVgap(5);
for (int i = 0; i < 6 ; i++) {
root.getChildren().add(new TestPane());
}
Scene scene = new Scene(root);
stage.setScene(scene);
stage.show();
}
public static void main(String[] args) {
launch();
}
}
Which results in:
This plays nicely with the layout pane; resizing the window gives
I am developping a game, in JavaFX 3D, in which the player is running down a long road. There are items and obstacles coming his way. The point is: now the objects just appear out of nowhere if they enter my clipping distance. Therefore I want to create some kind of fog (or anything else that "hides" the initialization of the objects)
The problem is that I cannot find any example in which this is done. I am looking for an example piece of code/link to source/any other advice.
Having said it's too broad a question, and there must be many, many ways to do this, here's one possible implementation. This is 2D but could easily be adapted for a 3D app (I think). The idea is just to let a few light gray circles drift around on a white background, and apply an enormous blur to the whole thing.
You could then let your objects appear with this as a background, and fade them in from gray to their real color (or some such). The colors and velocities of the circles, and the blur radius, probably need some tuning...
import java.util.Random;
import javafx.animation.AnimationTimer;
import javafx.application.Application;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.effect.GaussianBlur;
import javafx.scene.layout.Pane;
import javafx.scene.layout.StackPane;
import javafx.scene.paint.Color;
import javafx.scene.shape.Circle;
import javafx.scene.shape.Rectangle;
import javafx.stage.Stage;
public class FogExample extends Application {
private static final int WIDTH = 600 ;
private static final int HEIGHT = 600 ;
#Override
public void start(Stage primaryStage) {
Fog fog = new Fog(WIDTH, HEIGHT);
Scene scene = new Scene(new StackPane(fog.getView()), WIDTH, HEIGHT);
primaryStage.setScene(scene);
primaryStage.show();
}
public static class Fog {
private final int width ;
private final int height ;
private final Pane fog ;
private final Random RNG = new Random();
public Fog(int width, int height) {
this.width = width ;
this.height = height ;
this.fog = new Pane();
Rectangle rect = new Rectangle(0, 0, width, height);
rect.setFill(Color.rgb(0xe0, 0xe0, 0xe0));
fog.getChildren().add(rect);
for (int i = 0; i < 50; i++) {
fog.getChildren().add(createFogElement());
}
fog.setEffect(new GaussianBlur((width + height) / 2.5));
}
private Circle createFogElement() {
Circle circle = new Circle(RNG.nextInt(width - 50) + 25, RNG.nextInt(height - 50) + 25, 15 + RNG.nextInt(50));
int shade = 0xcf + RNG.nextInt(0x20);
circle.setFill(Color.rgb(shade, shade, shade));
AnimationTimer anim = new AnimationTimer() {
double xVel = RNG.nextDouble()*40 - 20 ;
double yVel = RNG.nextDouble()*40 - 20 ;
long lastUpdate = 0 ;
#Override
public void handle(long now) {
if (lastUpdate > 0) {
double elapsedSeconds = (now - lastUpdate) / 1_000_000_000.0 ;
double x = circle.getCenterX() ;
double y = circle.getCenterY() ;
if ( x + elapsedSeconds * xVel > width || x + elapsedSeconds * xVel < 0) {
xVel = - xVel ;
}
if ( y + elapsedSeconds * yVel > height || y + elapsedSeconds * yVel < 0) {
yVel = - yVel ;
}
circle.setCenterX(x + elapsedSeconds*xVel);
circle.setCenterY(y + elapsedSeconds * yVel);
}
lastUpdate = now ;
}
};
anim.start();
return circle ;
}
public Node getView() {
return fog ;
}
}
public static void main(String[] args) {
launch(args);
}
}
Typical 3D systems use a Particle system to do this which involves a combination of transparency, alpha fluctuations, and billboard textures. Believe it or not Fog, smoke and flames... particle effects... are not actually 3D at all but 2D images that have been positioned, sized, and colored to appear in 3D. They are then animated rapidly enough so that the human viewer can't make out the nuances of the 2D images.
The problem with JavaFX 3D is that it does not support transparency "yet".(it is unofficially available). Also there is no particle type object that will let you overlay shapes and textures as James_D suggests without manually managing the positioning with respect to the camera.
However there is hope... the F(X)yz project provides a BillBoard class which will allow you to place an image perpendicular to the camera. You can rapidly update this image using a Timer and some Random.next() type Circle creations for the fog using the approach James_D suggested. An example on using the BillBoard in a manner like this is in BillBoardBehaviourTest and CameraViewTest. Performance is demonstrated in CameraViewTest.
What I would do if I were you is setup a large frustrum wide BillBoard object at the back -Z position of your frustrum (where the objects enter the scene) and then setup a Timer/Random circle generator in a pane off screen. Then using the approach in the CameraViewTest, snapshot the off screen pane (which has the circles) and then set that image to the BillBoard. You will have to have the SnapShot call be on some timer itself to achieve an effect of animation. The CameraViewTest demonstrates how to do that. The effect to the user will be a bunch of moving blurry tiny circles.
Fog on the cheap!
I’m making some trials with JavaFX RotateTransition, applying a simple model found on a jfx itself documentation file:
rotateTransition = RotateTransitionBuilder.create()
.node(elements)
.duration(Duration.seconds(4))
.fromAngle(0)
.toAngle(720)
.cycleCount(3)
.autoReverse(true)
.build();
Above, elements is a Group of bare Arc primitives.
When this group has a limited number of nodes, say 20, the animation goes smooth but when I increase the number of nodes to 500 (nested actually, Group of Group) the animation still works but does not result any more fluid.
The question is: does this nodes limit can be considered too much for this task? How to speed up the rendering?
I have found the thread below that in a similar context asserts that could be a matter of using the right Animation class, but I’m not sure that the proposed AnimationTimer does apply well to a rotation.
http://mail.openjdk.java.net/pipermail/openjfx-dev/2013-June/008104.html
I have also tried to use setCache(true) to every node with no visible improvements.
Thank you!
Edit: Arc generation. No strange things but a binder and a EventHandler.
Arc arc = new Arc();
arc.centerXProperty().bind(plotRadiusBinding);
arc.centerYProperty().bind(plotRadiusBinding);
arc.radiusXProperty().bind(plotRadiusBinding);
arc.radiusYProperty().bind(plotRadiusBinding);
arc.setStartAngle(startAngle * 180 / PI);
arc.setLength(radiansLength * 180 / PI);
arc.setType(ArcType.ROUND);
arc.setStroke(defaultArcColor);
arc.setStrokeType(StrokeType.INSIDE);
arc.setFill(null);
arc.setOnMouseClicked(arcEventHandler);
I had similar issues when moving images around when I used KeyFrames (KeyFrames)
I could improve it by using
I had success with implementing the stuff by myself:
Animation animation = new Transition() {
{
setCycleDuration(Duration.millis(1000));
}
#Override
protected void interpolate(double frac) {
// your rotation code here
}
}
If you could provide your arc generation source code it might be helpful.
for me this works flawlessly for 500 arcs:
package application;
import java.util.Random;
import javafx.animation.RotateTransition;
import javafx.application.Application;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.scene.layout.Pane;
import javafx.scene.shape.Arc;
import javafx.stage.Stage;
import javafx.util.Duration;
public class Main extends Application {
#Override
public void start(Stage primaryStage) {
try {
Pane root = new Pane();
Group group = new Group();
Random rand = new Random();
for(int i=0; i<1000; i++){
Arc c = new Arc(200 + rand.nextInt(400), 200 + rand.nextInt(400), 10, 10, 0, 360);
group.getChildren().add(c);
}
root.getChildren().add(group);
RotateTransition rotateTransition = new RotateTransition(Duration.millis(5000), group);
rotateTransition.setFromAngle(0);
rotateTransition.setToAngle(720);
rotateTransition.setCycleCount(3);
rotateTransition.setAutoReverse(true);
rotateTransition.play();
Scene scene = new Scene(root,800,800);
scene.getStylesheets().add(getClass().getResource("application.css").toExternalForm());
primaryStage.setScene(scene);
primaryStage.show();
} catch(Exception e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
launch(args);
}
}
if this is still to laggy and you only want to draw arcs you could directly invoke the graphics-context of a canvas.
When JavaFX8 code loads the color, bump and spec maps, the color and spec work as expected, but bump map is causing strange effects. All three are Mercator maps of Earth. Generally, there is no 3d effect added by the bump map. Bump map only causes Himalaya and Andes appear on the lit side of the globe as black areas with shiny border and on the shaded side as they appear on the color map. What am I doing wrong?
Image diffMap = null;
Image bumpMap = null;
Image specMap = null;
diffMap = new Image(MoleculeSampleApp.class.getResource("Color Map1.jpg").toExternalForm());
bumpMap = new Image(MoleculeSampleApp.class.getResource("Bump1.jpg").toExternalForm());
specMap = new Image(MoleculeSampleApp.class.getResource("Spec Mask1.png").toExternalForm());
final PhongMaterial earthMaterial = new PhongMaterial(Color.WHITE, diffMap, specMap, bumpMap, null);
earthMaterial.setDiffuseColor(Color.WHITE);
earthMaterial.setSpecularColor(Color.WHITE);
Being new to 3d my first thought is that there should be some kind of scaling pixel color values of the bump map into elevation, which I am missing.
Bump map for JavaFX is a normal map, not a height map, for more info see: normal map and height map info.
Here is a sample you can try.
The images for the maps are pretty large, so it might take a little while to download them before your scene shows.
Source I used for images was => Bored? Then Create a Planet (now a dead link).
import javafx.animation.*;
import javafx.application.Application;
import javafx.scene.*;
import javafx.scene.image.Image;
import javafx.scene.layout.StackPane;
import javafx.scene.paint.*;
import javafx.scene.shape.Sphere;
import javafx.scene.transform.Rotate;
import javafx.stage.Stage;
import javafx.util.Duration;
public class EarthViewer extends Application {
private static final double EARTH_RADIUS = 400;
private static final double VIEWPORT_SIZE = 800;
private static final double ROTATE_SECS = 30;
private static final double MAP_WIDTH = 4096;
private static final double MAP_HEIGHT = 2048;
private static final String DIFFUSE_MAP =
"https://imgur.com/vrNnXIs.jpeg";
private static final String NORMAL_MAP =
"https://imgur.com/5T2oAuk.jpeg";
private static final String SPECULAR_MAP =
"https://imgur.com/GV11WNV.jpeg";
private Group buildScene() {
Sphere earth = new Sphere(EARTH_RADIUS);
earth.setTranslateX(VIEWPORT_SIZE / 2d);
earth.setTranslateY(VIEWPORT_SIZE / 2d);
PhongMaterial earthMaterial = new PhongMaterial();
earthMaterial.setDiffuseMap(
new Image(
DIFFUSE_MAP,
MAP_WIDTH,
MAP_HEIGHT,
true,
true
)
);
earthMaterial.setBumpMap(
new Image(
NORMAL_MAP,
MAP_WIDTH,
MAP_HEIGHT,
true,
true
)
);
earthMaterial.setSpecularMap(
new Image(
SPECULAR_MAP,
MAP_WIDTH,
MAP_HEIGHT,
true,
true
)
);
earth.setMaterial(
earthMaterial
);
return new Group(earth);
}
#Override
public void start(Stage stage) {
Group group = buildScene();
Scene scene = new Scene(
new StackPane(group),
VIEWPORT_SIZE, VIEWPORT_SIZE,
true,
SceneAntialiasing.BALANCED
);
scene.setFill(Color.rgb(10, 10, 40));
scene.setCamera(new PerspectiveCamera());
stage.setScene(scene);
stage.show();
stage.setFullScreen(true);
rotateAroundYAxis(group).play();
}
private RotateTransition rotateAroundYAxis(Node node) {
RotateTransition rotate = new RotateTransition(
Duration.seconds(ROTATE_SECS),
node
);
rotate.setAxis(Rotate.Y_AXIS);
rotate.setFromAngle(360);
rotate.setToAngle(0);
rotate.setInterpolator(Interpolator.LINEAR);
rotate.setCycleCount(RotateTransition.INDEFINITE);
return rotate;
}
public static void main(String[] args) {
launch(args);
}
}
Normal? Why????
The JavaDoc states for the PhongMaterial bumpMapProperty states:
The bump map of this PhongMaterial, which is a normal map stored as a RGB Image.
A normal map is used rather than a height map because:
[normal maps] are much more accurate, as rather than only simulating the pixel
being away from the face along a line, they can simulate that pixel
being moved at any direction, in an arbitrary way.
A brief description of both normal mapping and height mapping is provided in the wikipedia bump mapping article.
Sample Images
Update, July 2021
Unfortunately the image source from "Bored? Then Create a Planet" is no longer available, so I updated the answer to link to different images (hopefully those will remain online). Because it is linked to different images, the resultant rendering of earth looks a bit different than the example image above, though it is similar. The code to render is basically no different, though the images changed.
Diffuse map
Normal map
Specular map
I'm writing some javaFX code and I have added some Paths to a Group, this Group has been added to another Group which has been added to the root and is displaying on the screen. Now at a certain point I want to use a PathTransition to animate this lowest level group to a new location. I'm having trouble working out the correct coordinates for the transition. I have read that the PathTransition will animate the node from its center and not the upper left hand corner so I have tried adding
Group.getLayoutX()/2 to the starting x and Group.getLayoutY()/2 to the starting y coord but it still seems to make the group jump to a new starting location before the animation begins.
The final destination seems a bit off as well.
Is there a better way to animate a Group that contains several Paths?
Adjust the layoutX and layoutY of the nodes following paths based upon half the layout bounds of the nodes being animated.
rect.setLayoutX(rect.getLayoutX() + rect.getLayoutBounds().getWidth() / 2);
rect.setLayoutY(rect.getLayoutY() + rect.getLayoutBounds().getHeight() / 2);
Try the modification of Uluk's circle path transition code from JavaFX 2 circle path for animation.
The code will have the upper left corner of the nodes original layout follow the path.
import javafx.animation.PathTransition.OrientationType;
import javafx.animation.*;
import javafx.application.Application;
import javafx.scene.*;
import javafx.scene.paint.Color;
import javafx.scene.shape.*;
import javafx.stage.Stage;
import javafx.util.Duration;
public class ArcToDemo extends Application {
private PathTransition pathTransitionEllipse;
private PathTransition pathTransitionCircle;
private void init(Stage primaryStage) {
Group root = new Group();
primaryStage.setResizable(false);
primaryStage.setScene(new Scene(root, 600, 460));
// Ellipse path example
Rectangle rect = new Rectangle(0, 0, 40, 40);
rect.setArcHeight(10);
rect.setArcWidth(10);
rect.setFill(Color.ORANGE);
root.getChildren().add(rect);
Path path = createEllipsePath(200, 200, 50, 100, 45);
root.getChildren().add(path);
pathTransitionEllipse = PathTransitionBuilder.create()
.duration(Duration.seconds(4))
.path(path)
.node(rect)
.orientation(OrientationType.NONE)
.cycleCount(Timeline.INDEFINITE)
.autoReverse(false)
.build();
rect.setLayoutX(rect.getLayoutX() + rect.getLayoutBounds().getWidth() / 2);
rect.setLayoutY(rect.getLayoutY() + rect.getLayoutBounds().getHeight() / 2);
// Circle path example
Rectangle rect2 = new Rectangle(0, 0, 20, 20);
rect2.setArcHeight(10);
rect2.setArcWidth(10);
rect2.setFill(Color.GREEN);
root.getChildren().add(rect2);
Path path2 = createEllipsePath(400, 200, 150, 150, 0);
root.getChildren().add(path2);
pathTransitionCircle = PathTransitionBuilder.create()
.duration(Duration.seconds(2))
.path(path2)
.node(rect2)
.orientation(OrientationType.NONE)
.cycleCount(Timeline.INDEFINITE)
.autoReverse(false)
.build();
rect2.setLayoutX(rect2.getLayoutX() + rect2.getLayoutBounds().getWidth() / 2);
rect2.setLayoutY(rect2.getLayoutY() + rect2.getLayoutBounds().getHeight() / 2);
}
private Path createEllipsePath(double centerX, double centerY, double radiusX, double radiusY, double rotate) {
ArcTo arcTo = new ArcTo();
arcTo.setX(centerX - radiusX + 1); // to simulate a full 360 degree celcius circle.
arcTo.setY(centerY - radiusY);
arcTo.setSweepFlag(false);
arcTo.setLargeArcFlag(true);
arcTo.setRadiusX(radiusX);
arcTo.setRadiusY(radiusY);
arcTo.setXAxisRotation(rotate);
Path path = PathBuilder.create()
.elements(
new MoveTo(centerX - radiusX, centerY - radiusY),
arcTo,
new ClosePath()) // close 1 px gap.
.build();
path.setStroke(Color.DODGERBLUE);
path.getStrokeDashArray().setAll(5d, 5d);
return path;
}
#Override
public void start(Stage primaryStage) throws Exception {
init(primaryStage);
primaryStage.show();
pathTransitionEllipse.play();
pathTransitionCircle.play();
}
public static void main(String[] args) {
launch(args);
}
}
Implementation Notes
The code assumes that the original layout position of the nodes being animated is 0,0 (if it is not then you will need to adjust accordingly).
For non-square nodes, there is a slight gap between the node and the path (because the calculations are based on the layout bounds rectangle and not the visible shape of the node).
Also the path transition is using an orientation of NONE rather than ORTHOGONAL_TO_TANGENT (otherwise the layout calculations give a strange effect as the node starts to rotate and it seems as though the node is traveling divergent to the path).
Instead of the above approach, you could define a custom subclass of Transition which modifies the TranslateX and TranslateY properties of the node so that the corner of the node follows the path rather than the center and no modifications to the node's layout are required, however that would be significantly more work.