в моем коде не совсем корректно закрашиваются треугольники. пикслеи просто черные. Я не знаю, с чем это может быть связано. Причём плохо отрисовываются именно 3д модели , возно что-то не так с z буффером или многопоточкой. Причем закрашиваются неверно лишь некоторые треугольниуки. У них как-бы такие черные пятна

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 = true; // Заливка полигонов
    private boolean enableInterpolatedLighting = true; // Освещение с интерполяцией нормалей
    private boolean enableFlatLighting = false; // Плоское освещение
    private boolean enableTextureMapping = true; // Наложение текстур

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

    // Одномерный Z-буфер и цветовой буфер
    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));
            // Здесь предполагается, что multiplyMatrix4ByVector3 выполняет перспективное деление (x/w, y/w, z/w)
            // Если это не так, необходимо добавить деление на w:
            // float w = ...; // Получить w из результата умножения
            // transformedVertices[i] = new Vector3f(x/w, y/w, z/w);
        }
    }

    /**
     * Выполняет рендеринг 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();
        IntBuffer intBuffer = IntBuffer.wrap(colorBuffer);
        pixelWriter.setPixels(0, 0, width, height, PixelFormat.getIntArgbInstance(), colorBuffer, 0, width);

        graphicsContext.drawImage(writableImage, 0, 0);

        if (enableWireframe) {
            renderWireframe(graphicsContext, model, mvpMatrix, width, height);
        }
    }

    /**
     * Проверяет, находится ли полигон внутри объема видимости камеры.
     *
     * @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) {
        // Вычисление границ треугольника
        float minX = Math.max(0, (float) Math.floor(Math.min(p0.getX(), Math.min(p1.getX(), p2.getX()))));
        float maxX = Math.min(bufferWidth - 1, (float) Math.ceil(Math.max(p0.getX(), Math.max(p1.getX(), p2.getX()))));
        float minY = Math.max(0, (float) Math.floor(Math.min(p0.getY(), Math.min(p1.getY(), p2.getY()))));
        float maxY = Math.min(bufferHeight - 1, (float) 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 = (int) minY; y <= (int) maxY; y++) {
            for (int x = (int) minX; x <= (int) 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 = computePixelColor(lambda0, lambda1, lambda2,
                                n0, n1, n2, tex0, tex1, tex2, depth, faceNormal, texture);

                        // Обновление буферов
                        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 depth        Глубина пикселя.
     * @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,
                                  float depth, 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 = 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;
    }

    /**
     * Выполняет отрисовку каркаса модели (wireframe) с использованием преобразованных вершин.
     *
     * @param graphicsContext Графический контекст для рисования.
     * @param model           Модель для рендеринга.
     * @param mvpMatrix       Матрица модель-представление-проекция.
     * @param width           Ширина области рендеринга.
     * @param height          Высота области рендеринга.
     */
    private void renderWireframe(GraphicsContext graphicsContext, Model model, Matrix4f mvpMatrix, int width, int height) {
        // Предварительно преобразованные вершины уже существуют, используем их
        for (Polygon polygon : model.getPolygons()) {
            int[] vertexIndices = polygon.getVertexIndices().stream().mapToInt(Integer::intValue).toArray();

            Point2f[] screenPoints = new Point2f[vertexIndices.length];

            for (int i = 0; i < vertexIndices.length; i++) {
                Vector3f transformedVertex = transformedVertices[vertexIndices[i]];
                screenPoints[i] = vertexToPoint(transformedVertex, width, height);
            }

            // Отрисовка линий между вершинами
            for (int i = 0; i < vertexIndices.length; i++) {
                int next = (i + 1) % vertexIndices.length;
                graphicsContext.strokeLine(
                        screenPoints[i].getX(), screenPoints[i].getY(),
                        screenPoints[next].getX(), screenPoints[next].getY()
                );
            }
        }
    }

    /**
     * Преобразует вершину в экранные координаты.
     *
     * @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));
    }

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