From 760f947c00e188a334ef5357b4b3fb0a1b545e5a Mon Sep 17 00:00:00 2001 From: veclav talica Date: Sat, 10 Feb 2024 22:38:35 +0500 Subject: [PATCH] push .static contents --- .gitattributes | 2 + .gitignore | 5 +- .../2d-visibility/.static/Visibility2D.gd.txt | 178 +++++++++++ articles/2d-visibility/.static/example.gif | 3 + .../hand-opt-simplex-2d/.static/noise.png | 3 + .../.static/incremental-delaunay.zig.txt | 286 ++++++++++++++++++ articles/incremental-delaunay/.static/web.png | 3 + articles/vector-pi-rotation/.static/noise.png | 3 + 8 files changed, 482 insertions(+), 1 deletion(-) create mode 100644 .gitattributes create mode 100644 articles/2d-visibility/.static/Visibility2D.gd.txt create mode 100644 articles/2d-visibility/.static/example.gif create mode 100644 articles/hand-opt-simplex-2d/.static/noise.png create mode 100644 articles/incremental-delaunay/.static/incremental-delaunay.zig.txt create mode 100644 articles/incremental-delaunay/.static/web.png create mode 100644 articles/vector-pi-rotation/.static/noise.png diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..4a9d8e8 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +*.png filter=lfs diff=lfs merge=lfs -text +*.gif filter=lfs diff=lfs merge=lfs -text diff --git a/.gitignore b/.gitignore index bec3509..472fdf2 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,9 @@ **/__pycache__/* html/ -**/.*/ +./.*/ +[articles/**/.static/] +articles/**/.dynamic/ +articles/**/.temp/ **/*.jpg/ **/*.png/ **/*.upload-checksum diff --git a/articles/2d-visibility/.static/Visibility2D.gd.txt b/articles/2d-visibility/.static/Visibility2D.gd.txt new file mode 100644 index 0000000..0c3b945 --- /dev/null +++ b/articles/2d-visibility/.static/Visibility2D.gd.txt @@ -0,0 +1,178 @@ +extends Node +class_name Visibility2D + +# Based on: https://www.redblobgames.com/articles/visibility/Visibility.hx + +# Limitations: +# - Segments cant intersect each other, splitting is required for such cases. + +# todo: Make it extend plain object, handle lifetime manually. +class EndPoint: + var point: Vector2 + var begin: bool + var segment: int + var angle: float + + static func sort(p_a: EndPoint, p_b: EndPoint) -> bool: + if p_a.angle > p_b.angle: return true + elif p_a.angle < p_b.angle: return false + elif not p_a.begin and p_b.begin: return true + else: return false + +var _endpoints: Array # of EndPoint +var _sorted_endpoints: Array # of EndPoint +var _open: PoolIntArray # of Segment indices + +var center: Vector2 +var output: PoolVector2Array + +# todo: Ability to cache builder state for static geometry. +class Builder: + var target + + func view_point(p_point: Vector2) -> Builder: + target.center = p_point + return self + + # todo: Use it to cull out endpoints out of working region. + func bounds(p_area: Rect2) -> Builder: + target._add_segment(p_area.position, Vector2(p_area.end.x, p_area.position.y)) + target._add_segment(Vector2(p_area.end.x, p_area.position.y), p_area.end) + target._add_segment(p_area.end, Vector2(p_area.position.x, p_area.end.y)) + target._add_segment(Vector2(p_area.position.x, p_area.end.y), p_area.position) + return self + + func line(p_line: Line2D) -> Builder: + for i in range(0, p_line.points.size() - 1): + target._add_segment(p_line.position + p_line.points[i], + p_line.position + p_line.points[i + 1]) + return self + + func polygon(p_polygon: Polygon2D) -> Builder: + var points := p_polygon.polygon + for i in range(0, points.size() - 1): + target._add_segment(p_polygon.position + points[i], + p_polygon.position + points[i + 1]) + target._add_segment(p_polygon.position + points[points.size() - 1], + p_polygon.position + points[0]) + return self + + func occluder(p_object: Object) -> Builder: + if p_object is Line2D: + return line(p_object) + elif p_object is Polygon2D: + return polygon(p_object) + else: + push_error("Unknown occluder type") + return self + + func finalize(): + target._finalize() + +func _add_segment(p_point0: Vector2, p_point1: Vector2): + var point0 := EndPoint.new() + var point1 := EndPoint.new() + point0.segment = _endpoints.size() + point1.segment = _endpoints.size() + point0.point = p_point0 + point1.point = p_point1 + _endpoints.append(point0) + _endpoints.append(point1) + +func init_builder() -> Builder: + # todo: Reuse + _endpoints.resize(0) + var result := Builder.new() + result.target = self + return result + +func _finalize(): + # todo: Only needs to be done when endpoints or center is changed. + for segment in range(0, _endpoints.size(), 2): + var p1 := _endpoints[segment] as EndPoint + var p2 := _endpoints[segment + 1] as EndPoint + p1.angle = (p1.point - center).angle() + p2.angle = (p2.point - center).angle() + # todo: Simplify to one expression. + var da := p2.angle - p1.angle + if da <= PI: da += TAU + if da > PI: da -= TAU + p1.begin = da > 0.0 + p2.begin = not p1.begin + +func _is_segment_in_front(p_segment1: int, p_segment2: int) -> bool: + var s1p1 := _endpoints[p_segment1].point as Vector2 + var s1p2 := _endpoints[p_segment1 + 1].point as Vector2 + var s2p1 := _endpoints[p_segment2].point as Vector2 + var s2p2 := _endpoints[p_segment2 + 1].point as Vector2 + + # todo: Can we use something simpler than interpolation? + var d := s1p2 - s1p1 + var p := s2p1.linear_interpolate(s2p2, 0.01) + var a1 := (d.x * (p.y - s1p1.y) \ + - d.y * (p.x - s1p1.x)) < 0.0 + p = s2p2.linear_interpolate(s2p1, 0.01) + var a2 := (d.x * (p.y - s1p1.y) \ + - d.y * (p.x - s1p1.x)) < 0.0 + var a3 := (d.x * (center.y - s1p1.y) \ + - d.y * (center.x - s1p1.x)) < 0.0 + + if a1 == a2 and a2 == a3: return true + + d = s2p2 - s2p1 + p = s1p1.linear_interpolate(s1p2, 0.01) + var b1 := (d.x * (p.y - s2p1.y) \ + - d.y * (p.x - s2p1.x)) < 0.0 + p = s1p2.linear_interpolate(s1p1, 0.01) + var b2 := (d.x * (p.y - s2p1.y) \ + - d.y * (p.x - s2p1.x)) < 0.0 + var b3 := (d.x * (center.y - s2p1.y) \ + - d.y * (center.x - s2p1.x)) < 0.0 + + return b1 == b2 and b2 != b3 + +func sweep() -> PoolVector2Array: + output.resize(0) + # todo: Only duplicate and sort on change. + _sorted_endpoints = _endpoints.duplicate() + _sorted_endpoints.sort_custom(EndPoint, "sort") + + var start_angle := 0.0 + + # todo: Inline passes. + for n_pass in range(2): + for p_idx in range(_sorted_endpoints.size() - 1, -1, -1): + var p := _sorted_endpoints[p_idx] as EndPoint + var old := -1 if _open.empty() else _open[0] + + if p.begin: + var idx := 0 + while idx < _open.size() and _is_segment_in_front(p.segment, _open[idx]): + idx += 1 + # warning-ignore:return_value_discarded + _open.insert(idx, p.segment) + else: + var idx := _open.rfind(p.segment) + if idx != -1: _open.remove(idx) + # todo: Second pass can assume that it will be found. + # _open.remove(_open.rfind(p.segment)) + + if old != (-1 if _open.empty() else _open[0]): + if n_pass == 1: + # todo: Distance should be configurable. + var p3 := _endpoints[old].point as Vector2 if old != -1 else \ + center + Vector2(cos(start_angle), sin(start_angle)) * 500.0 + var t2 := Vector2(cos(p.angle), sin(p.angle)) + var p4 := p3.direction_to(_endpoints[old + 1].point) if old != -1 else t2 + + var l = Geometry.line_intersects_line_2d(p3, p4, center, + Vector2(cos(start_angle), sin(start_angle))) + if l != null: output.append(l) + l = Geometry.line_intersects_line_2d(p3, p4, center, t2) + if l != null: output.append(l) + + start_angle = p.angle + + _open.resize(0) + + return output diff --git a/articles/2d-visibility/.static/example.gif b/articles/2d-visibility/.static/example.gif new file mode 100644 index 0000000..fd5567e --- /dev/null +++ b/articles/2d-visibility/.static/example.gif @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:65366a427e891c5c3ac823fb038cce9224ce053526fc4d86259610a6cceecd9e +size 35745 diff --git a/articles/hand-opt-simplex-2d/.static/noise.png b/articles/hand-opt-simplex-2d/.static/noise.png new file mode 100644 index 0000000..e4d7f51 --- /dev/null +++ b/articles/hand-opt-simplex-2d/.static/noise.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1fbf413e324a1c979bccb5523ecac5d4a7775cc15ffadcea480b1172335c21e6 +size 777761 diff --git a/articles/incremental-delaunay/.static/incremental-delaunay.zig.txt b/articles/incremental-delaunay/.static/incremental-delaunay.zig.txt new file mode 100644 index 0000000..d30bc6f --- /dev/null +++ b/articles/incremental-delaunay/.static/incremental-delaunay.zig.txt @@ -0,0 +1,286 @@ +//! Based on: https://www.cs.umd.edu/class/spring2020/cmsc754/Lects/lect13-delaun-alg.pdf +//! Optimizations involved: +//! - Cached neighbors for traversal. +//! - Minimal memory footprint. +//! - Cached circumferences. +//! - No circumference calculations for new subdivisions, - circumferences of neighbors are used instead. +//! - Lazy circumference calculation, as some places might not be neighboring new subdivisions. +//! - Extensive use of vectorization. +//! - Care given to linear access of memory. + +// todo: This method allows zero area triangles, we need to eliminate them. +// Points that lie on edges can be detected in pointRelation function by == 0 comparison. + +const std = @import("std"); + +// Could be redefined as pleased, but i consider these to be most sensical for given implementation. +pub const VertexComponent = f32; +pub const Vertex = @Vector(2, VertexComponent); +pub const Index = u15; +pub const Area = GenericArea(VertexComponent); + +pub const Builder = struct { + triangles: std.ArrayList(Triangle), + vertices: std.ArrayList(Vertex), + allocator: std.mem.Allocator, + + // todo: init with expected amount of points to preallocate beforehand. + pub fn init(allocator: std.mem.Allocator, area: Area) !@This() { + var triangles = try std.ArrayList(Triangle).initCapacity(allocator, 2); + errdefer triangles.deinit(); + var vertices = try std.ArrayList(Vertex).initCapacity(allocator, 4); + errdefer vertices.deinit(); + + try vertices.ensureUnusedCapacity(4); + try triangles.ensureUnusedCapacity(2); + + for (area.corners()) |corner| + vertices.append(corner) catch unreachable; + + triangles.append(Triangle{ + .points = [3]Index{ 0, 2, 1 }, + .neighbors = [3]?Index{ null, 1, null }, + }) catch unreachable; + + triangles.append(Triangle{ + .points = [3]Index{ 3, 1, 2 }, + .neighbors = [3]?Index{ null, 0, null }, + }) catch unreachable; + + return .{ + .triangles = triangles, + .vertices = vertices, + .allocator = allocator, + }; + } + + pub fn insertAtRandom(self: *@This(), point: Vertex, generator: std.rand.Random) !void { + // Find a triangle the point lies starting from some random triangle. + var abc_index: Index = @intCast(generator.int(Index) % self.triangles.items.len); + var abc = &self.triangles.items[abc_index]; + + var relation = abc.pointRelation(self.vertices, point); + while (relation != .contained) { + abc_index = abc.neighbors[@intCast(@intFromEnum(relation))].?; + abc = &self.triangles.items[abc_index]; + relation = abc.pointRelation(self.vertices, point); + } + + // Allocate two new triangles, as well as new vertex. + const new_vertex_index: Index = @intCast(self.vertices.items.len); + try self.vertices.append(point); + + const pbc_index: Index = @intCast(self.triangles.items.len); + const apc_index: Index = @intCast(self.triangles.items.len + 1); + try self.triangles.ensureUnusedCapacity(2); + + // Divide the abc triangle into three. + + abc = &self.triangles.items[abc_index]; + + // Insert pbc. + self.triangles.append(Triangle{ + .points = [3]Index{ new_vertex_index, abc.points[1], abc.points[2] }, + .neighbors = [3]?Index{ abc_index, abc.neighbors[1], apc_index }, + }) catch unreachable; + + // Insert apc. + self.triangles.append(Triangle{ + .points = [3]Index{ abc.points[0], new_vertex_index, abc.points[2] }, + .neighbors = [3]?Index{ abc_index, pbc_index, abc.neighbors[2] }, + }) catch unreachable; + + // Update neighbors to be aware of new triangles. + inline for (abc.neighbors[1..], [2]Index{ pbc_index, apc_index }) |n, e| + if (n) |i| { + const p = &self.triangles.items[i]; + p.neighbors[p.neighborPosition(abc_index)] = e; + }; + + // Existing abc is reused. + abc.points[2] = new_vertex_index; + abc.neighbors[1] = pbc_index; + abc.neighbors[2] = apc_index; + abc.circumference = null; + + // Recursively adjust edges of triangles so that circumferences are only encasing 3 points at a time. + // todo: Try inlining initial calls via @call(.always_inline, ...). + self.trySwapping(abc_index, 0); + self.trySwapping(pbc_index, 1); + self.trySwapping(apc_index, 2); + } + + fn trySwapping(self: @This(), triangle_index: Index, edge: u2) void { + // First find opposite to edge point that lies in neighbor. + const triangle = &self.triangles.items[triangle_index]; + const neighbor_index = triangle.neighbors[edge]; + if (neighbor_index == null) + return; + + const neighbor = &self.triangles.items[neighbor_index.?]; + + if (neighbor.circumference == null) + neighbor.circumference = Triangle.Circumference.init(neighbor.*, self.vertices); + + // Position of neighbor's point opposide to shared with triangle edge. + const point_order = neighbor.nextAfter(triangle.points[edge]); + const point_index = neighbor.points[point_order]; + const prev_edge = if (edge == 0) 2 else edge - 1; + if (neighbor.doesFailIncircleTest(self.vertices.items[triangle.points[prev_edge]])) { + // Incircle test failed, swap edges of two triangles and then try swapping newly swapped ones. + const next_edge = (edge + 1) % 3; + const next_point_order = (point_order + 1) % 3; + const prev_point_order = if (point_order == 0) 2 else point_order - 1; + + // Update neighbors of triangles in which edge was swapped. + if (triangle.neighbors[next_edge]) |i| { + const n = &self.triangles.items[i]; + n.neighbors[n.neighborPosition(triangle_index)] = neighbor_index.?; + } + + if (neighbor.neighbors[prev_point_order]) |i| { + const n = &self.triangles.items[i]; + n.neighbors[n.neighborPosition(neighbor_index.?)] = triangle_index; + } + + const neighbor_prev_point_order_neighbor_index_cache = neighbor.neighbors[prev_point_order]; + + neighbor.points[prev_point_order] = triangle.points[prev_edge]; + neighbor.neighbors[next_point_order] = triangle.neighbors[next_edge]; + neighbor.neighbors[prev_point_order] = triangle_index; + neighbor.circumference = null; + + triangle.points[next_edge] = point_index; + triangle.neighbors[next_edge] = neighbor_index.?; + triangle.neighbors[edge] = neighbor_prev_point_order_neighbor_index_cache; + triangle.circumference = null; + + self.trySwapping(triangle_index, edge); + self.trySwapping(neighbor_index.?, point_order); + } + } +}; + +const Triangle = struct { + // References to vertices it's composed of, named abc, in CCW orientation. + points: [3]Index, + + // References to triangles that are on other side of any edge, if any. + // Order is: ab, bc, ca + neighbors: [3]?Index, + + // Lazily calculated and cached for incircle tests. + circumference: ?Circumference = null, + + pub const Circumference = struct { + center: Vertex, + radius_squared: VertexComponent, // todo: Way to get a type capable of holding squared values. + + pub fn init(triangle: Triangle, vertices: std.ArrayList(Vertex)) @This() { + const a = vertices.items[triangle.points[0]]; + const b = vertices.items[triangle.points[1]]; + const c = vertices.items[triangle.points[2]]; + + const ab: Vertex = @splat(magnitudeSquared(a)); + const cd: Vertex = @splat(magnitudeSquared(b)); + const ef: Vertex = @splat(magnitudeSquared(c)); + + const cmb = @shuffle(VertexComponent, c - b, undefined, [2]i32{ 1, 0 }); + const amc = @shuffle(VertexComponent, a - c, undefined, [2]i32{ 1, 0 }); + const bma = @shuffle(VertexComponent, b - a, undefined, [2]i32{ 1, 0 }); + + const center = ((ab * cmb + cd * amc + ef * bma) / (a * cmb + b * amc + c * bma)) / @as(Vertex, @splat(2)); + + return .{ + .center = center, + .radius_squared = magnitudeSquared(a - center), + }; + } + }; + + // todo: Try perpendicular dot product approach. + pub fn pointRelation(self: @This(), vertices: std.ArrayList(Vertex), point: Vertex) enum(u2) { + outside_ab = 0, + outside_bc = 1, + outside_ca = 2, + contained = 3, + } { + const a = vertices.items[self.points[0]]; + const b = vertices.items[self.points[1]]; + const c = vertices.items[self.points[2]]; + + // https://stackoverflow.com/questions/1560492/how-to-tell-whether-a-point-is-to-the-right-or-left-side-of-a-line + + const p = point; + + // Calculate cross products for all edges at once. + const q = @Vector(12, VertexComponent){ b[0], b[1], c[0], c[1], a[0], a[1], p[1], p[0], p[1], p[0], p[1], p[0] }; + const w = @Vector(12, VertexComponent){ a[0], a[1], b[0], b[1], c[0], c[1], a[1], a[0], b[1], b[0], c[1], c[0] }; + const e = q - w; + + const r = @shuffle(VertexComponent, e, undefined, [6]i32{ 0, 1, 2, 3, 4, 5 }); + const t = @shuffle(VertexComponent, e, undefined, [6]i32{ 6, 7, 8, 9, 10, 11 }); + const y = r * t; + + const u = @shuffle(VertexComponent, y, undefined, [3]i32{ 0, 2, 4 }); + const i = @shuffle(VertexComponent, y, undefined, [3]i32{ 1, 3, 5 }); + const o = (u - i) > @Vector(3, VertexComponent){ 0, 0, 0 }; + + // const o = (u - i) <= @Vector(3, VertexComponent){ 0, 0, 0 }; + + // if (@reduce(.And, o)) + // return .contained + // else if (!o[0]) + // return .outside_ab + // else if (!o[1]) + // return .outside_bc + // else + // return .outside_ca; + + const mask = @as(u3, @intFromBool(o[0])) << 2 | @as(u3, @intFromBool(o[1])) << 1 | @as(u3, @intFromBool(o[2])); + + return @enumFromInt(@clz(mask)); + } + + pub inline fn doesFailIncircleTest(self: @This(), point: Vertex) bool { + return magnitudeSquared(self.circumference.?.center - point) < self.circumference.?.radius_squared; + } + + // todo: Shouldn't be here. + pub inline fn magnitudeSquared(p: Vertex) VertexComponent { + return @reduce(.Add, p * p); + } + + // Finds which point comes after given one, by index, CCW. + // Used to translate point names when traveling between neighbors. + pub inline fn nextAfter(self: @This(), point_index: Index) u2 { + inline for (self.points, 0..) |p, i| + if (point_index == p) + return @intCast((i + 1) % 3); + unreachable; + } + + pub inline fn neighborPosition(self: @This(), triangle_index: Index) usize { + inline for (self.neighbors, 0..) |n, i| + if (triangle_index == n) + return i; + unreachable; + } +}; + +pub fn GenericArea(comptime T: type) type { + return struct { + // note: Upper-left origin is assumed, if second point lies left or up of first it willn't work. + xyxy: @Vector(4, T), + + /// Order: Upperleft, upperright, bottomleft, bottomright. + pub fn corners(self: @This()) [4]@Vector(2, T) { + return [4]@Vector(2, T){ + @Vector(2, T){ self.xyxy[0], self.xyxy[1] }, + @Vector(2, T){ self.xyxy[2], self.xyxy[1] }, + @Vector(2, T){ self.xyxy[0], self.xyxy[3] }, + @Vector(2, T){ self.xyxy[2], self.xyxy[3] }, + }; + } + }; +} diff --git a/articles/incremental-delaunay/.static/web.png b/articles/incremental-delaunay/.static/web.png new file mode 100644 index 0000000..3e985a5 --- /dev/null +++ b/articles/incremental-delaunay/.static/web.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:73d0062486bda899c458554285a8195d0d6c6198089a7d733bd89ee03f9c02ed +size 27026 diff --git a/articles/vector-pi-rotation/.static/noise.png b/articles/vector-pi-rotation/.static/noise.png new file mode 100644 index 0000000..e4d7f51 --- /dev/null +++ b/articles/vector-pi-rotation/.static/noise.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1fbf413e324a1c979bccb5523ecac5d4a7775cc15ffadcea480b1172335c21e6 +size 777761