Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 41 additions & 20 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,18 @@
//! use glam::Vec3;
//! use a3d::model::load_model;
//! use a3d::render::Framebuffer;
//! use a3d::{render_frame, framebuffer_to_string};
//! use a3d::{render_frame, framebuffer_to_string, RenderParams};
//!
//! let mesh = load_model(Path::new("models/dog.stl")).expect("load model");
//! let mut fb = Framebuffer::new(80, 24);
//! let light = Vec3::new(1.0, -1.0, 0.0).normalize();
//! render_frame(&mut fb, &mesh, 0.0, 0.0, 1.0, light, None);
//! let params = RenderParams {
//! azimuth: 0.0,
//! altitude: 0.0,
//! zoom: 1.0,
//! light_dir: Vec3::new(1.0, -1.0, 0.0).normalize(),
//! fg_override: None,
//! };
//! render_frame(&mut fb, &mesh, &params);
//! print!("{}", framebuffer_to_string(&fb));
//! ```
//!
Expand Down Expand Up @@ -57,17 +63,32 @@ pub fn rotate_x(v: Vec3, cos_a: f32, sin_a: f32) -> Vec3 {
Vec3::new(v.x, v.y * cos_a - v.z * sin_a, v.y * sin_a + v.z * cos_a)
}

/// Camera pose and shading parameters shared by the CPU and GPU renderers.
#[derive(Clone, Copy, Debug)]
pub struct RenderParams {
/// Orbit azimuth in radians.
pub azimuth: f32,
/// Orbit altitude in radians.
pub altitude: f32,
/// Zoom factor applied to projected X/Y (depth is left unscaled).
pub zoom: f32,
/// Direction the scene light points toward (expected normalized).
pub light_dir: Vec3,
/// Optional foreground color override in linear RGB `[0, 1]`; `None` keeps
/// each triangle's own material/vertex color.
pub fg_override: Option<[f32; 3]>,
}

/// Render a single frame into the framebuffer.
/// This is the core rendering function, usable without a terminal for testing.
pub fn render_frame(
fb: &mut Framebuffer,
mesh: &model::Mesh,
azimuth: f32,
altitude: f32,
zoom: f32,
light_dir: Vec3,
fg_override: Option<[f32; 3]>,
) {
pub fn render_frame(fb: &mut Framebuffer, mesh: &model::Mesh, params: &RenderParams) {
let RenderParams {
azimuth,
altitude,
zoom,
light_dir,
fg_override,
} = *params;
let w = fb.width;
let h = fb.height;

Expand Down Expand Up @@ -127,19 +148,19 @@ pub fn render_frame(
}

/// Render a single frame using the GPU compute pipeline.
// Mirrors `render_frame`'s parameter set; a `RenderParams` struct is tracked as
// follow-up cleanup (see issue #3).
#[allow(clippy::too_many_arguments)]
pub fn render_frame_gpu(
fb: &mut Framebuffer,
pipeline: &RasterPipeline,
ctx: &GpuContext,
azimuth: f32,
altitude: f32,
zoom: f32,
light_dir: Vec3,
fg_override: Option<[f32; 3]>,
params: &RenderParams,
) {
let RenderParams {
azimuth,
altitude,
zoom,
light_dir,
fg_override,
} = *params;
let w = fb.width;
let h = fb.height;

Expand Down
15 changes: 10 additions & 5 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ use a3d::gpu::{GpuContext, RasterPipeline};
use a3d::model::load_model;
use a3d::render::Framebuffer;
use a3d::terminal::{InputEvent, TerminalDisplay};
use a3d::{AL_SPEED, AZ_SPEED, render_frame, render_frame_gpu};
use a3d::{AL_SPEED, AZ_SPEED, RenderParams, render_frame, render_frame_gpu};

#[derive(Parser)]
#[command(name = "a3d", about = "GPU-accelerated ASCII 3D renderer")]
Expand Down Expand Up @@ -184,12 +184,17 @@ fn main() {
}

// Render
let params = RenderParams {
azimuth,
altitude,
zoom,
light_dir,
fg_override: fg_color,
};
if let (Some(pipeline), Some(ctx)) = (&gpu_pipeline, &gpu_ctx) {
render_frame_gpu(
&mut fb, pipeline, ctx, azimuth, altitude, zoom, light_dir, fg_color,
);
render_frame_gpu(&mut fb, pipeline, ctx, &params);
} else {
render_frame(&mut fb, &mesh, azimuth, altitude, zoom, light_dir, fg_color);
render_frame(&mut fb, &mesh, &params);
}

let fps_label = if current_fps > 0.0 {
Expand Down
16 changes: 13 additions & 3 deletions tests/empty_mesh.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ use glam::Vec3;
use a3d::gpu::{GpuContext, RasterPipeline};
use a3d::model::Mesh;
use a3d::render::Framebuffer;
use a3d::{framebuffer_to_string, render_frame, render_frame_gpu};
use a3d::{RenderParams, framebuffer_to_string, render_frame, render_frame_gpu};

const LIGHT_DIR: Vec3 = Vec3::new(0.70710677, -0.70710677, 0.0);
const W: usize = 80;
Expand All @@ -23,10 +23,20 @@ fn is_blank(s: &str) -> bool {
s.chars().all(|c| c == ' ' || c == '\n')
}

fn front_view() -> RenderParams {
RenderParams {
azimuth: 0.0,
altitude: 0.0,
zoom: 1.0,
light_dir: LIGHT_DIR,
fg_override: None,
}
}

#[test]
fn cpu_renders_empty_mesh_blank() {
let mut fb = Framebuffer::new(W, H);
render_frame(&mut fb, &empty_mesh(), 0.0, 0.0, 1.0, LIGHT_DIR, None);
render_frame(&mut fb, &empty_mesh(), &front_view());
assert!(
is_blank(&framebuffer_to_string(&fb)),
"empty mesh should render blank on the CPU"
Expand All @@ -42,7 +52,7 @@ fn gpu_builds_and_renders_empty_mesh_without_crashing() {
// Constructing the pipeline used to panic here on a zero-sized buffer.
let pipeline = RasterPipeline::new(&ctx, &empty_mesh(), W as u32, H as u32);
let mut fb = Framebuffer::new(W, H);
render_frame_gpu(&mut fb, &pipeline, &ctx, 0.0, 0.0, 1.0, LIGHT_DIR, None);
render_frame_gpu(&mut fb, &pipeline, &ctx, &front_view());
assert!(
is_blank(&framebuffer_to_string(&fb)),
"empty mesh should render blank on the GPU"
Expand Down
11 changes: 9 additions & 2 deletions tests/gen_snapshots.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ use glam::Vec3;

use a3d::model::load_model;
use a3d::render::Framebuffer;
use a3d::{framebuffer_to_string, render_frame};
use a3d::{RenderParams, framebuffer_to_string, render_frame};

const LIGHT_DIR: Vec3 = Vec3::new(0.70710677, -0.70710677, 0.0);

Expand All @@ -20,7 +20,14 @@ fn render_snapshot(
) -> String {
let mesh = load_model(Path::new(model_path)).expect("model should load");
let mut fb = Framebuffer::new(width, height);
render_frame(&mut fb, &mesh, azimuth, altitude, zoom, LIGHT_DIR, None);
let params = RenderParams {
azimuth,
altitude,
zoom,
light_dir: LIGHT_DIR,
fg_override: None,
};
render_frame(&mut fb, &mesh, &params);
framebuffer_to_string(&fb)
}

Expand Down
13 changes: 10 additions & 3 deletions tests/gpu_compare.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ use a3d::gpu::GpuContext;
use a3d::gpu::RasterPipeline;
use a3d::model::load_model;
use a3d::render::Framebuffer;
use a3d::{framebuffer_to_string, render_frame, render_frame_gpu};
use a3d::{RenderParams, framebuffer_to_string, render_frame, render_frame_gpu};

const LIGHT_DIR: Vec3 = Vec3::new(0.70710677, -0.70710677, 0.0);
const W: usize = 80;
Expand All @@ -19,15 +19,22 @@ const H: usize = 24;
/// gracefully on GPU-less CI runners instead of failing the suite.
fn compare(model: &str, az: f32, al: f32, zoom: f32) -> Option<(String, String, f64)> {
let mesh = load_model(Path::new(model)).expect("model should load");
let params = RenderParams {
azimuth: az,
altitude: al,
zoom,
light_dir: LIGHT_DIR,
fg_override: None,
};

let mut cpu_fb = Framebuffer::new(W, H);
render_frame(&mut cpu_fb, &mesh, az, al, zoom, LIGHT_DIR, None);
render_frame(&mut cpu_fb, &mesh, &params);
let cpu_str = framebuffer_to_string(&cpu_fb);

let ctx = pollster::block_on(GpuContext::new())?;
let pipeline = RasterPipeline::new(&ctx, &mesh, W as u32, H as u32);
let mut gpu_fb = Framebuffer::new(W, H);
render_frame_gpu(&mut gpu_fb, &pipeline, &ctx, az, al, zoom, LIGHT_DIR, None);
render_frame_gpu(&mut gpu_fb, &pipeline, &ctx, &params);
let gpu_str = framebuffer_to_string(&gpu_fb);

let total = cpu_str.chars().count();
Expand Down
11 changes: 9 additions & 2 deletions tests/gpu_resize.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,20 @@ use glam::Vec3;
use a3d::gpu::{GpuContext, RasterPipeline};
use a3d::model::load_model;
use a3d::render::Framebuffer;
use a3d::{framebuffer_to_string, render_frame_gpu};
use a3d::{RenderParams, framebuffer_to_string, render_frame_gpu};

const LIGHT_DIR: Vec3 = Vec3::new(0.70710677, -0.70710677, 0.0);

fn render(pipeline: &RasterPipeline, ctx: &GpuContext, w: usize, h: usize) -> String {
let mut fb = Framebuffer::new(w, h);
render_frame_gpu(&mut fb, pipeline, ctx, 0.0, 0.0, 1.0, LIGHT_DIR, None);
let params = RenderParams {
azimuth: 0.0,
altitude: 0.0,
zoom: 1.0,
light_dir: LIGHT_DIR,
fg_override: None,
};
render_frame_gpu(&mut fb, pipeline, ctx, &params);
framebuffer_to_string(&fb)
}

Expand Down
11 changes: 9 additions & 2 deletions tests/regression.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ use glam::Vec3;

use a3d::model::load_model;
use a3d::render::Framebuffer;
use a3d::{framebuffer_to_string, render_frame};
use a3d::{RenderParams, framebuffer_to_string, render_frame};

const LIGHT_DIR: Vec3 = Vec3::new(0.70710677, -0.70710677, 0.0);

Expand All @@ -19,7 +19,14 @@ fn render_snapshot(
) -> String {
let mesh = load_model(Path::new(model_path)).expect("model should load");
let mut fb = Framebuffer::new(width, height);
render_frame(&mut fb, &mesh, azimuth, altitude, zoom, LIGHT_DIR, None);
let params = RenderParams {
azimuth,
altitude,
zoom,
light_dir: LIGHT_DIR,
fg_override: None,
};
render_frame(&mut fb, &mesh, &params);
framebuffer_to_string(&fb)
}

Expand Down
Loading