use std::collections::HashMap;
use super::{
Viewport, Mat4,
vec3_sub, vec3_dot, vec3_length, clampf, lerpf, saturate,
};
use super::gbuffer::GBuffer;
use super::materials::MaterialSortKey;
#[derive(Debug, Clone)]
pub enum LightType {
Directional {
direction: [f32; 3],
color: [f32; 3],
intensity: f32,
cast_shadows: bool,
},
Point {
position: [f32; 3],
color: [f32; 3],
intensity: f32,
range: f32,
cast_shadows: bool,
},
Spot {
position: [f32; 3],
direction: [f32; 3],
color: [f32; 3],
intensity: f32,
range: f32,
inner_angle: f32,
outer_angle: f32,
cast_shadows: bool,
},
Area {
position: [f32; 3],
normal: [f32; 3],
up: [f32; 3],
width: f32,
height: f32,
color: [f32; 3],
intensity: f32,
},
Ambient {
color: [f32; 3],
intensity: f32,
},
}
impl LightType {
pub fn position(&self) -> Option<[f32; 3]> {
match self {
Self::Directional { .. } | Self::Ambient { .. } => None,
Self::Point { position, .. }
| Self::Spot { position, .. }
| Self::Area { position, .. } => Some(*position),
}
}
pub fn range(&self) -> f32 {
match self {
Self::Directional { .. } | Self::Ambient { .. } => f32::MAX,
Self::Point { range, .. } | Self::Spot { range, .. } => *range,
Self::Area { width, height, .. } => (*width + *height) * 2.0,
}
}
pub fn color(&self) -> [f32; 3] {
match self {
Self::Directional { color, .. }
| Self::Point { color, .. }
| Self::Spot { color, .. }
| Self::Area { color, .. }
| Self::Ambient { color, .. } => *color,
}
}
pub fn intensity(&self) -> f32 {
match self {
Self::Directional { intensity, .. }
| Self::Point { intensity, .. }
| Self::Spot { intensity, .. }
| Self::Area { intensity, .. }
| Self::Ambient { intensity, .. } => *intensity,
}
}
pub fn casts_shadows(&self) -> bool {
match self {
Self::Directional { cast_shadows, .. }
| Self::Point { cast_shadows, .. }
| Self::Spot { cast_shadows, .. } => *cast_shadows,
_ => false,
}
}
pub fn evaluate(&self, surface_pos: [f32; 3]) -> ([f32; 3], f32, [f32; 3]) {
match self {
Self::Directional { direction, color, intensity, .. } => {
let dir = [-direction[0], -direction[1], -direction[2]];
(dir, *intensity, *color)
}
Self::Point { position, color, intensity, range, .. } => {
let to_light = vec3_sub(*position, surface_pos);
let dist = vec3_length(to_light);
if dist > *range || dist < 1e-6 {
return ([0.0, 0.0, 0.0], 0.0, *color);
}
let dir = [to_light[0] / dist, to_light[1] / dist, to_light[2] / dist];
let att = point_attenuation(dist, *range) * *intensity;
(dir, att, *color)
}
Self::Spot {
position, direction, color, intensity, range,
inner_angle, outer_angle, ..
} => {
let to_light = vec3_sub(*position, surface_pos);
let dist = vec3_length(to_light);
if dist > *range || dist < 1e-6 {
return ([0.0, 0.0, 0.0], 0.0, *color);
}
let dir = [to_light[0] / dist, to_light[1] / dist, to_light[2] / dist];
let cos_angle = -vec3_dot(dir, *direction);
let cos_inner = inner_angle.cos();
let cos_outer = outer_angle.cos();
let spot_att = saturate((cos_angle - cos_outer) / (cos_inner - cos_outer).max(1e-6));
let att = point_attenuation(dist, *range) * spot_att * *intensity;
(dir, att, *color)
}
Self::Area { position, color, intensity, .. } => {
let to_light = vec3_sub(*position, surface_pos);
let dist = vec3_length(to_light);
if dist < 1e-6 {
return ([0.0, 0.0, 1.0], *intensity, *color);
}
let dir = [to_light[0] / dist, to_light[1] / dist, to_light[2] / dist];
let att = *intensity / (dist * dist + 1.0);
(dir, att, *color)
}
Self::Ambient { color, intensity } => {
([0.0, 1.0, 0.0], *intensity, *color)
}
}
}
}
fn point_attenuation(distance: f32, range: f32) -> f32 {
let ratio = clampf(distance / range, 0.0, 1.0);
let att_factor = 1.0 - ratio * ratio;
(att_factor * att_factor).max(0.0) / (distance * distance + 1.0)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SortMode {
FrontToBack,
BackToFront,
ByMaterial,
None,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum RenderBucket {
Opaque,
Transparent,
Overlay,
Debug,
Sky,
}
impl RenderBucket {
pub fn default_sort_mode(&self) -> SortMode {
match self {
Self::Opaque => SortMode::FrontToBack,
Self::Transparent => SortMode::BackToFront,
Self::Overlay => SortMode::None,
Self::Debug => SortMode::None,
Self::Sky => SortMode::None,
}
}
}
#[derive(Debug, Clone)]
pub struct RenderItem {
pub id: u64,
pub transform: Mat4,
pub mesh_handle: u64,
pub material_index: u32,
pub sort_key: MaterialSortKey,
pub camera_distance: f32,
pub bucket: RenderBucket,
pub visible: bool,
pub bounds_center: [f32; 3],
pub bounds_radius: f32,
pub instance_count: u32,
pub instance_buffer: u64,
pub vertex_count: u32,
pub index_count: u32,
pub alpha_test: bool,
pub two_sided: bool,
}
impl RenderItem {
pub fn new(id: u64, mesh_handle: u64, material_index: u32) -> Self {
Self {
id,
transform: Mat4::IDENTITY,
mesh_handle,
material_index,
sort_key: MaterialSortKey::default(),
camera_distance: 0.0,
bucket: RenderBucket::Opaque,
visible: true,
bounds_center: [0.0; 3],
bounds_radius: 1.0,
instance_count: 1,
instance_buffer: 0,
vertex_count: 0,
index_count: 0,
alpha_test: false,
two_sided: false,
}
}
pub fn with_transform(mut self, t: Mat4) -> Self {
self.transform = t;
self.bounds_center = [t.cols[3][0], t.cols[3][1], t.cols[3][2]];
self
}
pub fn with_bucket(mut self, b: RenderBucket) -> Self {
self.bucket = b;
self
}
pub fn with_bounds(mut self, center: [f32; 3], radius: f32) -> Self {
self.bounds_center = center;
self.bounds_radius = radius;
self
}
pub fn compute_camera_distance(&mut self, camera_pos: [f32; 3]) {
let dx = self.bounds_center[0] - camera_pos[0];
let dy = self.bounds_center[1] - camera_pos[1];
let dz = self.bounds_center[2] - camera_pos[2];
self.camera_distance = dx * dx + dy * dy + dz * dz;
}
pub fn frustum_cull(&mut self, frustum_planes: &[[f32; 4]; 6]) -> bool {
for plane in frustum_planes {
let dist = plane[0] * self.bounds_center[0]
+ plane[1] * self.bounds_center[1]
+ plane[2] * self.bounds_center[2]
+ plane[3];
if dist < -self.bounds_radius {
self.visible = false;
return false;
}
}
self.visible = true;
true
}
}
#[derive(Debug)]
pub struct RenderQueue {
pub buckets: HashMap<RenderBucket, Vec<RenderItem>>,
pub sort_modes: HashMap<RenderBucket, SortMode>,
pub camera_position: [f32; 3],
pub frustum_planes: [[f32; 4]; 6],
pub total_submitted: u32,
pub total_visible: u32,
pub total_culled: u32,
next_id: u64,
}
impl RenderQueue {
pub fn new() -> Self {
let mut sort_modes = HashMap::new();
sort_modes.insert(RenderBucket::Opaque, SortMode::FrontToBack);
sort_modes.insert(RenderBucket::Transparent, SortMode::BackToFront);
sort_modes.insert(RenderBucket::Overlay, SortMode::None);
sort_modes.insert(RenderBucket::Debug, SortMode::None);
sort_modes.insert(RenderBucket::Sky, SortMode::None);
Self {
buckets: HashMap::new(),
sort_modes,
camera_position: [0.0; 3],
frustum_planes: [[0.0; 4]; 6],
total_submitted: 0,
total_visible: 0,
total_culled: 0,
next_id: 1,
}
}
pub fn clear(&mut self) {
for bucket in self.buckets.values_mut() {
bucket.clear();
}
self.total_submitted = 0;
self.total_visible = 0;
self.total_culled = 0;
}
pub fn set_camera(&mut self, position: [f32; 3], frustum_planes: [[f32; 4]; 6]) {
self.camera_position = position;
self.frustum_planes = frustum_planes;
}
pub fn submit(&mut self, mut item: RenderItem) {
item.id = self.next_id;
self.next_id += 1;
self.total_submitted += 1;
item.compute_camera_distance(self.camera_position);
let visible = item.frustum_cull(&self.frustum_planes);
if visible {
self.total_visible += 1;
} else {
self.total_culled += 1;
}
let bucket = item.bucket;
self.buckets.entry(bucket).or_default().push(item);
}
pub fn submit_batch(&mut self, items: Vec<RenderItem>) {
for item in items {
self.submit(item);
}
}
pub fn sort(&mut self) {
for (bucket, items) in &mut self.buckets {
items.retain(|item| item.visible);
let mode = self.sort_modes.get(bucket)
.copied()
.unwrap_or(bucket.default_sort_mode());
match mode {
SortMode::FrontToBack => {
items.sort_by(|a, b| {
a.camera_distance.partial_cmp(&b.camera_distance)
.unwrap_or(std::cmp::Ordering::Equal)
});
}
SortMode::BackToFront => {
items.sort_by(|a, b| {
b.camera_distance.partial_cmp(&a.camera_distance)
.unwrap_or(std::cmp::Ordering::Equal)
});
}
SortMode::ByMaterial => {
items.sort_by(|a, b| a.sort_key.cmp(&b.sort_key));
}
SortMode::None => {}
}
}
}
pub fn get_bucket(&self, bucket: RenderBucket) -> &[RenderItem] {
self.buckets.get(&bucket).map(|v| v.as_slice()).unwrap_or(&[])
}
pub fn opaque_items(&self) -> &[RenderItem] {
self.get_bucket(RenderBucket::Opaque)
}
pub fn transparent_items(&self) -> &[RenderItem] {
self.get_bucket(RenderBucket::Transparent)
}
pub fn overlay_items(&self) -> &[RenderItem] {
self.get_bucket(RenderBucket::Overlay)
}
pub fn total_triangles(&self) -> u64 {
self.buckets.values()
.flat_map(|items| items.iter())
.filter(|i| i.visible)
.map(|i| {
let tris = if i.index_count > 0 {
i.index_count / 3
} else {
i.vertex_count / 3
};
tris as u64 * i.instance_count as u64
})
.sum()
}
pub fn total_draw_calls(&self) -> u32 {
self.buckets.values()
.flat_map(|items| items.iter())
.filter(|i| i.visible)
.count() as u32
}
}
impl Default for RenderQueue {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug)]
pub struct HdrFramebuffer {
pub fbo_handle: u64,
pub color_handle: u64,
pub depth_handle: u64,
pub width: u32,
pub height: u32,
pub allocated: bool,
pub generation: u32,
next_handle: u64,
pub bloom_handle: u64,
pub bloom_threshold: f32,
pub bloom_mip_levels: u32,
pub bloom_mips: Vec<u64>,
}
impl HdrFramebuffer {
pub fn new(width: u32, height: u32) -> Self {
Self {
fbo_handle: 0,
color_handle: 0,
depth_handle: 0,
width,
height,
allocated: false,
generation: 0,
next_handle: 1000,
bloom_handle: 0,
bloom_threshold: 1.0,
bloom_mip_levels: 5,
bloom_mips: Vec::new(),
}
}
pub fn create(&mut self) -> Result<(), String> {
self.fbo_handle = self.alloc_handle();
self.color_handle = self.alloc_handle();
self.depth_handle = self.alloc_handle();
self.bloom_handle = self.alloc_handle();
self.bloom_mips.clear();
for _ in 0..self.bloom_mip_levels {
let handle = self.alloc_handle();
self.bloom_mips.push(handle);
}
self.allocated = true;
self.generation += 1;
Ok(())
}
pub fn destroy(&mut self) {
self.fbo_handle = 0;
self.color_handle = 0;
self.depth_handle = 0;
self.bloom_handle = 0;
self.bloom_mips.clear();
self.allocated = false;
}
pub fn resize(&mut self, width: u32, height: u32) {
if self.width == width && self.height == height {
return;
}
self.width = width;
self.height = height;
if self.allocated {
self.generation += 1;
}
}
pub fn bind(&self) {
}
pub fn unbind(&self) {
}
pub fn memory_bytes(&self) -> u64 {
let base = self.width as u64 * self.height as u64;
let color = base * 8; let depth = base * 4; let bloom = base * 8; let bloom_mips = (bloom as f64 * 0.334) as u64; color + depth + bloom + bloom_mips
}
fn alloc_handle(&mut self) -> u64 {
let h = self.next_handle;
self.next_handle += 1;
h
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ExposureMode {
Manual,
AverageLuminance,
Histogram,
SpotMetering,
}
#[derive(Debug, Clone)]
pub struct ExposureController {
pub mode: ExposureMode,
pub exposure: f32,
pub target_exposure: f32,
pub manual_exposure: f32,
pub adaptation_speed_up: f32,
pub adaptation_speed_down: f32,
pub min_exposure: f32,
pub max_exposure: f32,
pub ev_compensation: f32,
pub key_value: f32,
pub histogram_low_percentile: f32,
pub histogram_high_percentile: f32,
pub histogram_bins: u32,
pub histogram: Vec<u32>,
pub average_luminance: f32,
pub spot_radius: f32,
}
impl ExposureController {
pub fn new() -> Self {
Self {
mode: ExposureMode::AverageLuminance,
exposure: 1.0,
target_exposure: 1.0,
manual_exposure: 1.0,
adaptation_speed_up: 2.0,
adaptation_speed_down: 1.0,
min_exposure: 0.001,
max_exposure: 100.0,
ev_compensation: 0.0,
key_value: 0.18,
histogram_low_percentile: 0.1,
histogram_high_percentile: 0.9,
histogram_bins: 256,
histogram: vec![0; 256],
average_luminance: 0.18,
spot_radius: 0.1,
}
}
pub fn update(&mut self, dt: f32) {
match self.mode {
ExposureMode::Manual => {
self.exposure = self.manual_exposure;
return;
}
ExposureMode::AverageLuminance => {
self.target_exposure = self.compute_exposure_from_luminance(self.average_luminance);
}
ExposureMode::Histogram => {
let avg = self.compute_histogram_average();
self.target_exposure = self.compute_exposure_from_luminance(avg);
}
ExposureMode::SpotMetering => {
self.target_exposure = self.compute_exposure_from_luminance(self.average_luminance);
}
}
self.target_exposure *= (2.0f32).powf(self.ev_compensation);
self.target_exposure = clampf(self.target_exposure, self.min_exposure, self.max_exposure);
let speed = if self.target_exposure > self.exposure {
self.adaptation_speed_up
} else {
self.adaptation_speed_down
};
let factor = 1.0 - (-speed * dt).exp();
self.exposure = lerpf(self.exposure, self.target_exposure, factor);
self.exposure = clampf(self.exposure, self.min_exposure, self.max_exposure);
}
fn compute_exposure_from_luminance(&self, luminance: f32) -> f32 {
if luminance < 1e-6 {
return self.max_exposure;
}
self.key_value / luminance
}
fn compute_histogram_average(&self) -> f32 {
let total: u32 = self.histogram.iter().sum();
if total == 0 {
return 0.18;
}
let low_count = (total as f32 * self.histogram_low_percentile) as u32;
let high_count = (total as f32 * self.histogram_high_percentile) as u32;
let mut running = 0u32;
let mut weighted_sum = 0.0f64;
let mut valid_count = 0u32;
for (i, &count) in self.histogram.iter().enumerate() {
let prev_running = running;
running += count;
if running <= low_count {
continue;
}
if prev_running >= high_count {
break;
}
let contributing = if prev_running < low_count {
count - (low_count - prev_running)
} else if running > high_count {
high_count - prev_running
} else {
count
};
let t = i as f32 / self.histogram_bins as f32;
let log_lum = t * 20.0 - 10.0; let lum = log_lum.exp();
weighted_sum += lum as f64 * contributing as f64;
valid_count += contributing;
}
if valid_count == 0 {
return 0.18;
}
(weighted_sum / valid_count as f64) as f32
}
pub fn feed_luminance(&mut self, luminance: f32) {
self.average_luminance = luminance.max(1e-6);
}
pub fn feed_histogram(&mut self, histogram: Vec<u32>) {
self.histogram = histogram;
}
pub fn exposure_multiplier(&self) -> f32 {
self.exposure
}
pub fn reset(&mut self) {
*self = Self::new();
}
}
impl Default for ExposureController {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ToneMappingOperator {
Reinhard,
ReinhardExtended,
AcesFilmic,
Hable,
Linear,
AgX,
}
impl ToneMappingOperator {
pub fn apply(&self, color: [f32; 3], exposure: f32) -> [f32; 3] {
let c = [
color[0] * exposure,
color[1] * exposure,
color[2] * exposure,
];
match self {
Self::Reinhard => Self::reinhard(c),
Self::ReinhardExtended => Self::reinhard_extended(c, 4.0),
Self::AcesFilmic => Self::aces_filmic(c),
Self::Hable => Self::hable(c),
Self::Linear => [
saturate(c[0]),
saturate(c[1]),
saturate(c[2]),
],
Self::AgX => Self::agx(c),
}
}
fn reinhard(c: [f32; 3]) -> [f32; 3] {
[
c[0] / (1.0 + c[0]),
c[1] / (1.0 + c[1]),
c[2] / (1.0 + c[2]),
]
}
fn reinhard_extended(c: [f32; 3], white_point: f32) -> [f32; 3] {
let wp2 = white_point * white_point;
[
c[0] * (1.0 + c[0] / wp2) / (1.0 + c[0]),
c[1] * (1.0 + c[1] / wp2) / (1.0 + c[1]),
c[2] * (1.0 + c[2] / wp2) / (1.0 + c[2]),
]
}
fn aces_filmic(c: [f32; 3]) -> [f32; 3] {
fn aces_channel(x: f32) -> f32 {
let a = 2.51;
let b = 0.03;
let c = 2.43;
let d = 0.59;
let e = 0.14;
saturate((x * (a * x + b)) / (x * (c * x + d) + e))
}
[aces_channel(c[0]), aces_channel(c[1]), aces_channel(c[2])]
}
fn hable(c: [f32; 3]) -> [f32; 3] {
fn hable_partial(x: f32) -> f32 {
let a = 0.15;
let b = 0.50;
let cc = 0.10;
let d = 0.20;
let e = 0.02;
let f = 0.30;
((x * (a * x + cc * b) + d * e) / (x * (a * x + b) + d * f)) - e / f
}
let exposure_bias = 2.0;
let white_scale = 1.0 / hable_partial(11.2);
[
hable_partial(c[0] * exposure_bias) * white_scale,
hable_partial(c[1] * exposure_bias) * white_scale,
hable_partial(c[2] * exposure_bias) * white_scale,
]
}
fn agx(c: [f32; 3]) -> [f32; 3] {
fn agx_channel(x: f32) -> f32 {
let x = x.max(0.0);
let a = x.ln().max(-10.0).min(10.0);
let mapped = 0.5 + 0.5 * (a * 0.3).tanh();
mapped
}
[agx_channel(c[0]), agx_channel(c[1]), agx_channel(c[2])]
}
pub fn glsl_function(&self) -> &'static str {
match self {
Self::Reinhard => {
r#"vec3 tonemap(vec3 c) { return c / (1.0 + c); }"#
}
Self::ReinhardExtended => {
r#"vec3 tonemap(vec3 c) {
float wp2 = 16.0;
return c * (1.0 + c / wp2) / (1.0 + c);
}"#
}
Self::AcesFilmic => {
r#"vec3 tonemap(vec3 x) {
float a = 2.51; float b = 0.03;
float c = 2.43; float d = 0.59; float e = 0.14;
return clamp((x*(a*x+b))/(x*(c*x+d)+e), 0.0, 1.0);
}"#
}
Self::Hable => {
r#"float hable(float x) {
float A=0.15,B=0.50,C=0.10,D=0.20,E=0.02,F=0.30;
return ((x*(A*x+C*B)+D*E)/(x*(A*x+B)+D*F))-E/F;
}
vec3 tonemap(vec3 c) {
float w = 1.0/hable(11.2);
return vec3(hable(c.x*2.0)*w, hable(c.y*2.0)*w, hable(c.z*2.0)*w);
}"#
}
Self::Linear => {
r#"vec3 tonemap(vec3 c) { return clamp(c, 0.0, 1.0); }"#
}
Self::AgX => {
r#"vec3 tonemap(vec3 c) {
vec3 a = log(max(c, vec3(0.0001)));
return 0.5 + 0.5 * tanh(a * 0.3);
}"#
}
}
}
}
#[derive(Debug)]
pub struct DepthPrePass {
pub enabled: bool,
pub shader_handle: u64,
pub items: Vec<u64>,
pub use_gbuffer_depth: bool,
pub rendered_count: u32,
pub time_us: u64,
pub depth_func: DepthFunction,
pub depth_write: bool,
pub alpha_test_in_prepass: bool,
pub alpha_threshold: f32,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DepthFunction {
Less,
LessEqual,
Greater,
GreaterEqual,
Equal,
NotEqual,
Always,
Never,
}
impl DepthPrePass {
pub fn new() -> Self {
Self {
enabled: true,
shader_handle: 0,
items: Vec::new(),
use_gbuffer_depth: true,
rendered_count: 0,
time_us: 0,
depth_func: DepthFunction::Less,
depth_write: true,
alpha_test_in_prepass: false,
alpha_threshold: 0.5,
}
}
pub fn execute(&mut self, queue: &RenderQueue, _gbuffer: &mut GBuffer) {
let start = std::time::Instant::now();
self.items.clear();
self.rendered_count = 0;
if !self.enabled {
return;
}
let opaque = queue.opaque_items();
for item in opaque {
if !item.visible {
continue;
}
if item.alpha_test && !self.alpha_test_in_prepass {
continue;
}
self.items.push(item.id);
self.rendered_count += 1;
}
self.time_us = start.elapsed().as_micros() as u64;
}
pub fn vertex_shader() -> &'static str {
r#"#version 330 core
layout(location = 0) in vec3 a_position;
uniform mat4 u_model;
uniform mat4 u_view_projection;
void main() {
gl_Position = u_view_projection * u_model * vec4(a_position, 1.0);
}
"#
}
pub fn fragment_shader_alpha_test() -> &'static str {
r#"#version 330 core
uniform sampler2D u_albedo_tex;
uniform float u_alpha_threshold;
in vec2 v_texcoord;
void main() {
float alpha = texture(u_albedo_tex, v_texcoord).a;
if (alpha < u_alpha_threshold) discard;
}
"#
}
}
impl Default for DepthPrePass {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug)]
pub struct GeometryPass {
pub enabled: bool,
pub shader_handle: u64,
pub draw_call_count: u32,
pub triangle_count: u64,
pub time_us: u64,
pub depth_test: bool,
pub depth_func: DepthFunction,
pub depth_write: bool,
pub backface_cull: bool,
pub polygon_offset: Option<(f32, f32)>,
pub use_instancing: bool,
pub max_instances_per_draw: u32,
}
impl GeometryPass {
pub fn new() -> Self {
Self {
enabled: true,
shader_handle: 0,
draw_call_count: 0,
triangle_count: 0,
time_us: 0,
depth_test: true,
depth_func: DepthFunction::Equal,
depth_write: false,
backface_cull: true,
polygon_offset: None,
use_instancing: true,
max_instances_per_draw: 1024,
}
}
pub fn execute(&mut self, queue: &RenderQueue, gbuffer: &mut GBuffer) {
let start = std::time::Instant::now();
self.draw_call_count = 0;
self.triangle_count = 0;
if !self.enabled {
return;
}
let _ = gbuffer.bind();
gbuffer.clear_all();
let opaque = queue.opaque_items();
for item in opaque {
if !item.visible {
continue;
}
self.draw_call_count += 1;
let tris = if item.index_count > 0 {
item.index_count / 3
} else {
item.vertex_count / 3
};
self.triangle_count += tris as u64 * item.instance_count as u64;
}
gbuffer.unbind();
gbuffer.stats.geometry_draw_calls = self.draw_call_count;
self.time_us = start.elapsed().as_micros() as u64;
}
pub fn vertex_shader() -> &'static str {
r#"#version 330 core
layout(location = 0) in vec3 a_position;
layout(location = 1) in vec3 a_normal;
layout(location = 2) in vec2 a_texcoord;
layout(location = 3) in vec3 a_tangent;
uniform mat4 u_model;
uniform mat4 u_view;
uniform mat4 u_projection;
uniform mat3 u_normal_matrix;
out vec3 v_world_pos;
out vec3 v_normal;
out vec2 v_texcoord;
out vec3 v_tangent;
void main() {
vec4 world_pos = u_model * vec4(a_position, 1.0);
v_world_pos = world_pos.xyz;
v_normal = u_normal_matrix * a_normal;
v_texcoord = a_texcoord;
v_tangent = u_normal_matrix * a_tangent;
gl_Position = u_projection * u_view * world_pos;
}
"#
}
pub fn fragment_shader() -> &'static str {
r#"#version 330 core
in vec3 v_world_pos;
in vec3 v_normal;
in vec2 v_texcoord;
in vec3 v_tangent;
layout(location = 0) out vec4 out_position;
layout(location = 1) out vec2 out_normal;
layout(location = 2) out vec4 out_albedo;
layout(location = 3) out vec4 out_emission;
layout(location = 4) out float out_matid;
layout(location = 5) out float out_roughness;
layout(location = 6) out float out_metallic;
uniform sampler2D u_albedo_map;
uniform sampler2D u_normal_map;
uniform sampler2D u_roughness_map;
uniform sampler2D u_metallic_map;
uniform sampler2D u_emission_map;
uniform vec4 u_albedo_color;
uniform float u_roughness;
uniform float u_metallic;
uniform vec3 u_emission;
uniform float u_material_id;
uniform bool u_has_normal_map;
// Octahedral encoding
vec2 oct_encode(vec3 n) {
float sum = abs(n.x) + abs(n.y) + abs(n.z);
vec2 o = n.xy / sum;
if (n.z < 0.0) {
o = (1.0 - abs(o.yx)) * vec2(o.x >= 0.0 ? 1.0 : -1.0, o.y >= 0.0 ? 1.0 : -1.0);
}
return o;
}
void main() {
out_position = vec4(v_world_pos, 1.0);
vec3 N = normalize(v_normal);
if (u_has_normal_map) {
vec3 T = normalize(v_tangent);
vec3 B = cross(N, T);
mat3 TBN = mat3(T, B, N);
vec3 tangent_normal = texture(u_normal_map, v_texcoord).xyz * 2.0 - 1.0;
N = normalize(TBN * tangent_normal);
}
out_normal = oct_encode(N);
out_albedo = texture(u_albedo_map, v_texcoord) * u_albedo_color;
out_emission = vec4(u_emission + texture(u_emission_map, v_texcoord).rgb, 1.0);
out_matid = u_material_id / 255.0;
out_roughness = texture(u_roughness_map, v_texcoord).r * u_roughness;
out_metallic = texture(u_metallic_map, v_texcoord).r * u_metallic;
}
"#
}
}
impl Default for GeometryPass {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug)]
pub struct LightingPass {
pub enabled: bool,
pub shader_handle: u64,
pub lights: Vec<LightType>,
pub max_lights_per_pixel: u32,
pub use_light_volumes: bool,
pub time_us: u64,
pub lights_evaluated: u32,
pub ambient_color: [f32; 3],
pub ambient_intensity: f32,
pub ssao_enabled: bool,
pub ssao_radius: f32,
pub ssao_bias: f32,
pub ssao_kernel_size: u32,
pub environment_map: u64,
pub ibl_enabled: bool,
pub ibl_intensity: f32,
}
impl LightingPass {
pub fn new() -> Self {
Self {
enabled: true,
shader_handle: 0,
lights: Vec::new(),
max_lights_per_pixel: 128,
use_light_volumes: false,
time_us: 0,
lights_evaluated: 0,
ambient_color: [0.03, 0.03, 0.05],
ambient_intensity: 1.0,
ssao_enabled: false,
ssao_radius: 0.5,
ssao_bias: 0.025,
ssao_kernel_size: 64,
environment_map: 0,
ibl_enabled: false,
ibl_intensity: 1.0,
}
}
pub fn add_light(&mut self, light: LightType) {
self.lights.push(light);
}
pub fn clear_lights(&mut self) {
self.lights.clear();
}
pub fn execute(
&mut self,
gbuffer: &GBuffer,
hdr_fb: &HdrFramebuffer,
view_matrix: &Mat4,
projection_matrix: &Mat4,
camera_pos: [f32; 3],
) {
let start = std::time::Instant::now();
if !self.enabled {
return;
}
hdr_fb.bind();
let _bindings = gbuffer.bind_for_reading();
self.lights_evaluated = self.lights.len().min(self.max_lights_per_pixel as usize) as u32;
let _ = view_matrix;
let _ = projection_matrix;
let _ = camera_pos;
hdr_fb.unbind();
self.time_us = start.elapsed().as_micros() as u64;
}
pub fn evaluate_pbr(
&self,
position: [f32; 3],
normal: [f32; 3],
albedo: [f32; 3],
roughness: f32,
metallic: f32,
camera_pos: [f32; 3],
) -> [f32; 3] {
let v = super::vec3_normalize(vec3_sub(camera_pos, position));
let mut total = [
self.ambient_color[0] * self.ambient_intensity * albedo[0],
self.ambient_color[1] * self.ambient_intensity * albedo[1],
self.ambient_color[2] * self.ambient_intensity * albedo[2],
];
for light in &self.lights {
let (l, attenuation, light_color) = light.evaluate(position);
if attenuation < 1e-6 {
continue;
}
let n_dot_l = vec3_dot(normal, l).max(0.0);
if n_dot_l < 1e-6 {
continue;
}
let h = super::vec3_normalize(super::vec3_add(v, l));
let n_dot_h = vec3_dot(normal, h).max(0.0);
let n_dot_v = vec3_dot(normal, v).max(0.001);
let a = roughness * roughness;
let a2 = a * a;
let denom = n_dot_h * n_dot_h * (a2 - 1.0) + 1.0;
let d = a2 / (std::f32::consts::PI * denom * denom).max(1e-6);
let k = (roughness + 1.0) * (roughness + 1.0) / 8.0;
let g1_v = n_dot_v / (n_dot_v * (1.0 - k) + k);
let g1_l = n_dot_l / (n_dot_l * (1.0 - k) + k);
let g = g1_v * g1_l;
let f0_base = lerpf(0.04, 1.0, metallic);
let f0 = [
lerpf(0.04, albedo[0], metallic),
lerpf(0.04, albedo[1], metallic),
lerpf(0.04, albedo[2], metallic),
];
let _ = f0_base;
let v_dot_h = vec3_dot(v, h).max(0.0);
let fresnel_factor = (1.0 - v_dot_h).powf(5.0);
let f = [
f0[0] + (1.0 - f0[0]) * fresnel_factor,
f0[1] + (1.0 - f0[1]) * fresnel_factor,
f0[2] + (1.0 - f0[2]) * fresnel_factor,
];
let spec_denom = (4.0 * n_dot_v * n_dot_l).max(1e-6);
let spec = [
d * g * f[0] / spec_denom,
d * g * f[1] / spec_denom,
d * g * f[2] / spec_denom,
];
let kd = [
(1.0 - f[0]) * (1.0 - metallic),
(1.0 - f[1]) * (1.0 - metallic),
(1.0 - f[2]) * (1.0 - metallic),
];
let diffuse = [
kd[0] * albedo[0] / std::f32::consts::PI,
kd[1] * albedo[1] / std::f32::consts::PI,
kd[2] * albedo[2] / std::f32::consts::PI,
];
for i in 0..3 {
total[i] += (diffuse[i] + spec[i]) * light_color[i] * attenuation * n_dot_l;
}
}
total
}
pub fn fragment_shader() -> &'static str {
r#"#version 330 core
in vec2 v_texcoord;
out vec4 frag_color;
uniform sampler2D g_position;
uniform sampler2D g_normal;
uniform sampler2D g_albedo;
uniform sampler2D g_emission;
uniform sampler2D g_roughness;
uniform sampler2D g_metallic;
uniform sampler2D g_depth;
uniform vec3 u_camera_pos;
uniform mat4 u_inv_view_proj;
struct Light {
int type; // 0=dir, 1=point, 2=spot
vec3 position;
vec3 direction;
vec3 color;
float intensity;
float range;
float inner_angle;
float outer_angle;
};
#define MAX_LIGHTS 128
uniform Light u_lights[MAX_LIGHTS];
uniform int u_light_count;
uniform vec3 u_ambient;
const float PI = 3.14159265359;
vec3 oct_decode(vec2 o) {
float z = 1.0 - abs(o.x) - abs(o.y);
vec2 xy = z >= 0.0 ? o : (1.0 - abs(o.yx)) * vec2(o.x >= 0.0 ? 1.0 : -1.0, o.y >= 0.0 ? 1.0 : -1.0);
return normalize(vec3(xy, z));
}
float ggx_distribution(float NdotH, float roughness) {
float a2 = roughness * roughness * roughness * roughness;
float d = NdotH * NdotH * (a2 - 1.0) + 1.0;
return a2 / (PI * d * d);
}
float geometry_schlick(float NdotV, float NdotL, float roughness) {
float k = (roughness + 1.0) * (roughness + 1.0) / 8.0;
float g1 = NdotV / (NdotV * (1.0 - k) + k);
float g2 = NdotL / (NdotL * (1.0 - k) + k);
return g1 * g2;
}
vec3 fresnel_schlick(float cosTheta, vec3 F0) {
return F0 + (1.0 - F0) * pow(1.0 - cosTheta, 5.0);
}
void main() {
vec3 pos = texture(g_position, v_texcoord).xyz;
vec3 N = oct_decode(texture(g_normal, v_texcoord).xy);
vec4 albedo_alpha = texture(g_albedo, v_texcoord);
vec3 albedo = albedo_alpha.rgb;
vec3 emission = texture(g_emission, v_texcoord).rgb;
float roughness = texture(g_roughness, v_texcoord).r;
float metallic = texture(g_metallic, v_texcoord).r;
vec3 V = normalize(u_camera_pos - pos);
vec3 F0 = mix(vec3(0.04), albedo, metallic);
vec3 Lo = vec3(0.0);
for (int i = 0; i < u_light_count && i < MAX_LIGHTS; i++) {
vec3 L;
float attenuation;
if (u_lights[i].type == 0) {
L = -u_lights[i].direction;
attenuation = u_lights[i].intensity;
} else {
vec3 toLight = u_lights[i].position - pos;
float dist = length(toLight);
L = toLight / dist;
float r = dist / u_lights[i].range;
attenuation = u_lights[i].intensity * max((1.0 - r*r), 0.0) / (dist*dist + 1.0);
if (u_lights[i].type == 2) {
float cosAngle = dot(-L, u_lights[i].direction);
float spot = clamp((cosAngle - cos(u_lights[i].outer_angle)) /
(cos(u_lights[i].inner_angle) - cos(u_lights[i].outer_angle)), 0.0, 1.0);
attenuation *= spot;
}
}
vec3 H = normalize(V + L);
float NdotL = max(dot(N, L), 0.0);
float NdotH = max(dot(N, H), 0.0);
float NdotV = max(dot(N, V), 0.001);
float D = ggx_distribution(NdotH, roughness);
float G = geometry_schlick(NdotV, NdotL, roughness);
vec3 F = fresnel_schlick(max(dot(H, V), 0.0), F0);
vec3 spec = D * G * F / max(4.0 * NdotV * NdotL, 0.001);
vec3 kD = (1.0 - F) * (1.0 - metallic);
vec3 diffuse = kD * albedo / PI;
Lo += (diffuse + spec) * u_lights[i].color * attenuation * NdotL;
}
vec3 ambient = u_ambient * albedo;
vec3 color = ambient + Lo + emission;
frag_color = vec4(color, 1.0);
}
"#
}
}
impl Default for LightingPass {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug)]
pub struct ForwardPass {
pub enabled: bool,
pub shader_handle: u64,
pub draw_call_count: u32,
pub triangle_count: u64,
pub time_us: u64,
pub depth_test: bool,
pub depth_write: bool,
pub blend_mode: ForwardBlendMode,
pub premultiplied_alpha: bool,
pub render_particles: bool,
pub render_glyphs: bool,
pub oit_layers: u32,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ForwardBlendMode {
AlphaBlend,
Additive,
PremultipliedAlpha,
Multiply,
}
impl ForwardPass {
pub fn new() -> Self {
Self {
enabled: true,
shader_handle: 0,
draw_call_count: 0,
triangle_count: 0,
time_us: 0,
depth_test: true,
depth_write: false,
blend_mode: ForwardBlendMode::AlphaBlend,
premultiplied_alpha: false,
render_particles: true,
render_glyphs: true,
oit_layers: 0,
}
}
pub fn execute(
&mut self,
queue: &RenderQueue,
hdr_fb: &HdrFramebuffer,
_gbuffer: &GBuffer,
_lights: &[LightType],
_view: &Mat4,
_proj: &Mat4,
_camera_pos: [f32; 3],
) {
let start = std::time::Instant::now();
self.draw_call_count = 0;
self.triangle_count = 0;
if !self.enabled {
return;
}
hdr_fb.bind();
let transparent = queue.transparent_items();
for item in transparent {
if !item.visible {
continue;
}
self.draw_call_count += 1;
let tris = if item.index_count > 0 {
item.index_count / 3
} else {
item.vertex_count / 3
};
self.triangle_count += tris as u64 * item.instance_count as u64;
}
let overlay = queue.overlay_items();
for item in overlay {
if !item.visible {
continue;
}
self.draw_call_count += 1;
}
hdr_fb.unbind();
self.time_us = start.elapsed().as_micros() as u64;
}
}
impl Default for ForwardPass {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug)]
pub struct PostProcessPass {
pub enabled: bool,
pub shader_handle: u64,
pub time_us: u64,
pub bloom_enabled: bool,
pub bloom_intensity: f32,
pub bloom_threshold: f32,
pub bloom_radius: f32,
pub bloom_mip_count: u32,
pub tone_mapping: ToneMappingOperator,
pub exposure: ExposureController,
pub gamma: f32,
pub vignette_enabled: bool,
pub vignette_intensity: f32,
pub vignette_smoothness: f32,
pub chromatic_aberration_enabled: bool,
pub chromatic_aberration_intensity: f32,
pub film_grain_enabled: bool,
pub film_grain_intensity: f32,
pub dithering_enabled: bool,
pub color_lut_handle: u64,
pub color_lut_enabled: bool,
pub saturation: f32,
pub contrast: f32,
pub brightness: f32,
}
impl PostProcessPass {
pub fn new() -> Self {
Self {
enabled: true,
shader_handle: 0,
time_us: 0,
bloom_enabled: true,
bloom_intensity: 0.5,
bloom_threshold: 1.0,
bloom_radius: 5.0,
bloom_mip_count: 5,
tone_mapping: ToneMappingOperator::AcesFilmic,
exposure: ExposureController::new(),
gamma: 2.2,
vignette_enabled: false,
vignette_intensity: 0.3,
vignette_smoothness: 2.0,
chromatic_aberration_enabled: false,
chromatic_aberration_intensity: 0.005,
film_grain_enabled: false,
film_grain_intensity: 0.05,
dithering_enabled: true,
color_lut_handle: 0,
color_lut_enabled: false,
saturation: 1.0,
contrast: 1.0,
brightness: 0.0,
}
}
pub fn execute(&mut self, hdr_fb: &HdrFramebuffer, _viewport: &Viewport, dt: f32) {
let start = std::time::Instant::now();
if !self.enabled {
return;
}
self.exposure.update(dt);
let _ = hdr_fb;
self.time_us = start.elapsed().as_micros() as u64;
}
pub fn bloom_extract(&self, color: [f32; 3]) -> [f32; 3] {
let luminance = 0.2126 * color[0] + 0.7152 * color[1] + 0.0722 * color[2];
if luminance > self.bloom_threshold {
let excess = luminance - self.bloom_threshold;
let factor = excess / luminance.max(1e-6);
[
color[0] * factor * self.bloom_intensity,
color[1] * factor * self.bloom_intensity,
color[2] * factor * self.bloom_intensity,
]
} else {
[0.0, 0.0, 0.0]
}
}
pub fn apply_vignette(&self, color: [f32; 3], uv: [f32; 2]) -> [f32; 3] {
if !self.vignette_enabled {
return color;
}
let center = [uv[0] - 0.5, uv[1] - 0.5];
let dist = (center[0] * center[0] + center[1] * center[1]).sqrt() * 1.414;
let vignette = 1.0 - self.vignette_intensity * dist.powf(self.vignette_smoothness);
let v = vignette.max(0.0);
[color[0] * v, color[1] * v, color[2] * v]
}
pub fn color_adjust(&self, color: [f32; 3]) -> [f32; 3] {
let c = [
color[0] + self.brightness,
color[1] + self.brightness,
color[2] + self.brightness,
];
let c = [
(c[0] - 0.5) * self.contrast + 0.5,
(c[1] - 0.5) * self.contrast + 0.5,
(c[2] - 0.5) * self.contrast + 0.5,
];
let lum = 0.2126 * c[0] + 0.7152 * c[1] + 0.0722 * c[2];
[
lerpf(lum, c[0], self.saturation),
lerpf(lum, c[1], self.saturation),
lerpf(lum, c[2], self.saturation),
]
}
pub fn fragment_shader(&self) -> String {
let mut s = String::from(r#"#version 330 core
in vec2 v_texcoord;
out vec4 frag_color;
uniform sampler2D u_hdr_color;
uniform sampler2D u_bloom;
uniform float u_exposure;
uniform float u_gamma;
uniform float u_bloom_intensity;
uniform float u_time;
"#);
s.push_str(self.tone_mapping.glsl_function());
s.push('\n');
s.push_str(r#"
void main() {
vec3 hdr = texture(u_hdr_color, v_texcoord).rgb;
vec3 bloom = texture(u_bloom, v_texcoord).rgb;
hdr += bloom * u_bloom_intensity;
hdr *= u_exposure;
vec3 mapped = tonemap(hdr);
mapped = pow(mapped, vec3(1.0 / u_gamma));
frag_color = vec4(mapped, 1.0);
}
"#);
s
}
}
impl Default for PostProcessPass {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, Default)]
pub struct DeferredFrameStats {
pub total_time_us: u64,
pub depth_prepass_us: u64,
pub geometry_pass_us: u64,
pub lighting_pass_us: u64,
pub forward_pass_us: u64,
pub postprocess_pass_us: u64,
pub aa_pass_us: u64,
pub total_draw_calls: u32,
pub opaque_draw_calls: u32,
pub transparent_draw_calls: u32,
pub total_triangles: u64,
pub items_submitted: u32,
pub items_visible: u32,
pub items_culled: u32,
pub gbuffer_memory_mb: f32,
pub exposure: f32,
pub frame_number: u64,
}
#[derive(Debug)]
pub struct DeferredPipeline {
pub gbuffer: GBuffer,
pub hdr_framebuffer: HdrFramebuffer,
pub depth_prepass: DepthPrePass,
pub geometry_pass: GeometryPass,
pub lighting_pass: LightingPass,
pub forward_pass: ForwardPass,
pub postprocess_pass: PostProcessPass,
pub render_queue: RenderQueue,
pub viewport: Viewport,
pub view_matrix: Mat4,
pub projection_matrix: Mat4,
pub camera_position: [f32; 3],
pub frame_stats: DeferredFrameStats,
pub initialized: bool,
pub frame_number: u64,
pub dt: f32,
}
impl DeferredPipeline {
pub fn new(width: u32, height: u32) -> Self {
let viewport = Viewport::new(width, height);
Self {
gbuffer: GBuffer::new(viewport),
hdr_framebuffer: HdrFramebuffer::new(width, height),
depth_prepass: DepthPrePass::new(),
geometry_pass: GeometryPass::new(),
lighting_pass: LightingPass::new(),
forward_pass: ForwardPass::new(),
postprocess_pass: PostProcessPass::new(),
render_queue: RenderQueue::new(),
viewport,
view_matrix: Mat4::IDENTITY,
projection_matrix: Mat4::IDENTITY,
camera_position: [0.0; 3],
frame_stats: DeferredFrameStats::default(),
initialized: false,
frame_number: 0,
dt: 0.016,
}
}
pub fn initialize(&mut self) -> Result<(), String> {
self.gbuffer.create().map_err(|e| e.to_string())?;
self.hdr_framebuffer.create()?;
self.initialized = true;
Ok(())
}
pub fn shutdown(&mut self) {
self.gbuffer.destroy();
self.hdr_framebuffer.destroy();
self.initialized = false;
}
pub fn resize(&mut self, width: u32, height: u32) {
self.viewport = Viewport::new(width, height);
let _ = self.gbuffer.resize(width, height);
self.hdr_framebuffer.resize(width, height);
}
pub fn set_camera(
&mut self,
position: [f32; 3],
view: Mat4,
projection: Mat4,
frustum_planes: [[f32; 4]; 6],
) {
self.camera_position = position;
self.view_matrix = view;
self.projection_matrix = projection;
self.render_queue.set_camera(position, frustum_planes);
}
pub fn submit(&mut self, item: RenderItem) {
self.render_queue.submit(item);
}
pub fn execute_frame(&mut self, dt: f32) {
let frame_start = std::time::Instant::now();
self.dt = dt;
self.frame_number += 1;
self.render_queue.sort();
self.depth_prepass.execute(&self.render_queue, &mut self.gbuffer);
self.geometry_pass.execute(&self.render_queue, &mut self.gbuffer);
self.lighting_pass.execute(
&self.gbuffer,
&self.hdr_framebuffer,
&self.view_matrix,
&self.projection_matrix,
self.camera_position,
);
let lights_clone: Vec<LightType> = self.lighting_pass.lights.clone();
self.forward_pass.execute(
&self.render_queue,
&self.hdr_framebuffer,
&self.gbuffer,
&lights_clone,
&self.view_matrix,
&self.projection_matrix,
self.camera_position,
);
self.postprocess_pass.execute(&self.hdr_framebuffer, &self.viewport, dt);
self.frame_stats = DeferredFrameStats {
total_time_us: frame_start.elapsed().as_micros() as u64,
depth_prepass_us: self.depth_prepass.time_us,
geometry_pass_us: self.geometry_pass.time_us,
lighting_pass_us: self.lighting_pass.time_us,
forward_pass_us: self.forward_pass.time_us,
postprocess_pass_us: self.postprocess_pass.time_us,
aa_pass_us: 0,
total_draw_calls: self.geometry_pass.draw_call_count + self.forward_pass.draw_call_count,
opaque_draw_calls: self.geometry_pass.draw_call_count,
transparent_draw_calls: self.forward_pass.draw_call_count,
total_triangles: self.geometry_pass.triangle_count + self.forward_pass.triangle_count,
items_submitted: self.render_queue.total_submitted,
items_visible: self.render_queue.total_visible,
items_culled: self.render_queue.total_culled,
gbuffer_memory_mb: self.gbuffer.stats().total_memory_bytes as f32 / (1024.0 * 1024.0),
exposure: self.postprocess_pass.exposure.exposure,
frame_number: self.frame_number,
};
self.render_queue.clear();
}
pub fn stats_summary(&self) -> String {
let s = &self.frame_stats;
format!(
"Frame {} | {:.1}ms total | Draws: {} | Tris: {} | Visible: {}/{} | Exposure: {:.2} | GBuf: {:.1}MB",
s.frame_number,
s.total_time_us as f64 / 1000.0,
s.total_draw_calls,
s.total_triangles,
s.items_visible,
s.items_submitted,
s.exposure,
s.gbuffer_memory_mb,
)
}
}
impl Default for DeferredPipeline {
fn default() -> Self {
Self::new(1920, 1080)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_render_queue_sorting() {
let mut queue = RenderQueue::new();
queue.set_camera(
[0.0, 0.0, 0.0],
[[0.0, 0.0, 1.0, 1000.0]; 6],
);
let item1 = RenderItem::new(0, 1, 0).with_bounds([0.0, 0.0, 10.0], 1.0);
let item2 = RenderItem::new(0, 2, 0).with_bounds([0.0, 0.0, 5.0], 1.0);
let item3 = RenderItem::new(0, 3, 0).with_bounds([0.0, 0.0, 20.0], 1.0);
queue.submit(item1);
queue.submit(item2);
queue.submit(item3);
queue.sort();
let opaque = queue.opaque_items();
assert_eq!(opaque.len(), 3);
assert!(opaque[0].camera_distance <= opaque[1].camera_distance);
assert!(opaque[1].camera_distance <= opaque[2].camera_distance);
}
#[test]
fn test_render_queue_transparent_sorting() {
let mut queue = RenderQueue::new();
queue.set_camera(
[0.0, 0.0, 0.0],
[[0.0, 0.0, 1.0, 1000.0]; 6],
);
let item1 = RenderItem::new(0, 1, 0)
.with_bucket(RenderBucket::Transparent)
.with_bounds([0.0, 0.0, 10.0], 1.0);
let item2 = RenderItem::new(0, 2, 0)
.with_bucket(RenderBucket::Transparent)
.with_bounds([0.0, 0.0, 5.0], 1.0);
queue.submit(item1);
queue.submit(item2);
queue.sort();
let transparent = queue.transparent_items();
assert_eq!(transparent.len(), 2);
assert!(transparent[0].camera_distance >= transparent[1].camera_distance);
}
#[test]
fn test_light_evaluation() {
let light = LightType::Directional {
direction: [0.0, -1.0, 0.0],
color: [1.0, 1.0, 1.0],
intensity: 1.0,
cast_shadows: false,
};
let (dir, att, _color) = light.evaluate([0.0, 0.0, 0.0]);
assert!((dir[1] - 1.0).abs() < 0.001); assert!((att - 1.0).abs() < 0.001);
}
#[test]
fn test_point_light_attenuation() {
let light = LightType::Point {
position: [0.0, 5.0, 0.0],
color: [1.0, 1.0, 1.0],
intensity: 10.0,
range: 20.0,
cast_shadows: false,
};
let (_, att_near, _) = light.evaluate([0.0, 4.0, 0.0]);
let (_, att_far, _) = light.evaluate([0.0, -10.0, 0.0]);
assert!(att_near > att_far, "Near attenuation should be greater");
}
#[test]
fn test_tone_mapping() {
let color = [2.0, 1.0, 0.5];
let reinhard = ToneMappingOperator::Reinhard.apply(color, 1.0);
for c in &reinhard {
assert!(*c >= 0.0 && *c <= 1.0);
}
let aces = ToneMappingOperator::AcesFilmic.apply(color, 1.0);
for c in &aces {
assert!(*c >= 0.0 && *c <= 1.0);
}
}
#[test]
fn test_exposure_controller() {
let mut ec = ExposureController::new();
ec.mode = ExposureMode::AverageLuminance;
ec.feed_luminance(0.5);
ec.update(0.016);
assert!(ec.exposure > 0.0);
}
#[test]
fn test_pipeline_creation() {
let mut pipeline = DeferredPipeline::new(1920, 1080);
assert!(!pipeline.initialized);
pipeline.initialize().unwrap();
assert!(pipeline.initialized);
}
#[test]
fn test_pipeline_frame() {
let mut pipeline = DeferredPipeline::new(800, 600);
pipeline.initialize().unwrap();
pipeline.lighting_pass.add_light(LightType::Directional {
direction: [0.0, -1.0, 0.0],
color: [1.0, 1.0, 1.0],
intensity: 1.0,
cast_shadows: false,
});
pipeline.set_camera(
[0.0, 5.0, 10.0],
Mat4::IDENTITY,
Mat4::IDENTITY,
[[0.0, 0.0, 1.0, 1000.0]; 6],
);
let item = RenderItem::new(0, 1, 0)
.with_bounds([0.0, 0.0, 0.0], 5.0);
pipeline.submit(item);
pipeline.execute_frame(0.016);
assert_eq!(pipeline.frame_stats.frame_number, 1);
}
#[test]
fn test_hdr_framebuffer() {
let mut fb = HdrFramebuffer::new(1920, 1080);
fb.create().unwrap();
assert!(fb.allocated);
assert!(fb.memory_bytes() > 0);
fb.resize(2560, 1440);
assert_eq!(fb.width, 2560);
}
#[test]
fn test_bloom_extract() {
let pp = PostProcessPass::new();
let bright = pp.bloom_extract([2.0, 2.0, 2.0]);
assert!(bright[0] > 0.0);
let dark = pp.bloom_extract([0.1, 0.1, 0.1]);
assert!((dark[0] - 0.0).abs() < 0.001);
}
#[test]
fn test_pbr_lighting() {
let mut lp = LightingPass::new();
lp.add_light(LightType::Directional {
direction: [0.0, -1.0, 0.0],
color: [1.0, 1.0, 1.0],
intensity: 2.0,
cast_shadows: false,
});
let result = lp.evaluate_pbr(
[0.0, 0.0, 0.0],
[0.0, 1.0, 0.0],
[0.8, 0.2, 0.2],
0.5,
0.0,
[0.0, 5.0, 5.0],
);
assert!(result[0] > 0.0);
assert!(result[1] > 0.0);
assert!(result[2] > 0.0);
}
}