mirror of
https://github.com/aseprite/aseprite.git
synced 2025-03-01 01:13:40 +00:00
Compose groups separately from all other layers
Maintain hierarchical structure of sprite groups instead of flattening. Allows opacity and blend mode to be applied correctly to groups. Sets the foundation for future features like mask layers. Note: Requires full image rendering and impacts performance in some scenarios. Avoids complex code changes for minor performance gains. Co-authored-by: Guilherme Marcondes <guilherme.marcondes@tecnico.ulisboa.pt>
This commit is contained in:
parent
6855c1368d
commit
ffc3684b1b
@ -236,6 +236,7 @@
|
||||
<option id="multiple_windows" type="bool" default="false" />
|
||||
<option id="new_render_engine" type="bool" default="true" />
|
||||
<option id="new_blend" type="bool" default="true" />
|
||||
<option id="compose_groups" type="bool" default="false" />
|
||||
<option id="use_native_clipboard" type="bool" default="true" />
|
||||
<option id="use_native_file_dialog" type="bool" default="true" />
|
||||
<option id="use_shaders_for_color_selectors" type="bool" default="true" />
|
||||
|
@ -1498,6 +1498,7 @@ color_quantization = Color Quantization
|
||||
performance = Performance
|
||||
multiple_windows = UI with multiple windows
|
||||
new_blend = New layer blending method
|
||||
compose_groups = Compose groups separately
|
||||
new_render_engine = New render engine for sprite editor
|
||||
native_clipboard = Use native clipboard
|
||||
native_file_dialog = Use native file dialog
|
||||
|
@ -574,6 +574,11 @@
|
||||
pref="experimental.new_blend" />
|
||||
<link text="(#1096)" url="https://github.com/aseprite/aseprite/issues/1096" />
|
||||
</hbox>
|
||||
<hbox>
|
||||
<check text="@.compose_groups"
|
||||
pref="experimental.compose_groups" />
|
||||
<link text="(#3225)" url="https://github.com/aseprite/aseprite/issues/3225" />
|
||||
</hbox>
|
||||
<check id="native_clipboard" text="@.native_clipboard"
|
||||
pref="experimental.use_native_clipboard" />
|
||||
<check id="native_file_dialog" text="@.native_file_dialog"
|
||||
|
@ -260,9 +260,9 @@ private:
|
||||
if ((count > 1) ||
|
||||
(count == 1 && m_layer && (newName != m_layer->name() ||
|
||||
newUserData != m_layer->userData() ||
|
||||
(m_layer->isImage() &&
|
||||
(newOpacity != static_cast<LayerImage*>(m_layer)->opacity() ||
|
||||
newBlendMode != static_cast<LayerImage*>(m_layer)->blendMode()))))) {
|
||||
(m_layer->isImage() || (m_layer->isGroup() && Preferences::instance().experimental.composeGroups())) ||
|
||||
(newOpacity != m_layer->opacity() ||
|
||||
newBlendMode != m_layer->blendMode())))) {
|
||||
try {
|
||||
ContextWriter writer(UIContext::instance());
|
||||
Tx tx(writer, "Set Layer Properties");
|
||||
@ -275,10 +275,13 @@ private:
|
||||
range.endRange(m_layer, -1);
|
||||
}
|
||||
|
||||
const bool shouldChangeProperties = m_layer->isImage() ||
|
||||
(m_layer->isGroup() && Preferences::instance().experimental.composeGroups());
|
||||
|
||||
const bool nameChanged = (newName != m_layer->name());
|
||||
const bool userDataChanged = (newUserData != m_layer->userData());
|
||||
const bool opacityChanged = (m_layer->isImage() && newOpacity != static_cast<LayerImage*>(m_layer)->opacity());
|
||||
const bool blendModeChanged = (m_layer->isImage() && newBlendMode != static_cast<LayerImage*>(m_layer)->blendMode());
|
||||
const bool opacityChanged = (shouldChangeProperties && newOpacity != m_layer->opacity());
|
||||
const bool blendModeChanged = (shouldChangeProperties && newBlendMode != m_layer->blendMode());
|
||||
|
||||
for (Layer* layer : range.selectedLayers()) {
|
||||
if (nameChanged && newName != layer->name())
|
||||
@ -287,7 +290,7 @@ private:
|
||||
if (userDataChanged && newUserData != layer->userData())
|
||||
tx(new cmd::SetUserData(layer, newUserData, m_document));
|
||||
|
||||
if (layer->isImage()) {
|
||||
if (layer->isImage() || (layer->isGroup() && Preferences::instance().experimental.composeGroups())) {
|
||||
if (opacityChanged && newOpacity != static_cast<LayerImage*>(layer)->opacity())
|
||||
tx(new cmd::SetLayerOpacity(static_cast<LayerImage*>(layer), newOpacity));
|
||||
|
||||
@ -430,21 +433,20 @@ private:
|
||||
name()->setText(m_layer->name().c_str());
|
||||
name()->setEnabled(true);
|
||||
|
||||
if (m_layer->isImage()) {
|
||||
if (m_layer->isImage() || (m_layer->isGroup() && Preferences::instance().experimental.composeGroups())) {
|
||||
mode()->setSelectedItem(nullptr);
|
||||
for (auto item : *mode()) {
|
||||
if (auto blendModeItem = dynamic_cast<BlendModeItem*>(item)) {
|
||||
if (blendModeItem->mode() == static_cast<LayerImage*>(m_layer)->blendMode()) {
|
||||
if (blendModeItem->mode() == m_layer->blendMode()) {
|
||||
mode()->setSelectedItem(item);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
mode()->setEnabled(!m_layer->isBackground());
|
||||
opacity()->setValue(static_cast<LayerImage*>(m_layer)->opacity());
|
||||
opacity()->setValue(m_layer->opacity());
|
||||
opacity()->setEnabled(!m_layer->isBackground());
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
mode()->setEnabled(false);
|
||||
opacity()->setEnabled(false);
|
||||
}
|
||||
|
@ -42,6 +42,11 @@ namespace app {
|
||||
// True if renderSprite() composite an unpremultiplied RGBA
|
||||
// surface when we draw on a transparent background.
|
||||
bool outputsUnpremultiplied = false;
|
||||
|
||||
// True if the renderer can compose groups of layers in a
|
||||
// layer image, in other case the renderer should render each
|
||||
// layer separately merging one by one.
|
||||
bool composeGroups = false;
|
||||
};
|
||||
|
||||
virtual ~Renderer() { }
|
||||
@ -56,6 +61,7 @@ namespace app {
|
||||
virtual void setRefLayersVisiblity(const bool visible) = 0;
|
||||
virtual void setNonactiveLayersOpacity(const int opacity) = 0;
|
||||
virtual void setNewBlendMethod(const bool newBlend) = 0;
|
||||
virtual void setComposeGroups(bool composeGroups) = 0;
|
||||
virtual void setBgOptions(const render::BgOptions& bg) = 0;
|
||||
virtual void setProjection(const render::Projection& projection) = 0;
|
||||
|
||||
|
@ -110,6 +110,11 @@ void ShaderRenderer::setNewBlendMethod(const bool newBlend)
|
||||
// TODO impl
|
||||
}
|
||||
|
||||
void ShaderRenderer::setComposeGroups(const bool m_composeGroups)
|
||||
{
|
||||
// TODO impl
|
||||
}
|
||||
|
||||
void ShaderRenderer::setBgOptions(const render::BgOptions& bg)
|
||||
{
|
||||
m_bgOptions = bg;
|
||||
|
@ -39,6 +39,7 @@ namespace app {
|
||||
void setRefLayersVisiblity(const bool visible) override;
|
||||
void setNonactiveLayersOpacity(const int opacity) override;
|
||||
void setNewBlendMethod(const bool newBlend) override;
|
||||
void setComposeGroups(const bool composeGroups) override;
|
||||
void setBgOptions(const render::BgOptions& bg) override;
|
||||
void setProjection(const render::Projection& projection) override;
|
||||
|
||||
|
@ -37,6 +37,11 @@ void SimpleRenderer::setNewBlendMethod(const bool newBlend)
|
||||
m_render.setNewBlend(newBlend);
|
||||
}
|
||||
|
||||
void SimpleRenderer::setComposeGroups(const bool composeGroups)
|
||||
{
|
||||
m_render.setComposeGroups(composeGroups);
|
||||
}
|
||||
|
||||
void SimpleRenderer::setBgOptions(const render::BgOptions& bg)
|
||||
{
|
||||
m_render.setBgOptions(bg);
|
||||
|
@ -24,6 +24,7 @@ namespace app {
|
||||
void setRefLayersVisiblity(const bool visible) override;
|
||||
void setNonactiveLayersOpacity(const int opacity) override;
|
||||
void setNewBlendMethod(const bool newBlend) override;
|
||||
void setComposeGroups(bool composeGroups) override;
|
||||
void setBgOptions(const render::BgOptions& bg) override;
|
||||
void setProjection(const render::Projection& projection) override;
|
||||
|
||||
|
@ -107,6 +107,7 @@ void render_sprite(Image* dst,
|
||||
{
|
||||
render::Render render;
|
||||
render.setNewBlend(true);
|
||||
render.setComposeGroups(Preferences::instance().experimental.composeGroups());
|
||||
render.renderSprite(
|
||||
dst, sprite, frame,
|
||||
gfx::Clip(x, y,
|
||||
|
@ -20,6 +20,7 @@
|
||||
#include "app/script/engine.h"
|
||||
#include "app/script/luacpp.h"
|
||||
#include "app/script/userdata.h"
|
||||
#include "app/pref/preferences.h"
|
||||
#include "app/tx.h"
|
||||
#include "doc/layer.h"
|
||||
#include "doc/layer_tilemap.h"
|
||||
@ -130,7 +131,7 @@ int Layer_get_name(lua_State* L)
|
||||
int Layer_get_opacity(lua_State* L)
|
||||
{
|
||||
auto layer = get_docobj<Layer>(L, 1);
|
||||
if (layer->isImage()) {
|
||||
if (layer->isImage() || (layer->isGroup() && app::Preferences::instance().experimental.composeGroups())) {
|
||||
lua_pushinteger(L, static_cast<LayerImage*>(layer)->opacity());
|
||||
return 1;
|
||||
}
|
||||
@ -141,7 +142,7 @@ int Layer_get_opacity(lua_State* L)
|
||||
int Layer_get_blendMode(lua_State* L)
|
||||
{
|
||||
auto layer = get_docobj<Layer>(L, 1);
|
||||
if (layer->isImage()) {
|
||||
if (layer->isImage() || (layer->isGroup() && app::Preferences::instance().experimental.composeGroups())) {
|
||||
lua_pushinteger(
|
||||
L, int(base::convert_to<app::script::BlendMode>(
|
||||
static_cast<LayerImage*>(layer)->blendMode())));
|
||||
@ -261,7 +262,7 @@ int Layer_set_opacity(lua_State* L)
|
||||
{
|
||||
auto layer = get_docobj<Layer>(L, 1);
|
||||
const int opacity = lua_tointeger(L, 2);
|
||||
if (layer->isImage()) {
|
||||
if (layer->isImage() || layer->isGroup()) {
|
||||
Tx tx(layer->sprite());
|
||||
tx(new cmd::SetLayerOpacity(static_cast<LayerImage*>(layer), opacity));
|
||||
tx.commit();
|
||||
@ -273,7 +274,7 @@ int Layer_set_blendMode(lua_State* L)
|
||||
{
|
||||
auto layer = get_docobj<Layer>(L, 1);
|
||||
auto blendMode = app::script::BlendMode(lua_tointeger(L, 2));
|
||||
if (layer->isImage()) {
|
||||
if (layer->isImage() || layer->isGroup()) {
|
||||
Tx tx(layer->sprite());
|
||||
tx(new cmd::SetLayerBlendMode(static_cast<LayerImage*>(layer),
|
||||
base::convert_to<doc::BlendMode>(blendMode)));
|
||||
|
@ -670,6 +670,7 @@ void Editor::drawOneSpriteUnclippedRect(ui::Graphics* g, const gfx::Rect& sprite
|
||||
// the original cel) before it can be used by the RenderEngine.
|
||||
m_document->notifyExposeSpritePixels(m_sprite, gfx::Region(expose));
|
||||
|
||||
m_renderEngine->setComposeGroups(pref.experimental.composeGroups());
|
||||
m_renderEngine->setNewBlendMethod(pref.experimental.newBlend());
|
||||
m_renderEngine->setRefLayersVisiblity(true);
|
||||
m_renderEngine->setSelectedLayer(m_layer);
|
||||
|
@ -26,6 +26,8 @@ EditorRender::EditorRender()
|
||||
{
|
||||
m_renderer->setNewBlendMethod(
|
||||
Preferences::instance().experimental.newBlend());
|
||||
m_renderer->setComposeGroups(
|
||||
Preferences::instance().experimental.composeGroups());
|
||||
}
|
||||
|
||||
EditorRender::~EditorRender()
|
||||
@ -72,6 +74,11 @@ void EditorRender::setNewBlendMethod(const bool newBlend)
|
||||
m_renderer->setNewBlendMethod(newBlend);
|
||||
}
|
||||
|
||||
void EditorRender::setComposeGroups(const bool composeGroups)
|
||||
{
|
||||
m_renderer->setComposeGroups(composeGroups);
|
||||
}
|
||||
|
||||
void EditorRender::setProjection(const render::Projection& projection)
|
||||
{
|
||||
m_renderer->setProjection(projection);
|
||||
|
@ -57,6 +57,7 @@ namespace app {
|
||||
void setRefLayersVisiblity(const bool visible);
|
||||
void setNonactiveLayersOpacity(const int opacity);
|
||||
void setNewBlendMethod(const bool newBlend);
|
||||
void setComposeGroups(bool composeGroups);
|
||||
|
||||
void setProjection(const render::Projection& projection);
|
||||
|
||||
|
@ -29,6 +29,8 @@ Layer::Layer(ObjectType type, Sprite* sprite)
|
||||
, m_flags(LayerFlags(
|
||||
int(LayerFlags::Visible) |
|
||||
int(LayerFlags::Editable)))
|
||||
, m_blendmode(BlendMode::NORMAL)
|
||||
, m_opacity(255)
|
||||
{
|
||||
ASSERT(type == ObjectType::LayerImage ||
|
||||
type == ObjectType::LayerGroup ||
|
||||
@ -228,8 +230,6 @@ Cel* Layer::cel(frame_t frame) const
|
||||
|
||||
LayerImage::LayerImage(ObjectType type, Sprite* sprite)
|
||||
: Layer(type, sprite)
|
||||
, m_blendmode(BlendMode::NORMAL)
|
||||
, m_opacity(255)
|
||||
{
|
||||
}
|
||||
|
||||
|
@ -125,6 +125,12 @@ namespace doc {
|
||||
m_flags = LayerFlags(int(m_flags) & ~int(flags));
|
||||
}
|
||||
|
||||
BlendMode blendMode() const { return m_blendmode; }
|
||||
void setBlendMode(BlendMode blendmode) { m_blendmode = blendmode; }
|
||||
|
||||
int opacity() const { return m_opacity; }
|
||||
void setOpacity(int opacity) { m_opacity = opacity; }
|
||||
|
||||
virtual Grid grid() const;
|
||||
virtual Cel* cel(frame_t frame) const;
|
||||
virtual void getCels(CelList& cels) const = 0;
|
||||
@ -136,6 +142,9 @@ namespace doc {
|
||||
LayerGroup* m_parent; // parent layer
|
||||
LayerFlags m_flags; // stack order cannot be changed
|
||||
|
||||
BlendMode m_blendmode;
|
||||
int m_opacity;
|
||||
|
||||
// Disable assigment
|
||||
Layer& operator=(const Layer& other);
|
||||
};
|
||||
@ -151,12 +160,6 @@ namespace doc {
|
||||
|
||||
virtual int getMemSize() const override;
|
||||
|
||||
BlendMode blendMode() const { return m_blendmode; }
|
||||
void setBlendMode(BlendMode blendmode) { m_blendmode = blendmode; }
|
||||
|
||||
int opacity() const { return m_opacity; }
|
||||
void setOpacity(int opacity) { m_opacity = opacity; }
|
||||
|
||||
void addCel(Cel *cel);
|
||||
void removeCel(Cel *cel);
|
||||
void moveCel(Cel *cel, frame_t frame);
|
||||
@ -180,9 +183,7 @@ namespace doc {
|
||||
|
||||
private:
|
||||
void destroyAllCels();
|
||||
|
||||
BlendMode m_blendmode;
|
||||
int m_opacity;
|
||||
|
||||
CelList m_cels; // List of all cels inside this layer used by frames.
|
||||
};
|
||||
|
||||
|
@ -35,21 +35,12 @@ void RenderPlan::addLayer(const Layer* layer,
|
||||
if (!layer->isVisible())
|
||||
return;
|
||||
|
||||
switch (layer->type()) {
|
||||
|
||||
case ObjectType::LayerImage:
|
||||
case ObjectType::LayerTilemap: {
|
||||
m_items.emplace_back(m_order, layer, layer->cel(frame));
|
||||
break;
|
||||
if (layer->isGroup() && !m_composeGroups) {
|
||||
for (auto *const child : static_cast<const LayerGroup*>(layer)->layers()) {
|
||||
addLayer(child, frame);
|
||||
}
|
||||
|
||||
case ObjectType::LayerGroup: {
|
||||
for (const auto child : static_cast<const LayerGroup*>(layer)->layers()) {
|
||||
addLayer(child, frame);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
} else {
|
||||
m_items.emplace_back(m_order, layer, layer->cel(frame));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -33,6 +33,8 @@ namespace doc {
|
||||
using Items = std::vector<Item>;
|
||||
|
||||
RenderPlan();
|
||||
RenderPlan(const bool composeGroups)
|
||||
: m_composeGroups(composeGroups) { }
|
||||
|
||||
const Items& items() const {
|
||||
if (m_processZIndex)
|
||||
@ -49,6 +51,7 @@ namespace doc {
|
||||
int m_order = 0;
|
||||
mutable Items m_items;
|
||||
mutable bool m_processZIndex = true;
|
||||
bool m_composeGroups = false;
|
||||
};
|
||||
|
||||
} // namespace doc
|
||||
|
@ -25,16 +25,19 @@ using namespace doc;
|
||||
#define HELPER_LOG(a, b) \
|
||||
a->layer()->name() << " instead of " << b->layer()->name()
|
||||
|
||||
#define EXPECT_PLAN(a, b, c, d) \
|
||||
{ \
|
||||
RenderPlan plan; \
|
||||
plan.addLayer(spr->root(), 0); \
|
||||
const auto& items = plan.items(); \
|
||||
EXPECT_EQ(a, items[0].cel) << HELPER_LOG(items[0].cel, a); \
|
||||
EXPECT_EQ(b, items[1].cel) << HELPER_LOG(items[1].cel, b); \
|
||||
EXPECT_EQ(c, items[2].cel) << HELPER_LOG(items[2].cel, c); \
|
||||
EXPECT_EQ(d, items[3].cel) << HELPER_LOG(items[3].cel, d); \
|
||||
}
|
||||
#define HELPER_LOG_LAYER(a, b) \
|
||||
a->name() << " instead of " << b->name()
|
||||
|
||||
#define EXPECT_PLAN(a, b, c, d) \
|
||||
{ \
|
||||
RenderPlan plan; \
|
||||
plan.addLayer(spr->root(), 0); \
|
||||
const auto items = plan.items(); \
|
||||
EXPECT_EQ(a, items[0].cel) << HELPER_LOG(items[0].cel, a); \
|
||||
EXPECT_EQ(b, items[1].cel) << HELPER_LOG(items[1].cel, b); \
|
||||
EXPECT_EQ(c, items[2].cel) << HELPER_LOG(items[2].cel, c); \
|
||||
EXPECT_EQ(d, items[3].cel) << HELPER_LOG(items[3].cel, d); \
|
||||
} \
|
||||
|
||||
TEST(RenderPlan, ZIndex)
|
||||
{
|
||||
@ -134,6 +137,59 @@ TEST(RenderPlan, ZIndexBugWithEmptyCels)
|
||||
d->setZIndex(-3); EXPECT_PLAN(d, a, b);
|
||||
}
|
||||
|
||||
TEST(RenderPlan, DontAddChildrenOnComposeGroupFlag)
|
||||
{
|
||||
#undef EXPECT_PLAN
|
||||
#define EXPECT_PLAN(a, b, c, d) \
|
||||
{ \
|
||||
RenderPlan plan(true), subplan(true); \
|
||||
plan.addLayer(spr->root(), 0); \
|
||||
const auto& items = plan.items(); \
|
||||
EXPECT_EQ(spr->root(), items[0].layer) << HELPER_LOG_LAYER(items[0].layer, spr->root()); \
|
||||
EXPECT_EQ(items.size(), 1); \
|
||||
const auto& subItems = subplan.items(); \
|
||||
for (const Layer* child : static_cast<const LayerGroup*>(spr->root())->layers()) \
|
||||
if (child->isVisible()) subplan.addLayer(child, 0); \
|
||||
EXPECT_EQ(subItems.size(), 4); \
|
||||
EXPECT_EQ(a, subItems[0].layer) << HELPER_LOG_LAYER(subItems[0].layer, a); \
|
||||
EXPECT_EQ(b, subItems[1].layer) << HELPER_LOG_LAYER(subItems[1].layer, b); \
|
||||
EXPECT_EQ(c, subItems[2].layer) << HELPER_LOG_LAYER(subItems[2].layer, c); \
|
||||
EXPECT_EQ(d, subItems[3].layer) << HELPER_LOG_LAYER(subItems[2].layer, d); \
|
||||
}
|
||||
|
||||
auto doc = std::make_shared<Document>();
|
||||
ImageSpec spec(ColorMode::INDEXED, 2, 2);
|
||||
Sprite* spr;
|
||||
doc->sprites().add(spr = Sprite::MakeStdSprite(spec));
|
||||
|
||||
LayerImage
|
||||
*lay0 = static_cast<LayerImage*>(spr->root()->firstLayer()),
|
||||
*lay1 = new LayerImage(spr),
|
||||
*lay2 = new LayerImage(spr);
|
||||
|
||||
LayerGroup
|
||||
*group0 = new LayerGroup(spr),
|
||||
*group1 = new LayerGroup(spr);
|
||||
|
||||
lay0->setName("a");
|
||||
lay1->setName("b");
|
||||
lay2->setName("c");
|
||||
group0->setName("g0");
|
||||
group1->setName("g1");
|
||||
|
||||
group0->addLayer(lay1);
|
||||
|
||||
Cel *a, *b;
|
||||
lay1->addCel(a = new Cel(0, ImageRef(Image::create(spec))));
|
||||
lay2->addCel(b = new Cel(0, ImageRef(Image::create(spec))));
|
||||
|
||||
spr->root()->insertLayer(group0, lay0);
|
||||
spr->root()->insertLayer(group1, group0);
|
||||
spr->root()->insertLayer(lay2, group1);
|
||||
|
||||
EXPECT_PLAN(lay0, group0, group1, lay2);
|
||||
}
|
||||
|
||||
int main(int argc, char** argv)
|
||||
{
|
||||
::testing::InitGoogleTest(&argc, argv);
|
||||
|
@ -565,6 +565,11 @@ void Render::setNewBlend(const bool newBlend)
|
||||
m_newBlendMethod = newBlend;
|
||||
}
|
||||
|
||||
void Render::setComposeGroups(const bool composeGroup)
|
||||
{
|
||||
m_composeGroups = composeGroup;
|
||||
}
|
||||
|
||||
void Render::setProjection(const Projection& projection)
|
||||
{
|
||||
m_proj = projection;
|
||||
@ -669,7 +674,7 @@ void Render::renderLayer(
|
||||
|
||||
m_globalOpacity = 255;
|
||||
|
||||
doc::RenderPlan plan;
|
||||
doc::RenderPlan plan(m_composeGroups);
|
||||
plan.addLayer(layer, frame);
|
||||
renderPlan(
|
||||
plan, dstImage, area,
|
||||
@ -768,7 +773,7 @@ void Render::renderSpriteLayers(Image* dstImage,
|
||||
frame_t frame,
|
||||
CompositeImageFunc compositeImage)
|
||||
{
|
||||
doc::RenderPlan plan;
|
||||
doc::RenderPlan plan(m_composeGroups);
|
||||
plan.addLayer(m_sprite->root(), frame);
|
||||
|
||||
// Draw the background layer.
|
||||
@ -884,7 +889,7 @@ void Render::renderOnionskin(
|
||||
else if (m_onionskin.type() == OnionskinType::RED_BLUE_TINT)
|
||||
blendMode = (frameOut < frame ? BlendMode::RED_TINT: BlendMode::BLUE_TINT);
|
||||
|
||||
doc::RenderPlan plan;
|
||||
doc::RenderPlan plan(m_composeGroups);
|
||||
plan.addLayer(onionLayer, frameIn);
|
||||
renderPlan(
|
||||
plan, dstImage,
|
||||
@ -1082,21 +1087,20 @@ void Render::renderPlan(
|
||||
}
|
||||
|
||||
if (celImage) {
|
||||
const LayerImage* imgLayer = static_cast<const LayerImage*>(layer);
|
||||
BlendMode layerBlendMode =
|
||||
(blendMode == BlendMode::UNSPECIFIED ?
|
||||
imgLayer->blendMode():
|
||||
layer->blendMode():
|
||||
blendMode);
|
||||
|
||||
ASSERT(cel->opacity() >= 0);
|
||||
ASSERT(cel->opacity() <= 255);
|
||||
ASSERT(imgLayer->opacity() >= 0);
|
||||
ASSERT(imgLayer->opacity() <= 255);
|
||||
ASSERT(layer->opacity() >= 0);
|
||||
ASSERT(layer->opacity() <= 255);
|
||||
|
||||
// Multiple three opacities: cel*layer*global (*nonactive-layer-opacity)
|
||||
int t;
|
||||
int opacity = cel->opacity();
|
||||
opacity = MUL_UN8(opacity, imgLayer->opacity(), t);
|
||||
opacity = MUL_UN8(opacity, layer->opacity(), t);
|
||||
opacity = MUL_UN8(opacity, m_globalOpacity, t);
|
||||
if (!isSelected && m_nonactiveLayersOpacity != 255)
|
||||
opacity = MUL_UN8(opacity, m_nonactiveLayersOpacity, t);
|
||||
@ -1140,10 +1144,39 @@ void Render::renderPlan(
|
||||
break;
|
||||
}
|
||||
|
||||
case ObjectType::LayerGroup:
|
||||
ASSERT(false);
|
||||
break;
|
||||
case ObjectType::LayerGroup: {
|
||||
if (!m_composeGroups) {
|
||||
ASSERT(false);
|
||||
break;
|
||||
}
|
||||
|
||||
RenderPlan subPlan(m_composeGroups);
|
||||
|
||||
for (const Layer* child : static_cast<const LayerGroup*>(layer)->layers()) {
|
||||
if (child->isVisible())
|
||||
subPlan.addLayer(child, frame);
|
||||
}
|
||||
|
||||
// We treat the group layer as a separate image so we can apply modifiers
|
||||
// in the whole group while not affecting the layers behind it.
|
||||
ImageRef groupImage(Image::createCopy(image));
|
||||
groupImage.get()->clear(0);
|
||||
|
||||
// Render the group sublayers
|
||||
// We don't apply any blend mode here, as the group layer is a separate image
|
||||
// and we want to first calculate the layer blendmodes separately and then merge the images
|
||||
renderPlan(subPlan, groupImage.get(), area, frame, compositeImage,
|
||||
render_background, render_transparent, BlendMode::UNSPECIFIED);
|
||||
|
||||
// Get the pallete of the sprite in the current frame
|
||||
Palette* pal = m_sprite->palette(frame);
|
||||
|
||||
// Render the group image in the main image, applying the group modifiers
|
||||
// The global opacity is not applied here, as it is applied in the LayerImage case.
|
||||
composite_image(image, groupImage.get(), pal, 0 ,0 , layer->opacity(), layer->blendMode());
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Draw extras
|
||||
|
@ -60,6 +60,7 @@ namespace render {
|
||||
void setRefLayersVisiblity(const bool visible);
|
||||
void setNonactiveLayersOpacity(const int opacity);
|
||||
void setNewBlend(const bool newBlend);
|
||||
void setComposeGroups(bool composeGroup);
|
||||
void setProjection(const Projection& projection);
|
||||
void setBgOptions(const BgOptions& bg);
|
||||
void setSelectedLayer(const Layer* layer);
|
||||
@ -224,6 +225,7 @@ namespace render {
|
||||
BlendMode m_previewBlendMode;
|
||||
OnionskinOptions m_onionskin;
|
||||
ImageBufferPtr m_tmpBuf;
|
||||
bool m_composeGroups = false;
|
||||
};
|
||||
|
||||
void composite_image(Image* dst,
|
||||
|
186
tests/scripts/compose_groups.lua
Normal file
186
tests/scripts/compose_groups.lua
Normal file
@ -0,0 +1,186 @@
|
||||
-- Copyright (C) 2024 Igara Studio S.A.
|
||||
--
|
||||
-- This file is released under the terms of the MIT license.
|
||||
-- Read LICENSE.txt for more information.
|
||||
|
||||
dofile('./test_utils.lua')
|
||||
|
||||
-- Enable experimental compose groups feature
|
||||
app.preferences.experimental.compose_groups = true
|
||||
|
||||
function create_group_layer()
|
||||
local s = Sprite(2, 2)
|
||||
assert(#s.layers == 1)
|
||||
|
||||
-- create two layers and a group layer
|
||||
local a = s.layers[1] a.name = "a"
|
||||
assert(#s.layers == 1)
|
||||
local b = s:newLayer() b.name = "b"
|
||||
assert(#s.layers == 2)
|
||||
local g = s:newGroup() g.name = "g"
|
||||
assert(#s.layers == 3)
|
||||
|
||||
-- b is child of g
|
||||
b.parent = g
|
||||
assert(g.parent == s)
|
||||
assert(b.parent == g)
|
||||
|
||||
assert(#s.layers == 2)
|
||||
assert(s.layers[1] == a)
|
||||
assert(s.layers[2] == g)
|
||||
|
||||
return s, a, b, g
|
||||
end
|
||||
|
||||
-- Test groups opacity to impact on children layers
|
||||
do
|
||||
assert(app.preferences.experimental.compose_groups == true)
|
||||
|
||||
local s, a, b, g = create_group_layer()
|
||||
|
||||
-- draw in b layer
|
||||
local cel = s:newCel(b, 1, Image(2, 2, ColorMode.RGB))
|
||||
local img = cel.image
|
||||
|
||||
img:drawPixel(0, 0, Color(255, 0, 0, 255))
|
||||
img:drawPixel(1, 0, Color(0, 255, 0, 255))
|
||||
img:drawPixel(0, 1, Color(0, 0, 255, 255))
|
||||
img:drawPixel(1, 1, Color(255, 255, 0, 255))
|
||||
|
||||
local r = Image(s.spec) -- render
|
||||
r:drawSprite(s, 1, 0, 0)
|
||||
|
||||
expect_clr(r:getPixel(0, 0), app.pixelColor.rgba(255, 0, 0, 255))
|
||||
expect_clr(r:getPixel(1, 0), app.pixelColor.rgba(0, 255, 0, 255))
|
||||
expect_clr(r:getPixel(0, 1), app.pixelColor.rgba(0, 0, 255, 255))
|
||||
expect_clr(r:getPixel(1, 1), app.pixelColor.rgba(255, 255, 0, 255))
|
||||
|
||||
-- Set opacity to 50%
|
||||
g.opacity = 128
|
||||
|
||||
r = Image(s.spec)
|
||||
r:drawSprite(s, 1, 0, 0)
|
||||
|
||||
print(g.opacity)
|
||||
|
||||
print(r:getPixel(0, 0), app.pixelColor.rgba(255, 0, 0, 255))
|
||||
|
||||
-- Assert that the image is drawn with 50% opacity
|
||||
expect_clr(r:getPixel(0, 0), app.pixelColor.rgba(255, 0, 0, 128))
|
||||
expect_clr(r:getPixel(1, 0), app.pixelColor.rgba(0, 255, 0, 128))
|
||||
expect_clr(r:getPixel(0, 1), app.pixelColor.rgba(0, 0, 255, 128))
|
||||
expect_clr(r:getPixel(1, 1), app.pixelColor.rgba(255, 255, 0, 128))
|
||||
|
||||
-- Set opacity to 100%
|
||||
g.opacity = 255
|
||||
|
||||
r = Image(s.spec)
|
||||
r:drawSprite(s, 1, 0, 0)
|
||||
|
||||
expect_eq(g.opacity, 255)
|
||||
|
||||
-- Assert that the image is drawn with 100% opacity
|
||||
expect_clr(r:getPixel(0, 0), app.pixelColor.rgba(255, 0, 0, 255))
|
||||
expect_clr(r:getPixel(1, 0), app.pixelColor.rgba(0, 255, 0, 255))
|
||||
expect_clr(r:getPixel(0, 1), app.pixelColor.rgba(0, 0, 255, 255))
|
||||
expect_clr(r:getPixel(1, 1), app.pixelColor.rgba(255, 255, 0, 255))
|
||||
|
||||
-- Set group opacity to 50% and layer opacity to 50%
|
||||
g.opacity = 128
|
||||
b.opacity = 128
|
||||
|
||||
r = Image(s.spec)
|
||||
r:drawSprite(s, 1, 0, 0)
|
||||
|
||||
assert(g.opacity == 128)
|
||||
assert(b.opacity == 128)
|
||||
|
||||
-- Assert that the image is drawn with 25% opacity
|
||||
expect_clr(r:getPixel(0, 0), app.pixelColor.rgba(255, 0, 0, 64))
|
||||
expect_clr(r:getPixel(1, 0), app.pixelColor.rgba(0, 255, 0, 64))
|
||||
expect_clr(r:getPixel(0, 1), app.pixelColor.rgba(0, 0, 255, 64))
|
||||
expect_clr(r:getPixel(1, 1), app.pixelColor.rgba(255, 255, 0, 64))
|
||||
|
||||
-- Create a new layer in front of the group and check that it's not affected by the group opacity
|
||||
local c = s:newLayer()
|
||||
c.name = "c"
|
||||
|
||||
-- draw in c layer
|
||||
local cel = s:newCel(c, 1, Image(1, 1, ColorMode.RGB))
|
||||
local img = cel.image
|
||||
|
||||
img:drawPixel(0, 0, Color(255, 0, 255, 255))
|
||||
|
||||
r = Image(s.spec)
|
||||
r:drawSprite(s, 1, 0, 0)
|
||||
|
||||
-- Assert that the first pixel is drawn with 100% opacity and the remaining ones with 25% opacity
|
||||
expect_clr(r:getPixel(0, 0), app.pixelColor.rgba(255, 0, 255, 255))
|
||||
expect_clr(r:getPixel(1, 0), app.pixelColor.rgba(0, 255, 0, 64))
|
||||
expect_clr(r:getPixel(0, 1), app.pixelColor.rgba(0, 0, 255, 64))
|
||||
expect_clr(r:getPixel(1, 1), app.pixelColor.rgba(255, 255, 0, 64))
|
||||
|
||||
end
|
||||
|
||||
do -- test group blend to impact children layers
|
||||
assert(app.preferences.experimental.compose_groups == true)
|
||||
|
||||
local s, a, b, g = create_group_layer()
|
||||
|
||||
-- draw all black in layer a
|
||||
local cel = s:newCel(a, 1, Image(2, 2, ColorMode.RGB))
|
||||
local img = cel.image
|
||||
|
||||
img:drawPixel(0, 0, Color(0, 0, 0, 255))
|
||||
img:drawPixel(1, 0, Color(0, 0, 0, 255))
|
||||
img:drawPixel(0, 1, Color(0, 0, 0, 255))
|
||||
img:drawPixel(1, 1, Color(0, 0, 0, 255))
|
||||
|
||||
-- draw in b layer
|
||||
local cel = s:newCel(b, 1, Image(2, 2, ColorMode.RGB))
|
||||
local img = cel.image
|
||||
|
||||
img:drawPixel(0, 0, Color(255, 0, 0, 255))
|
||||
img:drawPixel(1, 0, Color(0, 255, 0, 255))
|
||||
img:drawPixel(0, 1, Color(0, 0, 255, 255))
|
||||
img:drawPixel(1, 1, Color(255, 255, 0, 255))
|
||||
|
||||
local r = Image(s.spec) -- render
|
||||
r:drawSprite(s, 1, 0, 0)
|
||||
|
||||
expect_clr(r:getPixel(0, 0), app.pixelColor.rgba(255, 0, 0, 255))
|
||||
expect_clr(r:getPixel(1, 0), app.pixelColor.rgba(0, 255, 0, 255))
|
||||
expect_clr(r:getPixel(0, 1), app.pixelColor.rgba(0, 0, 255, 255))
|
||||
expect_clr(r:getPixel(1, 1), app.pixelColor.rgba(255, 255, 0, 255))
|
||||
|
||||
-- Set blend to MULTIPLY
|
||||
g.blendMode = BlendMode.MULTIPLY
|
||||
|
||||
r = Image(s.spec)
|
||||
r:drawSprite(s, 1, 0, 0)
|
||||
|
||||
-- Assert that the image is drawn with MULTIPLY blend mode
|
||||
expect_clr(r:getPixel(0, 0), app.pixelColor.rgba(0, 0, 0, 255))
|
||||
expect_clr(r:getPixel(1, 0), app.pixelColor.rgba(0, 0, 0, 255))
|
||||
expect_clr(r:getPixel(0, 1), app.pixelColor.rgba(0, 0, 0, 255))
|
||||
expect_clr(r:getPixel(1, 1), app.pixelColor.rgba(0, 0, 0, 255))
|
||||
|
||||
-- Create a new layer in front of the group and check that it's not affected by the group blend mode
|
||||
local c = s:newLayer()
|
||||
c.name = "c"
|
||||
|
||||
-- draw in c layer
|
||||
local cel = s:newCel(c, 1, Image(1, 1, ColorMode.RGB))
|
||||
local img = cel.image
|
||||
|
||||
img:drawPixel(0, 0, Color(255, 0, 255, 255))
|
||||
|
||||
r = Image(s.spec)
|
||||
r:drawSprite(s, 1, 0, 0)
|
||||
|
||||
-- Assert that the first pixel is drawn with Normal blend mode and the remaining ones with Multiply blend mode
|
||||
expect_clr(r:getPixel(0, 0), app.pixelColor.rgba(255, 0, 255, 255))
|
||||
expect_clr(r:getPixel(1, 0), app.pixelColor.rgba(0, 0, 0, 255))
|
||||
expect_clr(r:getPixel(0, 1), app.pixelColor.rgba(0, 0, 0, 255))
|
||||
expect_clr(r:getPixel(1, 1), app.pixelColor.rgba(0, 0, 0, 255))
|
||||
end
|
@ -42,6 +42,24 @@ local function dump_img(image)
|
||||
print('}')
|
||||
end
|
||||
|
||||
function expect_clr(color, expectedColor)
|
||||
if color ~= expectedColor then
|
||||
print(debug.traceback())
|
||||
print('Expected A == B but:')
|
||||
print(string.format(' - Value A = rgba(%d,%d,%d,%d)',
|
||||
app.pixelColor.rgbaR(color),
|
||||
app.pixelColor.rgbaG(color),
|
||||
app.pixelColor.rgbaB(color),
|
||||
app.pixelColor.rgbaA(color)))
|
||||
print(string.format(' - Value B = rgba(%d,%d,%d,%d)',
|
||||
app.pixelColor.rgbaR(expectedColor),
|
||||
app.pixelColor.rgbaG(expectedColor),
|
||||
app.pixelColor.rgbaB(expectedColor),
|
||||
app.pixelColor.rgbaA(expectedColor)))
|
||||
assert(color == expectedColor)
|
||||
end
|
||||
end
|
||||
|
||||
function expect_img(image, expectedPixels)
|
||||
local w = image.width
|
||||
local h = image.height
|
||||
|
Loading…
x
Reference in New Issue
Block a user