From 9e08386a28a62acf01e19d1753c7b39870ea82e7 Mon Sep 17 00:00:00 2001 From: David Capello Date: Fri, 17 Mar 2023 15:09:32 -0300 Subject: [PATCH] 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. --- src/app/render/shader_renderer.cpp | 153 +++++++++++++++++++++++----- src/app/render/shader_renderer.h | 11 +- src/app/ui/editor/editor_render.cpp | 3 +- src/doc/palette.cpp | 6 +- src/doc/palette.h | 7 +- src/render/bg_options.h | 1 + src/render/render.cpp | 7 +- 7 files changed, 151 insertions(+), 37 deletions(-) diff --git a/src/app/render/shader_renderer.cpp b/src/app/render/shader_renderer.cpp index bd545d4f6..7e6228655 100644 --- a/src/app/render/shader_renderer.cpp +++ b/src/app/render/shader_renderer.cpp @@ -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; isize(); ++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(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 diff --git a/src/app/render/shader_renderer.h b/src/app/render/shader_renderer.h index eec90cb17..e70ae3304 100644 --- a/src/app/render/shader_renderer.h +++ b/src/app/render/shader_renderer.h @@ -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 m_bgEffect; + sk_sp 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 diff --git a/src/app/ui/editor/editor_render.cpp b/src/app/ui/editor/editor_render.cpp index b29261ff8..c33242c48 100644 --- a/src/app/ui/editor/editor_render.cpp +++ b/src/app/ui/editor/editor_render.cpp @@ -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; diff --git a/src/doc/palette.cpp b/src/doc/palette.cpp index ae86d081e..17ec68ac2 100644 --- a/src/doc/palette.cpp +++ b/src/doc/palette.cpp @@ -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; } diff --git a/src/doc/palette.h b/src/doc/palette.h index 632843358..cf938a1ed 100644 --- a/src/doc/palette.h +++ b/src/doc/palette.h @@ -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; } diff --git a/src/render/bg_options.h b/src/render/bg_options.h index 9e5016184..0151f518e 100644 --- a/src/render/bg_options.h +++ b/src/render/bg_options.h @@ -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}; diff --git a/src/render/render.cpp b/src/render/render.cpp index 3c9d36ffe..c89211c9a 100644 --- a/src/render/render.cpp +++ b/src/render/render.cpp @@ -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;