Skip to content

A glTF material, in the vocabulary glTF states it - #333

Merged
CagdasErturk merged 3 commits into
mainfrom
gltf-materials
Sep 9, 2026
Merged

A glTF material, in the vocabulary glTF states it#333
CagdasErturk merged 3 commits into
mainfrom
gltf-materials

Conversation

@CagdasErturk

Copy link
Copy Markdown
Member

There are two material models in this crate now, and they are not two spellings of one thing. A material library carries the Phong vocabulary — ambient, diffuse, specular, a specular exponent — because that is what the format stores. A glTF material is metallic-roughness: a base colour, how metallic a surface is, how rough, and the maps that modulate those.

Nothing converts between them. Every mapping in circulation is a heuristic — there is no specular exponent that is a roughness, only a formula somebody found acceptable for their renderer — and a reader that applied one would hand back a material no file contained. That is what this crate declines to do everywhere else, and the module says so. A caller wanting one model out of both converts where the conversion can be seen.

Every default is the format's, read from the normative schemas rather than recalled. Two are easy to get wrong and are pinned by tests: a material object has no required members, so an empty one is legal and means every default; and baseColorFactor/emissiveFactor bound each item to 0..=1 rather than the array. alphaCutoff is bounded below and not above. index is required on all three texture-info kinds — the normal and occlusion ones inherit it through allOf rather than restating it.

A factor outside its stated range is refused rather than clamped, and the refusal names the member. That is the opposite of what this crate does with the material library's specular exponent, and the two differ because the formats do: that range is a convention files exceed, this one is stated by the schema, so clamping would report a material the document does not contain.

The refusal deliberately does not carry the offending value. A factor is a float and this error vocabulary is compared for equality, so carrying one would cost every refusal in it that property — including the geometry refusals, which have no materials in them at all. The message states the range and the member says where to look.

A primitive's material is reported, not stored. Geometry and the surface it wears are two facts and the canonical form carries one of them; putting an index into a mesh would change the stored form, its version question and everything reading it, for a value nothing in this engine can yet use. gltf::primitive_material answers for the pairing instead.

Attacked in the same row, per this tree's rule for anything that reads bytes nobody here wrote. The glTF target reads the material table from the same bytes and separately — the geometry path never touches it — and holds whatever it accepts to every bound the format states. Six seeds carry materials in the shapes that decide the answer, with a corpus floor for them and one for a seed that provokes this layer's own refusals, since a material changes no geometry and no existing floor would have noticed their loss.

The wildcard-free refusal census refused to compile until the new refusal was named, which is the third format or vocabulary in a row it has caught.

Local gates: cargo fmt --all --check clean, cargo clippy --workspace --all-targets -D warnings clean, cargo test --workspace 264 suites / 2,966 passed / 0 failed, renew check reports the workspace healthy.

There are two material models in this crate now and they are not two
spellings of one thing. A material library carries the Phong vocabulary -
ambient, diffuse, specular, a specular exponent - because that is what
the format stores. A glTF material is metallic-roughness: a base colour,
how metallic a surface is, how rough, and the maps that modulate those.

Nothing converts between them. Every mapping in circulation is a
heuristic: there is no specular exponent that is a roughness, only a
formula somebody found acceptable for their renderer, and a reader that
applied one would hand back a material no file contained. That is the
thing this crate declines to do everywhere else, and the module says so
in as many words. A caller wanting one model out of both converts where
the conversion can be seen and argued with.

Every default here is the format's. A material object has no required
members at all, so an empty one is legal and means every default, which
is why the type's own default is the answer to it rather than a separate
notion of unset.

A factor outside the range the schema states is refused rather than
clamped, and the refusal names the member. That is the opposite of what
this crate does with the material library's specular exponent, and the
two differ because the formats do: that range is a convention files
exceed, this one is stated by the schema, so clamping would report a
material the document does not contain. The value itself is not carried
in the refusal - a factor is a float and this vocabulary is compared for
equality, so carrying one would cost every refusal in it that property,
including the geometry ones that have no materials in them.

A primitive's material is reported rather than stored. Geometry and the
surface it wears are two facts and the canonical form carries one of
them; putting an index into a mesh would change the stored form, its
version question and everything that reads it, for a value nothing here
can yet use.

The reader is attacked from the same bytes as the geometry and
separately, because the geometry path never touches a material table:
whatever the target accepts is held to every bound the format states,
and six seeds carry materials in the shapes that decide the answer. The
gate has a floor for them and one for a seed that provokes this layer's
own refusals, since a material changes no geometry and no existing floor
would have noticed their loss.
Four reviews against this branch, and the checks were green throughout.

The reader clamped where its own documentation promises it refuses. A
factor's range was checked after narrowing the document's number to the
width this crate keeps, and a value one unit in the last place past the
maximum narrows to exactly the maximum - so the check could not see it,
the document was not conforming, and the caller got the limit with
nothing said. The whole argument of this module is that the format's
ranges are stated rather than conventional, so they are refused rather
than clamped; the ordering inverted that in the one place the difference
is invisible. Checked wide, then narrowed.

A material, and its shading half, are asked their kind rather than
merely their presence. Every member lookup answers absent on a number, a
string or an array, so a table of those read as a table of materials
that each said nothing, and this reader's own rule then handed back a
full default material for a document that described none. An
out-of-range factor one array wrapper deep read clean the same way.

The material table has the ceiling this crate's amplification rule asks
for. Geometry is capped because a megabyte of legitimate input built
fifty-eight megabytes of it; a two-byte array element is a whole
material, which amplifies harder, and the table had no equivalent.

Pairing a mesh with its surfaces is one pass. Asking per primitive walked
both tables from the head each time, which is the cost this file
converted three other readers away from, and the singular form's own
documentation invited exactly that loop.

A texture index and a material index are bounded by the table they name.
Neither is resolved - this crate does not read the texture table - and
the documentation says that rather than implying more. Passing them
through unchecked was the one thing this reader's own module doc calls
the commonest fault in a hand-edited document.

An alpha mode the format does not define has its own refusal, carrying
what was spelled. It was being reported as a construct the format defines
and this reader does not, which told the caller to convert a file that
needed repairing.

A colour array is exactly as long as the format states. Reading the first
few and dropping the rest hid whatever the tail said, including
components that would have been refused. A cutoff is range-checked
whatever the mode names, and refused when no mode is named at all, which
the format forbids - and a test that enshrined the accepting behaviour is
now the test that pins the refusal.

Three assertions that could never fire are gone: the layer below already
refuses a number that is not finite. The material half of the fuzz target
dispatches on the container magic the way the reader does, so the
nineteen container seeds it used to walk past now reach it, and the
pairing function it never called is attacked too.

Smaller: an enum tag is compared without building a string, the table is
sized from what was parsed, two members are fetched once rather than
twice, glTF ids are stored at the width the format states them in, and
the crate root no longer makes one of two material vocabularies the
default spelling of the word.
…stable

The coverage gate named six lines. Four were the ceiling on the material
table, which no test can reach: it sits at two hundred and fifty-six
megabytes of materials and nothing builds a table that size.

The geometry ceiling beside it had already answered this by being a
function rather than an inline comparison - a small check tested at its
boundary, so the call site is one line that always runs. The material
ceiling has that shape now, tested at the last row it allows and the
first it does not.

Its documentation claims two probes, and both were run: loosening the
comparison lets the exactly-full table take one more row, and removing
it lets a table past the ceiling through. A comment claiming a probe
nobody ran is the same defect as a test that asserts nothing.

The other two lines were fixtures too rich to reach the format's own
defaults. The whole-material fixture states a normal scale and an
occlusion strength, so the branch that supplies one when the document
does not was reached by nothing - and a default that drifted would have
been a value this crate invented with nothing to say so. A mesh with no
primitives, which pairs with nothing and is not a refusal, had no test
either.
@CagdasErturk
CagdasErturk merged commit 529798f into main Sep 9, 2026
31 checks passed
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.

1 participant