javajavafxjavafx-3d

javafx 3d sphere partial texture


I am trying to draw a texture on a sphere with JavaFX (16). I add the material with the texture but the texture is stretched to the whole surface. It is possible to set the texture on only a portion of the surface? Like the image below (not mine, taken from SO):

enter image description here

My code so far (very trivial):

public void start(Stage stage) throws Exception {
   Sphere sphere = new Sphere(200);
   PhongMaterial material = new PhongMaterial();
   material.setDiffuseMap(new Image(new File("picture.png").toURI().toURL().toExternalForm()));
   sphere.setMaterial(material);
   Group group = new Group(sphere);
   Scene scene = new Scene( new StackPane(group), 640, 480, true, SceneAntialiasing.BALANCED);
   scene.setCamera(new PerspectiveCamera());
   stage.setScene(scene);
   stage.show();
}

Solution

  • The reason why the texture you apply is stretched to the whole sphere is that the texture coordinates that define the Sphere mesh are mapping the whole sphere surface, and therefore, when you apply a diffuse image, it is translated 1-1 to that surface.

    You could create a custom mesh, with custom texture coordinates values, but that can be more complex.

    Another option is to create the diffuse image "on demand", based on your needs.

    For a sphere, a 2D image that can be wrapped around the 3D sphere can be defined by a 2*r*PI x 2*r rectangular container (a JavaFX Pane for our purposes).

    Then, you can draw inside your images, scaling and translating them accordingly.

    Finally, you need a way to convert that drawing into an image, and for that you can use Scene::snapshot.

    Just to play around with this idea, I'll create a rectangular grid that will be wrapped around the sphere, in order to have some kind of a coordinate system.

        private Image getTexture(double r) {
            double h = 2 * r;
            double w = 2 * r * 3.125; // 3.125 is ~ PI, rounded to get perfect squares.
    
            Pane pane = new Pane();
            pane.setPrefSize(w, h);
            pane.getStyleClass().add("pane-grid");
           
            Group rootAux = new Group(pane);
            Scene sceneAux = new Scene(rootAux, rootAux.getBoundsInLocal().getWidth(), rootAux.getBoundsInLocal().getHeight());
            sceneAux.getStylesheets().add(getClass().getResource("/style.css").toExternalForm());
            SnapshotParameters sp = new SnapshotParameters();
            return rootAux.snapshot(sp, null);
        }
    

    where style.css has:

    .pane-grid {
        -fx-background-color: #D3D3D333,
        linear-gradient(from 0.5px 0.0px to 50.5px  0.0px, repeat, black 5%, transparent 5%),
        linear-gradient(from 0.0px 0.5px to  0.0px 50.5px, repeat, black 5%, transparent 5%);
    }
    
    .pane-solid {
        -fx-background-color: black;
    }
    

    (based on this answer)

    With a radius of 400, you get this image:

    2D grid

    each square is 50x50, and there are 50x16 squares.

    If you apply this diffuse map to an Sphere:

        @Override
        public void start(Stage stage) {
            PhongMaterial earthMaterial = new PhongMaterial();
            earthMaterial.setDiffuseMap(getTexture(400));
    
            final Sphere earth = new Sphere(400);
            earth.setMaterial(earthMaterial);
        
            Scene scene = new Scene(root, 800, 600, true);
            scene.setFill(Color.WHITESMOKE);
            stage.setScene(scene);
            stage.show();
        }
    

    you get:

    3D grid

    In theory, now you could fill any of the grid squares, like:

    private Image getTexture(double r) throws IOException {
            double h = 2 * r;
            double w = 2 * r * 3.125;
    
            Pane pane = new Pane();
            pane.setPrefSize(w, h);
            pane.getStyleClass().add("pane-grid");
    
            Rectangle rectangle = new Rectangle(50, 50, Color.RED);
            rectangle.setStroke(Color.WHITE);
            rectangle.setStrokeWidth(2);
    // fill rectangle at 20 x 10
            rectangle.setTranslateX(20 * 50 + 1);
            rectangle.setTranslateY(10 * 50 + 1);
            Group rootAux = new Group(pane, rectangle);
        ...
    

    with the result:

    1 rectangle

    Now that you have a well positioned image (for now just a red rectangle), you can get rid of the grid, and simply use a black color for the texture image:

       private Image getTexture(double r) throws IOException {
            double h = 2 * r;
            double w = 2 * r * 3.125;
    
            Pane pane = new Pane();
            pane.setPrefSize(w, h);
    //        pane.getStyleClass().add("pane-grid");
            pane.getStyleClass().add("pane-solid");
    

    resulting in:

    black sphere

    Now it is up to you to apply this idea to your needs. Note that you can use an ImageView with size 50x50, or 100x100, ... instead of the red rectangle, so you can use a more complex image.