Uğur Yürümez

Prosedürel dünya üreteciProcedural world generator

Cartula

Tarayıcıda çalışan bir prosedürel dünya üreteci. Her seed kurallı adımlarda büyüyen yeni bir kıta; harita üstten ya da izometrik voksel olarak çizilir, fırçayla düzenlenir ve canlı kalır: nehirler akar, bulutlar geçer, sabahları vadilerde sis toplanır.

A procedural world generator that runs in the browser. Each seed grows a new continent in rule-based passes; the map is drawn top-down or as isometric voxels, can be edited with brushes, and stays alive: rivers flow, clouds drift, morning fog pools in the valleys.

Ürün değil, portfolyo parçası: üretim kurallarını, shader işini ve aracın kendisini tek projede göstermek için yaptım.

It is a portfolio piece, not a product: I built it to show generation rules, shader work and the tool itself in one project.

ZamanWhen
Eylül–Ekim 2026September–October 2026
TeknikBuilt with
WebGL2, GLSL, JavaScript
BağımlılıkDependencies
Yok, build adımı da yokNone, and no build step
Seed 1337, 320², izometrik görünüm: volkan ve dumanı, yarı saydam yağmur bulutları, voksel bulutlar ve gölgeleri.Seed 1337 at 320², isometric view: a volcano and its smoke, translucent rain clouds, voxel clouds and their shadows.

NedenWhy

Üç şey istedim: kurallarla büyüyen bir dünya, üretimden sonra elle düzeltebilmek ve aynı haritayı hem üstten hem izometrik görebilmek.

I wanted three things: a world that grows by rules, a way to fix it by hand after generation, and the same map seen both from above and in isometric view.

Kısıt: framework yok, build adımı yok. index.html çift tıklanınca açılıyor; modüller bu yüzden ES modülü değil, sırayla yüklenen düz script'ler.

The constraint: no framework, no build step. Double-click index.html and it opens, which is why the modules are plain scripts loaded in order rather than ES modules.

Tek ızgara, iki görünümOne grid, two views

Her hücre yükselti, nem, sıcaklık, biyom, su ve kademe taşır. Üstten görünüm de izometrik görünüm de bu aynı ızgaranın izdüşümü; ayrı iki harita yok, o yüzden iki görünüm her zaman aynı yeri gösterir.

Each cell holds elevation, moisture, temperature, biome, water and level. The top-down and the isometric views are both projections of this one grid, not two maps, so they always show the same place.

Üretim hattıThe generation pipeline

Üretim on adlandırılmış aşamadan geçer, her aşama ayrı bir kural:

Generation runs through ten named stages, each one a separate rule:

  1. Dünya uzayında noise: fBm ve domain warp dünya koordinatında örneklenir; harita büyüyünce aynı dünyanın daha fazlası açılır, harita gerilmez.
  2. Dağlar ve onarım: fay hatları boyunca sıradağlar ve platolar; tek tile'lık diken ve çukurlar kırpılır, kule kalmaz.
  3. Deniz seviyesi: sabit bir referanstan mutlak eşik; harita büyüyünce kıyı yerinden oynamaz.
  4. İklim ve biyom: yağmur gölgeli nem, enlem ve rakımdan sıcaklık; her tile 18 biyomdan birine girer.
  5. Kıyı temizliği: benekler silinir, kapalı su göl olur, küçük adalar birleşir.
  6. Fiyortlar, kum okları, volkanlar: soğuk ve dik koylar, lagünlü kum okları, lav akıntılı volkan konileri.
  7. Nehirler: zirvelerden yokuş aşağı, vadi oyarak; ağızda delta ya da haliç.
  8. Kıyı yeşili ve kurallar: su kenarında yeşil şerit; çıkışa ulaşmayan nehir parçaları silinir.
  9. Yatak derecelendirme: 2 kademelik basamaklar oyulur, 3 ve fazlası şelale olur (aşağıda).
  10. Voksel: ayrık kademeler ve şelale etiketleri; iki görünümün okuduğu ızgara.
  1. World-space noise: fBm and domain warp are sampled in world coordinates, so a bigger map opens up more of the same world instead of stretching it.
  2. Mountains and repair: ranges along fault lines and plateaus; single-tile spikes and pits are clamped, so no towers are left.
  3. Sea level: an absolute threshold from a fixed reference, so the coast stays put when the map grows.
  4. Climate and biomes: moisture with rain shadows, temperature from latitude and altitude; every tile lands in one of 18 biomes.
  5. Coast cleanup: specks are removed, enclosed water becomes a lake, small islands merge.
  6. Fjords, spits, volcanoes: steep cold inlets, sand spits with lagoons, volcanic cones with lava flows.
  7. Rivers: downhill from the peaks, carving valleys, ending in a delta or an estuary.
  8. Riparian and invariants: a green strip along the water; river pieces that never reach an outlet are removed.
  9. River grading: 2-level steps are carved away, drops of 3 or more become waterfalls (below).
  10. Voxelize: discrete levels and waterfall tags: the grid both views read.
1. Ham noise1. Raw noise
3. Deniz seviyesi3. Sea level
4. İklim ve biyom4. Climate and biomes
7. Nehirler7. Rivers
10. Son harita10. Final map

Araçta bir adım adım modu var: mevcut harita aynı ayarlarla bir kez kayıtla yeniden üretilir ve her aşamanın kopyası bir kaydırıcıya dizilir. Kayıt haritayı değiştirmez; bunu bir test denetliyor. Yukarıdaki görseller bu moddan, seed 1337, 192².

The tool has a step-through mode: the current map is generated once more with the same settings and a recorder, and a copy of every stage is lined up on a slider. Recording does not change the map, and a test checks that. The images above come from this mode, seed 1337 at 192².

Voksel görünümThe voxel view

İlk izometrik görünüm 2D canvas'ta, dört yöne önceden çizilmiş görüntülerdi. Yerine ham WebGL2 ile yazılmış bir voksel renderer geldi: kendi shader'ları, 360° orbit kamera, vertex başına ambient occlusion ve güneş gölgesi. Eski canvas yolu bütün yardımcılarıyla silindi, yaklaşık 830 satır.

The first isometric view was a 2D canvas with images pre-drawn for four directions. It was replaced by a voxel renderer written on raw WebGL2: its own shaders, a 360° orbit camera, per-vertex ambient occlusion and sun shadows. The old canvas path was deleted with all its helpers, about 830 lines.

Eski yolu tutmak için bir sebep kalmamıştı: iki ayrı bulut sistemi taşıyordum ve eskisinin tek bir testi yoktu, bozulsa fark etmeyecektim.

There was no reason left to keep the old path: I was carrying two separate cloud systems, and the old one had no tests at all, so I would not have noticed if it broke.

Bedeli: WebGL2 yoksa izometrik görünüm de yok. Sessizce üstten görünüme düşmek yerine sekme devre dışı kalır ve sebebini yazar.

The cost: without WebGL2 there is no isometric view. Instead of silently falling back to top-down, the tab is disabled and says why.

Canlı haritaA living map

Arazi çizildikten sonra harita durmaz. Her katmanın kendi testi var:

The map keeps moving after the terrain is drawn. Each layer has its own tests:

  • Gökyüzü: dört tipte voksel bulut (düz levha, kümülüs, top, küçük küme), şekli bulutla aynı gölgeler ve kanat çırpan kuşlar. Kuşların JS tarafı yok; yörünge ve kanat çırpma vertex shader'da.
  • Su: dalgalar her kıyıya doğru halkalanıp söner, kıyıda bloklu bir köpük çizgisi var; nehirler ve basamak havuzları yokuş aşağı akar.
  • Hava: yağmur ya da kar, altındaki araziye göre kendi yarı saydam bulutlarından yağar; yağmur bulutları arada şimşek çakar, dakikada en çok beş kez.
  • Rüzgâr: vadileri izleyen ince rüzgâr çizgileri; volkan dumanı aynı rüzgâr alanıyla bükülür.
  • Saat ve mevsim: saat ayarı ışığı ve gölgeyi değiştirir, sabahları vadilerde sis toplanır, gece yalnız lav ışır. Mevsim ayarı sonbaharda ormanları sarartır, kışın kar çizgisini indirir ve soğuk suları dondurur.
  • Sky: voxel clouds in four types (flat sheets, cumulus, round puffs, small clusters), shadows with the cloud's own shape, and birds flapping their wings. The birds have no JavaScript side; orbit and wingbeat live in the vertex shader.
  • Water: waves ring in toward every coast and settle, with a blocky foam line at the shore; rivers and their stepped pools flow downhill.
  • Weather: rain or snow, chosen by the ground below, falls from its own translucent clouds; rain clouds strike lightning now and then, at most five times a minute.
  • Wind: thin wind lines that follow the valleys; volcano smoke bends in the same wind field.
  • Time and season: the time of day changes light and shadow, fog pools in the valleys in the morning, and at night only lava glows. The season control turns forests gold in autumn, brings the snow line down in winter and freezes cold water.
Gökyüzü: bulutlar ve gölgeleri birlikte kayar, yağmur bulutları daha yavaş geçer; kuşlar, rüzgâr çizgileri ve volkan dumanı. Uygulamadan kaydedildi, 8 saniyelik döngü.The sky: clouds and their shadows drift together, rain clouds pass more slowly; birds, wind lines and volcano smoke. Recorded from the app, an 8-second loop.

Bulut gövdesi ve gölgesi ayrı çiziliyor, ama kayma JS'te bir kez hesaplanıp ikisine veriliyor. Ayrı hesaplansalar zamanla ayrışırlardı ve gölge kendi bulutunun altından kayardı; tek bir ekran görüntüsünde görünmeyen, yalnız hareket hâlinde fark edilen bir hata. Gölge önce arazi shader'ında her bulut parçası için tek tek hesaplanıyordu; bulut tipleri gelince bu, yazılımsal rasterizer'da kare başına 25–30 ms tuttu. Şimdi JS gölgeyi yarım çözünürlüklü bir dokuya çiziyor, shader onu tek okumayla alıyor.

Cloud bodies and their shadows are drawn separately, but the drift is computed once in JavaScript and handed to both. Computed apart, they would slowly diverge and a shadow would slide out from under its cloud: a bug no single screenshot shows, only motion. The shadow used to be evaluated in the terrain shader for every cloud part; once the cloud types arrived, that cost 25–30 ms per frame on a software rasterizer. Now JavaScript draws the shadow into a half-resolution texture and the shader reads it once.

Nehirler ve şelalelerRivers and waterfalls

Voksel dünyada nehir, kara gibi kendi yüksekliğinde duran bir su sütunu. Ham arazide komşu nehir tile'ları arasında 2 kademelik basamaklar vardı (ölçüldüğünde seed başına 13–19 tane) ve hiçbir renderer bunları çizmiyordu: nehir bir toprak basamağından yürüyerek iniyordu. Karar ikiye ayrıldı: 2 kademe bir kusur, yatak oyulur; 3 ve fazlası tasarlanmış bir şelale, çizilir.

In a voxel world a river is a column of water standing at its own height, like land. The raw terrain had 2-level steps between neighbouring river tiles (13–19 per seed when measured) and no renderer drew them: the river walked down a dirt step. The decision split in two: a 2-level step is a defect and the bed is carved; a drop of 3 or more is a designed waterfall and gets drawn.

  • Derecelendirme: nehre değen her su-su kenarında farkı en çok 1 olan en alçak seviyeler aranır. Yatak yalnız aşağı oyulur; göl ve deniz sabit kalır.
  • Şelale geometriden: akış yönü yalnız nehirlerin orta hattında vardı, nehir tile'larının yaklaşık %75'inde yoktu. Bu yüzden şelale son kademelerden bulunuyor: dudak ve iniş tile'ı etiketlenir, düşen su yüzü shader'da aşağı akan şeritlerle çizilir.
  • Sonuç: 2 kademelik nehir basamakları seed başına 13–19'dan 0'a indi. Bugün bir test her nehir kenarını tek tek denetliyor (5 haritada 938 kenar); tek istisna, sayısı yazdırılan göl taşma eşikleri: 7 kenar.
  • Akış: nehirler, basamak havuzları ve şelale dipleri yokuş aşağı akar. Yön geometriden, tek seviyedeki her tatlı su gövdesinin çıkışına olan uzaklıktan bulunur; büyük bir gölün ortası durgun kalır. Desen tam bloklara yuvarlanır, rüzgâr çizgileri gibi kare kare kayar.
  • Grading: on every water-water edge that touches a river, find the lowest levels that differ by at most 1. The bed is only ever carved down; lakes and the sea stay fixed.
  • Waterfalls from geometry: flow direction existed only on river centrelines and was missing on about 75% of river tiles. So waterfalls are found from the final levels: the lip and landing tiles are tagged, and the falling face is drawn in the shader with streaks sliding down.
  • Result: 2-level river steps went from 13–19 per seed to 0. A test now checks every river edge one by one (938 edges on 5 maps); the one exception, with its count printed, is lake outflow sills: 7 edges.
  • Flow: rivers, stepped pools and the pools under waterfalls flow downhill. Direction comes from geometry, from each one-level body of fresh water's distance to its outlet, and the middle of a large lake stays still. The pattern snaps to whole blocks and moves frame by frame, like the wind lines.
Bir şelale: dudak, düşen su yüzü ve iniş havuzu; üstteki basamak havuzları da akıyor, yukarıdan bir yağmur bulutu geçiyor. Uygulamadan kaydedildi, 6 saniyelik döngü.A waterfall: the lip, the falling face and the plunge pool; the stepped pools above flow too, and a rain cloud passes overhead. Recorded from the app, a 6-second loop.

Derinlik renkleDepth in colour

Deniz eskiden kıyıdan uzaklaştıkça aşağı kademelenen bir basendi ve voksel görünümde haritada delik gibi duruyordu. Şimdi deniz tek düz yüzey, derinliği renk taşıyor: deniz tabanının gerçek derinliğinden üç bant, paletin zaten var olan sığ ve derin mavisi ile ikisinin ortası. Referans harita başına değil mutlak, yani aynı derinlik her seed'de ve her boyutta aynı tonu alıyor. Üstten görünüm, voksel görünüm ve editörün canlı boyaması aynı fonksiyonu çağırıyor.

The sea used to be a basin that stepped down away from the coast, and in the voxel view it read as holes in the map. Now the sea is one flat surface and colour carries the depth: three bands from the real depth of the sea floor, the palette's existing shallow and deep blues and the tone between them. The reference is absolute, not per map, so the same depth gets the same tone on every seed and every size. The top-down view, the voxel view and the editor's live painting all call the same function.

FırçalarBrushes

Üretimden sonra, üstten görünümde yedi fırça: yükselt, alçalt, yumuşat, su, kara, nehir çiz ve biyom boya. Nehir fırçası bir kanal açar ve yatağı kıyıların altına oyar: düz mavi bir şerit boya gibi duruyor, oyulmuş kanal voksel görünümde nehir gibi okunuyor. Nehir lavı söndürdüğü için geri alma kaydı lav alanını da tutuyor; tutmasaydı geri alma lavı geri getiremezdi.

After generation, seven brushes in the top-down view: raise, lower, smooth, water, land, draw river and paint biome. The river brush cuts a channel and carves the bed below its banks: a flat blue strip reads as paint, a carved channel reads as a river in the voxel view. Because a river puts out lava, the undo record keeps the lava field too; without it, undo could not bring the lava back.

Deniz ve kara yalnız fırçanın yönünde eşik geçilince yer değiştirir. Üreteç deniz eşiğinin altında kumsal ve delta gibi kara bırakıyor; değişim eşikten doğrudan türetildiğinde hafif bir yükseltme kumsalı denize çeviriyor, yükseklik fırçaları nehir ve gölleri kurutuyordu.

Sea and land only swap when the brush crosses the threshold in its own direction. The generator leaves land such as beaches and deltas below the sea threshold; when the swap was derived straight from the threshold, a light raise turned a beach into sea and the height brushes dried up rivers and lakes.

ÖğrenilenlerWhat I learned

  • Palette sadık kalmak. Daha canlı bir palet denedim, beğenmedim ve eski, daha doğal paleti geri aldım. Sonrasında yeni ton eklemek yerine paletteki renkleri karıştırdım: deniz derinliği altı banttan paletin üç mavisine indi, yağmur bulutu ve duman da paletin kar ve kaya tonlarından.
  • Önce ölçüm aracı. Yeni görsel katmanlardan önce bir performans paneli geldi (P tuşu): kare süresi, GPU süresi, çizim çağrısı, üçgen ve GPU belleği. Renderer'daki her çizim ve bellek ayırma bir sayaçtan geçiyor; sayaçsız bir çağrı testi kırmızıya çeviriyor. Bir keresinde gündüz karesi ~20 ms yavaşladı; sebep hiç çalışmayan gece koduydu, çünkü headless sınamadaki yazılımsal rasterizer (SwiftShader) dallanmanın iki tarafını da çalıştırıyor. Çözüm derlenmiş shader varyantları oldu.
  • Önbellek iki kez saatler yedi. index.html?cb=N yalnız HTML'i tazeliyor; src/*.js ayrı adresler ve tarayıcı onları saklıyor, yani eski kod çalışıyordu. Kalıcı çözüm no-store gönderen küçük bir geliştirme sunucusu.
  • Sessiz shader link hataları. İki shader'da farklı varsayılan hassasiyetle tanımlanan bir uniform, WebGL2 programını link etmiyor: hata mesajı yok, katman yalnızca görünmüyor. İki kez oldu; artık bir test shader'ların ortak uniform bildirimlerini karşılaştırıyor.
  • "Export PNG" çoğu kullanıcı için hiçbir şey yapmıyordu. İzometrik görünümde uyarı yazıp dönüyordu ve izometrik varsayılan görünümdü. Voksel renderer'a gerçek bir capture() eklendi: aynı karede çizip readPixels ile okuyor.
  • Yeşil test kanıt değil. "Nehirler tırmanmaz" testi bir hafta boyunca geçti, çünkü bir hata bütün tatlı suyu deniz seviyesine yapıştırıyordu ve test hiç gerçek veri görmedi. Düzeltilince test %10 tolerans ve bir muafiyet listesiyle geri geldi; bir hafta sonra ikisi de kalktı, şimdi her nehir kenarı tek tek denetleniyor.
  • Stay with the palette. I tried a brighter palette, did not like it, and went back to the older, more natural one. After that I mixed colours from the palette instead of adding new ones: sea depth went from six bands to the palette's three blues, and the rain clouds and smoke come from its snow and rock tones.
  • The measuring tool first. A performance panel came before the new visual layers (press P): frame time, GPU time, draw calls, triangles and GPU memory. Every draw and allocation in the renderer goes through a counter, and an uncounted call turns a test red. Once the day frame got ~20 ms slower, and the cause was night code that never ran: the software rasterizer used in headless tests (SwiftShader) executes both sides of a branch. Compiled shader variants fixed it.
  • The cache ate hours, twice. index.html?cb=N only refreshes the HTML; src/*.js are separate URLs that the browser keeps, so old code was running. The lasting fix is a small dev server that sends no-store.
  • Silent shader link errors. A uniform declared in two shaders with different default precision stops a WebGL2 program from linking: no error message, the layer simply does not appear. It happened twice; now a test compares the shared uniform declarations.
  • "Export PNG" did nothing for most users. In the isometric view it logged a warning and returned, and isometric was the default view. The voxel renderer got a real capture() that draws and reads back with readPixels in the same frame.
  • A green test is not proof. The "rivers never climb" test passed for a week because a bug pinned all fresh water to sea level, so the test never saw real data. After the fix it came back with a 10% tolerance and an exemption list; a week later both were gone, and now every river edge is checked one by one.

SonuçWhere it stands

Dört milestone bitti; sonrasında harita canlandı: akan nehirler, duman, sis, şimşek ve mevsimler geldi. Bunları headless tarayıcıda ölçerek sınadım; gerçek bir telefonda, Firefox'ta ve Safari'de henüz denemedim. Unity'ye dışa aktarma yazıldı ama motorda hiç denenmedi; şimdilik kapalı.

The four milestones are done, and after them the map came alive: flowing rivers, smoke, fog, lightning and seasons. I tested these in a headless browser with measurements; I have not tried them on a real phone, in Firefox or in Safari yet. An export to Unity is written but was never tried in the engine; it is off for now.

KodCode
~12.600 satır JavaScript, ~3.600 satır test~12,600 lines of JavaScript, ~3,600 lines of tests
Otomatik kontrolAutomated checks
21 harness içinde 285 kontrol285 checks in 21 harnesses
Üretim süresiGeneration time
192²: ~95 ms, 320²: ~175 ms, 448²: ~280 ms
Voksel mesh, 320²Voxel mesh, 320²
~329 bin üçgen, ~165 ms~329k triangles, ~165 ms
BağımlılıkDependencies
Hâlâ sıfırStill none

Ölçüm 2026-10-09, Cartula 7af1bc0: Node 26, Apple M5 Pro, seed 1337, 4242 ve 90210 ortalaması. Satırlar src/ ve test dosyası tools/headless.js.

Measured 2026-10-09 on Cartula 7af1bc0: Node 26, Apple M5 Pro, averaged over seeds 1337, 4242 and 90210. Lines are src/ and the test file tools/headless.js.