@three-flatland/normals 0.1.0-alpha.1 → 0.1.0-alpha.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/bake.js CHANGED
@@ -1,123 +1,158 @@
1
- import {
2
- DEFAULT_BUMP,
3
- DEFAULT_PITCH,
4
- DEFAULT_STRENGTH,
5
- resolveRegion
6
- } from "./descriptor.js";
1
+ import { DEFAULT_BUMP, DEFAULT_PITCH, resolveRegion } from "./descriptor.js";
2
+ //#region src/bake.ts
3
+ /**
4
+ * Produce a tangent-space normal map from source pixels + a descriptor.
5
+ *
6
+ * Cross-platform: browser + node. No filesystem, no pngjs. Used by
7
+ * the CLI baker (wrapped with file I/O) and the runtime loader
8
+ * (called directly on decoded image pixels).
9
+ *
10
+ * Per texel in each region:
11
+ * 1. `bump: 'alpha'` — central-difference gradient on the source
12
+ * alpha channel, clamped to the region bounds so adjacent regions
13
+ * (e.g. atlas cells) can't bleed into each other.
14
+ * 2. Compose with a tilt rotation that takes `+Z` to the region's
15
+ * `direction` at `pitch` radians.
16
+ * 3. Normalize and encode to `rgb = normal * 0.5 + 0.5`. Alpha is
17
+ * copied from the source so the baked map carries its own
18
+ * silhouette.
19
+ *
20
+ * Texels outside every region receive the flat normal `(0, 0, 1)`
21
+ * with the source's alpha — safe default for sparse descriptors.
22
+ *
23
+ * @param pixels RGBA pixel buffer, row-major, 4 bytes per pixel.
24
+ * @param width Pixel width.
25
+ * @param height Pixel height.
26
+ * @param descriptor Region / tilt / bump control. Defaults to a single
27
+ * whole-texture flat region with alpha-derived bump.
28
+ * @returns RGBA pixel buffer containing the encoded normal map.
29
+ */
7
30
  function bakeNormalMap(pixels, width, height, descriptor = {}) {
8
- const out = new Uint8Array(pixels.length);
9
- for (let i = 0; i < pixels.length; i += 4) {
10
- out[i] = 128;
11
- out[i + 1] = 128;
12
- out[i + 2] = 0;
13
- out[i + 3] = pixels[i + 3];
14
- }
15
- const regions = descriptor.regions && descriptor.regions.length > 0 ? descriptor.regions : [{ x: 0, y: 0, w: width, h: height }];
16
- for (const region of regions) {
17
- const resolved = resolveRegion(region, descriptor);
18
- bakeRegion(pixels, out, width, resolved);
19
- }
20
- return out;
31
+ const out = new Uint8Array(pixels.length);
32
+ for (let i = 0; i < pixels.length; i += 4) {
33
+ out[i] = 128;
34
+ out[i + 1] = 128;
35
+ out[i + 2] = 0;
36
+ out[i + 3] = pixels[i + 3];
37
+ }
38
+ const regions = descriptor.regions && descriptor.regions.length > 0 ? descriptor.regions : [{
39
+ x: 0,
40
+ y: 0,
41
+ w: width,
42
+ h: height
43
+ }];
44
+ for (const region of regions) bakeRegion(pixels, out, width, resolveRegion(region, descriptor));
45
+ return out;
21
46
  }
47
+ /**
48
+ * Bake a single region into the output buffer. Region-local alpha
49
+ * clamping prevents cross-region gradient bleed.
50
+ */
22
51
  function bakeRegion(pixels, out, width, region) {
23
- const x0 = region.x;
24
- const y0 = region.y;
25
- const x1 = region.x + region.w;
26
- const y1 = region.y + region.h;
27
- const strength = region.strength;
28
- const bumpMode = region.bump;
29
- const bumpChannel = bumpMode === "alpha" ? 3 : bumpMode === "red" ? 0 : bumpMode === "green" ? 1 : bumpMode === "blue" ? 2 : -1;
30
- const useLuminance = bumpMode === "luminance";
31
- const useBump = bumpChannel >= 0 || useLuminance;
32
- let r00 = 1, r01 = 0, r02 = 0;
33
- let r10 = 0, r11 = 1, r12 = 0;
34
- let r20 = 0, r21 = 0, r22 = 1;
35
- const hasTilt = region.angle !== null;
36
- if (hasTilt) {
37
- const theta = region.angle;
38
- const pitch = region.pitch;
39
- const ax = -Math.sin(theta);
40
- const ay = Math.cos(theta);
41
- const c = Math.cos(pitch);
42
- const s = Math.sin(pitch);
43
- const t = 1 - c;
44
- r00 = c + ax * ax * t;
45
- r01 = ax * ay * t;
46
- r02 = ay * s;
47
- r10 = ay * ax * t;
48
- r11 = c + ay * ay * t;
49
- r12 = -ax * s;
50
- r20 = -ay * s;
51
- r21 = ax * s;
52
- r22 = c;
53
- }
54
- const heightAt = (x, y) => {
55
- const cx = x < x0 ? x0 : x >= x1 ? x1 - 1 : x;
56
- const cy = y < y0 ? y0 : y >= y1 ? y1 - 1 : y;
57
- const base = (cy * width + cx) * 4;
58
- if (useLuminance) {
59
- return (0.2126 * pixels[base] + 0.7152 * pixels[base + 1] + 0.0722 * pixels[base + 2]) / 255;
60
- }
61
- return pixels[base + bumpChannel] / 255;
62
- };
63
- for (let y = y0; y < y1; y++) {
64
- for (let x = x0; x < x1; x++) {
65
- const idx = (y * width + x) * 4;
66
- let lx;
67
- let ly;
68
- let lz;
69
- if (useBump) {
70
- const hL = heightAt(x - 1, y);
71
- const hR = heightAt(x + 1, y);
72
- const hD = heightAt(x, y - 1);
73
- const hU = heightAt(x, y + 1);
74
- const dx = (hR - hL) * strength;
75
- const dy = (hU - hD) * strength;
76
- lx = -dx;
77
- ly = -dy;
78
- lz = 1;
79
- } else {
80
- lx = 0;
81
- ly = 0;
82
- lz = 1;
83
- }
84
- let nx;
85
- let ny;
86
- let nz;
87
- if (hasTilt) {
88
- nx = r00 * lx + r01 * ly + r02 * lz;
89
- ny = r10 * lx + r11 * ly + r12 * lz;
90
- nz = r20 * lx + r21 * ly + r22 * lz;
91
- } else {
92
- nx = lx;
93
- ny = ly;
94
- nz = lz;
95
- }
96
- const len = Math.hypot(nx, ny, nz);
97
- nx /= len;
98
- ny /= len;
99
- nz /= len;
100
- out[idx] = Math.round((nx * 0.5 + 0.5) * 255);
101
- out[idx + 1] = Math.round((ny * 0.5 + 0.5) * 255);
102
- out[idx + 2] = Math.round(region.elevation * 255);
103
- out[idx + 3] = pixels[idx + 3];
104
- }
105
- }
52
+ const x0 = region.x;
53
+ const y0 = region.y;
54
+ const x1 = region.x + region.w;
55
+ const y1 = region.y + region.h;
56
+ const strength = region.strength;
57
+ const bumpMode = region.bump;
58
+ const bumpChannel = bumpMode === "alpha" ? 3 : bumpMode === "red" ? 0 : bumpMode === "green" ? 1 : bumpMode === "blue" ? 2 : -1;
59
+ const useLuminance = bumpMode === "luminance";
60
+ const useBump = bumpChannel >= 0 || useLuminance;
61
+ let r00 = 1, r01 = 0, r02 = 0;
62
+ let r10 = 0, r11 = 1, r12 = 0;
63
+ let r20 = 0, r21 = 0, r22 = 1;
64
+ const hasTilt = region.angle !== null;
65
+ if (hasTilt) {
66
+ const theta = region.angle;
67
+ const pitch = region.pitch;
68
+ const ax = -Math.sin(theta);
69
+ const ay = Math.cos(theta);
70
+ const c = Math.cos(pitch);
71
+ const s = Math.sin(pitch);
72
+ const t = 1 - c;
73
+ r00 = c + ax * ax * t;
74
+ r01 = ax * ay * t;
75
+ r02 = ay * s;
76
+ r10 = ay * ax * t;
77
+ r11 = c + ay * ay * t;
78
+ r12 = -ax * s;
79
+ r20 = -ay * s;
80
+ r21 = ax * s;
81
+ r22 = c;
82
+ }
83
+ const heightAt = (x, y) => {
84
+ const cx = x < x0 ? x0 : x >= x1 ? x1 - 1 : x;
85
+ const base = ((y < y0 ? y0 : y >= y1 ? y1 - 1 : y) * width + cx) * 4;
86
+ if (useLuminance) return (.2126 * pixels[base] + .7152 * pixels[base + 1] + .0722 * pixels[base + 2]) / 255;
87
+ return pixels[base + bumpChannel] / 255;
88
+ };
89
+ for (let y = y0; y < y1; y++) for (let x = x0; x < x1; x++) {
90
+ const idx = (y * width + x) * 4;
91
+ let lx;
92
+ let ly;
93
+ let lz;
94
+ if (useBump) {
95
+ const hL = heightAt(x - 1, y);
96
+ const hR = heightAt(x + 1, y);
97
+ const hD = heightAt(x, y - 1);
98
+ const hU = heightAt(x, y + 1);
99
+ const dx = (hR - hL) * strength;
100
+ const dy = (hU - hD) * strength;
101
+ lx = -dx;
102
+ ly = -dy;
103
+ lz = 1;
104
+ } else {
105
+ lx = 0;
106
+ ly = 0;
107
+ lz = 1;
108
+ }
109
+ let nx;
110
+ let ny;
111
+ let nz;
112
+ if (hasTilt) {
113
+ nx = r00 * lx + r01 * ly + r02 * lz;
114
+ ny = r10 * lx + r11 * ly + r12 * lz;
115
+ nz = r20 * lx + r21 * ly + r22 * lz;
116
+ } else {
117
+ nx = lx;
118
+ ny = ly;
119
+ nz = lz;
120
+ }
121
+ const len = Math.hypot(nx, ny, nz);
122
+ nx /= len;
123
+ ny /= len;
124
+ nz /= len;
125
+ out[idx] = Math.round((nx * .5 + .5) * 255);
126
+ out[idx + 1] = Math.round((ny * .5 + .5) * 255);
127
+ out[idx + 2] = Math.round(region.elevation * 255);
128
+ out[idx + 3] = pixels[idx + 3];
129
+ }
106
130
  }
131
+ /**
132
+ * Legacy back-compat wrapper. Use `bakeNormalMap(pixels, w, h, {
133
+ * strength })` for equivalent behavior in new code.
134
+ *
135
+ * @deprecated
136
+ */
107
137
  function bakeNormalMapFromPixels(pixels, width, height, options = {}) {
108
- const strength = options.strength ?? DEFAULT_STRENGTH;
109
- return bakeNormalMap(pixels, width, height, {
110
- strength,
111
- bump: DEFAULT_BUMP,
112
- pitch: DEFAULT_PITCH
113
- });
138
+ return bakeNormalMap(pixels, width, height, {
139
+ strength: options.strength ?? 1,
140
+ bump: DEFAULT_BUMP,
141
+ pitch: DEFAULT_PITCH
142
+ });
114
143
  }
144
+ /**
145
+ * Derive the conventional `.normal.png` sibling URL for a sprite PNG.
146
+ *
147
+ * Runtime loaders call this to try the baked output before falling
148
+ * back to the runtime TSL path.
149
+ *
150
+ * Pure string rewrite — browser-safe, no filesystem.
151
+ */
115
152
  function bakedNormalURL(spriteURL) {
116
- return spriteURL.replace(/\.png($|[?#])/i, ".normal.png$1");
153
+ return spriteURL.replace(/\.png($|[?#])/i, ".normal.png$1");
117
154
  }
118
- export {
119
- bakeNormalMap,
120
- bakeNormalMapFromPixels,
121
- bakedNormalURL
122
- };
155
+ //#endregion
156
+ export { bakeNormalMap, bakeNormalMapFromPixels, bakedNormalURL };
157
+
123
158
  //# sourceMappingURL=bake.js.map
package/dist/bake.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/bake.ts"],"sourcesContent":["import {\n DEFAULT_BUMP,\n DEFAULT_PITCH,\n DEFAULT_STRENGTH,\n resolveRegion,\n type NormalSourceDescriptor,\n type ResolvedNormalRegion,\n} from './descriptor.js'\n\n/**\n * @deprecated — legacy flat-texture bake options. Use\n * `bakeNormalMap(pixels, w, h, descriptor)` for region-aware control.\n */\nexport interface BakeOptions {\n /** Scales the alpha gradient before normalization. Default 1. */\n strength?: number\n}\n\n/**\n * Produce a tangent-space normal map from source pixels + a descriptor.\n *\n * Cross-platform: browser + node. No filesystem, no pngjs. Used by\n * the CLI baker (wrapped with file I/O) and the runtime loader\n * (called directly on decoded image pixels).\n *\n * Per texel in each region:\n * 1. `bump: 'alpha'` — central-difference gradient on the source\n * alpha channel, clamped to the region bounds so adjacent regions\n * (e.g. atlas cells) can't bleed into each other.\n * 2. Compose with a tilt rotation that takes `+Z` to the region's\n * `direction` at `pitch` radians.\n * 3. Normalize and encode to `rgb = normal * 0.5 + 0.5`. Alpha is\n * copied from the source so the baked map carries its own\n * silhouette.\n *\n * Texels outside every region receive the flat normal `(0, 0, 1)`\n * with the source's alpha — safe default for sparse descriptors.\n *\n * @param pixels RGBA pixel buffer, row-major, 4 bytes per pixel.\n * @param width Pixel width.\n * @param height Pixel height.\n * @param descriptor Region / tilt / bump control. Defaults to a single\n * whole-texture flat region with alpha-derived bump.\n * @returns RGBA pixel buffer containing the encoded normal map.\n */\nexport function bakeNormalMap(\n pixels: Uint8Array,\n width: number,\n height: number,\n descriptor: NormalSourceDescriptor = {}\n): Uint8Array {\n const out = new Uint8Array(pixels.length)\n\n // Initialize every texel to flat-normal (nx=0, ny=0), elevation=0,\n // source alpha. Runtime reconstructs nz = sqrt(1 − nx² − ny²) — a\n // zero XY yields nz=1 (flat floor). Regions that cover texels will\n // overwrite; texels outside any region keep this safe default.\n //\n // Encoding layout:\n // R = nx encoded as (nx * 0.5 + 0.5) * 255 (0.5 = 128)\n // G = ny encoded the same (0.5 = 128)\n // B = elevation in [0, 1] scaled to [0, 255] (0 = ground default)\n // A = source alpha preserved\n for (let i = 0; i < pixels.length; i += 4) {\n out[i] = 128\n out[i + 1] = 128\n out[i + 2] = 0\n out[i + 3] = pixels[i + 3]!\n }\n\n const regions =\n descriptor.regions && descriptor.regions.length > 0\n ? descriptor.regions\n : [{ x: 0, y: 0, w: width, h: height }]\n\n for (const region of regions) {\n const resolved = resolveRegion(region, descriptor)\n bakeRegion(pixels, out, width, resolved)\n }\n\n return out\n}\n\n/**\n * Bake a single region into the output buffer. Region-local alpha\n * clamping prevents cross-region gradient bleed.\n */\nfunction bakeRegion(\n pixels: Uint8Array,\n out: Uint8Array,\n width: number,\n region: ResolvedNormalRegion\n): void {\n const x0 = region.x\n const y0 = region.y\n const x1 = region.x + region.w\n const y1 = region.y + region.h\n const strength = region.strength\n // Index into the RGBA pixel run for the chosen height source, or -1\n // for 'none'. 0 = R, 1 = G, 2 = B, 3 = A. Luminance needs all three\n // RGB components and is handled separately below.\n const bumpMode = region.bump\n const bumpChannel =\n bumpMode === 'alpha'\n ? 3\n : bumpMode === 'red'\n ? 0\n : bumpMode === 'green'\n ? 1\n : bumpMode === 'blue'\n ? 2\n : -1\n const useLuminance = bumpMode === 'luminance'\n const useBump = bumpChannel >= 0 || useLuminance\n\n // Build the tilt rotation matrix (+Z → target direction at pitch).\n // For direction angle θ, the tilt axis is perpendicular to the tilt\n // direction in the XY plane: `axis = (-sin θ, cos θ, 0)`.\n // Rodrigues' formula composes the rotation matrix from the axis\n // and `pitch` radians.\n let r00 = 1,\n r01 = 0,\n r02 = 0\n let r10 = 0,\n r11 = 1,\n r12 = 0\n let r20 = 0,\n r21 = 0,\n r22 = 1\n const hasTilt = region.angle !== null\n if (hasTilt) {\n const theta = region.angle as number\n const pitch = region.pitch\n const ax = -Math.sin(theta)\n const ay = Math.cos(theta)\n const c = Math.cos(pitch)\n const s = Math.sin(pitch)\n const t = 1 - c\n // Axis is in the XY plane (az = 0), so several Rodrigues terms drop out.\n r00 = c + ax * ax * t\n r01 = ax * ay * t\n r02 = ay * s\n r10 = ay * ax * t\n r11 = c + ay * ay * t\n r12 = -ax * s\n r20 = -ay * s\n r21 = ax * s\n r22 = c\n }\n\n // Height sample for the chosen bump mode, clamped to region bounds\n // so adjacent regions (e.g., atlas cells) can't bleed gradients into\n // each other. Returns a value in [0, 1].\n const heightAt = (x: number, y: number): number => {\n const cx = x < x0 ? x0 : x >= x1 ? x1 - 1 : x\n const cy = y < y0 ? y0 : y >= y1 ? y1 - 1 : y\n const base = (cy * width + cx) * 4\n if (useLuminance) {\n // Rec. 709 luminance — matches perceptual brightness, so art\n // authored against a monitor reads consistently.\n return (\n (0.2126 * pixels[base]! +\n 0.7152 * pixels[base + 1]! +\n 0.0722 * pixels[base + 2]!) /\n 255\n )\n }\n return pixels[base + bumpChannel]! / 255\n }\n\n for (let y = y0; y < y1; y++) {\n for (let x = x0; x < x1; x++) {\n const idx = (y * width + x) * 4\n\n // Local tangent-space bump. Central difference on the height\n // source — `heightAt` selects alpha / luminance / a color\n // channel. Standard tangent-space convention: high values are\n // raised, low values are sunken, normal points away from peaks.\n let lx: number\n let ly: number\n let lz: number\n if (useBump) {\n const hL = heightAt(x - 1, y)\n const hR = heightAt(x + 1, y)\n const hD = heightAt(x, y - 1)\n const hU = heightAt(x, y + 1)\n const dx = (hR - hL) * strength\n const dy = (hU - hD) * strength\n lx = -dx\n ly = -dy\n lz = 1\n } else {\n lx = 0\n ly = 0\n lz = 1\n }\n\n // Apply tilt: n = R · local.\n let nx: number\n let ny: number\n let nz: number\n if (hasTilt) {\n nx = r00 * lx + r01 * ly + r02 * lz\n ny = r10 * lx + r11 * ly + r12 * lz\n nz = r20 * lx + r21 * ly + r22 * lz\n } else {\n nx = lx\n ny = ly\n nz = lz\n }\n\n const len = Math.hypot(nx, ny, nz)\n nx /= len\n ny /= len\n nz /= len\n // Note: nz is not written — runtime reconstructs it as\n // sqrt(1 − nx² − ny²). Outward-facing convention means nz ≥ 0\n // always, so the sign is implicit.\n\n out[idx] = Math.round((nx * 0.5 + 0.5) * 255)\n out[idx + 1] = Math.round((ny * 0.5 + 0.5) * 255)\n out[idx + 2] = Math.round(region.elevation * 255)\n out[idx + 3] = pixels[idx + 3]!\n }\n }\n}\n\n/**\n * Legacy back-compat wrapper. Use `bakeNormalMap(pixels, w, h, {\n * strength })` for equivalent behavior in new code.\n *\n * @deprecated\n */\nexport function bakeNormalMapFromPixels(\n pixels: Uint8Array,\n width: number,\n height: number,\n options: BakeOptions = {}\n): Uint8Array {\n const strength = options.strength ?? DEFAULT_STRENGTH\n return bakeNormalMap(pixels, width, height, {\n strength,\n bump: DEFAULT_BUMP,\n pitch: DEFAULT_PITCH,\n })\n}\n\n/**\n * Derive the conventional `.normal.png` sibling URL for a sprite PNG.\n *\n * Runtime loaders call this to try the baked output before falling\n * back to the runtime TSL path.\n *\n * Pure string rewrite — browser-safe, no filesystem.\n */\nexport function bakedNormalURL(spriteURL: string): string {\n return spriteURL.replace(/\\.png($|[?#])/i, '.normal.png$1')\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"file":"bake.js","names":[],"sources":["../src/bake.ts"],"sourcesContent":["import {\n DEFAULT_BUMP,\n DEFAULT_PITCH,\n DEFAULT_STRENGTH,\n resolveRegion,\n type NormalSourceDescriptor,\n type ResolvedNormalRegion,\n} from './descriptor.js'\n\n/**\n * @deprecated — legacy flat-texture bake options. Use\n * `bakeNormalMap(pixels, w, h, descriptor)` for region-aware control.\n */\nexport interface BakeOptions {\n /** Scales the alpha gradient before normalization. Default 1. */\n strength?: number\n}\n\n/**\n * Produce a tangent-space normal map from source pixels + a descriptor.\n *\n * Cross-platform: browser + node. No filesystem, no pngjs. Used by\n * the CLI baker (wrapped with file I/O) and the runtime loader\n * (called directly on decoded image pixels).\n *\n * Per texel in each region:\n * 1. `bump: 'alpha'` — central-difference gradient on the source\n * alpha channel, clamped to the region bounds so adjacent regions\n * (e.g. atlas cells) can't bleed into each other.\n * 2. Compose with a tilt rotation that takes `+Z` to the region's\n * `direction` at `pitch` radians.\n * 3. Normalize and encode to `rgb = normal * 0.5 + 0.5`. Alpha is\n * copied from the source so the baked map carries its own\n * silhouette.\n *\n * Texels outside every region receive the flat normal `(0, 0, 1)`\n * with the source's alpha — safe default for sparse descriptors.\n *\n * @param pixels RGBA pixel buffer, row-major, 4 bytes per pixel.\n * @param width Pixel width.\n * @param height Pixel height.\n * @param descriptor Region / tilt / bump control. Defaults to a single\n * whole-texture flat region with alpha-derived bump.\n * @returns RGBA pixel buffer containing the encoded normal map.\n */\nexport function bakeNormalMap(\n pixels: Uint8Array,\n width: number,\n height: number,\n descriptor: NormalSourceDescriptor = {}\n): Uint8Array {\n const out = new Uint8Array(pixels.length)\n\n // Initialize every texel to flat-normal (nx=0, ny=0), elevation=0,\n // source alpha. Runtime reconstructs nz = sqrt(1 − nx² − ny²) — a\n // zero XY yields nz=1 (flat floor). Regions that cover texels will\n // overwrite; texels outside any region keep this safe default.\n //\n // Encoding layout:\n // R = nx encoded as (nx * 0.5 + 0.5) * 255 (0.5 = 128)\n // G = ny encoded the same (0.5 = 128)\n // B = elevation in [0, 1] scaled to [0, 255] (0 = ground default)\n // A = source alpha preserved\n for (let i = 0; i < pixels.length; i += 4) {\n out[i] = 128\n out[i + 1] = 128\n out[i + 2] = 0\n out[i + 3] = pixels[i + 3]!\n }\n\n const regions =\n descriptor.regions && descriptor.regions.length > 0 ? descriptor.regions : [{ x: 0, y: 0, w: width, h: height }]\n\n for (const region of regions) {\n const resolved = resolveRegion(region, descriptor)\n bakeRegion(pixels, out, width, resolved)\n }\n\n return out\n}\n\n/**\n * Bake a single region into the output buffer. Region-local alpha\n * clamping prevents cross-region gradient bleed.\n */\nfunction bakeRegion(pixels: Uint8Array, out: Uint8Array, width: number, region: ResolvedNormalRegion): void {\n const x0 = region.x\n const y0 = region.y\n const x1 = region.x + region.w\n const y1 = region.y + region.h\n const strength = region.strength\n // Index into the RGBA pixel run for the chosen height source, or -1\n // for 'none'. 0 = R, 1 = G, 2 = B, 3 = A. Luminance needs all three\n // RGB components and is handled separately below.\n const bumpMode = region.bump\n const bumpChannel =\n bumpMode === 'alpha' ? 3 : bumpMode === 'red' ? 0 : bumpMode === 'green' ? 1 : bumpMode === 'blue' ? 2 : -1\n const useLuminance = bumpMode === 'luminance'\n const useBump = bumpChannel >= 0 || useLuminance\n\n // Build the tilt rotation matrix (+Z → target direction at pitch).\n // For direction angle θ, the tilt axis is perpendicular to the tilt\n // direction in the XY plane: `axis = (-sin θ, cos θ, 0)`.\n // Rodrigues' formula composes the rotation matrix from the axis\n // and `pitch` radians.\n let r00 = 1,\n r01 = 0,\n r02 = 0\n let r10 = 0,\n r11 = 1,\n r12 = 0\n let r20 = 0,\n r21 = 0,\n r22 = 1\n const hasTilt = region.angle !== null\n if (hasTilt) {\n const theta = region.angle as number\n const pitch = region.pitch\n const ax = -Math.sin(theta)\n const ay = Math.cos(theta)\n const c = Math.cos(pitch)\n const s = Math.sin(pitch)\n const t = 1 - c\n // Axis is in the XY plane (az = 0), so several Rodrigues terms drop out.\n r00 = c + ax * ax * t\n r01 = ax * ay * t\n r02 = ay * s\n r10 = ay * ax * t\n r11 = c + ay * ay * t\n r12 = -ax * s\n r20 = -ay * s\n r21 = ax * s\n r22 = c\n }\n\n // Height sample for the chosen bump mode, clamped to region bounds\n // so adjacent regions (e.g., atlas cells) can't bleed gradients into\n // each other. Returns a value in [0, 1].\n const heightAt = (x: number, y: number): number => {\n const cx = x < x0 ? x0 : x >= x1 ? x1 - 1 : x\n const cy = y < y0 ? y0 : y >= y1 ? y1 - 1 : y\n const base = (cy * width + cx) * 4\n if (useLuminance) {\n // Rec. 709 luminance — matches perceptual brightness, so art\n // authored against a monitor reads consistently.\n return (0.2126 * pixels[base]! + 0.7152 * pixels[base + 1]! + 0.0722 * pixels[base + 2]!) / 255\n }\n return pixels[base + bumpChannel]! / 255\n }\n\n for (let y = y0; y < y1; y++) {\n for (let x = x0; x < x1; x++) {\n const idx = (y * width + x) * 4\n\n // Local tangent-space bump. Central difference on the height\n // source — `heightAt` selects alpha / luminance / a color\n // channel. Standard tangent-space convention: high values are\n // raised, low values are sunken, normal points away from peaks.\n let lx: number\n let ly: number\n let lz: number\n if (useBump) {\n const hL = heightAt(x - 1, y)\n const hR = heightAt(x + 1, y)\n const hD = heightAt(x, y - 1)\n const hU = heightAt(x, y + 1)\n const dx = (hR - hL) * strength\n const dy = (hU - hD) * strength\n lx = -dx\n ly = -dy\n lz = 1\n } else {\n lx = 0\n ly = 0\n lz = 1\n }\n\n // Apply tilt: n = R · local.\n let nx: number\n let ny: number\n let nz: number\n if (hasTilt) {\n nx = r00 * lx + r01 * ly + r02 * lz\n ny = r10 * lx + r11 * ly + r12 * lz\n nz = r20 * lx + r21 * ly + r22 * lz\n } else {\n nx = lx\n ny = ly\n nz = lz\n }\n\n const len = Math.hypot(nx, ny, nz)\n nx /= len\n ny /= len\n nz /= len\n // Note: nz is not written — runtime reconstructs it as\n // sqrt(1 − nx² − ny²). Outward-facing convention means nz ≥ 0\n // always, so the sign is implicit.\n\n out[idx] = Math.round((nx * 0.5 + 0.5) * 255)\n out[idx + 1] = Math.round((ny * 0.5 + 0.5) * 255)\n out[idx + 2] = Math.round(region.elevation * 255)\n out[idx + 3] = pixels[idx + 3]!\n }\n }\n}\n\n/**\n * Legacy back-compat wrapper. Use `bakeNormalMap(pixels, w, h, {\n * strength })` for equivalent behavior in new code.\n *\n * @deprecated\n */\nexport function bakeNormalMapFromPixels(\n pixels: Uint8Array,\n width: number,\n height: number,\n options: BakeOptions = {}\n): Uint8Array {\n const strength = options.strength ?? DEFAULT_STRENGTH\n return bakeNormalMap(pixels, width, height, {\n strength,\n bump: DEFAULT_BUMP,\n pitch: DEFAULT_PITCH,\n })\n}\n\n/**\n * Derive the conventional `.normal.png` sibling URL for a sprite PNG.\n *\n * Runtime loaders call this to try the baked output before falling\n * back to the runtime TSL path.\n *\n * Pure string rewrite — browser-safe, no filesystem.\n */\nexport function bakedNormalURL(spriteURL: string): string {\n return spriteURL.replace(/\\.png($|[?#])/i, '.normal.png$1')\n}\n"],"mappings":"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"}
@@ -0,0 +1,20 @@
1
+ import { NormalSourceDescriptor } from "./descriptor.js";
2
+ import { BakeOptions } from "./bake.js";
3
+ //#region src/bake.node.d.ts
4
+ /**
5
+ * Bake a normal map from a PNG file on disk.
6
+ *
7
+ * Node-only wrapper around `bakeNormalMap` — handles file I/O and
8
+ * stamps the output PNG with a `tEXt` chunk containing the descriptor
9
+ * hash, so `probeBakedSibling` can invalidate stale outputs.
10
+ *
11
+ * Second argument accepts either:
12
+ * - A full `NormalSourceDescriptor` (region-aware).
13
+ * - A legacy `BakeOptions` (`{ strength }`) — for back-compat with
14
+ * existing callers; promoted to a zero-region descriptor.
15
+ * - A path to a descriptor JSON file.
16
+ */
17
+ declare function bakeNormalMapFile(inputPath: string, descriptorOrOptions?: NormalSourceDescriptor | BakeOptions | string, outputPath?: string): string;
18
+ //#endregion
19
+ export { bakeNormalMapFile };
20
+ //# sourceMappingURL=bake.node.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bake.node.d.ts","names":[],"sources":["../src/bake.node.ts"],"mappings":";;;;;;;;;;;;;;;;iBAoBgB,kBACd,mBACA,sBAAsB,yBAAyB,sBAC/C"}
package/dist/bake.node.js CHANGED
@@ -1,38 +1,45 @@
1
+ import { bakeNormalMap } from "./bake.js";
2
+ import { hashDescriptor } from "@three-flatland/bake";
1
3
  import { readFileSync } from "node:fs";
2
4
  import { PNG } from "pngjs";
3
- import { hashDescriptor } from "@three-flatland/bake";
4
5
  import { writeSidecarPng } from "@three-flatland/bake/node";
5
- import { bakeNormalMap } from "./bake.js";
6
+ //#region src/bake.node.ts
7
+ /**
8
+ * Bake a normal map from a PNG file on disk.
9
+ *
10
+ * Node-only wrapper around `bakeNormalMap` — handles file I/O and
11
+ * stamps the output PNG with a `tEXt` chunk containing the descriptor
12
+ * hash, so `probeBakedSibling` can invalidate stale outputs.
13
+ *
14
+ * Second argument accepts either:
15
+ * - A full `NormalSourceDescriptor` (region-aware).
16
+ * - A legacy `BakeOptions` (`{ strength }`) — for back-compat with
17
+ * existing callers; promoted to a zero-region descriptor.
18
+ * - A path to a descriptor JSON file.
19
+ */
6
20
  function bakeNormalMapFile(inputPath, descriptorOrOptions, outputPath) {
7
- const buffer = readFileSync(inputPath);
8
- const png = PNG.sync.read(buffer);
9
- const pixels = new Uint8Array(
10
- png.data.buffer,
11
- png.data.byteOffset,
12
- png.data.byteLength
13
- );
14
- const descriptor = resolveDescriptorInput(descriptorOrOptions);
15
- const normalPixels = bakeNormalMap(pixels, png.width, png.height, descriptor);
16
- const resolvedOut = outputPath ?? inputPath.replace(/\.png$/i, ".normal.png");
17
- writeSidecarPng(resolvedOut, normalPixels, png.width, png.height, {
18
- hash: hashDescriptor(descriptor),
19
- v: 1
20
- });
21
- return resolvedOut;
21
+ const buffer = readFileSync(inputPath);
22
+ const png = PNG.sync.read(buffer);
23
+ const pixels = new Uint8Array(png.data.buffer, png.data.byteOffset, png.data.byteLength);
24
+ const descriptor = resolveDescriptorInput(descriptorOrOptions);
25
+ const normalPixels = bakeNormalMap(pixels, png.width, png.height, descriptor);
26
+ const resolvedOut = outputPath ?? inputPath.replace(/\.png$/i, ".normal.png");
27
+ writeSidecarPng(resolvedOut, normalPixels, png.width, png.height, {
28
+ hash: hashDescriptor(descriptor),
29
+ v: 1
30
+ });
31
+ return resolvedOut;
22
32
  }
23
33
  function resolveDescriptorInput(input) {
24
- if (input === void 0) return {};
25
- if (typeof input === "string") {
26
- const json = readFileSync(input, "utf8");
27
- return JSON.parse(json);
28
- }
29
- if ("regions" in input || "direction" in input || "bump" in input || "pitch" in input) {
30
- return input;
31
- }
32
- const legacy = input;
33
- return { strength: legacy.strength };
34
+ if (input === void 0) return {};
35
+ if (typeof input === "string") {
36
+ const json = readFileSync(input, "utf8");
37
+ return JSON.parse(json);
38
+ }
39
+ if ("regions" in input || "direction" in input || "bump" in input || "pitch" in input) return input;
40
+ return { strength: input.strength };
34
41
  }
35
- export {
36
- bakeNormalMapFile
37
- };
42
+ //#endregion
43
+ export { bakeNormalMapFile };
44
+
38
45
  //# sourceMappingURL=bake.node.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/bake.node.ts"],"sourcesContent":["import { readFileSync } from 'node:fs'\nimport { PNG } from 'pngjs'\nimport { hashDescriptor } from '@three-flatland/bake'\nimport { writeSidecarPng } from '@three-flatland/bake/node'\nimport { bakeNormalMap, type BakeOptions } from './bake.js'\nimport type { NormalSourceDescriptor } from './descriptor.js'\n\n/**\n * Bake a normal map from a PNG file on disk.\n *\n * Node-only wrapper around `bakeNormalMap` — handles file I/O and\n * stamps the output PNG with a `tEXt` chunk containing the descriptor\n * hash, so `probeBakedSibling` can invalidate stale outputs.\n *\n * Second argument accepts either:\n * - A full `NormalSourceDescriptor` (region-aware).\n * - A legacy `BakeOptions` (`{ strength }`) — for back-compat with\n * existing callers; promoted to a zero-region descriptor.\n * - A path to a descriptor JSON file.\n */\nexport function bakeNormalMapFile(\n inputPath: string,\n descriptorOrOptions?: NormalSourceDescriptor | BakeOptions | string,\n outputPath?: string\n): string {\n const buffer = readFileSync(inputPath)\n const png = PNG.sync.read(buffer)\n const pixels = new Uint8Array(\n png.data.buffer,\n png.data.byteOffset,\n png.data.byteLength\n )\n\n const descriptor = resolveDescriptorInput(descriptorOrOptions)\n const normalPixels = bakeNormalMap(pixels, png.width, png.height, descriptor)\n\n const resolvedOut = outputPath ?? inputPath.replace(/\\.png$/i, '.normal.png')\n writeSidecarPng(resolvedOut, normalPixels, png.width, png.height, {\n hash: hashDescriptor(descriptor),\n v: 1,\n })\n return resolvedOut\n}\n\nfunction resolveDescriptorInput(\n input: NormalSourceDescriptor | BakeOptions | string | undefined\n): NormalSourceDescriptor {\n if (input === undefined) return {}\n if (typeof input === 'string') {\n const json = readFileSync(input, 'utf8')\n return JSON.parse(json) as NormalSourceDescriptor\n }\n // Heuristic: a BakeOptions has only `strength`; a descriptor may have\n // `regions`, `direction`, etc. Treat as descriptor when any non-\n // `strength` field is present.\n if ('regions' in input || 'direction' in input || 'bump' in input || 'pitch' in input) {\n return input\n }\n const legacy = input as BakeOptions\n return { strength: legacy.strength }\n}\n"],"mappings":"AAAA,SAAS,oBAAoB;AAC7B,SAAS,WAAW;AACpB,SAAS,sBAAsB;AAC/B,SAAS,uBAAuB;AAChC,SAAS,qBAAuC;AAgBzC,SAAS,kBACd,WACA,qBACA,YACQ;AACR,QAAM,SAAS,aAAa,SAAS;AACrC,QAAM,MAAM,IAAI,KAAK,KAAK,MAAM;AAChC,QAAM,SAAS,IAAI;AAAA,IACjB,IAAI,KAAK;AAAA,IACT,IAAI,KAAK;AAAA,IACT,IAAI,KAAK;AAAA,EACX;AAEA,QAAM,aAAa,uBAAuB,mBAAmB;AAC7D,QAAM,eAAe,cAAc,QAAQ,IAAI,OAAO,IAAI,QAAQ,UAAU;AAE5E,QAAM,cAAc,cAAc,UAAU,QAAQ,WAAW,aAAa;AAC5E,kBAAgB,aAAa,cAAc,IAAI,OAAO,IAAI,QAAQ;AAAA,IAChE,MAAM,eAAe,UAAU;AAAA,IAC/B,GAAG;AAAA,EACL,CAAC;AACD,SAAO;AACT;AAEA,SAAS,uBACP,OACwB;AACxB,MAAI,UAAU,OAAW,QAAO,CAAC;AACjC,MAAI,OAAO,UAAU,UAAU;AAC7B,UAAM,OAAO,aAAa,OAAO,MAAM;AACvC,WAAO,KAAK,MAAM,IAAI;AAAA,EACxB;AAIA,MAAI,aAAa,SAAS,eAAe,SAAS,UAAU,SAAS,WAAW,OAAO;AACrF,WAAO;AAAA,EACT;AACA,QAAM,SAAS;AACf,SAAO,EAAE,UAAU,OAAO,SAAS;AACrC;","names":[]}
1
+ {"version":3,"file":"bake.node.js","names":["legacy"],"sources":["../src/bake.node.ts"],"sourcesContent":["import { readFileSync } from 'node:fs'\nimport { PNG } from 'pngjs'\nimport { hashDescriptor } from '@three-flatland/bake'\nimport { writeSidecarPng } from '@three-flatland/bake/node'\nimport { bakeNormalMap, type BakeOptions } from './bake.js'\nimport type { NormalSourceDescriptor } from './descriptor.js'\n\n/**\n * Bake a normal map from a PNG file on disk.\n *\n * Node-only wrapper around `bakeNormalMap` — handles file I/O and\n * stamps the output PNG with a `tEXt` chunk containing the descriptor\n * hash, so `probeBakedSibling` can invalidate stale outputs.\n *\n * Second argument accepts either:\n * - A full `NormalSourceDescriptor` (region-aware).\n * - A legacy `BakeOptions` (`{ strength }`) — for back-compat with\n * existing callers; promoted to a zero-region descriptor.\n * - A path to a descriptor JSON file.\n */\nexport function bakeNormalMapFile(\n inputPath: string,\n descriptorOrOptions?: NormalSourceDescriptor | BakeOptions | string,\n outputPath?: string\n): string {\n const buffer = readFileSync(inputPath)\n const png = PNG.sync.read(buffer)\n const pixels = new Uint8Array(png.data.buffer, png.data.byteOffset, png.data.byteLength)\n\n const descriptor = resolveDescriptorInput(descriptorOrOptions)\n const normalPixels = bakeNormalMap(pixels, png.width, png.height, descriptor)\n\n const resolvedOut = outputPath ?? inputPath.replace(/\\.png$/i, '.normal.png')\n writeSidecarPng(resolvedOut, normalPixels, png.width, png.height, {\n hash: hashDescriptor(descriptor),\n v: 1,\n })\n return resolvedOut\n}\n\nfunction resolveDescriptorInput(\n input: NormalSourceDescriptor | BakeOptions | string | undefined\n): NormalSourceDescriptor {\n if (input === undefined) return {}\n if (typeof input === 'string') {\n const json = readFileSync(input, 'utf8')\n return JSON.parse(json) as NormalSourceDescriptor\n }\n // Heuristic: a BakeOptions has only `strength`; a descriptor may have\n // `regions`, `direction`, etc. Treat as descriptor when any non-\n // `strength` field is present.\n if ('regions' in input || 'direction' in input || 'bump' in input || 'pitch' in input) {\n return input\n }\n const legacy = input as BakeOptions\n return { strength: legacy.strength }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;AAoBA,SAAgB,kBACd,WACA,qBACA,YACQ;CACR,MAAM,SAAS,aAAa,SAAS;CACrC,MAAM,MAAM,IAAI,KAAK,KAAK,MAAM;CAChC,MAAM,SAAS,IAAI,WAAW,IAAI,KAAK,QAAQ,IAAI,KAAK,YAAY,IAAI,KAAK,UAAU;CAEvF,MAAM,aAAa,uBAAuB,mBAAmB;CAC7D,MAAM,eAAe,cAAc,QAAQ,IAAI,OAAO,IAAI,QAAQ,UAAU;CAE5E,MAAM,cAAc,cAAc,UAAU,QAAQ,WAAW,aAAa;CAC5E,gBAAgB,aAAa,cAAc,IAAI,OAAO,IAAI,QAAQ;EAChE,MAAM,eAAe,UAAU;EAC/B,GAAG;CACL,CAAC;CACD,OAAO;AACT;AAEA,SAAS,uBACP,OACwB;CACxB,IAAI,UAAU,KAAA,GAAW,OAAO,CAAC;CACjC,IAAI,OAAO,UAAU,UAAU;EAC7B,MAAM,OAAO,aAAa,OAAO,MAAM;EACvC,OAAO,KAAK,MAAM,IAAI;CACxB;CAIA,IAAI,aAAa,SAAS,eAAe,SAAS,UAAU,SAAS,WAAW,OAC9E,OAAO;CAGT,OAAO,EAAE,UAAUA,MAAO,SAAS;AACrC"}
package/dist/cli.d.ts CHANGED
@@ -1,5 +1,6 @@
1
- import { Baker } from '@three-flatland/bake';
2
-
1
+ import { Baker } from "@three-flatland/bake";
2
+ //#region src/cli.d.ts
3
3
  declare const baker: Baker;
4
-
4
+ //#endregion
5
5
  export { baker as default };
6
+ //# sourceMappingURL=cli.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.d.ts","names":[],"sources":["../src/cli.ts"],"mappings":";;cA6BM,OAAO"}