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
|
@@ -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
|
+
};
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
// The ONE shader-injection module (onBeforeCompile). Everything a future TSL /
|
|
2
|
+
// WebGPU port would have to rewrite lives here and nowhere else.
|
|
3
|
+
//
|
|
4
|
+
// All patterns work in OBJECT space (the sub-part's delivered mesh frame), so
|
|
5
|
+
// they move with the sub-part under animation and never slide when the camera
|
|
6
|
+
// moves. Layer lines additionally map object space through `pfPrintFrame`
|
|
7
|
+
// (print-frame.js: display→export), so they run the way the part will print.
|
|
8
|
+
import * as THREE from "three";
|
|
9
|
+
|
|
10
|
+
const VERT_DECL = "varying vec3 vPfObjPos;\nvarying vec3 vPfObjNormal;\nvarying vec3 vPfPrintUpView;\nuniform mat4 pfPrintFrame;\n";
|
|
11
|
+
// Wood's triplanar normal map builds its normal in OBJECT space; the fragment
|
|
12
|
+
// stage has no normalMatrix, so its three columns ride along as varyings.
|
|
13
|
+
const VERT_DECL_NM = "varying vec3 vPfNmX;\nvarying vec3 vPfNmY;\nvarying vec3 vPfNmZ;\n";
|
|
14
|
+
const VERT_BODY_NM = "vPfNmX = normalMatrix[0];\nvPfNmY = normalMatrix[1];\nvPfNmZ = normalMatrix[2];\n";
|
|
15
|
+
// vPfPrintUpView: the print direction (export +Z) in VIEW space — the gradient
|
|
16
|
+
// of the layer height, carried by normalMatrix like any other normal — so the
|
|
17
|
+
// layer-line normal map can tilt normals along it (identity print frame for the
|
|
18
|
+
// other patterns, which never read it).
|
|
19
|
+
const VERT_BODY = "vPfObjPos = position;\nvPfObjNormal = normal;\nvPfPrintUpView = normalMatrix * vec3(pfPrintFrame[0].z, pfPrintFrame[1].z, pfPrintFrame[2].z);\n";
|
|
20
|
+
|
|
21
|
+
const FRAG_DECL = `
|
|
22
|
+
varying vec3 vPfObjPos;
|
|
23
|
+
varying vec3 vPfObjNormal;
|
|
24
|
+
varying vec3 vPfPrintUpView;
|
|
25
|
+
uniform float pfPatternScale;
|
|
26
|
+
uniform mat4 pfPrintFrame;
|
|
27
|
+
uniform sampler2D pfPatternMap;
|
|
28
|
+
float pfHash(vec3 p) { p = fract(p * 0.3183099 + 0.1); p *= 17.0; return fract(p.x * p.y * p.z * (p.x + p.y + p.z)); }
|
|
29
|
+
float pfNoise(vec3 x) {
|
|
30
|
+
vec3 i = floor(x); vec3 f = fract(x); f = f * f * (3.0 - 2.0 * f);
|
|
31
|
+
return mix(mix(mix(pfHash(i), pfHash(i + vec3(1,0,0)), f.x), mix(pfHash(i + vec3(0,1,0)), pfHash(i + vec3(1,1,0)), f.x), f.y),
|
|
32
|
+
mix(mix(pfHash(i + vec3(0,0,1)), pfHash(i + vec3(1,0,1)), f.x), mix(pfHash(i + vec3(0,1,1)), pfHash(i + vec3(1,1,1)), f.x), f.y), f.z);
|
|
33
|
+
}
|
|
34
|
+
vec3 pfTriplanar(sampler2D map, vec3 p, vec3 n, float scale) {
|
|
35
|
+
vec3 w = pow(abs(normalize(n)), vec3(4.0)); w /= (w.x + w.y + w.z);
|
|
36
|
+
return texture2D(map, p.yz / scale).rgb * w.x + texture2D(map, p.xz / scale).rgb * w.y + texture2D(map, p.xy / scale).rgb * w.z;
|
|
37
|
+
}
|
|
38
|
+
`;
|
|
39
|
+
|
|
40
|
+
// Extra declarations only the wood kind needs: its normal and roughness maps and
|
|
41
|
+
// the normal-matrix varyings. Kept out of FRAG_DECL so the other kinds don't bind
|
|
42
|
+
// two empty samplers.
|
|
43
|
+
const FRAG_DECL_WOOD = `
|
|
44
|
+
uniform sampler2D pfNormalMap;
|
|
45
|
+
uniform sampler2D pfRoughMap;
|
|
46
|
+
uniform float pfRoughMean;
|
|
47
|
+
uniform float pfNormalStrength;
|
|
48
|
+
varying vec3 vPfNmX;
|
|
49
|
+
varying vec3 vPfNmY;
|
|
50
|
+
varying vec3 vPfNmZ;
|
|
51
|
+
vec3 pfTriplanarWeights(vec3 n) { vec3 w = pow(abs(n), vec3(4.0)); return w / (w.x + w.y + w.z); }
|
|
52
|
+
// Triplanar tangent-space normal map, whiteout-blended (Golus, "Normal Mapping
|
|
53
|
+
// for a Triplanar Shader"). Each projection's map is read as a height field over
|
|
54
|
+
// the two object axes its UVs run along, so its tangent normal (x, y) is a slope
|
|
55
|
+
// along those axes and z points along the face's own axis — the whiteout form
|
|
56
|
+
// (t.xy + n.<uv axes>, |t.z| * n.<face axis>) needs no per-side sign flips: on
|
|
57
|
+
// the back face n.<face axis> is negative and the swizzle follows the same height
|
|
58
|
+
// field, so light still catches the same grain. OpenGL-format maps (+Y up), the
|
|
59
|
+
// Poly Haven "nor_gl" convention. Returns an OBJECT-space unit normal.
|
|
60
|
+
vec3 pfTriplanarNormal(vec3 p, vec3 n, float scale) {
|
|
61
|
+
vec3 w = pfTriplanarWeights(n);
|
|
62
|
+
vec3 tx = texture2D(pfNormalMap, p.yz / scale).xyz * 2.0 - 1.0;
|
|
63
|
+
vec3 ty = texture2D(pfNormalMap, p.xz / scale).xyz * 2.0 - 1.0;
|
|
64
|
+
vec3 tz = texture2D(pfNormalMap, p.xy / scale).xyz * 2.0 - 1.0;
|
|
65
|
+
tx.xy *= pfNormalStrength; ty.xy *= pfNormalStrength; tz.xy *= pfNormalStrength;
|
|
66
|
+
tx = vec3(tx.xy + n.yz, abs(tx.z) * n.x); // uv = (y, z), face axis x
|
|
67
|
+
ty = vec3(ty.xy + n.xz, abs(ty.z) * n.y); // uv = (x, z), face axis y
|
|
68
|
+
tz = vec3(tz.xy + n.xy, abs(tz.z) * n.z); // uv = (x, y), face axis z
|
|
69
|
+
return normalize(tx.zxy * w.x + ty.xzy * w.y + tz.xyz * w.z);
|
|
70
|
+
}
|
|
71
|
+
`;
|
|
72
|
+
|
|
73
|
+
// Per-kind fragment code, injected after <roughnessmap_fragment> so it can
|
|
74
|
+
// modulate both diffuseColor (already written by <map_fragment>) and roughnessFactor.
|
|
75
|
+
const FRAG_BODY = {
|
|
76
|
+
"layer-lines": `
|
|
77
|
+
{
|
|
78
|
+
float h = (pfPrintFrame * vec4(vPfObjPos, 1.0)).z / pfPatternScale;
|
|
79
|
+
float ridge = abs(fract(h) - 0.5) * 2.0; // 0 at a layer seam, 1 mid-layer
|
|
80
|
+
float groove = smoothstep(0.0, 0.35, ridge);
|
|
81
|
+
// Anti-alias: fwidth(h) is layers per pixel. Where a layer spans fewer
|
|
82
|
+
// than ~6 px the band fades to its mean over a period (0.825: the groove
|
|
83
|
+
// ramps over 35% of it, the rest is flat), and past ~2.5 px it is gone —
|
|
84
|
+
// point-sampling finer layers than that is what aliased into moiré.
|
|
85
|
+
groove = mix(0.825, groove, 1.0 - smoothstep(0.16, 0.4, fwidth(h)));
|
|
86
|
+
diffuseColor.rgb *= mix(0.9, 1.0, groove); // the normal map (FRAG_NORMAL) carries most of the relief
|
|
87
|
+
roughnessFactor = clamp(roughnessFactor + (1.0 - groove) * 0.18, 0.0, 1.0);
|
|
88
|
+
// Filament mottling: a fine grain (~0.17 mm) over slow blotches (~2 mm),
|
|
89
|
+
// fixed to the part, so a print doesn't read as flat injection-moulded plastic.
|
|
90
|
+
float mottle = pfNoise(vPfObjPos * 6.0) * 0.6 + pfNoise(vPfObjPos * 0.51) * 0.4;
|
|
91
|
+
diffuseColor.rgb *= mix(0.893, 1.047, mottle);
|
|
92
|
+
roughnessFactor = clamp(roughnessFactor + (mottle - 0.5) * 0.084, 0.0, 1.0);
|
|
93
|
+
}`,
|
|
94
|
+
"sls-grain": `
|
|
95
|
+
{
|
|
96
|
+
float g = pfNoise(vPfObjPos / pfPatternScale);
|
|
97
|
+
diffuseColor.rgb *= mix(0.92, 1.04, g);
|
|
98
|
+
roughnessFactor = clamp(roughnessFactor + (g - 0.5) * 0.1, 0.0, 1.0);
|
|
99
|
+
}`,
|
|
100
|
+
// Wood is a full PBR set sampled triplanar in object space: the colour map
|
|
101
|
+
// (sRGB, so already linear here) multiplies diffuseColor — white unless the
|
|
102
|
+
// author tinted it, physical.js — and the roughness map varies roughnessFactor
|
|
103
|
+
// around the preset's value (divided by the map's own mean, so the preset sets
|
|
104
|
+
// the average). Mipmaps (and RepeatWrapping) do the far-distance filtering.
|
|
105
|
+
wood: `
|
|
106
|
+
{
|
|
107
|
+
vec3 pfN = normalize(vPfObjNormal);
|
|
108
|
+
diffuseColor.rgb *= pfTriplanar(pfPatternMap, vPfObjPos, pfN, pfPatternScale);
|
|
109
|
+
float r = pfTriplanar(pfRoughMap, vPfObjPos, pfN, pfPatternScale).g;
|
|
110
|
+
roughnessFactor = clamp(roughnessFactor * r / pfRoughMean, 0.02, 1.0);
|
|
111
|
+
}`,
|
|
112
|
+
// Carbon samples a luminance MASK (raw, not sRGB-decoded: physical.js) and
|
|
113
|
+
// modulates around the texture's own mean (0.171; its spread is ~0.06), so a
|
|
114
|
+
// mipmapped far swatch keeps its preset colour and the weave reads at full
|
|
115
|
+
// contrast up close.
|
|
116
|
+
carbon: `
|
|
117
|
+
{
|
|
118
|
+
vec3 t = pfTriplanar(pfPatternMap, vPfObjPos, vPfObjNormal, pfPatternScale);
|
|
119
|
+
float l = dot(t, vec3(0.2126, 0.7152, 0.0722));
|
|
120
|
+
float n = clamp((l - 0.171) / 0.06, -1.0, 1.0);
|
|
121
|
+
diffuseColor.rgb *= 1.0 + n * 0.75;
|
|
122
|
+
roughnessFactor = clamp(roughnessFactor - n * 0.2, 0.0, 1.0);
|
|
123
|
+
}`,
|
|
124
|
+
concrete: `
|
|
125
|
+
{
|
|
126
|
+
vec3 t = pfTriplanar(pfPatternMap, vPfObjPos, vPfObjNormal, pfPatternScale);
|
|
127
|
+
diffuseColor.rgb *= t * 1.4;
|
|
128
|
+
}`,
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
// Per-kind NORMAL perturbation, injected after <normal_fragment_maps> (where the
|
|
132
|
+
// view-space \`normal\` is final). Layer lines are a procedural normal map: each
|
|
133
|
+
// layer is a round bead, so across one layer the surface normal swings from
|
|
134
|
+
// facing down (bottom of the bead) through straight out to facing up, along
|
|
135
|
+
// the print direction projected onto the surface. Faces parallel to the layers
|
|
136
|
+
// (tops and bottoms) get no ridges, as on a real print. Same fwidth fade as the
|
|
137
|
+
// colour bands, so far-away layers don't alias.
|
|
138
|
+
const FRAG_NORMAL = {
|
|
139
|
+
"layer-lines": `
|
|
140
|
+
{
|
|
141
|
+
float h = (pfPrintFrame * vec4(vPfObjPos, 1.0)).z / pfPatternScale;
|
|
142
|
+
float aa = 1.0 - smoothstep(0.16, 0.4, fwidth(h));
|
|
143
|
+
float t = fract(h) * 2.0 - 1.0; // -1 at a seam, 0 bead crest, 1 next seam
|
|
144
|
+
float slope = clamp(t / sqrt(max(1.0 - t * t, 0.04)), -3.0, 3.0);
|
|
145
|
+
vec3 up = normalize(vPfPrintUpView);
|
|
146
|
+
// NOT normalised: its length is how steeply the surface cuts across the
|
|
147
|
+
// layers (1 on a vertical wall, 0 on a face parallel to them), so ridges fade
|
|
148
|
+
// out on tops and bottoms. Normalising it once made a flat top pick up a
|
|
149
|
+
// full-strength tilt in a direction set by rounding noise, flipping sign
|
|
150
|
+
// wherever the face sat on a layer boundary — a visible patch.
|
|
151
|
+
vec3 along = up - normal * dot(normal, up);
|
|
152
|
+
normal = normalize(normal + along * slope * 0.35 * aa);
|
|
153
|
+
}`,
|
|
154
|
+
// Wood replaces the view-space normal with its triplanar normal map, built in
|
|
155
|
+
// object space and carried to view space through the normal matrix. A
|
|
156
|
+
// double-sided back face is flipped the way <normal_fragment_begin> flips it.
|
|
157
|
+
wood: `
|
|
158
|
+
{
|
|
159
|
+
vec3 pfObjN = pfTriplanarNormal(vPfObjPos, normalize(vPfObjNormal), pfPatternScale);
|
|
160
|
+
normal = normalize(mat3(vPfNmX, vPfNmY, vPfNmZ) * pfObjN);
|
|
161
|
+
#ifdef DOUBLE_SIDED
|
|
162
|
+
normal *= faceDirection;
|
|
163
|
+
#endif
|
|
164
|
+
}`,
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
export function applyPattern(material, { kind, scale = 1, printFrame, texture, normalMap, roughnessMap, roughnessMean = 0.5, normalScale = 1 } = {}) {
|
|
168
|
+
if (!kind || !FRAG_BODY[kind]) return material;
|
|
169
|
+
const wood = kind === "wood";
|
|
170
|
+
const uniforms = {
|
|
171
|
+
pfPatternScale: { value: scale },
|
|
172
|
+
pfPrintFrame: { value: new THREE.Matrix4().fromArray(printFrame ?? new THREE.Matrix4().toArray()) },
|
|
173
|
+
pfPatternMap: { value: texture ?? null },
|
|
174
|
+
...(wood ? {
|
|
175
|
+
pfNormalMap: { value: normalMap ?? null },
|
|
176
|
+
pfRoughMap: { value: roughnessMap ?? null },
|
|
177
|
+
pfRoughMean: { value: roughnessMean },
|
|
178
|
+
pfNormalStrength: { value: normalScale },
|
|
179
|
+
} : {}),
|
|
180
|
+
};
|
|
181
|
+
material.userData.patternUniforms = uniforms;
|
|
182
|
+
material.onBeforeCompile = (shader) => {
|
|
183
|
+
Object.assign(shader.uniforms, uniforms);
|
|
184
|
+
shader.vertexShader = shader.vertexShader
|
|
185
|
+
.replace("#include <common>", `#include <common>\n${VERT_DECL}${wood ? VERT_DECL_NM : ""}`)
|
|
186
|
+
.replace("#include <begin_vertex>", `#include <begin_vertex>\n${VERT_BODY}${wood ? VERT_BODY_NM : ""}`);
|
|
187
|
+
shader.fragmentShader = shader.fragmentShader
|
|
188
|
+
.replace("#include <common>", `#include <common>\n${FRAG_DECL}${wood ? FRAG_DECL_WOOD : ""}`)
|
|
189
|
+
.replace("#include <roughnessmap_fragment>", `#include <roughnessmap_fragment>\n${FRAG_BODY[kind]}`)
|
|
190
|
+
.replace("#include <normal_fragment_maps>", `#include <normal_fragment_maps>\n${FRAG_NORMAL[kind] ?? ""}`);
|
|
191
|
+
};
|
|
192
|
+
material.customProgramCacheKey = () => `pf-pattern:${kind}`;
|
|
193
|
+
material.needsUpdate = true;
|
|
194
|
+
return material;
|
|
195
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
// src/framework/materials/physical.js
|
|
2
|
+
// three.js materials from resolved library data. The CAD half keeps the viewer's
|
|
3
|
+
// MeshStandardMaterial and only swaps its numbers; the realistic half (Task 8)
|
|
4
|
+
// builds MeshPhysicalMaterial. Both read resolve.js, so the two modes can never
|
|
5
|
+
// disagree about which preset a sub-part is.
|
|
6
|
+
import * as THREE from "three";
|
|
7
|
+
import { cadAppearance, resolveMaterial } from "./resolve.js";
|
|
8
|
+
import { applyPattern } from "./patterns.js";
|
|
9
|
+
import { PATTERN_TEXTURES } from "./assets.js";
|
|
10
|
+
|
|
11
|
+
const APPEARANCE_KEYS = ["color", "opacity", "material", "roughness", "metalness", "clearcoat", "clearcoatRoughness", "anisotropy", "textureScale"];
|
|
12
|
+
const hasAppearance = (display) => !!display && APPEARANCE_KEYS.some((k) => display[k] != null);
|
|
13
|
+
|
|
14
|
+
export function buildCadMaterial(display, base) {
|
|
15
|
+
if (!hasAppearance(display)) return base;
|
|
16
|
+
const a = cadAppearance(display);
|
|
17
|
+
const m = base.clone();
|
|
18
|
+
m.color = new THREE.Color(a.color);
|
|
19
|
+
m.metalness = a.metalness;
|
|
20
|
+
m.roughness = a.roughness;
|
|
21
|
+
if (a.opacity < 1) { m.transparent = true; m.opacity = a.opacity; m.depthWrite = false; }
|
|
22
|
+
return m;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Realistic-mode material. `loadTexture(fileName)` is injected (the viewer owns
|
|
26
|
+
// a caching TextureLoader), so this stays unit-testable without a network.
|
|
27
|
+
export function buildPhysicalMaterial(display, { printFrame, loadTexture } = {}) {
|
|
28
|
+
const { params, tinted } = resolveMaterial(display);
|
|
29
|
+
const set = params.textures;
|
|
30
|
+
// A textured preset's colour map carries its own colour; the preset `color` is
|
|
31
|
+
// only its average (for CAD and 3MF). So the material starts white and an
|
|
32
|
+
// explicit `color` tints the map.
|
|
33
|
+
const base = set?.color && !tinted ? 0xffffff : params.color;
|
|
34
|
+
const m = new THREE.MeshPhysicalMaterial({
|
|
35
|
+
color: new THREE.Color(base),
|
|
36
|
+
metalness: params.metalness,
|
|
37
|
+
roughness: params.roughness,
|
|
38
|
+
clearcoat: params.clearcoat,
|
|
39
|
+
clearcoatRoughness: params.clearcoatRoughness,
|
|
40
|
+
transmission: params.transmission,
|
|
41
|
+
thickness: params.thickness,
|
|
42
|
+
ior: params.ior,
|
|
43
|
+
anisotropy: params.anisotropy,
|
|
44
|
+
envMapIntensity: params.envIntensity,
|
|
45
|
+
specularIntensity: params.specularIntensity,
|
|
46
|
+
});
|
|
47
|
+
if (params.opacity < 1) { m.transparent = true; m.opacity = params.opacity; m.depthWrite = false; }
|
|
48
|
+
m.userData.pfAnisotropic = params.anisotropy > 0;
|
|
49
|
+
if (!loadTexture) {
|
|
50
|
+
applyPattern(m, { kind: params.pattern, scale: params.textureScale, printFrame });
|
|
51
|
+
return m;
|
|
52
|
+
}
|
|
53
|
+
if (set) {
|
|
54
|
+
// A full PBR set: colour decoded as sRGB, normal and roughness are data.
|
|
55
|
+
const load = (file, colorSpace) => {
|
|
56
|
+
if (!file) return undefined;
|
|
57
|
+
const t = loadTexture(file);
|
|
58
|
+
t.wrapS = t.wrapT = THREE.RepeatWrapping;
|
|
59
|
+
t.colorSpace = colorSpace;
|
|
60
|
+
return t;
|
|
61
|
+
};
|
|
62
|
+
applyPattern(m, {
|
|
63
|
+
kind: params.pattern, scale: params.textureScale, printFrame,
|
|
64
|
+
texture: load(set.color, THREE.SRGBColorSpace),
|
|
65
|
+
normalMap: load(set.normal, THREE.NoColorSpace),
|
|
66
|
+
roughnessMap: load(set.roughness, THREE.NoColorSpace),
|
|
67
|
+
roughnessMean: set.roughnessMean,
|
|
68
|
+
normalScale: set.normalScale,
|
|
69
|
+
});
|
|
70
|
+
return m;
|
|
71
|
+
}
|
|
72
|
+
const textureFile = PATTERN_TEXTURES[params.pattern];
|
|
73
|
+
const texture = textureFile ? loadTexture(textureFile) : undefined;
|
|
74
|
+
// The carbon texture is a luminance mask, read raw; decoding it as sRGB
|
|
75
|
+
// crushed its contrast to nothing. Concrete is a colour map.
|
|
76
|
+
if (texture) {
|
|
77
|
+
texture.wrapS = texture.wrapT = THREE.RepeatWrapping;
|
|
78
|
+
texture.colorSpace = params.pattern === "concrete" ? THREE.SRGBColorSpace : THREE.NoColorSpace;
|
|
79
|
+
}
|
|
80
|
+
applyPattern(m, { kind: params.pattern, scale: params.textureScale, printFrame, texture });
|
|
81
|
+
return m;
|
|
82
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
// src/framework/materials/presets.js
|
|
2
|
+
// The material library: plain data, deliberately three-free and DOM-free so lint,
|
|
3
|
+
// the worker's 3MF writer and the docs parity test can all read it (see
|
|
4
|
+
// test/lint-purity.test.js and test/worker-layering.test.js). The viewer turns a
|
|
5
|
+
// record into a three.js material in physical.js; nothing here knows about three.
|
|
6
|
+
//
|
|
7
|
+
// Metal base colours are measured reflectance (F0) from physicallybased.info,
|
|
8
|
+
// converted to sRGB. Adding a preset is one record here plus a look at the
|
|
9
|
+
// contact sheet (materials.html) in every environment.
|
|
10
|
+
|
|
11
|
+
export const DEFAULT_PRESET_ID = "default";
|
|
12
|
+
|
|
13
|
+
const P = (id, label, category, use, fields) => ({ id, label, category, use, tintable: false, ...fields });
|
|
14
|
+
|
|
15
|
+
export const PRESETS = {
|
|
16
|
+
// The viewer's look before this library existed. Not documented as a choice:
|
|
17
|
+
// it is what a sub-part with no `material` renders as.
|
|
18
|
+
default: P("default", "Default", "default", "The viewer's neutral blue-grey, used when a sub-part names no material.",
|
|
19
|
+
{ color: 0x9fb4cc, metalness: 0.25, roughness: 0.55 }),
|
|
20
|
+
|
|
21
|
+
"machined-aluminum": P("machined-aluminum", "Machined aluminium", "aluminium", "Bare CNC-milled aluminium, fine tool marks, satin sheen.",
|
|
22
|
+
{ color: 0xf5f6f6, metalness: 1, roughness: 0.32 }),
|
|
23
|
+
"brushed-aluminum": P("brushed-aluminum", "Brushed aluminium", "aluminium", "Directionally brushed aluminium panels and enclosures.",
|
|
24
|
+
{ color: 0xf5f6f6, metalness: 1, roughness: 0.38, anisotropy: 0.7 }),
|
|
25
|
+
"anodized-aluminum": P("anodized-aluminum", "Anodized aluminium", "aluminium", "Dyed anodized aluminium; tint with `color` (e.g. red, black, blue).",
|
|
26
|
+
{ color: 0xb9bcc0, metalness: 1, roughness: 0.3, clearcoat: 0.4, clearcoatRoughness: 0.25, tintable: true }),
|
|
27
|
+
"bead-blasted-aluminum": P("bead-blasted-aluminum", "Bead-blasted aluminium", "aluminium", "Matte, even-textured aluminium (laptop-shell finish).",
|
|
28
|
+
{ color: 0xe8e9ea, metalness: 1, roughness: 0.62 }),
|
|
29
|
+
|
|
30
|
+
"brushed-stainless": P("brushed-stainless", "Brushed stainless steel", "steel", "Brushed 304 stainless: kitchen, marine and fastener hardware.",
|
|
31
|
+
{ color: 0xc4c5c6, metalness: 1, roughness: 0.34, anisotropy: 0.6 }),
|
|
32
|
+
"polished-chrome": P("polished-chrome", "Polished chrome", "steel", "Mirror chrome plating; shows the environment strongly.",
|
|
33
|
+
{ color: 0xcacbcc, metalness: 1, roughness: 0.05 }),
|
|
34
|
+
"black-oxide-steel": P("black-oxide-steel", "Black-oxide steel", "steel", "Blackened steel tooling and fasteners with a slight oily sheen.",
|
|
35
|
+
{ color: 0x2c2d30, metalness: 1, roughness: 0.45 }),
|
|
36
|
+
"cast-iron": P("cast-iron", "Cast iron", "steel", "Raw sand-cast iron: dark, rough and matte.",
|
|
37
|
+
{ color: 0x6d6b69, metalness: 1, roughness: 0.78 }),
|
|
38
|
+
titanium: P("titanium", "Titanium", "steel", "Bare titanium: slightly warm grey, satin.",
|
|
39
|
+
{ color: 0xc1bab1, metalness: 1, roughness: 0.35 }),
|
|
40
|
+
|
|
41
|
+
brass: P("brass", "Brass", "warm-metal", "Yellow brass fittings and decorative hardware.",
|
|
42
|
+
{ color: 0xf8dc82, metalness: 1, roughness: 0.28 }),
|
|
43
|
+
copper: P("copper", "Copper", "warm-metal", "Bare copper: busbars, heat sinks, decorative parts.",
|
|
44
|
+
{ color: 0xfad0c0, metalness: 1, roughness: 0.3 }),
|
|
45
|
+
bronze: P("bronze", "Bronze", "warm-metal", "Cast bronze bushings and sculpture.",
|
|
46
|
+
{ color: 0xd9a86c, metalness: 1, roughness: 0.42 }),
|
|
47
|
+
|
|
48
|
+
"powder-coat": P("powder-coat", "Powder coat", "coating", "Durable textured paint over metal; tint with `color`.",
|
|
49
|
+
{ color: 0x303236, metalness: 0, roughness: 0.55, clearcoat: 0.15, clearcoatRoughness: 0.5, tintable: true }),
|
|
50
|
+
"painted-metal": P("painted-metal", "Painted metal", "coating", "Glossy enamel or automotive-style paint; tint with `color`.",
|
|
51
|
+
{ color: 0xb3261e, metalness: 0, roughness: 0.4, clearcoat: 1, clearcoatRoughness: 0.08, tintable: true }),
|
|
52
|
+
|
|
53
|
+
"pla-print": P("pla-print", "PLA print", "print", "FDM-printed PLA with visible layer lines; tint with `color`.",
|
|
54
|
+
{ color: 0xe8e4da, metalness: 0, roughness: 0.55, envIntensity: 0.6, specularIntensity: 0.5, pattern: "layer-lines", textureScale: 0.2, tintable: true }),
|
|
55
|
+
"petg-print": P("petg-print", "PETG print", "print", "FDM-printed PETG: glossier than PLA, visible layers; tint with `color`.",
|
|
56
|
+
{ color: 0x2d6cdf, metalness: 0, roughness: 0.32, envIntensity: 0.7, specularIntensity: 0.7, pattern: "layer-lines", textureScale: 0.2, tintable: true }),
|
|
57
|
+
"resin-print": P("resin-print", "Resin print", "print", "SLA/MSLA resin print: smooth, faintly waxy; tint with `color`.",
|
|
58
|
+
{ color: 0x8a8f96, metalness: 0, roughness: 0.35, clearcoat: 0.2, clearcoatRoughness: 0.3, tintable: true }),
|
|
59
|
+
"nylon-sls": P("nylon-sls", "Nylon SLS", "print", "Powder-bed nylon: matte, grainy, usually white or dyed black.",
|
|
60
|
+
{ color: 0xf0eee8, metalness: 0, roughness: 0.85, pattern: "sls-grain", textureScale: 0.15, tintable: true }),
|
|
61
|
+
|
|
62
|
+
"abs-plastic": P("abs-plastic", "ABS plastic", "plastic", "Injection-moulded ABS housings and knobs; tint with `color`.",
|
|
63
|
+
{ color: 0x1c1d20, metalness: 0, roughness: 0.42, tintable: true }),
|
|
64
|
+
"clear-acrylic": P("clear-acrylic", "Clear acrylic", "plastic", "Transparent PMMA/polycarbonate windows and covers; tint for coloured acrylic.",
|
|
65
|
+
{ color: 0xffffff, metalness: 0, roughness: 0.04, transmission: 1, thickness: 3, ior: 1.49, tintable: true }),
|
|
66
|
+
rubber: P("rubber", "Rubber", "plastic", "Matte elastomer: gaskets, feet, grips; tint with `color`.",
|
|
67
|
+
{ color: 0x1a1a1a, metalness: 0, roughness: 0.9, tintable: true }),
|
|
68
|
+
|
|
69
|
+
// Wood is a full PBR texture set (colour, normal, roughness — Poly Haven scans,
|
|
70
|
+
// assets/SOURCES.md), projected triplanar in object space (patterns.js).
|
|
71
|
+
// textureScale is one tile in mm. It is NOT the scans' stated size (1.83 m oak,
|
|
72
|
+
// 2 m walnut): a 1K map spread that wide is ~2 mm a texel, so a hand-sized part
|
|
73
|
+
// showed blurred blotches and no grain. 250/400 mm keep the grain crisp at part
|
|
74
|
+
// scale and still read as plausible figure (checked in the contact sheet). `color` is the
|
|
75
|
+
// texture's average colour: what the CAD view and a 3MF export show. In realistic
|
|
76
|
+
// mode the texture carries the colour itself; an explicit `color` tints it.
|
|
77
|
+
// `roughnessMean` is the roughness map's mean, so the preset `roughness` (or an
|
|
78
|
+
// override) sets the AVERAGE roughness and the map only varies it. `normalScale`
|
|
79
|
+
// strengthens the normal map: these scans' tangent normals barely leave +Z (a
|
|
80
|
+
// ~1% tilt), which reads as flat under an environment map.
|
|
81
|
+
oak: P("oak", "Oak", "natural", "Light oak with open grain.",
|
|
82
|
+
{ color: 0xa17e57, metalness: 0, roughness: 0.65, pattern: "wood", textureScale: 250,
|
|
83
|
+
textures: { color: "pattern-oak-color.jpg", normal: "pattern-oak-normal.jpg", roughness: "pattern-oak-rough.jpg", roughnessMean: 0.53, normalScale: 2.5 } }),
|
|
84
|
+
walnut: P("walnut", "Walnut", "natural", "Dark oiled walnut.",
|
|
85
|
+
{ color: 0x5d2a18, metalness: 0, roughness: 0.5, clearcoat: 0.3, clearcoatRoughness: 0.4, pattern: "wood", textureScale: 400,
|
|
86
|
+
textures: { color: "pattern-walnut-color.jpg", normal: "pattern-walnut-normal.jpg", roughness: "pattern-walnut-rough.jpg", roughnessMean: 0.628, normalScale: 3 } }),
|
|
87
|
+
"carbon-fiber": P("carbon-fiber", "Carbon fibre", "natural", "2x2 twill carbon fibre under clear coat.",
|
|
88
|
+
{ color: 0x1b1c1e, metalness: 0.2, roughness: 0.35, clearcoat: 1, clearcoatRoughness: 0.05, pattern: "carbon", textureScale: 48 }),
|
|
89
|
+
};
|