+
+
+
+
+{{{example url="../webgpu-post-processing-3d-lookup-table(lut).html"}}}
+
+
+
+
+
diff --git a/webgpu/lessons/webgpu-image-adjustments.css b/webgpu/lessons/webgpu-image-adjustments.css
new file mode 100644
index 00000000..6606efe5
--- /dev/null
+++ b/webgpu/lessons/webgpu-image-adjustments.css
@@ -0,0 +1,21 @@
+/* intentionally empty at the moment */
+
+.img-grid[data-diagram] {
+ display:grid;
+ grid-template-columns:repeat(auto-fit,250px); /* same as column width */
+ gap:10px; /* same as column gap */
+}
+.img-grid-cols {
+ columns: 250px;
+ column-gap: 1em;
+ grid-column: 1/-1; /* take all the columns of the grid*/
+}
+.img-grid-item {
+ display: inline-flex;
+ flex-direction: column;
+ margin-bottom: 0.5em;
+}
+.img-grid img {
+ width: 100%;
+ border: 1px solid #888;
+}
\ No newline at end of file
diff --git a/webgpu/lessons/webgpu-image-adjustments.js b/webgpu/lessons/webgpu-image-adjustments.js
new file mode 100644
index 00000000..09ab2fed
--- /dev/null
+++ b/webgpu/lessons/webgpu-image-adjustments.js
@@ -0,0 +1,53 @@
+import {
+ renderDiagrams
+} from './resources/diagrams.js';
+import {
+ createElem as el
+} from './resources/elem.js';
+
+async function luts(elem) {
+ const luts = [
+ { name: 'monochrome', url: 'resources/images/lut/monochrome-s8.png' },
+ { name: 'sepia', url: 'resources/images/lut/sepia-s8.png' },
+ { name: 'saturated', url: 'resources/images/lut/saturated-s8.png', },
+ { name: 'posterize', url: 'resources/images/lut/posterize-s8n.png', },
+ { name: 'posterize-3-rgb', url: 'resources/images/lut/posterize-3-rgb-s8n.png', },
+ { name: 'posterize-3-lab', url: 'resources/images/lut/posterize-3-lab-s8n.png', },
+ { name: 'posterize-4-lab', url: 'resources/images/lut/posterize-4-lab-s8n.png', },
+ { name: 'posterize-more', url: 'resources/images/lut/posterize-more-s8n.png', },
+ { name: 'inverse', url: 'resources/images/lut/inverse-s8.png', },
+ { name: 'color negative', url: 'resources/images/lut/color-negative-s8.png', },
+ { name: 'high contrast', url: 'resources/images/lut/high-contrast-bw-s8.png', },
+ { name: 'funky contrast', url: 'resources/images/lut/funky-contrast-s8.png', },
+ { name: 'nightvision', url: 'resources/images/lut/nightvision-s8.png', },
+ { name: 'thermal', url: 'resources/images/lut/thermal-s8.png', },
+ { name: 'b/w', url: 'resources/images/lut/black-white-s8n.png', },
+ { name: 'hue +60', url: 'resources/images/lut/hue-plus-60-s8.png', },
+ { name: 'hue +180', url: 'resources/images/lut/hue-plus-180-s8.png', },
+ { name: 'hue -60', url: 'resources/images/lut/hue-minus-60-s8.png', },
+ { name: 'red to cyan', url: 'resources/images/lut/red-to-cyan-s8.png' },
+ { name: 'blues', url: 'resources/images/lut/blues-s8.png' },
+ { name: 'infrared', url: 'resources/images/lut/infrared-s8.png' },
+ { name: 'radioactive', url: 'resources/images/lut/radioactive-s8.png' },
+ { name: 'goolgey', url: 'resources/images/lut/googley-s8.png' },
+ { name: 'bgy', url: 'resources/images/lut/bgy-s8.png' },
+ ];
+
+ elem.append(
+ el('div', { className: 'img-grid-cols' }, luts.map(({ name, url}) =>
+ el('div', { className: 'img-grid-item' }, [
+ el('img', { src: `/webgpu/${url}`, alt: name }),
+ el('div', { textContent: name }),
+ ]))
+ )
+ );
+}
+
+
+async function main() {
+ renderDiagrams({
+ luts,
+ });
+}
+
+main();
diff --git a/webgpu/lessons/webgpu-image-adjustments.md b/webgpu/lessons/webgpu-image-adjustments.md
new file mode 100644
index 00000000..66e49c14
--- /dev/null
+++ b/webgpu/lessons/webgpu-image-adjustments.md
@@ -0,0 +1,26 @@
+Title: WebGPU Post Processing - Image Adjustments
+Description: Image Adjustments
+TOC: Image Adjustments
+
+In previous article we covered how to do [post processing](webgpu-post-processing.html). Some common operations to
+want to do are often called, image adjustments as seen in
+image editing programs like Photoshop, gIMP, Affinity Photo, etc...
+
+In preparation, lets make an example that load an image and has
+a post processing step. This wil be effectively the first part
+of [the previous article](webgpu-post-processing.html) merged
+with our example of loading an image from
+[the article on loading images into textures](webgpu-importing-textures.html).
+
+
+
+
+
+
+
+{{{example url="../webgpu-post-processing-3d-lookup-table(lut).html"}}}
+
+
+
+
+
diff --git a/webgpu/lessons/webgpu-post-processing.md b/webgpu/lessons/webgpu-post-processing.md
index ef9d698d..e892c04f 100644
--- a/webgpu/lessons/webgpu-post-processing.md
+++ b/webgpu/lessons/webgpu-post-processing.md
@@ -5,7 +5,7 @@ TOC: Basic CRT Effect
Post Processing just means to do some processing after you've created the "original" image.
Post processing can apply to a photo, a video, a 2d scene, a 3d scene. It just generally
means you have an image and you apply some effects to that image, like choosing a filter
-in Instagram.
+on Instagram.
In almost every example on this site we render to the canvas texture. To do post processing
we instead render to a different texture. Then render that texture to the canvas while
@@ -720,7 +720,6 @@ This works
This is much faster! But, unfortunately, on some GPUs it is still slower than using a render pass.
-
@@ -750,6 +749,12 @@ benefit from the shared data of workgroups and or subgroups. GPUs have been rend
to textures for much longer than they've been running compute shaders so many things
about that process are highly optimized.
+---
+
+This article introduced the concept of *post processing*.
+In the next article we'll cover some
+[common post processing image adjustments](webgpu-image-adjustments.html).
+
diff --git a/webgpu/resources/js/on-paste-image.js b/webgpu/resources/js/on-paste-image.js
new file mode 100644
index 00000000..a86e58eb
--- /dev/null
+++ b/webgpu/resources/js/on-paste-image.js
@@ -0,0 +1,13 @@
+export default function onPasteImage(fn) {
+ document.addEventListener('paste', (e) => {
+ e.preventDefault();
+ const items = (event.clipboardData || event.originalEvent.clipboardData).items;
+ for (let i = 0; i < items.length; i++) {
+ if (items[i].kind === 'file' && items[i].type.startsWith('image/')) {
+ const blob = items[i].getAsFile(); // Get the image as a Blob (File object)
+ fn(blob);
+ break;
+ }
+ }
+ });
+}
\ No newline at end of file
diff --git a/webgpu/resources/js/pick-image.js b/webgpu/resources/js/pick-image.js
new file mode 100644
index 00000000..5f43245e
--- /dev/null
+++ b/webgpu/resources/js/pick-image.js
@@ -0,0 +1,28 @@
+const fileElem = document.createElement('input');
+fileElem.type = 'file';
+fileElem.accept = 'image/*';
+
+let resolve;
+
+const finish = (file) => {
+ console.log('finish:', file);
+ fileElem.removeEventListener('change', onChange);
+ fileElem.removeEventListener('cancel', onCancel);
+ const r = resolve;
+ resolve = undefined;
+ r(file);
+};
+
+const onChange = fileElem.addEventListener('change', e => {
+ finish(e.target.files[0]);
+});
+const onCancel = () => finish();
+
+const pickImage = () => new Promise(_resolve => {
+ resolve = _resolve;
+ fileElem.addEventListener('change', onChange);
+ fileElem.addEventListener('cancel', onCancel);
+ fileElem.click();
+});
+
+export default pickImage;
diff --git a/webgpu/webgpu-post-processing-3d-lookup-table(lut).html b/webgpu/webgpu-post-processing-3d-lookup-table(lut).html
new file mode 100644
index 00000000..973e123e
--- /dev/null
+++ b/webgpu/webgpu-post-processing-3d-lookup-table(lut).html
@@ -0,0 +1,524 @@
+
+
+
+
+
+ WebGPU Post Processing - Step 1 - No-op
+
+
+
+
+
+
+
diff --git a/webgpu/webgpu-post-processing-image-adjustments-noop.html b/webgpu/webgpu-post-processing-image-adjustments-noop.html
new file mode 100644
index 00000000..ca2e96e9
--- /dev/null
+++ b/webgpu/webgpu-post-processing-image-adjustments-noop.html
@@ -0,0 +1,320 @@
+
+
+
+
+
+ WebGPU Post Processing - Image Adjustment - No-op
+
+
+
+
+