Skip to content

Planar texgen maps a face that faces one way as one plane, and its normal map takes the planar frame - #561

Open
RyeMutt wants to merge 3 commits into
rye/vertex-cache-orderfrom
rye/planar-texgen
Open

RyeMutt wants to merge 3 commits into
rye/vertex-cache-orderfrom
rye/planar-texgen

Conversation

@RyeMutt

@RyeMutt RyeMutt commented Oct 23, 2026 •

Copy link
Copy Markdown
Member

Planar texgen projected each vertex along that vertex's own normal. It switched the projection axis at |n.x| ≥ 0.5, and left the tangent b × n unnormalized. A round prim puts columns right on that switch at every LOD, and the unnormalized tangent scales V by the normal. The result is a seam and a jump in scale across every curved face.

A planar face's normal map also sampled at the planar coordinates while taking its tangent frame from the default UVs.

This is part 6 of a stack of 7. Merge them in order: each PR is based on the one before it and shows only its own commits.

  1. Prim normals point straight out of their surfaces, caps face out, and a flexible prim rebuilds in place #556 prim normals
  2. A straight prim gets one quad per profile edge, and rows its parameters collapse emit no triangles #557 vertex reduction
  3. A prim every LOD builds alike is generated once, and its LOD changes rebuild nothing #558 LOD-invariant prims
  4. Mesh and GLTF tangents come from meshoptimizer's MikkTSpace mode, welded and ordered in one pass #559 mesh tangents
  5. Prims, sculpts, GLTF primitives and avatar meshes are ordered for the vertex cache #560 vertex cache order
  6. Planar texgen maps a face that faces one way as one plane, and its normal map takes the planar frame #561 planar texgen ← this PR
  7. The uploader's LODs and normals come from meshoptimizer's attribute-aware simplifier #562 uploader LODs and normals

Changes

One plane per face

  • LLVolumeFace::getPlanarNormal: on a face's first planar use, gives the face one normal. Where all the vertices share a normal, it's that one. Otherwise it's the area-weighted sum of the face's triangle normals.
  • Projection: a face is one plane when every vertex normal lies within 60° of that normal. Such a face is projected along it at every point, by the same formula as before, so a flat face maps as it did and a curved one has no seam.
  • Faces that aren't one plane (a full cylinder, sphere or torus) keep per-vertex mapping. Where an x falls within 1e-4 below the switch, the vertex takes the y binormal, so a column maps one way on every row and at every LOD.
  • alsimd::planar_texcoords: maps a face in one pass, with the texture entry's transform folded in. It reproduces exactly what MSVC's /fp:fast had made of LLFace's per-vertex loops, bit for bit over 734,028 coordinates on the sweep's faces that have one normal.
  • Touches: surfaceToTexture maps a touch through the same kernel. Touch ST, which is sent to the simulator, therefore agrees with the rendered texture.

Normal maps

  • alsimd::planar_tangents: gives the tangent that the generators would give for UVs equal to the planar coordinates, from the normals alone.

    • getGeometryVolume writes these tangents for planar faces.
    • So do the legacy emboss offsets.
    • getPlanarProjectedParams builds its frame from them.

    Planar texgen no longer makes a face's default-UV tangents. Before, normal maps on angled faces, sculpts and meshes shaded turned or mirrored, legacy and PBR alike.

Results

  • Speed: a box face maps in 5.3 ns, where it took 7.6. A cylinder cap is 2.6 to 3.3× faster, and spheres, tori and sculpts 2.5 to 5.7× faster.
  • Tangent cost: about 1.5 ns per vertex on each position rebuild.
  • Tangent accuracy: checked against each face's own generator run on its planar coordinates, over the sweep and the mesh corpus (297,310 tangents).
    • Faces with one normal are within 0.02°.
    • Curved faces are within 0.73 of the bound the generator's sum allows.
  • Faces that change: of the sweep's 26,152 faces, 45.3% map as one plane. Six prism sides that are flat only to rounding change. Their normals straddle the switch, so half their vertices took each axis before.

A tapered round side's normals turn about half a degree from row to row, which is more than the tie-break absorbs. Its case is marked [!shouldfail].

Testing

  • llmath_tests, shuffled: 432 cases. 430 pass, 1 is skipped, and 1 fails as expected. The expected failure is the tapered round side described above.
  • Shuffled: llprimitive_tests passes all 48 cases. llcommon_tests has 723 cases: 720 pass and 3 are skipped.
  • Viewer: not yet checked. To check:
    • planar mapping on a tilted box, which should look as before;
    • a quarter cylinder and a full cylinder, with touch ST checked against the rendered texture;
    • a normal map on planar angled faces and sculpts.

🤖 Generated with Claude Code

@coderabbitai

coderabbitai Bot commented Oct 23, 2026 •

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Next included review available in 59 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used the included review currently available.

Learn how review limits work.

Review configuration:

⚙️ Run configuration
  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: Repository UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 9133f33e-9a06-4cf1-bd75-1b8cf905e2c2
📥 Commits

Reviewing files that changed from the base of the PR and between 5df7295 and 2f59a2f.

📒 Files selected for processing: 9
  • indra/llmath/alsimdkernels.cpp
  • indra/llmath/alsimdkernels.h
  • indra/llmath/alsimdkernels.inl
  • indra/llmath/llvolume.cpp
  • indra/llmath/llvolume.h
  • indra/llmath/tests/alsimdkernels_test.cpp
  • indra/llmath/tests/llvolume_bench.cpp
  • indra/llmath/tests/llvolume_test.cpp
  • indra/newview/llface.cpp
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

RyeMutt and others added 2 commits October 23, 2026 09:05
Planar texgen projected each vertex along its own normal, switching the
projection's axis at |n.x| >= 0.5, with the tangent b x n left
unnormalized. A round prim puts columns on that switch at every LOD,
and the tangent scales V by the normal: a seam and a jump in scale
across every curved face.

LLVolumeFace::getPlanarNormal makes, on a face's first planar use, the
vertices' one normal where they all share it, and otherwise the sum of
its triangles' normals weighted by area, normalized; the face is one
plane where every vertex normal lies within 60 degrees of it. Such a
face is projected along that normal at every point, by the formula as
it was, so a flat face maps as it did and a curved one has no seam. Any
other face, a full cylinder, sphere or torus, keeps per-vertex mapping,
where an x within 1e-4 below the switch takes the y binormal, so that a
column maps one way on every row and at every LOD. The planar normal
lasts until a full build; a flexible prim's frames keep their first
build's, as they keep its planar coordinates.

alsimd::planar_texcoords maps a face in one pass with the texture
entry's transform folded in. MSVC's /fp:fast had rewritten LLFace's
per-vertex loops: 1 + (2d - 0.5) folded to 2d + 0.5, one product of each
turned sum fused, the offset added before the 0.5. The kernel rounds
exactly so, fenced by float_control(precise) against fusing on its own,
and over the 9,194 sweep faces with one normal, 734,028 coordinates,
gives what those loops gave bit for bit. Of the sweep's 26,152 faces,
45.3% are one plane: 9,194 with one normal, 1,051 flat to rounding and
1,596 curved.

surfaceToTexture maps a touch through the same kernel and transform,
with its normal normalized to full precision: the fast one moved
vertices across the switch. getPlanarProjectedParams aligns a face
projected as one plane by its planar normal, without its tangents.

Six prism sides flat to rounding change, a triangle profile with a
square hole at 0.85 and a cut: their normals straddle the switch, and
half their vertices took each axis.

A box face maps in 5.3 ns where it took 7.6 (6.3 for 9.2 through a
transform), a cylinder cap 2.6-3.3x faster, spheres, tori and sculpts
2.5-5.7x; the planar normal costs about one old pass, once. A tapered
round side's normals still turn about half a degree from row to row,
more than the tie; its case is marked [!shouldfail].

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A face with planar texgen samples its normal map at the planar
coordinates, but its vertex tangents came from the default UVs. Box and
cylinder faces happen to agree; angled faces, sculpts and meshes shaded
with the normal map turned or mirrored, legacy and PBR alike.

alsimd::planar_tangents gives the tangent the generators would give for
UVs that are the planar coordinates, from the normals alone: with s's
gradient gs = scale * b and t's gt = -(scale * t), w is the sign of
n . (gs x gt) and the tangent is w (gt x n), normalized. getGeometryVolume
writes those for planar faces, turned by the vertex matrix as before;
the PBR shaders' texture-rotation fix-up applies to them as it does to
default ones. The legacy emboss offsets take them too, and
getPlanarProjectedParams builds its frame from them on every face, so
planar texgen no longer makes the face's default-UV tangents.

Against each face's own generator run on its planar coordinates, over
the sweep and the mesh corpus, 297,310 tangents: within 0.02 degrees on
faces with one normal, within 0.73 of the bound the generator's sum
allows on curved ones (0.5 degrees plus twice the vertex normal's lean
from its triangles), with the same handedness except at 30 vertices of
folded strips whose normals lie 86-89 degrees from a triangle. A planar
face's tangents cost about 1.5 ns a vertex on each position rebuild,
12.8 ns for a box face.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant