проверь мою корректность работ с нормалями
package com.cgvsu.model;

import com.cgvsu.math.Vector2f;
import com.cgvsu.math.Vector3f;

import java.util.ArrayList;
import java.util.List;

/**
 * Класс, представляющий 3D модель с вершинами, текстурными вершинами, нормалями и полигонами.
 */
public class Model {

    private Texture texture; // Замена Image на Texture

    private final ArrayList<Vector2f> textureVertices = new ArrayList<>();
    private final ArrayList<Vector3f> vertices = new ArrayList<>();
    private final ArrayList<Vector3f> normals = new ArrayList<>();
    private final ArrayList<Polygon> polygons = new ArrayList<>();

    private final Vector3f rotate = new Vector3f(0.0f, 0.0f, 0.0f);
    private final Vector3f scale = new Vector3f(1.0f, 1.0f, 1.0f);
    private final Vector3f translate = new Vector3f(0.0f, 0.0f, 0.0f);

    /**
     * Загружает текстуру модели из указанного файла.
     *
     * @param texturePath Путь к файлу текстуры.
     */
    public void loadTexture(String texturePath) {
        this.texture = new Texture(texturePath);


    }

    /**
     * Возвращает текстуру модели.
     *
     * @return Объект Texture, или {@code null}, если текстура не загружена.
     */
    public Texture getTexture() {
        return texture;
    }

    /**
     * Возвращает список текстурных вершин модели.
     *
     * @return Список текстурных вершин.
     */
    public ArrayList<Vector2f> getTextureVertices() {
        return textureVertices;
    }

    /**
     * Возвращает список 3D вершин модели.
     *
     * @return Список 3D вершин.
     */
    public ArrayList<Vector3f> getVertices() {
        return vertices;
    }

    /**
     * Возвращает список нормалей вершин модели.
     *
     * @return Список нормалей вершин.
     */
    public ArrayList<Vector3f> getNormals() {
        return normals;
    }

    /**
     * Возвращает список полигонов модели.
     *
     * @return Список полигонов.
     */
    public ArrayList<Polygon> getPolygons() {
        return polygons;
    }

    /**
     * Возвращает вектор поворота модели.
     *
     * @return Вектор поворота.
     */
    public Vector3f getRotate() {
        return rotate;
    }

    /**
     * Возвращает вектор масштабирования модели.
     *
     * @return Вектор масштабирования.
     */
    public Vector3f getScale() {
        return scale;
    }

    /**
     * Возвращает вектор переноса модели.
     *
     * @return Вектор переноса.
     */
    public Vector3f getTranslate() {
        return translate;
    }

    /**
     * Разбивает полигоны модели на треугольники.
     * Если у полигона больше трех вершин, применяется алгоритм триангуляции.
     */
    public void triangulate() {
        ArrayList<Polygon> triangulatedPolygons = new ArrayList<>();

        for (Polygon polygon : polygons) {
            if (polygon.isTriangle()) {
                triangulatedPolygons.add(polygon);
            } else {
                List<Integer> vertexIndices = polygon.getVertexIndices();
                List<Integer> textureIndices = polygon.getTextureVertexIndices();
                List<Integer> normalIndices = polygon.getNormalIndices();

                for (int i = 1; i < vertexIndices.size() - 1; i++) {
                    Polygon triangle = new Polygon();
                    // Вершины
                    triangle.addVertexIndex(vertexIndices.get(0));
                    triangle.addVertexIndex(vertexIndices.get(i));
                    triangle.addVertexIndex(vertexIndices.get(i + 1));

                    // Текстурные вершины
                    if (!textureIndices.isEmpty()) {
                        if (i + 1 < textureIndices.size()) {
                            triangle.addTextureVertexIndex(textureIndices.get(0));
                            triangle.addTextureVertexIndex(textureIndices.get(i));
                            triangle.addTextureVertexIndex(textureIndices.get(i + 1));
                        } else {
                            triangle.addTextureVertexIndex(textureIndices.get(0));
                            triangle.addTextureVertexIndex(textureIndices.get(i % textureIndices.size()));
                            triangle.addTextureVertexIndex(textureIndices.get((i + 1) % textureIndices.size()));
                        }
                    }

                    // Нормали
                    if (!normalIndices.isEmpty()) {
                        if (i + 1 < normalIndices.size()) {
                            triangle.addNormalIndex(normalIndices.get(0));
                            triangle.addNormalIndex(normalIndices.get(i));
                            triangle.addNormalIndex(normalIndices.get(i + 1));
                        } else {
                            triangle.addNormalIndex(normalIndices.get(0));
                            triangle.addNormalIndex(normalIndices.get(i % normalIndices.size()));
                            triangle.addNormalIndex(normalIndices.get((i + 1) % normalIndices.size()));
                        }
                    }

                    triangulatedPolygons.add(triangle);
                }
            }
        }
        polygons.clear();
        polygons.addAll(triangulatedPolygons);
    }

    /**
     * Вычисляет нормали для каждого полигона и усредняет их для каждой вершины.
     */
    public void calculateNormals() {
        normals.clear();
        for (int i = 0; i < vertices.size(); i++) {
            normals.add(new Vector3f(0, 0, 0));
        }

        for (Polygon polygon : polygons) {
            List<Integer> vertexIndices = polygon.getVertexIndices();

            Vector3f v0 = vertices.get(vertexIndices.get(0));
            Vector3f v1 = vertices.get(vertexIndices.get(1));
            Vector3f v2 = vertices.get(vertexIndices.get(2));

            Vector3f edge1 = v1.sub(v0);
            Vector3f edge2 = v2.sub(v0);

            Vector3f faceNormal = edge1;
            faceNormal.cross(edge2).normalize();

            for (int index : vertexIndices) {
                normals.set(index, normals.get(index).add(faceNormal));
            }
        }

        for (int i = 0; i < normals.size(); i++) {

            Vector3f normal = normals.get(i);
            normal.normalize();
            normals.set(i, normal);
        }
    }

    /**
     * Устанавливает текстуру модели.
     *
     * @param texture Объект Texture.
     */
    public void setTexture(Texture texture) {
        this.texture = texture;
    }

    public void clearTexture() {
        this.texture = null;
    }
}




    private void startRendering() {
        if (timeline == null) {
            timeline = new Timeline(new KeyFrame(Duration.millis(15), event -> render()));
            timeline.setCycleCount(Animation.INDEFINITE);
            timeline.play();
        }
    }

    private void render() {
        double width = canvas.getWidth();
        double height = canvas.getHeight();

        canvas.getGraphicsContext2D().clearRect(0, 0, width, height);
        camera.setAspectRatio((float) (width / height));

        if (mesh != null) {
            RenderEngine renderEngine = new RenderEngine();
            renderEngine.render(canvas.getGraphicsContext2D(), camera, mesh, (int) width, (int) height);
        }
    }




    private void addModelControlButtons() {
        counterModels++;

        // Кнопка для выбора модели
        Button modelButton = new Button("Model " + counterModels);
        modelButton.setOnAction(event -> selectActiveModel(counterModels));

        // Кнопка для добавления текстуры
        Button addTextureButton = new Button("Add Texture");
        addTextureButton.setOnAction(event -> addTexture(counterModels));

        // Кнопка для удаления текстуры
        Button removeTextureButton = new Button("Remove Texture");
        removeTextureButton.setOnAction(event -> removeTexture(counterModels));

        // Кнопка для удаления модели
        Button deleteModelButton = new Button("Delete");
        deleteModelButton.setOnAction(event -> deleteModel(counterModels, modelButton));

        // Группа кнопок для одной модели
        HBox modelControls = new HBox(5, modelButton, addTextureButton, removeTextureButton, deleteModelButton);

        // Добавляем HBox в VBox (modelsManager)
        modelsManager.getChildren().add(modelControls);
    }

    private void selectActiveModel(int modelId) {
        // Установите идентификатор модели как активной
        // Здесь можно изменить переменную, указывающую на выбранную модель для рендеринга
    }

    private void addTexture(int modelId) {
        FileChooser fileChooser = new FileChooser();
        fileChooser.getExtensionFilters().add(new FileChooser.ExtensionFilter("Model (*.png)", "*.png"));
        fileChooser.setTitle("Load Textrure");

        File initialDirectory = new File("3DModels");
        if (initialDirectory.exists()) {
            fileChooser.setInitialDirectory(initialDirectory);
        }

        File file = fileChooser.showOpenDialog((Stage) canvas.getScene().getWindow());
        if (file == null) {
            return;
        }
        String filePath = file.getAbsolutePath();

        mesh.loadTexture(filePath);

    }
    public void loadModel() {
        FileChooser fileChooser = new FileChooser();
        fileChooser.getExtensionFilters().add(new FileChooser.ExtensionFilter("Model (*.obj)", "*.obj"));
        fileChooser.setTitle("Load Model");

        File initialDirectory = new File("3DModels");
        if (initialDirectory.exists()) {
            fileChooser.setInitialDirectory(initialDirectory);
        }

        File file = fileChooser.showOpenDialog((Stage) canvas.getScene().getWindow());
        if (file == null) {
            return;
        }

        try {
            String fileContent = Files.readString(Path.of(file.getAbsolutePath()));
            mesh = ObjReader.read(fileContent);
            mesh.triangulate();
            mesh.calculateNormals();
            addModelControlButtons();  // Добавляем кнопки для управления моделью
            startRendering();          // Запускаем рендеринг
        } catch (IOException e) {
            showErrorAlert("Ошибка загрузки модели", null, "Не удалось загрузить модель: " + e.getMessage());
        }
    }


и добавь примитивную модель освещения со сглаживанием нормалей

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.paint.Color;

import static com.cgvsu.render_engine.GraphicConveyor.multiplyMatrix4ByVector3;
import static com.cgvsu.render_engine.GraphicConveyor.scaleRotateTranslate;

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

    private boolean enableFill = true; // Заливка полигона
    private boolean enableWireframe = false; // Отрисовка каркаса
    private boolean enableTexture = false; // Наложение текстур


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

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

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






    /**
     * Вычисляет матрицу модель-представление-проекция с учетом камеры и трансформаций модели.
     *
     * @param camera    Объект камеры, содержащий матрицы вида и проекции.
     * @param rotate    Вектор поворота модели.
     * @param scale     Вектор масштабирования модели.
     * @param translate Вектор переноса модели.
     * @return Матрица модель-представление-проекция.
     */
    private Matrix4f calculateModelViewProjectionMatrix(Camera camera, Vector3f rotate, Vector3f scale, Vector3f translate) {
        Matrix4f modelMatrix = 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 Высота экрана.
     * @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());
        Texture texture = model.getTexture();

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

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

    /**
     * Рендерит отдельный многоугольник на графическом контексте.
     *
     * @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, Texture texture) {

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

        Point2f[] screenPoints = new Point2f[vertexIndices.length];
        float[] zValues = new float[vertexIndices.length];
        Vector2f[] textureCoords = new Vector2f[vertexIndices.length];
        Vector3f[] normals = new Vector3f[vertexIndices.length];

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

            if (enableTexture && textureIndices.length == vertexIndices.length && texture != null) {
                Vector2f texCoord = model.getTextureVertices().get(textureIndices[i]);
                textureCoords[i] = texCoord;
            }

            // Retrieve the normal for the current vertex
            Vector3f temp = model.getNormals().get(vertexIndices[i]);
            temp.normalize();
            normals[i] = temp;
        }

        if (enableTexture && textureCoords.length == vertexIndices.length && texture != null) {
            rasterizeTriangleWithTexture(screenPoints, zValues, textureCoords, zBuffer, graphicsContext, texture, normals);
        } else if (enableFill) {
            rasterizeTriangle(screenPoints, normals, zValues, zBuffer, graphicsContext);
        }
    }

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

        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;

                        // Interpolate texture coordinates
                        float u = lambda0 * textureCoords[0].getX() + lambda1 * textureCoords[1].getX() + lambda2 * textureCoords[2].getX();
                        float v = lambda0 * textureCoords[0].getY() + lambda1 * textureCoords[1].getY() + lambda2 * textureCoords[2].getY();

                        // Fetch the color from the texture
                        Color textureColor = texture.getColor(u, v);



                        // Set the final color to the pixel
                        graphicsContext.getPixelWriter().setColor(x, y, textureColor);
                    }
                }
            }
        }
    }

    /**
     * Выполняет растеризацию треугольника и обновляет графический контекст.
     * Добавлено простейшее диффузное освещение.
     *
     * @param points          Координаты точек треугольника на экране.
     * @param normals         Нормали для каждой вершины треугольника.
     * @param zValues         Значения глубины для каждой вершины.
     * @param zBuffer         Z-буфер для отслеживания глубины пикселей.
     * @param graphicsContext Графический контекст для рисования.
     */
    private void rasterizeTriangle(Point2f[] points, Vector3f[] normals, float[] zValues, float[][] zBuffer, GraphicsContext graphicsContext) {


        // Determine the bounding box of the triangle
        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++) {
                // Calculate barycentric coordinates for the current pixel
                float[] barycentric = calculateBarycentricCoordinates(points, x, y);
                float lambda0 = barycentric[0];
                float lambda1 = barycentric[1];
                float lambda2 = barycentric[2];

                // Check if the pixel is inside the triangle
                if (isInsideTriangle(lambda0, lambda1, lambda2)) {
                    // Interpolate the depth value
                    float depth = interpolate(lambda0, lambda1, lambda2, zValues);

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



                        Color baseColor = Color.LIGHTGRAY;


                        graphicsContext.getPixelWriter().setColor(x, y, baseColor);
                    }
                }
            }
        }

    }

    /**
     * Interpolates the color of a pixel inside the triangle based on barycentric coordinates.
     *
     * @param lambda0 Barycentric coordinate λ0.
     * @param lambda1 Barycentric coordinate λ1.
     * @param lambda2 Barycentric coordinate λ2.
     * @param colorA  Color at the first vertex.
     * @param colorB  Color at the second vertex.
     * @param colorC  Color at the third vertex.
     * @return Interpolated Color.
     */
    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);
    }

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

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

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

            // Draw lines between vertices
            for (int i = 0; i < vertexIndices.length; i++) {
                int next = (i + 1) % vertexIndices.length;
                drawLine(graphicsContext, screenPoints[i], screenPoints[next], zValues[i], zValues[next], zBuffer);
            }
        }
    }

    /**
     * Рисует линию между двумя точками с учетом Z-буфера.
     *
     * @param gc      Графический контекст.
     * @param p1      Первая точка.
     * @param p2      Вторая точка.
     * @param z1      Глубина первой точки.
     * @param z2      Глубина второй точки.
     * @param zBuffer Z-буфер.
     */
    private void drawLine(GraphicsContext gc, Point2f p1, Point2f p2, float z1, float z2, float[][] zBuffer) {
        int x0 = (int) p1.getX();
        int y0 = (int) p1.getY();
        int x1 = (int) p2.getX();
        int y1 = (int) p2.getY();

        int dx = Math.abs(x1 - x0);
        int dy = Math.abs(y1 - y0);
        int sx = x0 < x1 ? 1 : -1;
        int sy = y0 < y1 ? 1 : -1;
        int err = dx - dy;

        while (true) {
            if (x0 >= 0 && x0 < zBuffer.length && y0 >= 0 && y0 < zBuffer[0].length) {
                float depth = interpolateDepth(x0, y0, p1, p2, z1, z2);
                if (depth < zBuffer[x0][y0]) {
                    zBuffer[x0][y0] = depth;
                    gc.getPixelWriter().setColor(x0, y0, Color.BLACK);
                }
            }

            if (x0 == x1 && y0 == y1) break;
            int e2 = 2 * err;
            if (e2 > -dy) {
                err -= dy;
                x0 += sx;
            }
            if (e2 < dx) {
                err += dx;
                y0 += sy;
            }
        }
    }

    /**
     * Интерполирует глубину для пикселя в линии.
     *
     * @param x  Координата X пикселя.
     * @param y  Координата Y пикселя.
     * @param p1 Первая точка линии.
     * @param p2 Вторая точка линии.
     * @param z1 Глубина первой точки.
     * @param z2 Глубина второй точки.
     * @return Интерполированная глубина.
     */
    private float interpolateDepth(int x, int y, Point2f p1, Point2f p2, float z1, float z2) {
        float totalDistance = (float) Math.hypot(p2.getX() - p1.getX(), p2.getY() - p1.getY());
        float currentDistance = (float) Math.hypot(x - p1.getX(), y - p1.getY());
        return z1 + (z2 - z1) * (currentDistance / totalDistance);
    }

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

    /**
     * Применяет трансформацию к вершине модели.
     *
     * @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 (float) (lambda0 * values[0] + lambda1 * values[1] + lambda2 * values[2]);
    }

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

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

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


}