- City In A Bottle, 256 baytlık HTML içine bir raycasting motoru ve şehir üreticisi sığdıran bir JavaScript demosu
- Çalıştırma yalnızca
<canvas> ve onclick=setInterval(...) ile başlıyor; 99 piksel genişliğindeki canvas üzerinde her karede sahne yeniden çiziliyor
- Render işlemi trigonometrik fonksiyonlar olmadan, temel cebir ve bit işlemleri ile yapılıyor; her piksel için kamera ışını ve ışık yönüne giden ışın izlenerek gölgeler oluşturuluyor
- Şehir biçimi,
X, Y, Z koordinatları ile X/9^Z/8, X%99, Z>32 gibi koşullar üzerinden bina yükseklikleri, yollar ve boş alanlara ayrılıyor
- Son parlaklık, mesafe sisi, gölge ve
(X&Y&Z)%3/Z dokusunun fillRect genişliğine karıştırılmasıyla ifade ediliyor; son derece küçük bir kodla şehir, perspektif ve doku birlikte uygulanıyor
256 baytlık HTML içindeki genel yapı
- Demo, bir JavaScript parçası olarak değil, geçerli bir HTML programı olarak çalışıyor
<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)>
- Bütün yapı, HTML kodu, kare güncelleme döngüsü, render sistemi, raycasting motoru ve şehir üretim mantığına ayrılıyor
- Trigonometrik fonksiyonlar ya da ileri matematik yerine yalnızca temel cebir ve bit işlemleri kullanılıyor
- İlk yayımlandıktan sonra yaklaşık 2 yıl boyunca yazarın Twitter zaman akışındaki en popüler gönderilerden biri olmuştu
HTML ve çalıştırma döngüsü
- HTML, tek bir canvas ve bir tıklama olayından oluşuyor
<canvas style=width:99% id=c onclick=setInterval('',t=9)>
canvas öğesinin id değeri c olarak verilerek JavaScript içinden kısa yoldan erişiliyor
style=width:99% zorunlu değil; olmasa da çalışıyor
onclick içinde setInterval çağrılarak güncelleme döngüsü başlatılıyor
- Aralık
9 milisaniye olarak belirlenmiş
- Zaman değişkeni
t de burada 9 olarak başlatılarak yer kazanılıyor
- Canvas'a birden fazla kez tıklanırsa birden çok interval çalışıp yavaşlamaya yol açan küçük bir hata var
Daha okunabilir hale getirilmiş JavaScript
- Tıklamadan sonra çalışan JavaScript payload'u 199 bayt
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
- Boşluklar ve satır sonları eklendiğinde şu akışla okunabiliyor
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, canvas'ı temizleyip genişliğini 99 piksel olarak ayarlıyor ve aynı değeri tekrar kullanmak için w içine koyuyor
- Varsayılan canvas yüksekliği 150 ve bunun altındaki alan boş kalıyor
++t, her karede zaman değerini artırarak sahneyi animasyonlu hale getiriyor
for (i = 6e3; i--;), piksel başına parlaklığı belirleyen dış döngü
Kamera ışını hesabı
- Her piksel, kameradan çıkan tek bir ışın olarak ele alınıyor
- Yatay bileşen
a içinde tutuluyor
a = i % w / 50 - 1
i % w ile mevcut pikselin yatay konumu alınıyor, 50'ye bölünüp 1 çıkarılarak yaklaşık -1 ile 1 arasına normalize ediliyor
- Dikey bileşen
b içinde tutuluyor ve aynı değer arka plan fade'i için s içine de giriyor
b = s = 1 - i / 4e3
- Tam doğru dikey oran hesabı yerine
i / 4e3 kullanılarak kod boyutu küçültülüyor
- Bu sadeleştirme neredeyse fark edilmeyen bir eğilme oluşturuyor ama bayt tasarrufu sağlıyor
4e3, ufuk çizgisini merkezin altına taşımak için seçilmiş bir değer
- Kamera başlangıç konumu zaman değerini kullanıyor ve sağa doğru hareket eden bir sahne oluşturuyor
X = t
Y = Z = d = 1
Y, Z ve mesafe sisi için kullanılan d değerlerinin hepsi 1 olarak başlatılıyor
Şehir üretimi ve çarpışma tespiti
- İç döngü, raycasting sisteminin çekirdeği ve bir şeye çarpana kadar
Z'yi ileri taşıyor
for(; ++Z<w &
Z, w, yani 99'dan küçük kaldığı sürece artırılıyor
- Şehrin binaları, sokak araları ve deniz kıyısına bakan boş alanlar şu koşulla oluşturuluyor
Y < 6 - (32<Z & 27<X%w && X/9^Z/8)*8%46
- Işının o konumdaki yükseklikten daha aşağıda olup olmadığı kontrol edilerek çarpışma durumu belirleniyor
6 -, yükseklik sonucunu merkezin altına indiriyor ve zeminin altta olacak şekilde ters çevrilmesini sağlıyor
- Parantez içindeki koşul şehrin biçimini belirliyor
32<Z, kamera ile ilk bina sırası arasında boşluk bırakıyor
27<X%w, periyodik boşluklar oluşturarak şehir bloklarını yollar gibi ayırıyor
- Negatif değerlerde her zaman false olduğundan deniz benzeri boş bir bölge oluşuyor
X/9^Z/8, bit düzeyinde XOR ile bina yüksekliklerinde rastgeleymiş gibi görünen bir dağılım oluşturuyor
9 ve 8 bölenleri bina genişliği ve derinliğini ayarlıyor
X/9, yol genişliğiyle ilgili sayılarla birlikte 9'a tam bölünecek şekilde ayarlanarak kenarlarda çok ince binaların oluşmasını engelliyor
- Parantez içindeki sonuç
8 ile çarpılıp 46 ile mod alınarak azami yükseklik aralığı oluşturuluyor
8 ve 46, farklı bina yükseklikleri üretmek için deneysel olarak seçilmiş değerler
Gölge, doku ve mesafe sisi
- Çarpışma olduğunda aynı iç döngü ikinci bir rol üstleniyor ve ışık yönü kontrolü yapıyor
d | (s = (X&Y&Z)%3/Z, a = b = 1, d = Z/w)
d |, mevcut ışının kameradan çıkan ışın mı yoksa ışık yönüne giderek gölge kontrolü yapan ışın mı olduğunu ayırıyor
- Başlangıçta
d = 1 olduğu için kamera ışını gibi davranıyor
- Çarpışmadan sonra
d = Z/w oluyor; bu değer 1'den küçük hale gelince bit OR değerlendirmesi değişiyor, döngü yeniden çalışıyor ve gölge kontrolü başlıyor
- Gölge kontrolü sırasında yeniden çarpışma olursa döngüden çıkılıyor ve o piksel gölge olarak çiziliyor
- Doku değeri
s şu ifadeyle üretiliyor
s = (X&Y&Z)%3/Z
X, Y, Z üzerinde bit AND uygulanıp 3 ile bölümünden kalan kullanılarak pencereye benzeyen gri bir doku oluşturuluyor
- Sonra
Z'ye yeniden bölünerek uzaktaki dokuların soluk görünmesi sağlanıyor
- Işını güneş benzeri yönlü bir ışık kaynağına göndermek için
a ve b her ikisi de 1 yapılıyor
d = Z/w, mesafe sisi değeri ve uzaktaki binaları aydınlatmak için kullanılıyor
Piksel çizimi ve parlaklık ifadesi
- Her piksel
fillRect ile çiziliyor
c.getContext`2d`.fillRect(i%w, i/w|0, 1 - d*Z/w + s, 1)
i%w x koordinatını, i/w|0 ise y koordinatını üretiyor
- Parlaklık, piksel genişliğini küçültme yöntemiyle ifade ediliyor; bu, küçük kodla gri tonlamalı bir görüntü üretmenin temel tekniği
1, siyah piksele karşılık geldiğinden son ifade 1 değerinden çıkarma yaparak görüntü parlaklığını oluşturuyor
d * Z/w, gölge ve mesafe etkisini birleştiriyor
- Gölge yoksa ışın azami mesafe
w'ye kadar ilerlediğinden Z/w değeri 1 oluyor
- Gölgenin içinde
Z, w'den küçük kaldığı için daha koyu görünüyor
- Işığı engelleyen nesne ne kadar yakınsa gölge o kadar koyu oluyor ve ambient occlusion benzeri bir etki bırakıyor
- Son olarak
s eklenerek bina dokusu nihai parlaklığa karıştırılıyor
Sonraki demolar ve deney araçları
- Revision 2022 demo party'ye gönderildi ve Pouet üzerinde görülebiliyor
- O sırada yanlış kategoriye gönderildiği için yüksek sıralara çıkamadı
- Daha sonra Shadertoy üzerinde Xor ve diğer kod yazarları JavaScript sürümünü yeniden üreten 256 baytlık shader'lar yaptı
- Daniel Darabos, programın çeşitli öğelerini gerçek zamanlı olarak değiştirmenizi sağlayan bir Observable aracı hazırladı
- Kod, Dwitter üzerinde remixlenebilir ve CapJS içinde de denenebilir
1 yorum
Hacker News yorumları
Erken dönem 8 bit oyun Elite'ın prosedürel üretimi: https://procedural-generation.tumblr.com/post/112509130817/e...
Prosedürel üretim hakkında genel bir belge: https://en.wikipedia.org/wiki/Procedural_generation
Biraz ilişkili bir kavram olarak fonksiyonel programlama dillerindeki tembel değerlendirme de var: https://en.wikipedia.org/wiki/Lazy_evaluation
Eğer bir ışın izleme algoritması 3D veriden 2D görüntü üretirken yalnızca görünen noktaları değerlendiriyorsa, bu “değer gerekene kadar ifadenin değerlendirilmesini erteleme” anlamındaki tembel değerlendirme kavramına benziyor: https://en.wikipedia.org/wiki/Ray_tracing_(graphics)
Hatta görünüşte kaba bir şekilde “gözlem dalga fonksiyonunu çökertir” fikriyle de bağlantılı hissettiriyor: https://en.wikipedia.org/wiki/Wave_function_collapse
Yine de bunun JavaScript ile yapılmış olması daha etkileyici; çünkü JavaScript'in kod yoğunluğu genelde 16 bit x86 makine kodundan daha düşüktür
Kaynak kodu da içeriyor
http://www.youtube.com/watch?v=36BPql6Nl_U
Adı, temel aldığı Menger sponge fraktalinden geliyor: https://en.wikipedia.org/wiki/Menger_sponge
Burada JavaScript'in büyük avantajı, standart dweet kütüphanesinde sin/cos/fill/line gibi işlevlerin bulunması. Buna karşılık x86 BIOS en fazla “mod değiştirme” yapıyor; ondan sonra doğrudan piksel dizisini elle yönetmeniz gerekiyor
Bu da büyük dil modellerinin dünyayı modellemenin en iyi yolu olmayabileceğini düşündürüyor
Ben olsam bunu 256 satırda yapabilsem bile memnun olurdum; gerçekte ise muhtemelen çok daha fazlası gerekirdi
Görsel şeyler, ister yapay ister doğal olsun, çoğu durumda örüntüleri takip eder. Aksi halde zaten tanınmaları zor olurdu. Bu yüzden matematiksel olarak yararlanılabilecek çok şey var. Yükseklik dokularında çeşitli gürültü türleri veya örüntüler kullanılabilir, fraktaller ilginç biçimde değerlendirilebilir; bitki örtüsü ve ağaçlarda Fibonacci/altın oran, iskelet öğeleri ile kemikler arasında da boyut oranları kullanılabilir. Bunun için sihirli bir tahmin makinesine gerek yok
Bunlar Pico-8 sanal konsolu için tweet boyutunda programlar