Skip to content

Color: setLinear, and the glTF sRGB bridge moves onto it - #1708

Merged
obiot merged 1 commit into
masterfrom
refactor/color-setlinear
Oct 3, 2026
Merged

obiot merged 1 commit into
masterfrom
refactor/color-setlinear

Conversation

@obiot

@obiot obiot commented Oct 3, 2026

Copy link
Copy Markdown
Member

src/level/gltf/srgb.js was a private 26-line module holding one function,
used only by the glTF loader. The conversion it does is a Color concern, not
a glTF one, so it moves onto Color and the module goes.

Why the conversion exists

glTF defines baseColorFactor and emissiveFactor as linear (spec
§3.9.2), while a melonJS tint is sRGB — the same space as a css colour or a
PNG texel. Handing the linear number straight through renders every untextured
material markedly too dark, by 60 to 70 counts per channel in the midtones.
It is easy to leave in, because the result still looks coherent, just moody,
so lighting gets tuned against the wrong values.

The API

Color#setLinear(r, g, b, alpha) is the sibling of the existing setFloat,
which takes the same 0..1 range but treats it as already sRGB. The two are
not interchangeable
: a linear 0.42 is sRGB 0.68, not 0.42. Alpha
carries no transfer function and is taken as-is.

// before, via the private module
mesh.tint.setColor(linearToSrgb8(f[0]), linearToSrgb8(f[1]), linearToSrgb8(f[2]));

// after
mesh.tint.setLinear(f[0], f[1], f[2]);

linearToSrgb is exported @internal, so it is stripped from the published
declarations and setLinear is the only public way in.

One behavioural difference

The old helper rounded to an 8-bit integer (Math.round(s * 255)) before
Color stored the value as a float. setLinear writes straight into
normalizedRGBA, so nothing is rounded through 8 bits on the way in. This
changes existing glTF material colours very slightly
— worth a look before
merge, since it is the only part of this that moves pixels.

Both the clamp and the transfer function are carried over unchanged: out-of-
range factors are clamped first, because a negative base under a fractional
power is NaN and would poison the whole colour rather than one channel.

Tests

tests/color.spec.ts covers the transfer function at its breakpoints, the
linear-versus-setFloat distinction, and clamping. tests/gltf-srgb.spec.js
no longer reaches into a private module and tests the loader through the
public API instead.

Full suite green, eslint 0 errors, biome clean.

🤖 Generated with Claude Code

https://claude.ai/code/session_01NGvtaUNATVCVxD2qcbiY4t

Copilot AI balanced review requested due to automatic review settings October 3, 2026 07:43

Copilot AI left a comment

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.

Copilot review overview

🟡 Changes recommended

NaN inputs still poison renderer-facing color data, and some public documentation incorrectly recommends conversion for emissive factors.

Review effort: Balanced
Findings: 2 Medium severity · 3 Low severity

Open (5)
What changed in this PR

Moves linear-to-sRGB conversion into the public Color#setLinear API and updates glTF tint handling.

Changes:

  • Adds Color#setLinear with focused tests.
  • Migrates glTF loaders and removes the private helper.
  • Documents correct baseColorFactor handling.
File Description
packages/​melonjs/​src/​math/​color.ts Adds linear-to-sRGB conversion API.
packages/​melonjs/​src/​level/​gltf/​GLTFScene.js Uses setLinear for scene tints.
packages/​melonjs/​src/​level/​gltf/​GLTFModel.js Uses setLinear for model tints.
packages/​melonjs/​src/​level/​gltf/​srgb.js Removes obsolete helper.
packages/​melonjs/​tests/​color.spec.ts Tests the new API.
packages/​melonjs/​tests/​gltf-srgb.spec.js Updates loader integration coverage.
packages/​melonjs/​skills/​melonjs-3d-assets/​SKILL.md Documents glTF color-space handling.
packages/​melonjs/​CHANGELOG.md Announces the API.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

linearToSrgb8(f[1]),
linearToSrgb8(f[2]),
);
mesh.tint.setLinear(f[0], f[1], f[2]);
const c = new Color().setLinear(-0.5, 2, Number.NaN);
expect(c.r).toBe(0);
expect(c.g).toBe(255);
expect(Number.isNaN(c.b)).toBe(false);
## [20.8.0] (melonJS 2) - _unreleased_

### Added
- `Color#setLinear(r, g, b, alpha)` sets a colour from LINEAR values, encoding them to sRGB. The sibling of `setFloat`, which takes the same `0..1` range and treats it as already sRGB: the two are not interchangeable, since a linear `0.42` is sRGB `0.68`. Reach for it whenever the numbers come from a renderer's own colour space rather than from a css string or an image, glTF's `baseColorFactor` and `emissiveFactor` being the common case. Handing those to `setFloat`, or scaling them by 255 into `setColor`, renders every untextured material markedly too dark, by 60 to 70 counts per channel in the midtones, and it is easy to leave in because the result still looks coherent
* A color manipulation object.
* @category Math
*/
/**
Comment on lines +362 to +369
* Reach for this whenever a value arrives from a renderer's own colour
* space rather than from a CSS string or an image. The common case is
* glTF, which defines `baseColorFactor` and `emissiveFactor` as linear
* (spec 3.9.2) — handing those straight to `setFloat` or scaling them by
* 255 into `setColor` renders every untextured material markedly too
* DARK, by 60 to 70 counts per channel in the midtones. It is an easy
* mistake to leave in, because the result still looks coherent, just
* moody, so lighting gets tuned against the wrong values.
glTF defines baseColorFactor and emissiveFactor as linear (spec 3.9.2),
while a melonJS tint is sRGB, so the loader carried a private module to
encode between them. That conversion is a Color concern rather than a glTF
one: Color#setLinear is the public way in, the sibling of setFloat, which
takes the same 0..1 range but treats it as already sRGB. The two are not
interchangeable, since a linear 0.42 is sRGB 0.68.

src/level/gltf/srgb.js is deleted and both call sites use setLinear. The
old helper rounded to an 8-bit integer before Color stored it as a float;
setLinear writes straight into normalizedRGBA, so nothing is rounded
through 8 bits on the way in.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01NGvtaUNATVCVxD2qcbiY4t
Copilot AI balanced review requested due to automatic review settings October 3, 2026 07:53
@obiot
obiot force-pushed the refactor/color-setlinear branch from e239df8 to 427edb5 Compare October 3, 2026 07:53

Copilot AI left a comment

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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@obiot
obiot merged commit a71d207 into master Oct 3, 2026
6 checks passed
@obiot
obiot deleted the refactor/color-setlinear branch October 3, 2026 08:00
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