use engine_core::{Assets, Scene};
use image::RgbImage;
#[derive(Copy, Clone)]
struct Vec3 {
x: f32,
y: f32,
z: f32,
}
impl Vec3 {
fn new(x: f32, y: f32, z: f32) -> Self {
Self { x, y, z }
}
fn add(self, b: Vec3) -> Vec3 {
Vec3::new(self.x + b.x, self.y + b.y, self.z + b.z)
}
fn sub(self, b: Vec3) -> Vec3 {
Vec3::new(self.x - b.x, self.y - b.y, self.z - b.z)
}
fn mul(self, k: f32) -> Vec3 {
Vec3::new(self.x * k, self.y * k, self.z * k)
}
fn abs(self) -> Vec3 {
Vec3::new(self.x.abs(), self.y.abs(), self.z.abs())
}
fn dot(self, b: Vec3) -> f32 {
self.x * b.x + self.y * b.y + self.z * b.z
}
fn len(self) -> f32 {
self.dot(self).sqrt()
}
fn norm(self) -> Vec3 {
let l = self.len();
if l > 1e-8 {
self.mul(1.0 / l)
} else {
self
}
}
}
#[derive(Copy, Clone)]
struct Vec2 {
x: f32,
y: f32,
}
impl Vec2 {
fn new(x: f32, y: f32) -> Self {
Self { x, y }
}
fn add(self, b: Vec2) -> Vec2 {
Vec2::new(self.x + b.x, self.y + b.y)
}
fn mul(self, k: f32) -> Vec2 {
Vec2::new(self.x * k, self.y * k)
}
}
#[derive(Copy, Clone)]
struct ProjVert {
x: f32,
y: f32,
z: f32,
}
#[derive(Copy, Clone)]
struct ShadedVert {
p: ProjVert,
invz: f32,
pos_over_z: Vec3,
n_over_z: Vec3,
uv_over_z: Vec2,
}
#[derive(Copy, Clone)]
struct Camera {
pos: Vec3,
rot: [f32; 3],
w: u32,
h: u32,
f: f32,
aspect: f32,
near: f32,
}
struct RenderTarget<'a> {
color: &'a mut [[f32; 3]],
zbuf: &'a mut [f32],
w: u32,
h: u32,
}
#[derive(Copy, Clone)]
struct ShadeParams {
base_rgb: [f32; 3],
material_id: u8,
cam_pos: Vec3,
light_dir: Vec3,
}
pub fn render_scene(scene: &Scene, assets: &Assets, out_path: &str) {
let out_w = 800u32;
let out_h = 480u32;
let ssaa = 2u32;
let w = out_w * ssaa;
let h = out_h * ssaa;
let fov = 60.0f32.to_radians();
let aspect = w as f32 / h as f32;
let f = 1.0 / (fov * 0.5).tan();
let cam_i = scene.find("camera");
let cam_pos = cam_i
.map(|i| {
let p = scene.world_pos(i);
Vec3::new(p[0], p[1], p[2])
})
.unwrap_or(Vec3::new(0.0, 0.0, 6.0));
let cam_rot = cam_i.map(|i| scene.nodes[i].r).unwrap_or([0.0, 0.0, 0.0]);
let light_i = scene.find("light");
let light_rot = light_i
.map(|i| scene.nodes[i].r)
.unwrap_or([-0.9, 0.6, 0.0]);
let light_dir = rotate_zyx(light_rot, Vec3::new(0.0, 0.0, -1.0)).norm();
let bg_top = srgb8_to_linear([15, 23, 42]);
let bg_bot = srgb8_to_linear([30, 41, 59]);
let mut color = vec![[0.0f32; 3]; (w * h) as usize];
for y in 0..h {
let t = y as f32 / (h - 1) as f32;
let c = [
bg_top[0] * (1.0 - t) + bg_bot[0] * t,
bg_top[1] * (1.0 - t) + bg_bot[1] * t,
bg_top[2] * (1.0 - t) + bg_bot[2] * t,
];
let row = (y * w) as usize;
for x in 0..w {
color[row + x as usize] = c;
}
}
let mut zbuf = vec![f32::INFINITY; (w * h) as usize];
let near = 0.1f32;
let cam = Camera {
pos: cam_pos,
rot: cam_rot,
w,
h,
f,
aspect,
near,
};
{
let mut target = RenderTarget {
color: &mut color,
zbuf: &mut zbuf,
w,
h,
};
for (i, n) in scene.nodes.iter().enumerate() {
if n.id == "root" || n.id == "camera" || n.id == "light" {
continue;
}
let base_srgb = match n.id.as_str() {
"player" => [34, 197, 94],
"door" => [234, 179, 8],
"trigger" => [59, 130, 246],
"ground" => [148, 163, 184],
"mesh1" => [96, 165, 250],
_ => [147, 197, 253],
};
let material_id = match n.id.as_str() {
"ground" => 0u8,
"door" => 1u8,
"player" => 2u8,
"trigger" => 3u8,
_ => 4u8,
};
let shade = ShadeParams {
base_rgb: srgb8_to_linear(base_srgb),
material_id,
cam_pos: cam.pos,
light_dir,
};
let world = scene.world_matrix(i);
if let Some(mesh_name) = &n.mesh {
let Some(mesh) = assets.get_mesh(mesh_name) else {
continue;
};
for tri in &mesh.indices {
let i0 = tri[0] as usize;
let i1 = tri[1] as usize;
let i2 = tri[2] as usize;
if i0 >= mesh.positions.len()
|| i1 >= mesh.positions.len()
|| i2 >= mesh.positions.len()
|| i0 >= mesh.normals.len()
|| i1 >= mesh.normals.len()
|| i2 >= mesh.normals.len()
{
continue;
}
let v0 = world.transform_point(mesh.positions[i0]);
let v1 = world.transform_point(mesh.positions[i1]);
let v2 = world.transform_point(mesh.positions[i2]);
let v0w = Vec3::new(v0[0], v0[1], v0[2]);
let v1w = Vec3::new(v1[0], v1[1], v1[2]);
let v2w = Vec3::new(v2[0], v2[1], v2[2]);
let n0 = world.transform_normal(mesh.normals[i0]);
let n1 = world.transform_normal(mesh.normals[i1]);
let n2 = world.transform_normal(mesh.normals[i2]);
let n0w = Vec3::new(n0[0], n0[1], n0[2]).norm();
let n1w = Vec3::new(n1[0], n1[1], n1[2]).norm();
let n2w = Vec3::new(n2[0], n2[1], n2[2]).norm();
let uv0 = mesh.uvs.get(i0).copied().unwrap_or([0.0, 0.0]);
let uv1 = mesh.uvs.get(i1).copied().unwrap_or([0.0, 0.0]);
let uv2 = mesh.uvs.get(i2).copied().unwrap_or([0.0, 0.0]);
let uv0 = Vec2::new(uv0[0], uv0[1]);
let uv1 = Vec2::new(uv1[0], uv1[1]);
let uv2 = Vec2::new(uv2[0], uv2[1]);
let Some(p0) = project(v0w, &cam) else {
continue;
};
let Some(p1) = project(v1w, &cam) else {
continue;
};
let Some(p2) = project(v2w, &cam) else {
continue;
};
let v0s = shaded_vert(p0, v0w, n0w, uv0);
let v1s = shaded_vert(p1, v1w, n1w, uv1);
let v2s = shaded_vert(p2, v2w, n2w, uv2);
draw_triangle_shaded(&mut target, v0s, v1s, v2s, &shade);
}
}
}
}
let mut out = vec![0u8; (out_w * out_h * 3) as usize];
for y in 0..out_h {
for x in 0..out_w {
let mut acc = [0.0f32; 3];
for sy in 0..ssaa {
for sx in 0..ssaa {
let ix = x * ssaa + sx;
let iy = y * ssaa + sy;
let c = color[(iy * w + ix) as usize];
acc[0] += c[0];
acc[1] += c[1];
acc[2] += c[2];
}
}
let inv = 1.0 / (ssaa * ssaa) as f32;
acc[0] *= inv;
acc[1] *= inv;
acc[2] *= inv;
let rgb = linear_to_srgb8(acc);
let o = ((y * out_w + x) * 3) as usize;
out[o] = rgb[0];
out[o + 1] = rgb[1];
out[o + 2] = rgb[2];
}
}
let img = RgbImage::from_raw(out_w, out_h, out).expect("rgb image");
img.save(out_path).expect("save");
}
fn srgb8_to_linear(c: [u8; 3]) -> [f32; 3] {
[
(c[0] as f32 / 255.0).powf(2.2),
(c[1] as f32 / 255.0).powf(2.2),
(c[2] as f32 / 255.0).powf(2.2),
]
}
fn linear_to_srgb8(c: [f32; 3]) -> [u8; 3] {
let r = c[0].clamp(0.0, 1.0).powf(1.0 / 2.2);
let g = c[1].clamp(0.0, 1.0).powf(1.0 / 2.2);
let b = c[2].clamp(0.0, 1.0).powf(1.0 / 2.2);
[
(r * 255.0 + 0.5) as u8,
(g * 255.0 + 0.5) as u8,
(b * 255.0 + 0.5) as u8,
]
}
fn rot_x(a: f32, v: Vec3) -> Vec3 {
let (s, c) = a.sin_cos();
Vec3::new(v.x, c * v.y - s * v.z, s * v.y + c * v.z)
}
fn rot_y(a: f32, v: Vec3) -> Vec3 {
let (s, c) = a.sin_cos();
Vec3::new(c * v.x + s * v.z, v.y, -s * v.x + c * v.z)
}
fn rot_z(a: f32, v: Vec3) -> Vec3 {
let (s, c) = a.sin_cos();
Vec3::new(c * v.x - s * v.y, s * v.x + c * v.y, v.z)
}
fn rotate_zyx(r: [f32; 3], v: Vec3) -> Vec3 {
let v = rot_x(r[0], v);
let v = rot_y(r[1], v);
rot_z(r[2], v)
}
fn rotate_inv_zyx(r: [f32; 3], v: Vec3) -> Vec3 {
let v = rot_z(-r[2], v);
let v = rot_y(-r[1], v);
rot_x(-r[0], v)
}
fn project(pw: Vec3, cam: &Camera) -> Option<ProjVert> {
let v = pw.sub(cam.pos);
let v = rotate_inv_zyx(cam.rot, v);
if v.z >= -cam.near {
return None;
}
let z = -v.z;
let nx = (v.x * cam.f / cam.aspect) / z;
let ny = (v.y * cam.f) / z;
let sx = (nx * 0.5 + 0.5) * (cam.w as f32 - 1.0);
let sy = (1.0 - (ny * 0.5 + 0.5)) * (cam.h as f32 - 1.0);
Some(ProjVert { x: sx, y: sy, z })
}
fn shaded_vert(p: ProjVert, pos: Vec3, n: Vec3, uv: Vec2) -> ShadedVert {
let invz = 1.0 / (p.z.max(1e-6));
ShadedVert {
p,
invz,
pos_over_z: pos.mul(invz),
n_over_z: n.mul(invz),
uv_over_z: uv.mul(invz),
}
}
fn draw_triangle_shaded(
target: &mut RenderTarget<'_>,
v0: ShadedVert,
v1: ShadedVert,
v2: ShadedVert,
shade: &ShadeParams,
) {
let w = target.w;
let h = target.h;
let minx = v0.p.x.min(v1.p.x).min(v2.p.x).floor().max(0.0) as i32;
let miny = v0.p.y.min(v1.p.y).min(v2.p.y).floor().max(0.0) as i32;
let maxx = v0.p.x.max(v1.p.x).max(v2.p.x).ceil().min((w - 1) as f32) as i32;
let maxy = v0.p.y.max(v1.p.y).max(v2.p.y).ceil().min((h - 1) as f32) as i32;
let area = edge(v0.p.x, v0.p.y, v1.p.x, v1.p.y, v2.p.x, v2.p.y);
if area.abs() < 1e-10 {
return;
}
let inv_area = 1.0 / area;
let l = shade.light_dir.mul(-1.0).norm();
let fog_color = srgb8_to_linear([34, 62, 96]);
let fog_near = 10.0f32;
let fog_far = 40.0f32;
for y in miny..=maxy {
for x in minx..=maxx {
let fx = x as f32 + 0.5;
let fy = y as f32 + 0.5;
let w0 = edge(v1.p.x, v1.p.y, v2.p.x, v2.p.y, fx, fy) * inv_area;
let w1 = edge(v2.p.x, v2.p.y, v0.p.x, v0.p.y, fx, fy) * inv_area;
let w2 = 1.0 - w0 - w1;
if !(w0 >= 0.0 && w1 >= 0.0 && w2 >= 0.0) {
continue;
}
let invz = w0 * v0.invz + w1 * v1.invz + w2 * v2.invz;
if invz <= 1e-10 {
continue;
}
let z = 1.0 / invz;
let idx = (y as u32 * w + x as u32) as usize;
if z >= target.zbuf[idx] {
continue;
}
let pos = v0
.pos_over_z
.mul(w0)
.add(v1.pos_over_z.mul(w1))
.add(v2.pos_over_z.mul(w2))
.mul(z);
let n = v0
.n_over_z
.mul(w0)
.add(v1.n_over_z.mul(w1))
.add(v2.n_over_z.mul(w2))
.mul(z)
.norm();
let uv = v0
.uv_over_z
.mul(w0)
.add(v1.uv_over_z.mul(w1))
.add(v2.uv_over_z.mul(w2))
.mul(z);
let albedo = material_albedo(shade.material_id, pos, n, uv, shade.base_rgb);
let vdir = shade.cam_pos.sub(pos).norm();
let ndotl = n.dot(l).max(0.0);
let halfv = l.add(vdir).norm();
let spec_pow = if shade.material_id == 1 { 96.0 } else { 48.0 };
let spec_str = if shade.material_id == 0 { 0.05 } else { 0.18 };
let spec = spec_str * n.dot(halfv).max(0.0).powf(spec_pow);
let ambient = if shade.material_id == 0 { 0.22 } else { 0.16 };
let diffuse = 0.98 * ndotl;
let mut rgb = [
albedo[0] * (ambient + diffuse) + spec,
albedo[1] * (ambient + diffuse) + spec,
albedo[2] * (ambient + diffuse) + spec,
];
let fog_t = ((z - fog_near) / (fog_far - fog_near)).clamp(0.0, 1.0);
rgb[0] = rgb[0] * (1.0 - fog_t) + fog_color[0] * fog_t;
rgb[1] = rgb[1] * (1.0 - fog_t) + fog_color[1] * fog_t;
rgb[2] = rgb[2] * (1.0 - fog_t) + fog_color[2] * fog_t;
target.zbuf[idx] = z;
target.color[idx] = rgb;
}
}
}
fn edge(ax: f32, ay: f32, bx: f32, by: f32, cx: f32, cy: f32) -> f32 {
(cx - ax) * (by - ay) - (cy - ay) * (bx - ax)
}
fn material_albedo(material_id: u8, pos: Vec3, n: Vec3, uv: Vec2, base: [f32; 3]) -> [f32; 3] {
let p = Vec3::new(pos.x, pos.y, pos.z);
if material_id == 1 || material_id == 2 || material_id == 3 {
let scale = if material_id == 1 { 6.0 } else { 10.0 };
let c = tex2(uv.x, uv.y, scale, material_id);
[
c[0] * 0.8 + base[0] * 0.2,
c[1] * 0.8 + base[1] * 0.2,
c[2] * 0.8 + base[2] * 0.2,
]
} else {
let na = n.abs();
let mut w = Vec3::new(na.x, na.y, na.z);
let sum = (w.x + w.y + w.z).max(1e-6);
w = w.mul(1.0 / sum);
let scale = if material_id == 0 { 0.35 } else { 0.9 };
let c_xy = tex2(p.x, p.y, scale, material_id);
let c_xz = tex2(p.x, p.z, scale, material_id);
let c_yz = tex2(p.y, p.z, scale, material_id);
let tri = [
c_yz[0] * w.x + c_xz[0] * w.y + c_xy[0] * w.z,
c_yz[1] * w.x + c_xz[1] * w.y + c_xy[1] * w.z,
c_yz[2] * w.x + c_xz[2] * w.y + c_xy[2] * w.z,
];
if material_id == 0 {
[
tri[0] * 0.95 + base[0] * 0.05,
tri[1] * 0.95 + base[1] * 0.05,
tri[2] * 0.95 + base[2] * 0.05,
]
} else {
[
tri[0] * 0.75 + base[0] * 0.25,
tri[1] * 0.75 + base[1] * 0.25,
tri[2] * 0.75 + base[2] * 0.25,
]
}
}
}
fn tex2(u: f32, v: f32, scale: f32, material_id: u8) -> [f32; 3] {
let uu = u * scale;
let vv = v * scale;
let iu = uu.floor() as i32;
let iv = vv.floor() as i32;
let checker = ((iu ^ iv) & 1) as f32;
let fu = uu - uu.floor();
let fv = vv - vv.floor();
let edge_u = (fu * (1.0 - fu) * 4.0).clamp(0.0, 1.0);
let edge_v = (fv * (1.0 - fv) * 4.0).clamp(0.0, 1.0);
let edge = (edge_u * edge_v).powf(0.6);
let n = (uu * 1.7).sin() * (vv * 1.3).cos();
let noise = (0.5 + 0.5 * n) * 0.12;
if material_id == 0 {
let grass = [0.10 + noise, 0.34 + noise, 0.14 + noise];
let dirt = [0.16 + noise, 0.12 + noise, 0.08 + noise];
let mix = (0.35 + 0.65 * edge).clamp(0.0, 1.0);
[
(grass[0] * (1.0 - checker) + dirt[0] * checker) * mix,
(grass[1] * (1.0 - checker) + dirt[1] * checker) * mix,
(grass[2] * (1.0 - checker) + dirt[2] * checker) * mix,
]
} else {
let a = [0.24 + noise, 0.27 + noise, 0.32 + noise];
let b = [0.40 + noise, 0.42 + noise, 0.46 + noise];
let m = 0.35 + 0.65 * edge;
[
(a[0] * (1.0 - checker) + b[0] * checker) * m,
(a[1] * (1.0 - checker) + b[1] * checker) * m,
(a[2] * (1.0 - checker) + b[2] * checker) * m,
]
}
}