добавь возможность рисовать полигональную сетку используя встроенные методы (javafx)
создай отдельный булевый флаг
package com.cgvsu.render_engine;

import com.cgvsu.math.Matrix4f;
import com.cgvsu.math.Point2f;
import com.cgvsu.math.Vector2f;
import com.cgvsu.math.Vector3f;
import com.cgvsu.model.Model;
import com.cgvsu.model.Polygon;
import com.cgvsu.model.Texture;
import javafx.scene.canvas.GraphicsContext;
import javafx.scene.image.PixelFormat;
import javafx.scene.image.PixelWriter;
import javafx.scene.image.WritableImage;
import javafx.scene.paint.Color;

import java.nio.IntBuffer;
import java.util.Arrays;
import java.util.concurrent.ForkJoinPool;

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

    private boolean enableWireframe = false; // Отрисовка каркаса модели
    private boolean enableFillPolygon = false; // Заливка полигонов
    private boolean enableInterpolatedLighting = false; // Освещение с интерполяцией нормалей
    private boolean enableFlatLighting = false; // Плоское освещение
    private boolean enableTextureMapping = false; // Наложение текстур

    private float lightingCoefficient = 0.9f; // Коэффициент освещения (k)
    private Vector3f lightDirection = new Vector3f(1, 1, 1); // Направление света

    private float[] zBuffer;
    private int[] colorBuffer;
    private int bufferWidth;
    private int bufferHeight;


    private Vector3f[] transformedVertices;

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

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

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

    /**
     * Устанавливает режим плоского освещения без интерполяции нормалей.
     *
     * @param enableFlatLighting true для включения, false для отключения.
     */
    public void setEnableFlatLighting(boolean enableFlatLighting) {
        this.enableFlatLighting = enableFlatLighting;
    }

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

    /**
     * Вычисляет матрицу модель-представление-проекция с учетом камеры и трансформаций модели.
     *
     * @param camera    Объект камеры, содержащий матрицы вида и проекции.
     * @param rotate    Вектор поворота модели.
     * @param scale     Вектор масштабирования модели.
     * @param translate Вектор переноса модели.
     * @return Матрица модель-представление-проекция.
     */
    private Matrix4f calculateModelViewProjectionMatrix(Camera camera, Vector3f rotate, Vector3f scale, Vector3f translate) {
        Matrix4f modelMatrix = GraphicConveyor.scaleRotateTranslate(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 Высота экрана.
     */
    private void initializeBuffers(int width, int height) {
        if (zBuffer == null || width != bufferWidth || height != bufferHeight) {
            bufferWidth = width;
            bufferHeight = height;
            zBuffer = new float[width * height];
            colorBuffer = new int[width * height];
            transformedVertices = new Vector3f[0]; // Инициализируем пустым массивом
        }
        Arrays.fill(zBuffer, Float.MAX_VALUE);
        Arrays.fill(colorBuffer, 0); // Чёрный цвет по умолчанию
    }

    /**
     * Предварительно преобразует все вершины модели.
     *
     * @param model     Модель для рендеринга.
     * @param mvpMatrix Матрица модель-представление-проекция.
     */
    private void transformAllVertices(Model model, Matrix4f mvpMatrix) {
        if (transformedVertices.length != model.getVertices().size()) {
            transformedVertices = new Vector3f[model.getVertices().size()];
        }
        for (int i = 0; i < model.getVertices().size(); i++) {
            transformedVertices[i] = GraphicConveyor.multiplyMatrix4ByVector3(mvpMatrix, model.getVertices().get(i));

        }
    }

    /**
     * Выполняет рендеринг 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) {

        initializeBuffers(width, height);
        Matrix4f mvpMatrix = calculateModelViewProjectionMatrix(camera, model.getRotate(), model.getScale(), model.getTranslate());
        transformAllVertices(model, mvpMatrix);
        Texture texture = model.getTexture();

        ForkJoinPool.commonPool().submit(() ->
                model.getPolygons().parallelStream()
                        .filter(this::isPolygonInFrustum)
                        .forEach(polygon -> {
                            renderPolygonOptimized(polygon, model, texture);
                        })
        ).join();

        WritableImage writableImage = new WritableImage(width, height);
        PixelWriter pixelWriter = writableImage.getPixelWriter();
        pixelWriter.setPixels(0, 0, width, height, PixelFormat.getIntArgbInstance(), colorBuffer, 0, width);

        graphicsContext.drawImage(writableImage, 0, 0);


    }

    /**
     * Проверяет, находится ли полигон внутри объема видимости камеры.
     *
     * @param polygon Проверяемый полигон.
     * @return true, если полигон видим, иначе false.
     */
    private boolean isPolygonInFrustum(Polygon polygon) {
        boolean allXLess = true, allXGreater = true;
        boolean allYLess = true, allYGreater = true;
        boolean allZLess = true, allZGreater = true;

        for (int vertexIndex : polygon.getVertexIndices()) {
            Vector3f vertex = transformedVertices[vertexIndex];
            float x = vertex.getX();
            float y = vertex.getY();
            float z = vertex.getZ();

            if (x >= -1) {
                allXLess = false;
            }
            if (x <= 1) {
                allXGreater = false;
            }
            if (y >= -1) {
                allYLess = false;
            }
            if (y <= 1) {
                allYGreater = false;
            }
            if (z >= -1) {
                allZLess = false;
            }
            if (z <= 1) {
                allZGreater = false;
            }
        }

        // Если все вершины полигонов находятся за одной из плоскостей NDC, полигон не видим
        if (allXLess || allXGreater || allYLess || allYGreater || allZLess || allZGreater) {
            return false; // Не отрисовывать полигон
        }
        return true;
    }

    /**
     * Оптимизированный метод рендеринга отдельного многоугольника.
     *
     * @param polygon Многоугольник для рендеринга.
     * @param model   Модель, содержащая вершины.
     * @param texture Текстура модели.
     */
    private void renderPolygonOptimized(Polygon polygon, Model model, Texture texture) {
        int[] vertexIndices = polygon.getVertexIndices().stream().mapToInt(Integer::intValue).toArray();
        int[] textureIndices = polygon.getTextureVertexIndices().stream().mapToInt(Integer::intValue).toArray();

        if (vertexIndices.length < 3) return; // Пропуск неполных многоугольников

        for (int i = 1; i < vertexIndices.length - 1; i++) {
            int idx0 = vertexIndices[0];
            int idx1 = vertexIndices[i];
            int idx2 = vertexIndices[i + 1];

            Vector3f v0 = transformedVertices[idx0];
            Vector3f v1 = transformedVertices[idx1];
            Vector3f v2 = transformedVertices[idx2];

            // Экранные координаты
            Point2f p0 = vertexToPoint(v0, bufferWidth, bufferHeight);
            Point2f p1 = vertexToPoint(v1, bufferWidth, bufferHeight);
            Point2f p2 = vertexToPoint(v2, bufferWidth, bufferHeight);

            // Задание текстурных координат
            Vector2f tex0 = null, tex1 = null, tex2 = null;
            if (enableTextureMapping && texture != null && textureIndices.length >= 3) {
                tex0 = model.getTextureVertices().get(textureIndices[0]);
                tex1 = model.getTextureVertices().get(textureIndices[i]);
                tex2 = model.getTextureVertices().get(textureIndices[i + 1]);
            }

            // Нормали вершин
            Vector3f n0 = null, n1 = null, n2 = null;
            if (enableInterpolatedLighting || enableFlatLighting) {
                n0 = model.getNormals().get(idx0);
                n1 = model.getNormals().get(idx1);
                n2 = model.getNormals().get(idx2);
            }


            rasterizeTriangleOptimized(p0, p1, p2, v0.getZ(), v1.getZ(), v2.getZ(),
                    n0, n1, n2, tex0, tex1, tex2, texture);

        }
    }

    /**
     * Оптимизированная растеризация треугольника с возможностью отрисовки заполненной области и/или краев.
     *
     * @param p0      Первая вершина треугольника.
     * @param p1      Вторая вершина треугольника.
     * @param p2      Третья вершина треугольника.
     * @param z0      Глубина первой вершины.
     * @param z1      Глубина второй вершины.
     * @param z2      Глубина третьей вершины.
     * @param n0      Нормаль первой вершины.
     * @param n1      Нормаль второй вершины.
     * @param n2      Нормаль третьей вершины.
     * @param tex0    Текстурная координата первой вершины.
     * @param tex1    Текстурная координата второй вершины.
     * @param tex2    Текстурная координата третьей вершины.
     * @param texture Текстура модели.
     */
    private void rasterizeTriangleOptimized(Point2f p0, Point2f p1, Point2f p2,
                                            float z0, float z1, float z2,
                                            Vector3f n0, Vector3f n1, Vector3f n2,
                                            Vector2f tex0, Vector2f tex1, Vector2f tex2,
                                            Texture texture) {
        // Пороговое значение для определения края треугольника
        final float EDGE_THRESHOLD = 0.03f; // Настраиваемое значение

        // Вычисление границ треугольника
        int minX = (int) Math.max(0, Math.floor(Math.min(p0.getX(), Math.min(p1.getX(), p2.getX()))));
        int maxX = (int) Math.min(bufferWidth - 1, Math.ceil(Math.max(p0.getX(), Math.max(p1.getX(), p2.getX()))));
        int minY = (int) Math.max(0, Math.floor(Math.min(p0.getY(), Math.min(p1.getY(), p2.getY()))));
        int maxY = (int) Math.min(bufferHeight - 1, Math.ceil(Math.max(p0.getY(), Math.max(p1.getY(), p2.getY()))));

        // Предварительные вычисления для барицентрических координат
        float denom = ((p1.getY() - p2.getY()) * (p0.getX() - p2.getX()) +
                (p2.getX() - p1.getX()) * (p0.getY() - p2.getY()));

        if (denom == 0) return; // Избегаем деления на ноль

        float invDenom = 1.0f / denom;

        // Предварительные значения для освещения
        Vector3f faceNormal = null;
        if (enableFlatLighting) {
            faceNormal = calculateFaceNormal(n0, n1, n2);
            faceNormal.normalize();
        }

        // Итерация по пикселям треугольника
        for (int y = minY; y <= maxY; y++) {
            for (int x = minX; x <= maxX; x++) {
                // Барицентрические координаты
                float lambda0 = ((p1.getY() - p2.getY()) * (x - p2.getX()) +
                        (p2.getX() - p1.getX()) * (y - p2.getY())) * invDenom;
                float lambda1 = ((p2.getY() - p0.getY()) * (x - p2.getX()) +
                        (p0.getX() - p2.getX()) * (y - p2.getY())) * invDenom;
                float lambda2 = 1.0f - lambda0 - lambda1;

                // Проверка, находится ли пиксель внутри треугольника
                if (lambda0 >= 0 && lambda1 >= 0 && lambda2 >= 0) {
                    int index = y * bufferWidth + x;
                    float depth = lambda0 * z0 + lambda1 * z1 + lambda2 * z2;

                    if (depth < zBuffer[index]) {
                        int color = 0; // Инициализация цвет

                        boolean isEdgePixel = false;
                        if (enableWireframe) {
                            if (lambda0 < EDGE_THRESHOLD || lambda1 < EDGE_THRESHOLD || lambda2 < EDGE_THRESHOLD) {
                                isEdgePixel = true;
                                color = 0xFF000000; // Чёрный цвет в формате ARGB
                            }
                        }

                        if (enableFillPolygon && !isEdgePixel) {
                            // Вычисление цвета пикселя, если это не краевое и заливка включена
                            color = computePixelColor(lambda0, lambda1, lambda2,
                                    n0, n1, n2, tex0, tex1, tex2, faceNormal, texture);
                        }

                        // Обновление Z-буфера и цветового буфера только если цвет был установлен
                        if (isEdgePixel || (enableFillPolygon && color != 0) || enableWireframe) {
                            zBuffer[index] = depth;
                            colorBuffer[index] = color;
                        }
                    }
                }
            }
        }
    }



    /**
     * Вычисляет цвет пикселя с учетом текстурирования и освещения.
     *
     * @param lambda0    Барицентрическая координата λ0.
     * @param lambda1    Барицентрическая координата λ1.
     * @param lambda2    Барицентрическая координата λ2.
     * @param n0         Нормаль первой вершины.
     * @param n1         Нормаль второй вершины.
     * @param n2         Нормаль третьей вершины.
     * @param tex0       Текстурная координата первой вершины.
     * @param tex1       Текстурная координата второй вершины.
     * @param tex2       Текстурная координата третьей вершины.
     * @param faceNormal Нормаль грани для плоского освещения.
     * @param texture    Текстура модели.
     * @return Цвет пикселя в формате ARGB.
     */
    private int computePixelColor(float lambda0, float lambda1, float lambda2,
                                  Vector3f n0, Vector3f n1, Vector3f n2,
                                  Vector2f tex0, Vector2f tex1, Vector2f tex2,
                                  Vector3f faceNormal, Texture texture) {
        // Базовый цвет
        float r = 0.0f, g = 0.0f, b = 0.0f;

        // Текстурирование
        if (enableTextureMapping && texture != null && tex0 != null && tex1 != null && tex2 != null) {
            float u = 1.0f - (lambda0 * tex0.getX() + lambda1 * tex1.getX() + lambda2 * tex2.getX());
            float v = (lambda0 * tex0.getY() + lambda1 * tex1.getY() + lambda2 * tex2.getY());
            Color texColor = texture.getColor(u, v);
            r = (float) texColor.getRed();
            g = (float) texColor.getGreen();
            b = (float) texColor.getBlue();
        } else {
            // Если текстуры нет, используем базовый цвет (например, серый)
            r = 0.5f;
            g = 0.5f;
            b = 0.5f;
        }

        // Освещение
        if (enableInterpolatedLighting || enableFlatLighting) {
            Vector3f normal;
            if (enableInterpolatedLighting && n0 != null && n1 != null && n2 != null) {
                normal = interpolateNormal(lambda0, lambda1, lambda2, n0, n1, n2);
            } else if (enableFlatLighting && faceNormal != null) {
                normal = faceNormal;
            } else {
                normal = new Vector3f(0, 0, 1); // Нормаль по умолчанию
            }

            // Диффузное освещение
            float diffuse = Math.max(0, normal.dot(lightDirection));
            float brightness = lightingCoefficient * diffuse;

            // Применение освещения
            r *= brightness;
            g *= brightness;
            b *= brightness;

            // Ограничение значений до [0,1]
            r = clamp(r);
            g = clamp(g);
            b = clamp(b);
        }

        // Преобразование цвета в формат ARGB
        int argb = ((255 & 0xFF) << 24) | // Альфа
                ((int) (clamp(r) * 255) << 16) | // Красный
                ((int) (clamp(g) * 255) << 8) |  // Зеленый
                ((int) (clamp(b) * 255));       // Синий
        return argb;
    }

    /**
     * Интерполирует нормаль на основе барицентрических координат.
     *
     * @param lambda0 Барицентрическая координата λ0.
     * @param lambda1 Барицентрическая координата λ1.
     * @param lambda2 Барицентрическая координата λ2.
     * @param n0      Нормаль первой вершины.
     * @param n1      Нормаль второй вершины.
     * @param n2      Нормаль третьей вершины.
     * @return Интерполированная нормаль.
     */
    private Vector3f interpolateNormal(float lambda0, float lambda1, float lambda2,
                                       Vector3f n0, Vector3f n1, Vector3f n2) {
        Vector3f interpolated = new Vector3f(
                lambda0 * n0.getX() + lambda1 * n1.getX() + lambda2 * n2.getX(),
                lambda0 * n0.getY() + lambda1 * n1.getY() + lambda2 * n2.getY(),
                lambda0 * n0.getZ() + lambda1 * n1.getZ() + lambda2 * n2.getZ()
        );
        interpolated.normalize();
        return interpolated;
    }

    /**
     * Вычисляет нормаль грани для плоского освещения.
     *
     * @param n0 Нормаль первой вершины.
     * @param n1 Нормаль второй вершины.
     * @param n2 Нормаль третьей вершины.
     * @return Средняя нормаль грани.
     */
    private Vector3f calculateFaceNormal(Vector3f n0, Vector3f n1, Vector3f n2) {
        Vector3f averageNormal = new Vector3f(
                (n0.getX() + n1.getX() + n2.getX()) / 3,
                (n0.getY() + n1.getY() + n2.getY()) / 3,
                (n0.getZ() + n1.getZ() + n2.getZ()) / 3
        );
        averageNormal.normalize();
        return averageNormal;
    }

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

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


}