# Native-grid material studio

## What this is

`../scripts/pixel_studio.py` renders an **original procedural native-grid illustration**: a small research desk with an articulated lamp, ceramic sphere, beveled machined-steel block, glass light capsule, notebook, cable and cast/contact shadows. It is not hand-drawn artwork, a mosaic filter, a downloaded game asset, an image-model output or a physically certified renderer.

The illustration palette is separate from the DSM brand palette. A host guide may use approved DSM tokens around the image; the scene's material/albedo colors are not new brand roles. No brand logo, proprietary font, system font or third-party artwork is bundled. The tiny labels use simple glyph patterns defined in the script. Their raster text is decorative evidence inside the illustration, not a replacement for readable HTML descriptions or controls.

## Requirements and quick start

- Python **3.9+**; standard library plus **Pillow >=9.1,<13**.
- Verified here with **Python 3.9.6 / Pillow 11.3.0** on macOS. No numpy, compiler, GPU, network access, cache directory or external assets are needed at runtime.
- To reproduce the verified dependency exactly, install `Pillow==11.3.0` into a project-local virtual environment. This pin is compatible with the tested Python. No global install is needed.
- Paths are relative to the current directory or derived from `__file__` in tests. No symlinks to caches or machine-specific paths are used.

From the repository root:

```sh
# Optional isolated setup, only if Pillow is missing in your chosen interpreter:
python3 -m venv .venv-pixel
.venv-pixel/bin/python -m pip install 'Pillow==11.3.0'
# Windows: use .venv-pixel\Scripts\python.exe instead.

python3 skills/pixel-craft/scripts/pixel_studio.py --help
python3 skills/pixel-craft/scripts/pixel_studio.py \
  --suite --scale 3 --out-dir skills/pixel-craft/assets/studies
python3 -m unittest discover -s skills/pixel-craft/tests -p test_pixel_studio.py -v
```

Use the virtual environment's Python in place of `python3` if you created it. For a standalone copied skill directory, run the same commands with `scripts/pixel_studio.py`, `tests` and `assets/studies` (remove `skills/pixel-craft/`). `PYTHONDONTWRITEBYTECODE=1` is an optional POSIX prefix when avoiding runtime `__pycache__` files; it is not necessary for correctness. Do not package interpreter caches or the virtual environment.

Examples:

```sh
python3 scripts/pixel_studio.py --preset neutral --out-dir output
python3 scripts/pixel_studio.py --preset cool --scale 4 --out-dir output
python3 scripts/pixel_studio.py --preset warm --palette-lock --out-dir output
python3 scripts/pixel_studio.py --preset warm --failure --out-dir output
```

`--out-dir` defaults to `pixel-studies` under the current directory. Existing files of the same name are overwritten. `--scale` accepts integers **1 through 12**. Invalid presets, noninteger/out-of-range scales and contradictory suite flags return an argparse error with exit code 2. `--suite` emits all prescribed cases and cannot be combined with `--palette-lock`, `--failure` or a nondefault `--preset`.

Every case writes a native RGB PNG, an exact nearest-neighbor `@Nx.png`, and a JSON record. A single case writes `<name>.json`; the full suite writes `suite.json`. PNGs carry an explicit sRGB chunk. The manifest records decoded-pixel readback results, dimensions and actual unique RGB counts. Preservation is verified by direct pixel comparison; no checksums are needed. Both native and enlarged exports are decoded and compared against the render before success is reported. Reproducibility is checked on the tested software; do not promise identical compressed file bytes across Pillow/zlib versions.

## Ready-to-embed outputs

All links are relative to this reference document. The five individual scenes are **384×216**; the two-panel comparison is **768×216**. At 3× they are **1152×648** and **2304×648**, respectively.

| Study | Native | Enlarged | Actual unique RGB colors |
|---|---|---|---:|
| Warm key / cool fill | [warm.png](../assets/studies/warm.png) | [warm@3x.png](../assets/studies/warm@3x.png) | 586 |
| Cool key / warm practical | [cool.png](../assets/studies/cool.png) | [cool@3x.png](../assets/studies/cool@3x.png) | 592 |
| Neutral illumination study | [neutral.png](../assets/studies/neutral.png) | [neutral@3x.png](../assets/studies/neutral@3x.png) | 521 |
| Exact authored palette lock | [warm-locked.png](../assets/studies/warm-locked.png) | [warm-locked@3x.png](../assets/studies/warm-locked@3x.png) | 27 |
| Deliberately incorrect lighting | [warm-failure.png](../assets/studies/warm-failure.png) | [warm-failure@3x.png](../assets/studies/warm-failure@3x.png) | 617 |
| Failure / correction, left / right | [comparison.png](../assets/studies/comparison.png) | [comparison@3x.png](../assets/studies/comparison@3x.png) | 1105 |

The metadata contract is [suite.json](../assets/studies/suite.json). The initial matte-looking metal revision is preserved separately as [iteration-before-steel@3x.png](../assets/studies/iteration-before-steel@3x.png); it is critique evidence, not the current render.

For a browser guide under `dsm/pixel-craft/`, the native warm image path is `../../skills/pixel-craft/assets/studies/warm.png`. Use `image-rendering: pixelated` and integer CSS sizes when exact displayed blocks matter. A responsive noninteger fit can remain convenient but is not an integer-grid integrity guarantee. Keep concise alt text and the material/lighting explanation in real HTML. Optional CRT/VHS/glitch belongs on a bounded display layer, never in these clean native masters.

Suggested alt: “A dark research desk: a warm-lit ceramic sphere, a beveled steel block with a narrow reflection, and a cyan emitter visible through a glass capsule; shadows fall to the right.”

## Color and lighting pipeline

1. **Compose on the native grid.** Draw polygons, stepped ellipses, glyphs and individual shaded surface samples directly at 384×216. Do not render high resolution and pixelate afterward. The lamp and notebook establish a specific still-life setting; the sphere owns the broad bright mass; metal supplies a narrower secondary highlight; emissive color stays local.
2. **Keep albedos stable.** `BASE` defines ceramic `(209,205,188)`, steel `(137,151,155)`, floor `(105,103,99)` and enamel `(64,76,78)`. These are artist-authored encoded-sRGB values, not measured reflectance spectra. Every preset uses the same material bases and geometry. “Neutral” refers to the key/fill chromaticity, not a mandatory gray-world grade: enamel/glass and illustration ink retain their authored tints.
3. **Decode before multiplying or mixing light.** The sRGB transfer uses `c/12.92` at `|c| <= 0.04045`, otherwise the signed `((|c|+0.055)/1.055)^2.4`; the inverse uses the `0.0031308` threshold. These constants and the extended signed treatment follow the W3C sample conversion code.[1]
4. **Evaluate normal/light relationships.** Coordinates are image-space: +x right, +y down, +z toward the viewer. The sphere's visible normal is `(nx,ny,sqrt(1-nx²-ny²))`; steel bevels have authored face normals. Diffuse uses `max(0,N·L)`. The Lambertian BRDF in PBRT is `R/pi`; this workbench absorbs that factor into artist-scaled light intensity rather than claiming calibrated radiometry.[2]
   Directional lighting and separate diffuse/specular/material parameters have documented primary API precedents.[3][4][5]
5. **Separate material highlights.** The artistic specular term is `ks * max(0,N·H)^p`, with `H=normalize(L+V)` and `V=(0,0,1)`. Blinn and Newell describe this half-direction high-power highlight in their 1976 paper and explicitly distinguish it from true reflections of surrounding objects.[6] Here ceramic uses a broad low-amplitude lobe; steel uses a concentrated lobe, tinted by base color, plus authored bevels. The specular amplitude is an illustration control, not an energy-conserving physical material parameter.
6. **Build intentional value clusters.** Quantize cosine response to 9 steps on ceramic, generally 10 elsewhere, and highlights to 8 steps. This is controlled lighting banding, not global color reduction. It produces several hundred RGB combinations because two colored lights, glass layering and other surfaces can mix. Exact palette lock is a separate final operation.
7. **Ground objects before decorating.** Project authored tabletop shadow ellipses opposite the key's horizontal direction, with stepped penumbra and denser contact. Visibility attenuates the key, not the whole scene; fill and ambient survive. This is image-space 2.5D compositing, not ray tracing. Overlapping masks are illustrative painter's-order projections, not a physically solved union of occluders.
8. **Make glass and emission distinguishable.** Glass transmits the previously drawn background through a slight tint; the rim uses the authored edge weight `0.04 + 0.96*(1-Nz)^5`, plus a narrow highlight. A thin emitter is visible inside the housing, and a small local floor spill is mixed in linear light. This edge heuristic is Fresnel-shaped, not a simulated dielectric. There is no refractive ray bending, spectral dispersion or caustic computation. Practical fill is an artistic directional surrogate; the visible capsule contributes bounded floor spill, not general global illumination.
9. **Encode once at the output of each lighting/compositing operation.** Clamp to display range, apply the inverse sRGB transfer and round to 8-bit RGB. Later local blends decode these stored pixels again; intermediate 8-bit rounding is a known approximation. No HDR tone mapper, display calibration or gamut-mapping transform is claimed.
10. **Apply semantic marks last.** Three small top-right status marks are unlit overlays. They are not surface reflectances or physical emitters. Optional palette locking preserves only marked semantic pixels, not every pixel resembling an accent color.

### Light presets

`PRESETS` stores light RGB **in linear light**, not encoded hex and not color-temperature measurements. All keys use normalized `(-0.65,-0.7,0.8)`; fills use normalized `(0.85,-0.15,0.5)`. Intensities are artistic, dimensionless controls.

| Preset | Key RGB / intensity | Fill RGB / intensity | Capsule encoded sRGB |
|---|---|---|---|
| `warm` | `(1,.76,.48)` / `1.05` | `(.32,.57,1)` / `.34` | `(94,221,204)` |
| `cool` | `(.43,.68,1)` / `1.12` | `(1,.60,.29)` / `.27` | `(255,192,109)` |
| `neutral` | `(1,1,1)` / `.9` | `(1,1,1)` / `.25` | `(224,226,218)` |

For directional experiments, import the script and call `render_scene('neutral', key_direction=(0.65,-0.7,0.8))`. Zero, nonfinite and non-three-component directions are rejected. Plausible tabletop shadows assume a key above the scene (`y < 0`); arbitrary below-table directions are not a general shadow solver. The changed-direction test checks both sphere highlight movement and the tabletop region. The composition's drawn lamp stays put: a changed API direction should be read as an external study light, not as a relocated physical lamp.

### Authored ramps and semantic reservation

`RAMPS` is the explicit palette source of truth: nine neutral entries, seven warm, seven cool and five emission entries. `LOCK_PALETTE` flattens these **28** nonsemantic colors in stable tie-breaking order. The semantic colors are `signal=(82,217,220)`, `warning=(255,191,89)`, `ready=(144,207,164)`; total allowed lock membership is **31** colors. The current warm-locked scene actually uses **27**, not all 31.

`lock_palette(image, reserved_mask)` selects the nearest permitted ramp entry by squared distance in **linear RGB**, without dithering. This metric is not perceptually uniform. Marked pixels are preserved only if their RGB exactly matches a known semantic accent. Accent colors are excluded from normal matching: a highlight cannot accidentally consume the “warning” role.

**Palette lock trades color fidelity for exact color membership.** It does not preserve the exact light/material response. The warm locked sphere gets larger, more saturated warm/gray clusters; subtle fill variation compresses, the tabletop shifts warmer and the steel/glass can lose intermediate values. Do not describe the exact-palette version as a more color-accurate renderer. Keep the unlocked masters as the lighting reference, and choose lock only when the brief values constrained ramps over smooth light relationships.

## Failure/correction lesson

`--failure` intentionally evaluates material lighting on encoded sRGB channel values instead of linear components and omits the computed tabletop cast/contact visibility mask. Geometry, material bases, light directions, ink, semantic accents and emitter layout remain fixed. Some graphic support/base shapes remain dark; “no shadow” in the tiny image label means the calculated tabletop shadow field is disabled, not that every dark pixel disappears.

In the comparison, the **left** sphere has a darker, more saturated terminator and an exaggerated warm highlight; the unshadowed floor makes the objects feel less grounded. The **right** has the corrected transfer-function workflow and the directional shadow field. The left can still look stylistically appealing: aesthetic preference is not evidence that encoded-channel lighting represents the same light mixture. This comparison deliberately changes two mistakes together; it is not a one-variable photometric experiment. To isolate mixing, use the unit test's known half-black/half-white blend: linear mixing encodes to `(188,188,188)`, not `(128,128,128)`.

## Critique and correction record

The first actual rendered warm and palette-locked PNGs were inspected visually. The sphere clearly held the hero mass, and the small glass capsule read as transmissive around an emissive core, but the steel face looked like matte painted plastic: its front-face normal variation missed the strongest half-vector direction and its highlight was too weak/broad.

A focused test was added **before** the material change. The first steel-strip RGB-sum range was **78**, failing the required range greater than 150. The correction increased front-face normal curvature, aligned its vertical normal nearer the key half-vector, and increased specular concentration/amplitude. The final strip range is **270**, with maximum channel **227**; the brighter part remains narrower than half the strip. The refined PNG was inspected again: a warm vertical reflection is now visibly distinct from the broad ceramic highlight. The cool preset changes that stripe to blue-white while its warm practical remains localized.

The before image is preserved; current warm/cool/locked/native/enlarged assets were regenerated after the change. This is agent visual critique and a numerical regression guard, not a human taste approval or an objective proof of material realism.

### Verification performed

- Initial test-first run failed because `pixel_studio.py` did not yet exist; tests were authored before the implementation.
- Final focused suite: **12 tests passed** with Python 3.9.6 / Pillow 11.3.0.
- Transfer-function roundtrip for every 8-bit input; known linear black/white and red/green blends; light-facing/opposed surface response.
- Identical repeated native render pixels; distinct presets; actual highlight and shadow changes with changed key direction.
- Exact locked membership and reserved accents, including prohibition of accent selection outside the mask.
- Integer scale API/CLI rejection paths; invalid preset/direction handling; real CLI PNG write/decode; explicit sRGB metadata.
- Every pixel of the 3× warm export checked for exact constant source-pixel blocks, not just a downsample.
- All **12 suite PNGs** decoded; every nearest export matched the corresponding full nearest expansion; dimensions and unique-color totals agreed with `suite.json`; direct decoded-pixel readback passed.
- Fresh-location check copied **only the script** into a new directory with spaces in its name, ran the suite with a separate spaced output path and **2×** scaling, and decoded all **12** PNGs. All native pixels and every expanded pixel matched; no art or fonts were copied. The temporary directory was removed after the successful check.
- The steel refinement has its own actual failing-before/passing-after test. Current art was visually reviewed in warm, cool and failure/correction views.

Remaining limitations: shadows are stylized projections; metal lacks an environment map and energy conservation; glass lacks refraction and caustics; the emitter is an illustration, not photometry; wall/ink/notebook details are authored colors rather than fully relit geometry. PNG decoding and arithmetic checks do not prove monitor calibration, human color matching or taste mastery. Native-grid banding is deliberate but contour clusters can still benefit from manual pixel editing. This workbench teaches a reproducible material/lighting pipeline without claiming a complete physically based renderer or hand-authored pixel mastery.

## Sources

[1] https://www.w3.org/TR/css-color-4
[2] https://pbr-book.org/4ed/Reflection_Models/Diffuse_Reflection
[3] https://registry.khronos.org/OpenGL-Refpages/gl2.1/xhtml/glLight.xml
[4] https://registry.khronos.org/OpenGL-Refpages/gl2.1/xhtml/glMaterial.xml
[5] https://registry.khronos.org/OpenGL-Refpages/gl2.1/xhtml/glLightModel.xml
[6] https://www.microsoft.com/en-us/research/wp-content/uploads/1976/10/p542-blinn.pdf
