Skip to content

Add support for all eight UV sets in StandardMaterial - #9376

Merged
mvaligursky merged 3 commits into
mainfrom
mv-multi-uv
Sep 14, 2026
Merged

mvaligursky merged 3 commits into
mainfrom
mv-multi-uv

Conversation

@mvaligursky

Copy link
Copy Markdown
Contributor

StandardMaterial sampled texture maps from UV sets 0 and 1 only: every *MapUv above 1 was silently clamped to 1, so glb files carrying more sets (loaded since #3591) rendered with the wrong coordinates. Maps can now use any of the eight UV sets the mesh provides.

Fixes #3975

Changes:

  • StandardMaterial accepts *MapUv values from 0 to 7 for every map, including lightMapUv. A map is still only sampled when the mesh provides the set it is assigned to, now checked for every set rather than just the first two, so a glb material pointing at a missing set is dropped instead of producing an invalid pipeline on WebGPU.
  • VertexFormat tracks the texture coordinate sets it contains in a bit mask and exposes hasUv(index). The undocumented hasUv0 / hasUv1 fields are removed in its favour.
  • The mesh instance folds the mesh's UV set mask into its shader variant hash, so the same material rendered on meshes with different UV sets no longer shares a variant. No new SHADERDEF_* bits were needed, which matters because the toggle half of the shader defs is full.
  • The lit vertex shader emits UV sets 2 to 7 through looped includes, in the same way it already expands UV transforms. uv0VS and uv1VS are unchanged, so nine-slicing and existing chunk overrides keep working.
  • The *MapUv docs state the valid range, and the class description explains that a map assigned to a set the mesh lacks is ignored.

API Changes:

  • Added VertexFormat#hasUv(index), returning whether the format contains SEMANTIC_TEXCOORD0 + index.
  • Added vertex shader chunks uvSetAttributeVS, uvSetVS and uvSetVaryingVS.
  • StandardMaterial *MapUv properties accept 0 to 7 instead of 0 to 1.

Examples:

  • New hidden test/multi-uv example. Each box carries all eight UV sets, with set N mapping onto atlas cell N; the top row samples the diffuse map from sets 0 to 7, the bottom row covers a transformed map on UV3, two maps from two sets in one shader, a lightmap on UV5, and a material asking for a set the mesh does not have.

Notes:

  • Tests that call material.getShaderVariant directly must pass vertexFormat, since UV presence is now read from it. The parallax test is updated accordingly.
  • Building a mesh with position, normal and eight UV sets through the Mesh stream API produces a non-interleaved buffer, which needs one WebGPU vertex buffer slot per stream and exceeds WebGPU's limit of 8. The example builds an interleaved buffer by hand; glb meshes are interleaved and unaffected.

StandardMaterial silently clamped every *MapUv property to 0 or 1, so a map
assigned to UV2 or higher sampled UV1 instead. glb files with more UV sets
loaded fine since #3591, but their materials rendered wrong.

- VertexFormat tracks the texture coordinate sets it contains in a bit mask,
  exposed through hasUv(index); the undocumented hasUv0/hasUv1 are removed.
- The standard material gates each map on the vertex format the mesh instance
  already passes to getShaderVariant, and the mesh instance folds the uv mask
  into its shader variant hash, so no further shader-def bits are needed.
- The lit vertex shader emits sets 2 to 7 through looped includes
  (uvSetAttributeVS, uvSetVS, uvSetVaryingVS), keeping uv0VS and uv1VS as
  they are.
- Adds a hidden multi-uv test example rendering all eight sets, and unit
  tests for the vertex format and the generated shaders.

Fixes #3975
@github-actions

Copy link
Copy Markdown

Public API report

This PR changes the public API surface (+1 / −0), per the docs' rules (@ignore / @Private / undocumented are excluded).

Show API diff
+VertexFormat.hasUv(index: number): boolean

Informational only — this never fails the build.

@github-actions

github-actions Bot commented Sep 14, 2026 •

Copy link
Copy Markdown

Build size report

This PR changes the size of the minified bundles.

Bundle Minified Gzip Brotli
playcanvas.min.js 2431.8 KB (+1.1 KB, +0.04%) 626.8 KB (+330 B, +0.05%) 486.8 KB (+242 B, +0.05%)
playcanvas.min.mjs 2429.2 KB (+1.1 KB, +0.04%) 625.9 KB (+254 B, +0.04%) 485.9 KB (−106 B, −0.02%)

@mvaligursky mvaligursky left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Automated PR review by Codex (GPT-5).

I found one blocking correctness issue in the new high-UV path; see the inline comment. The fixed semantic locations collide with attributes used by hardware instancing and MSDF, so some newly advertised UV sets cannot be used with those existing features.

The current Build Examples Browser check is also failing because examples/src/examples/test/multi-uv.example.mjs does not pass Prettier (the long VertexIterator#set and IndexBuffer calls need formatting).

Validation performed: the focused VertexFormat / multi-UV / parallax tests pass (28 tests), the full unit suite passes (2872 passing, 2 pending), git diff --check passes, and an additional WGSL preprocessing probe for UV6 passed. I also inspected the generated example thumbnail. I did not run controlled WebGL/WebGPU pixel-equivalence captures, so visual equivalence for the existing UV0/UV1 paths is not independently proven here.

for (let i = 0; i < useUv.length; i++) {
if (useUv[i]) {
vDefines.set(`UV${i}`, true);
attributes[`vertex_texCoord${i}`] = `TEXCOORD${i}`;

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

[P1] Resolve attribute-location collisions for high UV sets

When a map selects UV6 or UV7, this adds TEXCOORD6 / TEXCOORD7 to the shader. Those semantics are fixed to locations 11 / 12, but the default instancing path above adds instance_line1 / instance_line2 as ATTR11 / ATTR12, which use the same locations. WebGL binds both active attributes to one location and WebGPU emits duplicate @location entries, so an instanced StandardMaterial sampling UV6/7 cannot create a valid shader/pipeline. UV3/4 have the same problem with the MSDF ATTR8 / ATTR9 inputs. Please add a compatible location-allocation strategy (or explicitly constrain the supported combinations) and cover these cases in tests.

@mvaligursky
mvaligursky merged commit 1180f57 into main Sep 14, 2026
10 checks passed
@mvaligursky
mvaligursky deleted the mv-multi-uv branch September 14, 2026 11:11

This branch was successfully deployed

2 active deployments
Preview – engine — 0bdf2d50 Deployed Sep 14, 2026 by vercel[bot]
Preview – engine-api-docs — 0bdf2d50 Deployed Sep 14, 2026 by vercel[bot]
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.

Add support for additional UV channels

1 participant