use std::collections::BTreeMap;
use std::fmt;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use std::time::{Instant, SystemTime};
use bevy_ecs::prelude::Resource;
use serde::{Deserialize, Serialize};
use serde_json::Value;
#[derive(Clone, Copy, Debug, Default, PartialEq)]
pub struct ShaderWorld {
pub world_time: f32,
pub day_fraction: f32,
pub moon_phase: f32,
pub sun_direction: [f32; 3],
pub sun_lux: f32,
pub sun_color: [f32; 3],
pub moon_direction: [f32; 3],
pub moon_lux: f32,
pub rain: f32,
pub thunder: f32,
pub temperature: f32,
pub humidity: f32,
pub underwater: bool,
pub eye_sky_light: f32,
pub eye_block_light: f32,
pub sky_hidden: bool,
pub params: [[f32; 4]; 32],
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum ShaderPackSource {
Builtin,
Game,
User,
}
#[derive(
Clone, Copy, Debug, Default, PartialEq, Eq, Hash, Serialize, Deserialize,
)]
#[serde(rename_all = "lowercase")]
pub enum PackCost {
#[default]
Low,
Medium,
High,
}
#[derive(Clone, Debug, PartialEq)]
pub enum PackOptionKind {
Bool,
Enum(Vec<String>),
Slider {
min: f32,
max: f32,
step: f32,
},
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(untagged)]
pub enum PackOptionValue {
Bool(bool),
Enum(String),
Slider(f32),
}
#[derive(Clone, Debug, PartialEq)]
pub struct PackOption {
pub id: String,
pub label: String,
pub kind: PackOptionKind,
pub default: PackOptionValue,
pub cost: PackCost,
pub hint: String,
}
#[derive(Clone, Debug, PartialEq)]
pub struct ShaderPackInfo {
pub id: String,
pub name: String,
pub author: String,
pub version: String,
pub description: String,
pub source: ShaderPackSource,
pub path: PathBuf,
pub options: Vec<PackOption>,
pub presets: Vec<String>,
pub default_preset: String,
}
pub const PRESETS: [&str; 4] = ["Low", "Medium", "High", "Ultra"];
pub const SURFACE_PROGRAMS: [&str; 5] =
["terrain", "water", "translucent", "entities", "hand"];
pub const MAX_COMPOSITE_PASSES: usize = 8;
const SCREEN_PROGRAMS: [(&str, &str); 6] = [
("particles", "particle"),
("sky", "sky"),
("clouds", "clouds"),
("fog", "fog"),
("final", "post"),
("sky_lut", "sky_lut"),
];
const TOP_LEVEL_KEYS: [&str; 13] = [
"format",
"name",
"author",
"version",
"description",
"include",
"programs",
"composite",
"textures",
"options",
"presets",
"default_preset",
"values",
];
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ShaderPackError {
pub code: &'static str,
pub file: String,
pub line: usize,
pub message: String,
}
impl fmt::Display for ShaderPackError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}:{}: {}", self.file, self.line, self.message)
}
}
impl std::error::Error for ShaderPackError {}
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct PackFile {
pub name: String,
pub text: String,
}
#[derive(Clone, Debug, Default, PartialEq, Eq, Hash)]
pub struct PackProgram {
pub vertex: Option<PackFile>,
pub fragment: Option<PackFile>,
pub shadow: Option<PackFile>,
pub layer_divisor: u8,
}
#[derive(Clone, Debug, PartialEq)]
pub struct CompositePass {
pub name: String,
pub fragment: PackFile,
pub scale: f32,
pub when: Vec<String>,
}
#[derive(Clone, Debug, PartialEq)]
pub struct PackTexture {
pub name: String,
pub path: PathBuf,
pub size: [u32; 2],
}
const MAX_PACK_TEXTURES: usize = 8;
const MAX_ATLAS_SIDE: u32 = 8192;
const ATLAS_GAP: u32 = 16;
#[must_use]
pub fn texture_atlas(sizes: &[[u32; 2]]) -> ([u32; 2], Vec<[u32; 2]>) {
let mut corners = Vec::with_capacity(sizes.len());
let mut y = 0;
for (index, size) in sizes.iter().enumerate() {
if index > 0 {
y += ATLAS_GAP;
}
corners.push([0, y]);
y += size[1];
}
let width = sizes.iter().map(|size| size[0]).max().unwrap_or(1);
([width.max(1), y.max(1)], corners)
}
fn inside_pack(name: &str) -> bool {
let relative = Path::new(name);
!relative.as_os_str().is_empty()
&& relative
.components()
.all(|part| matches!(part, std::path::Component::Normal(_)))
}
#[derive(Clone, Debug, PartialEq)]
pub struct ShaderPack {
pub info: ShaderPackInfo,
pub include: Vec<PackFile>,
pub programs: BTreeMap<String, PackProgram>,
pub composite: Vec<CompositePass>,
pub textures: Vec<PackTexture>,
pub presets: BTreeMap<String, BTreeMap<String, PackOptionValue>>,
pub values: BTreeMap<String, Vec<f32>>,
pub files: Vec<PathBuf>,
}
fn invalid(
file: &str,
line: usize,
message: impl Into<String>,
) -> ShaderPackError {
ShaderPackError {
code: "SHADER_PACK_INVALID",
file: file.to_owned(),
line,
message: message.into(),
}
}
fn line_of(text: &str, key: &str, after: usize) -> usize {
let needle = format!("\"{key}\"");
let start = if after <= 1 {
0
} else {
text.match_indices('\n')
.nth(after - 2)
.map_or(text.len(), |(i, _)| i + 1)
};
text[start..].find(&needle).map_or(after.max(1), |index| {
text[..start + index].matches('\n').count() + 1
})
}
fn is_id(text: &str) -> bool {
!text.is_empty()
&& text
.chars()
.all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '_')
}
fn check_header(kind: &str, file: &PackFile) -> Result<(), ShaderPackError> {
let (index, header) = file
.text
.lines()
.enumerate()
.find(|(_, line)| !line.trim().is_empty())
.unwrap_or((0, ""));
let words: Vec<&str> = header.split_whitespace().collect();
if words != ["#rusting", kind, "1"] {
return Err(invalid(
&file.name,
index + 1,
format!(
"this file starts with `#rusting {kind} 1`, not {header:?}"
),
));
}
Ok(())
}
impl ShaderPack {
pub fn load(
folder: &Path,
id: &str,
source: ShaderPackSource,
) -> Result<Self, ShaderPackError> {
let manifest = folder.join("pack.json");
let text = std::fs::read_to_string(&manifest).map_err(|error| {
invalid("pack.json", 0, format!("cannot read pack.json: {error}"))
})?;
Self::parse(folder, id, source, &text)
}
pub fn parse(
folder: &Path,
id: &str,
source: ShaderPackSource,
text: &str,
) -> Result<Self, ShaderPackError> {
const PJ: &str = "pack.json";
let root: Value = serde_json::from_str(text)
.map_err(|error| invalid(PJ, error.line(), error.to_string()))?;
let Some(root) = root.as_object() else {
return Err(invalid(PJ, 1, "pack.json is a JSON object"));
};
for key in root.keys() {
if !TOP_LEVEL_KEYS.contains(&key.as_str()) {
return Err(invalid(
PJ,
line_of(text, key, 1),
format!(
"unknown key `{key}`; pack.json has {}",
TOP_LEVEL_KEYS.join(", ")
),
));
}
}
let at = |key: &str| line_of(text, key, 1);
match root.get("format").and_then(Value::as_u64) {
Some(1) => {}
_ => {
return Err(invalid(
PJ,
at("format"),
"`format` is 1 for this engine",
))
}
}
let string = |key: &str| -> Result<String, ShaderPackError> {
match root.get(key) {
None => Ok(String::new()),
Some(Value::String(value)) => Ok(value.clone()),
Some(_) => {
Err(invalid(PJ, at(key), format!("`{key}` is text")))
}
}
};
let mut files = vec![folder.join(PJ)];
let mut read = |name: &str,
line: usize|
-> Result<PackFile, ShaderPackError> {
let relative = Path::new(name);
if !inside_pack(name) {
return Err(invalid(
PJ,
line,
format!("`{name}` must be a path inside the pack folder"),
));
}
let path = folder.join(relative);
let text = std::fs::read_to_string(&path).map_err(|error| {
invalid(PJ, line, format!("cannot read `{name}`: {error}"))
})?;
files.push(path);
Ok(PackFile {
name: name.to_owned(),
text,
})
};
let file_name = |value: &Value, what: &str, line: usize| {
value.as_str().map(str::to_owned).ok_or_else(|| {
invalid(PJ, line, format!("{what} is a file name"))
})
};
let mut include = Vec::new();
if let Some(list) = root.get("include") {
let Some(list) = list.as_array() else {
return Err(invalid(
PJ,
at("include"),
"`include` is a list of file names",
));
};
for item in list {
let line = at("include");
let name = file_name(item, "each `include` entry", line)?;
include.push(read(&name, line_of(text, &name, line))?);
}
}
let mut programs = BTreeMap::new();
if let Some(table) = root.get("programs") {
let Some(table) = table.as_object() else {
return Err(invalid(
PJ,
at("programs"),
"`programs` is an object",
));
};
let base = at("programs");
for (name, value) in table {
let line = line_of(text, name, base);
let slots: &[&str] = if SURFACE_PROGRAMS
.contains(&name.as_str())
{
&["vertex", "fragment", "shadow"]
} else if let Some(&(_, _)) =
SCREEN_PROGRAMS.iter().find(|(program, _)| program == name)
{
&["fragment"]
} else {
return Err(invalid(
PJ,
line,
format!(
"unknown program `{name}`; programs are {}, {}",
SURFACE_PROGRAMS.join(", "),
SCREEN_PROGRAMS.map(|(p, _)| p).join(", ")
),
));
};
let Some(value) = value.as_object() else {
return Err(invalid(
PJ,
line,
format!("program `{name}` is an object"),
));
};
let mut program = PackProgram::default();
for (slot, file) in value {
let slot_line = line_of(text, slot, line);
if name == "clouds" && slot == "scale" {
program.layer_divisor = match file.as_f64() {
Some(1.0) => 1,
Some(0.5) => 2,
Some(0.25) => 4,
_ => {
return Err(invalid(
PJ,
slot_line,
"the clouds program's `scale` is 1, 0.5 or 0.25",
))
}
};
continue;
}
if !slots.contains(&slot.as_str()) {
return Err(invalid(
PJ,
slot_line,
format!(
"unknown key `{slot}` in program `{name}`; it has {}",
slots.join(", ")
),
));
}
let file_ref = file_name(
file,
&format!("`{name}.{slot}`"),
slot_line,
)?;
let file = read(&file_ref, slot_line)?;
let kind = match slot.as_str() {
"fragment" => SCREEN_PROGRAMS
.iter()
.find(|(program, _)| program == name)
.map_or("surface", |(_, kind)| kind),
_ => "vertex",
};
check_header(kind, &file)?;
match slot.as_str() {
"vertex" => program.vertex = Some(file),
"shadow" => program.shadow = Some(file),
_ => program.fragment = Some(file),
}
}
programs.insert(name.clone(), program);
}
}
let mut composite = Vec::new();
let mut when_lines = Vec::new();
if let Some(list) = root.get("composite") {
let Some(list) = list.as_array() else {
return Err(invalid(
PJ,
at("composite"),
"`composite` is a list of passes",
));
};
let mut line = at("composite");
for pass in list {
let Some(pass) = pass.as_object() else {
return Err(invalid(
PJ,
line,
"each composite pass is an object",
));
};
for key in pass.keys() {
if !["name", "fragment", "scale", "when"]
.contains(&key.as_str())
{
return Err(invalid(
PJ,
line_of(text, key, line),
format!("unknown key `{key}` in a composite pass; it has name, fragment, scale, when"),
));
}
}
let name = pass
.get("name")
.and_then(Value::as_str)
.unwrap_or_default()
.to_owned();
line = line_of(text, "fragment", line);
let file_ref = file_name(
pass.get("fragment").unwrap_or(&Value::Null),
"a composite pass's `fragment`",
line,
)?;
if composite.len() == MAX_COMPOSITE_PASSES {
return Err(invalid(
PJ,
line,
format!("a pack has at most {MAX_COMPOSITE_PASSES} composite passes"),
));
}
let fragment = read(&file_ref, line)?;
check_header("composite", &fragment)?;
let scale = match pass.get("scale") {
None => 1.0,
Some(value) => match value.as_f64() {
Some(s) if [1.0, 0.5, 0.25].contains(&s) => s as f32,
_ => {
return Err(invalid(
PJ,
line_of(text, "scale", line),
"a composite pass's `scale` is 1, 0.5 or 0.25",
))
}
},
};
let when_line = line_of(text, "when", line);
let when = match pass.get("when") {
None => Vec::new(),
Some(value) => value
.as_array()
.and_then(|list| {
list.iter().map(|id| id.as_str().map(str::to_owned)).collect()
})
.ok_or_else(|| {
invalid(PJ, when_line, "a composite pass's `when` is a list of option ids")
})?,
};
when_lines.push(when_line);
composite.push(CompositePass {
name,
fragment,
scale,
when,
});
line += 1;
}
}
let options = parse_options(root.get("options"), text)?;
for (pass, line) in composite.iter().zip(when_lines) {
if let Some(id) = pass
.when
.iter()
.find(|id| !options.iter().any(|o| &o.id == *id))
{
return Err(invalid(
PJ,
line,
format!("composite pass `{}` names unknown option `{id}` in `when`", pass.name),
));
}
}
let presets = parse_presets(root.get("presets"), text, &options)?;
let default_preset = match root.get("default_preset") {
None => "High".to_owned(),
Some(Value::String(name)) if PRESETS.contains(&name.as_str()) => {
name.clone()
}
Some(_) => {
return Err(invalid(
PJ,
at("default_preset"),
"`default_preset` is Low, Medium, High or Ultra",
))
}
};
let mut values = BTreeMap::new();
if let Some(table) = root.get("values") {
let Some(table) = table.as_object() else {
return Err(invalid(
PJ,
at("values"),
"`values` is an object of number lists",
));
};
let base = at("values");
for (name, value) in table {
let line = line_of(text, name, base);
if name.is_empty()
|| !name
.chars()
.all(|c| c.is_ascii_alphanumeric() || c == '_')
{
return Err(invalid(PJ, line, format!("value name `{name}` uses only letters, digits and _")));
}
let numbers: Option<Vec<f32>> =
value.as_array().and_then(|list| {
list.iter()
.map(|n| {
n.as_f64()
.map(|n| n as f32)
.filter(|n| n.is_finite())
})
.collect()
});
match numbers {
Some(numbers) if !numbers.is_empty() => {
values.insert(name.clone(), numbers);
}
_ => {
return Err(invalid(
PJ,
line,
format!(
"value `{name}` is a list of one or more numbers"
),
))
}
}
}
}
let mut textures = Vec::new();
if let Some(table) = root.get("textures") {
let Some(table) = table.as_object() else {
return Err(invalid(
PJ,
at("textures"),
"`textures` is an object of image file names",
));
};
let base = at("textures");
if table.len() > MAX_PACK_TEXTURES {
return Err(invalid(
PJ,
base,
format!(
"`textures` holds at most {MAX_PACK_TEXTURES} images"
),
));
}
for (name, value) in table {
let line = line_of(text, name, base);
if !is_id(name) {
return Err(invalid(
PJ,
line,
format!("texture name `{name}` is lower_snake_case"),
));
}
let file =
file_name(value, &format!("texture `{name}`"), line)?;
if !inside_pack(&file) {
return Err(invalid(
PJ,
line,
format!(
"`{file}` must be a path inside the pack folder"
),
));
}
let path = folder.join(&file);
let (width, height) =
image::image_dimensions(&path).map_err(|error| {
invalid(
PJ,
line,
format!("cannot read image `{file}`: {error}"),
)
})?;
files.push(path.clone());
textures.push(PackTexture {
name: name.clone(),
path,
size: [width, height],
});
}
let sizes: Vec<_> =
textures.iter().map(|texture| texture.size).collect();
let ([width, height], _) = texture_atlas(&sizes);
if width > MAX_ATLAS_SIDE || height > MAX_ATLAS_SIDE {
return Err(invalid(
PJ,
base,
format!("the `textures` images stacked make a {width}x{height} atlas; keep it within {MAX_ATLAS_SIDE}x{MAX_ATLAS_SIDE}"),
));
}
}
let info = ShaderPackInfo {
id: id.to_owned(),
name: match string("name")? {
name if name.is_empty() => id.to_owned(),
name => name,
},
author: string("author")?,
version: string("version")?,
description: string("description")?,
source,
path: folder.to_owned(),
options,
presets: PRESETS.map(str::to_owned).to_vec(),
default_preset,
};
Ok(Self {
info,
include,
programs,
composite,
textures,
presets,
values,
files,
})
}
#[must_use]
pub fn preset_values(
&self,
preset: &str,
) -> BTreeMap<String, PackOptionValue> {
self.presets
.get(preset)
.cloned()
.unwrap_or_else(|| self.default_values())
}
#[must_use]
pub fn composite_on(
&self,
pass: &CompositePass,
values: &BTreeMap<String, PackOptionValue>,
) -> bool {
pass.when.is_empty()
|| pass.when.iter().any(|id| {
let Some(option) =
self.info.options.iter().find(|o| &o.id == id)
else {
return false;
};
match (values.get(id).unwrap_or(&option.default), &option.kind)
{
(PackOptionValue::Bool(on), _) => *on,
(
PackOptionValue::Enum(name),
PackOptionKind::Enum(names),
) => names.first() != Some(name),
(PackOptionValue::Slider(value), _) => *value > 0.0,
_ => false,
}
})
}
#[must_use]
pub fn default_values(&self) -> BTreeMap<String, PackOptionValue> {
self.info
.options
.iter()
.map(|option| (option.id.clone(), option.default.clone()))
.collect()
}
#[must_use]
pub fn preset_named_by(
&self,
values: &BTreeMap<String, PackOptionValue>,
) -> String {
PRESETS
.iter()
.find(|preset| self.presets.get(**preset) == Some(values))
.map_or_else(|| "Custom".to_owned(), |preset| (*preset).to_owned())
}
#[must_use]
pub fn prelude(
&self,
values: &BTreeMap<String, PackOptionValue>,
) -> String {
use std::fmt::Write;
let mut glsl = String::from(
"#extension GL_GOOGLE_cpp_style_line_directive : require\n\
#ifndef RUSTING_PACK_PRELUDE\n#define RUSTING_PACK_PRELUDE\n",
);
glsl.push_str(include_str!("../shaders/frame.glsl"));
glsl.push_str(&vertex_flag_glsl());
for option in &self.info.options {
let id = option.id.to_uppercase();
let value = values.get(&option.id).unwrap_or(&option.default);
match (&option.kind, value) {
(PackOptionKind::Bool, PackOptionValue::Bool(on)) => {
let _ =
writeln!(glsl, "#define OPT_{id} {}", u8::from(*on));
}
(
PackOptionKind::Enum(names),
PackOptionValue::Enum(current),
) => {
for (index, name) in names.iter().enumerate() {
let _ = writeln!(
glsl,
"#define OPT_{id}_{} {index}",
name.to_uppercase()
);
}
let index = names
.iter()
.position(|name| name == current)
.unwrap_or(0);
let _ = writeln!(glsl, "#define OPT_{id} {index}");
}
(
PackOptionKind::Slider { .. },
PackOptionValue::Slider(number),
) => {
let _ = writeln!(
glsl,
"#define OPT_{id} {}",
glsl_float(*number)
);
}
_ => {}
}
}
for (name, numbers) in &self.values {
let name = name.to_uppercase();
let list = numbers
.iter()
.map(|n| glsl_float(*n))
.collect::<Vec<_>>()
.join(", ");
let _ = match numbers.len() {
1 => writeln!(glsl, "const float PACK_{name} = {list};"),
n @ 2..=4 => {
writeln!(glsl, "const vec{n} PACK_{name} = vec{n}({list});")
}
n => writeln!(
glsl,
"const float PACK_{name}[{n}] = float[]({list});"
),
};
}
let sizes: Vec<_> =
self.textures.iter().map(|texture| texture.size).collect();
let ([width, height], corners) = texture_atlas(&sizes);
for (texture, [x, y]) in self.textures.iter().zip(corners) {
let [w, h] = texture.size;
let rect = [
x as f32 / width as f32,
y as f32 / height as f32,
(x + w) as f32 / width as f32,
(y + h) as f32 / height as f32,
];
let _ = writeln!(
glsl,
"const vec4 PACK_TEXTURE_{} = vec4({});",
texture.name.to_uppercase(),
rect.map(glsl_float).join(", ")
);
}
for file in &self.include {
let _ = writeln!(glsl, "#line 1 \"{}\"\n{}", file.name, file.text);
}
glsl.push_str("#endif\n");
glsl
}
#[must_use]
pub fn hook_source(file: &PackFile, prelude: &str) -> String {
let mut lines = file.text.lines().enumerate();
let header = lines
.by_ref()
.find(|(_, line)| !line.trim().is_empty())
.map_or((0, ""), |(index, line)| (index, line));
let body: Vec<&str> = lines.map(|(_, line)| line).collect();
format!(
"{}\n{prelude}\n#line {} \"{}\"\n{}\n",
header.1.trim(),
header.0 + 2,
file.name,
body.join("\n")
)
}
}
fn glsl_float(number: f32) -> String {
let text = format!("{number:?}");
if text.contains(['.', 'e', 'E']) {
text
} else {
format!("{text}.0")
}
}
#[must_use]
pub fn vertex_flag_glsl() -> String {
use crate::assets::vertex_data as v;
let mut glsl = format!("const uint RUSTING_ID_MASK = {}u;\n", v::ID_MASK);
for (name, flag) in [
("FOLIAGE", v::FOLIAGE),
("LEAVES", v::LEAVES),
("WATER", v::WATER),
("LAVA", v::LAVA),
("EMISSIVE", v::EMISSIVE),
("GLASS", v::GLASS),
("METAL", v::METAL),
] {
glsl.push_str(&format!(
"const uint RUSTING_FLAG_{name} = 1u << {};\n",
flag.trailing_zeros()
));
}
glsl
}
fn parse_options(
value: Option<&Value>,
text: &str,
) -> Result<Vec<PackOption>, ShaderPackError> {
const PJ: &str = "pack.json";
let Some(value) = value else {
return Ok(Vec::new());
};
let base = line_of(text, "options", 1);
let Some(list) = value.as_array() else {
return Err(invalid(PJ, base, "`options` is a list"));
};
let mut options: Vec<PackOption> = Vec::new();
let mut line = base;
for item in list {
let Some(item) = item.as_object() else {
return Err(invalid(PJ, line, "each option is an object"));
};
let id = item.get("id").and_then(Value::as_str).unwrap_or_default();
line = line_of(text, id, line);
if !is_id(id) {
return Err(invalid(
PJ,
line,
format!("option id {id:?} uses only a-z, 0-9 and _"),
));
}
if options.iter().any(|option| option.id == id) {
return Err(invalid(
PJ,
line,
format!("option `{id}` is declared twice"),
));
}
let kind_name =
item.get("type").and_then(Value::as_str).unwrap_or_default();
let allowed: &[&str] = match kind_name {
"bool" => &["id", "type", "label", "default", "cost", "hint"],
"enum" => &["id", "type", "label", "default", "cost", "hint", "values"],
"slider" => &["id", "type", "label", "default", "cost", "hint", "min", "max", "step"],
other => {
return Err(invalid(
PJ,
line_of(text, "type", line),
format!("option `{id}` has type {other:?}; types are bool, enum, slider"),
))
}
};
for key in item.keys() {
if !allowed.contains(&key.as_str()) {
return Err(invalid(
PJ,
line_of(text, key, line),
format!("unknown key `{key}` in {kind_name} option `{id}`; it has {}", allowed.join(", ")),
));
}
}
let number = |key: &str| -> Result<f32, ShaderPackError> {
item.get(key)
.and_then(Value::as_f64)
.map(|n| n as f32)
.filter(|n| n.is_finite())
.ok_or_else(|| {
invalid(
PJ,
line_of(text, key, line),
format!("slider `{id}` needs a number `{key}`"),
)
})
};
let kind = match kind_name {
"bool" => PackOptionKind::Bool,
"enum" => {
let names: Option<Vec<String>> = item
.get("values")
.and_then(Value::as_array)
.and_then(|list| {
list.iter()
.map(|v| v.as_str().map(str::to_owned))
.collect()
});
match names {
Some(names) if !names.is_empty() && names.iter().all(|n| is_id(n)) => {
PackOptionKind::Enum(names)
}
_ => {
return Err(invalid(
PJ,
line_of(text, "values", line),
format!("enum `{id}` needs `values`, a list of names using only a-z, 0-9 and _"),
))
}
}
}
_ => PackOptionKind::Slider {
min: number("min")?,
max: number("max")?,
step: number("step")?,
},
};
let default_line = line_of(text, "default", line);
let default = match item.get("default") {
Some(value) => option_value(&kind, value).ok_or_else(|| {
invalid(
PJ,
default_line,
format!(
"default of `{id}` does not match its type {kind_name}"
),
)
})?,
None => match &kind {
PackOptionKind::Bool => PackOptionValue::Bool(false),
PackOptionKind::Enum(names) => {
PackOptionValue::Enum(names[0].clone())
}
PackOptionKind::Slider { min, .. } => {
PackOptionValue::Slider(*min)
}
},
};
let cost = match item.get("cost") {
None => PackCost::Low,
Some(value) => {
serde_json::from_value(value.clone()).map_err(|_| {
invalid(
PJ,
line_of(text, "cost", line),
format!("cost of `{id}` is low, medium or high"),
)
})?
}
};
let hint = match item.get("hint") {
None => String::new(),
Some(Value::String(hint)) if hint.chars().count() <= 120 => {
hint.clone()
}
Some(_) => {
return Err(invalid(
PJ,
line_of(text, "hint", line),
format!(
"hint of `{id}` is a text of at most 120 characters"
),
))
}
};
options.push(PackOption {
id: id.to_owned(),
label: item
.get("label")
.and_then(Value::as_str)
.unwrap_or(id)
.to_owned(),
kind,
default,
cost,
hint,
});
}
Ok(options)
}
#[must_use]
pub fn option_value(
kind: &PackOptionKind,
value: &Value,
) -> Option<PackOptionValue> {
match kind {
PackOptionKind::Bool => value.as_bool().map(PackOptionValue::Bool),
PackOptionKind::Enum(names) => value
.as_str()
.filter(|name| names.iter().any(|n| n == name))
.map(|name| PackOptionValue::Enum(name.to_owned())),
PackOptionKind::Slider { min, max, .. } => value
.as_f64()
.map(|n| n as f32)
.filter(|n| n.is_finite() && *n >= *min && *n <= *max)
.map(PackOptionValue::Slider),
}
}
#[must_use]
pub fn value_fits(kind: &PackOptionKind, value: &PackOptionValue) -> bool {
let json = serde_json::to_value(value).unwrap_or(Value::Null);
option_value(kind, &json).is_some()
}
fn parse_presets(
value: Option<&Value>,
text: &str,
options: &[PackOption],
) -> Result<BTreeMap<String, BTreeMap<String, PackOptionValue>>, ShaderPackError>
{
const PJ: &str = "pack.json";
let defaults: BTreeMap<String, PackOptionValue> = options
.iter()
.map(|option| (option.id.clone(), option.default.clone()))
.collect();
let base = line_of(text, "presets", 1);
let table = match value {
None => {
return Ok(PRESETS
.iter()
.map(|preset| ((*preset).to_owned(), defaults.clone()))
.collect())
}
Some(value) => value.as_object().ok_or_else(|| {
invalid(
PJ,
base,
"`presets` is an object with Low, Medium, High and Ultra",
)
})?,
};
for name in table.keys() {
if !PRESETS.contains(&name.as_str()) {
return Err(invalid(PJ, line_of(text, name, base), format!("unknown preset `{name}`; presets are Low, Medium, High, Ultra")));
}
}
let mut presets = BTreeMap::new();
for preset in PRESETS {
let Some(entries) = table.get(preset) else {
return Err(invalid(PJ, base, format!("`presets` is missing `{preset}`; use {{}} for all defaults")));
};
let line = line_of(text, preset, base);
let Some(entries) = entries.as_object() else {
return Err(invalid(
PJ,
line,
format!("preset `{preset}` is an object of option values"),
));
};
let mut values = defaults.clone();
for (id, value) in entries {
let entry_line = line_of(text, id, line);
let Some(option) = options.iter().find(|option| &option.id == id)
else {
return Err(invalid(
PJ,
entry_line,
format!("preset `{preset}` sets unknown option `{id}`"),
));
};
let Some(value) = option_value(&option.kind, value) else {
return Err(invalid(PJ, entry_line, format!("preset `{preset}` gives `{id}` a value that does not fit its type or range")));
};
values.insert(id.clone(), value);
}
presets.insert(preset.to_owned(), values);
}
Ok(presets)
}
#[must_use]
pub fn discover(
project: Option<&Path>,
user: Option<&Path>,
) -> Vec<ShaderPackInfo> {
let mut packs = vec![builtin_info()];
for (folder, source) in [
(
project.map(|p| p.join("assets").join("shaderpacks")),
ShaderPackSource::Game,
),
(user.map(|u| u.join("shaderpacks")), ShaderPackSource::User),
] {
let Some(folder) = folder else { continue };
let Ok(entries) = std::fs::read_dir(&folder) else {
continue;
};
let mut names: Vec<String> = entries
.filter_map(Result::ok)
.filter(|entry| entry.path().join("pack.json").is_file())
.filter_map(|entry| entry.file_name().into_string().ok())
.collect();
names.sort();
for name in names {
let id = match source {
ShaderPackSource::User => format!("user/{name}"),
_ => name.clone(),
};
let path = folder.join(&name);
packs.push(match ShaderPack::load(&path, &id, source) {
Ok(pack) => pack.info,
Err(error) => ShaderPackInfo {
id: id.clone(),
name: id,
author: String::new(),
version: String::new(),
description: error.to_string(),
source,
path,
options: Vec::new(),
presets: Vec::new(),
default_preset: String::new(),
},
});
}
}
packs
}
#[must_use]
pub fn builtin_info() -> ShaderPackInfo {
ShaderPackInfo {
id: String::new(),
name: "Built-in".to_owned(),
author: "Rusting".to_owned(),
version: env!("CARGO_PKG_VERSION").to_owned(),
description: "The engine's own shading.".to_owned(),
source: ShaderPackSource::Builtin,
path: PathBuf::new(),
options: Vec::new(),
presets: Vec::new(),
default_preset: String::new(),
}
}
#[must_use]
pub fn pack_folder(
id: &str,
project: Option<&Path>,
user: Option<&Path>,
) -> Option<(PathBuf, ShaderPackSource)> {
if let Some(name) = id.strip_prefix("user/") {
let user = user?;
is_folder_name(name).then(|| {
(user.join("shaderpacks").join(name), ShaderPackSource::User)
})
} else {
let project = project?;
is_folder_name(id).then(|| {
(
project.join("assets").join("shaderpacks").join(id),
ShaderPackSource::Game,
)
})
}
}
fn is_folder_name(name: &str) -> bool {
!name.is_empty()
&& !name.contains(['/', '\\'])
&& name != "."
&& name != ".."
}
#[derive(Clone, Debug)]
pub struct ActiveShaderPack {
pub pack: Arc<ShaderPack>,
pub values: BTreeMap<String, PackOptionValue>,
pub prelude: Arc<str>,
pub revision: u64,
}
impl ActiveShaderPack {
#[must_use]
pub fn hook(&self, program: &str, slot: &str) -> String {
self.hook_with(program, slot, "")
}
#[must_use]
pub fn hook_with(
&self,
program: &str,
slot: &str,
defines: &str,
) -> String {
let program = self.pack.programs.get(program);
let file = program.and_then(|program| match slot {
"vertex" => program.vertex.as_ref(),
"shadow" => program.shadow.as_ref().or(program.vertex.as_ref()),
_ => program.fragment.as_ref(),
});
file.map(|file| {
if defines.is_empty() {
ShaderPack::hook_source(file, &self.prelude)
} else {
ShaderPack::hook_source(
file,
&format!("{defines}{}", self.prelude),
)
}
})
.unwrap_or_default()
}
}
#[derive(Resource, Default)]
pub struct ShaderPackState {
pub(crate) initialized: bool,
pub(crate) project: Option<PathBuf>,
pub(crate) default_id: String,
pub(crate) default_pack: Option<Arc<ShaderPack>>,
pub(crate) active_id: String,
pub(crate) active: Option<ActiveShaderPack>,
pub(crate) preset: String,
pub(crate) revision: u64,
pub(crate) error: Option<String>,
pub(crate) world: Option<ShaderWorld>,
pub(crate) composite_off: std::collections::BTreeSet<String>,
pub(crate) last_poll: Option<Instant>,
pub(crate) stamps: Vec<Option<SystemTime>>,
pub(crate) failed_banner: Option<String>,
}
fn stamps(pack: &ShaderPack) -> Vec<Option<SystemTime>> {
pack.files
.iter()
.map(|path| std::fs::metadata(path).and_then(|m| m.modified()).ok())
.collect()
}
impl ShaderPackState {
pub(crate) fn init(&mut self, project: Option<&Path>) {
if self.initialized {
return;
}
self.initialized = true;
self.project = project.map(Path::to_owned);
let id = project
.and_then(|p| std::fs::read_to_string(p.join("project.json")).ok())
.and_then(|text| serde_json::from_str::<Value>(&text).ok())
.and_then(|json| {
json.get("shader_pack")?.as_str().map(str::to_owned)
})
.unwrap_or_default();
self.default_id = id.clone();
if id.is_empty() {
return;
}
if let Err(error) = self.switch(&id, None) {
self.fail(&id, &error);
} else {
self.default_pack = self.active.as_ref().map(|a| a.pack.clone());
}
}
fn user(&self) -> Option<PathBuf> {
Some(crate::project::user_data_folder())
}
pub(crate) fn list(&self) -> Vec<ShaderPackInfo> {
discover(self.project.as_deref(), self.user().as_deref())
}
fn load(&self, id: &str) -> Result<ShaderPack, ShaderPackError> {
let user = self.user();
let Some((folder, source)) =
pack_folder(id, self.project.as_deref(), user.as_deref())
else {
return Err(invalid(
"pack.json",
0,
format!("no shader pack `{id}`"),
));
};
if !folder.join("pack.json").is_file() {
return Err(invalid(
"pack.json",
0,
format!("no shader pack `{id}` at {}", folder.display()),
));
}
ShaderPack::load(&folder, id, source)
}
pub(crate) fn switch(
&mut self,
id: &str,
values: Option<(String, BTreeMap<String, PackOptionValue>)>,
) -> Result<(), ShaderPackError> {
if id.is_empty() {
self.active_id.clear();
self.active = None;
self.preset.clear();
self.stamps.clear();
self.error = None;
self.revision += 1;
return Ok(());
}
let pack = Arc::new(self.load(id)?);
let (preset, values) = values.unwrap_or_else(|| {
let preset = pack.info.default_preset.clone();
let values = pack.preset_values(&preset);
(preset, values)
});
self.apply(id, pack, preset, values)
}
fn apply(
&mut self,
id: &str,
pack: Arc<ShaderPack>,
preset: String,
values: BTreeMap<String, PackOptionValue>,
) -> Result<(), ShaderPackError> {
let active = ActiveShaderPack {
prelude: pack.prelude(&values).into(),
pack,
values,
revision: self.revision + 1,
};
crate::rendering::composite::validate_pack(&active)?;
self.stamps = stamps(&active.pack);
self.active_id = id.to_owned();
self.active = Some(active);
self.preset = preset;
self.error = None;
self.revision += 1;
Ok(())
}
pub(crate) fn fail(&mut self, id: &str, error: &ShaderPackError) {
let fallback_ok = id != self.default_id
&& !self.default_id.is_empty()
&& self.switch(&self.default_id.clone(), None).is_ok();
if !fallback_ok {
self.active_id.clear();
self.active = None;
self.preset.clear();
self.revision += 1;
}
let fallback = if fallback_ok {
self.default_id.clone()
} else {
"built-in".to_owned()
};
eprintln!(
"{}: Shader pack {id} failed: {error} (using {fallback})",
error.code
);
self.error = Some(error.to_string());
self.failed_banner = Some(format!(
"Shader pack {id} failed: {error} (using {fallback})"
));
}
pub(crate) fn set_pack(&mut self, id: &str) -> Result<(), String> {
self.switch(id, None).map_err(|error| error.to_string())
}
pub(crate) fn set_preset(&mut self, preset: &str) -> Result<(), String> {
let Some(active) = &self.active else {
return Err("no shader pack is active".to_owned());
};
if !PRESETS.contains(&preset) {
return Err(format!("unknown preset `{preset}`; presets are Low, Medium, High, Ultra"));
}
let pack = active.pack.clone();
let values = pack.preset_values(preset);
let id = self.active_id.clone();
self.apply(&id, pack, preset.to_owned(), values)
.map_err(|e| e.to_string())
}
pub(crate) fn set_option(
&mut self,
id: &str,
value: PackOptionValue,
) -> Result<(), String> {
let Some(active) = &self.active else {
return Err("no shader pack is active".to_owned());
};
let Some(option) = active.pack.info.options.iter().find(|o| o.id == id)
else {
return Err(format!(
"shader pack `{}` has no option `{id}`",
self.active_id
));
};
if !value_fits(&option.kind, &value) {
return Err(format!("{value:?} does not fit option `{id}`"));
}
let pack = active.pack.clone();
let mut values = active.values.clone();
values.insert(id.to_owned(), value);
let preset = if pack.presets.get(&self.preset) == Some(&values) {
self.preset.clone()
} else {
pack.preset_named_by(&values)
};
let pack_id = self.active_id.clone();
self.apply(&pack_id, pack, preset, values)
.map_err(|e| e.to_string())
}
pub(crate) fn reload(&mut self) {
let id = self.active_id.clone();
if id.is_empty() {
if self.error.is_some() && !self.default_id.is_empty() {
let default = self.default_id.clone();
match self.switch(&default, None) {
Ok(()) => self.failed_banner = None,
Err(error) => self.fail(&default, &error),
}
} else {
self.revision += 1;
}
return;
}
let keep = self
.active
.as_ref()
.map(|a| (self.preset.clone(), a.values.clone()));
let result = self.load(&id).and_then(|pack| {
let pack = Arc::new(pack);
let (preset, mut values) = keep.unwrap_or_default();
let mut fresh = pack.preset_values(&pack.info.default_preset);
for option in &pack.info.options {
if let Some(value) = values.remove(&option.id) {
if value_fits(&option.kind, &value) {
fresh.insert(option.id.clone(), value);
}
}
}
let preset = if pack.presets.get(&preset) == Some(&fresh) {
preset
} else {
pack.preset_named_by(&fresh)
};
self.apply(&id, pack, preset, fresh)
});
match result {
Ok(()) => {
self.failed_banner = None;
if id == self.default_id {
self.default_pack =
self.active.as_ref().map(|a| a.pack.clone());
}
}
Err(error) => self.fail(&id, &error),
}
}
pub(crate) fn poll(&mut self) {
let now = Instant::now();
if self
.last_poll
.is_some_and(|last| now.duration_since(last).as_secs_f32() < 0.5)
{
return;
}
self.last_poll = Some(now);
let changed = match &self.active {
Some(active) => stamps(&active.pack) != self.stamps,
None => false,
};
if changed {
self.reload();
}
}
pub(crate) fn value(&self, name: &str) -> Option<Vec<f32>> {
let find = |pack: &ShaderPack| {
pack.values
.iter()
.find(|(key, _)| key.eq_ignore_ascii_case(name))
.map(|(_, v)| v.clone())
};
self.active
.as_ref()
.and_then(|a| find(&a.pack))
.or_else(|| self.default_pack.as_deref().and_then(find))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn tiny_pack_parses_and_makes_its_prelude() {
let folder = Path::new(env!("CARGO_MANIFEST_DIR"))
.join("docs/examples/shaderpacks/tiny");
let pack =
ShaderPack::load(&folder, "tiny", ShaderPackSource::Game).unwrap();
assert_eq!(pack.info.name, "Tiny");
let prelude = pack.prelude(&pack.preset_values("Low"));
assert!(prelude.contains("#define OPT_SEPIA 1\n"), "{prelude}");
assert!(prelude.contains("#define OPT_TONE_WARM 0\n"), "{prelude}");
assert!(prelude.contains("#define OPT_TONE_COLD 1\n"), "{prelude}");
assert!(prelude.contains("#define OPT_TONE 0\n"), "{prelude}");
assert!(prelude.contains("#define OPT_STRENGTH 0.5\n"), "{prelude}");
assert!(
prelude.contains("const float PACK_SWAY = 0.1;"),
"{prelude}"
);
assert!(
prelude.contains("const vec3 PACK_TINT = vec3(1.0, 0.9, 0.7);"),
"{prelude}"
);
assert!(prelude.contains("const uint RUSTING_FLAG_FOLIAGE = 1u << 16;"));
let ultra = pack.prelude(&pack.preset_values("Ultra"));
assert!(ultra.contains("#define OPT_STRENGTH 1.0\n"), "{ultra}");
assert_eq!(pack.preset_named_by(&pack.preset_values("High")), "High");
let broken = ShaderPack::parse(
&folder,
"tiny",
ShaderPackSource::Game,
"{\n \"format\": 1,\n \"programs\": {\"sky\": {\"vertex\": \"a.glsl\"}}\n}",
)
.unwrap_err();
assert_eq!(
(broken.code, broken.line),
("SHADER_PACK_INVALID", 3),
"{broken}"
);
}
#[test]
fn textures_stack_into_an_atlas_named_in_the_prelude() {
let folder = std::env::temp_dir()
.join(format!("rusting_pack_textures_{}", std::process::id()));
std::fs::create_dir_all(folder.join("img")).unwrap();
image::RgbaImage::new(64, 32)
.save(folder.join("img/moon.png"))
.unwrap();
image::RgbaImage::new(16, 16)
.save(folder.join("img/star.png"))
.unwrap();
let manifest = |textures: &str| {
format!("{{\"format\": 1, \"name\": \"T\", \"presets\": {{\"Low\": {{}}, \"Medium\": {{}}, \"High\": {{}}, \"Ultra\": {{}}}}, \"textures\": {textures}}}")
};
let pack = ShaderPack::parse(
&folder,
"t",
ShaderPackSource::Game,
&manifest(r#"{"star": "img/star.png", "moon": "img/moon.png"}"#),
);
let pack = pack.unwrap();
assert_eq!(
pack.textures
.iter()
.map(|t| (t.name.as_str(), t.size))
.collect::<Vec<_>>(),
[("moon", [64, 32]), ("star", [16, 16])]
);
assert!(pack.files.contains(&folder.join("img/moon.png")));
assert_eq!(
texture_atlas(&[[64, 32], [16, 16]]),
([64, 64], vec![[0, 0], [0, 48]])
);
let prelude = pack.prelude(&pack.default_values());
assert!(
prelude.contains(
"const vec4 PACK_TEXTURE_MOON = vec4(0.0, 0.0, 1.0, 0.5);"
),
"{prelude}"
);
assert!(
prelude.contains(
"const vec4 PACK_TEXTURE_STAR = vec4(0.0, 0.75, 0.25, 1.0);"
),
"{prelude}"
);
for bad in [
r#"{"moon": "../moon.png"}"#,
r#"{"Moon": "img/moon.png"}"#,
r#"{"moon": "img/none.png"}"#,
"[]",
] {
let error = ShaderPack::parse(
&folder,
"t",
ShaderPackSource::Game,
&manifest(bad),
)
.unwrap_err();
assert_eq!(error.code, "SHADER_PACK_INVALID", "{bad}");
}
std::fs::remove_dir_all(&folder).unwrap();
}
#[test]
fn composite_when_and_option_hints() {
let tiny = Path::new(env!("CARGO_MANIFEST_DIR"))
.join("docs/examples/shaderpacks/tiny");
let folder = std::env::temp_dir()
.join(format!("rusting-when-{}", std::process::id()));
std::fs::create_dir_all(&folder).unwrap();
for file in ["foliage.vert.glsl", "final.glsl"] {
std::fs::copy(tiny.join(file), folder.join(file)).unwrap();
}
std::fs::write(folder.join("pass.glsl"), "#rusting composite 1\n")
.unwrap();
let json = std::fs::read_to_string(tiny.join("pack.json")).unwrap();
let with = |hint: &str, when: &str| {
let pass = |name: &str, when: &str| {
format!("{{ \"name\": \"{name}\", \"fragment\": \"pass.glsl\"{when} }}")
};
let composite = [
pass("a", ", \"when\": [\"sepia\"]"),
pass("b", ", \"when\": [\"tone\"]"),
pass("c", &format!(", \"when\": [{when}]")),
pass("d", ""),
]
.join(",\n ");
json.replacen(
"\"cost\": \"low\" },",
&format!("\"cost\": \"low\", \"hint\": \"{hint}\" }},"),
1,
)
.replacen(
"\"options\": [",
&format!(
"\"composite\": [\n {composite}\n ],\n \"options\": ["
),
1,
)
};
let parse = |hint: &str, when: &str| {
ShaderPack::parse(
&folder,
"when",
ShaderPackSource::Game,
&with(hint, when),
)
};
let pack = parse("Brown old photo look", "\"strength\", \"tone\"");
let unknown = parse("Brown", "\"missing\"");
let long = parse(&"x".repeat(121), "\"strength\"");
let _ = std::fs::remove_dir_all(&folder);
let pack = pack.unwrap();
assert_eq!(pack.info.options[0].hint, "Brown old photo look");
assert_eq!(pack.info.options[1].hint, "");
let on = |values: &BTreeMap<String, PackOptionValue>| -> Vec<bool> {
pack.composite
.iter()
.map(|pass| pack.composite_on(pass, values))
.collect()
};
let mut values = pack.preset_values("High");
assert_eq!(on(&values), [true, false, true, true]);
values.insert("sepia".into(), PackOptionValue::Bool(false));
values.insert("tone".into(), PackOptionValue::Enum("cold".into()));
values.insert("strength".into(), PackOptionValue::Slider(0.0));
assert_eq!(on(&values), [false, true, true, true]);
let unknown = unknown.unwrap_err();
assert_eq!(
(unknown.code, unknown.line),
("SHADER_PACK_INVALID", 14),
"{unknown}"
);
assert!(unknown.message.contains("missing"), "{unknown}");
let long = long.unwrap_err();
assert!(long.message.contains("120"), "{long}");
}
#[test]
fn sky_lut_program_parses_and_validates() {
let tiny = Path::new(env!("CARGO_MANIFEST_DIR"))
.join("docs/examples/shaderpacks/tiny");
let folder = std::env::temp_dir()
.join(format!("rusting-sky-lut-{}", std::process::id()));
std::fs::create_dir_all(&folder).unwrap();
for file in ["foliage.vert.glsl", "final.glsl"] {
std::fs::copy(tiny.join(file), folder.join(file)).unwrap();
}
let json = std::fs::read_to_string(tiny.join("pack.json")).unwrap().replacen(
"\"final\": { \"fragment\": \"final.glsl\" }",
"\"final\": { \"fragment\": \"final.glsl\" },\n \"sky_lut\": { \"fragment\": \"sky_lut.glsl\" }",
1,
);
std::fs::write(folder.join("pack.json"), json).unwrap();
std::fs::write(
folder.join("sky_lut.glsl"),
"#rusting sky_lut 1\n\
void sky_lut(SkyLutInput i, inout vec3 color) {\n\
color.r += step(i.uv.y, 0.5) * sky_param(1).w * sin(frame.time.x);\n\
}\n",
)
.unwrap();
let loaded = ShaderPack::load(&folder, "sky", ShaderPackSource::Game);
let _ = std::fs::remove_dir_all(&folder);
let pack = loaded.unwrap();
assert!(pack.programs["sky_lut"].fragment.is_some());
let active = |pack: ShaderPack| ActiveShaderPack {
prelude: pack.prelude(&pack.preset_values("High")).into(),
values: pack.preset_values("High"),
pack: Arc::new(pack),
revision: 1,
};
let sky = active(pack);
assert!(sky
.hook("sky_lut", "fragment")
.starts_with("#rusting sky_lut 1"));
if std::process::Command::new("glslc")
.arg("--version")
.output()
.is_err()
{
eprintln!("glslc is missing; skipped compiling the packs");
return;
}
crate::rendering::composite::validate_pack(&sky).unwrap();
let tiny =
ShaderPack::load(&tiny, "tiny", ShaderPackSource::Game).unwrap();
crate::rendering::composite::validate_pack(&active(tiny)).unwrap();
}
#[test]
fn clouds_scale_makes_a_cloud_layer() {
let tiny = Path::new(env!("CARGO_MANIFEST_DIR"))
.join("docs/examples/shaderpacks/tiny");
let folder = std::env::temp_dir()
.join(format!("rusting-clouds-{}", std::process::id()));
std::fs::create_dir_all(&folder).unwrap();
for file in ["foliage.vert.glsl", "final.glsl"] {
std::fs::copy(tiny.join(file), folder.join(file)).unwrap();
}
std::fs::write(
folder.join("clouds.glsl"),
"#rusting clouds 1\n\
#ifdef RUSTING_CLOUD_LAYER\n\
void cloud_layer(CloudLayerInput i, inout vec4 layer) {\n\
vec4 last = cloud_layer_history(cloud_layer_previous_uv(i.direction));\n\
layer = mix(layer, vec4(sky_lut_at(i.direction) * 0.1, 0.5), step(0.0, i.direction.y)) + 0.0 * last + frame.params[0];\n\
}\n\
#else\n\
void clouds(SkyInput i, inout vec3 color) {}\n\
#endif\n",
)
.unwrap();
let json = std::fs::read_to_string(tiny.join("pack.json")).unwrap();
let with = |clouds: &str| {
json.replacen(
"\"final\": { \"fragment\": \"final.glsl\" }",
&format!("\"final\": {{ \"fragment\": \"final.glsl\" }},\n {clouds}"),
1,
)
};
let parse = |clouds: &str| {
ShaderPack::parse(
&folder,
"clouds",
ShaderPackSource::Game,
&with(clouds),
)
};
let layer = parse(
"\"clouds\": { \"fragment\": \"clouds.glsl\", \"scale\": 0.25 }",
);
let inline = parse("\"clouds\": { \"fragment\": \"clouds.glsl\" }");
let odd = parse(
"\"clouds\": { \"fragment\": \"clouds.glsl\", \"scale\": 0.3 }",
);
let sky =
parse("\"sky\": { \"fragment\": \"clouds.glsl\", \"scale\": 0.5 }");
let _ = std::fs::remove_dir_all(&folder);
let layer = layer.unwrap();
assert_eq!(layer.programs["clouds"].layer_divisor, 4);
assert_eq!(inline.unwrap().programs["clouds"].layer_divisor, 0);
let odd = odd.unwrap_err();
assert_eq!((odd.code, odd.line), ("SHADER_PACK_INVALID", 10), "{odd}");
assert_eq!(sky.unwrap_err().code, "SHADER_PACK_INVALID");
let active = ActiveShaderPack {
prelude: layer.prelude(&layer.preset_values("High")).into(),
values: layer.preset_values("High"),
pack: Arc::new(layer),
revision: 1,
};
let hooks = crate::rendering::composite::PackHooks::new(&active);
assert!(hooks.clouds.is_empty() && !hooks.cloud_layer.is_empty());
assert_eq!(hooks.cloud_divisor, 4);
if std::process::Command::new("glslc")
.arg("--version")
.output()
.is_err()
{
eprintln!("glslc is missing; skipped compiling the pack");
return;
}
crate::rendering::composite::validate_pack(&active).unwrap();
}
}