Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -13,20 +13,24 @@ final class OutlineVisibility {
private static final double EPSILON = 2.0E-7D;
private static final int MAX_VIEW_OPERATIONS = 200_000;
private final List<ConvexShape> shapes;
private final double distanceEpsilon;
private final List<List<Interval>> seams;

private OutlineVisibility(List<ConvexShape> shapes, List<List<Interval>> seams) {
private OutlineVisibility(List<ConvexShape> shapes, List<List<Interval>> seams, double distanceEpsilon) {
this.shapes = shapes;
this.distanceEpsilon = distanceEpsilon;
this.seams = seams;
}

static OutlineVisibility build(SelectionGeometry geometry, List<OutlineBuilder.Segment> segments, Runnable step) {
List<List<Interval>> seams = new ArrayList<>(segments.size());
boolean any = false;
double offset = OFFSET * geometry.unitsPerBlock();
double epsilon = EPSILON * geometry.unitsPerBlock();
for (OutlineBuilder.Segment segment : segments) {
step.run();
List<Interval> intervals = new ArrayList<>();
List<ConvexShape> nearby = geometry.intersecting(new AABB(segment.start(), segment.end()).inflate(OFFSET));
List<ConvexShape> nearby = geometry.intersecting(new AABB(segment.start(), segment.end()).inflate(offset));
for (int i = 0; i < nearby.size(); i++) {
for (int j = i + 1; j < nearby.size(); j++) {
ConvexShape first = nearby.get(i), second = nearby.get(j);
Expand All @@ -38,16 +42,18 @@ static OutlineVisibility build(SelectionGeometry geometry, List<OutlineBuilder.S
double otherOffset = Math.copySign(1.0D, alignment) * b.planeOffset();
double distance = Math.abs(a.planeOffset() - otherOffset);
// 模型坐标来自 float;边界误差不得把恰好 0.1 的偏移算作小于 0.1。
if (distance <= EPSILON || distance >= OFFSET - EPSILON) continue;
double low = Math.min(a.planeOffset(), otherOffset) - EPSILON;
double high = Math.max(a.planeOffset(), otherOffset) + EPSILON;
if (distance <= epsilon || distance >= offset - epsilon) {
continue;
}
double low = Math.min(a.planeOffset(), otherOffset) - epsilon;
double high = Math.max(a.planeOffset(), otherOffset) + epsilon;
Interval interval = new Interval(0, 1);
interval = clip(interval, a.normal().dot(segment.start()) - high,
a.normal().dot(segment.end()) - high);
interval = clip(interval, low - a.normal().dot(segment.start()),
low - a.normal().dot(segment.end()));
interval = clipShape(interval, segment, first, a.normal(), distance, step);
interval = clipShape(interval, segment, second, a.normal(), distance, step);
interval = clipShape(interval, segment, first, a.normal(), distance, epsilon, step);
interval = clipShape(interval, segment, second, a.normal(), distance, epsilon, step);
if (interval.valid()) intervals.add(interval);
}
}
Expand All @@ -57,32 +63,34 @@ static OutlineVisibility build(SelectionGeometry geometry, List<OutlineBuilder.S
seams.add(merged);
any |= !merged.isEmpty();
}
return new OutlineVisibility(any ? geometry.shapes() : List.of(), List.copyOf(seams));
return new OutlineVisibility(any ? geometry.shapes() : List.of(), List.copyOf(seams), epsilon);
}

private static Interval clipShape(Interval interval, OutlineBuilder.Segment segment, ConvexShape shape,
Vec3 normal, double distance, Runnable step) {
Vec3 normal, double distance, double epsilon, Runnable step) {
for (ConvexShape.Face face : shape.faces()) {
step.run();
if (!interval.valid()) break;
// 只沿错位面的法向扩展,不把相邻的真实细节一起膨胀掉。
double allowance = Math.abs(face.normal().dot(normal)) * distance + EPSILON;
double allowance = Math.abs(face.normal().dot(normal)) * distance + epsilon;
interval = clip(interval, face.signedDistance(segment.start()) - allowance,
face.signedDistance(segment.end()) - allowance);
}
return interval;
}

long estimatedBytes() {
return 64L + this.seams.size() * 32L + this.seams.stream().mapToLong(list -> list.size() * 40L).sum();
return 72L + this.seams.size() * 32L + this.seams.stream().mapToLong(list -> list.size() * 40L).sum();
}

PackedOutline forView(PackedOutline outline, Vec3 camera) {
if (this.shapes.isEmpty()) return outline;
Work work = new Work();
try {
List<List<Vec3>> cones = new ArrayList<>(this.shapes.size());
for (ConvexShape shape : this.shapes) cones.add(project(shape, camera, work));
for (ConvexShape shape : this.shapes) {
cones.add(project(shape, camera, work, this.distanceEpsilon));
}
List<OutlineBuilder.Segment> visible = new ArrayList<>();
for (int index = 0; index < outline.segmentCount(); index++) {
work.step();
Expand All @@ -98,7 +106,7 @@ PackedOutline forView(PackedOutline outline, Vec3 camera) {
for (Vec3 plane : cone) {
work.step();
double a = plane.dot(start.subtract(camera)), b = plane.dot(end.subtract(camera));
if (Math.abs(a) <= EPSILON && Math.abs(b) <= EPSILON) {
if (Math.abs(a) <= this.distanceEpsilon && Math.abs(b) <= this.distanceEpsilon) {
double direction = plane.dot(side);
if (direction > EPSILON) coversLeft = false;
if (direction < -EPSILON) coversRight = false;
Expand Down Expand Up @@ -139,7 +147,7 @@ private static void append(List<OutlineBuilder.Segment> output, Vec3 start, Vec3
output.add(new OutlineBuilder.Segment(start.lerp(end, low), start.lerp(end, high)));
}

private static List<Vec3> project(ConvexShape shape, Vec3 camera, Work work) {
private static List<Vec3> project(ConvexShape shape, Vec3 camera, Work work, double epsilon) {
List<Vec3> planes = new ArrayList<>();
for (ConvexShape.Edge edge : shape.edges()) {
work.step();
Expand All @@ -153,8 +161,11 @@ private static List<Vec3> project(ConvexShape shape, Vec3 camera, Work work) {
min = Math.min(min, distance);
max = Math.max(max, distance);
}
if (max <= EPSILON && min < -EPSILON) planes.add(normal);
else if (min >= -EPSILON && max > EPSILON) planes.add(normal.scale(-1));
if (max <= epsilon && min < -epsilon) {
planes.add(normal);
} else if (min >= -epsilon && max > epsilon) {
planes.add(normal.scale(-1));
}
}
return planes;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,24 @@ public final class SelectionGeometry {
private final Node root;
private final PackedOutline fallback;
private final long estimatedBytes;
private final double unitsPerBlock;

public SelectionGeometry(List<ConvexShape> shapes) {
this(shapes, 1);
}

/**
* 指定几何坐标中一格对应的单位数,使放大后的薄层仍按实际尺寸判断微小接缝。
* 此参数只影响轮廓接缝容差,调用方仍须通过 SelectionPart 的变换还原坐标。
*
* @param shapes 局部坐标中的闭凸体
* @param unitsPerBlock 有限正数;普通方块坐标传 1,放大 128 倍的坐标传 128
*/
public SelectionGeometry(List<ConvexShape> shapes, double unitsPerBlock) {
if (!Double.isFinite(unitsPerBlock) || unitsPerBlock <= 0) {
throw new IllegalArgumentException("unitsPerBlock must be finite and positive");
}
this.unitsPerBlock = unitsPerBlock;
if (shapes.isEmpty() || shapes.size() > MAX_SHAPES) throw new IllegalArgumentException("Invalid shape count");
this.shapes = List.copyOf(shapes);
this.root = build(new ArrayList<>(shapes));
Expand All @@ -28,10 +44,14 @@ public SelectionGeometry(List<ConvexShape> shapes) {
for (ConvexShape.Edge edge : shape.edges()) edges.add(new OutlineBuilder.Segment(edge.start(), edge.end()));
}
this.fallback = PackedOutline.of(edges);
this.estimatedBytes = 160L + shapes.size() * 256L + this.fallback.estimatedBytes()
this.estimatedBytes = 168L + shapes.size() * 256L + this.fallback.estimatedBytes()
+ shapes.stream().mapToLong(ConvexShape::estimatedBytes).sum();
}

double unitsPerBlock() {
return this.unitsPerBlock;
}

public List<ConvexShape> shapes() { return this.shapes; }
public AABB bounds() { return this.root.bounds; }
public PackedOutline fallbackOutline() { return this.fallback; }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertSame;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.assertThrows;

class OutlineVisibilityTest {
private static final double SMALL = 0.05D / 16;
Expand Down Expand Up @@ -137,6 +138,48 @@ void transformedPartsConvertTheCameraToGeometrySpace() {
assertFalse(covers(build(step(SMALL)).forView(local), new Vec3(0.5, 1 + SMALL, 0.5)));
}

@Test
void scaledCoordinatesPreserveSeamThresholdAndCameraTransform() {
for (double scale : new double[] {0.5d, 1, 128}) {
for (double offset : new double[] {SMALL, 0.09999d / 16, 0.1d / 16, 0.10001d / 16, (float) 16.1 / 16d - 1, 0.2d / 16}) {
List<ConvexShape> shapes = step(offset).stream()
.map(shape -> shape.transform(new Matrix4d().scaling(scale))).toList();
SelectionGeometry geometry = new SelectionGeometry(shapes, scale);
Matrix4f transform = new Matrix4f().translation(4, -2, 1).rotateY(0.7f).scale((float) (1 / scale));
SelectionPart part = new SelectionPart(geometry, transform);
Vec3 camera = new Vec3(0.75, 8, 0.5).scale(scale);
Vec3 localCamera = part.localPosition(transform(new Matrix4d(transform), camera));
PackedOutline outline = OutlineBuilder.build(geometry, TimeUnit.SECONDS.toNanos(10), 10_000_000);
Vec3 seam = new Vec3(0.5, 1 + offset, 0.5).scale(scale);
assertTrue(covers(outline, seam));
assertEquals(offset >= 0.1d / 16, covers(outline.forView(localCamera), seam));
assertTrue(covers(outline.forView(localCamera), new Vec3(0, 1, 0.5).scale(scale)));
}
}
}

@Test
void scaledCoordinatesPreservePartiallyVisibleSeams() {
double scale = 128;
List<ConvexShape> shapes = List.of(box(0, 0, 0, 1, 1, 1),
box(0.5, 0, 0.1, 1, 1 + SMALL, 0.9), box(1.1, 0, 0.4, 2, 1.2, 0.6)).stream()
.map(shape -> shape.transform(new Matrix4d().scaling(scale))).toList();
PackedOutline outline = OutlineBuilder.build(new SelectionGeometry(shapes, scale),
TimeUnit.SECONDS.toNanos(10), 10_000_000);
PackedOutline visible = outline.forView(new Vec3(-3, 3, 0.5).scale(scale));
assertTrue(covers(visible, new Vec3(1, 1 + SMALL, 0.2).scale(scale)));
assertFalse(covers(visible, new Vec3(1, 1 + SMALL, 0.5).scale(scale)));
assertTrue(covers(visible, new Vec3(1, 1 + SMALL, 0.8).scale(scale)));
}

@Test
void rejectsInvalidCoordinateScales() {
List<ConvexShape> shapes = step(SMALL);
for (double scale : new double[] {0, -1, Double.NaN, Double.POSITIVE_INFINITY}) {
assertThrows(IllegalArgumentException.class, () -> new SelectionGeometry(shapes, scale));
}
}

private static List<ConvexShape> step(double offset) {
return List.of(box(0, 0, 0, 1, 1, 1), box(0.5, 0, 0.25, 1.5, 1 + offset, 0.75));
}
Expand Down
Loading