Compartilhar via


Implementando efeitos personalizados

O Win2D fornece várias APIs para representar objetos que podem ser desenhados, que são divididos em duas categorias: imagens e efeitos. As imagens, representadas pela ICanvasImage interface, não têm entradas e podem ser desenhadas diretamente em uma determinada superfície. Por exemplo, CanvasBitmap, VirtualizedCanvasBitmap e CanvasRenderTarget são exemplos de tipos de imagem. Os efeitos, por outro lado, são representados pela ICanvasEffect interface. Eles podem ter entradas, bem como recursos adicionais, e podem aplicar lógica arbitrária para produzir suas saídas (como um efeito também é uma imagem). O Win2D inclui efeitos que envolvem a maioria dos efeitos D2D, como GaussianBlurEffect, TintEffect e LuminanceToAlphaEffect.

Imagens e efeitos também podem ser encadeados para criar gráficos arbitrários que podem ser exibidos em seu aplicativo (consulte também os documentos D2D sobre efeitos Direct2D). Juntos, eles fornecem um sistema extremamente flexível para criar gráficos complexos de maneira eficiente. No entanto, há casos em que os efeitos internos não são suficientes e talvez você queira criar seu próprio efeito Win2D. Para dar suporte a isso, o Win2D inclui um conjunto de APIs de interoperabilidade poderosas que permitem definir imagens e efeitos personalizados que podem se integrar perfeitamente ao Win2D.

Dica

Se você estiver usando C# e quiser implementar um efeito personalizado ou um grafo de efeito, é recomendável usar o ComputeSharp em vez de tentar implementar um efeito do zero. Consulte o parágrafo abaixo para obter uma explicação detalhada de como usar essa biblioteca para implementar efeitos personalizados que se integram perfeitamente ao Win2D.

APIs da plataforma: ICanvasImage, CanvasBitmap, VirtualizedCanvasBitmap, CanvasEffectICanvasLuminanceToAlphaEffectImageCanvasRenderTargetTintEffectIGraphicsEffectSourceGaussianBlurEffectID2D21Image, , ID2D1Factory1ID2D1Effect

Implementando um ICanvasImage

O cenário mais simples de oferecer suporte é criar um arquivo .ICanvasImage Como mencionamos, essa é a interface WinRT definida pelo Win2D, que representa todos os tipos de imagens com as quais o Win2D pode interoperar. Essa interface expõe apenas dois GetBounds métodos e estende IGraphicsEffectSource, que é uma interface de marcador que representa "alguma fonte de efeito".

Como você pode ver, não há APIs "funcionais" expostas por essa interface para realmente executar qualquer desenho. Para implementar seu próprio ICanvasImage objeto, você também precisará implementar a ICanvasImageInterop interface, que expõe toda a lógica necessária para que o Win2D desenhe a imagem. Essa é uma interface COM definida no cabeçalho público Microsoft.Graphics.Canvas.native.h , que é fornecida com o Win2D.

A interface é definida assim:

[uuid("E042D1F7-F9AD-4479-A713-67627EA31863")]
class ICanvasImageInterop : IUnknown
{
    HRESULT GetDevice(
        ICanvasDevice** device,
        WIN2D_GET_DEVICE_ASSOCIATION_TYPE* type);

    HRESULT GetD2DImage(
        ICanvasDevice* device,
        ID2D1DeviceContext* deviceContext,
        WIN2D_GET_D2D_IMAGE_FLAGS flags,
        float targetDpi,
        float* realizeDpi,
        ID2D1Image** ppImage);
}

E também se baseia nesses dois tipos de enumeração, do mesmo cabeçalho:

enum WIN2D_GET_DEVICE_ASSOCIATION_TYPE
{
    WIN2D_GET_DEVICE_ASSOCIATION_TYPE_UNSPECIFIED,
    WIN2D_GET_DEVICE_ASSOCIATION_TYPE_REALIZATION_DEVICE,
    WIN2D_GET_DEVICE_ASSOCIATION_TYPE_CREATION_DEVICE
}

enum WIN2D_GET_D2D_IMAGE_FLAGS
{
    WIN2D_GET_D2D_IMAGE_FLAGS_NONE,
    WIN2D_GET_D2D_IMAGE_FLAGS_READ_DPI_FROM_DEVICE_CONTEXT,
    WIN2D_GET_D2D_IMAGE_FLAGS_ALWAYS_INSERT_DPI_COMPENSATION,
    WIN2D_GET_D2D_IMAGE_FLAGS_NEVER_INSERT_DPI_COMPENSATION,
    WIN2D_GET_D2D_IMAGE_FLAGS_MINIMAL_REALIZATION,
    WIN2D_GET_D2D_IMAGE_FLAGS_ALLOW_NULL_EFFECT_INPUTS,
    WIN2D_GET_D2D_IMAGE_FLAGS_UNREALIZE_ON_FAILURE
}

Os dois GetDevice métodos e GetD2DImage são tudo o que é necessário para implementar imagens personalizadas (ou efeitos), pois fornecem ao Win2D os pontos de extensibilidade para inicializá-los em um determinado dispositivo e recuperar a imagem D2D subjacente a ser desenhada. A implementação correta desses métodos é fundamental para garantir que as coisas funcionem corretamente em todos os cenários com suporte.

Vamos examiná-los para ver como cada método funciona.

Implementação GetDevice

O GetDevice método é o mais simples dos dois. O que ele faz é recuperar o dispositivo de tela associado ao efeito, para que o Win2D possa inspecioná-lo, se necessário (por exemplo, para garantir que ele corresponda ao dispositivo em uso). O type parâmetro indica o "tipo de associação" para o dispositivo retornado.

Existem dois casos principais possíveis:

  • Se a imagem for um efeito, ela deve suportar ser "realizada" e "não realizada" em vários dispositivos. O que isso significa é: um determinado efeito é criado em um estado não inicializado, então ele pode ser realizado quando um dispositivo é passado durante o desenho e, depois disso, pode continuar sendo usado com esse dispositivo ou pode ser movido para um dispositivo diferente. Nesse caso, o efeito redefinirá seu estado interno e se realizará novamente no novo dispositivo. Isso significa que o dispositivo de tela associado pode mudar com o tempo e também pode ser null. Por causa disso, type deve ser definido como WIN2D_GET_DEVICE_ASSOCIATION_TYPE_REALIZATION_DEVICE, e o dispositivo retornado deve ser definido como o dispositivo de realização atual, se houver um disponível.
  • Algumas imagens têm um único "dispositivo proprietário" que é atribuído no momento da criação e nunca pode ser alterado. Por exemplo, esse seria o caso de uma imagem que representa uma textura, pois ela é alocada em um dispositivo específico e não pode ser movida. Quando GetDevice for chamado, ele deverá retornar o dispositivo de criação e definido type como WIN2D_GET_DEVICE_ASSOCIATION_TYPE_CREATION_DEVICE. Observe que, quando esse tipo é especificado, o dispositivo retornado não deve ser null.

Observação

O Win2D pode chamar GetDevice enquanto percorre recursivamente um grafo de efeito, o que significa que pode haver várias chamadas ativas para GetD2DImage na pilha. Por causa disso, GetDevice não deve haver um bloqueio de bloqueio na imagem atual, pois isso pode travar. Em vez disso, ele deve usar um bloqueio reentrante de maneira não bloqueada e retornar um erro se não puder ser adquirido. Isso garante que o mesmo thread que o chama recursivamente o adquira com êxito, enquanto os threads simultâneos que fazem o mesmo falharão normalmente.

Implementação GetD2DImage

GetD2DImage é onde a maior parte do trabalho acontece. Esse método é responsável por recuperar o objeto que o ID2D1Image Win2D pode desenhar, opcionalmente percebendo o efeito atual, se necessário. Isso também inclui percorrer e realizar recursivamente o grafo de efeito para todas as fontes, se houver, bem como inicializar qualquer estado que a imagem possa precisar (por exemplo, buffers constantes e outras propriedades, texturas de recursos, etc.).

A implementação exata desse método é altamente dependente do tipo de imagem e pode variar muito, mas, de um modo geral, para um efeito arbitrário, você pode esperar que o método execute as seguintes etapas:

  • Verifique se a chamada foi recursiva na mesma instância e falhe em caso afirmativo. Isso é necessário para detectar ciclos em um gráfico de efeito (por exemplo, efeito A tem efeito B como fonte e efeito B tem efeito A como fonte).
  • Adquira um bloqueio na instância de imagem para proteger contra acesso simultâneo.
  • Manipular os DPIs de destino de acordo com os sinalizadores de entrada
  • Valide se o dispositivo de entrada corresponde ao que está em uso, se houver. Se não corresponder e o efeito atual suportar a realização, não perceba o efeito.
  • Perceba o efeito no dispositivo de entrada. Isso pode incluir o registro do efeito D2D no ID2D1Factory1 objeto recuperado do dispositivo de entrada ou do contexto do dispositivo, se necessário. Além disso, todo o estado necessário deve ser definido na instância do efeito D2D que está sendo criada.
  • Percorra recursivamente todas as fontes e vincule-as ao efeito D2D.

Com relação aos sinalizadores de entrada, há vários casos possíveis que os efeitos personalizados devem tratar corretamente, para garantir a compatibilidade com todos os outros efeitos Win2D. Excluindo WIN2D_GET_D2D_IMAGE_FLAGS_NONE, os sinalizadores a serem manipulados são os seguintes:

  • WIN2D_GET_D2D_IMAGE_FLAGS_READ_DPI_FROM_DEVICE_CONTEXT: neste caso, device é garantido que não seja null. O efeito deve verificar se o destino de contexto do dispositivo é um ID2D1CommandListe, em caso afirmativo, adicionar o WIN2D_GET_D2D_IMAGE_FLAGS_ALWAYS_INSERT_DPI_COMPENSATION sinalizador. Caso contrário, ele deve definir targetDpi (o que também é garantido para não ser null) para os DPIs recuperados do contexto de entrada. Em seguida, ele deve ser removido WIN2D_GET_D2D_IMAGE_FLAGS_READ_DPI_FROM_DEVICE_CONTEXT das bandeiras.
  • WIN2D_GET_D2D_IMAGE_FLAGS_ALWAYS_INSERT_DPI_COMPENSATION e WIN2D_GET_D2D_IMAGE_FLAGS_NEVER_INSERT_DPI_COMPENSATION: usado ao definir fontes de efeito (consulte as notas abaixo).
  • WIN2D_GET_D2D_IMAGE_FLAGS_MINIMAL_REALIZATION: se definido, ignora a realização recursiva das origens do efeito e apenas retorna o efeito realizado sem outras alterações.
  • WIN2D_GET_D2D_IMAGE_FLAGS_ALLOW_NULL_EFFECT_INPUTS: se definido, as fontes de efeito que estão sendo realizadas podem ser null, se o usuário ainda não as tiver definido para uma fonte existente.
  • WIN2D_GET_D2D_IMAGE_FLAGS_UNREALIZE_ON_FAILURE: se definido e uma fonte de efeito que está sendo definida não for válida, o efeito deve ser realizado antes de falhar. Ou seja, se o erro ocorreu ao resolver as fontes de efeito depois de realizar o efeito, o efeito deve se desrealizar antes de retornar o erro ao chamador.

Com relação aos sinalizadores relacionados ao DPI, eles controlam como as fontes de efeito são definidas. Para garantir a compatibilidade com o Win2D, os efeitos devem adicionar automaticamente efeitos de compensação de DPI às suas entradas quando necessário. Eles podem controlar se esse é o caso da seguinte forma:

  • Se WIN2D_GET_D2D_IMAGE_FLAGS_MINIMAL_REALIZATION estiver definido, um efeito de compensação de DPI será necessário sempre que o inputDpi parâmetro não 0for .
  • Caso contrário, a compensação de DPI será necessária se inputDpi não for , WIN2D_GET_D2D_IMAGE_FLAGS_NEVER_INSERT_DPI_COMPENSATION não estiver definido e WIN2D_GET_D2D_IMAGE_FLAGS_ALWAYS_INSERT_DPI_COMPENSATION estiver definido ou o DPI de entrada e os valores de DPI de destino não 0corresponderem.

Essa lógica deve ser aplicada sempre que uma fonte estiver sendo realizada e vinculada a uma entrada do efeito atual. Observe que, se um efeito de compensação de DPI for adicionado, essa deverá ser a entrada definida para a imagem D2D subjacente. Mas, se o usuário tentar recuperar o wrapper do WinRT para essa origem, o efeito deverá ter o cuidado de detectar se um efeito DPI foi usado e, em vez disso, retornar um wrapper para o objeto de origem original. Ou seja, os efeitos de compensação de DPI devem ser transparentes para os usuários do efeito.

Depois que toda a lógica de inicialização for feita, o resultado ID2D1Image (assim como com objetos Win2D, um efeito D2D também é uma imagem) deve estar pronto para ser desenhado pelo Win2D no contexto de destino, que ainda não é conhecido pelo receptor no momento.

Observação

A implementação correta desse método (e ICanvasImageInterop em geral) é extremamente complicada e só deve ser feita por usuários avançados que precisam absolutamente de flexibilidade extra. Recomenda-se uma compreensão sólida de D2D, Win2D, COM, WinRT e C++ antes de tentar escrever uma ICanvasImageInterop implementação. Se o efeito Win2D personalizado também precisar encapsular um efeito D2D personalizado, você também precisará implementar seu próprio ID2D1Effect objeto (consulte os documentos D2D sobre efeitos personalizados para obter mais informações sobre isso). Esses documentos não são uma descrição exaustiva de toda a lógica necessária (por exemplo, eles não abordam como as fontes de efeito devem ser empacotadas e gerenciadas através do limite D2D/Win2D), portanto, é recomendável usar também a CanvasEffect implementação na base de código do Win2D como um ponto de referência para um efeito personalizado e modificá-lo conforme necessário.

Implementação GetBounds

O último componente ausente para implementar totalmente um efeito personalizado ICanvasImage é dar suporte às duas GetBounds sobrecargas. Para facilitar isso, o Win2D expõe uma exportação C que pode ser usada para aproveitar a lógica existente para isso do Win2D em qualquer imagem personalizada. A exportação é a seguinte:

HRESULT GetBoundsForICanvasImageInterop(
    ICanvasResourceCreator* resourceCreator,
    ICanvasImageInterop* image,
    Numerics::Matrix3x2 const* transform,
    Rect* rect);

As imagens personalizadas podem invocar essa API e passar a si mesmas como o image parâmetro e, em seguida, simplesmente retornar o resultado para seus chamadores. O transform parâmetro pode ser null, se nenhuma transformação estiver disponível.

Otimizando acessos ao contexto do dispositivo

Às vezes, o deviceContext parâmetro em ICanvasImageInterop::GetD2DImage pode ser null, se um contexto não estiver imediatamente disponível antes da invocação. Isso é feito de propósito, para que um contexto só seja criado preguiçosamente quando for realmente necessário. Ou seja, se um contexto estiver disponível, o Win2D o passará para a GetD2DImage invocação, caso contrário, permitirá que os receptores recuperem um por conta própria, se necessário.

Criar um contexto de dispositivo é relativamente caro, portanto, para tornar a recuperação mais rápida, o Win2D expõe APIs para acessar seu pool de contexto de dispositivo interno. Isso permite que os efeitos personalizados aluguem e retornem contextos de dispositivo associados a um determinado dispositivo de tela de maneira eficiente.

As APIs de concessão de contexto do dispositivo são definidas da seguinte maneira:

[uuid("A0928F38-F7D5-44DD-A5C9-E23D94734BBB")]
interface ID2D1DeviceContextLease : IUnknown
{
    HRESULT GetD2DDeviceContext(ID2D1DeviceContext** deviceContext);
}

[uuid("454A82A1-F024-40DB-BD5B-8F527FD58AD0")]
interface ID2D1DeviceContextPool : IUnknown
{
    HRESULT GetDeviceContextLease(ID2D1DeviceContextLease** lease);
}

A ID2D1DeviceContextPool interface é implementada por CanvasDevice, que é o tipo Win2D que implementa a ICanvasDevice interface. Para usar o pool, use QueryInterface na interface do dispositivo para obter uma ID2D1DeviceContextPool referência e, em seguida, chame ID2D1DeviceContextPool::GetDeviceContextLease para obter um ID2D1DeviceContextLease objeto para acessar o contexto do dispositivo. Quando isso não for mais necessário, libere o contrato. Certifique-se de não tocar no contexto do dispositivo após a liberação da concessão, pois ele pode ser usado simultaneamente por outros threads.

Habilitando a pesquisa de wrappers do WinRT

Conforme visto nos documentos de interoperabilidade do Win2D, o cabeçalho público do Win2D também expõe um GetOrCreate método (acessível na ICanvasFactoryNative fábrica de ativação ou por meio dos GetOrCreate auxiliares C++/CX definidos no mesmo cabeçalho). Isso permite recuperar um wrapper do WinRT de um determinado recurso nativo. Por exemplo, ele permite que você recupere ou crie uma CanvasDevice instância a partir de um ID2D1Device1 objeto, a CanvasBitmap partir de um ID2D1Bitmap, etc.

Esse método também funciona para todos os efeitos internos do Win2D: recuperar o recurso nativo para um determinado efeito e, em seguida, usá-lo para recuperar o wrapper Win2D correspondente retornará corretamente o efeito Win2D proprietário para ele. Para que os efeitos personalizados também se beneficiem do mesmo sistema de mapeamento, o Win2D expõe várias APIs na interface de interoperabilidade para a fábrica de ativação para CanvasDevice, que é o ICanvasFactoryNative tipo, bem como uma interface de fábrica de efeitos adicional, ICanvasEffectFactoryNative:

[uuid("29BA1A1F-1CFE-44C3-984D-426D61B51427")]
class ICanvasEffectFactoryNative : IUnknown
{
    HRESULT CreateWrapper(
        ICanvasDevice* device,
        ID2D1Effect* resource,
        float dpi,
        IInspectable** wrapper);
};

[uuid("695C440D-04B3-4EDD-BFD9-63E51E9F7202")]
class ICanvasFactoryNative : IInspectable
{
    HRESULT GetOrCreate(
        ICanvasDevice* device,
        IUnknown* resource,
        float dpi,
        IInspectable** wrapper);

    HRESULT RegisterWrapper(IUnknown* resource, IInspectable* wrapper);

    HRESULT UnregisterWrapper(IUnknown* resource);

    HRESULT RegisterEffectFactory(
        REFIID effectId,
        ICanvasEffectFactoryNative* factory);

    HRESULT UnregisterEffectFactory(REFIID effectId);
};

Há várias APIs a serem consideradas aqui, pois elas são necessárias para dar suporte a todos os vários cenários em que os efeitos Win2D podem ser usados, bem como como os desenvolvedores podem fazer interoperabilidade com a camada D2D e, em seguida, tentar resolver wrappers para eles. Vamos examinar cada uma dessas APIs.

Os RegisterWrapper métodos and UnregisterWrapper devem ser invocados por efeitos personalizados para se adicionarem ao cache interno do Win2D:

  • RegisterWrapper: registra um recurso nativo e seu wrapper WinRT proprietário. O wrapper parâmetro também é necessário para implementar IWeakReferenceSource, para que possa ser armazenado em cache corretamente sem causar ciclos de referência que levariam a vazamentos de memória. O método retorna S_OK se o recurso nativo puder ser adicionado ao cache, S_FALSE se já houver um wrapper registrado para resource, e um código de erro se ocorrer um erro.
  • UnregisterWrapper: cancela o registro de um recurso nativo e seu wrapper. Retorna S_OK se o recurso puder ser removido, S_FALSE se resource ainda não estiver registrado e um código de erro se ocorrer outro erro.

Os efeitos personalizados devem ser chamados RegisterWrapper e UnregisterWrapper sempre que forem realizados e não realizados, ou seja, quando um novo recurso nativo for criado e associado a eles. Os efeitos personalizados que não suportam a realização (por exemplo, aqueles que têm um dispositivo fixo associado) podem chamar RegisterWrapper e UnregisterWrapper quando são criados e destruídos. Os efeitos personalizados devem certificar-se de cancelar corretamente o registro de todos os caminhos de código possíveis que fariam com que o wrapper se tornasse inválido (por exemplo, incluindo quando o objeto é finalizado, caso seja implementado em uma linguagem gerenciada).

Os RegisterEffectFactory métodos and UnregisterEffectFactory também devem ser usados por efeitos personalizados, para que eles também possam registrar um retorno de chamada para criar um novo wrapper caso um desenvolvedor tente resolver um para um recurso D2D "órfão":

  • RegisterEffectFactory: registre um retorno de chamada que recebe os mesmos parâmetros que um desenvolvedor passou para GetOrCreate, e cria um novo wrapper inspecionável para o efeito de entrada. A id do efeito é usada como chave, para que cada efeito personalizado possa registrar uma fábrica para ele quando for carregado pela primeira vez. Claro, isso só deve ser feito uma vez por tipo de efeito, e não toda vez que o efeito é realizado. Os deviceparâmetros e wrapper resource são verificados pelo Win2D antes de invocar qualquer retorno de chamada registrado, portanto, é garantido que eles não sejam null quando CreateWrapper é invocado. O dpi é considerado opcional e pode ser ignorado caso o tipo de efeito não tenha um uso específico para ele. Observe que, quando um novo wrapper é criado a partir de uma fábrica registrada, essa fábrica também deve garantir que o novo wrapper esteja registrado no cache (o Win2D não adicionará automaticamente wrappers produzidos por fábricas externas ao cache).
  • UnregisterEffectFactory: remove um retorno de chamada registrado anteriormente. Por exemplo, isso pode ser usado se um wrapper de efeito for implementado em um assembly gerenciado que está sendo descarregado.

Observação

ICanvasFactoryNative é implementado pela fábrica de ativação para CanvasDevice, que você pode recuperar chamando RoGetActivationFactorymanualmente ou usando APIs auxiliares das extensões de linguagem que você está usando (por exemplo winrt::get_activation_factory , em C++/WinRT). Para obter mais informações, consulte Sistema de tipos do WinRT para obter mais informações sobre como isso funciona.

Para obter um exemplo prático de onde esse mapeamento entra em ação, considere como os efeitos internos do Win2D funcionam. Se eles não forem realizados, todos os estados (por exemplo, propriedades, fontes, etc.) são armazenados em um cache interno em cada instância de efeito. Quando eles são realizados, todo o estado é transferido para o recurso nativo (por exemplo, as propriedades são definidas no efeito D2D, todas as fontes são resolvidas e mapeadas para entradas de efeito, etc.) e, desde que o efeito seja realizado, ele atuará como a autoridade sobre o estado do wrapper. Ou seja, se o valor de qualquer propriedade for buscado no wrapper, ele recuperará o valor atualizado para ela do recurso D2D nativo associado a ela.

Isso garante que, se alguma alteração for feita diretamente no recurso D2D, ela também ficará visível no wrapper externo e os dois nunca estarão "fora de sincronia". Quando o efeito não é realizado, todo o estado é transferido de volta do recurso nativo para o estado wrapper, antes que o recurso seja liberado. Ele será mantido e atualizado lá até a próxima vez que o efeito for realizado. Agora, considere esta sequência de eventos:

  • Você tem algum efeito Win2D (embutido ou personalizado).
  • Você obtém o ID2D1Image dele (que é um ID2D1Effect).
  • Você cria uma instância de um efeito personalizado.
  • Você também obtém o ID2D1Image disso.
  • Você define manualmente esta imagem como entrada para o efeito anterior (via ID2D1Effect::SetInput).
  • Em seguida, você solicita esse primeiro efeito para o wrapper do WinRT para essa entrada.

Como o efeito é realizado (foi realizado quando o recurso nativo foi solicitado), ele usará o recurso nativo como fonte da verdade. Dessa forma, ele obterá o ID2D1Image correspondente à fonte solicitada e tentará recuperar o wrapper do WinRT para ele. Se o efeito do qual essa entrada foi recuperada tiver adicionado corretamente seu próprio par de recursos nativos e wrapper do WinRT ao cache do Win2D, o wrapper será resolvido e retornado aos chamadores. Caso contrário, esse acesso à propriedade falhará, pois o Win2D não pode resolver wrappers do WinRT para efeitos que não possui, pois não sabe como instanciá-los.

É aqui RegisterWrapper que e UnregisterWrapper ajudam, pois permitem que os efeitos personalizados participem perfeitamente da lógica de resolução do wrapper do Win2D, para que o wrapper correto sempre possa ser recuperado para qualquer fonte de efeito, independentemente de ter sido definido nas APIs do WinRT ou diretamente da camada D2D subjacente.

Para explicar como as fábricas de efeitos também entram em jogo, considere este cenário:

  • Um usuário cria uma instância de um wrapper personalizado e a percebe
  • Em seguida, eles obtêm uma referência ao efeito D2D subjacente e o mantêm.
  • Em seguida, o efeito é realizado em um dispositivo diferente. O efeito será desrealizado e rerealizado e, ao fazê-lo, criará um novo efeito D2D. O efeito D2D anterior não é mais um wrapper inspecionável associado neste momento.
  • Em seguida, o usuário chama GetOrCreate o primeiro efeito D2D.

Sem um retorno de chamada, o Win2D simplesmente não resolveria um wrapper, pois não há nenhum wrapper registrado para ele. Se uma fábrica for registrada, um novo wrapper para esse efeito D2D poderá ser criado e retornado, para que o cenário continue funcionando perfeitamente para o usuário.

Implementando um ICanvasEffect

A interface Win2D ICanvasEffect estende ICanvasImage, portanto, todos os pontos anteriores também se aplicam a efeitos personalizados. A única diferença é o fato de que ICanvasEffect também implementa métodos adicionais específicos para efeitos, como invalidar um retângulo de origem, obter os retângulos necessários e assim por diante.

Para dar suporte a isso, o Win2D expõe exportações C que os autores de efeitos personalizados podem usar, para que eles não precisem reimplementar toda essa lógica extra do zero. Isso funciona da mesma forma que a exportação C para GetBounds. Aqui estão as exportações disponíveis para efeitos:

HRESULT InvalidateSourceRectangleForICanvasImageInterop(
    ICanvasResourceCreatorWithDpi* resourceCreator,
    ICanvasImageInterop* image,
    uint32_t sourceIndex,
    Rect const* invalidRectangle);

HRESULT GetInvalidRectanglesForICanvasImageInterop(
    ICanvasResourceCreatorWithDpi* resourceCreator,
    ICanvasImageInterop* image,
    uint32_t* valueCount,
    Rect** valueElements);

HRESULT GetRequiredSourceRectanglesForICanvasImageInterop(
    ICanvasResourceCreatorWithDpi* resourceCreator,
    ICanvasImageInterop* image,
    Rect const* outputRectangle,
    uint32_t sourceEffectCount,
    ICanvasEffect* const* sourceEffects,
    uint32_t sourceIndexCount,
    uint32_t const* sourceIndices,
    uint32_t sourceBoundsCount,
    Rect const* sourceBounds,
    uint32_t valueCount,
    Rect* valueElements);

Vamos ver como eles podem ser usados:

  • InvalidateSourceRectangleForICanvasImageInterop destina-se a apoiar InvalidateSourceRectangle. Basta empacotar os parâmetros de entrada e invocá-los diretamente, e ele cuidará de todo o trabalho necessário. Observe que o image parâmetro é a instância de efeito atual que está sendo implementada.
  • GetInvalidRectanglesForICanvasImageInterop suporta GetInvalidRectangles. Isso também não requer nenhuma consideração especial, além da necessidade de descartar a matriz COM retornada quando ela não for mais necessária.
  • GetRequiredSourceRectanglesForICanvasImageInterop é um método compartilhado que pode dar suporte a arquivos GetRequiredSourceRectangle e GetRequiredSourceRectangles. Ou seja, é necessário um ponteiro para uma matriz existente de valores para preencher, para que os chamadores possam passar um ponteiro para um único valor (que também pode estar na pilha, para evitar uma alocação) ou para uma matriz de valores. A implementação é a mesma em ambos os casos, portanto, uma única exportação C é suficiente para alimentar os dois.

Efeitos personalizados em C# usando ComputeSharp

Como mencionamos, se você estiver usando C# e quiser implementar um efeito personalizado, a abordagem recomendada é usar a biblioteca ComputeSharp . Ele permite que você implemente sombreadores de pixel D2D1 personalizados inteiramente em C#, bem como defina facilmente grafos de efeitos personalizados compatíveis com Win2D. A mesma biblioteca também é usada na Microsoft Store para alimentar vários componentes gráficos no aplicativo.

Você pode adicionar uma referência a ComputeSharp em seu projeto por meio do NuGet:

Observação

Muitas APIs em ComputeSharp.D2D1.* são idênticas nos destinos UWP e WinAppSDK, a única diferença é o namespace (terminando em ou .Uwp .WinUI). No entanto, o destino UWP está em manutenção sustentada e não está recebendo novos recursos. Dessa forma, algumas alterações de código podem ser necessárias em comparação com os exemplos mostrados aqui para WinUI. Os snippets neste documento refletem a superfície da API a partir de ComputeSharp.D2D1.WinUI 3.0.0 (a última versão do destino UWP é 2.1.0).

Há dois componentes principais no ComputeSharp para interoperabilidade com o Win2D:

  • PixelShaderEffect<T>: um efeito Win2D que é alimentado por um sombreador de pixel D2D1. O sombreador em si é escrito em C# usando as APIs fornecidas pelo ComputeSharp. Essa classe também fornece propriedades para definir fontes de efeito, valores constantes e muito mais.
  • CanvasEffect: uma classe base para efeitos Win2D personalizados que encapsula um grafo de efeito arbitrário. Ele pode ser usado para "empacotar" efeitos complexos em um objeto fácil de usar que pode ser reutilizado em várias partes de um aplicativo.

Aqui está um exemplo de um sombreador de pixel personalizado (portado deste sombreador de brinquedo de sombreamento), usado com PixelShaderEffect<T> e depois desenhado em um Win2D CanvasControl (observe que PixelShaderEffect<T> implementa ICanvasImage):

um sombreador de pixel de exemplo exibindo hexágonos coloridos infinitos, sendo desenhado em um controle Win2D e exibido em execução em uma janela de aplicativo

Você pode ver como em apenas duas linhas de código você pode criar um efeito e desenhá-lo via Win2D. O ComputeSharp cuida de todo o trabalho necessário para compilar o sombreador, registrá-lo e gerenciar o tempo de vida complexo de um efeito compatível com Win2D.

A seguir, vamos ver um guia passo a passo sobre como criar um efeito Win2D personalizado que também usa um sombreador de pixel D2D1 personalizado. Veremos como criar um sombreador com ComputeSharp e configurar suas propriedades e, em seguida, como criar um grafo de efeito personalizado empacotado em um CanvasEffect tipo que pode ser facilmente reutilizado em seu aplicativo.

Projetando o efeito

Para esta demonstração, queremos criar um efeito de vidro fosco simples.

Isso incluirá os seguintes componentes:

  • Desfoque gaussiano
  • Efeito de tonalidade
  • Ruído (que podemos gerar processualmente com um sombreador)

Também queremos expor propriedades para controlar a quantidade de desfoque e ruído. O efeito final conterá uma versão "empacotada" desse gráfico de efeito e será fácil de usar apenas criando uma instância, definindo essas propriedades, conectando uma imagem de origem e desenhando-a. Vamos começar!

Criando um sombreador de pixel D2D1 personalizado

Para o ruído na parte superior do efeito, podemos usar um sombreador de pixel D2D1 simples. O sombreador calculará um valor aleatório com base em suas coordenadas (que atuará como uma "semente" para o número aleatório) e, em seguida, usará esse valor de ruído para calcular a quantidade RGB para esse pixel. Podemos então misturar esse ruído em cima da imagem resultante.

Para escrever o sombreador com ComputeSharp, precisamos apenas definir um partial struct tipo que implementa a ID2D1PixelShader interface e, em seguida, escrever nossa lógica no Execute método. Para este sombreador de ruído, podemos escrever algo assim:

using ComputeSharp;
using ComputeSharp.D2D1;

[D2DInputCount(0)]
[D2DRequiresScenePosition]
[D2DShaderProfile(D2D1ShaderProfile.PixelShader40)]
[D2DGeneratedPixelShaderDescriptor]
public readonly partial struct NoiseShader(float amount) : ID2D1PixelShader
{
    /// <inheritdoc/>
    public float4 Execute()
    {
        // Get the current pixel coordinate (in pixels)
        int2 position = (int2)D2D.GetScenePosition().XY;

        // Compute a random value in the [0, 1] range for each target pixel. This line just
        // calculates a hash from the current position and maps it into the [0, 1] range.
        // This effectively provides a "random looking" value for each pixel.
        float hash = Hlsl.Frac(Hlsl.Sin(Hlsl.Dot(position, new float2(41, 289))) * 45758.5453f);

        // Map the random value in the [0, amount] range, to control the strength of the noise
        float alpha = Hlsl.Lerp(0, amount, hash);

        // Return a white pixel with the random value modulating the opacity
        return new(1, 1, 1, alpha);
    }
}

Observação

Embora o sombreador seja escrito inteiramente em C#, recomenda-se o conhecimento básico de HLSL (a linguagem de programação para sombreadores DirectX, para a qual a ComputeSharp transpila C#).

Vamos examinar esse sombreador em detalhes:

  • O shader não tem entradas, apenas produz uma imagem infinita com ruído aleatório em tons de cinza.
  • O sombreador requer acesso à coordenada de pixel atual.
  • O sombreador é pré-compilado no momento da compilação (usando o PixelShader40 perfil, que é garantido para estar disponível em qualquer GPU em que o aplicativo possa estar em execução).
  • O [D2DGeneratedPixelShaderDescriptor] atributo é necessário para disparar o gerador de origem empacotado com o ComputeSharp, que analisará o código C#, o transcompilará para HLSL, compilará o sombreador para bytecode etc.
  • O sombreador captura um float amount parâmetro, por meio de seu construtor primário. O gerador de origem no ComputeSharp cuidará automaticamente da extração de todos os valores capturados em um sombreador e preparará o buffer constante que o D2D precisa para inicializar o estado do sombreador.

E esta parte está feita! Este shader irá gerar nossa textura de ruído personalizada sempre que necessário. Em seguida, precisamos criar nosso efeito empacotado com o gráfico de efeitos conectando todos os nossos efeitos.

Criando um efeito personalizado

Para nosso efeito empacotado fácil de usar, podemos usar o CanvasEffect tipo do ComputeSharp. Esse tipo fornece uma maneira direta de configurar toda a lógica necessária para criar um grafo de efeito e atualizá-lo por meio de propriedades públicas com as quais os usuários do efeito podem interagir. Existem dois métodos principais que precisaremos implementar:

  • BuildEffectGraph: este método é responsável por construir o grafo de efeito que queremos desenhar. Ou seja, ele precisa criar todos os efeitos de que precisamos e registrar o nó de saída para o gráfico. Para efeitos que podem ser atualizados posteriormente, o registro é feito com um valor associado CanvasEffectNode<T> , que atua como chave de pesquisa para recuperar os efeitos do gráfico quando necessário.
  • ConfigureEffectGraph: esse método atualiza o grafo de efeito aplicando as configurações que o usuário definiu. Esse método é invocado automaticamente quando necessário, logo antes de desenhar o efeito e somente se pelo menos uma propriedade de efeito tiver sido modificada desde a última vez que o efeito foi usado.

Nosso efeito personalizado pode ser definido da seguinte forma:

using ComputeSharp.D2D1.WinUI;
using Microsoft.Graphics.Canvas;
using Microsoft.Graphics.Canvas.Effects;

public sealed class FrostedGlassEffect : CanvasEffect
{
    private static readonly CanvasEffectNode<GaussianBlurEffect> BlurNode = new();
    private static readonly CanvasEffectNode<PixelShaderEffect<NoiseShader>> NoiseNode = new();

    private ICanvasImage? _source;
    private double _blurAmount;
    private double _noiseAmount;

    public ICanvasImage? Source
    {
        get => _source;
        set => SetAndInvalidateEffectGraph(ref _source, value);
    }

    public double BlurAmount
    {
        get => _blurAmount;
        set => SetAndInvalidateEffectGraph(ref _blurAmount, value);
    }

    public double NoiseAmount
    {
        get => _noiseAmount;
        set => SetAndInvalidateEffectGraph(ref _noiseAmount, value);
    }

    /// <inheritdoc/>
    protected override void BuildEffectGraph(CanvasEffectGraph effectGraph)
    {
        // Create the effect graph as follows:
        //
        // ┌────────┐   ┌──────┐
        // │ source ├──►│ blur ├─────┐
        // └────────┘   └──────┘     ▼
        //                       ┌───────┐   ┌────────┐
        //                       │ blend ├──►│ output │
        //                       └───────┘   └────────┘
        //    ┌───────┐              ▲   
        //    │ noise ├──────────────┘
        //    └───────┘
        //
        GaussianBlurEffect gaussianBlurEffect = new();
        BlendEffect blendEffect = new() { Mode = BlendEffectMode.Overlay };
        PixelShaderEffect<NoiseShader> noiseEffect = new();
        PremultiplyEffect premultiplyEffect = new();

        // Connect the effect graph
        premultiplyEffect.Source = noiseEffect;
        blendEffect.Background = gaussianBlurEffect;
        blendEffect.Foreground = premultiplyEffect;

        // Register all effects. For those that need to be referenced later (ie. the ones with
        // properties that can change), we use a node as a key, so we can perform lookup on
        // them later. For others, we register them anonymously. This allows the effect
        // to autommatically and correctly handle disposal for all effects in the graph.
        effectGraph.RegisterNode(BlurNode, gaussianBlurEffect);
        effectGraph.RegisterNode(NoiseNode, noiseEffect);
        effectGraph.RegisterNode(premultiplyEffect);
        effectGraph.RegisterOutputNode(blendEffect);
    }

    /// <inheritdoc/>
    protected override void ConfigureEffectGraph(CanvasEffectGraph effectGraph)
    {
        // Set the effect source
        effectGraph.GetNode(BlurNode).Source = Source;

        // Configure the blur amount
        effectGraph.GetNode(BlurNode).BlurAmount = (float)BlurAmount;

        // Set the constant buffer of the shader
        effectGraph.GetNode(NoiseNode).ConstantBuffer = new NoiseShader((float)NoiseAmount);
    }
}

Você pode ver que há quatro seções nesta classe:

  • Primeiro, temos campos para rastrear todos os estados mutáveis, como os efeitos que podem ser atualizados, bem como os campos de suporte para todas as propriedades do efeito que queremos expor aos usuários do efeito.
  • Em seguida, temos propriedades para configurar o efeito. O setter de cada propriedade usa o SetAndInvalidateEffectGraph método exposto por CanvasEffect, que invalidará automaticamente o efeito se o valor que está sendo definido for diferente do atual. Isso garante que o efeito seja configurado novamente apenas quando realmente necessário.
  • Por fim, temos os BuildEffectGraph métodos e ConfigureEffectGraph que mencionamos acima.

Observação

O PremultiplyEffect nó após o efeito de ruído é muito importante: isso ocorre porque os efeitos Win2D pressupõem que a saída é pré-multiplicada, enquanto os sombreadores de pixel geralmente funcionam com pixels não pré-multiplicados. Dessa forma, lembre-se de inserir manualmente os nós de pré-multiplicação/despré-multiplicação antes e depois dos sombreadores personalizados, para garantir que as cores sejam preservadas corretamente.

Observação

Este efeito de exemplo está usando namespaces do WinUI 3, mas o mesmo código também pode ser usado na UWP. Nesse caso, o namespace para ComputeSharp será ComputeSharp.Uwp, correspondendo ao nome do pacote.

Pronto para desenhar!

E com isso, nosso efeito de vidro fosco personalizado está pronto! Podemos facilmente desenhá-lo da seguinte forma:

private void CanvasControl_Draw(CanvasControl sender, CanvasDrawEventArgs args)
{
    FrostedGlassEffect effect = new()
    {
        Source = _canvasBitmap,
        BlurAmount = 12,
        NoiseAmount = 0.1
    };

    args.DrawingSession.DrawImage(effect);
}

Neste exemplo, estamos extraindo o efeito do Draw manipulador de um CanvasControl, usando um CanvasBitmap que carregamos anteriormente como origem. Esta é a imagem de entrada que usaremos para testar o efeito:

uma imagem de algumas montanhas sob um céu nublado

E aqui está o resultado:

uma versão borrada da imagem acima

Observação

Créditos para Dominic Lange pela foto.

Recursos adicionais

  • Confira o código-fonte do Win2D para obter mais informações.
  • Para obter mais informações sobre o ComputeSharp, confira os aplicativos de exemplo e os testes de unidade.