Add options to change the downsampling algorithm (fix #3183)

Also we've restored the default algorithm to bilinear + mipmapping,
which was the default on the Aseprite before we switched to Skia m96.
This was requested by some users.
This commit is contained in:
David Capello 2022-02-21 15:30:09 -03:00
parent 2d3de1728c
commit 5ccf414183
9 changed files with 183 additions and 24 deletions

View File

@ -127,6 +127,12 @@
<value id="YES" value="1" />
<value id="NO" value="2" />
</enum>
<enum id="Downsampling">
<value id="NEAREST" value="0" />
<value id="BILINEAR" value="1" />
<value id="BILINEAR_MIPMAP" value="2" />
<value id="TRILINEAR_MIPMAP" value="3" />
</enum>
</types>
<global>
@ -176,6 +182,7 @@
some performance issue rendering huge sprites with small
zoom levels -->
<option id="auto_fit" type="bool" default="false" />
<option id="downsampling" type="Downsampling" default="Downsampling::BILINEAR_MIPMAP" />
</section>
<section id="timeline">
<option id="keep_selection" type="bool" default="false" />

2
laf

@ -1 +1 @@
Subproject commit f473cc6f8c68629ef0e8c480afbba5659ef5de75
Subproject commit 7c819791e50116b54e0666dbf96822e0993d1d1f

View File

@ -1,5 +1,5 @@
// Aseprite
// Copyright (C) 2019-2021 Igara Studio S.A.
// Copyright (C) 2019-2022 Igara Studio S.A.
//
// This program is distributed under the terms of
// the End-User License Agreement for Aseprite.
@ -165,6 +165,7 @@ FOR_ENUM(app::gen::BgType)
FOR_ENUM(app::gen::BrushPreview)
FOR_ENUM(app::gen::BrushType)
FOR_ENUM(app::gen::ColorProfileBehavior)
FOR_ENUM(app::gen::Downsampling)
FOR_ENUM(app::gen::EyedropperChannel)
FOR_ENUM(app::gen::EyedropperSample)
FOR_ENUM(app::gen::FillReferTo)

View File

@ -56,6 +56,7 @@
#include "doc/selected_objects.h"
#include "doc/slice.h"
#include "obs/connection.h"
#include "os/sampling.h"
#include "os/surface.h"
#include "os/system.h"
#include "render/dithering.h"
@ -133,6 +134,49 @@ private:
}
};
class ContextBar::SamplingOptions : public HBox {
public:
class Item : public ListItem {
public:
Item(const char* label,
const gen::Downsampling sampling)
: ListItem(label)
, m_sampling(sampling) {
}
const gen::Downsampling& sampling() const { return m_sampling; }
private:
gen::Downsampling m_sampling;
};
SamplingOptions()
: m_downsamplingLabel("Downsampling:")
{
addChild(&m_downsamplingLabel);
addChild(&m_downsampling);
m_downsampling.addItem(new Item("Nearest", gen::Downsampling::NEAREST));
m_downsampling.addItem(new Item("Bilinear", gen::Downsampling::BILINEAR));
m_downsampling.addItem(new Item("Bilinear mipmapping", gen::Downsampling::BILINEAR_MIPMAP));
m_downsampling.addItem(new Item("Trilinear mipmapping", gen::Downsampling::TRILINEAR_MIPMAP));
m_downsampling.setSelectedItemIndex(
(int)Preferences::instance().editor.downsampling());
m_downsampling.Change.connect([this]{ onDownsamplingChange(); });
}
private:
void onDownsamplingChange() {
if (auto item = dynamic_cast<Item*>(m_downsampling.getSelectedItem())) {
Preferences::instance().editor.downsampling(
item->sampling());
}
}
Label m_downsamplingLabel;
ComboBox m_downsampling;
};
class ContextBar::BrushBackField : public ButtonSet {
public:
BrushBackField()
@ -1537,6 +1581,7 @@ ContextBar::ContextBar(TooltipManager* tooltipManager,
m_selectionOptionsBox->addChild(m_rotAlgo = new RotAlgorithmField());
addChild(m_zoomButtons = new ZoomButtons);
addChild(m_samplingOptions = new SamplingOptions);
addChild(m_brushBack = new BrushBackField);
addChild(m_brushType = new BrushTypeField(this));
@ -1843,13 +1888,6 @@ void ContextBar::updateForTool(tools::Tool* tool)
(activeBrush()->type() == kSquareBrushType ||
activeBrush()->type() == kLineBrushType);
// True if the current tool is eyedropper.
const bool needZoomButtons = tool &&
(tool->getInk(0)->isZoom() ||
tool->getInk(1)->isZoom() ||
tool->getInk(0)->isScrollMovement() ||
tool->getInk(1)->isScrollMovement());
// True if the current tool is eyedropper.
const bool isEyedropper = tool &&
(tool->getInk(0)->isEyedropper() ||
@ -1902,7 +1940,7 @@ void ContextBar::updateForTool(tools::Tool* tool)
const bool supportDynamics = (!hasImageBrush);
// Show/Hide fields
m_zoomButtons->setVisible(needZoomButtons);
m_zoomButtons->setVisible(needZoomButtons(tool));
m_brushBack->setVisible(supportOpacity && hasImageBrush && !withDithering);
m_brushType->setVisible(supportOpacity && (!isFloodfill || (isFloodfill && hasImageBrush && !withDithering)));
m_brushSize->setVisible(supportOpacity && !isFloodfill && !hasImageBrush);
@ -1940,7 +1978,11 @@ void ContextBar::updateForTool(tools::Tool* tool)
if (updateShade)
m_inkShades->updateShadeFromColorBarPicks();
layout();
if (!updateSamplingVisibility(tool)) {
// updateSamplingVisibility() returns false if it doesn't layout()
// the ContextBar.
layout();
}
}
void ContextBar::updateForMovingPixels()
@ -1980,6 +2022,26 @@ void ContextBar::updateToolLoopModifiersIndicators(tools::ToolLoopModifiers modi
m_selectionMode->setSelectionMode(mode);
}
bool ContextBar::updateSamplingVisibility(tools::Tool* tool)
{
if (!tool)
tool = App::instance()->activeTool();
const bool newVisibility =
needZoomButtons(tool) &&
current_editor &&
(current_editor->projection().scaleX() < 1.0 ||
current_editor->projection().scaleY() < 1.0) &&
current_editor->isUsingNewRenderEngine();
if (newVisibility == m_samplingOptions->hasFlags(HIDDEN)) {
m_samplingOptions->setVisible(newVisibility);
layout();
return true;
}
return false;
}
void ContextBar::updateAutoSelectLayer(bool state)
{
m_autoSelectLayer->setSelected(state);
@ -2285,4 +2347,13 @@ void ContextBar::showDynamics()
m_dynamics->switchPopup();
}
bool ContextBar::needZoomButtons(tools::Tool* tool) const
{
return tool &&
(tool->getInk(0)->isZoom() ||
tool->getInk(1)->isZoom() ||
tool->getInk(0)->isScrollMovement() ||
tool->getInk(1)->isScrollMovement());
}
} // namespace app

View File

@ -1,5 +1,5 @@
// Aseprite
// Copyright (C) 2018-2020 Igara Studio S.A.
// Copyright (C) 2018-2022 Igara Studio S.A.
// Copyright (C) 2001-2017 David Capello
//
// This program is distributed under the terms of
@ -67,6 +67,7 @@ namespace app {
void updateForMovingPixels();
void updateForSelectingBox(const std::string& text);
void updateToolLoopModifiersIndicators(tools::ToolLoopModifiers modifiers);
bool updateSamplingVisibility(tools::Tool* tool = nullptr);
void updateAutoSelectLayer(bool state);
bool isAutoSelectLayer() const;
@ -129,8 +130,10 @@ namespace app {
void registerCommands();
void showBrushes();
void showDynamics();
bool needZoomButtons(tools::Tool* tool) const;
class ZoomButtons;
class SamplingOptions;
class BrushBackField;
class BrushTypeField;
class BrushAngleField;
@ -158,6 +161,7 @@ namespace app {
class SliceFields;
ZoomButtons* m_zoomButtons;
SamplingOptions* m_samplingOptions;
BrushBackField* m_brushBack;
BrushTypeField* m_brushType;
BrushAngleField* m_brushAngle;

View File

@ -60,6 +60,7 @@
#include "doc/mask_boundaries.h"
#include "doc/slice.h"
#include "os/color_space.h"
#include "os/sampling.h"
#include "os/surface.h"
#include "os/system.h"
#include "os/window.h"
@ -174,6 +175,10 @@ Editor::Editor(Doc* document, EditorFlags flags)
Preferences::instance().colorBar.fgColor.AfterChange.connect(
[this]{ onFgColorChange(); });
m_samplingChangeConn =
Preferences::instance().editor.downsampling.AfterChange.connect(
[this]{ onSamplingChange(); });
m_contextBarBrushChangeConn =
App::instance()->contextBar()->BrushChange.connect(
[this]{ onContextBarBrushChange(); });
@ -239,6 +244,18 @@ bool Editor::isActive() const
return (current_editor == this);
}
bool Editor::isUsingNewRenderEngine() const
{
ASSERT(m_sprite);
return
(Preferences::instance().experimental.newRenderEngine()
// Reference layers + zoom > 100% need the old render engine for
// sub-pixel rendering.
&& (!m_sprite->hasVisibleReferenceLayers()
|| (m_proj.scaleX() <= 1.0
&& m_proj.scaleY() <= 1.0)));
}
// static
WidgetType Editor::Type()
{
@ -410,6 +427,9 @@ void Editor::setZoom(const render::Zoom& zoom)
if (m_proj.zoom() != zoom) {
m_proj.setZoom(zoom);
notifyZoomChanged();
if (isActive())
App::instance()->contextBar()->updateSamplingVisibility();
}
else {
// Just copy the zoom as the internal "Zoom::m_internalScale"
@ -582,13 +602,8 @@ void Editor::drawOneSpriteUnclippedRect(ui::Graphics* g, const gfx::Rect& sprite
return;
// rc2 is the rectangle used to create a temporal rendered image of the sprite
const bool newEngine =
(Preferences::instance().experimental.newRenderEngine()
// Reference layers + zoom > 100% need the old render engine for
// sub-pixel rendering.
&& (!m_sprite->hasVisibleReferenceLayers()
|| (m_proj.scaleX() <= 1.0
&& m_proj.scaleY() <= 1.0)));
const auto& pref = Preferences::instance();
const bool newEngine = isUsingNewRenderEngine();
gfx::Rect rc2;
if (newEngine) {
rc2 = expose; // New engine, exposed rectangle (without zoom)
@ -614,11 +629,11 @@ void Editor::drawOneSpriteUnclippedRect(ui::Graphics* g, const gfx::Rect& sprite
rendered.reset(Image::create(IMAGE_RGB, rc2.w, rc2.h,
m_renderEngine->getRenderImageBuffer()));
m_renderEngine->setNewBlendMethod(Preferences::instance().experimental.newBlend());
m_renderEngine->setNewBlendMethod(pref.experimental.newBlend());
m_renderEngine->setRefLayersVisiblity(true);
m_renderEngine->setSelectedLayer(m_layer);
if (m_flags & Editor::kUseNonactiveLayersOpacityWhenEnabled)
m_renderEngine->setNonactiveLayersOpacity(Preferences::instance().experimental.nonactiveLayersOpacity());
m_renderEngine->setNonactiveLayersOpacity(pref.experimental.nonactiveLayersOpacity());
else
m_renderEngine->setNonactiveLayersOpacity(255);
m_renderEngine->setProjection(
@ -710,7 +725,30 @@ void Editor::drawOneSpriteUnclippedRect(ui::Graphics* g, const gfx::Rect& sprite
tmp.get(), 0, 0, 0, 0, rc2.w, rc2.h);
if (newEngine) {
g->drawSurface(tmp.get(), gfx::Rect(0, 0, rc2.w, rc2.h), dest);
os::Sampling sampling;
if (m_proj.scaleX() < 1.0) {
switch (pref.editor.downsampling()) {
case gen::Downsampling::NEAREST:
sampling = os::Sampling(os::Sampling::Filter::Nearest);
break;
case gen::Downsampling::BILINEAR:
sampling = os::Sampling(os::Sampling::Filter::Linear);
break;
case gen::Downsampling::BILINEAR_MIPMAP:
sampling = os::Sampling(os::Sampling::Filter::Linear,
os::Sampling::Mipmap::Nearest);
break;
case gen::Downsampling::TRILINEAR_MIPMAP:
sampling = os::Sampling(os::Sampling::Filter::Linear,
os::Sampling::Mipmap::Linear);
break;
}
}
g->drawSurface(tmp.get(),
gfx::Rect(0, 0, rc2.w, rc2.h),
dest,
sampling);
}
else {
g->blit(tmp.get(), 0, 0, dest.x, dest.y, dest.w, dest.h);
@ -2112,6 +2150,15 @@ void Editor::onActiveToolChange(tools::Tool* tool)
}
}
void Editor::onSamplingChange()
{
if (m_proj.scaleX() < 1.0 &&
m_proj.scaleY() < 1.0 &&
isUsingNewRenderEngine()) {
invalidate();
}
}
void Editor::onFgColorChange()
{
m_brushPreview.redraw();

View File

@ -108,6 +108,7 @@ namespace app {
static void destroyEditorSharedInternals();
bool isActive() const;
bool isUsingNewRenderEngine() const;
DocView* getDocView() { return m_docView; }
void setDocView(DocView* docView) { m_docView = docView; }
@ -311,6 +312,7 @@ namespace app {
void onResize(ui::ResizeEvent& ev) override;
void onPaint(ui::PaintEvent& ev) override;
void onInvalidateRegion(const gfx::Region& region) override;
void onSamplingChange();
void onFgColorChange();
void onContextBarBrushChange();
void onTiledModeBeforeChange();
@ -405,6 +407,7 @@ namespace app {
ui::Timer m_antsTimer;
int m_antsOffset;
obs::scoped_connection m_samplingChangeConn;
obs::scoped_connection m_fgColorChangeConn;
obs::scoped_connection m_contextBarBrushChangeConn;
obs::scoped_connection m_showExtrasConn;

View File

@ -1,5 +1,5 @@
// Aseprite UI Library
// Copyright (C) 2019-2021 Igara Studio S.A.
// Copyright (C) 2019-2022 Igara Studio S.A.
// Copyright (C) 2001-2018 David Capello
//
// This file is released under the terms of the MIT license.
@ -21,6 +21,7 @@
#include "gfx/size.h"
#include "os/draw_text.h"
#include "os/font.h"
#include "os/sampling.h"
#include "os/surface.h"
#include "os/system.h"
#include "os/window.h"
@ -260,6 +261,25 @@ void Graphics::drawSurface(os::Surface* surface,
gfx::Rect(dstRect).offset(m_dx, m_dy));
}
void Graphics::drawSurface(os::Surface* surface,
const gfx::Rect& srcRect,
const gfx::Rect& dstRect,
const os::Sampling& sampling,
const ui::Paint* paint)
{
dirty(gfx::Rect(m_dx+dstRect.x, m_dy+dstRect.y,
dstRect.w, dstRect.h));
os::SurfaceLock lockSrc(surface);
os::SurfaceLock lockDst(m_surface.get());
m_surface->drawSurface(
surface,
srcRect,
gfx::Rect(dstRect).offset(m_dx, m_dy),
sampling,
paint);
}
void Graphics::drawRgbaSurface(os::Surface* surface, int x, int y)
{
dirty(gfx::Rect(m_dx+x, m_dy+y, surface->width(), surface->height()));

View File

@ -1,5 +1,5 @@
// Aseprite UI Library
// Copyright (C) 2019-2020 Igara Studio S.A.
// Copyright (C) 2019-2022 Igara Studio S.A.
// Copyright (C) 2001-2018 David Capello
//
// This file is released under the terms of the MIT license.
@ -30,6 +30,7 @@ namespace gfx {
namespace os {
class DrawTextDelegate;
class Sampling;
}
namespace ui {
@ -89,6 +90,11 @@ namespace ui {
void drawSurface(os::Surface* surface,
const gfx::Rect& srcRect,
const gfx::Rect& dstRect);
void drawSurface(os::Surface* surface,
const gfx::Rect& srcRect,
const gfx::Rect& dstRect,
const os::Sampling& sampling,
const ui::Paint* paint = nullptr);
void drawRgbaSurface(os::Surface* surface, int x, int y);
void drawRgbaSurface(os::Surface* surface, int srcx, int srcy, int dstx, int dsty, int w, int h);
void drawRgbaSurface(os::Surface* surface,