@developerz.ai/ts23d 0.1.0 → 0.2.0
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/CHANGELOG.md +11 -0
- package/CLAUDE.md +1 -1
- package/README.md +10 -5
- package/dist/{chunk-a8fxakcj.js → chunk-6b05mnmx.js} +2 -2
- package/dist/{chunk-kgvbpqqw.js → chunk-7rtfjh9f.js} +6 -5
- package/dist/chunk-7rtfjh9f.js.map +10 -0
- package/dist/{chunk-1ypfs7ga.js → chunk-8j4c28jt.js} +790 -276
- package/dist/chunk-8j4c28jt.js.map +46 -0
- package/dist/{chunk-wtqhq529.js → chunk-d1fv6hrm.js} +6 -6
- package/dist/{chunk-wtqhq529.js.map → chunk-d1fv6hrm.js.map} +1 -1
- package/dist/{chunk-gcyp9b4d.js → chunk-dbq4qf52.js} +362 -215
- package/dist/chunk-dbq4qf52.js.map +25 -0
- package/dist/{chunk-xnt2sdhf.js → chunk-drna2rny.js} +2 -2
- package/dist/{chunk-ravbb06q.js → chunk-dxvypr7y.js} +2 -2
- package/dist/{chunk-11v28977.js → chunk-f6yme003.js} +85 -71
- package/dist/{chunk-11v28977.js.map → chunk-f6yme003.js.map} +6 -6
- package/dist/{chunk-4ddzsqtc.js → chunk-hg7pf34j.js} +4 -3
- package/dist/{chunk-4ddzsqtc.js.map → chunk-hg7pf34j.js.map} +5 -5
- package/dist/{chunk-w1zyhj40.js → chunk-jq4e17tj.js} +2 -2
- package/dist/{chunk-kvcep7ft.js → chunk-y63c6zcv.js} +967 -218
- package/dist/chunk-y63c6zcv.js.map +53 -0
- package/dist/cli/args.d.ts +7 -1
- package/dist/cli/bin.js +66 -25
- package/dist/cli/bin.js.map +6 -5
- package/dist/cli/manifest.d.ts +5 -1
- package/dist/cli/texture-cache.d.ts +4 -0
- package/dist/core/definition.d.ts +7 -0
- package/dist/core/errors.d.ts +1 -1
- package/dist/core/index.js +1 -1
- package/dist/export/credits.d.ts +4 -0
- package/dist/export/glb-options.d.ts +48 -0
- package/dist/export/glb.d.ts +5 -35
- package/dist/export/hierarchy.d.ts +23 -0
- package/dist/export/index.d.ts +2 -1
- package/dist/export/index.js +21 -7
- package/dist/export/index.js.map +1 -1
- package/dist/export/lod-coverage.d.ts +12 -0
- package/dist/export/metadata.d.ts +2 -0
- package/dist/export/version.d.ts +2 -2
- package/dist/grammar/index.js +2 -2
- package/dist/index.js +63 -17
- package/dist/index.js.map +1 -1
- package/dist/kinds/head-armour.d.ts +3 -7
- package/dist/kinds/head-bust.d.ts +3 -2
- package/dist/kinds/head-face.d.ts +3 -1
- package/dist/kinds/head-garb.d.ts +4 -0
- package/dist/kinds/head-mesh.d.ts +4 -0
- package/dist/kinds/head-mouth.d.ts +4 -0
- package/dist/kinds/head-params.d.ts +9 -1
- package/dist/kinds/head-tone.d.ts +10 -0
- package/dist/kinds/head-torso.d.ts +35 -0
- package/dist/kinds/index.js +6 -6
- package/dist/material/cc0.d.ts +4 -0
- package/dist/material/define.d.ts +34 -0
- package/dist/material/generator.d.ts +1 -1
- package/dist/material/index.d.ts +8 -3
- package/dist/material/index.js +39 -7
- package/dist/material/index.js.map +1 -1
- package/dist/material/library.d.ts +7 -0
- package/dist/material/material-def.d.ts +30 -0
- package/dist/material/paint/canvas.d.ts +7 -0
- package/dist/material/paint/default-sheet.d.ts +3 -1
- package/dist/material/paint/fabric.d.ts +4 -0
- package/dist/material/paint/image-strip.d.ts +4 -0
- package/dist/material/paint/kit.d.ts +5 -0
- package/dist/material/paint/metal.d.ts +3 -0
- package/dist/material/paint/orm.d.ts +1 -1
- package/dist/material/paint/strip.d.ts +1 -1
- package/dist/material/paint/types.d.ts +32 -2
- package/dist/material/palette.d.ts +2 -0
- package/dist/material/slots.d.ts +5 -0
- package/dist/material/texture-source.d.ts +34 -0
- package/dist/material/trim-sheet.d.ts +20 -4
- package/dist/neural/index.js +2 -2
- package/dist/passes/index.js +2 -2
- package/dist/primitives/index.js +3 -3
- package/dist/qa/index.js +2 -2
- package/dist/render/beauty.d.ts +1 -1
- package/dist/render/index.js +3 -3
- package/dist/render/lighting.d.ts +7 -0
- package/dist/render/shade.d.ts +1 -1
- package/dist/testing/index.js +5 -5
- package/docs/README.md +4 -0
- package/docs/architecture/gltf-capabilities.md +38 -1
- package/docs/architecture/materials.md +151 -0
- package/docs/idea/10-sculpting.md +166 -0
- package/docs/idea/11-materials-authoring.md +132 -0
- package/docs/idea/12-blender-bridge.md +136 -0
- package/docs/idea/13-asset-pipeline-research.md +89 -0
- package/docs/plans/roadmap.md +12 -0
- package/package.json +2 -1
- package/recipes/boxcar/README.md +7 -0
- package/recipes/boxcar/boxcar.ts +118 -34
- package/recipes/goblin_head/README.md +27 -14
- package/recipes/goblin_head/goblin_head.ts +11 -9
- package/recipes/human_head/README.md +8 -4
- package/recipes/human_head/human_head.ts +4 -3
- package/recipes/material_swatches/material_swatches.ts +11 -5
- package/recipes/orc_head/README.md +32 -20
- package/recipes/orc_head/orc_head.ts +9 -7
- package/src/cli/args.ts +16 -3
- package/src/cli/manifest.ts +6 -0
- package/src/cli/run.ts +33 -9
- package/src/cli/texture-cache.ts +24 -0
- package/src/core/asset.ts +1 -0
- package/src/core/definition.ts +7 -0
- package/src/core/errors.ts +6 -1
- package/src/export/credits.ts +25 -0
- package/src/export/glb-options.ts +53 -0
- package/src/export/glb.ts +100 -113
- package/src/export/hierarchy.ts +122 -0
- package/src/export/index.ts +17 -1
- package/src/export/lod-coverage.ts +71 -0
- package/src/export/metadata.ts +3 -0
- package/src/export/version.ts +1 -1
- package/src/kinds/head-armour.ts +103 -73
- package/src/kinds/head-bust.ts +49 -86
- package/src/kinds/head-face.ts +94 -74
- package/src/kinds/head-garb.ts +181 -0
- package/src/kinds/head-hair.ts +74 -33
- package/src/kinds/head-mesh.ts +43 -5
- package/src/kinds/head-mouth.ts +111 -0
- package/src/kinds/head-params.ts +11 -2
- package/src/kinds/head-presets.ts +30 -27
- package/src/kinds/head-tone.ts +52 -0
- package/src/kinds/head-torso.ts +230 -0
- package/src/kinds/head.ts +8 -3
- package/src/material/cc0.ts +41 -0
- package/src/material/define.ts +130 -0
- package/src/material/generator.ts +15 -8
- package/src/material/index.ts +34 -3
- package/src/material/library.ts +196 -0
- package/src/material/material-def.ts +84 -0
- package/src/material/paint/canvas.ts +11 -1
- package/src/material/paint/compose.ts +1 -1
- package/src/material/paint/decode-png.ts +19 -13
- package/src/material/paint/default-sheet.ts +40 -28
- package/src/material/paint/fabric.ts +63 -0
- package/src/material/paint/image-strip.ts +88 -0
- package/src/material/paint/kit.ts +37 -0
- package/src/material/paint/masks.ts +3 -1
- package/src/material/paint/metal.ts +109 -0
- package/src/material/paint/orm.ts +14 -1
- package/src/material/paint/planks.ts +49 -4
- package/src/material/paint/strip.ts +35 -29
- package/src/material/paint/types.ts +45 -2
- package/src/material/paint/validate.ts +19 -3
- package/src/material/palette.ts +3 -0
- package/src/material/slots.ts +18 -0
- package/src/material/texture-source.ts +86 -0
- package/src/material/trim-sheet.ts +79 -13
- package/src/render/beauty.ts +2 -2
- package/src/render/lighting.ts +26 -0
- package/src/render/shade.ts +10 -5
- package/dist/chunk-1ypfs7ga.js.map +0 -42
- package/dist/chunk-gcyp9b4d.js.map +0 -22
- package/dist/chunk-kgvbpqqw.js.map +0 -10
- package/dist/chunk-kvcep7ft.js.map +0 -43
- /package/dist/{chunk-a8fxakcj.js.map → chunk-6b05mnmx.js.map} +0 -0
- /package/dist/{chunk-xnt2sdhf.js.map → chunk-drna2rny.js.map} +0 -0
- /package/dist/{chunk-ravbb06q.js.map → chunk-dxvypr7y.js.map} +0 -0
- /package/dist/{chunk-w1zyhj40.js.map → chunk-jq4e17tj.js.map} +0 -0
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
// Project materials: `defineMaterial` validates a MaterialDef, gives it one of the sheet's custom
|
|
2
|
+
// strip rows (in registration order: define materials in one shared module so every recipe sees
|
|
3
|
+
// the same order) and adds it to the house sheet, so `DEFAULT_SHEET` paints it and primitives map
|
|
4
|
+
// onto it. Redefining an id with the same definition is a no-op; a different one is a conflict.
|
|
5
|
+
// `materialOf` answers what any strip is (built-in or defined) for docs, review and tests;
|
|
6
|
+
// `textureSources` / `stripCredits` serve the CC0 image strips (fetch list, GLB credits).
|
|
7
|
+
|
|
8
|
+
import { Ts23dError } from '../core/index.ts';
|
|
9
|
+
import { MATERIAL_LIBRARY } from './library.ts';
|
|
10
|
+
import { type MaterialDef, toStripRecipe } from './material-def.ts';
|
|
11
|
+
import { HOUSE_STRIPS } from './paint/default-sheet.ts';
|
|
12
|
+
import type { StripRecipe } from './paint/types.ts';
|
|
13
|
+
import { clearCustom, customIds, customIndex, pushCustom } from './slots.ts';
|
|
14
|
+
import { creditOf, type TextureSource } from './texture-source.ts';
|
|
15
|
+
import {
|
|
16
|
+
CLASSIC_STRIPS,
|
|
17
|
+
CUSTOM_STRIP_SLOTS,
|
|
18
|
+
type CustomStripId,
|
|
19
|
+
STRIPS,
|
|
20
|
+
type StripId,
|
|
21
|
+
stripRect,
|
|
22
|
+
} from './trim-sheet.ts';
|
|
23
|
+
|
|
24
|
+
const defined = new Map<string, MaterialDef>();
|
|
25
|
+
|
|
26
|
+
/** The same definition, field by field (painters by identity, colours and params by value). */
|
|
27
|
+
function same(a: MaterialDef, b: MaterialDef): boolean {
|
|
28
|
+
return (
|
|
29
|
+
JSON.stringify({ ...a, paint: 0 }) === JSON.stringify({ ...b, paint: 0 }) && a.paint === b.paint
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Register a project material and return its strip id for `strip:` options.
|
|
35
|
+
* @example
|
|
36
|
+
* const teak = defineMaterial({ id: 'teak_deck', kind: 'planks', roughness: 0.6,
|
|
37
|
+
* albedo: '#8a5a32', albedo2: '#5a3a1e', params: { width: 0.12, rings: 0.5 } });
|
|
38
|
+
* box({ size: [4, 2, 0.05], strip: teak, color: NATURAL });
|
|
39
|
+
*/
|
|
40
|
+
export function defineMaterial(def: MaterialDef): CustomStripId {
|
|
41
|
+
const recipe = toStripRecipe(def);
|
|
42
|
+
const id = def.id;
|
|
43
|
+
if ((STRIPS as readonly string[]).includes(id))
|
|
44
|
+
throw new Ts23dError('T3D_MATERIAL_CONFLICT', {
|
|
45
|
+
cause: `material '${id}' is a built-in strip`,
|
|
46
|
+
fix: `pick another id (e.g. '${id}_custom'), or restyle the built-in via paintTrimSheet({ ...DEFAULT_SHEET, strips: { ...DEFAULT_SHEET.strips, ${id}: … } })`,
|
|
47
|
+
});
|
|
48
|
+
const prior = defined.get(id);
|
|
49
|
+
if (prior) {
|
|
50
|
+
if (same(prior, def)) return id as CustomStripId;
|
|
51
|
+
throw new Ts23dError('T3D_MATERIAL_CONFLICT', {
|
|
52
|
+
cause: `material '${id}' is already defined differently`,
|
|
53
|
+
fix: 'define each material once in a shared module and import it, or give the variant its own id',
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
if (customIds().length >= CUSTOM_STRIP_SLOTS)
|
|
57
|
+
throw new Ts23dError('T3D_SHEET_FULL', {
|
|
58
|
+
cause: `the trim sheet has ${CUSTOM_STRIP_SLOTS} custom strip rows, all taken (${customIds().join(', ')})`,
|
|
59
|
+
fix: 'reuse a built-in strip with a vertex colour, or restyle an unused built-in strip in your SheetRecipe',
|
|
60
|
+
});
|
|
61
|
+
defined.set(id, def);
|
|
62
|
+
pushCustom(id);
|
|
63
|
+
(HOUSE_STRIPS as Record<string, StripRecipe>)[id] = recipe;
|
|
64
|
+
return id as CustomStripId;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Forget every defined material (tests; long-lived workers that reload recipes). Built-ins stay.
|
|
69
|
+
* Meshes compiled before the reset keep UVs into rows that may be reassigned.
|
|
70
|
+
*/
|
|
71
|
+
export function resetMaterials(): void {
|
|
72
|
+
for (const id of defined.keys()) delete (HOUSE_STRIPS as Record<string, StripRecipe>)[id];
|
|
73
|
+
defined.clear();
|
|
74
|
+
clearCustom();
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export interface MaterialInfo {
|
|
78
|
+
readonly id: StripId;
|
|
79
|
+
/** classic = greyscale strip tinted by vertex colour; library/defined carry their own colour. */
|
|
80
|
+
readonly origin: 'classic' | 'library' | 'defined';
|
|
81
|
+
readonly recipe: StripRecipe;
|
|
82
|
+
readonly description: string;
|
|
83
|
+
readonly sources: readonly string[];
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** What a strip is: its recipe, origin, description and sources. Unknown ids throw. */
|
|
87
|
+
export function materialOf(id: StripId): MaterialInfo {
|
|
88
|
+
const recipe = HOUSE_STRIPS[id];
|
|
89
|
+
const lib = (MATERIAL_LIBRARY as Readonly<Record<string, MaterialDef>>)[id];
|
|
90
|
+
const def = lib ?? defined.get(id);
|
|
91
|
+
if (!recipe || (customIndex(id) < 0 && !(STRIPS as readonly string[]).includes(id)))
|
|
92
|
+
throw new Ts23dError('T3D_MATERIAL_UNKNOWN', {
|
|
93
|
+
cause: `strip '${id}' is neither built in nor defined`,
|
|
94
|
+
fix: `use one of ${STRIPS.join(', ')}, or defineMaterial({ id: '${id}', ... }) first`,
|
|
95
|
+
});
|
|
96
|
+
const classic = (CLASSIC_STRIPS as readonly string[]).includes(id);
|
|
97
|
+
return {
|
|
98
|
+
id,
|
|
99
|
+
origin: classic ? 'classic' : lib ? 'library' : 'defined',
|
|
100
|
+
recipe,
|
|
101
|
+
description:
|
|
102
|
+
def?.description ?? `classic ${recipe.kind} strip (greyscale, vertex colour tints)`,
|
|
103
|
+
sources: def?.sources ?? [],
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** Every texture set a defined material references (what `bun run textures` fetches). */
|
|
108
|
+
export function textureSources(): readonly TextureSource[] {
|
|
109
|
+
return [...defined.values()].flatMap((d) => (d.texture ? [d.texture] : []));
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Credit lines for the image strips a mesh samples: any V coordinate inside such a strip's row
|
|
114
|
+
* counts. Empty when no image strip is used (the GLB stays byte-identical).
|
|
115
|
+
*/
|
|
116
|
+
export function stripCredits(vs: ArrayLike<number>): string[] {
|
|
117
|
+
const out: string[] = [];
|
|
118
|
+
for (const [id, d] of defined) {
|
|
119
|
+
if (!d.texture) continue;
|
|
120
|
+
const r = stripRect(id as StripId);
|
|
121
|
+
for (let i = 0; i < vs.length; i++) {
|
|
122
|
+
const v = vs[i] ?? -1;
|
|
123
|
+
if (v >= r.v0 && v <= r.v1) {
|
|
124
|
+
out.push(creditOf(d.texture));
|
|
125
|
+
break;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return out;
|
|
130
|
+
}
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
// Procedural material generator: paints the shared trim sheet (albedo, normal, ORM) from code —
|
|
2
|
-
// brick bonds, plank grain
|
|
3
|
-
//
|
|
2
|
+
// brick bonds, plank grain (oak, pine, weathered, painted), riveted plates, bare/cast/painted/
|
|
3
|
+
// galvanised metal, canvas, leather, slate, concrete, glass, decal cells and cutout foliage cards
|
|
4
|
+
// — at a fixed seed, so the texture is as deterministic as the meshes. Supplied PNGs (reference
|
|
4
5
|
// photos, kubeez.com generations) can stamp cells; they are inputs, never the only path.
|
|
5
6
|
//
|
|
6
7
|
// Conventions: V (image row) grows with world up, as box/planar map faces; strips tile seamlessly
|
|
7
|
-
// along U and span `stripRect().metres` there and `scaleMetres` along V;
|
|
8
|
-
// sRGB (vertex colour tints
|
|
8
|
+
// along U and span `stripRect().metres` there and `scaleMetres` along V; classic strips are
|
|
9
|
+
// near-greyscale sRGB (vertex colour tints them); library/defined material strips, foliage and
|
|
10
|
+
// stamped cells carry colour. Project strips (`defineMaterial`) paint after the built-ins.
|
|
9
11
|
|
|
10
12
|
import { createRng } from '../core/index.ts';
|
|
11
13
|
import { createCanvas } from './paint/canvas.ts';
|
|
@@ -15,12 +17,16 @@ import { DEFAULT_SHEET } from './paint/default-sheet.ts';
|
|
|
15
17
|
import { paintStrip } from './paint/strip.ts';
|
|
16
18
|
import type { PaintedSheet, SheetRecipe } from './paint/types.ts';
|
|
17
19
|
import { validateSheet, validateStrip } from './paint/validate.ts';
|
|
18
|
-
import {
|
|
20
|
+
import { customIds } from './slots.ts';
|
|
21
|
+
import { CELLS, cellRect, STRIPS, type StripId, stripRect } from './trim-sheet.ts';
|
|
19
22
|
|
|
20
23
|
export { DEFAULT_SHEET } from './paint/default-sheet.ts';
|
|
21
24
|
export type {
|
|
22
25
|
CellImage,
|
|
26
|
+
MaterialPainter,
|
|
27
|
+
MaterialSurface,
|
|
23
28
|
PaintedSheet,
|
|
29
|
+
PaintKit,
|
|
24
30
|
SheetRecipe,
|
|
25
31
|
StripKind,
|
|
26
32
|
StripRecipe,
|
|
@@ -30,18 +36,19 @@ const FALLBACK = { kind: 'noise', scaleMetres: 1, roughness: 0.6 } as const;
|
|
|
30
36
|
|
|
31
37
|
export function paintTrimSheet(recipe: SheetRecipe = DEFAULT_SHEET): PaintedSheet {
|
|
32
38
|
validateSheet(recipe);
|
|
33
|
-
const size = recipe.size ??
|
|
39
|
+
const size = recipe.size ?? 2048;
|
|
34
40
|
const sheet = blankSheet(size);
|
|
35
41
|
const rng = createRng(recipe.seed);
|
|
36
42
|
const seedOf = (name: string): number => rng.fork(name).int(0, 0x7fffffff);
|
|
37
43
|
const pad = Math.max(2, size / 256);
|
|
38
|
-
|
|
44
|
+
const all = [...STRIPS, ...(customIds() as readonly StripId[])];
|
|
45
|
+
for (const id of all) {
|
|
39
46
|
const r = recipe.strips[id] ?? DEFAULT_SHEET.strips[id] ?? FALLBACK;
|
|
40
47
|
validateStrip(`strips.${id}`, r);
|
|
41
48
|
const rect = stripRect(id);
|
|
42
49
|
const h = Math.round((rect.v1 - rect.v0) * size);
|
|
43
50
|
const c = createCanvas(size, h, rect.metres, r.scaleMetres, true);
|
|
44
|
-
paintStrip(c, r, seedOf(id));
|
|
51
|
+
paintStrip(c, r, seedOf(id), `strips.${id}`);
|
|
45
52
|
composeRegion(sheet, c, 0, Math.round(rect.v0 * size), pad);
|
|
46
53
|
}
|
|
47
54
|
for (const id of CELLS) {
|
package/src/material/index.ts
CHANGED
|
@@ -1,29 +1,60 @@
|
|
|
1
|
-
// Public API of @ts23d/material: the shared trim-sheet layout, the procedural sheet
|
|
2
|
-
// palettes and engine
|
|
1
|
+
// Public API of @developerz.ai/ts23d/material: the shared trim-sheet layout, the procedural sheet
|
|
2
|
+
// painter, the material library + `defineMaterial` for project materials, palettes and engine
|
|
3
|
+
// shader sources.
|
|
3
4
|
|
|
5
|
+
export { CC0_MATERIALS, CC0_WOOD_PLANKS_GREY } from './cc0.ts';
|
|
6
|
+
export {
|
|
7
|
+
defineMaterial,
|
|
8
|
+
type MaterialInfo,
|
|
9
|
+
materialOf,
|
|
10
|
+
resetMaterials,
|
|
11
|
+
stripCredits,
|
|
12
|
+
textureSources,
|
|
13
|
+
} from './define.ts';
|
|
4
14
|
export {
|
|
5
15
|
type CellImage,
|
|
6
16
|
DEFAULT_SHEET,
|
|
17
|
+
type MaterialPainter,
|
|
18
|
+
type MaterialSurface,
|
|
7
19
|
type PaintedSheet,
|
|
20
|
+
type PaintKit,
|
|
8
21
|
paintTrimSheet,
|
|
9
22
|
type SheetRecipe,
|
|
10
23
|
type StripKind,
|
|
11
24
|
type StripRecipe,
|
|
12
25
|
} from './generator.ts';
|
|
13
|
-
export {
|
|
26
|
+
export { MATERIAL_LIBRARY } from './library.ts';
|
|
27
|
+
export type { MaterialDef } from './material-def.ts';
|
|
28
|
+
export { definePalette, hex, NATURAL, type Palette, palettes } from './palette.ts';
|
|
14
29
|
export { godotShaderSource, type ShaderHints, threeMaterialSource } from './shader.ts';
|
|
15
30
|
export {
|
|
31
|
+
creditOf,
|
|
32
|
+
provideTextures,
|
|
33
|
+
type TextureCache,
|
|
34
|
+
type TextureFile,
|
|
35
|
+
type TextureSource,
|
|
36
|
+
} from './texture-source.ts';
|
|
37
|
+
export {
|
|
38
|
+
type BuiltinStripId,
|
|
16
39
|
CELLS,
|
|
17
40
|
type CellId,
|
|
41
|
+
CLASSIC_STRIPS,
|
|
42
|
+
type ClassicStripId,
|
|
43
|
+
CUSTOM_STRIP_SLOTS,
|
|
44
|
+
type CustomStripId,
|
|
18
45
|
cellRect,
|
|
19
46
|
DECAL_CELLS,
|
|
20
47
|
type DecalCellId,
|
|
21
48
|
FOLIAGE_CELLS,
|
|
22
49
|
type FoliageCellId,
|
|
23
50
|
isFoliage,
|
|
51
|
+
MATERIAL_STRIPS,
|
|
52
|
+
type MaterialStripId,
|
|
53
|
+
STRIP_ROWS,
|
|
24
54
|
STRIPS,
|
|
25
55
|
type StripId,
|
|
26
56
|
stripRect,
|
|
57
|
+
stripRow,
|
|
27
58
|
TRIM_SHEET_SIZE,
|
|
28
59
|
type UvRect,
|
|
29
60
|
} from './trim-sheet.ts';
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
// The house material library: sixteen physically distinct strips with measured or sourced PBR
|
|
2
|
+
// numbers (docs/architecture/materials.md has the table and the reasoning). Metals use the
|
|
3
|
+
// physicallybased.info reflectance (sRGB here), dielectrics stay inside the 30–240 sRGB albedo
|
|
4
|
+
// range, roughness follows engine/asset references. Colour lives in the texture, so tint these
|
|
5
|
+
// with a near-white vertex colour (`NATURAL`) — except where noted (paint films take the hue).
|
|
6
|
+
|
|
7
|
+
import type { MaterialDef } from './material-def.ts';
|
|
8
|
+
import type { MaterialStripId } from './trim-sheet.ts';
|
|
9
|
+
|
|
10
|
+
const PB = 'https://physicallybased.info';
|
|
11
|
+
const ROBLOX = 'https://create.roblox.com/docs/art/modeling/material-reference';
|
|
12
|
+
const PBR_GUIDE = 'https://www.adobe.com/learn/substance-3d-designer/web/the-pbr-guide-part-2';
|
|
13
|
+
|
|
14
|
+
type Library = Readonly<Record<MaterialStripId, MaterialDef & { readonly id: MaterialStripId }>>;
|
|
15
|
+
|
|
16
|
+
export const MATERIAL_LIBRARY: Library = {
|
|
17
|
+
oak: {
|
|
18
|
+
id: 'oak',
|
|
19
|
+
kind: 'planks',
|
|
20
|
+
scaleMetres: 0.6,
|
|
21
|
+
roughness: 0.68,
|
|
22
|
+
albedo: '#a27b52',
|
|
23
|
+
albedo2: '#6a482b',
|
|
24
|
+
params: { width: 0.14, rings: 0.55, rays: 0.6, knots: 0.6 },
|
|
25
|
+
description: 'Oak boards: warm brown, dark latewood rings, pale ray flecks.',
|
|
26
|
+
sources: [PBR_GUIDE, ROBLOX],
|
|
27
|
+
},
|
|
28
|
+
pine: {
|
|
29
|
+
id: 'pine',
|
|
30
|
+
kind: 'planks',
|
|
31
|
+
scaleMetres: 0.6,
|
|
32
|
+
roughness: 0.72,
|
|
33
|
+
albedo: '#d2b183',
|
|
34
|
+
albedo2: '#9b6a3c',
|
|
35
|
+
params: { width: 0.16, rings: 0.75, knots: 2.5 },
|
|
36
|
+
description: 'Pine boards: pale yellow, strong orange latewood, frequent knots.',
|
|
37
|
+
sources: [PBR_GUIDE, ROBLOX],
|
|
38
|
+
},
|
|
39
|
+
wood_weathered: {
|
|
40
|
+
id: 'wood_weathered',
|
|
41
|
+
kind: 'planks',
|
|
42
|
+
scaleMetres: 0.6,
|
|
43
|
+
roughness: 0.86,
|
|
44
|
+
albedo: '#9b968c',
|
|
45
|
+
albedo2: '#4f463c',
|
|
46
|
+
params: { width: 0.2, rings: 0.35, checks: 1, knots: 1.5, grime: 0.15 },
|
|
47
|
+
description: 'Barn-grey weathered boards: silvered, eroded grain, open checks.',
|
|
48
|
+
sources: [ROBLOX],
|
|
49
|
+
},
|
|
50
|
+
wood_painted: {
|
|
51
|
+
id: 'wood_painted',
|
|
52
|
+
kind: 'planks',
|
|
53
|
+
scaleMetres: 0.6,
|
|
54
|
+
roughness: 0.84,
|
|
55
|
+
albedo: '#ededed',
|
|
56
|
+
albedo2: '#a08c74',
|
|
57
|
+
params: { width: 0.14, paint: 1, paintRough: 0.62, chips: 0.38, knots: 0.8, grime: 0.3 },
|
|
58
|
+
description: 'Painted boards (vertex colour = paint) worn through to grey wood at the edges.',
|
|
59
|
+
sources: [ROBLOX],
|
|
60
|
+
},
|
|
61
|
+
iron_rust: {
|
|
62
|
+
id: 'iron_rust',
|
|
63
|
+
kind: 'metal',
|
|
64
|
+
scaleMetres: 0.5,
|
|
65
|
+
roughness: 0.55,
|
|
66
|
+
metalness: 0.9,
|
|
67
|
+
albedo: '#8e8a85',
|
|
68
|
+
albedo2: '#764323',
|
|
69
|
+
params: { oxide: 0.62, cast: 0.25, streaks: 0.5 },
|
|
70
|
+
description: 'Weathered wrought/rolled iron, rust blooming in patches and runs.',
|
|
71
|
+
sources: [PB, ROBLOX],
|
|
72
|
+
},
|
|
73
|
+
iron_cast: {
|
|
74
|
+
id: 'iron_cast',
|
|
75
|
+
kind: 'metal',
|
|
76
|
+
scaleMetres: 0.5,
|
|
77
|
+
roughness: 0.68,
|
|
78
|
+
metalness: 0.75,
|
|
79
|
+
albedo: '#6c6966',
|
|
80
|
+
albedo2: '#55372a',
|
|
81
|
+
params: { cast: 1, oxide: 0.35 },
|
|
82
|
+
description: 'Sand-cast iron (wheels, frames): dark pitted skin, dull oxide.',
|
|
83
|
+
sources: [PB],
|
|
84
|
+
},
|
|
85
|
+
steel_painted: {
|
|
86
|
+
id: 'steel_painted',
|
|
87
|
+
kind: 'metal',
|
|
88
|
+
scaleMetres: 0.5,
|
|
89
|
+
roughness: 0.4,
|
|
90
|
+
metalness: 1,
|
|
91
|
+
albedo: '#eeeeee',
|
|
92
|
+
albedo2: '#5a3b2a',
|
|
93
|
+
params: { paint: 1, paintRough: 0.42, chips: 0.2, streaks: 0.2, grime: 0.2 },
|
|
94
|
+
description: 'Painted steel (vertex colour = paint): chips show rusty steel beneath.',
|
|
95
|
+
sources: [ROBLOX],
|
|
96
|
+
},
|
|
97
|
+
steel_galvanised: {
|
|
98
|
+
id: 'steel_galvanised',
|
|
99
|
+
kind: 'metal',
|
|
100
|
+
scaleMetres: 0.5,
|
|
101
|
+
roughness: 0.42,
|
|
102
|
+
metalness: 1,
|
|
103
|
+
albedo: '#d5dadc',
|
|
104
|
+
albedo2: '#9a9c98',
|
|
105
|
+
params: { spangle: 1, oxide: 0.12 },
|
|
106
|
+
description: 'Hot-dip galvanised steel: zinc spangle, white-rust bloom.',
|
|
107
|
+
sources: [PB],
|
|
108
|
+
},
|
|
109
|
+
brass: {
|
|
110
|
+
id: 'brass',
|
|
111
|
+
kind: 'metal',
|
|
112
|
+
scaleMetres: 0.5,
|
|
113
|
+
roughness: 0.28,
|
|
114
|
+
metalness: 1,
|
|
115
|
+
albedo: '#f5e5ae',
|
|
116
|
+
albedo2: '#6b5a3a',
|
|
117
|
+
params: { brush: 0.3, oxide: 0.1 },
|
|
118
|
+
description: 'Brass fittings: warm gold reflectance, dark tarnish in patches.',
|
|
119
|
+
sources: [PB, ROBLOX],
|
|
120
|
+
},
|
|
121
|
+
copper: {
|
|
122
|
+
id: 'copper',
|
|
123
|
+
kind: 'metal',
|
|
124
|
+
scaleMetres: 0.5,
|
|
125
|
+
roughness: 0.32,
|
|
126
|
+
metalness: 1,
|
|
127
|
+
albedo: '#f2b89c',
|
|
128
|
+
albedo2: '#6fa58f',
|
|
129
|
+
params: { oxide: 0.15 },
|
|
130
|
+
description: 'Copper: salmon reflectance with verdigris patches.',
|
|
131
|
+
sources: [PB],
|
|
132
|
+
},
|
|
133
|
+
aluminium: {
|
|
134
|
+
id: 'aluminium',
|
|
135
|
+
kind: 'metal',
|
|
136
|
+
scaleMetres: 0.5,
|
|
137
|
+
roughness: 0.34,
|
|
138
|
+
metalness: 1,
|
|
139
|
+
albedo: '#e6e8e9',
|
|
140
|
+
params: { brush: 1 },
|
|
141
|
+
description: 'Brushed aluminium: bright, cool, streaked along U.',
|
|
142
|
+
sources: [PB],
|
|
143
|
+
},
|
|
144
|
+
chrome: {
|
|
145
|
+
id: 'chrome',
|
|
146
|
+
kind: 'metal',
|
|
147
|
+
scaleMetres: 0.5,
|
|
148
|
+
roughness: 0.07,
|
|
149
|
+
metalness: 1,
|
|
150
|
+
albedo: '#d3d9dc',
|
|
151
|
+
description: 'Chrome plate: near-mirror.',
|
|
152
|
+
sources: [PB],
|
|
153
|
+
},
|
|
154
|
+
rubber: {
|
|
155
|
+
id: 'rubber',
|
|
156
|
+
kind: 'noise',
|
|
157
|
+
scaleMetres: 0.5,
|
|
158
|
+
roughness: 0.78,
|
|
159
|
+
albedo: '#2c2c2c',
|
|
160
|
+
params: { tone: 0.95, mottle: 0.06, relief: 0.0002, size: 0.05, grime: 0.2 },
|
|
161
|
+
description: 'Vulcanised rubber (tyres, hoses, seals).',
|
|
162
|
+
sources: [PB, ROBLOX],
|
|
163
|
+
},
|
|
164
|
+
canvas: {
|
|
165
|
+
id: 'canvas',
|
|
166
|
+
kind: 'weave',
|
|
167
|
+
scaleMetres: 1,
|
|
168
|
+
roughness: 0.88,
|
|
169
|
+
albedo: '#cbbea2',
|
|
170
|
+
albedo2: '#8a7c62',
|
|
171
|
+
params: { pitch: 0.004, folds: 0.6, stains: 0.35 },
|
|
172
|
+
description: 'Cotton duck canvas: natural, weave relief, folds and water stains.',
|
|
173
|
+
sources: [ROBLOX],
|
|
174
|
+
},
|
|
175
|
+
leather: {
|
|
176
|
+
id: 'leather',
|
|
177
|
+
kind: 'leather',
|
|
178
|
+
scaleMetres: 0.5,
|
|
179
|
+
roughness: 0.58,
|
|
180
|
+
albedo: '#7c5438',
|
|
181
|
+
albedo2: '#3e281a',
|
|
182
|
+
params: { creases: 0.5, wear: 0.4 },
|
|
183
|
+
description: 'Worn brown leather: pebble grain, dark creases, burnished wear.',
|
|
184
|
+
sources: [ROBLOX],
|
|
185
|
+
},
|
|
186
|
+
plastic: {
|
|
187
|
+
id: 'plastic',
|
|
188
|
+
kind: 'noise',
|
|
189
|
+
scaleMetres: 0.5,
|
|
190
|
+
roughness: 0.35,
|
|
191
|
+
albedo: '#ececec',
|
|
192
|
+
params: { tone: 0.97, mottle: 0.01, relief: 0.00004, size: 0.2 },
|
|
193
|
+
description: 'Moulded plastic (vertex colour = the plastic colour): satin, even.',
|
|
194
|
+
sources: [ROBLOX],
|
|
195
|
+
},
|
|
196
|
+
};
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
// The material definition contract: what a project (or the house library) says about a strip —
|
|
2
|
+
// PBR numbers, intrinsic colour, a base painter kind and an optional painter layer — and its
|
|
3
|
+
// translation to the sheet's StripRecipe. Validation throws T3D_MATERIAL_INVALID with the fix.
|
|
4
|
+
|
|
5
|
+
import { Ts23dError, type Vec3 } from '../core/index.ts';
|
|
6
|
+
import type { MaterialPainter, StripKind, StripRecipe } from './paint/types.ts';
|
|
7
|
+
import { hex } from './palette.ts';
|
|
8
|
+
import { type TextureSource, validateTexture } from './texture-source.ts';
|
|
9
|
+
|
|
10
|
+
export interface MaterialDef {
|
|
11
|
+
/** snake_case, unique; becomes the strip id recipes pass as `strip`. */
|
|
12
|
+
readonly id: string;
|
|
13
|
+
/** Base painter the strip starts from. Default 'noise' (a quiet surface). */
|
|
14
|
+
readonly kind?: StripKind;
|
|
15
|
+
/** World metres the strip's V span represents (typical face height). Default 1. */
|
|
16
|
+
readonly scaleMetres?: number;
|
|
17
|
+
/** Perceptual roughness 0..1 (glTF): 0 mirror, 1 chalk. */
|
|
18
|
+
readonly roughness: number;
|
|
19
|
+
/** 0 dielectric, 1 metal. Default 0. Avoid in-between values except for dirt/oxide blends. */
|
|
20
|
+
readonly metalness?: number;
|
|
21
|
+
/** Intrinsic base colour: sRGB '#rrggbb' or linear RGB. Default white (vertex colour = hue). */
|
|
22
|
+
readonly albedo?: string | Vec3;
|
|
23
|
+
/** Second colour the painter blends to (latewood, rust, verdigris, bare wood under paint). */
|
|
24
|
+
readonly albedo2?: string | Vec3;
|
|
25
|
+
/** Painter knobs for `kind` (see each painter's header in src/material/paint/). */
|
|
26
|
+
readonly params?: Readonly<Record<string, number>>;
|
|
27
|
+
/** Your own layer, painted after `kind`: write height/tone/rough/metal/mix per texel. */
|
|
28
|
+
readonly paint?: MaterialPainter;
|
|
29
|
+
/** Paint from a CC0 texture set (opt-in, needs `bun run textures`) instead of `kind`. */
|
|
30
|
+
readonly texture?: TextureSource;
|
|
31
|
+
readonly description?: string;
|
|
32
|
+
/** Where the numbers come from (URLs). */
|
|
33
|
+
readonly sources?: readonly string[];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const ID = /^[a-z][a-z0-9_]{1,31}$/;
|
|
37
|
+
const unit = (v: number): boolean => Number.isFinite(v) && v >= 0 && v <= 1;
|
|
38
|
+
|
|
39
|
+
function fail(id: string, cause: string, fix: string): never {
|
|
40
|
+
throw new Ts23dError('T3D_MATERIAL_INVALID', { cause: `material '${id}': ${cause}`, fix });
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function colour(id: string, key: string, v: string | Vec3 | undefined): Vec3 | undefined {
|
|
44
|
+
if (v === undefined) return undefined;
|
|
45
|
+
if (typeof v === 'string') {
|
|
46
|
+
if (!/^#?[0-9a-fA-F]{6}$/.test(v))
|
|
47
|
+
fail(id, `${key} '${v}' is not a hex colour`, "use '#rrggbb'");
|
|
48
|
+
return hex(v);
|
|
49
|
+
}
|
|
50
|
+
if (!(v.length === 3 && v.every(unit)))
|
|
51
|
+
fail(id, `${key} [${v}] is not linear RGB 0..1`, "use '#rrggbb' or three numbers in 0..1");
|
|
52
|
+
return v;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Validate a definition and turn it into the strip recipe the sheet painter runs. */
|
|
56
|
+
export function toStripRecipe(d: MaterialDef): StripRecipe {
|
|
57
|
+
const id = String(d.id);
|
|
58
|
+
if (!ID.test(id))
|
|
59
|
+
fail(id, 'id is not snake_case (2–32 chars, starts with a letter)', "e.g. 'teak_deck'");
|
|
60
|
+
if (!unit(d.roughness)) fail(id, `roughness ${d.roughness}`, 'roughness is 0..1');
|
|
61
|
+
if (d.metalness !== undefined && !unit(d.metalness))
|
|
62
|
+
fail(id, `metalness ${d.metalness}`, 'metalness is 0..1 (0 dielectric, 1 metal)');
|
|
63
|
+
const scale = d.scaleMetres ?? 1;
|
|
64
|
+
if (!(Number.isFinite(scale) && scale > 0))
|
|
65
|
+
fail(id, `scaleMetres ${scale}`, 'scaleMetres is the face height in metres, > 0');
|
|
66
|
+
if (d.paint !== undefined && typeof d.paint !== 'function')
|
|
67
|
+
fail(id, 'paint is not a function', 'paint is (surface, kit) => void');
|
|
68
|
+
for (const [k, v] of Object.entries(d.params ?? {}))
|
|
69
|
+
if (!Number.isFinite(v)) fail(id, `params.${k} is ${v}`, 'params are finite numbers');
|
|
70
|
+
if (d.texture) validateTexture(id, d.texture);
|
|
71
|
+
const albedo = colour(id, 'albedo', d.albedo);
|
|
72
|
+
const albedo2 = colour(id, 'albedo2', d.albedo2);
|
|
73
|
+
return {
|
|
74
|
+
kind: d.kind ?? 'noise',
|
|
75
|
+
scaleMetres: scale,
|
|
76
|
+
roughness: d.roughness,
|
|
77
|
+
...(d.metalness !== undefined ? { metalness: d.metalness } : {}),
|
|
78
|
+
...(d.params ? { params: d.params } : {}),
|
|
79
|
+
...(albedo ? { albedo } : {}),
|
|
80
|
+
...(albedo2 ? { albedo2 } : {}),
|
|
81
|
+
...(d.paint ? { paint: d.paint } : {}),
|
|
82
|
+
...(d.texture ? { texture: d.texture } : {}),
|
|
83
|
+
};
|
|
84
|
+
}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
// The painting surface for one sheet region (a strip or a cell): float layers in real metres.
|
|
2
2
|
// Painters write height (metres of relief, + is out of the surface), linear grey tone,
|
|
3
|
-
// roughness and
|
|
3
|
+
// roughness, metalness and a two-colour `mix`; `compose` turns them into albedo + normal + ORM.
|
|
4
4
|
|
|
5
|
+
import type { Vec3 } from '../../core/index.ts';
|
|
5
6
|
import { fbm } from './noise.ts';
|
|
6
7
|
|
|
7
8
|
export interface Canvas {
|
|
@@ -18,8 +19,14 @@ export interface Canvas {
|
|
|
18
19
|
readonly metal: Float32Array;
|
|
19
20
|
/** Extra occlusion multiplied into the cavity AO (foliage darkens toward the twig). */
|
|
20
21
|
readonly occl: Float32Array;
|
|
22
|
+
/** 0 = `colours[0]`, 1 = `colours[1]` (latewood, rust, bare wood under chipped paint…). */
|
|
23
|
+
readonly mix: Float32Array;
|
|
24
|
+
/** Linear albedo pair for material strips; null = greyscale (vertex colour carries hue). */
|
|
25
|
+
colours: readonly [Vec3, Vec3] | null;
|
|
21
26
|
/** sRGB RGBA override (stamped images, coloured foliage); null when painted greyscale. */
|
|
22
27
|
rgba: Uint8Array | null;
|
|
28
|
+
/** Tangent-space normal override (image strips); null = derived from `height`. */
|
|
29
|
+
normalRgba: Uint8Array | null;
|
|
23
30
|
}
|
|
24
31
|
|
|
25
32
|
export function createCanvas(
|
|
@@ -41,7 +48,10 @@ export function createCanvas(
|
|
|
41
48
|
rough: new Float32Array(n).fill(0.8),
|
|
42
49
|
metal: new Float32Array(n),
|
|
43
50
|
occl: new Float32Array(n).fill(1),
|
|
51
|
+
mix: new Float32Array(n),
|
|
52
|
+
colours: null,
|
|
44
53
|
rgba: null,
|
|
54
|
+
normalRgba: null,
|
|
45
55
|
};
|
|
46
56
|
}
|
|
47
57
|
|
|
@@ -32,7 +32,7 @@ export function composeRegion(
|
|
|
32
32
|
const ao = cavityOcclusion(c);
|
|
33
33
|
const maps = [
|
|
34
34
|
[sheet.albedo, packAlbedo(c, ao)],
|
|
35
|
-
[sheet.normal, sobelNormals(c)],
|
|
35
|
+
[sheet.normal, c.normalRgba ?? sobelNormals(c)],
|
|
36
36
|
[sheet.orm, packOrm(c, ao)],
|
|
37
37
|
] as const;
|
|
38
38
|
const padX = c.wrapU ? 0 : pad;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
// Minimal PNG decoder for stamping supplied images into cells: 8-bit grey,
|
|
2
|
-
// and palette (with tRNS), non-interlaced. Zero dependencies beyond node:zlib. Anything else
|
|
1
|
+
// Minimal PNG decoder for stamping supplied images into cells and image strips: 8- or 16-bit grey,
|
|
2
|
+
// grey+alpha, RGB, RGBA (16-bit keeps the high byte) and 8-bit palette (with tRNS), non-interlaced. Zero dependencies beyond node:zlib. Anything else
|
|
3
3
|
// throws T3D_RECIPE_INVALID with the conversion to make.
|
|
4
4
|
|
|
5
5
|
import { inflateSync } from 'node:zlib';
|
|
@@ -17,7 +17,7 @@ const CHANNELS: Readonly<Record<number, number>> = { 0: 1, 2: 3, 3: 1, 4: 2, 6:
|
|
|
17
17
|
function bad(cause: string): never {
|
|
18
18
|
throw new Ts23dError('T3D_RECIPE_INVALID', {
|
|
19
19
|
cause: `cell image: ${cause}`,
|
|
20
|
-
fix: 'pass PNG bytes, 8 bits per channel, non-interlaced (e.g. re-save as RGBA8 PNG)',
|
|
20
|
+
fix: 'pass PNG bytes, 8 or 16 bits per channel, non-interlaced (e.g. re-save as RGBA8 PNG)',
|
|
21
21
|
});
|
|
22
22
|
}
|
|
23
23
|
|
|
@@ -35,6 +35,7 @@ export function decodePng(bytes: Uint8Array): Rgba {
|
|
|
35
35
|
let width = 0;
|
|
36
36
|
let height = 0;
|
|
37
37
|
let type = -1;
|
|
38
|
+
let depth = 8;
|
|
38
39
|
let palette: Uint8Array = new Uint8Array();
|
|
39
40
|
let trns: Uint8Array = new Uint8Array();
|
|
40
41
|
const idat: Uint8Array[] = [];
|
|
@@ -46,7 +47,9 @@ export function decodePng(bytes: Uint8Array): Rgba {
|
|
|
46
47
|
width = dv.getUint32(o + 8);
|
|
47
48
|
height = dv.getUint32(o + 12);
|
|
48
49
|
type = body[9] ?? -1;
|
|
49
|
-
|
|
50
|
+
depth = body[8] ?? 0;
|
|
51
|
+
if (depth !== 8 && !(depth === 16 && type !== 3))
|
|
52
|
+
bad(`bit depth ${depth} (8, or 16 for non-palette images, is supported)`);
|
|
50
53
|
if (body[12] !== 0) bad('interlaced');
|
|
51
54
|
} else if (name === 'PLTE') palette = body;
|
|
52
55
|
else if (name === 'tRNS') trns = body;
|
|
@@ -57,7 +60,9 @@ export function decodePng(bytes: Uint8Array): Rgba {
|
|
|
57
60
|
const ch = CHANNELS[type];
|
|
58
61
|
if (!ch || width === 0 || height === 0) bad(`colour type ${type} or empty image`);
|
|
59
62
|
const raw = new Uint8Array(inflateSync(Buffer.concat(idat)));
|
|
60
|
-
const
|
|
63
|
+
const B = depth / 8;
|
|
64
|
+
const bpp = ch * B;
|
|
65
|
+
const stride = width * bpp;
|
|
61
66
|
if (raw.length < height * (stride + 1)) bad('truncated image data');
|
|
62
67
|
const px = new Uint8Array(height * stride);
|
|
63
68
|
for (let y = 0; y < height; y++) {
|
|
@@ -65,9 +70,9 @@ export function decodePng(bytes: Uint8Array): Rgba {
|
|
|
65
70
|
const src = y * (stride + 1) + 1;
|
|
66
71
|
const row = y * stride;
|
|
67
72
|
for (let x = 0; x < stride; x++) {
|
|
68
|
-
const a = x >=
|
|
73
|
+
const a = x >= bpp ? (px[row + x - bpp] ?? 0) : 0;
|
|
69
74
|
const b = y > 0 ? (px[row - stride + x] ?? 0) : 0;
|
|
70
|
-
const c = x >=
|
|
75
|
+
const c = x >= bpp && y > 0 ? (px[row - stride + x - bpp] ?? 0) : 0;
|
|
71
76
|
const v = raw[src + x] ?? 0;
|
|
72
77
|
const pred =
|
|
73
78
|
f === 1 ? a : f === 2 ? b : f === 3 ? (a + b) >> 1 : f === 4 ? paeth(a, b, c) : 0;
|
|
@@ -76,8 +81,9 @@ export function decodePng(bytes: Uint8Array): Rgba {
|
|
|
76
81
|
}
|
|
77
82
|
const data = new Uint8Array(width * height * 4);
|
|
78
83
|
for (let i = 0; i < width * height; i++) {
|
|
79
|
-
const s = i *
|
|
80
|
-
const
|
|
84
|
+
const s = i * bpp;
|
|
85
|
+
const at = (k: number): number => px[s + k * B] ?? 0;
|
|
86
|
+
const g = at(0);
|
|
81
87
|
const o = i * 4;
|
|
82
88
|
if (type === 3) {
|
|
83
89
|
data[o] = palette[g * 3] ?? 0;
|
|
@@ -86,14 +92,14 @@ export function decodePng(bytes: Uint8Array): Rgba {
|
|
|
86
92
|
data[o + 3] = trns[g] ?? 255;
|
|
87
93
|
} else if (ch >= 3) {
|
|
88
94
|
data[o] = g;
|
|
89
|
-
data[o + 1] =
|
|
90
|
-
data[o + 2] =
|
|
91
|
-
data[o + 3] = ch === 4 ? (
|
|
95
|
+
data[o + 1] = at(1);
|
|
96
|
+
data[o + 2] = at(2);
|
|
97
|
+
data[o + 3] = ch === 4 ? at(3) : 255;
|
|
92
98
|
} else {
|
|
93
99
|
data[o] = g;
|
|
94
100
|
data[o + 1] = g;
|
|
95
101
|
data[o + 2] = g;
|
|
96
|
-
data[o + 3] = ch === 2 ? (
|
|
102
|
+
data[o + 3] = ch === 2 ? at(1) : 255;
|
|
97
103
|
}
|
|
98
104
|
}
|
|
99
105
|
return { width, height, data };
|