1

I have a TriangleMesh with a texture/diffuse map that is a 1024x1024 texture which is fully black except the last 3 lines which are filled with red, green and blue. I gave each vertex of each triangle a constant V value (either 1021, 1022 or 1023) added 0.5 to it to center and divided it by the texture's height so it would only use one of the 3 colors and arbitrary U values.

// arbitrary u values.
float u1 = 0.1f;
float u2 = 0.3f;
float u3 = 0.9f;

int randomY = ThreadLocalRandom.current()
        .nextInt((int) atlas.getHeight() - 3, (int) atlas.getHeight());
float y = randomY + 0.5f;

float v = (float) (y / atlas.getHeight());

int texIndex1 = mesh.addUV(u1, v);
int texIndex2 = mesh.addUV(u2, v);
int texIndex3 = mesh.addUV(u3, v);

mesh.getFaces().addAll(
        vertexIndex1, texIndex1,
        vertexIndex2, texIndex2,
        vertexIndex3, texIndex3
);

The addUV method looks like this(i have my own mesh class that extends TriangleMesh which contains some helper methods)

public int addUV(float u, float v) {
    int cur = getTexCoords().size() / 2;
    getTexCoords().addAll(u, v);
    return cur;
}

The expected result is a mesh that has it's triangles colored solid red, green and blue because V is constant and each line(y) is filled with a single color however what i got instead was a bunch of different colors that change as u zoom in/out.

If i use the same U value for each vertex as well, it does give the correct result but i don't understand why it wouldn't do the same with arbitrary U values given that the color at any given U is the exact same.

The current result(gif to show the color changing): https://i.imgur.com/4lTcLfH.gif | As seen it actually does show the correct colors but only if u zoom in a lot

The expected result(can be produced if i have constant U values as well like 0.5, 0.5, 0.5): https://i.imgur.com/x35u6xv.gif | Looks as it should, doesn't change when u zoom in/out

The texture i used as the diffuse map: https://i.stack.imgur.com/ufOTi.png

Minimal reproducible example with a quad made of 2 triangles:

Create a main method (either in the same class or another) and add: Application.launch(TextureMappingIssue.class); i didn't add it in my example as depending on the setup, the main method must be in a different class

import javafx.application.Application;
import javafx.embed.swing.SwingFXUtils;
import javafx.scene.*;
import javafx.scene.image.Image;
import javafx.scene.image.PixelWriter;
import javafx.scene.image.WritableImage;
import javafx.scene.layout.AnchorPane;
import javafx.scene.paint.Color;
import javafx.scene.paint.PhongMaterial;
import javafx.scene.shape.MeshView;
import javafx.scene.shape.TriangleMesh;
import javafx.scene.transform.Rotate;
import javafx.scene.transform.Scale;
import javafx.scene.transform.Translate;
import javafx.stage.Stage;

import javax.imageio.ImageIO;
import java.nio.file.Path;

/**
 * Note that when the application is first opened without
 * the camera moved, it looks as it should, as soon as
 * the camera is moved (i.e if the mouse is moved in this case)
 * it looks completely different, even if the camera is moved slightly
 */

public class TextureMappingIssue extends Application {

    private static final int WIDTH = 800;
    private static final int HEIGHT = 600;

    private AnchorPane modelPane;

    private Group scene;
    private SubScene subScene;

    @Override
    public void start(Stage primaryStage) throws Exception {

        modelPane = new AnchorPane();
        modelPane.setPrefWidth(WIDTH);
        modelPane.setPrefHeight(HEIGHT);

        initScene();

        // smaller palette = looks correct until u zoom out more
        int paletteWidth = 1024;
        int paletteHeight = 1024;
        /*
         * amount of copies for red, green, blue(the colors at the bottom), the center one is picked
         * note that copies = 1 just writes the original color, copies = 2 writes the original + 1 copy and so on (so with copies = 3, it writes the color 3 times and picks the 2nd one for v)
         */
        int copies = 1;

        float QUAD_SCALE = 1f;

        float[] vertices = {
                -QUAD_SCALE, -QUAD_SCALE, 0,
                -QUAD_SCALE, QUAD_SCALE, 0,
                QUAD_SCALE, QUAD_SCALE, 0,
                QUAD_SCALE, -QUAD_SCALE, 0
        };

        int[] indices = {
                0, 0, 1, 1, 2, 2, // first triangle
                0, 3, 2, 4, 3, 5, // second triangle
        };

        // set these to 0f, 0f, 0f (or any value as long as they're identical to get the expected result)
        float u1 = 0.1f;
        float u2 = 0.3f;
        float u3 = 0.5f;

        int colorIndex = 1; // either 0, 1 or 2 (red, green, blue)
        int offset = (3 - colorIndex) * copies;
        // v is constant for each vertex in my actual application as well.
        float v1 = (paletteHeight - offset + (copies / 2) + 0.5f) / paletteHeight;

        float[] texCoords = {
                u1, v1, u2, v1, u3, v1
        };

        Image palette = generatePalette(paletteWidth, paletteHeight, copies);
        ImageIO.write(SwingFXUtils.fromFXImage(palette, null), "png", Path.of("./testpalette.png")
                .toFile());

        TriangleMesh triangle = new TriangleMesh();

        triangle.getPoints().addAll(vertices);
        triangle.getFaces().addAll(indices);
        triangle.getTexCoords().addAll(texCoords);
        triangle.getTexCoords().addAll(texCoords);

        MeshView view = new MeshView(triangle);
        PhongMaterial material = new PhongMaterial();
        material.setDiffuseMap(palette);
        //material.setSpecularMap(specular);
        // material.setSpecularPower(32); // default
        view.setMaterial(material);
        scene.getChildren().add(view);

        Scene scene = new Scene(modelPane, WIDTH, HEIGHT, true, SceneAntialiasing.BALANCED);
        scene.setFill(Color.BLACK);
        primaryStage.setScene(scene);
        primaryStage.show();
    }

    private void initScene() {
        scene = new Group();
        //Group grid = new Grid3D().create(48f, 1.25f);
        //scene.getChildren().add(grid);
        subScene = createScene3D();
        scene.getChildren().add(new AmbientLight(Color.WHITE));
        modelPane.getChildren().addAll(subScene);
    }

    private SubScene createScene3D() {
        SubScene scene3d = new SubScene(scene, modelPane.getPrefWidth(), modelPane.getPrefHeight(), true, SceneAntialiasing.BALANCED);
        scene3d.setFill(Color.rgb(25, 25, 25));
        new OrbitCamera(scene3d, scene);
        return scene3d;
    }

    private Image generatePalette(int width, int height, int copies) {
        WritableImage palette = new WritableImage(width, height);
        Color[] debugColors = {Color.RED, Color.GREEN, Color.BLUE};
        PixelWriter writer = palette.getPixelWriter();
        int offset = height - (debugColors.length * copies);
        for (int y = 0; y < offset; y++) {
            for (int x = 0; x < width; x++) {
                writer.setColor(x, y, Color.BLACK);
            }
        }

        int colorOff = 0;
        for (int y = offset; y < height - (copies - 1); y += copies) {
            Color c = debugColors[colorOff];
            if (c == Color.GREEN) {
                System.out.println("Y = " + y);
            }
            for (int k = 0; k < copies; k++) {
                for (int x = 0; x < width; x++) {
                    writer.setColor(x, y + k, c);
                }
            }
            colorOff++;
        }

        return palette;

    }

    private Image generateSpecular(int width, int height) {
        WritableImage specular = new WritableImage(width, height);
        PixelWriter writer = specular.getPixelWriter();
        for (int y = 0; y < height; y++) {
            for (int x = 0; x < width; x++) {
                writer.setColor(x, y, Color.WHITE);
            }
        }

        return specular;
    }

    /*
     * Orbit camera
     */

    private static class OrbitCamera {

        private final SubScene subScene;
        private final Group root3D;

        private final double MAX_ZOOM = 300.0;

        public OrbitCamera(SubScene subScene, Group root) {
            this.subScene = subScene;
            this.root3D = root;
            init();
        }

        private void init() {
            camera.setNearClip(0.1D);
            camera.setFarClip(MAX_ZOOM * 1.15D);
            camera.getTransforms().addAll(
                    yUpRotate,
                    cameraPosition,
                    cameraLookXRotate,
                    cameraLookZRotate
            );

            Group rotateGroup = new Group();
            try {
                rotateGroup.getChildren().addAll(cameraXform);
            } catch (Exception e) {
                e.printStackTrace();
            }
            cameraXform.ry.setAngle(0);
            cameraXform.rx.setAngle(-18);
            cameraXform.getChildren().add(cameraXform2);
            cameraXform2.getChildren().add(cameraXform3);
            cameraXform3.getChildren().add(camera);
            cameraPosition.setZ(-cameraDistance);

            root3D.getChildren().addAll(rotateGroup);

            subScene.setCamera(camera);
            subScene.setOnScroll(event -> {

                double zoomFactor = 1.05;
                double deltaY = event.getDeltaY();

                if (deltaY < 0) {
                    zoomFactor = 2.0 - zoomFactor;
                }
                double z = cameraPosition.getZ() / zoomFactor;
                z = Math.max(z, -MAX_ZOOM);
                z = Math.min(z, 10.0);
                cameraPosition.setZ(z);
            });

            subScene.setOnMousePressed(event -> {
                if (!event.isAltDown()) {
                    dragStartX = event.getSceneX();
                    dragStartY = event.getSceneY();
                    dragStartRotateX = cameraXRotate.getAngle();
                    dragStartRotateY = cameraYRotate.getAngle();
                    mousePosX = event.getSceneX();
                    mousePosY = event.getSceneY();
                    mouseOldX = event.getSceneX();
                    mouseOldY = event.getSceneY();
                }
            });

            subScene.setOnMouseDragged(event -> {
                if (!event.isAltDown()) {
                    double modifier = 1.0;
                    double modifierFactor = 0.3;

                    if (event.isControlDown()) modifier = 0.1;
                    if (event.isSecondaryButtonDown()) modifier = 0.035;

                    mouseOldX = mousePosX;
                    mouseOldY = mousePosY;
                    mousePosX = event.getSceneX();
                    mousePosY = event.getSceneY();
                    mouseDeltaX = mousePosX - mouseOldX;
                    mouseDeltaY = mousePosY - mouseOldY;

                    double flip = -1.0;

                    if (event.isSecondaryButtonDown()) {
                        double newX = cameraXform2.t.getX() + flip * mouseDeltaX * modifierFactor * modifier * 2.0;
                        double newY = cameraXform2.t.getY() + 1.0 * -mouseDeltaY * modifierFactor * modifier * 2.0;
                        cameraXform2.t.setX(newX);
                        cameraXform2.t.setY(newY);
                    } else if (event.isPrimaryButtonDown()) {
                        double yAngle = cameraXform.ry.getAngle() - 1.0 * -mouseDeltaX * modifierFactor * modifier * 2.0;
                        double xAngle = cameraXform.rx.getAngle() + flip * mouseDeltaY * modifierFactor * modifier * 2.0;
                        cameraXform.ry.setAngle(yAngle);
                        cameraXform.rx.setAngle(xAngle);
                    }
                }
            });
        }


        private final PerspectiveCamera camera = new PerspectiveCamera(true);
        private final Rotate cameraXRotate = new Rotate(-20.0, 0.0, 0.0, 0.0, Rotate.X_AXIS);
        private final Rotate cameraYRotate = new Rotate(-20.0, 0.0, 0.0, 0.0, Rotate.Y_AXIS);
        private final Rotate cameraLookXRotate = new Rotate(0.0, 0.0, 0.0, 0.0, Rotate.X_AXIS);
        private final Rotate cameraLookZRotate = new Rotate(0.0, 0.0, 0.0, 0.0, Rotate.Z_AXIS);
        private final Translate cameraPosition = new Translate(0.0, 0.0, 0.0);
        private Xform cameraXform = new Xform();
        private Xform cameraXform2 = new Xform();
        private Xform cameraXform3 = new Xform();
        private double cameraDistance = 25.0;
        private double dragStartX = 0;
        private double dragStartY = 0;
        private double dragStartRotateX = 0;
        private double dragStartRotateY = 0;
        private double mousePosX = 0;
        private double mousePosY = 0;
        private double mouseOldX = 0;
        private double mouseOldY = 0;
        private double mouseDeltaX = 0;
        private double mouseDeltaY = 0;
        private Rotate yUpRotate = new Rotate(0.0, 0.0, 0.0, 0.0, Rotate.X_AXIS);

        public Camera getCamera() {
            return camera;
        }

        public Xform getCameraXform() {
            return cameraXform;
        }
    }

    private static class Xform extends Group {

        Translate t = new Translate();
        Translate p = new Translate();
        public Rotate rx = new Rotate();
        public Rotate ry = new Rotate();
        Rotate rz = new Rotate();
        Scale s = new Scale();

        public Xform() {
            rx.setAxis(Rotate.X_AXIS);
            ry.setAxis(Rotate.Y_AXIS);
            rz.setAxis(Rotate.Z_AXIS);
            getTransforms().addAll(t, rz, ry, rx, s);
        }
    }

}

Edit: updated the code to support generating multiple copies of a single color and picking the center one, however this doesn't solve the issue either, it's just less visible :/

Update: the issue can be reproduced even with a 128x3 image (where it's just the red, green, blue color with 128 pixel rows)

Update 2: I can reproduce the same issue in my original code even with my original palette (that is a 128x512 image of colors that are all potentially used)

Update 3: I have decided to go for per pixel shading instead (i.e provide my mesh with a set of normals and add light sources to the scene(other than ambient)) what i wanted to do initially with the palette was to export all the vertex colors generated from a function that emulates gouraud shading but because of these interpolation issues i have went for per pixel shading (which looks better anyway, altho ideally i would've wanted to emulate gouraud shading as the game engine i use for which the javafx program is for also uses gouraud shading)

Suic
  • 433
  • 1
  • 3
  • 9
  • 1
    [mcve] please.. – kleopatra Aug 01 '22 at 22:27
  • Thanks, i will try my best to create something short that produces the same result and can be run easily. – Suic Aug 01 '22 at 22:30
  • Some related examples are seen [here](https://stackoverflow.com/search?tab=votes&q=%5bjavafx%5d%20TriangleMesh). – trashgod Aug 01 '22 at 23:24
  • 1
    Added minimal reproducible example – Suic Aug 01 '22 at 23:37
  • I don't understand why you make a 1024*1024 raster image for difuse map but you are mapping your uvs to a very small portion of it . one size is just 3 pixels – Giovanni Contreras Aug 02 '22 at 01:50
  • It was just an example, the issue can be reproduced with a 128x128 texture easily as well, in my original code i use a 128x515 (for example, the height differs) image where all UV's are used but it still gives the same problem for the last 3 lines/columns. – Suic Aug 02 '22 at 02:31
  • Have you checked this [question](https://stackoverflow.com/questions/46782788/javafx-3d-coloring-each-vertex-in-triangle-mesh-with-specific-color)? It has some considerations on how interpolation works. – José Pereda Aug 02 '22 at 08:42
  • I have indeed checked that however i am still not sure why this is happening, at first i thought that it had to do with some filtering when sampling the interpolated texels, so what i did was try to have 3(i tried with a higher number as well) copies of each color(red, green, blue) and pick the center one, however the issue still present, with a large number of copies it's not as visible but definitely still there. I have updated my post with the code that also supports generating copies & picking the center. – Suic Aug 02 '22 at 14:27
  • Have you tried with `copies = 5` or even `copies = 10`? That makes the 3 coloured lines thicker, and interpolation works way better. – José Pereda Aug 02 '22 at 16:37
  • Still, I don't understand why you need a huge texture image being most of it black, if all you need is 4 different colours? See the edit in this [answer](https://stackoverflow.com/a/26834319/3956070). – José Pereda Aug 02 '22 at 16:42
  • It was just an example, im sorry if it was misleading. in my original code i have a 128x512 palette initially of different colors (128 very similar but still different colors per column) and specifying UV's for that palette works perfectly, and then at the bottom of it i have certain columns of the same palette repeated with transparency applied to each row. Example of my actual palette for a certain mesh: https://i.imgur.com/H9WAI6f.png And yes i have tried with copies = 5 and copies = 10, and a bunch of other values, it is still quite visible if i view it at a certain angle/distance. – Suic Aug 02 '22 at 17:03

0 Answers0