перепиши с использованем

PixelBuffer CPU
The PixelBuffer API has been introduced in JavaFX 13 and allows drawing into a WritableImage without copying the pixel data. Unlike the Canvas API, this buffer does not provide any means of rasterization. In other words, the developer is responsible for converting shapes into pixels, which is a significant limitation of this approach. In the following sample code, there are two major differences, when compared to the Canvas API. First, colors are represented as a single ARGB int and second, we need to manually update the buffer once rendering is done. Sample code:

// set up pixel buffer
IntBuffer buffer = IntBuffer.allocate(width * height);
int[] pixels = buffer.array();
PixelBuffer<IntBuffer> pixelBuffer = new PixelBuffer<>(width, height, buffer, PixelFormat.getIntArgbPreInstance());
WritableImage image = new WritableImage(pixelBuffer);
// draw a single _red_ pixel
int alpha = 255;
int red = 255;
int green = 0;
int blue = 0;
int colorARGB = alpha << 24 | red << 16 | green << 8 | blue;
pixels[(x % width) + (y * width)] = colorARGB;
// tell the buffer that the entire area needs redrawing
pixelBuffer.updateBuffer(b -> null);

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; // Наложение текстур
    private boolean enableLighting = false;


    private double lightingCoefficient = 0.5; // Коэффициент освещения (k)// Освещение

    /**
     * Устанавливает режим заливки.
     *
     * @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 enableLighting true для включения, false для отключения.
     */
    public void setEnableLighting(boolean enableLighting) {
        this.enableLighting = enableLighting;
    }

    /**
     * Вычисляет матрицу модель-представление-проекция с учетом камеры и трансформаций модели.
     *
     * @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());

        lightDirection  = new Vector3f(camera.getPosition().getX(),camera.getPosition().getY(),camera.getPosition().getZ());

        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[3];
        float[] zValues = new float[3];
        Vector2f[] textureCoords = new Vector2f[3];
        Vector3f[] vertexNormals = 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();

            // Получаем нормаль для текущей вершины
            vertexNormals[i] = model.getNormals().get(vertexIndices[i]);

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

        if (enableTexture && textureCoords[0] != null) {
            rasterizeTriangleWithTexture(screenPoints, zValues, textureCoords, zBuffer, graphicsContext, texture);
        } else if (enableFill) {
            rasterizeTriangle(screenPoints, vertexNormals, zValues, zBuffer, graphicsContext);
        }
    }


    /**
     * Выполняет растеризацию треугольника с наложением текстуры и обновляет графический контекст.
     *
     * @param points          Координаты точек треугольника на экране.
     * @param zValues         Значения глубины для каждой вершины.
     * @param textureCoords   Текстурные координаты для каждой вершины.
     * @param zBuffer         Z-буфер для отслеживания глубины пикселей.
     * @param graphicsContext Графический контекст для рисования.
     * @param texture         Текстура модели.
     */
    private void rasterizeTriangleWithTexture(Point2f[] points, float[] zValues, Vector2f[] textureCoords,
                                              float[][] zBuffer, GraphicsContext graphicsContext, Texture texture) {
        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;

                        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()});

                        Color color = texture.getColor(u, v);
                        graphicsContext.getPixelWriter().setColor(x, y, color);
                    }
                }
            }
        }
    }


    private Vector3f lightDirection;

    private void rasterizeTriangle(Point2f[] points, Vector3f[] normals, float[] zValues, float[][] zBuffer, GraphicsContext graphicsContext) {
        lightDirection.normalize();

        // Находим границы треугольника
        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);

                if (isInsideTriangle(barycentric[0], barycentric[1], barycentric[2])) {
                    float depth = interpolate(barycentric[0], barycentric[1], barycentric[2], zValues);

                    if (depth < zBuffer[x][y]) {

                        // Интерполируем нормаль на основе барицентрических координат
                        Vector3f interpolatedNormal = new Vector3f();

                        Vector3f temp1 = new Vector3f(normals[0].getX(), normals[0].getY(), normals[0].getZ());
                        temp1.multiply(barycentric[0]);

                        Vector3f temp2 = new Vector3f(normals[1].getX(), normals[1].getY(), normals[1].getZ());
                        temp2.multiply(barycentric[1]);

                        Vector3f temp3 = new Vector3f(normals[2].getX(), normals[2].getY(), normals[2].getZ());
                        temp3.multiply(barycentric[2]);

                        interpolatedNormal = temp1.add(temp2);
                        interpolatedNormal = interpolatedNormal.add(temp3);

                        interpolatedNormal.normalize();

                        float diffuse = Math.max(0, interpolatedNormal.dot(lightDirection));
                        float brightness = 0.9f * diffuse;
                        Color baseColor = Color.LIGHTGRAY;

                        Color shadedColor = new Color(
                                clamp(baseColor.getRed() * brightness),
                                clamp(baseColor.getGreen() * brightness),
                                clamp(baseColor.getBlue() * brightness),
                                baseColor.getOpacity()
                        );

                        zBuffer[x][y] = depth;
                        graphicsContext.getPixelWriter().setColor(x, y, shadedColor);
                    }
                }
            }
        }
    }




//    /**
//     * Выполняет растеризацию треугольника и обновляет графический контекст.
//     *
//     * @param points          Координаты точек треугольника на экране.
//     * @param zValues         Значения глубины для каждой вершины.
//     * @param zBuffer         Z-буфер для отслеживания глубины пикселей.
//     * @param graphicsContext Графический контекст для рисования.
//     */
//    private void rasterizeTriangle(Point2f[] points, float[] zValues, float[][] zBuffer, GraphicsContext graphicsContext) {
//        // Цвет для закрашивания треугольника
//        Color fillColor = Color.RED; // Один цвет для всего треугольника
//
//        // Находим координаты вершин треугольника
//        double[] xPoints = new double[3];
//        double[] yPoints = new double[3];
//
//        for (int i = 0; i < 3; i++) {
//            xPoints[i] = points[i].getX();
//            yPoints[i] = points[i].getY();
//        }
//
//        // Закрашиваем треугольник с использованием метода fillPolygon
//        graphicsContext.setFill(fillColor);
//        graphicsContext.fillPolygon(xPoints, yPoints, 3);
//    }

    /**
     * Выполняет отрисовку каркаса модели (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();
            }

            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 = (vertex.getX() + 1) * 0.5f * width;
        float y = (1 - (vertex.getY() + 1) * 0.5f) * 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 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));
    }

    /**
     * Интерполирует глубину для пикселя внутри треугольника.
     *
     * @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];
    }

}