@three-flatland/normals 0.1.0-alpha.1
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/LICENSE +21 -0
- package/README.md +223 -0
- package/dist/NormalMapLoader.js +129 -0
- package/dist/NormalMapLoader.js.map +1 -0
- package/dist/bake.js +123 -0
- package/dist/bake.js.map +1 -0
- package/dist/bake.node.js +38 -0
- package/dist/bake.node.js.map +1 -0
- package/dist/cli.d.ts +5 -0
- package/dist/cli.js +128 -0
- package/dist/cli.js.map +1 -0
- package/dist/descriptor.js +61 -0
- package/dist/descriptor.js.map +1 -0
- package/dist/index.d.ts +320 -0
- package/dist/index.js +33 -0
- package/dist/index.js.map +1 -0
- package/dist/node.d.ts +21 -0
- package/dist/node.js +6 -0
- package/dist/node.js.map +1 -0
- package/dist/resolveNormalMap.js +97 -0
- package/dist/resolveNormalMap.js.map +1 -0
- package/package.json +82 -0
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DataTexture,
|
|
3
|
+
RGBAFormat,
|
|
4
|
+
UnsignedByteType,
|
|
5
|
+
TextureLoader as ThreeTextureLoader
|
|
6
|
+
} from "three";
|
|
7
|
+
import {
|
|
8
|
+
bakedSiblingURL,
|
|
9
|
+
devtimeWarn,
|
|
10
|
+
hashDescriptor,
|
|
11
|
+
probeBakedSibling
|
|
12
|
+
} from "@three-flatland/bake";
|
|
13
|
+
async function resolveNormalMap(sourceURL, descriptor, options = {}) {
|
|
14
|
+
const hash = hashDescriptor(descriptor);
|
|
15
|
+
if (!options.forceRuntime) {
|
|
16
|
+
const bakedURL = bakedSiblingURL(sourceURL, ".normal.png");
|
|
17
|
+
const probe = await probeBakedSibling(bakedURL, { expectedHash: hash });
|
|
18
|
+
if (probe.ok && probe.hashMatches) {
|
|
19
|
+
const tex2 = await loadTextureURL(bakedURL);
|
|
20
|
+
if (options.flipY !== void 0) tex2.flipY = options.flipY;
|
|
21
|
+
tex2.needsUpdate = true;
|
|
22
|
+
return tex2;
|
|
23
|
+
}
|
|
24
|
+
if (probe.ok && !probe.hashMatches) {
|
|
25
|
+
devtimeWarn(
|
|
26
|
+
"normal",
|
|
27
|
+
sourceURL,
|
|
28
|
+
`${bakedURL} exists but its descriptor hash is stale \u2014 re-baking in memory. Run \`npx flatland-bake normal ${sourceURL} --descriptor <descriptor>.json\` to refresh.`
|
|
29
|
+
);
|
|
30
|
+
} else {
|
|
31
|
+
devtimeWarn(
|
|
32
|
+
"normal",
|
|
33
|
+
sourceURL,
|
|
34
|
+
`No baked sibling at ${bakedURL} \u2014 baking in memory. Run \`npx flatland-bake normal\` for production.`
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
const tex = await bakeInMemory(sourceURL, descriptor);
|
|
39
|
+
if (options.flipY !== void 0) tex.flipY = options.flipY;
|
|
40
|
+
tex.needsUpdate = true;
|
|
41
|
+
return tex;
|
|
42
|
+
}
|
|
43
|
+
async function loadTextureURL(url) {
|
|
44
|
+
return new Promise((resolve, reject) => {
|
|
45
|
+
const loader = new ThreeTextureLoader();
|
|
46
|
+
loader.load(
|
|
47
|
+
url,
|
|
48
|
+
(tex) => resolve(tex),
|
|
49
|
+
void 0,
|
|
50
|
+
(err) => reject(err)
|
|
51
|
+
);
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
async function bakeInMemory(sourceURL, descriptor) {
|
|
55
|
+
const res = await fetch(sourceURL);
|
|
56
|
+
if (!res.ok) {
|
|
57
|
+
throw new Error(`resolveNormalMap: failed to fetch ${sourceURL} (${res.status})`);
|
|
58
|
+
}
|
|
59
|
+
const blob = await res.blob();
|
|
60
|
+
const bitmap = await createImageBitmap(blob);
|
|
61
|
+
const { width, height } = bitmap;
|
|
62
|
+
const pixels = imageBitmapToRGBA(bitmap, width, height);
|
|
63
|
+
bitmap.close();
|
|
64
|
+
const { bakeNormalMap } = await import("./bake.js");
|
|
65
|
+
const normalPixels = bakeNormalMap(pixels, width, height, descriptor);
|
|
66
|
+
const texture = new DataTexture(
|
|
67
|
+
normalPixels,
|
|
68
|
+
width,
|
|
69
|
+
height,
|
|
70
|
+
RGBAFormat,
|
|
71
|
+
UnsignedByteType
|
|
72
|
+
);
|
|
73
|
+
texture.needsUpdate = true;
|
|
74
|
+
return texture;
|
|
75
|
+
}
|
|
76
|
+
function imageBitmapToRGBA(bitmap, width, height) {
|
|
77
|
+
if (typeof OffscreenCanvas !== "undefined") {
|
|
78
|
+
const canvas2 = new OffscreenCanvas(width, height);
|
|
79
|
+
const ctx2 = canvas2.getContext("2d");
|
|
80
|
+
if (!ctx2) throw new Error("resolveNormalMap: OffscreenCanvas 2D context unavailable");
|
|
81
|
+
ctx2.drawImage(bitmap, 0, 0);
|
|
82
|
+
const data2 = ctx2.getImageData(0, 0, width, height);
|
|
83
|
+
return new Uint8Array(data2.data.buffer, data2.data.byteOffset, data2.data.byteLength);
|
|
84
|
+
}
|
|
85
|
+
const canvas = document.createElement("canvas");
|
|
86
|
+
canvas.width = width;
|
|
87
|
+
canvas.height = height;
|
|
88
|
+
const ctx = canvas.getContext("2d");
|
|
89
|
+
if (!ctx) throw new Error("resolveNormalMap: Canvas 2D context unavailable");
|
|
90
|
+
ctx.drawImage(bitmap, 0, 0);
|
|
91
|
+
const data = ctx.getImageData(0, 0, width, height);
|
|
92
|
+
return new Uint8Array(data.data.buffer, data.data.byteOffset, data.data.byteLength);
|
|
93
|
+
}
|
|
94
|
+
export {
|
|
95
|
+
resolveNormalMap
|
|
96
|
+
};
|
|
97
|
+
//# sourceMappingURL=resolveNormalMap.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/resolveNormalMap.ts"],"sourcesContent":["import {\n DataTexture,\n RGBAFormat,\n UnsignedByteType,\n TextureLoader as ThreeTextureLoader,\n type Texture,\n} from 'three'\nimport {\n bakedSiblingURL,\n devtimeWarn,\n hashDescriptor,\n probeBakedSibling,\n} from '@three-flatland/bake'\nimport type { NormalSourceDescriptor } from './descriptor.js'\n\nexport interface ResolveNormalMapOptions {\n /**\n * Generate this asset's normal map in the browser on every load\n * instead of loading a pre-baked `.normal.png` sidecar. Fetches the\n * source image, decodes its pixels, and runs `bakeNormalMap` in\n * memory each time — no HEAD probe, no devtime \"no baked sibling\"\n * warning.\n *\n * The returned data is always a valid normal map: this flag chooses\n * *where* the bake happens (browser vs CI), not whether you get one.\n *\n * Use when runtime is the right home for the bake — procedurally\n * varied content, throwaway prototypes, asset bundles where shipping\n * the sidecar isn't worth the bytes. Not a dev-iteration knob: the\n * default path (probe → bake on miss + warn) already handles that.\n *\n * Mirrors `SlugFontLoader.forceRuntime` and every other baked-asset\n * loader in the codebase.\n */\n forceRuntime?: boolean\n /**\n * Force the returned texture's `flipY` to this value. Pass the\n * diffuse texture's `flipY` so the normal map samples 1:1 with the\n * diffuse on the GPU regardless of whether the normal came from the\n * image-loader path (default true) or the in-memory `DataTexture`\n * path (default false).\n */\n flipY?: boolean\n}\n\n/**\n * Resolve the normal map for an asset URL + descriptor.\n *\n * 1. Hash the descriptor.\n * 2. Probe the baked sibling `<source>.normal.png` (unless\n * `forceRuntime`). If present and its stamped hash matches, load\n * and return that texture directly.\n * 3. On miss (or stale hash), fetch the source, decode its pixels,\n * lazy-import + run `bakeNormalMap` in memory, and wrap the result\n * in a `DataTexture`. Runtime bake is the always-on fallback when\n * normals were requested.\n *\n * Browser-only — uses `fetch`, `createImageBitmap`, `OffscreenCanvas`.\n * Safe to call from any Three.js loader runtime.\n */\nexport async function resolveNormalMap(\n sourceURL: string,\n descriptor: NormalSourceDescriptor,\n options: ResolveNormalMapOptions = {}\n): Promise<Texture> {\n const hash = hashDescriptor(descriptor)\n\n if (!options.forceRuntime) {\n const bakedURL = bakedSiblingURL(sourceURL, '.normal.png')\n const probe = await probeBakedSibling(bakedURL, { expectedHash: hash })\n if (probe.ok && probe.hashMatches) {\n const tex = await loadTextureURL(bakedURL)\n if (options.flipY !== undefined) tex.flipY = options.flipY\n tex.needsUpdate = true\n return tex\n }\n if (probe.ok && !probe.hashMatches) {\n devtimeWarn(\n 'normal',\n sourceURL,\n `${bakedURL} exists but its descriptor hash is stale — re-baking in memory. ` +\n `Run \\`npx flatland-bake normal ${sourceURL} --descriptor <descriptor>.json\\` to refresh.`\n )\n } else {\n devtimeWarn(\n 'normal',\n sourceURL,\n `No baked sibling at ${bakedURL} — baking in memory. ` +\n `Run \\`npx flatland-bake normal\\` for production.`\n )\n }\n }\n\n const tex = await bakeInMemory(sourceURL, descriptor)\n if (options.flipY !== undefined) tex.flipY = options.flipY\n tex.needsUpdate = true\n return tex\n}\n\n// ─── Private ──────────────────────────────────────────────────────────────\n\nasync function loadTextureURL(url: string): Promise<Texture> {\n return new Promise((resolve, reject) => {\n const loader = new ThreeTextureLoader()\n loader.load(\n url,\n (tex) => resolve(tex as Texture),\n undefined,\n (err) => reject(err as Error)\n )\n })\n}\n\nasync function bakeInMemory(\n sourceURL: string,\n descriptor: NormalSourceDescriptor\n): Promise<Texture> {\n const res = await fetch(sourceURL)\n if (!res.ok) {\n throw new Error(`resolveNormalMap: failed to fetch ${sourceURL} (${res.status})`)\n }\n const blob = await res.blob()\n const bitmap = await createImageBitmap(blob)\n const { width, height } = bitmap\n const pixels = imageBitmapToRGBA(bitmap, width, height)\n bitmap.close()\n\n const { bakeNormalMap } = await import('./bake.js')\n const normalPixels = bakeNormalMap(pixels, width, height, descriptor)\n const texture = new DataTexture(\n normalPixels,\n width,\n height,\n RGBAFormat,\n UnsignedByteType\n )\n texture.needsUpdate = true\n return texture\n}\n\nfunction imageBitmapToRGBA(\n bitmap: ImageBitmap,\n width: number,\n height: number\n): Uint8Array {\n // OffscreenCanvas where available (workers + modern browsers), fall\n // back to a regular 2D canvas in environments that don't ship it.\n if (typeof OffscreenCanvas !== 'undefined') {\n const canvas = new OffscreenCanvas(width, height)\n const ctx = canvas.getContext('2d')\n if (!ctx) throw new Error('resolveNormalMap: OffscreenCanvas 2D context unavailable')\n ctx.drawImage(bitmap, 0, 0)\n const data = ctx.getImageData(0, 0, width, height)\n return new Uint8Array(data.data.buffer, data.data.byteOffset, data.data.byteLength)\n }\n const canvas = document.createElement('canvas')\n canvas.width = width\n canvas.height = height\n const ctx = canvas.getContext('2d')\n if (!ctx) throw new Error('resolveNormalMap: Canvas 2D context unavailable')\n ctx.drawImage(bitmap, 0, 0)\n const data = ctx.getImageData(0, 0, width, height)\n return new Uint8Array(data.data.buffer, data.data.byteOffset, data.data.byteLength)\n}\n\n"],"mappings":"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","names":["tex","canvas","ctx","data"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@three-flatland/normals",
|
|
3
|
+
"version": "0.1.0-alpha.1",
|
|
4
|
+
"description": "Offline + runtime normal map generation for sprites and tilesets. Contributes a `normal` subcommand to the flatland-bake CLI.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"exports": {
|
|
7
|
+
".": {
|
|
8
|
+
"source": "./src/index.ts",
|
|
9
|
+
"node": {
|
|
10
|
+
"types": "./dist/node.d.ts",
|
|
11
|
+
"import": "./dist/node.js"
|
|
12
|
+
},
|
|
13
|
+
"browser": {
|
|
14
|
+
"types": "./dist/index.d.ts",
|
|
15
|
+
"import": "./dist/index.js"
|
|
16
|
+
},
|
|
17
|
+
"default": {
|
|
18
|
+
"types": "./dist/index.d.ts",
|
|
19
|
+
"import": "./dist/index.js"
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
"./node": {
|
|
23
|
+
"source": "./src/node.ts",
|
|
24
|
+
"types": "./dist/node.d.ts",
|
|
25
|
+
"import": "./dist/node.js"
|
|
26
|
+
},
|
|
27
|
+
"./cli": {
|
|
28
|
+
"source": "./src/cli.ts",
|
|
29
|
+
"types": "./dist/cli.d.ts",
|
|
30
|
+
"import": "./dist/cli.js"
|
|
31
|
+
},
|
|
32
|
+
"./bake": {
|
|
33
|
+
"source": "./src/bake.ts",
|
|
34
|
+
"types": "./dist/bake.d.ts",
|
|
35
|
+
"import": "./dist/bake.js"
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
"module": "./dist/index.js",
|
|
39
|
+
"types": "./dist/index.d.ts",
|
|
40
|
+
"files": [
|
|
41
|
+
"dist"
|
|
42
|
+
],
|
|
43
|
+
"sideEffects": false,
|
|
44
|
+
"dependencies": {
|
|
45
|
+
"pngjs": "^7.0.0",
|
|
46
|
+
"@three-flatland/bake": "0.1.0-alpha.1"
|
|
47
|
+
},
|
|
48
|
+
"peerDependencies": {
|
|
49
|
+
"three": ">=0.170.0"
|
|
50
|
+
},
|
|
51
|
+
"devDependencies": {
|
|
52
|
+
"@types/pngjs": "^6.0.5",
|
|
53
|
+
"@types/three": "^0.183.1",
|
|
54
|
+
"three": "^0.183.1"
|
|
55
|
+
},
|
|
56
|
+
"flatland": {
|
|
57
|
+
"bake": [
|
|
58
|
+
{
|
|
59
|
+
"name": "normal",
|
|
60
|
+
"description": "Bake a tangent-space normal map from a sprite PNG",
|
|
61
|
+
"entry": "./dist/cli.js"
|
|
62
|
+
}
|
|
63
|
+
]
|
|
64
|
+
},
|
|
65
|
+
"keywords": [
|
|
66
|
+
"three-flatland",
|
|
67
|
+
"normal-map",
|
|
68
|
+
"bake"
|
|
69
|
+
],
|
|
70
|
+
"license": "MIT",
|
|
71
|
+
"repository": {
|
|
72
|
+
"type": "git",
|
|
73
|
+
"url": "https://github.com/thejustinwalsh/three-flatland.git",
|
|
74
|
+
"directory": "packages/normals"
|
|
75
|
+
},
|
|
76
|
+
"scripts": {
|
|
77
|
+
"build": "tsup",
|
|
78
|
+
"dev": "tsup --watch",
|
|
79
|
+
"typecheck": "tsc --noEmit",
|
|
80
|
+
"clean": "rm -rf dist"
|
|
81
|
+
}
|
|
82
|
+
}
|