PointLight in JavaFX 3D Game Doesn't Light Faces Normal to Light in Positive Z Direction

76 Views Asked by At

I am creating a 3D game in JavaFX modeled after the Octagon 3D runner game, where the player runs through a tube of octagons, rotating them to stay on the path. I am facing an issue with a PointLight, which should radiate light in all directions. When I position the light inside the octagons, it lights up faces that have a smaller Z than the light, and doesn't light faces with a larger Z than the light, creating a dark plane that cuts the light off (see image below). What's confusing is that the front facing faces of the octagon are lit up (faces parallel with the XY vertical plane), and the pink character is lit up completely fine when its Z is greater than the light's Z. This tells me the issue is probably in the gameGroup, where I keep all the octagons, as their faces are the only ones with rendering issues. Here is a pic for visualization: Image of Octagon game in JavaFX with bad lighting

To create my octagons I have an object that extends a group

public class Octagon extends Group {
/** the gap between the sides */
private int sideGap;

/** the length of the sides (they are square) */
private int sideLength;

/** a list corresponding with which sides to render. A 0 means don't render and 1 means render. List should always be length 8 */
private int[] sideRenderMap;

/** the angle to rotate each box, so they form an octagon */
private final int ROTATION_ANGLE = -45;

/** the speed in which the octagon is moving forward at the screen */
private double movementSpeed;

/** the number of sides an octagon has */
private final int NUM_SIDES = 8;

/**
 * Constructs an octagon given the # of sides to render, the gap between them, and the length of each side
 * @param sideGap, gap between sides
 * @param sideLength, length of sides
 */
public Octagon(int sideGap, int sideLength, int[] sideRenderMap, double movementSpeed) {
    super();
    this.sideGap = sideGap;
    this.sideLength = sideLength;
    this.movementSpeed = movementSpeed;

    if (sideRenderMap.length != NUM_SIDES) {
        throw new IllegalArgumentException("Render Map has too many elements!");
    } else {
        this.sideRenderMap = sideRenderMap;
    }

    generateOctagon();
}

/**
 * Function that generates an octagon from a for loop
 * Creates a group of boxes that are rotated and translated to form octagon
 */
private void generateOctagon() {
    for (int i = 0; i < NUM_SIDES; i++) {

        if (sideRenderMap[i] == 1 || sideRenderMap[i] == 2 || sideRenderMap[i] == 3) {
            // find rotation for each side
            int rotation = i * ROTATION_ANGLE;

            // create a side and move it to correct position
            Box box = new Box(sideLength, (double) sideLength / 5, sideLength);
            box.getTransforms().add(new Rotate(rotation, Rotate.Z_AXIS));


            // render counterclockwise with a gap between sides
            box.setTranslateX((sideGap + sideLength) * Math.cos(Math.toRadians(rotation + 90)));
            box.setTranslateY((sideGap + sideLength) * Math.sin(Math.toRadians(rotation + 90)));

            box.setDrawMode(DrawMode.FILL);
            // alternate colors for better visibility
            // orange and blue
            box.setMaterial(i % 2 == 0 ? new PhongMaterial(Color.web("#E87722")) : new PhongMaterial(Color.web("#003865")));

            // color chosen path in green
            if (sideRenderMap[i] == 3 || sideRenderMap[i] == 2) {
                box.setMaterial(new PhongMaterial(Color.GREEN));
            }
            this.getChildren().add(0, box);
        }


    }
}

/**
 * Function that moves the octagon along the Z axis depending on the movement speed specified and the delta time
 * This is called in the animation loop every frame
 * @param deltaTime, the time elapsed since the last update
 */
public void move(double deltaTime) {
    double distToMove = movementSpeed * deltaTime;
    setTranslateZ(getTranslateZ() - distToMove);
}

// getters and setters
public int[] getSideRenderMap() {
    return sideRenderMap;
}

public int getSideLength() {
    return sideLength;
}

public void setMovementSpeed(double newSpeed) {
    movementSpeed = newSpeed;
}

}

In my view, I have this scene graph:

    public OctagonView(OctagonModel model) {
    this.theModel = model;
    initSceneGraph();
    initStyling();
}

/**
 * init the scene graph for this view
 * sets up a stack pane with the bottom node being the group of the 3D octagon game
 * the top node is a 2D VBox that holds metadata like score/timer, basically a 2D overlay for the game
 */
private void initSceneGraph() {
    root = new StackPane();

    // build the control layout
    controlsLayout = new VBox();
    switchToEndingButton = new Button("Switch to Ending Scene");
    controlsLayout.getChildren().addAll(new Label("Octagon Game Scene!"), switchToEndingButton);

    // build the 3d game group layout
    gameGroup = new Group();

    int h = 2000;
    int r = 5;
    // set a light on the scene to render colors better
    PointLight l1 = new PointLight(Color.WHITE);
    l1.setTranslateZ(-300);

    // Red line along the x-axis
    Cylinder xAxisRed = new Cylinder(r, h);
    xAxisRed.setMaterial(new PhongMaterial(Color.RED));
    xAxisRed.getTransforms().add(new Rotate(90, Rotate.Z_AXIS));

    // Green line along the y-axis
    Cylinder yAxisGreen = new Cylinder(r, h);
    yAxisGreen.setMaterial(new PhongMaterial(Color.GREEN));

    // Blue line along the z-axis
    Cylinder zAxisBlue = new Cylinder(r, h);
    zAxisBlue.getTransforms().add(new Rotate(90, Rotate.X_AXIS));
    zAxisBlue.setMaterial(new PhongMaterial(Color.BLUE));

    Group axes = new Group();
    axes.getChildren().addAll(xAxisRed, yAxisGreen, zAxisBlue);

    Box player = new Box(60, 100, 30);
    player.setTranslateZ(-200);
    player.setTranslateY(80);
    player.setMaterial(new PhongMaterial(Color.HOTPINK));

    root.getChildren().addAll(l1, axes, gameGroup, player, controlsLayout);
}

And I am rendering the octagons & moving them towards the camera in the controller like this:

public class OctagonController {
/** reference to octagon model */
private OctagonModel theModel;

/** reference to octagon view */
private OctagonView theView;

/** global level generator for continuously generating safe levels */
private LevelGenerator levelGenerator;

/** rotate transition for smoothly rotating octagons */
private RotateTransition rotateTransition;

/** timer that controls the game loop */
private AnimationTimer gameLoop;

/** duration of rotation transition */
private final int TRANSITION_DURATION = 100;

/** number of octagons to include in a single rendered chunk */
private final int CHUNK_SIZE = 20;

/** instance var that holds the # of sides to remove from the octagon, controls the difficulty of the game */
private int sidesToRemove;

private boolean started;

/**
 * A controller class that handles communication between the model and view to create the octagon game
 * @param theModel, reference to octagon model
 * @param theView, reference to octagon view
 */
public OctagonController(OctagonModel theModel, OctagonView theView) {
    this.theModel = theModel;
    this.theView = theView;
    this.levelGenerator = new LevelGenerator(this.theModel, CHUNK_SIZE);
    this.sidesToRemove = 1;
    this.started = false;
    initTransitions();
    initBindings();
    initEventHandlers();
}

/**
 * Init all the bindings for this controller
 */
private void initBindings() {

}

/**
 * Inits the rotate transition that is used to rotate octagons for core gameplay mechanics
 */
private void initTransitions() {
    rotateTransition = new RotateTransition();
    rotateTransition.setDuration(Duration.millis(TRANSITION_DURATION));
}

/**
 * Inits all the event listeners for the octagon game
 * Controls, scene switching, etc
 */
private void initEventHandlers() {
    // reset the game and switch scenes when scene switch is clicked
    // this will eventually be triggered when the user loses the game
    theView.getSwitchToEndingButton().setOnMouseClicked(event -> {
        stopGame();
        OctagonMain.getInstance().switchToEndingScene();
    });

    // code to rotate the octagon in the clockwise and counter-clockwise directions (around the z)
    // uses the rotation transition object
    Group octGroup = theView.getGameGroup();
    theView.getRoot().addEventHandler(KeyEvent.KEY_PRESSED, event -> {
        switch (event.getCode()) {
            case A:
                // rotate octagons counter-clockwise
                rotateTransition.setNode(octGroup);
                rotateTransition.setByAngle(-45);
                rotateTransition.play();
                break;
            case D:
                // rotate octagons clockwise
                rotateTransition.setNode(octGroup);
                rotateTransition.setByAngle(45);
                rotateTransition.play();
                break;
            case P:
                // simple logic to change animation state (pause)
                if (started) {
                    started = false;
                } else {
                    started = true;
                }
                break;
        }
    });
}

/**
 * Gain start function to start the game
 * Called in OctagonMain
 * Renders the initial octagons, then starts the game loop
 */
public void startGame() {
    started = true;
    updateDifficulty(); // reset the difficulty
    renderInitialOctagons();
    initGameLoop();
    gameLoop.start();
}

/**
 * Stops the animation and clears the entire game (data and view)
 */
private void stopGame() {
    started = false;
    sidesToRemove = 1;
    gameLoop.stop();
    theModel.setSharedMovementSpeed(500);
    theModel.resetOctagonModel();
    theView.clearOctagons();
}

/**
 * Injects a new render map to render the sides of the octagons
 * and generates/renders as many octagons as allowed by CHUNK_SIZE
 * updates the model and the view
 */
private void renderInitialOctagons() {
    // set up initial octagon render map for first chunk
    theModel.ingestOctagonRenderMap(levelGenerator.generateChunkRenderMap(sidesToRemove));

    // render octagons based on their render map
    for (int i = 0; i < CHUNK_SIZE; i++) {
        Octagon o = new Octagon(theModel.getSIDE_GAP(), theModel.getSIDE_LENGTH(), theModel.getRenderMapAt(i), theModel.getSharedMovementSpeed());
        o.setTranslateZ((o.getSideLength()) * i);
        Platform.runLater(() -> {
            theModel.addOctagon(o);
            theView.renderOctagon(o);
        });

        // when all 10 inital octagons have been created, generate another chunk render map and save the last octagon
        if (i == CHUNK_SIZE - 1) {
            theModel.ingestOctagonRenderMap(levelGenerator.generateChunkRenderMap(sidesToRemove));
            theModel.setLastOctInPrevChunk(o);
        }
    }
}

/**
 * Inits the game loop with the animation timer to update the octagons every frame
 */
private void initGameLoop() {
    gameLoop = new AnimationTimer() {
        private long lastUpdateTime = 0;

        // fires every frame
        @Override
        public void handle(long now) {
            // update the octagons position continuously, and handle efficient removing/adding of new octagons
            if (lastUpdateTime != 0 && started) {
                // calc time since last update
                double deltaTime = (now - lastUpdateTime) / 1_000_000_000.0;
                // update position of octagons to move at the camera
                theModel.updateOctagonPosition(deltaTime);
                // add/remove octagons as they go out of view
                handleOctagonBehindCamera();
            }
            lastUpdateTime = now;
        }
    };
}

/**
 * Handles adding and removing octagons when they go behind the camera
 * Remove the octagon when its out of view, and add another at the end of the sequence to make it inifinite
 */
private void handleOctagonBehindCamera() {
    int distanceThreshold = 500;
    List<int[]> octRenderMap = theModel.getRenderMap();

    // check all octagons in the game
    for (Octagon o : theModel.getOctagons()) {

        // if octagon is out of view
        if (o.getTranslateZ() < theView.getCamera().getTranslateZ() - distanceThreshold) {

            // use runLater to ensure this runs on a safe JavaFX UI thread
            // avoid thread exceptions
            Platform.runLater(() -> {
                // remove the octagons from the game
                theModel.removeOctagon(o);
                theView.removeOctagon(o);

                // if the render map is getting too small, add another and save current oct as last oct
                if (octRenderMap.size() <= CHUNK_SIZE) {
                    theModel.setLastOctInPrevChunk(o);
                    theModel.ingestOctagonRenderMap(levelGenerator.generateChunkRenderMap(sidesToRemove));
                }

                // generate a new oct and add it to the end
                Octagon newOct = theModel.generateNewOctagon();
                theView.renderOctagon(newOct);

                // increase the score by 1 because the user passed an octagon (scoring can be changed later)
                theModel.addOneToScore();
                updateDifficulty();
            });

        }
    }
}

/**
 * Use the score of the game to update the number of sides to remove which controls the difficulty of the game
 * TODO: In the future update the speed at which the octagons move forward to increase game speed
 */
private void updateDifficulty() {
    // update difficulty based on score
    int score = theModel.getScore();
    if (score == 25) {
        sidesToRemove = 2;
        theModel.setSharedMovementSpeed(600);
    } else if (score == 50) {
        sidesToRemove = 2;
        theModel.setSharedMovementSpeed(750);
    } else if (score == 75) {
        sidesToRemove = 3;
        theModel.setSharedMovementSpeed(1000);
    } else if (score == 100) {
        sidesToRemove = 4;
    }
}

}

Does anyone have any idea of why this is happening? I've looked into the depth buffer, diffusion on PhongMaterials, creating multiple lights, and moving the lights and the scene around. I've found no explanation or solution, even my professors are very confused and don't know whats happening. Any help is greatly appreciated.

0

There are 0 best solutions below