3 puan yazan GN⁺ 2024-05-21 | 1 yorum | WhatsApp'ta paylaş
  • 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

 
GN⁺ 2024-05-21
Hacker News yorumları
  • İlgili bir kaynak olarak Atari 2600 için Pitfall'ın dünya kurma yöntemi var: https://evoniuk.github.io/posts/pitfall.html
    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
  • Aynı yazarın 1K Pinball Game in JavaScript çalışması da var: https://frankforce.com/lu1ky-pinball-code-deep-dive/
  • Alcatraz'ın Remnants'ı da benzer bir 256 baytlık MS-DOS demosu ve içinde YouTube bağlantısı da var: https://www.pouet.net/prod.php
    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
  • Gerçekten inanılmaz. Bu kadar az kodda hissettiğimiz bilgi yoğunluğu akıl almaz derecede yüksek
    Bu da büyük dil modellerinin dünyayı modellemenin en iyi yolu olmayabileceğini düşündürüyor
    • Kısayollarla dolu obfuscation kodu da muhtemelen dünyayı modellemenin en iyi yolu değildir. Yine de bu kesinlikle etkileyici bir başarı
      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
    • Büyük dil modellerinin en iyi yöntem olduğunu neden düşünesin ki?
  • Çok havalı ama yazıyı okurken döngünün sürekli çalışması yüzünden dizüstü bilgisayarımın aşırı ısınması biraz can sıkıcı
    • Demek son birkaç dakikadır telefonumun alışılmadık derecede sıcak olmasının sebebi buydu!
  • Böyle şeyleri seviyorsanız Twitter'daki #tweetcart da hoşunuza gider
    Bunlar Pico-8 sanal konsolu için tweet boyutunda programlar