// Vertex shader struct InstanceInput { @location(2) position: vec2, @location(3) parallax: f32, @location(4) height: f32, @location(5) tint: vec2, }; struct VertexInput { @location(0) position: vec3, @location(1) texture_coords: vec2, } struct VertexOutput { @builtin(position) position: vec4, @location(0) texture_coords: vec2, @location(1) tint: vec2, } @group(1) @binding(0) var global: GlobalUniform; struct GlobalUniform { camera_position: vec2, camera_zoom: vec2, window_size: vec2, window_aspect: vec2, starfield_texture: vec2, starfield_tile_size: vec2, }; fn fmod(x: vec2, m: f32) -> vec2 { return x - floor(x / m) * m; } @vertex fn vertex_shader_main( vertex: VertexInput, instance: InstanceInput, ) -> VertexOutput { var out: VertexOutput; out.texture_coords = vertex.texture_coords; out.tint = instance.tint; // Center of the tile the camera is currently in, in game coordinates. // x div y = x - (x mod y) let tile_center = ( global.camera_position.xy - ( fmod( global.camera_position.xy + global.starfield_tile_size.x / 2.0, global.starfield_tile_size.x ) - global.starfield_tile_size.x / 2.0 ) ); // Apply sprite aspect ratio & scale factor // also applies screen aspect ratio var scale: f32 = instance.height / global.camera_zoom.x; // Minimum scale to prevent flicker at large zoom levels var real_size = scale * global.window_size.xy; // TODO: configurable. // Uniform distribution! if (real_size.x < 0.5 || real_size.y < 0.5) { // If this star is too small, don't even show it out.position = vec4(2.0, 2.0, 0.0, 1.0); return out; } if (real_size.x < 2.0 || real_size.y < 2.0) { // Otherwise, clamp to a minimum scale scale = 2.0 / max(global.window_size.x, global.window_size.y); } var pos: vec2 = vec2( vertex.position.x * scale / global.window_aspect.x, vertex.position.y * scale ); // World position relative to camera // (Note that instance position is in a different // coordinate system than usual) let camera_pos = (instance.position + tile_center) - global.camera_position.xy; // Translate pos = pos + ( // Don't forget to correct distance for screen aspect ratio too! (camera_pos / (global.camera_zoom.x * (instance.parallax))) / vec2(global.window_aspect.x, 1.0) ); out.position = vec4(pos, 0.0, 1.0) * instance.parallax; return out; } @group(0) @binding(0) var texture_array: binding_array>; @group(0) @binding(1) var sampler_array: binding_array; // Fragment shader @fragment fn fragment_shader_main(in: VertexOutput) -> @location(0) vec4 { let b = 0.8 - (in.tint.x / 1.5); // brightness let c = in.tint.y; // color // TODO: saturation // TODO: more subtle starfield // TODO: blur stars more? let c_top = vec3(0.369, 0.819, 0.796); let c_bot = vec3(0.979, 0.556, 0.556); let c_del = c_bot - c_top; return textureSampleLevel( texture_array[global.starfield_texture.x], sampler_array[global.starfield_texture.x], in.texture_coords, 0.0 ).rgba * vec4( (c_top + (c_del * c)) * b, 1.0 ); }