проверь данный код на правильность. дело в том что он иногда определяет нормаль к вершине как null и здесь выкидывет исключение
  Vector3f interpolatedNormal = new Vector3f(
                                    lambda0 * normals[0].getX() + lambda1 * normals[1].getX() + lambda2 * normals[2].getX(),
                                    lambda0 * normals[0].getY() + lambda1 * normals[1].getY() + lambda2 * normals[2].getY(),
                                    lambda0 * normals[0].getZ() + lambda1 * normals[1].getZ() + lambda2 * normals[2].getZ()
                            );


/**
 * Класс для рендеринга 3D моделей с использованием графического контекста JavaFX.
 */
public class RenderEngine {

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

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

    /**
     * Коэффициент влияния освещения на цвет.
     * Значение от 0 до 1.
     */
    private float lightingCoefficient;

    /**
     * Направление источника света в мировых координатах.
     * Должно быть нормализовано.
     */
    private Vector3f lightDirection;

    /**
     * Конструктор RenderEngine.
     *
     * @param useTexture           Флаг для выбора режима рендеринга.
     * @param useLighting          Флаг для включения/отключения освещения.
     * @param lightingCoefficient  Коэффициент влияния освещения на цвет.
     * @param lightDirection       Направление источника света.
     */
    public RenderEngine(boolean useTexture, boolean useLighting, float lightingCoefficient, Vector3f lightDirection) {
        this.useTexture = useTexture;
        this.useLighting = useLighting;
        this.lightingCoefficient = lightingCoefficient;
        lightDirection.normalize();
        this.lightDirection = lightDirection; // Убедимся, что направление нормализовано
    }

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

    /**
     * Устанавливает режим освещения.
     *
     * @param useLighting true для включения освещения, false для отключения.
     */
    public void setUseLighting(boolean useLighting) {
        this.useLighting = useLighting;
    }

    /**
     * Устанавливает коэффициент влияния освещения.
     *
     * @param lightingCoefficient Коэффициент от 0 до 1.
     */
//    public void setLightingCoefficient(float lightingCoefficient) {
//        this.lightingCoefficient = clamp(lightingCoefficient, 0.0f, 1.0f);
//    }

    /**
     * Устанавливает направление света.
     *
     * @param lightDirection Вектор направления света.
     */
    public void setLightDirection(Vector3f lightDirection) {
        lightDirection.normalize();
        this.lightDirection = lightDirection;
    }

    /**
     * Вычисляет матрицу модель-представление-проекция с учетом камеры и трансформаций модели.
     *
     * @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();
        int[] normalIndices = polygon.getNormalIndices().stream().mapToInt(Integer::intValue).toArray();

        Point2f[] screenPoints = new Point2f[3];
        float[] zValues = new float[3];
        Vector2f[] textureCoords = new Vector2f[3];
        Vector3f[] normals = new Vector3f[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 (useLighting && normalIndices.length == 3) {
                // Получаем нормаль вершины в локальной системе координат
                Vector3f localNormal = model.getNormals().get(normalIndices[i]);
                // Преобразуем нормаль в мировую систему координат
                Vector3f worldNormal = transformNormal(model, localNormal, mvpMatrix);
                worldNormal.normalize();
                normals[i] = worldNormal;
            }
        }

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

    /**
     * Выполняет растеризацию треугольника с наложением текстуры и освещением, обновляя графический контекст.
     *
     * @param points          Координаты точек треугольника на экране.
     * @param zValues         Значения глубины для каждой вершины.
     * @param textureCoords   Текстурные координаты для каждой вершины.
     * @param normals         Нормали для каждой вершины.
     * @param zBuffer         Z-буфер для отслеживания глубины пикселей.
     * @param graphicsContext Графический контекст для рисования.
     * @param texture         Текстура модели.
     */
    private void rasterizeTriangleWithTexture(Point2f[] points, float[] zValues, Vector2f[] textureCoords,
                                              Vector3f[] normals, 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) ((u) * (texture.getWidth() - 1)), 0, (int) texture.getWidth() - 1);
                        int texY = clamp((int) ((1 - v) * (texture.getHeight() - 1)), 0, (int) texture.getHeight() - 1);

                        Color baseColor = pixelReader.getColor(texX, texY);

                        if (useLighting && normals != null && normals.length == 3) {
                            // Интерполяция нормали
                            Vector3f interpolatedNormal = new Vector3f(
                                    lambda0 * normals[0].getX() + lambda1 * normals[1].getX() + lambda2 * normals[2].getX(),
                                    lambda0 * normals[0].getY() + lambda1 * normals[1].getY() + lambda2 * normals[2].getY(),
                                    lambda0 * normals[0].getZ() + lambda1 * normals[1].getZ() + lambda2 * normals[2].getZ()
                            );
                            interpolatedNormal.normalize();

                            // Вычисление коэффициента освещения
                            float l = Math.max(0.0f, interpolatedNormal.dot(lightDirection));

                            // Применение освещения к цвету
                            double red = baseColor.getRed() * (1 - lightingCoefficient) + baseColor.getRed() * lightingCoefficient * l;
                            double green = baseColor.getGreen() * (1 - lightingCoefficient) + baseColor.getGreen() * lightingCoefficient * l;
                            double blue = baseColor.getBlue() * (1 - lightingCoefficient) + baseColor.getBlue() * lightingCoefficient * l;

                            Color finalColor = new Color(clamp(red), clamp(green), clamp(blue), baseColor.getOpacity());
                            graphicsContext.getPixelWriter().setColor(x, y, finalColor);
                        } else {
                            // Без освещения
                            graphicsContext.getPixelWriter().setColor(x, y, baseColor);
                        }
                    }
                }
            }
        }
    }

    /**
     * Выполняет растеризацию треугольника и освещения, обновляя графический контекст.
     *
     * @param points          Координаты точек треугольника на экране.
     * @param zValues         Значения глубины для каждой вершины.
     * @param normals         Нормали для каждой вершины.
     * @param zBuffer         Z-буфер для отслеживания глубины пикселей.
     * @param graphicsContext Графический контекст для рисования.
     */
    private void rasterizeTriangle(Point2f[] points, float[] zValues, Vector3f[] normals,
                                   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);
                        Color color = Color.AQUA;

                        if (useLighting && normals != null && normals.length == 3 ) {
                            // Интерполяция нормали
                            Vector3f interpolatedNormal = new Vector3f(
                                    lambda0 * normals[0].getX() + lambda1 * normals[1].getX() + lambda2 * normals[2].getX(),
                                    lambda0 * normals[0].getY() + lambda1 * normals[1].getY() + lambda2 * normals[2].getY(),
                                    lambda0 * normals[0].getZ() + lambda1 * normals[1].getZ() + lambda2 * normals[2].getZ()
                            );
                            interpolatedNormal.normalize();

                            // Вычисление коэффициента освещения
                            float l = Math.max(0.0f, interpolatedNormal.dot(lightDirection));

                            // Применение освещения к цвету
                            double red = color.getRed() * (1 - lightingCoefficient) + color.getRed() * lightingCoefficient * l;
                            double green = color.getGreen() * (1 - lightingCoefficient) + color.getGreen() * lightingCoefficient * l;
                            double blue = color.getBlue() * (1 - lightingCoefficient) + color.getBlue() * lightingCoefficient * l;

                            Color finalColor = new Color(clamp(red), clamp(green), clamp(blue), color.getOpacity());
                            graphicsContext.getPixelWriter().setColor(x, y, finalColor);
                        } else {
                            // Без освещения
                            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 model       Модель, содержащая нормали.
     * @param localNormal Нормаль в локальной системе координат.
     * @param mvpMatrix   Матрица модель-представление-проекция.
     * @return Нормаль в мировых координатах.
     */
    private Vector3f transformNormal(Model model, Vector3f localNormal, Matrix4f mvpMatrix) {
        // Используем только модельную матрицу для трансформации нормали
        Matrix4f modelMatrix = calculateModelViewProjectionMatrix(new Camera( new Vector3f(0, 0, 100),
                new Vector3f(0, 0, 0),
                1.0F, 1, 0.01F, 100), model.getRotate(), model.getScale(), model.getTranslate());
        // Трансформируем нормаль, предположив, что mvpMatrix включает только модельные преобразования
        // В реальной ситуации нужна отдельная матрица для нормалей
        Vector3f transformedNormal = multiplyMatrix4ByVector3(mvpMatrix, localNormal);
        return transformedNormal;
    }

    /**
     * Вычисляет барицентрические координаты точки относительно треугольника.
     *
     * @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));
    }

    /**
     * Умножает матрицу 4x4 на вектор 3D и возвращает нормализованный вектор 3D.
     *
     * @param matrix Матрица 4x4.
     * @param vector Вектор 3D.
     * @return Результирующий нормализованный вектор 3D.
     */
    private Vector3f multiplyMatrix4ByVector3(Matrix4f matrix, Vector3f vector) {
        float x = matrix.getElement(0, 0) * vector.getX() +
                matrix.getElement(0, 1) * vector.getY() +
                matrix.getElement(0, 2) * vector.getZ() +
                matrix.getElement(0, 3) * 1.0f; // Добавляем компоненту w = 1

        float y = matrix.getElement(1, 0) * vector.getX() +
                matrix.getElement(1, 1) * vector.getY() +
                matrix.getElement(1, 2) * vector.getZ() +
                matrix.getElement(1, 3) * 1.0f; // Добавляем компоненту w = 1

        float z = matrix.getElement(2, 0) * vector.getX() +
                matrix.getElement(2, 1) * vector.getY() +
                matrix.getElement(2, 2) * vector.getZ() +
                matrix.getElement(2, 3) * 1.0f; // Добавляем компоненту w = 1

        float w = matrix.getElement(3, 0) * vector.getX() +
                matrix.getElement(3, 1) * vector.getY() +
                matrix.getElement(3, 2) * vector.getZ() +
                matrix.getElement(3, 3) * 1.0f; // Добавляем компоненту w = 1

        if (w != 0.0f) {
            x /= w;
            y /= w;
            z /= w;
        }

        return new Vector3f(x, y, z);
    }

    /**
     * Преобразует трёхмерную вершину в точку на экране.
     *
     * @param vertex Вершина в пространстве экрана.
     * @param width  Ширина экрана.
     * @param height Высота экрана.
     * @return Точка на экране.
     */
    private Point2f vertexToPoint(Vector3f vertex, int width, int height) {
        // Преобразование нормированных координат (-1,1) в экранные координаты
        float x = (vertex.getX() + 1) * 0.5f * width;
        float y = (1 - (vertex.getY() + 1) * 0.5f) * height; // Инвертируем Y для экранных координат
        return new Point2f(x, y);
    }
}