Skip to main content

shaders/
shaders.rs

1//! Custom material shaders: a lake with waves, grass in the wind, and a
2//! procedural pattern, with post-processing.
3//!
4//!   `cargo run --release --example shaders --features 3d`
5//!
6//! The shaders are in `assets/shaders/`: edit one while the game runs and
7//! it reloads. Left and right arrows turn the time of day, F switches fog.
8//! `SHOT=path.png` saves the second frame and quits; `HOUR=19` and `FOG=1`
9//! set the time and the fog.
10
11use ponko::*;
12
13/// One blade of grass: a thin quad, texture v from 0 at the root to 1 at the tip.
14fn blade() -> MeshData {
15    MeshData {
16        positions: vec![
17            vec3(-0.03, 0.0, 0.0),
18            vec3(0.03, 0.0, 0.0),
19            vec3(0.01, 0.6, 0.0),
20            vec3(-0.01, 0.6, 0.0),
21        ],
22        normals: vec![Vec3::Z; 4],
23        uvs: vec![vec2(0.0, 0.0), vec2(1.0, 0.0), vec2(1.0, 1.0), vec2(0.0, 1.0)],
24        indices: vec![0, 1, 2, 0, 2, 3],
25        ..Default::default()
26    }
27}
28
29/// A pattern made in the shader: no texture needed.
30const TILES: &str = "
31fn surface(s: Surface) -> Surface {
32    var out = s;
33    let cell = floor(s.uv * vec2<f32>(16.0, 8.0));
34    let checker = (cell.x + cell.y) % 2.0;
35    out.albedo = mix(vec3<f32>(0.8, 0.3, 0.1), vec3<f32>(0.95, 0.85, 0.6), checker);
36    out.roughness = mix(0.25, 0.7, checker);
37    return out;
38}";
39
40fn main() {
41    let env = |name| std::env::var(name).ok().and_then(|v| v.parse::<f32>().ok());
42    let mut hour = env("HOUR").unwrap_or(15.5);
43    let mut fog = env("FOG").unwrap_or(0.0) > 0.0;
44    Game::new("Crabby Engine: shaders", 1280, 720)
45        .system(move |ctx: &mut Ctx| {
46            if ctx.key_pressed(Key::Escape) {
47                ctx.quit();
48            }
49            // Two hours a second.
50            if ctx.key_down(Key::Right) {
51                hour = (hour + ctx.dt() * 2.0) % 24.0;
52            }
53            if ctx.key_down(Key::Left) {
54                hour = (hour - ctx.dt() * 2.0).rem_euclid(24.0);
55            }
56            if ctx.key_pressed(Key::F) {
57                fog = !fog;
58            }
59            ctx.lighting.sun_direction = Sky::sun_direction_at(hour);
60            ctx.sky.fog_density = if fog { 0.02 } else { 0.0 };
61            ctx.sky.fog_falloff = 8.0;
62            if ctx.frame() == 2
63                && let Ok(path) = std::env::var("SHOT")
64            {
65                ctx.save_screenshot(path);
66            }
67            if ctx.frame() == 4 && std::env::var("SHOT").is_ok() {
68                ctx.quit();
69            }
70        })
71        .run(|ctx| {
72            ctx.clear(Color::hex(0x9CC4E4));
73            ctx.camera3d.position = vec3(0.0, 4.0, 11.0);
74            ctx.camera3d.look_at(vec3(0.0, 0.5, 0.0));
75            ctx.post.bloom = 0.1;
76
77            let ground_look = MaterialDesc {
78                roughness: 0.95,
79                ..MaterialDesc::color(Color::hex(0x5E7A3A))
80            };
81            let ground = ctx.create_material(ground_look);
82            let ground = ctx.create_mesh(&MeshData::plane(600.0), ground);
83            ctx.world.spawn((Transform3D::default(), Model::new(ground)));
84
85            let water = ctx.load_shader("shaders/water.wgsl");
86            let lake = ctx.create_material(MaterialDesc {
87                shader: Some(water),
88                blend: Blend::Transparent,
89                custom: [vec4(0.06, 1.5, 1.2, 1.2), Vec4::ZERO, Vec4::ZERO, Vec4::ZERO],
90                ..MaterialDesc::color(Color::hex(0x1E4D6B))
91            });
92            let mut surface = MeshData::plane(12.0);
93            // More vertices, so the waves have something to move.
94            surface = subdivide(surface, 5);
95            let lake = ctx.create_mesh(&surface, lake);
96            ctx.world
97                .spawn((Transform3D::at(vec3(-3.0, 0.15, -4.0)), Model::new(lake)));
98
99            // The lake bed, under the water: sand sloping down, and stones.
100            let sand = ctx.create_material(MaterialDesc {
101                roughness: 0.9,
102                ..MaterialDesc::color(Color::hex(0xC2A878))
103            });
104            let bed = ctx.create_mesh(&MeshData::plane(12.0), sand);
105            let tilt = Quat::from_rotation_x(0.12);
106            ctx.world.spawn((
107                Transform3D {
108                    rotation: tilt,
109                    ..Transform3D::at(vec3(-3.0, -0.6, -4.0))
110                },
111                Model::new(bed),
112            ));
113            let stone = ctx.create_material(MaterialDesc {
114                roughness: 0.7,
115                ..MaterialDesc::color(Color::hex(0x8A8A80))
116            });
117            let pebble = ctx.create_mesh(&MeshData::sphere(0.35, 16), stone);
118            for i in 0..12 {
119                let at = vec3(
120                    -7.0 + i as f32 * 0.8,
121                    -0.3 - (i % 3) as f32 * 0.2,
122                    -3.0 + (i % 4) as f32 * -1.5,
123                );
124                ctx.world.spawn((Transform3D::at(at), Model::new(pebble)));
125            }
126
127            let wind = ctx.load_shader("shaders/wind.wgsl");
128            let grass = ctx.create_material(MaterialDesc {
129                shader: Some(wind),
130                double_sided: true,
131                roughness: 0.8,
132                custom: [vec4(0.25, 2.0, 0.0, 0.0), Vec4::ZERO, Vec4::ZERO, Vec4::ZERO],
133                ..MaterialDesc::color(Color::hex(0x7FA83C))
134            });
135            let blade = ctx.create_mesh(&blade(), grass);
136            let mut seed = 7u32;
137            let mut random = move || {
138                seed = seed.wrapping_mul(1_664_525).wrapping_add(1_013_904_223);
139                (seed >> 8) as f32 / (1 << 24) as f32
140            };
141            for _ in 0..6000 {
142                let at = vec3(random() * 14.0 - 2.0, 0.0, random() * 10.0 - 3.0);
143                if at.x < 4.0 && at.z < 2.5 {
144                    continue; // the lake
145                }
146                let t = Transform3D {
147                    rotation: Quat::from_rotation_y(random() * 6.3),
148                    scale: Vec3::splat(0.6 + random() * 0.8),
149                    ..Transform3D::at(at)
150                };
151                ctx.world.spawn((t, Model::new(blade)));
152            }
153
154            let tiles = ctx.create_shader("tiles", TILES);
155            let ball = ctx.create_material(MaterialDesc {
156                shader: Some(tiles),
157                ..Default::default()
158            });
159            let ball = ctx.create_mesh(&MeshData::sphere(1.0, 48), ball);
160            ctx.world
161                .spawn((Transform3D::at(vec3(3.5, 1.0, 1.0)), Model::new(ball)));
162        });
163}
164
165/// Splits every triangle into four, `times` times.
166fn subdivide(mut mesh: MeshData, times: u32) -> MeshData {
167    for _ in 0..times {
168        let mut indices = Vec::with_capacity(mesh.indices.len() * 4);
169        for tri in mesh.indices.as_chunks::<3>().0 {
170            let mut mid = |a: u32, b: u32| {
171                let (a, b) = (a as usize, b as usize);
172                mesh.positions.push((mesh.positions[a] + mesh.positions[b]) / 2.0);
173                mesh.normals.push(mesh.normals[a]);
174                mesh.uvs.push((mesh.uvs[a] + mesh.uvs[b]) / 2.0);
175                mesh.positions.len() as u32 - 1
176            };
177            let (a, b, c) = (tri[0], tri[1], tri[2]);
178            let (ab, bc, ca) = (mid(a, b), mid(b, c), mid(c, a));
179            indices.extend_from_slice(&[a, ab, ca, ab, b, bc, ca, bc, c, ab, bc, ca]);
180        }
181        mesh.indices = indices;
182    }
183    mesh
184}