#import bevy_vector_shapes::core
#import bevy_vector_shapes::core::{view, image, image_sampler}
#import bevy_vector_shapes::constants::{PI, TAU}
struct Vertex {
@builtin(instance_index) index: u32,
@location(0) pos: vec3<f32>
};
struct Shape {
@location(0) matrix_0: vec4<f32>,
@location(1) matrix_1: vec4<f32>,
@location(2) matrix_2: vec4<f32>,
@location(3) matrix_3: vec4<f32>,
@location(4) color: vec4<f32>,
@location(5) thickness: f32,
@location(6) flags: u32,
@location(7) start: vec3<f32>,
@location(8) end: vec3<f32>,
};
#ifdef PER_OBJECT_BUFFER_BATCH_SIZE
@group(1) @binding(0) var<uniform> shapes: array<Shape, #{PER_OBJECT_BUFFER_BATCH_SIZE}u>;
#else
@group(1) @binding(0) var<storage> shapes: array<Shape>;
#endif
struct VertexOutput {
@builtin(position) clip_position: vec4<f32>,
@location(0) color: vec4<f32>,
@location(1) uv: vec2<f32>,
@location(2) cap_ratio: f32,
#ifdef TEXTURED
@location(3) texture_uv: vec2<f32>,
#endif
};
@vertex
fn vertex(v: Vertex) -> VertexOutput {
var out: VertexOutput;
// Vertex positions for a basic quad
let vertex = v.pos;
let shape = shapes[v.index];
// Reconstruct our transformation matrix
let matrix = mat4x4<f32>(
shape.matrix_0,
shape.matrix_1,
shape.matrix_2,
shape.matrix_3
);
// Vector from start -> end
var line_vec = shape.end - shape.start;
// Center of line in world space
var center = line_vec / 2.0;
// Line length in local space
var line_length = length(line_vec);
// Get our start and end in world space
var world_start = (matrix * vec4<f32>(shape.start, 1.0)).xyz;
var world_end = (matrix * vec4<f32>(shape.end, 1.0)).xyz;
// The y basis is the normalized vector along the line
var y_basis = normalize(world_start - world_end);
// Choose which point we will work in reference to based on our y position
var origin = select(world_end, world_start, vertex.y < 0.0);
// Calculate the remainder of our basis vectors
var alignment = core::f_alignment(shape.flags) << 1u;
var basis_vectors = core::get_basis_vectors_from_up(matrix, origin, y_basis, alignment);
// Calculate thickness data
var thickness_type = core::f_thickness_type(shape.flags);
var thickness_data = core::get_thickness_data(shape.thickness, thickness_type, origin, basis_vectors[1]);
let scale = vec3<f32>(length(matrix[0].xyz), length(matrix[1].xyz), length(matrix[2].xyz));
// If our thickness in pixels is less than 1, clamp to 1 and reduce the alpha instead
var out_color = shape.color;
if thickness_data.thickness_p * max(scale.x, scale.y) < 1.0 {
out_color.a = out_color.a * thickness_data.thickness_p * max(scale.x, scale.y);
thickness_data.thickness_p = 1.;
}
// Calculate thickness and radius in world units
var thickness = thickness_data.thickness_p / thickness_data.pixels_per_u;
var radius = thickness / 2.0;
var cap_type = core::f_cap(shape.flags);
var cap_length = 0.0;
// If we have caps increase the cap length to our radius
if cap_type > 0u {
cap_length = radius;
}
// If our caps are round store the ratio of the length of our caps to the entire length of the line
if cap_type == 2u {
out.cap_ratio = thickness / (line_length + thickness);
}
// Calculate the vertex position with scaling
var local_pos = vertex.xy * vec2<f32>(radius, cap_length + line_length / 2.0) * scale.xy;
// Scale our padding to world space and match direction of our vertex
var aa_padding_u = core::AA_PADDING / thickness_data.pixels_per_u;
var aa_padding = sign(vertex.xy) * aa_padding_u;
// Pad our position and determine the ratio by which to scale uv such that uvs ignore the padding
var padded_pos = local_pos + aa_padding;
var uv_ratio = padded_pos / local_pos;
// Calculate the offset from our origin point
var local_offset = vertex.xy * (vec2<f32>(radius, cap_length) * scale.xy + aa_padding_u);
// Determine final world position by offsetting by the origin we chose and rotating by our basis vectors
var world_pos = origin + local_offset.x * basis_vectors[0] + local_offset.y * basis_vectors[1];
// Multiply the world space position by the view projection matrix to convert to our clip position
out.clip_position = view.view_proj * vec4<f32>(world_pos, 1.0);
out.uv = vertex.xy * uv_ratio;
out.color = out_color;
#ifdef TEXTURED
out.texture_uv = core::get_texture_uv(vertex.xy);
#endif
return out;
}
struct FragmentInput {
@location(0) color: vec4<f32>,
@location(1) uv: vec2<f32>,
@location(2) cap_ratio: f32,
#ifdef TEXTURED
@location(3) texture_uv: vec2<f32>,
#endif
};
// Due to https://github.com/gfx-rs/naga/issues/1743 this cannot be compiled into the vertex shader on web
#ifdef FRAGMENT
@fragment
fn fragment(f: FragmentInput) -> @location(0) vec4<f32> {
var in_shape = f.color.a;
// If we have rounded caps mask them
if f.cap_ratio > 0.0 {
// Lines are symmetrical across both axis so we can mirror our point
// onto the positive x and y axis by taking the absolute value
var pos = abs(f.uv);
// Our x value already represents the x distance to our line so we now must transform our y value
// We want y = 0 to represent being within the body of the line, and 1 to be at the tip of our cap
// Calculate the -y distance to the end of the quad in caps
// The end of the quad is y = 1 so subtract to get the distance and then scale by cap length
var to_end_cap = (pos.y - 1.) / f.cap_ratio;
// We want y = 0 when the amount of caps until the end of the quad is 1
// and y = 1 when the number of quads is 0 so take 1 + to_end_cap
// If the total is negative we are within the line so clamp to > 0
pos.y = max(0., 1. + to_end_cap);
// We now have the shortest vector from our point to the line so take the distance
var dist = length(pos);
// Mask out corners
in_shape = min(in_shape, core::step_aa(dist, 1.));
} else {
// Simple rectangle sdf for no caps or square caps
in_shape = min(in_shape, core::step_aa(abs(f.uv.x), 1.) * core::step_aa(abs(f.uv.y), 1.0));
}
var color = core::color_output(vec4<f32>(f.color.rgb, in_shape));
#ifdef TEXTURED
color = color * textureSample(image, image_sampler, f.texture_uv);
#endif
// Discard fragments no longer in the shape
if in_shape < 0.0001 {
discard;
}
return color;
}
#endif