CultMath shaders: restore the float2 segment overloads, sample close distinct sites, document the border step's owner contract
Shader overloads stay native (ruling shader-overloads-stay-native): cultmath_segment_closest and cultmath_distance_to_segment have float2 and float3 forms again, with mirror asserts and fixture families. The rejected "avoid overloads" comment and design text are gone. The border step's outside-cell behaviour (distance 0 with that plane's normal) is documented in C# and the HLSL header and pinned by a test; the mirror and fixture gain distinct sites closer than 1e-2. GLSL and fixture regenerate in the next commit; this one does not pass CommittedGlslEqualsLowering. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
fcd580a8ed
commit
16657fa20f
7 changed files with 81 additions and 8 deletions
|
|
@ -510,7 +510,8 @@ general). The exact power cell border is a second pass: `power_border_step(p, ow
|
|||
running `float4(unit normal toward the owner, distance)` and the radical plane of `owner` against `other`, with
|
||||
the distance built from the offset `owner - p` and the weight difference `(w1 - w2)(w1 + w2)` so it does not
|
||||
cancel for close sites seen from far; the cell boundary distance is the least over every site except the owner
|
||||
(the fold's nearest), and a site coincident with the owner has no plane. `power_border(p, sites)` is that loop.
|
||||
(the fold's nearest), and a site coincident with the owner has no plane. The step trusts the caller's owner: a
|
||||
query outside the owner's cell reports distance 0 with that plane's normal, not an error. `power_border(p, sites)` is that loop.
|
||||
Cell spans refuse a non-finite query, site position or weight, and more than 2^24 sites (indices are floats).
|
||||
`power_cells` and
|
||||
`multiplicative_cells` are that loop over a `ReadOnlySpan<float4>` and nothing else; a shader caller writes the
|
||||
|
|
@ -521,8 +522,7 @@ apply. `CultSiteCells` holds the two score fields and the two site indices (floa
|
|||
circle, negative inside it, -1 when `a` and `b` coincide. `soft_gabriel_gate(sites, first, second, softness)`
|
||||
is `smoothstep(-softness, softness, least margin over every other site)` on positions alone, 1 when no other site
|
||||
exists; it is C# only. `distance_to_segment(point, start, end)` has out-free 2D and 3D forms beside the `out`
|
||||
form, all over one public `segment_closest` per dimension. The shader twins keep one name each (3D only, a 2D
|
||||
caller uses z = 0), so none joins the overload rename list.
|
||||
form, all over one public `segment_closest` per dimension, each with its 2D and 3D shader overload.
|
||||
|
||||
## Intervals
|
||||
|
||||
|
|
|
|||
|
|
@ -608,7 +608,7 @@ CultCellular cultmath_cellular(float3 p)
|
|||
// The exact power cell border is a second loop over the buffer, skipping the owner cells.nearestSite:
|
||||
// float4 border = float4(0.0, 0.0, 0.0, 1.0e30); int owner = (int)cells.nearestSite;
|
||||
// for (int k = 0; k < count; k++) if (k != owner) border = cultmath_power_border_step(p, sites[owner], sites[k], border);
|
||||
// Shader names are unique (one 3D form of segment distance; a 2D caller uses z = 0), so none joins the overload rename list.
|
||||
// The step trusts the owner: a p outside the owner's cell reports distance 0 with that plane's normal, not an error.
|
||||
// Statement for statement with math.Sites.cs (power_score, multiplicative_score, site_cells_*, gabriel_margin).
|
||||
struct CultSiteCells
|
||||
{
|
||||
|
|
@ -688,6 +688,17 @@ float cultmath_gabriel_margin(float3 a, float3 b, float3 other)
|
|||
return (cultmath_distance(other, (a + b) * 0.5) - r) / r;
|
||||
}
|
||||
|
||||
float2 cultmath_segment_closest(float2 point_value, float2 start, float2 end)
|
||||
{
|
||||
float2 segment = end - start;
|
||||
float segmentLengthSq = cultmath_lengthsq(segment);
|
||||
if (segmentLengthSq <= 0.0)
|
||||
return start;
|
||||
|
||||
float t = cultmath_saturate(dot(point_value - start, segment) / segmentLengthSq);
|
||||
return start + segment * t;
|
||||
}
|
||||
|
||||
float3 cultmath_segment_closest(float3 point_value, float3 start, float3 end)
|
||||
{
|
||||
float3 segment = end - start;
|
||||
|
|
@ -699,6 +710,11 @@ float3 cultmath_segment_closest(float3 point_value, float3 start, float3 end)
|
|||
return start + segment * t;
|
||||
}
|
||||
|
||||
float cultmath_distance_to_segment(float2 point_value, float2 start, float2 end)
|
||||
{
|
||||
return cultmath_distance(point_value, cultmath_segment_closest(point_value, start, end));
|
||||
}
|
||||
|
||||
float cultmath_distance_to_segment(float3 point_value, float3 start, float3 end)
|
||||
{
|
||||
return cultmath_distance(point_value, cultmath_segment_closest(point_value, start, end));
|
||||
|
|
|
|||
|
|
@ -69,7 +69,9 @@ public static partial class math
|
|||
// difference (w1 - w2)(w1 + w2), never from a difference of two large squared distances, so it does not
|
||||
// cancel for a far query or close sites. border is the running (unit normal toward the owner, distance),
|
||||
// starting at (0, 0, 0, 1.0e30): the cell's boundary distance is the least plane distance over every other
|
||||
// site. A site coincident with the owner has no plane and leaves border unchanged.
|
||||
// site. A site coincident with the owner has no plane and leaves border unchanged. The step trusts the caller's
|
||||
// owner: a p outside the owner's cell has a negative signed distance to some plane, which is clamped to 0, so the
|
||||
// step reports distance 0 with that plane's normal, a plausible on-the-border answer, not an error.
|
||||
public static float4 power_border_step(float3 p, float4 owner, float4 other, float4 border)
|
||||
{
|
||||
var d = other.xyz - owner.xyz;
|
||||
|
|
|
|||
|
|
@ -522,6 +522,8 @@ static int4 clamp(int4 x, int4 a, int4 b) { if (a.x > b.x || a.y > b.y || a.z >
|
|||
private sealed record Family(string Glsl, string Tolerance, Func<System.Random, object[]> Arguments, string? Check = null, Platform Platform = Platform.None, Func<JsonArray, string>? Verify = null);
|
||||
|
||||
private static float Uniform(System.Random r, float lo, float hi) => lo + (hi - lo) * r.NextSingle();
|
||||
private static float2 Point2(System.Random r) => new(Uniform(r, -50.0f, 50.0f), Uniform(r, -50.0f, 50.0f));
|
||||
|
||||
private static float3 Point3(System.Random r) => new(Uniform(r, -50.0f, 50.0f), Uniform(r, -50.0f, 50.0f), Uniform(r, -50.0f, 50.0f));
|
||||
|
||||
// A point on the simplex tie set, where snoise's x0 has equal components (NoiseGradTests.SnoiseIsContinuousOnTheSimplexDiagonal):
|
||||
|
|
@ -797,6 +799,16 @@ static int4 clamp(int4 x, int4 a, int4 b) { if (a.x > b.x || a.y > b.y || a.z >
|
|||
new Family("mat3 cultmath_snoise_hessian(vec3)", "ulp-bounded, coordinate bands 1e2 to 4e6", r => new object[] { BandPoint3(r, 1.0e2, 4.0e6) }, SpanAllowanceRead, Verify: cases => AssertSpanAllowance(cases, hessian: true, small: false)),
|
||||
// Segment distance in 2D and 3D (one segment in eight degenerate), then the site cells: scores, fold step
|
||||
// over states built by inserting 0-3 random scores (one in four with an exactly tied score), border, margin.
|
||||
new Family("float cultmath_distance_to_segment(vec2, vec2, vec2)", "ulp-bounded", r =>
|
||||
{
|
||||
var start = Point2(r);
|
||||
return new object[] { Point2(r), start, r.Next(8) == 0 ? start : Point2(r) };
|
||||
}),
|
||||
new Family("vec2 cultmath_segment_closest(vec2, vec2, vec2)", "ulp-bounded", r =>
|
||||
{
|
||||
var start = Point2(r);
|
||||
return new object[] { Point2(r), start, r.Next(8) == 0 ? start : Point2(r) };
|
||||
}),
|
||||
new Family("float cultmath_distance_to_segment(vec3, vec3, vec3)", "ulp-bounded", r =>
|
||||
{
|
||||
var start = Point3(r);
|
||||
|
|
@ -808,12 +820,15 @@ static int4 clamp(int4 x, int4 a, int4 b) { if (a.x > b.x || a.y > b.y || a.z >
|
|||
return new object[] { Point3(r), start, r.Next(8) == 0 ? start : Point3(r) };
|
||||
}),
|
||||
// The border step: owner and other random (one in six coincident), a running border either empty or a
|
||||
// random (normal, distance).
|
||||
// random (normal, distance); one in six other is a distinct site within 1e-3 of the owner (inside the 1e-2 guard a
|
||||
// changed length test would trip), one in six coincident.
|
||||
new Family("vec4 cultmath_power_border_step(vec3, vec4, vec4, vec4)", "ulp-bounded", r =>
|
||||
{
|
||||
var owner = Site(r, 0.1f, 20.0f);
|
||||
var border = r.Next(2) == 0 ? new float4(0.0f, 0.0f, 0.0f, 1.0e30f) : new float4(Point3(r), Uniform(r, 0.0f, 50.0f));
|
||||
return new object[] { Point3(r), owner, r.Next(6) == 0 ? owner : Site(r, 0.1f, 20.0f), border };
|
||||
var pick = r.Next(6);
|
||||
var near = new float4(owner.xyz + new float3(Uniform(r, 1.0e-4f, 1.0e-3f), Uniform(r, -1.0e-3f, 1.0e-3f), Uniform(r, -1.0e-3f, 1.0e-3f)), Uniform(r, 0.1f, 20.0f));
|
||||
return new object[] { Point3(r), owner, pick == 0 ? owner : pick == 1 ? near : Site(r, 0.1f, 20.0f), border };
|
||||
}),
|
||||
new Family("vec4 cultmath_power_score(vec3, vec4)", "ulp-bounded", r => new object[] { Point3(r), Site(r, 0.1f, 20.0f) }),
|
||||
new Family("vec4 cultmath_multiplicative_score(vec3, vec4)", "ulp-bounded", r => new object[] { Point3(r), Site(r, 0.1f, 100.0f, logWeight: true) }),
|
||||
|
|
|
|||
|
|
@ -35,6 +35,8 @@ public sealed class HlslSourceCompatibilityTests
|
|||
Assert.NotNull(ShaderFunction(shader, "cultmath_site_cells_border", siteCells));
|
||||
Assert.NotNull(ShaderFunction(shader, "cultmath_gabriel_margin", typeof(float3), typeof(float3), typeof(float3)));
|
||||
Assert.NotNull(ShaderFunction(shader, "cultmath_power_border_step", typeof(float3), typeof(float4), typeof(float4), typeof(float4)));
|
||||
Assert.NotNull(ShaderFunction(shader, "cultmath_segment_closest", typeof(float2), typeof(float2), typeof(float2)));
|
||||
Assert.NotNull(ShaderFunction(shader, "cultmath_distance_to_segment", typeof(float2), typeof(float2), typeof(float2)));
|
||||
Assert.NotNull(ShaderFunction(shader, "cultmath_segment_closest", typeof(float3), typeof(float3), typeof(float3)));
|
||||
Assert.NotNull(ShaderFunction(shader, "cultmath_distance_to_segment", typeof(float3), typeof(float3), typeof(float3)));
|
||||
Assert.NotNull(ShaderFunction(shader, "cultmath_snoise_grad", typeof(float3)));
|
||||
|
|
@ -130,6 +132,16 @@ public sealed class HlslSourceCompatibilityTests
|
|||
for (var k = 0; k < 64; k++) { var values = Enumerable.Range(0, 32).Select(_ => Next()).ToArray(); cases.Add(i => values[i]); }
|
||||
for (var k = 0; k < 16; k++) { var values = Enumerable.Range(0, 32).Select(_ => Next()).ToArray(); cases.Add(i => values[i & ~1]); }
|
||||
|
||||
// Distinct sites closer than 1e-2 (slots 3-6 are the owner and 7-10 the other site of
|
||||
// cultmath_power_border_step): a length guard wider than "coincident" changes the result for these only.
|
||||
for (var k = 0; k < 16; k++)
|
||||
{
|
||||
var values = Enumerable.Range(0, 32).Select(_ => Next()).ToArray();
|
||||
for (var c = 0; c < 3; c++) values[7 + c] = values[3 + c] + (random.NextSingle() * 2.0f - 1.0f) * 1.0e-3f;
|
||||
values[7] += 2.0e-4f;
|
||||
cases.Add(i => values[i]);
|
||||
}
|
||||
|
||||
// Points aimed squarely at cellular's HLSL mirror (F6/F3 Soul findings): the generic random
|
||||
// and special-value cases above pass ~92 inputs through cultmath_cellular and never happen to
|
||||
// hit a point where a radius-2 cell wins (about 3.5e-5 of points, Soul measured) or an
|
||||
|
|
|
|||
|
|
@ -488,6 +488,18 @@ public sealed class SiteCellsTests
|
|||
Assert.Equal(new float2(4.0f, 0.0f), segment_closest(new float2(9.0f, 3.0f), new float2(0.0f, 0.0f), new float2(4.0f, 0.0f)));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PowerBorderStepOutsideTheOwnersCellReportsDistanceZeroWithThatPlanesNormal()
|
||||
{
|
||||
var left = new float4(0.0f, 0.0f, 0.0f, 1.0f);
|
||||
var right = new float4(4.0f, 0.0f, 0.0f, 1.0f);
|
||||
var empty = new float4(0.0f, 0.0f, 0.0f, 1.0e30f);
|
||||
var inside = power_border_step(new float3(1.0f, 0.0f, 0.0f), left, right, empty);
|
||||
Assert.Equal(new float4(-1.0f, 0.0f, 0.0f, 1.0f), inside);
|
||||
var outside = power_border_step(new float3(3.5f, 0.0f, 0.0f), left, right, empty);
|
||||
Assert.Equal(new float4(-1.0f, 0.0f, 0.0f, 0.0f), outside);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PowerBorderStepLeavesTheBorderAloneForASiteCoincidentWithTheOwner()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -608,7 +608,7 @@ CultCellular cultmath_cellular(float3 p)
|
|||
// The exact power cell border is a second loop over the buffer, skipping the owner cells.nearestSite:
|
||||
// float4 border = float4(0.0, 0.0, 0.0, 1.0e30); int owner = (int)cells.nearestSite;
|
||||
// for (int k = 0; k < count; k++) if (k != owner) border = cultmath_power_border_step(p, sites[owner], sites[k], border);
|
||||
// Shader names are unique (one 3D form of segment distance; a 2D caller uses z = 0), so none joins the overload rename list.
|
||||
// The step trusts the owner: a p outside the owner's cell reports distance 0 with that plane's normal, not an error.
|
||||
// Statement for statement with math.Sites.cs (power_score, multiplicative_score, site_cells_*, gabriel_margin).
|
||||
struct CultSiteCells
|
||||
{
|
||||
|
|
@ -688,6 +688,17 @@ float cultmath_gabriel_margin(float3 a, float3 b, float3 other)
|
|||
return (cultmath_distance(other, (a + b) * 0.5) - r) / r;
|
||||
}
|
||||
|
||||
float2 cultmath_segment_closest(float2 point_value, float2 start, float2 end)
|
||||
{
|
||||
float2 segment = end - start;
|
||||
float segmentLengthSq = cultmath_lengthsq(segment);
|
||||
if (segmentLengthSq <= 0.0)
|
||||
return start;
|
||||
|
||||
float t = cultmath_saturate(dot(point_value - start, segment) / segmentLengthSq);
|
||||
return start + segment * t;
|
||||
}
|
||||
|
||||
float3 cultmath_segment_closest(float3 point_value, float3 start, float3 end)
|
||||
{
|
||||
float3 segment = end - start;
|
||||
|
|
@ -699,6 +710,11 @@ float3 cultmath_segment_closest(float3 point_value, float3 start, float3 end)
|
|||
return start + segment * t;
|
||||
}
|
||||
|
||||
float cultmath_distance_to_segment(float2 point_value, float2 start, float2 end)
|
||||
{
|
||||
return cultmath_distance(point_value, cultmath_segment_closest(point_value, start, end));
|
||||
}
|
||||
|
||||
float cultmath_distance_to_segment(float3 point_value, float3 start, float3 end)
|
||||
{
|
||||
return cultmath_distance(point_value, cultmath_segment_closest(point_value, start, end));
|
||||
|
|
|
|||
Loading…
Reference in a new issue