← 블로그 목록

[Core2026] ETC - Blending

Core2026 › Engine › CPU Engine › ETC

@Nor-s
  • #ThorVG
  • #Core
  • #CPU
  • #Blending
목차

1. Pipeline Overview

One pair of overlapping rounded rectangles follows Paint::blend, Canvas update, SwShapeTask and RLE preparation, raster dispatch, destination reads, coverage, and Surface writes. Every section loops independently; final comparisons remain fixed.
0:00
코드 보기: 블렌딩 예제와 CPU 처리

두 Shape의 RLE를 준비하고 선택한 블렌딩 모드로 대상 픽셀에 합성합니다.

auto d = Shape::gen();
d->appendRect(2, 1, 9, 7, 1.5f, 1.5f);
d->fill(48, 136, 208);

auto s = Shape::gen();
s->appendRect(6, 4, 9, 7, 1.5f, 1.5f);
s->fill(230, 97, 33);
s->blend(BlendMethod::Multiply);  // Paint::blend()

canvas->add(d);
canvas->add(s);
canvas->update();
canvas->draw(true);
canvas->sync();
17개 모드를 처음부터 함께 표시합니다. 같은 source가 겹쳤다 떨어지는 동작을 반복하며 CPU에서 추출한 겹침 색을 동시에 비교합니다.
0:00

2. BlendMethod 도표

기호의미
s, d0–1로 정규화한 불투명 source / destination의 RGB 채널
G(d)d ≤ 0.25이면 ((16d−12)d+4)d, 아니면 sqrt(d)
도표의 식색 연산 개념식. CPU의 8비트 반올림·alpha 처리는 아래 구현 코드 기준
계열BlendMethod색 연산 / 분기CPU 함수
기본Normal불투명: s, 그 외 alpha blendingblender = nullptr, 일반 Raster
어둡게Multiplys × dblendMultiply
어둡게Darkenmin(s,d)blendDarken
어둡게ColorBurn1−min(1,(1−d)/s); d=1 → 1, 그 외 s=0 → 0blendColorBurn
밝게Screens+d−sdblendScreen
밝게Lightenmax(s,d)blendLighten
밝게ColorDodgemin(1,d/(1−s)); d=0 → 0, 그 외 s=1 → 1blendColorDodge
밝게Addmin(1,s+d)blendAdd
대비Overlayd < 0.5: 2sd, 그 외 1−2(1−s)(1−d)blendOverlay
대비HardLights < 0.5: 2sd, 그 외 1−2(1−s)(1−d)blendHardLight
대비SoftLights ≤ 0.5: d−(1−2s)d(1−d), 그 외 d+(2s−1)(G(d)−d)blendSoftLight
차이Differenceabs(s−d)blendDifference
차이Exclusions+d−2sdblendExclusion
성분HueS의 색상 + D의 채도·명도blendHue
성분SaturationS의 채도 + D의 색상·명도blendSaturation
성분ColorS의 색상·채도 + D의 명도blendColor
성분LuminosityS의 명도 + D의 색상·채도blendLuminosity
별도 설정값처리
Composition255중간 합성용. Scene은 합성 flag를 표시하고 SW custom blender는 null. 17개 색상 모드와 별도

BlendMethod 선언

3. CPU 구현

단계구현처리
APIPaint::blend()BlendMethod 저장, Blend 갱신 flag 표시
Update / PrepareShapeImpl::update() → SwRenderer::prepare()SwShapeTask에 transform, clipBox, clips, opacity, flags 전달
GeometrySwShapeTask::run() → shapeGenRle()fastTrack 사각형 또는 SwRle 준비; Blend만 바뀌면 재사용 가능
모드 설정SwRenderer::blend()Surface의 함수 포인터 선택
Raster 분기_rasterRect() / _rasterRle()Mask·Matte → custom Blend → Solid / Translucent
색 연산surface->blender(surface, color, *dst)Source와 현재 Destination 결합
Coverage 255_rasterBlendingRle()Blend 결과 직접 기록
부분 CoverageINTERPOLATE(B, D, coverage)원래 Destination과 보간
Scene 합성SwRenderer::endComposite()원래 Surface 복원 → rasterDirectImage()
코드 보기: SwRenderer::blend — 모드별 함수 선택

블렌딩 모드에 맞는 픽셀 연산 함수 포인터를 설정합니다.

bool SwRenderer::blend(BlendMethod method)
{
    if (surface->blendMethod == method) return true;
    surface->blendMethod = method;

    switch (method) {
        case BlendMethod::Multiply:
            surface->blender = blendMultiply;
            break;
        case BlendMethod::Screen:
            surface->blender = blendScreen;
            break;
        case BlendMethod::Overlay:
            surface->blender = blendOverlay;
            break;
        case BlendMethod::Darken:
            surface->blender = blendDarken;
            break;
        case BlendMethod::Lighten:
            surface->blender = blendLighten;
            break;
        case BlendMethod::ColorDodge:
            surface->blender = blendColorDodge;
            break;
        case BlendMethod::ColorBurn:
            surface->blender = blendColorBurn;
            break;
        case BlendMethod::HardLight:
            surface->blender = blendHardLight;
            break;
        case BlendMethod::SoftLight:
            surface->blender = blendSoftLight;
            break;
        case BlendMethod::Difference:
            surface->blender = blendDifference;
            break;
        case BlendMethod::Exclusion:
            surface->blender = blendExclusion;
            break;
        case BlendMethod::Hue:
            surface->blender = blendHue;
            break;
        case BlendMethod::Saturation:
            surface->blender = blendSaturation;
            break;
        case BlendMethod::Color:
            surface->blender = blendColor;
            break;
        case BlendMethod::Luminosity:
            surface->blender = blendLuminosity;
            break;
        case BlendMethod::Add:
            surface->blender = blendAdd;
            break;
        default:
            surface->blender = nullptr;
            break;
    }
    return true;
}
코드 보기: RLE 분기·블렌딩·커버리지

RLE의 합성 상태와 블렌딩 모드에 따라 픽셀 처리 경로를 선택합니다.

static bool _rasterRle(SwSurface* surface, SwRle* rle, const RenderRegion& bbox, const RenderColor& c)
{
    if (!rle || rle->invalid()) return false;

    if (_compositing(surface)) {
        if (_matting(surface)) return _rasterMattedRle(surface, rle, bbox, c);
        return _rasterMaskedRle(surface, rle, bbox, c);
    }
    if (_blending(surface)) return _rasterBlendingRle(surface, rle, bbox, c);
    if (c.a == 255) return _rasterSolidRle(surface, rle, bbox, c);
    return _rasterTranslucentRle(surface, rle, bbox, c);
}

static bool _rasterBlendingRle(SwSurface* surface, const SwRle* rle, const RenderRegion& bbox, const RenderColor& c)
{
    if (surface->channelSize != sizeof(uint32_t)) {
        TVGERR("SW_ENGINE", "channel size is not 32bits?");
        return false;
    }

    auto color = surface->join(c.r, c.g, c.b, c.a);
    const SwSpan* end;
    int32_t x, len;

    for (auto span = rle->fetch(bbox, &end); span < end; ++span) {
        if (!span->fetch(bbox, x, len)) continue;
        auto dst = &surface->buf32[span->y * surface->stride + x];
        if (span->coverage == 255) {
            for (auto x = 0; x < len; ++x, ++dst) {
                *dst = surface->blender(surface, color, *dst);
            }
        } else {
            for (auto x = 0; x < len; ++x, ++dst) {
                *dst = INTERPOLATE(surface->blender(surface, color, *dst), *dst, span->coverage);
            }
        }
    }
    return true;
}
코드 보기: Normal — 단색 RLE 알파 합성

단색 RLE의 커버리지와 알파를 반영해 대상 픽셀에 합성합니다.

static bool _rasterSolidRle(SwSurface* surface, const SwRle* rle, const RenderRegion& bbox, const RenderColor& c)
{
    const SwSpan* end;
    int32_t x, len;

    //32bit channels
    if (surface->channelSize == sizeof(uint32_t)) {
        auto color = surface->join(c.r, c.g, c.b, 255);
        for (auto span = rle->fetch(bbox, &end); span < end; ++span) {
            if (!span->fetch(bbox, x, len)) continue;
            if (span->coverage == 255) rasterPixel32(surface->buf32 + span->y * surface->stride, color, x, len);
            else {
                auto dst = &surface->buf32[span->y * surface->stride + x];
                auto src = ALPHA_BLEND(color, span->coverage);
                auto ialpha = 255 - span->coverage;
                for (auto x = 0; x < len; ++x, ++dst) {
                    *dst = src + ALPHA_BLEND(*dst, ialpha);
                }
            }
        }
    //8bit grayscale
    } else if (surface->channelSize == sizeof(uint8_t)) {
        for (auto span = rle->fetch(bbox, &end); span < end; ++span) {
            if (!span->fetch(bbox, x, len)) continue;
            if (span->coverage == 255) rasterGrayscale8(surface->buf8, span->coverage, span->y * surface->stride + x, len);
            else {
                auto dst = &surface->buf8[span->y * surface->stride + x];
                auto ialpha = 255 - span->coverage;
                for (auto x = 0; x < len; ++x, ++dst) {
                    *dst = span->coverage + MULTIPLY(*dst, ialpha);
                }
            }
        }
    }
    return true;
}
코드 보기: 정수 픽셀 연산 — MULTIPLY / ALPHA_BLEND / INTERPOLATE

8비트 정수로 알파 곱셈과 픽셀 보간을 계산합니다.

static inline uint8_t MULTIPLY(uint8_t c, uint8_t a)
{
    return (((c) * (a) + 0xff) >> 8);
}

static inline uint32_t ALPHA_BLEND(uint32_t c, uint32_t a)
{
    ++a;
    return (((((c >> 8) & 0x00ff00ff) * a) & 0xff00ff00) + ((((c & 0x00ff00ff) * a) >> 8) & 0x00ff00ff));
}

static inline uint32_t INTERPOLATE(uint32_t s, uint32_t d, uint8_t a)
{
    return (((((((s >> 8) & 0xff00ff) - ((d >> 8) & 0xff00ff)) * a) + (d & 0xff00ff00)) & 0xff00ff00) + ((((((s & 0xff00ff) - (d & 0xff00ff)) * a) >> 8) + (d & 0xff00ff)) & 0xff00ff));
}

3.1 모드별 색 연산

코드 보기: Multiply / Darken / ColorBurn

곱셈·최솟값·색상 번 연산으로 겹친 픽셀의 색을 계산합니다.

uint32_t blendMultiply(TVG_UNUSED const SwSurface* surface, uint32_t s, uint32_t d)
{
    auto o = _unpremultiply(d);

    auto f = [](uint8_t s, uint8_t d) {
        return MULTIPLY(s, d);
    };

    return _premultiply(JOIN(255, f(C1(s), o.r), f(C2(s), o.g), f(C3(s), o.b)), s, o.a);
}

uint32_t blendDarken(TVG_UNUSED const SwSurface* surface, uint32_t s, uint32_t d)
{
    auto o = _unpremultiply(d);

    auto f = [](uint8_t s, uint8_t d) {
        return std::min(s, d);
    };

    return _premultiply(JOIN(255, f(C1(s), o.r), f(C2(s), o.g), f(C3(s), o.b)), s, o.a);
}

uint32_t blendColorBurn(TVG_UNUSED const SwSurface* surface, uint32_t s, uint32_t d)
{
    auto o = _unpremultiply(d);

    auto f = [](uint8_t s, uint8_t d) {
        return d == 255 ? 255 : (s == 0 ? 0 : 255 - std::min((255 - d) * 255 / s, 255));
    };

    return _premultiply(JOIN(255, f(C1(s), o.r), f(C2(s), o.g), f(C3(s), o.b)), s, o.a);
}
코드 보기: Screen / Lighten / ColorDodge / Add

스크린·최댓값·색상 닷지·덧셈 연산으로 겹친 픽셀의 색을 계산합니다.

uint32_t blendScreen(TVG_UNUSED const SwSurface* surface, uint32_t s, uint32_t d)
{
    auto f = [](uint8_t s, uint8_t d) {
        return s + d - MULTIPLY(s, d);
    };

    return JOIN(255, f(C1(s), C1(d)), f(C2(s), C2(d)), f(C3(s), C3(d)));
}

uint32_t blendLighten(TVG_UNUSED const SwSurface* surface, uint32_t s, uint32_t d)
{
    auto f = [](uint8_t s, uint8_t d) {
        return std::max(s, d);
    };

    return JOIN(255, f(C1(s), C1(d)), f(C2(s), C2(d)), f(C3(s), C3(d)));
}

uint32_t blendColorDodge(TVG_UNUSED const SwSurface* surface, uint32_t s, uint32_t d)
{
    auto o = _unpremultiply(d);

    auto f = [](uint8_t s, uint8_t d) {
        return d == 0 ? 0 : (s == 255 ? 255 : std::min(d * 255 / (255 - s), 255));
    };

    return _premultiply(JOIN(255, f(C1(s), o.r), f(C2(s), o.g), f(C3(s), o.b)), s, o.a);
}

uint32_t blendAdd(TVG_UNUSED const SwSurface* surface, uint32_t s, uint32_t d)
{
    auto f = [](uint8_t s, uint8_t d) {
        return std::min(s + d, 255);
    };

    return JOIN(255, f(C1(s), C1(d)), f(C2(s), C2(d)), f(C3(s), C3(d)));
}
코드 보기: Overlay / HardLight / SoftLight

소스와 대상의 밝기에 따라 곱셈·스크린 등 채널 연산을 적용합니다.

uint32_t blendOverlay(TVG_UNUSED const SwSurface* surface, uint32_t s, uint32_t d)
{
    auto o = _unpremultiply(d);

    auto f = [](uint8_t s, uint8_t d) {
        return (d < 128) ? std::min(255, 2 * MULTIPLY(s, d)) : (255 - std::min(255, 2 * MULTIPLY(255 - s, 255 - d)));
    };

    return _premultiply(JOIN(255, f(C1(s), o.r), f(C2(s), o.g), f(C3(s), o.b)), s, o.a);
}

uint32_t blendHardLight(TVG_UNUSED const SwSurface* surface, uint32_t s, uint32_t d)
{
    auto o = _unpremultiply(d);

    auto f = [](uint8_t s, uint8_t d) {
        return (s < 128) ? std::min(255, 2 * MULTIPLY(s, d)) : (255 - std::min(255, 2 * MULTIPLY(255 - s, 255 - d)));
    };

    return _premultiply(JOIN(255, f(C1(s), o.r), f(C2(s), o.g), f(C3(s), o.b)), s, o.a);
}

uint32_t blendSoftLight(TVG_UNUSED const SwSurface* surface, uint32_t s, uint32_t d)
{
    auto o = _unpremultiply(d);

    auto f = [](uint8_t s, uint8_t d) -> uint8_t {
        if (s <= 127) return (d - ((255 - 2 * s) * d * (255 - d)) / 65025);

        // use Look up table for skip sqrt per every pixels.
        static constexpr uint8_t SQRT_LUT[256] = {
            0, 15, 22, 27, 31, 35, 39, 42, 45, 47, 50, 52, 55, 57, 59, 61,
            63, 65, 67, 69, 71, 73, 74, 76, 78, 79, 81, 82, 84, 85, 87, 88,
            90, 91, 93, 94, 95, 97, 98, 99, 100, 102, 103, 104, 105, 107, 108, 109,
            110, 111, 112, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126,
            127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 141,
            142, 143, 144, 145, 146, 147, 148, 148, 149, 150, 151, 152, 153, 153, 154, 155,
            156, 157, 158, 158, 159, 160, 161, 162, 162, 163, 164, 165, 165, 166, 167, 168,
            168, 169, 170, 171, 171, 172, 173, 174, 174, 175, 176, 177, 177, 178, 179, 179,
            180, 181, 182, 182, 183, 184, 184, 185, 186, 186, 187, 188, 188, 189, 190, 190,
            191, 192, 192, 193, 194, 194, 195, 196, 196, 197, 198, 198, 199, 200, 200, 201,
            201, 202, 203, 203, 204, 205, 205, 206, 206, 207, 208, 208, 209, 210, 210, 211,
            211, 212, 213, 213, 214, 214, 215, 216, 216, 217, 217, 218, 218, 219, 220, 220,
            221, 221, 222, 222, 223, 224, 224, 225, 225, 226, 226, 227, 228, 228, 229, 229,
            230, 230, 231, 231, 232, 233, 233, 234, 234, 235, 235, 236, 236, 237, 237, 238,
            238, 239, 240, 240, 241, 241, 242, 242, 243, 243, 244, 244, 245, 245, 246, 246,
            247, 247, 248, 248, 249, 249, 250, 250, 251, 251, 252, 252, 253, 253, 254, 255};
        auto D = (d <= 64) ? (4 * d - (12 * d * d) / 255 + (16 * d * d * d) / 65025) : SQRT_LUT[d];
        return static_cast<uint8_t>(d + ((2 * s - 255) * (D - d)) / 255);
    };

    return _premultiply(JOIN(255, f(C1(s), o.r), f(C2(s), o.g), f(C3(s), o.b)), s, o.a);
}
코드 보기: Difference / Exclusion

채널 차이와 제외 연산으로 겹친 픽셀의 색을 계산합니다.

uint32_t blendDifference(TVG_UNUSED const SwSurface* surface, uint32_t s, uint32_t d)
{
    auto f = [](uint8_t s, uint8_t d) {
        return (s > d) ? (s - d) : (d - s);
    };

    return JOIN(255, f(C1(s), C1(d)), f(C2(s), C2(d)), f(C3(s), C3(d)));
}

uint32_t blendExclusion(TVG_UNUSED const SwSurface* surface, uint32_t s, uint32_t d)
{
    auto f = [](uint8_t s, uint8_t d) {
        return tvg::clamp(s + d - 2 * MULTIPLY(s, d), 0, 255);
    };

    return JOIN(255, f(C1(s), C1(d)), f(C2(s), C2(d)), f(C3(s), C3(d)));
}
코드 보기: Hue / Saturation / Color / Luminosity

소스와 대상의 색상·채도·휘도 성분을 조합합니다.

uint32_t blendHue(const SwSurface* surface, uint32_t s, uint32_t d)
{
    auto o = _unpremultiply(d);

    auto c1 = C1(s);
    auto c2 = C2(s);
    auto c3 = C3(s);
    _saturate(c1, c2, c3, o.r, o.g, o.b);
    _luminance(c1, c2, c3, surface->luma((c1 << 16 | c2 << 8 | c3)), surface->luma((o.r << 16 | o.g << 8 | o.b)));

    return _premultiply(JOIN(255, c1, c2, c3), s, o.a);
}

uint32_t blendSaturation(const SwSurface* surface, uint32_t s, uint32_t d)
{
    auto o = _unpremultiply(d);

    auto c1 = o.r;
    auto c2 = o.g;
    auto c3 = o.b;
    _saturate(c1, c2, c3, C1(s), C2(s), C3(s));
    _luminance(c1, c2, c3, surface->luma((c1 << 16 | c2 << 8 | c3)), surface->luma((o.r << 16 | o.g << 8 | o.b)));

    return _premultiply(JOIN(255, c1, c2, c3), s, o.a);
}

uint32_t blendColor(const SwSurface* surface, uint32_t s, uint32_t d)
{
    auto o = _unpremultiply(d);

    auto c1 = C1(s);
    auto c2 = C2(s);
    auto c3 = C3(s);
    _luminance(c1, c2, c3, surface->luma(s), surface->luma((o.r << 16 | o.g << 8 | o.b)));

    return _premultiply(JOIN(255, c1, c2, c3), s, o.a);
}

uint32_t blendLuminosity(const SwSurface* surface, uint32_t s, uint32_t d)
{
    auto o = _unpremultiply(d);

    _luminance(o.r, o.g, o.b, surface->luma((o.r << 16 | o.g << 8 | o.b)), surface->luma(s));

    return _premultiply(JOIN(255, o.r, o.g, o.b), s, o.a);
}
코드 보기: 공통 처리 — 알파·채도·휘도

알파 반영과 채도·휘도 조정에 쓰는 공통 연산을 정의합니다.

static uint32_t _premultiply(uint32_t c1, uint32_t c2, uint8_t a)
{
    if (a == 255) return c1;
    else if (a == 0) return c2;
    return ALPHA_BLEND(c1, a) + ALPHA_BLEND(c2, 255 - a);
}

static RenderColor _unpremultiply(uint32_t c)
{
    RenderColor o = {C1(c), C2(c), C3(c), A(c)};
    if (o.a > 0 && o.a < 255) {
        o.r = std::min(o.r * 255u / o.a, 255u);
        o.g = std::min(o.g * 255u / o.a, 255u);
        o.b = std::min(o.b * 255u / o.a, 255u);
    }
    return o;
}

static void _saturate(uint8_t& c1, uint8_t& c2, uint8_t& c3, const uint8_t& s1, const uint8_t& s2, const uint8_t& s3)
{
    // to saturation
    auto s = std::max(s1, std::max(s2, s3)) - std::min(s1, std::min(s2, s3));

    uint8_t v[3] = {c1, c2, c3};
    int lo = 0, mid = 1, hi = 2;

    if (v[lo] > v[mid]) std::swap(lo, mid);
    if (v[mid] > v[hi]) std::swap(mid, hi);
    if (v[lo] > v[mid]) std::swap(lo, mid);

    auto minv = v[lo];
    auto maxv = v[hi];

    if (maxv > minv) {
        v[mid] = uint8_t((int(v[mid] - minv) * s) / (maxv - minv));
        v[hi] = s;
    } else {
        v[mid] = v[hi] = 0;
    }

    v[lo] = 0;
    c1 = v[0];
    c2 = v[1];
    c3 = v[2];
}

static void _luminance(uint8_t& c1, uint8_t& c2, uint8_t& c3, int cl, int l)
{
    auto d = l - cl;
    auto r = int(c1) + d;
    auto g = int(c2) + d;
    auto b = int(c3) + d;
    auto minv = std::min(r, std::min(g, b));
    auto maxv = std::max(r, std::max(g, b));

    if (minv < 0) {
        r = l + ((r - l) * l) / (l - minv);
        g = l + ((g - l) * l) / (l - minv);
        b = l + ((b - l) * l) / (l - minv);
        maxv = std::max(r, std::max(g, b));
    }

    if (maxv > 255) {
        r = l + ((r - l) * (255 - l)) / (maxv - l);
        g = l + ((g - l) * (255 - l)) / (maxv - l);
        b = l + ((b - l) * (255 - l)) / (maxv - l);
    }

    c1 = static_cast<uint8_t>(r);
    c2 = static_cast<uint8_t>(g);
    c3 = static_cast<uint8_t>(b);
}
코드 보기: Scene — Surface 복원과 그룹 합성

이전 Surface를 복원하고 임시 버퍼의 Scene을 대상에 합성합니다.

bool SwRenderer::endComposite(RenderCompositor* cmp)
{
    if (!cmp) return false;

    auto p = static_cast<SwCompositor*>(cmp);

    //Recover Context
    surface = p->recoverSfc;
    surface->compositor = p->recoverCmp;

    //only invalid (currently used) surface can be composited
    if (p->valid) return true;
    p->valid = true;

    //Default is alpha blending
    if (p->method == MaskMethod::None) {
        return rasterDirectImage(surface, p->image, p->bbox, p->opacity);
    }

    return true;
}
소스참조
CPU renderertvgSwRenderer.cpp
CPU RastertvgSwRaster.cpp
Blend 연산tvgSwBlendOp.cpp
비교 입력 참조thorvg.example / Blending.cpp