текстура накладывается зеркально
 /**
     * Оптимизированная растеризация треугольника с возможностью отрисовки заполненной области и/или краев.
     *
     * @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, depth, 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 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;
    }