use crate::{
asset::{state::LoadError, untyped::ResourceKind},
core::{
algebra::{Matrix4, Point3, Vector2, Vector3, Vector4},
color::Color,
math::{aabb::AxisAlignedBoundingBox, frustum::Frustum, ray::Ray, Rect},
pool::Handle,
reflect::prelude::*,
type_traits::prelude::*,
uuid::{uuid, Uuid},
uuid_provider,
variable::InheritableVariable,
visitor::{Visit, VisitResult, Visitor},
},
graph::SceneGraph,
resource::texture::{
TextureKind, TexturePixelKind, TextureResource, TextureResourceExtension, TextureWrapMode,
},
scene::{
base::{Base, BaseBuilder},
debug::SceneDrawingContext,
graph::Graph,
node::constructor::NodeConstructor,
node::{Node, NodeTrait, UpdateContext},
},
};
use fyrox_graph::constructor::ConstructorProvider;
use serde::{Deserialize, Serialize};
use std::{
fmt::{Display, Formatter},
ops::{Deref, DerefMut},
};
use strum_macros::{AsRefStr, EnumString, VariantNames};
#[derive(Reflect, Clone, Debug, PartialEq, Visit, Serialize, Deserialize)]
pub struct PerspectiveProjection {
#[reflect(min_value = 0.0, max_value = 6.28, step = 0.1)]
pub fov: f32,
#[reflect(min_value = 0.0, step = 0.1)]
pub z_near: f32,
#[reflect(min_value = 0.0, step = 0.1)]
pub z_far: f32,
}
impl Default for PerspectiveProjection {
fn default() -> Self {
Self {
fov: 75.0f32.to_radians(),
z_near: 0.025,
z_far: 2048.0,
}
}
}
impl PerspectiveProjection {
#[inline]
pub fn matrix(&self, frame_size: Vector2<f32>) -> Matrix4<f32> {
let limit = 10.0 * f32::EPSILON;
let z_near = self.z_far.min(self.z_near);
let mut z_far = self.z_far.max(self.z_near);
if z_far - z_near < limit {
z_far += limit;
}
Matrix4::new_perspective(
(frame_size.x / frame_size.y).max(limit),
self.fov,
z_near,
z_far,
)
}
}
#[derive(Reflect, Clone, Debug, PartialEq, Visit, Serialize, Deserialize)]
pub struct OrthographicProjection {
#[reflect(min_value = 0.0, step = 0.1)]
pub z_near: f32,
#[reflect(min_value = 0.0, step = 0.1)]
pub z_far: f32,
#[reflect(step = 0.1)]
pub vertical_size: f32,
}
impl Default for OrthographicProjection {
fn default() -> Self {
Self {
z_near: 0.0,
z_far: 2048.0,
vertical_size: 5.0,
}
}
}
impl OrthographicProjection {
#[inline]
pub fn matrix(&self, frame_size: Vector2<f32>) -> Matrix4<f32> {
fn clamp_to_limit_signed(value: f32, limit: f32) -> f32 {
if value < 0.0 && -value < limit {
-limit
} else if value >= 0.0 && value < limit {
limit
} else {
value
}
}
let limit = 10.0 * f32::EPSILON;
let aspect = (frame_size.x / frame_size.y).max(limit);
let vertical_size = clamp_to_limit_signed(self.vertical_size, limit);
let horizontal_size = clamp_to_limit_signed(aspect * vertical_size, limit);
let z_near = self.z_far.min(self.z_near);
let mut z_far = self.z_far.max(self.z_near);
if z_far - z_near < limit {
z_far += limit;
}
let left = -horizontal_size;
let top = vertical_size;
let right = horizontal_size;
let bottom = -vertical_size;
Matrix4::new_orthographic(left, right, bottom, top, z_near, z_far)
}
}
#[derive(
Reflect,
Clone,
Debug,
PartialEq,
Visit,
AsRefStr,
EnumString,
VariantNames,
Serialize,
Deserialize,
)]
pub enum Projection {
Perspective(PerspectiveProjection),
Orthographic(OrthographicProjection),
}
uuid_provider!(Projection = "0eb5bec0-fc4e-4945-99b6-e6c5392ad971");
impl Projection {
#[inline]
#[must_use]
pub fn with_z_near(mut self, z_near: f32) -> Self {
match self {
Projection::Perspective(ref mut v) => v.z_near = z_near,
Projection::Orthographic(ref mut v) => v.z_near = z_near,
}
self
}
#[inline]
#[must_use]
pub fn with_z_far(mut self, z_far: f32) -> Self {
match self {
Projection::Perspective(ref mut v) => v.z_far = z_far,
Projection::Orthographic(ref mut v) => v.z_far = z_far,
}
self
}
#[inline]
pub fn set_z_near(&mut self, z_near: f32) {
match self {
Projection::Perspective(v) => v.z_near = z_near,
Projection::Orthographic(v) => v.z_near = z_near,
}
}
#[inline]
pub fn set_z_far(&mut self, z_far: f32) {
match self {
Projection::Perspective(v) => v.z_far = z_far,
Projection::Orthographic(v) => v.z_far = z_far,
}
}
#[inline]
pub fn z_near(&self) -> f32 {
match self {
Projection::Perspective(v) => v.z_near,
Projection::Orthographic(v) => v.z_near,
}
}
#[inline]
pub fn z_far(&self) -> f32 {
match self {
Projection::Perspective(v) => v.z_far,
Projection::Orthographic(v) => v.z_far,
}
}
#[inline]
pub fn matrix(&self, frame_size: Vector2<f32>) -> Matrix4<f32> {
match self {
Projection::Perspective(v) => v.matrix(frame_size),
Projection::Orthographic(v) => v.matrix(frame_size),
}
}
#[inline]
pub fn is_perspective(&self) -> bool {
matches!(self, Projection::Perspective(_))
}
#[inline]
pub fn is_orthographic(&self) -> bool {
matches!(self, Projection::Orthographic(_))
}
}
impl Default for Projection {
fn default() -> Self {
Self::Perspective(PerspectiveProjection::default())
}
}
#[derive(
Visit,
Copy,
Clone,
PartialEq,
Debug,
Reflect,
AsRefStr,
EnumString,
VariantNames,
Serialize,
Deserialize,
)]
pub enum Exposure {
Auto {
#[reflect(min_value = 0.0, step = 0.1)]
min_luminance: f32,
#[reflect(min_value = 0.0, step = 0.1)]
max_luminance: f32,
},
Manual(f32),
}
uuid_provider!(Exposure = "0e35ee3d-8baa-4b0c-b3dd-6c31a08c121e");
impl Default for Exposure {
fn default() -> Self {
Self::Manual(1.0)
}
}
#[derive(Debug, Visit, Reflect, Clone, ComponentProvider)]
#[reflect(derived_type = "Node")]
pub struct Camera {
base: Base,
#[reflect(setter = "set_projection")]
projection: InheritableVariable<Projection>,
#[reflect(setter = "set_viewport")]
viewport: InheritableVariable<Rect<f32>>,
#[reflect(setter = "set_enabled")]
enabled: InheritableVariable<bool>,
#[reflect(setter = "set_environment")]
environment: InheritableVariable<Option<TextureResource>>,
#[reflect(setter = "set_exposure")]
exposure: InheritableVariable<Exposure>,
#[reflect(setter = "set_color_grading_lut")]
color_grading_lut: InheritableVariable<Option<ColorGradingLut>>,
#[reflect(setter = "set_color_grading_enabled")]
color_grading_enabled: InheritableVariable<bool>,
#[reflect(setter = "set_hdr_adaptation_speed")]
hdr_adaptation_speed: InheritableVariable<f32>,
#[reflect(setter = "set_render_target")]
#[visit(skip)]
render_target: Option<TextureResource>,
#[visit(skip)]
#[reflect(hidden)]
view_matrix: Matrix4<f32>,
#[visit(skip)]
#[reflect(hidden)]
projection_matrix: Matrix4<f32>,
}
impl Deref for Camera {
type Target = Base;
fn deref(&self) -> &Self::Target {
&self.base
}
}
impl DerefMut for Camera {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.base
}
}
impl Default for Camera {
fn default() -> Self {
CameraBuilder::new(BaseBuilder::new()).build_camera()
}
}
impl TypeUuidProvider for Camera {
fn type_uuid() -> Uuid {
uuid!("198d3aca-433c-4ce1-bb25-3190699b757f")
}
}
pub enum FitParameters {
Perspective {
position: Vector3<f32>,
distance: f32,
},
Orthographic {
position: Vector3<f32>,
vertical_size: f32,
},
}
impl FitParameters {
fn fallback_perspective() -> Self {
Self::Perspective {
position: Default::default(),
distance: 1.0,
}
}
}
impl Camera {
#[inline]
pub fn calculate_matrices(&mut self, frame_size: Vector2<f32>) {
let pos = self.base.global_position();
let look = self.base.look_vector();
let up = self.base.up_vector();
self.view_matrix = Matrix4::look_at_rh(&Point3::from(pos), &Point3::from(pos + look), &up);
self.projection_matrix = self.projection.matrix(frame_size);
}
pub fn set_viewport(&mut self, mut viewport: Rect<f32>) -> Rect<f32> {
viewport.position.x = viewport.position.x.clamp(0.0, 1.0);
viewport.position.y = viewport.position.y.clamp(0.0, 1.0);
viewport.size.x = viewport.size.x.clamp(0.0, 1.0);
viewport.size.y = viewport.size.y.clamp(0.0, 1.0);
self.viewport.set_value_and_mark_modified(viewport)
}
pub fn viewport(&self) -> Rect<f32> {
*self.viewport
}
#[inline]
pub fn viewport_pixels(&self, frame_size: Vector2<f32>) -> Rect<i32> {
Rect::new(
(self.viewport.x() * frame_size.x) as i32,
(self.viewport.y() * frame_size.y) as i32,
((self.viewport.w() * frame_size.x) as i32).max(1),
((self.viewport.h() * frame_size.y) as i32).max(1),
)
}
#[inline]
pub fn view_projection_matrix(&self) -> Matrix4<f32> {
self.projection_matrix * self.view_matrix
}
#[inline]
pub fn projection_matrix(&self) -> Matrix4<f32> {
self.projection_matrix
}
#[inline]
pub fn view_matrix(&self) -> Matrix4<f32> {
self.view_matrix
}
#[inline]
pub fn inv_view_matrix(&self) -> Option<Matrix4<f32>> {
self.view_matrix.try_inverse()
}
#[inline]
pub fn projection(&self) -> &Projection {
&self.projection
}
#[inline]
pub fn projection_value(&self) -> Projection {
(*self.projection).clone()
}
#[inline]
pub fn projection_mut(&mut self) -> &mut Projection {
self.projection.get_value_mut_and_mark_modified()
}
#[inline]
pub fn set_projection(&mut self, projection: Projection) -> Projection {
self.projection.set_value_and_mark_modified(projection)
}
#[inline]
pub fn is_enabled(&self) -> bool {
*self.enabled
}
#[inline]
pub fn set_enabled(&mut self, enabled: bool) -> bool {
self.enabled.set_value_and_mark_modified(enabled)
}
pub fn set_environment(
&mut self,
environment: Option<TextureResource>,
) -> Option<TextureResource> {
self.environment.set_value_and_mark_modified(environment)
}
pub fn environment_mut(&mut self) -> Option<&mut TextureResource> {
self.environment.get_value_mut_and_mark_modified().as_mut()
}
pub fn environment_ref(&self) -> Option<&TextureResource> {
self.environment.as_ref()
}
pub fn environment_map(&self) -> Option<TextureResource> {
(*self.environment).clone()
}
pub fn set_hdr_adaptation_speed(&mut self, speed: f32) -> f32 {
self.hdr_adaptation_speed.set_value_and_mark_modified(speed)
}
pub fn hdr_adaptation_speed(&self) -> f32 {
*self.hdr_adaptation_speed
}
pub fn make_ray(&self, screen_coord: Vector2<f32>, screen_size: Vector2<f32>) -> Ray {
let viewport = self.viewport_pixels(screen_size);
let nx = screen_coord.x / (viewport.w() as f32) * 2.0 - 1.0;
let ny = (viewport.h() as f32 - screen_coord.y) / (viewport.h() as f32) * 2.0 - 1.0;
let inv_view_proj = self
.view_projection_matrix()
.try_inverse()
.unwrap_or_default();
let near = inv_view_proj * Vector4::new(nx, ny, -1.0, 1.0);
let far = inv_view_proj * Vector4::new(nx, ny, 1.0, 1.0);
let begin = near.xyz().scale(1.0 / near.w);
let end = far.xyz().scale(1.0 / far.w);
Ray::from_two_points(begin, end)
}
#[inline]
#[must_use]
pub fn fit(
&self,
aabb: &AxisAlignedBoundingBox,
aspect_ratio: f32,
scale: f32,
) -> FitParameters {
if aabb.is_invalid_or_degenerate() {
return FitParameters::fallback_perspective();
}
let look_vector = self
.look_vector()
.try_normalize(f32::EPSILON)
.unwrap_or_default();
match self.projection.deref() {
Projection::Perspective(perspective) => {
let radius = aabb.half_extents().max();
let denominator = (perspective.fov * 0.5).sin();
if denominator == 0.0 {
return FitParameters::fallback_perspective();
}
let distance = radius / denominator * scale;
FitParameters::Perspective {
position: aabb.center() - look_vector.scale(distance),
distance,
}
}
Projection::Orthographic(_) => {
let mut min_x = f32::MAX;
let mut min_y = f32::MAX;
let mut max_x = -f32::MAX;
let mut max_y = -f32::MAX;
let inv = self.global_transform().try_inverse().unwrap_or_default();
for point in aabb.corners() {
let local = inv.transform_point(&Point3::from(point));
if local.x < min_x {
min_x = local.x;
}
if local.y < min_y {
min_y = local.y;
}
if local.x > max_x {
max_x = local.x;
}
if local.y > max_y {
max_y = local.y;
}
}
FitParameters::Orthographic {
position: aabb.center()
- look_vector.scale((aabb.max - aabb.min).norm() * scale),
vertical_size: (max_y - min_y).max((max_x - min_x) * aspect_ratio) * scale,
}
}
}
}
#[inline]
pub fn frustum(&self) -> Frustum {
Frustum::from_view_projection_matrix(self.view_projection_matrix()).unwrap_or_default()
}
pub fn project(
&self,
world_pos: Vector3<f32>,
screen_size: Vector2<f32>,
) -> Option<Vector2<f32>> {
let viewport = self.viewport_pixels(screen_size);
let proj = self.view_projection_matrix()
* Vector4::new(world_pos.x, world_pos.y, world_pos.z, 1.0);
if proj.w != 0.0 && proj.z >= 0.0 {
let k = (1.0 / proj.w) * 0.5;
Some(Vector2::new(
viewport.x() as f32 + viewport.w() as f32 * (proj.x * k + 0.5),
viewport.h() as f32
- (viewport.y() as f32 + viewport.h() as f32 * (proj.y * k + 0.5)),
))
} else {
None
}
}
pub fn set_color_grading_lut(
&mut self,
lut: Option<ColorGradingLut>,
) -> Option<ColorGradingLut> {
self.color_grading_lut.set_value_and_mark_modified(lut)
}
pub fn color_grading_lut(&self) -> Option<ColorGradingLut> {
(*self.color_grading_lut).clone()
}
pub fn color_grading_lut_ref(&self) -> Option<&ColorGradingLut> {
self.color_grading_lut.as_ref()
}
pub fn set_color_grading_enabled(&mut self, enable: bool) -> bool {
self.color_grading_enabled
.set_value_and_mark_modified(enable)
}
pub fn color_grading_enabled(&self) -> bool {
*self.color_grading_enabled
}
pub fn set_exposure(&mut self, exposure: Exposure) -> Exposure {
self.exposure.set_value_and_mark_modified(exposure)
}
pub fn exposure(&self) -> Exposure {
*self.exposure
}
pub fn set_render_target(
&mut self,
render_target: Option<TextureResource>,
) -> Option<TextureResource> {
std::mem::replace(&mut self.render_target, render_target)
}
pub fn render_target(&self) -> Option<&TextureResource> {
self.render_target.as_ref()
}
}
impl ConstructorProvider<Node, Graph> for Camera {
fn constructor() -> NodeConstructor {
NodeConstructor::new::<Self>().with_variant("Camera", |_| {
CameraBuilder::new(BaseBuilder::new().with_name("Camera"))
.build_node()
.into()
})
}
}
impl NodeTrait for Camera {
#[inline]
fn local_bounding_box(&self) -> AxisAlignedBoundingBox {
self.base.local_bounding_box()
}
fn world_bounding_box(&self) -> AxisAlignedBoundingBox {
self.base.world_bounding_box()
}
fn id(&self) -> Uuid {
Self::type_uuid()
}
fn update(&mut self, context: &mut UpdateContext) {
let frame_size = if let Some(TextureKind::Rectangle { width, height }) = self
.render_target
.as_ref()
.and_then(|rt| rt.data_ref().as_loaded_ref().map(|rt| rt.kind()))
{
Vector2::new(width as f32, height as f32)
} else {
context.frame_size
};
self.calculate_matrices(frame_size);
}
fn debug_draw(&self, ctx: &mut SceneDrawingContext) {
let transform = self.global_transform_without_scaling();
ctx.draw_pyramid(
self.frustum().center(),
self.frustum().right_top_front_corner(),
self.frustum().left_top_front_corner(),
self.frustum().left_bottom_front_corner(),
self.frustum().right_bottom_front_corner(),
Color::GREEN,
transform,
);
}
}
#[derive(Debug)]
pub enum ColorGradingLutCreationError {
NotEnoughData {
required: usize,
current: usize,
},
InvalidPixelFormat(TexturePixelKind),
Texture(LoadError),
}
impl Display for ColorGradingLutCreationError {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
ColorGradingLutCreationError::NotEnoughData { required, current } => {
write!(
f,
"There is not enough data in provided \
texture to build LUT. Required: {required}, current: {current}.",
)
}
ColorGradingLutCreationError::InvalidPixelFormat(v) => {
write!(
f,
"Pixel format is not supported. It must be either RGB8 \
or RGBA8, but texture has {v:?} pixel format"
)
}
ColorGradingLutCreationError::Texture(v) => {
write!(f, "Texture load error: {v}")
}
}
}
}
#[derive(Visit, Clone, Default, PartialEq, Debug, Reflect, Eq)]
pub struct ColorGradingLut {
unwrapped_lut: Option<TextureResource>,
#[visit(skip)]
#[reflect(hidden)]
lut: Option<TextureResource>,
}
uuid_provider!(ColorGradingLut = "bca9c90a-7cde-4960-8814-c132edfc9614");
impl ColorGradingLut {
pub async fn new(unwrapped_lut: TextureResource) -> Result<Self, ColorGradingLutCreationError> {
match unwrapped_lut.await {
Ok(unwrapped_lut) => {
let data = unwrapped_lut.data_ref();
if data.pixel_kind() != TexturePixelKind::RGBA8
&& data.pixel_kind() != TexturePixelKind::RGB8
{
return Err(ColorGradingLutCreationError::InvalidPixelFormat(
data.pixel_kind(),
));
}
let bytes = data.data();
const RGBA8_SIZE: usize = 16 * 16 * 16 * 4;
const RGB8_SIZE: usize = 16 * 16 * 16 * 3;
if data.pixel_kind() == TexturePixelKind::RGBA8 {
if bytes.len() != RGBA8_SIZE {
return Err(ColorGradingLutCreationError::NotEnoughData {
required: RGBA8_SIZE,
current: bytes.len(),
});
}
} else if bytes.len() != RGB8_SIZE {
return Err(ColorGradingLutCreationError::NotEnoughData {
required: RGB8_SIZE,
current: bytes.len(),
});
}
let pixel_size = if data.pixel_kind() == TexturePixelKind::RGBA8 {
4
} else {
3
};
let mut lut_bytes = Vec::with_capacity(16 * 16 * 16 * 3);
for z in 0..16 {
for y in 0..16 {
for x in 0..16 {
let pixel_index = z * 16 + y * 16 * 16 + x;
let pixel_byte_pos = pixel_index * pixel_size;
lut_bytes.push(bytes[pixel_byte_pos]); lut_bytes.push(bytes[pixel_byte_pos + 1]); lut_bytes.push(bytes[pixel_byte_pos + 2]); }
}
}
let lut = TextureResource::from_bytes(
Uuid::new_v4(),
TextureKind::Volume {
width: 16,
height: 16,
depth: 16,
},
TexturePixelKind::RGB8,
lut_bytes,
ResourceKind::Embedded,
)
.unwrap();
let mut lut_ref = lut.data_ref();
lut_ref.set_s_wrap_mode(TextureWrapMode::ClampToEdge);
lut_ref.set_t_wrap_mode(TextureWrapMode::ClampToEdge);
drop(lut_ref);
drop(data);
Ok(Self {
lut: Some(lut),
unwrapped_lut: Some(unwrapped_lut),
})
}
Err(e) => Err(ColorGradingLutCreationError::Texture(e)),
}
}
pub fn unwrapped_lut(&self) -> TextureResource {
self.unwrapped_lut.clone().unwrap()
}
pub fn lut(&self) -> TextureResource {
self.lut.clone().unwrap()
}
pub fn lut_ref(&self) -> &TextureResource {
self.lut.as_ref().unwrap()
}
}
pub struct CameraBuilder {
base_builder: BaseBuilder,
fov: f32,
z_near: f32,
z_far: f32,
viewport: Rect<f32>,
enabled: bool,
environment: Option<TextureResource>,
exposure: Exposure,
color_grading_lut: Option<ColorGradingLut>,
color_grading_enabled: bool,
projection: Projection,
render_target: Option<TextureResource>,
hdr_adaptation_speed: f32,
}
impl CameraBuilder {
pub fn new(base_builder: BaseBuilder) -> Self {
Self {
enabled: true,
base_builder,
fov: 75.0f32.to_radians(),
z_near: 0.025,
z_far: 2048.0,
viewport: Rect::new(0.0, 0.0, 1.0, 1.0),
environment: None,
exposure: Default::default(),
color_grading_lut: None,
color_grading_enabled: false,
projection: Projection::default(),
render_target: None,
hdr_adaptation_speed: 0.5,
}
}
pub fn with_fov(mut self, fov: f32) -> Self {
self.fov = fov;
self
}
pub fn with_z_near(mut self, z_near: f32) -> Self {
self.z_near = z_near;
self
}
pub fn with_z_far(mut self, z_far: f32) -> Self {
self.z_far = z_far;
self
}
pub fn with_viewport(mut self, viewport: Rect<f32>) -> Self {
self.viewport = viewport;
self
}
pub fn enabled(mut self, enabled: bool) -> Self {
self.enabled = enabled;
self
}
pub fn with_environment(mut self, environment: TextureResource) -> Self {
self.environment = Some(environment);
self
}
pub fn with_color_grading_lut(mut self, lut: ColorGradingLut) -> Self {
self.color_grading_lut = Some(lut);
self
}
pub fn with_color_grading_enabled(mut self, enabled: bool) -> Self {
self.color_grading_enabled = enabled;
self
}
pub fn with_exposure(mut self, exposure: Exposure) -> Self {
self.exposure = exposure;
self
}
pub fn with_projection(mut self, projection: Projection) -> Self {
self.projection = projection;
self
}
pub fn with_render_target(mut self, render_target: Option<TextureResource>) -> Self {
self.render_target = render_target;
self
}
pub fn with_hdr_adaptation_speed(mut self, speed: f32) -> Self {
self.hdr_adaptation_speed = speed;
self
}
pub fn build_camera(self) -> Camera {
Camera {
enabled: self.enabled.into(),
base: self.base_builder.build_base(),
projection: self.projection.into(),
viewport: self.viewport.into(),
view_matrix: Matrix4::identity(),
projection_matrix: Matrix4::identity(),
environment: self.environment.into(),
exposure: self.exposure.into(),
color_grading_lut: self.color_grading_lut.into(),
color_grading_enabled: self.color_grading_enabled.into(),
hdr_adaptation_speed: self.hdr_adaptation_speed.into(),
render_target: self.render_target,
}
}
pub fn build_node(self) -> Node {
Node::new(self.build_camera())
}
pub fn build(self, graph: &mut Graph) -> Handle<Camera> {
graph.add_node(self.build_node()).to_variant()
}
}