use fyrox_core::some_or_continue;
use fyrox_core::{
io::FileError, reflect::prelude::*, sparse::AtomicIndex, uuid::Uuid, visitor::prelude::*,
TypeUuidProvider,
};
pub use fyrox_graphics::gpu_program::{
SamplerFallback, ShaderResourceDefinition, ShaderResourceKind,
};
use fyrox_graphics::{gpu_program::ShaderProperty, DrawParameters};
use fyrox_resource::{
embedded_data_source, io::ResourceIo, manager::BuiltInResource, untyped::ResourceKind,
Resource, ResourceData,
};
use ron::ser::PrettyConfig;
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use std::ops::{Deref, DerefMut};
use std::sync::LazyLock;
use std::{
error::Error,
fmt::{Display, Formatter},
fs::File,
io::Write,
path::Path,
sync::Arc,
};
use uuid::uuid;
pub mod loader;
pub const STANDARD_SHADER_NAME: &str = "Default Shader";
pub const STANDARD_2D_SHADER_NAME: &str = "2D Shader";
pub const STANDARD_PARTICLE_SYSTEM_SHADER_NAME: &str = "Particle System Shader";
pub const STANDARD_TWOSIDES_SHADER_NAME: &str = "Two Sides Shader";
pub const STANDARD_TERRAIN_SHADER_NAME: &str = "Terrain Shader";
pub const STANDARD_TILE_SHADER_NAME: &str = "Tile Shader";
pub const STANDARD_SPRITE_SHADER_NAME: &str = "Sprite Shader";
pub const STANDARD_WIDGET_SHADER_NAME: &str = "Widget Shader";
#[derive(Default, Debug, Clone, Reflect, Visit)]
pub struct Shader {
#[visit(optional)]
pub definition: ShaderDefinition,
#[reflect(hidden)]
#[visit(skip)]
pub cache_index: Arc<AtomicIndex>,
}
impl TypeUuidProvider for Shader {
fn type_uuid() -> Uuid {
uuid!("f1346417-b726-492a-b80f-c02096c6c019")
}
}
#[derive(Default, Clone, Debug, PartialEq, Eq, Reflect, Visit, TypeUuidProvider)]
#[type_uuid(id = "d2aa5ba0-59e8-4f21-b7af-d6aab3a65379")]
pub struct ShaderSourceCode(pub String);
impl Serialize for ShaderSourceCode {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
self.0.serialize(serializer)
}
}
impl<'de> Deserialize<'de> for ShaderSourceCode {
fn deserialize<D>(deserializer: D) -> Result<ShaderSourceCode, D::Error>
where
D: Deserializer<'de>,
{
Ok(Self(String::deserialize(deserializer)?))
}
}
impl Deref for ShaderSourceCode {
type Target = String;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl DerefMut for ShaderSourceCode {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
#[derive(
Default, Clone, Serialize, Deserialize, Debug, PartialEq, Eq, Reflect, Visit, TypeUuidProvider,
)]
#[type_uuid(id = "450f2f3a-bdc8-4fd8-b62e-c4c924cd94ca")]
pub struct RenderPassDefinition {
pub name: String,
#[serde(default)]
pub draw_parameters: DrawParameters,
pub vertex_shader: ShaderSourceCode,
#[serde(default)]
#[reflect(hidden)]
pub vertex_shader_line: isize,
pub fragment_shader: ShaderSourceCode,
#[serde(default)]
#[reflect(hidden)]
pub fragment_shader_line: isize,
}
#[derive(Default, Clone, Serialize, Deserialize, Debug, PartialEq, Reflect, Visit)]
pub struct ShaderDefinition {
pub name: String,
pub passes: Vec<RenderPassDefinition>,
pub resources: Vec<ShaderResourceDefinition>,
#[serde(default)]
pub disabled_passes: Vec<String>,
}
impl ShaderDefinition {
pub const MAX_LIGHTS: usize = 16;
pub const MAX_BONE_MATRICES: usize = 255;
pub const MAX_BLEND_SHAPE_WEIGHT_GROUPS: usize = 32;
pub const MAX_GRADIENT_VALUE_COUNT: usize = 16;
fn find_shader_line_locations(&mut self, str: &str) {
let mut line_ends = Vec::new();
for (i, ch) in str.bytes().enumerate() {
if ch == b'\n' {
line_ends.push(i);
}
}
if str.bytes().last().is_some_and(|ch| ch != b'\n') {
line_ends.push(str.len());
}
fn find_line(line_ends: &[usize], byte_pos: usize) -> isize {
line_ends
.windows(2)
.enumerate()
.find_map(|(line_num, ends)| {
if (ends[0]..ends[1]).contains(&byte_pos) {
Some(line_num)
} else {
None
}
})
.unwrap_or(0) as isize
+ 1
}
let vertex_shader_regex = regex::Regex::new(r#"vertex_shader\s*:\s*r?#*""#).unwrap();
let fragment_shader_regex = regex::Regex::new(r#"fragment_shader\s*:\s*r?#*""#).unwrap();
let mut substr = str;
for pass in self.passes.iter_mut() {
let name_location = some_or_continue!(substr.find(&format!("\"{}\"", pass.name)));
let vertex_shader_location = some_or_continue!(vertex_shader_regex.find(substr));
let fragment_shader_location = some_or_continue!(fragment_shader_regex.find(substr));
let offset = str.len() - substr.len();
pass.vertex_shader_line = find_line(&line_ends, offset + vertex_shader_location.end());
pass.fragment_shader_line =
find_line(&line_ends, offset + fragment_shader_location.end());
let max = name_location
.max(vertex_shader_location.end())
.max(fragment_shader_location.end());
substr = &substr[(max + 1)..];
}
}
fn from_str(str: &str) -> Result<Self, ShaderError> {
let mut definition: ShaderDefinition = ron::de::from_str(str)?;
definition.generate_built_in_resources();
definition.find_shader_line_locations(str);
Ok(definition)
}
fn generate_built_in_resources(&mut self) {
for resource in self.resources.iter_mut() {
let ShaderResourceKind::PropertyGroup(ref mut properties) = resource.kind else {
continue;
};
match resource.name.as_str() {
"fyrox_widgetData" => {
properties.clear();
properties.extend([
ShaderProperty::new_matrix4("projectionMatrix"),
ShaderProperty::new_matrix3("worldMatrix"),
ShaderProperty::new_color("solidColor"),
ShaderProperty::new_vec4_f32_array(
"gradientColors",
Self::MAX_GRADIENT_VALUE_COUNT,
),
ShaderProperty::new_f32_array(
"gradientStops",
Self::MAX_GRADIENT_VALUE_COUNT,
),
ShaderProperty::new_vector2("gradientOrigin"),
ShaderProperty::new_vector2("gradientEnd"),
ShaderProperty::new_vector2("resolution"),
ShaderProperty::new_vector2("boundsMin"),
ShaderProperty::new_vector2("boundsMax"),
ShaderProperty::new_bool("isFont"),
ShaderProperty::new_float("opacity"),
ShaderProperty::new_int("brushType"),
ShaderProperty::new_int("gradientPointCount"),
]);
}
"fyrox_cameraData" => {
properties.clear();
properties.extend([
ShaderProperty::new_matrix4("viewProjectionMatrix"),
ShaderProperty::new_vector3("position"),
ShaderProperty::new_vector3("upVector"),
ShaderProperty::new_vector3("sideVector"),
ShaderProperty::new_float("zNear"),
ShaderProperty::new_float("zFar"),
ShaderProperty::new_float("zRange"),
]);
}
"fyrox_lightData" => {
properties.clear();
properties.extend([
ShaderProperty::new_vector3("lightPosition"),
ShaderProperty::new_vector4("ambientLightColor"),
]);
}
"fyrox_graphicsSettings" => {
properties.clear();
properties.extend([ShaderProperty::new_bool("usePOM")]);
}
"fyrox_lightsBlock" => {
properties.clear();
properties.extend([
ShaderProperty::new_int("lightCount"),
ShaderProperty::new_vec4_f32_array("lightsColorRadius", Self::MAX_LIGHTS),
ShaderProperty::new_vec2_f32_array("lightsParameters", Self::MAX_LIGHTS),
ShaderProperty::new_vec3_f32_array("lightsPosition", Self::MAX_LIGHTS),
ShaderProperty::new_vec3_f32_array("lightsDirection", Self::MAX_LIGHTS),
])
}
"fyrox_instanceData" => {
properties.clear();
properties.extend([
ShaderProperty::new_matrix4("worldMatrix"),
ShaderProperty::new_matrix4("worldViewProjection"),
ShaderProperty::new_int("blendShapesCount"),
ShaderProperty::new_bool("useSkeletalAnimation"),
ShaderProperty::new_vec4_f32_array(
"blendShapesWeights",
Self::MAX_BLEND_SHAPE_WEIGHT_GROUPS,
),
]);
}
"fyrox_boneMatrices" => {
properties.clear();
properties.extend([ShaderProperty::new_mat4_f32_array(
"matrices",
Self::MAX_BONE_MATRICES,
)])
}
_ => (),
}
}
}
}
impl Shader {
pub async fn from_file<P: AsRef<Path>>(
path: P,
io: &dyn ResourceIo,
) -> Result<Self, ShaderError> {
let bytes = io.load_file(path.as_ref()).await?;
let content = String::from_utf8_lossy(&bytes);
Ok(Self {
definition: ShaderDefinition::from_str(&content)?,
cache_index: Default::default(),
})
}
pub fn from_string(str: &str) -> Result<Self, ShaderError> {
Ok(Self {
definition: ShaderDefinition::from_str(str)?,
cache_index: Default::default(),
})
}
pub fn from_string_bytes(bytes: &[u8]) -> Result<Self, ShaderError> {
Ok(Self {
definition: ShaderDefinition::from_str(
std::str::from_utf8(bytes).map_err(|_| ShaderError::NotUtf8Source)?,
)?,
cache_index: Default::default(),
})
}
pub fn find_texture_resource(&self, name: &str) -> Option<&ShaderResourceDefinition> {
self.definition.resources.iter().find(|res| {
res.name.as_str() == name && matches!(res.kind, ShaderResourceKind::Texture { .. })
})
}
pub fn find_property_group_resource(&self, name: &str) -> Option<&ShaderResourceDefinition> {
self.definition.resources.iter().find(|res| {
res.name.as_str() == name && matches!(res.kind, ShaderResourceKind::PropertyGroup(_))
})
}
pub fn has_texture_resource(&self, name: &str) -> bool {
self.find_texture_resource(name).is_some()
}
pub fn has_property_group_resource(&self, name: &str) -> bool {
self.find_property_group_resource(name).is_some()
}
}
impl ResourceData for Shader {
fn type_uuid(&self) -> Uuid {
<Self as TypeUuidProvider>::type_uuid()
}
fn save(&mut self, path: &Path) -> Result<(), Box<dyn Error>> {
let mut file = File::create(path)?;
file.write_all(
ron::ser::to_string_pretty(&self.definition, PrettyConfig::default())?.as_bytes(),
)?;
Ok(())
}
fn can_be_saved(&self) -> bool {
true
}
fn try_clone_box(&self) -> Option<Box<dyn ResourceData>> {
Some(Box::new(self.clone()))
}
}
#[derive(Debug)]
pub enum ShaderError {
Io(FileError),
ParseError(ron::error::SpannedError),
NotUtf8Source,
}
impl Display for ShaderError {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
ShaderError::Io(v) => {
write!(f, "A file load error has occurred {v:?}")
}
ShaderError::ParseError(v) => {
write!(f, "A parsing error has occurred {v:?}")
}
ShaderError::NotUtf8Source => {
write!(f, "Bytes does not represent Utf8-encoded string.")
}
}
}
}
impl From<ron::error::SpannedError> for ShaderError {
fn from(e: ron::error::SpannedError) -> Self {
Self::ParseError(e)
}
}
impl From<FileError> for ShaderError {
fn from(e: FileError) -> Self {
Self::Io(e)
}
}
pub type ShaderResource = Resource<Shader>;
pub trait ShaderResourceExtension: Sized {
fn from_str(id: Uuid, str: &str, kind: ResourceKind) -> Result<Self, ShaderError>;
fn standard() -> Self;
fn standard_2d() -> Self;
fn standard_particle_system() -> Self;
fn standard_sprite() -> Self;
fn standard_terrain() -> Self;
fn standard_tile() -> Self;
fn standard_twosides() -> Self;
fn standard_widget() -> Self;
fn standard_shaders() -> [&'static BuiltInResource<Shader>; 8];
}
impl ShaderResourceExtension for ShaderResource {
fn from_str(id: Uuid, str: &str, kind: ResourceKind) -> Result<Self, ShaderError> {
Ok(Resource::new_ok(id, kind, Shader::from_string(str)?))
}
fn standard() -> Self {
STANDARD.resource()
}
fn standard_2d() -> Self {
STANDARD_2D.resource()
}
fn standard_particle_system() -> Self {
STANDARD_PARTICLE_SYSTEM.resource()
}
fn standard_sprite() -> Self {
STANDARD_SPRITE.resource()
}
fn standard_terrain() -> Self {
STANDARD_TERRAIN.resource()
}
fn standard_tile() -> Self {
STANDARD_TILE.resource()
}
fn standard_twosides() -> Self {
STANDARD_TWOSIDES.resource()
}
fn standard_widget() -> Self {
STANDARD_WIDGET.resource()
}
fn standard_shaders() -> [&'static BuiltInResource<Shader>; 8] {
[
&STANDARD,
&STANDARD_2D,
&STANDARD_PARTICLE_SYSTEM,
&STANDARD_SPRITE,
&STANDARD_TERRAIN,
&STANDARD_TWOSIDES,
&STANDARD_TILE,
&STANDARD_WIDGET,
]
}
}
pub static STANDARD: LazyLock<BuiltInResource<Shader>> = LazyLock::new(|| {
BuiltInResource::new(
STANDARD_SHADER_NAME,
embedded_data_source!("standard/standard.shader"),
|data| {
ShaderResource::new_ok(
uuid!("87195f6e-cba4-4c27-9f89-d0bf726db965"),
ResourceKind::External,
Shader::from_string_bytes(data).unwrap(),
)
},
)
});
pub static STANDARD_2D: LazyLock<BuiltInResource<Shader>> = LazyLock::new(|| {
BuiltInResource::new(
STANDARD_2D_SHADER_NAME,
embedded_data_source!("standard/standard2d.shader"),
|data| {
ShaderResource::new_ok(
uuid!("55fa05b0-3c25-4e46-bae7-65f093185b75"),
ResourceKind::External,
Shader::from_string_bytes(data).unwrap(),
)
},
)
});
pub static STANDARD_PARTICLE_SYSTEM: LazyLock<BuiltInResource<Shader>> = LazyLock::new(|| {
BuiltInResource::new(
STANDARD_PARTICLE_SYSTEM_SHADER_NAME,
embedded_data_source!("standard/standard_particle_system.shader"),
|data| {
ShaderResource::new_ok(
uuid!("eb474445-6a25-4481-bca9-f919699c300f"),
ResourceKind::External,
Shader::from_string_bytes(data).unwrap(),
)
},
)
});
pub static STANDARD_SPRITE: LazyLock<BuiltInResource<Shader>> = LazyLock::new(|| {
BuiltInResource::new(
STANDARD_SPRITE_SHADER_NAME,
embedded_data_source!("standard/standard_sprite.shader"),
|data| {
ShaderResource::new_ok(
uuid!("a135826a-4c1b-46d5-ba1f-0c9a226aa52c"),
ResourceKind::External,
Shader::from_string_bytes(data).unwrap(),
)
},
)
});
pub static STANDARD_TERRAIN: LazyLock<BuiltInResource<Shader>> = LazyLock::new(|| {
BuiltInResource::new(
STANDARD_TERRAIN_SHADER_NAME,
embedded_data_source!("standard/terrain.shader"),
|data| {
ShaderResource::new_ok(
uuid!("4911aafe-9bb1-4115-a958-25b57b87b51e"),
ResourceKind::External,
Shader::from_string_bytes(data).unwrap(),
)
},
)
});
pub static STANDARD_TILE: LazyLock<BuiltInResource<Shader>> = LazyLock::new(|| {
BuiltInResource::new(
STANDARD_TILE_SHADER_NAME,
embedded_data_source!("standard/tile.shader"),
|data| {
ShaderResource::new_ok(
uuid!("5f29dd3a-ea99-480c-bb02-d2c6420843b1"),
ResourceKind::External,
Shader::from_string_bytes(data).unwrap(),
)
},
)
});
pub static STANDARD_TWOSIDES: LazyLock<BuiltInResource<Shader>> = LazyLock::new(|| {
BuiltInResource::new(
STANDARD_TWOSIDES_SHADER_NAME,
embedded_data_source!("standard/standard-two-sides.shader"),
|data| {
ShaderResource::new_ok(
uuid!("f7979409-5185-4e1c-a644-d53cea64af8f"),
ResourceKind::External,
Shader::from_string_bytes(data).unwrap(),
)
},
)
});
pub static STANDARD_WIDGET: LazyLock<BuiltInResource<Shader>> = LazyLock::new(|| {
BuiltInResource::new(
STANDARD_WIDGET_SHADER_NAME,
embedded_data_source!("standard/widget.shader"),
|data| {
ShaderResource::new_ok(
uuid!("f5908aa4-e187-42a8-95d2-dc6577f6def4"),
ResourceKind::External,
Shader::from_string_bytes(data).unwrap(),
)
},
)
});
#[cfg(test)]
mod test {
use crate::shader::{
RenderPassDefinition, SamplerFallback, ShaderDefinition, ShaderResource,
ShaderResourceDefinition, ShaderResourceExtension, ShaderResourceKind, ShaderSourceCode,
};
use fyrox_graphics::gpu_program::SamplerKind;
use fyrox_resource::untyped::ResourceKind;
use uuid::Uuid;
#[test]
fn test_shader_load() {
let code = r#"
(
name: "TestShader",
resources: [
(
name: "diffuseTexture",
kind: Texture(kind: Sampler2D, fallback: White),
binding: 0
),
],
passes: [
(
name: "GBuffer",
draw_parameters: DrawParameters(
cull_face: Some(Back),
color_write: ColorMask(
red: true,
green: true,
blue: true,
alpha: true,
),
depth_write: true,
stencil_test: None,
depth_test: Some(Less),
blend: None,
stencil_op: StencilOp(
fail: Keep,
zfail: Keep,
zpass: Keep,
write_mask: 0xFFFF_FFFF,
),
scissor_box: None
),
vertex_shader: "<CODE>",
fragment_shader: "<CODE>",
),
],
)
"#;
let shader =
ShaderResource::from_str(Uuid::new_v4(), code, ResourceKind::External).unwrap();
let data = shader.data_ref();
let reference_definition = ShaderDefinition {
name: "TestShader".to_owned(),
resources: vec![ShaderResourceDefinition {
name: "diffuseTexture".into(),
kind: ShaderResourceKind::Texture {
kind: SamplerKind::Sampler2D,
fallback: SamplerFallback::White,
},
binding: 0,
}],
passes: vec![RenderPassDefinition {
name: "GBuffer".to_string(),
draw_parameters: Default::default(),
vertex_shader: ShaderSourceCode("<CODE>".to_string()),
vertex_shader_line: 35,
fragment_shader: ShaderSourceCode("<CODE>".to_string()),
fragment_shader_line: 36,
}],
disabled_passes: vec![],
};
assert_eq!(data.definition, reference_definition);
}
}