Skip to content

Commit

Permalink
feat(shader): allow uploading and using a custom shader for sprites
Browse files Browse the repository at this point in the history
  • Loading branch information
tversteeg committed Nov 29, 2024
1 parent e512f6d commit 6102230
Show file tree
Hide file tree
Showing 21 changed files with 852 additions and 455 deletions.
9 changes: 4 additions & 5 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ categories = [
"games",
"game-engines",
]
rust-version = "1.79.0"
rust-version = "1.83.0"
include = ["/src", "build.rs", "/shaders"]

[package.metadata.docs.rs]
Expand Down Expand Up @@ -55,7 +55,7 @@ downcast-rs = "1.2.1"
fastrand = "2.2.0"
gilrs = "0.11.0"
glam = { version = "0.29.2", features = ["bytemuck", "fast-math"] }
hashbrown = "0.15.1"
hashbrown = "0.15.2"
imgref = { version = "1.11.0", default-features = false, optional = true }
kira = { version = "0.9.6", default-features = false, features = ["cpal", "ogg"] }
nanoserde = "0.1.37"
Expand All @@ -72,14 +72,14 @@ winit = "0.30.5"
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
notify-debouncer-mini = "0.5.0"
pollster = "0.4.0"
wgpu = { version = "23.0.0", default-features = false, features = ["wgsl"] }
wgpu = { version = "23.0.1", default-features = false, features = ["wgsl"] }

# Dependencies specifically for the web platform
[target.'cfg(target_arch = "wasm32")'.dependencies]
console_error_panic_hook = "0.1.7"
wasm-bindgen-futures = "0.4.45"
web-sys = { version = "0.3.72", features = ["Document", "Window", "Element"] }
wgpu = { version = "23.0.0", default-features = false, features = ["webgl", "wgsl"] }
wgpu = { version = "23.0.1", default-features = false, features = ["webgl", "wgsl"] }

[build-dependencies]
naga = { version = "23.0.0", default-features = false, features = ["wgsl-in", "wgsl-out"] }
Expand Down Expand Up @@ -293,7 +293,6 @@ trivially_copy_pass_by_ref = "warn"
uninlined_format_args = "warn"
unnecessary_box_returns = "warn"
unnecessary_join = "warn"
unnecessary_literal_bound = "warn"
unnecessary_wraps = "warn"
unnested_or_patterns = "warn"
unused_async = "warn"
Expand Down
26 changes: 26 additions & 0 deletions assets/shader.wgsl
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
struct VertexOutput {
// These two fields must be here
@builtin(position) clip_position: vec4f,
@location(0) tex_coords: vec2f,
}

@vertex
fn vs_main(
model: VertexInput,
instance: InstanceInput,
) -> VertexOutput {
// This function needs this name and input types

// Use the complicated vertex shader setup from the engine
return vs_main_impl(model, instance);
}

@fragment
fn fs_main(in: VertexOutput) -> @location(0) vec4f {
// This function needs this name and output types

// Return the pixel in a nearest-neighbor fashion
return textureSample(t_diffuse, s_diffuse, in.tex_coords)
// Add a red hue to the pixel
+ vec4f(1.0, 0.0, 0.0, 0.0);
}
19 changes: 16 additions & 3 deletions build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,17 @@ use naga::{
};

/// Compile a WGSL shader into Spir-V bytes and write it to file.
fn minify_wgsl(source: impl AsRef<Path>, target: impl AsRef<Path>) {
fn minify_wgsl(source: impl AsRef<Path>, target: impl AsRef<Path>, include_base: bool) {
// Read the source WGSL
let source = std::fs::read_to_string(source).expect("Error reading WGSL shader file");

// Include the base if set
let source = if include_base {
include_str!("shaders/custom_shader_base.wgsl").to_owned() + &source
} else {
source
};

// Parse into NAGA module
let mut module = naga::front::wgsl::parse_str(&source).expect("Error compiling WGSL shader");

Expand Down Expand Up @@ -39,15 +46,21 @@ fn main() {
println!("cargo::rerun-if-changed=shaders/downscale.wgsl");
println!("cargo::rerun-if-changed=shaders/rotation.wgsl");
println!("cargo::rerun-if-changed=shaders/nearest_neighbor.wgsl");
println!("cargo::rerun-if-changed=shaders/custom_shader_base.wgsl");

let out_dir_str = std::env::var_os("OUT_DIR").unwrap();
let out_dir = Path::new(&out_dir_str);

// Compile the shaders into binaries placed in the OUT_DIR
minify_wgsl("shaders/downscale.wgsl", out_dir.join("downscale.wgsl"));
minify_wgsl("shaders/rotation.wgsl", out_dir.join("rotation.wgsl"));
minify_wgsl(
"shaders/downscale.wgsl",
out_dir.join("downscale.wgsl"),
false,
);
minify_wgsl("shaders/rotation.wgsl", out_dir.join("rotation.wgsl"), true);
minify_wgsl(
"shaders/nearest_neighbor.wgsl",
out_dir.join("nearest_neighbor.wgsl"),
true,
);
}
2 changes: 1 addition & 1 deletion crates/macros/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ proc-macro2 = "1.0.92"
walkdir = "2.5.0"

png = { version = "0.17.14", optional = true }
sprite_dicing = { version = "0.1.3", optional = true }
sprite_dicing = { version = "0.1.4", optional = true }
bytemuck = { version = "1.20.0", optional = true }
oxipng = { version = "9.1.2", optional = true }
phf_codegen = { version = "0.11.2", optional = true }
Expand Down
41 changes: 41 additions & 0 deletions examples/custom_shader.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
//! Show how to load a custom shader.
//! TODO.
use chuot::{Config, Context, Game};

/// Define a game state for our example.
struct GameState;

impl Game for GameState {
/// Render the game.
fn render(&mut self, ctx: Context) {
// Draw a sprite with a custom shader
ctx.sprite("threeforms")
// Use the custom shader
.shader("shader")
.translate_x(50.0)
.draw();

// Draw a sprite with the default shader
ctx.sprite("threeforms").translate_x(-50.0).draw();
}

/// Do nothing during the update loop.
fn update(&mut self, _ctx: Context) {}
}

/// Open an empty window.
fn main() {
// Game configuration
let config = Config {
buffer_width: 240.0,
buffer_height: 192.0,
// Apply a minimum of 3 times scaling for the buffer
// Will result in a minimum, and on web exact, window size of 720x576
scaling: 3.0,
..Default::default()
};

// Spawn the window and run the 'game'
GameState.run(chuot::load_assets!(), config);
}
2 changes: 1 addition & 1 deletion examples/sprite_scale.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
//! (pivot_x: Center, pivot_y: Center)
//! ```
use chuot::{config::RotationAlgorithm, Config, Context, Game};
use chuot::{Config, Context, Game, config::RotationAlgorithm};

/// How much we will scale with the mouse.
const SCALE_FACTOR: f32 = 50.0;
Expand Down
74 changes: 74 additions & 0 deletions shaders/custom_shader_base.wgsl
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
// Size of both width and height of the atlas texture
const ATLAS_TEXTURE_SIZE: f32 = 4096.0;

@group(0) @binding(0)
var t_diffuse: texture_2d<f32>;
@group(0) @binding(1)
var s_diffuse: sampler;

struct TextureInfo {
@location(0) offset: vec2f,
@location(1) _ignore: vec2f,
}

struct ScreenInfo {
@location(0) size: vec2f,
@location(1) half_size: vec2f,
}

@group(1) @binding(0)
var<uniform> tex_info: array<TextureInfo, 1024>;

@group(2) @binding(0)
var<uniform> screen_info: ScreenInfo;

struct VertexInput {
@location(0) position: vec3f,
@location(1) tex_coords: vec2f,
}

struct InstanceInput {
// Matrix type is not supported in vertex input, construct it from 3 vec3's
// The last row of the matrix is always 0 0 1 so we can save some bytes by constructing that ourselves
@location(2) matrix: vec4f,
// X and Y position used in the transformation matrix
@location(3) translation: vec2f,
// Sub rectangle of the texture to render, offset of the texture will be determined by the texture info uniform
@location(4) sub_rectangle: vec4f,
// Which texture to render, dimensions are stored in the uniform buffer
@location(5) tex_index: u32,
}

fn vs_main_impl(
model: VertexInput,
instance: InstanceInput,
) -> VertexOutput {
// Create the 2D affine transformation matrix for each instance
let instance_matrix = mat3x3<f32>(
vec3f(instance.matrix.xy, 0.0),
vec3f(instance.matrix.zw, 0.0),
vec3f(instance.translation, 1.0),
);

// Get the texture rectangle from the atlas
let offset = tex_info[instance.tex_index].offset;

// Resize the quad to the size of the texture
let model_position = model.position.xy * instance.sub_rectangle.zw;

// Translate, rotate and skew with the instance matrix
let projected_position = instance_matrix * vec3f(model_position, 1.0);

// Move from 0..width to -1..1
let screen_offset = projected_position.xy / screen_info.half_size - 1.0;
// Move the 0..1 texture coordinates to relative coordinates within the 4096x4096 atlas texture for the specified texture
// Also apply the sub rectangle offset from the instance
let tex_coords = (offset + instance.sub_rectangle.xy + instance.sub_rectangle.zw * model.tex_coords) / ATLAS_TEXTURE_SIZE;

var out: VertexOutput;
out.tex_coords = tex_coords;
out.clip_position = vec4f(screen_offset.x, screen_offset.y, model.position.z, 1.0);

return out;
}

75 changes: 4 additions & 71 deletions shaders/nearest_neighbor.wgsl
Original file line number Diff line number Diff line change
@@ -1,84 +1,17 @@
//! Optimized base shader without special rotation algorithms.

// Size of both width and height of the atlas texture
const ATLAS_TEXTURE_SIZE: f32 = 4096.0;

@group(0) @binding(0)
var t_diffuse: texture_2d<f32>;
@group(0) @binding(1)
var s_diffuse: sampler;

struct TextureInfo {
@location(0) offset: vec2f,
// Not used
@location(1) size: vec2f,
}

struct ScreenInfo {
@location(0) size: vec2f,
@location(1) half_size: vec2f,
}

@group(1) @binding(0)
var<uniform> tex_info: array<TextureInfo, 1024>;

@group(2) @binding(0)
var<uniform> screen_info: ScreenInfo;

struct VertexInput {
@location(0) position: vec3f,
@location(1) tex_coords: vec2f,
}

struct InstanceInput {
// Matrix type is not supported in vertex input, construct it from 3 vec3's
// The last row of the matrix is always 0 0 1 so we can save some bytes by constructing that ourselves
@location(2) matrix: vec4f,
// X and Y position used in the transformation matrix
@location(3) translation: vec2f,
// Sub rectangle of the texture to render, offset of the texture will be determined by the texture info uniform
@location(4) sub_rectangle: vec4f,
// Which texture to render, dimensions are stored in the uniform buffer
@location(5) tex_index: u32,
}


struct VertexOutput {
@builtin(position) clip_position: vec4f,
@location(0) tex_coords: vec2f,
}

@vertex
fn vs_main(
model: VertexInput,
instance: InstanceInput,
) -> VertexOutput {
// Create the 2D affine transformation matrix for each instance
let instance_matrix = mat3x3<f32>(
vec3f(instance.matrix.xy, 0.0),
vec3f(instance.matrix.zw, 0.0),
vec3f(instance.translation, 1.0),
);

// Get the texture rectangle from the atlas
let offset = tex_info[instance.tex_index].offset;

// Resize the quad to the size of the texture
let model_position = model.position.xy * instance.sub_rectangle.zw;

// Translate, rotate and skew with the instance matrix
let projected_position = instance_matrix * vec3f(model_position, 1.0);

// Move from 0..width to -1..1
let screen_offset = projected_position.xy / screen_info.half_size - 1.0;
// Move the 0..1 texture coordinates to relative coordinates within the 4096x4096 atlas texture for the specified texture
// Also apply the sub rectangle offset from the instance
let tex_coords = (offset + instance.sub_rectangle.xy + instance.sub_rectangle.zw * model.tex_coords) / ATLAS_TEXTURE_SIZE;

var out: VertexOutput;
out.tex_coords = tex_coords;
out.clip_position = vec4f(screen_offset.x, screen_offset.y, model.position.z, 1.0);

return out;
// Use base shader from 'shaders/custom_base_shader.wgsl'
return vs_main_impl(model, instance);
}

@fragment
Expand Down
Loading

0 comments on commit 6102230

Please sign in to comment.