Initial commit: AtomicEngine v0.2

- Rust/WASM physics engine (grid, material, physics, render_buffer)
- 16 material types (stone, sand, water, wood, fire, lava, glass, ice, oil, acid, steam, spark, smoke + player placeholder)
- Fire system (fuel-based, flammability-scaled consumption, spread, smoke/sparks)
- Lighting system (emission, flood-fill propagation, glow, reflection)
- Per-material pixel texturing (wood grain, dirt specks, stone noise, flame variation)
- WebGL2 renderer with camera (WASD move, mouse wheel zoom)
- Native x86 debug binary (identical dimensions, FPS counter)
- Large world 1024x768 with active-region simulation (camera + 200px margin)
- UI material palette + FPS display
This commit is contained in:
2026-07-09 11:13:39 +02:00
commit 1bce9da0c6
21 changed files with 3261 additions and 0 deletions
+224
View File
@@ -0,0 +1,224 @@
# AtomicEngine
> Eine 2D-Platformer-Gameengine bei der jeder Pixel wie ein Atom mit eigenen
> Eigenschaften und Anziehungskräften zu seinen Nachbarn agiert.
> Browser-basiert mit Rust/WASM für die Physik und WebGL2 fürs Rendering.
## Architektur-Übersicht
```
engine/ (Rust → WASM) web/ (TypeScript + Vite)
┌─────────────────────┐ ┌──────────────────────┐
│ grid.rs │ │ main.ts │
│ material.rs │ │ renderer.ts (WebGL2) │
│ physics.rs │ │ input.ts │
│ render_buffer.rs │ │ camera.ts │
│ lib.rs (wasm API) │ │ │
└─────────────────────┘ └──────────────────────┘
│ │
└── SharedArrayBuffer ──────┘
```
## Tech-Stack
| Schicht | Technologie | Warum |
|---------------|--------------------------|------------------------------------|
| Physik | Rust → WASM | Performance, Sicherheit |
| WASM Bridge | wasm-bindgen, wasm-pack | Auto JS-Bindings |
| Renderer | WebGL2 (Canvas) | GPU-beschleunigt, 60fps |
| Web Shell | TypeScript, Vite | Build, Dev-Server, HMR |
| Speicher | SharedArrayBuffer | Zero-copy WASM ↔ JS |
## Welt-Modell
- **Große Welt**: 1024×768 Pixel (4×3 Chunks à 256×256)
- Nur Zellen im Sichtfeld + 80px Margin werden simuliert
- Rest eingefroren — keine CPU-Kosten für unsichtbare Bereiche
- Grid wächst auf 1024×768 = 786K Zellen (~3 MB Speicher)
### Datenstruktur Cell (pro Pixel)
```rust
struct Cell {
material: u8, // Material-ID (0 = Luft)
health: u8, // Lebenspunkte/Dichte
temp: u8, // Temperatur
flags: u8, // Bitflags (aktiv, Player, Flüssig, ...)
}
```
Grid ist SoA (Structure of Arrays):
- `Vec<u8>` für materials, healths, temps, flags
- Breite × Höhe = Chunk-Größe
## Material-System
16 Materialien (0=Luft, 1=Stein, 2=Erde, 3=Sand, 4=Wasser, 5=Holz, 6=Lava,
7=Player, 8=Dampf, 9=Feuer, 10=Glas, 11=Eis, 12=Öl, 13=Säure, 14=Funken, 15=Rauch).
Properties pro Material: `MaterialProps` in `engine/src/material.rs`:
- `color`, `density`, Bewegung-Typ (`is_powder/is_liquid/is_gas/is_solid/is_static`)
- `flammability`, `melt_temp`, `boil_temp`, `heat_conduct`, `acid_resist`
- `light_emit` (0255, wie viel Licht das Material abstrahlt)
- `light_block` (0255, wie stark das Material Licht blockiert)
### Interaktions-System
**Heat-Transfer**: Temperatur-Diffusion über Moore-Nachbarn (`heat_conduct`-abhängig).
**Phase-Transformationen** (`material::transform`):
- Eis (11) → Wasser (4) bei temp ≥ 5°C
- Wasser (4) → Dampf (8) bei temp ≥ 100°C
- Dampf (8) → Wasser (4) bei temp ≤ 80°C
- Lava (6) → Stein (1) bei temp ≤ 5°C
- Holz (5) → Feuer (9) bei temp ≥ 240°C
- Öl (12) → Feuer (9) bei temp ≥ 120°C
**Reaktionen** (`physics::react`):
- Wasser + Lava → Dampf + Stein
- Wasser + Feuer → Dampf + Luft (löscht Feuer)
- Lava + Holz/Öl → entzündet Holz/Öl sofort zu Feuer
- Säure + Holz/Sand → zerstört Holz/Sand
- Säure + Stein/Erde → zerstört Stein/Erde (langsamer)
**Feuer-System**:
- Braucht Brennstoff (Holz, Öl) um zu überleben — prüft direkt unter sich + Moore-Nachbarn
- Feuer-Säule: Hat eine Zelle Feuer unter sich, gilt sie als versorgt (Ketten-Propagation)
- Mit Brennstoff: keine Health-Decay
- Ohne Brennstoff: 6hp/Frame → erlischt in ~7 Frames (~0.1s)
- Brenn-Rate `flammability`-abhängig: `300/flam` Frames zwischen 1hp-Konsum
- Holz (30): ~10 Frames/Konsum → ~42s pro Block
- Öl (80): ~3 Frames/Konsum → ~13s pro Block
- Ausbreitung: 1/250 Chance auf brennbare Nachbarn
- 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)
**Ephemere Partikel** (Funken + Rauch):
- `update_ephemeral`: dekrementiert Health, bei 0 → Luft
- Flammen-Funken: 412 Frames, 1/5 Emission, kurzlebig für Flammenspitzen
- Funken: 3080 Frames Lebensdauer, orange, 1/16 Emission/Frame
- Rauch: 140220 Frames, hellgrau transparent, 1/6 Emission/Frame
- Gas-Pass separat (alle 2 Frames, top→bottom = max 1px/Frame Aufstieg)
## Physik-Loop (pro Frame)
Reihenfolge in `physics::update(cam_x, cam_y, rw, rh, zoom)`:
Nur Zellen innerhalb **Kamera-Sichtfeld + 80px Margin** werden simuliert.
Rest der Welt ist eingefroren (keine CPU-Kosten).
1. **update_fire** — Feuer-Update: Brennstoff-Verbrauch, Ausbreitung, Funken/Rauch-Emission
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
4. **update_ephemeral** (nur aktive Region): Spark/Smoke Health dekrementieren, bei 0 → Luft
## Level-Format
RGBA-PNG, jeder Pixel = 1 Atom:
- R = Material-ID (0=Luft)
- G = Dichte/Gesundheit
- B = Temperatur
- A = Flags
Levels in Aseprite/Photoshop malbar. Große Welten = Raster von PNG-Dateien.
## Rendering
1. WASM schreibt sichtbaren Bereich als RGBA-Buffer
2. Buffer als Uint8Array aus WASM-Speicher gelesen
3. WebGL2 lädt als Textur → Fullscreen-Quad
4. Kamera-Matrix für Scroll/Zoom
### Textur-System
Jedes Material bekommt deterministische Pixel-Variation basierend auf Grid-Koordinaten:
- **Holz**: Vertikale Maserung (dünne Fasern alle ~5px, ±1px Welle)
- **Erde**: Grobkörnige Flecken (niederfrequentes Rauschen, ±20 RGB)
- **Stein/Glas**: Subtiles Rauschen (±10 RGB)
- **Sand**: Feines Granulat (±6 RGB)
- **Eis**: Minimales Rauschen (±5 RGB)
- **Flüssigkeiten/Gase**: Keine Textur
- **Feuer/Lava**: Kanal-getrennte Varianz (R ±6, G ±12, B ±8) — natürliche Gelb-/Orange-Mischung
### Beleuchtungs-System
**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)
- Opaque Pixel blocken Weitergabe, werden aber selbst beleuchtet
3. Abwechselnde Scan-Richtung für gleichmäßige Verteilung
**Lichtquellen**: Feuer (255), Lava (200), Funken (180)
**Glow-Effekt**: Luft-Pixel mit Licht blenden von dunklem Hintergrund zu warmem Orange — quadratische Kurve (`t = lvl²/255`) für natürlichen Abfall. Nur direkt an der Quelle stark sichtbar.
**Reflexion**: Semi-transparente Materialien (Glas, Wasser, Eis — block 1179) werfen 50% des empfangenen Lichts an Nachbar-Pixel zurück → indirekte Beleuchtung.
**Beleuchtungs-Formel** (Material-Pixel): Additiv: `r = min(255, r × 150/256 + light)` — Ambient-Basis 59% + Licht obendrauf.
### UI
- **Material-Palette**: Leiste am unteren Bildschirmrand, Farb-Swatch + Name + Taste
- **FPS-Anzeige**: Oben rechts, grün, alle 500ms aktualisiert
## Build & Entwicklung
**WICHTIG**: Native Debug-Binary und WASM/Web-Build müssen immer identisch laufen.
Der Debug-Build nutzt dieselben Module (`grid`, `material`, `physics`, `render_buffer`)
und dieselben Dimensionen (256×192 Grid, 320×180 Render-Buffer). Performance-Unterschiede
sind WASM-Overhead, nicht Logik-Abweichungen. Bei Änderungen an der Engine immer beide
Builds testen.
```bash
# Rust → WASM bauen
cd engine && wasm-pack build --target web --out-dir ../web/pkg
# Native Debug-Binary (x86, ohne WASM)
cd engine && cargo run --bin atomic-debug --release
# Web Dev-Server
cd web && npm run dev
# Produktion
cd web && npm run build
```
## Datei-Index (was findet man wo)
| Was | Datei |
|----------------------------------|-----------------------------------|
| WASM öffentliche API | `engine/src/lib.rs` |
| Native Debug Binary | `engine/src/main.rs` |
| Cell + Chunk + Grid DS | `engine/src/grid.rs` |
| Material-Definitionen | `engine/src/material.rs` |
| Physik: Kräfte, Sand, Fluide | `engine/src/physics.rs` |
| RGBA-Buffer Export | `engine/src/render_buffer.rs` |
| Rust-Abhängigkeiten | `engine/Cargo.toml` |
| WebGL2 Renderer | `web/src/renderer.ts` |
| WASM-Bridge + Material-Konstanten| `web/src/engine.ts` |
| Game-Loop, Wiring | `web/src/main.ts` |
| Input (Keyboard, Maus) | `web/src/input.ts` |
| Kamera (Scroll, Zoom) | `web/src/camera.ts` |
| HTML Einstieg | `web/index.html` |
| Vite Konfiguration | `web/vite.config.ts` |
| TypeScript Konfiguration | `web/tsconfig.json` |
## Aktueller Status
- [x] Phase 0: Projekt-Struktur + WASM/WebGL End-to-End ← **fertig**
- [x] Phase 1: Sand-Physik (Pixel fällt nach unten) ← **fertig** (Sand + Flüssigkeiten)
- [x] Phase 2: Mehrere Materialien + Kräfte-Tabelle ← **fertig**
- [x] Phase 3: Chunk-System + große Welt + Kamera ← **fertig**
- [ ] Phase 4: Spieler als Atom-Cluster + Input
- [ ] Phase 5: Erweiterungen (Items, Player-Interaktion, Editor, ...)
## Namenskonventionen
- Rust: snake_case, English
- TypeScript: camelCase, English
- Keine Kommentare im Code (nur wenn explizit gewünscht)
+2
View File
@@ -0,0 +1,2 @@
target/
pkg/
+178
View File
@@ -0,0 +1,178 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "atomic-engine"
version = "0.1.0"
dependencies = [
"console_error_panic_hook",
"wasm-bindgen",
"wee_alloc",
]
[[package]]
name = "bumpalo"
version = "3.20.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649"
[[package]]
name = "cfg-if"
version = "0.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822"
[[package]]
name = "cfg-if"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
[[package]]
name = "console_error_panic_hook"
version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a06aeb73f470f66dcdbf7223caeebb85984942f22f1adb2a088cf9668146bbbc"
dependencies = [
"cfg-if 1.0.4",
"wasm-bindgen",
]
[[package]]
name = "libc"
version = "0.2.186"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66"
[[package]]
name = "memory_units"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8452105ba047068f40ff7093dd1d9da90898e63dd61736462e9cdda6a90ad3c3"
[[package]]
name = "once_cell"
version = "1.21.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
[[package]]
name = "proc-macro2"
version = "1.0.106"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.46"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dfbc457d0c7a0759a614551b11a6409e5951f6c7537be1f1b7682b9ae9230368"
dependencies = [
"proc-macro2",
]
[[package]]
name = "rustversion"
version = "1.0.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f"
[[package]]
name = "syn"
version = "2.0.118"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b9ae57f904213ebb649ce6895b8a66c66f0203b9319718f69a5612a065b1422"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "unicode-ident"
version = "1.0.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
[[package]]
name = "wasm-bindgen"
version = "0.2.126"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4b067c0c11094aef6b7a801c1e34a26affafdf3d051dba08456b868789aaf9a4"
dependencies = [
"cfg-if 1.0.4",
"once_cell",
"rustversion",
"wasm-bindgen-macro",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-macro"
version = "0.2.126"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "167ce5e579f6bcf889c4f7175a8a5a585de84e8ff93976ce393efa5f2837aab1"
dependencies = [
"quote",
"wasm-bindgen-macro-support",
]
[[package]]
name = "wasm-bindgen-macro-support"
version = "0.2.126"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f3997c7839262f4ef12cf90b818d6340c18e80f263f1a94bf157d0ec4420380e"
dependencies = [
"bumpalo",
"proc-macro2",
"quote",
"syn",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-shared"
version = "0.2.126"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc1b4cb0cc549fcf58d7dfc081778139b3d283a081644e833e84682ad71cea24"
dependencies = [
"unicode-ident",
]
[[package]]
name = "wee_alloc"
version = "0.4.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dbb3b5a6b2bb17cb6ad44a2e68a43e8d2722c997da10e928665c72ec6c0a0b8e"
dependencies = [
"cfg-if 0.1.10",
"libc",
"memory_units",
"winapi",
]
[[package]]
name = "winapi"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
dependencies = [
"winapi-i686-pc-windows-gnu",
"winapi-x86_64-pc-windows-gnu",
]
[[package]]
name = "winapi-i686-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
[[package]]
name = "winapi-x86_64-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
+20
View File
@@ -0,0 +1,20 @@
[package]
name = "atomic-engine"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib", "rlib"]
[[bin]]
name = "atomic-debug"
path = "src/main.rs"
[dependencies]
wasm-bindgen = "0.2"
console_error_panic_hook = "0.1"
wee_alloc = "0.4"
[profile.release]
opt-level = "s"
lto = true
+112
View File
@@ -0,0 +1,112 @@
pub struct Grid {
pub materials: Vec<u8>,
pub healths: Vec<u8>,
pub temps: Vec<u8>,
pub flags: Vec<u8>,
pub width: u32,
pub height: u32,
}
pub const FLAG_UPDATED: u8 = 0b0000_0001;
pub const FLAG_PLAYER: u8 = 0b0000_0010;
impl Grid {
pub fn new(width: u32, height: u32) -> Self {
let size = (width * height) as usize;
Self {
materials: vec![0; size],
healths: vec![255; size],
temps: vec![0; size],
flags: vec![0; size],
width,
height,
}
}
#[inline]
pub fn index(&self, x: u32, y: u32) -> usize {
(y * self.width + x) as usize
}
pub fn in_bounds(&self, x: i32, y: i32) -> bool {
x >= 0 && x < self.width as i32 && y >= 0 && y < self.height as i32
}
#[inline]
pub fn get(&self, x: u32, y: u32) -> u8 {
self.materials[self.index(x, y)]
}
#[inline]
pub fn get_signed(&self, x: i32, y: i32) -> u8 {
if self.in_bounds(x, y) {
self.materials[self.index(x as u32, y as u32)]
} else {
0
}
}
#[inline]
pub fn set(&mut self, x: u32, y: u32, material: u8) {
let idx = self.index(x, y);
self.materials[idx] = material;
self.flags[idx] |= FLAG_UPDATED;
if material == 6 {
self.temps[idx] = 200;
self.healths[idx] = 255;
} else if material == 9 {
self.temps[idx] = 200;
self.healths[idx] = 200;
} else if material == 11 {
self.temps[idx] = 0;
}
}
#[inline]
pub fn swap(&mut self, x1: u32, y1: u32, x2: u32, y2: u32) {
let i1 = self.index(x1, y1);
let i2 = self.index(x2, y2);
self.materials.swap(i1, i2);
self.healths.swap(i1, i2);
self.temps.swap(i1, i2);
self.flags[i1] |= FLAG_UPDATED;
self.flags[i2] |= FLAG_UPDATED;
}
pub fn is_empty(&self, x: u32, y: u32) -> bool {
self.get(x, y) == 0
}
pub fn clear_flags(&mut self) {
for f in &mut self.flags {
*f &= !FLAG_UPDATED;
}
}
pub fn fill_rect(&mut self, x: u32, y: u32, w: u32, h: u32, material: u8) {
for dy in 0..h {
for dx in 0..w {
let px = x + dx;
let py = y + dy;
if px < self.width && py < self.height {
self.set(px, py, material);
}
}
}
}
pub fn fill_circle(&mut self, cx: u32, cy: u32, radius: u32, material: u8) {
let r = radius as i32;
for dy in -r..=r {
for dx in -r..=r {
if dx * dx + dy * dy <= r * r {
let px = (cx as i32 + dx) as u32;
let py = (cy as i32 + dy) as u32;
if px < self.width && py < self.height {
self.set(px, py, material);
}
}
}
}
}
}
+117
View File
@@ -0,0 +1,117 @@
pub mod grid;
pub mod material;
pub mod physics;
pub mod render_buffer;
use std::cell::RefCell;
use grid::Grid;
use physics::Physics;
use render_buffer::RenderBuffer;
use wasm_bindgen::prelude::*;
thread_local! {
static ENGINE: RefCell<Option<Engine>> = RefCell::new(None);
}
struct Engine {
grid: Grid,
physics: Physics,
render_buffer: RenderBuffer,
}
#[wasm_bindgen(start)]
pub fn start() {
console_error_panic_hook::set_once();
}
fn with_engine<F, R>(f: F) -> R
where
F: FnOnce(&mut Engine) -> R,
{
ENGINE.with(|cell| {
let mut opt = cell.borrow_mut();
let engine = opt.as_mut().expect("Engine not initialized. Call init() first.");
f(engine)
})
}
#[wasm_bindgen]
pub fn init(width: u32, height: u32) {
let engine = Engine {
grid: Grid::new(width, height),
physics: Physics::new(),
render_buffer: RenderBuffer::new(320, 180),
};
ENGINE.with(|cell| {
*cell.borrow_mut() = Some(engine);
});
}
#[wasm_bindgen]
pub fn get_width() -> u32 {
with_engine(|e| e.grid.width)
}
#[wasm_bindgen]
pub fn get_height() -> u32 {
with_engine(|e| e.grid.height)
}
#[wasm_bindgen]
pub fn fill_rect(x: u32, y: u32, w: u32, h: u32, material: u8) {
with_engine(|e| e.grid.fill_rect(x, y, w, h, material));
}
#[wasm_bindgen]
pub fn fill_circle(x: u32, y: u32, radius: u32, material: u8) {
with_engine(|e| e.grid.fill_circle(x, y, radius, material));
}
#[wasm_bindgen]
pub fn simulate(cam_x: i32, cam_y: i32, zoom: f32) {
with_engine(|e| {
let rw = e.render_buffer.width;
let rh = e.render_buffer.height;
e.physics.update(&mut e.grid, cam_x, cam_y, rw, rh, zoom);
});
}
#[wasm_bindgen]
pub fn render_buffer_ptr() -> *const u8 {
ENGINE.with(|cell| {
let opt = cell.borrow();
let engine = opt.as_ref().expect("Engine not initialized.");
engine.render_buffer.pixels.as_ptr()
})
}
#[wasm_bindgen]
pub fn render_buffer_len() -> usize {
ENGINE.with(|cell| {
let opt = cell.borrow();
let engine = opt.as_ref().expect("Engine not initialized.");
engine.render_buffer.pixels.len()
})
}
#[wasm_bindgen]
pub fn render_width() -> u32 {
with_engine(|e| e.render_buffer.width)
}
#[wasm_bindgen]
pub fn render_height() -> u32 {
with_engine(|e| e.render_buffer.height)
}
#[wasm_bindgen]
pub fn update_render(cam_x: i32, cam_y: i32, zoom: f32) {
with_engine(|e| e.render_buffer.render(&e.grid, cam_x, cam_y, zoom));
}
#[wasm_bindgen]
pub fn reset() {
with_engine(|e| {
e.grid = Grid::new(e.grid.width, e.grid.height);
});
}
+118
View File
@@ -0,0 +1,118 @@
mod grid;
mod material;
mod physics;
mod render_buffer;
use grid::Grid;
use physics::Physics;
use render_buffer::RenderBuffer;
fn main() {
let width = 1024u32;
let height = 768u32;
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);
let cam_x = 512i32;
let cam_y = 600i32;
let zoom = 2.0f32;
let mut frame = 0;
let start = std::time::Instant::now();
loop {
physics.update(&mut grid, cam_x, cam_y, rw, rh, zoom);
rb.render(&grid, cam_x, cam_y, zoom);
frame += 1;
if frame % 60 == 0 {
let elapsed = start.elapsed();
let fps = frame as f64 / elapsed.as_secs_f64();
let (fire, wood, spark, smoke) = count_mats(&grid);
println!("--- {}s (frame {}) fps: {:.0} ---", frame / 60, frame, fps);
println!("fire: {}, wood: {}, spark: {}, smoke: {}", fire, wood, spark, smoke);
if fire == 0 {
let elapsed = start.elapsed();
let fps = frame as f64 / elapsed.as_secs_f64();
println!("Fire died at frame {}, avg fps: {:.0}", frame, fps);
break;
}
if frame >= 3600 {
println!("Timeout at 60s");
break;
}
}
}
}
fn count_mats(grid: &Grid) -> (u32, u32, u32, u32) {
let mut fire = 0u32;
let mut wood = 0u32;
let mut spark = 0u32;
let mut smoke = 0u32;
for y in 0..grid.height {
for x in 0..grid.width {
match grid.get(x, y) {
5 => wood += 1,
9 => fire += 1,
14 => spark += 1,
15 => smoke += 1,
_ => {}
}
}
}
(fire, wood, spark, smoke)
}
fn temp_range(grid: &Grid) -> (u8, u8) {
let mut min = 255u8;
let mut max = 0u8;
for y in 0..grid.height {
for x in 0..grid.width {
let idx = grid.index(x, y);
let t = grid.temps[idx];
if t > 0 {
min = min.min(t);
max = max.max(t);
}
}
}
if min == 255 { min = 0; }
(min, max)
}
fn print_grid(grid: &Grid) {
for y in 0..grid.height {
for x in 0..grid.width {
let c = match grid.get(x, y) {
5 => 'W',
9 => 'F',
14 => '*',
15 => '%',
0 => '.',
1 => '#',
4 => '~',
6 => 'L',
8 => '\'',
_ => '?',
};
print!("{}", c);
}
println!();
}
}
+141
View File
@@ -0,0 +1,141 @@
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[repr(u8)]
pub enum Material {
Air = 0,
Stone = 1,
Dirt = 2,
Sand = 3,
Water = 4,
Wood = 5,
Lava = 6,
Player = 7,
Steam = 8,
Fire = 9,
Glass = 10,
Ice = 11,
Oil = 12,
Acid = 13,
Spark = 14,
Smoke = 15,
}
impl Material {
pub fn from_u8(val: u8) -> Material {
match val {
0 => Material::Air,
1 => Material::Stone,
2 => Material::Dirt,
3 => Material::Sand,
4 => Material::Water,
5 => Material::Wood,
6 => Material::Lava,
7 => Material::Player,
8 => Material::Steam,
9 => Material::Fire,
10 => Material::Glass,
11 => Material::Ice,
12 => Material::Oil,
13 => Material::Acid,
14 => Material::Spark,
15 => Material::Smoke,
_ => Material::Air,
}
}
}
#[derive(Clone, Copy)]
pub struct MaterialProps {
pub color: [u8; 4],
pub density: u8,
pub is_powder: bool,
pub is_liquid: bool,
pub is_gas: bool,
pub is_solid: bool,
pub is_static: bool,
pub flammability: u8,
pub melt_temp: u8,
pub boil_temp: u8,
pub heat_conduct: u8,
pub acid_resist: u8,
pub light_emit: u8,
pub light_block: u8,
}
pub const M: usize = 16;
pub static PROPS: [MaterialProps; M] = [
MaterialProps { color: [0, 0, 0, 0], density: 0, is_powder: false, is_liquid: false, is_gas: false, is_solid: false, is_static: false, flammability: 0, melt_temp: 255, boil_temp: 255, heat_conduct: 0, acid_resist: 255, light_emit: 0, light_block: 0 },
MaterialProps { color: [120, 120, 130, 255], density: 100, is_powder: false, is_liquid: false, is_gas: false, is_solid: true, is_static: true, flammability: 0, melt_temp: 220, boil_temp: 255, heat_conduct: 3, acid_resist: 200, light_emit: 0, light_block: 255 },
MaterialProps { color: [101, 67, 33, 255], density: 60, is_powder: true, is_liquid: false, is_gas: false, is_solid: false, is_static: false, flammability: 0, melt_temp: 200, boil_temp: 255, heat_conduct: 2, acid_resist: 100, light_emit: 0, light_block: 255 },
MaterialProps { color: [194, 178, 128, 255], density: 40, is_powder: true, is_liquid: false, is_gas: false, is_solid: false, is_static: false, flammability: 0, melt_temp: 200, boil_temp: 255, heat_conduct: 5, acid_resist: 150, light_emit: 0, light_block: 200 },
MaterialProps { color: [30, 60, 200, 180], density: 10, is_powder: false, is_liquid: true, is_gas: false, is_solid: false, is_static: false, flammability: 0, melt_temp: 0, boil_temp: 100, heat_conduct: 10, acid_resist: 200, light_emit: 0, light_block: 80 },
MaterialProps { color: [90, 60, 30, 255], density: 80, is_powder: false, is_liquid: false, is_gas: false, is_solid: true, is_static: false, flammability: 30, melt_temp: 240, boil_temp: 255, heat_conduct: 1, acid_resist: 80, light_emit: 0, light_block: 255 },
MaterialProps { color: [255, 100, 0, 255], density: 50, is_powder: false, is_liquid: true, is_gas: false, is_solid: false, is_static: false, flammability: 0, melt_temp: 200, boil_temp: 255, heat_conduct: 20, acid_resist: 255, light_emit: 200, light_block: 60 },
MaterialProps { color: [50, 200, 50, 255], density: 90, is_powder: false, is_liquid: false, is_gas: false, is_solid: false, is_static: false, flammability: 0, melt_temp: 255, boil_temp: 255, heat_conduct: 5, acid_resist: 255, light_emit: 0, light_block: 100 },
MaterialProps { color: [200, 200, 220, 140], density: 1, is_powder: false, is_liquid: false, is_gas: true, is_solid: false, is_static: false, flammability: 0, melt_temp: 0, boil_temp: 0, heat_conduct: 5, acid_resist: 255, light_emit: 0, light_block: 20 },
MaterialProps { color: [255, 180, 30, 255], density: 2, is_powder: false, is_liquid: false, is_gas: false, is_solid: false, is_static: false, flammability: 255, melt_temp: 255, boil_temp: 255, heat_conduct: 50, acid_resist: 255, light_emit: 255, light_block: 30 },
MaterialProps { color: [180, 210, 220, 255], density: 95, is_powder: false, is_liquid: false, is_gas: false, is_solid: true, is_static: false, flammability: 0, melt_temp: 160, boil_temp: 255, heat_conduct: 1, acid_resist: 250, light_emit: 0, light_block: 40 },
MaterialProps { color: [180, 220, 255, 255], density: 85, is_powder: false, is_liquid: false, is_gas: false, is_solid: true, is_static: false, flammability: 0, melt_temp: 5, boil_temp: 100, heat_conduct: 8, acid_resist: 200, light_emit: 0, light_block: 100 },
MaterialProps { color: [180, 140, 40, 200], density: 15, is_powder: false, is_liquid: true, is_gas: false, is_solid: false, is_static: false, flammability: 80, melt_temp: 0, boil_temp: 120, heat_conduct: 3, acid_resist: 200, light_emit: 0, light_block: 60 },
MaterialProps { color: [100, 255, 60, 200], density: 30, is_powder: false, is_liquid: true, is_gas: false, is_solid: false, is_static: false, flammability: 0, melt_temp: 0, boil_temp: 150, heat_conduct: 15, acid_resist: 255, light_emit: 0, light_block: 80 },
MaterialProps { color: [255, 180, 40, 255], density: 1, is_powder: false, is_liquid: false, is_gas: true, is_solid: false, is_static: false, flammability: 0, melt_temp: 255, boil_temp: 255, heat_conduct: 30, acid_resist: 255, light_emit: 180, light_block: 30 },
MaterialProps { color: [160, 155, 150, 200], density: 1, is_powder: false, is_liquid: false, is_gas: true, is_solid: false, is_static: false, flammability: 0, melt_temp: 255, boil_temp: 255, heat_conduct: 5, acid_resist: 255, light_emit: 0, light_block: 80 },
];
#[derive(Clone, Copy, Debug)]
pub struct Interaction {
pub attraction: i8,
pub can_merge: bool,
pub bond_strength: u8,
}
impl Interaction {
const fn none() -> Self {
Interaction { attraction: 0, can_merge: false, bond_strength: 0 }
}
}
pub fn interact(a: u8, b: u8) -> Interaction {
match (a, b) {
(1, 4) | (4, 1) => Interaction { attraction: 5, can_merge: false, bond_strength: 3 },
(1, 6) | (6, 1) => Interaction { attraction: 0, can_merge: false, bond_strength: 0 },
(3, 4) | (4, 3) => Interaction { attraction: 8, can_merge: false, bond_strength: 0 },
(4, 6) | (6, 4) => Interaction { attraction: -20, can_merge: false, bond_strength: 0 },
(5, 9) | (9, 5) => Interaction { attraction: 0, can_merge: false, bond_strength: 0 },
(5, 4) | (4, 5) => Interaction { attraction: 10, can_merge: false, bond_strength: 2 },
(11, 4) | (4, 11) => Interaction { attraction: 15, can_merge: false, bond_strength: 5 },
(11, 6) | (6, 11) => Interaction { attraction: -30, can_merge: false, bond_strength: 0 },
(9, 4) | (4, 9) => Interaction { attraction: -10, can_merge: false, bond_strength: 0 },
(12, 9) | (9, 12) => Interaction { attraction: 0, can_merge: false, bond_strength: 0 },
(13, 1) | (1, 13) => Interaction { attraction: 0, can_merge: false, bond_strength: 0 },
(13, 3) | (3, 13) | (13, 5) | (5, 13) => Interaction { attraction: 0, can_merge: false, bond_strength: 0 },
_ => {
if a == b {
if a == 4 || a == 6 || a == 12 || a == 13 {
Interaction { attraction: 20, can_merge: true, bond_strength: 1 }
} else if a == 3 || a == 2 {
Interaction { attraction: 5, can_merge: false, bond_strength: 0 }
} else if a == 1 || a == 10 || a == 11 {
Interaction { attraction: 30, can_merge: false, bond_strength: 10 }
} else if a == 5 {
Interaction { attraction: 15, can_merge: false, bond_strength: 8 }
} else {
Interaction::none()
}
} else {
Interaction::none()
}
}
}
}
pub fn transform(temp: u8, material: u8) -> u8 {
match material {
4 => if temp >= 100 { 8 } else { 4 },
11 => if temp >= 5 { 4 } else { 11 },
6 => if temp <= 5 { 1 } else { 6 },
8 => if temp <= 80 { 4 } else { 8 },
5 => if temp >= 240 { 9 } else { 5 },
12 => if temp >= 120 { 9 } else { 12 },
_ => material,
}
}
+528
View File
@@ -0,0 +1,528 @@
use crate::grid::Grid;
use crate::material::{self, PROPS};
const MOORE_X: [i32; 8] = [-1, 0, 1, -1, 1, -1, 0, 1];
const MOORE_Y: [i32; 8] = [-1, -1, -1, 0, 0, 1, 1, 1];
pub struct Physics {
rand_state: u32,
frame: u32,
}
impl Physics {
pub fn new() -> Self {
Self { rand_state: 12345, frame: 0 }
}
fn rand(&mut self) -> u32 {
self.rand_state = self.rand_state.wrapping_mul(1103515245).wrapping_add(12345);
self.rand_state >> 16
}
fn rand_dir(&mut self) -> i32 {
if self.rand() & 1 == 0 { -1 } else { 1 }
}
pub fn update(&mut self, grid: &mut Grid, cam_x: i32, cam_y: i32, rw: u32, rh: u32, zoom: f32) {
self.frame = self.frame.wrapping_add(1);
let w = grid.width as i32;
let h = grid.height as i32;
let margin = 200;
let vw = (rw as f32 / zoom) as i32;
let vh = (rh as f32 / zoom) as i32;
let x0 = (cam_x - vw / 2 - margin).max(0) as u32;
let y0 = (cam_y - vh / 2 - margin).max(0) as u32;
let x1 = (cam_x + vw / 2 + margin).min(w) as u32;
let y1 = (cam_y + vh / 2 + margin).min(h) as u32;
self.update_fire(grid, x0, y0, x1, y1);
for y in (y0..y1).rev() {
let parity = (self.frame & 1) as u32;
let xs = if x0 & 1 == parity { x0 } else { x0 + 1 };
for x in (xs..x1).step_by(2) {
let idx = grid.index(x, y);
let mat = grid.materials[idx];
if mat == 0 {
continue;
}
let temp = grid.temps[idx];
self.transfer_heat(grid, x, y, mat, temp);
self.react(grid, x, y, mat);
let mat = grid.materials[grid.index(x, y)];
let temp = grid.temps[grid.index(x, y)];
self.transform(grid, x, y, mat, temp);
let mat = grid.materials[grid.index(x, y)];
if mat == 0 {
continue;
}
let props = &PROPS[mat as usize];
if props.is_powder {
self.update_powder(grid, x, y, mat);
} else if props.is_liquid {
self.update_liquid(grid, x, y, mat);
} else if props.is_solid && !props.is_static {
self.update_solid(grid, x, y, mat);
}
}
}
if self.frame % 2 == 0 {
self.update_gases(grid, x0, y0, x1, y1);
}
self.update_ephemeral(grid, x0, y0, x1, y1);
}
fn transfer_heat(&mut self, grid: &mut Grid, x: u32, y: u32, mat: u8, temp: u8) {
let props = &PROPS[mat as usize];
if props.heat_conduct == 0 {
return;
}
let idx = grid.index(x, y);
for i in 0..8 {
let nx = x as i32 + MOORE_X[i];
let ny = y as i32 + MOORE_Y[i];
if !grid.in_bounds(nx, ny) {
continue;
}
let ni = grid.index(nx as u32, ny as u32);
let nmat = grid.materials[ni];
if nmat == 0 {
continue;
}
let ntemp = grid.temps[ni];
if temp == ntemp {
continue;
}
let diff = if temp > ntemp {
let d = temp - ntemp;
let rate = (props.heat_conduct as u32 * d as u32) / 255;
((rate + 1) / 2) as u8
} else {
let d = ntemp - temp;
let nprops = &PROPS[nmat as usize];
let rate = (nprops.heat_conduct as u32 * d as u32) / 255;
((rate + 1) / 2) as u8
};
if diff == 0 {
continue;
}
if temp > ntemp {
grid.temps[idx] = temp.saturating_sub(diff);
grid.temps[ni] = ntemp.saturating_add(diff);
} else {
grid.temps[idx] = temp.saturating_add(diff);
grid.temps[ni] = ntemp.saturating_sub(diff);
}
}
}
fn react(&mut self, grid: &mut Grid, x: u32, y: u32, mat: u8) {
let idx = grid.index(x, y);
for i in 0..8 {
let nx = x as i32 + MOORE_X[i];
let ny = y as i32 + MOORE_Y[i];
if !grid.in_bounds(nx, ny) {
continue;
}
let ni = grid.index(nx as u32, ny as u32);
let nmat = grid.materials[ni];
if nmat == 0 {
continue;
}
match (mat, nmat) {
(4, 6) | (6, 4) => {
if self.rand() % 4 == 0 {
if mat == 4 {
grid.materials[idx] = 8;
grid.materials[ni] = 1;
grid.temps[idx] = 100;
grid.temps[ni] = 10;
} else {
grid.materials[idx] = 1;
grid.materials[ni] = 8;
grid.temps[idx] = 10;
grid.temps[ni] = 100;
}
grid.flags[idx] |= crate::grid::FLAG_UPDATED;
grid.flags[ni] |= crate::grid::FLAG_UPDATED;
}
}
(6, 5) | (5, 6) | (6, 12) | (12, 6) => {
if self.rand() % 2 == 0 {
if mat == 6 {
grid.materials[ni] = 9;
grid.temps[ni] = 200;
grid.healths[ni] = 200;
} else {
grid.materials[idx] = 9;
grid.temps[idx] = 200;
grid.healths[idx] = 200;
}
grid.flags[idx] |= crate::grid::FLAG_UPDATED;
grid.flags[ni] |= crate::grid::FLAG_UPDATED;
}
}
(4, 9) => {
if self.rand() % 2 == 0 {
grid.materials[idx] = 8;
grid.materials[ni] = 0;
grid.temps[idx] = 90;
grid.temps[ni] = 0;
grid.flags[idx] |= crate::grid::FLAG_UPDATED;
grid.flags[ni] |= crate::grid::FLAG_UPDATED;
}
}
(13, 5) | (13, 3) => {
if self.rand() % 4 == 0 {
grid.materials[ni] = 0;
grid.healths[idx] = grid.healths[idx].saturating_sub(5);
grid.flags[ni] |= crate::grid::FLAG_UPDATED;
}
}
(13, 1) | (13, 2) => {
if self.rand() % 8 == 0 {
grid.materials[ni] = 0;
grid.healths[idx] = grid.healths[idx].saturating_sub(2);
grid.flags[ni] |= crate::grid::FLAG_UPDATED;
}
}
_ => {}
}
}
}
fn transform(&mut self, grid: &mut Grid, x: u32, y: u32, mat: u8, temp: u8) {
let idx = grid.index(x, y);
let new_mat = material::transform(temp, mat);
if new_mat != mat {
grid.materials[idx] = new_mat;
grid.flags[idx] |= crate::grid::FLAG_UPDATED;
}
}
fn update_fire(&mut self, grid: &mut Grid, x0: u32, y0: u32, x1: u32, y1: u32) {
for y in (y0..y1).rev() {
for x in x0..x1 {
let idx = grid.index(x, y);
if grid.materials[idx] != 9 {
continue;
}
let mut extinguish = false;
for i in 0..8 {
let nx = x as i32 + MOORE_X[i];
let ny = y as i32 + MOORE_Y[i];
if !grid.in_bounds(nx, ny) { continue; }
let ni = grid.index(nx as u32, ny as u32);
let nmat = grid.materials[ni];
if nmat == 4 || nmat == 11 {
grid.materials[idx] = 0;
grid.temps[idx] = 0;
grid.flags[idx] |= crate::grid::FLAG_UPDATED;
if nmat == 4 {
grid.materials[ni] = 8;
grid.temps[ni] = 90;
} else {
grid.materials[ni] = 4;
grid.temps[ni] = 5;
}
grid.flags[ni] |= crate::grid::FLAG_UPDATED;
extinguish = true;
break;
}
}
if extinguish {
continue;
}
let mut on_fuel = false;
if y + 1 < grid.height {
let below_idx = grid.index(x, y + 1);
let below_mat = grid.materials[below_idx];
if PROPS[below_mat as usize].flammability > 0 && below_mat != 9 {
on_fuel = true;
let flam = PROPS[below_mat as usize].flammability as u32;
let threshold = (300 / flam).max(3);
if self.rand() % threshold == 0 {
let bh = grid.healths[below_idx];
if bh <= 1 {
grid.materials[below_idx] = 0;
grid.flags[below_idx] |= crate::grid::FLAG_UPDATED;
} else {
grid.healths[below_idx] = bh - 1;
}
}
if y > 0 && self.rand() % 6 == 0 {
for &sdx in &[0, -1, 1] {
let sx = x as i32 + sdx;
if grid.in_bounds(sx, y as i32 - 1) {
let si = grid.index(sx as u32, y - 1);
if grid.materials[si] == 0 {
grid.materials[si] = 15;
grid.temps[si] = 50;
grid.healths[si] = 140 + (self.rand() % 80) as u8;
grid.flags[si] |= crate::grid::FLAG_UPDATED;
break;
}
}
}
}
}
}
if !on_fuel && y + 1 < grid.height && grid.get(x, y + 1) == 9 {
on_fuel = true;
}
if !on_fuel {
for i in 0..8 {
let nx = x as i32 + MOORE_X[i];
let ny = y as i32 + MOORE_Y[i];
if !grid.in_bounds(nx, ny) { continue; }
let ni = grid.index(nx as u32, ny as u32);
let nmat = grid.materials[ni];
if nmat == 9 { continue; }
if PROPS[nmat as usize].flammability > 0 {
on_fuel = true;
break;
}
}
}
let fh = grid.healths[idx];
if on_fuel {
grid.healths[idx] = fh;
} else {
grid.healths[idx] = fh.saturating_sub(6);
}
if grid.healths[idx] == 0 {
grid.materials[idx] = 0;
grid.temps[idx] = 0;
grid.flags[idx] |= crate::grid::FLAG_UPDATED;
continue;
}
grid.temps[idx] = 200;
for i in 0..8 {
let nx = x as i32 + MOORE_X[i];
let ny = y as i32 + MOORE_Y[i];
if !grid.in_bounds(nx, ny) { continue; }
let ni = grid.index(nx as u32, ny as u32);
let nmat = grid.materials[ni];
if nmat == 0 { continue; }
if PROPS[nmat as usize].flammability > 0 && nmat != 9 && self.rand() % 250 == 0 {
grid.materials[ni] = 9;
grid.temps[ni] = 200;
grid.healths[ni] = 40;
grid.flags[ni] |= crate::grid::FLAG_UPDATED;
}
}
if y > 0 && self.rand() % 5 == 0 {
let above = grid.get(x, y - 1);
if above == 0 {
let ai = grid.index(x, y - 1);
grid.materials[ai] = 14;
grid.temps[ai] = 180;
grid.healths[ai] = 4 + (self.rand() % 8) as u8;
grid.flags[ai] |= crate::grid::FLAG_UPDATED;
} else {
let sdir = self.rand_dir();
let sx = x as i32 + sdir;
if grid.in_bounds(sx, y as i32 - 1) && grid.get(sx as u32, y - 1) == 0 {
let si = grid.index(sx as u32, y - 1);
grid.materials[si] = 14;
grid.temps[si] = 180;
grid.healths[si] = 4 + (self.rand() % 8) as u8;
grid.flags[si] |= crate::grid::FLAG_UPDATED;
}
}
}
if y > 0 && self.rand() % 16 == 0 {
let above = grid.get(x, y - 1);
if above == 0 {
let ai = grid.index(x, y - 1);
grid.materials[ai] = 14;
grid.temps[ai] = 150;
grid.healths[ai] = 30 + (self.rand() % 50) as u8;
grid.flags[ai] |= crate::grid::FLAG_UPDATED;
} else {
let sdir = self.rand_dir();
let sx = x as i32 + sdir;
if grid.in_bounds(sx, y as i32 - 1) && grid.get(sx as u32, y - 1) == 0 {
let si = grid.index(sx as u32, y - 1);
grid.materials[si] = 14;
grid.temps[si] = 150;
grid.healths[si] = 30 + (self.rand() % 50) as u8;
grid.flags[si] |= crate::grid::FLAG_UPDATED;
}
}
}
}
}
}
fn update_powder(&mut self, grid: &mut Grid, x: u32, y: u32, _mat: u8) {
if y + 1 >= grid.height {
return;
}
let below = grid.get(x, y + 1);
let bprops = &PROPS[below as usize];
if below == 0 || bprops.is_liquid || bprops.is_gas {
grid.swap(x, y, x, y + 1);
return;
}
let dir = self.rand_dir();
let lx = x as i32 + dir;
let rx = x as i32 - dir;
if grid.in_bounds(lx, y as i32 + 1) && grid.get(lx as u32, y + 1) == 0 {
grid.swap(x, y, lx as u32, y + 1);
} else if grid.in_bounds(rx, y as i32 + 1) && grid.get(rx as u32, y + 1) == 0 {
grid.swap(x, y, rx as u32, y + 1);
}
}
fn update_liquid(&mut self, grid: &mut Grid, x: u32, y: u32, mat: u8) {
if y + 1 >= grid.height {
return;
}
let mprops = &PROPS[mat as usize];
let below = grid.get(x, y + 1);
let bprops = &PROPS[below as usize];
let can_fall = below == 0 || bprops.is_gas
|| (bprops.is_liquid && mprops.density > bprops.density);
if can_fall {
let was_liquid = bprops.is_liquid;
grid.swap(x, y, x, y + 1);
if was_liquid {
let pdir = self.rand_dir();
for dist in 1..=4i32 {
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 {
grid.swap(x, y + 1, sxu, y + 1);
break;
}
}
}
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) {
let db = grid.get(dx as u32, y + 1);
let dp = &PROPS[db as usize];
if db == 0 || dp.is_gas || (dp.is_liquid && mprops.density > dp.density) {
grid.swap(x, y, dx as u32, y + 1);
return;
}
}
}
let max_spread = 10 + (self.rand() % 40) as i32;
for &d in &[dir, -dir] {
for dist in 1..=max_spread {
let sx = x as i32 + d * dist;
if !grid.in_bounds(sx, y as i32) { break; }
let sxu = sx as u32;
let cell = grid.get(sxu, y);
if cell != 0 && !PROPS[cell as usize].is_liquid { break; }
let below_cell = grid.get_signed(sx, y as i32 + 1);
if cell == 0 && below_cell != 0 {
grid.swap(x, y, sxu, y);
return;
}
}
}
}
fn update_solid(&mut self, grid: &mut Grid, x: u32, y: u32, _mat: u8) {
if y + 1 >= grid.height {
return;
}
let below = grid.get(x, y + 1);
let bprops = &PROPS[below as usize];
if below == 0 || bprops.is_liquid || bprops.is_gas {
grid.swap(x, y, x, y + 1);
}
}
fn update_gases(&mut self, grid: &mut Grid, x0: u32, y0: u32, x1: u32, y1: u32) {
for y in y0..y1 {
for x in x0..x1 {
let idx = grid.index(x, y);
let mat = grid.materials[idx];
if mat == 0 { continue; }
let props = &PROPS[mat as usize];
if !props.is_gas { continue; }
if y == 0 { continue; }
let above = grid.get(x, y - 1);
if above == 0 {
grid.swap(x, y, x, y - 1);
continue;
}
let dir = self.rand_dir();
let dx = x as i32 + dir;
if grid.in_bounds(dx, y as i32 - 1) {
let dau = dx as u32;
if grid.get(dau, y - 1) == 0 {
grid.swap(x, y, dau, y - 1);
continue;
}
}
let odx = x as i32 - dir;
if grid.in_bounds(odx, y as i32) && grid.get(odx as u32, y) == 0 {
grid.swap(x, y, odx as u32, y);
}
}
}
}
fn update_ephemeral(&mut self, grid: &mut Grid, x0: u32, y0: u32, x1: u32, y1: u32) {
for y in y0..y1 {
for x in x0..x1 {
let idx = grid.index(x, y);
let mat = grid.materials[idx];
if mat != 14 && mat != 15 {
continue;
}
let hh = grid.healths[idx];
if hh <= 1 {
grid.materials[idx] = 0;
grid.temps[idx] = 0;
grid.flags[idx] |= crate::grid::FLAG_UPDATED;
} else {
grid.healths[idx] = hh - 1;
}
}
}
}
}
+248
View File
@@ -0,0 +1,248 @@
use crate::grid::Grid;
use crate::material::PROPS;
pub struct RenderBuffer {
pub pixels: Vec<u8>,
pub width: u32,
pub height: u32,
light: Vec<u8>,
block: Vec<u8>,
}
impl RenderBuffer {
pub fn new(width: u32, height: u32) -> Self {
let size = (width * height * 4) as usize;
let lsize = (width * height) as usize;
Self {
pixels: vec![0; size],
width,
height,
light: vec![0; lsize],
block: vec![0; lsize],
}
}
pub fn render(&mut self, grid: &Grid, cam_x: i32, cam_y: i32, zoom: f32) {
let rw = self.width as i32;
let rh = self.height as i32;
let total = (rw * rh) as usize;
for i in 0..total {
self.light[i] = 0;
self.block[i] = 0;
}
for py in 0..rh {
let gy = cam_y + ((py as f32 - rh as f32 / 2.0) / zoom) as i32;
for px in 0..rw {
let gx = cam_x + ((px as f32 - rw as f32 / 2.0) / zoom) as i32;
let idx = (py * rw + px) as usize;
if grid.in_bounds(gx, gy) {
let gi = grid.index(gx as u32, gy as u32);
let mat = grid.materials[gi];
if mat > 0 {
let props = &PROPS[mat as usize];
self.light[idx] = props.light_emit;
self.block[idx] = props.light_block;
}
}
}
}
for _pass in 0..12 {
let dir = _pass & 1;
let mut changed = false;
if dir == 0 {
for py in 0..rh {
for px in 0..rw {
if self.spread_light(py, px, rw, rh) {
changed = true;
}
}
}
} else {
for py in (0..rh).rev() {
for px in (0..rw).rev() {
if self.spread_light(py, px, rw, rh) {
changed = true;
}
}
}
}
if !changed {
break;
}
}
for py in 0..rh {
for px in 0..rw {
let gx = cam_x + ((px as f32 - rw as f32 / 2.0) / zoom) as i32;
let gy = cam_y + ((py as f32 - rh as f32 / 2.0) / zoom) as i32;
let color = if grid.in_bounds(gx, gy) {
let gi = grid.index(gx as u32, gy as u32);
let mat = grid.materials[gi];
let li = (py * rw + px) as usize;
let lvl = self.light[li] as u32;
if mat == 0 {
let bg = [20u32, 20, 30];
let glow_r = 255u32;
let glow_g = 160u32;
let glow_b = 40u32;
let t = ((lvl * lvl) / 255).min(255);
let r = (bg[0] * (255 - t) + glow_r * t) / 255;
let g = (bg[1] * (255 - t) + glow_g * t) / 255;
let b = (bg[2] * (255 - t) + glow_b * t) / 255;
[r as u8, g as u8, b as u8, 255]
} else {
let props = &PROPS[mat as usize];
let base = props.color;
let temp = grid.temps[gi];
let t = (temp as f32 / 200.0).min(1.0);
let mut r = (base[0] as f32 + t * 60.0) as u8;
let mut g = (base[1] as f32 * (1.0 - t * 0.4)) as u8;
let mut b = (base[2] as f32 * (1.0 - t * 0.6)) as u8;
if mat == 9 || mat == 6 {
let (fr, fg, fb) = flame_variation(gx as u32, gy as u32, mat);
r = (r as i32 + fr).clamp(0, 255) as u8;
g = (g as i32 + fg).clamp(0, 255) as u8;
b = (b as i32 + fb).clamp(0, 255) as u8;
} else {
let var = texture_offset(gx as u32, gy as u32, mat);
r = (r as i32 + var).clamp(0, 255) as u8;
g = (g as i32 + var).clamp(0, 255) as u8;
b = (b as i32 + var).clamp(0, 255) as u8;
}
if props.light_emit == 0 {
let ambient = 150u32;
let add = lvl;
r = ((r as u32 * ambient >> 8) + add).min(255) as u8;
g = ((g as u32 * ambient >> 8) + add).min(255) as u8;
b = ((b as u32 * ambient >> 8) + add).min(255) as u8;
}
[r, g, b, base[3]]
}
} else {
[20, 20, 30, 255]
};
let pi = ((py * rw + px) * 4) as usize;
self.pixels[pi] = color[0];
self.pixels[pi + 1] = color[1];
self.pixels[pi + 2] = color[2];
self.pixels[pi + 3] = color[3];
}
}
}
fn spread_light(&mut self, py: i32, px: i32, rw: i32, rh: i32) -> bool {
let idx = (py * rw + px) as usize;
let cur = self.light[idx] as u32;
let blk = (self.block[idx] as u32).min(200);
let neighbors: [(i32, i32); 8] = [
(px - 1, py - 1), (px, py - 1), (px + 1, py - 1),
(px - 1, py), (px + 1, py),
(px - 1, py + 1), (px, py + 1), (px + 1, py + 1),
];
let mut best = cur;
for &(nx, ny) in &neighbors {
if nx >= 0 && nx < rw && ny >= 0 && ny < rh {
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;
if nlight > falloff {
let incoming = nlight - falloff;
if incoming > best {
best = incoming;
}
}
}
}
if blk > 0 && blk < 180 && best > cur && best > 4 {
for &(nx, ny) in &neighbors {
if nx >= 0 && nx < rw && ny >= 0 && ny < rh {
let ni = (ny * rw + nx) as usize;
let nlight = self.light[ni] as u32;
if nlight >= best { continue; }
let reflect = best >> 1;
if reflect > nlight {
self.light[ni] = reflect as u8;
}
}
}
}
if best != cur {
self.light[idx] = best as u8;
true
} else {
false
}
}
}
fn texture_offset(x: u32, y: u32, mat: u8) -> i32 {
match mat {
5 => {
let wobble = ((y.wrapping_mul(5) ^ y.wrapping_shr(2)) & 3) as i32 - 1;
let pos = (x as i32).wrapping_add(wobble);
let grain = pos % 5;
let line_id = (pos / 5) as u32;
let darkness = hash(line_id, 0) & 15;
if grain == 0 {
-12 - darkness as i32
} else if grain == 1 {
-5
} else {
let n = hash(x.wrapping_add(y >> 1), y);
((n as i32 - 128) * 7) >> 7
}
}
2 => {
let n = hash(x.wrapping_mul(3) >> 2, y.wrapping_mul(3) >> 2);
((n as i32 - 128) * 20) >> 7
}
1 | 10 => {
let n = hash(x, y);
((n as i32 - 128) * 10) >> 7
}
3 => {
let n = hash(x.wrapping_add(y), y);
((n as i32 - 128) * 6) >> 7
}
11 => {
let n = hash(x, y.wrapping_mul(y));
((n as i32 - 128) * 5) >> 7
}
_ => 0,
}
}
fn hash(x: u32, y: u32) -> u8 {
let h = x.wrapping_mul(374761393)
.wrapping_add(y.wrapping_mul(668265263));
(h ^ h.wrapping_shr(13)).wrapping_mul(1274126177) as u8
}
fn flame_variation(x: u32, y: u32, mat: u8) -> (i32, i32, i32) {
let h = hash(x, y) as i32;
let r_var = ((h - 128) * 6) >> 7;
if mat == 9 {
let g_var = ((h.wrapping_sub(40) as i32 - 128) * 12) >> 7;
let b_var = ((h.wrapping_add(60) as i32 - 128) * 8) >> 7;
(-r_var, g_var, b_var)
} else {
let g_var = ((h - 128) * 8) >> 7;
let b_var = ((h - 128) * 5) >> 7;
(r_var, g_var, b_var)
}
}
+3
View File
@@ -0,0 +1,3 @@
pkg/
dist/
node_modules/
+94
View File
@@ -0,0 +1,94 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>AtomicEngine</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
html, body { width: 100%; height: 100%; overflow: hidden; background: #000; }
canvas {
display: block;
position: absolute;
top: 0;
left: 0;
}
#panel {
position: fixed;
bottom: 8px;
left: 50%;
transform: translateX(-50%);
display: flex;
gap: 2px;
padding: 4px 8px;
background: rgba(0,0,0,0.7);
border-radius: 6px;
z-index: 10;
font-family: monospace;
font-size: 10px;
color: #ccc;
user-select: none;
}
.mat {
display: flex;
flex-direction: column;
align-items: center;
padding: 3px 4px;
border-radius: 4px;
cursor: default;
min-width: 36px;
border: 1px solid transparent;
transition: background 0.1s;
}
.mat.active {
background: rgba(255,255,255,0.15);
border-color: rgba(255,255,255,0.5);
}
.mat .swatch {
width: 14px;
height: 14px;
border-radius: 2px;
margin-bottom: 2px;
}
.mat .label {
text-align: center;
line-height: 1;
}
.mat .key {
color: #888;
}
#info {
position: fixed;
top: 8px;
left: 8px;
color: #fff;
font-family: monospace;
font-size: 11px;
background: rgba(0,0,0,0.6);
padding: 4px 8px;
border-radius: 4px;
z-index: 10;
}
#fps {
position: fixed;
top: 8px;
right: 8px;
color: #0f0;
font-family: monospace;
font-size: 14px;
font-weight: bold;
background: rgba(0,0,0,0.6);
padding: 4px 8px;
border-radius: 4px;
z-index: 10;
}
</style>
</head>
<body>
<canvas id="canvas"></canvas>
<div id="info">AtomicEngine v0.2</div>
<div id="fps">60 FPS</div>
<div id="panel"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>
+1041
View File
File diff suppressed because it is too large Load Diff
+15
View File
@@ -0,0 +1,15 @@
{
"name": "atomic-engine-web",
"version": "0.1.0",
"private": true,
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"preview": "vite preview"
},
"devDependencies": {
"typescript": "^5.5",
"vite": "^5.4"
}
}
+15
View File
@@ -0,0 +1,15 @@
export class Camera {
x = 0;
y = 0;
zoom = 1;
targetZoom = 1;
update(dt: number) {
const speed = 10;
this.zoom += (this.targetZoom - this.zoom) * speed * dt;
}
setZoom(z: number) {
this.targetZoom = Math.max(0.1, Math.min(10, z));
}
}
+97
View File
@@ -0,0 +1,97 @@
import __wbg_init, {
init as wasm_init,
fill_rect,
fill_circle,
simulate,
update_render,
render_buffer_ptr,
render_buffer_len,
render_width,
render_height,
} from "../pkg/atomic_engine.js";
export const M = {
AIR: 0,
STONE: 1,
DIRT: 2,
SAND: 3,
WATER: 4,
WOOD: 5,
LAVA: 6,
PLAYER: 7,
STEAM: 8,
FIRE: 9,
GLASS: 10,
ICE: 11,
OIL: 12,
ACID: 13,
SPARK: 14,
SMOKE: 15,
} as const;
export const MATERIAL_NAMES: Record<number, string> = {
0: "Air",
1: "Stone",
2: "Dirt",
3: "Sand",
4: "Water",
5: "Wood",
6: "Lava",
7: "Player",
8: "Steam",
9: "Fire",
10: "Glass",
11: "Ice",
12: "Oil",
13: "Acid",
14: "Spark",
15: "Smoke",
};
export const MATERIAL_COLORS: Record<number, string> = {
0: "transparent",
1: "#787882",
2: "#654321",
3: "#c2b280",
4: "#1e3cc8",
5: "#5a3c1e",
6: "#ff6400",
7: "#32c832",
8: "#c8c8dc",
9: "#ffb41e",
10: "#b4d2dc",
11: "#b4dcff",
12: "#b48c28",
13: "#64ff3c",
14: "#ffb428",
15: "#a09b96",
};
export interface Engine {
fillRect: (x: number, y: number, w: number, h: number, material: number) => void;
fillCircle: (x: number, y: number, radius: number, material: number) => void;
simulate: (camX: number, camY: number, zoom: number) => void;
updateRender: (camX: number, camY: number, zoom: number) => void;
renderBufferPtr: () => number;
renderBufferLen: () => number;
renderWidth: () => number;
renderHeight: () => number;
memory: WebAssembly.Memory;
}
export async function createEngine(gridWidth: number, gridHeight: number): Promise<Engine> {
const wasm = await __wbg_init();
wasm_init(gridWidth, gridHeight);
return {
fillRect: (x, y, w, h, m) => fill_rect(x, y, w, h, m),
fillCircle: (x, y, r, m) => fill_circle(x, y, r, m),
simulate: (cx: number, cy: number, z: number) => simulate(cx, cy, z),
updateRender: (cx, cy, z) => update_render(cx, cy, z),
renderBufferPtr: () => render_buffer_ptr(),
renderBufferLen: () => render_buffer_len(),
renderWidth: () => render_width(),
renderHeight: () => render_height(),
memory: wasm.memory,
};
}
+25
View File
@@ -0,0 +1,25 @@
export class Input {
keys: Set<string> = new Set();
mouseX = 0;
mouseY = 0;
mouseDown = false;
wheel = 0;
constructor() {
window.addEventListener("keydown", (e) => this.keys.add(e.code));
window.addEventListener("keyup", (e) => this.keys.delete(e.code));
window.addEventListener("mousemove", (e) => {
this.mouseX = e.clientX;
this.mouseY = e.clientY;
});
window.addEventListener("mousedown", () => (this.mouseDown = true));
window.addEventListener("mouseup", () => (this.mouseDown = false));
window.addEventListener("wheel", (e) => {
this.wheel += e.deltaY > 0 ? -1 : 1;
});
}
isDown(key: string): boolean {
return this.keys.has(key);
}
}
+153
View File
@@ -0,0 +1,153 @@
import { createEngine, M, MATERIAL_NAMES, MATERIAL_COLORS } from "./engine";
import { Renderer } from "./renderer";
import { Input } from "./input";
import { Camera } from "./camera";
const GRID_WIDTH = 1024;
const GRID_HEIGHT = 768;
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];
async function main() {
const canvas = document.getElementById("canvas") as HTMLCanvasElement;
const renderer = new Renderer(canvas);
const input = new Input();
const camera = new Camera();
camera.x = GRID_WIDTH / 2;
camera.y = GRID_HEIGHT / 2;
camera.zoom = 3;
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);
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);
}
window.addEventListener("resize", resize);
resize();
let paintMat: number = M.SAND;
function screenToGrid(sx: number, sy: number) {
const mx = (sx / window.innerWidth - 0.5) * rw / camera.zoom + camera.x;
const my = (sy / window.innerHeight - 0.5) * rh / camera.zoom + camera.y;
return { x: Math.floor(mx), y: Math.floor(my) };
}
const panel = document.getElementById("panel")!;
function buildPanel() {
panel.innerHTML = "";
PAINT_MATERIALS.forEach((mat, idx) => {
const div = document.createElement("div");
div.className = "mat" + (mat === paintMat ? " active" : "");
div.dataset.mat = String(mat);
div.innerHTML = `<div class="swatch" style="background:${MATERIAL_COLORS[mat]}"></div><div class="label"><span class="key">${idx}</span> ${MATERIAL_NAMES[mat]}</div>`;
div.addEventListener("click", () => selectMat(mat));
panel.appendChild(div);
});
}
function selectMat(mat: number) {
paintMat = mat;
document.querySelectorAll(".mat").forEach((el) => {
el.classList.toggle("active", (el as HTMLElement).dataset.mat === String(mat));
});
}
buildPanel();
window.addEventListener("keydown", (e) => {
const idx = parseInt(e.key);
if (idx >= 0 && idx < PAINT_MATERIALS.length) {
selectMat(PAINT_MATERIALS[idx]);
}
});
let lastTime = performance.now();
let simAccum = 0;
const simStep = 1 / 60;
let fpsFrames = 0;
let fpsLast = performance.now();
const fpsEl = document.getElementById("fps")!;
function loop(now: number) {
const dt = Math.min((now - lastTime) / 1000, 0.1);
lastTime = now;
fpsFrames++;
if (now - fpsLast >= 500) {
const fps = Math.round(fpsFrames / ((now - fpsLast) / 1000));
fpsEl.textContent = `${fps} FPS`;
fpsFrames = 0;
fpsLast = now;
}
simAccum += dt;
while (simAccum >= simStep) {
simAccum -= simStep;
engine.simulate(
Math.round(camera.x),
Math.round(camera.y),
camera.zoom,
);
}
const speed = 200;
if (input.isDown("KeyA") || input.isDown("ArrowLeft")) camera.x -= speed * dt;
if (input.isDown("KeyD") || input.isDown("ArrowRight")) camera.x += speed * dt;
if (input.isDown("KeyW") || input.isDown("ArrowUp")) camera.y -= speed * dt;
if (input.isDown("KeyS") || input.isDown("ArrowDown")) camera.y += speed * dt;
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);
input.wheel = 0;
}
if (input.mouseDown) {
const g = screenToGrid(input.mouseX, input.mouseY);
engine.fillCircle(g.x, g.y, 6, paintMat);
}
camera.update(dt);
engine.updateRender(
Math.round(camera.x),
Math.round(camera.y),
camera.zoom,
);
const ptr = engine.renderBufferPtr();
const len = engine.renderBufferLen();
const buf = new Uint8Array(engine.memory.buffer, ptr, len).slice(0);
renderer.uploadTexture(buf, rw, rh);
renderer.render();
requestAnimationFrame(loop);
}
requestAnimationFrame(loop);
}
main();
+102
View File
@@ -0,0 +1,102 @@
export class Renderer {
private gl: WebGL2RenderingContext;
private texture: WebGLTexture | null = null;
private program: WebGLProgram | null = null;
private vao: WebGLVertexArrayObject | null = null;
constructor(canvas: HTMLCanvasElement) {
const gl = canvas.getContext("webgl2", {
antialias: false,
alpha: false,
premultipliedAlpha: false,
});
if (!gl) throw new Error("WebGL2 not supported");
this.gl = gl;
this.init();
}
private init() {
const gl = this.gl;
const vs = gl.createShader(gl.VERTEX_SHADER)!;
gl.shaderSource(vs, `#version 300 es
in vec2 a_pos;
in vec2 a_uv;
out vec2 v_uv;
void main() {
gl_Position = vec4(a_pos, 0.0, 1.0);
v_uv = a_uv;
}
`);
gl.compileShader(vs);
const fs = gl.createShader(gl.FRAGMENT_SHADER)!;
gl.shaderSource(fs, `#version 300 es
precision mediump float;
in vec2 v_uv;
out vec4 fragColor;
uniform sampler2D u_tex;
void main() {
fragColor = texture(u_tex, v_uv);
}
`);
gl.compileShader(fs);
this.program = gl.createProgram()!;
gl.attachShader(this.program, vs);
gl.attachShader(this.program, fs);
gl.linkProgram(this.program);
const vertices = new Float32Array([
-1, -1, 0, 1,
1, -1, 1, 1,
1, 1, 1, 0,
-1, -1, 0, 1,
1, 1, 1, 0,
-1, 1, 0, 0,
]);
this.vao = gl.createVertexArray();
gl.bindVertexArray(this.vao);
const buf = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, buf);
gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW);
const stride = 4 * 4;
gl.enableVertexAttribArray(0);
gl.vertexAttribPointer(0, 2, gl.FLOAT, false, stride, 0);
gl.enableVertexAttribArray(1);
gl.vertexAttribPointer(1, 2, gl.FLOAT, false, stride, 2 * 4);
this.texture = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, this.texture);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
}
uploadTexture(data: Uint8Array, width: number, height: number) {
const gl = this.gl;
gl.bindTexture(gl.TEXTURE_2D, this.texture);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, width, height, 0, gl.RGBA, gl.UNSIGNED_BYTE, data);
}
render() {
const gl = this.gl;
gl.clearColor(0.0, 0.0, 0.0, 1.0);
gl.clear(gl.COLOR_BUFFER_BIT);
gl.useProgram(this.program);
gl.bindVertexArray(this.vao);
gl.drawArrays(gl.TRIANGLES, 0, 6);
}
resize(cssWidth: number, cssHeight: number, dpr: number) {
const canvas = this.gl.canvas as HTMLCanvasElement;
canvas.width = cssWidth * dpr;
canvas.height = cssHeight * dpr;
canvas.style.width = cssWidth + "px";
canvas.style.height = cssHeight + "px";
this.gl.viewport(0, 0, canvas.width, canvas.height);
}
}
+15
View File
@@ -0,0 +1,15 @@
{
"compilerOptions": {
"target": "ES2020",
"module": "ESNext",
"moduleResolution": "bundler",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"outDir": "./dist",
"rootDir": "./src",
"sourceMap": true
},
"include": ["src"]
}
+13
View File
@@ -0,0 +1,13 @@
import { defineConfig } from "vite";
export default defineConfig({
server: {
headers: {
"Cross-Origin-Opener-Policy": "same-origin",
"Cross-Origin-Embedder-Policy": "require-corp",
},
},
build: {
target: "es2020",
},
});