Skip to content

Drape vector polygons onto terrain via GPU lookup#13627

Open
danielzhong wants to merge 8 commits into
mainfrom
DanielZ/feat/vector-terrian-polygon
Open

Drape vector polygons onto terrain via GPU lookup#13627
danielzhong wants to merge 8 commits into
mainfrom
DanielZ/feat/vector-terrian-polygon

Conversation

@danielzhong

@danielzhong danielzhong commented Jul 15, 2026

Copy link
Copy Markdown
Member

Description

image

Follow-up to #13577, which added GPU-lookup draping for clamped polylines on
terrain. This PR extends the same architecture to polygon fills and
points

Issue number and link

Testing plan

Author checklist

  • I have submitted a Contributor License Agreement
  • I have added my name to CONTRIBUTORS.md
  • I have updated CHANGES.md with a short summary of my change
  • I have added or updated unit tests to ensure consistent code coverage
  • I have updated the inline documentation, and included code examples where relevant
  • I have performed a self-review of my code

AI acknowledgment

  • I used AI to generate content in this PR
  • If yes, I have reviewed the AI-generated content before submitting

If yes, I used the following Tools(s) and/or Service(s):

GithubCopilot
If yes, I used the following Model(s):
Claude Fable 5

PR Dependency Tree

This tree was auto-generated by Charcoal

@github-actions

Copy link
Copy Markdown
Contributor

Thank you for the pull request, @danielzhong!

✅ We can confirm we have a CLA on file for you.

@danielzhong
danielzhong marked this pull request as ready for review July 15, 2026 21:18
@danielzhong danielzhong changed the title Drape vector polygons and points onto terrain via GPU lookup Drape vector polygons onto terrain via GPU lookup Jul 16, 2026
Comment on lines +105 to +108
int indexEnd = int(texelFetch(u_vectorPolygonGridCellIndicesTexture, ivec2(cellIndex + 2, 0), 0).r);
int indexStart = cellIndex == 0
? 0
: int(texelFetch(u_vectorPolygonGridCellIndicesTexture, ivec2(cellIndex + 1, 0), 0).r);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Something I didn't think about on the first PR - have we determined that we'll never exceed the max texture size for the grid cell indices? It looks like, right now, it's expected to be a single row.

Seems reasonable to me that a given tile wouldn't need >4k grid cells, but just checking

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, fixed! The header texture now grows vertically too instead of being a single row, I did the same for GS few months ago

Comment on lines +105 to +106
int indexEnd = int(texelFetch(u_vectorPolygonGridCellIndicesTexture, ivec2(cellIndex + 2, 0), 0).r);
int indexStart = cellIndex == 0

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know it's alluded to above, but I think it would be helpful to leave a short comment that the texture coords are offset by 1, to account for the grid width + height headers. (And, importantly: offset by 1 and not by 2, because the indexStart for cell 0 is implicit, not stored explicitly).

Comment on lines +116 to +151
// One extra iteration (i == indexEnd) flushes the final primitive group.
for (int i = indexStart; i <= indexEnd; i++)
{
int primitiveIndex = -1;
vec4 edge = vec4(0.0);
if (i < indexEnd)
{
ivec2 edgeUv = vectorIndexToUv(i, edgeTextureSize);
edge = texelFetch(u_vectorPolygonEdgeTexture, edgeUv, 0);
primitiveIndex = int(texelFetch(u_vectorPolygonEdgePrimitiveIndicesTexture, edgeUv, 0).r);
}

if (primitiveIndex != currentPrimitive)
{
if (currentPrimitive >= 0 && inside)
{
ivec2 primitiveUv = vectorIndexToUv(currentPrimitive, primitiveTextureSize);
vec4 fillColor = texelFetch(u_vectorColorTexture, primitiveUv, 0);
baseColor = fillColor * vec4(fillColor.aaa, 1.0) + baseColor * (1.0 - fillColor.a);
}
currentPrimitive = primitiveIndex;
inside = false;
}

// Even-odd rule with a horizontal +x ray; the half-open interval
// (> vs <=) counts a ray through a shared vertex exactly once.
if (i < indexEnd && (edge.y > vectorUv.y) != (edge.w > vectorUv.y))
{
float t = (vectorUv.y - edge.y) / (edge.w - edge.y);
float xIntersect = edge.x + t * (edge.z - edge.x);
if (vectorUv.x < xIntersect)
{
inside = !inside;
}
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I find it a little difficult to digest this loop; the nested conditionals and the last-index-special-case are making already complex code more complicated. I think by moving the special-last-index case out of the loop and using some helper functions, this could be more readable (this is just a suggestion, and it could probably be improved even more):

// Composite a polygon's fill over baseColor when this pixel is inside it. A
// negative index (empty cell or first iteration) or an outside pixel is a no-op.
vec4 vectorCompositePolygonFill(vec4 baseColor, int primitiveIndex, bool inside, ivec2 primitiveTextureSize)
{
    if (!inside || primitiveIndex < 0)
    {
        return baseColor;
    }

    ivec2 primitiveUv = vectorIndexToUv(primitiveIndex, primitiveTextureSize);
    vec4 fillColor = texelFetch(u_vectorColorTexture, primitiveUv, 0);
    return fillColor * vec4(fillColor.aaa, 1.0) + baseColor * (1.0 - fillColor.a);
}

// True if a horizontal +x ray from p crosses the edge. The half-open interval
// (> vs <=) counts a ray through a shared vertex exactly once.
bool vectorEdgeCrossesRay(vec4 edge, vec2 p)
{
    if ((edge.y > p.y) == (edge.w > p.y))
    {
        return false;
    }

    float t = (p.y - edge.y) / (edge.w - edge.y);
    float xIntersect = edge.x + t * (edge.z - edge.x);
    return p.x < xIntersect;
}

vec4 vectorPolygonRender(vec2 vectorUv, vec4 baseColor)
{

   ...

    for (int i = indexStart; i < indexEnd; i++)
    {
        ivec2 edgeUv = vectorIndexToUv(i, edgeTextureSize);
        vec4 edge = texelFetch(u_vectorPolygonEdgeTexture, edgeUv, 0);
        int primitiveIndex = int(texelFetch(u_vectorPolygonEdgePrimitiveIndicesTexture, edgeUv, 0).r);

        // A new primitive means the previous group is complete: composite it,
        // then start counting the new one fresh.
        if (primitiveIndex != currentPrimitive)
        {
            baseColor = vectorCompositePolygonFill(baseColor, currentPrimitive, inside, primitiveTextureSize);
            currentPrimitive = primitiveIndex;
            inside = false;
        }

        if (vectorEdgeCrossesRay(edge, vectorUv))
        {
            inside = !inside;
        }
    }

    // The last primitive group has no trailing edge to trigger its composite.
    baseColor = vectorCompositePolygonFill(baseColor, currentPrimitive, inside, primitiveTextureSize);

collection,
collectionData,
rectangle,
width,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we need to pass width, since we already have rectangle. Could just call rectangle.width getter directly.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants