Add SkImage::makeRawShader

This creates SkImageShader instances that do not perform color space
conversion, nor do they premultiply their pixels (if they started out
unpremultiplied). These are useful for runtime shaders that want
non-color inputs (like normal maps or lookup tables).

Includes GM that demonstrates lack of color conversion, and lack of
premultiplication in the context of a lighting shader.

Bug: skia:10479
Change-Id: Ic07aa8b8d3407ae5f81bc075648fdcba6d4cce29
Reviewed-on: https://skia-review.googlesource.com/c/skia/+/477299
Reviewed-by: Brian Salomon <bsalomon@google.com>
Commit-Queue: Brian Osman <brianosman@google.com>
This commit is contained in:
Brian Osman 2021-11-29 12:27:00 -05:00 committed by SkCQ
parent 29af7d5971
commit 20c6a947af
7 changed files with 203 additions and 32 deletions

View File

@ -658,14 +658,45 @@ static sk_sp<SkShader> normal_map_shader() {
return effect->makeShader(nullptr, {}, nullptr, true);
}
static sk_sp<SkShader> normal_map_image_shader() {
static sk_sp<SkImage> normal_map_image() {
// Above, baked into an image:
auto surface = SkSurface::MakeRasterN32Premul(256, 256);
auto info = SkImageInfo::Make(256, 256, kN32_SkColorType, kPremul_SkAlphaType);
auto surface = SkSurface::MakeRaster(info);
SkPaint p;
p.setShader(normal_map_shader());
surface->getCanvas()->drawPaint(p);
auto image = surface->makeImageSnapshot();
return image->makeShader(SkSamplingOptions{});
return surface->makeImageSnapshot();
}
static sk_sp<SkShader> normal_map_image_shader() {
return normal_map_image()->makeShader(SkSamplingOptions{});
}
static sk_sp<SkShader> normal_map_raw_image_shader() {
return normal_map_image()->makeRawShader(SkSamplingOptions{});
}
static sk_sp<SkImage> normal_map_unpremul_image() {
auto image = normal_map_image();
SkPixmap pm;
SkAssertResult(image->peekPixels(&pm));
SkBitmap bmp;
bmp.allocPixels(image->imageInfo().makeAlphaType(kUnpremul_SkAlphaType));
// Copy all pixels over, but set alpha to 0
for (int y = 0; y < pm.height(); y++) {
for (int x = 0; x < pm.width(); x++) {
*bmp.getAddr32(x, y) = *pm.addr32(x, y) & 0x00FFFFFF;
}
}
return bmp.asImage();
}
static sk_sp<SkShader> normal_map_unpremul_image_shader() {
return normal_map_unpremul_image()->makeShader(SkSamplingOptions{});
}
static sk_sp<SkShader> normal_map_raw_unpremul_image_shader() {
return normal_map_unpremul_image()->makeRawShader(SkSamplingOptions{});
}
static sk_sp<SkShader> lit_shader(sk_sp<SkShader> normals) {
@ -706,3 +737,55 @@ DEF_SIMPLE_GM(paint_alpha_normals_rt, canvas, 512,512) {
draw_shader(256, 0, lit_shader(normal_map_shader()));
draw_shader(256, 256, lit_shader(normal_map_image_shader()));
}
DEF_SIMPLE_GM(raw_image_shader_normals_rt, canvas, 768, 512) {
// Demonstrates the utility of SkImage::makeRawShader, for non-color child shaders.
// First, make an offscreen surface, so we can control the destination color space:
auto surfInfo = SkImageInfo::Make(512, 512,
kN32_SkColorType,
kPremul_SkAlphaType,
SkColorSpace::MakeSRGB()->makeColorSpin());
auto surface = canvas->makeSurface(surfInfo);
if (!surface) {
surface = SkSurface::MakeRaster(surfInfo);
}
auto draw_shader = [](int x, int y, sk_sp<SkShader> shader, SkCanvas* canvas) {
SkPaint p;
p.setShader(shader);
canvas->save();
canvas->translate(x, y);
canvas->clipRect({0, 0, 256, 256});
canvas->drawPaint(p);
canvas->restore();
};
sk_sp<SkShader> colorNormals = normal_map_image_shader(),
rawNormals = normal_map_raw_image_shader();
// Draw our normal map as colors (will be color-rotated), and raw (untransformed)
draw_shader(0, 0, colorNormals, surface->getCanvas());
draw_shader(0, 256, rawNormals, surface->getCanvas());
// Now draw our lighting shader using the normal and raw versions of the normals as children.
// The top image will have the normals rotated (incorrectly), so the lighting is very dark.
draw_shader(256, 0, lit_shader(colorNormals), surface->getCanvas());
draw_shader(256, 256, lit_shader(rawNormals), surface->getCanvas());
// Now draw the offscreen surface back to our original canvas. If we do this naively, the image
// will be un-transformed back to the canvas' color space. That will have the effect of undoing
// the color spin on the upper-left, and APPLYING a color-spin on the bottom left. To preserve
// the intent of this GM (and make it draw consistently whether or not the original surface has
// a color space attached), we reinterpret the offscreen image as being in sRGB:
canvas->drawImage(
surface->makeImageSnapshot()->reinterpretColorSpace(SkColorSpace::MakeSRGB()), 0, 0);
// Finally, to demonstrate that raw unpremul image shaders don't premul, draw lighting two more
// times, with an unpremul normal map (containing ZERO in the alpha channel). THe top will
// premultiply the normals, resulting in totally dark lighting. The bottom will retain the RGB
// encoded normals, even with zero alpha:
draw_shader(512, 0, lit_shader(normal_map_unpremul_image_shader()), canvas);
draw_shader(512, 256, lit_shader(normal_map_raw_unpremul_image_shader()), canvas);
}

View File

@ -682,6 +682,34 @@ public:
return this->makeShader(SkTileMode::kClamp, SkTileMode::kClamp, sampling, lm);
}
/**
* makeRawShader functions like makeShader, but for images that contain non-color data.
* This includes images encoding things like normals, material properties (eg, roughness),
* heightmaps, or any other purely mathematical data that happens to be stored in an image.
* These types of images are useful with some programmable shaders (see: SkRuntimeEffect).
*
* Raw image shaders work like regular image shaders (including filtering and tiling), with
* a few major differences:
* - No color space transformation is ever applied (the color space of the image is ignored).
* - Images with an alpha type of kUnpremul are *not* automatically premultiplied.
* - Bicubic filtering is not supported. If SkSamplingOptions::useCubic is true, these
* factories will return nullptr.
*/
sk_sp<SkShader> makeRawShader(SkTileMode tmx, SkTileMode tmy, const SkSamplingOptions&,
const SkMatrix* localMatrix = nullptr) const;
sk_sp<SkShader> makeRawShader(SkTileMode tmx, SkTileMode tmy, const SkSamplingOptions& sampling,
const SkMatrix& lm) const {
return this->makeRawShader(tmx, tmy, sampling, &lm);
}
sk_sp<SkShader> makeRawShader(const SkSamplingOptions& sampling, const SkMatrix& lm) const {
return this->makeRawShader(SkTileMode::kClamp, SkTileMode::kClamp, sampling, &lm);
}
sk_sp<SkShader> makeRawShader(const SkSamplingOptions& sampling,
const SkMatrix* lm = nullptr) const {
return this->makeRawShader(SkTileMode::kClamp, SkTileMode::kClamp, sampling, lm);
}
using CubicResampler = SkCubicResampler;
/** Copies SkImage pixel address, row bytes, and SkImageInfo to pixmap, if address

View File

@ -111,10 +111,11 @@ public:
kBlenderInEffects = 88,
kNoExpandingClipOps = 89,
kBackdropScaleFactor = 90,
kRawImageShaders = 91,
// Only SKPs within the min/current picture version range (inclusive) can be read.
kMin_Version = kPictureShaderFilterParam_Version,
kCurrent_Version = kBackdropScaleFactor
kCurrent_Version = kRawImageShaders
};
};

View File

@ -148,6 +148,13 @@ sk_sp<SkShader> SkImage::makeShader(SkTileMode tmx, SkTileMode tmy,
sampling, localMatrix);
}
sk_sp<SkShader> SkImage::makeRawShader(SkTileMode tmx, SkTileMode tmy,
const SkSamplingOptions& sampling,
const SkMatrix* localMatrix) const {
return SkImageShader::MakeRaw(sk_ref_sp(const_cast<SkImage*>(this)), tmx, tmy,
sampling, localMatrix);
}
sk_sp<SkData> SkImage::encodeToData(SkEncodedImageFormat type, int quality) const {
// Context TODO: Elevate GrDirectContext requirement to public API.
auto dContext = as_IB(this)->directContext();

View File

@ -71,14 +71,21 @@ SkImageShader::SkImageShader(sk_sp<SkImage> img,
SkTileMode tmx, SkTileMode tmy,
const SkSamplingOptions& sampling,
const SkMatrix* localMatrix,
bool raw,
bool clampAsIfUnpremul)
: INHERITED(localMatrix)
, fImage(std::move(img))
, fSampling(sampling)
, fTileModeX(optimize(tmx, fImage->width()))
, fTileModeY(optimize(tmy, fImage->height()))
, fClampAsIfUnpremul(clampAsIfUnpremul)
{}
: INHERITED(localMatrix)
, fImage(std::move(img))
, fSampling(sampling)
, fTileModeX(optimize(tmx, fImage->width()))
, fTileModeY(optimize(tmy, fImage->height()))
, fRaw(raw)
, fClampAsIfUnpremul(clampAsIfUnpremul) {
// These options should never appear together:
SkASSERT(!fRaw || !fClampAsIfUnpremul);
// Bicubic filtering of raw image shaders would add a surprising clamp - so we don't support it
SkASSERT(!fRaw || !fSampling.useCubic);
}
// just used for legacy-unflattening
enum class LegacyFilterEnum {
@ -122,7 +129,11 @@ sk_sp<SkFlattenable> SkImageShader::CreateProc(SkReadBuffer& buffer) {
return nullptr;
}
return SkImageShader::Make(std::move(img), tmx, tmy, sampling, &localMatrix);
bool raw = buffer.isVersionLT(SkPicturePriv::Version::kRawImageShaders) ? false
: buffer.readBool();
return raw ? SkImageShader::MakeRaw(std::move(img), tmx, tmy, sampling, &localMatrix)
: SkImageShader::Make(std::move(img), tmx, tmy, sampling, &localMatrix);
}
void SkImageShader::flatten(SkWriteBuffer& buffer) const {
@ -134,6 +145,8 @@ void SkImageShader::flatten(SkWriteBuffer& buffer) const {
buffer.writeMatrix(this->getLocalMatrix());
buffer.writeImage(fImage.get());
SkASSERT(fClampAsIfUnpremul == false);
buffer.writeBool(fRaw);
}
bool SkImageShader::isOpaque() const {
@ -263,9 +276,22 @@ sk_sp<SkShader> SkImageShader::Make(sk_sp<SkImage> image,
if (!image) {
return sk_make_sp<SkEmptyShader>();
}
return sk_sp<SkShader>{
new SkImageShader(image, tmx, tmy, options, localMatrix, clampAsIfUnpremul)
};
return sk_sp<SkShader>{new SkImageShader(
image, tmx, tmy, options, localMatrix, /*raw=*/false, clampAsIfUnpremul)};
}
sk_sp<SkShader> SkImageShader::MakeRaw(sk_sp<SkImage> image,
SkTileMode tmx, SkTileMode tmy,
const SkSamplingOptions& options,
const SkMatrix* localMatrix) {
if (options.useCubic) {
return nullptr;
}
if (!image) {
return sk_make_sp<SkEmptyShader>();
}
return sk_sp<SkShader>{new SkImageShader(
image, tmx, tmy, options, localMatrix, /*raw=*/true, /*clampAsIfUnpremul=*/false)};
}
///////////////////////////////////////////////////////////////////////////////////////////////////
@ -292,16 +318,19 @@ std::unique_ptr<GrFragmentProcessor> SkImageShader::asFragmentProcessor(
return nullptr;
}
fp = GrColorSpaceXformEffect::Make(std::move(fp),
fImage->colorSpace(),
fImage->alphaType(),
args.fDstColorInfo->colorSpace(),
kPremul_SkAlphaType);
if (fImage->isAlphaOnly()) {
return GrBlendFragmentProcessor::Make(std::move(fp), nullptr, SkBlendMode::kDstIn);
} else {
return fp;
if (!fRaw) {
fp = GrColorSpaceXformEffect::Make(std::move(fp),
fImage->colorSpace(),
fImage->alphaType(),
args.fDstColorInfo->colorSpace(),
kPremul_SkAlphaType);
if (fImage->isAlphaOnly()) {
fp = GrBlendFragmentProcessor::Make(std::move(fp), nullptr, SkBlendMode::kDstIn);
}
}
return fp;
}
#endif
@ -519,7 +548,7 @@ bool SkImageShader::doStages(const SkStageRec& rec, TransformShader* updater) co
SkAlphaType at = pm.alphaType();
// Color for alpha-only images comes from the paint.
if (SkColorTypeIsAlphaOnly(pm.colorType())) {
if (SkColorTypeIsAlphaOnly(pm.colorType()) && !fRaw) {
SkColor4f rgb = rec.fPaint.getColor4f();
p->append_set_rgb(alloc, rgb);
@ -536,9 +565,9 @@ bool SkImageShader::doStages(const SkStageRec& rec, TransformShader* updater) co
}
// Transform color space and alpha type to match shader convention (dst CS, premul alpha).
alloc->make<SkColorSpaceXformSteps>(cs, at,
rec.fDstCS, kPremul_SkAlphaType)
->apply(p);
if (!fRaw) {
alloc->make<SkColorSpaceXformSteps>(cs, at, rec.fDstCS, kPremul_SkAlphaType)->apply(p);
}
return true;
};
@ -913,7 +942,7 @@ skvm::Color SkImageShader::makeProgram(
// Alpha-only images get their color from the paint (already converted to dst color space).
SkColorSpace* cs = upper.colorSpace();
SkAlphaType at = upper.alphaType();
if (SkColorTypeIsAlphaOnly(upper.colorType())) {
if (SkColorTypeIsAlphaOnly(upper.colorType()) && !fRaw) {
c.r = paint.r;
c.g = paint.g;
c.b = paint.b;
@ -934,5 +963,7 @@ skvm::Color SkImageShader::makeProgram(
c.b = clamp(c.b, 0.0f, limit);
}
return SkColorSpaceXformSteps{cs,at, dst.colorSpace(),dst.alphaType()}.program(p, uniforms, c);
return fRaw ? c
: SkColorSpaceXformSteps{cs, at, dst.colorSpace(), dst.alphaType()}.program(
p, uniforms, c);
}

View File

@ -21,6 +21,12 @@ public:
const SkMatrix* localMatrix,
bool clampAsIfUnpremul = false);
static sk_sp<SkShader> MakeRaw(sk_sp<SkImage>,
SkTileMode tmx,
SkTileMode tmy,
const SkSamplingOptions&,
const SkMatrix* localMatrix);
bool isOpaque() const override;
#if SK_SUPPORT_GPU
@ -37,7 +43,8 @@ private:
SkTileMode tmy,
const SkSamplingOptions&,
const SkMatrix* localMatrix,
bool clampAsIfUnpremul = false);
bool raw,
bool clampAsIfUnpremul);
void flatten(SkWriteBuffer&) const override;
#ifdef SK_ENABLE_LEGACY_SHADERCONTEXT
@ -66,6 +73,7 @@ private:
const SkSamplingOptions fSampling;
const SkTileMode fTileModeX;
const SkTileMode fTileModeY;
const bool fRaw;
const bool fClampAsIfUnpremul;
friend class SkShaderBase;

View File

@ -13,6 +13,7 @@
#include "include/core/SkTypes.h"
#include "include/gpu/GrDirectContext.h"
#include "tests/Test.h"
#include "tools/Resources.h"
static void test_bitmap_equality(skiatest::Reporter* reporter, SkBitmap& bm1, SkBitmap& bm2) {
REPORTER_ASSERT(reporter, bm1.computeByteSize() == bm2.computeByteSize());
@ -132,3 +133,15 @@ DEF_GPUTEST_FOR_RENDERING_CONTEXTS(ImageNewShader_GPU, reporter, ctxInfo) {
// RASTER -> GPU
raster_to_gpu(reporter, dContext);
}
DEF_TEST(ImageRawShader, reporter) {
auto image = GetResourceAsImage("images/mandrill_32.png");
REPORTER_ASSERT(reporter, image);
// We should be able to turn this into a "raw" image shader:
REPORTER_ASSERT(reporter, image->makeRawShader(SkSamplingOptions{}));
// ... but not if we request cubic filtering
REPORTER_ASSERT(reporter,
!image->makeRawShader(SkSamplingOptions{SkCubicResampler::Mitchell()}));
}