mod animator;
mod api;
mod clip;
mod collect;
mod compute;
mod effect;
pub(crate) mod gi;
mod gltf;
mod ik;
pub(crate) mod instances;
pub use instances::{InstanceBatch, InstanceBatchOptions};
mod lod;
pub(crate) mod occlusion;
pub use gi::{GlobalIllumination, MAX_GI_PROBES, MAX_GI_PROBES_PER_FRAME};
pub use ik::{look_at_joint, two_bone_ik};
pub use lod::{LodGroup, LodLevel, LodPick, LodRenderer, LodSettings, LodShadow};
pub(crate) mod lights;
mod mesh;
pub use lights::{LIGHT_SHADOW_ATLAS_SIZE, LIGHT_SHADOW_SIZE, MAX_LIGHT_SHADOW_VIEWS, PointLight3D, SpotLight3D};
pub(crate) use lights::{MAX_LIGHT_SHADOW_RESOLUTION, MIN_LIGHT_SHADOW_RESOLUTION};
pub(crate) mod shadow_atlas;
pub(crate) use shadow_atlas::{MAX_LIGHT_SHADOW_ATLAS, MIN_LIGHT_SHADOW_ATLAS};
pub(crate) mod particles;
pub use particles::{
ParticleBlend3D, ParticleCurve, ParticleEmitter3D, ParticleGradient, ParticleSettings3D, ParticleShape3D,
ParticleSpace3D,
};
mod decal;
mod post;
pub(crate) mod projected;
pub use projected::{Decal3D, MAX_DECALS, MAX_DECALS_PER_CLUSTER, ReceivesDecals};
pub(crate) mod probe;
pub use probe::ReflectionProbe3D;
mod quality;
mod skin;
pub(crate) mod strips;
pub use strips::{Line3D, Trail3D};
pub(crate) mod text3d;
pub use text3d::{Text3D, TextAlign3D};
mod far_terrain;
mod sky;
mod target;
mod terrain;
pub use far_terrain::FarTerrain;
mod texture_array;
pub use animator::{
AnimationCondition, AnimationEvent, AnimationGraph, AnimationLayer, AnimationParameter, AnimationState,
AnimationTransition, Animator, JointMask, LayerBlend, Motion, MotionChild, RootMotion,
};
pub(crate) use api::GpuMaterialDesc;
#[cfg(feature = "window")]
pub(crate) use api::MaterialTextures;
pub use clip::{AnimationClip, AnimationTrack, Interpolation, JointAnimation, MorphAnimation};
pub(crate) mod motion;
#[cfg(feature = "window")]
pub(crate) use collect::{FADED, Instance3D, MAX_CASCADES, MAX_SHADOW_VIEWS, shadow_views};
pub(crate) use collect::{Frame3D, MAX_SHADOW_RESOLUTION, MIN_SHADOW_RESOLUTION, SHADOW_MAP_SIZE};
pub use compute::{Compute, ComputePass, GpuBuffer};
pub use effect::{Effect, ScreenEffect};
pub use mesh::{MeshData, MorphFrame, MorphTarget};
pub use post::{AmbientOcclusion, AntiAliasing, MAX_RENDER_SCALE, MIN_RENDER_SCALE, Post, Tonemap};
#[cfg(feature = "window")]
pub(crate) use post::{PostUniform, scaled_size};
pub use quality::{GraphicsQuality, QualityLevel};
#[cfg(feature = "window")]
pub(crate) use skin::blend as blend_skin;
pub use skin::{AttachToJoint, Joint, MorphWeights, Skeleton, SkinOf};
pub(crate) use skin::{MeshMorph, MeshSkin, SkinFrame};
#[cfg(all(test, feature = "window"))]
pub(crate) use skin::{SkinDraw, normalized as normalized_weights};
pub use sky::Sky;
pub(crate) use sky::SkyLight;
pub use target::TargetCamera;
pub use terrain::{Heightfield, TERRAIN_LAYERS, TERRAIN_LODS, TerrainMaterialDesc, TerrainTile, TerrainTileData};
pub use texture_array::{TextureArray, TextureKind};
use glam::{Mat3, Mat4, Quat, Vec2, Vec3, Vec4};
use crate::{Color, Texture};
#[derive(Debug, Clone, Copy, PartialEq, serde::Serialize, serde::Deserialize)]
#[serde(default)]
#[repr(C, align(16))] pub struct Transform3D {
pub rotation: Quat,
pub position: Vec3,
pub scale: Vec3,
}
impl Default for Transform3D {
fn default() -> Self {
Self {
position: Vec3::ZERO,
rotation: Quat::IDENTITY,
scale: Vec3::ONE,
}
}
}
impl Transform3D {
#[must_use]
pub fn at(position: Vec3) -> Self {
Self {
position,
..Self::default()
}
}
#[must_use]
pub fn rotated(self, rotation: Quat) -> Self {
Self { rotation, ..self }
}
#[must_use]
pub fn scaled(self, scale: f32) -> Self {
Self {
scale: Vec3::splat(scale),
..self
}
}
#[must_use]
pub fn looking_at(self, target: Vec3) -> Self {
Self {
rotation: look_rotation(target - self.position),
..self
}
}
#[must_use]
pub fn forward(&self) -> Vec3 {
self.rotation * Vec3::NEG_Z
}
#[must_use]
pub fn matrix(&self) -> Mat4 {
Mat4::from_scale_rotation_translation(self.scale, self.rotation, self.position)
}
}
fn look_rotation(direction: Vec3) -> Quat {
let forward = direction.try_normalize().unwrap_or(Vec3::NEG_Z);
let up = if forward.y.abs() > 0.999 { Vec3::Z } else { Vec3::Y };
let right = forward.cross(up).normalize();
let up = right.cross(forward);
Quat::from_mat3(&Mat3::from_cols(right, up, -forward))
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[repr(C)] pub struct Mesh {
pub(crate) id: u32,
}
impl Mesh {
pub const NONE: Mesh = Mesh { id: u32::MAX };
}
#[derive(Debug, Clone, PartialEq)]
pub struct MeshCost {
pub mesh: Mesh,
pub name: Option<String>,
pub models: usize,
pub triangles: usize,
pub shadow_models: usize,
pub shadow_triangles: usize,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[repr(C)] pub struct Material {
pub(crate) id: u32,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[repr(C)] pub struct Shader {
pub(crate) id: u32,
}
impl Shader {
#[doc(hidden)]
#[must_use]
pub fn id(self) -> u32 {
self.id
}
#[doc(hidden)]
#[must_use]
pub fn from_id(id: u32) -> Self {
Self { id }
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
#[repr(u32)]
pub enum Blend {
#[default]
Opaque = 0,
Transparent = 1,
Cutout = 2,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct MaterialDesc {
pub color: Color,
pub texture: Option<Texture>,
pub roughness: f32,
pub metallic: f32,
pub emissive: Color,
pub normal_map: Option<Texture>,
pub normal_strength: f32,
pub orm_map: Option<Texture>,
pub emissive_map: Option<Texture>,
pub blend: Blend,
pub double_sided: bool,
pub shader: Option<Shader>,
pub custom: [Vec4; 4],
pub custom_textures: [Option<Texture>; 2],
pub custom_arrays: [Option<TextureArray>; 3],
}
impl Default for MaterialDesc {
fn default() -> Self {
Self {
color: Color::WHITE,
texture: None,
roughness: 0.6,
metallic: 0.0,
emissive: Color::rgb(0.0, 0.0, 0.0),
normal_map: None,
normal_strength: 1.0,
orm_map: None,
emissive_map: None,
blend: Blend::Opaque,
double_sided: false,
shader: None,
custom: [Vec4::ZERO; 4],
custom_textures: [None; 2],
custom_arrays: [None; 3],
}
}
}
impl MaterialDesc {
#[must_use]
pub fn color(color: Color) -> Self {
Self {
color,
..Self::default()
}
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
#[repr(C)] pub struct Model {
pub mesh: Mesh,
pub color: Color,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ShadowMesh(pub Option<Mesh>);
impl Model {
#[must_use]
pub fn new(mesh: Mesh) -> Self {
Self {
mesh,
color: Color::WHITE,
}
}
#[must_use]
pub fn with_color(self, color: Color) -> Self {
Self { color, ..self }
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Camera3D {
pub position: Vec3,
pub rotation: Quat,
pub fov_y: f32,
pub near: f32,
pub far: f32,
pub occlusion_culling: bool,
viewport: Vec2,
}
impl Camera3D {
pub(crate) fn new(viewport: Vec2) -> Self {
Self {
position: Vec3::new(0.0, 2.0, 8.0),
rotation: look_rotation(Vec3::new(0.0, -2.0, -8.0)),
fov_y: 60f32.to_radians(),
near: 0.1,
far: 1000.0,
occlusion_culling: false,
viewport,
}
}
pub(crate) fn set_viewport(&mut self, viewport: Vec2) {
self.viewport = viewport;
}
pub fn look_at(&mut self, target: Vec3) {
self.rotation = look_rotation(target - self.position);
}
#[must_use]
pub fn forward(&self) -> Vec3 {
self.rotation * Vec3::NEG_Z
}
#[must_use]
pub(crate) fn view_proj(&self) -> Mat4 {
let aspect = (self.viewport.x / self.viewport.y.max(1.0)).max(1e-3);
let projection = glam::camera::rh::proj::directx::perspective(self.fov_y, aspect, self.near, self.far);
projection * self.view()
}
pub(crate) fn view(&self) -> Mat4 {
Mat4::from_rotation_translation(self.rotation, self.position).inverse()
}
#[must_use]
pub fn screen_ray(&self, screen: Vec2) -> (Vec3, Vec3) {
let ndc = Vec2::new(
screen.x / self.viewport.x.max(1.0) * 2.0 - 1.0,
1.0 - screen.y / self.viewport.y.max(1.0) * 2.0,
);
let inverse = self.view_proj().inverse();
let far = inverse * Vec4::new(ndc.x, ndc.y, 1.0, 1.0);
let far = far.truncate() / far.w;
(self.position, (far - self.position).normalize_or(self.forward()))
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Lighting {
pub sun_direction: Vec3,
pub sun_color: Color,
pub sun_intensity: f32,
pub sky_color: Color,
pub ground_color: Color,
pub equator_color: Option<Color>,
pub ambient_intensity: f32,
pub shadows: bool,
pub shadow_distance: f32,
pub shadow_cascades: u32,
pub shadow_resolution: u32,
pub shadow_refresh: u32,
pub shadow_filter: u32,
pub light_shadow_views: u32,
pub light_shadow_resolution: u32,
pub light_shadow_atlas: u32,
pub gi: GlobalIllumination,
}
impl Default for Lighting {
fn default() -> Self {
Self {
sun_direction: Vec3::new(-0.4, -1.0, -0.3),
sun_color: Color::hex(0xFFF4E0),
sun_intensity: 3.0,
sky_color: Color::hex(0x869BB0),
ground_color: Color::hex(0x4D4135),
equator_color: None,
ambient_intensity: 1.0,
shadows: true,
shadow_distance: 60.0,
shadow_cascades: 4,
shadow_resolution: SHADOW_MAP_SIZE,
shadow_refresh: 2,
shadow_filter: 3,
light_shadow_views: 12,
light_shadow_resolution: LIGHT_SHADOW_SIZE,
light_shadow_atlas: LIGHT_SHADOW_ATLAS_SIZE,
gi: GlobalIllumination::default(),
}
}
}
impl Lighting {
pub(crate) fn shadow_size(&self, limit: u32) -> u32 {
let limit = if limit == 0 { u32::MAX } else { limit };
self.shadow_resolution
.clamp(MIN_SHADOW_RESOLUTION, MAX_SHADOW_RESOLUTION)
.min(limit)
}
pub(crate) fn light_shadow_size(&self, limit: u32) -> u32 {
let limit = if limit == 0 { u32::MAX } else { limit };
self.light_shadow_resolution
.clamp(MIN_LIGHT_SHADOW_RESOLUTION, MAX_LIGHT_SHADOW_RESOLUTION)
.min(limit)
}
pub(crate) fn light_shadow_atlas_size(&self, limit: u32) -> u32 {
let limit = if limit == 0 { u32::MAX } else { limit };
let size = self
.light_shadow_atlas
.clamp(MIN_LIGHT_SHADOW_ATLAS, MAX_LIGHT_SHADOW_ATLAS)
.min(limit);
shadow_atlas::floor_pow2(size)
}
pub(crate) fn light_shadow_budget(&self) -> usize {
(self.light_shadow_views as usize).min(MAX_LIGHT_SHADOW_VIEWS)
}
}
pub(crate) struct Scene3D<'a> {
pub frame: &'a Frame3D,
pub lighting: &'a Lighting,
pub sky: &'a Sky,
pub light: SkyLight,
pub camera: Vec3,
pub post: &'a Post,
pub time: (f32, f32),
pub occlusion: bool,
}
#[derive(Default)]
pub(crate) struct Assets3D {
pub morph_entities: Vec<crate::Entity>,
pub morph_bases: std::collections::HashMap<crate::Entity, (u32, MorphWeights, MorphWeights)>,
pub bounds: Vec<(Vec3, f32)>,
pub free_meshes: Vec<u32>,
pub probes: probe::ProbeAssets,
pub skins: Vec<Option<std::sync::Arc<MeshSkin>>>,
pub morphs: Vec<Option<std::sync::Arc<MeshMorph>>>,
pub loaded_clips: std::collections::HashMap<std::path::PathBuf, Vec<AnimationClip>>,
pub loaded_skeletons: std::collections::HashMap<std::path::PathBuf, Skeleton>,
pub shadowless: Vec<bool>,
pub materials: u32,
pub arrays: Vec<TextureArray>,
pub array_data: Vec<bool>,
pub decal_array: Option<TextureArray>,
pub decals_supported: bool,
pub gi_supported: bool,
pub render_targets: Vec<crate::Texture>,
pub effects: u32,
pub computes: u32,
pub gpu_buffers: Vec<u32>,
pub instance_batches: Vec<Option<instances::Batch>>,
pub free_gpu_buffers: Vec<u32>,
pub gpu_mesh_bounds: std::collections::HashMap<u32, (Vec3, f32)>,
pub loaded_computes: std::collections::HashMap<std::path::PathBuf, Compute>,
pub loaded_effects: std::collections::HashMap<std::path::PathBuf, Effect>,
pub terrain_shader: Option<Shader>,
pub shaders: u32,
pub loaded_shaders: std::collections::HashMap<std::path::PathBuf, Shader>,
pub default_material: Option<Material>,
pub loaded: std::collections::HashMap<std::path::PathBuf, Mesh>,
pub file_materials: std::collections::HashMap<std::path::PathBuf, Vec<Material>>,
pub shared_images: std::collections::HashMap<String, crate::Texture>,
pub loaded_materials: std::collections::HashMap<String, Vec<(Material, MaterialDesc)>>,
}
impl Assets3D {
pub(crate) fn live_bounds(&self, id: u32) -> Option<(Vec3, f32)> {
self.bounds.get(id as usize).copied().filter(|&(_, r)| r >= 0.0)
}
pub(crate) fn skin(&self, id: u32) -> Option<&MeshSkin> {
self.skins.get(id as usize)?.as_deref()
}
pub(crate) fn morph(&self, id: u32) -> Option<&MeshMorph> {
self.morphs.get(id as usize)?.as_deref()
}
pub(crate) fn casts_shadows(&self, id: u32) -> bool {
!self.shadowless.get(id as usize).copied().unwrap_or(false)
}
}
pub(crate) fn linear(c: Color) -> [f32; 4] {
if c.r == 1.0 && c.g == 1.0 && c.b == 1.0 {
return [1.0, 1.0, 1.0, c.a];
}
let lin = |v: f32| {
if v <= 0.04045 {
v / 12.92
} else {
((v + 0.055) / 1.055).powf(2.4)
}
};
[lin(c.r), lin(c.g), lin(c.b), c.a]
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn looking_at_points_minus_z_at_the_target() {
for target in [Vec3::X, Vec3::NEG_X, Vec3::new(1.0, 2.0, -3.0), Vec3::Y, Vec3::NEG_Y] {
let t = Transform3D::default().looking_at(target);
assert!((t.forward() - target.normalize()).length() < 1e-5, "{target}");
}
}
#[test]
fn the_screen_center_ray_goes_where_the_camera_looks() {
let mut camera = Camera3D::new(Vec2::new(800.0, 600.0));
camera.position = Vec3::new(3.0, 4.0, 5.0);
camera.look_at(Vec3::ZERO);
let (from, dir) = camera.screen_ray(Vec2::new(400.0, 300.0));
assert_eq!(from, camera.position);
assert!((dir - camera.forward()).length() < 1e-4);
let (_, left) = camera.screen_ray(Vec2::new(0.0, 300.0));
let right_axis = camera.rotation * Vec3::X;
assert!(left.dot(right_axis) < 0.0);
}
#[test]
fn the_view_projection_puts_the_target_in_the_middle() {
let mut camera = Camera3D::new(Vec2::new(800.0, 600.0));
camera.position = Vec3::new(0.0, 10.0, 10.0);
camera.look_at(Vec3::new(1.0, 0.0, 0.0));
let clip = camera.view_proj() * Vec4::new(1.0, 0.0, 0.0, 1.0);
let ndc = clip.truncate() / clip.w;
assert!(
ndc.x.abs() < 1e-4 && ndc.y.abs() < 1e-4 && (0.0..1.0).contains(&ndc.z),
"{ndc}"
);
}
}