Skip to content

feat(@typegpu/color): Tonemapping functions - #2691

Open
Aquel32 wants to merge 5 commits into
software-mansion:mainfrom
Aquel32:feat/typegpu-color-tonemapping-functions
Open

feat(@typegpu/color): Tonemapping functions#2691
Aquel32 wants to merge 5 commits into
software-mansion:mainfrom
Aquel32:feat/typegpu-color-tonemapping-functions

Conversation

@Aquel32

@Aquel32 Aquel32 commented Jul 3, 2026

Copy link
Copy Markdown

What was added:

  • Most common tonemapping functions such as aces, hable, reinhard and neutral.
  • A new dev example (tonemapping) in the docs.
  • Dedicated WGSL code validation test.

Closes #1730

Copilot AI review requested due to automatic review settings July 3, 2026 13:39

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.

Pull request overview

Adds a set of common tonemapping operators to @typegpu/color and wires them into a new docs dev example, with an accompanying shader-generation snapshot test to ensure the emitted WGSL stays valid/stable.

Changes:

  • Introduces tonemapping functions (aces, hable, reinhard, neutral) as tgpu.fn utilities in @typegpu/color.
  • Exposes the new tonemapping API via the package entrypoint.
  • Adds a new docs example (image-processing/tonemapping) plus an individual example test that snapshots generated WGSL.

Reviewed changes

Copilot reviewed 6 out of 7 changed files in this pull request and generated 12 comments.

Show a summary per file
File Description
packages/typegpu-color/src/tonemapping.ts New tonemapping function implementations.
packages/typegpu-color/src/index.ts Re-exports tonemapping functions from the package entrypoint.
apps/typegpu-docs/tests/individual-example-tests/tonemapping.test.ts Adds an example test that snapshots generated WGSL for the tonemapping demo.
apps/typegpu-docs/src/examples/image-processing/tonemapping/meta.json Registers the new docs example metadata.
apps/typegpu-docs/src/examples/image-processing/tonemapping/index.ts Implements the new docs demo using the added tonemapping functions.
apps/typegpu-docs/src/examples/image-processing/tonemapping/index.html Adds the canvas host for the new example.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread packages/typegpu-color/src/tonemapping.ts Outdated
Comment thread packages/typegpu-color/src/tonemapping.ts Outdated
Comment thread packages/typegpu-color/src/tonemapping.ts Outdated
Comment thread packages/typegpu-color/src/tonemapping.ts Outdated
Comment thread packages/typegpu-color/src/tonemapping.ts Outdated
Comment thread apps/typegpu-docs/src/examples/image-processing/tonemapping/index.ts Outdated
Comment thread apps/typegpu-docs/src/examples/image-processing/tonemapping/index.ts Outdated
Comment thread apps/typegpu-docs/src/examples/image-processing/tonemapping/index.ts Outdated
Comment thread apps/typegpu-docs/src/examples/image-processing/tonemapping/index.ts Outdated

@pullfrog pullfrog Bot 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.

Important

Two new files use import tgpu from 'typegpu', which fails the repository's internal no-tgpu-default-import lint rule. The PR also needs to run npx oxfmt --write over the changed files before CI will pass.

Reviewed changes — added four GPU tonemapping functions to @typegpu/color, a dev docs example, and an inline-snapshot WGSL smoke test.

  • Add aces, hable, reinhard, and neutral tonemapping helpers in packages/typegpu-color/src/tonemapping.ts.
  • Export the new functions from packages/typegpu-color/src/index.ts.
  • Add a dev example under apps/typegpu-docs/src/examples/image-processing/tonemapping/ with controls and a side-by-side comparison view.
  • Add a WGSL code-generation test for the example in apps/typegpu-docs/tests/individual-example-tests/tonemapping.test.ts.

⚠️ Default import tgpu will fail CI

Both packages/typegpu-color/src/tonemapping.ts and the new docs example import tgpu as the default member (import tgpu from 'typegpu'). The internal lint rule eslint-plugin-internal/no-tgpu-default-import is enabled as an error and flags exactly this pattern.

Technical details
# Default `import tgpu` will fail CI

## Affected sites
- `packages/typegpu-color/src/tonemapping.ts:1``import tgpu from "typegpu";`
- `apps/typegpu-docs/src/examples/image-processing/tonemapping/index.ts:1``import tgpu, { common, d, std } from 'typegpu';`

## Required outcome
- Replace both with `import { tgpu } from 'typegpu';` (and include `common`, `d`, `std` in the named import for the example).
- CI lint must pass after the change.

## Suggested approach
In `packages/typegpu-color/src/tonemapping.ts`: replace line 1 with `import { tgpu } from 'typegpu';`.
In `apps/typegpu-docs/src/examples/image-processing/tonemapping/index.ts`: replace line 1 with `import { tgpu, common, d, std } from 'typegpu';`.

## Open questions for the human
None.

⚠️ Formatting check fails across all changed text files

npx oxfmt --check reports formatting issues in all four changed .ts/.test.ts files. Running npx oxfmt --write packages/typegpu-color/src/tonemapping.ts packages/typegpu-color/src/index.ts apps/typegpu-docs/src/examples/image-processing/tonemapping/index.ts apps/typegpu-docs/tests/individual-example-tests/tonemapping.test.ts fixes them automatically.

Technical details
# Formatting check fails across all changed text files

## Affected sites
- `packages/typegpu-color/src/tonemapping.ts` — double quotes, trailing whitespace, missing trailing commas, missing EOF newline.
- `packages/typegpu-color/src/index.ts` — missing EOF newline.
- `apps/typegpu-docs/src/examples/image-processing/tonemapping/index.ts` — double quotes, `else if` spacing, missing EOF newline.
- `apps/typegpu-docs/tests/individual-example-tests/tonemapping.test.ts` — double quotes, missing trailing commas.

## Required outcome
All changed text files must pass `oxfmt --check`.

## Suggested approach
Run `npx oxfmt --write` on the four files listed above.

## Open questions for the human
None.

Pullfrog  | Fix all ➔Fix 👍s ➔View workflow run | Using Kimi K2 (free via Pullfrog for OSS) | 𝕏

Comment thread packages/typegpu-color/src/tonemapping.ts Outdated
Comment thread apps/typegpu-docs/src/examples/image-processing/tonemapping/index.ts Outdated
@cieplypolar

Copy link
Copy Markdown
Collaborator

Running pnpm run fix will resolve most of the issues. It starts by running oxlint --fix. It will list problems that cannot be automatically fixed. Once you have corrected those, a second run with format everything using oxfmt according to our repository standard 🫡 .

@iwoplaza iwoplaza changed the title feat (@typegpu/color): Tonemapping functions feat(@typegpu/color): Tonemapping functions Jul 10, 2026

@aleksanderkatan aleksanderkatan left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Great changes! 🎵🗺️

)((rgb) => {
'use gpu';

return saturate(rgb / (vec3f(1.0) + rgb));

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

nit

Suggested change
return saturate(rgb / (vec3f(1.0) + rgb));
return saturate(rgb / (1 + rgb));


const W = vec3f(11.2);

return saturate(hableCurve(rgb) / hableCurve(W));

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I found one implementation that multiplies rgb by 2 here, which one is correct?
https://github.com/GPUOpen-LibrariesAndSDKs/Cauldron/blob/master/src/VK/shaders/tonemappers.glsl


let color = rgb - offset;

const peak = max(color.r, max(color.g, color.b));

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

nit: our max supports this

Suggested change
const peak = max(color.r, max(color.g, color.b));
const peak = max(color.r, color.g, color.b);

return saturate(color);
}

const d = 1.0 - startCompression;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

nit: Using 1.0 instead of 1 does not change how we interpret numbers, in fact it may even be misleading since all of those are abstract ints, they just get converted to floats based on how they are used

Image

"title": "Tonemapping",
"category": "image-processing",
"tags": ["ecosystem", "color", "tonemapping"],
"dev": true,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I think it may be a good showcase, and it may make your functions more discoverable
Not every example needs to be 2k lines long

Suggested change
"dev": true,

Comment on lines +134 to +151
'Point Light X': {
initial: 0.5,
min: 0,
max: 1,
step: 0.01,
onSliderChange: (value: number) => {
pointLightX.write(value);
},
},
'Point Light Y': {
initial: 0.5,
min: 0,
max: 1,
step: 0.01,
onSliderChange: (value: number) => {
pointLightY.write(value);
},
},

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

You could use vector slider if you wanted to
Also, this would allow merging pointLightX and pointLightY buffers into one without using partial writes/keeping CPU state

Suggested change
'Point Light X': {
initial: 0.5,
min: 0,
max: 1,
step: 0.01,
onSliderChange: (value: number) => {
pointLightX.write(value);
},
},
'Point Light Y': {
initial: 0.5,
min: 0,
max: 1,
step: 0.01,
onSliderChange: (value: number) => {
pointLightY.write(value);
},
},
'Point Pos': {
initial: d.vec2f(0.5),
min: d.vec2f(),
max: d.vec2f(1),
step: d.vec2f(0.01),
onVectorSliderChange: (value) => {
pointLightX.write(value.x);
pointLightY.write(value.y);
},
},

})(({ uv }) => {
'use gpu';

const brightness = pointLightScale.$ / std.length(uv - d.vec2f(pointLightX.$, pointLightY.$));

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

really nit: this would make it more verbose

Suggested change
const brightness = pointLightScale.$ / std.length(uv - d.vec2f(pointLightX.$, pointLightY.$));
const brightness = pointLightScale.$ / std.distance(uv, d.vec2f(pointLightX.$, pointLightY.$));

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

If you find this view interesting, here's a code for it (no pressure to include it, I was just curious to how it would look like)

Image Image
const mappings = {
  0: (v: d.v3f) => {
    'use gpu';
    return d.vec3f(v);
  },
  1: aces,
  2: hable,
  3: reinhard,
  4: neutral,
} as const;
const COUNT = Object.keys(mappings).length;
const timeUniform = root.createUniform(d.f32);

const mainFragment = tgpu.fragmentFn({
  in: { uv: d.vec2f },
  out: d.vec4f,
})(({ uv }) => {
  'use gpu';

  const brightness = pointLightScale.$ / std.distance(uv, d.vec2f(pointLightX.$, pointLightY.$));
  let color = pointLightColor.$ * brightness;

  const z = uv - 0.5;
  const angle = std.atan2(z.y, z.x) + Math.PI;
  const segment = (Math.PI * 2) / COUNT;

  for (const i of tgpu.unroll(std.range(COUNT))) {
    if (i * segment <= angle && angle < (i + 1) * segment) {
      color = mappings[i as keyof typeof mappings](color);
    }
  }

  return d.vec4f(color, 1.0);
});

Also, here's how it looks when moving automatically:

Screen.Recording.2026-08-20.at.13.00.55.mov

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.

feat: Add tonemapping functions to @typegpu/color

4 participants