diff --git a/src/camera.rs b/src/camera.rs new file mode 100644 index 0000000..9bbb506 --- /dev/null +++ b/src/camera.rs @@ -0,0 +1,71 @@ +// camera.rs + +use nalgebra::*; + +use crate::renderer::{Ray,Intersection}; +use crate::elements::*; + + +enum Camera { + OrthoCamera(OrthoCamera), + PerspectiveCamera(PerspectiveCamera), +} + +// TODO: Create a separate scene object +pub trait RaySource { + fn trace(&self, ray: &Ray) -> Option; +} + +pub struct OrthoCamera { + pub pos: Vec3, + pub output_img: bmp::Image, + pub elements: Vec, + pub lights: Vec, + //spheres: Vec, + //light: LightSrc, + + pub shadow_bias: f64, + pub max_recursion_depth: u32 +} + +impl RaySource for OrthoCamera { + fn trace(&self, ray: &Ray) -> Option { + self.elements.iter() + .filter_map(|s| s.intersect(ray).map(|d| Intersection::new(d, s) )) + .min_by(|i1, i2| i1.distance.partial_cmp(&i2.distance).unwrap()) + } +} + +pub struct PerspectiveCamera { + pub pos: Vec3, + pub output_img: bmp::Image, + pub elements: Vec, + pub lights: Vec, + pub shadow_bias: f64, + pub max_recursion_depth: u32, + pub fov: f64, + pub scene_width: u32, + pub scene_height: u32, +} + +impl PerspectiveCamera { + pub fn create_prime(&self, x: u32, y: u32) -> Ray { + let sensor_x = ((x as f64 + 0.5) / self.scene_width as f64) * 2.0 - 1.0; + let sensor_y = 1.0 - ((y as f64 + 0.5) / self.scene_height as f64) * 2.0; + + Ray { + pos: self.pos, + dir: Vec3::new(sensor_x, sensor_y, 1.0).normalize(), + } + } +} + +impl RaySource for PerspectiveCamera { + fn trace(&self, ray: &Ray) -> Option { + self.elements.iter() + .filter_map(|s| s.intersect(ray).map(|d| Intersection::new(d, s) )) + .min_by(|i1, i2| i1.distance.partial_cmp(&i2.distance).unwrap()) + } +} + + diff --git a/src/elements.rs b/src/elements.rs new file mode 100644 index 0000000..1c431d3 --- /dev/null +++ b/src/elements.rs @@ -0,0 +1,128 @@ +// elements.rs + +use nalgebra::*; +use crate::renderer::{Ray,Color}; +use crate::materials::Material; + +// Element root class +pub enum Element { + Sphere(Sphere), + Plane(Plane), +} + +impl Element { + pub fn pos(&self) -> Vec3 { + match *self { + Element::Sphere(ref s) => s.pos, + Element::Plane(ref p) => p.pos, + } + } + + pub fn color(&self) -> &Color { + match *self { + Element::Sphere(ref s) => &s.material.coloration, + Element::Plane(ref p) => &p.material.coloration, + } + } + + pub fn normal(&self, pos: Vec3) -> Vec3 { + match *self { + Element::Sphere(ref s) => pos - s.pos, + Element::Plane(ref p) => -p.normal, + } + } + + pub fn material(&self) -> &Material { + match *self { + Element::Sphere(ref s) => &s.material, + Element::Plane(ref p) => &p.material, + } + } +} + +// Lights + +pub struct LightSrc { + pub pos: Vec3, + pub intensity: f32, +} + +impl LightSrc { + pub fn new(pos: Vec3, intensity: f32) -> LightSrc { + LightSrc { + pos: pos, + intensity: intensity + } + } + + pub fn distance(&self, hit_point: Vec3) -> f64 { + let difference = self.pos - hit_point; + difference.norm() + } +} + +// Specific Elements + +pub struct Sphere { + pub pos: Vec3, + pub radius: f64, + pub material: Material, + +} + +impl Intersectable for Sphere { + // Implemented from + // http://kylehalladay.com/blog/tutorial/math/2013/12/24/Ray-Sphere-Intersection.html + fn intersect(&self, ray: &Ray) -> Option { + let l = self.pos - ray.pos; + let adj = l.dot(&ray.dir); + let d2 = l.dot(&l) - (adj * adj); + let radius2 = self.radius * self.radius; + + if d2 > radius2 { + return None; + } + + let thc = (radius2 - d2).sqrt(); + let t0 = adj - thc; + let t1 = adj + thc; + + if t0 < 0.0 && t1 < 0.0 { + None + } else if t0 < 0.0 { + Some(t1) + } else if t1 < 0.0 { + Some(t0) + } else { + let distance = if t0 < t1 { t0 } else { t1 }; + Some(distance) + } + } + +} + +pub struct Plane { + pub pos: Vec3, + pub normal: Vec3, + //color: Color, + pub material: Material, +} + +pub trait Intersectable { + fn intersect(&self, ray: &Ray) -> Option; +} + +impl Intersectable for Plane { + fn intersect(&self, ray: &Ray) -> Option { + let normal = &self.normal; + let denom = normal.dot(&ray.dir); + if denom > 1e-6 { + let v = self.pos - ray.pos; + let distance = v.dot(&normal) / denom; + if distance >= 0.0 { + return Some(distance); + } + } + None + } +} diff --git a/src/main.rs b/src/main.rs index e6d26eb..ce2879d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,5 +1,17 @@ use std::f32; -use std::ops::{Add,Mul}; +//use std::ops::{Add,Mul}; + +mod camera; +use crate::camera::PerspectiveCamera; + +mod renderer; +use crate::renderer::{Color,cast_ray}; + +mod materials; +use crate::materials::{Material,SurfaceType}; + +mod elements; +use crate::elements::{Plane,Sphere,Element,LightSrc}; #[macro_use] extern crate bmp; @@ -11,352 +23,30 @@ use nalgebra::*; use bmp::Image; use bmp::Pixel; -const BLACK: Color = Color { - red: 0.0, - green: 0.0, - blue: 0.0, -}; - -pub struct Ray { - pos: Vec3, - dir: Vec3 -} - -impl Ray { - fn new(pos: Vec3, dir: Vec3) -> Ray { - Ray { - pos: pos, - dir: dir - } - } - - fn at(&self, t: f64) -> Vec3 { - self.pos + t * self.dir - } -} - -struct LightSrc { - pos: Vec3, - intensity: f32, -} - -impl LightSrc { - fn new(pos: Vec3, intensity: f32) -> LightSrc { - LightSrc { - pos: pos, - intensity: intensity - } - } - - fn distance(&self, hit_point: Vec3) -> f64 { - let difference = self.pos - hit_point; - difference.norm() - } -} - -enum Element { - Sphere(Sphere), - Plane(Plane), -} - -impl Element { - fn pos(&self) -> Vec3 { - match *self { - Element::Sphere(ref s) => s.pos, - Element::Plane(ref p) => p.pos, - } - } - - fn color(&self) -> &Color { - match *self { - Element::Sphere(ref s) => &s.material.coloration, - Element::Plane(ref p) => &p.material.coloration, - } - } - - fn normal(&self, pos: Vec3) -> Vec3 { - match *self { - Element::Sphere(ref s) => pos - s.pos, - Element::Plane(ref p) => -p.normal, - } - } - - fn material(&self) -> &Material { - match *self { - Element::Sphere(ref s) => &s.material, - Element::Plane(ref p) => &p.material, - } - } -} - -pub struct OrthoCamera { - pos: Vec3, - output_img: bmp::Image, - elements: Vec, - //spheres: Vec, - light: LightSrc, - - shadow_bias: f64, - max_recursion_depth: u32 -} - -impl OrthoCamera { - fn trace(&self, ray: &Ray) -> Option { - self.elements.iter() - .filter_map(|s| s.intersect(ray).map(|d| Intersection::new(d, s) )) - .min_by(|i1, i2| i1.distance.partial_cmp(&i2.distance).unwrap()) - } -} - -enum SurfaceType { - Diffuse, - Reflective { reflectivity: f32 }, -} - -struct Material { - coloration: Color, - albedo: f32, - surface: SurfaceType -} - -impl Material { - fn new(coloration: Color, albedo: f32, surface: SurfaceType) -> Material { - Material { - coloration: coloration, - albedo: albedo, - surface: surface - } - } -} - - -#[derive(Copy, Clone)] -pub struct Color { - red: f32, - green: f32, - blue: f32, -} - -impl Color { - pub fn new(red: f32, green: f32, blue: f32) -> Color { - Color { - red: red, - green: green, - blue: blue - } - } -} - -impl Mul for Color { - type Output = Color; - - fn mul(self, other: Color) -> Color { - Color { - red: self.red * other.red, - green: self.green * other.green, - blue: self.blue * other.blue, - } - } -} - -impl Mul for Color { - type Output = Color; - - fn mul(self, other: f32) -> Color { - Color { - red: self.red * other, - green: self.green * other, - blue: self.blue * other, - } - } -} - -impl Add for Color { - type Output = Color; - - fn add(self, other: Color) -> Color { - Color { - red: self.red + other.red, - green: self.green + other.green, - blue: self.blue + other.blue, - } - } -} - -impl Mul for f32 { - type Output = Color; - - fn mul(self, other: Color) -> Color { - other * self - } -} - -pub struct Sphere { - pos: Vec3, - radius: f64, - material: Material, - -} - -impl Intersectable for Sphere { - // Implemented from - // http://kylehalladay.com/blog/tutorial/math/2013/12/24/Ray-Sphere-Intersection.html - fn intersect(&self, ray: &Ray) -> Option { - let l = self.pos - ray.pos; - let adj = l.dot(&ray.dir); - let d2 = l.dot(&l) - (adj * adj); - let radius2 = self.radius * self.radius; - - if d2 > radius2 { - return None; - } - - let thc = (radius2 - d2).sqrt(); - let t0 = adj - thc; - let t1 = adj + thc; - - if t0 < 0.0 && t1 < 0.0 { - None - } else if t0 < 0.0 { - Some(t1) - } else if t1 < 0.0 { - Some(t0) - } else { - let distance = if t0 < t1 { t0 } else { t1 }; - Some(distance) - } - } - -} - -pub struct Plane { - pos: Vec3, - normal: Vec3, - //color: Color, - material: Material, -} - -pub trait Intersectable { - fn intersect(&self, ray: &Ray) -> Option; -} - -impl Intersectable for Plane { - fn intersect(&self, ray: &Ray) -> Option { - let normal = &self.normal; - let denom = normal.dot(&ray.dir); - if denom > 1e-6 { - let v = self.pos - ray.pos; - let distance = v.dot(&normal) / denom; - if distance >= 0.0 { - return Some(distance); - } - } - None - } -} - -struct Intersection<'a> { - distance: f64, - object: &'a Element -} - -impl<'a> Intersection<'a> { - fn new<'b>(distance: f64, object: &'b Element) -> Intersection<'b> { - Intersection { - distance: distance, - object: & object - } - } -} - -impl Intersectable for Element { - fn intersect(&self, ray: &Ray) -> Option { - match *self { - Element::Sphere(ref s) => s.intersect(ray), - Element::Plane(ref p) => p.intersect(ray), - } - } -} - - -fn create_reflection(normal: Vec3, incident: Vec3, hit_point: Vec3, bias: f64) -> Ray { - Ray { - pos: hit_point + (normal.normalize()), - dir: incident - (2.0 * incident.dot(&normal) * normal), - } -} - -fn get_color(camera: &OrthoCamera, ray: &Ray, intersection: &Intersection, depth: u32) -> Color { - let hit_point = ray.pos + (ray.dir * intersection.distance); - let surface_normal = intersection.object.normal(hit_point); - - let material = intersection.object.material(); - - // TODO: Add Albedo - let mut color = shade_diffuse(camera, intersection.object, hit_point, surface_normal); - //return color; - - if let SurfaceType::Reflective { reflectivity } = material.surface { - let reflection_ray = create_reflection(surface_normal, ray.dir, hit_point, camera.shadow_bias); - color = color * (1.0 - reflectivity); - color = color + (cast_ray(&camera, &reflection_ray, depth + 1) * reflectivity); - } - color -} - -fn shade_diffuse(camera: &OrthoCamera, object: &Element, hit_point: Vec3, surface_normal: Vec3) -> Color { - let mut color = BLACK; - - // Light processing - // TODO: Support multiple lights - let direction_to_light = camera.light.pos - hit_point; - - let material = object.material(); - // TODO: Change light intensity to take hit_point for some reason (read source) - // https://github.com/bheisler/raytracer/blob/7130556181de7fc59eaa29346f5d4134db3e720e/src/rendering.rs#L195 - - // Shadow stuff - let shadow_ray = Ray { - pos: hit_point + surface_normal.normalize(), - dir: direction_to_light.normalize(), - }; - - let shadow_intersection = camera.trace(&shadow_ray); - let in_light = shadow_intersection.is_none() - || shadow_intersection.unwrap().distance > camera.light.distance(hit_point); - let light_intensity = if in_light { camera.light.intensity } else { 0.0 }; - - let light_power = (surface_normal.normalize().dot(&direction_to_light.normalize()) as f32).max(0.0); - let light_reflected = material.albedo / f32::consts::PI; - - let light_color = light_intensity * light_power * light_reflected; - color = color + (material.coloration * light_color); - - return color; -} - -pub fn cast_ray(camera: &OrthoCamera, ray: &Ray, depth: u32) -> Color { - if depth >= camera.max_recursion_depth { - return BLACK; - } - - let intersection = camera.trace(&ray); - intersection.map(|i| get_color(camera, &ray, &i, depth)).unwrap_or(BLACK) -} - fn main() { - let mut camera = OrthoCamera { - pos: Vec3::new(0.0, 0.0, -1000.0), + //let mut camera = OrthoCamera { + // pos: Vec3::new(0.0, 0.0, -1000.0), + // output_img: Image::new(2560,2560), + // elements: Vec::new(), + // lights: Vec::new(), + // shadow_bias: 1e-3, + // max_recursion_depth: 5 + //}; + let mut camera = PerspectiveCamera { + pos: Vec3::new(1280.0, 1280.0, -1000.0), output_img: Image::new(2560,2560), elements: Vec::new(), - light: LightSrc::new(Vec3::new(200.0, 800.0, 300.0), 5.0), + lights: Vec::new(), shadow_bias: 1e-3, - max_recursion_depth: 5 - }; + max_recursion_depth: 5, + fov: 90.0, + scene_width: 2560, + scene_height: 2560, +}; + + camera.lights.push(LightSrc::new(Vec3::new(200.0, 800.0, 300.0), 5.0)); + camera.lights.push(LightSrc::new(Vec3::new(1200.0, 800.0, 300.0), 5.0)); -// camera.spheres.push(Sphere::new(Vec3::new(125.0, 75.0, 100.0), 20.0)); -// camera.spheres.push(Sphere::new(Vec3::new(115.0, 175.0, 100.0), 60.0)); -// camera.spheres.push(Sphere::new(Vec3::new(0.0, 0.0, 100.0), 10.0)); for i in 0..15 { let mut rng = rand::thread_rng(); let x: f64 = rng.gen::() * 250.0 * 10.0; @@ -370,10 +60,8 @@ fn main() { pos: Vec3::new(x, y, 100.0), radius: radius, material: Material::new(Color::new(red, green, blue), 2.0, SurfaceType::Reflective { reflectivity: rng.gen::() }), - //material: Material::new(Color::new(red, green, blue), 2.0, SurfaceType::Diffuse), }; camera.elements.push(Element::Sphere(sphere)); - //camera.spheres.push(Sphere::new(Vec3::new(x, y, 100.0), radius)); } let back_plane = Plane { @@ -396,7 +84,6 @@ fn main() { let center_sphere = Sphere { pos: Vec3::new(1280.0, 1290.0, 1000.0), radius: 300.0, - //material: Material::new(Color::new(20.0, 20.0, 20.0), 2.0, SurfaceType::Diffuse), material: Material::new(Color::new(255.0, 255.0, 255.0), 2.0, SurfaceType::Reflective { reflectivity: 0.8 }), }; camera.elements.push(Element::Sphere(center_sphere)); @@ -405,67 +92,23 @@ fn main() { pos: Vec3::new(200.0, 1800.0, 500.0), radius: 200.0, material: Material::new(Color::new(255.0, 20.0, 20.0), 2.0, SurfaceType::Reflective { reflectivity: 0.1 }), - //material: Material::new(Color::new(20.0, 20.0, 200.0), 2.0, SurfaceType::Diffuse), }; camera.elements.push(Element::Sphere(left_sphere)); let top_sphere = Sphere { pos: Vec3::new(1080.0, 700.0, 500.0), radius: 200.0, - //material: Material::new(Color::new(255.0, 20.0, 20.0), 2.0, SurfaceType::Reflective { reflectivity: 0.3 }), material: Material::new(Color::new(255.0, 20.0, 20.0), 2.0, SurfaceType::Diffuse), }; camera.elements.push(Element::Sphere(top_sphere)); - //let sky_sphere = Sphere { - // pos: Vec3::new(1280.0, 1280.0, 0.0), - // radius: 50000.0, - // material: Material::new(Color::new(255.0, 20.0, 20.0), 2.0, SurfaceType::Reflective { reflectivity: 1.0 }) - //}; - //camera.spheres.push(sky_sphere); - println!("Raytracing ..."); for (x, y) in camera.output_img.coordinates() { camera.output_img.set_pixel(x, y, px!(20, 20, 20)); - let prime_ray = Ray::new(Vec3::new(x as f64, y as f64, camera.pos.z as f64), Vec3::new(0.0, 0.0, 1.0)); + //let prime_ray = Ray::new(Vec3::new(x as f64, y as f64, camera.pos.z as f64), Vec3::new(0.0, 0.0, 1.0)); + let prime_ray = camera.create_prime(x, y); let pixel = cast_ray(&camera, &prime_ray, 0); camera.output_img.set_pixel(x, y, px!(pixel.red, pixel.green, pixel.blue)); - //let result = camera.trace(&ray); -// match result { -// Some(intersection) => { -// let hit_point = ray.at(intersection.distance); -// let object_pos = intersection.object.pos(); -// let normal = intersection.object.normal(hit_point); -// let light_dir = camera.light.pos - hit_point; //hit_point - camera.light.pos; -// let light_color = &intersection.object.color(); //&intersection.object.material.coloration; -// // -// let shadow_ray = Ray { -// pos: hit_point + (normal.normalize()), -// dir: light_dir.normalize() -// }; -// -// //if let SurfaceType::Reflective { reflectivity } = intersection.object.material().surface { -// // let reflection_ray = create_reflection(normal, ray.dir, hit_point, camera.shadow_bias); -// // color = color * (1.0 - reflectivity); -// // color = color + (camera.trace(&reflection_ray, depth + 1) * -// //} -// -// let shadow_intersection = camera.trace(&shadow_ray); -// let in_light = shadow_intersection.is_none() || shadow_intersection.unwrap().distance > camera.light.distance(hit_point); -// let light_intensity = if in_light { camera.light.intensity } else { 0.0 }; -// let light_power = (normal.normalize().dot(&light_dir.normalize()) as f64).max(0.0) * light_intensity; -// let light_reflected = 2.0 / std::f64::consts::PI; -// -// let red = light_color.red * light_power;// * light_reflected; -// let green = light_color.green * light_power;// * light_reflected; -// let blue = light_color.blue * light_power;// * light_reflected; -// -// camera.output_img.set_pixel(x, y, px!(red, green, blue)) -// }, -// None => { } -// } -// -// } } let _ = camera.output_img.save("img.bmp"); diff --git a/src/materials.rs b/src/materials.rs new file mode 100644 index 0000000..3cf4b0d --- /dev/null +++ b/src/materials.rs @@ -0,0 +1,24 @@ +// materials.rs + +use crate::Color; + +pub struct Material { + pub coloration: Color, + pub albedo: f32, + pub surface: SurfaceType +} + +impl Material { + pub fn new(coloration: Color, albedo: f32, surface: SurfaceType) -> Material { + Material { + coloration: coloration, + albedo: albedo, + surface: surface + } + } +} + +pub enum SurfaceType { + Diffuse, + Reflective { reflectivity: f32 }, +} diff --git a/src/renderer.rs b/src/renderer.rs new file mode 100644 index 0000000..52605e3 --- /dev/null +++ b/src/renderer.rs @@ -0,0 +1,188 @@ +// renderer.rs + +use std::f32; +use nalgebra::*; +use std::ops::{Add,Mul}; + +use crate::camera::*; + +use crate::elements::{Element,Intersectable}; +use crate::materials::SurfaceType; + +const BLACK: Color = Color { + red: 0.0, + green: 0.0, + blue: 0.0, +}; + +pub struct Ray { + pub pos: Vec3, + pub dir: Vec3 +} + +impl Ray { + fn new(pos: Vec3, dir: Vec3) -> Ray { + Ray { + pos: pos, + dir: dir + } + } + + fn at(&self, t: f64) -> Vec3 { + self.pos + t * self.dir + } +} + + + +#[derive(Copy, Clone)] +pub struct Color { + pub red: f32, + pub green: f32, + pub blue: f32, +} + +impl Color { + pub fn new(red: f32, green: f32, blue: f32) -> Color { + Color { + red: red, + green: green, + blue: blue + } + } +} + +impl Mul for Color { + type Output = Color; + + fn mul(self, other: Color) -> Color { + Color { + red: self.red * other.red, + green: self.green * other.green, + blue: self.blue * other.blue, + } + } +} + +impl Mul for Color { + type Output = Color; + + fn mul(self, other: f32) -> Color { + Color { + red: self.red * other, + green: self.green * other, + blue: self.blue * other, + } + } +} + +impl Add for Color { + type Output = Color; + + fn add(self, other: Color) -> Color { + Color { + red: self.red + other.red, + green: self.green + other.green, + blue: self.blue + other.blue, + } + } +} + +impl Mul for f32 { + type Output = Color; + + fn mul(self, other: Color) -> Color { + other * self + } +} + + +pub struct Intersection<'a> { + pub distance: f64, + pub object: &'a Element +} + +impl<'a> Intersection<'a> { + pub fn new<'b>(distance: f64, object: &'b Element) -> Intersection<'b> { + Intersection { + distance: distance, + object: & object + } + } +} + +impl Intersectable for Element { + fn intersect(&self, ray: &Ray) -> Option { + match *self { + Element::Sphere(ref s) => s.intersect(ray), + Element::Plane(ref p) => p.intersect(ray), + } + } +} + + +fn create_reflection(normal: Vec3, incident: Vec3, hit_point: Vec3, bias: f64) -> Ray { + Ray { + pos: hit_point + (normal.normalize()), + dir: incident - (2.0 * incident.dot(&normal) * normal), + } +} + +fn get_color(camera: &PerspectiveCamera, ray: &Ray, intersection: &Intersection, depth: u32) -> Color { + let hit_point = ray.pos + (ray.dir * intersection.distance); + let surface_normal = intersection.object.normal(hit_point); + + let material = intersection.object.material(); + + let mut color = shade_diffuse(camera, intersection.object, hit_point, surface_normal); + + if let SurfaceType::Reflective { reflectivity } = material.surface { + let reflection_ray = create_reflection(surface_normal, ray.dir, hit_point, camera.shadow_bias); + color = color * (1.0 - reflectivity); + color = color + (cast_ray(&camera, &reflection_ray, depth + 1) * reflectivity); + } + color +} + +fn shade_diffuse(camera: &PerspectiveCamera, object: &Element, hit_point: Vec3, surface_normal: Vec3) -> Color { + let mut color = BLACK; + + // Light processing + // TODO: Support multiple lights + for light in camera.lights.iter() { + let direction_to_light = light.pos - hit_point; + + let material = object.material(); + // TODO: Change light intensity to take hit_point for some reason (read source) + // https://github.com/bheisler/raytracer/blob/7130556181de7fc59eaa29346f5d4134db3e720e/src/rendering.rs#L195 + + // Shadow stuff + let shadow_ray = Ray { + pos: hit_point + surface_normal.normalize(), + dir: direction_to_light.normalize(), + }; + + let shadow_intersection = camera.trace(&shadow_ray); + let in_light = shadow_intersection.is_none() + || shadow_intersection.unwrap().distance > light.distance(hit_point); + let light_intensity = if in_light { light.intensity } else { 0.0 }; + + let light_power = (surface_normal.normalize().dot(&direction_to_light.normalize()) as f32).max(0.0); + let light_reflected = material.albedo / f32::consts::PI; + + let light_color = light_intensity * light_power * light_reflected; + color = color + (material.coloration * light_color); + } + + return color; +} + +pub fn cast_ray(camera: &PerspectiveCamera, ray: &Ray, depth: u32) -> Color { + if depth >= camera.max_recursion_depth { + return BLACK; + } + + let intersection = camera.trace(&ray); + intersection.map(|i| get_color(camera, &ray, &i, depth)).unwrap_or(BLACK) +} +