v0.3: Large world 4096x3072 + momentum waves + zoom-scaled lighting

- World expanded to 4096x3072 (16x area, tiled 4x4 copies)
- Active region margin increased to 200px
- Liquid momentum system (FLAG_MOMENTUM) for wave sloshing
- Zoom-at-cursor support (camera.zoomAt)
- Smoother zoom steps (0.2 per scroll tick)
- Light propagation scaled by zoom (passes & falloff)
- AGENTS.md updated with all current features
This commit is contained in:
2026-07-09 12:15:52 +02:00
parent 1bce9da0c6
commit 2c23216152
7 changed files with 267 additions and 54 deletions
+11 -8
View File
@@ -31,10 +31,10 @@ engine/ (Rust → WASM) web/ (TypeScript + Vite)
## Welt-Modell
- **Große Welt**: 1024×768 Pixel (4×3 Chunks à 256×256)
- Nur Zellen im Sichtfeld + 80px Margin werden simuliert
- **Große Welt**: 4096×3072 Pixel (16×12 Chunks à 256×256, tiled 4×4 Szenen)
- Nur Zellen im Sichtfeld + 200px Margin werden simuliert
- Rest eingefroren — keine CPU-Kosten für unsichtbare Bereiche
- Grid wächst auf 1024×768 = 786K Zellen (~3 MB Speicher)
- Grid wächst auf 4096×3072 = 12.5M Zellen (~48 MB Speicher)
### Datenstruktur Cell (pro Pixel)
@@ -93,6 +93,8 @@ Properties pro Material: `MaterialProps` in `engine/src/material.rs`:
- Wasser/Eis in Nachbarschaft → Feuer erlischt sofort
- Erzeugt Funken (14) und Rauch (15) beim Brennen
- **Flammen-Züngeln**: 1/5 Chance pro Zelle, Funken-Partikel nach oben zu feuern (412 Frames Lebensdauer)
- **Liquid-Momentum**: FLAG_MOMENTUM auf fallendem/sloshendem Wasser, waves klettern an Schalenwänden hoch
- **Flammen-Züngeln**: 1/5 Chance pro Zelle, Funken-Partikel nach oben zu feuern (412 Frames Lebensdauer)
**Ephemere Partikel** (Funken + Rauch):
- `update_ephemeral`: dekrementiert Health, bei 0 → Luft
@@ -105,16 +107,16 @@ Properties pro Material: `MaterialProps` in `engine/src/material.rs`:
Reihenfolge in `physics::update(cam_x, cam_y, rw, rh, zoom)`:
Nur Zellen innerhalb **Kamera-Sichtfeld + 80px Margin** werden simuliert.
Nur Zellen innerhalb **Kamera-Sichtfeld + 200px Margin** werden simuliert.
Rest der Welt ist eingefroren (keine CPU-Kosten).
1. **update_fire** — Feuer-Update: Brennstoff-Verbrauch, Ausbreitung, Funken/Rauch-Emission
1. **update_fire** — Feuer-Update: Brennstoff-Verbrauch, Ausbreitung, Funken/Rauch-Emission, Flammen-Züngeln
2. **Haupt-Loop** (bottom→top, alternierende Spalten, nur aktive Region):
- Heat-Transfer über Moore-Nachbarn
- Material-Reaktionen (Water+Lava, Säure+Holz, ...)
- Phasen-Transformation (Temp-basiert)
- Bewegung: Powders ↓, Liquids ↓↔, Solids ↓ (kein Gas!)
3. **update_gases** (top→bottom, nur jedes 2. Frame, nur aktive Region): Gas steigt 1px/Frame
3. **update_gases** (top→bottom, nur jedes 2. Frame, nur aktive Region): Gas + Flammen-Funken steigen max 1px/Frame
4. **update_ephemeral** (nur aktive Region): Spark/Smoke Health dekrementieren, bei 0 → Luft
## Level-Format
@@ -149,10 +151,11 @@ Jedes Material bekommt deterministische Pixel-Variation basierend auf Grid-Koord
**Light-Propagation** in `render_buffer.rs`:
1. Emissions-Pass: Jedes Pixel mit `light_emit > 0` strahlt Licht
2. Flood-Fill (max 12 Passes, early-termination): 8-Richtungs-Propagation
- Falloff: 2 (kardinal) / 3 (diagonal) + `light_block` (gecapped bei 200)
2. Flood-Fill (max `16/zoom` Passes, early-termination): 8-Richtungs-Propagation
- Falloff: `(2|3)/zoom` (kardinal/diagonal) + `light_block` (gecapped bei 200)
- Opaque Pixel blocken Weitergabe, werden aber selbst beleuchtet
3. Abwechselnde Scan-Richtung für gleichmäßige Verteilung
4. Zoom-skaliert: Passes + Falloff passen sich an Kamera-Zoom an
**Lichtquellen**: Feuer (255), Lava (200), Funken (180)
+1
View File
@@ -9,6 +9,7 @@ pub struct Grid {
pub const FLAG_UPDATED: u8 = 0b0000_0001;
pub const FLAG_PLAYER: u8 = 0b0000_0010;
pub const FLAG_MOMENTUM: u8 = 0b0000_0100;
impl Grid {
pub fn new(width: u32, height: u32) -> Self {
+80 -13
View File
@@ -8,26 +8,93 @@ use physics::Physics;
use render_buffer::RenderBuffer;
fn main() {
let width = 1024u32;
let height = 768u32;
let width = 4096u32;
let height = 3072u32;
let rw = 320u32;
let rh = 180u32;
let mut grid = Grid::new(width, height);
let mut physics = Physics::new();
let mut rb = RenderBuffer::new(rw, rh);
grid.fill_rect(440, 720, 144, 48, 1);
grid.fill_rect(460, 700, 104, 20, 2);
grid.fill_rect(470, 600, 84, 12, 5);
grid.fill_circle(490, 560, 20, 3);
grid.fill_rect(430, 650, 50, 30, 4);
grid.fill_rect(540, 650, 50, 30, 6);
grid.fill_rect(500, 640, 10, 18, 11);
grid.fill_rect(480, 630, 64, 24, 5);
grid.fill_rect(480, 618, 64, 12, 9);
for ty in 0..4 {
for tx in 0..4 {
let ox = tx * 1024;
let oy = ty * 768;
grid.fill_rect(ox, oy + 740, 1024, 28, 1);
grid.fill_rect(ox + 20, oy + 680, 80, 60, 1);
grid.fill_rect(ox + 30, oy + 640, 60, 40, 1);
grid.fill_rect(ox + 40, oy + 610, 40, 30, 1);
grid.fill_circle(ox + 60, oy + 600, 15, 2);
grid.fill_rect(ox + 50, oy + 660, 20, 20, 2);
grid.fill_rect(ox, oy + 720, 120, 20, 1);
grid.fill_rect(ox + 85, oy + 705, 35, 15, 1);
grid.fill_rect(ox + 5, oy + 705, 60, 15, 1);
grid.fill_circle(ox + 60, oy + 712, 12, 6);
grid.fill_circle(ox + 70, oy + 708, 6, 6);
grid.fill_rect(ox + 60, oy + 695, 20, 10, 2);
grid.fill_rect(ox + 350, oy + 680, 100, 60, 1);
grid.fill_rect(ox + 365, oy + 640, 70, 40, 1);
grid.fill_rect(ox + 380, oy + 610, 40, 30, 1);
grid.fill_rect(ox + 390, oy + 660, 20, 20, 10);
grid.fill_rect(ox + 370, oy + 700, 60, 40, 5);
grid.fill_circle(ox + 400, oy + 690, 8, 9);
grid.fill_rect(ox + 395, oy + 675, 10, 5, 9);
grid.fill_rect(ox + 750, oy + 640, 50, 100, 1);
grid.fill_rect(ox + 760, oy + 610, 30, 30, 1);
grid.fill_rect(ox + 770, oy + 590, 10, 20, 1);
grid.fill_circle(ox + 775, oy + 580, 8, 9);
grid.fill_rect(ox + 760, oy + 650, 30, 40, 5);
grid.fill_rect(ox + 180, oy + 420, 100, 20, 1);
grid.fill_rect(ox + 190, oy + 400, 80, 20, 2);
grid.fill_circle(ox + 230, oy + 390, 15, 3);
grid.fill_rect(ox + 200, oy + 380, 30, 20, 5);
grid.fill_circle(ox + 215, oy + 385, 6, 9);
grid.fill_rect(ox + 240, oy + 410, 10, 10, 10);
grid.fill_rect(ox + 620, oy + 350, 80, 18, 1);
grid.fill_rect(ox + 630, oy + 335, 60, 15, 2);
grid.fill_circle(ox + 660, oy + 325, 12, 3);
grid.fill_circle(ox + 640, oy + 340, 8, 11);
grid.fill_rect(ox, oy + 700, 100, 40, 4);
grid.fill_rect(ox + 10, oy + 680, 80, 20, 4);
grid.fill_rect(ox + 20, oy + 660, 60, 20, 4);
grid.fill_rect(ox + 880, oy + 700, 80, 40, 6);
grid.fill_rect(ox + 890, oy + 680, 60, 20, 6);
grid.fill_rect(ox + 900, oy + 660, 40, 20, 6);
grid.fill_circle(ox + 420, oy + 280, 30, 11);
grid.fill_circle(ox + 450, oy + 270, 20, 11);
grid.fill_circle(ox + 390, oy + 290, 18, 11);
grid.fill_rect(ox + 420, oy + 300, 20, 15, 11);
grid.fill_rect(ox + 100, oy + 720, 180, 20, 3);
grid.fill_circle(ox + 120, oy + 710, 25, 3);
grid.fill_circle(ox + 180, oy + 708, 30, 3);
grid.fill_circle(ox + 160, oy + 700, 18, 3);
grid.fill_circle(ox + 220, oy + 715, 20, 3);
grid.fill_circle(ox + 850, oy + 580, 22, 12);
grid.fill_circle(ox + 870, oy + 590, 14, 12);
grid.fill_rect(ox + 840, oy + 600, 40, 20, 12);
grid.fill_rect(ox + 470, oy + 660, 280, 10, 5);
grid.fill_rect(ox + 490, oy + 650, 40, 10, 5);
grid.fill_rect(ox + 710, oy + 650, 40, 10, 5);
grid.fill_rect(ox + 520, oy + 720, 180, 20, 1);
grid.fill_rect(ox + 540, oy + 710, 140, 10, 1);
grid.fill_circle(ox + 600, oy + 718, 15, 6);
grid.fill_circle(ox + 620, oy + 716, 10, 6);
grid.fill_rect(ox + 100, oy + 730, 40, 10, 13);
grid.fill_circle(ox + 120, oy + 732, 8, 13);
grid.fill_rect(ox + 280, oy + 640, 40, 40, 10);
grid.fill_rect(ox + 290, oy + 650, 20, 30, 1);
grid.fill_circle(ox + 300, oy + 670, 5, 9);
grid.fill_circle(ox + 550, oy + 520, 20, 2);
grid.fill_circle(ox + 570, oy + 530, 14, 2);
grid.fill_circle(ox + 200, oy + 520, 30, 1);
grid.fill_circle(ox + 220, oy + 500, 20, 1);
grid.fill_rect(ox + 300, oy + 580, 12, 20, 4);
grid.fill_rect(ox + 700, oy + 500, 10, 30, 4);
}
}
let cam_x = 512i32;
let cam_y = 600i32;
let cam_x = 400i32;
let cam_y = 630i32;
let zoom = 2.0f32;
let mut frame = 0;
+58 -1
View File
@@ -415,21 +415,56 @@ impl Physics {
if can_fall {
let was_liquid = bprops.is_liquid;
grid.swap(x, y, x, y + 1);
let mi = grid.index(x, y + 1);
grid.flags[mi] |= crate::grid::FLAG_MOMENTUM;
if was_liquid {
let idx_displaced = grid.index(x, y);
grid.flags[idx_displaced] |= crate::grid::FLAG_MOMENTUM;
if self.rand() % 10 < 7 {
if y > 0 && grid.get(x, y - 1) == 0 {
let ai = grid.index(x, y - 1);
grid.flags[ai] |= crate::grid::FLAG_MOMENTUM;
grid.swap(x, y, x, y - 1);
}
} else {
let pdir = self.rand_dir();
for dist in 1..=4i32 {
for dist in 3..=8i32 {
let sx = x as i32 + pdir * dist;
if !grid.in_bounds(sx, y as i32 + 1) { break; }
let sxu = sx as u32;
if grid.get(sxu, y + 1) == 0 {
let si = grid.index(sxu, y + 1);
grid.flags[si] |= crate::grid::FLAG_MOMENTUM;
grid.swap(x, y + 1, sxu, y + 1);
break;
}
}
}
}
return;
}
if grid.flags[grid.index(x, y)] & crate::grid::FLAG_MOMENTUM != 0 {
if y > 0 {
if grid.get(x, y - 1) == 0 {
let ai = grid.index(x, y - 1);
grid.flags[ai] |= crate::grid::FLAG_MOMENTUM;
grid.swap(x, y, x, y - 1);
return;
}
let dir = self.rand_dir();
for &d in &[dir, -dir] {
let dx = x as i32 + d;
if grid.in_bounds(dx, y as i32 - 1) && grid.get(dx as u32, y - 1) == 0 {
grid.swap(x, y, dx as u32, y - 1);
return;
}
}
}
let mi = grid.index(x, y);
grid.flags[mi] &= !crate::grid::FLAG_MOMENTUM;
}
let dir = self.rand_dir();
for &d in &[dir, -dir] {
let dx = x as i32 + d;
@@ -458,6 +493,28 @@ impl Physics {
}
}
}
let has_momentum = grid.flags[grid.index(x, y)] & crate::grid::FLAG_MOMENTUM != 0;
if has_momentum && below != 0 && !bprops.is_liquid && !bprops.is_gas {
if y > 1 {
for &d in &[self.rand_dir(), -self.rand_dir()] {
let dx = x as i32 + d;
if grid.in_bounds(dx, y as i32 - 2) && grid.get(dx as u32, y - 1) == 0 && grid.get(dx as u32, y - 2) == 0 {
grid.swap(x, y, dx as u32, y - 2);
return;
}
}
}
if y > 0 {
for &d in &[self.rand_dir(), -self.rand_dir()] {
let dx = x as i32 + d;
if grid.in_bounds(dx, y as i32 - 1) && grid.get(dx as u32, y - 1) == 0 {
grid.swap(x, y, dx as u32, y - 1);
return;
}
}
}
}
}
fn update_solid(&mut self, grid: &mut Grid, x: u32, y: u32, _mat: u8) {
+8 -2
View File
@@ -7,6 +7,7 @@ pub struct RenderBuffer {
pub height: u32,
light: Vec<u8>,
block: Vec<u8>,
zoom: f32,
}
impl RenderBuffer {
@@ -19,6 +20,7 @@ impl RenderBuffer {
height,
light: vec![0; lsize],
block: vec![0; lsize],
zoom: 1.0,
}
}
@@ -27,6 +29,8 @@ impl RenderBuffer {
let rh = self.height as i32;
let total = (rw * rh) as usize;
self.zoom = zoom;
for i in 0..total {
self.light[i] = 0;
self.block[i] = 0;
@@ -49,7 +53,8 @@ impl RenderBuffer {
}
}
for _pass in 0..12 {
let max_passes = (16.0 / zoom).max(6.0).min(24.0) as u32;
for _pass in 0..max_passes {
let dir = _pass & 1;
let mut changed = false;
if dir == 0 {
@@ -157,7 +162,8 @@ impl RenderBuffer {
let ni = (ny * rw + nx) as usize;
let nlight = self.light[ni] as u32;
if nlight <= 1 { continue; }
let falloff = (if nx == px || ny == py { 2 } else { 3 }) + blk;
let base_falloff = if nx == px || ny == py { 2.0 } else { 3.0 };
let falloff = (base_falloff / self.zoom) as u32 + blk;
if nlight > falloff {
let incoming = nlight - falloff;
if incoming > best {
+9
View File
@@ -12,4 +12,13 @@ export class Camera {
setZoom(z: number) {
this.targetZoom = Math.max(0.1, Math.min(10, z));
}
zoomAt(z: number, worldX: number, worldY: number) {
if (this.zoom <= 0) return;
const oldZoom = this.zoom;
this.setZoom(z);
const newZoom = this.targetZoom;
this.x = worldX - (worldX - this.x) * (oldZoom / newZoom);
this.y = worldY - (worldY - this.y) * (oldZoom / newZoom);
}
}
+93 -23
View File
@@ -3,8 +3,8 @@ import { Renderer } from "./renderer";
import { Input } from "./input";
import { Camera } from "./camera";
const GRID_WIDTH = 1024;
const GRID_HEIGHT = 768;
const GRID_WIDTH = 4096;
const GRID_HEIGHT = 3072;
const PAINT_MATERIALS = [M.SAND, M.WATER, M.LAVA, M.WOOD, M.FIRE, M.ICE, M.OIL, M.ACID, M.STONE, M.DIRT, M.GLASS];
@@ -19,30 +19,93 @@ async function main() {
const engine = await createEngine(GRID_WIDTH, GRID_HEIGHT);
engine.fillRect(0, 740, 1024, 28, M.STONE);
engine.fillRect(440, 720, 144, 20, M.WOOD);
engine.fillRect(460, 700, 104, 20, M.DIRT);
engine.fillCircle(490, 560, 25, M.SAND);
engine.fillCircle(520, 550, 20, M.SAND);
engine.fillRect(430, 650, 40, 30, M.WATER);
engine.fillRect(550, 650, 40, 25, M.LAVA);
engine.fillRect(505, 640, 10, 16, M.ICE);
engine.fillCircle(620, 580, 20, M.OIL);
engine.fillRect(480, 620, 64, 30, M.WOOD);
engine.fillRect(480, 600, 64, 20, M.FIRE);
engine.fillRect(300, 720, 40, 20, M.GLASS);
engine.fillCircle(200, 680, 18, M.SAND);
engine.fillRect(100, 700, 30, 40, M.WOOD);
engine.fillRect(750, 680, 60, 60, M.STONE);
engine.fillCircle(780, 660, 15, M.LAVA);
engine.fillRect(700, 720, 30, 20, M.ICE);
// === EPIC WORLD: tiled 16 copies of the scene ===
function buildScene(ox: number, oy: number) {
engine.fillRect(ox, oy + 740, 1024, 28, M.STONE);
engine.fillRect(ox + 20, oy + 680, 80, 60, M.STONE);
engine.fillRect(ox + 30, oy + 640, 60, 40, M.STONE);
engine.fillRect(ox + 40, oy + 610, 40, 30, M.STONE);
engine.fillCircle(ox + 60, oy + 600, 15, M.DIRT);
engine.fillRect(ox + 50, oy + 660, 20, 20, M.DIRT);
engine.fillRect(ox, oy + 720, 120, 20, M.STONE);
engine.fillRect(ox + 85, oy + 705, 35, 15, M.STONE);
engine.fillRect(ox + 5, oy + 705, 60, 15, M.STONE);
engine.fillCircle(ox + 60, oy + 712, 12, M.LAVA);
engine.fillCircle(ox + 70, oy + 708, 6, M.LAVA);
engine.fillRect(ox + 60, oy + 695, 20, 10, M.DIRT);
engine.fillRect(ox + 350, oy + 680, 100, 60, M.STONE);
engine.fillRect(ox + 365, oy + 640, 70, 40, M.STONE);
engine.fillRect(ox + 380, oy + 610, 40, 30, M.STONE);
engine.fillRect(ox + 390, oy + 660, 20, 20, M.GLASS);
engine.fillRect(ox + 370, oy + 700, 60, 40, M.WOOD);
engine.fillCircle(ox + 400, oy + 690, 8, M.FIRE);
engine.fillRect(ox + 395, oy + 675, 10, 5, M.FIRE);
engine.fillRect(ox + 750, oy + 640, 50, 100, M.STONE);
engine.fillRect(ox + 760, oy + 610, 30, 30, M.STONE);
engine.fillRect(ox + 770, oy + 590, 10, 20, M.STONE);
engine.fillCircle(ox + 775, oy + 580, 8, M.FIRE);
engine.fillRect(ox + 760, oy + 650, 30, 40, M.WOOD);
engine.fillRect(ox + 180, oy + 420, 100, 20, M.STONE);
engine.fillRect(ox + 190, oy + 400, 80, 20, M.DIRT);
engine.fillCircle(ox + 230, oy + 390, 15, M.SAND);
engine.fillRect(ox + 200, oy + 380, 30, 20, M.WOOD);
engine.fillCircle(ox + 215, oy + 385, 6, M.FIRE);
engine.fillRect(ox + 240, oy + 410, 10, 10, M.GLASS);
engine.fillRect(ox + 620, oy + 350, 80, 18, M.STONE);
engine.fillRect(ox + 630, oy + 335, 60, 15, M.DIRT);
engine.fillCircle(ox + 660, oy + 325, 12, M.SAND);
engine.fillCircle(ox + 640, oy + 340, 8, M.ICE);
engine.fillRect(ox, oy + 700, 100, 40, M.WATER);
engine.fillRect(ox + 10, oy + 680, 80, 20, M.WATER);
engine.fillRect(ox + 20, oy + 660, 60, 20, M.WATER);
engine.fillRect(ox + 880, oy + 700, 80, 40, M.LAVA);
engine.fillRect(ox + 890, oy + 680, 60, 20, M.LAVA);
engine.fillRect(ox + 900, oy + 660, 40, 20, M.LAVA);
engine.fillCircle(ox + 420, oy + 280, 30, M.ICE);
engine.fillCircle(ox + 450, oy + 270, 20, M.ICE);
engine.fillCircle(ox + 390, oy + 290, 18, M.ICE);
engine.fillRect(ox + 420, oy + 300, 20, 15, M.ICE);
engine.fillRect(ox + 100, oy + 720, 180, 20, M.SAND);
engine.fillCircle(ox + 120, oy + 710, 25, M.SAND);
engine.fillCircle(ox + 180, oy + 708, 30, M.SAND);
engine.fillCircle(ox + 160, oy + 700, 18, M.SAND);
engine.fillCircle(ox + 220, oy + 715, 20, M.SAND);
engine.fillCircle(ox + 850, oy + 580, 22, M.OIL);
engine.fillCircle(ox + 870, oy + 590, 14, M.OIL);
engine.fillRect(ox + 840, oy + 600, 40, 20, M.OIL);
engine.fillRect(ox + 470, oy + 660, 280, 10, M.WOOD);
engine.fillRect(ox + 490, oy + 650, 40, 10, M.WOOD);
engine.fillRect(ox + 710, oy + 650, 40, 10, M.WOOD);
engine.fillRect(ox + 520, oy + 720, 180, 20, M.STONE);
engine.fillRect(ox + 540, oy + 710, 140, 10, M.STONE);
engine.fillCircle(ox + 600, oy + 718, 15, M.LAVA);
engine.fillCircle(ox + 620, oy + 716, 10, M.LAVA);
engine.fillRect(ox + 100, oy + 730, 40, 10, M.ACID);
engine.fillCircle(ox + 120, oy + 732, 8, M.ACID);
engine.fillRect(ox + 280, oy + 640, 40, 40, M.GLASS);
engine.fillRect(ox + 290, oy + 650, 20, 30, M.STONE);
engine.fillCircle(ox + 300, oy + 670, 5, M.FIRE);
engine.fillCircle(ox + 550, oy + 520, 20, M.DIRT);
engine.fillCircle(ox + 570, oy + 530, 14, M.DIRT);
engine.fillCircle(ox + 200, oy + 520, 30, M.STONE);
engine.fillCircle(ox + 220, oy + 500, 20, M.STONE);
engine.fillRect(ox + 300, oy + 580, 12, 20, M.WATER);
engine.fillRect(ox + 700, oy + 500, 10, 30, M.WATER);
}
for (let ty = 0; ty < 4; ty++) {
for (let tx = 0; tx < 4; tx++) {
buildScene(tx * 1024, ty * 768);
}
}
// === Spawn point: center on the castle ===
camera.x = 400;
camera.y = 630;
const rw = engine.renderWidth();
const rh = engine.renderHeight();
camera.x = 512;
camera.y = 600;
function resize() {
const dpr = window.devicePixelRatio || 1;
renderer.resize(window.innerWidth, window.innerHeight, dpr);
@@ -58,6 +121,13 @@ async function main() {
return { x: Math.floor(mx), y: Math.floor(my) };
}
function mouseWorld() {
return {
x: (input.mouseX / window.innerWidth - 0.5) * rw / camera.zoom + camera.x,
y: (input.mouseY / window.innerHeight - 0.5) * rh / camera.zoom + camera.y,
};
}
const panel = document.getElementById("panel")!;
function buildPanel() {
panel.innerHTML = "";
@@ -122,7 +192,7 @@ async function main() {
if (input.isDown("Equal")) camera.setZoom(camera.zoom + 2 * dt);
if (input.isDown("Minus")) camera.setZoom(camera.zoom - 2 * dt);
if (input.wheel !== 0) {
camera.setZoom(camera.zoom + input.wheel * 0.5);
camera.setZoom(camera.zoom + input.wheel * 0.2);
input.wheel = 0;
}