mod post;
pub(crate) mod prepare;
mod raster;
pub(crate) mod reflection;
mod shade;
pub(crate) mod shadow;
use std::sync::atomic::{AtomicU64, Ordering};
use glam::{Vec2, Vec4};
use rayon::prelude::*;
pub use post::{Bloom, ToneMapping};
use crate::{Camera, Scene};
use prepare::{Viewport, World};
use raster::Blended;
use reflection::ReflectionMap;
use shade::ShadeContext;
use shadow::ShadowMap;
#[derive(Debug, Clone, PartialEq)]
pub struct Renderer {
pub width: u32,
pub height: u32,
pub antialias: bool,
pub exposure: f32,
pub tone_mapping: ToneMapping,
pub bloom: Option<Bloom>,
pub gpu: bool,
}
#[derive(Debug, Clone)]
pub struct Pixels {
pub width: u32,
pub height: u32,
pub rgba: Vec<u8>,
}
impl Pixels {
pub fn get(&self, x: u32, y: u32) -> [u8; 4] {
straight(self.rgba.as_chunks::<4>().0[(y * self.width + x) as usize])
}
pub fn to_straight_rgba(&self) -> Vec<u8> {
self.rgba
.as_chunks::<4>()
.0
.iter()
.flat_map(|p| straight(*p))
.collect()
}
pub fn into_image(self) -> fframes_media::ImageData<'static> {
use fframes_media::{ImageData, ImageMetadata};
static NEXT_ID: AtomicU64 = AtomicU64::new(0);
let id = format!(
"fframes_three_d:{}",
NEXT_ID.fetch_add(1, Ordering::Relaxed)
);
let (width, height) = (self.width, self.height);
let metadata = ImageMetadata { width, height };
#[cfg(not(target_arch = "wasm32"))]
return ImageData::new_from_raw_data(
std::sync::Arc::new(usvgr::PreloadedImageData {
data: std::borrow::Cow::Owned(self.rgba),
width,
height,
id: id.clone(),
}),
id,
metadata,
);
#[cfg(target_arch = "wasm32")]
{
use base64::Engine;
let mut png_bytes = Vec::new();
let mut encoder = png::Encoder::new(&mut png_bytes, width, height);
encoder.set_color(png::ColorType::Rgba);
encoder.set_depth(png::BitDepth::Eight);
encoder.set_compression(png::Compression::Fast);
if let Ok(mut writer) = encoder.write_header() {
let _ = writer.write_image_data(&self.to_straight_rgba());
}
let url = format!(
"data:image/png;base64,{}",
base64::engine::general_purpose::STANDARD.encode(&png_bytes)
);
ImageData::new_from_web_source(
fframes_media::OwnedSharedString::new_owned(url.as_str()),
url,
id,
metadata,
)
}
}
}
fn straight([r, g, b, a]: [u8; 4]) -> [u8; 4] {
if a == 0 {
return [0, 0, 0, 0];
}
let un = |c: u8| ((u32::from(c) * 255 + u32::from(a) / 2) / u32::from(a)).min(255) as u8;
[un(r), un(g), un(b), a]
}
impl Renderer {
pub fn new(width: u32, height: u32) -> Self {
#[cfg(target_arch = "wasm32")]
let (width, height) = ((width / 2).max(1), (height / 2).max(1));
Self {
width: width.max(1),
height: height.max(1),
antialias: true,
exposure: 1.0,
tone_mapping: ToneMapping::Aces,
bloom: None,
gpu: cfg!(feature = "gpu"),
}
}
pub fn gpu(mut self) -> Self {
self.gpu = true;
self
}
pub fn cpu(mut self) -> Self {
self.gpu = false;
self
}
pub fn uses_gpu(&self) -> bool {
#[cfg(all(feature = "gpu", not(target_arch = "wasm32")))]
return self.gpu && crate::gpu::gpu().is_some();
#[cfg(not(all(feature = "gpu", not(target_arch = "wasm32"))))]
return false;
}
pub fn bloom(mut self, bloom: Bloom) -> Self {
self.bloom = Some(bloom);
self
}
pub fn exposure(mut self, exposure: f32) -> Self {
self.exposure = exposure;
self
}
pub fn tone_mapping(mut self, tone_mapping: ToneMapping) -> Self {
self.tone_mapping = tone_mapping;
self
}
pub fn antialias(mut self, antialias: bool) -> Self {
self.antialias = antialias;
self
}
pub fn scaled(mut self, scale: f32) -> Self {
self.width = ((self.width as f32 * scale).round() as u32).max(1);
self.height = ((self.height as f32 * scale).round() as u32).max(1);
self
}
pub fn render(&self, scene: &Scene, camera: &Camera) -> fframes_media::ImageData<'static> {
self.image(&[(scene, camera)])
}
pub fn render_pixels(&self, scene: &Scene, camera: &Camera) -> Pixels {
self.pixels(&[(scene, camera)])
}
pub fn render_motion_blur(
&self,
samples: usize,
moment: impl FnMut(f32) -> (Scene, Camera),
) -> fframes_media::ImageData<'static> {
let moments: Vec<(Scene, Camera)> = shutter_offsets(samples).map(moment).collect();
let moments: Vec<(&Scene, &Camera)> = moments.iter().map(|(s, c)| (s, c)).collect();
self.image(&moments)
}
pub fn render_motion_blur_pixels(
&self,
samples: usize,
moment: impl FnMut(f32) -> (Scene, Camera),
) -> Pixels {
let moments: Vec<(Scene, Camera)> = shutter_offsets(samples).map(moment).collect();
let moments: Vec<(&Scene, &Camera)> = moments.iter().map(|(s, c)| (s, c)).collect();
self.pixels(&moments)
}
fn image(&self, moments: &[(&Scene, &Camera)]) -> fframes_media::ImageData<'static> {
#[cfg(all(feature = "gpu", not(target_arch = "wasm32")))]
if self.gpu
&& let Some(image) = crate::gpu::layer(self, moments)
{
return image;
}
self.pixels(moments).into_image()
}
fn pixels(&self, moments: &[(&Scene, &Camera)]) -> Pixels {
#[cfg(all(feature = "gpu", not(target_arch = "wasm32")))]
if self.gpu
&& let Some(gpu) = crate::gpu::gpu()
{
return gpu.render(self, moments);
}
self.cpu_pixels(moments)
}
fn cpu_pixels(&self, moments: &[(&Scene, &Camera)]) -> Pixels {
let mut hdr = moments
.par_iter()
.map(|(scene, camera)| self.render_hdr(scene, camera))
.collect::<Vec<_>>()
.into_iter()
.reduce(|mut sum, hdr| {
sum.iter_mut().zip(&hdr).for_each(|(a, b)| *a += *b);
sum
})
.unwrap_or_default();
if moments.len() > 1 {
let scale = 1.0 / moments.len() as f32;
for p in &mut hdr {
*p *= scale;
}
}
let (width, height) = (self.width as usize, self.height as usize);
if let Some(bloom) = &self.bloom {
post::bloom(&mut hdr, width, height, bloom);
}
Pixels {
width: self.width,
height: self.height,
rgba: post::encode(&hdr, width, height, self.exposure, self.tone_mapping),
}
}
fn render_hdr(&self, scene: &Scene, camera: &Camera) -> Vec<Vec4> {
let (width, height) = (self.width as usize, self.height as usize);
let (mut world, lights) = prepare::build_world(scene);
let shadows: Vec<Option<ShadowMap>> = lights
.shadow_casters
.iter()
.map(|(direction, settings)| ShadowMap::build(*direction, settings, &world))
.collect();
let baked = scene.environment.baked();
let ortho = camera.ortho_height.is_some();
let view_proj = camera.view_projection(width as f32 / height as f32);
let frame = ShadeContext {
lights: &lights,
shadows: &shadows,
environment: &scene.environment,
baked: baked.as_deref(),
fog: scene.fog,
background: &scene.background,
camera_position: camera.position,
ortho_forward: ortho.then(|| camera.forward()),
reflection: None,
viewport: Vec2::new(width as f32, height as f32),
};
let reflection = scene
.reflector
.filter(|_| world.draws.iter().any(|d| d.material.reflection > 0.0))
.map(|reflector| {
let size = (frame.viewport * reflector.scale).ceil().max(Vec2::ONE);
let mirror = reflector.matrix();
let shade = ShadeContext {
camera_position: mirror.transform_point3(camera.position),
ortho_forward: frame.ortho_forward.map(|f| mirror.transform_vector3(f)),
viewport: size,
..frame
};
let viewport = Viewport {
size,
view_proj: view_proj * mirror,
ortho,
mirrored: true,
clip_plane: Some(reflector.plane()),
};
let (hdr, _) = render_view(&mut world, &shade, &viewport, camera, false);
ReflectionMap::new(size.x as usize, size.y as usize, hdr)
});
let shade = ShadeContext {
reflection: reflection.as_ref(),
..frame
};
let viewport = Viewport {
size: frame.viewport,
view_proj,
ortho,
mirrored: false,
clip_plane: None,
};
let (mut hdr, depth) = render_view(&mut world, &shade, &viewport, camera, self.antialias);
if let Some(focus) = camera.focus
&& !ortho
&& focus.blur > 0.0
{
let scale = height as f32 / 1080.0;
post::depth_of_field(
&mut hdr,
&depth,
width,
height,
focus.distance,
focus.blur * scale,
);
}
hdr
}
}
fn shutter_offsets(samples: usize) -> impl Iterator<Item = f32> {
let samples = samples.max(1);
(0..samples).map(move |i| (i as f32 + 0.5) / samples as f32 - 0.5)
}
fn render_view(
world: &mut World,
shade: &ShadeContext,
viewport: &Viewport,
camera: &Camera,
multisample: bool,
) -> (Vec<Vec4>, Vec<f32>) {
let reflection = viewport.mirrored;
let (opaque, blended_tris) = prepare::setup_triangles(world, viewport, |d| {
!reflection || d.material.reflection <= 0.0
});
let sprites = if reflection {
Vec::new()
} else {
prepare::setup_sprites(&world.sprites, camera, viewport)
};
let mut blended: Vec<Blended> = blended_tris
.iter()
.map(Blended::Tri)
.chain(sprites.iter().map(Blended::Sprite))
.collect();
blended.sort_by(|a, b| b.depth().total_cmp(&a.depth()));
raster::rasterize(&raster::Frame {
width: viewport.size.x as usize,
height: viewport.size.y as usize,
streams: &world.streams,
draws: &world.draws,
opaque: &opaque,
blended: &blended,
shade,
inverse_view_proj: viewport.view_proj.inverse(),
multisample,
})
}