push .static contents
This commit is contained in:
parent
bb479c9371
commit
760f947c00
2
.gitattributes
vendored
Normal file
2
.gitattributes
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
*.png filter=lfs diff=lfs merge=lfs -text
|
||||
*.gif filter=lfs diff=lfs merge=lfs -text
|
5
.gitignore
vendored
5
.gitignore
vendored
@ -1,6 +1,9 @@
|
||||
**/__pycache__/*
|
||||
html/
|
||||
**/.*/
|
||||
./.*/
|
||||
[articles/**/.static/]
|
||||
articles/**/.dynamic/
|
||||
articles/**/.temp/
|
||||
**/*.jpg/
|
||||
**/*.png/
|
||||
**/*.upload-checksum
|
||||
|
178
articles/2d-visibility/.static/Visibility2D.gd.txt
Normal file
178
articles/2d-visibility/.static/Visibility2D.gd.txt
Normal file
@ -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
|
BIN
articles/2d-visibility/.static/example.gif
(Stored with Git LFS)
Normal file
BIN
articles/2d-visibility/.static/example.gif
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
articles/hand-opt-simplex-2d/.static/noise.png
(Stored with Git LFS)
Normal file
BIN
articles/hand-opt-simplex-2d/.static/noise.png
(Stored with Git LFS)
Normal file
Binary file not shown.
@ -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] },
|
||||
};
|
||||
}
|
||||
};
|
||||
}
|
BIN
articles/incremental-delaunay/.static/web.png
(Stored with Git LFS)
Normal file
BIN
articles/incremental-delaunay/.static/web.png
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
articles/vector-pi-rotation/.static/noise.png
(Stored with Git LFS)
Normal file
BIN
articles/vector-pi-rotation/.static/noise.png
(Stored with Git LFS)
Normal file
Binary file not shown.
Loading…
Reference in New Issue
Block a user