Pane cutting off when using light effects and perspective camera - javafx

I have encountered yet another problem with javaFX light effects. When using perspective camera to follow player in 2D world, everything works fine, until I add light effects!
The CYAN color represents the background.. When light effects are ON, the level pane does not render at the edges of the scene and the background can be seen instead. This makes the bottom and right side of my levels invisible and therefore unplayable. In the full screen mode it is even worse!
here is my demonstration code:
import javafx.application.Application;
import javafx.scene.PerspectiveCamera;
import javafx.scene.Scene;
import javafx.scene.effect.Light;
import javafx.scene.effect.Lighting;
import javafx.scene.layout.Pane;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;
import javafx.stage.Stage;
public class dd extends Application {
PerspectiveCamera cam = new PerspectiveCamera(false);
public void start(Stage alku) throws Exception {
Pane testpane = new Pane();
Rectangle background = new Rectangle(500, 500);
background.setFill(Color.CYAN);
Rectangle rec = new Rectangle(500, 300);
Lighting lig = new Lighting();
Light.Point l = new Light.Point();
l.setX(200);
l.setY(150);
l.setZ(20);
l.setColor(Color.WHITE);
lig.setLight(l);
rec.setFill(Color.RED);
testpane.getChildren().addAll(background, rec);
Scene scene = new Scene(testpane, 300, 300);
cam.setTranslateX(30);
cam.setTranslateY(30);
alku.setTitle("TEST");
alku.setScene(scene);
rec.setEffect(lig);
scene.setCamera(cam);
alku.show();
}
public static void main(String[] args) {
launch(args);
}
}
If you remove the camera or the light, the "rec" renders normally again. Just adjust the window size to see the problem better.
So the main question I have:
Is this a bug? If not, how can this rendering issue be solved without sacrificing lights and camera?

Related

Why strange painting behavior in JavaFX

I have a simple FX example with a simple component.
package fxtest;
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.StackPane;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;
import javafx.stage.Stage;
public class App extends Application {
#Override
public void start(Stage stage) {
var bp = new BorderPane();
var r = new Rectangle(0, 0, 200, 200);
r.setFill(Color.GREEN);
var sp = new StackPane(r);
bp.setCenter(sp);
bp.setTop(new XPane());
bp.setBottom(new XPane());
bp.setLeft(new XPane());
bp.setRight(new XPane());
var scene = new Scene(bp, 640, 480);
stage.setScene(scene);
stage.show();
}
public static void main(String[] args) {
launch();
}
}
package fxtest;
import javafx.collections.ObservableList;
import javafx.scene.Node;
import javafx.scene.layout.Region;
import javafx.scene.paint.Color;
import javafx.scene.shape.Line;
import javafx.scene.shape.Rectangle;
public class XPane extends Region {
public XPane() {
setMaxSize(Double.MAX_VALUE, Double.MAX_VALUE);
setMinSize(100, 100);
setPrefSize(100, 100);
widthProperty().addListener((o) -> {
populate();
});
heightProperty().addListener((o) -> {
populate();
});
populate();
}
private void populate() {
ObservableList<Node> children = getChildren();
Rectangle r = new Rectangle(getWidth(), getHeight());
r.setFill(Color.WHITE);
r.setStroke(Color.BLACK);
children.add(r);
Line line = new Line(0, 0, getWidth(), getHeight());
children.add(line);
line = new Line(0, getHeight(), getWidth(), 0);
children.add(line);
}
}
When run, it does what I expect:
When I grow the window, the X's grow.
But when I shrink the window, I get artifacts of the side panels.
I would have thought erasing the backgrounds would have fixed this, but I guess there's some ordering issue. But even still, when you drag the corner, all of the XPanes change size, and they all get repainted, but the artifacts remain.
I tried wrapping the XPanes in to a StackPane, but that didn't do anything (I didn't think it would, but tried it anyway).
How do I remedy this? This is JavaFX 13 on JDK 16 on macOS Big Sur.
Why you get artifacts
I think a different approach should be used rather than fixing the approach you have, but you could fix it if you want.
You are adding new rectangles and lines to your XPane in listeners. Every time the height or width changes, you add a new set of nodes, but the old set of nodes at the old height and widths remains. Eventually, if you resize enough, performance will drop or you will run out of memory or resources, making the program unusable.
A BorderPane paints its children (the center and the XPanes) in the order they were added without clipping, so these old lines will remain and the renderer will paint them over some panes as you resize. Similarly, some panes will paint over some lines because you are building up potentially lots of filled rectangles in the panes and they are partially overlapping lots of lines created.
To fix this, clear() the child node list in your populate() method before you add any new nodes.
private void populate() {
ObservableList<Node> children = getChildren();
children.clear();
// now you can add new nodes...
}
Alternate Solution
Change listeners on widths and heights aren't really the place to add content to a custom region, IMO.
I think that it is best to take advantage of the scene graph and let it handle the repainting and updating of existing nodes after you change the attributes of those nodes, instead of creating new nodes all the time.
Here is an example that subclasses Region and paints fine when a resize occurs.
import javafx.scene.layout.Region;
import javafx.scene.paint.Color;
import javafx.scene.shape.Line;
import javafx.scene.shape.Rectangle;
public class XPane extends Region {
public XPane() {
super();
Rectangle border = new Rectangle();
Line topLeftToBottomRight = new Line();
Line bottomLeftToTopRight = new Line();
getChildren().addAll(
border,
topLeftToBottomRight,
bottomLeftToTopRight
);
border.setStroke(Color.BLACK);
border.setFill(Color.WHITE);
border.widthProperty().bind(
widthProperty()
);
border.heightProperty().bind(
heightProperty()
);
topLeftToBottomRight.endXProperty().bind(
widthProperty()
);
topLeftToBottomRight.endYProperty().bind(
heightProperty()
);
bottomLeftToTopRight.startYProperty().bind(
heightProperty()
);
bottomLeftToTopRight.endXProperty().bind(
widthProperty()
);
setMinSize(100, 100);
setPrefSize(100, 100);
}
}
On Region vs Pane
I'm not sure if you should be subclassing Pane or Region, the main difference between the two is that a Pane has a public accessor for a modifiable child list, but a Region does not. So it would depend on what you are trying to do. If it is just drawing X's like the example, then Region is appropriate.
On layoutChildren() vs binding
The Region documentation states:
By default a Region inherits the layout behavior of its superclass,
Parent, which means that it will resize any resizable child nodes to
their preferred size, but will not reposition them. If an application
needs more specific layout behavior, then it should use one of the
Region subclasses: StackPane, HBox, VBox, TilePane, FlowPane,
BorderPane, GridPane, or AnchorPane.
To implement a more custom layout, a Region subclass must override
computePrefWidth, computePrefHeight, and layoutChildren. Note that
layoutChildren is called automatically by the scene graph while
executing a top-down layout pass and it should not be invoked directly
by the region subclass.
Region subclasses which layout their children will position nodes by
setting layoutX/layoutY and do not alter translateX/translateY, which
are reserved for adjustments and animation.
I am not actually doing that here, instead, I am binding in the constructor rather than overriding layoutChildren(). You could implement an alternate solution that operates as the documentation discusses, overriding layoutChildren() rather than using binding, but it is more complicated and less well documented on how to do that.
It is uncommon to subclass Region and override layoutChildren(). Instead, usually, a combination of standard layout Panes will be used and constraints set on the panes and nodes to get the desired layout. This lets the layout engine do a lot of the work such as snapping to pixels, calculating margins and insets, respecting constraints, repositioning content, etc, a lot of which would need to be done manually for a layoutChildren() implementation.
One common approach is to bind the relevant geometric properties to the desired properties of the enclosing container. A related example is examined here, and others are collected here.
The variation below binds the vertices of several Shape instances to the Pane width and height properties. Resize the enclosing stage to see how the BorderPane children conform to entries in the BorderPane Resize Table. The example also adds a red Circle, which stays centered in each child, growing and shrinking in the center to fill the smaller of the width or height. The approach relies on the fluent arithmetic API available to properties that implement NumberExpression or methods defined in Bindings.
c.centerXProperty().bind(widthProperty().divide(2));
c.centerYProperty().bind(heightProperty().divide(2));
NumberBinding diameter = Bindings.min(widthProperty(), heightProperty());
c.radiusProperty().bind(diameter.divide(2));
import javafx.application.Application;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.NumberBinding;
import javafx.scene.Scene;
import javafx.scene.layout.BorderPane;
import javafx.stage.Stage;
import javafx.scene.layout.Pane;
import javafx.scene.paint.Color;
import javafx.scene.shape.Circle;
import javafx.scene.shape.Line;
import javafx.scene.shape.Rectangle;
/**
* #see https://stackoverflow.com/q/70311488/230513
*/
public class App extends Application {
#Override
public void start(Stage stage) {
var bp = new BorderPane(new XPane(), new XPane(),
new XPane(), new XPane(), new XPane());
stage.setScene(new Scene(bp, 640, 480));
stage.show();
}
private static class XPane extends Pane {
private final Rectangle r = new Rectangle();
private final Circle c = new Circle(8, Color.RED);
private final Line line1 = new Line();
private final Line line2 = new Line();
public XPane() {
setPrefSize(100, 100);
r.widthProperty().bind(this.widthProperty());
r.heightProperty().bind(this.heightProperty());
r.setFill(Color.WHITE);
r.setStroke(Color.BLACK);
getChildren().add(r);
line1.endXProperty().bind(widthProperty());
line1.endYProperty().bind(heightProperty());
getChildren().add(line1);
line2.startXProperty().bind(widthProperty());
line2.endYProperty().bind(heightProperty());
getChildren().add(line2);
c.centerXProperty().bind(widthProperty().divide(2));
c.centerYProperty().bind(heightProperty().divide(2));
NumberBinding diameter = Bindings.min(widthProperty(), heightProperty());
c.radiusProperty().bind(diameter.divide(2));
getChildren().add(c);
}
}
public static void main(String[] args) {
launch();
}
}

Canvas: hover over text output

I want to hover over a shape I created in Canvas (JavaFx) and when hovering over with the mouse I want a text output pop-up to display. Is there a built in function for this? I can't find how to do it anywhere...
Unlike with the scene graph, a Canvas has no notion of what it contains. It's nothing but a two-dimensional array of pixels and provides no further distinctions than that. If you want to know if and when the mouse hovers over a "shape" in the Canvas you'll have to keep track of where the "shape" is and do the necessary computations manually. Here's an example which shows a popup at the mouse's location only while within the drawn rectangle:
import javafx.application.Application;
import javafx.geometry.Insets;
import javafx.geometry.Rectangle2D;
import javafx.scene.Scene;
import javafx.scene.canvas.Canvas;
import javafx.scene.control.Label;
import javafx.scene.effect.DropShadow;
import javafx.scene.layout.Background;
import javafx.scene.layout.BackgroundFill;
import javafx.scene.layout.CornerRadii;
import javafx.scene.layout.Pane;
import javafx.scene.layout.StackPane;
import javafx.scene.paint.Color;
import javafx.stage.Popup;
import javafx.stage.Stage;
public class App extends Application {
#Override
public void start(Stage primaryStage) {
// used to test if mouse is within the rectangle
var bounds = new Rectangle2D(200, 100, 100, 100);
var canvas = new Canvas(500, 300);
// draw rectangle using above bounds
canvas.getGraphicsContext2D().setFill(Color.FIREBRICK);
canvas
.getGraphicsContext2D()
.fillRect(bounds.getMinX(), bounds.getMinY(), bounds.getWidth(), bounds.getHeight());
var popup = createPopup();
canvas.setOnMouseMoved(
e -> {
// test if local mouse coordinates are within rectangle
if (bounds.contains(e.getX(), e.getY())) {
// convert local coordinates to screen coordinates
var point = canvas.localToScreen(e.getX(), e.getY());
// show the popup at the mouse's location on the screen
popup.show(canvas, point.getX(), point.getY());
} else if (popup.isShowing()) {
// hide popup if showing and mouse no longer within rectangle
popup.hide();
}
});
primaryStage.setScene(new Scene(new Pane(canvas)));
primaryStage.show();
}
private Popup createPopup() {
var content = new StackPane(new Label("Hello, World!"));
content.setPadding(new Insets(10, 5, 10, 5));
content.setBackground(
new Background(new BackgroundFill(Color.WHITE, new CornerRadii(10), null)));
content.setEffect(new DropShadow());
var popup = new Popup();
popup.getContent().add(content);
return popup;
}
}
As you can see, this is relatively simple for a static image consisting of a single, rectangular shape. This can quickly become more complicated just by making the image dynamic, let alone by having to test the bounds of irregular shapes.
An easier approach would be to use the scene graph. Instead of drawing to a Canvas you would add a Rectangle to a layout. Then you can use the Node API to know when the mouse enters and exits the Rectangle (e.g. setOnMouseXXX, hover property, etc.). It also makes it easier to use something like a Tooltip, which can simply be "installed" on the Node.

Can I only target bottom border at JavaFX Styling?

can I somehow only style the bottom border of an textfield?
I already tried
textfield.setStyle("-fx-border-bottom-color: #FF0000");
but it hasn't worked.
Is there an possibility to color the bottom border??
Greetings
MatsG23
Here is a quick and dirty example of how that can be done.
import javafx.application.Application;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.TextField;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
public class TextFieldStyleTest extends Application {
#Override
public void start(Stage primaryStage) throws Exception {
BorderPane root = new BorderPane();
VBox vBox = new VBox();
vBox.setAlignment(Pos.CENTER);
root.setCenter(vBox);
HBox hBox = new HBox();
hBox.setAlignment(Pos.CENTER);
vBox.getChildren().add(hBox);
TextField textField = new TextField("Hello World");
textField.setAlignment(Pos.BASELINE_CENTER);
hBox.getChildren().add(textField);
textField.setStyle("-fx-border-color: red; -fx-border-width: 0 0 10 0;");
Scene scene = new Scene(root, 800, 600);
primaryStage.setScene(scene);
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
}
class TextFieldStyleTestLauncher {public static void main(String[] args) {TextFieldStyleTest.main(args);}}
Yes, it is possible to give each side a different color. From the JavaFX CSS Reference Guide, for Region:
CSS Property: -fx-border-color
Values: <paint> | <paint> <paint> <paint> <paint> [ , [<paint> | <paint> <paint> <paint> <paint>] ]*
Default: null
Comments: A series of paint values or sets of four paint values, separated by commas. For each item in the series, if a single paint value is specified, then that paint is used as the border for all sides of the region; and if a set of four paints is specified, they are used for the top, right, bottom, and left borders of the region, in that order. If the border is not rectangular, only the first paint value in the set is used.
Note: The above is actually from one row of a table, but Stack Overflow doesn't give a way of formatting things in a table.
Meaning you can target the bottom border only by using:
.text-field {
-fx-border-color: transparent transparent red transparent;
}
The -fx-border-width CSS property (and really all the CSS properties dealing with the Region#background and Region#border properties) behaves the same way. This means you can accomplish the same thing by setting the width of every side but the bottom to zero, just like in mipa's answer.
Here's an exaple using inline CSS (i.e. setStyle):
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.TextField;
import javafx.scene.layout.Region;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;
public class App extends Application {
#Override
public void start(Stage primaryStage) {
TextField field = new TextField("Hello, World!");
field.setStyle("-fx-border-color: transparent transparent red transparent;");
field.setMaxWidth(Region.USE_PREF_SIZE);
primaryStage.setScene(new Scene(new StackPane(field), 300, 150));
primaryStage.show();
// Remove blue outline from when TextField is focused. This
// makes it easier to see the red border.
primaryStage.getScene().getRoot().requestFocus();
}
}
Which gives the following output:
Note that most of the "borders" added by modena.css (the default user-agent style sheet in JavaFX 8+) are not actually borders. Instead, they're multiple backgrounds with different insets.

Create a draggable selection box for a sketching program in JavaFX

I'm trying to create a draggable selection box for a sketching program in JavaFX, one like this:
I'm only not sure how to do it. I initially wanted to do it like this: capture the mouse coordinates when the mouse is pressed and do it again at the end of a drag, then calculate the height and width and make a transparent button with a black border with these properties.
But, then I realized that when I do it like this, it is not possible to see the button while you are scaling the plane, unless you draw and delete a lot of buttons.
So, I wondered if there is a better way to do something like this or is my reasoning above right? Thanks
I would use a Rectangle instead of a Button. Just do what you describe, but update the size (and position) of the rectangle on mouse drag, instead of only adding it when the mouse is released.
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.layout.Pane;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;
import javafx.stage.Stage;
public class SelectionRectangle extends Application {
private double mouseDownX ;
private double mouseDownY ;
#Override
public void start(Stage primaryStage) {
Rectangle selectionRectangle = new Rectangle();
selectionRectangle.setStroke(Color.BLACK);
selectionRectangle.setFill(Color.TRANSPARENT);
selectionRectangle.getStrokeDashArray().addAll(5.0, 5.0);
Pane pane = new Pane();
pane.setMinSize(600, 600);
pane.getChildren().add(selectionRectangle);
pane.setOnMousePressed(e -> {
mouseDownX = e.getX();
mouseDownY = e.getY();
selectionRectangle.setX(mouseDownX);
selectionRectangle.setY(mouseDownY);
selectionRectangle.setWidth(0);
selectionRectangle.setHeight(0);
});
pane.setOnMouseDragged(e -> {
selectionRectangle.setX(Math.min(e.getX(), mouseDownX));
selectionRectangle.setWidth(Math.abs(e.getX() - mouseDownX));
selectionRectangle.setY(Math.min(e.getY(), mouseDownY));
selectionRectangle.setHeight(Math.abs(e.getY() - mouseDownY));
});
primaryStage.setScene(new Scene(pane));
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
}
You can use a mouse released handler to figure out what's selected, by looking at the x, y, width, and height properties of the rectangle, as needed.

Javafx imported 3d model incorrectly displayed

I am playing around with the 3d model importers from interactivemesh.org in Javafx. The import of the models in a scene works without error. However, the models are being displayed in a weird way. Some of the faces that are behind other faces are being displayed even though they should be covered by the front faces. I have tried the tdsImporter, as well as obj and the fxml importer, all encountered the same issue. The models are shown correctly in the model browser, so I guess something is wrong with my code. Here is what the model looks like (tried it on different computers):
The HST Model from interactivemesh.org
Also the source code I use for the 3ds import:
import com.interactivemesh.jfx.importer.tds.TdsModelImporter;
import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Group;
import javafx.scene.Node;
import javafx.scene.Parent;
import javafx.scene.PerspectiveCamera;
import javafx.scene.Scene;
import javafx.scene.transform.Rotate;
import javafx.scene.transform.Translate;
import javafx.stage.Stage;
public class Test3d extends Application {
Group group = new Group();
#Override
public void start(Stage meineStage) throws Exception {
Parent root = FXMLLoader.load(getClass().getResource("test.fxml"));
Scene meineScene = new Scene(root, 1280, 800);
meineStage.setTitle("Startbildschirm");
meineStage.setScene(meineScene);
meineStage.show();
PerspectiveCamera camera = new PerspectiveCamera(true);
camera.getTransforms().addAll(
new Rotate(0, Rotate.Y_AXIS),
new Rotate(-45, Rotate.X_AXIS),
new Rotate(-45, Rotate.Z_AXIS),
new Translate(0, 0, -110));
meineScene.setCamera(camera);
camera.setNearClip(0.1);
camera.setFarClip(200);
TdsModelImporter tdsImporter = new TdsModelImporter();
tdsImporter.read("hst.3ds");
Node[] tdsMesh = (Node[]) tdsImporter.getImport();
tdsImporter.close();
for (int i = 0; i < tdsMesh.length; i++) {
tdsMesh[i].setScaleX(0.1);
tdsMesh[i].setScaleY(0.1);
tdsMesh[i].setScaleZ(0.1);
tdsMesh[i].getTransforms().setAll(new Rotate(60, Rotate.Y_AXIS), new Rotate(-90, Rotate.X_AXIS));
}
Group root1 = new Group(tdsMesh);
meineScene.setRoot(root1);
}
public static void main(String[] args) {
launch(args);
}
}
Does anybody have an idea what the problem could be and how to fix it?
According to the Scene javadoc:
An application may request depth buffer support or scene anti-aliasing support at the creation of a Scene. [...] A scene containing 3D shapes or 2D shapes with 3D transforms may use depth buffer support for proper depth sorted rendering; [...] A scene with 3D shapes may enable scene anti-aliasing to improve its rendering quality.
The depthBuffer and antiAliasing flags are conditional features. With the respective default values of: false and SceneAntialiasing.DISABLED.
So in your code, try:
Scene meineScene = new Scene(root, 1280, 800, true);
or even better:
Scene meineScene = new Scene(root, 1280, 800, true, SceneAntialiasing.BALANCED);

Resources