Skip to content

Commit

Permalink
Line strips are no longer a disconnected series of quads (#8065)
Browse files Browse the repository at this point in the history
### What

* Fixes #829
* Fixes #7191

Fixes a long standing issue that the segments of a line have small gaps
between them.

Other than originally planned, the solution employed here is entirely
based on fragment shader discard + elongation of the line. This can
cause some slight artifacts for 3D lines but it's quite hard to spot
those issues. Otherwise the biggest drawback of this is that we have
more overdraw which gonna be tricky to deal with once we add
transparency. On the flip side this is fairly simple and can re-use some
of the round cap handling code - round caps no longer use the "trailing
triangle" we had for every strip internally, and instead use the quad
extension, actually leading to better looking round caps.

⚠️ To allow loops to "just work", all line segments & line strips start
and end now with extended round caps. I.e. if your line has a radius of
1 and goes from (0,0,0) to (100,0,0), then what's drawn goes from
(-1,0,0) to (101,0,0). Before we didn't have caps on this, so we
essentially drew a square starting at (0,0,0) whereas now it's more
capsule like (it does not exactly behave like a capsule under
perspective though).


Line loops on map view:
Before:
<img width="383" alt="image"
src="https://github.com/user-attachments/assets/6d19ef7b-ea8e-422d-8ad9-6eb18f567340">

After:
<img width="303" alt="image"
src="https://github.com/user-attachments/assets/81ffb6a7-3216-4c96-84fd-3f4d9268300c">


RRT Star:
Before:
<img width="300" alt="image"
src="https://github.com/user-attachments/assets/a72352e7-8a73-4507-9f3a-5b4c713f7fc0">

After:
<img width="300" alt="image"
src="https://github.com/user-attachments/assets/03040348-f275-49c5-ad89-36fafc9fc1e9">


3D line snippet snippet:
Before:
<img width="420" alt="image"
src="https://github.com/user-attachments/assets/000dfef6-5d03-45e6-95bf-03a5033cc7a3">

After:
<img width="420" alt="image"
src="https://github.com/user-attachments/assets/1e511ee3-6464-4d30-aa30-34dce358bd85">


Failure Example in #7191
Before - confusingly broken:
<img width="1000" alt="image"
src="https://github.com/user-attachments/assets/261fe4dd-1d21-46e9-8139-6444af4e8cdf">


After - pretty bad rendering performance due to quite massive overdraw
of alpha tested geometry, but no longer broken.
<img width="1000" alt="image"
src="https://github.com/user-attachments/assets/1bbe8b0b-c90b-4164-bf14-6b2de1c6884b">

(After: setting the radius to something large, but sane)
<img width="1000" alt="image"
src="https://github.com/user-attachments/assets/5f02e388-80cf-472b-b99d-8f45dfb2e9f2">


re_renderer 2D demo. Left before, right after. Note that "shaded lines"
look a lot better now as a sideffect, with shading of arrows slightly
broken. Afaik we don't use this anywhere in Rerun today though.
However, rounded caps are higher quality now in general.

<img width="999" alt="Screenshot 2024-11-11 at 11 10 29"
src="https://github.com/user-attachments/assets/9404e2b1-218a-41c4-bc62-e711bdbcfe63">

----

Things that were several segments before can now be strips. No visual
change there:

Rectangles:
<img width="364" alt="image"
src="https://github.com/user-attachments/assets/85ef5d5d-da82-4e70-a828-54e802fdd447">

Frustum:
<img width="873" alt="image"
src="https://github.com/user-attachments/assets/a58ec4d6-9500-40b0-9023-61b077a63920">

Box wireframes:

This fixes a regression! In 0.19 those boxes had disconnected lines, in
0.18 this still worked since they were separate segments with rounded
corners.

Before (regression):
<img width="233" alt="image"
src="https://github.com/user-attachments/assets/b2a2aade-7271-46cc-a0ff-aa82031638e4">

After:
<img width="233" alt="image"
src="https://github.com/user-attachments/assets/727df4ea-9b63-43b1-ab48-64afdf508567">

----

Due to aforementioned always-round-caps, a list of segments can now look
indistinguishable from a single strip:

<img width="438" alt="image"
src="https://github.com/user-attachments/assets/ec0ca8da-201a-4d80-8e3e-c512c9fdb23b">


----

Under more extreme circumstances it is still (!) possible to have boxes
exhibit some artifacts:

<img width="400" alt="image"
src="https://github.com/user-attachments/assets/fee28464-f0f8-4b5c-9cbe-2e049df92153">

This is due to the quasi-capsule nature of our line segments.
Calculating a world position from ray-capsule intersection would yield
the correct result. What's actually happening right now is that we
interpolate the world position over the spanned quad (which is a lot
cheaper).

### Checklist
* [x] I have read and agree to [Contributor
Guide](https://github.com/rerun-io/rerun/blob/main/CONTRIBUTING.md) and
the [Code of
Conduct](https://github.com/rerun-io/rerun/blob/main/CODE_OF_CONDUCT.md)
* [x] I've included a screenshot or gif (if applicable)
* [x] I have tested the web demo (if applicable):
* Using examples from latest `main` build:
[rerun.io/viewer](https://rerun.io/viewer/pr/8065?manifest_url=https://app.rerun.io/version/main/examples_manifest.json)
* Using full set of examples from `nightly` build:
[rerun.io/viewer](https://rerun.io/viewer/pr/8065?manifest_url=https://app.rerun.io/version/nightly/examples_manifest.json)
* [x] The PR title and labels are set such as to maximize their
usefulness for the next release's CHANGELOG
* [x] If applicable, add a new check to the [release
checklist](https://github.com/rerun-io/rerun/blob/main/tests/python/release_checklist)!
* [x] If have noted any breaking changes to the log API in
`CHANGELOG.md` and the migration guide

- [PR Build Summary](https://build.rerun.io/pr/8065)
- [Recent benchmark results](https://build.rerun.io/graphs/crates.html)
- [Wasm size tracking](https://build.rerun.io/graphs/sizes.html)

To run all checks from `main`, comment on the PR with `@rerun-bot
full-check`.

### Testing
* [x] Tested on WebGL
* [x] Tested on WebGPU
  • Loading branch information
Wumpf authored Nov 12, 2024
1 parent 7b3135a commit 03cb015
Show file tree
Hide file tree
Showing 12 changed files with 217 additions and 175 deletions.
9 changes: 8 additions & 1 deletion crates/viewer/re_renderer/shader/composite.wgsl
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ struct CompositeUniformBuffer {
outline_color_layer_a: vec4f,
outline_color_layer_b: vec4f,
outline_radius_pixel: f32,
blend_with_background: u32,
};
@group(1) @binding(0)
var<uniform> uniforms: CompositeUniformBuffer;
Expand All @@ -27,11 +28,17 @@ fn main(in: FragmentInput) -> @location(0) vec4f {
// but are about the location of the texel in the target texture.
var color = textureSample(color_texture, nearest_sampler, in.texcoord);


// TODO(andreas): We assume that the color from the texture does *not* have pre-multiplied alpha.
// This is a brittle workaround for the alpha-to-coverage issue described in `ViewBuilder::MAIN_TARGET_ALPHA_TO_COVERAGE_COLOR_STATE`:
// We need this because otherwise the feathered edges of alpha-to-coverage would be overly bright, as after
// MSAA-resolve they end up with an unusually low alpha value relative to the color value.
color = vec4f(color.rgb * color.a, color.a);
if uniforms.blend_with_background == 0 {
// To not apply this hack needlessly and account for alpha from alpha to coverage, we have to ignore alpha values if blending is disabled.
color = vec4f(color.rgb, 1.0);
} else {
color = vec4f(color.rgb * color.a, color.a);
}

// Outlines
{
Expand Down
117 changes: 75 additions & 42 deletions crates/viewer/re_renderer/shader/lines.wgsl
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,9 @@ const FLAG_CAP_START_EXTEND_OUTWARDS: u32 = 32u;
const FLAG_COLOR_GRADIENT: u32 = 64u;
const FLAG_FORCE_ORTHO_SPANNING: u32 = 128u;

// Special flags used in the fragment shader.
const FLAG_CAP_TRIANGLE: u32 = FLAG_CAP_START_TRIANGLE | FLAG_CAP_END_TRIANGLE;

// A lot of the attributes don't need to be interpolated across triangles.
// To document that and safe some time we mark them up with @interpolate(flat)
// (see https://www.w3.org/TR/WGSL/#interpolation)
Expand All @@ -60,14 +63,14 @@ struct VertexOut {
@location(1) @interpolate(perspective)
position_world: vec3f,

@location(2) @interpolate(perspective)
center_position: vec3f,
@location(2) @interpolate(flat)
rounded_inner_line_begin: vec3f,

@location(3) @interpolate(flat)
active_radius: f32,
rounded_inner_line_end: vec3f,

@location(4) @interpolate(perspective)
round_cap_circle_center: vec3f,
@location(4) @interpolate(flat)
rounded_inner_line_radius: f32,

@location(5) @interpolate(flat)
fragment_flags: u32,
Expand Down Expand Up @@ -166,6 +169,8 @@ fn vs_main(@builtin(vertex_index) vertex_idx: u32) -> VertexOut {

// If the strip indices don't match up for start/end, then we're in a cap triangle!
let is_cap_triangle = pos_data_quad_begin.strip_index != pos_data_quad_end.strip_index;
let is_first_quad_after_cap = !is_cap_triangle && (pos_data_quad_begin.strip_index != pos_data_quad_before.strip_index);
let is_last_quad_before_cap = !is_cap_triangle && (pos_data_quad_end.strip_index != pos_data_quad_after.strip_index);

// Let's determine which one of the two position data is closer to our vertex.
// Which tells us things:
Expand All @@ -189,14 +194,14 @@ fn vs_main(@builtin(vertex_index) vertex_idx: u32) -> VertexOut {

// Compute quad_dir & correct center_position for triangle caps.
var quad_dir: vec3f;
var is_at_pointy_end = false;
let is_end_cap_triangle = is_cap_triangle && is_right_triangle && has_any_flag(strip_data.flags, FLAG_CAP_END_TRIANGLE | FLAG_CAP_END_ROUND);
let is_start_cap_triangle = is_cap_triangle && !is_right_triangle && has_any_flag(strip_data.flags, FLAG_CAP_START_TRIANGLE | FLAG_CAP_START_ROUND);
var is_at_pointy_arrow_end = false;
let is_end_cap_triangle = is_cap_triangle && is_right_triangle && has_any_flag(strip_data.flags, FLAG_CAP_END_TRIANGLE);
let is_start_cap_triangle = is_cap_triangle && !is_right_triangle && has_any_flag(strip_data.flags, FLAG_CAP_START_TRIANGLE);
if is_end_cap_triangle {
is_at_pointy_end = is_at_quad_end;
is_at_pointy_arrow_end = is_at_quad_end;
quad_dir = pos_data_quad_begin.pos - pos_data_quad_before.pos; // Go one pos data back.
} else if is_start_cap_triangle {
is_at_pointy_end = !is_at_quad_end;
is_at_pointy_arrow_end = !is_at_quad_end;
quad_dir = pos_data_quad_after.pos - pos_data_quad_end.pos; // Go one pos data forward.
} else if is_cap_triangle {
// Discard vertex.
Expand Down Expand Up @@ -226,19 +231,27 @@ fn vs_main(@builtin(vertex_index) vertex_idx: u32) -> VertexOut {
triangle_cap_length *= triangle_cap_size_factor;

// Make space for the end cap if this is either the cap itself or the cap follows right after/before this quad.
if !has_any_flag(strip_data.flags, FLAG_CAP_END_EXTEND_OUTWARDS) &&
(is_end_cap_triangle || (is_at_quad_end && pos_data_current.strip_index != pos_data_quad_after.strip_index)) {
var cap_length =
f32(has_any_flag(strip_data.flags, FLAG_CAP_END_ROUND)) * strip_radius +
f32(has_any_flag(strip_data.flags, FLAG_CAP_END_TRIANGLE)) * triangle_cap_length;
center_position -= quad_dir * cap_length;
}
if !has_any_flag(strip_data.flags, FLAG_CAP_START_EXTEND_OUTWARDS) &&
(is_start_cap_triangle || (!is_at_quad_end && pos_data_current.strip_index != pos_data_quad_before.strip_index)) {
var rounded_inner_line_begin = pos_data_quad_begin.pos;
if !has_any_flag(strip_data.flags, FLAG_CAP_START_EXTEND_OUTWARDS) && (is_start_cap_triangle || is_first_quad_after_cap) {
var cap_length =
f32(has_any_flag(strip_data.flags, FLAG_CAP_START_ROUND)) * strip_radius +
f32(has_any_flag(strip_data.flags, FLAG_CAP_START_TRIANGLE)) * triangle_cap_length;
center_position += quad_dir * cap_length;
let offset = quad_dir * cap_length;
rounded_inner_line_begin += offset;
if !is_at_quad_end || is_start_cap_triangle {
center_position += offset;
}
}
var rounded_inner_line_end = pos_data_quad_end.pos;
if !has_any_flag(strip_data.flags, FLAG_CAP_END_EXTEND_OUTWARDS) && (is_end_cap_triangle || is_last_quad_before_cap) {
var cap_length =
f32(has_any_flag(strip_data.flags, FLAG_CAP_END_ROUND)) * strip_radius +
f32(has_any_flag(strip_data.flags, FLAG_CAP_END_TRIANGLE)) * triangle_cap_length;
let offset = quad_dir * cap_length;
rounded_inner_line_end -= offset;
if is_at_quad_end || is_end_cap_triangle {
center_position -= offset;
}
}

// Boost radius only now that we subtracted/added the cap length.
Expand All @@ -253,57 +266,78 @@ fn vs_main(@builtin(vertex_index) vertex_idx: u32) -> VertexOut {
center_position += quad_dir * (size_boost * select(-1.0, 1.0, is_at_quad_end));
}

var active_radius = strip_radius;
// Filtered list of flags that the fragment shader is interested in.
var fragment_flags = strip_data.flags & FLAG_COLOR_GRADIENT;

// If this is a triangle cap, we blow up our ("virtual") quad by a given factor.
if (is_end_cap_triangle && has_any_flag(strip_data.flags, FLAG_CAP_END_TRIANGLE)) ||
(is_start_cap_triangle && has_any_flag(strip_data.flags, FLAG_CAP_START_TRIANGLE)) {
active_radius *= batch.triangle_cap_width_factor * triangle_cap_size_factor;
strip_radius *= batch.triangle_cap_width_factor * triangle_cap_size_factor;
fragment_flags |= FLAG_CAP_TRIANGLE;
}

// Span up the vertex away from the line's axis, orthogonal to the direction to the camera
let dir_up = normalize(cross(camera_ray.direction, quad_dir));

let round_cap_circle_center = center_position;

var pos: vec3f;
if is_cap_triangle && is_at_pointy_end {
// We extend the cap triangle far enough to handle triangle caps,
// and far enough to do rounded caps without any visible clipping.
// There is _some_ clipping, but we can't see it ;)
// If we want to do it properly, we would extend the radius for rounded caps too.
if is_cap_triangle && is_at_pointy_arrow_end {
// We extend the cap triangle far enough to handle triangle caps.
center_position += quad_dir * (triangle_cap_length * select(-1.0, 1.0, is_right_triangle));
pos = center_position;
} else {
pos = center_position + (active_radius * top_bottom) * dir_up;
pos = center_position + (strip_radius * top_bottom * 0.99) * dir_up;
}

// Extend the line for rendering smooth joints, as well as round start/end caps.
let is_at_inner_joint = !is_cap_triangle && !is_first_quad_after_cap && !is_last_quad_before_cap;
let is_at_quad_with_round_capped_start = !is_at_quad_end && is_first_quad_after_cap && has_any_flag(strip_data.flags, FLAG_CAP_START_ROUND);
let is_at_quad_with_round_capped_end = is_at_quad_end && is_last_quad_before_cap && has_any_flag(strip_data.flags, FLAG_CAP_END_ROUND);
if is_at_inner_joint || is_at_quad_with_round_capped_start || is_at_quad_with_round_capped_end {
let left_right_offset = quad_dir * strip_radius * select(-1.0, 1.0, is_at_quad_end);
pos += left_right_offset;
}

// Output, transform to projection space and done.
var out: VertexOut;
out.position = apply_depth_offset(frame.projection_from_world * vec4f(pos, 1.0), batch.depth_offset);
out.position_world = pos;
out.center_position = center_position;
out.round_cap_circle_center = round_cap_circle_center;
out.color = strip_data.color;
out.active_radius = active_radius;
out.fragment_flags = strip_data.flags &
(FLAG_COLOR_GRADIENT | (u32(is_cap_triangle) * select(FLAG_CAP_START_ROUND, FLAG_CAP_END_ROUND, is_right_triangle)));
out.rounded_inner_line_begin = rounded_inner_line_begin;
out.rounded_inner_line_end = rounded_inner_line_end;
out.rounded_inner_line_radius = strip_radius;
out.fragment_flags = fragment_flags;
out.picking_instance_id = strip_data.picking_instance_id;

return out;
}

fn distance_to_line_sq(pos: vec3f, line_a: vec3f, line_b: vec3f) -> f32 {
let a_to_pos = pos - line_a;
let a_to_b = line_b - line_a;
let h = saturate(dot(a_to_pos, a_to_b) / dot(a_to_b, a_to_b));
let to_line = a_to_pos - a_to_b * h;
return dot(to_line, to_line);
}

fn distance_to_line(pos: vec3f, line_a: vec3f, line_b: vec3f) -> f32 {
return sqrt(distance_to_line_sq(pos, line_a, line_b));
}

fn compute_coverage(in: VertexOut) -> f32 {
var coverage = 1.0;
if has_any_flag(in.fragment_flags, FLAG_CAP_START_ROUND | FLAG_CAP_END_ROUND) {
let distance_to_skeleton = length(in.position_world - in.round_cap_circle_center);

if !has_any_flag(in.fragment_flags, FLAG_CAP_TRIANGLE) {
let distance_to_skeleton = distance_to_line(in.position_world, in.rounded_inner_line_begin, in.rounded_inner_line_end);
let pixel_world_size = approx_pixel_world_size_at(length(in.position_world - frame.camera_position));

// It's important that we do antialias both inwards and outwards of the exact border.
// If we do only outwards, rectangle outlines won't line up nicely
let half_pixel_world_size = pixel_world_size * 0.5;
let signed_distance_to_border = distance_to_skeleton - in.active_radius;
let signed_distance_to_border = distance_to_skeleton - in.rounded_inner_line_radius;
coverage = 1.0 - saturate((signed_distance_to_border + half_pixel_world_size) / pixel_world_size);
}

// Debugging hack: An offset here makes the geometry visible!
//return coverage + 0.1;
return coverage;
}

Expand All @@ -317,9 +351,8 @@ fn fs_main(in: VertexOut) -> @location(0) vec4f {
// TODO(andreas): lighting setup
var shading = 1.0;
if has_any_flag(in.fragment_flags, FLAG_COLOR_GRADIENT) {
let to_center = in.position_world - in.center_position;
let relative_distance_to_center_sq = dot(to_center, to_center) / (in.active_radius * in.active_radius);
shading = max(0.2, 1.0 - relative_distance_to_center_sq) * 0.9;
let distance_to_inner = distance_to_line_sq(in.position_world, in.rounded_inner_line_begin, in.rounded_inner_line_end);
shading = max(0.2, 1.0 - distance_to_inner / (in.rounded_inner_line_radius * in.rounded_inner_line_radius)) * 0.9;
}

return vec4f(in.color.rgb * shading, coverage);
Expand Down
115 changes: 52 additions & 63 deletions crates/viewer/re_renderer/src/line_drawable_builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -89,10 +89,7 @@ impl<'ctx> LineDrawableBuilder<'ctx> {
}

pub fn default_box_flags() -> LineStripFlags {
LineStripFlags::FLAG_CAP_END_ROUND
| LineStripFlags::FLAG_CAP_START_ROUND
| LineStripFlags::FLAG_CAP_END_EXTEND_OUTWARDS
| LineStripFlags::FLAG_CAP_START_EXTEND_OUTWARDS
LineStripFlags::FLAGS_OUTWARD_EXTENDING_ROUND_CAPS
}
}

Expand Down Expand Up @@ -307,7 +304,6 @@ impl<'a, 'ctx> LineBatchBuilder<'a, 'ctx> {

/// Add box outlines from a unit cube transformed by `transform`.
///
/// Internally adds 12 line segments with rounded line heads.
/// Disables color gradient since we don't support gradients in this setup yet (i.e. enabling them does not look good)
#[inline]
pub fn add_box_outline_from_transform(
Expand All @@ -324,36 +320,15 @@ impl<'a, 'ctx> LineBatchBuilder<'a, 'ctx> {
transform.transform_point3(glam::vec3(0.5, 0.5, -0.5)),
transform.transform_point3(glam::vec3(0.5, 0.5, 0.5)),
];
self.add_segments(
[
// bottom:
(corners[0b000], corners[0b001]),
(corners[0b000], corners[0b010]),
(corners[0b011], corners[0b001]),
(corners[0b011], corners[0b010]),
// top:
(corners[0b100], corners[0b101]),
(corners[0b100], corners[0b110]),
(corners[0b111], corners[0b101]),
(corners[0b111], corners[0b110]),
// sides:
(corners[0b000], corners[0b100]),
(corners[0b001], corners[0b101]),
(corners[0b010], corners[0b110]),
(corners[0b011], corners[0b111]),
]
.into_iter(),
)
.flags(LineDrawableBuilder::default_box_flags())
self.add_box_from_corners(corners)
}

/// Add box outlines.
///
/// Internally adds 12 line segments with rounded line heads.
/// Internally a single closed line strip.
/// Disables color gradient since we don't support gradients in this setup yet (i.e. enabling them does not look good)
///
/// Returns None for empty and non-finite boxes.
#[inline]
pub fn add_box_outline(
&mut self,
bbox: &re_math::BoundingBox,
Expand All @@ -362,35 +337,54 @@ impl<'a, 'ctx> LineBatchBuilder<'a, 'ctx> {
return None;
}

let corners = bbox.corners();
Some(
self.add_segments(
[
// bottom:
(corners[0b000], corners[0b001]),
(corners[0b000], corners[0b010]),
(corners[0b011], corners[0b001]),
(corners[0b011], corners[0b010]),
// top:
(corners[0b100], corners[0b101]),
(corners[0b100], corners[0b110]),
(corners[0b111], corners[0b101]),
(corners[0b111], corners[0b110]),
// sides:
(corners[0b000], corners[0b100]),
(corners[0b001], corners[0b101]),
(corners[0b010], corners[0b110]),
(corners[0b011], corners[0b111]),
]
.into_iter(),
)
.flags(LineDrawableBuilder::default_box_flags()),
Some(self.add_box_from_corners(bbox.corners()))
}

fn add_box_from_corners(&mut self, corners: [glam::Vec3; 8]) -> LineStripBuilder<'_, 'ctx> {
let mut strip_index = self.0.strips_buffer.len() as u32;

// Bottom plus connection to top.
self.add_vertices(
[
// bottom loop
corners[0b000],
corners[0b001],
corners[0b011],
corners[0b010],
corners[0b000],
// joined to top loop
corners[0b100],
corners[0b101],
corners[0b111],
corners[0b110],
corners[0b100],
]
.into_iter(),
strip_index,
)
.ok_or_log_error_once();
strip_index += 1;

// remaining side edges.
for line in [
[corners[0b001], corners[0b101]],
[corners[0b010], corners[0b110]],
[corners[0b011], corners[0b111]],
] {
self.add_vertices(line.into_iter(), strip_index)
.ok_or_log_error_once();
strip_index += 1;
}

let num_strips_added = 4;
let num_vertices_added = 10 + 3 * 2;
self.create_strip_builder(num_strips_added, num_vertices_added)
.flags(LineDrawableBuilder::default_box_flags())
}

/// Add rectangle outlines.
///
/// Internally adds 4 line segments with rounded line heads.
/// Internally adds a single linestrip with 5 vertices.
/// Disables color gradient since we don't support gradients in this setup yet (i.e. enabling them does not look good)
#[inline]
pub fn add_rectangle_outline(
Expand All @@ -399,18 +393,13 @@ impl<'a, 'ctx> LineBatchBuilder<'a, 'ctx> {
extent_u: glam::Vec3,
extent_v: glam::Vec3,
) -> LineStripBuilder<'_, 'ctx> {
self.add_segments(
self.add_strip(
[
(top_left_corner, top_left_corner + extent_u),
(
top_left_corner + extent_u,
top_left_corner + extent_u + extent_v,
),
(
top_left_corner + extent_u + extent_v,
top_left_corner + extent_v,
),
(top_left_corner + extent_v, top_left_corner),
top_left_corner,
top_left_corner + extent_u,
top_left_corner + extent_u + extent_v,
top_left_corner + extent_v,
top_left_corner,
]
.into_iter(),
)
Expand Down
Loading

0 comments on commit 03cb015

Please sign in to comment.