Stjärnfält med en shader
Den gröna bakgrunden börjar kännas lite tråkig, så nu är det dags att göra en lite mer intressant bakgrund. Vi kommer använda oss av en pixel shader för att göra ett stjärnfält. Hur man kodar en shader ligger utanför den här guiden, utan vi kommer använda oss av en färdig utan att gå in på detaljerna.
Kortfattat är en shader ett program som körs på datorns GPU, skrivet i ett
C-liknande programmeringsspråk som kallas GLSL. Shadern består av två delar,
en vertex shader och en fragment shader. Vertex shadern konverterar från
koordinater i en 3D-miljö till 2D-koordinater för en skärm. Fragment shadern
körs sedan för varje pixel på skärmen, och sätter variabeln gl_FragColor
som
avgör vilken färg pixeln ska ha. Eftersom vårt spel är i 2D så gör vertex
shadern ingenting mer än att sätta positionen.
Implementering
Shaders
Längst upp i main.rs
ska vi lägga till en vertex shader och fragment-shadern
från en fil som vi kommer skapa senare. Vi använder oss av Rusts macro
include_str!()
som läser in filen som en &str
vid kompileringen.
Vertex-shadern är så kort att den kan läggas in direkt här i källkoden.
Den viktigaste raden i shadern är den som sätter gl_Position
. För
enkelhetens skull sätter vi iTime
som används av shadern från _Time.x
. Det
hade också gått att använda _Time
direkt i shadern.
const FRAGMENT_SHADER: &str = include_str!("starfield-shader.glsl");
const VERTEX_SHADER: &str = "#version 100
attribute vec3 position;
attribute vec2 texcoord;
attribute vec4 color0;
varying float iTime;
uniform mat4 Model;
uniform mat4 Projection;
uniform vec4 _Time;
void main() {
gl_Position = Projection * Model * vec4(position, 1);
iTime = _Time.x;
}
";
Initialisera shadern
I vår main()
funktion, innan loopen, så måste vi sätta upp några variabler
för att kunna rita ut shadern. Vi börjar med att skapa variabeln
direction_modifier
som vi ska använda för att påverka hur stjärnorna rör sig
medan cirkeln förflyttas i sidled. Därefter skapar vi en render_target
som
shadern kommer att renderas till.
Sen laddar vi in vår vertex shader och fragment shader till en Material
med
hjälp av en ShaderSource::Glsl
.
I parametrarna sätter vi även upp två uniforms till shadern som är globala
variabler som vi kan sätta för varje bildruta. Uniformen iResolution
innehåller fönstrets storlek, och direction_modifier
kommer användas för att
styra åt vilken riktning stjärnorna ska röra sig.
let mut direction_modifier: f32 = 0.0;
let render_target = render_target(320, 150);
render_target.texture.set_filter(FilterMode::Nearest);
let material = load_material(
ShaderSource::Glsl {
vertex: VERTEX_SHADER,
fragment: FRAGMENT_SHADER,
},
MaterialParams {
uniforms: vec![
UniformDesc::new("iResolution", UniformType::Float2),
UniformDesc::new("direction_modifier", UniformType::Float1),
],
..Default::default()
},
)
.unwrap();
Macroquad lägger automatiskt in några uniforms till shaders. De som finns
tillgängliga är _Time
, Model
, Projection
, Texture
och
_ScreenTexture
.
Rita ut shadern
Nu är det dags att byta ut den lila bakgrund till vårt stjärnfält. Byt ut
kodraden clear_background(DARKPURPLE);
till nedanstående kod.
Först måste vi tilldela fönstrets upplösning till materialets uniform
iResolution
för att alltid få rätt fönsterstorlek. Vi sätter även uniformen
direction_modifier
till värdet av den motsvarande variabeln.
Därefter använder vi funktionen gl_use_material()
för att använda
materialet. Slutligen använder vi funktionen draw_texture_ex()
för att rita
ut texturen från vår render_target
på skärmens bakgrund. Innan vi fortsätter
återställer vi shadern med gl_use_default_material()
så den inte används när
vi ritar ut resten av spelet.
clear_background(BLACK);
material.set_uniform("iResolution", (screen_width(), screen_height()));
material.set_uniform("direction_modifier", direction_modifier);
gl_use_material(&material);
draw_texture_ex(
&render_target.texture,
0.,
0.,
WHITE,
DrawTextureParams {
dest_size: Some(vec2(screen_width(), screen_height())),
..Default::default()
},
);
gl_use_default_material();
Styr stjärnornas rörelse
När spelaren håller ner höger eller vänster piltangent så lägger vi till
eller drar ifrån ett värde från variabeln direction_modifier
så att shadern
kan ändra riktningen på stjärnornas rörelse. Även här multiplicerar vi värdet
med delta_time
så det blir relativt till hur lång tid det har tagit sedan
föregående bildruta.
if is_key_down(KeyCode::Right) {
circle.x += MOVEMENT_SPEED * delta_time;
direction_modifier += 0.05 * delta_time;
}
if is_key_down(KeyCode::Left) {
circle.x -= MOVEMENT_SPEED * delta_time;
direction_modifier -= 0.05 * delta_time;
}
Skapa fil för shadern
Till sist måste vi skapa en fil som innehåller fragment shadern. Skapa en fil
med namnet starfield-shader.glsl
i din src
-katalog och lägg in följande
kod:
#version 100
// Starfield Tutorial by Martijn Steinrucken aka BigWings - 2020
// countfrolic@gmail.com
// License Creative Commons Attribution-NonCommercial-ShareAlike 3.0 Unported License.
// From The Art of Code: https://www.youtube.com/watch?v=rvDo9LvfoVE
precision highp float;
varying vec4 color;
varying vec2 uv;
varying float iTime;
uniform vec2 iResolution;
uniform float direction_modifier;
#define NUM_LAYERS 4.
mat2 Rot(float a) {
float s = sin(a), c = cos(a);
return mat2(c, -s, s, c);
}
float Star(vec2 uv, float flare) {
float d = length(uv);
float m = .05 / d;
float rays = max(0., 1. - abs(uv.x * uv.y * 1000.));
m += rays * flare;
uv *= Rot(3.1415 / 4.);
rays = max(0., 1. - abs(uv.x * uv.y * 1000.));
m += rays * .3 * flare;
m *= smoothstep(1., .2, d);
return m;
}
float Hash21(vec2 p) {
p = fract(p * vec2(123.34, 456.21));
p += dot(p, p + 45.32);
return fract(p.x * p.y);
}
vec3 StarLayer(vec2 uv) {
vec3 col = vec3(0);
vec2 gv = fract(uv) - .5;
vec2 id = floor(uv);
float t = iTime * 0.1;
for (int y = -1; y <= 1; y++) {
for (int x = -1; x <= 1; x++) {
vec2 offs = vec2(x, y);
float n = Hash21(id + offs); // random between 0 and 1
float size = fract(n * 345.32);
float star = Star(gv - offs - vec2(n, fract(n * 42.)) + .5, smoothstep(.9, 1., size) * .6);
vec3 color = sin(vec3(.8, .8, .8) * fract(n * 2345.2) * 123.2) * .5 + .5;
color = color * vec3(0.25, 0.25, 0.20);
star *= sin(iTime * 3. + n * 6.2831) * .5 + 1.;
col += star * size * color;
}
}
return col;
}
void main()
{
vec2 uv = (gl_FragCoord.xy - .5 * iResolution.xy) / iResolution.y;
float t = iTime * .02;
float speed = 3.0;
vec2 direction = vec2(-0.25 + direction_modifier, -1.0) * speed;
uv += direction;
vec3 col = vec3(0);
for (float i = 0.; i < 1.; i += 1. / NUM_LAYERS) {
float depth = fract(i+t);
float scale = mix(20., .5, depth);
float fade = depth * smoothstep(1., .9, depth);
col += StarLayer(uv * scale + i * 453.2) * fade;
}
gl_FragColor = vec4(col, 1.0);
}
Om du vill veta hur shadern fungerar så kan du titta på videon Shader Coding: Making a starfield av The Art of Code.
Nu är vårt stjärnfält klart och vårt spel börjar se ut som det utspelar sig i rymden!
Quiz
Testa dina nya kunskaper genom att svara på följande quiz innan du går vidare.