3 पॉइंट द्वारा GN⁺ 2024-05-21 | 1 टिप्पणियां | WhatsApp पर शेयर करें
  • City In A Bottle एक JavaScript demo है, जिसमें 256-बाइट HTML के अंदर raycasting engine और city generator समाया हुआ है
  • Execution केवल <canvas> और onclick=setInterval(...) से शुरू होता है, और 99-pixel चौड़े canvas पर हर frame में scene फिर से draw किया जाता है
  • Rendering बिना trigonometric functions के, basic algebra और bit operations से की जाती है; हर pixel के लिए camera ray और light-source direction ray को trace करके shadows बनाई जाती हैं
  • शहर का आकार X, Y, Z coordinates और X/9^Z/8, X%99, Z>32 जैसी conditions से building heights, roads और empty spaces में बांटा जाता है
  • Final brightness में distance fog, shadows और (X&Y&Z)%3/Z texture को fillRect की width में मिलाकर दिखाया जाता है; बेहद छोटे code में city, perspective और texture—तीनों implement होते हैं

256-बाइट HTML के अंदर पूरी संरचना

  • Demo कोई JavaScript snippet नहीं, बल्कि एक valid HTML program के रूप में काम करता है
<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)>
  • पूरा हिस्सा HTML code, frame update loop, rendering system, raycasting engine और city generation logic में बंटा है
  • Trigonometric functions या advanced math के बजाय सिर्फ basic algebra और bit operations का इस्तेमाल किया गया है
  • मूल रूप से public होने के बाद करीब 2 साल तक यह लेखक की Twitter timeline पर सबसे लोकप्रिय posts में से एक था

HTML और execution loop

  • HTML सिर्फ एक canvas और click event से बना है
<canvas style=width:99% id=c onclick=setInterval('',t=9)>
  • canvas का id c रखा गया है, ताकि JavaScript से इसे छोटे नाम से access किया जा सके
  • style=width:99% अनिवार्य नहीं है; इसके बिना भी काम करता है
  • onclick में setInterval call करके update loop शुरू किया जाता है
    • Interval 9 milliseconds पर set है
    • Space बचाने के लिए time variable t को भी यहीं 9 से initialize किया जाता है
  • Canvas को कई बार click करने पर कई intervals चलने लगते हैं और demo धीमा हो जाता है—यह एक छोटा bug है

पढ़ने में आसान रूप में JavaScript

  • Click के बाद चलने वाला JavaScript payload 199 bytes का है
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
  • Spaces और line breaks जोड़ने पर इसे इस flow में पढ़ा जा सकता है
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 को clear करता है, width को 99 pixels पर set करता है, और उसी value को बार-बार इस्तेमाल के लिए w में store करता है
  • Default canvas height 150 है, और उसके नीचे का area खाली रहता है
  • ++t हर frame में time value बढ़ाता है, जिससे scene animate होता है
  • for (i = 6e3; i--;) बाहरी loop है, जो हर pixel की brightness तय करता है

Camera ray calculation

  • हर pixel को camera से निकलने वाली एक ray के रूप में process किया जाता है
  • Horizontal component a में store होता है
a = i % w / 50 - 1
  • i % w से current pixel की horizontal position मिलती है; उसे 50 से divide करके और 1 घटाकर लगभग -1 से 1 के बीच normalize किया जाता है
  • Vertical component b में store होता है, और वही value background fade के लिए s में भी जाती है
b = s = 1 - i / 4e3
  • Exact vertical ratio calculation के बजाय code size कम करने के लिए i / 4e3 से simplification किया गया है
  • यह simplification मुश्किल से दिखने वाला tilt बनाता है, लेकिन bytes बचाता है
  • 4e3 वह value है जिसे horizon को center से थोड़ा नीचे shift करने के लिए चुना गया है
  • Camera start position time value का इस्तेमाल करती है, जिससे scene दाईं ओर move करता दिखता है
X = t
Y = Z = d = 1
  • Y, Z और distance fog में इस्तेमाल होने वाला d—तीनों 1 से initialize होते हैं

City generation और collision detection

  • Inner loop raycasting system का core है, और किसी चीज़ से टकराने तक Z को आगे बढ़ाता है
for(; ++Z<w &
  • Z, w यानी 99 से कम रहने तक बढ़ता है
  • शहर की buildings, गलियां और seaside की empty space नीचे दी गई condition से बनती हैं
Y < 6 - (32<Z & 27<X%w && X/9^Z/8)*8%46
  • Ray उस position की height से नीचे है या नहीं, यह check करके collision तय किया जाता है
  • 6 - height result को center से नीचे लाता है और ground को नीचे की ओर रखने के लिए invert करता है
  • Brackets के अंदर की condition शहर का form तय करती है
    • 32<Z camera और पहली building row के बीच space छोड़ता है
    • 27<X%w periodic empty spaces बनाता है, जिससे city blocks roads की तरह बंटते हैं
    • Negative values में यह हमेशा false होता है, जिससे समुद्र जैसा खाली area बनता है
    • X/9^Z/8 bit XOR से building height में random जैसी distribution बनाता है
    • 9 और 8 से divide करने वाली values building width और depth control करती हैं
    • X/9 road-width से जुड़े numbers के साथ 9 से divisible है, जिससे edges पर बेहद पतली buildings बनने से बचती हैं
  • Bracket result को 8 से multiply करके और 46 से modulo लेकर maximum height range बनाई जाती है
  • 8 और 46 अलग-अलग building heights बनाने के लिए experimentally चुनी गई values हैं

Shadows, texture और distance fog

  • Collision होने पर वही inner loop दूसरा role निभाता है और light direction check करता है
d | (s = (X&Y&Z)%3/Z, a = b = 1, d = Z/w)
  • d | यह distinguish करता है कि current ray camera से निकलने वाली ray है या light direction में shadow check करने वाली ray
  • शुरुआत में d = 1 होता है, इसलिए यह camera ray की तरह काम करता है
  • Collision के बाद d = Z/w 1 से छोटा value बन जाता है, और bit OR evaluation का result बदलता है; loop फिर से चलकर shadow check शुरू करता है
  • Shadow check के दौरान फिर से collision होने पर loop से बाहर निकलकर उस pixel को shadow के रूप में draw किया जाता है
  • Texture value s नीचे दिए expression से generate होती है
s = (X&Y&Z)%3/Z
  • X, Y, Z पर bit AND apply करके और 3 से division का remainder इस्तेमाल करके window जैसी grey texture बनाई जाती है
  • फिर Z से divide किया जाता है, ताकि दूर की textures faint दिखें
  • Ray को sun जैसी directional light की ओर भेजने के लिए a और b, दोनों को 1 पर set किया जाता है
  • d = Z/w distance fog value है, जिसका इस्तेमाल दूर की buildings को bright बनाने में होता है

Pixel drawing और brightness representation

  • हर pixel fillRect से draw होता है
c.getContext`2d`.fillRect(i%w, i/w|0, 1 - d*Z/w + s, 1)
  • i%w x-coordinate बनाता है, और i/w|0 y-coordinate
  • Brightness pixel की width घटाकर represent की जाती है; छोटे code में grayscale image बनाने की यह key technique है
  • 1 black pixel के बराबर है, इसलिए final expression 1 से value घटाकर image brightness बनाता है
  • d * Z/w shadows और distance effect को combine करता है
    • अगर shadow नहीं है, तो ray maximum distance w तक जाती है, इसलिए Z/w 1 होता है
    • Shadow में Z, w से छोटा हो जाता है, जिससे pixel ज्यादा dark दिखता है
    • Light को block करने वाली object जितनी करीब होती है, shadow उतनी ज्यादा dark होती है, और ambient occlusion जैसा effect बचता है
  • आखिर में s जोड़कर building texture को final brightness में मिलाया जाता है

बाद के demos और experimentation tools

  • इसे Revision 2022 demo party में submit किया गया था और Pouet पर देखा जा सकता है
  • उस समय इसे गलत category में submit किया गया था, इसलिए यह high rank हासिल नहीं कर पाया
  • बाद में Shadertoy पर Xor और अन्य coders ने JavaScript version को reproduce करने वाला 256-byte shader बनाया
  • Daniel Darabos ने program के कई elements को real time में manipulate करने वाला Observable tool बनाया
  • Code को Dwitter पर remix किया जा सकता है, और CapJS में भी experiment किया जा सकता है

1 टिप्पणियां

 
GN⁺ 2024-05-21
Hacker News की टिप्पणियाँ
  • संबंधित सामग्री में Atari 2600 के लिए Pitfall की world निर्माण पद्धति शामिल है: https://evoniuk.github.io/posts/pitfall.html
    शुरुआती 8-bit गेम Elite में procedural generation: https://procedural-generation.tumblr.com/post/112509130817/e...
    procedural generation पर सामान्य लेख: https://en.wikipedia.org/wiki/Procedural_generation
    थोड़ा संबंधित concept के तौर पर functional programming languages में lazy evaluation भी है: https://en.wikipedia.org/wiki/Lazy_evaluation
    अगर कोई ray tracing algorithm 3D data से 2D image बनाते समय केवल दिखने वाले points को evaluate करता है, तो यह lazy evaluation की उस धारणा जैसा लगता है जिसमें “किसी expression का evaluation तब तक टाला जाता है जब तक value की जरूरत न हो”: https://en.wikipedia.org/wiki/Ray_tracing_(graphics)
    आगे बढ़कर, सतही तौर पर यह “observation wave function को collapse कर देता है” से भी मोटे तौर पर जुड़ता हुआ लगता है: https://en.wikipedia.org/wiki/Wave_function_collapse
  • उसी लेखक का 1K Pinball Game in JavaScript भी है: https://frankforce.com/lu1ky-pinball-code-deep-dive/
  • Alcatraz का Remnants भी मिलता-जुलता 256-byte MS-DOS demo है, और इसमें YouTube link शामिल है: https://www.pouet.net/prod.php
    हालांकि यह काम JavaScript में बनाया गया है, यह बात ज्यादा प्रभावशाली है। क्योंकि JavaScript की code density आम तौर पर 16-bit x86 machine code से खराब होती है
    • सही link शायद यह है: https://www.pouet.net/prod.php?which=96536
      source code भी शामिल है
    • Spongy इसी तरह का काम 128 bytes, यानी आधे size में कर देता है: https://www.pouet.net/prod.php?which=53871
      http://www.youtube.com/watch?v=36BPql6Nl_U
      नाम उस Menger sponge fractal से आया है जिस पर यह आधारित है: https://en.wikipedia.org/wiki/Menger_sponge
    • जरूरी नहीं कि JavaScript की code density हमेशा खराब ही हो। variables को 1-byte नामों से लिखा जा सकता है, लेकिन x86 के load instructions आम तौर पर 2 bytes या उससे ज्यादा होते हैं, और अक्सर 3 bytes के होते हैं
      यहाँ JavaScript का बड़ा फायदा यह है कि standard dweet library में sin/cos/fill/line जैसी सुविधाएँ हैं। दूसरी ओर x86 BIOS बस “mode switch” जैसा काम करता है, और उसके बाद आपको pixel array को सीधे handle करना पड़ता है
  • वाकई कमाल है। इतने कम code में हमें महसूस होने वाली information density अविश्वसनीय रूप से ज्यादा है
    इसलिए लगता है कि शायद बड़े language models दुनिया को model करने का सबसे अच्छा तरीका न हों
    • ढेर सारे shortcuts से भरा obfuscated code भी शायद दुनिया को model करने का सबसे अच्छा तरीका नहीं है। फिर भी यह निश्चित रूप से प्रभावशाली उपलब्धि है
      अगर मैं इसे करीब 256 lines में भी बना पाऊँ तो संतुष्ट हो जाऊँगा, और असल में शायद उससे कहीं ज्यादा लगेगा
      visual चीजें, चाहे artificial हों या natural, अक्सर patterns follow करती हैं। नहीं तो उन्हें पहचानना ही मुश्किल होता। इसलिए mathematically इस्तेमाल करने लायक बहुत कुछ होता है। height textures में कई तरह के noise या patterns इस्तेमाल किए जा सकते हैं, fractals भी रोचक तरीके से काम आ सकते हैं, vegetation और trees में Fibonacci/golden ratio, skeletal elements और bones के बीच size ratios जैसी चीजें इस्तेमाल हो सकती हैं। इन सब के लिए किसी जादुई guessing machine की जरूरत नहीं है
    • आपको क्यों लगता है कि बड़े language models ही सबसे अच्छे होंगे?
  • बहुत शानदार तो है, लेकिन लेख पढ़ते समय loop लगातार चलता रहा और laptop overheat हो गया, यह थोड़ा खलता है
    • तो इसलिए पिछले कुछ मिनटों से मेरा phone असामान्य रूप से गर्म था!
  • अगर आपको ऐसी चीजें पसंद हैं, तो Twitter का #tweetcart भी पसंद आएगा
    ये Pico-8 virtual console के लिए tweet-size programs हैं