partforge 0.121.0 → 0.122.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/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/arc-math.js +27 -0
- package/src/framework/geometry/contour-corners.js +130 -0
- package/src/framework/geometry/contour-ops.js +11 -125
- package/src/framework/geometry/paper-bridge.js +4 -21
- 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 +195 -0
- package/src/framework/materials/physical.js +82 -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 +23 -0
- package/src/framework/mount.js +121 -4
- package/src/framework/oracle/shape-probe.js +2 -2
- 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
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
// package-global project — another consumer in the same worker may import paper too.
|
|
4
4
|
import paper from "paper/dist/paper-core.js";
|
|
5
5
|
import { tessellateContour, reverseContour, closeContourGap } from "./profile.js";
|
|
6
|
+
import { arcCenterAndSweep } from "./arc-math.js";
|
|
6
7
|
|
|
7
8
|
const ORIENT_SEGS = 8; // points/segment for the local orientation sampler below
|
|
8
9
|
|
|
@@ -12,27 +13,9 @@ function paperScope() {
|
|
|
12
13
|
return _scope;
|
|
13
14
|
}
|
|
14
15
|
|
|
15
|
-
//
|
|
16
|
-
//
|
|
17
|
-
|
|
18
|
-
// below and contour-ops.js's jointTangents (arc tangents are ⊥ radius, oriented by dA's sign).
|
|
19
|
-
export function arcCenterAndSweep(p0, via, to) {
|
|
20
|
-
const [ax, ay] = p0, [bx, by] = via, [cx, cy] = to;
|
|
21
|
-
const d = 2 * (ax * (by - cy) + bx * (cy - ay) + cx * (ay - by));
|
|
22
|
-
if (Math.abs(d) < 1e-12) return null;
|
|
23
|
-
const sa = ax*ax + ay*ay, sb = bx*bx + by*by, sc = cx*cx + cy*cy;
|
|
24
|
-
const ux = (sa * (by - cy) + sb * (cy - ay) + sc * (ay - by)) / d;
|
|
25
|
-
const uy = (sa * (cx - bx) + sb * (ax - cx) + sc * (bx - ax)) / d;
|
|
26
|
-
const r = Math.hypot(ax - ux, ay - uy);
|
|
27
|
-
const a0 = Math.atan2(ay - uy, ax - ux);
|
|
28
|
-
const av = Math.atan2(by - uy, bx - ux);
|
|
29
|
-
const a1 = Math.atan2(cy - uy, cx - ux);
|
|
30
|
-
const twoPi = 2 * Math.PI;
|
|
31
|
-
const ccw = (x) => { let v = x % twoPi; if (v < 0) v += twoPi; return v; };
|
|
32
|
-
const dCCW = ccw(a1 - a0), vCCW = ccw(av - a0);
|
|
33
|
-
const dA = vCCW <= dCCW ? dCCW : dCCW - twoPi;
|
|
34
|
-
return { center: [ux, uy], r, dA };
|
|
35
|
-
}
|
|
16
|
+
// arcCenterAndSweep now lives in arc-math.js (paper-free), re-exported here so every
|
|
17
|
+
// existing importer of it from this module keeps working unchanged.
|
|
18
|
+
export { arcCenterAndSweep } from "./arc-math.js";
|
|
36
19
|
|
|
37
20
|
// Circular arc through (p0, via, to) → cubic Bézier segments, ≤90° each, endpoints
|
|
38
21
|
// exact. Each piece uses the standard k = (4/3)·tan(θ/4) control-point offset.
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// Minimal 3MF writer. 3MF is an OPC package (a zip) holding an XML model; it's a
|
|
2
2
|
// mesh format like STL but supports units and multiple named objects in one file,
|
|
3
3
|
// so a multi-part view exports as a single .3mf. Built from indexed meshes.
|
|
4
|
+
// Each object may carry an optional colour (0xRRGGBB).
|
|
4
5
|
import { zipSync, strToU8 } from "fflate";
|
|
5
6
|
|
|
6
7
|
const CONTENT_TYPES =
|
|
@@ -67,16 +68,44 @@ function weld(positions, indices) {
|
|
|
67
68
|
return { verts, tris };
|
|
68
69
|
}
|
|
69
70
|
|
|
70
|
-
//
|
|
71
|
-
//
|
|
71
|
+
// Default for an uncoloured object in an export where some other object IS
|
|
72
|
+
// coloured: the viewer's own default look (presets.js "default"). Written as a
|
|
73
|
+
// literal so this module keeps its fflate-only import closure.
|
|
74
|
+
const DEFAULT_DISPLAY_COLOR = 0x9fb4cc;
|
|
75
|
+
// Base-material group id: outside the object id range (objects are 1..n).
|
|
76
|
+
const BASE_MATERIALS_ID = 1000;
|
|
77
|
+
const hexColor = (c) => `#${c.toString(16).padStart(6, "0").toUpperCase()}FF`;
|
|
78
|
+
|
|
79
|
+
// parts: [{ name, positions, indices, color? }] → ArrayBuffer of the .3mf zip.
|
|
80
|
+
// `color` (0xRRGGBB) is optional per part. When NO part carries one, the model is
|
|
81
|
+
// byte-for-byte what it was before colour support — no <basematerials>, no pid.
|
|
82
|
+
// When any does, every object points (pid/pindex) into one core-spec
|
|
83
|
+
// <basematerials> group, one <base> per distinct colour, so a slicer opens the
|
|
84
|
+
// file pre-split into coloured objects.
|
|
72
85
|
export function meshTo3MF(parts) {
|
|
86
|
+
const colored = parts.some((p) => p.color != null);
|
|
87
|
+
const palette = [];
|
|
88
|
+
const pindexOf = (c) => {
|
|
89
|
+
const color = c ?? DEFAULT_DISPLAY_COLOR;
|
|
90
|
+
let i = palette.indexOf(color);
|
|
91
|
+
if (i < 0) { i = palette.length; palette.push(color); }
|
|
92
|
+
return i;
|
|
93
|
+
};
|
|
94
|
+
const pindex = colored ? parts.map((p) => pindexOf(p.color)) : [];
|
|
95
|
+
|
|
73
96
|
const out = [
|
|
74
97
|
'<?xml version="1.0" encoding="UTF-8"?>',
|
|
75
98
|
'<model unit="millimeter" xml:lang="en-US" xmlns="http://schemas.microsoft.com/3dmanufacturing/core/2015/02">',
|
|
76
99
|
"<resources>",
|
|
77
100
|
];
|
|
101
|
+
if (colored) {
|
|
102
|
+
out.push(`<basematerials id="${BASE_MATERIALS_ID}">`);
|
|
103
|
+
palette.forEach((c, i) => out.push(`<base name="color-${i + 1}" displaycolor="${hexColor(c)}"/>`));
|
|
104
|
+
out.push("</basematerials>");
|
|
105
|
+
}
|
|
78
106
|
parts.forEach((p, i) => {
|
|
79
|
-
|
|
107
|
+
const mat = colored ? ` pid="${BASE_MATERIALS_ID}" pindex="${pindex[i]}"` : "";
|
|
108
|
+
out.push(`<object id="${i + 1}" type="model" name="${xmlEsc(p.name)}"${mat}><mesh><vertices>`);
|
|
80
109
|
const { verts: v, tris: t } = weld(p.positions, p.indices);
|
|
81
110
|
for (let k = 0; k < v.length; k += 3) out.push(`<vertex x="${v[k]}" y="${v[k + 1]}" z="${v[k + 2]}"/>`);
|
|
82
111
|
out.push("</vertices><triangles>");
|
package/src/framework/jobs.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
// the oracle and the collision check can share it without importing this async,
|
|
4
4
|
// kernel-bound module back.
|
|
5
5
|
import { meshTo3MF } from "./geometry/threemf.js";
|
|
6
|
+
import { printColor } from "./materials/resolve.js";
|
|
6
7
|
import { exportablePartNames } from "./export-select.js";
|
|
7
8
|
import { fontControlAllows, fontSourceAllowed, isNoFontSource } from "./font-source.js";
|
|
8
9
|
import { fontsFor, resolveFonts } from "./fonts.js";
|
|
@@ -449,7 +450,7 @@ export async function handle(kernel, part, msg, post, opts = {}) {
|
|
|
449
450
|
const meshes = names.map((name) => {
|
|
450
451
|
onProgress(`building ${label(name)}`);
|
|
451
452
|
const { positions, indices } = posed(name, "export", onProgress).toIndexedMesh({ quality: msg.quality ?? "print" });
|
|
452
|
-
return { name: exportName(name), positions, indices };
|
|
453
|
+
return { name: exportName(name), positions, indices, color: printColor(part.parts[name]?.display) };
|
|
453
454
|
});
|
|
454
455
|
onProgress("writing 3MF file");
|
|
455
456
|
const data = meshTo3MF(meshes);
|
|
@@ -20,8 +20,9 @@ import { FONT_RULES } from "./rules-fonts.js";
|
|
|
20
20
|
import { IMAGE_RULES } from "./rules-images.js";
|
|
21
21
|
import { SOURCE_RULES } from "./rules-source.js";
|
|
22
22
|
import { VECTOR_RULES } from "./rules-vector.js";
|
|
23
|
+
import { MATERIAL_RULES } from "./rules-materials.js";
|
|
23
24
|
|
|
24
|
-
export const RULES = [...SHAPE_RULES, ...SCHEMA_RULES, ...BUILD_RULES, ...VERIFY_RULES, ...ANIMATION_RULES, ...PLACE_RULES, ...IMPORT_RULES, ...FONT_RULES, ...IMAGE_RULES, ...SOURCE_RULES, ...VECTOR_RULES];
|
|
25
|
+
export const RULES = [...SHAPE_RULES, ...SCHEMA_RULES, ...BUILD_RULES, ...VERIFY_RULES, ...ANIMATION_RULES, ...PLACE_RULES, ...IMPORT_RULES, ...FONT_RULES, ...IMAGE_RULES, ...SOURCE_RULES, ...VECTOR_RULES, ...MATERIAL_RULES];
|
|
25
26
|
|
|
26
27
|
// A usable sources input, or null. Deliberately forgiving: lintPart's callers
|
|
27
28
|
// include hosted paths handing over user/LLM-authored trees, so a malformed
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// src/framework/lint/rules-materials.js
|
|
2
|
+
// Appearance findings. All WARNINGS by design: a misspelled material must never
|
|
3
|
+
// block geometry, and the viewer already falls back (resolve.js). Pure — imports
|
|
4
|
+
// only the three-free materials data (test/lint-purity.test.js).
|
|
5
|
+
import { warn } from "./finding.js";
|
|
6
|
+
import { PRESETS, DEFAULT_PRESET_ID } from "../materials/presets.js";
|
|
7
|
+
import { ENVIRONMENTS } from "../materials/environments.js";
|
|
8
|
+
import { resolveMaterial, resolveEnvironmentId, OVERRIDE_RANGES } from "../materials/resolve.js";
|
|
9
|
+
import { suggest } from "../geometry/op-options.js";
|
|
10
|
+
|
|
11
|
+
const listedPresets = () => Object.keys(PRESETS).filter((id) => id !== DEFAULT_PRESET_ID);
|
|
12
|
+
const subParts = (part) => Object.entries(part?.parts ?? {}).filter(([, sp]) => sp && typeof sp === "object");
|
|
13
|
+
const issuesOf = (part, kind) => subParts(part).flatMap(([name, sp]) =>
|
|
14
|
+
resolveMaterial(sp.display).issues.filter((i) => i.kind === kind).map((i) => ({ name, ...i })));
|
|
15
|
+
|
|
16
|
+
export const MATERIAL_RULES = [
|
|
17
|
+
{
|
|
18
|
+
id: "unknown-material",
|
|
19
|
+
run: ({ part }) => issuesOf(part, "unknown-material").map(({ name, value }) => {
|
|
20
|
+
const near = typeof value === "string" ? suggest(value, listedPresets()) : null;
|
|
21
|
+
return warn("unknown-material",
|
|
22
|
+
`sub-part "${name}" names material ${JSON.stringify(value)}, which is not in the library`,
|
|
23
|
+
`${near ? `Did you mean "${near}"? ` : ""}The viewer shows the default look instead. Known materials: ${listedPresets().join(", ")}.`,
|
|
24
|
+
`parts.${name}.display.material`);
|
|
25
|
+
}),
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
id: "unknown-environment",
|
|
29
|
+
run: ({ part }) => {
|
|
30
|
+
const env = part?.meta?.environment;
|
|
31
|
+
if (resolveEnvironmentId(env).known) return [];
|
|
32
|
+
return [warn("unknown-environment",
|
|
33
|
+
`meta.environment ${JSON.stringify(env)} is not a known environment`,
|
|
34
|
+
`Realistic mode uses "studio" instead. Known environments: ${Object.keys(ENVIRONMENTS).join(", ")}.`,
|
|
35
|
+
"meta.environment")];
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
id: "material-key-unknown",
|
|
40
|
+
run: ({ part }) => issuesOf(part, "material-key-unknown").map(({ name, key }) =>
|
|
41
|
+
warn("material-key-unknown",
|
|
42
|
+
`sub-part "${name}" display has unknown key "${key}"`,
|
|
43
|
+
`It is ignored. display accepts color, opacity, material and the overrides ${Object.keys(OVERRIDE_RANGES).join(", ")}; transmission, IOR, sheen and iridescence come only from a preset.`,
|
|
44
|
+
`parts.${name}.display.${key}`)),
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
id: "material-override-clamped",
|
|
48
|
+
run: ({ part }) => issuesOf(part, "material-override-clamped").map(({ name, key, value, clampedTo }) => {
|
|
49
|
+
const [lo, hi] = OVERRIDE_RANGES[key];
|
|
50
|
+
return warn("material-override-clamped",
|
|
51
|
+
`sub-part "${name}" display.${key} = ${JSON.stringify(value)} is outside ${lo}..${hi}; using ${clampedTo}`,
|
|
52
|
+
`Give ${key} a number from ${lo} to ${hi}.`,
|
|
53
|
+
`parts.${name}.display.${key}`);
|
|
54
|
+
}),
|
|
55
|
+
},
|
|
56
|
+
];
|
|
@@ -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
|
+
}
|