@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/README.md
CHANGED
|
@@ -59,7 +59,7 @@ flatland-bake normal <input.png> --descriptor <input.normal.json>
|
|
|
59
59
|
| `--strength <n>` | Gradient multiplier before normalization (default `1`) |
|
|
60
60
|
| `--descriptor <path>` | Region-aware descriptor JSON — per-frame / per-tile control for atlases and tilemaps |
|
|
61
61
|
|
|
62
|
-
See the [three-flatland docs](https://
|
|
62
|
+
See the [three-flatland docs](https://tjw.dev/three-flatland/) for descriptor examples covering sprite sheets, LDtk tilesets, and directional (3/4-view wall) tiles.
|
|
63
63
|
|
|
64
64
|
### Loader options
|
|
65
65
|
|
|
@@ -212,7 +212,7 @@ This package is renderer-agnostic and works with any Three.js WebGPU project. Ba
|
|
|
212
212
|
|
|
213
213
|
## Documentation
|
|
214
214
|
|
|
215
|
-
Full docs, interactive examples, and API reference at **[
|
|
215
|
+
Full docs, interactive examples, and API reference at **[tjw.dev/three-flatland](https://tjw.dev/three-flatland/)**
|
|
216
216
|
|
|
217
217
|
## License
|
|
218
218
|
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { NormalSourceDescriptor } from "./descriptor.js";
|
|
2
|
+
import { Loader, LoadingManager, Texture } from "three";
|
|
3
|
+
import { BakedAssetLoaderOptions } from "@three-flatland/bake";
|
|
4
|
+
//#region src/NormalMapLoader.d.ts
|
|
5
|
+
/**
|
|
6
|
+
* Options accepted by `NormalMapLoader.load()`. Inherits `forceRuntime`
|
|
7
|
+
* from the shared {@link BakedAssetLoaderOptions} so every baked-asset
|
|
8
|
+
* loader in the codebase advertises the same option shape.
|
|
9
|
+
*/
|
|
10
|
+
interface NormalMapLoaderStaticOptions extends BakedAssetLoaderOptions {
|
|
11
|
+
/**
|
|
12
|
+
* When provided, missing sidecars trigger an in-memory bake via
|
|
13
|
+
* `resolveNormalMap`. Without a descriptor, `NormalMapLoader.load()` can
|
|
14
|
+
* only probe the baked sibling and returns `null` on miss (legacy
|
|
15
|
+
* behavior, preserved for backward compat).
|
|
16
|
+
*/
|
|
17
|
+
descriptor?: NormalSourceDescriptor;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Result of loading a sprite's normal data.
|
|
21
|
+
*
|
|
22
|
+
* Either a baked normal texture (fast path) or `null`, signalling the caller
|
|
23
|
+
* to use the runtime TSL `normalFromSprite` helper against the sprite's own
|
|
24
|
+
* alpha channel.
|
|
25
|
+
*/
|
|
26
|
+
type NormalMapResult = Texture | null;
|
|
27
|
+
/**
|
|
28
|
+
* Loader for per-sprite normal maps following the canonical three-flatland
|
|
29
|
+
* "try baked → fall back to runtime" pattern documented in
|
|
30
|
+
* `planning/bake/loader-pattern.md`.
|
|
31
|
+
*
|
|
32
|
+
* Given a sprite PNG URL, this loader fetches the sibling `.normal.png` that
|
|
33
|
+
* `flatland-bake normal` produces. If it is absent, the loader resolves to
|
|
34
|
+
* `null` so the lit material can switch to its TSL runtime fallback.
|
|
35
|
+
*
|
|
36
|
+
* Only the baked path uses the network — the runtime branch is a shader
|
|
37
|
+
* concern and lives in `@three-flatland/nodes/lighting/normalFromSprite`.
|
|
38
|
+
*
|
|
39
|
+
* @example
|
|
40
|
+
* ```ts
|
|
41
|
+
* // Vanilla static API
|
|
42
|
+
* const normalTex = await NormalMapLoader.load('/sprites/knight.png')
|
|
43
|
+
* if (normalTex) material.normalMap = normalTex
|
|
44
|
+
* else material.useRuntimeNormals = true
|
|
45
|
+
*
|
|
46
|
+
* // Skip the baked probe — go straight to runtime (or null without descriptor)
|
|
47
|
+
* const tex = await NormalMapLoader.load(url, { forceRuntime: true })
|
|
48
|
+
*
|
|
49
|
+
* // R3F useLoader
|
|
50
|
+
* const tex = useLoader(NormalMapLoader, '/sprites/knight.png')
|
|
51
|
+
* ```
|
|
52
|
+
*/
|
|
53
|
+
declare class NormalMapLoader extends Loader<NormalMapResult> {
|
|
54
|
+
/**
|
|
55
|
+
* Generate this asset's normal map in the browser on every load
|
|
56
|
+
* instead of loading a pre-baked `.normal.png` sidecar. With a
|
|
57
|
+
* `descriptor`, the in-memory bake runs on every load. Without a
|
|
58
|
+
* descriptor, the loader resolves directly to `null` and the caller
|
|
59
|
+
* uses the TSL runtime fallback. Suppresses the "no baked sibling"
|
|
60
|
+
* warn either way.
|
|
61
|
+
*
|
|
62
|
+
* Use when runtime is the right home for the bake. Not a dev-iteration
|
|
63
|
+
* knob — the default path (probe → bake on miss + warn) already
|
|
64
|
+
* handles iteration. See {@link BakedAssetLoaderOptions.forceRuntime}.
|
|
65
|
+
*/
|
|
66
|
+
forceRuntime: boolean;
|
|
67
|
+
/**
|
|
68
|
+
* When set, missing sidecars trigger an in-memory bake via
|
|
69
|
+
* `resolveNormalMap`. Without a descriptor, the loader can only probe
|
|
70
|
+
* the baked sibling and returns `null` on miss (legacy behavior).
|
|
71
|
+
*
|
|
72
|
+
* Set via the `useLoader` callback:
|
|
73
|
+
* ```ts
|
|
74
|
+
* useLoader(NormalMapLoader, url, undefined, (l) => { l.descriptor = desc })
|
|
75
|
+
* ```
|
|
76
|
+
*/
|
|
77
|
+
descriptor: NormalSourceDescriptor | undefined;
|
|
78
|
+
constructor(manager?: LoadingManager);
|
|
79
|
+
load(url: string, onLoad?: (data: NormalMapResult) => void, onProgress?: (event: ProgressEvent) => void, onError?: (err: unknown) => void): NormalMapResult;
|
|
80
|
+
loadAsync(url: string): Promise<NormalMapResult>;
|
|
81
|
+
/**
|
|
82
|
+
* Vanilla cache: keyed by `(url, forceRuntime, descriptor hash)` so
|
|
83
|
+
* two callers passing *different* descriptors for the same URL get
|
|
84
|
+
* distinct results instead of colliding on the first one's bake.
|
|
85
|
+
*
|
|
86
|
+
* Instance API (R3F `useLoader`) bypasses this map — R3F has its own
|
|
87
|
+
* suspense cache and we don't want double-caching to fight the lifecycle.
|
|
88
|
+
*/
|
|
89
|
+
private static _cache;
|
|
90
|
+
static load(url: string, options?: NormalMapLoaderStaticOptions): Promise<NormalMapResult>;
|
|
91
|
+
static clearCache(): void;
|
|
92
|
+
private static _loadImpl;
|
|
93
|
+
private static _tryLoadBaked;
|
|
94
|
+
}
|
|
95
|
+
/** Clear the devtime-warning dedupe set. Intended for tests. */
|
|
96
|
+
declare function _resetDevtimeWarnings(): void;
|
|
97
|
+
//#endregion
|
|
98
|
+
export { NormalMapLoader, NormalMapLoaderStaticOptions, NormalMapResult, _resetDevtimeWarnings };
|
|
99
|
+
//# sourceMappingURL=NormalMapLoader.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"NormalMapLoader.d.ts","names":[],"sources":["../src/NormalMapLoader.ts"],"mappings":";;;;;;;;;UAeiB,qCAAqC;;;;;;;EAOpD,aAAa;;;;;;;;;KAUH,kBAAkB;;;;;;;;;;;;;;;;;;;;;;;;;;;cA4BjB,wBAAwB,OAAO;;;;;;;;;;;;;EAa1C;;;;;;;;;;;EAWA,YAAY;EAEA,YAAA,UAAU;EAMtB,KACE,aACA,UAAU,MAAM,0BAChB,cAAc,OAAO,wBACrB,WAAW,wBACV;EAgBH,UAAU,cAAc,QAAQ;;;;;;;;;iBAcjB;SAER,KAAK,aAAa,UAAU,+BAA+B,QAAQ;SAgBnE;iBAMc;iBA4BA;;;iBA6BP"}
|
package/dist/NormalMapLoader.js
CHANGED
|
@@ -1,129 +1,132 @@
|
|
|
1
|
-
import { Loader, TextureLoader } from "three";
|
|
2
|
-
import {
|
|
3
|
-
devtimeWarn as sharedDevtimeWarn,
|
|
4
|
-
_resetDevtimeWarnings as sharedResetDevtimeWarnings,
|
|
5
|
-
hashDescriptor
|
|
6
|
-
} from "@three-flatland/bake";
|
|
7
1
|
import { bakedNormalURL } from "./bake.js";
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
}
|
|
2
|
+
import { Loader, TextureLoader } from "three";
|
|
3
|
+
import { _resetDevtimeWarnings as _resetDevtimeWarnings$1, devtimeWarn, hashDescriptor } from "@three-flatland/bake";
|
|
4
|
+
//#region src/NormalMapLoader.ts
|
|
5
|
+
/**
|
|
6
|
+
* Loader for per-sprite normal maps following the canonical three-flatland
|
|
7
|
+
* "try baked → fall back to runtime" pattern documented in
|
|
8
|
+
* `planning/bake/loader-pattern.md`.
|
|
9
|
+
*
|
|
10
|
+
* Given a sprite PNG URL, this loader fetches the sibling `.normal.png` that
|
|
11
|
+
* `flatland-bake normal` produces. If it is absent, the loader resolves to
|
|
12
|
+
* `null` so the lit material can switch to its TSL runtime fallback.
|
|
13
|
+
*
|
|
14
|
+
* Only the baked path uses the network — the runtime branch is a shader
|
|
15
|
+
* concern and lives in `@three-flatland/nodes/lighting/normalFromSprite`.
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* ```ts
|
|
19
|
+
* // Vanilla static API
|
|
20
|
+
* const normalTex = await NormalMapLoader.load('/sprites/knight.png')
|
|
21
|
+
* if (normalTex) material.normalMap = normalTex
|
|
22
|
+
* else material.useRuntimeNormals = true
|
|
23
|
+
*
|
|
24
|
+
* // Skip the baked probe — go straight to runtime (or null without descriptor)
|
|
25
|
+
* const tex = await NormalMapLoader.load(url, { forceRuntime: true })
|
|
26
|
+
*
|
|
27
|
+
* // R3F useLoader
|
|
28
|
+
* const tex = useLoader(NormalMapLoader, '/sprites/knight.png')
|
|
29
|
+
* ```
|
|
30
|
+
*/
|
|
31
|
+
var NormalMapLoader = class NormalMapLoader extends Loader {
|
|
32
|
+
/**
|
|
33
|
+
* Generate this asset's normal map in the browser on every load
|
|
34
|
+
* instead of loading a pre-baked `.normal.png` sidecar. With a
|
|
35
|
+
* `descriptor`, the in-memory bake runs on every load. Without a
|
|
36
|
+
* descriptor, the loader resolves directly to `null` and the caller
|
|
37
|
+
* uses the TSL runtime fallback. Suppresses the "no baked sibling"
|
|
38
|
+
* warn either way.
|
|
39
|
+
*
|
|
40
|
+
* Use when runtime is the right home for the bake. Not a dev-iteration
|
|
41
|
+
* knob — the default path (probe → bake on miss + warn) already
|
|
42
|
+
* handles iteration. See {@link BakedAssetLoaderOptions.forceRuntime}.
|
|
43
|
+
*/
|
|
44
|
+
forceRuntime = false;
|
|
45
|
+
/**
|
|
46
|
+
* When set, missing sidecars trigger an in-memory bake via
|
|
47
|
+
* `resolveNormalMap`. Without a descriptor, the loader can only probe
|
|
48
|
+
* the baked sibling and returns `null` on miss (legacy behavior).
|
|
49
|
+
*
|
|
50
|
+
* Set via the `useLoader` callback:
|
|
51
|
+
* ```ts
|
|
52
|
+
* useLoader(NormalMapLoader, url, undefined, (l) => { l.descriptor = desc })
|
|
53
|
+
* ```
|
|
54
|
+
*/
|
|
55
|
+
descriptor = void 0;
|
|
56
|
+
constructor(manager) {
|
|
57
|
+
super(manager);
|
|
58
|
+
}
|
|
59
|
+
load(url, onLoad, onProgress, onError) {
|
|
60
|
+
const resolved = this.manager.resolveURL(url);
|
|
61
|
+
NormalMapLoader._loadImpl(resolved, this.forceRuntime, this.descriptor).then((result) => {
|
|
62
|
+
onLoad?.(result);
|
|
63
|
+
}).catch((err) => {
|
|
64
|
+
if (onError) onError(err);
|
|
65
|
+
else console.error("NormalMapLoader:", err);
|
|
66
|
+
this.manager.itemError(url);
|
|
67
|
+
});
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
loadAsync(url) {
|
|
71
|
+
return NormalMapLoader._loadImpl(this.manager.resolveURL(url), this.forceRuntime, this.descriptor);
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Vanilla cache: keyed by `(url, forceRuntime, descriptor hash)` so
|
|
75
|
+
* two callers passing *different* descriptors for the same URL get
|
|
76
|
+
* distinct results instead of colliding on the first one's bake.
|
|
77
|
+
*
|
|
78
|
+
* Instance API (R3F `useLoader`) bypasses this map — R3F has its own
|
|
79
|
+
* suspense cache and we don't want double-caching to fight the lifecycle.
|
|
80
|
+
*/
|
|
81
|
+
static _cache = /* @__PURE__ */ new Map();
|
|
82
|
+
static load(url, options) {
|
|
83
|
+
const forceRuntime = options?.forceRuntime ?? false;
|
|
84
|
+
const descriptor = options?.descriptor;
|
|
85
|
+
const descriptorKey = descriptor ? hashDescriptor(descriptor) : "nodesc";
|
|
86
|
+
const cacheKey = `${url}:${forceRuntime ? "runtime" : "probe"}:${descriptorKey}`;
|
|
87
|
+
const cached = this._cache.get(cacheKey);
|
|
88
|
+
if (cached) return cached;
|
|
89
|
+
const promise = this._loadImpl(url, forceRuntime, descriptor);
|
|
90
|
+
this._cache.set(cacheKey, promise);
|
|
91
|
+
return promise;
|
|
92
|
+
}
|
|
93
|
+
static clearCache() {
|
|
94
|
+
this._cache.clear();
|
|
95
|
+
}
|
|
96
|
+
static async _loadImpl(url, forceRuntime, descriptor) {
|
|
97
|
+
if (descriptor) {
|
|
98
|
+
const { resolveNormalMap } = await import("./resolveNormalMap.js");
|
|
99
|
+
return resolveNormalMap(url, descriptor, { forceRuntime });
|
|
100
|
+
}
|
|
101
|
+
if (!forceRuntime) {
|
|
102
|
+
const baked = await this._tryLoadBaked(url);
|
|
103
|
+
if (baked) return baked;
|
|
104
|
+
}
|
|
105
|
+
devtimeWarn("normal", url, `No baked normal sibling for ${url} and no descriptor passed. Either pre-bake (\`npx flatland-bake normal\`), pass a \`descriptor\` to NormalMapLoader.load() for in-memory bake, or use SpriteSheetLoader/LDtkLoader with \`normals: true\` (which synthesizes a descriptor for you).`);
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
static async _tryLoadBaked(spriteURL) {
|
|
109
|
+
const bakedURL = bakedNormalURL(spriteURL);
|
|
110
|
+
let head;
|
|
111
|
+
try {
|
|
112
|
+
head = await fetch(bakedURL, { method: "HEAD" });
|
|
113
|
+
} catch {
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
if (!head.ok) return null;
|
|
117
|
+
return new Promise((resolve, _reject) => {
|
|
118
|
+
new TextureLoader().load(bakedURL, (tex) => resolve(tex), void 0, (err) => {
|
|
119
|
+
console.warn(`[normal] Found ${bakedURL} via HEAD but TextureLoader failed — falling back to runtime.`, err);
|
|
120
|
+
resolve(null);
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
};
|
|
125
|
+
/** Clear the devtime-warning dedupe set. Intended for tests. */
|
|
122
126
|
function _resetDevtimeWarnings() {
|
|
123
|
-
|
|
127
|
+
_resetDevtimeWarnings$1();
|
|
124
128
|
}
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
};
|
|
129
|
+
//#endregion
|
|
130
|
+
export { NormalMapLoader, _resetDevtimeWarnings };
|
|
131
|
+
|
|
129
132
|
//# sourceMappingURL=NormalMapLoader.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/NormalMapLoader.ts"],"sourcesContent":["import { Loader, type LoadingManager, TextureLoader, type Texture } from 'three'\nimport {\n devtimeWarn as sharedDevtimeWarn,\n _resetDevtimeWarnings as sharedResetDevtimeWarnings,\n hashDescriptor,\n type BakedAssetLoaderOptions,\n} from '@three-flatland/bake'\nimport { bakedNormalURL } from './bake.js'\nimport type { NormalSourceDescriptor } from './descriptor.js'\n\n/**\n * Options accepted by `NormalMapLoader.load()`. Inherits `forceRuntime`\n * from the shared {@link BakedAssetLoaderOptions} so every baked-asset\n * loader in the codebase advertises the same option shape.\n */\nexport interface NormalMapLoaderStaticOptions extends BakedAssetLoaderOptions {\n /**\n * When provided, missing sidecars trigger an in-memory bake via\n * `resolveNormalMap`. Without a descriptor, `NormalMapLoader.load()` can\n * only probe the baked sibling and returns `null` on miss (legacy\n * behavior, preserved for backward compat).\n */\n descriptor?: NormalSourceDescriptor\n}\n\n/**\n * Result of loading a sprite's normal data.\n *\n * Either a baked normal texture (fast path) or `null`, signalling the caller\n * to use the runtime TSL `normalFromSprite` helper against the sprite's own\n * alpha channel.\n */\nexport type NormalMapResult = Texture | null\n\n/**\n * Loader for per-sprite normal maps following the canonical three-flatland\n * \"try baked → fall back to runtime\" pattern documented in\n * `planning/bake/loader-pattern.md`.\n *\n * Given a sprite PNG URL, this loader fetches the sibling `.normal.png` that\n * `flatland-bake normal` produces. If it is absent, the loader resolves to\n * `null` so the lit material can switch to its TSL runtime fallback.\n *\n * Only the baked path uses the network — the runtime branch is a shader\n * concern and lives in `@three-flatland/nodes/lighting/normalFromSprite`.\n *\n * @example\n * ```ts\n * // Vanilla static API\n * const normalTex = await NormalMapLoader.load('/sprites/knight.png')\n * if (normalTex) material.normalMap = normalTex\n * else material.useRuntimeNormals = true\n *\n * // Skip the baked probe — go straight to runtime (or null without descriptor)\n * const tex = await NormalMapLoader.load(url, { forceRuntime: true })\n *\n * // R3F useLoader\n * const tex = useLoader(NormalMapLoader, '/sprites/knight.png')\n * ```\n */\nexport class NormalMapLoader extends Loader<NormalMapResult> {\n /**\n * Generate this asset's normal map in the browser on every load\n * instead of loading a pre-baked `.normal.png` sidecar. With a\n * `descriptor`, the in-memory bake runs on every load. Without a\n * descriptor, the loader resolves directly to `null` and the caller\n * uses the TSL runtime fallback. Suppresses the \"no baked sibling\"\n * warn either way.\n *\n * Use when runtime is the right home for the bake. Not a dev-iteration\n * knob — the default path (probe → bake on miss + warn) already\n * handles iteration. See {@link BakedAssetLoaderOptions.forceRuntime}.\n */\n forceRuntime = false\n /**\n * When set, missing sidecars trigger an in-memory bake via\n * `resolveNormalMap`. Without a descriptor, the loader can only probe\n * the baked sibling and returns `null` on miss (legacy behavior).\n *\n * Set via the `useLoader` callback:\n * ```ts\n * useLoader(NormalMapLoader, url, undefined, (l) => { l.descriptor = desc })\n * ```\n */\n descriptor: NormalSourceDescriptor | undefined = undefined\n\n constructor(manager?: LoadingManager) {\n super(manager)\n }\n\n // ─── Instance API (R3F useLoader compatibility) ───\n\n load(\n url: string,\n onLoad?: (data: NormalMapResult) => void,\n onProgress?: (event: ProgressEvent) => void,\n onError?: (err: unknown) => void\n ): NormalMapResult {\n const resolved = this.manager.resolveURL(url)\n\n NormalMapLoader._loadImpl(resolved, this.forceRuntime, this.descriptor)\n .then((result) => {\n onLoad?.(result)\n })\n .catch((err) => {\n if (onError) onError(err)\n else console.error('NormalMapLoader:', err)\n this.manager.itemError(url)\n })\n\n return null\n }\n\n loadAsync(url: string): Promise<NormalMapResult> {\n return NormalMapLoader._loadImpl(
|
|
1
|
+
{"version":3,"file":"NormalMapLoader.js","names":[],"sources":["../src/NormalMapLoader.ts"],"sourcesContent":["import { Loader, type LoadingManager, TextureLoader, type Texture } from 'three'\nimport {\n devtimeWarn as sharedDevtimeWarn,\n _resetDevtimeWarnings as sharedResetDevtimeWarnings,\n hashDescriptor,\n type BakedAssetLoaderOptions,\n} from '@three-flatland/bake'\nimport { bakedNormalURL } from './bake.js'\nimport type { NormalSourceDescriptor } from './descriptor.js'\n\n/**\n * Options accepted by `NormalMapLoader.load()`. Inherits `forceRuntime`\n * from the shared {@link BakedAssetLoaderOptions} so every baked-asset\n * loader in the codebase advertises the same option shape.\n */\nexport interface NormalMapLoaderStaticOptions extends BakedAssetLoaderOptions {\n /**\n * When provided, missing sidecars trigger an in-memory bake via\n * `resolveNormalMap`. Without a descriptor, `NormalMapLoader.load()` can\n * only probe the baked sibling and returns `null` on miss (legacy\n * behavior, preserved for backward compat).\n */\n descriptor?: NormalSourceDescriptor\n}\n\n/**\n * Result of loading a sprite's normal data.\n *\n * Either a baked normal texture (fast path) or `null`, signalling the caller\n * to use the runtime TSL `normalFromSprite` helper against the sprite's own\n * alpha channel.\n */\nexport type NormalMapResult = Texture | null\n\n/**\n * Loader for per-sprite normal maps following the canonical three-flatland\n * \"try baked → fall back to runtime\" pattern documented in\n * `planning/bake/loader-pattern.md`.\n *\n * Given a sprite PNG URL, this loader fetches the sibling `.normal.png` that\n * `flatland-bake normal` produces. If it is absent, the loader resolves to\n * `null` so the lit material can switch to its TSL runtime fallback.\n *\n * Only the baked path uses the network — the runtime branch is a shader\n * concern and lives in `@three-flatland/nodes/lighting/normalFromSprite`.\n *\n * @example\n * ```ts\n * // Vanilla static API\n * const normalTex = await NormalMapLoader.load('/sprites/knight.png')\n * if (normalTex) material.normalMap = normalTex\n * else material.useRuntimeNormals = true\n *\n * // Skip the baked probe — go straight to runtime (or null without descriptor)\n * const tex = await NormalMapLoader.load(url, { forceRuntime: true })\n *\n * // R3F useLoader\n * const tex = useLoader(NormalMapLoader, '/sprites/knight.png')\n * ```\n */\nexport class NormalMapLoader extends Loader<NormalMapResult> {\n /**\n * Generate this asset's normal map in the browser on every load\n * instead of loading a pre-baked `.normal.png` sidecar. With a\n * `descriptor`, the in-memory bake runs on every load. Without a\n * descriptor, the loader resolves directly to `null` and the caller\n * uses the TSL runtime fallback. Suppresses the \"no baked sibling\"\n * warn either way.\n *\n * Use when runtime is the right home for the bake. Not a dev-iteration\n * knob — the default path (probe → bake on miss + warn) already\n * handles iteration. See {@link BakedAssetLoaderOptions.forceRuntime}.\n */\n forceRuntime = false\n /**\n * When set, missing sidecars trigger an in-memory bake via\n * `resolveNormalMap`. Without a descriptor, the loader can only probe\n * the baked sibling and returns `null` on miss (legacy behavior).\n *\n * Set via the `useLoader` callback:\n * ```ts\n * useLoader(NormalMapLoader, url, undefined, (l) => { l.descriptor = desc })\n * ```\n */\n descriptor: NormalSourceDescriptor | undefined = undefined\n\n constructor(manager?: LoadingManager) {\n super(manager)\n }\n\n // ─── Instance API (R3F useLoader compatibility) ───\n\n load(\n url: string,\n onLoad?: (data: NormalMapResult) => void,\n onProgress?: (event: ProgressEvent) => void,\n onError?: (err: unknown) => void\n ): NormalMapResult {\n const resolved = this.manager.resolveURL(url)\n\n NormalMapLoader._loadImpl(resolved, this.forceRuntime, this.descriptor)\n .then((result) => {\n onLoad?.(result)\n })\n .catch((err) => {\n if (onError) onError(err)\n else console.error('NormalMapLoader:', err)\n this.manager.itemError(url)\n })\n\n return null\n }\n\n loadAsync(url: string): Promise<NormalMapResult> {\n return NormalMapLoader._loadImpl(this.manager.resolveURL(url), this.forceRuntime, this.descriptor)\n }\n\n // ─── Static API (vanilla usage) ───\n\n /**\n * Vanilla cache: keyed by `(url, forceRuntime, descriptor hash)` so\n * two callers passing *different* descriptors for the same URL get\n * distinct results instead of colliding on the first one's bake.\n *\n * Instance API (R3F `useLoader`) bypasses this map — R3F has its own\n * suspense cache and we don't want double-caching to fight the lifecycle.\n */\n private static _cache = new Map<string, Promise<NormalMapResult>>()\n\n static load(url: string, options?: NormalMapLoaderStaticOptions): Promise<NormalMapResult> {\n const forceRuntime = options?.forceRuntime ?? false\n const descriptor = options?.descriptor\n // Hash the descriptor so distinct descriptors for the same URL get\n // distinct cache entries. `descriptor ? 'desc' : 'nodesc'` would have\n // collapsed every (URL, *) pair into one slot — first-bake-wins.\n const descriptorKey = descriptor ? hashDescriptor(descriptor) : 'nodesc'\n const cacheKey = `${url}:${forceRuntime ? 'runtime' : 'probe'}:${descriptorKey}`\n const cached = this._cache.get(cacheKey)\n if (cached) return cached\n\n const promise = this._loadImpl(url, forceRuntime, descriptor)\n this._cache.set(cacheKey, promise)\n return promise\n }\n\n static clearCache(): void {\n this._cache.clear()\n }\n\n // ─── Implementation ───\n\n private static async _loadImpl(\n url: string,\n forceRuntime: boolean,\n descriptor: NormalSourceDescriptor | undefined\n ): Promise<NormalMapResult> {\n // With a descriptor we can do the full resolve: try baked → in-memory bake.\n if (descriptor) {\n const { resolveNormalMap } = await import('./resolveNormalMap.js')\n return resolveNormalMap(url, descriptor, { forceRuntime })\n }\n\n // No descriptor → legacy URL-only behavior: probe sidecar, return null on miss.\n if (!forceRuntime) {\n const baked = await this._tryLoadBaked(url)\n if (baked) return baked\n }\n\n sharedDevtimeWarn(\n 'normal',\n url,\n `No baked normal sibling for ${url} and no descriptor passed. ` +\n `Either pre-bake (\\`npx flatland-bake normal\\`), pass a \\`descriptor\\` to ` +\n `NormalMapLoader.load() for in-memory bake, or use SpriteSheetLoader/LDtkLoader ` +\n `with \\`normals: true\\` (which synthesizes a descriptor for you).`\n )\n return null\n }\n\n private static async _tryLoadBaked(spriteURL: string): Promise<Texture | null> {\n const bakedURL = bakedNormalURL(spriteURL)\n\n // Probe with HEAD first so a 404 stays silent. TextureLoader swallows\n // network errors into a generic event, which is not what we want.\n let head: Response\n try {\n head = await fetch(bakedURL, { method: 'HEAD' })\n } catch {\n return null\n }\n if (!head.ok) return null\n\n return new Promise((resolve, _reject) => {\n const loader = new TextureLoader()\n loader.load(\n bakedURL,\n (tex) => resolve(tex as unknown as Texture),\n undefined,\n (err) => {\n console.warn(`[normal] Found ${bakedURL} via HEAD but TextureLoader failed — falling back to runtime.`, err)\n resolve(null)\n }\n )\n })\n }\n}\n\n/** Clear the devtime-warning dedupe set. Intended for tests. */\nexport function _resetDevtimeWarnings(): void {\n sharedResetDevtimeWarnings()\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA4DA,IAAa,kBAAb,MAAa,wBAAwB,OAAwB;;;;;;;;;;;;;CAa3D,eAAe;;;;;;;;;;;CAWf,aAAiD,KAAA;CAEjD,YAAY,SAA0B;EACpC,MAAM,OAAO;CACf;CAIA,KACE,KACA,QACA,YACA,SACiB;EACjB,MAAM,WAAW,KAAK,QAAQ,WAAW,GAAG;EAE5C,gBAAgB,UAAU,UAAU,KAAK,cAAc,KAAK,UAAU,CAAC,CACpE,MAAM,WAAW;GAChB,SAAS,MAAM;EACjB,CAAC,CAAC,CACD,OAAO,QAAQ;GACd,IAAI,SAAS,QAAQ,GAAG;QACnB,QAAQ,MAAM,oBAAoB,GAAG;GAC1C,KAAK,QAAQ,UAAU,GAAG;EAC5B,CAAC;EAEH,OAAO;CACT;CAEA,UAAU,KAAuC;EAC/C,OAAO,gBAAgB,UAAU,KAAK,QAAQ,WAAW,GAAG,GAAG,KAAK,cAAc,KAAK,UAAU;CACnG;;;;;;;;;CAYA,OAAe,yBAAS,IAAI,IAAsC;CAElE,OAAO,KAAK,KAAa,SAAkE;EACzF,MAAM,eAAe,SAAS,gBAAgB;EAC9C,MAAM,aAAa,SAAS;EAI5B,MAAM,gBAAgB,aAAa,eAAe,UAAU,IAAI;EAChE,MAAM,WAAW,GAAG,IAAI,GAAG,eAAe,YAAY,QAAQ,GAAG;EACjE,MAAM,SAAS,KAAK,OAAO,IAAI,QAAQ;EACvC,IAAI,QAAQ,OAAO;EAEnB,MAAM,UAAU,KAAK,UAAU,KAAK,cAAc,UAAU;EAC5D,KAAK,OAAO,IAAI,UAAU,OAAO;EACjC,OAAO;CACT;CAEA,OAAO,aAAmB;EACxB,KAAK,OAAO,MAAM;CACpB;CAIA,aAAqB,UACnB,KACA,cACA,YAC0B;EAE1B,IAAI,YAAY;GACd,MAAM,EAAE,qBAAqB,MAAM,OAAO;GAC1C,OAAO,iBAAiB,KAAK,YAAY,EAAE,aAAa,CAAC;EAC3D;EAGA,IAAI,CAAC,cAAc;GACjB,MAAM,QAAQ,MAAM,KAAK,cAAc,GAAG;GAC1C,IAAI,OAAO,OAAO;EACpB;EAEA,YACE,UACA,KACA,+BAA+B,IAAI,oPAIrC;EACA,OAAO;CACT;CAEA,aAAqB,cAAc,WAA4C;EAC7E,MAAM,WAAW,eAAe,SAAS;EAIzC,IAAI;EACJ,IAAI;GACF,OAAO,MAAM,MAAM,UAAU,EAAE,QAAQ,OAAO,CAAC;EACjD,QAAQ;GACN,OAAO;EACT;EACA,IAAI,CAAC,KAAK,IAAI,OAAO;EAErB,OAAO,IAAI,SAAS,SAAS,YAAY;GAEvC,IADmB,cACd,CAAC,CAAC,KACL,WACC,QAAQ,QAAQ,GAAyB,GAC1C,KAAA,IACC,QAAQ;IACP,QAAQ,KAAK,kBAAkB,SAAS,gEAAgE,GAAG;IAC3G,QAAQ,IAAI;GACd,CACF;EACF,CAAC;CACH;AACF;;AAGA,SAAgB,wBAA8B;CAC5C,wBAA2B;AAC7B"}
|
package/dist/bake.d.ts
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { NormalSourceDescriptor } from "./descriptor.js";
|
|
2
|
+
//#region src/bake.d.ts
|
|
3
|
+
/**
|
|
4
|
+
* @deprecated — legacy flat-texture bake options. Use
|
|
5
|
+
* `bakeNormalMap(pixels, w, h, descriptor)` for region-aware control.
|
|
6
|
+
*/
|
|
7
|
+
interface BakeOptions {
|
|
8
|
+
/** Scales the alpha gradient before normalization. Default 1. */
|
|
9
|
+
strength?: number;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Produce a tangent-space normal map from source pixels + a descriptor.
|
|
13
|
+
*
|
|
14
|
+
* Cross-platform: browser + node. No filesystem, no pngjs. Used by
|
|
15
|
+
* the CLI baker (wrapped with file I/O) and the runtime loader
|
|
16
|
+
* (called directly on decoded image pixels).
|
|
17
|
+
*
|
|
18
|
+
* Per texel in each region:
|
|
19
|
+
* 1. `bump: 'alpha'` — central-difference gradient on the source
|
|
20
|
+
* alpha channel, clamped to the region bounds so adjacent regions
|
|
21
|
+
* (e.g. atlas cells) can't bleed into each other.
|
|
22
|
+
* 2. Compose with a tilt rotation that takes `+Z` to the region's
|
|
23
|
+
* `direction` at `pitch` radians.
|
|
24
|
+
* 3. Normalize and encode to `rgb = normal * 0.5 + 0.5`. Alpha is
|
|
25
|
+
* copied from the source so the baked map carries its own
|
|
26
|
+
* silhouette.
|
|
27
|
+
*
|
|
28
|
+
* Texels outside every region receive the flat normal `(0, 0, 1)`
|
|
29
|
+
* with the source's alpha — safe default for sparse descriptors.
|
|
30
|
+
*
|
|
31
|
+
* @param pixels RGBA pixel buffer, row-major, 4 bytes per pixel.
|
|
32
|
+
* @param width Pixel width.
|
|
33
|
+
* @param height Pixel height.
|
|
34
|
+
* @param descriptor Region / tilt / bump control. Defaults to a single
|
|
35
|
+
* whole-texture flat region with alpha-derived bump.
|
|
36
|
+
* @returns RGBA pixel buffer containing the encoded normal map.
|
|
37
|
+
*/
|
|
38
|
+
declare function bakeNormalMap(pixels: Uint8Array, width: number, height: number, descriptor?: NormalSourceDescriptor): Uint8Array;
|
|
39
|
+
/**
|
|
40
|
+
* Legacy back-compat wrapper. Use `bakeNormalMap(pixels, w, h, {
|
|
41
|
+
* strength })` for equivalent behavior in new code.
|
|
42
|
+
*
|
|
43
|
+
* @deprecated
|
|
44
|
+
*/
|
|
45
|
+
declare function bakeNormalMapFromPixels(pixels: Uint8Array, width: number, height: number, options?: BakeOptions): Uint8Array;
|
|
46
|
+
/**
|
|
47
|
+
* Derive the conventional `.normal.png` sibling URL for a sprite PNG.
|
|
48
|
+
*
|
|
49
|
+
* Runtime loaders call this to try the baked output before falling
|
|
50
|
+
* back to the runtime TSL path.
|
|
51
|
+
*
|
|
52
|
+
* Pure string rewrite — browser-safe, no filesystem.
|
|
53
|
+
*/
|
|
54
|
+
declare function bakedNormalURL(spriteURL: string): string;
|
|
55
|
+
//#endregion
|
|
56
|
+
export { BakeOptions, bakeNormalMap, bakeNormalMapFromPixels, bakedNormalURL };
|
|
57
|
+
//# sourceMappingURL=bake.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bake.d.ts","names":[],"sources":["../src/bake.ts"],"mappings":";;;;;;UAaiB;;EAEf;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBA8Bc,cACd,QAAQ,YACR,eACA,gBACA,aAAY,yBACX;;;;;;;iBAmKa,wBACd,QAAQ,YACR,eACA,gBACA,UAAS,cACR;;;;;;;;;iBAiBa,eAAe"}
|