- City In A Bottle adalah demo JavaScript yang memuat mesin raycasting dan generator kota di dalam HTML 256 byte
- Eksekusi dimulai hanya dengan
<canvas> dan onclick=setInterval(...), lalu menggambar ulang adegan setiap frame pada kanvas selebar 99 piksel
- Rendering diproses tanpa fungsi trigonometri, hanya dengan aljabar dasar dan operasi bit, serta membuat bayangan dengan menelusuri sinar kamera dan sinar arah sumber cahaya untuk setiap piksel
- Bentuk kota membagi tinggi bangunan, jalan, dan ruang kosong berdasarkan koordinat
X, Y, Z serta kondisi seperti X/9^Z/8, X%99, Z>32
- Kecerahan akhir memadukan kabut jarak, bayangan, dan tekstur
(X&Y&Z)%3/Z ke dalam lebar fillRect, sehingga kota, perspektif, dan tekstur semuanya diwujudkan dengan kode yang sangat kecil
Struktur keseluruhan di dalam HTML 256 byte
- Demo ini bukan sekadar potongan JavaScript, melainkan berjalan sebagai satu program HTML yang valid
<canvas style=width:99% id=c onclick=setInterval('for(c.width=w=99,++t,i=6e3;i--;c.getContext`2d`.fillRect(i%w,i/w|0,1-d*Z/w+s,1))for(a=i%w/50-1,s=b=1-i/4e3,X=t,Y=Z=d=1;++Z<w&(Y<6-(32<Z&27<X%w&&X/9^Z/8)* 8%46||d|(s=(X&Y&Z)%3/Z,a=b=1,d=Z/w));Y-=b)X+=a',t=9)>
- Keseluruhannya terbagi menjadi kode HTML, loop pembaruan frame, sistem rendering, mesin raycasting, dan logika generasi kota
- Hanya menggunakan aljabar dasar dan operasi bit, bukan fungsi trigonometri atau matematika tingkat lanjut
- Setelah pertama kali dipublikasikan, selama sekitar 2 tahun ini menjadi salah satu posting paling populer di timeline Twitter pembuatnya
HTML dan loop eksekusi
- HTML hanya terdiri dari satu kanvas dan event klik
<canvas style=width:99% id=c onclick=setInterval('',t=9)>
id pada canvas ditetapkan sebagai c agar bisa diakses singkat dari JavaScript
style=width:99% tidak wajib; tanpa itu pun tetap berjalan
onclick memanggil setInterval untuk memulai loop pembaruan
- Interval ditetapkan
9 milidetik
- Variabel waktu
t juga diinisialisasi menjadi 9 di sini untuk menghemat ruang
- Ada bug kecil: jika kanvas diklik beberapa kali, beberapa interval akan berjalan sekaligus sehingga menjadi lambat
JavaScript yang ditulis ulang agar mudah dibaca
- Payload JavaScript yang dijalankan setelah klik berukuran 199 byte
for(c.width=w=99,++t,i=6e3;i--;c.getContext`2d`.fillRect(i%w,i/w|0,1-d*Z/w+s,1))for(a=i%w/50-1,s=b=1-i/4e3,X=t,Y=Z=d=1;++Z<w&(Y<6-(32<Z&27<X%w&&X/9^Z/8)* 8%46||d|(s=(X&Y&Z)%3/Z,a=b=1,d=Z/w));Y-=b)X+=a
- Jika diberi spasi dan baris baru, alurnya dapat dibaca seperti berikut
c.width = w = 99
++t
for (i = 6e3; i--;)
{
a = i%w/50 - 1
s = b = 1 - i/4e3
X = t
Y = Z = d = 1
for(; ++Z<w &
(Y < 6 - (32<Z & 27<X%w && X/9^Z/8)*8%46 ||
d | (s = (X&Y&Z)%3/Z, a = b = 1, d = Z/w));)
{
X += a
Y -= b
}
c.getContext`2d`.fillRect(i%w, i/w|0, 1 - d*Z/w + s, 1)
}
c.width = w = 99 membersihkan kanvas, menetapkan lebarnya menjadi 99 piksel, dan menyimpan nilai yang sama ke w untuk dipakai berulang
- Tinggi default kanvas adalah 150, dan area di bawahnya dibiarkan kosong
++t menaikkan nilai waktu di setiap frame untuk menganimasikan adegan
for (i = 6e3; i--;) adalah loop luar yang menentukan kecerahan per piksel
Perhitungan sinar kamera
- Setiap piksel diperlakukan sebagai satu sinar yang keluar dari kamera
- Komponen horizontal disimpan di
a
a = i % w / 50 - 1
i % w mendapatkan posisi horizontal piksel saat ini, lalu dibagi 50 dan dikurangi 1 untuk dinormalisasi kira-kira ke nilai antara -1 dan 1
- Komponen vertikal disimpan di
b, dan nilai yang sama juga dimasukkan ke s untuk fade latar belakang
b = s = 1 - i / 4e3
- Alih-alih menghitung rasio vertikal secara presisi, kode disederhanakan menjadi
i / 4e3 untuk mengurangi ukuran kode
- Penyederhanaan ini menghasilkan kemiringan yang hampir tidak terlihat, tetapi menghemat byte
4e3 dipilih untuk memindahkan horizon ke bawah dari bagian tengah
- Posisi awal kamera memakai nilai waktu untuk membuat adegan yang bergerak ke kanan
X = t
Y = Z = d = 1
Y, Z, dan d yang digunakan untuk kabut jarak semuanya diinisialisasi menjadi 1
Generasi kota dan deteksi tabrakan
- Loop dalam adalah inti sistem raycasting, memajukan
Z sampai menyentuh sesuatu
for(; ++Z<w &
Z bertambah selama masih lebih kecil dari w, yaitu 99
- Bangunan kota, gang, dan ruang kosong di sisi laut dibuat dari kondisi berikut
Y < 6 - (32<Z & 27<X%w && X/9^Z/8)*8%46
- Kode memeriksa apakah sinar lebih rendah daripada tinggi di posisi tersebut untuk menentukan ada tidaknya tabrakan
6 - berfungsi menurunkan hasil tinggi ke bawah tengah dan membaliknya agar permukaan tanah berada di bagian bawah
- Kondisi di dalam tanda kurung menentukan bentuk kota
32<Z menyisakan ruang antara kamera dan deretan bangunan pertama
27<X%w membuat ruang kosong periodik yang membagi blok kota seperti jalan
- Pada nilai negatif, hasilnya selalu false sehingga terbentuk area kosong seperti laut
X/9^Z/8 menggunakan bitwise XOR untuk membuat distribusi tinggi bangunan yang tampak acak
- Nilai pembagi
9 dan 8 mengatur lebar dan kedalaman bangunan
X/9 habis dibagi 9 bersama angka-angka terkait lebar jalan, sehingga mencegah munculnya bangunan yang sangat tipis di tepi
- Hasil tanda kurung dikalikan
8 lalu diambil sisanya dengan 46 untuk membuat rentang tinggi maksimum
8 dan 46 dipilih secara eksperimental untuk menghasilkan tinggi bangunan yang bervariasi
Bayangan, tekstur, dan kabut jarak
- Saat terjadi tabrakan, loop dalam yang sama mengambil peran kedua untuk melakukan pemeriksaan arah sumber cahaya
d | (s = (X&Y&Z)%3/Z, a = b = 1, d = Z/w)
d | membedakan apakah sinar saat ini adalah sinar dari kamera atau sinar untuk memeriksa bayangan ke arah cahaya
- Awalnya
d = 1, sehingga berjalan sebagai sinar kamera
- Setelah tabrakan,
d = Z/w menjadi nilai kurang dari 1, hasil evaluasi bit OR berubah, loop berjalan lagi, dan pemeriksaan bayangan dimulai
- Jika terjadi tabrakan lagi selama pemeriksaan bayangan, loop keluar dan piksel tersebut digambar sebagai bayangan
- Nilai tekstur
s dibuat dengan ekspresi berikut
s = (X&Y&Z)%3/Z
- Bitwise AND diterapkan pada
X, Y, dan Z, lalu sisa pembagian dengan 3 digunakan untuk membuat tekstur abu-abu yang tampak seperti jendela
- Kemudian dibagi lagi dengan
Z agar tekstur yang jauh terlihat lebih samar
- Untuk mengirim sinar ke sumber cahaya directional seperti matahari,
a dan b sama-sama disetel ke 1
d = Z/w adalah nilai kabut jarak, dan digunakan untuk membuat bangunan jauh menjadi lebih terang
Menggambar piksel dan representasi kecerahan
- Setiap piksel digambar dengan
fillRect
c.getContext`2d`.fillRect(i%w, i/w|0, 1 - d*Z/w + s, 1)
i%w membuat koordinat x, sedangkan i/w|0 membuat koordinat y
- Kecerahan direpresentasikan dengan mengurangi lebar piksel, teknik kunci untuk membuat citra grayscale dengan kode kecil
1 setara dengan piksel hitam, sehingga ekspresi akhir mengurangi nilai dari 1 untuk membuat kecerahan gambar
d * Z/w menggabungkan efek bayangan dan jarak
- Jika bukan bayangan, sinar bergerak sampai jarak maksimum
w, sehingga Z/w menjadi 1
- Di dalam bayangan,
Z lebih kecil dari w sehingga tampak lebih gelap
- Semakin dekat objek yang menghalangi cahaya, semakin gelap bayangannya, meninggalkan efek mirip ambient occlusion
- Di akhir,
s ditambahkan untuk mencampur tekstur bangunan ke kecerahan akhir
Demo lanjutan dan alat eksperimen
- Diajukan ke Revision 2022 demo party dan bisa dilihat di Pouet
- Saat itu demo ini dikirim ke kategori yang salah, sehingga tidak mendapat peringkat tinggi
- Setelah itu, di Shadertoy, Xor dan coder lain membuat shader 256 byte yang mereproduksi versi JavaScript ini
- Daniel Darabos membuat alat Observable yang memungkinkan berbagai elemen program dimanipulasi secara real-time
- Kode ini bisa di-remix di Dwitter, dan juga dapat dieksperimenkan di CapJS
1 komentar
Opini Hacker News
Generasi prosedural pada game 8-bit awal Elite: https://procedural-generation.tumblr.com/post/112509130817/e...
Artikel umum tentang generasi prosedural: https://en.wikipedia.org/wiki/Procedural_generation
Konsep yang agak terkait juga ada pada lazy evaluation dalam bahasa pemrograman fungsional: https://en.wikipedia.org/wiki/Lazy_evaluation
Jika suatu algoritma ray tracing hanya mengevaluasi titik yang terlihat saat membuat gambar 2D dari data 3D, itu tampak mirip dengan konsep lazy evaluation, yaitu “menunda evaluasi ekspresi sampai nilainya diperlukan”: https://en.wikipedia.org/wiki/Ray_tracing_(graphics)
Lebih jauh lagi, secara kasar terasa terhubung juga dengan “observasi meruntuhkan fungsi gelombang”: https://en.wikipedia.org/wiki/Wave_function_collapse
Namun karya ini lebih mengesankan karena dibuat dengan JavaScript. Sebab JavaScript biasanya sering memiliki kepadatan kode yang lebih buruk dibandingkan machine code x86 16-bit
Source code-nya juga disertakan
http://www.youtube.com/watch?v=36BPql6Nl_U
Namanya berasal dari fraktal Menger sponge yang menjadi dasarnya: https://en.wikipedia.org/wiki/Menger_sponge
Keunggulan besar JavaScript di sini adalah standard dweet library menyediakan fungsi seperti sin/cos/fill/line. Sebaliknya, BIOS x86 hanya melakukan hal seperti “peralihan mode”, lalu setelah itu kita harus menangani array piksel secara langsung
Karena itu, saya jadi berpikir bahwa large language model mungkin bukan cara terbaik untuk memodelkan dunia
Kalau saya bisa membuatnya dalam sekitar 256 baris saja, saya sudah akan puas, dan kenyataannya kemungkinan besar butuh jauh lebih banyak dari itu
Banyak hal visual, baik buatan maupun alami, mengikuti pola. Kalau tidak, sejak awal pun akan sulit dikenali. Karena itu ada banyak hal yang bisa dimanfaatkan secara matematis. Untuk tekstur ketinggian bisa dipakai berbagai jenis noise atau pola, fraktal juga bisa dimanfaatkan secara menarik, Fibonacci/golden ratio untuk vegetasi dan pohon, rasio ukuran antara elemen rangka dan tulang, dan sebagainya. Untuk hal seperti ini tidak diperlukan mesin tebak ajaib
Itu adalah program seukuran tweet untuk konsol virtual Pico-8