реализуй простейшую модель освещения. вот теория
Простейшая модель освещения
В самом простом варианте для того, чтобы картинка выглядела более реали-
стичной, достаточно добавить тени. Их расчёт основан на операции скалярного
произведения.
Яркость цвета в точке зависит от того, под каким углом на неё падает луч
света. Если тот приходит почти перпендикулярно, то и объект яркий. В ином
случае цвет следует слегка затемнить.
Рис. 28: Нормаль и падающий луч в точке
Для того, чтобы была возможность вычислить цвет в точке, нужно знать ко-
ординаты нормали и луча света. Так как от этих векторов используются только
направления, величины обычно нормализуют. Тогда становится возможным полу-
чить нормированный коэффициент:
l = −⃗n · r⃗ay
Если l отрицательный, это значит, что луч приходит изнутри модели. В таком
случае коэффициент можно обнулить.
В итоге получаем универсальный коэффициент l в диапазоне от 0 до 1 такой,
что его можно умножить на текущий цвет r⃗gb в точке, дабы обновить тот с учётом
освещения. Правда, если это сделать в лоб, переход может показаться слишком
резким и неестественным. Это связано с тем, что в реальном мире независимо
от конкретного источника света почти всегда есть какое-то фоновое освещение.
Поэтому в нашей модели можно не доводить до ситуации, когда цвет близок к
абсолютно чёрному и выбрать следующий формат:
rg⃗b′ = r⃗gb(1 − k) + r⃗gb ∗ k ∗ l,
68
где k — тоже какой-то коэффициент от 0 до 1, настраиваемый разработчиком.
Он показывает, какая доля от цвета отдается для настройки теней. Часть (1−k),
напротив, отвечает за фоновую неизменяемую яркость.
Осталось ответить на вопрос, как же посчитать нормаль в каждой точки мо-
дели. Если брать её одну на весь полигон, то после работы алгоритма будут вид-
ны переходы между гранями. Чтобы этого избежать, реализуют так называемое
сглаживание нормалей. В том числе ради него в модели хранятся нормали не
для полигонов, а для вершин.
Опять же, с помощью барицентрических координат в момент закрашивания
пикселя на треугольнике, можно на основе нормалей вершин получить нормаль в
точке:
vn⃗pixel = αv⃗nA + βvn⃗B + γvn⃗C
А затем применить её для вычисления яркости.
В случаях, когда модель может менять своё положение или совершать поворо-
ты, необходимо переводить нормали из локальной системы координат в мировую.
Иначе можно получить странную ситуацию, когда движение на сцене не влияет
на освещение.
P.s. А как в реальном мире образуются блики на предметах? И что следует
добавить в описанный алгоритм для того, чтобы их было возможно отрисовать.

вот мой код. Добавь также булевое значение флага которое будет отвечать  за то есть ли освещение или нет (вкл выкл)

public class RenderEngine {

    /**
     * Флаг, определяющий режим рендеринга.
     * true - с наложением текстур,
     * false - только растеризация с использованием цветов.
     */
    private boolean useTexture;

    /**
     * Конструктор RenderEngine.
     *
     * @param useTexture Флаг для выбора режима рендеринга.
     */
    public RenderEngine(boolean useTexture) {
        this.useTexture = useTexture;
    }

    /**
     * Устанавливает режим рендеринга.
     *
     * @param useTexture true для наложения текстур, false для растеризации.
     */
    public void setUseTexture(boolean useTexture) {
        this.useTexture = useTexture;
    }

    /**
     * Вычисляет матрицу модель-представление-проекция с учетом камеры и трансформаций модели.
     *
     * @param camera    Объект камеры, содержащий матрицы вида и проекции.
     * @param rotate    Вектор поворота модели.
     * @param scale     Вектор сжатия модели.
     * @param translate Вектор переноса модели.
     * @return Матрица модель-представление-проекция.
     */
    private Matrix4f calculateModelViewProjectionMatrix(Camera camera, Vector3f rotate, Vector3f scale, Vector3f translate) {
        Matrix4f modelMatrix = rotateScaleTranslate(rotate, scale, translate);
        Matrix4f viewMatrix = camera.getViewMatrix();
        Matrix4f projectionMatrix = camera.getProjectionMatrix();

        modelMatrix.transposition();
        viewMatrix.transposition();
        projectionMatrix.transposition();

        Matrix4f mvpMatrix = new Matrix4f(projectionMatrix);
        mvpMatrix.multiply(viewMatrix);
        mvpMatrix.multiply(modelMatrix);
        return mvpMatrix;
    }

    /**
     * Инициализирует Z-буфер заданными размерами.
     *
     * @param width  Ширина экрана.
     * @param height Высота экрана.
     * @return Двумерный массив Z-буфера, заполненный максимальными значениями глубины.
     */
    private float[][] initializeZBuffer(int width, int height) {
        float[][] zBuffer = new float[width][height];
        for (int x = 0; x < width; x++) {
            for (int y = 0; y < height; y++) {
                zBuffer[x][y] = Float.MAX_VALUE;
            }
        }
        return zBuffer;
    }

    /**
     * Выполняет рендеринг 3D модели на графическом контексте.
     *
     * @param graphicsContext Графический контекст JavaFX для рисования.
     * @param camera          Объект камеры, определяющий вид и проекцию.
     * @param model           Модель для рендеринга.
     * @param width           Ширина области рендеринга.
     * @param height          Высота области рендеринга.
     */
    public void render(GraphicsContext graphicsContext, Camera camera, Model model, int width, int height) {
        float[][] zBuffer = initializeZBuffer(width, height);
        Matrix4f mvpMatrix = calculateModelViewProjectionMatrix(camera, model.getRotate(), model.getScale(), model.getTranslate());

        Image texture = model.getTexture();

        for (Polygon polygon : model.getPolygons()) {
            renderPolygon(graphicsContext, polygon, mvpMatrix, width, height, zBuffer, model, texture);
        }
    }

    /**
     * Рендерит отдельный многоугольник на графическом контексте.
     *
     * @param graphicsContext Графический контекст для рисования.
     * @param polygon         Многоугольник для рендеринга.
     * @param mvpMatrix       Матрица модель-представление-проекция.
     * @param width           Ширина области рендеринга.
     * @param height          Высота области рендеринга.
     * @param zBuffer         Z-буфер для отслеживания глубины пикселей.
     * @param model           Модель, содержащая вершины.
     * @param texture         Текстура модели.
     */
    private void renderPolygon(GraphicsContext graphicsContext, Polygon polygon, Matrix4f mvpMatrix,
                               int width, int height, float[][] zBuffer, Model model, Image texture) {

        int[] vertexIndices = polygon.getVertexIndices().stream().mapToInt(Integer::intValue).toArray();
        int[] textureIndices = polygon.getTextureVertexIndices().stream().mapToInt(Integer::intValue).toArray();

        Point2f[] screenPoints = new Point2f[3];
        float[] zValues = new float[3];
        Vector2f[] textureCoords = new Vector2f[3];

        for (int i = 0; i < 3; i++) {
            Vector3f transformedVertex = transformVertex(model, vertexIndices[i], mvpMatrix);
            screenPoints[i] = vertexToPoint(transformedVertex, width, height);
            zValues[i] = transformedVertex.getZ();

            if (useTexture && textureIndices.length == 3) {
                Vector2f texCoord = model.getTextureVertices().get(textureIndices[i]);
                textureCoords[i] = texCoord;
            }
        }

        if (useTexture && textureIndices.length == 3) {
            rasterizeTriangleWithTexture(screenPoints, zValues, textureCoords, zBuffer, graphicsContext, texture);
        } else {
            rasterizeTriangle(screenPoints, zValues, zBuffer, graphicsContext);
        }
    }

    /**
     * Выполняет растеризацию треугольника с наложением текстуры и обновляет графический контекст.
     *
     * @param points          Координаты точек треугольника на экране.
     * @param zValues         Значения глубины для каждой вершины.
     * @param textureCoords   Текстурные координаты для каждой вершины.
     * @param zBuffer         Z-буфер для отслеживания глубины пикселей.
     * @param graphicsContext Графический контекст для рисования.
     * @param texture         Текстура модели.
     */
    private void rasterizeTriangleWithTexture(Point2f[] points, float[] zValues, Vector2f[] textureCoords,
                                              float[][] zBuffer, GraphicsContext graphicsContext, Image texture) {
        PixelReader pixelReader = texture.getPixelReader();
        if (pixelReader == null) {
            System.err.println("Не удалось получить PixelReader из текстуры.");
            return;
        }

        int minX = clamp((int) Math.floor(Math.min(points[0].getX(), Math.min(points[1].getX(), points[2].getX()))), 0, (int) texture.getWidth() - 1);
        int maxX = clamp((int) Math.ceil(Math.max(points[0].getX(), Math.max(points[1].getX(), points[2].getX()))), 0, zBuffer.length - 1);
        int minY = clamp((int) Math.floor(Math.min(points[0].getY(), Math.min(points[1].getY(), points[2].getY()))), 0, (int) texture.getHeight() - 1);
        int maxY = clamp((int) Math.ceil(Math.max(points[0].getY(), Math.max(points[1].getY(), points[2].getY()))), 0, zBuffer[0].length - 1);

        for (int x = minX; x <= maxX; x++) {
            for (int y = minY; y <= maxY; y++) {
                float[] barycentric = calculateBarycentricCoordinates(points, x, y);
                float lambda0 = barycentric[0];
                float lambda1 = barycentric[1];
                float lambda2 = barycentric[2];

                if (isInsideTriangle(lambda0, lambda1, lambda2)) {
                    float depth = interpolate(lambda0, lambda1, lambda2, zValues);

                    if (depth < zBuffer[x][y]) {
                        zBuffer[x][y] = depth;

                        float u = interpolate(lambda0, lambda1, lambda2, new float[]{
                                textureCoords[0].getX(),
                                textureCoords[1].getX(),
                                textureCoords[2].getX()});
                        float v = interpolate(lambda0, lambda1, lambda2, new float[]{
                                textureCoords[0].getY(),
                                textureCoords[1].getY(),
                                textureCoords[2].getY()});

                        int texX = clamp((int) ((1.0f - u) * (texture.getWidth() - 1)), 0, (int) texture.getWidth() - 1);
                        int texY = clamp((int) ((1 - v) * (texture.getHeight() - 1)), 0, (int) texture.getHeight() - 1);

                        Color color = pixelReader.getColor(texX, texY);
                        graphicsContext.getPixelWriter().setColor(x, y, color);
                    }
                }
            }
        }
    }

    /**
     * Выполняет растеризацию треугольника и обновляет графический контекст.
     *
     * @param points          Координаты точек треугольника на экране.
     * @param zValues         Значения глубины для каждой вершины.
     * @param zBuffer         Z-буфер для отслеживания глубины пикселей.
     * @param graphicsContext Графический контекст для рисования.
     */
    private void rasterizeTriangle(Point2f[] points, float[] zValues, float[][] zBuffer, GraphicsContext graphicsContext) {
        Color colorA = Color.RED;
        Color colorB = Color.BLACK;
        Color colorC = Color.BLUE;

        int minX = clamp((int) Math.floor(Math.min(points[0].getX(), Math.min(points[1].getX(), points[2].getX()))), 0, zBuffer.length - 1);
        int maxX = clamp((int) Math.ceil(Math.max(points[0].getX(), Math.max(points[1].getX(), points[2].getX()))), 0, zBuffer.length - 1);
        int minY = clamp((int) Math.floor(Math.min(points[0].getY(), Math.min(points[1].getY(), points[2].getY()))), 0, zBuffer[0].length - 1);
        int maxY = clamp((int) Math.ceil(Math.max(points[0].getY(), Math.max(points[1].getY(), points[2].getY()))), 0, zBuffer[0].length - 1);

        for (int x = minX; x <= maxX; x++) {
            for (int y = minY; y <= maxY; y++) {
                float[] barycentric = calculateBarycentricCoordinates(points, x, y);
                float lambda0 = barycentric[0];
                float lambda1 = barycentric[1];
                float lambda2 = barycentric[2];

                if (isInsideTriangle(lambda0, lambda1, lambda2)) {
                    float depth = interpolate(lambda0, lambda1, lambda2, zValues);

                    if (depth < zBuffer[x][y]) {
                        zBuffer[x][y] = depth;
                        Color color = interpolateColor(lambda0, lambda1, lambda2, colorA, colorB, colorC);
                        graphicsContext.getPixelWriter().setColor(x, y, color);
                    }
                }
            }
        }
    }

    /**
     * Применяет трансформацию к вершине модели.
     *
     * @param model       Модель, содержащая вершины.
     * @param vertexIndex Индекс вершины в модели.
     * @param mvpMatrix   Матрица модель-представление-проекция.
     * @return Трансформированная вершина.
     */
    private Vector3f transformVertex(Model model, int vertexIndex, Matrix4f mvpMatrix) {
        Vector3f vertex = model.getVertices().get(vertexIndex);
        return multiplyMatrix4ByVector3(mvpMatrix, vertex);
    }

    /**
     * Вычисляет барицентрические координаты точки относительно треугольника.
     *
     * @param points Координаты вершин треугольника.
     * @param x      Координата X точки.
     * @param y      Координата Y точки.
     * @return Массив из трех барицентрических координат.
     */
    private float[] calculateBarycentricCoordinates(Point2f[] points, int x, int y) {
        float denom = ((points[1].getY() - points[2].getY()) * (points[0].getX() - points[2].getX()) +
                (points[2].getX() - points[1].getX()) * (points[0].getY() - points[2].getY()));

        float lambda0 = ((points[1].getY() - points[2].getY()) * (x - points[2].getX()) +
                (points[2].getX() - points[1].getX()) * (y - points[2].getY())) / denom;

        float lambda1 = ((points[2].getY() - points[0].getY()) * (x - points[2].getX()) +
                (points[0].getX() - points[2].getX()) * (y - points[2].getY())) / denom;

        float lambda2 = 1.0f - lambda0 - lambda1;

        return new float[]{lambda0, lambda1, lambda2};
    }

    /**
     * Проверяет, находится ли точка внутри треугольника на основе барицентрических координат.
     *
     * @param lambda0 Барицентрическая координата λ0.
     * @param lambda1 Барицентрическая координата λ1.
     * @param lambda2 Барицентрическая координата λ2.
     * @return {@code true}, если точка внутри треугольника, иначе {@code false}.
     */
    private boolean isInsideTriangle(float lambda0, float lambda1, float lambda2) {
        return lambda0 >= 0 && lambda1 >= 0 && lambda2 >= 0;
    }

    /**
     * Интерполирует значение по заданным весам.
     *
     * @param lambda0 Барицентрическая координата λ0.
     * @param lambda1 Барицентрическая координата λ1.
     * @param lambda2 Барицентрическая координата λ2.
     * @param values  Массив значений для интерполяции.
     * @return Интерполированное значение.
     */
    private float interpolate(float lambda0, float lambda1, float lambda2, float[] values) {
        return lambda0 * values[0] + lambda1 * values[1] + lambda2 * values[2];
    }

    /**
     * Интерполирует цвет точки внутри треугольника на основе барицентрических координат.
     *
     * @param lambda0 Барицентрическая координата λ0.
     * @param lambda1 Барицентрическая координата λ1.
     * @param lambda2 Барицентрическая координата λ2.
     * @param colorA  Цвет первой вершины треугольника.
     * @param colorB  Цвет второй вершины треугольника.
     * @param colorC  Цвет третьей вершины треугольника.
     * @return Интерполированный цвет.
     */
    private Color interpolateColor(float lambda0, float lambda1, float lambda2, Color colorA, Color colorB, Color colorC) {
        double red = lambda0 * colorA.getRed() + lambda1 * colorB.getRed() + lambda2 * colorC.getRed();
        double green = lambda0 * colorA.getGreen() + lambda1 * colorB.getGreen() + lambda2 * colorC.getGreen();
        double blue = lambda0 * colorA.getBlue() + lambda1 * colorB.getBlue() + lambda2 * colorC.getBlue();
        return new Color(clamp(red), clamp(green), clamp(blue), 1.0);
    }

    /**
     * Ограничивает значение в диапазоне [0, 1].
     *
     * @param value Исходное значение.
     * @return Ограниченное значение.
     */
    private double clamp(double value) {
        return Math.max(0.0, Math.min(1.0, value));
    }

    /**
     * Ограничивает целочисленное значение в заданном диапазоне.
     *
     * @param value Исходное значение.
     * @param min   Минимальное значение.
     * @param max   Максимальное значение.
     * @return Ограниченное значение.
     */
    private int clamp(int value, int min, int max) {
        return Math.max(min, Math.min(max, value));
    }
}