JavaFX - избегайте размытия при рисовании прямоугольником на холсте с высоким разрешением

Я хотел бы рисовать на холсте JavaFX. Координаты для холста предоставляются двойными значениями, но я понимаю, что для «привязки» к пиксельной сетке мне нужны целые числа, чтобы избежать отрисовки «между» пикселями, что приведет к размытию/сглаживанию по краям.

Рассмотрим этот минимальный пример:

public class App extends Application {

    @Override
    public void start(Stage stage) {

        var vbox = new VBox();
        var canvas = new Canvas(600, 400);
        var gc = canvas.getGraphicsContext2D();

        // draw a background
        gc.beginPath();
        gc.setFill(Color.rgb(200,200,200));
        gc.rect(0, 0, 600, 400);
        gc.fill();

        // draw a smaller square
        gc.beginPath();
        gc.setFill(Color.rgb(100,100,100));
        gc.rect(9.0, 9.0, 50, 50);  // snap to grid
        gc.fill();

        vbox.getChildren().addAll(canvas);

        var scene = new Scene(vbox, 640, 480);
        stage.setScene(scene);
        stage.show();
    }

    public static void main(String[] args) {
        launch();
    }

}

Это работает и приведет к четкому краю, рассмотрите это изображение:

введите здесь описание изображения

Теперь предположим, что HighDPI включен, т.е. рассмотрим, масштабируем ли мы графику в Windows 10 до 125 процентов. Это приведет к коэффициенту масштабирования 1,25 (мы можем получить его с помощью getOutputScaleX/Y).

Однако тогда весь холст масштабируется с этим коэффициентом, и мы получаем размытие. Посмотрите прикрепленные изображения (но вам нужно увеличить масштаб и, возможно, просмотреть их в графической программе, а не в браузере).

введите здесь описание изображения

Затем я подумал, что мы можем настроить исходные координаты так, чтобы масштабированные координаты давали целые числа. Например, чтобы убедиться, что мы попали в целое число со смещением 9 * 1,25 = 11,25 на масштабированном изображении, давайте вместо этого попробуем нацелиться на 11,0, т.е. изменим эту строку

gc.rect(9.0, 9.0, 50, 50); 

to

gc.rect(11.0/1.25, 11.0/1.25, 50, 50); 

Но это еще приводит к размытию края.

Как мы можем обойти это? В идеале я хотел бы полностью отключить масштабирование dpi для холста и выполнить свои собственные (с точностью до пикселя) вычисления. Возможно ли это или есть другое решение?

Для drawImage есть параметр setSmoothing, но при рисовании фигур напрямую (например, rect) ничего нет.


person ndbd    schedule 07.06.2020    source источник
comment
Какие значения возвращают методы getRenderScaleX/Y в вашем случае, в отличие от метода getOutputScaleX/Y.   -  person mipa    schedule 11.06.2020
comment
О каком HighDPI варианте вы говорите? Настраивали ли вы какие-либо параметры, которые Windows предлагает для исполняемых файлов, такие как настройки высокого разрешения или режимы совместимости? Поскольку вам не нужно делать это для JavaFX, он поддерживает высокое разрешение.   -  person john16384    schedule 13.06.2020
comment
Приложение «Параметры Windows» -> «Система» -> «Экран» -> раздел «Масштаб и макет» -> «Изменить размер текста, приложений и других значков». Это фактически меняет настройку dpi в Windows.   -  person ndbd    schedule 14.06.2020


Ответы (1)


Вы можете решить эту проблему, исключив масштабирование, используемое путем масштабирования холста до меньшего размера. Вот пример (обратите внимание, что линии рисуются «между» пикселями в JavaFX, поэтому, чтобы сделать их четкими, вам нужно добавить 0,5).

В этом примере также размещается Group вокруг масштабированного Canvas. Это необходимо для учета масштаба при создании макета, но также имеет некоторое влияние на рендеринг. Этот пример был протестирован на 100%, 125% и 150%.

import javafx.application.Application;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.scene.canvas.Canvas;
import javafx.scene.canvas.GraphicsContext;
import javafx.scene.layout.HBox;
import javafx.scene.paint.Color;
import javafx.stage.Stage;

public class CanvasTest extends Application {

  public static void main(String[] args) {
    Application.launch(args);
  }

  @Override
  public void start(Stage stage) throws Exception {
    HBox hbox = new HBox();

    Canvas canvas = new Canvas(100,100);

    GraphicsContext g2d = canvas.getGraphicsContext2D();

    g2d.fillRect(15, 15, 60, 10);
    g2d.fillRect(15.5, 30, 60, 10);  // supposed to be blurry
    g2d.fillRect(16, 45, 60, 10);
    g2d.fillRect(16.5, 60, 60, 10);  // supposed to be blurry

    g2d.setStroke(Color.RED);
    g2d.strokeLine(13.5, 13.5, 13.5, 93.5);
    g2d.strokeLine(13.5, 13.5, 93.5, 93.5);
    hbox.getChildren().add(new Group(canvas));

    Scene scene = new Scene(hbox);

    stage.setScene(scene);
    stage.show();

    canvas.scaleXProperty().bind(new SimpleDoubleProperty(1.0).divide(scene.getWindow().outputScaleXProperty()));
    canvas.scaleYProperty().bind(new SimpleDoubleProperty(1.0).divide(scene.getWindow().outputScaleYProperty()));
  }
}

Вот как это выглядит на мониторе с масштабом 150% (левая часть увеличена в 5 раз, чтобы вы могли видеть отсутствие размытия):

введите здесь описание изображения

А вот как это выглядит на 125% с обновленным примером:

введите здесь описание изображения

person john16384    schedule 11.06.2020
comment
У меня экран 1920*1080. Если я запускаю ваш пример с масштабированием 150%, размытия не будет. Однако при масштабе 125% вокруг черного прямоугольника появляется одна пиксельная полоса серого размытия. - person ndbd; 13.06.2020
comment
Хм, какая версия JavaFX? Я использовал 11.0.1. У меня вышеприведенный пример работает без нареканий, даже когда я перетаскиваю окно из экрана 100% в экран 150%, оно тут же перерисовывается. Без слушателей OutputScale я получаю размытые результаты. Может быть, вы можете распечатать значения OutputScaleX/Y и посмотреть, изменятся ли они. Для экрана 4k (3840x2160) я получаю выходной масштаб 2560x1440 при 150%. - person john16384; 13.06.2020
comment
как сказано, нет проблем при масштабировании 150%, только при масштабировании 125%. Я подозреваю, что проблема с некоторыми фракциями. Версия Java здесь 11.0.6; outputScaleX/Y @125%, очевидно, 1,25 и 1,50 @ 150%. - person ndbd; 13.06.2020
comment
Как ни странно, при 125% рисование прямоугольника со смещением +0,5 пикселя (fillRect(15.5, 15.5, 60, 60)) полностью удаляет это размытие. Это начинает звучать как ошибка. - person john16384; 14.06.2020
comment
Добавление перевода к холсту +0,5 также решает проблему, когда он составляет 125%. Возможно, сделать эти корректировки настраиваемыми в соответствии с коэффициентом масштабирования было бы удовлетворительным решением на данный момент. - person john16384; 14.06.2020
comment
Я обнаружил эту проблему, которая имеет аналогичную проблему с масштабированием 125% (bugs.openjdk.java. net/browse/JDK-8220484). Это не совсем то же самое, но здесь может быть аналогичная проблема, когда имеет значение разница между Math.round и Math.ceil и используется неправильный. Я быстро просмотрел базу кода JFX, но не смог найти ничего очевидного, связанного с OutputScale, что могло бы вызвать то, что мы оба видим здесь. Может быть стоит подать заявку на. - person john16384; 14.06.2020
comment
Мне не удалось зарегистрировать ошибку на bugs.openjdk.java.net — экран входа есть, а зарегистрироваться негде. openjfx.io также не имеет ссылок на какую-либо систему отслеживания ошибок. В конце концов, я отправил сообщение об ошибке на bugs.java.com, но я думаю, что оно будет закрыто, поскольку JavaFX больше не является частью JDK... Посмотрим, получится ли что-нибудь из этого... - person ndbd; 14.06.2020
comment
Это должно быть хорошо, это не часть JDK, но все еще поддерживается Oracle. Если нет, то подам. - person john16384; 15.06.2020
comment
Изучил это немного дальше, и после небольшой настройки размера холста я больше не мог воспроизвести это на своем 125%-ном экране. Думаю, теперь я нашел способ решить эту проблему. Я обновил пример, но теперь с добавлением Group вокруг масштабированного холста. Это почему-то решает проблему на 125%. Я предполагаю, что Canvas после масштабирования без группы вокруг него не был отрисован (в целом) на точной границе пикселя. Однако с группой вокруг него теперь это работает. Пожалуйста, проверьте это :) - person john16384; 15.06.2020
comment
Добавление группы вокруг действительно решает проблему. Большое спасибо за погружение в это. Я принял ваш ответ. См. bugs.java.com/bugdatabase/view_bug.do?bug_id=JDK- 8247541 . - person ndbd; 15.06.2020