@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
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Recipe validation: every bad input throws T3D_RECIPE_INVALID with the fix, before any painting.
|
|
2
2
|
|
|
3
3
|
import { Ts23dError } from '../../core/index.ts';
|
|
4
|
-
import { CELLS, STRIPS } from '../trim-sheet.ts';
|
|
4
|
+
import { CELLS, STRIPS, stripRow } from '../trim-sheet.ts';
|
|
5
5
|
import type { CellImage, SheetRecipe, StripKind, StripRecipe } from './types.ts';
|
|
6
6
|
|
|
7
7
|
const KINDS: readonly StripKind[] = [
|
|
@@ -13,6 +13,9 @@ const KINDS: readonly StripKind[] = [
|
|
|
13
13
|
'slate',
|
|
14
14
|
'concrete',
|
|
15
15
|
'glass',
|
|
16
|
+
'metal',
|
|
17
|
+
'weave',
|
|
18
|
+
'leather',
|
|
16
19
|
];
|
|
17
20
|
const SIZES = [512, 1024, 2048];
|
|
18
21
|
|
|
@@ -33,6 +36,16 @@ export function validateStrip(where: string, r: StripRecipe): void {
|
|
|
33
36
|
if (!unit(r.roughness)) fail(`${where}.roughness is ${r.roughness}`, 'roughness is 0..1');
|
|
34
37
|
if (r.metalness !== undefined && !unit(r.metalness))
|
|
35
38
|
fail(`${where}.metalness is ${r.metalness}`, 'metalness is 0..1');
|
|
39
|
+
for (const key of ['albedo', 'albedo2'] as const) {
|
|
40
|
+
const a = r[key];
|
|
41
|
+
if (a !== undefined && !(a.length === 3 && a.every(unit)))
|
|
42
|
+
fail(
|
|
43
|
+
`${where}.${key} is [${a}]`,
|
|
44
|
+
`${key} is linear RGB, three numbers 0..1 (use hex('#rrggbb'))`,
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
if (r.paint !== undefined && typeof r.paint !== 'function')
|
|
48
|
+
fail(`${where}.paint is not a function`, 'paint is (surface, kit) => void');
|
|
36
49
|
for (const [k, v] of Object.entries(r.params ?? {}))
|
|
37
50
|
if (!Number.isFinite(v)) fail(`${where}.params.${k} is ${v}`, 'params are finite numbers');
|
|
38
51
|
}
|
|
@@ -42,8 +55,11 @@ export function validateSheet(recipe: SheetRecipe): void {
|
|
|
42
55
|
if (recipe.size !== undefined && !SIZES.includes(recipe.size))
|
|
43
56
|
fail(`size ${recipe.size}`, 'size is 512, 1024 or 2048');
|
|
44
57
|
for (const [id, r] of Object.entries(recipe.strips)) {
|
|
45
|
-
if (
|
|
46
|
-
fail(
|
|
58
|
+
if (stripRow(id) < 0)
|
|
59
|
+
fail(
|
|
60
|
+
`strip '${id}' is not in the layout`,
|
|
61
|
+
`strips are ${STRIPS.join(', ')} or a defineMaterial() id`,
|
|
62
|
+
);
|
|
47
63
|
if (r) validateStrip(`strips.${id}`, r);
|
|
48
64
|
}
|
|
49
65
|
for (const [id, e] of Object.entries(recipe.cells ?? {})) {
|
package/src/material/palette.ts
CHANGED
|
@@ -15,6 +15,9 @@ export function hex(value: string): Vec3 {
|
|
|
15
15
|
return [lin((n >> 16) & 255), lin((n >> 8) & 255), lin(n & 255)];
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
+
/** Vertex colour for material strips that carry their own colour (oak, brass, rubber…). */
|
|
19
|
+
export const NATURAL: Vec3 = [1, 1, 1];
|
|
20
|
+
|
|
18
21
|
export function definePalette(entries: Readonly<Record<string, string>>): Palette {
|
|
19
22
|
return Object.fromEntries(Object.entries(entries).map(([k, v]) => [k, hex(v)]));
|
|
20
23
|
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// The custom-strip slot table: ids registered by `defineMaterial`, in registration order. A leaf
|
|
2
|
+
// module (no imports) so the layout (trim-sheet.ts) and the registry (registry.ts) can both read it.
|
|
3
|
+
|
|
4
|
+
const custom: string[] = [];
|
|
5
|
+
|
|
6
|
+
/** Slot index among the custom strips, or −1. */
|
|
7
|
+
export const customIndex = (id: string): number => custom.indexOf(id);
|
|
8
|
+
|
|
9
|
+
export const customIds = (): readonly string[] => custom;
|
|
10
|
+
|
|
11
|
+
export function pushCustom(id: string): number {
|
|
12
|
+
custom.push(id);
|
|
13
|
+
return custom.length - 1;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function clearCustom(): void {
|
|
17
|
+
custom.length = 0;
|
|
18
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
// Image-backed strips: a CC0 PBR texture set (Poly Haven, ambientCG or a local file) declared as
|
|
2
|
+
// data — URL + sha256 per map, licence, author page, real-world size. Compile and paint stay
|
|
3
|
+
// offline: bytes come only from a content-addressed cache the caller provides (`provideTextures`,
|
|
4
|
+
// filled by `bun run textures`); a missing map throws T3D_TEXTURE_MISSING with that command.
|
|
5
|
+
|
|
6
|
+
import { Ts23dError } from '../core/index.ts';
|
|
7
|
+
|
|
8
|
+
export interface TextureFile {
|
|
9
|
+
/** Download URL (https) or, for `source: 'file'`, a path relative to the working directory. */
|
|
10
|
+
readonly url: string;
|
|
11
|
+
/** Lower-case hex sha256 of the file's bytes: the cache key and the integrity check. */
|
|
12
|
+
readonly sha256: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface TextureSource {
|
|
16
|
+
readonly source: 'polyhaven' | 'ambientcg' | 'file';
|
|
17
|
+
/** The set's id at the source, e.g. 'wood_planks_grey'. */
|
|
18
|
+
readonly id: string;
|
|
19
|
+
/** Only public-domain sets are accepted, so a generated asset never inherits a licence duty. */
|
|
20
|
+
readonly license: 'CC0-1.0';
|
|
21
|
+
/** Human page crediting the set (goes into the GLB's XMP credits). */
|
|
22
|
+
readonly page: string;
|
|
23
|
+
readonly author?: string;
|
|
24
|
+
/** Real-world size the image covers, metres (U × V). Poly Haven lists it as `dimensions`. */
|
|
25
|
+
readonly metres: readonly [number, number];
|
|
26
|
+
readonly maps: {
|
|
27
|
+
/** sRGB base colour. */
|
|
28
|
+
readonly albedo: TextureFile;
|
|
29
|
+
/** Tangent-space normal, OpenGL convention (+Y up in the image; Poly Haven `nor_gl`). */
|
|
30
|
+
readonly normal?: TextureFile;
|
|
31
|
+
/** Packed AO / roughness / metalness (Poly Haven `arm`, glTF ORM order). */
|
|
32
|
+
readonly arm?: TextureFile;
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** PNG bytes by sha256. */
|
|
37
|
+
export type TextureCache = ReadonlyMap<string, Uint8Array>;
|
|
38
|
+
|
|
39
|
+
const SHA = /^[0-9a-f]{64}$/;
|
|
40
|
+
const provided = new Map<string, Uint8Array>();
|
|
41
|
+
|
|
42
|
+
/** Hand verified texture bytes to the painter (e.g. from `loadTextureCache()` in scripts). */
|
|
43
|
+
export function provideTextures(cache: TextureCache): void {
|
|
44
|
+
for (const [k, v] of cache) provided.set(k, v);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function textureBytes(file: TextureFile, where: string): Uint8Array {
|
|
48
|
+
const hit = provided.get(file.sha256);
|
|
49
|
+
if (hit) return hit;
|
|
50
|
+
throw new Ts23dError('T3D_TEXTURE_MISSING', {
|
|
51
|
+
cause: `${where}: texture ${file.url} (sha256 ${file.sha256.slice(0, 12)}…) is not in the cache`,
|
|
52
|
+
fix: 'run `bun run textures <module defining the material>` once (downloads + verifies into node_modules/.cache/ts23d/textures), then provideTextures(await loadTextureCache()) before painting',
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function validateTexture(id: string, t: TextureSource): void {
|
|
57
|
+
const fail = (cause: string, fix: string): never => {
|
|
58
|
+
throw new Ts23dError('T3D_MATERIAL_INVALID', { cause: `material '${id}': ${cause}`, fix });
|
|
59
|
+
};
|
|
60
|
+
if (!['polyhaven', 'ambientcg', 'file'].includes(t.source))
|
|
61
|
+
fail(`texture.source '${t.source}'`, "use 'polyhaven', 'ambientcg' or 'file'");
|
|
62
|
+
if (t.license !== 'CC0-1.0')
|
|
63
|
+
fail(
|
|
64
|
+
`texture.license '${t.license}'`,
|
|
65
|
+
'only CC0-1.0 sets are accepted (Poly Haven and ambientCG are CC0)',
|
|
66
|
+
);
|
|
67
|
+
if (!(t.metres.length === 2 && t.metres.every((m) => Number.isFinite(m) && m > 0)))
|
|
68
|
+
fail(`texture.metres [${t.metres}]`, 'give the real size the image covers, e.g. [1.5, 1.5]');
|
|
69
|
+
for (const [map, f] of Object.entries(t.maps)) {
|
|
70
|
+
if (!f) continue;
|
|
71
|
+
if (!SHA.test(f.sha256))
|
|
72
|
+
fail(
|
|
73
|
+
`texture.maps.${map}.sha256 '${f.sha256}'`,
|
|
74
|
+
'lower-case hex sha256 of the file (sha256sum)',
|
|
75
|
+
);
|
|
76
|
+
if (!/\.png$/i.test(f.url))
|
|
77
|
+
fail(
|
|
78
|
+
`texture.maps.${map}.url '${f.url}'`,
|
|
79
|
+
'use the PNG download (8- or 16-bit); JPEG is not decoded',
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** One credit line for the GLB's XMP and the docs. */
|
|
85
|
+
export const creditOf = (t: TextureSource): string =>
|
|
86
|
+
`${t.id}${t.author ? ` by ${t.author}` : ''} (${t.source}, ${t.license}) ${t.page}`;
|
|
@@ -1,9 +1,15 @@
|
|
|
1
|
-
// The shared trim sheet: ONE
|
|
1
|
+
// The shared trim sheet: ONE 2048² atlas (albedo + normal + ORM) every asset maps into, so every
|
|
2
2
|
// asset is one material and one draw call. Strips tile along U; cells are fixed decals.
|
|
3
3
|
// Layout is data here; `paintTrimSheet` (generator.ts) paints it. V grows with world up on
|
|
4
|
-
// box/planar faces.
|
|
4
|
+
// box/planar faces. V 0–0.875 holds 28 strip rows (64 px each at 2048²): the 8 original strips,
|
|
5
|
+
// 16 physically distinct materials (woods, metals, rubber, canvas, leather, plastic) and 4 slots
|
|
6
|
+
// for project materials (`defineMaterial`). V 0.875–1 holds 128 px cells: decals, then foliage.
|
|
5
7
|
|
|
6
|
-
|
|
8
|
+
import { Ts23dError } from '../core/index.ts';
|
|
9
|
+
import { customIndex } from './slots.ts';
|
|
10
|
+
|
|
11
|
+
/** The original eight strips: greyscale, tinted by vertex colour. */
|
|
12
|
+
export type ClassicStripId =
|
|
7
13
|
| 'plain'
|
|
8
14
|
| 'brick'
|
|
9
15
|
| 'stone'
|
|
@@ -12,6 +18,28 @@ export type StripId =
|
|
|
12
18
|
| 'wood'
|
|
13
19
|
| 'glass'
|
|
14
20
|
| 'concrete';
|
|
21
|
+
/** Material strips with measured colour: use a near-white vertex colour (`NATURAL`) for truth. */
|
|
22
|
+
export type MaterialStripId =
|
|
23
|
+
| 'oak'
|
|
24
|
+
| 'pine'
|
|
25
|
+
| 'wood_weathered'
|
|
26
|
+
| 'wood_painted'
|
|
27
|
+
| 'iron_rust'
|
|
28
|
+
| 'iron_cast'
|
|
29
|
+
| 'steel_painted'
|
|
30
|
+
| 'steel_galvanised'
|
|
31
|
+
| 'brass'
|
|
32
|
+
| 'copper'
|
|
33
|
+
| 'aluminium'
|
|
34
|
+
| 'chrome'
|
|
35
|
+
| 'rubber'
|
|
36
|
+
| 'canvas'
|
|
37
|
+
| 'leather'
|
|
38
|
+
| 'plastic';
|
|
39
|
+
export type BuiltinStripId = ClassicStripId | MaterialStripId;
|
|
40
|
+
/** A project material's id, as returned by `defineMaterial` (a branded string). */
|
|
41
|
+
export type CustomStripId = string & { readonly __ts23dMaterial: true };
|
|
42
|
+
export type StripId = BuiltinStripId | CustomStripId;
|
|
15
43
|
export type DecalCellId = 'window_sash' | 'door_panel' | 'clock' | 'smokebox' | 'sign' | 'porthole';
|
|
16
44
|
/** Cutout leaf cards: the anchor (twig base) is the cell's bottom-centre, growth toward +V. */
|
|
17
45
|
export type FoliageCellId =
|
|
@@ -37,8 +65,10 @@ export interface UvRect {
|
|
|
37
65
|
readonly metres: number;
|
|
38
66
|
}
|
|
39
67
|
|
|
40
|
-
|
|
41
|
-
const
|
|
68
|
+
/** Strip rows in the atlas: built-ins first, then `CUSTOM_STRIP_SLOTS` project materials. */
|
|
69
|
+
export const STRIP_ROWS = 28;
|
|
70
|
+
const STRIP_H = 1 / 32;
|
|
71
|
+
const classic: readonly ClassicStripId[] = [
|
|
42
72
|
'plain',
|
|
43
73
|
'brick',
|
|
44
74
|
'stone',
|
|
@@ -48,6 +78,26 @@ const strips: readonly StripId[] = [
|
|
|
48
78
|
'glass',
|
|
49
79
|
'concrete',
|
|
50
80
|
];
|
|
81
|
+
const material: readonly MaterialStripId[] = [
|
|
82
|
+
'oak',
|
|
83
|
+
'pine',
|
|
84
|
+
'wood_weathered',
|
|
85
|
+
'wood_painted',
|
|
86
|
+
'iron_rust',
|
|
87
|
+
'iron_cast',
|
|
88
|
+
'steel_painted',
|
|
89
|
+
'steel_galvanised',
|
|
90
|
+
'brass',
|
|
91
|
+
'copper',
|
|
92
|
+
'aluminium',
|
|
93
|
+
'chrome',
|
|
94
|
+
'rubber',
|
|
95
|
+
'canvas',
|
|
96
|
+
'leather',
|
|
97
|
+
'plastic',
|
|
98
|
+
];
|
|
99
|
+
const strips: readonly BuiltinStripId[] = [...classic, ...material];
|
|
100
|
+
export const CUSTOM_STRIP_SLOTS = STRIP_ROWS - strips.length;
|
|
51
101
|
const decals: readonly DecalCellId[] = [
|
|
52
102
|
'window_sash',
|
|
53
103
|
'door_panel',
|
|
@@ -70,30 +120,46 @@ const foliage: readonly FoliageCellId[] = [
|
|
|
70
120
|
'foliage_needles',
|
|
71
121
|
];
|
|
72
122
|
const cells: readonly CellId[] = [...decals, ...foliage];
|
|
73
|
-
/**
|
|
123
|
+
/** Cells are 1/16 of the sheet, 16 per row from V 0.875: decals at 0–5, foliage from slot 8. */
|
|
124
|
+
const CELL = 1 / 16;
|
|
125
|
+
const CELL_V0 = STRIP_ROWS * STRIP_H;
|
|
74
126
|
const FOLIAGE_SLOT = 8;
|
|
75
127
|
const slotOf = (id: CellId): number => {
|
|
76
128
|
const d = decals.indexOf(id as DecalCellId);
|
|
77
129
|
return d >= 0 ? d : FOLIAGE_SLOT + foliage.indexOf(id as FoliageCellId);
|
|
78
130
|
};
|
|
79
131
|
|
|
80
|
-
export const TRIM_SHEET_SIZE =
|
|
132
|
+
export const TRIM_SHEET_SIZE = 2048;
|
|
133
|
+
|
|
134
|
+
/** Atlas row of a strip id, or −1 when it is neither built in nor defined. */
|
|
135
|
+
export function stripRow(id: string): number {
|
|
136
|
+
const b = strips.indexOf(id as BuiltinStripId);
|
|
137
|
+
if (b >= 0) return b;
|
|
138
|
+
const c = customIndex(id);
|
|
139
|
+
return c >= 0 ? strips.length + c : -1;
|
|
140
|
+
}
|
|
81
141
|
|
|
82
142
|
export function stripRect(id: StripId): UvRect {
|
|
83
|
-
const i =
|
|
143
|
+
const i = stripRow(id);
|
|
144
|
+
if (i < 0)
|
|
145
|
+
throw new Ts23dError('T3D_MATERIAL_UNKNOWN', {
|
|
146
|
+
cause: `strip '${id}' is neither built in nor defined`,
|
|
147
|
+
fix: `use a built-in strip (${strips.join(', ')}) or register it first with defineMaterial({ id: '${id}', ... })`,
|
|
148
|
+
});
|
|
84
149
|
return { u0: 0, v0: i * STRIP_H, u1: 1, v1: (i + 1) * STRIP_H, metres: 2 };
|
|
85
150
|
}
|
|
86
151
|
|
|
87
|
-
/** Cells sit in the lower half (V ≥ 0.5), 8 per row: decals on row 0, foliage from row 1. */
|
|
88
152
|
export function cellRect(id: CellId): UvRect {
|
|
89
153
|
const i = slotOf(id);
|
|
90
|
-
const
|
|
91
|
-
const
|
|
92
|
-
|
|
93
|
-
return { u0, v0, u1: u0 + size, v1: v0 + size, metres: 1 };
|
|
154
|
+
const u0 = (i % 16) * CELL;
|
|
155
|
+
const v0 = CELL_V0 + Math.floor(i / 16) * CELL;
|
|
156
|
+
return { u0, v0, u1: u0 + CELL, v1: v0 + CELL, metres: 1 };
|
|
94
157
|
}
|
|
95
158
|
|
|
159
|
+
/** Every built-in strip, classic first. */
|
|
96
160
|
export const STRIPS = strips;
|
|
161
|
+
export const CLASSIC_STRIPS = classic;
|
|
162
|
+
export const MATERIAL_STRIPS = material;
|
|
97
163
|
export const CELLS = cells;
|
|
98
164
|
export const DECAL_CELLS = decals;
|
|
99
165
|
export const FOLIAGE_CELLS = foliage;
|
package/src/render/beauty.ts
CHANGED
|
@@ -29,7 +29,7 @@ export interface BeautyOptions {
|
|
|
29
29
|
readonly fovDeg?: number;
|
|
30
30
|
/** Samples per pixel edge (1–4). Default 2 (4 samples per pixel). */
|
|
31
31
|
readonly supersample?: number;
|
|
32
|
-
/** The painted trim sheet; default paints `DEFAULT_SHEET` at
|
|
32
|
+
/** The painted trim sheet; default paints `DEFAULT_SHEET` at 1024² once per process. */
|
|
33
33
|
readonly sheet?: PaintedSheet;
|
|
34
34
|
/** Zoom into the default framing: window centre (0–1, 0–1) of the full shot and a zoom ≥ 1. */
|
|
35
35
|
readonly window?: { readonly x: number; readonly y: number; readonly zoom: number };
|
|
@@ -51,7 +51,7 @@ export interface BeautyRender {
|
|
|
51
51
|
let defaultSheet: PaintedSheet | undefined;
|
|
52
52
|
const sheetOf = (o: BeautyOptions): PaintedSheet => {
|
|
53
53
|
if (o.sheet) return o.sheet;
|
|
54
|
-
defaultSheet ??= paintTrimSheet({ ...DEFAULT_SHEET, size:
|
|
54
|
+
defaultSheet ??= paintTrimSheet({ ...DEFAULT_SHEET, size: 1024 });
|
|
55
55
|
return defaultSheet;
|
|
56
56
|
};
|
|
57
57
|
|
package/src/render/lighting.ts
CHANGED
|
@@ -48,6 +48,32 @@ export function studioRig(cameraAzDeg: number): Rig {
|
|
|
48
48
|
};
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
+
const HORIZON: V3 = [0.82, 0.8, 0.76];
|
|
52
|
+
const ZENITH: V3 = [0.3, 0.34, 0.44];
|
|
53
|
+
const FLOOR: V3 = [0.3, 0.28, 0.26];
|
|
54
|
+
const BAND = 0.12;
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* The studio environment a reflection sees, channel `k`, along a reflection vector with up
|
|
58
|
+
* component `rz`: a bright cyclorama horizon fading to a cooler zenith, a dark band where the
|
|
59
|
+
* floor meets the wall, the lit floor below. Roughness pre-blurs it toward its mean, so polished
|
|
60
|
+
* metal shows the horizon line and rough metal a soft gradient — what makes metal read as metal.
|
|
61
|
+
*/
|
|
62
|
+
export function envRadiance(rz: number, rough: number, k: number): number {
|
|
63
|
+
const h = HORIZON[k] ?? 0;
|
|
64
|
+
const sharp =
|
|
65
|
+
rz >= 0
|
|
66
|
+
? h + ((ZENITH[k] ?? 0) - h) * Math.sqrt(rz)
|
|
67
|
+
: rz > -0.3
|
|
68
|
+
? h * BAND + ((FLOOR[k] ?? 0) - h * BAND) * Math.min(1, -rz / 0.3) ** 0.5
|
|
69
|
+
: (FLOOR[k] ?? 0);
|
|
70
|
+
const blurred = 0.5 * h + 0.25 * (ZENITH[k] ?? 0) + 0.25 * (FLOOR[k] ?? 0);
|
|
71
|
+
// Above the horizon the blur keeps a little of the up/down split so rough metal still shades.
|
|
72
|
+
const tilt = 1 + 0.25 * Math.max(-1, Math.min(1, rz * 2));
|
|
73
|
+
const t = Math.min(1, rough * 1.6);
|
|
74
|
+
return sharp + (blurred * tilt - sharp) * t;
|
|
75
|
+
}
|
|
76
|
+
|
|
51
77
|
/** Poisson-ish disc of 12 taps (texel offsets), radius ~1.6, rotated four ways for dithering. */
|
|
52
78
|
const DISC: readonly (readonly [number, number])[] = [
|
|
53
79
|
[-0.94, -0.4],
|
package/src/render/shade.ts
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
// Per-pixel shading of the visibility buffer: reconstruct position, normal, UV and vertex colour
|
|
2
2
|
// from (triangle, barycentrics), perturb by the trim sheet's normal map, then light it — key sun
|
|
3
3
|
// with a PCF shadow, sky/bounce hemisphere × multi-direction AO × the sheet's baked occlusion,
|
|
4
|
-
//
|
|
4
|
+
// a roughness-driven sun specular (normalised Blinn–Phong + Schlick Fresnel) and a studio
|
|
5
|
+
// environment reflection (envRadiance) per texel from the sheet's ORM, so metal reads as metal.
|
|
5
6
|
|
|
6
7
|
import type { PerspectiveCamera } from './camera.ts';
|
|
7
8
|
import { type DepthMap, openness, visibility } from './depth-map.ts';
|
|
8
|
-
import type
|
|
9
|
+
import { envRadiance, type Rig } from './lighting.ts';
|
|
9
10
|
import type { Scene } from './scene.ts';
|
|
10
11
|
import { type SheetTextures, sample } from './texture.ts';
|
|
11
12
|
|
|
@@ -184,7 +185,10 @@ export function shadeSample(
|
|
|
184
185
|
const shin = Math.min(2048, 2 / (rough * rough * rough * rough) - 2);
|
|
185
186
|
const lobe = ((shin + 8) / 25.13) * ndh ** shin;
|
|
186
187
|
const fr = (1 - vdh) ** 5;
|
|
187
|
-
const
|
|
188
|
+
const fv = (1 - ndv) ** 5;
|
|
189
|
+
// Image-based term: the studio seen along the reflection, occluded like the sky (+ cavity AO).
|
|
190
|
+
const rz = 2 * ndv * n2 - vz;
|
|
191
|
+
const specOcc = open * (0.4 + 0.6 * occ);
|
|
188
192
|
const ch = [ar, ag, ab];
|
|
189
193
|
for (let k = 0; k < 3; k++) {
|
|
190
194
|
const al = ch[k] ?? 0;
|
|
@@ -192,8 +196,9 @@ export function shadeSample(
|
|
|
192
196
|
const sky = (r.bounce[k] ?? 0) * (1 - hemi) + (r.sky[k] ?? 0) * hemi;
|
|
193
197
|
const diffuse = al * dk * ((r.sunColor[k] ?? 0) * lit + sky * amb);
|
|
194
198
|
const spec = (f0 + (1 - f0) * fr) * lobe * lit * (r.sunColor[k] ?? 0) * 0.8;
|
|
195
|
-
const env =
|
|
196
|
-
(f0 + (1 - f0) *
|
|
199
|
+
const env = glass
|
|
200
|
+
? (f0 + (1 - f0) * fv * (1 - rough) * (1 - rough)) * (r.sky[k] ?? 0) * open * 2.5
|
|
201
|
+
: (f0 + (Math.max(1 - rough, f0) - f0) * fv) * envRadiance(rz, rough, k) * specOcc;
|
|
197
202
|
out[k] = (diffuse + spec + env) * r.exposure;
|
|
198
203
|
}
|
|
199
204
|
}
|