partforge 0.121.1 → 0.123.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/docs/AUTHORING-PARTS.md +164 -3
- package/docs/KERNEL-CONTRACT.md +10 -0
- package/package.json +3 -1
- package/src/app-materials.js +23 -0
- package/src/framework/app.css +16 -1
- package/src/framework/cutaway.js +12 -0
- package/src/framework/geometry/blend-surfaces.js +256 -0
- package/src/framework/geometry/creased-normals.js +121 -7
- package/src/framework/geometry/manifold-backend.js +56 -15
- package/src/framework/geometry/mesh-fillet.js +55 -5
- package/src/framework/geometry/threemf.js +32 -3
- package/src/framework/jobs.js +2 -1
- package/src/framework/lint/index.js +2 -1
- package/src/framework/lint/rules-materials.js +56 -0
- package/src/framework/materials/assets/SOURCES.md +90 -0
- package/src/framework/materials/assets/env-outdoor.jpg +0 -0
- package/src/framework/materials/assets/env-print-bed.jpg +0 -0
- package/src/framework/materials/assets/env-studio.jpg +0 -0
- package/src/framework/materials/assets/env-workshop.jpg +0 -0
- package/src/framework/materials/assets/ground-concrete-normal.jpg +0 -0
- package/src/framework/materials/assets/ground-concrete-rough.jpg +0 -0
- package/src/framework/materials/assets/ground-concrete.jpg +0 -0
- package/src/framework/materials/assets/ground-maple-color.jpg +0 -0
- package/src/framework/materials/assets/ground-maple-normal.jpg +0 -0
- package/src/framework/materials/assets/ground-paper-normal.jpg +0 -0
- package/src/framework/materials/assets/ground-paper-rough.jpg +0 -0
- package/src/framework/materials/assets/ground-paper.jpg +0 -0
- package/src/framework/materials/assets/ground-pei-rough.jpg +0 -0
- package/src/framework/materials/assets/ground-pei.jpg +0 -0
- package/src/framework/materials/assets/pattern-carbon.jpg +0 -0
- package/src/framework/materials/assets/pattern-oak-color.jpg +0 -0
- package/src/framework/materials/assets/pattern-oak-normal.jpg +0 -0
- package/src/framework/materials/assets/pattern-oak-rough.jpg +0 -0
- package/src/framework/materials/assets/pattern-walnut-color.jpg +0 -0
- package/src/framework/materials/assets/pattern-walnut-normal.jpg +0 -0
- package/src/framework/materials/assets/pattern-walnut-rough.jpg +0 -0
- package/src/framework/materials/assets.js +39 -0
- package/src/framework/materials/contact-shadow.js +155 -0
- package/src/framework/materials/environment.js +157 -0
- package/src/framework/materials/environments.js +82 -0
- package/src/framework/materials/patterns.js +243 -0
- package/src/framework/materials/physical.js +88 -0
- package/src/framework/materials/presets.js +89 -0
- package/src/framework/materials/print-bed.js +169 -0
- package/src/framework/materials/print-frame.js +18 -0
- package/src/framework/materials/resolve.js +117 -0
- package/src/framework/materials/tonemap-readback.js +36 -0
- package/src/framework/materials/uv.js +42 -0
- package/src/framework/mount.js +121 -4
- package/src/framework/realistic-controls.js +78 -0
- package/src/framework/selection/feature-highlight.js +3 -0
- package/src/framework/view-state.js +27 -1
- package/src/framework/viewer.js +623 -34
- package/src/materials-worker.js +3 -0
- package/src/parts/bracket.js +2 -1
- package/src/parts/hinged-box.js +3 -1
- package/src/parts/material-swatches.js +40 -0
- package/src/parts/planter.js +2 -1
- package/src/parts/propeller.js +2 -1
- package/src/parts/scott-label.js +3 -2
- package/src/parts/screw.js +2 -1
- package/types/index.d.ts +48 -3
- package/types/part.d.ts +15 -2
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# Asset sources
|
|
2
|
+
|
|
3
|
+
All source assets are CC0 (public domain dedication — no attribution required). Each
|
|
4
|
+
source page was checked for "CC0" at download time (2026-09-24). Baked outputs are
|
|
5
|
+
produced by `scripts/bake-environments.mjs`.
|
|
6
|
+
|
|
7
|
+
Environments and ground/pattern textures are keyed by the file names `assets.js`
|
|
8
|
+
resolves — see that module for the name -> URL map, and `environments.js` for which
|
|
9
|
+
file each render environment uses.
|
|
10
|
+
|
|
11
|
+
## Environments (HDRIs -> UltraHDR gainmap JPEGs)
|
|
12
|
+
|
|
13
|
+
| File | Source asset | Source URL | Fetched | Processing |
|
|
14
|
+
| --- | --- | --- | --- | --- |
|
|
15
|
+
| `env-studio.jpg` | Poly Haven HDRI "Studio Small 09" | https://polyhaven.com/a/studio_small_09 | 2026-09-24 | 2K `.hdr` -> `node scripts/bake-environments.mjs studio_small_09_2k.hdr env-studio.jpg` |
|
|
16
|
+
| `env-workshop.jpg` | Poly Haven HDRI "Industrial Workshop Foundry" | https://polyhaven.com/a/industrial_workshop_foundry | 2026-09-24 | 2K `.hdr` -> `node scripts/bake-environments.mjs industrial_workshop_foundry_2k.hdr env-workshop.jpg` |
|
|
17
|
+
| `env-print-bed.jpg` | Poly Haven HDRI "Brown Photostudio 02" | https://polyhaven.com/a/brown_photostudio_02 | 2026-09-24 | 2K `.hdr` -> `node scripts/bake-environments.mjs brown_photostudio_02_2k.hdr env-print-bed.jpg --contrast 2`. The contrast curve (luminance raised to the power 2 about the image's log-average) darkens the room and sharpens its lights, for the inside of a printer enclosure; the environment record adds a hard overhead key light and dims the backdrop further. |
|
|
18
|
+
| `env-outdoor.jpg` | Poly Haven HDRI "Kloofendal Overcast" | https://polyhaven.com/a/kloofendal_overcast | 2026-09-24 | 2K `.hdr` -> `node scripts/bake-environments.mjs kloofendal_overcast_2k.hdr env-outdoor.jpg` |
|
|
19
|
+
|
|
20
|
+
Poly Haven publishes every asset under CC0 (https://polyhaven.com/license, checked
|
|
21
|
+
2026-09-24). Licence text at fetch time also confirmed per-asset via
|
|
22
|
+
`https://api.polyhaven.com/info/<slug>` (categories/metadata only — the site-wide
|
|
23
|
+
license statement is what governs).
|
|
24
|
+
|
|
25
|
+
### Environment baking detail
|
|
26
|
+
|
|
27
|
+
`ultrahdr_app` (libultrahdr 2.0.2, `brew install libultrahdr`) only writes the newer
|
|
28
|
+
ISO 21496-1 binary-box gainmap metadata, not the legacy Adobe XMP ("hdrgm"-prefixed)
|
|
29
|
+
block. The baker therefore: (1) decodes the `.hdr` with three's own `HDRLoader` (pure
|
|
30
|
+
JS) into a raw RGBA half-float buffer, (2) hands that to `ultrahdr_app -m 0` to derive
|
|
31
|
+
the SDR rendition and gain map (`-a 4 -t 0`, sdr/gainmap quality 80, gainmap
|
|
32
|
+
downsampled 2x), (3) splits the two embedded JPEG streams `ultrahdr_app` produced back
|
|
33
|
+
apart, and (4) re-merges them with `@monogrid/gainmap-js`'s `encodeJPEGMetadata` (pure
|
|
34
|
+
JS — no WebGL, only JPEG marker writing) using the boost/offset/gamma values read back
|
|
35
|
+
off `ultrahdr_app`'s own probe of its ISO output, so both metadata conventions agree.
|
|
36
|
+
A re-probe of each committed file (`ultrahdr_app -m 1 -j <file> -P`) round-trips the
|
|
37
|
+
same boost numbers back out, confirming the legacy block is self-consistent. See
|
|
38
|
+
`scripts/bake-environments.mjs`'s header comment for the fuller account, including why
|
|
39
|
+
`@monogrid/gainmap-js`'s WebGL-based `encode()`/`findTextureMinMax()` path (the one the
|
|
40
|
+
task brief originally sketched) is impractical in plain Node — no WebGL context is
|
|
41
|
+
available, and this tree has no headless-GL shim.
|
|
42
|
+
|
|
43
|
+
Before the re-merge, the baker also strips `ultrahdr_app`'s own APP2 MPF and ISO
|
|
44
|
+
21496-1 metadata segments out of both the SDR and gain-map halves
|
|
45
|
+
(`stripAppSegments` in the baker). Without this, `encodeJPEGMetadata` prepends its
|
|
46
|
+
own freshly-computed MPF/XMP but leaves `ultrahdr_app`'s originals sitting later in
|
|
47
|
+
the same file; three's `UltraHDRLoader` walks every top-level JPEG section across the
|
|
48
|
+
whole concatenated file and keeps whichever MPF/ISO segment it sees LAST, so the
|
|
49
|
+
stale one silently won — its offsets were computed for the old, smaller file, so the
|
|
50
|
+
primary/gain-map slices it produced didn't even end on a real EOI marker. Verified
|
|
51
|
+
fixed by driving three's actual `UltraHDRLoader.parse()` (via a `happy-dom` DOMParser
|
|
52
|
+
polyfill, short-circuiting before the GPU decode step) against each committed file:
|
|
53
|
+
the sliced primary and gain-map buffers now start with SOI (`FF D8`) and end with EOI
|
|
54
|
+
(`FF D9`), and there is exactly one MPF segment and no leftover ISO box in the file.
|
|
55
|
+
|
|
56
|
+
## Ground and pattern textures
|
|
57
|
+
|
|
58
|
+
| File | Source asset | Source URL | Fetched | Processing |
|
|
59
|
+
| --- | --- | --- | --- | --- |
|
|
60
|
+
| `ground-paper.jpg` | ambientCG "Paper 001" (Color) | https://ambientcg.com/a/Paper001 | 2026-09-24 | 1K JPG -> `node scripts/bake-environments.mjs --texture Paper001_1K-JPG_Color.jpg ground-paper.jpg` |
|
|
61
|
+
| `ground-paper-normal.jpg` | ambientCG "Paper 001" (NormalGL) | https://ambientcg.com/a/Paper001 | 2026-09-24 | 2K JPG -> `node scripts/bake-environments.mjs --texture Paper001_2K-JPG_NormalGL.jpg ground-paper-normal.jpg --data --size 1024 --quality 70`. The paper's grain lives in its relief; the colour map is nearly flat. |
|
|
62
|
+
| `ground-paper-rough.jpg` | ambientCG "Paper 001" (Roughness) | https://ambientcg.com/a/Paper001 | 2026-09-24 | 2K JPG, grayscale -> `node scripts/bake-environments.mjs --texture Paper001_2K-JPG_Roughness.jpg ground-paper-rough.jpg --gray --size 1024` |
|
|
63
|
+
| `pattern-oak-color.jpg` | Poly Haven "Oak Veneer 01" (colour; API key `Diffuse`) | https://polyhaven.com/a/oak_veneer_01 — file https://dl.polyhaven.org/file/ph-assets/Textures/jpg/1k/oak_veneer_01/oak_veneer_01_diff_1k.jpg | 2026-09-24 | 1K JPG, sRGB colour -> `node scripts/bake-environments.mjs --texture oak_veneer_01_diff_1k.jpg pattern-oak-color.jpg` (q82). Scanned tile 1830 mm (`api.polyhaven.com/info/oak_veneer_01` dimensions). |
|
|
64
|
+
| `pattern-oak-normal.jpg` | Poly Haven "Oak Veneer 01" (OpenGL normal map; API key `nor_gl`) | https://polyhaven.com/a/oak_veneer_01 — file https://dl.polyhaven.org/file/ph-assets/Textures/jpg/1k/oak_veneer_01/oak_veneer_01_nor_gl_1k.jpg | 2026-09-24 | 1K JPG, OpenGL (+Y up) tangent normals, kept as data -> `node scripts/bake-environments.mjs --texture oak_veneer_01_nor_gl_1k.jpg pattern-oak-normal.jpg --data --quality 95` (4:4:4 chroma, no grayscale/tint; q95 because the map is nearly flat and the viewer amplifies it, so q90's 8x8 blocks showed). |
|
|
65
|
+
| `pattern-oak-rough.jpg` | Poly Haven "Oak Veneer 01" (roughness; API key `Rough`) | https://polyhaven.com/a/oak_veneer_01 — file https://dl.polyhaven.org/file/ph-assets/Textures/jpg/1k/oak_veneer_01/oak_veneer_01_rough_1k.jpg | 2026-09-24 | 1K JPG, grayscale data -> `node scripts/bake-environments.mjs --texture oak_veneer_01_rough_1k.jpg pattern-oak-rough.jpg --gray` (source is neutral grey; channel means unchanged). |
|
|
66
|
+
| `pattern-walnut-color.jpg` | Poly Haven "Dark Wood" (colour; API key `Diffuse`) | https://polyhaven.com/a/dark_wood — file https://dl.polyhaven.org/file/ph-assets/Textures/jpg/1k/dark_wood/dark_wood_diff_1k.jpg | 2026-09-24 | 1K JPG, sRGB colour -> `node scripts/bake-environments.mjs --texture dark_wood_diff_1k.jpg pattern-walnut-color.jpg` (q82). Scanned tile 2000 mm (`api.polyhaven.com/info/dark_wood` dimensions). |
|
|
67
|
+
| `pattern-walnut-normal.jpg` | Poly Haven "Dark Wood" (OpenGL normal map; API key `nor_gl`) | https://polyhaven.com/a/dark_wood — file https://dl.polyhaven.org/file/ph-assets/Textures/jpg/1k/dark_wood/dark_wood_nor_gl_1k.jpg | 2026-09-24 | 1K JPG, OpenGL (+Y up) tangent normals, kept as data -> `node scripts/bake-environments.mjs --texture dark_wood_nor_gl_1k.jpg pattern-walnut-normal.jpg --data --quality 95` (4:4:4 chroma, no grayscale/tint; q95 because the map is nearly flat and the viewer amplifies it, so q90's 8x8 blocks showed). |
|
|
68
|
+
| `pattern-walnut-rough.jpg` | Poly Haven "Dark Wood" (roughness; API key `Rough`) | https://polyhaven.com/a/dark_wood — file https://dl.polyhaven.org/file/ph-assets/Textures/jpg/1k/dark_wood/dark_wood_rough_1k.jpg | 2026-09-24 | 1K JPG, grayscale data -> `node scripts/bake-environments.mjs --texture dark_wood_rough_1k.jpg pattern-walnut-rough.jpg --gray` (source is neutral grey; channel means unchanged). |
|
|
69
|
+
| `ground-maple-color.jpg` | Poly Haven "Oak Veneer 01" (colour; API key `Diffuse`) | https://polyhaven.com/a/oak_veneer_01 — file https://dl.polyhaven.org/file/ph-assets/Textures/jpg/1k/oak_veneer_01/oak_veneer_01_diff_1k.jpg | 2026-09-24 | Workshop floor, "unfinished maple": `node scripts/bake-environments.mjs --texture oak_veneer_01_diff_1k.jpg ground-maple-color.jpg --modulate 1.5,0.7 --quality 85` (lightened 1.5x, saturation 0.7). |
|
|
70
|
+
| `ground-maple-normal.jpg` | Poly Haven "Oak Veneer 01" (OpenGL normal map; API key `nor_gl`) | https://polyhaven.com/a/oak_veneer_01 — file https://dl.polyhaven.org/file/ph-assets/Textures/jpg/1k/oak_veneer_01/oak_veneer_01_nor_gl_1k.jpg | 2026-09-24 | Workshop floor relief: `node scripts/bake-environments.mjs --texture oak_veneer_01_nor_gl_1k.jpg ground-maple-normal.jpg --data --quality 92`. A separate file from `pattern-oak-normal.jpg` so the floor and the oak material never share (and dispose) one texture. |
|
|
71
|
+
| `ground-pei.jpg` | None — procedural | n/a | n/a | Gold-brown fine-grain speckle, no external source. First attempt tinted ambientCG "Plastic 010"'s colour map (near-uniform, stdev < 1/channel — visibly flat, no grain, when actually viewed) gold-brown; that colour map carries no usable grain at all (its roughness map does, which is why `ground-pei-rough.jpg` below is still Plastic010's roughness). Checked ambientCG alternatives (Metal049A — also flat; Granite006A — real speckle but reads as rock, not powder-coat) and found nothing closer. Generated in-baker instead, same procedural precedent as `pattern-carbon.jpg`: per-pixel triangular-distributed noise around a `196,154,82` gold-brown base (asymmetric per channel so flecks warm rather than grey), then a 0.5px blur to turn single-pixel noise into soft grains -> `node scripts/bake-environments.mjs --speckle ground-pei.jpg --tint 196,154,82`. |
|
|
72
|
+
| `ground-pei-rough.jpg` | ambientCG "Plastic 010" (Roughness) | https://ambientcg.com/a/Plastic010 | 2026-09-24 | 1K JPG, grayscale -> `node scripts/bake-environments.mjs --texture Plastic010_1K-JPG_Roughness.jpg ground-pei-rough.jpg --gray` |
|
|
73
|
+
| `ground-concrete.jpg` | ambientCG "Concrete 047 A" (Color) | https://ambientcg.com/a/Concrete047A | 2026-09-24 | 2K JPG -> `node scripts/bake-environments.mjs --texture Concrete047A_2K-JPG_Color.jpg ground-concrete.jpg --size 1024 --modulate 1.35,0 --flatten 0.7 --quality 82` (grey, brightened to the old map's mean so the concrete material preset keeps its tone; `--flatten 0.7` is 30% less grain contrast). Replaced ambientCG "Concrete 034", whose colour map was nearly flat (stdev ~5/255) and read as blur up close; 047 A's exposed aggregate keeps fine grain. |
|
|
74
|
+
| `ground-concrete-rough.jpg` | ambientCG "Concrete 047 A" (Roughness) | https://ambientcg.com/a/Concrete047A | 2026-09-24 | 2K JPG, grayscale -> `node scripts/bake-environments.mjs --texture Concrete047A_2K-JPG_Roughness.jpg ground-concrete-rough.jpg --gray --size 1024` |
|
|
75
|
+
| `ground-concrete-normal.jpg` | ambientCG "Concrete 047 A" (NormalGL) | https://ambientcg.com/a/Concrete047A | 2026-09-24 | 2K JPG -> `node scripts/bake-environments.mjs --texture Concrete047A_2K-JPG_NormalGL.jpg ground-concrete-normal.jpg --data --size 1024 --quality 85` |
|
|
76
|
+
| `pattern-carbon.jpg` | None — procedural | n/a | n/a | ambientCG has no CC0 carbon-fiber weave (searched "carbon"; only unrelated fabric hits). Generated in-baker: a 256x256 2x2 twill tile (32 threads/tile, alternating dark/light grey diagonal bands) -> `node scripts/bake-environments.mjs --carbon pattern-carbon.jpg`. No external source/licence to record. |
|
|
77
|
+
|
|
78
|
+
ambientCG publishes every asset under CC0 (https://ambientcg.com/, "CC0 Public Domain
|
|
79
|
+
license", checked 2026-09-24). The wood sets (`pattern-oak-*`, `pattern-walnut-*`,
|
|
80
|
+
`ground-maple-*`) are Poly Haven textures, CC0 like its HDRIs above
|
|
81
|
+
(https://polyhaven.com/license, checked 2026-09-24); the exact 1K JPG URLs came
|
|
82
|
+
from `https://api.polyhaven.com/files/<slug>` (keys `Diffuse`, `nor_gl`, `Rough`).
|
|
83
|
+
The presets tile them smaller than the scanned size — see the comment on `oak` in
|
|
84
|
+
`presets.js` and on `workshop` in `environments.js`.
|
|
85
|
+
|
|
86
|
+
## Budgets
|
|
87
|
+
|
|
88
|
+
Environments stay under 1.5 MB (largest: `env-workshop.jpg` at ~398 KB); ground/pattern
|
|
89
|
+
textures stay under 400 KB (largest: `pattern-oak-rough.jpg` at ~218 KB). All committed
|
|
90
|
+
outputs are well inside both ceilings — see `test/framework/materials-assets.test.js`.
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
// src/framework/materials/assets.js
|
|
2
|
+
// The ONLY module that turns an asset file name into a URL. `new URL("./assets/x",
|
|
3
|
+
// import.meta.url)` with a LITERAL path is what makes a consuming Vite build emit
|
|
4
|
+
// the file and rewrite the URL — a computed path is invisible to the bundler and
|
|
5
|
+
// ships a 404 (the partforge/geometry incident class). So every file is listed
|
|
6
|
+
// literally, once, here.
|
|
7
|
+
const URLS = {
|
|
8
|
+
"env-studio.jpg": new URL("./assets/env-studio.jpg", import.meta.url).href,
|
|
9
|
+
"env-workshop.jpg": new URL("./assets/env-workshop.jpg", import.meta.url).href,
|
|
10
|
+
"env-print-bed.jpg": new URL("./assets/env-print-bed.jpg", import.meta.url).href,
|
|
11
|
+
"env-outdoor.jpg": new URL("./assets/env-outdoor.jpg", import.meta.url).href,
|
|
12
|
+
"ground-paper.jpg": new URL("./assets/ground-paper.jpg", import.meta.url).href,
|
|
13
|
+
"ground-paper-normal.jpg": new URL("./assets/ground-paper-normal.jpg", import.meta.url).href,
|
|
14
|
+
"ground-paper-rough.jpg": new URL("./assets/ground-paper-rough.jpg", import.meta.url).href,
|
|
15
|
+
"ground-maple-color.jpg": new URL("./assets/ground-maple-color.jpg", import.meta.url).href,
|
|
16
|
+
"ground-maple-normal.jpg": new URL("./assets/ground-maple-normal.jpg", import.meta.url).href,
|
|
17
|
+
"ground-pei.jpg": new URL("./assets/ground-pei.jpg", import.meta.url).href,
|
|
18
|
+
"ground-pei-rough.jpg": new URL("./assets/ground-pei-rough.jpg", import.meta.url).href,
|
|
19
|
+
"ground-concrete.jpg": new URL("./assets/ground-concrete.jpg", import.meta.url).href,
|
|
20
|
+
"ground-concrete-rough.jpg": new URL("./assets/ground-concrete-rough.jpg", import.meta.url).href,
|
|
21
|
+
"ground-concrete-normal.jpg": new URL("./assets/ground-concrete-normal.jpg", import.meta.url).href,
|
|
22
|
+
"pattern-oak-color.jpg": new URL("./assets/pattern-oak-color.jpg", import.meta.url).href,
|
|
23
|
+
"pattern-oak-normal.jpg": new URL("./assets/pattern-oak-normal.jpg", import.meta.url).href,
|
|
24
|
+
"pattern-oak-rough.jpg": new URL("./assets/pattern-oak-rough.jpg", import.meta.url).href,
|
|
25
|
+
"pattern-walnut-color.jpg": new URL("./assets/pattern-walnut-color.jpg", import.meta.url).href,
|
|
26
|
+
"pattern-walnut-normal.jpg": new URL("./assets/pattern-walnut-normal.jpg", import.meta.url).href,
|
|
27
|
+
"pattern-walnut-rough.jpg": new URL("./assets/pattern-walnut-rough.jpg", import.meta.url).href,
|
|
28
|
+
"pattern-carbon.jpg": new URL("./assets/pattern-carbon.jpg", import.meta.url).href,
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
// Single-mask patterns. Wood is a full PBR set named per preset (presets.js
|
|
32
|
+
// `textures`), so it has no entry here.
|
|
33
|
+
export const PATTERN_TEXTURES = { carbon: "pattern-carbon.jpg", concrete: "ground-concrete.jpg" };
|
|
34
|
+
|
|
35
|
+
export function assetUrl(name) {
|
|
36
|
+
const url = URLS[name];
|
|
37
|
+
if (!url) throw new Error(`partforge: unknown material asset "${name}"`);
|
|
38
|
+
return url;
|
|
39
|
+
}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
// src/framework/materials/contact-shadow.js
|
|
2
|
+
// A contact shadow for realistic mode (after three's webgl_shadow_contact
|
|
3
|
+
// example): render the casters' depth from BELOW into a target, blur it, and
|
|
4
|
+
// lay it on the ground. Rendered on demand — on geometry change, and at low
|
|
5
|
+
// resolution while an animation moves a sub-part — never every frame.
|
|
6
|
+
//
|
|
7
|
+
// Two layers, like a real shadow: a TIGHT one (high resolution, a millimetre
|
|
8
|
+
// or so of blur, a steep height falloff) that follows the part's own outline
|
|
9
|
+
// where it meets the ground, over a faint SOFT one (low resolution, a blur
|
|
10
|
+
// that scales with the part) that stands in for the ambient darkening around
|
|
11
|
+
// it. The tight layer alone reads as a cut-out; the soft one alone as a blob.
|
|
12
|
+
import * as THREE from "three";
|
|
13
|
+
import { HorizontalBlurShader } from "three/addons/shaders/HorizontalBlurShader.js";
|
|
14
|
+
import { VerticalBlurShader } from "three/addons/shaders/VerticalBlurShader.js";
|
|
15
|
+
|
|
16
|
+
const LAYERS = [
|
|
17
|
+
// blurMm: a fixed reach in millimetres; blurFrac: a reach as a fraction of
|
|
18
|
+
// the shadow plane (so it grows with the part). falloff: the power the
|
|
19
|
+
// height fade is raised to — higher is denser at contact, thinner above.
|
|
20
|
+
{ name: "soft", resolution: 256, darkness: 0.22, blurFrac: 0.035, falloff: 1.5 },
|
|
21
|
+
{ name: "tight", resolution: 1024, darkness: 0.42, blurMm: 1.2, falloff: 2.5 },
|
|
22
|
+
];
|
|
23
|
+
|
|
24
|
+
function depthMaterialFor(darkness, falloff) {
|
|
25
|
+
const m = new THREE.MeshDepthMaterial();
|
|
26
|
+
m.onBeforeCompile = (shader) => {
|
|
27
|
+
shader.fragmentShader = shader.fragmentShader.replace(
|
|
28
|
+
"gl_FragColor = vec4( vec3( 1.0 - fragCoordZ ), opacity );",
|
|
29
|
+
`gl_FragColor = vec4( vec3( 0.0 ), pow( 1.0 - fragCoordZ, ${falloff.toFixed(2)} ) * ${darkness.toFixed(3)} );`,
|
|
30
|
+
);
|
|
31
|
+
};
|
|
32
|
+
m.customProgramCacheKey = () => `pf-contact-${darkness}-${falloff}`;
|
|
33
|
+
m.depthTest = false;
|
|
34
|
+
m.depthWrite = false;
|
|
35
|
+
return m;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function createContactShadow({ renderer, sizeMm = 400 }) {
|
|
39
|
+
const group = new THREE.Group();
|
|
40
|
+
const planeGeo = new THREE.PlaneGeometry(1, 1).rotateX(Math.PI / 2);
|
|
41
|
+
|
|
42
|
+
const layers = LAYERS.map((spec, i) => {
|
|
43
|
+
const rt = new THREE.WebGLRenderTarget(spec.resolution, spec.resolution);
|
|
44
|
+
rt.texture.generateMipmaps = false;
|
|
45
|
+
const rtBlur = new THREE.WebGLRenderTarget(spec.resolution, spec.resolution);
|
|
46
|
+
rtBlur.texture.generateMipmaps = false;
|
|
47
|
+
// polygonOffset: the ground sits a hair below this plane and writes
|
|
48
|
+
// depth; at a normal viewing distance that hair is inside the depth
|
|
49
|
+
// buffer's precision, so without the offset the shadow z-fights it.
|
|
50
|
+
const plane = new THREE.Mesh(planeGeo, new THREE.MeshBasicMaterial({
|
|
51
|
+
map: rt.texture, opacity: 1, transparent: true, depthWrite: false,
|
|
52
|
+
polygonOffset: true, polygonOffsetFactor: -1, polygonOffsetUnits: -4 - i,
|
|
53
|
+
}));
|
|
54
|
+
plane.renderOrder = 1 + i; // tight over soft
|
|
55
|
+
group.add(plane);
|
|
56
|
+
return { spec, rt, rtBlur, plane, depthMaterial: depthMaterialFor(spec.darkness, spec.falloff) };
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
const blurPlane = new THREE.Mesh(planeGeo);
|
|
60
|
+
blurPlane.visible = false;
|
|
61
|
+
group.add(blurPlane);
|
|
62
|
+
|
|
63
|
+
const cam = new THREE.OrthographicCamera(-0.5, 0.5, 0.5, -0.5, 0, 1);
|
|
64
|
+
cam.rotation.x = Math.PI / 2;
|
|
65
|
+
group.add(cam);
|
|
66
|
+
|
|
67
|
+
const hBlur = new THREE.ShaderMaterial(HorizontalBlurShader); hBlur.depthTest = false;
|
|
68
|
+
const vBlur = new THREE.ShaderMaterial(VerticalBlurShader); vBlur.depthTest = false;
|
|
69
|
+
|
|
70
|
+
// The meshes are scaled; the camera is SIZED. three leaves scale out of a
|
|
71
|
+
// camera's view matrix, so a camera sized by a scaled parent would see a
|
|
72
|
+
// 1 mm square (it did: the shadow was empty in every environment).
|
|
73
|
+
let planeSize = sizeMm;
|
|
74
|
+
function setSize(size, height = size) {
|
|
75
|
+
planeSize = size;
|
|
76
|
+
for (const l of layers) l.plane.scale.set(size, -1, size); // y = -1: rendered from below
|
|
77
|
+
blurPlane.scale.set(size, 1, size);
|
|
78
|
+
cam.left = -size / 2; cam.right = size / 2;
|
|
79
|
+
cam.top = size / 2; cam.bottom = -size / 2;
|
|
80
|
+
cam.far = height;
|
|
81
|
+
cam.updateProjectionMatrix();
|
|
82
|
+
}
|
|
83
|
+
setSize(sizeMm);
|
|
84
|
+
|
|
85
|
+
// The blur plane sits in the shadow group, i.e. in the user's scene, so it
|
|
86
|
+
// is hidden again even if a blur render throws. three's blur shaders reach
|
|
87
|
+
// four taps either side, `h` apart in UV: a reach of `mm` millimetres on a
|
|
88
|
+
// plane `planeSize` across is h = mm / planeSize / 4.
|
|
89
|
+
function blurPass(layer, mm) {
|
|
90
|
+
const step = mm / planeSize / 4;
|
|
91
|
+
blurPlane.visible = true;
|
|
92
|
+
try {
|
|
93
|
+
blurPlane.material = hBlur; hBlur.uniforms.tDiffuse.value = layer.rt.texture; hBlur.uniforms.h.value = step;
|
|
94
|
+
renderer.setRenderTarget(layer.rtBlur); renderer.render(blurPlane, cam);
|
|
95
|
+
blurPlane.material = vBlur; vBlur.uniforms.tDiffuse.value = layer.rtBlur.texture; vBlur.uniforms.v.value = step;
|
|
96
|
+
renderer.setRenderTarget(layer.rt); renderer.render(blurPlane, cam);
|
|
97
|
+
} finally {
|
|
98
|
+
blurPlane.visible = false;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// `casters` are the visible sub-part meshes; everything else in `scene` is
|
|
103
|
+
// hidden for the depth pass so only the part casts. Every mutation here —
|
|
104
|
+
// to the scene and to the renderer's own state — is captured up front and
|
|
105
|
+
// restored in `finally`, so a throw mid-render (context loss, a shader
|
|
106
|
+
// compile failure) never leaves the user's scene with hidden objects, the
|
|
107
|
+
// depth override material pinned, no background, or the wrong render
|
|
108
|
+
// target bound.
|
|
109
|
+
//
|
|
110
|
+
// While a part moves (lowRes) only the soft layer is redrawn and the tight
|
|
111
|
+
// one is hidden: an outline left where the part WAS would be wrong, and the
|
|
112
|
+
// settle render that follows brings it back.
|
|
113
|
+
function render(scene, casters, { lowRes = false } = {}) {
|
|
114
|
+
const bg = scene.background;
|
|
115
|
+
const overrideBefore = scene.overrideMaterial;
|
|
116
|
+
const clear = renderer.getClearAlpha();
|
|
117
|
+
const prevRT = renderer.getRenderTarget();
|
|
118
|
+
const hidden = [];
|
|
119
|
+
scene.traverseVisible((o) => { if (o.isMesh || o.isLine || o.isLineSegments || o.isSprite) if (!casters.includes(o)) { o.visible = false; hidden.push(o); } });
|
|
120
|
+
for (const l of layers) l.plane.visible = false;
|
|
121
|
+
try {
|
|
122
|
+
scene.background = null;
|
|
123
|
+
renderer.setClearAlpha(0);
|
|
124
|
+
for (const l of layers) {
|
|
125
|
+
if (lowRes && l.spec.name === "tight") continue;
|
|
126
|
+
scene.overrideMaterial = l.depthMaterial;
|
|
127
|
+
renderer.setRenderTarget(l.rt);
|
|
128
|
+
renderer.render(scene, cam);
|
|
129
|
+
scene.overrideMaterial = overrideBefore;
|
|
130
|
+
const reach = l.spec.blurMm ?? l.spec.blurFrac * planeSize;
|
|
131
|
+
blurPass(l, reach);
|
|
132
|
+
if (!lowRes) blurPass(l, reach * 0.5);
|
|
133
|
+
}
|
|
134
|
+
} finally {
|
|
135
|
+
scene.overrideMaterial = overrideBefore;
|
|
136
|
+
renderer.setRenderTarget(prevRT);
|
|
137
|
+
renderer.setClearAlpha(clear);
|
|
138
|
+
for (const o of hidden) o.visible = true;
|
|
139
|
+
// After the restore above: the planes are in the scene too, so they were
|
|
140
|
+
// among `hidden`, and restoring them first would undo this.
|
|
141
|
+
for (const l of layers) l.plane.visible = !(lowRes && l.spec.name === "tight");
|
|
142
|
+
scene.background = bg;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return {
|
|
147
|
+
group,
|
|
148
|
+
render,
|
|
149
|
+
setSize,
|
|
150
|
+
dispose() {
|
|
151
|
+
for (const l of layers) { l.rt.dispose(); l.rtBlur.dispose(); l.plane.material.dispose(); l.depthMaterial.dispose(); }
|
|
152
|
+
planeGeo.dispose(); hBlur.dispose(); vBlur.dispose();
|
|
153
|
+
},
|
|
154
|
+
};
|
|
155
|
+
}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
// src/framework/materials/environment.js
|
|
2
|
+
// One environment "rig": PMREM-filtered lighting from a baked UltraHDR JPEG, a
|
|
3
|
+
// backdrop, a textured ground disc that fades into the backdrop at its rim, and a
|
|
4
|
+
// contact shadow. Loaded lazily the first time an environment is shown; the
|
|
5
|
+
// loaders are injected so this is testable without GL or network.
|
|
6
|
+
import * as THREE from "three";
|
|
7
|
+
import { ENVIRONMENTS } from "./environments.js";
|
|
8
|
+
import { resolveEnvironmentId } from "./resolve.js";
|
|
9
|
+
import { assetUrl } from "./assets.js";
|
|
10
|
+
import { createContactShadow } from "./contact-shadow.js";
|
|
11
|
+
import { createPrintBed } from "./print-bed.js";
|
|
12
|
+
|
|
13
|
+
const GROUND_RADIUS_FACTOR = 4; // ground disc radius, in part radii
|
|
14
|
+
|
|
15
|
+
// A ground normal map is OpenGL-format (+Y up, Poly Haven's "nor_gl"), which is
|
|
16
|
+
// what three's tangent-space normal maps expect: a positive normalScale.
|
|
17
|
+
const GROUND_NORMAL_SCALE = 1.5;
|
|
18
|
+
|
|
19
|
+
// ground.detail: {scale, strength} re-samples the colour map `scale` times
|
|
20
|
+
// finer and multiplies it in at `strength`, so a ground seen up close under a
|
|
21
|
+
// small part keeps crisp grain after the base map's texels have gone soft.
|
|
22
|
+
// `mean` is the map's average linear luminance, so the detail layer darkens
|
|
23
|
+
// and lightens about 1 rather than shifting the ground's overall tone.
|
|
24
|
+
function injectDetail(shader, { scale, strength, mean }) {
|
|
25
|
+
shader.fragmentShader = shader.fragmentShader.replace(
|
|
26
|
+
"#include <map_fragment>",
|
|
27
|
+
`#include <map_fragment>
|
|
28
|
+
#ifdef USE_MAP
|
|
29
|
+
{
|
|
30
|
+
float pfDl = dot(texture2D(map, vMapUv * ${scale.toFixed(3)}).rgb, vec3(0.3333));
|
|
31
|
+
diffuseColor.rgb *= mix(1.0, pfDl / ${mean.toFixed(3)}, ${strength.toFixed(3)});
|
|
32
|
+
}
|
|
33
|
+
#endif`,
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function groundMaterial(tex, rough, normal, { tint, roughness = 1, normalScale = GROUND_NORMAL_SCALE, bed = false, detail }) {
|
|
38
|
+
const m = new THREE.MeshStandardMaterial({
|
|
39
|
+
color: tint, map: tex, roughnessMap: rough ?? null, roughness, metalness: 0, transparent: !bed,
|
|
40
|
+
...(normal ? { normalMap: normal, normalScale: new THREE.Vector2(normalScale, normalScale) } : {}),
|
|
41
|
+
});
|
|
42
|
+
if (bed) {
|
|
43
|
+
// a build plate has a hard edge: no rim fade
|
|
44
|
+
if (detail) {
|
|
45
|
+
m.onBeforeCompile = (shader) => injectDetail(shader, detail);
|
|
46
|
+
m.customProgramCacheKey = () => "pf-ground-bed-detail";
|
|
47
|
+
}
|
|
48
|
+
return m;
|
|
49
|
+
}
|
|
50
|
+
// Radial fade to transparent at the rim so the disc melts into the backdrop.
|
|
51
|
+
m.onBeforeCompile = (shader) => {
|
|
52
|
+
if (detail) injectDetail(shader, detail);
|
|
53
|
+
shader.vertexShader = shader.vertexShader
|
|
54
|
+
.replace("#include <common>", "#include <common>\nvarying vec2 vPfDisc;")
|
|
55
|
+
.replace("#include <begin_vertex>", "#include <begin_vertex>\nvPfDisc = position.xz;");
|
|
56
|
+
shader.fragmentShader = shader.fragmentShader
|
|
57
|
+
.replace("#include <common>", "#include <common>\nvarying vec2 vPfDisc;")
|
|
58
|
+
.replace("#include <dithering_fragment>", "#include <dithering_fragment>\ngl_FragColor.a *= 1.0 - smoothstep(0.35, 0.5, length(vPfDisc));");
|
|
59
|
+
};
|
|
60
|
+
m.customProgramCacheKey = () => (detail ? "pf-ground-detail" : "pf-ground");
|
|
61
|
+
return m;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export async function loadEnvironmentRig(renderer, requestedId, { loadHdr, loadTexture, pmrem, createCanvas }) {
|
|
65
|
+
const { id } = resolveEnvironmentId(requestedId);
|
|
66
|
+
const env = ENVIRONMENTS[id];
|
|
67
|
+
const hdr = await loadHdr(assetUrl(env.hdr));
|
|
68
|
+
hdr.mapping = THREE.EquirectangularReflectionMapping;
|
|
69
|
+
let envMap;
|
|
70
|
+
try { envMap = pmrem.fromEquirectangular(hdr).texture; } catch (e) { hdr.dispose(); throw e; }
|
|
71
|
+
|
|
72
|
+
// Every environment draws its own photo, blurred, as the backdrop — so the
|
|
73
|
+
// equirect stays alive for the rig's life (freed in dispose()).
|
|
74
|
+
const background = hdr;
|
|
75
|
+
const backgroundBlurriness = env.blurriness ?? 0.6;
|
|
76
|
+
const backgroundIntensity = env.backgroundIntensity ?? 1;
|
|
77
|
+
|
|
78
|
+
const tex = loadTexture(env.ground.texture);
|
|
79
|
+
tex.wrapS = tex.wrapT = THREE.RepeatWrapping;
|
|
80
|
+
tex.colorSpace = THREE.SRGBColorSpace;
|
|
81
|
+
// Roughness and normal maps are data, not colour: never sRGB-decoded.
|
|
82
|
+
const dataMap = (file) => {
|
|
83
|
+
if (!file) return null;
|
|
84
|
+
const t = loadTexture(file);
|
|
85
|
+
t.wrapS = t.wrapT = THREE.RepeatWrapping;
|
|
86
|
+
t.colorSpace = THREE.NoColorSpace;
|
|
87
|
+
return t;
|
|
88
|
+
};
|
|
89
|
+
const rough = dataMap(env.ground.roughnessTexture);
|
|
90
|
+
const normal = dataMap(env.ground.normalTexture);
|
|
91
|
+
const shadow = createContactShadow({ renderer, sizeMm: env.ground.sizeMm });
|
|
92
|
+
|
|
93
|
+
// env.ground.bed: a standard-size build plate instead of the fading disc.
|
|
94
|
+
if (env.ground.bed) {
|
|
95
|
+
const material = groundMaterial(tex, rough, normal, { ...env.ground, bed: true });
|
|
96
|
+
const bed = createPrintBed({ pei: material, tileMm: env.ground.tileMm ?? env.ground.sizeMm, createCanvas });
|
|
97
|
+
// An enclosure's LED bar: one hard overhead light, riding with the bed so
|
|
98
|
+
// it always falls on the part from the same place.
|
|
99
|
+
let keyLight = null;
|
|
100
|
+
if (env.keyLight) {
|
|
101
|
+
keyLight = new THREE.DirectionalLight(0xffffff, env.keyLight.intensity);
|
|
102
|
+
keyLight.position.set(...env.keyLight.direction);
|
|
103
|
+
bed.object.add(keyLight, keyLight.target);
|
|
104
|
+
}
|
|
105
|
+
return {
|
|
106
|
+
id, exposure: env.exposure, envMap, background, backgroundBlurriness, backgroundIntensity,
|
|
107
|
+
rotationY: ((env.rotationDeg ?? 0) * Math.PI) / 180,
|
|
108
|
+
ground: bed.object,
|
|
109
|
+
shadow,
|
|
110
|
+
setGround({ y, centerX = 0, centerZ = 0, radius, footprintMm = radius }) {
|
|
111
|
+
bed.place({ y, centerX, centerZ, footprintMm });
|
|
112
|
+
shadow.group.position.set(centerX, y, centerZ);
|
|
113
|
+
// The shadow plane never hangs past the plate's edge into thin air.
|
|
114
|
+
const s = Math.min(Math.max(radius * 3, 20), bed.sizeMm);
|
|
115
|
+
shadow.setSize(s, Math.max(radius * 4, 20));
|
|
116
|
+
},
|
|
117
|
+
dispose() {
|
|
118
|
+
envMap.dispose(); hdr.dispose(); tex.dispose(); rough?.dispose(); normal?.dispose();
|
|
119
|
+
material.dispose(); bed.dispose(); shadow.dispose(); keyLight?.dispose();
|
|
120
|
+
},
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const discGeo = new THREE.CircleGeometry(0.5, 96).rotateX(-Math.PI / 2);
|
|
125
|
+
const ground = new THREE.Mesh(discGeo, groundMaterial(tex, rough, normal, env.ground));
|
|
126
|
+
ground.renderOrder = -1;
|
|
127
|
+
|
|
128
|
+
function setGround({ y, centerX = 0, centerZ = 0, radius }) {
|
|
129
|
+
const r = Math.max(radius * GROUND_RADIUS_FACTOR, env.ground.sizeMm / 2);
|
|
130
|
+
ground.position.set(centerX, y - 0.01, centerZ);
|
|
131
|
+
ground.scale.setScalar(r * 2);
|
|
132
|
+
// Keep the texture at real millimetres however large the disc is.
|
|
133
|
+
const repeat = (r * 2) / (env.ground.tileMm ?? env.ground.sizeMm);
|
|
134
|
+
tex.repeat.set(repeat, repeat);
|
|
135
|
+
if (rough) rough.repeat.set(repeat, repeat);
|
|
136
|
+
if (normal) normal.repeat.set(repeat, repeat);
|
|
137
|
+
shadow.group.position.set(centerX, y, centerZ);
|
|
138
|
+
shadow.setSize(Math.max(radius * 3, 20), Math.max(radius * 4, 20));
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return {
|
|
142
|
+
id,
|
|
143
|
+
exposure: env.exposure,
|
|
144
|
+
envMap,
|
|
145
|
+
background,
|
|
146
|
+
backgroundBlurriness,
|
|
147
|
+
backgroundIntensity,
|
|
148
|
+
rotationY: ((env.rotationDeg ?? 0) * Math.PI) / 180,
|
|
149
|
+
ground,
|
|
150
|
+
shadow,
|
|
151
|
+
setGround,
|
|
152
|
+
dispose() {
|
|
153
|
+
envMap.dispose(); hdr.dispose(); tex.dispose(); rough?.dispose(); normal?.dispose();
|
|
154
|
+
discGeo.dispose(); ground.material.dispose(); shadow.dispose();
|
|
155
|
+
},
|
|
156
|
+
};
|
|
157
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
// src/framework/materials/environments.js
|
|
2
|
+
// Environment records — plain data like presets.js. `hdr` and the ground textures
|
|
3
|
+
// are FILE NAMES under ./assets/, resolved to URLs by assets.js (which is the only
|
|
4
|
+
// module that may use `new URL(..., import.meta.url)`, so this one stays importable
|
|
5
|
+
// by lint and the worker).
|
|
6
|
+
//
|
|
7
|
+
// ground.sizeMm is the smallest the ground disc ever gets (and the shadow's
|
|
8
|
+
// starting size); ground.tileMm, when set, is how many millimetres one repeat of
|
|
9
|
+
// the ground texture covers — defaulting to sizeMm. rotationDeg, when set, turns
|
|
10
|
+
// the environment (lighting and backdrop together) about the vertical axis.
|
|
11
|
+
// ground.roughnessTexture and ground.normalTexture are optional data maps
|
|
12
|
+
// (normal maps OpenGL-format, +Y up) tiled the same way as the colour map;
|
|
13
|
+
// ground.roughness (default 1) scales the roughness map. backgroundIntensity
|
|
14
|
+
// (default 1) dims the drawn backdrop without dimming the light on the part.
|
|
15
|
+
// ground.detail ({scale, strength, mean}) blends the colour map back in at a
|
|
16
|
+
// finer scale so the ground keeps grain up close (environment.js).
|
|
17
|
+
|
|
18
|
+
export const DEFAULT_ENVIRONMENT_ID = "studio";
|
|
19
|
+
|
|
20
|
+
export const ENVIRONMENTS = {
|
|
21
|
+
studio: {
|
|
22
|
+
id: "studio", label: "Studio", exposure: 0.8,
|
|
23
|
+
hdr: "env-studio.jpg",
|
|
24
|
+
// Paper 001's fibre relief (normal + roughness) does the work: its colour
|
|
25
|
+
// map is nearly flat, so without them the sweep read as soft blotches up
|
|
26
|
+
// close. The detail layer keeps the grain crisp under a small part.
|
|
27
|
+
ground: {
|
|
28
|
+
texture: "ground-paper.jpg", normalTexture: "ground-paper-normal.jpg", roughnessTexture: "ground-paper-rough.jpg",
|
|
29
|
+
normalScale: 0.6, sizeMm: 400, tileMm: 200, tint: 0xf2f2f2,
|
|
30
|
+
detail: { scale: 5.17, strength: 0.5, mean: 0.9 },
|
|
31
|
+
},
|
|
32
|
+
// Turns the studio photo so its two octagonal softboxes glow behind the part
|
|
33
|
+
// from the viewer's default camera, the classic product-shot backdrop.
|
|
34
|
+
rotationDeg: 300,
|
|
35
|
+
blurriness: 0.3,
|
|
36
|
+
},
|
|
37
|
+
workshop: {
|
|
38
|
+
id: "workshop", label: "Workshop", exposure: 0.88,
|
|
39
|
+
hdr: "env-workshop.jpg",
|
|
40
|
+
// Unfinished maple: Poly Haven "Oak Veneer 01" lightened and desaturated
|
|
41
|
+
// at bake time, with its normal map at a gentle strength and NO roughness
|
|
42
|
+
// map — a flat, fully matte roughness, so the floor never shows a glossy
|
|
43
|
+
// spot. Tiled at 600 mm (finer grain than the 1.83 m scan, as maple is).
|
|
44
|
+
ground: {
|
|
45
|
+
texture: "ground-maple-color.jpg", normalTexture: "ground-maple-normal.jpg",
|
|
46
|
+
sizeMm: 600, tileMm: 600, roughness: 0.92, normalScale: 0.8, tint: 0xffffff,
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
"print-bed": {
|
|
50
|
+
id: "print-bed", label: "Print bed", exposure: 0.42,
|
|
51
|
+
// The studio photo re-baked with --contrast: a darker, harsher room, like
|
|
52
|
+
// the inside of a printer's enclosure.
|
|
53
|
+
hdr: "env-print-bed.jpg",
|
|
54
|
+
blurriness: 0.4,
|
|
55
|
+
backgroundIntensity: 0.6, // the enclosure walls sit darker than the light on the part
|
|
56
|
+
// A hard overhead light from above and a little in front, like the LED bar
|
|
57
|
+
// in a printer's enclosure (direction is toward the light, from the bed).
|
|
58
|
+
keyLight: { intensity: 4.5, direction: [0.4, 1, 0.6] },
|
|
59
|
+
// bed: a cut-out, standard-size build plate (print-bed.js) with printed
|
|
60
|
+
// markings, instead of a disc fading into the backdrop. tileMm is one PEI
|
|
61
|
+
// texture repeat.
|
|
62
|
+
ground: {
|
|
63
|
+
texture: "ground-pei.jpg", roughnessTexture: "ground-pei-rough.jpg", sizeMm: 256, tileMm: 128,
|
|
64
|
+
tint: 0xffffff, bed: true,
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
outdoor: {
|
|
68
|
+
id: "outdoor", label: "Outdoor", exposure: 0.72,
|
|
69
|
+
hdr: "env-outdoor.jpg",
|
|
70
|
+
// Tinted to a warm mid-grey: the overcast sky lights an untinted concrete
|
|
71
|
+
// map near-white and blue. Exposed-aggregate concrete with its own normal
|
|
72
|
+
// map, tiled at 400 mm so the grain stays crisp (about 0.4 mm a texel)
|
|
73
|
+
// under a small part seen up close.
|
|
74
|
+
ground: {
|
|
75
|
+
texture: "ground-concrete.jpg", roughnessTexture: "ground-concrete-rough.jpg",
|
|
76
|
+
normalTexture: "ground-concrete-normal.jpg", normalScale: 0.7,
|
|
77
|
+
sizeMm: 800, tileMm: 400, tint: 0x9e9282,
|
|
78
|
+
// Micro-grain for close-ups: the same map ~7x finer, blended lightly.
|
|
79
|
+
detail: { scale: 7.13, strength: 0.245, mean: 0.5 },
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
};
|