1 poin oleh GN⁺ 2024-07-22 | 1 komentar | Bagikan ke WhatsApp
  • CSS box-shadow biasanya merupakan properti untuk memberi kesan kedalaman pada UI, tetapi jika ratusan hingga ribuan bayangan ditumpuk pada satu div, ia dapat bekerja seperti semacam API menggambar
  • Meski terikat oleh batas bentuk kotak, kombinasi sudut membulat, beberapa rantai bayangan, serta warna·blur·transparansi dapat membuat titik berbentuk lingkaran dan efek berlapis
  • Dalam eksperimen, blur dan transparansi sangat mengurangi jumlah elemen yang bisa dianimasikan, dan tanpa keduanya, laptop M1 mampu menggambar ribuan box-shadow
  • Simulasi bola, piksel gambar, titik permukaan kubus·bola, dan visualisasi responsif musik semuanya diimplementasikan dengan cara memperbarui string box-shadow di setiap frame
  • Pada akhirnya, sebuah ray tracer berbasis CPU diparalelkan dengan Web Workers untuk menunjukkan bahwa rendering adegan pun dapat dilakukan hanya dengan satu div dan box-shadow

Menggunakan box-shadow seperti alat gambar

  • CSS box-shadow adalah salah satu bentuk drop shadow, yang awalnya digunakan untuk menggambar bayangan di belakang gambar atau elemen UI agar memberi kesan kedalaman
  • Drop shadow pada umumnya memindahkan raster gambar pada sumbu x/y lalu menggambarnya di belakang objek asli dengan satu warna untuk menciptakan kesan kedalaman
  • CSS filter: drop-shadow() menerima offset x/y dan warna, serta secara opsional mendukung nilai blur
  • Dengan melapiskan beberapa filter drop shadow, kedalaman pada komposisi layar bisa ditingkatkan

Karakteristik dan batasan box-shadow

  • “Box” pada box-shadow berarti bentuk bayangan dibatasi pada bounding box kontainer
  • Batasan ini terlihat restriktif, tetapi karena sebagian besar UI tersusun dari kotak, dalam UI nyata ini tetap berguna
  • Implementasi CSS box-shadow mendukung optimisasi matematis murah untuk menggambar kotak membulat, dan kotak membulat bisa terlihat seperti lingkaran
  • Desainer dapat memanfaatkan box-shadow tanpa memakai gambar yang sudah dirender sebelumnya, sehingga dapat menghindari bertambahnya ukuran unduhan
  • Pada satu div, beberapa box-shadow dapat dirangkai seperti rantai, dan contoh menggunakan cara ini untuk menyusun warna serta bayangan
  • Penyesuaian border-radius dapat dicoba di CSS border-radius generator

Cara menggunakan box-shadow yang “salah”

  • Dalam desain UI umum, bayangan digunakan sebagai elemen pendukung bersama margin, padding, tipografi, dan aksesibilitas untuk membedakan status dan interaksi
  • Namun box-shadow dapat diperlakukan seperti API painting yang mengecat sejumlah persegi panjang sewenang-wenang ke layar dan secara opsional menerapkan blur
  • Eksperimen awal dimulai dari seni minimalis di tulisan sebelumnya, dan menghasilkan keluaran visual hanya dengan pengaturan blok warna sederhana
  • Setelah itu dibuat alat visualisasi untuk melihat dampak lebih banyak box-shadow, blur, dan transparansi terhadap performa
    • Membuat string box-shadow besar lalu menetapkannya pada satu div
    • Animasi awalnya dilakukan dengan mengganti string box-shadow setiap 300ms lalu menyerahkannya ke transition: all
    • Cara ini menimbulkan jank dan lebih lambat dibanding menetapkan box-shadow pada setiap frame
  • Contoh 100 box-shadow me-remix palet warna lewat tap, dan menampilkan riwayat 10 palet terakhir di sisi kiri
  • Saat blur diterapkan, jumlah yang bisa dianimasikan menurun, dan warna transparan juga sangat mengurangi jumlah elemen yang dapat digambar
  • Ukuran div juga memengaruhi performa, dan saat animasi, rasterizer perangkat lunak tampak ikut terlibat
  • Tanpa transparansi dan blur, laptop M1 dapat menggambar ribuan box-shadow

Membuat simulasi bola dengan box-shadow

  • box-shadow tidak bisa diputar, tetapi dengan border-radius ia dapat dibuat terlihat seperti lingkaran
  • Beberapa bayangan melingkar diperlakukan seperti bola, dan ukurannya diskalakan berdasarkan nilai z untuk menciptakan kedalaman 3D palsu
  • Implementasinya memperbarui state permainan di requestAnimationFrame, lalu menetapkan string box-shadow besar ke div pada setiap frame
  • Proses renderingnya sebagai berikut
    • Mengurutkan bola berdasarkan nilai z
    • Menghitung ukuran berdasarkan nilai z
    • Mengubah posisi x/y, spread, dan warna tiap bola menjadi item box-shadow
    • Menggabungkan item-item itu dengan koma menjadi satu string
  • Contoh 50 bola memungkinkan bola digerakkan dengan drag dan memantul di dalam kotak
  • Skala 3D palsu memberi sedikit kesan kedalaman, tetapi saat bola mendekati “kamera”, lingkaran bisa tampak pecah
    • Karena div dasar pembentuk box-shadow terlalu kecil dibanding metode penskalaannya
    • Masalah ini bisa diatasi dengan memperbesar kontainer, tetapi performa melambat saat kontainer makin besar
  • Versi yang membuat bola saling bertabrakan memakai pemeriksaan tumbukan n^2 dan hanya memantulkan kecepatan saat tabrakan
    • Ini tidak mensimulasikan interaksi fisika yang sebenarnya
    • Agar mudah dilihat, posisi z dibuat tetap sehingga menjadi 2D
  • Ada juga contoh di mana bola berusaha kembali ke posisi awal acak, dan saat bola ditarik dengan gaya sentuhan, muncul efek seperti menarik potongan dari spons

Gambar dan point cloud 3D

  • Eksperimen berikutnya memetakan piksel gambar menjadi titik-titik pada bidang 2D, lalu menggambar tiap titik dengan box-shadow
  • Posisi dan warna piksel gambar dibaca lalu disimpan ke state.particles, dengan tiap piksel dipakai sebagai satu partikel
  • Sumbernya ada di contoh CodeSandbox
  • Contoh ini merender ribuan box-shadow di ruang 3D, dan saat di-drag memungkinkan interaksi seperti menyebarkan gambar
  • Versi live dengan rotasi otomatis tersedia di /box-shadow/v3?width=80&size=5&autoRotate=1, dengan peringatan soal konsumsi baterai saat dijalankan
  • Contoh dengan jumlah lebih besar berada di kisaran sekitar 12.000 box-shadow, dan pada tingkat ini mulai terlihat tersendat
  • Di M1 jumlah box-shadow yang sangat besar masih bisa diproses, tetapi desktop, iPhone, dan Android lama tidak mampu menangani tingkat yang sama

Menempatkan titik pada permukaan kubus dan bola

  • Eksperimen juga berlanjut ke proyeksi titik secara merata ke permukaan mesh
  • Kubus diimplementasikan dengan menempatkan titik di sepanjang setiap sisinya
    • Diasumsikan semua sisi memiliki panjang yang sama
    • Setiap sisi diiterasi dan diisi titik sesuai ukuran yang diberikan
    • Menambah jumlah titik meningkatkan ketelitian kubus
  • Contoh kubus dapat diinteraksikan lewat tap, dan menambahkan pencahayaan kecil yang mengikuti posisi mouse untuk menambah kesan kedalaman
  • Perhitungan pencahayaannya tidak akurat dan memakai beberapa “magic constants”
  • Fungsi pemetaan partikel kubus dan berbagai helper matematika didapat menggunakan gypity
    • Hasil pertama berupa distribusi acak
    • Pada percobaan kedua didapat penempatan merata di permukaan kubus
  • Penempatan merata di permukaan bola lebih rumit sehingga digunakan metode spiral discretization
    • Idenya adalah menempatkan titik secara merata di atas garis yang membungkus bola dari bawah ke atas
    • Ini dianalogikan seperti tali yang melilit bola
  • Pemetaan linear ini juga dipakai untuk membuat animasi yang dihubungkan dengan array nilai frekuensi musik
  • Pada titik-titik tersebut diterapkan animasi palet gradasi yang diinterpolasi seiring waktu, dan bagian ini lebih sulit dibanding pekerjaan sebelumnya
  • Contoh bola dengan jumlah putaran sedikit menunjukkan bahwa spiral mendekati distribusi merata, tetapi akan tampak pecah jika jumlah putarannya tidak cukup

Ray tracer box-shadow

  • Gagasan bahwa hampir semua CGI bisa dibuat jika segitiga dapat digambar dengan box-shadow mengarah ke eksperimen ray tracing
  • Tujuannya adalah menggambar adegan hanya dengan satu div dan box-shadow
  • Contoh-contoh berikutnya memakai kombinasi rendering live berpresisi rendah dan rendering gambar berpresisi tinggi, dan ada juga contoh tanpa batas presisi sehingga perlu hati-hati saat menjalankannya
  • Ray tracer dan ray marcher adalah metode pembuatan gambar yang akurat tetapi lambat, dan digunakan luas di industri CGI
  • Di sini digunakan tracer berbasis CPU alih-alih GPU
    • Karena jika memakai GPU, inti eksperimennya berkurang dan implementasinya juga lebih sulit
  • Tracer awal dibuat hanya untuk merender bola, dan model data adegannya mencakup kamera, beberapa bola, serta informasi material
  • Kode awal dari gypity berjalan real-time tetapi punya masalah
    • Kode untuk menambahkan bias coefficient yang diperlukan dalam optimisasi sampling hilang
    • Komentar terkait memang ada, tetapi baris kodenya tidak ada; setelah menelusuri teknik sampling, hal itu diperbaiki

Progressive rendering dan Three.js

  • Setelah itu, kodenya diubah ke struktur progressive rendering
  • Progressive rendering membagi banyak perhitungan ray ke beberapa frame sehingga proses rendering yang makin mendekati “ground truth” bisa terlihat
  • Struktur ini cocok dengan fitur menggerakkan kamera secara interaktif
  • Kamera dan orbit controls menggunakan pustaka Three.js
    • Agar tidak perlu mengimplementasikan sendiri matematika matriks dan dukungan mobile
  • Versi tersebut hanya bisa merender bola, dan semua elemen dalam adegan pada dasarnya adalah bola yang diskalakan
  • Contoh secara default berjalan pada sekitar sedikit di atas 6% dari resolusi penuh
  • Semakin jauh dari layar, adegan beresolusi rendah terlihat lebih tajam, dan saat dilihat dekat kekurangan presisinya menjadi lebih jelas
  • Melalui query parameter, skala resolusi, pixelSize, jumlah pantulan ray, dan jumlah sampel maksimum dapat diatur
  • Menaikkan resolusi dan sampel memperbaiki hasil, tetapi menjadi sangat lambat

Implementasi tracer dan bottleneck performa

  • Rendering dilakukan dengan cara standar: menembakkan ray dari kamera untuk setiap piksel, menghitung warna, lalu mengambil rata-rata antar-frame
  • Karena tracer memakai Three.js, banyak objek baru dibuat dan cepat menumpuk sebagai garbage
  • Ada beberapa upaya reuse objek, tetapi untuk memeras performa semaksimal mungkin, lebih baik tidak memakai Three.js
  • Berdasarkan profiler, garbage collection tidak membuat perbedaan yang besar, sehingga penggunaan Three.js dipertahankan
  • Ide dasar tracer adalah ray memantul setelah mengenai objek di adegan hingga mencapai cahaya, lalu mengembalikan warna sesuai sifat objek dan pencahayaan
  • Alasan perlu menembakkan banyak ray adalah karena ada kasus ray tidak mencapai sumber cahaya, dan ada juga yang mencapainya
  • Tracer ini memakai model pencahayaan sederhana
    • Tidak ada BRDF yang akurat secara fisik
    • Tidak ada tekstur
    • Tidak ada subsurface scattering
    • Menggunakan diffuse light sederhana dan specular reflection
  • Untuk bahan belajar ray tracing, bisa melihat Ray Tracing in One Weekend
  • Setelah itu ia bisa merender plane, dan setelah memperbaiki kesalahan manipulasi bit pada nilai floating point, persegi panjang cahaya juga bisa dirender

Paralelisasi dengan Web Workers

  • Peningkatan performa didekati lewat multithreading Web Workers alih-alih memperbaiki masalah GC di Three.js
  • Ray tracing sangat cocok diparalelkan karena setiap perhitungannya mengembalikan satu hasil dan tidak memiliki efek samping
  • Worker manager membuat pool worker dan menyediakan dua metode, render dan updateScene, agar adegan bisa diubah saat runtime
  • Kode worker merupakan salinan tracer yang ada, dan perlu data marshaling untuk mengganti adegan
  • Untuk mengurangi overhead postMessage, worker merender satu frame penuh, bukan satu piksel
  • Contoh layar penuh berjalan jauh lebih cepat dibanding sebelumnya
  • Kekurangannya, saat kamera atau adegan berubah, semua hasil perhitungan ray sebelumnya menjadi tidak valid sehingga layar bisa terlihat hitam
  • Solusinya diimplementasikan dengan menyalakan flag isDirty pada event input lalu memutuskan di loop update apakah frame perlu dihapus
  • Ada kasus saat worker mengirim frame yang dihitung dari data adegan lama tepat setelah pembaruan adegan
    • Ini bisa dibuang dengan memasukkan timestamp atau scene id ke postMessage
    • Dalam eksperimen ini dibiarkan saja karena data ray yang salah untuk satu frame segera dirata-ratakan
  • Pada akhirnya, ray tracing berbasis box-shadow terbukti cukup berjalan, dan sumbernya ada di CodeSandbox
  • Statistik performanya menampilkan estimasi jumlah total ray, jumlah sampel, dan resolusi rendering
  • Sampel default berhenti di 1200, tetapi bisa diatur
  • Model pencahayaan alternatif yang lebih cepat tetapi kurang akurat juga bisa di-toggle
  • Data adegannya berbentuk JSON sehingga mudah diedit langsung

Kesimpulan: eksperimen yang mungkin, tetapi tidak direkomendasikan

  • Hanya dengan box-shadow pada satu div, simulasi bola, point cloud, partikel berbasis gambar, permukaan kubus·bola, hingga adegan ray tracing dapat digambar
  • Namun secara keseluruhan, eksperimen ini lebih dekat ke pendekatan “jangan dilakukan” tanpa kasus penggunaan praktis
  • Masih ada ruang perbaikan jika ditambahkan pemuatan triangle mesh, acceleration structure, dan model pencahayaan yang lebih akurat
  • gypity awalnya menjawab bahwa box-shadow ray tracer tidak mungkin dibuat, tetapi contoh nyata akhirnya berhasil dibuat
  • CSS memang tidak intuitif, tetapi kadang memiliki kemungkinan aneh yang jelas-jelas bisa bekerja

1 komentar

 
GN⁺ 2024-07-22
Komentar Hacker News
  • Menggunakan warna transparan membuat batching penggambaran GPU menjadi terbatas, sehingga lebih lambat
    Penggambaran opak bisa memakai depth buffer sehingga urutannya dapat diubah sesuka hati dan batching bisa dimaksimalkan. Untuk yang transparan, agar blending berjalan benar, harus digambar sesuai urutan painter

    • Menurut saya lebih rumit dari itu. Browser web memang memakai rendering GPU, tetapi bukan game engine, dan jika semua objek di layar digambar ulang di setiap frame, halaman yang besar dan kompleks bisa mudah tersendat
      Khususnya Chromium berusaha meminimalkan jumlah layer keseluruhan, merender tiap layer menjadi peta piksel, lalu mengomposisikan layer-layer yang terlihat di setiap frame menjadi gambar akhir. Dalam praktiknya banyak layer yang pikselnya tidak berubah dan hanya posisinya yang bergerak, sehingga cukup dikomposisikan saja tanpa dirasterisasi setiap frame
      Jika ada banyak box-shadow tanpa transparansi, Chromium bisa merasterisasi semuanya sekaligus sebagai satu layer, tetapi jika ada banyak box-shadow transparan, masing-masing bisa dibuat sebagai layer terpisah. Dalam kasus ini bisa tidak efisien, tetapi jika box-shadow semitransparan itu harus bergerak secara independen di halaman, itu juga cara yang diperlukan
    • Urutan painter terbalik kadang lebih baik daripada urutan painter, karena objek yang sepenuhnya tertutup bisa dilewati
      Mulai dari buffer yang sepenuhnya transparan (α=0.0), lalu telusuri setiap permukaan dari depan ke belakang, dan untuk tiap piksel blend sebesar 1.0-buffer.α dari piksel baru ke buffer yang ada. Jika buffer.α == 1.0, bisa dilewati sepenuhnya seperti depth buffer
      Namun matematika untuk kasus ketika ada objek transparan lain di belakang objek transparan perlu diperiksa lagi, dan kasus permukaan yang saling tumpang tindih secara siklik atau saling menembus itu rumit
    • GPU juga tidak menyukai overdraw, jadi sebaiknya hindari menumpuk banyak elemen transparan
      Ini juga alasan mengapa menggambar lebih banyak segitiga umumnya lebih baik daripada memakai tekstur transparan
    • Tahap pengurutan itu sendiri biasanya bukan masalah besar. Sebelum mengirim draw call, pengurutan memang sering dilakukan. Biaya sebenarnya berasal dari overdraw
      Rendering opak bisa digambar dari depan ke belakang, sehingga yang dirender hanyalah yang benar-benar terlihat di framebuffer akhir, dan jumlah piksel setelah depth pass sebanding dengan ukuran framebuffer
      Rendering transparan harus digambar dari belakang ke depan, dan banyak bagian scene yang nantinya akan tertutup sebagian oleh poligon lain tetap harus dirender. Karena itu, jumlah piksel yang melewati pipeline shader bisa membesar hingga sebanding dengan ukuran mesh
      Jika elemennya tidak saling tumpang tindih, transparansi hampir tidak akan memperlambat. Bagaimanapun tiap piksel tetap harus disentuh sekali, dan yang berubah hanya rumus shader-nya
    • Ada satu hal lain yang perlu dipertimbangkan: terutama di perangkat mobile, bandwidth memori bisa menjadi bottleneck
      Jika penggambaran opak berada di atas penggambaran lain, dalam kasus terbaik semua pekerjaan menggambar yang bertumpuk bisa dihilangkan, dan bahkan dalam kasus terburuk hanya bandwidth sebesar penggambaran individual yang diperlukan
      Pada transparansi, jika operasi tidak bisa digabungkan dengan cara apa pun, seluruh area yang bertumpuk harus dibaca kembali, sehingga untuk setiap penggambaran transparan, bitmap berukuran minimal dua kali ukuran framebuffer akhir harus bolak-balik melalui bus memori
      Mengingat banyak perangkat mobile dulu bahkan tidak punya bandwidth memori yang cukup untuk melakukan blit seluruh layar dua kali dalam waktu yang diperlukan untuk mempertahankan 60fps, ini menjadi masalah yang cukup besar
  • Ini eksplorasi yang benar-benar menarik
    Benar juga bahwa layering adalah kata penting. Efek konyol tetapi kadang terlihat keren dari proyek text shadow yang saya buat 14 tahun lalu juga bertumpu pada layering: https://paulirish.github.io/mothereffingtextshadow/

    • Tulisan ini juga sepertinya punya beberapa layer, dan sebenarnya tampaknya bukan hanya tulisan tentang box-shadow
  • Saya agak malu karena baru di paragraf terakhir menyadari bahwa gypity merujuk pada ChatGPT

    • Itu istilah yang dipakai di kanal YouTube Primeagen
    • Setelah melihat ini, saya jadi tahu bahwa kata itu bukan lelucon internal buatan rekan kerja, melainkan ungkapan yang sedang menjadi meme
    • Saya kira itu semacam tooling sisi klien terkait gyp
  • Saya sepenuhnya menyambut hacking tidak praktis gaya lama yang bagus. Namun perlu diingat bahwa canvas sudah ada, dan pekerjaan seperti ini lebih mudah, cepat, dan baik dilakukan dengan canvas

    • Tapi nilai komedinya lebih rendah. Alasan lucunya melakukan ini hanya dengan box-shadow adalah justru karena itu sangat tidak praktis
    • Canvas membuat kita mengorbankan banyak hal, terutama dalam hal aksesibilitas
    • Canvas memang lebih baik, tetapi itu berlaku ketika membuat sesuatu yang tetap berada di dalam kotak berukuran tetap
    • Tentu saja, ini mirip dengan mengatakan bahwa karena keyboard sudah ada, tidak perlu membuat musik dengan floppy drive atau ayam karet
  • Visualisasi musiknya jelas keren. Saya benar-benar merindukan masa Winamp dulu, ketika kita memutar musik lalu menampilkan visualisasi layar penuh
    Saya berharap pemutar audio streaming sekarang juga menyediakan fitur seperti ini

    • Saya masih memakai Winamp, tepatnya WACUP, bersama visualisasi Milkdrop hampir setiap hari
      Namun memang benar bahwa pemutar audio streaming hanyalah perangkat lunak dengan fitur yang sangat dasar
  • Pada akhirnya, Firefox dan Chrome masih merender 1px box-shadow secara berbeda pada zoom browser 150%
    Saya berharap pada Baseline 2025

  • Saya penasaran apakah ada referensi untuk mempelajari lebih lanjut tentang “hack matematika untuk menggambar kotak membulat yang disukai orang UI dengan sangat murah”

  • Ini jenis hacking yang saya sukai
    Rasanya hampir kebalikan total dari tulisan-tulisan Josh Comeau yang pernah saya baca tentang topik ini: https://www.google.com/search?q=josh+comeau+shadows

  • Tulisan bagus yang mungkin salah satu terbaik yang saya baca tahun ini berakhir dengan your welcome, bukan you're
    Harus segera diperbaiki. Atau mungkin saya yang tidak menangkap leluconnya

  • Selama 30 tahun terakhir saya menjadi cukup mahir dalam pemrograman, tetapi karena tidak menyukai game, saya hampir tidak pernah menyentuh grafika
    Sekarang saya menganggap itu sebagai kesalahan besar dan sudah lebih dari setahun berusaha mengejar ketertinggalan, tetapi benar-benar sulit

    • Saya mulai dari game lalu beralih ke CRUD atau pemrograman Oracle Forms sederhana, dan dalam semalam saya mendapat gaji tiga kali lipat dibanding gaji saya sebagai programmer game sebelumnya