// INCLUDE: global uniform header struct InstanceInput { @location(2) position: vec3, @location(3) size: f32, @location(4) 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) texture_index: u32, @location(2) tint: vec2, } @group(0) @binding(0) var texture_array: binding_array>; @group(0) @binding(1) var sampler_array: binding_array; fn fmod(x: vec2, m: f32) -> vec2 { return x - floor(x / m) * m; } @vertex fn vertex_main( vertex: VertexInput, instance: InstanceInput, ) -> VertexOutput { var out: VertexOutput; 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 = ( vec2(global_data.camera_position_x, global_data.camera_position_y) - ( fmod( vec2(global_data.camera_position_x, global_data.camera_position_y) + global_data.starfield_tile_size / 2.0, global_data.starfield_tile_size ) - global_data.starfield_tile_size / 2.0 ) ); let zoom_min_times = ( global_data.camera_zoom / global_data.camera_zoom_min ); // Hide n% of the smallest stars // If we wanted a constant number of stars on the screen, we would do // `let hide_fraction = 1.0 - 1.0 / (zoom_min_times * zoom_min_times);` // We, however, don't want this: a bigger screen should have more stars, // but not *too* many. We thus scale linearly. let hide_fraction = 1.0 - 1.0 / (zoom_min_times * 0.8); if ( instance.size < ( hide_fraction * (global_data.starfield_size_max - global_data.starfield_size_min) + (global_data.starfield_size_min) ) ) { out.position = vec4(2.0, 2.0, 0.0, 1.0); return out; } // Apply sprite aspect ratio & scale factor // also applies screen aspect ratio // Note that we do NOT scale for distance here---this is intentional. var scale: f32 = instance.size / global_data.camera_zoom; // Minimum scale to prevent flicker at large zoom levels var real_size = scale * vec2(global_data.window_size_w, global_data.window_size_h); if (real_size.x < 2.0 || real_size.y < 2.0) { // Otherwise, clamp to a minimum scale scale = 2.0 / max(global_data.window_size_w, global_data.window_size_h); } // Divide by two, because viewport height is 2 in screen units // (coordinates go from -1 to 1) var pos: vec2 = vec2( vertex.position.x * (scale/2.0) / global_data.window_aspect, vertex.position.y * (scale/2.0) ); // World position relative to camera // (Note that instance position is in a different // coordinate system than usual) let camera_pos = ( (instance.position.xy + tile_center) - vec2(global_data.camera_position_x, global_data.camera_position_y) ); // Translate pos = pos + ( // Don't forget to correct distance for screen aspect ratio too! (camera_pos / (global_data.camera_zoom * instance.position.z)) / vec2(global_data.window_aspect, 1.0) ); out.position = vec4(pos, 0.0, 1.0) * instance.position.z; // Starfield sprites may not be animated let t = global_atlas[global_data.starfield_sprite]; out.texture_index = u32(t.atlas_texture); out.texture_coords = vec2(t.xpos, t.ypos); if vertex.texture_coords.x == 1.0 { out.texture_coords = vec2(out.texture_coords.x + t.width, out.texture_coords.y); } if vertex.texture_coords.y == 1.0 { out.texture_coords = vec2(out.texture_coords.x, out.texture_coords.y + t.height); } return out; } @fragment fn fragment_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[in.texture_index], sampler_array[0], in.texture_coords, 0.0 ).rgba * vec4( (c_top + (c_del * c)) * b, 1.0 ); }