Documentation: Do UVs need to match with position attribute? Or index attribute?

Unless I missed it, I didn’t see the docs describing the rules for UV attributes. For example, does the uv attribute need to match with position or with index (indexed geometry vs non-indexed geometry)?

1 Like

For each vertex there needs to be a set (matching item count) of any vertex attribute used.

1 Like

So

// always
geometry.attribute.position.count === geometry.attribute.uv.count

regardless of indices.

I would imagine that if I were making a framework like threejs, I could make it be aligned with the indices instead (or make an option for either behavior). This would make it easy to, for example, give each triangle a different piece of texture individually, rather than being locked to the vertices.

This index-ing was born in the early days of OpenGL and then went to WebGL. It is not a limitation of Three.js per se. Back then, there were two ways to make a draw call – either non-indexed (glDrawArrays) or indexed (glDrawElements). If the indexed call is used, all attributes had to be indexed (positions, colors, uvs, etc.) Indexing is not as good at is sounds, because even if two vertices share the same position, but different colors or uvs, they cannot use the same index.

As for geometry.attribute.*.count I’m not quite sure, as I have some vague recollection that it was possible to store more values in the attribute arrays than are actually used in a draw call.

3 Likes

I started writing down a whole treatise of the old Geometry vs BufferGeometry, and then instead, just asked GPT to summarize the issue:

Absolutely. The old THREE.Geometry class in Three.js was deprecated in favor of THREE.BufferGeometry for performance, memory efficiency, and GPU compatibility. Here’s a breakdown of why:


:brick: THREE.Geometry (Old)

  • Structure: Stored data in high-level JS arrays (geometry.vertices, geometry.faces, etc.).
  • Ease of use: Very beginner-friendly; you could push vertices and faces and see results immediately.
  • Internals: Required conversion to BufferGeometry under the hood before rendering (geometry.toBufferGeometry() was common).
  • Downsides:
    • Slow to upload to GPU.
    • Inefficient memory layout (objects in arrays, not typed arrays).
    • Frequent garbage collection hits.
    • Difficult to parallelize or batch.

:rocket: THREE.BufferGeometry (New)

  • Structure: Stores data in raw typed arrays (Float32Array, Uint16Array, etc.).
  • GPU-friendly: Directly maps to WebGL buffer objects—fast upload, low overhead.
  • Efficient: Smaller memory footprint, better for batching and instancing.
  • Lower-level: Requires more manual setup—positions, indices, normals, uvs must be added explicitly.
  • Flexible: You can define custom attributes and write your own shaders that read them.

:brain: Why the shift?

  1. Performance: BufferGeometry is much faster, especially for complex or dynamic scenes.
  2. WebGL Alignment: It fits WebGL’s native data model better—typed arrays and buffer objects.
  3. Maintainability: Avoids maintaining two parallel systems.
  4. Modern Practices: Encourages devs to work with raw data, which is essential for advanced rendering and effects.

TL;DR

Feature Geometry BufferGeometry
Ease of Use :+1: Easy to use :-1: More setup required
Performance :-1: Slower :+1: Faster
GPU Upload :-1: Needs conversion :+1: Native format
Memory Efficiency :-1: Poor (object-heavy) :+1: Great (typed arrays)
Advanced Features :-1: Limited :+1: Supports custom attrs
Deprecated :white_check_mark: Yes :cross_mark: No (current standard)

If you’re building anything in modern Three.js, BufferGeometry is the way to go—especially for anything dynamic, GPU-intensive, or procedural. Want a simple helper or abstraction to make BufferGeometry easier to use like Geometry used to be? I can show you that too.

3 Likes

One of the major takeaways here is that the old Geometry class had a fixed, rigid vertex layout. It stored positions, normals, UVs, etc., as separate high-level arrays of objects (like Vector3, Vector2, etc.). This made it easy to work with but offered no control over how vertex data was structured in memory. Every geometry essentially followed the same layout, which wasn’t optimal for performance.

In contrast, BufferGeometry gives you complete control over your vertex attribute layout. You can:

  • Store each attribute (position, normal, uv, etc.) in separate buffers (default),
  • Or pack everything into a single interleaved buffer, where all vertex attributes are tightly packed per vertex (e.g., position → normal → uv → …).

This interleaved format is often more GPU-friendly because it improves data locality. Instead of the GPU having to fetch data from multiple different buffers for each attribute (potentially triggering multiple memory reads per vertex), it can fetch all the vertex attributes in a single read from one buffer. This reduces cache misses and improves memory throughput — which is critical in real-time graphics and especially when drawing many vertices.

So beyond just performance or memory efficiency, BufferGeometry opens up low-level optimizations that weren’t possible before — letting you tailor your geometry to best suit how the GPU wants to consume data.

I’m making the assumption here that it has the minimum number of values (no extra), with default draw range of everything. This is the most common scenario, especially for high level users who don’t know how to modify those details.

This has absolutely nothing to do with the topic of the thread. lol Did you respond to the wrong thread maybe?

I’m explaining why you might not take the approach you outlined.

(Your approach being similar to what the original Geometry class offered.. i.e. “give each triangle a separate piece of texture by default” )

Trying to summarize the years of discussion that led to the current system.

Apologies if this isn’t exactly what you were discussing.. but I thought it relevant to understanding the current design.

1 Like

Yeah, that’s not really relevant. This is only discussing current Three.js: do uvs need to match with position, or with index (and getting that documented). That way people who define a uv attribute know what to define based on whether geometry.index exists or not.

yes for non-indexed geometreis, position.count normal.count, and uv.count should all be the same..

for indexed geometries, it might be possible to have different counts.. as long as none of the indices are out of range of the smallest count in the set.
:slight_smile:

With BufferGeometry you have complete freedom.

1 Like

All attributes need to have the same count.

index can have a different count than position.

For example? (with regards to UVs)

Important distinction here:

  • vertex attributes: if a mesh has N vertices, each vertex attribute must contain N items or vectors
  • instance attributes: if a mesh is drawn M times with instancing, each instance attribute must contain M items or vectors
  • indices: may have any length valid for the draw mode (e.g. for triangles, must be a multiple of 3). The index defines what triangles/lines/points are to be drawn, using the vertices available in the geometry.

Strictly I think indices are not usually referred to as an “attribute”. If indices are omitted, implicitly WebGL draws as if you had provided indices 1, 2, 3, 4, 5, 6, ... N. This is usually less efficient because no vertices are reused, and the vertex cache does nothing.

4 Likes

For example.. this works, and doesn’t generate errors afaik:

let plane = new THREE.Mesh(new THREE.PlaneGeometry(100,100),new THREE.MeshStandardMaterial());
plane.geometry.setAttribute('uv', new THREE.BufferAttribute(new Float32Array(51),2));
console.log(plane.geometry.attributes.uv.count);

(This logs a count of 25.5 :smiley: )
So.. a uv buffer arbitrarily larger than position or normal, and oddly sized.

Why one would need this is somewhat mysterious, but there are schemes (like batching) where these kinds of shenanigans might make sense… like sharing a common attribute amongst multiple geometries or smth.

@donmccurdy :

  • vertex attributes: if a mesh has N vertices, each vertex attribute must contain N items or vectors

would this more accurately be “must contain At Least N items or vectors?”

.. as, the above “works”.. (even with an odd sized array!?)

1 Like

Ah true! In WebGL generally that would be useful to upload data you’ll use for other draw calls later, but in three.js — where different geometries do not really share attributes at the GPU level, and geometries cannot be resized other than via drawRange — I can’t think of a real use case for mismatched attribute sizes.

Technically there’s also the concept of constant attributes, having the same value for all vertices. But three.js does not expose that, it doesn’t seem like they have any clear benefits over uniforms, and the OpenGL spec even says “It is not recommended that you use these.” :slight_smile:

1 Like

different geometries do not really share attributes at the GPU level

It’s allowed though.

You can have 2 planes, and set their position attribute to the same attribute.. later on update the single attributes array, and both will reflect the change.

r.e. constant attributes.. I’ve made use of these in the past.
Agree that they are probably undersupperted/tested though.. Ultimately driver implementations are the bane of all these fun/weird use cases. :smiley:

3 Likes

Duplicated on the GPU, but allowed as far as I know, yep! I don’t expect reusing a BufferAttribute would ever be prohibited. But I think mixing-and-matching with different attribute counts will likely run into bugs that would be viewed with some … skepticism … if reported on the bug tracker. So if that part works now, I do view it as more of an accident. :wink:

3 Likes

Agree 100%. definitely whiskey/weed territory.

1 Like