Add support to render indexed images

For this we need a new SkSL shader that receives the palette and the
indexed image data and outputs the RGBA values for each fragment.
This commit is contained in:
David Capello 2023-03-17 15:09:32 -03:00
parent af074fd6bf
commit 9e08386a28
7 changed files with 151 additions and 37 deletions

View File

@ -1,5 +1,5 @@
// Aseprite
// Copyright (C) 2022 Igara Studio S.A.
// Copyright (C) 2022-2023 Igara Studio S.A.
//
// This program is distributed under the terms of
// the End-User License Agreement for Aseprite.
@ -33,6 +33,16 @@ half4 main(vec2 fragcoord) {
}
)";
const char* kIndexedShaderCode = R"(
uniform shader iImg;
uniform shader iPal;
half4 main(vec2 fragcoord) {
int index = int(255.0 * iImg.eval(fragcoord).a);
return iPal.eval(half2(index, 0));
}
)";
inline SkBlendMode to_skia(const doc::BlendMode bm) {
switch (bm) {
case doc::BlendMode::NORMAL: return SkBlendMode::kSrcOver;
@ -65,13 +75,18 @@ ShaderRenderer::ShaderRenderer()
m_properties.renderBgOnScreen = true;
m_properties.requiresRgbaBackbuffer = true;
auto result = SkRuntimeEffect::MakeForShader(SkString(kBgShaderCode));
if (!result.errorText.isEmpty()) {
LOG(ERROR, "Shader error: %s\n", result.errorText.c_str());
std::printf("Shader error: %s\n", result.errorText.c_str());
throw std::runtime_error("Cannot compile shaders for ShaderRenderer");
}
m_bgEffect = result.effect;
auto makeShader = [](const char* code) {
auto result = SkRuntimeEffect::MakeForShader(SkString(code));
if (!result.errorText.isEmpty()) {
LOG(ERROR, "Shader error: %s\n", result.errorText.c_str());
std::printf("Shader error: %s\n", result.errorText.c_str());
throw std::runtime_error("Cannot compile shaders for ShaderRenderer");
}
return result;
};
m_bgEffect = makeShader(kBgShaderCode).effect;
m_indexedEffect = makeShader(kIndexedShaderCode).effect;
}
ShaderRenderer::~ShaderRenderer() = default;
@ -157,6 +172,25 @@ void ShaderRenderer::renderSprite(os::Surface* dstSurface,
const doc::frame_t frame,
const gfx::ClipF& area)
{
m_sprite = sprite;
// Copy the current color palette to a 256 palette (so all entries
// outside the valid range will be transparent in the kIndexedShaderCode)
if (m_sprite->pixelFormat() == IMAGE_INDEXED) {
const auto srcPal = m_sprite->palette(frame);
m_palette.resize(256, 0);
for (int i=0; i<srcPal->size(); ++i)
m_palette.setEntry(i, srcPal->entry(i));
m_bgLayer = sprite->backgroundLayer();
if (!m_bgLayer || !m_bgLayer->isVisible()) {
afterBackgroundLayerIsPainted();
}
}
else {
m_bgLayer = nullptr;
}
SkCanvas* canvas = &static_cast<os::SkiaSurface*>(dstSurface)->canvas();
canvas->save();
{
@ -308,6 +342,10 @@ void ShaderRenderer::drawLayerGroup(SkCanvas* canvas,
frame, area);
break;
}
if (layer == m_bgLayer) {
afterBackgroundLayerIsPainted();
}
}
}
@ -318,11 +356,11 @@ void ShaderRenderer::renderCheckeredBackground(os::Surface* dstSurface,
SkRuntimeShaderBuilder builder(m_bgEffect);
builder.uniform("iBg1") = gfxColor_to_SkV4(
color_utils::color_for_ui(
app::Color::fromImage(sprite->pixelFormat(),
app::Color::fromImage(m_bgOptions.colorPixelFormat,
m_bgOptions.color1)));
builder.uniform("iBg2") = gfxColor_to_SkV4(
color_utils::color_for_ui(
app::Color::fromImage(sprite->pixelFormat(),
app::Color::fromImage(m_bgOptions.colorPixelFormat,
m_bgOptions.color2)));
float sx = (m_bgOptions.zoom ? m_proj.scaleX(): 1.0);
@ -370,25 +408,75 @@ void ShaderRenderer::drawImage(SkCanvas* canvas,
(const void*)srcImage->getPixelAddress(0, 0),
srcImage->getMemSize());
// TODO support other color modes
ASSERT(srcImage->colorMode() == doc::ColorMode::RGB);
switch (srcImage->colorMode()) {
auto skImg = SkImage::MakeRasterData(
SkImageInfo::Make(srcImage->width(),
srcImage->height(),
kRGBA_8888_SkColorType,
kUnpremul_SkAlphaType),
skData,
srcImage->getRowStrideSize());
case doc::ColorMode::RGB: {
auto skImg = SkImage::MakeRasterData(
SkImageInfo::Make(srcImage->width(),
srcImage->height(),
kRGBA_8888_SkColorType,
kUnpremul_SkAlphaType),
skData,
srcImage->getRowStrideSize());
SkPaint p;
p.setAlpha(opacity);
p.setBlendMode(to_skia(blendMode));
canvas->drawImage(skImg.get(),
SkIntToScalar(x),
SkIntToScalar(y),
SkSamplingOptions(),
&p);
SkPaint p;
p.setAlpha(opacity);
p.setBlendMode(to_skia(blendMode));
canvas->drawImage(skImg.get(),
SkIntToScalar(x),
SkIntToScalar(y),
SkSamplingOptions(),
&p);
break;
}
case doc::ColorMode::GRAYSCALE: {
break;
}
case doc::ColorMode::INDEXED: {
// We use kAlpha_8_SkColorType to access to the index value through the alpha channel
auto skImg = SkImage::MakeRasterData(
SkImageInfo::Make(srcImage->width(),
srcImage->height(),
kAlpha_8_SkColorType,
kUnpremul_SkAlphaType),
skData,
srcImage->getRowStrideSize());
// Use the palette data as an "width x height" image where
// width=number of palette colors, and height=1
const size_t palSize = sizeof(color_t) * m_palette.size();
auto skPalData = SkData::MakeWithoutCopy(
(const void*)m_palette.rawColorsData(),
palSize);
auto skPal = SkImage::MakeRasterData(
SkImageInfo::Make(m_palette.size(), 1,
kRGBA_8888_SkColorType,
kUnpremul_SkAlphaType),
skPalData,
palSize);
SkRuntimeShaderBuilder builder(m_indexedEffect);
builder.child("iImg") = skImg->makeRawShader(SkSamplingOptions(SkFilterMode::kNearest));
builder.child("iPal") = skPal->makeShader(SkSamplingOptions(SkFilterMode::kNearest));
SkPaint p;
p.setAlpha(opacity);
p.setBlendMode(to_skia(blendMode));
p.setStyle(SkPaint::kFill_Style);
p.setShader(builder.makeShader());
canvas->save();
canvas->translate(
SkIntToScalar(x),
SkIntToScalar(y));
canvas->drawRect(SkRect::MakeXYWH(0, 0, srcImage->width(), srcImage->height()), p);
canvas->restore();
break;
}
}
}
// TODO this is equal to Render::checkIfWeShouldUsePreview(const Cel*),
@ -410,6 +498,17 @@ bool ShaderRenderer::checkIfWeShouldUsePreview(const doc::Cel* cel) const
return false;
}
void ShaderRenderer::afterBackgroundLayerIsPainted()
{
if (m_sprite && m_sprite->pixelFormat() == IMAGE_INDEXED) {
// Only after we draw the background layer we set the transparent
// index of the sprite as transparent, so the shader can return
// the transparent color for this specific index on transparent
// layers.
m_palette.setEntry(m_sprite->transparentColor(), 0);
}
}
} // namespace app
#endif // SK_ENABLE_SKSL

View File

@ -1,5 +1,5 @@
// Aseprite
// Copyright (C) 2022 Igara Studio S.A.
// Copyright (C) 2022-2023 Igara Studio S.A.
//
// This program is distributed under the terms of
// the End-User License Agreement for Aseprite.
@ -11,6 +11,7 @@
#if SK_ENABLE_SKSL
#include "app/render/renderer.h"
#include "doc/palette.h"
#include "include/core/SkRefCnt.h"
@ -81,11 +82,15 @@ namespace app {
const doc::BlendMode blendMode);
bool checkIfWeShouldUsePreview(const doc::Cel* cel) const;
void afterBackgroundLayerIsPainted();
Properties m_properties;
render::BgOptions m_bgOptions;
render::Projection m_proj;
sk_sp<SkRuntimeEffect> m_bgEffect;
sk_sp<SkRuntimeEffect> m_indexedEffect;
const doc::Sprite* m_sprite = nullptr;
const doc::LayerImage* m_bgLayer = nullptr;
// TODO these members are the same as in render::Render, we should
// see a way to merge both
const doc::Layer* m_selectedLayerForOpacity = nullptr;
@ -95,6 +100,10 @@ namespace app {
const doc::Tileset* m_previewTileset = nullptr;
gfx::Point m_previewPos;
doc::BlendMode m_previewBlendMode = doc::BlendMode::NORMAL;
// Palette of 256 colors (useful for the indexed shader to set all
// colors outside the valid range as transparent RGBA=0 values)
doc::Palette m_palette;
};
} // namespace app

View File

@ -1,5 +1,5 @@
// Aseprite
// Copyright (C) 2019-2022 Igara Studio S.A.
// Copyright (C) 2019-2023 Igara Studio S.A.
// Copyright (C) 2018 David Capello
//
// This program is distributed under the terms of
@ -116,6 +116,7 @@ void EditorRender::setupBackground(Doc* doc, doc::PixelFormat pixelFormat)
render::BgOptions bg;
bg.type = bgType;
bg.zoom = docPref.bg.zoom();
bg.colorPixelFormat = pixelFormat;
bg.color1 = color_utils::color_for_image_without_alpha(docPref.bg.color1(), pixelFormat);
bg.color2 = color_utils::color_for_image_without_alpha(docPref.bg.color2(), pixelFormat);
bg.stripeSize = tile;

View File

@ -1,5 +1,5 @@
// Aseprite Document Library
// Copyright (c) 2020-2022 Igara Studio S.A.
// Copyright (c) 2020-2023 Igara Studio S.A.
// Copyright (c) 2001-2017 David Capello
//
// This file is released under the terms of the MIT license.
@ -95,11 +95,11 @@ Palette* Palette::createGrayscale()
return graypal;
}
void Palette::resize(int ncolors)
void Palette::resize(int ncolors, color_t color)
{
ASSERT(ncolors >= 0);
m_colors.resize(ncolors, doc::rgba(0, 0, 0, 255));
m_colors.resize(ncolors, color);
++m_modifications;
}

View File

@ -1,5 +1,5 @@
// Aseprite Document Library
// Copyright (C) 2020-2022 Igara Studio S.A.
// Copyright (C) 2020-2023 Igara Studio S.A.
// Copyright (C) 2001-2018 David Capello
//
// This file is released under the terms of the MIT license.
@ -37,7 +37,10 @@ namespace doc {
static Palette* createGrayscale();
int size() const { return (int)m_colors.size(); }
void resize(int ncolors);
void resize(int ncolors, color_t color = doc::rgba(0, 0, 0, 255));
// Used to share the palette data with a SkSL shader
const color_t* rawColorsData() const { return m_colors.data(); }
const std::string& filename() const { return m_filename; }
const std::string& comment() const { return m_comment; }

View File

@ -18,6 +18,7 @@ namespace render {
struct BgOptions {
BgType type = BgType::TRANSPARENT;
bool zoom = false;
doc::PixelFormat colorPixelFormat = doc::PixelFormat::IMAGE_RGB;
doc::color_t color1 = 0;
doc::color_t color2 = 0;
gfx::Size stripeSize{16, 16};

View File

@ -1,5 +1,5 @@
// Aseprite Render Library
// Copyright (C) 2019-2022 Igara Studio S.A.
// Copyright (C) 2019-2023 Igara Studio S.A.
// Copyright (C) 2001-2018 David Capello
//
// This file is released under the terms of the MIT license.
@ -930,8 +930,9 @@ void Render::renderCheckeredBackground(
gfx::Rect dstBounds = area.dstBounds();
// Fix background color (make them opaque)
switch (image->pixelFormat()) {
// Fix background colors (make them opaque)
ASSERT(m_bg.colorPixelFormat == image->pixelFormat());
switch (m_bg.colorPixelFormat) {
case IMAGE_RGB:
m_bg.color1 |= doc::rgba_a_mask;
m_bg.color2 |= doc::rgba_a_mask;