@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/README.md +2 -2
- package/dist/NormalMapLoader.d.ts +99 -0
- package/dist/NormalMapLoader.d.ts.map +1 -0
- package/dist/NormalMapLoader.js +128 -125
- package/dist/NormalMapLoader.js.map +1 -1
- package/dist/bake.d.ts +57 -0
- package/dist/bake.d.ts.map +1 -0
- package/dist/bake.js +149 -114
- package/dist/bake.js.map +1 -1
- package/dist/bake.node.d.ts +20 -0
- package/dist/bake.node.d.ts.map +1 -0
- package/dist/bake.node.js +37 -30
- package/dist/bake.node.js.map +1 -1
- package/dist/cli.d.ts +4 -3
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +109 -122
- package/dist/cli.js.map +1 -1
- package/dist/descriptor.d.ts +129 -0
- package/dist/descriptor.d.ts.map +1 -0
- package/dist/descriptor.js +45 -52
- package/dist/descriptor.js.map +1 -1
- package/dist/index.d.ts +5 -320
- package/dist/index.js +5 -33
- package/dist/node.d.ts +7 -21
- package/dist/node.js +5 -5
- package/dist/resolveNormalMap.d.ts +51 -0
- package/dist/resolveNormalMap.d.ts.map +1 -0
- package/dist/resolveNormalMap.js +67 -88
- package/dist/resolveNormalMap.js.map +1 -1
- package/package.json +27 -9
- package/dist/index.js.map +0 -1
- package/dist/node.js.map +0 -1
package/dist/bake.js
CHANGED
|
@@ -1,123 +1,158 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
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
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
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
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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
|
-
|
|
153
|
+
return spriteURL.replace(/\.png($|[?#])/i, ".normal.png$1");
|
|
117
154
|
}
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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
|
-
|
|
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
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
-
|
|
36
|
-
|
|
37
|
-
|
|
42
|
+
//#endregion
|
|
43
|
+
export { bakeNormalMapFile };
|
|
44
|
+
|
|
38
45
|
//# sourceMappingURL=bake.node.js.map
|
package/dist/bake.node.js.map
CHANGED
|
@@ -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(
|
|
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
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli.d.ts","names":[],"sources":["../src/cli.ts"],"mappings":";;cA6BM,OAAO"}
|