pub mod camera;
pub mod color;
pub mod depth;
pub mod light;
pub mod material;
pub mod photon;
pub mod poly;
pub mod raster;
pub mod shapes;
pub mod toon;
pub mod vtex;
#[cfg(target_arch = "wasm32")]
pub mod audio_web;
#[cfg(target_arch = "wasm32")]
pub mod webgl;
#[cfg(feature = "gpu")]
pub mod wgpu_raster;
pub use camera::Camera3D;
pub use depth::DepthQueue;
pub use light::Light;
pub use material::LingMaterial;
pub use toon::ToonConfig;
#[derive(Clone, Copy)]
pub struct ShadowParams {
pub base: f32,
pub grow: f32,
pub alpha: f32,
pub fade: f32,
pub soft: f32,
}
impl Default for ShadowParams {
fn default() -> Self {
Self { base: 14.0, grow: 0.6, alpha: 0.55, fade: 0.012, soft: 0.45 }
}
}
#[cfg(not(target_arch = "wasm32"))]
pub struct GfxState {
pub window: Option<minifb::Window>,
pub buffer: Vec<u32>,
pub distort_buf: Vec<u32>,
pub width: usize,
pub height: usize,
pub color: u32,
pub camera: Camera3D,
pub lights: Vec<Light>,
pub ambient: f32,
pub depth_queue: DepthQueue,
pub mouse_dx: f32,
pub mouse_dy: f32,
pub last_mx: f32,
pub last_my: f32,
pub mouse_captured: bool,
pub shade_mode: u8,
pub shade: ling_graphics::shading::ShadeParams,
pub blend: u8,
pub alpha: f32,
pub shadow: ShadowParams,
pub linear_blend: bool,
pub grad_oklab: bool,
pub depth_test: bool,
pub depth_buf: Vec<f32>,
pub zbuf_needs_clear: bool,
pub fog_color: u32,
pub fog_start: f32,
pub fog_end: f32,
pub flat_shade: bool,
pub edge_set: poly::EdgeSet,
pub material: Option<LingMaterial>,
pub toon: ToonConfig,
pub meshes: Vec<Vec<([f32; 9], u32)>>,
pub mesh_capture: Option<Vec<([f32; 9], u32)>>,
pub mesh_free: Vec<usize>,
pub mesh_cache: std::collections::HashMap<i64, usize>,
}
#[cfg(not(target_arch = "wasm32"))]
impl GfxState {
pub fn new() -> Self {
Self {
window: None,
buffer: Vec::new(),
distort_buf: Vec::new(),
width: 0,
height: 0,
color: 0x00FF_FFFF,
camera: Camera3D::default(),
lights: Vec::new(),
ambient: 0.15,
depth_queue: DepthQueue::default(),
mouse_dx: 0.0,
mouse_dy: 0.0,
last_mx: f32::NAN,
last_my: f32::NAN,
mouse_captured: false,
shade_mode: 2,
shade: ling_graphics::shading::ShadeParams::default(),
blend: 0,
alpha: 1.0,
shadow: ShadowParams::default(),
linear_blend: false,
grad_oklab: true,
depth_test: false,
depth_buf: Vec::new(),
zbuf_needs_clear: true,
fog_color: 0x0000_0000,
fog_start: 0.0,
fog_end: 0.0,
flat_shade: false,
edge_set: poly::EdgeSet::default(),
material: None,
toon: ToonConfig::default(),
meshes: Vec::new(),
mesh_capture: None,
mesh_free: Vec::new(),
mesh_cache: std::collections::HashMap::new(),
}
}
#[inline]
pub fn fog_apply(&self, color: u32, depth: f32) -> u32 {
if self.fog_end <= 0.0 {
return color;
}
let span = self.fog_end - self.fog_start;
if span <= 0.0 {
return color;
}
let f = ((depth - self.fog_start) / span).clamp(0.0, 1.0);
if f <= 0.0 {
return color;
}
let lerp = |a: u32, b: u32| -> u32 { (a as f32 + (b as f32 - a as f32) * f) as u32 & 0xff };
let r = lerp((color >> 16) & 0xff, (self.fog_color >> 16) & 0xff);
let g = lerp((color >> 8) & 0xff, (self.fog_color >> 8) & 0xff);
let b = lerp(color & 0xff, self.fog_color & 0xff);
(r << 16) | (g << 8) | b
}
pub fn sync_projection(&mut self) {
self.camera.cx = self.width as f32 / 2.0;
self.camera.cy = self.height as f32 / 2.0;
self.camera.focal = self.height as f32;
self.camera.zdist = 5.0;
}
pub fn toon_post_process(&mut self) {
let w = self.width;
let h = self.height;
if self.buffer.len() < w * h { return; }
toon::apply(&self.toon, &mut self.buffer, &self.depth_buf, w, h);
}
}
#[cfg(target_arch = "wasm32")]
thread_local! {
static WASM_KEYS_PRESSED: std::cell::RefCell<std::collections::HashSet<String>> =
std::cell::RefCell::new(std::collections::HashSet::new());
static WASM_KEYS_DOWN: std::cell::RefCell<std::collections::HashSet<String>> =
std::cell::RefCell::new(std::collections::HashSet::new());
}
#[cfg(target_arch = "wasm32")]
pub fn wasm_key_down(key: &str) {
let key = normalize_key(key);
WASM_KEYS_DOWN.with(|keys_down| {
let mut down = keys_down.borrow_mut();
if !down.contains(&key) {
WASM_KEYS_PRESSED.with(|keys_pressed| {
keys_pressed.borrow_mut().insert(key.clone());
});
}
down.insert(key);
});
}
#[cfg(target_arch = "wasm32")]
pub fn wasm_key_up(key: &str) {
let key = normalize_key(key);
WASM_KEYS_DOWN.with(|keys_down| {
keys_down.borrow_mut().remove(&key);
});
}
#[cfg(target_arch = "wasm32")]
pub fn audio_resume() {
audio_web::resume();
}
#[cfg(target_arch = "wasm32")]
pub fn wasm_clear_frame_keys() {
WASM_KEYS_PRESSED.with(|keys| {
keys.borrow_mut().clear();
});
}
#[cfg(target_arch = "wasm32")]
pub fn wasm_is_key_pressed(key: &str) -> bool {
let key = normalize_key(key);
WASM_KEYS_PRESSED.with(|keys| {
keys.borrow().contains(&key)
})
}
#[cfg(target_arch = "wasm32")]
fn normalize_key(key: &str) -> String {
match key {
" " => "space".to_string(),
"ArrowUp" => "up".to_string(),
"ArrowDown" => "down".to_string(),
"ArrowLeft" => "left".to_string(),
"ArrowRight" => "right".to_string(),
"Enter" => "enter".to_string(),
"Escape" => "escape".to_string(),
"Shift" | "ShiftLeft" | "ShiftRight" => "shift".to_string(),
"Control" | "ControlLeft" | "ControlRight" => "ctrl".to_string(),
"Alt" | "AltLeft" | "AltRight" => "alt".to_string(),
"Tab" => "tab".to_string(),
"Backspace" => "backspace".to_string(),
_ => key.to_lowercase(),
}
}
#[cfg(target_arch = "wasm32")]
pub struct GfxState {
pub width: usize,
pub height: usize,
pub color: u32,
pub fill_r: f32,
pub fill_g: f32,
pub fill_b: f32,
pub camera: Camera3D,
pub lights: Vec<Light>,
pub ambient: f32,
pub depth_queue: DepthQueue,
pub shade_mode: u8,
pub shade: ling_graphics::shading::ShadeParams,
pub buffer: Vec<u32>,
pub distort_buf: Vec<u32>,
pub blend: u8,
pub alpha: f32,
pub shadow: ShadowParams,
pub linear_blend: bool,
pub grad_oklab: bool,
pub depth_test: bool,
pub depth_buf: Vec<f32>,
pub zbuf_needs_clear: bool,
pub fog_color: u32,
pub fog_start: f32,
pub fog_end: f32,
pub flat_shade: bool,
pub keys_pressed: std::collections::HashSet<String>,
pub keys_down: std::collections::HashSet<String>,
pub edge_set: poly::EdgeSet,
pub material: Option<LingMaterial>,
pub toon: ToonConfig,
pub meshes: Vec<Vec<([f32; 9], u32)>>,
pub mesh_capture: Option<Vec<([f32; 9], u32)>>,
pub mesh_free: Vec<usize>,
pub mesh_cache: std::collections::HashMap<i64, usize>,
}
#[cfg(target_arch = "wasm32")]
impl GfxState {
pub fn new() -> Self {
Self {
width: 800,
height: 600,
color: 0x00FF_FFFF,
fill_r: 0.0,
fill_g: 0.0,
fill_b: 0.0,
camera: Camera3D::default(),
lights: Vec::new(),
ambient: 0.15,
depth_queue: DepthQueue::default(),
shade_mode: 2,
shade: ling_graphics::shading::ShadeParams::default(),
buffer: vec![0u32; 800 * 600],
distort_buf: Vec::new(),
blend: 0,
alpha: 1.0,
shadow: ShadowParams::default(),
linear_blend: false,
grad_oklab: true,
depth_test: false,
depth_buf: Vec::new(),
zbuf_needs_clear: true,
fog_color: 0x0000_0000,
fog_start: 0.0,
fog_end: 0.0,
flat_shade: false,
keys_pressed: std::collections::HashSet::new(),
keys_down: std::collections::HashSet::new(),
edge_set: poly::EdgeSet::default(),
material: None,
toon: ToonConfig::default(),
meshes: Vec::new(),
mesh_capture: None,
mesh_free: Vec::new(),
mesh_cache: std::collections::HashMap::new(),
}
}
pub fn clear_frame_keys(&mut self) {
self.keys_pressed.clear();
}
pub fn on_key_down(&mut self, key: String) {
if !self.keys_down.contains(&key) {
self.keys_pressed.insert(key.clone());
}
self.keys_down.insert(key);
}
pub fn on_key_up(&mut self, key: String) {
self.keys_down.remove(&key);
}
#[inline]
pub fn fog_apply(&self, color: u32, depth: f32) -> u32 {
if self.fog_end <= 0.0 {
return color;
}
let span = self.fog_end - self.fog_start;
if span <= 0.0 {
return color;
}
let f = ((depth - self.fog_start) / span).clamp(0.0, 1.0);
if f <= 0.0 {
return color;
}
let lerp = |a: u32, b: u32| -> u32 { (a as f32 + (b as f32 - a as f32) * f) as u32 & 0xff };
let r = lerp((color >> 16) & 0xff, (self.fog_color >> 16) & 0xff);
let g = lerp((color >> 8) & 0xff, (self.fog_color >> 8) & 0xff);
let b = lerp(color & 0xff, self.fog_color & 0xff);
(r << 16) | (g << 8) | b
}
pub fn sync_projection(&mut self) {
self.camera.cx = self.width as f32 / 2.0;
self.camera.cy = self.height as f32 / 2.0;
self.camera.focal = self.height as f32;
self.camera.zdist = 5.0;
}
pub fn toon_post_process(&mut self) {
let w = self.width;
let h = self.height;
if self.buffer.len() < w * h { return; }
toon::apply(&self.toon, &mut self.buffer, &self.depth_buf, w, h);
}
}
impl GfxState {
#[inline]
pub fn submit_triangle(
&mut self,
ax: f32, ay: f32, az: f32,
bx: f32, by: f32, bz: f32,
cx: f32, cy: f32, cz: f32,
) {
let ux = bx - ax;
let uy = by - ay;
let uz = bz - az;
let vx = cx - ax;
let vy = cy - ay;
let vz = cz - az;
let normal = [uy * vz - uz * vy, uz * vx - ux * vz, ux * vy - uy * vx];
let (c0, c1, c2) = if self.flat_shade {
(self.color, self.color, self.color)
} else {
crate::gfx::light::compute_lit_color_vertices(
self.color,
normal,
[ax, ay, az],
[bx, by, bz],
[cx, cy, cz],
&self.lights,
self.ambient,
)
};
let near = -self.camera.zdist + 0.05;
let vw = [
(ax, ay, az, self.camera.depth(ax, ay, az), c0),
(bx, by, bz, self.camera.depth(bx, by, bz), c1),
(cx, cy, cz, self.camera.depth(cx, cy, cz), c2),
];
let mut poly: [(f32, f32, f32, u32); 4] = [(0.0, 0.0, 0.0, 0); 4];
let mut pn = 0usize;
let mut ei = 0;
while ei < 3 {
let a = vw[ei];
let b = vw[(ei + 1) % 3];
let ain = a.3 > near;
let bin = b.3 > near;
if ain && pn < 4 {
poly[pn] = (a.0, a.1, a.2, a.4);
pn += 1;
}
if ain != bin && pn < 4 {
let tt = (near - a.3) / (b.3 - a.3);
poly[pn] = (
a.0 + (b.0 - a.0) * tt,
a.1 + (b.1 - a.1) * tt,
a.2 + (b.2 - a.2) * tt,
crate::gfx::light::lerp_color(a.4, b.4, tt),
);
pn += 1;
}
ei += 1;
}
if pn < 3 {
return;
}
let mut proj: [(f32, f32, f32, u32); 4] = [(0.0, 0.0, 0.0, 0); 4];
let mut pi = 0;
while pi < pn {
let (sx, sy, sz) = self.camera.project(poly[pi].0, poly[pi].1, poly[pi].2);
let fc = self.fog_apply(poly[pi].3, sz);
proj[pi] = (sx, sy, sz, fc);
pi += 1;
}
let mut fk = 1;
while fk + 1 < pn {
self.depth_queue.push_triangle_g_zv(
proj[0].0, proj[0].1, proj[0].2, proj[0].3,
proj[fk].0, proj[fk].1, proj[fk].2, proj[fk].3,
proj[fk + 1].0, proj[fk + 1].1, proj[fk + 1].2, proj[fk + 1].3,
3,
);
fk += 1;
}
}
pub fn mesh_register(&mut self, tris: Vec<([f32; 9], u32)>) -> usize {
if let Some(id) = self.mesh_free.pop() {
self.meshes[id] = tris;
id
} else {
let id = self.meshes.len();
self.meshes.push(tris);
id
}
}
pub fn mesh_draw(
&mut self,
id: usize,
ox: f32, oy: f32, oz: f32,
rx: f32, ry: f32, rz: f32,
ux: f32, uy: f32, uz: f32,
s: f32,
use_baked_color: bool,
) {
if id >= self.meshes.len() {
return;
}
let fx = ry * uz - rz * uy;
let fy = rz * ux - rx * uz;
let fz = rx * uy - ry * ux;
let pen = self.color;
let mesh = std::mem::take(&mut self.meshes[id]);
for (t, col) in &mesh {
if use_baked_color {
self.color = *col;
}
let wx0 = ox + s * (t[0] * rx + t[1] * ux + t[2] * fx);
let wy0 = oy + s * (t[0] * ry + t[1] * uy + t[2] * fy);
let wz0 = oz + s * (t[0] * rz + t[1] * uz + t[2] * fz);
let wx1 = ox + s * (t[3] * rx + t[4] * ux + t[5] * fx);
let wy1 = oy + s * (t[3] * ry + t[4] * uy + t[5] * fy);
let wz1 = oz + s * (t[3] * rz + t[4] * uz + t[5] * fz);
let wx2 = ox + s * (t[6] * rx + t[7] * ux + t[8] * fx);
let wy2 = oy + s * (t[6] * ry + t[7] * uy + t[8] * fy);
let wz2 = oz + s * (t[6] * rz + t[7] * uz + t[8] * fz);
self.submit_triangle(wx0, wy0, wz0, wx1, wy1, wz1, wx2, wy2, wz2);
}
self.color = pen;
self.meshes[id] = mesh;
}
}