wde_renderer/assets/
shader.rs1use wde_logger::prelude::*;
2
3use bevy::{
4 asset::{AssetLoader, LoadContext, io::Reader},
5 prelude::*
6};
7use std::collections::HashSet;
8use std::io::{Error, ErrorKind};
9use thiserror::Error;
10
11#[derive(Asset, TypePath, Clone, Debug)]
15pub struct Shader {
16 pub content: String
18}
19
20#[derive(Debug, Error)]
21pub(crate) enum ShaderLoaderError {
22 #[error("Could not load shader: {0}")]
23 Io(#[from] std::io::Error),
24 #[error("Failed to resolve shader include: {0}")]
25 Include(String)
26}
27#[derive(Default, TypePath)]
28pub(crate) struct ShaderLoader;
29impl AssetLoader for ShaderLoader {
30 type Asset = Shader;
31 type Settings = ();
32 type Error = ShaderLoaderError;
33
34 async fn load(
35 &self,
36 reader: &mut dyn Reader,
37 _settings: &Self::Settings,
38 load_context: &mut LoadContext<'_>
39 ) -> Result<Self::Asset, Self::Error> {
40 debug!("Loading shader {}.", load_context.path());
41
42 let mut bytes = Vec::new();
44 reader.read_to_end(&mut bytes).await?;
45
46 let content = match String::from_utf8(bytes) {
48 Ok(content) => content,
49 Err(_) => {
50 return Err(ShaderLoaderError::Io(Error::new(
51 ErrorKind::InvalidData,
52 "Could not convert shader to string."
53 )));
54 }
55 };
56
57 let mut included = HashSet::new();
59 let content = resolve_includes(content, load_context, &mut included).await?;
60 Ok(Shader { content })
61 }
62
63 fn extensions(&self) -> &[&str] {
64 &["wgsl"]
65 }
66}
67
68async fn resolve_includes(
77 source: String,
78 load_context: &mut LoadContext<'_>,
79 included: &mut HashSet<String>
80) -> Result<String, ShaderLoaderError> {
81 let mut lines: Vec<String> = source.lines().map(str::to_owned).collect();
85 let mut i = 0;
86 while i < lines.len() {
87 let trimmed = lines[i].trim().to_owned();
88 if let Some(rest) = trimmed.strip_prefix("#include") {
89 let path_str = rest.trim();
90 if path_str.starts_with('"') && path_str.ends_with('"') && path_str.len() >= 2 {
91 let include_path = path_str[1..path_str.len() - 1].to_owned();
92 if !included.contains(&include_path) {
93 included.insert(include_path.clone());
94 let bytes = load_context
95 .read_asset_bytes(include_path.clone())
96 .await
97 .map_err(|e| {
98 ShaderLoaderError::Include(format!(
99 "Could not read '{}': {}",
100 include_path, e
101 ))
102 })?;
103 let include_source = String::from_utf8(bytes).map_err(|_| {
104 ShaderLoaderError::Include(format!("'{}' is not valid UTF-8", include_path))
105 })?;
106 let new_lines: Vec<String> =
107 include_source.lines().map(str::to_owned).collect();
108 lines.splice(i..=i, new_lines);
112 } else {
113 lines.remove(i);
115 }
116 continue;
117 } else {
118 return Err(ShaderLoaderError::Include(format!(
119 "Invalid #include syntax on line {i}: expected `#include \"path\"`"
120 )));
121 }
122 }
123 i += 1;
124 }
125 Ok(lines.join("\n"))
126}