Bagikan melalui


Menerapkan efek kustom

Win2D menyediakan beberapa API untuk mewakili objek yang dapat digambar, yang dibagi menjadi dua kategori: gambar dan efek. Gambar, yang diwakili oleh ICanvasImage antarmuka, tidak memiliki input dan dapat langsung digambar pada permukaan tertentu. Misalnya, CanvasBitmap, VirtualizedCanvasBitmap dan CanvasRenderTarget merupakan contoh jenis gambar. Efek, di sisi lain, diwakili oleh ICanvasEffect antarmuka. Mereka dapat memiliki input serta sumber daya tambahan, dan dapat menerapkan logika arbitrer untuk menghasilkan output mereka (sebagai efeknya juga merupakan gambar). Win2D mencakup efek membungkus sebagian besar efek D2D, seperti GaussianBlurEffect, TintEffect dan LuminanceToAlphaEffect.

Gambar dan efek juga dapat dirantai bersama-sama, untuk membuat grafik arbitrer yang kemudian dapat ditampilkan dalam aplikasi Anda (juga lihat dokumen D2D pada efek Direct2D). Bersama-sama, mereka menyediakan sistem yang sangat fleksibel untuk menulis grafik kompleks dengan cara yang efisien. Namun, ada kasus di mana efek bawaan tidak cukup, dan Anda mungkin ingin membangun efek Win2D Anda sendiri. Untuk mendukung hal ini, Win2D menyertakan serangkaian API interop kuat yang memungkinkan menentukan gambar dan efek kustom yang dapat diintegrasikan dengan mulus dengan Win2D.

Petunjuk / Saran

Jika Anda menggunakan C# dan ingin menerapkan efek kustom atau grafik efek, disarankan untuk menggunakan ComputeSharp daripada mencoba menerapkan efek dari awal. Lihat paragraf di bawah ini untuk penjelasan terperinci tentang cara menggunakan pustaka ini untuk mengimplementasikan efek kustom yang terintegrasi dengan mulus dengan Win2D.

API Platform:ICanvasImage, CanvasBitmap, VirtualizedCanvasBitmap, CanvasRenderTarget, CanvasEffect, GaussianBlurEffect, TintEffect, ICanvasLuminanceToAlphaEffectImage, IGraphicsEffectSource, ID2D21Image, ID2D1Factory1, ID2D1Effect

Menerapkan ICanvasImage khusus

Skenario paling sederhana untuk didukung adalah membuat kustom ICanvasImage. Seperti yang kami sebutkan, ini adalah antarmuka WinRT yang didefinisikan oleh Win2D yang mewakili semua jenis gambar yang dapat diinteropsi Win2D. Antarmuka ini hanya mengekspos dua GetBounds metode, dan memperluas IGraphicsEffectSource, yang merupakan antarmuka penanda yang mewakili "beberapa sumber efek".

Seperti yang Anda lihat, tidak ada API "fungsional" yang diekspos oleh antarmuka ini untuk benar-benar melakukan gambar apa pun. Untuk mengimplementasikan objek ICanvasImage Anda sendiri, Anda juga harus mengimplementasikan antarmuka ICanvasImageInterop, yang memaparkan semua logika yang diperlukan untuk Win2D dalam menggambar. Ini adalah antarmuka COM yang ditentukan dalam header publik Microsoft.Graphics.Canvas.native.h , yang dikirim dengan Win2D.

Antarmuka didefinisikan sebagai berikut:

[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);
}

Dan ini juga bergantung pada dua tipe enumerasi tersebut, berasal dari header yang sama:

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
}

Dua metode, GetDevice dan GetD2DImage, merupakan semua yang dibutuhkan untuk mengimplementasikan gambar kustom (atau efek), karena kedua metode tersebut memberi Win2D titik ekstensibilitas untuk menginisialisasinya pada perangkat tertentu dan mengambil gambar D2D yang mendasar untuk menggambar. Menerapkan metode ini dengan benar sangat penting untuk memastikan hal-hal akan berfungsi dengan baik dalam semua skenario yang didukung.

Mari kita membahasnya untuk melihat cara kerja setiap metode.

Menerapkan GetDevice

Metode GetDevice ini adalah yang paling sederhana dari keduanya. Apa yang dilakukannya adalah mengambil perangkat kanvas yang terkait dengan efeknya, sehingga Win2D dapat memeriksanya jika perlu (misalnya, untuk memastikannya cocok dengan perangkat yang digunakan). Parameter type menunjukkan "jenis asosiasi" untuk perangkat yang dikembalikan.

Ada dua kemungkinan kasus utama:

  • Jika gambar adalah efek, gambar harus mendukung "direalisasikan" dan "tidak direalisasi" pada beberapa perangkat. Artinya: efek tertentu dibuat dalam keadaan belum terinisialisasi, kemudian dapat diaktifkan ketika perangkat digunakan saat menggambar, dan setelah itu dapat terus digunakan dengan perangkat tersebut, atau dapat dipindahkan ke perangkat yang berbeda. Dalam hal ini, efek akan mengatur ulang status internalnya dan kemudian mewujudkan dirinya lagi pada perangkat baru. ** Ini berarti bahwa perangkat kanvas terkait dapat berubah dari waktu ke waktu, dan juga bisa menjadi null. Karena itu, type harus diatur ke WIN2D_GET_DEVICE_ASSOCIATION_TYPE_REALIZATION_DEVICE, dan perangkat yang dikembalikan harus diatur ke perangkat realisasi saat ini, jika tersedia.
  • Beberapa gambar memiliki satu "perangkat pemilik" yang ditetapkan pada waktu pembuatan dan tidak pernah dapat berubah. Misalnya, ini akan menjadi kasus untuk gambar yang mewakili tekstur, karena dialokasikan pada perangkat tertentu dan tidak dapat dipindahkan. Ketika GetDevice dipanggil, itu harus mengembalikan perangkat pembuatan dan mengatur type ke WIN2D_GET_DEVICE_ASSOCIATION_TYPE_CREATION_DEVICE. Perhatikan bahwa ketika jenis ini ditentukan, perangkat yang dikembalikan tidak boleh null.

Catatan

Win2D dapat memanggil GetDevice saat secara rekursif melintas grafik efek, yang berarti mungkin ada beberapa panggilan aktif ke GetD2DImage dalam tumpukan. Karena itu, GetDevice tidak boleh mengambil kunci pemblokiran pada gambar saat ini, karena hal tersebut berpotensi menyebabkan kebuntuan. Sebaliknya, ia harus menggunakan kunci entrant ulang dengan cara yang tidak memblokir, dan mengembalikan kesalahan jika tidak dapat diperoleh. Ini memastikan bahwa utas yang sama secara rekursif memanggilnya akan berhasil mendapatkannya, sedangkan utas bersamaan yang melakukan hal yang sama akan gagal dengan elegan.

Menerapkan GetD2DImage

GetD2DImage adalah tempat sebagian besar pekerjaan berlangsung. Metode ini bertanggung jawab untuk mengambil objek ID2D1Image yang dapat digambar oleh Win2D, dengan opsi untuk merealisasikan efek saat ini jika diperlukan. Ini juga termasuk melintas secara rekursif dan mewujudkan grafik efek untuk semua sumber, jika ada, serta menginisialisasi status apa pun yang mungkin dibutuhkan gambar (misalnya buffer konstan dan properti lainnya, tekstur sumber daya, dll.).

Implementasi yang tepat dari metode ini sangat tergantung pada jenis gambar dan dapat sangat bervariasi, tetapi umumnya berbicara untuk efek semena-mena Anda dapat mengharapkan metode untuk melakukan langkah-langkah berikut:

  • Periksa apakah panggilan rekursif pada instans yang sama, dan gagal jika demikian. Ini diperlukan untuk mendeteksi siklus dalam grafik efek (misalnya efek A berpengaruh B sebagai sumber, dan efek B berpengaruh A sebagai sumber).
  • Dapatkan kunci pada instans gambar untuk melindungi dari akses bersamaan.
  • Menangani DPI target sesuai dengan bendera input
  • Validasi apakah perangkat input cocok dengan perangkat yang digunakan, jika ada. Jika tidak cocok dan efek saat ini mendukung realisasi, batalkan realisasi efek.
  • Sadari efek pada perangkat masukan. Ini dapat mencakup mendaftarkan efek D2D pada objek yang diperoleh dari perangkat input atau konteks perangkat, jika diperlukan. Selain itu, semua status yang diperlukan harus diatur pada instans efek D2D yang sedang dibuat.
  • Menelusuri setiap sumber secara rekursif dan mengikatnya ke efek D2D.

Sehubungan dengan parameter input, ada beberapa kemungkinan kasus yang harus ditangani dengan tepat oleh efek kustom untuk memastikan kompatibilitas dengan semua efek Win2D lainnya. Tidak termasuk WIN2D_GET_D2D_IMAGE_FLAGS_NONE, bendera yang akan ditangani adalah sebagai berikut:

  • WIN2D_GET_D2D_IMAGE_FLAGS_READ_DPI_FROM_DEVICE_CONTEXT: dalam hal ini, device dijamin tidak menjadi null. Efeknya harus memeriksa apakah target konteks perangkat adalah ID2D1CommandList, dan jika demikian, tambahkan flag WIN2D_GET_D2D_IMAGE_FLAGS_ALWAYS_INSERT_DPI_COMPENSATION. Jika tidak, ia harus menyetel targetDpi (yang juga dijamin bukan null) ke DPI yang diambil dari konteks input. Kemudian, harus menghapus WIN2D_GET_D2D_IMAGE_FLAGS_READ_DPI_FROM_DEVICE_CONTEXT dari penanda.
  • WIN2D_GET_D2D_IMAGE_FLAGS_ALWAYS_INSERT_DPI_COMPENSATION dan WIN2D_GET_D2D_IMAGE_FLAGS_NEVER_INSERT_DPI_COMPENSATION: digunakan saat mengatur sumber efek (lihat catatan di bawah).
  • WIN2D_GET_D2D_IMAGE_FLAGS_MINIMAL_REALIZATION: jika diatur, secara rekursif melewati proses mewujudkan sumber efek, dan hanya mengembalikan efek yang telah diwujudkan tanpa perubahan lain.
  • WIN2D_GET_D2D_IMAGE_FLAGS_ALLOW_NULL_EFFECT_INPUTS: jika diatur, sumber efek yang direalisasikan diizinkan untuk menjadi null, jika pengguna belum mengaturnya ke sumber yang ada.
  • WIN2D_GET_D2D_IMAGE_FLAGS_UNREALIZE_ON_FAILURE: jika diatur, dan sumber efek yang diatur tidak valid, efeknya harus dibatalkan sebelum gagal. Artinya, jika kesalahan terjadi saat menyelesaikan sumber efek setelah merealisasikan efek, maka efeknya harus membatalkan realisasi sebelum mengembalikan kesalahan ke pemanggil.

Sehubungan dengan bendera terkait DPI, ini mengontrol bagaimana sumber efek diatur. Untuk memastikan kompatibilitas dengan Win2D, efek harus secara otomatis menambahkan efek kompensasi DPI ke inputnya saat diperlukan. Mereka dapat mengontrol apakah kondisinya seperti ini:

  • Jika WIN2D_GET_D2D_IMAGE_FLAGS_MINIMAL_REALIZATION telah diatur, efek kompensasi DPI diperlukan saat parameter inputDpi tidak 0.
  • Jika tidak, kompensasi DPI diperlukan jika inputDpi tidak 0, WIN2D_GET_D2D_IMAGE_FLAGS_NEVER_INSERT_DPI_COMPENSATION tidak diatur, dan WIN2D_GET_D2D_IMAGE_FLAGS_ALWAYS_INSERT_DPI_COMPENSATION diatur, atau nilai DPI input dan nilai DPI target tidak cocok.

Logika ini harus diterapkan setiap kali sumber diwujudkan dan dihubungkan ke input dari efek yang sedang berlangsung. Perhatikan bahwa jika efek kompensasi DPI ditambahkan, itu harus berupa input yang diatur ke gambar D2D yang mendasar. Tetapi, jika pengguna mencoba memperoleh pembungkus WinRT dari sumber tersebut, efeknya perlu memastikan apakah efek DPI digunakan dan mengembalikan pembungkus untuk objek sumber asli sebagai gantinya. Artinya, efek kompensasi DPI harus transparan bagi pengguna efeknya.

Setelah semua logika inisialisasi selesai, yang dihasilkan ID2D1Image (sama seperti objek Win2D, efek D2D juga merupakan gambar) harus siap untuk digambar oleh Win2D pada konteks target, yang belum diketahui oleh penerima panggilan saat ini.

Catatan

Menerapkan metode ini dengan benar (dan ICanvasImageInterop secara umum) sangat rumit, dan hanya dimaksudkan untuk dilakukan oleh pengguna tingkat lanjut yang benar-benar membutuhkan fleksibilitas tambahan. Pemahaman yang kuat tentang D2D, Win2D, COM, WinRT, dan C++ direkomendasikan sebelum mencoba menulis ICanvasImageInterop implementasi. Jika efek Win2D kustom Anda juga harus membungkus efek D2D kustom, Anda juga harus mengimplementasikan objek ID2D1Effect Anda sendiri (lihat dokumentasi D2D tentang efek kustom untuk informasi lebih lanjut). Dokumen ini bukan deskripsi lengkap dari semua logika yang diperlukan (misalnya, dokumen tersebut tidak mencakup bagaimana sumber efek harus disalurkan dan dikelola di seluruh batas antara D2D dan Win2D), jadi disarankan untuk juga menggunakan struktur implementasi dalam kode dasar Win2D sebagai titik referensi untuk efek kustom, dan memodifikasinya sesuai kebutuhan.

Menerapkan GetBounds

Komponen terakhir yang hilang untuk mengimplementasikan efek kustom ICanvasImage sepenuhnya adalah mendukung dua GetBounds kelebihan beban. Untuk mempermudah ini, Win2D mengekspos ekspor C yang dapat digunakan untuk memanfaatkan logika yang ada untuk ini dari Win2D pada gambar kustom apa pun. Ekspornya adalah sebagai berikut:

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

Citra kustom dapat memanggil API ini dengan meneruskan dirinya sendiri sebagai parameter image, dan kemudian mengembalikan hasil ke pemanggilnya. Parameter transform bisa menjadi null jika tidak ada transformasi yang tersedia.

Mengoptimalkan akses konteks perangkat

Parameter deviceContext dalam ICanvasImageInterop::GetD2DImage terkadang bisa menjadi null jika konteks tidak segera tersedia sebelum pemanggilan. Ini dilakukan dengan sengaja, sehingga konteks hanya dibuat dengan malas ketika benar-benar diperlukan. Artinya, jika sebuah konteks tersedia, Win2D akan meneruskannya ke GetD2DImage pemanggilan fungsi, jika tidak, pihak yang dipanggil dapat mengambilnya sendiri jika perlu.

Menciptakan konteks perangkat (device context) relatif mahal, jadi untuk mempercepat proses pengambilannya, Win2D menyediakan API untuk mengakses kumpulan konteks perangkat internalnya. Ini memungkinkan efek kustom untuk menyewakan dan mengembalikan konteks perangkat yang terkait dengan perangkat kanvas tertentu secara efisien.

API sewa konteks perangkat didefinisikan sebagai berikut:

[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);
}

Antarmuka ID2D1DeviceContextPool diimplementasikan oleh CanvasDevice, yang merupakan jenis Win2D yang mengimplementasikan ICanvasDevice antarmuka. Untuk menggunakan kumpulan, gunakan QueryInterface pada antarmuka perangkat untuk mendapatkan ID2D1DeviceContextPool referensi, lalu panggil ID2D1DeviceContextPool::GetDeviceContextLease untuk mendapatkan ID2D1DeviceContextLease objek untuk mengakses konteks perangkat. Setelah itu tidak lagi diperlukan, lepaskan sewa. Pastikan untuk tidak menyentuh konteks perangkat setelah sewa dirilis, karena dapat digunakan secara bersamaan oleh utas lain.

Mengaktifkan pencarian lapisan WinRT

Seperti yang terlihat di dokumentasi interop Win2D, header publik Win2D juga mengekspos GetOrCreate metode (dapat diakses dari ICanvasFactoryNative pabrik aktivasi, atau melalui C++/CX GetOrCreate helper yang terdefinisi pada header yang sama). Ini memungkinkan pengambilan pembungkus WinRT dari sumber daya asli tertentu. Misalnya, ini memungkinkan Anda mengambil atau membuat sebuah instance CanvasDevice dari sebuah objek ID2D1Device1, sebuah instance CanvasBitmap dari ID2D1Bitmap, dll.

Metode ini juga berfungsi untuk semua efek Win2D bawaan: mengambil sumber daya asli untuk efek tertentu dan kemudian menggunakannya untuk mengambil pembungkus Win2D yang sesuai akan mengembalikan efek Win2D miliknya dengan benar. Agar efek kustom juga mendapat manfaat dari sistem pemetaan yang sama, Win2D mengekspos beberapa API di antarmuka interop untuk pabrik aktivasi CanvasDevice, yang merupakan jenis ICanvasFactoryNative, serta antarmuka pabrik efek tambahan, 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);
};

Ada beberapa API yang perlu dipertimbangkan di sini, karena diperlukan untuk mendukung semua berbagai skenario di mana efek Win2D dapat digunakan, serta bagaimana pengembang dapat melakukan interop dengan lapisan D2D dan kemudian mencoba menyelesaikan pembungkus untuk mereka. Mari kita membahas masing-masing API ini.

Metode RegisterWrapper dan UnregisterWrapper dimaksudkan untuk dipanggil oleh efek kustom untuk menambahkan diri mereka ke dalam cache Win2D internal:

  • RegisterWrapper: mendaftarkan sumber daya asli dan pembungkus WinRT miliknya. Parameter wrapper diperlukan untuk juga menerapkan IWeakReferenceSource, sehingga dapat di-cache dengan benar tanpa menyebabkan siklus referensi karena akan menyebabkan kebocoran memori. Metode mengembalikan S_OK jika sumber daya asli dapat ditambahkan ke cache, S_FALSE jika sudah ada pembungkus yang terdaftar untuk resource, dan kode kesalahan jika terjadi kesalahan.
  • UnregisterWrapper: membatalkan pendaftaran sumber daya asli dan pembungkusnya. Mengembalikan S_OK jika sumber daya dapat dihapus, S_FALSE jika resource belum terdaftar, dan kode erro jika terjadi kesalahan lain.

Efek kustom harus memanggil RegisterWrapper dan UnregisterWrapper setiap kali mereka direalisasikan dan tidak direalisasikan, yaitu ketika sumber daya asli baru dibuat dan dikaitkan dengannya. Efek kustom yang tidak mendukung realisasi (misalnya mereka yang memiliki perangkat terkait tetap) dapat memanggil RegisterWrapper dan UnregisterWrapper ketika dibuat dan dihancurkan. Efek kustom harus memastikan untuk membatalkan pendaftaran diri dengan benar dari semua jalur kode yang mungkin yang akan menyebabkan pembungkus menjadi tidak valid (misalnya, termasuk ketika objek diselesaikan, jika diimplementasikan dalam bahasa terkelola).

Metode RegisterEffectFactory dan UnregisterEffectFactory juga dimaksudkan untuk digunakan oleh efek kustom, sehingga mereka juga dapat mendaftarkan panggilan balik untuk membuat pembungkus baru jika pengembang mencoba menyelesaikannya untuk sumber daya D2D "yatim piatu":

  • RegisterEffectFactory: daftarkan panggilan balik yang memasukkan parameter yang sama dengan yang diteruskan pengembang ke GetOrCreate, dan membuat pembungkus baru yang dapat diperiksa untuk efek input. ID efek digunakan sebagai kunci, sehingga setiap efek kustom dapat mendaftarkan pabrik saat pertama kali dimuat. Tentu saja, ini hanya boleh dilakukan sekali per jenis efek, dan tidak setiap kali efek diwujudkan. Parameter device, resource dan wrapper diperiksa oleh Win2D sebelum memanggil panggilan balik terdaftar apa pun, sehingga mereka dijamin tidak akan null ketika CreateWrapper dipanggil. dpi dianggap opsional, dan dapat diabaikan jika jenis efek tidak memiliki penggunaan khusus untuknya. Perhatikan bahwa ketika pembungkus baru dibuat dari pabrik terdaftar, pabrik itu juga harus memastikan bahwa pembungkus baru terdaftar dalam cache (Win2D tidak akan secara otomatis menambahkan pembungkus yang diproduksi oleh pabrik eksternal ke cache).
  • UnregisterEffectFactory: menghapus fungsi panggilan balik yang didaftarkan sebelumnya. Misalnya, ini dapat digunakan jika pembungkus efek diterapkan dalam rakitan terkelola yang sedang dibongkar.

Catatan

ICanvasFactoryNative diimplementasikan oleh pabrik aktivasi untuk CanvasDevice, yang dapat Anda ambil dengan memanggil RoGetActivationFactorysecara manual , atau menggunakan API pembantu dari ekstensi bahasa yang Anda gunakan (misalnya winrt::get_activation_factory di C++/WinRT). Untuk informasi selengkapnya, lihat Sistem jenis WinRT untuk informasi selengkapnya tentang cara kerjanya.

Sebagai contoh praktis tentang di mana pemetaan ini berperan, pertimbangkan cara kerja efek bawaan Win2D. Jika tidak direalisasikan, semua status (misalnya properti, sumber, dll.) disimpan dalam cache internal di setiap instans efek. Saat mereka direalisasikan, semua status sistem ditransfer ke sumber daya asli (misalnya, properti diatur pada efek D2D, semua sumber telah diselesaikan dan dipetakan ke input efek, dll.), dan selama efek direalisasikan, ia akan bertindak sebagai otoritas pada status pembungkus. Artinya, jika nilai properti apa pun diambil dari pembungkus, itu akan mengambil nilai yang diperbarui untuk itu dari sumber daya D2D asli yang terkait dengannya.

Ini memastikan bahwa jika ada perubahan yang dilakukan langsung ke sumber daya D2D, perubahan tersebut akan terlihat pada pembungkus luar juga, dan keduanya tidak akan pernah "tidak sinkron". Ketika efek tidak direalisasi, semua status ditransfer kembali dari sumber daya asli ke status pembungkus, sebelum sumber daya dirilis. Ini akan disimpan dan diperbarui di sana sampai saat efek berikutnya direalisasikan. Sekarang, pertimbangkan urutan peristiwa ini:

  • Anda memiliki beberapa efek Win2D (baik bawaan, atau kustom).
  • Anda mendapatkan ID2D1Image daripadanya (yang merupakan ID2D1Effect).
  • Anda membuat instance efek kustom.
  • Anda juga mendapatkan ID2D1Image daripadanya.
  • Anda secara manual mengatur gambar ini sebagai input untuk efek sebelumnya (melalui ID2D1Effect::SetInput).
  • Anda kemudian meminta efek awal dari pembungkus WinRT untuk input tersebut.

Karena efeknya terwujud (terwujud ketika sumber daya asli diminta), efek tersebut akan menggunakan sumber daya asli sebagai sumber kebenaran. Dengan demikian, ia akan mendapatkan yang ID2D1Image sesuai dengan sumber yang diminta, dan mencoba mengambil pembungkus WinRT untuk itu. Jika efek input ini diambil dari telah dengan benar menambahkan sepasang sumber daya aslinya sendiri dan pembungkus WinRT ke cache Win2D, pembungkus akan diselesaikan dan dikembalikan ke pemanggil. Jika tidak, akses properti tersebut akan gagal, karena Win2D tidak dapat memecahkan pembungkus WinRT untuk efek yang tidak dimilikinya, karena tidak tahu cara menginstansiasinya.

Di sinilah RegisterWrapper dan UnregisterWrapper membantu, karena mereka memungkinkan efek kustom untuk berpartisipasi dengan mulus dalam logika resolusi pembungkus Win2D, sehingga pembungkus yang benar selalu dapat diambil untuk sumber efek apa pun, terlepas dari apakah itu diatur dari API WinRT, atau langsung dari lapisan D2D yang mendasar.

Untuk menjelaskan bagaimana penghasil efek juga berperan, pertimbangkan skenario ini:

  • Pengguna membuat instans pembungkus kustom dan menyadarinya
  • Mereka kemudian mendapatkan referensi ke efek D2D yang dasar dan menyimpannya.
  • Kemudian, efeknya diwujudkan pada perangkat yang berbeda. Efeknya akan tidak terealisasi dan direalisasikan kembali, dan dalam melakukannya akan menciptakan efek D2D baru. Efek D2D sebelumnya tidak lagi sebagai pembungkus yang dapat diperiksa terkait pada saat ini.
  • Pengguna kemudian memanggil GetOrCreate pada efek D2D pertama.

Tanpa panggilan balik, Win2D hanya akan gagal menyelesaikan pembungkus, karena tidak ada pembungkus terdaftar untuk itu. Jika pabrik terdaftar sebagai gantinya, pembungkus baru untuk efek D2D tersebut dapat dibuat dan dikembalikan, sehingga skenario hanya terus bekerja dengan mulus untuk pengguna.

Menerapkan ICanvasEffect khusus

Antarmuka Win2D ICanvasEffect memperluas ICanvasImage, sehingga semua poin sebelumnya juga berlaku untuk efek kustom. Satu-satunya perbedaan adalah fakta bahwa ICanvasEffect juga menerapkan metode tambahan khusus untuk efek, seperti membatalkan persegi panjang sumber, mengambil persegi panjang yang dibutuhkan, dan sebagainya.

Untuk mendukung ini, Win2D mengekspos ekspor C yang dapat digunakan penulis efek kustom, sehingga mereka tidak perlu mengisi ulang semua logika tambahan ini dari awal. Ini bekerja dengan cara yang sama seperti ekspor C untuk GetBounds. Berikut adalah ekspor yang tersedia untuk efek:

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);

Mari kita membahas bagaimana mereka dapat digunakan:

  • InvalidateSourceRectangleForICanvasImageInterop dimaksudkan untuk mendukung InvalidateSourceRectangle. Cukup atur parameter inputnya kemudian panggil secara langsung, dan itu akan menangani semua pekerjaan yang diperlukan. Perhatikan bahwa image parameter adalah instans efek saat ini yang sedang diimplementasikan.
  • GetInvalidRectanglesForICanvasImageInterop mendukung GetInvalidRectangles. Ini juga tidak memerlukan pertimbangan khusus, selain perlu membuang array COM yang dikembalikan setelah tidak lagi diperlukan.
  • GetRequiredSourceRectanglesForICanvasImageInterop adalah metode bersama yang dapat mendukung GetRequiredSourceRectangle dan GetRequiredSourceRectangles. Artinya, diperlukan penunjuk ke array nilai yang sudah ada untuk diisi, sehingga pemanggil dapat mengirimkan penunjuk ke satu nilai (yang juga bisa berada pada stack, untuk menghindari satu alokasi), atau ke array nilai. Implementasinya sama dalam kedua kasus, sehingga satu ekspor C cukup untuk menggerakkan keduanya.

Efek kustom dalam C# menggunakan ComputeSharp

Seperti yang kami sebutkan, jika Anda menggunakan C# dan ingin menerapkan efek kustom, pendekatan yang direkomendasikan adalah menggunakan pustaka ComputeSharp . Ini memungkinkan Anda untuk mengimplementasikan shader piksel D2D1 kustom sepenuhnya dalam C#, serta untuk dengan mudah menentukan grafik efek kustom yang kompatibel dengan Win2D. Pustaka yang sama juga digunakan di Microsoft Store untuk mendukung beberapa komponen grafis dalam aplikasi.

Anda dapat menambahkan referensi ke ComputeSharp di proyek Anda melalui NuGet:

Catatan

Banyak API di ComputeSharp.D2D1.* identik di seluruh target UWP dan WinUI, satu-satunya perbedaan adalah namespace (berakhiran baik .Uwp atau .WinUI). Namun, target UWP dalam tahap pemeliharaan berkelanjutan dan tidak menerima fitur baru. Dengan demikian, beberapa perubahan kode mungkin diperlukan dibandingkan dengan sampel yang ditunjukkan di sini untuk WinUI. Cuplikan dalam dokumen ini mencerminkan permukaan API pada ComputeSharp.D2D1.WinUI.0.0 (rilis terakhir untuk target UWP adalah 2.1.0).

Ada dua komponen utama dalam ComputeSharp untuk diinteropsi dengan Win2D:

  • PixelShaderEffect<T>: efek Win2D yang didukung oleh shader piksel D2D1. Shader itu sendiri ditulis dalam C# menggunakan API yang disediakan oleh ComputeSharp. Kelas ini juga menyediakan properti untuk mengatur sumber efek, nilai konstanta, dan banyak lagi.
  • CanvasEffect: kelas dasar untuk efek Win2D kustom yang membungkus grafik efek arbitrer. Ini dapat digunakan untuk "mengemas" efek kompleks menjadi objek yang mudah digunakan yang dapat digunakan kembali di beberapa bagian aplikasi.

Berikut adalah contoh shader piksel kustom (di-port dari shader shadertoy ini), digunakan dengan PixelShaderEffect<T> lalu digambar ke dalam Win2D CanvasControl (perhatikan bahwa PixelShaderEffect<T> mengimplementasikan ICanvasImage):

contoh shader piksel yang menampilkan segi enam berwarna tak terbatas, digambar ke kontrol Win2D, dan ditampilkan berjalan di jendela aplikasi

Anda dapat melihat bagaimana hanya dalam dua baris kode Anda dapat membuat efek dan menggambarnya melalui Win2D. ComputeSharp mengurus semua pekerjaan yang diperlukan untuk mengompilasi shader, mendaftarkannya, dan mengelola masa pakai kompleks efek yang kompatibel dengan Win2D.

Selanjutnya, mari kita lihat panduan langkah demi langkah tentang cara membuat efek Win2D kustom yang juga menggunakan shader piksel D2D1 kustom. Kita akan membahas cara menulis shader dengan ComputeSharp dan menyiapkan propertinya, lalu cara membuat grafik efek kustom yang dipaketkan ke dalam CanvasEffect jenis yang dapat dengan mudah digunakan kembali di aplikasi Anda.

Merancang efek

Untuk demo ini, kami ingin membuat efek kaca beku sederhana.

Ini akan mencakup komponen-komponen berikut:

  • Gaussian Blur
  • Efek warna
  • Kebisingan (yang dapat kita hasilkan secara prosedural dengan shader)

Kami juga ingin memperlihatkan properti untuk mengontrol tingkat kekaburan dan kebisingan. Efek akhir akan berisi versi "dikemas" dari grafik efek ini dan mudah digunakan dengan hanya membuat sebuah contoh, mengatur properti yang dimaksud, menghubungkan gambar sumber, serta menggambarnya. Mari kita mulai!

Membuat shader piksel D2D1 kustom

Untuk kebisingan di atas efek, kita dapat menggunakan shader piksel D2D1 sederhana. Shader akan menghitung nilai acak berdasarkan koordinatnya (yang akan bertindak sebagai "seed" untuk angka acak), dan kemudian akan menggunakan nilai kebisingan tersebut untuk menghitung jumlah RGB untuk piksel tersebut. Kita kemudian dapat memadukan kebisingan ini di atas gambar yang dihasilkan.

Untuk menulis shader dengan ComputeSharp, kita hanya perlu menentukan tipe yang mengimplementasikan antarmuka ID2D1PixelShader, dan kemudian menulis logika kita dalam fungsi Execute. Untuk noise shader ini, kita dapat menulis sesuatu seperti ini:

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);
    }
}

Catatan

Sementara shader ditulis sepenuhnya dalam C#, pengetahuan dasar tentang HLSL (bahasa pemrograman untuk shader DirectX, yang digunakan ComputeSharp untuk mentranspilasi C# ke HLSL) direkomendasikan.

Mari kita membahas shader ini secara rinci:

  • Shader tidak memiliki input, itu hanya menghasilkan gambar tak terbatas dengan kebisingan skala abu-abu acak.
  • Shader memerlukan akses ke koordinat piksel saat ini.
  • Shader telah pra-kompilasi pada waktu build (menggunakan PixelShader40 profil, yang dijamin tersedia pada GPU apa pun tempat aplikasi dapat dijalankan).
  • Atribut [D2DGeneratedPixelShaderDescriptor] diperlukan untuk memicu generator sumber yang dibundel dengan ComputeSharp, yang akan menganalisis kode C#, menerjemahkannya ke HLSL, mengompilasi shader ke bytecode, dll.
  • Shader tersebut menangkap parameter, melalui konstruktor utamanya . Generator sumber di ComputeSharp akan secara otomatis mengurus ekstraksi semua nilai yang ditangkap dalam shader dan menyiapkan buffer konstan yang dibutuhkan D2D untuk menginisialisasi status shader.

Dan bagian ini sudah selesai! Shader ini akan menghasilkan tekstur kebisingan kustom kami kapan pun diperlukan. Selanjutnya, kita perlu membuat efek kemasan kita dengan grafik efek yang menghubungkan semua efek kita bersama-sama.

Membuat efek kustom

Untuk efek kemasan yang mudah digunakan, kita dapat menggunakan tipe CanvasEffect dari ComputeSharp. Jenis ini menyediakan cara mudah untuk mengatur semua logika yang diperlukan untuk membuat grafik efek dan memperbaruinya melalui properti publik yang dapat berinteraksi dengan pengguna efek. Ada dua metode utama yang perlu kita terapkan:

  • BuildEffectGraph: metode ini bertanggung jawab untuk membangun grafik efek yang ingin kita gambar. Artinya, perlu membuat semua efek yang kita butuhkan, dan mendaftarkan simpul output untuk grafik. Untuk efek yang dapat diperbarui di lain waktu, pendaftaran dilakukan dengan nilai terkait CanvasEffectNode<T> , yang bertindak sebagai kunci pencarian untuk mengambil efek dari grafik saat diperlukan.
  • ConfigureEffectGraph: metode ini me-refresh grafik efek dengan menerapkan pengaturan yang telah dikonfigurasi pengguna. Metode ini secara otomatis dipanggil ketika diperlukan, tepat sebelum menggambar efek, dan hanya jika setidaknya satu properti efek telah dimodifikasi sejak terakhir kali efek digunakan.

Efek kustom kami dapat didefinisikan sebagai berikut:

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);
    }
}

Anda dapat melihat ada empat bagian di kelas ini:

  • Pertama, kita memiliki bidang untuk melacak semua status yang dapat diubah, seperti efek yang dapat diperbarui serta bidang pencadangan untuk semua properti efek yang ingin kita ekspos ke pengguna efek.
  • Selanjutnya, kita memiliki properti untuk mengonfigurasi efeknya. Setter setiap properti menggunakan metode yang SetAndInvalidateEffectGraph diekspos oleh CanvasEffect, yang akan secara otomatis membatalkan efek jika nilai yang ditetapkan berbeda dari yang saat ini. Ini memastikan efeknya hanya dikonfigurasi lagi jika benar-benar diperlukan.
  • Terakhir, kami memiliki metode BuildEffectGraph dan ConfigureEffectGraph yang kami sebutkan di atas.

Catatan

Simpul PremultiplyEffect setelah efek kebisingan sangat penting: ini karena efek Win2D mengasumsikan bahwa outputnya sudah premultiplikasi, sedangkan shader piksel umumnya bekerja dengan piksel yang umumnya tidak premultiplikasi. Dengan demikian, ingatlah untuk menyisipkan simpul premultiply/unpremultiply secara manual sebelum dan sesudah shader kustom, agar warna tetap terjaga dengan baik.

Catatan

Efek sampel ini menggunakan namespace WinUI, tetapi kode yang sama juga dapat digunakan pada UWP. Dalam hal ini, namespace untuk ComputeSharp akan ComputeSharp.Uwp, sesuai dengan nama paket.

Siap untuk menggambar!

Dan dengan ini, efek kaca beku kustom kami sudah siap! Kita dapat dengan mudah menggambarnya sebagai berikut:

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

    args.DrawingSession.DrawImage(effect);
}

Dalam contoh ini, kita menggambar efek dari handler Draw pada CanvasControl, menggunakan CanvasBitmap yang sebelumnya telah kita muat sebagai sumber. Ini adalah gambar input yang akan kita gunakan untuk menguji efeknya:

gambar beberapa gunung di bawah langit mendung

Dan berikut adalah hasilnya:

versi gambar kabur di atas

Catatan

Kredit ke Dominic Lange untuk gambar.

Sumber Daya Tambahan: